#!/usr/bin/env python3 """Step 5 Gate pre-flight for `type=reference` absorbs —— 冻结复现 app.py:_verify_and_apply。 背景(learn_queue #1319,2026-10-11):reference 型的 Step5 指纹门存在结构性病理, 本脚本在收尾前离线复现判定逻辑,PASS 才提交,避免白烧一个 attempt。 病理(两段,均实测): 1) `fingerprint = result_summary[:30]`,而 `result_summary = result_text[:500]`(Step 4 报告正文) → 首 30 字恒为报告表头 `# 学习完成报告\n\n## 学习内容\n- 标题: …`,**结构上永不出现**在 memory.json / SKILL.md 中 → 主指纹路径恒失效。 2) ~~有人以为 fallback 会去查目标 SKILL.md——不会~~ **(已过时,2026-10-12 #1330 修)**: 旧版 app.py 用**带前缀的原始 target**(`new:xxx`)去 rglob,`*/new:xxx/SKILL.md` 永不匹配; 但 app.py L3069-3075 已加去前缀修复(`tgt_resolved` 剥 `new:`/`update:` 再 rglob)⇒ **目标 SKILL.md 现在是可达 grounding 通道**(`raw=0` 与 `clean>=1` 同时成立是正常的, 不是错误)。本脚本旧版只查 memory.json,会误报 FAIL——已按 app.py 现状对齐。 ⇒ **reference 的 grounding 通道 = 两处任一命中即 PASS**: ①目标 SKILL.md(去前缀后路径)②memory.json 短语回退(L3095–3108)。 取 `outline = summary[:500] + "\n\n" + plan[:500]`,再取 `outline[i:i+20]`(i=0/20/40/60), strip 后 ≥10 字且为 **磁盘原始文本的子串** → 至少 1 段命中即 PASS。 用法: python3 gate_preflight_reference.py [--target new:xxx] 修复手法(2026-10-12 #1330 实测有效):让 `.summary.md` **首行**取**已落盘文本**的 **逐字句子**——优先目标 SKILL.md 的 H1 / 章节标题(≥20 字、首 20 字内不含换行),或 memory.json 目标条目。`outline[0:20]` 命中即 PASS。禁止伪造短语:锚必须真实存在于磁盘文本中。 """ import argparse import json import sys from pathlib import Path SKILLS = Path("/data/data/com.termux/files/home/.hermes/skills") MEM_DEFAULT = Path("/data/data/com.termux/files/home/.hermes/memory.json") def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("content_file") ap.add_argument("--target", default="", help="learn_target(含 new:/update: 前缀,用于 skill 路径作证)") ap.add_argument("--memory", default=str(MEM_DEFAULT)) args = ap.parse_args() summary = Path(args.content_file + ".summary.md") plan = Path(args.content_file + ".plan.md") result = Path(args.content_file + ".result.md") mem = Path(args.memory) if not (summary.exists() and plan.exists()): print("FAIL: summary/plan missing -> %s" % summary) return 1 if not mem.exists(): print("FAIL: memory store missing -> %s" % mem) return 1 mem_text = mem.read_text(encoding="utf-8") outline = summary.read_text(encoding="utf-8")[:500] + "\n\n" + plan.read_text(encoding="utf-8")[:500] if result.exists(): result_summary = result.read_text(encoding="utf-8")[:1200][:500] else: result_summary = "# 学习完成报告\n\n## 学习内容\n- 标题: <未写报告时的模拟表头>" fingerprint = (result_summary or outline).strip()[:30] print("fingerprint = %r (表头 -> 主路径恒不可达,走短语回退)" % fingerprint) skill_files = [] if args.target: raw = list(SKILLS.rglob(f"*/{args.target}/SKILL.md")) or list(SKILLS.rglob(f"{args.target}/SKILL.md")) clean = args.target.replace("new:", "").replace("update:", "") skill_files = list(SKILLS.rglob(f"*/{clean}/SKILL.md")) or list(SKILLS.rglob(f"{clean}/SKILL.md")) print("skill_files(raw target) = %d <- 预期 0(app.py 已去前缀;raw 计数仅作证)" % len(raw)) print("skill_files(clean name) = %d <- 须 >=1,否则 Step5 会先报 SKILL_MISSING" % len(skill_files)) windows = [(i, outline[i:i + 20]) for i in range(0, 80, 20)] def _scan(label: str, text: str) -> bool: if fingerprint and fingerprint in text: print("hit: fingerprint in %s" % label) return True for i, ph in windows: hit = len(ph.strip()) >= 10 and ph.strip() in text print(" %s phrase[%d:%d] hit=%s :: %r" % (label, i, i + 20, hit, ph)) if hit: return True return False verified = False # 通道①:目标 SKILL.md(app.py L3069-3095;去前缀路径,#1323 修复后可达) for sf in skill_files: try: if _scan("skill:%s" % sf.parent.name, sf.read_text(encoding="utf-8")): verified = True break except OSError as exc: print(" (skip unreadable %s: %s)" % (sf, exc)) # 通道②:memory.json 短语回退(app.py L3095-3108) if not verified: verified = _scan("memory.json", mem_text) print("\nStep5 reference-grounded = %s" % ("PASS" if verified else "FAIL")) if not verified: print("修复:把 .summary.md 首行换成**已落盘文本**的逐字句子" "(目标 SKILL.md 的 H1/章节标题,或 memory.json 目标条目;>=20 字,首 20 字内无换行)") return 0 if verified else 2 if __name__ == "__main__": sys.exit(main())