@furongjun1999/dsh-memory 0.4.8 → 0.4.9

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 (231) hide show
  1. package/README.md +54 -26
  2. package/codebuddy/CODEBUDDY.md +196 -195
  3. package/codebuddy/README.md +13 -1
  4. package/codebuddy/mcp.json +9 -0
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
  6. package/docs/README.md +1 -1
  7. package/docs/discipline/harnesses.yaml +18 -7
  8. package/docs/discipline/templates/full.md.tmpl +4 -3
  9. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  10. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  11. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  12. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  13. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  14. package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
  15. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
  16. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -0
  17. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
  18. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
  19. 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 +503 -0
  20. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
  21. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  22. package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
  23. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -0
  24. 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 +47 -45
  25. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
  26. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
  27. package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
  28. package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
  29. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
  30. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
  31. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
  32. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +434 -433
  33. package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
  34. package/dsh/README.md +33 -0
  35. package/dsh/cordis.yml.example +13 -7
  36. package/dsh/hive-mcp-probe.mjs +94 -0
  37. package/dsh/hive-mcp.example.yml +62 -0
  38. package/dsh/update-lingshu.bat +11 -0
  39. package/dsh/update-lingshu.ps1 +337 -0
  40. package/lib/hooks.d.ts +3 -0
  41. package/lib/hooks.js +17 -23
  42. package/lib/index.d.ts +4 -2
  43. package/lib/index.js +29 -6
  44. package/lib/lib/datapath.d.ts +76 -1
  45. package/lib/lib/datapath.js +199 -13
  46. package/lib/lib/mdcg_client.d.ts +6 -3
  47. package/lib/lib/mdcg_client.js +6 -5
  48. package/lib/lib/mutual.js +4 -4
  49. package/lib/lib/token_store.js +4 -5
  50. package/md_cg/audit.py +17 -2
  51. package/md_cg/autonomy.py +86 -15
  52. package/md_cg/backfill.py +36 -1
  53. package/md_cg/backfill_bigdomain.py +34 -0
  54. package/md_cg/bench6_arms.py +28 -1
  55. package/md_cg/bench6_common.py +10 -1
  56. package/md_cg/bench6_competitors.py +6 -1
  57. package/md_cg/bench_axis_domain.py +9 -1
  58. package/md_cg/bench_blind_comp.py +7 -1
  59. package/md_cg/bench_en_atoms_public.py +9 -0
  60. package/md_cg/bench_governance.py +348 -0
  61. package/md_cg/bench_lme_zh.py +16 -1
  62. package/md_cg/bench_locomo.py +2 -1
  63. package/md_cg/bench_locomo_zh.py +16 -1
  64. package/md_cg/bench_locomo_zh_public.py +4 -1
  65. package/md_cg/bench_longmem.py +2 -1
  66. package/md_cg/bench_membench.py +27 -1
  67. package/md_cg/bench_p0.py +4 -1
  68. package/md_cg/bench_progressive.py +13 -1
  69. package/md_cg/bench_role_views.py +238 -0
  70. package/md_cg/bench_task_ab.py +8 -1
  71. package/md_cg/bench_task_ab_llm.py +13 -1
  72. package/md_cg/bench_unified_en.py +6 -1
  73. package/md_cg/bench_zh_mad.py +20 -1
  74. package/md_cg/blindspot_tickets.py +123 -0
  75. package/md_cg/branches.py +12 -1
  76. package/md_cg/build_postings.py +73 -0
  77. package/md_cg/ccgc.py +67 -2
  78. package/md_cg/census.py +5 -1
  79. package/md_cg/chain.py +24 -3
  80. package/md_cg/codeindex.py +134 -17
  81. package/md_cg/coldverify.py +265 -0
  82. package/md_cg/comment_gate.py +338 -0
  83. package/md_cg/cond_compose.py +190 -0
  84. package/md_cg/cond_facts.py +155 -0
  85. package/md_cg/cond_template.json +107 -0
  86. package/md_cg/condition_anchor.py +143 -0
  87. package/md_cg/conformance.py +69 -4
  88. package/md_cg/consistency.py +24 -1
  89. package/md_cg/consolidate.py +53 -2
  90. package/md_cg/corpus.py +4 -0
  91. package/md_cg/crosscheck.py +42 -2
  92. package/md_cg/crypto.py +35 -1
  93. package/md_cg/d_meta.py +310 -0
  94. package/md_cg/datapath.py +201 -26
  95. package/md_cg/docindex.py +122 -1
  96. package/md_cg/eval_common.py +29 -1
  97. package/md_cg/evidence.py +27 -1
  98. package/md_cg/evolution.py +21 -1
  99. package/md_cg/export.py +11 -1
  100. package/md_cg/forgetting.py +23 -1
  101. package/md_cg/fsutil.py +18 -1
  102. package/md_cg/hotcache.py +214 -0
  103. package/md_cg/hyperedge.py +251 -0
  104. package/md_cg/identity.py +18 -1
  105. package/md_cg/insight.py +17 -1
  106. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  107. package/md_cg/lexicon/build_standard_en.py +171 -168
  108. package/md_cg/lexicon/expand_en_zh.py +6 -0
  109. package/md_cg/lifecycle.py +12 -1
  110. package/md_cg/linkref.py +281 -0
  111. package/md_cg/links.py +29 -1
  112. package/md_cg/mcp_server.py +362 -43
  113. package/md_cg/md_whitebox.py +53 -1
  114. package/md_cg/mdcg.py +1003 -27
  115. package/md_cg/mdcos.py +558 -36
  116. package/md_cg/metacognition.py +37 -2
  117. package/md_cg/migrate.py +4 -0
  118. package/md_cg/migrate_aeis.py +221 -213
  119. package/md_cg/migrate_roleplay.py +8 -0
  120. package/md_cg/migrate_wisdom_graph.py +14 -1
  121. package/md_cg/mreview/__main__.py +3 -0
  122. package/md_cg/mreview/bundle.py +8 -0
  123. package/md_cg/mreview/candidates.py +9 -0
  124. package/md_cg/mreview/govern.py +21 -1
  125. package/md_cg/mreview/locate.py +34 -0
  126. package/md_cg/mreview/pipeline.py +29 -1
  127. package/md_cg/mreview/ruleset.py +16 -1
  128. package/md_cg/nodefile.py +233 -3
  129. package/md_cg/pooling.py +23 -1
  130. package/md_cg/postings.py +298 -0
  131. package/md_cg/predict.py +89 -9
  132. package/md_cg/progressive.py +3 -0
  133. package/md_cg/protect.py +14 -1
  134. package/md_cg/protocol.py +372 -0
  135. package/md_cg/provenance.py +262 -0
  136. package/md_cg/reach.py +453 -0
  137. package/md_cg/refindex.py +47 -2
  138. package/md_cg/refine.py +20 -1
  139. package/md_cg/roleviews.py +89 -0
  140. package/md_cg/routing.py +76 -0
  141. package/md_cg/scrub.py +63 -2
  142. package/md_cg/security.py +26 -1
  143. package/md_cg/self_state.py +64 -1
  144. package/md_cg/selfreport.py +151 -0
  145. package/md_cg/semantic/canonical.py +5 -0
  146. package/md_cg/semantic/en_normalizer.py +364 -355
  147. package/md_cg/semantic/zh_en_atoms.py +139 -136
  148. package/md_cg/signer.py +41 -1
  149. package/md_cg/sources.py +583 -547
  150. package/md_cg/statushdr.py +179 -0
  151. package/md_cg/stg.py +59 -18
  152. package/md_cg/subgraph.py +23 -0
  153. package/md_cg/sustain.py +56 -1
  154. package/md_cg/tasks.py +26 -2
  155. package/md_cg/test_autonomy.py +26 -0
  156. package/md_cg/test_bench_governance.py +102 -0
  157. package/md_cg/test_blindspot_tickets.py +166 -0
  158. package/md_cg/test_ccgc.py +10 -0
  159. package/md_cg/test_codeindex.py +338 -0
  160. package/md_cg/test_comment_gate.py +187 -0
  161. package/md_cg/test_cond_compose_anchors.py +76 -0
  162. package/md_cg/test_condition_anchor.py +82 -0
  163. package/md_cg/test_d_meta.py +412 -0
  164. package/md_cg/test_datapath_root.py +188 -0
  165. package/md_cg/test_gain_gate.py +47 -1
  166. package/md_cg/test_hot_cold.py +187 -0
  167. package/md_cg/test_hyperedge.py +245 -0
  168. package/md_cg/test_linkref.py +306 -0
  169. package/md_cg/test_md_access_parity.py +15 -3
  170. package/md_cg/test_mr_m1.py +108 -18
  171. package/md_cg/test_mr_m3.py +8 -1
  172. package/md_cg/test_p26_refindex.py +49 -20
  173. package/md_cg/test_p27_docindex.py +236 -2
  174. package/md_cg/test_p2_mcp.py +1 -1
  175. package/md_cg/test_p31_insight.py +24 -0
  176. package/md_cg/test_p44_md_whitebox.py +14 -1
  177. package/md_cg/test_protocol.py +243 -0
  178. package/md_cg/test_reach.py +378 -0
  179. package/md_cg/test_reach_keys.py +201 -0
  180. package/md_cg/test_reach_meta_exits.py +145 -0
  181. package/md_cg/test_read_clip.py +8 -4
  182. package/md_cg/test_retr_s1.py +340 -0
  183. package/md_cg/test_retr_s1b.py +209 -0
  184. package/md_cg/test_retr_s3.py +194 -0
  185. package/md_cg/test_retr_s4.py +163 -0
  186. package/md_cg/test_retr_s5.py +200 -0
  187. package/md_cg/test_retr_s6.py +157 -0
  188. package/md_cg/test_retr_s7.py +385 -0
  189. package/md_cg/test_retr_s8_time.py +316 -0
  190. package/md_cg/test_retr_s9_edges.py +286 -0
  191. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  192. package/md_cg/test_review_conformance.py +59 -2
  193. package/md_cg/test_role_views.py +354 -0
  194. package/md_cg/test_trust.py +361 -0
  195. package/md_cg/test_units_poll.py +71 -0
  196. package/md_cg/test_v14_fixes.py +397 -0
  197. package/md_cg/test_validity_filter.py +280 -0
  198. package/md_cg/test_wisdom_md_store.py +7 -3
  199. package/md_cg/test_writepipe.py +5 -1
  200. package/md_cg/theory.py +16 -1
  201. package/md_cg/tokens.py +40 -8
  202. package/md_cg/tool_face.py +13 -2
  203. package/md_cg/trust.py +943 -0
  204. package/md_cg/twophase.py +12 -1
  205. package/md_cg/units.py +129 -10
  206. package/md_cg/vision_evidence.py +24 -1
  207. package/md_cg/weights.py +24 -1
  208. package/md_cg/whitebox.py +32 -1
  209. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  210. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  211. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  212. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  213. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  214. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  215. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  216. package/md_cg/writelimit.py +18 -4
  217. package/md_cg/writepipe.py +178 -7
  218. package/package.json +2 -2
  219. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  220. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  221. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  222. package/src/hooks.ts +17 -21
  223. package/src/index.ts +33 -6
  224. package/src/lib/datapath.ts +211 -13
  225. package/src/lib/mdcg_client.ts +12 -8
  226. package/src/lib/mutual.ts +411 -411
  227. package/src/lib/token_store.ts +4 -5
  228. package/zcode/AGENTS.md +196 -195
  229. package/zcode/README.md +4 -0
  230. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
