Commit ac491fe7eccc6f50e2da3f82249ad2990e0e5604

Authored by zichun
1 parent 3d084808

workflow: 行为验收迁回阶段级(v3) + 样式层断言 + 前端测试目录隔离

行为门 v3(docs/design/2026-06-05-frontend-behavior-stage-gate.md):
- 行为验收从 per-FE review approve 子门迁回阶段末尾一次(phase Behavior,
  featureLoop 后、testGate 前),保留 fix 循环(BEHAVIOR_STAGE_MAX=3 轮,
  fix 后全量前端单测复验再重跑门)
- req-done/<FE> 语义降为「仅静态 review 过」;行为绿改由 reportPrompt 校验
  阶段级证据(module-reports/frontend-phase-behavior-r*-a*.md 最后一份非 RED)
- build-failed 取消「兄弟未实现」短路(阶段末尾全 FE 已实现);断言作用域 =
  全部 FE spec「行为验收作用域」小节并集,缺小节记 scope-missing 阻断 green
- 新增样式层 styleIssues(颜色 token 比对 + layout sanity 共 6 kind),
  降维并入 behaviorHard 与交互硬问题同口径 fix;环境仲裁透传 riders 计数

前端测试目录隔离(对齐后端 src/main↔src/test 物理分离):
- 锁定约定:单测 frontend/tests/** 镜像 src/(smoke 归 tests/__smoke__/ 且
  以 .test.* 结尾),e2e 在 frontend/e2e/,frontend/src/ 禁任何测试产物;
  vitest include 统一限定 tests/**/*.test.*
- 五层防线:docs-04 模板 §2.1 锁定约定 / planPrompt+tddPrompt 硬护栏 /
  fe-skeleton 单测基线 / code-reviewer 新增第 8 维「测试文件隔离」
- legacy 守卫:frontend/src/ 内已存在 colocated 测试时绝不收窄 include
  (防旧单测静默停跑),骨架幂等检测同步豁免,留人工迁移

经两轮多代理对抗审计(34 agents),确认项均已修复。
README.md
... ... @@ -48,12 +48,16 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。
48 48 └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag)
49 49 runBranchSetup(frontend-phase)
50 50 → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起;
51   - 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState)
  51 + 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState;
  52 + 含单测基线:vitest include 限定 tests/**/*.test.*——单测一律 frontend/tests/ 镜像 src/,
  53 + 交付源码 frontend/src/ 内禁测试文件,同后端 src/main↔src/test 物理分离)
52 54 → featureLoop(前端,FE-NN,路径限 frontend/):spec → plan → tdd → verify →
53   - review 循环内并入 per-FE 行为验收 approve 子门(reviewer approve 时才起本 FE 全栈
54   - +演示种子+sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix
55   - must-fix→重验,软文字按来源仲裁,行为 green 才打 req-done/<FE>)
56   - → testGate(frontend,全量回归 vitest+playwright,与 per-FE 行为验收职责正交)
  55 + review 循环(静态验收,approve 即打 req-done/<FE>)
  56 + → 阶段级行为门 behavior(整个前端阶段只跑一次:起全栈+演示种子+sentinel,
  57 + 按全部 FE spec 聚合的作用域并集枚举路由,交互/文字/样式三层断言;交互失效
  58 + /sentinel 错/样式违规(非 token 色、横向溢出、控件重叠等)转可 fix must-fix
  59 + →fix→单测复验→重跑门(≤3 轮),软文字按来源仲裁,green 才放行)
  60 + → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归)
57 61 → runMilestone(milestone/frontend-phase)
58 62  
59 63 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start
... ... @@ -143,7 +147,7 @@ erp-workflow-plugin/
143 147  
144 148 | Agent | 用途 | 谁调用 |
145 149 |---|---|---|
146   -| `code-reviewer` | 统一 reviewer。`phase=backend` 跑通用代码审查维度;`phase=frontend` 附加前端 7 维 checklist(prototype 一致性 / design tokens / a11y / 响应式 / 业务校验前端复刻 / API 一致性 / 状态机覆盖,主观维度仅标记明显问题不触发 request-changes)。非交互,返回结构化 verdict,绝不弹窗 | `workflows/coding.mjs` 的 review stage:`agent(..., {agentType:'erp-workflow:code-reviewer'})`(必须带 `erp-workflow:` 插件命名空间——裸 `code-reviewer` 会与其它插件的同名 agent 歧义) |
  150 +| `code-reviewer` | 统一 reviewer。`phase=backend` 跑通用代码审查维度;`phase=frontend` 附加前端 8 维 checklist(prototype 一致性 / design tokens / a11y / 响应式 / 业务校验前端复刻 / API 一致性 / 状态机覆盖 / 测试文件隔离,主观维度仅标记明显问题不触发 request-changes)。非交互,返回结构化 verdict,绝不弹窗 | `workflows/coding.mjs` 的 review stage:`agent(..., {agentType:'erp-workflow:code-reviewer'})`(必须带 `erp-workflow:` 插件命名空间——裸 `code-reviewer` 会与其它插件的同名 agent 歧义) |
