@furongjun1999/dsh-memory 0.5.0 → 0.6.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 (156) hide show
  1. package/README.md +572 -465
  2. package/docs/README.md +1 -0
  3. 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
  4. 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
  5. package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
  6. 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
  7. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  8. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  9. package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
  10. package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
  11. 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
  12. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
  13. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
  14. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
  15. 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_v1.md +156 -0
  16. 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_v10.md +210 -0
  17. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v11.md +227 -0
  18. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v12.md +203 -0
  19. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v13.md +233 -0
  20. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v14.md +191 -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_v15.md +213 -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_v16.md +214 -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_v17.md +199 -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_v18.md +183 -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_v19.md +207 -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_v2.md +156 -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_v3.md +152 -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_v4.md +128 -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_v5.md +114 -0
  30. 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_v6.md +192 -0
  31. 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_v7.md +187 -0
  32. 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_v8.md +207 -0
  33. 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_v9.md +203 -0
  34. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
  35. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +19 -3
  36. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  37. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
  38. 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
  39. package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
  40. package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
  41. package/dsh/cordis-patch-profile-web.example.yml +35 -0
  42. package/lib/bridge.d.ts +9 -0
  43. package/lib/bridge.js +35 -0
  44. package/lib/cli.d.ts +3 -0
  45. package/lib/cli.js +66 -0
  46. package/lib/index.js +7 -1
  47. package/lib/init.d.ts +88 -0
  48. package/lib/init.js +287 -0
  49. package/lib/lib/roleplay_web.js +530 -443
  50. package/lib/lib/token_store.d.ts +7 -1
  51. package/lib/lib/token_store.js +12 -3
  52. package/md_cg/audit.py +12 -1
  53. package/md_cg/backfill.py +16 -15
  54. package/md_cg/backfill_bucket_zh.py +35 -0
  55. package/md_cg/bench_e2e_judge.py +532 -0
  56. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  57. package/md_cg/bench_e2e_qa.py +256 -0
  58. package/md_cg/branches.py +18 -2
  59. package/md_cg/ccgc.py +3 -2
  60. package/md_cg/chain.py +19 -4
  61. package/md_cg/consolidate.py +7 -6
  62. package/md_cg/crosscheck.py +4 -3
  63. package/md_cg/crypto.py +439 -437
  64. package/md_cg/datapath.py +395 -335
  65. package/md_cg/evidence.py +8 -3
  66. package/md_cg/export.py +3 -1
  67. package/md_cg/forgetting.py +2 -2
  68. package/md_cg/fsutil.py +48 -0
  69. package/md_cg/hotcache.py +255 -238
  70. package/md_cg/interop.py +161 -22
  71. package/md_cg/judgment_manifest.py +177 -0
  72. package/md_cg/links.py +655 -622
  73. package/md_cg/mcp_server.py +282 -31
  74. package/md_cg/mdcg.py +702 -126
  75. package/md_cg/mdcos.py +454 -68
  76. package/md_cg/mreview/govern.py +5 -4
  77. package/md_cg/postings.py +4 -2
  78. package/md_cg/readcache.py +76 -18
  79. package/md_cg/reconcile.py +228 -0
  80. package/md_cg/review_cli.py +215 -0
  81. package/md_cg/routing.py +28 -0
  82. package/md_cg/run_tests.py +211 -0
  83. package/md_cg/scrub.py +862 -852
  84. package/md_cg/security.py +385 -275
  85. package/md_cg/selfreport.py +3 -2
  86. package/md_cg/signer.py +565 -562
  87. package/md_cg/sources.py +3 -2
  88. package/md_cg/stg.py +6 -0
  89. package/md_cg/sustain.py +1168 -1138
  90. package/md_cg/test_access_hints.py +147 -0
  91. package/md_cg/test_branch_discard_tombstone.py +136 -0
  92. package/md_cg/test_branches.py +259 -249
  93. package/md_cg/test_chain_read_isolate.py +168 -0
  94. package/md_cg/test_datapath_device_name.py +203 -0
  95. package/md_cg/test_emit_negtail_cache.py +156 -0
  96. package/md_cg/test_en_pipeline.py +186 -166
  97. package/md_cg/test_govern_directread.py +421 -0
  98. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  99. package/md_cg/test_identity_attribution.py +228 -147
  100. package/md_cg/test_index_crossprocess_reload.py +301 -0
  101. package/md_cg/test_index_durability.py +238 -224
  102. package/md_cg/test_interop.py +4 -2
  103. package/md_cg/test_interop_judgment.py +228 -0
  104. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  105. package/md_cg/test_links_concurrent_write.py +188 -0
  106. package/md_cg/test_merge_upsert.py +168 -0
  107. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  108. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  109. package/md_cg/test_n131_merge_gate.py +205 -0
  110. package/md_cg/test_none_id_write_guard.py +165 -0
  111. package/md_cg/test_p1x_ref_root.py +160 -0
  112. package/md_cg/test_p27_docindex.py +774 -765
  113. package/md_cg/test_p2_mcp.py +3 -0
  114. package/md_cg/test_p32_backfill.py +304 -298
  115. package/md_cg/test_p39_verify_flow.py +90 -50
  116. package/md_cg/test_p47_session_view.py +60 -25
  117. package/md_cg/test_propose_tail_index.py +157 -0
  118. package/md_cg/test_read_scope_b27.py +277 -0
  119. package/md_cg/test_readcache_default_on.py +168 -0
  120. package/md_cg/test_readcache_precise_inval.py +270 -0
  121. package/md_cg/test_readcache_prodpath.py +55 -7
  122. package/md_cg/test_reconcile_v0.py +294 -0
  123. package/md_cg/test_retr_s1.py +6 -2
  124. package/md_cg/test_retr_s1b.py +67 -0
  125. package/md_cg/test_retr_s7.py +8 -0
  126. package/md_cg/test_retr_s9_entity_ctx.py +181 -175
  127. package/md_cg/test_retr_score_once.py +208 -0
  128. package/md_cg/test_review_cli_attribution.py +177 -0
  129. package/md_cg/test_review_cli_visibility.py +235 -0
  130. package/md_cg/test_review_onepass.py +170 -0
  131. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  132. package/md_cg/test_security_audit.py +155 -0
  133. package/md_cg/test_security_audit_b26.py +161 -0
  134. package/md_cg/test_security_audit_v21.py +250 -0
  135. package/md_cg/test_semantic_canonical.py +255 -241
  136. package/md_cg/test_session_isolation.py +168 -0
  137. package/md_cg/test_snapshot_autoclose.py +187 -0
  138. package/md_cg/test_tail_watermark_race.py +208 -0
  139. package/md_cg/test_tenant_env_override_warn.py +139 -0
  140. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  141. package/md_cg/test_v14_fixes.py +415 -397
  142. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  143. package/md_cg/theory.py +276 -273
  144. package/md_cg/tokens.py +734 -677
  145. package/md_cg/units.py +3 -2
  146. package/md_cg/vision_evidence.py +4 -3
  147. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  148. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  149. package/md_cg/writepipe.py +554 -550
  150. package/package.json +11 -2
  151. package/src/bridge.ts +434 -401
  152. package/src/cli.ts +65 -0
  153. package/src/index.ts +526 -518
  154. package/src/init.ts +347 -0
  155. package/src/lib/roleplay_web.ts +1019 -932
  156. package/src/lib/token_store.ts +202 -192
