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:
@@ -88,6 +88,39 @@ def event(db, project_id, action, actor, detail=None, run_id=None):
|
||||
db.execute("INSERT INTO assistant_events(project_id,run_id,body) VALUES (?,?,?)", (project_id, run_id, json.dumps(body, ensure_ascii=False)))
|
||||
|
||||
|
||||
def idempotent_lookup(db, actor, operation, key, request_hash):
|
||||
"""幂等键查询(新增的共享 helper)。
|
||||
|
||||
既有 enqueue / create_report 各自内联同一段逻辑;本函数是新增实现,供新路径
|
||||
使用。**未改写那两处**,避免影响既有运行与报告流程。
|
||||
|
||||
返回既有 object_id;同键异载荷抛 Conflict(对应 E-09 IDEMPOTENCY_CONFLICT)。
|
||||
"""
|
||||
prior = db.execute(
|
||||
"SELECT request_hash,object_id FROM assistant_idempotency WHERE actor=? AND operation=? AND key=?",
|
||||
(actor, operation, key),
|
||||
).fetchone()
|
||||
if not prior:
|
||||
return None
|
||||
if prior[0] != request_hash:
|
||||
raise Conflict("幂等键已用于不同请求")
|
||||
return prior[1]
|
||||
|
||||
|
||||
def idempotent_remember(db, actor, operation, key, request_hash, object_id):
|
||||
"""登记幂等键。
|
||||
|
||||
用 INSERT OR REPLACE 而非既有内联代码的裸 INSERT:重试路径上同一个键可能再次
|
||||
到达,裸 INSERT 会因主键冲突抛 sqlite3.IntegrityError,那不是 Conflict,无法被
|
||||
translate() 映射为 409。同键同载荷在 lookup 阶段已提前返回,故此处的覆盖分支
|
||||
不可达,语义不变。
|
||||
"""
|
||||
db.execute(
|
||||
"INSERT OR REPLACE INTO assistant_idempotency VALUES (?,?,?,?,?)",
|
||||
(actor, operation, key, request_hash, object_id),
|
||||
)
|
||||
|
||||
|
||||
def time_event(db, project_id, phase_code, phase_name, role, category, actor, source_type, source_id,
|
||||
status="running", started_at=None, ended_at=None, detail=None, event_id=None):
|
||||
"""Create an immutable phase interval; callers close it with finish_time_event."""
|
||||
|
||||
Reference in New Issue
Block a user