Commit a5d6863d9ea871c5c0625dd999d99dad13fc0d9f

Authored by yanghl
1 parent bc9f593a

coding.mjs: 新增静态原型渲染门 Preview——截图基线 + 原型交互冒烟硬门

前端段开头一次性 Playwright headless 渲染 prototype/**/*.html 本身:
截图归档为可视化基线 + 跑原型自带交互点击冒烟 + 捕获 JS/console 错误。
与行为门正交(Preview 测静态原型、Behavior 测实现)。硬门:渲染失败 /
脚本未捕获异常(jsErrors)→仲裁(只许 retry/halt),halt 即停下等人工回
plan/add-req 修原型后重跑(coding 不改原型源码);console.error advisory;
环境未就绪(Playwright/浏览器缺失)才 retry→仲裁降级;无 prototype/ 跳过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README.md
@@ -54,6 +54,11 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 @@ -54,6 +54,11 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。
54 54
55 └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) 55 └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag)
56 runBranchSetup(frontend-phase) 56 runBranchSetup(frontend-phase)
  57 + → 静态原型渲染门 preview(前端段开头:Playwright headless 逐页渲染 prototype/**/*.html 本身
  58 + → 截图归档为可视化基线 + 跑原型自带交互点击冒烟 + 捕获 JS/console 错误。与行为门正交——
  59 + 前者测静态原型、后者测实现。硬门:原型渲染失败 / 脚本未捕获异常(jsErrors)→仲裁(只许 retry/halt),
  60 + halt 即停下等人工回 plan/add-req 修原型后重跑(coding 不改原型源码);console.error 仅 advisory;
  61 + 唯环境未就绪(Playwright/浏览器缺失)才 retry→仲裁降级。无 prototype/ 则跳过)
