Commit 840dcf7fb0f74251bf403a8e1709a80bbefbf0cf

Authored by yanghl
2 parents 73ee9f6b 1add785b

Merge remote-tracking branch 'origin/workflow' into workflow

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
... ... @@ -129,7 +133,7 @@ erp-workflow-plugin/
129 133 | # | Skill | 作用 | 流程中谁调用 |
130 134 |---|---|---|---|
131 135 | A0 | `project-init` | • **依赖检查**:检测 git / mysql / node 是否在 PATH,缺失则按 OS 自动安装,装不上再停下提示用户<br>• 空目录初始化:用 Read/Write/Glob 工具拷模板创建 CLAUDE.md / docs/01/index.md / docs/08<br>• `git init` | `plan-start` |
132   -| A1 | `scope-lock` | • 引导填项目概述 / 技术栈 / 需求索引<br>• 按 `docs/01-需求清单/<module>/{_module.md, REQ-*.md}` 子目录结构生成 REQ 卡片(CC 据 index.md 填 `{{req_id/title/goal/rules/constraints/acceptance}}` 6 个占位,模板其余内容含输入/输出示例字段表原样复制)<br>• **A1 终结校验**:REQ 6 个占位均填真实数据、无 `{{` 残留、`config-vars.yaml` **全部配置**(包名 / 端口 / 初始账号 + DB 凭据 / 密钥占位)已锁、各 stack 的 build/lint/unit/e2e 命令写入 docs/04 § 零;缺失则在此(Plan 期)用 `AskUserQuestion` 问清(敏感凭据由用户自填,不进会话)<br>• 据模板直接 `Write` 生成 `_module.md` / `REQ-*.md`<br>• 终结校验通过后**自动**调用 `Skill(skeleton-gen)` 进入 A2(不停下) | A0 |
  136 +| A1 | `scope-lock` | • 引导填项目概述 / 技术栈 / 需求索引<br>• 按 `docs/01-需求清单/<module>/{_module.md, <req_id>.md}` 子目录结构生成 REQ 卡片(req_id = `<模块代码>-<子模块代码>-<功能名>`,如 `USR-USR-LOGIN`;CC 据 index.md 填 `{{req_id/title/goal/rules/constraints/acceptance}}` 6 个占位,模板其余内容含输入/输出示例字段表原样复制)<br>• **A1 终结校验**:REQ 6 个占位均填真实数据、无 `{{` 残留、`config-vars.yaml` **全部配置**(包名 / 端口 / 初始账号 + DB 凭据 / 密钥占位)已锁、各 stack 的 build/lint/unit/e2e 命令写入 docs/04 § 零;缺失则在此(Plan 期)用 `AskUserQuestion` 问清(敏感凭据由用户自填,不进会话)<br>• 据模板直接 `Write` 生成 `_module.md` / `<req_id>.md`<br>• 终结校验通过后**自动**调用 `Skill(skeleton-gen)` 进入 A2(不停下) | A0 |
133 137 | A2 | `skeleton-gen` | • 生成架构文档:docs/04 § 一+<br>• 生成跨平台工具脚本:`scripts/*.mjs`(**无 chmod**;凭据 / 配置统一在 A1 产出的 config-vars.yaml)<br>• 据 `gitignore-append-template` 用 Read/Write 并入项目 .gitignore | `plan-start` |
134 138 | A3 | `db-design-gen` | • 套用固定 ERP 约定(列前缀 `i/s/t`、`iIncrement` 主键、`sBrandsId`/`sSubsidiaryId` 租户列)从 docs/01 REQ 卡片正向设计 `docs/03-数据库设计文档.md`(schema SSoT)<br>• 回填 REQ 卡片依赖表(`TBD(A3 自动补)` → 实际表名)<br>• **停下**等人工审阅 docs/03,审阅完毕用 `/plan-start` 续进 A4 | A2 |
135 139 | A4 | `db-init` | • LLM 解析 docs/03 → `sql/migrations/V1__initial_schema.sql`(DDL only)<br>• `node ${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs` 校验 DDL ↔ docs/03(5 维:表/列名/列类型/索引/FK),fail-closed<br>• `node ${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs config-vars.yaml V1.sql`(读取 config-vars.yaml database: 段 + mysql2 apply) | A3 |
... ... @@ -143,17 +147,17 @@ 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  
150 154 | 所属 Skill | 模板文件 | 用途 |
151 155 |---|---|---|
152 156 | project-init | `CLAUDE-template.md` | 项目根的 CLAUDE.md(4 条通用准则 + ERP 专属约定) |
153   -| project-init | `docs-01-index-template.md` | 需求清单索引骨架,等用户填模块表 |
  157 +| project-init | `docs-01-index-template.md` | 需求清单索引骨架,等用户填子模块索引表(五列,一行一个子模块) |
154 158 | project-init | `docs-04-stack-template.md` | docs/04 § 零 默认技术栈总览(零槽位,拷即可) |
155 159 | project-init | `docs-08-initial-template.md` | 工作流进度文件骨架(Plan A0~A5 checkbox) |
156   -| scope-lock | `req-card-template.md` | 单张 REQ-XXX-NNN 卡片模板(`{{req_id/title/goal/rules/constraints/acceptance}}` 占位 + 输入/输出示例字段表;A1 原样复制,只填这 6 个占位) |
  160 +| scope-lock | `req-card-template.md` | 单张 REQ 卡片模板(文件名 == req_id `<模块代码>-<子模块代码>-<功能名>`;`{{req_id/title/goal/rules/constraints/acceptance}}` 占位 + 输入/输出示例字段表;A1 原样复制,只填这 6 个占位) |
157 161 | scope-lock | `_module-template.md` | 模块子目录的 `_module.md` 模块头(模块代码-名 / 简述 / 依赖模块 TBD / 涉及表 TBD) |
158 162 | scope-lock | `config-vars-template.yaml` | 仓库根 `config-vars.yaml` 骨架(跨栈中立):项目**全部配置**——非敏感(包名/端口/前端包名/初始账号)+ 敏感凭据(database / admin_init.password / secrets);A1 E.2 锁定,随项目提交 |
159 163 | skeleton-gen | `docs-04-skeleton-template.md` | docs/04 § 一+ 编码规范大纲(HTML 注释引导 LLM) |
... ...
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/db-design-gen/SKILL.md
... ... @@ -19,7 +19,7 @@ allowed-tools: Read Write Edit Grep Glob
19 19  
20 20 - `docs/04-技术规范.md`
21 21 - `docs/01-需求清单/index.md` 模块索引
22   -- `docs/01-需求清单/*/REQ-*.md` 所有 REQ 卡片
  22 +- `docs/01-需求清单/*/*.md` 所有 REQ 卡片(跳过文件名为 `_module.md` 的模块头;卡片文件名 == req_id)
23 23  
24 24 ### B. 推导 schema
25 25  
... ... @@ -44,7 +44,7 @@ allowed-tools: Read Write Edit Grep Glob
44 44  
45 45 ### D. 回填模块头 + REQ 卡片的 TBD 字段
46 46  
47   -1. 列出 `docs/01-需求清单/*/_module.md`(模块头)和 `docs/01-需求清单/*/REQ-*.md`(REQ 卡片)。
  47 +1. 列出 `docs/01-需求清单/*/*.md`:`_module.md`(模块头)和其余 .md(REQ 卡片,文件名 == req_id)。
48 48 2. 在这些文件中搜索 `TBD(A3 自动补)` 的并回填。 不动 `TBD(A5 自动补)`
49 49 3. 打印回填统计:`A3 回填 <M> 处模块"涉及表" + <N> 处 REQ"依赖表"`。
50 50  
... ... @@ -69,4 +69,4 @@ allowed-tools: Read Write Edit Grep Glob
69 69 - `${CLAUDE_SKILL_DIR}/templates/docs-03-table-template.md`
70 70 - `docs/04-技术规范.md` § 一+(命名规范输入)
71 71 - `docs/01-需求清单/<module>/_module.md`(模块头:回填 `涉及表`)
72   -- `docs/01-需求清单/<module>/REQ-*.md`(REQ 输入 + 回填 `依赖表`)
  72 +- `docs/01-需求清单/<module>/<req_id>.md`(REQ 输入 + 回填 `依赖表`)
... ...
skills/plan/db-design-gen/templates/docs-03-header-template.md
... ... @@ -2,7 +2,7 @@
2 2  
3 3 - **Schema**: `{{schema_name}}`
4 4 - **Migration 清单**: `sql/migrations/V*.sql`(由 Flyway 顺序 apply)
5   -- **生成方式**: 由 A3 `db-design-gen` 基于 `docs/01-需求清单/<module>/REQ-*.md` REQ 卡片正向设计生成(schema SSoT)。
  5 +- **生成方式**: 由 A3 `db-design-gen` 基于 `docs/01-需求清单/<module>/` 下各 REQ 卡片(文件名 == req_id)正向设计生成(schema SSoT)。
6 6  
7 7 ## 项目标准列约定
8 8  
... ...
skills/plan/downstream-gen/SKILL.md
... ... @@ -32,7 +32,7 @@ allowed-tools: Read Write Edit Glob Grep AskUserQuestion
32 32  
33 33 ### B2. 回填模块头 + REQ 卡片的 TBD(A5) 字段
34 34  
35   -1. 在`docs/01-需求清单/*/_module.md`(模块头)和 `docs/01-需求清单/*/REQ-*.md`(REQ 卡片)中搜索并回填 `TBD(A5 自动补)`
  35 +1. 在 `docs/01-需求清单/*/*.md`(模块头 `_module.md` + 全部 REQ 卡片,卡片文件名 == req_id)中搜索并回填 `TBD(A5 自动补)`
