dsh-distillation-director 4.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,289 @@
1
+ # -*- coding: utf-8 -*-
2
+ """盲测闸 词表命中模拟器 v1(零 LLM 本地 py · 初判用)
3
+
4
+ 目标:description 触发词表 对 用户路由 prompt 做字面命中初判,筛掉“机械可判”路由;
5
+ 无字面锚/弱命中/邻族抢单 的 prompt 显式标记 needs_llm=True,交 LLM judge 结构化表
6
+ (13x{命中/目标/邻族风险},单发小批)——对应三闸方案里盲测的机器初判层。
7
+
8
+ 验收基准:反脆弱册 blind-test-report.md 的 12 条路由盲测(AD-1..6 / OC-1..6),
9
+ judge 结论=9 干脆 + 3 边界存疑(AD-3/OC-4/OC-5)+ 2 判停(AD-6/OC-6)。
10
+
11
+ 用法:
12
+ python blindtest_lexicon_mock_v1.py
13
+ python blindtest_lexicon_mock_v1.py --prompts <json> (覆盖内嵌 fixture)
14
+ 产物:blindtest-mock-v1.json + 盲测词表模拟器v1-试点报告.md(同目录)
15
+ """
16
+ import os
17
+ import re
18
+ import sys
19
+ import json
20
+
21
+ HERE = os.path.dirname(os.path.abspath(__file__))
22
+ ROOT = os.path.dirname(os.path.dirname(HERE))
23
+ TT = os.path.join(ROOT, "投资蒸馏", "塔勒布五部曲")
24
+ BOOKS = {
25
+ "反脆弱": os.path.join(TT, "反脆弱"),
26
+ "股票大作手回忆录": os.path.join(ROOT, "投资蒸馏", "股票大作手回忆录"),
27
+ "寻找鱼多的池塘": os.path.join(ROOT, "投资蒸馏", "寻找鱼多的池塘"),
28
+ }
29
+ SKILLS = {
30
+ "antifragile-designer": os.path.join(BOOKS["反脆弱"], "skills", "antifragile-designer", "SKILL.md"),
31
+ "optionality-convexity": os.path.join(BOOKS["反脆弱"], "skills", "optionality-convexity", "SKILL.md"),
32
+ "trader-discipline": os.path.join(BOOKS["股票大作手回忆录"], "skills", "trader-discipline", "SKILL.md"),
33
+ "market-structure-and-manipulation": os.path.join(BOOKS["股票大作手回忆录"], "skills", "market-structure-and-manipulation", "SKILL.md"),
34
+ "multi-asset-rotation": os.path.join(BOOKS["寻找鱼多的池塘"], "skills", "multi-asset-rotation", "SKILL.md"),
35
+ "convertible-pool-rotation": os.path.join(BOOKS["寻找鱼多的池塘"], "skills", "convertible-pool-rotation", "SKILL.md"),
36
+ "fund-selector": os.path.join(BOOKS["寻找鱼多的池塘"], "skills", "fund-selector", "SKILL.md"),
37
+ "investing-mindset": os.path.join(BOOKS["寻找鱼多的池塘"], "skills", "investing-mindset", "SKILL.md"),
38
+ }
39
+
40
+ # 判停红线词(时点预测等;盲测 AD-6/OC-6)
41
+ REDLINE = ["什么时候会", "何时发生", "何时会", "会不会崩", "会不会涨", "会崩", "会跌", "预测", "明天", "涨跌", "提前跑", "逃顶", "抄底", "会涨吗"] # v1.1:移除裸词“什么时候/何时”(会把“什么时候能拆线”误判为时点预测),见规范对比实验 sge-3 案例
42
+
43
+ FIXTURES = [
44
+ {"id": "AD-1", "target": "antifragile-designer", "judge_class": "clear", "prompt": "家庭财务就怕是裁员这种打击,怎么让我的职业收入不怕黑天鹅?"},
45
+ {"id": "AD-2", "target": "antifragile-designer", "judge_class": "clear", "prompt": "我现在零冗余、满负荷、效率极高,但总觉得哪里不对,是不是越优化越脆?"},
46
+ {"id": "AD-3", "target": "antifragile-designer", "judge_class": "boundary", "prompt": "我吃一堆保健品、每年全套体检来防病,这种过度保护是不是反而有害?"},
47
+ {"id": "AD-4", "target": "antifragile-designer", "judge_class": "clear", "prompt": "这本书刚出版就火了,值不值得买来学?新公司值不值得追?"},
48
+ {"id": "AD-5", "target": "antifragile-designer", "judge_class": "clear", "prompt": "我严格按计划、全面求稳,反而越来越脆,为什么越优化越脆?"},
49
+ {"id": "AD-6", "target": "antifragile-designer", "judge_class": "decline", "prompt": "股市房价什么时候会崩,我想提前跑。"},
50
+ {"id": "OC-1", "target": "optionality-convexity", "judge_class": "clear", "prompt": "10 个创业公司死 9 个,成了赚 50 倍,要不要拿全部积蓄投?"},
51
+ {"id": "OC-2", "target": "optionality-convexity", "judge_class": "clear", "prompt": "我胜率不到 40%,是不是方法不行该放弃?"},
52
+ {"id": "OC-3", "target": "optionality-convexity", "judge_class": "clear", "prompt": "小仓位试错和赌博有什么区别?"},
53
+ {"id": "OC-4", "target": "optionality-convexity", "judge_class": "boundary", "prompt": "这只基金十年年年正收益,历史数据这么好,能不能重仓?"},
54
+ {"id": "OC-5", "target": "optionality-convexity", "judge_class": "boundary", "prompt": "波动太大我睡不着,要不要降仓位、降波动?"},
55
+ {"id": "OC-6", "target": "optionality-convexity", "judge_class": "decline", "prompt": "预测比特币明天涨跌,涨了我就全仓跟。"},
56
+ ]
57
+
58
+
59
+ def read_description(path):
60
+ with open(path, encoding="utf-8") as f:
61
+ lines = f.read().splitlines()
62
+ if not lines or lines[0].strip() != "---":
63
+ return ""
64
+ desc = []
65
+ active = False
66
+ for ln in lines[1:]:
67
+ if ln.strip() == "---":
68
+ break
69
+ if ln.startswith("description:"):
70
+ active = True
71
+ desc.append(ln[len("description:"):].strip())
72
+ elif active and ln.startswith((" ", "\t")):
73
+ desc.append(ln.strip())
74
+ text = " ".join(desc)
75
+ if text.startswith("") and text.endswith("") and len(text) >= 2:
76
+ text = text[1:-1]
77
+ return text
78
+
79
+
80
+ def extract_lexicon(desc):
81
+ """v1.2(对比实验 v3.1 优化):词源=整段 description(触发词段+何时用/何时不用+引号话术示例),
82
+ 去括号后按标点/引号/斜杠/空格切词;2..16 字为 token(超长整句跳过防噪声)。"""
83
+ seg = re.sub(r"[((][^))]*[))]", "", desc)
84
+ toks = re.split(r"['\"、,,;;/||\s::。!?!?—…]+", seg)
85
+ out = []
86
+ seen = set()
87
+ for t in toks:
88
+ t = t.strip("  “”「」‘’::。,、!?…")
89
+ if 2 <= len(t) <= 16 and t not in seen and not re.fullmatch(r"[0-9a-zA-Z.]+", t):
90
+ seen.add(t)
91
+ out.append(t)
92
+ return out[:200]
93
+
94
+
95
+ def build_lexicons():
96
+ lex = {}
97
+ for slug, path in SKILLS.items():
98
+ desc = read_description(path)
99
+ lex[slug] = extract_lexicon(desc)
100
+ return lex
101
+
102
+
103
+ # ---- 通用化(A-37 修复 · 2026-09-13)------------------------------------------------
104
+ # 病根:docstring 写着 `--prompts <json>`,**代码里从未实现**(`prompts = FIXTURES` 无条件),
105
+ # 于是「换一本书跑盲测」会**静默地**拿内嵌的反脆弱册 fixture 出报告 —— 产出的是**另一本书的证据**。
106
+ # 修法:① 真实现 --prompts;② 词表语料必须由 --skills-root 显式给出并与 prompts 同批(否则拒跑);
107
+ # ③ 输出目录 --out(不再写死在工具目录,避免污染脚本同步/打包新鲜度闸);④ 语料来源写进产物。
108
+ def parse_args():
109
+ a = sys.argv[1:]
110
+ out = {}
111
+ for k in ('--prompts', '--skills-root', '--out'):
112
+ if k in a:
113
+ i = a.index(k) + 1
114
+ if i >= len(a):
115
+ sys.exit('🔴 %s 缺少取值' % k)
116
+ out[k[2:].replace('-', '_')] = a[i]
117
+ return out
118
+
119
+
120
+ def build_lexicons_from_root(root):
121
+ lex = {}
122
+ if not os.path.isdir(root):
123
+ sys.exit('🔴 --skills-root 不是目录:%s' % root)
124
+ for slug in sorted(os.listdir(root)):
125
+ p = os.path.join(root, slug, 'SKILL.md')
126
+ if os.path.isfile(p):
127
+ lex[slug] = extract_lexicon(read_description(p))
128
+ return lex
129
+
130
+
131
+ def load_prompts(path):
132
+ if not os.path.isfile(path):
133
+ sys.exit('🔴 --prompts 文件不存在:%s' % path)
134
+ d = json.load(open(path, encoding='utf-8'))
135
+ if isinstance(d, dict):
136
+ for k in ('probes', 'prompts', 'rows', 'items', 'cases'):
137
+ if isinstance(d.get(k), list):
138
+ d = d[k]
139
+ break
140
+ if not isinstance(d, list) or not d:
141
+ sys.exit('🔴 --prompts 无可用条目(%s):期望顶级数组或含 probes/prompts/rows 的对象' % path)
142
+ out = []
143
+ for it in d:
144
+ if not isinstance(it, dict) or not str(it.get('prompt', '')).strip():
145
+ sys.exit('🔴 --prompts 条目缺 prompt 字段:%r' % (it,))
146
+ it = dict(it)
147
+ it.setdefault('id', '?')
148
+ # 目标件别名(A-37):本库 test-prompts 用 expect_top1/owner_skill,投资册用 target
149
+ if not it.get('target'):
150
+ it['target'] = it.get('expect_top1') or it.get('owner_skill')
151
+ it.setdefault('judge_class', 'unknown')
152
+ out.append(it)
153
+ return out
154
+
155
+
156
+ def judge_prompt(prompt, lexicons):
157
+ red = [w for w in REDLINE if w in prompt]
158
+ scores = {}
159
+ hits = {}
160
+ for slug, toks in lexicons.items():
161
+ got = [t for t in toks if t in prompt]
162
+ hits[slug] = got
163
+ scores[slug] = len(got)
164
+ best = max(scores.values()) if scores else 0
165
+ tops = [s for s, v in scores.items() if v > 0]
166
+ if best > 0:
167
+ second = sorted(scores.values(), reverse=True)[1] if len(scores) > 1 else 0
168
+ rivals = [s for s in tops if s != (tops[0] if tops else None) and scores[s] >= best * 0.6] if tops else []
169
+ else:
170
+ rivals = []
171
+ tie = best > 0 and any(s != (tops[0] if tops else None) and scores[s] == best for s in tops)
172
+ if red:
173
+ verdict = "redline-decline"
174
+ needs_llm = False # 判停本身机器即可给(红灯),语义复核可选
175
+ elif best == 0:
176
+ verdict = "no-lexicon-hit"
177
+ needs_llm = True
178
+ elif tie:
179
+ verdict = "weak-or-ambiguous"
180
+ needs_llm = True
181
+ else:
182
+ verdict = "mechanical-clear"
183
+ needs_llm = False
184
+ return {
185
+ "redline_hits": red,
186
+ "scores": {k: v for k, v in sorted(scores.items(), key=lambda kv: -kv[1]) if v > 0},
187
+ "best": best, "top": tops[0] if tops else None,
188
+ "rivals": rivals, "verdict": verdict, "needs_llm": needs_llm,
189
+ }
190
+
191
+
192
+ def main():
193
+ args = parse_args()
194
+ if bool(args.get('prompts')) != bool(args.get('skills_root')):
195
+ sys.exit('🔴 --prompts 与 --skills-root 必须成对给出(A-37:只给 prompts ⇒ 词表仍取自**内嵌的另'
196
+ '一本书**语料,会静默产出跨书假证据)')
197
+ if args.get('prompts'):
198
+ prompts = load_prompts(args['prompts'])
199
+ psrc = args['prompts']
200
+ lexicons = build_lexicons_from_root(args['skills_root'])
201
+ lsrc = args['skills_root']
202
+ if not lexicons:
203
+ sys.exit('🔴 --skills-root 下未找到任何 <slug>/SKILL.md:%s' % lsrc)
204
+ targets = {p['target'] for p in prompts if p.get('target')}
205
+ miss = sorted(targets - set(lexicons))
206
+ if miss:
207
+ sys.exit('🔴 prompts 的 target 不在词表语料内:%s ⇒ **语料不匹配**(A-37),拒绝产出'
208
+ '(词表语料:%s)' % (miss, sorted(lexicons)[:8]))
209
+ else:
210
+ prompts, psrc = FIXTURES, '内嵌 fixture(反脆弱册 AD/OC 12 题)'
211
+ lexicons, lsrc = build_lexicons(), '内嵌 BOOKS(投资蒸馏三册)'
212
+ outdir = args.get('out') or HERE
213
+ if not os.path.isdir(outdir):
214
+ sys.exit('🔴 --out 不是目录:%s' % outdir)
215
+ print('# 语料来源:prompts=%s | 词表=%s | 共 %d 题' % (psrc, lsrc, len(prompts)))
216
+ print('# ⚠ 红线词表口径:内嵌 REDLINE=**投资域(时点预测)**专用;'
217
+ + ('本次为**外域语料**,判停红线须另备域内词表(本域用 tools/blindtest_generic.py),'
218
+ '本行的 redline 计数**不得**当作判停结论。' if args.get('prompts') else '内嵌 fixture 同域,可用。'))
219
+ rows = []
220
+ for fx in prompts:
221
+ r = judge_prompt(fx["prompt"], lexicons)
222
+ r.update({"id": fx["id"], "target": fx["target"], "judge_class": fx["judge_class"],
223
+ "prompt": fx["prompt"]})
224
+ rows.append(r)
225
+ stats = {
226
+ "total": len(rows),
227
+ "judge_clear": sum(1 for r in rows if r["judge_class"] == "clear"),
228
+ "judge_boundary": sum(1 for r in rows if r["judge_class"] == "boundary"),
229
+ "judge_decline": sum(1 for r in rows if r["judge_class"] == "decline"),
230
+ "machine_mechanical_clear": sum(1 for r in rows if r["verdict"] == "mechanical-clear"),
231
+ "machine_needs_llm": sum(1 for r in rows if r["needs_llm"]),
232
+ "machine_redline": sum(1 for r in rows if r["verdict"] == "redline-decline"),
233
+ "top_is_target": sum(1 for r in rows if r["top"] == r["target"] and r["top"] is not None),
234
+ }
235
+ out = {"schema": "blindtest-lexicon-mock-v1", "prompt_source": psrc, "lexicon_root": lsrc,
236
+ "lexicon_sizes": {k: len(v) for k, v in lexicons.items()},
237
+ "rows": rows, "stats": stats}
238
+ with open(os.path.join(outdir, "blindtest-mock-v1.json"), "w", encoding="utf-8") as f:
239
+ json.dump(out, f, ensure_ascii=False, indent=2)
240
+ # ---- md ----
241
+ L = []
242
+ A = L.append
243
+ A("# 盲测闸 词表命中模拟器 v1 —— 机器初判(%d 题)" % len(rows))
244
+ A("")
245
+ A("> 语料来源:prompts=`%s` | 词表=`%s`" % (psrc, lsrc))
246
+ A("> 角色:盲测 LLM judge 的机器初判层(词表命中,零 LLM)")
247
+ A("")
248
+ A("## 一、语料与词表")
249
+ A("")
250
+ A("%d 个候选 skill 的 description 词表(触发词段自动提取);词表规模:" % len(lexicons)
251
+ + ",".join("%s=%d" % (k, len(v)) for k, v in sorted(lexicons.items())))
252
+ A("")
253
+ A("## 二、%d 条初判结果" % len(rows))
254
+ A("")
255
+ A("| id | judge 类 | 初判 verdict | needs_llm | top1(命中数) | 邻族风险 |")
256
+ A("|---|---|---|---|---|---|")
257
+ for r in rows:
258
+ top_txt = (r["top"] + "(" + str(r["scores"].get(r["top"], 0)) + ")") if r["top"] else "—"
259
+ rival_txt = ",".join(r["rivals"]) if r["rivals"] else "—"
260
+ A("| %s | %s | %s | %s | %s | %s |" % (r["id"], r["judge_class"], r["verdict"], r["needs_llm"], top_txt, rival_txt))
261
+ A("")
262
+ A("## 三、统计")
263
+ A("")
264
+ A("| 项 | 值 |")
265
+ A("|---|---|")
266
+ _nd = sum(1 for r in rows if r["judge_class"] == "decline")
267
+ A("| 机器机械可判(mechanical-clear) | %d/%d |" % (stats["machine_mechanical_clear"], len(rows)))
268
+ A("| 机器判 需 LLM | %d/%d(无词表锚/弱命中/邻族竞合) |" % (stats["machine_needs_llm"], len(rows)))
269
+ A("| 判停红线机器命中 | %d/%d(judge_class=decline 的题) |" % (stats["machine_redline"], _nd))
270
+ A("| 词表 top1 = 期望目标技能 | %d/%d |" % (stats["top_is_target"], len(rows)))
271
+ A("")
272
+ A("## 四、结论(v1 数据)")
273
+ A("")
274
+ A("- 机器初判层的价值=先筛:机械可判的路由(verdict=mechanical-clear 且 top1=目标)可免 LLM;其余(无锚/弱锚/邻族抢单)才进 LLM 语义判定,输入从 12 条全量降到 needs_llm 子集。")
275
+ A("- 与 judge 盲测报告的印证与差异:AD-3/OC-5 无(弱)字面锚 → needs_llm(与报告边界结论一致);OC-4 现词表已含 重仓/回测/年年正收益/历史数据(报告改进建议 1 已落地)→ 机器机械命中;AD-4「值不值得追」与触发词「值不值得追新」差一字漏字面命中(词元化留 v2)。")
276
+ A("- 已知缺口(v2 候选):词表只取 description 触发词段,未用 何时用/何时不用 语义区;同义改写(保健品/体检 vs 过度保护)命中不了;判停红线目前只覆盖时点类。")
277
+ A("")
278
+ md = "\n".join(L)
279
+ md_path = os.path.join(outdir, "盲测词表模拟器v1-试点报告.md")
280
+ with open(md_path, "w", encoding="utf-8") as f:
281
+ f.write(md)
282
+ print(json.dumps(stats, ensure_ascii=False))
283
+ for r in rows:
284
+ print("%s %-7s %-20s llm=%-5s top=%s" % (r["id"], r["judge_class"], r["verdict"], r["needs_llm"], (r["top"] or "—")[:26]))
285
+ print("md ->", md_path)
286
+
287
+
288
+ if __name__ == "__main__":
289
+ main()
@@ -0,0 +1,199 @@
1
+ # -*- coding: utf-8 -*-
2
+ """试点 B · 防线3〔操作层〕冒充扫描 v1(零 LLM 本地 py)
3
+
4
+ 判定对象:SKILL.md 正文中 R 段之外(I/A1/A2/E/B/数据指引)把压缩转述或整句原文
5
+ 用全角/ASCII 引号包起来并紧邻〔页锚〕的行——这类行会冒充作者原话(fidelity 报告的
6
+ O1/O3 判据)。启发式(仿 judge 报告口径):
7
+ - 规则1 引号数数:逐行统计全角引号字符数与引号内运行段;
8
+ - 规则2 整句嫌疑:引号内运行段 长度>=20 字;或 长度>=12 且含句读(,,、。;;)
9
+ 且引号后 ≤32 字窗口内出现〔…(p页)〕页锚(v2 从 22 放宽,覆盖 judge ▲旁注样本);
10
+ - 规则3 转述豁免(v2 口径=judge ▲旁注口径):仅 引号前窗口(14 字)或引号内容 出现 原文意/转述/压缩/非逐字/词级/短语级 等标记才豁免;后置标签(如 洁食句后挂“ca15 压缩转述”)不再豁免,按 judge 记“未标注”处理为候选;
11
+ - 范围排除:R 段(作者原话合法引文)、E-示例/最小交付、块引用(>)与 frontmatter。
12
+
13
+ 验收(试点 B 召回率):
14
+ - rse 历史修复清单 3 处(fidelity-check-rse-v2.md 修复清单①-③)应全部命中;
15
+ - trv 独立报告 O3 明示 25+ 字长引号紧邻页锚 0 命中 → 现场文件应 0 候选(无假阳);
16
+ - rse v2-verify 后现场应 0 候选(▲旁注洁食句按带 压缩转述 标签计,边界见报告)。
17
+
18
+ 用法:
19
+ python defense3_impersonation_scan.py <SKILL.md> [更多 SKILL.md ...]
20
+ python defense3_impersonation_scan.py --selftest
21
+ 输出:stdout JSON 摘要;文件落盘用 runner(pilotB_def3_runner.py)
22
+ """
23
+ import os
24
+ import re
25
+ import sys
26
+ import json
27
+
28
+ MARKERS = ["原文意", "转述", "压缩", "非逐字", "非原话", "省略改写", "词级", "短语级",
29
+ "术语", "同构", "自述", "见原文", "非作者", "非实时判断"]
30
+ PUNCT = ",,、。;;??!!::"
31
+ OPENERS = [("“", "”"), ("「", "」"), ("\"", "\"")]
32
+
33
+
34
+ def quote_char_count(line):
35
+ return sum(1 for ch in line if ch in "“”「」")
36
+
37
+
38
+ # ⚠ 自伤 A-38(2026-09-13 · 由独立判官指出后自查确证):
39
+ # 旧实现把"页锚"硬编码为投资线的 `(p数字)` 形态(`has_page_anchor` 找 `(p`;
40
+ # `tight_trail` 要求窗口内同时出现 `〔` 与 `(p`)。本线(书籍蒸馏)的锚写作
41
+ # `【Barkley 第2章, PDF p306】`,**一个都不匹配** ⇒ 每条引文一律被判"引号后无紧邻页锚"
42
+ # ⇒ **suspects 恒为 0,与内容无关**(=恒空转的闸,不是已通过的闸)。
43
+ # 修法:锚形态表化(含本线形态)+ 正样本对照(见 selftest 的 `PDF p` 用例)。
44
+ ANCHOR_PATTERNS = [
45
+ r'〔[^〕]{0,40}(p\s*\d', # 投资线:〔…(p7)〕
46
+ r'【[^】]{0,80}PDF\s*p\s*\d', # 书籍线:【Barkley 第2章, PDF p306】
47
+ r'\[[^\]]{0,80}PDF\s*p\s*\d', # 变体:[bark PDF p84]
48
+ ]
49
+
50
+
51
+ def has_page_anchor(line):
52
+ for pat in ANCHOR_PATTERNS:
53
+ if re.search(pat, line):
54
+ return True
55
+ i = line.find("(p")
56
+ if i < 0:
57
+ return False
58
+ j = i + 2
59
+ while j < len(line) and (line[j].isdigit() or line[j] in "-–~,"):
60
+ j += 1
61
+ return any(c.isdigit() for c in line[i:j])
62
+
63
+
64
+ def anchor_in_window(ln, start, width):
65
+ """引号之后 width 字窗口内是否出现任一形态的页锚(A-38 修:形态表化,不再写死 `〔…(p)〕`)。"""
66
+ win = ln[start:start + width]
67
+ return has_page_anchor(win)
68
+
69
+
70
+ def quoted_runs(line):
71
+ """抽取引号对内的运行段(支持嵌套一层:外层 「」 或 “”,ASCII 双引号)。"""
72
+ runs = []
73
+ i = 0
74
+ while i < len(line):
75
+ hit = None
76
+ for o, c in OPENERS:
77
+ k = line.find(o, i)
78
+ if k >= 0 and (hit is None or k < hit[0]):
79
+ hit = (k, o, c)
80
+ if hit is None:
81
+ break
82
+ k, o, c = hit
83
+ # 跳过成对单引号里的撇号干扰:ASCII 双引号内部不再当开引号
84
+ e = line.find(c, k + 1)
85
+ if e < 0:
86
+ break
87
+ content = line[k + 1:e]
88
+ runs.append({"text": content, "start": k, "end": e, "open": o})
89
+ i = e + 1
90
+ return runs
91
+
92
+
93
+ def marked(seg):
94
+ return any(m in seg for m in MARKERS)
95
+
96
+
97
+ def scan_line(ln, body, heading):
98
+ """body=False 表示该行处于排除范围。返回记录列表。"""
99
+ recs = []
100
+ qcount = quote_char_count(ln)
101
+ anchor = has_page_anchor(ln)
102
+ for run in quoted_runs(ln):
103
+ txt = run["text"]
104
+ if len(txt) < 4:
105
+ continue
106
+ n = len(txt)
107
+ punct = any(ch in txt for ch in PUNCT)
108
+ before = ln[max(0, run["start"] - 14):run["start"]]
109
+ labeled = marked(before) or marked(txt[:40]) # v2:后置标签不计(judge ▲旁注口径)
110
+ win_start = run["end"] + 1
111
+ tight_trail = anchor_in_window(ln, win_start, 80)
112
+ suspect = body and tight_trail and not labeled and ((n >= 20) or (n >= 12 and punct))
113
+ recs.append({
114
+ "quote": txt[:80], "quote_len": n, "punct": punct, "labeled": labeled,
115
+ "anchor": anchor, "tight_trail": tight_trail, "suspect": suspect, "qcount": qcount,
116
+ "near": "标注豁免" if labeled else ("引号后无紧邻页锚" if not tight_trail else "命中"),
117
+ })
118
+ return recs
119
+
120
+
121
+ def is_excluded_line(ln, heading):
122
+ s = ln.lstrip()
123
+ if s.startswith(">"):
124
+ return True
125
+ if s.startswith("|") or s.startswith("-") and "反例" in heading and len(s) < 60:
126
+ return False
127
+ if heading.startswith("## R"):
128
+ return True
129
+ if heading.startswith("### E-示例") or heading.startswith("### 最小交付"):
130
+ return True
131
+ if ln.startswith("description:") or ln.startswith("---"):
132
+ return True
133
+ return False
134
+
135
+
136
+ def scan_file(path):
137
+ with open(path, encoding="utf-8") as f:
138
+ lines = f.read().splitlines()
139
+ heading = ""
140
+ body_lines = 0
141
+ suspects = []
142
+ stats = {"quote_lines": 0, "labeled_runs": 0}
143
+ for idx, ln in enumerate(lines):
144
+ if ln.startswith("#"):
145
+ heading = ln
146
+ if is_excluded_line(ln, heading):
147
+ continue
148
+ body_lines += 1
149
+ recs = scan_line(ln, True, heading)
150
+ if recs:
151
+ stats["quote_lines"] += 1
152
+ for r in recs:
153
+ if r["labeled"]:
154
+ stats["labeled_runs"] += 1
155
+ if r["suspect"]:
156
+ suspects.append({"line": idx + 1, "text": ln.strip()[:160], **r})
157
+ return {"file": os.path.abspath(path), "body_lines": body_lines, "stats": stats,
158
+ "suspects": suspects, "total_lines": len(lines)}
159
+
160
+
161
+ def selftest():
162
+ """召回样本:通用无版权示例(原样本含书籍原文片段,已替换为通用示例以规避版权;测试结构不变,如需原语料请自取原书)。"""
163
+ cases = [
164
+ ("示例-修复清单①(应命中)", "他分文未掏,反称这事儿要怪“机构没事儿大把赚钱、出事儿他说怪运气”(见 ca04〔示例·某案例(p1)〕)", True),
165
+ ("示例-修复清单②(应命中)", "「只做短期动作,等周期快结束就离场、每 5 年换一个策略」〔示例·同节(p2)〕", True),
166
+ ("示例-修复清单③(应命中)", "“不是提供好产品,而是没被某群体否决”〔第2章·否决(p3)〕", True),
167
+ ("示例-原文意标注(应豁免)", "某公司的成功不在提供优质产品,而在没有被某群体否决(原文意:成功靠“没有被某群体否决”,见 ca16〔示例·否决(p3)〕)", False),
168
+ ("示例-词级短引(应豁免)", "若某方案“98% 置信度安全”适用于个体重复暴露(〔示例·重复风险(p4-5)〕)", False),
169
+ ("示例-带引号后置标签(应命中)", "“某群体绝不吃某种食物、非某群体吃得下”(ca15 压缩转述,〔示例·某案例(p6)〕)", True),
170
+ ("示例-短术语引+页锚(应豁免)", "“平均值掩盖了深度差异”=均值安全掩盖路径上的深坑(R·〔p7〕)", False),
171
+ # ---- A-38 正样本对照:书籍线锚形态 `【… PDF pNNN】` 必须被认作页锚 ----
172
+ ("PDF锚-长引+紧邻锚(应命中)", "作者写道:“必须把规则列成一张清单并且不许有讨价还价余地的做法”【Barkley 第14章, PDF p306】", True),
173
+ ("PDF锚-转述标注(应豁免)", "该主张为压缩转述:“结束谈话时别把话说死”(转述,【Barkley 第10章, PDF p229】)", False),
174
+ ("PDF锚-短术语(应豁免)", "“执行功能”一词【Barkley 第2章, PDF p85】", False),
175
+ ]
176
+ results = []
177
+ for name, text, expect in cases:
178
+ recs = scan_line(text, True, "## 测试")
179
+ got = any(r["suspect"] for r in recs)
180
+ results.append({"case": name, "expect_hit": expect, "got_hit": got,
181
+ "pass": got == expect, "recs": recs})
182
+ return results
183
+
184
+
185
+ def main():
186
+ if "--selftest" in sys.argv:
187
+ out = {"mode": "selftest", "results": selftest()}
188
+ print(json.dumps(out, ensure_ascii=False, indent=2))
189
+ return
190
+ paths = [p for p in sys.argv[1:] if os.path.isfile(p)]
191
+ if not paths:
192
+ print(__doc__)
193
+ sys.exit(2)
194
+ res = [scan_file(p) for p in paths]
195
+ print(json.dumps({"mode": "scan", "files": res}, ensure_ascii=False, indent=2))
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+ """machine_layer_readycheck v1(v3.1 · 负样本校准版)
3
+
4
+ 作用:§13 机器层适用性前置检查的落地脚本。
5
+ 设计修正(负样本启发):不再用“固定四项(candidates 五池/quote-precheck)”一刀切,
6
+ 改为 按闸最小输入 判定——机器层各闸只需其自身所需的最小机器输入,
7
+ 塔勒布式家族资产(candidates 池/quote-precheck)视为可选强化而非适用前提。
8
+
9
+ 每闸适用条件:
10
+ blind-lexicon : SKILL description 含 何时用/触发词 任一标记
11
+ defense3 : SKILL 含 R 段且正文存在「」或引号引用形态(冒充扫描输入)
12
+ darwin : SKILL 含 R/I/E/B 分节 + description(机器初评输入)
13
+ fidelity-R : 册根存在可核对原文(parts/*.md 或 ocr 等)且 SKILL R 段非空
14
+ 用法: python machine_layer_readycheck.py <SKILL.md> <bookbase> [--json out.json]
15
+ """
16
+ import os, re, sys, json
17
+
18
+ def check(skill_path, bookbase):
19
+ text = open(skill_path, encoding="utf-8").read()
20
+ meta_ok = ("何时用" in text[:2000]) or ("触发词" in text[:2000])
21
+ has_R = re.search(r"^##\s*R", text, re.M) is not None
22
+ has_I = re.search(r"^##\s*I", text, re.M) is not None
23
+ has_E = re.search(r"^##\s*E", text, re.M) is not None
24
+ has_B = re.search(r"^##\s*B", text, re.M) is not None
25
+ quotes = "「" in text or "“" in text
26
+ # fidelity 源:册根 parts/ocr 等
27
+ srcs = []
28
+ # v0.2b:优先只取 <bookbase>/parts/ 下的正文源;并排除 *check* 等副本目录(原版把 parts-check 也计入 → 页数翻倍)
29
+ pref = os.path.join(bookbase, "parts") if bookbase else ""
30
+ if pref and os.path.isdir(pref):
31
+ for fn in sorted(os.listdir(pref)):
32
+ if fn.endswith((".md", ".txt")):
33
+ srcs.append(os.path.join(pref, fn))
34
+ elif os.path.isdir(bookbase):
35
+ for root, dirs, files in os.walk(bookbase):
36
+ if any(k in os.path.basename(root).lower() for k in ("check", "backup", "backups")):
37
+ continue
38
+ for fn in files:
39
+ if fn.endswith((".md", ".txt")) and ("part" in fn.lower() or "ocr" in fn.lower() or "ch" in fn.lower()):
40
+ srcs.append(os.path.join(root, fn))
41
+ r_quotes = len(re.findall(r"「[^」]+」", text.split("## I")[0] if "## I" in text else text))
42
+ # v0.2(M7):原版本闸只给 True/False 与 missing=[],属**自证式清单**、无可核计数。
43
+ # 现为每闸输出**可核计数**(desc 长度/触发词数、R 段条数、引号与页锚计数、原文页数、步骤与 CHECKPOINT 数),
44
+ # 并在结论中明确“本检查只判最小输入齐备度,不替代任何真实闸”。
45
+ _desc = re.search(r'^description:\s*[|"\'](.*?)(?:"\s*$|\n\S)', text, re.M | re.S)
46
+ _desc_txt = _desc.group(1) if _desc else ""
47
+ n_pages = 0
48
+ for _sp in srcs:
49
+ try:
50
+ n_pages += len(re.findall(r"=====\s*\[PAGE \d+\]\s*=====", open(_sp, encoding="utf-8").read()))
51
+ except Exception:
52
+ pass
53
+ counts = {
54
+ "desc_chars": len(_desc_txt),
55
+ "trigger_keywords": len(re.findall(r"[、,//]", _desc_txt[_desc_txt.find("触发词"):])) if "触发词" in _desc_txt else 0,
56
+ "R_quote_lines": len(re.findall(r"^\s*-\s*(?:R\d+|「)", text, re.M)),
57
+ "quote_pairs": len(re.findall(r"「[^」]{4,}」", text)),
58
+ "page_anchors": len(re.findall(r"〔p\d+", text)),
59
+ "source_md_files": len(srcs), "source_pages": n_pages,
60
+ "E_steps": len(set(re.findall(r"步骤\s*(\d)", text))),
61
+ "checkpoints": len(re.findall(r"CHECKPOINT", text)),
62
+ }
63
+
64
+ def _g(applicable, missing, note):
65
+ return {"applicable": bool(applicable), "missing": [] if applicable else missing,
66
+ "input_note": note, "counts": counts}
67
+
68
+ gates = {
69
+ "blind-lexicon": _g(meta_ok, ["description 缺 何时用/触发词 标记"], "词表取自 description(desc 长度与触发词数见 counts)"),
70
+ "defense3": _g(has_R and quotes, ["无 R 段或无引号引用形态"], "冒充扫描需 R 段边界与引号/页锚形态(R 行数与引号对见 counts)"),
71
+ "darwin": _g(has_R and has_I and has_E and has_B, ["缺 R/I/E/B 分节之一"], "9 维初评需 RIA 分节与 description"),
72
+ "fidelity-R": _g(bool(srcs) and has_R and r_quotes > 0,
73
+ ([] if srcs else ["册根无可核对原文(parts/ocr)"]) + ([] if r_quotes > 0 else ["R 段无引文"]),
74
+ "R 引文逐字需册根原文(源文件数与页标记数见 counts)"),
75
+ }
76
+ return {"skill": os.path.abspath(skill_path), "bookbase": os.path.abspath(bookbase) if bookbase else None,
77
+ "gates": gates, "counts": counts,
78
+ "recommendation": ("machine-ok(四闸最小输入齐备;**本检查只判最小输入齐备度,不替代任何真实闸**)"
79
+ if all(g["applicable"] for g in gates.values())
80
+ else "partial-degrade(不适用闸按 v2.3 全 LLM 并标 pending;其余闸照跑机器层)"),
81
+ "method_note": "v0.2(M7 修正):由‘清单式自证’升级为**可核计数**(desc/R/引号/页锚/原文源与页数/步骤/检查点),计数随本文件落盘可复核。"}
82
+
83
+ def main():
84
+ if len(sys.argv) < 3:
85
+ print(__doc__); sys.exit(2)
86
+ res = check(sys.argv[1], sys.argv[2])
87
+ if "--json" in sys.argv:
88
+ out = sys.argv[sys.argv.index("--json") + 1]
89
+ json.dump(res, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
90
+ print(json.dumps(res, ensure_ascii=False, indent=1))
91
+
92
+ if __name__ == "__main__":
93
+ main()