@dsh-workbench/module-tools 1.2.7 → 1.2.8-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.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  pnpm exec dsh-wb-module init my-unit # 建骨架(唯一模板,已默认)
10
10
  pnpm build # dist/host.js + 套了 loader 壳的 client.js
11
11
  pnpm check # 十四条判据
12
- pnpm design check # 设计底稿有没有空格、README 有没有落后
12
+ pnpm design check # 设计底稿有没有空格、readme.json / README / DEVELOPING 有没有落后
13
13
  ```
14
14
 
15
15
  要发货再加一步 `pnpm exec dsh-wb-module publish`:跑门禁、核 `files` 与 `npm pack` 清单,默认 **只 dry run,不真发**。
@@ -55,7 +55,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
55
55
  | `dsh-wb-module check [--only <判据>] [--package <包名或 unit>]` | 十四条判据;`--only` 单跑,也当查询口用;`--package` 只报这个模块的问题(见下);`--list [--json\|--markdown]` 只列判据元数据 |
56
56
  | `dsh-wb-module lint [--fix]` | 用模块自己那份 `eslint.config.mjs`;`--fix` 写回文件 |
57
57
  | `dsh-wb-module sync` | 按清单生成 `src/declared-tools.ts`,按 `design.json` 的 `key + recipe` 生成 `src/datasource.ts` |
58
- | `dsh-wb-module design <init\|check\|render\|accept>` | 设计底稿:填、查空格、生成 README、验收打勾 |
58
+ | `dsh-wb-module design <init\|migrate\|check\|render\|accept>` | 设计底稿:填、拆旧 README、查空格、生成 README 与 DEVELOPING、验收打勾 |
59
59
  | `dsh-wb-module migrate [--dry-run]` | 把模块清单迁到当前代:`apiVersion` → 3、缺/空的 `workbenchFaces` 补默认;源码类问题只给提示(check 会点名) |
60
60
  | `dsh-wb-module changelog check [版本]` | 查这一版有没有记录;发布时用 `release --changelog-note` 写进去 |
61
61
  | `dsh-wb-module publish` | 发货前清单:门禁 + `files` + `npm pack`(只 dry run) |
@@ -196,10 +196,12 @@ export default [
196
196
  `dsh-wb-module lint --fix` 先写回再跑一遍看还剩什么。**`check` 永远不动手改文件** ——
197
197
  门禁是判据,不是修复器。
198
198
 
199
+ 模板生成三份文档:`readme.json` 是终端用户文案源(标题、简介、会看到什么、能做什么、agent 能做什么、数据从哪来、排障、FAQ),`README.md` 只由它生成;`DEVELOPING.md` 是作者文档(实现、路由、判据、数据源与三张表),由 `design.json` 生成。`design render` 刷新这两份;`design check` 会拦住空文案、占位符和技术词;旧模块先跑 `design migrate`,它把旧 `README.md` 原样保存为 `DEVELOPING.md`,再生成 `readme.json` 与用户 README。
200
+
199
201
  ## `design.json`:三张表
200
202
 
201
203
  面板 / 能力 / 联动三张表、状态矩阵、数据源记录、写通道记录、验收清单都住在模块根目录的
202
- `design.json`,README 里那一段由它生成(夹在 `<!-- dsh-wb:design:start/end -->` 之间)。
204
+ `design.json`,DEVELOPING.md 里那一段由它生成(夹在 `<!-- dsh-wb:design:start/end -->` 之间)。README.md 的用户文案不看 design.json,只看模块根的 `readme.json`。
203
205
 
204
206
  它是**设计底稿**:三张表与探明记录装人话(`oneLine` / `shape` / `approvalText` / 三张表的文字);
205
207
  读源那一条可以多带 `key` 与 `recipe` 两个机器字段 —— 它们只用于生成 `src/datasource.ts`,
@@ -208,7 +210,7 @@ export default [
208
210
  `design.json` 的模块不查。
209
211
 
210
212
  有写能力的模块另在 `writes[]` 里落命令 / 入参 / 出参 / 预览档 / 回读 / 失败形状,
211
- 渲染成 README 里的「写通道探明记录」。没有写通道就不用写,不会因此变红。
213
+ 渲染成 DEVELOPING.md 里的「写通道探明记录」。没有写通道就不用写,不会因此变红。
212
214
  写也能落配方:`writes[]` 里加一对 `key` + `recipe`,`sync` 把它和读一起生成进
213
215
  `DATASOURCE`(`data.submit(DATASOURCE.<key>)`);配方盖不住的写写 `custom: { why }`(warn)。
214
216
 
@@ -262,8 +264,8 @@ export default [
262
264
  - 形状与六种配方见模块 DIY 的 [`DATASOURCE.md`](../core/skills/workbench-module-diy/DATASOURCE.md)。
263
265
 
264
266
  ```bash
265
- dsh-wb-module design check # 空着的格一格格点名叫出来 + README 有没有落后
266
- dsh-wb-module design render # 用 design.json 重新生成 README 那一段
267
+ dsh-wb-module design check # 空着的格一格格点名叫出来 + README / DEVELOPING 有没有落后
268
+ dsh-wb-module design render # 用 readme.json 重新生成 README,用 design.json 刷新 DEVELOPING 那一段
267
269
  dsh-wb-module design accept # 在真卡片上逐条走通后打勾
268
270
  ```
269
271
 
@@ -325,7 +327,7 @@ dsh-wb-module design accept # 在真卡片上逐条走通后打勾
325
327
  `eslint.config.mjs`。
326
328
 
327
329
  `init` 连模板里的 `*.tpl` 一起拷、按已知 token 名逐个盖章(`__UNIT__` / `__NAME__` / `__DESC__` /
328
- `__SHELL_RANGE__` / `__DSH_UI_VERSION__`)、剥掉后缀,再用 `design.json` 生成 README 那一段。
330
+ `__SHELL_RANGE__` / `__DSH_UI_VERSION__`)、剥掉后缀,再用 `readme.json` 生成 README、用 `design.json` 生成 DEVELOPING 那一段。
329
331
  `__SHELL_RANGE__` 取 init 所在工程实装的那份外壳版本;读不到就退回 module-tools 自己的版本;
330
332
  两者都读不到才保持 `^0.1.0` 并在 stderr 上说一句。
331
333
 
@@ -336,7 +338,7 @@ node scripts/selfcheck/index.mjs
336
338
  ```
337
339
 
338
340
  工具箱的回归网:模板 `init` 一遍、跑全套;判据的正例与负例都要对;打包清单不含
339
- `.tpl` 残留;`design` 的空格、README 同步、`accept`;`publish` 的清单;`release` 的验收闸、
341
+ `.tpl` 残留;`design` 的空格、README / DEVELOPING 同步、`accept`;`publish` 的清单;`release` 的验收闸、
340
342
  dry-run、真发后的元数据与发布失败回滚。
341
343
 
342
344
  ## 相关文档
