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