@furongjun1999/dsh-memory 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +618 -572
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/README.md +1 -0
  5. package/docs/discipline/harnesses.yaml +31 -0
  6. package/docs/discipline/templates/full.md.tmpl +1 -1
  7. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
  15. 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
  16. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
  17. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
  18. 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
  19. 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
  20. 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
  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_v20.md +283 -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_v21.md +224 -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_v22.md +223 -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_v23.md +293 -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_v24.md +303 -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_v25.md +230 -0
  27. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
  28. 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 +52 -2
  29. 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
  30. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -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/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
  33. 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
  34. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
  35. 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
  36. 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
  37. 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
  38. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
  39. 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
  40. 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
  41. package/lib/bridge.js +24 -2
  42. package/lib/hooks.d.ts +31 -2
  43. package/lib/hooks.js +219 -16
  44. package/lib/init.js +16 -1
  45. package/lib/lib/prompt_safety.d.ts +52 -1
  46. package/lib/lib/prompt_safety.js +76 -1
  47. package/md_cg/audit.py +652 -379
  48. package/md_cg/bench6_arms.py +1 -1
  49. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  50. package/md_cg/bench_p0.py +1 -1
  51. package/md_cg/branches.py +2 -2
  52. package/md_cg/ccgc.py +1071 -1006
  53. package/md_cg/chain.py +48 -1
  54. package/md_cg/consistency.py +97 -9
  55. package/md_cg/consolidate.py +34 -9
  56. package/md_cg/crypto.py +59 -26
  57. package/md_cg/docindex.py +16 -1
  58. package/md_cg/evolution.py +17 -1
  59. package/md_cg/export.py +1 -1
  60. package/md_cg/forgetting.py +309 -13
  61. package/md_cg/freshness.py +511 -0
  62. package/md_cg/fsutil.py +454 -8
  63. package/md_cg/generation.py +409 -0
  64. package/md_cg/hotcache.py +4 -1
  65. package/md_cg/identity.py +3 -3
  66. package/md_cg/insight.py +24 -3
  67. package/md_cg/linkref.py +1 -1
  68. package/md_cg/logref.py +327 -0
  69. package/md_cg/mcp_server.py +4261 -3818
  70. package/md_cg/mdcg.py +4695 -3547
  71. package/md_cg/mdcos.py +4672 -4159
  72. package/md_cg/mreview/pipeline.py +15 -1
  73. package/md_cg/nodefile.py +713 -576
  74. package/md_cg/protect.py +158 -11
  75. package/md_cg/protocol.py +41 -2
  76. package/md_cg/provenance.py +22 -4
  77. package/md_cg/reach.py +4 -4
  78. package/md_cg/readcache.py +76 -15
  79. package/md_cg/reconcile.py +1 -1
  80. package/md_cg/refindex.py +246 -37
  81. package/md_cg/refine.py +1 -1
  82. package/md_cg/routing.py +32 -5
  83. package/md_cg/run_tests.py +17 -0
  84. package/md_cg/scrub.py +30 -3
  85. package/md_cg/security.py +47 -1
  86. package/md_cg/self_state.py +5 -5
  87. package/md_cg/semantic/canonical.py +22 -0
  88. package/md_cg/semantic/unify.py +73 -22
  89. package/md_cg/semantic/unify_fixture.json +25 -0
  90. package/md_cg/sleep.py +1297 -0
  91. package/md_cg/sources.py +23 -6
  92. package/md_cg/srcindex.py +352 -0
  93. package/md_cg/stg.py +47 -3
  94. package/md_cg/subgraph.py +2 -2
  95. package/md_cg/sustain.py +1436 -1168
  96. package/md_cg/tasks.py +469 -470
  97. package/md_cg/test_auto_defaults.py +424 -0
  98. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  99. package/md_cg/test_b1b2_write_face.py +417 -0
  100. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  101. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  102. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  103. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  104. package/md_cg/test_boundary_hit.py +410 -0
  105. package/md_cg/test_c3_transient_read_negative.py +793 -0
  106. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  107. package/md_cg/test_ccg_form_parity.py +188 -0
  108. package/md_cg/test_ccgc.py +28 -3
  109. package/md_cg/test_en_pipeline.py +22 -13
  110. package/md_cg/test_generation_guard.py +352 -0
  111. package/md_cg/test_govern_directread.py +17 -4
  112. package/md_cg/test_h2_session_view_norm.py +233 -0
  113. package/md_cg/test_h4_sustain_snapshot.py +1387 -0
  114. package/md_cg/test_hive_ingest.py +285 -285
  115. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  116. package/md_cg/test_issue43_default_policy.py +865 -0
  117. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  118. package/md_cg/test_linkref.py +10 -3
  119. package/md_cg/test_lock.py +2 -2
  120. package/md_cg/test_logref.py +1109 -0
  121. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  122. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  123. package/md_cg/test_mr_m2.py +15 -3
  124. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  125. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  126. package/md_cg/test_n176_link_trust.py +221 -0
  127. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  128. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  129. package/md_cg/test_n195_writepath_reload.py +283 -0
  130. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  131. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  132. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  133. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  134. package/md_cg/test_n201_proposal_visibility.py +382 -0
  135. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  136. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  137. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  138. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  139. package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
  140. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  141. package/md_cg/test_n225_nonobject_load.py +1110 -0
  142. package/md_cg/test_n230_dirty_replay.py +375 -0
  143. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  144. package/md_cg/test_neg_condition_hits.py +333 -0
  145. package/md_cg/test_neg_tail_honesty.py +663 -0
  146. package/md_cg/test_none_id_write_guard.py +4 -3
  147. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  148. package/md_cg/test_p1.py +5 -5
  149. package/md_cg/test_p11_consistency.py +161 -12
  150. package/md_cg/test_p26_refindex.py +2 -2
  151. package/md_cg/test_p27_docindex.py +777 -774
  152. package/md_cg/test_p28_refcheck.py +740 -13
  153. package/md_cg/test_p29_session_ingest_export.py +2 -2
  154. package/md_cg/test_p2_mcp.py +12 -1
  155. package/md_cg/test_p2_six_elements.py +463 -0
  156. package/md_cg/test_p30_maintain.py +33 -2
  157. package/md_cg/test_p31_insight.py +1 -0
  158. package/md_cg/test_p3_legacy_closure.py +433 -0
  159. package/md_cg/test_p4_freshness.py +680 -0
  160. package/md_cg/test_p8_subgraph_chain.py +14 -1
  161. package/md_cg/test_p9c_dedup_hints.py +276 -0
  162. package/md_cg/test_policy_required_ccg.py +426 -0
  163. package/md_cg/test_protocol.py +22 -0
  164. package/md_cg/test_rank_parity_score_mode.py +522 -0
  165. package/md_cg/test_read_face_input_gates.py +363 -0
  166. package/md_cg/test_read_face_semantics.py +489 -0
  167. package/md_cg/test_recall_face_guards.py +812 -0
  168. package/md_cg/test_rejected_credential_forms.py +188 -0
  169. package/md_cg/test_rejected_redact.py +146 -0
  170. package/md_cg/test_retr_s1b.py +2 -2
  171. package/md_cg/test_retr_s5.py +20 -7
  172. package/md_cg/test_review_conformance.py +21 -3
  173. package/md_cg/test_security_audit_v21.py +2 -2
  174. package/md_cg/test_semantic_canonical.py +5 -3
  175. package/md_cg/test_server_version.py +67 -0
  176. package/md_cg/test_sleep.py +611 -0
  177. package/md_cg/test_sleep_p1.py +784 -0
  178. package/md_cg/test_srcindex.py +171 -0
  179. package/md_cg/test_tenant_env_override_warn.py +63 -50
  180. package/md_cg/test_time_core_lint.py +968 -0
  181. package/md_cg/test_token_lowercase_form.py +315 -0
  182. package/md_cg/test_unify_default_off.py +701 -0
  183. package/md_cg/test_unify_scope.py +182 -0
  184. package/md_cg/test_v21r1_package_version.py +310 -0
  185. package/md_cg/test_writepipe.py +10 -4
  186. package/md_cg/tokens.py +225 -95
  187. package/md_cg/tool_face.py +4 -4
  188. package/md_cg/trust.py +49 -5
  189. package/md_cg/units.py +204 -6
  190. package/md_cg/weights.py +4 -4
  191. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  192. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  193. package/md_cg/writelimit.py +42 -8
  194. package/md_cg/writepipe.py +651 -554
  195. package/package.json +4 -2
  196. package/skills/plugin.json +1 -1
  197. package/src/bridge.ts +25 -2
  198. package/src/hooks.ts +229 -16
  199. package/src/init.ts +15 -1
  200. package/src/lib/prompt_safety.ts +88 -1
  201. package/utf8_boot.py +237 -0
  202. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,525 @@
