覆盖 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>
64 lines
3.7 KiB
JavaScript
64 lines
3.7 KiB
JavaScript
/**
|
||
* S1d 界面源码守卫。
|
||
*
|
||
* 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2 已明确
|
||
* 现有 UI 脚本的性质)。它锁住的是可机械核对的约束:不新建路由、复用既有样式、
|
||
* 上传必须带批次标识、冲突裁定必须留痕、不得展示任何审批概率。
|
||
*/
|
||
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/ImportBatchPanel.tsx");
|
||
const workbench = read("src/app/xingbang-workbench/page.tsx");
|
||
const lib = read("src/lib/prescreen.ts");
|
||
|
||
// 1. 挂载在既有工作台内
|
||
assert.match(workbench, /import ImportBatchPanel from "@\/components\/xingbang\/ImportBatchPanel"/, "工作台应导入导入面板");
|
||
assert.match(workbench, /<ImportBatchPanel /, "工作台应渲染导入面板");
|
||
// AC-55:禁止的是**重复页面**(把面板搬到独立路由、把已合并页重新实现),不是新增路由本身。
|
||
for (const banned of ["prescreen", "import-batch", "material-import"]) {
|
||
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. 上传必须带批次标识(否则服务端无法回写条目状态)
|
||
assert.match(panel, /form\.append\("batch_id", batch\.id\)/, "上传必须提交 batch_id");
|
||
assert.match(panel, /form\.append\("item_id", itemId\)/, "上传必须提交 item_id");
|
||
|
||
// 4. 冲突裁定必须显式选择 + 填写说明,且未填说明时按钮禁用
|
||
assert.match(panel, /resolution_note: note/, "裁定必须提交 resolution_note");
|
||
assert.match(panel, /selected_evidence_ids: \[evidenceId\]/, "裁定必须显式选择来源");
|
||
assert.match(panel, /disabled=\{busy \|\| !note\.trim\(\)\}/, "未填裁定说明时不得可提交");
|
||
|
||
// 5. 必须向用户说明"未确认不入事实"与"冲突不覆盖"(BR-01 / BR-23 的用户可见面)
|
||
assert.match(panel, /未上传或未确认的文件不会进入项目事实/, "必须说明未确认不入事实");
|
||
assert.match(panel, /不会按上传先后覆盖/, "必须说明冲突不按上传先后覆盖");
|
||
assert.match(panel, /不会自动挑一个/, "必须说明系统不会自动挑选冲突来源");
|
||
|
||
// 6. 不得展示任何审批概率(BR-12 / AC-12:未校准即无概率)
|
||
assert.ok(!/通过率|probability|概率环/.test(panel), "S1 面板不得展示任何审批概率");
|
||
assert.ok(!/通过率|probability/.test(lib), "S1 客户端不得引入概率字段");
|
||
|
||
// 7. 不得在前端编造判定结果:值必须来自服务端字段
|
||
assert.ok(!/value:\s*["']?\d/.test(panel), "面板不得硬编码业务数值");
|
||
|
||
// 8. 客户端必须同时吃下两种错误形状,且不丢既有 detail 的信息量
|
||
assert.match(lib, /data\?\.error/, "客户端必须能解包统一错误信封");
|
||
assert.match(lib, /JSON\.stringify\(detail\)/, "既有 detail 数组形态不得丢信息");
|
||
|
||
// 9. 空状态不得绘制虚构行(UI Spec §3)
|
||
assert.match(panel, /ra-empty/, "空批次应使用既有空状态样式");
|
||
assert.match(panel, /尚无导入批次/, "空批次应给出明确的空状态文案");
|
||
|
||
console.log("prescreen-panel-ui: PASS");
|