57 → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起; 62 → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起;
58 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState; 63 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState;
59 含单测基线:vitest include 限定 tests/**/*.test.*——单测一律 frontend/tests/ 镜像 src/, 64 含单测基线:vitest include 限定 tests/**/*.test.*——单测一律 frontend/tests/ 镜像 src/,
workflows/coding.mjs
@@ -8,12 +8,16 @@ export const meta = { @@ -8,12 +8,16 @@ export const meta = {
8 name: 'erp-coding', 8 name: 'erp-coding',
9 description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.', 9 description: 'Run the entire ERP coding phase autonomously and silently: per-module backend+frontend feature loops, test gate, milestone tag.',
10 phases: [ 10 phases: [
11 - { title: 'Router' }, { title: 'Schedule' }, { title: 'Backend' }, { title: 'Frontend' }, 11 + { title: 'Router' }, { title: 'Schedule' }, { title: 'Backend' }, { title: 'Preview' }, { title: 'Frontend' },
12 { title: 'Behavior' }, { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' }, 12 { title: 'Behavior' }, { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' },
13 ], 13 ],
14 // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次** 14 // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次**
15 // 行为验收(含 fix 循环),不再在每个 FE 的 review 循环内做 approve 子门(v2 per-FE 形态已撤销)。 15 // 行为验收(含 fix 循环),不再在每个 FE 的 review 循环内做 approve 子门(v2 per-FE 形态已撤销)。
16 // 时序:featureLoop(frontend) → Behavior(行为门+fix)→ Gate(testGate 全量回归,兜底行为 fix 引入的回归)。 16 // 时序:featureLoop(frontend) → Behavior(行为门+fix)→ Gate(testGate 全量回归,兜底行为 fix 引入的回归)。
  17 + // 注:'Preview' = 静态原型渲染门——前端段开头一次性渲染 prototype/**/*.html 本身(Playwright headless
  18 + // 截图归档 + 原型自带交互点击冒烟)。与 Behavior 正交:Preview 测**静态原型**、Behavior 测**实现**。
  19 + // 硬门:原型渲染失败 / 脚本未捕获异常(jsErrors) → halt(原型属上游 plan 权威,coding 不改原型源码,
  20 + // 应回 plan/add-req 修后重跑);console.error advisory;环境未就绪 retry→仲裁。时序:Preview → Frontend(骨架+featureLoop)。
17 } 21 }
18 22
19 const ROUTER_SCHEMA = { type:'object', additionalProperties:false, 23 const ROUTER_SCHEMA = { type:'object', additionalProperties:false,
@@ -154,6 +158,38 @@ const BEHAVIOR_GATE_SCHEMA = { type:&#39;object&#39;, additionalProperties:false, @@ -154,6 +158,38 @@ const BEHAVIOR_GATE_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
154 confidence:{type:'string', enum:['high','medium','low']} } } }, 158 confidence:{type:'string', enum:['high','medium','low']} } } },
155 artifactPath:{type:'string'} } } 159 artifactPath:{type:'string'} } }
156 160
  161 +// PROTOTYPE_PREVIEW_SCHEMA:静态原型渲染门(phase('Preview'))返回。与 BEHAVIOR_GATE 正交——
  162 +// 后者测「实现」(冷起全栈 + Playwright 驱动真前端),本门只渲染 *静态* prototype/**/*.html 本身:
  163 +// 逐页 headless 渲染→截图归档(可视化基线)→跑原型自带交互点击冒烟→捕获 JS/console 错误。
  164 +// status:ok=渲染门正常跑完(含 advisory 问题);blocked=仅限环境未就绪(Playwright/浏览器缺失/超时)需仲裁。
  165 +// 原型属上游 plan 权威、coding 绝不改原型源码——故渲染/JS 问题是 advisory(记 decisions+证据,绝不硬 halt)。
  166 +const PROTOTYPE_PREVIEW_SCHEMA = { type:'object', additionalProperties:false,
  167 + required:['status','pagesFound','pagesRendered'], properties:{
  168 + status:{type:'string', enum:['ok','blocked']},
  169 + pagesFound:{type:'integer'}, // Glob 到的 prototype/**/*.html 总数
  170 + pagesRendered:{type:'integer'}, // 成功渲染并截图的页数
  171 + pages:{ type:'array', items:{ type:'object', additionalProperties:false,
  172 + required:['file','rendered'],
  173 + properties:{
  174 + file:{type:'string'}, // prototype 相对路径
  175 + rendered:{type:'boolean'},
  176 + screenshot:{type:'string'}, // assets 相对路径(已归档,纳入版本管理)
  177 + controlsExercised:{type:'integer'}, // 实际驱动(点击/切换)过的控件数
  178 + jsErrors:{type:'array', items:{type:'string'}}, // pageerror 文本(原型自身脚本质量信号)
  179 + consoleErrors:{type:'array', items:{type:'string'}}, // console.error 文本
  180 + notes:{type:'string'} } } },
  181 + // envError:仅环境未就绪(与「原型自身有问题」严格区分——后者是 advisory,不进 envError)。
  182 + envError:{ type:'object', additionalProperties:false,
  183 + required:['kind'], properties:{
  184 + kind:{type:'string', enum:['playwright-missing','browser-missing','timeout','none']},
  185 + detail:{type:'string'} } },
  186 + decisions:{ type:'array', items:{ type:'object', additionalProperties:false,
  187 + required:['question','choice','rationale'],
  188 + properties:{
  189 + question:{type:'string'}, choice:{type:'string'}, rationale:{type:'string'},
  190 + confidence:{type:'string', enum:['high','medium','low']} } } },
  191 + artifactPath:{type:'string'} } }
  192 +
