Commit 163161eca7c486ff1c6cf8b93b63402669586d6e
1 parent
2337cc32
perf(coding): ETA 播报 + 微步骤合并 + report 瘦身 + 行为门跨轮复用
实测口径(同一 ERP 项目 13 次运行 / 1012 个子代理会话 / 2988 分钟,按子代理
transcript 首末 timestamp 统计):test-gate 13.3%、module-report 11.7%(均值
14.6 分钟)、behavior 11.6%、tdd 合计 22.4%;微步骤 574 个会话只占 6.6% 时间
却占 57% 会话数。四项改动照此下刀。
1) ETA 播报(新增 STAGE_MIN / etaLog / itemMin / moduleMin)
左侧 Phases 面板的分母是 harness 侧「已派生子代理数」,脚本无 API 预声明
总量,串行链上只能 0/1→1/2 地长;运行时又禁 Date.now()(会破坏 resume
重放),拿不到真实计时。故用 log() narrator 行按「剩余段数 × 实测分布」
静态外推:STAGE_MIN 存各 stage 的 [p25, 中位数, p75],etaLog 挂在
runStage/runAction 的重试循环里,开跑与每次重跑各播一行「预计 / 再需
X–Y 分钟」;gate / behavior 走 agentR,另在 attempt 级显式挂。表外的
微步骤(0.2 分钟级)静默,不刷屏。纯显示,绝不参与控制流。
2) 微步骤合并(会话数)
- docs/08 勾选 2 会话 → 1:原「读勾选态」与「写勾选+commit」之间没有任何
JS 决策,纯白烧一次往返。合并为 flipDocs08CheckboxPromptM,三种非错误
终态经 ACTION_RESULT 的 detail 回传(not-found / already-checked /
flipped),调用方分支日志逐条对应。删 CHECKBOX_STATE_SCHEMA 与两个旧
prompt。每个 REQ/FE 省 1 个会话。
- 分支就位 3 会话 → 1:exists? → checkout|create → head 本是一条确定性
git 链。合并为 ensureBranchCheckedOutPromptM + BRANCH_READY_SCHEMA
(branch 必填 = 实测 HEAD,明写「据实回填、绝不改写成期望值」)。JS 侧
HEAD 不符仲裁重切循环原样保留,合并只是它的第一次尝试。lane 模式不走
这条(worktree add 已上分支)。每个模块省 2 个会话。
- runAction 加可选 schema(缺省仍 ACTION_RESULT_SCHEMA),只要含 success
布尔,失败/仲裁/重试语义一字不变。
3) module-report 瘦身
原实现让报告子代理 Glob + 按 attempt 升序通读**全部** test-gate 证据(前端
还要加全部 behavior-r*-a*.md)去自证「最后一份绿」——可红态根本走不到
report(testGate / runBehaviorGate 会先 halt),JS 才是这个事实的第一手
持有者。改为 testGate 返回 {...g, attempts}、runBehaviorGate 绿态返回
{rounds, attempts, routes/controls/authState},经 runModule 的 reportFacts
透传给 reportPrompt(module, c, facts):前置改成「门禁事实由编排层给定」,
子代理只读点名的那一两份(末次 green;attempts>1 才加首次 red),flake
标注也由 JS 判定后写死进 prompt。§ ⑤ / § ⑧ 的通配符通读同样收敛。
4) 行为门跨 round 复用(carry)
实测本门 40+ 分钟里约 18 分钟花在「读 spec → 查表 → 反查组件 → 推导期望
→ 写 runner」,而 fix 只改 frontend/ 源码——期望(来自 prototype/REQ/
docs05)与 runner(DOM/locator 驱动)在同一前端阶段内不失效。新增
.tmp/behavior-gate/frontend-phase/carry/ 存 expectations.md + run.mjs +
sentinel.sql 跨 round/attempt 存活:r1a1 全量推导后必须落 carry;r>1 或
attempt>1 时 step1 / step4 整节跳过、sentinel 直接灌 carry 那份,只在有
明确证据(runner 自身 bug / 路由表真变了)时增量修补并同步回 carry。
不复用的部分写成硬约束:运行栈每次冷起、库副本每次重建、种子每次重灌、
证据每轮独立文件——复用的只是「推导结果与脚本」。
验证:lib/check-workflow-syntax.mjs 语法门 + 悬空引用 grep 自检 + 把
behaviorGatePrompt / reportPrompt / 两个合并版微步骤用 stub 渲染出实际
prompt 眼验(含 reuse、flake、后端/前端各分支变体)。coding.mjs 无行为测试,
真实收益仍需目标项目实跑确认。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing
1 changed file
with
223 additions
and
98 deletions
workflows/coding.mjs
| @@ -208,12 +208,17 @@ const FIELD_VALUE_SCHEMA = { type:'object', additionalProperties:false, | @@ -208,12 +208,17 @@ const FIELD_VALUE_SCHEMA = { type:'object', additionalProperties:false, | ||
| 208 | value:{type:'string'}, | 208 | value:{type:'string'}, |
| 209 | lineNumber:{type:'integer'} } } | 209 | lineNumber:{type:'integer'} } } |
| 210 | 210 | ||
| 211 | -// CHECKBOX_STATE_SCHEMA:docs/08 功能行勾选态;state 必填——只 require found 时 cb.state 缺失会静默走 checked 分支。 | ||
| 212 | -const CHECKBOX_STATE_SCHEMA = { type:'object', additionalProperties:false, | ||
| 213 | - required:['found','state'], properties:{ | ||
| 214 | - found:{type:'boolean'}, | ||
| 215 | - state:{type:'string', enum:['checked','unchecked']}, | ||
| 216 | - lineNumber:{type:'integer'} } } | 211 | +// (原 CHECKBOX_STATE_SCHEMA 已随 docs/08 勾选的 read-then-write 两段合并为单会话而删除, |
| 212 | +// 见 flipDocs08CheckboxPromptM——终态经 ACTION_RESULT 的 detail 回传。) | ||
| 213 | + | ||
| 214 | +// BRANCH_READY_SCHEMA:ensureBranchCheckedOutPromptM(exists?+checkout|create+head 三合一)的回执。 | ||
| 215 | +// branch 必填——它是**实测 HEAD**,上层的分支不符仲裁循环靠它判定,缺失会让不符静默通过。 | ||
| 216 | +const BRANCH_READY_SCHEMA = { type:'object', additionalProperties:false, | ||
| 217 | + required:['success','branch'], properties:{ | ||
| 218 | + success:{type:'boolean'}, | ||
| 219 | + branch:{type:'string'}, | ||
| 220 | + created:{type:'boolean'}, | ||
| 221 | + error:{type:'string'} } } | ||
| 217 | 222 | ||
| 218 | const ALREADY_MERGED_SCHEMA = { type:'object', additionalProperties:false, | 223 | const ALREADY_MERGED_SCHEMA = { type:'object', additionalProperties:false, |
| 219 | required:['alreadyMerged'], properties:{ alreadyMerged:{type:'boolean'} } } | 224 | required:['alreadyMerged'], properties:{ alreadyMerged:{type:'boolean'} } } |
| @@ -978,6 +983,12 @@ function behaviorGateContract(c) { | @@ -978,6 +983,12 @@ function behaviorGateContract(c) { | ||
| 978 | function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | 983 | function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { |
| 979 | const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(调用方未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' | 984 | const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(调用方未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' |
| 980 | const tmpDir = `${c.root}/.tmp/behavior-gate/frontend-phase/r${behaviorRound}` | 985 | const tmpDir = `${c.root}/.tmp/behavior-gate/frontend-phase/r${behaviorRound}` |
| 986 | + // carry:跨 behaviorRound / attempt **存活**的派生物暂存区(run.mjs / sentinel.sql / expectations.md)。 | ||
| 987 | + // 实测:本门 40+ 分钟里约 18 分钟花在"读 spec → 查表 → 反查组件 → 推导期望 → 写 runner",而 fix 只 | ||
| 988 | + // 改 frontend/ 源码——期望(来自 prototype/REQ/docs05)与 runner(DOM/locator 驱动)在同一前端阶段内 | ||
| 989 | + // 不随 fix 失效。故只重跑不重推导。**运行栈仍绝不复用**(每次冷起,契约未变)。 | ||
| 990 | + const carryDir = `${c.root}/.tmp/behavior-gate/frontend-phase/carry` | ||
| 991 | + const reuse = behaviorRound > 1 || attempt > 1 | ||
| 981 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md` | 992 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md` |
| 982 | return [ | 993 | return [ |
| 983 | `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`, | 994 | `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`, |
| @@ -994,8 +1005,18 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | @@ -994,8 +1005,18 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | ||
| 994 | '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', | 1005 | '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', |
| 995 | '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', | 1006 | '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', |
| 996 | `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 | 1007 | `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 |
| 997 | - ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子),再新建本轮子目录 \`${tmpDir}/\`。` | ||
| 998 | - : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, | 1008 | + ? `本次是本阶段首轮首次 → 先删除整个 \`${c.root}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子/carry),再新建本轮子目录 \`${tmpDir}/\` 与派生物暂存区 \`${carryDir}/\`。` |
| 1009 | + : `本次 behaviorRound=${behaviorRound} / attempt=${attempt} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建;**\`${carryDir}/\` 绝不删**——它装着上一次已推导好的 runner/种子/期望,本次要复用(见下方「派生物复用」)。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, | ||
| 1010 | + // 派生物复用(省时改造):只重跑不重推导。首次跑完必须落 carry,后续轮次必须先读 carry。 | ||
| 1011 | + reuse | ||
| 1012 | + ? [ | ||
| 1013 | + `- **派生物复用(本次必须先做,${behaviorRound > 1 ? `上一轮 fix 只改了 \`frontend/\` 源码` : `上一次是同轮 attempt 重试`})**:先 Read \`${carryDir}/expectations.md\`(上次已推导的:全部 FE 作用域并集 / 逐路由预期控件与文字来源 / 每路由所需角色 / sentinel 值表 / 组件文件反查表)与 \`${carryDir}/run.mjs\`、\`${carryDir}/sentinel.sql\`。`, | ||
| 1014 | + ` - **三者齐备 → 直接复用**:把 \`${carryDir}/run.mjs\` / \`sentinel.sql\` 复制进本轮 \`${tmpDir}/\` 后使用,**跳过 step1 / step4 的重新推导**(期望以 \`expectations.md\` 为准),直接进 step2 起栈。这是本次的默认路径。`, | ||
| 1015 | + ` - **只在有明确证据时才改**:上次证据显示 runner 自身有 bug(选择器过时 / 断言写错 / 漏驱动),或 \`frontend/\` 的路由表在上一轮 fix 中真的变了 → 就地**增量修补**复制过来的 runner,并把修补同步回 \`${carryDir}/run.mjs\`;**绝不**因为"想重写得更好"而从头重推导。`, | ||
| 1016 | + ` - 缺文件 / 内容不可用 → 按首次流程完整推导,跑完照样落回 \`${carryDir}/\`(缺失是异常,在证据报告里注明)。`, | ||
| 1017 | + ` - **不复用的部分(硬约束)**:运行栈(每次冷起)、数据库副本(每次重建)、演示/sentinel 数据的**注入动作**(每次重灌)、证据报告(每次独立文件)。复用的只是"推导结果与脚本"。`, | ||
| 1018 | + ].join('\n') | ||
| 1019 | + : `- **派生物落盘(本次是首次推导,必须做)**:step1/step4 推导完成后,把 ① 推导结果写成 \`${carryDir}/expectations.md\`(全部 FE 作用域并集 / 逐路由预期控件与文字来源 / 每路由所需角色 / sentinel 值表 / route→组件文件反查表)、② runner 写一份到 \`${carryDir}/run.mjs\`、③ sentinel 种子写一份到 \`${carryDir}/sentinel.sql\`。后续 behaviorRound / attempt 会直接复用这三份**跳过重新推导**(fix 只改 \`frontend/\` 源码,不使这些失效),省掉每轮十几分钟的重复侦查。carry 与本轮 \`${tmpDir}/\` 同属临时区、跑完即弃,**不提交**。`, | ||
| 999 | `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, | 1020 | `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, |
| 1000 | // runner 的 status 必须是**算出来的**。曾实际发生:生成的 run.mjs 把 status 初始化为 'red' | 1021 | // runner 的 status 必须是**算出来的**。曾实际发生:生成的 run.mjs 把 status 初始化为 'red' |
| 1001 | // 后再未赋值,恒红且不带任何分类信号——若子代理照单全收,上层 runBehaviorGate 的 | 1022 | // 后再未赋值,恒红且不带任何分类信号——若子代理照单全收,上层 runBehaviorGate 的 |
| @@ -1016,7 +1037,8 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | @@ -1016,7 +1037,8 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | ||
| 1016 | ' - 根因不可归到 `frontend/` 源码(依赖 / 工具链 / 无法定位)→ `envError.kind="build-failed"`(如能定位仍填 `rootCausePath`)。', | 1037 | ' - 根因不可归到 `frontend/` 源码(依赖 / 工具链 / 无法定位)→ `envError.kind="build-failed"`(如能定位仍填 `rootCausePath`)。', |
| 1017 | ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。', | 1038 | ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。', |
| 1018 | '', | 1039 | '', |
| 1019 | - '## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)', | 1040 | + `## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)${reuse ? ' —— **本次复用 carry,整节跳过**' : ''}`, |
| 1041 | + reuse ? `- 本次已从 \`${carryDir}/expectations.md\` 取得分母与逐路由归属,**不要**重新逐 FE Read spec、不要重新对账 router(那是上一次已做完的确定性推导,重做只是重复烧时间)。仅当 carry 缺失/不可用时才回退执行本节。` : '', | ||
| 1020 | '- 分母来源 = 全部 FE spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。', | 1042 | '- 分母来源 = 全部 FE spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。', |
| 1021 | `- 与 \`${c.root}/frontend/\` router 配置对账:FE 作用域声明但 router 缺失的路由 → \`coverageGaps[reason="unreachable-no-route"]\`;router 声明但不属任何 FE 作用域的路由记证据(不入分母、不断言)。`, | 1043 | `- 与 \`${c.root}/frontend/\` router 配置对账:FE 作用域声明但 router 缺失的路由 → \`coverageGaps[reason="unreachable-no-route"]\`;router 声明但不属任何 FE 作用域的路由记证据(不入分母、不断言)。`, |
| 1022 | '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。', | 1044 | '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。', |
| @@ -1026,7 +1048,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | @@ -1026,7 +1048,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | ||
| 1026 | `1) \`node ${c.root}/scripts/setup-test-db.mjs\`(复制源库→一次性副本;源库绝不被 drop)。建副本前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`, | 1048 | `1) \`node ${c.root}/scripts/setup-test-db.mjs\`(复制源库→一次性副本;源库绝不被 drop)。建副本前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`, |
| 1027 | '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此把本轮新迁移 apply 到副本,历史里已有的自动跳过);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', | 1049 | '2) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此把本轮新迁移 apply 到副本,历史里已有的自动跳过);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', |
| 1028 | `3) **注入演示种子**:\`node ${c.root}/scripts/seed-demo-data.mjs\`(幂等账本 \`_demo_seed_history\` 自动跳过源库已灌过的文件,把新 \`sql/seed/*.sql\` 演示数据灌进副本)。失败 → \`envError.kind="seed-error"\` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / 引用序错 / 类型截断 / schema 未初始化),**不**混进交互 RED。`, | 1050 | `3) **注入演示种子**:\`node ${c.root}/scripts/seed-demo-data.mjs\`(幂等账本 \`_demo_seed_history\` 自动跳过源库已灌过的文件,把新 \`sql/seed/*.sql\` 演示数据灌进副本)。失败 → \`envError.kind="seed-error"\` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / 引用序错 / 类型截断 / schema 未初始化),**不**混进交互 RED。`, |
| 1029 | - '4) **此时才跑 sentinel 种子**:按 `docs/03-数据库设计文档.md` 派生 **按语义引用有序的 INSERT** sentinel 种子(先被引用方后引用方;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)。失败 → `envError.kind="seed-error"` + 结构化根因,**不**混进交互 RED。', | 1051 | + `4) **此时才跑 sentinel 种子**:${reuse ? `直接灌 \`${carryDir}/sentinel.sql\`(上次已派生好,**不要**重新按 docs/03 推导——库副本每次重建,数据当然要重灌,但 SQL 本身不必重写)` : '按 `docs/03-数据库设计文档.md` 派生 **按语义引用有序的 INSERT** sentinel 种子(先被引用方后引用方;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)'}。失败 → \`envError.kind="seed-error"\` + 结构化根因,**不**混进交互 RED。`, |
| 1030 | ' - **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_]` 格式。', | 1052 | ' - **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_]` 格式。', |
| 1031 | '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', | 1053 | '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', |
| 1032 | `- \`finally\` **硬要求 kill 本门起的全部子进程**,**并跑 \`node ${c.root}/scripts/drop-test-db.mjs\` 删副本**(无论成败;源库不动);端口 + pid 写入 \`envError.ports\` / \`envError.pids\`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。`, | 1054 | `- \`finally\` **硬要求 kill 本门起的全部子进程**,**并跑 \`node ${c.root}/scripts/drop-test-db.mjs\` 删副本**(无论成败;源库不动);端口 + pid 写入 \`envError.ports\` / \`envError.pids\`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。`, |
| @@ -1043,7 +1065,8 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | @@ -1043,7 +1065,8 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | ||
| 1043 | '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', | 1065 | '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', |
| 1044 | '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。', | 1066 | '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。', |
| 1045 | '', | 1067 | '', |
| 1046 | - '## step4 推导期望', | 1068 | + `## step4 推导期望${reuse ? ' —— **本次复用 carry,整节跳过**' : ''}`, |
| 1069 | + reuse ? `- 期望以 \`${carryDir}/expectations.md\` 为准(含每控件预期效果、每文字区域预期内容与来源、sentinel 值表)。**不要**重新读 prototype/REQ/docs05 推导——上一轮 fix 只改了 \`frontend/\` 实现,不改这些期望的来源。` : '', | ||
| 1047 | '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', | 1070 | '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', |
| 1048 | '', | 1071 | '', |
| 1049 | '## step5 断言(三层:交互/文字/样式 + 可观测效果白名单 + 硬问题带源码 locator)', | 1072 | '## step5 断言(三层:交互/文字/样式 + 可观测效果白名单 + 硬问题带源码 locator)', |
| @@ -1220,6 +1243,70 @@ const PROTOTYPE_PREVIEW_ATTEMPT_MAX = 2 | @@ -1220,6 +1243,70 @@ const PROTOTYPE_PREVIEW_ATTEMPT_MAX = 2 | ||
| 1220 | const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' | 1243 | const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' |
| 1221 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' | 1244 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' |
| 1222 | 1245 | ||
| 1246 | +// ── 进度 ETA(narrator 行)────────────────────────────────────────────────── | ||
| 1247 | +// 动机:左侧 Phases 面板的分母 = 该分组**已派生**的子代理数(harness 侧计数;脚本无 API 预声明 | ||
| 1248 | +// 总量),串行链上它只能 0/1→1/2 地长,看不出"还剩多少 / 还要多久"。故用 log() narrator 行补 | ||
| 1249 | +// 上「总量 + 预计分钟」——纯显示,绝不参与任何控制流判定。 | ||
| 1250 | +// STAGE_MIN:各 stage 的实测耗时 `[p25, 中位数, p75]`(分钟),键 = site/label 的 | ||
| 1251 | +// `<kind>` 或 `<kind>:<phase>` 前缀。样本 = 本机同一 ERP 项目 13 次 coding 运行的子代理 | ||
| 1252 | +// transcript(首末 timestamp 差;n 见行尾)。运行时禁 Date.now()(会破坏 resume 重放), | ||
| 1253 | +// 所以这里只能按"剩余段数 × 实测分布"静态外推,不做真实计时——是量级参考,不是承诺。 | ||
| 1254 | +const STAGE_MIN = { | ||
| 1255 | + 'spec:backend': [1.2, 3.4, 5.9], // n=31 | ||
| 1256 | + 'spec:frontend': [3.8, 4.7, 6.9], // n=15 | ||
| 1257 | + 'plan:backend': [2.8, 5.1, 6.5], // n=20 | ||
| 1258 | + 'plan:frontend': [3.3, 5.6, 6.6], // n=14 | ||
| 1259 | + 'tdd:backend': [3.5, 10.8, 25.6], // n=21 | ||
| 1260 | + 'tdd:frontend': [11.3, 19.4, 33.9], // n=14 | ||
| 1261 | + 'verify:backend': [3.7, 5.7, 7.7], // n=23 | ||
| 1262 | + 'verify:frontend':[2.8, 3.2, 5.1], // n=16 | ||
| 1263 | + 'review:backend': [2.4, 3.2, 4.0], // n=21(每轮) | ||
| 1264 | + 'review:frontend':[3.5, 4.2, 5.6], // n=14(每轮) | ||
| 1265 | + 'fix:backend': [2.4, 3.2, 4.0], // fix 轮与 review 同量级 | ||
| 1266 | + 'fix:frontend': [3.5, 4.2, 5.6], | ||
| 1267 | + gate: [2.5, 9.4, 11.8], // n=45(每 attempt) | ||
| 1268 | + seed: [22.0, 23.5, 25.0], // n=2 | ||
| 1269 | + report: [1.4, 4.9, 14.7], // n=24 | ||
| 1270 | + behavior: [10.2, 18.4, 45.0], // n=13(每 behaviorRound × attempt) | ||
| 1271 | + 'behavior-env': [10.2, 18.4, 45.0], | ||
| 1272 | + 'behavior-reverify': [2.1, 2.3, 2.4], // n=4 | ||
| 1273 | + preview: [3.1, 4.8, 6.9], // n=12 | ||
| 1274 | + skeleton: [3.8, 5.7, 5.7], // n=5 | ||
| 1275 | +} | ||
| 1276 | +const MICRO_MIN = 0.25 // tag / docs08 勾选 / 分支类微步骤(不在表内 → 不播报) | ||
| 1277 | +const MICRO_PER_ITEM = 3 // 每 item 的微步骤:docs08 查 + docs08 勾 + req-done tag | ||
| 1278 | +const fmtMin = (m) => m >= 90 ? `~${Math.round(m / 6) / 10} 小时` : `~${Math.round(m)} 分钟` | ||
| 1279 | +const fmtRange = (r) => `${Math.round(r[0])}–${Math.round(r[2])} 分钟` | ||
| 1280 | +// etaOf:site(`<kind>:<phase>:<id>` / `<kind>:<id>`)→ 三分位数组;表外(微步骤 / 仲裁)返回 null。 | ||
| 1281 | +function etaOf(site) { | ||
| 1282 | + const p = String(site || '').split(':') | ||
| 1283 | + return STAGE_MIN[`${p[0]}:${p[1]}`] || STAGE_MIN[p[0]] || null | ||
| 1284 | +} | ||
| 1285 | +// etaLog:每个可计时 stage 在**开跑**与**每次重跑**时各播报一行——"预计 X–Y 分钟" / | ||
| 1286 | +// "第 n 次重跑,再需 X–Y 分钟"。表外的微步骤静默(0.2 分钟级,播报只是噪声)。 | ||
| 1287 | +// 纯显示,绝不参与控制流;round 由调用方的重试循环下标透传。 | ||
| 1288 | +function etaLog(site, round = 1, note = '') { | ||
| 1289 | + const r = etaOf(site) | ||
| 1290 | + if (!r) return | ||
| 1291 | + const tail = note ? `(${note})` : '' | ||
| 1292 | + log(round <= 1 | ||
| 1293 | + ? `eta ${site}: 预计 ${fmtRange(r)}${tail}` | ||
| 1294 | + : `eta ${site}: 第 ${round} 次重跑,再需 ${fmtRange(r)}${tail}`) | ||
| 1295 | +} | ||
| 1296 | +// itemMin:单个 REQ/FE 的全链预计(spec+plan+tdd+verify+review 1 轮 + 微步骤),取中位数,顺利路径。 | ||
| 1297 | +const itemMin = (ph, q = 1) => ['spec', 'plan', 'tdd', 'verify', 'review'] | ||
| 1298 | + .reduce((s, k) => s + STAGE_MIN[`${k}:${ph}`][q], 0) + MICRO_MIN * MICRO_PER_ITEM | ||
| 1299 | +// moduleMin:单模块预计(顺利路径:review 1 轮、gate 1 次过、behavior 1 轮绿、零仲裁重试)。 | ||
| 1300 | +// q:0=p25(顺风)/ 1=中位数 / 2=p75(逆风),用于给出区间而非单点。 | ||
| 1301 | +function moduleMin(m, q = 1) { | ||
| 1302 | + let t = MICRO_MIN * 2 // 分支准备 + cross-module 记账 | ||
| 1303 | + const nb = (m.reqs || []).length, nf = (m.feItems || []).length | ||
| 1304 | + if (nb) t += nb * itemMin('backend', q) + STAGE_MIN.gate[q] + STAGE_MIN.seed[q] + STAGE_MIN.report[q] | ||
| 1305 | + if (nf) t += STAGE_MIN.preview[q] + STAGE_MIN.skeleton[q] + nf * itemMin('frontend', q) | ||
| 1306 | + + STAGE_MIN.behavior[q] + STAGE_MIN.gate[q] + STAGE_MIN.report[q] | ||
| 1307 | + return t | ||
| 1308 | +} | ||
| 1309 | + | ||
| 1223 | // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记后随结果回传供人工事后审阅。 | 1310 | // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记后随结果回传供人工事后审阅。 |
| 1224 | // sink 参数:模块作用域调用传 c.decisions(per-module 收集器,并行交错下水位线语义失效的替代); | 1311 | // sink 参数:模块作用域调用传 c.decisions(per-module 收集器,并行交错下水位线语义失效的替代); |
| 1225 | // 缺省落全局 autonomousDecisions(仅剩"漏传 dec 的兜底"语义,顶层 return 时作为残量并入)。 | 1312 | // 缺省落全局 autonomousDecisions(仅剩"漏传 dec 的兜底"语义,顶层 return 时作为残量并入)。 |
| @@ -1299,6 +1386,7 @@ async function adjudicate(site, context, grp, round, root = ROOT) { | @@ -1299,6 +1386,7 @@ async function adjudicate(site, context, grp, round, root = ROOT) { | ||
| 1299 | async function runStage(makePrompt, { site, grp, label, validate, allowContinue = false, dec, root = ROOT }) { | 1386 | async function runStage(makePrompt, { site, grp, label, validate, allowContinue = false, dec, root = ROOT }) { |
| 1300 | let guidance = '' | 1387 | let guidance = '' |
| 1301 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { | 1388 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { |
| 1389 | + etaLog(site, round) // 开跑 / 仲裁后重跑各播报一次预计耗时(纯显示) | ||
| 1302 | const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) | 1390 | const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) |
| 1303 | if (!res) { | 1391 | if (!res) { |
| 1304 | const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round, root) | 1392 | const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round, root) |
| @@ -1322,10 +1410,13 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue | @@ -1322,10 +1410,13 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue | ||
| 1322 | // runAction:跑一个 ACTION_RESULT 微步骤(git / 文件写),失败时经 adjudicate 决定 retry/continue/halt。 | 1410 | // runAction:跑一个 ACTION_RESULT 微步骤(git / 文件写),失败时经 adjudicate 决定 retry/continue/halt。 |
| 1323 | // allowContinue=true 时 continue 视为"接受失败并前进"(仅用于纯可视化等可安全跳过的副作用)。 | 1411 | // allowContinue=true 时 continue 视为"接受失败并前进"(仅用于纯可视化等可安全跳过的副作用)。 |
| 1324 | // dec:与 runStage 调用点签名统一而收(ACTION_RESULT 无 decisions 字段,当前不消费);root:仲裁取证树根(补 11)。 | 1412 | // dec:与 runStage 调用点签名统一而收(ACTION_RESULT 无 decisions 字段,当前不消费);root:仲裁取证树根(补 11)。 |
| 1325 | -async function runAction(makePrompt, { site, grp, label, allowContinue = false, dec, root = ROOT }) { | 1413 | +// schema:缺省 ACTION_RESULT_SCHEMA;合并版微步骤(如 BRANCH_READY_SCHEMA)在 success 之外还带 |
| 1414 | +// 回执字段,只要 schema 含 `success` 布尔,本函数的失败/仲裁/重试语义就逐字不变。 | ||
| 1415 | +async function runAction(makePrompt, { site, grp, label, allowContinue = false, dec, root = ROOT, schema = ACTION_RESULT_SCHEMA }) { | ||
| 1326 | let guidance = '' | 1416 | let guidance = '' |
| 1327 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { | 1417 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { |
| 1328 | - const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA}) | 1418 | + etaLog(site, round) // 表内的 action(seed 等)才播报;微步骤静默 |
| 1419 | + const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema}) | ||
| 1329 | if (!r) { | 1420 | if (!r) { |
| 1330 | const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round, root) | 1421 | const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round, root) |
| 1331 | if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) | 1422 | if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) |
| @@ -1479,16 +1570,6 @@ function worktreeCleanPromptM(c) { | @@ -1479,16 +1570,6 @@ function worktreeCleanPromptM(c) { | ||
| 1479 | ].join('\n') | 1570 | ].join('\n') |
| 1480 | } | 1571 | } |
| 1481 | 1572 | ||
| 1482 | -function checkBranchExistsPromptM(branch, c) { | ||
| 1483 | - return [ | ||
| 1484 | - `# 本地分支 \`${branch}\` 是否存在`, | ||
| 1485 | - microStepContract(c.root), | ||
| 1486 | - '', | ||
| 1487 | - `跑 \`git -C ${c.root} rev-parse --verify refs/heads/${branch}\`(用 2>/dev/null 抑制 stderr)。`, | ||
| 1488 | - '## 输出(EXISTS_SCHEMA)', | ||
| 1489 | - '- exit=0 → `{ "exists": true }`;非 0 → `{ "exists": false }`', | ||
| 1490 | - ].join('\n') | ||
| 1491 | -} | ||
| 1492 | 1573 | ||
| 1493 | function currentBranchPromptM(c) { | 1574 | function currentBranchPromptM(c) { |
| 1494 | return [ | 1575 | return [ |
| @@ -1502,6 +1583,27 @@ function currentBranchPromptM(c) { | @@ -1502,6 +1583,27 @@ function currentBranchPromptM(c) { | ||
| 1502 | } | 1583 | } |
| 1503 | 1584 | ||
| 1504 | // ── 微步骤:分支生命周期 action ── | 1585 | // ── 微步骤:分支生命周期 action ── |
| 1586 | +// ensureBranchCheckedOutPromptM:合并版(原为 exists? → checkout|create → head 三个子代理会话)。 | ||
| 1587 | +// 三段之间没有任何 JS 决策——JS 只是把"存在与否"这个布尔转手喂回下一个会话,再派第三个会话读回 | ||
| 1588 | +// HEAD 确认。git 层面本就是一条确定性链(rev-parse --verify → checkout / checkout -b → rev-parse | ||
| 1589 | +// --abbrev-ref),合成单会话后每个模块省两次会话往返,且 HEAD 由同一会话现场回报(比事后另起一 | ||
| 1590 | +// 个会话读更贴近真值)。JS 侧保留原有的 HEAD 不符仲裁重切循环(本函数只是它的第一次尝试)。 | ||
| 1591 | +function ensureBranchCheckedOutPromptM(branch, fromBranch, c) { | ||
| 1592 | + return [ | ||
| 1593 | + `# 确保 HEAD 落在本地分支 \`${branch}\`(不存在则从 \`${fromBranch}\` 新建)`, | ||
| 1594 | + microStepContract(c.root), | ||
| 1595 | + '', | ||
| 1596 | + `1. 跑 \`git -C ${c.root} rev-parse --verify refs/heads/${branch}\`(用 2>/dev/null 抑制 stderr)判断分支是否已存在。`, | ||
| 1597 | + `2. 按结果二选一——**只跑一条路径**:`, | ||
| 1598 | + ` - exit=0(已存在)→ 跑 \`git -C ${c.root} checkout ${branch}\`,\`created\` 填 false。`, | ||
| 1599 | + ` - 非 0(不存在)→ 按序跑 \`git -C ${c.root} checkout ${fromBranch}\` 再 \`git -C ${c.root} checkout -b ${branch}\`,\`created\` 填 true。`, | ||
| 1600 | + `3. 跑 \`git -C ${c.root} rev-parse --abbrev-ref HEAD\`,把 stdout 第一行去空白后填进 \`branch\`(**据实回填**——即便与期望不符也如实填,绝不改写成期望值,上层据此仲裁)。`, | ||
| 1601 | + '## 输出(BRANCH_READY_SCHEMA)', | ||
| 1602 | + '- 全部成功:`{ "success": true, "branch": "<第 3 步实测 HEAD>", "created": <true|false> }`', | ||
| 1603 | + '- 任一步失败:`{ "success": false, "error": "<which step + stderr 摘要>", "branch": "<能读到就填实测 HEAD,读不到填空串>" }`', | ||
| 1604 | + ].join('\n') | ||
| 1605 | +} | ||
| 1606 | + | ||
| 1505 | function checkoutExistingBranchPromptM(branch, c) { | 1607 | function checkoutExistingBranchPromptM(branch, c) { |
| 1506 | return [ | 1608 | return [ |
| 1507 | `# 切到已存在的本地分支 \`${branch}\``, | 1609 | `# 切到已存在的本地分支 \`${branch}\``, |
| @@ -1514,20 +1616,10 @@ function checkoutExistingBranchPromptM(branch, c) { | @@ -1514,20 +1616,10 @@ function checkoutExistingBranchPromptM(branch, c) { | ||
| 1514 | ].join('\n') | 1616 | ].join('\n') |
| 1515 | } | 1617 | } |
| 1516 | 1618 | ||
| 1517 | -function createBranchFromPromptM(fromBranch, newBranch, c) { | ||
| 1518 | - return [ | ||
| 1519 | - `# 从 \`${fromBranch}\` 新建并切到 \`${newBranch}\``, | ||
| 1520 | - microStepContract(c.root), | ||
| 1521 | - '', | ||
| 1522 | - `按序跑:\`git -C ${c.root} checkout ${fromBranch}\`,然后 \`git -C ${c.root} checkout -b ${newBranch}\`。`, | ||
| 1523 | - '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1524 | - '- 全成功:`{ "success": true }`;任一失败:`{ "success": false, "error": "<which step + stderr>" }`', | ||
| 1525 | - ].join('\n') | ||
| 1526 | -} | ||
| 1527 | 1619 | ||
| 1528 | // syncBranchWithDefaultPromptM:把默认分支合进当前功能分支(幂等)。 | 1620 | // syncBranchWithDefaultPromptM:把默认分支合进当前功能分支(幂等)。 |
| 1529 | // | 1621 | // |
| 1530 | -// 为什么必须有这一步:runBranchSetup 对**新建**分支以默认分支为起点(createBranchFromPromptM), | 1622 | +// 为什么必须有这一步:runBranchSetup 对**新建**分支以默认分支为起点(ensureBranchCheckedOutPromptM 的新建路径), |
| 1531 | // 但对**已存在**分支只做 checkout(主根)/ worktree add(lane 分支 2),二者都不带任何同步动作。 | 1623 | // 但对**已存在**分支只做 checkout(主根)/ worktree add(lane 分支 2),二者都不带任何同步动作。 |
| 1532 | // 于是「上一轮跑完留下的功能分支」会停在它当时的位置,其后提交到默认分支的一切——新 REQ 卡片、 | 1624 | // 于是「上一轮跑完留下的功能分支」会停在它当时的位置,其后提交到默认分支的一切——新 REQ 卡片、 |
| 1533 | // 新 migration、docs 增量——对本轮**全部不可见**。子代理据残缺事实作业不会报错,只会自行发明 | 1625 | // 新 migration、docs 增量——对本轮**全部不可见**。子代理据残缺事实作业不会报错,只会自行发明 |
| @@ -1946,40 +2038,31 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { | @@ -1946,40 +2038,31 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { | ||
| 1946 | ].join('\n') | 2038 | ].join('\n') |
| 1947 | } | 2039 | } |
| 1948 | 2040 | ||
| 1949 | -// ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect;read-then-write)── | ||
| 1950 | -function readDocs08CheckboxPromptM(fe, id, c) { | 2041 | +// ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect)─────── |
| 2042 | +// 合并版(原为 read-then-write 两个子代理会话):定位 + 判态 + 勾选 + commit 收进**一个**会话。 | ||
| 2043 | +// 拆两段的唯一理由曾是"让 JS 拿到 lineNumber 再喂写入方",但写入方本就带无行号的回退定位分支, | ||
| 2044 | +// 行号只是加速器不是必要输入——两段之间没有任何 JS 决策,纯白烧一次会话往返(每个 REQ/FE 一次)。 | ||
| 2045 | +// 三种非错误终态经 detail 回传(not-found / already-checked / flipped),调用方据此打日志, | ||
| 2046 | +// 与拆分版的分支日志逐条对应;任何真失败仍走 success:false(cosmetic,调用方只 log 不阻断)。 | ||
| 2047 | +function flipDocs08CheckboxPromptM(fe, id, phase, c) { | ||
| 1951 | const section = fe ? '§ 三' : '§ 二' | 2048 | const section = fe ? '§ 三' : '§ 二' |
| 1952 | - const kind = fe ? '功能' : 'REQ' | 2049 | + const scope = fe ? `§ 三 功能 ${id}` : `§ 二 REQ ${id}` |
| 1953 | const locator = fe | 2050 | const locator = fe |
| 1954 | ? `Read \`${c.root}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。` | 2051 | ? `Read \`${c.root}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。` |
| 1955 | : `Read \`${c.root}/docs/08-模块任务管理.md\`,定位 ${section},找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。` | 2052 | : `Read \`${c.root}/docs/08-模块任务管理.md\`,定位 ${section},找**去掉行首空白后**以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。` |
| 1956 | return [ | 2053 | return [ |
| 1957 | - `# 读 docs/08 ${section} ${kind} \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`, | 2054 | + `# 把 docs/08 ${scope} 勾选为 \`[x]\` 并 commit(含定位与判态)`, |
| 1958 | microStepContract(c.root), | 2055 | microStepContract(c.root), |
| 1959 | '', | 2056 | '', |
| 1960 | - locator, | ||
| 1961 | - '## 输出(CHECKBOX_STATE_SCHEMA)', | ||
| 1962 | - `- 命中 \`- [x] ${id} ...\`:\`{ "found": true, "state": "checked", "lineNumber": <行号> }\``, | ||
| 1963 | - `- 命中 \`- [ ] ${id} ...\`:\`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``, | ||
| 1964 | - '- 找不到:`{ "found": false, "state": "unchecked" }`(state 仍必填,避免 schema 失败掩盖真实缺口)。', | ||
| 1965 | - ].join('\n') | ||
| 1966 | -} | ||
| 1967 | - | ||
| 1968 | -function writeDocs08CheckboxPromptM(fe, id, phase, lineNumber, c) { | ||
| 1969 | - const scope = fe ? `§ 三 功能 ${id}` : `§ 二 REQ ${id}` | ||
| 1970 | - const lineGuard = (typeof lineNumber === 'number' && Number.isFinite(lineNumber)) | ||
| 1971 | - ? `先 Read \`${c.root}/docs/08-模块任务管理.md\` 第 ${lineNumber} 行(1-based),确认该行去掉行首空白后以 \`- [ ] ${id} \` 开头;不满足则返回 \`{success:false, error:"line-${lineNumber}-mismatch: actual=<actual>"}\`。然后只替换第 ${lineNumber} 行的第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。` | ||
| 1972 | - : `定位 docs/08 ${scope} 中去掉行首空白后以 \`- [ ] ${id} \` 开头的唯一一行,只替换该行第一个 \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本。` | ||
| 1973 | - return [ | ||
| 1974 | - `# 把 docs/08 ${scope} 的 \`[ ]\` 勾选为 \`[x]\` 并 commit`, | ||
| 1975 | - microStepContract(c.root), | ||
| 1976 | - '', | ||
| 1977 | - `调用方已读到状态 = \`unchecked\`(你不必再读一遍)。`, | ||
| 1978 | - `1. ${lineGuard}`, | ||
| 1979 | - `2. 跑 \`git -C ${c.root} add docs/08-模块任务管理.md\`。`, | ||
| 1980 | - `3. 跑 \`git -C ${c.root} commit -m "chore(${phase}:${id}): mark ${id} approved in docs/08"\`。`, | 2057 | + `1. ${locator}`, |
| 2058 | + `2. 按命中结果分流——**只有第 3 种才写文件**:`, | ||
| 2059 | + ` - 找不到该行 → 直接返回 \`{ "success": true, "detail": "not-found" }\`(不写、不 commit)。`, | ||
| 2060 | + ` - 命中 \`- [x] ${id} ...\`(已勾选)→ 直接返回 \`{ "success": true, "detail": "already-checked" }\`(幂等,不写、不 commit)。`, | ||
| 2061 | + ` - 命中 \`- [ ] ${id} ...\` → 只替换该行的**第一个** \`[ ]\` 为 \`[x]\`,保留缩进与 id 之后的全部文本;其余行一律不动。`, | ||
| 2062 | + `3. 仅在第 2 步真改了文件时:跑 \`git -C ${c.root} add docs/08-模块任务管理.md\`,再跑 \`git -C ${c.root} commit -m "chore(${phase}:${id}): mark ${id} approved in docs/08"\`,成功返回 \`{ "success": true, "detail": "flipped" }\`。`, | ||
| 1981 | '## 输出(ACTION_RESULT_SCHEMA)', | 2063 | '## 输出(ACTION_RESULT_SCHEMA)', |
| 1982 | - '- 三步全 OK:`{ "success": true }`;任一失败:`{ "success": false, "error": "<step + reason>" }`', | 2064 | + '- 三种非错误终态按上文填 `detail`(`not-found` / `already-checked` / `flipped`),`success` 恒 true。', |
| 2065 | + '- 任一步真失败(文件读不到 / 行内容与预期不符 / git 失败):`{ "success": false, "error": "<step + reason>" }`。', | ||
| 1983 | ].join('\n') | 2066 | ].join('\n') |
| 1984 | } | 2067 | } |
| 1985 | 2068 | ||
| @@ -2177,10 +2260,23 @@ function writeCrossModuleLogPromptM(moduleId, items, c) { | @@ -2177,10 +2260,23 @@ function writeCrossModuleLogPromptM(moduleId, items, c) { | ||
| 2177 | } | 2260 | } |
| 2178 | 2261 | ||
| 2179 | // ---- 模块完成报告(原 module-report)---- | 2262 | // ---- 模块完成报告(原 module-report)---- |
| 2180 | -function reportPrompt(module, c) { | 2263 | +// facts(Task:module-report 瘦身):调用方把**已在内存里的**门禁事实透传进来—— |
| 2264 | +// gate = testGate 的返回(status/attempts/failures),behavior = runBehaviorGate 绿态回执。 | ||
| 2265 | +// 原实现让报告子代理 Glob + 通读**全部** attempt / behaviorRound 证据来自证"最后一份绿", | ||
| 2266 | +// 而 JS 才是这个事实的第一手持有者(红根本走不到 report——testGate/behaviorGate 会先 halt)。 | ||
| 2267 | +// 实测 module-report 均值 14.6 分钟、占全流程墙钟 11.7%,通读证据是其中的大头。改为: | ||
| 2268 | +// 事实由入参给定,子代理只按给定的**文件名**读它真正要摘要的那一两份(末次 green;flake 时加首次 red)。 | ||
| 2269 | +function reportPrompt(module, c, facts = {}) { | ||
| 2181 | const id = module?.id ?? '<module>' | 2270 | const id = module?.id ?? '<module>' |
| 2182 | const fe = id === 'frontend-phase' | 2271 | const fe = id === 'frontend-phase' |
| 2183 | const phaseId = fe ? 'frontend-phase' : id | 2272 | const phaseId = fe ? 'frontend-phase' : id |
| 2273 | + const gate = facts.gate || {} | ||
| 2274 | + const bh = facts.behavior || {} | ||
| 2275 | + const gAttempts = Number(gate.attempts) || 1 | ||
| 2276 | + const gateEvidence = `${phaseId}-test-gate-r${gAttempts}.md` | ||
| 2277 | + const gateFlake = gAttempts > 1 | ||
| 2278 | + const bhRounds = Number(bh.rounds) || 1 | ||
| 2279 | + const bhEvidence = `frontend-phase-behavior-r${bhRounds}-a${Number(bh.attempts) || 1}.md` | ||
| 2184 | return [ | 2280 | return [ |
| 2185 | `# module-report — ${fe ? '前端阶段' : `模块 ${id}`} 12 节完成报告`, | 2281 | `# module-report — ${fe ? '前端阶段' : `模块 ${id}`} 12 节完成报告`, |
| 2186 | '', | 2282 | '', |
| @@ -2189,10 +2285,12 @@ function reportPrompt(module, c) { | @@ -2189,10 +2285,12 @@ function reportPrompt(module, c) { | ||
| 2189 | '## 目标', | 2285 | '## 目标', |
| 2190 | `test-gate 绿后渲染标准化 **12 节**完成报告,commit 到当前分支(供 milestone 标记)。**只读 git 摘要,不读 diff 正文进上下文。**`, | 2286 | `test-gate 绿后渲染标准化 **12 节**完成报告,commit 到当前分支(供 milestone 标记)。**只读 git 摘要,不读 diff 正文进上下文。**`, |
| 2191 | '', | 2287 | '', |
| 2192 | - '## 前置', | ||
| 2193 | - `- 验证上游 test-gate 绿:Glob \`${c.root}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, | 2288 | + '## 前置(门禁事实由编排层给定,**不要**再 Glob 通读全部证据自证)', |
| 2289 | + `- 上游 test-gate 已 green(编排层判定:共 ${gAttempts} 个 attempt,末次 \`${gateEvidence}\` = green)。红态根本走不到本步——testGate 会先 halt,故**不需要**你重新校验绿不绿。`, | ||
| 2290 | + `- 只需 Read 末次证据 \`${c.root}/docs/superpowers/module-reports/${gateEvidence}\` 取 § ⑤ 要用的摘要${gateFlake ? `;另 Read 首次 \`${phaseId}-test-gate-r1.md\`(attempt>1 = flake,§ ⑤ 需要首次失败用例)` : '(单 attempt,无 flake,无需读其它 attempt 文件)'}。该文件缺失 → halt(证据链断裂)。`, | ||
| 2291 | + gateFlake ? `- **flake 标注(编排层已判定)**:attempt 数 = ${gAttempts} 且末次 green → § ⑤ 顶部必须写 \`flake-detected: r1 red, r${gAttempts} green\`,并附首次失败用例与末次绿色记录链接。` : '', | ||
| 2194 | fe | 2292 | fe |
| 2195 | - ? `- **验证阶段级行为门绿**:Glob \`${c.root}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`,按 behaviorRound → attempt 数字升序读取。**最后一份必须非 RED(绿)**;最后一份 red 或一份证据都没有 → 立即 halt(绝不在行为红 / 无行为证据上打 milestone)。注意 per-FE 行为证据(\`reviews/<date>-<FE>-behavior-*\`)已不再产生,不要校验。` | 2293 | + ? `- 阶段级行为门已 green(编排层判定:behaviorRound=${bhRounds},末次证据 \`${bhEvidence}\`,routesPlanned/Reached=${bh.routesPlanned ?? '?'}/${bh.routesReached ?? '?'},controlsEnumerated=${bh.controlsEnumerated ?? '?'},authState=${bh.authState || '?'})。同样**不要**再 Glob 全部 \`frontend-phase-behavior-r*-a*.md\` 自证——只 Read 末次那一份取 § ⑤/§ ⑧ 需要的逐 FE 小节${bhRounds > 1 ? `;另 Read \`frontend-phase-behavior-reverify-r*.md\`(有 ${bhRounds - 1} 轮行为 fix,§ ⑤ 要记录 fix 轮数与复验结果)` : '(一轮即绿,无 fix、无复验文件)'}。` |
| 2196 | : '', | 2294 | : '', |
| 2197 | '', | 2295 | '', |
| 2198 | '## 收集输入(取摘要而非正文)', | 2296 | '## 收集输入(取摘要而非正文)', |
| @@ -2202,14 +2300,14 @@ function reportPrompt(module, c) { | @@ -2202,14 +2300,14 @@ function reportPrompt(module, c) { | ||
| 2202 | `- § ② "FE 完成清单":扫 \`${c.root}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, | 2300 | `- § ② "FE 完成清单":扫 \`${c.root}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, |
| 2203 | `- § ③ 文件变更:\`git -C ${c.root} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, | 2301 | `- § ③ 文件变更:\`git -C ${c.root} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, |
| 2204 | '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', | 2302 | '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', |
| 2205 | - `- § ⑤:把 \`${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 记录一并纳入 § ⑤ 汇总**。`, | ||
| 2206 | - `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按阶段级行为证据 \`${c.root}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(取最后一份的逐 FE 小节)汇总各 FE 的 \`coverageGaps\` + 样式 \`styleIssues\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, | 2303 | + `- § ⑤:摘要上方"前置"已点名的那一两份 test-gate 证据(${gateFlake ? `\`${phaseId}-test-gate-r1.md\` + \`${gateEvidence}\`` : `仅 \`${gateEvidence}\``})。**另把行为门末次证据 \`${bhEvidence}\` 的 flake / 环境 race(envError)/ 行为 fix 轮数(=${bhRounds - 1})/ 样式 styleIssues 修复轮次 / 文字 continue 记录纳入 § ⑤ 汇总**${bhRounds > 1 ? '(含复验文件)' : ''}。`, |
| 2304 | + `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按行为门末次证据 \`${bhEvidence}\` 的逐 FE 小节**汇总各 FE 的 \`coverageGaps\` + 样式 \`styleIssues\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集。`, | ||
| 2207 | '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', | 2305 | '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', |
| 2208 | ].join('\n') | 2306 | ].join('\n') |
| 2209 | : [ | 2307 | : [ |
| 2210 | `- § ③ 文件变更:\`git -C ${c.root} diff --stat <默认分支 main/master>...HEAD\` / \`--name-status\` / \`git log <默认分支>..HEAD --oneline\`(区间 = 功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)。`, | 2308 | `- § ③ 文件变更:\`git -C ${c.root} diff --stat <默认分支 main/master>...HEAD\` / \`--name-status\` / \`git log <默认分支>..HEAD --oneline\`(区间 = 功能分支 \`module-${id}\` 自默认分支分叉以来的全部改动)。`, |
| 2211 | `- § ② / § ⑨:读 \`${c.root}/docs/superpowers/{specs,plans,reviews}/<日期>-<本模块 REQ>.md\`。`, | 2309 | `- § ② / § ⑨:读 \`${c.root}/docs/superpowers/{specs,plans,reviews}/<日期>-<本模块 REQ>.md\`。`, |
| 2212 | - `- § ⑤:把 \`${c.root}/docs/superpowers/module-reports/${id}-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。`, | 2310 | + `- § ⑤:摘要上方"前置"已点名的那一两份 test-gate 证据(${gateFlake ? `\`${id}-test-gate-r1.md\` + \`${gateEvidence}\`` : `仅 \`${gateEvidence}\``})——不要再按通配符通读全部 attempt。`, |
| 2213 | `- § ⑥ Migration:\`git -C ${c.root} diff --name-only --diff-filter=A -- 'sql/migrations/V*.sql'\` 列新增,每个读第一行作说明。`, | 2311 | `- § ⑥ Migration:\`git -C ${c.root} diff --name-only --diff-filter=A -- 'sql/migrations/V*.sql'\` 列新增,每个读第一行作说明。`, |
| 2214 | `- § ⑦ 跨模块改动:读 \`${c.root}/docs/superpowers/module-reports/${id}-cross-module.md\`(如存在;其中不应再有 \`TBD(CC 补)\`,上一步 cross-module-log 已补齐)。`, | 2312 | `- § ⑦ 跨模块改动:读 \`${c.root}/docs/superpowers/module-reports/${id}-cross-module.md\`(如存在;其中不应再有 \`TBD(CC 补)\`,上一步 cross-module-log 已补齐)。`, |
| 2215 | '- § ④ 读写的表:grep 定位涉 SQL 文件后按需读片段,**不全量读 docs/03**。', | 2313 | '- § ④ 读写的表:grep 定位涉 SQL 文件后按需读片段,**不全量读 docs/03**。', |
| @@ -2256,19 +2354,13 @@ async function runBranchSetup(module, c) { | @@ -2256,19 +2354,13 @@ async function runBranchSetup(module, c) { | ||
| 2256 | log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) | 2354 | log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) |
| 2257 | } | 2355 | } |
| 2258 | 2356 | ||
| 2259 | - if (c.lane == null) { | ||
| 2260 | - // 主根模式:现行 checkout 路径,一行不差(lane 模式跳过——worktree add 已把 lane 放上目标分支, | ||
| 2261 | - // 且 checkout 语义在"分支被别的 worktree 持有"时会 fatal,绝不能在主根重复执行)。 | ||
| 2262 | - const exists = await agentR(checkBranchExistsPromptM(branch, c), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) | ||
| 2263 | - if (exists.exists) { | ||
| 2264 | - await runAction(g => checkoutExistingBranchPromptM(branch, c) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout'), dec: c.decisions, root: c.root}) | ||
| 2265 | - } else { | ||
| 2266 | - await runAction(g => createBranchFromPromptM(def.branch, branch, c) + g, {site:`branchSetup-create:${branch}`, grp:'Milestone', label: lbl('create'), dec: c.decisions, root: c.root}) | ||
| 2267 | - } | ||
| 2268 | - } | ||
| 2269 | - | ||
| 2270 | - // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。 | ||
| 2271 | - let head = await agentR(currentBranchPromptM(c), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | 2357 | + // 主根模式:exists?+checkout|create+head 三合一(lane 模式跳过——worktree add 已把 lane 放上目标 |
| 2358 | + // 分支,且 checkout 语义在"分支被别的 worktree 持有"时会 fatal,绝不能在主根重复执行)。 | ||
| 2359 | + // lane 模式仍需读回 HEAD 供下方仲裁循环判定,走原单读微步骤。 | ||
| 2360 | + let head = c.lane == null | ||
| 2361 | + ? await runAction(g => ensureBranchCheckedOutPromptM(branch, def.branch, c) + g, | ||
| 2362 | + {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('ready'), dec: c.decisions, root: c.root, schema: BRANCH_READY_SCHEMA}) | ||
| 2363 | + : await agentR(currentBranchPromptM(c), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) | ||
| 2272 | for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { | 2364 | for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { |
| 2273 | const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, | 2365 | const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, |
| 2274 | { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj, c.root) | 2366 | { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj, c.root) |
| @@ -2619,6 +2711,10 @@ async function featureLoop(items, phase, c, feStage = 'all') { | @@ -2619,6 +2711,10 @@ async function featureLoop(items, phase, c, feStage = 'all') { | ||
| 2619 | return r.exists | 2711 | return r.exists |
| 2620 | } | 2712 | } |
| 2621 | 2713 | ||
| 2714 | + // 段级进度 narrator(左侧面板分母只随派生增长,这里补真总量 + 预计)。放在并行分叉之前, | ||
| 2715 | + // 使并行批与串行链都能看到本段总量;并行批的实际墙钟按波次宽度缩短,这行是其串行上界。 | ||
| 2716 | + log(`eta ${grp} 段: ${items.length} 个 ${phase === 'backend' ? 'REQ' : 'FE'} × ${fmtMin(itemMin(phase))}/项 ≈ ${fmtMin(items.length * itemMin(phase))}(串行口径)`) | ||
| 2717 | + | ||
| 2622 | // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。 | 2718 | // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。 |
| 2623 | // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。 | 2719 | // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。 |
| 2624 | if (useFeatureParallel && phase === 'backend' && items.length >= 2) { | 2720 | if (useFeatureParallel && phase === 'backend' && items.length >= 2) { |
| @@ -2626,16 +2722,19 @@ async function featureLoop(items, phase, c, feStage = 'all') { | @@ -2626,16 +2722,19 @@ async function featureLoop(items, phase, c, feStage = 'all') { | ||
| 2626 | return | 2722 | return |
| 2627 | } | 2723 | } |
| 2628 | 2724 | ||
| 2629 | - for (const id of items) { | 2725 | + for (const [i, id] of items.entries()) { |
| 2726 | + const left = () => `(${i + 1}/${items.length},余 ${items.length - i - 1} 项 ≈ ${fmtMin((items.length - i - 1) * itemMin(phase))})` | ||
| 2630 | // dedup:phase1 用 fe-code-done;'all'/'e2e'(phase2)用 req-done。phase2 的 fe-code-done 必已存在 | 2727 | // dedup:phase1 用 fe-code-done;'all'/'e2e'(phase2)用 req-done。phase2 的 fe-code-done 必已存在 |
| 2631 | // (phase1 落过),spec/plan 经 ensureSpec/runPlanStage 复用既有产物,tdd(e2e) 只处理延后的 e2e 任务。 | 2728 | // (phase1 落过),spec/plan 经 ensureSpec/runPlanStage 复用既有产物,tdd(e2e) 只处理延后的 e2e 任务。 |
| 2632 | if (isPhase1 ? await isPhase1Done(id) : await isDone(id)) { | 2729 | if (isPhase1 ? await isPhase1Done(id) : await isDone(id)) { |
| 2633 | - log(`featureLoop skip ${phase}:${id} — tag ${isPhase1 ? 'fe-code-done' : 'req-done'}/${id} 已存在`); continue | 2730 | + log(`featureLoop skip ${phase}:${id} — tag ${isPhase1 ? 'fe-code-done' : 'req-done'}/${id} 已存在 ${left()}`); continue |
| 2634 | } | 2731 | } |
| 2732 | + log(`eta ${grp} ${i + 1}/${items.length} ${id} 开跑 ≈ ${fmtMin(itemMin(phase))}${i + 1 < items.length ? `,其后余 ${items.length - i - 1} 项 ≈ ${fmtMin((items.length - i - 1) * itemMin(phase))}` : '(本段最后一项)'}`) | ||
| 2635 | const spec = await ensureSpec(id) | 2733 | const spec = await ensureSpec(id) |
| 2636 | const plan = await runPlanStage(id, spec) | 2734 | const plan = await runPlanStage(id, spec) |
| 2637 | await runImplChain(id, plan.artifactPath, spec.artifactPath, c, undefined, feStage) | 2735 | await runImplChain(id, plan.artifactPath, spec.artifactPath, c, undefined, feStage) |
| 2638 | await (isPhase1 ? tagFeCodeDone(id) : tagDone(id)) | 2736 | await (isPhase1 ? tagFeCodeDone(id) : tagDone(id)) |
| 2737 | + log(`eta ${grp} ${id} 完成 ${left()}`) | ||
| 2639 | } | 2738 | } |
| 2640 | } | 2739 | } |
| 2641 | 2740 | ||
| @@ -2857,13 +2956,10 @@ const REVIEW_HARD_ROUNDS = 10 | @@ -2857,13 +2956,10 @@ const REVIEW_HARD_ROUNDS = 10 | ||
| 2857 | // 注意:docs/08 **checkbox** 发生在模块分支上(approve 后逐功能勾选)→ 穿线 c; | 2956 | // 注意:docs/08 **checkbox** 发生在模块分支上(approve 后逐功能勾选)→ 穿线 c; |
| 2858 | // docs/08 **里程碑字段**发生在 merge 后的默认分支上 → 主根专属(见 runMilestone),两组不要搞混。 | 2957 | // docs/08 **里程碑字段**发生在 merge 后的默认分支上 → 主根专属(见 runMilestone),两组不要搞混。 |
| 2859 | async function flipDocs08Checkbox(fe, id, phase, grp, c) { | 2958 | async function flipDocs08Checkbox(fe, id, phase, grp, c) { |
| 2860 | - const cb = await agent(readDocs08CheckboxPromptM(fe, id, c), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) | ||
| 2861 | - if (!cb) { log(`docs08-checkbox ${id}: 读取子代理无返回(不阻断)`); return } | ||
| 2862 | - if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } | ||
| 2863 | - if (cb.state === 'checked') return | ||
| 2864 | - if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } | ||
| 2865 | - const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber, c), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) | ||
| 2866 | - if (!wr || !wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${(wr && wr.error) || ''}),跳过——cosmetic,不阻断`) | 2959 | + const r = await agent(flipDocs08CheckboxPromptM(fe, id, phase, c), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) |
| 2960 | + if (!r) { log(`docs08-checkbox ${id}: 子代理无返回(不阻断)`); return } | ||
| 2961 | + if (!r.success) { log(`docs08-checkbox ${phase}:${id}: 勾选失败(${r.error || ''}),跳过——cosmetic,不阻断`); return } | ||
| 2962 | + if (r.detail === 'not-found') log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`) | ||
| 2867 | } | 2963 | } |
| 2868 | 2964 | ||
| 2869 | // rwOpts.flipDocs08(Phase F Task 13):false 时 approve 路径跳过 docs/08 checkbox 勾选——feature | 2965 | // rwOpts.flipDocs08(Phase F Task 13):false 时 approve 路径跳过 docs/08 checkbox 勾选——feature |
| @@ -2940,9 +3036,12 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath, c, { flipDoc | @@ -2940,9 +3036,12 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath, c, { flipDoc | ||
| 2940 | // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 | 3036 | // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 |
| 2941 | async function testGate(module, phase, c) { | 3037 | async function testGate(module, phase, c) { |
| 2942 | let attempt = 1 | 3038 | let attempt = 1 |
| 3039 | + const site = `gate:${phase}:${module.id}` | ||
| 3040 | + etaLog(site, 1, '全量回归 + 冷起栈') | ||
| 2943 | let g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) | 3041 | let g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) |
| 2944 | if (g.status === 'red') { // 自动重试 1 次(防 flaky) | 3042 | if (g.status === 'red') { // 自动重试 1 次(防 flaky) |
| 2945 | attempt = 2 | 3043 | attempt = 2 |
| 3044 | + etaLog(site, 2, 'red → 防 flaky 自动重跑') | ||
| 2946 | g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) | 3045 | g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) |
| 2947 | } | 3046 | } |
| 2948 | // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 | 3047 | // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 |
| @@ -2952,10 +3051,13 @@ async function testGate(module, phase, c) { | @@ -2952,10 +3051,13 @@ async function testGate(module, phase, c) { | ||
| 2952 | if (verdict.action !== 'retry') | 3051 | if (verdict.action !== 'retry') |
| 2953 | throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) | 3052 | throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) |
| 2954 | attempt += 1 // retry:再跑一个独立 attempt 证据文件 | 3053 | attempt += 1 // retry:再跑一个独立 attempt 证据文件 |
| 3054 | + etaLog(site, attempt, '仲裁判 retry(辨 flake)') | ||
| 2955 | g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) | 3055 | g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) |
| 2956 | } | 3056 | } |
| 2957 | if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) | 3057 | if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) |
| 2958 | - return g | 3058 | + // attempts 回传给 module-report:报告不必再 Glob + 通读全部 attempt 证据去自证"最后一份绿", |
| 3059 | + // JS 这里就是那个事实的第一手持有者(见 reportPrompt 的 gateFacts)。 | ||
| 3060 | + return { ...g, attempts: attempt } | ||
| 2959 | } | 3061 | } |
| 2960 | 3062 | ||
| 2961 | // ---- 前端阶段级行为验收控制流(runBehaviorGateOnce + runBehaviorGate)---- | 3063 | // ---- 前端阶段级行为验收控制流(runBehaviorGateOnce + runBehaviorGate)---- |
| @@ -3065,14 +3167,17 @@ async function runPrototypePreview(feItems, c) { | @@ -3065,14 +3167,17 @@ async function runPrototypePreview(feItems, c) { | ||
| 3065 | async function runBehaviorGateOnce(feItems, behaviorRound, c) { | 3167 | async function runBehaviorGateOnce(feItems, behaviorRound, c) { |
| 3066 | const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` | 3168 | const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` |
| 3067 | let attempt = 1 | 3169 | let attempt = 1 |
| 3170 | + etaLog('behavior:frontend-phase', 1, `r${behaviorRound},冷起全栈 + 枚举 ${(feItems || []).length} 个 FE`) | ||
| 3068 | let bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), | 3171 | let bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), |
| 3069 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | 3172 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 3070 | recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) | 3173 | recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) |
| 3174 | + const withAttempts = () => { bg = { ...bg, attempts: attempt }; return bg } // attempts 供 module-report 定位证据文件 | ||
| 3071 | 3175 | ||
| 3072 | // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 | 3176 | // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 |
| 3073 | // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 | 3177 | // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 |
| 3074 | while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { | 3178 | while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { |
| 3075 | attempt += 1 | 3179 | attempt += 1 |
| 3180 | + etaLog('behavior:frontend-phase', attempt, `r${behaviorRound},环境 race 自动重起全栈`) | ||
| 3076 | bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), | 3181 | bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), |
| 3077 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | 3182 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 3078 | recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) | 3183 | recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) |
| @@ -3091,13 +3196,14 @@ async function runBehaviorGateOnce(feItems, behaviorRound, c) { | @@ -3091,13 +3196,14 @@ async function runBehaviorGateOnce(feItems, behaviorRound, c) { | ||
| 3091 | allowContinue:false }, 'Behavior', adj, c.root) | 3196 | allowContinue:false }, 'Behavior', adj, c.root) |
| 3092 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) | 3197 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) |
| 3093 | attempt += 1 | 3198 | attempt += 1 |
| 3199 | + etaLog('behavior:frontend-phase', attempt, `r${behaviorRound},仲裁判 retry`) | ||
| 3094 | bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), | 3200 | bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt, c), |
| 3095 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | 3201 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 3096 | recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) | 3202 | recordDecisions('behavior:frontend-phase', bg.decisions, c.decisions) |
| 3097 | envState = behaviorEnvBlocked(bg) | 3203 | envState = behaviorEnvBlocked(bg) |
| 3098 | } | 3204 | } |
| 3099 | if (envState.blocked) throw new Error(`HALT behavior-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) | 3205 | if (envState.blocked) throw new Error(`HALT behavior-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) |
| 3100 | - return bg | 3206 | + return withAttempts() |
| 3101 | } | 3207 | } |
| 3102 | 3208 | ||
| 3103 | // runBehaviorGate:阶段级行为门主循环(被顶层 frontend 段调用,phase('Behavior') 下)。green 才正常返回(放行进 testGate)。 | 3209 | // runBehaviorGate:阶段级行为门主循环(被顶层 frontend 段调用,phase('Behavior') 下)。green 才正常返回(放行进 testGate)。 |
| @@ -3107,6 +3213,8 @@ async function runBehaviorGate(feItems, c) { | @@ -3107,6 +3213,8 @@ async function runBehaviorGate(feItems, c) { | ||
| 3107 | const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` | 3213 | const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` |
| 3108 | const softPassed = new Set() | 3214 | const softPassed = new Set() |
| 3109 | for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_STAGE_MAX; behaviorRound++) { | 3215 | for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_STAGE_MAX; behaviorRound++) { |
| 3216 | + if (behaviorRound > 1) | ||
| 3217 | + log(`eta behavior: 第 ${behaviorRound}/${BEHAVIOR_STAGE_MAX} 轮(上轮非绿,fix 已改源码)——本轮重跑整门再需 ${fmtRange(STAGE_MIN.behavior)}`) | ||
| 3110 | const bg = await runBehaviorGateOnce(feItems, behaviorRound, c) | 3218 | const bg = await runBehaviorGateOnce(feItems, behaviorRound, c) |
| 3111 | 3219 | ||
| 3112 | // 1) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 | 3220 | // 1) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 |
| @@ -3199,7 +3307,9 @@ async function runBehaviorGate(feItems, c) { | @@ -3199,7 +3307,9 @@ async function runBehaviorGate(feItems, c) { | ||
| 3199 | // 5) green 判定:behaviorHard 为空 ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 门 green 放行。 | 3307 | // 5) green 判定:behaviorHard 为空 ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 门 green 放行。 |
| 3200 | if (behaviorHard.length === 0) { | 3308 | if (behaviorHard.length === 0) { |
| 3201 | log(`behavior frontend-phase green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) | 3309 | log(`behavior frontend-phase green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) |
| 3202 | - return | 3310 | + // 绿态事实回传 module-report(同 testGate 的 attempts:报告不再重新 Glob 自证行为门绿)。 |
| 3311 | + return { rounds: behaviorRound, attempts: bg.attempts || 1, routesPlanned: bg.routesPlanned, | ||
| 3312 | + routesReached: bg.routesReached, controlsEnumerated: bg.controlsEnumerated, authState: bg.authState || '' } | ||
| 3203 | } | 3313 | } |
| 3204 | 3314 | ||
| 3205 | // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不放行)。 | 3315 | // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不放行)。 |
| @@ -3281,6 +3391,14 @@ for (const m of routed.modules) { | @@ -3281,6 +3391,14 @@ for (const m of routed.modules) { | ||
| 3281 | 3391 | ||
| 3282 | const todo = routed.modules.filter(m => !m.done) | 3392 | const todo = routed.modules.filter(m => !m.done) |
| 3283 | log(`coding: ${todo.length}/${routed.modules.length} modules to run`) | 3393 | log(`coding: ${todo.length}/${routed.modules.length} modules to run`) |
| 3394 | +// ETA 总览(顺利路径中位数外推;并行开启时是串行上界,实际按波次宽度缩短)。 | ||
| 3395 | +{ | ||
| 3396 | + const nb = todo.reduce((s, m) => s + (m.reqs || []).length, 0) | ||
| 3397 | + const nf = todo.reduce((s, m) => s + (m.feItems || []).length, 0) | ||
| 3398 | + const tot = todo.reduce((s, m) => s + moduleMin(m), 0) | ||
| 3399 | + const lo = todo.reduce((s, m) => s + moduleMin(m, 0), 0), hi = todo.reduce((s, m) => s + moduleMin(m, 2), 0) | ||
| 3400 | + log(`eta 总览: 待跑 ${nb} 个后端 REQ + ${nf} 个 FE,全串行顺利路径 ${fmtMin(tot)}(区间 ${fmtMin(lo)}–${fmtMin(hi)};每轮 review-fix / gate 重跑 / behavior 重跑各另加)`) | ||
| 3401 | +} | ||
| 3284 | 3402 | ||
| 3285 | // ============================================================================ | 3403 | // ============================================================================ |
| 3286 | // 调度器(Phase B):LLM 出依赖边 + JS 拓扑波次。Phase D 的波次主循环消费——useParallel 开启时 | 3404 | // 调度器(Phase B):LLM 出依赖边 + JS 拓扑波次。Phase D 的波次主循环消费——useParallel 开启时 |
| @@ -3477,8 +3595,12 @@ async function runFrontendPhase1(feModule, c) { | @@ -3477,8 +3595,12 @@ async function runFrontendPhase1(feModule, c) { | ||
| 3477 | } | 3595 | } |
| 3478 | 3596 | ||
| 3479 | async function runModule(module, c) { | 3597 | async function runModule(module, c) { |
| 3598 | + // 门禁事实收集器:testGate / runBehaviorGate 的绿态回执,末尾透传给 reportPrompt—— | ||
| 3599 | + // 报告不必再 Glob 通读全部证据文件去自证"最后一份绿"(JS 才是第一手持有者,红态走不到 report)。 | ||
| 3600 | + const reportFacts = {} | ||
| 3480 | try { | 3601 | try { |
| 3481 | if (c.lane == null) phase('Milestone') | 3602 | if (c.lane == null) phase('Milestone') |
| 3603 | + log(`eta ${module.id}: ${(module.reqs || []).length} 个后端 REQ + ${(module.feItems || []).length} 个 FE ≈ ${fmtMin(moduleMin(module))}(区间 ${fmtMin(moduleMin(module, 0))}–${fmtMin(moduleMin(module, 2))},顺利路径)`) | ||
| 3482 | await runBranchSetup(module, c) | 3604 | await runBranchSetup(module, c) |
| 3483 | if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过) | 3605 | if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过) |
| 3484 | if (c.lane == null) phase('Backend') | 3606 | if (c.lane == null) phase('Backend') |
| @@ -3486,7 +3608,8 @@ async function runModule(module, c) { | @@ -3486,7 +3608,8 @@ async function runModule(module, c) { | ||
| 3486 | if (c.lane == null) phase('Gate') | 3608 | if (c.lane == null) phase('Gate') |
| 3487 | // 起栈互斥(补 9):testGate 整段包锁——其 e2e 段 Playwright globalSetup 按固定端口冷起后端, | 3609 | // 起栈互斥(补 9):testGate 整段包锁——其 e2e 段 Playwright globalSetup 按固定端口冷起后端, |
| 3488 | // 不可单拆。串行模式锁恒空闲,行为不变(统一纪律,免按模式分叉)。 | 3610 | // 不可单拆。串行模式锁恒空闲,行为不变(统一纪律,免按模式分叉)。 |
| 3489 | - await withStackLock(() => testGate(module, 'backend', c)) | 3611 | + // 返回值(status/attempts)透传给 module-report:报告据此免去通读全部 attempt 证据自证绿。 |
| 3612 | + reportFacts.gate = await withStackLock(() => testGate(module, 'backend', c)) | ||
| 3490 | // 演示种子生成 stage(Seed):在 testGate 后跑——此时本模块 schema(含 tdd 新增的 V<n> migration) | 3613 | // 演示种子生成 stage(Seed):在 testGate 后跑——此时本模块 schema(含 tdd 新增的 V<n> migration) |
| 3491 | // 已终态且全绿,按它生成的种子才不会撞结构。allowContinue:false——e2e 基线(globalSetup 注入)与行为门 | 3614 | // 已终态且全绿,按它生成的种子才不会撞结构。allowContinue:false——e2e 基线(globalSetup 注入)与行为门 |
| 3492 | // step2 子项③(演示种子注入)都依赖该种子,坏种子放行会让整个前端阶段(行为验收/e2e)在脏数据上全线误判。 | 3615 | // step2 子项③(演示种子注入)都依赖该种子,坏种子放行会让整个前端阶段(行为验收/e2e)在脏数据上全线误判。 |
| @@ -3502,6 +3625,7 @@ async function runModule(module, c) { | @@ -3502,6 +3625,7 @@ async function runModule(module, c) { | ||
| 3502 | // 截图归档为可视化基线 + 跑原型自带交互点击冒烟。与行为门(测实现)正交;advisory(原型属上游 | 3625 | // 截图归档为可视化基线 + 跑原型自带交互点击冒烟。与行为门(测实现)正交;advisory(原型属上游 |
| 3503 | // plan 权威,coding 不改原型源码,渲染/JS 问题只记证据不阻断)。静态渲染、不起全栈。终波主根执行。 | 3626 | // plan 权威,coding 不改原型源码,渲染/JS 问题只记证据不阻断)。静态渲染、不起全栈。终波主根执行。 |
| 3504 | if (c.lane == null) phase('Preview') | 3627 | if (c.lane == null) phase('Preview') |
| 3628 | + log(`eta 前端段: Preview ${fmtMin(STAGE_MIN.preview[1])} + 骨架 ${fmtMin(STAGE_MIN.skeleton[1])} + ${module.feItems.length} 个 FE × ${fmtMin(itemMin('frontend'))} + Behavior ${fmtMin(STAGE_MIN.behavior[1])} + Gate ${fmtMin(STAGE_MIN.gate[1])} + 报告 ${fmtMin(STAGE_MIN.report[1])} ≈ ${fmtMin(moduleMin(module))}(区间 ${fmtMin(moduleMin(module, 0))}–${fmtMin(moduleMin(module, 2))})`) | ||
| 3505 | await runPrototypePreview(module.feItems, c) | 3629 | await runPrototypePreview(module.feItems, c) |
| 3506 | if (c.lane == null) phase('Frontend') | 3630 | if (c.lane == null) phase('Frontend') |
| 3507 | // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy | 3631 | // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy |
| @@ -3518,15 +3642,16 @@ async function runModule(module, c) { | @@ -3518,15 +3642,16 @@ async function runModule(module, c) { | ||
| 3518 | if (c.lane == null) phase('Behavior') | 3642 | if (c.lane == null) phase('Behavior') |
| 3519 | // 行为门虽也冷起全栈,但只在 frontend-phase 终波(宽 1、主根)跑——无兄弟 lane 可互杀, | 3643 | // 行为门虽也冷起全栈,但只在 frontend-phase 终波(宽 1、主根)跑——无兄弟 lane 可互杀, |
| 3520 | // 不进起栈互斥(补 9 范围仅 Seed / testGate)。 | 3644 | // 不进起栈互斥(补 9 范围仅 Seed / testGate)。 |
| 3521 | - await runBehaviorGate(module.feItems, c) | 3645 | + reportFacts.behavior = await runBehaviorGate(module.feItems, c) |
| 3522 | if (c.lane == null) phase('Gate') | 3646 | if (c.lane == null) phase('Gate') |
| 3523 | // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交;起栈互斥(补 9)同后端段整段包锁。 | 3647 | // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交;起栈互斥(补 9)同后端段整段包锁。 |
| 3524 | - await withStackLock(() => testGate(module, 'frontend', c)) | 3648 | + reportFacts.gate = await withStackLock(() => testGate(module, 'frontend', c)) |
| 3525 | } | 3649 | } |
| 3526 | if (c.lane == null) phase('Milestone') | 3650 | if (c.lane == null) phase('Milestone') |
| 3527 | - // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— | ||
| 3528 | - // 绝不 continue 放行去打 milestone(否则可能在红色测试上 milestone)。 | ||
| 3529 | - await runStage(g => reportPrompt(module, c) + g, { | 3651 | + // report allowContinue:false:报告产物是 milestone 的前置(runMilestone 校验其 § ⑫),绝不 continue |
| 3652 | + // 放行去打 milestone。注意"最后一次 test-gate 必须 green"已不再由报告子代理自证——红态在 | ||
| 3653 | + // testGate / runBehaviorGate 内就 halt 了,根本走不到这里;绿态事实经 reportFacts 直接给定。 | ||
| 3654 | + await runStage(g => reportPrompt(module, c, reportFacts) + g, { | ||
| 3530 | site:`report:${module.id}`, grp:'Milestone', label:`report:${module.id}`, allowContinue: false, dec: c.decisions, root: c.root, | 3655 | site:`report:${module.id}`, grp:'Milestone', label:`report:${module.id}`, allowContinue: false, dec: c.decisions, root: c.root, |
| 3531 | }) | 3656 | }) |
| 3532 | // lane 收口(附录补 15):lane 模式 milestone 前先在 lane 树上 wt-clean + 自主恢复——lane 里 tdd/fix | 3657 | // lane 收口(附录补 15):lane 模式 milestone 前先在 lane 树上 wt-clean + 自主恢复——lane 里 tdd/fix |