@furongjun1999/dsh-memory 0.4.8 → 0.4.10

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 (232) hide show
  1. package/README.md +54 -26
  2. package/codebuddy/CODEBUDDY.md +196 -195
  3. package/codebuddy/README.md +13 -1
  4. package/codebuddy/mcp.json +9 -0
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
  6. package/docs/README.md +1 -1
  7. package/docs/discipline/harnesses.yaml +18 -7
  8. package/docs/discipline/templates/full.md.tmpl +4 -3
  9. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  10. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  11. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  12. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  13. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  14. package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
  15. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
  16. 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 +102 -0
  17. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
  18. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
  19. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -0
  20. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
  21. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  22. package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
  23. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -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 +47 -45
  25. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
  26. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
  27. package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
  28. package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
  29. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
  30. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
  31. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
  32. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +434 -433
  33. package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
  34. package/dsh/README.md +33 -0
  35. package/dsh/cordis.yml.example +13 -7
  36. package/dsh/hive-mcp-probe.mjs +94 -0
  37. package/dsh/hive-mcp.example.yml +62 -0
  38. package/dsh/update-lingshu.bat +11 -0
  39. package/dsh/update-lingshu.ps1 +337 -0
  40. package/lib/hooks.d.ts +3 -0
  41. package/lib/hooks.js +17 -23
  42. package/lib/index.d.ts +4 -2
  43. package/lib/index.js +29 -6
  44. package/lib/lib/datapath.d.ts +76 -1
  45. package/lib/lib/datapath.js +199 -13
  46. package/lib/lib/mdcg_client.d.ts +40 -3
  47. package/lib/lib/mdcg_client.js +46 -22
  48. package/lib/lib/mutual.js +4 -4
  49. package/lib/lib/token_store.js +4 -5
  50. package/md_cg/audit.py +17 -2
  51. package/md_cg/autonomy.py +86 -15
  52. package/md_cg/backfill.py +36 -1
  53. package/md_cg/backfill_bigdomain.py +34 -0
  54. package/md_cg/bench6_arms.py +28 -1
  55. package/md_cg/bench6_common.py +10 -1
  56. package/md_cg/bench6_competitors.py +6 -1
  57. package/md_cg/bench_axis_domain.py +9 -1
  58. package/md_cg/bench_blind_comp.py +7 -1
  59. package/md_cg/bench_en_atoms_public.py +9 -0
  60. package/md_cg/bench_governance.py +348 -0
  61. package/md_cg/bench_lme_zh.py +16 -1
  62. package/md_cg/bench_locomo.py +2 -1
  63. package/md_cg/bench_locomo_zh.py +16 -1
  64. package/md_cg/bench_locomo_zh_public.py +4 -1
  65. package/md_cg/bench_longmem.py +2 -1
  66. package/md_cg/bench_membench.py +27 -1
  67. package/md_cg/bench_p0.py +4 -1
  68. package/md_cg/bench_progressive.py +13 -1
  69. package/md_cg/bench_role_views.py +238 -0
  70. package/md_cg/bench_task_ab.py +8 -1
  71. package/md_cg/bench_task_ab_llm.py +13 -1
  72. package/md_cg/bench_unified_en.py +6 -1
  73. package/md_cg/bench_zh_mad.py +20 -1
  74. package/md_cg/blindspot_tickets.py +123 -0
  75. package/md_cg/branches.py +12 -1
  76. package/md_cg/build_postings.py +73 -0
  77. package/md_cg/ccgc.py +67 -2
  78. package/md_cg/census.py +5 -1
  79. package/md_cg/chain.py +24 -3
  80. package/md_cg/codeindex.py +134 -17
  81. package/md_cg/coldverify.py +265 -0
  82. package/md_cg/comment_gate.py +338 -0
  83. package/md_cg/cond_compose.py +190 -0
  84. package/md_cg/cond_facts.py +155 -0
  85. package/md_cg/cond_template.json +107 -0
  86. package/md_cg/condition_anchor.py +143 -0
  87. package/md_cg/conformance.py +69 -4
  88. package/md_cg/consistency.py +24 -1
  89. package/md_cg/consolidate.py +53 -2
  90. package/md_cg/corpus.py +4 -0
  91. package/md_cg/crosscheck.py +42 -2
  92. package/md_cg/crypto.py +35 -1
  93. package/md_cg/d_meta.py +310 -0
  94. package/md_cg/datapath.py +201 -26
  95. package/md_cg/docindex.py +122 -1
  96. package/md_cg/eval_common.py +29 -1
  97. package/md_cg/evidence.py +27 -1
  98. package/md_cg/evolution.py +21 -1
  99. package/md_cg/export.py +11 -1
  100. package/md_cg/forgetting.py +23 -1
  101. package/md_cg/fsutil.py +18 -1
  102. package/md_cg/hotcache.py +214 -0
  103. package/md_cg/hyperedge.py +251 -0
  104. package/md_cg/identity.py +18 -1
  105. package/md_cg/insight.py +17 -1
  106. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  107. package/md_cg/lexicon/build_standard_en.py +171 -168
  108. package/md_cg/lexicon/expand_en_zh.py +6 -0
  109. package/md_cg/lifecycle.py +12 -1
  110. package/md_cg/linkref.py +281 -0
  111. package/md_cg/links.py +29 -1
  112. package/md_cg/mcp_server.py +362 -43
  113. package/md_cg/md_whitebox.py +53 -1
  114. package/md_cg/mdcg.py +1003 -27
  115. package/md_cg/mdcos.py +558 -36
  116. package/md_cg/metacognition.py +37 -2
  117. package/md_cg/migrate.py +4 -0
  118. package/md_cg/migrate_aeis.py +221 -213
  119. package/md_cg/migrate_roleplay.py +8 -0
  120. package/md_cg/migrate_wisdom_graph.py +14 -1
  121. package/md_cg/mreview/__main__.py +3 -0
  122. package/md_cg/mreview/bundle.py +8 -0
  123. package/md_cg/mreview/candidates.py +9 -0
  124. package/md_cg/mreview/govern.py +21 -1
  125. package/md_cg/mreview/locate.py +34 -0
  126. package/md_cg/mreview/pipeline.py +29 -1
  127. package/md_cg/mreview/ruleset.py +16 -1
  128. package/md_cg/nodefile.py +233 -3
  129. package/md_cg/pooling.py +23 -1
  130. package/md_cg/postings.py +298 -0
  131. package/md_cg/predict.py +89 -9
  132. package/md_cg/progressive.py +3 -0
  133. package/md_cg/protect.py +14 -1
  134. package/md_cg/protocol.py +372 -0
  135. package/md_cg/provenance.py +262 -0
  136. package/md_cg/reach.py +453 -0
  137. package/md_cg/refindex.py +47 -2
  138. package/md_cg/refine.py +20 -1
  139. package/md_cg/roleviews.py +89 -0
  140. package/md_cg/routing.py +76 -0
  141. package/md_cg/scrub.py +63 -2
  142. package/md_cg/security.py +26 -1
  143. package/md_cg/self_state.py +64 -1
  144. package/md_cg/selfreport.py +151 -0
  145. package/md_cg/semantic/canonical.py +5 -0
  146. package/md_cg/semantic/en_normalizer.py +364 -355
  147. package/md_cg/semantic/zh_en_atoms.py +139 -136
  148. package/md_cg/signer.py +41 -1
  149. package/md_cg/sources.py +583 -547
  150. package/md_cg/statushdr.py +179 -0
  151. package/md_cg/stg.py +59 -18
  152. package/md_cg/subgraph.py +23 -0
  153. package/md_cg/sustain.py +56 -1
  154. package/md_cg/tasks.py +26 -2
  155. package/md_cg/test_autonomy.py +26 -0
  156. package/md_cg/test_bench_governance.py +102 -0
  157. package/md_cg/test_blindspot_tickets.py +166 -0
  158. package/md_cg/test_ccgc.py +10 -0
  159. package/md_cg/test_codeindex.py +338 -0
  160. package/md_cg/test_comment_gate.py +187 -0
  161. package/md_cg/test_cond_compose_anchors.py +76 -0
  162. package/md_cg/test_condition_anchor.py +82 -0
  163. package/md_cg/test_d_meta.py +412 -0
  164. package/md_cg/test_datapath_root.py +188 -0
  165. package/md_cg/test_gain_gate.py +47 -1
  166. package/md_cg/test_hot_cold.py +187 -0
  167. package/md_cg/test_hyperedge.py +245 -0
  168. package/md_cg/test_linkref.py +306 -0
  169. package/md_cg/test_md_access_parity.py +15 -3
  170. package/md_cg/test_mr_m1.py +108 -18
  171. package/md_cg/test_mr_m3.py +8 -1
  172. package/md_cg/test_p26_refindex.py +49 -20
  173. package/md_cg/test_p27_docindex.py +236 -2
  174. package/md_cg/test_p2_mcp.py +1 -1
  175. package/md_cg/test_p31_insight.py +24 -0
  176. package/md_cg/test_p44_md_whitebox.py +14 -1
  177. package/md_cg/test_protocol.py +243 -0
  178. package/md_cg/test_reach.py +378 -0
  179. package/md_cg/test_reach_keys.py +201 -0
  180. package/md_cg/test_reach_meta_exits.py +145 -0
  181. package/md_cg/test_read_clip.py +8 -4
  182. package/md_cg/test_retr_s1.py +340 -0
  183. package/md_cg/test_retr_s1b.py +209 -0
  184. package/md_cg/test_retr_s3.py +194 -0
  185. package/md_cg/test_retr_s4.py +163 -0
  186. package/md_cg/test_retr_s5.py +200 -0
  187. package/md_cg/test_retr_s6.py +157 -0
  188. package/md_cg/test_retr_s7.py +385 -0
  189. package/md_cg/test_retr_s8_time.py +316 -0
  190. package/md_cg/test_retr_s9_edges.py +286 -0
  191. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  192. package/md_cg/test_review_conformance.py +59 -2
  193. package/md_cg/test_role_views.py +354 -0
  194. package/md_cg/test_subproc_encoding.py +188 -0
  195. package/md_cg/test_trust.py +361 -0
  196. package/md_cg/test_units_poll.py +71 -0
  197. package/md_cg/test_v14_fixes.py +397 -0
  198. package/md_cg/test_validity_filter.py +280 -0
  199. package/md_cg/test_wisdom_md_store.py +7 -3
  200. package/md_cg/test_writepipe.py +5 -1
  201. package/md_cg/theory.py +16 -1
  202. package/md_cg/tokens.py +40 -8
  203. package/md_cg/tool_face.py +13 -2
  204. package/md_cg/trust.py +943 -0
  205. package/md_cg/twophase.py +12 -1
  206. package/md_cg/units.py +132 -10
  207. package/md_cg/vision_evidence.py +24 -1
  208. package/md_cg/weights.py +24 -1
  209. package/md_cg/whitebox.py +32 -1
  210. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  211. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  212. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  213. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  214. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  215. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  216. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  217. package/md_cg/writelimit.py +18 -4
  218. package/md_cg/writepipe.py +178 -7
  219. package/package.json +2 -2
  220. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  221. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  222. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  223. package/src/hooks.ts +17 -21
  224. package/src/index.ts +33 -6
  225. package/src/lib/datapath.ts +211 -13
  226. package/src/lib/mdcg_client.ts +64 -25
  227. package/src/lib/mutual.ts +411 -411
  228. package/src/lib/token_store.ts +4 -5
  229. package/zcode/AGENTS.md +196 -195
  230. package/zcode/README.md +4 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  232. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
