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:
@@ -220,6 +220,29 @@ def freeze(db, plan_id, legacy_catalog=None):
|
||||
return result
|
||||
|
||||
|
||||
def version_key(version: str) -> tuple:
|
||||
"""点分版本的比较键;非数字段按 0 处理,不做语义猜测。"""
|
||||
return tuple(int(part) if part.isdigit() else 0 for part in str(version).split("."))
|
||||
|
||||
|
||||
def current_draft(db, component_ref: str) -> dict:
|
||||
"""按草稿 id(`组件id@版本`)或组件 id 找当前草稿。
|
||||
|
||||
组件 id 在"修订 → 升版本"后仍指向同一对象,业务侧更适合用它;草稿 id 用于对某个
|
||||
具体版本操作。找不到即 KeyError(调用方转 404)。
|
||||
"""
|
||||
if "@" in str(component_ref):
|
||||
return get(db, "component_draft", component_ref)
|
||||
matches = [row for row in listing(db, "component_draft")
|
||||
if row.get("component_id") == component_ref]
|
||||
if not matches:
|
||||
raise KeyError(f"组件草稿不存在:{component_ref}")
|
||||
# 先比**版本**再比 revision:升版本后旧版本草稿仍然存在(历史不可覆盖),
|
||||
# 而它的 revision 可能比新草稿大,只按 revision 取会退回旧版本。
|
||||
return max(matches, key=lambda row: (version_key(row.get("version", "")),
|
||||
row.get("revision", 0)))
|
||||
|
||||
|
||||
def draft(db, body, actor):
|
||||
body = normalize({**{k: v for k, v in body.items() if k not in ("evaluation", "evaluation_id", "approved_by", "published_at")}, "status": "draft", "edited_by": actor}, actor)
|
||||
validate(body)
|
||||
|
||||
Reference in New Issue
Block a user