@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,185 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""N130 守卫:verify(verdict="falsified") 的删除必须过「不可遗忘」保护。
|
|
3
|
+
|
|
4
|
+
缺陷形态(2026-09-25 引擎域新发现,编号 N130,high):verify() 的 falsified
|
|
5
|
+
分支(mdcg.py:3188-3195)对原节点 os.remove **永久硬删**,全程不经
|
|
6
|
+
protect.guard_forget / require_layer_write / override——self/anchor 层、
|
|
7
|
+
protected 标记、importance≥0.7 的受保护节点可被一次 verify 调用一键删除,
|
|
8
|
+
无快照、无留痕、不进 trash;对照 forget 路径(mdcos.py:2294)同一个删除
|
|
9
|
+
动作需 require_admin + guard_forget 双闸(旧版本先快照、动作全程留痕)。
|
|
10
|
+
|
|
11
|
+
攻击面:任何持 verify op 的主体(verify 角色 ops_allow 即含 verify,
|
|
12
|
+
designer ops=["*"] 天然含)对任意 node_id 调 cg(op=verify,
|
|
13
|
+
verdict=falsified) 一次调用永久删除;无需 override 无需 admin。
|
|
14
|
+
|
|
15
|
+
修复:①falsified 分支在任何副作用(add_rejected / os.remove)之前接
|
|
16
|
+
protect.guard_forget(受保护节点需显式 override=True——旧版本自动快照 +
|
|
17
|
+
_protected_audit 留痕,与 forget 路径同一语义;拒绝抛 ProtectionError,
|
|
18
|
+
原节点原样保留);②MdCGSecure.verify 对 falsified 按原节点层做
|
|
19
|
+
require_layer_write(与 add/add_rejected 同一闸口——verify 角色
|
|
20
|
+
layers_allow=["rejected","contextual"],本就「不得改被验证内容」,
|
|
21
|
+
tokens.py:171 显式 forbidden)。
|
|
22
|
+
|
|
23
|
+
红项 = R1-R7(四类受保护节点一键硬删复现 + secure 面层写缺口);
|
|
24
|
+
G 项锁语义不变(普通节点 falsified 负记忆化、override 显式放行 + 快照留痕)。
|
|
25
|
+
运行:python -m md_cg.test_n130_verify_falsified_protect
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from . import protect
|
|
36
|
+
from .mdcos import MdCGOS, MdCGSecure
|
|
37
|
+
from .security import AccessDenied, Principal
|
|
38
|
+
except ImportError: # 直接脚本运行
|
|
39
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
40
|
+
from md_cg import protect
|
|
41
|
+
from md_cg.mdcos import MdCGOS, MdCGSecure
|
|
42
|
+
from md_cg.security import AccessDenied, Principal
|
|
43
|
+
|
|
44
|
+
PASS = FAIL = 0
|
|
45
|
+
FAILS = []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ok(cond, label):
|
|
49
|
+
global PASS, FAIL
|
|
50
|
+
if cond:
|
|
51
|
+
PASS += 1
|
|
52
|
+
print(f" OK {label}")
|
|
53
|
+
else:
|
|
54
|
+
FAIL += 1
|
|
55
|
+
FAILS.append(label)
|
|
56
|
+
print(f" FAIL {label}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_BODY = "# 功能名:N130 守卫\n\n# 生效条件:无条件\n\n正文。\n"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _exists(cg, nid):
|
|
63
|
+
"""索引条目在且盘面文件在(硬删后两者都会消失)。"""
|
|
64
|
+
e = (cg.index.get("nodes") or {}).get(nid)
|
|
65
|
+
return (e is not None
|
|
66
|
+
and os.path.exists(os.path.join(cg.root, e["path"])))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _falsified_must_reject(cg, nid, label):
|
|
70
|
+
"""对 nid 做 falsified:期望 ProtectionError 且节点原样保留。"""
|
|
71
|
+
raised = None
|
|
72
|
+
try:
|
|
73
|
+
cg.verify(nid, "N130 守卫的反例证据", "falsified")
|
|
74
|
+
except protect.ProtectionError as exc:
|
|
75
|
+
raised = exc
|
|
76
|
+
ok(raised is not None, f"{label}1 falsified 受保护节点被拒(ProtectionError)")
|
|
77
|
+
ok(_exists(cg, nid), f"{label}2 拒绝后索引与盘面文件原样保留")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _seed(cg):
|
|
81
|
+
"""四类受保护节点 + 一类普通节点(对照)。"""
|
|
82
|
+
cg.add("n_self", _BODY, layer="self")
|
|
83
|
+
cg.add("n_anchor", _BODY, layer="anchor")
|
|
84
|
+
cg.add("n_prot", _BODY, layer="knowledge")
|
|
85
|
+
protect.mark(cg, "n_prot", "N130 守卫:显式保护标记")
|
|
86
|
+
cg.add("n_imp", _BODY, layer="knowledge", importance=0.9)
|
|
87
|
+
cg.add("n_plain", _BODY, layer="knowledge")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _base_shape(tmp):
|
|
91
|
+
"""A:基类引擎面(无令牌)——四类受保护节点不得被 falsified 一键硬删。"""
|
|
92
|
+
print("--- A 基类引擎面:guard_forget 前置")
|
|
93
|
+
root = os.path.join(tmp, "base")
|
|
94
|
+
cg = MdCGOS(root)
|
|
95
|
+
_seed(cg)
|
|
96
|
+
|
|
97
|
+
_falsified_must_reject(cg, "n_self", "R1_self")
|
|
98
|
+
_falsified_must_reject(cg, "n_anchor", "R2_anchor")
|
|
99
|
+
_falsified_must_reject(cg, "n_prot", "R3_protected")
|
|
100
|
+
_falsified_must_reject(cg, "n_imp", "R4_importance0.9")
|
|
101
|
+
|
|
102
|
+
ok(all(_exists(cg, n) for n in
|
|
103
|
+
("n_self", "n_anchor", "n_prot", "n_imp")),
|
|
104
|
+
"R5 四次拒绝后四个受保护节点全部健在")
|
|
105
|
+
ok(not os.path.isdir(os.path.join(cg.root, protect.HISTORY_DIR)),
|
|
106
|
+
"R6 拒绝路径零副作用(未创建 _protected_history)")
|
|
107
|
+
|
|
108
|
+
# G1:普通节点 falsified 语义不变(负记忆化 + 原节点移除)
|
|
109
|
+
r = cg.verify("n_plain", "普通节点反例", "falsified")
|
|
110
|
+
ok(r.get("action") == "falsified", "G1a 普通 knowledge 节点 falsified 照常放行")
|
|
111
|
+
ok(not _exists(cg, "n_plain"), "G1b 原 knowledge 节点按负记忆化移除(语义不变)")
|
|
112
|
+
neg = [e for e in (cg.index.get("nodes") or {}).values()
|
|
113
|
+
if e.get("layer") == "rejected"]
|
|
114
|
+
ok(len(neg) == 1, "G1c rejected 层出现负记忆条目(语义不变)")
|
|
115
|
+
|
|
116
|
+
# G2:override=True 显式放行 + 快照留痕(与 forget 路径同一能力面)
|
|
117
|
+
r2 = cg.verify("n_prot", "显式 override 的反例", "falsified", override=True)
|
|
118
|
+
ok(r2.get("action") == "falsified", "G2a override=True 显式放行 falsified")
|
|
119
|
+
ok(not _exists(cg, "n_prot"), "G2b override 放行后原节点移除")
|
|
120
|
+
hist = protect.history(cg, "n_prot")
|
|
121
|
+
ok(len(hist) == 1, "G2c override 放行前旧版本已快照进 _protected_history")
|
|
122
|
+
audit = os.path.join(cg.root, protect.AUDIT_FILE)
|
|
123
|
+
ok(os.path.exists(audit) and "override_forget" in open(
|
|
124
|
+
audit, encoding="utf-8").read(),
|
|
125
|
+
"G2d override_forget 动作已留痕 _protected_audit.jsonl")
|
|
126
|
+
cg.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _secure_shape(tmp):
|
|
130
|
+
"""B:MdCGSecure 面——falsified 删除按原节点层做 require_layer_write。"""
|
|
131
|
+
print("--- B 安全面:层写权限校验")
|
|
132
|
+
root = os.path.join(tmp, "secure")
|
|
133
|
+
verify_p = Principal(actor="verifier-1", role="verify", can_write=True,
|
|
134
|
+
can_admin=False, clearance="internal",
|
|
135
|
+
layers_allow=["rejected", "contextual"],
|
|
136
|
+
ops_allow=["info", "read", "verify"])
|
|
137
|
+
designer_p = Principal(actor="designer-1", role="designer", can_write=True,
|
|
138
|
+
can_admin=True, clearance="secret")
|
|
139
|
+
# 预置用 designer(knowledge 层只有可写该层的身份能建),攻击用 verify 角色
|
|
140
|
+
cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
|
|
141
|
+
cg.add("s_kn", _BODY, layer="knowledge") # verify 角色无权写 knowledge
|
|
142
|
+
cg.add("s_ctx", _BODY, layer="contextual") # verify 角色本职可写
|
|
143
|
+
cg.close()
|
|
144
|
+
|
|
145
|
+
# R7:verify 角色 falsified knowledge 节点 → 层写拒绝,节点原样保留
|
|
146
|
+
cg = MdCGSecure(root, principal=verify_p, master_key=bytes(range(32)))
|
|
147
|
+
raised = None
|
|
148
|
+
try:
|
|
149
|
+
cg.verify("s_kn", "verify 角色反例", "falsified")
|
|
150
|
+
except AccessDenied as exc:
|
|
151
|
+
raised = exc
|
|
152
|
+
ok(raised is not None, "R7a verify 角色 falsified knowledge 层被拒(AccessDenied)")
|
|
153
|
+
ok(_exists(cg, "s_kn"), "R7b 拒绝后 knowledge 节点索引与盘面原样保留")
|
|
154
|
+
|
|
155
|
+
# G3:verify 角色 falsified contextual 节点 → 本职可写,照常负记忆化
|
|
156
|
+
r = cg.verify("s_ctx", "verify 角色反例(contextual)", "falsified")
|
|
157
|
+
ok(r.get("action") == "falsified", "G3a verify 角色 falsified contextual 层放行")
|
|
158
|
+
ok(not _exists(cg, "s_ctx"), "G3b contextual 节点按负记忆化移除(语义不变)")
|
|
159
|
+
cg.close()
|
|
160
|
+
|
|
161
|
+
# R8:designer(层写不限)falsified self 层节点仍被 guard_forget 拦截
|
|
162
|
+
cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
|
|
163
|
+
cg.add("s_self", _BODY, layer="self")
|
|
164
|
+
_falsified_must_reject(cg, "s_self", "R8_designer_self")
|
|
165
|
+
cg.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main():
|
|
169
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_n130_guard_")
|
|
170
|
+
try:
|
|
171
|
+
_base_shape(tmp)
|
|
172
|
+
_secure_shape(tmp)
|
|
173
|
+
finally:
|
|
174
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
175
|
+
|
|
176
|
+
print("\n" + "=" * 60)
|
|
177
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
178
|
+
if FAILS:
|
|
179
|
+
for f in FAILS:
|
|
180
|
+
print(f" - {f}")
|
|
181
|
+
return 0 if FAIL == 0 else 1
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
sys.exit(main())
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""N131 守卫:review_decide(merge) 的目标写入必须过 add 同款双闸。
|
|
3
|
+
|
|
4
|
+
缺陷形态(2026-09-25 引擎域新发现,编号 N131,high):review_decide 的
|
|
5
|
+
merge 分支(mdcos.py:2221-2255)把提案内容直接 _write_node 追加进任意
|
|
6
|
+
merge_into 目标节点,绕过 guard_write 与 require_layer_write——accept/edit
|
|
7
|
+
走 self.add()(:2211)有 MdCGSecure.add :3769 require_layer_write 与基类
|
|
8
|
+
add mdcg.py:1311 guard_write 双闸,merge 直写零闸;mcp_server review_decide
|
|
9
|
+
注释「accept/edit 落节点时的层写仍由下游 require_layer_write 拦截」对 merge
|
|
10
|
+
不成立,orchestr 角色 forbidden「anchor/self/goals/knowledge 层」
|
|
11
|
+
(tokens.py:123)被打穿。
|
|
12
|
+
|
|
13
|
+
攻击面:持 review+write op 的令牌(orchestr 的 ops_allow 两者皆含,
|
|
14
|
+
tokens.py:120;designer 全权)——propose 任意内容 → review_decide(pid,
|
|
15
|
+
decision="merge", merge_into=<self/anchor/immutable/knowledge 任意节点 id>)
|
|
16
|
+
注入任意文本。
|
|
17
|
+
|
|
18
|
+
修复:merge 分支在 _write_node 之前接与 add 同款双闸——principal 在位先
|
|
19
|
+
require_layer_write(目标层, 目标敏感度)(与 MdCGSecure.add 同序同错型),
|
|
20
|
+
再 guard_write(self/anchor 层、immutable 标记,对照 add :1311;override
|
|
21
|
+
显式传参时快照+留痕放行,与 add 能力面对齐);闸在 _record_decision 之前
|
|
22
|
+
抛出——裁决不落库、提案留 pending(审计节点写入本就在 try/except 内,
|
|
23
|
+
闸放那里会被吞成 record_error,形同虚设)。
|
|
24
|
+
|
|
25
|
+
红项 = R1-R4(self/immutable 目标引擎面 + knowledge/self 目标 orchestr 面
|
|
26
|
+
注入复现);G 项锁语义不变(普通目标 merge 追加、orchestr 本职 contextual
|
|
27
|
+
目标放行、designer override 放行)+ 两对照(orchestr add self 被拒 /
|
|
28
|
+
designer add 覆 immutable 被拒——闸原本就在,只是 merge 绕过)。
|
|
29
|
+
运行:python -m md_cg.test_n131_merge_gate
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import shutil
|
|
35
|
+
import sys
|
|
36
|
+
import tempfile
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from . import protect
|
|
40
|
+
from .mdcos import MdCGOS, MdCGSecure
|
|
41
|
+
from .security import AccessDenied, Principal
|
|
42
|
+
except ImportError: # 直接脚本运行
|
|
43
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
44
|
+
from md_cg import protect
|
|
45
|
+
from md_cg.mdcos import MdCGOS, MdCGSecure
|
|
46
|
+
from md_cg.security import AccessDenied, Principal
|
|
47
|
+
|
|
48
|
+
PASS = FAIL = 0
|
|
49
|
+
FAILS = []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ok(cond, label):
|
|
53
|
+
global PASS, FAIL
|
|
54
|
+
if cond:
|
|
55
|
+
PASS += 1
|
|
56
|
+
print(f" OK {label}")
|
|
57
|
+
else:
|
|
58
|
+
FAIL += 1
|
|
59
|
+
FAILS.append(label)
|
|
60
|
+
print(f" FAIL {label}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_MARK = "攻击者注入段落"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _body(name):
|
|
67
|
+
return (f"# 功能名:{name}\n# 生效条件:无条件\n# 子功能:无\n"
|
|
68
|
+
f"# 执行:无\n# 验证方式:test\n# 不适用条件:无\n\n原始正文。")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _content_of(cg, nid):
|
|
72
|
+
n = cg.get(nid)
|
|
73
|
+
return (n or {}).get("content") or ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _merge_must_reject(cg, pid, target, exc_type, label):
|
|
77
|
+
"""merge 进 target:期望 exc_type 且目标内容原样。"""
|
|
78
|
+
raised = None
|
|
79
|
+
try:
|
|
80
|
+
cg.review_decide(pid, "merge", merge_into=target)
|
|
81
|
+
except exc_type as exc:
|
|
82
|
+
raised = exc
|
|
83
|
+
ok(raised is not None, f"{label}1 merge 进 {target} 被拒({exc_type.__name__})")
|
|
84
|
+
ok(_MARK not in _content_of(cg, target),
|
|
85
|
+
f"{label}2 拒绝后 {target} 正文无注入内容")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _base_shape(tmp):
|
|
89
|
+
"""A:基类引擎面(无令牌)——self/immutable 目标不得被 merge 直写。"""
|
|
90
|
+
print("--- A 基类引擎面:guard_write 闸")
|
|
91
|
+
root = os.path.join(tmp, "base")
|
|
92
|
+
cg = MdCGOS(root)
|
|
93
|
+
cg.add("tgt_self", _body("自我目标"), layer="self")
|
|
94
|
+
cg.add("tgt_imm", _body("不可覆盖目标"), layer="knowledge", immutable=True)
|
|
95
|
+
cg.add("tgt_plain", _body("普通目标"), layer="knowledge")
|
|
96
|
+
ok(protect.is_immutable(cg, "tgt_imm")[0],
|
|
97
|
+
"A0 预置生效:tgt_imm 判定为不可覆盖")
|
|
98
|
+
for nid in ("p_self", "p_imm", "p_plain"):
|
|
99
|
+
cg.propose(nid, _body(nid) + "\n" + _MARK + "\n", layer="knowledge")
|
|
100
|
+
|
|
101
|
+
_merge_must_reject(cg, next(r["pid"] for r in cg.review_list()
|
|
102
|
+
if r["id"] == "p_self"),
|
|
103
|
+
"tgt_self", protect.ProtectionError, "R1_self")
|
|
104
|
+
_merge_must_reject(cg, next(r["pid"] for r in cg.review_list()
|
|
105
|
+
if r["id"] == "p_imm"),
|
|
106
|
+
"tgt_imm", protect.ProtectionError, "R2_immutable")
|
|
107
|
+
|
|
108
|
+
# G1:普通目标 merge 照常放行(test_merge_upsert / test_p2 语义不变)
|
|
109
|
+
pid_plain = next(r["pid"] for r in cg.review_list() if r["id"] == "p_plain")
|
|
110
|
+
r = cg.review_decide(pid_plain, "merge", merge_into="tgt_plain")
|
|
111
|
+
ok(r.get("ok") and r.get("node_id") == "tgt_plain",
|
|
112
|
+
"G1a 普通 knowledge 目标 merge 照常放行")
|
|
113
|
+
ok(_MARK in _content_of(cg, "tgt_plain"),
|
|
114
|
+
"G1b 追加内容进入目标正文(merge 本职不变)")
|
|
115
|
+
cg.close()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _secure_shape(tmp):
|
|
119
|
+
"""B:MdCGSecure 面——merge 按目标层做 require_layer_write(orchestr 面)。"""
|
|
120
|
+
print("--- B 安全面:orchestr 层写校验")
|
|
121
|
+
root = os.path.join(tmp, "secure")
|
|
122
|
+
designer_p = Principal(actor="designer-1", role="designer", can_write=True,
|
|
123
|
+
can_admin=True, clearance="secret")
|
|
124
|
+
orch_p = Principal(actor="orch-1", role="orchestr", can_write=True,
|
|
125
|
+
can_admin=False, clearance="internal",
|
|
126
|
+
layers_allow=["contextual", "unresolved", "rejected"],
|
|
127
|
+
ops_allow=["info", "route", "read", "write", "review"])
|
|
128
|
+
cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
|
|
129
|
+
cg.add("t_kn", _body("知识目标"), layer="knowledge")
|
|
130
|
+
cg.add("t_ctx", _body("情境目标"), layer="contextual")
|
|
131
|
+
cg.add("t_self", _body("自我目标"), layer="self")
|
|
132
|
+
cg.close()
|
|
133
|
+
|
|
134
|
+
# 对照 R3:orchestr 直接 add 写 self 层被拒(身份确实受限,闸原本就在)
|
|
135
|
+
cg = MdCGSecure(root, principal=orch_p, master_key=bytes(range(32)))
|
|
136
|
+
denied = None
|
|
137
|
+
try:
|
|
138
|
+
cg.add("x_self", _body("越权"), layer="self")
|
|
139
|
+
except AccessDenied as exc:
|
|
140
|
+
denied = exc
|
|
141
|
+
ok(denied is not None, "R3 对照:orchestr add 写 self 层被拒(AccessDenied)")
|
|
142
|
+
|
|
143
|
+
# R4/R5:orchestr merge 注入 knowledge / self 层目标——旧码双写路径全绕
|
|
144
|
+
#(提案内容须互异:propose 按 payload_hash 幂等去重,同内容返回既有 pid)
|
|
145
|
+
pid_kn = cg.propose("a_kn", _body("提案注入知识") + "\n" + _MARK + "知识\n",
|
|
146
|
+
layer="contextual")
|
|
147
|
+
_merge_must_reject(cg, pid_kn, "t_kn", AccessDenied, "R4_orch_knowledge")
|
|
148
|
+
pid_self = cg.propose("a_self", _body("提案注入自我") + "\n" + _MARK + "自我\n",
|
|
149
|
+
layer="contextual")
|
|
150
|
+
_merge_must_reject(cg, pid_self, "t_self", AccessDenied, "R5_orch_self")
|
|
151
|
+
|
|
152
|
+
# G2:orchestr 本职可写层(contextual)目标 merge 照常放行
|
|
153
|
+
pid_ctx = cg.propose("a_ctx", _body("提案注入情境") + "\n" + _MARK + "情境\n",
|
|
154
|
+
layer="contextual")
|
|
155
|
+
r = cg.review_decide(pid_ctx, "merge", merge_into="t_ctx")
|
|
156
|
+
ok(r.get("ok") and _MARK in _content_of(cg, "t_ctx"),
|
|
157
|
+
"G2 orchestr merge 进本职 contextual 目标放行且内容追加(本职不变)")
|
|
158
|
+
cg.close()
|
|
159
|
+
|
|
160
|
+
# 对照 R6:designer add 覆写 immutable 既有节点被拒(guard_write 对照)+
|
|
161
|
+
# G3:designer merge 带 override 进 immutable 目标显式放行(快照+留痕)
|
|
162
|
+
cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
|
|
163
|
+
cg.add("t_imm2", _body("不可覆盖"), layer="knowledge", immutable=True)
|
|
164
|
+
prot = None
|
|
165
|
+
try:
|
|
166
|
+
cg.add("t_imm2", _body("覆写"), layer="knowledge")
|
|
167
|
+
except protect.ProtectionError as exc:
|
|
168
|
+
prot = exc
|
|
169
|
+
ok(prot is not None, "R6 对照:designer add 覆写 immutable 节点被拒"
|
|
170
|
+
"(ProtectionError)")
|
|
171
|
+
cg.close()
|
|
172
|
+
|
|
173
|
+
cg = MdCGSecure(root, principal=designer_p, master_key=bytes(range(32)))
|
|
174
|
+
pid_ov = cg.propose("a_ov", _body("提案") + "\n" + _MARK + "\n",
|
|
175
|
+
layer="knowledge")
|
|
176
|
+
r = cg.review_decide(pid_ov, "merge", merge_into="t_imm2", override=True)
|
|
177
|
+
ok(r.get("ok") and _MARK in _content_of(cg, "t_imm2"),
|
|
178
|
+
"G3a designer override=True merge 进 immutable 目标显式放行")
|
|
179
|
+
hist = protect.history(cg, "t_imm2")
|
|
180
|
+
ok(len(hist) >= 1, "G3b override 放行前旧版本已快照 _protected_history")
|
|
181
|
+
audit = os.path.join(cg.root, protect.AUDIT_FILE)
|
|
182
|
+
ok(os.path.exists(audit) and "override_write" in open(
|
|
183
|
+
audit, encoding="utf-8").read(),
|
|
184
|
+
"G3c override_write 动作已留痕 _protected_audit.jsonl")
|
|
185
|
+
cg.close()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main():
|
|
189
|
+
tmp = tempfile.mkdtemp(prefix="mdcg_n131_guard_")
|
|
190
|
+
try:
|
|
191
|
+
_base_shape(tmp)
|
|
192
|
+
_secure_shape(tmp)
|
|
193
|
+
finally:
|
|
194
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
195
|
+
|
|
196
|
+
print("\n" + "=" * 60)
|
|
197
|
+
print(f"结果:PASS {PASS} / FAIL {FAIL}")
|
|
198
|
+
if FAILS:
|
|
199
|
+
for f in FAILS:
|
|
200
|
+
print(f" - {f}")
|
|
201
|
+
return 0 if FAIL == 0 else 1
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
sys.exit(main())
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_none_id_write_guard —— None.md 脏节点写入面守卫(DSH 在役库实测缺陷 P3)
|
|
3
|
+
|
|
4
|
+
现场(DSH 端在役库实测):``knowledge/orphan/None.md`` 出现在检索与 route 候选中
|
|
5
|
+
参与打分。取证链(2026-09-26,临时库实弹复现):
|
|
6
|
+
|
|
7
|
+
1. 批次 24(2a57c2a2,2026-09-24)之前 ``add()`` 对 node_id **零校验**,
|
|
8
|
+
``path = os.path.join(d, f"{node_id}.md")`` 直拼——``add(None, ...)`` 直落
|
|
9
|
+
``None.md``(旧版直接源头,git 取证 ``git show 2a57c2a2^:md_cg/mdcg.py``)。
|
|
10
|
+
2. 现行代码 ``add(None)``/``add("")`` 已被 P0-1 白名单拒绝(mdcg.py _NODE_ID_RE),
|
|
11
|
+
**但字符串 ``"None"``(Python ``str(None)`` 的污染形态)形状合法照常放行**:
|
|
12
|
+
实弹复现 ``add("None", ...)`` → 生成 ``knowledge/orphan/None.md``。
|
|
13
|
+
``"null"``(TS/JS ``String(null)`` 形态)同族放行。这是现行写入面残留洞。
|
|
14
|
+
3. 盘上存量 ``None.md`` 经 ``_scan_nodes`` 全库扫描收录为 ``id="None"``,
|
|
15
|
+
``search``/route 候选参与打分(复现实测命中)——与 DSH 病理现象完全吻合。
|
|
16
|
+
|
|
17
|
+
修复(本守卫的红绿两态锚点):``md_cg/mdcg.py`` ``add()`` 写入边界把空语义值的
|
|
18
|
+
字符串化污染形态(整串精确等值 ``"None"`` / ``"null"``,**大小写敏感、不扩子串**)
|
|
19
|
+
结构化拒绝(ValueError,fail-closed)——add 是全部写入链(writepipe 执行器 /
|
|
20
|
+
review accept / identity / consolidate / branches)落盘的唯一真源边界,堵一处即
|
|
21
|
+
全链路堵死。``_move_layer`` 等直写点只对已在索引中的既有节点操作,不新造 id。
|
|
22
|
+
|
|
23
|
+
清理面(不在本守卫内,运维建议):存量 None.md 属历史脏数据——删除
|
|
24
|
+
``<root>/knowledge/orphan/None.md`` 文件后 ``rebuild_index()`` 即从索引与检索/
|
|
25
|
+
route 候选中消失([6] 的收录取证即清理有效性依据);勿用 ``add("None",...)``
|
|
26
|
+
覆写清理(修复后该形态被拒)。
|
|
27
|
+
|
|
28
|
+
隔离纪律:全程 ``tempfile.mkdtemp`` 临时库,MDCG_ROOT/MDCG_TOKEN_FILE 不参与
|
|
29
|
+
(库实例以显式 root 构造),不触真实令牌库与真实在役数据目录。
|
|
30
|
+
|
|
31
|
+
自检 floor:断言总数低于 FLOOR 视为失败——防「探针面失效 → 假绿」。
|
|
32
|
+
|
|
33
|
+
运行:python -X utf8 -m md_cg.test_none_id_write_guard
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import os
|
|
38
|
+
import sys
|
|
39
|
+
import tempfile
|
|
40
|
+
|
|
41
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
42
|
+
|
|
43
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
44
|
+
|
|
45
|
+
FLOOR = 12 # 断言总数下限(防假绿)
|
|
46
|
+
passed = failed = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check(name, cond, detail=""):
|
|
50
|
+
global passed, failed
|
|
51
|
+
if cond:
|
|
52
|
+
passed += 1
|
|
53
|
+
print(" [PASS] " + name)
|
|
54
|
+
else:
|
|
55
|
+
failed += 1
|
|
56
|
+
print(" [FAIL] " + name + " " + str(detail)[:200])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _none_md_hits(root):
|
|
60
|
+
"""盘面上所有 None.md(大小写不敏感——Windows 盘面本就不区分)。"""
|
|
61
|
+
hits = []
|
|
62
|
+
for dirpath, _dirs, files in os.walk(root):
|
|
63
|
+
for f in files:
|
|
64
|
+
if f.lower() == "none.md":
|
|
65
|
+
hits.append(os.path.relpath(os.path.join(dirpath, f), root)
|
|
66
|
+
.replace("\\", "/"))
|
|
67
|
+
return sorted(hits)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
root = tempfile.mkdtemp(prefix="mdcg_none_guard_")
|
|
72
|
+
try:
|
|
73
|
+
return _run(root)
|
|
74
|
+
finally:
|
|
75
|
+
import shutil
|
|
76
|
+
shutil.rmtree(root, ignore_errors=True) # 测毕清理(临时库纪律)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run(root):
|
|
80
|
+
cg = MdCGOS(root)
|
|
81
|
+
|
|
82
|
+
print("[1] 红形态:字符串 \"None\"(str(None) 污染形态)在写入边界拒绝")
|
|
83
|
+
rejected = False
|
|
84
|
+
try:
|
|
85
|
+
nid = cg.add("None", "污染形态内容 无条件")
|
|
86
|
+
check("add(\"None\") 被拒绝", False, "返回了 " + repr(nid))
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
rejected = True
|
|
89
|
+
check("add(\"None\") 被拒绝", True)
|
|
90
|
+
check("拒绝是结构化 ValueError 且消息点名形态",
|
|
91
|
+
"None" in str(e) and "node_id" in str(e), str(e)[:120])
|
|
92
|
+
check("盘面无 None.md 生成", _none_md_hits(root) == [], _none_md_hits(root))
|
|
93
|
+
check("索引无 id='None' 条目", "None" not in cg.index["nodes"])
|
|
94
|
+
|
|
95
|
+
print("[2] 红形态:字符串 \"null\"(String(null) 跨语言同族形态)")
|
|
96
|
+
try:
|
|
97
|
+
nid = cg.add("null", "跨语言污染形态内容 无条件")
|
|
98
|
+
check("add(\"null\") 被拒绝", False, "返回了 " + repr(nid))
|
|
99
|
+
except ValueError:
|
|
100
|
+
check("add(\"null\") 被拒绝", True)
|
|
101
|
+
check("盘面仍无 None.md/null.md", _none_md_hits(root) == [],
|
|
102
|
+
_none_md_hits(root))
|
|
103
|
+
|
|
104
|
+
print("[3] 防误杀:含 None/null 子串的合法 id 照常写入(只拒整串精确等值)")
|
|
105
|
+
ok1 = cg.add("NoneBot_1", "含 None 子串的合法 id 无条件")
|
|
106
|
+
ok2 = cg.add("nullify_test", "含 null 子串的合法 id 无条件")
|
|
107
|
+
check("add(\"NoneBot_1\") 正常", ok1 == "NoneBot_1")
|
|
108
|
+
check("add(\"nullify_test\") 正常", ok2 == "nullify_test")
|
|
109
|
+
|
|
110
|
+
print("[4] 既有语义不回退:None 本体 / falsy 仍结构化拒绝(P0-1)")
|
|
111
|
+
# 注:add(123) 是既有合法行为("123" 匹配白名单),不入守卫——非字符串
|
|
112
|
+
# 原值入索引快照属相邻缺陷(_scan_nodes/_load_index 混型键 sorted 崩),
|
|
113
|
+
# 超出本守卫目标,取证记录于修复报告。
|
|
114
|
+
for bad in (None, "", 0):
|
|
115
|
+
try:
|
|
116
|
+
cg.add(bad, "非法形态内容")
|
|
117
|
+
check("add(%r) 仍被拒绝" % (bad,), False, "竟返回成功")
|
|
118
|
+
except ValueError:
|
|
119
|
+
check("add(%r) 仍被拒绝(ValueError)" % (bad,), True)
|
|
120
|
+
|
|
121
|
+
print("[5] writepipe 链:node_id=\"None\" 不落盘(DEFER/BLINDSPOT 短路入队"
|
|
122
|
+
"或落盘前被 add 边界拒绝,两态皆 fail-closed)")
|
|
123
|
+
from md_cg import writepipe
|
|
124
|
+
pipe = writepipe.WritePipeline()
|
|
125
|
+
writepipe.install_default_gates(pipe)
|
|
126
|
+
committed = None
|
|
127
|
+
try:
|
|
128
|
+
out = pipe.execute(cg, {"content": "writepipe 污染形态", "node_id": "None",
|
|
129
|
+
"content_kind": "work_done",
|
|
130
|
+
"生效条件": "writepipe 守卫探针"})
|
|
131
|
+
committed = out.get("committed")
|
|
132
|
+
except ValueError:
|
|
133
|
+
committed = False # 落盘前被 add 边界拒绝(修复态)
|
|
134
|
+
check("writepipe 对 node_id='None' 不提交落盘", committed is not True,
|
|
135
|
+
repr(committed))
|
|
136
|
+
check("盘面无 None.md(writepipe 后复查)", _none_md_hits(root) == [],
|
|
137
|
+
_none_md_hits(root))
|
|
138
|
+
|
|
139
|
+
print("[6] 存量脏数据收录取证:盘上 None.md 被 _scan_nodes 收进索引(DSH 病理机理;"
|
|
140
|
+
"亦是清理建议的依据——删文件 + rebuild 即消失)")
|
|
141
|
+
d = os.path.join(root, "knowledge", "orphan")
|
|
142
|
+
os.makedirs(d, exist_ok=True)
|
|
143
|
+
with open(os.path.join(d, "None.md"), "w", encoding="utf-8") as f:
|
|
144
|
+
f.write("---\nid: None\nlayer: knowledge\nimportance: 0.5\n"
|
|
145
|
+
"confidence: 0.6\ncreated_at: 1\ntags: []\n---\n\n历史脏数据内容\n")
|
|
146
|
+
cg2 = MdCGOS(root) # 重开:走 _scan_nodes 全库扫描
|
|
147
|
+
cg2.rebuild_index()
|
|
148
|
+
check("历史 None.md 被扫描收录(机理复证)", "None" in cg2.index["nodes"])
|
|
149
|
+
res, _m = cg2.search_rrf("历史脏数据内容", k=10, judge=False)
|
|
150
|
+
check("检索候选命中 id='None'(病理复证)",
|
|
151
|
+
any(r[0]["id"] == "None" for r in res), [r[0]["id"] for r in res])
|
|
152
|
+
os.remove(os.path.join(d, "None.md")) # 清理后即消失(清理建议有效性)
|
|
153
|
+
cg3 = MdCGOS(root)
|
|
154
|
+
cg3.rebuild_index()
|
|
155
|
+
check("删文件 + rebuild 后 id='None' 从索引消失(清理有效)",
|
|
156
|
+
"None" not in cg3.index["nodes"])
|
|
157
|
+
|
|
158
|
+
check(f"断言总数不低于 FLOOR({passed} >= {FLOOR})", passed >= FLOOR)
|
|
159
|
+
|
|
160
|
+
print(f"\nnone_id_write_guard: {passed} 通过 / {failed} 失败")
|
|
161
|
+
return 0 if not failed else 1
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
sys.exit(main())
|