157 // ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)───────── 193 // ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)─────────
158 const WT_SCHEMA = { type:'object', additionalProperties:false, 194 const WT_SCHEMA = { type:'object', additionalProperties:false,
159 required:['clean'], properties:{ 195 required:['clean'], properties:{
@@ -838,6 +874,72 @@ function seedGenPrompt(module, c) { @@ -838,6 +874,72 @@ function seedGenPrompt(module, c) {
838 ].filter(Boolean).join('\n') 874 ].filter(Boolean).join('\n')
839 } 875 }
840 876
  877 +// ---- 静态原型渲染门(Preview)prompt ----
  878 +// 与下方「前端行为验收」正交:行为门测**实现**(冷起全栈 + Playwright 驱动真前端),本门只渲染
  879 +// **静态** prototype/**/*.html 本身——逐页 headless 渲染→截图归档(可视化基线)→跑原型自带交互点击
  880 +// 冒烟→捕获 JS/console 错误。原型是 plan/add-req 的上游权威产物:本门**只渲染不修改**,原型自身问题
  881 +// 走 advisory(记证据,不硬 halt)。不起后端/前端 app、不跑 setup-test-db/seed。
  882 +
  883 +// prototypePreviewContract:渲染门硬约束(只读、不起全栈、唯一可写区 = .tmp + assets + 证据报告)。
  884 +function prototypePreviewContract(c) {
  885 + return [
  886 + '## 硬约束(非交互原型渲染子代理)',
  887 + '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹 AskUserQuestion / 等人输入。**绝不要尝试问人**。',
  888 + '- 你是**静态原型只读渲染门**:用 Playwright headless 渲染 `prototype/**/*.html` 本身、截图、跑原型自带交互——**不是**实现功能、**不是**起后端/前端 app、**不是**改任何源码。',
  889 + `- **作用域(唯一可写区)**:\`${c.root}/.tmp/prototype-preview/\`(一次性 runner,跑完即弃)+ 截图归档到 \`${c.root}/docs/superpowers/module-reports/assets/prototype/\` + 证据报告 \`${c.root}/docs/superpowers/module-reports/prototype-preview.md\`。`,
  890 + '- **越界硬停**:**绝不**编辑 `prototype/` / `frontend/` / `backend/` / `sql/` / `scripts/` 下任何文件——原型是上游 plan 的权威产物,本门只渲染不修改。命中越界即以 `status:blocked` 或写清阻塞点结束。',
  891 + '- **不起全栈**:本门只渲染静态 HTML(`file://` 或仅服务 `prototype/` 的临时静态服务器),**绝不**冷起后端 / 前端 dev server、**绝不**跑 setup-test-db / seed。',
  892 + '- 证据报告**使用中文**;文件名 / 选择器标识符可用英文。',
  893 + '- **运行时确定性**:视口尺寸 / 临时目录名 / 截图文件名 / 临时端口一律确定性派生(按页面相对路径 / attempt 序号),**绝不**依赖时间戳 / 随机数。',
  894 + ].join('\n')
  895 +}
  896 +
  897 +// prototypePreviewPrompt:静态原型渲染门子代理完整流水线(step0-3 + schema)。
  898 +// feItems:本前端阶段全部 FE-NN(仅作分组参考,不强制与原型页一一对应);attempt:环境 race 重试序号(1..)。
  899 +function prototypePreviewPrompt(feItems, attempt, c) {
  900 + const feList = (feItems || []).map(x => `\`${x}\``).join(', ') || '(无 FE 清单)'
  901 + const tmpDir = `${c.root}/.tmp/prototype-preview`
  902 + const assets = 'docs/superpowers/module-reports/assets/prototype'
  903 + const evidence = 'docs/superpowers/module-reports/prototype-preview.md'
  904 + return [
  905 + `# preview — 静态原型渲染门(headless,attempt=${attempt})`,
  906 + '',
  907 + prototypePreviewContract(c),
  908 + '',
  909 + '## 目标',
  910 + '渲染**全部静态原型** `prototype/**/*.html` 本身,产出两类产物:① 每页**截图**(可视化基线,供人工 / 行为门比对);② 原型**自带交互**的点击冒烟(按钮 / 链接 / 表单切换是否触发 JS 错误)。这是对静态设计原型的「查看界面 + 模拟操作」,与实现无关。',
  911 + `- 关联前端功能(仅作分组参考,不强制一一对应):${feList}。`,
  912 + attempt > 1 ? `- 本次 = attempt ${attempt}(上次 \`status:blocked\` 环境未就绪后重试)。` : '',
  913 + '',
  914 + '## step0 发现原型 + 探测环境',
  915 + `- Glob \`${c.root}/prototype/**/*.html\`,得页面清单 = \`pagesFound\`。**空**(项目无原型)→ 直接返回 \`{ "status":"ok", "pagesFound":0, "pagesRendered":0, "pages":[] }\`,不报错、不写任何文件。`,
  916 + '- 探测 Playwright 是否可用(`frontend/node_modules` 内或全局)+ 浏览器内核是否已装。不可用 → `status:"blocked"` + `envError.kind="playwright-missing"|"browser-missing"`(**不要**尝试 `npm install` / `playwright install`——环境就绪由上层处理)。',
  917 + '',
  918 + '## step1 渲染 + 截图(确定性)',
  919 + `- 入口清目录:删除并重建 \`${tmpDir}/\`(确定性、跨平台:\`fs.rmSync(p,{recursive:true,force:true})\` 后 \`fs.mkdirSync(p,{recursive:true})\`;**仅限**此路径)。`,
  920 + `- 在 \`${tmpDir}/\` 写一次性 runner(如 \`run.mjs\`):用 Playwright headless 逐页加载。优先 \`file://\` 直开;若原型用了相对 fetch / 根绝对路径资源导致 file:// 加载不全 → 起一个**仅服务 \`${c.root}/prototype/\` 静态目录**的临时本地服务器(端口确定性派生,\`finally\` 中 teardown)。`,
  921 + '- 视口确定性(默认 1440×900;某原型在 `<meta viewport>` / 注释里声明了特定尺寸则用之并记 `decisions[]`)。每页 `waitForLoadState` 稳定后整页截图。',
  922 + `- 截图归档到**已纳入版本管理**的 \`${assets}/<安全文件名>.png\`(文件名按原型相对路径确定性派生,如 \`prototype/order/list.html\` → \`order__list.png\`;**不要**引用 \`.tmp\` 防断链)。\`pagesRendered\` = 成功截图页数。`,
  923 + '',
  924 + '## step2 原型自带交互冒烟(模拟操作)',
  925 + '- 每页注册 `page.on("pageerror")`(收 JS 异常)与 `page.on("console")`(收 `error` 级);注册 `page.on("dialog")` 自动 dismiss,防原生 `confirm/alert` 阻塞。',
  926 + '- 枚举页面可见可点控件(`button` / `a[href]` / `[role=button]` / `input` / `select` / 带 `onclick` 的元素),逐个**点击 / 切换一次**。`target=_blank` / 会 `window.open` 的只记不点(防新窗暴增)。`controlsExercised` 计实际驱动的控件数。',
  927 + '- 收集每页 `jsErrors`(pageerror 文本)/ `consoleErrors`(console.error 文本)——**原型自身**的脚本质量信号,仅作记录(advisory)。',
  928 + '- `finally` 中 kill 临时静态服务器(若起过)。',
  929 + '',
  930 + '## step3 证据落盘 + commit',
  931 + `- 写 \`${evidence}\`:按页一小节(原型相对路径 / 截图链接 / controlsExercised / jsErrors / consoleErrors / 视口决策),头部写 pagesFound-Rendered 汇总。渲染失败 / JS 错用**红字**标注为**阻断项**(上层据此 halt——回 /erp-workflow:plan-start 或 /add-req 修原型后重跑,**不在 coding 改**);console.error 标注为 advisory。`,
  932 + commitBlock(`${evidence} ${assets}`, 'docs(preview:prototype): 静态原型渲染基线 + 交互冒烟证据', undefined, c),
  933 + '',
  934 + '## 输出(必须符合下发的 PROTOTYPE_PREVIEW JSON schema)',
  935 + '- `status`: `ok`(渲染门正常跑完——即使某页渲染失败/有 JS 错也填 ok,硬/软由上层 JS 据 `pages[]` 分流)| `blocked`(**仅限**环境未就绪:Playwright / 浏览器缺失 / 超时)。',
  936 + '- `pages[]` **据实填**(上层据此判硬门):`rendered=false` 或 `jsErrors` 非空 = 硬缺陷(上层会 halt 等人工修原型);`consoleErrors` = 软信号(advisory)。`pagesFound` / `pagesRendered` 整数据实填;每页 file / rendered / screenshot / controlsExercised / jsErrors / consoleErrors / notes。',
  937 + '- `envError`: 无环境问题填 `{ "kind":"none" }`;环境未就绪填对应 kind + detail。',
  938 + '- 做过自主默认(视口 / 服务方式等)→ `decisions[]` 逐条登记。`artifactPath` = 证据报告项目根相对路径。',
  939 + '- schema 是 `additionalProperties:false`,不要返回额外字段。**不要自动重试**——重试由上层 Workflow 控制。',
  940 + ].filter(Boolean).join('\n')
  941 +}
  942 +
841 // ---- 前端行为验收(阶段级行为门,v3)---- 943 // ---- 前端行为验收(阶段级行为门,v3)----
842 // 设计权威:docs/design/2026-06-05-frontend-behavior-stage-gate.md。 944 // 设计权威:docs/design/2026-06-05-frontend-behavior-stage-gate.md。
843 // 时机:featureLoop(frontend) 全部 FE 通过静态 review(req-done tag 已打)之后、testGate 之前, 945 // 时机:featureLoop(frontend) 全部 FE 通过静态 review(req-done tag 已打)之后、testGate 之前,
@@ -1099,6 +1201,8 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限 @@ -1099,6 +1201,8 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限
1099 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 1201 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。
1100 const BEHAVIOR_STAGE_MAX = 3 1202 const BEHAVIOR_STAGE_MAX = 3
1101 const BEHAVIOR_ATTEMPT_MAX = 2 1203 const BEHAVIOR_ATTEMPT_MAX = 2
  1204 +// 静态原型渲染门(Preview)的环境 race 重起上限(Playwright/浏览器缺失/超时时自动重起,沿用 behavior attempt 思路)。
  1205 +const PROTOTYPE_PREVIEW_ATTEMPT_MAX = 2
1102 const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' 1206 const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试'
1103 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' 1207 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : ''
1104 1208
@@ -2747,6 +2851,85 @@ const isBuildFailed = (r) =&gt; !!(r.envError &amp;&amp; r.envError.kind === &#39;build-failed&#39; @@ -2747,6 +2851,85 @@ const isBuildFailed = (r) =&gt; !!(r.envError &amp;&amp; r.envError.kind === &#39;build-failed&#39;
2747 // runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 2851 // runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。
2748 // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。 2852 // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。
2749 // behaviorRound:阶段门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。 2853 // behaviorRound:阶段门内的行为 fix 轮;内部 attempt 1..BEHAVIOR_ATTEMPT_MAX(环境 race 重起)+ 仲裁兜底。
  2854 +// runPrototypePreview:静态原型渲染门主驱动(被前端段开头调用,phase('Preview') 下)。
  2855 +// 与 runBehaviorGate 正交——本门只渲染静态 prototype/。两类边界严格分流:
  2856 +// - **硬门(halt)**:原型**渲染失败**(rendered=false)或**原型脚本未捕获异常**(jsErrors=pageerror)——
  2857 +// 经仲裁 allowContinue:false(只许 retry/halt)。原型属上游 plan 权威、coding 不改原型源码——故 halt =
  2858 +// 「停下等人工回 /erp-workflow:plan-start 或 /add-req 修原型后重跑」,不是在 coding 里改原型。
  2859 +// - **advisory**:console.error(弱信号,缺图标/CDN 警告等噪声多)——只记 decisions 不阻断。
  2860 +// - **环境未就绪**(Playwright/浏览器缺失/超时):retry→仲裁 allowContinue:true(不让「机器没装浏览器」卡死 coding)。
  2861 +// 终波恒宽 1、主根执行(c.lane==null);静态渲染无 DB、无需 ERP_TEST_DB_SCHEMA 注入。
  2862 +async function runPrototypePreview(feItems, c) {
  2863 + const lbl = (a) => `preview:proto:a${a}`
  2864 + let attempt = 1
  2865 + let pv = await agentR(prototypePreviewPrompt(feItems, attempt, c),
  2866 + {label: lbl(attempt), phase: 'Preview', schema: PROTOTYPE_PREVIEW_SCHEMA})
  2867 + recordDecisions('preview:prototype', pv.decisions, c.decisions)
  2868 + // 环境未就绪 → 自动重起 attempt 1→PROTOTYPE_PREVIEW_ATTEMPT_MAX;仍 blocked → 进仲裁循环。
  2869 + while (pv.status === 'blocked' && attempt < PROTOTYPE_PREVIEW_ATTEMPT_MAX) {
  2870 + attempt += 1
  2871 + pv = await agentR(prototypePreviewPrompt(feItems, attempt, c),
  2872 + {label: lbl(attempt), phase: 'Preview', schema: PROTOTYPE_PREVIEW_SCHEMA})
  2873 + recordDecisions('preview:prototype', pv.decisions, c.decisions)
  2874 + }
  2875 + for (let adj = 1; pv.status === 'blocked' && adj <= ADJUDICATE_MAX; adj++) {
  2876 + const ev = pv.envError || { kind: '?', detail: '' }
  2877 + // allowContinue:true——渲染门只产可视化基线(非硬正确性边界);环境长期缺 Playwright 时仲裁可降级跳过,
  2878 + // 不让「机器没装浏览器」卡死整个 coding(与行为门的 allowContinue:false 严格区分)。
  2879 + const verdict = await adjudicate('preview-env:prototype',
  2880 + { problem: `原型渲染门环境未就绪 envError=${ev.kind}: ${ev.detail || ''}`, envError: pv.envError || null, allowContinue: true },
  2881 + 'Preview', adj, c.root)
  2882 + if (verdict.action === 'continue') {
  2883 + log(`原型渲染门:环境未就绪经仲裁降级为口头建议继续(${ev.kind})——本次跳过原型可视化基线`)
  2884 + recordDecisions('preview:prototype',
  2885 + [{ question: '原型渲染门环境未就绪', choice: '降级继续(跳过可视化基线)', rationale: ev.detail || ev.kind, confidence: 'low' }], c.decisions)
  2886 + return pv
  2887 + }
  2888 + if (verdict.action !== 'retry') throw new Error(`HALT preview-env prototype: ${verdict.rationale || ev.kind}`)
  2889 + attempt += 1
  2890 + pv = await agentR(prototypePreviewPrompt(feItems, attempt, c),
  2891 + {label: lbl(attempt), phase: 'Preview', schema: PROTOTYPE_PREVIEW_SCHEMA})
  2892 + recordDecisions('preview:prototype', pv.decisions, c.decisions)
  2893 + }
  2894 + if (pv.status === 'blocked') throw new Error(`HALT preview-env prototype: ${ADJUDICATE_MAX} 轮仲裁后原型渲染门仍环境未就绪`)
  2895 + // 至此 env 就绪(status:ok)。硬门评估(含仲裁 retry 重跑——疑似一次性渲染抖动/超时被误判时重跑一次门):
  2896 + // 硬缺陷 = 渲染失败(rendered=false) 或 原型脚本未捕获异常(jsErrors);console.error 仅 advisory(弱信号)。
  2897 + for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) {
  2898 + if (pv.pagesFound === 0) {
  2899 + log('原型渲染门:项目无 prototype/**/*.html,跳过(无可视化基线可产)')
  2900 + return pv
  2901 + }
  2902 + const pages = Array.isArray(pv.pages) ? pv.pages : []
  2903 + const hardBroken = pages.filter(p => p && (!p.rendered || (p.jsErrors || []).length))
  2904 + const consoleOnly = pages.filter(p => p && p.rendered && !(p.jsErrors || []).length && (p.consoleErrors || []).length)
  2905 + // advisory:渲染成功但有 console.error 的页只记 decisions,不阻断。
  2906 + if (consoleOnly.length) {
  2907 + recordDecisions('preview:prototype', consoleOnly.map(p => ({
  2908 + question: `原型 ${p.file} 有 console.error`,
  2909 + choice: '记录不阻断(弱信号,回 plan/add-req 酌情修)',
  2910 + rationale: `consoleErrors=${(p.consoleErrors || []).length}`,
  2911 + confidence: 'low' })), c.decisions)
  2912 + }
  2913 + if (!hardBroken.length) {
  2914 + log(`原型渲染门:${pv.pagesRendered}/${pv.pagesFound} 页全部渲染通过(无渲染失败/JS 异常)——可视化基线已归档${consoleOnly.length ? `;${consoleOnly.length} 页有 console.error(advisory)` : ''}`)
  2915 + return pv
  2916 + }
  2917 + const digest = hardBroken.map(p => `${p.file}(rendered=${p.rendered}, jsErrors=${(p.jsErrors || []).length})`).join('; ')
  2918 + // allowContinue:false——原型渲染失败/脚本异常是硬缺陷,仲裁只许 retry/halt,绝不放行带病原型进前端实现。
  2919 + const verdict = await adjudicate('preview-broken:prototype',
  2920 + { problem: `静态原型渲染门硬缺陷(渲染失败 / 原型脚本未捕获异常):${digest}。原型属上游 plan 权威,coding 不改原型源码——应回 /erp-workflow:plan-start 或 /add-req 修原型后重跑。`,
  2921 + allowContinue: false }, 'Preview', adj, c.root)
  2922 + if (verdict.action !== 'retry') throw new Error(`HALT preview-broken prototype: ${verdict.rationale || digest}`)
  2923 + // retry:重跑渲染门一次(仲裁判定疑似一次性,如渲染超时被误标 jsError)。
  2924 + attempt += 1
  2925 + pv = await agentR(prototypePreviewPrompt(feItems, attempt, c),
  2926 + {label: lbl(attempt), phase: 'Preview', schema: PROTOTYPE_PREVIEW_SCHEMA})
  2927 + recordDecisions('preview:prototype', pv.decisions, c.decisions)
  2928 + if (pv.status === 'blocked') throw new Error(`HALT preview-env prototype: 硬门 retry 后又遇环境未就绪 envError=${(pv.envError || {}).kind || '?'}`)
  2929 + }
  2930 + throw new Error(`HALT preview-broken prototype: ${ADJUDICATE_MAX} 轮仲裁后原型仍有硬缺陷(渲染失败/JS 异常)`)
  2931 +}
  2932 +
2750 async function runBehaviorGateOnce(feItems, behaviorRound, c) { 2933 async function runBehaviorGateOnce(feItems, behaviorRound, c) {
2751 const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` 2934 const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}`
2752 let attempt = 1 2935 let attempt = 1
@@ -3180,6 +3363,11 @@ async function runModule(module, c) { @@ -3180,6 +3363,11 @@ async function runModule(module, c) {
3180 await runCrossModule(module, c) // 替代被删 hook,JS 编排:diff → 分类 → 写日志 3363 await runCrossModule(module, c) // 替代被删 hook,JS 编排:diff → 分类 → 写日志
3181 } 3364 }
3182 if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块) 3365 if (module.feItems.length) { // 前端段(仅末尾 frontend-phase 聚合模块)
  3366 + // 静态原型渲染门(Preview):前端段开头先渲染 prototype/**/*.html 本身——Playwright headless
  3367 + // 截图归档为可视化基线 + 跑原型自带交互点击冒烟。与行为门(测实现)正交;advisory(原型属上游
  3368 + // plan 权威,coding 不改原型源码,渲染/JS 问题只记证据不阻断)。静态渲染、不起全栈。终波主根执行。
  3369 + if (c.lane == null) phase('Preview')
  3370 + await runPrototypePreview(module.feItems, c)
3183 if (c.lane == null) phase('Frontend') 3371 if (c.lane == null) phase('Frontend')
3184 // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy 3372 // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy
3185 // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, 3373 // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达,