@@ -0,0 +1,175 @@
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())
@@ -11,6 +11,9 @@
11
11
  ⑦ 并发幂等(真多进程):N 进程同内容并发 propose → 恰好 1 条、pid 全同
12
12
  ⑧ 并发不丢(真多进程):N 进程×M 条不同内容 → inbox 读回恰好 N×M 条
13
13
  ⑨ decisions 并发不丢(真多进程):并发裁决 → 记录恰好 N 条且指纹完整
14
+ ⑩ 安全层转发:MdCGSecure.propose 的幂等/info 透传
15
+ ⑪ NOOP 裁决:只留痕(jsonl + 审计节点)不落业务节点、不写负记忆、终态关闭、
16
+ 统计可见,且与 reject 的留痕状态可区分
14
17
 
15
18
  运行:python -m md_cg.test_review_conformance
16
19
  """
@@ -89,7 +92,7 @@ def _run_worker(workdir, root, mode, arg="", timeout=120):
89
92
  def main():
90
93
  workdir = tempfile.mkdtemp(prefix="mdcg_conf_wk_")
91
94
  root = tempfile.mkdtemp(prefix="mdcg_conf_")
92
- root2 = root2b = root3 = root4 = ""
95
+ root2 = root2b = root3 = root4 = root5 = ""
93
96
  _ensure_worker(workdir)
94
97
  try:
95
98
  cg = MdCGOS(root, actor="test")
@@ -295,13 +298,67 @@ def main():
295
298
  "info" not in (sec_rec.get("extra") or {}),
296
299
  str(sec_rec.get("extra")))
297
300
 
301
+ # ---------------- ⑪ NOOP 裁决(治理层动作) ----------------
302
+ print("\n【11】NOOP 裁决:只留痕、不落节点、不写负记忆、终态关闭")
303
+ root5 = tempfile.mkdtemp(prefix="mdcg_conf_noop_")
304
+ cg5 = MdCGOS(root5, actor="test-noop")
305
+
306
+ def _neg_n():
307
+ """负记忆条目数(rejected 层)——NOOP 不得增加它。"""
308
+ return sum(1 for e in cg5.index["nodes"].values()
309
+ if (e.get("layer") or "") == "rejected")
310
+
311
+ nb = _neg_n()
312
+ np1 = cg5.propose("noop1", "# 功能名:NOOP 探针\n\n已评估,判定无需改动。\n",
313
+ layer="knowledge")
314
+ dn = cg5.review_decide(np1, "noop", reason="评估过,无需改动")
315
+ check("noop 裁决成功且 decision=noop",
316
+ dn.get("ok") is True and dn.get("decision") == "noop", str(dn)[:80])
317
+ check("noop 不落业务节点", not cg5.get("noop1"), "")
318
+ nrecs = [r for r in cg5.decisions() if r.get("pid") == np1]
319
+ check("noop 进 decisions 留痕(status=noop + record_hash)",
320
+ len(nrecs) == 1 and nrecs[0].get("status") == "noop"
321
+ and bool(nrecs[0].get("record_hash")),
322
+ f"n={len(nrecs)} status={nrecs[0].get('status') if nrecs else '-'}")
323
+ check("noop 不写负记忆(rejected 条目数不增)", _neg_n() == nb,
324
+ f"{nb}→{_neg_n()}")
325
+ check("noop 后提案终态关闭(review_list 不再列出)",
326
+ not any(r.get("pid") == np1 for r in cg5.review_list()), "")
327
+ dn2 = cg5.review_decide(np1, "noop")
328
+ check("重复 noop 幂等拒绝(already_decided)",
329
+ dn2.get("ok") is False and dn2.get("error") == "already_decided",
330
+ str(dn2.get("error")))
331
+ audit = cg5.review_records(pid=np1)
332
+ check("noop 的 md 审计记录节点已落(外部来源可复核)",
333
+ len(audit) == 1, f"n={len(audit)}")
334
+ vrec = cg5.verify_review_record(audit[0]["id"]) if audit else {}
335
+ check("noop 审计记录指纹复核一致且状态为 noop",
336
+ vrec.get("ok") is True and vrec.get("status") == "noop",
337
+ str(vrec)[:90])
338
+ st5 = cg5.review_stats()
339
+ check("review_stats 统计到 noop(by_decision.noop=1)且队列已清",
340
+ st5.get("noop") == 1
341
+ and (st5.get("by_decision") or {}).get("noop") == 1
342
+ and st5.get("pending") == 0 and st5.get("closed") == 1,
343
+ str(st5))
344
+ # 语义分界:noop 与 reject 都终止提案,但留痕状态必须可区分
345
+ # (否则「评估过、无需改动」会被误读为「否掉了这条候选」)
346
+ rj1 = cg5.propose("rej1", "# 功能名:NOOP 对照探针\n\n对照用。\n",
347
+ layer="knowledge")
348
+ dr = cg5.review_decide(rj1, "reject", reason="对照")
349
+ rjrec = [r for r in cg5.decisions() if r.get("pid") == rj1]
350
+ check("noop 与 reject 留痕状态可区分(noop vs rejected)",
351
+ dr.get("ok") is True and bool(rjrec)
352
+ and rjrec[0].get("status") == "rejected",
353
+ f"reject_status={rjrec[0].get('status') if rjrec else '-'}")
354
+
298
355
  print(f"\n===== conformance:PASS={PASS} FAIL={FAIL} =====")
299
356
  if FAILS:
300
357
  print("失败项:", ";".join(FAILS))
301
358
  return 1 if FAIL else 0
302
359
  finally:
303
360
  shutil.rmtree(workdir, ignore_errors=True)
304
- for r in (root, root2, root2b, root3, root4):
361
+ for r in (root, root2, root2b, root3, root4, root5):
305
362
  if r:
306
363
  shutil.rmtree(r, ignore_errors=True)
307
364
 
@@ -0,0 +1,354 @@
1
+ # -*- coding: utf-8 -*-
2
+ """角色化读取视图(roleviews)验收:第四阶段计划批次 C(H4)。
3
+
4
+ T1 规则表守卫——WORK_ROLES 白名单字面一致 + content_kind 合法枚举
5
+ + 三视图资格维度与计划 §4.1 规则表一致;
6
+ T2 matches 单点谓词判定矩阵(三视图 × 各类元数据)+ 非法 view fail-closed;
7
+ T3 库层 fail-closed——candidates/search/search_rrf/recall 四入口
8
+ 非法 view 一律 ValueError(不静默回落);
9
+ T4 零行为变更(H4 核心)——view=None(缺省)与不传 view 逐位一致
10
+ (同 id 同序同分);
11
+ T5 三视图端到端——各自命中集不同;receipt 与 main 在 role 维度互补;
12
+ 无 role 的 code 节点三视图皆不可见(类型启发严格口径,bench 校准
13
+ 是法定修正通道);
14
+ T6 缓存隔离——同 query 不同 view 不串味(缓存键含 view),同 view
15
+ 命中缓存且结果一致;
16
+ T7 Secure 层——_candidates 签名同步(kw 透传不炸)+ search_rrf
17
+ 全称量词断言(结果集全部满足视图资格,图扩展扩散路径不泄漏);
18
+ T8 recall 双路径透传——use_rrf 两分支端到端 + fail-closed 贯通;
19
+ T9 MCP 透传面——源码断言(read/route 分支 view= + schema 登记项,
20
+ 与 test_hyperedge T1③④ 同口径,防「库层通了、工具面没接」)。
21
+
22
+ 运行:python -m md_cg.test_role_views
23
+ """
24
+
25
+ import os
26
+ import shutil
27
+ import sys
28
+ import tempfile
29
+ import traceback
30
+
31
+ from . import roleviews
32
+ from . import hotcache
33
+ from .mdcos import MdCGSecure, WORK_ROLES
34
+ from .security import Principal
35
+
36
+ _ok = 0
37
+ _bad = []
38
+
39
+
40
+ def _check(name, cond, detail=""):
41
+ global _ok
42
+ if cond:
43
+ _ok += 1
44
+ print(" ok %s" % name)
45
+ else:
46
+ _bad.append("%s %s" % (name, detail))
47
+ print(" FAIL %s %s" % (name, detail))
48
+
49
+
50
+ def _src(name):
51
+ here = os.path.dirname(os.path.abspath(__file__))
52
+ with open(os.path.join(here, name), encoding="utf-8") as f:
53
+ return f.read()
54
+
55
+
56
+ def _mk_cg(tmp):
57
+ p = Principal(tenant="default", actor="t_roleviews", role="designer",
58
+ can_write=True, can_admin=True)
59
+ return MdCGSecure(tempfile.mkdtemp(dir=tmp), principal=p)
60
+
61
+
62
+ # 混合语料:共享主题词「极光」(同 query 命中全体,视图过滤看差异)
63
+ _CORPUS = [
64
+ # (nid, layer, content_kind, role, tail)
65
+ ("rv_e1", "knowledge", "text", "user", "观测报告正文结论"),
66
+ ("rv_e2", "knowledge", "work_done", None, "工作完成结论记录"),
67
+ ("rv_e3", "knowledge", "ccg_marks", None, "判据面与环境陷阱"),
68
+ ("rv_e4", "contextual", "text", None, "情境上下文补充"),
69
+ ("rv_e5", "knowledge", "code", "command", "命令脚本片段"),
70
+ ("rv_e6", "knowledge", "text", "tool-output", "工具输出记录"),
71
+ ("rv_e7", "knowledge", "code", None, "无角色代码片段"),
72
+ ]
73
+ # 各视图预期可见集(T5 断言依据,与 roleviews.matches 语义独立推导)
74
+ _EXPECT = {
75
+ None: {"rv_e1", "rv_e2", "rv_e3", "rv_e4", "rv_e7"},
76
+ "main": {"rv_e1", "rv_e2"},
77
+ "verifier": {"rv_e1", "rv_e3", "rv_e4"},
78
+ "receipt": {"rv_e5", "rv_e6"},
79
+ }
80
+
81
+
82
+ def _seed(cg):
83
+ for nid, layer, ck, role, tail in _CORPUS:
84
+ kw = {"layer": layer, "content_kind": ck}
85
+ if role is not None:
86
+ kw["role"] = role
87
+ cg.add(nid, "极光 %s" % tail, **kw)
88
+
89
+
90
+ def _ids(res):
91
+ return [r[0]["id"] for r in res]
92
+
93
+
94
+ def _nid_of_entry(e):
95
+ """candidates 索引 entry 无顶层 id 键——nid 取 path 的 stem(*.md)。"""
96
+ p = (e.get("path") or "").replace("\\", "/")
97
+ return p.rsplit("/", 1)[-1][:-3] if p.endswith(".md") else p
98
+
99
+
100
+ def _collect_ids(o, acc=None):
101
+ """递归收集容器里所有节点标识(id 或 path,不依赖 recall 返回键形)。"""
102
+ if acc is None:
103
+ acc = set()
104
+ if isinstance(o, dict):
105
+ for k, v in o.items():
106
+ if isinstance(v, str) and k == "id":
107
+ acc.add(v)
108
+ elif isinstance(v, str) and k == "path":
109
+ acc.add(_nid_of_entry({"path": v}))
110
+ else:
111
+ _collect_ids(v, acc)
112
+ elif isinstance(o, (list, tuple)):
113
+ for it in o:
114
+ _collect_ids(it, acc)
115
+ return acc
116
+
117
+
118
+ def _t1_rules():
119
+ print("\n【T1】规则表守卫(真源一致性)")
120
+ _check("① receipt 白名单与 mdcos.WORK_ROLES 字面一致(防两处漂移)",
121
+ roleviews.ROLE_VIEWS["receipt"]["roles"] == WORK_ROLES,
122
+ "%r vs %r" % (roleviews.ROLE_VIEWS["receipt"]["roles"], WORK_ROLES))
123
+ _check("② 视图名集合 = {main, verifier, receipt}",
124
+ set(roleviews.ROLE_VIEWS) == {"main", "verifier", "receipt"},
125
+ str(sorted(roleviews.ROLE_VIEWS)))
126
+ from . import audit as audit_mod
127
+ _check("③ main/verifier 的 content_kinds 均为合法枚举(⊆audit.CONTENT_KINDS)",
128
+ set(roleviews.ROLE_VIEWS["main"]["content_kinds"])
129
+ | set(roleviews.ROLE_VIEWS["verifier"]["content_kinds"])
130
+ <= set(audit_mod.CONTENT_KINDS))
131
+ _check("④ main 资格=text|work_done ∧ knowledge 层(计划 §4.1)",
132
+ set(roleviews.ROLE_VIEWS["main"]["content_kinds"]) == {"text", "work_done"}
133
+ and roleviews.ROLE_VIEWS["main"]["layers"] == ("knowledge",))
134
+ _check("⑤ verifier 资格=ccg_marks|text ∧ knowledge|contextual 层(计划 §4.1)",
135
+ set(roleviews.ROLE_VIEWS["verifier"]["content_kinds"]) == {"ccg_marks", "text"}
136
+ and set(roleviews.ROLE_VIEWS["verifier"]["layers"])
137
+ == {"knowledge", "contextual"})
138
+ _check("⑥ include_work 旗标:main/verifier=False,receipt=True",
139
+ roleviews.ROLE_VIEWS["main"]["include_work"] is False
140
+ and roleviews.ROLE_VIEWS["verifier"]["include_work"] is False
141
+ and roleviews.ROLE_VIEWS["receipt"]["include_work"] is True)
142
+ _check("⑦ views() 返回排序稳定元组",
143
+ roleviews.views() == ("main", "receipt", "verifier"),
144
+ str(roleviews.views()))
145
+
146
+
147
+ def _t2_matches():
148
+ print("\n【T2】matches 单点谓词判定矩阵")
149
+ M = roleviews.matches
150
+ cases = [
151
+ # (fm, view, 期望, 说明)
152
+ ({"layer": "knowledge", "content_kind": "text"}, "main", True,
153
+ "text+knowledge 命中 main"),
154
+ ({"layer": "knowledge", "content_kind": "work_done"}, "main", True,
155
+ "work_done 命中 main"),
156
+ ({"layer": "contextual", "content_kind": "text"}, "main", False,
157
+ "contextual 层不在 main"),
158
+ ({"layer": "knowledge", "content_kind": "code", "role": "command"},
159
+ "main", False, "code 不在 main 类型集"),
160
+ ({"layer": "knowledge", "content_kind": "text", "role": "tool-output"},
161
+ "main", False, "工作角色在 main 一票否决"),
162
+ ({"layer": "knowledge", "content_kind": "text", "role": "command"},
163
+ "main", False, "command 角色在 main 一票否决"),
164
+ ({"layer": "knowledge", "content_kind": "text"}, "verifier", True,
165
+ "text+knowledge 命中 verifier"),
166
+ ({"layer": "knowledge", "content_kind": "ccg_marks"}, "verifier", True,
167
+ "marks 命中 verifier"),
168
+ ({"layer": "contextual", "content_kind": "text"}, "verifier", True,
169
+ "contextual+text 命中 verifier"),
170
+ ({"layer": "contextual", "content_kind": "ccg_marks"}, "verifier", True,
171
+ "contextual+marks 命中 verifier"),
172
+ ({"layer": "knowledge", "content_kind": "work_done"}, "verifier", False,
173
+ "work_done 不在 verifier 类型集"),
174
+ ({"role": "command"}, "receipt", True, "command 命中 receipt 白名单"),
175
+ ({"role": "tool-output"}, "receipt", True, "tool-output 命中 receipt"),
176
+ ({"role": "edit"}, "receipt", True, "edit 命中 receipt"),
177
+ ({"role": "user"}, "receipt", False, "普通角色不进 receipt"),
178
+ ({"content_kind": "code"}, "receipt", False,
179
+ "无 role 的 code 不进 receipt(类型启发严格口径)"),
180
+ ({"layer": "knowledge"}, "main", True,
181
+ "缺 content_kind 按 text 对待(fail-open 惯例)"),
182
+ ({"layer": "knowledge", "content_kind": "ccg_marks"}, "main", False,
183
+ "marks 不进 main"),
184
+ ]
185
+ for fm, view, exp, why in cases:
186
+ got = M(fm, view)
187
+ _check("matches(%s, %s)=%s · %s" % (fm, view, exp, why),
188
+ got == exp, "got %r" % got)
189
+ try:
190
+ M({"layer": "knowledge"}, "bogus")
191
+ _check("非法 view ValueError(fail-closed)", False, "未抛出")
192
+ except ValueError:
193
+ _check("非法 view ValueError(fail-closed)", True)
194
+ except Exception as exc: # noqa: BLE001
195
+ _check("非法 view ValueError(fail-closed)", False, repr(exc))
196
+
197
+
198
+ def _t3_failclosed(cg):
199
+ print("\n【T3】库层 fail-closed(四入口非法 view)")
200
+ for name, fn in [
201
+ ("_candidates", lambda: cg._candidates(view="bogus")),
202
+ ("search", lambda: cg.search("极光", view="bogus")),
203
+ ("search_rrf", lambda: cg.search_rrf("极光", view="bogus")),
204
+ ("recall/non-rrf", lambda: cg.recall("极光", budget_tokens=200,
205
+ use_rrf=False, view="bogus")),
206
+ ("recall/rrf", lambda: cg.recall("极光", budget_tokens=200,
207
+ view="bogus")),
208
+ ]:
209
+ try:
210
+ fn()
211
+ _check("%s 非法 view ValueError" % name, False, "未抛出")
212
+ except ValueError:
213
+ _check("%s 非法 view ValueError" % name, True)
214
+ except Exception as exc: # noqa: BLE001
215
+ _check("%s 非法 view ValueError" % name, False, repr(exc))
216
+
217
+
218
+ def _t4_zero_drift(cg):
219
+ print("\n【T4】零行为变更(view=None ≡ 不传 view,逐位)")
220
+ a, _ma = cg.search("极光")
221
+ b, _mb = cg.search("极光", view=None)
222
+ _check("search:同 id 同序同分",
223
+ [(x[0]["id"], round(x[1], 9)) for x in a]
224
+ == [(x[0]["id"], round(x[1], 9)) for x in b])
225
+ ra, _mra = cg.search_rrf("极光")
226
+ rb, _mrb = cg.search_rrf("极光", view=None)
227
+ _check("search_rrf:同 id 同序同分",
228
+ [(x[0]["id"], round(x[1], 9)) for x in ra]
229
+ == [(x[0]["id"], round(x[1], 9)) for x in rb])
230
+ ca = cg._candidates()
231
+ cb = cg._candidates(view=None)
232
+ _check("_candidates:同 id 同序",
233
+ [_nid_of_entry(x) for x in ca] == [_nid_of_entry(x) for x in cb])
234
+ m1 = cg.recall("极光", budget_tokens=4000)
235
+ m2 = cg.recall("极光", budget_tokens=4000, view=None)
236
+ _check("recall:结构同型", type(m1) is type(m2) and bool(m1) == bool(m2))
237
+
238
+
239
+ def _t5_views(cg):
240
+ print("\n【T5】三视图端到端(候选层 + search 面)")
241
+ for view, exp in _EXPECT.items():
242
+ if view is None:
243
+ continue
244
+ got = {_nid_of_entry(e) for e in cg._candidates(view=view)}
245
+ _check("candidates(view=%s) 命中集 %s" % (view, sorted(exp)),
246
+ got == exp, "got %s" % sorted(got))
247
+ res, _meta = cg.search("极光", k=20, view=view)
248
+ got_s = set(_ids(res))
249
+ _check("search(view=%s) 命中集一致" % view, got_s == exp,
250
+ "got %s" % sorted(got_s))
251
+ # receipt 与 main 在 role 维度互补(本语料:工作角色 ↔ 非工作角色不重叠)
252
+ main_set = {_nid_of_entry(e) for e in cg._candidates(view="main")}
253
+ rec_set = {_nid_of_entry(e) for e in cg._candidates(view="receipt")}
254
+ _check("main × receipt 在 role 维度互斥", not (main_set & rec_set),
255
+ "交集 %s" % sorted(main_set & rec_set))
256
+ # 无 role 的 code 节点三视图皆不可见(类型启发严格口径,bench 校准通道)
257
+ _check("rv_e7(无 role 的 code)三视图皆不可见",
258
+ all("rv_e7" not in {_nid_of_entry(e) for e in cg._candidates(view=v)}
259
+ for v in ("main", "verifier", "receipt")))
260
+
261
+
262
+ def _t6_cache(cg):
263
+ print("\n【T6】缓存隔离(view 进键)")
264
+ hotcache.attach(cg)
265
+ r1, _ = cg.search_rrf("极光", k=20, view="main")
266
+ r2, _ = cg.search_rrf("极光", k=20, view="receipt")
267
+ ids_main = set(_ids(r1))
268
+ ids_rec = set(_ids(r2))
269
+ _check("同 query 不同 view 结果互斥(不串味)",
270
+ ids_main == _EXPECT["main"] and ids_rec == _EXPECT["receipt"],
271
+ "main=%s receipt=%s" % (sorted(ids_main), sorted(ids_rec)))
272
+ r3, m3 = cg.search_rrf("极光", k=20, view="main")
273
+ _check("同 view 复用缓存且结果一致",
274
+ m3.get("cached") is True
275
+ and _ids(r3) == _ids(r1),
276
+ "cached=%r" % m3.get("cached"))
277
+
278
+
279
+ def _t7_secure(cg):
280
+ print("\n【T7】Secure 层(签名同步 + 扩散路径兜底)")
281
+ out = cg._candidates(view="main")
282
+ _check("Secure._candidates(view=...) kw 透传不炸(签名同步守卫)",
283
+ isinstance(out, list)
284
+ and {_nid_of_entry(e) for e in out} == _EXPECT["main"],
285
+ "got %s" % sorted({_nid_of_entry(e) for e in out}))
286
+ res, _meta = cg.search_rrf("极光", k=50, view="main")
287
+ nodes = cg.index["nodes"]
288
+ _check("search_rrf 全称量词:结果全部满足 main 资格(扩散路径不泄漏)",
289
+ all(roleviews.matches(nodes[x[0]["id"]], "main") for x in res),
290
+ str([x[0]["id"] for x in res
291
+ if not roleviews.matches(nodes[x[0]["id"]], "main")]))
292
+ res2, _meta2 = cg.search_rrf("极光", k=50, view="receipt")
293
+ _check("search_rrf receipt 全称量词",
294
+ all(roleviews.matches(nodes[x[0]["id"]], "receipt") for x in res2)
295
+ and set(_ids(res2)) == _EXPECT["receipt"],
296
+ str(sorted(set(_ids(res2)))))
297
+
298
+
299
+ def _t8_recall(cg):
300
+ print("\n【T8】recall 双路径透传")
301
+ out = cg.recall("极光", budget_tokens=4000, view="main")
302
+ got = _collect_ids(out) & {nid for nid, *_ in _CORPUS}
303
+ _check("recall(view=main) 结果全在 main 资格集", got == _EXPECT["main"],
304
+ "got %s" % sorted(got))
305
+ out2 = cg.recall("极光", budget_tokens=4000, view="receipt")
306
+ got2 = _collect_ids(out2) & {nid for nid, *_ in _CORPUS}
307
+ _check("recall(view=receipt) 结果全在 receipt 资格集",
308
+ got2 == _EXPECT["receipt"], "got %s" % sorted(got2))
309
+
310
+
311
+ def _t9_mcp_surface():
312
+ print("\n【T9】MCP 透传面(源码断言,与 test_hyperedge T1③④ 同口径)")
313
+ src = _src("mcp_server.py")
314
+ # route 分支为 kwarg 形态(cg.search(..., view=a.get("view"))),
315
+ # read 分支为 _tkw dict 形态("view": a.get("view"))——各恰 1 处,
316
+ # 比 >= 合计更严:防未来重复注册或单分支漏接。
317
+ _check("route 分支透传 view=a.get(\"view\")(kwarg 形态恰 1 处)",
318
+ src.count('view=a.get("view")') == 1,
319
+ "count=%d" % src.count('view=a.get("view")'))
320
+ _check("read 分支透传 view(_tkw dict 形态恰 1 处)",
321
+ src.count('"view": a.get("view")') == 1,
322
+ "count=%d" % src.count('"view": a.get("view")'))
323
+ _check("schema 登记 view 项(角色化读取视图)",
324
+ 'view=_p("string", "read/route 的角色化读取视图' in src)
325
+
326
+
327
+ def main():
328
+ tmp = tempfile.mkdtemp(prefix="mdcg_roleviews_")
329
+ try:
330
+ cg = _mk_cg(tmp)
331
+ _seed(cg)
332
+ _t1_rules()
333
+ _t2_matches()
334
+ _t3_failclosed(cg)
335
+ _t4_zero_drift(cg)
336
+ _t5_views(cg)
337
+ _t6_cache(cg)
338
+ _t7_secure(cg)
339
+ _t8_recall(cg)
340
+ _t9_mcp_surface()
341
+ finally:
342
+ shutil.rmtree(tmp, ignore_errors=True)
343
+ print("\n==== roleviews 验收:%d ok / %d fail ====" % (_ok, len(_bad)))
344
+ for b in _bad:
345
+ print(" FAIL> %s" % b)
346
+ return 1 if _bad else 0
347
+
348
+
349
+ if __name__ == "__main__":
350
+ try:
351
+ sys.exit(main())
352
+ except Exception: # noqa: BLE001
353
+ traceback.print_exc()
354
+ sys.exit(2)