@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
@@ -8,6 +8,8 @@
8
8
  - S1b 开:query 侧推断出候选桶 → 候选收敛(out < in)、**orphan/无桶恒留兜底**、不引入任何新节点
9
9
  - 命中不到任何桶 → no_key_match 且不收敛(回退)
10
10
  - topk / min_sim 可配(topk=0 关闭;min_sim=1.0 只认完全同名键)
11
+ - issue #33:中文 query × 英文桶键经中文别名收敛(别名=节点自身 tags/正文中文词,
12
+ 零翻译依赖);索引重建后口径一致;无别名时审计给 cross_lang_no_match(≠no_key_match)
11
13
  - 幂等:重复检索 / 索引重建后同口径
12
14
  """
13
15
  import os
@@ -177,6 +179,71 @@ def main():
177
179
  check("min_sim=0.34:部分匹配(感知→感知系统 0.5)入选",
178
180
  g6.get("keys") == [b_sense], str(g6))
179
181
 
182
+ # ---- 6b) issue #33:中文 query × 英文桶键(守卫场景按触发条件设计:
183
+ # 既有用例全是同语言对,对跨语言天然免疫——这正是缺陷长期未被发现的根因)
184
+ root_x = tempfile.mkdtemp(prefix="retr_s1bx_")
185
+ cgx = MdCG(root_x)
186
+ cgx.add("sw1", "蜂群调度 依赖门禁 worker 领取", "knowledge",
187
+ tags=["domain:swarm-dispatch"])
188
+ cgx.add("tk1", "令牌校验 签发 载荷", "knowledge",
189
+ tags=["domain:token-verify"])
190
+ cgx.flush()
191
+ e_sw = cgx.index["nodes"]["sw1"]
192
+ check("英文桶键节点落中文别名(自身正文中文词,零翻译)",
193
+ "蜂群调度" in (e_sw.get("bucket_zh") or []),
194
+ str(e_sw.get("bucket_zh")))
195
+ check("中文桶键节点不带别名(无跨语言问题)",
196
+ not (cg.index["nodes"].get("s1") or {}).get("bucket_zh"))
197
+ rx, mx = cgx.search("蜂群调度 依赖", k=10, judge=False, record=False)
198
+ gx = (mx.get("gates") or {}).get("s1b") or {}
199
+ check("issue#33:中文 query × 英文桶键经别名收敛",
200
+ gx.get("keys") == [e_sw["bucket"]], str(gx))
201
+ check("issue#33:收敛后剔除异桶(token 桶不入围)",
202
+ "tk1" not in _ids(rx), str(_ids(rx)))
203
+ cgx.rebuild_index()
204
+ rx2, mx2 = cgx.search("蜂群调度 依赖", k=10, judge=False, record=False)
205
+ gx2 = (mx2.get("gates") or {}).get("s1b") or {}
206
+ check("issue#33:索引重建后别名口径一致(fm 派生)",
207
+ gx2.get("keys") == gx.get("keys"), str(gx2))
208
+ # 止血审计:英文键 + 无中文面(正文也无中文)→ cross_lang_no_match ≠ no_key_match
209
+ root_y = tempfile.mkdtemp(prefix="retr_s1by_")
210
+ cgy = MdCG(root_y)
211
+ cgy.add("p1", "pure english body nothing else", "knowledge",
212
+ tags=["domain:latin-only"])
213
+ cgy.flush()
214
+ gy = (cgy.search("蜂群调度", k=10, judge=False, record=False)[1]
215
+ .get("gates") or {}).get("s1b") or {}
216
+ check("issue#33 止血:跨语言无别名 → cross_lang_no_match",
217
+ gy.get("reason") == "cross_lang_no_match", str(gy))
218
+ gy3 = (cgy.search("zzz qqq", k=10, judge=False, record=False)[1]
219
+ .get("gates") or {}).get("s1b") or {}
220
+ check("issue#33:非跨语言无匹配仍是 no_key_match",
221
+ gy3.get("reason") == "no_key_match", str(gy3))
222
+ # backfill:模拟存量节点(抹掉 fm 与索引两侧别名)→ 回填恢复 → 收敛恢复
223
+ import os as _os
224
+ for _nid in ("sw1", "tk1"): # 两桶别名都抹(cross_lang 判定看全局比较面)
225
+ _p = _os.path.join(cgx.root, cgx.index["nodes"][_nid]["path"])
226
+ _g = cgx.get(_nid)
227
+ _g["frontmatter"].pop("bucket_zh", None)
228
+ cgx._write_node(_nid, _p, _g["frontmatter"], _g["content"])
229
+ cgx.index["nodes"][_nid]["bucket_zh"] = None
230
+ _gy_pre = (cgx.search("蜂群调度 依赖", k=10, judge=False,
231
+ record=False)[1].get("gates") or {}).get("s1b") or {}
232
+ check("抹掉别名后回到 cross_lang_no_match(存量形态复现)",
233
+ _gy_pre.get("reason") == "cross_lang_no_match", str(_gy_pre))
234
+ st_bf = cgx.backfill_bucket_zh()
235
+ check("backfill 恢复别名(幂等统计)",
236
+ st_bf.get("written") == 2
237
+ and "蜂群调度" in (cgx.index["nodes"]["sw1"].get("bucket_zh") or []),
238
+ str(st_bf))
239
+ gxb = (cgx.search("蜂群调度 依赖", k=10, judge=False,
240
+ record=False)[1].get("gates") or {}).get("s1b") or {}
241
+ check("backfill 后中文 query 重新收敛英文桶",
242
+ gxb.get("keys") == [e_sw["bucket"]], str(gxb))
243
+ st_bf2 = cgx.backfill_bucket_zh()
244
+ check("backfill 幂等(二次全 already)",
245
+ st_bf2.get("written") == 0 and st_bf2.get("already") >= 2, str(st_bf2))
246
+
180
247
  # ---- 7) 幂等 ----
181
248
  _setenv(MDCG_BUCKET_TOPK="1", MDCG_BUCKET_MIN_SIM="0.34")
182
249
  ra, ma = cg.search(QUERY, k=10, judge=False, record=False)
@@ -253,6 +253,14 @@ def main():
253
253
  # 改写已有节点(节点数不变 → 只可能是目录 mtime 变化)
254
254
  cg5.add("n1", "阿尔法 贝塔 伽马 追加", "knowledge")
255
255
  cg5.flush()
256
+ # mtime 粒度守卫(批次 26):指纹用 st_mtime_ns,但同刻写入在负载波动下
257
+ # 可能落进同粒度(实测 43/0 与 41/2 交替——负载敏感 flaky,批次 20 同类)。
258
+ # 断言目标是「改写能被发现」机制而非文件系统粒度——显式前移 mtime 保证确定性。
259
+ _t = time.time_ns() + 60_000_000_000 # +60s
260
+ for _d, _ds, _fs in os.walk(os.path.join(root5, "knowledge")):
261
+ os.utime(_d, ns=(_t, _t))
262
+ for _f in _fs:
263
+ os.utime(os.path.join(_d, _f), ns=(_t, _t))
256
264
  _sr = postings.stale_reason(root5, cg5.index["nodes"])
257
265
  check("改写后:指纹判定过期(dir_touched)", _sr == "dir_touched", _sr)
258
266
  _setenv(MDCG_GATE_S7_POSTINGS=None)
@@ -1,175 +1,181 @@
1
- # -*- coding: utf-8 -*-
2
- """S9b 三元组反查 · 工具面集成探针(阶段二 4.2 T9,脚本式,exit code 定成败)。
3
-
4
- 用法:python -X utf8 -m md_cg.test_retr_s9_entity_ctx
5
- 库层原语语义归 `test_retr_s9_edges.py`,本件只测**工具面集成**:
6
- - 分派全链路:`_cg_dispatch` / `_cg_call` → `_edges_call` → `provenance.find_edges`
7
- - op 缺省推导防呆负例:`child=` 不构成 edges 签名,漏 op 走 read 不冒充反查
8
- - 权限闸双面:`ops_allow` 含 edges 放行;不含 → AccessDenied(通配 "*" 不限)
9
- - 渐进披露:slim 后 op=edges 段被投影、职责行 ≤160;`op=help,query=edges` 无损取回
10
- - schema 一致性:扁平参数面与库层签名同形(含 expand_nodes);无 subject/predicate/object 第二套参数
11
- - 协议对账:`protocol.audit` AST 自动发现 edges(extension_ops 报告项,ok=True)
12
- - FUNC_DESC 覆盖守卫:cogmap_sync 功能描述表必须登记 ("cg", "edges")
13
- """
14
- import inspect
15
- import json
16
- import os
17
- import sys
18
- import tempfile
19
-
20
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
-
22
- from md_cg import provenance, protocol, tool_face as tf # noqa: E402
23
- from md_cg.mdcg import MdCG # noqa: E402
24
- from md_cg.mdcos import MdCGSecure # noqa: E402
25
- from md_cg.security import AccessDenied, Principal # noqa: E402
26
- from md_cg import mcp_server as ms # noqa: E402
27
-
28
- passed = 0
29
- failed = 0
30
- T = "阿尔法 三元组 反查 探针"
31
-
32
- # _edges_call 透传的扁平参数面(schema ↔ 实现同形守卫的期望集)
33
- EDGE_ARGS = ("child", "parent", "relation", "batch",
34
- "start_time", "end_time", "start_operator", "end_operator",
35
- "time_axis", "ordering", "offset", "limit", "aggregation",
36
- "expand_nodes")
37
-
38
-
39
- def check(name, cond, detail=""):
40
- global passed, failed
41
- if cond:
42
- passed += 1
43
- print(" [PASS] " + name)
44
- else:
45
- failed += 1
46
- print(" [FAIL] " + name + " " + detail)
47
-
48
-
49
- def _build(root):
50
- cg = MdCG(root)
51
- for nid in ("p1", "p2"):
52
- cg.add(nid, T, "knowledge")
53
- cg.add("a1", T, "knowledge", derived_from=["p1"])
54
- cg.add("a2", T, "knowledge", derived_from=["p1"], relation="extracted_from")
55
- cg.flush()
56
- return cg
57
-
58
-
59
- def _raises(exc, fn):
60
- try:
61
- fn()
62
- except exc:
63
- return True
64
- except Exception: # noqa: BLE001
65
- return False
66
- return False
67
-
68
-
69
- def main():
70
- root = tempfile.mkdtemp(prefix="retr_s9b_")
71
- cg = _build(root)
72
-
73
- # ================= 1) 分派全链路 =================
74
- r = ms._cg_dispatch(cg, {"op": "edges", "child": "a1"})
75
- check("dispatch 直调:op=edges → _edges_call → find_edges(ok/readonly/total)",
76
- isinstance(r, dict) and r.get("ok") is True and r.get("readonly") is True
77
- and r.get("op") == "edges" and r.get("total") == 1
78
- and r["edges"][0]["child"] == "a1", str(r)[:200])
79
-
80
- rc = ms._cg_call(cg, {"op": "edges", "child": "a2", "expand_nodes": True})
81
- check("_cg_call 外层透传 expand_nodes:边附端点摘要(a2 在库 / p1 在库)",
82
- rc.get("ok") is True and rc.get("total") == 1
83
- and rc["edges"][0]["child_node"]["present"] is True
84
- and rc["edges"][0]["parent_node"]["present"] is True,
85
- str(rc.get("edges", [{}])[0].get("child_node")))
86
-
87
- # ================= 2) op 缺省推导防呆(负例)=================
88
- rd = ms._cg_call(cg, {"child": "a1"})
89
- check("漏 op 不冒充反查:child= 不是 edges 签名 → 按既有规则推导为 read(op_derived 透出)",
90
- rd.get("op") == "read" and rd.get("op_derived") is True,
91
- str({k: rd.get(k) for k in ("op", "op_derived")}))
92
- check("显式 op 优先:op=edges 时 op_derived 不出现(推导只兜底不覆盖)",
93
- "op_derived" not in rc)
94
-
95
- # ================= 3) 权限闸双面 =================
96
- p_ok = Principal(actor="anon", role="anonymous", can_write=False,
97
- ops_allow=("info", "route", "read", "edges"))
98
- p_no = Principal(actor="narrow", role="reader", can_write=False,
99
- ops_allow=("info", "route", "read"))
100
- cg_ok = MdCGSecure(root, principal=p_ok)
101
- cg_no = MdCGSecure(root, principal=p_no)
102
- check("ops_allow 含 edges → 放行(只读结果正常返回)",
103
- ms._cg_dispatch(cg_ok, {"op": "edges", "parent": "p1"}).get("total") == 2)
104
- check("ops_allow 不含 edges → AccessDenied(角色作用域闸,不静默降级)",
105
- _raises(AccessDenied, lambda: ms._cg_dispatch(cg_no, {"op": "edges"})),
106
- "expected AccessDenied")
107
- check("通配白名单不受新 op 影响:ops_allow=('*',) 放行(designer 形态)",
108
- ms._cg_dispatch(
109
- MdCGSecure(root, principal=Principal(
110
- actor="boss", role="designer", can_write=True, can_admin=True,
111
- ops_allow=("*",))),
112
- {"op": "edges", "relation": "derived_from"})["total"] == 1)
113
-
114
- # ================= 4) 渐进披露(投影 ↔ help 回取)=================
115
- slim = {t["name"]: t for t in tf.slim_tools(ms.ALL_TOOLS)}
116
- cg_slim_desc = slim["cg"].get("description") or ""
117
- check("slim 后 op=edges 段被投影(常驻上下文不付费)",
118
- "op=edges:" not in cg_slim_desc and "edges" not in tf.op_sections(cg_slim_desc),
119
- cg_slim_desc[:120])
120
- check("slim 后 cg 职责行仍 ≤160 字符(投影不破尺寸预算)",
121
- len(cg_slim_desc) <= 160, str(len(cg_slim_desc)))
122
- h = tf.help_text(ms.ALL_TOOLS, query="edges")
123
- hit = next((x for x in h.get("hits", []) if x.get("op") == "edges"), None)
124
- check("op=help,query=edges 无损取回:命中 op 段落与透传参数清单(expand_nodes 在列)",
125
- h.get("ok") is True and hit is not None
126
- and "expand_nodes" in json.dumps(hit.get("params") or []),
127
- str(h)[:200])
128
-
129
- # ================= 5) schema 一致性 =================
130
- cg_tool = next(t for t in ms.ALL_TOOLS if t["name"] == "cg")
131
- isch = cg_tool.get("inputSchema") or {}
132
- props = isch.get("properties") or {}
133
- req = set(isch.get("required") or [])
134
- check("schema:edges 全部透传参数在列且均为可选(required 不含)",
135
- all(k in props for k in EDGE_ARGS)
136
- and not (req & set(EDGE_ARGS)),
137
- str([k for k in EDGE_ARGS if k not in props]))
138
- check("schema:predicate/object 不进参数面(subject 是 identity/self_state/link 既有主体参数,非 edges 引入)",
139
- not ({"predicate", "object"} & set(props)))
140
- _ec_src = inspect.getsource(ms._edges_call)
141
- check("实现面:_edges_call 不读 subject/predicate/object 第二套参数键(术语映射只在 docstring)",
142
- not any(('"%s"' % k) in _ec_src or ("'%s'" % k) in _ec_src
143
- for k in ("subject", "predicate", "object")),
144
- str([k for k in ("subject", "predicate", "object")
145
- if k in _ec_src]))
146
- check("实现签名:find_edges 用 expand_nodes(旧名 expand 已废除,防回潮)",
147
- "expand_nodes" in inspect.signature(provenance.find_edges).parameters
148
- and "expand" not in inspect.signature(provenance.find_edges).parameters,
149
- str(list(inspect.signature(provenance.find_edges).parameters)))
150
-
151
- # ================= 6) 协议对账(AST 自动发现)=================
152
- au = protocol.audit()
153
- check("protocol.audit:ok=True、shape_errors 空(edges 属扩展面不违例)",
154
- au.get("ok") is True and not au.get("shape_errors")
155
- and not au.get("errors"), str(au.get("errors")))
156
- check("protocol.audit:AST 自动发现 edges 分支(extension_ops 报告项在列)",
157
- "edges" in (au.get("actual") or [])
158
- and "edges" in (au.get("extension_ops") or []),
159
- str(au.get("actual")))
160
-
161
- # ================= 7) FUNC_DESC 覆盖守卫 =================
162
- sync_path = os.path.join(os.path.dirname(os.path.dirname(
163
- os.path.abspath(__file__))), "scripts", "cogmap_sync.py")
164
- sync_src = open(sync_path, encoding="utf-8").read()
165
- check("cogmap_sync.FUNC_DESC 登记 (\"cg\", \"edges\")(功能调用映射表覆盖门禁的前置)",
166
- '("cg", "edges")' in sync_src)
167
-
168
- print("\ntest_retr_s9_entity_ctx: %d 通过 / %d 失败" % (passed, failed))
169
- return 0 if failed == 0 else 1
170
-
171
-
172
- if __name__ == "__main__":
173
- if hasattr(sys.stdout, "reconfigure"):
174
- sys.stdout.reconfigure(encoding="utf-8")
175
- sys.exit(main())
1
+ # -*- coding: utf-8 -*-
2
+ """S9b 三元组反查 · 工具面集成探针(阶段二 4.2 T9,脚本式,exit code 定成败)。
3
+
4
+ 用法:python -X utf8 -m md_cg.test_retr_s9_entity_ctx
5
+ 库层原语语义归 `test_retr_s9_edges.py`,本件只测**工具面集成**:
6
+ - 分派全链路:`_cg_dispatch` / `_cg_call` → `_edges_call` → `provenance.find_edges`
7
+ - op 缺省推导防呆负例:`child=` 不构成 edges 签名,漏 op 走 read 不冒充反查
8
+ - 权限闸双面:`ops_allow` 含 edges 放行;不含 → AccessDenied(通配 "*" 不限)
9
+ - 渐进披露:slim 后 op=edges 段被投影、职责行 ≤160;`op=help,query=edges` 无损取回
10
+ - schema 一致性:扁平参数面与库层签名同形(含 expand_nodes);无 subject/predicate/object 第二套参数
11
+ - 协议对账:`protocol.audit` AST 自动发现 edges(extension_ops 报告项,ok=True)
12
+ - FUNC_DESC 覆盖守卫:cogmap_sync 功能描述表必须登记 ("cg", "edges")
13
+ """
14
+ import inspect
15
+ import json
16
+ import os
17
+ import sys
18
+ import tempfile
19
+
20
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
+
22
+ from md_cg import provenance, protocol, tool_face as tf # noqa: E402
23
+ from md_cg.mdcg import MdCG # noqa: E402
24
+ from md_cg.mdcos import MdCGSecure # noqa: E402
25
+ from md_cg.security import AccessDenied, Principal # noqa: E402
26
+ from md_cg import mcp_server as ms # noqa: E402
27
+
28
+ passed = 0
29
+ failed = 0
30
+ T = "阿尔法 三元组 反查 探针"
31
+
32
+ # _edges_call 透传的扁平参数面(schema ↔ 实现同形守卫的期望集)
33
+ EDGE_ARGS = ("child", "parent", "relation", "batch",
34
+ "start_time", "end_time", "start_operator", "end_operator",
35
+ "time_axis", "ordering", "offset", "limit", "aggregation",
36
+ "expand_nodes")
37
+
38
+
39
+ def check(name, cond, detail=""):
40
+ global passed, failed
41
+ if cond:
42
+ passed += 1
43
+ print(" [PASS] " + name)
44
+ else:
45
+ failed += 1
46
+ print(" [FAIL] " + name + " " + detail)
47
+
48
+
49
+ def _build(root):
50
+ cg = MdCG(root)
51
+ for nid in ("p1", "p2"):
52
+ cg.add(nid, T, "knowledge")
53
+ cg.add("a1", T, "knowledge", derived_from=["p1"])
54
+ cg.add("a2", T, "knowledge", derived_from=["p1"], relation="extracted_from")
55
+ cg.flush()
56
+ return cg
57
+
58
+
59
+ def _raises(exc, fn):
60
+ try:
61
+ fn()
62
+ except exc:
63
+ return True
64
+ except Exception: # noqa: BLE001
65
+ return False
66
+ return False
67
+
68
+
69
+ def main():
70
+ root = tempfile.mkdtemp(prefix="retr_s9b_")
71
+ cg = _build(root)
72
+
73
+ # ================= 1) 分派全链路 =================
74
+ r = ms._cg_dispatch(cg, {"op": "edges", "child": "a1"})
75
+ check("dispatch 直调:op=edges → _edges_call → find_edges(ok/readonly/total)",
76
+ isinstance(r, dict) and r.get("ok") is True and r.get("readonly") is True
77
+ and r.get("op") == "edges" and r.get("total") == 1
78
+ and r["edges"][0]["child"] == "a1", str(r)[:200])
79
+
80
+ rc = ms._cg_call(cg, {"op": "edges", "child": "a2", "expand_nodes": True})
81
+ check("_cg_call 外层透传 expand_nodes:边附端点摘要(a2 在库 / p1 在库)",
82
+ rc.get("ok") is True and rc.get("total") == 1
83
+ and rc["edges"][0]["child_node"]["present"] is True
84
+ and rc["edges"][0]["parent_node"]["present"] is True,
85
+ str(rc.get("edges", [{}])[0].get("child_node")))
86
+
87
+ # ================= 2) op 缺省推导防呆(负例)=================
88
+ rd = ms._cg_call(cg, {"child": "a1"})
89
+ check("漏 op 不冒充反查:child= 不是 edges 签名 → 按既有规则推导为 read(op_derived 透出)",
90
+ rd.get("op") == "read" and rd.get("op_derived") is True,
91
+ str({k: rd.get(k) for k in ("op", "op_derived")}))
92
+ check("显式 op 优先:op=edges 时 op_derived 不出现(推导只兜底不覆盖)",
93
+ "op_derived" not in rc)
94
+
95
+ # ================= 3) 权限闸双面 =================
96
+ p_ok = Principal(actor="anon", role="anonymous", can_write=False,
97
+ ops_allow=("info", "route", "read", "edges"))
98
+ p_no = Principal(actor="narrow", role="reader", can_write=False,
99
+ ops_allow=("info", "route", "read"))
100
+ cg_ok = MdCGSecure(root, principal=p_ok)
101
+ cg_no = MdCGSecure(root, principal=p_no)
102
+ check("ops_allow 含 edges → 放行(只读结果正常返回)",
103
+ ms._cg_dispatch(cg_ok, {"op": "edges", "parent": "p1"}).get("total") == 2)
104
+ check("ops_allow 不含 edges → AccessDenied(角色作用域闸,不静默降级)",
105
+ _raises(AccessDenied, lambda: ms._cg_dispatch(cg_no, {"op": "edges"})),
106
+ "expected AccessDenied")
107
+ check("通配白名单不受新 op 影响:ops_allow=('*',) 放行(designer 形态)",
108
+ ms._cg_dispatch(
109
+ MdCGSecure(root, principal=Principal(
110
+ actor="boss", role="designer", can_write=True, can_admin=True,
111
+ ops_allow=("*",))),
112
+ {"op": "edges", "relation": "derived_from"})["total"] == 1)
113
+
114
+ # ================= 4) 渐进披露(投影 ↔ help 回取)=================
115
+ slim = {t["name"]: t for t in tf.slim_tools(ms.ALL_TOOLS)}
116
+ cg_slim_desc = slim["cg"].get("description") or ""
117
+ check("slim 后 op=edges 段被投影(常驻上下文不付费)",
118
+ "op=edges:" not in cg_slim_desc and "edges" not in tf.op_sections(cg_slim_desc),
119
+ cg_slim_desc[:120])
120
+ check("slim 后 cg 职责行仍 ≤160 字符(投影不破尺寸预算)",
121
+ len(cg_slim_desc) <= 160, str(len(cg_slim_desc)))
122
+ h = tf.help_text(ms.ALL_TOOLS, query="edges")
123
+ hit = next((x for x in h.get("hits", []) if x.get("op") == "edges"), None)
124
+ check("op=help,query=edges 无损取回:命中 op 段落与透传参数清单(expand_nodes 在列)",
125
+ h.get("ok") is True and hit is not None
126
+ and "expand_nodes" in json.dumps(hit.get("params") or []),
127
+ str(h)[:200])
128
+
129
+ # ================= 5) schema 一致性 =================
130
+ cg_tool = next(t for t in ms.ALL_TOOLS if t["name"] == "cg")
131
+ isch = cg_tool.get("inputSchema") or {}
132
+ props = isch.get("properties") or {}
133
+ req = set(isch.get("required") or [])
134
+ check("schema:edges 全部透传参数在列且均为可选(required 不含)",
135
+ all(k in props for k in EDGE_ARGS)
136
+ and not (req & set(EDGE_ARGS)),
137
+ str([k for k in EDGE_ARGS if k not in props]))
138
+ check("schema:predicate/object 不进参数面(subject 是 identity/self_state/link 既有主体参数,非 edges 引入)",
139
+ not ({"predicate", "object"} & set(props)))
140
+ _ec_src = inspect.getsource(ms._edges_call)
141
+ check("实现面:_edges_call 不读 subject/predicate/object 第二套参数键(术语映射只在 docstring)",
142
+ not any(('"%s"' % k) in _ec_src or ("'%s'" % k) in _ec_src
143
+ for k in ("subject", "predicate", "object")),
144
+ str([k for k in ("subject", "predicate", "object")
145
+ if k in _ec_src]))
146
+ check("实现签名:find_edges 用 expand_nodes(旧名 expand 已废除,防回潮)",
147
+ "expand_nodes" in inspect.signature(provenance.find_edges).parameters
148
+ and "expand" not in inspect.signature(provenance.find_edges).parameters,
149
+ str(list(inspect.signature(provenance.find_edges).parameters)))
150
+
151
+ # ================= 6) 协议对账(AST 自动发现)=================
152
+ au = protocol.audit()
153
+ check("protocol.audit:ok=True、shape_errors 空(edges 属扩展面不违例)",
154
+ au.get("ok") is True and not au.get("shape_errors")
155
+ and not au.get("errors"), str(au.get("errors")))
156
+ check("protocol.audit:AST 自动发现 edges 分支(extension_ops 报告项在列)",
157
+ "edges" in (au.get("actual") or [])
158
+ and "edges" in (au.get("extension_ops") or []),
159
+ str(au.get("actual")))
160
+
161
+ # ================= 7) FUNC_DESC 覆盖守卫 =================
162
+ # cogmap_sync 是**源码树**门禁脚本(出货包 files 不含 scripts/)→ 安装态
163
+ # 如实 SKIP:缺件不是被测缺陷,也不虚报通过(2026-09-24 修复)。
164
+ sync_path = os.path.join(os.path.dirname(os.path.dirname(
165
+ os.path.abspath(__file__))), "scripts", "cogmap_sync.py")
166
+ if not os.path.isfile(sync_path):
167
+ print(" SKIP cogmap_sync.FUNC_DESC 覆盖守卫(scripts/cogmap_sync.py "
168
+ "不在——源码树门禁脚本,出货包不含)")
169
+ else:
170
+ sync_src = open(sync_path, encoding="utf-8").read()
171
+ check("cogmap_sync.FUNC_DESC 登记 (\"cg\", \"edges\")(功能调用映射表覆盖门禁的前置)",
172
+ '("cg", "edges")' in sync_src)
173
+
174
+ print("\ntest_retr_s9_entity_ctx: %d 通过 / %d 失败" % (passed, failed))
175
+ return 0 if failed == 0 else 1
176
+
177
+
178
+ if __name__ == "__main__":
179
+ if hasattr(sys.stdout, "reconfigure"):
180
+ sys.stdout.reconfigure(encoding="utf-8")
181
+ sys.exit(main())