@@ -0,0 +1,397 @@
1
+ # -*- coding: utf-8 -*-
2
+ """v14 第三方验证缺陷回归集(md_cg 侧:B / C / D / E / F / G / H)。
3
+
4
+ 来源:第三方独立验证报告 v14(基线 784e91f)§4 复现取证。本文件把该报告中
5
+ **可复跑的观测**固化为机械断言——每条断言对应报告里的一条缺陷,修复前红、
6
+ 修复后绿;任一回归即红灯。
7
+
8
+ 覆盖(与报告编号一一对应):
9
+ B `trust.propagate` 多跳失效传播被 `doubted` 中间节点整体截断(下游永不降级)
10
+ C `hotcache._query_key` 未覆盖 include_work/roles/paths/fusion/judge_ranking/
11
+ goal_text/query_expand/context/path_weights/recall_only/early_stop_threshold
12
+ → 跨口径串味(roles+include_work 可致资格泄漏)
13
+ D `coldverify._process` 未检查 `trust.set_state` 返回值 → 迁移被拒却上报
14
+ ok=True(静默撒谎),且 stats.errors 掩盖
15
+ E `md_cg/units.py` 自持一份 serve 判活口径(ts-only + 5s vs 权威三层 + 15s)
16
+ F 测试面非幂等:旁路执行造出 `_md_cg_wisdom_graph/` 空壳 → runner 由 SKIP 转 FAIL
17
+ G 超边幂等键 `node_id_for` 分隔符未转义/无长度前缀 → 相邻列搬移得同 id
18
+ H `bench_en_atoms_public.py` 文件尾换行
19
+
20
+ 运行:python -m md_cg.test_v14_fixes
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import importlib.util
25
+ import os
26
+ import shutil
27
+ import sys
28
+ import tempfile
29
+ import time
30
+
31
+ try:
32
+ from . import coldverify, hotcache, hyperedge, trust, units
33
+ from .mdcos import MdCGOS
34
+ except ImportError: # 直接脚本运行
35
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
36
+ from md_cg import coldverify, hotcache, hyperedge, trust, units
37
+ from md_cg.mdcos import MdCGOS
38
+
39
+ _REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
40
+
41
+ PASS, FAIL = 0, []
42
+
43
+
44
+ def ok(cond, label):
45
+ global PASS
46
+ if cond:
47
+ PASS += 1
48
+ print(" ok " + label)
49
+ else:
50
+ FAIL.append(label)
51
+ print(" FAIL " + label)
52
+
53
+
54
+ def phase(name):
55
+ print("--- " + name)
56
+
57
+
58
+ def _ccg(name, deps=None, body=""):
59
+ sub = ("依赖 " + "、".join(deps)) if deps else "无"
60
+ return ("# 功能名:%s\n# 生效条件:无条件\n# 子功能:%s\n# 执行:无\n"
61
+ "# 验证方式:test\n# 不适用条件:无\n\n%s %s" % (name, sub, name, body))
62
+
63
+
64
+ def _st(cg, nid):
65
+ return trust.state_of(cg.index["nodes"].get(nid) or {})
66
+
67
+
68
+ def _load_run_tests():
69
+ """加载 scripts/run_tests.py(非包,按文件路径加载)。"""
70
+ p = os.path.join(_REPO, "scripts", "run_tests.py")
71
+ spec = importlib.util.spec_from_file_location("_v14_run_tests", p)
72
+ mod = importlib.util.module_from_spec(spec)
73
+ spec.loader.exec_module(mod)
74
+ return mod
75
+
76
+
77
+ def _hb(jd, pid, age_ms):
78
+ import json
79
+ with open(os.path.join(jd, "_serve.json"), "w", encoding="utf-8") as f:
80
+ json.dump({"ts": time.time() * 1000 - age_ms, "pid": pid, "workers": 1}, f)
81
+
82
+
83
+ # ------------------------------------------------------------------ B
84
+ def phase_b(tmp):
85
+ phase("B trust.propagate 多跳传播须穿过 doubted 中间节点")
86
+ ids = {"a": "mem_v14b_a", "b": "mem_v14b_b",
87
+ "c": "mem_v14b_c", "d": "mem_v14b_d"}
88
+
89
+ # B1 对照(中间节点非 doubted):多跳正常
90
+ cg1 = MdCGOS(root=os.path.join(tmp, "b1"))
91
+ cg1.add(ids["a"], _ccg("上游甲"), layer="knowledge")
92
+ cg1.add(ids["b"], _ccg("中间乙", ["甲"]), layer="knowledge",
93
+ depends_on=[ids["a"]])
94
+ cg1.add(ids["c"], _ccg("下游丙", ["乙"]), layer="knowledge",
95
+ depends_on=[ids["b"]])
96
+ cg1.add(ids["d"], _ccg("末端丁", ["丙"]), layer="knowledge",
97
+ depends_on=[ids["c"]])
98
+ ok(ids["b"] in trust.dependents_index(cg1).get(ids["a"], []),
99
+ "B1 反查索引就位(甲 ← 乙)")
100
+ trust.set_state(cg1, ids["a"], "expired", reason="t", actor="t")
101
+ r1 = trust.propagate(cg1, apply=True)
102
+ ok([_st(cg1, ids[k]) for k in ("b", "c", "d")] == ["doubted"] * 3,
103
+ "B2 对照:中间节点非 doubted 时乙/丙/丁全被标 doubted(实得 %s)"
104
+ % [_st(cg1, ids[k]) for k in ("b", "c", "d")])
105
+ ok(r1.get("updated_count") == 3, "B2b updated_count=3(实得 %s)"
106
+ % r1.get("updated_count"))
107
+
108
+ # B3 初始 doubts 中间节点:下游仍须可达
109
+ cg2 = MdCGOS(root=os.path.join(tmp, "b2"))
110
+ for k, kw in (("a", {}), ("b", {"depends_on": [ids["a"]]}),
111
+ ("c", {"depends_on": [ids["b"]]}),
112
+ ("d", {"depends_on": [ids["c"]]})):
113
+ cg2.add(ids[k], _ccg("甲" + k), layer="knowledge", **kw)
114
+ trust.set_state(cg2, ids["a"], "expired", reason="t", actor="t")
115
+ trust.set_state(cg2, ids["b"], "doubted", reason="t", actor="t")
116
+ r2 = trust.propagate(cg2, apply=True)
117
+ ok([_st(cg2, ids[k]) for k in ("c", "d")] == ["doubted", "doubted"],
118
+ "B3 中间节点已 doubted 时下游丙/丁仍被标 doubted(实得 %s)"
119
+ % [_st(cg2, ids[k]) for k in ("c", "d")])
120
+ ok(r2.get("updated_count") == 2, "B3b updated_count=2(实得 %s)"
121
+ % r2.get("updated_count"))
122
+ ok(ids["b"] not in (r2.get("updated") or []),
123
+ "B3c doubted 中间节点不被重复标记(幂等)")
124
+
125
+ # B4 真实序列:先 mark_dependents(预演直接下游)再 propagate
126
+ cg3 = MdCGOS(root=os.path.join(tmp, "b3"))
127
+ for k, kw in (("a", {}), ("b", {"depends_on": [ids["a"]]}),
128
+ ("c", {"depends_on": [ids["b"]]}),
129
+ ("d", {"depends_on": [ids["c"]]})):
130
+ cg3.add(ids[k], _ccg("乙" + k), layer="knowledge", **kw)
131
+ trust.set_state(cg3, ids["a"], "expired", reason="t", actor="t")
132
+ trust.mark_dependents(cg3, ids["a"])
133
+ ok(_st(cg3, ids["b"]) == "doubted", "B4 一跳标记生效(乙=doubted)")
134
+ r3 = trust.propagate(cg3, apply=True)
135
+ ok([_st(cg3, ids[k]) for k in ("c", "d")] == ["doubted", "doubted"],
136
+ "B5 真实序列(先 mark_dependents 再 propagate)多跳不失效(实得 %s)"
137
+ % [_st(cg3, ids[k]) for k in ("c", "d")])
138
+ ok(r3.get("updated_count") == 2, "B5b updated_count=2(实得 %s)"
139
+ % r3.get("updated_count"))
140
+ for cg in (cg1, cg2, cg3):
141
+ cg.close()
142
+
143
+
144
+ # ------------------------------------------------------------------ C
145
+ def phase_c(tmp):
146
+ phase("C hotcache query 键覆盖全部影响结果的参数")
147
+ cg = MdCGOS(root=os.path.join(tmp, "c"))
148
+ hotcache.attach(cg)
149
+ kw = "v14cache口径"
150
+ good, work = "mem_v14c_good", "mem_v14c_work"
151
+ cg.add(good, _ccg("常规记忆", body=kw), layer="knowledge")
152
+ cg.add(work, _ccg("工具输出", body=kw), layer="knowledge",
153
+ role="tool-output")
154
+
155
+ q = kw
156
+ ids = lambda rs: [x[0]["id"] for x in rs] # noqa: E731
157
+
158
+ # C1-C2:**先**以 include_work 口径查询(污染候选规则)
159
+ r1, m1 = cg.search_rrf(q, k=5, include_work=True)
160
+ ok(m1.get("cached") is not True, "C1 include_work 首次查询非缓存")
161
+ ok(work in ids(r1), "C2 include_work=True 时工作角色节点出现(该出的要出)")
162
+
163
+ # C3-C4:**随后的首次默认查询**不得复用该口径(报告 C10 资格泄漏)
164
+ r2, m2 = cg.search_rrf(q, k=5)
165
+ ok(work not in ids(r2), "C3 默认查询不得返回工作角色节点(无资格泄漏)")
166
+ ok(m2.get("cached") is not True,
167
+ "C4 默认查询不得复用 include_work 口径缓存(报告 C9/C10)")
168
+
169
+ # C5:默认口径自身缓存仍工作(修法不是「一律不缓存」)
170
+ r3, m3 = cg.search_rrf(q, k=5)
171
+ ok(work not in ids(r3) and m3.get("cached") is True,
172
+ "C5 默认口径重复查询命中缓存且结果不含工作角色")
173
+
174
+ r4, m4 = cg.search_rrf(q, k=5, paths=("bucket",)) # noqa: F841
175
+ r5, m5 = cg.search_rrf(q, k=5, paths=("lexical",))
176
+ ok(m5.get("cached") is not True, "C6 paths 不同口径不得互相命中(报告 C12)")
177
+ ok("bucket" not in (m5.get("paths") or {}),
178
+ "C7 paths=('lexical',) 的结果不含 bucket 路(实得 %s)"
179
+ % sorted((m5.get("paths") or {}).keys()))
180
+
181
+ r6, m6 = cg.search_rrf(q, k=5, fusion="max") # noqa: F841
182
+ ok(m6.get("cached") is not True, "C8 fusion 不同口径不得互相命中")
183
+
184
+ r7, m7 = cg.search_rrf(q, k=5, judge_ranking=True) # noqa: F841
185
+ ok(m7.get("cached") is not True, "C9 judge_ranking 不同口径不得互相命中")
186
+
187
+ r8, m8 = cg.search_rrf(q, k=5, roles=["tool-output"]) # noqa: F841
188
+ ok(m8.get("cached") is not True, "C10 roles 不同口径不得互相命中")
189
+
190
+ r9, m9 = cg.search_rrf( # noqa: F841
191
+ q, k=5, paths=("fuzzy",),
192
+ query_expand=lambda _q: {"__source__": "test", kw: 1.0})
193
+ ok(m9.get("cached") is not True,
194
+ "C11 query_expand(自定义可调用)不写不读缓存(无法稳定进键即绕行)")
195
+ _, m9b = cg.search_rrf(
196
+ q, k=5, paths=("fuzzy",),
197
+ query_expand=lambda _q: {"__source__": "test", kw: 1.0})
198
+ ok(m9b.get("cached") is not True, "C11b 绕行为持续生效(非一次性)")
199
+
200
+ r10, m10 = cg.search_rrf(q, k=5, goal_text="完全不同目标向量", # noqa: F841
201
+ paths=("goal",))
202
+ ok(m10.get("cached") is not True, "C12 goal_text 不同口径不得互相命中")
203
+
204
+ # C13-C15 对照(报告 C13):时间算子启用时读写双侧绕行缓存
205
+ cg.add("mem_v14c_time", _ccg("时间轴记忆", body=kw), layer="knowledge")
206
+ _, mt1 = cg.search_rrf(q, k=5, start_time=0.0, end_time=time.time() + 1e9)
207
+ ok(mt1.get("cached") is not True, "C13 时间算子启用时不写不读缓存(对照)")
208
+ _, mt2 = cg.search_rrf(q, k=5, start_time=0.0, end_time=time.time() + 1e9)
209
+ ok(mt2.get("cached") is not True, "C14 时间算子重复查询仍不命中缓存(对照)")
210
+ rt3, _mt3 = cg.search_rrf(q, k=5)
211
+ ok(work not in ids(rt3),
212
+ "C15 时间算子绕行后默认口径结果不变(无工作角色混入)")
213
+
214
+ # C16 同口径确实仍能命中(修法不是「一律不缓存」)
215
+ _, mh1 = cg.search_rrf(q, k=5)
216
+ _, mh2 = cg.search_rrf(q, k=5)
217
+ ok(mh2.get("cached") is True,
218
+ "C16 同口径重复查询仍命中缓存(保留热路径收益)")
219
+ cg.close()
220
+
221
+
222
+ # ------------------------------------------------------------------ D
223
+ def phase_d(tmp):
224
+ phase("D coldverify 非法迁移须如实报错(不得静默撒谎)")
225
+ cg = MdCGOS(root=os.path.join(tmp, "d"))
226
+ coldverify.attach(cg)
227
+ q = coldverify.get(cg)
228
+
229
+ # D1 doubted + 已过期 → doubted→expired 被 TRANSITIONS 拒绝
230
+ n1 = "mem_v14d_expired_doubted"
231
+ cg.add(n1, _ccg("存疑且过期"), layer="knowledge",
232
+ valid_from="2020-01-01", valid_until="2021-01-01")
233
+ trust.set_state(cg, n1, "doubted", reason="t", actor="t")
234
+ coldverify.enqueue(cg, n1, action="reverify")
235
+ r1 = q.drain(cg)
236
+ ok(r1 and r1[0].get("ok") is False,
237
+ "D1 非法迁移(doubted→expired)上报 ok=False(报告 V6)")
238
+ ok(_st(cg, n1) == "doubted", "D2 节点状态未被迁移(仍 doubted)")
239
+ ok((r1[0].get("error") or ""), "D3 失败原因如实透出(error 非空)")
240
+ ok(q.status()["stats"]["errors"] >= 1,
241
+ "D4 stats.errors 计入(不再掩盖,报告 V7b)")
242
+
243
+ # D5 verified + 未生效 → verified→unverified 被拒绝
244
+ n2 = "mem_v14d_notyet_verified"
245
+ cg.add(n2, _ccg("已验证但未生效"), layer="knowledge",
246
+ valid_from="2099-01-01")
247
+ trust.set_state(cg, n2, "verified", reason="t", actor="t")
248
+ coldverify.enqueue(cg, n2, action="reverify")
249
+ r2 = q.drain(cg)
250
+ ok(r2 and r2[0].get("ok") is False,
251
+ "D5 非法迁移(verified→unverified)上报 ok=False(报告 V7)")
252
+ ok(_st(cg, n2) == "verified", "D6 节点状态未被迁移(仍 verified)")
253
+
254
+ # D7 合法迁移仍正常(修法不是「一律报错」)
255
+ n3 = "mem_v14d_ok"
256
+ cg.add(n3, _ccg("已验证且过期"), layer="knowledge",
257
+ valid_from="2020-01-01", valid_until="2021-01-01")
258
+ trust.set_state(cg, n3, "verified", reason="t", actor="t")
259
+ coldverify.enqueue(cg, n3, action="reverify")
260
+ r3 = q.drain(cg)
261
+ ok(r3 and r3[0].get("ok") is True and _st(cg, n3) == "expired",
262
+ "D7 合法迁移(verified→expired)仍正常(实得 %s / %s)"
263
+ % (r3[0].get("ok") if r3 else None, _st(cg, n3)))
264
+ cg.close()
265
+
266
+
267
+ # ------------------------------------------------------------------ E
268
+ def phase_e(tmp):
269
+ phase("E units 判活口径须与权威三层同口径")
270
+ import hive.serve_start as serve_start
271
+
272
+ ok(float(units.FRESH_S) == float(serve_start.FRESH_S),
273
+ "E1 units.FRESH_S 与 serve_start.FRESH_S 同值(实得 %s vs %s)"
274
+ % (units.FRESH_S, serve_start.FRESH_S))
275
+ ok(float(units.FRESH_S) == 15.0, "E2 阈值为权威 15s(实得 %s)"
276
+ % units.FRESH_S)
277
+ src = open(units.__file__, encoding="utf-8").read()
278
+ ok('"pid_alive":' in src and '"pid_is_self_program":' in src,
279
+ "E3 units 透出三层判据明细(新鲜/pid 存活/pid 身份,可审计)")
280
+ ok("def pid_is_self_program" in src, "E4 units 有身份层判据")
281
+ ok("(同 _serve_alive)" not in src,
282
+ "E5 契约注释不再自称「同 _serve_alive」(该断言曾在 v13 后失效)")
283
+
284
+ jd = os.path.join(tmp, "hive_jobs")
285
+ os.makedirs(jd, exist_ok=True)
286
+ cases = [("新鲜+本进程+非 hive 映像", os.getpid(), 0),
287
+ ("同类心跳 age=8s", os.getpid(), 8000),
288
+ ("同类心跳 age=20s", os.getpid(), 20000),
289
+ ("pid 不存在", 999999, 0),
290
+ ("pid 不存在且陈旧", 999999, 60000)]
291
+ for label, pid, age in cases:
292
+ _hb(jd, pid, age)
293
+ a = bool(units.serve_state(jd)["alive"])
294
+ b = bool(serve_start.serve_alive(jd))
295
+ ok(a == b, "E6 %s:units=%s / serve_start=%s(须同结论)"
296
+ % (label, a, b))
297
+ with open(os.path.join(jd, "_serve.json"), "w", encoding="utf-8") as f:
298
+ f.write("{ 坏 json")
299
+ ok(bool(units.serve_state(jd)["alive"]) is False,
300
+ "E7 心跳损坏时不判活且不抛")
301
+ os.remove(os.path.join(jd, "_serve.json"))
302
+ ok(bool(units.serve_state(jd)["alive"]) is False, "E8 无心跳时不判活")
303
+
304
+
305
+ # ------------------------------------------------------------------ F
306
+ def phase_f(tmp):
307
+ phase("F 测试面幂等:空壳目录不算依赖就绪")
308
+ from . import md_whitebox
309
+
310
+ cr = getattr(md_whitebox, "corpus_ready", None)
311
+ if not callable(cr):
312
+ ok(False, "F1 corpus_ready 判据未提供(修复前:只判目录存在)")
313
+ else:
314
+ shell = os.path.join(tmp, "shell")
315
+ os.makedirs(shell)
316
+ ok(cr(shell) is False, "F1 空壳目录不算语料就绪")
317
+
318
+ real = os.path.join(tmp, "real", "knowledge")
319
+ os.makedirs(real)
320
+ with open(os.path.join(real, "n.md"), "w", encoding="utf-8") as f:
321
+ f.write("---\nid: x\n---\n正文\n")
322
+ ok(cr(os.path.join(tmp, "real")) is True, "F2 含 md 的目录算就绪")
323
+
324
+ missing = os.path.join(tmp, "missing_corpus")
325
+ try:
326
+ md_whitebox.build_db_from_md(root=missing, verbose=False)
327
+ ok(False, "F3 语料缺失须 fail-closed")
328
+ except FileNotFoundError:
329
+ ok(True, "F3 语料缺失 fail-closed(FileNotFoundError)")
330
+ except Exception as exc: # noqa: BLE001
331
+ ok(False, "F3 语料缺失 fail-closed(实得 %s: %s)"
332
+ % (type(exc).__name__, exc))
333
+ ok(not os.path.exists(missing), "F4 fail-closed 不产生空壳目录副作用")
334
+
335
+ rt = _load_run_tests()
336
+ old_repo = rt._REPO
337
+ try:
338
+ rt._REPO = tmp
339
+ sh = os.path.join(tmp, "_md_cg_wisdom_graph")
340
+ os.makedirs(sh)
341
+ ok(rt._dep_mdroot() is not None,
342
+ "F5 runner:空壳 _md_cg_wisdom_graph 不等于依赖就绪")
343
+ with open(os.path.join(sh, "n.md"), "w", encoding="utf-8") as f:
344
+ f.write("---\nid: x\n---\n")
345
+ ok(rt._dep_mdroot() is None, "F6 runner:含 md 时依赖就绪")
346
+ finally:
347
+ rt._REPO = old_repo
348
+
349
+ for rel in ("md_cg/test_md_access_parity.py",
350
+ "md_cg/test_p44_md_whitebox.py",
351
+ "md_cg/test_wisdom_md_store.py"):
352
+ txt = open(os.path.join(_REPO, rel), encoding="utf-8").read()
353
+ ok("SKIP" in txt,
354
+ "F7 %s 自带依赖缺失自辩(SKIP 出口)" % os.path.basename(rel))
355
+
356
+
357
+ # ------------------------------------------------------------------ G / H
358
+ def phase_g():
359
+ phase("G 超边幂等键无歧义(长度前缀)")
360
+ base = {"received_at": "t", "verify_id": "a", "commit": "b",
361
+ "verdict": "v", "receipt_job": "j", "iter_id": "i"}
362
+ row1 = dict(base, commit="b\x1fc")
363
+ row2 = dict(base, verify_id="a\x1fb", commit="c")
364
+ ok(hyperedge.node_id_for(row1) != hyperedge.node_id_for(row2),
365
+ "G1 相邻列搬移含 \\x1f 的文本得不同 id(报告 H17)")
366
+ ok(hyperedge.node_id_for(row1) == hyperedge.node_id_for(dict(row1)),
367
+ "G2 同一台账行恒同 id(幂等保持)")
368
+ ok(hyperedge.node_id_for(base).startswith("he_"), "G3 id 前缀不变")
369
+ bad = dict(base, commit="c")
370
+ ok(hyperedge.node_id_for(base) != hyperedge.node_id_for(bad),
371
+ "G4 无分隔符对照组 id 不同(问题是规范化而非哈希碰撞)")
372
+
373
+ phase("H 卫生:文件尾换行")
374
+ p = os.path.join(_REPO, "md_cg", "bench_en_atoms_public.py")
375
+ with open(p, "rb") as f:
376
+ ok(f.read().endswith(b"\n"), "H1 bench_en_atoms_public.py 尾换行在位")
377
+
378
+
379
+ def main():
380
+ tmp = tempfile.mkdtemp(prefix="mdcg_v14_")
381
+ try:
382
+ phase_b(tmp)
383
+ phase_c(tmp)
384
+ phase_d(tmp)
385
+ phase_e(tmp)
386
+ phase_f(tmp)
387
+ phase_g()
388
+ finally:
389
+ shutil.rmtree(tmp, ignore_errors=True)
390
+ print("\nPASS=%d FAIL=%d" % (PASS, len(FAIL)))
391
+ if FAIL:
392
+ for f in FAIL:
393
+ print(" - " + f)
394
+ sys.exit(1)
395
+
396
+
397
+ main()
@@ -0,0 +1,280 @@
1
+ # -*- coding: utf-8 -*-
2
+ """查询侧时效过滤(validity)验收 · GBrain 可借鉴点交接 §3②
3
+
4
+ 【裁决】使用者三处拍板,是本文件全部断言的判据来源:
5
+ ① 候选面**只排已过期**(valid_until 已过);**未生效(valid_from 未到)保留**。
6
+ 依据仓内既有纪律:`scrub` 模块「valid_from 绝不并入 _EXPIRY_KEYS」——
7
+ `not_yet`(尚未开始)与 `expired`(已经失效)语义相反;预约/计划类记忆
8
+ 在生效前仍应可召回,故不得用「时间轴上不活跃」一刀切。
9
+ ② 启用默认值 = **opt-in 默认关**(缺省不过滤):零 rust 镜像义务(保 py↔rust
10
+ 零漂移硬承诺)、零 bench 漂移、完全可逆。
11
+ ③ 过滤落在**候选层**(一处过滤全链路生效:search / search_rrf / recall),
12
+ 与 session / branch 过滤同构;MCP 面三入口透传。
13
+
14
+ 【覆盖】
15
+ ① `trust.is_expired` 布尔语义(四态→布尔;端点不可解析不猜测)+ scrub 纪律交叉守卫
16
+ ② 候选面:MdCG.search(裸核心)/ MdCGSecure.search(角色面)
17
+ ③ 默认关:缺省口径与启用口径逐条对照(opt-in 可逆性)
18
+ ④ search_rrf:融合面 + query 热缓存分键(validity 不入键会串口径)
19
+ ⑤ recall:RRF 分支 + 非 RRF 分支
20
+ ⑥ 图扩展:零词面交集节点经 edges 扩散可达时同样受 validity 约束
21
+ ⑦ MCP 面三入口透传(cg op=read / mdcg_search / mdcg_recall)
22
+
23
+ 【不适用】写入侧(add 落 valid_from/valid_until 是本项前提,非本项范围);
24
+ 库内真实数据面(当前 13514 节点零有效期,故本项为 opt-in 零漂移)。
25
+ """
26
+ import os
27
+ import shutil
28
+ import sys
29
+ import tempfile
30
+ import traceback
31
+
32
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
+
34
+ # Windows cmd 默认 GBK:测试自带 UTF-8 兜底,不依赖调用方记得加 -X utf8。
35
+ if hasattr(sys.stdout, "reconfigure"):
36
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
37
+
38
+ from md_cg import hotcache, mcp_server as MS, scrub, trust
39
+ from md_cg.mdcg import MdCG
40
+ from md_cg.mdcos import MdCGSecure
41
+ from md_cg.security import DEFAULT_SENSITIVITY, Principal
42
+
43
+ _ok = 0
44
+ _bad = []
45
+
46
+
47
+ def check(name, cond, detail=""):
48
+ global _ok
49
+ if cond:
50
+ _ok += 1
51
+ print("[PASS] " + name)
52
+ else:
53
+ _bad.append(name)
54
+ print("[FAIL] %s · %s" % (name, str(detail)[:220]))
55
+
56
+
57
+ Q = "量子咖啡机萃取压力曲线" # 罕见词:测试库内确定性命中
58
+ PAST, FUT = "2001-01-01", "2099-01-01"
59
+
60
+
61
+ def _princ():
62
+ return Principal(actor="t_validity", clearance=DEFAULT_SENSITIVITY,
63
+ can_write=True, can_admin=True, role="designer")
64
+
65
+
66
+ def _ids(res):
67
+ """命中集 id 清单。
68
+
69
+ 口径=**nid 是否在命中集**,而非「结果为空」——全扫兜底返回低分节点
70
+ 是检索器既有行为,不属过滤失败。
71
+ """
72
+ out = []
73
+ for r in res or []:
74
+ n = r[0] if isinstance(r, tuple) else r
75
+ if isinstance(n, dict) and isinstance(n.get("node"), dict):
76
+ n = n["node"] # _cg_call(op=read) 的 {"node": view} 包装
77
+ out.append((n or {}).get("id"))
78
+ return out
79
+
80
+
81
+ def _seed(cg):
82
+ """三态语料:expired / not_yet / unknown + 一条图扩展专用的 expired。"""
83
+ cg.add("mem_now", Q + " 现行规格 9 巴", layer="knowledge",
84
+ verification_basis="test", consistency=False)
85
+ cg.add("mem_old", Q + " 旧版规格已被取代", layer="knowledge",
86
+ verification_basis="test", consistency=False, valid_until=PAST)
87
+ cg.add("mem_future", Q + " 下一代规格预告", layer="knowledge",
88
+ verification_basis="test", consistency=False, valid_from=FUT)
89
+ cg.add("mem_hub", Q + " 主档索引", layer="knowledge",
90
+ verification_basis="test", consistency=False,
91
+ edges=[{"target": "mem_ghost", "relation_type": "causal"}])
92
+ # 词面与 Q 零交集:只能经图扩展(edges)抵达 —— 图扩展绕过候选层时的靶子
93
+ cg.add("mem_ghost", "旁系条目与提问没有任何词面重合", layer="knowledge",
94
+ verification_basis="test", consistency=False, valid_until=PAST)
95
+
96
+
97
+ # ---------------------------------------------------------------- ① 纯函数
98
+ def _t1():
99
+ check("①expired → True", trust.is_expired({"valid_until": PAST}) is True)
100
+ check("①not_yet 不算过期(语义相反纪律)",
101
+ trust.is_expired({"valid_from": FUT}) is False)
102
+ check("①active → False",
103
+ trust.is_expired({"valid_from": "2020-01-01",
104
+ "valid_until": "2030-01-01"}) is False)
105
+ check("①unknown(无时间轴)→ False", trust.is_expired({}) is False)
106
+ check("①None 入参 → False", trust.is_expired(None) is False)
107
+ check("①端点不可解析 → 不猜测(False,不误判为过期)",
108
+ trust.is_expired({"valid_until": "不是时间"}) is False)
109
+ check("①now 显式传入生效",
110
+ trust.is_expired({"valid_until": "2030-01-01"},
111
+ now=trust.parse_time("2035-01-01")) is True)
112
+ check("①判定序:未来起点优先(not_yet 而非 expired)",
113
+ trust.validity({"valid_from": FUT, "valid_until": PAST})[0] == "not_yet")
114
+ check("①scrub 纪律交叉守卫:valid_from 绝不并入 _EXPIRY_KEYS",
115
+ "valid_from" not in scrub._EXPIRY_KEYS
116
+ and "valid_until" in scrub._EXPIRY_KEYS
117
+ and "valid_from" in scrub._NOT_YET_KEYS,
118
+ str(scrub._EXPIRY_KEYS))
119
+ # ---- 2026-09-19 阶段一:规范键族 + 第三类语义(信念时间)交叉守卫 ----
120
+ check("①阶段一:effective_from 属未生效族、绝不入已结束族",
121
+ "effective_from" in scrub._NOT_YET_KEYS
122
+ and "effective_from" not in scrub._EXPIRY_KEYS,
123
+ str(scrub._EXPIRY_KEYS))
124
+ check("①阶段一:effective_until/expired_at 属已结束族、不入未生效族",
125
+ "effective_until" in scrub._EXPIRY_KEYS
126
+ and "expired_at" in scrub._EXPIRY_KEYS
127
+ and "effective_until" not in scrub._NOT_YET_KEYS
128
+ and "expired_at" not in scrub._NOT_YET_KEYS,
129
+ str(scrub._EXPIRY_KEYS))
130
+ check("①阶段一:believed_at 两族都不入(第三类语义·物理隔离)",
131
+ trust.BELIEVED_FIELD not in scrub._EXPIRY_KEYS
132
+ and trust.BELIEVED_FIELD not in scrub._NOT_YET_KEYS,
133
+ str(scrub._EXPIRY_KEYS))
134
+ check("①键族同步守卫:trust 别名元组 ⊆ scrub 对应键族(两侧禁各自漂移)",
135
+ set(trust.FROM_ALIASES) <= set(scrub._NOT_YET_KEYS)
136
+ and set(trust.UNTIL_ALIASES) <= set(scrub._EXPIRY_KEYS),
137
+ str(trust.FROM_ALIASES) + " / " + str(scrub._NOT_YET_KEYS))
138
+ # ---- 规范键优先、别名回落(双轨读取)----
139
+ check("①规范键优先:effective_from 压过 valid_from",
140
+ trust.validity({"effective_from": FUT,
141
+ "valid_from": "2020-01-01"})[0] == "not_yet")
142
+ check("①别名回落:仅历史键时照常判定(存量兼容)",
143
+ trust.validity({"valid_from": FUT})[0] == "not_yet")
144
+ check("①规范键不可解析 → 继续回落别名(不因写坏一键而漏判)",
145
+ trust.validity({"effective_until": "坏值",
146
+ "valid_until": PAST})[0] == "expired")
147
+ check("①believed_at 不参与时效判定(物理隔离·绝不误判)",
148
+ trust.validity({"believed_at": PAST})[0] == "unknown"
149
+ and trust.is_expired({"believed_at": PAST}) is False)
150
+ check("①believed_at 读取器:epoch 值 / 缺字段 None",
151
+ trust.believed_at({"believed_at": PAST}) is not None
152
+ and trust.believed_at({}) is None)
153
+ check("①time_window_msg 点名实际命中键(规范名/历史名各归其位)",
154
+ "effective_from" in trust.time_window_msg({"effective_from": FUT})
155
+ and "valid_from" in trust.time_window_msg({"valid_from": FUT}))
156
+
157
+
158
+ # ------------------------------------------------- ②③ 候选面 + 默认关
159
+ def _t2(root_core, root_sec):
160
+ core = MdCG(root_core)
161
+ _seed(core)
162
+
163
+ off = _ids(core.search(Q, k=20)[0])
164
+ check("③MdCG.search 默认关:expired 可见(opt-in 可逆)",
165
+ "mem_old" in off, off)
166
+ check("③MdCG.search 默认关:not_yet 可见", "mem_future" in off, off)
167
+ on = _ids(core.search(Q, k=20, validity=True)[0])
168
+ check("②MdCG.search 启用后 expired 剔除", "mem_old" not in on, on)
169
+ check("②MdCG.search 启用后 not_yet 保留", "mem_future" in on, on)
170
+ check("②MdCG.search 启用后 unknown 保留", "mem_now" in on, on)
171
+
172
+ sec = MdCGSecure(root_sec, principal=_princ())
173
+ _seed(sec)
174
+ sec_off = _ids(sec.search(Q, k=20)[0])
175
+ check("③Secure.search 默认关:expired 可见", "mem_old" in sec_off, sec_off)
176
+ sec_on = _ids(sec.search(Q, k=20, validity=True)[0])
177
+ check("②Secure.search 启用后 expired 剔除", "mem_old" not in sec_on, sec_on)
178
+ check("②Secure.search 启用后 not_yet 保留",
179
+ "mem_future" in sec_on, sec_on)
180
+
181
+
182
+ # --------------------------- ④ RRF + 缓存分键 ⑤ recall ⑥ 图扩展
183
+ def _t3(root):
184
+ cg = MdCGSecure(root, principal=_princ())
185
+ _seed(cg)
186
+ hotcache.attach(cg)
187
+
188
+ off, _ = cg.search_rrf(Q, k=20)
189
+ on, _ = cg.search_rrf(Q, k=20, validity=True)
190
+ check("④search_rrf 默认关:expired 可见", "mem_old" in _ids(off), _ids(off))
191
+ check("④search_rrf 启用后 expired 剔除", "mem_old" not in _ids(on), _ids(on))
192
+ check("④search_rrf 启用后 not_yet 保留", "mem_future" in _ids(on), _ids(on))
193
+
194
+ # 缓存分键:两口径必须各自成键,否则第二次调用命中第一次的缓存即串口径
195
+ hc = hotcache.get(cg)
196
+ on2, meta2 = cg.search_rrf(Q, k=20, validity=True)
197
+ off2, meta_off = cg.search_rrf(Q, k=20)
198
+ check("④缓存命中路径仍零 expired(meta.cached 可证命中)",
199
+ "mem_old" not in _ids(on2) and meta2.get("cached") is True,
200
+ "cached=%s ids=%s" % (meta2.get("cached"), _ids(on2)))
201
+ check("④两口径各自成键且互不顶替(默认口径仍见 expired 且命中自身缓存)",
202
+ "mem_old" in _ids(off2) and meta_off.get("cached") is True,
203
+ "off_cached=%s ids=%s" % (meta_off.get("cached"), _ids(off2)))
204
+ check("④缓存中两口径键并存(>=2 条)",
205
+ hc.stats()["query_cache_size"] >= 2, hc.stats())
206
+
207
+ # ⑥ 图扩展:零词面交集节点只能经 edges 抵达 —— 前置证据 + 过滤断言
208
+ check("⑥前置证据:不过滤时 ghost 经图扩展可达(零词面交集)",
209
+ "mem_ghost" in _ids(off), _ids(off))
210
+ check("⑥启用后 ghost 被剔除(图扩展不绕过时效过滤)",
211
+ "mem_ghost" not in _ids(on), _ids(on))
212
+
213
+ # ⑤ recall 两分支
214
+ p_off = [p.get("id") for p in cg.recall(Q, budget_tokens=6000, k=20)["pack"]]
215
+ check("⑤recall(RRF 分支)默认关:expired 可见", "mem_old" in p_off, p_off)
216
+ p_rrf = [p.get("id") for p in cg.recall(Q, budget_tokens=6000, k=20,
217
+ validity=True)["pack"]]
218
+ check("⑤recall(RRF 分支)启用后 expired 剔除", "mem_old" not in p_rrf, p_rrf)
219
+ check("⑤recall(RRF 分支)not_yet 保留", "mem_future" in p_rrf, p_rrf)
220
+ p_lin = [p.get("id") for p in cg.recall(Q, budget_tokens=6000, k=20,
221
+ use_rrf=False, validity=True)["pack"]]
222
+ check("⑤recall(非 RRF 分支)启用后 expired 剔除",
223
+ "mem_old" not in p_lin, p_lin)
224
+ check("⑤recall(非 RRF 分支)not_yet 保留", "mem_future" in p_lin, p_lin)
225
+
226
+
227
+ # ------------------------------------------------------------- ⑦ MCP 面
228
+ def _t4(root):
229
+ cg = MdCGSecure(root, principal=_princ())
230
+ _seed(cg)
231
+
232
+ s_on = MS.call_tool(cg, "mdcg_search",
233
+ {"query": Q, "k": 20, "validity": True})
234
+ ids_on = [r["node"]["id"] for r in s_on["results"]]
235
+ check("⑦mdcg_search 透传 validity(expired 剔除)",
236
+ "mem_old" not in ids_on, ids_on)
237
+ check("⑦mdcg_search 透传后 not_yet 保留", "mem_future" in ids_on, ids_on)
238
+ s_off = MS.call_tool(cg, "mdcg_search", {"query": Q, "k": 20})
239
+ ids_off = [r["node"]["id"] for r in s_off["results"]]
240
+ check("⑦默认关:mdcg_search 缺省 expired 可见", "mem_old" in ids_off, ids_off)
241
+
242
+ rc = MS.call_tool(cg, "mdcg_recall",
243
+ {"query": Q, "k": 20, "budget_tokens": 6000,
244
+ "validity": True})
245
+ pids = [p.get("id") for p in rc["pack"]]
246
+ check("⑦mdcg_recall 透传 validity(expired 剔除)",
247
+ "mem_old" not in pids, pids)
248
+ rc_off = MS.call_tool(cg, "mdcg_recall",
249
+ {"query": Q, "k": 20, "budget_tokens": 6000})
250
+ check("⑦默认关:mdcg_recall 缺省 expired 可见",
251
+ "mem_old" in [p.get("id") for p in rc_off["pack"]], rc_off.get("pack"))
252
+
253
+ out = MS._cg_call(cg, {"op": "read", "query": Q, "k": 20, "validity": True})
254
+ oids = _ids(out.get("results") or [])
255
+ check("⑦cg op=read 透传 validity(expired 剔除)", "mem_old" not in oids, oids)
256
+ out_off = MS._cg_call(cg, {"op": "read", "query": Q, "k": 20})
257
+ check("⑦默认关:cg op=read 缺省 expired 可见",
258
+ "mem_old" in _ids(out_off.get("results") or []),
259
+ _ids(out_off.get("results") or []))
260
+
261
+
262
+ def main():
263
+ tmp = tempfile.mkdtemp(prefix="mdcg_validity_")
264
+ try:
265
+ _t1()
266
+ _t2(os.path.join(tmp, "core"), os.path.join(tmp, "sec"))
267
+ _t3(os.path.join(tmp, "rrf"))
268
+ _t4(os.path.join(tmp, "mcp"))
269
+ except Exception:
270
+ traceback.print_exc()
271
+ _bad.append("未捕获异常")
272
+ finally:
273
+ shutil.rmtree(tmp, ignore_errors=True)
274
+ print("\nvalidity 验收:%d 通过%s" % (
275
+ _ok, (",%d 失败:%s" % (len(_bad), "; ".join(_bad))) if _bad else ""))
276
+ return 1 if _bad else 0
277
+
278
+
279
+ if __name__ == "__main__":
280
+ sys.exit(main())