Files
mini-openclaw-v2/scripts/test_prescreen_ui.cjs
T
qiaoqichaoandClaude Code 2e7e19471c feat(v2): 纠偏第二批——接通历史经验加工闭环
覆盖 AC-50/51/52(BR-48/49、E-28)。审计确认的 P1-3 缺口"历史提炼与卡片
不在实时 API 调用图上"由此关闭:提炼、候选治理、卡片进方案全部有真实调用方。

后端:
- contracts:新增 E-28 EXTRACTION_UNAVAILABLE(运行级,不进 HTTP 信封)与
  KnowledgeExtraction 记录类型。契约层强制"失败不得带候选",
  AC-50「模型不可用⇒不产生候选」是类型约束而不是文档约定。
- services/experience_extraction.py(新):真实提炼入口。模型不可用或调用失败
  → 记 E-28 且 candidate_id 为空;不产半成品候选。传输纪律同 assistant_model
  (禁进程内代理、零重试),且 tools=[] 使"越权读材料"在结构上不可能。
  自然语言修订同样必须真实调用模型,没有"把中文猜成条件"的回退路径。
- services/knowledge_cards.py:新增 current_draft/revise/list_cards 与
  registry_conditions。修订只产草稿、保留 origin 与来源、升版本记 parent_version;
  test_cases 纳入可改字段——它是 component_evaluation 对自定义 Skill 的发布门禁,
  不开放则提炼出的候选永远卡在评测门外。
- services/project_assistant.py:enqueue 改用 review_plan.assemble(),
  让 T06-b 的依赖预检(E-19 可定位文案)与维度解析不再被绕过。
- api/prescreen.py:case-timeline、knowledge-extractions、knowledge-candidates
  (创建/修订/状态机)、review-plans(装配/状态机)。角色门禁与既有
  /components 入口一致;id 与数据域由服务端生成;按 id 的路由一律在 run() 内
  查归属,否则 KeyError 会变成 500 而不是 404。
- component_registry/review_plan:下沉 current_draft/version_key 与 list_plans。

前端:
- 新增 /experience-center 独立入口(BR-50)与 ExperienceCenter 组件;
  AgentNav 增加「历史经验加工」。
- originBadge 此前零调用方、.origin-* 无任何 CSS——守卫只 grep 库文件字符串,
  于是"测试通过但功能不存在"。本批补齐消费方与样式,并用左边框样式区分四种来源,
  保证去色仍可区分(AC-42/BR-48)。
- 守卫语义按 AC-55 调整:由"禁止新建路由"改为"禁止重复页面"
  (面板不得搬到独立路由、已合并页必须保持重定向),并新增
  experience-center-ui 守卫(已登记 verify.py,共 12 项检查)。

测试:新增 36 项(提炼 15 / 候选治理 15 / 卡片到运行 6),全量 unittest 无回归;
两次同项目运行对照证明卡片确实改变了核查结果,且 basis_ids 指向 卡片id@版本。

未闭合(记录在 17 的剩余缺口):真实模型提炼尚未实测(需端点);
界面未经浏览器点击验证;例外边界未进入可求值语言;规则类候选不在本批。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 00:31:42 +08:00

