Commit 2463c1e59126e07a9a3ade4d99ba84a2bda484de
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。
Showing
19 changed files
with
368 additions
and
98 deletions
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('parseEnv on empty / non-string input returns empty object', () => { |
| 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('strips HTML comments used as template guides', () => { |
| 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('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 "${CLAUDE_PLUGIN_ROOT}/lib/merge-gitignore.mjs" .gitignore "${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 "${CLAUDE_PLUGIN_ROOT}/lib/merge-gitignore.mjs" .gitignore "${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' }) | ... | ... |