1
+ # -*- coding: utf-8 -*-
2
+ # 功能名:分区健康度自检的两向诚实化(M3)· protect.mark 打标失败不得冒充成功(H9)
3
+ # ——守卫员席的独立语义级判别力守卫
4
+ # 生效条件:md_cg/routing.py 的 `bucket_health(counts, total_nodes=None)`、md_cg/mdcg.py
5
+ # 的 `MdCG.health` 呼叫点(把节点总数传进判据)、md_cg/protect.py 的 `mark`
6
+ # 三处修复在位时成立;沙箱条件:所有库根一律 tempfile.mkdtemp,绝不触在役库
7
+ # (D:/program/dsh-memory-main 的 in-service 库)与在役服务;本文件只读源码,
8
+ # 变异时只写目标文件且在 finally 里按原始字节复原。
9
+ # 子功能:钉的是**消费方看得见的目标语义**,不是站点文本——
10
+ # A M3 假警报方向:「从未分区」的库(单桶)不得被读成「分区退化」——读数里不得
11
+ # 出现「巨桶 / 碎片化 / 路由无效」这类退化结论,必须把原因归到「单桶·条件路由
12
+ # 本就不可用」;ok 仍为 False、数值面(expected_scan/max_bucket_share)仍为真值
13
+ # B M3 不得放宽方向:真巨桶 / 碎片化 / 期望扫描超阈仍必须被报出并判退化,
14
+ # 阈值(>30% / >50%)边界与多桶形态逐位不变(消假警报 ≠ 放宽判据)
15
+ # C M3 假健康方向:库里有节点而分桶表为空 ⇒ 消费方(`cg.health_os()`,即 MCP
16
+ # `mdcg_health` 的返回体)不得读到 ok=True;真空库仍读 ok=True;缺省
17
+ # total_nodes 时旧调用方读数与加参数前逐位一致(兼容面)
18
+ # D H9 假成功方向:节点不存在时打标读数必须自证失败,且与「成功」形态互斥、
19
+ # 与仓内既有负路由契约同形;畸形 id 一律不裸穿透
20
+ # E H9 返回值与事实一致(反方向):成功打标必须真落保护位(返回值 / 内存判据 /
21
+ # 盘上 frontmatter / guard_forget 四面),失败打标必须真的什么都没发生
22
+ # F 单点与接线:体检面的分区读数 == 判据单点(`routing.bucket_health`)的读数;
23
+ # 呼叫点显式传节点总数;MCP mark 分支无第二套判据(逐位透传)
24
+ # 执行:python -X utf8 -m md_cg.test_m3_h9_semantic_guard
25
+ # python -X utf8 -m md_cg.test_m3_h9_semantic_guard --mutation=m3_bucket_guard
26
+ # python -X utf8 -m md_cg.test_m3_h9_semantic_guard --mutation=all
27
+ # 验证方式:本文件自跑(自带断言计数、live 计数与退出码 rc=0/1);定点变异自证见
28
+ # --mutation(把每处修复逐点抽回改动前语义 → 本守卫必红且红项名可贴出;
29
+ # 变异在 finally 中按原始字节复原并回读核对逐字节相等 → 无残余)。
30
+ # 不适用条件:情感交互|闲聊|纯查询无改动;routing.py 的**阈值与文案措辞**本身不在
31
+ # 本席改动面(属 routing 判据席)——本守卫只钉「读数不得说假话」与「判据
32
+ # 不得被放宽」两向语义,不钉措辞;`protect.mark` 在 MCP 面「不存在节点该给
33
+ # error 值还是 null」的协议登记(md_cg/protocol.py)不在本席改动面内。
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ import os
38
+ import shutil
39
+ import subprocess
40
+ import sys
41
+ import tempfile
42
+
43
+ # ── 隔离前置:在任何 md_cg 子模块 import 之前把根指到临时目录 ──
44
+ _TMP = tempfile.mkdtemp(prefix="m3h9sem_")
45
+ os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "auxroot")
46
+ os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cgroot")
47
+ for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
48
+ "DSH_SESSION_ID", "MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
49
+ "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_STATE_ROOT",
50
+ "MDCG_DATA_ROOT", "MDCG_TENANT_REGISTRY", "MDCG_VERIFIER_MODULES"):
51
+ os.environ.pop(_k, None)
52
+
53
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
54
+
55
+ from md_cg import protect, routing, trust # noqa: E402
56
+ from md_cg.mdcos import MdCGOS # noqa: E402
57
+
58
+ try:
59
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
60
+ except Exception: # noqa: BLE001
61
+ pass
62
+
63
+ _REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
64
+
65
+ PASS = FAIL = LIVE = 0
66
+ LIVE_FLOOR = 8
67
+ FAILS = []
68
+ _OPEN = []
69
+
70
+ BODY = ("# 功能名:分区健康度探针\n"
71
+ "# 生效条件:M3/H9 语义守卫临时库\n"
72
+ "# 子功能:给沙箱库写入可打标节点\n"
73
+ "# 执行:直接调用 cg.add\n"
74
+ "# 验证方式:本守卫断言\n"
75
+ "# 不适用条件:无\n"
76
+ "守卫探针正文。\n")
77
+
78
+
79
+ def check(name, cond, detail="", live=False):
80
+ global PASS, FAIL, LIVE
81
+ if live:
82
+ LIVE += 1
83
+ if cond:
84
+ PASS += 1
85
+ print(" [PASS] %s" % name)
86
+ else:
87
+ FAIL += 1
88
+ FAILS.append(name)
89
+ print(" [FAIL] %s · %s" % (name, detail))
90
+
91
+
92
+ def _mk(rel):
93
+ cg = MdCGOS(os.path.join(_TMP, rel), autoflush=0)
94
+ _OPEN.append(cg)
95
+ return cg
96
+
97
+
98
+ def _close_all():
99
+ for cg in _OPEN:
100
+ try:
101
+ cg.close()
102
+ except Exception: # noqa: BLE001
103
+ pass
104
+
105
+
106
+ def _cleanup():
107
+ import gc
108
+ import time
109
+ for _ in range(6):
110
+ shutil.rmtree(_TMP, ignore_errors=True)
111
+ if not os.path.exists(_TMP):
112
+ return
113
+ gc.collect()
114
+ time.sleep(0.25)
115
+
116
+
117
+ # ───────────────────────── 语义判据(两向) ─────────────────────────
118
+
119
+ DEGRADE_WORDS = ("巨桶", "碎片化", "路由无效")
120
+
121
+
122
+ def _degraded_reading(h) -> bool:
123
+ """读数是否断言了「分区退化」(消费方会据此去重建分桶键)。"""
124
+ return any(any(w in p for w in DEGRADE_WORDS)
125
+ for p in (h.get("problems") or []))
126
+
127
+
128
+ def _fail_shape(r) -> bool:
129
+ """打标失败必须自证失败:显式 ok=False + 非空 error 值。"""
130
+ return (isinstance(r, dict) and r.get("ok") is False
131
+ and isinstance(r.get("error"), str) and bool(r["error"]))
132
+
133
+
134
+ def _ok_shape(r) -> bool:
135
+ """打标成功形态:protected=True 且不带任何失败标记。"""
136
+ return (isinstance(r, dict) and r.get("protected") is True
137
+ and "error" not in r and "ok" not in r)
138
+
139
+
140
+ LEGACY_EMPTY = {"ok": True, "reason": "empty", "buckets": 0}
141
+
142
+
143
+ def main():
144
+ # ================= ⓪ 隔离自证 =================
145
+ print("【⓪】隔离自证(临时根,不触在役库)")
146
+ cg_one = _mk("lib_one") # 全 knowledge 层节点 ⇒ 单桶(orphan)
147
+ cg_self = _mk("lib_self") # 全 self 层节点 ⇒ 有节点但分桶表为空
148
+ cg_empty = _mk("lib_empty") # 真空库
149
+ cg_mark = _mk("lib_mark") # 打标面(contextual 层,默认不带保护位)
150
+ check("0a 库根落在本次临时目录内(不指在役库)",
151
+ all(os.path.abspath(c.root).startswith(os.path.abspath(_TMP))
152
+ for c in (cg_one, cg_self, cg_empty, cg_mark)),
153
+ str([c.root for c in (cg_one, cg_self, cg_empty, cg_mark)]), live=True)
154
+ check("0b MDCG_ROOT / MDCG_AUX_ROOT 亦在临时目录内(import 期不会冻到 ~/.mdcg)",
155
+ os.path.abspath(os.environ["MDCG_ROOT"]).startswith(
156
+ os.path.abspath(_TMP))
157
+ and os.path.abspath(os.environ["MDCG_AUX_ROOT"]).startswith(
158
+ os.path.abspath(_TMP)),
159
+ os.environ["MDCG_ROOT"], live=True)
160
+ n_one = [cg_one.add("sem_one_%d" % i, BODY, layer="knowledge")
161
+ for i in range(3)]
162
+ cg_one.flush()
163
+ for i in range(3):
164
+ cg_self.add("sem_self_%d" % i, BODY, layer="self")
165
+ cg_self.flush()
166
+ cg_mark.add("sem_mark_0", BODY, layer="contextual")
167
+ cg_mark.flush()
168
+ check("0c 沙箱库非空(防 fixture 空转绿)",
169
+ len(cg_one.index["nodes"]) == 3 and len(cg_self.index["nodes"]) == 3
170
+ and len(cg_mark.index["nodes"]) == 1
171
+ and len(cg_empty.index["nodes"]) == 0,
172
+ "%d / %d / %d / %d" % (len(cg_one.index["nodes"]),
173
+ len(cg_self.index["nodes"]),
174
+ len(cg_mark.index["nodes"]),
175
+ len(cg_empty.index["nodes"])), live=True)
176
+ check("0d fixture 前提:单桶库确为单桶、self 库确为零分桶",
177
+ len(cg_one.index.get("buckets") or {}) == 1
178
+ and (cg_self.index.get("buckets") or {}) == {},
179
+ "%r / %r" % (cg_one.index.get("buckets"),
180
+ cg_self.index.get("buckets")), live=True)
181
+
182
+ # ================= ① M3 假警报方向 =================
183
+ print("\n【①】M3:从未分区(单桶)不得被读成分区退化")
184
+ one = routing.bucket_health({"cond_a": 100})
185
+ one_p = one.get("problems") or []
186
+ one1 = routing.bucket_health({"cond_a": 1})
187
+ check("A1 单桶库读数不含任何「分区退化」结论(巨桶/碎片化/路由无效)",
188
+ not _degraded_reading(one), str(one_p))
189
+ check("A2 单桶库 ok 仍为 False(读数更真 ≠ 判据放宽)",
190
+ one.get("ok") is False, str(one))
191
+ check("A3 单桶库数值面仍为真值(expected_scan/max_bucket_share 1.0、buckets 1、nodes 100)",
192
+ one.get("expected_scan") == 1.0 and one.get("max_bucket_share") == 1.0
193
+ and one.get("buckets") == 1 and one.get("nodes") == 100, str(one))
194
+ check("A4 单桶库读数的原因归属:「单桶」+「不可用」(不是泛化退化结论)",
195
+ any("单桶" in p and "不可用" in p for p in one_p), str(one_p))
196
+ check("A5 单桶单节点 {a:1} 同口径(不因 n==1 走另一分支)",
197
+ not _degraded_reading(one1)
198
+ and any("单桶" in p and "不可用" in p
199
+ for p in (one1.get("problems") or [])), str(one1))
200
+ h_e2e = cg_one.health_os() # = MCP `mdcg_health` 返回体
201
+ check("A6 端到端:真库(同桶)经 cg.health_os() 亦不含退化结论且 ok False",
202
+ not _degraded_reading(h_e2e) and h_e2e.get("buckets") == 1
203
+ and h_e2e.get("ok") is False,
204
+ str(h_e2e.get("problems")), live=True)
205
+ check("A7 端到端:该库读数点名「单桶」(消费方知道该去分区,而非去重分键)",
206
+ any("单桶" in p for p in (h_e2e.get("problems") or [])),
207
+ str(h_e2e.get("problems")), live=True)
208
+
209
+ # ================= ② M3 不得放宽方向 =================
210
+ print("\n【②】M3:真退化仍必须被报出(消假警报 ≠ 放宽判据)")
211
+ giant = routing.bucket_health({"cond_a": 80, "cond_b": 10, "cond_c": 10})
212
+ check("B1 多桶真巨桶(80/10/10)仍报「巨桶」且 ok False",
213
+ any("巨桶" in p for p in (giant.get("problems") or []))
214
+ and giant.get("ok") is False, str(giant))
215
+ frag = routing.bucket_health({"k1": 1, "k2": 1, "k3": 1, "k4": 1, "k5": 1,
216
+ "cond_a": 3})
217
+ check("B2 多桶碎片化仍报「碎片化」且 ok False",
218
+ any("碎片化" in p for p in (frag.get("problems") or []))
219
+ and frag.get("ok") is False, str(frag))
220
+ scan = routing.bucket_health({"cond_a": 3, "cond_b": 3, "cond_c": 4})
221
+ check("B3 多桶期望扫描超阈仍报「路由无效」",
222
+ any("路由无效" in p for p in (scan.get("problems") or [])), str(scan))
223
+ over = routing.bucket_health({"cond_a": 31, "cond_b": 35, "cond_c": 34})
224
+ at = routing.bucket_health({"cond_a": 3, "cond_b": 3, "cond_c": 3,
225
+ "cond_d": 1})
226
+ check("B4 阈值边界:多桶 top/n=0.31 报巨桶、=0.30 不报(阈值未被抬高)",
227
+ any("巨桶" in p for p in (over.get("problems") or []))
228
+ and not any("巨桶" in p for p in (at.get("problems") or [])),
229
+ "%r / %r" % (over.get("problems"), at.get("problems")))
230
+ live_buckets = dict(cg_one.index.get("buckets") or {})
231
+ cg_one.index["buckets"] = {"cond_a": 80, "cond_b": 10, "cond_c": 10}
232
+ try:
233
+ h_deg = cg_one.health_os()
234
+ finally:
235
+ cg_one.index["buckets"] = live_buckets
236
+ check("B5 端到端:真退化经 cg.health_os() 仍报「巨桶」且 ok False",
237
+ any("巨桶" in p for p in (h_deg.get("problems") or []))
238
+ and h_deg.get("ok") is False,
239
+ str(h_deg.get("problems")), live=True)
240
+
241
+ # ================= ③ M3 假健康方向 + 兼容面 =================
242
+ print("\n【③】M3:有节点而分桶表为空不得被读成健康;兼容面逐位不变")
243
+ n_self = len(cg_self.index["nodes"])
244
+ h_self = cg_self.health_os() # = MCP `mdcg_health` 返回体
245
+ check("C1 端到端:有节点而分桶表为空 ⇒ 消费方读到 ok False(不得冒充健康)",
246
+ (cg_self.index.get("buckets") or {}) == {}
247
+ and h_self.get("ok") is False, str(h_self.get("ok")), live=True)
248
+ check("C2 该读数可被机械分流(reason=empty_buckets、nodes/total_nodes=节点数)",
249
+ h_self.get("reason") == "empty_buckets"
250
+ and h_self.get("nodes") == n_self
251
+ and h_self.get("total_nodes") == n_self,
252
+ str({k: h_self.get(k) for k in ("reason", "nodes", "total_nodes")}))
253
+ h_empty = cg_empty.health_os()
254
+ check("C3 真空库仍读 ok True / reason empty(反方向不误报)",
255
+ h_empty.get("ok") is True and h_empty.get("reason") == "empty"
256
+ and h_empty.get("total_nodes") == 0, str(h_empty.get("reason")))
257
+ check("C4 缺省 total_nodes 时空 counts 读数与加参数前逐位一致(旧调用方零变更)",
258
+ routing.bucket_health({}) == LEGACY_EMPTY
259
+ and routing.bucket_health({}, total_nodes=None) == LEGACY_EMPTY
260
+ and routing.bucket_health({"cond_a": 0}) == LEGACY_EMPTY
261
+ and routing.bucket_health({}, total_nodes=0) == LEGACY_EMPTY,
262
+ "%r / %r" % (routing.bucket_health({}),
263
+ routing.bucket_health({"cond_a": 0})))
264
+ check("C5 缺省时键集亦未变(不给空桶路径偷加键)",
265
+ set(routing.bucket_health({})) == set(LEGACY_EMPTY),
266
+ str(sorted(routing.bucket_health({}))))
267
+ gc_counts = {"cond_a": 80, "cond_b": 10, "cond_c": 10}
268
+ check("C6 total_nodes 只参与「空 counts」一支:多桶库传任意值读数逐位一致",
269
+ routing.bucket_health(gc_counts, total_nodes=7)
270
+ == routing.bucket_health(gc_counts, total_nodes=99999)
271
+ == routing.bucket_health(gc_counts),
272
+ str([routing.bucket_health(gc_counts, total_nodes=7),
273
+ routing.bucket_health(gc_counts)]))
274
+ bad = routing.bucket_health({}, total_nodes=7)
275
+ check("C7 两种读数互不同一(有节点零分桶 ≠ 真空库,消费方可分流处置)",
276
+ set(bad) != set(LEGACY_EMPTY)
277
+ and bad.get("reason") != LEGACY_EMPTY["reason"], str(bad))
278
+ lie = routing.bucket_health(cg_self.index.get("buckets") or {})
279
+ truth = routing.bucket_health(cg_self.index.get("buckets") or {},
280
+ total_nodes=n_self)
281
+ check("C8 旧假话可复现且被新读数否证(缺省=ok True 冒充健康 / 显式传参=ok False)",
282
+ lie.get("ok") is True and truth.get("ok") is False
283
+ and truth.get("nodes") == n_self, "%r / %r" % (lie, truth), live=True)
284
+
285
+ # ================= ④ H9 假成功方向 =================
286
+ print("\n【④】H9:打标失败必须自证失败,且与成功形态互斥")
287
+ missing = "sem_不存在_%d" % os.getpid()
288
+ miss = protect.mark(cg_mark, missing, "语义守卫·显式保护标记")
289
+ check("D1 不存在节点打标读数显式标记失败(ok False + error 值在场)",
290
+ _fail_shape(miss), repr(miss))
291
+ check("D2 失败读数不得与成功形态混淆(成功形态判据必须为假)",
292
+ not _ok_shape(miss), repr(miss))
293
+ check("D3 失败读数不得被读成「打了保护位」(protected 不得为 True)",
294
+ not (isinstance(miss, dict) and miss.get("protected") is True),
295
+ repr(miss))
296
+ ts = trust.set_state(cg_mark, missing, "verified")
297
+ check("D4 与仓内既有负路由契约同形(trust.set_state 缺失分支:键集同、error 同值)",
298
+ _fail_shape(miss) and set(miss) == set(ts)
299
+ and miss.get("error") == ts.get("error"), "%r / %r" % (miss, ts))
300
+ shapes, details = [], []
301
+ for odd in ("", None, 0, True, ["x"], {"a": 1}, ("t",), 1.5):
302
+ try:
303
+ r = protect.mark(cg_mark, odd, "理由")
304
+ except Exception as exc: # noqa: BLE001
305
+ shapes.append(False)
306
+ details.append("%r -> RAISED %s" % (odd, type(exc).__name__))
307
+ else:
308
+ shapes.append(_fail_shape(r) and not _ok_shape(r))
309
+ details.append("%r -> %r" % (odd, r))
310
+ check("D5 畸形/空 node_id 一律不裸穿透且给出同一失败形态(消费方无需 try)",
311
+ all(shapes), ";".join(details))
312
+ try:
313
+ from .mcp_server import _protect_call
314
+ except Exception as exc: # noqa: BLE001
315
+ check("D6 MCP 面 _protect_call 可取到(否则 H9 的假成功无发生面)",
316
+ False, type(exc).__name__)
317
+ else:
318
+ mcp_bad = _protect_call(cg_mark, {"action": "mark", "node_id": missing,
319
+ "reason": "语义守卫·显式保护标记"})
320
+ mcp_ok = _protect_call(cg_mark, {"action": "mark", "node_id": "sem_mark_0",
321
+ "reason": "理由"})
322
+ check("D6 MCP 面 mark:缺失 id 不得给出成功形态,存在 id 必须给出成功形态",
323
+ _fail_shape(mcp_bad) and not _ok_shape(mcp_bad)
324
+ and _ok_shape(mcp_ok) and not _fail_shape(mcp_ok),
325
+ "%r / %r" % (mcp_bad, mcp_ok), live=True)
326
+ check("D7 MCP 面 mark 与判据单点逐位一致(无第二套包装/判据)",
327
+ mcp_bad == miss, "%r vs %r" % (mcp_bad, miss))
328
+ rt = json.loads(json.dumps(mcp_bad, ensure_ascii=False))
329
+ check("D8 失败读数经 JSON 往返(模拟 MCP 序列化)后仍可判别",
330
+ _fail_shape(rt) and not _ok_shape(rt), repr(rt), live=True)
331
+
332
+ # ================= ⑤ H9 返回值与事实一致(反方向) =================
333
+ print("\n【⑤】H9:成功必须真落保护位;失败必须真的什么都没发生")
334
+ mark_id = "sem_mark_%d" % os.getpid() # ⑤ 组专用新鲜节点(不被 D6 沾过)
335
+ cg_mark.add(mark_id, BODY, layer="contextual")
336
+ cg_mark.flush()
337
+ pre_prot, pre_why = protect.is_protected(cg_mark, mark_id)
338
+ check("E0 前置:该节点初始未被保护(防 fixture 自带保护位使 E3 空转)",
339
+ pre_prot is False, "%r / %r" % (pre_prot, pre_why), live=True)
340
+ before = protect.stats(cg_mark)["protected_count"]
341
+ r_bad = protect.mark(cg_mark, missing, "理由")
342
+ after = protect.stats(cg_mark)["protected_count"]
343
+ check("E1 失败打标真的什么都没发生:无节点被物化、保护位未设、保护计数不变",
344
+ _fail_shape(r_bad) and cg_mark.get(missing) is None
345
+ and protect.is_protected(cg_mark, missing)[0] is False
346
+ and before == after,
347
+ "%r / %r / %d->%d" % (r_bad, cg_mark.get(missing), before, after),
348
+ live=True)
349
+ r_ok = protect.mark(cg_mark, mark_id, "语义守卫·保护标记")
350
+ pro, why = protect.is_protected(cg_mark, mark_id)
351
+ disk_fm = (cg_mark.get(mark_id) or {}).get("frontmatter") or {}
352
+ check("E2 成功打标返回值与事实三面一致(返回体 / 内存判据 / 盘上 frontmatter)",
353
+ _ok_shape(r_ok) and pro is True
354
+ and disk_fm.get("protected") is True
355
+ and disk_fm.get("protection_reason") == "语义守卫·保护标记",
356
+ "%r / %r / %r" % (r_ok, pro, disk_fm), live=True)
357
+ try:
358
+ protect.guard_forget(cg_mark, mark_id)
359
+ except Exception as exc: # noqa: BLE001
360
+ check("E3 打标成功后守卫真的拦得住遗忘(ProtectionError 在场)",
361
+ type(exc).__name__ == "ProtectionError", type(exc).__name__,
362
+ live=True)
363
+ else:
364
+ check("E3 打标成功后守卫真的拦得住遗忘(ProtectionError 在场)",
365
+ False, "guard_forget 静默放行——「打标成功」是空话")
366
+ r_again = protect.mark(cg_mark, mark_id, "语义守卫·保护标记二")
367
+ check("E4 幂等:同节点再打标仍成功、原因更新、保护位仍在",
368
+ _ok_shape(r_again)
369
+ and protect.is_protected(cg_mark, mark_id)[0] is True
370
+ and protect.is_protected(cg_mark, mark_id)[1] == "语义守卫·保护标记二",
371
+ "%r / %r" % (r_again, protect.is_protected(cg_mark, mark_id)))
372
+
373
+ # ================= ⑥ 单点与接线 =================
374
+ print("\n【⑥】单点与接线(判据唯一真源 + 呼叫点显式传参)")
375
+ seen = []
376
+ orig_bh = routing.bucket_health
377
+
378
+ def _recorder(counts, total_nodes=None):
379
+ seen.append((dict(counts), total_nodes))
380
+ return orig_bh(counts, total_nodes=total_nodes)
381
+
382
+ routing.bucket_health = _recorder
383
+ try:
384
+ h_from_health = cg_self.health()
385
+ finally:
386
+ routing.bucket_health = orig_bh
387
+ check("F1 呼叫点把节点总数显式传进判据(否则「有节点零分桶」不可判别)",
388
+ len(seen) >= 1 and all(s[1] == n_self for s in seen),
389
+ str(seen), live=True)
390
+ ref = orig_bh(cg_self.index.get("buckets") or {}, total_nodes=n_self)
391
+ keys = ("ok", "reason", "buckets", "nodes", "max_bucket_share",
392
+ "singleton_ratio", "expected_scan", "problems")
393
+ check("F2 体检面的分区读数 == 判据单点的读数(不是第二份判据)",
394
+ all(h_from_health.get(k) == ref.get(k) for k in keys),
395
+ "%r / %r" % ({k: h_from_health.get(k) for k in keys},
396
+ {k: ref.get(k) for k in keys}))
397
+ check("F3 判据单点在位且空桶缺省读数不变(旧调用方零变更)",
398
+ callable(orig_bh) and orig_bh({}) == LEGACY_EMPTY,
399
+ repr(orig_bh({})))
400
+
401
+ # ================= ⑦ floor =================
402
+ print("\n【⑦】live floor(防空转绿)")
403
+ print(" [INFO] live 断言数 = %d(floor=%d)" % (LIVE, LIVE_FLOOR))
404
+ check("G1 live 断言数不低于 floor", LIVE >= LIVE_FLOOR, "live=%d" % LIVE)
405
+
406
+ print("\n" + "=" * 66)
407
+ print("PASS=%d FAIL=%d LIVE=%d" % (PASS, FAIL, LIVE))
408
+ if FAILS:
409
+ print("失败项:" + ";".join(FAILS))
410
+ return 0 if FAIL == 0 else 1
411
+
412
+
413
+ # =========================================================================
414
+ # 定点变异(把修复逐点抽回改动前语义)——字节保真改写 → 跑 → finally 复原
415
+ # =========================================================================
416
+
417
+ def _read_bytes(rel):
418
+ with open(os.path.join(_REPO, rel), "rb") as f:
419
+ return f.read()
420
+
421
+
422
+ def _j(*lines):
423
+ return "\n".join(lines) + "\n"
424
+
425
+
426
+ MUTATIONS = {
427
+ # M3①:巨桶判据退回「无 nb > 1 守卫」⇒ 从未分区的库被读成「巨桶·分区失效」
428
+ "m3_bucket_guard": ("md_cg/routing.py",
429
+ " if nb > 1 and top / n > 0.30:\n",
430
+ " if top / n > 0.30:\n"),
431
+ # M3②:单桶如实读数退回泛化的「路由无效·接近全量」
432
+ "m3_single_read": ("md_cg/routing.py",
433
+ " if nb == 1:\n",
434
+ " if False:\n"),
435
+ # M3③a:空 counts 的「有节点」分支抽掉(保留形参)
436
+ "m3_empty_branch": ("md_cg/routing.py",
437
+ " if total_nodes and total_nodes > 0:\n",
438
+ " if False:\n"),
439
+ # M3③b:呼叫点退回只传 counts(等价改动前形态)
440
+ "m3_call_site": ("md_cg/mdcg.py",
441
+ _j(' h = routing.bucket_health(self.index.get("buckets", {}),',
442
+ ' total_nodes=len(self.index["nodes"]))'),
443
+ _j(' h = routing.bucket_health(self.index.get("buckets", {}))')),
444
+ # H9:失败分支退回裸 None(None 与成功在调用方不可区分)
445
+ "h9_none": ("md_cg/protect.py",
446
+ _j(' return {"ok": False, "error": "node_not_found", '
447
+ '"node_id": node_id}'),
448
+ _j(" return None")),
449
+ # H9 反向:失败分支退回「成功形态」(不存在的 id 读成打标成功)
450
+ "h9_fake_ok": ("md_cg/protect.py",
451
+ _j(' return {"ok": False, "error": "node_not_found", '
452
+ '"node_id": node_id}'),
453
+ _j(' return {"node_id": node_id, "protected": True, '
454
+ '"reason": reason}')),
455
+ # H9 反向:一律走失败分支(成功路径被抽掉——防守卫片面)
456
+ "h9_always_fail": ("md_cg/protect.py",
457
+ _j(" if not node:",
458
+ ' return {"ok": False, "error": "node_not_found", '
459
+ '"node_id": node_id}'),
460
+ _j(" if True:",
461
+ ' return {"ok": False, "error": "node_not_found", '
462
+ '"node_id": node_id}')),
463
+ }
464
+
465
+
466
+ def _run_child():
467
+ env = dict(os.environ)
468
+ env["PYTHONUTF8"] = "1"
469
+ p = subprocess.run([sys.executable, "-X", "utf8", "-m",
470
+ "md_cg.test_m3_h9_semantic_guard"],
471
+ cwd=_REPO, env=env, capture_output=True, text=True,
472
+ encoding="utf-8", errors="replace")
473
+ return p.returncode, (p.stdout or "") + (p.stderr or "")
474
+
475
+
476
+ def _mutate(name):
477
+ rel, old, new = MUTATIONS[name]
478
+ path = os.path.join(_REPO, rel)
479
+ raw = _read_bytes(rel)
480
+ text = raw.decode("utf-8")
481
+ if text.count(old) != 1:
482
+ raise SystemExit("[MUT] %s:锚点命中 %d 次(应为 1)——树已变,请复核"
483
+ % (name, text.count(old)))
484
+ try:
485
+ with open(path, "wb") as f:
486
+ f.write(text.replace(old, new, 1).encode("utf-8"))
487
+ rc, out = _run_child()
488
+ finally:
489
+ with open(path, "wb") as f:
490
+ f.write(raw)
491
+ restored = _read_bytes(rel) == raw
492
+ reds = [ln.strip() for ln in out.splitlines() if "[FAIL]" in ln]
493
+ print("\n" + "=" * 66)
494
+ print("[MUT] %s(%s)→ 子进程 rc=%d" % (name, rel, rc))
495
+ for ln in reds:
496
+ print(" RED " + ln)
497
+ print("[MUT] 红项数 = %d" % len(reds))
498
+ print("[MUT] 恢复核对:%s 逐字节复原=%s" % (rel, restored))
499
+ return rc, len(reds), restored
500
+
501
+
502
+ if __name__ == "__main__":
503
+ args = list(sys.argv[1:])
504
+ _mut = [a for a in args if a.startswith("--mutation")]
505
+ if _mut:
506
+ want = _mut[-1].split("=", 1)[1] if "=" in _mut[-1] else "all"
507
+ names = list(MUTATIONS) if want == "all" else [want]
508
+ bad = []
509
+ for nm in names:
510
+ if nm not in MUTATIONS:
511
+ raise SystemExit("未知变异:%s" % nm)
512
+ rc, nred, ok = _mutate(nm)
513
+ if rc == 0 or nred == 0 or not ok:
514
+ bad.append(nm)
515
+ print("\n[SUMMARY] 变异 %d 项;未变红或未复原的:%s"
516
+ % (len(names), bad or "无"))
517
+ _close_all()
518
+ _cleanup()
519
+ sys.exit(1 if bad else 0)
520
+ try:
521
+ rc = main()
522
+ finally:
523
+ _close_all()
524
+ _cleanup()
525
+ sys.exit(rc)
@@ -26,6 +26,7 @@ import sys
26
26
  import tempfile
