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:
+66
-12
@@ -629,26 +629,80 @@ def propose_improvement(project_id: str, payload: ImprovementInput, request: Req
|
||||
return run(request, action)
|
||||
|
||||
|
||||
def _revision_draft(db, ref: dict) -> dict:
|
||||
"""把候选声明的 `proposed_revision_ref` 解析成**那一份**草稿。
|
||||
|
||||
坚持按 `组件id@版本` 精确定位:只按组件 id 取"当前草稿"会解析到另一个修订
|
||||
(`revise()` 升版本后旧草稿仍在),于是出现"审核的是 A、发布的是 B"。
|
||||
解析不到就报未完成,**不退回猜一个草稿**。
|
||||
"""
|
||||
ref = ref or {}
|
||||
component_id = str(ref.get("id") or "").strip()
|
||||
version = str(ref.get("version") or "").strip()
|
||||
if not component_id or not version:
|
||||
raise improvements.NotValidated("候选必须声明要发布的组件版本(proposed_revision_ref)")
|
||||
draft_id = component_id if "@" in component_id else f"{component_id}@{version}"
|
||||
try:
|
||||
return knowledge_cards.current_draft(db, draft_id)
|
||||
except KeyError as exc:
|
||||
raise improvements.NotValidated(
|
||||
f"候选指向的草稿 {draft_id} 不存在;请先形成草稿修订再发布") from exc
|
||||
|
||||
|
||||
def _knowledge_publisher(db, actor: str, preview: bool):
|
||||
"""把改进候选的发布接到**真实知识发布入口**(BR-51 / AC-57)。
|
||||
|
||||
走 `knowledge_cards.publish` → `component_registry.release`,因此"自批拒绝、
|
||||
隔离评测门禁、依赖可解析"三道既有保证原样生效,发布成功才覆盖 `release` 指针、
|
||||
后续运行由 `freeze() → resolve()` 取到新版本。候选只是**指向**这次真实发布。
|
||||
|
||||
`db` 必须是调用方同一个连接:另开连接会让"写组件版本"与"写候选状态"分裂成
|
||||
两个事务,出现"版本已发布、候选没标记"的裂口。
|
||||
publisher 抛异常时 `improvements.transition` 不写候选状态(发布失败保留旧版)。
|
||||
"""
|
||||
def publish(candidate: dict) -> dict:
|
||||
if candidate.get("target") != "knowledge":
|
||||
raise improvements.NotValidated(
|
||||
f"target={candidate.get('target')} 尚无真实发布入口;不得用改状态冒充发布")
|
||||
draft = _revision_draft(db, candidate.get("proposed_revision_ref"))
|
||||
try:
|
||||
published = knowledge_cards.publish(db, draft["id"], actor, preview)
|
||||
except store.Conflict as exc:
|
||||
# 评测/依赖门禁未过:统一按"未完成门禁"回报,界面只需处理一个码(E-24)
|
||||
raise improvements.NotValidated(str(exc)) from exc
|
||||
return {"id": published["component_id"], "version": published["version"]}
|
||||
|
||||
return publish
|
||||
|
||||
|
||||
@router.post("/improvements/{candidate_id}/transitions")
|
||||
def transition_improvement(candidate_id: str, payload: TransitionInput, request: Request):
|
||||
"""候选状态机。泄漏按 E-11 拒绝,未通过对照/审核按 E-24 拒绝。"""
|
||||
"""候选状态机。泄漏按 E-11 拒绝,未通过对照/审核按 E-24 拒绝。
|
||||
|
||||
`release` **不接受调用方自报的版本号**:版本由服务端调用真实知识发布入口产生
|
||||
(BR-51 / AC-57)。只改 `released` 状态或返回一个版本号都不算发布,因此带了
|
||||
`released_ref` 的请求一律 422 拒绝,而不是静默忽略它。
|
||||
"""
|
||||
def action():
|
||||
if payload.released_ref is not None:
|
||||
raise ValueError("发布版本由服务端实际发布产生,不接受客户端提交的 released_ref")
|
||||
with _db() as db:
|
||||
probe = store.get(db, improvements.KIND, candidate_id)
|
||||
blocked = scope(request, probe["project_id"])
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
try:
|
||||
with _db() as db:
|
||||
blocked = scope(request, probe["project_id"])
|
||||
if blocked is not None:
|
||||
return blocked
|
||||
try:
|
||||
return improvements.transition(
|
||||
db, candidate_id, payload.action, _actor(request),
|
||||
payload.expected_revision, evaluation=payload.evaluation,
|
||||
contrast=payload.contrast, released_ref=payload.released_ref,
|
||||
reason=payload.reason)
|
||||
except improvements.LeakageDetected as exc:
|
||||
return _envelope(request, ErrorCode.EVALUATION_CONTAMINATED, str(exc))
|
||||
except improvements.NotValidated as exc:
|
||||
return _envelope(request, ErrorCode.IMPROVEMENT_NOT_VALIDATED, str(exc))
|
||||
contrast=payload.contrast, reason=payload.reason,
|
||||
publisher=_knowledge_publisher(
|
||||
db, _actor(request),
|
||||
request.state.identity.get("preview", False)))
|
||||
except improvements.LeakageDetected as exc:
|
||||
return _envelope(request, ErrorCode.EVALUATION_CONTAMINATED, str(exc))
|
||||
except improvements.NotValidated as exc:
|
||||
return _envelope(request, ErrorCode.IMPROVEMENT_NOT_VALIDATED, str(exc))
|
||||
|
||||
return run(request, action)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user