36 36 2. 打印回填统计:`A5 回填 <M> 处模块"依赖模块" + <N> 处 REQ"依赖接口"`。
37 37  
38 38 勾选:` - [ ] REQ 卡片依赖接口已回填`
... ... @@ -60,12 +60,12 @@ allowed-tools: Read Write Edit Glob Grep AskUserQuestion
60 60 1. **合并扫描三类问题**(最多 3 轮自主修复,docs/01 是 REQ SSoT 不动)——一次性扫一致性 + `TBD(A3/A5 自动补)` + 结构性残留:
61 61  
62 62 - **一致性**:docs/01 REQ 全部出现在 docs/05;`docs/02 § 二` `module_id` 集合 = `docs/08 § 二` `module_id` 集合。
63   - - **TBD 残留**:`TBD(A3 自动补)` → 查 docs/03 填依赖表;`TBD(A5 自动补)` → 查 docs/05 按 REQ-ID 填依赖接口。
64   - - **结构性**:docs/05 每个 `### REQ-` 端点段含非空 `- **请求**:` / `- **响应**:`(非 `TBD`/`—`/`【人工填写:…】`);docs/01 全部 REQ-ID 都出现在 `docs/02 § 二` 顺序清单。
  63 + - **TBD 残留**:`TBD(A3 自动补)` → 查 docs/03 填依赖表;`TBD(A5 自动补)` → 查 docs/05 按 req_id 填依赖接口。
  64 + - **结构性**:docs/05 每个 `### ` 三级标题端点段(标题形如 `### <req_id> <标题>`)含非空 `- **请求**:` / `- **响应**:`(非 `TBD`/`—`/`【人工填写:…】`);docs/01 全部 req_id 都出现在 `docs/02 § 二` 顺序清单。
65 65  
66 66 命中即按对应步骤规则就地补填(缺端点 → 按 B 推测;缺 module 行 → 按 D 渲染;缺 REQ 顺序行 → 按 A 子流程拓扑插入)。3 轮后仍残留 `【人工填写:】` 或结构缺口 → 打印清单 + 用 `AskUserQuestion` 弹「继续」/「有疑问想先沟通」二选一;每次弹问前重扫一次,避免脏读放行。
67 67  
68   -2. **docs/05 + docs/02 人工评审闸**(未确认不得勾选 A5):摘要展示 docs/05 全部端点(`METHOD PATH — REQ-ID`,标注「由 A5 自动推断」的项)+ docs/02 `req_order[]`(特别标 `note ≠ —` 的环依赖打破项)。`AskUserQuestion` 多问题表单同时问两项:「docs/05 端点/字段无误」+「docs/02 构建顺序可接受」,各二选一 `确认` / `需要修改`。任一需修改 → 收集修改点就地修订并重跑本闸,直到两项均 `确认`;否则禁止勾选 A5、禁止打印横幅。
  68 +2. **docs/05 + docs/02 人工评审闸**(未确认不得勾选 A5):摘要展示 docs/05 全部端点(`METHOD PATH — <req_id>`,标注「由 A5 自动推断」的项)+ docs/02 `req_order[]`(特别标 `note ≠ —` 的环依赖打破项)。`AskUserQuestion` 多问题表单同时问两项:「docs/05 端点/字段无误」+「docs/02 构建顺序可接受」,各二选一 `确认` / `需要修改`。任一需修改 → 收集修改点就地修订并重跑本闸,直到两项均 `确认`;否则禁止勾选 A5、禁止打印横幅。
69 69  
70 70 3. 勾选 A5 父项:`- [ ] A5 下游文档生成 — downstream-gen`
71 71  
... ...
skills/plan/plan-start/SKILL.md
... ... @@ -46,7 +46,7 @@ A 阶段 checkbox 全部 `[x]` 后先跑下面 4 项前移闸门; 全过才放
46 46 1. **REQ 卡片真实数据**(来自 A1 scope-lock)
47 47 - `Glob` 找出全部 REQ 卡片(如 `docs/01-需求清单/**/*.md`)。
48 48 - 对每张卡片 `Grep` 命中以下任一即缺口: `【人工填写` / `TBD` / `{{`(`{{` = 6 个标量占位未替换;`TBD` = A3/A5 应回填的依赖表/依赖接口仍未补——A1 时这些 `TBD(A3/A5 自动补)` 是合法保留,到本闸必须已解析,故此处比 scope-lock E.1 多查 `TBD`/`【人工填写`)。
49   - - 缺口表述示例:`REQ-USER-001 仍含 TBD / {{title}} 占位未替换`。
  49 + - 缺口表述示例:`USR-USR-LOGIN 仍含 TBD / {{title}} 占位未替换`。
50 50  
51 51 2. **全部配置全锁**(来自 A1 写入 `config-vars.yaml` 的非敏感配置 + 敏感凭据,单一文件)
52 52 - `Read` `config-vars.yaml`(项目全部配置,含敏感凭据,随项目提交):校验所有字段均无 `【人工填写`/`TBD`;除 `database.password` 可显式为空串外,其余字段不得为空——含非敏感项(`backend`/`frontend` 包名 / 端口、`admin_init.username`、`database.host/port/user/schema`)与敏感项(`admin_init.password`、`secrets.*`)。
... ... @@ -95,7 +95,7 @@ A 阶段 checkbox 全部 `[x]` 后先跑下面 4 项前移闸门; 全过才放
95 95  
96 96 <逐条列出每个缺口,格式:[闸门] 缺口描述 → 回填位置>
97 97 例:
98   - [REQ 真实数据] REQ-USER-001 仍含 {{goal}} 占位未替换 → docs/01-需求清单/...
  98 + [REQ 真实数据] USR-USR-LOGIN 仍含 {{goal}} 占位未替换 → docs/01-需求清单/...
99 99 [配置] database.password 仍是占位(如本地空密码请显式填 `''`)→ config-vars.yaml
100 100 [docs/04 §零] node 栈缺 e2e 命令 → docs/04-技术规范.md §零
101 101  
... ...
skills/plan/project-init/templates/CLAUDE-template.md
... ... @@ -20,7 +20,7 @@
20 20 1. **严格遵循** `docs/04-技术规范.md`——命名 / 编码 / 统一响应 / 异常处理 / 数据访问 / 配置与安全 等项目专属技术规约全部在此
21 21 2. **严格遵循** `docs/04-技术规范.md § 1.2 分层结构 / § 2.1 目录约定`——文件放对位置
22 22 3. **每个后端接口** 必须先在 `docs/05-API接口契约.md` 定义,再编码实现
23   -4. **每个功能可追溯到 `REQ-XXX-NNN`**——commit tag + 代码注释(如 `// REQ-SYS-001: 用户登录`)+ plan/spec 文件名均用此 tag
  23 +4. **每个功能可追溯到 req_id `<模块代码>-<子模块代码>-<功能名>`**——commit tag + 代码注释(如 `// USR-USR-LOGIN: 用户登录`)+ plan/spec 文件名均用此 tag
24 24 5. **遇到跨模块改动**(动到非当前模块的代码)——允许改,但必须在《模块完成报告》记录原因 / 影响评估(留痕)
25 25  
26 26 ### 你禁止做的 🚫
... ... @@ -56,7 +56,7 @@
56 56 ```
57 57  
58 58 - `scope`: 模块名,如 `user` / `inventory` / `order`
59   -- `subject`: 简短描述;业务类(feat / fix / test)必须带 `REQ-XXX-NNN` 后缀
  59 +- `subject`: 简短描述;业务类(feat / fix / test)必须带 req_id(`<模块代码>-<子模块代码>-<功能名>`,如 `USR-USR-LOGIN`)后缀
60 60  
61 61 `type` 含义:
62 62  
... ...
skills/plan/project-init/templates/docs-01-index-template.md
1 1 # 需求清单
2 2  
3   -> 本目录按模块组织所有功能需求。每个模块一个子目录,含 `_module.md`(模块头)和 `REQ-XXX-NNN.md`(每张 REQ 卡片一个文件)。下方核心功能点供 CC 拆分出 REQ 编号 + 标题 + 草拟规则;卡片内输入 / 输出的简述句和 N 张字段表由人工编辑。
  3 +> 本目录按模块组织所有功能需求。每个模块一个子目录,含 `_module.md`(模块头)和 `<req_id>.md`(每张 REQ 卡片一个文件;req_id = `<模块代码>-<子模块代码>-<功能名>` 恒 3 段,如 `USR-USR-LOGIN`——功能名由 CC 据核心功能点推断:英文大写短词、字符集 `[A-Z0-9_]`、多词用下划线如 `PWD_RESET`、同一 模块-子模块 内唯一)。下方核心功能点供 CC 拆分出 req_id + 标题 + 草拟规则;卡片内输入 / 输出的简述句和 N 张字段表由人工编辑。
4 4  
5 5 ## 模块索引
6 6  
7   -| 模块代码 | 模块名称 | 核心功能点(简要) |
8   -|----------|----------|--------------------|
9   -| 【人工填写:模块代码】 | 【人工填写:模块名称】 | 【人工填写:核心功能点】 |
10   -| SYS | 系统管理 | 用户/角色/权限/部门/字典 等 |
  7 +> 一行一个子模块;同一模块有多个子模块时写多行(模块代码 / 模块名称 重复填写)。
  8 +
  9 +| 模块代码 | 模块名称 | 子模块代码 | 子模块名称 | 核心功能点(简要) |
  10 +|----------|----------|------------|------------|--------------------|
  11 +| 【人工填写:模块代码】 | 【人工填写:模块名称】 | 【人工填写:子模块代码】 | 【人工填写:子模块名称】 | 【人工填写:核心功能点】 |
  12 +| USR | 用户管理 | USR | 用户账户 | 登录/注销/改密 等 |
  13 +| USR | 用户管理 | ROLE | 角色权限 | 角色 CRUD/权限分配 等 |
... ...
skills/plan/project-init/templates/docs-08-initial-template.md
... ... @@ -13,7 +13,7 @@
13 13 - [ ] 项目概述已填写(CLAUDE.md § 🎯 项目概述)
14 14 - [ ] 技术栈已确认(docs/04 § 零)
15 15 - [ ] 需求清单索引已填写(docs/01-需求清单/index.md)
16   - - [ ] REQ 卡片骨架已生成(docs/01-需求清单/<module>/REQ-*.md,业务内容留待人工填写)
  16 + - [ ] REQ 卡片骨架已生成(docs/01-需求清单/<module>/<req_id>.md,业务内容留待人工填写)
17 17  
18 18 - [ ] A2 骨架生成 — skeleton-gen
19 19 - [ ] 架构文档已生成(docs/04 § 一+)
... ...
skills/plan/scope-lock/SKILL.md
... ... @@ -21,23 +21,23 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *)
21 21  
22 22 - **A**:`CLAUDE.md` § 🎯 项目概述。占位符含项目名称 / 简述 / 目标用户 / 部署方式。
23 23 - **B**:`docs/04-技术规范.md` § 零。让用户检查 / 调整默认技术栈表(删不需要的行 / 改技术 / 加条目)。
24   -- **C**:`docs/01-需求清单/index.md`。让用户按业务列出所有模块(每行一个,如 SYS / PUR / SAL),「核心功能点」给关键词即可,CC 会拆 REQ 卡片。
  24 +- **C**:`docs/01-需求清单/index.md`。让用户按业务列出所有子模块(一行一个子模块,五列:模块代码/模块名称/子模块代码/子模块名称/核心功能点;同一模块有多个子模块时写多行,如 USR-USR / USR-ROLE / PUR-ORD),「核心功能点」给关键词即可,CC 会拆 REQ 卡片。
25 25  
26 26 ### D. 生成 REQ 卡片骨架并停下等人工审阅
27 27  
28   -1. 读 `index.md` 解析模块索引;读两个模板 `${CLAUDE_SKILL_DIR}/templates/_module-template.md`、`${CLAUDE_SKILL_DIR}/templates/req-card-template.md` 作为卡片结构参照。
29   -2. **每模块/每 REQ 直接 `Write`**:对每个模块先 `mkdir -p "docs/01-需求清单/<MOD>-<模块名>"`,再照模板结构 `Write`,`<MOD>` / `<模块名>` / `<REQ-MOD-NNN>` 按 `index.md` 实际值替换:
30   - - **模块头** `_module.md`:据 `index.md` 填 `module_code` / `module_name` / `module_brief`;`依赖模块: TBD(A5 自动补)` / `涉及表: TBD(A3 自动补)` 两行原样保留。
31   - - **每个 REQ** `<REQ-MOD-NNN>.md`:**照模板原样 `Write`**,只把 `{{req_id}}` / `{{title}}` / `{{goal}}` / `{{rules}}` / `{{constraints}}` / `{{acceptance}}` 这 6 个占位替换为据 `index.md` 推断的真实值;模板其余内容(`输入` / `输出` 示例字段表、`依赖表: TBD(A3 自动补)` / `依赖接口: TBD(A5 自动补)` 两行)**原样复制不动**;模板顶部 HTML 引导注释**不写进产物**。
  28 +1. 读 `index.md` 解析模块索引(五列,一行一个子模块;按「模块代码」聚合——同一模块的多行共用一个模块子目录);读两个模板 `${CLAUDE_SKILL_DIR}/templates/_module-template.md`、`${CLAUDE_SKILL_DIR}/templates/req-card-template.md` 作为卡片结构参照。
  29 +2. **每模块/每 REQ 直接 `Write`**:对每个模块先 `mkdir -p "docs/01-需求清单/<MOD>-<模块名>"`,再照模板结构 `Write`,`<MOD>` / `<模块名>` / `<req_id>` 按 `index.md` 实际值替换:
  30 + - **模块头** `_module.md`:据 `index.md` 填 `module_code` / `module_name` / `module_brief`(module_brief 汇总该模块全部子模块行的核心功能点);`依赖模块: TBD(A5 自动补)` / `涉及表: TBD(A3 自动补)` 两行原样保留。
  31 + - **每个 REQ** `<req_id>.md`(req_id = `<模块代码>-<子模块代码>-<功能名>` 恒 3 段,如 `USR-USR-LOGIN`;功能名由 CC 据该行核心功能点推断:英文大写短词、字符集 `[A-Z0-9_]`、多词用下划线如 `PWD_RESET`、同一 模块-子模块 内唯一;文件名 == req_id):**照模板原样 `Write`**,只把 `{{req_id}}` / `{{title}}` / `{{goal}}` / `{{rules}}` / `{{constraints}}` / `{{acceptance}}` 这 6 个占位替换为据 `index.md` 推断的真实值;模板其余内容(`输入` / `输出` 示例字段表、`依赖表: TBD(A3 自动补)` / `依赖接口: TBD(A5 自动补)` 两行)**原样复制不动**;模板顶部 HTML 引导注释**不写进产物**。
32 32 3. 用 `Edit` 在 `docs/08-模块任务管理.md` 勾选(A1 子项):
33   - - ` - [ ] REQ 卡片骨架已生成(docs/01-需求清单/<module>/REQ-*.md,业务内容留待人工填写)`
  33 + - ` - [ ] REQ 卡片骨架已生成(docs/01-需求清单/<module>/<req_id>.md,业务内容留待人工填写)`
34 34 4. 打印「请人工填写 REQ 卡片」横幅并提示用户填完后回来继续:
35 35  
36 36 ```
37 37 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
38 38 [scope-lock] REQ 卡片骨架已生成
39 39  
40   - - 产出: docs/01-需求清单/<module>/{_module.md, REQ-*.md}
  40 + - 产出: docs/01-需求清单/<module>/{_module.md, <req_id>.md}