27
27
  import time
28
28
 
29
+ from . import audit
29
30
  from . import conformance as CF
30
31
  from . import crosscheck as CC
31
32
  from . import mdcos
@@ -347,16 +348,27 @@ def phase_c(tmp):
347
348
  ok(r3.get("ok") and r3.get("skipped") and not r3.get("committed"),
348
349
  "C10 无有效意见 → skipped 不落库(零噪声)")
349
350
 
350
- # C4b 合规规则库缺失 → 合规闸 DEFER → 转审核队列(诚实降级,不假装落库)
351
+ # C4b 策略不可用 → 写入 fail-closed(不落盘、不入审核队列)。
352
+ # 迁移(issue #43,2026-09-29):原断言为「无规则库 → DEFER → 转审核队列」。
353
+ # 该口径正是本 issue 的病态出口——DEFER 走 propose,意见正文(可能含凭据)
354
+ # 明文落 hippocampus/inbox.jsonl 且不脱敏(脱敏只在 REJECT 分支)。使用者
355
+ # 裁定方向②③:策略不可用时必须**在提案入队之前** fail-closed。故按语义
356
+ # 更新本断言:同一场景下 moved_to=policy_unavailable 且 committed=False。
357
+ # 触发条件也随之明确:env 未设 **且** 包内默认不可得(否则回落包内策略,
358
+ # 不属「不可用」)——故临时把包内默认路径指向不存在的文件。
351
359
  _saved = os.environ.pop("MDCG_POLICY_FILE", None)
