@furongjun1999/dsh-memory 0.6.1 → 0.7.1

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.
Files changed (105) hide show
  1. package/README.md +49 -18
  2. package/docs/README.md +1 -0
  3. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
  4. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
  5. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
  6. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
  7. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
  8. package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
  9. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
  10. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
  11. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
  12. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
  13. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
  14. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
  15. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
  16. 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
  17. 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
  18. package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
  19. 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
  20. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
  21. 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
  22. 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
  23. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  24. 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
  25. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  26. 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
  27. package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
  28. 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
  29. package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
  30. package/md_cg/admission.py +718 -0
  31. package/md_cg/autonomy_modes.py +642 -0
  32. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  33. package/md_cg/chain.py +47 -0
  34. package/md_cg/consistency.py +133 -8
  35. package/md_cg/forgetting.py +38 -6
  36. package/md_cg/freshness.py +527 -0
  37. package/md_cg/generation.py +409 -0
  38. package/md_cg/hotcache.py +4 -1
  39. package/md_cg/lifecycle.py +30 -2
  40. package/md_cg/mcp_server.py +191 -26
  41. package/md_cg/mdcg.py +594 -39
  42. package/md_cg/mdcos.py +869 -38
  43. package/md_cg/nodefile.py +74 -1
  44. package/md_cg/protect.py +79 -4
  45. package/md_cg/provenance.py +1 -1
  46. package/md_cg/review_cli.py +31 -2
  47. package/md_cg/rollback.py +411 -0
  48. package/md_cg/rollback_cli.py +104 -0
  49. package/md_cg/semantic/canonical.py +22 -0
  50. package/md_cg/semantic/unify.py +73 -22
  51. package/md_cg/semantic/unify_fixture.json +25 -0
  52. package/md_cg/sleep.py +1297 -0
  53. package/md_cg/stg.py +400 -50
  54. package/md_cg/stgidx.py +281 -0
  55. package/md_cg/sustain.py +146 -9
  56. package/md_cg/test_auto_defaults.py +424 -0
  57. package/md_cg/test_autonomy_admission.py +1098 -0
  58. package/md_cg/test_autonomy_modes.py +1972 -0
  59. package/md_cg/test_b1_auto_id_multiproc.py +7 -0
  60. package/md_cg/test_b1b2_write_face.py +7 -0
  61. package/md_cg/test_b3_merge_keeps_content.py +7 -0
  62. package/md_cg/test_boundary_hit.py +410 -0
  63. package/md_cg/test_cons200_scan_selection.py +791 -0
  64. package/md_cg/test_en_pipeline.py +22 -13
  65. package/md_cg/test_generation_guard.py +352 -0
  66. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  67. package/md_cg/test_i50a_half_dup_defer.py +408 -0
  68. package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
  69. package/md_cg/test_i50c_meta_passthrough.py +608 -0
  70. package/md_cg/test_i50d_importance_source.py +531 -0
  71. package/md_cg/test_i50e_readside_protection.py +656 -0
  72. package/md_cg/test_issue39_utf8_stdio.py +9 -1
  73. package/md_cg/test_issue52_scan_condition_first.py +677 -0
  74. package/md_cg/test_linkref.py +7 -0
  75. package/md_cg/test_mode_parity.py +1393 -0
  76. package/md_cg/test_mutation_rollback.py +805 -0
  77. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
  78. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  79. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
  80. package/md_cg/test_n230_dirty_replay.py +375 -0
  81. package/md_cg/test_p2_mcp.py +8 -0
  82. package/md_cg/test_p2_six_elements.py +463 -0
  83. package/md_cg/test_p3_legacy_closure.py +433 -0
  84. package/md_cg/test_p4_freshness.py +680 -0
  85. package/md_cg/test_p8_subgraph_chain.py +14 -1
  86. package/md_cg/test_p9_forget_protect.py +7 -0
  87. package/md_cg/test_p9c_dedup_hints.py +7 -0
  88. package/md_cg/test_policy_required_ccg.py +5 -1
  89. package/md_cg/test_protocol.py +7 -0
  90. package/md_cg/test_rank_parity_score_mode.py +6 -0
  91. package/md_cg/test_semantic_canonical.py +5 -3
  92. package/md_cg/test_sleep.py +611 -0
  93. package/md_cg/test_sleep_p1.py +784 -0
  94. package/md_cg/test_stgidx_index_parity.py +1020 -0
  95. package/md_cg/test_time_core_lint.py +968 -0
  96. package/md_cg/test_unify_default_off.py +701 -0
  97. package/md_cg/test_unify_scope.py +182 -0
  98. package/md_cg/test_writelimit.py +36 -9
  99. package/md_cg/test_writepipe.py +5 -2
  100. package/md_cg/weights.py +15 -1
  101. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  102. package/md_cg/writepipe.py +101 -2
  103. package/package.json +3 -2
  104. package/skills/plugin.json +1 -1
  105. package/utf8_boot.py +237 -0
