@furongjun1999/dsh-memory 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +588 -552
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/discipline/harnesses.yaml +31 -0
  5. package/docs/discipline/templates/full.md.tmpl +1 -1
  6. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  7. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
  8. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
  9. package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
  10. 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
  11. package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
  18. 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
  19. 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
  20. 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
  21. 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
  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_v18.md +183 -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_v19.md +207 -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_v20.md +283 -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_v21.md +224 -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_v22.md +223 -0
  27. 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
  28. 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
  29. 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
  30. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +20 -0
  31. 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
  32. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
  33. 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
  34. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
  35. 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
  36. 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
  37. 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
  38. 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
  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/dsh/cordis-patch-profile-web.example.yml +35 -0
  42. package/lib/bridge.js +24 -2
  43. package/lib/cli.d.ts +3 -0
  44. package/lib/cli.js +66 -0
  45. package/lib/hooks.d.ts +31 -2
  46. package/lib/hooks.js +219 -16
  47. package/lib/init.d.ts +88 -0
  48. package/lib/init.js +302 -0
  49. package/lib/lib/prompt_safety.d.ts +52 -1
  50. package/lib/lib/prompt_safety.js +76 -1
  51. package/md_cg/audit.py +652 -379
  52. package/md_cg/bench6_arms.py +1 -1
  53. package/md_cg/bench_p0.py +1 -1
  54. package/md_cg/branches.py +2 -2
  55. package/md_cg/ccgc.py +1071 -1006
  56. package/md_cg/chain.py +1 -1
  57. package/md_cg/consistency.py +97 -9
  58. package/md_cg/consolidate.py +34 -9
  59. package/md_cg/crypto.py +59 -26
  60. package/md_cg/docindex.py +16 -1
  61. package/md_cg/evidence.py +585 -582
  62. package/md_cg/evolution.py +17 -1
  63. package/md_cg/export.py +1 -1
  64. package/md_cg/forgetting.py +309 -13
  65. package/md_cg/fsutil.py +454 -8
  66. package/md_cg/identity.py +3 -3
  67. package/md_cg/insight.py +24 -3
  68. package/md_cg/linkref.py +1 -1
  69. package/md_cg/logref.py +327 -0
  70. package/md_cg/mcp_server.py +4152 -3818
  71. package/md_cg/mdcg.py +4208 -3462
  72. package/md_cg/mdcos.py +4491 -4137
  73. package/md_cg/mreview/pipeline.py +15 -1
  74. package/md_cg/nodefile.py +639 -575
  75. package/md_cg/protect.py +158 -11
  76. package/md_cg/protocol.py +41 -2
  77. package/md_cg/provenance.py +22 -4
  78. package/md_cg/reach.py +4 -4
  79. package/md_cg/readcache.py +76 -15
  80. package/md_cg/reconcile.py +1 -1
  81. package/md_cg/refindex.py +246 -37
  82. package/md_cg/refine.py +1 -1
  83. package/md_cg/review_cli.py +49 -4
  84. package/md_cg/routing.py +32 -5
  85. package/md_cg/run_tests.py +17 -0
  86. package/md_cg/scrub.py +30 -3
  87. package/md_cg/security.py +47 -1
  88. package/md_cg/self_state.py +5 -5
  89. package/md_cg/sources.py +23 -6
  90. package/md_cg/srcindex.py +352 -0
  91. package/md_cg/stg.py +47 -3
  92. package/md_cg/subgraph.py +2 -2
  93. package/md_cg/sustain.py +1299 -1168
  94. package/md_cg/tasks.py +469 -470
  95. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  96. package/md_cg/test_b1b2_write_face.py +417 -0
  97. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  98. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  99. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  100. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  101. package/md_cg/test_c3_transient_read_negative.py +793 -0
  102. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  103. package/md_cg/test_ccg_form_parity.py +188 -0
  104. package/md_cg/test_ccgc.py +28 -3
  105. package/md_cg/test_govern_directread.py +17 -4
  106. package/md_cg/test_h2_session_view_norm.py +233 -0
  107. package/md_cg/test_h4_sustain_snapshot.py +1377 -0
  108. package/md_cg/test_hive_ingest.py +285 -285
  109. package/md_cg/test_index_crossprocess_reload.py +301 -0
  110. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  111. package/md_cg/test_issue43_default_policy.py +865 -0
  112. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  113. package/md_cg/test_linkref.py +10 -3
  114. package/md_cg/test_lock.py +2 -2
  115. package/md_cg/test_logref.py +1109 -0
  116. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  117. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  118. package/md_cg/test_mr_m2.py +15 -3
  119. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  120. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  121. package/md_cg/test_n176_link_trust.py +221 -0
  122. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  123. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  124. package/md_cg/test_n195_writepath_reload.py +283 -0
  125. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  126. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  127. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  128. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  129. package/md_cg/test_n201_proposal_visibility.py +382 -0
  130. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  131. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  132. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  133. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  134. package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
  135. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  136. package/md_cg/test_n225_nonobject_load.py +1110 -0
  137. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  138. package/md_cg/test_neg_condition_hits.py +333 -0
  139. package/md_cg/test_neg_tail_honesty.py +663 -0
  140. package/md_cg/test_none_id_write_guard.py +166 -0
  141. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  142. package/md_cg/test_p1.py +5 -5
  143. package/md_cg/test_p11_consistency.py +161 -12
  144. package/md_cg/test_p26_refindex.py +2 -2
  145. package/md_cg/test_p27_docindex.py +777 -774
  146. package/md_cg/test_p28_refcheck.py +740 -13
  147. package/md_cg/test_p29_session_ingest_export.py +2 -2
  148. package/md_cg/test_p2_mcp.py +12 -1
  149. package/md_cg/test_p30_maintain.py +33 -2
  150. package/md_cg/test_p31_insight.py +1 -0
  151. package/md_cg/test_p9c_dedup_hints.py +276 -0
  152. package/md_cg/test_policy_required_ccg.py +426 -0
  153. package/md_cg/test_protocol.py +22 -0
  154. package/md_cg/test_rank_parity_score_mode.py +516 -0
  155. package/md_cg/test_read_face_input_gates.py +363 -0
  156. package/md_cg/test_read_face_semantics.py +489 -0
  157. package/md_cg/test_recall_face_guards.py +812 -0
  158. package/md_cg/test_rejected_credential_forms.py +188 -0
  159. package/md_cg/test_rejected_redact.py +146 -0
  160. package/md_cg/test_retr_s1b.py +2 -2
  161. package/md_cg/test_retr_s5.py +20 -7
  162. package/md_cg/test_review_cli_attribution.py +177 -0
  163. package/md_cg/test_review_cli_visibility.py +235 -0
  164. package/md_cg/test_review_conformance.py +21 -3
  165. package/md_cg/test_security_audit_v21.py +2 -2
  166. package/md_cg/test_server_version.py +67 -0
  167. package/md_cg/test_srcindex.py +171 -0
  168. package/md_cg/test_tenant_env_override_warn.py +63 -50
  169. package/md_cg/test_token_lowercase_form.py +315 -0
  170. package/md_cg/test_v21r1_package_version.py +310 -0
  171. package/md_cg/test_writepipe.py +10 -4
  172. package/md_cg/tokens.py +225 -95
  173. package/md_cg/tool_face.py +4 -4
  174. package/md_cg/trust.py +49 -5
  175. package/md_cg/units.py +204 -6
  176. package/md_cg/weights.py +4 -4
  177. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  178. package/md_cg/writelimit.py +42 -8
  179. package/md_cg/writepipe.py +651 -554
  180. package/package.json +8 -2
  181. package/skills/plugin.json +1 -1
  182. package/src/bridge.ts +25 -2
  183. package/src/cli.ts +65 -0
  184. package/src/hooks.ts +229 -16
  185. package/src/init.ts +361 -0
  186. package/src/lib/prompt_safety.ts +88 -1
  187. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,166 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_none_id_write_guard —— None.md 脏节点写入面守卫(DSH 在役库实测缺陷 P3)
