- api/prescreen.py:transition_improvement 注入 _knowledge_publisher, release 委托 knowledge_cards.publish → component_registry.release; 按 proposed_revision_ref 以「组件id@版本」精确解析草稿(不取"最新草稿", 否则会出现"审核的是 A、发布的是 B");客户端自报 released_ref 一律 422; 非 knowledge 目标 409 诚实拒绝;发布与候选状态同一事务,失败保留旧版。 - TrajectoryPanel:新增会议结果录入(追加式更正走 supersedes_id; 未匹配单列且不计入分母)。 - ImprovementPanel(新增):偏差→候选→两道评测门→独立审核→发布; 请求体不含版本号,无自动上线,不自批。 - 界面并入「轨迹复盘」,不新增第四个导航入口(AC-53)。 - 守卫:新增 scripts/test_improvement_panel.cjs(verify.py 13→14 项); test_module_shell_ui.cjs 的「只读」断言改为分别锁死「回放只读」与「结果只可追加」。 - 测试:新增 backend/test_improvement_release.py(AC-57 端到端 + 三条反例); test_improvements.py 补「自报版本号被拒」。 - 文档:17 第四批交付记录、15 §7 交接、03 §9.4 发布事务边界。 验证:scripts/verify.py 14/14 PASS(backend-unit 572 项),证据 test-evidence/20261001-013517-3e15bda/;scripts/xingbang_batch4_smoke.py 对真实服务器实跑通过(发布指针实测移到新版本)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
100 lines
6.5 KiB
JavaScript
100 lines
6.5 KiB
JavaScript
/**
|
||
* 改进对照与发布 界面源码守卫(纠偏第四批 · BR-51 / AC-56 / AC-57)。
|
||
*
|
||
* 这是**源码断言,不是浏览器端到端测试**(docs/v2/06-validation.md §2)。
|
||
* 它专防审计确认过的偏差模式:「函数在、功能不在」——面板看着有按钮,
|
||
* 实际没接真实端点,或者把版本号从界面自报上去冒充发布。
|
||
*/
|
||
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 exists = (file) => fs.existsSync(path.join(root, file));
|
||
|
||
const panel = read("src/components/xingbang/ImprovementPanel.tsx");
|
||
const page = read("src/app/trajectory-center/page.tsx");
|
||
const trajectory = read("src/components/xingbang/TrajectoryPanel.tsx");
|
||
const prescreenApi = read("backend/api/prescreen.py");
|
||
|
||
// ---------------------------------------------------------------- 1. 入口与挂载
|
||
assert.ok(exists("src/components/xingbang/ImprovementPanel.tsx"), "应有改进对照面板组件");
|
||
assert.match(page, /import ImprovementPanel from "@\/components\/xingbang\/ImprovementPanel"/,
|
||
"轨迹复盘路由应复用改进面板组件,而不是在路由里再写一份");
|
||
assert.match(page, /<ImprovementPanel/, "轨迹复盘路由必须真的挂载改进面板");
|
||
assert.match(page, /<TrajectoryPanel/, "轨迹复盘路由应仍挂载轨迹面板");
|
||
|
||
// ---------------------------------------------------------------- 2. 不新增第四个入口(AC-53/BR-50)
|
||
const nav = read("src/components/xingbang/AgentNav.tsx");
|
||
assert.ok(!nav.includes("/improvement"), "改进模块必须并入轨迹复盘,不得新增导航入口");
|
||
for (const banned of ["improvement", "improvements"]) {
|
||
assert.ok(!exists(`src/app/${banned}`), `不得新建第四个模块路由 src/app/${banned}`);
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 3. 端点真的接上了
|
||
for (const call of [
|
||
/`\$\{PRESCREEN\}\$\{path\}\/improvements`/, // 项目级列表与提出候选
|
||
/`\$\{PRESCREEN\}\/improvements\/\$\{encodeURIComponent\(item\.id\)\}\/transitions`/,
|
||
/knowledge-candidates/, // 选草稿 + 草稿隔离评测
|
||
/review-outcomes/, // 偏差来源:会议结果
|
||
]) {
|
||
assert.match(panel, call, `改进面板必须真的调用既有端点:${call}`);
|
||
}
|
||
assert.match(panel, /action: "evaluate"|action,\s*\.\.\.extra/, "必须能提交对照评测");
|
||
assert.match(panel, /action: "release"|transition\(item, "release"\)/, "必须提供发布动作");
|
||
|
||
// ---------------------------------------------------------------- 4. 版本号不得由界面提交(BR-51 的核心)
|
||
assert.ok(!/released_ref\s*:/.test(panel),
|
||
"界面不得提交 released_ref:发布版本必须由服务端从真实发布结果产生");
|
||
assert.ok(!/released_ref\s*:/.test(trajectory), "会议结果录入不得提交 released_ref");
|
||
// 展示时只能读服务端返回的引用
|
||
assert.match(panel, /item\.released_ref/, "发布结果应展示服务端返回的版本引用");
|
||
|
||
// ---------------------------------------------------------------- 5. 不提供自动上线
|
||
const buttonTexts = [...panel.matchAll(/<button[^>]*>([\s\S]*?)<\/button>/g)].map((m) => m[1]);
|
||
assert.ok(!buttonTexts.some((text) => /自动上线|一键|自动发布/.test(text)),
|
||
"不得提供自动上线/一键发布按钮");
|
||
assert.match(panel, /没有"自动上线"|不提供自动上线/, "界面必须写明不提供自动上线");
|
||
assert.ok(!/enqueue|\/runs`/.test(panel), "发布后不得自动发起运行");
|
||
|
||
// ---------------------------------------------------------------- 6. 不自批
|
||
assert.match(panel, /proposed_by !== identity\?\.id/, "提出者不得看到自己的通过审核按钮");
|
||
assert.match(panel, /独立审核|不能由提出者|审核需要独立/, "界面必须说明审核需要独立的人");
|
||
assert.match(panel, /release_blockers|blockersOf|未完成门禁/, "必须把未完成门禁展示出来(E-24)");
|
||
|
||
// ---------------------------------------------------------------- 7. 两道评测门必须分开
|
||
assert.match(panel, /隔离评测/, "必须有草稿的隔离评测入口(发布门禁之一)");
|
||
assert.match(panel, /旧\/新对照/, "必须有候选的旧/新对照入口(另一道门)");
|
||
// 草稿评测必须精确到 `组件id@版本`:只传组件 id 会评测到"当前最新草稿",
|
||
// 于是出现"评测的是 A、发布的是 B"。
|
||
assert.match(panel, /proposed_revision_ref\.id \+ "@" \+ item\.proposed_revision_ref\.version/,
|
||
"草稿隔离评测必须精确定位到候选声明的版本");
|
||
assert.match(panel, /泄漏/, "泄漏样本必须按失败显示,不得渲染成评测完成");
|
||
|
||
// ---------------------------------------------------------------- 8. AC-56:会议结果录入
|
||
assert.match(trajectory, /review-outcomes/, "轨迹面板应消费会议结果端点");
|
||
assert.match(trajectory, /"POST"/, "轨迹面板必须能追加会议结果");
|
||
assert.match(trajectory, /raw_code/, "录入必须围绕原始结果码,不得自行推断通过或否决");
|
||
assert.match(trajectory, /supersedes_id/, "更正只能追加并指向旧记录");
|
||
assert.match(trajectory, /不计入分母/, "未匹配的结果必须单列且不计入分母");
|
||
|
||
// ---------------------------------------------------------------- 9. 后端真的接上了(交叉断言)
|
||
assert.match(prescreenApi, /publisher=_knowledge_publisher\(/, "端点必须注入真实 publisher");
|
||
assert.match(prescreenApi, /knowledge_cards\.publish\(db, draft\["id"\], actor, preview\)/,
|
||
"publisher 必须调用真实知识发布入口");
|
||
assert.ok(!/released_ref=payload\.released_ref/.test(prescreenApi),
|
||
"端点不得再把客户端提交的 released_ref 透传给服务层");
|
||
assert.match(prescreenApi, /不接受客户端提交的 released_ref/, "必须显式拒绝客户端自报版本号");
|
||
assert.match(read("backend/services/improvements.py"), /released_ref = publisher\(body\)/,
|
||
"服务层发布必须委托注入的 publisher");
|
||
|
||
// ---------------------------------------------------------------- 10. 不新增样式表 / 既有禁令
|
||
assert.ok(!/^import .*\.css";/m.test(panel), "改进面板应复用 workspace.css,不得新增样式表");
|
||
for (const [name, source] of [["improvement", panel], ["trajectory", trajectory]]) {
|
||
assert.ok(!/0\.(42|58|51|64)\b/.test(source), `${name} 不得硬编码演示概率值`);
|
||
assert.ok(!/"(00|10|01|11)"\s*:/.test(source), `${name} 不得复制演示情景表`);
|
||
}
|
||
|
||
console.log("improvement-panel-ui: PASS");
|