@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,576 @@
1
+ # -*- coding: utf-8 -*-
2
+ # 功能名:B3 合并面「新正文去向」+ ⑤ 条件槽空值语义哨兵 守卫
3
+ # 生效条件:forgetting.reinforce 有 content/session 槽(:248)、mdcos.remember_gated
4
+ # 的 MERGE 分支传 content(:3475)、DROP 分支走 forgetting.record_drop
5
+ # (:3490 附近)、mdcos._ccg_field 对 _COND_FIELDS 做空值化(:160 区)、
6
+ # mdcos._declared_conditions._push 过 _is_null_condition、mdcos.check_consistency
7
+ # 剔除入参哨兵;沙箱条件:MDCG_ROOT / MDCG_AUX_ROOT / MDCG_MASTER_KEY 全指向
8
+ # tempfile.mkdtemp 建的独立目录,绝不触在役库。
9
+ # 子功能:① knowledge 层近重复第二次写入的**新值必须落库**(MERGE 侧:聚合行进目标
10
+ # 正文 + 返回体带 content_sink 去向;DROP 侧:全文与去向落 _forgetting.jsonl
11
+ # + 返回体带 dropped 去向单;加密级不落明文);② reinforce 缺省 content=None
12
+ # 与改动前逐位一致(既有调用零破坏);③ 聚合幂等(同文再写零追加);
13
+ # ④ H3 跨会话合并把双方归属并列落 fm(session 保留 + merge_sources);
14
+ # ⑤ CCG 哨兵「# 不适用条件:无」不再当真实条件:逐字相同的两节点不再判
15
+ # condition_clash、不建工单,而真互斥仍判冲突(防放宽)。
16
+ # 执行:python -X utf8 -m md_cg.test_b3_merge_keeps_content
17
+ # python -X utf8 -m md_cg.test_b3_merge_keeps_content --drop-fix=merge
18
+ # python -X utf8 -m md_cg.test_b3_merge_keeps_content --drop-fix=drop
19
+ # python -X utf8 -m md_cg.test_b3_merge_keeps_content --drop-fix=sentinel
20
+ # python -X utf8 -m md_cg.test_b3_merge_keeps_content --drop-fix=conv_session
21
+ # python -X utf8 -m md_cg.test_b3_merge_keeps_content --drop-fix=index
22
+ # 验证方式:本文件自跑(自带断言计数与退出码 rc=0/1);定点变异自证见 --drop-fix
23
+ # (把修复逐点抽回改动前语义 → 本守卫必红,且红项必须覆盖预期集合)。
24
+ # 不适用条件:情感交互|闲聊|纯查询无改动;CONVERGE(contextual 层同构聚合)的
25
+ # 80 字截断属同族第二处(本轮已一并收口为同一单点,见组 F 断言);
26
+ # mdcg.add 直连 consistency.check 的入参哨兵(mdcg.py:1785,非本席文件)
27
+ # 不在本守卫面内。
28
+ # 2026-09-30 补强(复核员实测的覆盖缺口,两条):
29
+ # · F5 —— mdcos.py:3466-3468 CONVERGE 分支调 writelimit.converge_into 时的
30
+ # `session=_writer_session(self, kw)` 接线,此前无断言覆盖(源码级抽掉实参
31
+ # 后本守卫全绿)。F5 钉住写盘 fm 的归属面(merge_sources)。
32
+ # · C4 —— forgetting.reinforce 的索引更新段此前只镜像 importance / lifecycle
33
+ # state / protected,不同步 merge_sources(与 writelimit.converge_into :257
34
+ # 不同口径)。C4 钉住「写盘后同进程读 cg.index 即见新值」。
35
+ """B3 + ⑤ 守卫:合并面不许静默吞正文,条件槽的哨兵不许当真实条件。
36
+
37
+ 缺陷(修复前,探针读数):
38
+ · B3:knowledge 层「近重复但关键值不同」的第二次写入,新正文 100% 丢弃——
39
+ MERGE 侧 `reinforce` 没有 content 槽、写回 `node.get("content")`(既有正文),
40
+ 新值全库 0 命中、目标正文逐字节未变(只 importance+0.05/merge_count+1);
41
+ DROP 侧(`assess` :214 的 internal_deterministic ∧ dup≥0.60 **先于** MERGE)
42
+ 连目标都不强化,新正文零去向。
43
+ · ⑤:CCG 哨兵「# 不适用条件:无」被原样收成负条件词,单字词「无」是既有生效
44
+ 条件「…;约束:无」的子串 ⇒ `_weighted_coverage` 恒 1.0 ≥ CLASH_HIGH(0.6)
45
+ ⇒ 任意两个带该哨兵的节点无条件 condition_clash——**连逐字相同也判 DEFER 并
46
+ 建工单**(实测 conflict_strength=1.0)。
47
+
48
+ 本守卫的判别力锚点(每条都可被 --drop-fix 逐点打红):
49
+ A 组 MERGE 侧:新值落库 / 聚合行进目标正文(行号可指)/ 返回体带去向 / 语义未变
50
+ (merge_count+1、importance+0.05、不新增节点)/ 幂等零追加 / content=None 逐位一致。
51
+ B 组 DROP 侧:分支仍判 DROP(未被改成 MERGE)+ 目标未被强化 + 返回体带 trace_id +
52
+ 留痕行含新正文全文(可检索)+ 加密级不落明文。
53
+ C 组 H3:跨会话合并 → 目标 fm.session 保留 + fm.merge_sources 记新写入方 + 新值落库。
54
+ D 组 ⑤:哨兵逐字相同 → ACCEPT/strength 0/无工单;真互斥 → 仍判冲突(防放宽);
55
+ 判据正反例(无/无。/(无)/None/空串/无依赖 命中;无法确定/无缓存场景/无持久化介质
56
+ 不命中);fm 列表形态与入参列表形态同口径;非条件字段不受影响。
57
+ F 组 CONVERGE 第二处:contextual 层同构聚合的新值落在 80 字外也不再丢(同单点)。
58
+
59
+ 沙箱(硬约束):一切读写都在 tempfile.mkdtemp 内;跑完 rmtree。绝不碰在役库。
60
+ """
61
+ import json
62
+ import os
63
+ import shutil
64
+ import sys
65
+ import tempfile
66
+ import time
67
+
68
+ # 沙箱必须在**任何** md_cg 子模块 import 之前设好(crypto.MASTER_FILE 在模块
69
+ # 导入时求值)。
70
+ _SANDBOX = tempfile.mkdtemp(prefix="b3merge_sandbox_")
71
+ os.environ["MDCG_AUX_ROOT"] = _SANDBOX
72
+ os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
73
+ os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
74
+ os.environ.pop("MDCG_TEST_LIVE_ROOT", None)
75
+
76
+ from . import consistency, forgetting, mdcos, writelimit # noqa: E402
77
+ from .mdcos import MdCGOS # noqa: E402
78
+
79
+ _OK = 0
80
+ _BAD = []
81
+ _RUN = [0] # 轮次计数:每轮 _run_all 用独立子根(见 _mk)
82
+
83
+ # 近重复对:只差一个端口号(关键值)
84
+ PORT_A = "9090"
85
+ PORT_B = "9595"
86
+
87
+
88
+ def _check(name, cond, detail=""):
89
+ global _OK
90
+ if cond:
91
+ _OK += 1
92
+ print(" ok %s" % name)
93
+ else:
94
+ _BAD.append(name)
95
+ print(" FAIL %s %s" % (name, detail))
96
+
97
+
98
+ def _body(port):
99
+ """六要素 + 正文的 CCG 文本(正文行含关键值 port)。"""
100
+ return ("# 功能名:部署面端口约定\n"
101
+ "# 生效条件:载体/位置:prod 集群;时间:2026-09-30 起;"
102
+ "方法:部署清单核对;约束:无\n"
103
+ "# 子功能:登记各服务监听端口\n"
104
+ "# 执行:核对 manifest 的 ports 段\n"
105
+ "# 验证方式:test\n"
106
+ "# 不适用条件:无\n"
107
+ "\n网关监听端口 8080/HTTP;管理面监听端口 8081/HTTP;"
108
+ f"指标面监听端口 {port}/HTTP;日志面监听端口 9091/HTTP;"
109
+ "链路面监听端口 9092/HTTP\n")
110
+
111
+
112
+ SENT_A = _body(PORT_A)
113
+ SENT_B = _body(PORT_B)
114
+ # 真负条件的对照体(哨兵换成真实条件)
115
+ REAL_A = SENT_A.replace("# 不适用条件:无", "# 不适用条件:本地开发环境")
116
+
117
+
118
+ def _mk(tag):
119
+ # 每轮 _run_all 用独立子根:--drop-fix 会连跑「变异态 + 恢复态」两轮,
120
+ # 共用目录会让第二轮读到第一轮的盘面/限流状态(实测 F1 假红)。
121
+ return MdCGOS(os.path.join(_SANDBOX, "lib_%s_r%d" % (tag, _RUN[0])),
122
+ autoflush=0)
123
+
124
+
125
+ def _lib_hits(cg, needle):
126
+ """全库节点正文里字面含 needle 的节点 id(判据:新值在库里可被检索)。"""
127
+ return sorted(n for n in cg.index["nodes"]
128
+ if needle in ((cg.get(n) or {}).get("content") or ""))
129
+
130
+
131
+ def _log_rows(cg, trace_id=None):
132
+ p = os.path.join(cg.root, forgetting.LOG_FILE)
133
+ out = []
134
+ if not os.path.exists(p):
135
+ return out
136
+ with open(p, encoding="utf-8") as f:
137
+ for line in f:
138
+ line = line.strip()
139
+ if not line:
140
+ continue
141
+ try:
142
+ r = json.loads(line)
143
+ except ValueError:
144
+ continue
145
+ if trace_id is None or r.get("trace_id") == trace_id:
146
+ out.append(r)
147
+ return out
148
+
149
+
150
+ # ---------------------------------------------------------------- A 组:MERGE 侧
151
+ def group_a():
152
+ print("\n[A] MERGE 侧(knowledge 层近重复,role=user):新正文去向")
153
+ cg = _mk("merge")
154
+ cg.session = "sess_merge"
155
+ cg.add("k_base", SENT_A, layer="knowledge")
156
+ n_before = len(cg.index["nodes"])
157
+ fm_before = dict(cg.get("k_base")["frontmatter"])
158
+ r = cg.remember_gated("k_second", SENT_B, layer="knowledge", role="user")
159
+
160
+ _check("A1 近重复第二次写入仍判 MERGE(合并语义未变)",
161
+ r.get("verdict") == "MERGE" and r.get("merged_into") == "k_base",
162
+ repr(r.get("verdict")))
163
+ _check("A2 新值在库中可检索(全库字面命中 = [目标节点])",
164
+ _lib_hits(cg, PORT_B) == ["k_base"], str(_lib_hits(cg, PORT_B)))
165
+ rein = r.get("reinforced") or {}
166
+ sink = rein.get("content_sink") or {}
167
+ body = (cg.get("k_base") or {}).get("content") or ""
168
+ _check("A3 聚合行落进目标正文(返回体给的 line 逐字在正文中)",
169
+ bool(sink.get("line")) and sink["line"] in body,
170
+ "line=%r" % (sink.get("line"),))
171
+ # line_no 必须先做**有界**检查再索引:真实回归(正文回写成旧正文)下
172
+ # line_no 指向旧正文之外,直接索引会抛 IndexError 裸崩——红项被截断、
173
+ # 后续组整段不跑(守卫员独立变异实测)。有界检查把「越界」变成干净的 FAIL。
174
+ _ln = int(sink.get("line_no") or 0)
175
+ _lines = body.splitlines()
176
+ _check("A4 返回体显式带去向(节点/行/行号,可定位)",
177
+ sink.get("node_id") == "k_base" and sink.get("action") == "appended"
178
+ and 1 <= _ln <= len(_lines)
179
+ and sink["line"] == _lines[_ln - 1],
180
+ json.dumps(sink, ensure_ascii=False)[:160])
181
+ _check("A5 MERGE 既有语义未变(不新增节点 / importance+0.05 / merge_count+1)",
182
+ len(cg.index["nodes"]) == n_before
183
+ and abs(float(rein.get("importance") or 0)
184
+ - (float(fm_before.get("importance") or 0.5) + 0.05)) < 1e-9
185
+ and int(rein.get("merge_count") or 0) == 1,
186
+ json.dumps(rein, ensure_ascii=False)[:120])
187
+ # 幂等:同文再写一次 → 零追加(正文长度不变、merge_count 仍 +1)
188
+ len1 = len(body)
189
+ r2 = cg.remember_gated("k_third", SENT_B, layer="knowledge", role="user")
190
+ body2 = (cg.get("k_base") or {}).get("content") or ""
191
+ _check("A6 聚合幂等:同文再写零追加(正文长度不变)",
192
+ r2.get("verdict") == "MERGE" and len(body2) == len1,
193
+ "%d -> %d" % (len1, len(body2)))
194
+ # 缺省 content=None:与改动前逐位一致(既有调用零破坏)
195
+ cg.add("k_plain", SENT_A, layer="knowledge")
196
+ raw_before = (cg.get("k_plain") or {}).get("content")
197
+ out = forgetting.reinforce(cg, "k_plain")
198
+ raw_after = (cg.get("k_plain") or {}).get("content")
199
+ _check("A7 reinforce 缺省 content=None:正文逐位不变(既有调用零破坏)",
200
+ raw_after == raw_before and (out or {}).get("content_sink", {})
201
+ .get("action") == "not_provided",
202
+ "before=%r after=%r" % (raw_before[:40], raw_after[:40]))
203
+ return cg
204
+
205
+
206
+ # ---------------------------------------------------------------- B 组:DROP 侧
207
+ def group_b():
208
+ print("\n[B] DROP 侧(internal_deterministic ∧ dup≥0.60):新正文去向")
209
+ cg = _mk("drop")
210
+ cg.session = "sess_drop"
211
+ cg.add("d_base", SENT_A, layer="knowledge")
212
+ imp0 = float(cg.get("d_base")["frontmatter"].get("importance") or 0)
213
+ r = cg.remember_gated("d_second", SENT_B, layer="knowledge",
214
+ role="command")
215
+ _check("B1 内部确定性来源仍判 DROP(:214 分支先于 MERGE,未被改成 MERGE)",
216
+ r.get("verdict") == "DROP", repr(r.get("verdict")))
217
+ fmd = cg.get("d_base")["frontmatter"]
218
+ _check("B2 DROP 未强化目标(importance 不变 / 无 merge_count)",
219
+ abs(float(fmd.get("importance") or 0) - imp0) < 1e-9
220
+ and not fmd.get("merge_count"),
221
+ "imp=%s mc=%s" % (fmd.get("importance"), fmd.get("merge_count")))
222
+ drop = r.get("dropped") or {}
223
+ _check("B3 返回体带去向单(trace_id / 留痕文件 / sha1 / 是否留全文)",
224
+ bool(drop.get("trace_id")) and drop.get("kept_in") == forgetting.LOG_FILE
225
+ and bool(drop.get("content_sha1")) and drop.get("content_kept") is True,
226
+ json.dumps(drop, ensure_ascii=False)[:160])
227
+ rows = _log_rows(cg, drop.get("trace_id"))
228
+ _check("B4 去向留痕可检索:该 trace_id 行含新正文全文(含关键新值)",
229
+ len(rows) == 1 and PORT_B in (rows[0].get("dropped_content") or "")
230
+ and rows[0].get("duplicate_with") == "d_base",
231
+ json.dumps(rows[:1], ensure_ascii=False)[:160])
232
+ _check("B5 留痕带会话/主体归属(谁写的、从哪个目标旁落)",
233
+ rows and rows[0].get("session") == "sess_drop"
234
+ and bool(rows[0].get("actor")),
235
+ json.dumps(rows[:1], ensure_ascii=False)[:120])
236
+ # 加密级:明文不落留痕(与写面 fail-closed 同口径)
237
+ cg2 = _mk("drop_sec")
238
+ cg2.add("s_base", SENT_A, layer="knowledge")
239
+ from .mdcos import MdCGSecure
240
+ from .security import Principal
241
+ sec = MdCGSecure(os.path.join(_SANDBOX, "lib_secure_r%d" % _RUN[0]),
242
+ principal=Principal(tenant="t1", actor="alice",
243
+ role="designer", clearance="secret",
244
+ can_write=True, can_admin=True),
245
+ master_key=os.urandom(32))
246
+ sec.add("s_base", SENT_A, layer="knowledge")
247
+ rs = sec.remember_gated("s_second", SENT_B, layer="knowledge",
248
+ role="command", sensitivity="private")
249
+ drops = rs.get("dropped") or {}
250
+ srows = _log_rows(sec, drops.get("trace_id"))
251
+ raw_log = ""
252
+ p = os.path.join(sec.root, forgetting.LOG_FILE)
253
+ if os.path.exists(p):
254
+ with open(p, encoding="utf-8") as f:
255
+ raw_log = f.read()
256
+ _check("B6 加密级(private)DROP:明文不落留痕(只留摘要/长度)",
257
+ rs.get("verdict") == "DROP" and drops.get("content_kept") is False
258
+ and bool(drops.get("redacted_reason"))
259
+ and PORT_B not in raw_log
260
+ and srows and srows[0].get("content_chars"),
261
+ "kept=%s raw_has_new=%s" % (drops.get("content_kept"),
262
+ PORT_B in raw_log))
263
+ return cg
264
+
265
+
266
+ # ---------------------------------------------------------------- C 组:H3
267
+ def group_c():
268
+ print("\n[C] H3 跨会话合并:双方归属并列落 fm(不按会话切分合并判定)")
269
+ cg = _mk("sess")
270
+ cg.session = "sess_A"
271
+ cg.add("c_a", SENT_A, layer="knowledge")
272
+ fm0 = cg.get("c_a")["frontmatter"]
273
+ cg.session = "sess_B"
274
+ r = cg.remember_gated("c_b", SENT_B, layer="knowledge", role="user")
275
+ fm1 = cg.get("c_a")["frontmatter"]
276
+ _check("C1 跨会话近重复仍并入(合并判定不看会话,是既有设计)",
277
+ r.get("verdict") == "MERGE" and r.get("merged_into") == "c_a",
278
+ repr(r.get("verdict")))
279
+ _check("C2 目标原归属保留 + 新写入方并列记入 fm.merge_sources",
280
+ fm1.get("session") == fm0.get("session") == "sess_A"
281
+ and [x.get("session") for x in (fm1.get("merge_sources") or [])]
282
+ == ["sess_B"],
283
+ json.dumps(fm1.get("merge_sources"), ensure_ascii=False))
284
+ _check("C3 当前会话的新内容不消失(新值落库可检索)",
285
+ PORT_B in ((cg.get("c_a") or {}).get("content") or ""),
286
+ str(_lib_hits(cg, PORT_B)))
287
+ # C4(2026-09-30 补强):索引镜像不对称——reinforce 此前只把 importance /
288
+ # lifecycle state / protected 同步进内存索引条目,**不镜像 merge_sources**,
289
+ # 于是盘面 fm 已记新写入方而同进程 `cg.index[目标]` 读到 None(直到索引
290
+ # 重载),与 converge_into 侧的镜像(writelimit.py:257)不同口径,也与
291
+ # 「写盘后标脏、同进程读到新值」的 C-1/N133 既有纪律不一致。断言打在可
292
+ # 观察结果(索引条目本身)上,不看在哪个函数里写了哪一行。
293
+ idx_c = ((cg.index.get("nodes") or {}).get("c_a")) or {}
294
+ _check("C4 索引条目同步镜像 merge_sources(同进程读 cg.index 即见新值)",
295
+ [x.get("session") for x in (idx_c.get("merge_sources") or [])]
296
+ == ["sess_B"],
297
+ json.dumps(idx_c.get("merge_sources"), ensure_ascii=False))
298
+ return cg
299
+
300
+
301
+ # ---------------------------------------------------------------- D 组:⑤ 哨兵
302
+ def group_d():
303
+ print("\n[D] ⑤ 条件槽空值语义哨兵:不再无条件 condition_clash")
304
+ # D1 逐字相同 + 哨兵 → ACCEPT(修前:DEFER + strength 1.0 + 建工单)
305
+ cg = _mk("sent")
306
+ cg.add("z_a", SENT_A, layer="knowledge")
307
+ vd = consistency.check(cg, SENT_A, layer="knowledge", auto_flywheel=True)
308
+ _check("D1 逐字相同 + 哨兵 → ACCEPT(无冲突 / 无工单)",
309
+ vd.get("verdict") == "ACCEPT"
310
+ and float(vd.get("conflict_strength") or 0) == 0.0
311
+ and not vd.get("unresolved_id")
312
+ and not (vd.get("conflicts") or []),
313
+ "%s strength=%s unresolved=%s" % (vd.get("verdict"),
314
+ vd.get("conflict_strength"),
315
+ vd.get("unresolved_id")))
316
+ # D2 真负条件对照:逐字相同仍 ACCEPT(与 D1 同口径,防「只在哨兵上放行」)
317
+ cg2 = _mk("sent_real")
318
+ cg2.add("z_a", REAL_A, layer="knowledge")
319
+ vd2 = consistency.check(cg2, REAL_A, layer="knowledge", auto_flywheel=True)
320
+ _check("D2 对照:真负条件 + 逐字相同 → ACCEPT(两例同口径)",
321
+ vd2.get("verdict") == "ACCEPT",
322
+ "%s strength=%s" % (vd2.get("verdict"),
323
+ vd2.get("conflict_strength")))
324
+ # D3 真互斥防放宽:新负条件覆盖既有生效条件 → 仍判冲突(REJECT/DEFER)
325
+ vd3 = consistency.check(
326
+ cg2, REAL_A.replace("# 不适用条件:本地开发环境",
327
+ "# 不适用条件:载体/位置:prod 集群"),
328
+ layer="knowledge", auto_flywheel=True)
329
+ _check("D3 防放宽:真互斥(负条件覆盖既有生效条件)仍判冲突",
330
+ vd3.get("verdict") in ("REJECT", "DEFER")
331
+ and float(vd3.get("conflict_strength") or 0) >= consistency.CLASH_HIGH,
332
+ "%s strength=%s" % (vd3.get("verdict"),
333
+ vd3.get("conflict_strength")))
334
+ # D4 判据正反例
335
+ pos_cases = ["无", "无。", "无.", "(无)", "(无)", "None", "", " ",
336
+ "无依赖", "-", "—", None, "不适用"]
337
+ neg_cases = ["无法确定", "无缓存场景", "无持久化介质", "问完全无关主题",
338
+ "本地开发环境"]
339
+ _check("D4a 空值语义正例全部命中(无/无。/(无)/None/空串/无依赖…)",
340
+ all(mdcos._is_null_condition(x) for x in pos_cases),
341
+ str([x for x in pos_cases if not mdcos._is_null_condition(x)]))
342
+ _check("D4b 真实条件反例全部不命中(不做语义猜测)",
343
+ not any(mdcos._is_null_condition(x) for x in neg_cases),
344
+ str([x for x in neg_cases if mdcos._is_null_condition(x)]))
345
+ # D5 fm 列表形态同口径(mdcg.add :1898 会把 CCG 哨兵结构化进 fm)
346
+ fm_list = {"non_applicable_conditions": ["无"]}
347
+ _push_pos, _push_neg = mdcos._declared_conditions(fm_list, SENT_A)
348
+ _check("D5 fm.non_applicable_conditions=[\"无\"] 同口径空值化(既有侧)",
349
+ _push_neg == [], str(_push_neg))
350
+ # D6 CCG 行形态:修点落在取值单点(consistency._new_terms 经 _prims 委托)
351
+ _check("D6a _ccg_field 对条件字段空值化、对非条件字段不动(收口范围)",
352
+ mdcos._ccg_field(SENT_A, "不适用条件") == ""
353
+ and mdcos._ccg_field(SENT_A, "生效条件") != ""
354
+ and mdcos._ccg_field("# 执行:无\n正文", "执行") == "无",
355
+ "_ccg(不适用)=%r _ccg(执行)=%r" % (
356
+ mdcos._ccg_field(SENT_A, "不适用条件"),
357
+ mdcos._ccg_field("# 执行:无\n正文", "执行")))
358
+ from .consistency import _new_terms
359
+ _check("D6b 新节点侧(_new_terms)不再收哨兵(委托同一读点)",
360
+ _new_terms(SENT_A, None, None)[1] == [],
361
+ str(_new_terms(SENT_A, None, None)))
362
+ # D7 入参列表形态同口径(mdcos.check_consistency 是 consistency.check 的唯一
363
+ # 工具面/写链入口;mdcg.py:1785 的直连不在本席文件面内,见守卫头注)
364
+ cg3 = _mk("sent_param")
365
+ cg3.add("z_a", SENT_A, layer="knowledge")
366
+ vd_sent = cg3.check_consistency(SENT_A, layer="knowledge",
367
+ non_applicable_conditions=["无"])
368
+ vd_none = cg3.check_consistency(SENT_A, layer="knowledge",
369
+ non_applicable_conditions=None)
370
+ _check("D7 入参哨兵列表与不传等价(上游收口,判据不落在阈值上)",
371
+ vd_sent.get("verdict") == vd_none.get("verdict") == "ACCEPT"
372
+ and float(vd_sent.get("conflict_strength") or 0) == 0.0,
373
+ "%s/%s" % (vd_sent.get("verdict"), vd_none.get("verdict")))
374
+ return cg3
375
+
376
+
377
+ # ---------------------------------------------------------------- F 组:CONVERGE
378
+ def group_f():
379
+ print("\n[F] CONVERGE(contextual 同构聚合):新值落在 80 字外也不丢")
380
+ cg = _mk("conv")
381
+ long_a = ("# 功能名:批次流水核对报告\n端口清单:" +
382
+ "、".join("p%d" % i for i in range(1, 60)) +
383
+ f";尾值 {PORT_A}\n")
384
+ # F5 前置:目标节点与第二个写入方**用不同会话**写(若同会话,merges_sources
385
+ # 里的值与目标自身 fm.session 无从区分,断言就没有判别力了)。
386
+ cg.session = "sess_conv_a"
387
+ r1 = cg.remember_gated("cv_1", long_a, layer="contextual", role="user")
388
+ cg.session = "sess_conv_b"
389
+ r2 = cg.remember_gated("cv_2", long_a.replace(PORT_A, PORT_B),
390
+ layer="contextual", role="user")
391
+ body = (cg.get("cv_1") or {}).get("content") or ""
392
+ _check("F1 同构聚合仍判 CONVERGE→MERGE(既有语义未变)",
393
+ r1.get("verdict") == "ACCEPT" and r2.get("verdict") == "MERGE"
394
+ and r2.get("merged_into") == "cv_1",
395
+ "%s/%s" % (r1.get("verdict"), r2.get("verdict")))
396
+ _check("F2 新值(在 80 字外)落进目标正文(旧口径在此丢值)",
397
+ PORT_B in body, "hits=%s" % _lib_hits(cg, PORT_B))
398
+ _check("F3 聚合幂等:同文再写零追加",
399
+ (lambda before: (cg.remember_gated("cv_3",
400
+ long_a.replace(PORT_A, PORT_B),
401
+ layer="contextual",
402
+ role="user")
403
+ and len((cg.get("cv_1") or {}).get("content")
404
+ or "") == before))(len(body)),
405
+ "len=%d" % len(body))
406
+ _check("F4 单点:converge_into 与 reinforce 共用同一聚合行实现",
407
+ "forgetting.aggregate_line" in _src_of("writelimit.py")
408
+ and "forgetting.aggregate_line" in _src_of("forgetting.py"),
409
+ "两处实现点必须同源")
410
+ # F5(2026-09-30 补强):CONVERGE 落点的 session 接线。mdcos.py:3468 调用
411
+ # converge_into 时传 `session=_writer_session(self, kw)`;复核员实测**抽掉
412
+ # 该实参后本守卫全绿**——即该接线此前无覆盖。断言打在可观察结果(写盘 fm
413
+ # 的归属字段)上:目标自身 fm.session 保持第一个写入方不动,第二个写入方
414
+ # 的 session 并列落进 fm.merge_sources(单点 note_source 的产出)。
415
+ fm_cv = (cg.get("cv_1") or {}).get("frontmatter") or {}
416
+ srcs = [x.get("session") for x in (fm_cv.get("merge_sources") or [])]
417
+ _check("F5 CONVERGE 落点:新写入方 session 并列落目标 fm.merge_sources"
418
+ "(目标自身归属不动)",
419
+ fm_cv.get("session") == "sess_conv_a" and srcs
420
+ and set(srcs) == {"sess_conv_b"},
421
+ "fm.session=%r merge_sources=%s" % (
422
+ fm_cv.get("session"),
423
+ json.dumps(fm_cv.get("merge_sources"), ensure_ascii=False)))
424
+ return cg
425
+
426
+
427
+ def _src_of(name):
428
+ p = os.path.join(os.path.dirname(os.path.abspath(__file__)), name)
429
+ with open(p, encoding="utf-8", errors="replace") as f:
430
+ return f.read().replace("\r\n", "\n")
431
+
432
+
433
+ # ---------------------------------------------------------------- 变异(自证)
434
+ def _pre_fix_aggregate_line(body, content, stamp=None):
435
+ """改动前语义:新正文不进正文(返回 None = 只改 fm)。"""
436
+ return None, None, None
437
+
438
+
439
+ def _pre_fix_record_drop(cg, content, node_id, **kw):
440
+ """改动前语义:只记一行元数据日志,新正文零去向。"""
441
+ forgetting.log(cg, {"t": time.time(), "node_id": node_id,
442
+ "layer": kw.get("layer"), "verdict": "DROP",
443
+ "reason": "确定性内部产生且冗余(低熵噪音,不编码)",
444
+ "actor": kw.get("actor")})
445
+ return {}
446
+
447
+
448
+ def _pre_fix_null_condition(value):
449
+ """改动前语义:无空值语义判据(哨兵原样当真实条件)。"""
450
+ return False
451
+
452
+
453
+ def _pre_fix_conv_session():
454
+ """改动前语义:CONVERGE 落点不往下传新写入方 session——等价于抽掉
455
+ mdcos.py:3468 的 `session=_writer_session(self, kw)` 实参(复核员实测该
456
+ 实参可静默抽掉)。补丁只截断该实参,落库动作原样保留。"""
457
+ _orig = writelimit.converge_into
458
+
459
+ def patched(cg, target, content, override=False, actor=None,
460
+ session=None, **kw):
461
+ return _orig(cg, target, content, override=override, actor=actor,
462
+ session=None, **kw)
463
+
464
+ writelimit.converge_into = patched
465
+
466
+
467
+ def _pre_fix_index_mirror():
468
+ """改动前语义:reinforce 的内存索引条目不同步 merge_sources(盘面 fm 已记
469
+ 新写入方、同进程 `cg.index[目标]` 读到 None,直到索引重载)——等价于抽掉
470
+ forgetting.py 索引更新段里的镜像两行。"""
471
+ _orig = forgetting.reinforce
472
+
473
+ def patched(cg, node_id, *a, **kw):
474
+ out = _orig(cg, node_id, *a, **kw)
475
+ e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
476
+ if isinstance(e, dict):
477
+ e.pop("merge_sources", None)
478
+ return out
479
+
480
+ forgetting.reinforce = patched
481
+
482
+
483
+ _MUTATIONS = {
484
+ # 名称: (打补丁, 必须转红的断言名)
485
+ "merge": (lambda: setattr(forgetting, "aggregate_line",
486
+ _pre_fix_aggregate_line),
487
+ ["A2", "A3", "A4", "C3", "F2"]),
488
+ "drop": (lambda: setattr(forgetting, "record_drop",
489
+ _pre_fix_record_drop),
490
+ ["B3", "B4"]),
491
+ "sentinel": (lambda: setattr(mdcos, "_is_null_condition",
492
+ _pre_fix_null_condition),
493
+ ["D1", "D4a", "D5", "D6a", "D6b", "D7"]),
494
+ # 2026-09-30 补强两处复核员实测的覆盖缺口
495
+ "conv_session": (_pre_fix_conv_session, ["F5"]),
496
+ "index": (_pre_fix_index_mirror, ["C4"]),
497
+ }
498
+ for _k, (_fn, _want) in _MUTATIONS.items():
499
+ _MUTATIONS[_k] = (_fn, _want)
500
+
501
+
502
+ def _run_all():
503
+ global _OK, _BAD
504
+ _RUN[0] += 1
505
+ _OK, _BAD = 0, []
506
+ group_a()
507
+ group_b()
508
+ group_c()
509
+ group_d()
510
+ group_f()
511
+ return _OK, list(_BAD)
512
+
513
+
514
+ def _snapshot_fix():
515
+ return {"aggregate_line": forgetting.aggregate_line,
516
+ "record_drop": forgetting.record_drop,
517
+ "_is_null_condition": mdcos._is_null_condition,
518
+ "converge_into": writelimit.converge_into,
519
+ "reinforce": forgetting.reinforce}
520
+
521
+
522
+ def _restore_fix(snap):
523
+ forgetting.aggregate_line = snap["aggregate_line"]
524
+ forgetting.record_drop = snap["record_drop"]
525
+ mdcos._is_null_condition = snap["_is_null_condition"]
526
+ writelimit.converge_into = snap["converge_into"]
527
+ forgetting.reinforce = snap["reinforce"]
528
+
529
+
530
+ def main(argv):
531
+ which = None
532
+ for a in argv:
533
+ if a.startswith("--drop-fix"):
534
+ which = a.split("=", 1)[1] if "=" in a else "all"
535
+ if which is None:
536
+ ok, bad = _run_all()
537
+ print("\n==== B3/⑤ 守卫(MERGE 去向 + 哨兵):%d 通过%s ====" % (
538
+ ok, ",%d 失败:%s" % (len(bad), "; ".join(bad)) if bad else ""))
539
+ return 1 if bad else 0
540
+ names = list(_MUTATIONS) if which == "all" else [which]
541
+ rc = 0
542
+ print("\n=== 定点变异自证(抽掉修复 → 守卫必红 → 恢复 → 转绿)===")
543
+ for name in names:
544
+ if name not in _MUTATIONS:
545
+ print(" 未知变异 %r(可选:%s)" % (name, sorted(_MUTATIONS)))
546
+ return 2
547
+ patch, want = _MUTATIONS[name]
548
+ snap = _snapshot_fix()
549
+ patch()
550
+ ok, bad = _run_all()
551
+ hit = [n for n in want if any(b.split()[0] == n or
552
+ b.startswith(n) for b in bad)]
553
+ missed = [n for n in want if n not in hit]
554
+ print(" [%s] 红项 %d 个:%s" % (name, len(bad), "; ".join(bad) or "(无)"))
555
+ if missed or not bad:
556
+ print(" FAIL 变异 %s 未打红预期断言:%s" % (name, missed or "全绿"))
557
+ rc = 1
558
+ else:
559
+ print(" ok 变异 %s 打红预期断言 %d 项(判别力自证)"
560
+ % (name, len(hit)))
561
+ _restore_fix(snap)
562
+ ok2, bad2 = _run_all()
563
+ if bad2:
564
+ print(" FAIL 变异 %s 恢复后未转绿,残余红项:%s" % (name, bad2))
565
+ rc = 1
566
+ else:
567
+ print(" ok 变异 %s 恢复后转绿(%d 通过,无残余)" % (name, ok2))
568
+ return rc
569
+
570
+
571
+ if __name__ == "__main__":
572
+ try:
573
+ _rc = main(sys.argv[1:])
574
+ finally:
575
+ shutil.rmtree(_SANDBOX, ignore_errors=True)
576
+ sys.exit(_rc)