Commit 094033f2537e729b10304bfe057e8133678af3e4
1 parent
7a0069c5
coding.mjs: 前端 jsdom-only 重叠实现(开关 frontendOverlap,缺省关)
让前端不依赖后端的工作与后端波次并发,所有真后端测试延后。5 类改动: - tddPrompt/verifyPrompt 加 feStage:jsdom 模式只处理 jsdom 任务、只跑 vitest、 硬禁 e2e 命令;e2e 模式只补延后的 e2e 任务。新增 fe-code-done/<FE> 中间 tag。 - featureLoop 加 feStage 分流:jsdom 跳 review 打 fe-code-done;e2e/all 走 review→req-done。新增 runFrontendPhase1(前端 worktree,不调 phase()、不 merge/milestone、永不 throw)。 - 主循环:frontendOverlap 开时把 frontend-phase 从波次图剔除、起 phase1 与后端 波次并发;屏障后 reconcile(merge default→frontend-phase)+ 标准 runModule(feStage=e2e) 主根全栈跑 phase2。后端未全 done / phase1 halt → phase2 延后、产物保留 resume,绝不新增 halt 点。 - coding-start SKILL.md + README:开关说明,缺省不传(同 features 闸纪律)。 重叠期前端无栈/无固定端口/无 DB,补 8/9/10 不变量与端口/lane 安全全保住。 缺省关时 feStage 恒 all、新分支不激活、新编排整段跳过,现行行为零变化。 语法门通过。设计真值见 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing
3 changed files
with
181 additions
and
12 deletions
README.md
| @@ -67,6 +67,13 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 | @@ -67,6 +67,13 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 | ||
| 67 | → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归) | 67 | → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归) |
| 68 | → runMilestone(milestone/frontend-phase) | 68 | → runMilestone(milestone/frontend-phase) |
| 69 | 69 | ||
| 70 | + · 前端 jsdom-only 重叠(可选,parallel.frontendOverlap,缺省关):前端**不依赖后端**的部分 | ||
| 71 | + (骨架 + spec/plan + tdd 的 jsdom 任务 + vitest verify)在独立前端 worktree 与后端波次**并发**, | ||
| 72 | + 打中间 tag fe-code-done/<FE>;后端全部 done 后 reconcile(合 default→frontend-phase)+ phase2 | ||
| 73 | + (e2e 任务 + review + req-done + 行为门 + 测试闸 + 里程碑,主根全栈)。重叠期前端无栈/无固定 | ||
| 74 | + 端口/无 DB,端口与 lane 不变量全保住;缺省关 / phase1 失败 / 后端未全 done 回退现行终波路径。 | ||
| 75 | + 设计:docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md | ||
| 76 | + | ||
| 70 | 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start | 77 | 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start |
| 71 | 78 | ||
| 72 | 续跑 handoff:主循环结束(halt 或全完成)时 best-effort 追加 docs/superpowers/RESUME.md | 79 | 续跑 handoff:主循环结束(halt 或全完成)时 best-effort 追加 docs/superpowers/RESUME.md |
skills/coding/coding-start/SKILL.md
| @@ -106,6 +106,8 @@ Workflow({ | @@ -106,6 +106,8 @@ Workflow({ | ||
| 106 | > `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。 | 106 | > `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。 |
| 107 | > | 107 | > |
| 108 | > `parallel` 是模块级并行总开关:`modules: true` 时 coding.mjs 先跑调度器(LLM 只输出模块间依赖边,JS 做确定性拓扑波次),同波次 ≥2 个后端模块各自在 lane worktree + lane 测试库 + 专属 migration 版本段内并行推进;任何调度/隔离环节失败一律 fail-open 降级串行,绝不因并行机制 halt。**逃生口**:改传 `parallel: { modules: false }`(或整个不传 `parallel`)即完全回退现行串行路径。`features`(feature 级并行)**刻意不传**——维持缺省关闭,待并行化计划 Task 14 收益评估通过后才在此处开启(附录补 1)。 | 108 | > `parallel` 是模块级并行总开关:`modules: true` 时 coding.mjs 先跑调度器(LLM 只输出模块间依赖边,JS 做确定性拓扑波次),同波次 ≥2 个后端模块各自在 lane worktree + lane 测试库 + 专属 migration 版本段内并行推进;任何调度/隔离环节失败一律 fail-open 降级串行,绝不因并行机制 halt。**逃生口**:改传 `parallel: { modules: false }`(或整个不传 `parallel`)即完全回退现行串行路径。`features`(feature 级并行)**刻意不传**——维持缺省关闭,待并行化计划 Task 14 收益评估通过后才在此处开启(附录补 1)。 |
| 109 | +> | ||
| 110 | +> `frontendOverlap`(前端 jsdom-only 重叠)**同样刻意不传**——维持缺省关闭。开启(`parallel: { modules: true, maxWidth: 3, frontendOverlap: true }`)时,前端**不依赖后端**的部分(骨架 + spec/plan + tdd 的 jsdom 任务 + vitest verify)在独立前端 worktree 与后端波次**并发**跑,打中间 tag `fe-code-done/<FE>`;后端全部 done 后才 reconcile(合 default→frontend-phase)并跑 phase2(e2e 任务 + review + req-done + 行为门 + 测试闸 + 里程碑,主根全栈)。重叠期前端无栈/无固定端口/无 DB,补 8/9/10 不变量与端口/lane 安全全部保住。**风险**:前端对 docs/05 契约写代码,后端编码期若偏离契约,错误拖到 phase2 e2e/行为门才暴露、需返工——故列为显式评估开关,收益/正确性评估通过后才在此处开启(设计:`docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md`)。缺省关 / phase1 失败 / 后端未全 done 一律回退现行终波路径,绝不新增 halt 点。 | ||
| 109 | 111 | ||
| 110 | ### 步骤 5:告知用户 Workflow 已启动 | 112 | ### 步骤 5:告知用户 Workflow 已启动 |
| 111 | 113 |
workflows/coding.mjs
| @@ -512,7 +512,7 @@ function planPrompt(id, phase, specPath, c) { | @@ -512,7 +512,7 @@ function planPrompt(id, phase, specPath, c) { | ||
| 512 | 512 | ||
| 513 | // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)---- | 513 | // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)---- |
| 514 | // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。 | 514 | // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。 |
| 515 | -function tddPrompt(id, phase, planPath, c) { | 515 | +function tddPrompt(id, phase, planPath, c, feStage = 'all') { |
| 516 | const fe = isFrontend(phase) | 516 | const fe = isFrontend(phase) |
| 517 | return [ | 517 | return [ |
| 518 | `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`, | 518 | `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`, |
| @@ -543,6 +543,14 @@ function tddPrompt(id, phase, planPath, c) { | @@ -543,6 +543,14 @@ function tddPrompt(id, phase, planPath, c) { | ||
| 543 | // schema 时 ERP_TEST_DB_SCHEMA 传进去无人消费(脚本支持 ≠ 测试进程支持)。占位 env 缺省回退字面量, | 543 | // schema 时 ERP_TEST_DB_SCHEMA 传进去无人消费(脚本支持 ≠ 测试进程支持)。占位 env 缺省回退字面量, |
| 544 | // 串行行为一字不变;gradle test fork 与 bootRun 默认继承 env,无需 SPRING_DATASOURCE_URL 拼装。 | 544 | // 串行行为一字不变;gradle test fork 与 bootRun 默认继承 env,无需 SPRING_DATASOURCE_URL 拼装。 |
| 545 | fe ? '' : '- **数据源 schema 占位约定**:生成 / 修改后端 `application*.yml`(含测试 profile)时,JDBC URL 的 schema 段必须写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:<db.schema 字面量>}`(env 缺省回退原 schema,行为不变);**绝不**把 schema 名写死成纯字面量。', | 545 | fe ? '' : '- **数据源 schema 占位约定**:生成 / 修改后端 `application*.yml`(含测试 profile)时,JDBC URL 的 schema 段必须写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:<db.schema 字面量>}`(env 缺省回退原 schema,行为不变);**绝不**把 schema 名写死成纯字面量。', |
| 546 | + // feStage 任务子集(前端 jsdom-only 重叠;设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)。 | ||
| 547 | + // 'all'(缺省,含全部后端)与现行一字不差;'jsdom'/'e2e' 仅前端 phase1/phase2 传入。 | ||
| 548 | + fe && feStage === 'jsdom' | ||
| 549 | + ? '- **本阶段 = 仅 jsdom 任务(重叠 phase1,不依赖后端)**:只处理 plan 中 `测试先行类型 = jsdom` 的代码任务;`= e2e` 的任务**全部跳过**,并在 `summary` 里以 `deferredE2e: [<task...>]` 列出(留待 phase2)。测试命令**只用** vitest(`frontend.test_command` / `frontend.unit_test_runner`)。**硬护栏**:本阶段**绝不**运行 Playwright / `pnpm e2e:ci` / `frontend.e2e_command` / 任何冷起全栈或固定端口的 e2e 命令——命中即 `{status:"halt", reason:"jsdom 模式禁跑 e2e(重叠 phase1 后端尚未就绪)"}`。FeStub→真组件替换(见下)**在本阶段完成**。' | ||
| 550 | + : '', | ||
| 551 | + fe && feStage === 'e2e' | ||
| 552 | + ? '- **本阶段 = 仅 e2e 任务(phase2,后端已就绪;jsdom 已在 phase1 绿)**:只处理 plan 中 `测试先行类型 = e2e` 的任务;jsdom 任务**跳过**(phase1 已完成实现与单测)。e2e 基线约束见下;FeStub→真组件替换 phase1 已做,本阶段确认 router 仍指向真组件即可。' | ||
| 553 | + : '', | ||
| 546 | '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。', | 554 | '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。', |
| 547 | fe | 555 | fe |
| 548 | ? '- **测试落位(硬约定,对齐 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 复刻。' | 556 | ? '- **测试落位(硬约定,对齐 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 复刻。' |
| @@ -575,7 +583,7 @@ function tddPrompt(id, phase, planPath, c) { | @@ -575,7 +583,7 @@ function tddPrompt(id, phase, planPath, c) { | ||
| 575 | // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)---- | 583 | // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)---- |
| 576 | // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件, | 584 | // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件, |
| 577 | // 避免 reverify 覆盖前轮证据)。 | 585 | // 避免 reverify 覆盖前轮证据)。 |
| 578 | -function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { | 586 | +function verifyPrompt(id, phase, implSummary, specPath, round = 0, c, feStage = 'all') { |
| 579 | const fe = isFrontend(phase) | 587 | const fe = isFrontend(phase) |
| 580 | const suffix = round === 0 ? 'verify' : `verify-r${round}` | 588 | const suffix = round === 0 ? 'verify' : `verify-r${round}` |
| 581 | return [ | 589 | return [ |
| @@ -589,7 +597,14 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { | @@ -589,7 +597,14 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { | ||
| 589 | implSummary ? `- 上游 TDD 摘要:${implSummary}` : '', | 597 | implSummary ? `- 上游 TDD 摘要:${implSummary}` : '', |
| 590 | '', | 598 | '', |
| 591 | '## 流程', | 599 | '## 流程', |
| 592 | - fe | 600 | + fe && feStage === 'jsdom' |
| 601 | + ? [ | ||
| 602 | + // 重叠 phase1:只验 jsdom 单测(不依赖后端),e2e 留 phase2。 | ||
| 603 | + `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式。命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.unit_test_runner\` 取(缺失默认 \`pnpm test:ci\`)。`, | ||
| 604 | + '- 派子会话**只跑 unit(vitest)**,返回 `{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt} }`(`stdout_excerpt` ≤ 30 行)。**硬护栏**:本阶段绝不跑 e2e / Playwright / `pnpm e2e:ci`(后端尚未就绪)。', | ||
| 605 | + '- **unit `exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt。', | ||
| 606 | + ].join('\n') | ||
| 607 | + : fe | ||
| 593 | ? [ | 608 | ? [ |
| 594 | `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 取(缺失默认 \`pnpm test:ci\` / \`pnpm e2e:ci\`)。`, | 609 | `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 取(缺失默认 \`pnpm test:ci\` / \`pnpm e2e:ci\`)。`, |
| 595 | '- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。', | 610 | '- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。', |
| @@ -1580,6 +1595,34 @@ function createReqDoneTagPromptM(id, phase, c) { | @@ -1580,6 +1595,34 @@ function createReqDoneTagPromptM(id, phase, c) { | ||
| 1580 | ].join('\n') | 1595 | ].join('\n') |
| 1581 | } | 1596 | } |
| 1582 | 1597 | ||
| 1598 | +// ── 微步骤:前端 jsdom-only 重叠的中间完成态 tag `fe-code-done/<FE>`(设计见 | ||
| 1599 | +// docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)── | ||
| 1600 | +// 语义:phase1 完成(组件代码 + jsdom 绿,e2e/review/req-done 全部延后)。与 req-done 正交、双 tag 续跑链: | ||
| 1601 | +// fe-code-done 存在 → phase1 跳过该 FE;req-done 存在 → phase2 跳过;两者皆缺 → phase1 重跑该 FE。 | ||
| 1602 | +// 仅在 frontendOverlap 开启时产生;关闭时全程只用 req-done,与现行一字不差。 | ||
| 1603 | +function checkFeCodeDoneTagPromptM(id, c) { | ||
| 1604 | + return [ | ||
| 1605 | + `# tag \`fe-code-done/${id}\` 是否存在(前端 phase1 完成态 dedup 真值)`, | ||
| 1606 | + microStepContract(c.root), | ||
| 1607 | + '', | ||
| 1608 | + `跑 \`git -C ${c.root} tag -l fe-code-done/${id}\`。`, | ||
| 1609 | + '## 输出(EXISTS_SCHEMA)', | ||
| 1610 | + '- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`', | ||
| 1611 | + ].join('\n') | ||
| 1612 | +} | ||
| 1613 | + | ||
| 1614 | +function createFeCodeDoneTagPromptM(id, c) { | ||
| 1615 | + return [ | ||
| 1616 | + `# 打 annotated tag \`fe-code-done/${id}\`(前端 phase1:组件代码 + jsdom 绿,e2e/review 延后)`, | ||
| 1617 | + microStepContract(c.root), | ||
| 1618 | + '', | ||
| 1619 | + `跑 \`git -C ${c.root} tag -a fe-code-done/${id} -m "fe-code(${id}): jsdom green, e2e/review deferred (overlap phase1)"\`。`, | ||
| 1620 | + `先用 \`git -C ${c.root} tag -l fe-code-done/${id}\` 检查;已存在则视为成功(幂等)直接返回 success。`, | ||
| 1621 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1622 | + '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`', | ||
| 1623 | + ].join('\n') | ||
| 1624 | +} | ||
| 1625 | + | ||
| 1583 | // ── 微步骤:批量预生成 spec 的统一提交(Phase E Task 11)── | 1626 | // ── 微步骤:批量预生成 spec 的统一提交(Phase E Task 11)── |
| 1584 | // batch 模式 spec 只写盘不 commit(避免同一工作树并行 commit 争 .git/index.lock);全部预生成 | 1627 | // batch 模式 spec 只写盘不 commit(避免同一工作树并行 commit 争 .git/index.lock);全部预生成 |
| 1585 | // 落盘后由本微步骤一次性 add+commit 收口。幂等:resume 场景 spec 可能是"复用已提交文件、就地 | 1628 | // 落盘后由本微步骤一次性 add+commit 收口。幂等:resume 场景 spec 可能是"复用已提交文件、就地 |
| @@ -1693,6 +1736,24 @@ function executeMergePromptM(defaultBranch, branch, phaseId) { | @@ -1693,6 +1736,24 @@ function executeMergePromptM(defaultBranch, branch, phaseId) { | ||
| 1693 | ].join('\n') | 1736 | ].join('\n') |
| 1694 | } | 1737 | } |
| 1695 | 1738 | ||
| 1739 | +// reconcile(前端 jsdom-only 重叠 phase1→phase2 边界,设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md): | ||
| 1740 | +// phase1 的 frontend-phase 分支自**重叠开始时**的 default 分叉,不含后端代码;phase2 的 e2e 要打真后端, | ||
| 1741 | +// 故先把已完成的 default(含全部后端 milestone)合并进 frontend-phase。路径互斥(frontend/ vs backend/) | ||
| 1742 | +// → 冲突概率极低;冲突按 milestone merge 同口径「留树 halt 给人工」,绝不 abort。幂等:default 已是 | ||
| 1743 | +// frontend-phase 祖先(无重叠 / resume 已 merge)→ git "Already up to date" → success。主根专属。 | ||
| 1744 | +function mergeDefaultIntoFrontendPromptM(branch, defaultBranch) { | ||
| 1745 | + return [ | ||
| 1746 | + `# reconcile:把 \`${defaultBranch}\`(已完成后端)合并进 \`${branch}\``, | ||
| 1747 | + microStepContract(ROOT), | ||
| 1748 | + '', | ||
| 1749 | + `1. 跑 \`git -C ${ROOT} checkout ${branch}\` 确保 HEAD 在 ${branch}(phase1 worktree 已移除,分支空闲;若仍被占用则整步失败留人工)。`, | ||
| 1750 | + `2. 跑 \`git -C ${ROOT} merge --no-ff ${defaultBranch} -m "merge(reconcile): integrate backend into ${branch} for e2e"\`。`, | ||
| 1751 | + '## 输出(ACTION_RESULT_SCHEMA)', | ||
| 1752 | + '- 成功 / 已是最新(Already up to date)→ `{ "success": true }`', | ||
| 1753 | + '- 合并冲突 / 其它失败 → `{ "success": false, "error": "<simplified>", "detail": "<冲突文件换行分隔 或 stderr 前 30 行>" }`;**不要**自动 abort / stash / 改文件。', | ||
| 1754 | + ].join('\n') | ||
| 1755 | +} | ||
| 1756 | + | ||
| 1696 | function readDocs08FieldPromptM(fe, id) { | 1757 | function readDocs08FieldPromptM(fe, id) { |
| 1697 | const section = fe ? '§ 三' : '§ 二' | 1758 | const section = fe ? '§ 三' : '§ 二' |
| 1698 | const title = fe | 1759 | const title = fe |
| @@ -2190,8 +2251,13 @@ const useSpecPrefetch = ARGS?.parallel?.features === true || ARGS?.parallel?.mod | @@ -2190,8 +2251,13 @@ const useSpecPrefetch = ARGS?.parallel?.features === true || ARGS?.parallel?.mod | ||
| 2190 | // 冲突风险、两开关任一开启即用;feature 级 tdd/fix 链并行只看本闸。 | 2251 | // 冲突风险、两开关任一开启即用;feature 级 tdd/fix 链并行只看本闸。 |
| 2191 | const useFeatureParallel = ARGS?.parallel?.features === true | 2252 | const useFeatureParallel = ARGS?.parallel?.features === true |
| 2192 | const featureMaxWidth = Math.max(1, ARGS?.parallel?.featureMaxWidth ?? 2) | 2253 | const featureMaxWidth = Math.max(1, ARGS?.parallel?.featureMaxWidth ?? 2) |
| 2193 | -async function featureLoop(items, phase, c) { | 2254 | +// feStage(前端 jsdom-only 重叠;设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md): |
| 2255 | +// 'all'(缺省)= 现行完整链(spec→plan→tdd→verify→review→req-done),后端恒走此;前端 overlap 关时亦走此。 | ||
| 2256 | +// 'jsdom'(overlap phase1)= spec→plan→tdd(仅 jsdom 任务)→verify(仅 vitest)→tag fe-code-done;**不** review、**不** req-done。 | ||
| 2257 | +// 'e2e'(phase2,后端已就绪)= dedup 读 fe-code-done → spec/plan(复用)→tdd(仅 e2e 任务)→verify(unit+e2e)→review→req-done。 | ||
| 2258 | +async function featureLoop(items, phase, c, feStage = 'all') { | ||
| 2194 | const grp = phase === 'backend' ? 'Backend' : 'Frontend' | 2259 | const grp = phase === 'backend' ? 'Backend' : 'Frontend' |
| 2260 | + const isPhase1 = feStage === 'jsdom' // 只推进到 fe-code-done(中间态) | ||
| 2195 | // spec 的 artifactPath 校验(主链与批量预生成共用同一份,确保两条路径产物口径一致)。 | 2261 | // spec 的 artifactPath 校验(主链与批量预生成共用同一份,确保两条路径产物口径一致)。 |
| 2196 | const specValidate = r => { | 2262 | const specValidate = r => { |
| 2197 | if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' | 2263 | if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' |
| @@ -2285,17 +2351,20 @@ async function featureLoop(items, phase, c) { | @@ -2285,17 +2351,20 @@ async function featureLoop(items, phase, c) { | ||
| 2285 | } | 2351 | } |
| 2286 | // fc:执行上下文(串行 = 模块 ctx c;Phase F 并行批 = feature lane ctx)。rwOpts 透传 | 2352 | // fc:执行上下文(串行 = 模块 ctx c;Phase F 并行批 = feature lane ctx)。rwOpts 透传 |
| 2287 | // reviewWithFixLoop(feature lane 内跳过 docs/08 勾选,理由见该函数头注释)。 | 2353 | // reviewWithFixLoop(feature lane 内跳过 docs/08 勾选,理由见该函数头注释)。 |
| 2288 | - const runImplChain = async (id, planPath, specPath, fc, rwOpts) => { | 2354 | + // stage(末位,缺省 'all'=现行):透传 tddPrompt/verifyPrompt 的 feStage;'jsdom' 额外跳过 review(phase1 |
| 2355 | + // 只到 fe-code-done,review→req-done 延到 phase2)。'e2e'/'all' 含 review,一字不差。 | ||
| 2356 | + const runImplChain = async (id, planPath, specPath, fc, rwOpts, stage = 'all') => { | ||
| 2289 | // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, | 2357 | // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, |
| 2290 | // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 | 2358 | // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 |
| 2291 | - const impl = await runStage(g => tddPrompt(id, phase, planPath, fc) + g, { | 2359 | + const impl = await runStage(g => tddPrompt(id, phase, planPath, fc, stage) + g, { |
| 2292 | site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, | 2360 | site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, |
| 2293 | }) | 2361 | }) |
| 2294 | // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, | 2362 | // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, |
| 2295 | // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 | 2363 | // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 |
| 2296 | - const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', specPath, 0, fc) + g, { | 2364 | + const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', specPath, 0, fc, stage) + g, { |
| 2297 | site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, | 2365 | site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, |
| 2298 | }) | 2366 | }) |
| 2367 | + if (stage === 'jsdom') return // overlap phase1:jsdom 绿即止,不 review(review→req-done 留 phase2) | ||
| 2299 | const reviewResult = await reviewWithFixLoop(id, phase, v0, specPath, fc, rwOpts) | 2368 | const reviewResult = await reviewWithFixLoop(id, phase, v0, specPath, fc, rwOpts) |
| 2300 | log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) | 2369 | log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) |
| 2301 | } | 2370 | } |
| @@ -2304,6 +2373,15 @@ async function featureLoop(items, phase, c) { | @@ -2304,6 +2373,15 @@ async function featureLoop(items, phase, c) { | ||
| 2304 | const tagDone = async (id) => runAction(g => createReqDoneTagPromptM(id, phase, c) + g, { | 2373 | const tagDone = async (id) => runAction(g => createReqDoneTagPromptM(id, phase, c) + g, { |
| 2305 | site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root, | 2374 | site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root, |
| 2306 | }) | 2375 | }) |
| 2376 | + // overlap phase1 中间完成态 tag:fe-code-done/<id>(组件代码 + jsdom 绿)。 | ||
| 2377 | + const tagFeCodeDone = async (id) => runAction(g => createFeCodeDoneTagPromptM(id, c) + g, { | ||
| 2378 | + site:`fe-code-done-tag:${id}`, grp, label:`fecodedone:${id}`, dec: c.decisions, root: c.root, | ||
| 2379 | + }) | ||
| 2380 | + // phase1 入口 dedup:fe-code-done/<id> 已存在 → 本 FE phase1 已完成,跳过(phase2 用 req-done dedup,走 isDone)。 | ||
| 2381 | + const isPhase1Done = async (id) => { | ||
| 2382 | + const r = await agentR(checkFeCodeDoneTagPromptM(id, c), {label:`fecodecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) | ||
| 2383 | + return r.exists | ||
| 2384 | + } | ||
| 2307 | 2385 | ||
| 2308 | // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。 | 2386 | // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。 |
| 2309 | // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。 | 2387 | // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。 |
| @@ -2313,11 +2391,15 @@ async function featureLoop(items, phase, c) { | @@ -2313,11 +2391,15 @@ async function featureLoop(items, phase, c) { | ||
| 2313 | } | 2391 | } |
| 2314 | 2392 | ||
| 2315 | for (const id of items) { | 2393 | for (const id of items) { |
| 2316 | - if (await isDone(id)) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } | 2394 | + // dedup:phase1 用 fe-code-done;'all'/'e2e'(phase2)用 req-done。phase2 的 fe-code-done 必已存在 |
| 2395 | + // (phase1 落过),spec/plan 经 ensureSpec/runPlanStage 复用既有产物,tdd(e2e) 只处理延后的 e2e 任务。 | ||
| 2396 | + if (isPhase1 ? await isPhase1Done(id) : await isDone(id)) { | ||
| 2397 | + log(`featureLoop skip ${phase}:${id} — tag ${isPhase1 ? 'fe-code-done' : 'req-done'}/${id} 已存在`); continue | ||
| 2398 | + } | ||
| 2317 | const spec = await ensureSpec(id) | 2399 | const spec = await ensureSpec(id) |
| 2318 | const plan = await runPlanStage(id, spec) | 2400 | const plan = await runPlanStage(id, spec) |
| 2319 | - await runImplChain(id, plan.artifactPath, spec.artifactPath, c) | ||
| 2320 | - await tagDone(id) | 2401 | + await runImplChain(id, plan.artifactPath, spec.artifactPath, c, undefined, feStage) |
| 2402 | + await (isPhase1 ? tagFeCodeDone(id) : tagDone(id)) | ||
| 2321 | } | 2403 | } |
| 2322 | } | 2404 | } |
| 2323 | 2405 | ||
| @@ -3054,6 +3136,28 @@ const withStackLock = makeChainLock() | @@ -3054,6 +3136,28 @@ const withStackLock = makeChainLock() | ||
| 3054 | // phase(...) 全局 UI 分组只在主根模式调用(c.lane == null):lane 并行下兄弟模块会竞态改全局 phase 状态, | 3136 | // phase(...) 全局 UI 分组只在主根模式调用(c.lane == null):lane 并行下兄弟模块会竞态改全局 phase 状态, |
| 3055 | // 而 agent 级 opts.phase(grp)已全覆盖分组——注意不能写 `!c.lane`(lane 序号 0 是合法 lane,falsy 会误判主根)。 | 3137 | // 而 agent 级 opts.phase(grp)已全覆盖分组——注意不能写 `!c.lane`(lane 序号 0 是合法 lane,falsy 会误判主根)。 |
| 3056 | const resumeFlushed = new Set() // per-module RESUME flush 成功记账(替代 flushedCount 水位线;loop-end 据此补记失败模块) | 3138 | const resumeFlushed = new Set() // per-module RESUME flush 成功记账(替代 flushedCount 水位线;loop-end 据此补记失败模块) |
| 3139 | + | ||
| 3140 | +// ---- runFrontendPhase1:前端 jsdom-only 重叠的 phase1(设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)---- | ||
| 3141 | +// 与后端波次循环**并发**跑,**只做不依赖后端**的事:branchSetup(建前端 worktree)→ skeleton → featureLoop(jsdom)。 | ||
| 3142 | +// 跑在**独立前端 worktree**(c.lane 非 null、root=前端 lane 路径、dbSchema=''——jsdom 无 DB,featureStageContract | ||
| 3143 | +// 的 lane-DB 注入按 c.dbSchema 非空判定,此处不触发),与后端主根/lane 的 git index 互不干扰;**不**调全局 | ||
| 3144 | +// phase()(并发下会与后端波次竞态改全局 UI 状态——agent 级 grp='Frontend' 已覆盖分组),**不** merge/milestone/ | ||
| 3145 | +// RESUME(全部留 phase2)。**永不 throw**:catch 结构化返回,phase1 halt 不停后端(reason 带前端 worktree 取证前缀)。 | ||
| 3146 | +// 幂等(resume):skeleton 凭 fe-skeleton-done、各 FE 凭 fe-code-done 去重,已完成项整段 skip。 | ||
| 3147 | +async function runFrontendPhase1(feModule, c) { | ||
| 3148 | + try { | ||
| 3149 | + await runBranchSetup(feModule, c) | ||
| 3150 | + await runFrontendSkeleton(feModule.feItems, c) | ||
| 3151 | + await featureLoop(feModule.feItems, 'frontend', c, 'jsdom') | ||
| 3152 | + log(`fe-overlap phase1 完成:${feModule.feItems.length} 个 FE 的组件代码 + jsdom 已绿(e2e/review 留 phase2)`) | ||
| 3153 | + return { module: 'frontend-phase', status: 'done', decisions: c.decisions } | ||
| 3154 | + } catch (e) { | ||
| 3155 | + const reason = `[fe-overlap phase1 保留取证: ${c.root}] ${String(e?.message || e)}` | ||
| 3156 | + log(`⛔ HALT — fe-overlap phase1:${reason}`) | ||
| 3157 | + return { module: 'frontend-phase', status: 'halted', reason, decisions: c.decisions } | ||
| 3158 | + } | ||
| 3159 | +} | ||
| 3160 | + | ||
| 3057 | async function runModule(module, c) { | 3161 | async function runModule(module, c) { |
| 3058 | try { | 3162 | try { |
| 3059 | if (c.lane == null) phase('Milestone') | 3163 | if (c.lane == null) phase('Milestone') |
| @@ -3082,7 +3186,9 @@ async function runModule(module, c) { | @@ -3082,7 +3186,9 @@ async function runModule(module, c) { | ||
| 3082 | // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 | 3186 | // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 |
| 3083 | await runFrontendSkeleton(module.feItems, c) | 3187 | await runFrontendSkeleton(module.feItems, c) |
| 3084 | // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。 | 3188 | // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。 |
| 3085 | - await featureLoop(module.feItems, 'frontend', c) | 3189 | + // module.feStage(前端 jsdom-only 重叠):缺省 'all'=现行完整链;overlap phase2 传 'e2e'(phase1 已在 |
| 3190 | + // 前端 worktree 跑完 jsdom,本处只补 e2e 任务 + review + req-done)。skeleton 上方已幂等(fe-skeleton-done)。 | ||
| 3191 | + await featureLoop(module.feItems, 'frontend', c, module.feStage || 'all') | ||
| 3086 | // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 | 3192 | // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 |
| 3087 | // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 | 3193 | // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 |
| 3088 | // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 | 3194 | // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 |
| @@ -3288,6 +3394,24 @@ const results = [] | @@ -3288,6 +3394,24 @@ const results = [] | ||
| 3288 | const doneSet = new Set(routed.modules.filter(m => m.done).map(m => m.id)) | 3394 | const doneSet = new Set(routed.modules.filter(m => m.done).map(m => m.id)) |
| 3289 | let remaining = useParallel ? await runScheduler(todo, routed.modules) : serialChainDeps(todo) | 3395 | let remaining = useParallel ? await runScheduler(todo, routed.modules) : serialChainDeps(todo) |
| 3290 | 3396 | ||
| 3397 | +// ── 前端 jsdom-only 重叠(设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)── | ||
| 3398 | +// frontendOverlap 开 + 存在待跑 frontend-phase(有 feItems)时:把 frontend-phase 从波次图剔除,起 phase1 | ||
| 3399 | +// (前端 worktree,只跑 skeleton + jsdom,不依赖后端)与后端波次循环**并发**;屏障(后端全部 done)后再 | ||
| 3400 | +// reconcile(合 default→frontend-phase)+ 标准 runModule(feStage='e2e')(主根全栈,保住补 8/9/10)。缺省关 / | ||
| 3401 | +// phase1 失败 / 后端未全 done → 完全回退现行终波路径,phase1 产物(fe-code-done)落 tag、resume 续跑,绝不新增 halt 点。 | ||
| 3402 | +const useFrontendOverlap = ARGS?.parallel?.frontendOverlap === true | ||
| 3403 | +const feModule = routed.modules.find(m => m.id === 'frontend-phase' && !m.done && (m.feItems || []).length) | ||
| 3404 | +const overlapOn = useFrontendOverlap && !!feModule | ||
| 3405 | +const feDecisions = [] // phase1/phase2 共享决策收集器(RESUME flush 在 phase2 milestone 覆盖两段) | ||
| 3406 | +const fePending = [] // 后端未全 done 时,frontend-phase 的 pending 记账(并入下方 pending) | ||
| 3407 | +let fe1Promise = null | ||
| 3408 | +if (overlapOn) { | ||
| 3409 | + remaining = remaining.filter(m => m.id !== 'frontend-phase') // 从波次图剔除,后端波次独立推进 | ||
| 3410 | + const feCtx = makeCtx({ lane: 'fe', root: laneRoot('frontend-phase'), dbSchema: '', vBase: 0, decisions: feDecisions }) | ||
| 3411 | + fe1Promise = runFrontendPhase1(feModule, feCtx) // 不 await:与下方后端波次循环并发 | ||
| 3412 | + log(`fe-overlap: 前端 phase1 已与后端波次并发启动(worktree ${laneRoot('frontend-phase')})`) | ||
| 3413 | +} | ||
| 3414 | + | ||
| 3291 | while (remaining.length) { | 3415 | while (remaining.length) { |
| 3292 | let wave = nextWave(remaining, doneSet) | 3416 | let wave = nextWave(remaining, doneSet) |
| 3293 | if (!useParallel) wave = wave.slice(0, 1) // 防御:串行闸下恒单宽(链式图本就每轮恰出 1 个) | 3417 | if (!useParallel) wave = wave.slice(0, 1) // 防御:串行闸下恒单宽(链式图本就每轮恰出 1 个) |
| @@ -3346,13 +3470,49 @@ while (remaining.length) { | @@ -3346,13 +3470,49 @@ while (remaining.length) { | ||
| 3346 | if (results.some(r => r.status === 'halted')) break | 3470 | if (results.some(r => r.status === 'halted')) break |
| 3347 | } | 3471 | } |
| 3348 | 3472 | ||
| 3473 | +// ── 前端 jsdom-only 重叠:屏障 + phase2 ──(overlap 关时整段跳过,现行终波路径不变) | ||
| 3474 | +if (overlapOn) { | ||
| 3475 | + const fe1 = await fe1Promise // 屏障:等前端 phase1(与后端波次并发)收束 | ||
| 3476 | + // 后端是否全部 done:无 halted(含 sweep/runtime-null)且 remaining 已空(无 pending 后端)。 | ||
| 3477 | + const backendAllDone = !results.some(r => r.status === 'halted') && remaining.length === 0 | ||
| 3478 | + if (fe1.status === 'halted') { | ||
| 3479 | + results.push(fe1) // phase1 halt:phase2 不跑;已完成 FE 的 fe-code-done 已保留,resume 续跑 | ||
| 3480 | + } else if (!backendAllDone) { | ||
| 3481 | + // 后端未全 done(halt / 还有 pending):phase1 产物(fe-code-done)已落 tag,phase2 留待 resume。 | ||
| 3482 | + fePending.push({ module: 'frontend-phase', status: 'pending', | ||
| 3483 | + blockedBy: ['<全部后端 done>'], decisions: fe1.decisions }) | ||
| 3484 | + log('fe-overlap: 后端未全部完成 → 前端 phase2 延后;phase1 产物已保留,resume 续跑') | ||
| 3485 | + } else { | ||
| 3486 | + // 后端全部 done + phase1 done → phase2:reconcile(合 default→frontend-phase + 移除 phase1 worktree)→ | ||
| 3487 | + // 标准 runModule(feStage='e2e')(主根全栈,skeleton 幂等跳过,behaviorGate/testGate/milestone 原样)。 | ||
| 3488 | + phase('Frontend') | ||
| 3489 | + const def = await agentR(detectDefaultBranchPromptM(), { label:'fe-overlap:default', phase:'Milestone', schema: DEFAULT_BRANCH_SCHEMA }) | ||
| 3490 | + await bestEffortAction(removeLaneWorktreePromptM(laneRoot('frontend-phase')), | ||
| 3491 | + { label:'fe-overlap:rm-worktree', phase:'Milestone', okMsg:'fe-overlap: 已移除 phase1 worktree', failTag:'fe-overlap phase1 worktree 移除' }) | ||
| 3492 | + const rec = await agentR(mergeDefaultIntoFrontendPromptM('frontend-phase', def.branch), | ||
| 3493 | + { label:'fe-overlap:reconcile', phase:'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 3494 | + if (!rec.success) { | ||
| 3495 | + // reconcile merge 冲突/失败:与 milestone merge 同口径,结构化 halt 留人工(phase1 产物已在 frontend-phase 分支)。 | ||
| 3496 | + const reason = `fe-overlap reconcile(merge ${def.branch}→frontend-phase)失败:${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}` | ||
| 3497 | + log(`⛔ HALT — frontend-phase:${reason}`) | ||
| 3498 | + results.push({ module: 'frontend-phase', status: 'halted', reason, decisions: fe1.decisions }) | ||
| 3499 | + } else { | ||
| 3500 | + feModule.feStage = 'e2e' // 让 runModule 前端段走 e2e(jsdom 已在 phase1 绿) | ||
| 3501 | + const fe2 = await runModule(feModule, makeCtx({ decisions: feDecisions })) | ||
| 3502 | + results.push(fe2) | ||
| 3503 | + if (fe2.status === 'done') doneSet.add(fe2.module) | ||
| 3504 | + } | ||
| 3505 | + } | ||
| 3506 | +} | ||
| 3507 | + | ||
| 3349 | // pending:图余量(Task 10)——halt 收敛后 remaining 中每项标注 blockedBy(尚未满足的依赖), | 3508 | // pending:图余量(Task 10)——halt 收敛后 remaining 中每项标注 blockedBy(尚未满足的依赖), |
| 3350 | // caller / coding-start 据此告知"修好后还有哪些待跑、各自被谁挡着",而非线性 slice 误导 | 3509 | // caller / coding-start 据此告知"修好后还有哪些待跑、各自被谁挡着",而非线性 slice 误导 |
| 3351 | // (波次图下 halt 模块之后的 docs/02 顺位可能与 halt 毫无依赖关系)。 | 3510 | // (波次图下 halt 模块之后的 docs/02 顺位可能与 halt 毫无依赖关系)。 |
| 3511 | +// fePending:前端 jsdom-only 重叠下「phase1 done 但后端未全 done」的 frontend-phase pending(并入图余量)。 | ||
| 3352 | const pending = remaining.map(m => ({ | 3512 | const pending = remaining.map(m => ({ |
| 3353 | module: m.id, status: 'pending', | 3513 | module: m.id, status: 'pending', |
| 3354 | blockedBy: (m.deps || []).filter(d => !doneSet.has(d)), | 3514 | blockedBy: (m.deps || []).filter(d => !doneSet.has(d)), |
| 3355 | -})) | 3515 | +})).concat(fePending) |
| 3356 | 3516 | ||
| 3357 | // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 | 3517 | // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 |
| 3358 | // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 | 3518 | // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 |