@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.
- package/README.md +572 -465
- package/docs/README.md +1 -0
- 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
- 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
- package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
- 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
- package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
- package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
- package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
- 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
- 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
- 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
- package/dsh/cordis-patch-profile-web.example.yml +35 -0
- package/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +66 -0
- package/lib/index.js +7 -1
- package/lib/init.d.ts +88 -0
- package/lib/init.js +287 -0
- package/lib/lib/roleplay_web.js +530 -443
- package/lib/lib/token_store.d.ts +7 -1
- package/lib/lib/token_store.js +12 -3
- package/md_cg/audit.py +12 -1
- package/md_cg/backfill.py +16 -15
- package/md_cg/backfill_bucket_zh.py +35 -0
- package/md_cg/bench_e2e_judge.py +532 -0
- package/md_cg/bench_e2e_locomo_qa.py +368 -0
- package/md_cg/bench_e2e_qa.py +256 -0
- package/md_cg/branches.py +18 -2
- package/md_cg/ccgc.py +3 -2
- package/md_cg/chain.py +19 -4
- package/md_cg/consolidate.py +7 -6
- package/md_cg/crosscheck.py +4 -3
- package/md_cg/crypto.py +439 -437
- package/md_cg/datapath.py +395 -335
- package/md_cg/evidence.py +8 -3
- package/md_cg/export.py +3 -1
- package/md_cg/forgetting.py +2 -2
- package/md_cg/fsutil.py +48 -0
- package/md_cg/hotcache.py +255 -238
- package/md_cg/interop.py +161 -22
- package/md_cg/judgment_manifest.py +177 -0
- package/md_cg/links.py +655 -622
- package/md_cg/mcp_server.py +282 -31
- package/md_cg/mdcg.py +702 -126
- package/md_cg/mdcos.py +454 -68
- package/md_cg/mreview/govern.py +5 -4
- package/md_cg/postings.py +4 -2
- package/md_cg/readcache.py +76 -18
- package/md_cg/reconcile.py +228 -0
- package/md_cg/review_cli.py +215 -0
- package/md_cg/routing.py +28 -0
- package/md_cg/run_tests.py +211 -0
- package/md_cg/scrub.py +862 -852
- package/md_cg/security.py +385 -275
- package/md_cg/selfreport.py +3 -2
- package/md_cg/signer.py +565 -562
- package/md_cg/sources.py +3 -2
- package/md_cg/stg.py +6 -0
- package/md_cg/sustain.py +1168 -1138
- package/md_cg/test_access_hints.py +147 -0
- package/md_cg/test_branch_discard_tombstone.py +136 -0
- package/md_cg/test_branches.py +259 -249
- package/md_cg/test_chain_read_isolate.py +168 -0
- package/md_cg/test_datapath_device_name.py +203 -0
- package/md_cg/test_emit_negtail_cache.py +156 -0
- package/md_cg/test_en_pipeline.py +186 -166
- package/md_cg/test_govern_directread.py +421 -0
- package/md_cg/test_i32_hotcache_env_key.py +218 -0
- package/md_cg/test_identity_attribution.py +228 -147
- package/md_cg/test_index_crossprocess_reload.py +301 -0
- package/md_cg/test_index_durability.py +238 -224
- package/md_cg/test_interop.py +4 -2
- package/md_cg/test_interop_judgment.py +228 -0
- package/md_cg/test_issue39_utf8_stdio.py +273 -0
- package/md_cg/test_links_concurrent_write.py +188 -0
- package/md_cg/test_merge_upsert.py +168 -0
- package/md_cg/test_n123_derive_expiry_chain.py +205 -0
- package/md_cg/test_n130_verify_falsified_protect.py +185 -0
- package/md_cg/test_n131_merge_gate.py +205 -0
- package/md_cg/test_none_id_write_guard.py +165 -0
- package/md_cg/test_p1x_ref_root.py +160 -0
- package/md_cg/test_p27_docindex.py +774 -765
- package/md_cg/test_p2_mcp.py +3 -0
- package/md_cg/test_p32_backfill.py +304 -298
- package/md_cg/test_p39_verify_flow.py +90 -50
- package/md_cg/test_p47_session_view.py +60 -25
- package/md_cg/test_propose_tail_index.py +157 -0
- package/md_cg/test_read_scope_b27.py +277 -0
- package/md_cg/test_readcache_default_on.py +168 -0
- package/md_cg/test_readcache_precise_inval.py +270 -0
- package/md_cg/test_readcache_prodpath.py +55 -7
- package/md_cg/test_reconcile_v0.py +294 -0
- package/md_cg/test_retr_s1.py +6 -2
- package/md_cg/test_retr_s1b.py +67 -0
- package/md_cg/test_retr_s7.py +8 -0
- package/md_cg/test_retr_s9_entity_ctx.py +181 -175
- package/md_cg/test_retr_score_once.py +208 -0
- package/md_cg/test_review_cli_attribution.py +177 -0
- package/md_cg/test_review_cli_visibility.py +235 -0
- package/md_cg/test_review_onepass.py +170 -0
- package/md_cg/test_rrf_graph_seed_cache.py +154 -0
- package/md_cg/test_security_audit.py +155 -0
- package/md_cg/test_security_audit_b26.py +161 -0
- package/md_cg/test_security_audit_v21.py +250 -0
- package/md_cg/test_semantic_canonical.py +255 -241
- package/md_cg/test_session_isolation.py +168 -0
- package/md_cg/test_snapshot_autoclose.py +187 -0
- package/md_cg/test_tail_watermark_race.py +208 -0
- package/md_cg/test_tenant_env_override_warn.py +139 -0
- package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
- package/md_cg/test_v14_fixes.py +415 -397
- package/md_cg/test_verify_dirty_reconcile.py +157 -0
- package/md_cg/theory.py +276 -273
- package/md_cg/tokens.py +734 -677
- package/md_cg/units.py +3 -2
- package/md_cg/vision_evidence.py +4 -3
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
- package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
- package/md_cg/writepipe.py +554 -550
- package/package.json +11 -2
- package/src/bridge.ts +434 -401
- package/src/cli.ts +65 -0
- package/src/index.ts +526 -518
- package/src/init.ts +347 -0
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
package/md_cg/test_branches.py
CHANGED
|
@@ -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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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())
|