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>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 纠偏第二批 · 历史经验加工界面源码守卫(BR-48 / BR-49 / BR-50、AC-42/50/51/52)。
|
||||
*
|
||||
* 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2)。
|
||||
* 它锁住四类可机械核对的约束:
|
||||
*
|
||||
* 1. **入口独立**:模块有自己的路由与导航项,不是旧页的第 7 个页签(BR-50)。
|
||||
* 2. **来源徽标真的被用**:`originBadge` 必须被消费,且 .origin-* 样式与图标同时存在。
|
||||
* ← 这条是补既有漏洞:此前该函数零调用方、无任何 CSS,而守卫只 grep 库文件字符串,
|
||||
* 于是"测试通过但功能不存在"。
|
||||
* 3. **失败如实呈现**:E-28 未完成原因必须渲染,不得只显示成功态。
|
||||
* 4. **不诱导违规**:同一身份既编辑又发布时不得出现发布按钮(自批由后端强制,界面不应提供)。
|
||||
*/
|
||||
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 page = read("src/app/experience-center/page.tsx");
|
||||
const panel = read("src/components/xingbang/ExperienceCenter.tsx");
|
||||
const nav = read("src/components/xingbang/AgentNav.tsx");
|
||||
const lib = read("src/lib/prescreen-run.ts");
|
||||
const css = read("src/app/data-access/workspace.css");
|
||||
|
||||
// 1. 独立入口(BR-50:三个模块各有独立入口,不得只在旧页下叠加面板)
|
||||
assert.ok(fs.existsSync(path.join(root, "src/app/experience-center/page.tsx")), "历史经验加工应有独立路由");
|
||||
assert.match(page, /<ExperienceCenter /, "路由页应渲染加工中心组件");
|
||||
assert.match(nav, /\/experience-center/, "导航应提供历史经验加工入口");
|
||||
assert.match(nav, /active==="experience"/, "导航应能标记当前模块");
|
||||
// 复用既有 shell,不另立视觉;也不得把已合并旧页恢复成独立页面(AC-55 的"禁止重复页面")
|
||||
assert.match(page, /AgentNav/, "页面应复用既有导航组件");
|
||||
assert.match(page, /\.\.\/data-access\/workspace\.css/, "页面应复用既有工作区样式");
|
||||
for (const merged of ["trajectory", "review-workbench", "report-workbench", "collateral-workbench"]) {
|
||||
assert.match(read(`src/app/${merged}/page.tsx`), /redirect\(/,
|
||||
`已合并页 src/app/${merged} 必须保持重定向`);
|
||||
}
|
||||
assert.ok(!/^import .*\.css";/m.test(panel), "加工中心不得新增自己的样式表");
|
||||
|
||||
// 2. 来源徽标:文字 + 图标 + 样式三者齐全,去色仍可区分(BR-48 / AC-42)
|
||||
assert.match(lib, /export function originBadge/, "来源徽标解析函数应存在");
|
||||
assert.match(panel, /import \{[^}]*originBadge[^}]*\} from "@\/lib\/prescreen-run"/,
|
||||
"加工中心必须真的消费 originBadge(此前它零调用方,属于'函数在、功能不在')");
|
||||
assert.match(panel, /originBadge\(/, "必须调用 originBadge 而不是自己写一份来源映射");
|
||||
assert.match(panel, /ORIGIN_ICONS/, "来源徽标必须配图标,不能只靠颜色");
|
||||
for (const tone of ["origin-history", "origin-manual", "origin-policy", "origin-unknown"]) {
|
||||
assert.ok(css.includes(`.${tone}{`), `workspace.css 必须定义 .${tone} 样式`);
|
||||
}
|
||||
// 去色仍可区分:四种来源用不同的左边框样式,而不只是不同颜色
|
||||
const borderStyles = ["dashed", "solid", "double", "dotted"].filter((style) => css.includes(`border-left-style:${style}`));
|
||||
assert.equal(borderStyles.length, 4, "四种来源应使用不同的左边框样式,保证去色后仍可区分");
|
||||
|
||||
// 3. 失败如实呈现:E-28 未完成原因必须渲染(AC-50 后半句)
|
||||
assert.match(panel, /degradations/, "提炼记录必须渲染降级/未完成原因");
|
||||
assert.match(panel, /不产生任何候选草稿|不会用一个固定返回值冒充提炼成功/, "界面须说明模型不可用时不产生候选");
|
||||
assert.match(panel, /item\.code/, "未完成原因必须带错误码,便于定位");
|
||||
|
||||
// 4. 不自批:同一身份不得看到自己的发布按钮(BR-42 / AC-43)
|
||||
assert.match(panel, /mayPublish/, "发布按钮必须经过「不自批」判定");
|
||||
assert.match(panel, /card\.edited_by !== identity\?\.id|edited_by !== identity/, "不自批判据必须比较编辑人与当前身份");
|
||||
assert.match(panel, /发布需要独立审核人/, "被隐藏发布按钮时应说明原因,而不是静默消失");
|
||||
|
||||
// 5. 系统沙盘/预审的既有禁令继续有效:这里也不得出现任何概率数值或演示情景表
|
||||
for (const [name, source] of [["panel", panel], ["page", page]]) {
|
||||
assert.ok(!/0\.(42|58|51|64)\b/.test(source), `${name} 不得硬编码演示概率值`);
|
||||
assert.ok(!/"(00|10|01|11)"\s*:/.test(source), `${name} 不得复制演示情景表`);
|
||||
assert.ok(!/probability\s*[:=]\s*[0-9]/.test(source), `${name} 不得写入任何概率数值`);
|
||||
}
|
||||
|
||||
// 6. 加工动作必须落在已定义的端点上,不自行拼 SQL 或绕过服务端
|
||||
for (const endpoint of ["case-timeline", "knowledge-extractions", "knowledge-candidates", "review-plans"]) {
|
||||
assert.ok(panel.includes(endpoint), `加工中心应通过 ${endpoint} 端点读写`);
|
||||
}
|
||||
assert.match(panel, /PRESCREEN/, "必须走既有 prescreen 命名空间");
|
||||
|
||||
console.log("experience-center-ui: PASS");
|
||||
@@ -16,11 +16,16 @@ 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. 挂载在既有工作台内,不新建路由(docs/v2/04-ui-spec.md §1:不恢复已合并的旧页面)
|
||||
// 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)), `不得为 S1 新建路由目录 src/app/${banned}`);
|
||||
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. 复用既有样式,不新增样式表
|
||||
|
||||
@@ -17,11 +17,18 @@ 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. 挂载在既有工作台内,不新建路由(04-ui-spec §1:不恢复已合并的旧页面)
|
||||
// 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)), `不得为 T09 新建路由 src/app/${banned}`);
|
||||
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. 复用既有样式,不新增样式表
|
||||
|
||||
@@ -214,6 +214,7 @@ def main() -> int:
|
||||
("ui-readability", [executable("node"), "scripts/test_ui_readability.cjs"], ROOT),
|
||||
("prescreen-panel-ui", [executable("node"), "scripts/test_prescreen_panel.cjs"], ROOT),
|
||||
("prescreen-ui", [executable("node"), "scripts/test_prescreen_ui.cjs"], ROOT),
|
||||
("experience-center-ui", [executable("node"), "scripts/test_experience_center_ui.cjs"], ROOT),
|
||||
("frontend-build", [npm, "run", "build"], ROOT),
|
||||
("compose-local-config", [docker, "compose", "-f", "docker-compose.yml", "config", "--quiet"], ROOT),
|
||||
("compose-aliyun-config", [docker, "compose", "--env-file", "deploy/aliyun.env.example", "-f", "deploy/docker-compose.aliyun.yml", "config", "--quiet"], ROOT),
|
||||
|
||||
Reference in New Issue
Block a user