@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,249 +1,259 @@
1
- # -*- coding: utf-8 -*-
2
- """记忆演化分支(branches,Pi 移植④)验收:交接文档 §3④。
3
-
4
- 蓝图验收两条:
5
- ① fork → 分支改写 → merge 后主支节点 derived_from 链完整可回溯;
6
- ② 放弃分支不污染主支检索(召回主支时分支节点零命中)。
7
- """
8
-
9
- import os
10
- import shutil
11
- import sys
12
- import tempfile
13
- import traceback
14
-
15
- from . import branches
16
- from .mdcos import MdCGSecure
17
- from .security import AccessDenied, Principal
18
-
19
- _ok = 0
20
- _bad = []
21
-
22
-
23
- def _check(name, cond, detail=""):
24
- global _ok
25
- if cond:
26
- _ok += 1
27
- print(" ok %s" % name)
28
- else:
29
- _bad.append("%s %s" % (name, detail))
30
- print(" FAIL %s %s" % (name, detail))
31
-
32
-
33
- def _mk_cg(tmp, role="designer", seq=[0]):
34
- seq[0] += 1
35
- root = os.path.join(tmp, "root_%02d_%s" % (seq[0], role))
36
- p = Principal(tenant="default", actor="t_" + role, role=role,
37
- can_write=(role != "guest"),
38
- can_admin=(role == "designer"))
39
- return MdCGSecure(root, principal=p)
40
-
41
-
42
- def _fm(cg, nid):
43
- rec = cg.get(nid) or {}
44
- return rec.get("frontmatter") or {}
45
-
46
-
47
- def _content(cg, nid):
48
- return ((cg.get(nid) or {}).get("content") or "").strip()
49
-
50
-
51
- def _hits(cg, q, **kw):
52
- """命中集 id 清单(零污染断言的口径:分支 nid 不出现,而非结果为空——
53
- T3 全扫兜底本就会返回低分主支节点,那是检索器既有行为不是污染)。"""
54
- return [h[0].get("id") for h in cg.search(q, **kw)[0]]
55
-
56
-
57
- def main():
58
- tmp = tempfile.mkdtemp(prefix="branches_")
59
- try:
60
- _run(tmp)
61
- except Exception:
62
- traceback.print_exc()
63
- _bad.append("未捕获异常")
64
- finally:
65
- shutil.rmtree(tmp, ignore_errors=True)
66
- print("\nbranches 验收:%d 通过%s" % (
67
- _ok, (",%d 失败:%s" % (len(_bad), "; ".join(_bad))) if _bad else ""))
68
- return 1 if _bad else 0
69
-
70
-
71
- def _run(tmp):
72
- # ---------- 1 fork:副本血缘 + 防呆 + 幂等 ----------
73
- cg = _mk_cg(tmp)
74
- cg.add("mem_a", "A内容alpha", layer="knowledge", verification_basis="test",
75
- consistency=False)
76
- cg.add("mem_b", "B内容beta", layer="contextual", verification_basis="test",
77
- consistency=False, derived_from=["anc1"], relation="merged_from")
78
- r = branches.fork(cg, ["mem_a", "mem_b"], branch_id="exp1", note="实验一")
79
- _check("fork成功", r.get("ok") is True and r.get("branch_id") == "exp1",
80
- repr(r))
81
- fm_a = _fm(cg, "mem_a@exp1")
82
- _check("副本血缘", fm_a.get("branch_id") == "exp1"
83
- and fm_a.get("branched_from") == "mem_a"
84
- and fm_a.get("derived_from") == ["mem_a"]
85
- and fm_a.get("derived_relation") == "split_from", repr(fm_a))
86
- _check("副本内容继承", _content(cg, "mem_a@exp1") == "A内容alpha")
87
- _check("fork空入参拒", branches.fork(cg, []).get("ok") is False)
88
- _check("fork非法id拒", branches.fork(
89
- cg, ["mem_a"], branch_id="BAD ID!").get("ok") is False)
90
- r2 = branches.fork(cg, ["mem_a", "mem_a", "mem_c"], branch_id="exp1")
91
- _check("fork幂等skipped", r2.get("ok") is False
92
- and len([s for s in r2.get("skipped", [])
93
- if "已存在" in s.get("why", "")]) == 2
94
- and len([s for s in r2.get("skipped", [])
95
- if "不存在" in s.get("why", "")]) == 1, repr(r2))
96
-
97
- # ---------- 2 蓝图验收②:主支检索零污染 ----------
98
- # 分支上改写副本走 rewrite(唯一正路)——add 是全量重建 fm,裸 add
99
- # 不重传 branch_id 会让副本在索引里退化成主支(实验泄漏进主支检索)。
100
- rr = branches.rewrite(cg, "mem_a@exp1", "A改写exp版")
101
- _check("rewrite改写成功", rr.get("ok") is True
102
- and rr.get("branch_id") == "exp1", repr(rr))
103
- # rewrite 防呆:非分支/不存在拒;归属与血缘不可篡改(extra 伪造键被剥离)
104
- _check("rewrite非分支拒", branches.rewrite(
105
- cg, "mem_a", "x").get("ok") is False)
106
- _check("rewrite不存在拒", branches.rewrite(
107
- cg, "mem_z@exp1", "x").get("ok") is False)
108
- branches.rewrite(cg, "mem_a@exp1", "A改写exp版", branch_id="伪造",
109
- branched_from="mem_zzz", derived_from=["fake"])
110
- fm_rr = _fm(cg, "mem_a@exp1")
111
- _check("rewrite归属不可篡改", fm_rr.get("branch_id") == "exp1"
112
- and fm_rr.get("branched_from") == "mem_a"
113
- and fm_rr.get("derived_from") == ["mem_a"], repr(fm_rr))
114
- _check("主支检索分支内容零命中",
115
- "mem_a@exp1" not in _hits(cg, "A改写exp版"),
116
- repr(_hits(cg, "A改写exp版")))
117
- _check("主支检索原内容仍命中", bool(cg.search("A内容alpha")[0]))
118
- _check("分支内检索命中改写",
119
- bool(branches.search(cg, "A改写exp版", "exp1")[0]),
120
- repr(branches.search(cg, "A改写exp版", "exp1")))
121
- _check("分支内检索主支也可见",
122
- bool(branches.search(cg, "B内容beta", "exp1")[0]))
123
- # 其他分支互不可见(用各自独有内容词验:对方分支 nid 不在命中集)
124
- branches.fork(cg, ["mem_a"], branch_id="exp2")
125
- branches.rewrite(cg, "mem_a@exp2", "A改写exp2版")
126
- h_e1 = [h[0].get("id") for h in branches.search(cg, "A改写exp2版",
127
- "exp1")[0]]
128
- h_e2 = [h[0].get("id") for h in branches.search(cg, "A改写exp版",
129
- "exp2")[0]]
130
- _check("跨分支隐身",
131
- "mem_a@exp2" not in h_e1 and "mem_a@exp1" not in h_e2,
132
- "e1=%r e2=%r" % (h_e1, h_e2))
133
-
134
- # ---------- 3 蓝图验收①:merge 溯源链完整 ----------
135
- r = branches.merge(cg, "exp1", reason="实验通过")
136
- _check("merge成功", r.get("ok") is True and len(r.get("merged", [])) == 2,
137
- repr(r))
138
- _check("主支内容已回写", _content(cg, "mem_a") == "A改写exp版")
139
- fm_a = _fm(cg, "mem_a")
140
- chain = fm_a.get("derived_from") or []
141
- _check("主支链追加分支且原链保留",
142
- "mem_a@exp1" in chain
143
- and fm_a.get("derived_relation") == "merged_from"
144
- and fm_a.get("merged_from_branch") == "exp1", repr(fm_a))
145
- anc = _fm(cg, "mem_b").get("derived_from") or []
146
- _check("原链保留(mem_b)", "anc1" in anc and "mem_b@exp1" in anc, repr(anc))
147
- _check("merge后主支检索命中回写", bool(cg.search("A改写exp版")[0]))
148
- _check("merge幂等安全", branches.merge(cg, "exp1").get("ok") is True
149
- and _content(cg, "mem_a") == "A改写exp版")
150
- r = branches.merge(cg, "br_void")
151
- _check("merge空分支拒", r.get("ok") is False)
152
-
153
- # merge 时主支不可达 → skipped 不炸(索引与脏缓存一并摘除才是完整模拟)
154
- cg2 = _mk_cg(tmp)
155
- cg2.add("mem_x", "X内容", layer="knowledge", verification_basis="test",
156
- consistency=False)
157
- branches.fork(cg2, ["mem_x"], branch_id="orphan")
158
- (cg2.index["nodes"]).pop("mem_x", None)
159
- (cg2._dirty or {}).pop("mem_x", None)
160
- r = branches.merge(cg2, "orphan")
161
- _check("merge主支缺失skipped", r.get("ok") is False
162
- and len(r.get("skipped", [])) == 1, repr(r))
163
-
164
- # ---------- 4 discard:教训必填 + 冷归档 + 权限 ----------
165
- cg3 = _mk_cg(tmp)
166
- cg3.add("mem_k", "K内容", layer="knowledge", verification_basis="test",
167
- consistency=False)
168
- branches.fork(cg3, ["mem_k"], branch_id="dead")
169
- branches.rewrite(cg3, "mem_k@dead", "K改写dead版")
170
- r = branches.discard(cg3, "dead", "假设有错,教训:不要急")
171
- _check("discard缺标记拒", r.get("ok") is False
172
- and set(r.get("missing", [])) == {"分支假设", "实验结果"}
173
- and r.get("required") == list(branches.BRANCH_MARKS), repr(r))
174
- # guest 无 can_admin → 结构性拒绝(guest 无写权建不了节点,
175
- # 分支由 designer 建好并 flush 后,guest 开同一 root 只试 discard)
176
- branches.fork(cg3, ["mem_k"], branch_id="guest_br")
177
- cg3.flush()
178
- cg_guest = MdCGSecure(cg3.root,
179
- principal=Principal(tenant="default",
180
- actor="t_guest", role="guest",
181
- can_write=False,
182
- can_admin=False))
183
- try:
184
- branches.discard(cg_guest, "guest_br",
185
- "分支假设x 实验结果y 教训z")
186
- _check("discard需can_admin", False, "guest 未被拒")
187
- except AccessDenied:
188
- _check("discard需can_admin", True)
189
- except Exception as e: # noqa: BLE001
190
- _check("discard需can_admin", False,
191
- "异常=%s:%s" % (type(e).__name__, e))
192
- # designer 全流程
193
- r = branches.discard(cg3, "dead",
194
- "分支假设:覆盖写更快 实验结果:丢失血缘 教训:血缘必保留")
195
- _check("discard成功", r.get("ok") is True and r.get("moved") == 1, repr(r))
196
- _check("教训节点入库", bool(_fm(cg3, "branch_summary_dead")))
197
- _check("教训节点可检索", bool(cg3.search("血缘必保留")[0]),
198
- repr(cg3.search("血缘必保留")))
199
- cold = os.path.join(cg3.root, branches.COLD_DIR, "dead",
200
- "mem_k@dead.md")
201
- _check("分支文件冷区保留", os.path.exists(cold), cold)
202
- _check("索引已摘除", "mem_k@dead" not in cg3.index["nodes"])
203
- _check("list_branches已不含", all(
204
- b.get("branch_id") != "dead"
205
- for b in branches.list_branches(cg3).get("branches", [])))
206
- _check("discard空分支拒",
207
- branches.discard(cg3, "dead", "分支假设a 实验结果b 教训c").get("ok")
208
- is False)
209
-
210
- # ---------- 5 list_branches 聚合 + insight act 分派冒烟 ----------
211
- cg4 = _mk_cg(tmp)
212
- cg4.add("mem_p", "P内容", layer="knowledge", verification_basis="test",
213
- consistency=False)
214
- branches.fork(cg4, ["mem_p"], branch_id="live1")
215
- lst = branches.list_branches(cg4)
216
- g = {b.get("branch_id"): b for b in lst.get("branches", [])}.get("live1")
217
- _check("list_branches聚合", g is not None and g.get("nodes") == ["mem_p@live1"]
218
- and g.get("branched_from") == ["mem_p"], repr(lst))
219
- r = cg4.insight("fork", node_ids=["mem_p"], branch_id="live2", note="n")
220
- _check("insight act=fork", r.get("ok") is True
221
- and r.get("branch_id") == "live2", repr(r))
222
- r = cg4.insight("branches")
223
- _check("insight act=branches", r.get("ok") is True
224
- and len(r.get("branches", [])) == 2, repr(r))
225
- r = cg4.insight("branch_rewrite", node_id="mem_p@live2", content="P内容v2")
226
- _check("insight act=branch_rewrite", r.get("ok") is True
227
- and _content(cg4, "mem_p@live2") == "P内容v2", repr(r))
228
- r = cg4.insight("branch_search", content="P内容v2", branch_id="live2")
229
- _check("insight act=branch_search", bool(r[0]), repr(r))
230
- r = cg4.insight("branch_merge", branch_id="live2", reason="r")
231
- _check("insight act=branch_merge", r.get("ok") is True
232
- and _content(cg4, "mem_p") == "P内容v2", repr(r))
233
- r = cg4.insight("branch_discard", branch_id="live2",
234
- content="分支假设p 实验结果q 教训r")
235
- _check("insight act=branch_discard", r.get("ok") is True, repr(r))
236
-
237
- # ---------- 6 session 过滤正交不回归 ----------
238
- cg5 = _mk_cg(tmp)
239
- cg5.add("mem_s1", "会话一内容", layer="knowledge", verification_basis="test",
240
- consistency=False)
241
- branches.fork(cg5, ["mem_s1"], branch_id="sx")
242
- res, meta = cg5.search("会话一内容", session="sess_zzz")
243
- _check("session过滤与branch过滤正交", not res, repr(res))
244
- res, meta = cg5.search("会话一内容")
245
- _check("默认检索主支正常", bool(res), repr(meta))
246
-
247
-
248
- if __name__ == "__main__":
249
- sys.exit(main())
1
+ # -*- coding: utf-8 -*-
2
+ """记忆演化分支(branches,Pi 移植④)验收:交接文档 §3④。
3
+
4
+ 蓝图验收两条:
5
+ ① fork → 分支改写 → merge 后主支节点 derived_from 链完整可回溯;
6
+ ② 放弃分支不污染主支检索(召回主支时分支节点零命中)。
7
+ """
8
+
9
+ import os
10
+ import shutil
11
+ import sys
12
+ import tempfile
13
+ import traceback
14
+
15
+ from . import branches
16
+ from .mdcos import MdCGSecure
17
+ from .security import AccessDenied, Principal
18
+
19
+ _ok = 0
20
+ _bad = []
21
+
22
+
23
+ def _check(name, cond, detail=""):
24
+ global _ok
25
+ if cond:
26
+ _ok += 1
27
+ print(" ok %s" % name)
28
+ else:
29
+ _bad.append("%s %s" % (name, detail))
30
+ print(" FAIL %s %s" % (name, detail))
31
+
32
+
33
+ def _mk_cg(tmp, role="designer", seq=[0]):
34
+ seq[0] += 1
35
+ root = os.path.join(tmp, "root_%02d_%s" % (seq[0], role))
36
+ p = Principal(tenant="default", actor="t_" + role, role=role,
37
+ can_write=(role != "guest"),
38
+ can_admin=(role == "designer"))
39
+ return MdCGSecure(root, principal=p)
40
+
41
+
42
+ def _fm(cg, nid):
43
+ rec = cg.get(nid) or {}
44
+ return rec.get("frontmatter") or {}
45
+
46
+
47
+ def _content(cg, nid):
48
+ return ((cg.get(nid) or {}).get("content") or "").strip()
49
+
50
+
51
+ def _hits(cg, q, **kw):
52
+ """命中集 id 清单(零污染断言的口径:分支 nid 不出现,而非结果为空——
53
+ T3 全扫兜底本就会返回低分主支节点,那是检索器既有行为不是污染)。"""
54
+ return [h[0].get("id") for h in cg.search(q, **kw)[0]]
55
+
56
+
57
+ def main():
58
+ tmp = tempfile.mkdtemp(prefix="branches_")
59
+ try:
60
+ _run(tmp)
61
+ except Exception:
62
+ traceback.print_exc()
63
+ _bad.append("未捕获异常")
64
+ finally:
65
+ shutil.rmtree(tmp, ignore_errors=True)
66
+ print("\nbranches 验收:%d 通过%s" % (
67
+ _ok, (",%d 失败:%s" % (len(_bad), "; ".join(_bad))) if _bad else ""))
68
+ return 1 if _bad else 0
69
+
70
+
71
+ def _run(tmp):
72
+ # ---------- 1 fork:副本血缘 + 防呆 + 幂等 ----------
73
+ cg = _mk_cg(tmp)
74
+ cg.add("mem_a", "A内容alpha", layer="knowledge", verification_basis="test",
75
+ consistency=False)
76
+ cg.add("mem_b", "B内容beta", layer="contextual", verification_basis="test",
77
+ consistency=False, derived_from=["anc1"], relation="merged_from")
78
+ r = branches.fork(cg, ["mem_a", "mem_b"], branch_id="exp1", note="实验一")
79
+ _check("fork成功", r.get("ok") is True and r.get("branch_id") == "exp1",
80
+ repr(r))
81
+ fm_a = _fm(cg, "mem_a@exp1")
82
+ _check("副本血缘", fm_a.get("branch_id") == "exp1"
83
+ and fm_a.get("branched_from") == "mem_a"
84
+ and fm_a.get("derived_from") == ["mem_a"]
85
+ and fm_a.get("derived_relation") == "split_from", repr(fm_a))
86
+ _check("副本内容继承", _content(cg, "mem_a@exp1") == "A内容alpha")
87
+ _check("fork空入参拒", branches.fork(cg, []).get("ok") is False)
88
+ _check("fork非法id拒", branches.fork(
89
+ cg, ["mem_a"], branch_id="BAD ID!").get("ok") is False)
90
+ r2 = branches.fork(cg, ["mem_a", "mem_a", "mem_c"], branch_id="exp1")
91
+ _check("fork幂等skipped", r2.get("ok") is False
92
+ and len([s for s in r2.get("skipped", [])
93
+ if "已存在" in s.get("why", "")]) == 2
94
+ and len([s for s in r2.get("skipped", [])
95
+ if "不存在" in s.get("why", "")]) == 1, repr(r2))
96
+
97
+ # ---------- 2 蓝图验收②:主支检索零污染 ----------
98
+ # 分支上改写副本走 rewrite(唯一正路)——add 是全量重建 fm,裸 add
99
+ # 不重传 branch_id 会让副本在索引里退化成主支(实验泄漏进主支检索)。
100
+ rr = branches.rewrite(cg, "mem_a@exp1", "A改写exp版")
101
+ _check("rewrite改写成功", rr.get("ok") is True
102
+ and rr.get("branch_id") == "exp1", repr(rr))
103
+ # rewrite 防呆:非分支/不存在拒;归属与血缘不可篡改(extra 伪造键被剥离)
104
+ _check("rewrite非分支拒", branches.rewrite(
105
+ cg, "mem_a", "x").get("ok") is False)
106
+ _check("rewrite不存在拒", branches.rewrite(
107
+ cg, "mem_z@exp1", "x").get("ok") is False)
108
+ branches.rewrite(cg, "mem_a@exp1", "A改写exp版", branch_id="伪造",
109
+ branched_from="mem_zzz", derived_from=["fake"])
110
+ fm_rr = _fm(cg, "mem_a@exp1")
111
+ _check("rewrite归属不可篡改", fm_rr.get("branch_id") == "exp1"
112
+ and fm_rr.get("branched_from") == "mem_a"
113
+ and fm_rr.get("derived_from") == ["mem_a"], repr(fm_rr))
114
+ _check("主支检索分支内容零命中",
115
+ "mem_a@exp1" not in _hits(cg, "A改写exp版"),
116
+ repr(_hits(cg, "A改写exp版")))
117
+ _check("主支检索原内容仍命中", bool(cg.search("A内容alpha")[0]))
118
+ _check("分支内检索命中改写",
119
+ bool(branches.search(cg, "A改写exp版", "exp1")[0]),
120
+ repr(branches.search(cg, "A改写exp版", "exp1")))
121
+ _check("分支内检索主支也可见",
122
+ bool(branches.search(cg, "B内容beta", "exp1")[0]))
123
+ # 其他分支互不可见(用各自独有内容词验:对方分支 nid 不在命中集)
124
+ branches.fork(cg, ["mem_a"], branch_id="exp2")
125
+ branches.rewrite(cg, "mem_a@exp2", "A改写exp2版")
126
+ h_e1 = [h[0].get("id") for h in branches.search(cg, "A改写exp2版",
127
+ "exp1")[0]]
128
+ h_e2 = [h[0].get("id") for h in branches.search(cg, "A改写exp版",
129
+ "exp2")[0]]
130
+ _check("跨分支隐身",
131
+ "mem_a@exp2" not in h_e1 and "mem_a@exp1" not in h_e2,
132
+ "e1=%r e2=%r" % (h_e1, h_e2))
133
+
134
+ # ---------- 3 蓝图验收①:merge 溯源链完整 ----------
135
+ r = branches.merge(cg, "exp1", reason="实验通过")
136
+ _check("merge成功", r.get("ok") is True and len(r.get("merged", [])) == 2,
137
+ repr(r))
138
+ _check("主支内容已回写", _content(cg, "mem_a") == "A改写exp版")
139
+ fm_a = _fm(cg, "mem_a")
140
+ chain = fm_a.get("derived_from") or []
141
+ _check("主支链追加分支且原链保留",
142
+ "mem_a@exp1" in chain
143
+ and fm_a.get("derived_relation") == "merged_from"
144
+ and fm_a.get("merged_from_branch") == "exp1", repr(fm_a))
145
+ anc = _fm(cg, "mem_b").get("derived_from") or []
146
+ _check("原链保留(mem_b)", "anc1" in anc and "mem_b@exp1" in anc, repr(anc))
147
+ _check("merge后主支检索命中回写", bool(cg.search("A改写exp版")[0]))
148
+ _check("merge幂等安全", branches.merge(cg, "exp1").get("ok") is True
149
+ and _content(cg, "mem_a") == "A改写exp版")
150
+ r = branches.merge(cg, "br_void")
151
+ _check("merge空分支拒", r.get("ok") is False)
152
+
153
+ # merge 时主支不可达 → skipped 不炸(索引与脏缓存一并摘除才是完整模拟)
154
+ cg2 = _mk_cg(tmp)
155
+ cg2.add("mem_x", "X内容", layer="knowledge", verification_basis="test",
156
+ consistency=False)
157
+ branches.fork(cg2, ["mem_x"], branch_id="orphan")
158
+ (cg2.index["nodes"]).pop("mem_x", None)
159
+ (cg2._dirty or {}).pop("mem_x", None)
160
+ r = branches.merge(cg2, "orphan")
161
+ _check("merge主支缺失skipped", r.get("ok") is False
162
+ and len(r.get("skipped", [])) == 1, repr(r))
163
+
164
+ # ---------- 4 discard:教训必填 + 冷归档 + 权限 ----------
165
+ cg3 = _mk_cg(tmp)
166
+ cg3.add("mem_k", "K内容", layer="knowledge", verification_basis="test",
167
+ consistency=False)
168
+ branches.fork(cg3, ["mem_k"], branch_id="dead")
169
+ branches.rewrite(cg3, "mem_k@dead", "K改写dead版")
170
+ r = branches.discard(cg3, "dead", "假设有错,教训:不要急")
171
+ _check("discard缺标记拒", r.get("ok") is False
172
+ and set(r.get("missing", [])) == {"分支假设", "实验结果"}
173
+ and r.get("required") == list(branches.BRANCH_MARKS), repr(r))
174
+ # guest 无 can_admin → 结构性拒绝(guest 无写权建不了节点,
175
+ # 分支由 designer 建好并 flush 后,guest 开同一 root 只试 discard)
176
+ branches.fork(cg3, ["mem_k"], branch_id="guest_br")
177
+ cg3.flush()
178
+ cg_guest = MdCGSecure(cg3.root,
179
+ principal=Principal(tenant="default",
180
+ actor="t_guest", role="guest",
181
+ can_write=False,
182
+ can_admin=False))
183
+ try:
184
+ branches.discard(cg_guest, "guest_br",
185
+ "分支假设x 实验结果y 教训z")
186
+ _check("discard需can_admin", False, "guest 未被拒")
187
+ except AccessDenied:
188
+ _check("discard需can_admin", True)
189
+ except Exception as e: # noqa: BLE001
190
+ _check("discard需can_admin", False,
191
+ "异常=%s:%s" % (type(e).__name__, e))
192
+ # designer 全流程
193
+ r = branches.discard(cg3, "dead",
194
+ "分支假设:覆盖写更快 实验结果:丢失血缘 教训:血缘必保留")
195
+ _check("discard成功", r.get("ok") is True and r.get("moved") == 1, repr(r))
196
+ _check("教训节点入库", bool(_fm(cg3, "branch_summary_dead")))
197
+ _check("教训节点可检索", bool(cg3.search("血缘必保留")[0]),
198
+ repr(cg3.search("血缘必保留")))
199
+ cold = os.path.join(cg3.root, branches.COLD_DIR, "dead",
200
+ "mem_k@dead.md")
201
+ _check("分支文件冷区保留", os.path.exists(cold), cold)
202
+ _check("索引已摘除", "mem_k@dead" not in cg3.index["nodes"])
203
+ _check("list_branches已不含", all(
204
+ b.get("branch_id") != "dead"
205
+ for b in branches.list_branches(cg3).get("branches", [])))
206
+ _check("discard空分支拒",
207
+ branches.discard(cg3, "dead", "分支假设a 实验结果b 教训c").get("ok")
208
+ is False)
209
+
210
+ # ---------- 5 list_branches 聚合 + insight act 分派冒烟 ----------
211
+ cg4 = _mk_cg(tmp)
212
+ cg4.add("mem_p", "P内容", layer="knowledge", verification_basis="test",
213
+ consistency=False)
214
+ branches.fork(cg4, ["mem_p"], branch_id="live1")
215
+ lst = branches.list_branches(cg4)
216
+ g = {b.get("branch_id"): b for b in lst.get("branches", [])}.get("live1")
217
+ _check("list_branches聚合", g is not None and g.get("nodes") == ["mem_p@live1"]
218
+ and g.get("branched_from") == ["mem_p"], repr(lst))
219
+ r = cg4.insight("fork", node_ids=["mem_p"], branch_id="live2", note="n")
220
+ _check("insight act=fork", r.get("ok") is True
221
+ and r.get("branch_id") == "live2", repr(r))
222
+ r = cg4.insight("branches")
223
+ _check("insight act=branches", r.get("ok") is True
224
+ and len(r.get("branches", [])) == 2, repr(r))
225
+ r = cg4.insight("branch_rewrite", node_id="mem_p@live2", content="P内容v2")
226
+ _check("insight act=branch_rewrite", r.get("ok") is True
227
+ and _content(cg4, "mem_p@live2") == "P内容v2", repr(r))
228
+ r = cg4.insight("branch_search", content="P内容v2", branch_id="live2")
229
+ _check("insight act=branch_search", bool(r[0]), repr(r))
230
+ r = cg4.insight("branch_merge", branch_id="live2", reason="r")
231
+ _check("insight act=branch_merge", r.get("ok") is True
232
+ and _content(cg4, "mem_p") == "P内容v2", repr(r))
233
+ r = cg4.insight("branch_discard", branch_id="live2",
234
+ content="分支假设p 实验结果q 教训r")
235
+ _check("insight act=branch_discard", r.get("ok") is True, repr(r))
236
+
237
+ # ---------- 6 分支可见性 × 会话参数(正交,不回归) ----------
238
+ # issue #35 起:共享档(internal 默认)跨会话可见 ⇒ 查询侧 session 参数
239
+ # **不再改变可见性**(同口径见 test_p47_session_view 的 D1a/D2/D3)。本段要守
240
+ # 的是「分支过滤与 session 参数正交」:传别人的会话不得把默认隐身的**分支
241
+ # 副本**带回来;显式 branch= 才可见;主支节点两种情况都在。
242
+ cg5 = _mk_cg(tmp)
243
+ cg5.add("mem_s1", "会话一内容", layer="knowledge", verification_basis="test",
244
+ consistency=False)
245
+ branches.fork(cg5, ["mem_s1"], branch_id="sx")
246
+ ids_other = _hits(cg5, "会话一内容", session="sess_zzz")
247
+ _check("共享档跨会话可见:显式他人 session 不缩小可见性",
248
+ "mem_s1" in ids_other, repr(ids_other))
249
+ _check("分支副本默认隐身:传他人 session 也带不回来",
250
+ all("@sx" not in str(i) for i in ids_other), repr(ids_other))
251
+ ids_br = _hits(cg5, "会话一内容", branch="sx")
252
+ _check("显式 branch=sx 时分支副本可见(与 session 参数正交)",
253
+ any("@sx" in str(i) for i in ids_br), repr(ids_br))
254
+ res, meta = cg5.search("会话一内容")
255
+ _check("默认检索主支正常", bool(res), repr(meta))
256
+
257
+
258
+ if __name__ == "__main__":
259
+ sys.exit(main())