@furongjun1999/dsh-memory 0.5.0 → 0.5.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 (138) hide show
  1. package/README.md +552 -465
  2. package/docs/README.md +1 -0
  3. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  4. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  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_v14.md +191 -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_v15.md +213 -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_v16.md +214 -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_v17.md +199 -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_v2.md +156 -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_v3.md +152 -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_v4.md +128 -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_v5.md +114 -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_v6.md +192 -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_v7.md +187 -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_v8.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_v9.md +203 -0
  27. 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
  28. 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
  29. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  30. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -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/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
  33. 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
  34. package/lib/bridge.d.ts +9 -0
  35. package/lib/bridge.js +35 -0
  36. package/lib/index.js +7 -1
  37. package/lib/lib/roleplay_web.js +530 -443
  38. package/lib/lib/token_store.d.ts +7 -1
  39. package/lib/lib/token_store.js +12 -3
  40. package/md_cg/audit.py +12 -1
  41. package/md_cg/backfill.py +16 -15
  42. package/md_cg/backfill_bucket_zh.py +35 -0
  43. package/md_cg/bench_e2e_judge.py +532 -0
  44. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  45. package/md_cg/bench_e2e_qa.py +256 -0
  46. package/md_cg/branches.py +18 -2
  47. package/md_cg/ccgc.py +3 -2
  48. package/md_cg/chain.py +19 -4
  49. package/md_cg/consolidate.py +7 -6
  50. package/md_cg/crosscheck.py +4 -3
  51. package/md_cg/crypto.py +439 -437
  52. package/md_cg/datapath.py +395 -335
  53. package/md_cg/evidence.py +582 -580
  54. package/md_cg/export.py +3 -1
  55. package/md_cg/forgetting.py +2 -2
  56. package/md_cg/fsutil.py +48 -0
  57. package/md_cg/hotcache.py +255 -238
  58. package/md_cg/interop.py +161 -22
  59. package/md_cg/judgment_manifest.py +177 -0
  60. package/md_cg/links.py +655 -622
  61. package/md_cg/mcp_server.py +280 -29
  62. package/md_cg/mdcg.py +616 -125
  63. package/md_cg/mdcos.py +430 -66
  64. package/md_cg/mreview/govern.py +5 -4
  65. package/md_cg/postings.py +4 -2
  66. package/md_cg/readcache.py +76 -18
  67. package/md_cg/reconcile.py +228 -0
  68. package/md_cg/review_cli.py +170 -0
  69. package/md_cg/routing.py +28 -0
  70. package/md_cg/run_tests.py +211 -0
  71. package/md_cg/scrub.py +862 -852
  72. package/md_cg/security.py +385 -275
  73. package/md_cg/selfreport.py +3 -2
  74. package/md_cg/signer.py +565 -562
  75. package/md_cg/sources.py +3 -2
  76. package/md_cg/stg.py +6 -0
  77. package/md_cg/sustain.py +1168 -1138
  78. package/md_cg/test_access_hints.py +147 -0
  79. package/md_cg/test_branch_discard_tombstone.py +136 -0
  80. package/md_cg/test_branches.py +259 -249
  81. package/md_cg/test_chain_read_isolate.py +168 -0
  82. package/md_cg/test_datapath_device_name.py +203 -0
  83. package/md_cg/test_emit_negtail_cache.py +156 -0
  84. package/md_cg/test_en_pipeline.py +186 -166
  85. package/md_cg/test_govern_directread.py +421 -0
  86. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  87. package/md_cg/test_identity_attribution.py +228 -147
  88. package/md_cg/test_index_durability.py +238 -224
  89. package/md_cg/test_interop.py +4 -2
  90. package/md_cg/test_interop_judgment.py +228 -0
  91. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  92. package/md_cg/test_links_concurrent_write.py +188 -0
  93. package/md_cg/test_merge_upsert.py +168 -0
  94. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  95. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  96. package/md_cg/test_n131_merge_gate.py +205 -0
  97. package/md_cg/test_p1x_ref_root.py +160 -0
  98. package/md_cg/test_p27_docindex.py +774 -765
  99. package/md_cg/test_p2_mcp.py +3 -0
  100. package/md_cg/test_p32_backfill.py +304 -298
  101. package/md_cg/test_p39_verify_flow.py +90 -50
  102. package/md_cg/test_p47_session_view.py +60 -25
  103. package/md_cg/test_propose_tail_index.py +157 -0
  104. package/md_cg/test_read_scope_b27.py +277 -0
  105. package/md_cg/test_readcache_default_on.py +168 -0
  106. package/md_cg/test_readcache_precise_inval.py +270 -0
  107. package/md_cg/test_readcache_prodpath.py +55 -7
  108. package/md_cg/test_reconcile_v0.py +294 -0
  109. package/md_cg/test_retr_s1.py +6 -2
  110. package/md_cg/test_retr_s1b.py +67 -0
  111. package/md_cg/test_retr_s7.py +8 -0
  112. package/md_cg/test_retr_s9_entity_ctx.py +181 -175
  113. package/md_cg/test_retr_score_once.py +208 -0
  114. package/md_cg/test_review_onepass.py +170 -0
  115. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  116. package/md_cg/test_security_audit.py +155 -0
  117. package/md_cg/test_security_audit_b26.py +161 -0
  118. package/md_cg/test_security_audit_v21.py +250 -0
  119. package/md_cg/test_semantic_canonical.py +255 -241
  120. package/md_cg/test_session_isolation.py +168 -0
  121. package/md_cg/test_snapshot_autoclose.py +187 -0
  122. package/md_cg/test_tail_watermark_race.py +208 -0
  123. package/md_cg/test_tenant_env_override_warn.py +139 -0
  124. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  125. package/md_cg/test_v14_fixes.py +415 -397
  126. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  127. package/md_cg/theory.py +276 -273
  128. package/md_cg/tokens.py +734 -677
  129. package/md_cg/units.py +3 -2
  130. package/md_cg/vision_evidence.py +4 -3
  131. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  132. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  133. package/md_cg/writepipe.py +554 -550
  134. package/package.json +6 -2
  135. package/src/bridge.ts +434 -401
  136. package/src/index.ts +526 -518
  137. package/src/lib/roleplay_web.ts +1019 -932
  138. package/src/lib/token_store.ts +202 -192
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """生产 search T2/T3 重打分守卫(v7 N51 留档复测·修复,缺陷迭代第 14 轮)。
4
+
5
+ 缺陷形态(缺陷报告/v9.md:111 留档,600 节点临时库实测逐位复现):
6
+ 生产 MdCGOS.search 的 try_stage(mdcos.py)无 scored 形参——T2/T3 均为
7
+ 「打分截断后又对 picked 重打」:cut_by_relevance(hits, self._score(hits))
8
+ 之后 try_stage(hits) 内部再 _score 一遍;T3 再对 picked 重打。实测无交集
9
+ 查询 _score 恰 4 调、per-call docs=[0,0,600,500]、共打分 1100 文档——
10
+ 500/1100≈45% 打分工作量冗余(GLOBAL_CAP=500)。基类 try_stage 早有
11
+ scored=None 形参(mdcg.py P2-4,批次 31,reach 分支已用)未回移生产覆写。
12
+
13
+ 修复口径:生产 try_stage 照基类 P2-4 模板补 scored=None 形参;T2 hits 腿
14
+ (截断打分结果经 doc_key 键表回填)与 T3 picked 腿、reach hits_r 腿一并
15
+ 透传——同一 _score 纯函数(单次 search 内 docs/q/qb/pool_cfg 均不变),
16
+ 透传不改变任何分值与输出。
17
+
18
+ 本守卫(调用计数/复杂度断言形态,非时间阈值):
19
+ T3 段(无交集查询,600 节点):_score 恰 2 调、per-call [0,600]、共 600
20
+ 文档(旧实现 4 调 [0,0,600,500]=1100 → 红);tier/scanned/结果规模
21
+ 不变;
22
+ T2 段(有交集查询):_score 恰 1 调、共 600 文档(旧实现 2 调
23
+ [600,500]=1100 → 红);
24
+ 纯度段:对计数包装记录的每笔 (docs, scored) 复算 _score 逐位相等
25
+ (透传值 ≡ 现算值——不改结果的直接证据);
26
+ 确定性段:同查询连跑两轮结果 id 序与分值逐位一致。
27
+
28
+ 运行:python -m md_cg.test_retr_score_once
29
+ """
30
+ import os
31
+ import shutil
32
+ import sys
33
+ import tempfile
34
+
35
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
36
+
37
+ from md_cg.mdcos import MdCGOS # noqa: E402
38
+
39
+ PASS = 0
40
+ FAIL = 0
41
+ FAILS = []
42
+
43
+ N = 600
44
+ QUERY_MISS = "完全无交集查询词项甲乙丙" # LIKE 零命中 → T3 全量兜底
45
+ QUERY_HIT = "蜂群调度" # 有交集 → T2 命中
46
+
47
+
48
+ def check(name, cond, detail=""):
49
+ global PASS, FAIL
50
+ if cond:
51
+ PASS += 1
52
+ print(f" OK {name}")
53
+ else:
54
+ FAIL += 1
55
+ FAILS.append(name)
56
+ print(f" FAIL {name} {detail}")
57
+
58
+
59
+ def build_lib(root):
60
+ cg = MdCGOS(root)
61
+ for i in range(N):
62
+ cg.add("mem_%04d" % i,
63
+ "# 功能名:打分样本 %d\n# 正文:%s 节点 %d" % (i, QUERY_HIT, i),
64
+ layer="knowledge")
65
+ cg.flush()
66
+ return cg
67
+
68
+
69
+ def count_score(cg, fn):
70
+ """包装实例 _score:记录每笔 (docs 数, scored);另存 docs 引用于旁表。"""
71
+ calls = [] # [docs_n]
72
+ pairs = [] # [(docs, scored)]
73
+ real = MdCGOS._score
74
+
75
+ def _spy(self, docs, *a, **kw):
76
+ out = real(self, docs, *a, **kw)
77
+ calls.append(len(docs))
78
+ # 记录时拷贝:_emit 会对 scored 列表原地排序(记录引用会被事后变更)
79
+ pairs.append((list(docs), list(out)))
80
+ return out
81
+
82
+ cg._score = _spy.__get__(cg, MdCGOS)
83
+ try:
84
+ return fn(), calls, pairs
85
+ finally:
86
+ del cg._score # 还原类属性查找
87
+
88
+
89
+ def main():
90
+ os.environ.pop("MDCG_READ_CACHE", None)
91
+ root = tempfile.mkdtemp(prefix="mdcg_score_once_")
92
+ cg = None
93
+ try:
94
+ cg = build_lib(root)
95
+ cg.search(QUERY_MISS) # 预热(读缓存装载,非度量面)
96
+
97
+ print("== T3 无交集查询:打分恰一次/文档 ==")
98
+ for rep in range(2):
99
+ (res, meta), calls, _ = count_score(
100
+ cg, lambda: cg.search(QUERY_MISS))
101
+ check("T3a rep%d _score 调用数 == 2(旧 4)" % rep,
102
+ len(calls) == 2, f"calls={calls}")
103
+ check("T3b rep%d per-call docs == [0, 600](旧 [0,0,600,500])" % rep,
104
+ calls == [0, 600], f"calls={calls}")
105
+ check("T3c rep%d 共打分 600 文档(旧 1100,省 45)" % rep,
106
+ sum(calls) == 600, f"total={sum(calls)}")
107
+ check("T3d rep%d tier/scanned/结果规模不变" % rep,
108
+ meta.get("tier") == "T3_global_scan"
109
+ and meta.get("scanned") == N and len(res) == 20,
110
+ f"tier={meta.get('tier')} scanned={meta.get('scanned')} "
111
+ f"n={len(res)}")
112
+
113
+ print("== T2 有交集查询:打分恰一次/文档 ==")
114
+ for rep in range(2):
115
+ (res, meta), calls, _ = count_score(
116
+ cg, lambda: cg.search(QUERY_HIT))
117
+ check("T2a rep%d _score 调用数 == 1(旧 2)" % rep,
118
+ len(calls) == 1, f"calls={calls}")
119
+ check("T2b rep%d 共打分 600 文档(旧 1100)" % rep,
120
+ calls == [600], f"calls={calls}")
121
+ check("T2c rep%d tier 不变(T2_global_like)" % rep,
122
+ meta.get("tier") == "T2_global_like",
123
+ f"tier={meta.get('tier')}")
124
+
125
+ print("== 纯度:透传 scored ≡ 现算 _score(不改结果的直接证据)==")
126
+ (res, meta), calls, pairs = count_score(
127
+ cg, lambda: cg.search(QUERY_MISS))
128
+ # 与 search 同口径重建 q/qb(mdcos.py:751-773 的打分输入)
129
+ from md_cg.mdcg import bigrams, normalize_en, en_zh_bigrams
130
+ from md_cg.semantic.unify import unify_query
131
+ q_p = unify_query(QUERY_MISS)
132
+ qb_p = bigrams(normalize_en(q_p)) | en_zh_bigrams(q_p)
133
+ pure = True
134
+ for i, (docs, scored) in enumerate(pairs):
135
+ again = MdCGOS._score(cg, docs, q_p, qb_p, None)
136
+ if again != scored:
137
+ pure = False
138
+ print(f" 第{i}笔复算不一致: {len(docs)} docs")
139
+ check("P1a 每笔 (docs→scored) 复算逐位相等(_score 纯函数)", pure)
140
+
141
+ print("== 确定性:同查询两轮结果逐位一致 ==")
142
+ r1, m1 = cg.search(QUERY_MISS)
143
+ r2, m2 = cg.search(QUERY_MISS)
144
+ ids1 = [(x[0].get("id"), round(x[1], 12)) for x in r1]
145
+ ids2 = [(x[0].get("id"), round(x[1], 12)) for x in r2]
146
+ check("D1a 两轮结果 id 序与分值逐位一致", ids1 == ids2,
147
+ f"n={len(ids1)}/{len(ids2)}")
148
+ rh1, _ = cg.search(QUERY_HIT)
149
+ rh2, _ = cg.search(QUERY_HIT)
150
+ check("D1b T2 路径两轮结果逐位一致",
151
+ [(x[0].get("id"), round(x[1], 12)) for x in rh1]
152
+ == [(x[0].get("id"), round(x[1], 12)) for x in rh2])
153
+
154
+ print("== reach 腿(MDCG_REACH=1):P2-4 模板形态修复 ==")
155
+ # 相邻缺陷(本轮 stash 取证):基类 reach 分支旧 _smap 模板对 scored
156
+ # 的 card dict 调 pooling.doc_key(形参须 (entry,fm,content) 三元组)
157
+ # → MDCG_REACH=1 + 命中集非空即 KeyError: 1(HEAD 原生在位)。
158
+ from md_cg.mdcg import MdCG
159
+ os.environ["MDCG_REACH"] = "1"
160
+ try:
161
+ root2 = tempfile.mkdtemp(prefix="mdcg_score_reach_")
162
+ cg2 = MdCGOS(root2)
163
+ for i in range(30):
164
+ cg2.add("rm_%03d" % i,
165
+ "# 功能名:reach 样本\n# 正文:%s 节点 %d" % (QUERY_HIT, i),
166
+ layer="knowledge")
167
+ cg2.flush()
168
+ try:
169
+ (res_r, meta_r), calls_r, _ = count_score(
170
+ cg2, lambda: cg2.search(QUERY_HIT))
171
+ check("R1a 生产 reach 腿不再 KeyError(HEAD 实测崩)且有结果",
172
+ len(res_r) >= 1, f"n={len(res_r)} tier={meta_r.get('tier')}")
173
+ check("R1b 生产 reach 腿打分 ≤ 2 调(透传无重打)",
174
+ len(calls_r) <= 2, f"calls={calls_r}")
175
+ finally:
176
+ cg2.close()
177
+ shutil.rmtree(root2, ignore_errors=True)
178
+ root3 = tempfile.mkdtemp(prefix="mdcg_score_reachb_")
179
+ cg3 = MdCG(root3)
180
+ for i in range(30):
181
+ cg3.add("bm_%03d" % i,
182
+ "# 功能名:reach 样本\n# 正文:%s 节点 %d" % (QUERY_HIT, i),
183
+ layer="knowledge")
184
+ cg3.flush()
185
+ try:
186
+ res_b, meta_b = cg3.search(QUERY_HIT)
187
+ check("R2a 基类 reach 分支不再 KeyError(stash 取证 HEAD 崩)",
188
+ len(res_b) >= 1, f"n={len(res_b)} tier={meta_b.get('tier')}")
189
+ finally:
190
+ cg3.close()
191
+ shutil.rmtree(root3, ignore_errors=True)
192
+ finally:
193
+ os.environ.pop("MDCG_REACH", None)
194
+ cg.close()
195
+ finally:
196
+ os.environ.pop("MDCG_READ_CACHE", None)
197
+ shutil.rmtree(root, ignore_errors=True)
198
+
199
+ print("\n" + "=" * 60)
200
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
201
+ if FAILS:
202
+ for f in FAILS:
203
+ print(f" - {f}")
204
+ return 0 if FAIL == 0 else 1
205
+
206
+
207
+ if __name__ == "__main__":
208
+ sys.exit(main())
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """review 治理面单次调用复用一次解析守卫(issue:同一 jsonl 单调用内重复全量读)。
4
+
5
+ 缺陷形态(缺陷报告实测 + 当前代码复核):review_stats 链路每次全量读
6
+ decisions 1 遍(by_decision 统计)+ review_list 全量读 inbox 1 遍;
7
+ review_decide 线性扫 inbox 找 pid + _cascade_dedup 再全量读 inbox 一遍;
8
+ verify_review_record 无 pid 索引线性扫全量 decisions 找单条——解析量随
9
+ M/D 线性放大(M=D=4000 实测 review_stats 15.97ms / decide 21.31ms /
10
+ verify 28.92ms)。
11
+
12
+ 本守卫断言(调用计数/对账形态,非时间阈值):
13
+ R1 稳态 review_stats 零全量读:预热装载后 read_jsonl 调用 == 0
14
+ (旧代码每次 decisions+inbox 各 1 次 → 红);
15
+ R2 稳态 review_decide(reject) 零全量读:同上(旧代码 inbox 2 次 → 红);
16
+ R3 稳态 verify_review_record 零全量读:同上(旧代码 decisions 1 次 → 红);
17
+ R4 增量不漏账:外部(他进程形态)append 新裁决/新提案后,统计口径与
18
+ 队列视图立即可见;
19
+ R5 语义对账:review_stats 的 records/by_decision/pending/closed 与
20
+ 手工全量直读的独立口径逐项一致(缓存不改变结果,只改变速度)。
21
+ """
22
+ import os
23
+ import shutil
24
+ import sys
25
+ import tempfile
26
+
27
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
+
29
+ import md_cg.mdcos as mdcos_mod # noqa: E402
30
+ from md_cg.fsutil import append_jsonl, read_jsonl as fs_read_jsonl # noqa: E402
31
+ from md_cg.mdcos import MdCGOS, _sig # noqa: E402
32
+
33
+ PASS = 0
34
+ FAIL = 0
35
+ FAILS = []
36
+
37
+
38
+ def check(name, cond, detail=""):
39
+ global PASS, FAIL
40
+ if cond:
41
+ PASS += 1
42
+ print(f" OK {name}")
43
+ else:
44
+ FAIL += 1
45
+ FAILS.append(name)
46
+ print(f" FAIL {name} {detail}")
47
+
48
+
49
+ def count_full_reads(fn):
50
+ n = 0
51
+ real = mdcos_mod.read_jsonl
52
+
53
+ def _spy(path, *a, **kw):
54
+ nonlocal n
55
+ n += 1
56
+ return real(path, *a, **kw)
57
+
58
+ mdcos_mod.read_jsonl = _spy
59
+ try:
60
+ return fn(), n
61
+ finally:
62
+ mdcos_mod.read_jsonl = real
63
+
64
+
65
+ def build(root, m=8, d=5):
66
+ cg = MdCGOS(root)
67
+ for i in range(m):
68
+ c = "存量提案 %d 蜂群调度 %d" % (i, i)
69
+ append_jsonl(cg.inbox_log,
70
+ {"t": 1.0, "pid": "prop_g%03d" % i, "id": "n%03d" % i,
71
+ "content": c, "layer": "knowledge", "tags": [],
72
+ "condition_space": {}, "verify": {},
73
+ "payload_hash": _sig(c), "actor": "g"})
74
+ for j in range(d):
75
+ append_jsonl(cg.decisions_log,
76
+ {"t": 1.0, "pid": "prop_g%03d" % j, "decision": "accept",
77
+ "status": "accepted", "round": 1, "actor": "g"})
78
+ return cg
79
+
80
+
81
+ def main():
82
+ root = tempfile.mkdtemp(prefix="mdcg_review_guard_")
83
+ try:
84
+ cg = build(root)
85
+ cg.review_stats() # 预热装载(不计数)
86
+
87
+ # R1 稳态 review_stats 零全量读
88
+ print("== R1 稳态 review_stats 零全量读 ==")
89
+ st1, n1 = count_full_reads(lambda: cg.review_stats())
90
+ check("R1a 预热后 review_stats 全量 read_jsonl==0(旧每次 2 次)",
91
+ n1 == 0, f"full_reads={n1}")
92
+ check("R1b 统计口径 sane(records≥d、pending≥1)",
93
+ st1["records"] >= 5 and st1["pending"] >= 1, str(st1)[:100])
94
+
95
+ # R2 稳态 review_decide(reject) 零全量读
96
+ print("== R2 稳态 review_decide(reject) 零全量读 ==")
97
+ def _decide():
98
+ _decide.i += 1
99
+ pid = cg.propose("c1", "纯 pending 候选 星云编译 %d" % _decide.i)
100
+ return cg.review_decide(pid, "reject", reason="g")
101
+ _decide.i = 0
102
+ _decide() # 预热:装载 phash 索引 +
103
+ out2, n2 = count_full_reads(_decide) # decisions 创建后的 pid 状态缓存
104
+ check("R2a decide(reject) 全量 read_jsonl==0(旧 inbox 2 次;"
105
+ "首装装载属预期不在此窗)",
106
+ n2 == 0, f"full_reads={n2}")
107
+ check("R2b 裁决 ok", out2.get("ok"), str(out2)[:80])
108
+
109
+ # R3 稳态 verify_review_record 零全量读
110
+ print("== R3 稳态 verify_review_record 零全量读 ==")
111
+ def _verify():
112
+ _verify.i += 1
113
+ pid = cg.propose("c2", "复核候选 信任聚合 %d" % _verify.i)
114
+ out = cg.review_decide(pid, "accept", reason="g")
115
+ assert out.get("ok"), out
116
+ return cg.verify_review_record(out["record_node_id"])
117
+ _verify.i = 0
118
+ _verify() # 预热同上
119
+ ok3, n3 = count_full_reads(_verify)
120
+ check("R3a verify 链路全量 read_jsonl==0(旧 decisions 1 次扫全量找单条)",
121
+ n3 == 0, f"full_reads={n3}")
122
+ check("R3b 复核通过", ok3.get("ok") is True, str(ok3)[:80])
123
+
124
+ # R4 增量不漏账:他进程形态 append
125
+ print("== R4 跨进程 append 增量并入 ==")
126
+ append_jsonl(cg.inbox_log,
127
+ {"t": 2.0, "pid": "prop_ext_x", "id": "nx",
128
+ "content": "外部新提案 水位信箱", "layer": "knowledge",
129
+ "tags": [], "condition_space": {}, "verify": {},
130
+ "payload_hash": _sig("外部新提案 水位信箱"),
131
+ "actor": "ext"})
132
+ append_jsonl(cg.decisions_log,
133
+ {"t": 2.0, "pid": "prop_ext_x", "decision": "noop",
134
+ "status": "noop", "round": 1, "actor": "ext"})
135
+ st4 = cg.review_stats()
136
+ check("R4a 外部新裁决进统计(by_decision.noop=1)",
137
+ st4["by_decision"].get("noop") == 1, str(st4["by_decision"]))
138
+ pend_ids = [r.get("pid") for r in cg.review_list()]
139
+ check("R4b 外部新裁决立即可见(prop_ext_x 已关闭,不在 pending)",
140
+ "prop_ext_x" not in pend_ids, f"pending={pend_ids}")
141
+
142
+ # R5 语义对账:与独立全量直读口径逐项一致
143
+ print("== R5 统计口径独立对账 ==")
144
+ by = {}
145
+ for r in fs_read_jsonl(cg.decisions_log):
146
+ d_ = str(r.get("decision") or "").strip() or "unknown"
147
+ by[d_] = by.get(d_, 0) + 1
148
+ n_pid = len({r.get("pid") for r in fs_read_jsonl(cg.decisions_log)
149
+ if r.get("pid")})
150
+ st5 = cg.review_stats()
151
+ check("R5a records/by_decision 与独立直读一致",
152
+ st5["records"] == sum(by.values())
153
+ and st5["by_decision"] == by,
154
+ f"got={st5['by_decision']} want={by}")
155
+ check("R5b proposals 与独立直读 pid 数一致",
156
+ st5["proposals"] == n_pid,
157
+ f"got={st5['proposals']} want={n_pid}")
158
+ finally:
159
+ shutil.rmtree(root, ignore_errors=True)
160
+
161
+ print("\n" + "=" * 60)
162
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
163
+ if FAILS:
164
+ for f in FAILS:
165
+ print(f" - {f}")
166
+ return 0 if FAIL == 0 else 1
167
+
168
+
169
+ if __name__ == "__main__":
170
+ sys.exit(main())
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """search_rrf 图路种子读缓存守卫(v9:92 / v13 留档复测·修复,第 14 轮)。
4
+
5
+ 缺陷形态(缺陷报告实测,40 带 tags/edges 节点临时库,逐位复现):
6
+ _path_graph 种子腿 mdcos.py `node = self.get(n["id"])` 在边循环前执行——
7
+ get 裸 open+nodefile.loads(mdcg.py get 不在 readcache 包装面,install 只包
8
+ cg._read),search_rrf 默认 paths 含 graph → 每查询固定 ≤5 次盘读
9
+ (Q1 loads=45=词法 40+种子 5;Q2 增量恰 5、get 恒 5/查;graph 零产出也
10
+ 在边循环前发生)。边目标(:1017)早已走 self._read 同形。
11
+
12
+ 修复口径:种子读改走 self._read(by_id[...])(读缓存覆盖,Q2 增量归零);
13
+ 种子 fm.id≠文件名等 by_id 不含形态回落 get 保旧口径(冷路径,正确性优先)。
14
+ 种子来自 lexical(entries/_candidates 派生,MdCGSecure._candidates 已过
15
+ _readable)——绕过 get 的读隔离门无损失;图路扩展产物本就由
16
+ MdCGSecure.search_rrf 终态二次过滤兜底。
17
+
18
+ 本守卫(调用计数断言形态,非时间阈值):
19
+ S1 热缓存后同查询 loads 增量 == 0(旧实现恒 5 → 红);
20
+ S2 热缓存后同查询 get 调用 == 0(旧实现恒 5 → 红);
21
+ S3 图路功能在位:词法零命中的「幽灵」节点经种子边一跳进入结果
22
+ (graph 路不是被静默关掉);
23
+ S4 两轮查询结果 ids 逐位一致(确定性);meta.paths 含四路计数;
24
+ S5 开/关读缓存(MDCG_READ_CACHE=1 vs 0)同库同查询 ids 逐位一致
25
+ (缓存只改速度不改结果)。
26
+
27
+ 运行:python -m md_cg.test_rrf_graph_seed_cache
28
+ """
29
+ import os
30
+ import shutil
31
+ import sys
32
+ import tempfile
33
+
34
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
35
+
36
+ from md_cg import nodefile # noqa: E402
37
+ from md_cg.mdcos import MdCGOS # noqa: E402
38
+
39
+ PASS = 0
40
+ FAIL = 0
41
+ FAILS = []
42
+
43
+ QUERY = "蜂群调度"
44
+
45
+
46
+ def check(name, cond, detail=""):
47
+ global PASS, FAIL
48
+ if cond:
49
+ PASS += 1
50
+ print(f" OK {name}")
51
+ else:
52
+ FAIL += 1
53
+ FAILS.append(name)
54
+ print(f" FAIL {name} {detail}")
55
+
56
+
57
+ def build_lib(root, n=40):
58
+ cg = MdCGOS(root)
59
+ for i in range(n):
60
+ edges = []
61
+ if i % 4 == 0: # 部分节点带边(指向池内目标)
62
+ edges = [{"target": "mem_%03d" % ((i + 1) % n),
63
+ "relation_type": "related_to"}]
64
+ if i == 0:
65
+ # 幽灵目标:词法零命中(正文无查询词),只经种子边一跳可达
66
+ edges.append({"target": "ghost_x", "relation_type": "related_to"})
67
+ cg.add("mem_%03d" % i,
68
+ "# 功能名:图路样本 %d\n# 正文:%s 节点 %d" % (i, QUERY, i),
69
+ layer="knowledge",
70
+ tags=["tag_%d" % (i % 5)] if i % 3 == 0 else None,
71
+ edges=edges)
72
+ cg.add("ghost_x", "# 功能名:幽灵目标\n# 正文:完全不相关的孤岛内容",
73
+ layer="knowledge")
74
+ cg.flush()
75
+ return cg
76
+
77
+
78
+ def counters(cg, fn):
79
+ """同时计数 nodefile.loads 与实例 get。"""
80
+ st = {"loads": 0, "gets": 0}
81
+ real_loads = nodefile.loads
82
+ real_get = MdCGOS.get
83
+
84
+ def _spy_loads(*a, **kw):
85
+ st["loads"] += 1
86
+ return real_loads(*a, **kw)
87
+
88
+ def _spy_get(self, node_id):
89
+ st["gets"] += 1
90
+ return real_get(self, node_id)
91
+
92
+ nodefile.loads = _spy_loads
93
+ cg.get = _spy_get.__get__(cg, MdCGOS)
94
+ try:
95
+ return fn(), st
96
+ finally:
97
+ nodefile.loads = real_loads
98
+ del cg.get
99
+
100
+
101
+ def main():
102
+ root = tempfile.mkdtemp(prefix="mdcg_rrf_seed_")
103
+ cg = None
104
+ try:
105
+ os.environ.pop("MDCG_READ_CACHE", None)
106
+ cg = build_lib(root)
107
+ (r1, m1), c1 = counters(cg, lambda: cg.search_rrf(QUERY))
108
+ ids1 = [x[0].get("id") for x in r1]
109
+ check("S0a 首查正常(40+1 节点池,词法+图路融合)",
110
+ len(r1) >= 1 and c1["loads"] > 0,
111
+ f"n={len(r1)} loads={c1['loads']}")
112
+
113
+ (r2, m2), c2 = counters(cg, lambda: cg.search_rrf(QUERY))
114
+ ids2 = [x[0].get("id") for x in r2]
115
+ check("S1a 热缓存后同查询 loads 增量 == 0(旧实现恒 5)",
116
+ c2["loads"] == 0, f"loads={c2['loads']}")
117
+ check("S2a 热缓存后同查询 get 调用 == 0(旧实现恒 5,种子改走 _read)",
118
+ c2["gets"] == 0, f"gets={c2['gets']}")
119
+ check("S3a 图路功能在位:词法零命中的 ghost 经种子边一跳进入结果",
120
+ "ghost_x" in ids1, f"ghost in={('ghost_x' in ids1)}")
121
+ paths_meta = (m2.get("paths") or {})
122
+ check("S3b meta.paths 四路计数在位(graph 未被静默关掉)",
123
+ {"lexical", "graph"} <= set(paths_meta),
124
+ f"paths={paths_meta}")
125
+ check("S4a 两轮查询结果 ids 逐位一致", ids1 == ids2,
126
+ f"n={len(ids1)}/{len(ids2)}")
127
+
128
+ # S5 开/关读缓存结果一致(新实例同库同查询)
129
+ r_on = ids2
130
+ os.environ["MDCG_READ_CACHE"] = "0"
131
+ try:
132
+ cg0 = MdCGOS(root)
133
+ r_off, _ = cg0.search_rrf(QUERY)
134
+ cg0.close()
135
+ check("S5a 读缓存开/关同库同查询 ids 逐位一致",
136
+ [x[0].get("id") for x in r_off] == r_on,
137
+ f"off={[x[0].get('id') for x in r_off][:5]}")
138
+ finally:
139
+ os.environ.pop("MDCG_READ_CACHE", None)
140
+ cg.close()
141
+ finally:
142
+ os.environ.pop("MDCG_READ_CACHE", None)
143
+ shutil.rmtree(root, ignore_errors=True)
144
+
145
+ print("\n" + "=" * 60)
146
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
147
+ if FAILS:
148
+ for f in FAILS:
149
+ print(f" - {f}")
150
+ return 0 if FAIL == 0 else 1
151
+
152
+
153
+ if __name__ == "__main__":
154
+ sys.exit(main())
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """安全审计修复守卫(批次 24,外部审查报告 P0-1/P1-1/P2-1/P1-8)。
4
+
5
+ P0-1:node_id 是模型可控输入、直接拼落盘路径——`..` 穿越出 root、
6
+ Windows 绝对路径(os.path.join 丢弃前缀)= 任意 .md 覆盖。修复 = 白名单
7
+ + realpath 纵深闸。守卫断言(能红修复前实现):
8
+ - 穿越/绝对路径/超长/空 id 必 ValueError;
9
+ - 合法 id(存量形态)照常写入。
10
+ P1-1:mdcg_* 工具此前完全绕过 ops_allow 白名单——只读令牌可经
11
+ mdcg_remember 写入。守卫断言(能红修复前实现):
12
+ - ops_allow=["read"] 的 principal 调 mdcg_remember 必 AccessDenied;
13
+ - ops_allow=None(不限)令牌照常放行(兼容性);
14
+ - 读工具在 read 令牌下放行。
15
+ P1-8:tarfile filter="data"(静态断言 + import 回归)。
16
+ """
17
+ import os
18
+ import sys
19
+ import tempfile
20
+
21
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
+
23
+ PASS = 0
24
+ FAIL = 0
25
+ FAILS = []
26
+
27
+
28
+ def check(name, cond, detail=""):
29
+ global PASS, FAIL
30
+ if cond:
31
+ PASS += 1
32
+ print(f" OK {name}")
33
+ else:
34
+ FAIL += 1
35
+ FAILS.append(name)
36
+ print(f" FAIL {name} {detail}")
37
+
38
+
39
+ def main():
40
+ from md_cg.mdcg import _NODE_ID_RE
41
+ from md_cg.mdcos import MdCGSecure
42
+ from md_cg.mcp_server import _dispatch, _MDCG_OP_REQUIRE
43
+
44
+ root = tempfile.mkdtemp(prefix="mdcg_secaudit_")
45
+ # 生产形态(MdCGSecure——mdcg_search 分支走 OS 层签名,基类 MdCG 无
46
+ # roles 参数;P1-1c 因此必须用安全实例而非基类)
47
+ cg = MdCGSecure(root)
48
+
49
+ print("== P0-1 node_id 白名单 + realpath 纵深闸 ==")
50
+ bad_ids = [
51
+ (".. 穿越", "../evil"),
52
+ ("多层穿越", "sub/../../../evil"),
53
+ ("Windows 绝对路径", "C:/Users/test/evil"),
54
+ ("类 Unix 绝对路径", "/etc/evil"),
55
+ ("隐式 ..", "a/../b..c"),
56
+ ("空 id", ""),
57
+ ("超长 id", "a" * 129),
58
+ ]
59
+ for label, nid in bad_ids:
60
+ try:
61
+ cg.add(nid, "# 功能名:x\n# 正文:y", layer="knowledge")
62
+ check(f"P0-1 拒绝 {label}", False, "未抛异常且写入成功")
63
+ except ValueError:
64
+ check(f"P0-1 拒绝 {label}", True)
65
+ except Exception as e: # noqa: BLE001
66
+ check(f"P0-1 拒绝 {label}", False, f"异常类型错误: {type(e).__name__}: {e}")
67
+
68
+ ok_ids = ["mem_1790156428627", "n00001", "mde_abcdef123456",
69
+ "uuid-4f2b-9c.a-d", "a" * 128]
70
+ ok = True
71
+ for i, nid in enumerate(ok_ids):
72
+ try:
73
+ cg.add(nid, f"# 功能名:合法 {i}\n# 正文:白名单内存活样本 {i}",
74
+ layer="knowledge")
75
+ except Exception as e: # noqa: BLE001
76
+ ok = False
77
+ check(f"P0-1 合法 id {nid[:16]}… 照常写入", False,
78
+ f"{type(e).__name__}: {e}")
79
+ check("P0-1 合法 id(存量 5 形态)照常写入", ok and _NODE_ID_RE.match("mem_1"))
80
+
81
+ print("== P1-1 mdcg_* op 闸(能红无闸实现)==")
82
+ from md_cg.security import Principal
83
+
84
+ def principal_with(ops):
85
+ return Principal(actor="auditor", clearance="secret",
86
+ can_write=True, role="recorder", auth_mode="test",
87
+ ops_allow=ops)
88
+
89
+ # 只读令牌:mdcg_remember 必拒(能红:修复前无闸直接写入)
90
+ cg.principal = principal_with(["read"])
91
+ try:
92
+ _dispatch(cg, "mdcg_remember", {"content": "# 功能名:越权\n# 正文:x",
93
+ "layer": "contextual"})
94
+ check("P1-1a 只读令牌 mdcg_remember 必拒", False, "未抛 AccessDenied")
95
+ except Exception as e: # noqa: BLE001
96
+ check("P1-1a 只读令牌 mdcg_remember 必拒(AccessDenied)",
97
+ type(e).__name__ == "AccessDenied",
98
+ f"{type(e).__name__}: {e}")
99
+
100
+ # ops_allow=None(不限)令牌:放行(兼容性——存量直构形态零影响;
101
+ # _in_scope(None)=True 是「未声明=不限制」的既有语义)
102
+ saved = cg.principal
103
+ cg.principal = principal_with(None)
104
+ try:
105
+ res = _dispatch(cg, "mdcg_remember",
106
+ {"content": "# 功能名:兼容\n# 正文:不限令牌照常",
107
+ "layer": "contextual"})
108
+ check("P1-1b ops_allow=None(不限)令牌照常放行", True)
109
+ except Exception as e: # noqa: BLE001
110
+ check("P1-1b ops_allow=None(不限)令牌照常放行", False,
111
+ f"{type(e).__name__}: {e}")
112
+ finally:
113
+ cg.principal = saved
114
+
115
+ # 读令牌:读工具放行
116
+ cg.principal = principal_with(["read"])
117
+ try:
118
+ res = _dispatch(cg, "mdcg_search", {"query": "星云"})
119
+ check("P1-1c 读令牌读工具放行", not isinstance(res, dict) or not res.get("error"),
120
+ str(res)[:120])
121
+ except Exception as e: # noqa: BLE001
122
+ check("P1-1c 读令牌读工具放行", False, f"{type(e).__name__}: {e}")
123
+ cg.principal = None
124
+
125
+ # 映射完备性:mdcg_* 全部注册工具都被映射覆盖(防未来新增工具漏闸)
126
+ from md_cg import mcp_server as ms
127
+ import re as _re
128
+ src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
129
+ "mcp_server.py"), encoding="utf-8").read()
130
+ registered = set(_re.findall(r'"name": "(mdcg_[a-z_]+)"', src))
131
+ uncovered = sorted(registered - set(_MDCG_OP_REQUIRE) - {"mdcg_service_info"})
132
+ check("P1-1d mdcg_* 注册工具全覆盖(除自描述 service_info)",
133
+ not uncovered, f"uncovered={uncovered}")
134
+
135
+ print("== P1-8 tarfile filter ==")
136
+ src_cf = open(os.path.join(os.path.dirname(os.path.dirname(
137
+ os.path.abspath(__file__))), "scripts", "criteria_fingerprint.py"),
138
+ encoding="utf-8").read()
139
+ check("P1-8 extractall 显式 filter=\"data\"", 'extractall(tmp, filter="data")'
140
+ in src_cf)
141
+
142
+ # 清理
143
+ import shutil
144
+ shutil.rmtree(root, ignore_errors=True)
145
+
146
+ print("\n" + "=" * 60)
147
+ print(f"结果:PASS {PASS} / FAIL {FAIL}")
148
+ if FAILS:
149
+ for f in FAILS:
150
+ print(f" - {f}")
151
+ return 0 if FAIL == 0 else 1
152
+
153
+
154
+ if __name__ == "__main__":
155
+ sys.exit(main())