41 41 - 6 个占位已填真实值;输入/输出字段表为模板示例内容(如需可自行调整)。
42 42 - 审阅后选「继续」进 A1 校验.
43 43 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
... ... @@ -54,7 +54,7 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *)
54 54  
55 55 卡片正文是模板原样复制,只有 6 个 `{{}}` 标量由 CC 填——本步只校验这 6 个占位填全填真。
56 56  
57   -1. 用 `Glob` 列出所有 `docs/01-需求清单/<module>/REQ-*.md`
  57 +1. 用 `Glob` 列出 `docs/01-需求清单/*/*.md`,跳过文件名为 `_module.md` 的模块头——其余即 REQ 卡片(文件名 == req_id)
58 58 2. 对每张卡片用 `Read` + `Grep` 校验:
59 59 - **无 `{{` 残留**:不得残留任何 `{{` 占位(命中即说明 6 个标量未全部替换)。
60 60 - **6 个标量为真实值**:`req_id` / `title` / `goal` / `rules` / `constraints` / `acceptance` 均据 `index.md` 填为真实内容,非空、非回显占位名。
... ... @@ -93,7 +93,7 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *)
93 93 ✓ docs/04 § 零 技术栈 + build/lint/unit/e2e 命令
94 94 ✓ docs/01-需求清单/index.md 模块索引
95 95 ✓ docs/01-需求清单/<module>/_module.md 模块头
96   - ✓ docs/01-需求清单/<module>/REQ-*.md 6 个占位已填真实值(字段表为模板示例)
  96 + ✓ docs/01-需求清单/<module>/<req_id>.md 6 个占位已填真实值(字段表为模板示例)
97 97 ✓ config-vars.yaml 配置已锁(非敏感已填;DB 凭据 / 密钥占位待人工填,plan-start 把关)
98 98  
99 99 自动进入 A2:skeleton-gen
... ... @@ -108,7 +108,7 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *)
108 108 - `docs/04-技术规范.md`
109 109 - `docs/01-需求清单/index.md`
110 110 - `docs/01-需求清单/<module>/_module.md`
111   -- `docs/01-需求清单/<module>/REQ-*.md`
  111 +- `docs/01-需求清单/<module>/<req_id>.md`
112 112 - `config-vars.yaml`
113 113 - `${CLAUDE_SKILL_DIR}/templates/config-vars-template.yaml`
114 114 - `${CLAUDE_SKILL_DIR}/templates/req-card-template.md`
... ...
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 请求封装
... ... @@ -28,7 +34,7 @@
28 34 ## 三、共同约定
29 35  
30 36 ### 3.1 Git 提交
31   -`<type>(<scope>): <subject> REQ-XXX-NNN`
  37 +`<type>(<scope>): <subject> <req_id>`(req_id = `<模块代码>-<子模块代码>-<功能名>`,如 `USR-USR-LOGIN`)
