@furongjun1999/dsh-memory 0.5.0 → 0.5.1
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 +552 -465
- package/docs/README.md +1 -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//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_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/lib/bridge.d.ts +9 -0
- package/lib/bridge.js +35 -0
- package/lib/index.js +7 -1
- 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 +582 -580
- 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 +280 -29
- package/md_cg/mdcg.py +616 -125
- package/md_cg/mdcos.py +430 -66
- 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 +170 -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_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_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_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 +6 -2
- package/src/bridge.ts +434 -401
- package/src/index.ts +526 -518
- package/src/lib/roleplay_web.ts +1019 -932
- package/src/lib/token_store.ts +202 -192
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_access_hints —— AccessDenied 可操作指引专项(issue #34)
|
|
3
|
+
|
|
4
|
+
背景:新用户三步接入后必然撞上的第一条失败是 guest 只读写入被拒,而该错误
|
|
5
|
+
无任何 hint(与本仓 md_cg/*.py 70 处 hint 标准不一致)——issue #34 判定为
|
|
6
|
+
「同一仓内标准不一致,且恰落在最影响首次体验的位置」。
|
|
7
|
+
|
|
8
|
+
覆盖:
|
|
9
|
+
- guest op 拒绝:hint 含为什么(MDCG_TOKEN 未配置)+ 怎么办(tokens issue /
|
|
10
|
+
setx / 重启 / README 指针)+ 安抚(不是故障)+ help 指引
|
|
11
|
+
- 有令牌但白名单缺 op:hint 指「换角色 / --ops-allow 补授权重签」(不误导成配凭据)
|
|
12
|
+
- 令牌过期:hint 指重新签发
|
|
13
|
+
- guest require_write:hint 同步到位(同口径)
|
|
14
|
+
- MCP stdio 端到端(issue 复现路径):干净库 + 无 MDCG_TOKEN → cg(op=write)
|
|
15
|
+
的 error JSON 结构化携带 hint
|
|
16
|
+
|
|
17
|
+
运行:python -X utf8 -m md_cg.test_access_hints
|
|
18
|
+
"""
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
26
|
+
|
|
27
|
+
from md_cg.security import AccessDenied, Principal # noqa: E402
|
|
28
|
+
|
|
29
|
+
passed = failed = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check(name, cond, detail=""):
|
|
33
|
+
global passed, failed
|
|
34
|
+
if cond:
|
|
35
|
+
passed += 1
|
|
36
|
+
print(" [PASS] " + name)
|
|
37
|
+
else:
|
|
38
|
+
failed += 1
|
|
39
|
+
print(" [FAIL] " + name + " " + str(detail)[:200])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _denied(fn):
|
|
43
|
+
try:
|
|
44
|
+
fn()
|
|
45
|
+
return None
|
|
46
|
+
except AccessDenied as e:
|
|
47
|
+
return e
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
GUEST_OPS = ("info", "route", "read", "recent", "whitebox", "status", "edges")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
print("[1] guest op 拒绝(issue #34 主场景)")
|
|
55
|
+
guest = Principal(actor="world", role="guest", ops_allow=GUEST_OPS,
|
|
56
|
+
can_write=False)
|
|
57
|
+
e = _denied(lambda: guest.require_op("write"))
|
|
58
|
+
check("guest op=write 抛 AccessDenied", e is not None)
|
|
59
|
+
h = (e.hint if e else "") or ""
|
|
60
|
+
for kw, label in [("MDCG_TOKEN", "点明凭据来源"),
|
|
61
|
+
("tokens issue", "给签发命令"),
|
|
62
|
+
("setx", "给配置动作"),
|
|
63
|
+
("README", "给文档指针"),
|
|
64
|
+
("不是故障", "安抚语气"),
|
|
65
|
+
('cg(op="help"', "help 指引"),
|
|
66
|
+
("重启", "重启生效提示")]:
|
|
67
|
+
check(f"hint {label}({kw})", kw in h, h[:120])
|
|
68
|
+
check("hint 非空且 error 消息保留原口径",
|
|
69
|
+
bool(h) and "无权执行 op=write" in str(e))
|
|
70
|
+
|
|
71
|
+
print("[2] 有令牌但白名单缺 op(不误导成配凭据)")
|
|
72
|
+
rec = Principal(actor="demo", role="recorder",
|
|
73
|
+
ops_allow=("read", "write", "recent"), can_write=True)
|
|
74
|
+
e2 = _denied(lambda: rec.require_op("forget"))
|
|
75
|
+
check("recorder op=forget 抛 AccessDenied", e2 is not None)
|
|
76
|
+
h2 = (e2.hint if e2 else "") or ""
|
|
77
|
+
check("hint 指补授权/换角色(--ops-allow / 重新签发)",
|
|
78
|
+
"--ops-allow" in h2 and "重新签发" in h2, h2[:120])
|
|
79
|
+
check("hint 不误指 guest 成因(不含 MDCG_TOKEN 未配置口径)",
|
|
80
|
+
"未检测到写入凭据" not in h2, h2[:120])
|
|
81
|
+
|
|
82
|
+
print("[3] 令牌过期")
|
|
83
|
+
exp = Principal(actor="demo", role="designer",
|
|
84
|
+
expires_at=1, can_write=True)
|
|
85
|
+
e3 = _denied(lambda: exp.require_op("read"))
|
|
86
|
+
check("过期抛 AccessDenied 且 hint 指重新签发",
|
|
87
|
+
e3 is not None and "重新签发" in (e3.hint or ""), e3.hint)
|
|
88
|
+
|
|
89
|
+
print("[4] guest require_write 同口径")
|
|
90
|
+
e4 = _denied(lambda: guest.require_write("internal"))
|
|
91
|
+
check("guest 写拒绝带 hint(含 MDCG_TOKEN)",
|
|
92
|
+
e4 is not None and "MDCG_TOKEN" in (e4.hint or ""), e4.hint)
|
|
93
|
+
|
|
94
|
+
print("[5] MCP stdio 端到端(issue 复现路径:干净库 + 无令牌)")
|
|
95
|
+
root = tempfile.mkdtemp(prefix="acc_hint_")
|
|
96
|
+
env = dict(os.environ)
|
|
97
|
+
env.pop("MDCG_TOKEN", None)
|
|
98
|
+
env["MDCG_ROOT"] = root
|
|
99
|
+
env["PYTHONUTF8"] = "1"
|
|
100
|
+
py = sys.executable
|
|
101
|
+
proc = subprocess.Popen(
|
|
102
|
+
[py, "-X", "utf8", "-m", "md_cg.mcp_server"],
|
|
103
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
104
|
+
env=env, cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
105
|
+
text=True, encoding="utf-8")
|
|
106
|
+
|
|
107
|
+
def rpc(id_, method, params=None):
|
|
108
|
+
req = {"jsonrpc": "2.0", "id": id_, "method": method,
|
|
109
|
+
"params": params or {}}
|
|
110
|
+
proc.stdin.write(json.dumps(req) + "\n")
|
|
111
|
+
proc.stdin.flush()
|
|
112
|
+
line = proc.stdout.readline()
|
|
113
|
+
return json.loads(line) if line.strip() else {}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
rpc(1, "initialize", {"protocolVersion": "2024-11-05",
|
|
117
|
+
"capabilities": {}, "clientInfo": {"name": "t"}})
|
|
118
|
+
# 通知无应答:直写不等(带 id 等通知应答会错位/阻塞)
|
|
119
|
+
proc.stdin.write(json.dumps(
|
|
120
|
+
{"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
|
|
121
|
+
proc.stdin.flush()
|
|
122
|
+
r = rpc(3, "tools/call", {"name": "cg", "arguments": {
|
|
123
|
+
"op": "write", "content": "测试写入", "content_kind": "code"}})
|
|
124
|
+
proc.stdin.close()
|
|
125
|
+
body = ((r.get("result") or {}).get("content") or [{}])[0].get("text", "")
|
|
126
|
+
obj = json.loads(body) if body.startswith("{") else {}
|
|
127
|
+
check("stdio error JSON 含 AccessDenied", "AccessDenied" in obj.get("error", ""),
|
|
128
|
+
body[:160])
|
|
129
|
+
check("stdio error JSON 结构化携带 hint(含 MDCG_TOKEN 与签发命令)",
|
|
130
|
+
"MDCG_TOKEN" in (obj.get("hint") or "")
|
|
131
|
+
and "tokens issue" in (obj.get("hint") or ""),
|
|
132
|
+
(obj.get("hint") or "")[:160])
|
|
133
|
+
check("isError=True 形态", bool((r.get("result") or {}).get("isError")))
|
|
134
|
+
finally:
|
|
135
|
+
try:
|
|
136
|
+
proc.wait(timeout=10)
|
|
137
|
+
except Exception: # noqa: BLE001
|
|
138
|
+
proc.kill()
|
|
139
|
+
import shutil
|
|
140
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
141
|
+
|
|
142
|
+
print(f"\naccess_hints: {passed} 通过 / {failed} 失败")
|
|
143
|
+
return 0 if not failed else 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
sys.exit(main())
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""branches.discard 摘索引落 tombstone 守卫(缺陷:只 pop 内存 → 幽灵条目固化)。
|
|
3
|
+
|
|
4
|
+
缺陷形态(缺陷报告 2026-09-25 复现证实,medium):discard()(branches.py:261)
|
|
5
|
+
摘除分支索引条目只 `pop` 内存、不写删除记录(未走 `_unstage`),且其内部
|
|
6
|
+
flush(:262-267)把 fork 阶段仍在 `_dirty` 的分支条目作为 **upsert** 持久化进
|
|
7
|
+
`_index_log`。close 时 compact 的「计数对账重扫 + 日志重放」(mdcg.py:1010-1012
|
|
8
|
+
`_apply_log(self._scan_nodes())`)把已物理移走(publish 至 `_branches/<id>/`,
|
|
9
|
+
LAYERS 之外)的分支 upsert **叠回**干净扫描之上——幽灵条目连同目录指纹一起固化
|
|
10
|
+
进 `_index.json` 快照,此后重开指纹校验放行、幽灵永久存活(get()=None、
|
|
11
|
+
检索候选白跑、dangling 不归零)。
|
|
12
|
+
|
|
13
|
+
修复口径(forget 的正确模式,mdcos.py:2244-2246 注释明载同构坑):discard
|
|
14
|
+
摘除分支条目走 `_unstage(nid)`——pop 内存 + `_dirty[nid]=None`(tombstone)
|
|
15
|
+
+ 立即 flush,重放时按序 pop 掉 fork upsert。
|
|
16
|
+
|
|
17
|
+
红守卫 = 缺陷报告复现流程:S1 主支落快照 → S2 fork+discard → S3 close 重开
|
|
18
|
+
断言索引不含 `mem_base@br1` 幽灵条目。B1/B4/B5 锁 discard 既有语义不变
|
|
19
|
+
(返回形态 / 教训节点存活 / 主支完好),B2/B3/B6 为红项。
|
|
20
|
+
运行:python -m md_cg.test_branch_discard_tombstone
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
|
|
29
|
+
from . import branches
|
|
30
|
+
from .fsutil import ShardedLog
|
|
31
|
+
from .mdcg import LAYERS
|
|
32
|
+
from .mdcos import MdCGOS
|
|
33
|
+
|
|
34
|
+
PASS = FAIL = 0
|
|
35
|
+
FAILS = []
|
|
36
|
+
|
|
37
|
+
BID = "br1"
|
|
38
|
+
GHOST = "mem_base@br1"
|
|
39
|
+
SUMMARY = "branch_summary_br1"
|
|
40
|
+
SUMMARY_TEXT = "分支假设:改写可提检索命中 实验结果:无提升 教训:回归再上分支"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def ok(cond, label):
|
|
44
|
+
global PASS, FAIL
|
|
45
|
+
if cond:
|
|
46
|
+
PASS += 1
|
|
47
|
+
print(f" OK {label}")
|
|
48
|
+
else:
|
|
49
|
+
FAIL += 1
|
|
50
|
+
FAILS.append(label)
|
|
51
|
+
print(f" FAIL {label}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _layer_md_ids(root):
|
|
55
|
+
"""盘面真值:LAYERS 各层目录下现存 .md 的节点 id 集合(含子桶)。"""
|
|
56
|
+
ids = set()
|
|
57
|
+
for layer in LAYERS:
|
|
58
|
+
base = os.path.join(root, layer)
|
|
59
|
+
for _dp, _dirs, files in os.walk(base):
|
|
60
|
+
for fn in files:
|
|
61
|
+
if fn.endswith(".md"):
|
|
62
|
+
ids.add(fn[:-3])
|
|
63
|
+
return ids
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
68
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_br_tomb_")
|
|
69
|
+
try:
|
|
70
|
+
root = os.path.join(tmp, "root")
|
|
71
|
+
os.makedirs(root)
|
|
72
|
+
|
|
73
|
+
# S1:主支节点入库并 close 落快照(此后 S2 的 close 走 compact 而非 rebuild)
|
|
74
|
+
cg0 = MdCGOS(root)
|
|
75
|
+
cg0.add("mem_base", "# 功能名:分支守卫主支\n\n正文:蜂群调度基线。\n",
|
|
76
|
+
layer="knowledge")
|
|
77
|
+
cg0.close()
|
|
78
|
+
|
|
79
|
+
# S2:fork → discard(fork 后不 flush——fork 阶段条目仍在 _dirty)
|
|
80
|
+
cg1 = MdCGOS(root)
|
|
81
|
+
fr = branches.fork(cg1, ["mem_base"], branch_id=BID,
|
|
82
|
+
note="守卫:命中提升实验")
|
|
83
|
+
ok(fr.get("ok") and fr["forked"][0]["to"] == GHOST,
|
|
84
|
+
f"B0a fork 出分支副本 {GHOST}")
|
|
85
|
+
r = branches.discard(cg1, BID, SUMMARY_TEXT)
|
|
86
|
+
ok(r.get("ok") is True and r.get("moved") == 1
|
|
87
|
+
and r.get("summary_node") == SUMMARY,
|
|
88
|
+
f"B1 discard 语义不变(ok/moved/summary_node,实测 {r!r})")
|
|
89
|
+
|
|
90
|
+
# B2(机制,红项):摘除必须落删除记录——discard 返回时 _index_log
|
|
91
|
+
# 已含该 nid 的 tombstone(e is None),且 _dirty 中无待写 upsert
|
|
92
|
+
recs = list(ShardedLog.read_all(cg1.index_log_dir))
|
|
93
|
+
ok(any(x.get("id") == GHOST and x.get("e") is None for x in recs),
|
|
94
|
+
f"B2 discard 已落 tombstone(日志含 {GHOST} 删除记录;实测"
|
|
95
|
+
f" {[(x.get('id'), x.get('e') is None) for x in recs]})")
|
|
96
|
+
ok(GHOST not in cg1._dirty
|
|
97
|
+
and GHOST not in cg1.index["nodes"],
|
|
98
|
+
"B2b 内存索引已摘除且 _dirty 无该节点待写条目")
|
|
99
|
+
cg1.close()
|
|
100
|
+
|
|
101
|
+
# S3:重开——幽灵条目不得复活
|
|
102
|
+
cg2 = MdCGOS(root)
|
|
103
|
+
idx_ids = set((cg2.index.get("nodes") or {}).keys())
|
|
104
|
+
ok(GHOST not in idx_ids,
|
|
105
|
+
f"B3 重开后索引不含幽灵条目 {GHOST}(实测索引节点 "
|
|
106
|
+
f"{sorted(idx_ids)})")
|
|
107
|
+
ok("mem_base" in idx_ids and SUMMARY in idx_ids,
|
|
108
|
+
"B4 主支节点与教训节点存活(discard 其余语义不变)")
|
|
109
|
+
disk_ids = _layer_md_ids(root)
|
|
110
|
+
ok(idx_ids == disk_ids,
|
|
111
|
+
f"B5 索引与盘面对账一致(索引 {sorted(idx_ids)} / 盘面 "
|
|
112
|
+
f"{sorted(disk_ids)})")
|
|
113
|
+
ok(cg2.get(GHOST) is None and cg2.get("mem_base") is not None,
|
|
114
|
+
"B6 幽灵不可取回、主支可正常读取")
|
|
115
|
+
ok(cg2.get(SUMMARY) is not None
|
|
116
|
+
and "教训" in (cg2.get(SUMMARY) or {}).get("content", ""),
|
|
117
|
+
"B7 教训节点内容可读(可检索召回)")
|
|
118
|
+
# 冷归档物理保留(不违背永不删除)
|
|
119
|
+
cold = os.path.join(root, branches.COLD_DIR, BID,
|
|
120
|
+
GHOST + ".md")
|
|
121
|
+
ok(os.path.isfile(cold), f"B8 分支文件冷归档保留({cold})")
|
|
122
|
+
cg2.close()
|
|
123
|
+
finally:
|
|
124
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
125
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
126
|
+
|
|
127
|
+
print("\n" + "=" * 60)
|
|
128
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
129
|
+
if FAILS:
|
|
130
|
+
for f in FAILS:
|
|
131
|
+
print(f" - {f}")
|
|
132
|
+
return 0 if FAIL == 0 else 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
sys.exit(main())
|