Commit 0588d0dcc496863a40c1de4916d6b0bc07dcc3f4
1 parent
3d54e883
coding.mjs: move frontend behavior verification INTO per-FE reviewWithFixLoop (fixable dimension)
Replaces the phase-level read-only behavior-gate with a per-FE acceptance dimension: each FE is approved only when the code-reviewer approves AND runtime behavior verification is green. Behavior defects (dead control / sentinel text mismatch) become fixable must-fix that drive verify->fix->re-verify, not halts. - reviewWithFixLoop (frontend only, via if(fe)): at the approve gate, behaviorSubGate boots this FE's full stack + seeds sentinels, enumerates this FE's routes, two-tier asserts. Hard issues with a locator -> fixPrompt -> functional reverify -> next behaviorRound; soft text (i18n/literal/semantic) -> adjudicate(continue); behaviorRound bounded by BEHAVIOR_FE_MAX=3, env race by BEHAVIOR_ATTEMPT_MAX=2. Backend featureLoop branch unchanged. - New runFrontendSkeleton stage (before featureLoop(frontend)): App shell + full lazy router + FeStub placeholders + shared nav, so the app is buildable at every mid-phase point; tdd swaps FeStub->real component per FE. Idempotent via fe-skeleton-done tag. - BEHAVIOR_GATE_SCHEMA gains build-failed envError kind (sibling-FE-unimpl short-circuit, not a bug) + locator-not-resolvable coverage reason; deriveSpec emits a per-FE route-scope section, reviewer validates it. - Removed phase-level runBehaviorGate + 'Behavior' phase; kept phase-level testGate (regression). REVIEW_HARD_ROUNDS 8->10. - Safety: test-DB naming guard pushed into scripts-setup-test-db.mjs template (fail-closed unless name contains test/_dev/_local or ALLOW_NONTEST_DROP=1) + 3 tests. - agentType stays erp-workflow:code-reviewer. v1 design doc marked SUPERSEDED; v2 design at docs/design/2026-06-02-frontend-behavior-in-review-loop.md. Verified: wrapped syntax check SYNTAX_OK, 87/87 lib tests pass, no orphan refs, no time/random builtins, top-level return intact. Not yet run end-to-end against a real ERP project.
Showing
7 changed files
with
739 additions
and
194 deletions
README.md
| ... | ... | @@ -43,9 +43,13 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 |
| 43 | 43 | │ 10+ 微 agent,全部跳过/分支条件由 JS 判定,幂等) |
| 44 | 44 | │ |
| 45 | 45 | └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) |
| 46 | - runBranchSetup(frontend-phase) → featureLoop(前端,FE-NN,路径限 frontend/) | |
| 47 | - → testGate(frontend) → 前端行为闸 behavior-gate(headless 全栈起栈+种子 sentinel, | |
| 48 | - 逐路由枚举控件/文字两层断言:交互失效硬 halt,文字不符按来源仲裁) | |
| 46 | + runBranchSetup(frontend-phase) | |
| 47 | + → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起) | |
| 48 | + → featureLoop(前端,FE-NN,路径限 frontend/):spec → plan → tdd → verify → | |
| 49 | + review 循环内并入 per-FE 行为验收 approve 子门(reviewer approve 时才起本 FE 全栈 | |
| 50 | + +种子 sentinel,枚举本 FE 路由控件/文字两层断言;交互失效/sentinel 错转可 fix | |
| 51 | + must-fix→重验,软文字按来源仲裁,行为 green 才打 req-done/<FE>) | |
| 52 | + → testGate(frontend,全量回归 vitest+playwright,与 per-FE 行为验收职责正交) | |
| 49 | 53 | → runMilestone(milestone/frontend-phase) |
| 50 | 54 | |
| 51 | 55 | 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start | ... | ... |
docs/design/2026-06-02-frontend-behavior-gate.md
| 1 | 1 | # 前端行为门(behavior-gate)— 最终设计(综合评审后) |
| 2 | 2 | |
| 3 | +> ⚠️ **已作废(SUPERSEDED)** —— 本文描述的是**阶段级、只读、red 即 halt** 的行为门(frontend-phase 末尾跑一次)。 | |
| 4 | +> 该设计已被 **per-FE 版**取代:行为验收并入每个 FE 的 `reviewWithFixLoop`、成为可 fix 的验收维度(verify→fix→重验循环), | |
| 5 | +> 并新增前端骨架占位阶段(`runFrontendSkeleton` + `FeStub` 全量 lazy 路由)保证中途可构建。 | |
| 6 | +> **现行设计见 [`2026-06-02-frontend-behavior-in-review-loop.md`](./2026-06-02-frontend-behavior-in-review-loop.md)**;本文仅作历史保留,勿据此实现。 | |
| 7 | + | |
| 3 | 8 | > 本文是 5 维对抗式评审后的收敛版。锁定决策(用户拍板)默认保留;评审给出的有依据调整已并入; |
| 4 | 9 | > 无依据 / 过度工程的建议在文末「拒绝的建议」记明理由。所有改动可追溯到 changeLog。 |
| 5 | 10 | ... | ... |
docs/design/2026-06-02-frontend-behavior-in-review-loop.md
0 → 100644
| 1 | +# 前端行为验收并入 reviewWithFixLoop(v2 最终设计:per-FE + fix 循环) | |
| 2 | + | |
| 3 | +> 状态:可实现(ready-to-implement),含 3 项实现前置依赖。 | |
| 4 | +> 上游:本设计取代 `docs/design/2026-06-02-frontend-behavior-gate.md` 的「阶段级末尾只读 halt 门」形态。 | |
| 5 | +> 运行时红线(不可违反):禁用 time/random builtin(`Date.now()` / `Math.random()` / `new Date()`);顶层 `return` 是结果通道;`agent/phase/parallel/log/adjudicate/recordDecisions` 是注入全局;**后端 featureLoop 分支逐字不变**。 | |
| 6 | + | |
| 7 | +--- | |
| 8 | + | |
| 9 | +## 0. 用户拍板的方向(不可推翻) | |
| 10 | + | |
| 11 | +- 行为验收**并入 per-FE reviewWithFixLoop**,与静态 code-reviewer 并列为另一个验收维度。 | |
| 12 | +- 行为发现的硬问题**可 fix**(带 locator 的 must-fix),驱动 fix→重验循环,不再 halt。 | |
| 13 | +- **仅前端 FE** 有此维度;后端 REQ 分支(无 UI)逐字不变。 | |
| 14 | +- 接受**每个 FE 起一次(或少数几次)全栈**的代价。 | |
| 15 | + | |
| 16 | +本设计在守住上述方向的前提下,落实了 5 维评审里 **确凿的 blocker**:中途可构建性(头号)、起栈成本笛卡尔积爆炸、起栈不可跨子会话复用、locator 不可靠降级=放行、缺 locator 硬问题被现有 filter 静默吞、删阶段门后 report 失去绿前置锚点、测试库护栏只在 LLM 层。 | |
| 17 | + | |
| 18 | +--- | |
| 19 | + | |
| 20 | +## 1. 关键架构决策:行为验收是 reviewer-approve 的「approve 子门」,不是每轮都起栈 | |
| 21 | + | |
| 22 | +这是本设计相对「种子设计」最重要的调整,一次性化解 3 个 blocker(成本笛卡尔积 / 不可复用栈 / 中途构建脆弱性被乘以 N×round)。 | |
| 23 | + | |
| 24 | +**原种子设计**:每个 review round 的 step2 跑一次行为验收、fix 后 step5 再跑一次 → 单 FE 最坏 `REVIEW_HARD_ROUNDS(10) × 2 = 20` 次全栈起栈,N 个 FE 串行 → N×20,墙钟在最坏路径不收敛。 | |
| 25 | + | |
| 26 | +**v2 决策**:把行为验收从「每个 review round 内」**解耦**为「**reviewer 即将 approve 时才触发的 approve 前置子门**」: | |
| 27 | + | |
| 28 | +``` | |
| 29 | +reviewWithFixLoop(FE): | |
| 30 | + ┌─ 静态 review→fix 循环(与现状几乎不变;后端逐字不变) | |
| 31 | + │ round 1..N: reviewer 判 → request-changes 则 filter locator must-fix → fix → reverify(功能测试) → 再 review | |
| 32 | + │ | |
| 33 | + └─ 当某轮 reviewer 判 approve(现 1386 分支)→ 不立即 return,先进【行为 approve 子门】: | |
| 34 | + behaviorSubGate(FE): | |
| 35 | + for behaviorRound = 1..BEHAVIOR_FE_MAX(=3): | |
| 36 | + 跑一次 per-FE 行为验收(runBehaviorGateOnce,内含 envError attempt 重试) | |
| 37 | + ├─ envError / 空覆盖 → 内部 attempt 重试;确定性失败(build-failed) → 记 coverageGap 短路,不 retry 不 halt | |
| 38 | + ├─ behaviorHard(interactionFailures + sentinel textIssues)为空 → 子门 green → break | |
| 39 | + └─ behaviorHard 非空: | |
| 40 | + · 有 locator 的 → 合并进 fixPrompt 的 issues,跑 fix(runStage) | |
| 41 | + · 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions / retry / halt),永不阻断 approve | |
| 42 | + · 无 locator 的 behaviorHard → adjudicate(allowContinue:false)(retry 重判/重跑 或 halt),绝不静默丢弃、绝不 approve | |
| 43 | + fix 后只重跑「本 FE 行为验收」(不必重跑功能 reverify,除非 fix 也动了功能逻辑——见 §6) | |
| 44 | + 子门 BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved | |
| 45 | + → 静态 approve ∧ 行为 green ⇒ 才 flipDocs08Checkbox + return{approved:true} | |
| 46 | + → featureLoop:1344 在 return 后打 req-done(落点不动) | |
| 47 | +``` | |
| 48 | + | |
| 49 | +**收益**: | |
| 50 | + | |
| 51 | +- 每 FE 行为起栈次数从 `O(review rounds)` 降到 `O(行为 fix 轮)`,典型 **1 次**(一次过)到 **最多 BEHAVIOR_FE_MAX=3 次**。 | |
| 52 | +- 静态 must-fix 反复震荡阶段**不起全栈**——只有静态已 clean、reviewer 认可后才付全栈代价,与用户「接受每 FE 起一次全栈」精确对齐(典型就是一次)。 | |
| 53 | +- 中途构建脆弱性不再被 `N×round` 放大,只在每 FE 的 approve 时刻面对一次(仍需 §2 骨架占位保证可构建)。 | |
| 54 | + | |
| 55 | +> **为何不冲突用户决策**:用户要的是「行为验收是 reviewWithFixLoop 内的另一个验收维度、行为问题可 fix、有 fix→重验循环」。本设计完全满足:行为验收在 reviewWithFixLoop 函数内、是 approve 的合取前置、行为硬问题转 must-fix 喂 fix、fix 后重跑行为验收循环。它只是把「行为验收的触发时机」定在「reviewer approve 那一刻」而非「每个 round 顶」——这是控制流优化,不改变验收维度的存在与可 fix 性。 | |
| 56 | + | |
| 57 | +--- | |
| 58 | + | |
| 59 | +## 2. 实现前置依赖 A(blocker):前端骨架全量路由占位阶段——保证任意时刻 app 可构建可起 | |
| 60 | + | |
| 61 | +**问题(已核实)**:`skeleton-gen` 只生成 docs/04 / scripts/*.mjs / tokens.css / .gitignore,**不生成任何 frontend/ 源码、router、路由占位**。frontend/ 全部源码由 coding 期 featureLoop 逐 FE 增量产出。验 FE-N 时 FE-N+1..M 的组件文件不存在 → eager import 路由表编译失败 / 共享 nav 指向未建路由 → 整 SPA 起不来,连 FE-N 页面都加载不出。per-FE 行为验收前移到「前端只建了一部分」的每一轮,直接踩头号风险。 | |
| 62 | + | |
| 63 | +**v2 方案(必做,不是候选)**:在 `featureLoop(frontend)` **之前**新增一个 coding 期 stage `runFrontendSkeleton(module)`,由独立子代理依据 `docs/08 §三` FE 清单 + `frontend/` router 约定一次性生成: | |
| 64 | + | |
| 65 | +1. **App 外壳**(`frontend/src/App.*` + 入口 `main.*`,若不存在)。 | |
| 66 | +2. **router 全量路由表**:每个 FE-NN 对应路由都声明,且**全部 lazy import**(`() => import(...)`)。未实现的 FE 路由指向一个最小占位组件 `FeStub`(如 `frontend/src/views/_stub/FeStub.vue`,渲染 `<div data-fe-stub="FE-NN">FE-NN 占位</div>`)。 | |
| 67 | +3. **共享布局/导航**:导航链接全部指向已声明的路由 path(不指向不存在的 path),保证任意时刻无悬空链接。 | |
| 68 | + | |
| 69 | +落点与时序: | |
| 70 | +- 在顶层循环 `if (module.feItems.length)` 段、`phase('Frontend')` 之后、`featureLoop` 之前调用 `await runFrontendSkeleton(module)`。 | |
| 71 | +- **幂等**:以 git tag `fe-skeleton-done` 或检测 router 文件存在 + 全 FE 路由已声明为 ground truth;已建则 skip(resume 安全)。子代理产出后自行 commit(沿用 commitBlock 习惯)。 | |
| 72 | +- FE-N 实现时(tddPrompt),把对应路由的占位 import 替换为真组件——这要求 **tddPrompt 前端分支补一句**:「若 router 中本 FE 路由仍指向 `FeStub`,实现完成后把该路由 import 改为本 FE 真组件」(属 frontend/ 路径内,不破坏护栏)。 | |
| 73 | + | |
| 74 | +> **为何这是根因解**:让 router 始终 lazy + 占位齐全 → 任意时刻 `vite build` / dev server 可起、每个 FE 路由可达 → 把「中途起不来」从高频降为罕见 → per-FE 行为验收的 flake/误判面收敛、`build-failed`(依赖 B)成为罕见兜底而非常态。 | |
| 75 | + | |
| 76 | +> **Plan 期 vs coding 期**:放在 coding 期(而非改 skeleton-gen)的理由——FE 清单在 Plan 末尾才稳定、且骨架要落 frontend/ 源码属 coding 范畴;放 coding 期不触碰已锁定的 Plan 闸门,且能用 git tag 幂等。这是新增一个 Plan/coding 边界 stage 的代价,已被本设计接受并显式声明。 | |
| 77 | + | |
| 78 | +--- | |
| 79 | + | |
| 80 | +## 3. 实现前置依赖 B(blocker):BEHAVIOR_GATE_SCHEMA 增 `build-failed` kind + 确定性短路控制流 | |
| 81 | + | |
| 82 | +**问题**:中途构建失败是**确定性编译错误**(缺组件 / import 解析失败 / 类型错),但 `envError.kind` 枚举只有 `port-conflict/stack-not-ready/seed-error/auth-failed/timeout/none`。门面对 `Cannot find module ./views/FE-07.vue` 只能误归类:归 `stack-not-ready` → retry 空转到耗尽后 HALT(编译错永不自愈);归 `interactionFailures` → 假 must-fix 污染源码。两条路都失败。 | |
| 83 | + | |
| 84 | +**v2 方案**: | |
| 85 | + | |
| 86 | +1. `BEHAVIOR_GATE_SCHEMA.envError.kind` 枚举**新增 `build-failed`**(确定性失败语义;`route-not-buildable` 不单列,统一用 `build-failed` + detail 区分)。 | |
| 87 | +2. **控制流**(在 per-FE 行为门 helper 内):`build-failed` **既不 retry 也不 halt**——记 `coverageGap`(reason 新增枚举 `build-failed-sibling-unimpl`)+ recordDecisions,**本轮行为门视为「本 FE 行为维度无法判定但非本 FE 缺陷」直接放行 approve**(因为它是「后续 FE 未实现」的预期中途态,不是 FE-N 的 bug;§2 骨架占位让这种情况罕见,一旦发生说明占位未覆盖,留证据供人工)。 | |
| 88 | +3. `behaviorGatePrompt`(per-FE 版)step0/step2 **明确归因指令**:先 `build` / 起 dev server;若失败,先用 `git` / `Grep` 判断报错根因文件路径—— | |
| 89 | + - 落在**非本 FE 的 frontend/ 路径**(兄弟 FE / 占位未覆盖)→ 判 `envError.kind="build-failed"`(预期中途态)。 | |
| 90 | + - 落在**本 FE 路径** → 才可能是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]` 或带 locator must-fix。 | |
| 91 | + | |
| 92 | +> 没有这层「确定性失败短路 + 根因归属」,per-FE 行为门无法落地——这是把行为门从「全 FE 已建的安全环境」迁到「部分 FE 已建的敌对环境」的必须保障。 | |
| 93 | + | |
| 94 | +--- | |
| 95 | + | |
| 96 | +## 4. 实现前置依赖 C(blocker):FE-NN → 路由 path 确定性映射,锁进 spec 产物 | |
| 97 | + | |
| 98 | +**问题**:per-FE 只验「本 FE 关联路由」,但 FE→路由关系当前只在 spec 顶部「关联原型」散文 + 子代理对 router 的 Grep 推断,**无结构化真值**。推窄→漏验(假绿);推宽→把别的未实现 FE 的死控件算到本 FE(误 must-fix)。且若 router 全量声明(依赖 A),`routesPlanned = router 全部路由` 会让覆盖率分母被未建路由污染。 | |
| 99 | + | |
| 100 | +**v2 方案**: | |
| 101 | + | |
| 102 | +1. **deriveSpecPrompt 前端分支**强制在 spec 头部产出结构化小节(不只是散文): | |
| 103 | + ``` | |
| 104 | + ## 行为验收作用域(per-FE 行为门唯一断言依据) | |
| 105 | + - 关联路由: [/orders, /orders/:id] | |
| 106 | + - 负责控件白名单: [data-testid 约定 或 page+DOM 选择器清单] | |
| 107 | + ``` | |
| 108 | + 并要求 fe-feature-review(code-reviewer)校验该小节存在且与 router 配置一致(缺失 / 不一致 → request-changes)。 | |
| 109 | +2. **behaviorGatePrompt 改为接收「本 FE 路由清单 + 控件白名单」入参**(per-FE 版必需): | |
| 110 | + - `routesPlanned` **只数本 FE 关联路由**(不是 router 全部),未建兄弟路由既不计入分母也不计 coverageGap。 | |
| 111 | + - 行为门只对白名单内控件判 must-fix;白名单外控件 / 共享控件若属其它未 approve FE → 归 `coverageGap`(reason `deep-control-not-driven` 或 `build-failed-sibling-unimpl`),**绝不**归本 FE 的 interactionFailure。 | |
| 112 | +3. **空覆盖兜底保留**:`routesReached==0 || controlsEnumerated==0`(针对本 FE 路由子集)仍归 envBlocked,绝不静默判 green。 | |
| 113 | + | |
| 114 | +> 没有这个确定性映射,per-FE 路由作用域无法界定,覆盖率与归因全失真。 | |
| 115 | + | |
| 116 | +--- | |
| 117 | + | |
| 118 | +## 5. 实现前置依赖 D(blocker / 安全):测试库护栏下沉到 setup-test-db.mjs 模板自身 | |
| 119 | + | |
| 120 | +**问题(已核实)**:`scripts-setup-test-db-template.mjs` 只校验 schema 是合法标识符(`/^[A-Za-z0-9_$]+$/`),**不判它是不是测试库**,DROP+CREATE 无条件执行。测试库命名护栏(库名须含 `test/_test/_dev/_local`)当前只在**门子代理生成的 runner 内**(LLM 级 prompt 检查)。per-FE × per-behaviorRound 反复 DROP+CREATE,任一轮子代理漏写护栏 → 对 config-vars 指向的库(可能=开发库)无条件 DROP,反复次数越多撞上漏写的概率越高。真实数据销毁风险。 | |
| 121 | + | |
| 122 | +**v2 方案**:把测试库命名护栏**下沉到 `setup-test-db.mjs` 模板**(确定性 JS 边界,不依赖每个子代理记得复述):在现有标识符校验后追加—— | |
| 123 | + | |
| 124 | +```js | |
| 125 | +// 测试库命名护栏:DROP+CREATE 只允许作用于明确的测试/本地库,防误删开发/生产库。 | |
| 126 | +const ALLOW = process.env.ALLOW_NONTEST_DROP === '1' | |
| 127 | +if (!ALLOW && !/(^|_)(test|dev|local)$|(^|_)test_|^test_/.test(DB_SCHEMA) && !/test|_dev|_local/.test(DB_SCHEMA)) { | |
| 128 | + console.error(`[setup-test-db] 拒绝:schema=${JSON.stringify(DB_SCHEMA)} 不像测试库(须含 test/_test/_dev/_local),设 ALLOW_NONTEST_DROP=1 显式放行`) | |
| 129 | + process.exit(1) | |
| 130 | +} | |
| 131 | +``` | |
| 132 | +(具体正则以实现为准,语义=库名须含 `test`/`_test`/`_dev`/`_local` 之一,否则 fail-closed。) | |
| 133 | + | |
| 134 | +- 这样不论被行为门调用多少次都安全。 | |
| 135 | +- coding.mjs 行为门控制流里,对「测试库护栏触发的红」**保持不重试不仲裁直接 throw** 的硬边界语义(与现 v1 一致,见 behaviorGatePrompt step2 第 1 条)。 | |
| 136 | +- 这是 skeleton-gen 模板的一次性改动,**不属于 coding.mjs 改造**,但列为本设计前置(否则反复起栈的安全暴露面不可接受)。 | |
| 137 | + | |
| 138 | +--- | |
| 139 | + | |
| 140 | +## 6. reviewWithFixLoop 改造后的逐轮控制流(实现级) | |
| 141 | + | |
| 142 | +**仅 `phase==frontend` 改造;`fe=isFrontend(phase)` 现已存在(1373),后端分支逐字不变。** | |
| 143 | + | |
| 144 | +### 6.1 数据流:两类 must-fix 独立来源,schema 不合并、fix 入参合并 | |
| 145 | + | |
| 146 | +- **review-must-fix**:reviewer 的 `REVIEW_SCHEMA.issues`,照现状 1392 的 locator filter(缺 locator 降级丢弃)。 | |
| 147 | +- **behavior-hard**:行为门返回的 `BEHAVIOR_GATE_SCHEMA.interactionFailures` + `source=='sentinel'` 的 `textIssues`。 | |
| 148 | +- **schema 不杂交**(采纳「schema 选型」维度的定调):行为验收**保留独立 `BEHAVIOR_GATE_SCHEMA` 返回**,不压扁进 `REVIEW_SCHEMA.issues`(否则丢失 envError / 空覆盖 / coverageGaps / source 软硬分流这三个赖以正确的区分)。**仅在喂 fix 时**把「有 locator 的 behavior-hard」降维成 `{summary, locator, severity}` 喂现有 `fixPrompt`(fix 步天然吃这形状)。即:**schema 不合并,fix 入参合并**。 | |
| 149 | + | |
| 150 | +### 6.2 approve 闸(显式 AND,钉死落点) | |
| 151 | + | |
| 152 | +approve 出口(现 1386 `if (r.verdict==='approve')` 分支)**改为合取**: | |
| 153 | + | |
| 154 | +``` | |
| 155 | +reviewer.verdict==='approve' | |
| 156 | + ∧ behaviorSubGate(FE) 返回 green | |
| 157 | + 其中 green ≡ behaviorHard.length===0 ∧ envError∈{none, build-failed} ∧ 本FE覆盖非空(或 build-failed 短路) | |
| 158 | +``` | |
| 159 | + | |
| 160 | +只有合取成立才 `flipDocs08Checkbox` + `return {approved:true}`。这保证(采纳「删阶段门」维度 blocker 的钉死): | |
| 161 | +- **行为 green 是 reviewWithFixLoop 的 return 前置条件**——req-done tag 落点(featureLoop:1344)**保持不动**,语义自动升级为「静态过+行为过」。 | |
| 162 | +- **无 locator 的 behavior-hard 绝不 approve**(采纳「控制流/schema」维度 blocker#1):走 adjudicate(allowContinue:false) 决定 retry(重跑行为验收/重判)还是 halt,绝不被 1392 的 locator filter 静默吞掉。 | |
| 163 | +- flipDocs08Checkbox 翻转自动晚于行为 green(checkbox 纯装饰、resume 只认 req-done tag,无视觉误导)。 | |
| 164 | + | |
| 165 | +### 6.3 behaviorSubGate(approve 子门)逐步 | |
| 166 | + | |
| 167 | +``` | |
| 168 | +async function behaviorSubGate(id, specPath, feScope): | |
| 169 | + // feScope = {routes:[...], controlWhitelist:[...]}(来自 §4 spec 结构化小节) | |
| 170 | + for behaviorRound in 1..BEHAVIOR_FE_MAX(=3): | |
| 171 | + bg = await runBehaviorGateOnce(id, behaviorRound, feScope) // 见 §7,内含 envError attempt 重试 | |
| 172 | + // 1) build-failed 短路(依赖 B):兄弟未实现 → 记 coverageGap + decisions,子门视为 green-by-skip,return passed | |
| 173 | + if (bg.envError.kind === 'build-failed' && 根因在非本FE路径) { recordDecisions; return {green:true, skipped:true} } | |
| 174 | + // 2) envError(其它) / 空覆盖:runBehaviorGateOnce 内部已 attempt 重试;到这里仍 blocked → adjudicate(allowContinue:false) retry/halt | |
| 175 | + if (envBlocked(bg)) { adjudicate; 仍 blocked → throw HALT } | |
| 176 | + // 3) 软文字:for-of 走 adjudicate;continue→recordDecisions + 加入跨轮 softPassed;sentinel→并入 behaviorHard;retry/halt 同现 | |
| 177 | + processTextIssues(bg, softPassed) // softPassed 提升到 reviewWithFixLoop 顶层作用域,跨 behaviorRound 持久 | |
| 178 | + // 4) behaviorHard = interactionFailures + sentinel textIssues | |
| 179 | + if (behaviorHard.length === 0) return {green:true} | |
| 180 | + // 5) 分流 | |
| 181 | + const withLoc = behaviorHard.filter(有 locator) | |
| 182 | + const noLoc = behaviorHard.filter(无 locator) | |
| 183 | + if (noLoc.length) { v=adjudicate(allowContinue:false); v!=='retry' → throw HALT; else 下一轮重跑 } | |
| 184 | + if (withLoc.length) { | |
| 185 | + await runStage(fixPrompt(id, 'frontend', withLoc降维)) // 复用现有 fix 步 | |
| 186 | + // fix 后:只重跑本 FE 行为验收(下一轮 behaviorRound);若 fix 同时改了功能逻辑,附带重跑功能 verify(见下) | |
| 187 | + } | |
| 188 | + throw HALT behavior-unresolved(BEHAVIOR_FE_MAX 轮仍未 green) | |
| 189 | +``` | |
| 190 | + | |
| 191 | +- **softPassed Set 提升到 reviewWithFixLoop 顶层作用域**(与 round 同寿命,跨 behaviorRound 持久)——直接照搬现 runBehaviorGate 的 softPassed 语义,否则文字层每轮重新消耗仲裁预算撞 ADJUDICATE_MAX。 | |
| 192 | +- **行为软文字永不进 approve 闸**,只 recordDecisions(采纳「控制流/schema」维度 high#3)。 | |
| 193 | +- **fix 后的功能复验**:behaviorSubGate 内的 fix 改的是 frontend/ UI 源码,可能引入功能回归。策略——fix 后先跑一次现有 `verifyPrompt` 功能 reverify(allowContinue:false,复用 runStage),红则当功能回归(与现 reverify 同级硬边界),绿后再重跑行为验收。这把「fix 引入功能回归」纳入兜底,且功能 reverify 是 scoped 组件测试(不起全栈),成本低。 | |
| 194 | + | |
| 195 | +### 6.4 轮次预算与计数(二维,钉死防证据覆盖) | |
| 196 | + | |
| 197 | +- 静态 review/fix 仍用 `REVIEW_SOFT_ROUNDS=5` / `REVIEW_HARD_ROUNDS=10`(现状不变)。 | |
| 198 | +- 行为子门独立、更小预算:**新增 `BEHAVIOR_FE_MAX=3`**(每 FE 行为 fix 轮硬上限;超限 throw HALT)。**不**复用 review 的 10 轮驱动起栈,**不**让 `REVIEW_HARD_ROUNDS × BEHAVIOR_GATE_PASS_MAX` 隐式相乘到 120 量级。 | |
| 199 | +- **runBehaviorGateOnce 内部**的 envError attempt 重试用独立小预算(沿用 testGate 的 attempt 1→2 + ADJUDICATE_MAX 思路;§7)。 | |
| 200 | +- **二维计数表**(采纳「控制流/schema」维度 medium#5): | |
| 201 | + - `behaviorRound`:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX)。 | |
| 202 | + - `attempt`:单次 runBehaviorGateOnce 内的环境 race 重试序号。 | |
| 203 | + - 证据文件名用复合编号:`<date>-<FE>-behavior-r<behaviorRound>-a<attempt>.md`,每次起栈独立证据不互相覆盖、不丢 flake 信号。 | |
| 204 | +- **单 FE 行为起栈次数硬上界** = `BEHAVIOR_FE_MAX(3) × 每轮 attempt 上限(≤2 + ADJUDICATE_MAX 内)`,量级远小于种子设计的 10×12。典型一次过 = 1 次起栈。 | |
| 205 | + | |
| 206 | +### 6.5 contract 严格分离(采纳「控制流/schema」维度 medium#7) | |
| 207 | + | |
| 208 | +同一 FE 循环内不同 stage 各自 contract,**绝不混用**: | |
| 209 | +- fix / review / verify stage:套 `featureStageContract('frontend')`(硬护栏:命中 backend//sql//scripts/ 即越界硬停)。 | |
| 210 | +- 行为验收 stage:**仍独立套 `behaviorGateContract()`**(作用域例外:允许运行 setup-test-db / 起后端 / 跑 sql 种子 / 跑 playwright;唯一可写 `.tmp/behavior-gate/<FE>/...` + 证据)。 | |
| 211 | +- behaviorGateContract **新增一条中途态豁免**(采纳头号维度 low#6):「本门在 per-FE 模式下运行,frontend/ 中本 FE 之外的路由/组件可能尚未实现属预期;遇到指向未建路由的链接/404/编译缺件,一律记 coverageGap 或 envError.kind=build-failed,绝不归为本 FE 的 interactionFailure。本 FE 路由清单是唯一断言作用域。」 | |
| 212 | + | |
| 213 | +--- | |
| 214 | + | |
| 215 | +## 7. per-FE 行为验收子代理(runBehaviorGateOnce + behaviorGatePrompt per-FE 版)要点 | |
| 216 | + | |
| 217 | +**runBehaviorGateOnce(id, behaviorRound, feScope)**:保留现 runBehaviorGate 的失败分层(不推倒重写——采纳「删阶段门」维度 high#4),但 scope 缩到单 FE: | |
| 218 | + | |
| 219 | +- **复用现 enforceEnv 思路**做内部 envError attempt 重试 + 空覆盖兜底(attempt 1→2,仍异常经 adjudicate(allowContinue:false) retry/halt)。 | |
| 220 | +- 返回 `BEHAVIOR_GATE_SCHEMA`(含本 FE scope);把「interaction/sentinel 仍非空」作为「本轮未过」返回给 behaviorSubGate 外层。 | |
| 221 | +- **不在 runBehaviorGateOnce 内嵌 BEHAVIOR_GATE_PASS_MAX 的多次 rerun 收敛**(那是阶段级单次门的设计);交互/文字层 retry 限制为每 behaviorRound 至多 1 次重起,收敛靠外层 behaviorSubGate 推进(采纳成本维度 medium#6)。 | |
| 222 | + | |
| 223 | +**behaviorGatePrompt per-FE 版**(由整 app 阶段级改造): | |
| 224 | + | |
| 225 | +- `id` 入参从写死 `frontend-phase` 改为本 FE id;新增入参 `specPath` / `behaviorRound` / `attempt` / `feScope`。 | |
| 226 | +- **起栈**:runner 自起后端+前端(项目无既有 e2e webServer/playwright.config——已核实 F1,**删除「复用既有 webServer」这条死路暗示**,避免实现者照已证伪的假设做;只走「冷起栈」,**明确写死 round 间不复用运行栈、无 HMR**,这是现运行时硬约束,采纳成本维度 blocker#2)。 | |
| 227 | +- **四段时序不变**:空库→起后端等 Flyway 建 schema+健康就绪→sentinel 种子(FK 有序)→起前端 headless。测试库护栏现由模板兜底(§5),runner 仍可复述但不再是唯一防线。 | |
| 228 | +- **step0/step2 build 归因**(依赖 B):先 build / 起 dev server,失败按根因路径归 `build-failed`(非本 FE) 或本 FE 真 bug。 | |
| 229 | +- **step1 路由真值**:`routesPlanned` 只数 `feScope.routes`(本 FE 路由),不数 router 全部(依赖 C)。 | |
| 230 | +- **枚举**:只驱动 `feScope.routes` + `feScope.controlWhitelist`;非白名单 / 共享未 approve FE 控件 → coverageGap,不归本 FE。 | |
| 231 | +- **行为硬问题带源码 locator**(采纳 locator 维度 blocker#1 的拆分): | |
| 232 | + - A 类(可经 route→router 配置→view 组件文件反查到**组件级文件路径**):locator = 「组件文件 + DOM 选择器 + 失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值」。**fixPrompt 放宽**:locator 允许「文件 + DOM 描述」而非强制 file:line,由 fix 子代理在该组件内 Grep 定位 handler/绑定。 | |
| 233 | + - B 类(连组件文件都反查不出):**不静默降级为放行**——归 coverageGap 并**计入未覆盖**,使 behaviorSubGate 不能判 green(降级≠放行)。或归 envError(stack-not-ready) 走 retry。 | |
| 234 | + - **起栈强制 dev/source-map 模式**,runner 注入定位辅助(`data-testid` 约定 / Vue `__file`),把 page+selector 映射到组件文件作为契约前置。 | |
| 235 | +- **binding-garbage / sentinel-mismatch**:locator 除组件文件外,附带 DOM 路径 + 绑定文本片段 + 期望 sentinel + 实际渲染值(写进 summary,不依赖 file:line),供 fix 在组件内 Grep 该绑定表达式。 | |
| 236 | +- **临时件隔离 per-FE×per-behaviorRound**:`.tmp/behavior-gate/<FE>/r<behaviorRound>/`(采纳「删阶段门」维度 medium#5);每轮跑前清空本子目录,runner `finally` 必须 kill 本 FE 起的全部子进程并按本 FE 端口集回收;FE 间起栈端口先探测占用 + 动态回退。每次 coding-start 首个 FE 行为验收前清一次 `.tmp/behavior-gate/` 整目录入口(去跨 resume 串味)。 | |
| 237 | +- **证据**:`docs/superpowers/reviews/<date>-<FE>-behavior-r<behaviorRound>-a<attempt>.md`(与 review 报告同目录);截图归档到版本管理的 assets。 | |
| 238 | +- **确定性端口/pid 回收前置**(采纳安全维度 high#3):起栈前先按既知端口 + `.tmp/*.pid` 强制回收上一 attempt 残留(编排层 + runner 双保险),对反复 port-conflict 设独立硬上限直接 halt 提示人工清理,避免连环 retry 烧时间。 | |
| 239 | + | |
| 240 | +--- | |
| 241 | + | |
| 242 | +## 8. 删除 / 改写的阶段级门引用(实现级清单) | |
| 243 | + | |
| 244 | +### 8.1 删除(顶层 frontend 段) | |
| 245 | +- 删 `phase('Behavior')`(1644)+ `await runBehaviorGate(module)`(1645)。 | |
| 246 | +- `runBehaviorGate`(1465-1582)**改造为** per-FE 的 `runBehaviorGateOnce` + `behaviorSubGate`(被 reviewWithFixLoop 调用),**不推倒重写**——保留 enforceEnv / 空覆盖兜底 / interaction 分层 / 软文字 source 分流 / softPassed 语义,只把 scope 缩到单 FE、把「硬问题转 must-fix locator」作为新增出口。 | |
| 247 | +- `meta.phases`:**彻底删除 `{ title: 'Behavior' }`**(12 行)——行为验收并入 Frontend phase 内,所有行为相关 `agent()`/`adjudicate()` 的 phase 入参从 `'Behavior'` 统一改为 `'Frontend'`(与 reviewWithFixLoop 现有 grp 一致)。不保留 'Behavior' 作 UI 分组(否则成无 `phase()` 驱动的孤儿)(采纳「删阶段门」维度 medium#3)。 | |
| 248 | + | |
| 249 | +### 8.2 reportPrompt 前端分支改写(采纳「删阶段门」维度 high#2 + 控制流维度 medium#6) | |
| 250 | +- **删/改 1096 绿前置**:原 Glob `frontend-phase-behavior-gate-r*.md`「最后一份非 RED」整条**删除**(阶段级文件已不再产生,留之必断链或不确定 halt)。改为:**对每个 `req-done/<FE>` tag 视为行为已过**(因 per-FE 行为 green 已是 req-done 前置,report 不必再独立校验行为绿,避免双真值)。可选加一句轻量校验:每个 FE 存在对应 `<date>-<FE>-behavior-r*-a*.md` 证据且最后一份非 RED。 | |
| 251 | +- **改 1106 §⑤**:把 `frontend-phase-behavior-gate-r*.md` 汇总改为按 per-FE 证据目录 `docs/superpowers/reviews/<date>-FE-*-behavior-r*-a*.md` 汇总 flake / 环境 race / 文字 continue。 | |
| 252 | +- **改 1107 §⑧**:偏离清单的 behavior-gate coverageGaps / textIssues continue / 逐控件判定 / authState 来源,从阶段级文件改为 per-FE 证据汇总。 | |
| 253 | +- 注意 testGate 的 `frontend-phase-test-gate-r*.md` 绿前置(1094)**保留不动**——testGate(全量回归)不并入 per-FE 循环。 | |
| 254 | + | |
| 255 | +### 8.3 保留不变 | |
| 256 | +- **阶段级 testGate(全量回归 vitest+playwright)保留**(1642-1643)——职责正交,per-FE 行为验收不替代全量回归。 | |
| 257 | +- 后端 featureLoop / featureLoop backend 分支 / runMilestone / runCrossModule / 顶层 backend 段:**逐字不变**。 | |
| 258 | + | |
| 259 | +--- | |
| 260 | + | |
| 261 | +## 9. README / SKILL 文案改动 | |
| 262 | + | |
| 263 | +- **README**(46-49 行):把「→ testGate(frontend) → 前端行为闸 behavior-gate(…逐路由枚举…)→ runMilestone」改为:「featureLoop(前端,FE-NN,每个 FE 在 review 循环内并入 per-FE 行为验收 approve 子门:reviewer approve 时起本 FE 全栈+sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才打 req-done) → testGate(frontend,全量回归) → runMilestone」。补一句「前端骨架占位阶段保证中途可构建」。 | |
| 264 | +- **coding-start SKILL**(步骤 0 横幅,26 行):把「前端行为闸 behavior-gate(…逐路由枚举:交互失效→halt,文字不符→仲裁)」改为「前端功能循环内含 per-FE 行为验收(reviewer approve 时起本 FE 全栈验『按钮真生效/文字对』,硬问题可 fix 重验,不再是末尾独立门)」。 | |
| 265 | + | |
| 266 | +--- | |
| 267 | + | |
| 268 | +## 10. 残留风险(接受 / 已缓解) | |
| 269 | + | |
| 270 | +1. **冷起栈墙钟**:round 间不复用栈是现运行时硬约束(无跨子会话常驻进程原语)。已用「approve 子门 + BEHAVIOR_FE_MAX=3」把起栈次数压到典型 1 次/FE、最坏 3 次/FE 来控成本,而非靠不可行的栈复用。每次起栈含全量 Flyway apply(随 migration 增多单调增长),计入墙钟预算。若 N 很大仍可能数小时——这是用户已接受「每 FE 起一次全栈」的直接代价,本设计已把它从 N×20 降到 N×(1~3)。 | |
| 271 | +2. **locator 可靠性**:A 类(组件文件级)映射可行;B 类不可降级放行而是计入未覆盖阻断 approve。仍可能出现 fix 在组件内 Grep 不中绑定行、多轮修不中逼近 BEHAVIOR_FE_MAX 后 halt 转人工——这是「运行时 DOM→源码」固有难度的残留,已用「附 DOM 路径+绑定片段+期望/实际值」最大化命中率、用比 review 更紧的 BEHAVIOR_FE_MAX=3 控制空转成本。 | |
| 272 | +3. **骨架占位覆盖不全**:若 runFrontendSkeleton 漏建某 FE 的路由占位,验该 FE 前的某个兄弟 FE 时仍可能 build-failed;已用 build-failed 短路(不 halt、记证据)兜底,但会留覆盖盲点供人工。 | |
| 273 | +4. **per-FE 库状态与阶段级 testGate 隔离**:行为门入口 DROP+CREATE 自带空库语义,跑完不为 testGate 留状态(testGate 自带 setup-test-db 重置);二者共用同一物理测试库,时序上串行无并发争用。 | |
| 274 | +5. **deriveSpec 的 FE→路由结构化小节依赖 LLM 正确产出 + reviewer 校验**:若两者都失误,feScope 可能不准;已让 reviewer 把「小节存在且与 router 一致」作为 request-changes 项兜底,但非确定性。 | |
| 275 | + | |
| 276 | +--- | |
| 277 | + | |
| 278 | +## 11. 实现顺序建议 | |
| 279 | + | |
| 280 | +1. 前置 D(setup-test-db 模板护栏)——独立、最安全、先做。 | |
| 281 | +2. 前置 C(deriveSpecPrompt FE→路由结构化小节 + reviewer 校验)——为 feScope 入参铺路。 | |
| 282 | +3. 前置 A(runFrontendSkeleton 骨架占位 stage + tddPrompt 占位替换指令)——保证中途可构建。 | |
| 283 | +4. 前置 B(BEHAVIOR_GATE_SCHEMA 增 build-failed + 归因控制流)。 | |
| 284 | +5. 主改造(reviewWithFixLoop 加 behaviorSubGate / runBehaviorGate→runBehaviorGateOnce per-FE / 二维计数 / softPassed 提升 / contract 分离)。 | |
| 285 | +6. 删除阶段级门 + reportPrompt 改写 + meta.phases 删 Behavior + phase 入参改 Frontend。 | |
| 286 | +7. README / coding-start SKILL 文案。 | |
| 287 | + | |
| 288 | +每步后端分支必须逐字不变(diff 校验);运行时红线(time/random builtin / 顶层 return / 注入全局)每步复核。 | ... | ... |
lib/setup-test-db-template.test.mjs
| ... | ... | @@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url' |
| 11 | 11 | |
| 12 | 12 | const TEMPLATE = fileURLToPath(new URL('../skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs', import.meta.url)) |
| 13 | 13 | |
| 14 | -function runWithSchema(schemaLine) { | |
| 14 | +function runWithSchema(schemaLine, env = {}) { | |
| 15 | 15 | const dir = mkdtempSync(join(tmpdir(), 'erp-stdb-')) |
| 16 | 16 | mkdirSync(join(dir, 'scripts')) |
| 17 | 17 | copyFileSync(TEMPLATE, join(dir, 'scripts', 'setup-test-db.mjs')) |
| ... | ... | @@ -19,7 +19,7 @@ function runWithSchema(schemaLine) { |
| 19 | 19 | join(dir, 'config-vars.yaml'), |
| 20 | 20 | ['database:', ' host: 127.0.0.1', ' port: 1', ' user: root', ' password: x', ' ' + schemaLine, ''].join('\n'), |
| 21 | 21 | ) |
| 22 | - return spawnSync('node', [join(dir, 'scripts', 'setup-test-db.mjs')], { encoding: 'utf8' }) | |
| 22 | + return spawnSync('node', [join(dir, 'scripts', 'setup-test-db.mjs')], { encoding: 'utf8', env: { ...process.env, ...env } }) | |
| 23 | 23 | } |
| 24 | 24 | |
| 25 | 25 | // ROBUST-3:空 schema 不应进到 DROP DATABASE `` —— 守卫应先拦下。 |
| ... | ... | @@ -50,3 +50,24 @@ test('setup-test-db: a valid identifier schema passes the guard (no false positi |
| 50 | 50 | // 连不上 127.0.0.1:1 → 非零退出;关键是错误不来自 schema 守卫。 |
| 51 | 51 | assert.doesNotMatch(r.stderr, /database\.schema 非法|schema 非法或未填/, 'stderr: ' + r.stderr) |
| 52 | 52 | }) |
| 53 | + | |
| 54 | +// 前置依赖 D(安全):测试库命名护栏——非测试库名默认 fail-closed,防误删开发/生产库。 | |
| 55 | +test('setup-test-db: a non-test schema fails closed by default (D non-test guard)', () => { | |
| 56 | + const r = runWithSchema('schema: erp_prod') | |
| 57 | + assert.equal(r.status, 1) | |
| 58 | + assert.match(r.stderr, /不像测试库|ALLOW_NONTEST_DROP/, '应是测试库命名护栏报错 — stderr: ' + r.stderr) | |
| 59 | +}) | |
| 60 | + | |
| 61 | +// D:测试库名(含 test / _test / _dev / _local)应通过命名护栏,错误不来自该护栏。 | |
| 62 | +test('setup-test-db: test-like schema names pass the naming guard (erp_test / erp_dev)', () => { | |
| 63 | + for (const name of ['erp_test', 'erp_dev', 'erp_local', 'test_db']) { | |
| 64 | + const r = runWithSchema('schema: ' + name) | |
| 65 | + assert.doesNotMatch(r.stderr, /不像测试库/, `${name} 不应被命名护栏拒绝 — stderr: ` + r.stderr) | |
| 66 | + } | |
| 67 | +}) | |
| 68 | + | |
| 69 | +// D:ALLOW_NONTEST_DROP=1 显式放行非测试库名(错误不再来自命名护栏)。 | |
| 70 | +test('setup-test-db: ALLOW_NONTEST_DROP=1 explicitly bypasses the naming guard', () => { | |
| 71 | + const r = runWithSchema('schema: erp_prod', { ALLOW_NONTEST_DROP: '1' }) | |
| 72 | + assert.doesNotMatch(r.stderr, /不像测试库/, '显式放行后不应再被命名护栏拒绝 — stderr: ' + r.stderr) | |
| 73 | +}) | ... | ... |
skills/coding/coding-start/SKILL.md
| ... | ... | @@ -21,9 +21,11 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) |
| 21 | 21 | 每个模块: |
| 22 | 22 | 后端功能循环 spec → plan → tdd → verify → review(≤5轮) |
| 23 | 23 | 后端测试闸 test-gate(RED 自动重试 1 次,仍 RED → halt) |
| 24 | - 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/) | |
| 25 | - 前端测试闸 test-gate | |
| 26 | - 前端行为闸 behavior-gate(headless 全栈起栈+种子 sentinel,逐路由枚举:交互失效→halt,文字不符→仲裁) | |
| 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(全量回归) | |
| 27 | 29 | 跨模块记录 → 模块报告 → 里程碑(merge --no-ff + milestone/<id> tag) |
| 28 | 30 | 任一模块 halt → fail-fast 停在该模块,修复后重跑本入口即可续跑 |
| 29 | 31 | ... | ... |
skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs
| ... | ... | @@ -83,6 +83,16 @@ if (!/^[A-Za-z0-9_$]+$/.test(DB_SCHEMA)) { |
| 83 | 83 | process.exit(1) |
| 84 | 84 | } |
| 85 | 85 | |
| 86 | +// 测试库命名护栏(确定性 JS 边界,唯一防线):本脚本无条件 DROP + CREATE schema; | |
| 87 | +// per-FE × per-behaviorRound 反复起栈会反复 DROP,对 config-vars 指向的库(可能 = 开发/生产库) | |
| 88 | +// 误删风险随次数放大。故只允许作用于"明确像测试/本地库"的库名——库名须含 test / _test / _dev / _local | |
| 89 | +// 之一(不区分大小写),否则 fail-closed;确需对非测试库执行时,显式设 ALLOW_NONTEST_DROP=1 放行。 | |
| 90 | +// 不依赖任何调用方(如行为门 runner)记得复述同等检查——模板是唯一防线。 | |
| 91 | +if (process.env.ALLOW_NONTEST_DROP !== '1' && !/test|_dev|_local/i.test(DB_SCHEMA)) { | |
| 92 | + console.error(`[setup-test-db] 拒绝:schema=${JSON.stringify(DB_SCHEMA)} 不像测试库(库名须含 test/_test/_dev/_local),设 ALLOW_NONTEST_DROP=1 显式放行`) | |
| 93 | + process.exit(1) | |
| 94 | +} | |
| 95 | + | |
| 86 | 96 | console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) |
| 87 | 97 | |
| 88 | 98 | const sql = | ... | ... |
workflows/coding.mjs
| ... | ... | @@ -9,8 +9,10 @@ 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: 'Behavior' }, { title: 'Milestone' }, | |
| 12 | + { title: 'Gate' }, { title: 'Milestone' }, | |
| 13 | 13 | ], |
| 14 | + // 注:'Behavior' phase 已删除——前端行为验收并入 per-FE reviewWithFixLoop 的 approve 子门, | |
| 15 | + // 所有行为相关 agent()/adjudicate() 的 phase 入参统一用 'Frontend'(与 reviewWithFixLoop grp 一致)。 | |
| 14 | 16 | } |
| 15 | 17 | |
| 16 | 18 | const ROUTER_SCHEMA = { type:'object', additionalProperties:false, |
| ... | ... | @@ -65,24 +67,27 @@ const GATE_SCHEMA = { type:'object', additionalProperties:false, |
| 65 | 67 | required:['status'], properties:{ status:{type:'string',enum:['green','red']}, |
| 66 | 68 | failures:{type:'array',items:{type:'string'}} } } |
| 67 | 69 | |
| 68 | -// BEHAVIOR_GATE_SCHEMA:前端行为门(headless behavior-gate)返回。 | |
| 70 | +// BEHAVIOR_GATE_SCHEMA:前端行为门(per-FE behavior 子门)返回。 | |
| 69 | 71 | // 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 覆盖率 / 环境错误分别结构化, |
| 70 | -// JS 据 source/kind 分流(交互硬 halt,文字按 source 二分 allowContinue,envError 走 retry)。 | |
| 71 | -// 设计:见 docs/design/2026-06-02-frontend-behavior-gate.md § 2。 | |
| 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。 | |
| 72 | 74 | const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, |
| 73 | 75 | required:['status','routesPlanned','routesReached','controlsEnumerated'], properties:{ |
| 74 | 76 | status:{type:'string', enum:['green','red']}, |
| 75 | - routesPlanned:{type:'integer'}, // router 声明的路由数(覆盖率分母来源) | |
| 76 | - routesReached:{type:'integer'}, // 实际带鉴权加载成功的路由数 | |
| 77 | - controlsEnumerated:{type:'integer'}, // live 枚举到的控件数(空覆盖必须可见) | |
| 77 | + routesPlanned:{type:'integer'}, // 本 FE 关联路由数(覆盖率分母来源;per-FE 只数 feScope.routes,不数 router 全部) | |
| 78 | + routesReached:{type:'integer'}, // 实际带鉴权加载成功的本 FE 路由数 | |
| 79 | + controlsEnumerated:{type:'integer'}, // live 枚举到的本 FE 白名单控件数(空覆盖必须可见) | |
| 78 | 80 | authState:{type:'string'}, // 以何角色登录 / 覆盖角色 / 未覆盖角色集 |
| 81 | + // interactionFailures.locator:行为硬问题的源码定位(组件文件 [+ DOM 描述])。per-FE 行为门必须反查到 | |
| 82 | + // 组件文件路径才能转 must-fix 喂 fix;反查不出(B 类)→ 不入 interactionFailures,归 coverageGap(不放行)。 | |
| 79 | 83 | // 交互层硬边界:no-observable-effect / js-error / console-error / missing-docs05-call / binding-garbage |
| 80 | 84 | interactionFailures:{ type:'array', items:{ type:'object', additionalProperties:false, |
| 81 | 85 | required:['page','control','kind','detail'], |
| 82 | 86 | properties:{ |
| 83 | 87 | page:{type:'string'}, control:{type:'string'}, |
| 84 | 88 | kind:{type:'string', enum:['no-observable-effect','js-error','console-error','missing-docs05-call','binding-garbage']}, |
| 85 | - detail:{type:'string'} } } }, | |
| 89 | + detail:{type:'string'}, | |
| 90 | + locator:{type:'string'} } } }, // 组件文件路径 [+ DOM 选择器/绑定片段描述];有则可转 must-fix 喂 fix | |
| 86 | 91 | // 文字层软边界:source 决定 allowContinue(sentinel 客观 bug 不可 continue;i18n/literal/semantic 可 adjudicate continue) |
| 87 | 92 | textIssues:{ type:'array', items:{ type:'object', additionalProperties:false, |
| 88 | 93 | required:['page','region','expected','actual','source'], |
| ... | ... | @@ -91,18 +96,21 @@ const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, |
| 91 | 96 | expected:{type:'string'}, actual:{type:'string'}, |
| 92 | 97 | source:{type:'string', enum:['sentinel','i18n','literal','semantic']} } } }, |
| 93 | 98 | // 覆盖率缺口:写证据 + recordDecisions,不单独 halt(空覆盖由 controlsEnumerated==0 兜底) |
| 99 | + // build-failed-sibling-unimpl:兄弟 FE 未实现导致本 FE 之外路由/组件编译缺件(预期中途态,不归本 FE 缺陷) | |
| 100 | + // locator-not-resolvable:行为硬问题连组件文件都反查不出(B 类),计入未覆盖阻断 approve,不静默放行 | |
| 94 | 101 | coverageGaps:{ type:'array', items:{ type:'object', additionalProperties:false, |
| 95 | 102 | required:['page','reason','detail'], |
| 96 | 103 | properties:{ |
| 97 | 104 | page:{type:'string'}, |
| 98 | - reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed']}, | |
| 105 | + reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed','build-failed-sibling-unimpl','locator-not-resolvable']}, | |
| 99 | 106 | detail:{type:'string'} } } }, |
| 100 | - // 环境错误(与业务断言失败严格区分,走 retry):none 表示无环境问题 | |
| 107 | + // 环境错误(与业务断言失败严格区分):none 表示无环境问题;build-failed 是确定性短路(既不 retry 也不 halt)。 | |
| 108 | + // build-failed 时 rootCausePath 写报错根因文件路径——落在非本 FE 路径=兄弟未实现(短路放行),落在本 FE=真构建 bug。 | |
| 101 | 109 | envError:{ type:'object', additionalProperties:false, |
| 102 | 110 | required:['kind'], |
| 103 | 111 | properties:{ |
| 104 | - kind:{type:'string', enum:['port-conflict','stack-not-ready','seed-error','auth-failed','timeout','none']}, | |
| 105 | - detail:{type:'string'}, ports:{type:'string'}, pids:{type:'string'} } }, | |
| 112 | + kind:{type:'string', enum:['port-conflict','stack-not-ready','seed-error','auth-failed','timeout','build-failed','none']}, | |
| 113 | + detail:{type:'string'}, ports:{type:'string'}, pids:{type:'string'}, rootCausePath:{type:'string'} } }, | |
| 106 | 114 | // decisions[]:复用 STAGE_RESULT 形状,缺值自主决策日志 |
| 107 | 115 | decisions:{ type:'array', items:{ type:'object', additionalProperties:false, |
| 108 | 116 | required:['question','choice','rationale'], |
| ... | ... | @@ -288,6 +296,21 @@ function deriveSpecPrompt(id, phase) { |
| 288 | 296 | fe |
| 289 | 297 | ? '- 规格至少含:关联 REQ + 关联原型;组件树(按页面 / 区域分块,推导自 prototype DOM);页面状态机(loading / empty / error / 正常 / 表单提交中 至少 5 态);消费的后端端点(对齐 docs/05);业务规则前端复刻清单(逐条:规则 / 触发时机 / 报错文案 / 来源 REQ);Design Tokens 引用清单(`var(--color-*)`)。' |
| 290 | 298 | : '- 规格覆盖:goal / 输入输出 / 业务规则 / 约束 / schema / API 引用 / acceptance criteria。', |
| 299 | + fe | |
| 300 | + ? [ | |
| 301 | + '', | |
| 302 | + '## 行为验收作用域结构化小节(per-FE 行为门唯一断言依据,**强制写到 spec 头部**)', | |
| 303 | + '- 在 spec 文件头部(紧随标题/关联 REQ 之后)写一个**结构化小节**,标题逐字为 `## 行为验收作用域`,内含两条机器可读清单:', | |
| 304 | + ' ```', | |
| 305 | + ' ## 行为验收作用域', | |
| 306 | + ' - 关联路由: [/orders, /orders/:id]', | |
| 307 | + ' - 负责控件白名单: [data-testid=order-submit, /orders 页 "提交" 按钮, ...]', | |
| 308 | + ' ```', | |
| 309 | + `- **关联路由**:从 \`${ROOT}/frontend/\` router 配置(用 Grep 定位)取本 FE 真正负责渲染的路由 path(与 router 一致;带参动态路由保留 \`:id\` 占位)。**只列本 FE 路由**,不要列兄弟 FE / 共享路由。`, | |
| 310 | + '- **负责控件白名单**:本 FE 页面上"点了必须有可观测效果 / 显示必须正确"的控件清单(优先 `data-testid` 约定;无 testid 时用 `<页面> + DOM 选择器/可见文案` 描述)。行为门只对白名单内控件判 must-fix;白名单外 / 共享控件归 coverageGap,绝不算本 FE 缺陷。', | |
| 311 | + '- 该小节是**确定性映射**(fe-feature-review 会校验其存在且与 router 一致,缺失/不一致 → request-changes);推不出路由(router 尚未声明本 FE 路由)→ 按硬约束登记 decisions 取最有依据的占位 path 或 halt(不要留空)。', | |
| 312 | + ].join('\n') | |
| 313 | + : '', | |
| 291 | 314 | '', |
| 292 | 315 | commitBlock('<spec artifactPath>', `docs(spec:${id}): 派生规格`), |
| 293 | 316 | '', |
| ... | ... | @@ -366,6 +389,9 @@ function tddPrompt(id, phase, planPath) { |
| 366 | 389 | fe |
| 367 | 390 | ? '- jsdom 类型用 vitest/jest 写组件单测;e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' |
| 368 | 391 | : '', |
| 392 | + fe | |
| 393 | + ? `- **占位替换(保证中途可构建 + per-FE 行为门可达本 FE 路由)**:前端骨架阶段已在 router 里为本 FE 路由声明 lazy import 但指向占位组件 \`FeStub\`。本 FE 实现完成后,**必须**把 router 中本 FE 路由的 import 从 \`FeStub\` 改为本 FE 真组件(用 Grep 在 \`${ROOT}/frontend/\` router 定位本 FE 路由 path 的 import 行;仍在 \`frontend/\` 路径内,不破坏护栏)。改完确保 router 该路由 lazy import 指向真组件、可构建可达。` | |
| 394 | + : '', | |
| 369 | 395 | '', |
| 370 | 396 | '## 护栏', |
| 371 | 397 | '- **绝不**在主会话直接跑测试(mvn / pnpm / playwright / scripts/test.mjs)——必须通过 Agent 子会话。', |
| ... | ... | @@ -441,6 +467,7 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { |
| 441 | 467 | `- 本 ${fe ? 'FE' : 'REQ'} 引入的代码 diff + 规格 \`${specPath}\`。`, |
| 442 | 468 | fe ? `- 本 FE 关联的所有 prototype 文件(spec 顶部"关联原型"列表),供对照渲染结构。` : '', |
| 443 | 469 | `- **phase = ${fe ? 'frontend → 附加前端 7 维 checklist。其中仅"颜色对比度"(§3 子项)与"响应式"(§4)为主观/best-effort,绝不单独触发 request-changes;a11y 的 label/键盘可达/危险操作确认等客观项仍可作 must-fix(与 agents/code-reviewer.md §3-4 对齐,避免非确定性循环耗尽 5 轮)。' : 'backend → 通用代码审查维度(正确性 / 边界 / 错误处理 / 一致性)。'}**`, |
| 470 | + fe ? `- **行为验收作用域小节校验(per-FE 行为门前置真值,必查)**:spec \`${specPath}\` 头部**必须**含逐字标题为 \`## 行为验收作用域\` 的结构化小节,且其 \`关联路由:\` 清单与 \`${ROOT}/frontend/\` router 配置一致(本 FE 路由都在 router 声明、无悬空/错配)。该小节缺失 或 与 router 不一致 → **必须 request-changes**,把"补齐/对齐 行为验收作用域小节"列入 issues(locator 指向 spec 文件路径)。这是 approve 前置——行为门只能据此确定本 FE 路由作用域。` : '', | |
| 444 | 471 | round > 1 && lastVerifySummary |
| 445 | 472 | ? `\n## 上轮 fix 后复验摘要(round ${round - 1})\n${lastVerifySummary}\n\n你必须把"上轮 must-fix 在本轮 diff 中是否真的被修"作为本轮裁决的核心维度。已修的不要再次纳入 must-fix;未修 / 修得不对,单点列入 issues。` |
| 446 | 473 | : '', |
| ... | ... | @@ -518,106 +545,191 @@ function gatePrompt(module, phase, attempt = 1) { |
| 518 | 545 | ].filter(Boolean).join('\n') |
| 519 | 546 | } |
| 520 | 547 | |
| 521 | -// ---- 前端行为门(headless behavior-gate)---- | |
| 522 | -// 设计权威:docs/design/2026-06-02-frontend-behavior-gate.md。frontend testGate 绿后、report/milestone 前跑, | |
| 523 | -// 仅 frontend-phase 聚合模块触发。门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend') | |
| 548 | +// ---- 前端行为验收(per-FE behavior 子门)---- | |
| 549 | +// 设计权威:docs/design/2026-06-02-frontend-behavior-in-review-loop.md。 | |
| 550 | +// 不再是阶段级末尾独立门——并入 per-FE reviewWithFixLoop 的 approve 子门:某轮 reviewer 判 approve 时才触发, | |
| 551 | +// 起本 FE 全栈 + sentinel 种子,枚举本 FE 路由控件/文字,硬问题转可 fix must-fix→重验,行为 green 才放行 approve。 | |
| 552 | +// 门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend') | |
| 524 | 553 | // (其路径护栏命中 backend/sql/scripts 即越界硬停,与门必须运行 setup-test-db / 起后端 / 生成 SQL 种子自相矛盾)。 |
| 525 | 554 | |
| 526 | 555 | // behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符; |
| 527 | 556 | // 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright, |
| 528 | -// 唯一**可写** = .tmp/behavior-gate/r<attempt>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 | |
| 557 | +// 唯一**可写** = .tmp/behavior-gate/<FE>/r<behaviorRound>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 | |
| 529 | 558 | function behaviorGateContract() { |
| 530 | 559 | return [ |
| 531 | - '## 硬约束(非交互行为门子代理)', | |
| 560 | + '## 硬约束(非交互行为验收子代理)', | |
| 532 | 561 | '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', |
| 533 | - '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', | |
| 534 | - '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json / playwright.config.*)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', | |
| 535 | - `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、起前端 headless(vite / playwright webServer)、跑 Playwright;唯一允许**写入**的路径是 \`${ROOT}/.tmp/behavior-gate/r<attempt>/\`(spec/种子 SQL/runner,跑完即弃)+ 证据报告 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-gate-r<attempt>.md\` + 其 assets(截图归档到 \`${ROOT}/docs/superpowers/module-reports/assets/...\`)。`, | |
| 562 | + '- 你是**跨栈只读验证门**:用真实运行(起后端 + 起前端 headless + Playwright 枚举)证明「本 FE 每个按钮/点击真的生效、每段文字显示正确内容」,**不是**实现功能、**不是**改源码。', | |
| 563 | + '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md § 零` → `docs/05-API接口契约.md` → `docs/03-数据库设计文档.md` → `prototype/`(前端布局/交互权威)→ `frontend/`(router 配置 / package.json)→ 现有代码。仍查不到时**优先自主决策继续**,把决策写进证据报告显著位置并登记到返回 `decisions[]`(`{question,choice,rationale,confidence}`)。', | |
| 564 | + `- **作用域例外(关键)**:本门为跨栈验证,明确允许**运行**(不修改)以下命令——\`node ${ROOT}/scripts/setup-test-db.mjs\`、起后端服务(spring-boot:run 等)、起前端 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/...\`)。`, | |
| 536 | 565 | `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, |
| 566 | + '- **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 缺陷。', | |
| 537 | 567 | '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', |
| 538 | 568 | '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。', |
| 539 | - '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / attempt 序号),**绝不**依赖时间戳 / 随机数。', | |
| 569 | + '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / FE id / behaviorRound / attempt 序号),**绝不**依赖时间戳 / 随机数。', | |
| 540 | 570 | ].join('\n') |
| 541 | 571 | } |
| 542 | 572 | |
| 543 | -// behaviorGatePrompt:门子代理的完整流水线提示(step0-6 + schema)。 | |
| 544 | -// attempt:1 = 首跑;2.. = flake / 环境 race retry。每 attempt 独立 .tmp 子目录 + 独立证据文件。 | |
| 545 | -function behaviorGatePrompt(module, attempt) { | |
| 546 | - const id = module?.id ?? 'frontend-phase' | |
| 547 | - const tmpDir = `${ROOT}/.tmp/behavior-gate/r${attempt}` | |
| 548 | - const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-gate-r${attempt}.md` | |
| 573 | +// behaviorGatePrompt:per-FE 行为验收子代理的完整流水线提示(step0-6 + schema)。 | |
| 574 | +// id:本 FE id(如 FE-07);specPath:本 FE spec(含 ## 行为验收作用域 小节,feScope 来源 + 日期前缀); | |
| 575 | +// behaviorRound:approve 子门内的行为 fix 轮(1..BEHAVIOR_FE_MAX);attempt:本轮内环境 race 重试序号(1..)。 | |
| 576 | +// 每 (FE × behaviorRound × attempt) 独立 .tmp 子目录 + 独立证据文件,绝不互相覆盖(不丢 flake 信号)。 | |
| 577 | +function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { | |
| 578 | + const safeId = id ?? 'FE' | |
| 579 | + const tmpDir = `${ROOT}/.tmp/behavior-gate/${safeId}/r${behaviorRound}` | |
| 580 | + const date = (() => { try { return dateFromArtifactPath(specPath) } catch { return '<date>' } })() | |
| 581 | + const evidence = `docs/superpowers/reviews/${date}-${safeId}-behavior-r${behaviorRound}-a${attempt}.md` | |
| 549 | 582 | return [ |
| 550 | - `# behavior-gate — 前端行为门(headless,attempt=${attempt})`, | |
| 583 | + `# behavior — 前端 per-FE 行为验收(headless,FE=${safeId}, behaviorRound=${behaviorRound}, attempt=${attempt})`, | |
| 551 | 584 | '', |
| 552 | 585 | behaviorGateContract(), |
| 553 | 586 | '', |
| 554 | 587 | '## 目标', |
| 555 | - `用真实全栈运行证明前端 \`${id}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`, | |
| 556 | - `单个子会话内**收敛完成**:起栈 → 逐路由枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`, | |
| 557 | - attempt > 1 ? `- 本次 = 第 ${attempt} 次(上一次 red 或 envError;本轮用于辨识 flake / 等环境就绪);证据**写到独立文件 r${attempt}** 不要覆盖前一次。` : '', | |
| 558 | - '', | |
| 559 | - '## 运行机制(无常驻进程跨会话;起栈→跑→teardown 收敛进单 runner)', | |
| 560 | - `- **入口清目录(跑前第一步,去串味,§7/C25)**:${attempt === 1 | |
| 561 | - ? `本次 attempt=1 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/\` 目录(清掉所有历史 attempt 残留 runner/种子/spec,避免跨 resume 串味),再新建本 attempt 子目录 \`${tmpDir}/\`。` | |
| 562 | - : `本次 attempt=${attempt} → 仅删除/清空本 attempt 子目录 \`${tmpDir}/\`(保证幂等,不动其它 attempt 的已提交证据无关的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, | |
| 563 | - `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 全部子进程**并透传结构化结果。**绝不**让前台 spring-boot:run / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, | |
| 564 | - `- \`${tmpDir}/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, | |
| 565 | - '', | |
| 566 | - '## step0 探测起栈能力', | |
| 567 | - `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/frontend/playwright.config.*\` + \`${ROOT}/config-vars.yaml\`。`, | |
| 568 | - '- (a) 有 `webServer` / `reuseExistingServer` → 复用 playwright 起前端;(b) 无 → runner 自负起**后端 + 前端**(项目通常无既有 e2e 起栈,须显式探测 + 自负起栈);无法判定 / 起栈失败 → `envError.kind="stack-not-ready"`。', | |
| 569 | - '', | |
| 570 | - '## step1 路由真值发现(覆盖率分母)', | |
| 571 | - `- 主来源 = \`${ROOT}/frontend/\` 的 router 配置(Vue Router / React Router \`routes\`,用 Grep 定位);\`routesPlanned\` = router 声明的路由数。`, | |
| 572 | - '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**每路由的预期控件与文字来源**(作覆盖率分母);每路由标注所需登录角色。', | |
| 588 | + `用真实全栈运行证明本 FE \`${safeId}\` 的「每个按钮/点击都真的生效、每段文字都显示正确内容(right context)」。`, | |
| 589 | + `单个子会话内**收敛完成**:冷起栈 → 逐**本 FE 路由**枚举 + 两层断言 → teardown。期望即时推导(prototype/ + REQ + docs/05),**不**持久化为契约,但推导期望写进已提交证据报告。`, | |
| 590 | + `- 本 FE 行为验收作用域唯一真值 = spec \`${specPath}\` 头部的 \`## 行为验收作用域\` 小节(\`关联路由:\` + \`负责控件白名单:\`)。先 Read 该 spec 取出 feScope;缺该小节 → \`envError.kind="stack-not-ready"\` 并在 detail 写明(不应出现:reviewer 已校验它存在)。`, | |
| 591 | + behaviorRound > 1 || attempt > 1 ? `- 本次 = behaviorRound ${behaviorRound} / attempt ${attempt}(上一次 red / envError / fix 后重验);证据**写到独立文件 r${behaviorRound}-a${attempt}** 不要覆盖前一次。` : '', | |
| 592 | + '', | |
| 593 | + '## 运行机制(无常驻进程跨会话;冷起栈→跑→teardown 收敛进单 runner)', | |
| 594 | + '- **冷起栈(运行时硬约束)**:本项目**无既有 e2e webServer / playwright.config 复用入口**——runner 必须**自负冷起后端 + 前端**,behaviorRound / attempt 之间**绝不复用运行栈、无 HMR**,每次从头 spawn 起栈→跑→teardown。', | |
| 595 | + `- **入口清目录(跑前第一步,去串味)**:${behaviorRound === 1 && attempt === 1 | |
| 596 | + ? `本次是本 FE 首轮首次 → 先删除整个 \`${ROOT}/.tmp/behavior-gate/${safeId}/\` 目录(清掉本 FE 历史残留 runner/种子/spec),再新建本轮子目录 \`${tmpDir}/\`。` | |
| 597 | + : `本次 behaviorRound=${behaviorRound} → 仅删除/清空本轮子目录 \`${tmpDir}/\`(幂等,不动其它 round 的临时残留),再新建。`}用确定性、跨平台方式删除(如 \`fs.rmSync(path, { recursive:true, force:true })\` 后 \`fs.mkdirSync(path, { recursive:true })\`),**仅限上述受控路径**,绝不删 \`.tmp/behavior-gate/\` 之外的任何路径。`, | |
| 598 | + `- 你在 \`${tmpDir}/\` 写一个一次性 runner(如 \`run.mjs\`),用 spawn 起进程树、轮询就绪、\`finally\` 中 **kill 本 FE 起的全部子进程**并透传结构化结果。**绝不**让前台 spring-boot:run / vite 挂死会话——它们永不退出,必须 spawn 到后台进程树 + 轮询健康端点 + 跑完 teardown。`, | |
| 599 | + `- **确定性端口/pid 回收前置**:起栈前先按既知端口 + \`${tmpDir}/*.pid\` 强制回收上一 attempt 残留(编排层 + runner 双保险);端口先探测占用,占用则回收或退到动态空闲端口 + 把 baseURL 注入下游。`, | |
| 600 | + `- \`${ROOT}/.tmp/behavior-gate/\`(含子目录)已被仓库 \`.gitignore\` 忽略,是唯一临时写区;跑完即弃,只提交证据报告 + assets。`, | |
| 601 | + '', | |
| 602 | + '## step0 探测 + build 归因(确定性短路前置,依赖 build-failed kind)', | |
| 603 | + `- 读 \`${ROOT}/docs/04-技术规范.md § 零\` + \`${ROOT}/frontend/package.json\` + \`${ROOT}/config-vars.yaml\`。`, | |
| 604 | + '- runner 自负冷起后端 + 前端 headless(无既有 webServer 可复用)。**起 dev / source-map 模式**(注入定位辅助:`data-testid` 约定 / Vue `__file`),便于把 page+selector 映射回组件文件。', | |
| 605 | + '- **build / 起 dev server 失败时先归因**:用 `git` / `Grep` 判断报错根因文件路径——', | |
| 606 | + ` - 落在**非本 FE 的 \`frontend/\` 路径**(兄弟 FE 组件缺失 / 骨架占位未覆盖 / 指向未建路由)→ \`envError.kind="build-failed"\` + \`rootCausePath=<非本FE路径>\`(**预期中途态**,不是本 FE bug)。`, | |
| 607 | + ' - 落在**本 FE 路径**(feScope 关联组件)→ 才是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]`(带 locator=组件文件)。', | |
| 608 | + ' - 起栈本身就绪失败但非编译错(端口/超时)→ `envError.kind="stack-not-ready"|"timeout"`。', | |
| 609 | + '', | |
| 610 | + '## step1 路由真值发现(覆盖率分母 = 本 FE 路由,不数 router 全部)', | |
| 611 | + '- 分母来源 = spec `## 行为验收作用域` 小节的 `关联路由:` 清单(**只数本 FE 路由**);`routesPlanned` = 本 FE 关联路由数。**不要**把 router 全部路由计入分母(router 含兄弟 FE + 占位路由)。', | |
| 612 | + '- 由 `prototype/` + 关联 REQ 卡片 + `docs/05` 推导**本 FE 每路由的预期控件与文字来源**;每路由标注所需登录角色。', | |
| 573 | 613 | '- 带参动态路由用**种子已知主键**实例化;无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', |
| 614 | + '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', | |
| 574 | 615 | '', |
| 575 | 616 | '## step2 安全护栏 + 起栈四段严格时序(schema 由 Flyway 在后端启动时才建)', |
| 576 | - `1) **测试库安全护栏(确定性,先于一切)**:读 config-vars 的数据库名;若**不匹配测试库命名**(库名须含或以 \`test\` / \`_test\` / \`_dev\` / \`_local\` 结尾)→ runner 非零退出,返回 \`status:red\` + \`envError.kind\` 留空走 HALT 语义(在 detail 写明「测试库护栏:库名 <x> 非测试库,拒绝 DROP,留人工确认」)。**绝不**对非测试库跑 setup-test-db。`, | |
| 617 | + `1) **测试库安全护栏**:测试库命名护栏现已下沉到 \`${ROOT}/scripts/setup-test-db.mjs\` 模板自身(确定性 JS 边界,库名须含 test/_test/_dev/_local,否则 fail-closed,\`ALLOW_NONTEST_DROP=1\` 显式放行)。runner 可复述但模板是唯一防线;若模板因测试库护栏非零退出 → 返回 \`status:red\` + 在 detail 写明「测试库护栏触发」(上层对此**不重试不仲裁直接 halt**,留人工确认)。`, | |
| 577 | 618 | `2) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程。`, |
| 578 | 619 | '3) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', |
| 579 | 620 | '4) **此时才跑种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** 种子(先父后子)。失败 → `envError.kind="seed-error"` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不**混进交互 RED。', |
| 580 | 621 | ' - **sentinel 规则**:按列类型派生类型合法且可辨识的值——字符串列逐字段唯一编码(如 `CUST_NAME_S001`,抓绑错字段)+ 行序号保 UNIQUE;数值列用高位魔数;enum 列从 docs/03 值域取并标注。插入前扫 Flyway / config-vars 既有初始数据(admin_init 等)键,sentinel 主键偏移到不冲突区;断言按 sentinel 行已知主键定位。所有 SQL 值参数化 / 白名单转义,sentinel 用受控 `[A-Za-z0-9_]` 格式。', |
| 581 | - '5) **起前端 headless**:(a) playwright webServer / (b) spawn + 轮询 ready;端口同样探测 + 动态回退。', | |
| 582 | - '- `finally` **硬要求 kill 全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。', | |
| 622 | + '5) **起前端 headless**:spawn + 轮询 ready;端口同样探测 + 动态回退。', | |
| 623 | + '- `finally` **硬要求 kill 本 FE 起的全部子进程**;端口 + pid 写入 `envError.ports` / `envError.pids`(即便成功也回填,便于审计)。反复 port-conflict 设独立硬上限直接 halt 提示人工清理(不连环 retry 烧时间)。', | |
| 583 | 624 | '', |
| 584 | 625 | '## step2.5 鉴权 bootstrap(确定性前置)', |
| 585 | 626 | '- 用 config-vars `admin_init` 或种子已知凭据,经 `docs/05` 登录端点**真实登录**拿 JWT,注入 Playwright `storageState`;`authState` 记角色覆盖(覆盖 / 未覆盖角色集)。', |
| 586 | 627 | '- 登录失败 = `envError.kind="auth-failed"`(环境 race,走 retry),**绝不**当成死控件。', |
| 587 | 628 | '', |
| 588 | - '## step3 枚举(可达性驱动 + 分母对账,非首帧快照)', | |
| 589 | - '- 每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 推导清单,分子 = live 枚举。', | |
| 629 | + '## step3 枚举(可达性驱动 + 分母对账,非首帧快照;只驱动本 FE feScope)', | |
| 630 | + '- **只枚举/驱动 feScope.routes + feScope.controlWhitelist**(本 FE 白名单控件)。每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 本 FE 推导清单,分子 = live 枚举。', | |
| 590 | 631 | '- 分母有但首帧无的控件:runner 尝试**驱动到出现态**(种子保列表非空触发行级操作 / 进多步流程下屏 / 展开 dropdown / 切 tab 后二次枚举);仍不可达 → `coverageGaps[reason="deep-control-not-driven"]`,不静默判 green。到不了的路由 → `coverageGaps[reason="unreachable-auth"|"unreachable-no-route"]`,与「到达了但控件死」严格区分。', |
| 632 | + '- **白名单外 / 共享控件**:若属其它未 approve FE 或共享区 → 归 `coverageGaps[reason="deep-control-not-driven"]`,**绝不**归本 FE 的 `interactionFailures`。', | |
| 591 | 633 | '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', |
| 592 | - '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。', | |
| 634 | + '- `routesReached` / `controlsEnumerated` 据实填(本 FE 子集空覆盖必须可见)。', | |
| 593 | 635 | '', |
| 594 | 636 | '## step4 推导期望', |
| 595 | 637 | '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', |
| 596 | 638 | '', |
| 597 | - '## step5 断言(两层 + 可观测效果白名单)', | |
| 639 | + '## step5 断言(两层 + 可观测效果白名单 + 硬问题带源码 locator)', | |
| 598 | 640 | '- **交互层可观测效果白名单**: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`)。', |
| 599 | 641 | ' - 无任何效果 → `interactionFailures[kind="no-observable-effect"]`;JS 异常 → `js-error`;`console.error` → `console-error`;应发未发网络调用 → `missing-docs05-call`。断言用 auto-waiting / `expect.poll`,**不用**固定 sleep。', |
| 600 | 642 | '- **文字层**:动态文字格对比该 region 字段的唯一 sentinel(抓绑错字段)。', |
| 601 | - '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`(硬 halt);双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', | |
| 602 | - '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`。', | |
| 603 | - '', | |
| 604 | - `## step6 证据落盘 + commit(运行时行为,沿用 test-gate 证据 commit 习惯)`, | |
| 605 | - `- 写 \`${evidence}\`:推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`, | |
| 606 | - `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/module-reports/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, | |
| 643 | + '- **绑定垃圾分级**:`null` / `undefined` / `[object Object]` / `NaN` / `lorem` 出现在绑定位 → `interactionFailures[kind="binding-garbage"]`;双花括号未渲染 / 空占位 `—` / 疑似 i18n key → `textIssues`(走 adjudicate;i18n 类额外加载真实 locale 比对)。', | |
| 644 | + '- **文字不符按来源分流到 source**:绑定 sentinel 不符 → `source="sentinel"`(客观 bug,转 must-fix);i18n key / 字面 / 语义类 → `source="i18n"|"literal"|"semantic"`(软文字,走仲裁,永不阻断 approve)。', | |
| 645 | + '- **行为硬问题必须带源码 locator(转 must-fix 喂 fix 的前置)**:', | |
| 646 | + ' - **A 类(可反查到组件文件)**:经 route → router 配置 → view 组件文件反查到**组件级文件路径**。`interactionFailures[].locator` = `<组件文件路径>`(可附 DOM 选择器 / 绑定文本片段,写进 `detail`);`detail` 写「失败 kind + 期望端点/期望 sentinel 值 + 实际渲染值 + DOM 路径 + 绑定片段」,供 fix 子代理在该组件内 Grep 定位 handler/绑定。binding-garbage / sentinel-mismatch 同样附 DOM 路径 + 绑定片段 + 期望 sentinel + 实际渲染值。', | |
| 647 | + ' - **B 类(连组件文件都反查不出)**:**不静默降级放行**——归 `coverageGaps[reason="locator-not-resolvable"]`(计入未覆盖,使本轮不能判 green),或归 `envError.kind="stack-not-ready"` 走 retry。绝不把无 locator 的硬问题塞进 `interactionFailures` 不带 locator(上层会因无 locator 走 adjudicate(allowContinue:false),绝不放行)。', | |
| 648 | + '', | |
| 649 | + `## step6 证据落盘 + commit(运行时行为,沿用证据 commit 习惯)`, | |
| 650 | + `- 写 \`${evidence}\`:本 FE feScope / 推导期望 / 逐控件判定 / routesPlanned-Reached-controlsEnumerated / authState(含未覆盖角色集)/ coverageGaps / 截图。`, | |
| 651 | + `- 截图归档到**已纳入版本管理**的 \`docs/superpowers/reviews/assets/...\`(**不要**引用 \`.tmp\` 防断链)。`, | |
| 607 | 652 | `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`, |
| 608 | - commitBlock(`${evidence} docs/superpowers/module-reports/assets`, | |
| 609 | - `docs(behavior-gate:r${attempt}): 前端行为门证据`), | |
| 653 | + commitBlock(`${evidence} docs/superpowers/reviews/assets`, | |
| 654 | + `docs(behavior:${safeId}:r${behaviorRound}-a${attempt}): per-FE 行为验收证据`), | |
| 610 | 655 | '', |
| 611 | 656 | '## 输出(必须符合下发的 BEHAVIOR_GATE JSON schema)', |
| 612 | - '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无 envError + 覆盖非空)| `red`。', | |
| 613 | - '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(空覆盖必须可见)。', | |
| 614 | - '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举。', | |
| 615 | - '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids。', | |
| 657 | + '- `status`: `green`(交互层无失败 + 文字层无 sentinel 类失败 + 无阻断性 envError + 本 FE 覆盖非空)| `red`。', | |
| 658 | + '- `routesPlanned` / `routesReached` / `controlsEnumerated`: 整数,据实填(**只数本 FE feScope**;空覆盖必须可见)。', | |
| 659 | + '- `interactionFailures` / `textIssues` / `coverageGaps`: 见 schema 的 kind / source / reason 枚举;硬问题 A 类带 `locator`。', | |
| 660 | + '- `envError`: 无环境问题填 `{ "kind": "none" }`;有则填对应 kind + detail + ports + pids;`build-failed` 时填 `rootCausePath`。', | |
| 616 | 661 | '- 做过任何自主默认 → `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。', |
| 617 | 662 | '- 不要返回额外字段(schema 是 `additionalProperties:false`)。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', |
| 618 | 663 | ].filter(Boolean).join('\n') |
| 619 | 664 | } |
| 620 | 665 | |
| 666 | +// ---- 前端骨架占位 stage(runFrontendSkeleton 用)---- | |
| 667 | +// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 2(前置依赖 A,blocker)。 | |
| 668 | +// 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(未实现 FE 路由指向 FeStub 占位) | |
| 669 | +// + 不指悬空 path 的共享导航——保证「前端只建了一部分」的任意时刻 app 仍可构建可起、每个 FE 路由可达。 | |
| 670 | +// 由此 per-FE 行为门的「可构建前提」成立、tddPrompt 的占位替换有真值起点、build-failed 退化为罕见兜底。 | |
| 671 | +// feItems:本前端阶段的全部 FE-NN(来自 Router 的 frontend-phase 聚合模块),即 router 全量路由表的清单。 | |
| 672 | +function frontendSkeletonPrompt(feItems) { | |
| 673 | + const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(Router 未给 FE 清单——不应出现,调用方仅在 feItems 非空时调用)' | |
| 674 | + return [ | |
| 675 | + '# fe-skeleton — 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位)', | |
| 676 | + '', | |
| 677 | + featureStageContract('frontend'), | |
| 678 | + '', | |
| 679 | + '## 目标', | |
| 680 | + '在逐 FE 实现开始**之前**,一次性建出前端「可构建可起」的骨架:App 外壳 + router **全量** lazy 路由表(每个 FE 路由都声明,未实现的指向占位组件 `FeStub`)+ 不指悬空 path 的共享导航。', | |
| 681 | + '保证后续「只建了一部分 FE」的任意时刻 `vite build` / dev server 都能起、每个 FE 路由都可达(加载到占位);逐 FE 实现时再把对应路由的 import 从 `FeStub` 换成真组件。', | |
| 682 | + '', | |
| 683 | + `## 本前端阶段 FE 清单(router 全量路由表必须覆盖的全部 FE)`, | |
| 684 | + `- ${list}`, | |
| 685 | + '', | |
| 686 | + '## 收集上下文(确定技术栈 + 目录约定 + 路由)', | |
| 687 | + `- \`${ROOT}/docs/04-技术规范.md § 零\`(\`frontend.ui_lib\` / framework / 构建工具)+ \`§ 二 前端规范\`(§ 2.1 目录约定 = 落盘位置 / 路由库 / 入口文件名)。`, | |
| 688 | + `- \`${ROOT}/docs/08-模块任务管理.md § 三\`(前端阶段元数据 + \`功能:\` 下全部 \`FE-NN\` 行;与上面清单核对,以本提示给出的清单为准)。`, | |
| 689 | + `- \`${ROOT}/docs/01-需求清单/\` 各 FE 关联 REQ + \`${ROOT}/prototype/\`(页面/路由结构权威)+ \`${ROOT}/docs/05-API接口契约.md\`,据此推导每个 FE-NN 对应的**路由 path**(带参动态路由保留 \`:id\` 占位)。`, | |
| 690 | + `- 用 Grep 在 \`${ROOT}/frontend/\` 探测现有 App 外壳 / 入口 / router 是否已存在(幂等:已存在则按需补齐,不重复创建/不覆盖已实现的真组件)。`, | |
| 691 | + '', | |
| 692 | + '## 产出(全部落在 `frontend/` 路径内——遵守前端阶段路径作用域护栏)', | |
| 693 | + '1. **App 外壳 + 入口**:`frontend/src/App.*` 与入口 `frontend/src/main.*`(按 framework / docs/04 约定的扩展名;不存在才创建)。挂载共享布局 + `<router-view>`(或等价 outlet)。', | |
| 694 | + '2. **router 全量路由表**(按 docs/04 § 2.1 约定的路由文件位置,如 `frontend/src/router/index.*`):', | |
| 695 | + ' - **每个** FE-NN 对应路由都声明,**全部用 lazy import**(`component: () => import(...)` 或 framework 等价的动态 import;**绝不** eager `import X from ...` 顶部静态引入,否则未建组件会让整表编译失败)。', | |
| 696 | + ' - **未实现的 FE 路由全部指向占位组件 `FeStub`**:`component: () => import("../views/_stub/FeStub.vue")`(或 framework 等价)。逐 FE 实现后由 tdd stage 把对应路由 import 换成真组件。', | |
| 697 | + ' - 路由 path 取自上面推导的 FE→path 映射;带参路由用 `:id` 等占位。', | |
| 698 | + '3. **占位组件 `FeStub`**:`frontend/src/views/_stub/FeStub.vue`(framework 非 Vue 时落对应等价文件,如 `FeStub.tsx`),最小渲染一个带 `data-fe-stub` 属性的元素(如 `<div data-fe-stub>占位</div>`;行为门据 `data-fe-stub` 识别占位态)。**不实现任何业务逻辑**。', | |
| 699 | + '4. **共享布局/导航**:导航链接**全部指向已在 router 声明的路由 path**(不指向任何不存在的 path),保证任意时刻无悬空链接。', | |
| 700 | + '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', | |
| 701 | + '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', | |
| 702 | + '', | |
| 703 | + '## 自检(可构建)', | |
| 704 | + '- 推断本项目前端 build / typecheck 命令(docs/04 § 零 / `frontend/package.json` scripts)。若可在子会话内安全跑(不挂死),**派 Agent 子会话**跑一次 build / dev-server 就绪探测确认骨架可构建可起;不可行则至少静态核对「全部 FE 路由已声明 + 全 lazy + 导航无悬空 path + FeStub 存在」。', | |
| 705 | + '- 占位符扫描:`TBD` / `TODO` / `【人工填写:】` → 命中即修。', | |
| 706 | + '', | |
| 707 | + commitBlock('frontend/', 'feat(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位', | |
| 708 | + '- commit 失败 → halt,把 stderr 摘要写进 reason。'), | |
| 709 | + '', | |
| 710 | + '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', | |
| 711 | + '- 成功:`{ "status": "ok", "summary": "<已声明的 FE 路由数 / 入口与 router 文件路径摘要>" }`(artifactPath 可省)。', | |
| 712 | + '- 任一护栏 / 缺值(如无法推导某 FE 的路由 path 且无任何旁证)→ `{ "status": "halt", "reason": "<具体阻塞点>" }`。', | |
| 713 | + '- 做过自主默认 → `decisions[]` 逐条登记;schema 是 `additionalProperties:false`,不要返回额外字段。', | |
| 714 | + ].filter(Boolean).join('\n') | |
| 715 | +} | |
| 716 | + | |
| 717 | +// fe-skeleton 幂等判定:检测 router 是否已声明本阶段全部 FE 路由(全量 + 全 lazy)。 | |
| 718 | +// fe-skeleton-done tag 是首选 ground truth(下面 runFrontendSkeleton 先查 tag);此 prompt 用于 tag 缺失时 | |
| 719 | +// 的二次确认(resume / tag 被手工删除场景),避免无谓重建已建好的骨架。 | |
| 720 | +function frontendSkeletonStatePromptM(feItems) { | |
| 721 | + const list = (feItems || []).map(x => `\`${x}\``).join(', ') || '(无)' | |
| 722 | + return [ | |
| 723 | + '# 检测前端骨架是否已建(router 已声明全部 FE 路由 + 全 lazy)', | |
| 724 | + microStepContract(), | |
| 725 | + '', | |
| 726 | + `用 Grep / Read 检查 \`${ROOT}/frontend/\`:是否已存在 router 配置文件,且其中**本阶段全部 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),且占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在。`, | |
| 727 | + '- 全部满足(骨架已建齐)→ `{ "exists": true }`', | |
| 728 | + '- 任一缺失(无 router / 缺某 FE 路由 / 存在 eager import / 无 FeStub)→ `{ "exists": false }`', | |
| 729 | + '## 输出(EXISTS_SCHEMA)', | |
| 730 | + ].join('\n') | |
| 731 | +} | |
| 732 | + | |
| 621 | 733 | // ---- 微步骤 prompt builders(runBranchSetup / runMilestone / runCrossModule 用)---- |
| 622 | 734 | // 每个 prompt 单职责、短文本;返回严格 schema;执行(action)步统一返回 ACTION_RESULT_SCHEMA。 |
| 623 | 735 | function microStepContract() { |
| ... | ... | @@ -639,7 +751,12 @@ function microStepContract() { |
| 639 | 751 | // ============================================================================ |
| 640 | 752 | |
| 641 | 753 | const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环) |
| 642 | -const BEHAVIOR_GATE_PASS_MAX = ADJUDICATE_MAX * 4 // 行为门 ①②③ 整体收敛轮上限:每次文字层 retry 跳回从头过硬门,超出则确定性 halt(防无限循环) | |
| 754 | +// per-FE 行为子门预算(二维,钉死防证据覆盖;设计 §6.4): | |
| 755 | +// - BEHAVIOR_FE_MAX = approve 子门内的行为 fix 轮硬上限(每 FE);超限 throw HALT。**不**复用 review 的 10 轮、 | |
| 756 | +// **不**让 REVIEW_HARD_ROUNDS × 行为重试隐式相乘——典型一次过(1 轮),最坏 3 轮。 | |
| 757 | +// - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 | |
| 758 | +const BEHAVIOR_FE_MAX = 3 | |
| 759 | +const BEHAVIOR_ATTEMPT_MAX = 2 | |
| 643 | 760 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' |
| 644 | 761 | |
| 645 | 762 | // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 |
| ... | ... | @@ -978,6 +1095,19 @@ function createTagPromptM(phaseId, fe) { |
| 978 | 1095 | ].join('\n') |
| 979 | 1096 | } |
| 980 | 1097 | |
| 1098 | +// fe-skeleton-done:前端骨架占位 stage 的幂等真值 tag(runFrontendSkeleton resume 跳过用)。 | |
| 1099 | +function createFeSkeletonTagPromptM() { | |
| 1100 | + return [ | |
| 1101 | + '# 打 annotated tag `fe-skeleton-done`(前端骨架占位已建,幂等真值)', | |
| 1102 | + microStepContract(), | |
| 1103 | + '', | |
| 1104 | + `先用 \`git -C ${ROOT} tag -l fe-skeleton-done\` 检查;已存在则视为成功(幂等)直接返回 success。`, | |
| 1105 | + `否则跑 \`git -C ${ROOT} tag -a fe-skeleton-done -m "chore(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位已建"\`。`, | |
| 1106 | + '## 输出(ACTION_RESULT_SCHEMA)', | |
| 1107 | + '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`', | |
| 1108 | + ].join('\n') | |
| 1109 | +} | |
| 1110 | + | |
| 981 | 1111 | function findReportPromptM(phaseId) { |
| 982 | 1112 | return [ |
| 983 | 1113 | `# 找最新的 \`${phaseId}\` 完成报告并读取 § ⑫ 的 milestone tag 字段当前值`, |
| ... | ... | @@ -1093,7 +1223,7 @@ function reportPrompt(module) { |
| 1093 | 1223 | '## 前置', |
| 1094 | 1224 | `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, |
| 1095 | 1225 | fe |
| 1096 | - ? `- **验证上游 behavior-gate(前端行为门)绿**:Glob \`${ROOT}/docs/superpowers/module-reports/frontend-phase-behavior-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须非 RED**(status:green / 无 envError);最后一份 red 立即 halt。各 attempt 的 flake / 环境 race / 文字 continue 记录纳入 § ⑤ 汇总。` | |
| 1226 | + ? `- **前端行为验收已并入 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(仅在 § ⑤/⑧ 标注)。` | |
| 1097 | 1227 | : '', |
| 1098 | 1228 | '', |
| 1099 | 1229 | '## 收集输入(取摘要而非正文)', |
| ... | ... | @@ -1103,8 +1233,8 @@ function reportPrompt(module) { |
| 1103 | 1233 | `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, |
| 1104 | 1234 | `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, |
| 1105 | 1235 | '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', |
| 1106 | - `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。**另把 \`frontend-phase-behavior-gate-r*.md\` 各 attempt(按序)的 flake / 环境 race(envError)/ 文字 continue 记录一并纳入 § ⑤ 汇总**。`, | |
| 1107 | - `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外纳入 behavior-gate 报告的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, | |
| 1236 | + `- § ⑤:把 \`${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 记录一并纳入 § ⑤ 汇总**。`, | |
| 1237 | + `- § ⑧ 偏离清单:审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出;**额外按 per-FE 行为证据 \`${ROOT}/docs/superpowers/reviews/<date>-FE-*-behavior-r*-a*.md\` 汇总各 FE 的 \`coverageGaps\` + 文字 \`textIssues\` 的 continue 记录 + 逐控件判定摘要 + authState 未覆盖角色集**。`, | |
| 1108 | 1238 | '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', |
| 1109 | 1239 | ].join('\n') |
| 1110 | 1240 | : [ |
| ... | ... | @@ -1274,6 +1404,39 @@ async function runCrossModule(module) { |
| 1274 | 1404 | log(`cross-module-log: 模块 ${id} 更新 ${classified.crossModule.length} 行`) |
| 1275 | 1405 | } |
| 1276 | 1406 | |
| 1407 | +// ---- runFrontendSkeleton:前端骨架占位 stage 的 JS 编排(设计 § 2,前置依赖 A)---- | |
| 1408 | +// 在 featureLoop(frontend) 之前一次性建出 App 外壳 + router 全量 lazy 路由表(FeStub 占位)+ 无悬空导航。 | |
| 1409 | +// 幂等(resume 安全):先查 git tag `fe-skeleton-done`,已存在则 skip;tag 缺失时再二次确认 router 是否已声明 | |
| 1410 | +// 全 FE 路由(手工删 tag / 残留场景),已建则补打 tag 后 skip;都未建才派子代理生成,成功后打 tag。 | |
| 1411 | +async function runFrontendSkeleton(feItems) { | |
| 1412 | + const lbl = (k) => `fe-skeleton:${k}` | |
| 1413 | + | |
| 1414 | + // step 1: tag 幂等(首选 ground truth) | |
| 1415 | + const tag = await agent(checkTagExistsPromptM('fe-skeleton-done'), | |
| 1416 | + {label: lbl('tag?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) | |
| 1417 | + if (tag.exists) { log('fe-skeleton: tag fe-skeleton-done 已存在,跳过骨架生成'); return } | |
| 1418 | + | |
| 1419 | + // step 2: tag 缺失时二次确认 router 是否已声明全 FE 路由(手工删 tag / resume 残留);已建则只补打 tag。 | |
| 1420 | + const state = await agent(frontendSkeletonStatePromptM(feItems), | |
| 1421 | + {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) | |
| 1422 | + if (state.exists) { | |
| 1423 | + log('fe-skeleton: router 已声明全部 FE 路由(tag 缺失但骨架已建),补打 fe-skeleton-done tag') | |
| 1424 | + await runAction(g => createFeSkeletonTagPromptM() + g, | |
| 1425 | + {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag-backfill')}) | |
| 1426 | + return | |
| 1427 | + } | |
| 1428 | + | |
| 1429 | + // step 3: 派子代理生成骨架(成功后子代理自行 commit;此处仅经 runStage 仲裁 halt 收敛)。 | |
| 1430 | + await runStage(g => frontendSkeletonPrompt(feItems) + g, | |
| 1431 | + {site:'fe-skeleton', grp:'Frontend', label: lbl('gen')}) | |
| 1432 | + | |
| 1433 | + // step 4: 打 fe-skeleton-done tag(幂等真值,resume 跳过)。 | |
| 1434 | + await runAction(g => createFeSkeletonTagPromptM() + g, | |
| 1435 | + {site:'fe-skeleton-tag', grp:'Frontend', label: lbl('tag')}) | |
| 1436 | + | |
| 1437 | + log(`fe-skeleton: 已生成前端骨架(覆盖 ${(feItems || []).length} 个 FE 路由),打 fe-skeleton-done tag`) | |
| 1438 | +} | |
| 1439 | + | |
| 1277 | 1440 | // ============================================================================ |
| 1278 | 1441 | // 编排逻辑(结构按 plan 骨架;featureLoop / reviewWithFixLoop / testGate / 顶层循环) |
| 1279 | 1442 | // ============================================================================ |
| ... | ... | @@ -1356,7 +1519,7 @@ async function featureLoop(items, phase) { |
| 1356 | 1519 | // reverify 经 runStage 但 allowContinue:false(复验红色 = 修复没生效,绝不放行)。 |
| 1357 | 1520 | // - approve 后的 docs/08 checkbox 是纯可视化副作用(req-done tag 才是完成真值),缺失/写失败一律 log 跳过不 halt。 |
| 1358 | 1521 | const REVIEW_SOFT_ROUNDS = 5 |
| 1359 | -const REVIEW_HARD_ROUNDS = 8 | |
| 1522 | +const REVIEW_HARD_ROUNDS = 10 | |
| 1360 | 1523 | |
| 1361 | 1524 | // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 |
| 1362 | 1525 | async function flipDocs08Checkbox(fe, id, phase, grp) { |
| ... | ... | @@ -1374,6 +1537,9 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { |
| 1374 | 1537 | let lastVerify = verifyResult |
| 1375 | 1538 | let lastIssuesCount = 0 |
| 1376 | 1539 | let reviewGuidance = '' // 仲裁 retry 时注入下一轮 review 的纠正指令 |
| 1540 | + // softPassed 提升到 reviewWithFixLoop 顶层作用域(与本 FE review 同寿命,跨 behaviorRound 持久)—— | |
| 1541 | + // 行为软文字一旦被仲裁 continue 放行(降级),重跑后即便仍在 textIssues 也不再追问,避免反复消耗仲裁预算。 | |
| 1542 | + const behaviorSoftPassed = new Set() | |
| 1377 | 1543 | for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { |
| 1378 | 1544 | const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' |
| 1379 | 1545 | // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 |
| ... | ... | @@ -1384,6 +1550,13 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { |
| 1384 | 1550 | reviewGuidance = '' // 已消费 |
| 1385 | 1551 | |
| 1386 | 1552 | if (r.verdict === 'approve') { |
| 1553 | + // approve 闸显式 AND(设计 §6.2):reviewer.verdict==='approve' ∧ behaviorSubGate green(仅前端)。 | |
| 1554 | + // 后端逐字不变(无行为维度);前端:静态 approve 后**不立即 return**,先进 per-FE 行为 approve 子门—— | |
| 1555 | + // 起本 FE 全栈验「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才放行; | |
| 1556 | + // 行为 green ⇒ 才 flipDocs08Checkbox + return(req-done tag 落点 featureLoop 不动,语义自动升级为「静态过+行为过」)。 | |
| 1557 | + if (fe) { | |
| 1558 | + await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) | |
| 1559 | + } | |
| 1387 | 1560 | await flipDocs08Checkbox(fe, id, phase, grp) |
| 1388 | 1561 | return { id, phase, approved:true, rounds:round } |
| 1389 | 1562 | } |
| ... | ... | @@ -1397,7 +1570,11 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { |
| 1397 | 1570 | const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`, |
| 1398 | 1571 | { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)', |
| 1399 | 1572 | reviewerIssues: r.issues || [] }, grp, round) |
| 1400 | - if (verdict.action === 'continue') { await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } } | |
| 1573 | + // continue 视为「无 must-fix → 静态 approve」——前端仍须先过行为 approve 子门(行为 green 是任何 approve return 的前置)。 | |
| 1574 | + if (verdict.action === 'continue') { | |
| 1575 | + if (fe) await behaviorSubGate(id, specPath, grp, behaviorSoftPassed) | |
| 1576 | + await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } | |
| 1577 | + } | |
| 1401 | 1578 | if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`) |
| 1402 | 1579 | reviewGuidance = verdict.guidance || '' // retry:带 guidance 重判(进入下一轮) |
| 1403 | 1580 | continue |
| ... | ... | @@ -1449,136 +1626,170 @@ async function testGate(module, phase) { |
| 1449 | 1626 | return g |
| 1450 | 1627 | } |
| 1451 | 1628 | |
| 1452 | -// ---- 前端行为门控制流(runBehaviorGate)---- | |
| 1453 | -// 设计:docs/design/2026-06-02-frontend-behavior-gate.md § 4。 | |
| 1454 | -// 仅 frontend-phase 触发(入口二次保险);每 attempt 独立 .tmp 子目录(门子代理负责清/写)。 | |
| 1455 | -// 失败分层: | |
| 1456 | -// - envError != none(端口/起栈未就绪/种子/鉴权/超时)= 环境 race:同 testGate 跑 attempt=2;仍 envError → | |
| 1457 | -// adjudicate(allowContinue:false, retry/halt);retry 再起独立 attempt;绝不当死控件。 | |
| 1458 | -// - 空覆盖(controlsEnumerated==0 || routesReached==0)→ 绝不 green,归 env race 走 retry/halt。 | |
| 1459 | -// - interactionFailures(含 binding-garbage)= 交互硬边界:attempt=1 出现不立刻 throw,先跑 attempt=2 辨 flake; | |
| 1460 | -// 仍非空 → adjudicate(allowContinue:false, retry/halt),绝不 continue。 | |
| 1461 | -// - textIssues(软边界):逐条 for-of —— source=='sentinel' → adjudicate(allowContinue:false)(客观 bug,只许 retry/halt); | |
| 1462 | -// source∈{i18n,literal,semantic} → adjudicate(allowContinue:true)(continue 时 recordDecisions 记入 autonomousDecisions)。 | |
| 1463 | -// - coverageGaps:写证据 + recordDecisions,不单独 halt。 | |
| 1464 | -// RED 在 milestone tag 前 throw 冒泡到顶层 try/catch → break,绝不带红进里程碑。 | |
| 1465 | -async function runBehaviorGate(module) { | |
| 1466 | - // 入口二次保险:仅 frontend-phase 聚合模块跑行为门(同 runMilestone / reportPrompt 惯例)。 | |
| 1467 | - const fe = module?.id === 'frontend-phase' | |
| 1468 | - if (!fe) { log(`behavior-gate: ${module?.id ?? '<module>'} 非 frontend-phase,跳过行为门`); return } | |
| 1469 | - const lbl = (a) => `behavior:frontend-phase:r${a}` | |
| 1629 | +// ---- 前端 per-FE 行为验收控制流(runBehaviorGateOnce + behaviorSubGate)---- | |
| 1630 | +// 设计:docs/design/2026-06-02-frontend-behavior-in-review-loop.md § 6.3 / 7。 | |
| 1631 | +// 行为验收并入 per-FE reviewWithFixLoop 的 approve 子门——reviewer 即将 approve 时才触发,绝不每 review round 起栈。 | |
| 1632 | +// behaviorSubGate 失败分层(per-FE 缩 scope,保留原 runBehaviorGate 的分层语义): | |
| 1633 | +// - build-failed(兄弟 FE 未实现 / 占位未覆盖,根因落非本 FE 路径)= 确定性短路:记 coverageGap + decisions, | |
| 1634 | +// 本轮行为门视为「本 FE 非缺陷」直接放行 approve(预期中途态,不 retry 不 halt)。 | |
| 1635 | +// - envError(其它) / 空覆盖 = 环境 race:runBehaviorGateOnce 内部 attempt 1→2 重试;仍异常 → adjudicate(allowContinue:false)。 | |
| 1636 | +// - 软文字(i18n/literal/semantic) → adjudicate(continue 记 decisions + 跨 behaviorRound softPassed;sentinel 并入 behaviorHard);永不阻断 approve。 | |
| 1637 | +// - behaviorHard = interactionFailures + sentinel textIssues:有 locator → 降维喂 fixPrompt 跑 fix(fix 后功能 reverify + 下一轮重跑行为); | |
| 1638 | +// 无 locator → adjudicate(allowContinue:false) retry/halt,绝不静默丢弃、绝不 approve。 | |
| 1639 | +// - BEHAVIOR_FE_MAX 轮仍未 green → throw HALT behavior-unresolved(冒泡到顶层 try/catch → fail-fast)。 | |
| 1640 | + | |
| 1641 | +// envBlocked / ifails:per-FE bg 的环境/空覆盖与交互失败判定(build-failed 不计 envBlocked——它走确定性短路分支)。 | |
| 1642 | +function behaviorEnvBlocked(r) { | |
| 1643 | + const k = r.envError && r.envError.kind | |
| 1644 | + const ev = (k && k !== 'none' && k !== 'build-failed') ? r.envError : null | |
| 1645 | + const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0) | |
| 1646 | + return { ev, emptyCov, blocked: !!ev || emptyCov } | |
| 1647 | +} | |
| 1648 | +function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] } | |
| 1470 | 1649 | |
| 1650 | +// runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 | |
| 1651 | +// 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。 | |
| 1652 | +// behaviorRound:approve 子门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 | |
| 1653 | +async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) { | |
| 1654 | + const lbl = (a) => `behavior:${id}:r${behaviorRound}:a${a}` | |
| 1471 | 1655 | let attempt = 1 |
| 1472 | - let bg = await agent(behaviorGatePrompt(module, attempt), {label: lbl(attempt), phase:'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | |
| 1473 | - recordDecisions('behavior-gate:frontend-phase', bg.decisions) | |
| 1656 | + let bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), | |
| 1657 | + {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) | |
| 1658 | + recordDecisions(`behavior:${id}`, bg.decisions) | |
| 1659 | + | |
| 1660 | + // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。 | |
| 1661 | + const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed' | |
| 1662 | + if (isBuildFailedShortCircuit(bg)) return bg | |
| 1474 | 1663 | |
| 1475 | - // 共享重跑:每次 retry 都开一个独立 attempt(独立 .tmp/r<attempt> 证据),刷新 bg + 记录决策。 | |
| 1476 | - // 文字层 retry 也走这里,确保重跑后能跳回 ①②③ 重新整体过硬门(见下方收敛循环),绝不拿旧快照继续。 | |
| 1477 | - const rerun = async () => { | |
| 1664 | + // 内部 envError / 空覆盖重试:attempt 1→BEHAVIOR_ATTEMPT_MAX(沿用 testGate 思路);仍异常 → adjudicate(allowContinue:false)。 | |
| 1665 | + while (behaviorEnvBlocked(bg).blocked && attempt < BEHAVIOR_ATTEMPT_MAX) { | |
| 1478 | 1666 | attempt += 1 |
| 1479 | - bg = await agent(behaviorGatePrompt(module, attempt), {label: lbl(attempt), phase:'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) | |
| 1480 | - recordDecisions('behavior-gate:frontend-phase', bg.decisions) | |
| 1667 | + bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), | |
| 1668 | + {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) | |
| 1669 | + recordDecisions(`behavior:${id}`, bg.decisions) | |
| 1670 | + if (isBuildFailedShortCircuit(bg)) return bg | |
| 1481 | 1671 | } |
| 1482 | - | |
| 1483 | - // helper:环境 race / 空覆盖归一处理——先跑一次 flake 重试,仍异常则 adjudicate(allowContinue:false)。 | |
| 1484 | - const envBlocked = (r) => { | |
| 1485 | - const ev = r.envError && r.envError.kind && r.envError.kind !== 'none' ? r.envError : null | |
| 1486 | - const emptyCov = (Number(r.controlsEnumerated) === 0) || (Number(r.routesReached) === 0) | |
| 1487 | - return { ev, emptyCov, blocked: !!ev || emptyCov } | |
| 1672 | + let envState = behaviorEnvBlocked(bg) | |
| 1673 | + for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { | |
| 1674 | + const reason = envState.ev | |
| 1675 | + ? `behavior envError=${envState.ev.kind}: ${envState.ev.detail || ''}` | |
| 1676 | + : `behavior 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)` | |
| 1677 | + const verdict = await adjudicate(`behavior-env:${id}`, | |
| 1678 | + { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, grp, adj) | |
| 1679 | + if (verdict.action !== 'retry') throw new Error(`HALT behavior-env ${id}: ${verdict.rationale || reason}`) | |
| 1680 | + attempt += 1 | |
| 1681 | + bg = await agent(behaviorGatePrompt(id, specPath, behaviorRound, attempt), | |
| 1682 | + {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) | |
| 1683 | + recordDecisions(`behavior:${id}`, bg.decisions) | |
| 1684 | + if (isBuildFailedShortCircuit(bg)) return bg | |
| 1685 | + envState = behaviorEnvBlocked(bg) | |
| 1488 | 1686 | } |
| 1489 | - const ifails = (r) => Array.isArray(r.interactionFailures) ? r.interactionFailures : [] | |
| 1490 | - | |
| 1491 | - // ① 环境 / 空覆盖硬门:仍异常 → 仲裁(allowContinue:false → retry/halt)。 | |
| 1492 | - // 抽成闭包,便于文字层 retry 后由收敛循环重新整体校验(绝不带空覆盖 / envError 判 green)。 | |
| 1493 | - const enforceEnv = async () => { | |
| 1494 | - let envState = envBlocked(bg) | |
| 1495 | - for (let adj = 1; envState.blocked && adj <= ADJUDICATE_MAX; adj++) { | |
| 1496 | - const reason = envState.ev | |
| 1497 | - ? `behavior-gate envError=${envState.ev.kind}: ${envState.ev.detail || ''}` | |
| 1498 | - : `behavior-gate 空覆盖:routesReached=${bg.routesReached} controlsEnumerated=${bg.controlsEnumerated}(绝不带空覆盖判 green)` | |
| 1499 | - const verdict = await adjudicate('behavior-gate-env:frontend-phase', | |
| 1500 | - { problem: reason, envError: bg.envError || null, ports:(bg.envError||{}).ports, pids:(bg.envError||{}).pids, allowContinue:false }, 'Behavior', adj) | |
| 1501 | - if (verdict.action !== 'retry') throw new Error(`HALT behavior-gate-env frontend-phase: ${verdict.rationale || reason}`) | |
| 1502 | - await rerun() | |
| 1503 | - envState = envBlocked(bg) | |
| 1687 | + if (envState.blocked) throw new Error(`HALT behavior-env ${id}: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) | |
| 1688 | + return bg | |
| 1689 | +} | |
| 1690 | + | |
| 1691 | +// behaviorSubGate:reviewer approve 的「行为 approve 子门」。green 才允许 reviewWithFixLoop return approve。 | |
| 1692 | +// softPassed:由 reviewWithFixLoop 顶层注入,跨 behaviorRound 持久(软文字一旦放行不再追问)。 | |
| 1693 | +// green ≡ behaviorHard.length===0 ∧ envError∈{none,build-failed} ∧ 本 FE 覆盖非空(或 build-failed 短路)。 | |
| 1694 | +async function behaviorSubGate(id, specPath, grp, softPassed) { | |
| 1695 | + const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` | |
| 1696 | + for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) { | |
| 1697 | + const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound) | |
| 1698 | + | |
| 1699 | + // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → 记 coverageGap + decisions,子门 green-by-skip 放行。 | |
| 1700 | + if (bg.envError && bg.envError.kind === 'build-failed') { | |
| 1701 | + recordDecisions(`behavior-build-failed:${id}`, [{ | |
| 1702 | + question:`本 FE ${id} 行为验收遇 build-failed(根因 ${bg.envError.rootCausePath || '?'})`, | |
| 1703 | + choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)', | |
| 1704 | + rationale: bg.envError.detail || '', confidence:'low' }]) | |
| 1705 | + log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${bg.envError.rootCausePath || '?'}),记证据不阻断`) | |
| 1706 | + return | |
| 1504 | 1707 | } |
| 1505 | - if (envState.blocked) throw new Error(`HALT behavior-gate-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) | |
| 1506 | - } | |
| 1507 | 1708 | |
| 1508 | - // ② 交互层硬门(含 binding-garbage):仍非空 → 仲裁(allowContinue:false → retry/halt),绝不 continue。 | |
| 1509 | - // 每次 retry 重跑后可能新冒环境问题,由收敛循环回到 ① 兜底,避免把环境 race 当死控件。 | |
| 1510 | - const enforceInteraction = async () => { | |
| 1511 | - for (let adj = 1; ifails(bg).length && adj <= ADJUDICATE_MAX; adj++) { | |
| 1512 | - const summary = ifails(bg).map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') | |
| 1513 | - const verdict = await adjudicate('behavior-gate-interaction:frontend-phase', | |
| 1514 | - { problem:`behavior-gate 交互层失败(含 binding-garbage 硬边界,绝不 continue):${summary}`, | |
| 1515 | - interactionFailures: ifails(bg), allowContinue:false }, 'Behavior', adj) | |
| 1516 | - if (verdict.action !== 'retry') | |
| 1517 | - throw new Error(`HALT behavior-gate-interaction frontend-phase: ${verdict.rationale || summary}`) | |
| 1518 | - await rerun() | |
| 1519 | - await enforceEnv() // 重跑后先过 ① 环境兜底,再回到本门继续辨交互 | |
| 1709 | + // 2) coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 runBehaviorGateOnce 兜底)。 | |
| 1710 | + // locator-not-resolvable(B 类硬问题反查不出)计入未覆盖——下面会因 behaviorHard 仍非空或覆盖不足而不 green。 | |
| 1711 | + for (const cg of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { | |
| 1712 | + if (!cg) continue | |
| 1713 | + recordDecisions(`behavior-coverage:${id}`, | |
| 1714 | + [{ question:`覆盖缺口 ${cg.page}(${cg.reason})`, choice:'记录不阻断', rationale: cg.detail || '', confidence:'low' }]) | |
| 1520 | 1715 | } |
| 1521 | - if (ifails(bg).length) | |
| 1522 | - throw new Error(`HALT behavior-gate-interaction frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后交互层仍有失败`) | |
| 1523 | - } | |
| 1524 | 1716 | |
| 1525 | - // attempt=1 出现环境/交互问题不立刻 throw——先跑独立 attempt 辨 flake,再进入硬门收敛。 | |
| 1526 | - if (envBlocked(bg).blocked || ifails(bg).length) await rerun() | |
| 1527 | - | |
| 1528 | - // ①②③ 收敛循环:任何文字层 retry 重跑都跳回此处,重新整体校验 | |
| 1529 | - // envError → 空覆盖 → interactionFailures → textIssues(while 而非 for-of 快照), | |
| 1530 | - // 杜绝「文字 retry 后用旧数组继续、且新 bg 携带非空 interactionFailures/envError 滑过硬门」的逃逸。 | |
| 1531 | - // softPassed:已被仲裁 continue 放行(降级)的 region,重跑后即便仍在 textIssues 也不再追问,避免死循环。 | |
| 1532 | - const softPassed = new Set() | |
| 1533 | - let converged = false | |
| 1534 | - for (let pass = 1; pass <= BEHAVIOR_GATE_PASS_MAX && !converged; pass++) { | |
| 1535 | - // ① 环境 / 空覆盖(硬门) | |
| 1536 | - await enforceEnv() | |
| 1537 | - // ② 交互层(硬门,含 binding-garbage) | |
| 1538 | - await enforceInteraction() | |
| 1539 | - | |
| 1540 | - // ③ 文字层(软边界,按 source 分流):while 取当前 bg 第一条未决 textIssue。 | |
| 1541 | - // source=='sentinel' → allowContinue:false(门自灌确定值,绑错字段 / 显示错是客观 bug,只许 retry/halt); | |
| 1542 | - // source∈{i18n,literal,semantic} → allowContinue:true(continue 时 recordDecisions 记入决策日志)。 | |
| 1543 | - const regionKey = (x) => `${x.page || '?'}::${x.region || '?'}` | |
| 1544 | - const pickIssue = () => (Array.isArray(bg.textIssues) ? bg.textIssues : []) | |
| 1545 | - .find(x => x && !softPassed.has(regionKey(x))) | |
| 1546 | - let needRerun = false | |
| 1547 | - let ti | |
| 1548 | - while ((ti = pickIssue())) { | |
| 1549 | - const hard = ti.source === 'sentinel' | |
| 1550 | - const site = `behavior-gate-text:frontend-phase:${ti.page || '?'}:${ti.region || '?'}` | |
| 1717 | + // 3) 软文字(i18n/literal/semantic)→ 仲裁 continue 记 decisions + softPassed;sentinel 客观 bug 不在此处放行(下面并入 behaviorHard)。 | |
| 1718 | + // 永不阻断 approve;retry/halt 同现。一旦有软文字 retry → 重跑本 behaviorRound(continue 进下一轮迭代)。 | |
| 1719 | + let softRetry = false | |
| 1720 | + for (const ti of (Array.isArray(bg.textIssues) ? bg.textIssues : [])) { | |
| 1721 | + if (!ti || ti.source === 'sentinel') continue // sentinel 归 behaviorHard,不在软文字处理 | |
| 1722 | + if (softPassed.has(regionKey(ti))) continue | |
| 1723 | + const site = `behavior-text:${id}:${ti.page || '?'}:${ti.region || '?'}` | |
| 1551 | 1724 | const verdict = await adjudicate(site, |
| 1552 | - { problem:`文字不符(source=${ti.source}${hard ? ',sentinel 客观 bug 不可 continue' : ',可 continue 降级'}):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, | |
| 1553 | - textIssue: ti, allowContinue: !hard }, 'Behavior', pass) | |
| 1554 | - if (verdict.action === 'continue' && !hard) { | |
| 1555 | - // continue:把放行决策记入 autonomousDecisions(供人工事后审阅),并标记该 region 已软放行。 | |
| 1725 | + { problem:`文字不符(source=${ti.source},可 continue 降级;永不阻断 approve):${ti.page}:${ti.region} 期望=${JSON.stringify(ti.expected)} 实际=${JSON.stringify(ti.actual)}`, | |
| 1726 | + textIssue: ti, allowContinue: true }, grp, behaviorRound) | |
| 1727 | + if (verdict.action === 'continue') { | |
| 1556 | 1728 | recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`, |
| 1557 | 1729 | choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }]) |
| 1558 | - softPassed.add(regionKey(ti)) | |
| 1559 | - continue // 处理同一 bg 内的下一条 | |
| 1730 | + softPassed.add(regionKey(ti)); continue | |
| 1560 | 1731 | } |
| 1732 | + if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || `文字不符 source=${ti.source}`}`) | |
| 1733 | + softRetry = true; break // retry → 重跑本 behaviorRound(跳到下一轮迭代重起整门) | |
| 1734 | + } | |
| 1735 | + if (softRetry) continue | |
| 1736 | + | |
| 1737 | + // 3.5) B 类硬问题(locator-not-resolvable coverageGap):连组件文件都反查不出,不静默放行—— | |
| 1738 | + // 计入未覆盖阻断 approve,走 adjudicate(allowContinue:false) retry/halt(绝不当 green 放行,降级≠放行)。 | |
| 1739 | + const bClass = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []).filter(cg => cg && cg.reason === 'locator-not-resolvable') | |
| 1740 | + if (bClass.length) { | |
| 1741 | + const summary = bClass.map(cg => `${cg.page} — ${cg.detail}`).join('; ') | |
| 1742 | + const verdict = await adjudicate(`behavior-bclass:${id}`, | |
| 1743 | + { problem:`behavior 硬问题连组件文件都反查不出(B 类,不可降级放行,计入未覆盖阻断 approve):${summary}`, | |
| 1744 | + coverageGaps: bClass, allowContinue:false }, grp, behaviorRound) | |
| 1745 | + if (verdict.action !== 'retry') throw new Error(`HALT behavior-bclass ${id}: ${verdict.rationale || summary}`) | |
| 1746 | + continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) | |
| 1747 | + } | |
| 1748 | + | |
| 1749 | + // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。 | |
| 1750 | + const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : []) | |
| 1751 | + .filter(t => t && t.source === 'sentinel') | |
| 1752 | + .map(t => ({ page:t.page, control:t.region, kind:'binding-garbage', detail:`sentinel 不符 期望=${t.expected} 实际=${t.actual}`, locator:t.locator })) | |
| 1753 | + const behaviorHard = [...behaviorIfails(bg), ...sentinelHard] | |
| 1754 | + | |
| 1755 | + // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)→ 子门 green 放行。 | |
| 1756 | + if (behaviorHard.length === 0) { | |
| 1757 | + log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) | |
| 1758 | + return | |
| 1759 | + } | |
| 1760 | + | |
| 1761 | + // 6) 分流:无 locator 的硬问题 → adjudicate(allowContinue:false) retry/halt(绝不静默丢弃、绝不 approve)。 | |
| 1762 | + const withLoc = behaviorHard.filter(x => typeof x.locator === 'string' && x.locator.trim()) | |
| 1763 | + const noLoc = behaviorHard.filter(x => !(typeof x.locator === 'string' && x.locator.trim())) | |
| 1764 | + if (noLoc.length) { | |
| 1765 | + const summary = noLoc.map(f => `[${f.kind}] ${f.page}:${f.control} — ${f.detail}`).join('; ') | |
| 1766 | + const verdict = await adjudicate(`behavior-noloc-hard:${id}`, | |
| 1767 | + { problem:`behavior 硬问题无源码 locator(无法转 must-fix 喂 fix,绝不 continue/approve):${summary}`, | |
| 1768 | + interactionFailures: noLoc, allowContinue:false }, grp, behaviorRound) | |
| 1561 | 1769 | if (verdict.action !== 'retry') |
| 1562 | - throw new Error(`HALT ${site}: ${verdict.rationale || `文字不符 source=${ti.source}`}`) | |
| 1563 | - // retry:重跑整门取最新判定,并跳回 ①②③ 重新整体过全部硬门(绝不拿旧快照继续)。 | |
| 1564 | - await rerun() | |
| 1565 | - needRerun = true | |
| 1566 | - break | |
| 1770 | + throw new Error(`HALT behavior-noloc-hard ${id}: ${verdict.rationale || summary}`) | |
| 1771 | + continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) | |
| 1567 | 1772 | } |
| 1568 | - if (!needRerun) converged = true // 无 retry → 文字层已收敛(全软放行 / 全消失) | |
| 1569 | - } | |
| 1570 | - if (!converged) throw new Error(`HALT behavior-gate-text frontend-phase: ${BEHAVIOR_GATE_PASS_MAX} 轮收敛后文字层仍未解决`) | |
| 1571 | 1773 | |
| 1572 | - // ④ coverageGaps:写证据 + recordDecisions(不单独 halt;空覆盖已在 ① 兜底)。 | |
| 1573 | - for (const g of (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : [])) { | |
| 1574 | - if (!g) continue | |
| 1575 | - recordDecisions('behavior-gate-coverage:frontend-phase', | |
| 1576 | - [{ question:`覆盖缺口 ${g.page}(${g.reason})`, choice:'记录不阻断', rationale: g.detail || '', confidence:'low' }]) | |
| 1577 | - } | |
| 1774 | + // 7) 有 locator 的硬问题 → 降维成 {summary,locator,severity} 喂现有 fixPrompt 跑 fix(schema 不合并、fix 入参合并)。 | |
| 1775 | + const fixIssues = withLoc.map(f => ({ | |
| 1776 | + summary: `[behavior:${f.kind}] ${f.page}:${f.control} — ${f.detail}`, | |
| 1777 | + locator: f.locator, | |
| 1778 | + severity: 'high', | |
| 1779 | + })) | |
| 1780 | + await runStage(g => fixPrompt(id, 'frontend', fixIssues) + g, { | |
| 1781 | + site:`behavior-fix:${id}:r${behaviorRound}`, grp, label:`behavior-fix:${id}:r${behaviorRound}`, | |
| 1782 | + }) | |
| 1578 | 1783 | |
| 1579 | - if (bg.status === 'red') | |
| 1580 | - throw new Error(`HALT behavior-gate-red frontend-phase: 门返回 status:red 但未归入交互/文字/环境分支——拒绝带红进里程碑`) | |
| 1581 | - log(`behavior-gate: frontend-phase green(routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) | |
| 1784 | + // 8) fix 后功能复验(allowContinue:false):behaviorSubGate 的 fix 改的是 frontend/ UI 源码,可能引入功能回归—— | |
| 1785 | + // 先跑 scoped 组件测试 reverify(不起全栈,成本低),红则当功能回归硬边界;绿后下一 behaviorRound 重跑行为验收。 | |
| 1786 | + await runStage( | |
| 1787 | + g => verifyPrompt(id, 'frontend', `(behaviorRound ${behaviorRound} 行为 fix 后功能复验,本轮 must-fix: ${fixIssues.length} 项)`, specPath, REVIEW_HARD_ROUNDS + behaviorRound) + g, | |
| 1788 | + { site:`behavior-reverify:${id}:r${behaviorRound}`, grp, label:`behavior-reverify:${id}:r${behaviorRound}`, allowContinue: false }, | |
| 1789 | + ) | |
| 1790 | + // 进入下一 behaviorRound → 重跑本 FE 行为验收 | |
| 1791 | + } | |
| 1792 | + throw new Error(`HALT behavior-unresolved ${id}: ${BEHAVIOR_FE_MAX} 轮 per-FE 行为子门仍未 green(硬问题未清)`) | |
| 1582 | 1793 | } |
| 1583 | 1794 | |
| 1584 | 1795 | phase('Router') |
| ... | ... | @@ -1638,11 +1849,15 @@ for (const [idx, module] of todo.entries()) { |
| 1638 | 1849 | } |
| 1639 | 1850 | if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块) |
| 1640 | 1851 | phase('Frontend') |
| 1852 | + // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy | |
| 1853 | + // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, | |
| 1854 | + // 使 per-FE 行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 | |
| 1855 | + await runFrontendSkeleton(module.feItems) | |
| 1856 | + // 前端行为验收已并入 featureLoop→reviewWithFixLoop 的 per-FE approve 子门(reviewer approve 时起本 FE 全栈验 | |
| 1857 | + // 「按钮真生效/文字对」,硬问题转可 fix must-fix→重验,行为 green 才打 req-done)——不再有阶段级末尾独立行为门。 | |
| 1641 | 1858 | await featureLoop(module.feItems, 'frontend') |
| 1642 | 1859 | phase('Gate') |
| 1643 | - await testGate(module, 'frontend') | |
| 1644 | - phase('Behavior') // 前端行为门:testGate 绿后、report/milestone 前(仅 frontend-phase 聚合) | |
| 1645 | - await runBehaviorGate(module) | |
| 1860 | + await testGate(module, 'frontend') // 阶段级 testGate(全量回归 vitest+playwright)保留,与 per-FE 行为验收职责正交 | |
| 1646 | 1861 | } |
| 1647 | 1862 | phase('Milestone') |
| 1648 | 1863 | // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— | ... | ... |