diff --git a/.claude/skills/parse-book/scripts/parse_ingest.py b/.claude/skills/parse-book/scripts/parse_ingest.py index cf348ba..3cdb01a 100644 --- a/.claude/skills/parse-book/scripts/parse_ingest.py +++ b/.claude/skills/parse-book/scripts/parse_ingest.py @@ -171,12 +171,15 @@ def patterns(work_id, chapter_order, file_): ch_id, src = chapter_of(conn, work_id, chapter_order) contracts = {t: field_contract(conn, t) for t in PATTERN_TYPES} book = conn.execute("SELECT title FROM muse_content_work WHERE id=%s", (work_id,)).fetchone()[0] - # 幂等:重跑本章 = 软删本章旧 draft - conn.execute( - """UPDATE muse_knowledge_draft SET deleted=TRUE, updater=%s - WHERE tenant_id=%s AND source_type='parse_book' AND source_id=%s - AND draft_payload->>'章序'=%s AND deleted=FALSE""", - (ACTOR, TENANT, work_id, str(chapter_order))) + # 幂等:重跑本章 = 软删本章旧 draft。 + # 但新轮 0 卡时保留旧卡——LLM 判卡有随机性,重跑「0 卡」不应清掉上轮已验证的产出 + #(实测组合病:判重名录+随机性+软删 → 每次重跑都损耗卡) + if cards: + conn.execute( + """UPDATE muse_knowledge_draft SET deleted=TRUE, updater=%s + WHERE tenant_id=%s AND source_type='parse_book' AND source_id=%s + AND draft_payload->>'章序'=%s AND deleted=FALSE""", + (ACTOR, TENANT, work_id, str(chapter_order))) ok, rejected = 0, [] for i, c in enumerate(cards): t = c.get("型") @@ -189,10 +192,19 @@ def patterns(work_id, chapter_order, file_): if not (src_ref.get("书名") and src_ref.get("回目")): reasons.append("出处不完整(需书名+回目)") fields = c.get("字段") or {} - if t in contracts: + retyped = None + if t in contracts and fields: illegal = set(fields) - contracts[t] if illegal: - reasons.append(f"字段越合同:{sorted(illegal)}") + # 字段指纹改型:LLM 的 type 标注不可靠(实测 M3 系统性全标 craft), + # 五型字段 key 互不重叠——字段集合全命中唯一他型合同时,以字段为准改型 + fits = [t2 for t2 in PATTERN_TYPES + if t2 != t and fields and set(fields) <= contracts[t2]] + if len(fits) == 1: + retyped = f"{t}→{fits[0]}(字段指纹改型)" + c["型"] = t = fits[0] + else: + reasons.append(f"字段越合同:{sorted(illegal)}") texts = [c.get("名称"), c.get("一句话摘要"), *fields.values()] leak = leak_check([x for x in texts if x], src) if leak: @@ -203,6 +215,8 @@ def patterns(work_id, chapter_order, file_): payload = {"型": t, "名称": c["名称"], "一句话摘要": c.get("一句话摘要", ""), "字段": fields, "出处": src_ref, "目标库": "公共范式库", "章序": chapter_order, "来源": f"拆书@{book}", "状态": "草稿"} + if retyped: + payload["改型"] = retyped # 审计:机械改型可追溯 cid = f"parse-{work_id}-{chapter_order}-{i}-" + hashlib.sha256( json.dumps(payload, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:8] conn.execute( diff --git a/.claude/skills/parse-book/scripts/parse_llm.py b/.claude/skills/parse-book/scripts/parse_llm.py index 8b712d9..8f4f9b4 100644 --- a/.claude/skills/parse-book/scripts/parse_llm.py +++ b/.claude/skills/parse-book/scripts/parse_llm.py @@ -167,10 +167,13 @@ def main(work_id, from_, to, model): JOIN muse_content_chapter c ON c.id=s.chapter_id WHERE s.tenant_id=%s AND s.work_id=%s AND c.order_no<%s AND s.deleted=FALSE""", (TENANT, work_id, ch)).fetchall() for e in ents] + # 判重名录排除本章自己的卡:否则重跑时 M3 对着自己首轮的卡判重不出卡, + # ingest 幂等软删旧卡后插 0 张——卡被自己删了(重跑自噬,实测病) cards = conn.execute( """SELECT draft_payload->>'型', draft_payload->>'名称' FROM muse_knowledge_draft - WHERE tenant_id=%s AND source_type='parse_book' AND deleted=FALSE""", - (TENANT,)).fetchall() + WHERE tenant_id=%s AND source_type='parse_book' AND deleted=FALSE + AND NOT (source_id=%s AND (draft_payload->>'章序')::int=%s)""", + (TENANT, work_id, ch)).fetchall() outline = None if s_st == "done": r = conn.execute(