@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,168 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""读隔离守卫:因果链面(causal_chain / explain_chain / causal_path / 检索
|
|
3
|
+
chain 路 provenance)不得泄漏不可见节点。
|
|
4
|
+
|
|
5
|
+
缺陷形态(2026-09-25 检索域新发现,报告编号 N130 与他域冲突,以 where 定位,
|
|
6
|
+
high):MdCGOS.causal_chain / explain_chain(mdcos.py:3491-3506)裸转调
|
|
7
|
+
chain.walk / chain.explain,causal_path(mdcos.py:3544→predict.py:1020)BFS
|
|
8
|
+
直用 chain.adjacency——而 chain.adjacency(chain.py:127-159)以 cg.index 全量
|
|
9
|
+
节点 frontmatter 建出边表,全程不经 cg.get / MdCGSecure._readable。任何持
|
|
10
|
+
read op 的低权令牌(ROLE_SPECS 全表 ops_allow 均含 read;mdcg_causal 的
|
|
11
|
+
_MDCG_OP_REQUIRE 即为 "read")可:①对不可见节点起链;②看到链上不可见节点
|
|
12
|
+
id、边条件文本与下游拓扑;③检索 chain 路 provenance(_path_chain→
|
|
13
|
+
expand_from_seeds,同走 adjacency)把全链与秘密条件文本附带在可见节点条目上。
|
|
14
|
+
|
|
15
|
+
修复:在 chain.adjacency 单点接可见性闸——cg 提供 `_chain_visible(nid)` 谓词
|
|
16
|
+
(MdCGSecure 注入 = 索引有条目 ∧ _readable(e);基类不注入行为零变化)时,
|
|
17
|
+
不可见节点的出边整体不入表、可见节点指向不可见目标的边截断、层级合成边同
|
|
18
|
+
闸;缓存键带上闸存在位。单点覆盖 walk / explain / causal_path /
|
|
19
|
+
expand_from_seeds(检索 chain 路 provenance)全部消费者——比三方法各自覆盖
|
|
20
|
+
多堵住 provenance 面,且预测器其余 adjacency 消费者(causal roadmap /
|
|
21
|
+
共同父节点等)一并隔离。_readable 的绑定档判定恒用 principal.session
|
|
22
|
+
(mdcos.py:3901-3906),实例身份稳定,adjacency 缓存无跨身份串台面。
|
|
23
|
+
|
|
24
|
+
红项 = R1-R5(链/路径/解释/检索 provenance 四面泄漏复现);
|
|
25
|
+
G 项锁语义不变(designer/admin 可见性不变、可见-可见边链照常展开、get 隔离
|
|
26
|
+
基线)。
|
|
27
|
+
运行:python -m md_cg.test_chain_read_isolate
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import shutil
|
|
33
|
+
import sys
|
|
34
|
+
import tempfile
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from .mdcos import MdCGSecure
|
|
38
|
+
from .security import Principal
|
|
39
|
+
except ImportError: # 直接脚本运行
|
|
40
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
41
|
+
from md_cg.mdcos import MdCGSecure
|
|
42
|
+
from md_cg.security import Principal
|
|
43
|
+
|
|
44
|
+
PASS = FAIL = 0
|
|
45
|
+
FAILS = []
|
|
46
|
+
|
|
47
|
+
_SECRET_COND = "Y侧秘密条件文本YY"
|
|
48
|
+
_SECRET_ID = "Y秘密"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ok(cond, label):
|
|
52
|
+
global PASS, FAIL
|
|
53
|
+
if cond:
|
|
54
|
+
PASS += 1
|
|
55
|
+
print(f" OK {label}")
|
|
56
|
+
else:
|
|
57
|
+
FAIL += 1
|
|
58
|
+
FAILS.append(label)
|
|
59
|
+
print(f" FAIL {label}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _body(name):
|
|
63
|
+
return (f"# 功能名:{name}\n# 生效条件:无条件\n# 子功能:无\n"
|
|
64
|
+
f"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n正文。")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_chain_iso_")
|
|
69
|
+
try:
|
|
70
|
+
root = os.path.join(tmp, "kg")
|
|
71
|
+
# 写者:designer(can_admin → private 档可见),建链 X→Y→Z
|
|
72
|
+
dw = Principal(actor="designer-1", role="designer", can_write=True,
|
|
73
|
+
can_admin=True, clearance="secret")
|
|
74
|
+
cg = MdCGSecure(root, principal=dw, master_key=bytes(range(32)))
|
|
75
|
+
cg.add("X苹果", _body("X苹果"), layer="knowledge", sensitivity="internal",
|
|
76
|
+
edges=[{"target": _SECRET_ID, "relation_type": "causal",
|
|
77
|
+
"condition": _SECRET_COND, "confidence": 0.9}])
|
|
78
|
+
cg.add(_SECRET_ID, _body("Y秘密"), layer="knowledge", sensitivity="private",
|
|
79
|
+
edges=[{"target": "Z结论", "relation_type": "causal",
|
|
80
|
+
"condition": "到Z的显性条件", "confidence": 0.9}])
|
|
81
|
+
cg.add("Z结论", _body("Z结论"), layer="knowledge", sensitivity="internal")
|
|
82
|
+
# 可见-可见对照链:A内→B内(reader 应照常展开)
|
|
83
|
+
cg.add("A内", _body("A内"), layer="knowledge", sensitivity="internal",
|
|
84
|
+
edges=[{"target": "B内", "relation_type": "causal",
|
|
85
|
+
"condition": "显性条件AB", "confidence": 0.8}])
|
|
86
|
+
cg.add("B内", _body("B内"), layer="knowledge", sensitivity="internal")
|
|
87
|
+
cg.close()
|
|
88
|
+
|
|
89
|
+
# 读者:record 角色,clearance=internal(低于 private),异会话
|
|
90
|
+
rd = Principal(actor="reader-1", role="record", can_write=True,
|
|
91
|
+
can_admin=False, clearance="internal",
|
|
92
|
+
layers_allow=["knowledge"], ops_allow=["read"])
|
|
93
|
+
r = MdCGSecure(root, principal=rd, master_key=bytes(range(32)))
|
|
94
|
+
|
|
95
|
+
# R0 基线:get 读隔离本就生效(旧码即过,锁语义)
|
|
96
|
+
ok(r.get(_SECRET_ID) is None, "R0 基线:reader.get(Y秘密) 隔离生效")
|
|
97
|
+
|
|
98
|
+
# R1 链展开:不可见中转节点 id + 边条件文本不得出现在链输出
|
|
99
|
+
chains = r.causal_chain("X苹果")
|
|
100
|
+
blob = repr(chains)
|
|
101
|
+
ok(_SECRET_ID not in blob,
|
|
102
|
+
"R1a causal_chain(X苹果) 输出不含不可见节点 id")
|
|
103
|
+
ok(_SECRET_COND not in blob,
|
|
104
|
+
"R1b causal_chain(X苹果) 输出不含秘密边条件文本")
|
|
105
|
+
|
|
106
|
+
# R2 不可见节点起链:应与节点不存在同形(空链)
|
|
107
|
+
ok(r.causal_chain(_SECRET_ID) == [],
|
|
108
|
+
"R2 causal_chain(Y秘密) 不可见起点返回空链")
|
|
109
|
+
|
|
110
|
+
# R3 解释面:同源 walk,渲染输出不得带秘密
|
|
111
|
+
ex = r.explain_chain("X苹果")
|
|
112
|
+
ok(_SECRET_ID not in repr(ex) and _SECRET_COND not in repr(ex),
|
|
113
|
+
"R3 explain_chain(X苹果) 渲染输出零泄漏")
|
|
114
|
+
|
|
115
|
+
# R4 因果路径:经不可见中转的路径不可达
|
|
116
|
+
cp = r.causal_path("X苹果", "Z结论")
|
|
117
|
+
ok(not cp.get("reachable") and _SECRET_ID not in repr(cp)
|
|
118
|
+
and _SECRET_COND not in repr(cp),
|
|
119
|
+
"R4 causal_path(X苹果→Z结论) 不可达且零泄漏")
|
|
120
|
+
|
|
121
|
+
# R5 检索 chain 路:provenance/chain 字段(evidence ⑤ 所指)不得携带
|
|
122
|
+
# 不可见全链。注意范围:可读节点**自身 frontmatter 的 edges 元数据**
|
|
123
|
+
# 随节点视图返回是另一回事(读者本就可读该节点的 md 原文),不在
|
|
124
|
+
# 本缺陷(链展开面)范围。prov 形态:nid → [{"path","rank",
|
|
125
|
+
# "chain"?,"conditions"?}, …];结果 4 元组第 4 位同源。
|
|
126
|
+
res, meta = r.search_rrf("苹果", paths=("lexical", "chain"), record=False)
|
|
127
|
+
chain_fields = []
|
|
128
|
+
for entries in ((meta or {}).get("provenance") or {}).values():
|
|
129
|
+
chain_fields.extend(en for en in entries
|
|
130
|
+
if isinstance(en, dict) and en.get("chain"))
|
|
131
|
+
for t in res:
|
|
132
|
+
if isinstance(t, tuple) and len(t) >= 4 and isinstance(t[3], list):
|
|
133
|
+
chain_fields.extend(en for en in t[3]
|
|
134
|
+
if isinstance(en, dict) and en.get("chain"))
|
|
135
|
+
ok(all(_SECRET_ID not in repr(en.get("chain")) for en in chain_fields),
|
|
136
|
+
"R5a search_rrf provenance 链不含不可见节点 id")
|
|
137
|
+
ok(all(_SECRET_COND not in repr(en.get("conditions"))
|
|
138
|
+
for en in chain_fields),
|
|
139
|
+
"R5b search_rrf provenance 条件不含秘密文本")
|
|
140
|
+
|
|
141
|
+
# G1 可见-可见链照常展开(闸不误伤本职语义)
|
|
142
|
+
ok(r.causal_chain("A内") != [],
|
|
143
|
+
"G1a reader 对可见链 A内→B内 照常展开")
|
|
144
|
+
ok("显性条件AB" in repr(r.causal_chain("A内")),
|
|
145
|
+
"G1b 可见边条件文本照常返回")
|
|
146
|
+
|
|
147
|
+
# G2 designer/admin 可见性不变(private 档链对设计者完整可见)
|
|
148
|
+
cg2 = MdCGSecure(root, principal=dw, master_key=bytes(range(32)))
|
|
149
|
+
dch = cg2.causal_chain("X苹果")
|
|
150
|
+
ok(any(_SECRET_ID in c.get("nodes", []) for c in dch),
|
|
151
|
+
"G2a designer causal_chain 照常含 private 节点(admin 可见性不变)")
|
|
152
|
+
ok(any(_SECRET_COND in c.get("conditions", []) for c in dch),
|
|
153
|
+
"G2b designer 照常见到边条件文本")
|
|
154
|
+
cg2.close()
|
|
155
|
+
r.close()
|
|
156
|
+
finally:
|
|
157
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
158
|
+
|
|
159
|
+
print("\n" + "=" * 60)
|
|
160
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
161
|
+
if FAILS:
|
|
162
|
+
for f in FAILS:
|
|
163
|
+
print(f" - {f}")
|
|
164
|
+
return 0 if FAIL == 0 else 1
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main())
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_datapath_device_name.py · Windows 保留设备名末段吞路径的守卫(issue #39 相邻缺陷)
|
|
3
|
+
|
|
4
|
+
现场(Windows 实测):``ntpath.abspath`` 经 ``GetFullPathNameW`` 归一路径时,若
|
|
5
|
+
路径**末段是 Windows 保留设备名**(aux/con/nul/prn/com1-9/lpt1-9 等),整个路径
|
|
6
|
+
被吞成设备命名空间形态——env 设 ``MDCG_AUX_ROOT=<临时目录>\\aux`` 时
|
|
7
|
+
``datapath.aux_root()`` 返回 ``\\\\.\\aux``,原目录语义**静默丢失**:密钥/令牌/
|
|
8
|
+
信任面被指到不存在的设备路径,覆盖键静默失联(不报错、最难察觉的失败形态)。
|
|
9
|
+
``abspath`` 是纯字符串归一**不触盘**,故守卫只需把 env 指向含保留名末段的
|
|
10
|
+
**不存在路径**即可实弹复现——绝不能真的创建 aux 末段目录(Win32
|
|
11
|
+
CreateDirectory 同样被设备名劫持)。
|
|
12
|
+
|
|
13
|
+
修复(本守卫的红绿两态锚点):``md_cg/datapath.py`` 新增模块级
|
|
14
|
+
``_abs_host_path(p, env_key)``——Windows(os.name=="nt")上归一结果以
|
|
15
|
+
``\\\\.\\`` 前缀开头即抛 ValueError(消息含键名与「保留设备名」字样及改法);
|
|
16
|
+
``state_root``/``data_root``/``mdcg_root``/``aux_root`` 四处 env 覆盖面(含
|
|
17
|
+
paths.json 直值面)改走该辅助。非 Windows 平台 ``\\\\.\\aux`` 是合法目录名
|
|
18
|
+
字面量,**不判定**(§4 在 POSIX 上反断言「不抛」,守住平台闸本身)。
|
|
19
|
+
承接面:``mcp_server.main()`` 启动早期对 root/aux 各探一次,ValueError →
|
|
20
|
+
stderr 告警 + return 2(对照 `_md_cg_` 前缀守卫先例「启动即拒绝,不带病运行」)。
|
|
21
|
+
|
|
22
|
+
隔离纪律:四个覆盖键(MDCG_STATE_ROOT/MDCG_DATA_ROOT/MDCG_ROOT/MDCG_AUX_ROOT)
|
|
23
|
+
与 DSH_HOME 以 os.environ 副本注入、finally 逐键还原;探针路径全部是
|
|
24
|
+
tempfile.mkdtemp 下**不存在**的纯字符串(或正常临时子目录),不创建任何含
|
|
25
|
+
保留名的真实目录,不触真实令牌库与真实状态根。env 直设即命中各函数第一分支,
|
|
26
|
+
paths.json 回落面零参与。
|
|
27
|
+
|
|
28
|
+
自检 floor:断言总数低于 FLOOR 视为失败——防「探针面失效 → 假绿」。
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import shutil
|
|
34
|
+
import sys
|
|
35
|
+
import tempfile
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
39
|
+
except AttributeError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
from .datapath import (ENV_AUX_ROOT, ENV_DATA_ROOT, ENV_MDCG_ROOT,
|
|
43
|
+
ENV_STATE_ROOT, aux_root, data_root, mdcg_root,
|
|
44
|
+
state_root)
|
|
45
|
+
|
|
46
|
+
FLOOR = 8 # 断言总数下限(防假绿)
|
|
47
|
+
PASS, FAIL = 0, 0
|
|
48
|
+
|
|
49
|
+
#: 守卫注入/还原面(含 DSH_HOME——state_root 的第二档,一并与真实环境隔离)
|
|
50
|
+
_GUARD_KEYS = (ENV_STATE_ROOT, ENV_DATA_ROOT, ENV_MDCG_ROOT, ENV_AUX_ROOT,
|
|
51
|
+
"DSH_HOME")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def check(name, cond, detail=""):
|
|
55
|
+
global PASS, FAIL
|
|
56
|
+
if cond:
|
|
57
|
+
PASS += 1
|
|
58
|
+
print(f" [ok] {name}")
|
|
59
|
+
else:
|
|
60
|
+
FAIL += 1
|
|
61
|
+
print(f" [FAIL] {name} {detail}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _probe_reject(fn, key, tail, tmp):
|
|
65
|
+
"""设 env=<tmp>/<tail>(不存在的纯字符串路径)→ 返回 (抛出异常, 异常消息)。
|
|
66
|
+
|
|
67
|
+
红态(修复前)fn() 不抛而返回 \\\\.\\<tail> 形态——由调用方断言兜住。
|
|
68
|
+
"""
|
|
69
|
+
os.environ[key] = os.path.join(tmp, tail)
|
|
70
|
+
try:
|
|
71
|
+
fn()
|
|
72
|
+
except ValueError as ve:
|
|
73
|
+
return True, str(ve)
|
|
74
|
+
return False, ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _func_window(src: str, func_name: str) -> str:
|
|
78
|
+
"""取 src 中 ``def <func_name>`` 起到下一个顶层 ``def ``/``class`` 的源窗口。"""
|
|
79
|
+
start = src.find(f"def {func_name}(")
|
|
80
|
+
if start < 0:
|
|
81
|
+
return ""
|
|
82
|
+
nxt = src.find("\ndef ", start + 1)
|
|
83
|
+
nxt_cls = src.find("\nclass ", start + 1)
|
|
84
|
+
ends = [e for e in (nxt, nxt_cls) if e > 0]
|
|
85
|
+
return src[start:min(ends)] if ends else src[start:]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
saved = {k: os.environ.get(k) for k in _GUARD_KEYS}
|
|
90
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_devname_")
|
|
91
|
+
try:
|
|
92
|
+
for k in _GUARD_KEYS:
|
|
93
|
+
os.environ.pop(k, None)
|
|
94
|
+
|
|
95
|
+
# ---- 1. 实弹红转绿核心:MDCG_AUX_ROOT 末段 aux → aux_root() 必须拒绝 ----
|
|
96
|
+
print("[1] 实弹核心:MDCG_AUX_ROOT=<临时目录>\\aux(不存在,纯字符串)")
|
|
97
|
+
raised, msg = _probe_reject(aux_root, ENV_AUX_ROOT, "aux", tmp)
|
|
98
|
+
if os.name == "nt":
|
|
99
|
+
check("1a aux_root() 抛 ValueError(红态:静默返回 \\\\.\\aux 不抛)",
|
|
100
|
+
raised, f"msg={msg[:160]!r}")
|
|
101
|
+
check("1b 消息含覆盖键名 MDCG_AUX_ROOT(定位误配来源)",
|
|
102
|
+
ENV_AUX_ROOT in msg, f"msg={msg[:160]!r}")
|
|
103
|
+
check("1c 消息含「保留设备名」字样(说清劫持机理)",
|
|
104
|
+
"保留设备名" in msg, f"msg={msg[:160]!r}")
|
|
105
|
+
else:
|
|
106
|
+
# 与 [4] 同口径:平台门在 `_abs_host_path`(`os.name == "nt"`),POSIX 上
|
|
107
|
+
# `\\.\aux` 是合法目录名字面量,不判定 → 必须不抛且原样返回。
|
|
108
|
+
check("1a 非 Windows 不判定(\\\\.\\aux 是合法目录名字面量,原样返回不抛)",
|
|
109
|
+
not raised, f"msg={msg[:160]!r}")
|
|
110
|
+
check("1b 非 Windows 不判定:不产生保留设备名拒因(消息为空)",
|
|
111
|
+
msg == "", f"msg={msg[:160]!r}")
|
|
112
|
+
check("1c 非 Windows 不判定:aux_root() 原样返回该路径(未被吞成设备路径)",
|
|
113
|
+
aux_root() == os.path.abspath(os.path.join(tmp, "aux")),
|
|
114
|
+
f"aux_root()={aux_root()!r}")
|
|
115
|
+
|
|
116
|
+
# ---- 2. 同族四面:state/data/mdcg 各 env 面同样拒绝 ----
|
|
117
|
+
print("[2] 同族 env 面(MDCG_STATE_ROOT/MDCG_DATA_ROOT/MDCG_ROOT)")
|
|
118
|
+
for key, fn, tail in ((ENV_STATE_ROOT, state_root, "con"),
|
|
119
|
+
(ENV_DATA_ROOT, data_root, "nul"),
|
|
120
|
+
(ENV_MDCG_ROOT, mdcg_root, "aux")):
|
|
121
|
+
raised, msg = _probe_reject(fn, key, tail, tmp)
|
|
122
|
+
if os.name == "nt":
|
|
123
|
+
check(f"2·{key}: {fn.__name__}() 抛 ValueError 且消息含键名"
|
|
124
|
+
"(env 直设命中第一分支,paths.json 零参与)",
|
|
125
|
+
raised and key in msg,
|
|
126
|
+
f"raised={raised} msg={msg[:160]!r}")
|
|
127
|
+
else:
|
|
128
|
+
# 与 [4] 同口径:平台门在 `_abs_host_path`,POSIX 不判定保留设备名。
|
|
129
|
+
check(f"2·{key}: 非 Windows 不判定(末段 {tail} 是合法目录名,"
|
|
130
|
+
f"{fn.__name__}() 原样返回不抛)",
|
|
131
|
+
not raised
|
|
132
|
+
and fn() == os.path.abspath(os.path.join(tmp, tail)),
|
|
133
|
+
f"raised={raised} msg={msg[:160]!r} got={fn()!r}")
|
|
134
|
+
|
|
135
|
+
# ---- 3. 正常路径不误伤:无保留名末段 → abspath 原样返回 ----
|
|
136
|
+
print("[3] 正常路径不误伤(临时子目录,末段无保留名)")
|
|
137
|
+
normals = {}
|
|
138
|
+
for name in ("state", "data", "cgroot", "auxroot"):
|
|
139
|
+
p = os.path.join(tmp, name)
|
|
140
|
+
os.makedirs(p, exist_ok=True)
|
|
141
|
+
normals[name] = p
|
|
142
|
+
os.environ[ENV_STATE_ROOT] = normals["state"]
|
|
143
|
+
os.environ[ENV_DATA_ROOT] = normals["data"]
|
|
144
|
+
os.environ[ENV_MDCG_ROOT] = normals["cgroot"]
|
|
145
|
+
os.environ[ENV_AUX_ROOT] = normals["auxroot"]
|
|
146
|
+
check("3a 四面正常值全部原样返回(不抛、不变形)",
|
|
147
|
+
state_root() == os.path.abspath(normals["state"])
|
|
148
|
+
and data_root() == os.path.abspath(normals["data"])
|
|
149
|
+
and mdcg_root() == os.path.abspath(normals["cgroot"])
|
|
150
|
+
and aux_root() == os.path.abspath(normals["auxroot"]),
|
|
151
|
+
f"state={state_root()!r} data={data_root()!r} "
|
|
152
|
+
f"mdcg={mdcg_root()!r} aux={aux_root()!r}")
|
|
153
|
+
|
|
154
|
+
# ---- 4. 保留名清单抽查(aux_root 面):Windows 全抛;POSIX 反断言不抛 ----
|
|
155
|
+
print("[4] 保留名清单抽查(aux/con/nul/com1/lpt1)")
|
|
156
|
+
for tail in ("aux", "con", "nul", "com1", "lpt1"):
|
|
157
|
+
raised, msg = _probe_reject(aux_root, ENV_AUX_ROOT, tail, tmp)
|
|
158
|
+
if os.name == "nt":
|
|
159
|
+
check(f"4·{tail}: aux_root() 抛 ValueError(GetFullPathNameW "
|
|
160
|
+
"劫持被拦)", raised, f"msg={msg[:160]!r}")
|
|
161
|
+
else:
|
|
162
|
+
check(f"4·{tail}: 非 Windows 不判定(\\\\.\\{tail} 是合法目录名"
|
|
163
|
+
"字面量,原样返回不抛)",
|
|
164
|
+
not raised, f"msg={msg[:160]!r}")
|
|
165
|
+
|
|
166
|
+
# ---- 5. 源断言:辅助函数在、四处 env 面改走它、未改面逐字保留 ----
|
|
167
|
+
print("[5] 源断言(datapath.py 四处 env 面 + mcp_server 承接)")
|
|
168
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
169
|
+
with open(os.path.join(here, "datapath.py"), encoding="utf-8") as fh:
|
|
170
|
+
src = fh.read()
|
|
171
|
+
check("5a datapath.py 定义 _abs_host_path",
|
|
172
|
+
"def _abs_host_path(" in src)
|
|
173
|
+
for fname in ("state_root", "data_root", "mdcg_root", "aux_root"):
|
|
174
|
+
check(f"5·{fname}: 函数体改走 _abs_host_path(env 覆盖面全覆盖)",
|
|
175
|
+
"_abs_host_path(" in _func_window(src, fname))
|
|
176
|
+
check("5e plugin_root 基于 __file__ 的 abspath 未改(非攻击/误配面,逐字保留)",
|
|
177
|
+
"os.path.abspath(__file__)" in _func_window(src, "plugin_root"))
|
|
178
|
+
with open(os.path.join(here, "mcp_server.py"), encoding="utf-8") as fh:
|
|
179
|
+
msrc = fh.read()
|
|
180
|
+
mwin = msrc[msrc.find("def main"):msrc.find("def main") + 2000]
|
|
181
|
+
check("5f mcp_server.main() 启动早期探 root/aux 且 ValueError → return 2",
|
|
182
|
+
"_probe_root" in mwin and "_probe_aux" in mwin
|
|
183
|
+
and "except ValueError" in mwin and "return 2" in mwin,
|
|
184
|
+
"main() 首段缺承接受理")
|
|
185
|
+
|
|
186
|
+
# ---- 6. 守卫自检 floor ----
|
|
187
|
+
print("[6] 守卫自检")
|
|
188
|
+
check(f"6a 断言总数 ≥ {FLOOR}(防探针面失效假绿)",
|
|
189
|
+
PASS + FAIL >= FLOOR, f"total={PASS + FAIL}")
|
|
190
|
+
finally:
|
|
191
|
+
for k, v in saved.items():
|
|
192
|
+
if v is None:
|
|
193
|
+
os.environ.pop(k, None)
|
|
194
|
+
else:
|
|
195
|
+
os.environ[k] = v
|
|
196
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
197
|
+
|
|
198
|
+
print(f"\n=== datapath device-name tests: {PASS} passed, {FAIL} failed ===")
|
|
199
|
+
sys.exit(1 if FAIL else 0)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
sys.exit(main())
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""_emit 尾部 covered_neg 裸读守卫(v9 留档复测·修复,缺陷迭代第 14 轮)。
|
|
4
|
+
|
|
5
|
+
缺陷形态(缺陷报告实测,60 knowledge + 8 rejected 临时库,逐位复现):
|
|
6
|
+
_emit 尾部「把被负记忆覆盖的查询也作为结果条目返回」对 neg_coverage[:3]
|
|
7
|
+
逐条裸 open+nodefile.loads(mdcg.py _emit 尾,不经 self._read 不在读缓存
|
|
8
|
+
包装面)——每条命中负层的查询恒 ≤3 次盘读(Q1 loads=71=全池 60+负层
|
|
9
|
+
扫描 8+尾部 3;Q2 同查询增量恰 3);负层主面 IO 已被读缓存覆盖(残余项)。
|
|
10
|
+
|
|
11
|
+
修复口径:尾部改走 self._read(nc)(读缓存覆盖,增量归零);_read 同款
|
|
12
|
+
_node_disk_path 边界闸(P2-20——索引被污染时不得绕过统一校验读 root 外
|
|
13
|
+
文件)、OSError→(None,None) 与旧 continue 同义;负层写路径(add/
|
|
14
|
+
add_rejected → _stage 标脏带 path)在读缓存脏集精确失效面内——覆写后
|
|
15
|
+
尾部条目即时见新内容,不陈旧。
|
|
16
|
+
|
|
17
|
+
本守卫(调用计数断言形态,非时间阈值):
|
|
18
|
+
E1 热缓存后同查询 loads 增量 == 0(旧实现恒 3 → 红);
|
|
19
|
+
E2 功能在位:rejected 前 3 条仍在结果、covered_neg 计数/tier/scanned
|
|
20
|
+
不变(尾部不是被静默关掉);
|
|
21
|
+
E3 两轮查询结果 ids 逐位一致(确定性);
|
|
22
|
+
E4 读缓存开/关(MDCG_READ_CACHE=1/0)同库同查询 ids 逐位一致;
|
|
23
|
+
E5 覆写负层节点后尾部条目见新内容(脏集精确失效联动,不陈旧)。
|
|
24
|
+
|
|
25
|
+
运行:python -m md_cg.test_emit_negtail_cache
|
|
26
|
+
"""
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
|
|
32
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
33
|
+
|
|
34
|
+
from md_cg import nodefile # noqa: E402
|
|
35
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
36
|
+
|
|
37
|
+
PASS = 0
|
|
38
|
+
FAIL = 0
|
|
39
|
+
FAILS = []
|
|
40
|
+
|
|
41
|
+
N = 60
|
|
42
|
+
NNEG = 8
|
|
43
|
+
QUERY = "已被否决的做法"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check(name, cond, detail=""):
|
|
47
|
+
global PASS, FAIL
|
|
48
|
+
if cond:
|
|
49
|
+
PASS += 1
|
|
50
|
+
print(f" OK {name}")
|
|
51
|
+
else:
|
|
52
|
+
FAIL += 1
|
|
53
|
+
FAILS.append(name)
|
|
54
|
+
print(f" FAIL {name} {detail}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_lib(root):
|
|
58
|
+
cg = MdCGOS(root)
|
|
59
|
+
for i in range(N):
|
|
60
|
+
cg.add("mem_%03d" % i,
|
|
61
|
+
"# 功能名:正层样本 %d\n# 正文:蜂群调度 节点 %d" % (i, i),
|
|
62
|
+
layer="knowledge")
|
|
63
|
+
for j in range(NNEG):
|
|
64
|
+
cg.add("r%d" % j,
|
|
65
|
+
"# 假设:%s 第 %d 号\n# 原因:与既有验证结论冲突" % (QUERY, j),
|
|
66
|
+
layer="rejected", verification_basis="test")
|
|
67
|
+
cg.flush()
|
|
68
|
+
return cg
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def count_loads(fn):
|
|
72
|
+
st = {"n": 0}
|
|
73
|
+
real = nodefile.loads
|
|
74
|
+
|
|
75
|
+
def _spy(*a, **kw):
|
|
76
|
+
st["n"] += 1
|
|
77
|
+
return real(*a, **kw)
|
|
78
|
+
|
|
79
|
+
nodefile.loads = _spy
|
|
80
|
+
try:
|
|
81
|
+
return fn(), st["n"]
|
|
82
|
+
finally:
|
|
83
|
+
nodefile.loads = real
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def neg_ids(res):
|
|
87
|
+
return [x[0].get("id") for x in res if str(x[0].get("id")).startswith("rejected/")]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main():
|
|
91
|
+
root = tempfile.mkdtemp(prefix="mdcg_negtail_")
|
|
92
|
+
cg = None
|
|
93
|
+
try:
|
|
94
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
95
|
+
cg = build_lib(root)
|
|
96
|
+
(r1, m1), n1 = count_loads(lambda: cg.search(QUERY))
|
|
97
|
+
ids1 = [x[0].get("id") for x in r1]
|
|
98
|
+
check("E0a 首查口径复现:covered_neg=8、tier=T3、scanned=60",
|
|
99
|
+
len(m1.get("covered_neg") or []) == NNEG
|
|
100
|
+
and m1.get("tier") == "T3_global_scan"
|
|
101
|
+
and m1.get("scanned") == N,
|
|
102
|
+
f"covered={len(m1.get('covered_neg') or [])} "
|
|
103
|
+
f"tier={m1.get('tier')} scanned={m1.get('scanned')}")
|
|
104
|
+
check("E0b rejected 前 3 条在结果(尾部功能在位)",
|
|
105
|
+
neg_ids(r1)[:3] == ["rejected/r0.md", "rejected/r1.md",
|
|
106
|
+
"rejected/r2.md"],
|
|
107
|
+
f"neg={neg_ids(r1)[:3]}")
|
|
108
|
+
|
|
109
|
+
(r2, m2), n2 = count_loads(lambda: cg.search(QUERY))
|
|
110
|
+
ids2 = [x[0].get("id") for x in r2]
|
|
111
|
+
check("E1a 热缓存后同查询 loads 增量 == 0(旧实现恒 3)",
|
|
112
|
+
n2 == 0, f"loads={n2}")
|
|
113
|
+
check("E2a rejected 前 3 条仍逐位在位、meta 口径不变",
|
|
114
|
+
neg_ids(r2)[:3] == ["rejected/r0.md", "rejected/r1.md",
|
|
115
|
+
"rejected/r2.md"]
|
|
116
|
+
and len(m2.get("covered_neg") or []) == NNEG
|
|
117
|
+
and m2.get("tier") == "T3_global_scan"
|
|
118
|
+
and m2.get("scanned") == N)
|
|
119
|
+
check("E3a 两轮查询结果 ids 逐位一致", ids1 == ids2,
|
|
120
|
+
f"n={len(ids1)}/{len(ids2)}")
|
|
121
|
+
|
|
122
|
+
# E4 读缓存开/关结果一致
|
|
123
|
+
os.environ["MDCG_READ_CACHE"] = "0"
|
|
124
|
+
try:
|
|
125
|
+
cg0 = MdCGOS(root)
|
|
126
|
+
r_off, m_off = cg0.search(QUERY)
|
|
127
|
+
cg0.close()
|
|
128
|
+
check("E4a 读缓存开/关同库同查询 ids 逐位一致",
|
|
129
|
+
[x[0].get("id") for x in r_off] == ids2)
|
|
130
|
+
finally:
|
|
131
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
132
|
+
|
|
133
|
+
# E5 覆写负层节点 → 尾部条目即时见新内容(脏集精确失效联动)
|
|
134
|
+
cg.add("r0", "# 假设:%s 第 0 号\n# 原因:覆写后的新结论ZZZ" % QUERY,
|
|
135
|
+
layer="rejected", verification_basis="test")
|
|
136
|
+
r3, _ = cg.search(QUERY)
|
|
137
|
+
neg3 = next((x[0] for x in r3
|
|
138
|
+
if x[0].get("id") == "rejected/r0.md"), None)
|
|
139
|
+
check("E5a 覆写 r0 后尾部条目见新内容(不陈旧)",
|
|
140
|
+
neg3 is not None and "新结论ZZZ" in (neg3.get("content") or ""),
|
|
141
|
+
f"content={str((neg3 or {}).get('content'))[:40]}")
|
|
142
|
+
cg.close()
|
|
143
|
+
finally:
|
|
144
|
+
os.environ.pop("MDCG_READ_CACHE", None)
|
|
145
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
146
|
+
|
|
147
|
+
print("\n" + "=" * 60)
|
|
148
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
149
|
+
if FAILS:
|
|
150
|
+
for f in FAILS:
|
|
151
|
+
print(f" - {f}")
|
|
152
|
+
return 0 if FAIL == 0 else 1
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
sys.exit(main())
|