360
+ _saved_dfl = audit.default_policy_path
361
+ audit.default_policy_path = lambda: os.path.join(root, "_no_such_policy.json")
352
362
  try:
353
363
  r3b = PL.apply_opinions(cg, pkg, _ops(), reviewer="w", applier="a",
354
364
  node_id="mr_opinion_nopolicy")
355
- ok(not r3b.get("committed") and r3b.get("moved_to") == "review_queue"
365
+ ok(not r3b.get("committed")
366
+ and r3b.get("moved_to") == "policy_unavailable"
356
367
  and r3b.get("deferred") is True,
357
- "C4b 无规则库 → 转审核队列而非落盘(moved_to=%s deferred=%s)"
368
+ "C4b 策略不可用 → fail-closed 不落盘不入队(moved_to=%s deferred=%s)"
358
369
  % (r3b.get("moved_to"), r3b.get("deferred")))
359
370
  finally:
371
+ audit.default_policy_path = _saved_dfl
360
372
  if _saved is not None:
361
373
  os.environ["MDCG_POLICY_FILE"] = _saved
362
374
 
@@ -109,7 +109,7 @@ def _base_shape(tmp):
109
109
  r = cg.verify("n_plain", "普通节点反例", "falsified")
110
110
  ok(r.get("action") == "falsified", "G1a 普通 knowledge 节点 falsified 照常放行")
111
111
  ok(not _exists(cg, "n_plain"), "G1b 原 knowledge 节点按负记忆化移除(语义不变)")
112
- neg = [e for e in (cg.index.get("nodes") or {}).values()
112
+ neg = [e for e in list((cg.index.get("nodes") or {}).values())
113
113
  if e.get("layer") == "rejected"]
114
114
  ok(len(neg) == 1, "G1c rejected 层出现负记忆条目(语义不变)")
115
115