diff --git a/README.md b/README.md index 4179042..d880d64 100644 --- a/README.md +++ b/README.md @@ -48,12 +48,16 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) runBranchSetup(frontend-phase) → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起; - 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState) + 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState; + 含单测基线:vitest include 限定 tests/**/*.test.*——单测一律 frontend/tests/ 镜像 src/, + 交付源码 frontend/src/ 内禁测试文件,同后端 src/main↔src/test 物理分离) → featureLoop(前端,FE-NN,路径限 frontend/):spec → plan → tdd → verify → - review 循环内并入 per-FE 行为验收 approve 子门(reviewer approve 时才起本 FE 全栈 - +演示种子+sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix - must-fix→重验,软文字按来源仲裁,行为 green 才打 req-done/) - → testGate(frontend,全量回归 vitest+playwright,与 per-FE 行为验收职责正交) + review 循环(静态验收,approve 即打 req-done/) + → 阶段级行为门 behavior(整个前端阶段只跑一次:起全栈+演示种子+sentinel, + 按全部 FE spec 聚合的作用域并集枚举路由,交互/文字/样式三层断言;交互失效 + /sentinel 错/样式违规(非 token 色、横向溢出、控件重叠等)转可 fix must-fix + →fix→单测复验→重跑门(≤3 轮),软文字按来源仲裁,green 才放行) + → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归) → runMilestone(milestone/frontend-phase) 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start @@ -129,7 +133,7 @@ erp-workflow-plugin/ | # | Skill | 作用 | 流程中谁调用 | |---|---|---|---| | A0 | `project-init` | • **依赖检查**:检测 git / mysql / node 是否在 PATH,缺失则按 OS 自动安装,装不上再停下提示用户
• 空目录初始化:用 Read/Write/Glob 工具拷模板创建 CLAUDE.md / docs/01/index.md / docs/08
• `git init` | `plan-start` | -| A1 | `scope-lock` | • 引导填项目概述 / 技术栈 / 需求索引
• 按 `docs/01-需求清单//{_module.md, REQ-*.md}` 子目录结构生成 REQ 卡片(CC 据 index.md 填 `{{req_id/title/goal/rules/constraints/acceptance}}` 6 个占位,模板其余内容含输入/输出示例字段表原样复制)
• **A1 终结校验**:REQ 6 个占位均填真实数据、无 `{{` 残留、`config-vars.yaml` **全部配置**(包名 / 端口 / 初始账号 + DB 凭据 / 密钥占位)已锁、各 stack 的 build/lint/unit/e2e 命令写入 docs/04 § 零;缺失则在此(Plan 期)用 `AskUserQuestion` 问清(敏感凭据由用户自填,不进会话)
• 据模板直接 `Write` 生成 `_module.md` / `REQ-*.md`
• 终结校验通过后**自动**调用 `Skill(skeleton-gen)` 进入 A2(不停下) | A0 | +| A1 | `scope-lock` | • 引导填项目概述 / 技术栈 / 需求索引
• 按 `docs/01-需求清单//{_module.md, .md}` 子目录结构生成 REQ 卡片(req_id = `<模块代码>-<子模块代码>-<功能名>`,如 `USR-USR-LOGIN`;CC 据 index.md 填 `{{req_id/title/goal/rules/constraints/acceptance}}` 6 个占位,模板其余内容含输入/输出示例字段表原样复制)
• **A1 终结校验**:REQ 6 个占位均填真实数据、无 `{{` 残留、`config-vars.yaml` **全部配置**(包名 / 端口 / 初始账号 + DB 凭据 / 密钥占位)已锁、各 stack 的 build/lint/unit/e2e 命令写入 docs/04 § 零;缺失则在此(Plan 期)用 `AskUserQuestion` 问清(敏感凭据由用户自填,不进会话)
• 据模板直接 `Write` 生成 `_module.md` / `.md`
• 终结校验通过后**自动**调用 `Skill(skeleton-gen)` 进入 A2(不停下) | A0 | | A2 | `skeleton-gen` | • 生成架构文档:docs/04 § 一+
• 生成跨平台工具脚本:`scripts/*.mjs`(**无 chmod**;凭据 / 配置统一在 A1 产出的 config-vars.yaml)
• 据 `gitignore-append-template` 用 Read/Write 并入项目 .gitignore | `plan-start` | | A3 | `db-design-gen` | • 套用固定 ERP 约定(列前缀 `i/s/t`、`iIncrement` 主键、`sBrandsId`/`sSubsidiaryId` 租户列)从 docs/01 REQ 卡片正向设计 `docs/03-数据库设计文档.md`(schema SSoT)
• 回填 REQ 卡片依赖表(`TBD(A3 自动补)` → 实际表名)
• **停下**等人工审阅 docs/03,审阅完毕用 `/plan-start` 续进 A4 | A2 | | A4 | `db-init` | • LLM 解析 docs/03 → `sql/migrations/V1__initial_schema.sql`(DDL only)
• `node ${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs` 校验 DDL ↔ docs/03(5 维:表/列名/列类型/索引/FK),fail-closed
• `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/ | Agent | 用途 | 谁调用 | |---|---|---| -| `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 歧义) | +| `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 歧义) | ## Templates 清单(26 份) | 所属 Skill | 模板文件 | 用途 | |---|---|---| | project-init | `CLAUDE-template.md` | 项目根的 CLAUDE.md(4 条通用准则 + ERP 专属约定) | -| project-init | `docs-01-index-template.md` | 需求清单索引骨架,等用户填模块表 | +| project-init | `docs-01-index-template.md` | 需求清单索引骨架,等用户填子模块索引表(五列,一行一个子模块) | | project-init | `docs-04-stack-template.md` | docs/04 § 零 默认技术栈总览(零槽位,拷即可) | | project-init | `docs-08-initial-template.md` | 工作流进度文件骨架(Plan A0~A5 checkbox) | -| scope-lock | `req-card-template.md` | 单张 REQ-XXX-NNN 卡片模板(`{{req_id/title/goal/rules/constraints/acceptance}}` 占位 + 输入/输出示例字段表;A1 原样复制,只填这 6 个占位) | +| scope-lock | `req-card-template.md` | 单张 REQ 卡片模板(文件名 == req_id `<模块代码>-<子模块代码>-<功能名>`;`{{req_id/title/goal/rules/constraints/acceptance}}` 占位 + 输入/输出示例字段表;A1 原样复制,只填这 6 个占位) | | scope-lock | `_module-template.md` | 模块子目录的 `_module.md` 模块头(模块代码-名 / 简述 / 依赖模块 TBD / 涉及表 TBD) | | scope-lock | `config-vars-template.yaml` | 仓库根 `config-vars.yaml` 骨架(跨栈中立):项目**全部配置**——非敏感(包名/端口/前端包名/初始账号)+ 敏感凭据(database / admin_init.password / secrets);A1 E.2 锁定,随项目提交 | | skeleton-gen | `docs-04-skeleton-template.md` | docs/04 § 一+ 编码规范大纲(HTML 注释引导 LLM) | diff --git a/agents/code-reviewer.md b/agents/code-reviewer.md index eeb099a..8946720 100644 --- a/agents/code-reviewer.md +++ b/agents/code-reviewer.md @@ -22,7 +22,7 @@ Cover the four standard axes — **plan-alignment** (implementation matches plan ## When phase=frontend, additionally -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. +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. For each dimension below, classify Critical / Important / Suggestion as above. @@ -60,3 +60,8 @@ For each dimension below, classify Critical / Important / Suggestion as above. ### 7. 状态机覆盖 (objective → can request-changes) - The 5 states from the spec (loading / empty / error / 正常 / 提交中) must each be handled in code. - Missing state handling → `request-changes` for the specific state. + +### 8. 测试文件隔离 (objective → can request-changes) +- 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). +- 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). +- 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. diff --git a/docs/design/2026-06-02-frontend-behavior-gate.md b/docs/design/2026-06-02-frontend-behavior-gate.md index ab5b606..e113b9e 100644 --- a/docs/design/2026-06-02-frontend-behavior-gate.md +++ b/docs/design/2026-06-02-frontend-behavior-gate.md @@ -1,8 +1,8 @@ -# 前端行为门(旧阶段级设计,已作废) +# 前端行为门(v1 阶段级只读设计,已作废) -> 状态:SUPERSEDED。当前实现依据见 [`2026-06-02-frontend-behavior-in-review-loop.md`](./2026-06-02-frontend-behavior-in-review-loop.md)。 +> 状态:SUPERSEDED。当前实现依据见 [`2026-06-05-frontend-behavior-stage-gate.md`](./2026-06-05-frontend-behavior-stage-gate.md)(v3)。 -本文原先描述的是“frontend-phase 末尾跑一次、只读、red 即 halt”的阶段级行为门。该方案已被 per-FE 方案取代:行为验收并入每个 FE 的 `reviewWithFixLoop` approve 子门,行为硬问题带 locator 后进入 fix→重验循环。 +本文原先描述的是“frontend-phase 末尾跑一次、只读、red 即 halt”的阶段级行为门。该方案先被 v2(per-FE approve 子门 + fix 循环)取代;v3 又把触发时机迁回阶段末尾一次,但保留了 v2 的 fix→复验→重跑循环,**不是**回到本文的只读 halt 形态。 保留的历史结论: diff --git a/docs/design/2026-06-02-frontend-behavior-in-review-loop.md b/docs/design/2026-06-02-frontend-behavior-in-review-loop.md index 07a8922..f58cd5a 100644 --- a/docs/design/2026-06-02-frontend-behavior-in-review-loop.md +++ b/docs/design/2026-06-02-frontend-behavior-in-review-loop.md @@ -1,7 +1,8 @@ -# 前端行为验收并入 reviewWithFixLoop(v2 最终设计:per-FE + fix 循环) +# 前端行为验收并入 reviewWithFixLoop(v2 设计:per-FE + fix 循环,已作废) -> 状态:可实现(ready-to-implement),含 3 项实现前置依赖。 -> 上游:本设计取代 `docs/design/2026-06-02-frontend-behavior-gate.md` 的「阶段级末尾只读 halt 门」形态。 +> 状态:SUPERSEDED。当前实现依据见 [`2026-06-05-frontend-behavior-stage-gate.md`](./2026-06-05-frontend-behavior-stage-gate.md)(v3:行为验收回迁阶段末尾,整个前端阶段只跑一次,保留 fix 循环)。 +> 本文保留作历史依据:v3 沿用了本文的失败分层 / 两层断言 / locator A-B 分级 / 作用域小节(依赖 C)/ 骨架占位(依赖 A)等机制,仅把触发时机从 per-FE approve 子门改回阶段级一次。 +> 上游:本设计曾取代 `docs/design/2026-06-02-frontend-behavior-gate.md` 的「阶段级末尾只读 halt 门」形态。 > 运行时红线(不可违反):禁用 time/random builtin(`Date.now()` / `Math.random()` / `new Date()`);顶层 `return` 是结果通道;`agent/phase/parallel/log/adjudicate/recordDecisions` 是注入全局;**后端 featureLoop 分支逐字不变**。 --- diff --git a/docs/design/2026-06-05-frontend-behavior-stage-gate.md b/docs/design/2026-06-05-frontend-behavior-stage-gate.md new file mode 100644 index 0000000..dfe7729 --- /dev/null +++ b/docs/design/2026-06-05-frontend-behavior-stage-gate.md @@ -0,0 +1,87 @@ +# 前端行为验收回迁阶段级(v3:阶段末尾一次 + 保留 fix 循环) + +> 状态:已实现(implemented)。 +> 上游:本设计取代 [`2026-06-02-frontend-behavior-in-review-loop.md`](./2026-06-02-frontend-behavior-in-review-loop.md)(v2,per-FE approve 子门形态)。 +> 运行时红线(不可违反):禁用 time/random builtin;顶层 `return` 是结果通道;`agent/phase/parallel/log` 是注入全局;**后端 featureLoop 分支逐字不变**。 + +--- + +## 0. 用户拍板的方向(不可推翻) + +- 行为验收**挪到前端阶段末尾**:整个 frontend-phase 只跑**一次**行为验收,不再在每个 FE 的 review 内循环(approve 子门)中运行。 +- **保留 fix 循环**(用户确认):行为门发现带 locator 的硬问题 → fixPrompt 自动修复 → 前端单测复验 → 重跑门,硬上限 `BEHAVIOR_STAGE_MAX = 3` 轮;不回到 v1 的「只读 red 即 halt」。 +- 时序:`featureLoop(frontend) → phase('Behavior') runBehaviorGate → phase('Gate') testGate → report → milestone`。 + 行为门放在 testGate **之前**——行为 fix 会改 `frontend/` 源码,绿后由 testGate 全量回归兜底,避免回归证据过期。 + +## 1. 控制流(实现级) + +``` +顶层 frontend 段: + runFrontendSkeleton(feItems) # 保留(中途可构建仍是逐 FE verify(e2e) 的前提) + featureLoop(feItems, 'frontend') # review 仅静态验收;approve 即打 req-done/ + phase('Behavior') + runBehaviorGate(feItems): # 阶段级行为门(原 behaviorSubGate 改造) + softPassed = Set() # 跨 behaviorRound 持久(软文字一旦 continue 不再追问) + for behaviorRound in 1..BEHAVIOR_STAGE_MAX(=3): + bg = runBehaviorGateOnce(feItems, behaviorRound) # 内部 attempt 1→2 环境重试 + 仲裁兜底 + coverageGaps → recordDecisions(记录不阻断) + 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + softPassed;永不阻断 green) + B 类(locator-not-resolvable)+ scope-missing → adjudicate(allowContinue:false) retry/halt + 覆盖率对账(planned-reached-路由级gap > 0 → adjudicate(allowContinue:false)) + 未分类 red → adjudicate(allowContinue:false) + behaviorHard(interactionFailures + sentinel textIssues + styleIssues)为空 → green → return + 无 locator 硬问题 → adjudicate(allowContinue:false) retry/halt + 有 locator → 降维喂 fixPrompt('frontend-phase')(一轮批量修当轮全部 must-fix,跨 FE 同一 fix 子会话) + → behaviorReverifyPrompt(全量前端单测 vitest,不跑 e2e)allowContinue:false + → 下一 behaviorRound 重跑门 + 3 轮仍未 green → throw HALT behavior-unresolved + phase('Gate') + testGate(frontend) # 全量回归 vitest+playwright,兜底行为 fix 引入的回归 +``` + +## 2. 与 v2 的关键差异 + +| 维度 | v2(per-FE approve 子门) | v3(阶段级,本设计) | +|---|---|---| +| 触发时机 | 每个 FE reviewer 判 approve 时 | featureLoop 全部 FE 完成后一次 | +| 起栈次数 | N 个 FE × (1~3) 次 | 1~3 次(整阶段) | +| 断言作用域 | 单 FE 的 feScope | 全部 FE spec「行为验收作用域」小节**并集**(路由去重、标注归属 FE) | +| `req-done/` 语义 | 静态过 ∧ 行为过 | **仅静态过**;行为 green 是 milestone 前置(reportPrompt 校验) | +| build-failed | 根因非本 FE → green-by-skip 短路 | **无短路**(阶段末尾无「兄弟未实现」)。根因在 frontend/ 源码且可定位 → `interactionFailures[js-error]`(可 fix);不可归因 → envError,跳过自动 attempt 重试直送仲裁 | +| `build-failed-sibling-unimpl` | coverageGap reason 枚举之一 | **删除**(新增 `scope-missing`:某 FE spec 缺作用域小节,与 B 类同级阻断 green) | +| FeStub 残留 | 预期中途态 | **硬缺陷**(tdd 漏做占位替换)→ `interactionFailures[no-observable-effect]`,locator=router import 行 | +| 证据路径 | `reviews/--behavior-r*-a*.md` | `module-reports/frontend-phase-behavior-r-a.md`(与 test-gate 命名同构;截图 → `module-reports/assets/`);fix 后复验 `frontend-phase-behavior-reverify-r.md` | +| fix 后复验 | per-FE verifyPrompt(scoped 组件测试) | `behaviorReverifyPrompt`:全量前端单测(vitest),不跑 e2e | +| 轮次预算 | `BEHAVIOR_FE_MAX=3`(每 FE) | `BEHAVIOR_STAGE_MAX=3`(整阶段;每轮 fix 批量修当轮全部 must-fix) | +| UI phase 分组 | 'Frontend'(与 review 循环同组) | 独立 `'Behavior'` phase(meta.phases 恢复该项) | + +## 3. 保留不变的机制 + +- **前端骨架占位**(runFrontendSkeleton)+ tddPrompt 的 FeStub→真组件占位替换:中途可构建仍是逐 FE verify(e2e) / 阶段 testGate 的前提,且让阶段门可达每个 FE 路由。 +- **spec「行为验收作用域」小节 + fe-feature-review 校验**:仍是 FE→路由的确定性映射真值,阶段门据此聚合分母并把硬问题归因到 FE/组件。 +- **起栈五段时序**(空库 → 后端/Flyway → 演示种子 → sentinel 种子 → 前端 headless)、step2.5 鉴权 bootstrap、两层断言(交互可观测效果白名单 + sentinel 文字)、A/B 类 locator 分级、软硬文字 source 分流、空覆盖/部分覆盖对账、未分类 red 兜底——全部沿用 v2 语义,仅 scope 从单 FE 放大为并集。 +- **阶段级 testGate(全量回归)**:职责正交保留,且新增「兜底行为 fix 引入的回归」职责。 +- 后端 featureLoop / 顶层 backend 段:逐字不变。 + +## 4. reportPrompt(前端分支) + +- 绿前置恢复:Glob `module-reports/frontend-phase-behavior-r*-a*.md` 按 round→attempt 升序,**最后一份必须非 RED**;红或缺证据 → halt(绝不在行为红上打 milestone)。 +- § ⑤:阶段级行为证据 + behavior-reverify 的 flake / envError / fix 轮数 / 文字 continue 汇总。 +- § ⑧:取最后一份行为证据的逐 FE 小节,汇总 coverageGaps / textIssues continue / 逐控件判定 / authState。 + +## 5. 样式层断言(第一档,后补) + +行为门 step5 在交互/文字之外新增第三层**客观样式断言**,结果落 `BEHAVIOR_GATE_SCHEMA.styleIssues`,JS 全部并入 behaviorHard(有 locator → must-fix 进 fix 循环;无 locator → noLoc 仲裁),与交互硬问题完全同口径。 + +- **颜色 token 比对**:runner 解析 `src/styles/tokens.css` 的 `--color-*`,用探针元素 getComputedStyle 把任意色值归一化为 canonical rgb 集合;被检元素渲染值(color/background/border)同法归一化后比对。`non-token-color`(∉ 集合)/ `token-mismatch`(≠ spec 点名 token 的解析值)。 +- **layout sanity 几何断言**:`horizontal-overflow`(scrollWidth 超 1px 容差)/ `overlap`(白名单控件 boundingBox 交叠 >4px²)/ `zero-size` / `offscreen`(scrollIntoView 后仍不可见)。 +- **误报防线**:断言作用域 = 白名单控件及直接容器 + spec/prototype 点名区域,组件库深层内部元素不查;半透明/无法归一化的值不入 styleIssues 只记 decisions(宁漏勿误)。 +- **与静态 review 维度 2 的关系**:正交——静态查源码 token 引用(commit 前拦截),运行时查最终渲染值(兜级联覆盖 / 组件库 prop 注入色 / 运行时 style 的漏)。 +- **明确未做**(后续权衡):`misalignment` 对齐容差(“同组”无确定性定义)、AI 判图风格相似度、多断点响应式。若 `non-token-color` 实跑误报偏高,退路是单独把它降为 record-only(一行分流改动),`token-mismatch` + 几何类保持硬门。 + +## 6. 残留风险(接受) + +1. **问题堆到最后**:v1 历史结论指出阶段级末尾门会把所有 FE 的行为问题堆到末尾一次性暴露,定位/修复成本高于 per-FE。已用「fix 循环 + 一轮批量修全部 must-fix + locator 含归属 FE」缓解;这是用户为省 N 次起栈成本明确接受的取舍。 +2. **单子会话枚举全量路由的上下文压力**:路由/控件多时单次门会话变重;runner 程序化枚举 + 证据落盘(非全量进上下文)缓解,超限时表现为 envError/timeout 走仲裁。 +3. **req-done 不再含行为语义**:resume 时已打 req-done 的 FE 不会重走静态链,但行为门每次 coding-start 重跑(milestone 未打则 Router 仍把 frontend-phase 算待跑)——行为验收无独立完成 tag,幂等性由「行为证据 + reportPrompt 校验」承载。 +4. **3 轮预算对整阶段共用**:FE 多且问题分散时可能不够;每轮 fix 批量修复 + 仲裁可 halt 转人工,未做自动扩轮(保持预算钉死、防空转)。 diff --git a/skills/coding/coding-start/SKILL.md b/skills/coding/coding-start/SKILL.md index fcf7da9..cb92d75 100644 --- a/skills/coding/coding-start/SKILL.md +++ b/skills/coding/coding-start/SKILL.md @@ -22,10 +22,11 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) 后端功能循环 spec → plan → tdd → verify → review(≤5轮) 后端测试闸 test-gate(RED 自动重试 1 次,仍 RED → halt) 前端骨架占位 router 全量 lazy 路由表 + FeStub 占位(保证中途可构建) - 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/);review 循环内含 - per-FE 行为验收(reviewer approve 时起本 FE 全栈验「按钮真生效/文字对」, - 硬问题可 fix 重验,行为 green 才打 req-done;不再是末尾独立门) - 前端测试闸 test-gate(全量回归) + 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/);review 仅静态验收, + approve 即打 req-done + 前端行为门 阶段级 behavior(整个前端只跑一次:起全栈验「按钮真生效/文字对/样式合规」, + 硬问题可 fix→单测复验→重跑门(≤3 轮),green 才进测试闸) + 前端测试闸 test-gate(全量回归,兜底行为 fix 引入的回归) 跨模块记录 → 模块报告 → 里程碑(merge --no-ff + milestone/ tag) 任一模块 halt → fail-fast 停在该模块,修复后重跑本入口即可续跑 diff --git a/skills/plan/db-design-gen/SKILL.md b/skills/plan/db-design-gen/SKILL.md index 78ca84e..1268d5f 100644 --- a/skills/plan/db-design-gen/SKILL.md +++ b/skills/plan/db-design-gen/SKILL.md @@ -19,7 +19,7 @@ allowed-tools: Read Write Edit Grep Glob - `docs/04-技术规范.md` - `docs/01-需求清单/index.md` 模块索引 -- `docs/01-需求清单/*/REQ-*.md` 所有 REQ 卡片 +- `docs/01-需求清单/*/*.md` 所有 REQ 卡片(跳过文件名为 `_module.md` 的模块头;卡片文件名 == req_id) ### B. 推导 schema @@ -44,7 +44,7 @@ allowed-tools: Read Write Edit Grep Glob ### D. 回填模块头 + REQ 卡片的 TBD 字段 -1. 列出 `docs/01-需求清单/*/_module.md`(模块头)和 `docs/01-需求清单/*/REQ-*.md`(REQ 卡片)。 +1. 列出 `docs/01-需求清单/*/*.md`:`_module.md`(模块头)和其余 .md(REQ 卡片,文件名 == req_id)。 2. 在这些文件中搜索 `TBD(A3 自动补)` 的并回填。 不动 `TBD(A5 自动补)` 3. 打印回填统计:`A3 回填 处模块"涉及表" + 处 REQ"依赖表"`。 @@ -69,4 +69,4 @@ allowed-tools: Read Write Edit Grep Glob - `${CLAUDE_SKILL_DIR}/templates/docs-03-table-template.md` - `docs/04-技术规范.md` § 一+(命名规范输入) - `docs/01-需求清单//_module.md`(模块头:回填 `涉及表`) -- `docs/01-需求清单//REQ-*.md`(REQ 输入 + 回填 `依赖表`) +- `docs/01-需求清单//.md`(REQ 输入 + 回填 `依赖表`) diff --git a/skills/plan/db-design-gen/templates/docs-03-header-template.md b/skills/plan/db-design-gen/templates/docs-03-header-template.md index 6ef65d9..c8ad6a1 100644 --- a/skills/plan/db-design-gen/templates/docs-03-header-template.md +++ b/skills/plan/db-design-gen/templates/docs-03-header-template.md @@ -2,7 +2,7 @@ - **Schema**: `{{schema_name}}` - **Migration 清单**: `sql/migrations/V*.sql`(由 Flyway 顺序 apply) -- **生成方式**: 由 A3 `db-design-gen` 基于 `docs/01-需求清单//REQ-*.md` REQ 卡片正向设计生成(schema SSoT)。 +- **生成方式**: 由 A3 `db-design-gen` 基于 `docs/01-需求清单//` 下各 REQ 卡片(文件名 == req_id)正向设计生成(schema SSoT)。 ## 项目标准列约定 diff --git a/skills/plan/downstream-gen/SKILL.md b/skills/plan/downstream-gen/SKILL.md index 1bd0e01..162e59b 100644 --- a/skills/plan/downstream-gen/SKILL.md +++ b/skills/plan/downstream-gen/SKILL.md @@ -32,7 +32,7 @@ allowed-tools: Read Write Edit Glob Grep AskUserQuestion ### B2. 回填模块头 + REQ 卡片的 TBD(A5) 字段 -1. 在`docs/01-需求清单/*/_module.md`(模块头)和 `docs/01-需求清单/*/REQ-*.md`(REQ 卡片)中搜索并回填 `TBD(A5 自动补)` +1. 在 `docs/01-需求清单/*/*.md`(模块头 `_module.md` + 全部 REQ 卡片,卡片文件名 == req_id)中搜索并回填 `TBD(A5 自动补)` 2. 打印回填统计:`A5 回填 处模块"依赖模块" + 处 REQ"依赖接口"`。 勾选:` - [ ] REQ 卡片依赖接口已回填` @@ -60,12 +60,12 @@ allowed-tools: Read Write Edit Glob Grep AskUserQuestion 1. **合并扫描三类问题**(最多 3 轮自主修复,docs/01 是 REQ SSoT 不动)——一次性扫一致性 + `TBD(A3/A5 自动补)` + 结构性残留: - **一致性**:docs/01 REQ 全部出现在 docs/05;`docs/02 § 二` `module_id` 集合 = `docs/08 § 二` `module_id` 集合。 - - **TBD 残留**:`TBD(A3 自动补)` → 查 docs/03 填依赖表;`TBD(A5 自动补)` → 查 docs/05 按 REQ-ID 填依赖接口。 - - **结构性**:docs/05 每个 `### REQ-` 端点段含非空 `- **请求**:` / `- **响应**:`(非 `TBD`/`—`/`【人工填写:…】`);docs/01 全部 REQ-ID 都出现在 `docs/02 § 二` 顺序清单。 + - **TBD 残留**:`TBD(A3 自动补)` → 查 docs/03 填依赖表;`TBD(A5 自动补)` → 查 docs/05 按 req_id 填依赖接口。 + - **结构性**:docs/05 每个 `### ` 三级标题端点段(标题形如 `### <标题>`)含非空 `- **请求**:` / `- **响应**:`(非 `TBD`/`—`/`【人工填写:…】`);docs/01 全部 req_id 都出现在 `docs/02 § 二` 顺序清单。 命中即按对应步骤规则就地补填(缺端点 → 按 B 推测;缺 module 行 → 按 D 渲染;缺 REQ 顺序行 → 按 A 子流程拓扑插入)。3 轮后仍残留 `【人工填写:】` 或结构缺口 → 打印清单 + 用 `AskUserQuestion` 弹「继续」/「有疑问想先沟通」二选一;每次弹问前重扫一次,避免脏读放行。 -2. **docs/05 + docs/02 人工评审闸**(未确认不得勾选 A5):摘要展示 docs/05 全部端点(`METHOD PATH — REQ-ID`,标注「由 A5 自动推断」的项)+ docs/02 `req_order[]`(特别标 `note ≠ —` 的环依赖打破项)。`AskUserQuestion` 多问题表单同时问两项:「docs/05 端点/字段无误」+「docs/02 构建顺序可接受」,各二选一 `确认` / `需要修改`。任一需修改 → 收集修改点就地修订并重跑本闸,直到两项均 `确认`;否则禁止勾选 A5、禁止打印横幅。 +2. **docs/05 + docs/02 人工评审闸**(未确认不得勾选 A5):摘要展示 docs/05 全部端点(`METHOD PATH — `,标注「由 A5 自动推断」的项)+ docs/02 `req_order[]`(特别标 `note ≠ —` 的环依赖打破项)。`AskUserQuestion` 多问题表单同时问两项:「docs/05 端点/字段无误」+「docs/02 构建顺序可接受」,各二选一 `确认` / `需要修改`。任一需修改 → 收集修改点就地修订并重跑本闸,直到两项均 `确认`;否则禁止勾选 A5、禁止打印横幅。 3. 勾选 A5 父项:`- [ ] A5 下游文档生成 — downstream-gen` diff --git a/skills/plan/plan-start/SKILL.md b/skills/plan/plan-start/SKILL.md index ecb717d..25e5000 100644 --- a/skills/plan/plan-start/SKILL.md +++ b/skills/plan/plan-start/SKILL.md @@ -46,7 +46,7 @@ A 阶段 checkbox 全部 `[x]` 后先跑下面 4 项前移闸门; 全过才放 1. **REQ 卡片真实数据**(来自 A1 scope-lock) - `Glob` 找出全部 REQ 卡片(如 `docs/01-需求清单/**/*.md`)。 - 对每张卡片 `Grep` 命中以下任一即缺口: `【人工填写` / `TBD` / `{{`(`{{` = 6 个标量占位未替换;`TBD` = A3/A5 应回填的依赖表/依赖接口仍未补——A1 时这些 `TBD(A3/A5 自动补)` 是合法保留,到本闸必须已解析,故此处比 scope-lock E.1 多查 `TBD`/`【人工填写`)。 - - 缺口表述示例:`REQ-USER-001 仍含 TBD / {{title}} 占位未替换`。 + - 缺口表述示例:`USR-USR-LOGIN 仍含 TBD / {{title}} 占位未替换`。 2. **全部配置全锁**(来自 A1 写入 `config-vars.yaml` 的非敏感配置 + 敏感凭据,单一文件) - `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 项前移闸门; 全过才放 <逐条列出每个缺口,格式:[闸门] 缺口描述 → 回填位置> 例: - [REQ 真实数据] REQ-USER-001 仍含 {{goal}} 占位未替换 → docs/01-需求清单/... + [REQ 真实数据] USR-USR-LOGIN 仍含 {{goal}} 占位未替换 → docs/01-需求清单/... [配置] database.password 仍是占位(如本地空密码请显式填 `''`)→ config-vars.yaml [docs/04 §零] node 栈缺 e2e 命令 → docs/04-技术规范.md §零 diff --git a/skills/plan/project-init/templates/CLAUDE-template.md b/skills/plan/project-init/templates/CLAUDE-template.md index c1ff32e..1b7eefb 100644 --- a/skills/plan/project-init/templates/CLAUDE-template.md +++ b/skills/plan/project-init/templates/CLAUDE-template.md @@ -20,7 +20,7 @@ 1. **严格遵循** `docs/04-技术规范.md`——命名 / 编码 / 统一响应 / 异常处理 / 数据访问 / 配置与安全 等项目专属技术规约全部在此 2. **严格遵循** `docs/04-技术规范.md § 1.2 分层结构 / § 2.1 目录约定`——文件放对位置 3. **每个后端接口** 必须先在 `docs/05-API接口契约.md` 定义,再编码实现 -4. **每个功能可追溯到 `REQ-XXX-NNN`**——commit tag + 代码注释(如 `// REQ-SYS-001: 用户登录`)+ plan/spec 文件名均用此 tag +4. **每个功能可追溯到 req_id `<模块代码>-<子模块代码>-<功能名>`**——commit tag + 代码注释(如 `// USR-USR-LOGIN: 用户登录`)+ plan/spec 文件名均用此 tag 5. **遇到跨模块改动**(动到非当前模块的代码)——允许改,但必须在《模块完成报告》记录原因 / 影响评估(留痕) ### 你禁止做的 🚫 @@ -56,7 +56,7 @@ ``` - `scope`: 模块名,如 `user` / `inventory` / `order` -- `subject`: 简短描述;业务类(feat / fix / test)必须带 `REQ-XXX-NNN` 后缀 +- `subject`: 简短描述;业务类(feat / fix / test)必须带 req_id(`<模块代码>-<子模块代码>-<功能名>`,如 `USR-USR-LOGIN`)后缀 `type` 含义: diff --git a/skills/plan/project-init/templates/docs-01-index-template.md b/skills/plan/project-init/templates/docs-01-index-template.md index 372d784..606d35b 100644 --- a/skills/plan/project-init/templates/docs-01-index-template.md +++ b/skills/plan/project-init/templates/docs-01-index-template.md @@ -1,10 +1,13 @@ # 需求清单 -> 本目录按模块组织所有功能需求。每个模块一个子目录,含 `_module.md`(模块头)和 `REQ-XXX-NNN.md`(每张 REQ 卡片一个文件)。下方核心功能点供 CC 拆分出 REQ 编号 + 标题 + 草拟规则;卡片内输入 / 输出的简述句和 N 张字段表由人工编辑。 +> 本目录按模块组织所有功能需求。每个模块一个子目录,含 `_module.md`(模块头)和 `.md`(每张 REQ 卡片一个文件;req_id = `<模块代码>-<子模块代码>-<功能名>` 恒 3 段,如 `USR-USR-LOGIN`——功能名由 CC 据核心功能点推断:英文大写短词、字符集 `[A-Z0-9_]`、多词用下划线如 `PWD_RESET`、同一 模块-子模块 内唯一)。下方核心功能点供 CC 拆分出 req_id + 标题 + 草拟规则;卡片内输入 / 输出的简述句和 N 张字段表由人工编辑。 ## 模块索引 -| 模块代码 | 模块名称 | 核心功能点(简要) | -|----------|----------|--------------------| -| 【人工填写:模块代码】 | 【人工填写:模块名称】 | 【人工填写:核心功能点】 | -| SYS | 系统管理 | 用户/角色/权限/部门/字典 等 | +> 一行一个子模块;同一模块有多个子模块时写多行(模块代码 / 模块名称 重复填写)。 + +| 模块代码 | 模块名称 | 子模块代码 | 子模块名称 | 核心功能点(简要) | +|----------|----------|------------|------------|--------------------| +| 【人工填写:模块代码】 | 【人工填写:模块名称】 | 【人工填写:子模块代码】 | 【人工填写:子模块名称】 | 【人工填写:核心功能点】 | +| USR | 用户管理 | USR | 用户账户 | 登录/注销/改密 等 | +| USR | 用户管理 | ROLE | 角色权限 | 角色 CRUD/权限分配 等 | diff --git a/skills/plan/project-init/templates/docs-08-initial-template.md b/skills/plan/project-init/templates/docs-08-initial-template.md index ae179b9..d057613 100644 --- a/skills/plan/project-init/templates/docs-08-initial-template.md +++ b/skills/plan/project-init/templates/docs-08-initial-template.md @@ -13,7 +13,7 @@ - [ ] 项目概述已填写(CLAUDE.md § 🎯 项目概述) - [ ] 技术栈已确认(docs/04 § 零) - [ ] 需求清单索引已填写(docs/01-需求清单/index.md) - - [ ] REQ 卡片骨架已生成(docs/01-需求清单//REQ-*.md,业务内容留待人工填写) + - [ ] REQ 卡片骨架已生成(docs/01-需求清单//.md,业务内容留待人工填写) - [ ] A2 骨架生成 — skeleton-gen - [ ] 架构文档已生成(docs/04 § 一+) diff --git a/skills/plan/scope-lock/SKILL.md b/skills/plan/scope-lock/SKILL.md index 04b56d5..60d1644 100644 --- a/skills/plan/scope-lock/SKILL.md +++ b/skills/plan/scope-lock/SKILL.md @@ -21,23 +21,23 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) - **A**:`CLAUDE.md` § 🎯 项目概述。占位符含项目名称 / 简述 / 目标用户 / 部署方式。 - **B**:`docs/04-技术规范.md` § 零。让用户检查 / 调整默认技术栈表(删不需要的行 / 改技术 / 加条目)。 -- **C**:`docs/01-需求清单/index.md`。让用户按业务列出所有模块(每行一个,如 SYS / PUR / SAL),「核心功能点」给关键词即可,CC 会拆 REQ 卡片。 +- **C**:`docs/01-需求清单/index.md`。让用户按业务列出所有子模块(一行一个子模块,五列:模块代码/模块名称/子模块代码/子模块名称/核心功能点;同一模块有多个子模块时写多行,如 USR-USR / USR-ROLE / PUR-ORD),「核心功能点」给关键词即可,CC 会拆 REQ 卡片。 ### D. 生成 REQ 卡片骨架并停下等人工审阅 -1. 读 `index.md` 解析模块索引;读两个模板 `${CLAUDE_SKILL_DIR}/templates/_module-template.md`、`${CLAUDE_SKILL_DIR}/templates/req-card-template.md` 作为卡片结构参照。 -2. **每模块/每 REQ 直接 `Write`**:对每个模块先 `mkdir -p "docs/01-需求清单/-<模块名>"`,再照模板结构 `Write`,`` / `<模块名>` / `` 按 `index.md` 实际值替换: - - **模块头** `_module.md`:据 `index.md` 填 `module_code` / `module_name` / `module_brief`;`依赖模块: TBD(A5 自动补)` / `涉及表: TBD(A3 自动补)` 两行原样保留。 - - **每个 REQ** `.md`:**照模板原样 `Write`**,只把 `{{req_id}}` / `{{title}}` / `{{goal}}` / `{{rules}}` / `{{constraints}}` / `{{acceptance}}` 这 6 个占位替换为据 `index.md` 推断的真实值;模板其余内容(`输入` / `输出` 示例字段表、`依赖表: TBD(A3 自动补)` / `依赖接口: TBD(A5 自动补)` 两行)**原样复制不动**;模板顶部 HTML 引导注释**不写进产物**。 +1. 读 `index.md` 解析模块索引(五列,一行一个子模块;按「模块代码」聚合——同一模块的多行共用一个模块子目录);读两个模板 `${CLAUDE_SKILL_DIR}/templates/_module-template.md`、`${CLAUDE_SKILL_DIR}/templates/req-card-template.md` 作为卡片结构参照。 +2. **每模块/每 REQ 直接 `Write`**:对每个模块先 `mkdir -p "docs/01-需求清单/-<模块名>"`,再照模板结构 `Write`,`` / `<模块名>` / `` 按 `index.md` 实际值替换: + - **模块头** `_module.md`:据 `index.md` 填 `module_code` / `module_name` / `module_brief`(module_brief 汇总该模块全部子模块行的核心功能点);`依赖模块: TBD(A5 自动补)` / `涉及表: TBD(A3 自动补)` 两行原样保留。 + - **每个 REQ** `.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 引导注释**不写进产物**。 3. 用 `Edit` 在 `docs/08-模块任务管理.md` 勾选(A1 子项): - - ` - [ ] REQ 卡片骨架已生成(docs/01-需求清单//REQ-*.md,业务内容留待人工填写)` + - ` - [ ] REQ 卡片骨架已生成(docs/01-需求清单//.md,业务内容留待人工填写)` 4. 打印「请人工填写 REQ 卡片」横幅并提示用户填完后回来继续: ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [scope-lock] REQ 卡片骨架已生成 - - 产出: docs/01-需求清单//{_module.md, REQ-*.md} + - 产出: docs/01-需求清单//{_module.md, .md} - 6 个占位已填真实值;输入/输出字段表为模板示例内容(如需可自行调整)。 - 审阅后选「继续」进 A1 校验. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -54,7 +54,7 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) 卡片正文是模板原样复制,只有 6 个 `{{}}` 标量由 CC 填——本步只校验这 6 个占位填全填真。 -1. 用 `Glob` 列出所有 `docs/01-需求清单//REQ-*.md`。 +1. 用 `Glob` 列出 `docs/01-需求清单/*/*.md`,跳过文件名为 `_module.md` 的模块头——其余即 REQ 卡片(文件名 == req_id)。 2. 对每张卡片用 `Read` + `Grep` 校验: - **无 `{{` 残留**:不得残留任何 `{{` 占位(命中即说明 6 个标量未全部替换)。 - **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 *) ✓ docs/04 § 零 技术栈 + build/lint/unit/e2e 命令 ✓ docs/01-需求清单/index.md 模块索引 ✓ docs/01-需求清单//_module.md 模块头 - ✓ docs/01-需求清单//REQ-*.md 6 个占位已填真实值(字段表为模板示例) + ✓ docs/01-需求清单//.md 6 个占位已填真实值(字段表为模板示例) ✓ config-vars.yaml 配置已锁(非敏感已填;DB 凭据 / 密钥占位待人工填,plan-start 把关) 自动进入 A2:skeleton-gen @@ -108,7 +108,7 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) - `docs/04-技术规范.md` - `docs/01-需求清单/index.md` - `docs/01-需求清单//_module.md` -- `docs/01-需求清单//REQ-*.md` +- `docs/01-需求清单//.md` - `config-vars.yaml` - `${CLAUDE_SKILL_DIR}/templates/config-vars-template.yaml` - `${CLAUDE_SKILL_DIR}/templates/req-card-template.md` diff --git a/skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md b/skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md index 23f0c35..6b3805b 100644 --- a/skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md +++ b/skills/plan/skeleton-gen/templates/docs-04-skeleton-template.md @@ -19,6 +19,12 @@ ### 2.1 目录约定 +**测试目录隔离(锁定约定,生成时原样保留,项目专属布局写在其后)**: +- 前端交付源码 = `frontend/src/**`,**不含任何测试文件**(对齐后端 `src/main/java` ↔ `src/test/java` 的物理分离)。 +- 前端单测(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 匹配)。 +- 前端 e2e(Playwright)一律放 `frontend/e2e/**`。 +- **禁止** `frontend/src/**` 内出现 `*.test.*` / `*.spec.*` / `__tests__/` / `__mocks__/` / `__smoke__/`;vitest 配置 `include` 限定 `tests/**/*.test.*`(src 内测试残留不被执行,约定漂移立即可见;tests/ 下的 helpers/fixtures 不带 `.test.` 中缀即不被当测试加载)。 + ### 2.2 状态管理 ### 2.3 请求封装 @@ -28,7 +34,7 @@ ## 三、共同约定 ### 3.1 Git 提交 -`(): REQ-XXX-NNN` +`(): `(req_id = `<模块代码>-<子模块代码>-<功能名>`,如 `USR-USR-LOGIN`) ### 3.2 分页查询 diff --git a/workflows/coding.mjs b/workflows/coding.mjs index 3f2ee84..d691887 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -9,10 +9,11 @@ export const meta = { description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.', phases: [ { title: 'Router' }, { title: 'Backend' }, { title: 'Frontend' }, - { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' }, + { title: 'Behavior' }, { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' }, ], - // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门, - // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。 + // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次** + // 行为验收(含 fix 循环),不再在每个 FE 的 review 循环内做 approve 子门(v2 per-FE 形态已撤销)。 + // 时序:featureLoop(frontend) → Behavior(行为门+fix)→ Gate(testGate 全量回归,兜底行为 fix 引入的回归)。 } const ROUTER_SCHEMA = { type:'object', additionalProperties:false, @@ -67,18 +68,18 @@ const GATE_SCHEMA = { type:'object', additionalProperties:false, required:['status'], properties:{ status:{type:'string',enum:['green','red']}, failures:{type:'array',items:{type:'string'}} } } -// BEHAVIOR_GATE_SCHEMA:前端行为门(per-FE behavior 子门)返回。 -// 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 覆盖率 / 环境错误分别结构化, -// JS 据 source/kind 分流(交互硬边界转 must-fix,文字按 source 二分 allowContinue,envError 走 retry, -// build-failed 确定性短路)。设计:见 docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 3/6/7。 +// BEHAVIOR_GATE_SCHEMA:前端行为门(阶段级,frontend-phase 末尾一次)返回。 +// 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 样式层 / 覆盖率 / 环境错误分别结构化, +// JS 据 source/kind 分流(交互/样式硬边界转 must-fix,文字按 source 二分 allowContinue,envError 走 retry)。 +// 设计:见 docs/design/2026-06-05-frontend-behavior-stage-gate.md(v3,取代 per-FE approve 子门形态)。 const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, required:['status','routesPlanned','routesReached','controlsEnumerated'], properties:{ status:{type:'string', enum:['green','red']}, - routesPlanned:{type:'integer'}, // 本 FE 关联路由数(覆盖率分母来源;per-FE 只数 feScope.routes,不数 router 全部) - routesReached:{type:'integer'}, // 实际带鉴权加载成功的本 FE 路由数 - controlsEnumerated:{type:'integer'}, // live 枚举到的本 FE 白名单控件数(空覆盖必须可见) + routesPlanned:{type:'integer'}, // 覆盖率分母 = 全部 FE spec「行为验收作用域」小节关联路由的并集(去重) + routesReached:{type:'integer'}, // 实际带鉴权加载成功的路由数 + controlsEnumerated:{type:'integer'}, // live 枚举到的白名单控件数(全 FE 并集;空覆盖必须可见) authState:{type:'string'}, // 以何角色登录 / 覆盖角色 / 未覆盖角色集 - // interactionFailures.locator:行为硬问题的源码定位(组件文件 [+ DOM 描述])。per-FE 行为门必须反查到 + // interactionFailures.locator:行为硬问题的源码定位(组件文件 [+ DOM 描述])。行为门必须反查到 // 组件文件路径才能转 must-fix 喂 fix;反查不出(B 类)→ 不入 interactionFailures,归 coverageGap(不放行)。 // 交互层硬边界:no-observable-effect / js-error / console-error / missing-docs05-call / binding-garbage interactionFailures:{ type:'array', items:{ type:'object', additionalProperties:false, @@ -96,17 +97,35 @@ const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, expected:{type:'string'}, actual:{type:'string'}, source:{type:'string', enum:['sentinel','i18n','literal','semantic']}, locator:{type:'string'} } } }, + // styleIssues:样式/布局客观断言(颜色 token 比对 + layout sanity)。全部客观、可 fix—— + // 有 locator → JS 并入 behaviorHard 转 must-fix;无 locator → 与交互硬问题同口径走 noLoc 仲裁。 + // 不确定项(半透明混合 / 无法归一化)按 prompt 约定不入此数组,记 decisions(宁漏勿误)。 + styleIssues:{ type:'array', items:{ type:'object', additionalProperties:false, + required:['page','element','kind','expected','actual'], + properties:{ + page:{type:'string'}, element:{type:'string'}, + kind:{type:'string', enum:[ + 'non-token-color', // 渲染色 ∉ tokens.css 色值集合(限项目自有样式作用域) + 'token-mismatch', // 应取某 token 但渲染值 ≠ 该 token 解析值(被硬编码/级联覆盖) + 'horizontal-overflow', // 路由页面出现横向滚动条(容差 1px) + 'overlap', // 白名单控件 bounding box 相互重叠(双方可见可点) + 'zero-size', // 预期可见的白名单控件渲染为 0 尺寸 + 'offscreen']}, // scrollIntoView 后仍不在视口内 + expected:{type:'string'}, actual:{type:'string'}, + locator:{type:'string'} } } }, // 覆盖率缺口:写证据 + recordDecisions,不单独 halt(空覆盖由 controlsEnumerated==0 兜底) - // build-failed-sibling-unimpl:兄弟 FE 未实现导致本 FE 之外路由/组件编译缺件(预期中途态,不归本 FE 缺陷) - // locator-not-resolvable:行为硬问题连组件文件都反查不出(B 类),计入未覆盖阻断 approve,不静默放行 + // locator-not-resolvable:行为硬问题连组件文件都反查不出(B 类),计入未覆盖阻断 green,不静默放行 + // scope-missing:某 FE spec 缺「行为验收作用域」小节(该 FE 路由不在分母)——与 B 类同级阻断 green coverageGaps:{ type:'array', items:{ type:'object', additionalProperties:false, required:['page','reason','detail'], properties:{ page:{type:'string'}, - reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed','build-failed-sibling-unimpl','locator-not-resolvable']}, + reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed','locator-not-resolvable','scope-missing']}, detail:{type:'string'} } } }, - // 环境错误(与业务断言失败严格区分):none 表示无环境问题;build-failed 是确定性短路(既不 retry 也不 halt)。 - // build-failed 时 rootCausePath 写报错根因文件路径——落在非本 FE 路径=兄弟未实现(短路放行),落在本 FE=真构建 bug。 + // 环境错误(与业务断言失败严格区分):none 表示无环境问题。 + // build-failed:阶段末尾全部 FE 已实现,不再有「兄弟未实现」短路——根因落在 frontend/ 源码且可定位 → + // 应归 interactionFailures[kind="js-error"](带 locator,可 fix);仅根因不可归到 frontend/ 源码 + // (依赖/环境/无法定位)才用本 kind(确定性失败,跳过自动 attempt 重试直送仲裁)。rootCausePath 写报错根因文件路径。 envError:{ type:'object', additionalProperties:false, required:['kind'], properties:{ @@ -218,7 +237,7 @@ function featureStageContract(phase) { `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe ? '实现文件必须落在 `frontend/` 下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。' : '产出范围限定 controller / service / repository / DTO / 校验 / SQL migration / REST 契约;**禁止**写 `frontend/` 路径下的实现(UI 推迟到前端阶段)。'}`, - `- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `REQ-XXX-NNN`。'}`, + `- id 形态:${fe ? '前端为 `FE-NN`(业务功能粒度,可关联多个 prototype 区域与多个 REQ)。' : '后端为 `<模块代码>-<子模块代码>-<功能名>`(3 段大写 req_id,如 `USR-USR-LOGIN`)。'}`, ].join('\n') } @@ -302,7 +321,7 @@ function deriveSpecPrompt(id, phase) { fe ? [ '', - '## 行为验收作用域结构化小节(per-FE 行为门唯一断言依据,**强制写到 spec 头部**)', + '## 行为验收作用域结构化小节(阶段末尾行为门按全部 FE 聚合断言作用域的唯一来源,**强制写到 spec 头部**)', '- 在 spec 文件头部(紧随标题/关联 REQ 之后)写一个**结构化小节**,标题逐字为 `## 行为验收作用域`,内含两条机器可读清单:', ' ```', ' ## 行为验收作用域', @@ -310,8 +329,8 @@ function deriveSpecPrompt(id, phase) { ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]', ' ```', `- **关联路由**:从 \`${ROOT}/frontend/\` router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 \`:id\` 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`, - '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外 / 共享控件归 coverageGap,绝不算本 FE 缺陷。', - '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。', + '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外控件记证据不算缺陷。', + '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes;阶段末尾的行为门会聚合**全部 FE** 的该小节作为整体断言作用域,缺失的 FE 会被记 `scope-missing` 阻断 green);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。', ].join('\n') : '', '', @@ -353,7 +372,7 @@ function planPrompt(id, phase, specPath) { '## 任务结构(每个 task = 一个 red-green-commit 单元,4 step)', '1. 写失败测试(给 `test_file::test_name` + 测试意图);2. 实现最小代码(给 `impl_file`);3. 子会话验证 PASS;4. commit。任务粒度 2-5 分钟。', fe - ? `- **硬护栏**:每个任务 \`impl_file\` 必须以 \`frontend/\` 开头;命中 \`backend/\` / \`sql/\` / \`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);命中 \`backend/\` / \`sql/\` / \`scripts/\` → 修正后重渲染。` : `- **硬护栏**:任务粒度限定后端文件(controller / service / repository / DTO / 校验 / SQL migration);**禁止**生成 \`frontend/\` 路径任务。`, '- 允许写死的少数场景:DDL / migration 语句、合同级常量(错误码 / JWT claim / Redis key / 路由 path / API client 签名 / Design Tokens 名)、可选的测试断言 sketch。其余一律散文 + 签名描述。', '- 首次出现的类 / 方法 / 组件 / hook / API client 函数必须给出签名;跨 task 的签名 / 错误码 / props 类型必须一致。', @@ -390,13 +409,13 @@ function tddPrompt(id, phase, planPath) { fe ? '' : '- **Schema 改动前置**(仅当 plan 声明需要):第一个任务写 migration 文件 `V__.sql`(`` = 现有 `sql/migrations/V*.sql` 最大版本号 + 1,只含 DDL),**同步**把新 CREATE / ALTER 反向更新到 `docs/03-数据库设计文档.md` 对应表小节(docs/03 是 schema 的 SSoT),migration + docs/03 改动同一 commit。', '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。', fe - ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' + ? '- **测试落位(硬约定,对齐 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 复刻。' : '', fe ? '- **e2e 基线约束**:e2e 跑在「空库重建 + Flyway schema + 演示种子」基线上(骨架 globalSetup 已注入 `sql/seed`,无需测试自行建库/起栈)。e2e 断言**优先**定位**演示种子已知主键行**(1000–9999)或**测试自建数据**;**禁止**「全表恰好 N 行」式依赖全局计数的脆弱断言(演示种子行数会随后续模块种子增长,全局计数断言必然 flaky)。' : '', fe - ? `- **占位替换(保证中途可构建 + per-FE 行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/\` router 定位本 FE 路由 path 的 import 行;仍在 \`frontend/\` 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` + ? `- **占位替换(保证中途可构建 + 阶段末尾行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/\` router 定位本 FE 路由 path 的 import 行;仍在 \`frontend/\` 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` : '', '', '## 护栏', @@ -472,8 +491,8 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { '## 输入给 reviewer', `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`, fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '', - `- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`, - fe ? `- **行为验收作用域小节校验(per-FE 行为门前置真值,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——行为门只能据此确定本 FE 路由作用域。` : '', + `- **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 → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`, + fe ? `- **行为验收作用域小节校验(阶段级行为门的作用域真值来源,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——阶段末尾的行为门按全部 FE spec 的该小节聚合断言作用域,缺失/错配会让该 FE 漏验或归因失真。` : '', round > 1 && lastVerifySummary ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。` : '', @@ -641,73 +660,74 @@ function seedGenPrompt(module) { ].filter(Boolean).join('\n') } -// ---- 前端行为验收(per-FE behavior 子门)---- -// 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。 -// 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发, -// 起本 FE 全栈 + sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才放行 approve。 +// ---- 前端行为验收(阶段级行为门,v3)---- +// 设计权威:docs/design/2026-06-05-frontend-behavior-stage-gate.md。 +// 时机:featureLoop(frontend) 全部 FE 通过静态 review(req-done tag 已打)之后、testGate 之前, +// 整个前端阶段只跑**一次**行为验收:起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合的作用域并集 +// 枚举路由控件/文字,硬问题转可 fix must-fix→fix→复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮),green 才进 testGate。 // 门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend') // (其路径护栏命中 backend/sql/scripts 即越界硬停,与门必须运行 setup-test-db / 起后端 / 生成 SQL 种子自相矛盾)。 // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符; // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright, -// 唯一**可写** = .tmp/behavior-gate//r/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 +// 唯一**可写** = .tmp/behavior-gate/frontend-phase/r/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 function behaviorGateContract() { return [ '## 硬约束(非交互行为验收子代理)', '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', - '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', + '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「每个 FE 的每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(gradle bootRun 等)、\`node ${ROOT}/scripts/seed-demo-data.mjs\`(只运行注入演示种子,不修改脚本)、起前端 headless(vite / playwright)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate//r/\`(spec/种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/reviews/--behavior-r-a.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/reviews/assets/...\`)。`, + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`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/\`(种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r-a.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/module-reports/assets/...\`)。`, `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, - '- **per-FE 中途态豁免(关键)**:本门在 **per-FE 模式**下运行——`frontend/` 中**本 FE 之外**的路由/组件可能尚未实现,属预期中途态。遇到指向未建路由的链接 / 404 / 编译缺件(兄弟 FE 或骨架占位未覆盖),一律记 `coverageGaps[reason="build-failed-sibling-unimpl"]` 或 `envError.kind="build-failed"`(按根因路径归属,见 step0/step2),**绝不**归为本 FE 的 `interactionFailures`。**本 FE 路由清单(feScope.routes)是唯一断言作用域**;白名单外 / 共享控件归 coverageGap,不算本 FE 缺陷。', + '- **全量终态前提(关键)**:本门跑在**全部 FE 已实现并通过静态 review 之后**——`frontend/` 不应再有未实现路由 / FeStub 占位。某路由仍渲染 `data-fe-stub` 占位 → 这是硬缺陷(tdd 漏做占位替换),归 `interactionFailures[kind="no-observable-effect"]`,locator 指向 router 文件该路由 import 行,detail 写明「路由仍指向 FeStub 占位」。**断言作用域 = 全部 FE spec 的 `## 行为验收作用域` 小节并集**;白名单外控件记证据不入断言集。', '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。', - '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / FE id / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。', + '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。', ].join('\n') } -// behaviorGatePrompt:per-FE 行为验收子代理的完整流水线提示(step0-6 + schema)。 -// id:本 FE id(如 FE-07);specPath:本 FE spec(含 ## 行为验收作用域 小节,feScope 来源 + 日期前缀); -// behaviorRound:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX);attempt:本轮内环境 race 重试序号(1..)。 -// 每 (FE × behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。 -function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { - const safeId = id ?? 'FE' - const tmpDir = `${ROOT}/.tmp/behavior-gate/${safeId}/r${behaviorRound}` - const date = (() => { try { return dateFromArtifactPath(specPath) } catch { return '' } })() - const evidence = `docs/superpowers/reviews/${date}-${safeId}-behavior-r${behaviorRound}-a${attempt}.md` +// behaviorGatePrompt:阶段级行为验收子代理的完整流水线提示(step0-6 + schema)。 +// feItems:本前端阶段全部 FE-NN(作用域聚合的清单真值,来自 Router frontend-phase 模块); +// behaviorRound:阶段门内的行为 fix 轮(1..BEHAVIOR_STAGE_MAX);attempt:本轮内环境 race 重试序号(1..)。 +// 每 (behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。 +function behaviorGatePrompt(feItems, behaviorRound, attempt) { + const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(调用方未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' + const tmpDir = `${ROOT}/.tmp/behavior-gate/frontend-phase/r${behaviorRound}` + const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-r${behaviorRound}-a${attempt}.md` return [ - `# behavior — 前端 per-FE 行为验收(headless,FE=${safeId}, behaviorRound=${behaviorRound}, attempt=${attempt})`, + `# behavior — 前端阶段级行为验收(headless,frontend-phase, behaviorRound=${behaviorRound}, attempt=${attempt})`, '', behaviorGateContract(), '', '## 目标', - `用真实全栈运行证明本 FE \`${safeId}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`, - `单个子会话内**收敛完成**:冷起栈 → 逐**本 FE 路由**枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`, - `- 本 FE 行为验收作用域唯一真值 = spec \`${specPath}\` 头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先 Read 该 spec 取出 feScope;缺该小节 → \`envError.kind="stack-not-ready"\` 并在 detail 写明(不应出现:reviewer 已校验它存在)。`, + '用真实全栈运行证明**全部 FE** 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。整个前端阶段只跑这一道行为门(featureLoop 全部 FE 已过静态 review)。', + '单个子会话内**收敛完成**:冷起栈 → 逐路由枚举(全 FE 作用域并集)+ 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。', + `- 本阶段 FE 清单:${feList}。`, + `- 断言作用域真值 = **每个 FE** 的 spec(\`${ROOT}/docs/superpowers/specs/-.md\`,同一 FE 多份取最新日期)头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先逐 FE Read 取出并**聚合为并集**(路由去重、逐路由标注归属 FE);某 FE 缺 spec 或缺该小节 → 记 \`coverageGaps[reason="scope-missing", page=""]\`(该 FE 路由不计入分母,**绝不**静默跳过)。`, behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '', '', '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 - ? `本次是本 FE 首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/${safeId}/\` 目录(清掉本 FE 历史残留 runner/种子/spec),再新建本轮子目录 \`${tmpDir}/\`。` + ? `本次是本阶段首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/frontend-phase/\` 目录(清掉历史残留 runner/种子),再新建本轮子目录 \`${tmpDir}/\`。` : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, - `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本 FE 起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, + `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本门起的全部子进程**并透传结构化结果。**绝不**让前台 gradle bootRun / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`, `- \`${ROOT}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, '', - '## step0 探测 + build 归因(确定性短路前置,依赖 build-failed kind)', + '## step0 探测 + build 归因', `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/config-vars.yaml\`。`, '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。', - '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——', - ` - 落在**非本 FE 的 \`frontend/\` 路径**(兄弟 FE 组件缺失 / 骨架占位未覆盖 / 指向未建路由)→ \`envError.kind="build-failed"\` + \`rootCausePath=<非本FE路径>\`(**预期中途态**,不是本 FE bug)。`, - ' - 落在**本 FE 路径**(feScope 关联组件)→ 才是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]`(带 locator=组件文件)。', + '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——全部 FE 已实现,**没有**「兄弟未实现」豁免:', + ' - 根因落在 `frontend/` 源码且可定位到文件 → 真构建 bug → 归 `interactionFailures[kind="js-error"]`(locator=根因文件路径,可转 must-fix 喂 fix)。', + ' - 根因不可归到 `frontend/` 源码(依赖 / 工具链 / 无法定位)→ `envError.kind="build-failed"`(如能定位仍填 `rootCausePath`)。', ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。', '', - '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)', - '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。', - '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。', + '## step1 路由真值发现(覆盖率分母 = 全部 FE 作用域路由并集)', + '- 分母来源 = 全部 FE spec `## 行为验收作用域` 小节 `关联路由:` 清单的**并集(去重)**;`routesPlanned` = 并集路由数。逐路由标注归属 FE(证据分小节与硬问题归因用)。', + `- 与 \`${ROOT}/frontend/\` router 配置对账:FE 作用域声明但 router 缺失的路由 → \`coverageGaps[reason="unreachable-no-route"]\`;router 声明但不属任何 FE 作用域的路由记证据(不入分母、不断言)。`, + '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**;每路由标注所需登录角色。', '- 带参动态路由用**种子已知主键**实例化(可用**演示种子已知主键**(1000–9999)或 **sentinel 主键**(≥100000));无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', - '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', '', '## step2 起栈五段严格时序(schema 由 Flyway 在后端启动时才建)', `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) { '4) **此时才跑 sentinel 种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** sentinel 种子(先父后子;专司绑定断言——「保列表非空触发行级操作」已由本 step2 子项 3) 注入的演示种子承担)。失败 → `envError.kind="seed-error"` + 结构化根因,**不**混进交互 RED。', ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——数值主键**一律 ≥100000**(固定区间,不再动态扫描既有键:初始数据 1–999 / 演示种子 1000–9999 已由区间约定隔离,sentinel 落 ≥100000 天然不冲突);字符串列**仍逐字段唯一编码**(`_S` 样式,如 `CUST_NAME_S001`,抓绑错字段——演示数据已被禁用该样式,故 sentinel 独占)+ 行序号保 UNIQUE;enum 列从 docs/03 值域取并标注。断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。', '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', - '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。', + '- `finally` **硬要求 kill 本门起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。', '', '## step2.5 鉴权 bootstrap(确定性前置)', '- 用 config-vars `admin_init` 或种子已知凭据,经 `docs/05` 登录端点**真实登录**拿 JWT,注入 Playwright `storageState`;`authState` 记角色覆盖(覆盖 / 未覆盖角色集)。', '- 登录失败 = `envError.kind="auth-failed"`(环境 race,走 retry),**绝不**当成死控件。', '', - '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;只驱动本 FE feScope)', - '- **只枚举/驱动 feScope.routes + feScope.controlWhitelist**(本 FE 白名单控件)。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 本 FE 推导清单,分子 = live 枚举。', + '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;驱动全部 FE 作用域并集)', + '- **枚举/驱动 step1 聚合的全部路由 + 各 FE 控件白名单并集**。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 聚合清单,分子 = live 枚举。', + '- **FeStub 残留检测**:每路由加载后检查 `data-fe-stub` 元素;仍渲染占位 → 该 FE 的 tdd 漏做占位替换(硬缺陷),归 `interactionFailures[kind="no-observable-effect"]`(locator=router 文件该路由 import 行,detail 写「路由仍指向 FeStub 占位」)。', '- 分母有但首帧无的控件:runner 尝试**驱动到出现态**(种子保列表非空触发行级操作 / 进多步流程下屏 / 展开 dropdown / 切 tab 后二次枚举);仍不可达 → `coverageGaps[reason="deep-control-not-driven"]`,不静默判 green。到不了的路由 → `coverageGaps[reason="unreachable-auth"|"unreachable-no-route"]`,与「到达了但控件死」严格区分。', - '- **白名单外 / 共享控件**:若属其它未 approve FE 或共享区 → 归 `coverageGaps[reason="deep-control-not-driven"]`,**绝不**归本 FE 的 `interactionFailures`。', + '- **白名单外控件**(任何 FE 白名单都未列,如共享导航/布局区):不入「必须有效果」断言集,记证据即可;确属可疑死控件可记 `coverageGaps[reason="deep-control-not-driven"]`。', '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', - '- `routesReached` / `controlsEnumerated` 据实填(本 FE 子集空覆盖必须可见)。', + '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。', '', '## step4 推导期望', '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', '', - '## step5 断言(两层 + 可观测效果白名单 + 硬问题带源码 locator)', + '## step5 断言(三层:交互/文字/样式 + 可观测效果白名单 + 硬问题带源码 locator)', '- **交互层可观测效果白名单**:URL 变化 / docs05 网络调用(`page.on("request")` 比对端点)/ DOM 变更 / 校验信息 / 弹层 / toast / 原生对话框(枚举前注册 `page.on("dialog")`,confirm/alert/beforeunload 计合法效果,防 confirm 阻塞误判 missing-docs05-call)/ 下载(`page.on("download")`)/ 新标签(`page.on("popup")` / `target=_blank`)。', ' - 无任何效果 → `interactionFailures[kind="no-observable-effect"]`;JS 异常 → `js-error`;`console.error` → `console-error`;应发未发网络调用 → `missing-docs05-call`。断言用 auto-waiting / `expect.poll`,**不用**固定 sleep。', '- **文字层**:动态文字格对比该 region 字段的唯一 sentinel(抓绑错字段)。', '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', - '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 approve)。', + '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix,必须带 `locator`;反查不到组件文件则归 `coverageGaps[reason="locator-not-resolvable"]`);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 green)。', + '- **样式层(客观断言:颜色 token 比对 + layout sanity)**。断言作用域 = **白名单控件及其直接容器 + spec/prototype 点名区域**;组件库深层内部元素**不查**,只查可见元素:', + ` - **色值基准确定性派生**:读 \`${ROOT}/src/styles/tokens.css\` 解析全部 \`--color-*\`,用探针元素 getComputedStyle 把任意色值格式归一化为 canonical rgb 集合;被检元素的渲染值(\`color\` / \`background-color\` / \`border-color\`)同法归一化后比对。`, + ' - **颜色断言**:渲染色 ∉ token 集合 → `styleIssues[kind="non-token-color"]`;spec「Design Tokens 引用清单」点名了具体 token 的元素,渲染值 ≠ 该 token 解析值 → `styleIssues[kind="token-mismatch"]`。半透明混合 / 无法归一化的值 → **不入** styleIssues,记 `decisions[]`(宁漏勿误)。', + ' - **几何断言(layout sanity)**:每路由 `scrollWidth > clientWidth + 1` → `styleIssues[kind="horizontal-overflow"]`(locator=该路由 view 组件文件);白名单控件两两 boundingBox 交叠 >4px² 且双方可见非 inert → `overlap`;预期可见控件 box 为零 → `zero-size`;`scrollIntoViewIfNeeded` 后仍不在视口 → `offscreen`。', + ' - **视口**:默认用 Playwright 默认视口;prototype 明确声明 viewport 时用之并记 `decisions[]`。', + ' - styleIssues 全部是客观硬问题:locator 要求与交互层逐字同口径(A 类反查组件文件;反查不出归 `coverageGaps[reason="locator-not-resolvable"]`),`expected`/`actual` 写比对双方的具体值(如 `expected="var(--color-primary)→rgb(22,119,255)" actual="rgb(255,0,0)"`)。', '- **行为硬问题必须带源码 locator(转 must-fix 喂 fix 的前置)**:', - ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。', + ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 归属 FE + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。', ' - **B 类(连组件文件都反查不出)**:**不静默降级放行**——归 `coverageGaps[reason="locator-not-resolvable"]`(计入未覆盖,使本轮不能判 green),或归 `envError.kind="stack-not-ready"` 走 retry。绝不把无 locator 的硬问题塞进 `interactionFailures` 不带 locator(上层会因无 locator 走 adjudicate(allowContinue:false),绝不放行)。', '', `## step6 证据落盘 + commit(运行时行为,沿用证据 commit 习惯)`, - `- 写 \`${evidence}\`:本 FE feScope / 推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`, - `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/reviews/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, + `- 写 \`${evidence}\`:**按 FE 分小节**(每 FE:作用域 / 推导期望 / 逐控件判定 / 该 FE 的 styleIssues 与 coverageGaps),全局段写 routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ token 色值集合摘要 / 截图索引。`, + `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/module-reports/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`, - commitBlock(`${evidence} docs/superpowers/reviews/assets`, - `docs(behavior:${safeId}:r${behaviorRound}-a${attempt}): per-FE 行为验收证据`), + commitBlock(`${evidence} docs/superpowers/module-reports/assets`, + `docs(behavior:frontend-phase:r${behaviorRound}-a${attempt}): 阶段级行为验收证据`), '', '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)', - '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无阻断性 envError + 本 FE 覆盖非空)| `red`。', - '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数本 FE feScope**;空覆盖必须可见)。', - '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`(含 `source="sentinel"` 的 textIssue)。', + '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + **样式层无失败** + 无阻断性 envError + 覆盖非空)| `red`。', + '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数全部 FE 作用域并集**;空覆盖必须可见)。', + '- `interactionFailures` / `textIssues` / `styleIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`(含 `source="sentinel"` 的 textIssue 与全部 styleIssues)。', '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids;`build-failed` 时填 `rootCausePath`。', '- 做过任何自主默认 → `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。', '- 不要返回额外字段(schema 是 `additionalProperties:false`)。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', ].filter(Boolean).join('\n') } +// behaviorReverifyPrompt:阶段级行为 fix 后的功能复验。fix 改的是 frontend/ UI 源码,可能引入功能回归—— +// 在下一 behaviorRound 重起全栈之前,先派子会话跑**全量前端单测**(vitest,不跑 e2e——e2e/行为维度由下一轮 +// 行为门重跑 + 阶段 testGate 全量回归兜底),红则当功能回归硬边界(调用方 allowContinue:false)。 +function behaviorReverifyPrompt(behaviorRound, fixedCount) { + const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-reverify-r${behaviorRound}.md` + return [ + `# behavior-reverify — 行为 fix 后前端单测复验(behaviorRound=${behaviorRound})`, + '', + featureStageContract('frontend'), + '', + '## 目标', + `阶段级行为门第 ${behaviorRound} 轮 fix(${fixedCount} 项 must-fix)改动了 \`frontend/\` 源码——**派发 Agent 子会话**跑全量前端单测确认无功能回归。**主会话从不直接跑测试,也不自由编写证据。**`, + '', + '## 流程', + `- 命令从 \`${ROOT}/docs/04-技术规范.md § 零 frontend.test_command\` 取(缺失默认 \`pnpm test:ci\`);**只跑单测(vitest),不跑 e2e**。`, + '- 派子会话执行,子会话只返回结构化 JSON:`{command, exit_code, passed, failed, failed_list, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行)。', + '- **`exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt(fix 引入功能回归,绝不带红进入下一轮行为门)。', + `- 证据写入 \`${evidence}\`(每轮独立文件不覆盖前轮)。`, + '', + commitBlock(evidence, `docs(behavior:frontend-phase:r${behaviorRound}): 行为 fix 后单测复验`, + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'), + '', + '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', + `- 全部通过:\`{ "status": "ok", "artifactPath": "${evidence}", "summary": "" }\`。`, + '- 任一红色 / 缺值 → `{ "status": "halt", "reason": "<失败用例摘要>" }`。', + ].join('\n') +} + // ---- 前端骨架占位 stage(runFrontendSkeleton 用)---- -// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A,blocker)。 +// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A;v3 阶段级行为门下仍保留)。 // 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(未实现 FE 路由指向 FeStub 占位) // + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。 -// 由此 per-FE 行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点、build-failed 退化为罕见兜底。 +// 由此逐 FE 的 verify(e2e) 与阶段末尾行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点。 // feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。 function frontendSkeletonPrompt(feItems) { const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' @@ -798,6 +853,7 @@ function frontendSkeletonPrompt(feItems) { ` - **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 复用)。`, ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树。', ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 空库重建 + Flyway schema + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐。', + '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/ 镜像路径后方可收窄)。', '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', '', @@ -823,9 +879,10 @@ function frontendSkeletonStatePromptM(feItems) { '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', microStepContract(), '', - `用 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.*\`)。`, - '- 全部满足(骨架已建齐,含 e2e 基线脚手架)→ `{ "exists": true }`', - '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup)→ `{ "exists": false }`', + `用 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.*\`。`, + `- **legacy 豁免**:若 \`${ROOT}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`, + '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`', + '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`', '## 输出(EXISTS_SCHEMA)', ].join('\n') } @@ -851,11 +908,11 @@ function microStepContract() { // ============================================================================ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环) -// per-FE 行为子门预算(二维,钉死防证据覆盖;设计 §6.4): -// - BEHAVIOR_FE_MAX = approve 子门内的行为 fix 轮硬上限(每 FE);超限 throw HALT。**不**复用 review 的 10 轮、 -// **不**让 REVIEW_HARD_ROUNDS × 行为重试隐式相乘——典型一次过(1 轮),最坏 3 轮。 +// 阶段级行为门预算(二维,钉死防证据覆盖): +// - BEHAVIOR_STAGE_MAX = 阶段门内的行为 fix 轮硬上限(整个前端阶段共用,每轮 fix 可批量修当轮全部 must-fix); +// 超限 throw HALT。典型一次过(1 轮),最坏 3 轮。 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 -const BEHAVIOR_FE_MAX = 3 +const BEHAVIOR_STAGE_MAX = 3 const BEHAVIOR_ATTEMPT_MAX = 2 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' @@ -1264,7 +1321,7 @@ function classifyCrossModulePromptM(moduleId, files) { '- 落在共享根(如 `docs/`、`scripts/`、`sql/migrations/`、`README.md` 等)→ **不算**跨模块。', '', '## 输出(CROSS_CLASSIFY_SCHEMA)', - '- `{ "crossModule": [ { "file": "...", "targetModule": "module_x", "reason": "<本模块哪个 REQ-XXX-NNN 迫使改它,1 句>", "impact": "<目标模块哪些 API/行为/调用方/测试受影响,1-3 句>" }, ... ] }`', + '- `{ "crossModule": [ { "file": "...", "targetModule": "module_x", "reason": "<本模块哪个 REQ(req_id,如 USR-USR-LOGIN)迫使改它,1 句>", "impact": "<目标模块哪些 API/行为/调用方/测试受影响,1-3 句>" }, ... ] }`', '- 无跨模块改动:`{ "crossModule": [] }`', '- **不要留 `TBD(CC 补)`**:本步骤就是补齐的唯一时机;推不出原因 / 影响 → 整步失败(schema 失败即可,调用方会 halt)。', ].join('\n') @@ -1324,7 +1381,7 @@ function reportPrompt(module) { '## 前置', `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, fe - ? `- **前端行为验收已并入 per-FE review 循环**(reviewer approve 子门,行为 green 是 \`req-done/\` 的前置真值)——report **不再**校验阶段级 behavior-gate 文件(已不再产生)。**对每个 \`req-done/\` tag 即视为该 FE 行为已过**(避免双真值)。可选轻量校验:每个 FE 存在对应 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/--behavior-r*-a*.md\` 且最后一份非 RED;缺证据不 halt(仅在 § ⑤/⑧ 标注)。` + ? `- **验证阶段级行为门绿**:Glob \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`,按 behaviorRound → attempt 数字升序读取。**最后一份必须非 RED(绿)**;最后一份 red 或一份证据都没有 → 立即 halt(绝不在行为红 / 无行为证据上打 milestone)。注意 per-FE 行为证据(\`reviews/--behavior-*\`)已不再产生,不要校验。` : '', '', '## 收集输入(取摘要而非正文)', @@ -1334,8 +1391,8 @@ function reportPrompt(module) { `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', - `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/-FE-*-behavior-r*-a*.md\`(按 FE → behaviorRound → attempt 排序)的 flake / 环境 race(envError,含 build-failed 短路)/ 文字 continue 记录一并纳入 § ⑤ 汇总**。`, - `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/-FE-*-behavior-r*-a*.md\` 汇总各 FE 的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, + `- § ⑤:把 \`${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 记录一并纳入 § ⑤ 汇总**。`, + `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按阶段级行为证据 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-r*-a*.md\`(取最后一份的逐 FE 小节)汇总各 FE 的 \`coverageGaps\` + 样式 \`styleIssues\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', ].join('\n') : [ @@ -1632,9 +1689,6 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { let lastVerify = verifyResult let lastIssuesCount = 0 let reviewGuidance = '' // 仲裁 retry 时注入下一轮 review 的纠正指令 - // softPassed 提升到 reviewWithFixLoop 顶层作用域(与本 FE review 同寿命,跨 behaviorRound 持久)—— - // 行为软文字一旦被仲裁 continue 放行(降级),重跑后即便仍在 textIssues 也不再追问,避免反复消耗仲裁预算。 - const behaviorSoftPassed = new Set() for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 @@ -1645,13 +1699,9 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { reviewGuidance = '' // 已消费 if (r.verdict === 'approve') { - // approve 闸显式 AND(设计 §6.2):reviewer.verdict==='approve' ∧ behaviorSubGate green(仅前端)。 - // 后端逐字不变(无行为维度);前端:静态 approve 后**不立即 return**,先进 per-FE 行为 approve 子门—— - // 起本 FE 全栈验「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才放行; - // 行为 green ⇒ 才 flipDocs08Checkbox + return(req-done tag 落点 featureLoop 不动,语义自动升级为「静态过+行为过」)。 - if (fe) { - await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) - } + // approve = 静态 review 通过即放行(前后端同构)。行为验收已挪到阶段末尾的 phase('Behavior') + // 一次性阶段级行为门(runBehaviorGate)——不再是 per-FE approve 子门。req-done/ 语义 =「静态过」; + // 行为维度由阶段门统一验收,行为 green 是 milestone 的前置(reportPrompt 校验行为证据非 RED)。 await flipDocs08Checkbox(fe, id, phase, grp) return { id, phase, approved:true, rounds:round } } @@ -1665,9 +1715,8 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`, { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)', reviewerIssues: r.issues || [] }, grp, round) - // continue 视为「无 must-fix → 静态 approve」——前端仍须先过行为 approve 子门(行为 green 是任何 approve return 的前置)。 + // continue 视为「无 must-fix → 静态 approve」(行为维度由阶段末尾的行为门统一验收,不在此处)。 if (verdict.action === 'continue') { - if (fe) await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } } if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`) @@ -1721,125 +1770,96 @@ async function testGate(module, phase) { return g } -// ---- 前端 per-FE 行为验收控制流(runBehaviorGateOnce + behaviorSubGate)---- -// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 6.3 / 7。 -// 行为验收并入 per-FE reviewWithFixLoop 的 approve 子门——reviewer 即将 approve 时才触发,绝不每 review round 起栈。 -// behaviorSubGate 失败分层(per-FE 缩 scope,保留原 runBehaviorGate 的分层语义): -// - build-failed(兄弟 FE 未实现 / 占位未覆盖,根因落非本 FE 路径)= 确定性短路:记 coverageGap + decisions, -// 本轮行为门视为「本 FE 非缺陷」直接放行 approve(预期中途态,不 retry 不 halt)。 -// - envError(其它) / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。 -// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 approve。 -// - behaviorHard = interactionFailures + sentinel textIssues:有 locator → 降维喂 fixPrompt 跑 fix(fix 后功能 reverify + 下一轮重跑行为); -// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不 approve。 -// - BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。 +// ---- 前端阶段级行为验收控制流(runBehaviorGateOnce + runBehaviorGate)---- +// 设计:docs/design/2026-06-05-frontend-behavior-stage-gate.md(v3)。 +// 时机:顶层 frontend 段 featureLoop 全部 FE 完成(req-done 已打)之后、testGate 之前,phase('Behavior') 下 +// 整个前端阶段只跑**一次**行为验收(含 fix 循环)。失败分层: +// - envError / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。 +// build-failed(阶段末尾无「兄弟未实现」豁免)属确定性失败:跳过自动 attempt 重起(重跑不自愈),直送仲裁。 +// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 green。 +// - behaviorHard = interactionFailures + sentinel textIssues + styleIssues(颜色 token / layout sanity): +// 有 locator → 降维喂 fixPrompt 跑 fix(fix 后全量前端单测复验 + 下一 behaviorRound 重跑门); +// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不放行。 +// - BEHAVIOR_STAGE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。 -// envBlocked / ifails:per-FE bg 的环境/空覆盖与交互失败判定(build-failed 不计 envBlocked——它走确定性短路分支)。 +// envBlocked / ifails:环境/空覆盖与交互失败判定。v3:build-failed 计入 envBlocked(不再有短路放行分支)。 function behaviorEnvBlocked(r) { const k = r.envError && r.envError.kind - const ev = (k && k !== 'none' && k !== 'build-failed') ? r.envError : null + const ev = (k && k !== 'none') ? r.envError : null const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0) return { ev, emptyCov, blocked: !!ev || emptyCov } } function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] } +const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed') -// runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 -// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。 -// behaviorRound:approve 子门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 -async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) { - const lbl = (a) => `behavior:${id}:r${behaviorRound}:a${a}` +// runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 +// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。 +// behaviorRound:阶段门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 +async function runBehaviorGateOnce(feItems, behaviorRound) { + const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` let attempt = 1 - let bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) - recordDecisions(`behavior:${id}`, bg.decisions) - - // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。 - const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed' - if (isBuildFailedShortCircuit(bg)) return bg + let bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) + recordDecisions('behavior:frontend-phase', bg.decisions) // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 - while (behaviorEnvBlocked(bg).blocked && attempt < BEHAVIOR_ATTEMPT_MAX) { + // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 + while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { attempt += 1 - bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) - recordDecisions(`behavior:${id}`, bg.decisions) - if (isBuildFailedShortCircuit(bg)) return bg + bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) + recordDecisions('behavior:frontend-phase', bg.decisions) } let envState = behaviorEnvBlocked(bg) for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { const reason = envState.ev - ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}` + ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}${envState.ev.rootCausePath ? `(rootCausePath=${envState.ev.rootCausePath})` : ''}` : `behavior 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)` - const verdict = await adjudicate(`behavior-env:${id}`, - { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, grp, adj) - if (verdict.action !== 'retry') throw new Error(`HALT behavior-env ${id}: ${verdict.rationale || reason}`) + // riders:环境失败同轮搭车的硬问题(如 build-failed 时已归因到的 interactionFailures/styleIssues)—— + // 本轮不进 fix(环境未就绪,fix 无意义),但透传给仲裁者辅助 retry/halt 判断;若环境修复后仍真实,下一轮门会重新发现。 + const verdict = await adjudicate('behavior-env:frontend-phase', + { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, + riders: { interactionFailures: behaviorIfails(bg).length, styleIssues: (bg.styleIssues || []).length, + sentinelTextIssues: (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length }, + allowContinue:false }, 'Behavior', adj) + if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) attempt += 1 - bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), - {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) - recordDecisions(`behavior:${id}`, bg.decisions) - if (isBuildFailedShortCircuit(bg)) return bg + bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), + {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) + recordDecisions('behavior:frontend-phase', bg.decisions) envState = behaviorEnvBlocked(bg) } - if (envState.blocked) throw new Error(`HALT behavior-env ${id}: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) + if (envState.blocked) throw new Error(`HALT behavior-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) return bg } -// behaviorSubGate:reviewer approve 的「行为 approve 子门」。green 才允许 reviewWithFixLoop return approve。 -// softPassed:由 reviewWithFixLoop 顶层注入,跨 behaviorRound 持久(软文字一旦放行不再追问)。 -// green ≡ behaviorHard.length===0 ∧ envError∈{none,build-failed} ∧ 本 FE 覆盖非空(或 build-failed 短路)。 -async function behaviorSubGate(id, specPath, grp, softPassed) { +// runBehaviorGate:阶段级行为门主循环(被顶层 frontend 段调用,phase('Behavior') 下)。green 才正常返回(放行进 testGate)。 +// softPassed:本函数内声明,跨 behaviorRound 持久(软文字一旦放行不再追问,避免反复消耗仲裁预算)。 +// green ≡ behaviorHard.length===0 ∧ envError===none ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空 ∧ 无未解释漏达路由。 +async function runBehaviorGate(feItems) { const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` - for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) { - const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound) - - // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → green-by-skip 放行。但骨架(lazy router + FeStub) - // 令「合法的兄弟未实现 build-failed」极罕见,故一个 build-failed 更可能是本 FE 引入的真共享代码回归; - // 绝不凭未校验的 LLM 归因静默放行——先过轻量前置校验(comment §107-108 声称 load-bearing 的边界,此前无 JS 兜底): - // a) 必须有 rootCausePath(否则无从判定根因落点); - // b) 不得同时携带交互硬问题(interactionFailures / source=sentinel 文字)——那是真缺陷搭车。 - // 任一不满足 = 「脏」build-failed → 不短路,过 adjudicate(allowContinue:false) retry/halt,绝不 green-by-skip。 - if (bg.envError && bg.envError.kind === 'build-failed') { - const rootCausePath = (bg.envError.rootCausePath || '').trim() - const hardRiders = behaviorIfails(bg).length - + (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length - const dirty = !rootCausePath - ? 'build-failed 未给 rootCausePath(无法判定根因是否落在本 FE 之外)' - : hardRiders - ? `build-failed 同时携带 ${hardRiders} 项交互/sentinel 硬问题(疑似本 FE 真构建 bug 搭车)` - : null - if (dirty) { - const verdict = await adjudicate(`behavior-buildfailed-dirty:${id}`, - { problem:`build-failed 归因不可信,绝不短路放行:${dirty}(rootCausePath=${rootCausePath || '∅'})`, - envError: bg.envError, allowContinue:false }, grp, behaviorRound) - if (verdict.action !== 'retry') throw new Error(`HALT behavior-buildfailed ${id}: ${verdict.rationale || dirty}`) - continue // retry → 下一 behaviorRound 重跑整门 - } - // 干净的 build-failed(有 rootCausePath 且无硬问题搭车)→ green-by-skip 放行,记低置信证据。 - recordDecisions(`behavior-build-failed:${id}`, [{ - question:`本 FE ${id} 行为验收遇 build-failed(根因 ${rootCausePath})`, - choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)', - rationale: bg.envError.detail || '', confidence:'low' }]) - log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${rootCausePath}),记证据不阻断`) - return - } + const softPassed = new Set() + for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_STAGE_MAX; behaviorRound++) { + const bg = await runBehaviorGateOnce(feItems, behaviorRound) - // 2) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 - // locator-not-resolvable(B 类硬问题反查不出)计入未覆盖——下面会因 behaviorHard 仍非空或覆盖不足而不 green。 + // 1) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 + // locator-not-resolvable / scope-missing 在 §3.5 单独阻断 green。 for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { if (!cg) continue - recordDecisions(`behavior-coverage:${id}`, + recordDecisions('behavior-coverage:frontend-phase', [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }]) } - // 3) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。 - // 永不阻断 approve;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。 + // 2) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。 + // 永不阻断 green;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。 let softRetry = false for (const ti of (Array.isArray(bg.textIssues) ? bg.textIssues : [])) { if (!ti || ti.source === 'sentinel') continue // sentinel 归 behaviorHard,不在软文字处理 if (softPassed.has(regionKey(ti))) continue - const site = `behavior-text:${id}:${ti.page || '?'}:${ti.region || '?'}` + const site = `behavior-text:${ti.page || '?'}:${ti.region || '?'}` const verdict = await adjudicate(site, - { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 approve):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, - textIssue: ti, allowContinue: true }, grp, behaviorRound) + { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 green):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, + textIssue: ti, allowContinue: true }, 'Behavior', behaviorRound) if (verdict.action === 'continue') { recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`, choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }]) @@ -1850,24 +1870,26 @@ async function behaviorSubGate(id, specPath, grp, softPassed) { } if (softRetry) continue - // 3.5) B 类硬问题(locator-not-resolvable coverageGap):连组件文件都反查不出,不静默放行—— - // 计入未覆盖阻断 approve,走 adjudicate(allowContinue:false) retry/halt(绝不当 green 放行,降级≠放行)。 - const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []).filter(cg => cg && cg.reason === 'locator-not-resolvable') + // 3.5) B 类硬问题(locator-not-resolvable:连组件文件都反查不出)+ scope-missing(某 FE 作用域小节缺失, + // 其路由整体漏验):不静默放行——计入未覆盖阻断 green,走 adjudicate(allowContinue:false) retry/halt。 + const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []) + .filter(cg => cg && (cg.reason === 'locator-not-resolvable' || cg.reason === 'scope-missing')) if (bClass.length) { - const summary = bClass.map(cg => `${cg.page} — ${cg.detail}`).join('; ') - const verdict = await adjudicate(`behavior-bclass:${id}`, - { problem:`behavior 硬问题连组件文件都反查不出(B 类,不可降级放行,计入未覆盖阻断 approve):${summary}`, - coverageGaps: bClass, allowContinue:false }, grp, behaviorRound) - if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass ${id}: ${verdict.rationale || summary}`) - continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) + const summary = bClass.map(cg => `[${cg.reason}] ${cg.page} — ${cg.detail}`).join('; ') + const verdict = await adjudicate('behavior-bclass:frontend-phase', + { problem:`behavior 不可降级的未覆盖(B 类反查不出 / FE 作用域缺失,阻断 green):${summary}`, + coverageGaps: bClass, allowContinue:false }, 'Behavior', behaviorRound) + if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass frontend-phase: ${verdict.rationale || summary}`) + continue // retry → 重跑行为验收(下一 behaviorRound) } // 3.6) 覆盖率对账(确定性兜底):空覆盖只兜 ==0;这里兜 0 cg && ROUTE_GAP.has(cg.reason) && typeof cg.page === 'string' && cg.page.trim()) .map(cg => cg.page.trim())) @@ -1875,70 +1897,78 @@ async function behaviorSubGate(id, specPath, grp, softPassed) { const missedRoutes = Math.max(0, planned - reached) const unaccounted = Math.max(0, missedRoutes - routeGapCount) if (planned > 0 && unaccounted > 0) { - const verdict = await adjudicate(`behavior-undercoverage:${id}`, - { problem:`本 FE 路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`, - coverageGaps: bg.coverageGaps || [], allowContinue: false }, grp, behaviorRound) - if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage ${id}: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`) + const verdict = await adjudicate('behavior-undercoverage:frontend-phase', + { problem:`路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条不同路由有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`, + coverageGaps: bg.coverageGaps || [], allowContinue: false }, 'Behavior', behaviorRound) + if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage frontend-phase: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`) continue // retry → 下一 behaviorRound 重跑整门 } - // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。 + // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues + // + styleIssues(颜色 token / layout sanity,全部客观可 fix)。 const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : []) .filter(t => t && t.source === 'sentinel') .map(t => ({ page:t.page, control:t.region, kind:'binding-garbage', detail:`sentinel 不符 期望=${t.expected} 实际=${t.actual}`, locator:t.locator })) - const behaviorHard = [...behaviorIfails(bg), ...sentinelHard] + const styleHard = (Array.isArray(bg.styleIssues) ? bg.styleIssues : []) + .filter(Boolean) + .map(s => ({ page:s.page, control:s.element, kind:`style-${s.kind}`, + detail:`期望=${s.expected} 实际=${s.actual}`, locator:s.locator })) + const behaviorHard = [...behaviorIfails(bg), ...sentinelHard, ...styleHard] const hasEnvSignal = !!(bg.envError && bg.envError.kind && bg.envError.kind !== 'none') const hasAnyClassifiedSignal = hasEnvSignal || behaviorHard.length > 0 || (Array.isArray(bg.textIssues) && bg.textIssues.length > 0) + || (Array.isArray(bg.styleIssues) && bg.styleIssues.length > 0) || (Array.isArray(bg.coverageGaps) && bg.coverageGaps.length > 0) if (bg.status === 'red' && !hasAnyClassifiedSignal) { - const verdict = await adjudicate(`behavior-red-unclassified:${id}`, - { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green', - behaviorResult: bg, allowContinue:false }, grp, behaviorRound) - if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified ${id}: ${verdict.rationale || 'status:red 无分类原因'}`) + const verdict = await adjudicate('behavior-red-unclassified:frontend-phase', + { problem:'behavior 返回 status:red,但没有 envError / interactionFailures / textIssues / styleIssues / coverageGaps 可解释该 red;拒绝把未分类红灯判 green', + behaviorResult: bg, allowContinue:false }, 'Behavior', behaviorRound) + if (verdict.action !== 'retry') throw new Error(`HALT behavior-red-unclassified frontend-phase: ${verdict.rationale || 'status:red 无分类原因'}`) continue } - // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 子门 green 放行。 + // 5) green 判定:behaviorHard 为空 ∧ 无 B 类/scope-missing 未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 门 green 放行。 if (behaviorHard.length === 0) { - log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) + log(`behavior frontend-phase green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) return } - // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不 approve)。 + // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不放行)。 const withLoc = behaviorHard.filter(x => typeof x.locator === 'string' && x.locator.trim()) const noLoc = behaviorHard.filter(x => !(typeof x.locator === 'string' && x.locator.trim())) if (noLoc.length) { const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') - const verdict = await adjudicate(`behavior-noloc-hard:${id}`, - { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue/approve):${summary}`, - interactionFailures: noLoc, allowContinue:false }, grp, behaviorRound) + const verdict = await adjudicate('behavior-noloc-hard:frontend-phase', + { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue 放行):${summary}`, + interactionFailures: noLoc, allowContinue:false }, 'Behavior', behaviorRound) if (verdict.action !== 'retry') - throw new Error(`HALT behavior-noloc-hard ${id}: ${verdict.rationale || summary}`) - continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) + throw new Error(`HALT behavior-noloc-hard frontend-phase: ${verdict.rationale || summary}`) + continue // retry → 重跑行为验收(下一 behaviorRound) } // 7) 有 locator 的硬问题 → 降维成 {summary,locator,severity} 喂现有 fixPrompt 跑 fix(schema 不合并、fix 入参合并)。 + // 一轮 fix 批量修当轮全部 must-fix(跨 FE 也在同一 fix 子会话内逐项修,locator 已含组件文件路径)。 const fixIssues = withLoc.map(f => ({ summary: `[behavior:${f.kind}] ${f.page}:${f.control} — ${f.detail}`, locator: f.locator, severity: 'high', })) - await runStage(g => fixPrompt(id, 'frontend', fixIssues) + g, { - site:`behavior-fix:${id}:r${behaviorRound}`, grp, label:`behavior-fix:${id}:r${behaviorRound}`, allowContinue: true, + await runStage(g => fixPrompt('frontend-phase', 'frontend', fixIssues) + g, { + site:`behavior-fix:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-fix:r${behaviorRound}`, allowContinue: true, }) - // 8) fix 后功能复验(allowContinue:false):behaviorSubGate 的 fix 改的是 frontend/ UI 源码,可能引入功能回归—— - // 先跑 scoped 组件测试 reverify(不起全栈,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。 + // 8) fix 后功能复验(allowContinue:false):行为 fix 改的是 frontend/ UI 源码,可能引入功能回归—— + // 先跑全量前端单测(不起全栈、不跑 e2e,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。 + // (e2e 维度由下一轮行为门 + 后续阶段 testGate 全量回归兜底。) await runStage( - g => verifyPrompt(id, 'frontend', `(behaviorRound ${behaviorRound} 行为 fix 后功能复验,本轮 must-fix: ${fixIssues.length} 项)`, specPath, REVIEW_HARD_ROUNDS + behaviorRound) + g, - { site:`behavior-reverify:${id}:r${behaviorRound}`, grp, label:`behavior-reverify:${id}:r${behaviorRound}`, allowContinue: false }, + g => behaviorReverifyPrompt(behaviorRound, fixIssues.length) + g, + { site:`behavior-reverify:frontend-phase:r${behaviorRound}`, grp:'Behavior', label:`behavior-reverify:r${behaviorRound}`, allowContinue: false }, ) - // 进入下一 behaviorRound → 重跑本 FE 行为验收 + // 进入下一 behaviorRound → 重跑行为验收 } - throw new Error(`HALT behavior-unresolved ${id}: ${BEHAVIOR_FE_MAX} 轮 per-FE 行为子门仍未 green(硬问题未清)`) + throw new Error(`HALT behavior-unresolved frontend-phase: ${BEHAVIOR_STAGE_MAX} 轮阶段级行为门仍未 green(硬问题未清)`) } phase('Router') @@ -2006,13 +2036,17 @@ for (const [idx, module] of todo.entries()) { phase('Frontend') // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, - // 使 per-FE 行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 + // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 await runFrontendSkeleton(module.feItems) - // 前端行为验收已并入 featureLoop→reviewWithFixLoop 的 per-FE approve 子门(reviewer approve 时起本 FE 全栈验 - // 「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才打 req-done)——不再有阶段级末尾独立行为门。 + // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。 await featureLoop(module.feItems, 'frontend') + // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 + // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 + // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 + phase('Behavior') + await runBehaviorGate(module.feItems) phase('Gate') - await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright)保留,与 per-FE 行为验收职责正交 + await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交 } phase('Milestone') // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"——