@furongjun1999/dsh-memory 0.5.0 → 0.5.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 (138) hide show
  1. package/README.md +552 -465
  2. package/docs/README.md +1 -0
  3. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  4. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  5. package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
  6. package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
  7. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
  8. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
  9. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
  10. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v1.md +156 -0
  11. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v10.md +210 -0
  12. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v11.md +227 -0
  13. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v12.md +203 -0
  14. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v13.md +233 -0
  15. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v14.md +191 -0
  16. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v15.md +213 -0
  17. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v16.md +214 -0
  18. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v17.md +199 -0
  19. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v2.md +156 -0
  20. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v3.md +152 -0
  21. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v4.md +128 -0
  22. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v5.md +114 -0
  23. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v6.md +192 -0
  24. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v7.md +187 -0
  25. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v8.md +207 -0
  26. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v9.md +203 -0
  27. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
  28. 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 +19 -3
  29. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  30. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
  31. 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
  32. package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
  33. package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
  34. package/lib/bridge.d.ts +9 -0
  35. package/lib/bridge.js +35 -0
  36. package/lib/index.js +7 -1
  37. package/lib/lib/roleplay_web.js +530 -443
  38. package/lib/lib/token_store.d.ts +7 -1
  39. package/lib/lib/token_store.js +12 -3
  40. package/md_cg/audit.py +12 -1
  41. package/md_cg/backfill.py +16 -15
  42. package/md_cg/backfill_bucket_zh.py +35 -0
  43. package/md_cg/bench_e2e_judge.py +532 -0
  44. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  45. package/md_cg/bench_e2e_qa.py +256 -0
  46. package/md_cg/branches.py +18 -2
  47. package/md_cg/ccgc.py +3 -2
  48. package/md_cg/chain.py +19 -4
  49. package/md_cg/consolidate.py +7 -6
  50. package/md_cg/crosscheck.py +4 -3
  51. package/md_cg/crypto.py +439 -437
  52. package/md_cg/datapath.py +395 -335
  53. package/md_cg/evidence.py +582 -580
  54. package/md_cg/export.py +3 -1
  55. package/md_cg/forgetting.py +2 -2
  56. package/md_cg/fsutil.py +48 -0
  57. package/md_cg/hotcache.py +255 -238
  58. package/md_cg/interop.py +161 -22
  59. package/md_cg/judgment_manifest.py +177 -0
  60. package/md_cg/links.py +655 -622
  61. package/md_cg/mcp_server.py +280 -29
  62. package/md_cg/mdcg.py +616 -125
  63. package/md_cg/mdcos.py +430 -66
  64. package/md_cg/mreview/govern.py +5 -4
  65. package/md_cg/postings.py +4 -2
  66. package/md_cg/readcache.py +76 -18
  67. package/md_cg/reconcile.py +228 -0
  68. package/md_cg/review_cli.py +170 -0
  69. package/md_cg/routing.py +28 -0
  70. package/md_cg/run_tests.py +211 -0
  71. package/md_cg/scrub.py +862 -852
  72. package/md_cg/security.py +385 -275
  73. package/md_cg/selfreport.py +3 -2
  74. package/md_cg/signer.py +565 -562
  75. package/md_cg/sources.py +3 -2
  76. package/md_cg/stg.py +6 -0
  77. package/md_cg/sustain.py +1168 -1138
  78. package/md_cg/test_access_hints.py +147 -0
  79. package/md_cg/test_branch_discard_tombstone.py +136 -0
  80. package/md_cg/test_branches.py +259 -249
  81. package/md_cg/test_chain_read_isolate.py +168 -0
  82. package/md_cg/test_datapath_device_name.py +203 -0
  83. package/md_cg/test_emit_negtail_cache.py +156 -0
  84. package/md_cg/test_en_pipeline.py +186 -166
  85. package/md_cg/test_govern_directread.py +421 -0
  86. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  87. package/md_cg/test_identity_attribution.py +228 -147
  88. package/md_cg/test_index_durability.py +238 -224
  89. package/md_cg/test_interop.py +4 -2
  90. package/md_cg/test_interop_judgment.py +228 -0
  91. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  92. package/md_cg/test_links_concurrent_write.py +188 -0
  93. package/md_cg/test_merge_upsert.py +168 -0
  94. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  95. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  96. package/md_cg/test_n131_merge_gate.py +205 -0
  97. package/md_cg/test_p1x_ref_root.py +160 -0
  98. package/md_cg/test_p27_docindex.py +774 -765
  99. package/md_cg/test_p2_mcp.py +3 -0
  100. package/md_cg/test_p32_backfill.py +304 -298
  101. package/md_cg/test_p39_verify_flow.py +90 -50
  102. package/md_cg/test_p47_session_view.py +60 -25
  103. package/md_cg/test_propose_tail_index.py +157 -0
  104. package/md_cg/test_read_scope_b27.py +277 -0
  105. package/md_cg/test_readcache_default_on.py +168 -0
  106. package/md_cg/test_readcache_precise_inval.py +270 -0
  107. package/md_cg/test_readcache_prodpath.py +55 -7
  108. package/md_cg/test_reconcile_v0.py +294 -0
  109. package/md_cg/test_retr_s1.py +6 -2
  110. package/md_cg/test_retr_s1b.py +67 -0
  111. package/md_cg/test_retr_s7.py +8 -0
  112. package/md_cg/test_retr_s9_entity_ctx.py +181 -175
  113. package/md_cg/test_retr_score_once.py +208 -0
  114. package/md_cg/test_review_onepass.py +170 -0
  115. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  116. package/md_cg/test_security_audit.py +155 -0
  117. package/md_cg/test_security_audit_b26.py +161 -0
  118. package/md_cg/test_security_audit_v21.py +250 -0
  119. package/md_cg/test_semantic_canonical.py +255 -241
  120. package/md_cg/test_session_isolation.py +168 -0
  121. package/md_cg/test_snapshot_autoclose.py +187 -0
  122. package/md_cg/test_tail_watermark_race.py +208 -0
  123. package/md_cg/test_tenant_env_override_warn.py +139 -0
  124. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  125. package/md_cg/test_v14_fixes.py +415 -397
  126. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  127. package/md_cg/theory.py +276 -273
  128. package/md_cg/tokens.py +734 -677
  129. package/md_cg/units.py +3 -2
  130. package/md_cg/vision_evidence.py +4 -3
  131. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  132. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  133. package/md_cg/writepipe.py +554 -550
  134. package/package.json +6 -2
  135. package/src/bridge.ts +434 -401
  136. package/src/index.ts +526 -518
  137. package/src/lib/roleplay_web.ts +1019 -932
  138. package/src/lib/token_store.ts +202 -192
