feat(v2): S1 存储层——材料导入批次与事实冲突检测
T02 第二个子卡。复用 assistant_objects(新 kind),**未新建表**;旧库结构不变。 新增服务: - services/material_import.py 批次生命周期:create/load/listing/mark_item/ retry/cancel;状态由逐项状态派生(ready/partial/failed/parsing) - services/fact_conflict.py 冲突检测/阻断/裁定;FactConflictUnresolved(E-17) 解决的**真实缺口**(BR-23 / AC-24):既有 apply_candidate 对 values/facts 是 后写覆盖,sources 虽累积多来源但无人检测分歧、无人阻断。本层在**新路径**前置 检测:冲突时证据仍入 sources(BR-22 留痕)但值不采用(BR-23),已确认值不被覆盖。 增量改动(均为可加性,未改既有语义与调用方行为): - assistant_store.py:新增 idempotent_lookup / idempotent_remember 共享 helper。 既有 enqueue 与 create_report 的内联实现**未改写**。用 INSERT OR REPLACE 而非 裸 INSERT:重试路径上裸 INSERT 会抛 sqlite3.IntegrityError(非 Conflict, translate() 无法映射为 409)。 - assistant_materials.apply_candidate 新增 adopt=True 形参。adopt=False 只记证据 不改值;既有调用方不传参,行为完全不变(有回归测试)。 设计决定: - T00/04 曾提 3 个 kind;实现时发现契约中 ImportItem 无 revision 且内嵌于批次, items 是值对象,故收敛为 2 个 kind(import_batch / fact_conflict) - E-17 有两个面:确认动作被拒 = 422;运行级结果 = HTTP 200 且冲突存于 degradations。契约映射与注释均已写明,并有测试锁定 测试:新增 test_prescreen_storage.py(33 项)。全量 196 项 OK(124 → 196)。 scripts/verify.py 9/9 PASS(exit 0)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -206,7 +206,15 @@ def demo_assets(project):
|
||||
return material(project["id"], project["id"] + "设备清单.csv", stream.getvalue().encode("utf-8-sig"), "demo-fixture")
|
||||
|
||||
|
||||
def apply_candidate(project, candidate, actor, reason):
|
||||
def apply_candidate(project, candidate, actor, reason, adopt=True):
|
||||
"""记录一个材料候选为证据。
|
||||
|
||||
adopt=True(默认,既有行为不变):证据入 sources,并把值写进
|
||||
values / selected_sources / facts。
|
||||
adopt=False(新增,供 S1 事实确认使用):**只**把证据记入 sources,不改
|
||||
values / selected_sources / facts。用于"候选与已确认值冲突、待人工裁定"
|
||||
的情形——证据已按 BR-22 留痕,但值不得被覆盖(BR-23)。
|
||||
"""
|
||||
value = scalar(candidate["field"], candidate.get("value"))
|
||||
evidence = dict(candidate, id=uid("ev"), project_id=project["id"], value=value, actor_id=actor, reason=reason, accepted_at=now())
|
||||
money_fields = NUMERIC - {"lease.duplicate_count", "credit.overdue_count", "facility.term"}
|
||||
@@ -218,6 +226,8 @@ def apply_candidate(project, candidate, actor, reason):
|
||||
value = str(number(value) * SCALE[candidate["unit"]])
|
||||
evidence["value"] = value
|
||||
project.setdefault("sources", {}).setdefault(candidate["field"], []).append(evidence)
|
||||
if not adopt:
|
||||
return evidence
|
||||
project.setdefault("values", {})[candidate["field"]] = value
|
||||
project.setdefault("selected_sources", {})[candidate["field"]] = evidence["id"]
|
||||
old = next((k for k, v in FIELDS.items() if v == candidate["field"]), candidate["field"])
|
||||
|
||||
Reference in New Issue
Block a user