@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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""生产 search T2/T3 重打分守卫(v7 N51 留档复测·修复,缺陷迭代第 14 轮)。
|
|
4
|
+
|
|
5
|
+
缺陷形态(缺陷报告/v9.md:111 留档,600 节点临时库实测逐位复现):
|
|
6
|
+
生产 MdCGOS.search 的 try_stage(mdcos.py)无 scored 形参——T2/T3 均为
|
|
7
|
+
「打分截断后又对 picked 重打」:cut_by_relevance(hits, self._score(hits))
|
|
8
|
+
之后 try_stage(hits) 内部再 _score 一遍;T3 再对 picked 重打。实测无交集
|
|
9
|
+
查询 _score 恰 4 调、per-call docs=[0,0,600,500]、共打分 1100 文档——
|
|
10
|
+
500/1100≈45% 打分工作量冗余(GLOBAL_CAP=500)。基类 try_stage 早有
|
|
11
|
+
scored=None 形参(mdcg.py P2-4,批次 31,reach 分支已用)未回移生产覆写。
|
|
12
|
+
|
|
13
|
+
修复口径:生产 try_stage 照基类 P2-4 模板补 scored=None 形参;T2 hits 腿
|
|
14
|
+
(截断打分结果经 doc_key 键表回填)与 T3 picked 腿、reach hits_r 腿一并
|
|
15
|
+
透传——同一 _score 纯函数(单次 search 内 docs/q/qb/pool_cfg 均不变),
|
|
16
|
+
透传不改变任何分值与输出。
|
|
17
|
+
|
|
18
|
+
本守卫(调用计数/复杂度断言形态,非时间阈值):
|
|
19
|
+
T3 段(无交集查询,600 节点):_score 恰 2 调、per-call [0,600]、共 600
|
|
20
|
+
文档(旧实现 4 调 [0,0,600,500]=1100 → 红);tier/scanned/结果规模
|
|
21
|
+
不变;
|
|
22
|
+
T2 段(有交集查询):_score 恰 1 调、共 600 文档(旧实现 2 调
|
|
23
|
+
[600,500]=1100 → 红);
|
|
24
|
+
纯度段:对计数包装记录的每笔 (docs, scored) 复算 _score 逐位相等
|
|
25
|
+
(透传值 ≡ 现算值——不改结果的直接证据);
|
|
26
|
+
确定性段:同查询连跑两轮结果 id 序与分值逐位一致。
|
|
27
|
+
|
|
28
|
+
运行:python -m md_cg.test_retr_score_once
|
|
29
|
+
"""
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import sys
|
|
33
|
+
import tempfile
|
|
34
|
+
|
|
35
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
36
|
+
|
|
37
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
38
|
+
|
|
39
|
+
PASS = 0
|
|
40
|
+
FAIL = 0
|
|
41
|
+
FAILS = []
|
|
42
|
+
|
|
43
|
+
N = 600
|
|
44
|
+
QUERY_MISS = "完全无交集查询词项甲乙丙" # LIKE 零命中 → T3 全量兜底
|
|
45
|
+
QUERY_HIT = "蜂群调度" # 有交集 → T2 命中
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check(name, cond, detail=""):
|
|
49
|
+
global PASS, FAIL
|
|
50
|
+
if cond:
|
|
51
|
+
PASS += 1
|
|
52
|
+
print(f" OK {name}")
|
|
53
|
+
else:
|
|
54
|
+
FAIL += 1
|
|
55
|
+
FAILS.append(name)
|
|
56
|
+
print(f" FAIL {name} {detail}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_lib(root):
|
|
60
|
+
cg = MdCGOS(root)
|
|
61
|
+
for i in range(N):
|
|
62
|
+
cg.add("mem_%04d" % i,
|
|
63
|
+
"# 功能名:打分样本 %d\n# 正文:%s 节点 %d" % (i, QUERY_HIT, i),
|
|
64
|
+
layer="knowledge")
|
|
65
|
+
cg.flush()
|
|
66
|
+
return cg
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def count_score(cg, fn):
|
|
70
|
+
"""包装实例 _score:记录每笔 (docs 数, scored);另存 docs 引用于旁表。"""
|
|
71
|
+
calls = [] # [docs_n]
|
|
72
|
+
pairs = [] # [(docs, scored)]
|
|
73
|
+
real = MdCGOS._score
|
|
74
|
+
|
|
75
|
+
def _spy(self, docs, *a, **kw):
|
|
76
|
+
out = real(self, docs, *a, **kw)
|
|
77
|
+
calls.append(len(docs))
|
|
78
|
+
# 记录时拷贝:_emit 会对 scored 列表原地排序(记录引用会被事后变更)
|
|
79
|
+
pairs.append((list(docs), list(out)))
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
cg._score = _spy.__get__(cg, MdCGOS)
|
|
83
|
+
try:
|
|
84
|
+
return fn(), calls, pairs
|
|
85
|
+
finally:
|
|
86
|
+
del cg._score # 还原类属性查找
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
91
|
+
root = tempfile.mkdtemp(prefix="mdcg_score_once_")
|
|
92
|
+
cg = None
|
|
93
|
+
try:
|
|
94
|
+
cg = build_lib(root)
|
|
95
|
+
cg.search(QUERY_MISS) # 预热(读缓存装载,非度量面)
|
|
96
|
+
|
|
97
|
+
print("== T3 无交集查询:打分恰一次/文档 ==")
|
|
98
|
+
for rep in range(2):
|
|
99
|
+
(res, meta), calls, _ = count_score(
|
|
100
|
+
cg, lambda: cg.search(QUERY_MISS))
|
|
101
|
+
check("T3a rep%d _score 调用数 == 2(旧 4)" % rep,
|
|
102
|
+
len(calls) == 2, f"calls={calls}")
|
|
103
|
+
check("T3b rep%d per-call docs == [0, 600](旧 [0,0,600,500])" % rep,
|
|
104
|
+
calls == [0, 600], f"calls={calls}")
|
|
105
|
+
check("T3c rep%d 共打分 600 文档(旧 1100,省 45)" % rep,
|
|
106
|
+
sum(calls) == 600, f"total={sum(calls)}")
|
|
107
|
+
check("T3d rep%d tier/scanned/结果规模不变" % rep,
|
|
108
|
+
meta.get("tier") == "T3_global_scan"
|
|
109
|
+
and meta.get("scanned") == N and len(res) == 20,
|
|
110
|
+
f"tier={meta.get('tier')} scanned={meta.get('scanned')} "
|
|
111
|
+
f"n={len(res)}")
|
|
112
|
+
|
|
113
|
+
print("== T2 有交集查询:打分恰一次/文档 ==")
|
|
114
|
+
for rep in range(2):
|
|
115
|
+
(res, meta), calls, _ = count_score(
|
|
116
|
+
cg, lambda: cg.search(QUERY_HIT))
|
|
117
|
+
check("T2a rep%d _score 调用数 == 1(旧 2)" % rep,
|
|
118
|
+
len(calls) == 1, f"calls={calls}")
|
|
119
|
+
check("T2b rep%d 共打分 600 文档(旧 1100)" % rep,
|
|
120
|
+
calls == [600], f"calls={calls}")
|
|
121
|
+
check("T2c rep%d tier 不变(T2_global_like)" % rep,
|
|
122
|
+
meta.get("tier") == "T2_global_like",
|
|
123
|
+
f"tier={meta.get('tier')}")
|
|
124
|
+
|
|
125
|
+
print("== 纯度:透传 scored ≡ 现算 _score(不改结果的直接证据)==")
|
|
126
|
+
(res, meta), calls, pairs = count_score(
|
|
127
|
+
cg, lambda: cg.search(QUERY_MISS))
|
|
128
|
+
# 与 search 同口径重建 q/qb(mdcos.py:751-773 的打分输入)
|
|
129
|
+
from md_cg.mdcg import bigrams, normalize_en, en_zh_bigrams
|
|
130
|
+
from md_cg.semantic.unify import unify_query
|
|
131
|
+
q_p = unify_query(QUERY_MISS)
|
|
132
|
+
qb_p = bigrams(normalize_en(q_p)) | en_zh_bigrams(q_p)
|
|
133
|
+
pure = True
|
|
134
|
+
for i, (docs, scored) in enumerate(pairs):
|
|
135
|
+
again = MdCGOS._score(cg, docs, q_p, qb_p, None)
|
|
136
|
+
if again != scored:
|
|
137
|
+
pure = False
|
|
138
|
+
print(f" 第{i}笔复算不一致: {len(docs)} docs")
|
|
139
|
+
check("P1a 每笔 (docs→scored) 复算逐位相等(_score 纯函数)", pure)
|
|
140
|
+
|
|
141
|
+
print("== 确定性:同查询两轮结果逐位一致 ==")
|
|
142
|
+
r1, m1 = cg.search(QUERY_MISS)
|
|
143
|
+
r2, m2 = cg.search(QUERY_MISS)
|
|
144
|
+
ids1 = [(x[0].get("id"), round(x[1], 12)) for x in r1]
|
|
145
|
+
ids2 = [(x[0].get("id"), round(x[1], 12)) for x in r2]
|
|
146
|
+
check("D1a 两轮结果 id 序与分值逐位一致", ids1 == ids2,
|
|
147
|
+
f"n={len(ids1)}/{len(ids2)}")
|
|
148
|
+
rh1, _ = cg.search(QUERY_HIT)
|
|
149
|
+
rh2, _ = cg.search(QUERY_HIT)
|
|
150
|
+
check("D1b T2 路径两轮结果逐位一致",
|
|
151
|
+
[(x[0].get("id"), round(x[1], 12)) for x in rh1]
|
|
152
|
+
== [(x[0].get("id"), round(x[1], 12)) for x in rh2])
|
|
153
|
+
|
|
154
|
+
print("== reach 腿(MDCG_REACH=1):P2-4 模板形态修复 ==")
|
|
155
|
+
# 相邻缺陷(本轮 stash 取证):基类 reach 分支旧 _smap 模板对 scored
|
|
156
|
+
# 的 card dict 调 pooling.doc_key(形参须 (entry,fm,content) 三元组)
|
|
157
|
+
# → MDCG_REACH=1 + 命中集非空即 KeyError: 1(HEAD 原生在位)。
|
|
158
|
+
from md_cg.mdcg import MdCG
|
|
159
|
+
os.environ["MDCG_REACH"] = "1"
|
|
160
|
+
try:
|
|
161
|
+
root2 = tempfile.mkdtemp(prefix="mdcg_score_reach_")
|
|
162
|
+
cg2 = MdCGOS(root2)
|
|
163
|
+
for i in range(30):
|
|
164
|
+
cg2.add("rm_%03d" % i,
|
|
165
|
+
"# 功能名:reach 样本\n# 正文:%s 节点 %d" % (QUERY_HIT, i),
|
|
166
|
+
layer="knowledge")
|
|
167
|
+
cg2.flush()
|
|
168
|
+
try:
|
|
169
|
+
(res_r, meta_r), calls_r, _ = count_score(
|
|
170
|
+
cg2, lambda: cg2.search(QUERY_HIT))
|
|
171
|
+
check("R1a 生产 reach 腿不再 KeyError(HEAD 实测崩)且有结果",
|
|
172
|
+
len(res_r) >= 1, f"n={len(res_r)} tier={meta_r.get('tier')}")
|
|
173
|
+
check("R1b 生产 reach 腿打分 ≤ 2 调(透传无重打)",
|
|
174
|
+
len(calls_r) <= 2, f"calls={calls_r}")
|
|
175
|
+
finally:
|
|
176
|
+
cg2.close()
|
|
177
|
+
shutil.rmtree(root2, ignore_errors=True)
|
|
178
|
+
root3 = tempfile.mkdtemp(prefix="mdcg_score_reachb_")
|
|
179
|
+
cg3 = MdCG(root3)
|
|
180
|
+
for i in range(30):
|
|
181
|
+
cg3.add("bm_%03d" % i,
|
|
182
|
+
"# 功能名:reach 样本\n# 正文:%s 节点 %d" % (QUERY_HIT, i),
|
|
183
|
+
layer="knowledge")
|
|
184
|
+
cg3.flush()
|
|
185
|
+
try:
|
|
186
|
+
res_b, meta_b = cg3.search(QUERY_HIT)
|
|
187
|
+
check("R2a 基类 reach 分支不再 KeyError(stash 取证 HEAD 崩)",
|
|
188
|
+
len(res_b) >= 1, f"n={len(res_b)} tier={meta_b.get('tier')}")
|
|
189
|
+
finally:
|
|
190
|
+
cg3.close()
|
|
191
|
+
shutil.rmtree(root3, ignore_errors=True)
|
|
192
|
+
finally:
|
|
193
|
+
os.environ.pop("MDCG_REACH", None)
|
|
194
|
+
cg.close()
|
|
195
|
+
finally:
|
|
196
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
197
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
198
|
+
|
|
199
|
+
print("\n" + "=" * 60)
|
|
200
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
201
|
+
if FAILS:
|
|
202
|
+
for f in FAILS:
|
|
203
|
+
print(f" - {f}")
|
|
204
|
+
return 0 if FAIL == 0 else 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
sys.exit(main())
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · review_cli 归因守卫(P1 裁决面身份归因退化,DSH 端在役实测)
|
|
3
|
+
|
|
4
|
+
缺陷(修复前):`review_cli._cg` 构造 `Principal(actor="designer-cli", ...)`
|
|
5
|
+
不传 session → `security.Principal.__init__` 每次随机生成 `sess_<uuid12>`;
|
|
6
|
+
accept 落盘经 `MdCGSecure._attribution` 的 setdefault,writer 缺省成裁决者
|
|
7
|
+
designer-cli——于是 DSH 端实测:5 个裁决节点 5 个互不相同的随机会话 id、
|
|
8
|
+
writer 全被 designer-cli 覆盖,原始写入者(dsh-memory,提案记录 actor 有)
|
|
9
|
+
不可追溯。
|
|
10
|
+
|
|
11
|
+
守卫(修复前红):
|
|
12
|
+
① review_cli accept --session <s> → 落盘节点 frontmatter.session == <s>
|
|
13
|
+
② 落盘节点 frontmatter.writer == 提案记录的原始 actor(dsh-memory)
|
|
14
|
+
③ 裁决者身份不丢失:frontmatter.reviewer == "designer-cli"
|
|
15
|
+
④ 稳定性:同一 --session 两次裁决 → 两节点 session 一致
|
|
16
|
+
⑤ 兜底:未传 --session 时环境变量 MDCG_SESSION 生效
|
|
17
|
+
⑥ 仍无(两者皆缺省)→ stderr 告警一行 + 维持随机会话(现状兼容)
|
|
18
|
+
|
|
19
|
+
运行:python -m md_cg.test_review_cli_attribution
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
|
|
29
|
+
from . import nodefile
|
|
30
|
+
from .mdcos import MdCGOS, MdCGSecure
|
|
31
|
+
from .security import Principal
|
|
32
|
+
|
|
33
|
+
PASS = FAIL = 0
|
|
34
|
+
FAILS = []
|
|
35
|
+
|
|
36
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
37
|
+
|
|
38
|
+
DUMMY_KEY = "ab" * 32 # 哑主密钥:子进程绝不触真实 ~/.mdcg/master.key
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check(name, cond, detail=""):
|
|
42
|
+
global PASS, FAIL
|
|
43
|
+
if cond:
|
|
44
|
+
PASS += 1
|
|
45
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
46
|
+
else:
|
|
47
|
+
FAIL += 1
|
|
48
|
+
FAILS.append(name)
|
|
49
|
+
print(f" [FAIL] {name} · {detail}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _child_env(extra=None, remove=("MDCG_SESSION", "DSH_SESSION_ID",
|
|
53
|
+
"MDCG_REDTEAM_REQUIRED")):
|
|
54
|
+
"""子进程环境:哑密钥 + UTF-8 + PYTHONPATH 指向本仓(纪律 15)。
|
|
55
|
+
|
|
56
|
+
remove 里的会话/红队环境变量先摘除——守卫 ①~④⑥ 必须证明 --session
|
|
57
|
+
是唯一来源,不能被外层环境偷跑。
|
|
58
|
+
"""
|
|
59
|
+
env = dict(os.environ)
|
|
60
|
+
for k in remove:
|
|
61
|
+
env.pop(k, None)
|
|
62
|
+
env["MDCG_MASTER_KEY"] = DUMMY_KEY
|
|
63
|
+
env["PYTHONUTF8"] = "1"
|
|
64
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
65
|
+
env["PYTHONPATH"] = REPO + os.pathsep + env.get("PYTHONPATH", "")
|
|
66
|
+
env.update(extra or {})
|
|
67
|
+
return env
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run_cli(root, argv, extra=None):
|
|
71
|
+
"""跑一次 review_cli 子命令(包内入口 python -m md_cg.review_cli)。"""
|
|
72
|
+
return subprocess.run(
|
|
73
|
+
[sys.executable, "-m", "md_cg.review_cli"] + argv,
|
|
74
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
75
|
+
env=_child_env(extra), cwd=REPO)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _propose(root, nid, content):
|
|
79
|
+
"""以原始写入者身份(dsh-memory,DSH 端 agent 形态)入队一条提案。"""
|
|
80
|
+
cg = MdCGSecure(root, principal=Principal(
|
|
81
|
+
actor="dsh-memory", clearance="internal", can_write=True,
|
|
82
|
+
can_admin=False, role="recorder", session="sess_writer_orig"))
|
|
83
|
+
try:
|
|
84
|
+
return cg.propose(nid, content, layer="knowledge", tags=["attr"])
|
|
85
|
+
finally:
|
|
86
|
+
cg.close()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _fm(root, nid):
|
|
90
|
+
"""直读节点文件 frontmatter(不经密级读闸——守卫只看归因字段)。"""
|
|
91
|
+
probe = MdCGOS(root, actor="probe")
|
|
92
|
+
try:
|
|
93
|
+
e = (probe.index.get("nodes") or {}).get(nid)
|
|
94
|
+
if not e:
|
|
95
|
+
return None
|
|
96
|
+
with open(os.path.join(root, e["path"]), encoding="utf-8") as f:
|
|
97
|
+
fm, _c = nodefile.loads(f.read())
|
|
98
|
+
return fm
|
|
99
|
+
finally:
|
|
100
|
+
probe.close()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main():
|
|
104
|
+
roots = []
|
|
105
|
+
try:
|
|
106
|
+
print("\n【①~③】accept --session → 落盘归因(session/writer/reviewer)")
|
|
107
|
+
root_a = tempfile.mkdtemp(prefix="cliattrib_a_")
|
|
108
|
+
roots.append(root_a)
|
|
109
|
+
pid = _propose(root_a, "attr-node-1", "P1 归因守卫:原始写入者应可追溯。")
|
|
110
|
+
r = _run_cli(root_a, ["accept", pid, "--session", "sess_decide_01",
|
|
111
|
+
"--root", root_a, "--reason", "归因守卫"])
|
|
112
|
+
check("review_cli accept --session 退出码 0", r.returncode == 0,
|
|
113
|
+
(r.stdout or r.stderr or "")[:200])
|
|
114
|
+
fm = _fm(root_a, "attr-node-1")
|
|
115
|
+
check("frontmatter.session == 传入 --session",
|
|
116
|
+
bool(fm) and fm.get("session") == "sess_decide_01",
|
|
117
|
+
str(fm and fm.get("session")))
|
|
118
|
+
check("frontmatter.writer == 提案原始 actor(dsh-memory)",
|
|
119
|
+
bool(fm) and fm.get("writer") == "dsh-memory",
|
|
120
|
+
str(fm and fm.get("writer")))
|
|
121
|
+
check("裁决者身份保留于 frontmatter.reviewer == designer-cli",
|
|
122
|
+
bool(fm) and fm.get("reviewer") == "designer-cli",
|
|
123
|
+
str(fm and fm.get("reviewer")))
|
|
124
|
+
|
|
125
|
+
print("\n【④】同一 --session 两次裁决 → 落盘归属一致")
|
|
126
|
+
pid2 = _propose(root_a, "attr-node-2",
|
|
127
|
+
"P1 归因守卫第二提案:同批裁决同会话。")
|
|
128
|
+
r2 = _run_cli(root_a, ["accept", pid2, "--session", "sess_decide_01",
|
|
129
|
+
"--root", root_a, "--reason", "归因守卫"])
|
|
130
|
+
fm2 = _fm(root_a, "attr-node-2")
|
|
131
|
+
check("第二节点 session 与第一节点一致",
|
|
132
|
+
r2.returncode == 0 and bool(fm) and bool(fm2)
|
|
133
|
+
and fm2.get("session") == fm.get("session") == "sess_decide_01",
|
|
134
|
+
"%r vs %r" % (fm and fm.get("session"), fm2 and fm2.get("session")))
|
|
135
|
+
|
|
136
|
+
print("\n【⑤】MDCG_SESSION 环境变量兜底")
|
|
137
|
+
pid3 = _propose(root_a, "attr-node-3",
|
|
138
|
+
"P1 归因守卫第三提案:环境变量会话。")
|
|
139
|
+
r3 = _run_cli(root_a, ["accept", pid3, "--root", root_a,
|
|
140
|
+
"--reason", "归因守卫"],
|
|
141
|
+
extra={"MDCG_SESSION": "sess_env_03"})
|
|
142
|
+
fm3 = _fm(root_a, "attr-node-3")
|
|
143
|
+
check("无 --session 时 MDCG_SESSION 生效",
|
|
144
|
+
r3.returncode == 0 and bool(fm3)
|
|
145
|
+
and fm3.get("session") == "sess_env_03",
|
|
146
|
+
str(fm3 and fm3.get("session")))
|
|
147
|
+
|
|
148
|
+
print("\n【⑥】两者皆缺省 → stderr 告警一行 + 维持随机会话")
|
|
149
|
+
pid4 = _propose(root_a, "attr-node-4",
|
|
150
|
+
"P1 归因守卫第四提案:随机会话告警。")
|
|
151
|
+
r4 = _run_cli(root_a, ["accept", pid4, "--root", root_a,
|
|
152
|
+
"--reason", "归因守卫"])
|
|
153
|
+
fm4 = _fm(root_a, "attr-node-4")
|
|
154
|
+
check("退出码 0(告警不阻断裁决)", r4.returncode == 0,
|
|
155
|
+
(r4.stdout or r4.stderr or "")[:200])
|
|
156
|
+
warn = [ln for ln in (r4.stderr or "").splitlines() if ln.strip()]
|
|
157
|
+
check("stderr 恰一行随机会话告警",
|
|
158
|
+
len(warn) == 1 and "随机会话" in warn[0], str(warn))
|
|
159
|
+
check("缺省仍落随机会话 id(sess_ 前缀,现状兼容)",
|
|
160
|
+
bool(fm4) and str(fm4.get("session", "")).startswith("sess_"),
|
|
161
|
+
str(fm4 and fm4.get("session")))
|
|
162
|
+
check("无 --session 时原始 writer 归因同样在位",
|
|
163
|
+
bool(fm4) and fm4.get("writer") == "dsh-memory",
|
|
164
|
+
str(fm4 and fm4.get("writer")))
|
|
165
|
+
finally:
|
|
166
|
+
for r_ in roots:
|
|
167
|
+
shutil.rmtree(r_, ignore_errors=True)
|
|
168
|
+
|
|
169
|
+
print("\n" + "=" * 64)
|
|
170
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
171
|
+
if FAILS:
|
|
172
|
+
print("失败项:" + ";".join(FAILS))
|
|
173
|
+
return 0 if FAIL == 0 else 1
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
sys.exit(main())
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · review_cli 裁决落盘跨进程可见性守卫(P1b autoflush 第二落点,DSH 端在役实测)
|
|
3
|
+
|
|
4
|
+
缺陷(修复前):`review_cli._cg` 构造 `MdCGSecure(_root(args), principal=p)`
|
|
5
|
+
不传 autoflush → 缺省 64。库的索引增量链路是「写入先经 `_stage` 入内存
|
|
6
|
+
`_dirty` 与内存索引,攒够 autoflush 条才 flush() 落分片日志 `_index_log/`」;
|
|
7
|
+
短命进程的**正常退出**另有 main finally `cg.close()` + 库层 atexit 双兜底
|
|
8
|
+
(mdcg.py `_LIVE_CGS`,2026-09-16 取证),但**被 kill / 崩溃退出时两者都不
|
|
9
|
+
执行**(兜底刻意只覆盖正常退出这一条路径)——裁决落盘的少量节点停在内存
|
|
10
|
+
索引、分片日志从未落,盘面节点文件在而索引链路断。DSH 端在役实测:裁决后
|
|
11
|
+
MCP 读面 read null。对照:mcp_server.py:3695-3702 同症状 2026-09-16 已修
|
|
12
|
+
(server 级 autoflush=1 兜底),review_cli 这个短命工具落点被遗漏——本修复
|
|
13
|
+
补齐,与 server 同款。
|
|
14
|
+
|
|
15
|
+
守卫(四组,全部临时库 + 哑主密钥,绝不触真实 ~/.mdcg):
|
|
16
|
+
① 真实在役入口:subprocess 跑 `python -m md_cg.review_cli accept`(正常
|
|
17
|
+
完成)→ 进程 B(全新 python 子进程 _load_index + read)断言业务节点与
|
|
18
|
+
审计记录节点均可见。
|
|
19
|
+
② kill 形态可见性:进程 A' 等价直接调库落盘 2 条(无任何显式 flush)后
|
|
20
|
+
`os._exit(0)`(模拟被杀:close/atexit 全部跳过)→ 进程 B 全新子进程
|
|
21
|
+
断言读回可见。
|
|
22
|
+
③ 落账契约(autoflush=1 的直接可观察效应,**修复前红**):② 的进程 A'
|
|
23
|
+
被杀退出后,盘面 `_index_log/` 必须已含其全部写入的索引记录——"写入即
|
|
24
|
+
落分片日志",与 mcp_server 的 server 级 autoflush=1 同一契约。
|
|
25
|
+
④ 同病工具面:evidence._open_cg / scripts/linkref_backfill /
|
|
26
|
+
scripts/migrate_restricted 同样补 autoflush=1(前两者行为断言
|
|
27
|
+
.autoflush==1,后两者源级断言构造语句带参)。
|
|
28
|
+
|
|
29
|
+
如实边界:本仓 HEAD 上 ①② 的读面断言在修复前已绿——正常路径由 09-16 的
|
|
30
|
+
review_decide 显式 flush(mdcos.py)+ close/atexit 兜底覆盖;kill 形态的
|
|
31
|
+
重开读面由批次39(2026-09-25)`_load_index` 指纹失配全扫自愈兜住。修复前
|
|
32
|
+
红的只有 ③ 的落账契约与在役旧读面(0.4.x 副本 `_load_index` 盲信快照)——
|
|
33
|
+
后者正是 DSH 端观察到 read null 的形态;本守卫把读面断言一并钉住,防上述
|
|
34
|
+
两层兜底未来被移除时旧症复发。
|
|
35
|
+
|
|
36
|
+
运行:python -m md_cg.test_review_cli_visibility
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import os
|
|
42
|
+
import re
|
|
43
|
+
import shutil
|
|
44
|
+
import subprocess
|
|
45
|
+
import sys
|
|
46
|
+
import tempfile
|
|
47
|
+
|
|
48
|
+
from .mdcg import ShardedLog
|
|
49
|
+
from .mdcos import MdCGSecure
|
|
50
|
+
from .security import Principal
|
|
51
|
+
|
|
52
|
+
PASS = FAIL = 0
|
|
53
|
+
FAILS = []
|
|
54
|
+
|
|
55
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
56
|
+
|
|
57
|
+
DUMMY_KEY = "ab" * 32 # 哑主密钥:子进程绝不触真实 ~/.mdcg/master.key
|
|
58
|
+
|
|
59
|
+
KILL_CHILD = r'''
|
|
60
|
+
import argparse, os, sys
|
|
61
|
+
sys.path.insert(0, sys.argv[2])
|
|
62
|
+
from md_cg import review_cli
|
|
63
|
+
# 等价直接调库落盘:走 review_cli 的生产构造路径 _cg(与 CLI 同一 autoflush),
|
|
64
|
+
# 随后裸 add 两条(无任何显式 flush)——覆盖 review_decide 之外的写入形态。
|
|
65
|
+
cg = review_cli._cg(argparse.Namespace(root=sys.argv[1]))
|
|
66
|
+
sys.stderr.write("staged=0 autoflush=%d\n" % cg.autoflush)
|
|
67
|
+
cg.add("vis-node-1", "P1b 可见性守卫节点一:kill 形态落账。", layer="knowledge",
|
|
68
|
+
tags=["vis:p1b"])
|
|
69
|
+
cg.add("vis-node-2", "P1b 可见性守卫节点二:kill 形态落账。", layer="knowledge",
|
|
70
|
+
tags=["vis:p1b"])
|
|
71
|
+
os._exit(0) # 硬退:close / atexit 一律不执行(模拟被杀)
|
|
72
|
+
'''
|
|
73
|
+
|
|
74
|
+
READ_CHILD = r'''
|
|
75
|
+
import sys
|
|
76
|
+
sys.path.insert(0, sys.argv[2])
|
|
77
|
+
from md_cg.mdcos import MdCGSecure
|
|
78
|
+
from md_cg.security import Principal
|
|
79
|
+
p = Principal(actor="vis-reader", clearance="internal", can_write=True,
|
|
80
|
+
can_admin=True, role="designer", auth_mode="test")
|
|
81
|
+
cg = MdCGSecure(sys.argv[1], principal=p)
|
|
82
|
+
ids = sys.argv[3].split(",")
|
|
83
|
+
print("READ " + " ".join("1" if cg.get(nid) is not None else "0" for nid in ids))
|
|
84
|
+
'''
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check(name, cond, detail=""):
|
|
88
|
+
global PASS, FAIL
|
|
89
|
+
if cond:
|
|
90
|
+
PASS += 1
|
|
91
|
+
print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
|
|
92
|
+
else:
|
|
93
|
+
FAIL += 1
|
|
94
|
+
FAILS.append(name)
|
|
95
|
+
print(f" [FAIL] {name} · {detail}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _child_env(extra=None):
|
|
99
|
+
"""子进程环境:哑密钥 + UTF-8 + PYTHONPATH 指向本仓(纪律 15)。"""
|
|
100
|
+
env = dict(os.environ)
|
|
101
|
+
env["MDCG_MASTER_KEY"] = DUMMY_KEY
|
|
102
|
+
env["PYTHONUTF8"] = "1"
|
|
103
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
104
|
+
env["PYTHONPATH"] = REPO + os.pathsep + env.get("PYTHONPATH", "")
|
|
105
|
+
env.update(extra or {})
|
|
106
|
+
return env
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _run_cli(root, argv):
|
|
110
|
+
"""跑一次 review_cli(包内入口 python -m md_cg.review_cli,真实在役入口)。"""
|
|
111
|
+
return subprocess.run(
|
|
112
|
+
[sys.executable, "-X", "utf8", "-m", "md_cg.review_cli"] + argv,
|
|
113
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
114
|
+
env=_child_env(), cwd=REPO)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _spawn(code, root, arg3=""):
|
|
118
|
+
"""独立子进程跑一段内嵌脚本(进程 A' 杀退形态 / 进程 B 全新读面)。"""
|
|
119
|
+
return subprocess.run(
|
|
120
|
+
[sys.executable, "-X", "utf8", "-c", code, root, REPO, arg3],
|
|
121
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
122
|
+
env=_child_env())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _propose(root, nid, content):
|
|
126
|
+
"""以原始写入者身份入队一条提案(复刻 DSH 端 agent 形态)。"""
|
|
127
|
+
cg = MdCGSecure(root, principal=Principal(
|
|
128
|
+
actor="dsh-memory", clearance="internal", can_write=True,
|
|
129
|
+
can_admin=False, role="recorder", session="sess_writer_orig"))
|
|
130
|
+
try:
|
|
131
|
+
return cg.propose(nid, content, layer="knowledge", tags=["vis:p1b"])
|
|
132
|
+
finally:
|
|
133
|
+
cg.close()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _log_ids(root):
|
|
137
|
+
"""读盘面分片日志的记录 id 集(落账契约的观察点)。"""
|
|
138
|
+
log_dir = os.path.join(root, "_index_log")
|
|
139
|
+
if not os.path.isdir(log_dir):
|
|
140
|
+
return None # 目录不存在 = 一条都没落过
|
|
141
|
+
return {rec.get("id") for rec in ShardedLog.read_all(log_dir)
|
|
142
|
+
if rec.get("id")}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
roots = []
|
|
147
|
+
try:
|
|
148
|
+
# ---------- ① 真实在役入口:review_cli accept → 进程 B 读 ----------
|
|
149
|
+
print("\n【①】subprocess 跑 review_cli accept(正常完成)→ 进程 B 读")
|
|
150
|
+
root_1 = tempfile.mkdtemp(prefix="clivis_1_")
|
|
151
|
+
roots.append(root_1)
|
|
152
|
+
pid = _propose(root_1, "vis-acc-node",
|
|
153
|
+
"P1b 可见性守卫:accept 落盘后其它进程立即可见。")
|
|
154
|
+
r = _run_cli(root_1, ["accept", pid, "--root", root_1,
|
|
155
|
+
"--reason", "可见性守卫"])
|
|
156
|
+
check("review_cli accept 退出码 0", r.returncode == 0,
|
|
157
|
+
(r.stdout or r.stderr or "")[:200])
|
|
158
|
+
b = _spawn(READ_CHILD, root_1, "vis-acc-node") # 全新子进程 _load_index+read
|
|
159
|
+
m = re.search(r"READ (\d)", b.stdout or "")
|
|
160
|
+
check("进程 B 读回业务节点 vis-acc-node 可见",
|
|
161
|
+
bool(m) and m.group(1) == "1",
|
|
162
|
+
(b.stdout or b.stderr or "")[:200])
|
|
163
|
+
pa = MdCGSecure(root_1, principal=Principal(
|
|
164
|
+
actor="vis-reader", clearance="internal", can_write=True,
|
|
165
|
+
can_admin=True, role="designer", auth_mode="test"))
|
|
166
|
+
try:
|
|
167
|
+
recs = pa.review_records(pid)
|
|
168
|
+
finally:
|
|
169
|
+
pa.close()
|
|
170
|
+
check("进程 B 侧审计记录节点可见(review_records 命中 1 条)",
|
|
171
|
+
len(recs) == 1, str(recs))
|
|
172
|
+
|
|
173
|
+
# ---------- ②③ kill 形态:调库落盘后 os._exit → 落账契约 + 读 ----------
|
|
174
|
+
print("\n【②③】进程 A' 调库落 2 条即 os._exit(模拟被杀)→ 落账契约 + 进程 B 读")
|
|
175
|
+
root_2 = tempfile.mkdtemp(prefix="clivis_2_")
|
|
176
|
+
roots.append(root_2)
|
|
177
|
+
a = _spawn(KILL_CHILD, root_2)
|
|
178
|
+
check("进程 A' 经生产构造 _cg(autoflush=1 回显)",
|
|
179
|
+
a.returncode == 0 and "autoflush=1" in (a.stderr or ""),
|
|
180
|
+
(a.stderr or "")[:200])
|
|
181
|
+
ids = _log_ids(root_2)
|
|
182
|
+
check("③ 落账契约:A' 被杀退出后 _index_log 已含全部 2 条写入记录"
|
|
183
|
+
"(autoflush=1 写毕即落日志;修复前 autoflush=64 未达阈值=红)",
|
|
184
|
+
ids is not None and {"vis-node-1", "vis-node-2"} <= ids,
|
|
185
|
+
"log_ids=%s" % sorted(ids or []))
|
|
186
|
+
b2 = _spawn(READ_CHILD, root_2, "vis-node-1,vis-node-2") # 全新子进程读
|
|
187
|
+
m2 = re.search(r"READ (\d) (\d)", b2.stdout or "")
|
|
188
|
+
check("② 进程 B 读回 kill 形态的两条节点均可见",
|
|
189
|
+
bool(m2) and m2.group(1) == "1" and m2.group(2) == "1",
|
|
190
|
+
(b2.stdout or b2.stderr or "")[:200])
|
|
191
|
+
|
|
192
|
+
# ---------- ④ 同病工具面:三处同款补 autoflush=1 ----------
|
|
193
|
+
print("\n【④】同病工具面 autoflush=1")
|
|
194
|
+
cg_rv = None
|
|
195
|
+
try:
|
|
196
|
+
from . import review_cli as _rv
|
|
197
|
+
cg_rv = _rv._cg(argparse.Namespace(root=root_1))
|
|
198
|
+
check("review_cli._cg 构造 autoflush==1", cg_rv.autoflush == 1,
|
|
199
|
+
str(cg_rv.autoflush))
|
|
200
|
+
finally:
|
|
201
|
+
if cg_rv is not None:
|
|
202
|
+
cg_rv.close()
|
|
203
|
+
from . import evidence as _ev
|
|
204
|
+
cg_ev = _ev._open_cg(root_1)
|
|
205
|
+
try:
|
|
206
|
+
check("evidence._open_cg 构造 autoflush==1", cg_ev.autoflush == 1,
|
|
207
|
+
str(cg_ev.autoflush))
|
|
208
|
+
finally:
|
|
209
|
+
cg_ev.close()
|
|
210
|
+
for rel, anchor in (
|
|
211
|
+
("scripts/linkref_backfill.py",
|
|
212
|
+
"cg = MdCGSecure(a.root, principal=p, autoflush=1)"),
|
|
213
|
+
("scripts/migrate_restricted.py", None)):
|
|
214
|
+
with open(os.path.join(REPO, rel), encoding="utf-8") as f:
|
|
215
|
+
src = f.read()
|
|
216
|
+
if anchor is not None:
|
|
217
|
+
ok = anchor in src
|
|
218
|
+
else:
|
|
219
|
+
# migrate_restricted 构造跨多行:断言构造语句带 autoflush=1
|
|
220
|
+
ok = re.search(r"MdCGSecure\([^)]*?auth_mode=\"test\"\), "
|
|
221
|
+
r"autoflush=1\)", src, re.S) is not None
|
|
222
|
+
check("%s 构造语句带 autoflush=1(源级断言)" % rel, ok)
|
|
223
|
+
finally:
|
|
224
|
+
for r_ in roots:
|
|
225
|
+
shutil.rmtree(r_, ignore_errors=True)
|
|
226
|
+
|
|
227
|
+
print("\n" + "=" * 64)
|
|
228
|
+
print(f"PASS={PASS} FAIL={FAIL}")
|
|
229
|
+
if FAILS:
|
|
230
|
+
print("失败项:" + ";".join(FAILS))
|
|
231
|
+
return 0 if FAIL == 0 else 1
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__":
|
|
235
|
+
sys.exit(main())
|