Commit 947cc95f0feccf5327241f4b14ab5449acb25928

Authored by zichun
1 parent 27fae376

workflow: add demo seed data generation + injection (Seed stage, seed-demo-data.mjs, e2e baseline)

- coding.mjs: per-module Seed stage after backend testGate (generate
  sql/seed/NN__<module>.sql, cold-stack verify with PK-range COUNT
  reconciliation); behavior gate step2 -> 5-step ordering (demo seed
  before sentinel, sentinel fixed >=100000 range); fe-skeleton adds
  Playwright globalSetup e2e baseline (seed + admin storageState);
  fe tdd e2e assertion constraints
- skeleton-gen: new scripts-seed-demo-data-template.mjs (mysql CLI,
  _demo_seed_history idempotency ledger, offline-validatable, atomic
  apply+ledger batch) + lib offline tests (93/93 green)
- db-init: B.3 re-clean DB after DDL smoke apply (hand schema back to
  Flyway, avoid missing-history-table error); fix step-D typo
- docs-04/CLAUDE templates: data baseline & demo-seed conventions
  (PK ranges 1-999 init / 1000-9999 seed / >=100000 sentinel)
README.md
@@ -36,7 +36,10 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 @@ -36,7 +36,10 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。
36 │ checkout/create → confirm HEAD(5 微 agent) 36 │ checkout/create → confirm HEAD(5 微 agent)
37 │ → featureLoop(后端):spec → plan → tdd → verify → review(有界 5 轮修复, 37 │ → featureLoop(后端):spec → plan → tdd → verify → review(有界 5 轮修复,
38 │ throw 自然冒泡到模块主循环 try → fail-fast) 38 │ throw 自然冒泡到模块主循环 try → fail-fast)
39 - │ → testGate(backend) → runCrossModule(JS 编排:diff → 分类 → 写日志) 39 + │ → testGate(backend)
  40 + │ → Seed stage(testGate green 后:生成 sql/seed/NN__<module>.sql 演示种子
  41 + │ + 冷起栈真跑验证:空库→Flyway→seed-demo-data.mjs 注入→按 -- expect: 行对账)
  42 + │ → runCrossModule(JS 编排:diff → 分类 → 写日志)
40 │ → reportPrompt(LLM 12 节叙述) 43 │ → reportPrompt(LLM 12 节叙述)
41 │ → runMilestone(JS 编排:wt → default → 已合入? → merge → 字段当前值? 44 │ → runMilestone(JS 编排:wt → default → 已合入? → merge → 字段当前值?
42 │ → 写字段 → tag 已存在? → 打 tag → 报告 § ⑫ 当前值? → 替换占位; 45 │ → 写字段 → tag 已存在? → 打 tag → 报告 § ⑫ 当前值? → 替换占位;
@@ -44,10 +47,11 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 @@ -44,10 +47,11 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。
44 47
45 └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) 48 └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag)
46 runBranchSetup(frontend-phase) 49 runBranchSetup(frontend-phase)
47 - → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起) 50 + → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起;
  51 + 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState)
48 → featureLoop(前端,FE-NN,路径限 frontend/):spec → plan → tdd → verify → 52 → featureLoop(前端,FE-NN,路径限 frontend/):spec → plan → tdd → verify →
49 review 循环内并入 per-FE 行为验收 approve 子门(reviewer approve 时才起本 FE 全栈 53 review 循环内并入 per-FE 行为验收 approve 子门(reviewer approve 时才起本 FE 全栈
50 - +种子 sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix 54 + +演示种子+sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix
51 must-fix→重验,软文字按来源仲裁,行为 green 才打 req-done/<FE>) 55 must-fix→重验,软文字按来源仲裁,行为 green 才打 req-done/<FE>)
52 → testGate(frontend,全量回归 vitest+playwright,与 per-FE 行为验收职责正交) 56 → testGate(frontend,全量回归 vitest+playwright,与 per-FE 行为验收职责正交)
53 → runMilestone(milestone/frontend-phase) 57 → runMilestone(milestone/frontend-phase)
@@ -141,7 +145,7 @@ erp-workflow-plugin/ @@ -141,7 +145,7 @@ erp-workflow-plugin/
141 |---|---|---| 145 |---|---|---|
142 | `code-reviewer` | 统一 reviewer。`phase=backend` 跑通用代码审查维度;`phase=frontend` 附加前端 7 维 checklist(prototype 一致性 / design tokens / a11y / 响应式 / 业务校验前端复刻 / API 一致性 / 状态机覆盖,主观维度仅标记明显问题不触发 request-changes)。非交互,返回结构化 verdict,绝不弹窗 | `workflows/coding.mjs` 的 review stage:`agent(..., {agentType:'erp-workflow:code-reviewer'})`(必须带 `erp-workflow:` 插件命名空间——裸 `code-reviewer` 会与其它插件的同名 agent 歧义) | 146 | `code-reviewer` | 统一 reviewer。`phase=backend` 跑通用代码审查维度;`phase=frontend` 附加前端 7 维 checklist(prototype 一致性 / design tokens / a11y / 响应式 / 业务校验前端复刻 / API 一致性 / 状态机覆盖,主观维度仅标记明显问题不触发 request-changes)。非交互,返回结构化 verdict,绝不弹窗 | `workflows/coding.mjs` 的 review stage:`agent(..., {agentType:'erp-workflow:code-reviewer'})`(必须带 `erp-workflow:` 插件命名空间——裸 `code-reviewer` 会与其它插件的同名 agent 歧义) |
143 147
144 -## Templates 清单(25 份) 148 +## Templates 清单(26 份)
145 149
146 | 所属 Skill | 模板文件 | 用途 | 150 | 所属 Skill | 模板文件 | 用途 |
147 |---|---|---| 151 |---|---|---|
@@ -154,6 +158,7 @@ erp-workflow-plugin/ @@ -154,6 +158,7 @@ erp-workflow-plugin/
154 | scope-lock | `config-vars-template.yaml` | 仓库根 `config-vars.yaml` 骨架(跨栈中立):项目**全部配置**——非敏感(包名/端口/前端包名/初始账号)+ 敏感凭据(database / admin_init.password / secrets);A1 E.2 锁定,随项目提交 | 158 | scope-lock | `config-vars-template.yaml` | 仓库根 `config-vars.yaml` 骨架(跨栈中立):项目**全部配置**——非敏感(包名/端口/前端包名/初始账号)+ 敏感凭据(database / admin_init.password / secrets);A1 E.2 锁定,随项目提交 |
155 | skeleton-gen | `docs-04-skeleton-template.md` | docs/04 § 一+ 编码规范大纲(HTML 注释引导 LLM) | 159 | skeleton-gen | `docs-04-skeleton-template.md` | docs/04 § 一+ 编码规范大纲(HTML 注释引导 LLM) |
156 | skeleton-gen | `scripts-setup-test-db-template.mjs` | 跨平台 drop + create 空库脚本(内联极简 YAML 读 config-vars.yaml database: 段);schema apply 交给 Flyway | 160 | skeleton-gen | `scripts-setup-test-db-template.mjs` | 跨平台 drop + create 空库脚本(内联极简 YAML 读 config-vars.yaml database: 段);schema apply 交给 Flyway |
  161 +| skeleton-gen | `scripts-seed-demo-data-template.mjs` | 演示种子注入脚本(schema 建好后按 `sql/seed/<NN>__<module>.sql` 文件名升序幂等注入;`_demo_seed_history` 账本表记已应用文件跳过;同走 mysql;调用方 = 前端 e2e globalSetup / 行为门 / 里程碑后人工验收) |
