Commit 5c491c18a286fd7febf5461d3c5f703b6e4c6f0d
1 parent
5f95dbf0
coding.mjs: 并行编排——模块级依赖波次 + feature 级受限批并行(lane 隔离,fail-open 降级串行)
- 调度:scheduler 子代理只输出依赖边+证据(宁串勿并),JS 做完整性校验 + Kahn 拓扑波次;frontend-phase 由 JS 硬规则依赖全部后端 - 隔离:lane worktree(独立 index)+ lane 测试库(ERP_TEST_DB_SCHEMA)+ migration 版本段(maxV 全局并集)+ 起栈互斥锁(Seed/testGate) - 执行:波次 parallel + milestone/RESUME 主根互斥 + halt 波次边界收敛 + 图状 pending(blockedBy);runModule 永不 throw - feature 级:spec 预生成(写盘不 commit、末端统一提交)+ 后端 REQ 批并行(准入闸=文件集两两不相交∧不改 schema,冲突即弃分支串行重跑) - 穿线:模块作用域 prompt/runner 经 ctx(c) 取根,ROOT 仅主根专属操作;decisions 记账 per-module sink 取代全局水位线 - 降级原则:任何并行机制失败一律 fail-open 回串行,零新增 halt 点;parallel 缺省/modules:false 与原行为一行不差
Showing
1 changed file
with
1491 additions
and
339 deletions
workflows/coding.mjs
| @@ -8,7 +8,7 @@ export const meta = { | @@ -8,7 +8,7 @@ export const meta = { | ||
| 8 | name: 'erp-coding', | 8 | name: 'erp-coding', |
| 9 | description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.', | 9 | description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.', |
| 10 | phases: [ | 10 | phases: [ |
| 11 | - { title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' }, | 11 | + { title: 'Router' }, { title: 'Schedule' }, { title: 'Backend' }, { title: 'Frontend' }, |
| 12 | { title: 'Behavior' }, { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' }, | 12 | { title: 'Behavior' }, { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' }, |
| 13 | ], | 13 | ], |
| 14 | // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次** | 14 | // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次** |
| @@ -24,6 +24,21 @@ const ROUTER_SCHEMA = { type:'object', additionalProperties:false, | @@ -24,6 +24,21 @@ const ROUTER_SCHEMA = { type:'object', additionalProperties:false, | ||
| 24 | reqs:{type:'array',items:{type:'string'}}, | 24 | reqs:{type:'array',items:{type:'string'}}, |
| 25 | feItems:{type:'array',items:{type:'string'}} } } } } } | 25 | feItems:{type:'array',items:{type:'string'}} } } } } } |
| 26 | 26 | ||
| 27 | +// SCHEDULE_SCHEMA:调度子代理只输出"依赖边 + 证据",绝不直接输出"并行组"—— | ||
| 28 | +// 并行性是全局属性由 JS 拓扑推导;LLM 只做局部、可引证的判断。kind=uncertain 也算边(宁串勿并)。 | ||
| 29 | +const SCHEDULE_SCHEMA = { type:'object', additionalProperties:false, | ||
| 30 | + required:['deps'], properties:{ deps:{ type:'array', items:{ | ||
| 31 | + type:'object', additionalProperties:false, | ||
| 32 | + required:['id','dependsOn'], | ||
| 33 | + properties:{ | ||
| 34 | + id:{type:'string'}, | ||
| 35 | + dependsOn:{ type:'array', items:{ type:'object', additionalProperties:false, | ||
| 36 | + required:['id','kind','evidence'], | ||
| 37 | + properties:{ | ||
| 38 | + id:{type:'string'}, | ||
| 39 | + kind:{type:'string', enum:['fk','api','seed','order','doc','uncertain']}, | ||
| 40 | + evidence:{type:'string'} } } } } } } } } | ||
| 41 | + | ||
| 27 | // REVIEW_SCHEMA:reviewer 裁决;issues 结构化对象(summary/locator/severity)驱动 fix。 | 42 | // REVIEW_SCHEMA:reviewer 裁决;issues 结构化对象(summary/locator/severity)驱动 fix。 |
| 28 | const REVIEW_SCHEMA = { type:'object', additionalProperties:false, | 43 | const REVIEW_SCHEMA = { type:'object', additionalProperties:false, |
| 29 | required:['verdict','round','issues'], properties:{ | 44 | required:['verdict','round','issues'], properties:{ |
| @@ -193,6 +208,33 @@ const ACTION_RESULT_SCHEMA = { type:'object', additionalProperties:false, | @@ -193,6 +208,33 @@ const ACTION_RESULT_SCHEMA = { type:'object', additionalProperties:false, | ||
| 193 | error:{type:'string'}, | 208 | error:{type:'string'}, |
| 194 | detail:{type:'string'} } } | 209 | detail:{type:'string'} } } |
| 195 | 210 | ||
| 211 | +// lane DB 支持探测结果(附录补 6b:两点联查)。两布尔各自独立据实返回,"支持与否"的合取判定与 | ||
| 212 | +// 降级决策(任一 false → 本波次降级 maxWidth=1,不 halt)留给编排层(Phase D 波次执行器)—— | ||
| 213 | +// 子代理只做局部、可验证的事实陈述,与调度器"LLM 出边、JS 推全局"同一职责切分。 | ||
| 214 | +const LANE_DB_PROBE_SCHEMA = { type:'object', additionalProperties:false, | ||
| 215 | + required:['scriptsEnv','ymlPlaceholder'], properties:{ | ||
| 216 | + scriptsEnv:{type:'boolean'}, | ||
| 217 | + ymlPlaceholder:{type:'boolean'}, | ||
| 218 | + detail:{type:'string'} } } | ||
| 219 | + | ||
| 220 | +// 残留 lane 枚举(附录补 3a:波次前置占用感知)。只列 `<ROOT>-lanes/` 下已注册的 worktree—— | ||
| 221 | +// 它们持有的 module-* 分支会让主根 checkout 与新 worktree add 双向 fatal(git 实测), | ||
| 222 | +// 必须在**每个波次**(含宽 1)开跑前感知并收口,否则"降级回串行"这条兜底路径本身会被残留 lane 毒化。 | ||
| 223 | +const LANE_LIST_SCHEMA = { type:'object', additionalProperties:false, | ||
| 224 | + required:['lanes'], properties:{ lanes:{ type:'array', items:{ | ||
| 225 | + type:'object', additionalProperties:false, | ||
| 226 | + required:['path','branch'], | ||
| 227 | + properties:{ path:{type:'string'}, branch:{type:'string'} } } } } } | ||
| 228 | + | ||
| 229 | +// PLAN_FACTS_SCHEMA(Phase F Task 12:feature 级并行准入的事实提取)。LLM 只陈述"plan 锁定了 | ||
| 230 | +// 哪些文件、是否声明 schema 改动"——局部、可对照 plan 原文复核的事实;"能否并行"由 JS 确定性 | ||
| 231 | +// 判定(批内两两 files 不相交 ∧ 全部 schemaChange=false ∧ 批宽限额),不信 LLM 单方判断。 | ||
| 232 | +const PLAN_FACTS_SCHEMA = { type:'object', additionalProperties:false, | ||
| 233 | + required:['schemaChange','files'], properties:{ | ||
| 234 | + schemaChange:{type:'boolean'}, | ||
| 235 | + files:{type:'array', items:{type:'string'}}, | ||
| 236 | + detail:{type:'string'} } } | ||
| 237 | + | ||
| 196 | // 兼容 harness 把 args 以 JSON 字符串(而非对象)传入的情况。 | 238 | // 兼容 harness 把 args 以 JSON 字符串(而非对象)传入的情况。 |
| 197 | const ARGS = typeof args === 'string' ? (() => { try { return JSON.parse(args) } catch { return undefined } })() : args | 239 | const ARGS = typeof args === 'string' ? (() => { try { return JSON.parse(args) } catch { return undefined } })() : args |
| 198 | const ROOT = ARGS?.projectRoot || '.' | 240 | const ROOT = ARGS?.projectRoot || '.' |
| @@ -204,6 +246,16 @@ if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { | @@ -204,6 +246,16 @@ if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { | ||
| 204 | // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 | 246 | // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 |
| 205 | const PLUGIN = ARGS?.pluginRoot || '' | 247 | const PLUGIN = ARGS?.pluginRoot || '' |
| 206 | 248 | ||
| 249 | +// 模块执行上下文:串行/主根模式 root=ROOT、lane=null;并行 lane 模式由 Phase C/D 填充。 | ||
| 250 | +// 所有"模块作用域"的 prompt builder 与 runner 一律经 C 取根,不再直接闭包 ROOT。 | ||
| 251 | +// ROOT 仍保留:仅供"主根专属"操作(milestone merge / docs/08 字段 / RESUME / ledger / preflight)使用。 | ||
| 252 | +// 字段:root=本模块工作树根(lane 模式 = lane worktree 路径);lane=lane 序号(null=串行/主根); | ||
| 253 | +// dbSchema=lane 测试库 schema 名(串行为空串);vBase=migration 版本段基址(0=不分段,沿用 max+1); | ||
| 254 | +// decisions=per-module 自主决策收集器(替代全局水位线,见 recordDecisions 的 sink 参数)。 | ||
| 255 | +function makeCtx(overrides = {}) { | ||
| 256 | + return { root: ROOT, lane: null, dbSchema: '', vBase: 0, decisions: [], ...overrides } | ||
| 257 | +} | ||
| 258 | + | ||
| 207 | // ── Feature-loop stage prompts(共享非交互契约见 featureStageContract)── | 259 | // ── Feature-loop stage prompts(共享非交互契约见 featureStageContract)── |
| 208 | 260 | ||
| 209 | function isFrontend(phase) { return phase === 'frontend' } | 261 | function isFrontend(phase) { return phase === 'frontend' } |
| @@ -226,7 +278,10 @@ function dateFromArtifactPath(artifactPath) { | @@ -226,7 +278,10 @@ function dateFromArtifactPath(artifactPath) { | ||
| 226 | } | 278 | } |
| 227 | 279 | ||
| 228 | // 所有子代理共享的"非交互静默"硬约束。 | 280 | // 所有子代理共享的"非交互静默"硬约束。 |
| 229 | -function featureStageContract(phase) { | 281 | +// c:模块执行上下文(末位参数,调用方必传)。lane 模式(c.dbSchema 非空)追加 lane 测试库硬约束—— |
| 282 | +// env 前缀是 JS 拼好的**成品字符串**(附录补 6c):tdd 等主会话还会把命令转述给嵌套子会话(提示词 | ||
| 283 | +// 链两跳),任何一跳自行拼接都可能丢/错前缀,而 setup-test-db 是 DROP+CREATE——成品照抄是唯一可靠形态。 | ||
| 284 | +function featureStageContract(phase, c) { | ||
| 230 | const fe = isFrontend(phase) | 285 | const fe = isFrontend(phase) |
| 231 | return [ | 286 | return [ |
| 232 | '## 硬约束(非交互子代理)', | 287 | '## 硬约束(非交互子代理)', |
| @@ -236,21 +291,28 @@ function featureStageContract(phase) { | @@ -236,21 +291,28 @@ function featureStageContract(phase) { | ||
| 236 | '- 红线:**绝不**留 `【人工填写:】` / `TBD` / `TODO` 占位;**绝不**编造与现有约定/技术规范冲突的"事实";自主默认必须可被现有证据支撑且记入 `decisions[]`。', | 291 | '- 红线:**绝不**留 `【人工填写:】` / `TBD` / `TODO` 占位;**绝不**编造与现有约定/技术规范冲突的"事实";自主默认必须可被现有证据支撑且记入 `decisions[]`。', |
| 237 | '- 仅当缺失的是**无法自洽决策的硬事实**(如某表结构 / 业务主键语义完全缺失且无任何旁证,任何默认都可能污染源码或伪造业务语义)时,才以 `status:halt` 结束并把阻塞点写清;上层会再经仲裁评估能否继续,halt 是最后手段而非首选。', | 292 | '- 仅当缺失的是**无法自洽决策的硬事实**(如某表结构 / 业务主键语义完全缺失且无任何旁证,任何默认都可能污染源码或伪造业务语义)时,才以 `status:halt` 结束并把阻塞点写清;上层会再经仲裁评估能否继续,halt 是最后手段而非首选。', |
| 238 | '- 输出纪律:本次若做过任何自主默认 / 解读,成功返回(status:ok)**必须**带 `decisions[]`(逐条 `{question,choice,rationale,confidence}`,与上面登记要求一致);完全没有自主决策时才可省略——别照抄"输出"段里不含 decisions 的最简示例而漏登记。', | 293 | '- 输出纪律:本次若做过任何自主默认 / 解读,成功返回(status:ok)**必须**带 `decisions[]`(逐条 `{question,choice,rationale,confidence}`,与上面登记要求一致);完全没有自主决策时才可省略——别照抄"输出"段里不含 decisions 的最简示例而漏登记。', |
| 294 | + c.dbSchema | ||
| 295 | + ? `- **并行 lane 测试库(硬约束)**:本模块运行在并行 lane。**所有**测试 / 建库 / 种子命令(gradle test、pnpm test/e2e、\`node scripts/setup-test-db.mjs\`、\`node scripts/seed-demo-data.mjs\`、playwright 等,**含派发给子会话 / 嵌套子会话的命令**)必须前置环境变量 \`ERP_TEST_DB_SCHEMA=${c.dbSchema}\`——成品字符串照抄使用,**绝不**自行拼接 / 改名 / 省略前缀。` | ||
| 296 | + : '', | ||
| 297 | + c.dbSchema | ||
| 298 | + ? '- **lane 内禁全量测试入口(硬约束)**:lane 内 tdd/verify/fix 阶段**只允许目标化测试命令**(如 gradle 按类/按模块过滤、vitest 过滤模式);**禁止**运行含 e2e / 冷起全栈的全量入口(`node scripts/test.mjs`、`pnpm e2e:ci` 等)——它们按固定端口起栈,会击杀兄弟 lane 正在验证的进程;全量回归只发生在 test-gate(已由编排层起栈互斥保护)。' | ||
| 299 | + : '', | ||
| 239 | '- 全部输出文档**使用中文**。', | 300 | '- 全部输出文档**使用中文**。', |
| 240 | `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe | 301 | `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe |
| 241 | ? '实现文件必须落在 `frontend/` 下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。' | 302 | ? '实现文件必须落在 `frontend/` 下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。' |
| 242 | : '产出范围限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约;**禁止**写 `frontend/` 路径下的实现(UI 推迟到前端阶段)。'}`, | 303 | : '产出范围限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约;**禁止**写 `frontend/` 路径下的实现(UI 推迟到前端阶段)。'}`, |
| 243 | `- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `<模块代码>-<子模块代码>-<功能名>`(3 段 req_id,模块代码大写短码 + 子模块代码/功能名大驼峰,如 `USR-UserInfo-Login`)。'}`, | 304 | `- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `<模块代码>-<子模块代码>-<功能名>`(3 段 req_id,模块代码大写短码 + 子模块代码/功能名大驼峰,如 `USR-UserInfo-Login`)。'}`, |
| 244 | - ].join('\n') | 305 | + ].filter(Boolean).join('\n') |
| 245 | } | 306 | } |
| 246 | 307 | ||
| 247 | // commitBlock:spec/plan/verify/review 共用的"写完 → add → commit → 失败 halt"四行块。 | 308 | // commitBlock:spec/plan/verify/review 共用的"写完 → add → commit → 失败 halt"四行块。 |
| 248 | -function commitBlock(addPath, msg, tail = '- commit 失败 → halt,把 stderr 摘要写进 reason。') { | 309 | +// c 为末位参数(调用方必传);tail 用缺省时调用方传 undefined 占位(null 不触发参数默认值)。 |
| 310 | +function commitBlock(addPath, msg, tail = '- commit 失败 → halt,把 stderr 摘要写进 reason。', c) { | ||
| 249 | return [ | 311 | return [ |
| 250 | '## commit', | 312 | '## commit', |
| 251 | `- 写完后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`, | 313 | `- 写完后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`, |
| 252 | - ` 1. \`git -C ${ROOT} add ${addPath}\``, | ||
| 253 | - ` 2. \`git -C ${ROOT} commit -m "${msg}"\``, | 314 | + ` 1. \`git -C ${c.root} add ${addPath}\``, |
| 315 | + ` 2. \`git -C ${c.root} commit -m "${msg}"\``, | ||
| 254 | tail, | 316 | tail, |
| 255 | ].join('\n') | 317 | ].join('\n') |
| 256 | } | 318 | } |
| @@ -289,32 +351,78 @@ function routerPrompt(root) { | @@ -289,32 +351,78 @@ function routerPrompt(root) { | ||
| 289 | ].join('\n') | 351 | ].join('\n') |
| 290 | } | 352 | } |
| 291 | 353 | ||
| 354 | +// Scheduler(Phase B):模块依赖边取证。只读、主根专属(调度发生在任何 lane 建立之前, | ||
| 355 | +// root 传 ROOT,与 routerPrompt 同口径)。LLM 只输出局部、可引证的"依赖边 + 证据"; | ||
| 356 | +// 并行组/波次是全局属性,由 JS 拓扑(nextWave)确定性推导——职责切分是本调度器的设计核心。 | ||
| 357 | +function schedulerPrompt(todoBackendIds, allBackendIds, root) { | ||
| 358 | + return [ | ||
| 359 | + '# Coding Scheduler — 模块依赖边取证', | ||
| 360 | + '', | ||
| 361 | + `项目根:\`${root}\``, | ||
| 362 | + '', | ||
| 363 | + '你是 Coding 阶段的调度子代理。**只读不写**(不改任何代码 / 文档)。逐对审视下列待跑后端模块,输出"谁必须先于谁"的**依赖边 + 证据**,供脚本做确定性拓扑波次调度。', | ||
| 364 | + '**绝不输出"并行组 / 波次"**——并行性是全局属性,由脚本侧拓扑推导;你只做局部、有证据可查的判断。', | ||
| 365 | + '', | ||
| 366 | + '## 保守偏置(最高优先级)', | ||
| 367 | + '不确定就加边(kind=uncertain)。错误串行只损失时间,错误并行损失正确性。', | ||
| 368 | + '', | ||
| 369 | + '## 待跑后端模块(只对它们逐项输出 deps;`frontend-phase` 不在你的职责内——脚本硬规则强制它依赖全部后端模块)', | ||
| 370 | + todoBackendIds.map(id => `- \`${id}\``).join('\n'), | ||
| 371 | + '', | ||
| 372 | + '## dependsOn 合法取值域(全部后端模块 id;不在待跑清单内的为已完成模块,其依赖恒满足,但查到证据时仍应如实列出,保留可审计性)', | ||
| 373 | + allBackendIds.map(id => `- \`${id}\``).join('\n'), | ||
| 374 | + '', | ||
| 375 | + '## 边规则(按证据来源定 kind;方向:`dependsOn` 列出的是"必须先完成"的模块)', | ||
| 376 | + `1. **order**(人定全序,默认全保留):\`${root}/docs/02-开发计划.md § 二 开发顺序清单\` 的模块顺序默认逐字保留——每个待跑模块对其在 docs/02 顺序中的**紧邻前驱**(待跑清单序)输出一条 order 边(链式边传递地保留全序)。**只有当**该模块与其全部前序待跑模块**逐对**都查不到任何 fk/api/seed 证据、**且** \`${root}/docs/06-实现策略.md\` 未对任何一对声明顺序约束时,才可松开(不输出该边);逐对核查中任一对命中证据 → 对该对输出对应 kind 的边。松开是例外,保留是默认。`, | ||
| 377 | + `2. **fk**:\`${root}/docs/03-数据库设计文档.md\` 中本模块归属表的外键引用了另一模块归属的表 → 本模块依赖对方。evidence 写"docs/03 + 表名.外键列 → 被引表"。`, | ||
| 378 | + `3. **api**:\`${root}/docs/05-API接口契约.md\` 中本模块消费另一模块提供的端点,或 \`${root}/docs/01-需求清单/\` REQ 卡「依赖接口」字段指向对方模块端点 → api 边。evidence 写"文件 + 端点路径"。`, | ||
| 379 | + '4. **seed**:演示种子跨模块主键引用(本模块种子数据需引用对方模块表已存在的主键)→ seed 边。evidence 写引用的表与主键区间出处。', | ||
| 380 | + `5. **共享表**(必须串行):两个模块的演示种子会触达**同一张表**(按 docs/03 的表归属 / \`${root}/docs/08-模块任务管理.md\` 各模块 \`路径:\` 字段判定共同表)→ 必须加 fk 或 seed 边(宁串勿并),evidence 写共同表名。**方向固定单向**:按 \`docs/02 § 二\` 顺序**后者依赖前者**(只在后序模块的 dependsOn 列前序模块);**绝不**双向各输出一条——那会构造环,白白损失并行度(环虽有降级兜底)。`, | ||
| 381 | + '6. **doc**:docs/06 实现策略、REQ 卡等文档**显式声明**的顺序/依赖约束,而证据无法归入 fk/api/seed → doc 边。evidence 写"文件 + 小节"。', | ||
| 382 | + '7. **uncertain**:怀疑存在依赖但证据不完整 → 照样输出边(uncertain 也是边,同样阻断并行)。evidence 写怀疑依据。', | ||
| 383 | + '', | ||
| 384 | + '## evidence 纪律', | ||
| 385 | + '- 每条边 `evidence` 必须**可定位**:文件 + 小节 / 表名 / 端点,让人工能直接翻到出处复核。', | ||
| 386 | + '- 空泛的"业务上相关 / 感觉有关"不构成 fk/api/seed/doc 证据——拿不准时按保守偏置改出 uncertain 边,把怀疑点写进 evidence。', | ||
| 387 | + '', | ||
| 388 | + '## 输出(必须符合下发的 JSON schema)', | ||
| 389 | + '- `deps`: 数组,**待跑后端模块每个恰好一项**:`{ "id": "<module>", "dependsOn": [{ "id", "kind", "evidence" }] }`;无依赖则 `dependsOn: []`。', | ||
| 390 | + '- `dependsOn[].id` 只能取上面列出的后端模块 id;**绝不**含 `frontend-phase`,**绝不**自依赖,**绝不**构造互相依赖的环。', | ||
| 391 | + '- 不要返回任何额外字段(schema 为 `additionalProperties:false`)。', | ||
| 392 | + ].join('\n') | ||
| 393 | +} | ||
| 394 | + | ||
| 292 | // ---- 功能内循环 stage 1:派生 spec(原 feature-brainstorm / fe-feature-brainstorm)---- | 395 | // ---- 功能内循环 stage 1:派生 spec(原 feature-brainstorm / fe-feature-brainstorm)---- |
| 293 | -function deriveSpecPrompt(id, phase) { | 396 | +// batch(Phase E Task 11):spec 批量预生成模式——spec 只读 docs/prototype/tokens、不读兄弟 |
| 397 | +// feature 代码,故可在同一工作树并行预生成;但并行 commit 会争 .git/index.lock,batch 下把 | ||
| 398 | +// commitBlock 替换为「只写盘不 commit」,统一提交由 featureLoop 的单一微步骤收口。 | ||
| 399 | +// 注:batch 接在 c 之后是计划 Task 11 的字面签名(`deriveSpecPrompt(id, phase, c, { batch: true })`), | ||
| 400 | +// 是"c 为末位参数"约定的唯一特例——现有 3 参调用点无须改动。 | ||
| 401 | +function deriveSpecPrompt(id, phase, c, { batch = false } = {}) { | ||
| 294 | const fe = isFrontend(phase) | 402 | const fe = isFrontend(phase) |
| 295 | return [ | 403 | return [ |
| 296 | `# ${fe ? 'fe-feature-brainstorm' : 'feature-brainstorm'} — 派生规格 ${id}`, | 404 | `# ${fe ? 'fe-feature-brainstorm' : 'feature-brainstorm'} — 派生规格 ${id}`, |
| 297 | '', | 405 | '', |
| 298 | - featureStageContract(phase), | 406 | + featureStageContract(phase, c), |
| 299 | '', | 407 | '', |
| 300 | '## 目标', | 408 | '## 目标', |
| 301 | - `静默派生 \`${id}\` 的实现规格(无 Q&A)。需求歧义本应在 Plan 期的结构化 per-REQ 表单锁定,前端布局/交互以 \`${ROOT}/prototype/- `静默派生 \`${id}\` 的实现规格(无 Q&A)。需求歧义本应在 Plan 期的结构化 per-REQ 表单锁定,前端布局/交互以 \`${ROOT 为权威;这里**只消费已锁定的事实**,不再澄清。`, | 409 | + `静默派生 \`${id}\` 的实现规格(无 Q&A)。需求歧义本应在 Plan 期的结构化 per-REQ 表单锁定,前端布局/交互以 \`${c.root}/prototype/+ `静默派生 \`${id}\` 的实现规格(无 Q&A)。需求歧义本应在 Plan 期的结构化 per-REQ 表单锁定,前端布局/交互以 \`${c.root 为权威;这里**只消费已锁定的事实**,不再澄清。`, |
| 302 | '', | 410 | '', |
| 303 | '## 收集上下文', | 411 | '## 收集上下文', |
| 304 | fe | 412 | fe |
| 305 | ? [ | 413 | ? [ |
| 306 | - `- 关联 REQ 卡片:\`${ROOT}/docs/01-需求清单/<module>/<REQ>.md\`(提取业务校验规则、acceptance、UI 描述)。`, | ||
| 307 | - `- 关联 prototype:Read \`${ROOT}/prototype/**/*.html\`(含 anchor 时聚焦相应区域),作为页面布局权威。`, | ||
| 308 | - `- API 契约:\`${ROOT}/docs/05-API接口契约.md\`,按本 FE 关联的 REQ 过滤出消费的端点。`, | ||
| 309 | - `- Design Tokens:\`${ROOT}/src/styles/tokens.css\`(色值 / 状态色单一来源;只用 var(--color-*),禁硬编码 hex)。**与 prototype 的色值冲突时以 tokens.css 为准**(prototype 管结构/布局/交互)。`, | ||
| 310 | - `- 前端组件库:\`${ROOT}/docs/04-技术规范.md § 零\` 的 \`frontend.ui_lib\`,决定组件选型。`, | ||
| 311 | - `- 实现策略:\`${ROOT}/docs/06-实现策略.md\`(A2 人工填写;关键技术决策 / 取舍、实现顺序与依赖、难点·风险、对默认流程的偏离——遵循其中与本 FE 相关的指引)。`, | 414 | + `- 关联 REQ 卡片:\`${c.root}/docs/01-需求清单/<module>/<REQ>.md\`(提取业务校验规则、acceptance、UI 描述)。`, |
| 415 | + `- 关联 prototype:Read \`${c.root}/prototype/**/*.html\`(含 anchor 时聚焦相应区域),作为页面布局权威。`, | ||
| 416 | + `- API 契约:\`${c.root}/docs/05-API接口契约.md\`,按本 FE 关联的 REQ 过滤出消费的端点。`, | ||
| 417 | + `- Design Tokens:\`${c.root}/src/styles/tokens.css\`(色值 / 状态色单一来源;只用 var(--color-*),禁硬编码 hex)。**与 prototype 的色值冲突时以 tokens.css 为准**(prototype 管结构/布局/交互)。`, | ||
| 418 | + `- 前端组件库:\`${c.root}/docs/04-技术规范.md § 零\` 的 \`frontend.ui_lib\`,决定组件选型。`, | ||
| 419 | + `- 实现策略:\`${c.root}/docs/06-实现策略.md\`(A2 人工填写;关键技术决策 / 取舍、实现顺序与依赖、难点·风险、对默认流程的偏离——遵循其中与本 FE 相关的指引)。`, | ||
| 312 | ].join('\n') | 420 | ].join('\n') |
| 313 | : [ | 421 | : [ |
| 314 | - `- REQ 卡片:\`${ROOT}/docs/01-需求清单/<module>/${id}.md\`。**忽略 UI 描述**(控件类型 / 按钮位置 / 列表布局),但校验规则、业务规则仍要落到后端 DTO + service。`, | ||
| 315 | - `- 涉及的数据表定义:\`${ROOT}/docs/03-数据库设计文档.md\`(必要时实时查 mysql 只读)。`, | ||
| 316 | - `- API 契约:\`${ROOT}/docs/05-API接口契约.md\` 中本 REQ 相关端点。`, | ||
| 317 | - `- 实现策略:\`${ROOT}/docs/06-实现策略.md\`(A2 人工填写;关键技术决策 / 取舍、实现顺序与依赖、难点·风险、对默认流程的偏离——遵循其中与本 REQ 相关的指引)。`, | 422 | + `- REQ 卡片:\`${c.root}/docs/01-需求清单/<module>/${id}.md\`。**忽略 UI 描述**(控件类型 / 按钮位置 / 列表布局),但校验规则、业务规则仍要落到后端 DTO + service。`, |
| 423 | + `- 涉及的数据表定义:\`${c.root}/docs/03-数据库设计文档.md\`(必要时实时查 mysql 只读)。`, | ||
| 424 | + `- API 契约:\`${c.root}/docs/05-API接口契约.md\` 中本 REQ 相关端点。`, | ||
| 425 | + `- 实现策略:\`${c.root}/docs/06-实现策略.md\`(A2 人工填写;关键技术决策 / 取舍、实现顺序与依赖、难点·风险、对默认流程的偏离——遵循其中与本 REQ 相关的指引)。`, | ||
| 318 | ].join('\n'), | 426 | ].join('\n'), |
| 319 | '', | 427 | '', |
| 320 | '## 写 spec', | 428 | '## 写 spec', |
| @@ -333,13 +441,19 @@ function deriveSpecPrompt(id, phase) { | @@ -333,13 +441,19 @@ function deriveSpecPrompt(id, phase) { | ||
| 333 | ' - 关联路由: [/orders, /orders/:id]', | 441 | ' - 关联路由: [/orders, /orders/:id]', |
| 334 | ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]', | 442 | ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]', |
| 335 | ' ```', | 443 | ' ```', |
| 336 | - `- **关联路由**:从 \`${ROOT}/frontend/- `- **关联路由**:从 \`${ROOT router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 - `- **关联路由**:从 \`${ROOT:id- `- **关联路由**:从 \`${ROOT 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`, | 444 | + `- **关联路由**:从 \`${c.root}/frontend/+ `- **关联路由**:从 \`${c.root router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 + `- **关联路由**:从 \`${c.root:id+ `- **关联路由**:从 \`${c.root 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`, |
| 337 | '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外控件记证据不算缺陷。', | 445 | '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外控件记证据不算缺陷。', |
| 338 | '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes;阶段末尾的行为门会聚合**全部 FE** 的该小节作为整体断言作用域,缺失的 FE 会被记 `scope-missing` 阻断 green);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。', | 446 | '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes;阶段末尾的行为门会聚合**全部 FE** 的该小节作为整体断言作用域,缺失的 FE 会被记 `scope-missing` 阻断 green);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。', |
| 339 | ].join('\n') | 447 | ].join('\n') |
| 340 | : '', | 448 | : '', |
| 341 | '', | 449 | '', |
| 342 | - commitBlock('<spec artifactPath>', `docs(spec:${id}): 派生规格`), | 450 | + batch |
| 451 | + ? [ | ||
| 452 | + '## 落盘纪律(批量预生成模式:**只写盘,不 commit**)', | ||
| 453 | + '- 本 stage 正与兄弟 feature 的 spec 在**同一工作树并行**预生成:写完 spec 文件即止,**绝不** `git add` / `git commit` / 任何改写 .git 状态的命令(并行 commit 会争 .git/index.lock)。', | ||
| 454 | + '- 统一提交由编排层在全部 spec 落盘后以单个微步骤完成(`git add docs/superpowers/specs` + commit),无须你善后。', | ||
| 455 | + ].join('\n') | ||
| 456 | + : commitBlock('<spec artifactPath>', `docs(spec:${id}): 派生规格`, undefined, c), | ||
| 343 | '', | 457 | '', |
| 344 | '## 自审(inline 修,无须等待)', | 458 | '## 自审(inline 修,无须等待)', |
| 345 | `- 占位符扫描:\`TBD\` / \`TODO\` / \`【人工填写:】\`${fe ? ' / `controller` / `service` / `SQL` / `migration`(前端 spec 不应出现后端字样)' : ''} → 命中即修;修不掉的缺值按硬约束失败。`, | 459 | `- 占位符扫描:\`TBD\` / \`TODO\` / \`【人工填写:】\`${fe ? ' / `controller` / `service` / `SQL` / `migration`(前端 spec 不应出现后端字样)' : ''} → 命中即修;修不掉的缺值按硬约束失败。`, |
| @@ -354,18 +468,18 @@ function deriveSpecPrompt(id, phase) { | @@ -354,18 +468,18 @@ function deriveSpecPrompt(id, phase) { | ||
| 354 | 468 | ||
| 355 | // ---- stage 2:spec → 任务级 TDD 计划(原 feature-plan / fe-feature-plan)---- | 469 | // ---- stage 2:spec → 任务级 TDD 计划(原 feature-plan / fe-feature-plan)---- |
| 356 | // specPath:调用方传入的 spec artifactPath(含 YYYY-MM-DD 前缀),plan 复用该日期。 | 470 | // specPath:调用方传入的 spec artifactPath(含 YYYY-MM-DD 前缀),plan 复用该日期。 |
| 357 | -function planPrompt(id, phase, specPath) { | 471 | +function planPrompt(id, phase, specPath, c) { |
| 358 | const fe = isFrontend(phase) | 472 | const fe = isFrontend(phase) |
| 359 | return [ | 473 | return [ |
| 360 | `# ${fe ? 'fe-feature-plan' : 'feature-plan'} — 任务级计划 ${id}`, | 474 | `# ${fe ? 'fe-feature-plan' : 'feature-plan'} — 任务级计划 ${id}`, |
| 361 | '', | 475 | '', |
| 362 | - featureStageContract(phase), | 476 | + featureStageContract(phase, c), |
| 363 | '', | 477 | '', |
| 364 | '## 输入', | 478 | '## 输入', |
| 365 | `- 上游 spec:\`${specPath}\`(已由 spec stage 落盘;不存在则 halt)。**plan 文件名日期前缀必须与 spec 一致**:取 spec 文件名首段 \`YYYY-MM-DD\`,写到 plan 路径,不要重新解析"今天"。`, | 479 | `- 上游 spec:\`${specPath}\`(已由 spec stage 落盘;不存在则 halt)。**plan 文件名日期前缀必须与 spec 一致**:取 spec 文件名首段 \`YYYY-MM-DD\`,写到 plan 路径,不要重新解析"今天"。`, |
| 366 | fe | 480 | fe |
| 367 | - ? `- \`${ROOT}/docs/04-技术规范.md § 二 前端规范\`(§ 2.1 目录约定 = 落盘位置;状态管理 / 请求封装 / 错误处理);色值 / 样式见 \`${ROOT}/src/styles/tokens.css\`;测试栈见 § 零。用 Grep 在 \`${ROOT}/frontend/\` 定位现有文件。` | ||
| 368 | - : `- \`${ROOT}/docs/04-技术规范.md\`(编码规范 + § 1.2 分层结构 = 后端落盘)。用 Grep 在现有代码定位待修改文件。`, | 481 | + ? `- \`${c.root}/docs/04-技术规范.md § 二 前端规范\`(§ 2.1 目录约定 = 落盘位置;状态管理 / 请求封装 / 错误处理);色值 / 样式见 \`${c.root}/src/styles/tokens.css\`;测试栈见 § 零。用 Grep 在 \`${c.root}/frontend/\` 定位现有文件。` |
| 482 | + : `- \`${c.root}/docs/04-技术规范.md\`(编码规范 + § 1.2 分层结构 = 后端落盘)。用 Grep 在现有代码定位待修改文件。`, | ||
| 369 | '', | 483 | '', |
| 370 | '## 计划写作原则', | 484 | '## 计划写作原则', |
| 371 | '- Plan 告诉 TDD 执行者**做什么**,不是**怎么写代码**(执行者是同模型、全上下文的 tdd stage)。', | 485 | '- Plan 告诉 TDD 执行者**做什么**,不是**怎么写代码**(执行者是同模型、全上下文的 tdd stage)。', |
| @@ -386,7 +500,7 @@ function planPrompt(id, phase, specPath) { | @@ -386,7 +500,7 @@ function planPrompt(id, phase, specPath) { | ||
| 386 | `- 落盘路径:\`docs/superpowers/plans/<同 spec 的 YYYY-MM-DD>-${id}.md\`,文件头含 Goal / Architecture / Tech Stack + checkbox 任务。`, | 500 | `- 落盘路径:\`docs/superpowers/plans/<同 spec 的 YYYY-MM-DD>-${id}.md\`,文件头含 Goal / Architecture / Tech Stack + checkbox 任务。`, |
| 387 | '- 自审:占位符扫描(按硬约束清单);spec coverage(spec 每节至少指向一个 task,补 gap);类型一致性(签名 / 方法名 / 错误码 / props 一致)。', | 501 | '- 自审:占位符扫描(按硬约束清单);spec coverage(spec 每节至少指向一个 task,补 gap);类型一致性(签名 / 方法名 / 错误码 / props 一致)。', |
| 388 | '', | 502 | '', |
| 389 | - commitBlock('<plan artifactPath>', `docs(plan:${id}): 任务级 TDD 计划`), | 503 | + commitBlock('<plan artifactPath>', `docs(plan:${id}): 任务级 TDD 计划`, undefined, c), |
| 390 | '', | 504 | '', |
| 391 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', | 505 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', |
| 392 | '- 成功:`{ "status": "ok", "artifactPath": "docs/superpowers/plans/YYYY-MM-DD-' + id + '.md", "summary": "<1-2 句中文摘要:任务数 / 涉及文件作用域>" }`', | 506 | '- 成功:`{ "status": "ok", "artifactPath": "docs/superpowers/plans/YYYY-MM-DD-' + id + '.md", "summary": "<1-2 句中文摘要:任务数 / 涉及文件作用域>" }`', |
| @@ -397,21 +511,37 @@ function planPrompt(id, phase, specPath) { | @@ -397,21 +511,37 @@ function planPrompt(id, phase, specPath) { | ||
| 397 | 511 | ||
| 398 | // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)---- | 512 | // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)---- |
| 399 | // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。 | 513 | // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。 |
| 400 | -function tddPrompt(id, phase, planPath) { | 514 | +function tddPrompt(id, phase, planPath, c) { |
| 401 | const fe = isFrontend(phase) | 515 | const fe = isFrontend(phase) |
| 402 | return [ | 516 | return [ |
| 403 | `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`, | 517 | `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`, |
| 404 | '', | 518 | '', |
| 405 | - featureStageContract(phase), | 519 | + featureStageContract(phase, c), |
| 406 | '', | 520 | '', |
| 407 | '## 输入', | 521 | '## 输入', |
| 408 | `- 计划文件:\`${planPath}\`(不存在则 halt)。`, | 522 | `- 计划文件:\`${planPath}\`(不存在则 halt)。`, |
| 409 | - `- 测试命令来源:\`${ROOT}/docs/04-技术规范.md § 零\`${fe | 523 | + // 后端默认命令按 lane 分支:顶部硬约束已禁 lane 内全量入口,输入段若仍把 `node scripts/test.mjs` |
| 524 | + // 钉成默认,同一 prompt 就顶部禁止、底部指定——命令还要两跳转述给嵌套子会话,必须在源头给出 | ||
| 525 | + // lane 内合法默认(目标化 gradle 过滤)。串行(c.dbSchema 空)与前端文案一字不动。 | ||
| 526 | + `- 测试命令来源:\`${c.root}/docs/04-技术规范.md § 零\`${fe | ||
| 410 | ? ' 的 `frontend.unit_test_runner` / `frontend.e2e_runner` / `frontend.test_command` / `frontend.e2e_command`(缺失则默认 `pnpm test:ci` / `pnpm e2e:ci`)。' | 527 | ? ' 的 `frontend.unit_test_runner` / `frontend.e2e_runner` / `frontend.test_command` / `frontend.e2e_command`(缺失则默认 `pnpm test:ci` / `pnpm e2e:ci`)。' |
| 411 | - : ' 确认的后端测试命令(如 Gradle task / `./scripts/test.mjs`);缺失则默认 `node scripts/test.mjs`(与 test-gate 一致)。'}`, | 528 | + : c.dbSchema |
| 529 | + ? ' 确认的后端测试命令,并**目标化**到本功能涉及的测试类/模块(如 Gradle `test --tests` 按类过滤)——lane 模式见上方硬约束,**禁全量入口**;docs/04 缺后端命令时默认 Gradle 按类过滤跑本功能测试,**绝不**回退 `node scripts/test.mjs` 等冷起全栈的全量命令。' | ||
| 530 | + : ' 确认的后端测试命令(如 Gradle task / `./scripts/test.mjs`);缺失则默认 `node scripts/test.mjs`(与 test-gate 一致)。'}`, | ||
| 412 | '', | 531 | '', |
| 413 | '## 流程', | 532 | '## 流程', |
| 414 | - fe ? '' : '- **Schema 改动前置**(仅当 plan 声明需要):第一个任务写 migration 文件 `V<n>__<snake_case>.sql`(`<n>` = 现有 `sql/migrations/V*.sql` 最大版本号 + 1,只含 DDL),**同步**把新 CREATE / ALTER 反向更新到 `docs/03-数据库设计文档.md` 对应表小节(docs/03 是 schema 的 SSoT),migration + docs/03 改动同一 commit。', | 533 | + // V<n> 段规则(Task 7 Step 3):c.vBase > 0(并行 lane)时取号改走专属百段,杜绝同波次模块从同一 |
| 534 | + // 基点各算 max+1 撞号。依据:同波次以"无依赖边"为准入——独立模块的 migration 互不引用,版本相对 | ||
| 535 | + // 顺序任意;lane 测试库每次从零重建、合并后全量按版本序 apply,段间空洞无影响。并行版本段下版本号 | ||
| 536 | + // **只保唯一、不保连续、不保合并序**(附录补 5:高版本段可能先合入默认分支,骨架约定 Flyway | ||
| 537 | + // out-of-order: true,乱序 apply 安全)。串行(vBase=0)维持现行 max+1 文案,一字不差。 | ||
| 538 | + fe ? '' : `- **Schema 改动前置**(仅当 plan 声明需要):第一个任务写 migration 文件 \`V<n>__<snake_case>.sql\`(${c.vBase > 0 | ||
| 539 | + ? `\`<n>\` 取**你的专属版本段 [${c.vBase}, ${c.vBase + 99}]** 内现有最大版本号 +1(段内尚无文件则取 ${c.vBase});**绝不**使用段外版本号——并行版本段下版本号只保唯一、不保连续,段间空洞与乱序合并安全(骨架已约定 Flyway out-of-order),**不要**"补洞"或改用段外小号` | ||
| 540 | + : '\`<n>\` = 现有 \`sql/migrations/V*.sql\` 最大版本号 + 1'},只含 DDL),**同步**把新 CREATE / ALTER 反向更新到 \`docs/03-数据库设计文档.md\` 对应表小节(docs/03 是 schema 的 SSoT),migration + docs/03 改动同一 commit。`, | ||
| 541 | + // 数据源占位约定(附录补 6a):lane 测试库 env 能打穿 Spring 进程的前提——application*.yml 写死 | ||
| 542 | + // schema 时 ERP_TEST_DB_SCHEMA 传进去无人消费(脚本支持 ≠ 测试进程支持)。占位 env 缺省回退字面量, | ||
| 543 | + // 串行行为一字不变;gradle test fork 与 bootRun 默认继承 env,无需 SPRING_DATASOURCE_URL 拼装。 | ||
| 544 | + fe ? '' : '- **数据源 schema 占位约定**:生成 / 修改后端 `application*.yml`(含测试 profile)时,JDBC URL 的 schema 段必须写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:<db.schema 字面量>}`(env 缺省回退原 schema,行为不变);**绝不**把 schema 名写死成纯字面量。', | ||
| 415 | '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。', | 545 | '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。', |
| 416 | fe | 546 | fe |
| 417 | ? '- **测试落位(硬约定,对齐 docs/04 § 2.1)**:jsdom 单测用 vitest/jest 一律写到 `frontend/tests/`,目录**镜像** `frontend/src/` 相对路径(如 `src/pages/home/HomePage.tsx` → `tests/pages/home/HomePage.test.tsx`);e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。**绝不**把 `*.test.*` / `*.spec.*` / `__tests__/` / `__mocks__/` / `__smoke__/` 落在 `frontend/src/` 内(交付源码与测试物理分离,同后端 src/main↔src/test)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' | 547 | ? '- **测试落位(硬约定,对齐 docs/04 § 2.1)**:jsdom 单测用 vitest/jest 一律写到 `frontend/tests/`,目录**镜像** `frontend/src/` 相对路径(如 `src/pages/home/HomePage.tsx` → `tests/pages/home/HomePage.test.tsx`);e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。**绝不**把 `*.test.*` / `*.spec.*` / `__tests__/` / `__mocks__/` / `__smoke__/` 落在 `frontend/src/` 内(交付源码与测试物理分离,同后端 src/main↔src/test)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' |
| @@ -420,7 +550,7 @@ function tddPrompt(id, phase, planPath) { | @@ -420,7 +550,7 @@ function tddPrompt(id, phase, planPath) { | ||
| 420 | ? '- **e2e 基线约束**:e2e 跑在「空库重建 + Flyway schema + 演示种子」基线上(骨架 globalSetup 已注入 `sql/seed`,无需测试自行建库/起栈)。e2e 断言**优先**定位**演示种子已知主键行**(1000–9999)或**测试自建数据**;**禁止**「全表恰好 N 行」式依赖全局计数的脆弱断言(演示种子行数会随后续模块种子增长,全局计数断言必然 flaky)。' | 550 | ? '- **e2e 基线约束**:e2e 跑在「空库重建 + Flyway schema + 演示种子」基线上(骨架 globalSetup 已注入 `sql/seed`,无需测试自行建库/起栈)。e2e 断言**优先**定位**演示种子已知主键行**(1000–9999)或**测试自建数据**;**禁止**「全表恰好 N 行」式依赖全局计数的脆弱断言(演示种子行数会随后续模块种子增长,全局计数断言必然 flaky)。' |
| 421 | : '', | 551 | : '', |
| 422 | fe | 552 | fe |
| 423 | - ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/- ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT router 定位本 FE 路由 path 的 import 行;仍在 - ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOTfrontend/- ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` | 553 | + ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${c.root}/frontend/+ ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${c.root router 定位本 FE 路由 path 的 import 行;仍在 + ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${c.rootfrontend/+ ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${c.root 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` |
| 424 | : '', | 554 | : '', |
| 425 | '', | 555 | '', |
| 426 | '## 护栏', | 556 | '## 护栏', |
| @@ -444,13 +574,13 @@ function tddPrompt(id, phase, planPath) { | @@ -444,13 +574,13 @@ function tddPrompt(id, phase, planPath) { | ||
| 444 | // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)---- | 574 | // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)---- |
| 445 | // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件, | 575 | // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件, |
| 446 | // 避免 reverify 覆盖前轮证据)。 | 576 | // 避免 reverify 覆盖前轮证据)。 |
| 447 | -function verifyPrompt(id, phase, implSummary, specPath, round = 0) { | 577 | +function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { |
| 448 | const fe = isFrontend(phase) | 578 | const fe = isFrontend(phase) |
| 449 | const suffix = round === 0 ? 'verify' : `verify-r${round}` | 579 | const suffix = round === 0 ? 'verify' : `verify-r${round}` |
| 450 | return [ | 580 | return [ |
| 451 | `# ${fe ? 'fe-feature-verify' : 'feature-verify'} — 证据验证 ${id}${round > 0 ? `(第 ${round} 轮 fix 后复验)` : ''}`, | 581 | `# ${fe ? 'fe-feature-verify' : 'feature-verify'} — 证据验证 ${id}${round > 0 ? `(第 ${round} 轮 fix 后复验)` : ''}`, |
| 452 | '', | 582 | '', |
| 453 | - featureStageContract(phase), | 583 | + featureStageContract(phase, c), |
| 454 | '', | 584 | '', |
| 455 | '## 目标', | 585 | '## 目标', |
| 456 | `把 \`${id}\` 的功能测试**派发到 Agent 子会话**执行,按结构化结果渲染证据。**主会话从不直接跑测试,也不自由编写证据。**`, | 586 | `把 \`${id}\` 的功能测试**派发到 Agent 子会话**执行,按结构化结果渲染证据。**主会话从不直接跑测试,也不自由编写证据。**`, |
| @@ -460,19 +590,20 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0) { | @@ -460,19 +590,20 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0) { | ||
| 460 | '## 流程', | 590 | '## 流程', |
| 461 | fe | 591 | fe |
| 462 | ? [ | 592 | ? [ |
| 463 | - `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command- `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOT / - `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOTfrontend.e2e_command- `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOT 取(缺失默认 - `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOTpnpm test:ci- `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOT / - `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOTpnpm e2e:ci- `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${ROOT)。`, | 593 | + `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command+ `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root / + `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.rootfrontend.e2e_command+ `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root 取(缺失默认 + `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.rootpnpm test:ci+ `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root / + `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.rootpnpm e2e:ci+ `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root)。`, |
| 464 | '- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。', | 594 | '- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。', |
| 465 | '- **任一目标 `exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt,不进入 review。', | 595 | '- **任一目标 `exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt,不进入 review。', |
| 466 | ].join('\n') | 596 | ].join('\n') |
| 467 | : [ | 597 | : [ |
| 468 | - `- 测试目标:从 plan 或项目标准命令确定(Gradle task / pnpm script / pytest path / \`${ROOT}/docs/04-技术规范.md § 零\` 的后端命令)。`, | 598 | + // lane 模式追加过滤提示:泛指入口里混着全量命令形态,不点破会与顶部"禁全量入口"硬约束打架。 |
| 599 | + `- 测试目标:从 plan 或项目标准命令确定(Gradle task / pnpm script / pytest path / \`${c.root}/docs/04-技术规范.md § 零\` 的后端命令)。${c.dbSchema ? 'lane 模式见上方硬约束:只取**目标化**命令(gradle 按类/按模块过滤),**禁全量入口**。' : ''}`, | ||
| 469 | '- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行,不塞全文 stdout)。', | 600 | '- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行,不塞全文 stdout)。', |
| 470 | '- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt,不进入 review。', | 601 | '- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt,不进入 review。', |
| 471 | ].join('\n'), | 602 | ].join('\n'), |
| 472 | `- 证据落盘路径固定为 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}-${suffix}.md\`(与 review 报告同目录;round=0 → \`-verify.md\`;round>=1 → \`-verify-r<N>.md\`,**每轮独立文件不覆盖前轮**)。同时把核心结构化结果摘要打印到会话便于上层 review stage 引用,**不要**自行另起目录或自由命名文件。`, | 603 | `- 证据落盘路径固定为 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}-${suffix}.md\`(与 review 报告同目录;round=0 → \`-verify.md\`;round>=1 → \`-verify-r<N>.md\`,**每轮独立文件不覆盖前轮**)。同时把核心结构化结果摘要打印到会话便于上层 review stage 引用,**不要**自行另起目录或自由命名文件。`, |
| 473 | '', | 604 | '', |
| 474 | commitBlock('<证据 artifactPath>', `docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证`, | 605 | commitBlock('<证据 artifactPath>', `docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证`, |
| 475 | - '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'), | 606 | + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。', c), |
| 476 | '', | 607 | '', |
| 477 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', | 608 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', |
| 478 | `- 全部通过:\`{ "status": "ok", "artifactPath": "docs/superpowers/reviews/YYYY-MM-DD-${id}-${suffix}.md", "summary": "<exit_code / passed / failed / failed_list 摘要 ≤ 200 字>" }\`。`, | 609 | `- 全部通过:\`{ "status": "ok", "artifactPath": "docs/superpowers/reviews/YYYY-MM-DD-${id}-${suffix}.md", "summary": "<exit_code / passed / failed / failed_list 摘要 ≤ 200 字>" }\`。`, |
| @@ -483,12 +614,12 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0) { | @@ -483,12 +614,12 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0) { | ||
| 483 | // ---- stage 5a:AI 自审 diff(原 feature-review / fe-feature-review)——委托统一 reviewer agent ---- | 614 | // ---- stage 5a:AI 自审 diff(原 feature-review / fe-feature-review)——委托统一 reviewer agent ---- |
| 484 | // lastVerifySummary:round>1 时传入上轮 fix 后复验摘要,让 reviewer 看到"上轮 must-fix 真的修了什么"。 | 615 | // lastVerifySummary:round>1 时传入上轮 fix 后复验摘要,让 reviewer 看到"上轮 must-fix 真的修了什么"。 |
| 485 | // specPath:spec artifactPath(日期前缀来源 + reviewer 上下文输入)。 | 616 | // specPath:spec artifactPath(日期前缀来源 + reviewer 上下文输入)。 |
| 486 | -function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { | 617 | +function reviewPrompt(id, phase, round, lastVerifySummary, specPath, c) { |
| 487 | const fe = isFrontend(phase) | 618 | const fe = isFrontend(phase) |
| 488 | return [ | 619 | return [ |
| 489 | `# ${fe ? 'fe-feature-review' : 'feature-review'} — AI 自审 ${id}(第 ${round} 轮)`, | 620 | `# ${fe ? 'fe-feature-review' : 'feature-review'} — AI 自审 ${id}(第 ${round} 轮)`, |
| 490 | '', | 621 | '', |
| 491 | - featureStageContract(phase), | 622 | + featureStageContract(phase, c), |
| 492 | '', | 623 | '', |
| 493 | '## 目标', | 624 | '## 目标', |
| 494 | `对 \`${id}\` 本轮引入的代码 diff 做 AI 自审,给出 \`approve\` 或 \`request-changes\` 裁决。`, | 625 | `对 \`${id}\` 本轮引入的代码 diff 做 AI 自审,给出 \`approve\` 或 \`request-changes\` 裁决。`, |
| @@ -497,7 +628,7 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { | @@ -497,7 +628,7 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { | ||
| 497 | `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`, | 628 | `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`, |
| 498 | fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '', | 629 | fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '', |
| 499 | `- **phase = ${fe ? 'frontend → 附加前端 8 维 checklist(含 §8 测试文件隔离:本轮 diff 在 frontend/src/ 内引入任何 *.test.* / *.spec.* / __tests__ / __mocks__ / __smoke__ → must-fix,应移至 frontend/tests/ 镜像路径)。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`, | 630 | `- **phase = ${fe ? 'frontend → 附加前端 8 维 checklist(含 §8 测试文件隔离:本轮 diff 在 frontend/src/ 内引入任何 *.test.* / *.spec.* / __tests__ / __mocks__ / __smoke__ → must-fix,应移至 frontend/tests/ 镜像路径)。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`, |
| 500 | - fe ? `- **行为验收作用域小节校验(阶段级行为门的作用域真值来源,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/- fe ? `- **行为验收作用域小节校验(阶段级行为门的作用域真值来源,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——阶段末尾的行为门按全部 FE spec 的该小节聚合断言作用域,缺失/错配会让该 FE 漏验或归因失真。` : '', | 631 | + fe ? `- **行为验收作用域小节校验(阶段级行为门的作用域真值来源,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${c.root}/frontend/+ fe ? `- **行为验收作用域小节校验(阶段级行为门的作用域真值来源,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${c.root router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——阶段末尾的行为门按全部 FE spec 的该小节聚合断言作用域,缺失/错配会让该 FE 漏验或归因失真。` : '', |
| 501 | round > 1 && lastVerifySummary | 632 | round > 1 && lastVerifySummary |
| 502 | ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。` | 633 | ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。` |
| 503 | : '', | 634 | : '', |
| @@ -512,13 +643,13 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { | @@ -512,13 +643,13 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { | ||
| 512 | '', | 643 | '', |
| 513 | commitBlock(`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md`, | 644 | commitBlock(`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md`, |
| 514 | `docs(review:${id}:r${round}): <verdict>`, | 645 | `docs(review:${id}:r${round}): <verdict>`, |
| 515 | - '- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。'), | 646 | + '- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。', c), |
| 516 | ].filter(Boolean).join('\n') | 647 | ].filter(Boolean).join('\n') |
| 517 | } | 648 | } |
| 518 | 649 | ||
| 519 | // ---- stage 5b:按 review must-fix 修复并重新 commit(review 循环的 fix 步)---- | 650 | // ---- stage 5b:按 review must-fix 修复并重新 commit(review 循环的 fix 步)---- |
| 520 | // issues:结构化对象数组 {summary, locator, severity}(见 REVIEW_SCHEMA)。 | 651 | // issues:结构化对象数组 {summary, locator, severity}(见 REVIEW_SCHEMA)。 |
| 521 | -function fixPrompt(id, phase, issues) { | 652 | +function fixPrompt(id, phase, issues, c) { |
| 522 | const fe = isFrontend(phase) | 653 | const fe = isFrontend(phase) |
| 523 | const list = Array.isArray(issues) && issues.length | 654 | const list = Array.isArray(issues) && issues.length |
| 524 | ? issues.map((x, i) => ` ${i + 1}. [${x.severity}] ${x.summary} — locator: \`${x.locator}\``).join('\n') | 655 | ? issues.map((x, i) => ` ${i + 1}. [${x.severity}] ${x.summary} — locator: \`${x.locator}\``).join('\n') |
| @@ -526,14 +657,14 @@ function fixPrompt(id, phase, issues) { | @@ -526,14 +657,14 @@ function fixPrompt(id, phase, issues) { | ||
| 526 | return [ | 657 | return [ |
| 527 | `# ${fe ? 'fe-feature' : 'feature'} fix — 修复 review must-fix ${id}`, | 658 | `# ${fe ? 'fe-feature' : 'feature'} fix — 修复 review must-fix ${id}`, |
| 528 | '', | 659 | '', |
| 529 | - featureStageContract(phase), | 660 | + featureStageContract(phase, c), |
| 530 | '', | 661 | '', |
| 531 | '## 待修复 must-fix(已结构化)', | 662 | '## 待修复 must-fix(已结构化)', |
| 532 | list, | 663 | list, |
| 533 | '', | 664 | '', |
| 534 | '## 流程', | 665 | '## 流程', |
| 535 | '- 逐项编辑 locator 指向的代码文件(遵守阶段路径作用域护栏)。', | 666 | '- 逐项编辑 locator 指向的代码文件(遵守阶段路径作用域护栏)。', |
| 536 | - `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT} cat-file -e HEAD:<locator 的文件部分>- `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT(locator 形如 - `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOTpath:line- `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT 时取 - `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOTpath- `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT)。文件不存在 → halt,把 locator 写进 reason,不要"修一个不存在的文件"。`, | 667 | + `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${c.root} cat-file -e HEAD:<locator 的文件部分>+ `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${c.root(locator 形如 + `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${c.rootpath:line+ `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${c.root 时取 + `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${c.rootpath+ `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${c.root)。文件不存在 → halt,把 locator 写进 reason,不要"修一个不存在的文件"。`, |
| 537 | `- 修复后 commit:\`fix(<scope>): 修复 review must-fix ${fe ? `FE: ${id}` : `REQ: ${id}`}\`(不混合无关改动)。`, | 668 | `- 修复后 commit:\`fix(<scope>): 修复 review must-fix ${fe ? `FE: ${id}` : `REQ: ${id}`}\`(不混合无关改动)。`, |
| 538 | '- 修复完成后本步骤即结束;上层 Workflow 会重新跑 verify + review(下一轮)。', | 669 | '- 修复完成后本步骤即结束;上层 Workflow 会重新跑 verify + review(下一轮)。', |
| 539 | '', | 670 | '', |
| @@ -546,14 +677,14 @@ function fixPrompt(id, phase, issues) { | @@ -546,14 +677,14 @@ function fixPrompt(id, phase, issues) { | ||
| 546 | // ---- 测试闸(原 test-gate)---- | 677 | // ---- 测试闸(原 test-gate)---- |
| 547 | // attempt:1 = 首次跑;2 = 上轮 red 后的 flake 重试。每次 attempt 写到独立证据文件,避免 retry | 678 | // attempt:1 = 首次跑;2 = 上轮 red 后的 flake 重试。每次 attempt 写到独立证据文件,避免 retry |
| 548 | // 把首次 red 证据覆盖掉(report § ⑤ 失去 flake 信号)。 | 679 | // 把首次 red 证据覆盖掉(report § ⑤ 失去 flake 信号)。 |
| 549 | -function gatePrompt(module, phase, attempt = 1) { | 680 | +function gatePrompt(module, phase, attempt = 1, c) { |
| 550 | const fe = isFrontend(phase) | 681 | const fe = isFrontend(phase) |
| 551 | const id = module?.id ?? '<module>' | 682 | const id = module?.id ?? '<module>' |
| 552 | const phaseId = fe ? 'frontend-phase' : id | 683 | const phaseId = fe ? 'frontend-phase' : id |
| 553 | return [ | 684 | return [ |
| 554 | `# test-gate — ${fe ? '前端阶段' : `模块 ${id}`} 硬测试闸(phase=${phase}, attempt=${attempt})`, | 685 | `# test-gate — ${fe ? '前端阶段' : `模块 ${id}`} 硬测试闸(phase=${phase}, attempt=${attempt})`, |
| 555 | '', | 686 | '', |
| 556 | - featureStageContract(phase), | 687 | + featureStageContract(phase, c), |
| 557 | '', | 688 | '', |
| 558 | '## 目标', | 689 | '## 目标', |
| 559 | `打里程碑 tag 前的唯一硬测试门。**派发 Agent 子会话**跑测试,绿则通过,红则失败。**绝不**在主会话直接跑测试,红色时**绝不**跳过。`, | 690 | `打里程碑 tag 前的唯一硬测试门。**派发 Agent 子会话**跑测试,绿则通过,红则失败。**绝不**在主会话直接跑测试,红色时**绝不**跳过。`, |
| @@ -561,12 +692,17 @@ function gatePrompt(module, phase, attempt = 1) { | @@ -561,12 +692,17 @@ function gatePrompt(module, phase, attempt = 1) { | ||
| 561 | '', | 692 | '', |
| 562 | '## 命令', | 693 | '## 命令', |
| 563 | fe | 694 | fe |
| 564 | - ? `- 前端:命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 拼接(缺失则 \`pnpm test:ci && pnpm e2e:ci\`),跑 vitest + playwright(含全 FE 回归)。` | ||
| 565 | - : `- 后端:跑 \`${ROOT}/scripts/test.mjs\`(跨平台 Node 测试入口;含本模块新增 + 已合并模块回归)。`, | 695 | + ? `- 前端:命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 拼接(缺失则 \`pnpm test:ci && pnpm e2e:ci\`),跑 vitest + playwright(含全 FE 回归)。` |
| 696 | + : `- 后端:跑 \`${c.root}/scripts/test.mjs\`(跨平台 Node 测试入口;含本模块新增 + 已合并模块回归)。`, | ||
| 566 | '- 子会话只返回结构化 JSON:`{command, exit_code, passed, failed, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行含 FAIL 摘要)。', | 697 | '- 子会话只返回结构化 JSON:`{command, exit_code, passed, failed, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行含 FAIL 摘要)。', |
| 698 | + // 补 9:testGate 的 e2e(Playwright globalSetup 按固定端口冷起后端)与 Seed 同属起栈类 stage—— | ||
| 699 | + // 整段由编排层包进全局起栈互斥(stackLock,Phase D 定义);lane 测试库硬约束已由 featureStageContract 注入。 | ||
| 700 | + c.dbSchema | ||
| 701 | + ? '- **起栈互斥纪律(并行 lane)**:本 test-gate 整段在全局起栈互斥内执行——端口探测 / 残留 pid 回收**只允许**在持有该互斥的本 stage 内做;**绝不**触碰非本 stage 启动的进程(兄弟 lane 的栈不归你管)。' | ||
| 702 | + : '', | ||
| 567 | '', | 703 | '', |
| 568 | '## 证据 + commit', | 704 | '## 证据 + commit', |
| 569 | - `- 渲染证据写入 \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r${attempt}.md- `- 渲染证据写入 \`${ROOT 并 commit 到当前分支(每个 attempt 独立文件,retry 不覆盖前一次 red 证据)。`, | 705 | + `- 渲染证据写入 \`${c.root}/docs/superpowers/module-reports/${phaseId}-test-gate-r${attempt}.md+ `- 渲染证据写入 \`${c.root 并 commit 到当前分支(每个 attempt 独立文件,retry 不覆盖前一次 red 证据)。`, |
| 570 | `- 文件头注明 \`attempt: ${attempt}\` + 命令 + 时间窗口(如可从子会话拿到),便于 report § ⑤ 识别 flake。`, | 706 | `- 文件头注明 \`attempt: ${attempt}\` + 命令 + 时间窗口(如可从子会话拿到),便于 report § ⑤ 识别 flake。`, |
| 571 | '', | 707 | '', |
| 572 | '## 输出(必须符合下发的 GATE JSON schema)', | 708 | '## 输出(必须符合下发的 GATE JSON schema)', |
| @@ -588,16 +724,22 @@ function gatePrompt(module, phase, attempt = 1) { | @@ -588,16 +724,22 @@ function gatePrompt(module, phase, attempt = 1) { | ||
| 588 | // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端 / scripts/seed-demo-data.mjs / mysql 只读查询, | 724 | // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端 / scripts/seed-demo-data.mjs / mysql 只读查询, |
| 589 | // 唯一**可写** = sql/seed/ + .tmp/seed-gen/<module_id>/(跑完即弃)+ docs/superpowers/module-reports/<module_id>-seed-verify.md; | 725 | // 唯一**可写** = sql/seed/ + .tmp/seed-gen/<module_id>/(跑完即弃)+ docs/superpowers/module-reports/<module_id>-seed-verify.md; |
| 590 | // 改 backend//frontend//scripts/ 源码即越界硬停。 | 726 | // 改 backend//frontend//scripts/ 源码即越界硬停。 |
| 591 | -function seedStageContract() { | 727 | +function seedStageContract(c) { |
| 592 | return [ | 728 | return [ |
| 593 | '## 硬约束(非交互演示种子子代理)', | 729 | '## 硬约束(非交互演示种子子代理)', |
| 594 | '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', | 730 | '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', |
| 595 | '- 你的职责 = **为本模块生成演示种子(demo seed)并冷起栈真跑验证**——**不是**实现功能、**不是**改源码、**不是**改 schema。', | 731 | '- 你的职责 = **为本模块生成演示种子(demo seed)并冷起栈真跑验证**——**不是**实现功能、**不是**改源码、**不是**改 schema。', |
| 596 | '- 缺值查找顺序:`config-vars.yaml` → `docs/03-数据库设计文档.md` → `docs/01-需求清单/` 各 REQ 卡(业务语义)→ 既有 `sql/seed/*`(跨模块语义引用前序模块种子的已知主键)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', | 732 | '- 缺值查找顺序:`config-vars.yaml` → `docs/03-数据库设计文档.md` → `docs/01-需求清单/` 各 REQ 卡(业务语义)→ 既有 `sql/seed/*`(跨模块语义引用前序模块种子的已知主键)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', |
| 597 | - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下——\`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)、起后端服务(gradle bootRun 等,Flyway 在此建 schema)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(注入种子)、mysql **只读** COUNT/查询;唯一允许**写入**的路径是 \`${ROOT}/sql/seed/\`(种子文件,随 git 提交)+ \`${ROOT}/.tmp/seed-gen/<module_id>/\`(一次性 runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/<module_id>-seed-verify.md- `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下——\`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)、起后端服务(gradle bootRun 等,Flyway 在此建 schema)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(注入种子)、mysql **只读** COUNT/查询;唯一允许**写入**的路径是 \`${ROOT}/sql/seed/\`(种子文件,随 git 提交)+ \`${ROOT}/.tmp/seed-gen/<module_id>/\`(一次性 runner,跑完即弃)+ 证据报告 \`${ROOT。`, | 733 | + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下——\`node ${c.root}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)、起后端服务(gradle bootRun 等,Flyway 在此建 schema)、\`node ${c.root}/scripts/seed-demo-data.mjs\`(注入种子)、mysql **只读** COUNT/查询;唯一允许**写入**的路径是 \`${c.root}/sql/seed/\`(种子文件,随 git 提交)+ \`${c.root}/.tmp/seed-gen/<module_id>/\`(一次性 runner,跑完即弃)+ 证据报告 \`${c.root}/docs/superpowers/module-reports/<module_id>-seed-verify.md+ `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下——\`node ${c.root}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)、起后端服务(gradle bootRun 等,Flyway 在此建 schema)、\`node ${c.root}/scripts/seed-demo-data.mjs\`(注入种子)、mysql **只读** COUNT/查询;唯一允许**写入**的路径是 \`${c.root}/sql/seed/\`(种子文件,随 git 提交)+ \`${c.root}/.tmp/seed-gen/<module_id>/\`(一次性 runner,跑完即弃)+ 证据报告 \`${c.root。`, |
| 598 | `- **越界硬停**:**绝不**编辑 \`backend/\` / \`frontend/\` / \`scripts/\` 下的任何源码文件(只许**运行** scripts/setup-test-db.mjs 与 scripts/seed-demo-data.mjs,不许改它们)。区分「运行 backend 服务 / 运行脚本」(允许)与「写 backend 实现 / 改脚本」(越界)。命中越界即以 \`status:halt\` 写清阻塞点结束。`, | 734 | `- **越界硬停**:**绝不**编辑 \`backend/\` / \`frontend/\` / \`scripts/\` 下的任何源码文件(只许**运行** scripts/setup-test-db.mjs 与 scripts/seed-demo-data.mjs,不许改它们)。区分「运行 backend 服务 / 运行脚本」(允许)与「写 backend 实现 / 改脚本」(越界)。命中越界即以 \`status:halt\` 写清阻塞点结束。`, |
| 599 | '- **确定性红线(关键)**:种子值一律**显式主键**(1000–9999 区间)+ **固定历史日期**(写死字面量,如 `2024-03-15`),**绝不**依赖时间戳 / `NOW()` / 随机数 / 自增主键的隐式取值。', | 735 | '- **确定性红线(关键)**:种子值一律**显式主键**(1000–9999 区间)+ **固定历史日期**(写死字面量,如 `2024-03-15`),**绝不**依赖时间戳 / `NOW()` / 随机数 / 自增主键的隐式取值。', |
| 600 | '- **区间隔离红线**:演示数据值**绝不含 `_S<数字>` 样式编码串**(如 `CUST_NAME_S001`)——该样式预留给行为门 sentinel;数值主键固定落 1000–9999(1–999=初始数据 / ≥100000=sentinel)。', | 736 | '- **区间隔离红线**:演示数据值**绝不含 `_S<数字>` 样式编码串**(如 `CUST_NAME_S001`)——该样式预留给行为门 sentinel;数值主键固定落 1000–9999(1–999=初始数据 / ≥100000=sentinel)。', |
| 737 | + // lane 注入(附录补 10):Seed 是不套 featureStageContract 的"第三类 stage",必须单独注入同一条 | ||
| 738 | + // 硬约束——Seed 全链恰是 DROP+CREATE / 起栈 / 灌种子的大户,漏注入会在并行 lane 下直接 DROP 共享库。 | ||
| 739 | + // 与 marker 兜底(补 7,lane 根 .tmp/erp-lane-db)自洽:lane 内一切脚本确定性指向 lane 库。 | ||
| 740 | + ...(c.dbSchema ? [ | ||
| 741 | + `- **并行 lane 测试库(硬约束)**:本模块运行在并行 lane。Seed 全链——\`node scripts/setup-test-db.mjs\` / 起后端(gradle bootRun 等)/ \`node scripts/seed-demo-data.mjs\` / mysql COUNT 对账——的**全部**命令(含派发给子会话的)必须前置环境变量 \`ERP_TEST_DB_SCHEMA=${c.dbSchema}\`:成品字符串照抄使用,**绝不**自行拼接 / 改名 / 省略(bootRun 经 application*.yml 的 \`\${ERP_TEST_DB_SCHEMA:...}\` 占位吃到同一 lane 库;COUNT 对账必须连 lane 库查询)。`, | ||
| 742 | + ] : []), | ||
| 601 | '- 红线:**绝不**伪造验证通过;**绝不**留 `TBD` / `TODO` / `【人工填写:】`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', | 743 | '- 红线:**绝不**伪造验证通过;**绝不**留 `TBD` / `TODO` / `【人工填写:】`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', |
| 602 | '- 证据报告 / 注释 / 提示**使用中文**;SQL / 标识符 / 表名可用英文(受控 `[A-Za-z0-9_]` 格式)。', | 744 | '- 证据报告 / 注释 / 提示**使用中文**;SQL / 标识符 / 表名可用英文(受控 `[A-Za-z0-9_]` 格式)。', |
| 603 | ].join('\n') | 745 | ].join('\n') |
| @@ -605,28 +747,43 @@ function seedStageContract() { | @@ -605,28 +747,43 @@ function seedStageContract() { | ||
| 605 | 747 | ||
| 606 | // seedGenPrompt:单模块演示种子生成 + 冷起栈真跑验证的完整流水线提示。 | 748 | // seedGenPrompt:单模块演示种子生成 + 冷起栈真跑验证的完整流水线提示。 |
| 607 | // module:本后端模块(含 id);本 stage 在该模块 testGate green 之后跑(schema 含 tdd 新增 V<n> 已终态全绿)。 | 749 | // module:本后端模块(含 id);本 stage 在该模块 testGate green 之后跑(schema 含 tdd 新增 V<n> 已终态全绿)。 |
| 608 | -function seedGenPrompt(module) { | 750 | +function seedGenPrompt(module, c) { |
| 609 | const id = module?.id ?? '<module>' | 751 | const id = module?.id ?? '<module>' |
| 610 | - const tmpDir = `${ROOT}/.tmp/seed-gen/${id}` | 752 | + const tmpDir = `${c.root}/.tmp/seed-gen/${id}` |
| 611 | const evidence = `docs/superpowers/module-reports/${id}-seed-verify.md` | 753 | const evidence = `docs/superpowers/module-reports/${id}-seed-verify.md` |
| 612 | return [ | 754 | return [ |
| 613 | `# seed — 演示种子生成 + 冷起栈真跑验证(模块 ${id})`, | 755 | `# seed — 演示种子生成 + 冷起栈真跑验证(模块 ${id})`, |
| 614 | '', | 756 | '', |
| 615 | - seedStageContract(), | 757 | + seedStageContract(c), |
| 616 | '', | 758 | '', |
| 617 | '## 目标', | 759 | '## 目标', |
| 618 | - `为本模块 \`${id}\` 生成**演示假数据(demo seed)**并冷起栈真跑验证:生成 → \`node ${ROOT}/scripts/setup-test-db.mjs\` → 起后端(Flyway 建 schema)→ \`node ${ROOT}/scripts/seed-demo-data.mjs- `为本模块 \`${id}\` 生成**演示假数据(demo seed)**并冷起栈真跑验证:生成 → \`node ${ROOT}/scripts/setup-test-db.mjs\` → 起后端(Flyway 建 schema)→ \`node ${ROOT → mysql 只读 COUNT 对账。`, | 760 | + `为本模块 \`${id}\` 生成**演示假数据(demo seed)**并冷起栈真跑验证:生成 → \`node ${c.root}/scripts/setup-test-db.mjs\` → 起后端(Flyway 建 schema)→ \`node ${c.root}/scripts/seed-demo-data.mjs+ `为本模块 \`${id}\` 生成**演示假数据(demo seed)**并冷起栈真跑验证:生成 → \`node ${c.root}/scripts/setup-test-db.mjs\` → 起后端(Flyway 建 schema)→ \`node ${c.root → mysql 只读 COUNT 对账。`, |
| 619 | '种子产物随 git 提交(不保证「存活」,保证「随时可复现」——三处 DROP+CREATE 各在自己时序里固定重注入)。', | 761 | '种子产物随 git 提交(不保证「存活」,保证「随时可复现」——三处 DROP+CREATE 各在自己时序里固定重注入)。', |
| 620 | '', | 762 | '', |
| 621 | '## 输入', | 763 | '## 输入', |
| 622 | - `- \`${ROOT}/docs/03-数据库设计文档.md\`:本模块各表结构(列 / 类型 / enum 值域 / 语义引用关系 / NOT NULL / UNIQUE 约束)。`, | ||
| 623 | - `- \`${ROOT}/docs/01-需求清单/<module>/\` 本模块 REQ 卡:业务语义(让假数据有真实感、符合业务取值)。`, | ||
| 624 | - `- 既有 \`${ROOT}/sql/seed/*.sql\`:跨模块语义引用前序模块种子的**已知确定性主键**(你的语义引用列必须指向这些已存在的主键,不可悬空)。`, | ||
| 625 | - `- \`${ROOT}/config-vars.yaml\`:database 段凭据(seed-demo-data.mjs / setup-test-db.mjs 自行读取,你只需确保起栈参数一致)。`, | 764 | + `- \`${c.root}/docs/03-数据库设计文档.md\`:本模块各表结构(列 / 类型 / enum 值域 / 语义引用关系 / NOT NULL / UNIQUE 约束)。`, |
| 765 | + `- \`${c.root}/docs/01-需求清单/<module>/\` 本模块 REQ 卡:业务语义(让假数据有真实感、符合业务取值)。`, | ||
| 766 | + `- 既有 \`${c.root}/sql/seed/*.sql\`:跨模块语义引用前序模块种子的**已知确定性主键**(你的语义引用列必须指向这些已存在的主键,不可悬空)。`, | ||
| 767 | + `- \`${c.root}/config-vars.yaml\`:database 段凭据(seed-demo-data.mjs / setup-test-db.mjs 自行读取,你只需确保起栈参数一致)。`, | ||
| 626 | '', | 768 | '', |
| 627 | '## 幂等(resume 安全)', | 769 | '## 幂等(resume 安全)', |
| 628 | - `- 用 Glob 查 \`${ROOT}/sql/seed/*__${id}.sql- `- 用 Glob 查 \`${ROOT。**已存在** → **Edit 复用该文件**(保留原 - `- 用 Glob 查 \`${ROOTNN- `- 用 Glob 查 \`${ROOT 序号,不另起新文件);按需补齐/修正内容。`, | 770 | + `- 用 Glob 查 \`${c.root}/sql/seed/*__${id}.sql+ `- 用 Glob 查 \`${c.root。**已存在** → **Edit 复用该文件**(保留原 + `- 用 Glob 查 \`${c.rootNN+ `- 用 Glob 查 \`${c.root 序号,不另起新文件);按需补齐/修正内容。`, |
| 629 | `- **不存在** → 新建 \`sql/seed/<NN>__${id}.sql\`,其中 \`NN\` = 既有 \`sql/seed/*.sql\` 文件名最大序号 + 1(两位补零,如既有最大为 \`03\` → 本文件用 \`04\`;无任何既有文件 → \`01\`)。`, | 771 | `- **不存在** → 新建 \`sql/seed/<NN>__${id}.sql\`,其中 \`NN\` = 既有 \`sql/seed/*.sql\` 文件名最大序号 + 1(两位补零,如既有最大为 \`03\` → 本文件用 \`04\`;无任何既有文件 → \`01\`)。`, |
| 772 | + // 并行 lane 附注(仅 lane 模式渲染,串行输出一字不变)。 | ||
| 773 | + // 补 9:冷起栈资源(config-vars 固定端口 + 进程树)全局唯一——本 stage 整段由编排层包在全局起栈 | ||
| 774 | + // 互斥(stackLock,Phase D 定义)内串行执行;"按既知端口回收残留 pid"若不在互斥内做,会把兄弟 lane | ||
| 775 | + // 正在验证的后端当残留 kill 掉(≥2 宽波次必然踩中的确定性互杀,非小概率 race)。 | ||
| 776 | + ...(c.dbSchema ? [ | ||
| 777 | + '', | ||
| 778 | + '## 并行 lane 附注', | ||
| 779 | + '- **起栈互斥纪律**:本 Seed stage 整段在全局起栈互斥内执行——端口探测 / 按既知端口回收残留 pid **只允许**在持有该互斥的本 stage 内做;**绝不**在 stage 外、或对非本 stage 启动的进程做回收。', | ||
| 780 | + ] : []), | ||
| 781 | + // 补 17:两个并行模块从同一基点各算 NN=max+1 必产同号文件——豁免必须显式落进 prompt,否则与上文 | ||
| 782 | + // "NN = 最大序号 + 1"字面约定静默冲突,实现者会各自发挥。(NN, module_id) 才是唯一键:文件名含 | ||
| 783 | + // module id(git 无冲突),_demo_seed_history 按完整文件名记账、注入按文件名字典序应用(现状已满足)。 | ||
| 784 | + ...(c.vBase > 0 ? [ | ||
| 785 | + '- **并行 lane 间允许同 NN**:NN 仅表观排序,`(NN, module_id)` 才是唯一键;同波次模块无 seed 依赖边、互不引用,注入按完整文件名字典序应用。发现兄弟分支存在同 NN 文件**不算冲突,不要改号**。', | ||
| 786 | + ] : []), | ||
| 630 | '', | 787 | '', |
| 631 | '## 生成规则', | 788 | '## 生成规则', |
| 632 | '- **按语义引用有序(先被引用方后引用方)**:同一文件内 INSERT 先被引用方后引用方;跨模块语义引用列指向既有 `sql/seed/*` 中前序模块种子的已知主键。', | 789 | '- **按语义引用有序(先被引用方后引用方)**:同一文件内 INSERT 先被引用方后引用方;跨模块语义引用列指向既有 `sql/seed/*` 中前序模块种子的已知主键。', |
| @@ -641,9 +798,9 @@ function seedGenPrompt(module) { | @@ -641,9 +798,9 @@ function seedGenPrompt(module) { | ||
| 641 | '## 运行验证(写一次性 runner,仿行为门冷起栈纪律的简化版)', | 798 | '## 运行验证(写一次性 runner,仿行为门冷起栈纪律的简化版)', |
| 642 | `- **入口清目录**:先用确定性、跨平台方式重建 \`${tmpDir}/\`(\`fs.rmSync(tmpDir,{recursive:true,force:true})\` 后 \`fs.mkdirSync(tmpDir,{recursive:true})\`),仅限该受控路径,绝不删其它路径。`, | 799 | `- **入口清目录**:先用确定性、跨平台方式重建 \`${tmpDir}/\`(\`fs.rmSync(tmpDir,{recursive:true,force:true})\` 后 \`fs.mkdirSync(tmpDir,{recursive:true})\`),仅限该受控路径,绝不删其它路径。`, |
| 643 | `- 在 \`${tmpDir}/\` 写一次性 runner \`run.mjs\`,依序:`, | 800 | `- 在 \`${tmpDir}/\` 写一次性 runner \`run.mjs\`,依序:`, |
| 644 | - ` 1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。`, | ||
| 645 | - ` 2) **起后端**:从 \`${ROOT}/config-vars.yaml\` 取端口;起栈前先探测端口占用并按 \`${tmpDir}/*.pid\` / 既知端口回收上一次残留 pid;spawn 到后台进程树 + 轮询健康端点(\`/actuator/health\` 或登录端点 200)就绪(Flyway 在此 apply 建 schema)。`, | ||
| 646 | - ` 3) \`node ${ROOT}/scripts/seed-demo-data.mjs\`(注入种子;幂等账本 \`_demo_seed_history\` 自动跳过已应用文件)。`, | 801 | + ` 1) \`node ${c.root}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。`, |
| 802 | + ` 2) **起后端**:从 \`${c.root}/config-vars.yaml\` 取端口;起栈前先探测端口占用并按 \`${tmpDir}/*.pid\` / 既知端口回收上一次残留 pid;spawn 到后台进程树 + 轮询健康端点(\`/actuator/health\` 或登录端点 200)就绪(Flyway 在此 apply 建 schema)。`, | ||
| 803 | + ` 3) \`node ${c.root}/scripts/seed-demo-data.mjs\`(注入种子;幂等账本 \`_demo_seed_history\` 自动跳过已应用文件)。`, | ||
| 647 | ' 4) **mysql 只读 COUNT 对账**:对本模块种子涉及的**每张表**,跑 `SELECT COUNT(*) ... WHERE <主键列> BETWEEN 1000 AND 9999`(**只数演示种子区间**——后端启动可能把 admin_init 等初始数据 bootstrap 进共表,其键落 1–999,不计入 expect),与「全部 `sql/seed/*.sql` 文件头 `-- expect: <table>=<rows>` 之和」逐表比对(同一张表可能被多个种子文件插入,必须求和后再比)。', | 804 | ' 4) **mysql 只读 COUNT 对账**:对本模块种子涉及的**每张表**,跑 `SELECT COUNT(*) ... WHERE <主键列> BETWEEN 1000 AND 9999`(**只数演示种子区间**——后端启动可能把 admin_init 等初始数据 bootstrap 进共表,其键落 1–999,不计入 expect),与「全部 `sql/seed/*.sql` 文件头 `-- expect: <table>=<rows>` 之和」逐表比对(同一张表可能被多个种子文件插入,必须求和后再比)。', |
| 648 | ' - `finally` **硬要求 kill 本 stage 起的全部子进程**(绝不让 gradle bootRun 挂死会话)。', | 805 | ' - `finally` **硬要求 kill 本 stage 起的全部子进程**(绝不让 gradle bootRun 挂死会话)。', |
| 649 | '- **失败归类(reason 里必须分清)**:', | 806 | '- **失败归类(reason 里必须分清)**:', |
| @@ -655,7 +812,7 @@ function seedGenPrompt(module) { | @@ -655,7 +812,7 @@ function seedGenPrompt(module) { | ||
| 655 | '- 若验证失败(环境类或数据类)→ 证据**头部用红字标注根因**(区分环境类 vs 数据类)。', | 812 | '- 若验证失败(环境类或数据类)→ 证据**头部用红字标注根因**(区分环境类 vs 数据类)。', |
| 656 | '', | 813 | '', |
| 657 | commitBlock(`sql/seed ${evidence}`, `chore(seed:${id}): 演示种子数据`, | 814 | commitBlock(`sql/seed ${evidence}`, `chore(seed:${id}): 演示种子数据`, |
| 658 | - '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的种子/证据路径)。'), | 815 | + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的种子/证据路径)。', c), |
| 659 | '', | 816 | '', |
| 660 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', | 817 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', |
| 661 | `- 成功(含验证 COUNT 全部对账通过):\`{ "status": "ok", "artifactPath": "sql/seed/<NN>__${id}.sql", "summary": "<种子表数 / 总行数 / 验证结论 ≤ 200 字>" }\`。`, | 818 | `- 成功(含验证 COUNT 全部对账通过):\`{ "status": "ok", "artifactPath": "sql/seed/<NN>__${id}.sql", "summary": "<种子表数 / 总行数 / 验证结论 ≤ 200 字>" }\`。`, |
| @@ -676,14 +833,17 @@ function seedGenPrompt(module) { | @@ -676,14 +833,17 @@ function seedGenPrompt(module) { | ||
| 676 | // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符; | 833 | // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符; |
| 677 | // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright, | 834 | // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright, |
| 678 | // 唯一**可写** = .tmp/behavior-gate/frontend-phase/r<behaviorRound>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 | 835 | // 唯一**可写** = .tmp/behavior-gate/frontend-phase/r<behaviorRound>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 |
| 679 | -function behaviorGateContract() { | 836 | +// lane 注入豁免(附录补 10 审计口径):行为门只在 frontend-phase 跑——其 deps 恒为全部后端模块, |
| 837 | +// 终波恒宽 1、主根执行(不建 lane、c.dbSchema 恒空),prompt 含 setup-test-db/seed-demo-data 字样 | ||
| 838 | +// 但天然不需要 ERP_TEST_DB_SCHEMA 注入分支。 | ||
| 839 | +function behaviorGateContract(c) { | ||
| 680 | return [ | 840 | return [ |
| 681 | '## 硬约束(非交互行为验收子代理)', | 841 | '## 硬约束(非交互行为验收子代理)', |
| 682 | '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', | 842 | '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', |
| 683 | '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「每个 FE 的每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', | 843 | '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「每个 FE 的每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', |
| 684 | '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', | 844 | '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', |
| 685 | - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(gradle bootRun 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate/frontend-phase/r<behaviorRound>/\`(种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r<behaviorRound>-a<attempt>.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/module-reports/assets/...\`)。`, | ||
| 686 | - `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, | 845 | + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${c.root}/scripts/setup-test-db.mjs\`、起后端服务(gradle bootRun 等)、\`node ${c.root}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${c.root}/.tmp/behavior-gate/frontend-phase/r<behaviorRound>/\`(种子 SQL/runner,跑完即弃)+ 证据报告 \`${c.root}/docs/superpowers/module-reports/frontend-phase-behavior-r<behaviorRound>-a<attempt>.md\` + 其 assets(截图归档到 \`${c.root}/docs/superpowers/module-reports/assets/...\`)。`, |
| 846 | + `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${c.root}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, | ||
| 687 | '- **全量终态前提(关键)**:本门跑在**全部 FE 已实现并通过静态 review 之后**——`frontend/` 不应再有未实现路由 / FeStub 占位。某路由仍渲染 `data-fe-stub` 占位 → 这是硬缺陷(tdd 漏做占位替换),归 `interactionFailures[kind="no-observable-effect"]`,locator 指向 router 文件该路由 import 行,detail 写明「路由仍指向 FeStub 占位」。**断言作用域 = 全部 FE spec 的 `## 行为验收作用域` 小节并集**;白名单外控件记证据不入断言集。', | 847 | '- **全量终态前提(关键)**:本门跑在**全部 FE 已实现并通过静态 review 之后**——`frontend/` 不应再有未实现路由 / FeStub 占位。某路由仍渲染 `data-fe-stub` 占位 → 这是硬缺陷(tdd 漏做占位替换),归 `interactionFailures[kind="no-observable-effect"]`,locator 指向 router 文件该路由 import 行,detail 写明「路由仍指向 FeStub 占位」。**断言作用域 = 全部 FE spec 的 `## 行为验收作用域` 小节并集**;白名单外控件记证据不入断言集。', |
| 688 | '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', | 848 | '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', |
| 689 | '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。', | 849 | '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。', |
| @@ -695,33 +855,33 @@ function behaviorGateContract() { | @@ -695,33 +855,33 @@ function behaviorGateContract() { | ||
| 695 | // feItems:本前端阶段全部 FE-NN(作用域聚合的清单真值,来自 Router frontend-phase 模块); | 855 | // feItems:本前端阶段全部 FE-NN(作用域聚合的清单真值,来自 Router frontend-phase 模块); |
| 696 | // behaviorRound:阶段门内的行为 fix 轮(1..BEHAVIOR_STAGE_MAX);attempt:本轮内环境 race 重试序号(1..)。 | 856 | // behaviorRound:阶段门内的行为 fix 轮(1..BEHAVIOR_STAGE_MAX);attempt:本轮内环境 race 重试序号(1..)。 |
| 697 | // 每 (behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。 | 857 | // 每 (behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。 |
| 698 | -function behaviorGatePrompt(feItems, behaviorRound, attempt) { | 858 | +function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { |
| 699 | const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(调用方未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' | 859 | const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(调用方未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' |
| 700 | - const tmpDir = `${ROOT}/.tmp/behavior-gate/frontend-phase/r${behaviorRound}` | 860 | + const tmpDir = `${c.root}/.tmp/behavior-gate/frontend-phase/r${behaviorRound}` |
| 701 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md` | 861 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md` |
| 702 | return [ | 862 | return [ |
| 703 | `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`, | 863 | `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`, |
| 704 | '', | 864 | '', |
| 705 | - behaviorGateContract(), | 865 | + behaviorGateContract(c), |
| 706 | '', | 866 | '', |
| 707 | '## 目标', | 867 | '## 目标', |
| 708 | '用真实全栈运行证明**全部 FE** 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。整个前端阶段只跑这一道行为门(featureLoop 全部 FE 已过静态 review)。', | 868 | '用真实全栈运行证明**全部 FE** 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。整个前端阶段只跑这一道行为门(featureLoop 全部 FE 已过静态 review)。', |
| 709 | '单个子会话内**收敛完成**:冷起栈 → 逐路由枚举(全 FE 作用域并集)+ 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。', | 869 | '单个子会话内**收敛完成**:冷起栈 → 逐路由枚举(全 FE 作用域并集)+ 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。', |
| 710 | `- 本阶段 FE 清单:${feList}。`, | 870 | `- 本阶段 FE 清单:${feList}。`, |
| 711 | - `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT}/docs/superpowers/specs/<date>-<FE-NN>.md- `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT,同一 FE 多份取最新日期)头部的 - `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT## 行为验收作用域- `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT 小节(- `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT关联路由:- `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT + - `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT负责控件白名单:- `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT)。先逐 FE Read 取出并**聚合为并集**(路由去重、逐路由标注归属 FE);某 FE 缺 spec 或缺该小节 → 记 - `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOTcoverageGaps[reason="scope-missing", page="<FE-NN>"]- `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT(该 FE 路由不计入分母,**绝不**静默跳过)。`, | 871 | + `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root}/docs/superpowers/specs/<date>-<FE-NN>.md+ `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root,同一 FE 多份取最新日期)头部的 + `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root## 行为验收作用域+ `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root 小节(+ `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root关联路由:+ `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root + + `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root负责控件白名单:+ `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root)。先逐 FE Read 取出并**聚合为并集**(路由去重、逐路由标注归属 FE);某 FE 缺 spec 或缺该小节 → 记 + `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.rootcoverageGaps[reason="scope-missing", page="<FE-NN>"]+ `- 断言作用域真值 = **每个 FE** 的 spec(\`${c.root(该 FE 路由不计入分母,**绝不**静默跳过)。`, |
| 712 | behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '', | 872 | behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '', |
| 713 | '', | 873 | '', |
| 714 | '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', | 874 | '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', |
| 715 | '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', | 875 | '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', |
| 716 | `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 | 876 | `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 |
| 717 | - ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/- ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT 目录(清掉历史残留 runner/种子),再新建本轮子目录 - ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT${tmpDir}/- ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT。` | 877 | + ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root}/.tmp/behavior-gate/frontend-phase/+ ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root 目录(清掉历史残留 runner/种子),再新建本轮子目录 + ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root${tmpDir}/+ ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root。` |
| 718 | : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, | 878 | : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, |
| 719 | `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, | 879 | `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, |
| 720 | `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`, | 880 | `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`, |
| 721 | - `- \`${ROOT}/.tmp/behavior-gate/- `- \`${ROOT(含子目录)已被仓库 - `- \`${ROOT.gitignore- `- \`${ROOT 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, | 881 | + `- \`${c.root}/.tmp/behavior-gate/+ `- \`${c.root(含子目录)已被仓库 + `- \`${c.root.gitignore+ `- \`${c.root 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, |
| 722 | '', | 882 | '', |
| 723 | '## step0 探测 + build 归因', | 883 | '## step0 探测 + build 归因', |
| 724 | - `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/config-vars.yaml- `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT。`, | 884 | + `- 读 \`${c.root}/docs/04-技术规范.md § 零\` + \`${c.root}/frontend/package.json\` + \`${c.root}/config-vars.yaml+ `- 读 \`${c.root}/docs/04-技术规范.md § 零\` + \`${c.root}/frontend/package.json\` + \`${c.root。`, |
| 725 | '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。', | 885 | '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。', |
| 726 | '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——全部 FE 已实现,**没有**「兄弟未实现」豁免:', | 886 | '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——全部 FE 已实现,**没有**「兄弟未实现」豁免:', |
| 727 | ' - 根因落在 `frontend/` 源码且可定位到文件 → 真构建 bug → 归 `interactionFailures[kind="js-error"]`(locator=根因文件路径,可转 must-fix 喂 fix)。', | 887 | ' - 根因落在 `frontend/` 源码且可定位到文件 → 真构建 bug → 归 `interactionFailures[kind="js-error"]`(locator=根因文件路径,可转 must-fix 喂 fix)。', |
| @@ -730,14 +890,14 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | @@ -730,14 +890,14 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | ||
| 730 | '', | 890 | '', |
| 731 | '## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)', | 891 | '## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)', |
| 732 | '- 分母来源 = 全部 FE spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。', | 892 | '- 分母来源 = 全部 FE spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。', |
| 733 | - `- 与 \`${ROOT}/frontend/- `- 与 \`${ROOT router 配置对账:FE 作用域声明但 router 缺失的路由 → - `- 与 \`${ROOTcoverageGaps[reason="unreachable-no-route"]- `- 与 \`${ROOT;router 声明但不属任何 FE 作用域的路由记证据(不入分母、不断言)。`, | 893 | + `- 与 \`${c.root}/frontend/+ `- 与 \`${c.root router 配置对账:FE 作用域声明但 router 缺失的路由 → + `- 与 \`${c.rootcoverageGaps[reason="unreachable-no-route"]+ `- 与 \`${c.root;router 声明但不属任何 FE 作用域的路由记证据(不入分母、不断言)。`, |
| 734 | '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。', | 894 | '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。', |
| 735 | '- 带参动态路由用**种子已知主键**实例化(可用**演示种子已知主键**(1000–9999)或 **sentinel 主键**(≥100000));无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', | 895 | '- 带参动态路由用**种子已知主键**实例化(可用**演示种子已知主键**(1000–9999)或 **sentinel 主键**(≥100000));无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', |
| 736 | '', | 896 | '', |
| 737 | '## step2 起栈五段严格时序(schema 由 Flyway 在后端启动时才建)', | 897 | '## step2 起栈五段严格时序(schema 由 Flyway 在后端启动时才建)', |
| 738 | - `1) \`node ${ROOT}/scripts/setup-test-db.mjs- `1) \`node ${ROOT(DROP+CREATE 空库)。DROP 前按 - `1) \`node ${ROOT${tmpDir}/*.pid- `1) \`node ${ROOT / 既知端口优雅回收残留进程;脚本失败按普通 - `1) \`node ${ROOTstack-not-ready- `1) \`node ${ROOT 处理。`, | 898 | + `1) \`node ${c.root}/scripts/setup-test-db.mjs+ `1) \`node ${c.root(DROP+CREATE 空库)。DROP 前按 + `1) \`node ${c.root${tmpDir}/*.pid+ `1) \`node ${c.root / 既知端口优雅回收残留进程;脚本失败按普通 + `1) \`node ${c.rootstack-not-ready+ `1) \`node ${c.root 处理。`, |
| 739 | '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', | 899 | '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', |
| 740 | - `3) **注入演示种子**:\`node ${ROOT}/scripts/seed-demo-data.mjs- `3) **注入演示种子**:\`node ${ROOT(幂等账本 - `3) **注入演示种子**:\`node ${ROOT_demo_seed_history- `3) **注入演示种子**:\`node ${ROOT 自动跳过已应用文件,把 - `3) **注入演示种子**:\`node ${ROOTsql/seed/*.sql- `3) **注入演示种子**:\`node ${ROOT 演示数据注入空库)。失败 → - `3) **注入演示种子**:\`node ${ROOTenvError.kind="seed-error"- `3) **注入演示种子**:\`node ${ROOT + 结构化根因(缺列 / 撞唯一键 / enum 越界 / 引用序错 / 类型截断 / schema 未初始化),**不**混进交互 RED。`, | 900 | + `3) **注入演示种子**:\`node ${c.root}/scripts/seed-demo-data.mjs+ `3) **注入演示种子**:\`node ${c.root(幂等账本 + `3) **注入演示种子**:\`node ${c.root_demo_seed_history+ `3) **注入演示种子**:\`node ${c.root 自动跳过已应用文件,把 + `3) **注入演示种子**:\`node ${c.rootsql/seed/*.sql+ `3) **注入演示种子**:\`node ${c.root 演示数据注入空库)。失败 → + `3) **注入演示种子**:\`node ${c.rootenvError.kind="seed-error"+ `3) **注入演示种子**:\`node ${c.root + 结构化根因(缺列 / 撞唯一键 / enum 越界 / 引用序错 / 类型截断 / schema 未初始化),**不**混进交互 RED。`, |
| 741 | '4) **此时才跑 sentinel 种子**:按 `docs/03-数据库设计文档.md` 派生 **按语义引用有序的 INSERT** sentinel 种子(先被引用方后引用方;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)。失败 → `envError.kind="seed-error"` + 结构化根因,**不**混进交互 RED。', | 901 | '4) **此时才跑 sentinel 种子**:按 `docs/03-数据库设计文档.md` 派生 **按语义引用有序的 INSERT** sentinel 种子(先被引用方后引用方;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)。失败 → `envError.kind="seed-error"` + 结构化根因,**不**混进交互 RED。', |
| 742 | ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——数值主键**一律 ≥100000**(固定区间,不再动态扫描既有键:初始数据 1–999 / 演示种子 1000–9999 已由区间约定隔离,sentinel 落 ≥100000 天然不冲突);字符串列**仍逐字段唯一编码**(`_S<NNN>` 样式,如 `CUST_NAME_S001`,抓绑错字段——演示数据已被禁用该样式,故 sentinel 独占)+ 行序号保 UNIQUE;enum 列从 docs/03 值域取并标注。断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。', | 902 | ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——数值主键**一律 ≥100000**(固定区间,不再动态扫描既有键:初始数据 1–999 / 演示种子 1000–9999 已由区间约定隔离,sentinel 落 ≥100000 天然不冲突);字符串列**仍逐字段唯一编码**(`_S<NNN>` 样式,如 `CUST_NAME_S001`,抓绑错字段——演示数据已被禁用该样式,故 sentinel 独占)+ 行序号保 UNIQUE;enum 列从 docs/03 值域取并标注。断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。', |
| 743 | '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', | 903 | '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', |
| @@ -765,7 +925,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | @@ -765,7 +925,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | ||
| 765 | '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', | 925 | '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', |
| 766 | '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 green)。', | 926 | '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 green)。', |
| 767 | '- **样式层(客观断言:颜色 token 比对 + layout sanity)**。断言作用域 = **白名单控件及其直接容器 + spec/prototype 点名区域**;组件库深层内部元素**不查**,只查可见元素:', | 927 | '- **样式层(客观断言:颜色 token 比对 + layout sanity)**。断言作用域 = **白名单控件及其直接容器 + spec/prototype 点名区域**;组件库深层内部元素**不查**,只查可见元素:', |
| 768 | - ` - **色值基准确定性派生**:读 \`${ROOT}/src/styles/tokens.css- ` - **色值基准确定性派生**:读 \`${ROOT 解析全部 - ` - **色值基准确定性派生**:读 \`${ROOT--color-*- ` - **色值基准确定性派生**:读 \`${ROOT,用探针元素 getComputedStyle 把任意色值格式归一化为 canonical rgb 集合;被检元素的渲染值(- ` - **色值基准确定性派生**:读 \`${ROOTcolor- ` - **色值基准确定性派生**:读 \`${ROOT / - ` - **色值基准确定性派生**:读 \`${ROOTbackground-color- ` - **色值基准确定性派生**:读 \`${ROOT / - ` - **色值基准确定性派生**:读 \`${ROOTborder-color- ` - **色值基准确定性派生**:读 \`${ROOT)同法归一化后比对。`, | 928 | + ` - **色值基准确定性派生**:读 \`${c.root}/src/styles/tokens.css+ ` - **色值基准确定性派生**:读 \`${c.root 解析全部 + ` - **色值基准确定性派生**:读 \`${c.root--color-*+ ` - **色值基准确定性派生**:读 \`${c.root,用探针元素 getComputedStyle 把任意色值格式归一化为 canonical rgb 集合;被检元素的渲染值(+ ` - **色值基准确定性派生**:读 \`${c.rootcolor+ ` - **色值基准确定性派生**:读 \`${c.root / + ` - **色值基准确定性派生**:读 \`${c.rootbackground-color+ ` - **色值基准确定性派生**:读 \`${c.root / + ` - **色值基准确定性派生**:读 \`${c.rootborder-color+ ` - **色值基准确定性派生**:读 \`${c.root)同法归一化后比对。`, |
| 769 | ' - **颜色断言**:渲染色 ∉ token 集合 → `styleIssues[kind="non-token-color"]`;spec「Design Tokens 引用清单」点名了具体 token 的元素,渲染值 ≠ 该 token 解析值 → `styleIssues[kind="token-mismatch"]`。半透明混合 / 无法归一化的值 → **不入** styleIssues,记 `decisions[]`(宁漏勿误)。', | 929 | ' - **颜色断言**:渲染色 ∉ token 集合 → `styleIssues[kind="non-token-color"]`;spec「Design Tokens 引用清单」点名了具体 token 的元素,渲染值 ≠ 该 token 解析值 → `styleIssues[kind="token-mismatch"]`。半透明混合 / 无法归一化的值 → **不入** styleIssues,记 `decisions[]`(宁漏勿误)。', |
| 770 | ' - **几何断言(layout sanity)**:每路由 `scrollWidth > clientWidth + 1` → `styleIssues[kind="horizontal-overflow"]`(locator=该路由 view 组件文件);白名单控件两两 boundingBox 交叠 >4px² 且双方可见非 inert → `overlap`;预期可见控件 box 为零 → `zero-size`;`scrollIntoViewIfNeeded` 后仍不在视口 → `offscreen`。', | 930 | ' - **几何断言(layout sanity)**:每路由 `scrollWidth > clientWidth + 1` → `styleIssues[kind="horizontal-overflow"]`(locator=该路由 view 组件文件);白名单控件两两 boundingBox 交叠 >4px² 且双方可见非 inert → `overlap`;预期可见控件 box 为零 → `zero-size`;`scrollIntoViewIfNeeded` 后仍不在视口 → `offscreen`。', |
| 771 | ' - **视口**:默认用 Playwright 默认视口;prototype 明确声明 viewport 时用之并记 `decisions[]`。', | 931 | ' - **视口**:默认用 Playwright 默认视口;prototype 明确声明 viewport 时用之并记 `decisions[]`。', |
| @@ -779,7 +939,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | @@ -779,7 +939,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | ||
| 779 | `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/module-reports/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, | 939 | `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/module-reports/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, |
| 780 | `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`, | 940 | `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`, |
| 781 | commitBlock(`${evidence} docs/superpowers/module-reports/assets`, | 941 | commitBlock(`${evidence} docs/superpowers/module-reports/assets`, |
| 782 | - `docs(behavior:frontend-phase:r${behaviorRound}-a${attempt}): 阶段级行为验收证据`), | 942 | + `docs(behavior:frontend-phase:r${behaviorRound}-a${attempt}): 阶段级行为验收证据`, undefined, c), |
| 783 | '', | 943 | '', |
| 784 | '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)', | 944 | '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)', |
| 785 | '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + **样式层无失败** + 无阻断性 envError + 覆盖非空)| `red`。', | 945 | '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + **样式层无失败** + 无阻断性 envError + 覆盖非空)| `red`。', |
| @@ -794,24 +954,24 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | @@ -794,24 +954,24 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt) { | ||
| 794 | // behaviorReverifyPrompt:阶段级行为 fix 后的功能复验。fix 改的是 frontend/ UI 源码,可能引入功能回归—— | 954 | // behaviorReverifyPrompt:阶段级行为 fix 后的功能复验。fix 改的是 frontend/ UI 源码,可能引入功能回归—— |
| 795 | // 在下一 behaviorRound 重起全栈之前,先派子会话跑**全量前端单测**(vitest,不跑 e2e——e2e/行为维度由下一轮 | 955 | // 在下一 behaviorRound 重起全栈之前,先派子会话跑**全量前端单测**(vitest,不跑 e2e——e2e/行为维度由下一轮 |
| 796 | // 行为门重跑 + 阶段 testGate 全量回归兜底),红则当功能回归硬边界(调用方 allowContinue:false)。 | 956 | // 行为门重跑 + 阶段 testGate 全量回归兜底),红则当功能回归硬边界(调用方 allowContinue:false)。 |
| 797 | -function behaviorReverifyPrompt(behaviorRound, fixedCount) { | 957 | +function behaviorReverifyPrompt(behaviorRound, fixedCount, c) { |
| 798 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-reverify-r${behaviorRound}.md` | 958 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-reverify-r${behaviorRound}.md` |
| 799 | return [ | 959 | return [ |
| 800 | `# behavior-reverify — 行为 fix 后前端单测复验(behaviorRound=${behaviorRound})`, | 960 | `# behavior-reverify — 行为 fix 后前端单测复验(behaviorRound=${behaviorRound})`, |
| 801 | '', | 961 | '', |
| 802 | - featureStageContract('frontend'), | 962 | + featureStageContract('frontend', c), |
| 803 | '', | 963 | '', |
| 804 | '## 目标', | 964 | '## 目标', |
| 805 | `阶段级行为门第 ${behaviorRound} 轮 fix(${fixedCount} 项 must-fix)改动了 \`frontend/\` 源码——**派发 Agent 子会话**跑全量前端单测确认无功能回归。**主会话从不直接跑测试,也不自由编写证据。**`, | 965 | `阶段级行为门第 ${behaviorRound} 轮 fix(${fixedCount} 项 must-fix)改动了 \`frontend/\` 源码——**派发 Agent 子会话**跑全量前端单测确认无功能回归。**主会话从不直接跑测试,也不自由编写证据。**`, |
| 806 | '', | 966 | '', |
| 807 | '## 流程', | 967 | '## 流程', |
| 808 | - `- 命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command- `- 命令从 \`${ROOT 取(缺失默认 - `- 命令从 \`${ROOTpnpm test:ci- `- 命令从 \`${ROOT);**只跑单测(vitest),不跑 e2e**。`, | 968 | + `- 命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command+ `- 命令从 \`${c.root 取(缺失默认 + `- 命令从 \`${c.rootpnpm test:ci+ `- 命令从 \`${c.root);**只跑单测(vitest),不跑 e2e**。`, |
| 809 | '- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行)。', | 969 | '- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行)。', |
| 810 | '- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt(fix 引入功能回归,绝不带红进入下一轮行为门)。', | 970 | '- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt(fix 引入功能回归,绝不带红进入下一轮行为门)。', |
| 811 | `- 证据写入 \`${evidence}\`(每轮独立文件不覆盖前轮)。`, | 971 | `- 证据写入 \`${evidence}\`(每轮独立文件不覆盖前轮)。`, |
| 812 | '', | 972 | '', |
| 813 | commitBlock(evidence, `docs(behavior:frontend-phase:r${behaviorRound}): 行为 fix 后单测复验`, | 973 | commitBlock(evidence, `docs(behavior:frontend-phase:r${behaviorRound}): 行为 fix 后单测复验`, |
| 814 | - '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'), | 974 | + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。', c), |
| 815 | '', | 975 | '', |
| 816 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', | 976 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', |
| 817 | `- 全部通过:\`{ "status": "ok", "artifactPath": "${evidence}", "summary": "<exit_code / passed / failed 摘要>" }\`。`, | 977 | `- 全部通过:\`{ "status": "ok", "artifactPath": "${evidence}", "summary": "<exit_code / passed / failed 摘要>" }\`。`, |
| @@ -825,12 +985,14 @@ function behaviorReverifyPrompt(behaviorRound, fixedCount) { | @@ -825,12 +985,14 @@ function behaviorReverifyPrompt(behaviorRound, fixedCount) { | ||
| 825 | // + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。 | 985 | // + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。 |
| 826 | // 由此逐 FE 的 verify(e2e) 与阶段末尾行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点。 | 986 | // 由此逐 FE 的 verify(e2e) 与阶段末尾行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点。 |
| 827 | // feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。 | 987 | // feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。 |
| 828 | -function frontendSkeletonPrompt(feItems) { | 988 | +// lane 注入豁免(附录补 10 审计口径):本 builder 仅 frontend-phase 调用(终波恒宽 1、主根执行), |
| 989 | +// globalSetup 文案里的 seed-demo-data.mjs 字样不需要 ERP_TEST_DB_SCHEMA 注入分支。 | ||
| 990 | +function frontendSkeletonPrompt(feItems, c) { | ||
| 829 | const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' | 991 | const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' |
| 830 | return [ | 992 | return [ |
| 831 | '# fe-skeleton — 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位)', | 993 | '# fe-skeleton — 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位)', |
| 832 | '', | 994 | '', |
| 833 | - featureStageContract('frontend'), | 995 | + featureStageContract('frontend', c), |
| 834 | '', | 996 | '', |
| 835 | '## 目标', | 997 | '## 目标', |
| 836 | '在逐 FE 实现开始**之前**,一次性建出前端「可构建可起」的骨架:App 外壳 + router **全量** lazy 路由表(每个 FE 路由都声明,未实现的指向占位组件 `FeStub`)+ 不指悬空 path 的共享导航。', | 998 | '在逐 FE 实现开始**之前**,一次性建出前端「可构建可起」的骨架:App 外壳 + router **全量** lazy 路由表(每个 FE 路由都声明,未实现的指向占位组件 `FeStub`)+ 不指悬空 path 的共享导航。', |
| @@ -840,10 +1002,10 @@ function frontendSkeletonPrompt(feItems) { | @@ -840,10 +1002,10 @@ function frontendSkeletonPrompt(feItems) { | ||
| 840 | `- ${list}`, | 1002 | `- ${list}`, |
| 841 | '', | 1003 | '', |
| 842 | '## 收集上下文(确定技术栈 + 目录约定 + 路由)', | 1004 | '## 收集上下文(确定技术栈 + 目录约定 + 路由)', |
| 843 | - `- \`${ROOT}/docs/04-技术规范.md § 零\`(\`frontend.ui_lib\` / framework / 构建工具)+ \`§ 二 前端规范\`(§ 2.1 目录约定 = 落盘位置 / 路由库 / 入口文件名)。`, | ||
| 844 | - `- \`${ROOT}/docs/08-模块任务管理.md § 三\`(前端阶段元数据 + \`功能:\` 下全部 \`FE-NN\` 行;与上面清单核对,以本提示给出的清单为准)。`, | ||
| 845 | - `- \`${ROOT}/docs/01-需求清单/\` 各 FE 关联 REQ + \`${ROOT}/prototype/\`(页面/路由结构权威)+ \`${ROOT}/docs/05-API接口契约.md\`,据此推导每个 FE-NN 对应的**路由 path**(带参动态路由保留 \`:id\` 占位)。`, | ||
| 846 | - `- 用 Grep 在 \`${ROOT}/frontend/\` 探测现有 App 外壳 / 入口 / router 是否已存在(幂等:已存在则按需补齐,不重复创建/不覆盖已实现的真组件)。`, | 1005 | + `- \`${c.root}/docs/04-技术规范.md § 零\`(\`frontend.ui_lib\` / framework / 构建工具)+ \`§ 二 前端规范\`(§ 2.1 目录约定 = 落盘位置 / 路由库 / 入口文件名)。`, |
| 1006 | + `- \`${c.root}/docs/08-模块任务管理.md § 三\`(前端阶段元数据 + \`功能:\` 下全部 \`FE-NN\` 行;与上面清单核对,以本提示给出的清单为准)。`, | ||
| 1007 | + `- \`${c.root}/docs/01-需求清单/\` 各 FE 关联 REQ + \`${c.root}/prototype/\`(页面/路由结构权威)+ \`${c.root}/docs/05-API接口契约.md\`,据此推导每个 FE-NN 对应的**路由 path**(带参动态路由保留 \`:id\` 占位)。`, | ||
| 1008 | + `- 用 Grep 在 \`${c.root}/frontend/\` 探测现有 App 外壳 / 入口 / router 是否已存在(幂等:已存在则按需补齐,不重复创建/不覆盖已实现的真组件)。`, | ||
| 847 | '', | 1009 | '', |
| 848 | '## 产出(全部落在 `frontend/` 路径内——遵守前端阶段路径作用域护栏)', | 1010 | '## 产出(全部落在 `frontend/` 路径内——遵守前端阶段路径作用域护栏)', |
| 849 | '1. **App 外壳 + 入口**:`frontend/src/App.*` 与入口 `frontend/src/main.*`(按 framework / docs/04 约定的扩展名;不存在才创建)。挂载共享布局 + `<router-view>`(或等价 outlet)。', | 1011 | '1. **App 外壳 + 入口**:`frontend/src/App.*` 与入口 `frontend/src/main.*`(按 framework / docs/04 约定的扩展名;不存在才创建)。挂载共享布局 + `<router-view>`(或等价 outlet)。', |
| @@ -855,7 +1017,7 @@ function frontendSkeletonPrompt(feItems) { | @@ -855,7 +1017,7 @@ function frontendSkeletonPrompt(feItems) { | ||
| 855 | '4. **共享布局/导航**:导航链接**全部指向已在 router 声明的路由 path**(不指向任何不存在的 path),保证任意时刻无悬空链接。', | 1017 | '4. **共享布局/导航**:导航链接**全部指向已在 router 声明的路由 path**(不指向任何不存在的 path),保证任意时刻无悬空链接。', |
| 856 | '5. **e2e 基线脚手架(全部落 `frontend/` 内)**:', | 1018 | '5. **e2e 基线脚手架(全部落 `frontend/` 内)**:', |
| 857 | ' - **Playwright 配置**(按 docs/04 § 零 `frontend.e2e_runner` 约定,如 `frontend/playwright.config.*`):声明 `globalSetup` / `globalTeardown` 入口 + 共享 `storageState`。', | 1019 | ' - **Playwright 配置**(按 docs/04 § 零 `frontend.e2e_runner` 约定,如 `frontend/playwright.config.*`):声明 `globalSetup` / `globalTeardown` 入口 + 共享 `storageState`。', |
| 858 | - ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT}/scripts/seed-demo-data.mjs- ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT(注入演示种子)→ 用 - ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOTconfig-vars.yaml- ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT 的 - ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOTadmin_init- ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT 凭据经 - ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOTdocs/05-API接口契约.md- ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT 登录端点取 JWT,写 - ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOTstorageState- ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT(admin 登录态供 e2e 复用)。`, | 1020 | + ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.root}/scripts/seed-demo-data.mjs+ ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.root(注入演示种子)→ 用 + ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.rootconfig-vars.yaml+ ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.root 的 + ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.rootadmin_init+ ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.root 凭据经 + ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.rootdocs/05-API接口契约.md+ ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.root 登录端点取 JWT,写 + ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.rootstorageState+ ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${c.root(admin 登录态供 e2e 复用)。`, |
| 859 | ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树。', | 1021 | ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树。', |
| 860 | ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 空库重建 + Flyway schema + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐。', | 1022 | ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 空库重建 + Flyway schema + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐。', |
| 861 | '6. **单测基线(测试隔离接线点)**:vitest 配置(按 docs/04 § 零 `frontend.unit_test_runner` 约定,如 `frontend/vitest.config.*` 或 package.json 内配置段)`include` **限定** `tests/**/*.test.*`——单测一律落 `frontend/tests/**`(镜像 `src/` 结构,smoke 类归 `tests/__smoke__/` 且文件名同样以 `.test.*` 结尾,见 docs/04 § 2.1),`frontend/src/` 内的测试残留不被执行(约定漂移立即可见)。不存在才创建,已存在则只补齐 include 限定。**legacy 守卫**:若 `frontend/src/` 内已存在测试文件(旧约定 colocation 残留),**绝不**收窄 include(否则旧单测静默停跑、回归覆盖丢失)——保持现有 include 不动,登记 `decisions[]` 提示人工迁移(src/ 内测试迁至 tests/ 镜像路径后方可收窄)。', | 1023 | '6. **单测基线(测试隔离接线点)**:vitest 配置(按 docs/04 § 零 `frontend.unit_test_runner` 约定,如 `frontend/vitest.config.*` 或 package.json 内配置段)`include` **限定** `tests/**/*.test.*`——单测一律落 `frontend/tests/**`(镜像 `src/` 结构,smoke 类归 `tests/__smoke__/` 且文件名同样以 `.test.*` 结尾,见 docs/04 § 2.1),`frontend/src/` 内的测试残留不被执行(约定漂移立即可见)。不存在才创建,已存在则只补齐 include 限定。**legacy 守卫**:若 `frontend/src/` 内已存在测试文件(旧约定 colocation 残留),**绝不**收窄 include(否则旧单测静默停跑、回归覆盖丢失)——保持现有 include 不动,登记 `decisions[]` 提示人工迁移(src/ 内测试迁至 tests/ 镜像路径后方可收窄)。', |
| @@ -867,7 +1029,7 @@ function frontendSkeletonPrompt(feItems) { | @@ -867,7 +1029,7 @@ function frontendSkeletonPrompt(feItems) { | ||
| 867 | '- 占位符扫描:`TBD` / `TODO` / `【人工填写:】` → 命中即修。', | 1029 | '- 占位符扫描:`TBD` / `TODO` / `【人工填写:】` → 命中即修。', |
| 868 | '', | 1030 | '', |
| 869 | commitBlock('frontend/', 'feat(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位', | 1031 | commitBlock('frontend/', 'feat(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位', |
| 870 | - '- commit 失败 → halt,把 stderr 摘要写进 reason。'), | 1032 | + '- commit 失败 → halt,把 stderr 摘要写进 reason。', c), |
| 871 | '', | 1033 | '', |
| 872 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', | 1034 | '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', |
| 873 | '- 成功:`{ "status": "ok", "summary": "<已声明的 FE 路由数 / 入口与 router 文件路径摘要>" }`(artifactPath 可省)。', | 1035 | '- 成功:`{ "status": "ok", "summary": "<已声明的 FE 路由数 / 入口与 router 文件路径摘要>" }`(artifactPath 可省)。', |
| @@ -878,14 +1040,14 @@ function frontendSkeletonPrompt(feItems) { | @@ -878,14 +1040,14 @@ function frontendSkeletonPrompt(feItems) { | ||
| 878 | 1040 | ||
| 879 | // fe-skeleton 幂等判定:检测 router 是否已声明本阶段全部 FE 路由(全量 + 全 lazy)。 | 1041 | // fe-skeleton 幂等判定:检测 router 是否已声明本阶段全部 FE 路由(全量 + 全 lazy)。 |
| 880 | // router/state 是骨架真实完成态;fe-skeleton-done tag 只作补记,避免陈旧 tag 跳过缺失骨架。 | 1042 | // router/state 是骨架真实完成态;fe-skeleton-done tag 只作补记,避免陈旧 tag 跳过缺失骨架。 |
| 881 | -function frontendSkeletonStatePromptM(feItems) { | 1043 | +function frontendSkeletonStatePromptM(feItems, c) { |
| 882 | const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(无)' | 1044 | const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(无)' |
| 883 | return [ | 1045 | return [ |
| 884 | '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', | 1046 | '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', |
| 885 | - microStepContract(), | 1047 | + microStepContract(c.root), |
| 886 | '', | 1048 | '', |
| 887 | - `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在,**且 e2e 基线脚手架存在**——Playwright 配置文件(\`frontend/playwright.config.*\`)+ globalSetup 文件(如 \`frontend/e2e/global-setup.*\`),**且单测基线存在**——vitest 配置 \`include\` 限定 \`tests/**/*.test.*\`。`, | ||
| 888 | - `- **legacy 豁免**:若 \`${ROOT}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`, | 1049 | + `用 Grep / Read 检查 \`${c.root}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在,**且 e2e 基线脚手架存在**——Playwright 配置文件(\`frontend/playwright.config.*\`)+ globalSetup 文件(如 \`frontend/e2e/global-setup.*\`),**且单测基线存在**——vitest 配置 \`include\` 限定 \`tests/**/*.test.*\`。`, |
| 1050 | + `- **legacy 豁免**:若 \`${c.root}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`, | ||
| 889 | '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`', | 1051 | '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`', |
| 890 | '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`', | 1052 | '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`', |
| 891 | '## 输出(EXISTS_SCHEMA)', | 1053 | '## 输出(EXISTS_SCHEMA)', |
| @@ -894,12 +1056,14 @@ function frontendSkeletonStatePromptM(feItems) { | @@ -894,12 +1056,14 @@ function frontendSkeletonStatePromptM(feItems) { | ||
| 894 | 1056 | ||
| 895 | // ---- 微步骤 prompt builders(runBranchSetup / runMilestone / runCrossModule 用)---- | 1057 | // ---- 微步骤 prompt builders(runBranchSetup / runMilestone / runCrossModule 用)---- |
| 896 | // 每个 prompt 单职责、短文本;返回严格 schema;执行(action)步统一返回 ACTION_RESULT_SCHEMA。 | 1058 | // 每个 prompt 单职责、短文本;返回严格 schema;执行(action)步统一返回 ACTION_RESULT_SCHEMA。 |
| 897 | -function microStepContract() { | 1059 | +// root 显式入参(附录补 11):模块作用域微步骤传 c.root(lane 模式在 lane 树上取证/操作), |
| 1060 | +// 主根专属微步骤(milestone / RESUME / ledger / preflight / 默认分支探测)传 ROOT——两组名单见各调用方。 | ||
| 1061 | +function microStepContract(root) { | ||
| 898 | return [ | 1062 | return [ |
| 899 | '## 硬约束(非交互子代理)', | 1063 | '## 硬约束(非交互子代理)', |
| 900 | '- 你是 Workflow 派生的**非交互子代理**,绝不弹问。', | 1064 | '- 你是 Workflow 派生的**非交互子代理**,绝不弹问。', |
| 901 | '- 全部输出**使用中文**。', | 1065 | '- 全部输出**使用中文**。', |
| 902 | - `- 项目根 = \`${ROOT}\`。所有 git 命令必须用 \`git -C ${ROOT} ...\`;Read/Edit/Write 的路径都以 \`${ROOT}- `- 项目根 = \`${ROOT}\`。所有 git 命令必须用 \`git -C ${ROOT} ...\`;Read/Edit/Write 的路径都以 \`${ROOT 为根。`, | 1066 | + `- 项目根 = \`${root}\`。所有 git 命令必须用 \`git -C ${root} ...\`(除非步骤明示了其它 \`-C\` 目标——lane / feature worktree 路径,以步骤原文为准);Read/Edit/Write 的路径都以 \`${root}+ `- 项目根 = \`${root}\`。所有 git 命令必须用 \`git -C ${root} ...\`(除非步骤明示了其它 \`-C\` 目标——lane / feature worktree 路径,以步骤原文为准);Read/Edit/Write 的路径都以 \`${root 为根。`, |
| 903 | '- 严格按下方"输出"段返回 schema 字段;**不要**在 schema 外追加自由叙述。', | 1067 | '- 严格按下方"输出"段返回 schema 字段;**不要**在 schema 外追加自由叙述。', |
| 904 | ].join('\n') | 1068 | ].join('\n') |
| 905 | } | 1069 | } |
| @@ -922,16 +1086,18 @@ const BEHAVIOR_ATTEMPT_MAX = 2 | @@ -922,16 +1086,18 @@ const BEHAVIOR_ATTEMPT_MAX = 2 | ||
| 922 | const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' | 1086 | const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' |
| 923 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' | 1087 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' |
| 924 | 1088 | ||
| 925 | -// 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 | 1089 | +// 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记后随结果回传供人工事后审阅。 |
| 1090 | +// sink 参数:模块作用域调用传 c.decisions(per-module 收集器,并行交错下水位线语义失效的替代); | ||
| 1091 | +// 缺省落全局 autonomousDecisions(仅剩"漏传 dec 的兜底"语义,顶层 return 时作为残量并入)。 | ||
| 926 | const autonomousDecisions = [] | 1092 | const autonomousDecisions = [] |
| 927 | const decisionDigestMd = (list, emptyText) => list.length | 1093 | const decisionDigestMd = (list, emptyText) => list.length |
| 928 | ? list.map(d => ` - [\`${d.site}\`] ${d.question || '?'} → ${d.choice || '?'}(${d.confidence || '?'})`).join('\n') | 1094 | ? list.map(d => ` - [\`${d.site}\`] ${d.question || '?'} → ${d.choice || '?'}(${d.confidence || '?'})`).join('\n') |
| 929 | : ` - ${emptyText}` | 1095 | : ` - ${emptyText}` |
| 930 | -function recordDecisions(site, decisions) { | 1096 | +function recordDecisions(site, decisions, sink = autonomousDecisions) { |
| 931 | if (!Array.isArray(decisions)) return | 1097 | if (!Array.isArray(decisions)) return |
| 932 | for (const d of decisions) { | 1098 | for (const d of decisions) { |
| 933 | if (!d) continue | 1099 | if (!d) continue |
| 934 | - autonomousDecisions.push({ site, question:d.question, choice:d.choice, rationale:d.rationale, confidence:d.confidence }) | 1100 | + sink.push({ site, question:d.question, choice:d.choice, rationale:d.rationale, confidence:d.confidence }) |
| 935 | log(`decision ${site}: ${d.question || '?'} → ${d.choice || '?'} (${d.confidence || '?'})`) | 1101 | log(`decision ${site}: ${d.question || '?'} → ${d.choice || '?'} (${d.confidence || '?'})`) |
| 936 | } | 1102 | } |
| 937 | } | 1103 | } |
| @@ -951,11 +1117,13 @@ async function agentR(prompt, opts) { | @@ -951,11 +1117,13 @@ async function agentR(prompt, opts) { | ||
| 951 | return r | 1117 | return r |
| 952 | } | 1118 | } |
| 953 | 1119 | ||
| 954 | -function adjudicatePromptM(site, context) { | 1120 | +// root(附录补 11):仲裁取证树根——lane stage 失败时传 c.root,让仲裁子代理看到 lane 分支上 |
| 1121 | +// 刚 commit 的 spec/plan/源码(主根工作树可能停在默认分支,树面错误会让裁决与 guidance 失真)。 | ||
| 1122 | +function adjudicatePromptM(site, context, root) { | ||
| 955 | const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2) | 1123 | const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2) |
| 956 | return [ | 1124 | return [ |
| 957 | `# 仲裁:\`${site}\` 触发潜在 halt,请裁决 retry / continue / halt`, | 1125 | `# 仲裁:\`${site}\` 触发潜在 halt,请裁决 retry / continue / halt`, |
| 958 | - microStepContract(), | 1126 | + microStepContract(root), |
| 959 | '', | 1127 | '', |
| 960 | '## 你的角色', | 1128 | '## 你的角色', |
| 961 | '你是 ERP 编码 Workflow 的**仲裁子代理**。某上游步骤触发了一个原本会让整阶段 fail-fast 停下的护栏。', | 1129 | '你是 ERP 编码 Workflow 的**仲裁子代理**。某上游步骤触发了一个原本会让整阶段 fail-fast 停下的护栏。', |
| @@ -976,8 +1144,8 @@ function adjudicatePromptM(site, context) { | @@ -976,8 +1144,8 @@ function adjudicatePromptM(site, context) { | ||
| 976 | ].join('\n') | 1144 | ].join('\n') |
| 977 | } | 1145 | } |
| 978 | 1146 | ||
| 979 | -async function adjudicate(site, context, grp, round) { | ||
| 980 | - const verdict = await agent(adjudicatePromptM(site, context), | 1147 | +async function adjudicate(site, context, grp, round, root = ROOT) { |
| 1148 | + const verdict = await agent(adjudicatePromptM(site, context, root), | ||
| 981 | {label:`adjudicate:${site}:r${round}`, phase: grp, schema: ADJUDICATE_SCHEMA}) | 1149 | {label:`adjudicate:${site}:r${round}`, phase: grp, schema: ADJUDICATE_SCHEMA}) |
| 982 | if (!verdict) { // 仲裁子代理自身遭遇终端 API 错误 → 默认 retry(确定性,防 null 级联) | 1150 | if (!verdict) { // 仲裁子代理自身遭遇终端 API 错误 → 默认 retry(确定性,防 null 级联) |
| 983 | log(`adjudicate ${site} r${round}: 仲裁子代理无返回(终端 API 错误/被跳过),默认 retry`) | 1151 | log(`adjudicate ${site} r${round}: 仲裁子代理无返回(终端 API 错误/被跳过),默认 retry`) |
| @@ -993,22 +1161,23 @@ async function adjudicate(site, context, grp, round) { | @@ -993,22 +1161,23 @@ async function adjudicate(site, context, grp, round) { | ||
| 993 | // allowContinue=true 只用于后续 reviewer / behavior 会再次兜底的软 stage;流程前提默认不可 continue。 | 1161 | // allowContinue=true 只用于后续 reviewer / behavior 会再次兜底的软 stage;流程前提默认不可 continue。 |
| 994 | // allowContinue=false:本 stage 的 halt 代表**硬正确性边界**(功能测试红色 verify/reverify、路径越界/卡死 tdd、 | 1162 | // allowContinue=false:本 stage 的 halt 代表**硬正确性边界**(功能测试红色 verify/reverify、路径越界/卡死 tdd、 |
| 995 | // test-gate 红 report),仲裁只许 retry/halt,**绝不 continue 放行**残缺/越界状态去 approve / milestone。 | 1163 | // test-gate 红 report),仲裁只许 retry/halt,**绝不 continue 放行**残缺/越界状态去 approve / milestone。 |
| 996 | -async function runStage(makePrompt, { site, grp, label, validate, allowContinue = false }) { | 1164 | +// dec:decisions 落点(模块作用域调用点传 c.decisions);root:仲裁取证树根(模块作用域传 c.root,补 11)。 |
| 1165 | +async function runStage(makePrompt, { site, grp, label, validate, allowContinue = false, dec, root = ROOT }) { | ||
| 997 | let guidance = '' | 1166 | let guidance = '' |
| 998 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { | 1167 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { |
| 999 | const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) | 1168 | const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) |
| 1000 | if (!res) { | 1169 | if (!res) { |
| 1001 | - const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round) | 1170 | + const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round, root) |
| 1002 | if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) | 1171 | if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) |
| 1003 | guidance = verdict.guidance || '' | 1172 | guidance = verdict.guidance || '' |
| 1004 | continue | 1173 | continue |
| 1005 | } | 1174 | } |
| 1006 | - recordDecisions(site, res.decisions) | 1175 | + recordDecisions(site, res.decisions, dec) |
| 1007 | let problem = null | 1176 | let problem = null |
| 1008 | if (res.status === 'halt') problem = `stage 返回 status:halt;reason: ${res.reason || '(空)'}` | 1177 | if (res.status === 'halt') problem = `stage 返回 status:halt;reason: ${res.reason || '(空)'}` |
| 1009 | else if (validate) { try { problem = validate(res) } catch (e) { problem = String(e?.message || e) } } | 1178 | else if (validate) { try { problem = validate(res) } catch (e) { problem = String(e?.message || e) } } |
| 1010 | if (!problem) return res | 1179 | if (!problem) return res |
| 1011 | - const verdict = await adjudicate(site, { problem, stageResult: res, allowContinue }, grp, round) | 1180 | + const verdict = await adjudicate(site, { problem, stageResult: res, allowContinue }, grp, round, root) |
| 1012 | if (verdict.action === 'continue' && allowContinue) return res | 1181 | if (verdict.action === 'continue' && allowContinue) return res |
| 1013 | if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || problem}`) | 1182 | if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || problem}`) |
| 1014 | guidance = verdict.guidance || '' // retry:带 guidance 重跑 | 1183 | guidance = verdict.guidance || '' // retry:带 guidance 重跑 |
| @@ -1018,19 +1187,20 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue | @@ -1018,19 +1187,20 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue | ||
| 1018 | 1187 | ||
| 1019 | // runAction:跑一个 ACTION_RESULT 微步骤(git / 文件写),失败时经 adjudicate 决定 retry/continue/halt。 | 1188 | // runAction:跑一个 ACTION_RESULT 微步骤(git / 文件写),失败时经 adjudicate 决定 retry/continue/halt。 |
| 1020 | // allowContinue=true 时 continue 视为"接受失败并前进"(仅用于纯可视化等可安全跳过的副作用)。 | 1189 | // allowContinue=true 时 continue 视为"接受失败并前进"(仅用于纯可视化等可安全跳过的副作用)。 |
| 1021 | -async function runAction(makePrompt, { site, grp, label, allowContinue = false }) { | 1190 | +// dec:与 runStage 调用点签名统一而收(ACTION_RESULT 无 decisions 字段,当前不消费);root:仲裁取证树根(补 11)。 |
| 1191 | +async function runAction(makePrompt, { site, grp, label, allowContinue = false, dec, root = ROOT }) { | ||
| 1022 | let guidance = '' | 1192 | let guidance = '' |
| 1023 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { | 1193 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { |
| 1024 | const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA}) | 1194 | const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA}) |
| 1025 | if (!r) { | 1195 | if (!r) { |
| 1026 | - const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round) | 1196 | + const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round, root) |
| 1027 | if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) | 1197 | if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) |
| 1028 | guidance = verdict.guidance || '' | 1198 | guidance = verdict.guidance || '' |
| 1029 | continue | 1199 | continue |
| 1030 | } | 1200 | } |
| 1031 | if (r.success) return r | 1201 | if (r.success) return r |
| 1032 | const verdict = await adjudicate(site, | 1202 | const verdict = await adjudicate(site, |
| 1033 | - { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round) | 1203 | + { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round, root) |
| 1034 | if (verdict.action === 'continue' && allowContinue) return r | 1204 | if (verdict.action === 'continue' && allowContinue) return r |
| 1035 | if (verdict.action === 'halt' || verdict.action === 'continue') | 1205 | if (verdict.action === 'halt' || verdict.action === 'continue') |
| 1036 | throw new Error(`HALT ${site}: ${verdict.rationale || r.error || ''}`) | 1206 | throw new Error(`HALT ${site}: ${verdict.rationale || r.error || ''}`) |
| @@ -1039,7 +1209,7 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } | @@ -1039,7 +1209,7 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } | ||
| 1039 | throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) | 1209 | throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) |
| 1040 | } | 1210 | } |
| 1041 | 1211 | ||
| 1042 | -// best-effort 微步骤统一包裹:失败/异常只 log 绝不阻断主流程。返回是否成功(供水位线推进判断)。// ≠ runAction:本 helper 不经 adjudicate、绝不 halt。 | 1212 | +// best-effort 微步骤统一包裹:失败/异常只 log 绝不阻断主流程。返回是否成功(供 RESUME flush 记账判断)。// ≠ runAction:本 helper 不经 adjudicate、绝不 halt。 |
| 1043 | async function bestEffortAction(prompt, { label, phase: ph, okMsg, failTag, errTag = failTag }) { | 1213 | async function bestEffortAction(prompt, { label, phase: ph, okMsg, failTag, errTag = failTag }) { |
| 1044 | try { | 1214 | try { |
| 1045 | const r = await agent(prompt, { label, phase: ph, schema: ACTION_RESULT_SCHEMA }) | 1215 | const r = await agent(prompt, { label, phase: ph, schema: ACTION_RESULT_SCHEMA }) |
| @@ -1061,7 +1231,7 @@ const RESUME_PATH = 'docs/superpowers/RESUME.md' | @@ -1061,7 +1231,7 @@ const RESUME_PATH = 'docs/superpowers/RESUME.md' | ||
| 1061 | function resumeJournalPromptM(sectionMd) { | 1231 | function resumeJournalPromptM(sectionMd) { |
| 1062 | return [ | 1232 | return [ |
| 1063 | '# 追加续跑日志(RESUME handoff)— 非交互静默', | 1233 | '# 追加续跑日志(RESUME handoff)— 非交互静默', |
| 1064 | - microStepContract(), | 1234 | + microStepContract(ROOT), |
| 1065 | '', | 1235 | '', |
| 1066 | `## 任务:把给定条目**追加到 \`${RESUME_PATH}\` 末尾**后 commit(不改其它任何文件)`, | 1236 | `## 任务:把给定条目**追加到 \`${RESUME_PATH}\` 末尾**后 commit(不改其它任何文件)`, |
| 1067 | `1. 确保目录 \`${ROOT}/docs/superpowers/\` 存在(不存在则创建)。`, | 1237 | `1. 确保目录 \`${ROOT}/docs/superpowers/\` 存在(不存在则创建)。`, |
| @@ -1089,7 +1259,7 @@ async function recordResume(sectionMd) { | @@ -1089,7 +1259,7 @@ async function recordResume(sectionMd) { | ||
| 1089 | function ledgerBaselinePromptM() { | 1259 | function ledgerBaselinePromptM() { |
| 1090 | return [ | 1260 | return [ |
| 1091 | '# 需求台账基线(首跑)— 非交互静默', | 1261 | '# 需求台账基线(首跑)— 非交互静默', |
| 1092 | - microStepContract(), | 1262 | + microStepContract(ROOT), |
| 1093 | '', | 1263 | '', |
| 1094 | `## 任务:若 \`${ROOT}/.req-ledger.json\` 不存在,则建立并 commit;已存在则跳过`, | 1264 | `## 任务:若 \`${ROOT}/.req-ledger.json\` 不存在,则建立并 commit;已存在则跳过`, |
| 1095 | `1. 检查 \`${ROOT}/.req-ledger.json\` 是否存在。存在 → 直接返回 \`{ "success": true, "detail": "ledger 已存在,跳过" }\`。`, | 1265 | `1. 检查 \`${ROOT}/.req-ledger.json\` 是否存在。存在 → 直接返回 \`{ "success": true, "detail": "ledger 已存在,跳过" }\`。`, |
| @@ -1109,7 +1279,7 @@ async function ensureLedgerBaseline() { | @@ -1109,7 +1279,7 @@ async function ensureLedgerBaseline() { | ||
| 1109 | function preflightPromptM() { | 1279 | function preflightPromptM() { |
| 1110 | return [ | 1280 | return [ |
| 1111 | '# Preflight:编码阶段环境探测(只读探测 + 少量无副作用命令,不改任何文件)', | 1281 | '# Preflight:编码阶段环境探测(只读探测 + 少量无副作用命令,不改任何文件)', |
| 1112 | - microStepContract(), | 1282 | + microStepContract(ROOT), |
| 1113 | '', | 1283 | '', |
| 1114 | `读 \`${ROOT}/docs/04-技术规范.md § 零\`(锁定技术栈与命令清单),逐项探测本机环境是否满足:`, | 1284 | `读 \`${ROOT}/docs/04-技术规范.md § 零\`(锁定技术栈与命令清单),逐项探测本机环境是否满足:`, |
| 1115 | '1. **node**:`node --version` 主版本满足 docs/04 要求。', | 1285 | '1. **node**:`node --version` 主版本满足 docs/04 要求。', |
| @@ -1127,11 +1297,11 @@ function preflightPromptM() { | @@ -1127,11 +1297,11 @@ function preflightPromptM() { | ||
| 1127 | // **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD | 1297 | // **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD |
| 1128 | // 停在默认分支、resume 时残留落在默认分支),绝不 add -A/commit——否则会把绕过 review/test-gate 的改动 | 1298 | // 停在默认分支、resume 时残留落在默认分支),绝不 add -A/commit——否则会把绕过 review/test-gate 的改动 |
| 1129 | // 直接提交进默认分支,且该改动对模块 `<default>...HEAD` 三点 diff 不可见(污染 cross-module / 完成报告)。 | 1299 | // 直接提交进默认分支,且该改动对模块 `<default>...HEAD` 三点 diff 不可见(污染 cross-module / 完成报告)。 |
| 1130 | -function recoverDirtyWorktreePromptM(dirty, branch, scopeHint) { | 1300 | +function recoverDirtyWorktreePromptM(dirty, branch, scopeHint, c) { |
| 1131 | const list = (dirty || []).map(p => `- ${p}`).join('\n') || '(调用方未给清单,请自行 `git status --porcelain` 复核)' | 1301 | const list = (dirty || []).map(p => `- ${p}`).join('\n') || '(调用方未给清单,请自行 `git status --porcelain` 复核)' |
| 1132 | return [ | 1302 | return [ |
| 1133 | '# 工作树不干净——判定能否自主提交后继续', | 1303 | '# 工作树不干净——判定能否自主提交后继续', |
| 1134 | - microStepContract(), | 1304 | + microStepContract(c.root), |
| 1135 | '', | 1305 | '', |
| 1136 | '## 背景', | 1306 | '## 背景', |
| 1137 | `分支切换 / 里程碑前要求工作树干净,当前存在未提交改动。${scopeHint || ''}`, | 1307 | `分支切换 / 里程碑前要求工作树干净,当前存在未提交改动。${scopeHint || ''}`, |
| @@ -1141,9 +1311,9 @@ function recoverDirtyWorktreePromptM(dirty, branch, scopeHint) { | @@ -1141,9 +1311,9 @@ function recoverDirtyWorktreePromptM(dirty, branch, scopeHint) { | ||
| 1141 | list, | 1311 | list, |
| 1142 | '', | 1312 | '', |
| 1143 | '## 流程', | 1313 | '## 流程', |
| 1144 | - `0. **分支护栏(必须先做)**:跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。若当前分支 **!= \`${branch}\`**(目标功能分支),**绝不提交**——直接返回 \`{ "success": false, "error": "dirty-on-wrong-branch", "detail": "HEAD=<当前分支>, expected ${branch};拒绝把残留提交到非功能分支,留给人工" }\`。只有当前已在 \`${branch}\` 才继续 step 1。`, | ||
| 1145 | - `1. 逐一检查改动(\`git -C ${ROOT} status --porcelain\`,必要时 \`git -C ${ROOT} diff\`)。`, | ||
| 1146 | - `2. **全部都是本阶段合法产物**(spec/plan/verify/review/report/源码/migration,且落在当前阶段路径作用域内)→ \`git -C ${ROOT} add -A\` 后 \`git -C ${ROOT} commit -m "chore: 自动提交上一步残留改动"\`,返回 \`{ "success": true, "detail": "committed-in-scope" }\`。`, | 1314 | + `0. **分支护栏(必须先做)**:跑 \`git -C ${c.root} rev-parse --abbrev-ref HEAD\`。若当前分支 **!= \`${branch}\`**(目标功能分支),**绝不提交**——直接返回 \`{ "success": false, "error": "dirty-on-wrong-branch", "detail": "HEAD=<当前分支>, expected ${branch};拒绝把残留提交到非功能分支,留给人工" }\`。只有当前已在 \`${branch}\` 才继续 step 1。`, |
| 1315 | + `1. 逐一检查改动(\`git -C ${c.root} status --porcelain\`,必要时 \`git -C ${c.root} diff\`)。`, | ||
| 1316 | + `2. **全部都是本阶段合法产物**(spec/plan/verify/review/report/源码/migration,且落在当前阶段路径作用域内)→ \`git -C ${c.root} add -A\` 后 \`git -C ${c.root} commit -m "chore: 自动提交上一步残留改动"\`,返回 \`{ "success": true, "detail": "committed-in-scope" }\`。`, | ||
| 1147 | '3. 含**越界 / 不明 / 与本阶段无关**的改动(手工临时文件、其它模块代码、构建产物等)→ **不要提交**,返回 `{ "success": false, "error": "dirty-out-of-scope", "detail": "<可疑文件 + 原因>" }`。', | 1317 | '3. 含**越界 / 不明 / 与本阶段无关**的改动(手工临时文件、其它模块代码、构建产物等)→ **不要提交**,返回 `{ "success": false, "error": "dirty-out-of-scope", "detail": "<可疑文件 + 原因>" }`。', |
| 1148 | '## 输出(ACTION_RESULT_SCHEMA)', | 1318 | '## 输出(ACTION_RESULT_SCHEMA)', |
| 1149 | ].join('\n') | 1319 | ].join('\n') |
| @@ -1153,7 +1323,7 @@ function recoverDirtyWorktreePromptM(dirty, branch, scopeHint) { | @@ -1153,7 +1323,7 @@ function recoverDirtyWorktreePromptM(dirty, branch, scopeHint) { | ||
| 1153 | function detectDefaultBranchPromptM() { | 1323 | function detectDefaultBranchPromptM() { |
| 1154 | return [ | 1324 | return [ |
| 1155 | '# 检测本地默认分支', | 1325 | '# 检测本地默认分支', |
| 1156 | - microStepContract(), | 1326 | + microStepContract(ROOT), |
| 1157 | '', | 1327 | '', |
| 1158 | `用 \`git -C ${ROOT} rev-parse --verify refs/heads/<name>\` 依次试 \`main\` / \`master\`,取第一个 exit=0 的为默认分支。`, | 1328 | `用 \`git -C ${ROOT} rev-parse --verify refs/heads/<name>\` 依次试 \`main\` / \`master\`,取第一个 exit=0 的为默认分支。`, |
| 1159 | '## 输出(DEFAULT_BRANCH_SCHEMA)', | 1329 | '## 输出(DEFAULT_BRANCH_SCHEMA)', |
| @@ -1162,95 +1332,344 @@ function detectDefaultBranchPromptM() { | @@ -1162,95 +1332,344 @@ function detectDefaultBranchPromptM() { | ||
| 1162 | ].join('\n') | 1332 | ].join('\n') |
| 1163 | } | 1333 | } |
| 1164 | 1334 | ||
| 1165 | -function worktreeCleanPromptM() { | 1335 | +function worktreeCleanPromptM(c) { |
| 1166 | return [ | 1336 | return [ |
| 1167 | '# 检查工作树是否干净', | 1337 | '# 检查工作树是否干净', |
| 1168 | - microStepContract(), | 1338 | + microStepContract(c.root), |
| 1169 | '', | 1339 | '', |
| 1170 | - `跑 \`git -C ${ROOT} status --porcelain- `跑 \`git -C ${ROOT,按行解析 dirty 文件路径(第 4 字符起)。`, | 1340 | + `跑 \`git -C ${c.root} status --porcelain+ `跑 \`git -C ${c.root,按行解析 dirty 文件路径(第 4 字符起)。`, |
| 1171 | '## 输出(WT_SCHEMA)', | 1341 | '## 输出(WT_SCHEMA)', |
| 1172 | '- 干净:`{ "clean": true }`', | 1342 | '- 干净:`{ "clean": true }`', |
| 1173 | '- 不干净:`{ "clean": false, "dirty": ["<path>", ...] }`', | 1343 | '- 不干净:`{ "clean": false, "dirty": ["<path>", ...] }`', |
| 1174 | ].join('\n') | 1344 | ].join('\n') |
| 1175 | } | 1345 | } |
| 1176 | 1346 | ||
| 1177 | -function checkBranchExistsPromptM(branch) { | 1347 | +function checkBranchExistsPromptM(branch, c) { |
| 1178 | return [ | 1348 | return [ |
| 1179 | `# 本地分支 \`${branch}\` 是否存在`, | 1349 | `# 本地分支 \`${branch}\` 是否存在`, |
| 1180 | - microStepContract(), | 1350 | + microStepContract(c.root), |
| 1181 | '', | 1351 | '', |
| 1182 | - `跑 \`git -C ${ROOT} rev-parse --verify refs/heads/${branch}- `跑 \`git -C ${ROOT(用 2>/dev/null 抑制 stderr)。`, | 1352 | + `跑 \`git -C ${c.root} rev-parse --verify refs/heads/${branch}+ `跑 \`git -C ${c.root(用 2>/dev/null 抑制 stderr)。`, |
| 1183 | '## 输出(EXISTS_SCHEMA)', | 1353 | '## 输出(EXISTS_SCHEMA)', |
| 1184 | '- exit=0 → `{ "exists": true }`;非 0 → `{ "exists": false }`', | 1354 | '- exit=0 → `{ "exists": true }`;非 0 → `{ "exists": false }`', |
| 1185 | ].join('\n') | 1355 | ].join('\n') |
| 1186 | } | 1356 | } |
| 1187 | 1357 | ||
| 1188 | -function currentBranchPromptM() { | 1358 | +function currentBranchPromptM(c) { |
| 1189 | return [ | 1359 | return [ |
| 1190 | '# 当前所在分支', | 1360 | '# 当前所在分支', |
| 1191 | - microStepContract(), | 1361 | + microStepContract(c.root), |
| 1192 | '', | 1362 | '', |
| 1193 | - `跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。`, | ||
| 1194 | - '## 输出(BRANCH_NAME_SCHEMA)', | 1363 | + `跑 \`git -C ${c.root} rev-parse --abbrev-ref HEAD\`。`, |
| 1364 | + '## 输出(DEFAULT_BRANCH_SCHEMA)', | ||
| 1195 | '- `{ "branch": "<stdout 第一行去空白>" }`', | 1365 | '- `{ "branch": "<stdout 第一行去空白>" }`', |
| 1196 | ].join('\n') | 1366 | ].join('\n') |
| 1197 | } | 1367 | } |
| 1198 | 1368 | ||
| 1199 | // ── 微步骤:分支生命周期 action ── | 1369 | // ── 微步骤:分支生命周期 action ── |
| 1200 | -function checkoutExistingBranchPromptM(branch) { | 1370 | +function checkoutExistingBranchPromptM(branch, c) { |
| 1201 | return [ | 1371 | return [ |
| 1202 | `# 切到已存在的本地分支 \`${branch}\``, | 1372 | `# 切到已存在的本地分支 \`${branch}\``, |
| 1203 | - microStepContract(), | 1373 | + microStepContract(c.root), |
| 1204 | '', | 1374 | '', |
| 1205 | - `跑 \`git -C ${ROOT} checkout ${branch}- `跑 \`git -C ${ROOT。`, | 1375 | + `跑 \`git -C ${c.root} checkout ${branch}+ `跑 \`git -C ${c.root。`, |
| 1206 | '## 输出(ACTION_RESULT_SCHEMA)', | 1376 | '## 输出(ACTION_RESULT_SCHEMA)', |
| 1207 | '- 成功:`{ "success": true }`', | 1377 | '- 成功:`{ "success": true }`', |
| 1208 | '- 失败:`{ "success": false, "error": "<stderr 摘要>" }`', | 1378 | '- 失败:`{ "success": false, "error": "<stderr 摘要>" }`', |
| 1209 | ].join('\n') | 1379 | ].join('\n') |
| 1210 | } | 1380 | } |
| 1211 | 1381 | ||
| 1212 | -function createBranchFromPromptM(fromBranch, newBranch) { | 1382 | +function createBranchFromPromptM(fromBranch, newBranch, c) { |
| 1213 | return [ | 1383 | return [ |
| 1214 | `# 从 \`${fromBranch}\` 新建并切到 \`${newBranch}\``, | 1384 | `# 从 \`${fromBranch}\` 新建并切到 \`${newBranch}\``, |
| 1215 | - microStepContract(), | 1385 | + microStepContract(c.root), |
| 1216 | '', | 1386 | '', |
| 1217 | - `按序跑:\`git -C ${ROOT} checkout ${fromBranch}\`,然后 \`git -C ${ROOT} checkout -b ${newBranch}- `按序跑:\`git -C ${ROOT} checkout ${fromBranch}\`,然后 \`git -C ${ROOT。`, | 1387 | + `按序跑:\`git -C ${c.root} checkout ${fromBranch}\`,然后 \`git -C ${c.root} checkout -b ${newBranch}+ `按序跑:\`git -C ${c.root} checkout ${fromBranch}\`,然后 \`git -C ${c.root。`, |
| 1218 | '## 输出(ACTION_RESULT_SCHEMA)', | 1388 | '## 输出(ACTION_RESULT_SCHEMA)', |
| 1219 | '- 全成功:`{ "success": true }`;任一失败:`{ "success": false, "error": "<which step + stderr>" }`', | 1389 | '- 全成功:`{ "success": true }`;任一失败:`{ "success": false, "error": "<which step + stderr>" }`', |
| 1220 | ].join('\n') | 1390 | ].join('\n') |
| 1221 | } | 1391 | } |
| 1222 | 1392 | ||
| 1393 | +// ── lane 物理隔离基础设施(Phase C:worktree 生命周期 + migration 版本段 + lane 测试库)── | ||
| 1394 | +// 启用时机:Phase D 波次执行器在 ≥2 宽波次的前置串行段调用(读 maxV / 读 schema 基底 / 探测支持 / | ||
| 1395 | +// 建 lane),本阶段只提供确定性原语;串行 / 主根路径(c.lane == null)不触达其中任何一个,行为不变。 | ||
| 1396 | + | ||
| 1397 | +// lane worktree 路径约定(Task 6 Step 1):主根同级的 `<ROOT>-lanes/<moduleId>`——由 module id | ||
| 1398 | +// 确定性导出(运行时禁时间/随机源),落在 ROOT 之外(不脏主树、不被主根 status / 脏树恢复误扫)。 | ||
| 1399 | +function laneRoot(moduleId) { return `${ROOT}-lanes/${moduleId}` } | ||
| 1400 | + | ||
| 1401 | +// migration 版本段基址(Task 7 Step 2):lane 0 得下一个整百段、lane 1 再下一段——确定性、零随机。 | ||
| 1402 | +// maxV 必须取"主根文件 ∪ 全部 module-* 分支"的全局并集口径(maxMigrationVersionPromptM,附录补 4), | ||
| 1403 | +// 由此 halt-resume 后 laneIdx 漂移**无害**:每个新波次的段基址恒高于一切已知版本(含未合并分支已占 | ||
| 1404 | +// 用的段),同段绝不二次发放,只产生无害的段空洞(独立模块 migration 互不引用 + lane 库从零重建 + | ||
| 1405 | +// 合并后全量按版本序 apply,空洞与乱序均安全——段规则文案见 tddPrompt)。 | ||
| 1406 | +function vBase(maxV, laneIdx) { return (Math.floor(maxV / 100) + 1 + laneIdx) * 100 } | ||
| 1407 | + | ||
| 1408 | +// lane 测试库命名(附录补 8,序号制统一口径):`<schema>_lane<N>`。固定短后缀避开 moduleId 进 | ||
| 1409 | +// schema 名的三个坑:64 字符上限的截断规则、截断后长前缀撞名重新互相清库、`-` 字符的 MySQL 标识符 | ||
| 1410 | +// 转义。N = 波次内 lane 序号(波次严格串行执行,不存在并发撞名)。波次执行器读 config-vars.yaml 的 | ||
| 1411 | +// database.schema(readDbSchemaPromptM)后经本函数拼成**成品**放 c.dbSchema——prompt 只渲染成品 | ||
| 1412 | +// 字符串,绝不让子代理自己拼(附录补 6c)。 | ||
| 1413 | +function laneDbSchema(baseSchema, laneIdx) { return `${baseSchema}_lane${laneIdx}` } | ||
| 1414 | + | ||
| 1415 | +// feature lane 路径约定(Phase F Task 13):`<模块工作树根>-feats/<featureId>`——由 feature id | ||
| 1416 | +// 确定性导出(id 已经 assertSafeId 校验,路径安全)。串行主根模式 = `<ROOT>-feats/<id>`;模块 | ||
| 1417 | +// lane 模式 = `<ROOT>-lanes/<m>-feats/<id>`,后者带 `<ROOT>-lanes/` 前缀会被 listLaneWorktrees | ||
| 1418 | +// 枚举到,但其分支为 feat/*——占用感知只按 module-*/frontend-phase 分支匹配,互不干扰。 | ||
| 1419 | +function featureLaneRoot(moduleRoot, id) { return `${moduleRoot}-feats/${id}` } | ||
| 1420 | + | ||
| 1421 | +// createLaneWorktreePromptM(Task 6 Step 2):建/复用 lane worktree,幂等(resume 安全)。 | ||
| 1422 | +// worktree add 在主仓库(ROOT)上执行;每个 worktree 独立 index,跨 lane 并行 commit 不争 | ||
| 1423 | +// .git/index.lock。分支语义与主根路径完全一致(module-<id> / frontend-phase),defaultBranch 由 | ||
| 1424 | +// 调用方传 detectDefaultBranchPromptM 的结果——不让子代理自行探测,保持单一真值来源。 | ||
| 1425 | +function createLaneWorktreePromptM(branch, path, defaultBranch) { | ||
| 1426 | + return [ | ||
| 1427 | + `# 建立 lane worktree \`${path}\`(分支 \`${branch}\`,幂等)`, | ||
| 1428 | + microStepContract(ROOT), | ||
| 1429 | + '', | ||
| 1430 | + '按下列三分支判定后执行(全部幂等,resume 安全):', | ||
| 1431 | + `1. **path 已注册**:跑 \`git -C ${ROOT} worktree list --porcelain\`,按行解析各条目的 \`worktree <path>\` 行——路径字段**全等于** \`${path}\` 才算已注册(**绝不**用子串包含判断:lane id 互为前缀时会误匹配兄弟树)→ 校验该树的 HEAD 分支:跑 \`git -C ${path} rev-parse --abbrev-ref HEAD\`,等于 \`${branch}\` → 直接返回成功(resume 复用);**不等** → 返回失败(error 写明实际分支;**绝不**自行强切 / 删树,留给上层处置)。`, | ||
| 1432 | + `2. path 未注册、分支已存在(\`git -C ${ROOT} rev-parse --verify refs/heads/${branch}\` exit=0)→ 跑 \`git -C ${ROOT} worktree add ${path} ${branch}\`。`, | ||
| 1433 | + `3. path 未注册、分支不存在 → 跑 \`git -C ${ROOT} worktree add -b ${branch} ${path} ${defaultBranch}\`。`, | ||
| 1434 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1435 | + '- 成功(含 resume 复用):`{ "success": true, "detail": "<reused|added|added-new-branch>" }`', | ||
| 1436 | + '- 失败:`{ "success": false, "error": "<分支不符 / git stderr 摘要>" }`', | ||
| 1437 | + ].join('\n') | ||
| 1438 | +} | ||
| 1439 | + | ||
| 1440 | +// removeLaneWorktreePromptM(Task 6 Step 2/4):移除 lane worktree,幂等。脏树**禁用 --force**—— | ||
| 1441 | +// 脏 lane 说明有未收口的工作(理应已被 runModule 的 lane 收口段提交干净),照实返回失败留给人工, | ||
| 1442 | +// 绝不静默丢工作。调用方一律 bestEffortAction:删失败只 log(分支/对象库全仓共享,留树不损正确性)。 | ||
| 1443 | +function removeLaneWorktreePromptM(path) { | ||
| 1444 | + return [ | ||
| 1445 | + `# 移除 lane worktree \`${path}\`(幂等;脏树禁 --force)`, | ||
| 1446 | + microStepContract(ROOT), | ||
| 1447 | + '', | ||
| 1448 | + `1. 注册判定:跑 \`git -C ${ROOT} worktree list --porcelain\`,按行解析各条目的 \`worktree <path>\` 行——路径字段**全等于** \`${path}\` 才算已注册(**绝不**用子串包含判断:lane 路径是其 feature 树路径的严格前缀,feature 树残留会让已删的 lane 误判"仍注册",把幂等成功变成 spurious 失败)。未注册 → 本就不存在,直接返回成功(幂等)。`, | ||
| 1449 | + `2. 否则跑 \`git -C ${ROOT} worktree remove ${path}\`。**绝不**加 \`--force\`:树脏 / 含未提交改动时该命令会失败——照实返回失败并把 stderr 摘要写进 error(lane 留给人工取证)。`, | ||
| 1450 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1451 | + '- 成功 / 本就不存在:`{ "success": true }`', | ||
| 1452 | + '- 失败(脏树等):`{ "success": false, "error": "<stderr 摘要>" }`', | ||
| 1453 | + ].join('\n') | ||
| 1454 | +} | ||
| 1455 | + | ||
| 1456 | +// maxMigrationVersionPromptM(Task 7 Step 1):读全仓已知最大 migration 版本号——**全局并集口径** | ||
| 1457 | +// (附录补 4)。只看主根文件不够:halt 波次收敛 + 保留 lane 意味着 halted 模块的 V 文件已 commit 在 | ||
| 1458 | +// 未合并的 module-<id> 分支上、对主根 ls 不可见,漏看会把同一版本段二次发放给别的模块(两分支各写 | ||
| 1459 | +// V<n>,文件名不同 git 合并静默通过,Flyway 在"同版本号多文件"上硬失败)。纯 git 只读、确定性。 | ||
| 1460 | +// 主根专属(波次前置串行段每波读一次,microStepContract(ROOT));FIELD_VALUE_SCHEMA 复用。 | ||
| 1461 | +function maxMigrationVersionPromptM() { | ||
| 1462 | + return [ | ||
| 1463 | + '# 读全仓最大 migration 版本号(主根文件 ∪ 全部 module-* 分支,全局并集)', | ||
| 1464 | + microStepContract(ROOT), | ||
| 1465 | + '', | ||
| 1466 | + `1. 主根文件:列出 \`${ROOT}/sql/migrations/V*.sql\`(Glob 即可)。`, | ||
| 1467 | + `2. 分支并集:\`git -C ${ROOT} for-each-ref --format='%(refname:short)' refs/heads/module-*\` 列出全部模块分支;对**每个**分支跑 \`git -C ${ROOT} ls-tree -r --name-only <branch> -- sql/migrations\`。`, | ||
| 1468 | + '3. 对以上全部文件名解析 `V<n>__*.sql` 的 `<n>`(十进制整数),取**全体最大值**。', | ||
| 1469 | + '## 输出(FIELD_VALUE_SCHEMA)', | ||
| 1470 | + '- 有任何 migration 文件:`{ "found": true, "value": "<最大版本号十进制字符串,如 "12">" }`', | ||
| 1471 | + '- 全仓(含全部 module-* 分支)无任何 V*.sql:`{ "found": true, "value": "0" }`', | ||
| 1472 | + '- 命令失败无法判定:`{ "found": false, "value": "" }`(调用方按失败降级处理,**不要**猜值)。', | ||
| 1473 | + ].join('\n') | ||
| 1474 | +} | ||
| 1475 | + | ||
| 1476 | +// readDbSchemaPromptM(Task 8 Step 2):读 config-vars.yaml 的 database.schema 字面量——lane 库名 | ||
| 1477 | +// 基底。主根专属(波次前置串行段读一次);拼接由 JS 的 laneDbSchema 完成,子代理只取字面值。 | ||
| 1478 | +function readDbSchemaPromptM() { | ||
| 1479 | + return [ | ||
| 1480 | + '# 读 config-vars.yaml 的 database.schema(lane 测试库名基底)', | ||
| 1481 | + microStepContract(ROOT), | ||
| 1482 | + '', | ||
| 1483 | + `Read \`${ROOT}/config-vars.yaml\`,定位 \`database\` 段的 \`schema\` 字段,取其字面值(去引号去空白)。`, | ||
| 1484 | + '## 输出(FIELD_VALUE_SCHEMA)', | ||
| 1485 | + '- 命中:`{ "found": true, "value": "<schema 字面值>", "lineNumber": <行号> }`', | ||
| 1486 | + '- 文件或字段不存在:`{ "found": false, "value": "" }`(调用方据此降级单宽,**不要**猜值)。', | ||
| 1487 | + ].join('\n') | ||
| 1488 | +} | ||
| 1489 | + | ||
| 1490 | +// laneDbSupportPromptM(Task 8 Step 3 / 附录补 6b:两点联查替换单 grep):存量项目 lane 测试库 | ||
| 1491 | +// 支持探测。只 grep scripts/ 不够——schema 引用若只活在 Node 脚本里,gradle test / bootRun 的 | ||
| 1492 | +// datasource 仍写死共享库,单点探测会假阳性放行(lane 建了库、两条 lane 的 gradle 测试照样互相清库)。 | ||
| 1493 | +// 两点都满足才算支持;任一不满足 → Phase D 波次执行器把本波次降级 maxWidth=1(log 写明原因 + 建议 | ||
| 1494 | +// 重跑 skeleton 升级脚本),**不 halt**——与其并行却在共享库上互相清库,不如明示串行。主根专属、只读。 | ||
| 1495 | +function laneDbSupportPromptM() { | ||
| 1496 | + return [ | ||
| 1497 | + '# 探测目标项目是否支持 lane 测试库(ERP_TEST_DB_SCHEMA 两点联查,只读)', | ||
| 1498 | + microStepContract(ROOT), | ||
| 1499 | + '', | ||
| 1500 | + `1. \`scriptsEnv\`:Grep \`${ROOT}/scripts/\` 是否含字符串 \`ERP_TEST_DB_SCHEMA\`(setup-test-db / test / seed-demo-data 脚本支持 env 覆盖 schema)。`, | ||
| 1501 | + `2. \`ymlPlaceholder\`:Grep \`${ROOT}/backend/src/\` 下的 \`application*.yml\` / \`application*.properties\` 是否含 \`\${ERP_TEST_DB_SCHEMA\` 占位(Spring datasource 的 JDBC URL schema 段支持 env 打穿)。`, | ||
| 1502 | + '## 输出(LANE_DB_PROBE_SCHEMA)', | ||
| 1503 | + '- `{ "scriptsEnv": <boolean>, "ymlPlaceholder": <boolean>, "detail": "<命中文件路径摘要 / 未命中说明>" }`——两项独立据实填,**不要**互相推断或合并判断。', | ||
| 1504 | + ].join('\n') | ||
| 1505 | +} | ||
| 1506 | + | ||
| 1507 | +// listLaneWorktreesPromptM(附录补 3a:占用感知枚举,主根专属、只读)。 | ||
| 1508 | +// 只看 `<ROOT>-lanes/` 前缀——主根自身与用户自建的其它 worktree 一概不碰、不上报。 | ||
| 1509 | +function listLaneWorktreesPromptM() { | ||
| 1510 | + return [ | ||
| 1511 | + '# 枚举残留 lane worktree(只读)', | ||
| 1512 | + microStepContract(ROOT), | ||
| 1513 | + '', | ||
| 1514 | + `1. 跑 \`git -C ${ROOT} worktree list --porcelain\`,按空行分组解析每个条目的 \`worktree <path>\` 与 \`branch refs/heads/<name>\` 行。`, | ||
| 1515 | + `2. 只保留 path 以 \`${ROOT}-lanes/\` 开头的条目;branch 取短名(去 \`refs/heads/\` 前缀);detached 条目 branch 填空串。`, | ||
| 1516 | + '## 输出(LANE_LIST_SCHEMA)', | ||
| 1517 | + '- `{ "lanes": [ { "path": "<绝对路径>", "branch": "<分支短名>" }, ... ] }`;无残留 lane → `{ "lanes": [] }`。', | ||
| 1518 | + ].join('\n') | ||
| 1519 | +} | ||
| 1520 | + | ||
| 1521 | +// pruneWorktreesPromptM(计划 D2):清理"目录已删"的 worktree 管理残留。注意(附录补 3 末注): | ||
| 1522 | +// 它对目录完好的 lane(恰是 halt 刻意保留的取证态)毫无作用——占用感知(listLaneWorktrees + 恢复/ | ||
| 1523 | +// 移除)才是收口主力,绝不要依赖 prune 释放被残留 lane 持有的分支。 | ||
| 1524 | +function pruneWorktreesPromptM() { | ||
| 1525 | + return [ | ||
| 1526 | + '# git worktree prune(清理目录已删的管理残留)', | ||
| 1527 | + microStepContract(ROOT), | ||
| 1528 | + '', | ||
| 1529 | + `跑 \`git -C ${ROOT} worktree prune\`。`, | ||
| 1530 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1531 | + '- exit=0 → `{ "success": true }`;失败 → `{ "success": false, "error": "<stderr 摘要>" }`(不要抛错)。', | ||
| 1532 | + ].join('\n') | ||
| 1533 | +} | ||
| 1534 | + | ||
| 1535 | +// writeLaneDbMarkerPromptM(附录补 7:lane schema 的 fail-closed 兜底 marker)。 | ||
| 1536 | +// lane 注入是纯 prompt-only,且 tdd 主会话会把命令转述给二级子会话(提示词链两跳)——任何一跳丢掉 | ||
| 1537 | +// ERP_TEST_DB_SCHEMA 前缀,setup-test-db 就对共享 schema DROP+CREATE(兄弟 lane 随机红测、烧 | ||
| 1538 | +// testGate/仲裁预算甚至假 halt)。最具毁灭性的操作需要确定性兜底:三个脚本模板按 | ||
| 1539 | +// 「env → 本仓库根 .tmp/erp-lane-db → db.schema」顺序取值,lane 副本天然 lane 作用域;主根无此 | ||
| 1540 | +// 文件,串行行为一字不变。`.tmp/` 已被 gitignore 忽略,绝不会被 recoverDirtyWorktree 判 in-scope | ||
| 1541 | +// 自动 commit。明令**禁止**改 lane 副本 config-vars.yaml 的 database.schema 兜底(该文件随 git | ||
| 1542 | +// 提交,lane 树一脏会被自动 commit 并随 milestone 合回默认分支,把真 schema 名搞坏)。幂等(覆盖写)。 | ||
| 1543 | +function writeLaneDbMarkerPromptM(lanePath, schema) { | ||
| 1544 | + return [ | ||
| 1545 | + `# 写 lane 测试库 marker \`${lanePath}/.tmp/erp-lane-db\`(幂等覆盖写)`, | ||
| 1546 | + microStepContract(ROOT), | ||
| 1547 | + '', | ||
| 1548 | + `1. 确保目录 \`${lanePath}/.tmp/\` 存在(不存在则创建)。`, | ||
| 1549 | + `2. 把单行内容 \`${schema}\`(行尾换行,无其它任何内容)**覆盖写**入 \`${lanePath}/.tmp/erp-lane-db\`。`, | ||
| 1550 | + '3. **不 commit**(该文件被 .gitignore 忽略,仅供 lane 内脚本兜底读取);**绝不**改 config-vars.yaml。', | ||
| 1551 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1552 | + '- 成功 → `{ "success": true }`;失败 → `{ "success": false, "error": "<原因>" }`(不要抛错)。', | ||
| 1553 | + ].join('\n') | ||
| 1554 | +} | ||
| 1555 | + | ||
| 1223 | // ── 微步骤:REQ/FE 完成态 git tag(featureLoop dedup 的唯一 ground truth)── | 1556 | // ── 微步骤:REQ/FE 完成态 git tag(featureLoop dedup 的唯一 ground truth)── |
| 1224 | // req-done/<id> 是功能级 git tag,approve 时打一次;featureLoop 入口先 check,存在就 skip, | 1557 | // req-done/<id> 是功能级 git tag,approve 时打一次;featureLoop 入口先 check,存在就 skip, |
| 1225 | // 避免 Router LLM 自审失误导致已 approve 的 REQ 被重新 spec→plan→tdd(撞 V<n>、污染源码)。 | 1558 | // 避免 Router LLM 自审失误导致已 approve 的 REQ 被重新 spec→plan→tdd(撞 V<n>、污染源码)。 |
| 1226 | -function checkReqDoneTagPromptM(id) { | 1559 | +function checkReqDoneTagPromptM(id, c) { |
| 1227 | return [ | 1560 | return [ |
| 1228 | `# tag \`req-done/${id}\` 是否存在(功能级 dedup 真值)`, | 1561 | `# tag \`req-done/${id}\` 是否存在(功能级 dedup 真值)`, |
| 1229 | - microStepContract(), | 1562 | + microStepContract(c.root), |
| 1230 | '', | 1563 | '', |
| 1231 | - `跑 \`git -C ${ROOT} tag -l req-done/${id}- `跑 \`git -C ${ROOT。`, | 1564 | + `跑 \`git -C ${c.root} tag -l req-done/${id}+ `跑 \`git -C ${c.root。`, |
| 1232 | '## 输出(EXISTS_SCHEMA)', | 1565 | '## 输出(EXISTS_SCHEMA)', |
| 1233 | '- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`', | 1566 | '- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`', |
| 1234 | ].join('\n') | 1567 | ].join('\n') |
| 1235 | } | 1568 | } |
| 1236 | 1569 | ||
| 1237 | -function createReqDoneTagPromptM(id, phase) { | 1570 | +function createReqDoneTagPromptM(id, phase, c) { |
| 1238 | return [ | 1571 | return [ |
| 1239 | `# 打 annotated tag \`req-done/${id}\`(${phase==='frontend'?'前端 FE':'后端 REQ'} approve 后落地)`, | 1572 | `# 打 annotated tag \`req-done/${id}\`(${phase==='frontend'?'前端 FE':'后端 REQ'} approve 后落地)`, |
| 1240 | - microStepContract(), | 1573 | + microStepContract(c.root), |
| 1241 | '', | 1574 | '', |
| 1242 | - `跑 \`git -C ${ROOT} tag -a req-done/${id} -m "feature(${id}): approved by code-reviewer (phase=${phase})"\`。`, | ||
| 1243 | - `先用 \`git -C ${ROOT} tag -l req-done/${id}\` 检查;已存在则视为成功(幂等)直接返回 success。`, | 1575 | + `跑 \`git -C ${c.root} tag -a req-done/${id} -m "feature(${id}): approved by code-reviewer (phase=${phase})"\`。`, |
| 1576 | + `先用 \`git -C ${c.root} tag -l req-done/${id}\` 检查;已存在则视为成功(幂等)直接返回 success。`, | ||
| 1244 | '## 输出(ACTION_RESULT_SCHEMA)', | 1577 | '## 输出(ACTION_RESULT_SCHEMA)', |
| 1245 | '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`', | 1578 | '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`', |
| 1246 | ].join('\n') | 1579 | ].join('\n') |
| 1247 | } | 1580 | } |
| 1248 | 1581 | ||
| 1582 | +// ── 微步骤:批量预生成 spec 的统一提交(Phase E Task 11)── | ||
| 1583 | +// batch 模式 spec 只写盘不 commit(避免同一工作树并行 commit 争 .git/index.lock);全部预生成 | ||
| 1584 | +// 落盘后由本微步骤一次性 add+commit 收口。幂等:resume 场景 spec 可能是"复用已提交文件、就地 | ||
| 1585 | +// Edit 后无实际变更"——无 staged 变更直接视为成功,绝不空 commit 报错。 | ||
| 1586 | +function commitPrefetchedSpecsPromptM(ids, phase, c) { | ||
| 1587 | + return [ | ||
| 1588 | + `# 统一提交预生成 spec(${phase},共 ${ids.length} 份)`, | ||
| 1589 | + microStepContract(c.root), | ||
| 1590 | + '', | ||
| 1591 | + `1. 跑 \`git -C ${c.root} add docs/superpowers/specs\`。`, | ||
| 1592 | + `2. 跑 \`git -C ${c.root} diff --cached --quiet\`:exit=0(无 staged 变更)→ 直接返回成功,跳过第 3 步。`, | ||
| 1593 | + `3. 跑 \`git -C ${c.root} commit -m "docs(spec:${phase}): 批量预生成派生规格" -m "ids: ${ids.join('、')}"\`。`, | ||
| 1594 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1595 | + '- 成功 / 无变更:`{ "success": true }`;失败:`{ "success": false, "error": "<stderr 摘要>" }`(不要抛错)。', | ||
| 1596 | + ].join('\n') | ||
| 1597 | +} | ||
| 1598 | + | ||
| 1599 | +// ── 微步骤:feature 级受限并行(Phase F Task 12/13;仅后端 REQ,附录补 2)── | ||
| 1600 | + | ||
| 1601 | +// planFactsPromptM(Task 12):从 plan 提取并行准入事实。保守偏置同调度器:拿不准 schemaChange | ||
| 1602 | +// → true、文件边界不清 → files 空数组——错误串行只损失时间,错误并行损失正确性。模块作用域 | ||
| 1603 | +// (plan 在模块分支上,主根树面可能看不到,microStepContract(c.root))。 | ||
| 1604 | +function planFactsPromptM(planPath, c) { | ||
| 1605 | + return [ | ||
| 1606 | + `# 提取 plan 并行准入事实(只读):\`${planPath}\``, | ||
| 1607 | + microStepContract(c.root), | ||
| 1608 | + '', | ||
| 1609 | + `Read \`${c.root}/${planPath}\`(任务级 TDD 计划),只提取两项事实:`, | ||
| 1610 | + '1. `schemaChange`:plan 是否声明**任何** schema 改动——migration 文件(`sql/migrations/V*.sql`)/ DDL / docs/03 反向更新任务,或任何会写 `sql/` 路径的任务。**拿不准 → `true`**(宁串勿并)。', | ||
| 1611 | + '2. `files`:全部任务的 `impl_file` 与 `test_file` 路径**全集**(项目根相对、去重、按 plan 原文字面取,不要归一化 / 猜补)。某任务文件边界模糊 / 给不出明确路径 → **`files` 返回空数组**(调用方会让该 feature 留在串行链)。', | ||
| 1612 | + '## 输出(PLAN_FACTS_SCHEMA)', | ||
| 1613 | + '- `{ "schemaChange": <boolean>, "files": ["backend/src/.../Foo.java", ...], "detail": "<一句提取依据>" }`;不要返回额外字段。', | ||
| 1614 | + ].join('\n') | ||
| 1615 | +} | ||
| 1616 | + | ||
| 1617 | +// createFeatureWorktreePromptM(Task 13):建/复用 feature worktree——复用 createLaneWorktreePromptM | ||
| 1618 | +// 的幂等三分支形态,差异仅两点:基分支是**模块分支**(feature 从模块分支 HEAD 分叉,不是默认分支)、 | ||
| 1619 | +// git 操作经 c.root(worktree 命令对任意同仓工作树等效,模块作用域保持穿线纪律)。 | ||
| 1620 | +function createFeatureWorktreePromptM(featBranch, path, baseBranch, c) { | ||
| 1621 | + return [ | ||
| 1622 | + `# 建立 feature worktree \`${path}\`(分支 \`${featBranch}\` 自 \`${baseBranch}\`,幂等)`, | ||
| 1623 | + microStepContract(c.root), | ||
| 1624 | + '', | ||
| 1625 | + '按下列三分支判定后执行(全部幂等,resume 安全):', | ||
| 1626 | + `1. **path 已注册**:跑 \`git -C ${c.root} worktree list --porcelain\`,按行解析各条目的 \`worktree <path>\` 行——路径字段**全等于** \`${path}\` 才算已注册(**绝不**用子串包含判断:feature id 互为前缀时会误匹配兄弟树)→ 校验该树 HEAD:跑 \`git -C ${path} rev-parse --abbrev-ref HEAD\`,等于 \`${featBranch}\` → 直接返回成功(resume 复用);**不等** → 返回失败(error 写实际分支;**绝不**自行强切 / 删树)。`, | ||
| 1627 | + `2. path 未注册、分支已存在(\`git -C ${c.root} rev-parse --verify refs/heads/${featBranch}\` exit=0)→ 跑 \`git -C ${c.root} worktree add ${path} ${featBranch}\`。`, | ||
| 1628 | + `3. path 未注册、分支不存在 → 跑 \`git -C ${c.root} worktree add -b ${featBranch} ${path} ${baseBranch}\`。`, | ||
| 1629 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1630 | + '- 成功(含 resume 复用):`{ "success": true, "detail": "<reused|added|added-new-branch>" }`', | ||
| 1631 | + '- 失败:`{ "success": false, "error": "<分支不符 / git stderr 摘要>" }`', | ||
| 1632 | + ].join('\n') | ||
| 1633 | +} | ||
| 1634 | + | ||
| 1635 | +// mergeFeatureBranchPromptM(Task 13):批后把 feature 分支串行合并回模块分支。冲突处置与 | ||
| 1636 | +// milestone 合并的"留树给人工"**刻意相反**:准入已保证文件集不相交,冲突本身即 facts 提取失真 | ||
| 1637 | +// 的信号,计划钉死了自动恢复路径(丢弃 feature 分支 → 回模块分支串行重跑全链,不 adjudicate)—— | ||
| 1638 | +// abort 恢复干净模块分支正是该路径的前置,树面没有人工要救的东西。 | ||
| 1639 | +function mergeFeatureBranchPromptM(featBranch, moduleBranch, id, c) { | ||
| 1640 | + return [ | ||
| 1641 | + `# 把 feature 分支 \`${featBranch}\` 合并回模块分支 \`${moduleBranch}\``, | ||
| 1642 | + microStepContract(c.root), | ||
| 1643 | + '', | ||
| 1644 | + `0. **前置校验**:跑 \`git -C ${c.root} rev-parse --abbrev-ref HEAD\`,必须等于 \`${moduleBranch}\`;不等 → 直接返回失败(error 写实际分支,**不要**自行 checkout)。`, | ||
| 1645 | + `1. 跑 \`git -C ${c.root} merge --no-ff ${featBranch} -m "merge(feat:${id}): integrate ${featBranch}"\`。`, | ||
| 1646 | + `2. **合并冲突** → 先跑 \`git -C ${c.root} merge --abort\` 恢复干净模块分支(调用方要在它上面串行重跑),再返回 \`{ "success": false, "error": "merge-conflict", "detail": "<冲突文件清单,换行分隔>" }\`。`, | ||
| 1647 | + '3. 其它失败照实返回 failure(不要重试、不要改文件)。', | ||
| 1648 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1649 | + '- 成功:`{ "success": true }`;失败:`{ "success": false, "error": "<merge-conflict 或 stderr 摘要>", "detail": "<冲突文件 / stderr 前 30 行>" }`', | ||
| 1650 | + ].join('\n') | ||
| 1651 | +} | ||
| 1652 | + | ||
| 1653 | +// deleteFeatureBranchPromptM(Task 13):删除 feature 分支,幂等。-D 强删的依据:合并成功路径上 | ||
| 1654 | +// 该分支已并入模块分支(无独有 commit);丢弃路径则是有意弃置(冲突 = facts 失真,串行重跑会在 | ||
| 1655 | +// 模块分支重做)——两条调用路径都不需要 -d 的"未合并"安全网。 | ||
| 1656 | +function deleteFeatureBranchPromptM(featBranch, c) { | ||
| 1657 | + return [ | ||
| 1658 | + `# 删除 feature 分支 \`${featBranch}\`(幂等)`, | ||
| 1659 | + microStepContract(c.root), | ||
| 1660 | + '', | ||
| 1661 | + `1. \`git -C ${c.root} rev-parse --verify refs/heads/${featBranch}\` 非 0(分支不存在)→ 直接返回成功(幂等)。`, | ||
| 1662 | + `2. 否则跑 \`git -C ${c.root} branch -D ${featBranch}\`。`, | ||
| 1663 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1664 | + '- 成功 / 本就不存在:`{ "success": true }`;失败:`{ "success": false, "error": "<stderr 摘要>" }`', | ||
| 1665 | + ].join('\n') | ||
| 1666 | +} | ||
| 1667 | + | ||
| 1249 | // ── 微步骤:milestone 专用 ── | 1668 | // ── 微步骤:milestone 专用 ── |
| 1250 | function checkAlreadyMergedPromptM(branch, defaultBranch) { | 1669 | function checkAlreadyMergedPromptM(branch, defaultBranch) { |
| 1251 | return [ | 1670 | return [ |
| 1252 | `# \`${branch}\` 是否已合入 \`${defaultBranch}\``, | 1671 | `# \`${branch}\` 是否已合入 \`${defaultBranch}\``, |
| 1253 | - microStepContract(), | 1672 | + microStepContract(ROOT), |
| 1254 | '', | 1673 | '', |
| 1255 | `先跑 \`git -C ${ROOT} checkout ${defaultBranch}\` 确保 HEAD 在 ${defaultBranch};然后跑 \`git -C ${ROOT} merge-base --is-ancestor ${branch} HEAD\`。`, | 1674 | `先跑 \`git -C ${ROOT} checkout ${defaultBranch}\` 确保 HEAD 在 ${defaultBranch};然后跑 \`git -C ${ROOT} merge-base --is-ancestor ${branch} HEAD\`。`, |
| 1256 | '## 输出(ALREADY_MERGED_SCHEMA)', | 1675 | '## 输出(ALREADY_MERGED_SCHEMA)', |
| @@ -1263,7 +1682,7 @@ function checkAlreadyMergedPromptM(branch, defaultBranch) { | @@ -1263,7 +1682,7 @@ function checkAlreadyMergedPromptM(branch, defaultBranch) { | ||
| 1263 | function executeMergePromptM(defaultBranch, branch, phaseId) { | 1682 | function executeMergePromptM(defaultBranch, branch, phaseId) { |
| 1264 | return [ | 1683 | return [ |
| 1265 | `# 把 \`${branch}\` 合并进 \`${defaultBranch}\`(已确认尚未合入,已在默认分支)`, | 1684 | `# 把 \`${branch}\` 合并进 \`${defaultBranch}\`(已确认尚未合入,已在默认分支)`, |
| 1266 | - microStepContract(), | 1685 | + microStepContract(ROOT), |
| 1267 | '', | 1686 | '', |
| 1268 | `跑 \`git -C ${ROOT} merge --no-ff ${branch} -m "merge(${phaseId}): integrate ${branch}"\`。`, | 1687 | `跑 \`git -C ${ROOT} merge --no-ff ${branch} -m "merge(${phaseId}): integrate ${branch}"\`。`, |
| 1269 | '- 成功 → `{ "success": true }`', | 1688 | '- 成功 → `{ "success": true }`', |
| @@ -1286,7 +1705,7 @@ function readDocs08FieldPromptM(fe, id) { | @@ -1286,7 +1705,7 @@ function readDocs08FieldPromptM(fe, id) { | ||
| 1286 | : `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\`` | 1705 | : `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\`` |
| 1287 | return [ | 1706 | return [ |
| 1288 | title, | 1707 | title, |
| 1289 | - microStepContract(), | 1708 | + microStepContract(ROOT), |
| 1290 | '', | 1709 | '', |
| 1291 | locator, | 1710 | locator, |
| 1292 | '## 输出(FIELD_VALUE_SCHEMA)', | 1711 | '## 输出(FIELD_VALUE_SCHEMA)', |
| @@ -1306,7 +1725,7 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { | @@ -1306,7 +1725,7 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { | ||
| 1306 | : `严禁全局替换:通过定位上下文(${fe ? '§ 三' : `§ 二 中 module_id == \`${id}\` 的 bullet 段`})找到该 bullet 的 \`里程碑\` 子项行,仅替换这一行。` | 1725 | : `严禁全局替换:通过定位上下文(${fe ? '§ 三' : `§ 二 中 module_id == \`${id}\` 的 bullet 段`})找到该 bullet 的 \`里程碑\` 子项行,仅替换这一行。` |
| 1307 | return [ | 1726 | return [ |
| 1308 | `# 把 docs/08 ${scope} 从 \`—\` 改为 \`${targetValue}\` 并 commit`, | 1727 | `# 把 docs/08 ${scope} 从 \`—\` 改为 \`${targetValue}\` 并 commit`, |
| 1309 | - microStepContract(), | 1728 | + microStepContract(ROOT), |
| 1310 | '', | 1729 | '', |
| 1311 | `调用方已确认字段当前值 = \`—\`(你不必再读一遍)。`, | 1730 | `调用方已确认字段当前值 = \`—\`(你不必再读一遍)。`, |
| 1312 | `1. ${lineGuard} Edit \`${ROOT}/docs/08-模块任务管理.md\`:把整行 \`${oldStr}\` 替换为 \`${newStr}\`(精确字符串替换;**只动一处**)。`, | 1731 | `1. ${lineGuard} Edit \`${ROOT}/docs/08-模块任务管理.md\`:把整行 \`${oldStr}\` 替换为 \`${newStr}\`(精确字符串替换;**只动一处**)。`, |
| @@ -1318,15 +1737,15 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { | @@ -1318,15 +1737,15 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { | ||
| 1318 | } | 1737 | } |
| 1319 | 1738 | ||
| 1320 | // ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect;read-then-write)── | 1739 | // ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect;read-then-write)── |
| 1321 | -function readDocs08CheckboxPromptM(fe, id) { | 1740 | +function readDocs08CheckboxPromptM(fe, id, c) { |
| 1322 | const section = fe ? '§ 三' : '§ 二' | 1741 | const section = fe ? '§ 三' : '§ 二' |
| 1323 | const kind = fe ? '功能' : 'REQ' | 1742 | const kind = fe ? '功能' : 'REQ' |
| 1324 | const locator = fe | 1743 | const locator = fe |
| 1325 | - ? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。` | ||
| 1326 | - : `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section},找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。` | 1744 | + ? `Read \`${c.root}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。` |
| 1745 | + : `Read \`${c.root}/docs/08-模块任务管理.md\`,定位 ${section},找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。` | ||
| 1327 | return [ | 1746 | return [ |
| 1328 | `# 读 docs/08 ${section} ${kind} \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`, | 1747 | `# 读 docs/08 ${section} ${kind} \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`, |
| 1329 | - microStepContract(), | 1748 | + microStepContract(c.root), |
| 1330 | '', | 1749 | '', |
| 1331 | locator, | 1750 | locator, |
| 1332 | '## 输出(CHECKBOX_STATE_SCHEMA)', | 1751 | '## 输出(CHECKBOX_STATE_SCHEMA)', |
| @@ -1336,19 +1755,19 @@ function readDocs08CheckboxPromptM(fe, id) { | @@ -1336,19 +1755,19 @@ function readDocs08CheckboxPromptM(fe, id) { | ||
| 1336 | ].join('\n') | 1755 | ].join('\n') |
| 1337 | } | 1756 | } |
| 1338 | 1757 | ||
| 1339 | -function writeDocs08CheckboxPromptM(fe, id, phase, lineNumber) { | 1758 | +function writeDocs08CheckboxPromptM(fe, id, phase, lineNumber, c) { |
| 1340 | const scope = fe ? `§ 三 功能 ${id}` : `§ 二 REQ ${id}` | 1759 | const scope = fe ? `§ 三 功能 ${id}` : `§ 二 REQ ${id}` |
| 1341 | const lineGuard = (typeof lineNumber === 'number' && Number.isFinite(lineNumber)) | 1760 | const lineGuard = (typeof lineNumber === 'number' && Number.isFinite(lineNumber)) |
| 1342 | - ? `先 Read \`${ROOT}/docs/08-模块任务管理.md- ? `先 Read \`${ROOT 第 ${lineNumber} 行(1-based),确认该行去掉行首空白后以 - ? `先 Read \`${ROOT- [ ] ${id} - ? `先 Read \`${ROOT 开头;不满足则返回 - ? `先 Read \`${ROOT{success:false, error:"line-${lineNumber}-mismatch: actual=<actual>"}- ? `先 Read \`${ROOT。然后只替换第 ${lineNumber} 行的第一个 - ? `先 Read \`${ROOT[ ]- ? `先 Read \`${ROOT 为 - ? `先 Read \`${ROOT[x]- ? `先 Read \`${ROOT,保留缩进与 id 之后的全部文本。` | 1761 | + ? `先 Read \`${c.root}/docs/08-模块任务管理.md+ ? `先 Read \`${c.root 第 ${lineNumber} 行(1-based),确认该行去掉行首空白后以 + ? `先 Read \`${c.root- [ ] ${id} + ? `先 Read \`${c.root 开头;不满足则返回 + ? `先 Read \`${c.root{success:false, error:"line-${lineNumber}-mismatch: actual=<actual>"}+ ? `先 Read \`${c.root。然后只替换第 ${lineNumber} 行的第一个 + ? `先 Read \`${c.root[ ]+ ? `先 Read \`${c.root 为 + ? `先 Read \`${c.root[x]+ ? `先 Read \`${c.root,保留缩进与 id 之后的全部文本。` |
| 1343 | : `定位 docs/08 ${scope} 中去掉行首空白后以 \`- [ ] ${id} \` 开头的唯一一行,只替换该行第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。` | 1762 | : `定位 docs/08 ${scope} 中去掉行首空白后以 \`- [ ] ${id} \` 开头的唯一一行,只替换该行第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。` |
| 1344 | return [ | 1763 | return [ |
| 1345 | `# 把 docs/08 ${scope} 的 \`[ ]\` 勾选为 \`[x]\` 并 commit`, | 1764 | `# 把 docs/08 ${scope} 的 \`[ ]\` 勾选为 \`[x]\` 并 commit`, |
| 1346 | - microStepContract(), | 1765 | + microStepContract(c.root), |
| 1347 | '', | 1766 | '', |
| 1348 | `调用方已读到状态 = \`unchecked\`(你不必再读一遍)。`, | 1767 | `调用方已读到状态 = \`unchecked\`(你不必再读一遍)。`, |
| 1349 | `1. ${lineGuard}`, | 1768 | `1. ${lineGuard}`, |
| 1350 | - `2. 跑 \`git -C ${ROOT} add docs/08-模块任务管理.md\`。`, | ||
| 1351 | - `3. 跑 \`git -C ${ROOT} commit -m "chore(${phase}:${id}): mark ${id} approved in docs/08"\`。`, | 1769 | + `2. 跑 \`git -C ${c.root} add docs/08-模块任务管理.md\`。`, |
| 1770 | + `3. 跑 \`git -C ${c.root} commit -m "chore(${phase}:${id}): mark ${id} approved in docs/08"\`。`, | ||
| 1352 | '## 输出(ACTION_RESULT_SCHEMA)', | 1771 | '## 输出(ACTION_RESULT_SCHEMA)', |
| 1353 | '- 三步全 OK:`{ "success": true }`;任一失败:`{ "success": false, "error": "<step + reason>" }`', | 1772 | '- 三步全 OK:`{ "success": true }`;任一失败:`{ "success": false, "error": "<step + reason>" }`', |
| 1354 | ].join('\n') | 1773 | ].join('\n') |
| @@ -1357,7 +1776,7 @@ function writeDocs08CheckboxPromptM(fe, id, phase, lineNumber) { | @@ -1357,7 +1776,7 @@ function writeDocs08CheckboxPromptM(fe, id, phase, lineNumber) { | ||
| 1357 | function checkTagExistsPromptM(tagName) { | 1776 | function checkTagExistsPromptM(tagName) { |
| 1358 | return [ | 1777 | return [ |
| 1359 | `# tag \`${tagName}\` 是否存在`, | 1778 | `# tag \`${tagName}\` 是否存在`, |
| 1360 | - microStepContract(), | 1779 | + microStepContract(ROOT), |
| 1361 | '', | 1780 | '', |
| 1362 | `跑 \`git -C ${ROOT} tag -l ${tagName}\`。`, | 1781 | `跑 \`git -C ${ROOT} tag -l ${tagName}\`。`, |
| 1363 | '## 输出(EXISTS_SCHEMA)', | 1782 | '## 输出(EXISTS_SCHEMA)', |
| @@ -1368,7 +1787,7 @@ function checkTagExistsPromptM(tagName) { | @@ -1368,7 +1787,7 @@ function checkTagExistsPromptM(tagName) { | ||
| 1368 | function createTagPromptM(phaseId, fe) { | 1787 | function createTagPromptM(phaseId, fe) { |
| 1369 | return [ | 1788 | return [ |
| 1370 | `# 打 annotated tag \`milestone/${phaseId}\``, | 1789 | `# 打 annotated tag \`milestone/${phaseId}\``, |
| 1371 | - microStepContract(), | 1790 | + microStepContract(ROOT), |
| 1372 | '', | 1791 | '', |
| 1373 | `跑 \`git -C ${ROOT} tag -a milestone/${phaseId} -m "milestone(${phaseId}): ${fe ? '前端' : '后端'}阶段完成"\`。`, | 1792 | `跑 \`git -C ${ROOT} tag -a milestone/${phaseId} -m "milestone(${phaseId}): ${fe ? '前端' : '后端'}阶段完成"\`。`, |
| 1374 | '## 输出(ACTION_RESULT_SCHEMA)', | 1793 | '## 输出(ACTION_RESULT_SCHEMA)', |
| @@ -1377,13 +1796,13 @@ function createTagPromptM(phaseId, fe) { | @@ -1377,13 +1796,13 @@ function createTagPromptM(phaseId, fe) { | ||
| 1377 | } | 1796 | } |
| 1378 | 1797 | ||
| 1379 | // fe-skeleton-done:前端骨架占位 stage 的补记 tag;真实完成态以 router/state 检测为准。 | 1798 | // fe-skeleton-done:前端骨架占位 stage 的补记 tag;真实完成态以 router/state 检测为准。 |
| 1380 | -function createFeSkeletonTagPromptM() { | 1799 | +function createFeSkeletonTagPromptM(c) { |
| 1381 | return [ | 1800 | return [ |
| 1382 | '# 打 annotated tag `fe-skeleton-done`(前端骨架占位已建)', | 1801 | '# 打 annotated tag `fe-skeleton-done`(前端骨架占位已建)', |
| 1383 | - microStepContract(), | 1802 | + microStepContract(c.root), |
| 1384 | '', | 1803 | '', |
| 1385 | - `先用 \`git -C ${ROOT} tag -l fe-skeleton-done\` 检查;已存在则视为成功(幂等)直接返回 success。`, | ||
| 1386 | - `否则跑 \`git -C ${ROOT} tag -a fe-skeleton-done -m "chore(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位已建"\`。`, | 1804 | + `先用 \`git -C ${c.root} tag -l fe-skeleton-done\` 检查;已存在则视为成功(幂等)直接返回 success。`, |
| 1805 | + `否则跑 \`git -C ${c.root} tag -a fe-skeleton-done -m "chore(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位已建"\`。`, | ||
| 1387 | '## 输出(ACTION_RESULT_SCHEMA)', | 1806 | '## 输出(ACTION_RESULT_SCHEMA)', |
| 1388 | '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`', | 1807 | '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`', |
| 1389 | ].join('\n') | 1808 | ].join('\n') |
| @@ -1392,7 +1811,7 @@ function createFeSkeletonTagPromptM() { | @@ -1392,7 +1811,7 @@ function createFeSkeletonTagPromptM() { | ||
| 1392 | function findReportPromptM(phaseId) { | 1811 | function findReportPromptM(phaseId) { |
| 1393 | return [ | 1812 | return [ |
| 1394 | `# 找最新的 \`${phaseId}\` 完成报告并读取 § ⑫ 的 milestone tag 字段当前值`, | 1813 | `# 找最新的 \`${phaseId}\` 完成报告并读取 § ⑫ 的 milestone tag 字段当前值`, |
| 1395 | - microStepContract(), | 1814 | + microStepContract(ROOT), |
| 1396 | '', | 1815 | '', |
| 1397 | `用 Glob 在 \`${ROOT}/docs/superpowers/module-reports/\` 查找 \`*-${phaseId}.md\`(按文件名 YYYY-MM-DD 日期前缀降序取最新一份)。`, | 1816 | `用 Glob 在 \`${ROOT}/docs/superpowers/module-reports/\` 查找 \`*-${phaseId}.md\`(按文件名 YYYY-MM-DD 日期前缀降序取最新一份)。`, |
| 1398 | 'Read 该文件,定位 § ⑫("里程碑"小节)。', | 1817 | 'Read 该文件,定位 § ⑫("里程碑"小节)。', |
| @@ -1405,7 +1824,7 @@ function findReportPromptM(phaseId) { | @@ -1405,7 +1824,7 @@ function findReportPromptM(phaseId) { | ||
| 1405 | function updateReportPromptM(reportPath, targetTag, phaseId) { | 1824 | function updateReportPromptM(reportPath, targetTag, phaseId) { |
| 1406 | return [ | 1825 | return [ |
| 1407 | `# 把 \`${reportPath}\` § ⑫ 的 \`{{milestone_tag}}\` 替换为 \`${targetTag}\` 并 commit`, | 1826 | `# 把 \`${reportPath}\` § ⑫ 的 \`{{milestone_tag}}\` 替换为 \`${targetTag}\` 并 commit`, |
| 1408 | - microStepContract(), | 1827 | + microStepContract(ROOT), |
| 1409 | '', | 1828 | '', |
| 1410 | `1. Edit \`${ROOT}/${reportPath}\`:把字面量 \`{{milestone_tag}}\` 替换为 \`${targetTag}\`(精确替换;如多处出现就全部替换)。`, | 1829 | `1. Edit \`${ROOT}/${reportPath}\`:把字面量 \`{{milestone_tag}}\` 替换为 \`${targetTag}\`(精确替换;如多处出现就全部替换)。`, |
| 1411 | `2. \`git -C ${ROOT} add ${reportPath}\`;3. \`git -C ${ROOT} commit -m "docs(${phaseId}): record ${targetTag} in completion report"\`。`, | 1830 | `2. \`git -C ${ROOT} add ${reportPath}\`;3. \`git -C ${ROOT} commit -m "docs(${phaseId}): record ${targetTag} in completion report"\`。`, |
| @@ -1415,25 +1834,25 @@ function updateReportPromptM(reportPath, targetTag, phaseId) { | @@ -1415,25 +1834,25 @@ function updateReportPromptM(reportPath, targetTag, phaseId) { | ||
| 1415 | } | 1834 | } |
| 1416 | 1835 | ||
| 1417 | // ── 微步骤:cross-module 专用 ── | 1836 | // ── 微步骤:cross-module 专用 ── |
| 1418 | -function collectCrossModuleChangedPromptM(defaultBranch) { | 1837 | +function collectCrossModuleChangedPromptM(defaultBranch, c) { |
| 1419 | return [ | 1838 | return [ |
| 1420 | `# 收集功能分支自 \`${defaultBranch}\` 分叉以来的全部改动文件`, | 1839 | `# 收集功能分支自 \`${defaultBranch}\` 分叉以来的全部改动文件`, |
| 1421 | - microStepContract(), | 1840 | + microStepContract(c.root), |
| 1422 | '', | 1841 | '', |
| 1423 | - `跑 \`git -C ${ROOT} diff --name-status ${defaultBranch}...HEAD- `跑 \`git -C ${ROOT(三点 diff)。按行解析每行 - `跑 \`git -C ${ROOT<status>\t<path>- `跑 \`git -C ${ROOT(status 通常为 M/A/D/R/C 等)。`, | 1842 | + `跑 \`git -C ${c.root} diff --name-status ${defaultBranch}...HEAD+ `跑 \`git -C ${c.root(三点 diff)。按行解析每行 + `跑 \`git -C ${c.root<status>\t<path>+ `跑 \`git -C ${c.root(status 通常为 M/A/D/R/C 等)。`, |
| 1424 | '## 输出(CHANGED_FILES_SCHEMA)', | 1843 | '## 输出(CHANGED_FILES_SCHEMA)', |
| 1425 | '- `{ "files": [ { "status": "M", "path": "backend/.../X.java" }, ... ] }`', | 1844 | '- `{ "files": [ { "status": "M", "path": "backend/.../X.java" }, ... ] }`', |
| 1426 | '- diff 为空 → `{ "files": [] }`', | 1845 | '- diff 为空 → `{ "files": [] }`', |
| 1427 | ].join('\n') | 1846 | ].join('\n') |
| 1428 | } | 1847 | } |
| 1429 | 1848 | ||
| 1430 | -function classifyCrossModulePromptM(moduleId, files) { | 1849 | +function classifyCrossModulePromptM(moduleId, files, c) { |
| 1431 | const filesText = files.map(f => `- ${f.status} ${f.path}`).join('\n') | 1850 | const filesText = files.map(f => `- ${f.status} ${f.path}`).join('\n') |
| 1432 | return [ | 1851 | return [ |
| 1433 | `# 把改动文件分类:哪些落在**非本模块 \`${moduleId}\`** 的目录下`, | 1852 | `# 把改动文件分类:哪些落在**非本模块 \`${moduleId}\`** 的目录下`, |
| 1434 | - microStepContract(), | 1853 | + microStepContract(c.root), |
| 1435 | '', | 1854 | '', |
| 1436 | - `本模块目录归属以 \`${ROOT}/docs/08-模块任务管理.md § 二- `本模块目录归属以 \`${ROOT 中本模块 bullet 的 - `本模块目录归属以 \`${ROOT路径:- `本模块目录归属以 \`${ROOT 字段为准。Read 它以建立"路径 → 模块"映射(粒度/分层约定见 docs/04 § 1.2/2.1)。`, | 1855 | + `本模块目录归属以 \`${c.root}/docs/08-模块任务管理.md § 二+ `本模块目录归属以 \`${c.root 中本模块 bullet 的 + `本模块目录归属以 \`${c.root路径:+ `本模块目录归属以 \`${c.root 字段为准。Read 它以建立"路径 → 模块"映射(粒度/分层约定见 docs/04 § 1.2/2.1)。`, |
| 1437 | '', | 1856 | '', |
| 1438 | '## 改动文件清单', | 1857 | '## 改动文件清单', |
| 1439 | filesText, | 1858 | filesText, |
| @@ -1453,11 +1872,11 @@ function classifyCrossModulePromptM(moduleId, files) { | @@ -1453,11 +1872,11 @@ function classifyCrossModulePromptM(moduleId, files) { | ||
| 1453 | // dedup-and-rewrite 不再 append:resume / 多次跑同一模块时,append 会产生重复行污染 § ⑦。 | 1872 | // dedup-and-rewrite 不再 append:resume / 多次跑同一模块时,append 会产生重复行污染 § ⑦。 |
| 1454 | // 改为整体重写:读现有行 → 与本次 items 合并 → 按 (file, targetModule) dedup(本次 items 覆盖旧值) | 1873 | // 改为整体重写:读现有行 → 与本次 items 合并 → 按 (file, targetModule) dedup(本次 items 覆盖旧值) |
| 1455 | // → 按 (targetModule, file) 排序 → 整表重写。commit 前用 `git diff --quiet` 判定,无变更则跳过 commit。 | 1874 | // → 按 (targetModule, file) 排序 → 整表重写。commit 前用 `git diff --quiet` 判定,无变更则跳过 commit。 |
| 1456 | -function writeCrossModuleLogPromptM(moduleId, items) { | 1875 | +function writeCrossModuleLogPromptM(moduleId, items, c) { |
| 1457 | const newRowsJson = JSON.stringify(items, null, 2) | 1876 | const newRowsJson = JSON.stringify(items, null, 2) |
| 1458 | return [ | 1877 | return [ |
| 1459 | `# 把跨模块改动以 dedup-and-rewrite 方式写入 \`docs/superpowers/module-reports/${moduleId}-cross-module.md\``, | 1878 | `# 把跨模块改动以 dedup-and-rewrite 方式写入 \`docs/superpowers/module-reports/${moduleId}-cross-module.md\``, |
| 1460 | - microStepContract(), | 1879 | + microStepContract(c.root), |
| 1461 | '', | 1880 | '', |
| 1462 | `目标文件(项目根相对):\`docs/superpowers/module-reports/${moduleId}-cross-module.md\`。`, | 1881 | `目标文件(项目根相对):\`docs/superpowers/module-reports/${moduleId}-cross-module.md\`。`, |
| 1463 | '', | 1882 | '', |
| @@ -1473,7 +1892,7 @@ function writeCrossModuleLogPromptM(moduleId, items) { | @@ -1473,7 +1892,7 @@ function writeCrossModuleLogPromptM(moduleId, items) { | ||
| 1473 | ' |---|---|---|---|', | 1892 | ' |---|---|---|---|', |
| 1474 | ' <已排序的全部行>', | 1893 | ' <已排序的全部行>', |
| 1475 | ' ```', | 1894 | ' ```', |
| 1476 | - `5. **空变更跳过 commit**:跑 \`git -C ${ROOT} diff --quiet -- docs/superpowers/module-reports/${moduleId}-cross-module.md- `5. **空变更跳过 commit**:跑 \`git -C ${ROOT。`, | 1895 | + `5. **空变更跳过 commit**:跑 \`git -C ${c.root} diff --quiet -- docs/superpowers/module-reports/${moduleId}-cross-module.md+ `5. **空变更跳过 commit**:跑 \`git -C ${c.root。`, |
| 1477 | ' - exit_code = 0(无变更)→ 不要 commit,直接返回 `{ "success": true, "detail": "no-diff-skip-commit" }`。', | 1896 | ' - exit_code = 0(无变更)→ 不要 commit,直接返回 `{ "success": true, "detail": "no-diff-skip-commit" }`。', |
| 1478 | ` - exit_code != 0(有变更)→ \`git add\` + \`git commit -m "chore(${moduleId}): record cross-module log"\`。`, | 1897 | ` - exit_code != 0(有变更)→ \`git add\` + \`git commit -m "chore(${moduleId}): record cross-module log"\`。`, |
| 1479 | '', | 1898 | '', |
| @@ -1489,41 +1908,41 @@ function writeCrossModuleLogPromptM(moduleId, items) { | @@ -1489,41 +1908,41 @@ function writeCrossModuleLogPromptM(moduleId, items) { | ||
| 1489 | } | 1908 | } |
| 1490 | 1909 | ||
| 1491 | // ---- 模块完成报告(原 module-report)---- | 1910 | // ---- 模块完成报告(原 module-report)---- |
| 1492 | -function reportPrompt(module) { | 1911 | +function reportPrompt(module, c) { |
| 1493 | const id = module?.id ?? '<module>' | 1912 | const id = module?.id ?? '<module>' |
| 1494 | const fe = id === 'frontend-phase' | 1913 | const fe = id === 'frontend-phase' |
| 1495 | const phaseId = fe ? 'frontend-phase' : id | 1914 | const phaseId = fe ? 'frontend-phase' : id |
| 1496 | return [ | 1915 | return [ |
| 1497 | `# module-report — ${fe ? '前端阶段' : `模块 ${id}`} 12 节完成报告`, | 1916 | `# module-report — ${fe ? '前端阶段' : `模块 ${id}`} 12 节完成报告`, |
| 1498 | '', | 1917 | '', |
| 1499 | - featureStageContract(fe ? 'frontend' : 'backend'), | 1918 | + featureStageContract(fe ? 'frontend' : 'backend', c), |
| 1500 | '', | 1919 | '', |
| 1501 | '## 目标', | 1920 | '## 目标', |
| 1502 | `test-gate 绿后渲染标准化 **12 节**完成报告,commit 到当前分支(供 milestone 标记)。**只读 git 摘要,不读 diff 正文进上下文。**`, | 1921 | `test-gate 绿后渲染标准化 **12 节**完成报告,commit 到当前分支(供 milestone 标记)。**只读 git 摘要,不读 diff 正文进上下文。**`, |
| 1503 | '', | 1922 | '', |
| 1504 | '## 前置', | 1923 | '## 前置', |
| 1505 | - `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md- `- 验证上游 test-gate 绿:Glob \`${ROOT,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, | 1924 | + `- 验证上游 test-gate 绿:Glob \`${c.root}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md+ `- 验证上游 test-gate 绿:Glob \`${c.root,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, |
| 1506 | fe | 1925 | fe |
| 1507 | - ? `- **验证阶段级行为门绿**:Glob \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md- ? `- **验证阶段级行为门绿**:Glob \`${ROOT,按 behaviorRound → attempt 数字升序读取。**最后一份必须非 RED(绿)**;最后一份 red 或一份证据都没有 → 立即 halt(绝不在行为红 / 无行为证据上打 milestone)。注意 per-FE 行为证据(- ? `- **验证阶段级行为门绿**:Glob \`${ROOTreviews/<date>-<FE>-behavior-*- ? `- **验证阶段级行为门绿**:Glob \`${ROOT)已不再产生,不要校验。` | 1926 | + ? `- **验证阶段级行为门绿**:Glob \`${c.root}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md+ ? `- **验证阶段级行为门绿**:Glob \`${c.root,按 behaviorRound → attempt 数字升序读取。**最后一份必须非 RED(绿)**;最后一份 red 或一份证据都没有 → 立即 halt(绝不在行为红 / 无行为证据上打 milestone)。注意 per-FE 行为证据(+ ? `- **验证阶段级行为门绿**:Glob \`${c.rootreviews/<date>-<FE>-behavior-*+ ? `- **验证阶段级行为门绿**:Glob \`${c.root)已不再产生,不要校验。` |
| 1508 | : '', | 1927 | : '', |
| 1509 | '', | 1928 | '', |
| 1510 | '## 收集输入(取摘要而非正文)', | 1929 | '## 收集输入(取摘要而非正文)', |
| 1511 | fe | 1930 | fe |
| 1512 | ? [ | 1931 | ? [ |
| 1513 | '- § ① `module_id = frontend-phase`,`module_name = 前端阶段(整体)`。', | 1932 | '- § ① `module_id = frontend-phase`,`module_name = 前端阶段(整体)`。', |
| 1514 | - `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, | ||
| 1515 | - `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, | 1933 | + `- § ② "FE 完成清单":扫 \`${c.root}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, |
| 1934 | + `- § ③ 文件变更:\`git -C ${c.root} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, | ||
| 1516 | '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', | 1935 | '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', |
| 1517 | - `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把阶段级行为证据 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(按 behaviorRound → attempt 排序)与行为复验 \`frontend-phase-behavior-reverify-r*.md\` 的 flake / 环境 race(envError)/ 行为 fix 轮数 / 样式 styleIssues 修复轮次 / 文字 continue 记录一并纳入 § ⑤ 汇总**。`, | ||
| 1518 | - `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按阶段级行为证据 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(取最后一份的逐 FE 小节)汇总各 FE 的 \`coverageGaps\` + 样式 \`styleIssues\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, | 1936 | + `- § ⑤:把 \`${c.root}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把阶段级行为证据 \`${c.root}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(按 behaviorRound → attempt 排序)与行为复验 \`frontend-phase-behavior-reverify-r*.md\` 的 flake / 环境 race(envError)/ 行为 fix 轮数 / 样式 styleIssues 修复轮次 / 文字 continue 记录一并纳入 § ⑤ 汇总**。`, |
| 1937 | + `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按阶段级行为证据 \`${c.root}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(取最后一份的逐 FE 小节)汇总各 FE 的 \`coverageGaps\` + 样式 \`styleIssues\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, | ||
| 1519 | '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', | 1938 | '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', |
| 1520 | ].join('\n') | 1939 | ].join('\n') |
| 1521 | : [ | 1940 | : [ |
| 1522 | - `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\` / \`--name-status\` / \`git log <默认分支>..HEAD --oneline\`(区间 = 功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)。`, | ||
| 1523 | - `- § ② / § ⑨:读 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-<本模块 REQ>.md\`。`, | ||
| 1524 | - `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/${id}-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。`, | ||
| 1525 | - `- § ⑥ Migration:\`git -C ${ROOT} diff --name-only --diff-filter=A -- 'sql/migrations/V*.sql'\` 列新增,每个读第一行作说明。`, | ||
| 1526 | - `- § ⑦ 跨模块改动:读 \`${ROOT}/docs/superpowers/module-reports/${id}-cross-module.md\`(如存在;其中不应再有 \`TBD(CC 补)\`,上一步 cross-module-log 已补齐)。`, | 1941 | + `- § ③ 文件变更:\`git -C ${c.root} diff --stat <默认分支 main/master>...HEAD\` / \`--name-status\` / \`git log <默认分支>..HEAD --oneline\`(区间 = 功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)。`, |
| 1942 | + `- § ② / § ⑨:读 \`${c.root}/docs/superpowers/{specs,plans,reviews}/<日期>-<本模块 REQ>.md\`。`, | ||
| 1943 | + `- § ⑤:把 \`${c.root}/docs/superpowers/module-reports/${id}-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。`, | ||
| 1944 | + `- § ⑥ Migration:\`git -C ${c.root} diff --name-only --diff-filter=A -- 'sql/migrations/V*.sql'\` 列新增,每个读第一行作说明。`, | ||
| 1945 | + `- § ⑦ 跨模块改动:读 \`${c.root}/docs/superpowers/module-reports/${id}-cross-module.md\`(如存在;其中不应再有 \`TBD(CC 补)\`,上一步 cross-module-log 已补齐)。`, | ||
| 1527 | '- § ④ 读写的表:grep 定位涉 SQL 文件后按需读片段,**不全量读 docs/03**。', | 1946 | '- § ④ 读写的表:grep 定位涉 SQL 文件后按需读片段,**不全量读 docs/03**。', |
| 1528 | ].join('\n'), | 1947 | ].join('\n'), |
| 1529 | '', | 1948 | '', |
| @@ -1539,7 +1958,12 @@ function reportPrompt(module) { | @@ -1539,7 +1958,12 @@ function reportPrompt(module) { | ||
| 1539 | 1958 | ||
| 1540 | // ---- runBranchSetup:原 branchSetupPrompt 的散文流程 → JS 编排 + 微步骤 agent ---- | 1959 | // ---- runBranchSetup:原 branchSetupPrompt 的散文流程 → JS 编排 + 微步骤 agent ---- |
| 1541 | // 幂等:分支已存在则 checkout,否则从默认分支新建。条件分支由 JS 判定,子代理只负责执行单一动作。 | 1960 | // 幂等:分支已存在则 checkout,否则从默认分支新建。条件分支由 JS 判定,子代理只负责执行单一动作。 |
| 1542 | -async function runBranchSetup(module) { | 1961 | +// c:模块上下文——checkout/HEAD/脏树恢复全部作用于 c.root(串行 = 主根;lane 分叉见下)。 |
| 1962 | +// 默认分支探测保持 ROOT(读 refs,全仓共享,主根专属)。 | ||
| 1963 | +// lane 分叉(Task 6 Step 3):c.lane != null 时不做主根 checkout——`git worktree add` 原子完成 | ||
| 1964 | +// "建树 + 上分支"(分支语义不变:module-<id> / frontend-phase)。波次前置串行段通常已建好 lane, | ||
| 1965 | +// 这里的 createLaneWorktree 是幂等兜底(resume / 单独重派模块时仍能自洽建树)。 | ||
| 1966 | +async function runBranchSetup(module, c) { | ||
| 1543 | const id = module?.id ?? '<module>' | 1967 | const id = module?.id ?? '<module>' |
| 1544 | const fe = id === 'frontend-phase' | 1968 | const fe = id === 'frontend-phase' |
| 1545 | const branch = fe ? 'frontend-phase' : `module-${id}` | 1969 | const branch = fe ? 'frontend-phase' : `module-${id}` |
| @@ -1547,31 +1971,42 @@ async function runBranchSetup(module) { | @@ -1547,31 +1971,42 @@ async function runBranchSetup(module) { | ||
| 1547 | 1971 | ||
| 1548 | const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | 1972 | const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) |
| 1549 | 1973 | ||
| 1974 | + if (c.lane != null) { | ||
| 1975 | + // lane 模式:先确保 lane worktree 存在且在目标分支(幂等三分支见 prompt),树才存在、脏树检查才有意义。 | ||
| 1976 | + await runAction(g => createLaneWorktreePromptM(branch, c.root, def.branch) + g, | ||
| 1977 | + {site:`branchSetup-lane-worktree:${branch}`, grp:'Milestone', label: lbl('lane-add'), dec: c.decisions, root: c.root}) | ||
| 1978 | + } | ||
| 1979 | + | ||
| 1550 | // 工作树脏:先自主恢复(in-scope 残留 → 自动 commit);含越界改动则恢复失败 → halt(留给人工)。 | 1980 | // 工作树脏:先自主恢复(in-scope 残留 → 自动 commit);含越界改动则恢复失败 → halt(留给人工)。 |
| 1551 | - const wt = await agentR(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) | 1981 | + // 作用树 = c.root:lane 模式查 lane 树(halt 保留的 lane 在 resume 复用时可能带残留),主根模式与现行一致。 |
| 1982 | + const wt = await agentR(worktreeCleanPromptM(c), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) | ||
| 1552 | if (!wt.clean) { | 1983 | if (!wt.clean) { |
| 1553 | - const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`), | 1984 | + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`, c), |
| 1554 | {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) | 1985 | {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) |
| 1555 | if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) | 1986 | if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) |
| 1556 | log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) | 1987 | log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) |
| 1557 | } | 1988 | } |
| 1558 | 1989 | ||
| 1559 | - const exists = await agentR(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) | ||
| 1560 | - if (exists.exists) { | ||
| 1561 | - await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout')}) | ||
| 1562 | - } else { | ||
| 1563 | - await runAction(g => createBranchFromPromptM(def.branch, branch) + g, {site:`branchSetup-create:${branch}`, grp:'Milestone', label: lbl('create')}) | 1990 | + if (c.lane == null) { |
| 1991 | + // 主根模式:现行 checkout 路径,一行不差(lane 模式跳过——worktree add 已把 lane 放上目标分支, | ||
| 1992 | + // 且 checkout 语义在"分支被别的 worktree 持有"时会 fatal,绝不能在主根重复执行)。 | ||
| 1993 | + const exists = await agentR(checkBranchExistsPromptM(branch, c), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) | ||
| 1994 | + if (exists.exists) { | ||
| 1995 | + await runAction(g => checkoutExistingBranchPromptM(branch, c) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout'), dec: c.decisions, root: c.root}) | ||
| 1996 | + } else { | ||
| 1997 | + await runAction(g => createBranchFromPromptM(def.branch, branch, c) + g, {site:`branchSetup-create:${branch}`, grp:'Milestone', label: lbl('create'), dec: c.decisions, root: c.root}) | ||
| 1998 | + } | ||
| 1564 | } | 1999 | } |
| 1565 | 2000 | ||
| 1566 | // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。 | 2001 | // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。 |
| 1567 | - let head = await agentR(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | 2002 | + let head = await agentR(currentBranchPromptM(c), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) |
| 1568 | for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { | 2003 | for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { |
| 1569 | const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, | 2004 | const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, |
| 1570 | - { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj) | 2005 | + { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj, c.root) |
| 1571 | if (verdict.action !== 'retry') | 2006 | if (verdict.action !== 'retry') |
| 1572 | throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`) | 2007 | throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`) |
| 1573 | - await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-recheckout:${branch}`, grp:'Milestone', label: lbl('recheckout')}) | ||
| 1574 | - head = await agentR(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | 2008 | + await runAction(g => checkoutExistingBranchPromptM(branch, c) + g, {site:`branchSetup-recheckout:${branch}`, grp:'Milestone', label: lbl('recheckout'), dec: c.decisions, root: c.root}) |
| 2009 | + head = await agentR(currentBranchPromptM(c), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | ||
| 1575 | } | 2010 | } |
| 1576 | if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`) | 2011 | if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`) |
| 1577 | 2012 | ||
| @@ -1580,6 +2015,8 @@ async function runBranchSetup(module) { | @@ -1580,6 +2015,8 @@ async function runBranchSetup(module) { | ||
| 1580 | 2015 | ||
| 1581 | // ---- runMilestone:原 milestonePrompt 的 6 步散文流程 → JS 编排 ---- | 2016 | // ---- runMilestone:原 milestonePrompt 的 6 步散文流程 → JS 编排 ---- |
| 1582 | // 所有"已是目标态则跳过"的条件由 JS 在 read 结果上判定,子代理只执行确定性动作。 | 2017 | // 所有"已是目标态则跳过"的条件由 JS 在 read 结果上判定,子代理只执行确定性动作。 |
| 2018 | +// 主根专属(不穿线 c):merge / docs/08 字段 / tag 全部作用于主根 + 默认分支。step 1 的 wt-clean | ||
| 2019 | +// 只管 merge 的机械安全(恒查主根树,用 mc);lane 树的 milestone 前收口由 runModule 另行处理(补 15)。 | ||
| 1583 | async function runMilestone(module) { | 2020 | async function runMilestone(module) { |
| 1584 | const id = module?.id ?? '<module>' | 2021 | const id = module?.id ?? '<module>' |
| 1585 | const fe = id === 'frontend-phase' | 2022 | const fe = id === 'frontend-phase' |
| @@ -1587,11 +2024,12 @@ async function runMilestone(module) { | @@ -1587,11 +2024,12 @@ async function runMilestone(module) { | ||
| 1587 | const branch = fe ? 'frontend-phase' : `module-${id}` | 2024 | const branch = fe ? 'frontend-phase' : `module-${id}` |
| 1588 | const targetTag = `milestone/${phaseId}` | 2025 | const targetTag = `milestone/${phaseId}` |
| 1589 | const lbl = (k) => `milestone:${k}:${phaseId}` | 2026 | const lbl = (k) => `milestone:${k}:${phaseId}` |
| 2027 | + const mc = makeCtx() // 主根上下文:worktreeClean / recoverDirtyWorktree 已穿线,本函数恒以主根调用 | ||
| 1590 | 2028 | ||
| 1591 | // step 1: worktree clean precondition(脏树先自主恢复 in-scope 残留;含越界改动则 halt 留人工) | 2029 | // step 1: worktree clean precondition(脏树先自主恢复 in-scope 残留;含越界改动则 halt 留人工) |
| 1592 | - const wt = await agentR(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) | 2030 | + const wt = await agentR(worktreeCleanPromptM(mc), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) |
| 1593 | if (!wt.clean) { | 2031 | if (!wt.clean) { |
| 1594 | - const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`), | 2032 | + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`, mc), |
| 1595 | {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) | 2033 | {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) |
| 1596 | if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) | 2034 | if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) |
| 1597 | log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) | 2035 | log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) |
| @@ -1661,26 +2099,27 @@ async function runMilestone(module) { | @@ -1661,26 +2099,27 @@ async function runMilestone(module) { | ||
| 1661 | 2099 | ||
| 1662 | // ---- runCrossModule:原 crossModulePrompt 的"diff → 分类 → 写日志" → JS 编排 ---- | 2100 | // ---- runCrossModule:原 crossModulePrompt 的"diff → 分类 → 写日志" → JS 编排 ---- |
| 1663 | // diff 和写文件是机械动作;"按 docs/08 § 二 路径归属判定哪些是跨模块"需要 LLM 判断,独立成一步。 | 2101 | // diff 和写文件是机械动作;"按 docs/08 § 二 路径归属判定哪些是跨模块"需要 LLM 判断,独立成一步。 |
| 1664 | -async function runCrossModule(module) { | 2102 | +// c:diff/分类/写日志全部在 c.root 上做(lane 模式读 lane 树;diff 基准默认分支为只读,不需要锁)。 |
| 2103 | +async function runCrossModule(module, c) { | ||
| 1665 | const id = module?.id ?? '<module>' | 2104 | const id = module?.id ?? '<module>' |
| 1666 | const lbl = (k) => `xmod:${k}:${id}` | 2105 | const lbl = (k) => `xmod:${k}:${id}` |
| 1667 | 2106 | ||
| 1668 | const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | 2107 | const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) |
| 1669 | 2108 | ||
| 1670 | - const changed = await agentR(collectCrossModuleChangedPromptM(def.branch), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) | 2109 | + const changed = await agentR(collectCrossModuleChangedPromptM(def.branch, c), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) |
| 1671 | if (!changed.files.length) { | 2110 | if (!changed.files.length) { |
| 1672 | log(`cross-module-log: 模块 ${id} 无文件改动,跳过`) | 2111 | log(`cross-module-log: 模块 ${id} 无文件改动,跳过`) |
| 1673 | return | 2112 | return |
| 1674 | } | 2113 | } |
| 1675 | 2114 | ||
| 1676 | - const classified = await agentR(classifyCrossModulePromptM(id, changed.files), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA}) | 2115 | + const classified = await agentR(classifyCrossModulePromptM(id, changed.files, c), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA}) |
| 1677 | if (!classified.crossModule.length) { | 2116 | if (!classified.crossModule.length) { |
| 1678 | log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`) | 2117 | log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`) |
| 1679 | return | 2118 | return |
| 1680 | } | 2119 | } |
| 1681 | 2120 | ||
| 1682 | - await runAction(g => writeCrossModuleLogPromptM(id, classified.crossModule) + g, | ||
| 1683 | - {site:`crossModule-write:${id}`, grp:'Milestone', label: lbl('write')}) | 2121 | + await runAction(g => writeCrossModuleLogPromptM(id, classified.crossModule, c) + g, |
| 2122 | + {site:`crossModule-write:${id}`, grp:'Milestone', label: lbl('write'), dec: c.decisions, root: c.root}) | ||
| 1684 | 2123 | ||
| 1685 | log(`cross-module-log: 模块 ${id} 更新 ${classified.crossModule.length} 行`) | 2124 | log(`cross-module-log: 模块 ${id} 更新 ${classified.crossModule.length} 行`) |
| 1686 | } | 2125 | } |
| @@ -1688,26 +2127,26 @@ async function runCrossModule(module) { | @@ -1688,26 +2127,26 @@ async function runCrossModule(module) { | ||
| 1688 | // ---- runFrontendSkeleton:前端骨架占位 stage 的 JS 编排(设计 § 2,前置依赖 A)---- | 2127 | // ---- runFrontendSkeleton:前端骨架占位 stage 的 JS 编排(设计 § 2,前置依赖 A)---- |
| 1689 | // 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(FeStub 占位)+ 无悬空导航。 | 2128 | // 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(FeStub 占位)+ 无悬空导航。 |
| 1690 | // 幂等(resume 安全):router/state 是唯一真实完成态;fe-skeleton-done 只作补记,避免陈旧 tag 跳过缺失骨架。 | 2129 | // 幂等(resume 安全):router/state 是唯一真实完成态;fe-skeleton-done 只作补记,避免陈旧 tag 跳过缺失骨架。 |
| 1691 | -async function runFrontendSkeleton(feItems) { | 2130 | +async function runFrontendSkeleton(feItems, c) { |
| 1692 | const lbl = (k) => `fe-skeleton:${k}` | 2131 | const lbl = (k) => `fe-skeleton:${k}` |
| 1693 | 2132 | ||
| 1694 | // step 1: 检查 router 是否已声明全 FE 路由;已建则只确保补记 tag 存在。 | 2133 | // step 1: 检查 router 是否已声明全 FE 路由;已建则只确保补记 tag 存在。 |
| 1695 | - const state = await agentR(frontendSkeletonStatePromptM(feItems), | 2134 | + const state = await agentR(frontendSkeletonStatePromptM(feItems, c), |
| 1696 | {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) | 2135 | {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) |
| 1697 | if (state.exists) { | 2136 | if (state.exists) { |
| 1698 | log('fe-skeleton: router 已声明全部 FE 路由,确保 fe-skeleton-done tag 存在') | 2137 | log('fe-skeleton: router 已声明全部 FE 路由,确保 fe-skeleton-done tag 存在') |
| 1699 | - await runAction(g => createFeSkeletonTagPromptM() + g, | ||
| 1700 | - {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag')}) | 2138 | + await runAction(g => createFeSkeletonTagPromptM(c) + g, |
| 2139 | + {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag'), dec: c.decisions, root: c.root}) | ||
| 1701 | return | 2140 | return |
| 1702 | } | 2141 | } |
| 1703 | 2142 | ||
| 1704 | // step 2: 派子代理生成骨架(成功后子代理自行 commit;此处仅经 runStage 仲裁 halt 收敛)。 | 2143 | // step 2: 派子代理生成骨架(成功后子代理自行 commit;此处仅经 runStage 仲裁 halt 收敛)。 |
| 1705 | - await runStage(g => frontendSkeletonPrompt(feItems) + g, | ||
| 1706 | - {site:'fe-skeleton', grp:'Frontend', label: lbl('gen')}) | 2144 | + await runStage(g => frontendSkeletonPrompt(feItems, c) + g, |
| 2145 | + {site:'fe-skeleton', grp:'Frontend', label: lbl('gen'), dec: c.decisions, root: c.root}) | ||
| 1707 | 2146 | ||
| 1708 | // step 3: 打 fe-skeleton-done 补记 tag。 | 2147 | // step 3: 打 fe-skeleton-done 补记 tag。 |
| 1709 | - await runAction(g => createFeSkeletonTagPromptM() + g, | ||
| 1710 | - {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag')}) | 2148 | + await runAction(g => createFeSkeletonTagPromptM(c) + g, |
| 2149 | + {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag'), dec: c.decisions, root: c.root}) | ||
| 1711 | 2150 | ||
| 1712 | log(`fe-skeleton: 已生成前端骨架(覆盖 ${(feItems || []).length} 个 FE 路由),打 fe-skeleton-done tag`) | 2151 | log(`fe-skeleton: 已生成前端骨架(覆盖 ${(feItems || []).length} 个 FE 路由),打 fe-skeleton-done tag`) |
| 1713 | } | 2152 | } |
| @@ -1720,8 +2159,13 @@ async function runFrontendSkeleton(feItems) { | @@ -1720,8 +2159,13 @@ async function runFrontendSkeleton(feItems) { | ||
| 1720 | // **顺序 for-await**(不是 pipeline)。理由: | 2159 | // **顺序 for-await**(不是 pipeline)。理由: |
| 1721 | // - tdd / fix stage 会编辑共享工作树并 git commit;并发会争 .git/index.lock、撞 migration V<n>。 | 2160 | // - tdd / fix stage 会编辑共享工作树并 git commit;并发会争 .git/index.lock、撞 migration V<n>。 |
| 1722 | // - pipeline 的"stage throw → item 掉 null、pipeline 永不 reject"语义会吞掉 reviewWithFixLoop / | 2161 | // - pipeline 的"stage throw → item 掉 null、pipeline 永不 reject"语义会吞掉 reviewWithFixLoop / |
| 1723 | -// verify / tdd 的 HALT throw,让模块主循环 try/catch 捕获不到,残缺模块照样被推进到 milestone。 | ||
| 1724 | -// 顺序 for-await 让 throw 自然冒泡到主循环 try → catch → break,使 fail-fast 真正生效。 | 2162 | +// verify / tdd 的 HALT throw,让 runModule 的 try/catch 捕获不到,残缺模块照样被推进到 milestone。 |
| 2163 | +// 顺序 for-await 让 throw 自然冒泡到 runModule try → catch → 结构化 halted 返回,使 fail-fast 真正生效。 | ||
| 2164 | +// | ||
| 2165 | +// 唯一并行豁免 = 入口 spec 预生成(Phase E Task 11):spec 只读 docs/prototype/tokens、不读兄弟 | ||
| 2166 | +// feature 代码(plan 才要 Grep 现有代码定位文件,依赖前序 feature 产出,**不**预生成),且 batch 模式 | ||
| 2167 | +// 只写盘不 commit(文件名含 id 不相撞、不碰 .git),上面两条顺序理由对它都不成立;失败项 thunk 内 | ||
| 2168 | +// catch 落 null 回退主链串行重跑(现行路径含 commit),halt 语义不受 parallel() 吞 throw 影响。 | ||
| 1725 | // | 2169 | // |
| 1726 | // 派生 stage 全部 schema 化:spec/plan/tdd/verify/fix 共用 STAGE_RESULT_SCHEMA,统一经 runStage 跑: | 2170 | // 派生 stage 全部 schema 化:spec/plan/tdd/verify/fix 共用 STAGE_RESULT_SCHEMA,统一经 runStage 跑: |
| 1727 | // stage 优先自主决策继续(缺值挑默认/解读并记入 decisions[]);返回 status:halt 或结构校验失败时不再立即 | 2171 | // stage 优先自主决策继续(缺值挑默认/解读并记入 decisions[]);返回 status:halt 或结构校验失败时不再立即 |
| @@ -1734,27 +2178,102 @@ async function runFrontendSkeleton(feItems) { | @@ -1734,27 +2178,102 @@ async function runFrontendSkeleton(feItems) { | ||
| 1734 | // coding-start 时此 dedup 会跳过 spec/plan/tdd/verify/review,**不会**再次审阅这些后期改动。 | 2178 | // coding-start 时此 dedup 会跳过 spec/plan/tdd/verify/review,**不会**再次审阅这些后期改动。 |
| 1735 | // 这是有意的设计:避免在共享工作树里因为别的模块的 cross-cut 改动反复重跑前面所有 REQ。 | 2179 | // 这是有意的设计:避免在共享工作树里因为别的模块的 cross-cut 改动反复重跑前面所有 REQ。 |
| 1736 | // 若需要"approve 后改动必须再次走 review"的语义,请在改动前手动删除对应 `req-done/<id>` tag。 | 2180 | // 若需要"approve 后改动必须再次走 review"的语义,请在改动前手动删除对应 `req-done/<id>` tag。 |
| 1737 | -async function featureLoop(items, phase) { | 2181 | +// spec 预生成闸(Phase E Task 11):spec 预生成无物理冲突风险(batch 只写盘不 commit、文件名含 id |
| 2182 | +// 不相撞),与模块并行开关解耦——parallel.features 或 parallel.modules 任一开启即启用,前后端 | ||
| 2183 | +// featureLoop 同享;缺省(parallel 缺省 / modules:false 逃生口且未开 features)全关,维持与 master | ||
| 2184 | +// 串行等价。注意它不是 Phase F 的 feature 级并行(tdd/fix 链仍严格串行,那才归 features 闸全权管辖)。 | ||
| 2185 | +const useSpecPrefetch = ARGS?.parallel?.features === true || ARGS?.parallel?.modules === true | ||
| 2186 | +// feature 级受限并行闸(Phase F Task 12-14 全权归 features 闸管辖;附录补 1:coding-start 不传 | ||
| 2187 | +// features——开启是 Task 14 收益评估通过后的显式一行改动)。范围仅后端 REQ(附录补 2); | ||
| 2188 | +// featureMaxWidth 缺省 2(计划 Task 12 字面默认)。与 useSpecPrefetch 解耦:spec 预生成无物理 | ||
| 2189 | +// 冲突风险、两开关任一开启即用;feature 级 tdd/fix 链并行只看本闸。 | ||
| 2190 | +const useFeatureParallel = ARGS?.parallel?.features === true | ||
| 2191 | +const featureMaxWidth = Math.max(1, ARGS?.parallel?.featureMaxWidth ?? 2) | ||
| 2192 | +async function featureLoop(items, phase, c) { | ||
| 1738 | const grp = phase === 'backend' ? 'Backend' : 'Frontend' | 2193 | const grp = phase === 'backend' ? 'Backend' : 'Frontend' |
| 1739 | - for (const id of items) { | ||
| 1740 | - // 入口 dedup:req-done/<id> 已存在 → 已 approve,整段 skip。 | ||
| 1741 | - const done = await agentR(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) | ||
| 1742 | - if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } | 2194 | + // spec 的 artifactPath 校验(主链与批量预生成共用同一份,确保两条路径产物口径一致)。 |
| 2195 | + const specValidate = r => { | ||
| 2196 | + if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' | ||
| 2197 | + dateFromArtifactPath(r.artifactPath) // 文件名日期前缀非法 → 抛,被 runStage 捕获转为仲裁 | ||
| 2198 | + return null | ||
| 2199 | + } | ||
| 1743 | 2200 | ||
| 1744 | - const spec = await runStage(g => deriveSpecPrompt(id, phase) + g, { | ||
| 1745 | - site:`spec:${phase}:${id}`, grp, label:`spec:${phase}:${id}`, | ||
| 1746 | - validate: r => { | ||
| 1747 | - if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' | ||
| 1748 | - dateFromArtifactPath(r.artifactPath) // 文件名日期前缀非法 → 抛,被 runStage 捕获转为仲裁 | ||
| 1749 | - return null | ||
| 1750 | - }, | 2201 | + // ── 入口 spec 预生成(Phase E Task 11;准入与豁免理由见函数头注释)──────────── |
| 2202 | + // tag check 并行(只读)→ 未完成项 parallel() 跑 batch spec(thunk 内 catch,失败项落 null)→ | ||
| 2203 | + // 单一微步骤统一 add+commit → 成功项存 specById 供主链直接消费;null 项回退主链重跑(含 commit)。 | ||
| 2204 | + const specById = new Map() // id → 预生成 spec 的 STAGE_RESULT(主链命中即跳过 spec stage) | ||
| 2205 | + // id → 预生成期间的临时决策 sink:仅当该项成果被主链**实际消费**(ensureSpec 命中)时才并入 | ||
| 2206 | + // c.decisions——回退路径(thunk null / 统一提交失败清空 specById)连 sink 一起丢弃,主链重跑 | ||
| 2207 | + // spec 会重新登记,同一 stage 的决策绝不记两遍。 | ||
| 2208 | + const specDecsById = new Map() | ||
| 2209 | + const doneById = new Map() // id → tag check 布尔缓存(批量查失败的槽位缺席,主链回退逐项重查) | ||
| 2210 | + if (useSpecPrefetch && items.length >= 2) { | ||
| 2211 | + // 直用 agent() 而非 agentR:thunk 内 throw 会被 parallel() 折成 null 丢失诊断;null 槽位留给 | ||
| 2212 | + // 主链 agentR 重查,那里的 halt 语义与现行串行路径逐字一致。 | ||
| 2213 | + const checks = await parallel(items.map(id => () => | ||
| 2214 | + agent(checkReqDoneTagPromptM(id, c), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}))) | ||
| 2215 | + checks.forEach((r, i) => { if (r) doneById.set(items[i], r.exists) }) | ||
| 2216 | + // 只对"确认未完成"的项预生成;tag check 失败(缺席)的项不预生成——其完成态未知,预生成可能 | ||
| 2217 | + // 白烧一次 spec 子代理,且主链重查后若已完成会整段 skip。 | ||
| 2218 | + const todo = items.filter(id => doneById.get(id) === false) | ||
| 2219 | + if (todo.length >= 2) { // 仅 1 项时预生成无并行收益,纯多一次统一 commit 微步骤,走主链即可 | ||
| 2220 | + const specs = await parallel(todo.map(id => () => { | ||
| 2221 | + const sink = [] // 临时决策 sink(F11):与成果同生共死,回退时一起丢弃 | ||
| 2222 | + return runStage(g => deriveSpecPrompt(id, phase, c, { batch: true }) + g, { | ||
| 2223 | + site:`spec:${phase}:${id}`, grp, label:`spec:${phase}:${id}`, dec: sink, root: c.root, | ||
| 2224 | + validate: specValidate, | ||
| 2225 | + }).then(r => ({ r, sink })) | ||
| 2226 | + .catch(e => { // thunk 内 catch:runStage 的 HALT throw 折成 null(先 log 保诊断), | ||
| 2227 | + log(`spec 预生成失败 ${phase}:${id}(落 null,回退主链串行重跑):${String(e?.message || e)}`) | ||
| 2228 | + return null // 绝不让 throw 进 parallel()——那会静默吞掉 halt 原因 | ||
| 2229 | + }) | ||
| 2230 | + })) | ||
| 2231 | + specs.forEach((x, i) => { if (x) { specById.set(todo[i], x.r); specDecsById.set(todo[i], x.sink) } }) | ||
| 2232 | + if (specById.size) { | ||
| 2233 | + // 统一提交(bestEffort 不 halt):失败 → 丢弃全部预生成结果,主链按现行路径逐项重跑 spec | ||
| 2234 | + // (含 commit;写盘内容已在树上,spec prompt 的"已存在同 id spec 复用/就地 Edit"规则会复用它, | ||
| 2235 | + // commit 若仍失败由现行 commitBlock 的 halt 语义兜底——不为预生成机制新增 halt 点)。 | ||
| 2236 | + const ok = await bestEffortAction(commitPrefetchedSpecsPromptM([...specById.keys()], phase, c), | ||
| 2237 | + { label:`spec-batch-commit:${phase}`, phase: grp, | ||
| 2238 | + okMsg:`spec 预生成: 统一提交 ${specById.size} 份(${phase})`, failTag:`spec 预生成统一提交(${phase})` }) | ||
| 2239 | + if (!ok) { specById.clear(); specDecsById.clear() } // 全量回退:决策随成果一起丢,主链重跑重新登记 | ||
| 2240 | + } | ||
| 2241 | + } | ||
| 2242 | + } | ||
| 2243 | + | ||
| 2244 | + // ── 链段共用闭包:现行串行链与 Phase F 并行批共用同一组调用点(site/label/allowContinue/ | ||
| 2245 | + // dec/root 口径一字不差,仅执行 ctx 可换成 feature lane 的 fc)────────────────── | ||
| 2246 | + // 入口 dedup:req-done/<id> 已存在 → 已 approve,整段 skip。批量 tag check 命中缓存直接消费 | ||
| 2247 | + // (同一次运行刚查过;req-done/<id> 按 id 归属本模块,兄弟 lane 不会代打);缺席槽位回退逐项重查。 | ||
| 2248 | + const isDone = async (id) => { | ||
| 2249 | + const cached = doneById.get(id) | ||
| 2250 | + if (cached !== undefined) return cached | ||
| 2251 | + const done = await agentR(checkReqDoneTagPromptM(id, c), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) | ||
| 2252 | + return done.exists | ||
| 2253 | + } | ||
| 2254 | + // 预生成命中 → 直接消费 artifactPath、跳过 spec stage(统一提交已落 commit),并把预生成期间的 | ||
| 2255 | + // 临时决策 sink 并入 c.decisions(F11:仅消费时并入,delete 防同 id 二次并入;条目在预生成时已 | ||
| 2256 | + // 经 recordDecisions 落过 log,此处只并收集器不重复打日志);未命中/落 null → 现行串行路径重跑 | ||
| 2257 | + // (含 commitBlock,决策直落 c.decisions)。spec/plan 恒在模块树(ctx = c)上串行产出——Phase F | ||
| 2258 | + // 只把 tdd→verify→review→fix 段挪进 feature lane。 | ||
| 2259 | + const ensureSpec = async (id) => { | ||
| 2260 | + const pre = specById.get(id) | ||
| 2261 | + if (pre) { | ||
| 2262 | + const sink = specDecsById.get(id) | ||
| 2263 | + if (sink) { c.decisions.push(...sink); specDecsById.delete(id) } | ||
| 2264 | + return pre | ||
| 2265 | + } | ||
| 2266 | + return runStage(g => deriveSpecPrompt(id, phase, c) + g, { | ||
| 2267 | + site:`spec:${phase}:${id}`, grp, label:`spec:${phase}:${id}`, dec: c.decisions, root: c.root, | ||
| 2268 | + validate: specValidate, | ||
| 1751 | }) | 2269 | }) |
| 2270 | + } | ||
| 2271 | + const runPlanStage = async (id, spec) => { | ||
| 1752 | // spec 经仲裁 continue 时 artifactPath 仍可能不带合法日期前缀——防御取值,避免重算抛出把 continue 变成隐式 halt。 | 2272 | // spec 经仲裁 continue 时 artifactPath 仍可能不带合法日期前缀——防御取值,避免重算抛出把 continue 变成隐式 halt。 |
| 1753 | let specDate = null | 2273 | let specDate = null |
| 1754 | try { specDate = dateFromArtifactPath(spec.artifactPath) } catch { specDate = null } | 2274 | try { specDate = dateFromArtifactPath(spec.artifactPath) } catch { specDate = null } |
| 1755 | - | ||
| 1756 | - const plan = await runStage(g => planPrompt(id, phase, spec.artifactPath) + g, { | ||
| 1757 | - site:`plan:${phase}:${id}`, grp, label:`plan:${phase}:${id}`, | 2275 | + return runStage(g => planPrompt(id, phase, spec.artifactPath, c) + g, { |
| 2276 | + site:`plan:${phase}:${id}`, grp, label:`plan:${phase}:${id}`, dec: c.decisions, root: c.root, | ||
| 1758 | validate: r => { | 2277 | validate: r => { |
| 1759 | if (!r.artifactPath) return 'plan 返回 ok 但缺 artifactPath' | 2278 | if (!r.artifactPath) return 'plan 返回 ok 但缺 artifactPath' |
| 1760 | if (specDate && dateFromArtifactPath(r.artifactPath) !== specDate) | 2279 | if (specDate && dateFromArtifactPath(r.artifactPath) !== specDate) |
| @@ -1762,26 +2281,245 @@ async function featureLoop(items, phase) { | @@ -1762,26 +2281,245 @@ async function featureLoop(items, phase) { | ||
| 1762 | return null | 2281 | return null |
| 1763 | }, | 2282 | }, |
| 1764 | }) | 2283 | }) |
| 1765 | - | 2284 | + } |
| 2285 | + // fc:执行上下文(串行 = 模块 ctx c;Phase F 并行批 = feature lane ctx)。rwOpts 透传 | ||
| 2286 | + // reviewWithFixLoop(feature lane 内跳过 docs/08 勾选,理由见该函数头注释)。 | ||
| 2287 | + const runImplChain = async (id, planPath, specPath, fc, rwOpts) => { | ||
| 1766 | // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, | 2288 | // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, |
| 1767 | // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 | 2289 | // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 |
| 1768 | - const impl = await runStage(g => tddPrompt(id, phase, plan.artifactPath) + g, { | ||
| 1769 | - site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, | 2290 | + const impl = await runStage(g => tddPrompt(id, phase, planPath, fc) + g, { |
| 2291 | + site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, | ||
| 1770 | }) | 2292 | }) |
| 1771 | - | ||
| 1772 | // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, | 2293 | // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, |
| 1773 | // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 | 2294 | // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 |
| 1774 | - const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', spec.artifactPath, 0) + g, { | ||
| 1775 | - site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, | 2295 | + const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', specPath, 0, fc) + g, { |
| 2296 | + site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, | ||
| 1776 | }) | 2297 | }) |
| 1777 | - | ||
| 1778 | - const reviewResult = await reviewWithFixLoop(id, phase, v0, spec.artifactPath) | 2298 | + const reviewResult = await reviewWithFixLoop(id, phase, v0, specPath, fc, rwOpts) |
| 1779 | log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) | 2299 | log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) |
| 2300 | + } | ||
| 2301 | + // approve 后落地 dedup 真值:req-done/<id> tag(失败经仲裁重试,确不可恢复才 halt)。 | ||
| 2302 | + // tag 全仓共享,恒以模块 ctx c 打(Phase F 在合并回模块分支**之后**才打,见 featureBatchRun 批后收口)。 | ||
| 2303 | + const tagDone = async (id) => runAction(g => createReqDoneTagPromptM(id, phase, c) + g, { | ||
| 2304 | + site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root, | ||
| 2305 | + }) | ||
| 2306 | + | ||
| 2307 | + // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。 | ||
| 2308 | + // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。 | ||
| 2309 | + if (useFeatureParallel && phase === 'backend' && items.length >= 2) { | ||
| 2310 | + await featureBatchRun(items, phase, c, { grp, isDone, ensureSpec, runPlanStage, runImplChain, tagDone }) | ||
| 2311 | + return | ||
| 2312 | + } | ||
| 1780 | 2313 | ||
| 1781 | - // approve 后落地 dedup 真值:req-done/<id> tag(失败经仲裁重试,确不可恢复才 halt)。 | ||
| 1782 | - await runAction(g => createReqDoneTagPromptM(id, phase) + g, { | ||
| 1783 | - site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, | ||
| 1784 | - }) | 2314 | + for (const id of items) { |
| 2315 | + if (await isDone(id)) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } | ||
| 2316 | + const spec = await ensureSpec(id) | ||
| 2317 | + const plan = await runPlanStage(id, spec) | ||
| 2318 | + await runImplChain(id, plan.artifactPath, spec.artifactPath, c) | ||
| 2319 | + await tagDone(id) | ||
| 2320 | + } | ||
| 2321 | +} | ||
| 2322 | + | ||
| 2323 | +// laneEnvCache:lane 库 schema 基底 + lane DB 支持度是**运行内不变量**(同一运行环境不变),首个 | ||
| 2324 | +// 探测点(featureBatchRun 的 lane 前置 / ≥2 宽波次的 prepareParallelWave,谁先到谁填)探测后缓存 | ||
| 2325 | +// 复用,成功失败都缓存(不支持也是稳定事实,后续免重复子代理调用直接降级)。agent 无返回 / 抛错 | ||
| 2326 | +// 是瞬时态不是环境事实,**不缓存**,留给下个探测点重探。形态:null=未探测;{ fail } / { baseSchema }。 | ||
| 2327 | +let laneEnvCache = null | ||
| 2328 | + | ||
| 2329 | +// ---- Phase F(Task 12/13):后端 REQ 的 feature 级受限并行批 ---- | ||
| 2330 | +// 准入闸的灵魂:**LLM 提议、JS 验证**——plan 本就锁文件边界,文件集相交与否是确定性可算的, | ||
| 2331 | +// 不信 LLM 单方判断。准入 = 批内两两 files 不相交 ∧ 全部 schemaChange=false ∧ 批宽 ≤ | ||
| 2332 | +// featureMaxWidth;改 schema 的 feature **永远串行**;frontend phase 不进准入(附录补 2:FE 的 | ||
| 2333 | +// e2e 要打穿 vite/playwright/后端三层端口配置,复杂度/收益比不成立,FE 收益由 spec 预生成承担)。 | ||
| 2334 | +// 失败路径一律 fail-open:准入不过 / facts 提取失败 / feature lane 建失败 → 该 feature 留串行链, | ||
| 2335 | +// 绝不因并行机制新增 halt 点(feature 链自身的 stage halt 是真实正确性边界,不在豁免之列)。 | ||
| 2336 | +// 隔离三前提逐一替换(与模块级 lane 同构):feature worktree(独立 index 不争 .git/index.lock)、 | ||
| 2337 | +// 准入排除 schema 改动(无 V<n> 取号即无撞号)、feature 专属测试库(`<schema>_lane<N>_f<i>`, | ||
| 2338 | +// 附录补 8 序号制;tdd/verify 不起栈不占固定端口,无需 stackLock——这正是收窄到后端 REQ 的原因)。 | ||
| 2339 | +// h:featureLoop 的链段共用闭包(isDone/ensureSpec/runPlanStage/runImplChain/tagDone)——并行批 | ||
| 2340 | +// 与串行链共用同一组调用点。 | ||
| 2341 | +async function featureBatchRun(items, phase, c, h) { | ||
| 2342 | + const { grp, isDone, ensureSpec, runPlanStage, runImplChain, tagDone } = h | ||
| 2343 | + const serialChain = async (f) => { await runImplChain(f.id, f.planPath, f.specPath, c); await tagDone(f.id) } | ||
| 2344 | + | ||
| 2345 | + // 执行序第 1 段(Task 13 字面):模块内先**串行**跑完全部 spec+plan(轻;plan 要 Grep 现有代码 | ||
| 2346 | + // 定位文件,必须在模块树上串行产出)——与现行链的逐 feature 交错不同:plan 全集先行,才有 | ||
| 2347 | + // facts 可提取、可整体分批。 | ||
| 2348 | + const feats = [] // { id, specPath, planPath, facts: Set<string>|null } | ||
| 2349 | + for (const id of items) { | ||
| 2350 | + if (await isDone(id)) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } | ||
| 2351 | + const spec = await ensureSpec(id) | ||
| 2352 | + const plan = await runPlanStage(id, spec) | ||
| 2353 | + feats.push({ id, specPath: spec.artifactPath, planPath: plan.artifactPath, facts: null }) | ||
| 2354 | + } | ||
| 2355 | + | ||
| 2356 | + // 执行序第 2 段:提取 PLAN_FACTS(只读微步骤,agent 直调——失败不仲裁不 halt)。null / 形状 | ||
| 2357 | + // 不符 / 文件集为空 / 改 schema → facts 留 null(该 feature 走串行链);待跑不足 2 个则无并行 | ||
| 2358 | + // 可言,全员 facts=null 自然全串行(省 facts 子代理)。 | ||
| 2359 | + if (feats.length >= 2) { | ||
| 2360 | + for (const f of feats) { | ||
| 2361 | + const r = await agent(planFactsPromptM(f.planPath, c), { label:`plan-facts:${f.id}`, phase: grp, schema: PLAN_FACTS_SCHEMA }) | ||
| 2362 | + if (!r) { log(`feature-parallel: ${f.id} facts 提取无返回(fail-open 留串行链)`); continue } | ||
| 2363 | + if (r.schemaChange !== false) { log(`feature-parallel: ${f.id} plan 声明 schema 改动 → 永远串行`); continue } | ||
| 2364 | + const files = Array.isArray(r.files) ? r.files.map(x => typeof x === 'string' ? x.trim() : '').filter(Boolean) : [] | ||
| 2365 | + if (!files.length) { log(`feature-parallel: ${f.id} plan 文件集为空/不可辨(fail-open 留串行链)`); continue } | ||
| 2366 | + f.facts = new Set(files) | ||
| 2367 | + } | ||
| 2368 | + } | ||
| 2369 | + | ||
| 2370 | + // JS 准入分批(Task 12):保 docs/02 原序的贪心**连续**分批。批内并发放弃成员间顺序——两两 | ||
| 2371 | + // 不相交 + 无 schema 改动正是顺序无关的准入前提;但**跨批**顺序保持原序:无 facts 的 feature | ||
| 2372 | + // 自成串行项并截断当前批,绝不跨越它重排(后续 feature 可能依赖它的 schema/代码产出)。 | ||
| 2373 | + const runs = [] // 每项 = feats 子数组;length 1 即串行项 | ||
| 2374 | + { | ||
| 2375 | + let cur = [] | ||
| 2376 | + const flush = () => { if (cur.length) { runs.push(cur); cur = [] } } | ||
| 2377 | + const disjoint = (f, batch) => batch.every(b => { for (const x of f.facts) if (b.facts.has(x)) return false; return true }) | ||
| 2378 | + for (const f of feats) { | ||
| 2379 | + if (!f.facts) { flush(); runs.push([f]); continue } | ||
| 2380 | + if (cur.length >= featureMaxWidth || (cur.length && !disjoint(f, cur))) flush() | ||
| 2381 | + cur.push(f) | ||
| 2382 | + } | ||
| 2383 | + flush() | ||
| 2384 | + } | ||
| 2385 | + | ||
| 2386 | + // feature lane 前置(首个 ≥2 批前惰性准备一次;任何一步失败 → 本模块全部批降级串行,fail-open)。 | ||
| 2387 | + // lane 库命名(附录补 8 序号制):`<schema>_lane<N>_f<i>`——模块 lane 模式 c.dbSchema 已是 | ||
| 2388 | + // `<schema>_lane<N>`,直接缀 `_f<i>`;串行主根模式 N 取 0。后缀固定短(≤10 字符),不引入 | ||
| 2389 | + // moduleId 进库名的截断/撞名/转义问题。 | ||
| 2390 | + let featPrep // undefined=未试;null=失败(降级);{ base, moduleBranch } | ||
| 2391 | + const prepareFeatureLanes = async () => { | ||
| 2392 | + if (featPrep !== undefined) return featPrep | ||
| 2393 | + featPrep = null | ||
| 2394 | + try { | ||
| 2395 | + // 模块分支名 = c.root 当前 HEAD(runBranchSetup 已确保在 module-<id> 上)——featureLoop | ||
| 2396 | + // 不接收 module 形参,取 HEAD 是唯一不另起约定的真值来源。 | ||
| 2397 | + const head = await agent(currentBranchPromptM(c), { label:'feat:module-branch', phase: grp, schema: DEFAULT_BRANCH_SCHEMA }) | ||
| 2398 | + if (!head || !head.branch) { log('feature-parallel: 模块分支名读取失败 → 本模块 feature 批全部降级串行'); return featPrep } | ||
| 2399 | + let base = c.dbSchema | ||
| 2400 | + if (!base) { | ||
| 2401 | + // 串行主根模式:lane 测试库支持与模块级同判据(附录补 6b 两点联查)——并行 feature 链的 | ||
| 2402 | + // tdd 内循环测试若同连共享库会互相清库,任一不满足 → 老老实实串行,不 halt。 | ||
| 2403 | + // (模块 lane 模式 c.dbSchema 非空 = 波次前置已探测通过,免重查;探测/读基底两个微步骤 | ||
| 2404 | + // 均主根专属,串行主根下 c.root === ROOT,口径一致。) | ||
| 2405 | + // 经 laneEnvCache 缓存:串行主根 + features 闸模式下波次前置永不执行、缓存恒空,不在此 | ||
| 2406 | + // 读/填会逐模块白烧两个探测子代理。agent 无返回不缓存(瞬时态,仅本模块降级,留待重探)。 | ||
| 2407 | + if (!laneEnvCache) { | ||
| 2408 | + const probe = await agent(laneDbSupportPromptM(), { label:'feat:lane-db?', phase: grp, schema: LANE_DB_PROBE_SCHEMA }) | ||
| 2409 | + if (!probe) { log('feature-parallel: lane 测试库支持度探测无返回 → 本模块 feature 批全部降级串行'); return featPrep } | ||
| 2410 | + if (!probe.scriptsEnv || !probe.ymlPlaceholder) { | ||
| 2411 | + laneEnvCache = { fail: `目标项目不支持 lane 测试库(scriptsEnv=${probe.scriptsEnv}, ymlPlaceholder=${probe.ymlPlaceholder})` } | ||
| 2412 | + } else { | ||
| 2413 | + const sb = await agent(readDbSchemaPromptM(), { label:'feat:db-base', phase: grp, schema: FIELD_VALUE_SCHEMA }) | ||
| 2414 | + if (!sb) { log('feature-parallel: database.schema 读取无返回 → 本模块 feature 批全部降级串行'); return featPrep } | ||
| 2415 | + laneEnvCache = (!sb.found || !sb.value) | ||
| 2416 | + ? { fail: 'config-vars.yaml 的 database.schema 读不到(lane 库名无基底)' } | ||
| 2417 | + : { baseSchema: sb.value } | ||
| 2418 | + } | ||
| 2419 | + } | ||
| 2420 | + if (laneEnvCache.fail) { log(`feature-parallel: ${laneEnvCache.fail} → 本模块 feature 批全部降级串行`); return featPrep } | ||
| 2421 | + base = laneDbSchema(laneEnvCache.baseSchema, 0) // serial 主根跑 feature 并行:N 取 0(附录补 8) | ||
| 2422 | + } | ||
| 2423 | + featPrep = { base, moduleBranch: head.branch } | ||
| 2424 | + } catch (e) { | ||
| 2425 | + log(`feature-parallel: lane 前置准备异常(${String(e?.message || e)})→ 本模块 feature 批全部降级串行`) | ||
| 2426 | + } | ||
| 2427 | + return featPrep | ||
| 2428 | + } | ||
| 2429 | + // lane 清理/丢弃共用:移除 worktree + 删分支(均 bestEffort——分支与对象库全仓共享,残留不损 | ||
| 2430 | + // 正确性,删失败只 log;残留 feature lane 持 feat/* 分支,不挡 module-* 占用感知)。 | ||
| 2431 | + const dropFeatureLane = async (l) => { | ||
| 2432 | + await bestEffortAction(removeLaneWorktreePromptM(l.path), | ||
| 2433 | + { label:`feat:lane-remove:${l.f.id}`, phase: grp, okMsg:`feature-parallel: 已移除 lane ${l.path}`, failTag:`feature-parallel: 移除 lane(${l.path})` }) | ||
| 2434 | + await bestEffortAction(deleteFeatureBranchPromptM(l.branch, c), | ||
| 2435 | + { label:`feat:branch-del:${l.f.id}`, phase: grp, okMsg:`feature-parallel: 已删除分支 ${l.branch}`, failTag:`feature-parallel: 删除分支(${l.branch})` }) | ||
| 2436 | + } | ||
| 2437 | + | ||
| 2438 | + for (const run of runs) { | ||
| 2439 | + const prep = run.length >= 2 ? await prepareFeatureLanes() : null | ||
| 2440 | + if (run.length < 2 || !prep) { // 串行项 / 前置失败降级 → 现行串行链(与非并行路径同源闭包) | ||
| 2441 | + for (const f of run) await serialChain(f) | ||
| 2442 | + continue | ||
| 2443 | + } | ||
| 2444 | + | ||
| 2445 | + // 建 feature lanes(复用 createLaneWorktreePromptM 的幂等三分支形态;从模块分支 HEAD 建 | ||
| 2446 | + // feat/<id>)。fail-open:建 lane / 写 marker(附录补 7 fail-closed 兜底——setup-test-db 是 | ||
| 2447 | + // DROP+CREATE,不能只靠提示词注入)失败 → 该 feature 退串行;可并行的不足 2 条 → 整批退串行。 | ||
| 2448 | + const lanes = [] // { f, branch, path, fc } | ||
| 2449 | + const fallback = [] | ||
| 2450 | + for (const [i, f] of run.entries()) { | ||
| 2451 | + const l = { f, branch: `feat/${f.id}`, path: featureLaneRoot(c.root, f.id) } | ||
| 2452 | + const schema = `${prep.base}_f${i}` | ||
| 2453 | + const add = await agent(createFeatureWorktreePromptM(l.branch, l.path, prep.moduleBranch, c), | ||
| 2454 | + { label:`feat:lane-add:${f.id}`, phase: grp, schema: ACTION_RESULT_SCHEMA }) | ||
| 2455 | + if (!add || !add.success) { | ||
| 2456 | + log(`feature-parallel: ${f.id} 建 feature lane 失败(${(add && add.error) || 'agent 无返回'})→ 留串行链`) | ||
| 2457 | + fallback.push(f); continue | ||
| 2458 | + } | ||
| 2459 | + const mk = await agent(writeLaneDbMarkerPromptM(l.path, schema), { label:`feat:lane-marker:${f.id}`, phase: grp, schema: ACTION_RESULT_SCHEMA }) | ||
| 2460 | + if (!mk || !mk.success) { | ||
| 2461 | + log(`feature-parallel: ${f.id} 写 lane DB marker 失败(${(mk && mk.error) || 'agent 无返回'})→ 留串行链`) | ||
| 2462 | + await dropFeatureLane(l); fallback.push(f); continue | ||
| 2463 | + } | ||
| 2464 | + // feature 级 ctx:root=feature lane、dbSchema=feature 专属库;decisions **共享**模块收集器 | ||
| 2465 | + // (单线程事件循环下并发 push 原子,逐模块 RESUME flush 口径不变);vBase 透传仅保字段语义 | ||
| 2466 | + // 一致(准入已排除 schema 改动,tdd 不会取号)。 | ||
| 2467 | + l.fc = makeCtx({ lane: c.lane, root: l.path, dbSchema: schema, vBase: c.vBase, decisions: c.decisions }) | ||
| 2468 | + lanes.push(l) | ||
| 2469 | + } | ||
| 2470 | + if (lanes.length < 2) { | ||
| 2471 | + // 并行不成立:孤 lane 弃用回收,全员按原序走串行链(在模块树上跑,与 lane 无关)。 | ||
| 2472 | + for (const l of lanes) await dropFeatureLane(l) | ||
| 2473 | + for (const f of run) await serialChain(f) | ||
| 2474 | + continue | ||
| 2475 | + } | ||
| 2476 | + | ||
| 2477 | + // 并行跑 tdd→verify→review→fix(thunk 内 catch:HALT throw 先 log 保诊断再折成结构化失败, | ||
| 2478 | + // 绝不让 throw 进 parallel()——那会静默折成 null 丢失原因)。lane 内跳过 docs/08 勾选 | ||
| 2479 | + // (flipDocs08:false):相邻 REQ 行的并行编辑会在批后合并时制造无谓 docs/08 冲突,批后统一勾。 | ||
| 2480 | + const rs = await parallel(lanes.map(l => () => | ||
| 2481 | + runImplChain(l.f.id, l.f.planPath, l.f.specPath, l.fc, { flipDocs08: false }) | ||
| 2482 | + .then(() => ({ ok: true })) | ||
| 2483 | + .catch(e => { | ||
| 2484 | + const msg = String(e?.message || e) | ||
| 2485 | + log(`feature-parallel: ${l.f.id} 链失败(feature lane 保留取证 ${l.path}):${msg}`) | ||
| 2486 | + return { ok: false, error: msg } | ||
| 2487 | + }))) | ||
| 2488 | + | ||
| 2489 | + // 批后串行收口(Task 13):成功链按批内序合并回模块分支 → 勾 docs/08 → 打 req-done → 清理 | ||
| 2490 | + // lane。模块内本就串行(同一时刻只有本批在收口),无需全局锁。 | ||
| 2491 | + const haltMsgs = [] | ||
| 2492 | + const rerun = [] | ||
| 2493 | + for (const [i, l] of lanes.entries()) { | ||
| 2494 | + const r = rs[i] | ||
| 2495 | + if (!r || r.ok !== true) { | ||
| 2496 | + // 链失败 / runtime-null:feature lane 整树保留取证(reason 带路径),批边界收敛后统一 | ||
| 2497 | + // halt——这是真实 stage halt 的批级聚合(串行链同样会在此 halt),不属 fail-open 范畴。 | ||
| 2498 | + haltMsgs.push(`${l.f.id} [feature-lane 保留取证: ${l.path}]: ${r ? r.error : 'runtime-null(parallel() 槽位为 null)'}`) | ||
| 2499 | + continue | ||
| 2500 | + } | ||
| 2501 | + // 合并经 agentR(null 走 NULL_AGENT halt——计划只豁免"冲突不 adjudicate",不豁免 runtime 故障)。 | ||
| 2502 | + const mg = await agentR(mergeFeatureBranchPromptM(l.branch, prep.moduleBranch, l.f.id, c), | ||
| 2503 | + { label:`feat:merge:${l.f.id}`, phase: grp, schema: ACTION_RESULT_SCHEMA }) | ||
| 2504 | + if (mg.success) { | ||
| 2505 | + await flipDocs08Checkbox(false, l.f.id, phase, grp, c) // lane 内跳过的勾选在此补落(纯可视化,失败只 log) | ||
| 2506 | + await tagDone(l.f.id) | ||
| 2507 | + await dropFeatureLane(l) | ||
| 2508 | + } else { | ||
| 2509 | + // 合并冲突(文件集不相交下理论不可能,命中即 facts 提取失真)→ 不 adjudicate:丢弃 | ||
| 2510 | + // feature 分支,回模块分支串行重跑该 feature 全链(merge 微步骤已 abort 恢复干净模块分支)。 | ||
| 2511 | + log(`feature-parallel: ${l.f.id} 合并回 ${prep.moduleBranch} 失败(${mg.error || ''}${mg.detail ? ':' + mg.detail : ''})——文件集不相交下不应发生,按 facts 提取失真处置:丢弃 feat/${l.f.id},回模块分支串行重跑全链`) | ||
| 2512 | + await dropFeatureLane(l) | ||
| 2513 | + rerun.push(l.f) | ||
| 2514 | + } | ||
| 2515 | + } | ||
| 2516 | + if (haltMsgs.length) { | ||
| 2517 | + // 兄弟链已收口(成功者的工作已合并 + 打 tag 落地);冲突重跑搁置——模块即将 halt,不再起 | ||
| 2518 | + // 新链(与模块级"波次边界收敛后不再起新波"同口径),resume 时这些 feature 因缺 req-done tag 自动重跑。 | ||
| 2519 | + for (const f of rerun) log(`feature-parallel: ${f.id} 的串行重跑因同批链失败而搁置(resume 由 req-done tag 缺失兜底)`) | ||
| 2520 | + throw new Error(`HALT feature-batch ${phase}: ${haltMsgs.join(';')}`) | ||
| 2521 | + } | ||
| 2522 | + for (const f of [...rerun, ...fallback]) await serialChain(f) | ||
| 1785 | } | 2523 | } |
| 1786 | } | 2524 | } |
| 1787 | 2525 | ||
| @@ -1797,17 +2535,23 @@ const REVIEW_SOFT_ROUNDS = 5 | @@ -1797,17 +2535,23 @@ const REVIEW_SOFT_ROUNDS = 5 | ||
| 1797 | const REVIEW_HARD_ROUNDS = 10 | 2535 | const REVIEW_HARD_ROUNDS = 10 |
| 1798 | 2536 | ||
| 1799 | // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 | 2537 | // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 |
| 1800 | -async function flipDocs08Checkbox(fe, id, phase, grp) { | ||
| 1801 | - const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) | 2538 | +// 注意:docs/08 **checkbox** 发生在模块分支上(approve 后逐功能勾选)→ 穿线 c; |
| 2539 | +// docs/08 **里程碑字段**发生在 merge 后的默认分支上 → 主根专属(见 runMilestone),两组不要搞混。 | ||
| 2540 | +async function flipDocs08Checkbox(fe, id, phase, grp, c) { | ||
| 2541 | + const cb = await agent(readDocs08CheckboxPromptM(fe, id, c), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) | ||
| 1802 | if (!cb) { log(`docs08-checkbox ${id}: 读取子代理无返回(不阻断)`); return } | 2542 | if (!cb) { log(`docs08-checkbox ${id}: 读取子代理无返回(不阻断)`); return } |
| 1803 | if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } | 2543 | if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } |
| 1804 | if (cb.state === 'checked') return | 2544 | if (cb.state === 'checked') return |
| 1805 | if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } | 2545 | if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } |
| 1806 | - const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) | 2546 | + const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber, c), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) |
| 1807 | if (!wr || !wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${(wr && wr.error) || ''}),跳过——cosmetic,不阻断`) | 2547 | if (!wr || !wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${(wr && wr.error) || ''}),跳过——cosmetic,不阻断`) |
| 1808 | } | 2548 | } |
| 1809 | 2549 | ||
| 1810 | -async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | 2550 | +// rwOpts.flipDocs08(Phase F Task 13):false 时 approve 路径跳过 docs/08 checkbox 勾选——feature |
| 2551 | +// lane 内相邻 REQ 行的并行编辑会在批后合并时制造无谓 docs/08 冲突(冲突即整链串行重跑,cosmetic | ||
| 2552 | +// 副作用不配这个代价),勾选由批后收口在模块分支统一补落。opts 接在 c 之后是继 deriveSpecPrompt | ||
| 2553 | +// ({batch}) 之后"c 为末位参数"约定的第二个特例(同因:带默认值的 opts 不动既有调用点),后续勿仿。 | ||
| 2554 | +async function reviewWithFixLoop(id, phase, verifyResult, specPath, c, { flipDocs08 = true } = {}) { | ||
| 1811 | const grp = phase === 'backend' ? 'Backend' : 'Frontend' | 2555 | const grp = phase === 'backend' ? 'Backend' : 'Frontend' |
| 1812 | const fe = isFrontend(phase) | 2556 | const fe = isFrontend(phase) |
| 1813 | let lastVerify = verifyResult | 2557 | let lastVerify = verifyResult |
| @@ -1817,7 +2561,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | @@ -1817,7 +2561,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | ||
| 1817 | const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' | 2561 | const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' |
| 1818 | // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 | 2562 | // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 |
| 1819 | const r = await agentR( | 2563 | const r = await agentR( |
| 1820 | - reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance), | 2564 | + reviewPrompt(id, phase, round, lastVerifySummary, specPath, c) + adjGuidance(reviewGuidance), |
| 1821 | {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'erp-workflow:code-reviewer'} | 2565 | {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'erp-workflow:code-reviewer'} |
| 1822 | ) | 2566 | ) |
| 1823 | reviewGuidance = '' // 已消费 | 2567 | reviewGuidance = '' // 已消费 |
| @@ -1826,7 +2570,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | @@ -1826,7 +2570,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | ||
| 1826 | // approve = 静态 review 通过即放行(前后端同构)。行为验收已挪到阶段末尾的 phase('Behavior') | 2570 | // approve = 静态 review 通过即放行(前后端同构)。行为验收已挪到阶段末尾的 phase('Behavior') |
| 1827 | // 一次性阶段级行为门(runBehaviorGate)——不再是 per-FE approve 子门。req-done/<FE> 语义 =「静态过」; | 2571 | // 一次性阶段级行为门(runBehaviorGate)——不再是 per-FE approve 子门。req-done/<FE> 语义 =「静态过」; |
| 1828 | // 行为维度由阶段门统一验收,行为 green 是 milestone 的前置(reportPrompt 校验行为证据非 RED)。 | 2572 | // 行为维度由阶段门统一验收,行为 green 是 milestone 的前置(reportPrompt 校验行为证据非 RED)。 |
| 1829 | - await flipDocs08Checkbox(fe, id, phase, grp) | 2573 | + if (flipDocs08) await flipDocs08Checkbox(fe, id, phase, grp, c) |
| 1830 | return { id, phase, approved:true, rounds:round } | 2574 | return { id, phase, approved:true, rounds:round } |
| 1831 | } | 2575 | } |
| 1832 | 2576 | ||
| @@ -1838,10 +2582,11 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | @@ -1838,10 +2582,11 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | ||
| 1838 | // 无任何可执行 must-fix(空 issues 或全缺 locator)→ 仲裁,而非直接 halt。 | 2582 | // 无任何可执行 must-fix(空 issues 或全缺 locator)→ 仲裁,而非直接 halt。 |
| 1839 | const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`, | 2583 | const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`, |
| 1840 | { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)', | 2584 | { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)', |
| 1841 | - reviewerIssues: r.issues || [] }, grp, round) | 2585 | + reviewerIssues: r.issues || [] }, grp, round, c.root) |
| 1842 | // continue 视为「无 must-fix → 静态 approve」(行为维度由阶段末尾的行为门统一验收,不在此处)。 | 2586 | // continue 视为「无 must-fix → 静态 approve」(行为维度由阶段末尾的行为门统一验收,不在此处)。 |
| 1843 | if (verdict.action === 'continue') { | 2587 | if (verdict.action === 'continue') { |
| 1844 | - await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } | 2588 | + if (flipDocs08) await flipDocs08Checkbox(fe, id, phase, grp, c) |
| 2589 | + return { id, phase, approved:true, rounds:round } | ||
| 1845 | } | 2590 | } |
| 1846 | if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`) | 2591 | if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`) |
| 1847 | reviewGuidance = verdict.guidance || '' // retry:带 guidance 重判(进入下一轮) | 2592 | reviewGuidance = verdict.guidance || '' // retry:带 guidance 重判(进入下一轮) |
| @@ -1849,14 +2594,14 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | @@ -1849,14 +2594,14 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | ||
| 1849 | } | 2594 | } |
| 1850 | lastIssuesCount = issues.length | 2595 | lastIssuesCount = issues.length |
| 1851 | 2596 | ||
| 1852 | - await runStage(g => fixPrompt(id, phase, issues) + g, { | ||
| 1853 | - site:`fix:${phase}:${id}:r${round}`, grp, label:`fix:${phase}:${id}:r${round}`, allowContinue: true, | 2597 | + await runStage(g => fixPrompt(id, phase, issues, c) + g, { |
| 2598 | + site:`fix:${phase}:${id}:r${round}`, grp, label:`fix:${phase}:${id}:r${round}`, allowContinue: true, dec: c.decisions, root: c.root, | ||
| 1854 | }) | 2599 | }) |
| 1855 | 2600 | ||
| 1856 | // reverify allowContinue:false:fix 后复验红色 = 修复没真正生效,绝不 continue 放行去 approve。 | 2601 | // reverify allowContinue:false:fix 后复验红色 = 修复没真正生效,绝不 continue 放行去 approve。 |
| 1857 | lastVerify = await runStage( | 2602 | lastVerify = await runStage( |
| 1858 | - g => verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验,上轮 must-fix: ${issues.length} 项)`, specPath, round) + g, | ||
| 1859 | - { site:`reverify:${phase}:${id}:r${round}`, grp, label:`reverify:${phase}:${id}:r${round}`, allowContinue: false }, | 2603 | + g => verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验,上轮 must-fix: ${issues.length} 项)`, specPath, round, c) + g, |
| 2604 | + { site:`reverify:${phase}:${id}:r${round}`, grp, label:`reverify:${phase}:${id}:r${round}`, allowContinue: false, dec: c.decisions, root: c.root }, | ||
| 1860 | ) | 2605 | ) |
| 1861 | 2606 | ||
| 1862 | if (round >= REVIEW_SOFT_ROUNDS) { | 2607 | if (round >= REVIEW_SOFT_ROUNDS) { |
| @@ -1864,7 +2609,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | @@ -1864,7 +2609,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | ||
| 1864 | // 此处仍有 reviewer 认定的可定位 must-fix 未清,仲裁不得凌驾专用 code-reviewer 直接判 approve(approve 只能来自 reviewer 本身)。 | 2609 | // 此处仍有 reviewer 认定的可定位 must-fix 未清,仲裁不得凌驾专用 code-reviewer 直接判 approve(approve 只能来自 reviewer 本身)。 |
| 1865 | const verdict = await adjudicate(`review-extend:${phase}:${id}`, | 2610 | const verdict = await adjudicate(`review-extend:${phase}:${id}`, |
| 1866 | { problem:`已 ${round} 轮 review 仍未 approve(上轮 ${lastIssuesCount} 项可定位 must-fix 未清)`, | 2611 | { problem:`已 ${round} 轮 review 仍未 approve(上轮 ${lastIssuesCount} 项可定位 must-fix 未清)`, |
| 1867 | - lastVerify: lastVerify.summary || lastVerify.reason || '', allowContinue:false }, grp, round) | 2612 | + lastVerify: lastVerify.summary || lastVerify.reason || '', allowContinue:false }, grp, round, c.root) |
| 1868 | if (verdict.action !== 'retry') throw new Error(`HALT review-unresolved ${phase}:${id}: ${verdict.rationale || `${round} 轮仍有未修 must-fix`}`) | 2613 | if (verdict.action !== 'retry') throw new Error(`HALT review-unresolved ${phase}:${id}: ${verdict.rationale || `${round} 轮仍有未修 must-fix`}`) |
| 1869 | // retry → 继续跑到硬上限(approve 仍由后续轮的 reviewer 决定) | 2614 | // retry → 继续跑到硬上限(approve 仍由后续轮的 reviewer 决定) |
| 1870 | } | 2615 | } |
| @@ -1874,21 +2619,21 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | @@ -1874,21 +2619,21 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { | ||
| 1874 | 2619 | ||
| 1875 | // flake 重试:每个 attempt 写独立证据文件 `<id>-test-gate-r<attempt>.md`,不覆盖前一次 red 证据(report § ⑤ 用得到)。 | 2620 | // flake 重试:每个 attempt 写独立证据文件 `<id>-test-gate-r<attempt>.md`,不覆盖前一次 red 证据(report § ⑤ 用得到)。 |
| 1876 | // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 | 2621 | // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 |
| 1877 | -async function testGate(module, phase) { | 2622 | +async function testGate(module, phase, c) { |
| 1878 | let attempt = 1 | 2623 | let attempt = 1 |
| 1879 | - let g = await agentR(gatePrompt(module, phase, attempt), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) | 2624 | + let g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) |
| 1880 | if (g.status === 'red') { // 自动重试 1 次(防 flaky) | 2625 | if (g.status === 'red') { // 自动重试 1 次(防 flaky) |
| 1881 | attempt = 2 | 2626 | attempt = 2 |
| 1882 | - g = await agentR(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) | 2627 | + g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) |
| 1883 | } | 2628 | } |
| 1884 | // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 | 2629 | // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 |
| 1885 | for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { | 2630 | for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { |
| 1886 | const verdict = await adjudicate(`test-gate-red:${phase}:${module.id}`, | 2631 | const verdict = await adjudicate(`test-gate-red:${phase}:${module.id}`, |
| 1887 | - { problem:`test-gate 第 ${attempt} 次仍 red`, failures: g.failures || [], allowContinue:false }, 'Gate', adj) | 2632 | + { problem:`test-gate 第 ${attempt} 次仍 red`, failures: g.failures || [], allowContinue:false }, 'Gate', adj, c.root) |
| 1888 | if (verdict.action !== 'retry') | 2633 | if (verdict.action !== 'retry') |
| 1889 | throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) | 2634 | throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) |
| 1890 | attempt += 1 // retry:再跑一个独立 attempt 证据文件 | 2635 | attempt += 1 // retry:再跑一个独立 attempt 证据文件 |
| 1891 | - g = await agentR(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) | 2636 | + g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) |
| 1892 | } | 2637 | } |
| 1893 | if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) | 2638 | if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) |
| 1894 | return g | 2639 | return g |
| @@ -1919,20 +2664,20 @@ const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed' | @@ -1919,20 +2664,20 @@ const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed' | ||
| 1919 | // runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 | 2664 | // runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 |
| 1920 | // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。 | 2665 | // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。 |
| 1921 | // behaviorRound:阶段门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 | 2666 | // behaviorRound:阶段门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 |
| 1922 | -async function runBehaviorGateOnce(feItems, behaviorRound) { | 2667 | +async function runBehaviorGateOnce(feItems, behaviorRound, c) { |
| 1923 | const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` | 2668 | const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` |
| 1924 | let attempt = 1 | 2669 | let attempt = 1 |
| 1925 | - let bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), | 2670 | + let bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), |
| 1926 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | 2671 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 1927 | - recordDecisions('behavior:frontend-phase', bg.decisions) | 2672 | + recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) |
| 1928 | 2673 | ||
| 1929 | // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 | 2674 | // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 |
| 1930 | // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 | 2675 | // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 |
| 1931 | while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { | 2676 | while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { |
| 1932 | attempt += 1 | 2677 | attempt += 1 |
| 1933 | - bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), | 2678 | + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), |
| 1934 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | 2679 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 1935 | - recordDecisions('behavior:frontend-phase', bg.decisions) | 2680 | + recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) |
| 1936 | } | 2681 | } |
| 1937 | let envState = behaviorEnvBlocked(bg) | 2682 | let envState = behaviorEnvBlocked(bg) |
| 1938 | for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { | 2683 | for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { |
| @@ -1945,12 +2690,12 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { | @@ -1945,12 +2690,12 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { | ||
| 1945 | { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, | 2690 | { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, |
| 1946 | riders: { interactionFailures: behaviorIfails(bg).length, styleIssues: (bg.styleIssues || []).length, | 2691 | riders: { interactionFailures: behaviorIfails(bg).length, styleIssues: (bg.styleIssues || []).length, |
| 1947 | sentinelTextIssues: (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length }, | 2692 | sentinelTextIssues: (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length }, |
| 1948 | - allowContinue:false }, 'Behavior', adj) | 2693 | + allowContinue:false }, 'Behavior', adj, c.root) |
| 1949 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) | 2694 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) |
| 1950 | attempt += 1 | 2695 | attempt += 1 |
| 1951 | - bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), | 2696 | + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), |
| 1952 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | 2697 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 1953 | - recordDecisions('behavior:frontend-phase', bg.decisions) | 2698 | + recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) |
| 1954 | envState = behaviorEnvBlocked(bg) | 2699 | envState = behaviorEnvBlocked(bg) |
| 1955 | } | 2700 | } |
| 1956 | if (envState.blocked) throw new Error(`HALT behavior-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) | 2701 | if (envState.blocked) throw new Error(`HALT behavior-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) |
| @@ -1960,18 +2705,18 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { | @@ -1960,18 +2705,18 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { | ||
| 1960 | // runBehaviorGate:阶段级行为门主循环(被顶层 frontend 段调用,phase('Behavior') 下)。green 才正常返回(放行进 testGate)。 | 2705 | // runBehaviorGate:阶段级行为门主循环(被顶层 frontend 段调用,phase('Behavior') 下)。green 才正常返回(放行进 testGate)。 |
| 1961 | // softPassed:本函数内声明,跨 behaviorRound 持久(软文字一旦放行不再追问,避免反复消耗仲裁预算)。 | 2706 | // softPassed:本函数内声明,跨 behaviorRound 持久(软文字一旦放行不再追问,避免反复消耗仲裁预算)。 |
| 1962 | // green ≡ behaviorHard.length===0 ∧ envError===none ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空 ∧ 无未解释漏达路由。 | 2707 | // green ≡ behaviorHard.length===0 ∧ envError===none ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空 ∧ 无未解释漏达路由。 |
| 1963 | -async function runBehaviorGate(feItems) { | 2708 | +async function runBehaviorGate(feItems, c) { |
| 1964 | const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` | 2709 | const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` |
| 1965 | const softPassed = new Set() | 2710 | const softPassed = new Set() |
| 1966 | for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_STAGE_MAX; behaviorRound++) { | 2711 | for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_STAGE_MAX; behaviorRound++) { |
| 1967 | - const bg = await runBehaviorGateOnce(feItems, behaviorRound) | 2712 | + const bg = await runBehaviorGateOnce(feItems, behaviorRound, c) |
| 1968 | 2713 | ||
| 1969 | // 1) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 | 2714 | // 1) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 |
| 1970 | // locator-not-resolvable / scope-missing 在 §3.5 单独阻断 green。 | 2715 | // locator-not-resolvable / scope-missing 在 §3.5 单独阻断 green。 |
| 1971 | for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { | 2716 | for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { |
| 1972 | if (!cg) continue | 2717 | if (!cg) continue |
| 1973 | recordDecisions('behavior-coverage:frontend-phase', | 2718 | recordDecisions('behavior-coverage:frontend-phase', |
| 1974 | - [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }]) | 2719 | + [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }], c.decisions) |
| 1975 | } | 2720 | } |
| 1976 | 2721 | ||
| 1977 | // 2) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。 | 2722 | // 2) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。 |
| @@ -1983,10 +2728,10 @@ async function runBehaviorGate(feItems) { | @@ -1983,10 +2728,10 @@ async function runBehaviorGate(feItems) { | ||
| 1983 | const site = `behavior-text:${ti.page || '?'}:${ti.region || '?'}` | 2728 | const site = `behavior-text:${ti.page || '?'}:${ti.region || '?'}` |
| 1984 | const verdict = await adjudicate(site, | 2729 | const verdict = await adjudicate(site, |
| 1985 | { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 green):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, | 2730 | { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 green):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, |
| 1986 | - textIssue: ti, allowContinue: true }, 'Behavior', behaviorRound) | 2731 | + textIssue: ti, allowContinue: true }, 'Behavior', behaviorRound, c.root) |
| 1987 | if (verdict.action === 'continue') { | 2732 | if (verdict.action === 'continue') { |
| 1988 | recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`, | 2733 | recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`, |
| 1989 | - choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }]) | 2734 | + choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }], c.decisions) |
| 1990 | softPassed.add(regionKey(ti)); continue | 2735 | softPassed.add(regionKey(ti)); continue |
| 1991 | } | 2736 | } |
| 1992 | if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || `文字不符 source=${ti.source}`}`) | 2737 | if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || `文字不符 source=${ti.source}`}`) |
| @@ -2002,7 +2747,7 @@ async function runBehaviorGate(feItems) { | @@ -2002,7 +2747,7 @@ async function runBehaviorGate(feItems) { | ||
| 2002 | const summary = bClass.map(cg => `[${cg.reason}] ${cg.page} — ${cg.detail}`).join('; ') | 2747 | const summary = bClass.map(cg => `[${cg.reason}] ${cg.page} — ${cg.detail}`).join('; ') |
| 2003 | const verdict = await adjudicate('behavior-bclass:frontend-phase', | 2748 | const verdict = await adjudicate('behavior-bclass:frontend-phase', |
| 2004 | { problem:`behavior 不可降级的未覆盖(B 类反查不出 / FE 作用域缺失,阻断 green):${summary}`, | 2749 | { problem:`behavior 不可降级的未覆盖(B 类反查不出 / FE 作用域缺失,阻断 green):${summary}`, |
| 2005 | - coverageGaps: bClass, allowContinue:false }, 'Behavior', behaviorRound) | 2750 | + coverageGaps: bClass, allowContinue:false }, 'Behavior', behaviorRound, c.root) |
| 2006 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass frontend-phase: ${verdict.rationale || summary}`) | 2751 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass frontend-phase: ${verdict.rationale || summary}`) |
| 2007 | continue // retry → 重跑行为验收(下一 behaviorRound) | 2752 | continue // retry → 重跑行为验收(下一 behaviorRound) |
| 2008 | } | 2753 | } |
| @@ -2023,7 +2768,7 @@ async function runBehaviorGate(feItems) { | @@ -2023,7 +2768,7 @@ async function runBehaviorGate(feItems) { | ||
| 2023 | if (planned > 0 && unaccounted > 0) { | 2768 | if (planned > 0 && unaccounted > 0) { |
| 2024 | const verdict = await adjudicate('behavior-undercoverage:frontend-phase', | 2769 | const verdict = await adjudicate('behavior-undercoverage:frontend-phase', |
| 2025 | { problem:`路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`, | 2770 | { problem:`路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`, |
| 2026 | - coverageGaps: bg.coverageGaps || [], allowContinue: false }, 'Behavior', behaviorRound) | 2771 | + coverageGaps: bg.coverageGaps || [], allowContinue: false }, 'Behavior', behaviorRound, c.root) |
| 2027 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage frontend-phase: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`) | 2772 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage frontend-phase: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`) |
| 2028 | continue // retry → 下一 behaviorRound 重跑整门 | 2773 | continue // retry → 下一 behaviorRound 重跑整门 |
| 2029 | } | 2774 | } |
| @@ -2048,7 +2793,7 @@ async function runBehaviorGate(feItems) { | @@ -2048,7 +2793,7 @@ async function runBehaviorGate(feItems) { | ||
| 2048 | if (bg.status === 'red' && !hasAnyClassifiedSignal) { | 2793 | if (bg.status === 'red' && !hasAnyClassifiedSignal) { |
| 2049 | const verdict = await adjudicate('behavior-red-unclassified:frontend-phase', | 2794 | const verdict = await adjudicate('behavior-red-unclassified:frontend-phase', |
| 2050 | { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / styleIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green', | 2795 | { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / styleIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green', |
| 2051 | - behaviorResult: bg, allowContinue:false }, 'Behavior', behaviorRound) | 2796 | + behaviorResult: bg, allowContinue:false }, 'Behavior', behaviorRound, c.root) |
| 2052 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified frontend-phase: ${verdict.rationale || 'status:red 无分类原因'}`) | 2797 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified frontend-phase: ${verdict.rationale || 'status:red 无分类原因'}`) |
| 2053 | continue | 2798 | continue |
| 2054 | } | 2799 | } |
| @@ -2066,7 +2811,7 @@ async function runBehaviorGate(feItems) { | @@ -2066,7 +2811,7 @@ async function runBehaviorGate(feItems) { | ||
| 2066 | const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') | 2811 | const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') |
| 2067 | const verdict = await adjudicate('behavior-noloc-hard:frontend-phase', | 2812 | const verdict = await adjudicate('behavior-noloc-hard:frontend-phase', |
| 2068 | { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue 放行):${summary}`, | 2813 | { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue 放行):${summary}`, |
| 2069 | - interactionFailures: noLoc, allowContinue:false }, 'Behavior', behaviorRound) | 2814 | + interactionFailures: noLoc, allowContinue:false }, 'Behavior', behaviorRound, c.root) |
| 2070 | if (verdict.action !== 'retry') | 2815 | if (verdict.action !== 'retry') |
| 2071 | throw new Error(`HALT behavior-noloc-hard frontend-phase: ${verdict.rationale || summary}`) | 2816 | throw new Error(`HALT behavior-noloc-hard frontend-phase: ${verdict.rationale || summary}`) |
| 2072 | continue // retry → 重跑行为验收(下一 behaviorRound) | 2817 | continue // retry → 重跑行为验收(下一 behaviorRound) |
| @@ -2079,16 +2824,16 @@ async function runBehaviorGate(feItems) { | @@ -2079,16 +2824,16 @@ async function runBehaviorGate(feItems) { | ||
| 2079 | locator: f.locator, | 2824 | locator: f.locator, |
| 2080 | severity: 'high', | 2825 | severity: 'high', |
| 2081 | })) | 2826 | })) |
| 2082 | - await runStage(g => fixPrompt('frontend-phase', 'frontend', fixIssues) + g, { | ||
| 2083 | - site:`behavior-fix:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-fix:r${behaviorRound}`, allowContinue: true, | 2827 | + await runStage(g => fixPrompt('frontend-phase', 'frontend', fixIssues, c) + g, { |
| 2828 | + site:`behavior-fix:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-fix:r${behaviorRound}`, allowContinue: true, dec: c.decisions, root: c.root, | ||
| 2084 | }) | 2829 | }) |
| 2085 | 2830 | ||
| 2086 | // 8) fix 后功能复验(allowContinue:false):行为 fix 改的是 frontend/ UI 源码,可能引入功能回归—— | 2831 | // 8) fix 后功能复验(allowContinue:false):行为 fix 改的是 frontend/ UI 源码,可能引入功能回归—— |
| 2087 | // 先跑全量前端单测(不起全栈、不跑 e2e,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。 | 2832 | // 先跑全量前端单测(不起全栈、不跑 e2e,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。 |
| 2088 | // (e2e 维度由下一轮行为门 + 后续阶段 testGate 全量回归兜底。) | 2833 | // (e2e 维度由下一轮行为门 + 后续阶段 testGate 全量回归兜底。) |
| 2089 | await runStage( | 2834 | await runStage( |
| 2090 | - g => behaviorReverifyPrompt(behaviorRound, fixIssues.length) + g, | ||
| 2091 | - { site:`behavior-reverify:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-reverify:r${behaviorRound}`, allowContinue: false }, | 2835 | + g => behaviorReverifyPrompt(behaviorRound, fixIssues.length, c) + g, |
| 2836 | + { site:`behavior-reverify:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-reverify:r${behaviorRound}`, allowContinue: false, dec: c.decisions, root: c.root }, | ||
| 2092 | ) | 2837 | ) |
| 2093 | // 进入下一 behaviorRound → 重跑行为验收 | 2838 | // 进入下一 behaviorRound → 重跑行为验收 |
| 2094 | } | 2839 | } |
| @@ -2136,6 +2881,129 @@ for (const m of routed.modules) { | @@ -2136,6 +2881,129 @@ for (const m of routed.modules) { | ||
| 2136 | const todo = routed.modules.filter(m => !m.done) | 2881 | const todo = routed.modules.filter(m => !m.done) |
| 2137 | log(`coding: ${todo.length}/${routed.modules.length} modules to run`) | 2882 | log(`coding: ${todo.length}/${routed.modules.length} modules to run`) |
| 2138 | 2883 | ||
| 2884 | +// ============================================================================ | ||
| 2885 | +// 调度器(Phase B):LLM 出依赖边 + JS 拓扑波次。Phase D 的波次主循环消费——useParallel 开启时 | ||
| 2886 | +// 起跑前调一次 runScheduler 标注 deps,逐波 nextWave 取 ready 集;关闭时直接 serialChainDeps。 | ||
| 2887 | +// 降级原则(fail-open to serial):任何调度环节失败(scheduler agent null / schema 违例 / | ||
| 2888 | +// 仲裁耗尽 / 图卡死)一律 log 后降级 docs/02 全序(todo 顺序链式依赖),**绝不 halt**—— | ||
| 2889 | +// 错误并行损失正确性,错误串行只损失时间,调度永远不该成为新的停机点。 | ||
| 2890 | +// ============================================================================ | ||
| 2891 | + | ||
| 2892 | +// 波次宽度上限(确定性,仅由 args 导出)。Task 9 主循环直接复用本 const,勿重复定义; | ||
| 2893 | +// 是否启用并行由 Task 9 的 useParallel 闸决定,这里只约束"同波次最多取几个"。 | ||
| 2894 | +const maxWidth = Math.max(1, ARGS?.parallel?.maxWidth ?? 3) | ||
| 2895 | + | ||
| 2896 | +// 降级形态:docs/02 全序 = todo 顺序链式依赖(每项依赖紧邻前项)。 | ||
| 2897 | +// nextWave 在此图上每轮恰出 1 个,与现行串行主循环逐模块推进完全一致。 | ||
| 2898 | +function serialChainDeps(todo) { | ||
| 2899 | + return todo.map((m, i) => ({ ...m, deps: i > 0 ? [todo[i - 1].id] : [] })) | ||
| 2900 | +} | ||
| 2901 | + | ||
| 2902 | +// 仿 routerViolation 形态:返回 null=通过 / 违例描述串(喂 adjudicate guidance 重跑 scheduler)。 | ||
| 2903 | +// 检查:非 todo 后端 id / 重复项 / 缺项(完整性)/ 依赖指向未知模块 / 自依赖 / 环(Kahn 消解)。 | ||
| 2904 | +// 与 router 不同:违例最终不 halt,由 runScheduler 降级全序兜底(调度失败只损失并行收益)。 | ||
| 2905 | +function scheduleViolation(deps, todoBackendIds, allBackendIds) { | ||
| 2906 | + const todoSet = new Set(todoBackendIds) | ||
| 2907 | + const knownSet = new Set(allBackendIds) | ||
| 2908 | + const seen = new Set() | ||
| 2909 | + for (const d of deps) { | ||
| 2910 | + if (!todoSet.has(d.id)) return `deps 含非 todo 后端模块 id:${d.id}(只允许对待跑后端模块输出;frontend-phase 由 JS 硬规则处理)` | ||
| 2911 | + if (seen.has(d.id)) return `deps 含重复模块项:${d.id}(重复项会静默丢边,必须合并为一项)` | ||
| 2912 | + seen.add(d.id) | ||
| 2913 | + for (const e of d.dependsOn) { | ||
| 2914 | + if (e.id === d.id) return `模块 ${d.id} 自依赖` | ||
| 2915 | + if (!knownSet.has(e.id)) return `模块 ${d.id} 的依赖指向未知模块 id:${e.id}` | ||
| 2916 | + } | ||
| 2917 | + } | ||
| 2918 | + // 完整性:deps 必须**恰好**逐一覆盖全部 todo 后端模块。LLM 漏报的模块若静默放过, | ||
| 2919 | + // annotateDeps 会把它当"无依赖"放进首波最大并行——与保守偏置(宁串勿并)正相反,缺项必须打回。 | ||
| 2920 | + const missing = todoBackendIds.filter(id => !seen.has(id)) | ||
| 2921 | + if (missing.length) return `deps 缺待跑后端模块项:${missing.join('、')}(每个待跑模块必须恰好一项,无依赖也要输出 dependsOn: [])` | ||
| 2922 | + // 环检测(Kahn 消解):只看 todo 内部边——指向已完成模块的边恒满足,不参与成环。 | ||
| 2923 | + const blocking = new Map(deps.map(d => [d.id, d.dependsOn.map(e => e.id).filter(x => todoSet.has(x))])) | ||
| 2924 | + let rest = [...todoBackendIds] | ||
| 2925 | + const settled = new Set() | ||
| 2926 | + while (rest.length) { | ||
| 2927 | + const ready = rest.filter(id => (blocking.get(id) || []).every(x => settled.has(x))) | ||
| 2928 | + if (!ready.length) return `依赖图成环(Kahn 无法消解):${rest.join('、')}` | ||
| 2929 | + for (const id of ready) settled.add(id) | ||
| 2930 | + rest = rest.filter(id => !settled.has(id)) | ||
| 2931 | + } | ||
| 2932 | + return null | ||
| 2933 | +} | ||
| 2934 | + | ||
| 2935 | +// 成功路径标注:把校验通过的依赖边落到 todo 模块项上(deps = 依赖模块 id 数组,nextWave 消费; | ||
| 2936 | +// deps 可含已完成模块 id——Task 9 的 doneSet 初值含 routed 里 done:true 的模块,恒满足)。 | ||
| 2937 | +// frontend-phase 不经 LLM:JS 硬规则强制依赖**全部**后端模块(前端聚合消费所有后端契约/种子)。 | ||
| 2938 | +function annotateDeps(todo, deps, allBackendIds) { | ||
| 2939 | + const byId = new Map(deps.map(d => [d.id, d.dependsOn.map(e => e.id)])) | ||
| 2940 | + return todo.map(m => m.id === 'frontend-phase' | ||
| 2941 | + ? { ...m, deps: [...allBackendIds] } | ||
| 2942 | + // `|| []` 纯防御:scheduleViolation 的完整性校验保证每个 todo 后端模块在 deps 恰有一项, | ||
| 2943 | + // 校验通过后此 fallback 理论不可达——仅 0/1 模块免调度的空边集路径会走到(语义恰为"无依赖")。 | ||
| 2944 | + : { ...m, deps: byId.get(m.id) || [] }) | ||
| 2945 | +} | ||
| 2946 | + | ||
| 2947 | +// nextWave:Kahn 拓扑取下一波——remaining 中 deps ⊆ doneSet 的模块,按 docs/02 原序(remaining 保序) | ||
| 2948 | +// 取前 maxWidth 个。纯函数、不依赖 agent;coding.mjs 无法 node --test,以下内联 case 自证正确性: | ||
| 2949 | +// ① 菱形(b、c 依赖 a;d 依赖 b+c):done=∅ → [a];done={a} → [b,c];done={a,b,c} → [d]。 | ||
| 2950 | +// ② 链式(serialChainDeps 降级形态即此,b 依赖 a、c 依赖 b):每轮恰出 1 个,与现行串行全序一致。 | ||
| 2951 | +// ③ 全独立 a,b,c,d(maxWidth=3):第一轮 [a,b,c](原序取前 3),第二轮 [d]。 | ||
| 2952 | +// ④ 环 a⇄b(理论上 scheduleViolation 已排除):ready 恒空 → log 后按原序硬出 remaining[0]; | ||
| 2953 | +// a 跑完进 doneSet 后 b 的边随之消解——行为收敛于 docs/02 全序,绝不 halt、绝不空转。 | ||
| 2954 | +function nextWave(remaining, doneSet) { | ||
| 2955 | + const ready = remaining.filter(m => (m.deps || []).every(d => doneSet.has(d))) | ||
| 2956 | + if (!ready.length && remaining.length) { | ||
| 2957 | + log(`schedule: 图卡死(ready 空而 remaining=${remaining.map(m => m.id).join('、')}),降级 docs/02 全序逐个推进`) | ||
| 2958 | + return remaining.slice(0, 1) | ||
| 2959 | + } | ||
| 2960 | + return ready.slice(0, maxWidth) | ||
| 2961 | +} | ||
| 2962 | + | ||
| 2963 | +// runScheduler:调度执行点(波次主循环起跑前、useParallel 开启时调用一次)。返回 todo 模块数组 | ||
| 2964 | +// (保 docs/02 原序)每项追加 deps: string[],供 nextWave 消费。永不 throw: | ||
| 2965 | +// 一切失败路径走 serialChainDeps 降级(见上方设计注释),调度只能让流程变快、不能让它停下。 | ||
| 2966 | +async function runScheduler(todo, routedModules) { | ||
| 2967 | + const allBackendIds = routedModules.filter(m => m.id !== 'frontend-phase').map(m => m.id) | ||
| 2968 | + const todoBackendIds = todo.filter(m => m.id !== 'frontend-phase').map(m => m.id) | ||
| 2969 | + // 0/1 个 todo 后端模块:模块间不存在任何可判边(dependsOn 只能指向 done 模块,恒满足)—— | ||
| 2970 | + // 免去一次取证子代理,免校验直接按"无依赖"标注(不经 scheduleViolation:其完整性校验会把 | ||
| 2971 | + // 空边集判成缺项,但此处"无可判边"本身就是结论,无需取证也无需打回)。 | ||
| 2972 | + // phase('Schedule') 在早退**之后**才切:resume 全 done / 仅剩单模块的项目什么调度都没发生, | ||
| 2973 | + // 不该让 UI 终态从 Router 漂移到 Schedule。 | ||
| 2974 | + if (todoBackendIds.length <= 1) return annotateDeps(todo, [], allBackendIds) | ||
| 2975 | + phase('Schedule') | ||
| 2976 | + const degrade = (why) => { | ||
| 2977 | + log(`schedule: ${why} → 降级 docs/02 全序(todo 顺序链式依赖),不 halt`) | ||
| 2978 | + return serialChainDeps(todo) | ||
| 2979 | + } | ||
| 2980 | + // 整体 try/catch:fail-open 不能只依赖 agent() 永不 reject 的契约——adjudicate / 校验等任何 | ||
| 2981 | + // runtime 抛错同样必须降级全序,调度绝不把整个 Workflow 顶层带崩(与"永不 throw"承诺名副其实)。 | ||
| 2982 | + try { | ||
| 2983 | + let sched = await agent(schedulerPrompt(todoBackendIds, allBackendIds, ROOT), | ||
| 2984 | + { label: 'scheduler', phase: 'Schedule', schema: SCHEDULE_SCHEMA }) | ||
| 2985 | + if (!sched) return degrade(`scheduler agent 无返回(${NULL_AGENT_PROBLEM})`) | ||
| 2986 | + for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) { | ||
| 2987 | + const violation = scheduleViolation(sched.deps, todoBackendIds, allBackendIds) | ||
| 2988 | + if (!violation) break | ||
| 2989 | + const verdict = await adjudicate('schedule-violation', { problem: violation }, 'Schedule', adj) | ||
| 2990 | + if (verdict.action !== 'retry') return degrade(`调度违例且仲裁未予 retry(${verdict.rationale || violation})`) | ||
| 2991 | + sched = await agent(schedulerPrompt(todoBackendIds, allBackendIds, ROOT) + adjGuidance(verdict.guidance || ''), | ||
| 2992 | + { label: `scheduler:r${adj + 1}`, phase: 'Schedule', schema: SCHEDULE_SCHEMA }) | ||
| 2993 | + if (!sched) return degrade(`scheduler agent 重跑无返回(${NULL_AGENT_PROBLEM})`) | ||
| 2994 | + } | ||
| 2995 | + const finalViolation = scheduleViolation(sched.deps, todoBackendIds, allBackendIds) | ||
| 2996 | + if (finalViolation) return degrade(`${ADJUDICATE_MAX} 轮仲裁后仍违例:${finalViolation}`) | ||
| 2997 | + // 审计可见性:依赖边落 log,人工可复盘"为什么这两个模块没并行"(evidence 详情在子代理 transcript)。 | ||
| 2998 | + for (const d of sched.deps) { | ||
| 2999 | + if (d.dependsOn.length) log(`schedule: ${d.id} ← ${d.dependsOn.map(e => `${e.id}[${e.kind}]`).join(' ')}`) | ||
| 3000 | + } | ||
| 3001 | + return annotateDeps(todo, sched.deps, allBackendIds) | ||
| 3002 | + } catch (e) { | ||
| 3003 | + return degrade(`调度 runtime 异常(${String(e?.message || e)})`) | ||
| 3004 | + } | ||
| 3005 | +} | ||
| 3006 | + | ||
| 2139 | // P1#3:首跑建需求台账基线(幂等、best-effort),使日后 /add-req 能正确识别增量、免空跑。 | 3007 | // P1#3:首跑建需求台账基线(幂等、best-effort),使日后 /add-req 能正确识别增量、免空跑。 |
| 2140 | await ensureLedgerBaseline() | 3008 | await ensureLedgerBaseline() |
| 2141 | 3009 | ||
| @@ -2148,78 +3016,342 @@ if (todo.length) { | @@ -2148,78 +3016,342 @@ if (todo.length) { | ||
| 2148 | log('preflight: 环境探测通过') | 3016 | log('preflight: 环境探测通过') |
| 2149 | } | 3017 | } |
| 2150 | 3018 | ||
| 2151 | -const results = [] | ||
| 2152 | -let haltedAtIdx = -1 | ||
| 2153 | -let flushedCount = 0 | ||
| 2154 | -for (const [idx, module] of todo.entries()) { | ||
| 2155 | - const decStart = autonomousDecisions.length // 本模块自主决策水位线(P1#1 增量 flush 用) | 3019 | +// ── 全局互斥锁(Task 9 Step 1;附录补 9 / 补 13)──────────────────────────── |
| 3020 | +// promise 链实现:无原生锁可用,且运行时禁时间/随机源——链式 then 即"排队",确定性、零轮询。 | ||
| 3021 | +// fn 抛错不破坏链:尾指针始终指向 catch(()=>{}) 后的节点,下一个获取者照常排队;调用方拿到的 | ||
| 3022 | +// run 仍保留原始 rejection(halt 语义不被锁吞掉)。两把锁同构,统一由本工厂生成。 | ||
| 3023 | +// | ||
| 3024 | +// withMainRootLock:milestone(merge / docs/08 字段 / tag)与 RESUME 追加都作用于主根 + 默认分支, | ||
| 3025 | +// 并行下必须全局串行化(merge 冲突保持硬 halt 的设计原则不变)。runCrossModule 的 diff 基准读 | ||
| 3026 | +// 默认分支为**只读**,不需要锁。 | ||
| 3027 | +// withStackLock(补 9):冷起栈资源(config-vars 固定端口 + 进程树)全局唯一——相关 stage 串行化 | ||
| 3028 | +// 而非 lane 化:Seed 与 testGate(test.mjs 第 5 步 e2e 的 Playwright globalSetup 同按固定端口 | ||
| 3029 | +// 冷起后端,e2e 段不可单拆则整个 testGate 包锁)整段互斥。"按既知端口回收残留 pid"只允许在持锁 | ||
| 3030 | +// 的 stage 内做,否则后到者会把兄弟 lane 正在验证的后端当残留 kill 掉(≥2 宽波次必然踩中的确定 | ||
| 3031 | +// 性互杀)。Seed/testGate 占模块总时长比例小,串行损失远低于打穿三层端口配置;并行收益主体 | ||
| 3032 | +// (tdd/review/fix,不起栈、Spring 测试随机端口)完整保留、不进锁。 | ||
| 3033 | +// | ||
| 3034 | +// **非重入纪律(补 13,两把锁同适用)**:只允许 runModule 顶层 / loop-end 逐段获取 | ||
| 3035 | +// (`await withMainRootLock(() => runMilestone(m))`、`await withMainRootLock(() => recordResume(...))` | ||
| 3036 | +// 这种形态);被包函数及其调用链内**绝不**再调同一把锁,两把锁之间也**绝不**嵌套获取—— | ||
| 3037 | +// promise 链锁嵌套获取即互等死锁,且静默无诊断(Workflow 直接挂死)。 | ||
| 3038 | +function makeChainLock() { | ||
| 3039 | + let tail = Promise.resolve() | ||
| 3040 | + return (fn) => { | ||
| 3041 | + const run = tail.then(fn) | ||
| 3042 | + tail = run.catch(() => {}) | ||
| 3043 | + return run | ||
| 3044 | + } | ||
| 3045 | +} | ||
| 3046 | +const withMainRootLock = makeChainLock() | ||
| 3047 | +const withStackLock = makeChainLock() | ||
| 3048 | + | ||
| 3049 | +// ---- runModule:单模块全链(branchSetup → 后端段 → 前端段 → report → milestone → RESUME flush)---- | ||
| 3050 | +// **永不 throw**:内部 catch 全部 HALT 并结构化返回 { module, status, reason?, decisions }—— | ||
| 3051 | +// 这是它能放进 parallel() 的前提(parallel() 把 thunk 异常静默折成 null、永不 reject,throw 会丢失 halt 原因)。 | ||
| 3052 | +// c:模块执行上下文(makeCtx();串行 = 主根,lane 模式由 Phase C/D 填充)。 | ||
| 3053 | +// phase(...) 全局 UI 分组只在主根模式调用(c.lane == null):lane 并行下兄弟模块会竞态改全局 phase 状态, | ||
| 3054 | +// 而 agent 级 opts.phase(grp)已全覆盖分组——注意不能写 `!c.lane`(lane 序号 0 是合法 lane,falsy 会误判主根)。 | ||
| 3055 | +const resumeFlushed = new Set() // per-module RESUME flush 成功记账(替代 flushedCount 水位线;loop-end 据此补记失败模块) | ||
| 3056 | +async function runModule(module, c) { | ||
| 2156 | try { | 3057 | try { |
| 2157 | - phase('Milestone') | ||
| 2158 | - await runBranchSetup(module) | 3058 | + if (c.lane == null) phase('Milestone') |
| 3059 | + await runBranchSetup(module, c) | ||
| 2159 | if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过) | 3060 | if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过) |
| 2160 | - phase('Backend') | ||
| 2161 | - await featureLoop(module.reqs, 'backend') | ||
| 2162 | - phase('Gate') | ||
| 2163 | - await testGate(module, 'backend') | 3061 | + if (c.lane == null) phase('Backend') |
| 3062 | + await featureLoop(module.reqs, 'backend', c) | ||
| 3063 | + if (c.lane == null) phase('Gate') | ||
| 3064 | + // 起栈互斥(补 9):testGate 整段包锁——其 e2e 段 Playwright globalSetup 按固定端口冷起后端, | ||
| 3065 | + // 不可单拆。串行模式锁恒空闲,行为不变(统一纪律,免按模式分叉)。 | ||
| 3066 | + await withStackLock(() => testGate(module, 'backend', c)) | ||
| 2164 | // 演示种子生成 stage(Seed):在 testGate 后跑——此时本模块 schema(含 tdd 新增的 V<n> migration) | 3067 | // 演示种子生成 stage(Seed):在 testGate 后跑——此时本模块 schema(含 tdd 新增的 V<n> migration) |
| 2165 | // 已终态且全绿,按它生成的种子才不会撞结构。allowContinue:false——e2e 基线(globalSetup 注入)与行为门 | 3068 | // 已终态且全绿,按它生成的种子才不会撞结构。allowContinue:false——e2e 基线(globalSetup 注入)与行为门 |
| 2166 | // step2 子项③(演示种子注入)都依赖该种子,坏种子放行会让整个前端阶段(行为验收/e2e)在脏数据上全线误判。 | 3069 | // step2 子项③(演示种子注入)都依赖该种子,坏种子放行会让整个前端阶段(行为验收/e2e)在脏数据上全线误判。 |
| 2167 | - phase('Seed') | ||
| 2168 | - await runStage(g => seedGenPrompt(module) + g, | ||
| 2169 | - { site:`seed:${module.id}`, grp:'Seed', label:`seed:${module.id}`, allowContinue: false }) | ||
| 2170 | - phase('Milestone') | ||
| 2171 | - await runCrossModule(module) // 替代被删 hook,JS 编排:diff → 分类 → 写日志 | 3070 | + // 起栈互斥(补 9):Seed 冷起 gradle bootRun 占固定端口且会按既知端口回收"残留"pid——整段取锁。 |
| 3071 | + if (c.lane == null) phase('Seed') | ||
| 3072 | + await withStackLock(() => runStage(g => seedGenPrompt(module, c) + g, | ||
| 3073 | + { site:`seed:${module.id}`, grp:'Seed', label:`seed:${module.id}`, allowContinue: false, dec: c.decisions, root: c.root })) | ||
| 3074 | + if (c.lane == null) phase('Milestone') | ||
| 3075 | + await runCrossModule(module, c) // 替代被删 hook,JS 编排:diff → 分类 → 写日志 | ||
| 2172 | } | 3076 | } |
| 2173 | if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块) | 3077 | if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块) |
| 2174 | - phase('Frontend') | 3078 | + if (c.lane == null) phase('Frontend') |
| 2175 | // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy | 3079 | // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy |
| 2176 | // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, | 3080 | // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, |
| 2177 | // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 | 3081 | // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 |
| 2178 | - await runFrontendSkeleton(module.feItems) | 3082 | + await runFrontendSkeleton(module.feItems, c) |
| 2179 | // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。 | 3083 | // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。 |
| 2180 | - await featureLoop(module.feItems, 'frontend') | 3084 | + await featureLoop(module.feItems, 'frontend', c) |
| 2181 | // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 | 3085 | // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 |
| 2182 | // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 | 3086 | // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 |
| 2183 | // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 | 3087 | // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 |
| 2184 | - phase('Behavior') | ||
| 2185 | - await runBehaviorGate(module.feItems) | ||
| 2186 | - phase('Gate') | ||
| 2187 | - await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交 | 3088 | + if (c.lane == null) phase('Behavior') |
| 3089 | + // 行为门虽也冷起全栈,但只在 frontend-phase 终波(宽 1、主根)跑——无兄弟 lane 可互杀, | ||
| 3090 | + // 不进起栈互斥(补 9 范围仅 Seed / testGate)。 | ||
| 3091 | + await runBehaviorGate(module.feItems, c) | ||
| 3092 | + if (c.lane == null) phase('Gate') | ||
| 3093 | + // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交;起栈互斥(补 9)同后端段整段包锁。 | ||
| 3094 | + await withStackLock(() => testGate(module, 'frontend', c)) | ||
| 2188 | } | 3095 | } |
| 2189 | - phase('Milestone') | 3096 | + if (c.lane == null) phase('Milestone') |
| 2190 | // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— | 3097 | // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— |
| 2191 | // 绝不 continue 放行去打 milestone(否则可能在红色测试上 milestone)。 | 3098 | // 绝不 continue 放行去打 milestone(否则可能在红色测试上 milestone)。 |
| 2192 | - await runStage(g => reportPrompt(module) + g, { | ||
| 2193 | - site:`report:${module.id}`, grp:'Milestone', label:`report:${module.id}`, allowContinue: false, | 3099 | + await runStage(g => reportPrompt(module, c) + g, { |
| 3100 | + site:`report:${module.id}`, grp:'Milestone', label:`report:${module.id}`, allowContinue: false, dec: c.decisions, root: c.root, | ||
| 2194 | }) | 3101 | }) |
| 2195 | - await runMilestone(module) | ||
| 2196 | - results.push({ module: module.id, status:'done' }) | 3102 | + // lane 收口(附录补 15):lane 模式 milestone 前先在 lane 树上 wt-clean + 自主恢复——lane 里 tdd/fix |
| 3103 | + // 的未提交残留参与了 testGate(test.mjs 跑在工作树上)却不进 merge,默认分支会拿到"没测过的子集"。 | ||
| 3104 | + // in-scope 残留 commit 进模块分支;越界 → halt 留人工。串行/主根模式不加这一步:runMilestone 自身的 | ||
| 3105 | + // step 1 wt-clean 已覆盖同一棵树(主根),重复检查徒增一次子代理调用。 | ||
| 3106 | + if (c.lane != null) { | ||
| 3107 | + const branch = module.id === 'frontend-phase' ? 'frontend-phase' : `module-${module.id}` | ||
| 3108 | + const wt = await agentR(worktreeCleanPromptM(c), {label:`lane-wt:${module.id}`, phase:'Milestone', schema: WT_SCHEMA}) | ||
| 3109 | + if (!wt.clean) { | ||
| 3110 | + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `lane 收口(模块 ${module.id},milestone 前;lane 树 ${c.root})。`, c), | ||
| 3111 | + {label:`lane-wt-recover:${module.id}`, phase:'Milestone', schema: ACTION_RESULT_SCHEMA}) | ||
| 3112 | + if (!rec.success) throw new Error(`HALT lane-dirty-worktree ${module.id}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) | ||
| 3113 | + log(`lane 收口: ${module.id} 自动提交 lane 残留(${rec.detail || ''})`) | ||
| 3114 | + } | ||
| 3115 | + } | ||
| 3116 | + // 主根串行段互斥(Task 9 Step 1):milestone(merge/docs08/tag)与紧随的 RESUME 追加都作用于 | ||
| 3117 | + // 主根 + 默认分支——并行下逐段取锁串行化(补 13 非重入纪律:被包调用链内绝不再取锁,两段分别获取)。 | ||
| 3118 | + await withMainRootLock(() => runMilestone(module)) | ||
| 2197 | // P1#1 增量 flush:每个模块里程碑落定即追加 RESUME.md,使**硬中断**(进程被杀,到不了主循环末尾) | 3119 | // P1#1 增量 flush:每个模块里程碑落定即追加 RESUME.md,使**硬中断**(进程被杀,到不了主循环末尾) |
| 2198 | // 也能复盘已完成到哪、各模块做过哪些自主假设——不必等 loop-end 才写。best-effort,绝不阻断。 | 3120 | // 也能复盘已完成到哪、各模块做过哪些自主假设——不必等 loop-end 才写。best-effort,绝不阻断。 |
| 3121 | + // 决策改读 c.decisions(per-module 收集器,替代 decStart 水位线——并行交错下水位线语义失效)。 | ||
| 2199 | { | 3122 | { |
| 2200 | - const moduleDecisions = autonomousDecisions.slice(decStart) | ||
| 2201 | - const ok = await recordResume([ | 3123 | + const ok = await withMainRootLock(() => recordResume([ |
| 2202 | '## ✅ 模块完成 `' + module.id + '` → milestone/' + module.id + '(<ts>)', | 3124 | '## ✅ 模块完成 `' + module.id + '` → milestone/' + module.id + '(<ts>)', |
| 2203 | '', | 3125 | '', |
| 2204 | '- **本模块自主默认决策**(缺值时自动取的解读,可能含错误假设):', | 3126 | '- **本模块自主默认决策**(缺值时自动取的解读,可能含错误假设):', |
| 2205 | - decisionDigestMd(moduleDecisions, '(本模块无自主默认记录)'), | ||
| 2206 | - ].join('\n')) | ||
| 2207 | - if (ok) flushedCount = autonomousDecisions.length // 仅成功才推进水位线:flush 失败时 loop-end 兜底补记 | 3127 | + decisionDigestMd(c.decisions, '(本模块无自主默认记录)'), |
| 3128 | + ].join('\n'))) | ||
| 3129 | + if (ok) resumeFlushed.add(module.id) // 仅成功才记账:flush 失败时 loop-end 兜底补记该模块全部决策 | ||
| 2208 | } | 3130 | } |
| 3131 | + // lane 清理(Task 6 Step 4):milestone 成功后即移除 lane worktree——bestEffort,删失败只 log | ||
| 3132 | + // 绝不阻断(分支 / 对象库 / tag 全仓共享,留树不损正确性;残树由下个波次的前置占用感知收口,补 3)。 | ||
| 3133 | + if (c.lane != null) { | ||
| 3134 | + await bestEffortAction(removeLaneWorktreePromptM(c.root), | ||
| 3135 | + { label:`lane-remove:${module.id}`, phase:'Milestone', okMsg:`lane 清理: 已移除 ${c.root}`, failTag:`lane 清理(${c.root})` }) | ||
| 3136 | + } | ||
| 3137 | + return { module: module.id, status:'done', decisions: c.decisions } | ||
| 2209 | } catch (e) { | 3138 | } catch (e) { |
| 2210 | - const reason = String(e.message || e) | 3139 | + // halt:lane 整树**保留**供取证(绝不在此清理;resume 时由波次前置的占用感知统一恢复/移除,附录 |
| 3140 | + // 补 3)。reason 前缀 lane 路径——RESUME halt 条目与终端日志据此直接定位现场(Task 6 Step 4)。 | ||
| 3141 | + const reason = (c.lane != null ? `[lane 保留取证: ${c.root}] ` : '') + String(e.message || e) | ||
| 2211 | log(`⛔ HALT — 模块 ${module.id}:${reason}`) | 3142 | log(`⛔ HALT — 模块 ${module.id}:${reason}`) |
| 2212 | - results.push({ module: module.id, status:'halted', reason }) | ||
| 2213 | - haltedAtIdx = idx | ||
| 2214 | - break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start | 3143 | + return { module: module.id, status:'halted', reason, decisions: c.decisions } |
| 3144 | + } | ||
| 3145 | +} | ||
| 3146 | + | ||
| 3147 | +// ── 波次前置·占用感知(附录补 3a/3b)───────────────────────────────────────── | ||
| 3148 | +// 残留 lane(上次 halt 保留取证 / removeLaneWorktree bestEffort 删失败)持有 module-* 分支时, | ||
| 3149 | +// 主根 checkout 与新 worktree add 双向 fatal(git 实测)——本波要跑的模块若其分支被占用,必须先 | ||
| 3150 | +// 收口:树脏 → recoverDirtyWorktree(in-scope 残留 commit 进模块分支,工作不丢)→ 移除;恢复/移除 | ||
| 3151 | +// 失败(越界残留等)→ 该模块按 branchSetup-dirty-worktree 同类语义**结构化 halt**(等价前移,非 | ||
| 3152 | +// 新增 halt 类——串行路径走到 runBranchSetup 也会在同一种脏树上 halt),波次其余模块照常调度。 | ||
| 3153 | +// 每个波次(**含宽 1**)开跑前都要跑:宽 1 回退路径正是被残留 lane 卡死的高发场景。 | ||
| 3154 | +// 枚举自身失败 → fail-open 跳过感知(看不见就当没有,由 branchSetup 现有 fatal→仲裁→halt 兜底)。 | ||
| 3155 | +async function laneOccupancySweep(wave) { | ||
| 3156 | + const halted = [] | ||
| 3157 | + let lanes | ||
| 3158 | + try { | ||
| 3159 | + // 枚举直调 agent() 判空(不走 agentR):agentR 的 null 路径会先打 ⛔ HALT 再被下面 catch 吞掉 | ||
| 3160 | + // 继续跑,稀释「⛔=真 halt」约定——这里 null 与抛错同义,都安静 log 后 fail-open 跳过本波感知。 | ||
| 3161 | + const r = await agent(listLaneWorktreesPromptM(), { label: 'wave:lanes?', phase: 'Milestone', schema: LANE_LIST_SCHEMA }) | ||
| 3162 | + if (!r) { | ||
| 3163 | + log(`wave: 残留 lane 枚举无返回(${NULL_AGENT_PROBLEM}),本波跳过占用感知(由 branchSetup 现有护栏兜底)`) | ||
| 3164 | + return { wave, halted } | ||
| 3165 | + } | ||
| 3166 | + lanes = r.lanes || [] | ||
| 3167 | + } catch (e) { | ||
| 3168 | + log(`wave: 残留 lane 枚举失败(${String(e?.message || e)}),本波跳过占用感知(由 branchSetup 现有护栏兜底)`) | ||
| 3169 | + return { wave, halted } | ||
| 3170 | + } | ||
| 3171 | + if (!lanes.length) return { wave, halted } | ||
| 3172 | + const byBranch = new Map(lanes.filter(l => l && l.branch).map(l => [l.branch, l.path])) | ||
| 3173 | + const survivors = [] | ||
| 3174 | + for (const m of wave) { | ||
| 3175 | + const branch = m.id === 'frontend-phase' ? 'frontend-phase' : `module-${m.id}` | ||
| 3176 | + const lanePath = byBranch.get(branch) | ||
| 3177 | + if (!lanePath) { survivors.push(m); continue } | ||
| 3178 | + try { | ||
| 3179 | + const lc = makeCtx({ root: lanePath }) // 只为复用穿线 prompt 的取根;不是 lane 执行上下文 | ||
| 3180 | + const wt = await agentR(worktreeCleanPromptM(lc), { label: `wave:lane-wt:${m.id}`, phase: 'Milestone', schema: WT_SCHEMA }) | ||
| 3181 | + if (!wt.clean) { | ||
| 3182 | + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `残留 lane 占用感知(波次前置;lane 树 ${lanePath} 持有分支 ${branch})。`, lc), | ||
| 3183 | + { label: `wave:lane-recover:${m.id}`, phase: 'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3184 | + if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) | ||
| 3185 | + log(`wave: 残留 lane ${lanePath} in-scope 残留已 commit 进 ${branch}(${rec.detail || ''})`) | ||
| 3186 | + } | ||
| 3187 | + const rm = await agentR(removeLaneWorktreePromptM(lanePath), { label: `wave:lane-remove:${m.id}`, phase: 'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3188 | + if (!rm.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: 残留 lane 移除失败——${rm.error || ''}`) | ||
| 3189 | + log(`wave: 残留 lane ${lanePath} 已收口移除(释放分支 ${branch})`) | ||
| 3190 | + survivors.push(m) | ||
| 3191 | + } catch (e) { | ||
| 3192 | + const reason = `[lane 保留取证: ${lanePath}] ${String(e?.message || e)}` | ||
| 3193 | + log(`⛔ HALT — 模块 ${m.id}:${reason}`) | ||
| 3194 | + halted.push({ module: m.id, status: 'halted', reason, decisions: [] }) | ||
| 3195 | + } | ||
| 3196 | + } | ||
| 3197 | + return { wave: survivors, halted } | ||
| 3198 | +} | ||
| 3199 | + | ||
| 3200 | +// ── 波次前置·并行准备(Task 9 Step 3;计划 D2 + 附录补 3c / 补 4 / 补 6b / 补 7)── | ||
| 3201 | +// 仅 ≥2 宽波次调用(主根,串行)。成功返回 { defBranch, maxV, baseSchema };任何一步失败 → 先清 | ||
| 3202 | +// 掉本次已建的 lane(其持有的分支会毒化降级后的宽 1 legacy checkout)再返回 null,调用方把本波次 | ||
| 3203 | +// 降级 maxWidth=1(fail-open to serial,**绝不 halt**——隔离失败只损失并行收益,不该成为新停机点)。 | ||
| 3204 | +// laneEnvCache(声明在 featureBatchRun 旁,与其 lane 前置共享,语义见声明处注释):首个探测点 | ||
| 3205 | +// 填充后跨波次/跨模块复用。maxV 绝不入缓存——它随波次合并增长,必须每波重读。 | ||
| 3206 | +async function prepareParallelWave(wave) { | ||
| 3207 | + const created = [] | ||
| 3208 | + const degrade = async (why) => { | ||
| 3209 | + log(`wave: ${why} → 本波次降级 maxWidth=1(串行主根路径),不 halt`) | ||
| 3210 | + for (const path of created) { | ||
| 3211 | + await bestEffortAction(removeLaneWorktreePromptM(path), | ||
| 3212 | + { label: 'wave:lane-rollback', phase: 'Milestone', okMsg: `wave: 降级回收 lane ${path}`, failTag: `wave: 降级回收 lane(${path})` }) | ||
| 3213 | + } | ||
| 3214 | + return null | ||
| 2215 | } | 3215 | } |
| 3216 | + try { | ||
| 3217 | + const def = await agentR(detectDefaultBranchPromptM(), { label: 'wave:default', phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA }) | ||
| 3218 | + const mc = makeCtx() | ||
| 3219 | + // (1) 主根复位(D2 + 补 3c):module-* 分支必须未被主根持有(worktree add 才不 fatal),且兄弟 | ||
| 3220 | + // milestone 的 merge 要落在默认分支主根上。脏树恢复的目标分支 = **当前 HEAD 分支**(传默认分支 | ||
| 3221 | + // 会被 recoverDirtyWorktree 的分支护栏按 dirty-on-wrong-branch 拒绝,造成本可避免的降级); | ||
| 3222 | + // HEAD 已在默认分支且脏 → 不在此处理(绝不为并行机制新增"对默认分支自动 commit"),降级交给 | ||
| 3223 | + // legacy 路径现有恢复/halt 语义。 | ||
| 3224 | + const head = await agentR(currentBranchPromptM(mc), { label: 'wave:head', phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA }) | ||
| 3225 | + const wt = await agentR(worktreeCleanPromptM(mc), { label: 'wave:wt', phase: 'Milestone', schema: WT_SCHEMA }) | ||
| 3226 | + if (!wt.clean) { | ||
| 3227 | + if (head.branch === def.branch) return degrade(`主根在默认分支 ${def.branch} 上有未提交改动(绝不自动 commit 默认分支)`) | ||
| 3228 | + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, head.branch, `并行波次前置主根复位(HEAD=${head.branch},就地收口后回默认分支)。`, mc), | ||
| 3229 | + { label: 'wave:wt-recover', phase: 'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3230 | + if (!rec.success) return degrade(`主根脏树恢复失败(${rec.error || ''})`) | ||
| 3231 | + log(`wave: 主根 in-scope 残留已 commit 进 ${head.branch}(${rec.detail || ''})`) | ||
| 3232 | + } | ||
| 3233 | + if (head.branch !== def.branch) { | ||
| 3234 | + const co = await agentR(checkoutExistingBranchPromptM(def.branch, mc), { label: 'wave:checkout-default', phase: 'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3235 | + if (!co.success) return degrade(`主根切回默认分支失败(${co.error || ''})`) | ||
| 3236 | + } | ||
| 3237 | + // (2) prune:只清"目录已删"的管理残留(完好取证 lane 由占用感知收口,勿依赖 prune)。 | ||
| 3238 | + const pr = await agentR(pruneWorktreesPromptM(), { label: 'wave:prune', phase: 'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3239 | + if (!pr.success) return degrade(`git worktree prune 失败(${pr.error || ''})`) | ||
| 3240 | + // (3) 全仓最大 migration 版本(全局并集口径,补 4)→ 执行段据此给每条 lane 发 vBase 版本段。 | ||
| 3241 | + const mv = await agentR(maxMigrationVersionPromptM(), { label: 'wave:maxV', phase: 'Milestone', schema: FIELD_VALUE_SCHEMA }) | ||
| 3242 | + const maxV = mv.found ? parseInt(mv.value, 10) : NaN | ||
| 3243 | + if (!Number.isInteger(maxV) || maxV < 0) return degrade(`migration 版本并集读取失败(found=${mv.found}, value=${JSON.stringify(mv.value)})`) | ||
| 3244 | + // (4)(5) lane 测试库 schema 基底(config-vars.yaml database.schema)+ 支持度两点联查(补 6b) | ||
| 3245 | + // ——经 laneEnvCache 缓存(运行内不变量,见函数头注释)。agentR 抛错(runtime 故障)不缓存: | ||
| 3246 | + // 那是瞬时态不是环境事实,留给下个 ≥2 宽波次重探。 | ||
| 3247 | + if (!laneEnvCache) { | ||
| 3248 | + const sb = await agentR(readDbSchemaPromptM(), { label: 'wave:schema', phase: 'Milestone', schema: FIELD_VALUE_SCHEMA }) | ||
| 3249 | + if (!sb.found || !sb.value) { | ||
| 3250 | + laneEnvCache = { fail: 'config-vars.yaml 的 database.schema 读不到(lane 库名无基底)' } | ||
| 3251 | + } else { | ||
| 3252 | + // 任一不满足 → 降级(与其并行却在共享库上互相清库,不如明示串行)。 | ||
| 3253 | + const probe = await agentR(laneDbSupportPromptM(), { label: 'wave:lane-db?', phase: 'Milestone', schema: LANE_DB_PROBE_SCHEMA }) | ||
| 3254 | + laneEnvCache = (!probe.scriptsEnv || !probe.ymlPlaceholder) | ||
| 3255 | + ? { fail: `目标项目不支持 lane 测试库(scriptsEnv=${probe.scriptsEnv}, ymlPlaceholder=${probe.ymlPlaceholder};${probe.detail || ''}——建议重跑 skeleton 升级 scripts/ 与 application*.yml 占位)` } | ||
| 3256 | + : { baseSchema: sb.value } | ||
| 3257 | + } | ||
| 3258 | + } | ||
| 3259 | + if (laneEnvCache.fail) return degrade(laneEnvCache.fail) | ||
| 3260 | + const baseSchema = laneEnvCache.baseSchema | ||
| 3261 | + // (6) 建 lane worktrees + 写 fail-closed marker(补 7)。lane 序号 = 本波 wave 下标,与执行段 | ||
| 3262 | + // ctxs 的 laneDbSchema 取号严格同序(marker 内容必须 == c.dbSchema)。 | ||
| 3263 | + for (const [i, m] of wave.entries()) { | ||
| 3264 | + const branch = m.id === 'frontend-phase' ? 'frontend-phase' : `module-${m.id}` | ||
| 3265 | + const path = laneRoot(m.id) | ||
| 3266 | + const add = await agentR(createLaneWorktreePromptM(branch, path, def.branch), { label: `wave:lane-add:${m.id}`, phase: 'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3267 | + if (!add.success) return degrade(`建 lane worktree 失败(${m.id}:${add.error || ''})`) | ||
| 3268 | + created.push(path) | ||
| 3269 | + const mk = await agentR(writeLaneDbMarkerPromptM(path, laneDbSchema(baseSchema, i)), { label: `wave:lane-marker:${m.id}`, phase: 'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3270 | + if (!mk.success) return degrade(`写 lane DB marker 失败(${m.id}:${mk.error || ''})`) | ||
| 3271 | + } | ||
| 3272 | + log(`wave: 并行准备就绪——宽 ${wave.length}(${wave.map(m => m.id).join('、')}),maxV=${maxV},lane 库基底 ${baseSchema}`) | ||
| 3273 | + return { defBranch: def.branch, maxV, baseSchema } | ||
| 3274 | + } catch (e) { | ||
| 3275 | + return degrade(`波次前置串行段异常(${String(e?.message || e)})`) | ||
| 3276 | + } | ||
| 3277 | +} | ||
| 3278 | + | ||
| 3279 | +// ── 主循环:依赖波次执行(Phase D Task 9 Step 2;伪代码修正见附录补 14)──────── | ||
| 3280 | +// useParallel 总开关(计划 D4 / 附录补 1):ARGS.parallel 缺省或 modules !== true → 全串行—— | ||
| 3281 | +// 不调 scheduler(免 Schedule 阶段噪声)、不建 lane;serialChainDeps 链式图上 nextWave 每轮恰出 | ||
| 3282 | +// 1 个,主根 legacy 路径与 Phase A 重构后行为一致。逃生口:coding-start 传 parallel:{modules:false}。 | ||
| 3283 | +const useParallel = ARGS?.parallel?.modules === true | ||
| 3284 | +const results = [] | ||
| 3285 | +// doneSet:nextWave 的依赖消解集——必须以 routed 中 done:true 的模块初始化(runScheduler 标注的 | ||
| 3286 | +// deps 允许指向已完成模块 id,frontend-phase 的"依赖全部后端"硬规则尤其依赖此初值)。 | ||
| 3287 | +const doneSet = new Set(routed.modules.filter(m => m.done).map(m => m.id)) | ||
| 3288 | +let remaining = useParallel ? await runScheduler(todo, routed.modules) : serialChainDeps(todo) | ||
| 3289 | + | ||
| 3290 | +while (remaining.length) { | ||
| 3291 | + let wave = nextWave(remaining, doneSet) | ||
| 3292 | + if (!useParallel) wave = wave.slice(0, 1) // 防御:串行闸下恒单宽(链式图本就每轮恰出 1 个) | ||
| 3293 | + // 本轮消耗集(补 14):按波次成员记账、**绝不**以 rs 反查——runtime-null 槽位没有 r.module,反查 | ||
| 3294 | + // 会把故障模块留在 remaining,下轮 deps 不满足触发"图卡死降级全序"误重跑。降级被裁掉的成员**不** | ||
| 3295 | + // 入此集(本轮未跑,留在 remaining 等下波)。 | ||
| 3296 | + const consumed = new Set() | ||
| 3297 | + | ||
| 3298 | + // 占用感知(每波,含宽 1):恢复失败的模块已结构化 halt(其余照常调度),波次边界收敛会停在本波末。 | ||
| 3299 | + const swept = await laneOccupancySweep(wave) | ||
| 3300 | + wave = swept.wave | ||
| 3301 | + for (const h of swept.halted) { results.push(h); consumed.add(h.module) } | ||
| 3302 | + | ||
| 3303 | + // ≥2 宽波次的并行准备:任一失败 → 本波次降级 maxWidth=1(被裁掉的成员留在 remaining 等下波)。 | ||
| 3304 | + let prep = null | ||
| 3305 | + if (useParallel && wave.length >= 2) { | ||
| 3306 | + prep = await prepareParallelWave(wave) | ||
| 3307 | + if (!prep) wave = wave.slice(0, 1) | ||
| 3308 | + } | ||
| 3309 | + | ||
| 3310 | + if (wave.length) { | ||
| 3311 | + for (const m of wave) consumed.add(m.id) | ||
| 3312 | + if (wave.length === 1 || !useParallel) { | ||
| 3313 | + // 单宽波次(含 frontend-phase 终波)/ 并行关闭 → 主根 legacy 路径,不建 lane——makeCtx() 缺省 | ||
| 3314 | + // 即串行语义,与 Phase A 重构后行为一致。 | ||
| 3315 | + const r = await runModule(wave[0], makeCtx()) | ||
| 3316 | + results.push(r) | ||
| 3317 | + if (r.status === 'done') doneSet.add(r.module) | ||
| 3318 | + } else { | ||
| 3319 | + // lane ctx 预先构造(thunk 外):runtime-null 槽位仍能从 ctxs[i] 抢救出已登记的 decisions | ||
| 3320 | + // 与 lane 路径(补 12 的 loop-end 增量口径需要前者)。 | ||
| 3321 | + const ctxs = wave.map((m, i) => makeCtx({ | ||
| 3322 | + lane: i, root: laneRoot(m.id), | ||
| 3323 | + dbSchema: laneDbSchema(prep.baseSchema, i), vBase: vBase(prep.maxV, i), | ||
| 3324 | + })) | ||
| 3325 | + // parallel():结果数组与 thunk 数组按位次对齐(runtime 保证);thunk 异常被静默折成 null、 | ||
| 3326 | + // 调用本身永不 reject——runModule 永不 throw,null 只剩 runtime 级故障(补 14)。 | ||
| 3327 | + const rs = await parallel(wave.map((m, i) => () => runModule(m, ctxs[i]))) | ||
| 3328 | + for (const [i, r] of rs.entries()) { | ||
| 3329 | + if (r) { | ||
| 3330 | + results.push(r) | ||
| 3331 | + if (r.status === 'done') doneSet.add(r.module) | ||
| 3332 | + continue | ||
| 3333 | + } | ||
| 3334 | + // runtime-null 槽位 → 记 halted;lane 整树保留取证(与 runModule catch 同口径的 reason 前缀)。 | ||
| 3335 | + const reason = `[lane 保留取证: ${ctxs[i].root}] runtime-null` | ||
| 3336 | + log(`⛔ HALT — 模块 ${wave[i].id}:${reason}(parallel() 槽位为 null,runtime 级故障)`) | ||
| 3337 | + results.push({ module: wave[i].id, status: 'halted', reason, decisions: ctxs[i].decisions }) | ||
| 3338 | + } | ||
| 3339 | + } | ||
| 3340 | + } | ||
| 3341 | + | ||
| 3342 | + remaining = remaining.filter(m => !consumed.has(m.id)) | ||
| 3343 | + // halt 波次边界收敛:无取消原语,兄弟模块跑完本波(接受算力浪费)后在波次边界停下——比串行 | ||
| 3344 | + // "单模块即断"粗一档;RESUME 记账把同波次全部 halt 原因逐条列出(见 loop-end)。 | ||
| 3345 | + if (results.some(r => r.status === 'halted')) break | ||
| 2216 | } | 3346 | } |
| 2217 | 3347 | ||
| 2218 | -// pending:halt 后被跳过的剩余模块(M5)。caller / coding-start 可据此告知用户"修好后还有哪些待跑", | ||
| 2219 | -// 而不是仅看到一个 halted 模块就误以为只剩一个。 | ||
| 2220 | -const pending = haltedAtIdx >= 0 | ||
| 2221 | - ? todo.slice(haltedAtIdx + 1).map(m => ({ module: m.id, status: 'pending' })) | ||
| 2222 | - : [] | 3348 | +// pending:图余量(Task 10)——halt 收敛后 remaining 中每项标注 blockedBy(尚未满足的依赖), |
| 3349 | +// caller / coding-start 据此告知"修好后还有哪些待跑、各自被谁挡着",而非线性 slice 误导 | ||
| 3350 | +// (波次图下 halt 模块之后的 docs/02 顺位可能与 halt 毫无依赖关系)。 | ||
| 3351 | +const pending = remaining.map(m => ({ | ||
| 3352 | + module: m.id, status: 'pending', | ||
| 3353 | + blockedBy: (m.deps || []).filter(d => !doneSet.has(d)), | ||
| 3354 | +})) | ||
| 2223 | 3355 | ||
| 2224 | // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 | 3356 | // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 |
| 2225 | // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 | 3357 | // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 |
| @@ -2228,35 +3360,55 @@ const pending = haltedAtIdx >= 0 | @@ -2228,35 +3360,55 @@ const pending = haltedAtIdx >= 0 | ||
| 2228 | // 续跑 handoff(best-effort,静默):把本次运行结果落进 docs/superpowers/RESUME.md, | 3360 | // 续跑 handoff(best-effort,静默):把本次运行结果落进 docs/superpowers/RESUME.md, |
| 2229 | // 使下次 coding-start 重跑能复盘「上次为何 halt / 做过哪些自主假设 / 还剩哪些模块」。 | 3361 | // 使下次 coding-start 重跑能复盘「上次为何 halt / 做过哪些自主假设 / 还剩哪些模块」。 |
| 2230 | // 硬中断(进程被杀,到不了这里)时不写——那种情况无 halt 原因可记,且 tag+工件已够 Router 续跑。 | 3362 | // 硬中断(进程被杀,到不了这里)时不写——那种情况无 halt 原因可记,且 tag+工件已够 Router 续跑。 |
| 2231 | -const halted = results.find(r => r.status === 'halted') | ||
| 2232 | -const decDigest = decisionDigestMd(autonomousDecisions.slice(flushedCount), '(无未记录的自主默认;已完成模块的决策见上方各模块条目)') | ||
| 2233 | -if (halted) { | ||
| 2234 | - const pend = pending.length ? pending.map(p => `\`${p.module}\``).join('、') : '无' | ||
| 2235 | - await recordResume([ | ||
| 2236 | - '## ⛔ HALT — 模块 `' + halted.module + '`(<ts>)', | ||
| 2237 | - '', | ||
| 2238 | - `- **halt 原因**:${halted.reason || '(空)'}`, | 3363 | +// halts:同波次可能多个模块同时 halt(波次边界收敛跑完本波才停),一律逐条记账,绝不只取第一个。 |
| 3364 | +const halts = results.filter(r => r.status === 'halted') | ||
| 3365 | +// loop-end 汇总口径(附录补 12):= 「flush 失败/未 flush 模块(含 halted)的 r.decisions ∪ 全局 sink 残量」。 | ||
| 3366 | +// flush 成功的模块已被逐模块 RESUME 条目覆盖,不再重复列出——同一条决策绝不既丢失又重复。 | ||
| 3367 | +// 全局残量 = autonomousDecisions:模块作用域决策都进 c.decisions,这里只剩"漏传 dec"的兜底记录。 | ||
| 3368 | +const unflushedDecisions = [ | ||
| 3369 | + ...results.filter(r => !resumeFlushed.has(r.module)).flatMap(r => r.decisions || []), | ||
| 3370 | + ...autonomousDecisions, | ||
| 3371 | +] | ||
| 3372 | +const decDigest = decisionDigestMd(unflushedDecisions, '(无未记录的自主默认;已完成模块的决策见上方各模块条目)') | ||
| 3373 | +// loop-end 的 recordResume 统一过 withMainRootLock(补 13):波次循环已结束本无竞态,但保持 | ||
| 3374 | +// "RESUME 追加必持锁"的单一纪律避免遗忘(recordResume 自身及调用链内绝不再取锁)。 | ||
| 3375 | +if (halts.length) { | ||
| 3376 | + // RESUME halt 条目(Task 10):逐条列出同波次全部 halt 原因——lane 模式 reason 自带 | ||
| 3377 | + // 「[lane 保留取证: <path>]」前缀(即保留的 lane 路径);待跑模块逐个标注 blockedBy。 | ||
| 3378 | + const haltList = halts.map(h => ` - \`${h.module}\`:${h.reason || '(空)'}`).join('\n') | ||
| 3379 | + const pendList = pending.length | ||
| 3380 | + ? pending.map(p => ` - \`${p.module}\`${(p.blockedBy || []).length | ||
| 3381 | + ? `(blockedBy: ${p.blockedBy.map(b => `\`${b}\``).join('、')})` | ||
| 3382 | + : '(无未满足依赖,修复阻塞点后即可跑)'}`).join('\n') | ||
| 3383 | + : ' - 无' | ||
| 3384 | + await withMainRootLock(() => recordResume([ | ||
| 3385 | + '## ⛔ HALT — 模块 ' + halts.map(h => '`' + h.module + '`').join('、') + '(<ts>)', | ||
| 3386 | + '', | ||
| 3387 | + '- **halt 模块与原因**(同波次全部列出;lane 模式 reason 自带保留 lane 路径前缀,取证后人工移除):', | ||
| 3388 | + haltList, | ||
| 2239 | '- **本次自主默认决策**(仅未被逐模块条目覆盖的增量;已完成模块的决策见上方各模块条目,重跑前请复核):', | 3389 | '- **本次自主默认决策**(仅未被逐模块条目覆盖的增量;已完成模块的决策见上方各模块条目,重跑前请复核):', |
| 2240 | decDigest, | 3390 | decDigest, |
| 2241 | - `- **halt 后未跑的待办模块**:${pend}`, | 3391 | + '- **halt 后未跑的待办模块**(blockedBy = 尚未满足的依赖,重跑时由调度图自动消解):', |
| 3392 | + pendList, | ||
| 2242 | '- **下一步**:人工修复阻塞点后重跑 `/erp-workflow:coding-start`;Router 按 git tag 续跑,已完成模块自动跳过。', | 3393 | '- **下一步**:人工修复阻塞点后重跑 `/erp-workflow:coding-start`;Router 按 git tag 续跑,已完成模块自动跳过。', |
| 2243 | - ].join('\n')) | 3394 | + ].join('\n'))) |
| 2244 | } else if (results.length) { | 3395 | } else if (results.length) { |
| 2245 | const doneList = results.filter(r => r.status === 'done').map(r => `\`${r.module}\``).join('、') || '无' | 3396 | const doneList = results.filter(r => r.status === 'done').map(r => `\`${r.module}\``).join('、') || '无' |
| 2246 | - await recordResume([ | 3397 | + await withMainRootLock(() => recordResume([ |
| 2247 | '## ✅ 全部完成(<ts>)', | 3398 | '## ✅ 全部完成(<ts>)', |
| 2248 | '', | 3399 | '', |
| 2249 | `- **本次完成模块**:${doneList}`, | 3400 | `- **本次完成模块**:${doneList}`, |
| 2250 | '- **本次自主默认决策**(仅未被逐模块条目覆盖的增量):', | 3401 | '- **本次自主默认决策**(仅未被逐模块条目覆盖的增量):', |
| 2251 | decDigest, | 3402 | decDigest, |
| 2252 | - ].join('\n')) | 3403 | + ].join('\n'))) |
| 2253 | } | 3404 | } |
| 2254 | 3405 | ||
| 2255 | -if (halted) log(`⛔ 本次运行 halt — 模块 ${halted.module};原因:${halted.reason || '(空)'};待跑:${pending.map(p => p.module).join('、') || '无'}`) | 3406 | +if (halts.length) log(`⛔ 本次运行 halt — 模块 ${halts.map(h => h.module).join('、')}(原因逐条见上方 ⛔ 日志与 RESUME.md);待跑:${pending.map(p => p.module).join('、') || '无'}`) |
| 2256 | else if (results.length) log(`✅ 本次运行完成 ${results.length} 个模块,无 halt`) | 3407 | else if (results.length) log(`✅ 本次运行完成 ${results.length} 个模块,无 halt`) |
| 2257 | 3408 | ||
| 2258 | // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, | 3409 | // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, |
| 2259 | // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 | 3410 | // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 |
| 2260 | // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, | 3411 | // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, |
| 2261 | -// 否则 Workflow 拿不到 results / pending。 | ||
| 2262 | -return { results, pending, decisions: autonomousDecisions } | 3412 | +// 否则 Workflow 拿不到 results / pending。语法检查用 `node lib/check-workflow-syntax.mjs`(附录补 18)。 |
| 3413 | +// decisions 口径(附录补 12):results 各 r.decisions 拼接 ∪ 全局 sink 残量(漏传 dec 的兜底)。 | ||
| 3414 | +return { results, pending, decisions: [...results.flatMap(r => r.decisions || []), ...autonomousDecisions] } |