@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,502 @@
1
+ # -*- coding: utf-8 -*-
2
+ """C-8 守卫:RRF 读路径的门控状态**逐路断言**(search / search_rrf / MdCGSecure.search_rrf)。
3
+
4
+ 病灶(C-8 清单;本守卫作者按清单要求独立复现过一次,见 P3 的实测数字):
5
+ `apply_retrieval_gates`(md_cg/mdcg.py)自称「两份 search 的唯一实现」,但
6
+ `MdCGOS.search_rrf`(mdcos.py:1267 → `MdCGSecure.search_rrf`,mdcos.py:4164)
7
+ **整段不含门控**——meta 连 gates 键都没有、scanned 恒全表。同一开关下两条
8
+ **生产读路径**的候选面分裂:
9
+ cg(op=read, query) → MdCGSecure.search → 收敛(S1 实测 38→23)
10
+ cg(op=read, budget_tokens=…) → MdCGSecure.search_rrf → 恒 38(不收敛)
11
+ 既有守卫 `test_retr_gates_prodpath.py` 只覆盖 search 一路(其 P1/P3 皆调
12
+ cg.search)——「只覆盖一路」正是缺口本体:另两路即便完全不过门控也无红灯。
13
+
14
+ 本守卫的判据面(三路 = 三个入口,逐路断言,不靠读码):
15
+ P1 库面元数据就位:桶 / big_domain / observation_position 确实落盘(否则后续断言无意义)
16
+ P2 默认零变更:总开关未设 → 三路 meta 均无 gates 键;且与「总开关开 + 子门控全关」
17
+ 逐位同结果(零变更的另一半:未显式开门的开关保持原语义)
18
+ P3 逐路门控生效:总开关开 → 三路各自 gates 出现、scanned 真实收敛
19
+ (**修前 search_rrf 两路必红**:gates=None、scanned=38)
20
+ P4 三路审计**逐位相同**(同一实现 ⇒ 同一审计块)——即「两条路逐位一致」的可判定形态
21
+ P5 候选面对照(开门 vs 关门,机械可判定):结果 id 集合逐路比对,
22
+ 被门控剔除的组(域外 / 异桶 / 观测位不符)可点名
23
+ P6 结构守卫:三个入口的源码同引共享实现;且门控实现只有一份
24
+
25
+ 运行:
26
+ python -X utf8 -m md_cg.test_c8_search_rrf_gates # 正向(P1–P6)
27
+ python -X utf8 -m md_cg.test_c8_search_rrf_gates --mutate # 定点变异自证
28
+ python -X utf8 -m md_cg.test_c8_search_rrf_gates --mutate --list # 只列变异表
29
+
30
+ 退出码(fail-closed):
31
+ 0 = 全绿;1 = 有断言失败 / 变异未按预期条数转红 / 基线非 0 红;
32
+ 2 = **ANCHOR-MISS**(变异锚点在当前源码里找不到唯一命中——实现改了却没同步
33
+ 本表,本表就是判据,锚点漂移必须硬失败,不得静默跳过)。
34
+
35
+ **基线纪律(--mutate)**:变异**不在工作区落盘**——先在系统临时目录建最小镜像树
36
+ (`md_cg/` 副本 + `data/policy.json`,只读复制),再对镜像树的源文件做定点文本替换,
37
+ 每次变异前从工作区重新取原始副本(保证逐次独立)。工作区只读,绝不写入。
38
+ 变异覆盖的是 C-8 接线点本身(`MdCGOS.search_rrf` 的门控调用、`MdCGSecure` 的委托、
39
+ 以及共享实现的存在性),故「C-8 回退即红」可被后人一键复跑。
40
+ """
41
+ import inspect
42
+ import io
43
+ import json
44
+ import os
45
+ import shutil
46
+ import subprocess
47
+ import sys
48
+ import tempfile
49
+ import time
50
+ import unicodedata
51
+
52
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
53
+
54
+ from md_cg import routing # noqa: E402
55
+ from md_cg.mdcg import MdCG # noqa: E402
56
+ from md_cg.mdcos import MdCGSecure, MdCGOS # noqa: E402
57
+
58
+ PASS = 0
59
+ FAIL = 0
60
+ FAILS = []
61
+
62
+ #: 库面规模(三组节点 + 一组域内异桶):A 15 / B 15 / C 3 / D 5 = 38
63
+ #: A=域内主桶(S1 保留、S1b 保留);B=域外异桶(S1 靶);
64
+ #: C=域内主桶但观测位不符(S2 靶);D=域内异桶(S1b 靶)
65
+ QUERY = "记忆 认知 学习"
66
+ CTX = {"observation_position": "实验室", "domain": "认知心理"}
67
+ A_IDS = {"mem_%03d" % i for i in range(0, 15)}
68
+ B_IDS = {"mem_%03d" % i for i in range(15, 30)}
69
+ C_IDS = {"mem_%03d" % i for i in range(30, 33)}
70
+ D_IDS = {"mem_%03d" % i for i in range(33, 38)}
71
+ ALL_IDS = A_IDS | B_IDS | C_IDS | D_IDS
72
+ N_ALL = len(ALL_IDS)
73
+
74
+ #: 影响门控口径的进程级开关(逐条清理,防宿主环境的残留值改变本次实测口径)。
75
+ #: MDCG_HOTCACHE 显式置 0:query 结果缓存键(hotcache._ENV_SWITCHES)只认总开关,
76
+ #: 不含 S1/S1b/S2/S4 子开关——缓存命中会让「开/关对照」读到上一口径的结果。
77
+ GATE_ENV = ("MDCG_RETRIEVAL_PIPELINE", "MDCG_GATE_S1_DOMAIN",
78
+ "MDCG_GATE_S1B_BUCKET", "MDCG_GATE_S2_COND", "MDCG_GATE_S4_LAYER",
79
+ "MDCG_GATE_S3_SPREAD", "MDCG_GATE_S7_POSTINGS",
80
+ "MDCG_BUCKET_TOPK", "MDCG_BUCKET_MIN_SIM", "MDCG_HOTCACHE")
81
+
82
+
83
+ def check(name, cond, detail=""):
84
+ global PASS, FAIL
85
+ if cond:
86
+ PASS += 1
87
+ print(f" OK {name}")
88
+ else:
89
+ FAIL += 1
90
+ FAILS.append(name)
91
+ print(f" FAIL {name} {detail}")
92
+
93
+
94
+ class _Env:
95
+ """临时设置进程级开关:进入先清 GATE_ENV 全量,再按需置值;退出逐键还原。"""
96
+
97
+ def __init__(self, **kv):
98
+ self.kv = kv
99
+ self.old = {}
100
+
101
+ def __enter__(self):
102
+ self.old = {k: os.environ.get(k) for k in GATE_ENV}
103
+ for k in GATE_ENV:
104
+ os.environ.pop(k, None)
105
+ os.environ["MDCG_HOTCACHE"] = "0"
106
+ for k, v in self.kv.items():
107
+ if v is None:
108
+ os.environ.pop(k, None)
109
+ else:
110
+ os.environ[k] = v
111
+ return self
112
+
113
+ def __exit__(self, *_exc):
114
+ for k, v in self.old.items():
115
+ if v is None:
116
+ os.environ.pop(k, None)
117
+ else:
118
+ os.environ[k] = v
119
+ return False
120
+
121
+
122
+ def build_lib(root):
123
+ """确定性小库(建库期开总开关:门控元数据只在开关开时落索引)。
124
+
125
+ 域标签经 add(big_domain=…) 显式给出、桶经 tags 的 `domain:` 前缀路由。
126
+ **建库必须开 MDCG_RETRIEVAL_PIPELINE**:`_strip_empty_gate_fields`
127
+ (mdcg.py:685)的既有纪律是「总开关未设 → big_domain / observation_position
128
+ 两键一律不落条目」——这正是生产序列(开关打开后新写入的节点才带域标签,
129
+ 历史节点由 `backfill_big_domain` 补齐),故本库按该序列构造;否则索引里
130
+ 无域标签,S1/S2 无靶可打(守卫将退化为自证同义)。
131
+ """
132
+ with _Env(MDCG_RETRIEVAL_PIPELINE="1"):
133
+ cg = MdCGSecure(root)
134
+ for i in range(15): # A:域内(心理)+ 主桶(认知心理)
135
+ cg.add("mem_%03d" % i,
136
+ "# 功能名:认知心理样本 %d\n# 正文:记忆 认知 学习 心理 情感 性格 %d" % (i, i),
137
+ "knowledge", tags=["domain:认知心理"], big_domain="心理")
138
+ for i in range(15, 30): # B:域外(物理)+ 异桶,但词面同样命中查询
139
+ cg.add("mem_%03d" % i,
140
+ "# 功能名:力学物理样本 %d\n# 正文:记忆 物理 力学 电磁 量子 能量 %d" % (i, i),
141
+ "knowledge", tags=["domain:力学物理"], big_domain="物理")
142
+ for i in range(30, 33): # C:域内主桶,但观测位与情境不符(S2 靶)
143
+ cg.add("mem_%03d" % i,
144
+ "# 功能名:认知心理野外样本 %d\n# 正文:记忆 认知 学习 心理 野外 观测 %d" % (i, i),
145
+ "knowledge", tags=["domain:认知心理"], big_domain="心理",
146
+ condition_space={"observation_position": "野外"})
147
+ for i in range(33, 38): # D:域内(心理)但异桶(S1b 靶)
148
+ cg.add("mem_%03d" % i,
149
+ "# 功能名:记忆研究样本 %d\n# 正文:记忆 认知 学习 心理 研究 %d" % (i, i),
150
+ "knowledge", tags=["domain:记忆研究"], big_domain="心理")
151
+ cg.flush()
152
+ cg.close()
153
+
154
+
155
+ #: 本次进程建过的实验库根(系统临时目录);退出时统一清理——守卫跑一次建若干个
156
+ #: (每个开关态一个独立根),不清理会在 %TEMP% 里线性积压。
157
+ _TEMP_ROOTS = []
158
+
159
+
160
+ # 生效条件:始终在系统临时目录(tempfile.mkdtemp,前缀 mdcg_c8_)新建一个空目录并返回其 NFC 归一化绝对路径;目录登记进 _TEMP_ROOTS 供退出时清理。不触碰任何在役数据根。
161
+ def fresh_root():
162
+ d = unicodedata.normalize("NFC", tempfile.mkdtemp(prefix="mdcg_c8_"))
163
+ _TEMP_ROOTS.append(d)
164
+ return d
165
+
166
+
167
+ # 生效条件:把本进程建过的实验库根逐一 rmtree(不抛);可在任何退出路径调用,重复调用为空操作。先 gc.collect() 再最多重试 3 轮(每轮间隔 0.2s)——Windows 上 main() 刚返回时仍有未释放的文件句柄,单次 rmtree 会静默失败(实测 7 个根漏 5 个),重试后收敛为 0。
168
+ def _cleanup():
169
+ import gc
170
+ gc.collect()
171
+ while _TEMP_ROOTS:
172
+ path = _TEMP_ROOTS.pop()
173
+ for _try in range(3):
174
+ shutil.rmtree(path, ignore_errors=True)
175
+ if not os.path.exists(path):
176
+ break
177
+ time.sleep(0.2)
178
+
179
+
180
+ def measure(env, ctx):
181
+ """在给定开关态下取三路读数:结果 id 集合 / scanned / gates 审计块。
182
+
183
+ 三路(同一库、同一 query、同一开关):
184
+ search —— MdCGSecure.search(生产链 mcp_server → MdCGSecure → MdCGOS)
185
+ secure_rrf —— MdCGSecure.search_rrf(mdcg_recall / cg(op=read, budget_tokens=…))
186
+ os_rrf —— MdCGOS.search_rrf(基类实现;bench/eval 侧入口)
187
+ **每个开关态一个独立库根**:索引快照是派生物,跨态复用会让「带标签/不带
188
+ 标签」的条目形状随快照写入顺序漂移(实测:总开关关时落下的快照不带域标签,
189
+ 被后续开关态复用则 S1/S2 无靶可打)——独立根消除该串扰。
190
+ """
191
+ root = fresh_root()
192
+ build_lib(root)
193
+ with _Env(**env):
194
+ cg = MdCGSecure(root)
195
+ base = MdCGOS(root)
196
+ try:
197
+ rs, ms = cg.search(QUERY, k=40, judge=False, record=False, context=ctx)
198
+ rr, mr = cg.search_rrf(QUERY, k=40, judge=False, record=False, context=ctx)
199
+ rb, mb = base.search_rrf(QUERY, k=40, judge=False, record=False, context=ctx)
200
+ finally:
201
+ cg.close()
202
+ base.close()
203
+ return {
204
+ "root": root,
205
+ "search": {"gates": ms.get("gates"), "scanned": ms.get("scanned"),
206
+ "ids": sorted(x[0]["id"] for x in rs)},
207
+ "secure_rrf": {"gates": mr.get("gates"), "scanned": mr.get("scanned"),
208
+ "ids": sorted(x[0]["id"] for x in rr)},
209
+ "os_rrf": {"gates": mb.get("gates"), "scanned": mb.get("scanned"),
210
+ "ids": sorted(x[0]["id"] for x in rb)},
211
+ }
212
+
213
+
214
+ def _gates_json(reading):
215
+ return json.dumps(reading["gates"], ensure_ascii=False, sort_keys=True,
216
+ default=str)
217
+
218
+
219
+ # ===================== 定点变异自证(--mutate) =====================
220
+
221
+ #: 工作区根(本文件位于 <repo>/md_cg/)——镜像树从这里只读取材。
222
+ _REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
223
+
224
+ # 生效条件:--mutate 模式下,逐条对镜像树的 rel 文件做 old→new 的**唯一**文本替换(命中数必须为 1,否则 ANCHOR-MISS),随后跑本守卫正向面,比对该条应被打红的项数 expect。old 里的 \n 按目标文件实际换行(CRLF/LF)还原。
225
+ _MUTATIONS = (
226
+ ("C8-M1 断线:search_rrf 不再调用共享门控(退回 C-8 病态:无 gates 键、恒全表)",
227
+ "md_cg/mdcos.py",
228
+ " entries, gates = apply_retrieval_gates(\n"
229
+ " entries, terms, big_domain, context, 1)\n",
230
+ " gates = {}\n", 26),
231
+ ("C8-M2 域信号断供:search_rrf 传 big_domain=None(S1 退化 no_domain_signal)",
232
+ "md_cg/mdcos.py",
233
+ " entries, terms, big_domain, context, 1)\n",
234
+ " entries, terms, None, context, 1)\n", 11),
235
+ ("C8-M3 ctx 断供:search_rrf 传 context=None(S2 硬槽在 RRF 路失效)",
236
+ "md_cg/mdcos.py",
237
+ " entries, terms, big_domain, context, 1)\n",
238
+ " entries, terms, big_domain, None, 1)\n", 6),
239
+ ("C8-M4 第三份实现:RRF 入口自建 gates[\"s1b\"] 审计块(破坏「唯一实现」)",
240
+ "md_cg/mdcos.py",
241
+ " _gates_meta = {\"gates\": gates} if gates else {}\n",
242
+ " gates[\"s1b\"] = {\"third\": \"copy\"}\n"
243
+ " _gates_meta = {\"gates\": gates} if gates else {}\n", 11),
244
+ ("C8-M5 委托断裂:MdCGSecure.search_rrf 不再走 super()(父类接线被绕过)",
245
+ "md_cg/mdcos.py",
246
+ " res, meta = super().search_rrf(*a, **kw)\n",
247
+ " return [], {\"tier\": \"RRF\"}\n", 21),
248
+ ("C8-M6 门控错位:search_rrf 传 min_results=10**9(S1 恒回退不收敛)",
249
+ "md_cg/mdcos.py",
250
+ " entries, terms, big_domain, context, 1)\n",
251
+ " entries, terms, big_domain, context, 10**9)\n", 16),
252
+ ("C8-M7 零变更破坏:RRF 入口 meta 恒落 gates 键(默认态也落)",
253
+ "md_cg/mdcos.py",
254
+ " _gates_meta = {\"gates\": gates} if gates else {}\n",
255
+ " _gates_meta = {\"gates\": gates}\n", 4),
256
+ ("C8-M8 结构:MdCG.search 不再引用共享门控(共享实现被抄散)",
257
+ "md_cg/mdcg.py",
258
+ " entries, gates = apply_retrieval_gates(\n"
259
+ " entries, terms, big_domain, context, min_results)\n",
260
+ " entries, gates = entries, {}\n", 1),
261
+ )
262
+
263
+
264
+ # 生效条件:在系统临时目录建最小镜像树——只读复制 <repo>/md_cg(排除 __pycache__)与 <repo>/data/policy.json(写入闸门的 fail-closed 依赖它);返回镜像根。工作区不被写入。
265
+ def _overlay_build():
266
+ root = unicodedata.normalize("NFC", tempfile.mkdtemp(prefix="mdcg_c8_mut_"))
267
+ shutil.copytree(os.path.join(_REPO, "md_cg"), os.path.join(root, "md_cg"),
268
+ ignore=shutil.ignore_patterns("__pycache__"))
269
+ os.makedirs(os.path.join(root, "data"), exist_ok=True)
270
+ _pol = os.path.join(_REPO, "data", "policy.json")
271
+ if os.path.exists(_pol):
272
+ shutil.copy2(_pol, os.path.join(root, "data", "policy.json"))
273
+ return root
274
+
275
+
276
+ #: 变异表涉及的文件集合(每次变异前逐一路从工作区重取原始副本,保证逐次独立)。
277
+ _MUT_FILES = tuple(sorted({rel for _n, rel, _o, _x, _e in _MUTATIONS}))
278
+
279
+
280
+ def _run_guard_in(cwd):
281
+ """在 cwd 里跑本守卫**正向面**(不带 --mutate,无递归),返回 (rc, 红项列表)。"""
282
+ p = subprocess.run([sys.executable, "-X", "utf8", "-m",
283
+ "md_cg.test_c8_search_rrf_gates"],
284
+ cwd=cwd, capture_output=True, text=True,
285
+ encoding="utf-8", errors="replace", timeout=900)
286
+ out = (p.stdout or "") + (p.stderr or "")
287
+ reds = [l.strip()[5:].strip() for l in out.splitlines()
288
+ if l.strip().startswith("FAIL ")]
289
+ return p.returncode, reds, out
290
+
291
+
292
+ def _mutate_mode(list_only=False):
293
+ print("!! C-8 定点变异自证:逐条把接线退回病态,套件必须按预期条数转红\n")
294
+ if list_only:
295
+ for name, rel, _o, _n, exp in _MUTATIONS:
296
+ print(" %-54s %-14s expect_red=%d" % (name[:54], rel, exp))
297
+ return 0
298
+
299
+ overlay = _overlay_build()
300
+ anchor_miss, bad = [], []
301
+ try:
302
+ rc, reds, _out = _run_guard_in(overlay)
303
+ print(" 未变异基线(临时镜像树):红项=%d(必须为 0)、rc=%d"
304
+ % (len(reds), rc))
305
+ if reds or rc != 0:
306
+ bad.append("未变异基线即失败(红=%d rc=%d)" % (len(reds), rc))
307
+ for name, rel, old, new, expect in _MUTATIONS:
308
+ path = os.path.join(overlay, *rel.split("/"))
309
+ # 每次变异前把**变异表涉及的每个文件**都从工作区重取:逐次独立
310
+ # (只还原当前文件会让上一处变异在别的文件上残留、污染本次读数——
311
+ # 本表实测踩过:M7 留在 mdcos.py 的改动使 M8 多红 4 项),且绝不写工作区
312
+ for _r in _MUT_FILES:
313
+ shutil.copy2(os.path.join(_REPO, *_r.split("/")),
314
+ os.path.join(overlay, *_r.split("/")))
315
+ data = open(path, "rb").read()
316
+ _nl = "\r\n" if b"\r\n" in data else "\n"
317
+ _o = old.replace("\n", _nl).encode("utf-8")
318
+ _n = new.replace("\n", _nl).encode("utf-8")
319
+ if data.count(_o) != 1:
320
+ print(" ANCHOR-MISS %s —— 锚点在 %s 当前源码里命中 %d 次"
321
+ "(本表就是判据,漂移必须硬失败)"
322
+ % (name, rel, data.count(_o)))
323
+ anchor_miss.append(name)
324
+ continue
325
+ open(path, "wb").write(data.replace(_o, _n))
326
+ rc, reds, _out = _run_guard_in(overlay)
327
+ mark = "OK " if len(reds) == expect else "MISMATCH"
328
+ verdict = "红" if reds else "**仍全绿 = 该判据空转**"
329
+ print(" %s %-54s 红项=%d 预期=%d rc=%d %s"
330
+ % (mark, name[:54], len(reds), expect, rc, verdict))
331
+ for r in reds:
332
+ print(" " + r)
333
+ if len(reds) != expect:
334
+ bad.append("%s(红=%d 预期=%d)" % (name, len(reds), expect))
335
+ finally:
336
+ shutil.rmtree(overlay, ignore_errors=True)
337
+
338
+ if anchor_miss:
339
+ print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
340
+ print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
341
+ return 2
342
+ print("\nC-8 定点变异自证:%s"
343
+ % ("PASS(每处接线都被打红且恰好命中预期项数)" if not bad
344
+ else "FAIL —— " + "、".join(bad)))
345
+ return 0 if not bad else 1
346
+
347
+
348
+ def main():
349
+ # ---------------- P1 库面元数据就位 ----------------
350
+ print("== P1 库面元数据就位(桶 / 域 / 观测位)==")
351
+ root = fresh_root()
352
+ print("root =", root)
353
+ build_lib(root)
354
+ cg = MdCGSecure(root)
355
+ idx = cg.index["nodes"]
356
+ n_all = len(idx)
357
+ b_ok = {}
358
+ for nid in sorted(ALL_IDS):
359
+ e = idx.get(nid) or {}
360
+ b_ok[nid] = (routing.bucket_key_readable(e.get("bucket") or ""),
361
+ e.get("big_domain"), e.get("observation_position"))
362
+ cg.close()
363
+ check("P1a 节点数与分组一致", n_all == N_ALL, f"index={n_all} expect={N_ALL}")
364
+ check("P1b A 组桶/域落盘",
365
+ all(b_ok[i][0] == "认知心理" and b_ok[i][1] == "心理" for i in sorted(A_IDS)),
366
+ str(sorted(b_ok.items())[:3]))
367
+ check("P1c B 组桶/域落盘",
368
+ all(b_ok[i][0] == "力学物理" and b_ok[i][1] == "物理" for i in sorted(B_IDS)),
369
+ str(sorted(b_ok.items())[15:18]))
370
+ check("P1d C 组观测位落盘(S2 靶)",
371
+ all(b_ok[i][2] == "野外" for i in sorted(C_IDS)), str(sorted(C_IDS)))
372
+ check("P1e D 组域内异桶落盘(S1b 靶)",
373
+ all(b_ok[i][0] == "记忆研究" and b_ok[i][1] == "心理" for i in sorted(D_IDS)),
374
+ str(sorted(b_ok.items())[33:36]))
375
+
376
+ # ---------------- P2 默认零变更 ----------------
377
+ print("== P2 默认零变更(总开关未设 / 总开关开但子门控全关)==")
378
+ off = measure({}, None)
379
+ alloff = measure({"MDCG_RETRIEVAL_PIPELINE": "1",
380
+ "MDCG_GATE_S1_DOMAIN": "0",
381
+ "MDCG_GATE_S2_COND": "0"}, None)
382
+ for path in ("search", "secure_rrf", "os_rrf"):
383
+ o, a = off[path], alloff[path]
384
+ check(f"P2a {path} 总开关未设 → meta 无 gates 键",
385
+ o["gates"] is None, str(o["gates"])[:120])
386
+ check(f"P2b {path} 子门控全关 ≡ 未设(结果逐位一致、无 gates 键)",
387
+ o["ids"] == a["ids"] and o["scanned"] == a["scanned"]
388
+ and a["gates"] is None,
389
+ f"off={len(o['ids'])}/{o['scanned']} alloff={len(a['ids'])}/{a['scanned']}"
390
+ f" gates={a['gates']}")
391
+ check("P2c 默认态三路候选面 = 全库(不收敛)",
392
+ all(off[p]["ids"] == sorted(ALL_IDS) and off[p]["scanned"] == N_ALL
393
+ for p in ("search", "secure_rrf", "os_rrf")),
394
+ json.dumps({p: [len(off[p]["ids"]), off[p]["scanned"]]
395
+ for p in ("search", "secure_rrf", "os_rrf")},
396
+ ensure_ascii=False))
397
+
398
+ # ---------------- P3 逐路门控生效 ----------------
399
+ print("== P3 逐路门控生效(总开关=1,S1 未设=开)==")
400
+ p1 = measure({"MDCG_RETRIEVAL_PIPELINE": "1"}, None)
401
+ for path in ("search", "secure_rrf", "os_rrf"):
402
+ g = p1[path]["gates"] or {}
403
+ check(f"P3a {path} gates 审计出现(修前 rrf 两路必红:gates=None)",
404
+ "s1" in g, str(p1[path]["gates"])[:160])
405
+ check(f"P3b {path} scanned 真实收敛({N_ALL} → 23)",
406
+ p1[path]["scanned"] == 23, f"scanned={p1[path]['scanned']}")
407
+ s1b_off = measure({"MDCG_RETRIEVAL_PIPELINE": "1",
408
+ "MDCG_GATE_S1B_BUCKET": "1",
409
+ "MDCG_BUCKET_TOPK": "1"}, None)
410
+ for path in ("search", "secure_rrf", "os_rrf"):
411
+ g = (s1b_off[path]["gates"] or {}).get("s1b") or {}
412
+ check(f"P3c {path} S1b 显式开门 → 桶收敛(23 → 18、键=认知心理)",
413
+ routing.bucket_key_readable((g.get("keys") or [""])[0]) == "认知心理"
414
+ and g.get("out") == 18 and s1b_off[path]["scanned"] == 18,
415
+ json.dumps(g, ensure_ascii=False)[:160])
416
+ s2 = measure({"MDCG_RETRIEVAL_PIPELINE": "1", "MDCG_GATE_S1_DOMAIN": "0",
417
+ "MDCG_GATE_S2_COND": "1"}, CTX)
418
+ for path in ("search", "secure_rrf", "os_rrf"):
419
+ g = (s2[path]["gates"] or {}).get("s2") or {}
420
+ check(f"P3d {path} S2 硬槽门控(观测位不符剔除恰 3 条)",
421
+ g.get("dropped") == 3 and s2[path]["scanned"] == N_ALL - 3,
422
+ json.dumps(g, ensure_ascii=False)[:160])
423
+ s4 = measure({"MDCG_RETRIEVAL_PIPELINE": "1", "MDCG_GATE_S4_LAYER": "1"}, None)
424
+ for path in ("search", "secure_rrf", "os_rrf"):
425
+ g = s4[path]["gates"] or {}
426
+ check(f"P3e {path} S4 层级审计(显式 =1 才落键)",
427
+ "s4" in g and g["s4"].get("layers"),
428
+ json.dumps(g, ensure_ascii=False)[:160])
429
+
430
+ # ---------------- P4 三路审计逐位相同 ----------------
431
+ print("== P4 三路审计逐位相同(同一实现 ⇒ 同一审计块)==")
432
+ for label, reading in (("S1", p1), ("S1b", s1b_off), ("S2", s2), ("S4", s4)):
433
+ gs = {p: _gates_json(reading[p]) for p in ("search", "secure_rrf", "os_rrf")}
434
+ check(f"P4 {label} 配置下三路 gates 逐位相同",
435
+ gs["search"] == gs["secure_rrf"] == gs["os_rrf"],
436
+ json.dumps(gs, ensure_ascii=False)[:300])
437
+
438
+ # ---------------- P5 候选面对照(开门 vs 关门)----------------
439
+ print("== P5 候选面对照(机械可判定:结果 id 集合逐路比对)==")
440
+ for path in ("search", "secure_rrf", "os_rrf"):
441
+ ids = set(p1[path]["ids"])
442
+ check(f"P5a {path} S1 开门:域外 B 组被剔除、域内保留",
443
+ ids == (A_IDS | C_IDS | D_IDS) and not (ids & B_IDS),
444
+ f"n={len(ids)} extra={sorted(ids - (A_IDS|C_IDS|D_IDS))[:5]}"
445
+ f" missing={sorted((A_IDS|C_IDS|D_IDS) - ids)[:5]}")
446
+ check(f"P5b {path} S1 开门结果 ⊆ 关门结果(收敛只减不增)",
447
+ ids <= set(off[path]["ids"]), f"n={len(ids)}")
448
+ ids1b = set(s1b_off[path]["ids"])
449
+ check(f"P5c {path} S1b 开门:异桶 D 组再被剔除(保 orphan 兜底语义不变)",
450
+ ids1b == (A_IDS | C_IDS) and not (ids1b & (B_IDS | D_IDS)),
451
+ f"n={len(ids1b)} extra={sorted(ids1b - (A_IDS|C_IDS))[:5]}")
452
+ ids2 = set(s2[path]["ids"])
453
+ check(f"P5d {path} S2 开门:观测位不符的 C 组被剔除",
454
+ not (ids2 & C_IDS) and ids2 == (ALL_IDS - C_IDS),
455
+ f"n={len(ids2)} C剩余={sorted(ids2 & C_IDS)[:5]}")
456
+ # 三条路在同一开关态下候选面一致(search 与 rrf 的**排序判据**不同是设计,
457
+ # 但门控后的**候选面**必须同一 —— 分裂病灶的可判定形态)
458
+ check(f"P5e {path} 关门态候选面 = 全库(对照基线成立)",
459
+ set(off[path]["ids"]) == ALL_IDS, f"n={len(off[path]['ids'])}")
460
+ check(f"P5f {path} 开门/关门对照非空(差异确实发生,不是自证同义)",
461
+ set(off[path]["ids"]) - ids == B_IDS,
462
+ f"diff={sorted(set(off[path]['ids']) - ids)[:5]}")
463
+
464
+ print("== P5-2 三路候选面互相一致(同一库同一开关)==")
465
+ for label, reading in (("默认", off), ("S1", p1), ("S1b", s1b_off),
466
+ ("S2", s2), ("S4", s4)):
467
+ sets = {p: tuple(reading[p]["ids"]) for p in ("search", "secure_rrf", "os_rrf")}
468
+ check(f"P5-2 {label} 三路结果 id 序列逐位一致",
469
+ sets["search"] == sets["secure_rrf"] == sets["os_rrf"],
470
+ f"len={[len(v) for v in sets.values()]}")
471
+
472
+ # ---------------- P6 结构守卫(防第三份实现)----------------
473
+ print("== P6 结构守卫(共享实现唯一)==")
474
+ src = {}
475
+ for mod, path in ((MdCG, os.path.join(os.path.dirname(__file__), "mdcg.py")),
476
+ (MdCGOS, os.path.join(os.path.dirname(__file__), "mdcos.py"))):
477
+ with io.open(path, encoding="utf-8") as f:
478
+ src[mod] = f.read()
479
+ for name, fn in (("MdCG.search", MdCG.search), ("MdCGOS.search", MdCGOS.search),
480
+ ("MdCGOS.search_rrf", MdCGOS.search_rrf)):
481
+ check(f"P6a {name} 源码引用共享门控 apply_retrieval_gates",
482
+ "apply_retrieval_gates" in inspect.getsource(fn))
483
+ check("P6b MdCGSecure.search_rrf 经 super().search_rrf 委托(不自建门控)",
484
+ "super().search_rrf" in inspect.getsource(MdCGSecure.search_rrf))
485
+ check("P6c 门控实现只有一份(gates[\"s1b\"] 仅出现在 mdcg.py)",
486
+ 'gates["s1b"]' in src[MdCG] and 'gates["s1b"]' not in src[MdCGOS])
487
+
488
+ print()
489
+ if FAILS:
490
+ print(f"FAILED: {len(FAILS)} 项 → {', '.join(FAILS)}")
491
+ return 1
492
+ print(f"ALL OK: {PASS} 项(C-8 三路门控守卫全绿)")
493
+ return 0
494
+
495
+
496
+ if __name__ == "__main__":
497
+ try:
498
+ if "--mutate" in sys.argv:
499
+ sys.exit(_mutate_mode("--list" in sys.argv))
500
+ sys.exit(main())
501
+ finally:
502
+ _cleanup()