Commit 2463c1e59126e07a9a3ade4d99ba84a2bda484de

Authored by zichun
1 parent 6013e7fb

feat(config): 引入 config-vars.yaml 承载项目级配置 + lib 缺陷修复

- 新增 scope-lock/templates/config-vars-template.yaml:非敏感项目级配置
  (包名/端口/前端包名/初始账号)+ secrets_ref 键名引用 .env.local;
  A1 E.2 锁定。docs/07 改为只记规则/约定不记具体值。
- 连带更新 skeleton-gen / plan-start 闸门 2 / frontend-scope-lock /
  CLAUDE-template / README / docs-06·07 模板对齐新的配置分层与 docs/06 小节编号。
- lib 修复(含回归测试):
  - apply-ddl: 抽出 resolveDbConfig,DB_SCHEMA 为契约 schema 键,缺库名 fail-closed (M1)
  - render: 用 Object.hasOwn 拒绝原型链继承键,guard process.argv[1]
  - validate-ddl: 解析 ### 索引/### 外键 bullet (C2)、剥 SQL 注释防幽灵表 (L4)、PRIMARY 归一化
- coding.mjs: Router 把全部前端 FE 聚合为单一末尾 frontend-phase 模块;
  新增 branchSetupPrompt 功能分支生命周期 (C1);diff 区间改三点 diff;fix 后复跑 verify。
README.md
... ... @@ -2,7 +2,7 @@
2 2  
3 3 Claude Code 插件:ERP / 后端管理系统全流程开发框架。
4 4  
5   -把"从零到 N 个模块上线 + 前端整体阶段"的整个流程固化成 **9 个 skill(Plan 阶段,交互式)+ 1 个 Workflow(`workflows/coding.mjs`,Coding 阶段,全自动静默)+ 1 个 reviewer agent + 4 个跨平台 Node 助手(`lib/*.mjs`)+ 24 份模板**,让 CC 在 schema 演化用 Flyway migration、需求可追溯、纯本地 git 的前提下推进编码。Plan 阶段把全部需求/配置/前端约定问询前移(4 个闸门);Coding 阶段整体是单个 Workflow 脚本,子代理无法弹窗 → 结构性静默,后端按模块循环依次打里程碑 tag,所有后端模块打里程碑后进入前端整体阶段(以项目根 `prototype/` 静态 HTML mockup 为页面权威)。
  5 +把"从零到 N 个模块上线 + 前端整体阶段"的整个流程固化成 **9 个 skill(Plan 阶段,交互式)+ 1 个 Workflow(`workflows/coding.mjs`,Coding 阶段,全自动静默)+ 1 个 reviewer agent + 4 个跨平台 Node 助手(`lib/*.mjs`)+ 25 份模板**,让 CC 在 schema 演化用 Flyway migration、需求可追溯、纯本地 git 的前提下推进编码。Plan 阶段把全部需求/配置/前端约定问询前移(4 个闸门);Coding 阶段整体是单个 Workflow 脚本,子代理无法弹窗 → 结构性静默,后端按模块循环依次打里程碑 tag,所有后端模块打里程碑后进入前端整体阶段(以项目根 `prototype/` 静态 HTML mockup 为页面权威)。
