@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
@@ -1,147 +1,228 @@
1
- # -*- coding: utf-8 -*-
2
- """test_identity_attribution · 写入归属(writer/session/harness)与检索会话过滤
3
-
4
- 背景:多会话共用一个 root 时,读取记忆无法区分「本会话写的」与「其他会话写的」。
5
- 修复:写入自动归属(MdCGSecure 写路径注入 writer/session/harness)+ 检索
6
- session= 过滤(_candidates 一处生效:search/search_rrf/recall 全链路)+
7
- 审计/提案入队补 session + MDCG_SESSION env(部署侧固定会话归属)。
8
- 边界:归属是归因维度不参与授权;MCP 面不透传写入归属入参(客户端不得伪造),
9
- 库层调用方可显式覆盖(setdefault);writer 语义=最后写入者。
10
- """
11
- import glob
12
- import json
13
- import shutil
14
- import sys
15
- import tempfile
16
- import os
17
-
18
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
-
20
- # Windows cmd 默认 GBK 代码页:带圈数字 ⑪⑫⑬ 等不在 GBK 内,打印即
21
- # UnicodeEncodeError,且崩在断言之后、报告之前 —— 同一测试「因环境而异」。
22
- # 测试自带 UTF-8 兜底,不依赖调用方记得加 -X utf8(可复现性纪律)。
23
- if hasattr(sys.stdout, "reconfigure"):
24
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
25
-
26
- from md_cg.mdcg import MdCG
27
- from md_cg.mdcos import MdCGSecure
28
- from md_cg.security import DEFAULT_SENSITIVITY, Principal
29
-
30
- _ok = 0
31
- _fail = []
32
-
33
-
34
- def check(name, cond, detail=""):
35
- global _ok
36
- if cond:
37
- _ok += 1
38
- print("[PASS] " + name)
39
- else:
40
- _fail.append(name)
41
- print("[FAIL] %s · %s" % (name, str(detail)[:240]))
42
-
43
-
44
- def _p(actor, session):
45
- return Principal(actor=actor, clearance=DEFAULT_SENSITIVITY,
46
- can_write=True, can_admin=True, role="designer",
47
- session=session, harness="test-harness")
48
-
49
-
50
- def _last_jsonl(pattern):
51
- rec = None
52
- for path in glob.glob(pattern, recursive=True):
53
- with open(path, encoding="utf-8", errors="replace") as f:
54
- for line in f:
55
- if line.strip():
56
- rec = json.loads(line)
57
- return rec
58
-
59
-
60
- def _row(r):
61
- """search 原生返回 (node, score, qual, prov) tuple;dict 则原样。"""
62
- return r if isinstance(r, dict) else (r[0] if r else {})
63
-
64
-
65
- root = tempfile.mkdtemp(prefix="mdcg_attr_")
66
- try:
67
- cg = MdCGSecure(root, principal=_p("alice", "sess_A"))
68
-
69
- # ① 写入自动归属
70
- nid1 = cg.add("n1", "甲会话写的知识条目", layer="knowledge")
71
- fm1 = cg.get(nid1)["frontmatter"]
72
- check("①写入自动归属 writer/session",
73
- fm1.get("writer") == "alice" and fm1.get("session") == "sess_A", fm1)
74
- check("①b harness 归属落盘", fm1.get("harness") == "test-harness", fm1)
75
-
76
- # ② 索引同步(_stage 透传)
77
- e1 = cg.index["nodes"].get(nid1) or {}
78
- check("②索引带归属", e1.get("session") == "sess_A", e1)
79
-
80
- # ③ 显式覆盖(库层调用方可传;MCP 面不透传)
81
- nid2 = cg.add("n2", "显式会话标签条目", layer="knowledge", session="sess_custom")
82
- fm2 = cg.get(nid2)["frontmatter"]
83
- check("③显式覆盖尊重调用方", fm2.get("session") == "sess_custom", fm2)
84
-
85
- # ④ 更新刷新为最后写入者(另一身份写同 id)
86
- cg2 = MdCGSecure(root, principal=_p("bob", "sess_B"))
87
- cg2.add("n1", "乙会话更新了这条", layer="knowledge")
88
- fm1b = cg.get("n1")["frontmatter"]
89
- check("④更新刷新 writer/session",
90
- fm1b.get("writer") == "bob" and fm1b.get("session") == "sess_B", fm1b)
91
-
92
- # ⑤ 检索透出归属(node.frontmatter 零改动自动生效)
93
- res, _meta = cg2.search("乙会话更新")
94
- hit = next((_row(r) for r in res if _row(r).get("id") == "n1"), None)
95
- check("⑤检索透出归属",
96
- bool(hit) and (hit.get("frontmatter") or {}).get("session") == "sess_B",
97
- [_row(r).get("id") for r in res])
98
-
99
- # ⑥ search 会话过滤
100
- res_b, _ = cg2.search("知识", session="sess_B")
101
- ids_b = {_row(r).get("id") for r in res_b}
102
- check("⑥search 会话过滤", "n1" in ids_b and "n2" not in ids_b, ids_b)
103
-
104
- # ⑦ recall 会话过滤(RRF 主链)
105
- pack = cg2.recall("知识", session="sess_custom")
106
- hits = pack.get("pack") if isinstance(pack, dict) else None
107
- hid = {_row(h).get("id") for h in (hits or [])}
108
- check("⑦recall 会话过滤", "n2" in hid and "n1" not in hid,
109
- list(hid) or (list(pack.keys()) if isinstance(pack, dict) else type(pack)))
110
-
111
- # ⑧ 审计带 session
112
- last_audit = _last_jsonl(os.path.join(root, "**", "*audit*.jsonl"))
113
- check("⑧审计带 session",
114
- bool(last_audit) and last_audit.get("session") == "sess_B", last_audit)
115
-
116
- # ⑨ propose 入队带 session
117
- cg2.propose("n_prop", "提案内容", layer="knowledge")
118
- rec_inbox = _last_jsonl(os.path.join(root, "**", "*inbox*.jsonl"))
119
- check("⑨提案入队带 session",
120
- bool(rec_inbox) and rec_inbox.get("session") == "sess_B", rec_inbox)
121
-
122
- # ⑩ 旧节点兼容(基类写入、无归属字段)
123
- cg0 = MdCG(root)
124
- cg0.add("legacy", "旧库无归属节点的知识", layer="knowledge")
125
- cgs = MdCGSecure(root, principal=_p("carol", "sess_C"))
126
- res_all, _m = cgs.search("旧库无归属")
127
- check("⑩旧节点默认检索不受影响",
128
- any(_row(r).get("id") == "legacy" for r in res_all),
129
- [_row(r).get("id") for r in res_all])
130
- res_f, _ = cgs.search("旧库无归属", session="sess_C")
131
- check("⑩b 旧节点被会话过滤排除(不炸)",
132
- all(_row(r).get("id") != "legacy" for r in res_f),
133
- [_row(r).get("id") for r in res_f])
134
-
135
- # ⑪⑫ 负记忆归属
136
- rn = cg2.add_rejected("假设X", "已被证伪")
137
- fm_r = cg2.get(rn)["frontmatter"]
138
- check("⑪负记忆归属", fm_r.get("writer") == "bob"
139
- and fm_r.get("session") == "sess_B", fm_r)
140
- ru = cg2.add_unresolved("问题Y", "已有线索若干")
141
- fm_u = cg2.get(ru)["frontmatter"]
142
- check("⑫未解清单归属", fm_u.get("session") == "sess_B", fm_u)
143
-
144
- print("\n通过 %d / 失败 %d" % (_ok, len(_fail)))
145
- sys.exit(1 if _fail else 0)
146
- finally:
147
- shutil.rmtree(root, ignore_errors=True)
1
+ # -*- coding: utf-8 -*-
2
+ """test_identity_attribution · 写入归属(writer/session/harness)与会话可见性
3
+
4
+ 背景:多会话共用一个 root 时,读取记忆无法区分「本会话写的」与「其他会话写的」。
5
+ 修复:写入自动归属(MdCGSecure 写路径注入 writer/session/harness)+ 会话可见性
6
+ **分档判定**(issue #35 定稿:public/internal = 跨会话共享档;private/secret =
7
+ 绑定档,仅归属会话可见,设计者豁免;查询侧 session 参数只作归因过滤、不构成
8
+ 授权豁免)+ 审计/提案入队补 session + MDCG_SESSION env(部署侧固定会话归属)。
9
+ 边界:归属是归因维度不参与授权;MCP 面不透传写入归属入参(客户端不得伪造),
10
+ 库层调用方可显式覆盖(setdefault);writer 语义=最后写入者。
11
+ """
12
+ import glob
13
+ import json
14
+ import shutil
15
+ import sys
16
+ import tempfile
17
+ import os
18
+
19
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
20
+
21
+ # Windows cmd 默认 GBK 代码页:带圈数字 ⑪⑫⑬ 等不在 GBK 内,打印即
22
+ # UnicodeEncodeError,且崩在断言之后、报告之前 —— 同一测试「因环境而异」。
23
+ # 测试自带 UTF-8 兜底,不依赖调用方记得加 -X utf8(可复现性纪律)。
24
+ if hasattr(sys.stdout, "reconfigure"):
25
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
26
+
27
+ from md_cg.mdcg import MdCG
28
+ from md_cg.mdcos import MdCGSecure
29
+ from md_cg.security import DEFAULT_SENSITIVITY, Principal
30
+
31
+ _ok = 0
32
+ _fail = []
33
+
34
+
35
+ def check(name, cond, detail=""):
36
+ global _ok
37
+ if cond:
38
+ _ok += 1
39
+ print("[PASS] " + name)
40
+ else:
41
+ _fail.append(name)
42
+ print("[FAIL] %s · %s" % (name, str(detail)[:240]))
43
+
44
+
45
+ def _p(actor, session):
46
+ return Principal(actor=actor, clearance=DEFAULT_SENSITIVITY,
47
+ can_write=True, can_admin=True, role="designer",
48
+ session=session, harness="test-harness")
49
+
50
+
51
+ def _last_jsonl(pattern):
52
+ rec = None
53
+ for path in glob.glob(pattern, recursive=True):
54
+ with open(path, encoding="utf-8", errors="replace") as f:
55
+ for line in f:
56
+ if line.strip():
57
+ rec = json.loads(line)
58
+ return rec
59
+
60
+
61
+ def _row(r):
62
+ """search 原生返回 (node, score, qual, prov) tuple;dict 则原样。"""
63
+ return r if isinstance(r, dict) else (r[0] if r else {})
64
+
65
+
66
+ root = tempfile.mkdtemp(prefix="mdcg_attr_")
67
+ try:
68
+ cg = MdCGSecure(root, principal=_p("alice", "sess_A"))
69
+
70
+ # ① 写入自动归属
71
+ nid1 = cg.add("n1", "甲会话写的知识条目", layer="knowledge")
72
+ fm1 = cg.get(nid1)["frontmatter"]
73
+ check("①写入自动归属 writer/session",
74
+ fm1.get("writer") == "alice" and fm1.get("session") == "sess_A", fm1)
75
+ check("①b harness 归属落盘", fm1.get("harness") == "test-harness", fm1)
76
+
77
+ # ② 索引同步(_stage 透传)
78
+ e1 = cg.index["nodes"].get(nid1) or {}
79
+ check("②索引带归属", e1.get("session") == "sess_A", e1)
80
+
81
+ # ③ 显式覆盖(库层调用方可传;MCP 面不透传)
82
+ nid2 = cg.add("n2", "显式会话标签条目", layer="knowledge", session="sess_custom")
83
+ fm2 = cg.get(nid2)["frontmatter"]
84
+ check("③显式覆盖尊重调用方", fm2.get("session") == "sess_custom", fm2)
85
+
86
+ # ④ 更新刷新为最后写入者(另一身份写同 id)
87
+ cg2 = MdCGSecure(root, principal=_p("bob", "sess_B"))
88
+ cg2.add("n1", "乙会话更新了这条", layer="knowledge")
89
+ fm1b = cg.get("n1")["frontmatter"]
90
+ check("④更新刷新 writer/session",
91
+ fm1b.get("writer") == "bob" and fm1b.get("session") == "sess_B", fm1b)
92
+
93
+ # ⑤ 检索透出归属(node.frontmatter 零改动自动生效)
94
+ res, _meta = cg2.search("乙会话更新")
95
+ hit = next((_row(r) for r in res if _row(r).get("id") == "n1"), None)
96
+ check("⑤检索透出归属",
97
+ bool(hit) and (hit.get("frontmatter") or {}).get("session") == "sess_B",
98
+ [_row(r).get("id") for r in res])
99
+
100
+ # ⑥⑦ 会话语义(issue #35 定稿:**分档**判定,查询参数不构成豁免)
101
+ # · 共享档(public/internal)跨会话可见——n1/n2 都是 internal,故传任何
102
+ # session(他人会话 / "*" / 缺省)都应看得见;旧断言「按 session 过滤掉」
103
+ # 编码的是 #35 之前的契约(同口径见 test_p47_session_view 的 D1a/D2/D3)。
104
+ # · 绑定档(private/secret)仅归属会话——由 principal.session 判定,查询侧
105
+ # 自报的 session 不越权;can_admin(设计者)整体判断豁免。
106
+ res_b, _ = cg2.search("知识", session="sess_B")
107
+ ids_b = {_row(r).get("id") for r in res_b}
108
+ check("⑥a 共享档跨会话可见:显式 session 不缩小可见性",
109
+ "n1" in ids_b and "n2" in ids_b, ids_b)
110
+
111
+ # ⑥b~⑦ 绑定档对照(private 节点归 sess_B):
112
+ # · 跨身份(另一 actor/会话)不可见;
113
+ # · **同身份换会话**:非设计者不可见(绑定档生效);
114
+ # · **同身份换会话 + can_admin**:豁免可见(设计者整体判断)。
115
+ # 注:豁免的是「会话绑定判定」,不越过加密身份——private 内容按
116
+ # (tenant, actor) 派生 DEK,跨 actor 即便判定可见也解不开(get 无正文),
117
+ # 故可见性对照必须在同一 actor 内做。
118
+ w_priv = MdCGSecure(root, principal=Principal(
119
+ actor="bob", clearance="private", can_write=True, can_admin=False,
120
+ role="recorder", session="sess_B"))
121
+ try:
122
+ w_priv.add("n_priv_b", "乙会话私有知识条目", layer="knowledge",
123
+ sensitivity="private")
124
+ w_priv.flush()
125
+ finally:
126
+ w_priv.close()
127
+ carol = MdCGSecure(root, principal=Principal(
128
+ actor="carol", clearance="private", can_write=False, can_admin=False,
129
+ role="recorder", session="sess_C"))
130
+ try:
131
+ res_c, _ = carol.search("私有知识", session="sess_B") # 自报他人会话
132
+ ids_c = {_row(r).get("id") for r in res_c}
133
+ check("⑥b 绑定档按身份判定:自报他人会话不越权",
134
+ "n_priv_b" not in ids_c, ids_c)
135
+ finally:
136
+ carol.close()
137
+ bob_other = MdCGSecure(root, principal=Principal(
138
+ actor="bob", clearance="private", can_write=False, can_admin=False,
139
+ role="recorder", session="sess_OTHER"))
140
+ try:
141
+ res_o, _ = bob_other.search("私有知识")
142
+ check("⑥c 同一身份换会话:非设计者仍受绑定档约束",
143
+ "n_priv_b" not in {_row(r).get("id") for r in res_o},
144
+ [_row(r).get("id") for r in res_o])
145
+ finally:
146
+ bob_other.close()
147
+ bob_admin = MdCGSecure(root, principal=Principal(
148
+ actor="bob", clearance="private", can_write=False, can_admin=True,
149
+ role="designer", session="sess_OTHER"))
150
+ try:
151
+ res_d, _ = bob_admin.search("私有知识", session="sess_C")
152
+ check("⑥d 同一身份 + 设计者(can_admin):豁免绑定档",
153
+ "n_priv_b" in {_row(r).get("id") for r in res_d},
154
+ [_row(r).get("id") for r in res_d])
155
+
156
+ # ⑦ recall(RRF 主链)同口径:绑定档判定不因查询 session 变化
157
+ pack = bob_admin.recall("私有知识", session="sess_custom")
158
+ hits = pack.get("pack") if isinstance(pack, dict) else None
159
+ hid = {_row(h).get("id") for h in (hits or [])}
160
+ check("⑦recall 同口径:设计者可见绑定档(查询 session 不改判定)",
161
+ "n_priv_b" in hid,
162
+ list(hid) or (list(pack.keys()) if isinstance(pack, dict)
163
+ else type(pack)))
164
+ finally:
165
+ bob_admin.close()
166
+
167
+ # ⑧ 审计带 session
168
+ last_audit = _last_jsonl(os.path.join(root, "**", "*audit*.jsonl"))
169
+ check("⑧审计带 session",
170
+ bool(last_audit) and last_audit.get("session") == "sess_B", last_audit)
171
+
172
+ # ⑨ propose 入队带 session
173
+ cg2.propose("n_prop", "提案内容", layer="knowledge")
174
+ rec_inbox = _last_jsonl(os.path.join(root, "**", "*inbox*.jsonl"))
175
+ check("⑨提案入队带 session",
176
+ bool(rec_inbox) and rec_inbox.get("session") == "sess_B", rec_inbox)
177
+
178
+ # ⑩ 旧节点兼容(基类写入、无归属字段)
179
+ # · 共享档:跨会话可见(#35 定稿);
180
+ # · 绑定档但无归属:无法证明归属 → fail-closed,仅设计者(can_admin)可见。
181
+ cg0 = MdCG(root)
182
+ cg0.add("legacy", "旧库无归属节点的知识", layer="knowledge")
183
+ cg0.add("legacy_priv", "旧库无归属的私有知识条目", layer="knowledge",
184
+ sensitivity="private")
185
+ cgs = MdCGSecure(root, principal=_p("carol", "sess_C"))
186
+ res_all, _m = cgs.search("旧库无归属")
187
+ check("⑩旧节点默认检索不受影响",
188
+ any(_row(r).get("id") == "legacy" for r in res_all),
189
+ [_row(r).get("id") for r in res_all])
190
+ res_f, _ = cgs.search("旧库无归属", session="sess_C")
191
+ check("⑩b 旧节点(无归属·共享档)跨会话可见(不炸)",
192
+ any(_row(r).get("id") == "legacy" for r in res_f),
193
+ [_row(r).get("id") for r in res_f])
194
+ # ⑩c/⑩d 绑定档无归属的 fail-closed(同 clearance 的非设计者不可读 / 设计者豁免)
195
+ peak = MdCGSecure(root, principal=Principal(
196
+ actor="erin", clearance="private", can_write=False, can_admin=False,
197
+ role="recorder", session="sess_E"))
198
+ try:
199
+ res_p, _ = peak.search("旧库无归属")
200
+ ids_p = [_row(r).get("id") for r in res_p]
201
+ check("⑩c 绑定档无归属 fail-closed(同 clearance 非设计者不可读)",
202
+ "legacy_priv" not in ids_p and "legacy" in ids_p, ids_p)
203
+ finally:
204
+ peak.close()
205
+ lim = MdCGSecure(root, principal=Principal(
206
+ actor="dave2", clearance="private", can_write=True, can_admin=True,
207
+ role="designer", session="sess_D"))
208
+ try:
209
+ res_l, _ = lim.search("旧库无归属")
210
+ check("⑩d 设计者可见无归属绑定档(整体判断豁免)",
211
+ "legacy_priv" in [_row(r).get("id") for r in res_l],
212
+ [_row(r).get("id") for r in res_l])
213
+ finally:
214
+ lim.close()
215
+
216
+ # ⑪⑫ 负记忆归属
217
+ rn = cg2.add_rejected("假设X", "已被证伪")
218
+ fm_r = cg2.get(rn)["frontmatter"]
219
+ check("⑪负记忆归属", fm_r.get("writer") == "bob"
220
+ and fm_r.get("session") == "sess_B", fm_r)
221
+ ru = cg2.add_unresolved("问题Y", "已有线索若干")
222
+ fm_u = cg2.get(ru)["frontmatter"]
223
+ check("⑫未解清单归属", fm_u.get("session") == "sess_B", fm_u)
224
+
225
+ print("\n通过 %d / 失败 %d" % (_ok, len(_fail)))
226
+ sys.exit(1 if _fail else 0)
227
+ finally:
228
+ shutil.rmtree(root, ignore_errors=True)