diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index dba87c4..d400862 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "erp-workflow", - "description": "ERP 项目全流程框架:阶段 A 计划(plan-start 入口 + A0~A5 共 6 个 skill + B 阶段瘦入口 coding-start = 8 个 skill;plan-start 终结闸 4 项前移硬校验) + 阶段 B 编码(单个静默 Workflow 脚本 coding.mjs,子代理自动跑后端+前端功能循环、测试闸门、本地里程碑 tag)。", - "version": "0.2.0", + "description": "ERP 项目全流程框架:阶段 A 计划(plan-start 入口 + A0~A5 共 6 个 skill + 增量需求入口 add-req + B 阶段瘦入口 coding-start = 9 个 skill;plan-start 终结闸 4 项前移硬校验;add-req 经 req-ledger 哈希台账识别后续新增/变更需求只跑增量) + 阶段 B 编码(单个静默 Workflow 脚本 coding.mjs,子代理自动跑后端+前端功能循环、测试闸门、本地里程碑 tag)。", + "version": "0.3.0", "skills": [ "./skills/plan/plan-start", "./skills/plan/project-init", @@ -10,6 +10,7 @@ "./skills/plan/db-design-gen", "./skills/plan/db-init", "./skills/plan/downstream-gen", + "./skills/plan/add-req", "./skills/coding/coding-start" ] } diff --git a/README.md b/README.md index b9f9e45..b345485 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 ``` erp-workflow-plugin/ ├── .claude-plugin/ -│ └── plugin.json # 插件清单,显式列出 8 个 skill 路径 +│ └── plugin.json # 插件清单,显式列出 9 个 skill 路径 ├── README.md # 本文档 ├── workflows/ │ └── coding.mjs # 阶段 B:整个编码阶段编排为单个静默 Workflow @@ -103,30 +103,33 @@ erp-workflow-plugin/ │ ├── validate-ddl.mjs # docs/03 ↔ DDL 4 维校验(替代 validate.sh) │ ├── yaml-config.mjs # config-vars.yaml 极简 YAML 读取(2 层 map + 标量) │ ├── apply-ddl.mjs # 解析 config-vars.yaml database: 段 + mysql2 apply +│ ├── req-ledger.mjs # 需求台账:docs/01 REQ + docs/08 §三 FE 内容哈希,scan/commit 识别新增/变更(add-req 用) │ └── *.test.mjs # 各助手的 node:test 单测 ├── agents/ │ └── code-reviewer.md # 统一 reviewer(coding.mjs review stage 调用,phase 选维度集) └── skills/ # 按阶段分组(slug 不变,由 SKILL.md frontmatter name 决定) - ├── plan/ # 阶段 A:7 个 skill(入口 + A0~A5) + ├── plan/ # 阶段 A:8 个 skill(入口 + A0~A5 + 增量入口) │ ├── plan-start/ # A 阶段入口 + Plan 终结硬闸 │ ├── project-init/ # A0 │ ├── scope-lock/ # A1 │ ├── skeleton-gen/ # A2 │ ├── db-design-gen/ # A3 │ ├── db-init/ # A4 - │ └── downstream-gen/ # A5(含前端 FE 清单推导,原 A6 已并入) + │ ├── downstream-gen/ # A5(含前端 FE 清单推导,原 A6 已并入) + │ └── add-req/ # 增量需求入口(Plan 完结后追加/修改需求,经 req-ledger 哈希台账只跑增量) └── coding/ # 阶段 B:1 个 skill(瘦入口) └── coding-start/ # 启动 workflows/coding.mjs ``` -## Skill 清单(8 个) +## Skill 清单(9 个) -### 入口(2 个) +### 入口(3 个) | Skill | 作用 | 谁调用 | |---|---|---| | `plan-start` | **A 阶段入口 + Plan 终结硬闸**。读 docs/08 § 一 找第一个未勾 A 子项 → 派发对应 A skill;A 全部完成时校验 4 项前移闸门(REQ 真实数据、`config-vars.yaml` 全部配置(含 DB 凭据 / 密钥)全锁、docs/04 § 零 命令齐、docs/05+02 已评审),全过才提示运行 `/erp-workflow:coding-start`,否则指出缺口不放行 | **用户手动** `/erp-workflow:plan-start` | | `coding-start` | **B 阶段瘦入口**(`allowed-tools: Read Glob Workflow Bash(git ...)`)。校验 Plan 完成态(docs/08 § 一 全勾)+ 取得 projectRoot(git 就绪由 coding.mjs runBranchSetup 在运行时把守)→ 读 docs/08 § 二/§ 三 + `git tag -l 'milestone/*'` 概述阶段进度(Workflow Router 再用 `req-done/*` 判定功能级 resume)→ 调用 `Workflow({scriptPath:"${CLAUDE_PLUGIN_ROOT}/workflows/coding.mjs", args:{projectRoot}})` 启动整个编码阶段 → 告知"已在后台启动" | **用户手动** `/erp-workflow:coding-start` | +| `add-req` | **增量需求入口**(Plan 完结后追加 / 修改需求时用)。`node lib/req-ledger.mjs scan` 用内容哈希台账(`.req-ledger.json`)识别 docs/01 REQ 卡片 + docs/08 §三 FE 行的**新增 / 变更**;只对增量做 **A3-delta**(写新 `V_n` migration + 同步 docs/03,**绝不改 V1**)+ **A5-delta**(补 docs/05 端点 / docs/02 顺序 / docs/08 模块行 / FE 行),并 `git tag -d` 作废**变更**单元已有的 `req-done/*`(及所属 `milestone/*`,复位 docs/08 里程碑字段),使 coding.mjs Router 只重跑增量。首跑无台账则先建基线。 | **用户手动** `/erp-workflow:add-req` | ### Plan 阶段 A skill(A0~A5,共 6 个) diff --git a/lib/req-ledger.mjs b/lib/req-ledger.mjs new file mode 100644 index 0000000..964251c --- /dev/null +++ b/lib/req-ledger.mjs @@ -0,0 +1,181 @@ +// lib/req-ledger.mjs — 需求台账(增量识别) +// 跟踪 docs/01 REQ 卡片 + docs/08 §三 FE 行的内容哈希,识别后续新增 / 变更 / 删除的 +// 需求单元,使 Coding 阶段只跑增量、不必整仓重跑(变更项由调用方作废其 req-done tag)。 +// +// 用法(CLI): +// node lib/req-ledger.mjs scan → 打印 JSON {new,changed,removed,unchanged} +// node lib/req-ledger.mjs commit → 把当前哈希写入 /.req-ledger.json +// node lib/req-ledger.mjs status → 同 scan,但人类可读摘要打到 stderr +// 退出码:0 = 成功;2 = 用法 / 路径错误。 +// +// 程序内:import { computeUnits, loadLedger, diffLedger, writeLedger } from './req-ledger.mjs' +// +// 台账文件 /.req-ledger.json(随项目提交,机器状态,调用方勿手改): +// { "version": 1, "units": { "": { "kind": "req"|"fe", "hash": "" } } } +// +// 单元(unit)= Router 能据以 resume 的最小完成单位: +// - kind=req:后端 REQ 卡片,id = 卡片文件名(去 .md),== req_id +// - kind=fe :前端功能行,id = FE-NN(取自 docs/08 §三) + +import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs' +import { join, basename } from 'node:path' +import { createHash } from 'node:crypto' + +const LEDGER_BASENAME = '.req-ledger.json' +const REQ_DIR_REL = join('docs', '01-需求清单') +const DOCS08_REL = join('docs', '08-模块任务管理.md') + +export const ledgerPath = (root) => join(root, LEDGER_BASENAME) + +// ── 哈希:行尾归一(去 \r + 去行尾空白),整体 trim,sha256 取前 12 位十六进制 ── +export function hashContent(text) { + const norm = String(text) + .split('\n') + .map((l) => l.replace(/\r$/, '').replace(/[ \t]+$/, '')) + .join('\n') + .trim() + return createHash('sha256').update(norm, 'utf8').digest('hex').slice(0, 12) +} + +// ── 递归列出目录下所有 .md(用于 docs/01-需求清单)───────────────── +function walkMd(dir) { + const out = [] + let entries + try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out } + for (const e of entries) { + const full = join(dir, e.name) + if (e.isDirectory()) out.push(...walkMd(full)) + else if (e.isFile() && e.name.endsWith('.md')) out.push(full) + } + return out +} + +// ── 提取 docs/01 REQ 卡片单元 ──────────────────────────────────── +// 排除 _module.md(模块头)与 index.md(模块索引),其余 .md 文件名(去 .md)== req_id。 +export function collectReqUnits(root) { + const dir = join(root, REQ_DIR_REL) + const units = new Map() // id -> { kind:'req', hash, path } + for (const file of walkMd(dir)) { + const name = basename(file) + if (name === '_module.md' || name === 'index.md') continue + const id = name.slice(0, -3) // 去 .md + units.set(id, { kind: 'req', hash: hashContent(readFileSync(file, 'utf8')), path: file }) + } + return units +} + +// ── 提取 docs/08 §三 的 FE-NN 行 ───────────────────────────────── +// §三 从 `## 三、` 起,到下一个 `## ` 或文件末尾止。行形如:- [ ] FE-01 登录页 +export function collectFeUnits(root) { + const docs08 = join(root, DOCS08_REL) + const units = new Map() // id -> { kind:'fe', hash, line } + if (!existsSync(docs08)) return units + const lines = readFileSync(docs08, 'utf8').split('\n') + let inSec3 = false + for (const raw of lines) { + const line = raw.replace(/\r$/, '') + if (/^##\s+三[、.]/.test(line)) { inSec3 = true; continue } + if (inSec3 && /^##\s/.test(line)) break // 进入下一 ## 章节,§三 结束 + if (!inSec3) continue + const m = line.match(/^\s*-\s*\[.\]\s*(FE-\d+)\b(.*)$/) + if (!m) continue + const id = m[1] + const text = (id + ' ' + m[2].trim()).trim() + units.set(id, { kind: 'fe', hash: hashContent(text), line: text }) + } + return units +} + +// ── 当前全部单元(req + fe):id -> { kind, hash } ───────────────── +export function computeUnits(root) { + const out = new Map() + for (const [id, u] of collectReqUnits(root)) out.set(id, { kind: u.kind, hash: u.hash }) + for (const [id, u] of collectFeUnits(root)) out.set(id, { kind: u.kind, hash: u.hash }) + return out +} + +// ── 台账读写 ───────────────────────────────────────────────────── +export function loadLedger(root) { + const p = ledgerPath(root) + if (!existsSync(p)) return null + try { + const j = JSON.parse(readFileSync(p, 'utf8')) + if (!j || typeof j !== 'object' || !j.units) return { version: 1, units: {} } + return j + } catch { + return { version: 1, units: {} } + } +} + +export function writeLedger(root, units) { + const obj = { version: 1, units: {} } + // 稳定排序,保证 git diff 可读 + for (const id of [...units.keys()].sort()) { + const u = units.get(id) + obj.units[id] = { kind: u.kind, hash: u.hash } + } + writeFileSync(ledgerPath(root), JSON.stringify(obj, null, 2) + '\n', 'utf8') + return obj +} + +// ── 对比当前单元 vs 台账 → {new,changed,removed,unchanged} ───────── +// 每项为 { id, kind } 数组;台账缺失(null)时调用方应先 commit 建基线(此处一律算 new)。 +export function diffLedger(ledger, units) { + const stored = (ledger && ledger.units) || {} + const res = { new: [], changed: [], removed: [], unchanged: [] } + for (const [id, u] of units) { + const prev = stored[id] + if (!prev) res.new.push({ id, kind: u.kind }) + else if (prev.hash !== u.hash) res.changed.push({ id, kind: u.kind }) + else res.unchanged.push({ id, kind: u.kind }) + } + for (const id of Object.keys(stored)) { + if (!units.has(id)) res.removed.push({ id, kind: stored[id].kind }) + } + for (const k of ['new', 'changed', 'removed', 'unchanged']) { + res[k].sort((a, b) => a.id.localeCompare(b.id)) + } + return res +} + +// ── CLI ───────────────────────────────────────────────────────── +function isMain() { + try { + return process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replace(/\\/g, '/')}`).href + || (process.argv[1] && process.argv[1].endsWith('req-ledger.mjs')) + } catch { return false } +} + +if (isMain()) { + const [cmd, root] = process.argv.slice(2) + if (!cmd || !root) { + process.stderr.write('用法: node req-ledger.mjs \n') + process.exit(2) + } + if (!existsSync(root) || !statSync(root).isDirectory()) { + process.stderr.write(`错误: projectRoot 不是目录: ${root}\n`) + process.exit(2) + } + const units = computeUnits(root) + if (cmd === 'commit') { + const obj = writeLedger(root, units) + process.stdout.write(JSON.stringify({ committed: Object.keys(obj.units).length, path: ledgerPath(root) }) + '\n') + process.exit(0) + } + if (cmd === 'scan' || cmd === 'status') { + const ledger = loadLedger(root) + const diff = diffLedger(ledger, units) + if (cmd === 'status') { + const fmt = (arr) => arr.map((x) => `${x.id}(${x.kind})`).join(', ') || '无' + process.stderr.write( + `台账: ${ledger ? '已存在' : '缺失(需先 commit 建基线)'}\n` + + ` 新增: ${fmt(diff.new)}\n 变更: ${fmt(diff.changed)}\n` + + ` 删除: ${fmt(diff.removed)}\n 未变: ${diff.unchanged.length} 项\n`, + ) + } + process.stdout.write(JSON.stringify({ ledgerExists: !!ledger, ...diff }) + '\n') + process.exit(0) + } + process.stderr.write(`未知命令: ${cmd}\n`) + process.exit(2) +} diff --git a/lib/req-ledger.test.mjs b/lib/req-ledger.test.mjs new file mode 100644 index 0000000..87ea480 --- /dev/null +++ b/lib/req-ledger.test.mjs @@ -0,0 +1,89 @@ +// lib/req-ledger.test.mjs — 单测:需求台账 哈希 / 提取 / diff / 读写 +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { + hashContent, collectReqUnits, collectFeUnits, computeUnits, + loadLedger, writeLedger, diffLedger, +} from './req-ledger.mjs' + +// 搭一个最小项目目录:docs/01-需求清单//{_module.md,index.md,.md} + docs/08 +function scaffold() { + const root = mkdtempSync(join(tmpdir(), 'req-ledger-')) + const reqDir = join(root, 'docs', '01-需求清单', 'USR-UserInfo') + mkdirSync(reqDir, { recursive: true }) + writeFileSync(join(root, 'docs', '01-需求清单', 'index.md'), '# 索引\n| a | b |\n') + writeFileSync(join(reqDir, '_module.md'), '# 模块头\n') + writeFileSync(join(reqDir, 'USR-UserInfo-Login.md'), '# 登录\n目标: 用户登录\n') + writeFileSync(join(reqDir, 'USR-UserInfo-Logout.md'), '# 登出\n目标: 用户登出\n') + writeFileSync(join(root, 'docs', '08-模块任务管理.md'), + '## 一、Plan\n- [x] A0\n\n## 三、前端\n- 整体里程碑: —\n- 功能:\n - [ ] FE-01 登录页\n - [ ] FE-02 用户列表\n\n## 四、其它\n- [ ] FE-99 不该被收\n') + return root +} + +test('hashContent: CRLF 与行尾空白归一后哈希一致', () => { + assert.equal(hashContent('a\nb\n'), hashContent('a\r\nb \r\n')) + assert.equal(hashContent('a\nb'), hashContent(' a\nb \n\n')) + assert.notEqual(hashContent('a\nb'), hashContent('a\nc')) +}) + +test('collectReqUnits: 排除 _module.md 与 index.md,其余 == req_id', () => { + const root = scaffold() + try { + const u = collectReqUnits(root) + assert.deepEqual([...u.keys()].sort(), ['USR-UserInfo-Login', 'USR-UserInfo-Logout']) + assert.equal(u.get('USR-UserInfo-Login').kind, 'req') + } finally { rmSync(root, { recursive: true, force: true }) } +}) + +test('collectFeUnits: 只收 §三 的 FE 行,遇下一个 ## 即止', () => { + const root = scaffold() + try { + const u = collectFeUnits(root) + assert.deepEqual([...u.keys()].sort(), ['FE-01', 'FE-02']) // FE-99 在 §四,不收 + assert.equal(u.get('FE-01').kind, 'fe') + } finally { rmSync(root, { recursive: true, force: true }) } +}) + +test('computeUnits: req + fe 合并', () => { + const root = scaffold() + try { + const u = computeUnits(root) + assert.equal(u.size, 4) // 2 req + 2 fe + } finally { rmSync(root, { recursive: true, force: true }) } +}) + +test('diffLedger: 台账缺失 → 全部算 new', () => { + const root = scaffold() + try { + const diff = diffLedger(null, computeUnits(root)) + assert.equal(diff.new.length, 4) + assert.equal(diff.changed.length, 0) + } finally { rmSync(root, { recursive: true, force: true }) } +}) + +test('writeLedger/loadLedger 往返 + diff 识别 新增/变更/删除/未变', () => { + const root = scaffold() + try { + // 建基线 + writeLedger(root, computeUnits(root)) + let diff = diffLedger(loadLedger(root), computeUnits(root)) + assert.equal(diff.new.length, 0) + assert.equal(diff.changed.length, 0) + assert.equal(diff.unchanged.length, 4) + + // 改一张卡 → changed;加一张卡 → new;删 FE-02 一行靠改 docs/08 → removed + const reqDir = join(root, 'docs', '01-需求清单', 'USR-UserInfo') + writeFileSync(join(reqDir, 'USR-UserInfo-Login.md'), '# 登录\n目标: 用户登录(改了规则)\n') + writeFileSync(join(reqDir, 'USR-UserInfo-PwdReset.md'), '# 改密\n目标: 重置密码\n') + writeFileSync(join(root, 'docs', '08-模块任务管理.md'), + '## 一、Plan\n## 三、前端\n- 功能:\n - [ ] FE-01 登录页\n\n## 四、其它\n') + + diff = diffLedger(loadLedger(root), computeUnits(root)) + assert.deepEqual(diff.new.map((x) => x.id), ['USR-UserInfo-PwdReset']) + assert.deepEqual(diff.changed.map((x) => x.id), ['USR-UserInfo-Login']) + assert.deepEqual(diff.removed.map((x) => x.id), ['FE-02']) + } finally { rmSync(root, { recursive: true, force: true }) } +}) diff --git a/skills/plan/add-req/SKILL.md b/skills/plan/add-req/SKILL.md new file mode 100644 index 0000000..54f8c55 --- /dev/null +++ b/skills/plan/add-req/SKILL.md @@ -0,0 +1,134 @@ +--- +name: add-req +description: 增量需求入口——初始 Plan 完结后追加 / 修改需求时运行。基于 lib/req-ledger.mjs 内容哈希台账识别新增 / 变更的 REQ 卡片与 FE 行,只对增量做 A3-delta(写 V_n migration + 同步 docs/03,绝不改 V1)+ A5-delta(补 docs/05 端点 / docs/02 顺序 / docs/08 模块行),并作废变更单元已有的 req-done/milestone tag,使 coding.mjs Router 只重跑增量、不必整仓重跑。 +user-invocable: true +allowed-tools: Read Write Edit Grep Glob AskUserQuestion Bash(node *) Bash(git *) Bash(ls *) Bash(mkdir *) +--- + +**所有输出必须使用中文。** + +# add-req — 增量需求识别与生成 + +用于**初始 Plan(A0~A5)已完结**之后,往项目里**追加新需求**或**修改已有需求**。不重跑整条 Plan,只处理增量;产出与 A1/A3/A5 完全一致,交由 `coding.mjs` Router 增量编码。 + +> 用法约定:先**人工**在 `docs/01-需求清单/` 里把新 REQ 卡片写好(真实业务内容,同 A1 填法)或修改已有卡片,再运行 `/erp-workflow:add-req`。本 skill 负责「识别 + 下游生成 + 作废过期完成标记」,不替你写业务需求本身。 + +`` = 项目根(含 `docs/`、`config-vars.yaml`、`sql/`、`.git`)。`${CLAUDE_PLUGIN_ROOT}` = 插件根。 + +## 前置:Plan 必须已完结 + +用 `Read` 读 `docs/08-模块任务管理.md § 一`。若 § 一存在任一 `- [ ]` 未勾子项 → 初始 Plan 未完结,**停下**并提示: + +``` +[add-req] ⛔ 初始 Plan(A0~A5)尚未完结,增量入口暂不可用。 +请先运行 /erp-workflow:plan-start 完成初始规划,再用 /add-req 追加需求。 +``` + +## 步骤 0:台账基线(首次启用 / 老项目迁移) + +``` +node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan +``` + +解析输出 JSON 的 `ledgerExists`: + +- `false`(项目还没有 `.req-ledger.json`)→ 这是首次启用增量台账。**先建基线**: + ``` + node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs commit + ``` + 然后打印并**停下**: + ``` + [add-req] 已为现有 个需求单元建立台账基线(.req-ledger.json)。 + 首次基线无法区分存量与新增,故本次不做增量生成。 + 请在 docs/01 追加 / 修改需求后,再次运行 /erp-workflow:add-req。 + ``` + (理由:首跑若把存量全当新增,会重复生成已有的 docs/03/05 工件并误重跑整仓。基线建立后,后续改动才能被准确 diff。) +- `true` → 进入步骤 1。 + +## 步骤 1:检测增量 + +``` +node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan +``` + +解析 `new[]` / `changed[]` / `removed[]`(每项 `{id, kind}`,kind = `req` 后端卡片 / `fe` 前端功能行)。 + +- `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。` **停下**(不提交台账)。 +- `removed` 非空 → **仅提示、不自动删**:打印 `检测到台账登记但 docs 已移除的单元:<列出>。下线需求请人工同步 docs/02/03/05 并删除对应 tag,本 skill 不自动执行删除。` 然后继续处理 new/changed(removed 不写回台账,留待人工,下次仍会提示)。 + +## 步骤 2:校验新增 / 变更 REQ 卡真实数据(仅 kind=req) + +对 `new` ∪ `changed` 里 `kind=req` 的每个 id(卡片路径 `docs/01-需求清单//.md`):`Read` + `Grep` 校验**无 `{{` 残留、无 `【人工填写`**(同 A1 E.1)。命中缺口 → 打印卡片路径 + 缺口行,用 `AskUserQuestion` 引导用户补齐后重检,直到全部为真实数据。 +(`kind=fe` 的增量行来自 docs/08 §三 推导,不在此校验。) + +## 步骤 3:A3-delta — schema 增量(仅当新增/变更 REQ 影响数据模型) + +对 `new`/`changed` 的 REQ 卡片,判断是否需要**新表**或**给已有表加列**(依据卡片业务 + `依赖表`): + +1. **写增量 migration(绝不改 V1 或任何已存在 V_n)**: + - `ls sql/migrations/V*.sql` 取当前最大版本号 `n`,新文件 `sql/migrations/V__.sql`(如 `V2__add_order_refund.sql`)。 + - 新表 → `CREATE TABLE`;给已有表加列 → `ALTER TABLE ... ADD COLUMN`。**追加式**,套用与 A3/A4 相同的命名规范、匈牙利列前缀(`i/s/t/b/d`)、标准列约定(主表 15 列 / 从表 12 列 / 基础 11 列)与 DDL 默认值翻译规则(见 `CLAUDE.md` Schema 演化规约 + db-init 约定)。 +2. **同步 docs/03**:参照 `${CLAUDE_PLUGIN_ROOT}/skills/plan/db-design-gen/templates/docs-03-table-template.md`,新表追加表小节 / 已有表小节增列,保持 docs/03 为 schema SSoT。 +3. **回填卡片**:把该 REQ 卡片 `依赖表:` 的 `TBD` / 占位 `Edit` 成实际表名。 + +> **不在此 apply 到数据库、不跑 validate-ddl**:新 schema 由 Coding 阶段冷起栈时 Flyway 自动 apply 全部 `V*.sql`;validate-ddl 是「docs/03 ↔ 单一 V 文件」整库 4 维比对,多 migration 场景不适用。schema 一致性由 Coding 阶段 testGate / Seed 冷起栈兜底。 + +## 步骤 4:A5-delta — 下游文档增量(每个新增 REQ) + +对 `new` 的 `kind=req`: + +1. **docs/05 端点**:参照 `${CLAUDE_PLUGIN_ROOT}/skills/plan/downstream-gen/templates/docs-05-endpoint-template.md` 追加该 REQ 的接口小节(对齐 docs/06 实现策略的分页/鉴权/响应包络/错误码风格)。 +2. **docs/02 顺序**:把该 REQ 按依赖拓扑插入 `docs/02-开发计划.md` 顺序清单,**同模块 REQ 保持连续**;环依赖按启发式破环并在 `note` 注明。 +3. **回填卡片**:`依赖接口:` 的 `TBD` / 占位 `Edit` 成实际 endpoint。 +4. **docs/08 §二**: + - 该 REQ 属**新模块** → 参照 `${CLAUDE_PLUGIN_ROOT}/skills/plan/downstream-gen/templates/docs-08-module-row-template.md` 追加模块 bullet,`里程碑:` 字段填 `—`。 + - 属**已有模块** → 在该模块 bullet 下追加该 REQ 子项行。 + +对 `changed` 的 `kind=req`:若接口/字段语义变化,相应 `Edit` docs/05 端点小节、按需调整 docs/02(一般顺序不变)。 + +**前端增量**:若新增需求带来新前端功能,`Glob` `prototype/**/*.html` 确认有原型后,用 `AskUserQuestion` 与用户确认是否新增 FE 行;新增则在 `docs/08 §三` 「功能:」下追加 ` - [ ] FE-NN <功能名>`(NN 取现有最大值+1)。 + +## 步骤 5:作废变更单元的完成标记(关键——否则 Router 会跳过) + +对每个 `changed` 单元(`new` 单元无 tag,跳过): + +- **kind=req**: + - `git -C tag -l "req-done/"` 存在 → `git -C tag -d req-done/`。 + - 该 REQ 所属后端模块若已打里程碑(`git -C tag -l "milestone/"` 存在)→ `git -C tag -d milestone/`,并 `Edit` `docs/08 §二` 该模块 `里程碑:` 字段从 `milestone/` 复位为 `—`。(否则 Router 判定模块 done 而整模块跳过,改动不会重跑。) +- **kind=fe**: + - `git -C tag -l "req-done/"` 存在 → 删。 + - 前端阶段若已 `milestone/frontend-phase` → 删该 tag,并 `Edit` `docs/08 §三` `整体里程碑:` 复位 `—`。 + +逐条记录已删的 tag,供步骤 6 横幅汇总。 + +## 步骤 6:提交台账 + 完成横幅 + +``` +node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs commit +``` + +把当前哈希写回 `.req-ledger.json`(new/changed 单元自此成为新基线)。然后打印横幅并**停下**(不自动进编码,与「Plan 完不自动进 B」一致): + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + [add-req] ✅ 增量需求已处理 + + 新增 REQ:<列出 id 或 无> + 变更 REQ:<列出 id 或 无> + 新增 FE :<列出 FE-NN 或 无> + 新增 migration: + 作废 tag:<列出已删的 req-done/* 与 milestone/* 或 无> + + 台账已更新(.req-ledger.json)。 + 运行 /erp-workflow:coding-start 增量编码(Router 只跑缺 tag 的模块/功能)。 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## 参考 + +- `${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs`(台账:scan / commit) +- `docs/01-需求清单//.md`(REQ SSoT,人工先填) +- `docs/03-数据库设计文档.md` + `sql/migrations/V*.sql`(schema 演化,追加 V_n) +- `docs/05-API接口契约.md` / `docs/02-开发计划.md` / `docs/08-模块任务管理.md`(下游增量) +- `CLAUDE.md` Schema 演化规约(V1 永不改,增量走 V_n) +- 跨 skill 模板:`${CLAUDE_PLUGIN_ROOT}/skills/plan/db-design-gen/templates/docs-03-table-template.md`、`${CLAUDE_PLUGIN_ROOT}/skills/plan/downstream-gen/templates/{docs-05-endpoint-template.md,docs-08-module-row-template.md}` diff --git a/skills/plan/project-init/templates/CLAUDE-template.md b/skills/plan/project-init/templates/CLAUDE-template.md index f436e08..a589f09 100644 --- a/skills/plan/project-init/templates/CLAUDE-template.md +++ b/skills/plan/project-init/templates/CLAUDE-template.md @@ -35,6 +35,7 @@ 4. **已合并的 migration 永不修改**:如果发现错了,写一个补救 migration(如 `V7__fix_V5_index_name.sql`)修正,旧 `V_n.sql` 保持原样、永不回改 5. **临时调试 DDL**:临时在本地试字段/索引可手动 `mysql -e`,但不写 migration;下次 `setup-test-db.mjs` 会 drop+create 清掉 6. **A4 生成的 V1**:`V1__initial_schema.sql` 是 A 阶段由 `db-init` 从 `docs/03-数据库设计文档.md`(A3 正向设计的 schema SSoT)翻译生成的初始版本;后续 V2/V3/... 由 B 阶段每个 REQ 按需写入,**同时**反向同步更新 docs/03 对应表小节以保持 SSoT 一致 +7. **增量需求新表/新列**:Plan 完结后用 `/erp-workflow:add-req` 追加需求时,其新表 / 新列同样落为新的 `V_n`(`ls V*.sql` 取 max+1)追加式 migration,并同步 docs/03——**永不改 V1**;schema 由下次 Coding 冷起栈时 Flyway 统一 apply ---