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:
@@ -7,10 +7,11 @@
|
||||
S1 范围(T00/04 §4/§5):
|
||||
SourceLocator · ImportItem · ImportBatch · FactConflict + 共用约束 + 错误信封
|
||||
|
||||
**不在本文件**(属后续任务包,不得在此提前定义):CaseTimeline、DecisionEvent、
|
||||
ExperienceCard、KnowledgeCard、ReviewPlan、DimensionSpec、ScreeningRun、Judgment、
|
||||
Simulation、Evaluation、TrajectoryStep、ReviewOutcome、ImprovementCandidate、
|
||||
DatasetManifest、ProbabilityView。
|
||||
首版只含 S1 对象;三模块闭环所需类型(CaseTimeline、DecisionEvent、KnowledgeCard、
|
||||
KnowledgeExtraction、ReviewPlan、DimensionSpec、ScreeningRun、Judgment、Simulation、
|
||||
Evaluation、TrajectoryStep、ReviewOutcome、ImprovementCandidate、DatasetManifest、
|
||||
ProbabilityView)随后续任务包在本文件续写——**本文件始终是唯一 owner**,
|
||||
新增类型写在这里,不另立平行契约文件。
|
||||
|
||||
依赖方向:本模块**不导入** `services/`。标准字段白名单(`assistant_materials.FIELDS`)
|
||||
的校验属 service 层,避免 contracts → services 的反向依赖。
|
||||
@@ -122,6 +123,10 @@ class ErrorCode(StrEnum):
|
||||
# E-27:步骤未完成(非预算原因)。纠偏第一批使用。
|
||||
RUN_STEP_FAILED = "RUN_STEP_FAILED"
|
||||
RUN_BUDGET_EXCEEDED = "RUN_BUDGET_EXCEEDED"
|
||||
# E-28:经验提炼不可用(未配置模型或调用失败)。纠偏第二批使用。
|
||||
# 与 E-27 同属运行级:记在提炼记录的 degradations 里,**不是** HTTP 错误——
|
||||
# "HTTP 200 只表示记录可读取,不表示提炼成功"(01 §5 末)。
|
||||
EXTRACTION_UNAVAILABLE = "EXTRACTION_UNAVAILABLE"
|
||||
|
||||
|
||||
# HTTP 语义沿用既有 translate()(backend/api/project_assistant.py:36-46),
|
||||
@@ -158,6 +163,7 @@ RUN_LEVEL_CODES = frozenset({
|
||||
ErrorCode.JUDGMENT_CONTEXT_EXCEEDED,
|
||||
ErrorCode.RUN_STEP_FAILED,
|
||||
ErrorCode.RUN_BUDGET_EXCEEDED,
|
||||
ErrorCode.EXTRACTION_UNAVAILABLE,
|
||||
})
|
||||
|
||||
# 403/422 不自动重试;409 需客户端改载荷或刷新。见 03 §5。
|
||||
@@ -569,6 +575,48 @@ class KnowledgeCard(Contract):
|
||||
return self
|
||||
|
||||
|
||||
class KnowledgeExtraction(Contract):
|
||||
"""一次历史经验提炼**作业**的记录(BR-48 / AC-50 / E-28)。
|
||||
|
||||
为什么需要单独一条记录,而不是只返回草稿:纠偏规格 §§0 禁止两种假接通——
|
||||
"把固定返回值包装成模型提炼成功"与"模型不可用时照样产出候选"。只返回草稿时,
|
||||
界面无法区分"真的调用了模型"和"模型没跑起来但草稿还在"。把作业本身落成对象后,
|
||||
调用是否发生、失败原因、候选是否存在,在同一条记录里可核对。
|
||||
|
||||
`failed` 时 **candidate_id 必须为空**:这是 AC-50「模型不可用 ⇒ 不产生候选」的
|
||||
机械表达,不是文档约定。
|
||||
"""
|
||||
|
||||
id: Identifier
|
||||
project_id: Identifier
|
||||
data_domain: DataDomain
|
||||
revision: int = Field(ge=1)
|
||||
status: Literal["completed", "failed"]
|
||||
timeline_id: Identifier
|
||||
source_event_ids: list[Identifier] = Field(default_factory=list, max_length=EVIDENCE_MAX)
|
||||
requirement: str = Field(default="", max_length=DESC_MAX)
|
||||
prompt_version: str = Field(min_length=1, max_length=40)
|
||||
candidate_id: Identifier | None = None
|
||||
model: str | None = Field(default=None, max_length=ID_MAX)
|
||||
engine: str | None = Field(default=None, max_length=ID_MAX)
|
||||
degradations: list[dict] = Field(default_factory=list, max_length=EVIDENCE_MAX)
|
||||
created_at: AwareDatetime
|
||||
created_by: Identifier
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_extraction(self) -> KnowledgeExtraction:
|
||||
if self.status == "failed":
|
||||
if not self.degradations:
|
||||
raise ValueError("提炼失败必须记录降级原因(E-28),不得静默结束")
|
||||
if self.candidate_id is not None:
|
||||
raise ValueError("提炼失败不得产生候选草稿")
|
||||
elif self.candidate_id is None:
|
||||
raise ValueError("提炼完成必须引用产出的候选草稿")
|
||||
if not self.requirement.strip() and not self.source_event_ids:
|
||||
raise ValueError("提炼须至少给出要求或来源事件之一")
|
||||
return self
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 评审方案维度(T06)
|
||||
class ScaleLevel(Contract):
|
||||
"""刻度的一级。刻度由版本化标准驱动,**不由模型任意打分**(14 §5)。"""
|
||||
|
||||
Reference in New Issue
Block a user