@@ -105,7 +105,7 @@ const COMMANDS = {
105
105
  check: '十四条判据;--only 单跑,也当查询口用;--list 只列判据元数据',
106
106
  init: '建模块骨架(--dir / --template diy / --description;只有 diy 一个模板)',
107
107
  'market-init': '初始化市场索引仓库(写 market/ 与 README,推首个 commit)',
108
- design: '设计底稿:init 填 / check 查空格 / render 生成 README / accept 打勾',
108
+ design: '设计底稿:init 填 / migrate 拆旧 README / check 查空格 / render 生成 README 与 DEVELOPING / accept 打勾',
109
109
  migrate: '把模块清单迁到当前代(apiVersion / workbenchFaces);源码类问题只给提示',
110
110
  publish: '发货前清单:门禁 + files + npm pack(只 dry run)',
111
111
  release: '真发布:全门禁 + 验收闸 + 预检接管 + 发到出口 + 发后核包 + 更新市场索引(中断可 --resume)',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "1.2.7",
3
+ "version": "1.2.8-beta.0",
4
4
  "description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "semver": "^7.8.5",
34
34
  "typescript": "5.9.3",
35
35
  "typescript-eslint": "^8.71.0",
36
- "@dsh-workbench/shared": "1.2.7"
36
+ "@dsh-workbench/shared": "1.2.8-beta.0"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
@@ -727,7 +727,7 @@ export async function run() {
727
727
  for (const name0 of inDeclaration.filter((one) => !inDesign.includes(one))) {
728
728
  problems.push(
729
729
  `${name}: 清单的 tools[] 里声明了「${name0}」,design.json 的能力行里却没有它 —— ` +
730
- '底稿是给人读的那半(它干嘛、要不要点头,`design render` 拿它出 README):' +
730
+ '底稿是给人读的那半(它干嘛、要不要点头,`design render` 拿它出 DEVELOPING.md):' +
731
731
  `要么在 design.json 的 capabilities[] 里为「${name0}」补一行,要么把它从清单里删掉`,
732
732
  );
733
733
  }
@@ -218,7 +218,7 @@ export async function run() {
218
218
  const text = readFileSync(file, 'utf8');
219
219
  for (const line of commentViolationsOf(text, { css: file.endsWith('.css') })) {
220
220
  problems.push(
221
- `${name}: ${rel}:${line} 有注释 —— 模块源码零注释(说明写 README.md 或 spec;eslint / ts / vitest 指令注释除外)`,
221
+ `${name}: ${rel}:${line} 有注释 —— 模块源码零注释(用户说明写 README.md,作者说明写 DEVELOPING.md 或 spec;eslint / ts / vitest 指令注释除外)`,
222
222
  );
223
223
  }
224
224
  }
package/src/design.mjs CHANGED
@@ -1,8 +1,10 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { callsProblemOf, describeRecipe, recipeProblemOf } from '@dsh-workbench/shared/host';
4
4
 
5
5
  export const DESIGN_FILE = 'design.json';
6
+ export const DEVELOPING_FILE = 'DEVELOPING.md';
7
+ export const README_SOURCE_FILE = 'readme.json';
6
8
  export const START_MARK = '<!-- dsh-wb:design:start -->';
7
9
  export const END_MARK = '<!-- dsh-wb:design:end -->';
8
10
 
@@ -430,6 +432,123 @@ export function renderDesign(design) {
430
432
  ].join('\n');
431
433
  }
432
434
 
435
+ function sectionOf(heading, body) {
436
+ const text0 = text(body);
437
+ return text0 === '' ? [] : [`## ${heading}`, '', text0, ''];
438
+ }
439
+
440
+ export function renderUserReadme(source) {
441
+ const title = text(source?.title) || '模块';
442
+ const summary = text(source?.summary);
443
+ const faq = asRows(source?.faq);
444
+ const install = text(source?.install) === ''
445
+ ? '在市场「已安装」里装或卸。装完在当前项目的面板里勾选这个模块,卡片才会出现;卸掉后卡片消失。'
446
+ : text(source?.install);
447
+ const faqLines = faq.length === 0
448
+ ? []
449
+ : [
450
+ '## 常见问题',
451
+ '',
452
+ ...faq.flatMap((one) => [`### ${text(one?.q)}`, '', text(one?.a), '']),
453
+ ];
454
+
455
+ return [
456
+ `# ${title}`,
457
+ '',
458
+ summary,
459
+ '',
460
+ ...sectionOf('你会看到什么', source?.see),
461
+ ...sectionOf('你能做什么', source?.do),
462
+ ...sectionOf('agent 能替你做什么', source?.agent),
463
+ ...sectionOf('数据从哪来', source?.data),
464
+ ...sectionOf('装和卸', install),
465
+ ...sectionOf('出问题先看', source?.trouble),
466
+ ...faqLines,
467
+ ].join('\n');
468
+ }
469
+
470
+ export function readUserSource(root) {
471
+ const file = join(root, README_SOURCE_FILE);
472
+ if (!existsSync(file)) return undefined;
473
+ try {
474
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
475
+ return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
476
+ } catch (error) {
477
+ console.error(`✖ ${README_SOURCE_FILE} 不是合法 JSON:${error.message}`);
478
+ process.exit(1);
479
+ }
480
+ }
481
+
482
+ export function writeUserSource(root, value) {
483
+ writeFileSync(join(root, README_SOURCE_FILE), `${JSON.stringify(value, null, 2)}\n`);
484
+ }
485
+
486
+ function manifestTitleOf(manifest) {
487
+ const title = manifest?.dsh?.workbench?.title;
488
+ if (typeof title === 'string' && title.trim() !== '') return title.trim();
489
+ if (title !== null && typeof title === 'object') {
490
+ const zh = text(title.zh);
491
+ if (zh !== '') return zh;
492
+ const en = text(title.en);
493
+ if (en !== '') return en;
494
+ }
495
+ return text(manifest?.name);
496
+ }
497
+
498
+ export function userSourceSkeleton(manifest, design) {
499
+ return {
500
+ title: manifestTitleOf(manifest) || text(design?.name) || '模块',
501
+ summary: text(design?.oneLine) || text(manifest?.description),
502
+ see: '(待填:终端用户会在卡片上看到什么;不要写组件名、图号、像素值)',
503
+ do: '(待填:终端用户自己能点什么、能改什么)',
504
+ agent: '(待填:agent 能替用户做什么;哪些动作会先弹审批)',
505
+ data: '(待填:数据从哪个系统来、用谁的登录、会不会写数据)',
506
+ install: '',
507
+ trouble: '(待填:没登录、超时、读不到时用户会看到什么、下一步点哪)',
508
+ faq: [],
509
+ };
510
+ }
511
+
512
+ const USER_COPY_FIELDS = ['title', 'summary', 'see', 'do', 'agent', 'data', 'trouble'];
513
+ const USER_COPY_FORBIDDEN = [
514
+ ['反引号 / 代码片段', /`/],
515
+ ['代码块', /```/],
516
+ ['源码路径', /\bsrc\//],
517
+ ['设计底稿文件名', /\bdesign\.json\b/],
518
+ ['文案底稿文件名', /\breadme\.json\b/],
519
+ ['工具链命令', /\bdsh-wb-module\b/],
520
+ ['运行时名', /\bopencli\b/],
521
+ ['内部错误码', /\bauth-required\b|\bcli-[a-z-]+\b/],
522
+ ['客户端实现名', /\bapproval\.render\b/],
523
+ ['平台内部字段', /\bglobal_key\b/],
524
+ ];
525
+
526
+ export function userCopyProblemsOf(source) {
527
+ const problems = [];
528
+ if (source === undefined) return [`没有 ${README_SOURCE_FILE} —— 先跑 dsh-wb-module design init 或 design migrate`];
529
+ for (const field of USER_COPY_FIELDS) {
530
+ const value = text(source?.[field]);
531
+ if (value === '') problems.push(`${field} 是空的 —— README 的「${field}」还没有用户文案`);
532
+ else if (value.includes('(待填')) problems.push(`${field} 还留着「(待填)」`);
533
+ }
534
+ for (const field of ['title', 'summary', 'see', 'do', 'agent', 'data', 'install', 'trouble']) {
535
+ const value = text(source?.[field]);
536
+ for (const [label, pattern] of USER_COPY_FORBIDDEN) {
537
+ if (pattern.test(value)) problems.push(`${field} 里有${label} —— 这段只给终端用户看`);
538
+ }
539
+ }
540
+ for (const [index, one] of asRows(source?.faq).entries()) {
541
+ if (text(one?.q) === '') problems.push(`faq[${index}].q 是空的`);
542
+ if (text(one?.a) === '') problems.push(`faq[${index}].a 是空的`);
543
+ for (const [label, pattern] of USER_COPY_FORBIDDEN) {
544
+ if (pattern.test(text(one?.q)) || pattern.test(text(one?.a))) {
545
+ problems.push(`faq[${index}] 里有${label} —— 这段只给终端用户看`);
546
+ }
547
+ }
548
+ }
549
+ return problems;
550
+ }
551
+
433
552
  function regionOf(readme) {
434
553
  const start = readme.indexOf(START_MARK);
435
554
  const end = readme.indexOf(END_MARK);
@@ -437,11 +556,11 @@ function regionOf(readme) {
437
556
  return { start, end };
438
557
  }
439
558
 
440
- export function applyToReadme(root, markdown) {
441
- const file = join(root, 'README.md');
559
+ export function applyToDeveloping(root, markdown) {
560
+ const file = join(root, DEVELOPING_FILE);
442
561
  const block = `${START_MARK}\n${markdown}\n${END_MARK}`;
443
562
  if (!existsSync(file)) {
444
- writeFileSync(file, `# ${text(manifestOf(root).name) || '模块'}\n\n${block}\n`);
563
+ writeFileSync(file, `# ${text(manifestOf(root).name) || '模块'} · 作者文档\n\n${block}\n`);
445
564
  return 'created';
446
565
  }
447
566
  const text0 = readFileSync(file, 'utf8');
@@ -464,6 +583,15 @@ export function applyToReadme(root, markdown) {
464
583
  return 'inserted';
465
584
  }
466
585
 
586
+ export function writeUserReadme(root, source) {
587
+ const file = join(root, 'README.md');
588
+ const next = renderUserReadme(source);
589
+ const before = existsSync(file) ? readFileSync(file, 'utf8') : '';
590
+ if (before === next) return 'same';
591
+ writeFileSync(file, next);
592
+ return before === '' ? 'created' : 'updated';
593
+ }
594
+
467
595
  function manifestOf(root) {
468
596
  try {
469
597
  return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
@@ -484,9 +612,13 @@ function checkDesign(root) {
484
612
  const acceptance = asRows(design.acceptance);
485
613
  const accepted = acceptance.filter((one) => one?.checked === true).length;
486
614
 
487
- const readme = existsSync(join(root, 'README.md')) ? readFileSync(join(root, 'README.md'), 'utf8') : '';
488
- const at = regionOf(readme);
489
- const fresh = at === undefined ? false : readme.slice(at.start, at.end) === `${START_MARK}\n${renderDesign(design)}\n`;
615
+ const developing = existsSync(join(root, DEVELOPING_FILE)) ? readFileSync(join(root, DEVELOPING_FILE), 'utf8') : '';
616
+ const at = regionOf(developing);
617
+ const fresh = at === undefined ? false : developing.slice(at.start, at.end) === `${START_MARK}\n${renderDesign(design)}\n`;
618
+ const source = readUserSource(root);
619
+ const copyProblems = userCopyProblemsOf(source);
620
+ const user = existsSync(join(root, 'README.md')) ? readFileSync(join(root, 'README.md'), 'utf8') : '';
621
+ const userFresh = source !== undefined && user === renderUserReadme(source);
490
622
 
491
623
  console.log(`design.json:面板 ${rows('panel')} 行 · 状态 ${STATE_KEYS.length} 态 · 配置 ${rows('config')} 项 · 能力 ${rows('capabilities')} 条 · 联动 ${rows('linkage')} 行 · 验收 ${accepted}/${acceptance.length}`);
492
624
  for (const one of warnings) console.log(`⚠ ${one}`);
@@ -494,13 +626,22 @@ function checkDesign(root) {
494
626
  console.error(`✖ 还空着 ${cells.length} 格("空着的格就是还没定的事",别带着空格往下做):`);
495
627
  for (const one of cells) console.error(` · ${one}`);
496
628
  }
497
- if (!fresh) {
498
- console.error('✖ README 里那一段落后于 design.json(或者没有标记)—— 跑 dsh-wb-module design render');
629
+ if (developing === '') {
630
+ console.error(`✖ 没有 ${DEVELOPING_FILE} —— 旧模块先跑 dsh-wb-module design migrate;新模块跑 design init`);
631
+ } else if (!fresh) {
632
+ console.error(`✖ ${DEVELOPING_FILE} 里那一段落后于 design.json(或者没有标记)—— 跑 dsh-wb-module design render`);
633
+ }
634
+ if (copyProblems.length > 0) {
635
+ console.error(`✖ ${README_SOURCE_FILE} 还不能生成终端用户 README:`);
636
+ for (const one of copyProblems) console.error(` · ${one}`);
499
637
  }
500
- if (cells.length === 0 && fresh) {
501
- console.log(`✔ 三张表填全了,README 也是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted} 条(自己在真卡片上逐条走通后跑 design accept)`}`);
638
+ if (!userFresh) {
639
+ console.error(`✖ README.md 落后于 ${README_SOURCE_FILE}(或还没生成用户说明)—— 跑 dsh-wb-module design render`);
502
640
  }
503
- process.exit(cells.length === 0 && fresh ? 0 : 1);
641
+ if (cells.length === 0 && fresh && copyProblems.length === 0 && userFresh) {
642
+ console.log(`✔ 三张表填全了,README 与 ${DEVELOPING_FILE} 都是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted} 条(自己在真卡片上逐条走通后跑 design accept)`}`);
643
+ }
644
+ process.exit(cells.length === 0 && fresh && copyProblems.length === 0 && userFresh ? 0 : 1);
504
645
  }
505
646
 
506
647
  export async function design({ root, args = [] }) {
@@ -511,12 +652,16 @@ export async function design({ root, args = [] }) {
511
652
  console.error(`✖ ${DESIGN_FILE} 已经在了;要重来先删掉它`);
512
653
  process.exit(1);
513
654
  }
514
- const design0 = skeleton(manifestOf(root));
655
+ const manifest = manifestOf(root);
656
+ const design0 = skeleton(manifest);
515
657
  writeDesign(root, design0);
516
- const how = applyToReadme(root, renderDesign(design0));
517
- console.log(`✔ 写好 ${DESIGN_FILE}(三张表 + 状态矩阵 + 数据源记录 + 验收清单的空骨架)`);
518
- console.log(` README 的那一段:${how === 'created' ? '新建' : how === 'inserted' ? '插进「需求底稿」那一节' : '已更新'}`);
519
- console.log(' 接着把每一格填成"用户看得见的东西",然后 dsh-wb-module design check');
658
+ const source = readUserSource(root) ?? userSourceSkeleton(manifest, design0);
659
+ writeUserSource(root, source);
660
+ const how = applyToDeveloping(root, renderDesign(design0));
661
+ writeUserReadme(root, source);
662
+ console.log(`✔ 写好 ${DESIGN_FILE} 与 ${README_SOURCE_FILE}(工程底稿 + 用户文案骨架)`);
663
+ console.log(` ${DEVELOPING_FILE} 的那一段:${how === 'created' ? '新建' : how === 'inserted' ? '插进「需求底稿」那一节' : '已更新'}`);
664
+ console.log(' 接着把两边的空格填成"用户看得见的东西",然后 dsh-wb-module design check');
520
665
  return;
521
666
  }
522
667
 
@@ -526,8 +671,19 @@ export async function design({ root, args = [] }) {
526
671
  console.error(`✖ 没有 ${DESIGN_FILE} —— 先跑 dsh-wb-module design init`);
527
672
  process.exit(1);
528
673
  }
529
- const how = applyToReadme(root, renderDesign(design0));
530
- console.log(`✔ README 的那一段:${how === 'same' ? '本来就是最新的' : how === 'updated' ? '更新了' : how === 'inserted' ? '插进「需求底稿」那一节' : '建了 README'}`);
674
+ if (!existsSync(join(root, DEVELOPING_FILE))) {
675
+ console.error(`✖ 没有 ${DEVELOPING_FILE} —— 旧模块先跑 dsh-wb-module design migrate;新模块跑 design init`);
676
+ process.exit(1);
677
+ }
678
+ const source = readUserSource(root);
679
+ if (source === undefined) {
680
+ console.error(`✖ 没有 ${README_SOURCE_FILE} —— 旧模块先跑 dsh-wb-module design migrate;新模块跑 design init`);
681
+ process.exit(1);
682
+ }
683
+ const how = applyToDeveloping(root, renderDesign(design0));
684
+ const user = writeUserReadme(root, source);
685
+ console.log(`✔ ${DEVELOPING_FILE} 的那一段:${how === 'same' ? '本来就是最新的' : how === 'updated' ? '更新了' : how === 'inserted' ? '插进「需求底稿」那一节' : '已更新'}`);
686
+ console.log(` README.md:${user === 'same' ? '本来就是最新的' : user === 'created' ? '新建' : '重生成'}`);
531
687
  return;
532
688
  }
533
689
 
@@ -545,16 +701,43 @@ export async function design({ root, args = [] }) {
545
701
  design0.acceptance = acceptance.map((one) => ({ ...one, checked: true }));
546
702
  design0.acceptedAt = new Date().toISOString();
547
703
  writeDesign(root, design0);
548
- applyToReadme(root, renderDesign(design0));
704
+ applyToDeveloping(root, renderDesign(design0));
705
+ const source = readUserSource(root);
706
+ if (source !== undefined) writeUserReadme(root, source);
549
707
  console.log(`✔ ${acceptance.length} 条验收全部打勾(只在真的在真卡片上逐条走通过之后再跑这条)`);
550
708
  return;
551
709
  }
552
710
 
711
+ if (action === 'migrate') {
712
+ const design0 = readDesign(root);
713
+ if (design0 === undefined) {
714
+ console.error(`✖ 没有 ${DESIGN_FILE} —— 先跑 dsh-wb-module design init`);
715
+ process.exit(1);
716
+ }
717
+ if (existsSync(join(root, DEVELOPING_FILE))) {
718
+ console.error(`✖ ${DEVELOPING_FILE} 已经在了;拆过的模块不用再 migrate`);
719
+ process.exit(1);
720
+ }
721
+ const readmeFile = join(root, 'README.md');
722
+ if (existsSync(readmeFile)) {
723
+ copyFileSync(readmeFile, join(root, DEVELOPING_FILE));
724
+ console.log(`✔ 旧 README 已保存为 ${DEVELOPING_FILE}`);
725
+ }
726
+ const source = readUserSource(root) ?? userSourceSkeleton(manifestOf(root), design0);
727
+ writeUserSource(root, source);
728
+ const how = applyToDeveloping(root, renderDesign(design0));
729
+ const user = writeUserReadme(root, source);
730
+ console.log(`✔ ${DEVELOPING_FILE} 的那一段:${how === 'same' ? '本来就是最新的' : how === 'updated' ? '更新了' : '已更新'}`);
731
+ console.log(`✔ ${README_SOURCE_FILE} 与 README.md 已生成(${user === 'created' ? '新建' : '重生成'})`);
732
+ console.log(` 接着把 ${README_SOURCE_FILE} 里的(待填)按终端用户视角填完,再 dsh-wb-module design check`);
733
+ return;
734
+ }
735
+
553
736
  if (action === 'check') {
554
737
  checkDesign(root);
555
738
  return;
556
739
  }
557
740
 
558
- console.error(`不认识的 design 动作:${action}(可用:init / check / render / accept)`);
741
+ console.error(`不认识的 design 动作:${action}(可用:init / migrate / check / render / accept)`);
559
742
  process.exit(2);
560
743
  }
package/src/init.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { cpSync, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
- import { DESIGN_FILE, applyToReadme, readDesign, renderDesign } from './design.mjs';
2
+ import { DESIGN_FILE, applyToDeveloping, readDesign, readUserSource, renderDesign, writeUserReadme } from './design.mjs';
3
3
  import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
@@ -109,10 +109,14 @@ export async function init({ root, args = [] }) {
109
109
  };
110
110
  walk(target);
111
111
  const declared = readDesign(target);
112
- if (declared !== undefined) applyToReadme(target, renderDesign(declared));
112
+ if (declared !== undefined) {
113
+ applyToDeveloping(target, renderDesign(declared));
114
+ const source = readUserSource(target);
115
+ if (source !== undefined) writeUserReadme(target, source);
116
+ }
113
117
  const tables = existsSync(join(target, DESIGN_FILE)) ? `${DESIGN_FILE} 里那份三张表` : '还没有三张表(dsh-wb-module design init 生成)';
114
118
  console.log(`${target} 建好了:${unit} 模块骨架(模板 ${which};build / check / design / selfcheck 已接工具)`);
115
- console.log(` 需求底稿在 ${tables};空格用 dsh-wb-module design check 查,README 由它生成`);
119
+ console.log(` 需求底稿在 ${tables};空格用 dsh-wb-module design check 查,readme.json 出用户 README,design.json 出 DEVELOPING.md`);
116
120
  console.log(` 模板默认带了 ${DSH_UI}@${dshUi.version}(${dshUi.source})—— 这是 dsh 原语的类型面 + external 声明两处;`);
117
121
  console.log(' 确实用不到就连 devDependencies 与 dsh.client.external 两处一起删,判据不拦。');
118
122
  }
@@ -7,10 +7,12 @@ import { workbenchRootOf } from './release-util.mjs';
7
7
  const FINGERPRINT_FILES = [
8
8
  'package.json',
9
9
  'design.json',
10
+ 'readme.json',
10
11
  'dist/host.js',
11
12
  'client.js',
12
13
  'cordis.patch.yml',
13
14
  'README.md',
15
+ 'DEVELOPING.md',
14
16
  'CHANGELOG.md',
15
17
  ];
16
18
 
@@ -229,7 +229,7 @@ function gitInfoOf(root, manifest, args, exec) {
229
229
  });
230
230
  if (status.status !== 0) throw new ReleaseError(`读不了 git 状态:${tailOf(status.stderr)}`);
231
231
  const prefix = packagePath === '.' ? '' : `${packagePath}/`;
232
- const allowed = ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json'];
232
+ const allowed = ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'DEVELOPING.md', 'CHANGELOG.md', 'design.json', 'readme.json'];
233
233
  const dirty = status.stdout
234
234
  .trim()
235
235
  .split('\n')
package/src/release.mjs CHANGED
@@ -139,7 +139,7 @@ function runGate(exec, root, options) {
139
139
 
140
140
  function gitPathsOf(info) {
141
141
  const prefix = info.packagePath === '.' ? '' : `${info.packagePath}/`;
142
- return ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json']
142
+ return ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'DEVELOPING.md', 'CHANGELOG.md', 'design.json', 'readme.json']
143
143
  .map((one) => `${prefix}${one}`)
144
144
  .filter((one) => existsSync(join(info.toplevel, one)));
145
145
  }
@@ -1,6 +1,6 @@
1
1
  # __NAME__ · agent 工作说明
2
2
 
3
- 工作台模块(单元 `__UNIT__`)。模块行为与数据看 `README.md`;这份只说工具链怎么用。
3
+ 工作台模块(单元 `__UNIT__`)。终端用户说明在 `README.md`(文案源 `readme.json`);实现、数据源、设计底稿在 `DEVELOPING.md`;这份只说工具链怎么用。
4
4
 
5
5
  ## 干活循环
6
6
 
@@ -11,7 +11,7 @@
11
11
  **全绿**才算这件活做完。怎么读红、常见红:`@dsh-workbench/core/skills/workbench-module-diy/REVIEW.md`。
12
12
  3. `pnpm test` —— `src/**/__tests__/` 下、与改动同目录的 `*.test.ts`;新逻辑配对应用例,**全绿**才算完。
13
13
 
14
- `pnpm selfcheck` = build + `design check` + smoke;`design.json` 里的(待填)填完才绿。
14
+ `pnpm selfcheck` = build + `design check` + smoke;`design.json` 与 `readme.json` 里的(待填)填完才绿。
15
15
 
16
16
  ## 工具链约定
17
17
 
@@ -21,5 +21,7 @@
21
21
 
22
22
  ## 先读
23
23
 
24
- - `README.md`:模块行为、数据源、三张表。
24
+ - `readme.json`:终端用户文案源(标题、简介、会看到什么、能做什么、agent 能做什么、数据从哪来、排障、FAQ)。
25
+ - `README.md`:由 `readme.json` 生成,不要手改。
26
+ - `DEVELOPING.md`:实现、路由、判据、数据源与三张表。
25
27
  - `@dsh-workbench/core/skills/workbench-module-diy/`:路由 / 工具 / 卡片 / 文案的写法与约定(随外壳出货)。
@@ -0,0 +1,185 @@
1
+ # __NAME__ · 作者文档
2
+
3
+ 终端用户看 [README.md](README.md)(文案源 `readme.json`);这份讲实现、路由、判据和设计底稿。
4
+
5
+ `__UNIT__` 模块:一个独立的 dsh 插件包,只认外壳的服务名,不认识外壳源码。
6
+
7
+ 这份是 **唯一模板**(`--template diy`,也是默认)生成的骨架:一张**读当前项目目录的只读卡**,
8
+ 数据从宿主侧**真读**一个目录算出来(`node:fs`),另带一条**能跑**的流程 / 长任务 / 真工具 /
9
+ 动作通道样板;不是一个写死的样板。
10
+
11
+ ```bash
12
+ dsh-wb-module init <unit> --dir <目录> # 生成这份骨架(只有这一个模板)
13
+ dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
14
+ dsh-wb-module check # 十四条判据:契约 / 声明协议 / 产物边界 / 源码边界 / 执行缝 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
15
+ dsh-wb-module check --only cards # 十四条可以单跑
16
+ ```
17
+
18
+ 包自带的六个脚本(`build` / `typecheck` / `test` / `selfcheck` 是 `check` 的契约判据要求的那四个;`check` 跑全部判据,`lint` 只跑源码卫生那一条):
19
+
20
+ ```bash
21
+ pnpm build # 构建产物
22
+ pnpm typecheck # tsc --noEmit
23
+ pnpm test # vitest:路由真读数 + 卡片五态 + 长任务/工具契约
24
+ pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 真跑一趟流程 + 走 scripts/smoke.assert.mjs
25
+ pnpm check # dsh-wb-module check
26
+ pnpm lint # dsh-wb-module lint(`--fix` 就地改文件;`check` 永不动手)
27
+ ```
28
+
29
+ ## 需求底稿(做之前填,做完自己逐条走一遍)
30
+
31
+ 三张表既是**要问清的需求**,也是**最后的验收单** —— 表里每一格 agent 都要在真卡片上见过或点过。
32
+ **空着的格就是还没定的事**,别带着空格往下做:落点是同目录的 `design.json`
33
+ (`dsh-wb-module design check` 会把空格一格一格列出来),**下面标了 start/end 的那一段就是由它生成的**
34
+ —— 要改三张表就改 `design.json`,再 `dsh-wb-module design render`;手改那一段会被 next 一次 check 判红。
35
+ 所以下次他说"改一下"不用从头问。
36
+
37
+ **非生成段(这一节与下文)不会被 render 覆盖**:需求变了要自己改这里,`design render` 只管标记之间那一段。
38
+
39
+ 怎么问、怎么填:指引 `SKILL.md` 的七步 · `DESIGN.md`(判域 / 两条分工 / 三张表)·
40
+ `STEPS.md`(每步问什么、什么时候算做完)· `PANEL.md` · `TOOLS.md` · `LINKAGE.md`。
41
+
42
+ <!-- dsh-wb:design:start -->
43
+ (这一段由 `design.json` 生成:改需求改 `design.json`,再 `dsh-wb-module design render`;手改会被下一次 check 判红。)
44
+ <!-- dsh-wb:design:end -->
45
+
46
+ ## 生成出来的形状
47
+
48
+ **分层多文件**,不是单文件:数字越小越底层,上层只许 import 下层的(`arch.config.mjs` 那条
49
+ `arch` 判据盯着,`dsh-wb-module check` 会跑)。目录**按需要出现**:这份有状态逻辑,所以有 `model/`。
50
+ 宿主侧的辅助实现放 `src/host/`(`moduleArch()` 默认就认这一层;
51
+ `src/host.ts` 仍是对外的宿主面,两个面只认 `dsh.workbenchFaces` 里那两个文件)。
52
+
53
+ | 文件 | 是什么 | 层 |
54
+ |---|---|---|
55
+ | `package.json` | 自述清单:`dsh.workbench.unit` = `__UNIT__`、两个面(`src/host.ts` / `src/client.tsx`)、八个脚本、peer 下限 | — |
56
+ | `arch.config.mjs` | 本模块的架构方言:`moduleArch()`(`src/host/**` 默认在层序里)—— 层序表 / 两个运行时 / 阈值只有 `@dsh-workbench/module-tools/arch` 那一份,随工具链同代 | — |
57
+ | `eslint.config.mjs` | 本模块的**语法卫生方言**,同样两行:`moduleLint()` —— 规则表只有 `@dsh-workbench/module-tools/lint` 那一份(`console` / `as any` / hooks 那几条 / 手搓颜色与 `!important`);要开口子就 `moduleLint({ rules })` | — |
58
+ | `cordis.patch.yml` | 插进 dsh 组合的补丁 | — |
59
+ | `design.json` | 三张表 + 状态矩阵 + 数据源记录 + 验收清单(**唯一真相**,上面那段由它生成;`dsh-wb-module design check/render/accept` 读写它) | — |
60
+ | `src/model/overview.ts` | 纯逻辑:类型 + 五态状态机 + `formatBytes`。两个运行时都读得到,不碰 react / kit / dsh / `node:*` | 1 |
61
+ | `src/model/__tests__/overview.test.ts` | 模型层的配对测试(有 `src/model/**` 就必须有 —— `arch` 的 M08) | 99 |
62
+ | `src/model/flow.ts` | 样板流程的纯逻辑:`FlowDefinition` + 五态标题 / 是否还在跑;不碰宿主 | 1 |
63
+ | `src/model/__tests__/flow.test.ts` | 流程模型的配对测试 | 99 |
64
+ | `src/model/notes.ts` | 笔记的纯逻辑:校验 / 读回 / 最多 20 条 | 1 |
65
+ | `src/model/__tests__/notes.test.ts` | 笔记模型的配对测试 | 99 |
66
+ | `src/model/prefs.ts` | 配置的纯逻辑:按工作区存 `limit`(1..50),坏值回默认 | 1 |
67
+ | `src/model/__tests__/prefs.test.ts` | 配置模型的配对测试 | 99 |
68
+ | `src/messages.ts` | 卡片那几句文案(zh / en),键带 `<unit>.` 前缀 | 1 |
69
+ | `src/declared-tools.ts` | 清单里工具名的生成物(`dsh-wb-module sync` 写;代码只引 `DECLARED_TOOLS.run.name` / `DECLARED_TOOLS.addNote.name`,不写字面量) | 1 |
70
+ | `src/components/OverviewCard.tsx` | 卡片:`Card` 壳 + 五态 + 最大的几个文件 + 长任务行 + 笔记(写)/「显示几个」配置 | 2 |
71
+ | `src/components/OverviewBody.tsx` | 五态各自那一屏(待机 / 加载 / 就绪 / 空 / 出错) | 2 |
72
+ | `src/components/OverviewBody.module.css` | 就绪态「最大的几个文件」列表的布局 | 2 |
73
+ | `src/components/RunSection.tsx` | 长任务那一块:跑流程的钮 + `LongTaskRow`(五态 / 停止) | 2 |
74
+ | `src/components/NotesSection.tsx` | 笔记那一块:输入 + 存一条 + 列表 | 2 |
75
+ | `src/components/PrefsSection.tsx` | 「最大的几个文件」配置(`SelectField`,按工作区存) | 2 |
76
+ | `src/components/OverviewCard.module.css` | 这个组件自己的样式(CSS Modules,构建时类名按路径哈希)—— **样式跟组件同目录** | 2 |
77
+ | `src/components/__tests__/OverviewCard.test.ts` | 这张卡五态各画得出来 + 写失败留在卡上(测试住同目录的 `__tests__/`);流程 / 笔记 / 配置的接线由 `plugin.test.ts` 与 smoke 覆盖 | 99 |
78
+ | `src/host.ts` | 宿主面:接线 + 三条只读路由(`overview` / `notes` / `prefs`)+ 挂工具 / 长任务 / 动作通道 / `prefs` / 审批口径 | 10 |
79
+ | `src/host/overview.ts` | 宿主侧内部实现:`readOverview()`(真读目录)+ 路由回答 | 10 |
80
+ | `src/host/sample.ts` | 宿主侧内部实现:样板流程的动作、长任务、只读工具 | 10 |
81
+ | `src/host/notes.ts` | 宿主侧内部实现:笔记读写(`deps.store`)+ 写工具 + 动作通道 | 10 |
82
+ | `src/client.tsx` | 浏览器面:**只剩面** —— 注册 + 把 props 交给 `components/OverviewCard` | 10 |
83
+ | `src/__tests__/plugin.test.ts` | 面注册 + 路由真读 + 工具注册 + 流程真跑 + 笔记/配置/审批 | 99 |
84
+ | `scripts/smoke.mjs` | 接线自检:卡片进没进外壳、工具注册、流程真跑、笔记真写、配置存得住;真读断言在 `smoke.assert.mjs` | — |
85
+ | `scripts/smoke.assert.mjs` | **这个模块自己的**真读断言:换数据源时改这份,别删 smoke 里的调用 | — |
86
+ | `CHANGELOG.md` | 版本记录(`dsh-wb-module release` 追加;`package.json.files` 里带它) | — |
87
+ | `tsconfig.json` / `vitest.config.ts` / `types/globals.d.ts` / `types/styles.d.ts` | 经典 JSX 运行时(`jsxFactory: h`)、vitest 的别名、浏览器半侧的运行时垫片、`*.module.css` 的类名表声明 | — |
88
+ | `DEVELOPING.md` | 就是本文(作者文档;用户说明在 `README.md`) | — |
89
+
90
+ ## 长任务、工具、写通道与配置(样板)
91
+
92
+ 这份骨架故意带了一组**可跑**的能力,示范五件事:
93
+
94
+ - **宿主面接线**:`name` / `inject` / `apply` 用 `@dsh-workbench/shared/host` 的 `defineUnitHostPlugin`
95
+ 一次给齐;写工具的审批口径用 `writeApprovalOf`;`recordOf` / `textOf` 也从那里取,别每个文件抄一份。
96
+ - **只读工具**:`registerTools()` 是**调用时才注册**、返回的是清理函数;工具名必须和 `package.json` 的
97
+ `dsh.workbench.tools` 一字不差。`__UNIT___run` 起一趟流程,传 `runId` 则只看同一趟。
98
+ - **长任务**:`deps.longTasks.create()` 交回一张注册表,`run` 里调 `deps.flow.run()` 并把
99
+ `onStep` 交给 `longTaskStepOf`(shared/host 的默认映射);卡片用 `useLongTask` 读、`LongTaskRow` 画。
100
+ - **写通道 + 审批**:`__UNIT___add_note` 在清单里写 `writes: true`,`mount()` 返回 `needsApproval`(这次要点头)
101
+ 与 `approvalDetail`(审批屏上那句"会写什么");agent 调用前壳会拦一道。卡片那颗钮走动作通道
102
+ `<unit>-add-note`(点击即确认,不审批),与写工具共用同一份 `deps.store` 数据;网络 / 校验失败
103
+ 留在按钮下面那一行,不静默。
104
+ - **配置(prefs)**:`mount()` 返回 `prefs: { resource, handle }`,按工作区把"显示几个文件"存进
105
+ `deps.store`;卡片用 `useHostWrite({ channel: 'prefs' })` 写、用 `<unit>/prefs` 读。
106
+ - **动作通道**:用户直接点的单步动作走 `<unit>-…` 资源;与工具共用实现,不另写一份。
107
+
108
+ 换成你自己的能力时,五处一起改:`src/model/*` 的纯逻辑、`src/host/*` 的实现、
109
+ `src/host.ts` 的接线(路由 / 工具 / 动作 / prefs / 审批)、`src/components/OverviewCard.tsx` 的展示、
110
+ `design.json` 的三张表与能力行。
111
+
112
+ ## 数据是在哪算出来的
113
+
114
+ 宿主侧,`src/host/overview.ts` 的 `readOverview(path, limit)`:
115
+
116
+ * `readdirSync(path, { withFileTypes: true })` 数出**目录数**;
117
+ * 每个普通文件 `statSync` 一次,累加**总字节**、记下 `name / bytes / mtime`(mtime 是 ISO 串);
118
+ * 按字节降序(同字节按名字)排,取前 `limit` 个当**最大的几个文件**。
119
+
120
+ 已知的取舍(不是 bug,是这份骨架刻意保持简单的地方):
121
+
122
+ * **符号链接不计数**:既不算文件也不算目录(想要跟随就自己换成 `statSync` + `lstatSync` 判断);
123
+ * **同步读**:模板优先让代码一眼看得懂;真接大目录时改成 `node:fs/promises` 版本更合适;
124
+ * 只读**直接子项**,不递归。
125
+
126
+ ## 那条只读路由
127
+
128
+ 键是 `<unit>/overview`(`__UNIT__` 就是本模块的单元 id)——**必须住在自己那一格的命名空间里**,
129
+ 外壳装 / 升 / 卸时要能点名到它;写成别的名字注册当场就被判红。
130
+
131
+ ```
132
+ GET /api/__UNIT__/overview?path=<绝对路径>&limit=<1..50>
133
+ ```
134
+
135
+ 查询参数:
136
+
137
+ | 参数 | 缺省 | 说明 |
138
+ |---|---|---|
139
+ | `path` | 无(必填) | 要读的目录。卡片把 `props.world.current.workspace.path` 塞进来;缺了不回数据,直接回 `no-path` |
140
+ | `limit` | `5` | 返回几个"最大的文件",夹在 `1..50` 之间 |
141
+
142
+ 成功:
143
+
144
+ ```json
145
+ { "ok": true, "path": "/…", "files": 128, "dirs": 12, "bytes": 3145728,
146
+ "largest": [{ "name": "client.js", "bytes": 182400, "mtime": "2026-01-05T09:12:44.000Z" }] }
147
+ ```
148
+
149
+ 读不到(路径不存在 / 没权限 / 不是目录):**HTTP 200 + `ok:false`**,错误码 `overview-unreadable`。
150
+ 压根没给 `path`:同样 `ok:false`,错误码 `no-path` —— 别拿宿主进程的工作目录当默认值:
151
+ 那会让"我没说要看哪儿"变成"屏幕上是 dsh 服务启动目录",两个都不像用户想要的东西。
152
+ 之所以不让它抛:抛出去会被外壳的传输层转成 HTTP 500,卡片只剩一句"宿主返回 HTTP 500",
153
+ 用户看不到是哪条路径出了什么事。
154
+
155
+ ```json
156
+ { "ok": false, "error": { "code": "overview-unreadable", "message": "ENOENT: no such file or directory, scandir '/…'" } }
157
+ ```
158
+
159
+ 卡片侧读数的 hook 是 `kit.hooks.useApiResource`(`route` 就是 `__UNIT__/overview`)。
160
+ 注意它的 `value` 是**整个响应对象**(成功时形如 `{ ok: true, ...payload }`),不是 `payload` 本身。
161
+
162
+ ## 卡片画哪五态
163
+
164
+ `src/model/overview.ts` 的 `overviewStateOf({ phase, value, error })` 是一条纯函数,
165
+ 每个分支各对应一屏(`src/model/__tests__/overview.test.ts` 直接断言这条函数,
166
+ `src/components/__tests__/OverviewCard.test.ts` 再把每一屏画一遍):
167
+
168
+ | 态 | 什么时候 | 画什么 |
169
+ |---|---|---|
170
+ | 待机 | 没有选中工作区(`path === ''`),请求压根不发(`useApiResource(…, { enabled: false })`,`phase` 停在 `idle`) | `CenteredState`:先打开一个项目 |
171
+ | 加载 | 还没有值(首读),或 `phase === 'error'` 之外什么都没来 | `CenteredState`:正在读目录 |
172
+ | 就绪 | 有值,且文件数 + 目录数 > 0 | `StateBlock`:路径 + 「N 个文件 · M 个目录 · 总字节」+ 最大的几个文件列表 |
173
+ | 就绪但空 | 有值,但文件数 = 目录数 = 0 | `EmptyState`:这个目录是空的 |
174
+ | 出错 | `phase === 'error'` | `CenteredState`:读不到这个目录 + 宿主的原话 |
175
+
176
+ 刷新时 `useApiResource` 会把 `phase` 打成 `loading` **同时保留上一次的 `value`** ——
177
+ 这时刻意继续画旧数据(右上角的转圈由 `Card` 的 `refreshing` 表示),不退回骨架:
178
+ 一次刷新不该让整张卡闪一下。
179
+
180
+ ## 开发与发布
181
+
182
+ 开发命令见上面的六个脚本;dsh / 工作台的模块约束见工作台模块指引(随外壳出货);agent 的工作说明见 `AGENTS.md`。
183
+
184
+ 发布(维护者确认之后):`dsh-wb-module release --target team|public|tarball|git`;
185
+ `--dry-run` 只预览,`--verify` 只核已发布的包。
@@ -1,183 +1,5 @@
1
1
  # __NAME__
2
2
 
3
- `__UNIT__` 模块:一个独立的 dsh 插件包,只认外壳的服务名,不认识外壳源码。
3
+ __DESC__
4
4
 
5
- 这份是 **唯一模板**(`--template diy`,也是默认)生成的骨架:一张**读当前项目目录的只读卡**,
6
- 数据从宿主侧**真读**一个目录算出来(`node:fs`),另带一条**能跑**的流程 / 长任务 / 真工具 /
7
- 动作通道样板;不是一个写死的样板。
8
-
9
- ```bash
10
- dsh-wb-module init <unit> --dir <目录> # 生成这份骨架(只有这一个模板)
11
- dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
12
- dsh-wb-module check # 十四条判据:契约 / 声明协议 / 产物边界 / 源码边界 / 执行缝 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
13
- dsh-wb-module check --only cards # 十四条可以单跑
14
- ```
15
-
16
- 包自带的六个脚本(`build` / `typecheck` / `test` / `selfcheck` 是 `check` 的契约判据要求的那四个;`check` 跑全部判据,`lint` 只跑源码卫生那一条):
17
-
18
- ```bash
19
- pnpm build # 构建产物
20
- pnpm typecheck # tsc --noEmit
21
- pnpm test # vitest:路由真读数 + 卡片五态 + 长任务/工具契约
22
- pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 真跑一趟流程 + 走 scripts/smoke.assert.mjs
23
- pnpm check # dsh-wb-module check
24
- pnpm lint # dsh-wb-module lint(`--fix` 就地改文件;`check` 永不动手)
25
- ```
26
-
27
- ## 需求底稿(做之前填,做完自己逐条走一遍)
28
-
29
- 三张表既是**要问清的需求**,也是**最后的验收单** —— 表里每一格 agent 都要在真卡片上见过或点过。
30
- **空着的格就是还没定的事**,别带着空格往下做:落点是同目录的 `design.json`
31
- (`dsh-wb-module design check` 会把空格一格一格列出来),**下面标了 start/end 的那一段就是由它生成的**
32
- —— 要改三张表就改 `design.json`,再 `dsh-wb-module design render`;手改那一段会被 next 一次 check 判红。
33
- 所以下次他说"改一下"不用从头问。
34
-
35
- **非生成段(这一节与下文)不会被 render 覆盖**:需求变了要自己改这里,`design render` 只管标记之间那一段。
36
-
37
- 怎么问、怎么填:指引 `SKILL.md` 的七步 · `DESIGN.md`(判域 / 两条分工 / 三张表)·
38
- `STEPS.md`(每步问什么、什么时候算做完)· `PANEL.md` · `TOOLS.md` · `LINKAGE.md`。
39
-
40
- <!-- dsh-wb:design:start -->
41
- (这一段由 `design.json` 生成:改需求改 `design.json`,再 `dsh-wb-module design render`;手改会被下一次 check 判红。)
42
- <!-- dsh-wb:design:end -->
43
-
44
- ## 生成出来的形状
45
-
46
- **分层多文件**,不是单文件:数字越小越底层,上层只许 import 下层的(`arch.config.mjs` 那条
47
- `arch` 判据盯着,`dsh-wb-module check` 会跑)。目录**按需要出现**:这份有状态逻辑,所以有 `model/`。
48
- 宿主侧的辅助实现放 `src/host/`(`moduleArch()` 默认就认这一层;
49
- `src/host.ts` 仍是对外的宿主面,两个面只认 `dsh.workbenchFaces` 里那两个文件)。
50
-
51
- | 文件 | 是什么 | 层 |
52
- |---|---|---|
53
- | `package.json` | 自述清单:`dsh.workbench.unit` = `__UNIT__`、两个面(`src/host.ts` / `src/client.tsx`)、八个脚本、peer 下限 | — |
54
- | `arch.config.mjs` | 本模块的架构方言:`moduleArch()`(`src/host/**` 默认在层序里)—— 层序表 / 两个运行时 / 阈值只有 `@dsh-workbench/module-tools/arch` 那一份,随工具链同代 | — |
55
- | `eslint.config.mjs` | 本模块的**语法卫生方言**,同样两行:`moduleLint()` —— 规则表只有 `@dsh-workbench/module-tools/lint` 那一份(`console` / `as any` / hooks 那几条 / 手搓颜色与 `!important`);要开口子就 `moduleLint({ rules })` | — |
56
- | `cordis.patch.yml` | 插进 dsh 组合的补丁 | — |
57
- | `design.json` | 三张表 + 状态矩阵 + 数据源记录 + 验收清单(**唯一真相**,上面那段由它生成;`dsh-wb-module design check/render/accept` 读写它) | — |
58
- | `src/model/overview.ts` | 纯逻辑:类型 + 五态状态机 + `formatBytes`。两个运行时都读得到,不碰 react / kit / dsh / `node:*` | 1 |
59
- | `src/model/__tests__/overview.test.ts` | 模型层的配对测试(有 `src/model/**` 就必须有 —— `arch` 的 M08) | 99 |
60
- | `src/model/flow.ts` | 样板流程的纯逻辑:`FlowDefinition` + 五态标题 / 是否还在跑;不碰宿主 | 1 |
61
- | `src/model/__tests__/flow.test.ts` | 流程模型的配对测试 | 99 |
62
- | `src/model/notes.ts` | 笔记的纯逻辑:校验 / 读回 / 最多 20 条 | 1 |
63
- | `src/model/__tests__/notes.test.ts` | 笔记模型的配对测试 | 99 |
64
- | `src/model/prefs.ts` | 配置的纯逻辑:按工作区存 `limit`(1..50),坏值回默认 | 1 |
65
- | `src/model/__tests__/prefs.test.ts` | 配置模型的配对测试 | 99 |
66
- | `src/messages.ts` | 卡片那几句文案(zh / en),键带 `<unit>.` 前缀 | 1 |
67
- | `src/declared-tools.ts` | 清单里工具名的生成物(`dsh-wb-module sync` 写;代码只引 `DECLARED_TOOLS.run.name` / `DECLARED_TOOLS.addNote.name`,不写字面量) | 1 |
68
- | `src/components/OverviewCard.tsx` | 卡片:`Card` 壳 + 五态 + 最大的几个文件 + 长任务行 + 笔记(写)/「显示几个」配置 | 2 |
69
- | `src/components/OverviewBody.tsx` | 五态各自那一屏(待机 / 加载 / 就绪 / 空 / 出错) | 2 |
70
- | `src/components/OverviewBody.module.css` | 就绪态「最大的几个文件」列表的布局 | 2 |
71
- | `src/components/RunSection.tsx` | 长任务那一块:跑流程的钮 + `LongTaskRow`(五态 / 停止) | 2 |
72
- | `src/components/NotesSection.tsx` | 笔记那一块:输入 + 存一条 + 列表 | 2 |
73
- | `src/components/PrefsSection.tsx` | 「最大的几个文件」配置(`SelectField`,按工作区存) | 2 |
74
- | `src/components/OverviewCard.module.css` | 这个组件自己的样式(CSS Modules,构建时类名按路径哈希)—— **样式跟组件同目录** | 2 |
75
- | `src/components/__tests__/OverviewCard.test.ts` | 这张卡五态各画得出来 + 写失败留在卡上(测试住同目录的 `__tests__/`);流程 / 笔记 / 配置的接线由 `plugin.test.ts` 与 smoke 覆盖 | 99 |
76
- | `src/host.ts` | 宿主面:接线 + 三条只读路由(`overview` / `notes` / `prefs`)+ 挂工具 / 长任务 / 动作通道 / `prefs` / 审批口径 | 10 |
77
- | `src/host/overview.ts` | 宿主侧内部实现:`readOverview()`(真读目录)+ 路由回答 | 10 |
78
- | `src/host/sample.ts` | 宿主侧内部实现:样板流程的动作、长任务、只读工具 | 10 |
79
- | `src/host/notes.ts` | 宿主侧内部实现:笔记读写(`deps.store`)+ 写工具 + 动作通道 | 10 |
80
- | `src/client.tsx` | 浏览器面:**只剩面** —— 注册 + 把 props 交给 `components/OverviewCard` | 10 |
81
- | `src/__tests__/plugin.test.ts` | 面注册 + 路由真读 + 工具注册 + 流程真跑 + 笔记/配置/审批 | 99 |
82
- | `scripts/smoke.mjs` | 接线自检:卡片进没进外壳、工具注册、流程真跑、笔记真写、配置存得住;真读断言在 `smoke.assert.mjs` | — |
83
- | `scripts/smoke.assert.mjs` | **这个模块自己的**真读断言:换数据源时改这份,别删 smoke 里的调用 | — |
84
- | `CHANGELOG.md` | 版本记录(`dsh-wb-module release` 追加;`package.json.files` 里带它) | — |
85
- | `tsconfig.json` / `vitest.config.ts` / `types/globals.d.ts` / `types/styles.d.ts` | 经典 JSX 运行时(`jsxFactory: h`)、vitest 的别名、浏览器半侧的运行时垫片、`*.module.css` 的类名表声明 | — |
86
- | `README.md` | 就是本文 | — |
87
-
88
- ## 长任务、工具、写通道与配置(样板)
89
-
90
- 这份骨架故意带了一组**可跑**的能力,示范五件事:
91
-
92
- - **宿主面接线**:`name` / `inject` / `apply` 用 `@dsh-workbench/shared/host` 的 `defineUnitHostPlugin`
93
- 一次给齐;写工具的审批口径用 `writeApprovalOf`;`recordOf` / `textOf` 也从那里取,别每个文件抄一份。
94
- - **只读工具**:`registerTools()` 是**调用时才注册**、返回的是清理函数;工具名必须和 `package.json` 的
95
- `dsh.workbench.tools` 一字不差。`__UNIT___run` 起一趟流程,传 `runId` 则只看同一趟。
96
- - **长任务**:`deps.longTasks.create()` 交回一张注册表,`run` 里调 `deps.flow.run()` 并把
97
- `onStep` 交给 `longTaskStepOf`(shared/host 的默认映射);卡片用 `useLongTask` 读、`LongTaskRow` 画。
98
- - **写通道 + 审批**:`__UNIT___add_note` 在清单里写 `writes: true`,`mount()` 返回 `needsApproval`(这次要点头)
99
- 与 `approvalDetail`(审批屏上那句"会写什么");agent 调用前壳会拦一道。卡片那颗钮走动作通道
100
- `<unit>-add-note`(点击即确认,不审批),与写工具共用同一份 `deps.store` 数据;网络 / 校验失败
101
- 留在按钮下面那一行,不静默。
102
- - **配置(prefs)**:`mount()` 返回 `prefs: { resource, handle }`,按工作区把"显示几个文件"存进
103
- `deps.store`;卡片用 `useHostWrite({ channel: 'prefs' })` 写、用 `<unit>/prefs` 读。
104
- - **动作通道**:用户直接点的单步动作走 `<unit>-…` 资源;与工具共用实现,不另写一份。
105
-
106
- 换成你自己的能力时,五处一起改:`src/model/*` 的纯逻辑、`src/host/*` 的实现、
107
- `src/host.ts` 的接线(路由 / 工具 / 动作 / prefs / 审批)、`src/components/OverviewCard.tsx` 的展示、
108
- `design.json` 的三张表与能力行。
109
-
110
- ## 数据是在哪算出来的
111
-
112
- 宿主侧,`src/host/overview.ts` 的 `readOverview(path, limit)`:
113
-
114
- * `readdirSync(path, { withFileTypes: true })` 数出**目录数**;
115
- * 每个普通文件 `statSync` 一次,累加**总字节**、记下 `name / bytes / mtime`(mtime 是 ISO 串);
116
- * 按字节降序(同字节按名字)排,取前 `limit` 个当**最大的几个文件**。
117
-
118
- 已知的取舍(不是 bug,是这份骨架刻意保持简单的地方):
119
-
120
- * **符号链接不计数**:既不算文件也不算目录(想要跟随就自己换成 `statSync` + `lstatSync` 判断);
121
- * **同步读**:模板优先让代码一眼看得懂;真接大目录时改成 `node:fs/promises` 版本更合适;
122
- * 只读**直接子项**,不递归。
123
-
124
- ## 那条只读路由
125
-
126
- 键是 `<unit>/overview`(`__UNIT__` 就是本模块的单元 id)——**必须住在自己那一格的命名空间里**,
127
- 外壳装 / 升 / 卸时要能点名到它;写成别的名字注册当场就被判红。
128
-
129
- ```
130
- GET /api/__UNIT__/overview?path=<绝对路径>&limit=<1..50>
131
- ```
132
-
133
- 查询参数:
134
-
135
- | 参数 | 缺省 | 说明 |
136
- |---|---|---|
137
- | `path` | 无(必填) | 要读的目录。卡片把 `props.world.current.workspace.path` 塞进来;缺了不回数据,直接回 `no-path` |
138
- | `limit` | `5` | 返回几个"最大的文件",夹在 `1..50` 之间 |
139
-
140
- 成功:
141
-
142
- ```json
143
- { "ok": true, "path": "/…", "files": 128, "dirs": 12, "bytes": 3145728,
144
- "largest": [{ "name": "client.js", "bytes": 182400, "mtime": "2026-01-05T09:12:44.000Z" }] }
145
- ```
146
-
147
- 读不到(路径不存在 / 没权限 / 不是目录):**HTTP 200 + `ok:false`**,错误码 `overview-unreadable`。
148
- 压根没给 `path`:同样 `ok:false`,错误码 `no-path` —— 别拿宿主进程的工作目录当默认值:
149
- 那会让"我没说要看哪儿"变成"屏幕上是 dsh 服务启动目录",两个都不像用户想要的东西。
150
- 之所以不让它抛:抛出去会被外壳的传输层转成 HTTP 500,卡片只剩一句"宿主返回 HTTP 500",
151
- 用户看不到是哪条路径出了什么事。
152
-
153
- ```json
154
- { "ok": false, "error": { "code": "overview-unreadable", "message": "ENOENT: no such file or directory, scandir '/…'" } }
155
- ```
156
-
157
- 卡片侧读数的 hook 是 `kit.hooks.useApiResource`(`route` 就是 `__UNIT__/overview`)。
158
- 注意它的 `value` 是**整个响应对象**(成功时形如 `{ ok: true, ...payload }`),不是 `payload` 本身。
159
-
160
- ## 卡片画哪五态
161
-
162
- `src/model/overview.ts` 的 `overviewStateOf({ phase, value, error })` 是一条纯函数,
163
- 每个分支各对应一屏(`src/model/__tests__/overview.test.ts` 直接断言这条函数,
164
- `src/components/__tests__/OverviewCard.test.ts` 再把每一屏画一遍):
165
-
166
- | 态 | 什么时候 | 画什么 |
167
- |---|---|---|
168
- | 待机 | 没有选中工作区(`path === ''`),请求压根不发(`useApiResource(…, { enabled: false })`,`phase` 停在 `idle`) | `CenteredState`:先打开一个项目 |
169
- | 加载 | 还没有值(首读),或 `phase === 'error'` 之外什么都没来 | `CenteredState`:正在读目录 |
170
- | 就绪 | 有值,且文件数 + 目录数 > 0 | `StateBlock`:路径 + 「N 个文件 · M 个目录 · 总字节」+ 最大的几个文件列表 |
171
- | 就绪但空 | 有值,但文件数 = 目录数 = 0 | `EmptyState`:这个目录是空的 |
172
- | 出错 | `phase === 'error'` | `CenteredState`:读不到这个目录 + 宿主的原话 |
173
-
174
- 刷新时 `useApiResource` 会把 `phase` 打成 `loading` **同时保留上一次的 `value`** ——
175
- 这时刻意继续画旧数据(右上角的转圈由 `Card` 的 `refreshing` 表示),不退回骨架:
176
- 一次刷新不该让整张卡闪一下。
177
-
178
- ## 开发与发布
179
-
180
- 开发命令见上面的六个脚本;dsh / 工作台的模块约束见工作台模块指引(随外壳出货);agent 的工作说明见 `AGENTS.md`。
181
-
182
- 发布(维护者确认之后):`dsh-wb-module release --target team|public|tarball|git`;
183
- `--dry-run` 只预览,`--verify` 只核已发布的包。
5
+ <!-- 这份说明由 `dsh-wb-module design render` 从 readme.json 生成;作者文档在 DEVELOPING.md。 -->
@@ -8,7 +8,7 @@
8
8
  ".": "./dist/host.js",
9
9
  "./client": "./client.js"
10
10
  },
11
- "files": ["dist", "client.js", "cordis.patch.yml", "CHANGELOG.md"],
11
+ "files": ["dist", "client.js", "cordis.patch.yml", "DEVELOPING.md", "CHANGELOG.md"],
12
12
  "scripts": {
13
13
  "build": "dsh-wb-module build",
14
14
  "typecheck": "tsc --noEmit",
@@ -0,0 +1,11 @@
1
+ {
2
+ "title": "__UNIT__ 模块",
3
+ "summary": "__DESC__",
4
+ "see": "(待填:用户在卡片上会看到什么。写画面和数字,不写组件名、图号、像素值)",
5
+ "do": "(待填:用户自己能点什么、能改什么;没有就写「只能看,不能改」)",
6
+ "agent": "(待填:agent 能替用户做什么;哪些动作会先弹审批。没有就写「这个模块没有给 agent 开放工具」)",
7
+ "data": "(待填:数据从哪个系统来、用谁的登录、会不会写数据。没有数据源就写「没有数据源」)",
8
+ "install": "",
9
+ "trouble": "(待填:没登录、超时、读不到时用户会看到什么,下一步点哪)",
10
+ "faq": []
11
+ }
@@ -337,13 +337,13 @@ check('另一个工作区没被带上(还是默认 5)', otherPrefs?.prefs?.l
337
337
  const { assertModule } = await import('./smoke.assert.mjs');
338
338
  await assertModule({ check, routes, mounted, UNIT_ID });
339
339
 
340
- // ── 需求底稿在不在:这份骨架的"设计记录"住 README,别只留在对话里 ──────────
340
+ // ── 需求底稿在不在:这份骨架的"设计记录"住 DEVELOPING.md,别只留在对话里 ─────
341
341
  //
342
342
  // 只查**形状在不在**(那几节有没有被删掉),不查内容填得好不好 —— 内容由指引守,
343
343
  // 判据硬管内容只会逼出"把占位符原样留着"这种应付。
344
- const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8');
344
+ const developing = readFileSync(new URL('../DEVELOPING.md', import.meta.url), 'utf8');
345
345
  for (const section of ['## 需求底稿', '### 表 1 面板', '### 表 2 agent', '### 表 3 联动', '### 数据源探明记录']) {
346
- check(`README 里有「${section}」(需求与验收的落点)`, readme.includes(section));
346
+ check(`DEVELOPING.md 里有「${section}」(需求与验收的落点)`, developing.includes(section));
347
347
  }
348
348
 
349
349
  console.log(`\n${failures.length === 0 ? '模块自检通过' : `${failures.length} 条没过`}`);