Commit a5d6863d9ea871c5c0625dd999d99dad13fc0d9f
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>
Showing
2 changed files
with
194 additions
and
1 deletions
README.md
| ... | ... | @@ -54,6 +54,11 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 |
| 54 | 54 | │ |
| 55 | 55 | └─ B-前端(后端全部打里程碑后,整体 1 个里程碑 tag) |
| 56 | 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 | 62 | → 前端骨架占位阶段(router 全量 lazy 路由表 + FeStub 占位,保证中途任意时刻可构建可起; |
| 58 | 63 | 含 e2e 基线脚手架:Playwright globalSetup 按注入时序注种子 + admin 登录 storageState; |
| 59 | 64 | 含单测基线:vitest include 限定 tests/**/*.test.*——单测一律 frontend/tests/ 镜像 src/, | ... | ... |
workflows/coding.mjs
| ... | ... | @@ -8,12 +8,16 @@ export const meta = { |
| 8 | 8 | name: 'erp-coding', |
| 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 | - { title: 'Router' }, { title: 'Schedule' }, { title: 'Backend' }, { title: 'Frontend' }, | |
| 11 | + { title: 'Router' }, { title: 'Schedule' }, { title: 'Backend' }, { title: 'Preview' }, { title: 'Frontend' }, | |
| 12 | 12 | { title: 'Behavior' }, { title: 'Gate' }, { title: 'Seed' }, { title: 'Milestone' }, |
| 13 | 13 | ], |
| 14 | 14 | // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次** |
| 15 | 15 | // 行为验收(含 fix 循环),不再在每个 FE 的 review 循环内做 approve 子门(v2 per-FE 形态已撤销)。 |
| 16 | 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 | 23 | const ROUTER_SCHEMA = { type:'object', additionalProperties:false, |
| ... | ... | @@ -154,6 +158,38 @@ const BEHAVIOR_GATE_SCHEMA = { type:'object', additionalProperties:false, |
| 154 | 158 | confidence:{type:'string', enum:['high','medium','low']} } } }, |
| 155 | 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 | 193 | // ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)───────── |
| 158 | 194 | const WT_SCHEMA = { type:'object', additionalProperties:false, |
| 159 | 195 | required:['clean'], properties:{ |
| ... | ... | @@ -838,6 +874,72 @@ function seedGenPrompt(module, c) { |
| 838 | 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 | 943 | // ---- 前端行为验收(阶段级行为门,v3)---- |
| 842 | 944 | // 设计权威:docs/design/2026-06-05-frontend-behavior-stage-gate.md。 |
| 843 | 945 | // 时机:featureLoop(frontend) 全部 FE 通过静态 review(req-done tag 已打)之后、testGate 之前, |
| ... | ... | @@ -1099,6 +1201,8 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限 |
| 1099 | 1201 | // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 |
| 1100 | 1202 | const BEHAVIOR_STAGE_MAX = 3 |
| 1101 | 1203 | const BEHAVIOR_ATTEMPT_MAX = 2 |
| 1204 | +// 静态原型渲染门(Preview)的环境 race 重起上限(Playwright/浏览器缺失/超时时自动重起,沿用 behavior attempt 思路)。 | |
| 1205 | +const PROTOTYPE_PREVIEW_ATTEMPT_MAX = 2 | |
| 1102 | 1206 | const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' |
| 1103 | 1207 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' |
| 1104 | 1208 | |
| ... | ... | @@ -2747,6 +2851,85 @@ const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed' |
| 2747 | 2851 | // runBehaviorGateOnce:跑一次阶段级行为验收(含内部 envError attempt 重试 + 空覆盖兜底)。 |
| 2748 | 2852 | // 返回最终 bg(BEHAVIOR_GATE_SCHEMA);不在内部收敛交互/文字(交给外层 runBehaviorGate 推进)。 |
| 2749 | 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 | 2933 | async function runBehaviorGateOnce(feItems, behaviorRound, c) { |
| 2751 | 2934 | const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` |
| 2752 | 2935 | let attempt = 1 |
| ... | ... | @@ -3180,6 +3363,11 @@ async function runModule(module, c) { |
| 3180 | 3363 | await runCrossModule(module, c) // 替代被删 hook,JS 编排:diff → 分类 → 写日志 |
| 3181 | 3364 | } |
| 3182 | 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 | 3371 | if (c.lane == null) phase('Frontend') |
| 3184 | 3372 | // 前端骨架占位 stage(设计 § 2,前置依赖 A):featureLoop 之前一次性建 App 外壳 + router 全量 lazy |
| 3185 | 3373 | // 路由表(FeStub 占位)+ 无悬空导航——保证逐 FE 实现中途任意时刻 app 可构建可起、每 FE 路由可达, | ... | ... |