32 38  
33 39 ### 3.2 分页查询
34 40  
... ...
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:{
... ... @@ -218,7 +237,7 @@ function featureStageContract(phase) {
218 237 `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe
219 238 ? '实现文件必须落在 `frontend/` 下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。'
220 239 : '产出范围限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约;**禁止**写 `frontend/` 路径下的实现(UI 推迟到前端阶段)。'}`,
221   - `- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `REQ-XXX-NNN`。'}`,
  240 + `- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `<模块代码>-<子模块代码>-<功能名>`(3 段大写 req_id,如 `USR-USR-LOGIN`)。'}`,
222 241 ].join('\n')
223 242 }
224 243  
... ... @@ -302,7 +321,7 @@ function deriveSpecPrompt(id, phase) {
302 321 fe
303 322 ? [
304 323 '',
305   - '## 行为验收作用域结构化小节(per-FE 行为门唯一断言依据,**强制写到 spec 头部**)',
  324 + '## 行为验收作用域结构化小节(阶段末尾行为门按全部 FE 聚合断言作用域的唯一来源,**强制写到 spec 头部**)',
306 325 '- 在 spec 文件头部(紧随标题/关联 REQ 之后)写一个**结构化小节**,标题逐字为 `## 行为验收作用域`,内含两条机器可读清单:',
307 326 ' ```',
308 327 ' ## 行为验收作用域',
... ... @@ -310,8 +329,8 @@ function deriveSpecPrompt(id, phase) {
310 329 ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]',
311 330 ' ```',
312 331 `- **关联路由**:从 \`${ROOT}/frontend/\` router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 \`:id\` 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`,
313   - '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外 / 共享控件归 coverageGap,绝不算本 FE 缺陷。',
314   - '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。',
  332 + '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外控件记证据不算缺陷。',
  333 + '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes;阶段末尾的行为门会聚合**全部 FE** 的该小节作为整体断言作用域,缺失的 FE 会被记 `scope-missing` 阻断 green);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。',
315 334 ].join('\n')
316 335 : '',
317 336 '',
... ... @@ -353,7 +372,7 @@ function planPrompt(id, phase, specPath) {
353 372 '## 任务结构(每个 task = 一个 red-green-commit 单元,4 step)',
354 373 '1. 写失败测试(给 `test_file::test_name` + 测试意图);2. 实现最小代码(给 `impl_file`);3. 子会话验证 PASS;4. commit。任务粒度 2-5 分钟。',
355 374 fe
356   - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头;命中 - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头backend/- ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头 / - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头sql/- ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头 / - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头scripts/- ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头 → 修正后重渲染。`
  375 + ? `- **硬护栏**:每个任务 \`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) → 修正后重渲染。`
357 376 : `- **硬护栏**:任务粒度限定后端文件(controller / service / repository / DTO / 校验 / SQL migration);**禁止**生成 \`frontend/\` 路径任务。`,
358 377 '- 允许写死的少数场景:DDL / migration 语句、合同级常量(错误码 / JWT claim / Redis key / 路由 path / API client 签名 / Design Tokens 名)、可选的测试断言 sketch。其余一律散文 + 签名描述。',
359 378 '- 首次出现的类 / 方法 / 组件 / hook / API client 函数必须给出签名;跨 task 的签名 / 错误码 / props 类型必须一致。',
... ... @@ -390,13 +409,13 @@ function tddPrompt(id, phase, planPath) {
390 409 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。',
391 410 '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。',
392 411 fe
393   - ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。'
  412 + ? '- **测试落位(硬约定,对齐 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 复刻。'
394 413 : '',
395 414 fe
396 415 ? '- **e2e 基线约束**:e2e 跑在「空库重建 + Flyway schema + 演示种子」基线上(骨架 globalSetup 已注入 `sql/seed`,无需测试自行建库/起栈)。e2e 断言**优先**定位**演示种子已知主键行**(1000–9999)或**测试自建数据**;**禁止**「全表恰好 N 行」式依赖全局计数的脆弱断言(演示种子行数会随后续模块种子增长,全局计数断言必然 flaky)。'
397 416 : '',
398 417 fe
399   - ? `- **占位替换(保证中途可构建 + 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 指向真组件、可构建可达。`
  418 + ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 + ? `- **占位替换(保证中途可构建 + 阶段末尾FeStub+ ? `- **占位替换(保证中途可构建 + 阶段末尾。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 + ? `- **占位替换(保证中途可构建 + 阶段末尾FeStub+ ? `- **占位替换(保证中途可构建 + 阶段末尾 改为本 FE 真组件(用 Grep 在 + ? `- **占位替换(保证中途可构建 + 阶段末尾${ROOT}/frontend/+ ? `- **占位替换(保证中途可构建 + 阶段末尾 router 定位本 FE 路由 path 的 import 行;仍在 + ? `- **占位替换(保证中途可构建 + 阶段末尾frontend/+ ? `- **占位替换(保证中途可构建 + 阶段末尾 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。`
400 419 : '',
401 420 '',
402 421 '## 护栏',
... ... @@ -472,8 +491,8 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) {
472 491 '## 输入给 reviewer',
473 492 `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`,
474 493 fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '',
475   - `- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`,
476   - fe ? `- **行为验收作用域小节校验(per-FE 行为门前置真值,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——行为门只能据此确定本 FE 路由作用域。` : '',
  494 + `- **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 → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`,
  495 + fe ? `- **行为验收作用域小节校验(阶段级行为门的作用域真值来源,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——阶段末尾的行为门按全部 FE spec 的该小节聚合断言作用域,缺失/错配会让该 FE 漏验或归因失真。` : '',
477 496 round > 1 && lastVerifySummary
478 497 ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。`
479 498 : '',
... ... @@ -641,73 +660,74 @@ function seedGenPrompt(module) {
641 660 ].filter(Boolean).join('\n')
642 661 }
643 662  
644   -// ---- 前端行为验收(per-FE behavior 子门)----
645   -// 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。
646   -// 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发,
647   -// 起本 FE 全栈 + sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才放行 approve。
  663 +// ---- 前端行为验收(阶段级行为门,v3)----
  664 +// 设计权威:docs/design/2026-06-05-frontend-behavior-stage-gate.md。
  665 +// 时机:featureLoop(frontend) 全部 FE 通过静态 review(req-done tag 已打)之后、testGate 之前,
  666 +// 整个前端阶段只跑**一次**行为验收:起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合的作用域并集
  667 +// 枚举路由控件/文字,硬问题转可 fix must-fix→fix→复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮),green 才进 testGate。
648 668 // 门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend')
649 669 // (其路径护栏命中 backend/sql/scripts 即越界硬停,与门必须运行 setup-test-db / 起后端 / 生成 SQL 种子自相矛盾)。
650 670  
651 671 // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符;
652 672 // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright,
653   -// 唯一**可写** = .tmp/behavior-gate/<FE>/r<behaviorRound>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。
  673 +// 唯一**可写** = .tmp/behavior-gate/frontend-phase/r<behaviorRound>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。
654 674 function behaviorGateContract() {
655 675 return [
656 676 '## 硬约束(非交互行为验收子代理)',
657 677 '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。',
658   - '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。',
  678 + '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「每个 FE 的每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。',
659 679 '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。',
660   - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`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)。`,
  680 + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`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)。`,
661 681 `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`,
662   - '- **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 缺陷。',
  682 + '- **全量终态前提(关键)**:本门跑在**全部 FE 已实现并通过静态 review 之后**——`frontend/` 不应再有未实现路由 / FeStub 占位。某路由仍渲染 `data-fe-stub` 占位 → 这是硬缺陷(tdd 漏做占位替换),归 `interactionFailures[kind="no-observable-effect"]`,locator 指向 router 文件该路由 import 行,detail 写明「路由仍指向 FeStub 占位」。**断言作用域 = 全部 FE spec 的 `## 行为验收作用域` 小节并集**;白名单外控件记证据不入断言集。',
663 683 '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。',
664 684 '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。',
665   - '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / FE id / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。',
  685 + '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。',
666 686 ].join('\n')
667 687 }
668 688  
669   -// behaviorGatePrompt:per-FE 行为验收子代理的完整流水线提示(step0-6 + schema)。
670   -// id:本 FE id(如 FE-07);specPath:本 FE spec(含 ## 行为验收作用域 小节,feScope 来源 + 日期前缀);
671   -// behaviorRound:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX);attempt:本轮内环境 race 重试序号(1..)。
672   -// 每 (FE × behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。
673   -function behaviorGatePrompt(id, specPath, behaviorRound, attempt) {
674   - const safeId = id ?? 'FE'
675   - const tmpDir = `${ROOT}/.tmp/behavior-gate/${safeId}/r${behaviorRound}`
676   - const date = (() => { try { return dateFromArtifactPath(specPath) } catch { return '<date>' } })()
677   - const evidence = `docs/superpowers/reviews/${date}-${safeId}-behavior-r${behaviorRound}-a${attempt}.md`
  689 +// behaviorGatePrompt:阶段级行为验收子代理的完整流水线提示(step0-6 + schema)。
  690 +// feItems:本前端阶段全部 FE-NN(作用域聚合的清单真值,来自 Router frontend-phase 模块);
  691 +// behaviorRound:阶段门内的行为 fix 轮(1..BEHAVIOR_STAGE_MAX);attempt:本轮内环境 race 重试序号(1..)。
  692 +// 每 (behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。
  693 +function behaviorGatePrompt(feItems, behaviorRound, attempt) {
  694 + const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(调用方未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)'
  695 + const tmpDir = `${ROOT}/.tmp/behavior-gate/frontend-phase/r${behaviorRound}`
  696 + const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md`
678 697 return [
679   - `# behavior — 前端 per-FE 行为验收(headless,FE=${safeId}, behaviorRound=${behaviorRound}, attempt=${attempt})`,
  698 + `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`,
680 699 '',
681 700 behaviorGateContract(),
682 701 '',
683 702 '## 目标',
684   - `用真实全栈运行证明本 FE \`${safeId}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`,
685   - `单个子会话内**收敛完成**:冷起栈 → 逐**本 FE 路由**枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`,
686   - `- 本 FE 行为验收作用域唯一真值 = spec \`${specPath}\` 头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先 Read 该 spec 取出 feScope;缺该小节 → \`envError.kind="stack-not-ready"\` 并在 detail 写明(不应出现:reviewer 已校验它存在)。`,
  703 + '用真实全栈运行证明**全部 FE** 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。整个前端阶段只跑这一道行为门(featureLoop 全部 FE 已过静态 review)。',
  704 + '单个子会话内**收敛完成**:冷起栈 → 逐路由枚举(全 FE 作用域并集)+ 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。',
  705 + `- 本阶段 FE 清单:${feList}。`,
  706 + `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT}/docs/superpowers/specs/<date>-<FE-NN>.md\`,同一 FE 多份取最新日期)头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先逐 FE Read 取出并**聚合为并集**(路由去重、逐路由标注归属 FE);某 FE 缺 spec 或缺该小节 → 记 \`coverageGaps[reason="scope-missing", page="<FE-NN>"]\`(该 FE 路由不计入分母,**绝不**静默跳过)。`,
687 707 behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '',
688 708 '',
689 709 '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)',
690 710 '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。',
691 711 `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1
692   - ? `本次是本 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。`
  712 + ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子),再新建本轮子目录 + ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子${tmpDir}/+ ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子。`
693 713 : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`,
694   - `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本 FE 起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`,
  714 + `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`,
695 715 `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`,
696 716 `- \`${ROOT}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`,
697 717 '',
698   - '## step0 探测 + build 归因(确定性短路前置,依赖 build-failed kind)',
  718 + '## step0 探测 + build 归因',
699 719 `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/config-vars.yaml\`。`,
700 720 '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。',
701   - '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——',
702   - ` - 落在**非本 FE 的 \`frontend/\` 路径**(兄弟 FE 组件缺失 / 骨架占位未覆盖 / 指向未建路由)→ \`envError.kind="build-failed"\` + \`rootCausePath=<非本FE路径>\`(**预期中途态**,不是本 FE bug)。`,
703   - ' - 落在**本 FE 路径**(feScope 关联组件)→ 才是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]`(带 locator=组件文件)。',
  721 + '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——全部 FE 已实现,**没有**「兄弟未实现」豁免:',
  722 + ' - 根因落在 `frontend/` 源码且可定位到文件 → 真构建 bug → 归 `interactionFailures[kind="js-error"]`(locator=根因文件路径,可转 must-fix 喂 fix)。',
  723 + ' - 根因不可归到 `frontend/` 源码(依赖 / 工具链 / 无法定位)→ `envError.kind="build-failed"`(如能定位仍填 `rootCausePath`)。',
704 724 ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。',
705 725 '',
706   - '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)',
707   - '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。',
708   - '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。',
  726 + '## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)',
  727 + '- 分母来源 = 全部 FE spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。',
  728 + `- 与 \`${ROOT}/frontend/\` router 配置对账:FE 作用域声明但 router 缺失的路由 → \`coverageGaps[reason="unreachable-no-route"]\`;router 声明但不属任何 FE 作用域的路由记证据(不入分母、不断言)。`,
  729 + '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。',
709 730 '- 带参动态路由用**种子已知主键**实例化(可用**演示种子已知主键**(1000–9999)或 **sentinel 主键**(≥100000));无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。',
710   - '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。',
711 731 '',
712 732 '## step2 起栈五段严格时序(schema 由 Flyway 在后端启动时才建)',
713 733 `1) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程;脚本失败按普通 \`stack-not-ready\` 处理。`,
... ... @@ -716,54 +736,89 @@ function behaviorGatePrompt(id, specPath, behaviorRound, attempt) {
716 736 '4) **此时才跑 sentinel 种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** sentinel 种子(先父后子;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)。失败 → `envError.kind="seed-error"` + 结构化根因,**不**混进交互 RED。',
717 737 ' - **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_]` 格式。',
718 738 '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。',
719   - '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。',
  739 + '- `finally` **硬要求 kill 本起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。',
720 740 '',
721 741 '## step2.5 鉴权 bootstrap(确定性前置)',
722 742 '- 用 config-vars `admin_init` 或种子已知凭据,经 `docs/05` 登录端点**真实登录**拿 JWT,注入 Playwright `storageState`;`authState` 记角色覆盖(覆盖 / 未覆盖角色集)。',
723 743 '- 登录失败 = `envError.kind="auth-failed"`(环境 race,走 retry),**绝不**当成死控件。',
724 744 '',
725   - '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;只驱动本 FE feScope)',
726   - '- **只枚举/驱动 feScope.routes + feScope.controlWhitelist**(本 FE 白名单控件)。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 本 FE 推导清单,分子 = live 枚举。',
  745 + '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;驱动全部 FE 作用域并集)',
  746 + '- **枚举/驱动 step1 聚合的全部路由 + 各 FE 控件白名单并集**。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 聚合清单,分子 = live 枚举。',
  747 + '- **FeStub 残留检测**:每路由加载后检查 `data-fe-stub` 元素;仍渲染占位 → 该 FE 的 tdd 漏做占位替换(硬缺陷),归 `interactionFailures[kind="no-observable-effect"]`(locator=router 文件该路由 import 行,detail 写「路由仍指向 FeStub 占位」)。',
727 748 '- 分母有但首帧无的控件:runner 尝试**驱动到出现态**(种子保列表非空触发行级操作 / 进多步流程下屏 / 展开 dropdown / 切 tab 后二次枚举);仍不可达 → `coverageGaps[reason="deep-control-not-driven"]`,不静默判 green。到不了的路由 → `coverageGaps[reason="unreachable-auth"|"unreachable-no-route"]`,与「到达了但控件死」严格区分。',
728   - '- **白名单外 / 共享控件**:若属其它未 approve FE 或共享区 → 归 `coverageGaps[reason="deep-control-not-driven"]`,**绝不**归本 FE 的 `interactionFailures`。',
  749 + '- **白名单外控件**(任何 FE 白名单都未列,如共享导航/布局区):不入「必须有效果」断言集,记证据即可;确属可疑死控件可记 `coverageGaps[reason="deep-control-not-driven"]`。',
729 750 '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。',
730   - '- `routesReached` / `controlsEnumerated` 据实填(本 FE 子集空覆盖必须可见)。',
  751 + '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。',
731 752 '',
732 753 '## step4 推导期望',
733 754 '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。',
734 755 '',
735   - '## step5 断言(两层 + 可观测效果白名单 + 硬问题带源码 locator)',
  756 + '## step5 断言(三层:交互/文字/样式 + 可观测效果白名单 + 硬问题带源码 locator)',
736 757 '- **交互层可观测效果白名单**: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`)。',
737 758 ' - 无任何效果 → `interactionFailures[kind="no-observable-effect"]`;JS 异常 → `js-error`;`console.error` → `console-error`;应发未发网络调用 → `missing-docs05-call`。断言用 auto-waiting / `expect.poll`,**不用**固定 sleep。',
738 759 '- **文字层**:动态文字格对比该 region 字段的唯一 sentinel(抓绑错字段)。',
739 760 '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。',
740   - '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 approve)。',
  761 + '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 green)。',
  762 + '- **样式层(客观断言:颜色 token 比对 + layout sanity)**。断言作用域 = **白名单控件及其直接容器 + spec/prototype 点名区域**;组件库深层内部元素**不查**,只查可见元素:',
  763 + ` - **色值基准确定性派生**:读 \`${ROOT}/src/styles/tokens.css\` 解析全部 \`--color-*\`,用探针元素 getComputedStyle 把任意色值格式归一化为 canonical rgb 集合;被检元素的渲染值(\`color\` / \`background-color\` / \`border-color\`)同法归一化后比对。`,
  764 + ' - **颜色断言**:渲染色 ∉ token 集合 → `styleIssues[kind="non-token-color"]`;spec「Design Tokens 引用清单」点名了具体 token 的元素,渲染值 ≠ 该 token 解析值 → `styleIssues[kind="token-mismatch"]`。半透明混合 / 无法归一化的值 → **不入** styleIssues,记 `decisions[]`(宁漏勿误)。',
  765 + ' - **几何断言(layout sanity)**:每路由 `scrollWidth > clientWidth + 1` → `styleIssues[kind="horizontal-overflow"]`(locator=该路由 view 组件文件);白名单控件两两 boundingBox 交叠 >4px² 且双方可见非 inert → `overlap`;预期可见控件 box 为零 → `zero-size`;`scrollIntoViewIfNeeded` 后仍不在视口 → `offscreen`。',
  766 + ' - **视口**:默认用 Playwright 默认视口;prototype 明确声明 viewport 时用之并记 `decisions[]`。',
  767 + ' - styleIssues 全部是客观硬问题:locator 要求与交互层逐字同口径(A 类反查组件文件;反查不出归 `coverageGaps[reason="locator-not-resolvable"]`),`expected`/`actual` 写比对双方的具体值(如 `expected="var(--color-primary)→rgb(22,119,255)" actual="rgb(255,0,0)"`)。',
741 768 '- **行为硬问题必须带源码 locator(转 must-fix 喂 fix 的前置)**:',
742   - ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。',
  769 + ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 归属 FE + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。',
743 770 ' - **B 类(连组件文件都反查不出)**:**不静默降级放行**——归 `coverageGaps[reason="locator-not-resolvable"]`(计入未覆盖,使本轮不能判 green),或归 `envError.kind="stack-not-ready"` 走 retry。绝不把无 locator 的硬问题塞进 `interactionFailures` 不带 locator(上层会因无 locator 走 adjudicate(allowContinue:false),绝不放行)。',
744 771 '',
745 772 `## step6 证据落盘 + commit(运行时行为,沿用证据 commit 习惯)`,
746   - `- 写 \`${evidence}\`:本 FE feScope / 推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`,
747   - `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/reviews/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`,
  773 + `- 写 \`${evidence}\`:**按 FE 分小节**(每 FE:作用域 / 推导期望 / 逐控件判定 / 该 FE 的 styleIssues 与 coverageGaps),全局段写 routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ token 色值集合摘要 / 截图索引。`,
  774 + `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/module-reports/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`,
748 775 `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`,
749   - commitBlock(`${evidence} docs/superpowers/reviews/assets`,
750   - `docs(behavior:${safeId}:r${behaviorRound}-a${attempt}): per-FE 行为验收证据`),
  776 + commitBlock(`${evidence} docs/superpowers/module-reports/assets`,
  777 + `docs(behavior:frontend-phase:r${behaviorRound}-a${attempt}): 阶段级行为验收证据`),
751 778 '',
752 779 '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)',
753   - '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无阻断性 envError + 本 FE 覆盖非空)| `red`。',
754   - '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数本 FE feScope**;空覆盖必须可见)。',
755   - '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`(含 `source="sentinel"` 的 textIssue)。',
  780 + '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + **样式层无失败** + 无阻断性 envError + 覆盖非空)| `red`。',
  781 + '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数全部 FE 作用域并集**;空覆盖必须可见)。',
  782 + '- `interactionFailures` / `textIssues` / `styleIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`(含 `source="sentinel"` 的 textIssue 与全部 styleIssues)。',
756 783 '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids;`build-failed` 时填 `rootCausePath`。',
757 784 '- 做过任何自主默认 → `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。',
758 785 '- 不要返回额外字段(schema 是 `additionalProperties:false`)。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。',
759 786 ].filter(Boolean).join('\n')
760 787 }
761 788  
  789 +// behaviorReverifyPrompt:阶段级行为 fix 后的功能复验。fix 改的是 frontend/ UI 源码,可能引入功能回归——
  790 +// 在下一 behaviorRound 重起全栈之前,先派子会话跑**全量前端单测**(vitest,不跑 e2e——e2e/行为维度由下一轮
  791 +// 行为门重跑 + 阶段 testGate 全量回归兜底),红则当功能回归硬边界(调用方 allowContinue:false)。
  792 +function behaviorReverifyPrompt(behaviorRound, fixedCount) {
  793 + const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-reverify-r${behaviorRound}.md`
  794 + return [
  795 + `# behavior-reverify — 行为 fix 后前端单测复验(behaviorRound=${behaviorRound})`,
  796 + '',
  797 + featureStageContract('frontend'),
  798 + '',
  799 + '## 目标',
  800 + `阶段级行为门第 ${behaviorRound} 轮 fix(${fixedCount} 项 must-fix)改动了 \`frontend/\` 源码——**派发 Agent 子会话**跑全量前端单测确认无功能回归。**主会话从不直接跑测试,也不自由编写证据。**`,
  801 + '',
  802 + '## 流程',
  803 + `- 命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` 取(缺失默认 \`pnpm test:ci\`);**只跑单测(vitest),不跑 e2e**。`,
  804 + '- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行)。',
  805 + '- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt(fix 引入功能回归,绝不带红进入下一轮行为门)。',
  806 + `- 证据写入 \`${evidence}\`(每轮独立文件不覆盖前轮)。`,
  807 + '',
  808 + commitBlock(evidence, `docs(behavior:frontend-phase:r${behaviorRound}): 行为 fix 后单测复验`,
  809 + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'),
  810 + '',
  811 + '## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
  812 + `- 全部通过:\`{ "status": "ok", "artifactPath": "${evidence}", "summary": "<exit_code / passed / failed 摘要>" }\`。`,
  813 + '- 任一红色 / 缺值 → `{ "status": "halt", "reason": "<失败用例摘要>" }`。',
  814 + ].join('\n')
  815 +}
  816 +
762 817 // ---- 前端骨架占位 stage(runFrontendSkeleton 用)----
763   -// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A,blocker)。
  818 +// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A;v3 阶段级行为门下仍保留)。
764 819 // 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(未实现 FE 路由指向 FeStub 占位)
765 820 // + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。
766   -// 由此 per-FE 行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点、build-failed 退化为罕见兜底
  821 +// 由此逐 FE 的 verify(e2e) 与阶段末尾行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点
767 822 // feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。
768 823 function frontendSkeletonPrompt(feItems) {
769 824 const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)'
... ... @@ -798,6 +853,7 @@ function frontendSkeletonPrompt(feItems) {
798 853 ` - **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 复用)。`,
799 854 ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树。',
800 855 ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 空库重建 + Flyway schema + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐。',
  856 + '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/ 镜像路径后方可收窄)。',
801 857 '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。',
802 858 '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。',
803 859 '',
... ... @@ -823,9 +879,10 @@ function frontendSkeletonStatePromptM(feItems) {
823 879 '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)',
824 880 microStepContract(),
825 881 '',
826   - `用 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.*\`)。`,
827   - '- 全部满足(骨架已建齐,含 e2e 基线脚手架)→ `{ "exists": true }`',
828   - '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup)→ `{ "exists": false }`',
  882 + `用 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.*\`。`,
  883 + `- **legacy 豁免**:若 \`${ROOT}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`,
  884 + '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`',
  885 + '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`',
829 886 '## 输出(EXISTS_SCHEMA)',
830 887 ].join('\n')
831 888 }
... ... @@ -851,11 +908,11 @@ function microStepContract() {
851 908 // ============================================================================
852 909  
853 910 const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环)
854   -// per-FE 行为子门预算(二维,钉死防证据覆盖;设计 §6.4):
855   -// - BEHAVIOR_FE_MAX = approve 子门内的行为 fix 轮硬上限(每 FE);超限 throw HALT。**不**复用 review 的 10 轮、
856   -// **不**让 REVIEW_HARD_ROUNDS × 行为重试隐式相乘——典型一次过(1 轮),最坏 3 轮。
  911 +// 阶段级行为门预算(二维,钉死防证据覆盖):
  912 +// - BEHAVIOR_STAGE_MAX = 阶段门内的行为 fix 轮硬上限(整个前端阶段共用,每轮 fix 可批量修当轮全部 must-fix);
  913 +// 超限 throw HALT。典型一次过(1 轮),最坏 3 轮。
857 914 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。
858   -const BEHAVIOR_FE_MAX = 3
  915 +const BEHAVIOR_STAGE_MAX = 3
859 916 const BEHAVIOR_ATTEMPT_MAX = 2
860 917 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : ''
861 918  
... ... @@ -1264,7 +1321,7 @@ function classifyCrossModulePromptM(moduleId, files) {
1264 1321 '- 落在共享根(如 `docs/`、`scripts/`、`sql/migrations/`、`README.md` 等)→ **不算**跨模块。',
1265 1322 '',
1266 1323 '## 输出(CROSS_CLASSIFY_SCHEMA)',
1267   - '- `{ "crossModule": [ { "file": "...", "targetModule": "module_x", "reason": "<本模块哪个 REQ-XXX-NNN 迫使改它,1 句>", "impact": "<目标模块哪些 API/行为/调用方/测试受影响,1-3 句>" }, ... ] }`',
  1324 + '- `{ "crossModule": [ { "file": "...", "targetModule": "module_x", "reason": "<本模块哪个 REQ(req_id,如 USR-USR-LOGIN)迫使改它,1 句>", "impact": "<目标模块哪些 API/行为/调用方/测试受影响,1-3 句>" }, ... ] }`',
1268 1325 '- 无跨模块改动:`{ "crossModule": [] }`',
1269 1326 '- **不要留 `TBD(CC 补)`**:本步骤就是补齐的唯一时机;推不出原因 / 影响 → 整步失败(schema 失败即可,调用方会 halt)。',
1270 1327 ].join('\n')
... ... @@ -1324,7 +1381,7 @@ function reportPrompt(module) {
1324 1381 '## 前置',
1325 1382 `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`,
1326 1383 fe
1327   - ? `- **前端行为验收已并入 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(仅在 § ⑤/⑧ 标注)。`
  1384 + ? `- **验证阶段级行为门绿**:Glob \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`,按 behaviorRound → attempt 数字升序读取。**最后一份必须非 RED(绿)**;最后一份 red 或一份证据都没有 → 立即 halt(绝不在行为红 / 无行为证据上打 milestone)。注意 per-FE 行为证据(\`reviews/<date>-<FE>-behavior-*\`)已不再产生,不要校验。`
1328 1385 : '',
1329 1386 '',
1330 1387 '## 收集输入(取摘要而非正文)',
... ... @@ -1334,8 +1391,8 @@ function reportPrompt(module) {
1334 1391 `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`,
1335 1392 `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`,
1336 1393 '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。',
1337   - `- § ⑤:把 \`${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 记录一并纳入 § ⑤ 汇总**。`,
1338   - `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/<date>-FE-*-behavior-r*-a*.md\` 汇总各 FE 的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`,
  1394 + `- § ⑤:把 \`${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 记录一并纳入 § ⑤ 汇总**。`,
  1395 + `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按阶段级行为证据 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(取最后一份的逐 FE 小节)汇总各 FE 的 \`coverageGaps\` + 样式 \`styleIssues\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`,
1339 1396 '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。',
1340 1397 ].join('\n')
1341 1398 : [
... ... @@ -1632,9 +1689,6 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1632 1689 let lastVerify = verifyResult
1633 1690 let lastIssuesCount = 0
1634 1691 let reviewGuidance = '' // 仲裁 retry 时注入下一轮 review 的纠正指令
1635   - // softPassed 提升到 reviewWithFixLoop 顶层作用域(与本 FE review 同寿命,跨 behaviorRound 持久)——
1636   - // 行为软文字一旦被仲裁 continue 放行(降级),重跑后即便仍在 textIssues 也不再追问,避免反复消耗仲裁预算。
1637   - const behaviorSoftPassed = new Set()
1638 1692 for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) {
1639 1693 const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || ''
1640 1694 // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。
... ... @@ -1645,13 +1699,9 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1645 1699 reviewGuidance = '' // 已消费
1646 1700  
1647 1701 if (r.verdict === 'approve') {
1648   - // approve 闸显式 AND(设计 §6.2):reviewer.verdict==='approve' ∧ behaviorSubGate green(仅前端)。
1649   - // 后端逐字不变(无行为维度);前端:静态 approve 后**不立即 return**,先进 per-FE 行为 approve 子门——
1650   - // 起本 FE 全栈验「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才放行;
1651   - // 行为 green ⇒ 才 flipDocs08Checkbox + return(req-done tag 落点 featureLoop 不动,语义自动升级为「静态过+行为过」)。
1652   - if (fe) {
1653   - await behaviorSubGate(id, specPath, grp, behaviorSoftPassed)
1654   - }
  1702 + // approve = 静态 review 通过即放行(前后端同构)。行为验收已挪到阶段末尾的 phase('Behavior')
  1703 + // 一次性阶段级行为门(runBehaviorGate)——不再是 per-FE approve 子门。req-done/<FE> 语义 =「静态过」;
  1704 + // 行为维度由阶段门统一验收,行为 green 是 milestone 的前置(reportPrompt 校验行为证据非 RED)。
1655 1705 await flipDocs08Checkbox(fe, id, phase, grp)
1656 1706 return { id, phase, approved:true, rounds:round }
1657 1707 }
... ... @@ -1665,9 +1715,8 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1665 1715 const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`,
1666 1716 { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)',
1667 1717 reviewerIssues: r.issues || [] }, grp, round)
1668   - // continue 视为「无 must-fix → 静态 approve」——前端仍须先过行为 approve 子门(行为 green 是任何 approve return 的前置)。
  1718 + // continue 视为「无 must-fix → 静态 approve」(行为维度由阶段末尾的行为门统一验收,不在此处)。
1669 1719 if (verdict.action === 'continue') {
1670   - if (fe) await behaviorSubGate(id, specPath, grp, behaviorSoftPassed)
1671 1720 await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round }
1672 1721 }
1673 1722 if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`)
... ... @@ -1721,125 +1770,96 @@ async function testGate(module, phase) {
1721 1770 return g
1722 1771 }
1723 1772  
1724   -// ---- 前端 per-FE 行为验收控制流(runBehaviorGateOnce + behaviorSubGate)----
1725   -// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 6.3 / 7。
1726   -// 行为验收并入 per-FE reviewWithFixLoop 的 approve 子门——reviewer 即将 approve 时才触发,绝不每 review round 起栈。
1727   -// behaviorSubGate 失败分层(per-FE 缩 scope,保留原 runBehaviorGate 的分层语义):
1728   -// - build-failed(兄弟 FE 未实现 / 占位未覆盖,根因落非本 FE 路径)= 确定性短路:记 coverageGap + decisions,
1729   -// 本轮行为门视为「本 FE 非缺陷」直接放行 approve(预期中途态,不 retry 不 halt)。
1730   -// - envError(其它) / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。
1731   -// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 approve。
1732   -// - behaviorHard = interactionFailures + sentinel textIssues:有 locator → 降维喂 fixPrompt 跑 fix(fix 后功能 reverify + 下一轮重跑行为);
1733   -// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不 approve。
1734   -// - BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。
  1773 +// ---- 前端阶段级行为验收控制流(runBehaviorGateOnce + runBehaviorGate)----
  1774 +// 设计:docs/design/2026-06-05-frontend-behavior-stage-gate.md(v3)。
  1775 +// 时机:顶层 frontend 段 featureLoop 全部 FE 完成(req-done 已打)之后、testGate 之前,phase('Behavior') 下
  1776 +// 整个前端阶段只跑**一次**行为验收(含 fix 循环)。失败分层:
  1777 +// - envError / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。
  1778 +// build-failed(阶段末尾无「兄弟未实现」豁免)属确定性失败:跳过自动 attempt 重起(重跑不自愈),直送仲裁。
  1779 +// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 green。
  1780 +// - behaviorHard = interactionFailures + sentinel textIssues + styleIssues(颜色 token / layout sanity):
  1781 +// 有 locator → 降维喂 fixPrompt 跑 fix(fix 后全量前端单测复验 + 下一 behaviorRound 重跑门);
  1782 +// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不放行。
  1783 +// - BEHAVIOR_STAGE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。
1735 1784  
1736   -// envBlocked / ifails:per-FE bg 的环境/空覆盖与交互失败判定(build-failed 不计 envBlocked——它走确定性短路分支)。
  1785 +// envBlocked / ifails:环境/空覆盖与交互失败判定。v3:build-failed 计入 envBlocked(不再有短路放行分支)。
1737 1786 function behaviorEnvBlocked(r) {
1738 1787 const k = r.envError && r.envError.kind
1739   - const ev = (k && k !== 'none' && k !== 'build-failed') ? r.envError : null
  1788 + const ev = (k && k !== 'none') ? r.envError : null
1740 1789 const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0)
1741 1790 return { ev, emptyCov, blocked: !!ev || emptyCov }
1742 1791 }
1743 1792 function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] }
  1793 +const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed')
1744 1794  
1745   -// runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。
1746   -// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。
1747   -// behaviorRound:approve 子门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。
1748   -async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) {
1749   - const lbl = (a) => `behavior:${id}:r${behaviorRound}:a${a}`
  1795 +// runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。
  1796 +// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。
  1797 +// behaviorRound:阶段门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。
  1798 +async function runBehaviorGateOnce(feItems, behaviorRound) {
  1799 + const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}`
1750 1800 let attempt = 1
1751   - let bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt),
1752   - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA})
1753   - recordDecisions(`behavior:${id}`, bg.decisions)
1754   -
1755   - // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。
1756   - const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed'
1757   - if (isBuildFailedShortCircuit(bg)) return bg
  1801 + let bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt),
  1802 + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
  1803 + recordDecisions('behavior:frontend-phase', bg.decisions)
1758 1804  
1759 1805 // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。
1760   - while (behaviorEnvBlocked(bg).blocked && attempt < BEHAVIOR_ATTEMPT_MAX) {
  1806 + // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。
  1807 + while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) {
1761 1808 attempt += 1
1762   - bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt),
1763   - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA})
1764   - recordDecisions(`behavior:${id}`, bg.decisions)
1765   - if (isBuildFailedShortCircuit(bg)) return bg
  1809 + bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt),
  1810 + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
  1811 + recordDecisions('behavior:frontend-phase', bg.decisions)
1766 1812 }
1767 1813 let envState = behaviorEnvBlocked(bg)
1768 1814 for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) {
1769 1815 const reason = envState.ev
1770   - ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}`
  1816 + ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}${envState.ev.rootCausePath ? `(rootCausePath=${envState.ev.rootCausePath})` : ''}`
1771 1817 : `behavior 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)`
1772   - const verdict = await adjudicate(`behavior-env:${id}`,
1773   - { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, grp, adj)
1774   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-env ${id}: ${verdict.rationale || reason}`)
  1818 + // riders:环境失败同轮搭车的硬问题(如 build-failed 时已归因到的 interactionFailures/styleIssues)——
  1819 + // 本轮不进 fix(环境未就绪,fix 无意义),但透传给仲裁者辅助 retry/halt 判断;若环境修复后仍真实,下一轮门会重新发现。
  1820 + const verdict = await adjudicate('behavior-env:frontend-phase',
  1821 + { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids,
  1822 + riders: { interactionFailures: behaviorIfails(bg).length, styleIssues: (bg.styleIssues || []).length,
  1823 + sentinelTextIssues: (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length },
  1824 + allowContinue:false }, 'Behavior', adj)
  1825 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`)
1775 1826 attempt += 1
1776   - bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt),
1777   - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA})
1778   - recordDecisions(`behavior:${id}`, bg.decisions)
1779   - if (isBuildFailedShortCircuit(bg)) return bg
  1827 + bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt),
  1828 + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
  1829 + recordDecisions('behavior:frontend-phase', bg.decisions)
1780 1830 envState = behaviorEnvBlocked(bg)
1781 1831 }
1782   - if (envState.blocked) throw new Error(`HALT behavior-env ${id}: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`)
  1832 + if (envState.blocked) throw new Error(`HALT behavior-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`)
1783 1833 return bg
1784 1834 }
1785 1835  
1786   -// behaviorSubGate:reviewer approve 的「行为 approve 子门」。green 才允许 reviewWithFixLoop return approve。
1787   -// softPassed:由 reviewWithFixLoop 顶层注入,跨 behaviorRound 持久(软文字一旦放行不再追问)。
1788   -// green ≡ behaviorHard.length===0 ∧ envError∈{none,build-failed} ∧ 本 FE 覆盖非空(或 build-failed 短路)。
1789   -async function behaviorSubGate(id, specPath, grp, softPassed) {
  1836 +// runBehaviorGate:阶段级行为门主循环(被顶层 frontend 段调用,phase('Behavior') 下)。green 才正常返回(放行进 testGate)。
  1837 +// softPassed:本函数内声明,跨 behaviorRound 持久(软文字一旦放行不再追问,避免反复消耗仲裁预算)。
  1838 +// green ≡ behaviorHard.length===0 ∧ envError===none ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空 ∧ 无未解释漏达路由。
  1839 +async function runBehaviorGate(feItems) {
1790 1840 const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}`
1791   - for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) {
1792   - const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound)
1793   -
1794   - // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → green-by-skip 放行。但骨架(lazy router + FeStub)
1795   - // 令「合法的兄弟未实现 build-failed」极罕见,故一个 build-failed 更可能是本 FE 引入的真共享代码回归;
1796   - // 绝不凭未校验的 LLM 归因静默放行——先过轻量前置校验(comment §107-108 声称 load-bearing 的边界,此前无 JS 兜底):
1797   - // a) 必须有 rootCausePath(否则无从判定根因落点);
1798   - // b) 不得同时携带交互硬问题(interactionFailures / source=sentinel 文字)——那是真缺陷搭车。
1799   - // 任一不满足 = 「脏」build-failed → 不短路,过 adjudicate(allowContinue:false) retry/halt,绝不 green-by-skip。
1800   - if (bg.envError && bg.envError.kind === 'build-failed') {
1801   - const rootCausePath = (bg.envError.rootCausePath || '').trim()
1802   - const hardRiders = behaviorIfails(bg).length
1803   - + (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length
1804   - const dirty = !rootCausePath
1805   - ? 'build-failed 未给 rootCausePath(无法判定根因是否落在本 FE 之外)'
1806   - : hardRiders
1807   - ? `build-failed 同时携带 ${hardRiders} 项交互/sentinel 硬问题(疑似本 FE 真构建 bug 搭车)`
1808   - : null
1809   - if (dirty) {
1810   - const verdict = await adjudicate(`behavior-buildfailed-dirty:${id}`,
1811   - { problem:`build-failed 归因不可信,绝不短路放行:${dirty}(rootCausePath=${rootCausePath || '∅'})`,
1812   - envError: bg.envError, allowContinue:false }, grp, behaviorRound)
1813   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-buildfailed ${id}: ${verdict.rationale || dirty}`)
1814   - continue // retry → 下一 behaviorRound 重跑整门
1815   - }
1816   - // 干净的 build-failed(有 rootCausePath 且无硬问题搭车)→ green-by-skip 放行,记低置信证据。
1817   - recordDecisions(`behavior-build-failed:${id}`, [{
1818   - question:`本 FE ${id} 行为验收遇 build-failed(根因 ${rootCausePath})`,
1819   - choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)',
1820   - rationale: bg.envError.detail || '', confidence:'low' }])
1821   - log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${rootCausePath}),记证据不阻断`)
1822   - return
1823   - }
  1841 + const softPassed = new Set()
  1842 + for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_STAGE_MAX; behaviorRound++) {
  1843 + const bg = await runBehaviorGateOnce(feItems, behaviorRound)
1824 1844  
1825   - // 2) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。
1826   - // locator-not-resolvable(B 类硬问题反查不出)计入未覆盖——下面会因 behaviorHard 仍非空或覆盖不足而不 green。
  1845 + // 1) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。
  1846 + // locator-not-resolvable / scope-missing 在 §3.5 单独阻断 green。
1827 1847 for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) {
1828 1848 if (!cg) continue
1829   - recordDecisions(`behavior-coverage:${id}`,
  1849 + recordDecisions('behavior-coverage:frontend-phase',
1830 1850 [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }])
1831 1851 }
1832 1852  
1833   - // 3) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。
1834   - // 永不阻断 approve;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。
  1853 + // 2) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。
  1854 + // 永不阻断 green;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。
1835 1855 let softRetry = false
1836 1856 for (const ti of (Array.isArray(bg.textIssues) ? bg.textIssues : [])) {
1837 1857 if (!ti || ti.source === 'sentinel') continue // sentinel 归 behaviorHard,不在软文字处理
1838 1858 if (softPassed.has(regionKey(ti))) continue
1839   - const site = `behavior-text:${id}:${ti.page || '?'}:${ti.region || '?'}`
  1859 + const site = `behavior-text:${ti.page || '?'}:${ti.region || '?'}`
1840 1860 const verdict = await adjudicate(site,
1841   - { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 approve):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`,
1842   - textIssue: ti, allowContinue: true }, grp, behaviorRound)
  1861 + { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 green):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`,
  1862 + textIssue: ti, allowContinue: true }, 'Behavior', behaviorRound)
1843 1863 if (verdict.action === 'continue') {
1844 1864 recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`,
1845 1865 choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }])
... ... @@ -1850,24 +1870,26 @@ async function behaviorSubGate(id, specPath, grp, softPassed) {
1850 1870 }
1851 1871 if (softRetry) continue
1852 1872  
1853   - // 3.5) B 类硬问题(locator-not-resolvable coverageGap):连组件文件都反查不出,不静默放行——
1854   - // 计入未覆盖阻断 approve,走 adjudicate(allowContinue:false) retry/halt(绝不当 green 放行,降级≠放行)。
1855   - const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []).filter(cg => cg && cg.reason === 'locator-not-resolvable')
  1873 + // 3.5) B 类硬问题(locator-not-resolvable:连组件文件都反查不出)+ scope-missing(某 FE 作用域小节缺失,
  1874 + // 其路由整体漏验):不静默放行——计入未覆盖阻断 green,走 adjudicate(allowContinue:false) retry/halt。
  1875 + const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])
  1876 + .filter(cg => cg && (cg.reason === 'locator-not-resolvable' || cg.reason === 'scope-missing'))
1856 1877 if (bClass.length) {
1857   - const summary = bClass.map(cg => `${cg.page} — ${cg.detail}`).join('; ')
1858   - const verdict = await adjudicate(`behavior-bclass:${id}`,
1859   - { problem:`behavior 硬问题连组件文件都反查不出(B 类,不可降级放行,计入未覆盖阻断 approve):${summary}`,
1860   - coverageGaps: bClass, allowContinue:false }, grp, behaviorRound)
1861   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass ${id}: ${verdict.rationale || summary}`)
1862   - continue // retry → 重跑本 FE 行为验收(下一 behaviorRound)
  1878 + const summary = bClass.map(cg => `[${cg.reason}] ${cg.page} — ${cg.detail}`).join('; ')
  1879 + const verdict = await adjudicate('behavior-bclass:frontend-phase',
  1880 + { problem:`behavior 不可降级的未覆盖(B 类反查不出 / FE 作用域缺失,阻断 green):${summary}`,
  1881 + coverageGaps: bClass, allowContinue:false }, 'Behavior', behaviorRound)
  1882 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass frontend-phase: ${verdict.rationale || summary}`)
  1883 + continue // retry → 重跑行为验收(下一 behaviorRound)
1863 1884 }
1864 1885  
1865 1886 // 3.6) 覆盖率对账(确定性兜底):空覆盖只兜 ==0;这里兜 0<routesReached<routesPlanned 的「部分覆盖假绿」。
1866 1887 // 每条 planned-but-unreached 路由必须由「路由级 coverageGap」解释;未被解释的漏达路由 = 静默漏验,绝不判 green。
1867   - // 只数路由级 reason(控件级 deep-control-not-driven / locator-not-resolvable 不抵漏达路由);过计只会抑制本门、绝不误 halt。
  1888 + // 只数路由级 reason(控件级 deep-control-not-driven / locator-not-resolvable / scope-missing 不抵漏达路由——
  1889 + // scope-missing 的 FE 路由本就不在分母);过计只会抑制本门、绝不误 halt。
1868 1890 const planned = Number(bg.routesPlanned) || 0
1869 1891 const reached = Number(bg.routesReached) || 0
1870   - const ROUTE_GAP = new Set(['unreachable-auth', 'unreachable-no-route', 'dynamic-route-no-seed', 'build-failed-sibling-unimpl'])
  1892 + const ROUTE_GAP = new Set(['unreachable-auth', 'unreachable-no-route', 'dynamic-route-no-seed'])
1871 1893 const routeGapPages = new Set((Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])
1872 1894 .filter(cg => cg && ROUTE_GAP.has(cg.reason) && typeof cg.page === 'string' && cg.page.trim())
1873 1895 .map(cg => cg.page.trim()))
... ... @@ -1875,70 +1897,78 @@ async function behaviorSubGate(id, specPath, grp, softPassed) {
1875 1897 const missedRoutes = Math.max(0, planned - reached)
1876 1898 const unaccounted = Math.max(0, missedRoutes - routeGapCount)
1877 1899 if (planned > 0 && unaccounted > 0) {
1878   - const verdict = await adjudicate(`behavior-undercoverage:${id}`,
1879   - { problem:`本 FE 路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`,
1880   - coverageGaps: bg.coverageGaps || [], allowContinue: false }, grp, behaviorRound)
1881   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage ${id}: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`)
  1900 + const verdict = await adjudicate('behavior-undercoverage:frontend-phase',
  1901 + { problem:`路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`,
  1902 + coverageGaps: bg.coverageGaps || [], allowContinue: false }, 'Behavior', behaviorRound)
  1903 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage frontend-phase: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`)
1882 1904 continue // retry → 下一 behaviorRound 重跑整门
1883 1905 }
1884 1906  
1885   - // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。
  1907 + // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues
  1908 + // + styleIssues(颜色 token / layout sanity,全部客观可 fix)。
1886 1909 const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : [])
1887 1910 .filter(t => t && t.source === 'sentinel')
1888 1911 .map(t => ({ page:t.page, control:t.region, kind:'binding-garbage', detail:`sentinel 不符 期望=${t.expected} 实际=${t.actual}`, locator:t.locator }))
1889   - const behaviorHard = [...behaviorIfails(bg), ...sentinelHard]
  1912 + const styleHard = (Array.isArray(bg.styleIssues) ? bg.styleIssues : [])
  1913 + .filter(Boolean)
  1914 + .map(s => ({ page:s.page, control:s.element, kind:`style-${s.kind}`,
  1915 + detail:`期望=${s.expected} 实际=${s.actual}`, locator:s.locator }))
  1916 + const behaviorHard = [...behaviorIfails(bg), ...sentinelHard, ...styleHard]
1890 1917  
1891 1918 const hasEnvSignal = !!(bg.envError && bg.envError.kind && bg.envError.kind !== 'none')
1892 1919 const hasAnyClassifiedSignal = hasEnvSignal
1893 1920 || behaviorHard.length > 0
1894 1921 || (Array.isArray(bg.textIssues) && bg.textIssues.length > 0)
  1922 + || (Array.isArray(bg.styleIssues) && bg.styleIssues.length > 0)
1895 1923 || (Array.isArray(bg.coverageGaps) && bg.coverageGaps.length > 0)
1896 1924 if (bg.status === 'red' && !hasAnyClassifiedSignal) {
1897   - const verdict = await adjudicate(`behavior-red-unclassified:${id}`,
1898   - { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green',
1899   - behaviorResult: bg, allowContinue:false }, grp, behaviorRound)
1900   - if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified ${id}: ${verdict.rationale || 'status:red 无分类原因'}`)
  1925 + const verdict = await adjudicate('behavior-red-unclassified:frontend-phase',
  1926 + { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / styleIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green',
  1927 + behaviorResult: bg, allowContinue:false }, 'Behavior', behaviorRound)
  1928 + if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified frontend-phase: ${verdict.rationale || 'status:red 无分类原因'}`)
1901 1929 continue
1902 1930 }
1903 1931  
1904   - // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 子门 green 放行。
  1932 + // 5) green 判定:behaviorHard 为空 ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 门 green 放行。
1905 1933 if (behaviorHard.length === 0) {
1906   - log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`)
  1934 + log(`behavior frontend-phase green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`)
1907 1935 return
1908 1936 }
1909 1937  
1910   - // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不 approve)。
  1938 + // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不放行)。
1911 1939 const withLoc = behaviorHard.filter(x => typeof x.locator === 'string' && x.locator.trim())
1912 1940 const noLoc = behaviorHard.filter(x => !(typeof x.locator === 'string' && x.locator.trim()))
1913 1941 if (noLoc.length) {
1914 1942 const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ')
1915   - const verdict = await adjudicate(`behavior-noloc-hard:${id}`,
1916   - { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue/approve):${summary}`,
1917   - interactionFailures: noLoc, allowContinue:false }, grp, behaviorRound)
  1943 + const verdict = await adjudicate('behavior-noloc-hard:frontend-phase',
  1944 + { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue 放行):${summary}`,
  1945 + interactionFailures: noLoc, allowContinue:false }, 'Behavior', behaviorRound)
1918 1946 if (verdict.action !== 'retry')
1919   - throw new Error(`HALT behavior-noloc-hard ${id}: ${verdict.rationale || summary}`)
1920   - continue // retry → 重跑本 FE 行为验收(下一 behaviorRound)
  1947 + throw new Error(`HALT behavior-noloc-hard frontend-phase: ${verdict.rationale || summary}`)
  1948 + continue // retry → 重跑行为验收(下一 behaviorRound)
1921 1949 }
1922 1950  
1923 1951 // 7) 有 locator 的硬问题 → 降维成 {summary,locator,severity} 喂现有 fixPrompt 跑 fix(schema 不合并、fix 入参合并)。
  1952 + // 一轮 fix 批量修当轮全部 must-fix(跨 FE 也在同一 fix 子会话内逐项修,locator 已含组件文件路径)。
1924 1953 const fixIssues = withLoc.map(f => ({
1925 1954 summary: `[behavior:${f.kind}] ${f.page}:${f.control} — ${f.detail}`,
1926 1955 locator: f.locator,
1927 1956 severity: 'high',
1928 1957 }))
1929   - await runStage(g => fixPrompt(id, 'frontend', fixIssues) + g, {
1930   - site:`behavior-fix:${id}:r${behaviorRound}`, grp, label:`behavior-fix:${id}:r${behaviorRound}`, allowContinue: true,
  1958 + await runStage(g => fixPrompt('frontend-phase', 'frontend', fixIssues) + g, {
  1959 + site:`behavior-fix:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-fix:r${behaviorRound}`, allowContinue: true,
1931 1960 })
1932 1961  
1933   - // 8) fix 后功能复验(allowContinue:false):behaviorSubGate 的 fix 改的是 frontend/ UI 源码,可能引入功能回归——
1934   - // 先跑 scoped 组件测试 reverify(不起全栈,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。
  1962 + // 8) fix 后功能复验(allowContinue:false):行为 fix 改的是 frontend/ UI 源码,可能引入功能回归——
  1963 + // 先跑全量前端单测(不起全栈、不跑 e2e,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。
  1964 + // (e2e 维度由下一轮行为门 + 后续阶段 testGate 全量回归兜底。)
1935 1965 await runStage(
1936   - g => verifyPrompt(id, 'frontend', `(behaviorRound ${behaviorRound} 行为 fix 后功能复验,本轮 must-fix: ${fixIssues.length} 项)`, specPath, REVIEW_HARD_ROUNDS + behaviorRound) + g,
1937   - { site:`behavior-reverify:${id}:r${behaviorRound}`, grp, label:`behavior-reverify:${id}:r${behaviorRound}`, allowContinue: false },
  1966 + g => behaviorReverifyPrompt(behaviorRound, fixIssues.length) + g,
  1967 + { site:`behavior-reverify:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-reverify:r${behaviorRound}`, allowContinue: false },
1938 1968 )
1939   - // 进入下一 behaviorRound → 重跑本 FE 行为验收
  1969 + // 进入下一 behaviorRound → 重跑行为验收
1940 1970 }
1941   - throw new Error(`HALT behavior-unresolved ${id}: ${BEHAVIOR_FE_MAX} 轮 per-FE 行为子门仍未 green(硬问题未清)`)
  1971 + throw new Error(`HALT behavior-unresolved frontend-phase: ${BEHAVIOR_STAGE_MAX} 轮阶段级行为门仍未 green(硬问题未清)`)
1942 1972 }
1943 1973  
1944 1974 phase('Router')
... ... @@ -2006,13 +2036,17 @@ for (const [idx, module] of todo.entries()) {
2006 2036 phase('Frontend')
2007 2037 // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy
2008 2038 // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达,
2009   - // 使 per-FE 行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。
  2039 + // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。
2010 2040 await runFrontendSkeleton(module.feItems)
2011   - // 前端行为验收已并入 featureLoop→reviewWithFixLoop 的 per-FE approve 子门(reviewer approve 时起本 FE 全栈验
2012   - // 「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才打 req-done)——不再有阶段级末尾独立行为门。
  2041 + // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。
2013 2042 await featureLoop(module.feItems, 'frontend')
  2043 + // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合
  2044 + // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。
  2045 + // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。
  2046 + phase('Behavior')
  2047 + await runBehaviorGate(module.feItems)
2014 2048 phase('Gate')
2015   - await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright)保留,与 per-FE 行为验收职责正交
  2049 + await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交
2016 2050 }
2017 2051 phase('Milestone')
2018 2052 // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"——
... ...