@@ -1,397 +1,415 @@
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()
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
+ """加载 run_tests.py(源码树 scripts/ 优先,其次包内 md_cg/run_tests.py)。
70
+
71
+ 返回 None 表示两者都不在(安装态且未带源码树)→ 调用方如实 SKIP,
72
+ 不虚报通过、也不把「缺件」记成缺陷(2026-09-24 修复)。
73
+ """
74
+ for rel in (os.path.join("scripts", "run_tests.py"),
75
+ os.path.join("md_cg", "run_tests.py")):
76
+ p = os.path.join(_REPO, rel)
77
+ if not os.path.isfile(p):
78
+ continue
79
+ spec = importlib.util.spec_from_file_location("_v14_run_tests", p)
80
+ mod = importlib.util.module_from_spec(spec)
81
+ spec.loader.exec_module(mod)
82
+ return mod
83
+ return None
84
+
85
+
86
+ def _hb(jd, pid, age_ms):
87
+ import json
88
+ with open(os.path.join(jd, "_serve.json"), "w", encoding="utf-8") as f:
89
+ json.dump({"ts": time.time() * 1000 - age_ms, "pid": pid, "workers": 1}, f)
90
+
91
+
92
+ # ------------------------------------------------------------------ B
93
+ def phase_b(tmp):
94
+ phase("B trust.propagate 多跳传播须穿过 doubted 中间节点")
95
+ ids = {"a": "mem_v14b_a", "b": "mem_v14b_b",
96
+ "c": "mem_v14b_c", "d": "mem_v14b_d"}
97
+
98
+ # B1 对照(中间节点非 doubted):多跳正常
99
+ cg1 = MdCGOS(root=os.path.join(tmp, "b1"))
100
+ cg1.add(ids["a"], _ccg("上游甲"), layer="knowledge")
101
+ cg1.add(ids["b"], _ccg("中间乙", ["甲"]), layer="knowledge",
102
+ depends_on=[ids["a"]])
103
+ cg1.add(ids["c"], _ccg("下游丙", ["乙"]), layer="knowledge",
104
+ depends_on=[ids["b"]])
105
+ cg1.add(ids["d"], _ccg("末端丁", ["丙"]), layer="knowledge",
106
+ depends_on=[ids["c"]])
107
+ ok(ids["b"] in trust.dependents_index(cg1).get(ids["a"], []),
108
+ "B1 反查索引就位(甲 ← 乙)")
109
+ trust.set_state(cg1, ids["a"], "expired", reason="t", actor="t")
110
+ r1 = trust.propagate(cg1, apply=True)
111
+ ok([_st(cg1, ids[k]) for k in ("b", "c", "d")] == ["doubted"] * 3,
112
+ "B2 对照:中间节点非 doubted 时乙/丙/丁全被标 doubted(实得 %s)"
113
+ % [_st(cg1, ids[k]) for k in ("b", "c", "d")])
114
+ ok(r1.get("updated_count") == 3, "B2b updated_count=3(实得 %s)"
115
+ % r1.get("updated_count"))
116
+
117
+ # B3 初始 doubts 中间节点:下游仍须可达
118
+ cg2 = MdCGOS(root=os.path.join(tmp, "b2"))
119
+ for k, kw in (("a", {}), ("b", {"depends_on": [ids["a"]]}),
120
+ ("c", {"depends_on": [ids["b"]]}),
121
+ ("d", {"depends_on": [ids["c"]]})):
122
+ cg2.add(ids[k], _ccg("甲" + k), layer="knowledge", **kw)
123
+ trust.set_state(cg2, ids["a"], "expired", reason="t", actor="t")
124
+ trust.set_state(cg2, ids["b"], "doubted", reason="t", actor="t")
125
+ r2 = trust.propagate(cg2, apply=True)
126
+ ok([_st(cg2, ids[k]) for k in ("c", "d")] == ["doubted", "doubted"],
127
+ "B3 中间节点已 doubted 时下游丙/丁仍被标 doubted(实得 %s)"
128
+ % [_st(cg2, ids[k]) for k in ("c", "d")])
129
+ ok(r2.get("updated_count") == 2, "B3b updated_count=2(实得 %s)"
130
+ % r2.get("updated_count"))
131
+ ok(ids["b"] not in (r2.get("updated") or []),
132
+ "B3c doubted 中间节点不被重复标记(幂等)")
133
+
134
+ # B4 真实序列:先 mark_dependents(预演直接下游)再 propagate
135
+ cg3 = MdCGOS(root=os.path.join(tmp, "b3"))
136
+ for k, kw in (("a", {}), ("b", {"depends_on": [ids["a"]]}),
137
+ ("c", {"depends_on": [ids["b"]]}),
138
+ ("d", {"depends_on": [ids["c"]]})):
139
+ cg3.add(ids[k], _ccg("乙" + k), layer="knowledge", **kw)
140
+ trust.set_state(cg3, ids["a"], "expired", reason="t", actor="t")
141
+ trust.mark_dependents(cg3, ids["a"])
142
+ ok(_st(cg3, ids["b"]) == "doubted", "B4 一跳标记生效(乙=doubted)")
143
+ r3 = trust.propagate(cg3, apply=True)
144
+ ok([_st(cg3, ids[k]) for k in ("c", "d")] == ["doubted", "doubted"],
145
+ "B5 真实序列(先 mark_dependents 再 propagate)多跳不失效(实得 %s)"
146
+ % [_st(cg3, ids[k]) for k in ("c", "d")])
147
+ ok(r3.get("updated_count") == 2, "B5b updated_count=2(实得 %s)"
148
+ % r3.get("updated_count"))
149
+ for cg in (cg1, cg2, cg3):
150
+ cg.close()
151
+
152
+
153
+ # ------------------------------------------------------------------ C
154
+ def phase_c(tmp):
155
+ phase("C hotcache query 键覆盖全部影响结果的参数")
156
+ cg = MdCGOS(root=os.path.join(tmp, "c"))
157
+ hotcache.attach(cg)
158
+ kw = "v14cache口径"
159
+ good, work = "mem_v14c_good", "mem_v14c_work"
160
+ cg.add(good, _ccg("常规记忆", body=kw), layer="knowledge")
161
+ cg.add(work, _ccg("工具输出", body=kw), layer="knowledge",
162
+ role="tool-output")
163
+
164
+ q = kw
165
+ ids = lambda rs: [x[0]["id"] for x in rs] # noqa: E731
166
+
167
+ # C1-C2:**先**以 include_work 口径查询(污染候选规则)
168
+ r1, m1 = cg.search_rrf(q, k=5, include_work=True)
169
+ ok(m1.get("cached") is not True, "C1 include_work 首次查询非缓存")
170
+ ok(work in ids(r1), "C2 include_work=True 时工作角色节点出现(该出的要出)")
171
+
172
+ # C3-C4:**随后的首次默认查询**不得复用该口径(报告 C10 资格泄漏)
173
+ r2, m2 = cg.search_rrf(q, k=5)
174
+ ok(work not in ids(r2), "C3 默认查询不得返回工作角色节点(无资格泄漏)")
175
+ ok(m2.get("cached") is not True,
176
+ "C4 默认查询不得复用 include_work 口径缓存(报告 C9/C10)")
177
+
178
+ # C5:默认口径自身缓存仍工作(修法不是「一律不缓存」)
179
+ r3, m3 = cg.search_rrf(q, k=5)
180
+ ok(work not in ids(r3) and m3.get("cached") is True,
181
+ "C5 默认口径重复查询命中缓存且结果不含工作角色")
182
+
183
+ r4, m4 = cg.search_rrf(q, k=5, paths=("bucket",)) # noqa: F841
184
+ r5, m5 = cg.search_rrf(q, k=5, paths=("lexical",))
185
+ ok(m5.get("cached") is not True, "C6 paths 不同口径不得互相命中(报告 C12)")
186
+ ok("bucket" not in (m5.get("paths") or {}),
187
+ "C7 paths=('lexical',) 的结果不含 bucket 路(实得 %s)"
188
+ % sorted((m5.get("paths") or {}).keys()))
189
+
190
+ r6, m6 = cg.search_rrf(q, k=5, fusion="max") # noqa: F841
191
+ ok(m6.get("cached") is not True, "C8 fusion 不同口径不得互相命中")
192
+
193
+ r7, m7 = cg.search_rrf(q, k=5, judge_ranking=True) # noqa: F841
194
+ ok(m7.get("cached") is not True, "C9 judge_ranking 不同口径不得互相命中")
195
+
196
+ r8, m8 = cg.search_rrf(q, k=5, roles=["tool-output"]) # noqa: F841
197
+ ok(m8.get("cached") is not True, "C10 roles 不同口径不得互相命中")
198
+
199
+ r9, m9 = cg.search_rrf( # noqa: F841
200
+ q, k=5, paths=("fuzzy",),
201
+ query_expand=lambda _q: {"__source__": "test", kw: 1.0})
202
+ ok(m9.get("cached") is not True,
203
+ "C11 query_expand(自定义可调用)不写不读缓存(无法稳定进键即绕行)")
204
+ _, m9b = cg.search_rrf(
205
+ q, k=5, paths=("fuzzy",),
206
+ query_expand=lambda _q: {"__source__": "test", kw: 1.0})
207
+ ok(m9b.get("cached") is not True, "C11b 绕行为持续生效(非一次性)")
208
+
209
+ r10, m10 = cg.search_rrf(q, k=5, goal_text="完全不同目标向量", # noqa: F841
210
+ paths=("goal",))
211
+ ok(m10.get("cached") is not True, "C12 goal_text 不同口径不得互相命中")
212
+
213
+ # C13-C15 对照(报告 C13):时间算子启用时读写双侧绕行缓存
214
+ cg.add("mem_v14c_time", _ccg("时间轴记忆", body=kw), layer="knowledge")
215
+ _, mt1 = cg.search_rrf(q, k=5, start_time=0.0, end_time=time.time() + 1e9)
216
+ ok(mt1.get("cached") is not True, "C13 时间算子启用时不写不读缓存(对照)")
217
+ _, mt2 = cg.search_rrf(q, k=5, start_time=0.0, end_time=time.time() + 1e9)
218
+ ok(mt2.get("cached") is not True, "C14 时间算子重复查询仍不命中缓存(对照)")
219
+ rt3, _mt3 = cg.search_rrf(q, k=5)
220
+ ok(work not in ids(rt3),
221
+ "C15 时间算子绕行后默认口径结果不变(无工作角色混入)")
222
+
223
+ # C16 同口径确实仍能命中(修法不是「一律不缓存」)
224
+ _, mh1 = cg.search_rrf(q, k=5)
225
+ _, mh2 = cg.search_rrf(q, k=5)
226
+ ok(mh2.get("cached") is True,
227
+ "C16 同口径重复查询仍命中缓存(保留热路径收益)")
228
+ cg.close()
229
+
230
+
231
+ # ------------------------------------------------------------------ D
232
+ def phase_d(tmp):
233
+ phase("D coldverify 非法迁移须如实报错(不得静默撒谎)")
234
+ cg = MdCGOS(root=os.path.join(tmp, "d"))
235
+ coldverify.attach(cg)
236
+ q = coldverify.get(cg)
237
+
238
+ # D1 doubted + 已过期 → doubted→expired 被 TRANSITIONS 拒绝
239
+ n1 = "mem_v14d_expired_doubted"
240
+ cg.add(n1, _ccg("存疑且过期"), layer="knowledge",
241
+ valid_from="2020-01-01", valid_until="2021-01-01")
242
+ trust.set_state(cg, n1, "doubted", reason="t", actor="t")
243
+ coldverify.enqueue(cg, n1, action="reverify")
244
+ r1 = q.drain(cg)
245
+ ok(r1 and r1[0].get("ok") is False,
246
+ "D1 非法迁移(doubted→expired)上报 ok=False(报告 V6)")
247
+ ok(_st(cg, n1) == "doubted", "D2 节点状态未被迁移(仍 doubted)")
248
+ ok((r1[0].get("error") or ""), "D3 失败原因如实透出(error 非空)")
249
+ ok(q.status()["stats"]["errors"] >= 1,
250
+ "D4 stats.errors 计入(不再掩盖,报告 V7b)")
251
+
252
+ # D5 verified + 未生效 → verified→unverified 被拒绝
253
+ n2 = "mem_v14d_notyet_verified"
254
+ cg.add(n2, _ccg("已验证但未生效"), layer="knowledge",
255
+ valid_from="2099-01-01")
256
+ trust.set_state(cg, n2, "verified", reason="t", actor="t")
257
+ coldverify.enqueue(cg, n2, action="reverify")
258
+ r2 = q.drain(cg)
259
+ ok(r2 and r2[0].get("ok") is False,
260
+ "D5 非法迁移(verified→unverified)上报 ok=False(报告 V7)")
261
+ ok(_st(cg, n2) == "verified", "D6 节点状态未被迁移(仍 verified)")
262
+
263
+ # D7 合法迁移仍正常(修法不是「一律报错」)
264
+ n3 = "mem_v14d_ok"
265
+ cg.add(n3, _ccg("已验证且过期"), layer="knowledge",
266
+ valid_from="2020-01-01", valid_until="2021-01-01")
267
+ trust.set_state(cg, n3, "verified", reason="t", actor="t")
268
+ coldverify.enqueue(cg, n3, action="reverify")
269
+ r3 = q.drain(cg)
270
+ ok(r3 and r3[0].get("ok") is True and _st(cg, n3) == "expired",
271
+ "D7 合法迁移(verified→expired)仍正常(实得 %s / %s)"
272
+ % (r3[0].get("ok") if r3 else None, _st(cg, n3)))
273
+ cg.close()
274
+
275
+
276
+ # ------------------------------------------------------------------ E
277
+ def phase_e(tmp):
278
+ phase("E units 判活口径须与权威三层同口径")
279
+ # hive/ 是**源码树子系统**(出货包 files 不含它)→ 安装态如实 SKIP 整段,
280
+ # 不 FAIL(这条依赖是开发树边界,不是被测缺陷;2026-09-24 修复)。
281
+ try:
282
+ import hive.serve_start as serve_start
283
+ except ImportError:
284
+ print(" SKIP E 段需要 hive/(源码树子系统,出货包不含)——跳过 E1~E8")
285
+ return
286
+ ok(float(units.FRESH_S) == float(serve_start.FRESH_S),
287
+ "E1 units.FRESH_S 与 serve_start.FRESH_S 同值(实得 %s vs %s)"
288
+ % (units.FRESH_S, serve_start.FRESH_S))
289
+ ok(float(units.FRESH_S) == 15.0, "E2 阈值为权威 15s(实得 %s)"
290
+ % units.FRESH_S)
291
+ src = open(units.__file__, encoding="utf-8").read()
292
+ ok('"pid_alive":' in src and '"pid_is_self_program":' in src,
293
+ "E3 units 透出三层判据明细(新鲜/pid 存活/pid 身份,可审计)")
294
+ ok("def pid_is_self_program" in src, "E4 units 有身份层判据")
295
+ ok("(同 _serve_alive)" not in src,
296
+ "E5 契约注释不再自称「同 _serve_alive」(该断言曾在 v13 后失效)")
297
+
298
+ jd = os.path.join(tmp, "hive_jobs")
299
+ os.makedirs(jd, exist_ok=True)
300
+ cases = [("新鲜+本进程+非 hive 映像", os.getpid(), 0),
301
+ ("同类心跳 age=8s", os.getpid(), 8000),
302
+ ("同类心跳 age=20s", os.getpid(), 20000),
303
+ ("pid 不存在", 999999, 0),
304
+ ("pid 不存在且陈旧", 999999, 60000)]
305
+ for label, pid, age in cases:
306
+ _hb(jd, pid, age)
307
+ a = bool(units.serve_state(jd)["alive"])
308
+ b = bool(serve_start.serve_alive(jd))
309
+ ok(a == b, "E6 %s:units=%s / serve_start=%s(须同结论)"
310
+ % (label, a, b))
311
+ with open(os.path.join(jd, "_serve.json"), "w", encoding="utf-8") as f:
312
+ f.write("{ 坏 json")
313
+ ok(bool(units.serve_state(jd)["alive"]) is False,
314
+ "E7 心跳损坏时不判活且不抛")
315
+ os.remove(os.path.join(jd, "_serve.json"))
316
+ ok(bool(units.serve_state(jd)["alive"]) is False, "E8 无心跳时不判活")
317
+
318
+
319
+ # ------------------------------------------------------------------ F
320
+ def phase_f(tmp):
321
+ phase("F 测试面幂等:空壳目录不算依赖就绪")
322
+ from . import md_whitebox
323
+
324
+ cr = getattr(md_whitebox, "corpus_ready", None)
325
+ if not callable(cr):
326
+ ok(False, "F1 corpus_ready 判据未提供(修复前:只判目录存在)")
327
+ else:
328
+ shell = os.path.join(tmp, "shell")
329
+ os.makedirs(shell)
330
+ ok(cr(shell) is False, "F1 空壳目录不算语料就绪")
331
+
332
+ real = os.path.join(tmp, "real", "knowledge")
333
+ os.makedirs(real)
334
+ with open(os.path.join(real, "n.md"), "w", encoding="utf-8") as f:
335
+ f.write("---\nid: x\n---\n正文\n")
336
+ ok(cr(os.path.join(tmp, "real")) is True, "F2 含 md 的目录算就绪")
337
+
338
+ missing = os.path.join(tmp, "missing_corpus")
339
+ try:
340
+ md_whitebox.build_db_from_md(root=missing, verbose=False)
341
+ ok(False, "F3 语料缺失须 fail-closed")
342
+ except FileNotFoundError:
343
+ ok(True, "F3 语料缺失 fail-closed(FileNotFoundError)")
344
+ except Exception as exc: # noqa: BLE001
345
+ ok(False, "F3 语料缺失 fail-closed(实得 %s: %s)"
346
+ % (type(exc).__name__, exc))
347
+ ok(not os.path.exists(missing), "F4 fail-closed 不产生空壳目录副作用")
348
+
349
+ rt = _load_run_tests()
350
+ if rt is None:
351
+ print(" SKIP F5/F6 需要 run_tests.py(scripts/ 或 md_cg/ 内)——"
352
+ "安装态未带源码树,跳过这两条")
353
+ else:
354
+ old_repo = rt._REPO
355
+ try:
356
+ rt._REPO = tmp
357
+ sh = os.path.join(tmp, "_md_cg_wisdom_graph")
358
+ os.makedirs(sh)
359
+ ok(rt._dep_mdroot() is not None,
360
+ "F5 runner:空壳 _md_cg_wisdom_graph 不等于依赖就绪")
361
+ with open(os.path.join(sh, "n.md"), "w", encoding="utf-8") as f:
362
+ f.write("---\nid: x\n---\n")
363
+ ok(rt._dep_mdroot() is None, "F6 runner:含 md 时依赖就绪")
364
+ finally:
365
+ rt._REPO = old_repo
366
+
367
+ for rel in ("md_cg/test_md_access_parity.py",
368
+ "md_cg/test_p44_md_whitebox.py",
369
+ "md_cg/test_wisdom_md_store.py"):
370
+ txt = open(os.path.join(_REPO, rel), encoding="utf-8").read()
371
+ ok("SKIP" in txt,
372
+ "F7 %s 自带依赖缺失自辩(SKIP 出口)" % os.path.basename(rel))
373
+
374
+
375
+ # ------------------------------------------------------------------ G / H
376
+ def phase_g():
377
+ phase("G 超边幂等键无歧义(长度前缀)")
378
+ base = {"received_at": "t", "verify_id": "a", "commit": "b",
379
+ "verdict": "v", "receipt_job": "j", "iter_id": "i"}
380
+ row1 = dict(base, commit="b\x1fc")
381
+ row2 = dict(base, verify_id="a\x1fb", commit="c")
382
+ ok(hyperedge.node_id_for(row1) != hyperedge.node_id_for(row2),
383
+ "G1 相邻列搬移含 \\x1f 的文本得不同 id(报告 H17)")
384
+ ok(hyperedge.node_id_for(row1) == hyperedge.node_id_for(dict(row1)),
385
+ "G2 同一台账行恒同 id(幂等保持)")
386
+ ok(hyperedge.node_id_for(base).startswith("he_"), "G3 id 前缀不变")
387
+ bad = dict(base, commit="c")
388
+ ok(hyperedge.node_id_for(base) != hyperedge.node_id_for(bad),
389
+ "G4 无分隔符对照组 id 不同(问题是规范化而非哈希碰撞)")
390
+
391
+ phase("H 卫生:文件尾换行")
392
+ p = os.path.join(_REPO, "md_cg", "bench_en_atoms_public.py")
393
+ with open(p, "rb") as f:
394
+ ok(f.read().endswith(b"\n"), "H1 bench_en_atoms_public.py 尾换行在位")
395
+
396
+
397
+ def main():
398
+ tmp = tempfile.mkdtemp(prefix="mdcg_v14_")
399
+ try:
400
+ phase_b(tmp)
401
+ phase_c(tmp)
402
+ phase_d(tmp)
403
+ phase_e(tmp)
404
+ phase_f(tmp)
405
+ phase_g()
406
+ finally:
407
+ shutil.rmtree(tmp, ignore_errors=True)
408
+ print("\nPASS=%d FAIL=%d" % (PASS, len(FAIL)))
409
+ if FAIL:
410
+ for f in FAIL:
411
+ print(" - " + f)
412
+ sys.exit(1)
413
+
414
+
415
+ main()