Commit 778861b981b33d950723c2b148d0aca657e2710d
1 parent
0588d0dc
coding.mjs: harden per-FE behavior gate per multi-agent review (build-failed gua…
…rd, coverage reconciliation, testdb halt) Post-implementation multi-agent review (6 dims + per-finding adversarial verify) found the control-flow sound and the goal mostly-achieved; this lands the three deterministic, low-risk fixes among the confirmed findings. - build-failed short-circuit (must-fix): behaviorSubGate now validates the LLM's classification before green-by-skip — requires non-empty rootCausePath AND no interaction/sentinel hard issues riding along; a "dirty" build-failed goes through adjudicate(allowContinue:false) retry/halt instead of silently approving. The skeleton (lazy router + FeStub) makes legit sibling-unimpl build breaks rare, so a build-failed is more likely a real in-FE shared-code regression — the boundary comment §107-108 claimed load-bearing but had zero JS enforcement. - coverage reconciliation backstop (§3.6): empty-coverage only guarded ==0; now 0<routesReached<routesPlanned with the shortfall unexplained by route-level coverageGaps -> adjudicate(allowContinue:false). Closes the partial-coverage false-green. Counts route-level reasons only; over-counting can only suppress the gate, never false-halt. - test-DB guard direct-halt now implemented: TESTDB_GUARD_MARK in envError.detail + behaviorTestDbGuardTripped -> immediate HALT on the first result, honoring step2's "no retry, no adjudication" promise (previously prompt-only; a guard trip wrongly entered the generic stack-not-ready retry path, ~5 redundant setup-test-db.mjs runs). Left as accepted design trade-offs (documented in design §12): self-attested whitelist/route scope, soft-by-source non-data text, disabled-control should-work recovery limited to submit buttons. Verified: SYNTAX_OK (wrapped check), 87/87 lib tests pass, no time/random builtins. v2 design doc updated (§3/5/6.2/6.3 + new §12).
Showing
2 changed files
with
77 additions
and
11 deletions
docs/design/2026-06-02-frontend-behavior-in-review-loop.md
| @@ -84,7 +84,7 @@ reviewWithFixLoop(FE): | @@ -84,7 +84,7 @@ reviewWithFixLoop(FE): | ||
| 84 | **v2 方案**: | 84 | **v2 方案**: |
| 85 | 85 | ||
| 86 | 1. `BEHAVIOR_GATE_SCHEMA.envError.kind` 枚举**新增 `build-failed`**(确定性失败语义;`route-not-buildable` 不单列,统一用 `build-failed` + detail 区分)。 | 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 骨架占位让这种情况罕见,一旦发生说明占位未覆盖,留证据供人工)。 | 87 | +2. **控制流**(在 per-FE 行为门 helper 内):`build-failed` 经**确定性前置校验**后才 green-by-skip 放行(既不 retry 也不 halt);不满足前置 = 「脏」build-failed → 过 `adjudicate(allowContinue:false)` retry/halt,**绝不静默放行**。前置(评审加固,见下「评审加固」):(a) 必须有 `rootCausePath`;(b) 不得同时携带交互/`sentinel` 硬问题。干净放行时记 `coverageGap`(reason `build-failed-sibling-unimpl`)+ recordDecisions(「后续 FE 未实现」的预期中途态,非 FE-N 的 bug;§2 骨架占位让这种情况罕见)。 |
| 88 | 3. `behaviorGatePrompt`(per-FE 版)step0/step2 **明确归因指令**:先 `build` / 起 dev server;若失败,先用 `git` / `Grep` 判断报错根因文件路径—— | 88 | 3. `behaviorGatePrompt`(per-FE 版)step0/step2 **明确归因指令**:先 `build` / 起 dev server;若失败,先用 `git` / `Grep` 判断报错根因文件路径—— |
| 89 | - 落在**非本 FE 的 frontend/ 路径**(兄弟 FE / 占位未覆盖)→ 判 `envError.kind="build-failed"`(预期中途态)。 | 89 | - 落在**非本 FE 的 frontend/ 路径**(兄弟 FE / 占位未覆盖)→ 判 `envError.kind="build-failed"`(预期中途态)。 |
| 90 | - 落在**本 FE 路径** → 才可能是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]` 或带 locator must-fix。 | 90 | - 落在**本 FE 路径** → 才可能是本 FE 引入的真构建 bug → 归 `interactionFailures[kind="js-error"]` 或带 locator must-fix。 |
| @@ -132,7 +132,7 @@ if (!ALLOW && !/(^|_)(test|dev|local)$|(^|_)test_|^test_/.test(DB_SCHEMA) && !/t | @@ -132,7 +132,7 @@ if (!ALLOW && !/(^|_)(test|dev|local)$|(^|_)test_|^test_/.test(DB_SCHEMA) && !/t | ||
| 132 | (具体正则以实现为准,语义=库名须含 `test`/`_test`/`_dev`/`_local` 之一,否则 fail-closed。) | 132 | (具体正则以实现为准,语义=库名须含 `test`/`_test`/`_dev`/`_local` 之一,否则 fail-closed。) |
| 133 | 133 | ||
| 134 | - 这样不论被行为门调用多少次都安全。 | 134 | - 这样不论被行为门调用多少次都安全。 |
| 135 | -- coding.mjs 行为门控制流里,对「测试库护栏触发的红」**保持不重试不仲裁直接 throw** 的硬边界语义(与现 v1 一致,见 behaviorGatePrompt step2 第 1 条)。 | 135 | +- coding.mjs 行为门控制流里,对「测试库护栏触发的红」**不重试不仲裁直接 throw** 的硬边界语义现已落地为确定性机制(评审加固):门子代理在 `envError.detail` 以固定标记 `TESTDB_GUARD_MARK`(`[TESTDB-GUARD]`)开头,`runBehaviorGateOnce` 拿到首个结果即 `behaviorTestDbGuardTripped` 命中 → 立即 throw HALT,绝不进入 attempt 重试 / adjudicate(此前仅 step2 prompt 承诺、JS 无兑现,护栏红会误入 stack-not-ready 通用重试路径空转约 5 次)。 |
| 136 | - 这是 skeleton-gen 模板的一次性改动,**不属于 coding.mjs 改造**,但列为本设计前置(否则反复起栈的安全暴露面不可接受)。 | 136 | - 这是 skeleton-gen 模板的一次性改动,**不属于 coding.mjs 改造**,但列为本设计前置(否则反复起栈的安全暴露面不可接受)。 |
| 137 | 137 | ||
| 138 | --- | 138 | --- |
| @@ -154,7 +154,8 @@ approve 出口(现 1386 `if (r.verdict==='approve')` 分支)**改为合取** | @@ -154,7 +154,8 @@ approve 出口(现 1386 `if (r.verdict==='approve')` 分支)**改为合取** | ||
| 154 | ``` | 154 | ``` |
| 155 | reviewer.verdict==='approve' | 155 | reviewer.verdict==='approve' |
| 156 | ∧ behaviorSubGate(FE) 返回 green | 156 | ∧ behaviorSubGate(FE) 返回 green |
| 157 | - 其中 green ≡ behaviorHard.length===0 ∧ envError∈{none, build-failed} ∧ 本FE覆盖非空(或 build-failed 短路) | 157 | + 其中 green ≡ behaviorHard.length===0 ∧ envError∈{none, build-failed(经前置校验)} ∧ 本FE覆盖非空 |
| 158 | + ∧ 无未解释漏达路由(routesReached + 路由级 coverageGap ≥ routesPlanned) (或干净 build-failed 短路) | ||
| 158 | ``` | 159 | ``` |
| 159 | 160 | ||
| 160 | 只有合取成立才 `flipDocs08Checkbox` + `return {approved:true}`。这保证(采纳「删阶段门」维度 blocker 的钉死): | 161 | 只有合取成立才 `flipDocs08Checkbox` + `return {approved:true}`。这保证(采纳「删阶段门」维度 blocker 的钉死): |
| @@ -168,13 +169,18 @@ reviewer.verdict==='approve' | @@ -168,13 +169,18 @@ reviewer.verdict==='approve' | ||
| 168 | async function behaviorSubGate(id, specPath, feScope): | 169 | async function behaviorSubGate(id, specPath, feScope): |
| 169 | // feScope = {routes:[...], controlWhitelist:[...]}(来自 §4 spec 结构化小节) | 170 | // feScope = {routes:[...], controlWhitelist:[...]}(来自 §4 spec 结构化小节) |
| 170 | for behaviorRound in 1..BEHAVIOR_FE_MAX(=3): | 171 | 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} } | 172 | + bg = await runBehaviorGateOnce(id, behaviorRound, feScope) // 见 §7,内含 testdb-guard 直接 halt + envError attempt 重试 |
| 173 | + // 1) build-failed:经前置校验后短路(依赖 B)。脏(无 rootCausePath / 携带交互|sentinel 硬问题) → adjudicate(allowContinue:false),绝不静默放行 | ||
| 174 | + if (bg.envError.kind === 'build-failed') { | ||
| 175 | + if (dirty(bg)) { v=adjudicate(allowContinue:false); v!=='retry' → throw HALT; else 下一轮重跑 } | ||
| 176 | + else { recordDecisions; return {green:true, skipped:true} } // 干净:兄弟未实现,green-by-skip | ||
| 177 | + } | ||
| 174 | // 2) envError(其它) / 空覆盖:runBehaviorGateOnce 内部已 attempt 重试;到这里仍 blocked → adjudicate(allowContinue:false) retry/halt | 178 | // 2) envError(其它) / 空覆盖:runBehaviorGateOnce 内部已 attempt 重试;到这里仍 blocked → adjudicate(allowContinue:false) retry/halt |
| 175 | if (envBlocked(bg)) { adjudicate; 仍 blocked → throw HALT } | 179 | if (envBlocked(bg)) { adjudicate; 仍 blocked → throw HALT } |
| 176 | // 3) 软文字:for-of 走 adjudicate;continue→recordDecisions + 加入跨轮 softPassed;sentinel→并入 behaviorHard;retry/halt 同现 | 180 | // 3) 软文字:for-of 走 adjudicate;continue→recordDecisions + 加入跨轮 softPassed;sentinel→并入 behaviorHard;retry/halt 同现 |
| 177 | processTextIssues(bg, softPassed) // softPassed 提升到 reviewWithFixLoop 顶层作用域,跨 behaviorRound 持久 | 181 | processTextIssues(bg, softPassed) // softPassed 提升到 reviewWithFixLoop 顶层作用域,跨 behaviorRound 持久 |
| 182 | + // 3.6) 覆盖率对账(确定性兜底):未被路由级 coverageGap 解释的漏达路由(routesReached<routesPlanned) → adjudicate(allowContinue:false) | ||
| 183 | + if (planned>0 && planned-reached-routeGapCount > 0) { v=adjudicate(allowContinue:false); v!=='retry' → throw HALT; else 下一轮重跑 } | ||
| 178 | // 4) behaviorHard = interactionFailures + sentinel textIssues | 184 | // 4) behaviorHard = interactionFailures + sentinel textIssues |
| 179 | if (behaviorHard.length === 0) return {green:true} | 185 | if (behaviorHard.length === 0) return {green:true} |
| 180 | // 5) 分流 | 186 | // 5) 分流 |
| @@ -286,3 +292,15 @@ async function behaviorSubGate(id, specPath, feScope): | @@ -286,3 +292,15 @@ async function behaviorSubGate(id, specPath, feScope): | ||
| 286 | 7. README / coding-start SKILL 文案。 | 292 | 7. README / coding-start SKILL 文案。 |
| 287 | 293 | ||
| 288 | 每步后端分支必须逐字不变(diff 校验);运行时红线(time/random builtin / 顶层 return / 注入全局)每步复核。 | 294 | 每步后端分支必须逐字不变(diff 校验);运行时红线(time/random builtin / 顶层 return / 注入全局)每步复核。 |
| 295 | + | ||
| 296 | +--- | ||
| 297 | + | ||
| 298 | +## 12. 评审加固(实现后多代理评审产出,已落地) | ||
| 299 | + | ||
| 300 | +实现后用多代理评审(6 维 + 逐发现对抗复核)核验本设计的逻辑与目标达成。控制流(approve 合取不变量、fix 轮边界、无 stale-green、残留清除)与运行时红线(确定性 builtin / 顶层 return / 16 schema 合法)全部通过。目标维度(「每控件可用 / 每文字正确」)判为 mostly-achieves,确认了若干 escapability 缺口,其中确定性、低风险的三项已加固: | ||
| 301 | + | ||
| 302 | +1. **`build-failed` 短路加确定性前置(头号 must-fix)**:`behaviorSubGate` 在 green-by-skip 前校验 (a) `rootCausePath` 非空、(b) 无交互/`sentinel` 硬问题搭车;任一不满足 → `adjudicate(allowContinue:false)` retry/halt,绝不凭未校验的 LLM 归因静默放行。骨架(lazy router + FeStub)令合法的兄弟未实现 build-failed 极罕见,故一个 build-failed 更可能是本 FE 真共享代码回归——这是此前 comment §107-108 声称 load-bearing 却无 JS 兜底的边界。 | ||
| 303 | +2. **覆盖率对账兜底(§3.6)**:空覆盖此前只兜 `==0`;新增 `routesReached < routesPlanned` 且缺口未被「路由级 coverageGap」解释时 → `adjudicate(allowContinue:false)`,堵住「部分覆盖假绿」(只数路由级 reason,过计只抑制本门、绝不误 halt)。 | ||
| 304 | +3. **测试库护栏直接 halt 落地**:用 `TESTDB_GUARD_MARK` 标记 + `behaviorTestDbGuardTripped` 兑现 step2「不重试不仲裁直接 halt」承诺(详见 §5)。 | ||
| 305 | + | ||
| 306 | +未改(确认为可接受的设计取舍,留作后续):白名单/路由作用域自证(非从 live router/DOM 独立枚举)、非数据文字按 source 软分流、disabled 控件仅提交类有 should-work 复核——均为有意取舍,记于此供后续权衡。 |
workflows/coding.mjs
| @@ -614,7 +614,7 @@ function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { | @@ -614,7 +614,7 @@ function behaviorGatePrompt(id, specPath, behaviorRound, attempt) { | ||
| 614 | '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', | 614 | '- **未建兄弟路由既不计入分母也不计 coverageGap**(属预期中途态,按 step0 归 build-failed 短路)。', |
| 615 | '', | 615 | '', |
| 616 | '## step2 安全护栏 + 起栈四段严格时序(schema 由 Flyway 在后端启动时才建)', | 616 | '## step2 安全护栏 + 起栈四段严格时序(schema 由 Flyway 在后端启动时才建)', |
| 617 | - `1) **测试库安全护栏**:测试库命名护栏现已下沉到 \`${ROOT}/scripts/setup-test-db.mjs\` 模板自身(确定性 JS 边界,库名须含 test/_test/_dev/_local,否则 fail-closed,\`ALLOW_NONTEST_DROP=1\` 显式放行)。runner 可复述但模板是唯一防线;若模板因测试库护栏非零退出 → 返回 \`status:red\` + 在 detail 写明「测试库护栏触发」(上层对此**不重试不仲裁直接 halt**,留人工确认)。`, | 617 | + `1) **测试库安全护栏**:测试库命名护栏现已下沉到 \`${ROOT}/scripts/setup-test-db.mjs\` 模板自身(确定性 JS 边界,库名须含 test/_test/_dev/_local,否则 fail-closed,\`ALLOW_NONTEST_DROP=1\` 显式放行)。runner 可复述但模板是唯一防线;若模板因测试库护栏非零退出 → 返回 \`status:red\` + \`envError.kind="stack-not-ready"\` + \`envError.detail\` **以固定标记 \`${TESTDB_GUARD_MARK}\` 开头**并写明「测试库护栏触发」+ 库名。上层据此标记**不重试不仲裁直接 halt**(留人工确认)——务必只在确为护栏触发时打此标记,其它起栈失败照常用普通 detail。`, |
| 618 | `2) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程。`, | 618 | `2) \`node ${ROOT}/scripts/setup-test-db.mjs\`(DROP+CREATE 空库)。DROP 前按 \`${tmpDir}/*.pid\` / 既知端口优雅回收残留进程。`, |
| 619 | '3) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', | 619 | '3) **起后端**:spawn 到后台 + 轮询 `/actuator/health` 或登录端点 200(Flyway 在此 apply 建 schema);端口取 config-vars,先探测占用,占用则回收残留或退到动态空闲端口 + 把 baseURL 注入下游。', |
| 620 | '4) **此时才跑种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** 种子(先父后子)。失败 → `envError.kind="seed-error"` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不**混进交互 RED。', | 620 | '4) **此时才跑种子**:按 `docs/03-数据库设计文档.md` 派生 **FK 有序 INSERT** 种子(先父后子)。失败 → `envError.kind="seed-error"` + 结构化根因(缺列 / 撞唯一键 / enum 越界 / FK 序错 / 类型截断),**不**混进交互 RED。', |
| @@ -757,6 +757,8 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限 | @@ -757,6 +757,8 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限 | ||
| 757 | // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 | 757 | // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 |
| 758 | const BEHAVIOR_FE_MAX = 3 | 758 | const BEHAVIOR_FE_MAX = 3 |
| 759 | const BEHAVIOR_ATTEMPT_MAX = 2 | 759 | const BEHAVIOR_ATTEMPT_MAX = 2 |
| 760 | +// 测试库护栏触发的确定性标记:门子代理在 envError.detail 以此开头,JS 据此「不重试不仲裁直接 halt」(兑现 step2 第 1 条承诺)。 | ||
| 761 | +const TESTDB_GUARD_MARK = '[TESTDB-GUARD]' | ||
| 760 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' | 762 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' |
| 761 | 763 | ||
| 762 | // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 | 764 | // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 |
| @@ -1646,6 +1648,11 @@ function behaviorEnvBlocked(r) { | @@ -1646,6 +1648,11 @@ function behaviorEnvBlocked(r) { | ||
| 1646 | return { ev, emptyCov, blocked: !!ev || emptyCov } | 1648 | return { ev, emptyCov, blocked: !!ev || emptyCov } |
| 1647 | } | 1649 | } |
| 1648 | function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] } | 1650 | function behaviorIfails(r) { return Array.isArray(r.interactionFailures) ? r.interactionFailures : [] } |
| 1651 | +// 测试库护栏触发判定:门子代理按 step2 第 1 条在 envError.detail 打 TESTDB_GUARD_MARK;命中即确定性 halt(不重试不仲裁)。 | ||
| 1652 | +function behaviorTestDbGuardTripped(r) { | ||
| 1653 | + const d = (r.envError && r.envError.detail) || '' | ||
| 1654 | + return typeof d === 'string' && d.includes(TESTDB_GUARD_MARK) | ||
| 1655 | +} | ||
| 1649 | 1656 | ||
| 1650 | // runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 | 1657 | // runBehaviorGateOnce:跑一次本 FE 行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 |
| 1651 | // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。 | 1658 | // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 behaviorSubGate 推进)。 |
| @@ -1657,6 +1664,10 @@ async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) { | @@ -1657,6 +1664,10 @@ async function runBehaviorGateOnce(id, specPath, grp, behaviorRound) { | ||
| 1657 | {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) | 1664 | {label: lbl(attempt), phase: grp, schema: BEHAVIOR_GATE_SCHEMA}) |
| 1658 | recordDecisions(`behavior:${id}`, bg.decisions) | 1665 | recordDecisions(`behavior:${id}`, bg.decisions) |
| 1659 | 1666 | ||
| 1667 | + // 测试库护栏触发 → 不重试不仲裁直接 halt(兑现 step2 第 1 条承诺;首个结果即拦截,绝不进重试/仲裁烧预算)。 | ||
| 1668 | + if (behaviorTestDbGuardTripped(bg)) | ||
| 1669 | + throw new Error(`HALT behavior-testdb-guard ${id}: 测试库护栏触发(库名非测试库),不重试不仲裁直接 halt 待人工确认 — ${(bg.envError || {}).detail || ''}`) | ||
| 1670 | + | ||
| 1660 | // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。 | 1671 | // build-failed 短路:根因落非本 FE 路径(兄弟未实现)→ 直接返回(外层据此放行 approve),不重试不仲裁。 |
| 1661 | const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed' | 1672 | const isBuildFailedShortCircuit = (r) => r.envError && r.envError.kind === 'build-failed' |
| 1662 | if (isBuildFailedShortCircuit(bg)) return bg | 1673 | if (isBuildFailedShortCircuit(bg)) return bg |
| @@ -1696,13 +1707,34 @@ async function behaviorSubGate(id, specPath, grp, softPassed) { | @@ -1696,13 +1707,34 @@ async function behaviorSubGate(id, specPath, grp, softPassed) { | ||
| 1696 | for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) { | 1707 | for (let behaviorRound = 1; behaviorRound <= BEHAVIOR_FE_MAX; behaviorRound++) { |
| 1697 | const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound) | 1708 | const bg = await runBehaviorGateOnce(id, specPath, grp, behaviorRound) |
| 1698 | 1709 | ||
| 1699 | - // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → 记 coverageGap + decisions,子门 green-by-skip 放行。 | 1710 | + // 1) build-failed 短路(依赖 B):兄弟未实现 / 占位未覆盖 → green-by-skip 放行。但骨架(lazy router + FeStub) |
| 1711 | + // 令「合法的兄弟未实现 build-failed」极罕见,故一个 build-failed 更可能是本 FE 引入的真共享代码回归; | ||
| 1712 | + // 绝不凭未校验的 LLM 归因静默放行——先过确定性前置校验(comment §107-108 声称 load-bearing 的边界,此前无 JS 兜底): | ||
| 1713 | + // a) 必须有 rootCausePath(否则无从判定根因落点); | ||
| 1714 | + // b) 不得同时携带交互硬问题(interactionFailures / source=sentinel 文字)——那是真缺陷搭车。 | ||
| 1715 | + // 任一不满足 = 「脏」build-failed → 不短路,过 adjudicate(allowContinue:false) retry/halt,绝不 green-by-skip。 | ||
| 1700 | if (bg.envError && bg.envError.kind === 'build-failed') { | 1716 | if (bg.envError && bg.envError.kind === 'build-failed') { |
| 1717 | + const rootCausePath = (bg.envError.rootCausePath || '').trim() | ||
| 1718 | + const hardRiders = behaviorIfails(bg).length | ||
| 1719 | + + (Array.isArray(bg.textIssues) ? bg.textIssues : []).filter(t => t && t.source === 'sentinel').length | ||
| 1720 | + const dirty = !rootCausePath | ||
| 1721 | + ? 'build-failed 未给 rootCausePath(无法判定根因是否落在本 FE 之外)' | ||
| 1722 | + : hardRiders | ||
| 1723 | + ? `build-failed 同时携带 ${hardRiders} 项交互/sentinel 硬问题(疑似本 FE 真构建 bug 搭车)` | ||
| 1724 | + : null | ||
| 1725 | + if (dirty) { | ||
| 1726 | + const verdict = await adjudicate(`behavior-buildfailed-dirty:${id}`, | ||
| 1727 | + { problem:`build-failed 归因不可信,绝不短路放行:${dirty}(rootCausePath=${rootCausePath || '∅'})`, | ||
| 1728 | + envError: bg.envError, allowContinue:false }, grp, behaviorRound) | ||
| 1729 | + if (verdict.action !== 'retry') throw new Error(`HALT behavior-buildfailed ${id}: ${verdict.rationale || dirty}`) | ||
| 1730 | + continue // retry → 下一 behaviorRound 重跑整门 | ||
| 1731 | + } | ||
| 1732 | + // 干净的 build-failed(有 rootCausePath 且无硬问题搭车)→ green-by-skip 放行,记低置信证据。 | ||
| 1701 | recordDecisions(`behavior-build-failed:${id}`, [{ | 1733 | recordDecisions(`behavior-build-failed:${id}`, [{ |
| 1702 | - question:`本 FE ${id} 行为验收遇 build-failed(根因 ${bg.envError.rootCausePath || '?'})`, | 1734 | + question:`本 FE ${id} 行为验收遇 build-failed(根因 ${rootCausePath})`, |
| 1703 | choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)', | 1735 | choice:'green-by-skip(兄弟 FE 未实现属预期中途态,本 FE 非缺陷,放行 approve)', |
| 1704 | rationale: bg.envError.detail || '', confidence:'low' }]) | 1736 | rationale: bg.envError.detail || '', confidence:'low' }]) |
| 1705 | - log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${bg.envError.rootCausePath || '?'}),记证据不阻断`) | 1737 | + log(`behavior ${id}: build-failed 短路放行(根因非本 FE:${rootCausePath}),记证据不阻断`) |
| 1706 | return | 1738 | return |
| 1707 | } | 1739 | } |
| 1708 | 1740 | ||
| @@ -1746,13 +1778,29 @@ async function behaviorSubGate(id, specPath, grp, softPassed) { | @@ -1746,13 +1778,29 @@ async function behaviorSubGate(id, specPath, grp, softPassed) { | ||
| 1746 | continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) | 1778 | continue // retry → 重跑本 FE 行为验收(下一 behaviorRound) |
| 1747 | } | 1779 | } |
| 1748 | 1780 | ||
| 1781 | + // 3.6) 覆盖率对账(确定性兜底):空覆盖只兜 ==0;这里兜 0<routesReached<routesPlanned 的「部分覆盖假绿」。 | ||
| 1782 | + // 每条 planned-but-unreached 路由必须由「路由级 coverageGap」解释;未被解释的漏达路由 = 静默漏验,绝不判 green。 | ||
| 1783 | + // 只数路由级 reason(控件级 deep-control-not-driven / locator-not-resolvable 不抵漏达路由);过计只会抑制本门、绝不误 halt。 | ||
| 1784 | + const planned = Number(bg.routesPlanned) || 0 | ||
| 1785 | + const reached = Number(bg.routesReached) || 0 | ||
| 1786 | + const ROUTE_GAP = new Set(['unreachable-auth', 'unreachable-no-route', 'dynamic-route-no-seed', 'build-failed-sibling-unimpl']) | ||
| 1787 | + const routeGapCount = (Array.isArray(bg.coverageGaps) ? bg.coverageGaps : []).filter(cg => cg && ROUTE_GAP.has(cg.reason)).length | ||
| 1788 | + const unaccounted = planned - reached - routeGapCount | ||
| 1789 | + if (planned > 0 && unaccounted > 0) { | ||
| 1790 | + const verdict = await adjudicate(`behavior-undercoverage:${id}`, | ||
| 1791 | + { problem:`本 FE 路由覆盖不足:routesPlanned=${planned} routesReached=${reached},仅 ${routeGapCount} 条有路由级 coverageGap 解释,尚有 ${unaccounted} 条漏达路由无证据(绝不带静默漏达判 green)`, | ||
| 1792 | + coverageGaps: bg.coverageGaps || [], allowContinue: false }, grp, behaviorRound) | ||
| 1793 | + if (verdict.action !== 'retry') throw new Error(`HALT behavior-undercoverage ${id}: ${verdict.rationale || `${unaccounted} 条漏达路由无证据`}`) | ||
| 1794 | + continue // retry → 下一 behaviorRound 重跑整门 | ||
| 1795 | + } | ||
| 1796 | + | ||
| 1749 | // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。 | 1797 | // 4) behaviorHard = interactionFailures(含 binding-garbage)+ source=='sentinel' textIssues。 |
| 1750 | const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : []) | 1798 | const sentinelHard = (Array.isArray(bg.textIssues) ? bg.textIssues : []) |
| 1751 | .filter(t => t && t.source === 'sentinel') | 1799 | .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 })) | 1800 | .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] | 1801 | const behaviorHard = [...behaviorIfails(bg), ...sentinelHard] |
| 1754 | 1802 | ||
| 1755 | - // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)→ 子门 green 放行。 | 1803 | + // 5) green 判定:behaviorHard 为空 ∧ 无 B 类未覆盖 ∧ 覆盖非空(已兜底)∧ 无未解释漏达路由(§3.6 已兜底)→ 子门 green 放行。 |
| 1756 | if (behaviorHard.length === 0) { | 1804 | if (behaviorHard.length === 0) { |
| 1757 | log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) | 1805 | log(`behavior ${id} green(behaviorRound=${behaviorRound} routesPlanned=${bg.routesPlanned} routesReached=${bg.routesReached} controls=${bg.controlsEnumerated} authState=${bg.authState || '?'})`) |
| 1758 | return | 1806 | return |