3
+
4
+ 现场(DSH 端在役库实测):``knowledge/orphan/None.md`` 出现在检索与 route 候选中
5
+ 参与打分。取证链(2026-09-26,临时库实弹复现):
6
+
7
+ 1. 批次 24(2a57c2a2,2026-09-24)之前 ``add()`` 对 node_id **零校验**,
8
+ ``path = os.path.join(d, f"{node_id}.md")`` 直拼——``add(None, ...)`` 直落
9
+ ``None.md``(旧版直接源头,git 取证 ``git show 2a57c2a2^:md_cg/mdcg.py``)。
10
+ 2. 现行代码 ``add(None)``/``add("")`` 已被 P0-1 白名单拒绝(mdcg.py _NODE_ID_RE),
11
+ **但字符串 ``"None"``(Python ``str(None)`` 的污染形态)形状合法照常放行**:
12
+ 实弹复现 ``add("None", ...)`` → 生成 ``knowledge/orphan/None.md``。
13
+ ``"null"``(TS/JS ``String(null)`` 形态)同族放行。这是现行写入面残留洞。
14
+ 3. 盘上存量 ``None.md`` 经 ``_scan_nodes`` 全库扫描收录为 ``id="None"``,
15
+ ``search``/route 候选参与打分(复现实测命中)——与 DSH 病理现象完全吻合。
16
+
17
+ 修复(本守卫的红绿两态锚点):``md_cg/mdcg.py`` ``add()`` 写入边界把空语义值的
18
+ 字符串化污染形态(整串精确等值 ``"None"`` / ``"null"``,**大小写敏感、不扩子串**)
19
+ 结构化拒绝(ValueError,fail-closed)——add 是全部写入链(writepipe 执行器 /
20
+ review accept / identity / consolidate / branches)落盘的唯一真源边界,堵一处即
21
+ 全链路堵死。``_move_layer`` 等直写点只对已在索引中的既有节点操作,不新造 id。
22
+
23
+ 清理面(不在本守卫内,运维建议):存量 None.md 属历史脏数据——删除
24
+ ``<root>/knowledge/orphan/None.md`` 文件后 ``rebuild_index()`` 即从索引与检索/
25
+ route 候选中消失([6] 的收录取证即清理有效性依据);勿用 ``add("None",...)``
26
+ 覆写清理(修复后该形态被拒)。
27
+
28
+ 隔离纪律:全程 ``tempfile.mkdtemp`` 临时库,MDCG_ROOT/MDCG_TOKEN_FILE 不参与
29
+ (库实例以显式 root 构造),不触真实令牌库与真实在役数据目录。
30
+
31
+ 自检 floor:断言总数低于 FLOOR 视为失败——防「探针面失效 → 假绿」。
32
+
33
+ 运行:python -X utf8 -m md_cg.test_none_id_write_guard
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import os
38
+ import sys
39
+ import tempfile
40
+
41
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
42
+
43
+ from md_cg.mdcos import MdCGOS # noqa: E402
44
+
45
+ FLOOR = 12 # 断言总数下限(防假绿)
46
+ passed = failed = 0
47
+
48
+
49
+ def check(name, cond, detail=""):
50
+ global passed, failed
51
+ if cond:
52
+ passed += 1
53
+ print(" [PASS] " + name)
54
+ else:
55
+ failed += 1
56
+ print(" [FAIL] " + name + " " + str(detail)[:200])
57
+
58
+
59
+ def _none_md_hits(root):
60
+ """盘面上所有 None.md(大小写不敏感——Windows 盘面本就不区分)。"""
61
+ hits = []
62
+ for dirpath, _dirs, files in os.walk(root):
63
+ for f in files:
64
+ if f.lower() == "none.md":
65
+ hits.append(os.path.relpath(os.path.join(dirpath, f), root)
66
+ .replace("\\", "/"))
67
+ return sorted(hits)
68
+
69
+
70
+ def main():
71
+ root = tempfile.mkdtemp(prefix="mdcg_none_guard_")
72
+ try:
73
+ return _run(root)
74
+ finally:
75
+ import shutil
76
+ shutil.rmtree(root, ignore_errors=True) # 测毕清理(临时库纪律)
77
+
78
+
79
+ def _run(root):
80
+ cg = MdCGOS(root)
81
+
82
+ print("[1] 红形态:字符串 \"None\"(str(None) 污染形态)在写入边界拒绝")
83
+ rejected = False
84
+ try:
85
+ nid = cg.add("None", "污染形态内容 无条件")
86
+ check("add(\"None\") 被拒绝", False, "返回了 " + repr(nid))
87
+ except ValueError as e:
88
+ rejected = True
89
+ check("add(\"None\") 被拒绝", True)
90
+ check("拒绝是结构化 ValueError 且消息点名形态",
91
+ "None" in str(e) and "node_id" in str(e), str(e)[:120])
92
+ check("盘面无 None.md 生成", _none_md_hits(root) == [], _none_md_hits(root))
93
+ check("索引无 id='None' 条目", "None" not in cg.index["nodes"])
94
+
95
+ print("[2] 红形态:字符串 \"null\"(String(null) 跨语言同族形态)")
96
+ try:
97
+ nid = cg.add("null", "跨语言污染形态内容 无条件")
98
+ check("add(\"null\") 被拒绝", False, "返回了 " + repr(nid))
99
+ except ValueError:
100
+ check("add(\"null\") 被拒绝", True)
101
+ check("盘面仍无 None.md/null.md", _none_md_hits(root) == [],
102
+ _none_md_hits(root))
103
+
104
+ print("[3] 防误杀:含 None/null 子串的合法 id 照常写入(只拒整串精确等值)")
105
+ ok1 = cg.add("NoneBot_1", "含 None 子串的合法 id 无条件")
106
+ ok2 = cg.add("nullify_test", "含 null 子串的合法 id 无条件")
107
+ check("add(\"NoneBot_1\") 正常", ok1 == "NoneBot_1")
108
+ check("add(\"nullify_test\") 正常", ok2 == "nullify_test")
109
+
110
+ print("[4] 既有语义不回退:None 本体 / falsy 仍结构化拒绝(P0-1)")
111
+ # 注(2026-09-28 更新):这条相邻缺陷(非字符串 node_id 以原值入索引键)
112
+ # 已在 legacy P3 轮次修复——写面 add 对非 str node_id fail-closed 拒写 +
113
+ # 派生面 _scan_nodes/日志重放键归一为 str,守卫见
114
+ # md_cg/test_legacy_p3_node_id_type.py;此处 falsy 三形态的拒绝语义不变。
115
+ for bad in (None, "", 0):
116
+ try:
117
+ cg.add(bad, "非法形态内容")
118
+ check("add(%r) 仍被拒绝" % (bad,), False, "竟返回成功")
119
+ except ValueError:
120
+ check("add(%r) 仍被拒绝(ValueError)" % (bad,), True)
121
+
122
+ print("[5] writepipe 链:node_id=\"None\" 不落盘(DEFER/BLINDSPOT 短路入队"
123
+ "或落盘前被 add 边界拒绝,两态皆 fail-closed)")
124
+ from md_cg import writepipe
125
+ pipe = writepipe.WritePipeline()
126
+ writepipe.install_default_gates(pipe)
127
+ committed = None
128
+ try:
129
+ out = pipe.execute(cg, {"content": "writepipe 污染形态", "node_id": "None",
130
+ "content_kind": "work_done",
131
+ "生效条件": "writepipe 守卫探针"})
132
+ committed = out.get("committed")
133
+ except ValueError:
134
+ committed = False # 落盘前被 add 边界拒绝(修复态)
135
+ check("writepipe 对 node_id='None' 不提交落盘", committed is not True,
136
+ repr(committed))
137
+ check("盘面无 None.md(writepipe 后复查)", _none_md_hits(root) == [],
138
+ _none_md_hits(root))
139
+
140
+ print("[6] 存量脏数据收录取证:盘上 None.md 被 _scan_nodes 收进索引(DSH 病理机理;"
141
+ "亦是清理建议的依据——删文件 + rebuild 即消失)")
142
+ d = os.path.join(root, "knowledge", "orphan")
143
+ os.makedirs(d, exist_ok=True)
144
+ with open(os.path.join(d, "None.md"), "w", encoding="utf-8") as f:
145
+ f.write("---\nid: None\nlayer: knowledge\nimportance: 0.5\n"
146
+ "confidence: 0.6\ncreated_at: 1\ntags: []\n---\n\n历史脏数据内容\n")
147
+ cg2 = MdCGOS(root) # 重开:走 _scan_nodes 全库扫描
148
+ cg2.rebuild_index()
149
+ check("历史 None.md 被扫描收录(机理复证)", "None" in cg2.index["nodes"])
150
+ res, _m = cg2.search_rrf("历史脏数据内容", k=10, judge=False)
151
+ check("检索候选命中 id='None'(病理复证)",
152
+ any(r[0]["id"] == "None" for r in res), [r[0]["id"] for r in res])
153
+ os.remove(os.path.join(d, "None.md")) # 清理后即消失(清理建议有效性)
154
+ cg3 = MdCGOS(root)
155
+ cg3.rebuild_index()
156
+ check("删文件 + rebuild 后 id='None' 从索引消失(清理有效)",
157
+ "None" not in cg3.index["nodes"])
158
+
159
+ check(f"断言总数不低于 FLOOR({passed} >= {FLOOR})", passed >= FLOOR)
160
+
161
+ print(f"\nnone_id_write_guard: {passed} 通过 / {failed} 失败")
162
+ return 0 if not failed else 1
163
+
164
+
165
+ if __name__ == "__main__":
166
+ sys.exit(main())
@@ -0,0 +1,451 @@
1
+ # -*- coding: utf-8 -*-
2
+ """opt-batch1 守卫 · md_cg 侧(C-1 三处写盘标脏 + C-7 升格门槛接真源)。
3
+
4
+ 来源:外部双路调研(Codex 实测 + ZCode 工作流)确认、并经编排侧抽核实的
5
+ opt-batch1 批次。本文件只覆盖 md_cg 侧两条;rust 侧(R-1/R-2/R-3/R-6)的
6
+ 守卫是 crate 内单测(`rust/src/*.rs` 的 `#[cfg(test)]` 与 `rust/tests/`)。
7
+
8
+ 覆盖与判据:
9
+
10
+ C-1(三处「写后读」永久陈旧)——`forgetting.reinforce`(reinforce 落库)、
11
+ `insight.verify`(洞见裁决)、`scrub._apply_offset`(置信度校准)三处
12
+ 调 `cg._write_node` 后只改内存 index entry(scrub 连条目都不改),
13
+ **不标 `_dirty`**。读缓存默认开(`MDCG_READ_CACHE=1`,见 `readcache.enabled`),
14
+ 其新鲜度按 `_DirtyDict.path_gen` 判;不标脏 ⇒ 同进程「写后读」永久拿到
15
+ 旧 fm。对照先例:`md_cg/mdcg.py` 的 `update_tags` / verify 直写分支
16
+ `self._dirty[node_id] = e`。
17
+ 每处三条断言:①写盘确实发生(正对照,防夹具空转)②`nid in cg._dirty`
18
+ ③同实例 `cg._read(e)` 与 `readcache.direct_read`(穿透缓存读盘上真值)
19
+ 逐位一致——②③即「标脏」的可观测判据。
20
+
21
+ C-7(升格门槛自带第二份要素常量)——`consolidate.py` 原先自带
22
+ `CCG_REQUIRED = ("生效条件","子功能","执行","不适用条件")`(4 元素),
23
+ 与真源 `nodefile.CCG_REQUIRED`(6 元素,多「功能名」「验证方式」)不一致
24
+ ⇒ 缺「验证方式」的弱证据节点判 complete,可被 `promote_memories` 升格进
25
+ knowledge 层(`predict` 亦按同一常量判 node_ready)。
26
+ 断言:转发真源(`is` 同一对象)/ 缺「验证方式」者被拒 / 六要素齐全者不被误伤。
27
+
28
+ 运行:
29
+ python -X utf8 -m md_cg.test_opt_batch1_md_cg # 正向
30
+ python -X utf8 -m md_cg.test_opt_batch1_md_cg --mutate # 定点变异自证
31
+ python -X utf8 -m md_cg.test_opt_batch1_md_cg --mutate --list # 只列变异表
32
+
33
+ 退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
34
+ 2 = **ANCHOR-MISS**(变异锚点在当前源码里找不到,说明实现改了却没同步本表——
35
+ 本表就是判据,锚点漂移必须硬失败,不得静默跳过)。
36
+
37
+ **基线纪律**:本守卫**不以 git HEAD(或任何提交快照)为基线源**——所有
38
+ 「修复前」形态都由**在当前工作区源码上做定点文本变异**得到(`_MUTATIONS`),
39
+ 基线随代码一起走、不随提交漂移(本仓已有两次「基线绑提交即失效」的教训)。
40
+ 变异表每条都记 `expect_red`(预期转红条数),实际红数不等即判失败——这条
41
+ 同时钉住「有判别力」(红数 > 0)与「无过度杀伤」(红数恰好命中预期)。
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import os
46
+ import shutil
47
+ import sys
48
+ import tempfile
49
+ import time
50
+ import types
51
+
52
+ # ── 隔离前置:必须在 import md_cg 子模块**之前**(tokens/theory 等在模块级把
53
+ # aux_root() 冻成常量)——否则会指到真实 ~/.mdcg 并在那里建密钥/令牌库。
54
+ # 与 md_cg/test_n197_n208_write_face_gates.py 同款前置(同一纪律)。 ──
55
+ _TMP = tempfile.mkdtemp(prefix="optb1_")
56
+ os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "auxroot")
57
+ os.environ["MDCG_MASTER_KEY"] = "ab" * 32 # 哑主密钥(绝不触真实密钥面)
58
+ os.environ["MDCG_TOKEN_FILE"] = os.path.join(_TMP, "tokens.json")
59
+ os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cgroot")
60
+ for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
61
+ "DSH_SESSION_ID", "MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
62
+ "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_STATE_ROOT",
63
+ "MDCG_DATA_ROOT", "MDCG_TENANT_REGISTRY", "MDCG_VERIFIER_MODULES"):
64
+ os.environ.pop(_k, None)
65
+
66
+ from . import consolidate, forgetting, insight, nodefile, readcache, scrub, tokens # noqa: E402
67
+ from .mdcos import MdCGSecure # noqa: E402
68
+ from .security import Principal # noqa: E402
69
+
70
+ try:
71
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
72
+ except AttributeError:
73
+ pass
74
+
75
+ _HERE = os.path.dirname(os.path.abspath(__file__))
76
+ _REPO = os.path.dirname(_HERE)
77
+ _SELF = os.path.abspath(__file__)
78
+
79
+ # 被测模块的**可替换引用**:变异模式会把其中一项换成 exec 出来的变异副本,
80
+ # 断言组一律经本表取模块(不直接闭包捕获原模块),否则变异对断言不可见。
81
+ MOD = {
82
+ "forgetting": forgetting,
83
+ "insight": insight,
84
+ "scrub": scrub,
85
+ "consolidate": consolidate,
86
+ }
87
+
88
+ _PASS: list = []
89
+ _FAIL: list = []
90
+ _OPEN: list = []
91
+
92
+
93
+ def ok(cond, msg, extra=""):
94
+ (_PASS if cond else _FAIL).append(msg)
95
+ print((" PASS " if cond else " FAIL ") + msg
96
+ + ((" ← " + str(extra)) if (extra and not cond) else ""))
97
+ return bool(cond)
98
+
99
+
100
+ def _cleanup(path) -> bool:
101
+ import gc
102
+ for _ in range(6):
103
+ shutil.rmtree(path, ignore_errors=True)
104
+ if not os.path.exists(path):
105
+ return True
106
+ gc.collect()
107
+ time.sleep(0.2)
108
+ return not os.path.exists(path)
109
+
110
+
111
+ def _princ(role="designer", clearance="secret", can_admin=True):
112
+ spec = tokens.role_spec(role)
113
+ return Principal(tenant="default", actor="optb1-" + role,
114
+ clearance=clearance, can_write=bool(spec["can_write"]),
115
+ can_admin=can_admin, role=role,
116
+ layers_allow=spec["layers_allow"],
117
+ ops_allow=spec["ops_allow"], auth_mode="test")
118
+
119
+
120
+ def _secured(tag: str):
121
+ """建一个临时库 + designer 身份;返回 (cg, tmpdir)。"""
122
+ d = tempfile.mkdtemp(prefix="optb1_%s_" % tag)
123
+ cg = MdCGSecure(os.path.join(d, "root"), principal=_princ())
124
+ _OPEN.append(cg)
125
+ return cg, d
126
+
127
+
128
+ def _close_all():
129
+ for cg in _OPEN:
130
+ try:
131
+ cg.close()
132
+ except Exception: # noqa: BLE001 —— 清理面不得再抛
133
+ pass
134
+ _OPEN.clear()
135
+
136
+
137
+ def _prime_cache(cg, nid):
138
+ """把读缓存填成「写前旧值」(模拟真实的「先读过、再写」时序)。"""
139
+ e = cg.index["nodes"][nid]
140
+ cg._read(e) # 装缓存(写前快照)
141
+ return e
142
+
143
+
144
+ def _cache_matches_disk(cg, e) -> bool:
145
+ """同实例 `_read`(走读缓存)是否与穿透缓存的盘上真值一致。"""
146
+ cached = cg._read(e)
147
+ direct = readcache.direct_read(cg, e)
148
+ return (cached[0] or {}) == (direct[0] or {}) and \
149
+ (cached[1] or "") == (direct[1] or "")
150
+
151
+
152
+ # ================================================================ G0 前置自证
153
+ def g0():
154
+ print("== G0 隔离与前置自证 ==")
155
+ ok(readcache.enabled(), "G0a 读缓存默认开(C-1 的成立前提:未设 MDCG_READ_CACHE 时 enabled() 为真)")
156
+ ok(os.path.abspath(tokens.token_file()).lower().startswith(_TMP.lower()),
157
+ "G0b 令牌库落在守卫临时目录(绝不触真实凭据)",
158
+ "tok=%s" % tokens.token_file())
159
+ # 基线源自证(反「基线绑提交」):变异加载器直接 open 当前工作区源文件,
160
+ # 读到的就是它此刻在盘上的内容——不含任何提交快照读取面。
161
+ _rel = os.path.join("md_cg", "consolidate.py")
162
+ _live = open(os.path.join(_REPO, _rel), encoding="utf-8").read()
163
+ _mut, _src = _load_mutated(_rel, "CCG_REQUIRED = nodefile.CCG_REQUIRED",
164
+ "CCG_REQUIRED = ()")
165
+ ok(_mut is not None and _src == _live,
166
+ "G0c 变异基线源 = 当前工作区源码(非 git HEAD/提交快照)",
167
+ "lead=%s" % (_mut is not None))
168
+
169
+
170
+ # ================================================================ G1 C-1a reinforce
171
+ def g1():
172
+ print("== G1 C-1a forgetting.reinforce:写盘后标脏(写后读可见)==")
173
+ cg, d = _secured("reinforce")
174
+ try:
175
+ cg.add("n_re", "香蕉 optb1unique 强化正文", layer="knowledge",
176
+ importance=0.5)
177
+ cg.flush()
178
+ readcache.install(cg)
179
+ e = _prime_cache(cg, "n_re")
180
+ before = cg._read(e)[0].get("importance")
181
+ ok(before == 0.5, "G1a 前置:写前缓存快照 importance=0.5", before)
182
+ r = MOD["forgetting"].reinforce(cg, "n_re", delta=0.3)
183
+ ok(r is not None and r.get("importance") == 0.8,
184
+ "G1b 正对照:reinforce 写盘成功(返回值自认 0.8)", r)
185
+ ok("n_re" in cg._dirty,
186
+ "G1c 标脏:写盘后 cg._dirty 含该节点(不标脏则 path_gen 不推进)",
187
+ "dirty=%s" % list(cg._dirty))
188
+ ok(_cache_matches_disk(cg, e),
189
+ "G1d 同实例写后读与盘上真值逐位一致(读缓存已失效)",
190
+ "cached=%s direct=%s" % (cg._read(e)[0].get("importance"),
191
+ readcache.direct_read(cg, e)[0].get("importance")))
192
+ finally:
193
+ _close_all()
194
+ _cleanup(d)
195
+
196
+
197
+ # ================================================================ G2 C-1b insight.verify
198
+ def g2():
199
+ print("== G2 C-1b insight.verify:裁决写盘后标脏 ==")
200
+ cg, d = _secured("insight")
201
+ try:
202
+ cond = {"retrievability": 0.8, "outside_observer": True,
203
+ "cross_domain": ["存储", "网络"], "premise_questioned": True,
204
+ "pressure": "low", "continuity_turns": 3, "externalized": True,
205
+ "tone": "curious"}
206
+ ev = cg.insight(action="record",
207
+ statement="optb1 探针事件:写后读判据(zzins)",
208
+ conditions=cond)
209
+ nid = ev["node_id"]
210
+ cg.flush()
211
+ readcache.install(cg)
212
+ e = _prime_cache(cg, nid)
213
+ ok(cg._read(e)[0].get("insight_state") == "pending",
214
+ "G2a 前置:写前缓存快照 insight_state=pending",
215
+ cg._read(e)[0].get("insight_state"))
216
+ out = MOD["insight"].verify(
217
+ cg, node_id=nid, actor="designer", verdict="verified",
218
+ note="optb1 守卫",
219
+ evidence=[{"type": "v3", "ref": "optb1", "note": "n"}])
220
+ ok(bool(out.get("ok")) and out.get("state") == "verified",
221
+ "G2b 正对照:verify 裁决写盘成功(state=verified)", out)
222
+ ok(nid in cg._dirty,
223
+ "G2c 标脏:写盘后 cg._dirty 含该节点", "dirty=%s" % list(cg._dirty))
224
+ ok(_cache_matches_disk(cg, e),
225
+ "G2d 同实例写后读与盘上真值逐位一致(insight_state/importance/tags 同批可见)",
226
+ "cached=%s direct=%s" % (cg._read(e)[0].get("insight_state"),
227
+ readcache.direct_read(cg, e)[0].get("insight_state")))
228
+ finally:
229
+ _close_all()
230
+ _cleanup(d)
231
+
232
+
233
+ # ================================================================ G3 C-1c scrub._apply_offset
234
+ def g3():
235
+ print("== G3 C-1c scrub._apply_offset:校准写盘后标脏 ==")
236
+ cg, d = _secured("scrub")
237
+ try:
238
+ cg.add("n_cal", "# 功能名:校准探针\n# 正文:香蕉 校准 optb1\n",
239
+ layer="knowledge", importance=0.5, confidence=0.6)
240
+ cg.flush()
241
+ readcache.install(cg)
242
+ e = _prime_cache(cg, "n_cal")
243
+ ok(cg._read(e)[0].get("confidence") == 0.6,
244
+ "G3a 前置:写前缓存快照 confidence=0.6")
245
+ adj, skipped = MOD["scrub"]._apply_offset(cg, 0.2, min_evidence=0,
246
+ actor="designer")
247
+ ok(adj == 1, "G3b 正对照:校准写盘成功(adjusted=1)",
248
+ "adjusted=%s skipped=%s" % (adj, skipped))
249
+ ok("n_cal" in cg._dirty,
250
+ "G3c 标脏:写盘后 cg._dirty 含该节点(本处原先连条目都不改、更不标脏)",
251
+ "dirty=%s" % list(cg._dirty))
252
+ ok(_cache_matches_disk(cg, e),
253
+ "G3d 同实例写后读与盘上真值逐位一致(confidence=0.8 可见)",
254
+ "cached=%s direct=%s" % (cg._read(e)[0].get("confidence"),
255
+ readcache.direct_read(cg, e)[0].get("confidence")))
256
+ finally:
257
+ _close_all()
258
+ _cleanup(d)
259
+
260
+
261
+ # ================================================================ G4 C-7 升格门槛
262
+ def g4():
263
+ print("== G4 C-7 consolidate.promote_memories:升格门槛接六要素真源 ==")
264
+ cg, d = _secured("c7")
265
+ try:
266
+ root = cg.root
267
+ weak = ("# 功能名:弱证据候选\n# 生效条件:问弱证据候选时\n# 子功能:x\n"
268
+ "# 执行:y\n# 不适用条件:无\n") # 缺「验证方式」
269
+ strong = weak + "# 验证方式:编译器/静态检查通过\n"
270
+ cg.add("cand_weak", "# 源\n" + weak, layer="contextual",
271
+ merge_count=3, importance=0.8)
272
+ cg.add("cand_strong", "# 源\n" + strong, layer="contextual",
273
+ merge_count=3, importance=0.8)
274
+ cg.flush()
275
+
276
+ ccg_req = MOD["consolidate"].CCG_REQUIRED
277
+ ok(ccg_req is nodefile.CCG_REQUIRED,
278
+ "G4a 转发真源:consolidate.CCG_REQUIRED is nodefile.CCG_REQUIRED(非本地副本)",
279
+ "consolidate=%r nodefile=%r" % (ccg_req, nodefile.CCG_REQUIRED))
280
+ ok(set(ccg_req) == set(nodefile.CCG_MARKS) and len(ccg_req) == 6
281
+ and "验证方式" in ccg_req,
282
+ "G4b 要素集=六要素(含「验证方式」——缺它即弱证据)",
283
+ ccg_req)
284
+
285
+ rep = MOD["consolidate"].promote_memories(root, apply=False)
286
+ ids = [s["id"] for s in rep.get("samples") or []]
287
+ ok("cand_weak" not in ids and rep.get("skipped_incomplete") == 1,
288
+ "G4c 缺「验证方式」的候选被拒(不计入升格候选,计入 skipped_incomplete)",
289
+ "targeted=%s skipped_incomplete=%s samples=%s"
290
+ % (rep.get("targeted"), rep.get("skipped_incomplete"), ids))
291
+ ok("cand_strong" in ids,
292
+ "G4d 正对照:六要素齐全者仍被升格候选(门槛收紧不得误伤合格节点)",
293
+ "targeted=%s samples=%s" % (rep.get("targeted"), ids))
294
+ finally:
295
+ _close_all()
296
+ _cleanup(d)
297
+
298
+
299
+ # ================================================================ G5 单点结构
300
+ def g5():
301
+ print("== G5 单点结构(防副本回归)==")
302
+ p = os.path.join(_HERE, "consolidate.py")
303
+ src = open(p, encoding="utf-8").read()
304
+ ok("CCG_REQUIRED = nodefile.CCG_REQUIRED" in src,
305
+ "G5a consolidate.py 以转发形态接线真源(源码可见)")
306
+ code = "\n".join(l for l in src.splitlines() if not l.strip().startswith("#"))
307
+ ok('CCG_REQUIRED = ("生效条件"' not in code
308
+ and "CCG_REQUIRED = ('生效条件'" not in code,
309
+ "G5b 可执行行内不再有本地 4 元素字面量副本(副本回归即红)")
310
+ # 三处 C-1 写点的标脏在同源先例处也在位(对照面:证明修复方向是本仓既有范式)
311
+ mdsrc = open(os.path.join(_HERE, "mdcg.py"), encoding="utf-8").read()
312
+ ok(mdsrc.count("self._dirty[node_id] = e") >= 2,
313
+ "G5c 对照先例在位:mdcg.py 的 update_tags / verify 直写分支显式标脏",
314
+ mdsrc.count("self._dirty[node_id] = e"))
315
+
316
+
317
+ # ================================================================ 变异表
318
+ # 每条 = (名称, MOD 键, 源码锚点, 替换文本, 预期转红条数)。
319
+ # 锚点必须逐字命中当前源码(找不到 → ANCHOR-MISS → 退出码 2,fail-closed)。
320
+ _MUTATIONS = (
321
+ ("C-1a 删 forgetting.reinforce 的标脏",
322
+ "forgetting",
323
+ " _dirty = getattr(cg, \"_dirty\", None)\n"
324
+ " if isinstance(_dirty, dict):\n"
325
+ " _dirty[node_id] = e\n",
326
+ " pass # MUT\n",
327
+ 2),
328
+ ("C-1b 删 insight.verify 的标脏",
329
+ "insight",
330
+ " _dirty = getattr(cg, \"_dirty\", None)\n"
331
+ " if isinstance(_dirty, dict):\n"
332
+ " _dirty[node_id] = ent\n",
333
+ " pass # MUT\n",
334
+ 2),
335
+ ("C-1c 删 scrub._apply_offset 的标脏",
336
+ "scrub",
337
+ " _dirty = getattr(cg, \"_dirty\", None)\n"
338
+ " if isinstance(_dirty, dict):\n"
339
+ " _dirty[nid] = e\n",
340
+ " pass # MUT\n",
341
+ 2),
342
+ ("C-7 恢复本地 4 元素副本(撤掉转发)",
343
+ "consolidate",
344
+ "CCG_REQUIRED = nodefile.CCG_REQUIRED",
345
+ "CCG_REQUIRED = (\"生效条件\", \"子功能\", \"执行\", \"不适用条件\")",
346
+ 3),
347
+ )
348
+
349
+
350
+ def _load_mutated(relpath: str, old: str, new: str):
351
+ """把 `relpath` 的源码做定点文本替换后 exec 成独立模块(不落盘、不改工作区)。"""
352
+ path = os.path.join(_REPO, relpath)
353
+ src = open(path, encoding="utf-8").read()
354
+ if old not in src:
355
+ return None, src
356
+ mut_src = src.replace(old, new, 1)
357
+ mod = types.ModuleType("md_cg._optb1_mut")
358
+ mod.__package__ = "md_cg"
359
+ mod.__file__ = path
360
+ exec(compile(mut_src, path, "exec"), mod.__dict__) # noqa: S102 —— 定点变异
361
+ return mod, src
362
+
363
+
364
+ _RELPATH = {
365
+ "forgetting": os.path.join("md_cg", "forgetting.py"),
366
+ "insight": os.path.join("md_cg", "insight.py"),
367
+ "scrub": os.path.join("md_cg", "scrub.py"),
368
+ "consolidate": os.path.join("md_cg", "consolidate.py"),
369
+ }
370
+
371
+
372
+ def _run_groups() -> int:
373
+ """跑全部断言组,返回失败数(变异核验复用)。"""
374
+ _PASS.clear()
375
+ _FAIL.clear()
376
+ for g in (g0, g1, g2, g3, g4, g5):
377
+ try:
378
+ g()
379
+ except Exception as exc: # noqa: BLE001
380
+ ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
381
+ _close_all()
382
+ return len(_FAIL)
383
+
384
+
385
+ def _mutate_mode(list_only: bool = False) -> int:
386
+ print("!! 定点变异自证:逐条把修复点改回缺陷形态,套件必须按预期条数转红\n")
387
+ if list_only:
388
+ for name, key, old, new, exp in _MUTATIONS:
389
+ print(" %-40s %-12s expect_red=%d" % (name, key, exp))
390
+ return 0
391
+
392
+ import contextlib
393
+ import io
394
+ anchor_miss = []
395
+ bad = []
396
+ with contextlib.redirect_stdout(io.StringIO()):
397
+ clean = _run_groups()
398
+ print(" 未变异基线:失败=%d(必须为 0)" % clean)
399
+ if clean:
400
+ bad.append("未变异基线即失败")
401
+
402
+ for name, key, old, new, expect in _MUTATIONS:
403
+ mod, src = _load_mutated(_RELPATH[key], old, new)
404
+ if mod is None:
405
+ print(" ANCHOR-MISS %s —— 锚点在 %s 当前源码里找不到"
406
+ "(实现改了却没同步本表;基线不得静默漂移)"
407
+ % (name, _RELPATH[key]))
408
+ anchor_miss.append(name)
409
+ continue
410
+ live = MOD[key]
411
+ MOD[key] = mod
412
+ try:
413
+ with contextlib.redirect_stdout(buf := io.StringIO()):
414
+ fails_extra = _run_groups()
415
+ detail = buf.getvalue()
416
+ finally:
417
+ MOD[key] = live
418
+ red = [l for l in detail.splitlines() if l.strip().startswith("FAIL ")]
419
+ verdict = "红" if fails_extra else "**仍全绿 = 该判据空转**"
420
+ mark = "OK " if fails_extra == expect else "MISMATCH"
421
+ print(" %s %-40s 红项=%d 预期=%d %s" % (mark, name, fails_extra, expect, verdict))
422
+ for l in red:
423
+ print(" " + l.strip()[5:])
424
+ if fails_extra != expect:
425
+ bad.append("%s(红=%d 预期=%d)" % (name, fails_extra, expect))
426
+
427
+ if anchor_miss:
428
+ print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
429
+ print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
430
+ return 2
431
+ print("\n定点变异自证:%s" % ("PASS(每处判据都被打红且恰好命中预期项数)"
432
+ if not bad else "FAIL —— " + "、".join(bad)))
433
+ return 0 if not bad else 1
434
+
435
+
436
+ def main() -> int:
437
+ if "--mutate" in sys.argv:
438
+ try:
439
+ return _mutate_mode("--list" in sys.argv)
440
+ finally:
441
+ _cleanup(_TMP)
442
+ try:
443
+ n_fail = _run_groups()
444
+ print("\nopt-batch1 守卫(md_cg 侧):%d 通过,%d 失败" % (len(_PASS), n_fail))
445
+ return 0 if not n_fail else 1
446
+ finally:
447
+ _cleanup(_TMP)
448
+
449
+
450
+ if __name__ == "__main__":
451
+ sys.exit(main())
package/md_cg/test_p1.py CHANGED
@@ -157,13 +157,13 @@ def main():
157
157
  queries = ["能量守恒", "二分查找", "细胞呼吸", "贝塞尔不等式", "牛顿第二定律",
158
158
  "光合作用", "事务隔离", "内力与截面法", "熵增", "机会成本"]
159
159
  contents = {nid: (cg.get(nid) or {}).get("content", "")
160
- for nid, e in cg.index["nodes"].items() if e["layer"] == "knowledge"}
160
+ for nid, e in list(cg.index["nodes"].items()) if e["layer"] == "knowledge"}
161
161
  t1 = t5 = 0
162
162
  n = 0
163
163
  blindspots = []
164
164
  print(f" {'query':<14}{'真值集':>7}{'召回':>6}{'Top-1':>7}")
165
165
  for q in queries:
166
- gold = [nid for nid, c in contents.items() if q in c]
166
+ gold = [nid for nid, c in list(contents.items()) if q in c]
167
167
  if not gold:
168
168
  blindspots.append(q)
169
169
  continue
@@ -251,10 +251,10 @@ def main():
251
251
  check("weakened → confidence 下调", post2 < pre2, f"{pre2} → {post2}")
252
252
  # falsified:add_rejected 按内容 hash 幂等,故断言「存在负记忆条目」而非计数增量
253
253
  cg.verify("p1_verify", "发现反例:条件不成立", "falsified")
254
- neg_ids = [e.get("path") for e in cg.index["nodes"].values()
254
+ neg_ids = [e.get("path") for e in list(cg.index["nodes"].values())
255
255
  if e["layer"] == "rejected"]
256
256
  neg_hit = False
257
- for p in neg_ids:
257
+ for p in list(neg_ids):
258
258
  full_p = os.path.join(ROOT, p)
259
259
  try:
260
260
  with open(full_p, encoding="utf-8") as f:
@@ -273,7 +273,7 @@ def main():
273
273
  # 「knowledge 层恰好 = 语料 + 本测试显式写入」就必须被显式守住——
274
274
  # 有残留节点会在这里打红,而不是被 rmtree 静默掩盖。
275
275
  explicit_knowledge = ("p1_ok", "p1_accept", "p1_defer", "p1_spot", "p1_reject")
276
- n_know = sum(1 for e in cg.index["nodes"].values() if e["layer"] == "knowledge")
276
+ n_know = sum(1 for e in list(cg.index["nodes"].values()) if e["layer"] == "knowledge")
277
277
  check("重跑 ≡ 首跑:knowledge 层节点数不多不少(无残留污染)",
278
278
  n_know == corpus.EXPECTED_NODES + len(explicit_knowledge),
279
279
  f"{n_know} == {corpus.EXPECTED_NODES} + {len(explicit_knowledge)}")