From 0892bcad7c50d3279c3d69110372966b43acf5a0 Mon Sep 17 00:00:00 2001 From: zizi Date: Wed, 15 Jul 2026 21:49:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=86=E6=9E=B6:=20=E5=8D=87=E6=A0=BC?= =?UTF-8?q?=E7=AE=A1=E7=BA=BF=207=20=E5=A4=84=E5=AE=88=E5=8D=AB=E8=90=BD?= =?UTF-8?q?=E5=9C=B0(fable5=20=E6=8A=BD=E6=A3=80=205=20=E9=AB=98=E5=8D=B1?= =?UTF-8?q?=E6=A0=B9=E6=B2=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 结构垃圾拦截:变更包字符串化文本拒收留审计(H2 窗75事故,追加+覆写双路) - 摘要特判写卡顶层+纳入更新流程(M4 影子摘要/600章旧摘要) - 追加条目按窗号稳定排序归位(M2 补跑窗时间线倒流) - 立卡初始别名同步入 alias 表(L3 覆盖率26%致判重空转) - 同型名称子串防漏并:转观察材料留复核标记(H5 果子/开心果子) - 别名拒单字(M5) - prompt 纪律:禁相对时间指代/只写本实体信息/摘要跟进(H1 M3) --- .../parse-book/scripts/parse_upgrade.py | 92 ++++++++++++++++++- 1 file changed, 87 insertions(+), 5 deletions(-) diff --git a/.claude/skills/parse-book/scripts/parse_upgrade.py b/.claude/skills/parse-book/scripts/parse_upgrade.py index 5cf30fe..764f1c0 100644 --- a/.claude/skills/parse-book/scripts/parse_upgrade.py +++ b/.claude/skills/parse-book/scripts/parse_upgrade.py @@ -22,6 +22,7 @@ """ import json import pathlib +import re import sys import click @@ -194,8 +195,9 @@ def update_prompt(contracts, title, a, b, text, cards_with_obs): 下列每张卡给出「当前卡全文」与「本窗观察点」。对照本窗正文,**只输出需要变更的字段**: - 覆写类字段(性格底色/说话方式/当前状态等标量):**必须输出该字段完整的新全量值**——旧值里仍然成立的信息要保留进新值,禁止只写"新增…"式增量(那会把旧信息抹掉); - 追加类字段(成长弧线/演变轨迹等数组):只输出本窗新增条目(不要重抄旧条目,不要自己加 [窗N] 前缀,系统会加); -- 没有变化的字段不要输出;整卡无实质变化则不输出该卡。 -纪律:以正文为证据,不脑补;数值必须正文原样出现,禁止推算;字段 key 必须来自该型合同;每卡别名有新发现可在「别名新增」里给——只收**该实体自己**的新别名(他人对它的称呼算,它对别的实体的称呼不算),且必须是可在正文原样出现的纯名字(禁带括号注释与说明文字)。 +- 没有变化的字段不要输出;整卡无实质变化则不输出该卡; +- 该实体的身份/处境已发生重大变化时,把「一句话摘要」也作为变更字段输出(写当前态全量)。 +纪律:以正文为证据,不脑补;数值必须正文原样出现,禁止推算;字段 key 必须来自该型合同;**禁止「本窗/本段/近期」等相对时间指代**——写绝对状态或带章号(卡会脱离本窗被单独阅读);只写**该实体自己**的信息(他人的任命/心理/变化不得写入本卡);每卡别名有新发现可在「别名新增」里给——只收**该实体自己**的新别名(他人对它的称呼算,它对别的实体的称呼不算),且必须是可在正文原样出现的纯名字(禁带括号注释与说明文字,禁单字)。 【相关型字段合同】 {render_entity_contracts(contracts, sorted({p.get("type") for _, p, _ in cards_with_obs} & set(ENTITY_TYPES)))} @@ -255,15 +257,30 @@ def _strip_prefix(s): def _clean_alias(al): - """别名机械准入(抽检#6):拒收含括号注释/超长的备忘录式别名——预扫精确匹配永不命中=死数据。""" + """别名机械准入(抽检#6):拒收含括号注释/超长的备忘录式别名——预扫精确匹配永不命中=死数据。 + 单字别名一并拒收(复检 M5:'新'字入表后预扫全文命中率爆炸,纯噪声)。""" al = (al or "").strip() - if not al or len(al) > 12: + if not al or len(al) < 2 or len(al) > 12: return None if any(c in al for c in "()()。,,:"): return None return al +def _is_garbage(text): + """结构垃圾检测(抽检 H2 根治):窗75 实测模型把原始变更包字符串化塞进字段值, + 7 张卡被 {'draft_id':...} 类程序结构污染。含结构特征的文本一律拒收留审计。""" + t = str(text) + return ("draft_id" in t or "变更字段" in t or "别名新增" in t + or t.lstrip().startswith(("{'", '{"', "[{"))) + + +def _entry_win(x): + """追加条目的窗号(无 [窗N] 前缀的初卡条目记 0,排最前)。""" + m = re.match(r"^\[窗(\d+)\]", str(x)) + return int(m.group(1)) if m else 0 + + def merge_card(conn, draft_id, win_no, changes, alias_new, valid_keys=None): """按 5.1 三类规则合并变更字段:数组/白名单=追加(剥模型自带前缀+同文去重后带窗号), 标量=覆写留审计(增量式假全量拦截转追加——抽检#4 信息回退病)。 @@ -292,6 +309,27 @@ def merge_card(conn, draft_id, win_no, changes, alias_new, valid_keys=None): (draft_id, win_no, ("越合同:" + str(k))[:100], json.dumps(v, ensure_ascii=False)[:2000], TENANT)) continue + if k == "一句话摘要": + # 摘要住卡顶层而非"字段"子对象——此前写进子对象成"影子摘要"且顶层摘要 + # 从不更新(抽检 M4:600 章前的旧摘要一直挂着)。特判写顶层,同守水位闸。 + if isinstance(v, str) and v.strip() and not _is_garbage(v): + if win_no < watermark: + conn.execute( + """INSERT INTO example_upgrade_audit + (draft_id, window_no, field_name, old_value, new_value, tenant_id) + VALUES (%s,%s,%s,NULL,%s,%s)""", + (draft_id, win_no, "迟到覆写弃用:一句话摘要", + json.dumps(v, ensure_ascii=False)[:2000], TENANT)) + else: + conn.execute( + """INSERT INTO example_upgrade_audit + (draft_id, window_no, field_name, old_value, new_value, tenant_id) + VALUES (%s,%s,%s,%s,%s,%s)""", + (draft_id, win_no, "一句话摘要", + json.dumps(payload.get("一句话摘要"), ensure_ascii=False), + json.dumps(v.strip(), ensure_ascii=False), TENANT)) + payload["一句话摘要"] = v.strip() + continue is_append = k in APPEND_FIELDS or isinstance(v, list) or isinstance(fields.get(k), list) # 覆写值以增量口吻开头=模型把增量当全量(抽检#4:新值会抹掉旧基线),转追加不覆写 if not is_append and isinstance(v, str) and \ @@ -303,10 +341,27 @@ def merge_card(conn, draft_id, win_no, changes, alias_new, valid_keys=None): seen = {_strip_prefix(x) for x in old} # 同文去重(抽检#1 重复病) for it in items: core = _strip_prefix(it) # 剥模型自带前缀(抽检#1 堆叠病) + if core and _is_garbage(core): + # 结构垃圾条目拒收留审计(窗75 事故根治:变更包字符串化混进字段值) + conn.execute( + """INSERT INTO example_upgrade_audit + (draft_id, window_no, field_name, old_value, new_value, tenant_id) + VALUES (%s,%s,%s,NULL,%s,%s)""", + (draft_id, win_no, ("垃圾拦截:" + str(k))[:100], + str(core)[:2000], TENANT)) + continue if core and core not in seen: old.append(f"[窗{win_no}] {core}") seen.add(core) - fields[k] = old + # 窗序归位(抽检 M2:补跑/重试窗条目尾插致时间线倒流)——稳定排序,同窗保持原序 + fields[k] = sorted(old, key=_entry_win) + elif isinstance(v, str) and _is_garbage(v): + # 结构垃圾覆写值拒收留审计(同窗75 事故根治) + conn.execute( + """INSERT INTO example_upgrade_audit + (draft_id, window_no, field_name, old_value, new_value, tenant_id) + VALUES (%s,%s,%s,NULL,%s,%s)""", + (draft_id, win_no, ("垃圾拦截:" + str(k))[:100], v[:2000], TENANT)) elif win_no < watermark: # 迟到覆写弃用:本窗时序早于卡已生长到的窗位,覆写会让卡态倒流—— # 只留审计(标记可查),卡体保持高窗态不动 @@ -386,6 +441,15 @@ def new_card(conn, work_id, win_no, ent): (draft_id, window_no, field_name, old_value, new_value, tenant_id) VALUES (%s,%s,%s,NULL,%s,%s)""", (did, win_no, k, json.dumps(v, ensure_ascii=False), TENANT)) + # 初始别名同步入 alias 表(抽检 L3 根源:立卡别名只写卡内不入表, + # 别名表覆盖率仅 26%,"别名精确判重"层大面积空转导致同人两卡漏并) + for al in payload["别名"]: + conn.execute( + """INSERT INTO example_upgrade_alias + (work_id, canonical_name, alias, evidence_window, verdict_by, tenant_id) + VALUES (%s,%s,%s,%s,'init',%s) + ON CONFLICT (tenant_id, work_id, alias) DO NOTHING""", + (work_id, raw, al, win_no, TENANT)) conn.execute( """INSERT INTO example_upgrade_card_state (draft_id, work_id, watermark_window, tenant_id) VALUES (%s,%s,%s,%s) ON CONFLICT (draft_id) DO NOTHING""", @@ -542,6 +606,24 @@ def run(work_id, max_windows, max_calls, model, redo_window): ON CONFLICT (tenant_id, work_id, alias) DO NOTHING""", (work_id, hint, nm, win_no, TENANT)) continue + # 同型名称互为子串(抽检 H5:「果子」vs「开心果子」同人两卡): + # 不直接立卡,转观察材料并入既有卡由更新步 AI 甄别(软防护,留复核标记) + sub_hit = next( + (ex for ex, (d0, t0, _) in name_map.items() + if t0 == ent.get("型", "") and len(nm) >= 2 and len(ex) >= 2 + and nm != ex and (nm in ex or ex in nm)), None) + if sub_hit: + did = name_map[sub_hit][0] + to_update.setdefault(did, []).append( + f"(名称疑似同一实体「{nm}」≈「{sub_hit}」,请甄别后再并入)" + f"初卡材料:{json.dumps(ent, ensure_ascii=False)[:400]}") + conn.execute( + """INSERT INTO example_upgrade_alias + (work_id, canonical_name, alias, evidence_window, verdict_by, tenant_id) + VALUES (%s,%s,%s,%s,'substr',%s) + ON CONFLICT (tenant_id, work_id, alias) DO NOTHING""", + (work_id, sub_hit, nm, win_no, TENANT)) + continue chaps = set(ent.get("出场章", [])) hist = presence.get((ent.get("型", ""), nm), set()) if len(chaps | hist) >= 2: # 跨章(含跨窗合计)→ 立卡