Commit aee9252a3a67a55c5510f3df6b1a4b2d476b7452
1 parent
1117bb65
feat: validate-ddl 多文件累积校验(P1#2) + coding 首跑建 req-ledger 基线(P1#3)
P1#2 — validate-ddl 支持增量 migration: - parseDDL 新增第三遍 mergeAlterStatements:ALTER TABLE ADD COLUMN / ADD [UNIQUE] INDEX|KEY / ADD PRIMARY KEY 并入对应表(仅追加式, MODIFY/CHANGE/DROP/RENAME 不处理;并集与文件顺序无关)。 - CLI 接受多个 DDL 文件:node validate-ddl docs/03 sql/migrations/V*.sql, CREATE + 各 V_n 的 ALTER ADD 并集 ↔ docs/03 累积 SSoT 做 4 维比对。 - add-req SKILL 新增「增量 DDL 校验(fail-closed)」闸门,替换原「多 migration 不适用」说明。 - 7 个新测试(含 DEFAULT 字面量里的 ALTER 文本不被误吃);52/52 通过。 P1#3 — coding 首跑自动建需求台账基线: - coding-start 透传 pluginRoot;coding.mjs ensureLedgerBaseline() 首跑若 .req-ledger.json 缺失则 commit 基线(幂等、best-effort、缺 pluginRoot 静默跳过), 使日后 /add-req 能识别增量、免空跑。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing
5 changed files
with
170 additions
and
8 deletions
lib/validate-ddl.mjs
| 1 | 1 | // lib/validate-ddl.mjs — docs/03 表格 ↔ DDL(V1.sql)一致性 4 维校验 |
| 2 | 2 | // 替换 db-init/scripts/validate.sh(跨平台、纯 Node、零外部依赖)。 |
| 3 | 3 | // |
| 4 | -// 用法(CLI):node lib/validate-ddl.mjs <docs03Path> <ddlPath> | |
| 4 | +// 用法(CLI):node lib/validate-ddl.mjs <docs03Path> <ddlPath> [ddlPath2 ...] | |
| 5 | +// 单文件 = V1 整库校验(A4 db-init);多文件 = 累积校验(增量 add-req:传 sql/migrations/V*.sql, | |
| 6 | +// CREATE TABLE + ALTER ADD 并集 ↔ docs/03 累积 SSoT)。 | |
| 5 | 7 | // 退出码 0 = 一致;1 = 存在差异(diff 明细打印到 stderr);2 = 用法/路径错误。 |
| 6 | 8 | // 程序内:import { parseDocsTables, parseDDL, diffSchema } from './validate-ddl.mjs' |
| 7 | 9 | // |
| ... | ... | @@ -165,9 +167,56 @@ export function parseDDL(text) { |
| 165 | 167 | // 扫描前先抹掉字符串字面量内部,避免 DEFAULT / COMMENT 里的 "CREATE INDEX …" 文本被误当语句(REGEX-3)。 |
| 166 | 168 | const scanSrc = blankStringLiterals(src) |
| 167 | 169 | mergeStandaloneIndexes(scanSrc, tables) |
| 170 | + // 第三遍:增量 migration(V2+)的 ALTER TABLE ADD COLUMN / ADD [UNIQUE] INDEX|KEY 并入对应表。 | |
| 171 | + mergeAlterStatements(scanSrc, tables) | |
| 168 | 172 | return tables |
| 169 | 173 | } |
| 170 | 174 | |
| 175 | +// 第三遍:把增量 migration 的 `ALTER TABLE [db.]<t> ADD ...` 追加式变更并入对应表(P1#2)。 | |
| 176 | +// 支持 ADD [COLUMN] <col> <type> / ADD [UNIQUE] INDEX|KEY <name> (<cols>) / ADD PRIMARY KEY; | |
| 177 | +// 一条 ALTER 内多个逗号分隔变更逐个处理。**仅追加式**——MODIFY/CHANGE/DROP/RENAME 不处理: | |
| 178 | +// docs/03 是累积 SSoT,列/索引最终集合 = V1 CREATE + 各 V_n ADD 的并集,与文件顺序无关, | |
| 179 | +// 故对 `validate-ddl docs/03 sql/migrations/V*.sql`(glob 按字典序展开)的并集校验成立。 | |
| 180 | +// 表未在所提供 DDL 中 CREATE → 跳过(表集合维度另报缺,不凭空造表,与 mergeStandaloneIndexes 一致)。 | |
| 181 | +function mergeAlterStatements(src, tables) { | |
| 182 | + const re = new RegExp('ALTER\\s+TABLE\\s+(?:' + IDENT + '\\s*\\.\\s*)?(' + IDENT + ')\\s+', 'gi') | |
| 183 | + let m | |
| 184 | + while ((m = re.exec(src)) !== null) { | |
| 185 | + const tbl = stripTicks(m[1]) | |
| 186 | + const bodyStart = re.lastIndex | |
| 187 | + // 语句体到下一个分号 / 下一条 TABLE 级语句(防某些 migration 漏写分号时吞并后续语句)。 | |
| 188 | + const cand = [] | |
| 189 | + const semi = src.indexOf(';', bodyStart) | |
| 190 | + if (semi !== -1) cand.push(semi) | |
| 191 | + const nextStmt = src.slice(bodyStart).search(/\b(?:ALTER|CREATE|DROP)\s+TABLE\b/i) | |
| 192 | + if (nextStmt !== -1) cand.push(bodyStart + nextStmt) | |
| 193 | + const end = cand.length ? Math.min(...cand) : src.length | |
| 194 | + const t = tables.get(tbl) | |
| 195 | + if (!t) continue | |
| 196 | + for (const clauseRaw of splitTopLevelCommas(src.slice(bodyStart, end))) { | |
| 197 | + const clause = clauseRaw.trim() | |
| 198 | + if (!clause) continue | |
| 199 | + const idxM = clause.match(new RegExp('^ADD\\s+(UNIQUE\\s+)?(?:INDEX|KEY)\\s+(' + IDENT + ')\\s*\\(', 'i')) | |
| 200 | + if (idxM) { | |
| 201 | + const kind = idxM[1] ? 'UNIQUE' : 'INDEX' | |
| 202 | + const colsBody = extractBalancedParens(clause, idxM[0].length - 1) | |
| 203 | + t.indexes.add(`${stripTicks(idxM[2])}:${kind}:${normalizeIndexCols(colsBody || '')}`) | |
| 204 | + continue | |
| 205 | + } | |
| 206 | + if (/^ADD\s+PRIMARY\s+KEY/i.test(clause)) { t.indexes.add('PRIMARY'); continue } | |
| 207 | + if (/^ADD\s+(?:CONSTRAINT|FOREIGN)\b/i.test(clause)) continue // 外键维度已移除 | |
| 208 | + const colM = clause.match(/^ADD\s+(?:COLUMN\s+)?(`?)([A-Za-z0-9_]+)\1\s+(.+)$/is) | |
| 209 | + if (colM) { | |
| 210 | + const name = colM[2] | |
| 211 | + // 未加反引号时拒绝索引/约束保留字开头,避免误吃成列。 | |
| 212 | + if (colM[1] !== '`' && /^(?:KEY|INDEX|UNIQUE|FULLTEXT|SPATIAL|PRIMARY|CONSTRAINT|CHECK|FOREIGN|COLUMN)$/i.test(name)) continue | |
| 213 | + t.columns.set(name, extractType(colM[3])) | |
| 214 | + } | |
| 215 | + // 其它(MODIFY/CHANGE/DROP/RENAME/ALTER COLUMN)→ 追加式校验不处理,跳过。 | |
| 216 | + } | |
| 217 | + } | |
| 218 | +} | |
| 219 | + | |
| 171 | 220 | // 独立 `CREATE [UNIQUE] INDEX <name> [USING BTREE|HASH] ON [<db>.]<table> (<cols>)` → 并入 table.indexes(C1)。 |
| 172 | 221 | // USING 子句可出现在 ON 之前(合法 MySQL),需容忍(REGEX-4)。 |
| 173 | 222 | function mergeStandaloneIndexes(src, tables) { |
| ... | ... | @@ -486,16 +535,22 @@ const { pathToFileURL } = await import('node:url') // CLI entry guard:pathToFi |
| 486 | 535 | const isCliEntry = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href |
| 487 | 536 | if (isCliEntry) { |
| 488 | 537 | const { readFileSync, existsSync } = await import('node:fs') |
| 489 | - const [docsPath, ddlPath] = process.argv.slice(2) | |
| 490 | - if (!docsPath || !ddlPath) { | |
| 491 | - console.error('用法: node lib/validate-ddl.mjs <docs/03 path> <V1.sql path>') | |
| 538 | + const [docsPath, ...ddlPaths] = process.argv.slice(2) | |
| 539 | + if (!docsPath || ddlPaths.length === 0) { | |
| 540 | + console.error('用法: node lib/validate-ddl.mjs <docs/03 path> <V1.sql> [V2.sql ...]') | |
| 541 | + console.error(' 增量校验:node lib/validate-ddl.mjs docs/03-数据库设计文档.md sql/migrations/V*.sql') | |
| 492 | 542 | process.exit(2) |
| 493 | 543 | } |
| 494 | 544 | if (!existsSync(docsPath)) { console.error(`validate-ddl: docs 不存在: ${docsPath}`); process.exit(2) } |
| 495 | - if (!existsSync(ddlPath)) { console.error(`validate-ddl: DDL 不存在: ${ddlPath}`); process.exit(2) } | |
| 545 | + for (const p of ddlPaths) { | |
| 546 | + if (!existsSync(p)) { console.error(`validate-ddl: DDL 不存在: ${p}`); process.exit(2) } | |
| 547 | + } | |
| 496 | 548 | |
| 497 | 549 | const docsTables = parseDocsTables(readFileSync(docsPath, 'utf8')) |
| 498 | - const ddlTables = parseDDL(readFileSync(ddlPath, 'utf8')) | |
| 550 | + // 多个 migration(V1 + V_n...)拼成单一多语句文本:parseDDL 三遍扫描天然按 CREATE→ALTER | |
| 551 | + // 累积出最终 schema;追加式 ALTER 与文件顺序无关,故 glob 字典序展开(V1,V10,V2...)也正确。 | |
| 552 | + const ddlText = ddlPaths.map(p => readFileSync(p, 'utf8')).join('\n;\n') | |
| 553 | + const ddlTables = parseDDL(ddlText) | |
| 499 | 554 | const diff = diffSchema(docsTables, ddlTables) |
| 500 | 555 | |
| 501 | 556 | if (diff.hasDiff) { | ... | ... |
lib/validate-ddl.test.mjs
| ... | ... | @@ -532,3 +532,63 @@ test('parseDocsTables: 恰为「主键」/「唯一」仍正确映射(EFFICACY |
| 532 | 532 | const uk = parseDocsTables('## `t`\n### 字段\n| 列 | 类型 |\n|---|---|\n| `c` | int |\n### 索引\n- `uk` (唯一): c\n').get('t') |
| 533 | 533 | assert.ok(uk.indexes.has('uk:UNIQUE:c'), '「唯一」应映射 UNIQUE — got: ' + [...uk.indexes]) |
| 534 | 534 | }) |
| 535 | + | |
| 536 | +// ── P1#2:ALTER TABLE ADD(增量 migration)并入 + 多语句累积校验 ────────────── | |
| 537 | +test('parseDDL: ALTER TABLE ADD COLUMN 并入已有表的列集', () => { | |
| 538 | + const ddl = [ | |
| 539 | + 'CREATE TABLE t_order ( iId bigint PRIMARY KEY, sCode varchar(50) );', | |
| 540 | + 'ALTER TABLE t_order ADD COLUMN dRefund decimal(18,6);', | |
| 541 | + ].join('\n') | |
| 542 | + const t = parseDDL(ddl).get('t_order') | |
| 543 | + assert.deepEqual([...t.columns.keys()], ['iId', 'sCode', 'dRefund']) | |
| 544 | + assert.equal(t.columns.get('dRefund'), 'decimal(18,6)') | |
| 545 | +}) | |
| 546 | + | |
| 547 | +test('parseDDL: 一条 ALTER 内多个 ADD COLUMN(逗号分隔)逐个并入', () => { | |
| 548 | + const ddl = [ | |
| 549 | + 'CREATE TABLE t_x ( iId bigint );', | |
| 550 | + 'ALTER TABLE t_x ADD COLUMN sA varchar(10), ADD COLUMN sB varchar(20);', | |
| 551 | + ].join('\n') | |
| 552 | + const t = parseDDL(ddl).get('t_x') | |
| 553 | + assert.deepEqual([...t.columns.keys()], ['iId', 'sA', 'sB']) | |
| 554 | + assert.equal(t.columns.get('sB'), 'varchar(20)') | |
| 555 | +}) | |
| 556 | + | |
| 557 | +test('parseDDL: ALTER TABLE ADD UNIQUE INDEX 并入索引集', () => { | |
| 558 | + const ddl = [ | |
| 559 | + 'CREATE TABLE t_y ( iId bigint, sEmail varchar(50) );', | |
| 560 | + 'ALTER TABLE t_y ADD UNIQUE INDEX uq_email (sEmail);', | |
| 561 | + ].join('\n') | |
| 562 | + const t = parseDDL(ddl).get('t_y') | |
| 563 | + assert.ok([...t.indexes].some(ix => ix === 'uq_email:UNIQUE:sEmail')) | |
| 564 | +}) | |
| 565 | + | |
| 566 | +test('parseDDL: ALTER 指向未 CREATE 的表 → 不凭空造表', () => { | |
| 567 | + const tables = parseDDL('ALTER TABLE t_ghost ADD COLUMN sFoo varchar(10);') | |
| 568 | + assert.equal(tables.has('t_ghost'), false) | |
| 569 | +}) | |
| 570 | + | |
| 571 | +test('累积校验:docs/03(含新列)↔ V1+V2 并集一致 → 空 diff', () => { | |
| 572 | + const docs = '## `t_order`\n| 列 | 类型 |\n|---|---|\n| iId | bigint |\n| sCode | varchar(50) |\n| dRefund | decimal(18,6) |\n' | |
| 573 | + // 模拟 CLI 把 V1 + V2 拼成一段多语句文本 | |
| 574 | + const ddlUnion = [ | |
| 575 | + 'CREATE TABLE t_order ( iId bigint PRIMARY KEY, sCode varchar(50) );', // V1 | |
| 576 | + 'ALTER TABLE t_order ADD COLUMN dRefund decimal(18,6);', // V2 | |
| 577 | + ].join('\n;\n') | |
| 578 | + const d = diffSchema(parseDocsTables(docs), parseDDL(ddlUnion)) | |
| 579 | + assert.equal(d.hasDiff, false) | |
| 580 | +}) | |
| 581 | + | |
| 582 | +test('累积校验:docs/03 加了列但 V_n 漏写 ALTER → 报列缺失(fail-closed)', () => { | |
| 583 | + const docs = '## `t_order`\n| 列 | 类型 |\n|---|---|\n| iId | bigint |\n| dRefund | decimal(18,6) |\n' | |
| 584 | + const ddlOnlyV1 = 'CREATE TABLE t_order ( iId bigint PRIMARY KEY );' // 漏了 dRefund 的 ALTER | |
| 585 | + const d = diffSchema(parseDocsTables(docs), parseDDL(ddlOnlyV1)) | |
| 586 | + assert.ok(d.hasDiff) | |
| 587 | + assert.ok(d.columnMismatches.some(m => m.table === 't_order' && m.column === 'dRefund' && m.side === 'docs')) | |
| 588 | +}) | |
| 589 | + | |
| 590 | +test('parseDDL: DEFAULT 字面量里的 "ALTER TABLE ..." 文本不被误当真实 ALTER', () => { | |
| 591 | + const ddl = "CREATE TABLE t_z ( iId bigint, sNote varchar(200) DEFAULT 'ALTER TABLE t_z ADD COLUMN hacked int' );" | |
| 592 | + const t = parseDDL(ddl).get('t_z') | |
| 593 | + assert.deepEqual([...t.columns.keys()], ['iId', 'sNote']) // 无 hacked 列 | |
| 594 | +}) | ... | ... |
skills/coding/coding-start/SKILL.md
| ... | ... | @@ -94,10 +94,15 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) |
| 94 | 94 | ``` |
| 95 | 95 | Workflow({ |
| 96 | 96 | scriptPath: "${CLAUDE_PLUGIN_ROOT}/workflows/coding.mjs", |
| 97 | - args: { projectRoot: "<git rev-parse --show-toplevel 的 stdout>" } | |
| 97 | + args: { | |
| 98 | + projectRoot: "<git rev-parse --show-toplevel 的 stdout>", | |
| 99 | + pluginRoot: "${CLAUDE_PLUGIN_ROOT}" | |
| 100 | + } | |
| 98 | 101 | }) |
| 99 | 102 | ``` |
| 100 | 103 | |
| 104 | +> `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。 | |
| 105 | + | |
| 101 | 106 | ### 步骤 5:告知用户 Workflow 已启动 |
| 102 | 107 | |
| 103 | 108 | 启动后向用户输出: | ... | ... |
skills/plan/add-req/SKILL.md
| ... | ... | @@ -72,8 +72,16 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> |
| 72 | 72 | - 新表 → `CREATE TABLE`;给已有表加列 → `ALTER TABLE ... ADD COLUMN`。**追加式**,套用与 A3/A4 相同的命名规范、匈牙利列前缀(`i/s/t/b/d`)、标准列约定(主表 15 列 / 从表 12 列 / 基础 11 列)与 DDL 默认值翻译规则(见 `CLAUDE.md` Schema 演化规约 + db-init 约定)。 |
| 73 | 73 | 2. **同步 docs/03**:参照 `${CLAUDE_PLUGIN_ROOT}/skills/plan/db-design-gen/templates/docs-03-table-template.md`,新表追加表小节 / 已有表小节增列,保持 docs/03 为 schema SSoT。 |
| 74 | 74 | 3. **回填卡片**:把该 REQ 卡片 `依赖表:` 的 `TBD` / 占位 `Edit` 成实际表名。 |
| 75 | +4. **增量 DDL 校验(fail-closed)**:写完 V_n + 同步 docs/03 后,对**全部 migration 的累积并集**校验 docs/03 一致: | |
| 76 | + ``` | |
| 77 | + node ${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs docs/03-数据库设计文档.md sql/migrations/V*.sql | |
| 78 | + ``` | |
| 79 | + (validate-ddl 已支持多文件:CREATE TABLE + 各 V_n 的 `ALTER ... ADD` 并集 ↔ docs/03 累积 SSoT 做 4 维比对。) | |
| 80 | + - 退出码 `0` → 一致,继续。 | |
| 81 | + - 退出码 `1` → docs/03 与 migration 并集分叉(stderr 有 diff 明细):**就地修正**——通常是 docs/03 表小节/列与 V_n 不一致,或 V_n 漏写某列的 ALTER。修正后重跑校验,直到 `0`。**绝不带分叉进入步骤 4**(schema 不一致会让下游 docs/05/编码全线偏)。 | |
| 82 | + - 仅 ADD 追加式(改类型/重命名/删列暂不在 add-req 覆盖范围,见 P2)。 | |
| 75 | 83 | |
| 76 | -> **不在此 apply 到数据库、不跑 validate-ddl**:新 schema 由 Coding 阶段冷起栈时 Flyway 自动 apply 全部 `V*.sql`;validate-ddl 是「docs/03 ↔ 单一 V 文件」整库 4 维比对,多 migration 场景不适用。schema 一致性由 Coding 阶段 testGate / Seed 冷起栈兜底。 | |
| 84 | +> **不在此 apply 到数据库**:新 schema 由 Coding 阶段冷起栈时 Flyway 自动 apply 全部 `V*.sql`;本步只做静态 DDL↔docs/03 一致性校验,运行时 apply 与 Seed 真跑由 Coding 阶段兜底。 | |
| 77 | 85 | |
| 78 | 86 | ## 步骤 4:A5-delta — 下游文档增量(每个新增 REQ) |
| 79 | 87 | |
| ... | ... | @@ -139,6 +147,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> |
| 139 | 147 | ## 参考 |
| 140 | 148 | |
| 141 | 149 | - `${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs`(台账:scan / commit) |
| 150 | +- `${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs`(增量 DDL↔docs/03 校验,支持多 V 文件累积并集) | |
| 142 | 151 | - `docs/01-需求清单/<module>/<req_id>.md`(REQ SSoT,人工先填) |
| 143 | 152 | - `docs/03-数据库设计文档.md` + `sql/migrations/V*.sql`(schema 演化,追加 V_n) |
| 144 | 153 | - `docs/05-API接口契约.md` / `docs/02-开发计划.md` / `docs/08-模块任务管理.md`(下游增量) | ... | ... |
workflows/coding.mjs
| ... | ... | @@ -200,6 +200,9 @@ const ROOT = ARGS?.projectRoot || '.' |
| 200 | 200 | if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { |
| 201 | 201 | throw new Error(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`) |
| 202 | 202 | } |
| 203 | +// 插件根(coding-start 透传):用于调用插件 lib/*.mjs(如 req-ledger 建需求台账基线)。 | |
| 204 | +// 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 | |
| 205 | +const PLUGIN = ARGS?.pluginRoot || '' | |
| 203 | 206 | |
| 204 | 207 | // ── Feature-loop stage prompts(共享非交互契约见 featureStageContract)── |
| 205 | 208 | |
| ... | ... | @@ -1036,6 +1039,33 @@ async function recordResume(sectionMd) { |
| 1036 | 1039 | } catch (e) { log(`resume-journal 异常(不阻断):${String(e?.message || e)}`) } |
| 1037 | 1040 | } |
| 1038 | 1041 | |
| 1042 | +// ── 需求台账基线(P1#3):coding 首跑时若 .req-ledger.json 缺失,自动建立并提交 ────── | |
| 1043 | +// 语义 = 把「coding 开始时的需求快照」定为基线,使日后 /add-req 能正确 diff 出新增/变更, | |
| 1044 | +// 免去现有项目首次 add-req 只为建基线的空跑。幂等(已存在则跳过)、best-effort(失败不阻断)。 | |
| 1045 | +function ledgerBaselinePromptM() { | |
| 1046 | + return [ | |
| 1047 | + '# 需求台账基线(首跑)— 非交互静默', | |
| 1048 | + microStepContract(), | |
| 1049 | + '', | |
| 1050 | + `## 任务:若 \`${ROOT}/.req-ledger.json\` 不存在,则建立并 commit;已存在则跳过`, | |
| 1051 | + `1. 检查 \`${ROOT}/.req-ledger.json\` 是否存在。存在 → 直接返回 \`{ "success": true, "detail": "ledger 已存在,跳过" }\`。`, | |
| 1052 | + `2. 不存在 → 建立基线:\`node ${PLUGIN}/lib/req-ledger.mjs commit ${ROOT}\`(扫 docs/01 REQ 卡 + docs/08 §三 FE 行写哈希)。`, | |
| 1053 | + `3. commit 到当前分支:\`git -C ${ROOT} add .req-ledger.json\` → \`git -C ${ROOT} commit -m "chore(req-ledger): 建立需求台账基线(coding 首跑)"\`。`, | |
| 1054 | + ' (只 add .req-ledger.json 这一个文件;其余未提交的 Plan 产物由后续 runBranchSetup 的脏树恢复处理,勿在此一并提交。)', | |
| 1055 | + '', | |
| 1056 | + '## 输出(ACTION_RESULT_SCHEMA)', | |
| 1057 | + '- 成功 / 已存在 → `{ "success": true }`;失败 → `{ "success": false, "error": "...", "detail": "..." }`(不要抛错)。', | |
| 1058 | + ].join('\n') | |
| 1059 | +} | |
| 1060 | +async function ensureLedgerBaseline() { | |
| 1061 | + if (!PLUGIN) { log('req-ledger 基线跳过:未透传 pluginRoot(由 /add-req 首跑兜底)'); return } | |
| 1062 | + try { | |
| 1063 | + const r = await agent(ledgerBaselinePromptM(), { label:'req-ledger-baseline', phase:'Router', schema: ACTION_RESULT_SCHEMA }) | |
| 1064 | + if (r && r.success) log('req-ledger 基线就绪') | |
| 1065 | + else log(`req-ledger 基线失败(不阻断):${(r && r.error) || ''}`) | |
| 1066 | + } catch (e) { log(`req-ledger 基线异常(不阻断):${String(e?.message || e)}`) } | |
| 1067 | +} | |
| 1068 | + | |
| 1039 | 1069 | // recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。 |
| 1040 | 1070 | // 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。 |
| 1041 | 1071 | // **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD |
| ... | ... | @@ -2049,6 +2079,9 @@ for (const m of routed.modules) { |
| 2049 | 2079 | const todo = routed.modules.filter(m => !m.done) |
| 2050 | 2080 | log(`coding: ${todo.length}/${routed.modules.length} modules to run`) |
| 2051 | 2081 | |
| 2082 | +// P1#3:首跑建需求台账基线(幂等、best-effort),使日后 /add-req 能正确识别增量、免空跑。 | |
| 2083 | +await ensureLedgerBaseline() | |
| 2084 | + | |
| 2052 | 2085 | const results = [] |
| 2053 | 2086 | let haltedAtIdx = -1 |
| 2054 | 2087 | for (const [idx, module] of todo.entries()) { | ... | ... |