147 151  
148 152 ## Templates 清单(26 份)
149 153  
... ...
agents/code-reviewer.md
... ... @@ -22,7 +22,7 @@ Cover the four standard axes — **plan-alignment** (implementation matches plan
22 22  
23 23 ## When phase=frontend, additionally
24 24  
25   -Apply the frontend 7-dimension checklist **in addition to** the generic dimensions above. Frontend scope is enforced by the tdd/fix stage hard guard; do not propose backend-path changes.
  25 +Apply the frontend 8-dimension checklist **in addition to** the generic dimensions above. Frontend scope is enforced by the tdd/fix stage hard guard; do not propose backend-path changes.
26 26  
27 27 For each dimension below, classify Critical / Important / Suggestion as above.
28 28  
... ... @@ -60,3 +60,8 @@ For each dimension below, classify Critical / Important / Suggestion as above.
60 60 ### 7. 状态机覆盖 (objective → can request-changes)
61 61 - The 5 states from the spec (loading / empty / error / 正常 / 提交中) must each be handled in code.
62 62 - Missing state handling → `request-changes` for the specific state.
  63 +
  64 +### 8. 测试文件隔离 (objective → can request-changes)
  65 +- Delivery source (`frontend/src/**`) must contain NO test artifacts: any `*.test.*` / `*.spec.*` / `__tests__/` / `__mocks__/` / `__smoke__/` introduced inside `frontend/src/` by this diff → `request-changes` (locator = the offending file; the fix is moving it to `frontend/tests/**` mirroring the `src/` relative path, per docs/04 § 2.1).
  66 +- Unit tests belong in `frontend/tests/**` (directory structure mirrors `frontend/src/**`); Playwright e2e belongs in `frontend/e2e/**`. Same separation principle as backend `src/main/java` ↔ `src/test/java` (the backend side is enforced naturally by Gradle layout; flag only if violated).
  67 +- A unit test whose path does not mirror its subject's `src/` relative path is a must-fix only when the mapping is ambiguous; otherwise note it as a suggestion.
... ...
docs/design/2026-06-02-frontend-behavior-gate.md
1   -# 前端行为门(旧阶段级设计,已作废)
  1 +# 前端行为门(v1 阶段级只读设计,已作废)
2 2  
3   -> 状态:SUPERSEDED。当前实现依据见 [`2026-06-02-frontend-behavior-in-review-loop.md`](./2026-06-02-frontend-behavior-in-review-loop.md)
  3 +> 状态:SUPERSEDED。当前实现依据见 [`2026-06-05-frontend-behavior-stage-gate.md`](./2026-06-05-frontend-behavior-stage-gate.md)(v3)
4 4  
5   -本文原先描述的是“frontend-phase 末尾跑一次、只读、red 即 halt”的阶段级行为门。该方案已被 per-FE 方案取代:行为验收并入每个 FE 的 `reviewWithFixLoop` approve 子门,行为硬问题带 locator 后进入 fix→重验循环
  5 +本文原先描述的是“frontend-phase 末尾跑一次、只读、red 即 halt”的阶段级行为门。该方案先被 v2(per-FE approve 子门 + fix 循环)取代;v3 又把触发时机迁回阶段末尾一次,但保留了 v2 的 fix→复验→重跑循环,**不是**回到本文的只读 halt 形态
6 6  
7 7 保留的历史结论:
8 8  
... ...
docs/design/2026-06-02-frontend-behavior-in-review-loop.md
1   -# 前端行为验收并入 reviewWithFixLoop(v2 最终设计:per-FE + fix 循环
  1 +# 前端行为验收并入 reviewWithFixLoop(v2 设计:per-FE + fix 循环,已作废
2 2  
3   -> 状态:可实现(ready-to-implement),含 3 项实现前置依赖。
4   -> 上游:本设计取代 `docs/design/2026-06-02-frontend-behavior-gate.md` 的「阶段级末尾只读 halt 门」形态。
  3 +> 状态:SUPERSEDED。当前实现依据见 [`2026-06-05-frontend-behavior-stage-gate.md`](./2026-06-05-frontend-behavior-stage-gate.md)(v3:行为验收回迁阶段末尾,整个前端阶段只跑一次,保留 fix 循环)。
  4 +> 本文保留作历史依据:v3 沿用了本文的失败分层 / 两层断言 / locator A-B 分级 / 作用域小节(依赖 C)/ 骨架占位(依赖 A)等机制,仅把触发时机从 per-FE approve 子门改回阶段级一次。
  5 +> 上游:本设计曾取代 `docs/design/2026-06-02-frontend-behavior-gate.md` 的「阶段级末尾只读 halt 门」形态。
5 6 > 运行时红线(不可违反):禁用 time/random builtin(`Date.now()` / `Math.random()` / `new Date()`);顶层 `return` 是结果通道;`agent/phase/parallel/log/adjudicate/recordDecisions` 是注入全局;**后端 featureLoop 分支逐字不变**。
6 7  
7 8 ---
... ...
docs/design/2026-06-05-frontend-behavior-stage-gate.md 0 → 100644
  1 +# 前端行为验收回迁阶段级(v3:阶段末尾一次 + 保留 fix 循环)
  2 +
  3 +> 状态:已实现(implemented)。
  4 +> 上游:本设计取代 [`2026-06-02-frontend-behavior-in-review-loop.md`](./2026-06-02-frontend-behavior-in-review-loop.md)(v2,per-FE approve 子门形态)。
  5 +> 运行时红线(不可违反):禁用 time/random builtin;顶层 `return` 是结果通道;`agent/phase/parallel/log` 是注入全局;**后端 featureLoop 分支逐字不变**。
  6 +
  7 +---
  8 +
  9 +## 0. 用户拍板的方向(不可推翻)
  10 +
  11 +- 行为验收**挪到前端阶段末尾**:整个 frontend-phase 只跑**一次**行为验收,不再在每个 FE 的 review 内循环(approve 子门)中运行。
  12 +- **保留 fix 循环**(用户确认):行为门发现带 locator 的硬问题 → fixPrompt 自动修复 → 前端单测复验 → 重跑门,硬上限 `BEHAVIOR_STAGE_MAX = 3` 轮;不回到 v1 的「只读 red 即 halt」。
  13 +- 时序:`featureLoop(frontend) → phase('Behavior') runBehaviorGate → phase('Gate') testGate → report → milestone`。
  14 + 行为门放在 testGate **之前**——行为 fix 会改 `frontend/` 源码,绿后由 testGate 全量回归兜底,避免回归证据过期。
  15 +
  16 +## 1. 控制流(实现级)
  17 +
  18 +```
  19 +顶层 frontend 段:
  20 + runFrontendSkeleton(feItems) # 保留(中途可构建仍是逐 FE verify(e2e) 的前提)
  21 + featureLoop(feItems, 'frontend') # review 仅静态验收;approve 即打 req-done/<FE>
  22 + phase('Behavior')
  23 + runBehaviorGate(feItems): # 阶段级行为门(原 behaviorSubGate 改造)
  24 + softPassed = Set() # 跨 behaviorRound 持久(软文字一旦 continue 不再追问)
  25 + for behaviorRound in 1..BEHAVIOR_STAGE_MAX(=3):
  26 + bg = runBehaviorGateOnce(feItems, behaviorRound) # 内部 attempt 1→2 环境重试 + 仲裁兜底
  27 + coverageGaps → recordDecisions(记录不阻断)
  28 + 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + softPassed;永不阻断 green)
  29 + B 类(locator-not-resolvable)+ scope-missing → adjudicate(allowContinue:false) retry/halt
  30 + 覆盖率对账(planned-reached-路由级gap > 0 → adjudicate(allowContinue:false))
  31 + 未分类 red → adjudicate(allowContinue:false)
  32 + behaviorHard(interactionFailures + sentinel textIssues + styleIssues)为空 → green → return
  33 + 无 locator 硬问题 → adjudicate(allowContinue:false) retry/halt
  34 + 有 locator → 降维喂 fixPrompt('frontend-phase')(一轮批量修当轮全部 must-fix,跨 FE 同一 fix 子会话)
  35 + → behaviorReverifyPrompt(全量前端单测 vitest,不跑 e2e)allowContinue:false
  36 + → 下一 behaviorRound 重跑门
  37 + 3 轮仍未 green → throw HALT behavior-unresolved
  38 + phase('Gate')
  39 + testGate(frontend) # 全量回归 vitest+playwright,兜底行为 fix 引入的回归
  40 +```
  41 +
  42 +## 2. 与 v2 的关键差异
  43 +
  44 +| 维度 | v2(per-FE approve 子门) | v3(阶段级,本设计) |
  45 +|---|---|---|
  46 +| 触发时机 | 每个 FE reviewer 判 approve 时 | featureLoop 全部 FE 完成后一次 |
  47 +| 起栈次数 | N 个 FE × (1~3) 次 | 1~3 次(整阶段) |
  48 +| 断言作用域 | 单 FE 的 feScope | 全部 FE spec「行为验收作用域」小节**并集**(路由去重、标注归属 FE) |
  49 +| `req-done/<FE>` 语义 | 静态过 ∧ 行为过 | **仅静态过**;行为 green 是 milestone 前置(reportPrompt 校验) |
  50 +| build-failed | 根因非本 FE → green-by-skip 短路 | **无短路**(阶段末尾无「兄弟未实现」)。根因在 frontend/ 源码且可定位 → `interactionFailures[js-error]`(可 fix);不可归因 → envError,跳过自动 attempt 重试直送仲裁 |
  51 +| `build-failed-sibling-unimpl` | coverageGap reason 枚举之一 | **删除**(新增 `scope-missing`:某 FE spec 缺作用域小节,与 B 类同级阻断 green) |
  52 +| FeStub 残留 | 预期中途态 | **硬缺陷**(tdd 漏做占位替换)→ `interactionFailures[no-observable-effect]`,locator=router import 行 |
  53 +| 证据路径 | `reviews/<date>-<FE>-behavior-r*-a*.md` | `module-reports/frontend-phase-behavior-r<R>-a<A>.md`(与 test-gate 命名同构;截图 → `module-reports/assets/`);fix 后复验 `frontend-phase-behavior-reverify-r<R>.md` |
  54 +| fix 后复验 | per-FE verifyPrompt(scoped 组件测试) | `behaviorReverifyPrompt`:全量前端单测(vitest),不跑 e2e |
  55 +| 轮次预算 | `BEHAVIOR_FE_MAX=3`(每 FE) | `BEHAVIOR_STAGE_MAX=3`(整阶段;每轮 fix 批量修当轮全部 must-fix) |
  56 +| UI phase 分组 | 'Frontend'(与 review 循环同组) | 独立 `'Behavior'` phase(meta.phases 恢复该项) |
  57 +
  58 +## 3. 保留不变的机制
  59 +
  60 +- **前端骨架占位**(runFrontendSkeleton)+ tddPrompt 的 FeStub→真组件占位替换:中途可构建仍是逐 FE verify(e2e) / 阶段 testGate 的前提,且让阶段门可达每个 FE 路由。
  61 +- **spec「行为验收作用域」小节 + fe-feature-review 校验**:仍是 FE→路由的确定性映射真值,阶段门据此聚合分母并把硬问题归因到 FE/组件。
  62 +- **起栈五段时序**(空库 → 后端/Flyway → 演示种子 → sentinel 种子 → 前端 headless)、step2.5 鉴权 bootstrap、两层断言(交互可观测效果白名单 + sentinel 文字)、A/B 类 locator 分级、软硬文字 source 分流、空覆盖/部分覆盖对账、未分类 red 兜底——全部沿用 v2 语义,仅 scope 从单 FE 放大为并集。
  63 +- **阶段级 testGate(全量回归)**:职责正交保留,且新增「兜底行为 fix 引入的回归」职责。
  64 +- 后端 featureLoop / 顶层 backend 段:逐字不变。
  65 +
  66 +## 4. reportPrompt(前端分支)
  67 +
  68 +- 绿前置恢复:Glob `module-reports/frontend-phase-behavior-r*-a*.md` 按 round→attempt 升序,**最后一份必须非 RED**;红或缺证据 → halt(绝不在行为红上打 milestone)。
  69 +- § ⑤:阶段级行为证据 + behavior-reverify 的 flake / envError / fix 轮数 / 文字 continue 汇总。
  70 +- § ⑧:取最后一份行为证据的逐 FE 小节,汇总 coverageGaps / textIssues continue / 逐控件判定 / authState。
  71 +
  72 +## 5. 样式层断言(第一档,后补)
  73 +
  74 +行为门 step5 在交互/文字之外新增第三层**客观样式断言**,结果落 `BEHAVIOR_GATE_SCHEMA.styleIssues`,JS 全部并入 behaviorHard(有 locator → must-fix 进 fix 循环;无 locator → noLoc 仲裁),与交互硬问题完全同口径。
  75 +
  76 +- **颜色 token 比对**:runner 解析 `src/styles/tokens.css` 的 `--color-*`,用探针元素 getComputedStyle 把任意色值归一化为 canonical rgb 集合;被检元素渲染值(color/background/border)同法归一化后比对。`non-token-color`(∉ 集合)/ `token-mismatch`(≠ spec 点名 token 的解析值)。
  77 +- **layout sanity 几何断言**:`horizontal-overflow`(scrollWidth 超 1px 容差)/ `overlap`(白名单控件 boundingBox 交叠 >4px²)/ `zero-size` / `offscreen`(scrollIntoView 后仍不可见)。
  78 +- **误报防线**:断言作用域 = 白名单控件及直接容器 + spec/prototype 点名区域,组件库深层内部元素不查;半透明/无法归一化的值不入 styleIssues 只记 decisions(宁漏勿误)。
  79 +- **与静态 review 维度 2 的关系**:正交——静态查源码 token 引用(commit 前拦截),运行时查最终渲染值(兜级联覆盖 / 组件库 prop 注入色 / 运行时 style 的漏)。
  80 +- **明确未做**(后续权衡):`misalignment` 对齐容差(“同组”无确定性定义)、AI 判图风格相似度、多断点响应式。若 `non-token-color` 实跑误报偏高,退路是单独把它降为 record-only(一行分流改动),`token-mismatch` + 几何类保持硬门。
  81 +
  82 +## 6. 残留风险(接受)
  83 +
  84 +1. **问题堆到最后**:v1 历史结论指出阶段级末尾门会把所有 FE 的行为问题堆到末尾一次性暴露,定位/修复成本高于 per-FE。已用「fix 循环 + 一轮批量修全部 must-fix + locator 含归属 FE」缓解;这是用户为省 N 次起栈成本明确接受的取舍。
  85 +2. **单子会话枚举全量路由的上下文压力**:路由/控件多时单次门会话变重;runner 程序化枚举 + 证据落盘(非全量进上下文)缓解,超限时表现为 envError/timeout 走仲裁。
  86 +3. **req-done 不再含行为语义**:resume 时已打 req-done 的 FE 不会重走静态链,但行为门每次 coding-start 重跑(milestone 未打则 Router 仍把 frontend-phase 算待跑)——行为验收无独立完成 tag,幂等性由「行为证据 + reportPrompt 校验」承载。
  87 +4. **3 轮预算对整阶段共用**:FE 多且问题分散时可能不够;每轮 fix 批量修复 + 仲裁可 halt 转人工,未做自动扩轮(保持预算钉死、防空转)。
... ...
skills/coding/coding-start/SKILL.md
... ... @@ -22,10 +22,11 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *)
22 22 后端功能循环 spec → plan → tdd → verify → review(≤5轮)
23 23 后端测试闸 test-gate(RED 自动重试 1 次,仍 RED → halt)
24 24 前端骨架占位 router 全量 lazy 路由表 + FeStub 占位(保证中途可构建)
25   - 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/);review 循环内含
26   - per-FE 行为验收(reviewer approve 时起本 FE 全栈验「按钮真生效/文字对」,
27   - 硬问题可 fix 重验,行为 green 才打 req-done;不再是末尾独立门)
28   - 前端测试闸 test-gate(全量回归)
  25 + 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/);review 仅静态验收,
  26 + approve 即打 req-done
  27 + 前端行为门 阶段级 behavior(整个前端只跑一次:起全栈验「按钮真生效/文字对/样式合规」,
  28 + 硬问题可 fix→单测复验→重跑门(≤3 轮),green 才进测试闸)
  29 + 前端测试闸 test-gate(全量回归,兜底行为 fix 引入的回归)
29 30 跨模块记录 → 模块报告 → 里程碑(merge --no-ff + milestone/<id> tag)
30 31 任一模块 halt → fail-fast 停在该模块,修复后重跑本入口即可续跑
31 32  
... ...
skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md
... ... @@ -19,6 +19,12 @@
19 19  
20 20 ### 2.1 目录约定
21 21  
  22 +**测试目录隔离(锁定约定,生成时原样保留,项目专属布局写在其后)**:
  23 +- 前端交付源码 = `frontend/src/**`,**不含任何测试文件**(对齐后端 `src/main/java` ↔ `src/test/java` 的物理分离)。
  24 +- 前端单测(vitest/jest 组件测试)一律放 `frontend/tests/**`,目录结构**镜像** `frontend/src/**`(如 `src/components/AppShell.tsx` ↔ `tests/components/AppShell.test.tsx`);smoke 类测试归 `frontend/tests/__smoke__/`,**文件名同样以 `.test.*` 结尾**(如 `tests/__smoke__/app.smoke.test.ts`,否则不被 vitest include 匹配)。
  25 +- 前端 e2e(Playwright)一律放 `frontend/e2e/**`。
  26 +- **禁止** `frontend/src/**` 内出现 `*.test.*` / `*.spec.*` / `__tests__/` / `__mocks__/` / `__smoke__/`;vitest 配置 `include` 限定 `tests/**/*.test.*`(src 内测试残留不被执行,约定漂移立即可见;tests/ 下的 helpers/fixtures 不带 `.test.` 中缀即不被当测试加载)。
  27 +
