feat(v2): 纠偏第四批——会议结果录入与改进发布接真实知识发布入口

- 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>
This commit is contained in:
2026-10-01 02:21:11 +08:00
co-authored by Claude Code
parent 3e15bda8cd
commit f775c6cbe4
13 changed files with 1269 additions and 91 deletions
+14 -3
View File
@@ -111,13 +111,22 @@ const trajectory = read("src/components/xingbang/TrajectoryPanel.tsx");
assert.match(trajectory, /failed/, "轨迹面板必须能显示失败步骤(失败保留)");
assert.match(trajectory, /不填 0|未记录/, "成本缺失时必须留空,不得填 0");
assert.ok(!/cost\s*[:=]\s*0\b/.test(trajectory), "不得把缺失成本写成 0");
assert.match(trajectory, /只读/, "轨迹面板必须声明只读");
// 纠偏第四批把"会议结果录入"并入了本面板,因此这里不能再只断言"整页只读"——
// 要分别锁死两件事:**回放只读**(不改运行、不改历史结果)与**结果只可追加**(不覆盖历史)。
assert.match(trajectory, /回放是只读的|只读回放/, "回放必须声明只读(第四批加的是追加,不是改写)");
assert.match(trajectory, /supersedes_id/, "会议结果更正必须指向旧记录(supersedes_id)");
assert.match(trajectory, /不覆盖|只\*\*追加\*\*/, "界面必须声明更正只追加、不覆盖历史");
// 判据落在"是否渲染推理字段"上,而不是禁用词本身——否则解释规则的注释会把守卫自己绊倒。
assert.ok(!/\.\s*(reasoning|reasoning_content|thoughts?|chain_of_thought)\b/i.test(trajectory),
"不得渲染模型隐藏推理字段");
// 服务端已声明轨迹不含内部推理,界面必须把它显示出来(而不是自己编一句)
assert.match(trajectory, /trajectory\.note|note\}/, "应展示服务端对轨迹范围的说明");
assert.match(trajectory, /review-outcomes|会议结果/, "轨迹面板应与会议结果对照");
// AC-56:既读又写——只读列表不再是本面板的全部职责
assert.match(trajectory, /review-outcomes/, "轨迹面板应消费会议结果端点");
assert.match(trajectory, /"POST",\s*`\$\{PRESCREEN\}\$\{path\}\/review-outcomes`/,
"会议结果必须真的能录入(POST 到 review-outcomes)");
assert.match(trajectory, /raw_code/, "录入必须围绕原始结果码,不得自行推断通过或否决");
assert.match(trajectory, /不计入分母/, "未匹配的结果必须标明不计入分母(单列)");
// 预审模块必须真的挂载预审面板(否则"AI 预审入口"是空壳)
assert.match(read("src/app/prescreen-workbench/page.tsx"), /<PrescreenPanel /, "AI 预审模块应挂载预审面板");
@@ -125,7 +134,9 @@ assert.match(read("src/app/prescreen-workbench/page.tsx"), /<PrescreenPanel /, "
// 界面调用的端点必须在后端真的声明过——否则"入口可点"只是连到 404。
const prescreenApi = read("backend/api/prescreen.py");
for (const route of ['"/projects/{project_id}/runs/{run_id}/trajectory"',
'"/projects/{project_id}/review-outcomes"']) {
'"/projects/{project_id}/review-outcomes"',
'"/projects/{project_id}/improvements"',
'"/improvements/{candidate_id}/transitions"']) {
assert.ok(prescreenApi.includes(route), `后端应声明路由 ${route}`);
}
assert.ok(read("backend/api/project_assistant.py").includes('"/projects/{project_id}/runs"'),