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