@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
|
@@ -1,224 +1,238 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""md_cg · 索引持久化守卫(跨进程可见性 · 进程退出兜底)
|
|
3
|
-
|
|
4
|
-
运行:python -m md_cg.test_index_durability
|
|
5
|
-
|
|
6
|
-
背景(2026-09-16 取证,首例=审核裁决通路):
|
|
7
|
-
节点 .md 由 `add()` **立即写盘**,但索引条目先进内存 `_dirty`,靠 `flush()` /
|
|
8
|
-
`close()` 落分片日志。一次性进程(CLI / 脚本)写入 1~2 条、远未达 autoflush(64)
|
|
9
|
-
阈值即退出 → 日志无记录;而**已有 `_index.json` 的根重开时不重扫目录**
|
|
10
|
-
(`_load_index`:快照优先,仅重放日志),于是节点「**在盘上但索引无条目**」,
|
|
11
|
-
其它进程与重载后的长驻进程都检索不到,只能靠某次全量 `rebuild_index()` 偶然救回。
|
|
12
|
-
表象极易被误判为「写入丢失」——故本测试以**索引可见性**(而非 `get()`)为判据。
|
|
13
|
-
|
|
14
|
-
固化三处修复 + 一处反证(防测试空转):
|
|
15
|
-
① 库层进程退出兜底(`mdcg._LIVE_CGS` + atexit):子进程不显式收尾也能落盘;
|
|
16
|
-
② `review_decide` 统一收尾:目标节点 **与** 审计记录节点双落盘
|
|
17
|
-
(accept/edit 写前者、`_record_decision` 写后者;reject 路径**只**写后者);
|
|
18
|
-
③ `review_cli` 显式 `close()`:真实缺陷现场(一次性 CLI 裁决)端到端;
|
|
19
|
-
④ 反证组:清空兜底登记 → 复现「索引不可见」,证明 ①② 确在生效。
|
|
20
|
-
|
|
21
|
-
`_index.json` 快照是「贴真实库形态」的必要前提:空 root 首开会全扫目录,
|
|
22
|
-
恰好掩盖本缺陷(这也是首版复现实验失败的教训)。
|
|
23
|
-
"""
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import os
|
|
27
|
-
import shutil
|
|
28
|
-
import subprocess
|
|
29
|
-
import sys
|
|
30
|
-
import tempfile
|
|
31
|
-
|
|
32
|
-
from .mdcos import MdCGOS
|
|
33
|
-
|
|
34
|
-
PASS = FAIL = 0
|
|
35
|
-
FAILS = []
|
|
36
|
-
|
|
37
|
-
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
38
|
-
|
|
39
|
-
CCG = ("# 功能名:{n}\n# 生效条件:{c}\n# 子功能:{s}\n# 执行:{e}\n"
|
|
40
|
-
"# 验证方式:test\n# 不适用条件:{neg}\n\n{body}\n")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def check(name, cond, detail=""):
|
|
44
|
-
global PASS, FAIL
|
|
45
|
-
if cond:
|
|
46
|
-
PASS += 1
|
|
47
|
-
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
48
|
-
else:
|
|
49
|
-
FAIL += 1
|
|
50
|
-
FAILS.append(name)
|
|
51
|
-
print(f" [FAIL] {name} · {detail}")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def mk(n, cond, body, neg="其它"):
|
|
55
|
-
return CCG.format(n=n, c=cond, s=body[:20], e=body, neg=neg, body=body)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _child_env():
|
|
59
|
-
"""子进程环境:显式 UTF-8 + PYTHONPATH 指向本仓(纪律 15:不经 Windows shell)。"""
|
|
60
|
-
env = dict(os.environ)
|
|
61
|
-
env["PYTHONUTF8"] = "1"
|
|
62
|
-
env["PYTHONIOENCODING"] = "utf-8"
|
|
63
|
-
env["PYTHONPATH"] = REPO + os.pathsep + env.get("PYTHONPATH", "")
|
|
64
|
-
return env
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _run_child(code):
|
|
68
|
-
"""以子进程执行一段脚本(argv 列表,shell=False)。"""
|
|
69
|
-
fd, path = tempfile.mkstemp(suffix=".py", prefix="_idxguard_")
|
|
70
|
-
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
71
|
-
f.write(code)
|
|
72
|
-
try:
|
|
73
|
-
return subprocess.run([sys.executable, path], capture_output=True,
|
|
74
|
-
text=True, encoding="utf-8", errors="replace",
|
|
75
|
-
env=_child_env(), cwd=REPO)
|
|
76
|
-
finally:
|
|
77
|
-
try:
|
|
78
|
-
os.remove(path)
|
|
79
|
-
except OSError:
|
|
80
|
-
pass
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _seed(root, nid="seed"):
|
|
84
|
-
"""建一个「已有快照」的库形态(缺陷成立的必要前提)。"""
|
|
85
|
-
cg = MdCGOS(root, actor="seed")
|
|
86
|
-
cg.add(nid, mk("种子节点", "问种子", "仅用于建索引快照"),
|
|
87
|
-
verification_basis="test")
|
|
88
|
-
cg.flush()
|
|
89
|
-
cg.rebuild_index()
|
|
90
|
-
cg.close()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _visible(root, nid):
|
|
94
|
-
"""索引可见性判据:**新实例**(模拟其它进程 / 重载后的长驻进程)索引里是否有该条目。
|
|
95
|
-
|
|
96
|
-
刻意不用 `get()`:它夹带密级/密钥可读性判据,会把「索引缺失」与「读不到」混为一谈。
|
|
97
|
-
"""
|
|
98
|
-
probe = MdCGOS(root, actor="probe")
|
|
99
|
-
try:
|
|
100
|
-
return nid in (probe.index.get("nodes") or {})
|
|
101
|
-
finally:
|
|
102
|
-
probe.close()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
CHILD_BODY = (
|
|
106
|
-
"import sys\n"
|
|
107
|
-
"sys.path.insert(0, {repo!r})\n"
|
|
108
|
-
"from md_cg.mdcos import MdCGOS\n"
|
|
109
|
-
"cg = MdCGOS({root!r}, actor='child', autoflush=64)\n"
|
|
110
|
-
"cg.add({nid!r}, {content!r}, verification_basis='test')\n"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def main():
|
|
115
|
-
roots = []
|
|
116
|
-
try:
|
|
117
|
-
# ---------------- ① 进程退出兜底(atexit) ----------------
|
|
118
|
-
print("\n【①】库层进程退出兜底:子进程不显式收尾")
|
|
119
|
-
root_a = tempfile.mkdtemp(prefix="idxdur_a_")
|
|
120
|
-
roots.append(root_a)
|
|
121
|
-
_seed(root_a)
|
|
122
|
-
r = _run_child(CHILD_BODY.format(
|
|
123
|
-
repo=REPO, root=root_a, nid="n_atexit",
|
|
124
|
-
content=mk("兜底测试节点", "问兜底", "子进程写一条后直接退出")) +
|
|
125
|
-
"# 故意不 close:验证进程退出兜底(atexit)\n")
|
|
126
|
-
check("子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
|
|
127
|
-
check("兜底生效:新进程索引可见", _visible(root_a, "n_atexit"))
|
|
128
|
-
|
|
129
|
-
# ---------------- ④ 反证组(防测试空转) ----------------
|
|
130
|
-
print("\n【④】反证组:清空兜底登记 → 复现缺陷")
|
|
131
|
-
root_b = tempfile.mkdtemp(prefix="idxdur_b_")
|
|
132
|
-
roots.append(root_b)
|
|
133
|
-
_seed(root_b)
|
|
134
|
-
r = _run_child(CHILD_BODY.format(
|
|
135
|
-
repo=REPO, root=root_b, nid="n_nohook",
|
|
136
|
-
content=mk("反证测试节点", "问反证", "清空兜底登记后退出")) +
|
|
137
|
-
"# 反证:摘掉兜底登记(等价于修复前形态)\n"
|
|
138
|
-
"from md_cg.mdcg import _LIVE_CGS\n"
|
|
139
|
-
"_LIVE_CGS.clear()\n")
|
|
140
|
-
check("反证子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
check("
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# ----------------
|
|
157
|
-
print("\n
|
|
158
|
-
|
|
159
|
-
roots.append(
|
|
160
|
-
_seed(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
check("
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 索引持久化守卫(跨进程可见性 · 进程退出兜底)
|
|
3
|
+
|
|
4
|
+
运行:python -m md_cg.test_index_durability
|
|
5
|
+
|
|
6
|
+
背景(2026-09-16 取证,首例=审核裁决通路):
|
|
7
|
+
节点 .md 由 `add()` **立即写盘**,但索引条目先进内存 `_dirty`,靠 `flush()` /
|
|
8
|
+
`close()` 落分片日志。一次性进程(CLI / 脚本)写入 1~2 条、远未达 autoflush(64)
|
|
9
|
+
阈值即退出 → 日志无记录;而**已有 `_index.json` 的根重开时不重扫目录**
|
|
10
|
+
(`_load_index`:快照优先,仅重放日志),于是节点「**在盘上但索引无条目**」,
|
|
11
|
+
其它进程与重载后的长驻进程都检索不到,只能靠某次全量 `rebuild_index()` 偶然救回。
|
|
12
|
+
表象极易被误判为「写入丢失」——故本测试以**索引可见性**(而非 `get()`)为判据。
|
|
13
|
+
|
|
14
|
+
固化三处修复 + 一处反证(防测试空转):
|
|
15
|
+
① 库层进程退出兜底(`mdcg._LIVE_CGS` + atexit):子进程不显式收尾也能落盘;
|
|
16
|
+
② `review_decide` 统一收尾:目标节点 **与** 审计记录节点双落盘
|
|
17
|
+
(accept/edit 写前者、`_record_decision` 写后者;reject 路径**只**写后者);
|
|
18
|
+
③ `review_cli` 显式 `close()`:真实缺陷现场(一次性 CLI 裁决)端到端;
|
|
19
|
+
④ 反证组:清空兜底登记 → 复现「索引不可见」,证明 ①② 确在生效。
|
|
20
|
+
|
|
21
|
+
`_index.json` 快照是「贴真实库形态」的必要前提:空 root 首开会全扫目录,
|
|
22
|
+
恰好掩盖本缺陷(这也是首版复现实验失败的教训)。
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
|
|
32
|
+
from .mdcos import MdCGOS
|
|
33
|
+
|
|
34
|
+
PASS = FAIL = 0
|
|
35
|
+
FAILS = []
|
|
36
|
+
|
|
37
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
38
|
+
|
|
39
|
+
CCG = ("# 功能名:{n}\n# 生效条件:{c}\n# 子功能:{s}\n# 执行:{e}\n"
|
|
40
|
+
"# 验证方式:test\n# 不适用条件:{neg}\n\n{body}\n")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check(name, cond, detail=""):
|
|
44
|
+
global PASS, FAIL
|
|
45
|
+
if cond:
|
|
46
|
+
PASS += 1
|
|
47
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
48
|
+
else:
|
|
49
|
+
FAIL += 1
|
|
50
|
+
FAILS.append(name)
|
|
51
|
+
print(f" [FAIL] {name} · {detail}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def mk(n, cond, body, neg="其它"):
|
|
55
|
+
return CCG.format(n=n, c=cond, s=body[:20], e=body, neg=neg, body=body)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _child_env():
|
|
59
|
+
"""子进程环境:显式 UTF-8 + PYTHONPATH 指向本仓(纪律 15:不经 Windows shell)。"""
|
|
60
|
+
env = dict(os.environ)
|
|
61
|
+
env["PYTHONUTF8"] = "1"
|
|
62
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
63
|
+
env["PYTHONPATH"] = REPO + os.pathsep + env.get("PYTHONPATH", "")
|
|
64
|
+
return env
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _run_child(code):
|
|
68
|
+
"""以子进程执行一段脚本(argv 列表,shell=False)。"""
|
|
69
|
+
fd, path = tempfile.mkstemp(suffix=".py", prefix="_idxguard_")
|
|
70
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
71
|
+
f.write(code)
|
|
72
|
+
try:
|
|
73
|
+
return subprocess.run([sys.executable, path], capture_output=True,
|
|
74
|
+
text=True, encoding="utf-8", errors="replace",
|
|
75
|
+
env=_child_env(), cwd=REPO)
|
|
76
|
+
finally:
|
|
77
|
+
try:
|
|
78
|
+
os.remove(path)
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _seed(root, nid="seed"):
|
|
84
|
+
"""建一个「已有快照」的库形态(缺陷成立的必要前提)。"""
|
|
85
|
+
cg = MdCGOS(root, actor="seed")
|
|
86
|
+
cg.add(nid, mk("种子节点", "问种子", "仅用于建索引快照"),
|
|
87
|
+
verification_basis="test")
|
|
88
|
+
cg.flush()
|
|
89
|
+
cg.rebuild_index()
|
|
90
|
+
cg.close()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _visible(root, nid):
|
|
94
|
+
"""索引可见性判据:**新实例**(模拟其它进程 / 重载后的长驻进程)索引里是否有该条目。
|
|
95
|
+
|
|
96
|
+
刻意不用 `get()`:它夹带密级/密钥可读性判据,会把「索引缺失」与「读不到」混为一谈。
|
|
97
|
+
"""
|
|
98
|
+
probe = MdCGOS(root, actor="probe")
|
|
99
|
+
try:
|
|
100
|
+
return nid in (probe.index.get("nodes") or {})
|
|
101
|
+
finally:
|
|
102
|
+
probe.close()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
CHILD_BODY = (
|
|
106
|
+
"import sys\n"
|
|
107
|
+
"sys.path.insert(0, {repo!r})\n"
|
|
108
|
+
"from md_cg.mdcos import MdCGOS\n"
|
|
109
|
+
"cg = MdCGOS({root!r}, actor='child', autoflush=64)\n"
|
|
110
|
+
"cg.add({nid!r}, {content!r}, verification_basis='test')\n"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main():
|
|
115
|
+
roots = []
|
|
116
|
+
try:
|
|
117
|
+
# ---------------- ① 进程退出兜底(atexit) ----------------
|
|
118
|
+
print("\n【①】库层进程退出兜底:子进程不显式收尾")
|
|
119
|
+
root_a = tempfile.mkdtemp(prefix="idxdur_a_")
|
|
120
|
+
roots.append(root_a)
|
|
121
|
+
_seed(root_a)
|
|
122
|
+
r = _run_child(CHILD_BODY.format(
|
|
123
|
+
repo=REPO, root=root_a, nid="n_atexit",
|
|
124
|
+
content=mk("兜底测试节点", "问兜底", "子进程写一条后直接退出")) +
|
|
125
|
+
"# 故意不 close:验证进程退出兜底(atexit)\n")
|
|
126
|
+
check("子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
|
|
127
|
+
check("兜底生效:新进程索引可见", _visible(root_a, "n_atexit"))
|
|
128
|
+
|
|
129
|
+
# ---------------- ④ 反证组(防测试空转) ----------------
|
|
130
|
+
print("\n【④】反证组:清空兜底登记 → 复现缺陷")
|
|
131
|
+
root_b = tempfile.mkdtemp(prefix="idxdur_b_")
|
|
132
|
+
roots.append(root_b)
|
|
133
|
+
_seed(root_b)
|
|
134
|
+
r = _run_child(CHILD_BODY.format(
|
|
135
|
+
repo=REPO, root=root_b, nid="n_nohook",
|
|
136
|
+
content=mk("反证测试节点", "问反证", "清空兜底登记后退出")) +
|
|
137
|
+
"# 反证:摘掉兜底登记(等价于修复前形态)\n"
|
|
138
|
+
"from md_cg.mdcg import _LIVE_CGS\n"
|
|
139
|
+
"_LIVE_CGS.clear()\n")
|
|
140
|
+
check("反证子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
|
|
141
|
+
# issue #33(close 自动落快照 + 盘面指纹校验)后,「无 flush 退出 →
|
|
142
|
+
# 索引不可见」的原缺陷被修得更彻底:节点文件已落盘、目录 mtime 已
|
|
143
|
+
# 变 → 新进程 _load_index 指纹校验失败 → 回退全库扫描 → 节点**仍
|
|
144
|
+
# 可见**(可见性由盘面兜底接管,原「不可见」反证前提失效)。
|
|
145
|
+
# atexit 兜底的有效变量改为「增量日志是否落账」:摘掉 atexit →
|
|
146
|
+
# 子进程无 flush → 增量日志零记录(若【①】的兜底是空转,这里会
|
|
147
|
+
# 出现记录)——反证组据此继续防测试空转。
|
|
148
|
+
check("反证:可见性由盘面指纹兜底接管(原缺陷不可复现=修复增强)",
|
|
149
|
+
_visible(root_b, "n_nohook"))
|
|
150
|
+
log_dir = os.path.join(root_b, "_index_log")
|
|
151
|
+
n_log = (sum(1 for fn in os.listdir(log_dir) if fn.endswith(".log"))
|
|
152
|
+
if os.path.isdir(log_dir) else 0)
|
|
153
|
+
check("反证:增量日志零记录(atexit flush 是有效变量,防【①】空转)",
|
|
154
|
+
n_log == 0, f"log_files={n_log}")
|
|
155
|
+
|
|
156
|
+
# ---------------- ③ 显式 close 对照(原有正路不回归) ----------------
|
|
157
|
+
print("\n【③】显式收尾对照:close() 仍是正路")
|
|
158
|
+
root_c = tempfile.mkdtemp(prefix="idxdur_c_")
|
|
159
|
+
roots.append(root_c)
|
|
160
|
+
_seed(root_c)
|
|
161
|
+
r = _run_child(CHILD_BODY.format(
|
|
162
|
+
repo=REPO, root=root_c, nid="n_close",
|
|
163
|
+
content=mk("显式收尾节点", "问收尾", "子进程写一条后 close")) +
|
|
164
|
+
"cg.close()\n")
|
|
165
|
+
check("显式 close 子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
|
|
166
|
+
check("显式 close:新进程索引可见", _visible(root_c, "n_close"))
|
|
167
|
+
|
|
168
|
+
# ---------------- ② review_decide 通路统一收尾 ----------------
|
|
169
|
+
print("\n【②】审核裁决通路:目标节点 + 审计记录节点双落盘")
|
|
170
|
+
root_d = tempfile.mkdtemp(prefix="idxdur_d_")
|
|
171
|
+
roots.append(root_d)
|
|
172
|
+
_seed(root_d, nid="seed_d")
|
|
173
|
+
osd = MdCGOS(root_d, actor="designer")
|
|
174
|
+
pid = osd.propose("n_acc", mk("受审节点", "问受审", "审核落盘的目标节点"),
|
|
175
|
+
verify={"kind": "test", "assertions": ["断言通过"]})
|
|
176
|
+
out = osd.review_decide(pid, "accept", reason="测试判据")
|
|
177
|
+
check("accept 裁决成功", bool(out.get("ok")), str(out)[:160])
|
|
178
|
+
rec_nid = out.get("record_node_id")
|
|
179
|
+
probe = MdCGOS(root_d, actor="probe")
|
|
180
|
+
try:
|
|
181
|
+
check("accept:目标节点索引可见(新实例)",
|
|
182
|
+
out.get("node_id") in probe.index["nodes"], str(out.get("node_id")))
|
|
183
|
+
check("accept:审计记录节点索引可见(新实例)",
|
|
184
|
+
bool(rec_nid) and rec_nid in probe.index["nodes"], str(rec_nid))
|
|
185
|
+
check("accept:review_records 可列出(复核面不空)",
|
|
186
|
+
any(x["id"] == rec_nid for x in probe.review_records(pid=pid)))
|
|
187
|
+
finally:
|
|
188
|
+
probe.close()
|
|
189
|
+
|
|
190
|
+
pid2 = osd.propose("n_rej", mk("被拒节点", "问被拒", "reject 只记裁决不落目标节点"))
|
|
191
|
+
out2 = osd.review_decide(pid2, "reject", reason="不通过")
|
|
192
|
+
check("reject 裁决成功", bool(out2.get("ok")), str(out2)[:160])
|
|
193
|
+
rec2 = out2.get("record_node_id")
|
|
194
|
+
check("reject:审计记录节点索引可见(该路径只写它)",
|
|
195
|
+
bool(rec2) and _visible(root_d, rec2), str(rec2))
|
|
196
|
+
osd.close()
|
|
197
|
+
|
|
198
|
+
# ---------------- ③ review_cli 端到端(真实缺陷现场) ----------------
|
|
199
|
+
print("\n【③】review_cli 端到端(一次性 CLI 裁决)")
|
|
200
|
+
root_e = tempfile.mkdtemp(prefix="idxdur_e_")
|
|
201
|
+
roots.append(root_e)
|
|
202
|
+
_seed(root_e, nid="seed_e")
|
|
203
|
+
ose = MdCGOS(root_e, actor="designer")
|
|
204
|
+
pid_e = ose.propose("n_cli", mk("CLI 裁决节点", "问 CLI",
|
|
205
|
+
"review_cli 落盘的目标节点"))
|
|
206
|
+
ose.close()
|
|
207
|
+
# 入口为**包内**模块(`python -m md_cg.review_cli`):出货包不含 scripts/,
|
|
208
|
+
# 旧写法 `scripts/review_cli.py` 在安装态必 FileNotFoundError(2026-09-24 修复)。
|
|
209
|
+
r = subprocess.run(
|
|
210
|
+
[sys.executable, "-m", "md_cg.review_cli",
|
|
211
|
+
"accept", pid_e, "--root", root_e, "--reason", "端到端守卫测试"],
|
|
212
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
213
|
+
env=_child_env(), cwd=REPO)
|
|
214
|
+
check("review_cli 退出码 0", r.returncode == 0,
|
|
215
|
+
(r.stdout or r.stderr or "")[:200])
|
|
216
|
+
check("review_cli:目标节点索引可见(新进程)", _visible(root_e, "n_cli"))
|
|
217
|
+
probe = MdCGOS(root_e, actor="probe")
|
|
218
|
+
try:
|
|
219
|
+
recs = probe.review_records(pid=pid_e)
|
|
220
|
+
check("review_cli:审计记录节点索引可见",
|
|
221
|
+
bool(recs) and all(x["id"] in probe.index["nodes"] for x in recs),
|
|
222
|
+
str([x["id"] for x in recs]))
|
|
223
|
+
finally:
|
|
224
|
+
probe.close()
|
|
225
|
+
finally:
|
|
226
|
+
for r_ in roots:
|
|
227
|
+
shutil.rmtree(r_, ignore_errors=True)
|
|
228
|
+
|
|
229
|
+
print("\n" + "=" * 64)
|
|
230
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
231
|
+
if FAILS:
|
|
232
|
+
print("失败项:" + ";".join(FAILS))
|
|
233
|
+
return 0 if FAIL == 0 else 1
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
sys.exit(main())
|
|
238
|
+
|
package/md_cg/test_interop.py
CHANGED
|
@@ -79,8 +79,10 @@ def main():
|
|
|
79
79
|
print("[4] 冻结文件落盘形态(入库可复核)")
|
|
80
80
|
fp = os.path.join(tmp, "interop", "frozen.json")
|
|
81
81
|
data = json.load(open(fp, encoding="utf-8"))
|
|
82
|
-
check("4a 冻结文件只含结构化事实(iter_id/frozen_at/digest/files
|
|
83
|
-
|
|
82
|
+
check("4a 冻结文件只含结构化事实(iter_id/frozen_at/digest/files"
|
|
83
|
+
"/missing_patterns)",
|
|
84
|
+
set(data.keys()) == {"iter_id", "frozen_at", "digest", "files",
|
|
85
|
+
"missing_patterns"},
|
|
84
86
|
str(sorted(data.keys())))
|
|
85
87
|
finally:
|
|
86
88
|
shutil.rmtree(tmp, ignore_errors=True)
|