@furongjun1999/dsh-memory 0.6.0 → 0.6.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 (170) hide show
  1. package/README.md +588 -572
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/discipline/harnesses.yaml +31 -0
  5. package/docs/discipline/templates/full.md.tmpl +1 -1
  6. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  7. package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
  8. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
  9. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
  10. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
  11. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
  12. package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
  13. package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
  14. package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
  15. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
  16. package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -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_v20.md +283 -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_v21.md +224 -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_v22.md +223 -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_v23.md +293 -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_v24.md +303 -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_v25.md +230 -0
  23. 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 +20 -0
  24. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
  25. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
  26. 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
  27. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
  28. 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 +7 -2
  29. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
  30. package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
  31. package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
  32. package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
  33. 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 +460 -460
  34. package/lib/bridge.js +24 -2
  35. package/lib/hooks.d.ts +31 -2
  36. package/lib/hooks.js +219 -16
  37. package/lib/init.js +16 -1
  38. package/lib/lib/prompt_safety.d.ts +52 -1
  39. package/lib/lib/prompt_safety.js +76 -1
  40. package/md_cg/audit.py +652 -379
  41. package/md_cg/bench6_arms.py +1 -1
  42. package/md_cg/bench_p0.py +1 -1
  43. package/md_cg/branches.py +2 -2
  44. package/md_cg/ccgc.py +1071 -1006
  45. package/md_cg/chain.py +1 -1
  46. package/md_cg/consistency.py +97 -9
  47. package/md_cg/consolidate.py +34 -9
  48. package/md_cg/crypto.py +59 -26
  49. package/md_cg/docindex.py +16 -1
  50. package/md_cg/evolution.py +17 -1
  51. package/md_cg/export.py +1 -1
  52. package/md_cg/forgetting.py +309 -13
  53. package/md_cg/fsutil.py +454 -8
  54. package/md_cg/identity.py +3 -3
  55. package/md_cg/insight.py +24 -3
  56. package/md_cg/linkref.py +1 -1
  57. package/md_cg/logref.py +327 -0
  58. package/md_cg/mcp_server.py +4152 -3818
  59. package/md_cg/mdcg.py +4208 -3547
  60. package/md_cg/mdcos.py +4491 -4159
  61. package/md_cg/mreview/pipeline.py +15 -1
  62. package/md_cg/nodefile.py +639 -575
  63. package/md_cg/protect.py +158 -11
  64. package/md_cg/protocol.py +41 -2
  65. package/md_cg/provenance.py +22 -4
  66. package/md_cg/reach.py +4 -4
  67. package/md_cg/readcache.py +76 -15
  68. package/md_cg/reconcile.py +1 -1
  69. package/md_cg/refindex.py +246 -37
  70. package/md_cg/refine.py +1 -1
  71. package/md_cg/routing.py +32 -5
  72. package/md_cg/run_tests.py +17 -0
  73. package/md_cg/scrub.py +30 -3
  74. package/md_cg/security.py +47 -1
  75. package/md_cg/self_state.py +5 -5
  76. package/md_cg/sources.py +23 -6
  77. package/md_cg/srcindex.py +352 -0
  78. package/md_cg/stg.py +47 -3
  79. package/md_cg/subgraph.py +2 -2
  80. package/md_cg/sustain.py +1299 -1168
  81. package/md_cg/tasks.py +469 -470
  82. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  83. package/md_cg/test_b1b2_write_face.py +417 -0
  84. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  85. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  86. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  87. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  88. package/md_cg/test_c3_transient_read_negative.py +793 -0
  89. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  90. package/md_cg/test_ccg_form_parity.py +188 -0
  91. package/md_cg/test_ccgc.py +28 -3
  92. package/md_cg/test_govern_directread.py +17 -4
  93. package/md_cg/test_h2_session_view_norm.py +233 -0
  94. package/md_cg/test_h4_sustain_snapshot.py +1377 -0
  95. package/md_cg/test_hive_ingest.py +285 -285
  96. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  97. package/md_cg/test_issue43_default_policy.py +865 -0
  98. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  99. package/md_cg/test_linkref.py +10 -3
  100. package/md_cg/test_lock.py +2 -2
  101. package/md_cg/test_logref.py +1109 -0
  102. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  103. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  104. package/md_cg/test_mr_m2.py +15 -3
  105. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  106. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  107. package/md_cg/test_n176_link_trust.py +221 -0
  108. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  109. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  110. package/md_cg/test_n195_writepath_reload.py +283 -0
  111. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  112. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  113. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  114. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  115. package/md_cg/test_n201_proposal_visibility.py +382 -0
  116. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  117. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  118. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  119. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  120. package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
  121. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  122. package/md_cg/test_n225_nonobject_load.py +1110 -0
  123. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  124. package/md_cg/test_neg_condition_hits.py +333 -0
  125. package/md_cg/test_neg_tail_honesty.py +663 -0
  126. package/md_cg/test_none_id_write_guard.py +4 -3
  127. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  128. package/md_cg/test_p1.py +5 -5
  129. package/md_cg/test_p11_consistency.py +161 -12
  130. package/md_cg/test_p26_refindex.py +2 -2
  131. package/md_cg/test_p27_docindex.py +777 -774
  132. package/md_cg/test_p28_refcheck.py +740 -13
  133. package/md_cg/test_p29_session_ingest_export.py +2 -2
  134. package/md_cg/test_p2_mcp.py +12 -1
  135. package/md_cg/test_p30_maintain.py +33 -2
  136. package/md_cg/test_p31_insight.py +1 -0
  137. package/md_cg/test_p9c_dedup_hints.py +276 -0
  138. package/md_cg/test_policy_required_ccg.py +426 -0
  139. package/md_cg/test_protocol.py +22 -0
  140. package/md_cg/test_rank_parity_score_mode.py +516 -0
  141. package/md_cg/test_read_face_input_gates.py +363 -0
  142. package/md_cg/test_read_face_semantics.py +489 -0
  143. package/md_cg/test_recall_face_guards.py +812 -0
  144. package/md_cg/test_rejected_credential_forms.py +188 -0
  145. package/md_cg/test_rejected_redact.py +146 -0
  146. package/md_cg/test_retr_s1b.py +2 -2
  147. package/md_cg/test_retr_s5.py +20 -7
  148. package/md_cg/test_review_conformance.py +21 -3
  149. package/md_cg/test_security_audit_v21.py +2 -2
  150. package/md_cg/test_server_version.py +67 -0
  151. package/md_cg/test_srcindex.py +171 -0
  152. package/md_cg/test_tenant_env_override_warn.py +63 -50
  153. package/md_cg/test_token_lowercase_form.py +315 -0
  154. package/md_cg/test_v21r1_package_version.py +310 -0
  155. package/md_cg/test_writepipe.py +10 -4
  156. package/md_cg/tokens.py +225 -95
  157. package/md_cg/tool_face.py +4 -4
  158. package/md_cg/trust.py +49 -5
  159. package/md_cg/units.py +204 -6
  160. package/md_cg/weights.py +4 -4
  161. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  162. package/md_cg/writelimit.py +42 -8
  163. package/md_cg/writepipe.py +651 -554
  164. package/package.json +3 -2
  165. package/skills/plugin.json +1 -1
  166. package/src/bridge.ts +25 -2
  167. package/src/hooks.ts +229 -16
  168. package/src/init.ts +15 -1
  169. package/src/lib/prompt_safety.ts +88 -1
  170. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,301 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · 分区健康度自检对单桶库说假话(M3)+ protect.mark 不存在节点裸 None(H9)