@@ -0,0 +1,421 @@
1
+ # -*- coding: utf-8 -*-
2
+ """治理写面读-改-写直读守卫(缺陷:读缓存默认开 × 治理 RMW 读点未走 direct_read)。
3
+
4
+ 缺陷形态(缺陷报告 2026-09-25 复现证实,high):读缓存默认开
5
+ (readcache.py:36-38 install 默认装配,mdcos.py:366-368 构造即装)后,
6
+ `cg._read` 是进程内脏代际快照——**只对本实例写路径失效**。治理面的
7
+ 读-改-写(ccgc.link/recalibrate、crosscheck 主流程/回滚、
8
+ vision_evidence 执行/回滚)若在写前用 `cg._read` 重查节点,
9
+ 则「A 常驻实例检索装载缓存 → B 另一实例以统一写口 `_write_node`
10
+ 落盘修正 → A 同实例执行治理写」时,A 读到的是**装载时的旧快照**,
11
+ 写回后 B 的修正被静默覆盖丢失(默认开组幸存=False;
12
+ MDCG_READ_CACHE=0 对照组幸存=True)。
13
+
14
+ 修复口径(与第 7 轮 backfill 15 处 / mreview.govern 3 处一致):
15
+ 治理文件写前重查/回滚比对读点改走 `readcache.direct_read`
16
+ (穿透缓存读盘上真值)。本守卫先证旧代码红(G1-G6),再由修复转绿。
17
+
18
+ 断言分两类:
19
+ G1-G6 红守卫(cg/x 参数传入常驻实例 → 陈旧形态真实可达):
20
+ G1/G2 ccgc.recalibrate / link;G3/G4 crosscheck 主流程 / 回滚;
21
+ G5/G6 vision_evidence.apply / rollback。
22
+ G7-G12 口径守卫(consolidate 六读点为 root 入口内部自建**新实例**,
23
+ 单次调用内首触即盘上真值,先写 B 修正并不丢失——断言改为
24
+ 「direct_read 改动不破坏这些流程」,非红证明)。
25
+
26
+ 运行:python -m md_cg.test_govern_directread
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import shutil
33
+ import sys
34
+ import tempfile
35
+
36
+ from . import ccgc, consolidate, crosscheck, vision_evidence
37
+ from .mdcos import MdCGOS
38
+
39
+ PASS = FAIL = 0
40
+ FAILS = []
41
+
42
+ MARK = "B实例的重要修正v1"
43
+ MARK2 = "B实例的重要修正v2"
44
+
45
+ CS_FULL = {
46
+ "observation_position": "此功能在本地文件系统中生效",
47
+ "observation_tool": "该方法使用扩展名映射表进行路由",
48
+ "time_window": [0.0, 9999999999.0],
49
+ "existence_constraint": "仅对本地可读文件生效",
50
+ }
51
+
52
+
53
+ def ok(cond, label):
54
+ global PASS, FAIL
55
+ if cond:
56
+ PASS += 1
57
+ print(f" OK {label}")
58
+ else:
59
+ FAIL += 1
60
+ FAILS.append(label)
61
+ print(f" FAIL {label}")
62
+
63
+
64
+ def _disk(root, nid):
65
+ """盘上真值直读:新实例首触(构造后未读该路径)即磁盘当前内容。"""
66
+ cg = MdCGOS(root)
67
+ e = (cg.index.get("nodes") or {}).get(nid)
68
+ if not e:
69
+ return None, None
70
+ fn = getattr(cg, "_read_uncached", None) or cg._read
71
+ return fn(e)
72
+
73
+
74
+ def _b_write(root, nid, marker):
75
+ """B = 另一实例(全合作写者):读盘上真值 → 加 fm 标记 → 统一写口落盘。"""
76
+ b = MdCGOS(root)
77
+ e = b.index["nodes"][nid]
78
+ fm, content = b._read(e) # B 新实例首触 = 盘上真值
79
+ fm["owner_note"] = marker
80
+ b._write_node(nid, os.path.join(root, e["path"]), fm, content,
81
+ durable=True)
82
+ return marker
83
+
84
+
85
+ def _warm(a, nid):
86
+ """A 常驻实例检索装载缓存(生产形态:search/_read 装配后常驻)。"""
87
+ return a._read(a.index["nodes"][nid])
88
+
89
+
90
+ def _doc(fn, cond, sub, exe, va="", neg="问完全无关主题"):
91
+ lines = [f"# 功能名:{fn}", f"# 生效条件:{cond}",
92
+ f"# 子功能:{sub}", f"# 执行:{exe}"]
93
+ if va:
94
+ lines.append(f"# 验证方式:{va}")
95
+ lines.append(f"# 不适用条件:{neg}")
96
+ return "\n".join(lines) + "\n"
97
+
98
+
99
+ # ---- G1 ccgc.recalibrate(缺陷报告原样复现:ccgc.py:841 读 → :860 写)----
100
+
101
+ def g1_recalibrate(tmp):
102
+ print("== G1 ccgc.recalibrate(缺陷原样复现) ==")
103
+ root = os.path.join(tmp, "g1")
104
+ os.makedirs(root)
105
+ a = MdCGOS(root)
106
+ a.add("gd_recal", "# 功能名:直读守卫节点\n\n正文。\n", layer="knowledge",
107
+ condition_space=dict(CS_FULL))
108
+ a.flush()
109
+ _warm(a, "gd_recal") # A 装载缓存
110
+ _b_write(root, "gd_recal", MARK) # B 统一写口落盘重要修正
111
+ fm_a, _ = _warm(a, "gd_recal")
112
+ ok(fm_a.get("owner_note") is None,
113
+ "G1a A 进程内缓存陈旧(治理写前重查命中旧代际)")
114
+ rc = ccgc.recalibrate("gd_recal",
115
+ {"existence_constraint": "修正后的存在约束GD"},
116
+ "designer", "agent:a1", evidence="守卫复核",
117
+ cg=a, apply=True)
118
+ ok(rc.ok and rc.written == 1, "G1b recalibrate 执行成功(含写入)")
119
+ fm, _content = _disk(root, "gd_recal")
120
+ ok(fm.get("owner_note") == MARK,
121
+ "G1c B 修正幸存(写前重查走盘上真值,不被陈旧缓存覆盖)")
122
+ ok((fm.get("condition_space") or {}).get("existence_constraint")
123
+ == "修正后的存在约束GD", "G1d A 的治理写入仍生效")
124
+
125
+ # 对照组:MDCG_READ_CACHE=0(退出阀)同流程 → 无陈旧,恒幸存
126
+ os.environ["MDCG_READ_CACHE"] = "0"
127
+ try:
128
+ root0 = os.path.join(tmp, "g1_ctl")
129
+ os.makedirs(root0)
130
+ a0 = MdCGOS(root0)
131
+ a0.add("gd_recal", "# 功能名:直读守卫节点\n\n正文。\n",
132
+ layer="knowledge", condition_space=dict(CS_FULL))
133
+ a0.flush()
134
+ _warm(a0, "gd_recal")
135
+ _b_write(root0, "gd_recal", MARK)
136
+ rc0 = ccgc.recalibrate("gd_recal",
137
+ {"existence_constraint": "修正后的存在约束GD"},
138
+ "designer", "agent:a1", cg=a0, apply=True)
139
+ fm0, _ = _disk(root0, "gd_recal")
140
+ ok(rc0.ok and fm0.get("owner_note") == MARK,
141
+ "G1e 对照组(缓存关)B 修正幸存——丢失由默认开缓存引入")
142
+ finally:
143
+ os.environ.pop("MDCG_READ_CACHE", None)
144
+
145
+
146
+ # ---- G2 ccgc.link(ccgc.py:757 读 → :784 写)----
147
+
148
+ DIALOG = ("文件摄取分派:按扩展名把文件路由到对应摄取器"
149
+ "。该方法使用扩展名映射表进行路由"
150
+ "。此功能在本地文件系统中生效"
151
+ "。仅对本地可读文件生效"
152
+ "。2026-09-16"
153
+ "。通过回放测试验证"
154
+ "。二进制文件不适用")
155
+
156
+ SLOTS = {
157
+ "observation_position": {"value": "此功能在本地文件系统中生效"},
158
+ "observation_tool": {"value": "该方法使用扩展名映射表进行路由"},
159
+ "time_window": {"value": [0.0, 9999999999.0]},
160
+ "existence_constraint": {"value": "仅对本地可读文件生效"},
161
+ }
162
+
163
+ MARKS = {
164
+ "功能名": {"value": "文件摄取分派:按扩展名把文件路由到对应摄取器"},
165
+ "子功能": {"value": "按扩展名把文件路由到对应摄取器"},
166
+ "执行": {"value": "使用扩展名映射表进行路由"},
167
+ "验证方式": {"value": "回放测试", "basis": "test"},
168
+ "不适用条件": {"value": "二进制文件不适用"},
169
+ }
170
+
171
+
172
+ def g2_link(tmp):
173
+ print("== G2 ccgc.link ==")
174
+ root = os.path.join(tmp, "g2")
175
+ os.makedirs(root)
176
+ a = MdCGOS(root)
177
+ a.add("gd_link", "# 功能名:待编译节点\n\n占位正文。\n", layer="knowledge")
178
+ a.flush()
179
+ _warm(a, "gd_link")
180
+ _b_write(root, "gd_link", MARK)
181
+ r = ccgc.compile_dialog(DIALOG, "gd_link", "agent:a1",
182
+ slots=SLOTS, marks=MARKS, cg=a)
183
+ ok(r.success, "G2a 编译成功")
184
+ a_ok = ccgc.attest("gd_link", ccgc.ACCEPT, "designer", "agent:a1",
185
+ evidence="守卫:候选与对话原文一致", cg=a)
186
+ ok(a_ok.ok, "G2b 编外验证方签章")
187
+ l = ccgc.link(r, a_ok, cg=a, apply=True)
188
+ ok(l.ok and l.written == 6, "G2c link 写入六行")
189
+ fm, _c = _disk(root, "gd_link")
190
+ ok(fm.get("owner_note") == MARK, "G2d B 修正幸存(link 写前重查走直读)")
191
+ ok(fm.get("condition_space") == {k: v["value"] for k, v in SLOTS.items()},
192
+ "G2e link 自身写入仍生效(condition_space 落盘)")
193
+
194
+
195
+ # ---- G3/G4 crosscheck 主流程(:344/:786 读 → :674 写)与回滚(:941 读 → :970 写)----
196
+
197
+ SRC_MEASURE = ["《函数单调性基准集》v1 第 3 节"]
198
+ SCI_NAME = "数学·函数单调性判定"
199
+
200
+
201
+ def _verdict_rows(nid, value):
202
+ return [
203
+ {"id": nid, "unit": "reflect", "field": "验证方式", "value": value,
204
+ "basis": "measurement", "source": SRC_MEASURE, "verdict": "accept"},
205
+ {"id": nid, "unit": "verify", "field": "验证方式", "value": value,
206
+ "verdict": "accept"},
207
+ ]
208
+
209
+
210
+ def g3_crosscheck(tmp):
211
+ print("== G3 crosscheck 主流程(写前重查) ==")
212
+ root = os.path.join(tmp, "g3")
213
+ os.makedirs(root)
214
+ a = MdCGOS(root)
215
+ a.add("gd_cc", _doc("函数单调性判定", "问单调性", "判定单调区间", "取导数定号"),
216
+ layer="knowledge", state_attributes={"name": SCI_NAME})
217
+ a.flush()
218
+ _warm(a, "gd_cc")
219
+ _b_write(root, "gd_cc", MARK)
220
+ rep = crosscheck.crosscheck(a, ids=["gd_cc"],
221
+ verdicts=_verdict_rows("gd_cc",
222
+ "以基准函数集实测判定单调区间"),
223
+ apply=True, batch="gdxb", actor="guard")
224
+ ok(rep["written"] == 1, f"G3a crosscheck 写入 1(实测 {rep['written']})")
225
+ fm, c = _disk(root, "gd_cc")
226
+ ok(fm.get("owner_note") == MARK, "G3b B 修正幸存(:786 写前重查走直读)")
227
+ ok(fm.get("verification_basis") == "measurement"
228
+ and "# 验证方式:以基准函数集实测判定单调区间" in c,
229
+ "G3c crosscheck 自身写入仍生效")
230
+ return root
231
+
232
+
233
+ def g4_crosscheck_rollback(root):
234
+ print("== G4 crosscheck.rollback(回滚比对) ==")
235
+ a2 = MdCGOS(root)
236
+ _warm(a2, "gd_cc") # A2 装载(含上一步 evidence 现场)
237
+ _b_write(root, "gd_cc", MARK2) # B 再落一笔修正
238
+ rep2 = crosscheck.rollback(a2, batch="gdxb")
239
+ ok(rep2["reverted"] == 1, f"G4a 回滚 1(实测 {rep2['reverted']})")
240
+ fm, c = _disk(root, "gd_cc")
241
+ ok(fm.get("owner_note") == MARK2,
242
+ "G4b B 修正幸存(:941 回滚比对走直读,write_id 防护所验 fm 为盘上真值)")
243
+ ok(fm.get("verification_basis") is None
244
+ and "# 验证方式:" not in c,
245
+ "G4c 回滚自身仍生效(验证方式行/基底还原)")
246
+
247
+
248
+ # ---- G5/G6 vision_evidence.apply(:296/:495 读 → :522 写)与 rollback(:579 读 → :591 写)----
249
+
250
+ def g5_vision_apply(tmp):
251
+ print("== G5 vision_evidence.apply ==")
252
+ root = os.path.join(tmp, "g5")
253
+ os.makedirs(root)
254
+ aeis = os.path.join(tmp, "g5_aeis") # 空归档目录(不触真实数据目录)
255
+ os.makedirs(aeis)
256
+ a = MdCGOS(root)
257
+ a.add("imgpart_gd",
258
+ "img9 部件 torso: bbox=[1, 1, 2, 2] cond_hash=eeee555566667777 "
259
+ "verdict=ACCEPT reason=x fg=0.5\n",
260
+ layer="contextual", tags=["vision", "img9", "part", "torso"])
261
+ a.flush()
262
+ _warm(a, "imgpart_gd")
263
+ _b_write(root, "imgpart_gd", MARK)
264
+ rep = vision_evidence.apply(a, aeis_root_=aeis)
265
+ ok(rep["written"] >= 1, f"G5a 盲区落盘 ≥1(实测 {rep['written']})")
266
+ fm, _c = _disk(root, "imgpart_gd")
267
+ ok(fm.get("owner_note") == MARK,
268
+ "G5b B 修正幸存(:495 写前重查走直读)")
269
+ ok(fm.get("evidence_status") is not None,
270
+ "G5c 证据面写入仍生效")
271
+ return root, rep["batch"]
272
+
273
+
274
+ def g6_vision_rollback(root, batch):
275
+ print("== G6 vision_evidence.rollback ==")
276
+ a2 = MdCGOS(root)
277
+ _warm(a2, "imgpart_gd")
278
+ _b_write(root, "imgpart_gd", MARK2)
279
+ rep2 = vision_evidence.rollback(a2, batch=batch)
280
+ ok(rep2["reverted"] >= 1, f"G6a 回滚 ≥1(实测 {rep2['reverted']})")
281
+ fm, _c = _disk(root, "imgpart_gd")
282
+ ok(fm.get("owner_note") == MARK2, "G6b B 修正幸存(:579 回滚读走直读)")
283
+ ok(fm.get("evidence_status") is None, "G6c 回滚自身仍生效(证据键清除)")
284
+
285
+
286
+ # ---- G7-G12 consolidate(口径守卫:root 入口内部自建新实例,非红证明)----
287
+
288
+ def g7_fill_verification_basis(tmp):
289
+ print("== G7 consolidate.fill_verification_basis(口径) ==")
290
+ root = os.path.join(tmp, "g7")
291
+ os.makedirs(root)
292
+ a = MdCGOS(root)
293
+ a.add("gd_fvb", _doc("函数单调性判定", "问单调性", "判定单调区间", "取导数定号"),
294
+ layer="knowledge")
295
+ a.flush()
296
+ _b_write(root, "gd_fvb", MARK) # 先于调用的 B 写:新实例首触可见
297
+ rep = consolidate.fill_verification_basis(root, "编译器/静态检查通过",
298
+ apply=True)
299
+ ok(rep["written"] == 1, f"G7a 补写 1(实测 {rep['written']})")
300
+ fm, c = _disk(root, "gd_fvb")
301
+ ok(fm.get("owner_note") == MARK, "G7b B 修正幸存")
302
+ ok("# 验证方式:编译器/静态检查通过" in c, "G7c 补写自身生效")
303
+
304
+
305
+ def _g89_root(tmp):
306
+ root = os.path.join(tmp, "g8")
307
+ os.makedirs(root)
308
+ a = MdCGOS(root)
309
+ a.add("gd_pro",
310
+ "# 功能名:待提升节点\n# 生效条件:问提升\n# 子功能:说明提升\n"
311
+ "# 执行:按热度\n# 不适用条件:问无关\n",
312
+ layer="contextual", merge_count=2)
313
+ a.flush()
314
+ return root
315
+
316
+
317
+ def g8_promote(tmp):
318
+ print("== G8/G9 consolidate.promote / rollback_promotion(口径) ==")
319
+ root = _g89_root(tmp)
320
+ _b_write(root, "gd_pro", MARK)
321
+ rep = consolidate.promote_memories(root, min_merge=1, apply=True)
322
+ ok(rep["written"] == 1, f"G8a 提升 1(实测 {rep['written']})")
323
+ fm, _c = _disk(root, "gd_pro")
324
+ ok(fm.get("layer") == "knowledge", "G8b 已迁 knowledge")
325
+ ok(fm.get("owner_note") == MARK, "G8c B 修正幸存")
326
+ rep2 = consolidate.rollback_promotion(root)
327
+ ok(rep2.get("ok") and rep2["reverted"] == 1,
328
+ f"G9a 回滚 1(实测 {rep2})")
329
+ fm2, _c2 = _disk(root, "gd_pro")
330
+ ok(fm2.get("layer") == "contextual", "G9b 迁回 contextual")
331
+ ok(fm2.get("owner_note") == MARK, "G9c B 修正幸存(回滚读走直读口径)")
332
+
333
+
334
+ def g10_contextualize(tmp):
335
+ print("== G10/G11 consolidate.contextualize / rollback(口径) ==")
336
+ root = os.path.join(tmp, "g10")
337
+ os.makedirs(root)
338
+ a = MdCGOS(root)
339
+ a.add("note_gd", "# 功能名:批次流水账\n\n正文。\n", layer="knowledge")
340
+ a.flush()
341
+ _b_write(root, "note_gd", MARK)
342
+ rep = consolidate.contextualize_prefixes(root, prefixes=["note_"],
343
+ apply=True)
344
+ ok(rep["written"] == 1, f"G10a 归位 1(实测 {rep['written']})")
345
+ fm, _c = _disk(root, "note_gd")
346
+ ok(fm.get("layer") == "contextual", "G10b 已归位 contextual")
347
+ ok(fm.get("owner_note") == MARK, "G10c B 修正幸存")
348
+ rep2 = consolidate.rollback_contextualize(root)
349
+ ok(rep2.get("ok") and rep2["reverted"] == 1, "G11a 回滚 1")
350
+ fm2, _c2 = _disk(root, "note_gd")
351
+ ok(fm2.get("layer") == "knowledge", "G11b 迁回 knowledge")
352
+ ok(fm2.get("owner_note") == MARK, "G11c B 修正幸存(回滚读走直读口径)")
353
+
354
+
355
+ BODY = ("# 功能名:微分方程应用\n"
356
+ "微分方程应用:建模与求解,使用数值方法迭代求解,需给定边界条件\n")
357
+ CAND = {
358
+ "生效条件": ["微分方程", "数值方法"],
359
+ "子功能": ["建模", "求解"],
360
+ "执行": "使用数值方法迭代求解",
361
+ "不适用条件": ["边界条件缺失"],
362
+ }
363
+
364
+
365
+ def _fake_reflect(prompt):
366
+ return "```json\n" + json.dumps(CAND, ensure_ascii=False) + "\n```"
367
+
368
+
369
+ def _fake_verify(prompt):
370
+ out = {f: {"keep": (v if isinstance(v, list) else [v]), "drop": [],
371
+ "reason": "逐条核验"}
372
+ for f, v in CAND.items()}
373
+ return json.dumps(out, ensure_ascii=False)
374
+
375
+
376
+ def g12_consolidate(tmp):
377
+ print("== G12 consolidate.consolidate(LLM 固化流,口径) ==")
378
+ root = os.path.join(tmp, "g12")
379
+ os.makedirs(root)
380
+ a = MdCGOS(root)
381
+ a.add("gd_llm", BODY, layer="knowledge")
382
+ a.flush()
383
+ _b_write(root, "gd_llm", MARK)
384
+ rep = consolidate.consolidate(root, apply=True, reflect_fn=_fake_reflect,
385
+ verify_fn=_fake_verify,
386
+ verification_basis="回放测试核验(守卫)")
387
+ ok(rep["written"] == 1, f"G12a 固化 1(实测 {rep['written']})")
388
+ fm, c = _disk(root, "gd_llm")
389
+ ok(fm.get("owner_note") == MARK, "G12b B 修正幸存(:678 读走直读口径)")
390
+ ok("# 生效条件:" in c and "# 验证方式:回放测试核验(守卫)" in c,
391
+ "G12c 固化自身生效")
392
+
393
+
394
+ def main():
395
+ os.environ.pop("MDCG_READ_CACHE", None)
396
+ tmp = tempfile.mkdtemp(prefix="mdcg_gov_directread_")
397
+ try:
398
+ g1_recalibrate(tmp)
399
+ g2_link(tmp)
400
+ g3root = g3_crosscheck(tmp)
401
+ g4_crosscheck_rollback(g3root)
402
+ g5root, g5batch = g5_vision_apply(tmp)
403
+ g6_vision_rollback(g5root, g5batch)
404
+ g7_fill_verification_basis(tmp)
405
+ g8_promote(tmp)
406
+ g10_contextualize(tmp)
407
+ g12_consolidate(tmp)
408
+ finally:
409
+ os.environ.pop("MDCG_READ_CACHE", None)
410
+ shutil.rmtree(tmp, ignore_errors=True)
411
+
412
+ print("\n" + "=" * 60)
413
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
414
+ if FAILS:
415
+ for f in FAILS:
416
+ print(f" - {f}")
417
+ return 0 if FAIL == 0 else 1
418
+
419
+
420
+ if __name__ == "__main__":
421
+ sys.exit(main())
@@ -0,0 +1,218 @@
1
+ # -*- coding: utf-8 -*-
2
+ """issue #32 守卫:query 缓存的**口径键**必须覆盖进程级开关(env)。
3
+
4
+ 病灶(2026-09-24 审查复现):v14 缺陷 C 修的是**参数面**(include_work /
5
+ roles / paths / fusion / judge_ranking / …),但还有一类口径来自**环境变量**
6
+ ——MDCG_SEMANTIC / MDCG_EN_ATOMS / MDCG_UNIFY_QUERY。它们不入键时,同一 query
7
+ 在切换开关后会命中**另一口径**的缓存(cached=True + 陈旧结果),表现为静默
8
+ 错答:先按 MDCG_SEMANTIC=1 查一次写缓存,再关掉语义路重查,拿到的仍是
9
+ 「语义路开启时」的排序。
10
+
11
+ 修法(本件守门):`hotcache._KEYED_EXTRA` 增列 `env_switch`,
12
+ 调用方 `mdcos.search_rrf` 以 `hotcache.env_switch_key()` 填该键——开关一变
13
+ 键就变,缓存自然不命中;同一口径内缓存收益不变。
14
+
15
+ 断言(含能红旧实现的两条):
16
+ K1 _KEYED_EXTRA 登记 env_switch(清单真源,防漏登)
17
+ K2 env_switch_key() 随开关取值变化(未设 = None 也进键)
18
+ K3 semantic=1 首查:gold 经语义资格路命中(非缓存)
19
+ K4 切到 semantic=0 后同 query **不命中**旧口径缓存(旧实现这里 cached=True)
20
+ K5 切开关后的结果 == 关缓存重算的结果(无跨口径串味)
21
+ K6 同口径重复查仍命中缓存(修完不许把缓存打废)
22
+
23
+ 运行:python -m md_cg.test_i32_hotcache_env_key
24
+ """
25
+ import json
26
+ import os
27
+ import shutil
28
+ import sys
29
+ import tempfile
30
+
31
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
32
+
33
+ from md_cg import eval_common as ec # noqa: E402
34
+ from md_cg import hotcache, mdcos # noqa: E402
35
+
36
+ PASS = FAIL = 0
37
+ FAILS = []
38
+
39
+
40
+ def check(name, cond, detail=""):
41
+ global PASS, FAIL
42
+ if cond:
43
+ PASS += 1
44
+ print(f" OK {name}")
45
+ else:
46
+ FAIL += 1
47
+ FAILS.append(name)
48
+ print(f" FAIL {name} {detail}")
49
+
50
+
51
+ def _mk(root):
52
+ from md_cg.mdcos import MdCGOS
53
+ return MdCGOS(root, autoflush=500)
54
+
55
+
56
+ def _query(cg, q="horse meat"):
57
+ res, meta = cg.search_rrf(q, k=5, paths=("lexical",), judge=False,
58
+ record=False)
59
+ return [r[0]["id"] for r in res], res, meta
60
+
61
+
62
+ def main():
63
+ ec.unlock_global_cap()
64
+ ec.use_jaccard()
65
+
66
+ print("== K1/K2 清单真源与键值 ==")
67
+ check("K1 _KEYED_EXTRA 登记 env_switch",
68
+ "env_switch" in hotcache._KEYED_EXTRA, str(hotcache._KEYED_EXTRA))
69
+ os.environ.pop("MDCG_SEMANTIC", None)
70
+ before = hotcache.env_switch_key()
71
+ os.environ["MDCG_SEMANTIC"] = "1"
72
+ after = hotcache.env_switch_key()
73
+ os.environ.pop("MDCG_SEMANTIC", None)
74
+ check("K2 env_switch_key() 随开关变化(未设=None 也进键)",
75
+ before != after and any(v is None for _k, v in before),
76
+ f"{before} vs {after}")
77
+
78
+ tmp = tempfile.mkdtemp(prefix="mdcg_i32_")
79
+ try:
80
+ rooth = os.path.join(tmp, "hot")
81
+ rootn = os.path.join(tmp, "nocache")
82
+ for root in (rooth, rootn):
83
+ cg = _mk(root)
84
+ # gold 只有 fm.semantic(「马 肉」)能对上英文 query;正文是中文,
85
+ # 归一路关闭时词法零交集 → 语义路开关是唯二变量(另一个是归一层)。
86
+ cg.add("gold", "今天午饭吃了马肉,味道不错,下次还做。",
87
+ layer="knowledge", semantic="马 肉", verification_basis="data")
88
+ cg.add("d1", "下午去市场买了马,价格比昨天便宜一些。",
89
+ layer="knowledge", verification_basis="data")
90
+ cg.flush()
91
+ cg.close()
92
+
93
+ print("== K3~K6 跨口径缓存(MDCG_HOTCACHE=1,归一层关闭以隔离变量)==")
94
+ os.environ["MDCG_HOTCACHE"] = "1"
95
+ os.environ["MDCG_UNIFY_QUERY"] = "0"
96
+ cg = _mk(rooth)
97
+ try:
98
+ os.environ["MDCG_SEMANTIC"] = "1"
99
+ ids_sem, _r, m_sem = _query(cg)
100
+ check("K3 semantic=1 首查:gold 命中且非缓存",
101
+ bool(ids_sem) and ids_sem[0] == "gold"
102
+ and m_sem.get("cached") is not True,
103
+ f"{ids_sem} cached={m_sem.get('cached')}")
104
+
105
+ os.environ.pop("MDCG_SEMANTIC", None)
106
+ ids_off, _r2, m_off = _query(cg)
107
+ check("K4 切开关后不命中旧口径缓存(旧实现 cached=True)",
108
+ m_off.get("cached") is not True,
109
+ f"cached={m_off.get('cached')} ids={ids_off}")
110
+ check("K5 切开关后结果与 semantic=1 不同(无跨口径串味)",
111
+ ids_off != ids_sem,
112
+ f"sem={ids_sem} / off={ids_off}")
113
+
114
+ _ids3, _r3, m3 = _query(cg)
115
+ check("K6 同口径重复查仍命中缓存(缓存未被打废)",
116
+ m3.get("cached") is True, json.dumps(m3, default=str)[:160])
117
+ finally:
118
+ cg.close()
119
+ os.environ.pop("MDCG_SEMANTIC", None)
120
+
121
+ # 旁路对照:不挂热缓存重算 semantic=0 的结果必须与 K5 一致
122
+ os.environ.pop("MDCG_HOTCACHE", None)
123
+ cg2 = _mk(rootn)
124
+ try:
125
+ ids_plain, _r4, _m4 = _query(cg2)
126
+ finally:
127
+ cg2.close()
128
+ os.environ.pop("MDCG_UNIFY_QUERY", None)
129
+ check("K5b 与旁路(无缓存)重算结果一致",
130
+ ids_plain == ids_off, f"cached={ids_off} plain={ids_plain}")
131
+
132
+ # ---- v9 N78:MDCG_CN_GRAMS 漏登(中文 2-gram 召回开关)----
133
+ # 病灶同构:mdcg.py cn_recall_grams 每调用读 env(="0" 即回退旧行为),
134
+ # 改变 expand_query_terms 召回词集,但 _ENV_SWITCHES 没登记它 →
135
+ # 翻转后同 query 命中另一口径缓存(cached=True + 旧口径召回结果)。
136
+ print("== K7~K10 MDCG_CN_GRAMS 入键(v9 N78)==")
137
+ check("K7 MDCG_CN_GRAMS 登记在 _ENV_SWITCHES(清单真源,新增一个登记一个)",
138
+ "MDCG_CN_GRAMS" in hotcache._ENV_SWITCHES,
139
+ str(hotcache._ENV_SWITCHES))
140
+ os.environ.pop("MDCG_CN_GRAMS", None)
141
+ bg = hotcache.env_switch_key()
142
+ os.environ["MDCG_CN_GRAMS"] = "0"
143
+ ag = hotcache.env_switch_key()
144
+ os.environ.pop("MDCG_CN_GRAMS", None)
145
+ check("K8 env_switch_key() 随 MDCG_CN_GRAMS 变化(未设=None 也进键)",
146
+ bg != ag, f"{bg} vs {ag}")
147
+
148
+ rootg = os.path.join(tmp, "cgrams")
149
+ rootg2 = os.path.join(tmp, "cgrams_plain")
150
+ for root in (rootg, rootg2):
151
+ cgx = _mk(root)
152
+ # gold 正文含「抑制/制剂」2-gram 但不含整句——grams 开=经 bigram
153
+ # 召回命中;grams 关=只剩整句召回词 → 词法零命中(口径差可观测)。
154
+ cgx.add("gold", "酪氨酸激酶抑制剂用于白血病靶向治疗,需遵医嘱。",
155
+ layer="knowledge", verification_basis="data")
156
+ cgx.add("d1", "下午去市场买了马,价格比昨天便宜一些。",
157
+ layer="knowledge", verification_basis="data")
158
+ cgx.flush()
159
+ cgx.close()
160
+
161
+ def _q_cn(cg):
162
+ res, meta = cg.search_rrf("抑制剂怎么选", k=5, paths=("lexical",),
163
+ judge=False, record=False)
164
+ return [r[0]["id"] for r in res], meta
165
+
166
+ os.environ["MDCG_HOTCACHE"] = "1"
167
+ os.environ["MDCG_UNIFY_QUERY"] = "0"
168
+ os.environ.pop("MDCG_CN_GRAMS", None) # 缺省=grams 开
169
+ cg3 = _mk(rootg)
170
+ try:
171
+ ids_on, m_on = _q_cn(cg3)
172
+ check("K9a grams 开首查:gold 经 2-gram 召回命中且非缓存",
173
+ "gold" in ids_on and m_on.get("cached") is not True,
174
+ f"{ids_on} cached={m_on.get('cached')}")
175
+ os.environ["MDCG_CN_GRAMS"] = "0" # 运行期翻转(关 grams)
176
+ ids_offg, m_offg = _q_cn(cg3)
177
+ check("K9b 翻转后不命中旧口径缓存(旧实现 cached=True 静默错答)",
178
+ m_offg.get("cached") is not True,
179
+ f"cached={m_offg.get('cached')} ids={ids_offg}")
180
+ check("K9c 翻转后结果与 grams 开不同(无跨口径串味)",
181
+ ids_offg != ids_on, f"on={ids_on} / off={ids_offg}")
182
+ _ids3g, m3g = _q_cn(cg3)
183
+ check("K10 同口径重复查仍命中缓存(缓存未被打废)",
184
+ m3g.get("cached") is True
185
+ and _ids3g == ids_offg, json.dumps(m3g, default=str)[:160])
186
+ finally:
187
+ cg3.close()
188
+ os.environ.pop("MDCG_CN_GRAMS", None)
189
+
190
+ # 旁路对照:无缓存重算 grams=0 的结果必须与 K9c 一致
191
+ os.environ.pop("MDCG_HOTCACHE", None)
192
+ os.environ["MDCG_CN_GRAMS"] = "0"
193
+ cg4 = _mk(rootg2)
194
+ try:
195
+ ids_plain_g, _mg = _q_cn(cg4)
196
+ finally:
197
+ cg4.close()
198
+ os.environ.pop("MDCG_CN_GRAMS", None)
199
+ os.environ.pop("MDCG_UNIFY_QUERY", None)
200
+ check("K10b 与旁路(无缓存)重算结果一致",
201
+ ids_plain_g == ids_offg, f"cached={ids_offg} plain={ids_plain_g}")
202
+ finally:
203
+ for k in ("MDCG_HOTCACHE", "MDCG_SEMANTIC", "MDCG_UNIFY_QUERY",
204
+ "MDCG_CN_GRAMS"):
205
+ os.environ.pop(k, None)
206
+ shutil.rmtree(tmp, ignore_errors=True)
207
+
208
+ print("\n" + "=" * 64)
209
+ print(f"PASS={PASS} FAIL={FAIL}")
210
+ if FAILS:
211
+ print("失败项:" + ";".join(FAILS))
212
+ return 0 if FAIL == 0 else 1
213
+
214
+
215
+ if __name__ == "__main__":
216
+ if hasattr(sys.stdout, "reconfigure"):
217
+ sys.stdout.reconfigure(encoding="utf-8")
218
+ sys.exit(main())