gate_preflight_reference.py
5.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/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 <content_file> [--target new:xxx]
修复手法(2026-10-12 #1330 实测有效):让 `<content_file>.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())