diff --git a/.claude/skills/detect/SKILL.md b/.claude/skills/detect/SKILL.md
index 3cca3f7..637b18f 100644
--- a/.claude/skills/detect/SKILL.md
+++ b/.claude/skills/detect/SKILL.md
@@ -19,6 +19,7 @@ disable-model-invocation: true
| power_system.代价限制 | 境界与能力是否越过体系代价 |
| location.规则特例 | 地点规则是否被违反 |
| item 能力边界 | 物品表现是否超出卡面 |
+| 演变历程(各实体型) | 演变连续性/跳级穿帮(上一章还4级、这一章突然7级;登场/退场节点缺失或时间线倒挂) |
| outline.伏笔动作 | 埋/推/收是否照细纲执行,有无擅自新开或提前回收 |
| work_core.谜底与真相/结局方向 | **防提前泄底**——这正是检测可见底牌的原因 |
| style 黑名单与画像 | Grep 逐词扫黑名单;宣告情绪/形容词堆叠/无三件套场景抽查 |
diff --git a/.claude/skills/parse-book/scripts/migrate_upgrade_windows.py b/.claude/skills/parse-book/scripts/migrate_upgrade_windows.py
new file mode 100644
index 0000000..00ca3c2
--- /dev/null
+++ b/.claude/skills/parse-book/scripts/migrate_upgrade_windows.py
@@ -0,0 +1,368 @@
+#!/usr/bin/env python3
+"""存量升格卡 [窗N] → 真实章号迁移(升格卡改造 §6.1)——**只 --dry-run,全程零写库**。
+
+背景:老升格卡的演变类字段(character.成长弧线 / character_relation.演变轨迹)里,条目带
+运行时窗号前缀 [窗N]/[本窗]。窗号是切窗规则的运行产物(会变、脱离环境不可读),必须换成
+永不变的真实章号(第X章)。本脚本把这些条目:
+ - 实体型(character 等):对象化成里程碑 {章,台阶,周期} 汇入「演变历程」;成长弧线回归"未来计划"。
+ - 关系型(character_relation):保持「演变轨迹」字符串结构,只把 [窗N] 换真实章号前缀。
+
+真实章号定位优先级(落实拍板#5「人工精确化」,不接受 12 章宽近似):
+ 1) 内嵌章号——条目文字里模型已写"第420章/119章/Ch.21",正则直接抽,最准、零 LLM。
+ 2) 前缀误标章号——[窗307-315] 里 307 远超该书窗号上限(work4=80/work8=116),是"第307章"
+ 被误加了"窗"字,采信为章号、零 LLM。
+ 3) 真窗号 [窗N](N≤上限)——读该窗源文、LLM(走 chat_governed 全局治理)定位到真实章。
+ 4) [本窗]/无任何线索——定不了,打"待人工"标记,绝不用窗区间近似糊弄。
+
+范围边界(诚实标注):本脚本只迁**演变类字段**(成长弧线/演变轨迹/演变历程/大事记/经历)。
+item/power_system 等把升级线塞在当前态字段(流转计划/戏剧作用/跨体系换算…)的病根2,属于
+"当前态字段污染",需 P0 提示词改造后**重抽**清理,非本机械迁移能正确归位——脚本对这类字段
+只做「污染报告」提示、不动数据。
+
+连接纪律(硬约束):短连接读→释放→LLM→(本脚本不写库)。绝不在 DB 事务里夹 LLM 调用。
+本脚本无任何 UPDATE/INSERT muse_knowledge_draft——迁移落库由主代理 gate 后另行发起。
+"""
+import json
+import pathlib
+import re
+import sys
+
+import click
+import psycopg
+
+# 复用 parse_upgrade 的守卫/工具(剥前缀、剥尾残、垃圾拦截、内嵌章号、生命周期推断、章号排序键、
+# 窗源文加载、SOURCE_TYPE/TENANT/DSN)——迁移与抽取同一套清洗口径,不另立标准。
+HERE = pathlib.Path(__file__).resolve().parent
+sys.path.insert(0, str(HERE))
+sys.path.insert(0, str(HERE.parents[1] / "llm" / "scripts"))
+import parse_upgrade as pu # noqa: E402
+from llm import chat_governed, extract_json # noqa: E402
+
+DSN = pu.DSN # 带 keepalives(防 Tailscale 长空转掐断),与抽取同源
+SOURCE_TYPE = pu.SOURCE_TYPE
+TENANT = pu.TENANT
+
+# 演变类字段白名单:只有这些字段的 [窗N] 条目属"演变台阶"、走章号迁移;其余字段的 [窗N]
+# 是当前态字段被污染,不在迁移范围(见文件头「范围边界」)。
+EVOLUTION_FIELDS = {"成长弧线", "演变轨迹", "演变历程", "大事记", "经历"}
+RELATION_TYPE = pu.RELATION_TYPE
+
+# 条目行首 [窗…] 前缀内的原始标记('窗2' / '窗307-315' / '本窗' / '窗本窗')
+PREFIX_RE = re.compile(r"^\[([窗本][^\]]{0,12})\]")
+
+
+def parse_prefix(entry):
+ """提取条目行首 [窗…] 前缀内容;无前缀返回 None。"""
+ m = PREFIX_RE.match(str(entry).strip())
+ return m.group(1) if m else None
+
+
+def resolve_chapter(entry, win_max):
+ """给单条演变条目定真实章号。返回 (章号, 来源, 待LLM窗号)。
+ 章号:整数 / 区间字符串"307-315" / None(待LLM 或 待人工时为 None)。
+ 来源 ∈ 内嵌 / 前缀误标章号 / 待LLM / 待人工。"""
+ body = pu._strip_tail(pu._strip_prefix(entry))
+ inline = pu._extract_inline_chapter(body)
+ if inline is not None:
+ return inline, "内嵌", None # ① 最准:条目自带真实章号
+ pref = parse_prefix(entry)
+ if pref:
+ nums = re.findall(r"\d+", pref)
+ if nums:
+ lo = int(nums[0])
+ if lo > win_max: # ② 超窗号上限=章号被误标为窗号
+ ch = nums[0] if len(nums) == 1 else f"{nums[0]}-{nums[1]}"
+ return ch, "前缀误标章号", None
+ return None, "待LLM", lo # ③ 真窗号,需读源文 LLM 精确化
+ return None, "待人工", None # ④ [本窗]/无线索,定不了
+
+
+def migrate_relation_entry(entry, ch, src):
+ """关系型「演变轨迹」条目迁移:保持字符串,只把 [窗N] 换真实章号前缀(不对象化)。"""
+ body = pu._strip_tail(pu._strip_prefix(entry))
+ if parse_prefix(entry) is None:
+ return entry # 无 [窗N](已是真实章号或纯文本),原样不动
+ if src == "内嵌":
+ return body # body 自带章号,剥掉窗前缀即干净
+ if ch is not None:
+ return f"第{ch}章:{body}" # 误标章号/LLM 定位:补真实章号前缀
+ if src == "待LLM":
+ return f"[待LLM] {body}" # 真窗号未精确化:留标记,别当定不了
+ return f"[待人工] {body}" # 定不了([本窗]/无线索):显式标记,不糊弄
+
+
+def migrate_entity_entry(entry, ch, src):
+ """实体型演变条目对象化成里程碑 {章,台阶,周期};降级保底不丢条。
+ 章号定不了(src=待人工)打「待人工」标记供人工精确化;待LLM(未精确化)不标(开 --llm 会定到)。"""
+ body = pu._strip_tail(pu._strip_prefix(entry))
+ m = {"章": ch, "台阶": body, "周期": pu._infer_lifecycle(body)}
+ if ch is None and src == "待人工":
+ m["待人工"] = True
+ return m
+
+
+# ── 短连接读取(读完即释放,LLM/打印阶段不持连接)──
+
+def load_window_maxima(conn):
+ """各作品窗号上限:{work_id: 最大窗号}——判「前缀数字是否超上限=章号误标」。"""
+ rows = conn.execute(
+ "SELECT work_id, max(window_no) FROM example_upgrade_window WHERE deleted=FALSE GROUP BY work_id"
+ ).fetchall()
+ return {w: int(mx) for w, mx in rows}
+
+
+def load_win_ranges(conn, work_id, win_nos):
+ """读指定窗的章区间:{窗号: (from_ch, to_ch)}——给 LLM 精确化缩小定位范围。"""
+ if not win_nos:
+ return {}
+ rows = conn.execute(
+ """SELECT window_no, from_chapter, to_chapter FROM example_upgrade_window
+ WHERE tenant_id=%s AND work_id=%s AND window_no = ANY(%s) AND deleted=FALSE""",
+ (TENANT, work_id, list(win_nos))).fetchall()
+ return {int(w): (a, b) for w, a, b in rows}
+
+
+def load_target_cards(conn, work_id, ids, limit):
+ """读待迁卡:含演变类字段且该字段带 [窗/本窗] 前缀条目的 upgrade_book 卡。
+ 返回 [(id, work_id, payload)]。"""
+ sql = ["""SELECT id, work_id, draft_payload FROM muse_knowledge_draft
+ WHERE tenant_id=%s AND source_type=%s AND deleted=FALSE""", ]
+ args = [TENANT, SOURCE_TYPE]
+ if work_id is not None:
+ sql.append("AND work_id=%s"); args.append(work_id)
+ if ids:
+ sql.append("AND id = ANY(%s)"); args.append(ids)
+ sql.append("ORDER BY id")
+ if limit:
+ sql.append(f"LIMIT {int(limit)}")
+ rows = conn.execute("\n".join(sql), args).fetchall()
+ out = []
+ for did, wid, payload in rows:
+ fields = (payload or {}).get("字段") or {}
+ # 只留"演变类字段里存在带窗前缀条目"的卡(含内嵌章号但无窗前缀的条目不算需迁)
+ hit = any(k in EVOLUTION_FIELDS and isinstance(v, list)
+ and any(parse_prefix(x) for x in v if isinstance(x, str))
+ for k, v in fields.items())
+ if ids or hit: # 显式点名的卡即使无命中也纳入(验收要看"无可迁"边界)
+ out.append((did, wid, payload))
+ return out
+
+
+def scan_pollution(payload):
+ """当前态字段污染报告:非演变类字段里带 [窗N] 的条目数(仅提示,不迁)。返回 {字段: 条数}。"""
+ rep = {}
+ for k, v in ((payload or {}).get("字段") or {}).items():
+ if k in EVOLUTION_FIELDS:
+ continue
+ vals = v if isinstance(v, list) else [v]
+ n = sum(1 for x in vals if isinstance(x, str) and parse_prefix(x))
+ if n:
+ rep[k] = n
+ return rep
+
+
+# ── LLM 精确化(读窗源文 → chat_governed 批量定位;连接纪律:读源文短连接、LLM 无连接)──
+
+def llm_locate_prompt(title, a, b, text, entries):
+ """构造"把若干本窗演变条目定位到真实章号"的 prompt。"""
+ lines = "\n".join(f"{i + 1}. {e}" for i, e in enumerate(entries))
+ return f"""【功能指令(存量升格卡·演变条目真实章号精确化)】
+下面是《{title}》第 {a}-{b} 章的正文,和若干条"确定发生在这段正文里、但没标注精确章号"的演变条目。
+为每条在正文里找到它对应的**真实章号**(正文各章以「## 第N章」开头)。找不到确切章的返回 null,绝不猜。
+
+【本窗正文(第 {a}-{b} 章)】
+{text}
+
+【待定位条目】
+{lines}
+
+【输出规则(只输出一个 JSON 对象)】
+{{"定位": [{{"序号": 1, "章": 章号数字或null}}]}}"""
+
+
+def llm_precise(work_id, title, win_ranges, win_text, pending):
+ """对 {窗号: [条目文本…]} 逐窗 LLM 定位,返回 {(窗号, 条目文本): 章号}。
+ win_text 预读的窗源文(连接已释放);LLM 走 chat_governed(全局额度治理,不自写降级)。"""
+ located = {}
+ for wno, entries in pending.items():
+ if wno not in win_ranges or wno not in win_text:
+ continue
+ a, b = win_ranges[wno]
+ prompt = llm_locate_prompt(title, a, b, win_text[wno], entries)
+ content, _usage, used = chat_governed(prompt, system=pu.IDENTITY)
+ if used is None: # 全局降级链耗尽:本窗放弃精确化,条目留待人工
+ print(f" [窗{wno}] LLM 全链耗尽,该窗 {len(entries)} 条转待人工", file=sys.stderr)
+ continue
+ try:
+ data = extract_json(content)
+ except Exception as e:
+ print(f" [窗{wno}] LLM 输出解析失败({str(e)[:60]}),该窗转待人工", file=sys.stderr)
+ continue
+ for r in [x for x in (data.get("定位") or []) if isinstance(x, dict)]:
+ idx, ch = r.get("序号"), r.get("章")
+ if isinstance(idx, int) and 1 <= idx <= len(entries) and isinstance(ch, int):
+ located[(wno, entries[idx - 1])] = ch
+ return located
+
+
+# ── dry-run 主流程 ──
+
+def plan_card(payload, win_max, located, llm_ran):
+ """算单卡迁移计划(不写库)。返回 (字段级前后对照 dict, 来源计数 dict, 待人工数)。
+ located:{(窗号,条目文本): 真实章号} LLM 精确化结果(无则空 dict)。
+ llm_ran:该卡所属作品本次是否真跑了 LLM 精确化——决定真窗号条目未定到时算「待人工」还是仍留「待LLM」。"""
+ t = payload.get("type", "")
+ fields = payload.get("字段") or {}
+ diffs, srcs, pending_manual = {}, {}, 0
+ milestones = [] # 实体型:各演变类字段汇入的里程碑
+ for k, v in fields.items():
+ if k not in EVOLUTION_FIELDS or not isinstance(v, list):
+ continue
+ new_rel = [] # 关系型:演变轨迹换章号后的字符串条目
+ for entry in v:
+ if not isinstance(entry, str):
+ continue
+ if pu._is_garbage(entry): # 顺手清脏:结构垃圾条目丢弃
+ srcs["垃圾丢弃"] = srcs.get("垃圾丢弃", 0) + 1
+ continue
+ ch, src, wno = resolve_chapter(entry, win_max)
+ if src == "待LLM": # 真窗号条目:回填 LLM 精确化结果
+ body = pu._strip_tail(pu._strip_prefix(entry))
+ hit = located.get((wno, body))
+ if hit is not None:
+ ch, src = hit, "LLM定位"
+ elif llm_ran:
+ ch, src = None, "待人工" # 跑了 LLM 仍定不到 → 真待人工
+ # 否则(未跑 LLM):保持 src="待LLM"、ch=None,不计入待人工(待精确化,非定不了)
+ srcs[src] = srcs.get(src, 0) + 1
+ if src == "待人工":
+ pending_manual += 1
+ if t == RELATION_TYPE:
+ new_rel.append(migrate_relation_entry(entry, ch, src))
+ else:
+ milestones.append(migrate_entity_entry(entry, ch, src))
+ if t == RELATION_TYPE and new_rel:
+ # 去重(同文)+ 按章号排序(抽条目内嵌章号)
+ seen, dedup = set(), []
+ for x in sorted(new_rel, key=lambda s: pu._chapter_sort_key(pu._extract_inline_chapter(s))):
+ key = pu._strip_prefix(x).strip()
+ if key and key not in seen:
+ dedup.append(x); seen.add(key)
+ diffs[k] = {"前": v, "后": dedup}
+ if t != RELATION_TYPE and milestones:
+ # 实体型:成长弧线等 → 演变历程(对象化、去重按台阶、排序按章号);原演变类字段清空(回归未来计划)
+ seen, dedup = set(), []
+ for m in sorted(milestones, key=lambda m: pu._chapter_sort_key(m.get("章"))):
+ key = m["台阶"].strip()
+ if key and key not in seen:
+ dedup.append(m); seen.add(key)
+ old_evo = fields.get("演变历程") if isinstance(fields.get("演变历程"), list) else []
+ diffs["演变历程"] = {"前": old_evo, "后": dedup}
+ for k in EVOLUTION_FIELDS - {"演变历程"}:
+ if isinstance(fields.get(k), list) and fields[k]:
+ diffs[k] = {"前": fields[k], "后": []} # 已发生台阶搬走,原字段清空
+ return diffs, srcs, pending_manual
+
+
+@click.command()
+@click.option("--work-id", type=int, help="限定作品(不给=全部 upgrade_book 作品)")
+@click.option("--ids", help="逗号分隔的卡 id 白名单(验收指定样例卡)")
+@click.option("--limit", type=int, default=0, help="最多处理卡数(0=不限)")
+@click.option("--llm", "use_llm", is_flag=True,
+ help="对真窗号条目真调 LLM 读原文精确化(默认关=纯机械预览,真窗号条目暂标待LLM)")
+@click.option("--max-llm-windows", type=int, default=3, show_default=True,
+ help="LLM 精确化最多读几个窗(控额度,验收演示用)")
+@click.option("--samples", type=int, default=5, show_default=True, help="打印几张卡的前后对照")
+def main(work_id, ids, limit, use_llm, max_llm_windows, samples):
+ """存量 [窗N]→真实章号迁移 dry-run(零写库)。"""
+ id_list = [int(x) for x in ids.split(",") if x.strip()] if ids else None
+
+ # ── 短连接①:读窗号上限 + 待迁卡(读完即释放)──
+ with psycopg.connect(DSN) as conn:
+ win_max_map = load_window_maxima(conn)
+ cards = load_target_cards(conn, work_id, id_list, limit)
+ click.echo(f"待迁卡:{len(cards)} 张(演变类字段含 [窗N] 前缀条目)")
+ if not cards:
+ return
+
+ # ── 无连接:首轮机械解析,收集各作品待 LLM 精确化的 (窗号→条目) ──
+ pending_by_work = {} # work_id -> {窗号: set(条目body)}
+ for did, wid, payload in cards:
+ win_max = win_max_map.get(wid, 10 ** 9)
+ for k, v in ((payload or {}).get("字段") or {}).items():
+ if k not in EVOLUTION_FIELDS or not isinstance(v, list):
+ continue
+ for entry in v:
+ if not isinstance(entry, str) or pu._is_garbage(entry):
+ continue
+ _ch, src, wno = resolve_chapter(entry, win_max)
+ if src == "待LLM":
+ body = pu._strip_tail(pu._strip_prefix(entry))
+ pending_by_work.setdefault(wid, {}).setdefault(wno, set()).add(body)
+
+ # ── LLM 精确化(可选):短连接②读窗源文→释放→无连接调 chat_governed ──
+ located_by_work = {} # work_id -> {(窗号,body): 章号}
+ if use_llm and pending_by_work:
+ for wid, wmap in pending_by_work.items():
+ win_nos = sorted(wmap)[:max_llm_windows] # 控额度:只精确化前 N 个窗
+ with psycopg.connect(DSN) as conn: # 短连接读源文
+ title = conn.execute("SELECT title FROM muse_content_work WHERE id=%s",
+ (wid,)).fetchone()[0]
+ win_ranges = load_win_ranges(conn, wid, win_nos)
+ win_text = {w: pu.load_window_text(conn, wid, *win_ranges[w])
+ for w in win_nos if w in win_ranges}
+ # 连接已释放,此处纯 LLM(chat_governed 全局治理)
+ pend = {w: sorted(wmap[w]) for w in win_nos}
+ located_by_work[wid] = llm_precise(wid, title, win_ranges, win_text, pend)
+ skipped = sum(len(wmap) for wmap in pending_by_work.values()) - \
+ sum(min(len(wmap), max_llm_windows) for wmap in pending_by_work.values())
+ if skipped:
+ click.echo(f"(LLM 精确化受 --max-llm-windows={max_llm_windows} 限,"
+ f"{skipped} 个窗本次未精确化、其条目暂计待人工)")
+ elif pending_by_work:
+ n = sum(len(b) for wmap in pending_by_work.values() for b in wmap.values())
+ click.echo(f"(--llm 未开:{n} 条真窗号条目暂标『待LLM』,加 --llm 真调原文精确化)")
+
+ # ── 无连接:出迁移计划 + 汇总统计 + 样例前后对照 ──
+ total_src, total_manual, changed_cards = {}, 0, 0
+ printed = 0
+ for did, wid, payload in cards:
+ win_max = win_max_map.get(wid, 10 ** 9)
+ diffs, srcs, manual = plan_card(payload, win_max, located_by_work.get(wid, {}),
+ use_llm and wid in located_by_work)
+ pollution = scan_pollution(payload)
+ for s, n in srcs.items():
+ total_src[s] = total_src.get(s, 0) + n
+ total_manual += manual
+ if diffs:
+ changed_cards += 1
+ if printed < samples:
+ printed += 1
+ name = payload.get("名称", "")
+ t = payload.get("type", "")
+ click.echo(f"\n{'=' * 70}\n卡#{did} [{t}] {name}(work={wid})")
+ if not diffs:
+ click.echo(" 演变类字段:无 [窗N] 条目可迁")
+ for k, d in diffs.items():
+ click.echo(f" ── 字段「{k}」:{len(d['前'])} 条 → {len(d['后'])} 条")
+ for x in list(d["前"])[:3]:
+ click.echo(f" 前│ {str(x)[:110]}")
+ for x in list(d["后"])[:3]:
+ click.echo(f" 后│ {json.dumps(x, ensure_ascii=False)[:110] if isinstance(x, dict) else str(x)[:110]}")
+ if pollution:
+ click.echo(f" ⚠ 当前态字段 [窗N] 污染(非演变台阶,建议 P0 重抽清理、本脚本不迁):"
+ + ",".join(f"{k}×{n}" for k, n in pollution.items()))
+
+ # ── 总账 ──
+ click.echo(f"\n{'=' * 70}\n【迁移 dry-run 总账】(零写库,落库由主代理 gate 后另行发起)")
+ click.echo(f" 待迁卡 {len(cards)} 张,有实际迁移计划 {changed_cards} 张")
+ click.echo(f" 条目章号来源分布:" + ",".join(f"{s}×{n}" for s, n in sorted(total_src.items())))
+ click.echo(f" 待人工条目(章号实在定不了):{total_manual} 条")
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except (psycopg.Error, RuntimeError) as e:
+ click.echo(f"[迁移错误] {type(e).__name__}: {e}", err=True)
+ sys.exit(1)
diff --git a/.claude/skills/parse-book/scripts/parse_upgrade.py b/.claude/skills/parse-book/scripts/parse_upgrade.py
index ba05a14..f420826 100644
--- a/.claude/skills/parse-book/scripts/parse_upgrade.py
+++ b/.claude/skills/parse-book/scripts/parse_upgrade.py
@@ -32,6 +32,10 @@ import psycopg
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "llm" / "scripts"))
from parse_llm import m3_json, SensitiveHardStop, IDENTITY, TENANT, DSN # noqa: E402
+# 语义判重(P1)复用 embed skill 的嵌入通道(同模型同维、与检索端语义对齐)——
+# 只在开启 --semantic-dedup 时才真调,默认关(试跑期嵌入延后,见文件头注释)
+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2] / "embed" / "scripts"))
+from embed_drafts import _session as _embed_session, embed_texts # noqa: E402
# ── 窗切割参数(方案 §五B:3–5 万字/窗、10–15 章,取保守双闸防观察输出过载)──
WIN_MAX_CHARS = 35000 # 单窗正文字数上限
@@ -43,8 +47,19 @@ SOURCE_TYPE = "upgrade_book" # 独立来源标记:与范式卡 parse_book 隔
ENTITY_TYPES = ("character", "location", "item", "faction", "power_system", "event")
RELATION_TYPE = "character_relation"
-# 追加类字段白名单(值为数组的字段一律追加;这些字符串字段也强制追加、条目带窗号)
-APPEND_FIELDS = {"成长弧线", "演变轨迹", "大事记", "经历"}
+# 追加类字段白名单(值为数组的字段一律追加)。「演变历程」是里程碑对象数组,走独立对象
+# 合并路径(MILESTONE_FIELDS);其余是字符串条目数组(历史上带 [窗N] 前缀)。「大事记」「经历」
+# 是历史孤儿名(任何 schema 都没定义、会被合同守卫裁掉),保留仅为向后兼容旧数据、不再新用——
+# 升格卡改造(2026-07-17)后已发生台阶统一记入「演变历程」。
+APPEND_FIELDS = {"成长弧线", "演变轨迹", "大事记", "经历", "演变历程"}
+# 里程碑对象数组字段:条目是 {章,台阶,周期} 结构化对象,去重按台阶内容、排序按真实章号
+# (不用运行时窗号)——升格卡改造 P0 落点,区别于上面的字符串条目追加字段。
+MILESTONE_FIELDS = {"演变历程"}
+# 生命周期枚举(设计稿 §4.2):每条里程碑「周期」的取值域。
+LIFECYCLE = ("登场", "成长", "高光", "退场", "结局")
+# 语义判重(P1,设计稿 §8.2):召回同书近邻相似度 ≥ 此阈值才交 M3 终判。
+DEDUP_SIM_THRESHOLD = 0.78
+CHAP_BIG = 10 ** 9 # 章号缺失/待人工的里程碑,排序时排到最后
# ── 库内合同(元数据驱动公理:prompt 与守卫同源,禁手写合同)──
@@ -166,10 +181,11 @@ def observe_prompt(contracts, title, a, b, text, onstage):
onstage_lines = "\n".join(f"- {t}|{nm}|{brief}" for nm, (_, t, brief) in onstage.items()) or "(无)"
return f"""【功能指令(parse-book 作品面升格·实体观察)】
通读本窗正文,产出三类结果(只输出一个 JSON 对象):
-1) 新名字:正文出现、但「在场已知实体」清单里没有的实体(六型:{"/".join(ENTITY_TYPES)})。每个给:型、名称、别名、一句话摘要、按该型合同能填的字段(有正文证据才填)、出场章号。若你怀疑它其实是清单中某已知实体的别名/改名/化名,填「疑似别名指向」。
+1) 新名字:正文出现、但「在场已知实体」清单里没有的实体(六型:{"/".join(ENTITY_TYPES)})。每个给:型、名称、别名、一句话摘要、按该型合同能填的字段(有正文证据才填)、出场章号。若你怀疑它其实是清单中某已知实体的别名/改名/化名,填「疑似别名指向」。若本窗已见其登场或首个进化台阶,按合同给「演变历程」的首条里程碑对象。
2) 已知实体新信息:清单中实体在本窗的实质新信息(境界变化/性格显露/重大经历/立场转变),一条 ≤60 字观察点。没有实质新信息的不要报。
3) 纯出场:清单中实体本窗出现但无实质新信息的,只报名称+出场章。
纪律:一次性龙套(单章无名或仅路过)不报进新名字;实体判据与字段以合同为准,无证据不填;不脑补。数值(战力/指数/排名等)必须正文原样出现才可写,禁止推算或编造。组织改组/合并产生的新组织是**新实体**(走新名字),不是旧组织的别名。「疑似别名指向」只在确为同一实体改名/化名时填。
+里程碑纪律(凡填「演变历程」字段必守):每条是对象 {{"章": 正文原样出现的真实章号(整数如 420,跨多章连续事件用区间字符串如 "420-423"), "台阶": "进化到什么+靠什么事件的一句话", "周期": 登场/成长/高光/退场/结局 之一}};**必须用真实章号,禁止 [窗N] 窗号、禁止"本窗/近期/前段"这类相对指代**(卡会脱离运行环境被单独阅读);抽不出完整对象时至少给 {{"章","台阶"}},别整条丢;当前态字段(品阶/能力与限制/摘要等)只写"现在什么样"的干净值,历史进化流水一律进「演变历程」、不许塞进当前态字段。
【六型字段合同】
{render_entity_contracts(contracts, ENTITY_TYPES)}
@@ -194,7 +210,9 @@ def update_prompt(contracts, title, a, b, text, cards_with_obs):
return f"""【功能指令(parse-book 作品面升格·卡增量更新)】
下列每张卡给出「当前卡全文」与「本窗观察点」。对照本窗正文,**只输出需要变更的字段**:
- 覆写类字段(性格底色/说话方式/当前状态等标量):**必须输出该字段完整的新全量值**——旧值里仍然成立的信息要保留进新值,禁止只写"新增…"式增量(那会把旧信息抹掉);
-- 追加类字段(成长弧线/演变轨迹等数组):只输出本窗新增条目(不要重抄旧条目,不要自己加 [窗N] 前缀,系统会加);
+- 里程碑字段(演变历程):每当实体发生境界/代际/形态/能力的跃迁,或到达登场/高光/退场/结局节点,**追加**一条里程碑对象 {{"章": 真实章号(整数如 420 或跨章区间字符串 "420-423"), "台阶": "进化到什么+靠什么事件的一句话", "周期": 登场/成长/高光/退场/结局 之一}}——只输出本窗**新增**里程碑(不重抄旧条目,不输出 _win 等内部键);章必须是正文原样章号、**禁 [窗N] 与"本窗/近期"相对指代**;抽不出完整对象时至少给 {{"章","台阶"}},别整条丢;
+- 其他追加类字段(成长弧线=未来计划、演变轨迹等数组):只输出本窗新增条目(不要重抄旧条目,不要自己加 [窗N] 前缀,系统会加);
+- **当前态字段干净纪律**:品阶/能力与限制/当前状态/一句话摘要等只写"现在是什么样"的全量当前值——历史进化流水一律进「演变历程」里程碑,**绝不把成长史塞进当前态字段**(这是老卡把升级线塞进「戏剧作用/流转计划/跨体系换算」污染字段语义的病根,务必避免);
- 没有变化的字段不要输出;整卡无实质变化则不输出该卡;
- 该实体的身份/处境已发生重大变化时,把「一句话摘要」也作为变更字段输出(写当前态全量)。
纪律:以正文为证据,不脑补;数值必须正文原样出现,禁止推算;字段 key 必须来自该型合同;**禁止「本窗/本段/近期」等相对时间指代**——写绝对状态或带章号(卡会脱离本窗被单独阅读);只写**该实体自己**的信息(他人的任命/心理/变化不得写入本卡);每卡别名有新发现可在「别名新增」里给——只收**该实体自己**的新别名(他人对它的称呼算,它对别的实体的称呼不算),且必须是可在正文原样出现的纯名字(禁带括号注释与说明文字,禁单字)。
@@ -300,6 +318,90 @@ def _entry_win(x):
return int(m.group(1)) if m else 0
+# ── 里程碑对象(升格卡改造 P0):{章,台阶,周期} 结构化条目的清洗/合并 ──
+# 章级排序不用运行时窗号,而用真实章号(设计稿 §6.1「真实章号索引」的落点)。
+
+# 内嵌真实章号抽取:正文/台阶里原样出现的「第X章 / X章 / Ch.X」——迁移与降级共用
+INLINE_CHAP_RE = re.compile(r"第\s*(\d+)\s*章|(?:Ch|CH|ch)\.?\s*(\d+)|(?>'type', d.draft_payload->>'名称',
+ d.draft_payload->>'一句话摘要', 1 - (e.embedding <=> %s::vector) AS score
+ FROM example_knowledge_embedding e
+ JOIN muse_knowledge_draft d ON d.id = e.draft_id
+ WHERE e.tenant_id=%s AND e.deleted=FALSE AND d.deleted=FALSE
+ AND d.source_type=%s AND d.work_id=%s
+ ORDER BY score DESC LIMIT %s""",
+ (qvec, TENANT, SOURCE_TYPE, work_id, top)).fetchall()
+ return [(r[0], r[1], r[2], r[3], float(r[4])) for r in rows
+ if float(r[4]) >= DEDUP_SIM_THRESHOLD]
+
+
+def dedup_judge_prompt(ent, neighbors):
+ """M3 终判 prompt:候选新实体 vs 每个同书近邻,判 同一实体 / 前身 / 后继 / 无关。"""
+ nb = "\n".join(f"{i + 1}. 卡号{did}|{t}|{nm}|{brief or ''}"
+ for i, (did, t, nm, brief, _s) in enumerate(neighbors))
+ return f"""【功能指令(parse-book 作品面升格·语义判重终判)】
+下面是一个"候选新实体"和若干"同书既有卡"(向量召回的近邻)。逐一判断候选与每张近邻卡的关系,四选一:
+- 同一实体:同一对象的改名/化名/不同侧面(如"影杀者"与"IV代纯机械机甲·影杀者")——**仅同型可判**。
+- 前身:候选是该近邻卡的上一代/来源(同一条进化链的相邻代际,如"铁头(一代)"之于"铁卫(二代)")。
+- 后继:候选是该近邻卡的下一代/继承者。
+- 无关:只是题材相近,各自独立。
+判据:看名称/摘要/字段是否指向同一对象或同一条演变链;**跨型(如具体机甲 item vs 整套体系 power_system)绝不判同一实体,最多判前身/后继**——一整套体系不等于其中一台机体。
+
+【候选新实体】
+型={ent.get("型", "")}|名称={ent.get("名称", "")}|摘要={ent.get("一句话摘要", "")}
+字段:{json.dumps(ent.get("字段", {}), ensure_ascii=False)[:600]}
+
+【同书近邻卡】
+{nb}
+
+【输出规则(只输出一个 JSON 对象)】
+{{"判定": [{{"卡号": 数字, "关系": "同一实体|前身|后继|无关"}}]}}"""
+
+
+def semantic_dedup(conn, sess, work_id, ent):
+ """语义判重裁决。返回 (verdict, data):
+ ('merge', (did, 近邻名称)) 同型·同一实体 → 并卡(治改名漏并)
+ ('chain', [(did, 关系, 名称)]) 前身后继 → 不并卡但记串链候选
+ ('new', None) 无近邻或全判无关 → 各自立卡
+ 同型才允许 merge;跨型即便 M3 判同一实体也降级为串链候选(设计稿 §8.2:跨型仅提示、不自动并)。
+ 判重是增益非必需:召回失败/无向量/终判异常一律保守返回 new(不并可后补,误并难回退)。"""
+ neighbors = recall_neighbors(conn, sess, work_id, ent)
+ if not neighbors:
+ return "new", None
+ try:
+ data, _ = m3_json(dedup_judge_prompt(ent, neighbors), "MiniMax-M3", ("判定",))
+ except (SensitiveHardStop, RuntimeError):
+ return "new", None
+ nb_type = {did: t for did, t, _, _, _ in neighbors}
+ nb_name = {did: nm for did, _, nm, _, _ in neighbors}
+ ent_type = ent.get("型", "")
+ chain = []
+ for j in [x for x in (data.get("判定") or []) if isinstance(x, dict)]:
+ did, rel = j.get("卡号"), j.get("关系")
+ if did not in nb_type:
+ continue
+ same_type = nb_type[did] == ent_type
+ if rel == "同一实体" and same_type:
+ return "merge", (did, nb_name[did]) # 近邻按相似度降序,第一个同型同一实体即采
+ if rel in ("前身", "后继"):
+ chain.append((did, rel, nb_name[did]))
+ elif rel == "同一实体": # 跨型判同一实体不可信 → 降级串链候选
+ chain.append((did, "前身后继待定", nb_name[did]))
+ return ("chain", chain) if chain else ("new", None)
+
+
# ── 命令 ──
@click.group()
@@ -568,9 +787,14 @@ def windows(work_id):
@click.option("--max-calls", type=int, default=0, help="本次 LLM 调用上限(0=不限,含敏感失败)")
@click.option("--model", default="MiniMax-M3", show_default=True)
@click.option("--redo-window", type=int, default=0, help="指定窗号强制重跑(先撤销后重写)")
-def run(work_id, max_windows, max_calls, model, redo_window):
+@click.option("--semantic-dedup", "semantic_on", is_flag=True,
+ help="开启语义判重(P1):立卡前召回同书近邻+M3终判治改名/跨型漏并"
+ "(需同书已 embed 落库;默认关=试跑期嵌入延后)")
+def run(work_id, max_windows, max_calls, model, redo_window, semantic_on):
"""按窗顺序跑升格:断点续跑跳过 done 窗;敏感硬停=窗 failed+书停。"""
calls = {"n": 0} # 调用计数(含敏感失败换模型的次数由 m3_json 内部消化,此处计成功轮次)
+ # 语义判重嵌入会话(仅开启时建;禁系统代理,走内网直连)
+ embed_sess = _embed_session() if semantic_on else None
def call(prompt, need_keys):
calls["n"] += 1
@@ -673,6 +897,35 @@ def run(work_id, max_windows, max_calls, model, redo_window):
if len(chaps | hist) >= 2: # 跨章(含跨窗合计)→ 立卡
if hist: # 用留档补足初卡出场章
ent["出场章"] = sorted(chaps | hist)
+ # 语义判重(P1,--semantic-dedup 开启且同书已 embed 时生效):机械判重
+ # (名字/别名/子串)之后、立卡之前,召回同书近邻交 M3 终判治改名/跨型漏并。
+ if semantic_on:
+ verdict, vd = semantic_dedup(conn, embed_sess, work_id, ent)
+ if verdict == "merge": # 同型同一实体:并入既有卡,不另立
+ did0, canon = vd
+ to_update.setdefault(did0, []).append(
+ f"(语义判重·「{nm}」并入同一实体)初卡材料:"
+ 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,'semantic',%s)
+ ON CONFLICT (tenant_id, work_id, alias) DO NOTHING""",
+ (work_id, canon, nm, win_no, TENANT))
+ continue
+ if verdict == "chain": # 前身后继:仍立卡,串链关系记候选审计
+ did = new_card(conn, work_id, win_no, ent)
+ name_map[nm] = (did, ent.get("型", ""), ent.get("一句话摘要", ""))
+ # 只记候选提示、不自动写「前身/后继」字段——避免误串,链接由人工/后续确认落字段
+ for did2, rel, nm2 in vd:
+ 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)""",
+ (did, win_no, ("串链候选:" + str(rel))[:100],
+ json.dumps({"对方卡号": did2, "对方名称": nm2},
+ ensure_ascii=False), TENANT))
+ continue
did = new_card(conn, work_id, win_no, ent)
name_map[nm] = (did, ent.get("型", ""), ent.get("一句话摘要", ""))
else: # 单章龙套 → 留档(G4)
diff --git a/.claude/skills/read-context/SKILL.md b/.claude/skills/read-context/SKILL.md
index 31f3d8b..371dc3d 100644
--- a/.claude/skills/read-context/SKILL.md
+++ b/.claude/skills/read-context/SKILL.md
@@ -27,7 +27,7 @@ SoT 对齐:包结构=专题-03 §4.2 **四层上下文**;字段级 aiContext 裁
- **Layer 2 作品事实**(一致性/规划不可省略;只读已确认):
- **设定**:`设定.md` 四节;`generation` 用途裁掉「谜底与真相」「结局方向」「弃案记录」等非 generation 字段;
- **大纲**:`generation` 只取主线一句话+当前卷细纲+近三章细纲;「未来卷粗纲」仅 `planning` 可见;
- - **知识卡**:只取本章细纲出场清单涉及的人物/关系卡,及其**关联的地点/势力/功法/物品/事件卡**,**不整库倾倒**(planner/detector/extractor 按差异矩阵取全量索引);卡内字段同按 aiContext 裁——「创作备忘」永不进包,「成长弧线」续写不进(仅 planning/detection)。
+ - **知识卡**:只取本章细纲出场清单涉及的人物/关系卡,及其**关联的地点/势力/功法/物品/事件卡**,**不整库倾倒**(planner/detector/extractor 按差异矩阵取全量索引);卡内字段同按 aiContext 裁——「创作备忘」永不进包,「成长弧线」续写不进(仅 planning/detection);**「演变历程」(各实体型完整历史层的里程碑数组)续写不进(仅 detection/extraction)**——对续写关闭不是防剧透(台阶都已发生),而是怕上万字明细撑爆上下文;续写只需当前态+「演变概括」一句梗概,一致性检查才读全历史查跳级穿帮。
- **Layer 3 授权资料**(按场景可省):
- **公共范式**:仅 `装配.yaml` 已绑定的库;按本章场景类型选卡(如打斗章带打斗卡),不整库带入;extractor/judge 此层关闭(见差异矩阵)。
4. 产出**上下文包**——两个顺序必须分开:
diff --git a/docs/2026-07-16-升格卡改造设计.md b/docs/2026-07-16-升格卡改造设计.md
new file mode 100644
index 0000000..2a4d00f
--- /dev/null
+++ b/docs/2026-07-16-升格卡改造设计.md
@@ -0,0 +1,276 @@
+# 升格卡改造设计(P0 补骨架记里程碑 · P1 串链语义判重)
+
+- 版本:v1(2026-07-16 设计稿,供创始人评审,未实现、未动数据、未提交)
+- 目标读者:创始人 + 后续实现的子代理
+- 边界:只改**参考书作品面升格卡**(`source_type='upgrade_book'`)的字段骨架、抽取提示词、判重方式、上下文裁剪标注;不碰范式卡、不碰主仓。概念定义对齐 `../design-docs/`(专题-06、架构-02)与源方案 `docs/2026-07-14-参考书作品面数据入库方案.md`,本稿是对该方案 §五「实体卡生命周期」的一次修订,不自立门户。
+- 一句话结论:给机甲/武器/力量体系这些型补上一条"从登场到结局"的**演变历程**明细,用**真实章号**当索引(不用会变的运行时窗号),并把这条上万字的明细**只给一致性检查看、不给续写看**(续写只看当前态和一句话梗概);再用**语义判重**把改了名、换了型的同一条成长线并回一起、串成前后链。
+
+---
+
+## 一、先说人话:这次要解决什么
+
+升格卡 = 每本参考书里"会随剧情长大的实体卡"——人物、机甲、武器、力量体系都算。它的价值是:当 muse 学一本书时,能看清"这台生物机甲是怎么从 4 级一步步打到 7 级对决"的完整成长线。
+
+现在这条线**维护不出来**。典型病例:一台生物机甲开头 4 级出场,中段 5 级 6 级,终局 7 级对决——卡里完全看不到这条演变线,只剩一个"最终态",中间全被抹平了。
+
+这次改造分两步走:
+
+- **第一步(先补骨架、记里程碑)**:给机甲/武器/力量体系这些型补上"演变历程"字段,把每一次进化台阶记成一条条里程碑,不再被覆写抹平、不再塞错字段。
+- **第二步(再串链、上语义判重)**:加"前身/后继"把散落的碎卡串成一条进化链;判重从"只比名字字符串"升级到"比语义",治改名和跨型漏并。
+
+两个硬要求贯穿全程:**里程碑用真实章号索引(不用窗号)**;**卡片分两层给——续写只看当前态,一致性检查才看全历史**。
+
+---
+
+## 二、病根复述(都有真实卡为证)
+
+以下四条病根,均从库里真实升格卡(work_id=8)抽样核实,不是推断。
+
+### 病根 1:只有"人物"型有成长字段,机甲/武器/力量体系型是"骨架残缺"
+
+库里 23 型中,只有两个型有"记录演变"的数组字段:`character`(成长弧线)、`character_relation`(演变轨迹)。机甲/武器/战舰所属的 `item` 型、力量体系所属的 `power_system` 型,以及 `faction`/`location`/`event`,**一个演变字段都没有**。
+
+- 铁证:机甲卡「铁头机甲(一代)」(id=1154)出场横跨 64 章(第 10 章到第 548 章),字段却只有"品阶/类别/知情范围/能力与限制",全是会被覆写的当前值。它的一句话摘要是"已退役,封存于暗龙会仓库"——这是**最终态**,把它 10 章出场时"崭新训练机"的样子彻底抹平了。一台陪跑全书的机甲,成长线为零。
+
+### 病根 2:进阶要么被覆写抹平,要么塞进错字段成流水账
+
+没有专门的演变字段,模型只能把"进化"硬塞进当前态字段,结果字段语义被污染:
+
+- 机甲卡「影杀者」(id=4935):把"被重创 → 修复 → 配合突围"这条演变线,塞进了「流转计划」(本该写未来伏笔)和「能力与限制」(本该写当前能力),还带着"本窗/前窗"这种一旦脱离运行环境就没法读的相对指代。
+- 力量体系卡「生物机甲」(id=6223,正是病例本体):把"第 420 章 V 代编队 → 477 章拦截 → 489 → 490-492 → 549-550 → 572 章"整条升级线,塞进了「戏剧作用」(本该一句话写规则)和「跨体系换算」(本该写强弱对照),写成一大段散文流水账。它的摘要同样是最终态(200% 同步率对决 VI 型领主),4 级出场的初始态被抹平。
+- 对照「机甲世代」(id=4995)的「境界阶梯」="一代 < 二代 < 三代 < 四代 < 伪 V 代"——这是**静态的世界规则**(一把标尺),不是某台机甲实际爬台阶的历程。说明现有字段能记"规则",但记不了"某个实体自己怎么一步步长大"。
+
+### 病根 3:一条成长主线被拆成 30+ 张碎卡,跨多个型,彼此无链接
+
+"机甲代际"这条主线,散落在:力量体系型的「生物机甲」「机甲世代」「旧联邦机甲代际」,加上 item 型的「铁头机甲」「铁卫」「枪骑士」「影杀者」等一堆具体机甲卡。它们之间**没有任何链接**,看不出"铁头(一代)→ 铁卫(二代)→ 枪骑士(三代)→ 影杀者(四代)→ 生物机甲(V 代)"是同一条进化链。
+
+### 病根 4:判重只比名字字符串,语义判重是关着的
+
+现在的判重(在 `parse_upgrade.py` 的判重步)只有三招:名字/别名精确匹配、模型自报的疑似别名、同型名字互为子串。**没有接语义判重**——代码里白纸黑字写着"嵌入延后:试跑期不调嵌入 API"。
+
+后果:同一实体一旦改名(如"影杀者"→"IV 代纯机械机甲·影杀者")或跨型指代,字符串对不上就漏并,进一步加剧病根 3 的碎卡。
+
+> 补充:源方案 v6 本来就设计了"名字精确 → 嵌入近邻 → AI 终判"三级判重,只是试跑期把嵌入近邻这级**关掉了**。所以第二步的语义判重不是发明新东西,是**把 v6 已设计好、暂时关着的那一级打开**。
+
+---
+
+## 三、方案总览
+
+```mermaid
+flowchart TB
+ subgraph P0["第一步 P0:补骨架 + 记里程碑(治病根 1、2)"]
+ A1["给 item/power_system 等型
加『演变历程』数组字段"]
+ A2["加进追加白名单 APPEND_FIELDS
(只往后加,永不覆写抹平)"]
+ A3["改抽取提示词:
每次进化记一条里程碑
标生命周期(登场→高光→退场→结局)
当前态字段保持干净、不塞历史"]
+ end
+ subgraph P1["第二步 P1:串链 + 语义判重(治病根 3、4)"]
+ B1["加『前身/后继』链接字段
把碎卡串成一条进化链"]
+ B2["判重打开语义近邻(≥0.78)
+ M3 终判 → 治改名/跨型漏并"]
+ end
+ subgraph FIX["两点硬性细化(贯穿全程)"]
+ C1["① 里程碑用真实章号索引
绝不用运行时窗号 [窗N]"]
+ C2["② 卡片分两层披露
续写只看当前态+梗概
一致性检查才看全历史
(复用现成的 aiContext 按用途裁剪)"]
+ end
+ P0 --> P1
+ C1 -.贯穿.-> P0
+ C2 -.贯穿.-> P0
+```
+
+---
+
+## 四、第一步 P0:补骨架 + 记里程碑
+
+### 4.1 加什么字段
+
+给缺演变字段的实体型补一个统一的「演变历程」数组字段。核心是 `item`(机甲/武器/战舰)和 `power_system`(力量体系/机甲代际);`faction`/`location`/`event` 按需跟进(组织兴衰、地点易主、事件推进同样能用,建议同批补上,避免二次改)。
+
+配套还要两个字段(详见第五节 schema 定义):
+
+- 「演变概括」——一句话说清整条线的来龙去脉(现状层,续写可见)。
+- 「前身」「后继」——串链用(第二步 P1 用,schema 一次性加齐)。
+
+### 4.2 抽取提示词怎么改(要点,不写代码)
+
+改 `parse_upgrade.py` 里的两段提示词:
+
+- **实体观察提示词(observe)**:新实体立卡时,如果这一窗就观察到它的登场/首个进化台阶,产出「演变历程」的第一条里程碑(带真实章号 + 生命周期标记)。
+- **卡更新提示词(update)**:从现在的"记新信息"改为明确的"**记进化台阶 + 生命周期**"——每当实体发生境界/代际/形态/能力的跃迁,或到达登场、高光、退场、结局这些节点,就**追加**一条里程碑,绝不覆写、绝不抹平旧台阶。同时要求:当前态字段(品阶/能力与限制/摘要)只写"现在是什么样"的干净全量值,**不许把历史流水塞进来**(直接治病根 2 的"塞错字段")。
+
+提示词纪律要加两条硬约束:
+
+1. 每条里程碑**必须带真实章号**(正文里原样出现的"第 X 章"),**禁止 [窗N] 窗号、禁止"本窗/近期/前段"这类相对指代**——因为卡会脱离运行环境被单独阅读。
+2. 生命周期标记从固定枚举里选:**登场 / 成长 / 高光 / 退场 / 结局**。这一条直接治"铁头机甲摘要跳到已退役、中间态全丢"——有了登场和退场节点,一台机甲什么时候来的、什么时候谢幕,一目了然。
+
+### 4.3 合并逻辑要跟着改
+
+「演变历程」进 `APPEND_FIELDS` 追加白名单后,`parse_upgrade.py` 的合并/立卡/撤销逻辑(`merge_card`/`new_card`/`undo_window`)要从"处理带 [窗N] 前缀的字符串条目"适配为"处理带章号的里程碑条目":去重按里程碑内容、排序按**章号**(不再按窗号)。现有那套针对脏数据的守卫(剥前缀、剥尾部残渣、拦垃圾、拆巨型粘连)要保留并改造,别丢。
+
+> 顺带清理:`APPEND_FIELDS` 里的「大事记」「经历」两个名字在任何 schema 里都没有定义(是孤儿字段,模型就算输出也会被字段校验裁掉)。这次统一到「演变历程」,孤儿名可保留做向后兼容但不再新用。
+
+---
+
+## 五、「演变历程」字段的 schema 定义
+
+### 5.1 字段清单(加在 item.yaml / power_system.yaml 等型的"特有字段")
+
+| 字段名 | 类型 | 说明 | 属哪层 |
+|---|---|---|---|
+| 演变历程 | 里程碑对象数组 | 已发生的进化台阶明细,一条一台阶,永远追加不覆写 | 完整历史层 |
+| 演变概括 | 一句话文本 | 整条线的来龙去脉梗概(如"4 级训练机出场 → V 代编队主力 → 300% 同步率对决 VI 型领主") | 现状层 |
+| 前身 | 链接(卡号 + 名称) | 这张卡的上一代/来源,指向同书另一张卡 | 现状层 |
+| 后继 | 链接(卡号 + 名称) | 这张卡的下一代/继承者,指向同书另一张卡 | 现状层 |
+
+### 5.2 里程碑对象结构(每条演变历程条目长这样)
+
+推荐用**极简三字段结构化对象**,既能按章号排序/定位,又不会因字段太多让模型抽崩:
+
+```json
+{
+ "章": 420,
+ "台阶": "V代编队首战:暗星帝国出动十台生物机甲配合亡灵号,扭转多瑙星海战场局势",
+ "周期": "高光"
+}
+```
+
+- **章**:绝对章号,用于索引与排序。单章事件填整数(`420`);跨多章的连续事件填区间字符串(`"420-423"`)。**这是"用绝对章号索引"的落点**。
+- **台阶**:一句话讲清"进化到什么 + 靠什么事件"。刻意把"结果 + 经过"合成一句,不再往下拆细字段,避免模型输出格式崩。
+- **周期**:生命周期枚举,取值 `登场 / 成长 / 高光 / 退场 / 结局`。
+
+这个结构是有真实依据的:库里关系卡「苏铭×林初雨」早期就自发用过 `{状态, 章区间: "Ch.21", 转折事件}` 这种带章号的结构化格式,证明模型抽得出、章号索引可行。三字段是"结构化可查询"和"模型稳定"之间的最优平衡点。
+
+> 备选(若评审认为对象改造风险太大):退一步用**带章号前缀的字符串条目**,即把现在的 `[窗2] xxx` 换成 `[第420章·高光] xxx`。改动更小、迁移更平滑,但章号在文字前缀里,得靠正则抽取、不能直接用 SQL 过滤。推荐前者、备选后者,见第九节拍板点。
+
+### 5.3 一条真实成长线改造后长什么样(以病例"生物机甲"示意)
+
+现在(病根 2 的样子):升级史一大段塞在「戏剧作用」字段里,散文流水,摘要只剩最终态。
+
+改造后:
+
+- 演变概括(现状层,续写可见):"卡依斯生物反应驱动的奇袭兵器,从 4 级机体登场,历经 V 代编队、300% 同步率解放,终局对决近帝皇级 VI 型领主。"
+- 演变历程(完整历史层,只给一致性检查):
+ - `{章: 332, 台阶: "生物机甲首次登场,作为突破哈姆斯要塞的核心奇袭兵器", 周期: 登场}`
+ - `{章: 420, 台阶: "V代编队首战,配合亡灵号扭转多瑙星海战场", 周期: 成长}`
+ - `{章: "490-492", 台阶: "苏铭精神链接同步率飙至300%,压制伪VI代、夺龙基之枪贯穿拜鲁特", 周期: 高光}`
+ - `{章: "549-550", 台阶: "300%同步率独战近帝皇级VI型英雄级伊琳莉丝,龙基之枪刺穿其AT立场", 周期: 结局}`
+
+一条清清爽爽、按章号排序、能看出起承转合的成长线就立住了。
+
+---
+
+## 六、两点硬性细化的落地
+
+### 6.1 细化①:真实章号索引 + 现有 [窗N] 卡的迁移办法
+
+**为什么不能用窗号**:窗号([窗N])是抽取时"切正文窗"的运行产物——切窗规则是"每窗 12 章或 3.5 万字",规则一改窗号全变。它是运行时定义、会变、不该当卡的稳定属性。真实章号(第 420 章)才是永远不变的锚。
+
+**存量卡怎么迁移**(一次性迁移脚本,本稿只给办法、不实现):遍历所有 `upgrade_book` 卡的演变类字段(成长弧线/演变轨迹等)里每一条带 [窗N] 的条目,按下面优先级把 [窗N] 换成真实章号:
+
+1. **优先——抽条目文字里已有的真实章号**:很多条目正文里模型自己就写了"第 420 章""163 章""Ch.21",正则抽出来直接当「章」。这是最准的。
+2. **兜底——用窗到章的映射表**:条目里没有内嵌章号的,查 `example_upgrade_window` 表(窗号 → from_chapter~to_chapter),把 [窗N] 映射成该窗的**章区间**(如 [窗2] → "13-24"),并打一个"章区间近似"标记,表明这是窗级近似、不是精确到章。
+3. **顺手清脏**:迁移时一并清掉已知垃圾——`[窗17] ['窗23` 这类残片、重复条目、尾部 JSON 拼接残渣。复用 `parse_upgrade.py` 里现成的剥前缀/剥尾残/拦垃圾逻辑。
+
+> 诚实边界:上万字的历史里,兜底那批(无内嵌章号、只能靠窗映射)会损失精度——只能定位到 12 章宽的区间。这是存量迁移的固有局限,新抽取的卡因为提示词强制带真实章号不受影响。
+
+### 6.2 细化②:渐进式披露 = 现状层 + 完整历史层,接现有 aiContext 按用途裁剪
+
+**这不是新机制**。本仓早就有"字段按用途可见性裁剪"的机制:每个 schema 字段有个 `aiContext` 标注——`true` 任何场景可见、`false` 一律不给、`[用途…]` 只有列出的场景可见。用途有四种:`generation`(续写)、`detection`(一致性检查)、`planning`(规划)、`extraction`(抽取判重)。`read-context`(组装上下文)、`search`(检索)、`detect`(一致性检查)三个 skill 都已经按这个标注干活。渐进披露就是**给演变字段填对 aiContext**,一行标注的事。
+
+分两层:
+
+- **现状层**——写作 agent 续写时默认只读这层。包括实体当前态(品阶/能力与限制/当前持有者/一句话摘要)+ 演变概括 + 前身后继。让续写者知道"这台机甲现在几级、一路怎么来的梗概、前身是谁",但不被上万字明细淹没。
+- **完整历史层**——只有一致性检查(detector)读全。就是「演变历程」那个可能上万字的里程碑数组。续写用不上这么细,一致性检查却必须有它才能查出"上一章还 4 级、这一章怎么突然 7 级"这种跳级穿帮。
+
+**用途可见性分层表**(这张表就是要填进各型 schema 的 aiContext 标注):
+
+| 层 | 字段 | aiContext 标注 | 续写
generation | 一致性检查
detection | 规划
planning | 判重
extraction |
+|---|---|---|:---:|:---:|:---:|:---:|
+| 现状层 | 当前态(品阶/能力与限制/当前持有者) | `true` | ✓ | ✓ | ✓ | ✓ |
+| 现状层 | 一句话摘要 | `true` | ✓ | ✓ | ✓ | ✓ |
+| 现状层 | 演变概括 | `true` | ✓ | ✓ | ✓ | ✓ |
+| 现状层 | 前身 / 后继 | `true` | ✓ | ✓ | ✓ | ✓ |
+| **完整历史层** | **演变历程(全里程碑)** | **`[detection, extraction]`** | **✗** | ✓ | (可选) | ✓ |
+
+关键就是最后一行:把「演变历程」标成 `[detection, extraction]`——续写(generation)自动看不到,一致性检查(detection)和判重(extraction)才看得到。填完这一行,三件事**自动发生、不用再写别的代码**:
+
+1. `detect` skill 会自动把「演变历程」纳入检查项(它的规则就是"凡 aiContext 含 detection 的字段,自动成为一个检查项")——一致性检查从此多一道"演变连续性/跳级穿帮"关卡。
+2. `read-context` 组装续写上下文时,自动把「演变历程」裁掉——写作 agent 的上下文不会被上万字历史撑爆。
+3. `search --purpose generation` 检索时也自动裁掉它。
+
+> 一个要讲清的区别:`character` 现有的「成长弧线」标的是 `[planning, detection]`,它的语义是"**计划中**的变化,防抢进度剧透"——防的是"未来"。而「演变历程」是"**已发生**的历史",它对续写不可见的原因不是防剧透(都发生过了),而是**怕上万字撑爆上下文**。两者都对续写关闭,但一个防未来、一个防过载,别混为一谈。也因此,关系卡的「演变轨迹」保持 `true`(续写可见)是合理的——俩人现在什么关系、怎么走到这一步,对续写笔触很重要,且量不大;而机甲的上万字升级史续写用不上,只需当前几级。
+
+---
+
+## 七、改动清单(涉及哪些文件 / schema / prompt / skill)
+
+> 全部是设计稿层面的改动点,本稿不实现、不执行、不重跑种子、不提交。
+
+| 文件 | 改什么 | 属 |
+|---|---|---|
+| `meta/schemas/item.yaml` | 加「演变历程」`[detection,extraction]`、「演变概括」`true`、「前身」「后继」`true` 四字段 | schema |
+| `meta/schemas/power_system.yaml` | 同上 | schema |
+| `meta/schemas/faction.yaml` `location.yaml` `event.yaml` | 同上(建议同批补,避免二次改;见拍板点) | schema |
+| `meta/schemas/character.yaml` | 语义对齐:明确「成长弧线」=未来计划,已发生台阶归「演变历程」(是否给 character 也加演变历程见拍板点) | schema |
+| `meta/schemas/character_relation.yaml` | 「演变轨迹」说明里"章区间"沿用真实章号(本就如此),确认 aiContext 保持 `true` | schema |
+| `.claude/skills/parse-book/scripts/parse_upgrade.py` | ①`APPEND_FIELDS` 加「演变历程」②observe/update 提示词改为记台阶+生命周期+真实章号、禁窗号相对指代、当前态字段保持干净 ③合并/立卡/撤销逻辑适配里程碑对象(去重按内容、排序按章号)④判重步接语义近邻+M3 终判(P1) | prompt + 逻辑 |
+| `.claude/skills/db/scripts/seed_schemas.py` | 不改脚本;schema YAML 改完后**需重跑一次**把新字段和 aiContext 灌进库(本稿不跑,实施时跑) | 种子 |
+| `.claude/skills/read-context/SKILL.md` | Layer 2 知识卡裁剪说明里,点名"演变历程完整层仅一致性检查可见、续写不给"(机制已支持,补一句说明) | skill 文档 |
+| `.claude/skills/detect/SKILL.md` | 检查项示例表补一行"演变历程 → 演变连续性/跳级穿帮"(实际自动生成,文档补例) | skill 文档 |
+| 新增一次性迁移脚本(如 `docs/` 或 skill scripts 下) | 存量卡 [窗N] → 真实章号 + 字符串条目 → 里程碑对象 + 清脏(见 6.1) | 迁移 |
+
+**不需要改的**:数据库表结构(演变历程/前身/后继都是知识卡 JSON 里的字段,不需要 DDL 建表/改表);语义判重复用现成的 `example_knowledge_embedding` 向量表和 `embed`/`search` skill,不新建表。
+
+---
+
+## 八、第二步 P1:串链 + 语义判重(细化)
+
+### 8.1 串链:前身/后继
+
+用第五节已定义的「前身」「后继」链接字段(指向同书另一张卡的卡号 + 名称),把"铁头(一代)→ 铁卫(二代)→ 枪骑士(三代)→ 影杀者(四代)→ 生物机甲(V 代)"这条链串起来。链接机制照抄现成的做法——关系卡早就用"甲方卡号/乙方卡号"指向人物卡,一样的路子。标 `aiContext: true`,续写要知道"这台机甲的前身是谁"。
+
+### 8.2 语义判重:打开嵌入近邻 + M3 终判
+
+在判重步,除现有的名字精确匹配/子串外,新增(就是打开 v6 已设计、暂时关着的那级):
+
+1. 对候选新实体算向量(走 `embed` skill),用 `search` 召回**同书同型**相似度 ≥ 0.78 的近邻卡。
+2. 召回的近邻交给 M3 终判(走 `parse_llm.py` 的 `m3_json` 入口,构造"这两张卡是不是同一实体 / 是不是前身后继关系"的判断),判定三选一:**同一实体**(并卡)/ **前身后继**(不并卡但串链)/ **无关**(各自立卡)。
+
+这样"影杀者"和"IV 代纯机械机甲·影杀者"这种改名、以及跨型指代同一条线的,都能被语义召回并正确处理。
+
+**跨型判重要收着点**:`item`(具体某台机甲)和 `power_system`(一整套体系)很容易被向量拉近却其实不是一回事(如"生物机甲体系"vs 具体机甲"伊琳娅丝")。所以跨型只做"提示 + 串链候选",不自动并卡;同型才允许自动并。
+
+---
+
+## 九、风险与验证办法
+
+### 风险
+
+1. **条目从字符串变对象,合并逻辑改动面不小**。`parse_upgrade.py` 现有一大套针对字符串条目的守卫(剥前缀、剥尾残、拦垃圾、拆粘连、窗序归位),全要跟着改造成处理里程碑对象;存量卡还是字符串,迁移要兼容。→ 缓解:分两步落,先补字段和提示词(新卡受益),再做存量迁移;对象结构压到三字段降低崩坏面。
+2. **模型抽结构化对象比抽字符串更容易格式崩**。→ 缓解:三字段极简;保留降级——抽不出完整对象就退化成"章号 + 一句话"最小对象,别整条丢。
+3. **语义判重增加调用量和跨型误并风险**。→ 缓解:0.78 阈值 + 同型才自动并 + 跨型仅提示;判重近邻可像 v6 说的那样批量做、不逐条烧。
+4. **存量迁移精度损失**:无内嵌章号的条目只能靠窗映射到 12 章宽区间。→ 接受为存量固有局限,新卡不受影响。
+5. **改 schema 后必须重跑种子**,否则库里字段合同和 YAML 不一致,抽取会按旧合同把新字段裁掉。→ 实施清单里钉死"改 YAML 后重跑 `seed_schemas.py`"这一步。
+
+### 验证办法(实施后怎么确认真治好了)
+
+- **成长线立得住**:挑病例"生物机甲"这条线(work_id=8)跑改造后的抽取,看能不能维护出"4 级登场 → V 代 → 300% 高光 → 结局对决"这条按章号排序、带生命周期的清晰台阶,以及"铁头 →…→ 生物机甲"的前身后继链。
+- **分层裁剪真生效**:`search --purpose generation` 查一张机甲卡,确认「演变历程」被裁掉、只回现状层;`--purpose detection` 确认「演变历程」保留。
+- **一致性检查自动加项**:对一章跑 `detect`,确认检查清单里自动多了"演变连续性"这一项。
+- **语义判重召回**:造一个改名样例(影杀者 / IV 代纯机械机甲·影杀者),确认语义判重能召回并由 M3 判为同一实体或前身后继。
+- **章号无窗号残留**:迁移后全库扫一遍,确认演变类字段里不再有 [窗N] 前缀。
+
+---
+
+## 十、需要创始人拍板的点
+
+> **✅ 已拍板(2026-07-17,创始人 4 项 + 主代理定 #4)——实现以此为准,下方原始理由留档:**
+> 1. 里程碑格式 = **结构化对象(章 / 台阶 / 周期)**,配降级(抽不出完整对象退成"章号 + 一句话",不整条丢)。
+> 2. 补齐范围 = **五型全补**:item / power_system / faction / location / event。
+> 3. character = **一起改**:给人物也加「演变历程」、「成长弧线」回归"未来计划"本义,连带迁移 200+ 张存量人物卡。
+> 4. 演变概括 = **独立字段**(主代理定:摘要管当前态、概括管轨迹,职责清晰)。
+> 5. 存量迁移 = **人工精确化**:不接受 12 章宽近似——无内嵌章号的老条目靠再抽原文 / 人工核对精确到真实章号。
+
+1. **里程碑用"结构化对象"还是"带章号前缀的字符串"?** 本稿推荐三字段对象(章/台阶/周期,可 SQL 按章排序、结构清晰),备选字符串前缀 `[第420章·高光] …`(改动最小、迁移最平滑)。取舍是"规整可查询"对上"改动小、模型稳"。
+2. **演变字段是否给全部实体型补齐?** 核心必补 `item`/`power_system`。`faction`/`location`/`event` 建议同批补(组织兴衰、地点易主、事件推进同样用得上),避免将来二次改 schema。请拍板范围。
+3. **`character` 型怎么处理?** 现在它把"已发生历程"记在名为「成长弧线(未来计划)」的字段里,语义拧巴。是否给 character 也加「演变历程」、让「成长弧线」回归"未来计划"本义?这会牵动 200+ 张存量 character 卡的迁移,请拍板做还是暂缓。
+4. **「演变概括」是独立字段还是并进「一句话摘要」?** 本稿推荐独立字段(摘要管当前态、概括管轨迹,职责清晰);也可并进摘要写法要求省一个字段(更省、但摘要职责变重)。
+5. **存量迁移的精度损失可接受吗?** 无内嵌章号的老条目只能定位到 12 章宽区间。确认接受,还是要投入人工/再抽一遍把这批精确化。
diff --git a/meta/schemas/character.yaml b/meta/schemas/character.yaml
index b376a3c..0dde819 100644
--- a/meta/schemas/character.yaml
+++ b/meta/schemas/character.yaml
@@ -17,5 +17,12 @@ scope: entity
- { key: 行事逻辑, 说明: 面对冲突他会怎么选——行为一致性的判据, aiContext: true }
- { key: 说话方式, 说明: 语言指纹:口头禅/句长/称呼习惯/敬语与粗口, aiContext: true }
- { key: 秘密与底牌, 说明: 写手需要它才能写出言行的弦外之音, aiContext: true }
- - { key: 成长弧线, 说明: 计划中的变化轨迹,续写不可见防抢进度, aiContext: [planning, detection] }
+ - { key: 成长弧线, 说明: 「未来计划」——计划中(尚未发生)的变化轨迹,续写不可见防抢进度剧透;已发生的台阶改归「演变历程」,二者分工不混, aiContext: [planning, detection] }
+ # ── 升格卡改造(2026-07-17)新增:演变历程(完整历史层,已发生台阶)+演变概括(现状层)。定义见 docs/2026-07-16-升格卡改造设计.md 第五节、拍板#3 ──
+ - key: 演变历程
+ 说明: "已发生的成长台阶明细,一条一台阶只追加不覆写;每条是对象 {章: 绝对章号(整数如 420,或跨章区间字符串如 \"420-423\"), 台阶: 成长到什么+靠什么事件的一句话, 周期: 登场/成长/高光/退场/结局}。按真实章号排序索引(不用运行时窗号)——完整历史层,续写不给(防上万字明细撑爆上下文),仅一致性检查与判重可见;与成长弧线分工:本字段记已发生、成长弧线记未来计划"
+ aiContext: [detection, extraction]
+ - key: 演变概括
+ 说明: "整条成长线的一句话梗概。现状层续写可见;与一句话摘要分工——摘要写当前态、本字段写来龙去脉"
+ aiContext: true
- { key: 创作备忘, 说明: 作者碎碎念,任何用途不入 AI 上下文, aiContext: false }
diff --git a/meta/schemas/event.yaml b/meta/schemas/event.yaml
index 66e1914..e9d427b 100644
--- a/meta/schemas/event.yaml
+++ b/meta/schemas/event.yaml
@@ -14,3 +14,16 @@ scope: event
- { key: 因果后续, 说明: 它导致了什么、逼谁做了什么, aiContext: true }
- { key: 知情范围, 说明: 谁知道此事、知道到什么程度——「不该知道的人说漏嘴」是最常见穿帮, aiContext: true }
- { key: 事实状态, 说明: 已发生/预定发生;预定事件续写不可见, aiContext: [planning, detection] }
+ # ── 升格卡改造(2026-07-17)新增:演变历程(完整历史层)+演变概括/前身/后继(现状层)。定义见 docs/2026-07-16-升格卡改造设计.md 第五节 ──
+ - key: 演变历程
+ 说明: "已发生的推进台阶明细(事件多阶段发展:起因/激化/转折/收束),一条一台阶只追加不覆写;每条是对象 {章: 绝对章号(整数如 420,或跨章区间字符串如 \"420-423\"), 台阶: 推进到什么+靠什么触发的一句话, 周期: 登场/成长/高光/退场/结局}。按真实章号排序索引(不用运行时窗号)——完整历史层,续写不给(防上万字明细撑爆上下文),仅一致性检查与判重可见"
+ aiContext: [detection, extraction]
+ - key: 演变概括
+ 说明: "整条推进线的一句话梗概。现状层续写可见;与一句话摘要分工——摘要写当前态、本字段写来龙去脉"
+ aiContext: true
+ - key: 前身
+ 说明: "前置/起源事件,指向同书另一张卡(卡号+名称);把散落碎卡串成一条因果链"
+ aiContext: true
+ - key: 后继
+ 说明: "衍生/后续事件,指向同书另一张卡(卡号+名称);把散落碎卡串成一条因果链"
+ aiContext: true
diff --git a/meta/schemas/faction.yaml b/meta/schemas/faction.yaml
index b55463c..3d91d74 100644
--- a/meta/schemas/faction.yaml
+++ b/meta/schemas/faction.yaml
@@ -15,3 +15,16 @@ scope: entity
- { key: 地盘, 说明: 指向地点卡, aiContext: true }
- { key: 利益诉求, 说明: 它现在最想要什么——行为预测的依据, aiContext: true }
- { key: 与主角线的关系, aiContext: true }
+ # ── 升格卡改造(2026-07-17)新增:演变历程(完整历史层)+演变概括/前身/后继(现状层)。定义见 docs/2026-07-16-升格卡改造设计.md 第五节 ──
+ - key: 演变历程
+ 说明: "已发生的进化台阶明细(组织兴衰:创立/扩张/内乱/覆灭),一条一台阶只追加不覆写;每条是对象 {章: 绝对章号(整数如 420,或跨章区间字符串如 \"420-423\"), 台阶: 演变到什么+靠什么事件的一句话, 周期: 登场/成长/高光/退场/结局}。按真实章号排序索引(不用运行时窗号)——完整历史层,续写不给(防上万字明细撑爆上下文),仅一致性检查与判重可见"
+ aiContext: [detection, extraction]
+ - key: 演变概括
+ 说明: "整条演变线的一句话梗概。现状层续写可见;与一句话摘要分工——摘要写当前态、本字段写来龙去脉"
+ aiContext: true
+ - key: 前身
+ 说明: "前身组织/来源,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true
+ - key: 后继
+ 说明: "继承/分裂出的后续组织,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true
diff --git a/meta/schemas/item.yaml b/meta/schemas/item.yaml
index a9f866a..ab87f2b 100644
--- a/meta/schemas/item.yaml
+++ b/meta/schemas/item.yaml
@@ -14,3 +14,16 @@ scope: entity
- { key: 当前持有者, 说明: 指向人物卡,随确认更新, aiContext: true }
- { key: 知情范围, 说明: 谁知道它的存在与真实能力, aiContext: true }
- { key: 流转计划, 说明: 它未来到谁手里、何时显威——伏笔,续写不可见, aiContext: [planning, detection] }
+ # ── 升格卡改造(2026-07-17)新增:演变历程(完整历史层)+演变概括/前身/后继(现状层)。定义见 docs/2026-07-16-升格卡改造设计.md 第五节 ──
+ - key: 演变历程
+ 说明: "已发生的进化台阶明细,一条一台阶只追加不覆写;每条是对象 {章: 绝对章号(整数如 420,或跨章区间字符串如 \"420-423\"), 台阶: 进化到什么+靠什么事件的一句话, 周期: 登场/成长/高光/退场/结局}。按真实章号排序索引(不用运行时窗号)——完整历史层,续写不给(防上万字明细撑爆上下文),仅一致性检查与判重可见"
+ aiContext: [detection, extraction]
+ - key: 演变概括
+ 说明: "整条演变线的一句话梗概(如 '4级训练机出场→V代编队主力→300%同步率对决VI型领主')。现状层续写可见;与一句话摘要分工——摘要写当前态、本字段写来龙去脉"
+ aiContext: true
+ - key: 前身
+ 说明: "上一代/来源,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true
+ - key: 后继
+ 说明: "下一代/继承者,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true
diff --git a/meta/schemas/location.yaml b/meta/schemas/location.yaml
index a117329..0fc3b98 100644
--- a/meta/schemas/location.yaml
+++ b/meta/schemas/location.yaml
@@ -13,3 +13,16 @@ scope: entity
- { key: 戏剧功能, 说明: 什么类型的戏适合在此发生, aiContext: true }
- { key: 规则特例, 说明: 此地不同于世界常态的法则(禁空/禁法…)——一致性检查点, aiContext: true }
- { key: 与主线关联, aiContext: true }
+ # ── 升格卡改造(2026-07-17)新增:演变历程(完整历史层)+演变概括/前身/后继(现状层)。定义见 docs/2026-07-16-升格卡改造设计.md 第五节 ──
+ - key: 演变历程
+ 说明: "已发生的演变台阶明细(地点易主/兴废/规则变更),一条一台阶只追加不覆写;每条是对象 {章: 绝对章号(整数如 420,或跨章区间字符串如 \"420-423\"), 台阶: 演变到什么+靠什么事件的一句话, 周期: 登场/成长/高光/退场/结局}。按真实章号排序索引(不用运行时窗号)——完整历史层,续写不给(防上万字明细撑爆上下文),仅一致性检查与判重可见"
+ aiContext: [detection, extraction]
+ - key: 演变概括
+ 说明: "整条演变线的一句话梗概。现状层续写可见;与一句话摘要分工——摘要写当前态、本字段写来龙去脉"
+ aiContext: true
+ - key: 前身
+ 说明: "前身/旧称,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true
+ - key: 后继
+ 说明: "重建/更名后的后续,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true
diff --git a/meta/schemas/power_system.yaml b/meta/schemas/power_system.yaml
index d939780..87ecc35 100644
--- a/meta/schemas/power_system.yaml
+++ b/meta/schemas/power_system.yaml
@@ -17,3 +17,16 @@ scope: entity
aiContext: true
设计发现: 2026-07-09《焚忆》立项——字段隐含多体系假设,建议 SoT 标可选或给「不适用」取值(待回填专题-06/后端-04)
- { key: 戏剧作用, 说明: 这套排序如何制造冲突与目标, aiContext: [planning, detection] }
+ # ── 升格卡改造(2026-07-17)新增:演变历程(完整历史层)+演变概括/前身/后继(现状层)。定义见 docs/2026-07-16-升格卡改造设计.md 第五节 ──
+ - key: 演变历程
+ 说明: "已发生的进化台阶明细,一条一台阶只追加不覆写;每条是对象 {章: 绝对章号(整数如 420,或跨章区间字符串如 \"420-423\"), 台阶: 进化到什么+靠什么事件的一句话, 周期: 登场/成长/高光/退场/结局}。按真实章号排序索引(不用运行时窗号)——完整历史层,续写不给(防上万字明细撑爆上下文),仅一致性检查与判重可见"
+ aiContext: [detection, extraction]
+ - key: 演变概括
+ 说明: "整条演变线的一句话梗概(如 '4级训练机出场→V代编队主力→300%同步率对决VI型领主')。现状层续写可见;与一句话摘要分工——摘要写当前态、本字段写来龙去脉"
+ aiContext: true
+ - key: 前身
+ 说明: "上一代/来源,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true
+ - key: 后继
+ 说明: "下一代/继承者,指向同书另一张卡(卡号+名称);把散落碎卡串成一条进化链"
+ aiContext: true