Commit 3d54e8834afe257758b3c0e88be353b1647c00c0
1 parent
a68fa13f
coding.mjs: add frontend behavior-gate stage (headless full-stack button/text verification)
New 'Behavior' stage between Gate and Milestone, frontend-phase only, after testGate green and before report/milestone. Verifies every interactive control actually works and every text region shows the right content, independent of the tests the tdd agent wrote. - BEHAVIOR_GATE_SCHEMA / behaviorGateContract() / behaviorGatePrompt() / runBehaviorGate(); reportPrompt now gates the milestone on behavior-gate evidence (frontend-phase-behavior-gate-r*.md, last attempt must be non-RED). - Two-tier failure: interaction defects (incl. binding-garbage) flake-retry once then hard-halt via adjudicate(allowContinue:false); text issues split by source (sentinel=objective -> no continue; i18n/literal/semantic=adjudicable). Convergence loop re-runs the env + interaction hard gates after any text-layer retry so a refreshed result can't slip a green past the hard gates. - Full-stack seeded run: test-DB name guard (deterministic halt, not adjudicated), strict 4-phase ordering (empty DB -> boot backend so Flyway builds schema -> seed -> boot frontend), auth bootstrap via storageState, router-config-driven route discovery, ephemeral .tmp/behavior-gate runner with finally teardown, type-legal per-field-unique sentinels. - agentType uses the plugin-namespaced 'erp-workflow:code-reviewer' (a bare 'code-reviewer' is ambiguous with feature-dev:code-reviewer); README + agents/code-reviewer.md aligned (frontmatter name: stays bare). - Design: docs/design/2026-06-02-frontend-behavior-gate.md. README + coding-start banner updated.
Showing
5 changed files
with
708 additions
and
8 deletions
README.md
| ... | ... | @@ -44,7 +44,9 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 |
| 44 | 44 | │ |
| 45 | 45 | └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) |
| 46 | 46 | runBranchSetup(frontend-phase) → featureLoop(前端,FE-NN,路径限 frontend/) |
| 47 | - → testGate(frontend) → runMilestone(milestone/frontend-phase) | |
| 47 | + → testGate(frontend) → 前端行为闸 behavior-gate(headless 全栈起栈+种子 sentinel, | |
| 48 | + 逐路由枚举控件/文字两层断言:交互失效硬 halt,文字不符按来源仲裁) | |
| 49 | + → runMilestone(milestone/frontend-phase) | |
| 48 | 50 | |
| 49 | 51 | 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start |
| 50 | 52 | ``` |
| ... | ... | @@ -133,7 +135,7 @@ erp-workflow-plugin/ |
| 133 | 135 | |
| 134 | 136 | | Agent | 用途 | 谁调用 | |
| 135 | 137 | |---|---|---| |
| 136 | -| `code-reviewer` | 统一 reviewer。`phase=backend` 跑通用代码审查维度;`phase=frontend` 附加前端 7 维 checklist(prototype 一致性 / design tokens / a11y / 响应式 / 业务校验前端复刻 / API 一致性 / 状态机覆盖,主观维度仅标记明显问题不触发 request-changes)。非交互,返回结构化 verdict,绝不弹窗 | `workflows/coding.mjs` 的 review stage:`agent(..., {agentType:'code-reviewer'})` | | |
| 138 | +| `code-reviewer` | 统一 reviewer。`phase=backend` 跑通用代码审查维度;`phase=frontend` 附加前端 7 维 checklist(prototype 一致性 / design tokens / a11y / 响应式 / 业务校验前端复刻 / API 一致性 / 状态机覆盖,主观维度仅标记明显问题不触发 request-changes)。非交互,返回结构化 verdict,绝不弹窗 | `workflows/coding.mjs` 的 review stage:`agent(..., {agentType:'erp-workflow:code-reviewer'})`(必须带 `erp-workflow:` 插件命名空间——裸 `code-reviewer` 会与其它插件的同名 agent 歧义) | | |
| 137 | 139 | |
| 138 | 140 | ## Templates 清单(25 份) |
| 139 | 141 | ... | ... |
agents/code-reviewer.md
| 1 | 1 | --- |
| 2 | 2 | name: code-reviewer |
| 3 | 3 | description: | |
| 4 | - Unified code reviewer for the ERP coding Workflow. Invoked by `workflows/coding.mjs` at the review stage via `agentType:'code-reviewer'` (see `reviewWithFixLoop`). Reviews a single completed feature against its plan, spec, and the project's coding standards. The domain phase (`backend` / `frontend`) is read from the prompt body — NOT from any `phase` option on the `agent()` call (that option is a harness UI grouping option, unrelated to review scope). The prompt body contains an explicit `**phase = backend ...**` or `**phase = frontend ...**` line you must parse. Runs as a non-interactive subagent inside a bounded review/fix loop (max 5 rounds) — MUST return a structured verdict and never block on a prompt. | |
| 4 | + Unified code reviewer for the ERP coding Workflow. Invoked by `workflows/coding.mjs` at the review stage via `agentType:'erp-workflow:code-reviewer'` (the plugin-namespaced form is required — a bare `code-reviewer` is ambiguous with other plugins' agents; see `reviewWithFixLoop`). Reviews a single completed feature against its plan, spec, and the project's coding standards. The domain phase (`backend` / `frontend`) is read from the prompt body — NOT from any `phase` option on the `agent()` call (that option is a harness UI grouping option, unrelated to review scope). The prompt body contains an explicit `**phase = backend ...**` or `**phase = frontend ...**` line you must parse. Runs as a non-interactive subagent inside a bounded review/fix loop (max 5 rounds) — MUST return a structured verdict and never block on a prompt. | |
| 5 | 5 | model: inherit |
| 6 | 6 | --- |
| 7 | 7 | |
| 8 | -You are a Senior Code Reviewer reviewing a single completed feature for the ERP coding Workflow. You are invoked non-interactively by `workflows/coding.mjs` (`agentType:'code-reviewer'`) inside a **bounded review/fix loop (max 5 rounds)**. You MUST return a structured verdict — never ask the user a question, never block on input. | |
| 8 | +You are a Senior Code Reviewer reviewing a single completed feature for the ERP coding Workflow. You are invoked non-interactively by `workflows/coding.mjs` (`agentType:'erp-workflow:code-reviewer'`) inside a **bounded review/fix loop (max 5 rounds)**. You MUST return a structured verdict — never ask the user a question, never block on input. | |
| 9 | 9 | |
| 10 | 10 | ## Domain phase resolution |
| 11 | 11 | ... | ... |
docs/design/2026-06-02-frontend-behavior-gate.md
0 → 100644
| 1 | +# 前端行为门(behavior-gate)— 最终设计(综合评审后) | |
| 2 | + | |
| 3 | +> 本文是 5 维对抗式评审后的收敛版。锁定决策(用户拍板)默认保留;评审给出的有依据调整已并入; | |
| 4 | +> 无依据 / 过度工程的建议在文末「拒绝的建议」记明理由。所有改动可追溯到 changeLog。 | |
| 5 | + | |
| 6 | +## 用户目标 | |
| 7 | +确保前端「每个按钮 / 点击都真的生效、每段文字都显示正确内容(right context)」。在全自动静默的 | |
| 8 | +`coding.mjs` 编码阶段中,新增一个 headless 自动化门来达成。 | |
| 9 | + | |
| 10 | +## 锁定决策(用户拍板,默认不可推翻) | |
| 11 | +1. 机制 / 位置:headless,新增 behavior-gate stage 嵌入 `coding.mjs`;阶段级,跑在 frontend | |
| 12 | + `testGate` 变绿之后、`report`/`runMilestone` 之前;仅作用于末尾的 `frontend-phase` 聚合模块。 | |
| 13 | +2. 期望来源:门运行时**即时推导**(从 `prototype/` + REQ 卡片 + `docs/05`),不预先持久化为契约; | |
| 14 | + 但把推导出的期望写进已提交的证据报告供事后审计。 | |
| 15 | +3. 数据:全栈 + 种子库。`setup-test-db` DROP+CREATE → Flyway 建 schema,门用 `docs/03` 生成 FK 有序 | |
| 16 | + INSERT 种子(带可辨识 sentinel 值),起后端 + 前端,断言动态文字等于 sentinel。 | |
| 17 | +4. 失败语义:两层。交互缺陷(死控件 / 点击触发 JS 或 console 错误 / 应发的 docs05 调用未发)= 硬 RED | |
| 18 | + halt;文字 / 内容不符 = 走既有 `adjudicate` 并记入 `decisions`;渲染出的绑定垃圾归为交互层。 | |
| 19 | +5. 证据:即时推导但证据落盘。门把推导期望 + 逐控件判定 + 截图写入 | |
| 20 | + `docs/superpowers/module-reports/frontend-phase-behavior-gate-r<attempt>.md` 并 commit | |
| 21 | + (与 `*-test-gate-r*.md` 同构)。 | |
| 22 | +6. 生成测试:临时。生成的 Playwright spec + 种子 + runner 写入已被 `.gitignore` 忽略的 | |
| 23 | + `.tmp/behavior-gate/`,跑完即弃;只提交证据报告(避免生成套件悄悄变成永久契约源)。 | |
| 24 | + | |
| 25 | +> **关于锁定决策 4 的两处微调(不推翻,仅细化,见 changeLog C8 / C11)**: | |
| 26 | +> - 「绑定垃圾 = 硬 halt」收窄为**高置信子集**(`null`/`undefined`/`[object Object]`/`NaN`/lorem 出现在 | |
| 27 | +> 数据绑定位)才硬 halt;「双花括号未渲染 / 空占位 `—` / 疑似 i18n key」降级为 textIssue 走 adjudicate | |
| 28 | +> (否则误杀合法文案,且与「文字不符走 adjudicate」自相矛盾)。 | |
| 29 | +> - 「sentinel 文字不符」从「一律走 adjudicate」改为**按来源二分**:绑定 sentinel 的动态文字不符是客观 | |
| 30 | +> 可验证 bug,`allowContinue:false`(仲裁只许 retry/halt);i18n / 字面 / 语义等价类才 `allowContinue:true`。 | |
| 31 | +> 这两处都是把锁定决策落到「不误报 + 不放行真 bug」的可实现状态,不改变其交互硬 / 文字软的总分层。 | |
| 32 | + | |
| 33 | +--- | |
| 34 | + | |
| 35 | +## 0. 运行时硬事实(设计成立的前提,已核查代码) | |
| 36 | + | |
| 37 | +这些是评审核查出的、与原 DESIGN 隐含假设冲突的事实,**最终设计必须自洽**: | |
| 38 | + | |
| 39 | +- **F1(无既有 e2e 起栈可复用)**:项目**不存在** `playwright.config` / `webServer` / `reuseExistingServer` | |
| 40 | + / `e2e:ci`(全仓 grep 无生成物)。唯一 e2e 契约是 `scripts/test.mjs` 第 65 行的单条 shell 命令 | |
| 41 | + `{{e2e_cmd}}`,来源是 scope-lock E.3 用 `AskUserQuestion` 填进 `docs/04 §零` 的**自由字符串**(无则记 | |
| 42 | + `无`)。`coding.mjs` 里的 `pnpm e2e:ci` 只是 prompt 的 fallback 默认值,**无任何 Plan 产物保证它存在或 | |
| 43 | + 自带 webServer**。→ 「复用既有 e2e 起栈」是空对象,必须显式探测 + 自负起栈。 | |
| 44 | +- **F2(无常驻进程原语)**:所有命令执行都经 `agent()` 派子会话,子会话跑命令、返回 exit_code 后**即结束**。 | |
| 45 | + 前台 `mvn spring-boot:run` / `vite` 永不退出会把子会话挂死。`Bash` 工具的 `run_in_background` 句柄随子 | |
| 46 | + 会话结束而失去,跨 `agent()` 不可见。→ 门**绝不**在 JS 编排层跨多个 `agent()` 管理常驻进程;起栈→跑→ | |
| 47 | + teardown 必须收敛进**单个子会话内的一条命令**。 | |
| 48 | +- **F3(schema 由 Flyway 在 Spring Boot 启动时才建)**:`setup-test-db.mjs` 只 DROP+CREATE **空库** | |
| 49 | + (模板第 110 行注释 + README 第 166 行 + db-init SKILL)。Spring Boot 启动时 Flyway 才 apply | |
| 50 | + `sql/migrations/V*.sql`。→ 种子 INSERT 必须排在**后端起好(Flyway apply 完 + 健康检查就绪)之后**,否则 | |
| 51 | + 「table doesn't exist」**确定性失败**。 | |
| 52 | +- **F4(schema 守卫不判测试库)**:`setup-test-db.mjs` 第 81 行只校验 schema 匹配 `/^[A-Za-z0-9_$]+$/` | |
| 53 | + 标识符,**不判它是不是测试库**;config-vars 第 20 行 schema 是自由文本 + 口头建议「推荐含 test/_dev」; | |
| 54 | + README 第 164 行明说「按该值无条件 DROP+CREATE」。同一 schema 同时驱动开发期 `apply-ddl`。→ 「config 已 | |
| 55 | + 把守测试库」是假前提,**删除**该措辞,测试库判定由门自负(见 §5)。 | |
| 56 | +- **F5(无种子机检)**:db-init 的 `lib/validate-ddl.mjs` 只对 **DDL↔docs/03** 做 5 维机检(表/列/类型/ | |
| 57 | + 索引/外键),**没有任何工具校验种子 INSERT**。FK 拓扑序 / NOT NULL / UNIQUE / enum 值域 / 列类型长度全靠 | |
| 58 | + 门子代理推导。→ 种子失败必须单独归类(`seedError`),不混进交互层 RED。 | |
| 59 | +- **F6(运行时禁用 time/random builtin)**:`coding.mjs` 由 Workflow 运行时执行,禁用 `Date.now()` / | |
| 60 | + `Math.random()` / `new Date()`;「今天」交子代理解析(见 `dateFromArtifactPath` 注释,第 134 行);顶层 | |
| 61 | + `return` 是结果通道;`agent`/`phase`/`parallel`/`log`/`adjudicate` 是注入全局。→ sentinel / 端口 / 临时 | |
| 62 | + 目录名等**不得**在 mjs 编排层用 time/random 拼,由子代理在自身上下文确定性生成。 | |
| 63 | +- **F7(证据命名不带日期前缀)**:`frontend-phase-behavior-gate-r<attempt>.md` 与 `*-test-gate-r*.md` | |
| 64 | + 同构、**不带 `YYYY-MM-DD` 前缀**,正好绕开 `dateFromArtifactPath` 的解析。`.tmp/` 已被 gitignore 模板 | |
| 65 | + 忽略(gitignore-append-template)。→ 无需改 `.gitignore`;无需解析「今天」。 | |
| 66 | +- **F8(adjudicate 签名)**:`adjudicate(site, context, grp, round)` 四参;`context` 内 `allowContinue:false` | |
| 67 | + 时仲裁不得选 continue(第 528 行);`ADJUDICATE_MAX = 3`。`recordDecisions(site, decisions)` 把 stage | |
| 68 | + 自主决策汇总进全局 `autonomousDecisions`。 | |
| 69 | + | |
| 70 | +--- | |
| 71 | + | |
| 72 | +## 1. 插桩点(精确到行,避免误触发后端模块) | |
| 73 | + | |
| 74 | +顶层循环现状(`coding.mjs:1357-1362`): | |
| 75 | + | |
| 76 | +```js | |
| 77 | +if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块) | |
| 78 | + phase('Frontend') | |
| 79 | + await featureLoop(module.feItems, 'frontend') | |
| 80 | + phase('Gate') | |
| 81 | + await testGate(module, 'frontend') | |
| 82 | +} // ← behavior-gate 插在这里(if 闭合前) | |
| 83 | +``` | |
| 84 | + | |
| 85 | +**改为**(在 `testGate(module,'frontend')` 之后、`if` 闭合之前插入): | |
| 86 | + | |
| 87 | +```js | |
| 88 | +if (module.feItems.length) { | |
| 89 | + phase('Frontend') | |
| 90 | + await featureLoop(module.feItems, 'frontend') | |
| 91 | + phase('Gate') | |
| 92 | + await testGate(module, 'frontend') | |
| 93 | + phase('Behavior') | |
| 94 | + await runBehaviorGate(module) // 仅 frontend-phase 段;testGate 绿后跑 | |
| 95 | +} | |
| 96 | +``` | |
| 97 | + | |
| 98 | +- 放进 `if (module.feItems.length)` 块内 → 纯后端模块(`feItems` 恒空)**不会**触发,与「仅作用于末尾 | |
| 99 | + frontend-phase 聚合模块」一致。 | |
| 100 | +- `runBehaviorGate` 入口加二次保险守卫(与 `runMilestone`/`reportPrompt` 的 `id==='frontend-phase'` 判别 | |
| 101 | + 惯例一致):`const fe = module?.id === 'frontend-phase'; if (!fe) { log('behavior-gate skip: 非 frontend-phase'); return }`。 | |
| 102 | +- `meta.phases` 增 `{ title:'Behavior' }`(插在 `{ title:'Gate' }` 与 `{ title:'Milestone' }` 之间)。 | |
| 103 | + | |
| 104 | +--- | |
| 105 | + | |
| 106 | +## 2. 新增 schema(不杂交 GATE × STAGE_RESULT,复用既有词汇) | |
| 107 | + | |
| 108 | +评审 C5 指出原 schema 把 GATE 的 `status:green|red` 与 STAGE 的 `decisions[]`/`artifactPath` 杂交、且 | |
| 109 | +`decisions` 重复定义。**收敛做法**:复用 STAGE_RESULT 已有的 `decisions[]` 形状与 `artifactPath` 命名 | |
| 110 | +(不另起 `evidencePath`),只新增行为门**特有的两层结果数组**。 | |
| 111 | + | |
| 112 | +```js | |
| 113 | +const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, | |
| 114 | + required:['status','routesPlanned','routesReached','controlsEnumerated'], | |
| 115 | + properties:{ | |
| 116 | + status:{ type:'string', enum:['green','red'] }, | |
| 117 | + // 覆盖率计数(C20:空覆盖必须可见,绝不静默放行) | |
| 118 | + routesPlanned:{ type:'integer' }, // step1 路由真值(router 配置)声明的路由数 | |
| 119 | + routesReached:{ type:'integer' }, // 实际成功导航到达(鉴权后非登录页 / 非空壳)的路由数 | |
| 120 | + controlsEnumerated:{ type:'integer' }, // 枚举到的非 inert 可交互控件总数 | |
| 121 | + authState:{ type:'string' }, // C12:以何角色登录、覆盖了哪些角色、未覆盖角色集 | |
| 122 | + // 交互层失败(硬边界);kind 细分让仲裁能区分「门自身能力不足」与「真死控件」 | |
| 123 | + interactionFailures:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 124 | + required:['page','control','kind','detail'], | |
| 125 | + properties:{ | |
| 126 | + page:{type:'string'}, control:{type:'string'}, | |
| 127 | + kind:{type:'string', enum:[ | |
| 128 | + 'no-observable-effect', // 点击无任何可观测效果(真死控件) | |
| 129 | + 'js-error', // 点击触发未捕获 JS 异常 | |
| 130 | + 'console-error', // 点击触发 console.error | |
| 131 | + 'missing-docs05-call', // 应发的 docs/05 端点调用未发 | |
| 132 | + 'binding-garbage' ]}, // 高置信渲染垃圾(null/undefined/[object Object]/NaN/lorem 在绑定位) | |
| 133 | + detail:{type:'string'} } } }, | |
| 134 | + // 文字层问题(软边界,按 source 在 JS 侧分流 allowContinue) | |
| 135 | + textIssues:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 136 | + required:['page','region','expected','actual','source'], | |
| 137 | + properties:{ | |
| 138 | + page:{type:'string'}, region:{type:'string'}, | |
| 139 | + expected:{type:'string'}, actual:{type:'string'}, | |
| 140 | + source:{type:'string', enum:['sentinel','i18n','literal','semantic']} } } }, | |
| 141 | + // 覆盖缺口(C13/C15/C18:到不了的路由 / 多步深层控件未达 / 动态路由无种子可实例化) | |
| 142 | + coverageGaps:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 143 | + required:['page','reason'], | |
| 144 | + properties:{ | |
| 145 | + page:{type:'string'}, | |
| 146 | + reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed']}, | |
| 147 | + detail:{type:'string'} } } }, | |
| 148 | + // 起栈 / 种子 / 鉴权环境失败(与业务断言失败严格区分;走 retry 不当死控件) | |
| 149 | + envError:{ type:'object', additionalProperties:false, | |
| 150 | + required:['kind'], | |
| 151 | + properties:{ | |
| 152 | + kind:{type:'string', enum:['port-conflict','stack-not-ready','seed-error','auth-failed','timeout','none']}, | |
| 153 | + detail:{type:'string'}, | |
| 154 | + ports:{type:'string'}, pids:{type:'string'} } }, // C17/C19:写进证据便于人工清理残留 | |
| 155 | + decisions:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 156 | + required:['question','choice','rationale'], | |
| 157 | + properties:{ question:{type:'string'}, choice:{type:'string'}, | |
| 158 | + rationale:{type:'string'}, confidence:{type:'string', enum:['high','medium','low']} } } }, | |
| 159 | + artifactPath:{ type:'string' } } } // 证据报告路径(复用 STAGE_RESULT 命名,不叫 evidencePath) | |
| 160 | +``` | |
| 161 | + | |
| 162 | +`additionalProperties:false`。`decisions[]` 逐项形状与 STAGE_RESULT 完全一致,由 `recordDecisions` 汇总。 | |
| 163 | + | |
| 164 | +--- | |
| 165 | + | |
| 166 | +## 3. 门内部流水线(runBehaviorGate,JS 编排,每 attempt 一个子会话) | |
| 167 | + | |
| 168 | +> 关键运行时收敛(F2):**整个「探测 → setup-db → 起后端 → 等就绪 → 种子 → 起前端 → 鉴权 → 枚举 → 断言 → | |
| 169 | +> teardown」必须由门生成的临时 runner(`.tmp/behavior-gate/r<attempt>/run.mjs`)在 *单个子会话内的一条命令* | |
| 170 | +> 里完成**,runner 用 `spawn` 起进程树、轮询就绪、`finally` 里 kill 全部子进程并透传结构化结果。JS 编排层只 | |
| 171 | +> 负责:渲染 prompt → 派子会话跑 runner → 收 `BEHAVIOR_GATE_SCHEMA` → 控制流(flake/halt/adjudicate)。 | |
| 172 | + | |
| 173 | +`behaviorGatePrompt(module, attempt)` 指示门子代理在子会话内执行: | |
| 174 | + | |
| 175 | +### step 0 — 探测起栈能力(F1) | |
| 176 | +读 `docs/04 §零`(e2e 命令)+ `frontend/package.json` + `frontend/playwright.config.*`(若存在)+ | |
| 177 | +`config-vars.yaml`(端口 / 凭据)。判定: | |
| 178 | +- (a) 存在 `playwright.config` 且含 `webServer`/`reuseExistingServer` → runner 复用 playwright 自带 | |
| 179 | + webServer 起栈,门只负责 setup-db + 起后端 + 种子(前端交给 playwright)。 | |
| 180 | +- (b) 不存在 → runner 自负起后端 + 前端(见 step 2)。 | |
| 181 | +- 无法判定 / 探测失败 → 写 `envError.kind='stack-not-ready'`,**走 adjudicate(allowContinue:false)**, | |
| 182 | + 不静默假设默认命令。 | |
| 183 | + | |
| 184 | +### step 1 — 路由真值发现(推导 + 运行时校验对账,C16/C18) | |
| 185 | +- **主来源 = `frontend/` 的 router 配置**(Vue Router / React Router 的 `routes` 定义,Grep 即可)——SPA 的 | |
| 186 | + 运行时路由真值。`prototype/` + REQ + `docs/05` 用于**推导每条路由的预期控件清单与文字来源**(作为覆盖率 | |
| 187 | + 分母),不作为路由真值。 | |
| 188 | +- 每条路由标注**所需角色**(C12);带参路由(`/orders/:id`)用 step 3 种子的已知主键实例化具体 URL,无种子 | |
| 189 | + 可实例化的记 `coverageGaps[reason='dynamic-route-no-seed']`。 | |
| 190 | +- `routesPlanned` = router 声明的全部路由数。 | |
| 191 | + | |
| 192 | +### step 2 — 安全护栏 + setup-db + 起后端 + 等就绪 + 种子 + 起前端(严格时序,F3/F4) | |
| 193 | +runner 内严格四段时序(**种子在 schema 存在之后注入**): | |
| 194 | +1. **测试库安全护栏(确定性,先于一切,F4)**:读 `config-vars.yaml database.schema`,若不匹配测试库命名 | |
| 195 | + 约定(含/结尾 `test`/`_test`/`_dev`/`_local`)→ runner 立即非零退出,门返回 `envError` 并由 JS 层 | |
| 196 | + `throw HALT`(**不经 adjudicate**,与 `assertSafeId` 同级硬安全边界)。要求人工显式确认或改用物理隔离的 | |
| 197 | + `<schema>_behavior_gate`。 | |
| 198 | +2. `node scripts/setup-test-db.mjs`(DROP+CREATE 空库)。**DROP 前确保无旧后端连着该库**:先按 | |
| 199 | + `.tmp/behavior-gate/*.pid` 优雅回收上一轮残留进程(C9/C19)。 | |
| 200 | +3. **起后端**:runner `spawn` 后端进程;轮询健康探针(`/actuator/health` 200 或登录端点 200,带宽超时)直到 | |
| 201 | + 就绪——Flyway 在此窗口 apply schema(数十秒)。端口取 `config-vars.yaml` 的 `backend.http_port`,**但先 | |
| 202 | + 探测占用**:占用则先尝试回收残留 pid,仍占用则改用动态空闲端口并把 `baseURL` 注入 playwright(C17)。 | |
| 203 | +4. **此时**才跑 `docs/03` 派生的 **FK 有序 INSERT 种子**(schema 已存在)。种子失败 → `envError.kind='seed-error'` | |
| 204 | + + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不混进交互层 RED**(F5/C7)。 | |
| 205 | +5. **起前端**(headless):(a) 分支用 playwright webServer;(b) 分支 runner `spawn` 前端 dev/preview, | |
| 206 | + 轮询 ready,端口同样先探测占用 + 动态回退。 | |
| 207 | +- runner 的 `finally` **硬要求** kill 全部子进程(覆盖超时 / 异常 / 断言失败路径),把端口 + pid 写进结果 | |
| 208 | + (`envError.ports`/`envError.pids`),避免 attempt 间 / 跨 coding-start 端口冲突(C3/C9/C19)。 | |
| 209 | +- **种子 sentinel 规则(C10/C14,确定性 + 类型合法 + 不撞既有数据)**: | |
| 210 | + - 按列类型派生类型合法可辨识值:字符串列用 `字段名编码 + 行序号`(如 `CUST_NAME_S001`,**逐字段唯一**以抓 | |
| 211 | + 「绑错字段」);数值列用约定高位魔数(如 `999001`);enum 列只能从 `docs/03` 值域取一个并在证据标注 | |
| 212 | + 「enum 列无法 sentinel,改用值域成员校验」;手机/邮箱/金额等带格式列派生格式合法的可辨识值。 | |
| 213 | + - 多行场景 sentinel 带行序号保证 UNIQUE 不撞。 | |
| 214 | + - **插入前扫描 Flyway migration / config-vars 里既有初始数据键**(如 `admin_init.username=admin`、字典 | |
| 215 | + 数据),sentinel 主键 / 唯一键偏移到不冲突区间;文字断言按 sentinel 行的**已知主键定位**,而非断言整页 | |
| 216 | + 第一条(避免被既有初始数据行误判)。 | |
| 217 | + - **安全(C19)**:所有拼进 SQL 的值用参数化 / 严格转义(不裸字符串拼 INSERT;用占位符或对值做白名单); | |
| 218 | + sentinel 用门自生成的受控格式(`[A-Za-z0-9_]`),**不**从文档 / DOM 取任意文本拼 SQL。 | |
| 219 | + | |
| 220 | +### step 2.5 — 鉴权 bootstrap(C2/C11/C12,确定性前置步骤,非「风险记录」) | |
| 221 | +ERP 绝大多数路由在登录后才可达(Spring Security / JWT 已在模板确认)。runner 在枚举前: | |
| 222 | +- 用 `config-vars.yaml admin_init`(A1 已锁的已知账号)或种子里写入的已知凭据,经 `docs/05` 登录端点**真实 | |
| 223 | + 登录**拿 JWT,注入 Playwright `storageState`。 | |
| 224 | +- `authState` 记录「以何角色登录、覆盖了哪些角色、未覆盖角色集」。多角色权限分叉至少覆盖 admin 一遍。 | |
| 225 | +- **登录失败归类为 `envError.kind='auth-failed'`(环境 race,走 retry)**,绝不当死控件 halt。 | |
| 226 | + | |
| 227 | +### step 3 — 枚举(可达性驱动,分母对账,非首帧快照,C13/C15) | |
| 228 | +- 每条路由用 Playwright 加载(带 storageState)后收集 DOM 真实存在的全部可交互控件 | |
| 229 | + (`button/a/input/select/[role=button]/@click` 等)与可见文字区域。 | |
| 230 | +- **覆盖判据 = 可达性驱动有界探索 + 来源对账**(不是首帧快照): | |
| 231 | + - 分母 = step 1 推导的预期控件清单(从 prototype 链接/表单 action + FE spec 的 5 态状态机)。 | |
| 232 | + - 分子 = live 枚举到的控件。 | |
| 233 | + - 分母有、首帧无的控件,runner **尝试驱动到其出现态**:种子保证列表非空以触发行级操作、点击进入多步流程 | |
| 234 | + 下一屏 / 展开 dropdown / 切 tab 后做**二次枚举**。仍无法到达的记 `coverageGaps[reason='deep-control-not-driven']`, | |
| 235 | + **不静默判 green**。 | |
| 236 | + - 到达不了的路由(被重定向回登录 / 空壳)记 `coverageGaps[reason='unreachable-auth'|'unreachable-no-route']`, | |
| 237 | + 与「到达了但控件死」**严格区分**(前者 coverage-gap,后者才是 interactionFailure 硬 halt)。 | |
| 238 | +- **inert 过滤(C8)**:`disabled` / `[aria-disabled=true]` / `fieldset[disabled]` 内 / 计算样式 | |
| 239 | + `pointer-events:none` 的控件归为 `intentionally-inert`,**不纳入「必须有可观测效果」断言集**,但记入证据覆盖 | |
| 240 | + 清单(标注 inert + 推断禁用原因)。增强:对 disabled 提交类按钮,先用 sentinel 种子 / 输入把表单填到合法 | |
| 241 | + 态,观察是否解除 disabled——能解除即证明是活的且有正确门控;始终 disabled 且 spec 未说明的进 textIssues | |
| 242 | + 走 adjudicate,**不一律硬 halt**。 | |
| 243 | +- `routesReached` / `controlsEnumerated` 据实填。 | |
| 244 | + | |
| 245 | +### step 4 — 推导期望 | |
| 246 | +每控件给出预期可观测效果;每文字区域给出预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。 | |
| 247 | + | |
| 248 | +### step 5 — 断言(两层 + 可观测效果白名单,C6) | |
| 249 | +- **交互层**:点击 / 交互要求**可观测效果**,白名单(C6 扩充,避免误判生效按钮为死控件): | |
| 250 | + - URL 变化 / `docs05` 网络调用(`page.on('request')` 比对预期端点)/ DOM 变更 / 校验信息 / 弹层 / toast; | |
| 251 | + - **原生对话框**:枚举前 runner 必须注册 `page.on('dialog')`,「弹出原生 confirm/alert/beforeunload」本身 | |
| 252 | + 计为一类合法可观测效果(危险操作的 `confirm()` 不处理会阻塞 → 误判 missing-docs05-call); | |
| 253 | + - **下载** `page.on('download')` / **新标签** `page.on('popup')`/`target=_blank` 也是合法效果。 | |
| 254 | + - 无任何可观测效果 → `interactionFailures[kind='no-observable-effect']`;点击触发 JS 异常 → | |
| 255 | + `js-error`;console.error → `console-error`;应发未发 docs05 → `missing-docs05-call`。 | |
| 256 | + - 断言用 Playwright 的 **auto-waiting / `expect.poll`**(不用固定 sleep),从机制压低渲染时序 flake(C4)。 | |
| 257 | +- **文字层**:渲染对比推导期望。动态格对比对应字段的**唯一 sentinel**(不仅「等于某 sentinel」,而是「等于 | |
| 258 | + 该 region 推导期望字段的那个唯一 sentinel」,以抓绑错字段,C14)。 | |
| 259 | +- **绑定垃圾分级(C8,收窄锁定决策 4)**:`null`/`undefined`/`[object Object]`/`NaN`/lorem 出现在数据绑定位 | |
| 260 | + → `interactionFailures[kind='binding-garbage']`(高置信硬 halt);「双花括号未渲染 / 空占位 `—` / 疑似 | |
| 261 | + i18n key(含点号标识符且无对应文案)」→ `textIssues[source='i18n'|'literal']` 走 adjudicate。i18n 场景额外 | |
| 262 | + 要求 runner 加载真实 locale 资源比对。 | |
| 263 | + | |
| 264 | +### step 6 — 证据落盘 + commit(F7) | |
| 265 | +写 `docs/superpowers/module-reports/frontend-phase-behavior-gate-r<attempt>.md`(含推导期望、逐控件判定、 | |
| 266 | +覆盖率计数 `routesPlanned/routesReached/controlsEnumerated`、`authState`、`coverageGaps`、截图)并 commit。 | |
| 267 | +- **截图归档(C19)**:要 commit 的截图落到**已纳入版本管理的**目录(如 | |
| 268 | + `docs/superpowers/module-reports/assets/frontend-phase-behavior-gate/r<attempt>/`),证据报告只引用已提交 | |
| 269 | + 路径,**不**引用 `.tmp/`(避免 commit 后链接断链)。`.tmp/behavior-gate/r<attempt>/` 只放 spec/种子/runner/ | |
| 270 | + 原始截图,跑完即弃。 | |
| 271 | + | |
| 272 | +--- | |
| 273 | + | |
| 274 | +## 4. 失败 / 控制流(runBehaviorGate,与 testGate 骨架对齐,C2/C21/C22) | |
| 275 | + | |
| 276 | +> 评审 C21(失败语义与安全维度)指出原文「interactionFailures 非空即硬 throw」与「RED 自动重试 1 次辨 flake」 | |
| 277 | +> 自相矛盾——若一返回带 interactionFailures 就 throw,flake 重试根本没机会跑。**最终控制流显式分段**,并把交互 | |
| 278 | +> 层硬边界纳入 adjudicate 框架(`allowContinue:false`),与全仓「halt 经 adjudicator」收敛架构一致(C24)。 | |
| 279 | + | |
| 280 | +``` | |
| 281 | +1. attempt=1:派子会话跑 runner,收 BEHAVIOR_GATE_SCHEMA。 | |
| 282 | +2. 若 envError.kind != 'none'(端口冲突/起栈未就绪/种子错/鉴权失败/超时): | |
| 283 | + 归类为环境 race → 与 testGate 同款:attempt=2 重跑一次;仍 envError → adjudicate(allowContinue:false, | |
| 284 | + 只在 retry/halt 间裁),retry 再起独立 attempt。绝不把环境 race 当死控件。 | |
| 285 | + 测试库护栏触发的 HALT 例外:不重试、不仲裁,直接 throw(assertSafeId 同级)。 | |
| 286 | +3. 空覆盖检查(C20):frontend-phase 存在但 (controlsEnumerated==0 || routesReached==0) → | |
| 287 | + 绝不 green,归为 envError(stack/auth/seed 起不来) 走 step 2 的 retry/halt;证据报告头部红字标注 | |
| 288 | + 「本次门未覆盖任何控件,原因=<...>」。 | |
| 289 | +4. interactionFailures(交互硬边界): | |
| 290 | + - attempt=1 出现时【不立刻 throw】,先按 testGate 同款跑 attempt=2(独立证据文件 r2,辨 flake)。 | |
| 291 | + - 仅当 attempt=2 后 interactionFailures 仍非空,经 adjudicate(allowContinue:false) 在 retry/halt 间裁; | |
| 292 | + retry 用于「断言类红可在同一次起栈内重试单断言 / 环境抖动」,halt 用于真死控件。绝不 continue。 | |
| 293 | + - 仲裁可据 interactionFailures[].kind 区分「门自身未处理的弹窗类型 / 环境未就绪」与「真死控件」, | |
| 294 | + 前者倾向 retry,后者 halt。 | |
| 295 | +5. textIssues(文字软边界,按 source 分流 allowContinue,C11/C22): | |
| 296 | + for-of textIssues: | |
| 297 | + - source=='sentinel':actual≠唯一 sentinel 是客观 bug(门自己灌的确定值,非推导误报)→ | |
| 298 | + adjudicate(`behavior-text:${page}:${region}`, {expected,actual,source,allowContinue:false}, 'Behavior', round) | |
| 299 | + 仲裁只许 retry/halt,绝不 continue 放行绑错字段/显示错记录。 | |
| 300 | + - source ∈ {i18n,literal,semantic}:推导文案有误报风险 → | |
| 301 | + adjudicate(..., {allowContinue:true}, ...) retry=重判该条 / continue=recordDecisions 记入 | |
| 302 | + autonomousDecisions / halt=真内容 bug。 | |
| 303 | + - round 计数与 ADJUDICATE_MAX 上限按既有 adjudicate 循环惯例配(每条 site 独立计 round,上限 ADJUDICATE_MAX)。 | |
| 304 | +6. coverageGaps:写进证据报告 + recordDecisions(不单独 halt;空覆盖已在 step 3 兜底为 envError)。 | |
| 305 | +7. 全部通过(interactionFailures 空、sentinel textIssues 全消解、覆盖非空)→ 返回 { status:'green', ... }。 | |
| 306 | +8. 行为门 RED 发生在 milestone tag 之前,沿用 report 的 allowContinue:false 纪律,throw 自然冒泡到顶层 | |
| 307 | + try/catch → break,绝不带红进里程碑。 | |
| 308 | +``` | |
| 309 | + | |
| 310 | +**与既有原语的对齐**:交互层借 `testGate()` 同款 attempt=1→2 retry + adjudicate(allowContinue:false) | |
| 311 | +骨架;文字层借 `reviewWithFixLoop` 的逐条 for-of + adjudicate 先例(非 testGate 单裁面)。可选更省实现:把整门 | |
| 312 | +收敛为一次 `runStage(...)` 调用、让 `runStage` 现成的 adjudicate(retry/continue/halt)+ADJUDICATE_MAX 兜底 | |
| 313 | +软边界,交互硬边界单独前置判定 throw——但首选上面的显式分段以保证「环境 race 走 retry、死控件走 halt」的清晰。 | |
| 314 | + | |
| 315 | +--- | |
| 316 | + | |
| 317 | +## 5. behaviorGateContract(不直接套 featureStageContract('frontend'),C1/C6/C26) | |
| 318 | + | |
| 319 | +评审 C1/C6/C26 指出:`featureStageContract('frontend')` 的路径护栏明文「实现文件必须落 `frontend/`;命中 | |
| 320 | +`backend/`/`sql/`/`scripts/` 即越界硬停」(第 161-163 行),而行为门必须**运行** `scripts/setup-test-db.mjs`、 | |
| 321 | +起后端、生成 sql 语义种子——忠实执行该 contract 的子代理会把这些判为越界并自相矛盾。且 contract 还含「全部输出 | |
| 322 | +中文 / 缺值查找顺序 / 绝不留 TBD」等与门职责冲突的条款(门要写英文 sentinel / Playwright spec)。 | |
| 323 | + | |
| 324 | +**新增 `behaviorGateContract()`**(第三类:跨栈只读验证 + 临时产物),只保留真正通用的硬约束: | |
| 325 | + | |
| 326 | +``` | |
| 327 | +## 硬约束(行为门——只读验证门,非交互子代理) | |
| 328 | +- 你是 Workflow 派生的非交互子代理,物理上无法弹问,绝不尝试问人。 | |
| 329 | +- 全部输出文档使用中文(证据报告);但生成的 Playwright spec / sentinel / SQL 种子可用英文标识符。 | |
| 330 | +- 作用域例外(关键):本门为【只读验证门】。允许【运行(不可写)】scripts/setup-test-db.mjs、起后端/前端服务、 | |
| 331 | + 跑 playwright;唯一可写位置 = .tmp/behavior-gate/r<attempt>/(spec/种子/runner)+ 证据报告及其 assets 目录。 | |
| 332 | + 改动 frontend/ / backend/ / sql/ 任何【源码】即越界硬停。把「运行 backend 服务」与「写 backend 实现」显式区分。 | |
| 333 | +- 缺值时优先自主决策继续并记入 decisions[](与 featureStageContract 同口径);仅无法自洽的硬事实才 halt。 | |
| 334 | +- sentinel / 端口 / 临时目录名由你在自身上下文确定性生成;【绝不】依赖 mjs 编排层提供 time/random(编排层禁用)。 | |
| 335 | +``` | |
| 336 | + | |
| 337 | +`behaviorGatePrompt(module, attempt)` = `behaviorGateContract()` + `commitBlock(证据路径+assets, 'docs(behavior-gate:frontend-phase:r<attempt>): 行为门证据')` + step 0-6 指令 + `BEHAVIOR_GATE_SCHEMA` 输出契约。 | |
| 338 | + | |
| 339 | +--- | |
| 340 | + | |
| 341 | +## 6. report 前置接入(C23,闭合「按钮生效/文字正确」证据链) | |
| 342 | + | |
| 343 | +`reportPrompt` 前端分支(`coding.mjs:947`)现仅 Glob `${phaseId}-test-gate-r*.md` 并要求「最后一份 green」。 | |
| 344 | +behavior-gate 证据未进绿前置 → milestone tag 指向的 commit 报告对行为门「视而不见」。**扩展**: | |
| 345 | + | |
| 346 | +- 前端分支绿前置 Glob **追加** `frontend-phase-behavior-gate-r*.md`,按 attempt 升序,**最后一份必须 | |
| 347 | + status 非 RED**(与 test-gate 同纪律);最后一份 red 立即 halt。 | |
| 348 | +- §⑤ flake 汇总纳入 behavior-gate 各 attempt(红→绿切换标注 flake)。 | |
| 349 | +- §⑧ 偏离清单纳入行为门的 `coverageGaps` + textIssues continue 记录 + 逐控件判定摘要 + `authState` 未覆盖 | |
| 350 | + 角色集,让 milestone 真覆盖「按钮生效 / 文字正确」的证据。 | |
| 351 | + | |
| 352 | +--- | |
| 353 | + | |
| 354 | +## 7. resume / 幂等(C25,接受全量重跑但收敛非确定性) | |
| 355 | + | |
| 356 | +- behavior-gate 夹在 frontend `testGate` 与 `report` 之间,**不自打独立 tag**(完成真值仍是 | |
| 357 | + `milestone/frontend-phase`)。接受代价:halt 后人工修复重跑 `coding-start` 时,Router 见 `frontend-phase` | |
| 358 | + 无 milestone tag → 整段重跑,各 FE 因 `req-done/<FE>` 已存在而 skip(OK),但 frontend `testGate` 与 | |
| 359 | + behavior-gate 完整重跑(含重起栈 + 全量枚举)。 | |
| 360 | +- **收敛即时推导的逐次漂移**:门入口先清 `.tmp/behavior-gate/` 整目录(避免跨 resume 串味);每 attempt 用 | |
| 361 | + 独立子目录 `.tmp/behavior-gate/r<attempt>/`(与证据文件 `-r<attempt>` 对齐),跑前清空保证幂等。证据报告里 | |
| 362 | + 已落盘的推导期望可供人工审计;**不**要求 resume 复用上次推导(接受重推,但风险节记明漂移代价)。 | |
| 363 | +- **源码修复路径语义**(对齐 `coding.mjs:1148`):behavior-gate 触发的修复若需改 FE 源码,须**先手动删除对应 | |
| 364 | + `req-done/<FE>` tag** 才会在重跑时重走 review;否则该 FE 跳过 review。门**自身不改源码**(只读验证门),文字 | |
| 365 | + bug 经 adjudicate;真要改码留给人工 / 重跑,设计在此显式提示。 | |
| 366 | + | |
| 367 | +--- | |
| 368 | + | |
| 369 | +## 8. coding.mjs 新增面汇总(实现清单) | |
| 370 | + | |
| 371 | +| 新增 | 位置 | 说明 | | |
| 372 | +|---|---|---| | |
| 373 | +| `BEHAVIOR_GATE_SCHEMA` | 与其他 schema 同段(第 64 行 GATE_SCHEMA 附近) | §2 形状,`additionalProperties:false`,复用 decisions[]/artifactPath 词汇 | | |
| 374 | +| `behaviorGateContract()` | `microStepContract`/`featureStageContract` 附近 | §5,只读验证门作用域例外 | | |
| 375 | +| `behaviorGatePrompt(module, attempt)` | `gatePrompt` 附近 | §3+§5,contract + commitBlock + step 0-6 + schema | | |
| 376 | +| `runBehaviorGate(module)` | `testGate` 附近 | §4 控制流:frontend-phase 守卫 → 清 .tmp → attempt 循环(envError retry / 空覆盖兜底 / interactionFailures attempt2→adjudicate(false) / textIssues 按 source 分流) | | |
| 377 | +| `meta.phases += {title:'Behavior'}` | 第 10-13 行 | Gate 与 Milestone 之间 | | |
| 378 | +| 顶层插桩 | `coding.mjs:1361` 之后、if 闭合前 | `phase('Behavior'); await runBehaviorGate(module)` | | |
| 379 | +| `reportPrompt` 前端分支扩展 | `coding.mjs:947/956/957` | §6 绿前置 + §⑤/§⑧ 纳入 behavior-gate | | |
| 380 | + | |
| 381 | +**运行时约束自洽(F6)**:`runBehaviorGate` 编排层不调用 `Date.now()`/`Math.random()`/`new Date()`;sentinel/ | |
| 382 | +端口/目录名由子代理生成;顶层 `return` 完好;用注入全局 `agent`/`phase`/`log`/`adjudicate`/`recordDecisions`。 | |
| 383 | +所有拼进 git/shell 的标识符(`attempt` 经 `Number()`+整数校验后拼路径)仍过既有安全口径;BEHAVIOR_GATE_SCHEMA | |
| 384 | +`additionalProperties:false`。 | |
| 385 | + | |
| 386 | +--- | |
| 387 | + | |
| 388 | +## 9. 残留风险(已知、接受或缓解) | |
| 389 | + | |
| 390 | +- 全栈 headless 起栈 + 逐路由枚举 + 鉴权 + 多步驱动,单 attempt 墙钟可能数分钟到十几分钟;flake ×2 + | |
| 391 | + adjudicate 再拉长。缓解:仅 testGate 绿后跑 + **runner 内单次起栈跑完所有路由(硬约束,绝不每路由重起栈)** | |
| 392 | + + auto-waiting + 整体墙钟上限(超时归 envError 走 retry)。无法把墙钟压到很低,接受。 | |
| 393 | +- 即时推导的路由分母 / 文字期望仍有 LLM 非确定性;sentinel 绑定类已收为确定性硬比对,i18n/literal/semantic | |
| 394 | + 类保留 adjudicate(设计自身权衡)。逐次推导漂移已记入 §7。 | |
| 395 | +- 多角色权限分叉:至少覆盖 admin,未覆盖角色集显式记 `authState` + §⑧;非 admin 角色专属按钮可能漏测。 | |
| 396 | +- 动态参数路由无种子可实例化时记 `coverageGaps[dynamic-route-no-seed]`,可降级但显式可见,不当已覆盖。 | |
| 397 | +- 测试库护栏依赖命名约定(含 test/_dev/_local);若项目用非常规测试库名(如 `staging`)会被护栏挡下要求人工 | |
| 398 | + 确认——宁可误挡不可误删,接受。 | |
| 399 | + | |
| 400 | +--- | |
| 401 | + | |
| 402 | +## 拒绝的建议(无依据 / 过度工程,记明理由) | |
| 403 | + | |
| 404 | +- **「为 behavior-gate 自打独立幂等 tag(behavior-gate-pass/frontend-phase)」**(C25 选项):拒绝。会新增 | |
| 405 | + 一类 resume 真值,与现有「milestone/req-done 两级 tag」体系叠加复杂度;用户锁定决策未要求更细断点,且全量 | |
| 406 | + 重跑成本已被「仅 testGate 绿后跑」摊薄。改为接受全量重跑 + 在 §7/§9 记明代价。 | |
| 407 | +- **「种子改为确定性 JS 生成(读 docs/03 解析后排序)而非 LLM 推导」**(C-确定性 issue B 选项):部分采纳为 | |
| 408 | + *方向*但不强制实现。理由:锁定决策 2 明确「门运行时即时推导」,且新建确定性种子生成器是独立大工程(需复刻 | |
| 409 | + db-init 的 docs/03 解析 + FK 拓扑),超出本门范围。采纳其可落地内核——种子失败单独归类 `seedError` + | |
| 410 | + 结构化根因 + retry 带 guidance(§3 step2.4 / §4),把「同 docs/03 → 同种子」的非确定性收敛到「失败可诊断、 | |
| 411 | + sentinel 规则确定」,不强制改推导机制本身。 | |
| 412 | +- **「textIssues 一律升级 allowContinue:false」**:拒绝。会让 i18n/语义等价类误报变成不可恢复 halt,与锁定 | |
| 413 | + 决策 4「文字走 adjudicate 不硬 halt」冲突。改为按 source 二分(sentinel 硬、其余软),既守真 bug 又不卡死。 | ... | ... |
skills/coding/coding-start/SKILL.md
| ... | ... | @@ -23,6 +23,7 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) |
| 23 | 23 | 后端测试闸 test-gate(RED 自动重试 1 次,仍 RED → halt) |
| 24 | 24 | 前端功能循环 同一流水线,phase=frontend(FE-NN,限 frontend/) |
| 25 | 25 | 前端测试闸 test-gate |
| 26 | + 前端行为闸 behavior-gate(headless 全栈起栈+种子 sentinel,逐路由枚举:交互失效→halt,文字不符→仲裁) | |
| 26 | 27 | 跨模块记录 → 模块报告 → 里程碑(merge --no-ff + milestone/<id> tag) |
| 27 | 28 | 任一模块 halt → fail-fast 停在该模块,修复后重跑本入口即可续跑 |
| 28 | 29 | ... | ... |
workflows/coding.mjs
| ... | ... | @@ -9,7 +9,7 @@ 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: 'Milestone' }, | |
| 12 | + { title: 'Gate' }, { title: 'Behavior' }, { title: 'Milestone' }, | |
| 13 | 13 | ], |
| 14 | 14 | } |
| 15 | 15 | |
| ... | ... | @@ -65,6 +65,52 @@ const GATE_SCHEMA = { type:'object', additionalProperties:false, |
| 65 | 65 | required:['status'], properties:{ status:{type:'string',enum:['green','red']}, |
| 66 | 66 | failures:{type:'array',items:{type:'string'}} } } |
| 67 | 67 | |
| 68 | +// BEHAVIOR_GATE_SCHEMA:前端行为门(headless behavior-gate)返回。 | |
| 69 | +// 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 覆盖率 / 环境错误分别结构化, | |
| 70 | +// JS 据 source/kind 分流(交互硬 halt,文字按 source 二分 allowContinue,envError 走 retry)。 | |
| 71 | +// 设计:见 docs/design/2026-06-02-frontend-behavior-gate.md § 2。 | |
| 72 | +const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, | |
| 73 | + required:['status','routesPlanned','routesReached','controlsEnumerated'], properties:{ | |
| 74 | + status:{type:'string', enum:['green','red']}, | |
| 75 | + routesPlanned:{type:'integer'}, // router 声明的路由数(覆盖率分母来源) | |
| 76 | + routesReached:{type:'integer'}, // 实际带鉴权加载成功的路由数 | |
| 77 | + controlsEnumerated:{type:'integer'}, // live 枚举到的控件数(空覆盖必须可见) | |
| 78 | + authState:{type:'string'}, // 以何角色登录 / 覆盖角色 / 未覆盖角色集 | |
| 79 | + // 交互层硬边界:no-observable-effect / js-error / console-error / missing-docs05-call / binding-garbage | |
| 80 | + interactionFailures:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 81 | + required:['page','control','kind','detail'], | |
| 82 | + properties:{ | |
| 83 | + page:{type:'string'}, control:{type:'string'}, | |
| 84 | + kind:{type:'string', enum:['no-observable-effect','js-error','console-error','missing-docs05-call','binding-garbage']}, | |
| 85 | + detail:{type:'string'} } } }, | |
| 86 | + // 文字层软边界:source 决定 allowContinue(sentinel 客观 bug 不可 continue;i18n/literal/semantic 可 adjudicate continue) | |
| 87 | + textIssues:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 88 | + required:['page','region','expected','actual','source'], | |
| 89 | + properties:{ | |
| 90 | + page:{type:'string'}, region:{type:'string'}, | |
| 91 | + expected:{type:'string'}, actual:{type:'string'}, | |
| 92 | + source:{type:'string', enum:['sentinel','i18n','literal','semantic']} } } }, | |
| 93 | + // 覆盖率缺口:写证据 + recordDecisions,不单独 halt(空覆盖由 controlsEnumerated==0 兜底) | |
| 94 | + coverageGaps:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 95 | + required:['page','reason','detail'], | |
| 96 | + properties:{ | |
| 97 | + page:{type:'string'}, | |
| 98 | + reason:{type:'string', enum:['unreachable-auth','unreachable-no-route','deep-control-not-driven','dynamic-route-no-seed']}, | |
| 99 | + detail:{type:'string'} } } }, | |
| 100 | + // 环境错误(与业务断言失败严格区分,走 retry):none 表示无环境问题 | |
| 101 | + envError:{ type:'object', additionalProperties:false, | |
| 102 | + required:['kind'], | |
| 103 | + 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'} } }, | |
| 106 | + // decisions[]:复用 STAGE_RESULT 形状,缺值自主决策日志 | |
| 107 | + decisions:{ type:'array', items:{ type:'object', additionalProperties:false, | |
| 108 | + required:['question','choice','rationale'], | |
| 109 | + properties:{ | |
| 110 | + question:{type:'string'}, choice:{type:'string'}, rationale:{type:'string'}, | |
| 111 | + confidence:{type:'string', enum:['high','medium','low']} } } }, | |
| 112 | + artifactPath:{type:'string'} } } | |
| 113 | + | |
| 68 | 114 | // ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)───────── |
| 69 | 115 | const WT_SCHEMA = { type:'object', additionalProperties:false, |
| 70 | 116 | required:['clean'], properties:{ |
| ... | ... | @@ -472,6 +518,106 @@ function gatePrompt(module, phase, attempt = 1) { |
| 472 | 518 | ].filter(Boolean).join('\n') |
| 473 | 519 | } |
| 474 | 520 | |
| 521 | +// ---- 前端行为门(headless behavior-gate)---- | |
| 522 | +// 设计权威:docs/design/2026-06-02-frontend-behavior-gate.md。frontend testGate 绿后、report/milestone 前跑, | |
| 523 | +// 仅 frontend-phase 聚合模块触发。门是**跨栈只读验证 + 临时产物**的第三类 stage:不套 featureStageContract('frontend') | |
| 524 | +// (其路径护栏命中 backend/sql/scripts 即越界硬停,与门必须运行 setup-test-db / 起后端 / 生成 SQL 种子自相矛盾)。 | |
| 525 | + | |
| 526 | +// behaviorGateContract:门的硬约束。非交互;证据报告用中文但 spec/sentinel/SQL 可英文标识符; | |
| 527 | +// 作用域例外——允许**运行**(不可写)scripts/setup-test-db.mjs / 起后端前端 / 跑 playwright, | |
| 528 | +// 唯一**可写** = .tmp/behavior-gate/r<attempt>/ + 证据报告及 assets;改 frontend//backend//sql/ 源码即越界硬停。 | |
| 529 | +function behaviorGateContract() { | |
| 530 | + return [ | |
| 531 | + '## 硬约束(非交互行为门子代理)', | |
| 532 | + '- 你是 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/...\`)。`, | |
| 536 | + `- **越界硬停**:**绝不**编辑 \`frontend/\` / \`backend/\` / \`sql/\` 下的任何源码文件,也**绝不**编辑 \`${ROOT}/scripts/\` 下的脚本——只许**运行** scripts/setup-test-db.mjs。区分「运行 backend 服务」(允许)与「写 backend 实现」(越界)。命中越界即以 \`status:red\` + \`envError\` 或写清阻塞点结束。`, | |
| 537 | + '- 红线:**绝不**伪造断言通过;**绝不**留 `TBD` / `TODO`;自主默认必须可被现有证据支撑且记入 `decisions[]`。', | |
| 538 | + '- 证据报告**使用中文**;spec / sentinel 标识符 / SQL 可用英文(`[A-Za-z0-9_]`,受控格式,不取任意文本)。', | |
| 539 | + '- **运行时确定性**:sentinel 值 / 端口 / 临时目录名一律由你确定性派生(按列类型 / config-vars 端口 / attempt 序号),**绝不**依赖时间戳 / 随机数。', | |
| 540 | + ].join('\n') | |
| 541 | +} | |
| 542 | + | |
| 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` | |
| 549 | + return [ | |
| 550 | + `# behavior-gate — 前端行为门(headless,attempt=${attempt})`, | |
| 551 | + '', | |
| 552 | + behaviorGateContract(), | |
| 553 | + '', | |
| 554 | + '## 目标', | |
| 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` 推导**每路由的预期控件与文字来源**(作覆盖率分母);每路由标注所需登录角色。', | |
| 573 | + '- 带参动态路由用**种子已知主键**实例化;无法实例化 → 记 `coverageGaps[reason="dynamic-route-no-seed"]`,不静默判 green。', | |
| 574 | + '', | |
| 575 | + '## step2 安全护栏 + 起栈四段严格时序(schema 由 Flyway 在后端启动时才建)', | |
| 576 | + `1) **测试库安全护栏(确定性,先于一切)**:读 config-vars 的数据库名;若**不匹配测试库命名**(库名须含或以 \`test\` / \`_test\` / \`_dev\` / \`_local\` 结尾)→ runner 非零退出,返回 \`status:red\` + \`envError.kind\` 留空走 HALT 语义(在 detail 写明「测试库护栏:库名 <x> 非测试库,拒绝 DROP,留人工确认」)。**绝不**对非测试库跑 setup-test-db。`, | |
| 577 | + `2) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程。`, | |
| 578 | + '3) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', | |
| 579 | + '4) **此时才跑种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** 种子(先父后子)。失败 → `envError.kind="seed-error"` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不**混进交互 RED。', | |
| 580 | + ' - **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`(即便成功也回填,便于审计)。', | |
| 583 | + '', | |
| 584 | + '## step2.5 鉴权 bootstrap(确定性前置)', | |
| 585 | + '- 用 config-vars `admin_init` 或种子已知凭据,经 `docs/05` 登录端点**真实登录**拿 JWT,注入 Playwright `storageState`;`authState` 记角色覆盖(覆盖 / 未覆盖角色集)。', | |
| 586 | + '- 登录失败 = `envError.kind="auth-failed"`(环境 race,走 retry),**绝不**当成死控件。', | |
| 587 | + '', | |
| 588 | + '## step3 枚举(可达性驱动 + 分母对账,非首帧快照)', | |
| 589 | + '- 每路由带 `storageState` 加载,收集 DOM 真实控件与文字区域。分母 = step1 推导清单,分子 = live 枚举。', | |
| 590 | + '- 分母有但首帧无的控件:runner 尝试**驱动到出现态**(种子保列表非空触发行级操作 / 进多步流程下屏 / 展开 dropdown / 切 tab 后二次枚举);仍不可达 → `coverageGaps[reason="deep-control-not-driven"]`,不静默判 green。到不了的路由 → `coverageGaps[reason="unreachable-auth"|"unreachable-no-route"]`,与「到达了但控件死」严格区分。', | |
| 591 | + '- **inert 过滤**:`disabled` / `[aria-disabled]` / `fieldset[disabled]` / `pointer-events:none` 归 intentionally-inert,不入「必须有效果」断言集但记证据;disabled 的提交类按钮先填合法态观察是否解除 disabled。', | |
| 592 | + '- `routesReached` / `controlsEnumerated` 据实填(空覆盖必须可见)。', | |
| 593 | + '', | |
| 594 | + '## step4 推导期望', | |
| 595 | + '- 每控件预期可观测效果;每文字区域预期内容 + 来源(`literal` / `sentinel` / `i18n` / `semantic`)。', | |
| 596 | + '', | |
| 597 | + '## step5 断言(两层 + 可观测效果白名单)', | |
| 598 | + '- **交互层可观测效果白名单**: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 | + ' - 无任何效果 → `interactionFailures[kind="no-observable-effect"]`;JS 异常 → `js-error`;`console.error` → `console-error`;应发未发网络调用 → `missing-docs05-call`。断言用 auto-waiting / `expect.poll`,**不用**固定 sleep。', | |
| 600 | + '- **文字层**:动态文字格对比该 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\` 防断链)。`, | |
| 607 | + `- 若本次 \`status:red\` 或存在 envError,证据**头部用红字标注原因**。`, | |
| 608 | + commitBlock(`${evidence} docs/superpowers/module-reports/assets`, | |
| 609 | + `docs(behavior-gate:r${attempt}): 前端行为门证据`), | |
| 610 | + '', | |
| 611 | + '## 输出(必须符合下发的 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。', | |
| 616 | + '- 做过任何自主默认 → `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。', | |
| 617 | + '- 不要返回额外字段(schema 是 `additionalProperties:false`)。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', | |
| 618 | + ].filter(Boolean).join('\n') | |
| 619 | +} | |
| 620 | + | |
| 475 | 621 | // ---- 微步骤 prompt builders(runBranchSetup / runMilestone / runCrossModule 用)---- |
| 476 | 622 | // 每个 prompt 单职责、短文本;返回严格 schema;执行(action)步统一返回 ACTION_RESULT_SCHEMA。 |
| 477 | 623 | function microStepContract() { |
| ... | ... | @@ -493,6 +639,7 @@ function microStepContract() { |
| 493 | 639 | // ============================================================================ |
| 494 | 640 | |
| 495 | 641 | const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环) |
| 642 | +const BEHAVIOR_GATE_PASS_MAX = ADJUDICATE_MAX * 4 // 行为门 ①②③ 整体收敛轮上限:每次文字层 retry 跳回从头过硬门,超出则确定性 halt(防无限循环) | |
| 496 | 643 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' |
| 497 | 644 | |
| 498 | 645 | // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 |
| ... | ... | @@ -945,6 +1092,9 @@ function reportPrompt(module) { |
| 945 | 1092 | '', |
| 946 | 1093 | '## 前置', |
| 947 | 1094 | `- 验证上游 test-gate 绿:Glob \`${ROOT}/docs/superpowers/module-reports/${phaseId}-test-gate-r*.md\`,**按 attempt 数字升序**读取每一份。**最后一份必须 green**;只要最后一份 red 立即 halt。中间存在 red→green 切换 = flake,需在 § ⑤ 标注。`, |
| 1095 | + 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 记录纳入 § ⑤ 汇总。` | |
| 1097 | + : '', | |
| 948 | 1098 | '', |
| 949 | 1099 | '## 收集输入(取摘要而非正文)', |
| 950 | 1100 | fe |
| ... | ... | @@ -953,8 +1103,8 @@ function reportPrompt(module) { |
| 953 | 1103 | `- § ② "FE 完成清单":扫 \`${ROOT}/docs/superpowers/{specs,plans,reviews}/<日期>-FE-*.md\`,按 FE-NN 顺序列出。`, |
| 954 | 1104 | `- § ③ 文件变更:\`git -C ${ROOT} diff --stat <默认分支 main/master>...HEAD\`(三点 diff,区间 = 功能分支 \`frontend-phase\` 自默认分支分叉以来的全部改动)。`, |
| 955 | 1105 | '- § ④ 数据库使用表 / § ⑥ Migration / § ⑦ 跨模块:填 `N/A(前端阶段)`。', |
| 956 | - `- § ⑤:把 \`${ROOT}/docs/superpowers/module-reports/frontend-phase-test-gate-r*.md\` 全部(按 attempt 排序)摘要汇总。若 attempt 数 > 1 且首次 red 末次 green → 在 § ⑤ 顶部明确标注 \`flake-detected: r1 red, r${'<最后一次>'} green\`,并附首次失败用例与最终绿色记录链接。`, | |
| 957 | - '- § ⑧ 偏离清单:额外审查"实际渲染 DOM 与各 FE 关联原型主结构的差异",逐 FE 列出。', | |
| 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 未覆盖角色集**。`, | |
| 958 | 1108 | '- § ⑪ 下一模块预览:填"上线 / 部署后续步骤"。', |
| 959 | 1109 | ].join('\n') |
| 960 | 1110 | : [ |
| ... | ... | @@ -1229,7 +1379,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { |
| 1229 | 1379 | // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 |
| 1230 | 1380 | const r = await agent( |
| 1231 | 1381 | reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance), |
| 1232 | - {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'code-reviewer'} | |
| 1382 | + {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'erp-workflow:code-reviewer'} | |
| 1233 | 1383 | ) |
| 1234 | 1384 | reviewGuidance = '' // 已消费 |
| 1235 | 1385 | |
| ... | ... | @@ -1299,6 +1449,138 @@ async function testGate(module, phase) { |
| 1299 | 1449 | return g |
| 1300 | 1450 | } |
| 1301 | 1451 | |
| 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}` | |
| 1470 | + | |
| 1471 | + 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) | |
| 1474 | + | |
| 1475 | + // 共享重跑:每次 retry 都开一个独立 attempt(独立 .tmp/r<attempt> 证据),刷新 bg + 记录决策。 | |
| 1476 | + // 文字层 retry 也走这里,确保重跑后能跳回 ①②③ 重新整体过硬门(见下方收敛循环),绝不拿旧快照继续。 | |
| 1477 | + const rerun = async () => { | |
| 1478 | + 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) | |
| 1481 | + } | |
| 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 } | |
| 1488 | + } | |
| 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) | |
| 1504 | + } | |
| 1505 | + if (envState.blocked) throw new Error(`HALT behavior-gate-env frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后仍环境异常 / 空覆盖`) | |
| 1506 | + } | |
| 1507 | + | |
| 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() // 重跑后先过 ① 环境兜底,再回到本门继续辨交互 | |
| 1520 | + } | |
| 1521 | + if (ifails(bg).length) | |
| 1522 | + throw new Error(`HALT behavior-gate-interaction frontend-phase: ${ADJUDICATE_MAX} 轮仲裁后交互层仍有失败`) | |
| 1523 | + } | |
| 1524 | + | |
| 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 || '?'}` | |
| 1551 | + 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 已软放行。 | |
| 1556 | + recordDecisions(site, [{ question:`文字不符 ${ti.page}:${ti.region}(source=${ti.source})`, | |
| 1557 | + choice:'continue(仲裁判可安全前进)', rationale: verdict.rationale || '', confidence:'low' }]) | |
| 1558 | + softPassed.add(regionKey(ti)) | |
| 1559 | + continue // 处理同一 bg 内的下一条 | |
| 1560 | + } | |
| 1561 | + 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 | |
| 1567 | + } | |
| 1568 | + if (!needRerun) converged = true // 无 retry → 文字层已收敛(全软放行 / 全消失) | |
| 1569 | + } | |
| 1570 | + if (!converged) throw new Error(`HALT behavior-gate-text frontend-phase: ${BEHAVIOR_GATE_PASS_MAX} 轮收敛后文字层仍未解决`) | |
| 1571 | + | |
| 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 | + } | |
| 1578 | + | |
| 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 || '?'})`) | |
| 1582 | +} | |
| 1583 | + | |
| 1302 | 1584 | phase('Router') |
| 1303 | 1585 | // Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。 |
| 1304 | 1586 | // id 形状(assertSafeId)是**安全护栏**——失败立即硬 halt,绝不重试绕过。 |
| ... | ... | @@ -1359,6 +1641,8 @@ for (const [idx, module] of todo.entries()) { |
| 1359 | 1641 | await featureLoop(module.feItems, 'frontend') |
| 1360 | 1642 | phase('Gate') |
| 1361 | 1643 | await testGate(module, 'frontend') |
| 1644 | + phase('Behavior') // 前端行为门:testGate 绿后、report/milestone 前(仅 frontend-phase 聚合) | |
| 1645 | + await runBehaviorGate(module) | |
| 1362 | 1646 | } |
| 1363 | 1647 | phase('Milestone') |
| 1364 | 1648 | // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— | ... | ... |