"""定点补丁引擎:把进化输出的离散 edit 逐条应用到文本,逐条出状态报告。 借鉴 SkillOpt skill.py 的 apply 语义;守 P5:找不到锚点不静默乱改、不裸 except。 冻结区按全文坐标区间判定;append/退化追加插到最早冻结区之前(无则 EOF)。 """ from __future__ import annotations from loguru import logger APPENDIX_START = "" APPENDIX_END = "" APPENDIX_MAX_CHARS = 2000 # appendix 区软上限(守设计「长度上限+warning,不做去重」) MOMENTUM_START = "" MOMENTUM_END = "" MOMENTUM_MAX_CHARS = 2000 # momentum 区软上限(与 appendix 一致:超限 warning 不截断) MOMENTUM_HEADING = ( "## 动量指导(每轮重写,勿手改)" # replace_momentum 写入的固定标题行 ) def momentum_region_bounds(text: str) -> tuple[int, int] | None: """定位 momentum 受保护区的字符区间,并对损坏态显式报错(P5)。 momentum marker 由 replace_momentum 在 epoch 末反复重写,guidance 又来自 LLM 外部输入,因此 marker 可能出现损坏态。本函数是 momentum 路径的唯一边界判定入口, 把配对校验集中在一处: - START 与 END 各恰好出现一次且 START 在 END 之前 → 返回 (start_idx, end_idx), end_idx 指向 END marker 结束位置(即 content[start:end] 含完整两 marker)。 - 两 marker 都不出现 → 返回 None(合法的"无区"态,调用方据此新建)。 - 其余皆为损坏态(仅一个 marker、END 在 START 前、任一 marker 重复)→ raise ValueError,拒绝静默新建/跳过,要求人工修复。 参数: text: 待检测的文本(skill 全文)。 返回: (start_idx, end_idx) 表示区间,或 None 表示无 momentum 区。 异常: ValueError: momentum marker 损坏/不配对。 """ start_count = text.count(MOMENTUM_START) end_count = text.count(MOMENTUM_END) if start_count == 0 and end_count == 0: return None if start_count != 1 or end_count != 1: raise ValueError( f"momentum marker 损坏/不配对:MOMENTUM_START 出现 {start_count} 次、" f"MOMENTUM_END 出现 {end_count} 次(各须恰好 1 次),需人工修复" ) start_idx = text.index(MOMENTUM_START) end_idx = text.index(MOMENTUM_END) + len(MOMENTUM_END) if start_idx >= text.index(MOMENTUM_END): raise ValueError( "momentum marker 损坏/不配对:MOMENTUM_END 出现在 MOMENTUM_START 之前,需人工修复" ) return start_idx, end_idx def momentum_inner(content: str) -> str: """返回 momentum 受保护区的内层文本(去掉两 marker),无区返回空串。 与 _momentum_span(含 marker 的整段)的区别:本函数只取两 marker 之间的内层正文, 供 run_slow_momentum 的 prev_guidance 使用。prev_guidance 在 LLM 解析失败时会被 run_slow_momentum 原样返回、再喂给 replace_momentum;replace_momentum 禁止 guidance 含 marker 字面量,故 prev_guidance 必须是无 marker 的内层文本,否则一旦解析回退即 在 replace_momentum 抛 ValueError。 边界判定与配对校验统一委托 momentum_region_bounds:marker 损坏/不配对时由其 raise ValueError,本函数不把损坏态静默当作"无区"。 参数: content: skill 全文。 返回: momentum 区两 marker 之间的内层文本(已 strip);无区返回空串。 异常: ValueError: momentum marker 损坏/不配对。 """ bounds = momentum_region_bounds(content) if bounds is None: return "" start, end = bounds inner = content[start + len(MOMENTUM_START) : end - len(MOMENTUM_END)].strip() # 去掉 replace_momentum 写入的固定标题行,只回传纯指导文本,使其等价于上一轮 # 传给 replace_momentum 的 guidance(解析回退时原样回传不会引入重复标题)。 if inner.startswith(MOMENTUM_HEADING): inner = inner[len(MOMENTUM_HEADING) :].lstrip("\n") return inner.strip() def append_to_appendix(content: str, notes: list[str]) -> str: """把 LAPSE 提醒追加到文件尾的 appendix 受保护区;区不存在则创建。 护栏:appendix 区超过 APPENDIX_MAX_CHARS 时 logger.warning(不静默截断, 提示人工压缩;不做自动去重——YAGNI,见设计)。 参数: content: 原文。 notes: 待追加的提醒文本列表。 返回: 含 appendix 区的新文本。 """ if not notes: return content bullet = "\n".join(f"- {n.strip()}" for n in notes if n.strip()) if not bullet: return content if APPENDIX_START in content and APPENDIX_END in content: head, rest = content.split(APPENDIX_START, 1) inner, tail = rest.split(APPENDIX_END, 1) new_inner = f"{inner.rstrip()}\n{bullet}" out = f"{head}{APPENDIX_START}{new_inner}\n{APPENDIX_END}{tail}" else: new_inner = f"\n## 执行提醒(自动累积,勿手改)\n{bullet}" out = f"{content.rstrip()}\n\n{APPENDIX_START}{new_inner}\n{APPENDIX_END}\n" if len(new_inner) > APPENDIX_MAX_CHARS: logger.warning( "appendix 区长度 {} 超过上限 {},建议人工压缩", len(new_inner), APPENDIX_MAX_CHARS, ) return out def appendix_region_bounds(text: str) -> tuple[int, int] | None: """定位 appendix 受保护区的字符区间,对损坏态显式报错(P5,对称 momentum_region_bounds)。 appendix marker 由 append_to_appendix 维护、consolidation 回写,可能出现损坏态。 本函数是 appendix 路径的唯一边界判定入口,把配对校验集中一处: - START 与 END 各恰好一次且 START 在 END 之前 → 返回 (start_idx, end_idx), end_idx 指向 END marker 结束位置(content[start:end] 含完整两 marker)。 - 两 marker 都不出现 → 返回 None(合法的「无区」态)。 - 其余(仅一个 marker、END 在 START 前、任一 marker 重复)→ raise ValueError, 拒绝静默按字符串切片处理而误拼/吞掉区外正文。 参数: text: 待检测文本(skill 全文)。 返回: (start_idx, end_idx) 表示区间,或 None 表示无 appendix 区。 异常: ValueError: appendix marker 损坏/不配对。 """ start_count = text.count(APPENDIX_START) end_count = text.count(APPENDIX_END) if start_count == 0 and end_count == 0: return None if start_count != 1 or end_count != 1: raise ValueError( f"appendix marker 损坏/不配对:APPENDIX_START 出现 {start_count} 次、" f"APPENDIX_END 出现 {end_count} 次(各须恰好 1 次),需人工修复" ) start_idx = text.index(APPENDIX_START) end_idx = text.index(APPENDIX_END) + len(APPENDIX_END) if start_idx >= text.index(APPENDIX_END): raise ValueError( "appendix marker 损坏/不配对:APPENDIX_END 出现在 APPENDIX_START 之前,需人工修复" ) return start_idx, end_idx def extract_appendix_notes(content: str) -> list[str]: """从 appendix 受保护区解析出 bullet 提醒列表;无区返回空列表。 功能: 取 appendix 区内每行以 "- " 起头的文本为一条 note(去 "- " 前缀与首尾空白), 区内标题行(## 执行提醒…)不计。供 consolidation 读取现有 notes。 参数: content: skill 全文。 返回: note 字符串列表;无 appendix 区返回 []。 异常: ValueError: appendix marker 损坏/不配对(经 appendix_region_bounds,不静默切片)。 关键实现细节: 边界判定统一委托 appendix_region_bounds,只取两 marker 之间内层正文逐行解析。 """ bounds = appendix_region_bounds(content) if bounds is None: return [] start, end = bounds inner = content[start + len(APPENDIX_START) : end - len(APPENDIX_END)] notes: list[str] = [] for line in inner.splitlines(): stripped = line.strip() if stripped.startswith("- "): note = stripped[2:].strip() if note: notes.append(note) return notes def replace_appendix_notes(content: str, notes: list[str]) -> str: """用 notes 整体替换 appendix 区内容;notes 为空则删除整个 appendix 区。 功能: consolidation 回写压缩后 notes 的替换语义(区别于 append_to_appendix 累积): 区存在则整体覆盖区内 bullet;notes 空则连 marker 一并删除、保留区外正文; 区不存在且 notes 非空则按 append_to_appendix 格式新建。 参数: content: 原文(可能含 appendix 区)。 notes: 压缩后的提醒列表;空列表表示删区。 返回: 替换后的全文。 异常: ValueError: appendix marker 损坏/不配对(经 appendix_region_bounds)。 关键实现细节: 边界经 appendix_region_bounds 显式校验,按 (start,end) 切出 head/tail 拼接, 不做两次独立 split(避免损坏态误拼/吞掉区外正文)。 """ bounds = appendix_region_bounds(content) if bounds is not None: start, end = bounds head = content[:start] tail = content[end:] if not notes: return head.rstrip() + ("\n" + tail.lstrip("\n") if tail.strip() else "\n") bullet = "\n".join(f"- {n.strip()}" for n in notes if n.strip()) new_inner = f"\n## 执行提醒(自动累积,勿手改)\n{bullet}" return f"{head}{APPENDIX_START}{new_inner}\n{APPENDIX_END}{tail}" if not notes: return content return append_to_appendix(content, notes) def replace_momentum(content: str, guidance: str) -> str: """把「动量指导」整体写入文件尾的 momentum 受保护区;区不存在则创建。 与 append_to_appendix 的累积语义不同,momentum 是**替换**语义:慢更新周期每 epoch 末整体重写一段动量指导,旧指导被完全覆盖(不保留历史)。momentum 区与 appendix 区独立共存——本函数只触碰 momentum marker,不破坏已有 appendix 区。 护栏:momentum 区超过 MOMENTUM_MAX_CHARS 时 logger.warning(不静默截断,与 appendix 对齐)。 关键实现细节: - 替换非追加:区已存在时用 guidance 整体覆盖 marker 内 inner,旧动量不残留。 - 创建位置在文件尾(append_to_appendix 同样在文件尾,但两区 marker 不同, split 按各自 marker 定位,互不干扰)。 空 guidance 决策:与 appendix 的累积语义不同,momentum 是「每轮整体重写」,空 guidance 表示「本轮无动量指导」,属合法语义——照常写入(区内仅留标题,旧动量被清空), 而非返回原文保留旧动量。 参数: content: 原文(可能已含 appendix 区)。 guidance: 本轮动量指导全文(整体覆盖旧动量)。 返回: 含 momentum 区的新文本。 异常: ValueError: guidance 含 momentum marker 字面量(外部输入注入),或原文 momentum marker 损坏/不配对。 """ if MOMENTUM_START in guidance or MOMENTUM_END in guidance: raise ValueError( "guidance 不得包含 momentum marker 字面量" f"({MOMENTUM_START} / {MOMENTUM_END}),否则会破坏 marker 配对" ) bounds = momentum_region_bounds(content) new_inner = f"\n## 动量指导(每轮重写,勿手改)\n{guidance.strip()}" if bounds is not None: start_idx, end_idx = bounds head = content[:start_idx] tail = content[end_idx:] out = f"{head}{MOMENTUM_START}{new_inner}\n{MOMENTUM_END}{tail}" else: out = f"{content.rstrip()}\n\n{MOMENTUM_START}{new_inner}\n{MOMENTUM_END}\n" if len(new_inner) > MOMENTUM_MAX_CHARS: logger.warning( "momentum 区长度 {} 超过上限 {},建议人工压缩", len(new_inner), MOMENTUM_MAX_CHARS, ) return out def _protected_ranges(content: str, spans: list[str]) -> list[tuple[int, int]]: """把冻结文本块映射成 content 中的 [start, end) 坐标区间。""" ranges: list[tuple[int, int]] = [] for span in spans: idx = content.find(span) if idx != -1: ranges.append((idx, idx + len(span))) return ranges def _in_ranges(pos: int, ranges: list[tuple[int, int]]) -> bool: """判断位置 pos 是否落在任意冻结区间内。""" return any(start <= pos < end for start, end in ranges) def _append_at(content: str, ranges: list[tuple[int, int]]) -> int: """append/退化追加落点:最早一个 start>0 的冻结区之前;无则文末(头部 frontmatter 不计)。""" starts = [start for start, _ in ranges if start > 0] return min(starts) if starts else len(content) def _insert_at(content: str, at: int, payload: str) -> str: """在 at 位置插入 payload,自动补换行保持段落格式。""" head, tail = content[:at].rstrip(), content[at:].lstrip("\n") if tail: return head + "\n\n" + payload + "\n\n" + tail return head + "\n\n" + payload + "\n" def _do_append( content: str, payload: str, ranges: list[tuple[int, int]] ) -> tuple[str, str]: """执行 append 操作,返回更新后内容与状态字符串。""" return _insert_at(content, _append_at(content, ranges), payload), "applied_append" def _do_insert_after( content: str, target: str, payload: str, ranges: list[tuple[int, int]] ) -> tuple[str, str]: """执行 insert_after 操作,处理退化追加与冻结区跳过。""" pos = content.find(target) if target else -1 if pos == -1: logger.warning("insert_after 锚点缺失,退化为追加 target={}", target[:80]) return ( _insert_at(content, _append_at(content, ranges), payload), "applied_insert_after_fallback", ) if _in_ranges(pos, ranges): logger.warning("insert_after 目标在冻结区,跳过 target={}", target[:80]) return content, "skipped_protected" at = pos + len(target) nl = content.find("\n", at) at = nl + 1 if nl != -1 else len(content) return content[:at] + payload + "\n" + content[at:], "applied_insert_after" def _do_replace_delete( op: str, content: str, target: str, payload: str, ranges: list[tuple[int, int]], ) -> tuple[str, str]: """执行 replace 或 delete 操作,返回更新后内容与状态字符串。""" if not target: return content, "skipped_missing_target" pos = content.find(target) if pos == -1: logger.warning("{} 锚点缺失,跳过 target={}", op, target[:80]) return content, "skipped_target_not_found" if _in_ranges(pos, ranges): logger.warning("{} 目标在冻结区,跳过 target={}", op, target[:80]) return content, "skipped_protected" new_content = content.replace(target, payload if op == "replace" else "", 1) return new_content, "applied_" + op def _apply_one( content: str, edit: dict, ranges: list[tuple[int, int]] ) -> tuple[str, dict]: """应用单条 edit,返回 (更新后内容, 状态报告)。""" if not isinstance(edit, dict): return content, { "op": "", "target": "", "content_preview": "", "status": "error", "error": f"edit 非 dict: {type(edit).__name__}", } op = str(edit.get("op", "")) target = str(edit.get("target", "") or "") payload = str(edit.get("content", "") or "").strip() report = { "op": op, "target": target[:200], "content_preview": payload[:200], "status": "unknown", } if op == "append": content, report["status"] = _do_append(content, payload, ranges) return content, report if op == "insert_after": content, report["status"] = _do_insert_after(content, target, payload, ranges) return content, report if op in ("replace", "delete"): content, report["status"] = _do_replace_delete( op, content, target, payload, ranges ) return content, report logger.warning("未知 op,跳过: {}", op) report["status"] = "skipped_unknown_op" return content, report def apply_patch_with_report( content: str, edits: list[dict], protected_spans: list[str] | None = None, ) -> tuple[str, list[dict]]: """顺序应用 edit 列表,返回 (新内容, 逐条状态报告)。 参数: content: 原始文本。 edits: 每条 {op, target, content}。 protected_spans: 冻结文本块列表;目标落入其坐标区间即跳过,append 插到其前。 返回: (应用后文本, reports);reports 每条含 op/target/content_preview/status/index。 """ spans = protected_spans or [] reports: list[dict] = [] for i, edit in enumerate(edits, 1): try: ranges = _protected_ranges(content, spans) content, report = _apply_one(content, edit, ranges) except (KeyError, TypeError, ValueError, AttributeError) as exc: report = { "op": "", "target": "", "content_preview": "", "status": "error", "error": str(exc), } logger.exception("补丁应用异常 index={}", i) report["index"] = i reports.append(report) return content, reports