3
+
4
+ M3(`md_cg/routing.py::bucket_health`)——自检对「从未分区」的库报「分区退化」:
5
+ ① 巨桶判据 `top/n > 0.30` 在 nb == 1 时恒真(单桶必然 100%)⇒ 一把从未分区
6
+ 的库报成「巨桶 · 分区失效」。判据缺 `nb > 1` 守卫,而同函数碎片化判据早
7
+ 就带着这个守卫(同形缺口)。修复:巨桶判据加 `nb > 1`。
8
+ ② nb == 1 时巨桶与期望扫描(同样恒 1.0)互为同一事实的两种写法 ⇒ 合并为一条
9
+ 如实读数「单桶 · 条件路由本就不可用(未分区,非巨桶退化)」。**ok 仍为
10
+ False**(单桶库的一次查询确实是全量扫描——判据未放宽,只把假诊断换成真读数);
11
+ `expected_scan` 的**数值**仍在返回 dict 里(下游读的是数)。
12
+ ③ `counts == {}` 而库里有节点时返回 `{'ok': True, 'reason': 'empty'}` ⇒ 索引
13
+ 分桶表整体缺失被读成「健康」。修复:新增 `total_nodes` 形参(缺省 None)区分
14
+ 「真空库」与「有节点但零分桶」。**向后兼容**:缺省 None 时返回与加参数前
15
+ **逐位一致**(下面 3a/3b/3e 用整 dict 相等钉死),既有三个调用点
16
+ (test_p0:101 / bench_axis_domain:156 / bench6_arms:183 只传 counts)行为一字不变。
17
+
18
+ H9(`md_cg/protect.py::mark`)——节点不存在时裸返回 `None`:
19
+ ④ `None` 与「成功」在调用方眼里都非 dict:`mcp_server._protect_call:1217` 把
20
+ `protect.mark(...)` 的结果直接回给 MCP 客户端 ⇒ 不存在的 node_id 序列化成
21
+ `null`,读成「打标成功」,而 `_write_node` 从未发生(静默假成功)。修复:
22
+ 返回 `{"ok": False, "error": "node_not_found", "node_id": node_id}`
23
+ (形态对齐 `trust.set_state` 的同名分支)。成功路径的返回键**不变**
24
+ (node_id / protected / reason)。
25
+
26
+ 守卫(四组 + 源断言 + floor;独立临时根 + 哑 aux 根,绝不触在役库/~/.mdcg):
27
+ ⓿ 隔离自证;① 巨桶守卫(含真巨桶仍报、30% 边界不报);② 单桶如实读数;
28
+ ③ 空桶 + total_nodes(含缺省逐位一致);④ mark 负路由(含与 trust.set_state
29
+ 同形、MCP 面透出);⑤ 源断言 + live floor。
30
+
31
+ 运行:python -X utf8 -m md_cg.test_m3_h9_bucket_health_protect_mark
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import atexit
36
+ import os
37
+ import shutil
38
+ import sys
39
+ import tempfile
40
+
41
+ # ── 隔离前置:在任何 md_cg 子模块 import 之前把根指到临时目录(import 期可能把
42
+ # aux_root() 冻成常量;且绝不触在役库 D:/... 与真实 ~/.mdcg)。 ──
43
+ _TMP = tempfile.mkdtemp(prefix="m3h9_")
44
+ os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "auxroot")
45
+ os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cgroot")
46
+ for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
47
+ "DSH_SESSION_ID", "MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
48
+ "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_STATE_ROOT",
49
+ "MDCG_DATA_ROOT", "MDCG_TENANT_REGISTRY", "MDCG_VERIFIER_MODULES"):
50
+ os.environ.pop(_k, None)
51
+
52
+ from . import protect, routing, trust # noqa: E402
53
+ from .mdcos import MdCGOS # noqa: E402
54
+
55
+ try:
56
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
57
+ except Exception: # noqa: BLE001
58
+ pass
59
+
60
+ PASS = FAIL = LIVE = 0
61
+ LIVE_FLOOR = 6
62
+ FAILS = []
63
+
64
+ ROOT = os.path.join(_TMP, "cg1")
65
+ BODY = ("# 功能名:分桶健康度探针\n"
66
+ "# 生效条件:M3/H9 守卫临时库\n"
67
+ "# 子功能:给沙箱库写入可打标节点\n"
68
+ "# 执行:直接调用 cg.add\n"
69
+ "# 验证方式:本守卫断言\n"
70
+ "# 不适用条件:无\n"
71
+ "守卫探针正文。\n")
72
+
73
+ _CG = []
74
+
75
+
76
+ def check(name, cond, detail="", live=False):
77
+ global PASS, FAIL, LIVE
78
+ if live:
79
+ LIVE += 1
80
+ if cond:
81
+ PASS += 1
82
+ print(" [PASS] %s" % name)
83
+ else:
84
+ FAIL += 1
85
+ FAILS.append(name)
86
+ print(" [FAIL] %s · %s" % (name, detail))
87
+
88
+
89
+ def _close_all():
90
+ for cg in _CG:
91
+ try:
92
+ cg.close()
93
+ except Exception: # noqa: BLE001
94
+ pass
95
+
96
+
97
+ def _cleanup(path) -> bool:
98
+ import gc
99
+ import time
100
+ for _ in range(6):
101
+ shutil.rmtree(path, ignore_errors=True)
102
+ if not os.path.exists(path):
103
+ return True
104
+ gc.collect()
105
+ time.sleep(0.25)
106
+ return not os.path.exists(path)
107
+
108
+
109
+ atexit.register(lambda: (_close_all(), _cleanup(_TMP)))
110
+
111
+
112
+ def _cg():
113
+ cg = MdCGOS(ROOT)
114
+ _CG.append(cg)
115
+ return cg
116
+
117
+
118
+ def _src(rel):
119
+ with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), rel),
120
+ encoding="utf-8") as f:
121
+ return f.read()
122
+
123
+
124
+ def _window(text, start, end):
125
+ i = text.index(start)
126
+ j = text.index(end, i + len(start))
127
+ return text[i:j]
128
+
129
+
130
+ def legacy_empty():
131
+ """加 total_nodes 参数之前的空桶返回(逐位对照基线)。"""
132
+ return {"ok": True, "reason": "empty", "buckets": 0}
133
+
134
+
135
+ def main():
136
+ # ================= ⓿ 隔离自证 =================
137
+ print("【⓿】隔离自证(临时根,不触在役库)")
138
+ cg = _cg()
139
+ check("0a 库根落在本次临时目录内(不指在役库)",
140
+ os.path.abspath(cg.root).startswith(os.path.abspath(_TMP)),
141
+ cg.root, live=True)
142
+ check("0b MDCG_ROOT 亦在临时目录内(import 期不会冻到 ~/.mdcg)",
143
+ os.path.abspath(os.environ["MDCG_ROOT"]).startswith(
144
+ os.path.abspath(_TMP)),
145
+ os.environ["MDCG_ROOT"], live=True)
146
+ nid = cg.add("m3h9_alpha", BODY, layer="knowledge")
147
+ cg.add("m3h9_beta", BODY, layer="knowledge")
148
+ cg.add("m3h9_gamma", BODY, layer="knowledge")
149
+ cg.flush()
150
+ check("0c 沙箱库确有节点(防 fixture 空转)",
151
+ len(cg.index["nodes"]) == 3, len(cg.index["nodes"]), live=True)
152
+
153
+ # ================= ① 巨桶判据加 nb > 1 守卫 =================
154
+ print("\n【①】巨桶判据(M3①)")
155
+ one = routing.bucket_health({"cond_a": 100})
156
+ check("1a 单桶 {a:100} 不再报「巨桶」",
157
+ not any("巨桶" in p for p in one["problems"]), str(one["problems"]))
158
+ multi = routing.bucket_health({"cond_a": 80, "cond_b": 10, "cond_c": 10})
159
+ check("1b 多桶真巨桶 {a:80,b:10,c:10} 仍报「巨桶:最大桶占 80.0%」(未放宽)",
160
+ any("巨桶" in p and "80.0%" in p for p in multi["problems"])
161
+ and multi["ok"] is False, str(multi["problems"]))
162
+ edge = routing.bucket_health({"cond_a": 3, "cond_b": 3, "cond_c": 3,
163
+ "cond_d": 1})
164
+ check("1c 30% 边界(top/n == 0.30 且期望扫描 28%)两条判据均不触发",
165
+ edge["ok"] is True and edge["problems"] == []
166
+ and edge["max_bucket_share"] == 0.3, str(edge))
167
+ frag = routing.bucket_health({"k1": 1, "k2": 1, "k3": 1, "k4": 1, "k5": 1,
168
+ "cond_a": 3})
169
+ check("1d 碎片化判据(nb > 1 + 单例率 > 50%)仍报(同族判据未被误伤)",
170
+ any("碎片化" in p for p in frag["problems"]), str(frag["problems"]))
171
+
172
+ # ================= ② 单桶如实读数 =================
173
+ print("\n【②】单桶如实读数(M3②)")
174
+ check("2a 单桶 problems 点名「单桶」+「条件路由……不可用」",
175
+ len(one["problems"]) == 1 and "单桶" in one["problems"][0]
176
+ and "不可用" in one["problems"][0], str(one["problems"]))
177
+ check("2b 单桶 ok 仍为 False(判据未放宽:单桶查询确实是全量扫描)",
178
+ one["ok"] is False, str(one))
179
+ check("2c 单桶的 expected_scan / max_bucket_share 数值面上仍是 1.0(数不丢)",
180
+ one["expected_scan"] == 1.0 and one["max_bucket_share"] == 1.0
181
+ and one["nodes"] == 100 and one["buckets"] == 1, str(one))
182
+ one1 = routing.bucket_health({"cond_a": 1})
183
+ check("2d 单桶单节点 {a:1} 同口径(不因 n==1 走别的分支)",
184
+ len(one1["problems"]) == 1 and "单桶" in one1["problems"][0]
185
+ and "不可用" in one1["problems"][0], str(one1))
186
+
187
+ # ================= ③ 空分桶 + total_nodes =================
188
+ print("\n【③】空分桶与 total_nodes(M3③)")
189
+ check("3a 缺省 total_nodes 时空 counts 返回与改动前逐位一致",
190
+ routing.bucket_health({}) == legacy_empty()
191
+ and routing.bucket_health({}, total_nodes=None) == legacy_empty()
192
+ and routing.bucket_health({"cond_a": 0}) == legacy_empty(),
193
+ "%r / %r / %r" % (routing.bucket_health({}),
194
+ routing.bucket_health({}, total_nodes=None),
195
+ routing.bucket_health({"cond_a": 0})))
196
+ check("3b 缺省时键集亦未变(不因新参数给空桶路径加键)",
197
+ set(routing.bucket_health({})) == set(legacy_empty()),
198
+ str(sorted(routing.bucket_health({}))))
199
+ check("3c 真空库(total_nodes=0)仍报 ok=True·empty(不误伤空库)",
200
+ routing.bucket_health({}, total_nodes=0) == legacy_empty(),
201
+ str(routing.bucket_health({}, total_nodes=0)))
202
+ bad = routing.bucket_health({}, total_nodes=7)
203
+ check("3d 有节点但零分桶(total_nodes=7)不得再报 ok=True",
204
+ bad["ok"] is False and bad["reason"] == "empty_buckets"
205
+ and bad["buckets"] == 0 and bad["nodes"] == 7
206
+ and any("分区信息缺失" in p for p in bad["problems"]), str(bad))
207
+ check("3e 多桶返回键集与改动前一致(7 键,无新增键)",
208
+ set(multi) == {"ok", "buckets", "nodes", "max_bucket_share",
209
+ "singleton_ratio", "expected_scan", "problems"},
210
+ str(sorted(multi)))
211
+ live_buckets = dict(cg.index.get("buckets", {}))
212
+ cg.index["buckets"] = {} # 只改内存:模拟分桶表整体缺失
213
+ n_nodes = len(cg.index["nodes"])
214
+ lie = routing.bucket_health(cg.index.get("buckets", {}))
215
+ truth = routing.bucket_health(cg.index.get("buckets", {}),
216
+ total_nodes=n_nodes)
217
+ cg.index["buckets"] = live_buckets
218
+ check("3f 真库场景:分桶表清空后有节点仍判 ok=False(缺省则复现旧假话)",
219
+ truth["ok"] is False and lie["ok"] is True
220
+ and truth["nodes"] == n_nodes, "%r / %r" % (lie, truth), live=True)
221
+ check("3g 键集对照:有节点零分桶与真空库是两种读数(非「空」的同义)",
222
+ set(truth) != set(legacy_empty())
223
+ and truth["reason"] != legacy_empty()["reason"], str(truth))
224
+
225
+ # ================= ④ protect.mark 负路由 =================
226
+ print("\n【④】protect.mark 不存在节点(H9④)")
227
+ res = protect.mark(cg, nid, "M3H9 守卫·显式保护标记")
228
+ check("4a 成功路径既有返回键与值不变(node_id/protected/reason)",
229
+ isinstance(res, dict) and res.get("node_id") == nid
230
+ and res.get("protected") is True
231
+ and res.get("reason") == "M3H9 守卫·显式保护标记", repr(res))
232
+ pro, why = protect.is_protected(cg, nid)
233
+ disk = cg.get(nid)["frontmatter"]
234
+ check("4b 成功路径仍真的打上保护位(内存条目 + 盘上 frontmatter)",
235
+ pro is True and disk.get("protected") is True
236
+ and disk.get("protection_reason") == "M3H9 守卫·显式保护标记",
237
+ "%r / %r" % (pro, disk), live=True)
238
+ miss = protect.mark(cg, "m3h9_不存在", "理由")
239
+ check("4c 不存在节点返回结构化负路由(ok/error/node_id)",
240
+ isinstance(miss, dict) and miss.get("ok") is False
241
+ and miss.get("error") == "node_not_found"
242
+ and miss.get("node_id") == "m3h9_不存在", repr(miss))
243
+ check("4d 不再裸返回 None(None 与成功在调用方不可区分)",
244
+ miss is not None, repr(miss))
245
+ ts = trust.set_state(cg, "m3h9_不存在", "verified")
246
+ check("4e 形态与仓内既有负路由同形(trust.set_state 不存在分支)",
247
+ isinstance(miss, dict) and set(miss) == set(ts)
248
+ and ts.get("error") == "node_not_found", "%r / %r" % (miss, ts))
249
+ odd = protect.mark(cg, None, "理由")
250
+ check("4f 畸形 node_id(None)不裸穿透且同形",
251
+ isinstance(odd, dict) and odd.get("error") == "node_not_found"
252
+ and odd.get("node_id") is None, repr(odd))
253
+ try:
254
+ from .mcp_server import _protect_call
255
+ except Exception as exc: # noqa: BLE001
256
+ print(" [INFO] _protect_call 未取到(%s: %s),跳过 MCP 面断言"
257
+ % (type(exc).__name__, exc))
258
+ else:
259
+ mcp = _protect_call(cg, {"action": "mark", "node_id": "m3h9_不存在",
260
+ "reason": "理由"})
261
+ check("4g MCP 面 mark 分支对不存在节点透出负路由(不再是 null)",
262
+ isinstance(mcp, dict) and mcp.get("ok") is False
263
+ and mcp.get("error") == "node_not_found", repr(mcp), live=True)
264
+
265
+ # ================= ⑤ 源断言 + floor =================
266
+ print("\n【⑤】源断言(单点接线在位)")
267
+ s_routing = _src("routing.py")
268
+ bh = _window(s_routing, "def bucket_health", "\ndef ")
269
+ check("5a 巨桶判据带 nb > 1 守卫(与碎片化判据同形)",
270
+ "if nb > 1 and top / n > 0.30:" in bh, bh[:400])
271
+ check("5b 巨桶/期望扫描在 nb == 1 时合并为单桶如实读数",
272
+ "if nb == 1:" in bh and "单桶" in bh and "elif expected_scan > 0.30:" in bh,
273
+ bh[-600:])
274
+ check("5c 新参数 total_nodes 带缺省值(向后兼容面)",
275
+ "def bucket_health(counts: dict, total_nodes: int = None)" in s_routing,
276
+ bh[:120])
277
+ s_prot = _src("protect.py")
278
+ mk = _window(s_prot, "def mark", "def stats")
279
+ check("5d mark 不存在分支返回 node_not_found 负路由(不再是 None)",
280
+ '"ok": False, "error": "node_not_found", "node_id": node_id' in mk
281
+ and "return None" not in mk, mk[:600])
282
+ check("5e 成功路径返回键未被改动(仍 node_id/protected/reason)",
283
+ 'return {"node_id": node_id, "protected": True, "reason": reason}' in mk,
284
+ mk[-300:])
285
+ print(" [INFO] live 断言数 = %d(floor=%d)" % (LIVE, LIVE_FLOOR))
286
+ check("5f live 断言数不低于 floor(防空转绿)", LIVE >= LIVE_FLOOR,
287
+ "live=%d" % LIVE)
288
+
289
+ print("\n" + "=" * 64)
290
+ print("PASS=%d FAIL=%d LIVE=%d" % (PASS, FAIL, LIVE))
291
+ if FAILS:
292
+ print("失败项:" + ";".join(FAILS))
293
+ return 0 if FAIL == 0 else 1
294
+
295
+
296
+ if __name__ == "__main__":
297
+ try:
298
+ rc = main()
299
+ finally:
300
+ _close_all()
301
+ sys.exit(rc)