@furongjun1999/dsh-memory 0.6.1 → 0.7.0
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.
- package/README.md +48 -18
- package/docs/README.md +1 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/freshness.py +511 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/mcp_server.py +128 -19
- package/md_cg/mdcg.py +509 -22
- package/md_cg/mdcos.py +203 -22
- package/md_cg/nodefile.py +74 -1
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""拒绝域双语义判据守卫(P0-4 / §5.2)——`boundary_hit` 与资格 REJECT **分开**。
|
|
3
|
+
|
|
4
|
+
契约(设计稿 `docs/plans/睡眠与自迭代_功能优化设计_v0.4.md` §5.2):
|
|
5
|
+
|
|
6
|
+
拒绝域命中**必须标记为「边界命中」**,与资格判定的 REJECT **分开计数、分开
|
|
7
|
+
呈现**——即:检索「什么条件下不适用」→ 该节点以 `boundary_hit` 命中(可召回、
|
|
8
|
+
可展示);该节点的**正例资格不受影响**(不因边界命中就 REJECT 它)。
|
|
9
|
+
|
|
10
|
+
探针给的现状(`md_cg/**` 全仓 grep):
|
|
11
|
+
|
|
12
|
+
`boundary_hit` 字面量**零命中**(只出现在设计稿与本工作流草稿里);
|
|
13
|
+
`不适用条件` 现在**只在一处**被当负条件用——`MdCG.judge_qualification`
|
|
14
|
+
(`mdcg.py` :2997 取 `non_applicable_conditions` → :2998 调单点
|
|
15
|
+
`neg_condition_hits` → :3000-3002 命中即 `REJECT`)。
|
|
16
|
+
⇒「同一字段只承载一条语义(负条件→REJECT)」,P0-4 要的两件都**尚未存在**。
|
|
17
|
+
|
|
18
|
+
P0-4 当轮**不接检索面**(接线在 P2):只落判据函数与守卫,检索读数必须零位移。
|
|
19
|
+
**(P2-2 已接线,2026-10-01)**:接线点两处——检索面 `MdCGOS.search_rrf`
|
|
20
|
+
(`md_cg/mdcos.py:1754`)与默认打分收口 `MdCG._emit`(`md_cg/mdcg.py:4191`);
|
|
21
|
+
两处都调**同一个** `MdCG.judge_with_boundary`,边界标记一律经同族的 `boundary_mark`
|
|
22
|
+
取(其体内复用 P0-4 的 `boundary_hit`)——**单点复用、不另写第二份判据**。
|
|
23
|
+
上面那段「零命中」是 **P0-4 当轮的探针快照**,已被本文件 B5 组的接线断言取代。
|
|
24
|
+
|
|
25
|
+
断言分组:
|
|
26
|
+
|
|
27
|
+
B0 判据在位与形状——`boundary_hit` / `is_boundary_query` / `judge_with_boundary`
|
|
28
|
+
/ `tally_boundary` / `BOUNDARY_MARKER` 齐备;返回体的**键集逐项钉死**
|
|
29
|
+
(P2 直接依赖的形状)。
|
|
30
|
+
B1 单点结构(静态)——`boundary_hit` 的命中走**既有负条件判据单点**
|
|
31
|
+
`neg_condition_hits`;`judge_with_boundary` 的资格腿走
|
|
32
|
+
`MdCG.judge_qualification`(不重判、不抄第二份)。
|
|
33
|
+
B2 双语义可分(端到端,真建临时库)——边界问句 ⇒ `boundary_hit` 且
|
|
34
|
+
`qualification.state != REJECT`(**不因边界命中而 REJECT**);
|
|
35
|
+
非边界问句而情境真落在拒绝域 ⇒ 照旧 `REJECT`(**灵敏度不变**);
|
|
36
|
+
两者 `counts.reject` 与 `counts.boundary_hit` **分开计数**。
|
|
37
|
+
B3 分开呈现——返回体两个顶层键 `qualification` / `boundary` 各自独立;
|
|
38
|
+
标记只出现在 `boundary` 侧,`qualification` 侧不得出现 `marker`。
|
|
39
|
+
B4 聚合计数——`tally_boundary` 的 `self_negation` 恒 0(非 0 = 自否定残留);
|
|
40
|
+
reject 与 boundary_hit 分别计数、互不掩盖。
|
|
41
|
+
B5 **接线已落(P2-2 改判,2026-10-01)**——P0-4 时的「本轮零接线」断言
|
|
42
|
+
已被 P2 的**接线契约**取代(§5.4:六要素进默认检索后拒绝域必须双语义可分):
|
|
43
|
+
B5 检索面(`md_cg/mdcos.py`)与默认打分收口(`MdCG._emit`)**均已接线**;
|
|
44
|
+
B5a `boundary_mark` 仍是**单点复用**(体内调 P0-4 的 `boundary_hit`,
|
|
45
|
+
不另写第二份判据);
|
|
46
|
+
B5b **默认口径零位移**:非边界问句下 `judge_with_boundary` 的 qualification
|
|
47
|
+
≡ `judge_qualification`(逐例对拍),且无边界命中时 `_emit` 不落
|
|
48
|
+
边界键(默认链路的 meta 键集合不变)。
|
|
49
|
+
|
|
50
|
+
运行:python -X utf8 -m md_cg.test_boundary_hit
|
|
51
|
+
python -X utf8 -m md_cg.test_boundary_hit --head-baseline # 红基线自证
|
|
52
|
+
python -X utf8 -m md_cg.test_boundary_hit --mutate # 定点变异自证
|
|
53
|
+
|
|
54
|
+
退出码(fail-closed):0 = 全绿 / 红基线确有红项;1 = 断言失败 / 变异未按预期转红;
|
|
55
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到)。
|
|
56
|
+
"""
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import contextlib
|
|
60
|
+
import io
|
|
61
|
+
import os
|
|
62
|
+
import shutil
|
|
63
|
+
import sys
|
|
64
|
+
import tempfile
|
|
65
|
+
|
|
66
|
+
from . import mdcg as M
|
|
67
|
+
from .mdcg import (BOUNDARY_MARKER, MdCG, boundary_hit, is_boundary_query,
|
|
68
|
+
strip_boundary_terms, tally_boundary)
|
|
69
|
+
|
|
70
|
+
_PASS = []
|
|
71
|
+
_FAIL = []
|
|
72
|
+
_SKIP = []
|
|
73
|
+
_SRC = {}
|
|
74
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_boundary_")
|
|
75
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
76
|
+
|
|
77
|
+
#: 拒绝域条目(语料口径,与既有负条件守卫的 G7 同形)
|
|
78
|
+
NEG = "本条不覆盖 tags 字段的扫描面,闸门只扫 content"
|
|
79
|
+
DOC = ("# 功能名:写入闸门对 tags 的扫描面\n"
|
|
80
|
+
"# 生效条件:问写入闸门内容闸门的判据时\n"
|
|
81
|
+
"# 子功能:md_cg/writepipe.py\n# 执行:读闸门代码\n"
|
|
82
|
+
"# 验证方式:编译器/静态检查通过\n"
|
|
83
|
+
"# 不适用条件:%s\n" % NEG)
|
|
84
|
+
|
|
85
|
+
_SHAPE_JWB = {"qualification", "boundary", "counts", "recallable", "reason"}
|
|
86
|
+
_SHAPE_QUAL = {"state", "reason", "boundary_suppressed"}
|
|
87
|
+
_SHAPE_BND = {"hit", "terms", "marker", "reason"}
|
|
88
|
+
_SHAPE_CNT = {"accept", "reject", "defer", "blindspot", "boundary_hit"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def ok(cond, msg, extra=""):
|
|
92
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
93
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
94
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _rel_text(rel: str) -> str:
|
|
98
|
+
if rel in _SRC:
|
|
99
|
+
return _SRC[rel]
|
|
100
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
101
|
+
return f.read()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _mk_applicable():
|
|
105
|
+
"""真建临时库:一个六要素齐全、带拒绝域的节点(走完整写入面)。"""
|
|
106
|
+
from .mdcos import MdCGOS
|
|
107
|
+
r = tempfile.mkdtemp(prefix="bh_", dir=_TMP)
|
|
108
|
+
cg = MdCGOS(r)
|
|
109
|
+
cg.add("bh1", DOC, layer="knowledge", verification_basis="test",
|
|
110
|
+
non_applicable_conditions=[NEG])
|
|
111
|
+
cg.flush()
|
|
112
|
+
got = cg.get("bh1")
|
|
113
|
+
return cg, {"frontmatter": got["frontmatter"], "content": got["content"]}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------- B0
|
|
117
|
+
def b0():
|
|
118
|
+
print("== B0 判据在位与返回形状 ==")
|
|
119
|
+
ok(BOUNDARY_MARKER == "boundary_hit",
|
|
120
|
+
"B0 展示标记字面量 = boundary_hit(§5.2 的判据名)")
|
|
121
|
+
for name in ("boundary_hit", "is_boundary_query", "strip_boundary_terms",
|
|
122
|
+
"tally_boundary"):
|
|
123
|
+
ok(callable(globals().get(name) or getattr(M, name, None)),
|
|
124
|
+
"B0a 模块级判据 `%s` 在位" % name)
|
|
125
|
+
ok(hasattr(MdCG, "judge_with_boundary")
|
|
126
|
+
and isinstance(MdCG.__dict__.get("judge_with_boundary"), staticmethod),
|
|
127
|
+
"B0b MdCG.judge_with_boundary 在位且为 staticmethod(与 judge_qualification 同形)")
|
|
128
|
+
cg, node = _mk_applicable()
|
|
129
|
+
r = MdCG.judge_with_boundary(node, "问:什么条件下不适用?" + NEG)
|
|
130
|
+
ok(set(r) == _SHAPE_JWB, "B0c 顶层键集逐项一致", sorted(set(r)))
|
|
131
|
+
ok(set(r["qualification"]) == _SHAPE_QUAL, "B0d qualification 键集",
|
|
132
|
+
sorted(r["qualification"]))
|
|
133
|
+
ok(set(r["boundary"]) == _SHAPE_BND, "B0e boundary 键集",
|
|
134
|
+
sorted(r["boundary"]))
|
|
135
|
+
ok(set(r["counts"]) == _SHAPE_CNT, "B0f counts 键集", sorted(r["counts"]))
|
|
136
|
+
ok(isinstance(r["recallable"], bool), "B0g recallable 为 bool")
|
|
137
|
+
bh = boundary_hit(node, NEG)
|
|
138
|
+
ok(set(bh) == {"hit", "terms", "marker", "reason", "scene"},
|
|
139
|
+
"B0h boundary_hit 的键集", sorted(bh))
|
|
140
|
+
cg.close()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------- B1
|
|
144
|
+
def b1():
|
|
145
|
+
print("== B1 单点结构(静态)==")
|
|
146
|
+
src = _rel_text("md_cg/mdcg.py")
|
|
147
|
+
i = src.index("def boundary_hit(")
|
|
148
|
+
body = src[i:i + 1600]
|
|
149
|
+
ok("neg_condition_hits(terms, scene_str," in body,
|
|
150
|
+
"B1 boundary_hit 的命中走**既有负条件判据单点** neg_condition_hits")
|
|
151
|
+
code = "\n".join(l for l in body.splitlines()
|
|
152
|
+
if not l.strip().startswith("#"))
|
|
153
|
+
ok("any(w in scene" not in code and "for w in str(" not in code,
|
|
154
|
+
"B1a 不重判、不抄第二份切词实现")
|
|
155
|
+
j = src.index("def judge_with_boundary(")
|
|
156
|
+
jb = src[j:j + 2600]
|
|
157
|
+
ok("MdCG.judge_qualification(" in jb,
|
|
158
|
+
"B1b 资格腿走 MdCG.judge_qualification(不重判)")
|
|
159
|
+
ok(jb.count("MdCG.judge_qualification(") == 2,
|
|
160
|
+
"B1c 两条分支(边界问句摘词面重跑 / 其余原样)各调一次",
|
|
161
|
+
jb.count("MdCG.judge_qualification("))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------- B2
|
|
165
|
+
def b2():
|
|
166
|
+
print("== B2 双语义可分(端到端)==")
|
|
167
|
+
cg, node = _mk_applicable()
|
|
168
|
+
# ① 边界问句:问的就是该节点的拒绝域
|
|
169
|
+
q1 = "这条记忆在什么条件下不适用?" + NEG
|
|
170
|
+
ok(is_boundary_query(q1) is True, "B2 前置:q1 被识别为边界问句", q1)
|
|
171
|
+
r1 = MdCG.judge_with_boundary(node, q1)
|
|
172
|
+
ok(r1["boundary"]["hit"] is True,
|
|
173
|
+
"B2a 边界命中(hit=True)", r1["boundary"])
|
|
174
|
+
ok(r1["boundary"]["marker"] == "boundary_hit",
|
|
175
|
+
"B2b 命中即带展示标记 boundary_hit", r1["boundary"]["marker"])
|
|
176
|
+
ok(r1["qualification"]["state"] != "REJECT",
|
|
177
|
+
"B2c **不因边界命中而 REJECT**:资格态 = %s" % r1["qualification"]["state"],
|
|
178
|
+
r1["qualification"])
|
|
179
|
+
ok(r1["counts"]["boundary_hit"] == 1 and r1["counts"]["reject"] == 0,
|
|
180
|
+
"B2d 分开计数:boundary_hit=1 / reject=0", r1["counts"])
|
|
181
|
+
ok(r1["recallable"] is True, "B2e 可召回可展示(recallable=True)")
|
|
182
|
+
# ② 非边界问句 + 情境真落在拒绝域:灵敏度不变 ⇒ 照旧 REJECT
|
|
183
|
+
r2 = MdCG.judge_with_boundary(node, NEG)
|
|
184
|
+
ok(is_boundary_query(NEG) is False, "B2f 前置:q2 不是边界问句")
|
|
185
|
+
ok(r2["qualification"]["state"] == "REJECT",
|
|
186
|
+
"B2g **灵敏度不变**:非边界问句下情境落在拒绝域 ⇒ 照旧 REJECT",
|
|
187
|
+
r2["qualification"])
|
|
188
|
+
ok(r2["counts"]["reject"] == 1,
|
|
189
|
+
"B2h 该 REJECT 计入 counts.reject(不与边界命中混计)", r2["counts"])
|
|
190
|
+
# ③ 边界问句但拒绝域未命中 ⇒ 无边界命中
|
|
191
|
+
r3 = MdCG.judge_with_boundary(node, "这条记忆在什么条件下不适用?")
|
|
192
|
+
ok(r3["boundary"]["hit"] is False and r3["boundary"]["marker"] is None,
|
|
193
|
+
"B2i 边界问句但词面未命中 ⇒ 无边界标记", r3["boundary"])
|
|
194
|
+
# ④ 不变量批量:任取边界问句组合,(boundary.hit ∧ state==REJECT) 恒假
|
|
195
|
+
bad = []
|
|
196
|
+
for q in ("什么条件下不适用 " + NEG, "何时不适用:" + NEG,
|
|
197
|
+
"拒绝域 " + NEG, "边界条件?" + NEG, NEG + " 什么情况下不适用"):
|
|
198
|
+
rr = MdCG.judge_with_boundary(node, q)
|
|
199
|
+
if rr["boundary"]["hit"] and rr["qualification"]["state"] == "REJECT":
|
|
200
|
+
bad.append(q)
|
|
201
|
+
ok(not bad, "B2j **不变量**:边界命中 ∧ 资格 REJECT 的组合恒不出现", bad)
|
|
202
|
+
cg.close()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------- B3
|
|
206
|
+
def b3():
|
|
207
|
+
print("== B3 分开呈现 ==")
|
|
208
|
+
cg, node = _mk_applicable()
|
|
209
|
+
r = MdCG.judge_with_boundary(node, "什么条件下不适用 " + NEG)
|
|
210
|
+
ok("marker" not in r["qualification"],
|
|
211
|
+
"B3 资格面**不含**展示标记(两语义各管各的呈现)",
|
|
212
|
+
sorted(r["qualification"]))
|
|
213
|
+
ok("marker" in r["boundary"],
|
|
214
|
+
"B3a 展示标记只在边界面出现")
|
|
215
|
+
ok(r["qualification"]["boundary_suppressed"] is True,
|
|
216
|
+
"B3b 边界问句下资格判定走了「摘词面重跑」并如实留痕")
|
|
217
|
+
r2 = MdCG.judge_with_boundary(node, NEG)
|
|
218
|
+
ok(r2["qualification"]["boundary_suppressed"] is False,
|
|
219
|
+
"B3c 非边界问句下不摘词面(boundary_suppressed=False,灵敏度不变)")
|
|
220
|
+
cg.close()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------- B4
|
|
224
|
+
def b4():
|
|
225
|
+
print("== B4 聚合计数 ==")
|
|
226
|
+
cg, node = _mk_applicable()
|
|
227
|
+
recs = [MdCG.judge_with_boundary(node, "什么条件下不适用 " + NEG),
|
|
228
|
+
MdCG.judge_with_boundary(node, NEG),
|
|
229
|
+
MdCG.judge_with_boundary(node, "写入闸门怎么配"),
|
|
230
|
+
MdCG.judge_with_boundary(node, "何时不适用 " + NEG)]
|
|
231
|
+
t = tally_boundary(recs)
|
|
232
|
+
ok(t["self_negation"] == 0,
|
|
233
|
+
"B4 self_negation=0(边界问句下无自否定残留)", t)
|
|
234
|
+
ok(t["boundary_hit"] == 3 and t["reject"] == 1,
|
|
235
|
+
"B4a 边界命中 3 条 / 资格 REJECT 1 条——**分开计数、互不掩盖**", t)
|
|
236
|
+
ok(t["total"] == 4 and t["reject"] + t["accept"] + t["defer"]
|
|
237
|
+
+ t["blindspot"] == 4,
|
|
238
|
+
"B4b 资格四态计数自洽(相加 = total)", t)
|
|
239
|
+
ok(t["recallable"] == 3, "B4c 可召回 3 条(REJECT 那条不可召回)", t)
|
|
240
|
+
cg.close()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------- B5
|
|
244
|
+
def b5():
|
|
245
|
+
print("== B5 接线已落(P2-2 改判):检索面引用 + 单点复用 + 默认口径零位移 ==")
|
|
246
|
+
src = _rel_text("md_cg/mdcg.py")
|
|
247
|
+
mdcos = _rel_text("md_cg/mdcos.py")
|
|
248
|
+
ok("boundary_mark" in mdcos and "judge_with_boundary" in mdcos,
|
|
249
|
+
"B5 检索面(mdcos.py)已引用本组判据(P2-2 接线)")
|
|
250
|
+
i = src.index(" def _emit(self, scored")
|
|
251
|
+
body = src[i:i + 12000]
|
|
252
|
+
ok("boundary_mark(" in body,
|
|
253
|
+
"B5 默认打分收口 `MdCG._emit` 已接线(结果卡带 boundary_hit 标记)")
|
|
254
|
+
# 单点复用:boundary_mark 体内必须调 P0-4 的 boundary_hit,不得另写一份
|
|
255
|
+
j = src.index("def boundary_mark(")
|
|
256
|
+
bm = src[j:j + 2200]
|
|
257
|
+
ok("boundary_hit(" in bm and "rejection_index_terms(" in bm,
|
|
258
|
+
"B5a boundary_mark 单点复用 P0-4 判据(不另写第二份切词/命中实现)")
|
|
259
|
+
# 默认口径零位移:非边界问句下 judge_with_boundary ≡ judge_qualification
|
|
260
|
+
doc = ("# 功能名:x\n# 生效条件:无条件\n# 子功能:无\n"
|
|
261
|
+
"# 执行:无\n# 验证方式:test\n# 不适用条件:其它会话\n")
|
|
262
|
+
node = {"frontmatter": {"non_applicable_conditions": ["其它会话"]},
|
|
263
|
+
"content": doc}
|
|
264
|
+
same = True
|
|
265
|
+
for q, ctx in (("苹果 章节", None), ("其它会话", None),
|
|
266
|
+
("刚才在干什么", {"scene": "其它会话"}),
|
|
267
|
+
("怎么验证的", {"scene": "编译"})):
|
|
268
|
+
a = M.MdCG.judge_with_boundary(node, q, ctx)["qualification"]
|
|
269
|
+
b = dict(M.MdCG.judge_qualification(node, q, ctx))
|
|
270
|
+
b.setdefault("boundary_suppressed", False)
|
|
271
|
+
if a.get("state") != b.get("state"):
|
|
272
|
+
same = False
|
|
273
|
+
ok(same, "B5b 非边界问句下资格态 ≡ judge_qualification(默认口径零位移)")
|
|
274
|
+
ok('if _bnd["hit"]:' in body or "if _bnd[\"hit\"]:" in body,
|
|
275
|
+
"B5b 边界读数只在确有边界命中时落键(meta 键集合默认不变)")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
_GROUPS = (b0, b1, b2, b3, b4, b5)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _run_groups() -> int:
|
|
282
|
+
_PASS.clear()
|
|
283
|
+
_FAIL.clear()
|
|
284
|
+
for g in _GROUPS:
|
|
285
|
+
try:
|
|
286
|
+
g()
|
|
287
|
+
except Exception as exc: # noqa: BLE001
|
|
288
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
289
|
+
import traceback
|
|
290
|
+
print(traceback.format_exc()[-900:])
|
|
291
|
+
return len(_FAIL)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------- 变异表
|
|
295
|
+
# 锚点 = (名字, rel, old, new)。每次变异必须让套件**转红**:
|
|
296
|
+
# M1 边界问句不做抑制(= P0-4 之前的形态:命中即 REJECT)→ B2c 红
|
|
297
|
+
# M2 不设展示标记(= 边界不可展示)→ B2b/B2i/B3a/B4 红
|
|
298
|
+
# M3 判据不复用单点(抄第二份切词实现)→ B1/B1a 红
|
|
299
|
+
_MUTATIONS = (
|
|
300
|
+
("边界问句不做抑制(改动前形态)", "md_cg/mdcg.py",
|
|
301
|
+
' if bh["hit"] and bq:', " if False:"),
|
|
302
|
+
# 注:锚点带尾换行——`boundary_mark` 与 `boundary_hit` 各有一行同前缀,
|
|
303
|
+
# 只有 `boundary_hit` 那行以换行收尾(`boundary_mark` 那行后还接了 "why")
|
|
304
|
+
("不设展示标记", "md_cg/mdcg.py",
|
|
305
|
+
' "marker": BOUNDARY_MARKER if hit else None,\n',
|
|
306
|
+
' "marker": None,\n'),
|
|
307
|
+
("判据不复用单点(抄第二份实现)", "md_cg/mdcg.py",
|
|
308
|
+
" hit = neg_condition_hits(terms, scene_str, MdCG._self_topic_text(fm, content))",
|
|
309
|
+
" hit = [x for x in terms if any(w in scene_str for w in str(x).split())]"),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
314
|
+
print("!! 定点变异自证:逐条把双语义判据改回「改动前/失效」形态,套件必须转红\n")
|
|
315
|
+
if list_only:
|
|
316
|
+
for name, rel, _o, _n in _MUTATIONS:
|
|
317
|
+
print(" %-34s [%s]" % (name, rel))
|
|
318
|
+
return 0
|
|
319
|
+
anchor_miss, bad = [], []
|
|
320
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
321
|
+
clean = _run_groups()
|
|
322
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
323
|
+
if clean:
|
|
324
|
+
bad.append("未变异基线即失败")
|
|
325
|
+
|
|
326
|
+
for name, rel, old, new in _MUTATIONS:
|
|
327
|
+
src = _rel_text(rel)
|
|
328
|
+
if old not in src:
|
|
329
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没同步"
|
|
330
|
+
"本表)" % (name, rel))
|
|
331
|
+
anchor_miss.append(name)
|
|
332
|
+
continue
|
|
333
|
+
mut_src = src.replace(old, new, 1)
|
|
334
|
+
_SRC[rel] = mut_src
|
|
335
|
+
try:
|
|
336
|
+
mut = _exec_module("md_cg._bh_mut", rel, mut_src)
|
|
337
|
+
_install(mut)
|
|
338
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
339
|
+
reds = _run_groups()
|
|
340
|
+
detail = buf.getvalue()
|
|
341
|
+
finally:
|
|
342
|
+
_install(M)
|
|
343
|
+
_SRC.pop(rel, None)
|
|
344
|
+
red_lines = [l for l in detail.splitlines()
|
|
345
|
+
if l.strip().startswith("FAIL ")]
|
|
346
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
347
|
+
print(" %s %-34s 红项=%d %s"
|
|
348
|
+
% ("OK " if reds else "MISS ", name, reds, verdict))
|
|
349
|
+
for l in red_lines[:5]:
|
|
350
|
+
print(" " + l.strip()[5:])
|
|
351
|
+
if not reds:
|
|
352
|
+
bad.append(name)
|
|
353
|
+
|
|
354
|
+
if anchor_miss:
|
|
355
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
356
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
357
|
+
return 2
|
|
358
|
+
print("\n定点变异自证:%s"
|
|
359
|
+
% ("PASS(每处判据都被打红)" if not bad else "FAIL —— " + "、".join(bad)))
|
|
360
|
+
return 0 if not bad else 1
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _install(mod) -> None:
|
|
364
|
+
"""把某个 mdcg 模块的判据装进守卫的全局名(变异/还原共用)。"""
|
|
365
|
+
global boundary_hit, is_boundary_query, strip_boundary_terms, tally_boundary
|
|
366
|
+
global MdCG, BOUNDARY_MARKER
|
|
367
|
+
boundary_hit = mod.boundary_hit
|
|
368
|
+
is_boundary_query = mod.is_boundary_query
|
|
369
|
+
strip_boundary_terms = mod.strip_boundary_terms
|
|
370
|
+
tally_boundary = mod.tally_boundary
|
|
371
|
+
MdCG = mod.MdCG
|
|
372
|
+
BOUNDARY_MARKER = mod.BOUNDARY_MARKER
|
|
373
|
+
globals()["M"] = mod
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
377
|
+
import types
|
|
378
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
379
|
+
"__file__": os.path.join(_REPO, rel)}
|
|
380
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
381
|
+
m = types.ModuleType(name)
|
|
382
|
+
m.__dict__.update(ns)
|
|
383
|
+
return m
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def main() -> int:
|
|
387
|
+
try:
|
|
388
|
+
if "--mutate" in sys.argv:
|
|
389
|
+
return _mutate_mode("--list" in sys.argv)
|
|
390
|
+
if "--head-baseline" in sys.argv:
|
|
391
|
+
print("!! 红基线模式:`is_boundary_query` 换回「恒 False」"
|
|
392
|
+
"(= P0-4 之前的形态:边界词面命中即 REJECT),应当转红\n")
|
|
393
|
+
M.is_boundary_query = lambda q: False
|
|
394
|
+
globals()["is_boundary_query"] = lambda q: False
|
|
395
|
+
n = _run_groups()
|
|
396
|
+
print("\n== 红基线判定:应有失败 ==")
|
|
397
|
+
print("红基线失败数 = %d(>0 才算断言有判别力)" % n)
|
|
398
|
+
for f in _FAIL:
|
|
399
|
+
print(" 红:", f)
|
|
400
|
+
return 0 if n else 1
|
|
401
|
+
n = _run_groups()
|
|
402
|
+
print("\n拒绝域双语义判据守卫:%d 通过,%d 失败,%d 跳过"
|
|
403
|
+
% (len(_PASS), n, len(_SKIP)))
|
|
404
|
+
return 0 if not n else 1
|
|
405
|
+
finally:
|
|
406
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
if __name__ == "__main__":
|
|
410
|
+
sys.exit(main())
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
MDCG_EN_ATOMS=1 显式开启;纯中文 query 零触发;
|
|
14
14
|
mdcos 四路 RRF 的 lexical 路同源复用 expand_query_terms,自动受益。
|
|
15
15
|
|
|
16
|
-
⚠ 与**统一归一层**的关系(2026-09-24
|
|
17
|
-
`semantic/unify.py::unify_query`
|
|
18
|
-
|
|
19
|
-
query 先归一成标准中文原子序列——英文 query
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
⚠ 与**统一归一层**的关系(2026-09-24 澄清,曾造成本件红灯;2026-09-30 缺省翻关):
|
|
17
|
+
`semantic/unify.py::unify_query` 是**另一个**开关,缺省**关**(MDCG_UNIFY_QUERY,
|
|
18
|
+
2026-09-30 使用者裁定由「默认 "1"」翻为「未设即关、显式 "1" 才开」):显式开启时
|
|
19
|
+
检索入口把任意语言的 query 先归一成标准中文原子序列——英文 query 因此在**词法路**
|
|
20
|
+
上就能命中中文节点(与 en_zh_terms 默认关并不矛盾:一个在 query 侧归一,一个在召回
|
|
21
|
+
词扩展侧)。故「跨语词面零交集 ⇒ 召回必须归零」在**缺省态**(两个开关都不设)与
|
|
22
|
+
「两个开关都关」两种形态下都成立;命中只在显式 `MDCG_UNIFY_QUERY=1` 时断言。
|
|
23
|
+
三态都钉住(改口径必红)。
|
|
23
24
|
|
|
24
25
|
运行:python -m md_cg.test_en_pipeline
|
|
25
26
|
"""
|
|
@@ -143,14 +144,22 @@ try:
|
|
|
143
144
|
ids = [r[0].get("id") for r in res]
|
|
144
145
|
del os.environ["MDCG_EN_ATOMS"]
|
|
145
146
|
ok(len(res) >= 1 and ids[0] == "n_beef", "开启:英文 query top1 召回中文牛肉节点 %s" % ids)
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
147
|
+
# 2026-09-30 使用者裁定:统一归一层缺省由开翻为关(semantic/unify.py)。
|
|
148
|
+
# 故「两个开关都不设」= **两个都关** → 词面零交集,召回必须归零;
|
|
149
|
+
# 命中只在**显式** MDCG_UNIFY_QUERY=1 时才成立(英文对照路要用)。
|
|
150
|
+
# (旧断言「缺省态命中」对应的是 2026-09-23 的「默认开」口径,已随缺省翻转改写。)
|
|
150
151
|
res_u, _mu = cg.search("I ate beef yesterday", judge=False)
|
|
151
152
|
ids_u = [r[0].get("id") for r in res_u]
|
|
152
|
-
ok(
|
|
153
|
-
"
|
|
153
|
+
ok(not res_u or all(r[1] <= 0 for r in res_u),
|
|
154
|
+
"缺省(unify 缺省关 + EN_ATOMS 关):词面零重叠召回归零 %s" % ids_u)
|
|
155
|
+
os.environ["MDCG_UNIFY_QUERY"] = "1"
|
|
156
|
+
try:
|
|
157
|
+
res1, _m1 = cg.search("I ate beef yesterday", judge=False)
|
|
158
|
+
ids1 = [r[0].get("id") for r in res1]
|
|
159
|
+
finally:
|
|
160
|
+
os.environ.pop("MDCG_UNIFY_QUERY", None)
|
|
161
|
+
ok(len(res1) >= 1 and ids1[0] == "n_beef",
|
|
162
|
+
"显式 unify=1:英文 query 经统一归一后 top1 命中中文节点 %s" % ids1)
|
|
154
163
|
os.environ["MDCG_UNIFY_QUERY"] = "0"
|
|
155
164
|
try:
|
|
156
165
|
res0, _m0 = cg.search("I ate beef yesterday", judge=False) # 关归一+EN_ATOMS 关
|