From 0588d0dcc496863a40c1de4916d6b0bc07dcc3f4 Mon Sep 17 00:00:00 2001 From: zichun Date: Tue, 2 Jun 2026 13:56:14 +0800 Subject: [PATCH] coding.mjs: move frontend behavior verification INTO per-FE reviewWithFixLoop (fixable dimension) --- README.md | 10 +++++++--- docs/design/2026-06-02-frontend-behavior-gate.md | 5 +++++ docs/design/2026-06-02-frontend-behavior-in-review-loop.md | 288 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/setup-test-db-template.test.mjs | 25 +++++++++++++++++++++++-- skills/coding/coding-start/SKILL.md | 8 +++++--- skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs | 10 ++++++++++ workflows/coding.mjs | 587 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 7 files changed, 739 insertions(+), 194 deletions(-) create mode 100644 docs/design/2026-06-02-frontend-behavior-in-review-loop.md diff --git a/README.md b/README.md index 19febd6..cc07a70 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,13 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 │ 10+ 微 agent,全部跳过/分支条件由 JS 判定,幂等) │ └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) - runBranchSetup(frontend-phase) → featureLoop(前端,FE-NN,路径限 frontend/) - → testGate(frontend) → 前端行为闸 behavior-gate(headless 全栈起栈+种子 sentinel, - 逐路由枚举控件/文字两层断言:交互失效硬 halt,文字不符按来源仲裁) + runBranchSetup(frontend-phase) + → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起) + → featureLoop(前端,FE-NN,路径限 frontend/):spec → plan → tdd → verify → + review 循环内并入 per-FE 行为验收 approve 子门(reviewer approve 时才起本 FE 全栈 + +种子 sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix + must-fix→重验,软文字按来源仲裁,行为 green 才打 req-done/) + → testGate(frontend,全量回归 vitest+playwright,与 per-FE 行为验收职责正交) → runMilestone(milestone/frontend-phase) 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start diff --git a/docs/design/2026-06-02-frontend-behavior-gate.md b/docs/design/2026-06-02-frontend-behavior-gate.md index b8ac8da..c07d9ae 100644 --- a/docs/design/2026-06-02-frontend-behavior-gate.md +++ b/docs/design/2026-06-02-frontend-behavior-gate.md @@ -1,5 +1,10 @@ # 前端行为门(behavior-gate)— 最终设计(综合评审后) +> ⚠️ **已作废(SUPERSEDED)** —— 本文描述的是**阶段级、只读、red 即 halt** 的行为门(frontend-phase 末尾跑一次)。 +> 该设计已被 **per-FE 版**取代:行为验收并入每个 FE 的 `reviewWithFixLoop`、成为可 fix 的验收维度(verify→fix→重验循环), +> 并新增前端骨架占位阶段(`runFrontendSkeleton` + `FeStub` 全量 lazy 路由)保证中途可构建。 +> **现行设计见 [`2026-06-02-frontend-behavior-in-review-loop.md`](./2026-06-02-frontend-behavior-in-review-loop.md)**;本文仅作历史保留,勿据此实现。 + > 本文是 5 维对抗式评审后的收敛版。锁定决策(用户拍板)默认保留;评审给出的有依据调整已并入; > 无依据 / 过度工程的建议在文末「拒绝的建议」记明理由。所有改动可追溯到 changeLog。 diff --git a/docs/design/2026-06-02-frontend-behavior-in-review-loop.md b/docs/design/2026-06-02-frontend-behavior-in-review-loop.md new file mode 100644 index 0000000..87d8d98 --- /dev/null +++ b/docs/design/2026-06-02-frontend-behavior-in-review-loop.md @@ -0,0 +1,288 @@ +# 前端行为验收并入 reviewWithFixLoop(v2 最终设计:per-FE + fix 循环) + +> 状态:可实现(ready-to-implement),含 3 项实现前置依赖。 +> 上游:本设计取代 `docs/design/2026-06-02-frontend-behavior-gate.md` 的「阶段级末尾只读 halt 门」形态。 +> 运行时红线(不可违反):禁用 time/random builtin(`Date.now()` / `Math.random()` / `new Date()`);顶层 `return` 是结果通道;`agent/phase/parallel/log/adjudicate/recordDecisions` 是注入全局;**后端 featureLoop 分支逐字不变**。 + +--- + +## 0. 用户拍板的方向(不可推翻) + +- 行为验收**并入 per-FE reviewWithFixLoop**,与静态 code-reviewer 并列为另一个验收维度。 +- 行为发现的硬问题**可 fix**(带 locator 的 must-fix),驱动 fix→重验循环,不再 halt。 +- **仅前端 FE** 有此维度;后端 REQ 分支(无 UI)逐字不变。 +- 接受**每个 FE 起一次(或少数几次)全栈**的代价。 + +本设计在守住上述方向的前提下,落实了 5 维评审里 **确凿的 blocker**:中途可构建性(头号)、起栈成本笛卡尔积爆炸、起栈不可跨子会话复用、locator 不可靠降级=放行、缺 locator 硬问题被现有 filter 静默吞、删阶段门后 report 失去绿前置锚点、测试库护栏只在 LLM 层。 + +--- + +## 1. 关键架构决策:行为验收是 reviewer-approve 的「approve 子门」,不是每轮都起栈 + +这是本设计相对「种子设计」最重要的调整,一次性化解 3 个 blocker(成本笛卡尔积 / 不可复用栈 / 中途构建脆弱性被乘以 N×round)。 + +**原种子设计**:每个 review round 的 step2 跑一次行为验收、fix 后 step5 再跑一次 → 单 FE 最坏 `REVIEW_HARD_ROUNDS(10) × 2 = 20` 次全栈起栈,N 个 FE 串行 → N×20,墙钟在最坏路径不收敛。 + +**v2 决策**:把行为验收从「每个 review round 内」**解耦**为「**reviewer 即将 approve 时才触发的 approve 前置子门**」: + +``` +reviewWithFixLoop(FE): + ┌─ 静态 review→fix 循环(与现状几乎不变;后端逐字不变) + │ round 1..N: reviewer 判 → request-changes 则 filter locator must-fix → fix → reverify(功能测试) → 再 review + │ + └─ 当某轮 reviewer 判 approve(现 1386 分支)→ 不立即 return,先进【行为 approve 子门】: + behaviorSubGate(FE): + for behaviorRound = 1..BEHAVIOR_FE_MAX(=3): + 跑一次 per-FE 行为验收(runBehaviorGateOnce,内含 envError attempt 重试) + ├─ envError / 空覆盖 → 内部 attempt 重试;确定性失败(build-failed) → 记 coverageGap 短路,不 retry 不 halt + ├─ behaviorHard(interactionFailures + sentinel textIssues)为空 → 子门 green → break + └─ behaviorHard 非空: + · 有 locator 的 → 合并进 fixPrompt 的 issues,跑 fix(runStage) + · 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions / retry / halt),永不阻断 approve + · 无 locator 的 behaviorHard → adjudicate(allowContinue:false)(retry 重判/重跑 或 halt),绝不静默丢弃、绝不 approve + fix 后只重跑「本 FE 行为验收」(不必重跑功能 reverify,除非 fix 也动了功能逻辑——见 §6) + 子门 BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved + → 静态 approve ∧ 行为 green ⇒ 才 flipDocs08Checkbox + return{approved:true} + → featureLoop:1344 在 return 后打 req-done(落点不动) +``` + +**收益**: + +- 每 FE 行为起栈次数从 `O(review rounds)` 降到 `O(行为 fix 轮)`,典型 **1 次**(一次过)到 **最多 BEHAVIOR_FE_MAX=3 次**。 +- 静态 must-fix 反复震荡阶段**不起全栈**——只有静态已 clean、reviewer 认可后才付全栈代价,与用户「接受每 FE 起一次全栈」精确对齐(典型就是一次)。 +- 中途构建脆弱性不再被 `N×round` 放大,只在每 FE 的 approve 时刻面对一次(仍需 §2 骨架占位保证可构建)。 + +> **为何不冲突用户决策**:用户要的是「行为验收是 reviewWithFixLoop 内的另一个验收维度、行为问题可 fix、有 fix→重验循环」。本设计完全满足:行为验收在 reviewWithFixLoop 函数内、是 approve 的合取前置、行为硬问题转 must-fix 喂 fix、fix 后重跑行为验收循环。它只是把「行为验收的触发时机」定在「reviewer approve 那一刻」而非「每个 round 顶」——这是控制流优化,不改变验收维度的存在与可 fix 性。 + +--- + +## 2. 实现前置依赖 A(blocker):前端骨架全量路由占位阶段——保证任意时刻 app 可构建可起 + +**问题(已核实)**:`skeleton-gen` 只生成 docs/04 / scripts/*.mjs / tokens.css / .gitignore,**不生成任何 frontend/ 源码、router、路由占位**。frontend/ 全部源码由 coding 期 featureLoop 逐 FE 增量产出。验 FE-N 时 FE-N+1..M 的组件文件不存在 → eager import 路由表编译失败 / 共享 nav 指向未建路由 → 整 SPA 起不来,连 FE-N 页面都加载不出。per-FE 行为验收前移到「前端只建了一部分」的每一轮,直接踩头号风险。 + +**v2 方案(必做,不是候选)**:在 `featureLoop(frontend)` **之前**新增一个 coding 期 stage `runFrontendSkeleton(module)`,由独立子代理依据 `docs/08 §三` FE 清单 + `frontend/` router 约定一次性生成: + +1. **App 外壳**(`frontend/src/App.*` + 入口 `main.*`,若不存在)。 +2. **router 全量路由表**:每个 FE-NN 对应路由都声明,且**全部 lazy import**(`() => import(...)`)。未实现的 FE 路由指向一个最小占位组件 `FeStub`(如 `frontend/src/views/_stub/FeStub.vue`,渲染 `
FE-NN 占位
`)。 +3. **共享布局/导航**:导航链接全部指向已声明的路由 path(不指向不存在的 path),保证任意时刻无悬空链接。 + +落点与时序: +- 在顶层循环 `if (module.feItems.length)` 段、`phase('Frontend')` 之后、`featureLoop` 之前调用 `await runFrontendSkeleton(module)`。 +- **幂等**:以 git tag `fe-skeleton-done` 或检测 router 文件存在 + 全 FE 路由已声明为 ground truth;已建则 skip(resume 安全)。子代理产出后自行 commit(沿用 commitBlock 习惯)。 +- FE-N 实现时(tddPrompt),把对应路由的占位 import 替换为真组件——这要求 **tddPrompt 前端分支补一句**:「若 router 中本 FE 路由仍指向 `FeStub`,实现完成后把该路由 import 改为本 FE 真组件」(属 frontend/ 路径内,不破坏护栏)。 + +> **为何这是根因解**:让 router 始终 lazy + 占位齐全 → 任意时刻 `vite build` / dev server 可起、每个 FE 路由可达 → 把「中途起不来」从高频降为罕见 → per-FE 行为验收的 flake/误判面收敛、`build-failed`(依赖 B)成为罕见兜底而非常态。 + +> **Plan 期 vs coding 期**:放在 coding 期(而非改 skeleton-gen)的理由——FE 清单在 Plan 末尾才稳定、且骨架要落 frontend/ 源码属 coding 范畴;放 coding 期不触碰已锁定的 Plan 闸门,且能用 git tag 幂等。这是新增一个 Plan/coding 边界 stage 的代价,已被本设计接受并显式声明。 + +--- + +## 3. 实现前置依赖 B(blocker):BEHAVIOR_GATE_SCHEMA 增 `build-failed` kind + 确定性短路控制流 + +**问题**:中途构建失败是**确定性编译错误**(缺组件 / import 解析失败 / 类型错),但 `envError.kind` 枚举只有 `port-conflict/stack-not-ready/seed-error/auth-failed/timeout/none`。门面对 `Cannot find module ./views/FE-07.vue` 只能误归类:归 `stack-not-ready` → retry 空转到耗尽后 HALT(编译错永不自愈);归 `interactionFailures` → 假 must-fix 污染源码。两条路都失败。 + +**v2 方案**: + +1. `BEHAVIOR_GATE_SCHEMA.envError.kind` 枚举**新增 `build-failed`**(确定性失败语义;`route-not-buildable` 不单列,统一用 `build-failed` + detail 区分)。 +2. **控制流**(在 per-FE 行为门 helper 内):`build-failed` **既不 retry 也不 halt**——记 `coverageGap`(reason 新增枚举 `build-failed-sibling-unimpl`)+ recordDecisions,**本轮行为门视为「本 FE 行为维度无法判定但非本 FE 缺陷」直接放行 approve**(因为它是「后续 FE 未实现」的预期中途态,不是 FE-N 的 bug;§2 骨架占位让这种情况罕见,一旦发生说明占位未覆盖,留证据供人工)。 +3. `behaviorGatePrompt`(per-FE 版)step0/step2 **明确归因指令**:先 `build` / 起 dev server;若失败,先用 `git` / `Grep` 判断报错根因文件路径—— + - 落在**非本 FE 的 frontend/ 路径**(兄弟 FE / 占位未覆盖)→ 判 `envError.kind="build-failed"`(预期中途态)。 + - 落在**本 FE 路径** → 才可能是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]` 或带 locator must-fix。 + +> 没有这层「确定性失败短路 + 根因归属」,per-FE 行为门无法落地——这是把行为门从「全 FE 已建的安全环境」迁到「部分 FE 已建的敌对环境」的必须保障。 + +--- + +## 4. 实现前置依赖 C(blocker):FE-NN → 路由 path 确定性映射,锁进 spec 产物 + +**问题**:per-FE 只验「本 FE 关联路由」,但 FE→路由关系当前只在 spec 顶部「关联原型」散文 + 子代理对 router 的 Grep 推断,**无结构化真值**。推窄→漏验(假绿);推宽→把别的未实现 FE 的死控件算到本 FE(误 must-fix)。且若 router 全量声明(依赖 A),`routesPlanned = router 全部路由` 会让覆盖率分母被未建路由污染。 + +**v2 方案**: + +1. **deriveSpecPrompt 前端分支**强制在 spec 头部产出结构化小节(不只是散文): + ``` + ## 行为验收作用域(per-FE 行为门唯一断言依据) + - 关联路由: [/orders, /orders/:id] + - 负责控件白名单: [data-testid 约定 或 page+DOM 选择器清单] + ``` + 并要求 fe-feature-review(code-reviewer)校验该小节存在且与 router 配置一致(缺失 / 不一致 → request-changes)。 +2. **behaviorGatePrompt 改为接收「本 FE 路由清单 + 控件白名单」入参**(per-FE 版必需): + - `routesPlanned` **只数本 FE 关联路由**(不是 router 全部),未建兄弟路由既不计入分母也不计 coverageGap。 + - 行为门只对白名单内控件判 must-fix;白名单外控件 / 共享控件若属其它未 approve FE → 归 `coverageGap`(reason `deep-control-not-driven` 或 `build-failed-sibling-unimpl`),**绝不**归本 FE 的 interactionFailure。 +3. **空覆盖兜底保留**:`routesReached==0 || controlsEnumerated==0`(针对本 FE 路由子集)仍归 envBlocked,绝不静默判 green。 + +> 没有这个确定性映射,per-FE 路由作用域无法界定,覆盖率与归因全失真。 + +--- + +## 5. 实现前置依赖 D(blocker / 安全):测试库护栏下沉到 setup-test-db.mjs 模板自身 + +**问题(已核实)**:`scripts-setup-test-db-template.mjs` 只校验 schema 是合法标识符(`/^[A-Za-z0-9_$]+$/`),**不判它是不是测试库**,DROP+CREATE 无条件执行。测试库命名护栏(库名须含 `test/_test/_dev/_local`)当前只在**门子代理生成的 runner 内**(LLM 级 prompt 检查)。per-FE × per-behaviorRound 反复 DROP+CREATE,任一轮子代理漏写护栏 → 对 config-vars 指向的库(可能=开发库)无条件 DROP,反复次数越多撞上漏写的概率越高。真实数据销毁风险。 + +**v2 方案**:把测试库命名护栏**下沉到 `setup-test-db.mjs` 模板**(确定性 JS 边界,不依赖每个子代理记得复述):在现有标识符校验后追加—— + +```js +// 测试库命名护栏:DROP+CREATE 只允许作用于明确的测试/本地库,防误删开发/生产库。 +const ALLOW = process.env.ALLOW_NONTEST_DROP === '1' +if (!ALLOW && !/(^|_)(test|dev|local)$|(^|_)test_|^test_/.test(DB_SCHEMA) && !/test|_dev|_local/.test(DB_SCHEMA)) { + console.error(`[setup-test-db] 拒绝:schema=${JSON.stringify(DB_SCHEMA)} 不像测试库(须含 test/_test/_dev/_local),设 ALLOW_NONTEST_DROP=1 显式放行`) + process.exit(1) +} +``` +(具体正则以实现为准,语义=库名须含 `test`/`_test`/`_dev`/`_local` 之一,否则 fail-closed。) + +- 这样不论被行为门调用多少次都安全。 +- coding.mjs 行为门控制流里,对「测试库护栏触发的红」**保持不重试不仲裁直接 throw** 的硬边界语义(与现 v1 一致,见 behaviorGatePrompt step2 第 1 条)。 +- 这是 skeleton-gen 模板的一次性改动,**不属于 coding.mjs 改造**,但列为本设计前置(否则反复起栈的安全暴露面不可接受)。 + +--- + +## 6. reviewWithFixLoop 改造后的逐轮控制流(实现级) + +**仅 `phase==frontend` 改造;`fe=isFrontend(phase)` 现已存在(1373),后端分支逐字不变。** + +### 6.1 数据流:两类 must-fix 独立来源,schema 不合并、fix 入参合并 + +- **review-must-fix**:reviewer 的 `REVIEW_SCHEMA.issues`,照现状 1392 的 locator filter(缺 locator 降级丢弃)。 +- **behavior-hard**:行为门返回的 `BEHAVIOR_GATE_SCHEMA.interactionFailures` + `source=='sentinel'` 的 `textIssues`。 +- **schema 不杂交**(采纳「schema 选型」维度的定调):行为验收**保留独立 `BEHAVIOR_GATE_SCHEMA` 返回**,不压扁进 `REVIEW_SCHEMA.issues`(否则丢失 envError / 空覆盖 / coverageGaps / source 软硬分流这三个赖以正确的区分)。**仅在喂 fix 时**把「有 locator 的 behavior-hard」降维成 `{summary, locator, severity}` 喂现有 `fixPrompt`(fix 步天然吃这形状)。即:**schema 不合并,fix 入参合并**。 + +### 6.2 approve 闸(显式 AND,钉死落点) + +approve 出口(现 1386 `if (r.verdict==='approve')` 分支)**改为合取**: + +``` +reviewer.verdict==='approve' + ∧ behaviorSubGate(FE) 返回 green + 其中 green ≡ behaviorHard.length===0 ∧ envError∈{none, build-failed} ∧ 本FE覆盖非空(或 build-failed 短路) +``` + +只有合取成立才 `flipDocs08Checkbox` + `return {approved:true}`。这保证(采纳「删阶段门」维度 blocker 的钉死): +- **行为 green 是 reviewWithFixLoop 的 return 前置条件**——req-done tag 落点(featureLoop:1344)**保持不动**,语义自动升级为「静态过+行为过」。 +- **无 locator 的 behavior-hard 绝不 approve**(采纳「控制流/schema」维度 blocker#1):走 adjudicate(allowContinue:false) 决定 retry(重跑行为验收/重判)还是 halt,绝不被 1392 的 locator filter 静默吞掉。 +- flipDocs08Checkbox 翻转自动晚于行为 green(checkbox 纯装饰、resume 只认 req-done tag,无视觉误导)。 + +### 6.3 behaviorSubGate(approve 子门)逐步 + +``` +async function behaviorSubGate(id, specPath, feScope): + // feScope = {routes:[...], controlWhitelist:[...]}(来自 §4 spec 结构化小节) + for behaviorRound in 1..BEHAVIOR_FE_MAX(=3): + bg = await runBehaviorGateOnce(id, behaviorRound, feScope) // 见 §7,内含 envError attempt 重试 + // 1) build-failed 短路(依赖 B):兄弟未实现 → 记 coverageGap + decisions,子门视为 green-by-skip,return passed + if (bg.envError.kind === 'build-failed' && 根因在非本FE路径) { recordDecisions; return {green:true, skipped:true} } + // 2) envError(其它) / 空覆盖:runBehaviorGateOnce 内部已 attempt 重试;到这里仍 blocked → adjudicate(allowContinue:false) retry/halt + if (envBlocked(bg)) { adjudicate; 仍 blocked → throw HALT } + // 3) 软文字:for-of 走 adjudicate;continue→recordDecisions + 加入跨轮 softPassed;sentinel→并入 behaviorHard;retry/halt 同现 + processTextIssues(bg, softPassed) // softPassed 提升到 reviewWithFixLoop 顶层作用域,跨 behaviorRound 持久 + // 4) behaviorHard = interactionFailures + sentinel textIssues + if (behaviorHard.length === 0) return {green:true} + // 5) 分流 + const withLoc = behaviorHard.filter(有 locator) + const noLoc = behaviorHard.filter(无 locator) + if (noLoc.length) { v=adjudicate(allowContinue:false); v!=='retry' → throw HALT; else 下一轮重跑 } + if (withLoc.length) { + await runStage(fixPrompt(id, 'frontend', withLoc降维)) // 复用现有 fix 步 + // fix 后:只重跑本 FE 行为验收(下一轮 behaviorRound);若 fix 同时改了功能逻辑,附带重跑功能 verify(见下) + } + throw HALT behavior-unresolved(BEHAVIOR_FE_MAX 轮仍未 green) +``` + +- **softPassed Set 提升到 reviewWithFixLoop 顶层作用域**(与 round 同寿命,跨 behaviorRound 持久)——直接照搬现 runBehaviorGate 的 softPassed 语义,否则文字层每轮重新消耗仲裁预算撞 ADJUDICATE_MAX。 +- **行为软文字永不进 approve 闸**,只 recordDecisions(采纳「控制流/schema」维度 high#3)。 +- **fix 后的功能复验**:behaviorSubGate 内的 fix 改的是 frontend/ UI 源码,可能引入功能回归。策略——fix 后先跑一次现有 `verifyPrompt` 功能 reverify(allowContinue:false,复用 runStage),红则当功能回归(与现 reverify 同级硬边界),绿后再重跑行为验收。这把「fix 引入功能回归」纳入兜底,且功能 reverify 是 scoped 组件测试(不起全栈),成本低。 + +### 6.4 轮次预算与计数(二维,钉死防证据覆盖) + +- 静态 review/fix 仍用 `REVIEW_SOFT_ROUNDS=5` / `REVIEW_HARD_ROUNDS=10`(现状不变)。 +- 行为子门独立、更小预算:**新增 `BEHAVIOR_FE_MAX=3`**(每 FE 行为 fix 轮硬上限;超限 throw HALT)。**不**复用 review 的 10 轮驱动起栈,**不**让 `REVIEW_HARD_ROUNDS × BEHAVIOR_GATE_PASS_MAX` 隐式相乘到 120 量级。 +- **runBehaviorGateOnce 内部**的 envError attempt 重试用独立小预算(沿用 testGate 的 attempt 1→2 + ADJUDICATE_MAX 思路;§7)。 +- **二维计数表**(采纳「控制流/schema」维度 medium#5): + - `behaviorRound`:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX)。 + - `attempt`:单次 runBehaviorGateOnce 内的环境 race 重试序号。 + - 证据文件名用复合编号:`--behavior-r-a.md`,每次起栈独立证据不互相覆盖、不丢 flake 信号。 +- **单 FE 行为起栈次数硬上界** = `BEHAVIOR_FE_MAX(3) × 每轮 attempt 上限(≤2 + ADJUDICATE_MAX 内)`,量级远小于种子设计的 10×12。典型一次过 = 1 次起栈。 + +### 6.5 contract 严格分离(采纳「控制流/schema」维度 medium#7) + +同一 FE 循环内不同 stage 各自 contract,**绝不混用**: +- fix / review / verify stage:套 `featureStageContract('frontend')`(硬护栏:命中 backend//sql//scripts/ 即越界硬停)。 +- 行为验收 stage:**仍独立套 `behaviorGateContract()`**(作用域例外:允许运行 setup-test-db / 起后端 / 跑 sql 种子 / 跑 playwright;唯一可写 `.tmp/behavior-gate//...` + 证据)。 +- behaviorGateContract **新增一条中途态豁免**(采纳头号维度 low#6):「本门在 per-FE 模式下运行,frontend/ 中本 FE 之外的路由/组件可能尚未实现属预期;遇到指向未建路由的链接/404/编译缺件,一律记 coverageGap 或 envError.kind=build-failed,绝不归为本 FE 的 interactionFailure。本 FE 路由清单是唯一断言作用域。」 + +--- + +## 7. per-FE 行为验收子代理(runBehaviorGateOnce + behaviorGatePrompt per-FE 版)要点 + +**runBehaviorGateOnce(id, behaviorRound, feScope)**:保留现 runBehaviorGate 的失败分层(不推倒重写——采纳「删阶段门」维度 high#4),但 scope 缩到单 FE: + +- **复用现 enforceEnv 思路**做内部 envError attempt 重试 + 空覆盖兜底(attempt 1→2,仍异常经 adjudicate(allowContinue:false) retry/halt)。 +- 返回 `BEHAVIOR_GATE_SCHEMA`(含本 FE scope);把「interaction/sentinel 仍非空」作为「本轮未过」返回给 behaviorSubGate 外层。 +- **不在 runBehaviorGateOnce 内嵌 BEHAVIOR_GATE_PASS_MAX 的多次 rerun 收敛**(那是阶段级单次门的设计);交互/文字层 retry 限制为每 behaviorRound 至多 1 次重起,收敛靠外层 behaviorSubGate 推进(采纳成本维度 medium#6)。 + +**behaviorGatePrompt per-FE 版**(由整 app 阶段级改造): + +- `id` 入参从写死 `frontend-phase` 改为本 FE id;新增入参 `specPath` / `behaviorRound` / `attempt` / `feScope`。 +- **起栈**:runner 自起后端+前端(项目无既有 e2e webServer/playwright.config——已核实 F1,**删除「复用既有 webServer」这条死路暗示**,避免实现者照已证伪的假设做;只走「冷起栈」,**明确写死 round 间不复用运行栈、无 HMR**,这是现运行时硬约束,采纳成本维度 blocker#2)。 +- **四段时序不变**:空库→起后端等 Flyway 建 schema+健康就绪→sentinel 种子(FK 有序)→起前端 headless。测试库护栏现由模板兜底(§5),runner 仍可复述但不再是唯一防线。 +- **step0/step2 build 归因**(依赖 B):先 build / 起 dev server,失败按根因路径归 `build-failed`(非本 FE) 或本 FE 真 bug。 +- **step1 路由真值**:`routesPlanned` 只数 `feScope.routes`(本 FE 路由),不数 router 全部(依赖 C)。 +- **枚举**:只驱动 `feScope.routes` + `feScope.controlWhitelist`;非白名单 / 共享未 approve FE 控件 → coverageGap,不归本 FE。 +- **行为硬问题带源码 locator**(采纳 locator 维度 blocker#1 的拆分): + - A 类(可经 route→router 配置→view 组件文件反查到**组件级文件路径**):locator = 「组件文件 + DOM 选择器 + 失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值」。**fixPrompt 放宽**:locator 允许「文件 + DOM 描述」而非强制 file:line,由 fix 子代理在该组件内 Grep 定位 handler/绑定。 + - B 类(连组件文件都反查不出):**不静默降级为放行**——归 coverageGap 并**计入未覆盖**,使 behaviorSubGate 不能判 green(降级≠放行)。或归 envError(stack-not-ready) 走 retry。 + - **起栈强制 dev/source-map 模式**,runner 注入定位辅助(`data-testid` 约定 / Vue `__file`),把 page+selector 映射到组件文件作为契约前置。 +- **binding-garbage / sentinel-mismatch**:locator 除组件文件外,附带 DOM 路径 + 绑定文本片段 + 期望 sentinel + 实际渲染值(写进 summary,不依赖 file:line),供 fix 在组件内 Grep 该绑定表达式。 +- **临时件隔离 per-FE×per-behaviorRound**:`.tmp/behavior-gate//r/`(采纳「删阶段门」维度 medium#5);每轮跑前清空本子目录,runner `finally` 必须 kill 本 FE 起的全部子进程并按本 FE 端口集回收;FE 间起栈端口先探测占用 + 动态回退。每次 coding-start 首个 FE 行为验收前清一次 `.tmp/behavior-gate/` 整目录入口(去跨 resume 串味)。 +- **证据**:`docs/superpowers/reviews/--behavior-r-a.md`(与 review 报告同目录);截图归档到版本管理的 assets。 +- **确定性端口/pid 回收前置**(采纳安全维度 high#3):起栈前先按既知端口 + `.tmp/*.pid` 强制回收上一 attempt 残留(编排层 + runner 双保险),对反复 port-conflict 设独立硬上限直接 halt 提示人工清理,避免连环 retry 烧时间。 + +--- + +## 8. 删除 / 改写的阶段级门引用(实现级清单) + +### 8.1 删除(顶层 frontend 段) +- 删 `phase('Behavior')`(1644)+ `await runBehaviorGate(module)`(1645)。 +- `runBehaviorGate`(1465-1582)**改造为** per-FE 的 `runBehaviorGateOnce` + `behaviorSubGate`(被 reviewWithFixLoop 调用),**不推倒重写**——保留 enforceEnv / 空覆盖兜底 / interaction 分层 / 软文字 source 分流 / softPassed 语义,只把 scope 缩到单 FE、把「硬问题转 must-fix locator」作为新增出口。 +- `meta.phases`:**彻底删除 `{ title: 'Behavior' }`**(12 行)——行为验收并入 Frontend phase 内,所有行为相关 `agent()`/`adjudicate()` 的 phase 入参从 `'Behavior'` 统一改为 `'Frontend'`(与 reviewWithFixLoop 现有 grp 一致)。不保留 'Behavior' 作 UI 分组(否则成无 `phase()` 驱动的孤儿)(采纳「删阶段门」维度 medium#3)。 + +### 8.2 reportPrompt 前端分支改写(采纳「删阶段门」维度 high#2 + 控制流维度 medium#6) +- **删/改 1096 绿前置**:原 Glob `frontend-phase-behavior-gate-r*.md`「最后一份非 RED」整条**删除**(阶段级文件已不再产生,留之必断链或不确定 halt)。改为:**对每个 `req-done/` tag 视为行为已过**(因 per-FE 行为 green 已是 req-done 前置,report 不必再独立校验行为绿,避免双真值)。可选加一句轻量校验:每个 FE 存在对应 `--behavior-r*-a*.md` 证据且最后一份非 RED。 +- **改 1106 §⑤**:把 `frontend-phase-behavior-gate-r*.md` 汇总改为按 per-FE 证据目录 `docs/superpowers/reviews/-FE-*-behavior-r*-a*.md` 汇总 flake / 环境 race / 文字 continue。 +- **改 1107 §⑧**:偏离清单的 behavior-gate coverageGaps / textIssues continue / 逐控件判定 / authState 来源,从阶段级文件改为 per-FE 证据汇总。 +- 注意 testGate 的 `frontend-phase-test-gate-r*.md` 绿前置(1094)**保留不动**——testGate(全量回归)不并入 per-FE 循环。 + +### 8.3 保留不变 +- **阶段级 testGate(全量回归 vitest+playwright)保留**(1642-1643)——职责正交,per-FE 行为验收不替代全量回归。 +- 后端 featureLoop / featureLoop backend 分支 / runMilestone / runCrossModule / 顶层 backend 段:**逐字不变**。 + +--- + +## 9. README / SKILL 文案改动 + +- **README**(46-49 行):把「→ testGate(frontend) → 前端行为闸 behavior-gate(…逐路由枚举…)→ runMilestone」改为:「featureLoop(前端,FE-NN,每个 FE 在 review 循环内并入 per-FE 行为验收 approve 子门:reviewer approve 时起本 FE 全栈+sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才打 req-done) → testGate(frontend,全量回归) → runMilestone」。补一句「前端骨架占位阶段保证中途可构建」。 +- **coding-start SKILL**(步骤 0 横幅,26 行):把「前端行为闸 behavior-gate(…逐路由枚举:交互失效→halt,文字不符→仲裁)」改为「前端功能循环内含 per-FE 行为验收(reviewer approve 时起本 FE 全栈验『按钮真生效/文字对』,硬问题可 fix 重验,不再是末尾独立门)」。 + +--- + +## 10. 残留风险(接受 / 已缓解) + +1. **冷起栈墙钟**:round 间不复用栈是现运行时硬约束(无跨子会话常驻进程原语)。已用「approve 子门 + BEHAVIOR_FE_MAX=3」把起栈次数压到典型 1 次/FE、最坏 3 次/FE 来控成本,而非靠不可行的栈复用。每次起栈含全量 Flyway apply(随 migration 增多单调增长),计入墙钟预算。若 N 很大仍可能数小时——这是用户已接受「每 FE 起一次全栈」的直接代价,本设计已把它从 N×20 降到 N×(1~3)。 +2. **locator 可靠性**:A 类(组件文件级)映射可行;B 类不可降级放行而是计入未覆盖阻断 approve。仍可能出现 fix 在组件内 Grep 不中绑定行、多轮修不中逼近 BEHAVIOR_FE_MAX 后 halt 转人工——这是「运行时 DOM→源码」固有难度的残留,已用「附 DOM 路径+绑定片段+期望/实际值」最大化命中率、用比 review 更紧的 BEHAVIOR_FE_MAX=3 控制空转成本。 +3. **骨架占位覆盖不全**:若 runFrontendSkeleton 漏建某 FE 的路由占位,验该 FE 前的某个兄弟 FE 时仍可能 build-failed;已用 build-failed 短路(不 halt、记证据)兜底,但会留覆盖盲点供人工。 +4. **per-FE 库状态与阶段级 testGate 隔离**:行为门入口 DROP+CREATE 自带空库语义,跑完不为 testGate 留状态(testGate 自带 setup-test-db 重置);二者共用同一物理测试库,时序上串行无并发争用。 +5. **deriveSpec 的 FE→路由结构化小节依赖 LLM 正确产出 + reviewer 校验**:若两者都失误,feScope 可能不准;已让 reviewer 把「小节存在且与 router 一致」作为 request-changes 项兜底,但非确定性。 + +--- + +## 11. 实现顺序建议 + +1. 前置 D(setup-test-db 模板护栏)——独立、最安全、先做。 +2. 前置 C(deriveSpecPrompt FE→路由结构化小节 + reviewer 校验)——为 feScope 入参铺路。 +3. 前置 A(runFrontendSkeleton 骨架占位 stage + tddPrompt 占位替换指令)——保证中途可构建。 +4. 前置 B(BEHAVIOR_GATE_SCHEMA 增 build-failed + 归因控制流)。 +5. 主改造(reviewWithFixLoop 加 behaviorSubGate / runBehaviorGate→runBehaviorGateOnce per-FE / 二维计数 / softPassed 提升 / contract 分离)。 +6. 删除阶段级门 + reportPrompt 改写 + meta.phases 删 Behavior + phase 入参改 Frontend。 +7. README / coding-start SKILL 文案。 + +每步后端分支必须逐字不变(diff 校验);运行时红线(time/random builtin / 顶层 return / 注入全局)每步复核。 diff --git a/lib/setup-test-db-template.test.mjs b/lib/setup-test-db-template.test.mjs index 57189ce..688f777 100644 --- a/lib/setup-test-db-template.test.mjs +++ b/lib/setup-test-db-template.test.mjs @@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url' const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs', import.meta.url)) -function runWithSchema(schemaLine) { +function runWithSchema(schemaLine, env = {}) { const dir = mkdtempSync(join(tmpdir(), 'erp-stdb-')) mkdirSync(join(dir, 'scripts')) copyFileSync(TEMPLATE, join(dir, 'scripts', 'setup-test-db.mjs')) @@ -19,7 +19,7 @@ function runWithSchema(schemaLine) { join(dir, 'config-vars.yaml'), ['database:', ' host: 127.0.0.1', ' port: 1', ' user: root', ' password: x', ' ' + schemaLine, ''].join('\n'), ) - return spawnSync('node', [join(dir, 'scripts', 'setup-test-db.mjs')], { encoding: 'utf8' }) + return spawnSync('node', [join(dir, 'scripts', 'setup-test-db.mjs')], { encoding: 'utf8', env: { ...process.env, ...env } }) } // ROBUST-3:空 schema 不应进到 DROP DATABASE `` —— 守卫应先拦下。 @@ -50,3 +50,24 @@ test('setup-test-db: a valid identifier schema passes the guard (no false positi // 连不上 127.0.0.1:1 → 非零退出;关键是错误不来自 schema 守卫。 assert.doesNotMatch(r.stderr, /database\.schema 非法|schema 非法或未填/, 'stderr: ' + r.stderr) }) + +// 前置依赖 D(安全):测试库命名护栏——非测试库名默认 fail-closed,防误删开发/生产库。 +test('setup-test-db: a non-test schema fails closed by default (D non-test guard)', () => { + const r = runWithSchema('schema: erp_prod') + assert.equal(r.status, 1) + assert.match(r.stderr, /不像测试库|ALLOW_NONTEST_DROP/, '应是测试库命名护栏报错 — stderr: ' + r.stderr) +}) + +// D:测试库名(含 test / _test / _dev / _local)应通过命名护栏,错误不来自该护栏。 +test('setup-test-db: test-like schema names pass the naming guard (erp_test / erp_dev)', () => { + for (const name of ['erp_test', 'erp_dev', 'erp_local', 'test_db']) { + const r = runWithSchema('schema: ' + name) + assert.doesNotMatch(r.stderr, /不像测试库/, `${name} 不应被命名护栏拒绝 — stderr: ` + r.stderr) + } +}) + +// D:ALLOW_NONTEST_DROP=1 显式放行非测试库名(错误不再来自命名护栏)。 +test('setup-test-db: ALLOW_NONTEST_DROP=1 explicitly bypasses the naming guard', () => { + const r = runWithSchema('schema: erp_prod', { ALLOW_NONTEST_DROP: '1' }) + assert.doesNotMatch(r.stderr, /不像测试库/, '显式放行后不应再被命名护栏拒绝 — stderr: ' + r.stderr) +}) diff --git a/skills/coding/coding-start/SKILL.md b/skills/coding/coding-start/SKILL.md index fd2d1ab..fcf7da9 100644 --- a/skills/coding/coding-start/SKILL.md +++ b/skills/coding/coding-start/SKILL.md @@ -21,9 +21,11 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) 每个模块: 后端功能循环 spec → plan → tdd → verify → review(≤5轮) 后端测试闸 test-gate(RED 自动重试 1 次,仍 RED → halt) - 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/) - 前端测试闸 test-gate - 前端行为闸 behavior-gate(headless 全栈起栈+种子 sentinel,逐路由枚举:交互失效→halt,文字不符→仲裁) + 前端骨架占位 router 全量 lazy 路由表 + FeStub 占位(保证中途可构建) + 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/);review 循环内含 + per-FE 行为验收(reviewer approve 时起本 FE 全栈验「按钮真生效/文字对」, + 硬问题可 fix 重验,行为 green 才打 req-done;不再是末尾独立门) + 前端测试闸 test-gate(全量回归) 跨模块记录 → 模块报告 → 里程碑(merge --no-ff + milestone/ tag) 任一模块 halt → fail-fast 停在该模块,修复后重跑本入口即可续跑 diff --git a/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs b/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs index e028646..7ff160f 100644 --- a/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs +++ b/skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs @@ -83,6 +83,16 @@ if (!/^[A-Za-z0-9_$]+$/.test(DB_SCHEMA)) { process.exit(1) } +// 测试库命名护栏(确定性 JS 边界,唯一防线):本脚本无条件 DROP + CREATE schema; +// per-FE × per-behaviorRound 反复起栈会反复 DROP,对 config-vars 指向的库(可能 = 开发/生产库) +// 误删风险随次数放大。故只允许作用于"明确像测试/本地库"的库名——库名须含 test / _test / _dev / _local +// 之一(不区分大小写),否则 fail-closed;确需对非测试库执行时,显式设 ALLOW_NONTEST_DROP=1 放行。 +// 不依赖任何调用方(如行为门 runner)记得复述同等检查——模板是唯一防线。 +if (process.env.ALLOW_NONTEST_DROP !== '1' && !/test|_dev|_local/i.test(DB_SCHEMA)) { + console.error(`[setup-test-db] 拒绝:schema=${JSON.stringify(DB_SCHEMA)} 不像测试库(库名须含 test/_test/_dev/_local),设 ALLOW_NONTEST_DROP=1 显式放行`) + process.exit(1) +} + console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) const sql = diff --git a/workflows/coding.mjs b/workflows/coding.mjs index 3e2d2a6..2a27bad 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -9,8 +9,10 @@ export const meta = { description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.', phases: [ { title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' }, - { title: 'Gate' }, { title: 'Behavior' }, { title: 'Milestone' }, + { title: 'Gate' }, { title: 'Milestone' }, ], + // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门, + // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。 } const ROUTER_SCHEMA = { type:'object', additionalProperties:false, @@ -65,24 +67,27 @@ const GATE_SCHEMA = { type:'object', additionalProperties:false, required:['status'], properties:{ status:{type:'string',enum:['green','red']}, failures:{type:'array',items:{type:'string'}} } } -// BEHAVIOR_GATE_SCHEMA:前端行为门(headless behavior-gate)返回。 +// BEHAVIOR_GATE_SCHEMA:前端行为门(per-FE behavior 子门)返回。 // 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 覆盖率 / 环境错误分别结构化, -// JS 据 source/kind 分流(交互硬 halt,文字按 source 二分 allowContinue,envError 走 retry)。 -// 设计:见 docs/design/2026-06-02-frontend-behavior-gate.md § 2。 +// JS 据 source/kind 分流(交互硬边界转 must-fix,文字按 source 二分 allowContinue,envError 走 retry, +// build-failed 确定性短路)。设计:见 docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 3/6/7。 const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, required:['status','routesPlanned','routesReached','controlsEnumerated'], properties:{ status:{type:'string', enum:['green','red']}, - routesPlanned:{type:'integer'}, // router 声明的路由数(覆盖率分母来源) - routesReached:{type:'integer'}, // 实际带鉴权加载成功的路由数 - controlsEnumerated:{type:'integer'}, // live 枚举到的控件数(空覆盖必须可见) + routesPlanned:{type:'integer'}, // 本 FE 关联路由数(覆盖率分母来源;per-FE 只数 feScope.routes,不数 router 全部) + routesReached:{type:'integer'}, // 实际带鉴权加载成功的本 FE 路由数 + controlsEnumerated:{type:'integer'}, // live 枚举到的本 FE 白名单控件数(空覆盖必须可见) authState:{type:'string'}, // 以何角色登录 / 覆盖角色 / 未覆盖角色集 + // interactionFailures.locator:行为硬问题的源码定位(组件文件 [+ DOM 描述])。per-FE 行为门必须反查到 + // 组件文件路径才能转 must-fix 喂 fix;反查不出(B 类)→ 不入 interactionFailures,归 coverageGap(不放行)。 // 交互层硬边界:no-observable-effect / js-error / console-error / missing-docs05-call / binding-garbage interactionFailures:{ type:'array', items:{ type:'object', additionalProperties:false, required:['page','control','kind','detail'], properties:{ page:{type:'string'}, control:{type:'string'}, kind:{type:'string', enum:['no-observable-effect','js-error','console-error','missing-docs05-call','binding-garbage']}, - detail:{type:'string'} } } }, + detail:{type:'string'}, + locator:{type:'string'} } } }, // 组件文件路径 [+ DOM 选择器/绑定片段描述];有则可转 must-fix 喂 fix // 文字层软边界:source 决定 allowContinue(sentinel 客观 bug 不可 continue;i18n/literal/semantic 可 adjudicate continue) textIssues:{ type:'array', items:{ type:'object', additionalProperties:false, required:['page','region','expected','actual','source'], @@ -91,18 +96,21 @@ const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, expected:{type:'string'}, actual:{type:'string'}, source:{type:'string', enum:['sentinel','i18n','literal','semantic']} } } }, // 覆盖率缺口:写证据 + recordDecisions,不单独 halt(空覆盖由 controlsEnumerated==0 兜底) + // build-failed-sibling-unimpl:兄弟 FE 未实现导致本 FE 之外路由/组件编译缺件(预期中途态,不归本 FE 缺陷) + // locator-not-resolvable:行为硬问题连组件文件都反查不出(B 类),计入未覆盖阻断 approve,不静默放行 coverageGaps:{ type:'array', items:{ type:'object', additionalProperties:false, required:['page','reason','detail'], properties:{ page:{type:'string'}, - reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed']}, + reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed','build-failed-sibling-unimpl','locator-not-resolvable']}, detail:{type:'string'} } } }, - // 环境错误(与业务断言失败严格区分,走 retry):none 表示无环境问题 + // 环境错误(与业务断言失败严格区分):none 表示无环境问题;build-failed 是确定性短路(既不 retry 也不 halt)。 + // build-failed 时 rootCausePath 写报错根因文件路径——落在非本 FE 路径=兄弟未实现(短路放行),落在本 FE=真构建 bug。 envError:{ type:'object', additionalProperties:false, required:['kind'], properties:{ - kind:{type:'string', enum:['port-conflict','stack-not-ready','seed-error','auth-failed','timeout','none']}, - detail:{type:'string'}, ports:{type:'string'}, pids:{type:'string'} } }, + kind:{type:'string', enum:['port-conflict','stack-not-ready','seed-error','auth-failed','timeout','build-failed','none']}, + detail:{type:'string'}, ports:{type:'string'}, pids:{type:'string'}, rootCausePath:{type:'string'} } }, // decisions[]:复用 STAGE_RESULT 形状,缺值自主决策日志 decisions:{ type:'array', items:{ type:'object', additionalProperties:false, required:['question','choice','rationale'], @@ -288,6 +296,21 @@ function deriveSpecPrompt(id, phase) { fe ? '- 规格至少含:关联 REQ + 关联原型;组件树(按页面 / 区域分块,推导自 prototype DOM);页面状态机(loading / empty / error / 正常 / 表单提交中 至少 5 态);消费的后端端点(对齐 docs/05);业务规则前端复刻清单(逐条:规则 / 触发时机 / 报错文案 / 来源 REQ);Design Tokens 引用清单(`var(--color-*)`)。' : '- 规格覆盖:goal / 输入输出 / 业务规则 / 约束 / schema / API 引用 / acceptance criteria。', + fe + ? [ + '', + '## 行为验收作用域结构化小节(per-FE 行为门唯一断言依据,**强制写到 spec 头部**)', + '- 在 spec 文件头部(紧随标题/关联 REQ 之后)写一个**结构化小节**,标题逐字为 `## 行为验收作用域`,内含两条机器可读清单:', + ' ```', + ' ## 行为验收作用域', + ' - 关联路由: [/orders, /orders/:id]', + ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]', + ' ```', + `- **关联路由**:从 \`${ROOT}/frontend/\` router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 \`:id\` 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`, + '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外 / 共享控件归 coverageGap,绝不算本 FE 缺陷。', + '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。', + ].join('\n') + : '', '', commitBlock('', `docs(spec:${id}): 派生规格`), '', @@ -366,6 +389,9 @@ function tddPrompt(id, phase, planPath) { fe ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' : '', + fe + ? `- **占位替换(保证中途可构建 + per-FE 行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/\` router 定位本 FE 路由 path 的 import 行;仍在 \`frontend/\` 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` + : '', '', '## 护栏', '- **绝不**在主会话直接跑测试(mvn / pnpm / playwright / scripts/test.mjs)——必须通过 Agent 子会话。', @@ -441,6 +467,7 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`, fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '', `- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`, + fe ? `- **行为验收作用域小节校验(per-FE 行为门前置真值,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——行为门只能据此确定本 FE 路由作用域。` : '', round > 1 && lastVerifySummary ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。` : '', @@ -518,106 +545,191 @@ function gatePrompt(module, phase, attempt = 1) { ].filter(Boolean).join('\n') } -// ---- 前端行为门(headless behavior-gate)---- -// 设计权威:docs/design/2026-06-02-frontend-behavior-gate.md。frontend testGate 绿后、report/milestone 前跑, -// 仅 frontend-phase 聚合模块触发。门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend') +// ---- 前端行为验收(per-FE behavior 子门)---- +// 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。 +// 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发, +// 起本 FE 全栈 + sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才放行 approve。 +// 门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend') // (其路径护栏命中 backend/sql/scripts 即越界硬停,与门必须运行 setup-test-db / 起后端 / 生成 SQL 种子自相矛盾)。 // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符; // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright, -// 唯一**可写** = .tmp/behavior-gate/r/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 +// 唯一**可写** = .tmp/behavior-gate//r/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 function behaviorGateContract() { return [ - '## 硬约束(非交互行为门子代理)', + '## 硬约束(非交互行为验收子代理)', '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', - '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', - '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json / playwright.config.*)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、起前端 headless(vite / playwright webServer)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate/r/\`(spec/种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-gate-r.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/module-reports/assets/...\`)。`, + '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', + '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate//r/\`(spec/种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/reviews/--behavior-r-a.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/reviews/assets/...\`)。`, `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, + '- **per-FE 中途态豁免(关键)**:本门在 **per-FE 模式**下运行——`frontend/` 中**本 FE 之外**的路由/组件可能尚未实现,属预期中途态。遇到指向未建路由的链接 / 404 / 编译缺件(兄弟 FE 或骨架占位未覆盖),一律记 `coverageGaps[reason="build-failed-sibling-unimpl"]` 或 `envError.kind="build-failed"`(按根因路径归属,见 step0/step2),**绝不**归为本 FE 的 `interactionFailures`。**本 FE 路由清单(feScope.routes)是唯一断言作用域**;白名单外 / 共享控件归 coverageGap,不算本 FE 缺陷。', '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。', - '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / attempt 序号),**绝不**依赖时间戳 / 随机数。', + '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / FE id / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。', ].join('\n') } -// behaviorGatePrompt:门子代理的完整流水线提示(step0-6 + schema)。 -// attempt:1 = 首跑;2.. = flake / 环境 race retry。每 attempt 独立 .tmp 子目录 + 独立证据文件。 -function behaviorGatePrompt(module, attempt) { - const id = module?.id ?? 'frontend-phase' - const tmpDir = `${ROOT}/.tmp/behavior-gate/r${attempt}` - const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-gate-r${attempt}.md` +// behaviorGatePrompt:per-FE 行为验收子代理的完整流水线提示(step0-6 + schema)。 +// id:本 FE id(如 FE-07);specPath:本 FE spec(含 ## 行为验收作用域 小节,feScope 来源 + 日期前缀); +// behaviorRound:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX);attempt:本轮内环境 race 重试序号(1..)。 +// 每 (FE × behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。 +function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { + const safeId = id ?? 'FE' + const tmpDir = `${ROOT}/.tmp/behavior-gate/${safeId}/r${behaviorRound}` + const date = (() => { try { return dateFromArtifactPath(specPath) } catch { return '' } })() + const evidence = `docs/superpowers/reviews/${date}-${safeId}-behavior-r${behaviorRound}-a${attempt}.md` return [ - `# behavior-gate — 前端行为门(headless,attempt=${attempt})`, + `# behavior — 前端 per-FE 行为验收(headless,FE=${safeId}, behaviorRound=${behaviorRound}, attempt=${attempt})`, '', behaviorGateContract(), '', '## 目标', - `用真实全栈运行证明前端 \`${id}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`, - `单个子会话内**收敛完成**:起栈 → 逐路由枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`, - attempt > 1 ? `- 本次 = 第 ${attempt} 次(上一次 red 或 envError;本轮用于辨识 flake / 等环境就绪);证据**写到独立文件 r${attempt}** 不要覆盖前一次。` : '', - '', - '## 运行机制(无常驻进程跨会话;起栈→跑→teardown 收敛进单 runner)', - `- **入口清目录(跑前第一步,去串味,§7/C25)**:${attempt === 1 - ? `本次 attempt=1 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/\` 目录(清掉所有历史 attempt 残留 runner/种子/spec,避免跨 resume 串味),再新建本 attempt 子目录 \`${tmpDir}/\`。` - : `本次 attempt=${attempt} → 仅删除/清空本 attempt 子目录 \`${tmpDir}/\`(保证幂等,不动其它 attempt 的已提交证据无关的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, - `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 全部子进程**并透传结构化结果。**绝不**让前台 spring-boot:run / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, - `- \`${tmpDir}/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, - '', - '## step0 探测起栈能力', - `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/frontend/playwright.config.*\` + \`${ROOT}/config-vars.yaml\`。`, - '- (a) 有 `webServer` / `reuseExistingServer` → 复用 playwright 起前端;(b) 无 → runner 自负起**后端 + 前端**(项目通常无既有 e2e 起栈,须显式探测 + 自负起栈);无法判定 / 起栈失败 → `envError.kind="stack-not-ready"`。', - '', - '## step1 路由真值发现(覆盖率分母)', - `- 主来源 = \`${ROOT}/frontend/\` 的 router 配置(Vue Router / React Router \`routes\`,用 Grep 定位);\`routesPlanned\` = router 声明的路由数。`, - '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**(作覆盖率分母);每路由标注所需登录角色。', + `用真实全栈运行证明本 FE \`${safeId}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`, + `单个子会话内**收敛完成**:冷起栈 → 逐**本 FE 路由**枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`, + `- 本 FE 行为验收作用域唯一真值 = spec \`${specPath}\` 头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先 Read 该 spec 取出 feScope;缺该小节 → \`envError.kind="stack-not-ready"\` 并在 detail 写明(不应出现:reviewer 已校验它存在)。`, + behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '', + '', + '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', + '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', + `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 + ? `本次是本 FE 首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/${safeId}/\` 目录(清掉本 FE 历史残留 runner/种子/spec),再新建本轮子目录 \`${tmpDir}/\`。` + : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, + `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本 FE 起的全部子进程**并透传结构化结果。**绝不**让前台 spring-boot:run / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, + `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`, + `- \`${ROOT}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, + '', + '## step0 探测 + build 归因(确定性短路前置,依赖 build-failed kind)', + `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/config-vars.yaml\`。`, + '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。', + '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——', + ` - 落在**非本 FE 的 \`frontend/\` 路径**(兄弟 FE 组件缺失 / 骨架占位未覆盖 / 指向未建路由)→ \`envError.kind="build-failed"\` + \`rootCausePath=<非本FE路径>\`(**预期中途态**,不是本 FE bug)。`, + ' - 落在**本 FE 路径**(feScope 关联组件)→ 才是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]`(带 locator=组件文件)。', + ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。', + '', + '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)', + '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。', + '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。', '- 带参动态路由用**种子已知主键**实例化;无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', + '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', '', '## step2 安全护栏 + 起栈四段严格时序(schema 由 Flyway 在后端启动时才建)', - `1) **测试库安全护栏(确定性,先于一切)**:读 config-vars 的数据库名;若**不匹配测试库命名**(库名须含或以 \`test\` / \`_test\` / \`_dev\` / \`_local\` 结尾)→ runner 非零退出,返回 \`status:red\` + \`envError.kind\` 留空走 HALT 语义(在 detail 写明「测试库护栏:库名 非测试库,拒绝 DROP,留人工确认」)。**绝不**对非测试库跑 setup-test-db。`, + `1) **测试库安全护栏**:测试库命名护栏现已下沉到 \`${ROOT}/scripts/setup-test-db.mjs\` 模板自身(确定性 JS 边界,库名须含 test/_test/_dev/_local,否则 fail-closed,\`ALLOW_NONTEST_DROP=1\` 显式放行)。runner 可复述但模板是唯一防线;若模板因测试库护栏非零退出 → 返回 \`status:red\` + 在 detail 写明「测试库护栏触发」(上层对此**不重试不仲裁直接 halt**,留人工确认)。`, `2) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程。`, '3) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', '4) **此时才跑种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** 种子(先父后子)。失败 → `envError.kind="seed-error"` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不**混进交互 RED。', ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——字符串列逐字段唯一编码(如 `CUST_NAME_S001`,抓绑错字段)+ 行序号保 UNIQUE;数值列用高位魔数;enum 列从 docs/03 值域取并标注。插入前扫 Flyway / config-vars 既有初始数据(admin_init 等)键,sentinel 主键偏移到不冲突区;断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。', - '5) **起前端 headless**:(a) playwright webServer / (b) spawn + 轮询 ready;端口同样探测 + 动态回退。', - '- `finally` **硬要求 kill 全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。', + '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', + '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。', '', '## step2.5 鉴权 bootstrap(确定性前置)', '- 用 config-vars `admin_init` 或种子已知凭据,经 `docs/05` 登录端点**真实登录**拿 JWT,注入 Playwright `storageState`;`authState` 记角色覆盖(覆盖 / 未覆盖角色集)。', '- 登录失败 = `envError.kind="auth-failed"`(环境 race,走 retry),**绝不**当成死控件。', '', - '## step3 枚举(可达性驱动 + 分母对账,非首帧快照)', - '- 每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 推导清单,分子 = live 枚举。', + '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;只驱动本 FE feScope)', + '- **只枚举/驱动 feScope.routes + feScope.controlWhitelist**(本 FE 白名单控件)。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 本 FE 推导清单,分子 = live 枚举。', '- 分母有但首帧无的控件:runner 尝试**驱动到出现态**(种子保列表非空触发行级操作 / 进多步流程下屏 / 展开 dropdown / 切 tab 后二次枚举);仍不可达 → `coverageGaps[reason="deep-control-not-driven"]`,不静默判 green。到不了的路由 → `coverageGaps[reason="unreachable-auth"|"unreachable-no-route"]`,与「到达了但控件死」严格区分。', + '- **白名单外 / 共享控件**:若属其它未 approve FE 或共享区 → 归 `coverageGaps[reason="deep-control-not-driven"]`,**绝不**归本 FE 的 `interactionFailures`。', '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', - '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。', + '- `routesReached` / `controlsEnumerated` 据实填(本 FE 子集空覆盖必须可见)。', '', '## step4 推导期望', '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', '', - '## step5 断言(两层 + 可观测效果白名单)', + '## step5 断言(两层 + 可观测效果白名单 + 硬问题带源码 locator)', '- **交互层可观测效果白名单**:URL 变化 / docs05 网络调用(`page.on("request")` 比对端点)/ DOM 变更 / 校验信息 / 弹层 / toast / 原生对话框(枚举前注册 `page.on("dialog")`,confirm/alert/beforeunload 计合法效果,防 confirm 阻塞误判 missing-docs05-call)/ 下载(`page.on("download")`)/ 新标签(`page.on("popup")` / `target=_blank`)。', ' - 无任何效果 → `interactionFailures[kind="no-observable-effect"]`;JS 异常 → `js-error`;`console.error` → `console-error`;应发未发网络调用 → `missing-docs05-call`。断言用 auto-waiting / `expect.poll`,**不用**固定 sleep。', '- **文字层**:动态文字格对比该 region 字段的唯一 sentinel(抓绑错字段)。', - '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`(硬 halt);双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', - '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`。', - '', - `## step6 证据落盘 + commit(运行时行为,沿用 test-gate 证据 commit 习惯)`, - `- 写 \`${evidence}\`:推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`, - `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/module-reports/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, + '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', + '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 approve)。', + '- **行为硬问题必须带源码 locator(转 must-fix 喂 fix 的前置)**:', + ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。', + ' - **B 类(连组件文件都反查不出)**:**不静默降级放行**——归 `coverageGaps[reason="locator-not-resolvable"]`(计入未覆盖,使本轮不能判 green),或归 `envError.kind="stack-not-ready"` 走 retry。绝不把无 locator 的硬问题塞进 `interactionFailures` 不带 locator(上层会因无 locator 走 adjudicate(allowContinue:false),绝不放行)。', + '', + `## step6 证据落盘 + commit(运行时行为,沿用证据 commit 习惯)`, + `- 写 \`${evidence}\`:本 FE feScope / 推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`, + `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/reviews/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`, - commitBlock(`${evidence} docs/superpowers/module-reports/assets`, - `docs(behavior-gate:r${attempt}): 前端行为门证据`), + commitBlock(`${evidence} docs/superpowers/reviews/assets`, + `docs(behavior:${safeId}:r${behaviorRound}-a${attempt}): per-FE 行为验收证据`), '', '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)', - '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无 envError + 覆盖非空)| `red`。', - '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(空覆盖必须可见)。', - '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举。', - '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids。', + '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无阻断性 envError + 本 FE 覆盖非空)| `red`。', + '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数本 FE feScope**;空覆盖必须可见)。', + '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`。', + '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids;`build-failed` 时填 `rootCausePath`。', '- 做过任何自主默认 → `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。', '- 不要返回额外字段(schema 是 `additionalProperties:false`)。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', ].filter(Boolean).join('\n') } +// ---- 前端骨架占位 stage(runFrontendSkeleton 用)---- +// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A,blocker)。 +// 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(未实现 FE 路由指向 FeStub 占位) +// + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。 +// 由此 per-FE 行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点、build-failed 退化为罕见兜底。 +// feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。 +function frontendSkeletonPrompt(feItems) { + const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' + return [ + '# fe-skeleton — 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位)', + '', + featureStageContract('frontend'), + '', + '## 目标', + '在逐 FE 实现开始**之前**,一次性建出前端「可构建可起」的骨架:App 外壳 + router **全量** lazy 路由表(每个 FE 路由都声明,未实现的指向占位组件 `FeStub`)+ 不指悬空 path 的共享导航。', + '保证后续「只建了一部分 FE」的任意时刻 `vite build` / dev server 都能起、每个 FE 路由都可达(加载到占位);逐 FE 实现时再把对应路由的 import 从 `FeStub` 换成真组件。', + '', + `## 本前端阶段 FE 清单(router 全量路由表必须覆盖的全部 FE)`, + `- ${list}`, + '', + '## 收集上下文(确定技术栈 + 目录约定 + 路由)', + `- \`${ROOT}/docs/04-技术规范.md § 零\`(\`frontend.ui_lib\` / framework / 构建工具)+ \`§ 二 前端规范\`(§ 2.1 目录约定 = 落盘位置 / 路由库 / 入口文件名)。`, + `- \`${ROOT}/docs/08-模块任务管理.md § 三\`(前端阶段元数据 + \`功能:\` 下全部 \`FE-NN\` 行;与上面清单核对,以本提示给出的清单为准)。`, + `- \`${ROOT}/docs/01-需求清单/\` 各 FE 关联 REQ + \`${ROOT}/prototype/\`(页面/路由结构权威)+ \`${ROOT}/docs/05-API接口契约.md\`,据此推导每个 FE-NN 对应的**路由 path**(带参动态路由保留 \`:id\` 占位)。`, + `- 用 Grep 在 \`${ROOT}/frontend/\` 探测现有 App 外壳 / 入口 / router 是否已存在(幂等:已存在则按需补齐,不重复创建/不覆盖已实现的真组件)。`, + '', + '## 产出(全部落在 `frontend/` 路径内——遵守前端阶段路径作用域护栏)', + '1. **App 外壳 + 入口**:`frontend/src/App.*` 与入口 `frontend/src/main.*`(按 framework / docs/04 约定的扩展名;不存在才创建)。挂载共享布局 + ``(或等价 outlet)。', + '2. **router 全量路由表**(按 docs/04 § 2.1 约定的路由文件位置,如 `frontend/src/router/index.*`):', + ' - **每个** FE-NN 对应路由都声明,**全部用 lazy import**(`component: () => import(...)` 或 framework 等价的动态 import;**绝不** eager `import X from ...` 顶部静态引入,否则未建组件会让整表编译失败)。', + ' - **未实现的 FE 路由全部指向占位组件 `FeStub`**:`component: () => import("../views/_stub/FeStub.vue")`(或 framework 等价)。逐 FE 实现后由 tdd stage 把对应路由 import 换成真组件。', + ' - 路由 path 取自上面推导的 FE→path 映射;带参路由用 `:id` 等占位。', + '3. **占位组件 `FeStub`**:`frontend/src/views/_stub/FeStub.vue`(framework 非 Vue 时落对应等价文件,如 `FeStub.tsx`),最小渲染一个带 `data-fe-stub` 属性的元素(如 `
占位
`;行为门据 `data-fe-stub` 识别占位态)。**不实现任何业务逻辑**。', + '4. **共享布局/导航**:导航链接**全部指向已在 router 声明的路由 path**(不指向任何不存在的 path),保证任意时刻无悬空链接。', + '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', + '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', + '', + '## 自检(可构建)', + '- 推断本项目前端 build / typecheck 命令(docs/04 § 零 / `frontend/package.json` scripts)。若可在子会话内安全跑(不挂死),**派 Agent 子会话**跑一次 build / dev-server 就绪探测确认骨架可构建可起;不可行则至少静态核对「全部 FE 路由已声明 + 全 lazy + 导航无悬空 path + FeStub 存在」。', + '- 占位符扫描:`TBD` / `TODO` / `【人工填写:】` → 命中即修。', + '', + commitBlock('frontend/', 'feat(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位', + '- commit 失败 → halt,把 stderr 摘要写进 reason。'), + '', + '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', + '- 成功:`{ "status": "ok", "summary": "<已声明的 FE 路由数 / 入口与 router 文件路径摘要>" }`(artifactPath 可省)。', + '- 任一护栏 / 缺值(如无法推导某 FE 的路由 path 且无任何旁证)→ `{ "status": "halt", "reason": "<具体阻塞点>" }`。', + '- 做过自主默认 → `decisions[]` 逐条登记;schema 是 `additionalProperties:false`,不要返回额外字段。', + ].filter(Boolean).join('\n') +} + +// fe-skeleton 幂等判定:检测 router 是否已声明本阶段全部 FE 路由(全量 + 全 lazy)。 +// fe-skeleton-done tag 是首选 ground truth(下面 runFrontendSkeleton 先查 tag);此 prompt 用于 tag 缺失时 +// 的二次确认(resume / tag 被手工删除场景),避免无谓重建已建好的骨架。 +function frontendSkeletonStatePromptM(feItems) { + const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(无)' + return [ + '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', + microStepContract(), + '', + `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),且占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在。`, + '- 全部满足(骨架已建齐)→ `{ "exists": true }`', + '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub)→ `{ "exists": false }`', + '## 输出(EXISTS_SCHEMA)', + ].join('\n') +} + // ---- 微步骤 prompt builders(runBranchSetup / runMilestone / runCrossModule 用)---- // 每个 prompt 单职责、短文本;返回严格 schema;执行(action)步统一返回 ACTION_RESULT_SCHEMA。 function microStepContract() { @@ -639,7 +751,12 @@ function microStepContract() { // ============================================================================ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环) -const BEHAVIOR_GATE_PASS_MAX = ADJUDICATE_MAX * 4 // 行为门 ①②③ 整体收敛轮上限:每次文字层 retry 跳回从头过硬门,超出则确定性 halt(防无限循环) +// per-FE 行为子门预算(二维,钉死防证据覆盖;设计 §6.4): +// - BEHAVIOR_FE_MAX = approve 子门内的行为 fix 轮硬上限(每 FE);超限 throw HALT。**不**复用 review 的 10 轮、 +// **不**让 REVIEW_HARD_ROUNDS × 行为重试隐式相乘——典型一次过(1 轮),最坏 3 轮。 +// - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 +const BEHAVIOR_FE_MAX = 3 +const BEHAVIOR_ATTEMPT_MAX = 2 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 @@ -978,6 +1095,19 @@ function createTagPromptM(phaseId, fe) { ].join('\n') } +// fe-skeleton-done:前端骨架占位 stage 的幂等真值 tag(runFrontendSkeleton resume 跳过用)。 +function createFeSkeletonTagPromptM() { + return [ + '# 打 annotated tag `fe-skeleton-done`(前端骨架占位已建,幂等真值)', + microStepContract(), + '', + `先用 \`git -C ${ROOT} tag -l fe-skeleton-done\` 检查;已存在则视为成功(幂等)直接返回 success。`, + `否则跑 \`git -C ${ROOT} tag -a fe-skeleton-done -m "chore(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位已建"\`。`, + '## 输出(ACTION_RESULT_SCHEMA)', + '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "" }`', + ].join('\n') +} + function findReportPromptM(phaseId) { return [ `# 找最新的 \`${phaseId}\` 完成报告并读取 § ⑫ 的 milestone tag 字段当前值`, @@ -1093,7 +1223,7 @@ function reportPrompt(module) { '## 前置', `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, fe - ? `- **验证上游 behavior-gate(前端行为门)绿**:Glob \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须非 RED**(status:green / 无 envError);最后一份 red 立即 halt。各 attempt 的 flake / 环境 race / 文字 continue 记录纳入 § ⑤ 汇总。` + ? `- **前端行为验收已并入 per-FE review 循环**(reviewer approve 子门,行为 green 是 \`req-done/\` 的前置真值)——report **不再**校验阶段级 behavior-gate 文件(已不再产生)。**对每个 \`req-done/\` tag 即视为该 FE 行为已过**(避免双真值)。可选轻量校验:每个 FE 存在对应 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/--behavior-r*-a*.md\` 且最后一份非 RED;缺证据不 halt(仅在 § ⑤/⑧ 标注)。` : '', '', '## 收集输入(取摘要而非正文)', @@ -1103,8 +1233,8 @@ function reportPrompt(module) { `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', - `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把 \`frontend-phase-behavior-gate-r*.md\` 各 attempt(按序)的 flake / 环境 race(envError)/ 文字 continue 记录一并纳入 § ⑤ 汇总**。`, - `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外纳入 behavior-gate 报告的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, + `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/-FE-*-behavior-r*-a*.md\`(按 FE → behaviorRound → attempt 排序)的 flake / 环境 race(envError,含 build-failed 短路)/ 文字 continue 记录一并纳入 § ⑤ 汇总**。`, + `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/-FE-*-behavior-r*-a*.md\` 汇总各 FE 的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', ].join('\n') : [ @@ -1274,6 +1404,39 @@ async function runCrossModule(module) { log(`cross-module-log: 模块 ${id} 更新 ${classified.crossModule.length} 行`) } +// ---- runFrontendSkeleton:前端骨架占位 stage 的 JS 编排(设计 § 2,前置依赖 A)---- +// 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(FeStub 占位)+ 无悬空导航。 +// 幂等(resume 安全):先查 git tag `fe-skeleton-done`,已存在则 skip;tag 缺失时再二次确认 router 是否已声明 +// 全 FE 路由(手工删 tag / 残留场景),已建则补打 tag 后 skip;都未建才派子代理生成,成功后打 tag。 +async function runFrontendSkeleton(feItems) { + const lbl = (k) => `fe-skeleton:${k}` + + // step 1: tag 幂等(首选 ground truth) + const tag = await agent(checkTagExistsPromptM('fe-skeleton-done'), + {label: lbl('tag?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) + if (tag.exists) { log('fe-skeleton: tag fe-skeleton-done 已存在,跳过骨架生成'); return } + + // step 2: tag 缺失时二次确认 router 是否已声明全 FE 路由(手工删 tag / resume 残留);已建则只补打 tag。 + const state = await agent(frontendSkeletonStatePromptM(feItems), + {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) + if (state.exists) { + log('fe-skeleton: router 已声明全部 FE 路由(tag 缺失但骨架已建),补打 fe-skeleton-done tag') + await runAction(g => createFeSkeletonTagPromptM() + g, + {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag-backfill')}) + return + } + + // step 3: 派子代理生成骨架(成功后子代理自行 commit;此处仅经 runStage 仲裁 halt 收敛)。 + await runStage(g => frontendSkeletonPrompt(feItems) + g, + {site:'fe-skeleton', grp:'Frontend', label: lbl('gen')}) + + // step 4: 打 fe-skeleton-done tag(幂等真值,resume 跳过)。 + await runAction(g => createFeSkeletonTagPromptM() + g, + {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag')}) + + log(`fe-skeleton: 已生成前端骨架(覆盖 ${(feItems || []).length} 个 FE 路由),打 fe-skeleton-done tag`) +} + // ============================================================================ // 编排逻辑(结构按 plan 骨架;featureLoop / reviewWithFixLoop / testGate / 顶层循环) // ============================================================================ @@ -1356,7 +1519,7 @@ async function featureLoop(items, phase) { // reverify 经 runStage 但 allowContinue:false(复验红色 = 修复没生效,绝不放行)。 // - approve 后的 docs/08 checkbox 是纯可视化副作用(req-done tag 才是完成真值),缺失/写失败一律 log 跳过不 halt。 const REVIEW_SOFT_ROUNDS = 5 -const REVIEW_HARD_ROUNDS = 8 +const REVIEW_HARD_ROUNDS = 10 // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 async function flipDocs08Checkbox(fe, id, phase, grp) { @@ -1374,6 +1537,9 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { let lastVerify = verifyResult let lastIssuesCount = 0 let reviewGuidance = '' // 仲裁 retry 时注入下一轮 review 的纠正指令 + // softPassed 提升到 reviewWithFixLoop 顶层作用域(与本 FE review 同寿命,跨 behaviorRound 持久)—— + // 行为软文字一旦被仲裁 continue 放行(降级),重跑后即便仍在 textIssues 也不再追问,避免反复消耗仲裁预算。 + const behaviorSoftPassed = new Set() for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 @@ -1384,6 +1550,13 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { reviewGuidance = '' // 已消费 if (r.verdict === 'approve') { + // approve 闸显式 AND(设计 §6.2):reviewer.verdict==='approve' ∧ behaviorSubGate green(仅前端)。 + // 后端逐字不变(无行为维度);前端:静态 approve 后**不立即 return**,先进 per-FE 行为 approve 子门—— + // 起本 FE 全栈验「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才放行; + // 行为 green ⇒ 才 flipDocs08Checkbox + return(req-done tag 落点 featureLoop 不动,语义自动升级为「静态过+行为过」)。 + if (fe) { + await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) + } await flipDocs08Checkbox(fe, id, phase, grp) return { id, phase, approved:true, rounds:round } } @@ -1397,7 +1570,11 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`, { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)', reviewerIssues: r.issues || [] }, grp, round) - if (verdict.action === 'continue') { await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } } + // continue 视为「无 must-fix → 静态 approve」——前端仍须先过行为 approve 子门(行为 green 是任何 approve return 的前置)。 + if (verdict.action === 'continue') { + if (fe) await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) + await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } + } if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`) reviewGuidance = verdict.guidance || '' // retry:带 guidance 重判(进入下一轮) continue @@ -1449,136 +1626,170 @@ async function testGate(module, phase) { return g } -// ---- 前端行为门控制流(runBehaviorGate)---- -// 设计:docs/design/2026-06-02-frontend-behavior-gate.md § 4。 -// 仅 frontend-phase 触发(入口二次保险);每 attempt 独立 .tmp 子目录(门子代理负责清/写)。 -// 失败分层: -// - envError != none(端口/起栈未就绪/种子/鉴权/超时)= 环境 race:同 testGate 跑 attempt=2;仍 envError → -// adjudicate(allowContinue:false, retry/halt);retry 再起独立 attempt;绝不当死控件。 -// - 空覆盖(controlsEnumerated==0 || routesReached==0)→ 绝不 green,归 env race 走 retry/halt。 -// - interactionFailures(含 binding-garbage)= 交互硬边界:attempt=1 出现不立刻 throw,先跑 attempt=2 辨 flake; -// 仍非空 → adjudicate(allowContinue:false, retry/halt),绝不 continue。 -// - textIssues(软边界):逐条 for-of —— source=='sentinel' → adjudicate(allowContinue:false)(客观 bug,只许 retry/halt); -// source∈{i18n,literal,semantic} → adjudicate(allowContinue:true)(continue 时 recordDecisions 记入 autonomousDecisions)。 -// - coverageGaps:写证据 + recordDecisions,不单独 halt。 -// RED 在 milestone tag 前 throw 冒泡到顶层 try/catch → break,绝不带红进里程碑。 -async function runBehaviorGate(module) { - // 入口二次保险:仅 frontend-phase 聚合模块跑行为门(同 runMilestone / reportPrompt 惯例)。 - const fe = module?.id === 'frontend-phase' - if (!fe) { log(`behavior-gate: ${module?.id ?? ''} 非 frontend-phase,跳过行为门`); return } - const lbl = (a) => `behavior:frontend-phase:r${a}` +// ---- 前端 per-FE 行为验收控制流(runBehaviorGateOnce + behaviorSubGate)---- +// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 6.3 / 7。 +// 行为验收并入 per-FE reviewWithFixLoop 的 approve 子门——reviewer 即将 approve 时才触发,绝不每 review round 起栈。 +// behaviorSubGate 失败分层(per-FE 缩 scope,保留原 runBehaviorGate 的分层语义): +// - build-failed(兄弟 FE 未实现 / 占位未覆盖,根因落非本 FE 路径)= 确定性短路:记 coverageGap + decisions, +// 本轮行为门视为「本 FE 非缺陷」直接放行 approve(预期中途态,不 retry 不 halt)。 +// - envError(其它) / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。 +// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 approve。 +// - behaviorHard = interactionFailures + sentinel textIssues:有 locator → 降维喂 fixPrompt 跑 fix(fix 后功能 reverify + 下一轮重跑行为); +// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不 approve。 +// - BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。 + +// envBlocked / ifails:per-FE bg 的环境/空覆盖与交互失败判定(build-failed 不计 envBlocked——它走确定性短路分支)。 +function behaviorEnvBlocked(r) { + const k = r.envError && r.envError.kind + const ev = (k && k !== 'none' && k !== 'build-failed') ? r.envError : null + const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0) + return { ev, emptyCov, blocked: !!ev || emptyCov } +} +function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] } +// runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 +// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。 +// behaviorRound:approve 子门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 +async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) { + const lbl = (a) => `behavior:${id}:r${behaviorRound}:a${a}` let attempt = 1 - let bg = await agent(behaviorGatePrompt(module, attempt), {label: lbl(attempt), phase:'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) - recordDecisions('behavior-gate:frontend-phase', bg.decisions) + let bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), + {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) + recordDecisions(`behavior:${id}`, bg.decisions) + + // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。 + const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed' + if (isBuildFailedShortCircuit(bg)) return bg - // 共享重跑:每次 retry 都开一个独立 attempt(独立 .tmp/r 证据),刷新 bg + 记录决策。 - // 文字层 retry 也走这里,确保重跑后能跳回 ①②③ 重新整体过硬门(见下方收敛循环),绝不拿旧快照继续。 - const rerun = async () => { + // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 + while (behaviorEnvBlocked(bg).blocked && attempt < BEHAVIOR_ATTEMPT_MAX) { attempt += 1 - bg = await agent(behaviorGatePrompt(module, attempt), {label: lbl(attempt), phase:'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) - recordDecisions('behavior-gate:frontend-phase', bg.decisions) + bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), + {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) + recordDecisions(`behavior:${id}`, bg.decisions) + if (isBuildFailedShortCircuit(bg)) return bg } - - // helper:环境 race / 空覆盖归一处理——先跑一次 flake 重试,仍异常则 adjudicate(allowContinue:false)。 - const envBlocked = (r) => { - const ev = r.envError && r.envError.kind && r.envError.kind !== 'none' ? r.envError : null - const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0) - return { ev, emptyCov, blocked: !!ev || emptyCov } + let envState = behaviorEnvBlocked(bg) + for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { + const reason = envState.ev + ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}` + : `behavior 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)` + const verdict = await adjudicate(`behavior-env:${id}`, + { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, grp, adj) + if (verdict.action !== 'retry') throw new Error(`HALT behavior-env ${id}: ${verdict.rationale || reason}`) + attempt += 1 + bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), + {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) + recordDecisions(`behavior:${id}`, bg.decisions) + if (isBuildFailedShortCircuit(bg)) return bg + envState = behaviorEnvBlocked(bg) } - const ifails = (r) => Array.isArray(r.interactionFailures) ? r.interactionFailures : [] - - // ① 环境 / 空覆盖硬门:仍异常 → 仲裁(allowContinue:false → retry/halt)。 - // 抽成闭包,便于文字层 retry 后由收敛循环重新整体校验(绝不带空覆盖 / envError 判 green)。 - const enforceEnv = async () => { - let envState = envBlocked(bg) - for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { - const reason = envState.ev - ? `behavior-gate envError=${envState.ev.kind}: ${envState.ev.detail || ''}` - : `behavior-gate 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)` - const verdict = await adjudicate('behavior-gate-env:frontend-phase', - { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, 'Behavior', adj) - if (verdict.action !== 'retry') throw new Error(`HALT behavior-gate-env frontend-phase: ${verdict.rationale || reason}`) - await rerun() - envState = envBlocked(bg) + if (envState.blocked) throw new Error(`HALT behavior-env ${id}: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) + return bg +} + +// behaviorSubGate:reviewer approve 的「行为 approve 子门」。green 才允许 reviewWithFixLoop return approve。 +// softPassed:由 reviewWithFixLoop 顶层注入,跨 behaviorRound 持久(软文字一旦放行不再追问)。 +// green ≡ behaviorHard.length===0 ∧ envError∈{none,build-failed} ∧ 本 FE 覆盖非空(或 build-failed 短路)。 +async function behaviorSubGate(id, specPath, grp, softPassed) { + const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` + for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) { + const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound) + + // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → 记 coverageGap + decisions,子门 green-by-skip 放行。 + if (bg.envError && bg.envError.kind === 'build-failed') { + recordDecisions(`behavior-build-failed:${id}`, [{ + question:`本 FE ${id} 行为验收遇 build-failed(根因 ${bg.envError.rootCausePath || '?'})`, + choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)', + rationale: bg.envError.detail || '', confidence:'low' }]) + log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${bg.envError.rootCausePath || '?'}),记证据不阻断`) + return } - if (envState.blocked) throw new Error(`HALT behavior-gate-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) - } - // ② 交互层硬门(含 binding-garbage):仍非空 → 仲裁(allowContinue:false → retry/halt),绝不 continue。 - // 每次 retry 重跑后可能新冒环境问题,由收敛循环回到 ① 兜底,避免把环境 race 当死控件。 - const enforceInteraction = async () => { - for (let adj = 1; ifails(bg).length && adj <= ADJUDICATE_MAX; adj++) { - const summary = ifails(bg).map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') - const verdict = await adjudicate('behavior-gate-interaction:frontend-phase', - { problem:`behavior-gate 交互层失败(含 binding-garbage 硬边界,绝不 continue):${summary}`, - interactionFailures: ifails(bg), allowContinue:false }, 'Behavior', adj) - if (verdict.action !== 'retry') - throw new Error(`HALT behavior-gate-interaction frontend-phase: ${verdict.rationale || summary}`) - await rerun() - await enforceEnv() // 重跑后先过 ① 环境兜底,再回到本门继续辨交互 + // 2) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 + // locator-not-resolvable(B 类硬问题反查不出)计入未覆盖——下面会因 behaviorHard 仍非空或覆盖不足而不 green。 + for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { + if (!cg) continue + recordDecisions(`behavior-coverage:${id}`, + [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }]) } - if (ifails(bg).length) - throw new Error(`HALT behavior-gate-interaction frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后交互层仍有失败`) - } - // attempt=1 出现环境/交互问题不立刻 throw——先跑独立 attempt 辨 flake,再进入硬门收敛。 - if (envBlocked(bg).blocked || ifails(bg).length) await rerun() - - // ①②③ 收敛循环:任何文字层 retry 重跑都跳回此处,重新整体校验 - // envError → 空覆盖 → interactionFailures → textIssues(while 而非 for-of 快照), - // 杜绝「文字 retry 后用旧数组继续、且新 bg 携带非空 interactionFailures/envError 滑过硬门」的逃逸。 - // softPassed:已被仲裁 continue 放行(降级)的 region,重跑后即便仍在 textIssues 也不再追问,避免死循环。 - const softPassed = new Set() - let converged = false - for (let pass = 1; pass <= BEHAVIOR_GATE_PASS_MAX && !converged; pass++) { - // ① 环境 / 空覆盖(硬门) - await enforceEnv() - // ② 交互层(硬门,含 binding-garbage) - await enforceInteraction() - - // ③ 文字层(软边界,按 source 分流):while 取当前 bg 第一条未决 textIssue。 - // source=='sentinel' → allowContinue:false(门自灌确定值,绑错字段 / 显示错是客观 bug,只许 retry/halt); - // source∈{i18n,literal,semantic} → allowContinue:true(continue 时 recordDecisions 记入决策日志)。 - const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` - const pickIssue = () => (Array.isArray(bg.textIssues) ? bg.textIssues : []) - .find(x => x && !softPassed.has(regionKey(x))) - let needRerun = false - let ti - while ((ti = pickIssue())) { - const hard = ti.source === 'sentinel' - const site = `behavior-gate-text:frontend-phase:${ti.page || '?'}:${ti.region || '?'}` + // 3) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。 + // 永不阻断 approve;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。 + let softRetry = false + for (const ti of (Array.isArray(bg.textIssues) ? bg.textIssues : [])) { + if (!ti || ti.source === 'sentinel') continue // sentinel 归 behaviorHard,不在软文字处理 + if (softPassed.has(regionKey(ti))) continue + const site = `behavior-text:${id}:${ti.page || '?'}:${ti.region || '?'}` const verdict = await adjudicate(site, - { problem:`文字不符(source=${ti.source}${hard ? ',sentinel 客观 bug 不可 continue' : ',可 continue 降级'}):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, - textIssue: ti, allowContinue: !hard }, 'Behavior', pass) - if (verdict.action === 'continue' && !hard) { - // continue:把放行决策记入 autonomousDecisions(供人工事后审阅),并标记该 region 已软放行。 + { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 approve):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, + textIssue: ti, allowContinue: true }, grp, behaviorRound) + if (verdict.action === 'continue') { recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`, choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }]) - softPassed.add(regionKey(ti)) - continue // 处理同一 bg 内的下一条 + softPassed.add(regionKey(ti)); continue } + if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || `文字不符 source=${ti.source}`}`) + softRetry = true; break // retry → 重跑本 behaviorRound(跳到下一轮迭代重起整门) + } + if (softRetry) continue + + // 3.5) B 类硬问题(locator-not-resolvable coverageGap):连组件文件都反查不出,不静默放行—— + // 计入未覆盖阻断 approve,走 adjudicate(allowContinue:false) retry/halt(绝不当 green 放行,降级≠放行)。 + const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []).filter(cg => cg && cg.reason === 'locator-not-resolvable') + if (bClass.length) { + const summary = bClass.map(cg => `${cg.page} — ${cg.detail}`).join('; ') + const verdict = await adjudicate(`behavior-bclass:${id}`, + { problem:`behavior 硬问题连组件文件都反查不出(B 类,不可降级放行,计入未覆盖阻断 approve):${summary}`, + coverageGaps: bClass, allowContinue:false }, grp, behaviorRound) + if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass ${id}: ${verdict.rationale || summary}`) + continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) + } + + // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。 + const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : []) + .filter(t => t && t.source === 'sentinel') + .map(t => ({ page:t.page, control:t.region, kind:'binding-garbage', detail:`sentinel 不符 期望=${t.expected} 实际=${t.actual}`, locator:t.locator })) + const behaviorHard = [...behaviorIfails(bg), ...sentinelHard] + + // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)→ 子门 green 放行。 + if (behaviorHard.length === 0) { + log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) + return + } + + // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不 approve)。 + const withLoc = behaviorHard.filter(x => typeof x.locator === 'string' && x.locator.trim()) + const noLoc = behaviorHard.filter(x => !(typeof x.locator === 'string' && x.locator.trim())) + if (noLoc.length) { + const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') + const verdict = await adjudicate(`behavior-noloc-hard:${id}`, + { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue/approve):${summary}`, + interactionFailures: noLoc, allowContinue:false }, grp, behaviorRound) if (verdict.action !== 'retry') - throw new Error(`HALT ${site}: ${verdict.rationale || `文字不符 source=${ti.source}`}`) - // retry:重跑整门取最新判定,并跳回 ①②③ 重新整体过全部硬门(绝不拿旧快照继续)。 - await rerun() - needRerun = true - break + throw new Error(`HALT behavior-noloc-hard ${id}: ${verdict.rationale || summary}`) + continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) } - if (!needRerun) converged = true // 无 retry → 文字层已收敛(全软放行 / 全消失) - } - if (!converged) throw new Error(`HALT behavior-gate-text frontend-phase: ${BEHAVIOR_GATE_PASS_MAX} 轮收敛后文字层仍未解决`) - // ④ coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 ① 兜底)。 - for (const g of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { - if (!g) continue - recordDecisions('behavior-gate-coverage:frontend-phase', - [{ question:`覆盖缺口 ${g.page}(${g.reason})`, choice:'记录不阻断', rationale: g.detail || '', confidence:'low' }]) - } + // 7) 有 locator 的硬问题 → 降维成 {summary,locator,severity} 喂现有 fixPrompt 跑 fix(schema 不合并、fix 入参合并)。 + const fixIssues = withLoc.map(f => ({ + summary: `[behavior:${f.kind}] ${f.page}:${f.control} — ${f.detail}`, + locator: f.locator, + severity: 'high', + })) + await runStage(g => fixPrompt(id, 'frontend', fixIssues) + g, { + site:`behavior-fix:${id}:r${behaviorRound}`, grp, label:`behavior-fix:${id}:r${behaviorRound}`, + }) - if (bg.status === 'red') - throw new Error(`HALT behavior-gate-red frontend-phase: 门返回 status:red 但未归入交互/文字/环境分支——拒绝带红进里程碑`) - log(`behavior-gate: frontend-phase green(routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) + // 8) fix 后功能复验(allowContinue:false):behaviorSubGate 的 fix 改的是 frontend/ UI 源码,可能引入功能回归—— + // 先跑 scoped 组件测试 reverify(不起全栈,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。 + await runStage( + g => verifyPrompt(id, 'frontend', `(behaviorRound ${behaviorRound} 行为 fix 后功能复验,本轮 must-fix: ${fixIssues.length} 项)`, specPath, REVIEW_HARD_ROUNDS + behaviorRound) + g, + { site:`behavior-reverify:${id}:r${behaviorRound}`, grp, label:`behavior-reverify:${id}:r${behaviorRound}`, allowContinue: false }, + ) + // 进入下一 behaviorRound → 重跑本 FE 行为验收 + } + throw new Error(`HALT behavior-unresolved ${id}: ${BEHAVIOR_FE_MAX} 轮 per-FE 行为子门仍未 green(硬问题未清)`) } phase('Router') @@ -1638,11 +1849,15 @@ for (const [idx, module] of todo.entries()) { } if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块) phase('Frontend') + // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy + // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, + // 使 per-FE 行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 + await runFrontendSkeleton(module.feItems) + // 前端行为验收已并入 featureLoop→reviewWithFixLoop 的 per-FE approve 子门(reviewer approve 时起本 FE 全栈验 + // 「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才打 req-done)——不再有阶段级末尾独立行为门。 await featureLoop(module.feItems, 'frontend') phase('Gate') - await testGate(module, 'frontend') - phase('Behavior') // 前端行为门:testGate 绿后、report/milestone 前(仅 frontend-phase 聚合) - await runBehaviorGate(module) + await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright)保留,与 per-FE 行为验收职责正交 } phase('Milestone') // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— -- libgit2 0.22.2