22 28 ### 2.2 状态管理
23 29  
24 30 ### 2.3 请求封装
... ...
workflows/coding.mjs
... ... @@ -9,10 +9,11 @@ export const meta = {
9 9 description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.',
10 10 phases: [
11 11 { title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' },
12   - { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' },
  12 + { title: 'Behavior' }, { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' },
13 13 ],
14   - // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门,
15   - // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。
  14 + // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次**
  15 + // 行为验收(含 fix 循环),不再在每个 FE 的 review 循环内做 approve 子门(v2 per-FE 形态已撤销)。
  16 + // 时序:featureLoop(frontend) → Behavior(行为门+fix)→ Gate(testGate 全量回归,兜底行为 fix 引入的回归)。
16 17 }
17 18  
18 19 const ROUTER_SCHEMA = { type:'object', additionalProperties:false,
... ... @@ -67,18 +68,18 @@ const GATE_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
67 68 required:['status'], properties:{ status:{type:'string',enum:['green','red']},
68 69 failures:{type:'array',items:{type:'string'}} } }
69 70  
70   -// BEHAVIOR_GATE_SCHEMA:前端行为门(per-FE behavior 子门)返回。
71   -// 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 覆盖率 / 环境错误分别结构化,
72   -// JS 据 source/kind 分流(交互硬边界转 must-fix,文字按 source 二分 allowContinue,envError 走 retry,
73   -// build-failed 确定性短路)。设计:见 docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 3/6/7。
  71 +// BEHAVIOR_GATE_SCHEMA:前端行为门(阶段级,frontend-phase 末尾一次)返回。
  72 +// 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 样式层 / 覆盖率 / 环境错误分别结构化,
  73 +// JS 据 source/kind 分流(交互/样式硬边界转 must-fix,文字按 source 二分 allowContinue,envError 走 retry)。
  74 +// 设计:见 docs/design/2026-06-05-frontend-behavior-stage-gate.md(v3,取代 per-FE approve 子门形态)。
74 75 const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false,
75 76 required:['status','routesPlanned','routesReached','controlsEnumerated'], properties:{
76 77 status:{type:'string', enum:['green','red']},
77   - routesPlanned:{type:'integer'}, // 本 FE 关联路由数(覆盖率分母来源;per-FE 只数 feScope.routes,不数 router 全部)
78   - routesReached:{type:'integer'}, // 实际带鉴权加载成功的本 FE 路由数
79   - controlsEnumerated:{type:'integer'}, // live 枚举到的本 FE 白名单控件数(空覆盖必须可见)
  78 + routesPlanned:{type:'integer'}, // 覆盖率分母 = 全部 FE spec「行为验收作用域」小节关联路由的并集(去重)
  79 + routesReached:{type:'integer'}, // 实际带鉴权加载成功的路由数
  80 + controlsEnumerated:{type:'integer'}, // live 枚举到的白名单控件数(全 FE 并集;空覆盖必须可见)
80 81 authState:{type:'string'}, // 以何角色登录 / 覆盖角色 / 未覆盖角色集
81   - // interactionFailures.locator:行为硬问题的源码定位(组件文件 [+ DOM 描述])。per-FE 行为门必须反查到
  82 + // interactionFailures.locator:行为硬问题的源码定位(组件文件 [+ DOM 描述])。行为门必须反查到
82 83 // 组件文件路径才能转 must-fix 喂 fix;反查不出(B 类)→ 不入 interactionFailures,归 coverageGap(不放行)。
83 84 // 交互层硬边界:no-observable-effect / js-error / console-error / missing-docs05-call / binding-garbage
84 85 interactionFailures:{ type:'array', items:{ type:'object', additionalProperties:false,
... ... @@ -96,17 +97,35 @@ const BEHAVIOR_GATE_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
96 97 expected:{type:'string'}, actual:{type:'string'},
97 98 source:{type:'string', enum:['sentinel','i18n','literal','semantic']},
98 99 locator:{type:'string'} } } },
  100 + // styleIssues:样式/布局客观断言(颜色 token 比对 + layout sanity)。全部客观、可 fix——
  101 + // 有 locator → JS 并入 behaviorHard 转 must-fix;无 locator → 与交互硬问题同口径走 noLoc 仲裁。
  102 + // 不确定项(半透明混合 / 无法归一化)按 prompt 约定不入此数组,记 decisions(宁漏勿误)。
  103 + styleIssues:{ type:'array', items:{ type:'object', additionalProperties:false,
  104 + required:['page','element','kind','expected','actual'],
  105 + properties:{
  106 + page:{type:'string'}, element:{type:'string'},
  107 + kind:{type:'string', enum:[
  108 + 'non-token-color', // 渲染色 ∉ tokens.css 色值集合(限项目自有样式作用域)
  109 + 'token-mismatch', // 应取某 token 但渲染值 ≠ 该 token 解析值(被硬编码/级联覆盖)
  110 + 'horizontal-overflow', // 路由页面出现横向滚动条(容差 1px)
  111 + 'overlap', // 白名单控件 bounding box 相互重叠(双方可见可点)
  112 + 'zero-size', // 预期可见的白名单控件渲染为 0 尺寸
  113 + 'offscreen']}, // scrollIntoView 后仍不在视口内
  114 + expected:{type:'string'}, actual:{type:'string'},
  115 + locator:{type:'string'} } } },
99 116 // 覆盖率缺口:写证据 + recordDecisions,不单独 halt(空覆盖由 controlsEnumerated==0 兜底)
100   - // build-failed-sibling-unimpl:兄弟 FE 未实现导致本 FE 之外路由/组件编译缺件(预期中途态,不归本 FE 缺陷)
101   - // locator-not-resolvable:行为硬问题连组件文件都反查不出(B 类),计入未覆盖阻断 approve,不静默放行
  117 + // locator-not-resolvable:行为硬问题连组件文件都反查不出(B 类),计入未覆盖阻断 green,不静默放行
  118 + // scope-missing:某 FE spec 缺「行为验收作用域」小节(该 FE 路由不在分母)——与 B 类同级阻断 green
102 119 coverageGaps:{ type:'array', items:{ type:'object', additionalProperties:false,
103 120 required:['page','reason','detail'],
104 121 properties:{
105 122 page:{type:'string'},
106   - reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed','build-failed-sibling-unimpl','locator-not-resolvable']},
  123 + reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed','locator-not-resolvable','scope-missing']},
107 124 detail:{type:'string'} } } },
108   - // 环境错误(与业务断言失败严格区分):none 表示无环境问题;build-failed 是确定性短路(既不 retry 也不 halt)。
109   - // build-failed 时 rootCausePath 写报错根因文件路径——落在非本 FE 路径=兄弟未实现(短路放行),落在本 FE=真构建 bug。
  125 + // 环境错误(与业务断言失败严格区分):none 表示无环境问题。
  126 + // build-failed:阶段末尾全部 FE 已实现,不再有「兄弟未实现」短路——根因落在 frontend/ 源码且可定位 →
  127 + // 应归 interactionFailures[kind="js-error"](带 locator,可 fix);仅根因不可归到 frontend/ 源码
  128 + // (依赖/环境/无法定位)才用本 kind(确定性失败,跳过自动 attempt 重试直送仲裁)。rootCausePath 写报错根因文件路径。
110 129 envError:{ type:'object', additionalProperties:false,
111 130 required:['kind'],
112 131 properties:{
... ... @@ -300,7 +319,7 @@ function deriveSpecPrompt(id, phase) {
300 319 fe
301 320 ? [
302 321 '',
303   - '## 行为验收作用域结构化小节(per-FE 行为门唯一断言依据,**强制写到 spec 头部**)',
  322 + '## 行为验收作用域结构化小节(阶段末尾行为门按全部 FE 聚合断言作用域的唯一来源,**强制写到 spec 头部**)',
304 323 '- 在 spec 文件头部(紧随标题/关联 REQ 之后)写一个**结构化小节**,标题逐字为 `## 行为验收作用域`,内含两条机器可读清单:',
305 324 ' ```',
306 325 ' ## 行为验收作用域',
... ... @@ -308,8 +327,8 @@ function deriveSpecPrompt(id, phase) {
308 327 ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]',
309 328 ' ```',
310 329 `- **关联路由**:从 \`${ROOT}/frontend/\` router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 \`:id\` 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`,
311   - '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外 / 共享控件归 coverageGap,绝不算本 FE 缺陷。',
312   - '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。',
  330 + '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外控件记证据不算缺陷。',
  331 + '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes;阶段末尾的行为门会聚合**全部 FE** 的该小节作为整体断言作用域,缺失的 FE 会被记 `scope-missing` 阻断 green);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。',
313 332 ].join('\n')
314 333 : '',
315 334 '',
... ... @@ -351,7 +370,7 @@ function planPrompt(id, phase, specPath) {
351 370 '## 任务结构(每个 task = 一个 red-green-commit 单元,4 step)',
352 371 '1. 写失败测试(给 `test_file::test_name` + 测试意图);2. 实现最小代码(给 `impl_file`);3. 子会话验证 PASS;4. commit。任务粒度 2-5 分钟。',
353 372 fe
354   - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头;命中 - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头backend/- ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头 / - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头sql/- ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头 / - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头scripts/- ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头 → 修正后重渲染。`
  373 + ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头且**不得**是测试文件(\`*.test.*\` / \`*.spec.*\`,也不得落在 \`__tests__/\` / \`__mocks__/\` / \`__smoke__/\` 目录);\`test_file\` 必须以 \`frontend/tests/\`(jsdom 单测,目录镜像 \`frontend/src/\` 相对路径)或 \`frontend/e2e/\`(Playwright)开头,**绝不**把测试文件计划进 \`frontend/src/\`(交付源码与测试物理分离,见 docs/04 § 2.1);命中 + ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头且**不得**是测试文件(\`*.test.*\` / \`*.spec.*\`,也不得落在 \`__tests__/\` / \`__mocks__/\` / \`__smoke__/\` 目录);\`test_file\` 必须以 \`frontend/tests/\`(jsdom 单测,目录镜像 \`frontend/src/\` 相对路径)或 \`frontend/e2e/\`(Playwright)开头,**绝不**把测试文件计划进 \`frontend/src/\`(交付源码与测试物理分离,见 docs/04 § 2.1)backend/+ ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头且**不得**是测试文件(\`*.test.*\` / \`*.spec.*\`,也不得落在 \`__tests__/\` / \`__mocks__/\` / \`__smoke__/\` 目录);\`test_file\` 必须以 \`frontend/tests/\`(jsdom 单测,目录镜像 \`frontend/src/\` 相对路径)或 \`frontend/e2e/\`(Playwright)开头,**绝不**把测试文件计划进 \`frontend/src/\`(交付源码与测试物理分离,见 docs/04 § 2.1) / + ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头且**不得**是测试文件(\`*.test.*\` / \`*.spec.*\`,也不得落在 \`__tests__/\` / \`__mocks__/\` / \`__smoke__/\` 目录);\`test_file\` 必须以 \`frontend/tests/\`(jsdom 单测,目录镜像 \`frontend/src/\` 相对路径)或 \`frontend/e2e/\`(Playwright)开头,**绝不**把测试文件计划进 \`frontend/src/\`(交付源码与测试物理分离,见 docs/04 § 2.1)sql/+ ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头且**不得**是测试文件(\`*.test.*\` / \`*.spec.*\`,也不得落在 \`__tests__/\` / \`__mocks__/\` / \`__smoke__/\` 目录);\`test_file\` 必须以 \`frontend/tests/\`(jsdom 单测,目录镜像 \`frontend/src/\` 相对路径)或 \`frontend/e2e/\`(Playwright)开头,**绝不**把测试文件计划进 \`frontend/src/\`(交付源码与测试物理分离,见 docs/04 § 2.1) / + ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头且**不得**是测试文件(\`*.test.*\` / \`*.spec.*\`,也不得落在 \`__tests__/\` / \`__mocks__/\` / \`__smoke__/\` 目录);\`test_file\` 必须以 \`frontend/tests/\`(jsdom 单测,目录镜像 \`frontend/src/\` 相对路径)或 \`frontend/e2e/\`(Playwright)开头,**绝不**把测试文件计划进 \`frontend/src/\`(交付源码与测试物理分离,见 docs/04 § 2.1)scripts/+ ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头且**不得**是测试文件(\`*.test.*\` / \`*.spec.*\`,也不得落在 \`__tests__/\` / \`__mocks__/\` / \`__smoke__/\` 目录);\`test_file\` 必须以 \`frontend/tests/\`(jsdom 单测,目录镜像 \`frontend/src/\` 相对路径)或 \`frontend/e2e/\`(Playwright)开头,**绝不**把测试文件计划进 \`frontend/src/\`(交付源码与测试物理分离,见 docs/04 § 2.1) → 修正后重渲染。`
355 374 : `- **硬护栏**:任务粒度限定后端文件(controller / service / repository / DTO / 校验 / SQL migration);**禁止**生成 \`frontend/\` 路径任务。`,
356 375 '- 允许写死的少数场景:DDL / migration 语句、合同级常量(错误码 / JWT claim / Redis key / 路由 path / API client 签名 / Design Tokens 名)、可选的测试断言 sketch。其余一律散文 + 签名描述。',
357 376 '- 首次出现的类 / 方法 / 组件 / hook / API client 函数必须给出签名;跨 task 的签名 / 错误码 / props 类型必须一致。',
... ... @@ -388,13 +407,13 @@ function tddPrompt(id, phase, planPath) {
388 407 fe ? '' : '- **Schema 改动前置**(仅当 plan 声明需要):第一个任务写 migration 文件 `V<n>__<snake_case>.sql`(`<n>` = 现有 `sql/migrations/V*.sql` 最大版本号 + 1,只含 DDL),**同步**把新 CREATE / ALTER 反向更新到 `docs/03-数据库设计文档.md` 对应表小节(docs/03 是 schema 的 SSoT),migration + docs/03 改动同一 commit。',
389 408 '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。',
390 409 fe
391   - ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。'
  410 + ? '- **测试落位(硬约定,对齐 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 复刻。'
392 411 : '',
393 412 fe
394 413 ? '- **e2e 基线约束**:e2e 跑在「空库重建 + Flyway schema + 演示种子」基线上(骨架 globalSetup 已注入 `sql/seed`,无需测试自行建库/起栈)。e2e 断言**优先**定位**演示种子已知主键行**(1000–9999)或**测试自建数据**;**禁止**「全表恰好 N 行」式依赖全局计数的脆弱断言(演示种子行数会随后续模块种子增长,全局计数断言必然 flaky)。'
395 414 : '',
396 415 fe
397   - ? `- **占位替换(保证中途可构建 + per-FE 行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 - ? `- **占位替换(保证中途可构建 + per-FE FeStub- ? `- **占位替换(保证中途可构建 + per-FE 。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 - ? `- **占位替换(保证中途可构建 + per-FE FeStub- ? `- **占位替换(保证中途可构建 + per-FE 改为本 FE 真组件(用 Grep 在 - ? `- **占位替换(保证中途可构建 + per-FE ${ROOT}/frontend/- ? `- **占位替换(保证中途可构建 + per-FE router 定位本 FE 路由 path 的 import 行;仍在 - ? `- **占位替换(保证中途可构建 + per-FE frontend/- ? `- **占位替换(保证中途可构建 + per-FE 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。`
  416 + ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 + ? `- **占位替换(保证中途可构建 + 阶段末尾FeStub+ ? `- **占位替换(保证中途可构建 + 阶段末尾。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 + ? `- **占位替换(保证中途可构建 + 阶段末尾FeStub+ ? `- **占位替换(保证中途可构建 + 阶段末尾 改为本 FE 真组件(用 Grep 在 + ? `- **占位替换(保证中途可构建 + 阶段末尾${ROOT}/frontend/+ ? `- **占位替换(保证中途可构建 + 阶段末尾 router 定位本 FE 路由 path 的 import 行;仍在 + ? `- **占位替换(保证中途可构建 + 阶段末尾frontend/+ ? `- **占位替换(保证中途可构建 + 阶段末尾 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。`
398 417 : '',
399 418 '',
400 419 '## 护栏',
... ... @@ -470,8 +489,8 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) {
470 489 '## 输入给 reviewer',
471 490 `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`,
472 491 fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '',
473   - `- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`,
474   - fe ? `- **行为验收作用域小节校验(per-FE 行为门前置真值,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——行为门只能据此确定本 FE 路由作用域。` : '',
  492 + `- **phase = ${fe ? 'frontend → 附加前端 8 维 checklist(含 §8 测试文件隔离:本轮 diff 在 frontend/src/ 内引入任何 *.test.* / *.spec.* / __tests__ / __mocks__ / __smoke__ → must-fix,应移至 frontend/tests/ 镜像路径)。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`,
  493 + fe ? `- **行为验收作用域小节校验(阶段级行为门的作用域真值来源,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——阶段末尾的行为门按全部 FE spec 的该小节聚合断言作用域,缺失/错配会让该 FE 漏验或归因失真。` : '',
475 494 round > 1 && lastVerifySummary
476 495 ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。`
477 496 : '',
... ... @@ -639,73 +658,74 @@ function seedGenPrompt(module) {
639 658 ].filter(Boolean).join('\n')
640 659 }
641 660  
642   -// ---- 前端行为验收(per-FE behavior 子门)----
643   -// 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。
644   -// 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发,
645   -// 起本 FE 全栈 + sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才放行 approve。
  661 +// ---- 前端行为验收(阶段级行为门,v3)----
  662 +// 设计权威:docs/design/2026-06-05-frontend-behavior-stage-gate.md。
  663 +// 时机:featureLoop(frontend) 全部 FE 通过静态 review(req-done tag 已打)之后、testGate 之前,
  664 +// 整个前端阶段只跑**一次**行为验收:起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合的作用域并集
  665 +// 枚举路由控件/文字,硬问题转可 fix must-fix→fix→复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮),green 才进 testGate。
646 666 // 门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend')
647 667 // (其路径护栏命中 backend/sql/scripts 即越界硬停,与门必须运行 setup-test-db / 起后端 / 生成 SQL 种子自相矛盾)。
648 668  
649 669 // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符;
650 670 // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright,
651   -// 唯一**可写** = .tmp/behavior-gate/<FE>/r<behaviorRound>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。
  671 +// 唯一**可写** = .tmp/behavior-gate/frontend-phase/r<behaviorRound>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。
652 672 function behaviorGateContract() {
653 673 return [
654 674 '## 硬约束(非交互行为验收子代理)',
655 675 '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。',
656   - '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。',
  676 + '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「每个 FE 的每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。',
657 677 '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。',
658   - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(gradle bootRun 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate/<FE>/r<behaviorRound>/\`(spec/种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/reviews/<date>-<FE>-behavior-r<behaviorRound>-a<attempt>.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/reviews/assets/...- `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(gradle bootRun 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate/<FE>/r<behaviorRound>/\`(spec/种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/reviews/<date>-<FE>-behavior-r<behaviorRound>-a<attempt>.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/review)。`,
  678 + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(gradle bootRun 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate/frontend-phase/r<behaviorRound>/\`(种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r<behaviorRound>-a<attempt>.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/module-reports/assets/...+ `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(gradle bootRun 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate/frontend-phase/r<behaviorRound>/\`(种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r<behaviorRound>-a<attempt>.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/module-report)。`,
659 679 `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`,
660   - '- **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 缺陷。',
  680 + '- **全量终态前提(关键)**:本门跑在**全部 FE 已实现并通过静态 review 之后**——`frontend/` 不应再有未实现路由 / FeStub 占位。某路由仍渲染 `data-fe-stub` 占位 → 这是硬缺陷(tdd 漏做占位替换),归 `interactionFailures[kind="no-observable-effect"]`,locator 指向 router 文件该路由 import 行,detail 写明「路由仍指向 FeStub 占位」。**断言作用域 = 全部 FE spec 的 `## 行为验收作用域` 小节并集**;白名单外控件记证据不入断言集。',
661 681 '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。',
662 682 '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。',
663   - '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / FE id / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。',
  683 + '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。',
664 684 ].join('\n')
665 685 }
666 686  
667   -// behaviorGatePrompt:per-FE 行为验收子代理的完整流水线提示(step0-6 + schema)。
668   -// id:本 FE id(如 FE-07);specPath:本 FE spec(含 ## 行为验收作用域 小节,feScope 来源 + 日期前缀);
669   -// behaviorRound:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX);attempt:本轮内环境 race 重试序号(1..)。
670   -// 每 (FE × behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。
671   -function behaviorGatePrompt(id, specPath, behaviorRound, attempt) {
672   - const safeId = id ?? 'FE'
673   - const tmpDir = `${ROOT}/.tmp/behavior-gate/${safeId}/r${behaviorRound}`
674   - const date = (() => { try { return dateFromArtifactPath(specPath) } catch { return '<date>' } })()
675   - const evidence = `docs/superpowers/reviews/${date}-${safeId}-behavior-r${behaviorRound}-a${attempt}.md`
  687 +// behaviorGatePrompt:阶段级行为验收子代理的完整流水线提示(step0-6 + schema)。
  688 +// feItems:本前端阶段全部 FE-NN(作用域聚合的清单真值,来自 Router frontend-phase 模块);
  689 +// behaviorRound:阶段门内的行为 fix 轮(1..BEHAVIOR_STAGE_MAX);attempt:本轮内环境 race 重试序号(1..)。
  690 +// 每 (behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。
  691 +function behaviorGatePrompt(feItems, behaviorRound, attempt) {
  692 + const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(调用方未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)'
  693 + const tmpDir = `${ROOT}/.tmp/behavior-gate/frontend-phase/r${behaviorRound}`
  694 + const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md`
676 695 return [
677   - `# behavior — 前端 per-FE 行为验收(headless,FE=${safeId}, behaviorRound=${behaviorRound}, attempt=${attempt})`,
  696 + `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`,
678 697 '',
679 698 behaviorGateContract(),
680 699 '',
681 700 '## 目标',
682   - `用真实全栈运行证明本 FE \`${safeId}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`,
683   - `单个子会话内**收敛完成**:冷起栈 → 逐**本 FE 路由**枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`,
684   - `- 本 FE 行为验收作用域唯一真值 = spec \`${specPath}\` 头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先 Read 该 spec 取出 feScope;缺该小节 → \`envError.kind="stack-not-ready"\` 并在 detail 写明(不应出现:reviewer 已校验它存在)。`,
  701 + '用真实全栈运行证明**全部 FE** 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。整个前端阶段只跑这一道行为门(featureLoop 全部 FE 已过静态 review)。',
  702 + '单个子会话内**收敛完成**:冷起栈 → 逐路由枚举(全 FE 作用域并集)+ 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。',
  703 + `- 本阶段 FE 清单:${feList}。`,
  704 + `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT}/docs/superpowers/specs/<date>-<FE-NN>.md\`,同一 FE 多份取最新日期)头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先逐 FE Read 取出并**聚合为并集**(路由去重、逐路由标注归属 FE);某 FE 缺 spec 或缺该小节 → 记 \`coverageGaps[reason="scope-missing", page="<FE-NN>"]\`(该 FE 路由不计入分母,**绝不**静默跳过)。`,
685 705 behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '',
686 706 '',
687 707 '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)',
688 708 '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。',
689 709 `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1
690   - ? `本次是本 FE 首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/${safeId}/\` 目录(清掉本 FE 历史残留 runner/种子/spec),再新建本轮子目录 - ? `本次是本 FE 首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/${safeId}/\` 目录(清掉本 FE 历史残留 runner/种子/spec${tmpDir}/- ? `本次是本 FE 首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/${safeId}/\` 目录(清掉本 FE 历史残留 runner/种子/spec。`
  710 + ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子),再新建本轮子目录 + ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子${tmpDir}/+ ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子。`
691 711 : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`,
692   - `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本 FE 起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`,
  712 + `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`,
693 713 `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`,
694 714 `- \`${ROOT}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`,
695 715 '',
696   - '## step0 探测 + build 归因(确定性短路前置,依赖 build-failed kind)',
  716 + '## step0 探测 + build 归因',
697 717 `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/config-vars.yaml\`。`,
698 718 '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。',
699   - '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——',
700   - ` - 落在**非本 FE 的 \`frontend/\` 路径**(兄弟 FE 组件缺失 / 骨架占位未覆盖 / 指向未建路由)→ \`envError.kind="build-failed"\` + \`rootCausePath=<非本FE路径>\`(**预期中途态**,不是本 FE bug)。`,
701   - ' - 落在**本 FE 路径**(feScope 关联组件)→ 才是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]`(带 locator=组件文件)。',
  719 + '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——全部 FE 已实现,**没有**「兄弟未实现」豁免:',
  720 + ' - 根因落在 `frontend/` 源码且可定位到文件 → 真构建 bug → 归 `interactionFailures[kind="js-error"]`(locator=根因文件路径,可转 must-fix 喂 fix)。',
  721 + ' - 根因不可归到 `frontend/` 源码(依赖 / 工具链 / 无法定位)→ `envError.kind="build-failed"`(如能定位仍填 `rootCausePath`)。',
702 722 ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。',
703 723 '',
704   - '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)',
705   - '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。',
706   - '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。',
  724 + '## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)',
  725 + '- 分母来源 = 全部 FE spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。',
  726 + `- 与 \`${ROOT}/frontend/\` router 配置对账:FE 作用域声明但 router 缺失的路由 → \`coverageGaps[reason="unreachable-no-route"]\`;router 声明但不属任何 FE 作用域的路由记证据(不入分母、不断言)。`,
  727 + '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。',
707 728 '- 带参动态路由用**种子已知主键**实例化(可用**演示种子已知主键**(1000–9999)或 **sentinel 主键**(≥100000));无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。',
708   - '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。',
709 729 '',
710 730 '## step2 起栈五段严格时序(schema 由 Flyway 在后端启动时才建)',
711 731 `1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`,
... ... @@ -714,54 +734,89 @@ function behaviorGatePrompt(id, specPath, behaviorRound, attempt) {
714 734 '4) **此时才跑 sentinel 种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** sentinel 种子(先父后子;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)。失败 → `envError.kind="seed-error"` + 结构化根因,**不**混进交互 RED。',
715 735 ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——数值主键**一律 ≥100000**(固定区间,不再动态扫描既有键:初始数据 1–999 / 演示种子 1000–9999 已由区间约定隔离,sentinel 落 ≥100000 天然不冲突);字符串列**仍逐字段唯一编码**(`_S<NNN>` 样式,如 `CUST_NAME_S001`,抓绑错字段——演示数据已被禁用该样式,故 sentinel 独占)+ 行序号保 UNIQUE;enum 列从 docs/03 值域取并标注。断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。',
716 736 '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。',
717   - '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。',
  737 + '- `finally` **硬要求 kill 本起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。',
718 738 '',
719 739 '## step2.5 鉴权 bootstrap(确定性前置)',
720 740 '- 用 config-vars `admin_init` 或种子已知凭据,经 `docs/05` 登录端点**真实登录**拿 JWT,注入 Playwright `storageState`;`authState` 记角色覆盖(覆盖 / 未覆盖角色集)。',
721 741 '- 登录失败 = `envError.kind="auth-failed"`(环境 race,走 retry),**绝不**当成死控件。',
722 742 '',
723   - '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;只驱动本 FE feScope)',
724   - '- **只枚举/驱动 feScope.routes + feScope.controlWhitelist**(本 FE 白名单控件)。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 本 FE 推导清单,分子 = live 枚举。',
  743 + '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;驱动全部 FE 作用域并集)',
  744 + '- **枚举/驱动 step1 聚合的全部路由 + 各 FE 控件白名单并集**。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 聚合清单,分子 = live 枚举。',
  745 + '- **FeStub 残留检测**:每路由加载后检查 `data-fe-stub` 元素;仍渲染占位 → 该 FE 的 tdd 漏做占位替换(硬缺陷),归 `interactionFailures[kind="no-observable-effect"]`(locator=router 文件该路由 import 行,detail 写「路由仍指向 FeStub 占位」)。',
725 746 '- 分母有但首帧无的控件:runner 尝试**驱动到出现态**(种子保列表非空触发行级操作 / 进多步流程下屏 / 展开 dropdown / 切 tab 后二次枚举);仍不可达 → `coverageGaps[reason="deep-control-not-driven"]`,不静默判 green。到不了的路由 → `coverageGaps[reason="unreachable-auth"|"unreachable-no-route"]`,与「到达了但控件死」严格区分。',
726   - '- **白名单外 / 共享控件**:若属其它未 approve FE 或共享区 → 归 `coverageGaps[reason="deep-control-not-driven"]`,**绝不**归本 FE 的 `interactionFailures`。',
  747 + '- **白名单外控件**(任何 FE 白名单都未列,如共享导航/布局区):不入「必须有效果」断言集,记证据即可;确属可疑死控件可记 `coverageGaps[reason="deep-control-not-driven"]`。',
727 748 '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。',
728   - '- `routesReached` / `controlsEnumerated` 据实填(本 FE 子集空覆盖必须可见)。',
  749 + '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。',
729 750 '',
730 751 '## step4 推导期望',
731 752 '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。',
732 753 '',
733   - '## step5 断言(两层 + 可观测效果白名单 + 硬问题带源码 locator)',
  754 + '## step5 断言(三层:交互/文字/样式 + 可观测效果白名单 + 硬问题带源码 locator)',
734 755 '- **交互层可观测效果白名单**: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`)。',
735 756 ' - 无任何效果 → `interactionFailures[kind="no-observable-effect"]`;JS 异常 → `js-error`;`console.error` → `console-error`;应发未发网络调用 → `missing-docs05-call`。断言用 auto-waiting / `expect.poll`,**不用**固定 sleep。',
736 757 '- **文字层**:动态文字格对比该 region 字段的唯一 sentinel(抓绑错字段)。',
737 758 '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。',
738   - '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 approve)。',
  759 + '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 green)。',
  760 + '- **样式层(客观断言:颜色 token 比对 + layout sanity)**。断言作用域 = **白名单控件及其直接容器 + spec/prototype 点名区域**;组件库深层内部元素**不查**,只查可见元素:',
  761 + ` - **色值基准确定性派生**:读 \`${ROOT}/src/styles/tokens.css\` 解析全部 \`--color-*\`,用探针元素 getComputedStyle 把任意色值格式归一化为 canonical rgb 集合;被检元素的渲染值(\`color\` / \`background-color\` / \`border-color\`)同法归一化后比对。`,
  762 + ' - **颜色断言**:渲染色 ∉ token 集合 → `styleIssues[kind="non-token-color"]`;spec「Design Tokens 引用清单」点名了具体 token 的元素,渲染值 ≠ 该 token 解析值 → `styleIssues[kind="token-mismatch"]`。半透明混合 / 无法归一化的值 → **不入** styleIssues,记 `decisions[]`(宁漏勿误)。',
  763 + ' - **几何断言(layout sanity)**:每路由 `scrollWidth > clientWidth + 1` → `styleIssues[kind="horizontal-overflow"]`(locator=该路由 view 组件文件);白名单控件两两 boundingBox 交叠 >4px² 且双方可见非 inert → `overlap`;预期可见控件 box 为零 → `zero-size`;`scrollIntoViewIfNeeded` 后仍不在视口 → `offscreen`。',
  764 + ' - **视口**:默认用 Playwright 默认视口;prototype 明确声明 viewport 时用之并记 `decisions[]`。',
  765 + ' - styleIssues 全部是客观硬问题:locator 要求与交互层逐字同口径(A 类反查组件文件;反查不出归 `coverageGaps[reason="locator-not-resolvable"]`),`expected`/`actual` 写比对双方的具体值(如 `expected="var(--color-primary)→rgb(22,119,255)" actual="rgb(255,0,0)"`)。',
739 766 '- **行为硬问题必须带源码 locator(转 must-fix 喂 fix 的前置)**:',
740   - ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。',
  767 + ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 归属 FE + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。',
741 768 ' - **B 类(连组件文件都反查不出)**:**不静默降级放行**——归 `coverageGaps[reason="locator-not-resolvable"]`(计入未覆盖,使本轮不能判 green),或归 `envError.kind="stack-not-ready"` 走 retry。绝不把无 locator 的硬问题塞进 `interactionFailures` 不带 locator(上层会因无 locator 走 adjudicate(allowContinue:false),绝不放行)。',
742 769 '',
743 770 `## step6 证据落盘 + commit(运行时行为,沿用证据 commit 习惯)`,
744   - `- 写 \`${evidence}\`:本 FE feScope / 推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`,
745   - `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/reviews/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`,
  771 + `- 写 \`${evidence}\`:**按 FE 分小节**(每 FE:作用域 / 推导期望 / 逐控件判定 / 该 FE 的 styleIssues 与 coverageGaps),全局段写 routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ token 色值集合摘要 / 截图索引。`,
  772 + `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/module-reports/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`,
746 773 `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`,
747   - commitBlock(`${evidence} docs/superpowers/reviews/assets`,
748   - `docs(behavior:${safeId}:r${behaviorRound}-a${attempt}): per-FE 行为验收证据`),
  774 + commitBlock(`${evidence} docs/superpowers/module-reports/assets`,
  775 + `docs(behavior:frontend-phase:r${behaviorRound}-a${attempt}): 阶段级行为验收证据`),
749 776 '',
750 777 '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)',
751   - '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无阻断性 envError + 本 FE 覆盖非空)| `red`。',
752   - '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数本 FE feScope**;空覆盖必须可见)。',
753   - '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`(含 `source="sentinel"` 的 textIssue)。',
  778 + '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + **样式层无失败** + 无阻断性 envError + 覆盖非空)| `red`。',
  779 + '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数全部 FE 作用域并集**;空覆盖必须可见)。',
  780 + '- `interactionFailures` / `textIssues` / `styleIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`(含 `source="sentinel"` 的 textIssue 与全部 styleIssues)。',
754 781 '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids;`build-failed` 时填 `rootCausePath`。',
755 782 '- 做过任何自主默认 → `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。',
756 783 '- 不要返回额外字段(schema 是 `additionalProperties:false`)。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。',
757 784 ].filter(Boolean).join('\n')
758 785 }
759 786  
  787 +// behaviorReverifyPrompt:阶段级行为 fix 后的功能复验。fix 改的是 frontend/ UI 源码,可能引入功能回归——
  788 +// 在下一 behaviorRound 重起全栈之前,先派子会话跑**全量前端单测**(vitest,不跑 e2e——e2e/行为维度由下一轮
  789 +// 行为门重跑 + 阶段 testGate 全量回归兜底),红则当功能回归硬边界(调用方 allowContinue:false)。
  790 +function behaviorReverifyPrompt(behaviorRound, fixedCount) {
  791 + const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-reverify-r${behaviorRound}.md`
  792 + return [
  793 + `# behavior-reverify — 行为 fix 后前端单测复验(behaviorRound=${behaviorRound})`,
  794 + '',
  795 + featureStageContract('frontend'),
  796 + '',
  797 + '## 目标',
  798 + `阶段级行为门第 ${behaviorRound} 轮 fix(${fixedCount} 项 must-fix)改动了 \`frontend/\` 源码——**派发 Agent 子会话**跑全量前端单测确认无功能回归。**主会话从不直接跑测试,也不自由编写证据。**`,
  799 + '',
  800 + '## 流程',
  801 + `- 命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` 取(缺失默认 \`pnpm test:ci\`);**只跑单测(vitest),不跑 e2e**。`,
  802 + '- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行)。',
  803 + '- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt(fix 引入功能回归,绝不带红进入下一轮行为门)。',
  804 + `- 证据写入 \`${evidence}\`(每轮独立文件不覆盖前轮)。`,
  805 + '',
  806 + commitBlock(evidence, `docs(behavior:frontend-phase:r${behaviorRound}): 行为 fix 后单测复验`,
  807 + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'),
  808 + '',
  809 + '## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
  810 + `- 全部通过:\`{ "status": "ok", "artifactPath": "${evidence}", "summary": "<exit_code / passed / failed 摘要>" }\`。`,
  811 + '- 任一红色 / 缺值 → `{ "status": "halt", "reason": "<失败用例摘要>" }`。',
  812 + ].join('\n')
  813 +}
  814 +
760 815 // ---- 前端骨架占位 stage(runFrontendSkeleton 用)----
761   -// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A,blocker)。
  816 +// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A;v3 阶段级行为门下仍保留)。
762 817 // 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(未实现 FE 路由指向 FeStub 占位)
763 818 // + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。
764   -// 由此 per-FE 行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点、build-failed 退化为罕见兜底
  819 +// 由此逐 FE 的 verify(e2e) 与阶段末尾行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点
765 820 // feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。
766 821 function frontendSkeletonPrompt(feItems) {
767 822 const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)'
... ... @@ -796,6 +851,7 @@ function frontendSkeletonPrompt(feItems) {
796 851 ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):冷起后端 + 轮询健康端点就绪(Flyway 建 schema)→ 执行 \`node ${ROOT}/scripts/seed-demo-data.mjs\`(注入演示种子)→ 用 \`config-vars.yaml\` 的 \`admin_init\` 凭据经 \`docs/05-API接口契约.md\` 登录端点取 JWT,写 \`storageState\`(admin 登录态供 e2e 复用)。`,
797 852 ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树。',
798 853 ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 空库重建 + Flyway schema + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐。',
  854 + '6. **单测基线(测试隔离接线点)**:vitest 配置(按 docs/04 § 零 `frontend.unit_test_runner` 约定,如 `frontend/vitest.config.*` 或 package.json 内配置段)`include` **限定** `tests/**/*.test.*`——单测一律落 `frontend/tests/**`(镜像 `src/` 结构,smoke 类归 `tests/__smoke__/` 且文件名同样以 `.test.*` 结尾,见 docs/04 § 2.1),`frontend/src/` 内的测试残留不被执行(约定漂移立即可见)。不存在才创建,已存在则只补齐 include 限定。**legacy 守卫**:若 `frontend/src/` 内已存在测试文件(旧约定 colocation 残留),**绝不**收窄 include(否则旧单测静默停跑、回归覆盖丢失)——保持现有 include 不动,登记 `decisions[]` 提示人工迁移(src/ 内测试迁至 tests/ 镜像路径后方可收窄)。',
799 855 '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。',
800 856 '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。',
801 857 '',
... ... @@ -821,9 +877,10 @@ function frontendSkeletonStatePromptM(feItems) {
821 877 '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)',
822 878 microStepContract(),
823 879 '',
824   - `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在,**且 e2e 基线脚手架存在**——Playwright 配置文件(\`frontend/playwright.config.*\`)+ globalSetup 文件(如 \`frontend/e2e/global-setup.*\`)。`,
825   - '- 全部满足(骨架已建齐,含 e2e 基线脚手架)→ `{ "exists": true }`',
826   - '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup)→ `{ "exists": false }`',
  880 + `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在,**且 e2e 基线脚手架存在**——Playwright 配置文件(\`frontend/playwright.config.*\`)+ globalSetup 文件(如 \`frontend/e2e/global-setup.*\`),**且单测基线存在**——vitest 配置 \`include\` 限定 \`tests/**/*.test.*\`。`,
  881 + `- **legacy 豁免**:若 \`${ROOT}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`,
  882 + '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`',
  883 + '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`',
827 884 '## 输出(EXISTS_SCHEMA)',
828 885 ].join('\n')
829 886 }
... ... @@ -849,11 +906,11 @@ function microStepContract() {
849 906 // ============================================================================
850 907  
851 908 const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环)
852   -// per-FE 行为子门预算(二维,钉死防证据覆盖;设计 §6.4):
853   -// - BEHAVIOR_FE_MAX = approve 子门内的行为 fix 轮硬上限(每 FE);超限 throw HALT。**不**复用 review 的 10 轮、
854   -// **不**让 REVIEW_HARD_ROUNDS × 行为重试隐式相乘——典型一次过(1 轮),最坏 3 轮。
  909 +// 阶段级行为门预算(二维,钉死防证据覆盖):
  910 +// - BEHAVIOR_STAGE_MAX = 阶段门内的行为 fix 轮硬上限(整个前端阶段共用,每轮 fix 可批量修当轮全部 must-fix);
  911 +// 超限 throw HALT。典型一次过(1 轮),最坏 3 轮。
855 912 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。
856   -const BEHAVIOR_FE_MAX = 3
  913 +const BEHAVIOR_STAGE_MAX = 3
857 914 const BEHAVIOR_ATTEMPT_MAX = 2
858 915 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : ''
859 916  
... ... @@ -1322,7 +1379,7 @@ function reportPrompt(module) {
1322 1379 '## 前置',
1323 1380 `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`,
1324 1381 fe
1325   - ? `- **前端行为验收已并入 per-FE review 循环**(reviewer approve 子门,行为 green 是 \`req-done/<FE>\` 的前置真值)——report **不再**校验阶段级 behavior-gate 文件(已不再产生)。**对每个 \`req-done/<FE>\` tag 即视为该 FE 行为已过**(避免双真值)。可选轻量校验:每个 FE 存在对应 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/<date>-<FE>-behavior-r*-a*.md\` 且最后一份非 RED;缺证据不 halt(仅在 § ⑤/⑧ 标注)。`
  1382 + ? `- **验证阶段级行为门绿**:Glob \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`,按 behaviorRound → attempt 数字升序读取。**最后一份必须非 RED(绿)**;最后一份 red 或一份证据都没有 → 立即 halt(绝不在行为红 / 无行为证据上打 milestone)。注意 per-FE 行为证据(\`reviews/<date>-<FE>-behavior-*\`)已不再产生,不要校验。`
1326 1383 : '',
1327 1384 '',
1328 1385 '## 收集输入(取摘要而非正文)',
... ... @@ -1332,8 +1389,8 @@ function reportPrompt(module) {
1332 1389 `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`,
1333 1390 `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`,
1334 1391 '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。',
1335   - `- § ⑤:把 \`${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/<date>-FE-*-behavior-r*-a*.md\`(按 FE → behaviorRound → attempt 排序)的 flake / 环境 race(envError,含 build-failed 短路)/ 文字 continue 记录一并纳入 § ⑤ 汇总**。`,
1336   - `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/<date>-FE-*-behavior-r*-a*.md\` 汇总各 FE 的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`,
  1392 + `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把阶段级行为证据 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(按 behaviorRound → attempt 排序)与行为复验 \`frontend-phase-behavior-reverify-r*.md\` 的 flake / 环境 race(envError)/ 行为 fix 轮数 / 样式 styleIssues 修复轮次 / 文字 continue 记录一并纳入 § ⑤ 汇总**。`,
  1393 + `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按阶段级行为证据 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(取最后一份的逐 FE 小节)汇总各 FE 的 \`coverageGaps\` + 样式 \`styleIssues\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`,
1337 1394 '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。',
1338 1395 ].join('\n')
1339 1396 : [
... ... @@ -1630,9 +1687,6 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1630 1687 let lastVerify = verifyResult
1631 1688 let lastIssuesCount = 0
1632 1689 let reviewGuidance = '' // 仲裁 retry 时注入下一轮 review 的纠正指令
1633   - // softPassed 提升到 reviewWithFixLoop 顶层作用域(与本 FE review 同寿命,跨 behaviorRound 持久)——
1634   - // 行为软文字一旦被仲裁 continue 放行(降级),重跑后即便仍在 textIssues 也不再追问,避免反复消耗仲裁预算。
1635   - const behaviorSoftPassed = new Set()
1636 1690 for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) {
1637 1691 const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || ''
1638 1692 // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。
... ... @@ -1643,13 +1697,9 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1643 1697 reviewGuidance = '' // 已消费
1644 1698  
1645 1699 if (r.verdict === 'approve') {
1646   - // approve 闸显式 AND(设计 §6.2):reviewer.verdict==='approve' ∧ behaviorSubGate green(仅前端)。
1647   - // 后端逐字不变(无行为维度);前端:静态 approve 后**不立即 return**,先进 per-FE 行为 approve 子门——
1648   - // 起本 FE 全栈验「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才放行;
1649   - // 行为 green ⇒ 才 flipDocs08Checkbox + return(req-done tag 落点 featureLoop 不动,语义自动升级为「静态过+行为过」)。
1650   - if (fe) {
1651   - await behaviorSubGate(id, specPath, grp, behaviorSoftPassed)
1652   - }
  1700 + // approve = 静态 review 通过即放行(前后端同构)。行为验收已挪到阶段末尾的 phase('Behavior')
  1701 + // 一次性阶段级行为门(runBehaviorGate)——不再是 per-FE approve 子门。req-done/<FE> 语义 =「静态过」;
  1702 + // 行为维度由阶段门统一验收,行为 green 是 milestone 的前置(reportPrompt 校验行为证据非 RED)。
1653 1703 await flipDocs08Checkbox(fe, id, phase, grp)
1654 1704 return { id, phase, approved:true, rounds:round }
1655 1705 }
... ... @@ -1663,9 +1713,8 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1663 1713 const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`,
1664 1714 { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)',
1665 1715 reviewerIssues: r.issues || [] }, grp, round)
1666   - // continue 视为「无 must-fix → 静态 approve」——前端仍须先过行为 approve 子门(行为 green 是任何 approve return 的前置)。
  1716 + // continue 视为「无 must-fix → 静态 approve」(行为维度由阶段末尾的行为门统一验收,不在此处)。
1667 1717 if (verdict.action === 'continue') {
1668   - if (fe) await behaviorSubGate(id, specPath, grp, behaviorSoftPassed)
1669 1718 await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round }
1670 1719 }
1671 1720 if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`)
... ... @@ -1719,125 +1768,96 @@ async function testGate(module, phase) {
1719 1768 return g
1720 1769 }
1721 1770  
1722   -// ---- 前端 per-FE 行为验收控制流(runBehaviorGateOnce + behaviorSubGate)----
1723   -// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 6.3 / 7。
1724   -// 行为验收并入 per-FE reviewWithFixLoop 的 approve 子门——reviewer 即将 approve 时才触发,绝不每 review round 起栈。
1725   -// behaviorSubGate 失败分层(per-FE 缩 scope,保留原 runBehaviorGate 的分层语义):
1726   -// - build-failed(兄弟 FE 未实现 / 占位未覆盖,根因落非本 FE 路径)= 确定性短路:记 coverageGap + decisions,
1727   -// 本轮行为门视为「本 FE 非缺陷」直接放行 approve(预期中途态,不 retry 不 halt)。
1728   -// - envError(其它) / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。
1729   -// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 approve。
1730   -// - behaviorHard = interactionFailures + sentinel textIssues:有 locator → 降维喂 fixPrompt 跑 fix(fix 后功能 reverify + 下一轮重跑行为);
1731   -// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不 approve。
1732   -// - BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。
  1771 +// ---- 前端阶段级行为验收控制流(runBehaviorGateOnce + runBehaviorGate)----
  1772 +// 设计:docs/design/2026-06-05-frontend-behavior-stage-gate.md(v3)。
  1773 +// 时机:顶层 frontend 段 featureLoop 全部 FE 完成(req-done 已打)之后、testGate 之前,phase('Behavior') 下
  1774 +// 整个前端阶段只跑**一次**行为验收(含 fix 循环)。失败分层:
  1775 +// - envError / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。
  1776 +// build-failed(阶段末尾无「兄弟未实现」豁免)属确定性失败:跳过自动 attempt 重起(重跑不自愈),直送仲裁。
  1777 +// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 green。
  1778 +// - behaviorHard = interactionFailures + sentinel textIssues + styleIssues(颜色 token / layout sanity):
  1779 +// 有 locator → 降维喂 fixPrompt 跑 fix(fix 后全量前端单测复验 + 下一 behaviorRound 重跑门);
  1780 +// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不放行。
  1781 +// - BEHAVIOR_STAGE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。
1733 1782  
1734   -// envBlocked / ifails:per-FE bg 的环境/空覆盖与交互失败判定(build-failed 不计 envBlocked——它走确定性短路分支)。
  1783 +// envBlocked / ifails:环境/空覆盖与交互失败判定。v3:build-failed 计入 envBlocked(不再有短路放行分支)。
1735 1784 function behaviorEnvBlocked(r) {
1736 1785 const k = r.envError && r.envError.kind
1737   - const ev = (k && k !== 'none' && k !== 'build-failed') ? r.envError : null
  1786 + const ev = (k && k !== 'none') ? r.envError : null
1738 1787 const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0)
1739 1788 return { ev, emptyCov, blocked: !!ev || emptyCov }
1740 1789 }
1741 1790 function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] }
  1791 +const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed')
1742 1792  
1743   -// runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。
1744   -// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。
1745   -// behaviorRound:approve 子门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。
1746   -async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) {
1747   - const lbl = (a) => `behavior:${id}:r${behaviorRound}:a${a}`
  1793 +// runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。
  1794 +// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。
  1795 +// behaviorRound:阶段门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。
  1796 +async function runBehaviorGateOnce(feItems, behaviorRound) {
  1797 + const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}`
1748 1798 let attempt = 1
1749   - let bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt),
1750   - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA})
1751   - recordDecisions(`behavior:${id}`, bg.decisions)
1752   -
1753   - // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。
1754   - const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed'
1755   - if (isBuildFailedShortCircuit(bg)) return bg
  1799 + let bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt),
  1800 + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
  1801 + recordDecisions('behavior:frontend-phase', bg.decisions)
1756 1802  
1757 1803 // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。
1758   - while (behaviorEnvBlocked(bg).blocked && attempt < BEHAVIOR_ATTEMPT_MAX) {
  1804 + // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。
  1805 + while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) {
1759 1806 attempt += 1
1760   - bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt),
1761   - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA})
1762   - recordDecisions(`behavior:${id}`, bg.decisions)
1763   - if (isBuildFailedShortCircuit(bg)) return bg
  1807 + bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt),
  1808 + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
  1809 + recordDecisions('behavior:frontend-phase', bg.decisions)
1764 1810 }
1765 1811 let envState = behaviorEnvBlocked(bg)
1766 1812 for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) {
1767 1813 const reason = envState.ev
1768   - ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}`
  1814 + ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}${envState.ev.rootCausePath ? `(rootCausePath=${envState.ev.rootCausePath})` : ''}`
1769 1815 : `behavior 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)`
1770   - const verdict = await adjudicate(`behavior-env:${id}`,
1771   - { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, grp, adj)
1772   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-env ${id}: ${verdict.rationale || reason}`)
  1816 + // riders:环境失败同轮搭车的硬问题(如 build-failed 时已归因到的 interactionFailures/styleIssues)——
  1817 + // 本轮不进 fix(环境未就绪,fix 无意义),但透传给仲裁者辅助 retry/halt 判断;若环境修复后仍真实,下一轮门会重新发现。
  1818 + const verdict = await adjudicate('behavior-env:frontend-phase',
  1819 + { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids,
  1820 + riders: { interactionFailures: behaviorIfails(bg).length, styleIssues: (bg.styleIssues || []).length,
  1821 + sentinelTextIssues: (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length },
  1822 + allowContinue:false }, 'Behavior', adj)
  1823 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`)
1773 1824 attempt += 1
1774   - bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt),
1775   - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA})
1776   - recordDecisions(`behavior:${id}`, bg.decisions)
1777   - if (isBuildFailedShortCircuit(bg)) return bg
  1825 + bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt),
  1826 + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
  1827 + recordDecisions('behavior:frontend-phase', bg.decisions)
1778 1828 envState = behaviorEnvBlocked(bg)
1779 1829 }
1780   - if (envState.blocked) throw new Error(`HALT behavior-env ${id}: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`)
  1830 + if (envState.blocked) throw new Error(`HALT behavior-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`)
1781 1831 return bg
1782 1832 }
1783 1833  
1784   -// behaviorSubGate:reviewer approve 的「行为 approve 子门」。green 才允许 reviewWithFixLoop return approve。
1785   -// softPassed:由 reviewWithFixLoop 顶层注入,跨 behaviorRound 持久(软文字一旦放行不再追问)。
1786   -// green ≡ behaviorHard.length===0 ∧ envError∈{none,build-failed} ∧ 本 FE 覆盖非空(或 build-failed 短路)。
1787   -async function behaviorSubGate(id, specPath, grp, softPassed) {
  1834 +// runBehaviorGate:阶段级行为门主循环(被顶层 frontend 段调用,phase('Behavior') 下)。green 才正常返回(放行进 testGate)。
  1835 +// softPassed:本函数内声明,跨 behaviorRound 持久(软文字一旦放行不再追问,避免反复消耗仲裁预算)。
  1836 +// green ≡ behaviorHard.length===0 ∧ envError===none ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空 ∧ 无未解释漏达路由。
  1837 +async function runBehaviorGate(feItems) {
1788 1838 const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}`
1789   - for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) {
1790   - const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound)
1791   -
1792   - // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → green-by-skip 放行。但骨架(lazy router + FeStub)
1793   - // 令「合法的兄弟未实现 build-failed」极罕见,故一个 build-failed 更可能是本 FE 引入的真共享代码回归;
1794   - // 绝不凭未校验的 LLM 归因静默放行——先过轻量前置校验(comment §107-108 声称 load-bearing 的边界,此前无 JS 兜底):
1795   - // a) 必须有 rootCausePath(否则无从判定根因落点);
1796   - // b) 不得同时携带交互硬问题(interactionFailures / source=sentinel 文字)——那是真缺陷搭车。
1797   - // 任一不满足 = 「脏」build-failed → 不短路,过 adjudicate(allowContinue:false) retry/halt,绝不 green-by-skip。
1798   - if (bg.envError && bg.envError.kind === 'build-failed') {
1799   - const rootCausePath = (bg.envError.rootCausePath || '').trim()
1800   - const hardRiders = behaviorIfails(bg).length
1801   - + (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length
1802   - const dirty = !rootCausePath
1803   - ? 'build-failed 未给 rootCausePath(无法判定根因是否落在本 FE 之外)'
1804   - : hardRiders
1805   - ? `build-failed 同时携带 ${hardRiders} 项交互/sentinel 硬问题(疑似本 FE 真构建 bug 搭车)`
1806   - : null
1807   - if (dirty) {
1808   - const verdict = await adjudicate(`behavior-buildfailed-dirty:${id}`,
1809   - { problem:`build-failed 归因不可信,绝不短路放行:${dirty}(rootCausePath=${rootCausePath || '∅'})`,
1810   - envError: bg.envError, allowContinue:false }, grp, behaviorRound)
1811   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-buildfailed ${id}: ${verdict.rationale || dirty}`)
1812   - continue // retry → 下一 behaviorRound 重跑整门
1813   - }
1814   - // 干净的 build-failed(有 rootCausePath 且无硬问题搭车)→ green-by-skip 放行,记低置信证据。
1815   - recordDecisions(`behavior-build-failed:${id}`, [{
1816   - question:`本 FE ${id} 行为验收遇 build-failed(根因 ${rootCausePath})`,
1817   - choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)',
1818   - rationale: bg.envError.detail || '', confidence:'low' }])
1819   - log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${rootCausePath}),记证据不阻断`)
1820   - return
1821   - }
  1839 + const softPassed = new Set()
  1840 + for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_STAGE_MAX; behaviorRound++) {
  1841 + const bg = await runBehaviorGateOnce(feItems, behaviorRound)
1822 1842  
1823   - // 2) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。
1824   - // locator-not-resolvable(B 类硬问题反查不出)计入未覆盖——下面会因 behaviorHard 仍非空或覆盖不足而不 green。
  1843 + // 1) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。
  1844 + // locator-not-resolvable / scope-missing 在 §3.5 单独阻断 green。
1825 1845 for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) {
1826 1846 if (!cg) continue
1827   - recordDecisions(`behavior-coverage:${id}`,
  1847 + recordDecisions('behavior-coverage:frontend-phase',
1828 1848 [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }])
1829 1849 }
1830 1850  
1831   - // 3) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。
1832   - // 永不阻断 approve;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。
  1851 + // 2) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。
  1852 + // 永不阻断 green;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。
1833 1853 let softRetry = false
1834 1854 for (const ti of (Array.isArray(bg.textIssues) ? bg.textIssues : [])) {
1835 1855 if (!ti || ti.source === 'sentinel') continue // sentinel 归 behaviorHard,不在软文字处理
1836 1856 if (softPassed.has(regionKey(ti))) continue
1837   - const site = `behavior-text:${id}:${ti.page || '?'}:${ti.region || '?'}`
  1857 + const site = `behavior-text:${ti.page || '?'}:${ti.region || '?'}`
1838 1858 const verdict = await adjudicate(site,
1839   - { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 approve):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`,
1840   - textIssue: ti, allowContinue: true }, grp, behaviorRound)
  1859 + { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 green):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`,
  1860 + textIssue: ti, allowContinue: true }, 'Behavior', behaviorRound)
1841 1861 if (verdict.action === 'continue') {
1842 1862 recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`,
1843 1863 choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }])
... ... @@ -1848,24 +1868,26 @@ async function behaviorSubGate(id, specPath, grp, softPassed) {
1848 1868 }
1849 1869 if (softRetry) continue
1850 1870  
1851   - // 3.5) B 类硬问题(locator-not-resolvable coverageGap):连组件文件都反查不出,不静默放行——
1852   - // 计入未覆盖阻断 approve,走 adjudicate(allowContinue:false) retry/halt(绝不当 green 放行,降级≠放行)。
1853   - const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []).filter(cg => cg && cg.reason === 'locator-not-resolvable')
  1871 + // 3.5) B 类硬问题(locator-not-resolvable:连组件文件都反查不出)+ scope-missing(某 FE 作用域小节缺失,
  1872 + // 其路由整体漏验):不静默放行——计入未覆盖阻断 green,走 adjudicate(allowContinue:false) retry/halt。
  1873 + const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])
  1874 + .filter(cg => cg && (cg.reason === 'locator-not-resolvable' || cg.reason === 'scope-missing'))
1854 1875 if (bClass.length) {
1855   - const summary = bClass.map(cg => `${cg.page} — ${cg.detail}`).join('; ')
1856   - const verdict = await adjudicate(`behavior-bclass:${id}`,
1857   - { problem:`behavior 硬问题连组件文件都反查不出(B 类,不可降级放行,计入未覆盖阻断 approve):${summary}`,
1858   - coverageGaps: bClass, allowContinue:false }, grp, behaviorRound)
1859   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass ${id}: ${verdict.rationale || summary}`)
1860   - continue // retry → 重跑本 FE 行为验收(下一 behaviorRound)
  1876 + const summary = bClass.map(cg => `[${cg.reason}] ${cg.page} — ${cg.detail}`).join('; ')
  1877 + const verdict = await adjudicate('behavior-bclass:frontend-phase',
  1878 + { problem:`behavior 不可降级的未覆盖(B 类反查不出 / FE 作用域缺失,阻断 green):${summary}`,
  1879 + coverageGaps: bClass, allowContinue:false }, 'Behavior', behaviorRound)
  1880 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass frontend-phase: ${verdict.rationale || summary}`)
  1881 + continue // retry → 重跑行为验收(下一 behaviorRound)
1861 1882 }
1862 1883  
1863 1884 // 3.6) 覆盖率对账(确定性兜底):空覆盖只兜 ==0;这里兜 0<routesReached<routesPlanned 的「部分覆盖假绿」。
1864 1885 // 每条 planned-but-unreached 路由必须由「路由级 coverageGap」解释;未被解释的漏达路由 = 静默漏验,绝不判 green。
1865   - // 只数路由级 reason(控件级 deep-control-not-driven / locator-not-resolvable 不抵漏达路由);过计只会抑制本门、绝不误 halt。
  1886 + // 只数路由级 reason(控件级 deep-control-not-driven / locator-not-resolvable / scope-missing 不抵漏达路由——
  1887 + // scope-missing 的 FE 路由本就不在分母);过计只会抑制本门、绝不误 halt。
1866 1888 const planned = Number(bg.routesPlanned) || 0
1867 1889 const reached = Number(bg.routesReached) || 0
1868   - const ROUTE_GAP = new Set(['unreachable-auth', 'unreachable-no-route', 'dynamic-route-no-seed', 'build-failed-sibling-unimpl'])
  1890 + const ROUTE_GAP = new Set(['unreachable-auth', 'unreachable-no-route', 'dynamic-route-no-seed'])
1869 1891 const routeGapPages = new Set((Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])
1870 1892 .filter(cg => cg && ROUTE_GAP.has(cg.reason) && typeof cg.page === 'string' && cg.page.trim())
1871 1893 .map(cg => cg.page.trim()))
... ... @@ -1873,70 +1895,78 @@ async function behaviorSubGate(id, specPath, grp, softPassed) {
1873 1895 const missedRoutes = Math.max(0, planned - reached)
1874 1896 const unaccounted = Math.max(0, missedRoutes - routeGapCount)
1875 1897 if (planned > 0 && unaccounted > 0) {
1876   - const verdict = await adjudicate(`behavior-undercoverage:${id}`,
1877   - { problem:`本 FE 路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`,
1878   - coverageGaps: bg.coverageGaps || [], allowContinue: false }, grp, behaviorRound)
1879   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage ${id}: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`)
  1898 + const verdict = await adjudicate('behavior-undercoverage:frontend-phase',
  1899 + { problem:`路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`,
  1900 + coverageGaps: bg.coverageGaps || [], allowContinue: false }, 'Behavior', behaviorRound)
  1901 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage frontend-phase: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`)
1880 1902 continue // retry → 下一 behaviorRound 重跑整门
1881 1903 }
1882 1904  
1883   - // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。
  1905 + // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues
  1906 + // + styleIssues(颜色 token / layout sanity,全部客观可 fix)。
1884 1907 const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : [])
1885 1908 .filter(t => t && t.source === 'sentinel')
1886 1909 .map(t => ({ page:t.page, control:t.region, kind:'binding-garbage', detail:`sentinel 不符 期望=${t.expected} 实际=${t.actual}`, locator:t.locator }))
1887   - const behaviorHard = [...behaviorIfails(bg), ...sentinelHard]
  1910 + const styleHard = (Array.isArray(bg.styleIssues) ? bg.styleIssues : [])
  1911 + .filter(Boolean)
  1912 + .map(s => ({ page:s.page, control:s.element, kind:`style-${s.kind}`,
  1913 + detail:`期望=${s.expected} 实际=${s.actual}`, locator:s.locator }))
  1914 + const behaviorHard = [...behaviorIfails(bg), ...sentinelHard, ...styleHard]
1888 1915  
1889 1916 const hasEnvSignal = !!(bg.envError && bg.envError.kind && bg.envError.kind !== 'none')
1890 1917 const hasAnyClassifiedSignal = hasEnvSignal
1891 1918 || behaviorHard.length > 0
1892 1919 || (Array.isArray(bg.textIssues) && bg.textIssues.length > 0)
  1920 + || (Array.isArray(bg.styleIssues) && bg.styleIssues.length > 0)
1893 1921 || (Array.isArray(bg.coverageGaps) && bg.coverageGaps.length > 0)
1894 1922 if (bg.status === 'red' && !hasAnyClassifiedSignal) {
1895   - const verdict = await adjudicate(`behavior-red-unclassified:${id}`,
1896   - { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green',
1897   - behaviorResult: bg, allowContinue:false }, grp, behaviorRound)
1898   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified ${id}: ${verdict.rationale || 'status:red 无分类原因'}`)
  1923 + const verdict = await adjudicate('behavior-red-unclassified:frontend-phase',
  1924 + { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / styleIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green',
  1925 + behaviorResult: bg, allowContinue:false }, 'Behavior', behaviorRound)
  1926 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified frontend-phase: ${verdict.rationale || 'status:red 无分类原因'}`)
1899 1927 continue
1900 1928 }
1901 1929  
1902   - // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 子门 green 放行。
  1930 + // 5) green 判定:behaviorHard 为空 ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 门 green 放行。
1903 1931 if (behaviorHard.length === 0) {
1904   - log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`)
  1932 + log(`behavior frontend-phase green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`)
1905 1933 return
1906 1934 }
1907 1935  
1908   - // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不 approve)。
  1936 + // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不放行)。
1909 1937 const withLoc = behaviorHard.filter(x => typeof x.locator === 'string' && x.locator.trim())
1910 1938 const noLoc = behaviorHard.filter(x => !(typeof x.locator === 'string' && x.locator.trim()))
1911 1939 if (noLoc.length) {
1912 1940 const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ')
1913   - const verdict = await adjudicate(`behavior-noloc-hard:${id}`,
1914   - { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue/approve):${summary}`,
1915   - interactionFailures: noLoc, allowContinue:false }, grp, behaviorRound)
  1941 + const verdict = await adjudicate('behavior-noloc-hard:frontend-phase',
  1942 + { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue 放行):${summary}`,
  1943 + interactionFailures: noLoc, allowContinue:false }, 'Behavior', behaviorRound)
1916 1944 if (verdict.action !== 'retry')
1917   - throw new Error(`HALT behavior-noloc-hard ${id}: ${verdict.rationale || summary}`)
1918   - continue // retry → 重跑本 FE 行为验收(下一 behaviorRound)
  1945 + throw new Error(`HALT behavior-noloc-hard frontend-phase: ${verdict.rationale || summary}`)
  1946 + continue // retry → 重跑行为验收(下一 behaviorRound)
1919 1947 }
1920 1948  
1921 1949 // 7) 有 locator 的硬问题 → 降维成 {summary,locator,severity} 喂现有 fixPrompt 跑 fix(schema 不合并、fix 入参合并)。
  1950 + // 一轮 fix 批量修当轮全部 must-fix(跨 FE 也在同一 fix 子会话内逐项修,locator 已含组件文件路径)。
1922 1951 const fixIssues = withLoc.map(f => ({
1923 1952 summary: `[behavior:${f.kind}] ${f.page}:${f.control} — ${f.detail}`,
1924 1953 locator: f.locator,
1925 1954 severity: 'high',
1926 1955 }))
1927   - await runStage(g => fixPrompt(id, 'frontend', fixIssues) + g, {
1928   - site:`behavior-fix:${id}:r${behaviorRound}`, grp, label:`behavior-fix:${id}:r${behaviorRound}`, allowContinue: true,
  1956 + await runStage(g => fixPrompt('frontend-phase', 'frontend', fixIssues) + g, {
  1957 + site:`behavior-fix:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-fix:r${behaviorRound}`, allowContinue: true,
1929 1958 })
1930 1959  
1931   - // 8) fix 后功能复验(allowContinue:false):behaviorSubGate 的 fix 改的是 frontend/ UI 源码,可能引入功能回归——
1932   - // 先跑 scoped 组件测试 reverify(不起全栈,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。
  1960 + // 8) fix 后功能复验(allowContinue:false):行为 fix 改的是 frontend/ UI 源码,可能引入功能回归——
  1961 + // 先跑全量前端单测(不起全栈、不跑 e2e,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。
  1962 + // (e2e 维度由下一轮行为门 + 后续阶段 testGate 全量回归兜底。)
1933 1963 await runStage(
1934   - g => verifyPrompt(id, 'frontend', `(behaviorRound ${behaviorRound} 行为 fix 后功能复验,本轮 must-fix: ${fixIssues.length} 项)`, specPath, REVIEW_HARD_ROUNDS + behaviorRound) + g,
1935   - { site:`behavior-reverify:${id}:r${behaviorRound}`, grp, label:`behavior-reverify:${id}:r${behaviorRound}`, allowContinue: false },
  1964 + g => behaviorReverifyPrompt(behaviorRound, fixIssues.length) + g,
  1965 + { site:`behavior-reverify:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-reverify:r${behaviorRound}`, allowContinue: false },
1936 1966 )
1937   - // 进入下一 behaviorRound → 重跑本 FE 行为验收
  1967 + // 进入下一 behaviorRound → 重跑行为验收
1938 1968 }
1939   - throw new Error(`HALT behavior-unresolved ${id}: ${BEHAVIOR_FE_MAX} 轮 per-FE 行为子门仍未 green(硬问题未清)`)
  1969 + throw new Error(`HALT behavior-unresolved frontend-phase: ${BEHAVIOR_STAGE_MAX} 轮阶段级行为门仍未 green(硬问题未清)`)
1940 1970 }
1941 1971  
1942 1972 phase('Router')
... ... @@ -2004,13 +2034,17 @@ for (const [idx, module] of todo.entries()) {
2004 2034 phase('Frontend')
2005 2035 // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy
2006 2036 // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达,
2007   - // 使 per-FE 行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。
  2037 + // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。
2008 2038 await runFrontendSkeleton(module.feItems)
2009   - // 前端行为验收已并入 featureLoop→reviewWithFixLoop 的 per-FE approve 子门(reviewer approve 时起本 FE 全栈验
2010   - // 「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才打 req-done)——不再有阶段级末尾独立行为门。
  2039 + // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。
2011 2040 await featureLoop(module.feItems, 'frontend')
  2041 + // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合
  2042 + // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。
  2043 + // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。
  2044 + phase('Behavior')
  2045 + await runBehaviorGate(module.feItems)
2012 2046 phase('Gate')
2013   - await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright)保留,与 per-FE 行为验收职责正交
  2047 + await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交
2014 2048 }
2015 2049 phase('Milestone')
2016 2050 // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"——
... ...