6 6  
7 7 ## 这个插件做什么
8 8  
... ... @@ -173,7 +173,7 @@ coding-start(skill)校验 Plan 终结闸 → Workflow({scriptPath:"…/workf
173 173 |---|---|---|
174 174 | `code-reviewer` | 统一 reviewer。`phase=backend` 跑通用代码审查维度;`phase=frontend` 附加前端 7 维 checklist(prototype 一致性 / design tokens / a11y / 响应式 / 业务校验前端复刻 / API 一致性 / 状态机覆盖,主观维度仅标记明显问题不触发 request-changes)。非交互,返回结构化 verdict,绝不弹窗 | `workflows/coding.mjs` 的 review stage:`agent(..., {agentType:'code-reviewer'})` |
175 175  
176   -## Templates 清单(24 份)
  176 +## Templates 清单(25 份)
177 177  
178 178 | 所属 Skill | 模板文件 | 用途 |
179 179 |---|---|---|
... ... @@ -183,13 +183,14 @@ coding-start(skill)校验 Plan 终结闸 → Workflow({scriptPath:"…/workf
183 183 | project-init | `docs-08-initial-template.md` | 工作流进度文件骨架(Plan A0~A6 checkbox) |
184 184 | scope-lock | `req-card-template.md` | 单张 REQ-XXX-NNN 卡片骨架(结构化字段表:字段名/类型/必填/校验/业务规则/示例值,示例值须替换为真实约束) |
185 185 | scope-lock | `_module-template.md` | 模块子目录的 `_module.md` 模块头(模块代码-名 / 简述 / 依赖模块 TBD / 涉及表 TBD) |
  186 +| scope-lock | `config-vars-template.yaml` | 仓库根 `config-vars.yaml` 骨架(跨栈中立):非敏感项目级配置(包名/端口/前端包名/初始账号)+ `secrets_ref` 键名引用 `.env.local`;A1 E.2 锁定 |
186 187 | skeleton-gen | `docs-04-skeleton-template.md` | docs/04 § 一+ 编码规范大纲(HTML 注释引导 LLM) |
187 188 | skeleton-gen | `docs-06-static-template.md` | docs/06 § 一~二 大纲(通用交互 + Design Tokens;布局以 prototype/ 为权威) |
188   -| skeleton-gen | `docs-07-env-template.md` | docs/07 环境配置大纲 |
  189 +| skeleton-gen | `docs-07-env-template.md` | docs/07 环境配置大纲(只记规则/约定,不记具体值;配置值指向 config-vars.yaml + .env.local) |
189 190 | skeleton-gen | `docs-09-structure-template.md` | docs/09 目录结构大纲 |
190 191 | skeleton-gen | `scripts-setup-test-db-template.mjs` | 跨平台 drop + create 空库脚本(安全 env 解析,无 shell-source);schema apply 交给 Flyway |
191 192 | skeleton-gen | `scripts-test-template.mjs` | test.mjs 骨架(命令槽位 {{TEST_CMD}} / {{E2E_CMD}},`spawnSync(shell:true)` 跨平台执行) |
192   -| skeleton-gen | `env-local-template` | 凭据模板(DB_* + JWT_SECRET) |
  193 +| skeleton-gen | `env-local-template` | 凭据模板(DB_* + JWT_SECRET);A2 据 config-vars.yaml `secrets_ref` 追加项目专属 secret 键 |
193 194 | skeleton-gen | `gitignore-append-template` | 插件推荐忽略项(`.env.local`、`.tmp/`、构建产物等) |
194 195 | skeleton-gen | `styles-tokens-template.css` | 前端 design tokens CSS 变量骨架 |
195 196 | db-design-gen | `docs-03-header-template.md` | docs/03 数据库设计头部 |
... ...
lib/apply-ddl.mjs
... ... @@ -62,7 +62,11 @@ export function parseEnv(text) {
62 62 *
63 63 * Reads connection settings from the parsed env file. Recognised keys (with
64 64 * common aliases) — DB_HOST/MYSQL_HOST, DB_PORT/MYSQL_PORT, DB_USER/MYSQL_USER,
65   - * DB_PASS/DB_PASSWORD/MYSQL_PASSWORD, DB_NAME/MYSQL_DATABASE.
  65 + * DB_PASS/DB_PASSWORD/MYSQL_PASSWORD, DB_SCHEMA/DB_NAME/MYSQL_DATABASE.
  66 + * DB_SCHEMA is the plugin's canonical schema key (see .env.local template); the
  67 + * target database MUST resolve to a non-empty value or applyDDL fails closed —
  68 + * V1 DDL contains no `USE`/`CREATE DATABASE`, so a missing schema would only
  69 + * surface as an opaque `ER_NO_DB_ERROR` on the first CREATE TABLE.
66 70 *
67 71 * @param {{envPath: string, ddlPath: string}} opts
68 72 * @returns {Promise<void>}
... ... @@ -79,12 +83,7 @@ export async function applyDDL({ envPath, ddlPath }) {
79 83  
80 84 const env = parseEnv(readFileSync(envPath, 'utf8'))
81 85 const ddl = readFileSync(ddlPath, 'utf8')
82   -
83   - const host = env.DB_HOST || env.MYSQL_HOST || '127.0.0.1'
84   - const port = Number(env.DB_PORT || env.MYSQL_PORT || 3306)
85   - const user = env.DB_USER || env.MYSQL_USER || 'root'
86   - const password = env.DB_PASS || env.DB_PASSWORD || env.MYSQL_PASSWORD || ''
87   - const database = env.DB_NAME || env.MYSQL_DATABASE || undefined
  86 + const { host, port, user, password, database } = resolveDbConfig(env, envPath)
88 87  
89 88 const conn = await mysql.createConnection({
90 89 host,
... ... @@ -101,6 +100,30 @@ export async function applyDDL({ envPath, ddlPath }) {
101 100 }
102 101 }
103 102  
  103 +/**
  104 + * Resolve mysql2 connection settings from a parsed env object. Pure (no I/O),
  105 + * so it is unit-testable without mysql2 installed.
  106 + *
  107 + * fail-closed on missing schema: DB_SCHEMA is the plugin's canonical key; V1 DDL
  108 + * has no `USE`/`CREATE DATABASE`, so a missing database would otherwise surface
  109 + * only as an opaque ER_NO_DB_ERROR on the first CREATE TABLE.
  110 + *
  111 + * @param {Record<string,string>} env
  112 + * @param {string} [envPath] only used to make the error message actionable
  113 + * @returns {{host:string, port:number, user:string, password:string, database:string}}
  114 + */
  115 +export function resolveDbConfig(env, envPath = '.env.local') {
  116 + const host = env.DB_HOST || env.MYSQL_HOST || '127.0.0.1'
  117 + const port = Number(env.DB_PORT || env.MYSQL_PORT || 3306)
  118 + const user = env.DB_USER || env.MYSQL_USER || 'root'
  119 + const password = env.DB_PASS || env.DB_PASSWORD || env.MYSQL_PASSWORD || ''
  120 + const database = env.DB_SCHEMA || env.DB_NAME || env.MYSQL_DATABASE || undefined
  121 + if (!database) {
  122 + throw new Error(`apply-ddl: 缺数据库名 — 请在 ${envPath} 设置 DB_SCHEMA(或 DB_NAME / MYSQL_DATABASE)`)
  123 + }
  124 + return { host, port, user, password, database }
  125 +}
  126 +
104 127 /** Distinct error type so the CLI can emit a friendly install hint. */
105 128 export class MysqlUnavailableError extends Error {
106 129 constructor() {
... ...
lib/apply-ddl.test.mjs
1 1 import { test } from 'node:test'
2 2 import assert from 'node:assert/strict'
3   -import { parseEnv } from './apply-ddl.mjs'
  3 +import { parseEnv, resolveDbConfig } from './apply-ddl.mjs'
4 4  
5 5 test('parseEnv ignores comments, trims, keeps special chars literally', () => {
6 6 const env = parseEnv('# c\nDB_PASS=p@ss$word!\nDB_NAME = erp \n')
... ... @@ -55,3 +55,32 @@ test(&#39;parseEnv on empty / non-string input returns empty object&#39;, () =&gt; {
55 55 assert.deepEqual(parseEnv(undefined), {})
56 56 assert.deepEqual(parseEnv(null), {})
57 57 })
  58 +
  59 +// ── resolveDbConfig(M1:DB_SCHEMA 是插件契约的 schema 键)─────────
  60 +test('resolveDbConfig maps DB_SCHEMA to database (plugin canonical key)', () => {
  61 + const c = resolveDbConfig({ DB_SCHEMA: 'erp_test', DB_USER: 'u', DB_PASS: 'p', DB_HOST: 'db.local', DB_PORT: '3307' })
  62 + assert.equal(c.database, 'erp_test')
  63 + assert.equal(c.host, 'db.local')
  64 + assert.equal(c.port, 3307)
  65 + assert.equal(c.user, 'u')
  66 + assert.equal(c.password, 'p')
  67 +})
  68 +
  69 +test('resolveDbConfig honors DB_NAME / MYSQL_DATABASE aliases', () => {
  70 + assert.equal(resolveDbConfig({ DB_NAME: 'a' }).database, 'a')
  71 + assert.equal(resolveDbConfig({ MYSQL_DATABASE: 'b' }).database, 'b')
  72 + // DB_SCHEMA wins over aliases
  73 + assert.equal(resolveDbConfig({ DB_SCHEMA: 's', DB_NAME: 'a' }).database, 's')
  74 +})
  75 +
  76 +test('resolveDbConfig fails closed when no schema key is present (M1)', () => {
  77 + assert.throws(() => resolveDbConfig({ DB_USER: 'root' }, '.env.local'), /DB_SCHEMA/)
  78 +})
  79 +
  80 +test('resolveDbConfig applies sane defaults for host/port/user/password', () => {
  81 + const c = resolveDbConfig({ DB_SCHEMA: 's' })
  82 + assert.equal(c.host, '127.0.0.1')
  83 + assert.equal(c.port, 3306)
  84 + assert.equal(c.user, 'root')
  85 + assert.equal(c.password, '')
  86 +})
... ...
lib/render.mjs
... ... @@ -8,7 +8,9 @@
8 8 export function render(template, vars) {
9 9 const withoutComments = template.replace(/<!--[\s\S]*?-->/g, '')
10 10 return withoutComments.replace(/\{\{(\w+)\}\}/g, (_, key) => {
11   - if (!(key in vars)) throw new Error(`render: missing var "${key}"`)
  11 + // 用 Object.hasOwn 而非 `key in vars`:避免 {{constructor}} / {{toString}} 等
  12 + // 沿原型链命中继承属性、静默渲染出垃圾(应按"缺变量"抛错)。
  13 + if (!Object.hasOwn(vars, key)) throw new Error(`render: missing var "${key}"`)
12 14 return String(vars[key]) // 字面插入,不二次解释 $ 或 {}
13 15 })
14 16 }
... ... @@ -16,7 +18,7 @@ export function render(template, vars) {
16 18 // 入口判定用 pathToFileURL 规范化 process.argv[1],使其与 import.meta.url 编码一致
17 19 // (路径含空格/非 ASCII/Windows 反斜杠时,字面 `file://${argv[1]}` 比较会失配)。
18 20 const { pathToFileURL } = await import('node:url')
19   -if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  21 +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
20 22 const { readFileSync, writeFileSync } = await import('node:fs')
21 23 const [tplPath, jsonPath, outPath] = process.argv.slice(2)
22 24 const tpl = readFileSync(tplPath, 'utf8')
... ...
lib/render.test.mjs
... ... @@ -14,3 +14,10 @@ test(&#39;strips HTML comments used as template guides&#39;, () =&gt; {
14 14 test('missing key throws (no silent blank)', () => {
15 15 assert.throws(() => render('{{missing}}', {}), /missing/)
16 16 })
  17 +test('inherited prototype keys are treated as missing (not silently rendered)', () => {
  18 + // {{constructor}} / {{toString}} 不应沿原型链命中继承函数渲染出垃圾
  19 + assert.throws(() => render('{{constructor}}', {}), /missing var "constructor"/)
  20 + assert.throws(() => render('{{toString}}', {}), /missing var "toString"/)
  21 + // 但 own 属性即便名为 constructor 也应正常渲染
  22 + assert.equal(render('{{constructor}}', { constructor: 'X' }), 'X')
  23 +})
... ...
lib/validate-ddl.mjs
... ... @@ -17,7 +17,10 @@
17 17  
18 18 // ── 解析 docs/03 markdown 表定义 ─────────────────────────────────
19 19 // 约定:每张表一节,节标题形如 ## `表名` 或 ## `表名` — 业务含义
20   -// 节内的 markdown 表格首列是列名(可含反引号),次列是类型。
  20 +// 节内分 ### 字段(markdown 表格,首列列名、次列类型)、### 索引、### 外键(项目符号列表)。
  21 +// 索引/外键的 bullet 形态见 db-design-gen/templates/docs-03-table-template.md:
  22 +// ### 索引 → - `name` (type): cols
  23 +// ### 外键 → - `name`: from_col → to_table.to_col (on_delete)
21 24 // 跳过表头行(列/字段/类型等标签)与分隔行(---)。
22 25 // 形如「## 一、全局约定」这类非反引号标题不视为表。
23 26 export function parseDocsTables(text) {
... ... @@ -25,13 +28,15 @@ export function parseDocsTables(text) {
25 28 const lines = String(text).split('\n')
26 29 // 反引号包裹的表名:## `name` 或 ## `name` — purpose
27 30 const headerRe = /^##\s+`([^`]+)`/
28   - let current = null // { columns: Map }
  31 + let current = null // { columns, indexes, foreignKeys }
  32 + let mode = 'col' // 当前子区块:'col'(字段表格)/ 'idx'(索引)/ 'fk'(外键)
29 33  
30 34 for (const raw of lines) {
31 35 const line = raw.replace(/\r$/, '')
32 36 const h2 = line.match(headerRe)
33 37 if (h2) {
34 38 current = { columns: new Map(), indexes: new Set(), foreignKeys: new Set() }
  39 + mode = 'col'
35 40 tables.set(h2[1].trim(), current)
36 41 continue
37 42 }
... ... @@ -41,7 +46,16 @@ export function parseDocsTables(text) {
41 46 continue
42 47 }
43 48 if (!current) continue
44   - // markdown 表格行:以 | 开头
  49 + // ### 子区块切换(### 索引 / ### 外键 / 其它如 ### 字段、### 业务注记 → col)
  50 + const h3 = line.match(/^###\s+(.+)$/)
  51 + if (h3) {
  52 + const title = h3[1].trim()
  53 + mode = /索引|index/i.test(title) ? 'idx' : /外键|foreign/i.test(title) ? 'fk' : 'col'
  54 + continue
  55 + }
  56 + if (mode === 'idx') { parseIndexBullet(line, current.indexes); continue }
  57 + if (mode === 'fk') { parseForeignKeyBullet(line, current.foreignKeys); continue }
  58 + // mode === 'col':markdown 表格行(以 | 开头)
45 59 if (!/^\s*\|/.test(line)) continue
46 60 const cells = splitMarkdownRow(line)
47 61 if (cells.length < 2) continue
... ... @@ -56,11 +70,37 @@ export function parseDocsTables(text) {
56 70 return tables
57 71 }
58 72  
  73 +// 解析索引 bullet: - `name` (type): cols
  74 +// type 为 PRIMARY(不分大小写)→ 记 'PRIMARY'(匹配 parseDDL 对主键的归一化);
  75 +// 否则记索引名 name(匹配 parseDDL 对命名索引存 name)。
  76 +function parseIndexBullet(line, indexes) {
  77 + const m = line.match(/^\s*-\s+`?([^`():]+)`?\s*(?:\(([^)]*)\))?\s*:?/)
  78 + if (!m) return
  79 + const name = m[1].trim()
  80 + const type = (m[2] || '').trim()
  81 + if (!name) return
  82 + if (/^primary$/i.test(type) || /^primary$/i.test(name)) indexes.add('PRIMARY')
  83 + else indexes.add(name)
  84 +}
  85 +
  86 +// 解析外键 bullet: - `name`: from_col → to_table.to_col (on_delete)
  87 +// 归一化为 parseDDL 同形的 `${fromCol}->${toTable}(${toCol})`(注意 docs 用 unicode → / DDL 用 ->)。
  88 +function parseForeignKeyBullet(line, foreignKeys) {
  89 + const m = line.match(/^\s*-\s+`?[^`:]+`?\s*:\s*`?([A-Za-z0-9_,\s]+?)`?\s*(?:→|->|>)\s*`?([A-Za-z0-9_]+)`?\.`?([A-Za-z0-9_]+)`?/)
  90 + if (!m) return
  91 + const fromCols = m[1].replace(/`/g, '').replace(/\s+/g, '')
  92 + const toTable = m[2]
  93 + const toCols = m[3].replace(/`/g, '').replace(/\s+/g, '')
  94 + if (!fromCols || !toTable || !toCols) return
  95 + foreignKeys.add(`${fromCols}->${toTable}(${toCols})`)
  96 +}
  97 +
59 98 // ── 解析 CREATE TABLE DDL ────────────────────────────────────────
60 99 // 提取每个 CREATE TABLE 的:列名→类型、索引名集合、外键描述集合。
61 100 export function parseDDL(text) {
62 101 const tables = new Map()
63   - const src = String(text)
  102 + // 先剥离 SQL 注释,避免被注释掉的 CREATE TABLE 被当成真实表(幽灵表假阳性)。
  103 + const src = stripSqlComments(String(text))
64 104 // 抓取 CREATE TABLE <name> ( <body> ) ;name 可带反引号;body 到匹配的右括号
65 105 const createRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?([A-Za-z0-9_]+)`?\s*\(/gi
66 106 let m
... ... @@ -182,9 +222,10 @@ export function diffSchema(docsTables, ddlTables) {
182 222 if (!d.columns.has(col)) diff.columnMismatches.push({ table: t, column: col, side: 'ddl' })
183 223 }
184 224  
185   - // 维度 4:索引
186   - const dIdx = d.indexes || new Set()
187   - const sIdx = s.indexes || new Set()
  225 + // 维度 4:索引。PRIMARY 由列级主键约定治理(已在列维度校验),且 docs/03 常只在 ### 字段
  226 + // 表内体现 PK、不在 ### 索引 重列 → 从两侧索引集剔除 PRIMARY,避免假阳性;命名二级索引仍比对。
  227 + const dIdx = new Set([...(d.indexes || [])].filter(ix => ix !== 'PRIMARY'))
  228 + const sIdx = new Set([...(s.indexes || [])].filter(ix => ix !== 'PRIMARY'))
188 229 for (const ix of dIdx) if (!sIdx.has(ix)) diff.indexMismatches.push({ table: t, index: ix, side: 'docs' })
189 230 for (const ix of sIdx) if (!dIdx.has(ix)) diff.indexMismatches.push({ table: t, index: ix, side: 'ddl' })
190 231  
... ... @@ -202,6 +243,15 @@ export function diffSchema(docsTables, ddlTables) {
202 243 }
203 244  
204 245 // ── 工具函数 ─────────────────────────────────────────────────────
  246 +// 剥离 SQL 注释:-- 行注释(到行尾)、# 行注释(到行尾)、/* */ 块注释。
  247 +// 保守起见不解析字符串字面量内的注释符(DDL 极少在标识符/默认值里出现裸 -- 或 /*)。
  248 +function stripSqlComments(sql) {
  249 + return sql
  250 + .replace(/\/\*[\s\S]*?\*\//g, ' ') // 块注释
  251 + .replace(/--.*$/gm, '') // -- 行注释
  252 + .replace(/#.*$/gm, '') // # 行注释
  253 +}
  254 +
205 255 function stripTicks(s) {
206 256 return String(s).replace(/`/g, '').trim()
207 257 }
... ...
lib/validate-ddl.test.mjs
... ... @@ -61,6 +61,75 @@ test(&#39;parseDocsTables: real docs/03 format — ## `t` — purpose + ### 字段 +
61 61 assert.equal(order.columns.has('---'), false)
62 62 })
63 63  
  64 +// 全链路:模板格式 docs/03(### 字段 + ### 索引 + ### 外键 bullet)→ parseDocsTables 必须
  65 +// 把索引/外键解析进 Set(回归 C2:此前 parseDocsTables 从不写 indexes/foreignKeys)。
  66 +const DOCS_FULL = [
  67 + '## `t_order` — 订单主表',
  68 + '',
  69 + '### 字段',
  70 + '| 字段 | 类型 | Nullable | 默认 | 业务含义 |',
  71 + '|---|---|---|---|---|',
  72 + '| `iId` | bigint | 否 | 自增 | 主键 |',
  73 + '| `sUserId` | varchar(100) | 否 | — | 用户ID |',
  74 + '',
  75 + '### 索引',
  76 + '- `pk` (PRIMARY): iId',
  77 + '- `idx_user` (index): sUserId',
  78 + '',
  79 + '### 外键',
  80 + '- `fk_user`: sUserId → t_user.sId (CASCADE)',
  81 + '',
  82 +].join('\n')
  83 +const DDL_FULL = [
  84 + 'CREATE TABLE `t_order` (',
  85 + ' `iId` bigint NOT NULL AUTO_INCREMENT,',
  86 + ' `sUserId` varchar(100) NOT NULL,',
  87 + ' PRIMARY KEY (`iId`),',
  88 + ' KEY `idx_user` (`sUserId`),',
  89 + ' CONSTRAINT `fk_user` FOREIGN KEY (`sUserId`) REFERENCES `t_user` (`sId`)',
  90 + ') ENGINE=InnoDB;',
  91 +].join('\n')
  92 +
  93 +test('parseDocsTables: parses ### 索引 / ### 外键 bullets into sets (C2 regression)', () => {
  94 + const t = parseDocsTables(DOCS_FULL).get('t_order')
  95 + assert.ok(t)
  96 + assert.ok(t.indexes.has('PRIMARY'), 'PRIMARY index normalized')
  97 + assert.ok(t.indexes.has('idx_user'), 'named index by name')
  98 + assert.ok(t.foreignKeys.has('sUserId->t_user(sId)'), 'FK normalized to parseDDL form')
  99 +})
  100 +
  101 +test('full chain: matching docs/03 (with indexes+FK) ↔ DDL yields no diff (C2 regression)', () => {
  102 + const d = diffSchema(parseDocsTables(DOCS_FULL), parseDDL(DDL_FULL))
  103 + assert.deepEqual(d.indexMismatches, [], 'index dimension clean')
  104 + assert.deepEqual(d.foreignKeyMismatches, [], 'FK dimension clean')
  105 + assert.equal(d.hasDiff, false, 'no spurious diff on a faithful schema')
  106 +})
  107 +
  108 +test('full chain: a real FK present in docs but missing from DDL is caught', () => {
  109 + const ddlNoFk = [
  110 + 'CREATE TABLE `t_order` (',
  111 + ' `iId` bigint NOT NULL AUTO_INCREMENT,',
  112 + ' `sUserId` varchar(100) NOT NULL,',
  113 + ' PRIMARY KEY (`iId`),',
  114 + ' KEY `idx_user` (`sUserId`)',
  115 + ') ENGINE=InnoDB;',
  116 + ].join('\n')
  117 + const d = diffSchema(parseDocsTables(DOCS_FULL), parseDDL(ddlNoFk))
  118 + assert.ok(d.foreignKeyMismatches.some(m => m.side === 'docs' && m.foreignKey === 'sUserId->t_user(sId)'))
  119 + assert.equal(d.hasDiff, true)
  120 +})
  121 +
  122 +test('parseDDL: CREATE TABLE inside a comment is NOT counted as a table (L4)', () => {
  123 + const ddl = [
  124 + '-- CREATE TABLE ghost_line ( x int );',
  125 + '/* CREATE TABLE ghost_block ( y int ); */',
  126 + '# CREATE TABLE ghost_hash ( z int );',
  127 + 'CREATE TABLE real_one ( a int );',
  128 + ].join('\n')
  129 + const tables = parseDDL(ddl)
  130 + assert.deepEqual([...tables.keys()], ['real_one'])
  131 +})
  132 +
64 133 test('parseDocsTables: top-level ## headers like "## 一、全局约定" are NOT tables', () => {
65 134 const docs = [
66 135 '## 一、全局约定(人工填)',
... ...
skills/downstream-gen/templates/docs-02-template.md
... ... @@ -10,7 +10,7 @@
10 10  
11 11 ## 二、开发顺序清单(CC 分发权威)
12 12  
13   -> 本清单由 A5 `downstream-gen` 一次性生成。**每行是一个 REQ**,不是模块。CC 按表格行序从上到下扫描,对每个 REQ 所属模块查 `docs/08 § 二` 的 `里程碑:` 字段 + 本地 `git tag -l 'milestone/<id>'`:tag 存在则跳过,否则(`—` / tag 不存在)选为当前模块;`module-start` 会把该模块的所有 REQ 一次做完。
  13 +> 本清单由 A5 `downstream-gen` 一次性生成。**每行是一个 REQ**,不是模块。Coding 阶段(`coding.mjs` 的 Router)按表格行序确定模块顺序,对每个 REQ 所属模块查 `docs/08 § 二` 的 `里程碑:` 字段 + 本地 `git tag -l 'milestone/<id>'`:tag 存在则跳过,否则(`—` / tag 不存在)选为待跑模块;顶层循环对每个待跑后端模块依次跑功能链(spec→plan→tdd→verify→review)+ 测试闸 + 里程碑,把该模块的所有 REQ 一次做完。
14 14 >
15 15 > **约束**:同一模块的所有 REQ 必须**连续排列**。允许打破依赖拓扑(如环依赖、业务必须先做),但必须在「备注」列写明原因。
16 16  
... ... @@ -20,7 +20,7 @@
20 20 | {{index}} | **{{req_id}}** | {{module_id}} | {{rationale}} | {{note}} |
21 21 {{/each}}
22 22  
23   -> **后端模块全部打里程碑后**:milestone-tag 自动回调 `coding-start` → coding-start 检测到 `backend_done=true && frontend_done=false` → 派发 `frontend-start`。`frontend-start` 步骤 1 自带 prototype/ 门禁(≥ 1 个 `*.html` mockup,缺失则 AskUserQuestion 提示用户补齐)。前端阶段以业务功能(不是 HTML 文件数)为粒度拆分 FE,每个 FE 跑一次 feature 循环(fe-feature-*),最后整个阶段打 1 个里程碑 tag(分支 `frontend-phase`,记录在 `docs/08 § 三 整体里程碑`)。
  23 +> **后端模块全部打里程碑后**:`coding.mjs` 的 Router 把全部未完成前端 FE 聚合为**唯一一个** `frontend-phase` 阶段,排在所有后端模块之后由顶层循环跑(prototype/ 门禁已在 Plan 期 A6 `frontend-scope-lock` 前移完成)。前端阶段以业务功能(不是 HTML 文件数)为粒度拆分 FE(FE-NN,路径限 `frontend/`),每个 FE 跑一次功能链,整个阶段打 **1 个**里程碑 tag(分支 `frontend-phase`,`milestone/frontend-phase`,记录在 `docs/08 § 三 整体里程碑`)。
24 24  
25 25 ## 三、关键说明
26 26 {{notes}}
... ...
skills/downstream-gen/templates/docs-10-header-template.md
... ... @@ -12,5 +12,5 @@
12 12  
13 13 > 本文档仅维护项目级验收 SOP。粒度更细的验收信息分散在:
14 14 > - **每 REQ 的业务验收点**:`docs/01-需求清单/<module>/<req_id>.md § 验收`
15   -> - **每模块的实测验收(数据 / UI / 自动化用例位置)**:由 B 阶段 `module-report` 在该模块完成时填入模块完成报告
16   -> - **REQ 开发进度(feature-review approve 状态)**:`docs/08 § 二`
  15 +> - **每模块的实测验收(数据 / UI / 自动化用例位置)**:由 B 阶段 coding.mjs 的 module-report stage 在该模块完成时填入模块完成报告
  16 +> - **REQ 开发进度(review stage approve 状态)**:`docs/08 § 二`
... ...
skills/frontend-scope-lock/SKILL.md
... ... @@ -19,6 +19,8 @@ A6 是 **Plan 阶段最后一个前端守门 skill**,由 `plan-start` 在 A5
19 19  
20 20 ## 执行步骤
21 21  
  22 +> **关于 AskUserQuestion**:下文只描述「问什么、给哪些选项、各选项导向什么后续」。`header` / 各选项的 `description` / `multiSelect` 等具体参数由你按工具 schema 自行填全合法值——不要把下文的选项文字当成完整调用照抄。
  23 +
22 24 ### 步骤 0:打印当前位置流程图
23 25  
24 26 向用户**直接输出**(模型自己打印,不调 bash / cat)当前位置:
... ... @@ -38,11 +40,9 @@ A6 是 **Plan 阶段最后一个前端守门 skill**,由 `plan-start` 在 A5
38 40 用 `Glob` 检查项目根 `prototype/**/*.html`:
39 41  
40 42 - **至少 1 个 `.html`** → 通过,记下文件清单,进入步骤 2。
41   -- **0 个** → 这是 Plan 期,**可以问**。用 `AskUserQuestion` 告知用户:
42   - - **question**:`未在 prototype/ 找到任何 .html 原型。前端范围锁定依赖原型作为页面骨架权威。`
43   - - 选项:「我已补齐原型,请重新检查」(→ 重新 `Glob`,仍为 0 则重复本问)/「本项目无前端,跳过 A6」。
  43 +- **0 个** → 这是 Plan 期,**可以问**。用 `AskUserQuestion` 告知用户「未在 prototype/ 找到任何 .html 原型,前端范围锁定依赖原型作为页面骨架权威」,给「我已补齐原型,请重新检查」和「本项目无前端,跳过 A6」两个选项。
  44 + - 选「已补齐」→ 重新 `Glob`:命中则进入步骤 2,仍为 0 则重复本问。
44 45 - 选「无前端」→ 在 docs/08 § 一 勾选 A6 父项并注明「无前端,A6 跳过」,打印步骤 6 的终止横幅(产出标注「跳过」),**停止**,不写 docs/06 / docs/04。
45   - - 选「已补齐」且 `Glob` 命中 → 进入步骤 2。
46 46  
47 47 ### 步骤 2:收集证据(只读,不问)
48 48  
... ... @@ -51,7 +51,7 @@ A6 是 **Plan 阶段最后一个前端守门 skill**,由 `plan-start` 在 A5
51 51 - **prototype/**:所有 `*.html`,作为页面布局 / 组件 / 交互的**实测权威**(DOM 结构、表单展现、列表范式、状态色实例)。
52 52 - **docs/01-需求清单/**:各 `_module.md` + `REQ-*.md`,提取 UI 描述、业务校验、acceptance 中与界面相关的部分。
53 53 - **docs/05-API接口契约.md**:端点列表,确认前端要消费的接口集合(影响页面状态机 / 加载态)。
54   -- **docs/06-UI交互规范.md**:已有的整体布局(§ 一)、标准页面类型(§ 二)、通用交互规则(§ 三)、Design Tokens(§ 四)、页面清单(§ 三 由 A5 填)。本 skill 在此基础上**收敛 / 补全**,不推翻已确认内容。
  54 +- **docs/06-UI交互规范.md**:已有的通用交互规则(§ 一)、Design Tokens(§ 二)、页面清单(§ 三 由 A5 填)。布局以项目根 `prototype/` 为权威,docs/06 不设独立布局小节。本 skill 在此基础上**收敛 / 补全**,不推翻已确认内容。
55 55 - **docs/04-技术规范.md § 零**:技术栈表里的前端行(如 `前端 UI 组件` = Ant Design),作为组件库选型默认值来源。
56 56  
57 57 把证据归纳为三组**草案**:(a) 项目级 UI 约定、(b) Design Tokens(全局调色板 + 组件级状态色)、(c) 组件库选型。草案优先复用 docs/06 / docs/04 既有值(已锁定的不重问),仅对 prototype 与现文档**不一致**或**缺失**的点进入步骤 3 确认。
... ... @@ -70,13 +70,13 @@ A6 是 **Plan 阶段最后一个前端守门 skill**,由 `plan-start` 在 A5
70 70  
71 71 用 `${CLAUDE_SKILL_DIR}/templates/fe-scope-template.md` 作为填充骨架,把步骤 3 确认后的真实值填入(剥掉模板内 HTML 注释),分别落盘:
72 72  
73   -- **docs/06-UI交互规范.md**(用 `Edit` 合并,不另起文件):
74   - - 模板 § 一 → 收敛 / 补全 docs/06 § 一(整体布局)+ § 三(通用交互规则)的项目级约定。
75   - - 模板 § 二 → 写入 / 校正 docs/06 § 四(Design Tokens 全局调色板 + 组件级状态色 + Token 默认值),与 `src/styles/tokens.css` 命名规范(docs/04 § 2.5)一致。
  73 +- **docs/06-UI交互规范.md**(用 `Edit` 合并,不另起文件;小节编号以 `skeleton-gen/templates/docs-06-static-template.md` 为权威:§ 一 通用交互规则 / § 二 Design Tokens / § 三 页面清单):
  74 + - 模板 § 一 → 收敛 / 补全 docs/06 § 一(通用交互规则)的项目级约定。
  75 + - 模板 § 二 → 写入 / 校正 docs/06 § 二(Design Tokens 全局调色板 + 组件级状态色 + Token 默认值),与 `src/styles/tokens.css` 命名规范(docs/04 § 2.5)一致。
76 76 - 模板 § 五 → 追加到 docs/06 § 三(页面清单)之后,作为 **FE 级设计决策表**:FE 清单来自 docs/08 § 三(若 § 三 尚无 FE bullet,则在此按 prototype + docs/01 + docs/05 推导 FE 清单并**同时写入 docs/08 § 三**「功能:」项,行格式见 docs/08 模板)。一 FE 一行。
77 77 - **docs/04-技术规范.md § 二(前端编码规范)**(用 `Edit`):
78   - - 把组件库选型 + 模板 § 四前端栈摘要写入 / 校正 § 2.3(组件 / 页面编写规范)与 § 2.5(样式与主题)的引用说明;色值约定指向 docs/06 § 四。
79   - - 不重复抄 docs/06 全文,只写「前端组件库 = X、tokens 锁定于 docs/06 § 四」这类引用,保持 SSoT。
  78 + - 把组件库选型 + 模板 § 四前端栈摘要写入 / 校正 § 2.3(组件 / 页面编写规范)与 § 2.5(样式与主题)的引用说明;色值约定指向 docs/06 § 二。
  79 + - 不重复抄 docs/06 全文,只写「前端组件库 = X、tokens 锁定于 docs/06 § 二」这类引用,保持 SSoT。
80 80  
81 81 写入时遵循模板的字面安全约定:值含 `$` / `{` / `}` 等字符**原样写入**,不做二次解释。
82 82  
... ... @@ -101,8 +101,8 @@ A6 是 **Plan 阶段最后一个前端守门 skill**,由 `plan-start` 在 A5
101 101 [frontend-scope-lock] ✅ A6 前端范围锁定完成
102 102  
103 103 产出:
104   - ✓ docs/06 § 一/§ 三 项目级 UI 约定
105   - ✓ docs/06 § 四 Design Tokens(全局调色板 + 组件级状态色)
  104 + ✓ docs/06 § 一 项目级 UI 约定(通用交互规则)
  105 + ✓ docs/06 § 二 Design Tokens(全局调色板 + 组件级状态色)
106 106 ✓ docs/06 § 三之后 各 FE-NN 设计决策表
107 107 ✓ docs/04 § 二 前端栈 + 组件库选型(引用 docs/06)
108 108  
... ... @@ -119,7 +119,7 @@ A6 是 **Plan 阶段最后一个前端守门 skill**,由 `plan-start` 在 A5
119 119 - `prototype/**/*.html`(页面骨架实测权威,步骤 1 前置门由本 skill 自承)
120 120 - `docs/01-需求清单/**/*.md`(UI 描述 / 业务校验来源)
121 121 - `docs/05-API接口契约.md`(前端消费端点)
122   -- `docs/06-UI交互规范.md`(写入目标:§ 一/§ 三 约定、§ 四 Tokens、§ 三之后 FE 决策表)
  122 +- `docs/06-UI交互规范.md`(写入目标:§ 一 通用交互约定、§ 二 Tokens、§ 三之后 FE 决策表)
123 123 - `docs/04-技术规范.md § 二 / § 零`(前端栈 + 组件库选型写入目标)
124 124 - `docs/08-模块任务管理.md § 一`(A6 进度勾选)/ `§ 三`(FE 清单)
125 125 - 上游:`plan-start`(A5 完成后派发到此)
... ...
skills/plan-start/SKILL.md
... ... @@ -46,12 +46,13 @@ A 阶段所有 checkbox 均 `[x]` 时**不代表可以进 B 阶段**。Coding
46 46 1. **REQ 卡片真实数据**(来自 A1 scope-lock)
47 47 - `Glob` 找出全部 REQ 卡片(如 `docs/01-需求清单/**/*.md`)。
48 48 - 对每张卡片 `Grep` 残留占位:命中任一即缺口 —
49   - `【人工填写`、`TBD`、`待补`、`<示例`、`示例值`(结构化字段的 `示例值` 列若仍是模板默认占位)。
  49 + `【人工填写`、`TBD`、`待补`、`<示例`(用有区分度的 `<示例` 而非裸 `示例值`,避免误命中卡片合法表头行 `| ... | 示例值 |`;与 scope-lock E.1 写法一致)。
50 50 - 缺口表述示例:`REQ-USER-001 仍含 TBD / 示例值未替换为真实约束`。
51 51  
52   -2. **docs/07 secrets 全锁**(来自 A1 收集的 secret/account/package-name/namespace 清单)
53   - - `Read` `docs/07-环境配置.md`。
54   - - 校验:scope-lock 写入的每个 secret/account/package-name/namespace 字段均有真实值,无 `【人工填写`/`TBD`/空值。任一未填即缺口。
  52 +2. **secrets / 项目配置全锁**(来自 A1 收集的 secret/account/package-name/namespace 清单)
  53 + - `Read` `.env.local`(真实 secret 值所在;gitignored,docs/07 只记规则不记值):校验 `config-vars.yaml` 的 `secrets_ref` 列出的每个 secret 键(如 `DB_PASSWORD` / `JWT_SECRET`)均有真实值,无 `【人工填写`/`TBD`/空值。
  54 + - `Read` `config-vars.yaml`(非敏感项目级配置):校验包名 / namespace / 端口 / 初始账号等字段均已填,无 `【人工填写`/`TBD`。
  55 + - 任一未填即缺口。(docs/07-环境配置.md 仅承载规则/约定,不参与值校验。)
55 56  
56 57 3. **docs/04 §零 命令齐**(来自 A1 收集的每栈构建/lint/单测/e2e 命令)
57 58 - `Read` `docs/04-技术规范.md`,定位 `§ 零` 命令区。
... ... @@ -59,7 +60,7 @@ A 阶段所有 checkbox 均 `[x]` 时**不代表可以进 B 阶段**。Coding
59 60  
60 61 4. **docs/05 + docs/02 已评审**(来自 A5 downstream-gen 的评审闸)
61 62 - `Read` `docs/05-API接口契约.md` 与 `docs/02-开发计划.md`。
62   - - 校验:(a) docs/05 每个端点都有请求/响应 schema、无 `【人工填写`/`TBD`;(b) docs/02 每个 REQ 都在构建顺序 DAG 中、cycle-breaking 顺序有 `note` 说明;(c) downstream-gen 记录的「已人工评审」标记存在。缺任一即缺口。
  63 + - 校验:(a) docs/05 每个端点都有请求/响应 schema、无 `【人工填写`/`TBD`;(b) docs/02 每个 REQ 都在构建顺序 DAG 中、cycle-breaking 顺序有 `note` 说明。缺任一即缺口。(A5 父项已勾本身即蕴含 downstream-gen 评审闸已过——downstream-gen 在用户未确认时禁止勾 A5,故无需独立的「已评审」标记。)
63 64  
64 65 5. **A6 前端 scope 已锁**(来自 A6 frontend-scope-lock)
65 66 - `Read` `docs/06-UI交互规范.md`。
... ... @@ -75,7 +76,7 @@ A 阶段所有 checkbox 均 `[x]` 时**不代表可以进 B 阶段**。Coding
75 76  
76 77 已校验通过:
77 78 ✓ REQ 卡片均为真实数据(无占位/示例残留)
78   - ✓ docs/07 secrets/account/package/namespace 全锁
  79 + ✓ .env.local secrets + config-vars.yaml(account/package/namespace)全锁
79 80 ✓ docs/04 §零 各栈 build/lint/unit/e2e 命令齐全
80 81 ✓ docs/05 API 契约 + docs/02 构建顺序已评审
81 82 ✓ A6 前端 scope(UI 约定 / tokens / 组件库)已锁
... ... @@ -110,7 +111,7 @@ A 阶段所有 checkbox 均 `[x]` 时**不代表可以进 B 阶段**。Coding
110 111 <逐条列出每个缺口,格式:[闸门] 缺口描述 → 回填位置>
111 112 例:
112 113 [REQ 真实数据] REQ-USER-001 输入字段「示例值」列仍为模板占位 → docs/01-需求清单/...
113   - [docs/07 secrets] DB_PASSWORD 未填 → docs/07-环境配置.md
  114 + [secrets] DB_PASSWORD 未填 → .env.local
114 115 [docs/04 §零] node 栈缺 e2e 命令 → docs/04-技术规范.md §零
115 116  
116 117 补齐后再次运行 /erp-workflow:plan-start 重新校验。
... ...
skills/project-init/templates/CLAUDE-template.md
... ... @@ -172,7 +172,7 @@ B 阶段整体是**一个é™é»˜ Workflow 脚本 `workflows/coding.mjs`**(由ç˜
172 172 | # | 中断 | ä¾‹å­ |
173 173 | - | --- | --- |
174 174 | 1 | **测试åå¤å¤±è´¥** | åŒä¸€æµ‹è¯•åŒä¸€åŠŸèƒ½å†…è¿žç»­ **10 次**ä¿®å¤å¤±è´¥ |
175   -| 2 | **è¦æ”¹å¯†é’¥ / 账密 / 包å** | `docs/07-环境é…ç½®.md` 里由人工标注必须填的字段 |
  175 +| 2 | **è¦æ”¹å¯†é’¥ / 账密 / 包å** | 密钥 / 账密 在 `.env.local`ã€åŒ…å / 命å空间 / 端å£ç­‰åœ¨ `config-vars.yaml` é‡Œç”±äººå·¥æ ‡æ³¨å¿…é¡»å¡«çš„å­—æ®µï¼ˆè§„åˆ™è§ `docs/07-环境é…ç½®.md`) |
176 176 | 3 | **外部接å£ä¸å¯è¾¾** | 第三方 API 无法连接ã€è¯ä¹¦å¤±æ•ˆç­‰çŽ¯å¢ƒé—®é¢˜ï¼Œå¹¶æ— æ³•è‡ªè¡Œè§£å†³ |
177 177  
178 178 > 其余需è¦äººç±»åˆ¤æ–­çš„场景一律走普通 `AskUserQuestion` Q&A,ä¸ä¸­æ–­ã€ä¸å†™ Blocker 文件。
... ...
skills/project-init/templates/docs-08-initial-template.md
... ... @@ -50,7 +50,7 @@
50 50  
51 51 (A5 填入后,每行一个后端模块。每个模块的 `里程碑:` 字段在 `—` 和 `milestone/<id>` 之间变化,完成由本地 `git tag -l` 判定。`coding-start` 每次按 docs/02 REQ 序扫每模块的里程碑 tag 决定派发。后端模块全部打里程碑后自动进入 § 三 前端阶段。)
52 52  
53   -<!-- 模块格式示例(由 A5 downstream-gen 追加;功能子项由 feature-review 在 approve 时勾选):
  53 +<!-- 模块格式示例(由 A5 downstream-gen 追加;功能子项由 coding.mjs 的 review stage 在 approve 时勾选):
54 54 - module_0 系统管理
55 55 - 依赖: —
56 56 - 路径: backend/module/sys/
... ... @@ -62,7 +62,7 @@
62 62  
63 63 ## 三、Coding 阶段(前端整体)
64 64  
65   -(`frontend-start` 进入时扫 prototype/ + docs/01 + docs/05 → AI 自主推导 FE 业务功能清单写到下方"功能:"项(无人工审阅断点;合理性由整体里程碑标记时统一校核)。已有清单则直接加载。整个前端阶段 1 个里程碑 tag,分支 `frontend-phase`。)
  65 +(FE 业务功能清单在 Plan 期 A6 `frontend-scope-lock` 由 prototype/ + docs/01 + docs/05 推导后写入下方"功能:"项;Coding 阶段 `coding.mjs` 的 Router 把全部未完成 FE 聚合为单一 `frontend-phase` 阶段,排在所有后端模块之后。整个前端阶段 1 个里程碑 tag,分支 `frontend-phase`。)
66 66  
67 67 - 整体里程碑: —
68 68 - 功能:
... ...
skills/scope-lock/SKILL.md
1 1 ---
2 2 name: scope-lock
3   -description: A1 计划范围锁定——引导用户填写项目概述 + 技术栈 + 需求索引,并按模块子目录生成 REQ 卡片骨架(CC 推断 req_id/title/goal/rules/constraints/acceptance;输入/输出 字段表为结构化 6 列表单由人工逐行填真实数据);末尾执行 A1 终结校验:每张 REQ 卡片字段含真实数据、secrets/account/包名/命名空间字段名锁进 docs/07、build/lint/unit/e2e 命令锁进 docs/04 §零,缺则当场 AskUserQuestion 问清。
  3 +description: A1 计划范围锁定——引导用户填写项目概述 + 技术栈 + 需求索引,并按模块子目录生成 REQ 卡片骨架(CC 推断 req_id/title/goal/rules/constraints/acceptance;输入/输出 字段表为结构化 6 列表单由人工逐行填真实数据);末尾执行 A1 终结校验:每张 REQ 卡片字段含真实数据、配置字段名锁进 config-vars.yaml(非敏感填值 + secrets_ref 键名引用 .env.local)、build/lint/unit/e2e 命令锁进 docs/04 §零,缺则当场 AskUserQuestion 问清。
4 4 user-invocable: false
5 5 allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bash(node *) Bash(rm *)
6 6 ---
... ... @@ -9,6 +9,8 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
9 9  
10 10 # scope-lock
11 11  
  12 +> **关于 AskUserQuestion**:下文只描述「问什么、给哪些选项、各选项导向什么后续」。`header` / 各选项的 `description` / `multiSelect` 等具体参数由你按工具 schema 自行填全合法值——不要把下文的选项文字当成完整调用照抄。
  13 +
12 14 ## 执行步骤
13 15  
14 16 ### A. 提示用户填写项目概述并等待
... ... @@ -31,10 +33,10 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
31 33 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32 34 ```
33 35  
34   -用 `AskUserQuestion` 询问:
35   -- **question**: `项目概述填写完毕了吗?`
36   - - 用户选择「继续」→ 用 `Grep` 在 `CLAUDE.md` 搜索 `【人工填写:`(限定 § 🎯 项目概述 节)。命中 → 打印残留行 + 路径,重新弹出同样的 AskUserQuestion;0 命中 → 勾选并进入步骤 B。
37   - - 用户选择「有疑问想先沟通」→ 回答用户问题后,再次弹出同样的 QA。
  36 +用 `AskUserQuestion` 确认「项目概述填写完毕了吗?」,给「继续」和「有疑问想先沟通」两个选项。
  37 +
  38 +- 选「继续」→ 用 `Grep` 在 `CLAUDE.md` 搜索 `【人工填写:`(限定 § 🎯 项目概述 节)。命中 → 打印残留行 + 路径,重新确认一次;0 命中 → 勾选并进入步骤 B。
  39 +- 选「有疑问想先沟通」→ 回答用户问题后,再确认一次。
38 40  
39 41 0 命中后,用 `Edit` 在 `docs/08-模块任务管理.md` 中勾选:
40 42 - ` - [ ] 项目概述已填写(CLAUDE.md § 🎯 项目概述)`
... ... @@ -60,10 +62,10 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
60 62 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
61 63 ```
62 64  
63   -用 `AskUserQuestion` 询问:
64   -- **question**: `技术栈检查完毕了吗?`
65   - - 用户选择「继续」→ 进入步骤 C。
66   - - 用户选择「有疑问想先沟通」→ 回答用户问题后,再次弹出同样的 QA。
  65 +用 `AskUserQuestion` 确认「技术栈检查完毕了吗?」,给「继续」和「有疑问想先沟通」两个选项。
  66 +
  67 +- 选「继续」→ 进入步骤 C。
  68 +- 选「有疑问想先沟通」→ 回答用户问题后,再确认一次。
67 69  
68 70 完成后,用 `Edit` 在 `docs/08-模块任务管理.md` 中勾选:
69 71 - ` - [ ] 技术栈已确认(docs/04 § 零)`
... ... @@ -88,10 +90,10 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
88 90 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
89 91 ```
90 92  
91   -用 `AskUserQuestion` 询问:
92   -- **question**: `需求清单模块索引填写完毕了吗?`
93   - - 用户选择「继续」→ 进入步骤 D。
94   - - 用户选择「有疑问想先沟通」→ 回答用户问题后,再次弹出同样的 QA。
  93 +用 `AskUserQuestion` 确认「需求清单模块索引填写完毕了吗?」,给「继续」和「有疑问想先沟通」两个选项。
  94 +
  95 +- 选「继续」→ 进入步骤 D。
  96 +- 选「有疑问想先沟通」→ 回答用户问题后,再确认一次。
95 97  
96 98 完成后,用 `Edit` 在 `docs/08-模块任务管理.md` 中勾选:
97 99 - ` - [ ] 需求清单索引已填写(docs/01-需求清单/index.md)`
... ... @@ -150,14 +152,14 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
150 152 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
151 153 ```
152 154  
153   -用 `AskUserQuestion` 询问:
154   -- **question**: `所有 REQ 卡片的结构化字段都填成真实数据了吗?`
155   - - 用户选择「继续」→ 进入步骤 E(A1 终结校验)。
156   - - 用户选择「有疑问想先沟通」→ 回答用户问题后,再次弹出同样的 QA。
  155 +用 `AskUserQuestion` 确认「所有 REQ 卡片的结构化字段都填成真实数据了吗?」,给「继续」和「有疑问想先沟通」两个选项。
  156 +
  157 +- 选「继续」→ 进入步骤 E(A1 终结校验)。
  158 +- 选「有疑问想先沟通」→ 回答用户问题后,再确认一次。
157 159  
158 160 ### E. A1 终结校验(硬闸:全部通过才勾选 A1 顶层)
159 161  
160   -> 本步骤把原本会在编码期(feature-brainstorm / feature-plan)弹出的需求澄清 / config Q&A **全部前移到此处**。任一检查不满足,用 `AskUserQuestion` 在**此处(Plan 期)**问清并修订,**禁止**留待编码期。三项检查全过后才在 `docs/08` 勾选 `- [ ] A1 范围锁定 — scope-lock`。
  162 +> 本步骤把原本会在编码期(coding.mjs 的 spec / plan stage)弹出的需求澄清 / config Q&A **全部前移到此处**。任一检查不满足,用 `AskUserQuestion` 在**此处(Plan 期)**问清并修订,**禁止**留待编码期。三项检查全过后才在 `docs/08` 勾选 `- [ ] A1 范围锁定 — scope-lock`。
161 163  
162 164 #### E.1 真实数据校验(每张 REQ 卡片)
163 165  
... ... @@ -169,14 +171,16 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
169 171 - **`{{...}}` 残留**:不得残留任何 `{{` 占位(说明渲染未替换)。
170 172 3. 任一卡片不通过:打印不通过的卡片路径 + 具体缺口行,用 `AskUserQuestion` 引导用户当场补齐(可针对具体字段含义/校验规则发问),修订后重跑 E.1,直到全部通过。
171 173  
172   -#### E.2 secrets / account / 包名 / 命名空间字段名锁进 docs/07
  174 +#### E.2 配置字段名锁进 config-vars.yaml(敏感键名引用 .env.local)
173 175  
174   -1. 用 `Read` 读 `docs/04-技术规范.md` § 零技术栈,结合 REQ 卡片,盘点本项目需要的**敏感 / 专属配置字段名**,至少覆盖以下类别(按实际技术栈裁剪):
175   - - **secrets**:数据库密码、JWT/签名密钥、第三方 API key/secret、OSS/对象存储凭证、短信/邮件服务凭证等。
176   - - **account**:数据库账号、第三方服务账号、管理员初始账号等。
177   - - **包名 / 命名空间**:后端根包名(Java package / C# namespace / Python 顶层包 / Go module path 等)、前端 npm 包名 / scope。
178   -2. 用 `Read` 读 `docs/07-环境配置.md` § 零「人工占位速查表」。把上面盘点出的**每一个字段名**作为一行登记进该表(位置 / 含义 / 示例值三列),对应正文配置里以 `【人工填写:...】` 标记。**只锁字段名清单**(哪些值需要人工提供),不要求此刻填入真实 secret 值——真实值由人工在 docs/07 Step 3 填,但字段清单必须在此锁全。
179   -3. 用 `AskUserQuestion` 向用户确认:「以下 secret/account/包名/命名空间字段已登记进 docs/07 速查表,是否齐全?还有遗漏的吗?」展示登记的字段名清单。用户补充则继续登记,直到确认齐全。
  176 +1. 用 `Read` 读 `docs/04-技术规范.md` § 零技术栈,结合 REQ 卡片,盘点本项目需人工提供 / 确认的配置字段,分两类:
  177 + - **非敏感、项目级**:后端根包名 / 命名空间(Java package / C# namespace / Python 顶层包 / Go module path 等)、应用端口、前端包名 / scope、前端 dev 端口、管理员初始账号等。
  178 + - **敏感凭据**:数据库密码、JWT / 签名密钥、Redis 密码、第三方 API key/secret、OSS / 对象存储凭证、短信 / 邮件服务凭证、管理员初始密码等。
  179 +2. 用 `Read` 读模板 `${CLAUDE_SKILL_DIR}/templates/config-vars-template.yaml`,用 `Write` 落盘到仓库根 `config-vars.yaml`:
  180 + - **非敏感字段**:按技术栈推断默认值直接填入(如 `http_port: 8080`、`base_package: com.<org>.<app>`);无对应技术栈的整节删除(如纯后端项目删 `frontend:`);无法可靠推断的留 `【人工填写:…】`(A2 skeleton-gen 补填)。
  181 + - **敏感字段**:只把「键名 + 含义」登记进 `secrets_ref` 列表,**绝不写真实值**(真实值由 A2 写进 `.env.local`,A2 据 `secrets_ref` 核对齐全)。
  182 +3. **不**在此创建 / 填写 `docs/07-环境配置.md`——它由 A2 skeleton-gen 生成,只记规则不记具体值。
  183 +4. 用 `AskUserQuestion` 向用户确认:「以下配置 / 凭据字段已登记进 config-vars.yaml,是否齐全?还有遗漏的吗?」展示非敏感字段 + `secrets_ref` 键名清单。用户补充则继续登记,直到确认齐全。
180 184  
181 185 #### E.3 build / lint / unit / e2e 命令锁进 docs/04 § 零
182 186  
... ... @@ -205,7 +209,7 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
205 209 ✓ docs/01-需求清单/index.md 模块索引
206 210 ✓ docs/01-需求清单/<module>/_module.md 模块头
207 211 ✓ docs/01-需求清单/<module>/REQ-*.md 字段表已填真实数据
208   - ✓ docs/07 § 零 secrets/account/包名/命名空间字段清单已锁
  212 + ✓ config-vars.yaml 配置字段 + secrets_ref 键名已锁(敏感真实值留待 A2 .env.local)
209 213  
210 214 运行以下命令继续进入 A2:
211 215 /erp-workflow:plan-start
... ... @@ -220,7 +224,8 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) Bas
220 224 - `docs/01-需求清单/index.md`(模块索引输入)
221 225 - `docs/01-需求清单/<module>/_module.md`(模块头输出)
222 226 - `docs/01-需求清单/<module>/REQ-*.md`(REQ 卡片骨架输出,A3 db-design-gen / A5 downstream-gen 会回填 TBD 字段)
223   -- `docs/07-环境配置.md` § 零(A1 终结校验 E.2:secrets/account/包名/命名空间字段名锁定)
  227 +- `config-vars.yaml`(A1 终结校验 E.2 输出:非敏感配置字段填值 + secrets_ref 键名锁定;仓库根)
  228 +- `${CLAUDE_SKILL_DIR}/templates/config-vars-template.yaml`(E.2 渲染来源;跨栈中立,YAML 注释引导)
224 229 - `${CLAUDE_SKILL_DIR}/templates/req-card-template.md`(结构化 6 列字段表)
225 230 - `${CLAUDE_SKILL_DIR}/templates/_module-template.md`
226 231 - `${CLAUDE_PLUGIN_ROOT}/lib/render.mjs`(步骤 D 渲染助手;CLI 形态 `node render.mjs <模板> <vars.json> <输出>`,字面量安全 + 自动剥 HTML 注释)
... ...
skills/scope-lock/templates/config-vars-template.yaml 0 → 100644
  1 +# config-vars.yaml — 项目配置清单
  2 +#
  3 +# 用途:登记「需人工提供 / 确认」的非敏感、项目级配置(根包名、应用端口、前端包名、初始账号等)。
  4 +# YAML 分组,逐项填写与核对一目了然。
  5 +# 规则:命名约定 / 端口约定 / 安全要求统一写在 docs/07-环境配置.md,本文件只放值、不重复写规则。
  6 +# 边界:
  7 +# - 机器 / 环境相关的连接信息(DB_HOST / DB_PORT / DB_USER / DB_SCHEMA 等)→ 仓库根 .env.local,不写在此。
  8 +# - 敏感凭据(密码 / 密钥 / token)→ .env.local;本文件 secrets_ref 只登记键名供核对,绝不写真实值。
  9 +# 填写:A1 scope-lock 按 docs/04 § 零 技术栈推断默认值填入;无对应技术栈的整节删除;无法推断的留 【人工填写:…】(A2 skeleton-gen 补填)。
  10 +
  11 +backend:
  12 + base_package: 【人工填写:后端根包名 / 命名空间,如 com.acme.erp】
  13 + http_port: 【人工填写:后端 HTTP 端口,默认 8080】
  14 +
  15 +frontend:
  16 + pkg_name: 【人工填写:前端包名,如 acme-erp-web】
  17 + dev_port: 【人工填写:前端开发服务器端口,默认 5173】
  18 +
  19 +admin_init:
  20 + username: 【人工填写:超级管理员初始账号,如 admin】
  21 + # 初始密码属敏感 → 见 .env.local 的 ADMIN_INIT_PASSWORD
  22 +
  23 +# 敏感值引用:真实值在 .env.local,此处只登记「键名 + 含义」,供 A2 skeleton-gen 核对 .env.local 是否齐全。
  24 +# 按技术栈增删行(注释行表示可选,按需取消注释)。
  25 +secrets_ref:
  26 + - DB_PASSWORD # 数据库密码
  27 + - JWT_SECRET # JWT / 令牌签名密钥
  28 + # - REDIS_PASSWORD # 缓存 / 会话(用 Redis 时)
  29 + # - ADMIN_INIT_PASSWORD # 超级管理员初始密码(有初始账号时)
  30 + # - OSS_ACCESS_KEY_SECRET / SMS_API_SECRET ... # 第三方凭证按需添加
... ...
skills/skeleton-gen/SKILL.md
... ... @@ -34,6 +34,8 @@ allowed-tools: Read Write Edit Skill Grep Glob AskUserQuestion Bash(node *)
34 34 | `docs/07-环境配置.md` | `${CLAUDE_SKILL_DIR}/templates/docs-07-env-template.md` |
35 35 | `docs/09-项目目录结构.md` | `${CLAUDE_SKILL_DIR}/templates/docs-09-structure-template.md` |
36 36  
  37 +> **docs/07 只记规则,不记具体配置值**:端口 / 包名 / 库名 / 初始账号等真实值在 `config-vars.yaml`(A1 scope-lock 已锁)与 `.env.local`(敏感)里,docs/07 只写命名约定、端口约定、安全规则,并指向这两个文件。
  38 +
37 39 项目专属标识(根包名 / 命名空间)保留 `【人工填写:<说明>】` 占位,等人工在 docs/09 顶部补填一次后,其他文件复用。
38 40  
39 41 ### B.2 追加 docs/04 § 一+(保留 § 零 不覆盖)
... ... @@ -61,6 +63,8 @@ docs/04 已由 scope-lock 写入 § 零。本步骤追加 § 一 ~ 三。
61 63 | `${CLAUDE_SKILL_DIR}/templates/styles-tokens-template.css` | `src/styles/tokens.css` |
62 64 | (空文件) | `sql/migrations/.gitkeep` |
63 65  
  66 +> **`.env.local` 须与 `config-vars.yaml` 的 `secrets_ref` 对齐**:写完 `.env.local` 后用 `Read` 读仓库根 `config-vars.yaml` 的 `secrets_ref` 列表,逐个核对键名。env-local-template 已含 `DB_*` / `JWT_SECRET`;`secrets_ref` 里其余项目专属键(如 `REDIS_PASSWORD` / `ADMIN_INIT_PASSWORD` / 第三方凭证)用 `Edit` 追加到 `.env.local`,值统一为 `【人工填写:<说明>】`。
  67 +
64 68 #### C.2 渲染 scripts/test.mjs
65 69  
66 70 用 `Read` 读取 `${CLAUDE_SKILL_DIR}/templates/scripts-test-template.mjs`,基于步骤 A 的技术栈(docs/04 § 零)为 7 个占位推断命令后用 `Write` 写到 `scripts/test.mjs`:
... ... @@ -101,8 +105,9 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/merge-gitignore.mjs&quot; .gitignore &quot;${CLAUDE_SKILL_
101 105  
102 106 #### E.1 扫描 + 分组
103 107  
104   -用 `Grep` 在以下 8 个路径扫 `【人工填写:`,记录命中(文件 / 行号 / 说明):
  108 +用 `Grep` 在以下路径扫 `【人工填写:`,记录命中(文件 / 行号 / 说明):
105 109 - `docs/04-技术规范.md` / `docs/06-UI交互规范.md` / `docs/07-环境配置.md` / `docs/09-项目目录结构.md`
  110 +- `config-vars.yaml`(A1 留下的未推断字段,如包名 / 端口)
106 111 - `scripts/*.mjs` / `.gitignore`
107 112 - `.env.local`
108 113  
... ... @@ -124,12 +129,12 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/merge-gitignore.mjs&quot; .gitignore &quot;${CLAUDE_SKILL_
124 129 #### E.4 验证 + QA 闸门
125 130  
126 131 循环直到两条件**同时**满足:
127   -(a) `Grep` 重新扫 8 路径,0 命中
  132 +(a) `Grep` 重新扫 E.1 全部路径,0 命中
128 133 (b) 用户 `AskUserQuestion` 选「继续」
129 134  
130 135 每次弹 QA 前重扫;有残留则打印残留位置清单(文件:行号 — 说明)+ 再弹 QA。
131 136  
132   -QA 横幅涵盖:产出文件清单(docs/04 / 06 / 07 / 09 + scripts/*.mjs + .env.local + .gitignore)、占位状态(N=0 或待填清单)、「继续」/「有疑问先沟通」两选项。
  137 +QA 横幅涵盖:产出文件清单(docs/04 / 06 / 07 / 09 + config-vars.yaml + scripts/*.mjs + .env.local + .gitignore)、占位状态(N=0 或待填清单)、「继续」/「有疑问先沟通」两选项。
133 138  
134 139 通过后(N=0 且用户选「继续」),用 `Edit` 在 `docs/08-模块任务管理.md` 中勾选:
135 140 - `- [ ] A2 骨架生成 — skeleton-gen`
... ... @@ -148,7 +153,8 @@ QA 横幅涵盖:产出文件清单(docs/04 / 06 / 07 / 09 + scripts/*.mjs +
148 153 - `${CLAUDE_SKILL_DIR}/templates/docs-09-structure-template.md`(大纲)
149 154 - `${CLAUDE_SKILL_DIR}/templates/scripts-test-template.mjs`(推断命令填充 7 槽:backend/frontend × build/lint/test + e2e;缺席 stack 填 `echo skip`)
150 155 - `${CLAUDE_SKILL_DIR}/templates/scripts-setup-test-db-template.mjs`(0 槽位,跨平台 .env.local 解析 + DROP/CREATE)
151   -- `${CLAUDE_SKILL_DIR}/templates/env-local-template`(0 槽位)
  156 +- `${CLAUDE_SKILL_DIR}/templates/env-local-template`(0 槽位;C.1 据 `config-vars.yaml` 的 `secrets_ref` 追加项目专属 secret 键)
  157 +- `config-vars.yaml`(A1 scope-lock 输出,仓库根;C.1 读其 `secrets_ref` 对齐 `.env.local`,E.1 扫其残留 `【人工填写:`)
152 158 - `${CLAUDE_SKILL_DIR}/templates/gitignore-append-template`(0 槽位)
153 159 - `${CLAUDE_SKILL_DIR}/templates/styles-tokens-template.css`(0 槽位,样式 token 骨架)
154 160 - `${CLAUDE_PLUGIN_ROOT}/lib/merge-gitignore.mjs`(.gitignore 逐行判重并集合并,跨平台纯 Node)
... ...
skills/skeleton-gen/templates/docs-06-static-template.md
... ... @@ -6,7 +6,7 @@ skeleton-gen 读取 docs/04 § 零 和 docs/01 index,按下述大纲生成项
6 6  
7 7 # 06-UI交互规范
8 8  
9   -> 本项目所有页面布局以项目根 `prototype/` 目录下的静态 HTML mockup 为权威。前端阶段(fe-feature-*)实现时直接以 prototype/ HTML 推导组件树与样式。本文件仅承载跨页面通用规则与 Design Tokens。
  9 +> 本项目所有页面布局以项目根 `prototype/` 目录下的静态 HTML mockup 为权威。前端阶段实现时直接以 prototype/ HTML 推导组件树与样式。本文件仅承载跨页面通用规则与 Design Tokens。
10 10  
11 11 ## 一、通用交互规则
12 12  
... ...
skills/skeleton-gen/templates/docs-07-env-template.md
1 1 <!--
2 2 本文件是 docs/07-环境配置.md 的大纲。
  3 +docs/07 只记规则与约定,不记具体配置值——端口 / 包名 / 库名等真实值在 config-vars.yaml(A1 锁定)与 .env.local(敏感)。
3 4 skeleton-gen 基于 docs/04 § 零 技术栈表推导各节内容:
4 5 § 一 依赖清单 → 从技术栈的每一行技术推导运行时和构建依赖
5   - § 二 端口约定 → 从后端/前端/数据库/缓存/反向代理 各取默认端口
  6 + § 二 端口约定 → 从后端/前端/数据库/缓存/反向代理 各取默认端口(约定值;项目实际采用值记在 config-vars.yaml)
6 7 § 四 常用命令 → 基于构建工具、包管理器给出开发者最常用命令
7 8 -->
8 9  
... ... @@ -14,12 +15,19 @@ skeleton-gen 基于 docs/04 § 零 技术栈表推导各节内容:
14 15  
15 16 ## 二、端口约定
16 17  
17   -<!-- 表格:| 服务 | 端口 | 说明 |;至少列 后端 HTTP / 前端 dev / 数据库 / 缓存 / 反代。 -->
  18 +<!-- 表格:| 服务 | 端口 | 说明 |;至少列 后端 HTTP / 前端 dev / 数据库 / 缓存 / 反代。这里给默认约定值;项目实际采用的端口记在 config-vars.yaml。 -->
18 19  
19   -## 三、环境变量
  20 +## 三、配置与凭据规则
20 21  
21   -运行时凭据(数据库连接、JWT 密钥等)全部放在仓库根的 `.env.local`,不入 git。
22   -字段清单与占位符见该文件,真实值由开发者本地填写。
  22 +项目配置分两处存放,**本文档只记规则、不记具体值**:
  23 +
  24 +- **非敏感、项目级配置**(根包名 / 命名空间、应用端口、前端包名、管理员初始账号等)→ 仓库根 `config-vars.yaml`,结构化 YAML,随项目提交。
  25 +- **敏感凭据**(数据库密码、JWT / 签名密钥、Redis 密码、第三方 key/secret、管理员初始密码等)→ 仓库根 `.env.local`,入 `.gitignore`,**不提交**;`config-vars.yaml` 末尾 `secrets_ref` 只登记键名作引用。
  26 +
  27 +规则:
  28 +- 根包名 / 命名空间一经在 `config-vars.yaml` 锁定,全项目复用,不得各模块各写。
  29 +- 端口遵循 § 二 约定;调整时改 `config-vars.yaml`,本文档不写具体端口。
  30 +- 任何敏感值不得出现在 `config-vars.yaml`、docs、源码或日志中——只允许出现在 `.env.local`。
23 31  
24 32 ## 四、常用命令
25 33  
... ...
workflows/coding.mjs
... ... @@ -87,11 +87,12 @@ function routerPrompt(root) {
87 87 '- 前端 item(FE-NN)归属一个"逻辑前端模块"。前端阶段整体 `done` 当且仅当 §三 `整体里程碑:` == `milestone/frontend-phase` 且 `git tag -l "milestone/frontend-phase"` 存在。',
88 88 '',
89 89 '## 输出(必须符合下发的 JSON schema)',
90   - '- `modules`: 数组,按 `docs/02 § 二` 的模块顺序排列。每项:',
91   - ' - `id`: 模块标识(后端为英文蛇形 module id;前端聚合为单一逻辑模块时用 `frontend-phase`)。',
  90 + '- `modules`: 数组。**先**按 `docs/02 § 二` 的模块顺序列出全部后端模块,**再在末尾追加唯一一个前端聚合模块**(仅当存在前端 FE 时)。每项:',
  91 + ' - `id`: 模块标识(后端为英文蛇形 module id;前端聚合模块固定用 `frontend-phase`)。',
92 92 ' - `done`: 该模块是否已完成(按上面的判定)。',
93   - ' - `reqs`: 本模块**未完成**后端 REQ 的有序列表(已 `verdict=approve`(见 `docs/superpowers/reviews/*-<REQ>.md`)的 REQ 跳过)。模块已 done → 空数组。',
94   - ' - `feItems`: 本模块关联的**未完成**前端 FE-NN 列表(已 approve 的 FE 跳过);无前端 → 空数组。',
  93 + ' - `reqs`: **仅后端模块**填本模块**未完成**后端 REQ 的有序列表(已 `verdict=approve`,见 `docs/superpowers/reviews/*-<REQ>.md` 的 REQ 跳过);模块已 done → 空数组。**前端聚合模块 `reqs` 恒为空数组**。',
  94 + ' - `feItems`: **仅前端聚合模块**填——把**全部模块**的**未完成**前端 FE-NN 汇总为一个有序列表(已 approve 的 FE 跳过)放进 `frontend-phase` 这一项。**后端模块 `feItems` 恒为空数组**(前端不分摊到后端模块)。',
  95 + '- 即:后端模块只承载 `reqs`、`feItems=[]`;末尾的 `frontend-phase` 模块只承载 `feItems`、`reqs=[]`。整个项目至多一个前端聚合模块,对应至多一个 `milestone/frontend-phase` tag。',
95 96 '- 不要返回任何额外字段(schema 为 `additionalProperties:false`)。',
96 97 '',
97 98 '## 缺值处理',
... ... @@ -332,7 +333,7 @@ function crossModulePrompt(module) {
332 333 `替代被删的 \`log-cross-module\` hook + \`cross-module-log\` skill:扫描本模块周期内对**非本模块**文件的改动,落跨模块日志(原因 + 影响评估),供 module-report § ⑦ 嵌入。`,
333 334 '',
334 335 '## 流程',
335   - `- 用 \`git -C ${ROOT} diff --name-status\`(区间:模块分支起点 → HEAD)找出改动文件,判定哪些落在**其它模块**的目录下(按 docs/09 目录归属)。`,
  336 + `- 用 \`git -C ${ROOT} diff --name-status <默认分支 main/master>...HEAD\`(三点 diff,区间 = 本功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)找出改动文件,判定哪些落在**其它模块**的目录下(按 docs/09 目录归属)。`,
336 337 `- 写 / 更新 \`${ROOT}/docs/superpowers/module-reports/${id}-cross-module.md\`,每行列:时间戳(你自身上下文解析当天,脚本不传)/ 目标模块 / 文件 / 改动摘要 / **原因**(本模块哪个 REQ 迫使改它)/ **影响评估**(目标模块哪些 API / 行为 / 调用方受影响、现有测试是否仍有效、是否需新测试,1-3 句)。`,
337 338 '- 无跨模块改动 → 输出 `cross-module-log: 无跨模块改动,跳过`,不创建文件。',
338 339 '- **不要留 `TBD(CC 补)`**:本步骤就是补齐的唯一时机;推不出原因/影响 → 按硬约束写阻塞点并失败。',
... ... @@ -362,14 +363,14 @@ function reportPrompt(module) {
362 363 ? [
363 364 '- § ① `module_id = frontend-phase`,`module_name = 前端阶段(整体)`。',
364 365 `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`,
365   - `- § ③ 文件变更:\`git -C ${ROOT} diff --stat\`(区间 \`frontend-phase\` 分支起点 → HEAD)。`,
  366 + `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`,
366 367 '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。',
367 368 `- § ⑤:读 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate.md\`。`,
368 369 '- § ⑧ 偏离清单:额外审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出。',
369 370 '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。',
370 371 ].join('\n')
371 372 : [
372   - `- § ③ 文件变更:\`git -C ${ROOT} diff --stat\` / \`--name-status\` / \`git log --oneline\`(区间:module 分支起点 → HEAD)。`,
  373 + `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\` / \`--name-status\` / \`git log <默认分支>..HEAD --oneline\`(区间 = 功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)。`,
373 374 `- § ② / § ⑨:读 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-<本模块 REQ>.md\`。`,
374 375 `- § ⑤:读 \`${ROOT}/docs/superpowers/module-reports/${id}-test-gate.md\`。`,
375 376 `- § ⑥ Migration:\`git -C ${ROOT} diff --name-only --diff-filter=A -- 'sql/migrations/V*.sql'\` 列新增,每个读第一行作说明。`,
... ... @@ -414,6 +415,33 @@ function milestonePrompt(module) {
414 415 ].join('\n')
415 416 }
416 417  
  418 +// ---- 功能分支生命周期:进入模块前建/切功能分支(milestone 的 merge 源)----
  419 +// 幂等支持续跑:分支已存在则 checkout 续跑,否则从默认分支开新支。
  420 +function branchSetupPrompt(module) {
  421 + const id = module?.id ?? '<module>'
  422 + const fe = id === 'frontend-phase'
  423 + const branch = fe ? 'frontend-phase' : `module-${id}`
  424 + return [
  425 + `# branch-setup — ${fe ? '前端阶段' : `模块 ${id}`} 功能分支准备(幂等)`,
  426 + '',
  427 + commonContract(fe ? 'frontend' : 'backend'),
  428 + '',
  429 + '## 目标',
  430 + `为本${fe ? '前端阶段' : '模块'}准备功能分支 \`${branch}\`,使后续 featureLoop / testGate / report 的 commit 都落在该分支上;milestone stage 再把它 \`merge --no-ff\` 回默认分支。**本 stage 内重入幂等**。`,
  431 + '',
  432 + '## 流程(顺序执行,任一硬错误 → 停下打印诊断,不自动 stash / 覆盖)',
  433 + `1. **探测默认分支**:用 \`git -C ${ROOT} rev-parse --verify\` 依次试本地 \`main\` / \`master\`,取第一个存在的为 \`default_branch\`;都不存在 → 失败。`,
  434 + `2. **校验工作树干净**:\`git -C ${ROOT} status --porcelain\` 非空 → 失败并打印 dirty 文件清单(进入模块前必须是干净状态)。`,
  435 + `3. **建 / 切功能分支**(幂等):`,
  436 + ` - 若 \`git -C ${ROOT} rev-parse --verify ${branch}\` 成功(分支已存在,续跑场景)→ \`git -C ${ROOT} checkout ${branch}\`。`,
  437 + ` - 否则 → \`git -C ${ROOT} checkout <default_branch>\` 后 \`git -C ${ROOT} checkout -b ${branch}\`(从含上一里程碑成果的默认分支开新支)。`,
  438 + `4. 确认当前已在 \`${branch}\`:\`git -C ${ROOT} rev-parse --abbrev-ref HEAD\` == \`${branch}\`,否则失败。`,
  439 + '',
  440 + '## 结束',
  441 + `- 输出一行 \`branch-setup: ${id} → ${branch}\`。`,
  442 + ].join('\n')
  443 +}
  444 +
417 445 // ============================================================================
418 446 // 编排逻辑(结构按 plan 骨架;featureLoop / reviewWithFixLoop / testGate / 顶层循环)
419 447 // ============================================================================
... ... @@ -430,11 +458,15 @@ async function featureLoop(items, phase) {
430 458 }
431 459  
432 460 // 有界 5 轮修复;超出 → throw(终止态,非对话框)
  461 +// fix 后重新跑 verify(功能复验,verify 内部失败即 throw → halt),再进入下一轮 review,
  462 +// 使 fixPrompt 对子代理"上层会重新跑 verify + review"的承诺成真。
433 463 async function reviewWithFixLoop(id, phase, verifyResult) {
  464 + const grp = phase === 'backend' ? 'Backend' : 'Frontend'
434 465 for (let round = 1; round <= 5; round++) {
435   - const r = await agent(reviewPrompt(id, phase, round), {label:`review:${phase}:${id}:r${round}`, phase: phase==='backend'?'Backend':'Frontend', schema: REVIEW_SCHEMA, agentType:'code-reviewer'})
  466 + const r = await agent(reviewPrompt(id, phase, round), {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'code-reviewer'})
436 467 if (r.verdict === 'approve') return { id, phase, approved:true, rounds:round }
437   - await agent(fixPrompt(id, phase, r.issues), {label:`fix:${phase}:${id}:r${round}`, phase: phase==='backend'?'Backend':'Frontend'})
  468 + await agent(fixPrompt(id, phase, r.issues), {label:`fix:${phase}:${id}:r${round}`, phase: grp})
  469 + await agent(verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验)`), {label:`reverify:${phase}:${id}:r${round}`, phase: grp})
438 470 }
439 471 throw new Error(`HALT review-unresolved ${phase}:${id} after 5 rounds`)
440 472 }
... ... @@ -456,10 +488,17 @@ log(`coding: ${todo.length}/${routed.modules.length} modules to run`)
456 488 const results = []
457 489 for (const module of todo) {
458 490 try {
459   - await featureLoop(module.reqs, 'backend')
460   - await testGate(module, 'backend')
461   - if (module.feItems.length) { await featureLoop(module.feItems, 'frontend'); await testGate(module, 'frontend') }
462   - await agent(crossModulePrompt(module), {label:`xmod:${module.id}`, phase:'Milestone'}) // 替代被删 hook
  491 + // C1:进入模块前建/切功能分支(milestone 的 merge 源)。
  492 + await agent(branchSetupPrompt(module), {label:`branch:${module.id}`, phase:'Milestone'})
  493 + if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过)
  494 + await featureLoop(module.reqs, 'backend')
  495 + await testGate(module, 'backend')
  496 + await agent(crossModulePrompt(module), {label:`xmod:${module.id}`, phase:'Milestone'}) // 替代被删 hook
  497 + }
  498 + if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块)
  499 + await featureLoop(module.feItems, 'frontend')
  500 + await testGate(module, 'frontend')
  501 + }
463 502 await agent(reportPrompt(module), {label:`report:${module.id}`, phase:'Milestone'})
464 503 await agent(milestonePrompt(module), {label:`milestone:${module.id}`, phase:'Milestone'}) // git merge --no-ff + tag + 更新 docs/08(单 stage 内幂等)
465 504 results.push({ module: module.id, status:'done' })
... ...