157 | skeleton-gen | `scripts-test-template.mjs` | test.mjs 骨架(命令槽位按后端/前端/build/lint/test/e2e 分开,`spawnSync(shell:true)` 跨平台执行) | 162 | skeleton-gen | `scripts-test-template.mjs` | test.mjs 骨架(命令槽位按后端/前端/build/lint/test/e2e 分开,`spawnSync(shell:true)` 跨平台执行) |
158 | skeleton-gen | `gitignore-append-template` | 插件推荐忽略项(`.tmp/`、构建产物等;config-vars.yaml 随项目提交,不忽略) | 163 | skeleton-gen | `gitignore-append-template` | 插件推荐忽略项(`.tmp/`、构建产物等;config-vars.yaml 随项目提交,不忽略) |
159 | skeleton-gen | `styles-tokens-template.css` | 前端 design tokens CSS 变量骨架 | 164 | skeleton-gen | `styles-tokens-template.css` | 前端 design tokens CSS 变量骨架 |
@@ -167,7 +172,7 @@ erp-workflow-plugin/ @@ -167,7 +172,7 @@ erp-workflow-plugin/
167 ## 前置依赖 172 ## 前置依赖
168 173
169 - **Node.js ≥ 18**:`lib/*.mjs` 助手 + 生成进目标项目的 `scripts/*.mjs` 为 Node ESM;`workflows/coding.mjs` 是 Claude Workflow 运行时脚本(由 `Workflow` 工具执行,不作为普通 `node` CLI 入口)。A0 `project-init` 检测 git / mysql / node 在 PATH,缺失则按 OS 自动安装,装不上再停下提示用户 174 - **Node.js ≥ 18**:`lib/*.mjs` 助手 + 生成进目标项目的 `scripts/*.mjs` 为 Node ESM;`workflows/coding.mjs` 是 Claude Workflow 运行时脚本(由 `Workflow` 工具执行,不作为普通 `node` CLI 入口)。A0 `project-init` 检测 git / mysql / node 在 PATH,缺失则按 OS 自动安装,装不上再停下提示用户
170 -- **MySQL 8.x** 实例已就绪(host / 库名 / 凭据取自 `config-vars.yaml` 的 `database:` 段,由你填写并完全信任;本项目只面向开发/沙盒环境,`setup-test-db.mjs` 会按该值 DROP+CREATE) 175 +- **MySQL 8.x** 实例已就绪(host / 库名 / 凭据取自 `config-vars.yaml` 的 `database:` 段,由你填写并完全信任;本项目只面向开发/沙盒环境,`setup-test-db.mjs` 会按该值 DROP+CREATE)。生成的 `scripts/seed-demo-data.mjs`(演示种子注入)同走 `mysql` CLI、同读该 `database:` 段,故 `mysql` 客户端须在 PATH
171 - **`mysql2`(目标项目侧)**:A4 `db-init` 经 `lib/apply-ddl.mjs` 用 mysql2 连接 + 解析 config-vars.yaml `database:` 段 apply V1;生成的 `scripts/setup-test-db.mjs` 在测试闸门前后 drop+create 空库 176 - **`mysql2`(目标项目侧)**:A4 `db-init` 经 `lib/apply-ddl.mjs` 用 mysql2 连接 + 解析 config-vars.yaml `database:` 段 apply V1;生成的 `scripts/setup-test-db.mjs` 在测试闸门前后 drop+create 空库
172 - **Spring Boot + Flyway**(**必需**):build.gradle 声明 `flyway-core` + `flyway-mysql`;Spring 启动时自动 apply `sql/migrations/V*.sql`。本插件生成的 `setup-test-db.mjs` 只清库,schema 必须由 Flyway 应用 177 - **Spring Boot + Flyway**(**必需**):build.gradle 声明 `flyway-core` + `flyway-mysql`;Spring 启动时自动 apply `sql/migrations/V*.sql`。本插件生成的 `setup-test-db.mjs` 只清库,schema 必须由 Flyway 应用
173 - **本地 git 仓库**(纯本地,无需远程):A0 `project-init` 执行 `git init`;B 阶段每模块由 `coding.mjs` 的 milestone stage 本地 `git merge --no-ff` 进默认分支并 `git tag -a milestone/<id>`,完成信号由 `git tag -l` 判定。**不依赖任何远程仓库 / push / GitLab** 178 - **本地 git 仓库**(纯本地,无需远程):A0 `project-init` 执行 `git init`;B 阶段每模块由 `coding.mjs` 的 milestone stage 本地 `git merge --no-ff` 进默认分支并 `git tag -a milestone/<id>`,完成信号由 `git tag -l` 判定。**不依赖任何远程仓库 / push / GitLab**
lib/seed-demo-data-template.test.mjs 0 → 100644
  1 +// lib/seed-demo-data-template.test.mjs — 校验生成模板 scripts/seed-demo-data.mjs 的演示种子注入逻辑。
  2 +// 跑的是真实模板产物:复制到临时 scripts/ 下、写一个 ../config-vars.yaml、可选写 sql/seed/*.sql、再 node 执行。
  3 +// 所有会触达 DB 的用例 host/port 故意指向 127.0.0.1:1(必拒连),不会触碰真实库。
  4 +import { test } from 'node:test'
  5 +import assert from 'node:assert/strict'
  6 +import { spawnSync } from 'node:child_process'
  7 +import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync } from 'node:fs'
  8 +import { tmpdir } from 'node:os'
  9 +import { join } from 'node:path'
  10 +import { fileURLToPath } from 'node:url'
  11 +
  12 +const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/scripts-seed-demo-data-template.mjs', import.meta.url))
  13 +
  14 +// 搭一个临时目标项目:scripts/seed-demo-data.mjs + config-vars.yaml + 可选 sql/seed/*.sql,然后跑脚本。
  15 +// seedFiles 为 null 表示不创建 sql/seed 目录;为对象 { name: content } 表示创建目录并写入这些文件({} = 空目录)。
  16 +function run({
  17 + host = '127.0.0.1',
  18 + port = '1',
  19 + user = 'root',
  20 + password = 'x',
  21 + schemaLine = 'schema: erp_dev',
  22 + seedFiles = null,
  23 +} = {}) {
  24 + const dir = mkdtempSync(join(tmpdir(), 'erp-seed-'))
  25 + mkdirSync(join(dir, 'scripts'))
  26 + copyFileSync(TEMPLATE, join(dir, 'scripts', 'seed-demo-data.mjs'))
  27 + writeFileSync(
  28 + join(dir, 'config-vars.yaml'),
  29 + ['database:', ` host: ${host}`, ` port: ${port}`, ` user: ${user}`, ` password: ${password}`, ' ' + schemaLine, ''].join('\n'),
  30 + )
  31 + if (seedFiles !== null) {
  32 + const seedDir = join(dir, 'sql', 'seed')
  33 + mkdirSync(seedDir, { recursive: true })
  34 + for (const [name, content] of Object.entries(seedFiles)) {
  35 + writeFileSync(join(seedDir, name), content)
  36 + }
  37 + }
  38 + return spawnSync('node', [join(dir, 'scripts', 'seed-demo-data.mjs')], { encoding: 'utf8' })
  39 +}
  40 +
  41 +test('seed-demo-data: unfilled 【人工填写】 config placeholders fail before mysql is invoked', () => {
  42 + for (const cfg of [
  43 + { host: '【人工填写:MySQL host】' },
  44 + { port: '【人工填写:MySQL port】' },
  45 + { user: '【人工填写:账号】' },
  46 + { password: '【人工填写:密码】' },
  47 + { schemaLine: 'schema: 【人工填写:schema 名】' },
  48 + ]) {
  49 + const r = run(cfg)
  50 + assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
  51 + assert.match(r.stderr, /仍是占位/, 'stderr: ' + r.stderr)
  52 + }
  53 +})
  54 +
  55 +test('seed-demo-data: empty schema fails before mysql is invoked', () => {
  56 + const r = run({ schemaLine: 'schema:' })
  57 + assert.equal(r.status, 1)
  58 + assert.match(r.stderr, /database\.schema/, '应是 schema 缺失报错而非连库失败 — stderr: ' + r.stderr)
  59 +})
  60 +
  61 +test('seed-demo-data: missing sql/seed directory exits 0 with no-seed message', () => {
  62 + const r = run({ seedFiles: null })
  63 + assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
  64 + assert.match(r.stdout, /无种子文件/, 'stdout: ' + r.stdout)
  65 +})
  66 +
  67 +test('seed-demo-data: empty sql/seed directory exits 0 with no-seed message', () => {
  68 + const r = run({ seedFiles: {} })
  69 + assert.equal(r.status, 0, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
  70 + assert.match(r.stdout, /无种子文件/, 'stdout: ' + r.stdout)
  71 +})
  72 +
  73 +test('seed-demo-data: illegal seed filename fails before mysql is invoked', () => {
  74 + const r = run({ seedFiles: { '01 bad.sql': '-- expect: t=0\n' } })
  75 + assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
  76 + assert.match(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr)
  77 + assert.match(r.stderr, /01 bad\.sql/, '应列出违规文件名 — stderr: ' + r.stderr)
  78 + // 文件名校验在任何 mysql 调用之前 —— 不应出现已进入 DB 阶段的日志。
  79 + assert.doesNotMatch(r.stdout, /目标库/, 'stdout: ' + r.stdout)
  80 +})
  81 +
  82 +// 缺 <NN>__ 结构的文件名(会被旧宽松正则放过,但违反 <NN>__<module_id>.sql 契约)必须被拒。
  83 +test('seed-demo-data: filenames missing the <NN>__ structure are rejected', () => {
  84 + for (const bad of [
  85 + 'inventory.sql', // 无 NN__ 前缀
  86 + '1__x.sql', // 单位数 NN
  87 + '001__x.sql', // 三位数 NN
  88 + '01-x.sql', // 连字符而非双下划线
  89 + '01_x.sql', // 单下划线而非双下划线
  90 + '01__.sql', // module_id 为空
  91 + '01__bad-id.sql', // module_id 含连字符(非 [A-Za-z0-9_])
  92 + ]) {
  93 + const r = run({ seedFiles: { [bad]: '-- expect: t=0\n' } })
  94 + assert.equal(r.status, 1, `${bad} 应被拒 — stdout: ${r.stdout} stderr: ${r.stderr}`)
  95 + assert.match(r.stderr, /非法种子文件名/, `${bad} — stderr: ${r.stderr}`)
  96 + // 文件名校验在任何 mysql 调用之前 —— 不应进入 DB 阶段。
  97 + assert.doesNotMatch(r.stdout, /目标库/, `${bad} — stdout: ${r.stdout}`)
  98 + }
  99 +})
  100 +
  101 +// 合法 <NN>__<module_id>.sql 通过文件名校验后进入 DB 阶段(验证收紧后的正则不误伤合法名)。
  102 +test('seed-demo-data: well-formed <NN>__<module_id>.sql passes filename check', () => {
  103 + const r = run({ seedFiles: { '01__inventory.sql': '-- expect: inventory_item=3\nSELECT 1;\n' } })
  104 + assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
  105 + assert.doesNotMatch(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr)
  106 + assert.match(r.stdout, /目标库 erp_dev/, '应已进入 DB 阶段 — stdout: ' + r.stdout)
  107 +})
  108 +
  109 +test('seed-demo-data: valid seed + port 1 passes local checks then fails at DB connect', () => {
  110 + const r = run({ seedFiles: { '01__inventory.sql': '-- expect: inventory_item=3\nSELECT 1;\n' } })
  111 + assert.equal(r.status, 1, 'stdout: ' + r.stdout + ' stderr: ' + r.stderr)
  112 + // 已通过全部本地校验、进入 DB 阶段:应打印目标库摘要,且报错是连库失败(非占位/非法名)。
  113 + assert.match(r.stdout, /目标库 erp_dev/, '应已进入 DB 阶段 — stdout: ' + r.stdout)
  114 + assert.doesNotMatch(r.stderr, /仍是占位/, 'stderr: ' + r.stderr)
  115 + assert.doesNotMatch(r.stderr, /非法种子文件名/, 'stderr: ' + r.stderr)
  116 +})
skills/plan/db-init/SKILL.md
@@ -75,12 +75,24 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs&quot; config-vars.yaml sql/migrations/V @@ -75,12 +75,24 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs&quot; config-vars.yaml sql/migrations/V
75 ``` 75 ```
76 76
77 退出码与处理: 77 退出码与处理:
78 -- `0` → 成功,进入步骤 D 78 +- `0` → 成功,进入步骤 B.3
79 - `1` → 失败:打印 stderr 并停下 79 - `1` → 失败:打印 stderr 并停下
80 - `2` → 用法错(路径找不到),打印路径并停下 80 - `2` → 用法错(路径找不到),打印路径并停下
81 81
82 勾选:` - [ ] setup-test-db.mjs DROP+CREATE + apply V1 已执行` 82 勾选:` - [ ] setup-test-db.mjs DROP+CREATE + apply V1 已执行`
83 83
  84 +#### B.3 清库交还 Flyway
  85 +
  86 +B.1 → B.2 是脱离 Flyway 的 DDL 烟测(直接 mysql2 灌入 V1,**绕过 Flyway**,库里没有 `flyway_schema_history` 表)。这种状态会卡住后端首次启动:若后端集成测试连真库,Spring Boot 启动时 Flyway 会对一个**非空且无 schema 历史表**的库报 `Found non-empty schema "<db>" without schema history table`。所以烟测做完要把产物清掉、把 schema 交还 Flyway,让它在后端启动时自行重放 migration。
  87 +
  88 +再跑一次 `setup-test-db.mjs`(DROP+CREATE,把 B.2 灌入的烟测产物连同 schema 一并清空):
  89 +
  90 +```bash
  91 +node scripts/setup-test-db.mjs
  92 +```
  93 +
  94 +> B.2 的 DDL 烟测语义不变——它仍是「V1 能否被真库接受」的一次性验证;B.3 只是把烟测留下的非 Flyway 状态清掉,schema 由后端启动时的 Flyway 重新建立。
  95 +
84 ### C. 勾选 docs/08 进度 + 进入 A5 96 ### C. 勾选 docs/08 进度 + 进入 A5
85 97
86 1. 勾选 A4 顶层(5 维一致已由 A.3 的 `validate-ddl.mjs` 校验过,apply 不改 V1,无需复校): 98 1. 勾选 A4 顶层(5 维一致已由 A.3 的 `validate-ddl.mjs` 校验过,apply 不改 V1,无需复校):
skills/plan/project-init/templates/CLAUDE-template.md
@@ -38,6 +38,15 @@ @@ -38,6 +38,15 @@
38 38
39 --- 39 ---
40 40
  41 +## 🌱 演示数据(demo seed)
  42 +
  43 +1. **人工验收/演示三步走**:`node scripts/setup-test-db.mjs`(DROP+CREATE 空库)→ 起后端(Spring Boot 启动时 Flyway 建 schema)→ `node scripts/seed-demo-data.mjs`(注入演示种子)
  44 +2. **种子来源**:`sql/seed/*.sql` 由 B 阶段每个后端模块完成后自动生成、随 git 提交;演示数据用确定性显式主键,区间 `1000–9999`
  45 +3. **数据不持久**:库会被各测试闸门 DROP+CREATE 随时重建——种子不保证存活,只保证随时可复现;重建后按上述时序重新注入即可
  46 +4. **幂等账本**:账本表 `_demo_seed_history` 由 `seed-demo-data.mjs` 自建自管,重复执行自动跳过已应用文件
  47 +
  48 +---
  49 +
41 ## 🗂️ Git 提交规范 50 ## 🗂️ Git 提交规范
42 51
43 每次提交必须遵循以下格式: 52 每次提交必须遵循以下格式:
skills/plan/skeleton-gen/SKILL.md
@@ -44,8 +44,11 @@ docs/04 已由 scope-lock 写入 § 零。本步骤追加 § 一 ~ 三: @@ -44,8 +44,11 @@ docs/04 已由 scope-lock 写入 § 零。本步骤追加 § 一 ~ 三:
44 | 模板 | 目标路径 | 44 | 模板 | 目标路径 |
45 |---|---| 45 |---|---|
46 | `${CLAUDE_SKILL_DIR}/templates/scripts-setup-test-db-template.mjs` | `scripts/setup-test-db.mjs` | 46 | `${CLAUDE_SKILL_DIR}/templates/scripts-setup-test-db-template.mjs` | `scripts/setup-test-db.mjs` |
  47 +| `${CLAUDE_SKILL_DIR}/templates/scripts-seed-demo-data-template.mjs` | `scripts/seed-demo-data.mjs` |
47 | `${CLAUDE_SKILL_DIR}/templates/styles-tokens-template.css` | `src/styles/tokens.css` | 48 | `${CLAUDE_SKILL_DIR}/templates/styles-tokens-template.css` | `src/styles/tokens.css` |
48 49
  50 +> `scripts/seed-demo-data.mjs`:把 Coding 阶段逐模块生成的 `sql/seed/*.sql` 演示种子注入**已建好 schema** 的库;用 `_demo_seed_history` 账本表记录已应用文件以保证幂等(重复执行自动跳过);调用方 = 前端 e2e 的 globalSetup / 行为门 / 里程碑后人工验收。
  51 +
49 > `sql/migrations/` 目录不在此预建:A4 `db-init` 写 `V1__initial_schema.sql` 时 `Write` 会自动创建。 52 > `sql/migrations/` 目录不在此预建:A4 `db-init` 写 `V1__initial_schema.sql` 时 `Write` 会自动创建。
50 53
51 > 凭据 / 配置不在此生成:项目**全部配置**(含 DB 凭据 / 密钥)已由 A1 `scope-lock` 写入仓库根 `config-vars.yaml`;`scripts/setup-test-db.mjs` 运行时按 2 层 map 直接读它。 54 > 凭据 / 配置不在此生成:项目**全部配置**(含 DB 凭据 / 密钥)已由 A1 `scope-lock` 写入仓库根 `config-vars.yaml`;`scripts/setup-test-db.mjs` 运行时按 2 层 map 直接读它。
@@ -122,6 +125,7 @@ QA 横幅涵盖:产出文件清单(docs/04 + scripts/*.mjs + .gitignore;co @@ -122,6 +125,7 @@ QA 横幅涵盖:产出文件清单(docs/04 + scripts/*.mjs + .gitignore;co
122 - `${CLAUDE_SKILL_DIR}/templates/docs-04-skeleton-template.md` 125 - `${CLAUDE_SKILL_DIR}/templates/docs-04-skeleton-template.md`
123 - `${CLAUDE_SKILL_DIR}/templates/scripts-test-template.mjs` 126 - `${CLAUDE_SKILL_DIR}/templates/scripts-test-template.mjs`
124 - `${CLAUDE_SKILL_DIR}/templates/scripts-setup-test-db-template.mjs` 127 - `${CLAUDE_SKILL_DIR}/templates/scripts-setup-test-db-template.mjs`
  128 +- `${CLAUDE_SKILL_DIR}/templates/scripts-seed-demo-data-template.mjs`
125 - `config-vars.yaml`(A1 产出,含 DB 凭据;setup-test-db.mjs 运行时读取) 129 - `config-vars.yaml`(A1 产出,含 DB 凭据;setup-test-db.mjs 运行时读取)
126 - `${CLAUDE_SKILL_DIR}/templates/gitignore-append-template` 130 - `${CLAUDE_SKILL_DIR}/templates/gitignore-append-template`
127 - `${CLAUDE_SKILL_DIR}/templates/styles-tokens-template.css` 131 - `${CLAUDE_SKILL_DIR}/templates/styles-tokens-template.css`
skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md
@@ -35,3 +35,11 @@ @@ -35,3 +35,11 @@
35 ### 3.3 日期与金额 35 ### 3.3 日期与金额
36 36
37 ### 3.4 数据访问规约 37 ### 3.4 数据访问规约
  38 +
  39 +### 3.5 数据基线与演示种子
  40 +- 演示种子 SQL 放 `sql/seed/`,命名 `<NN>__<module_id>.sql`(NN=两位序号,按模块构建顺序;随 git 提交)。
  41 +- 注入由 `scripts/seed-demo-data.mjs` 负责:B 阶段每个后端模块完成后生成对应 seed 文件,脚本逐文件按名升序应用。
  42 +- 主键区间约定:`1–999`=初始数据(admin_init 等)/ `1000–9999`=演示种子 / `≥100000`=行为验收 sentinel;三段互不重叠,演示数据值不得含 `_S<数字>` 编码串(预留给 sentinel)。
  43 +- 注入时序恒为:`node scripts/setup-test-db.mjs`(DROP+CREATE 空库)→ 起后端(Flyway 建 schema)→ `node scripts/seed-demo-data.mjs`。
  44 +- e2e 基线 = 演示种子已注入(前端 Playwright globalSetup 走上述时序);后端单测/集成测基线 = 空库,不注入种子。
  45 +- 幂等账本表 `_demo_seed_history` 由 `seed-demo-data.mjs` 自建自管,已应用文件自动跳过;库被各测试闸门 DROP+CREATE 重建后按上述时序重新注入即可(数据可复现、不持久)。
skills/plan/skeleton-gen/templates/scripts-seed-demo-data-template.mjs 0 → 100644
  1 +#!/usr/bin/env node
  2 +// scripts/seed-demo-data.mjs —— 演示假数据(demo seed)的注入脚本。
  3 +//
  4 +// 用途(四个调用方,时序均为:空库重建 → 起后端让 Flyway 建 schema → 本脚本注入):
  5 +// 1) 前端 e2e(Playwright)globalSetup —— e2e 基线 = 空库 + Flyway schema + 演示种子;
  6 +// 2) coding.mjs 行为门 step2.3 —— 行为验收前注入演示数据;
  7 +// 3) 里程碑后人工验收 / 演示 —— 手动跑一次即可复现演示态;
  8 +// 4) coding.mjs Seed stage —— 模块种子生成后冷起栈真跑验证。
  9 +//
  10 +// 前提:schema 必须已由 Flyway 在 Spring Boot 启动时建好——本脚本绝不建 schema,只灌数据。
  11 +// 幂等机制:已应用的种子文件记入账本表 _demo_seed_history(file 为主键),再次运行自动跳过。
  12 +// 主键区间约定:1–999=初始数据(admin_init 等)/ 1000–9999=演示种子 / ≥100000=行为门 sentinel。
  13 +//
  14 +// DB 凭据从仓库根 config-vars.yaml 的 database: 段读取;host / user / password 信任该文件,port 仅校验范围。
  15 +// 纯 mysql CLI(spawnSync),零 npm 依赖。退出码:0 成功(含全跳过 / 无文件),1 失败。
  16 +
  17 +import { spawnSync } from 'node:child_process'
  18 +import { existsSync, readFileSync, readdirSync } from 'node:fs'
  19 +import { dirname, join } from 'node:path'
  20 +import { fileURLToPath } from 'node:url'
  21 +
  22 +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
  23 +const PROJECT_ROOT = join(SCRIPT_DIR, '..')
  24 +const CONFIG_FILE = join(PROJECT_ROOT, 'config-vars.yaml')
  25 +const SEED_DIR = join(PROJECT_ROOT, 'sql', 'seed')
  26 +
  27 +const LOG = '[seed-demo-data]'
  28 +
  29 +// 极简 YAML 读取(2 层 map + 标量;与 scripts/setup-test-db.mjs 同规则,内联以免运行时依赖)。
  30 +function parseScalar(raw) {
  31 + let s = String(raw).trim()
  32 + if (s === '' || s[0] === '#') return ''
  33 + const q = s[0]
  34 + if (q === '"' || q === "'") {
  35 + const end = s.indexOf(q, 1)
  36 + if (end !== -1) return s.slice(1, end)
  37 + }
  38 + const hash = s.indexOf(' #')
  39 + if (hash !== -1) s = s.slice(0, hash).trim()
  40 + return s
  41 +}
  42 +function parseYamlConfig(text) {
  43 + const root = {}
  44 + let section = null
  45 + for (const rawLine of text.split('\n')) {
  46 + const line = rawLine.replace(/\r$/, '')
  47 + const trimmed = line.trim()
  48 + if (trimmed === '' || trimmed[0] === '#') continue
  49 + const colon = line.indexOf(':')
  50 + if (colon === -1) continue
  51 + const key = line.slice(0, colon).trim()
  52 + if (key === '') continue
  53 + const indent = line.length - line.replace(/^\s+/, '').length
  54 + const value = parseScalar(line.slice(colon + 1))
  55 + if (indent === 0) {
  56 + if (value === '') {
  57 + section = {}
  58 + root[key] = section
  59 + } else {
  60 + root[key] = value
  61 + section = null
  62 + }
  63 + } else if (section) {
  64 + section[key] = value
  65 + } else {
  66 + root[key] = value
  67 + }
  68 + }
  69 + return root
  70 +}
  71 +
  72 +// 单引号字符串字面量转义(用于喂给 mysql 的 SQL 值,如 table_schema / file 名)。
  73 +function quoteSqlString(value) {
  74 + return "'" + String(value).replaceAll('\\', '\\\\').replaceAll("'", "''") + "'"
  75 +}
  76 +
  77 +// ──────────────────────────────────────────────────────────────────────────
  78 +// ① config-vars 校验(占位拒绝 / port 范围 / schema 非空,照抄 setup-test-db 模式)
  79 +// 所有本地校验前置于任何 mysql 调用,保证离线可测。
  80 +// ──────────────────────────────────────────────────────────────────────────
  81 +
  82 +if (!existsSync(CONFIG_FILE)) {
  83 + console.error(`${LOG} config-vars.yaml 不存在(${CONFIG_FILE})`)
  84 + process.exit(1)
  85 +}
  86 +
  87 +const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {}
  88 +
  89 +const DB_HOST = db.host ?? ''
  90 +const DB_PORT = db.port ?? '3306'
  91 +const DB_USER = db.user ?? ''
  92 +const DB_PASSWORD = db.password ?? ''
  93 +const DB_SCHEMA = db.schema ?? ''
  94 +
  95 +function rejectPlaceholder(key, value) {
  96 + if (typeof value === 'string' && value.includes('【人工填写')) {
  97 + console.error(`${LOG} database.${key} 仍是占位,请先在 config-vars.yaml 填真实值(database.password 可填 '' 表示空密码)`)
  98 + process.exit(1)
  99 + }
  100 +}
  101 +
  102 +for (const [key, value] of [['host', DB_HOST], ['port', DB_PORT], ['user', DB_USER], ['password', DB_PASSWORD], ['schema', DB_SCHEMA]]) {
  103 + rejectPlaceholder(key, value)
  104 +}
  105 +
  106 +if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) {
  107 + console.error(`${LOG} database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`)
  108 + process.exit(1)
  109 +}
  110 +
  111 +if (String(DB_SCHEMA).trim() === '') {
  112 + console.error(`${LOG} database.schema 未填`)
  113 + process.exit(1)
  114 +}
  115 +
  116 +// ──────────────────────────────────────────────────────────────────────────
  117 +// ② 列 sql/seed/*.sql 升序(确定性显式 .sort());校验文件名。
  118 +// ──────────────────────────────────────────────────────────────────────────
  119 +
  120 +if (!existsSync(SEED_DIR)) {
  121 + console.log(`${LOG} 无种子文件(目录不存在: ${SEED_DIR}),无需注入`)
  122 + process.exit(0)
  123 +}
  124 +
  125 +// 文件名契约:<NN>__<module_id>.sql —— NN 为两位序号、module_id 为 [A-Za-z0-9_]+,中间双下划线分隔。
  126 +const SEED_NAME_RE = /^[0-9]{2}__[A-Za-z0-9_]+\.sql$/
  127 +const seedFiles = readdirSync(SEED_DIR).filter((name) => name.toLowerCase().endsWith('.sql')).sort()
  128 +
  129 +if (seedFiles.length === 0) {
  130 + console.log(`${LOG} 无种子文件(${SEED_DIR} 为空),无需注入`)
  131 + process.exit(0)
  132 +}
  133 +
  134 +const illegal = seedFiles.filter((name) => !SEED_NAME_RE.test(name))
  135 +if (illegal.length > 0) {
  136 + console.error(`${LOG} 发现非法种子文件名(要求匹配 <NN>__<module_id>.sql,即 /^[0-9]{2}__[A-Za-z0-9_]+\\.sql$/):`)
  137 + for (const name of illegal) console.error(`${LOG} - ${name}`)
  138 + process.exit(1)
  139 +}
  140 +
  141 +// ──────────────────────────────────────────────────────────────────────────
  142 +// 以下进入 DB 阶段。目标库名作为 mysql 位置参数原样传值。
  143 +// ──────────────────────────────────────────────────────────────────────────
  144 +
  145 +// 查询型调用:mysql -N -B -e,捕获 stdout(utf8);目标库作为位置参数。
  146 +function mysqlQuery(sql) {
  147 + return spawnSync(
  148 + 'mysql',
  149 + [`--host=${DB_HOST}`, `--port=${DB_PORT}`, `--user=${DB_USER}`, `--password=${DB_PASSWORD}`, '-N', '-B', '-e', sql, DB_SCHEMA],
  150 + { encoding: 'utf8' },
  151 + )
  152 +}
  153 +
  154 +// 应用型调用:把文件内容喂 stdin(--comments 防剥注释);目标库作为位置参数。
  155 +function mysqlApply(sqlText) {
  156 + return spawnSync(
  157 + 'mysql',
  158 + [`--host=${DB_HOST}`, `--port=${DB_PORT}`, `--user=${DB_USER}`, `--password=${DB_PASSWORD}`, '--comments', DB_SCHEMA],
  159 + { input: sqlText, encoding: 'utf8' },
  160 + )
  161 +}
  162 +
  163 +function fatalMysql(res, label) {
  164 + if (res.error) {
  165 + console.error(`${LOG} FATAL: 无法执行 mysql(请确认其在 PATH 中): ${res.error.message}`)
  166 + process.exit(1)
  167 + }
  168 + if (res.status !== 0) {
  169 + if (res.stderr) process.stderr.write(res.stderr)
  170 + console.error(`${LOG} FAIL (${label}): mysql exit=${res.status}`)
  171 + process.exit(res.status === null ? 1 : res.status)
  172 + }
  173 +}
  174 +
  175 +console.log(`${LOG} 目标库 ${DB_SCHEMA} on ${DB_HOST}:${DB_PORT},待处理种子 ${seedFiles.length} 个`)
  176 +
  177 +// ──────────────────────────────────────────────────────────────────────────
  178 +// ③ 查 flyway_schema_history(information_schema)是否存在 —— 不存在说明 schema 未建。
  179 +// ──────────────────────────────────────────────────────────────────────────
  180 +
  181 +const flywayCheck = mysqlQuery(
  182 + `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ${quoteSqlString(DB_SCHEMA)} AND table_name = 'flyway_schema_history'`,
  183 +)
  184 +fatalMysql(flywayCheck, 'check-flyway')
  185 +if (flywayCheck.stdout.trim() !== '1') {
  186 + console.error(`${LOG} schema 未初始化(${DB_SCHEMA} 中找不到 flyway_schema_history)——请先起后端让 Flyway 建 schema,再注入种子`)
  187 + process.exit(1)
  188 +}
  189 +
  190 +// ──────────────────────────────────────────────────────────────────────────
  191 +// ④ 账本表 _demo_seed_history(已应用文件账本,幂等核心)。
  192 +// ──────────────────────────────────────────────────────────────────────────
  193 +
  194 +const createLedger = mysqlApply(
  195 + 'CREATE TABLE IF NOT EXISTS _demo_seed_history (' +
  196 + ' file VARCHAR(255) NOT NULL PRIMARY KEY,' +
  197 + ' applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP' +
  198 + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
  199 +)
  200 +fatalMysql(createLedger, 'create-ledger')
  201 +
  202 +// 读出已应用文件集合。
  203 +const appliedRes = mysqlQuery('SELECT file FROM _demo_seed_history')
  204 +fatalMysql(appliedRes, 'read-ledger')
  205 +const applied = new Set(
  206 + appliedRes.stdout.split('\n').map((s) => s.trim()).filter((s) => s !== ''),
  207 +)
  208 +
  209 +// ──────────────────────────────────────────────────────────────────────────
  210 +// ⑤ 逐文件按文件名升序应用(已应用跳过;失败 exit 1 透传 stderr;成功后写账本)。
  211 +// ──────────────────────────────────────────────────────────────────────────
  212 +
  213 +let appliedCount = 0
  214 +let skippedCount = 0
  215 +
  216 +for (const name of seedFiles) {
  217 + if (applied.has(name)) {
  218 + console.log(`${LOG} 跳过(已应用): ${name}`)
  219 + skippedCount += 1
  220 + continue
  221 + }
  222 +
  223 + console.log(`${LOG} 应用: ${name}`)
  224 + const sqlText = readFileSync(join(SEED_DIR, name), 'utf8')
  225 + // 账本 INSERT 拼到同一批 SQL 末尾、同一次 mysql 调用执行:mysql 批处理遇错即停,
  226 + // 账本行只在前面全部种子语句成功后才落——杜绝「已应用未记账 → 重跑重复插入」的半截状态。
  227 + const body = sqlText.trimEnd()
  228 + const sep = /;$/.test(body) ? '\n' : ';\n'
  229 + const applyRes = mysqlApply(`${body}${sep}INSERT INTO _demo_seed_history (file) VALUES (${quoteSqlString(name)});`)
  230 + fatalMysql(applyRes, `apply ${name}`)
  231 +
  232 + appliedCount += 1
  233 +}
  234 +
  235 +console.log(`${LOG} done — applied=${appliedCount} skipped=${skippedCount}(共 ${seedFiles.length} 个)`)
workflows/coding.mjs
@@ -9,7 +9,7 @@ export const meta = { @@ -9,7 +9,7 @@ export const meta = {
9 description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.', 9 description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.',
10 phases: [ 10 phases: [
11 { title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' }, 11 { title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' },
12 - { title: 'Gate' }, { title: 'Milestone' }, 12 + { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' },
13 ], 13 ],
14 // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门, 14 // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门,
15 // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。 15 // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。
@@ -391,6 +391,9 @@ function tddPrompt(id, phase, planPath) { @@ -391,6 +391,9 @@ function tddPrompt(id, phase, planPath) {
391 ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' 391 ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。'
392 : '', 392 : '',
393 fe 393 fe
  394 + ? '- **e2e 基线约束**:e2e 跑在「空库重建 + Flyway schema + 演示种子」基线上(骨架 globalSetup 已注入 `sql/seed`,无需测试自行建库/起栈)。e2e 断言**优先**定位**演示种子已知主键行**(1000–9999)或**测试自建数据**;**禁止**「全表恰好 N 行」式依赖全局计数的脆弱断言(演示种子行数会随后续模块种子增长,全局计数断言必然 flaky)。'
  395 + : '',
  396 + fe
394 ? `- **占位替换(保证中途可构建 + per-FE 行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/\` router 定位本 FE 路由 path 的 import 行;仍在 \`frontend/\` 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` 397 ? `- **占位替换(保证中途可构建 + per-FE 行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/\` router 定位本 FE 路由 path 的 import 行;仍在 \`frontend/\` 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。`
395 : '', 398 : '',
396 '', 399 '',
@@ -546,6 +549,96 @@ function gatePrompt(module, phase, attempt = 1) { @@ -546,6 +549,96 @@ function gatePrompt(module, phase, attempt = 1) {
546 ].filter(Boolean).join('\n') 549 ].filter(Boolean).join('\n')
547 } 550 }
548 551
  552 +// ---- 演示种子生成 stage(Seed)----
  553 +// 设计:每个后端模块 testGate green 之后生成本模块演示假数据(demo seed)并冷起栈真跑验证。
  554 +// 与 behaviorGateContract 同属「跨栈 stage 不套 featureStageContract」的**第三类 stage**:本门要**运行**
  555 +// scripts/setup-test-db.mjs / 起后端 / scripts/seed-demo-data.mjs(featureStageContract('backend') 的路径护栏
  556 +// 会把 scripts/ 命中越界硬停,与本门必须运行这些脚本自相矛盾),故另起 seedStageContract 自带契约。
  557 +// 锁定契约(与 A2/A4/test.mjs/行为门一致):种子文件 `sql/seed/<NN>__<module_id>.sql`(随 git 提交);头部
  558 +// 机器可读行 `-- demo-seed: <module_id>` + 每表一行 `-- expect: <table>=<rows>`;主键区间 1000–9999;
  559 +// 演示数据值绝不含 `_S<数字>` 样式(预留行为门 sentinel);注入脚本 scripts/seed-demo-data.mjs(A2 已生成)。
  560 +
  561 +// seedStageContract:种子 stage 的硬约束。非交互;证据报告用中文但 SQL/标识符可英文(受控格式);
  562 +// 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端 / scripts/seed-demo-data.mjs / mysql 只读查询,
  563 +// 唯一**可写** = sql/seed/ + .tmp/seed-gen/<module_id>/(跑完即弃)+ docs/superpowers/module-reports/<module_id>-seed-verify.md;
  564 +// 改 backend//frontend//scripts/ 源码即越界硬停。
  565 +function seedStageContract() {
  566 + return [
  567 + '## 硬约束(非交互演示种子子代理)',
  568 + '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。',
  569 + '- 你的职责 = **为本模块生成演示种子(demo seed)并冷起栈真跑验证**——**不是**实现功能、**不是**改源码、**不是**改 schema。',
  570 + '- 缺值查找顺序:`config-vars.yaml` → `docs/03-数据库设计文档.md` → `docs/01-需求清单/` 各 REQ 卡(业务语义)→ 既有 `sql/seed/*`(跨模块 FK 引用前序模块种子的已知主键)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。',
  571 + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下——\`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)、起后端服务(gradle bootRun 等,Flyway 在此建 schema)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(注入种子)、mysql **只读** COUNT/查询;唯一允许**写入**的路径是 \`${ROOT}/sql/seed/\`(种子文件,随 git 提交)+ \`${ROOT}/.tmp/seed-gen/<module_id>/\`(一次性 runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/<module_id>-seed-verify.md\`。`,
  572 + `- **越界硬停**:**绝不**编辑 \`backend/\` / \`frontend/\` / \`scripts/\` 下的任何源码文件(只许**运行** scripts/setup-test-db.mjs 与 scripts/seed-demo-data.mjs,不许改它们)。区分「运行 backend 服务 / 运行脚本」(允许)与「写 backend 实现 / 改脚本」(越界)。命中越界即以 \`status:halt\` 写清阻塞点结束。`,
  573 + '- **确定性红线(关键)**:种子值一律**显式主键**(1000–9999 区间)+ **固定历史日期**(写死字面量,如 `2024-03-15`),**绝不**依赖时间戳 / `NOW()` / 随机数 / 自增主键的隐式取值。',
  574 + '- **区间隔离红线**:演示数据值**绝不含 `_S<数字>` 样式编码串**(如 `CUST_NAME_S001`)——该样式预留给行为门 sentinel;数值主键固定落 1000–9999(1–999=初始数据 / ≥100000=sentinel)。',
  575 + '- 红线:**绝不**伪造验证通过;**绝不**留 `TBD` / `TODO` / `【人工填写:】`;自主默认必须可被现有证据支撑且记入 `decisions[]`。',
  576 + '- 证据报告 / 注释 / 提示**使用中文**;SQL / 标识符 / 表名可用英文(受控 `[A-Za-z0-9_]` 格式)。',
  577 + ].join('\n')
  578 +}
  579 +
  580 +// seedGenPrompt:单模块演示种子生成 + 冷起栈真跑验证的完整流水线提示。
  581 +// module:本后端模块(含 id);本 stage 在该模块 testGate green 之后跑(schema 含 tdd 新增 V<n> 已终态全绿)。
  582 +function seedGenPrompt(module) {
  583 + const id = module?.id ?? '<module>'
  584 + const tmpDir = `${ROOT}/.tmp/seed-gen/${id}`
  585 + const evidence = `docs/superpowers/module-reports/${id}-seed-verify.md`
  586 + return [
  587 + `# seed — 演示种子生成 + 冷起栈真跑验证(模块 ${id})`,
  588 + '',
  589 + seedStageContract(),
  590 + '',
  591 + '## 目标',
  592 + `为本模块 \`${id}\` 生成**演示假数据(demo seed)**并冷起栈真跑验证:生成 → \`node ${ROOT}/scripts/setup-test-db.mjs\` → 起后端(Flyway 建 schema)→ \`node ${ROOT}/scripts/seed-demo-data.mjs\` → mysql 只读 COUNT 对账。`,
  593 + '种子产物随 git 提交(不保证「存活」,保证「随时可复现」——三处 DROP+CREATE 各在自己时序里固定重注入)。',
  594 + '',
  595 + '## 输入',
  596 + `- \`${ROOT}/docs/03-数据库设计文档.md\`:本模块各表结构(列 / 类型 / enum 值域 / FK / NOT NULL / UNIQUE 约束)。`,
  597 + `- \`${ROOT}/docs/01-需求清单/<module>/\` 本模块 REQ 卡:业务语义(让假数据有真实感、符合业务取值)。`,
  598 + `- 既有 \`${ROOT}/sql/seed/*.sql\`:跨模块 FK 引用前序模块种子的**已知确定性主键**(你的 FK 列必须引用这些已存在的主键,不可悬空)。`,
  599 + `- \`${ROOT}/config-vars.yaml\`:database 段凭据(seed-demo-data.mjs / setup-test-db.mjs 自行读取,你只需确保起栈参数一致)。`,
  600 + '',
  601 + '## 幂等(resume 安全)',
  602 + `- 用 Glob 查 \`${ROOT}/sql/seed/*__${id}.sql\`。**已存在** → **Edit 复用该文件**(保留原 \`NN\` 序号,不另起新文件);按需补齐/修正内容。`,
  603 + `- **不存在** → 新建 \`sql/seed/<NN>__${id}.sql\`,其中 \`NN\` = 既有 \`sql/seed/*.sql\` 文件名最大序号 + 1(两位补零,如既有最大为 \`03\` → 本文件用 \`04\`;无任何既有文件 → \`01\`)。`,
  604 + '',
  605 + '## 生成规则',
  606 + '- **FK 有序**:同一文件内 INSERT 先父后子;跨模块 FK 列引用既有 `sql/seed/*` 中前序模块种子的已知主键。',
  607 + '- **显式主键**:本模块种子行主键固定落 **1000–9999** 区间(避开 1–999 初始数据 / ≥100000 sentinel);同表内主键唯一、确定性。',
  608 + '- **真实感中文业务数据**:依 REQ 卡业务语义取值(人名 / 机构 / 金额 / 状态等),不要 `测试1`/`aaa` 占位;但**绝不含 `_S<数字>` 样式编码**(预留 sentinel)。',
  609 + '- **enum 取值域**:enum 列只从 `docs/03` 声明的值域取值(越界即数据类失败)。',
  610 + '- **固定历史日期**:日期/时间列写死固定历史字面量(如 `2024-03-15 10:00:00`),绝不 `NOW()` / 时间戳。',
  611 + '- **行数**:主业务列表表(页面会分页展示的)给 **15–30 行**(够触发分页 + 行级操作);字典/配置类小表按需少量(够 FK 引用 + 下拉非空)。',
  612 + `- **头部注释(机器可读,验证对账依赖)**:文件头第一行 \`-- demo-seed: ${id}\`;随后**每张被本文件 INSERT 的表各一行** \`-- expect: <table>=<rows>\`(rows = 本文件向该表插入的行数)。`,
  613 + `- **本模块无可种表**(纯计算/无表模块)→ **不建文件**,直接 \`status:ok\` + summary 说明「模块 ${id} 无可种表,跳过」(跳过下面的验证与 commit)。`,
  614 + '',
  615 + '## 运行验证(写一次性 runner,仿行为门冷起栈纪律的简化版)',
  616 + `- **入口清目录**:先用确定性、跨平台方式重建 \`${tmpDir}/\`(\`fs.rmSync(tmpDir,{recursive:true,force:true})\` 后 \`fs.mkdirSync(tmpDir,{recursive:true})\`),仅限该受控路径,绝不删其它路径。`,
  617 + `- 在 \`${tmpDir}/\` 写一次性 runner \`run.mjs\`,依序:`,
  618 + ` 1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。`,
  619 + ` 2) **起后端**:从 \`${ROOT}/config-vars.yaml\` 取端口;起栈前先探测端口占用并按 \`${tmpDir}/*.pid\` / 既知端口回收上一次残留 pid;spawn 到后台进程树 + 轮询健康端点(\`/actuator/health\` 或登录端点 200)就绪(Flyway 在此 apply 建 schema)。`,
  620 + ` 3) \`node ${ROOT}/scripts/seed-demo-data.mjs\`(注入种子;幂等账本 \`_demo_seed_history\` 自动跳过已应用文件)。`,
  621 + ' 4) **mysql 只读 COUNT 对账**:对本模块种子涉及的**每张表**,跑 `SELECT COUNT(*) ... WHERE <主键列> BETWEEN 1000 AND 9999`(**只数演示种子区间**——后端启动可能把 admin_init 等初始数据 bootstrap 进共表,其键落 1–999,不计入 expect),与「全部 `sql/seed/*.sql` 文件头 `-- expect: <table>=<rows>` 之和」逐表比对(同一张表可能被多个种子文件插入,必须求和后再比)。',
  622 + ' - `finally` **硬要求 kill 本 stage 起的全部子进程**(绝不让 gradle bootRun 挂死会话)。',
  623 + '- **失败归类(reason 里必须分清)**:',
  624 + ' - **环境类**(端口占用 / 起栈超时 / setup-test-db 失败 / 健康端点不就绪)→ reason 标 `env-error` + 端口/pid。',
  625 + ' - **数据类**(撞主键/唯一键 / FK 错序或悬空 / enum 越界 / 类型截断 / COUNT 不符)→ reason 标 `data-error` + 具体表与根因(这是种子本身的 bug,必须修种子文件后重验)。',
  626 + '',
  627 + '## 证据落盘',
  628 + `- 写 \`${evidence}\`(中文):逐表「期望行数 / 实际行数 / 结论(match/mismatch)」表格 + 本模块种子文件路径 + 起栈端口 + 关键决策。`,
  629 + '- 若验证失败(环境类或数据类)→ 证据**头部用红字标注根因**(区分环境类 vs 数据类)。',
  630 + '',
  631 + commitBlock(`sql/seed ${evidence}`, `chore(seed:${id}): 演示种子数据`,
  632 + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的种子/证据路径)。'),
  633 + '',
  634 + '## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
  635 + `- 成功(含验证 COUNT 全部对账通过):\`{ "status": "ok", "artifactPath": "sql/seed/<NN>__${id}.sql", "summary": "<种子表数 / 总行数 / 验证结论 ≤ 200 字>" }\`。`,
  636 + `- 本模块无可种表:\`{ "status": "ok", "summary": "模块 ${id} 无可种表,跳过" }\`(artifactPath 可省)。`,
  637 + '- 验证失败 / 越界 / 缺值(无法自洽决策)→ `{ "status": "halt", "reason": "<env-error 或 data-error + 具体根因>", "artifactPath": "<已写入的种子路径(如有)>" }`。',
  638 + '- `artifactPath`(如有)必须为项目根相对路径;做过自主默认 → `decisions[]` 逐条登记;schema 是 `additionalProperties:false`,不要返回额外字段。',
  639 + ].filter(Boolean).join('\n')
  640 +}
  641 +
549 // ---- 前端行为验收(per-FE behavior 子门)---- 642 // ---- 前端行为验收(per-FE behavior 子门)----
550 // 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。 643 // 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。
551 // 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发, 644 // 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发,
@@ -562,7 +655,7 @@ function behaviorGateContract() { @@ -562,7 +655,7 @@ function behaviorGateContract() {
562 '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', 655 '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。',
563 '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', 656 '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。',
564 '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', 657 '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。',
565 - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、${ROOT}/.tmp/behavior-gate/<FE>/r<behaviorRound>/- `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、(spec/种子 SQL/runner,跑完即弃)+ 证据报告 - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、${ROOT}/docs/superpowers/reviews/<date>-<FE>-behavior-r<behaviorRound>-a<attempt>.md- `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、 + 其 assets(截图归档到 - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、${ROOT}/docs/superpowers/reviews/assets/...- `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、)。`, 658 + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、${ROOT}/.tmp/behavior-gate/<FE>/r<behaviorRound>/+ `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、(spec/种子 SQL/runner,跑完即弃)+ 证据报告 + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、${ROOT}/docs/superpowers/reviews/<date>-<FE>-behavior-r<behaviorRound>-a<attempt>.md+ `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、 + 其 assets(截图归档到 + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、${ROOT}/docs/superpowers/reviews/assets/...+ `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、)。`,
566 `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, 659 `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`,
567 '- **per-FE 中途态豁免(关键)**:本门在 **per-FE 模式**下运行——`frontend/` 中**本 FE 之外**的路由/组件可能尚未实现,属预期中途态。遇到指向未建路由的链接 / 404 / 编译缺件(兄弟 FE 或骨架占位未覆盖),一律记 `coverageGaps[reason="build-failed-sibling-unimpl"]` 或 `envError.kind="build-failed"`(按根因路径归属,见 step0/step2),**绝不**归为本 FE 的 `interactionFailures`。**本 FE 路由清单(feScope.routes)是唯一断言作用域**;白名单外 / 共享控件归 coverageGap,不算本 FE 缺陷。', 660 '- **per-FE 中途态豁免(关键)**:本门在 **per-FE 模式**下运行——`frontend/` 中**本 FE 之外**的路由/组件可能尚未实现,属预期中途态。遇到指向未建路由的链接 / 404 / 编译缺件(兄弟 FE 或骨架占位未覆盖),一律记 `coverageGaps[reason="build-failed-sibling-unimpl"]` 或 `envError.kind="build-failed"`(按根因路径归属,见 step0/step2),**绝不**归为本 FE 的 `interactionFailures`。**本 FE 路由清单(feScope.routes)是唯一断言作用域**;白名单外 / 共享控件归 coverageGap,不算本 FE 缺陷。',
568 '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', 661 '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。',
@@ -611,15 +704,16 @@ function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { @@ -611,15 +704,16 @@ function behaviorGatePrompt(id, specPath, behaviorRound, attempt) {
611 '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)', 704 '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)',
612 '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。', 705 '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。',
613 '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。', 706 '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。',
614 - '- 带参动态路由用**种子已知主键**实例化;无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', 707 + '- 带参动态路由用**种子已知主键**实例化(可用**演示种子已知主键**(1000–9999)或 **sentinel 主键**(≥100000));无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。',
615 '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', 708 '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。',
616 '', 709 '',
617 - '## step2 起栈段严格时序(schema 由 Flyway 在后端启动时才建)', 710 + '## step2 起栈段严格时序(schema 由 Flyway 在后端启动时才建)',
618 `1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`, 711 `1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`,
619 '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', 712 '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。',
620 - '3) **此时才跑种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** 种子(先父后子)。失败 → `envError.kind="seed-error"` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不**混进交互 RED。',  
621 - ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——字符串列逐字段唯一编码(如 `CUST_NAME_S001`,抓绑错字段)+ 行序号保 UNIQUE;数值列用高位魔数;enum 列从 docs/03 值域取并标注。插入前扫 Flyway / config-vars 既有初始数据(admin_init 等)键,sentinel 主键偏移到不冲突区;断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。',  
622 - '4) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', 713 + `3) **注入演示种子**:\`node ${ROOT}/scripts/seed-demo-data.mjs\`(幂等账本 \`_demo_seed_history\` 自动跳过已应用文件,把 \`sql/seed/*.sql\` 演示数据注入空库)。失败 → \`envError.kind="seed-error"\` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断 / schema 未初始化),**不**混进交互 RED。`,
  714 + '4) **此时才跑 sentinel 种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** sentinel 种子(先父后子;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)。失败 → `envError.kind="seed-error"` + 结构化根因,**不**混进交互 RED。',
  715 + ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——数值主键**一律 ≥100000**(固定区间,不再动态扫描既有键:初始数据 1–999 / 演示种子 1000–9999 已由区间约定隔离,sentinel 落 ≥100000 天然不冲突);字符串列**仍逐字段唯一编码**(`_S<NNN>` 样式,如 `CUST_NAME_S001`,抓绑错字段——演示数据已被禁用该样式,故 sentinel 独占)+ 行序号保 UNIQUE;enum 列从 docs/03 值域取并标注。断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。',
  716 + '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。',
623 '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。', 717 '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。',
624 '', 718 '',
625 '## step2.5 鉴权 bootstrap(确定性前置)', 719 '## step2.5 鉴权 bootstrap(确定性前置)',
@@ -697,6 +791,11 @@ function frontendSkeletonPrompt(feItems) { @@ -697,6 +791,11 @@ function frontendSkeletonPrompt(feItems) {
697 ' - 路由 path 取自上面推导的 FE→path 映射;带参路由用 `:id` 等占位。', 791 ' - 路由 path 取自上面推导的 FE→path 映射;带参路由用 `:id` 等占位。',
698 '3. **占位组件 `FeStub`**:`frontend/src/views/_stub/FeStub.vue`(framework 非 Vue 时落对应等价文件,如 `FeStub.tsx`),最小渲染一个带 `data-fe-stub` 属性的元素(如 `<div data-fe-stub>占位</div>`;行为门据 `data-fe-stub` 识别占位态)。**不实现任何业务逻辑**。', 792 '3. **占位组件 `FeStub`**:`frontend/src/views/_stub/FeStub.vue`(framework 非 Vue 时落对应等价文件,如 `FeStub.tsx`),最小渲染一个带 `data-fe-stub` 属性的元素(如 `<div data-fe-stub>占位</div>`;行为门据 `data-fe-stub` 识别占位态)。**不实现任何业务逻辑**。',
699 '4. **共享布局/导航**:导航链接**全部指向已在 router 声明的路由 path**(不指向任何不存在的 path),保证任意时刻无悬空链接。', 793 '4. **共享布局/导航**:导航链接**全部指向已在 router 声明的路由 path**(不指向任何不存在的 path),保证任意时刻无悬空链接。',
  794 + '5. **e2e 基线脚手架(全部落 `frontend/` 内)**:',
  795 + ' - **Playwright 配置**(按 docs/04 § 零 `frontend.e2e_runner` 约定,如 `frontend/playwright.config.*`):声明 `globalSetup` / `globalTeardown` 入口 + 共享 `storageState`。',
  796 + ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT}/scripts/seed-demo-data.mjs\`(注入演示种子)→ 用 \`config-vars.yaml\` 的 \`admin_init\` 凭据经 \`docs/05-API接口契约.md\` 登录端点取 JWT,写 \`storageState\`(admin 登录态供 e2e 复用)。`,
  797 + ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树。',
  798 + ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 空库重建 + Flyway schema + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐。',
700 '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', 799 '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。',
701 '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', 800 '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。',
702 '', 801 '',
@@ -722,9 +821,9 @@ function frontendSkeletonStatePromptM(feItems) { @@ -722,9 +821,9 @@ function frontendSkeletonStatePromptM(feItems) {
722 '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', 821 '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)',
723 microStepContract(), 822 microStepContract(),
724 '', 823 '',
725 - `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),且占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在。`,  
726 - '- 全部满足(骨架已建齐)→ `{ "exists": true }`',  
727 - '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub)→ `{ "exists": false }`', 824 + `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在,**且 e2e 基线脚手架存在**——Playwright 配置文件(\`frontend/playwright.config.*\`)+ globalSetup 文件(如 \`frontend/e2e/global-setup.*\`)。`,
  825 + '- 全部满足(骨架已建齐,含 e2e 基线脚手架)→ `{ "exists": true }`',
  826 + '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup)→ `{ "exists": false }`',
728 '## 输出(EXISTS_SCHEMA)', 827 '## 输出(EXISTS_SCHEMA)',
729 ].join('\n') 828 ].join('\n')
730 } 829 }
@@ -1892,6 +1991,12 @@ for (const [idx, module] of todo.entries()) { @@ -1892,6 +1991,12 @@ for (const [idx, module] of todo.entries()) {
1892 await featureLoop(module.reqs, 'backend') 1991 await featureLoop(module.reqs, 'backend')
1893 phase('Gate') 1992 phase('Gate')
1894 await testGate(module, 'backend') 1993 await testGate(module, 'backend')
  1994 + // 演示种子生成 stage(Seed):在 testGate 后跑——此时本模块 schema(含 tdd 新增的 V<n> migration)
  1995 + // 已终态且全绿,按它生成的种子才不会撞结构。allowContinue:false——e2e 基线(globalSetup 注入)与行为门
  1996 + // step2 子项③(演示种子注入)都依赖该种子,坏种子放行会让整个前端阶段(行为验收/e2e)在脏数据上全线误判。
  1997 + phase('Seed')
  1998 + await runStage(g => seedGenPrompt(module) + g,
  1999 + { site:`seed:${module.id}`, grp:'Seed', label:`seed:${module.id}`, allowContinue: false })
1895 phase('Milestone') 2000 phase('Milestone')
1896 await runCrossModule(module) // 替代被删 hook,JS 编排:diff → 分类 → 写日志 2001 await runCrossModule(module) // 替代被删 hook,JS 编排:diff → 分类 → 写日志
1897 } 2002 }