103 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* T09 预审工作台与沙盘界面源码守卫。
*
* 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2 已明确
* 现有 UI 脚本的性质)。它锁住的是可机械核对的约束:数值只能来自服务端、
* 演示值只在演示域、雷达只画 3–8 维、乱序响应丢弃、失败保留基线、
* 图的等价文字与键盘可达。
*/
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = path.resolve(__dirname, "..");
const read = (file) => fs.readFileSync(path.join(root, file), "utf8");
const panel = read("src/components/xingbang/PrescreenPanel.tsx");
const lib = read("src/lib/prescreen-run.ts");
const workbench = read("src/app/xingbang-workbench/page.tsx");
// 1. 面板挂载在既有工作台内
assert.match(workbench, /import PrescreenPanel from "@\/components\/xingbang\/PrescreenPanel"/, "工作台应导入预审面板");
assert.match(workbench, /<PrescreenPanel /, "工作台应渲染预审面板");
// AC-55(16-delivery-corrections):不再禁止新增路由,改为禁止**重复页面**。
// 两条机械判据:① 面板自己的路由目录仍然不得出现(那是把面板搬走、旧页变空壳);
// ② 已合并的旧页必须仍是重定向,不得被重新实现成独立页面。
for (const banned of ["prescreen", "sandbox", "simulation"]) {
assert.ok(!fs.existsSync(path.join(root, "src/app", banned)), `不得把面板搬到独立路由 src/app/${banned}`);
}
for (const merged of ["trajectory", "review-workbench", "report-workbench", "collateral-workbench"]) {
assert.match(read(`src/app/${merged}/page.tsx`), /redirect\(/,
`已合并页 src/app/${merged} 必须保持重定向,不得恢复成独立页面`);
}
// 2. 复用既有样式,不新增样式表
assert.ok(!/^import .*\.css";/m.test(panel), "面板应复用 workspace.css 既有 class,不新增样式表");
// 3. 数值只能来自服务端:前端不得复制演示情景表(AC-30/31)
for (const [name, source] of [["panel", panel], ["lib", lib]]) {
assert.ok(!/0\.(42|58|51|64)\b/.test(source), `${name} 不得硬编码演示概率值`);
assert.ok(!/"(00|10|01|11)"\s*:/.test(source), `${name} 不得在客户端复制演示情景表`);
}
assert.match(panel, /simulation\.probability_view/, "情景数值必须取自服务端响应");
// 4. 演示值只在演示域显示,且紧邻"人工设定"说明(AC-12/30、DESIGN §38)
assert.match(panel, /const domainName = run\?\.data_domain/, "演示值必须按数据域判定后才显示");
assert.match(panel, /demoValue\(domainName, simulation\?\.baseline_probability_view \|\| run\?\.probability_view\)/,
"基线演示值必须取自服务端基线视图,不能由前端合成");
assert.match(panel, /if \(first && first\.data_domain === "demo"\) await applyScenario\(\[\], first\)/,
"首屏演示值只对演示域取值,production 域不得请求演示表");
assert.match(panel, /未完成真实历史校准/, "未校准时必须说明为何不显示概率");
assert.match(panel, /正式审批由人工决定/, "演示值旁必须有用途边界说明");
assert.match(lib, /domain !== "demo"/, "lib 必须在非演示域返回 null");
// 5. 雷达只对同刻度 3–8 维绘制,其余给完整列表;空维度不补占位轴(AC-44)
assert.match(lib, /RADAR_MIN = 3/);
assert.match(lib, /RADAR_MAX = 8/);
assert.match(lib, /layout: "radar"/);
assert.match(lib, /DimensionLayout = "empty" \| "list" \| "grouped" \| "radar" \| "faceted"/, "呈现词汇必须与后端 review_plan.presentation 一致");
assert.match(lib, /EMPTY_DIMENSIONS = "当前方案尚未配置核查维度"/);
assert.match(panel, /shown\.layout === "radar" && <RadarChart/, "只有判定为雷达时才画雷达");
assert.match(panel, /shown\.layout === "empty" && <div className="ra-empty">/, "空维度用空状态,不补六轴");
assert.match(panel, /文字列表与图等价/, "图必须有等价文字列表");
// 纠偏第一批:面板必须展示本次核查结果与依据,缺证不得改写成通过
assert.match(panel, /核查结果(\{/, "面板必须展示本次核查结果");
assert.match(panel, /未完成项:\{item\.message\}/, "降级记录必须显示到界面");
assert.match(panel, /查看 \$\{item\.title \|\| item\.code\} 的依据/, "每条核查结果必须能打开依据");
assert.match(panel, /本次没有可展示的原文片段/, "没有证据时必须说明按缺证处理,不得留白或编造");
assert.match(panel, /没有发现问题不等于已经通过/, "空结果不得被读成通过");
assert.match(panel, /width=\{\d+\} height=\{\d+\} viewBox=/, "雷达必须给显式尺寸,否则会撑满容器(真渲染验证过的缺陷)");
assert.match(panel, /<text key=\{`label-\$\{item\.id\}`\}/, "雷达必须有轴标签,图不能只有形状");
assert.match(panel, /strokeDasharray="4 4"/, "基线必须用虚线,不能只靠颜色区分");
assert.match(panel, /hasScenario = Array\.from\(scenarioStages\.values\(\)\)\.some\(\(item\) => item\.assumed\)/,
"没有勾选假设时不得画出实线情景轮廓");
assert.match(panel, /阶段 \{stage \?\? "—"\} · \{stageName\(stage\)\}/, "维度必须同时给出阶段序号与名称");
// 6. 乱序与过期响应必须丢弃(AC-31)
assert.match(panel, /ticket !== seq\.current/, "旧请求晚到必须丢弃");
assert.match(panel, /returned !== \[\.\.\.keys\]\.sort\(\)\.join\(","\)/, "结果与当前选择不一致时必须丢弃");
// 7. 失败保留基线并标未完成;计算中不得先给乐观增幅
assert.match(panel, /该情景未完成。基线数值与原有意见保持不变/, "失败必须保留基线");
assert.match(panel, /不会先给一个乐观增幅/, "计算中不得预告结果");
assert.match(panel, /不是各项增量的简单相加/, "沙盘必须说明组合非加和");
// 8. 键盘可达与无障碍
assert.match(panel, /<input type="checkbox"/, "假设动作必须是原生可聚焦控件");
assert.match(panel, /aria-live="polite"/, "变化的数值区必须有 aria-live");
assert.match(panel, /aria-label=\{`查看 \$\{item\.id\} 的依据`\}/, "维度依据入口必须有可读名称");
assert.ok(/<svg[^>]*aria-label=/.test(panel.replace(/\n/g, " ")), "雷达必须有文字等价说明");
// 9. 来源与数据域用文字+图标共同区分,不靠颜色单独传意(AC-42)
assert.match(lib, /text: "历史提炼"/);
assert.match(lib, /text: "人工创建"/);
assert.match(lib, /text: "制度导入"/);
assert.match(lib, /icon: "history"/);
assert.match(panel, /domain\.text/, "数据域必须以文字呈现");
// 10. 阶段是序数,不得换算成概率
assert.match(lib, /雷达面积不换算成百分比|STAGE_LEGEND/, "必须说明阶段与概率无关");
assert.ok(!/probability\s*[:=]\s*[0-9]/.test(panel), "面板不得写入任何概率数值");
console.log("prescreen-ui: PASS");