@@ -0,0 +1,433 @@
1
+ # -*- coding: utf-8 -*-
2
+ """P3 遗留四条闭环守卫(A 融合口径披露 / B S3 不变量读数 / C 缺省路集分叉 / D ④部分满足)。
3
+
4
+ 来源:设计稿 §5.4 / §6.2 / §6.3 与交接单「P3 遗留四条·必须在本批闭环」。
5
+ 四条各缺一条即不得收口,故**每条一个断言组**:
6
+
7
+ A **默认 recall 的融合口径由 sum 变 max**(`use_causal` 缺省 True)——
8
+ A1 判据单点 `mcp_server.recall_fusion_default` 的取值断言(缺省态 == "max",
9
+ 显式关因果 == None;`use_temporal` **不入判据**);
10
+ A2 同一确定性库上「默认 vs 显式 sum」的 top-k 读数**逐条不同**(分数与名次);
11
+ A3 默认调用点确实走该单点(源码断言:`fusion = a.get("fusion") or
12
+ recall_fusion_default(...)`)。
13
+ B **S3 不变量 ① 与 ⑥ 的读数缺口**——
14
+ B1 ①「种子取自过 S1/S2 门控的命中」:因果路**种子** ⊆ 已过门控的候选面
15
+ (`entries`),且因果路**输出** ⊆ 候选面(两条断言,实跑)。
16
+ B2 ⑥「不走全表」实测不成立 ⇒ 如实降级声明:`chain.adjacency` **不读正文**
17
+ (读盘调用计数 == 0),但邻接构建为 **O(N) 索引条目级**——给代价读数
18
+ (N 条目的冷建耗时),并断言文档/注释里**不再**把「不走全表」当作
19
+ 对 `adjacency` 成立的表述。
20
+ C **Rust 侧缺省路集分叉**(如实披露,**不得**声称对拍通过)——
21
+ Rust `EngineConfig::default{paths:None}` 字面量 == 四路;
22
+ Python `search_rrf` 缺省 == 六路;两者**不相等**(分叉登记)。
23
+ D **S3 ④ 只部分满足**——类型集可配(`MDCG_CHAIN_TYPES`)、权重沿用唯一表
24
+ `EDGE_WEIGHTS`、方向固定为依赖方向(出边)——逐条断言,并断言措辞已改准。
25
+
26
+ 运行:
27
+ python -X utf8 -m md_cg.test_p3_legacy_closure
28
+ python -X utf8 -m md_cg.test_p3_legacy_closure --mutate
29
+ python -X utf8 -m md_cg.test_p3_legacy_closure --mutate --list
30
+
31
+ 退出码:0 全绿;1 有失败 / 变异未按预期转红;2 ANCHOR-MISS。
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import inspect
36
+ import os
37
+ import re
38
+ import statistics
39
+ import sys
40
+ import tempfile
41
+ import time
42
+
43
+ _TMP0 = tempfile.mkdtemp(prefix="p3_legacy_")
44
+ os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP0, "auxroot")
45
+ os.environ["MDCG_MASTER_KEY"] = "34" * 32
46
+ os.environ["MDCG_TOKEN_FILE"] = os.path.join(_TMP0, "tokens.json")
47
+ os.environ["MDCG_ROOT"] = os.path.join(_TMP0, "cgroot")
48
+ for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
49
+ "MDCG_RETRIEVAL_PIPELINE", "MDCG_POOLING", "MDCG_FRESHNESS",
50
+ "MDCG_CHAIN_TYPES"):
51
+ os.environ.pop(_k, None)
52
+
53
+ _HERE = os.path.dirname(os.path.abspath(__file__))
54
+ _REPO = os.path.dirname(_HERE)
55
+ sys.path.insert(0, _REPO)
56
+
57
+ try:
58
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
59
+ except AttributeError:
60
+ pass
61
+
62
+ from md_cg import chain # noqa: E402
63
+ from md_cg import mcp_server as MS # noqa: E402
64
+ from md_cg import mdcg as M # noqa: E402
65
+ from md_cg import subgraph as SG # noqa: E402
66
+ from md_cg.mdcos import MdCGOS # noqa: E402
67
+
68
+ PASS = 0
69
+ FAIL = 0
70
+ FAILS = []
71
+
72
+
73
+ def check(name, cond, detail=""):
74
+ global PASS, FAIL
75
+ if cond:
76
+ PASS += 1
77
+ print(f" OK {name}")
78
+ else:
79
+ FAIL += 1
80
+ FAILS.append(name)
81
+ print(f" FAIL {name} {detail}")
82
+ return bool(cond)
83
+
84
+
85
+ def _src(rel):
86
+ with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
87
+ return f.read()
88
+
89
+
90
+ _CCG = ("# 功能名:{n}\n# 生效条件:无条件\n# 子功能:无\n# 执行:占位\n"
91
+ "# 验证方式:compiler 复现\n# 不适用条件:其它会话\n\n{t}\n")
92
+
93
+
94
+ def _fusion_lib():
95
+ """受控库:两路共识 vs 单路更好名次——sum 与 max 的 top-k 序列在此分开。"""
96
+ root = tempfile.mkdtemp(prefix="p3_fuse_")
97
+ cg = MdCGOS(root)
98
+ cg.add("m_aa", _CCG.format(n="m_aa", t="苹果 别的内容 章节 分离"),
99
+ layer="knowledge", tags=["苹果"], importance=0.5)
100
+ cg.add("m_zz", _CCG.format(n="m_zz", t="苹果 别的内容 章节 分离"),
101
+ layer="knowledge", tags=["苹果"], importance=0.9)
102
+ cg.add("m_lex", _CCG.format(n="m_lex", t="苹果章节 同构"),
103
+ layer="knowledge", importance=0.5)
104
+ cg.add("m_other", _CCG.format(n="m_other", t="苹果 别的内容 章节 分离 更多"),
105
+ layer="knowledge", importance=0.5)
106
+ cg.flush()
107
+ return root, cg
108
+
109
+
110
+ def _chain_lib(n_seed=3, n_other=6):
111
+ root = tempfile.mkdtemp(prefix="p3_chain_")
112
+ cg = MdCGOS(root)
113
+ for i in range(n_seed):
114
+ cg.add(f"s{i}", _CCG.format(n=f"s{i}", t="苹果 章节 种子"),
115
+ layer="knowledge", importance=0.5,
116
+ edges=[{"target": f"t{i}", "relation_type": "causal",
117
+ "confidence": 0.9}])
118
+ cg.add(f"t{i}", _CCG.format(n=f"t{i}", t="下游 结论"),
119
+ layer="knowledge", importance=0.5)
120
+ for i in range(n_other):
121
+ cg.add(f"o{i}", _CCG.format(n=f"o{i}", t="无关 其它"),
122
+ layer="knowledge", importance=0.5)
123
+ # 负记忆层节点:**不在** `_candidates` 的候选面(NEG_ROUTE_LAYERS)——
124
+ # 有它,不变量①「种子 ⊆ 已过门控的候选面」才有判别力
125
+ cg.add("rj1", _CCG.format(n="rj1", t="苹果 章节 被否决的假设"),
126
+ layer="rejected", importance=0.0)
127
+ cg.flush()
128
+ return root, cg
129
+
130
+
131
+ def ga_fusion():
132
+ print("== A 默认 recall 融合口径 sum→max(披露 + 判据 + 读数)==")
133
+ check("A1 判据单点:缺省(use_causal=True)→ max",
134
+ MS.recall_fusion_default(False, False, False, True) == "max",
135
+ str(MS.recall_fusion_default(False, False, False, True)))
136
+ check("A1 显式关因果且其余全关 → None(回到 search_rrf 的 sum 缺省)",
137
+ MS.recall_fusion_default(False, False, False, False) is None)
138
+ check("A1 use_temporal 不入判据(时间路是排名项,非单路独有召回型)",
139
+ MS.recall_fusion_default(False, False, False, False, True) is None
140
+ and MS.recall_fusion_default(False, False, False, True, True) == "max")
141
+ src = _src(os.path.join("md_cg", "mcp_server.py"))
142
+ check("A3 默认调用点走判据单点",
143
+ "fusion = a.get(\"fusion\") or recall_fusion_default(" in src)
144
+ check("A3 use_causal 缺省 True(披露的根因)",
145
+ "use_causal = True if use_causal is None else bool(use_causal)" in src)
146
+ root, cg = _fusion_lib()
147
+ try:
148
+ def top(**kw):
149
+ pack = MS._dispatch(cg, "mdcg_recall",
150
+ dict({"query": "苹果 章节", "k": 5}, **kw))
151
+ return [[it.get("id"), it.get("score")]
152
+ for it in (pack.get("pack") or [])]
153
+ d = top()
154
+ s = top(fusion="sum")
155
+ mx = top(fusion="max")
156
+ print(f" default={d}")
157
+ print(f" sum ={s}")
158
+ print(f" max ={mx}")
159
+ check("A2 默认口径 == max(逐条相同)", d == mx, f"{d} vs {mx}")
160
+ check("A2 默认口径 ≠ sum(逐条不同:分数或名次)", d != s, f"{d} vs {s}")
161
+ check("A2 top-1 名次在 sum/max 下不同(口径差可观测)",
162
+ bool(d) and bool(s) and d[0][0] != s[0][0],
163
+ f"{d[:1]} vs {s[:1]}")
164
+ finally:
165
+ cg.close()
166
+
167
+
168
+ def gb_s3_invariants():
169
+ print("== B S3 不变量 ①⑥ 读数(种子 ⊆ 门控面 / 邻接代价)==")
170
+ root, cg = _chain_lib()
171
+ try:
172
+ # ① 因果路种子 ⊆ 已过 S1/S2 门控的候选面(entries)
173
+ seen = {}
174
+ orig = chain.expand_from_seeds
175
+
176
+ def spy(cg_, seeds, **kw):
177
+ seen["seeds"] = list(seeds.items()) if isinstance(seeds, dict) \
178
+ else list(seeds or [])
179
+ return orig(cg_, seeds, **kw)
180
+
181
+ chain.expand_from_seeds = spy
182
+ try:
183
+ res, meta = cg.search_rrf("苹果 章节", k=8, record=False,
184
+ paths=("lexical", "chain"))
185
+ finally:
186
+ chain.expand_from_seeds = orig
187
+ ent_ids = {e["path"].split("/")[-1][:-3] for e in cg._candidates()}
188
+ seed_ids = {nid for nid, _s in (seen.get("seeds") or [])}
189
+ check("① 因果路种子非空(夹具前提)", bool(seed_ids), str(seen))
190
+ check("① 种子 ⊆ 已过门控的候选面(entries)",
191
+ seed_ids <= ent_ids, f"{seed_ids - ent_ids}")
192
+ out_ids = {r[0]["id"] for r in res}
193
+ check("① 因果路输出 ⊆ 候选面(不得引入未过门控的节点)",
194
+ out_ids <= ent_ids, f"{out_ids - ent_ids}")
195
+ check("① 因果路确实产出(下游 t* 被召回)",
196
+ any(i.startswith("t") for i in out_ids), str(sorted(out_ids)))
197
+
198
+ # ⑥ 邻接:不读正文(读盘计数 == 0)+ O(N) 索引条目级代价读数
199
+ calls = {"n": 0}
200
+ orig_read, orig_get = M.MdCG._read, M.MdCG.get
201
+
202
+ def rd(self, *a, **kw):
203
+ calls["n"] += 1
204
+ return orig_read(self, *a, **kw)
205
+
206
+ def gt(self, *a, **kw):
207
+ calls["n"] += 1
208
+ return orig_get(self, *a, **kw)
209
+
210
+ M.MdCG._read, M.MdCG.get = rd, gt
211
+ try:
212
+ cg._chain_adj = None
213
+ adj = chain.adjacency(cg)
214
+ finally:
215
+ M.MdCG._read, M.MdCG.get = orig_read, orig_get
216
+ check("⑥ 邻接构建不读正文/不读盘(读盘调用计数 == 0)",
217
+ calls["n"] == 0, f"reads={calls['n']}")
218
+ # 代价读数:N 条目冷建耗时(夹具 N=9;再取一个 400 条目的规模读)
219
+ big = tempfile.mkdtemp(prefix="p3_adj_")
220
+ cg2 = MdCGOS(big)
221
+ N = 400
222
+ for i in range(N):
223
+ cg2.add(f"n{i:04d}", _CCG.format(n=f"n{i:04d}", t="苹果 章节"),
224
+ layer="knowledge", importance=0.5,
225
+ edges=[{"target": f"n{(i + 1) % N:04d}",
226
+ "relation_type": "causal"}])
227
+ cg2.flush()
228
+ cg2._chain_adj = None
229
+ t0 = time.perf_counter()
230
+ adj2 = chain.adjacency(cg2)
231
+ dt = time.perf_counter() - t0
232
+ print(f" 代价读数:N={N} 条目,cold adjacency = {dt * 1000:.3f} ms"
233
+ f"(邻接源数 {len(adj2)})")
234
+ check("⑥ O(N) 条目级代价(400 条目冷建 < 200ms,宽松上界)",
235
+ dt < 0.2, f"{dt:.4f}s")
236
+ cg2.close()
237
+ # 文档/注释措辞:不得把「不走全表」当作对 adjacency 成立的表述
238
+ ch = _src(os.path.join("md_cg", "chain.py"))
239
+ check("⑥ chain.adjacency 注释已改为「不读正文;O(N) 索引条目级」",
240
+ "邻接构建为 O(N) 索引条目级" in ch)
241
+ ct = _src(os.path.join("docs", "hive", "检索路径与认知结构契约_v0.1.md"))
242
+ check("⑥ 契约文档已改正(含代价读数与可选硬化路径)",
243
+ "不读正文;\n 邻接构建为 O(N) 索引条目级" in ct
244
+ or "邻接构建为 O(N) 索引条目级" in ct)
245
+ check("⑥ 契约文档不再出现无条件的「不走全表」断言",
246
+ "扩散 N 跳、按跳数衰减;不走全表" not in ct)
247
+ finally:
248
+ cg.close()
249
+
250
+
251
+ def gc_lang_divergence():
252
+ print("== C 缺省路集分叉:Rust 四路 vs Python 六路(登记,非对拍通过)==")
253
+ eng = _src(os.path.join("rust", "src", "engine.rs"))
254
+ m = re.search(r"cfg\.paths\.clone\(\)\.unwrap_or_else\(\|\|\s*\{(.*?)\}\)",
255
+ eng, re.S)
256
+ check("C Rust 缺省 paths 字面量可解析", m is not None)
257
+ rust_paths = re.findall(r'"([a-z_]+)"\.to_string\(\)', m.group(1)) if m else []
258
+ check("C Rust 缺省 == 四路(lexical/bucket/entity/graph)",
259
+ rust_paths == ["lexical", "bucket", "entity", "graph"],
260
+ str(rust_paths))
261
+ py_default = inspect.signature(MdCGOS.search_rrf).parameters["paths"].default
262
+ check("C Python search_rrf 缺省 == 六路",
263
+ tuple(py_default) == ("lexical", "bucket", "entity", "graph",
264
+ "chain", "temporal"), str(py_default))
265
+ check("C 两侧缺省**不相等**(分叉为已知边界,不得声称对拍通过)",
266
+ set(rust_paths) != set(py_default), f"{rust_paths} vs {list(py_default)}")
267
+ check("C 契约文档已登记该分叉(§4.2)",
268
+ "读侧**语言面**分叉" in _src(os.path.join(
269
+ "docs", "hive", "检索路径与认知结构契约_v0.1.md")))
270
+ # mdcg_recall 缺省构造:paths=None(交给 mdcos 六路缺省)
271
+ ms = _src(os.path.join("md_cg", "mcp_server.py"))
272
+ check("C mdcg_recall 缺省把 paths 交给 mdcos(paths = None)",
273
+ "paths = None # 缺省六路" in ms)
274
+
275
+
276
+ def gd_s3_partial():
277
+ print("== D S3 ④ 部分满足:类型集可配 / 权重唯一表 / 方向固定 ==")
278
+ ch = _src(os.path.join("md_cg", "chain.py"))
279
+ check("D 类型集可配(env MDCG_CHAIN_TYPES)",
280
+ 'CHAIN_TYPES_ENV = "MDCG_CHAIN_TYPES"' in ch
281
+ and callable(chain.chain_types_from_env))
282
+ os.environ["MDCG_CHAIN_TYPES"] = "reference,causal"
283
+ try:
284
+ check("D 类型集生效且只含已登记类型",
285
+ chain.chain_types_from_env() == ("reference", "causal"),
286
+ str(chain.chain_types_from_env()))
287
+ finally:
288
+ os.environ.pop("MDCG_CHAIN_TYPES", None)
289
+ os.environ["MDCG_CHAIN_TYPES"] = "不存在的类型"
290
+ try:
291
+ check("D 非法类型集回落缺省(剔除即回落,不静默走未知边)",
292
+ chain.chain_types_from_env() == chain.CHAIN_TYPES_DEFAULT)
293
+ finally:
294
+ os.environ.pop("MDCG_CHAIN_TYPES", None)
295
+ check("D 权重沿用**唯一表** EDGE_WEIGHTS(无第二份边权表)",
296
+ "EDGE_WEIGHTS = {" in ch
297
+ and ch.count("EDGE_WEIGHTS.get(") >= 1
298
+ and "def edge_weight(edge)" in ch)
299
+ check("D 类型集只从 EDGE_WEIGHTS 里挑子集(t in EDGE_WEIGHTS)",
300
+ "if t in EDGE_WEIGHTS" in ch)
301
+ sig = inspect.signature(chain.walk).parameters
302
+ check("D 方向固定为依赖方向(出边):walk 缺省 direction == 'out'",
303
+ sig["direction"].default == "out", str(sig["direction"].default))
304
+ check("D 措辞已改准(docstring 写明「部分满足」与三件口径)",
305
+ "可配面边界" in ch and "**部分满足**" in ch
306
+ and "**方向**:固定为**依赖方向(出边)**" in ch
307
+ and "沿用**唯一表** `EDGE_WEIGHTS`" in ch)
308
+ ct = _src(os.path.join("docs", "hive", "检索路径与认知结构契约_v0.1.md"))
309
+ check("D 契约文档已把 S3 ④ 标为部分满足",
310
+ "**部分满足**" in ct and "边权数值" in ct)
311
+
312
+
313
+ # --------------------------------------------------------------------------
314
+ # 定点变异(只改本进程内存对象/临时输入,绝不改仓库文件)
315
+ # --------------------------------------------------------------------------
316
+
317
+ def _mut_fusion_causal_dropped():
318
+ orig = MS.recall_fusion_default
319
+
320
+ def no_causal(use_fuzzy, use_semantic, use_goal, use_causal,
321
+ use_temporal=False):
322
+ return "max" if (use_fuzzy or use_semantic or use_goal) else None
323
+ MS.recall_fusion_default = no_causal
324
+
325
+ def restore():
326
+ MS.recall_fusion_default = orig
327
+ return restore
328
+
329
+
330
+ def _mut_chain_seed_all():
331
+ """把因果路种子改成「全库节点」——不变量 ① 应当立刻变红。"""
332
+ orig = MdCGOS._path_chain
333
+
334
+ def polluted(self, query, entries, seeds, context=None, **kw):
335
+ # 未过门控的节点**排在前 8**(`_path_chain` 只取 seeds[:8])——
336
+ # 这正是「种子引入了未过 S1/S2 门控的节点」这一违规形态
337
+ ent = {id(e) for e in entries}
338
+ nodes = list((self.index.get("nodes") or {}).items())
339
+ allseeds = [({"id": nid}, 1.0) for nid, e in nodes
340
+ if id(e) not in ent]
341
+ allseeds += [({"id": nid}, 1.0) for nid, e in nodes if id(e) in ent]
342
+ return orig(self, query, entries, allseeds, context, **kw)
343
+ MdCGOS._path_chain = polluted
344
+
345
+ def restore():
346
+ MdCGOS._path_chain = orig
347
+ return restore
348
+
349
+
350
+ def _mut_adjacency_reads():
351
+ """让邻接构建读正文(走 cg.get)——「不读正文」断言应当变红。"""
352
+ orig = SG._fm
353
+
354
+ def slow(cg_, nid):
355
+ node = cg_.get(nid)
356
+ return (node or {}).get("frontmatter") or {}
357
+ SG._fm = slow
358
+
359
+ def restore():
360
+ SG._fm = orig
361
+ return restore
362
+
363
+
364
+ def _mut_py_default_paths():
365
+ """把 mdcos.search_rrf 的 paths 缺省改成四路(源码复制体上验证)。"""
366
+ orig = MdCGOS.search_rrf.__defaults__
367
+
368
+ def restore():
369
+ MdCGOS.search_rrf.__defaults__ = orig
370
+ args = list(orig)
371
+ for i, a in enumerate(args):
372
+ if isinstance(a, tuple) and "chain" in a:
373
+ args[i] = ("lexical", "bucket", "entity", "graph")
374
+ MdCGOS.search_rrf.__defaults__ = tuple(args)
375
+ return restore
376
+
377
+
378
+ _MUTATIONS = [
379
+ ("fusion_drop_causal", "融合判据丢掉 use_causal(缺省退化为 sum)",
380
+ _mut_fusion_causal_dropped),
381
+ ("chain_seeds_all", "因果路种子改成全库节点且未过门控者排前(不变量①应红)",
382
+ _mut_chain_seed_all),
383
+ ("adjacency_reads_body", "邻接构建改成读正文(不变量⑥应红)",
384
+ _mut_adjacency_reads),
385
+ ("py_default_four_paths", "Python 缺省路集改成四路(分叉登记应红)",
386
+ _mut_py_default_paths),
387
+ ]
388
+
389
+
390
+ def _collect():
391
+ global PASS, FAIL, FAILS
392
+ PASS, FAIL, FAILS = 0, 0, []
393
+ ga_fusion()
394
+ gb_s3_invariants()
395
+ gc_lang_divergence()
396
+ gd_s3_partial()
397
+ return list(FAILS)
398
+
399
+
400
+ def main(argv):
401
+ if "--mutate" in argv:
402
+ if "--list" in argv:
403
+ for name, desc, _fn in _MUTATIONS:
404
+ print(f" {name:24s} {desc}")
405
+ return 0
406
+ base = _collect()
407
+ if base:
408
+ print(f"\n!! 基线不绿({len(base)} 条):{base}")
409
+ return 1
410
+ bad = []
411
+ for name, desc, fn in _MUTATIONS:
412
+ try:
413
+ restore = fn()
414
+ except (AttributeError, StopIteration) as exc:
415
+ print(f" ANCHOR-MISS {name}: {exc}")
416
+ return 2
417
+ got = _collect()
418
+ restore()
419
+ if got:
420
+ print(f" MUTATE-OK {name}({desc})→ 转红 {len(got)} 条:{got[:3]}")
421
+ else:
422
+ bad.append(name)
423
+ print(f" MUTATE-FAIL {name}({desc})→ 仍全绿")
424
+ print(f"\ntest_p3_legacy_closure --mutate: "
425
+ f"{len(_MUTATIONS) - len(bad)}/{len(_MUTATIONS)} 变异按预期转红")
426
+ return 0 if not bad else 1
427
+ fails = _collect()
428
+ print(f"\ntest_p3_legacy_closure: {PASS} 通过 / {FAIL} 失败")
429
+ return 0 if not fails else 1
430
+
431
+
432
+ if __name__ == "__main__":
433
+ sys.exit(main(sys.argv[1:]))