Commit 947cc95f0feccf5327241f4b14ab5449acb25928
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)
Showing
8 changed files
with
510 additions
and
16 deletions
README.md
| ... | ... | @@ -36,7 +36,10 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 |
| 36 | 36 | │ checkout/create → confirm HEAD(5 微 agent) |
| 37 | 37 | │ → featureLoop(后端):spec → plan → tdd → verify → review(有界 5 轮修复, |
| 38 | 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 | 43 | │ → reportPrompt(LLM 12 节叙述) |
| 41 | 44 | │ → runMilestone(JS 编排:wt → default → 已合入? → merge → 字段当前值? |
| 42 | 45 | │ → 写字段 → tag 已存在? → 打 tag → 报告 § ⑫ 当前值? → 替换占位; |
| ... | ... | @@ -44,10 +47,11 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 |
| 44 | 47 | │ |
| 45 | 48 | └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) |
| 46 | 49 | runBranchSetup(frontend-phase) |
| 47 | - → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起) | |
| 50 | + → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起; | |
| 51 | + 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState) | |
| 48 | 52 | → featureLoop(前端,FE-NN,路径限 frontend/):spec → plan → tdd → verify → |
| 49 | 53 | review 循环内并入 per-FE 行为验收 approve 子门(reviewer approve 时才起本 FE 全栈 |
| 50 | - +种子 sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix | |
| 54 | + +演示种子+sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix | |
| 51 | 55 | must-fix→重验,软文字按来源仲裁,行为 green 才打 req-done/<FE>) |
| 52 | 56 | → testGate(frontend,全量回归 vitest+playwright,与 per-FE 行为验收职责正交) |
| 53 | 57 | → runMilestone(milestone/frontend-phase) |
| ... | ... | @@ -141,7 +145,7 @@ erp-workflow-plugin/ |
| 141 | 145 | |---|---|---| |
| 142 | 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 | 150 | | 所属 Skill | 模板文件 | 用途 | |
| 147 | 151 | |---|---|---| |
| ... | ... | @@ -154,6 +158,7 @@ erp-workflow-plugin/ |
| 154 | 158 | | scope-lock | `config-vars-template.yaml` | 仓库根 `config-vars.yaml` 骨架(跨栈中立):项目**全部配置**——非敏感(包名/端口/前端包名/初始账号)+ 敏感凭据(database / admin_init.password / secrets);A1 E.2 锁定,随项目提交 | |
| 155 | 159 | | skeleton-gen | `docs-04-skeleton-template.md` | docs/04 § 一+ 编码规范大纲(HTML 注释引导 LLM) | |
| 156 | 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 | 162 | | skeleton-gen | `scripts-test-template.mjs` | test.mjs 骨架(命令槽位按后端/前端/build/lint/test/e2e 分开,`spawnSync(shell:true)` 跨平台执行) | |
| 158 | 163 | | skeleton-gen | `gitignore-append-template` | 插件推荐忽略项(`.tmp/`、构建产物等;config-vars.yaml 随项目提交,不忽略) | |
| 159 | 164 | | skeleton-gen | `styles-tokens-template.css` | 前端 design tokens CSS 变量骨架 | |
| ... | ... | @@ -167,7 +172,7 @@ erp-workflow-plugin/ |
| 167 | 172 | ## 前置依赖 |
| 168 | 173 | |
| 169 | 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 | 176 | - **`mysql2`(目标项目侧)**:A4 `db-init` 经 `lib/apply-ddl.mjs` 用 mysql2 连接 + 解析 config-vars.yaml `database:` 段 apply V1;生成的 `scripts/setup-test-db.mjs` 在测试闸门前后 drop+create 空库 |
| 172 | 177 | - **Spring Boot + Flyway**(**必需**):build.gradle 声明 `flyway-core` + `flyway-mysql`;Spring 启动时自动 apply `sql/migrations/V*.sql`。本插件生成的 `setup-test-db.mjs` 只清库,schema 必须由 Flyway 应用 |
| 173 | 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 "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" config-vars.yaml sql/migrations/V |
| 75 | 75 | ``` |
| 76 | 76 | |
| 77 | 77 | 退出码与处理: |
| 78 | -- `0` → 成功,进入步骤 D | |
| 78 | +- `0` → 成功,进入步骤 B.3 | |
| 79 | 79 | - `1` → 失败:打印 stderr 并停下 |
| 80 | 80 | - `2` → 用法错(路径找不到),打印路径并停下 |
| 81 | 81 | |
| 82 | 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 | 96 | ### C. 勾选 docs/08 进度 + 进入 A5 |
| 85 | 97 | |
| 86 | 98 | 1. 勾选 A4 顶层(5 维一致已由 A.3 的 `validate-ddl.mjs` 校验过,apply 不改 V1,无需复校): | ... | ... |
skills/plan/project-init/templates/CLAUDE-template.md
| ... | ... | @@ -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 | 50 | ## 🗂️ Git 提交规范 |
| 42 | 51 | |
| 43 | 52 | 每次提交必须遵循以下格式: | ... | ... |
skills/plan/skeleton-gen/SKILL.md
| ... | ... | @@ -44,8 +44,11 @@ docs/04 已由 scope-lock 写入 § 零。本步骤追加 § 一 ~ 三: |
| 44 | 44 | | 模板 | 目标路径 | |
| 45 | 45 | |---|---| |
| 46 | 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 | 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 | 52 | > `sql/migrations/` 目录不在此预建:A4 `db-init` 写 `V1__initial_schema.sql` 时 `Write` 会自动创建。 |
| 50 | 53 | |
| 51 | 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 | 125 | - `${CLAUDE_SKILL_DIR}/templates/docs-04-skeleton-template.md` |
| 123 | 126 | - `${CLAUDE_SKILL_DIR}/templates/scripts-test-template.mjs` |
| 124 | 127 | - `${CLAUDE_SKILL_DIR}/templates/scripts-setup-test-db-template.mjs` |
| 128 | +- `${CLAUDE_SKILL_DIR}/templates/scripts-seed-demo-data-template.mjs` | |
| 125 | 129 | - `config-vars.yaml`(A1 产出,含 DB 凭据;setup-test-db.mjs 运行时读取) |
| 126 | 130 | - `${CLAUDE_SKILL_DIR}/templates/gitignore-append-template` |
| 127 | 131 | - `${CLAUDE_SKILL_DIR}/templates/styles-tokens-template.css` | ... | ... |
skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md
| ... | ... | @@ -35,3 +35,11 @@ |
| 35 | 35 | ### 3.3 日期与金额 |
| 36 | 36 | |
| 37 | 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 | 9 | description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.', |
| 10 | 10 | phases: [ |
| 11 | 11 | { title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' }, |
| 12 | - { title: 'Gate' }, { title: 'Milestone' }, | |
| 12 | + { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' }, | |
| 13 | 13 | ], |
| 14 | 14 | // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门, |
| 15 | 15 | // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。 |
| ... | ... | @@ -391,6 +391,9 @@ function tddPrompt(id, phase, planPath) { |
| 391 | 391 | ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' |
| 392 | 392 | : '', |
| 393 | 393 | fe |
| 394 | + ? '- **e2e 基线约束**:e2e 跑在「空库重建 + Flyway schema + 演示种子」基线上(骨架 globalSetup 已注入 `sql/seed`,无需测试自行建库/起栈)。e2e 断言**优先**定位**演示种子已知主键行**(1000–9999)或**测试自建数据**;**禁止**「全表恰好 N 行」式依赖全局计数的脆弱断言(演示种子行数会随后续模块种子增长,全局计数断言必然 flaky)。' | |
| 395 | + : '', | |
| 396 | + fe | |
| 394 | 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 | 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 | 642 | // ---- 前端行为验收(per-FE behavior 子门)---- |
| 550 | 643 | // 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。 |
| 551 | 644 | // 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发, |
| ... | ... | @@ -562,7 +655,7 @@ function behaviorGateContract() { |
| 562 | 655 | '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', |
| 563 | 656 | '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', |
| 564 | 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 | 659 | `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, |
| 567 | 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 | 661 | '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', |
| ... | ... | @@ -611,15 +704,16 @@ function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { |
| 611 | 704 | '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)', |
| 612 | 705 | '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。', |
| 613 | 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 | 708 | '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', |
| 616 | 709 | '', |
| 617 | - '## step2 起栈四段严格时序(schema 由 Flyway 在后端启动时才建)', | |
| 710 | + '## step2 起栈五段严格时序(schema 由 Flyway 在后端启动时才建)', | |
| 618 | 711 | `1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`, |
| 619 | 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 | 717 | '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。', |
| 624 | 718 | '', |
| 625 | 719 | '## step2.5 鉴权 bootstrap(确定性前置)', |
| ... | ... | @@ -697,6 +791,11 @@ function frontendSkeletonPrompt(feItems) { |
| 697 | 791 | ' - 路由 path 取自上面推导的 FE→path 映射;带参路由用 `:id` 等占位。', |
| 698 | 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 | 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 | 799 | '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', |
| 701 | 800 | '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', |
| 702 | 801 | '', |
| ... | ... | @@ -722,9 +821,9 @@ function frontendSkeletonStatePromptM(feItems) { |
| 722 | 821 | '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', |
| 723 | 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 | 827 | '## 输出(EXISTS_SCHEMA)', |
| 729 | 828 | ].join('\n') |
| 730 | 829 | } |
| ... | ... | @@ -1892,6 +1991,12 @@ for (const [idx, module] of todo.entries()) { |
| 1892 | 1991 | await featureLoop(module.reqs, 'backend') |
| 1893 | 1992 | phase('Gate') |
| 1894 | 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 | 2000 | phase('Milestone') |
| 1896 | 2001 | await runCrossModule(module) // 替代被删 hook,JS 编排:diff → 分类 → 写日志 |
| 1897 | 2002 | } | ... | ... |