@furongjun1999/dsh-memory 0.7.1 → 0.7.2
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 +3 -2
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +1 -1
- package/docs/eval//345/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +4 -4
- package/md_cg/bench6_arms.py +10 -6
- package/md_cg/census.py +2 -1
- package/md_cg/mdcg.py +83 -25
- package/md_cg/mdcos.py +9 -3
- package/md_cg/mreview/locate.py +1 -1
- package/md_cg/rollback.py +12 -3
- package/md_cg/routing.py +10 -1
- package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
- package/md_cg/test_generation_guard.py +15 -9
- package/md_cg/test_health_corrupt_utf8.py +37 -0
- package/md_cg/test_index_crossprocess_reload.py +39 -24
- package/md_cg/test_issue53_cipher_health_hash.py +385 -0
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/zcode/AGENTS.md +5 -5
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""守卫:坏 UTF-8 节点在五个读取面的行为契约(PR #54 面 + 三处读取面同口径扩展)
|
|
3
|
+
|
|
4
|
+
背景(取证自 `git show --stat ccef3bc0 / 30e0580b` 与本文件注释逐字引用的现场)
|
|
5
|
+
--------------------------------------------------------------------------
|
|
6
|
+
`30e0580b`(健康检查)与 `ccef3bc0`(读取面)落了「损坏非 UTF-8 节点不应让读取
|
|
7
|
+
面崩溃」这条口径;工作区未提交改动把同一口径扩展到另三个站点(`git diff` 逐行
|
|
8
|
+
核对:`md_cg/census.py`、`md_cg/mreview/locate.py`、`md_cg/rollback.py`、
|
|
9
|
+
`md_cg/mdcos.py` 的 `restore`)。本守卫把**五个读取面的行为**一并钉死——不钉
|
|
10
|
+
「源码里有 except」(源文本在场不等于行为成立),全部断言经隔离临时库真跑:
|
|
11
|
+
|
|
12
|
+
① `census.load(root)`:坏 .md 不崩、跳过;良品照常入列。
|
|
13
|
+
② `MdCGOS.restore(node_id)`:坏 trash 文件 → `ok=False` / `error=="corrupt"`,
|
|
14
|
+
且 trash 源文件**原样保留**(不删不移、逐字节不变)、节点不回索引。
|
|
15
|
+
③ `rollback.read_preimage(cg, node_id, rel)`:坏前像 → 抛 `RollbackError`
|
|
16
|
+
(`code=="preimage_unreadable"`),**不是裸 UnicodeDecodeError**;
|
|
17
|
+
相邻分支(前像缺失)→ `RollbackError(code=="preimage_missing")`。
|
|
18
|
+
④ `mreview/locate`:坏节点不崩;「在索引但不可读」与「不在索引」**分野**
|
|
19
|
+
(前者 load 非 None、blindspot 空、正文长 0、按空盘面判定;后者
|
|
20
|
+
`blindspot==["节点 X 不在索引"]` + `load=None` + `hits==[]`);批量入口
|
|
21
|
+
`locate_many` 把坏节点计入 nodes(不是 missing)并给显式读数。
|
|
22
|
+
⑤ `health()["skipped_unreadable"] == 1`(1 坏 + 2 良品库;良品照常统计)。
|
|
23
|
+
|
|
24
|
+
定点变异自证(`--mutate`,与 `md_cg/test_i50e_readside_protection.py` 同口径)
|
|
25
|
+
--------------------------------------------------------------------------
|
|
26
|
+
每处受护站点配一个定点变异(把对应捕获/计数短路掉),变异**不落盘**——
|
|
27
|
+
`inspect.getsource` + 字面替换 + `exec` + `types.FunctionType` 绑回原 globals,
|
|
28
|
+
`setattr` 安装/还原(monkeypatch 行为面)。每轮跑全部五个检查面,逐变异打印
|
|
29
|
+
「变异 <n> 红项=<k>」;判定 = 红项数逐处吻合**且**红项全部落在对应面
|
|
30
|
+
(定点性:一处变异只打红它守的那一面)。表内 expect 列为实施期实测
|
|
31
|
+
(2026-10-04 本文件首跑);锚点漂移(实现改了却没同步本表)→ 报 ANCHOR-MISS
|
|
32
|
+
并 **exit 2**(fail-closed,默认模式同样先做锚点自检,不以 git HEAD 为基线源)。
|
|
33
|
+
|
|
34
|
+
安全边界:一切在 `tempfile.mkdtemp()` 临时库内进行——绝不写工作区、绝不碰
|
|
35
|
+
任何在役记忆库(不读不写 `MDCG_ROOT`/DSH 数据根)、不起常驻服务、不重启/终止
|
|
36
|
+
任何在跑进程;变异的源码替换只在进程内存里生效,磁盘上的实现文件一字不动。
|
|
37
|
+
|
|
38
|
+
运行(仓根直跑,退出码 0 = 全绿):
|
|
39
|
+
|
|
40
|
+
python -X utf8 -m md_cg.test_corrupt_utf8_read_surfaces
|
|
41
|
+
python -X utf8 -m md_cg.test_corrupt_utf8_read_surfaces --mutate
|
|
42
|
+
python -X utf8 -m md_cg.test_corrupt_utf8_read_surfaces --mutate --list
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import inspect
|
|
47
|
+
import os
|
|
48
|
+
import shutil
|
|
49
|
+
import sys
|
|
50
|
+
import tempfile
|
|
51
|
+
import textwrap
|
|
52
|
+
import types
|
|
53
|
+
|
|
54
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
55
|
+
if REPO not in sys.path: # 保证 `import md_cg.*` 能定位到当前树
|
|
56
|
+
sys.path.insert(0, REPO)
|
|
57
|
+
|
|
58
|
+
from md_cg import census, protect, rollback # noqa: E402
|
|
59
|
+
from md_cg.mdcg import MdCG # noqa: E402
|
|
60
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
61
|
+
from md_cg.mreview import locate as locate_mod # noqa: E402
|
|
62
|
+
|
|
63
|
+
#: 损坏字节:非法 UTF-8 起始字节(0xff 在任何位置都非法)。
|
|
64
|
+
BAD_BYTES = bytes((0xff, 0xfe, 0xfd))
|
|
65
|
+
#: 良品正文(CCG 六要素齐备;末尾带 \n 让盘上正文与写前逐字一致,
|
|
66
|
+
#: 消掉 `nodefile.dumps` 补行尾带来的 hash 归一噪声)。
|
|
67
|
+
GOOD_DOC = ("# 功能名:读面守卫良品节点\n"
|
|
68
|
+
"# 生效条件:任意情境\n"
|
|
69
|
+
"# 子功能:守卫夹具\n"
|
|
70
|
+
"# 执行:直接调用\n"
|
|
71
|
+
"# 验证方式:test\n"
|
|
72
|
+
"# 不适用条件:无\n"
|
|
73
|
+
"读面守卫良品正文。\n")
|
|
74
|
+
#: ④ 坏节点在 `locate_ex` 下的确定读数(实施期实测 2026-10-04,本文件首跑):
|
|
75
|
+
#: 读不到盘面 ⇒ 正文按空、fm 按空判定的五条命中(content_hash 矛盾 locator
|
|
76
|
+
#: 因 `content is None` 而**不**参与——与 locate_many 的读数差异见 ④ 末条)。
|
|
77
|
+
BAD_TRIPLES = [
|
|
78
|
+
("missing_field", "condition_space", "condition_slots"),
|
|
79
|
+
("missing_field", "content", "ccg_incomplete"),
|
|
80
|
+
("missing_field", "evidence_count", "field_absent"),
|
|
81
|
+
("missing_field", "role", "field_absent"),
|
|
82
|
+
("weak_source", "verification_basis", "basis_absent"),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
_PASS = []
|
|
86
|
+
_FAIL = []
|
|
87
|
+
_VERBOSE = True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _ok(cond, msg, extra=""):
|
|
91
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
92
|
+
if _VERBOSE:
|
|
93
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
94
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------- 夹具
|
|
98
|
+
def _synth(tmp, face, node_ids):
|
|
99
|
+
"""隔离临时库:建库 → add 全部节点(同形良品正文)→ flush。"""
|
|
100
|
+
root = os.path.join(tmp, face)
|
|
101
|
+
cg = MdCGOS(root, autoflush=0)
|
|
102
|
+
for nid in node_ids:
|
|
103
|
+
cg.add(nid, GOOD_DOC, tags=["domain:guard"])
|
|
104
|
+
cg.flush()
|
|
105
|
+
return cg, root
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _write_bad(path):
|
|
109
|
+
with open(path, "wb") as fh:
|
|
110
|
+
fh.write(BAD_BYTES)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _corrupt_node(cg, root, nid):
|
|
114
|
+
"""把某节点盘上文件替换成坏字节,返回其绝对路径。"""
|
|
115
|
+
rel = cg.index["nodes"][nid]["path"]
|
|
116
|
+
p = os.path.join(root, str(rel).replace("/", os.sep))
|
|
117
|
+
_write_bad(p)
|
|
118
|
+
return p
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------- ① census
|
|
122
|
+
def _check_census(tmp):
|
|
123
|
+
cg, root = _synth(tmp, "census", ("good1", "good2", "bad"))
|
|
124
|
+
try:
|
|
125
|
+
_corrupt_node(cg, root, "bad")
|
|
126
|
+
rows = None
|
|
127
|
+
raised = None
|
|
128
|
+
try:
|
|
129
|
+
rows = census.load(root)
|
|
130
|
+
except BaseException as exc: # noqa: BLE001
|
|
131
|
+
raised = exc
|
|
132
|
+
_ok(raised is None, "① census.load 遇坏 .md 不崩", repr(raised))
|
|
133
|
+
ids = sorted(r[0] for r in rows or [])
|
|
134
|
+
_ok(ids == ["good1", "good2"], "① 良品照常入列(恰 good1/good2)", ids)
|
|
135
|
+
_ok("bad" not in ids, "① 坏节点被跳过(不在产物中)")
|
|
136
|
+
finally:
|
|
137
|
+
cg.close()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------- ② restore
|
|
141
|
+
def _check_restore(tmp):
|
|
142
|
+
cg, root = _synth(tmp, "restore", ("good1", "bad", "victim"))
|
|
143
|
+
try:
|
|
144
|
+
_corrupt_node(cg, root, "bad")
|
|
145
|
+
_ok(cg.forget("victim").get("ok") is True, "② 夹具:victim 已软删(进 trash)")
|
|
146
|
+
tp = os.path.join(cg.trash_dir, "victim.md")
|
|
147
|
+
_ok(os.path.exists(tp), "② 夹具:trash 源文件在位")
|
|
148
|
+
_write_bad(tp)
|
|
149
|
+
before = open(tp, "rb").read()
|
|
150
|
+
out = None
|
|
151
|
+
raised = None
|
|
152
|
+
try:
|
|
153
|
+
out = cg.restore("victim", force=True)
|
|
154
|
+
except BaseException as exc: # noqa: BLE001
|
|
155
|
+
raised = exc
|
|
156
|
+
_ok(raised is None, "② 坏 trash 下 restore 不抛异常", repr(raised))
|
|
157
|
+
out = out if isinstance(out, dict) else {}
|
|
158
|
+
_ok(out.get("ok") is False, "② 返回 ok=False", out)
|
|
159
|
+
_ok(out.get("error") == "corrupt", "② error=='corrupt'", out)
|
|
160
|
+
after = open(tp, "rb").read() if os.path.exists(tp) else None
|
|
161
|
+
_ok(after == before,
|
|
162
|
+
"② trash 源文件原样保留(存在且逐字节不变)", after)
|
|
163
|
+
_ok("victim" not in cg.index["nodes"], "② 坏节点未回到索引")
|
|
164
|
+
finally:
|
|
165
|
+
cg.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------- ③ read_preimage
|
|
169
|
+
def _check_preimage(tmp):
|
|
170
|
+
cg, root = _synth(tmp, "preimage", ("good1", "bad"))
|
|
171
|
+
try:
|
|
172
|
+
_corrupt_node(cg, root, "bad")
|
|
173
|
+
rel = protect.snapshot_preimage(cg, "good1", action="C", pid="guard",
|
|
174
|
+
reason="守卫夹具")
|
|
175
|
+
_ok(bool(rel), "③ 夹具:前像已拍(真走 snapshot_preimage)")
|
|
176
|
+
pp = os.path.join(root, str(rel or "").replace("/", os.sep))
|
|
177
|
+
_write_bad(pp)
|
|
178
|
+
|
|
179
|
+
exc = None
|
|
180
|
+
try:
|
|
181
|
+
rollback.read_preimage(cg, "good1", rel)
|
|
182
|
+
except BaseException as e: # noqa: BLE001
|
|
183
|
+
exc = e
|
|
184
|
+
_ok(isinstance(exc, rollback.RollbackError),
|
|
185
|
+
"③ 坏前像抛 RollbackError", repr(exc))
|
|
186
|
+
_ok(not isinstance(exc, UnicodeDecodeError),
|
|
187
|
+
"③ 不是裸 UnicodeDecodeError")
|
|
188
|
+
_ok(getattr(exc, "code", None) == "preimage_unreadable",
|
|
189
|
+
"③ code=='preimage_unreadable'", getattr(exc, "code", None))
|
|
190
|
+
|
|
191
|
+
exc2 = None
|
|
192
|
+
try:
|
|
193
|
+
rollback.read_preimage(cg, "good1",
|
|
194
|
+
"_protected_history/good1/absent.md")
|
|
195
|
+
except BaseException as e: # noqa: BLE001
|
|
196
|
+
exc2 = e
|
|
197
|
+
_ok(isinstance(exc2, rollback.RollbackError)
|
|
198
|
+
and getattr(exc2, "code", None) == "preimage_missing",
|
|
199
|
+
"③ 相邻分支:前像缺失 → RollbackError(preimage_missing)", repr(exc2))
|
|
200
|
+
finally:
|
|
201
|
+
cg.close()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------- ④ locate
|
|
205
|
+
def _check_locate(tmp):
|
|
206
|
+
cg, root = _synth(tmp, "locate", ("good1", "good2", "bad"))
|
|
207
|
+
try:
|
|
208
|
+
_corrupt_node(cg, root, "bad")
|
|
209
|
+
ex = None
|
|
210
|
+
raised = None
|
|
211
|
+
try:
|
|
212
|
+
ex = locate_mod.locate_ex("bad", root=root, index=cg.index)
|
|
213
|
+
except BaseException as exc: # noqa: BLE001
|
|
214
|
+
raised = exc
|
|
215
|
+
_ok(raised is None, "④ locate_ex 对坏节点不崩", repr(raised))
|
|
216
|
+
ex = ex if isinstance(ex, dict) else {}
|
|
217
|
+
_ok(sorted(ex) == ["blindspot", "hits", "load"],
|
|
218
|
+
"④ 返回三键契约(hits/blindspot/load)", sorted(ex))
|
|
219
|
+
load = ex.get("load")
|
|
220
|
+
_ok(isinstance(load, dict) and load.get("content_len") == 0,
|
|
221
|
+
"④ 坏节点在索引:load 非 None 且正文长 0(≠ 不在索引)", load)
|
|
222
|
+
_ok(ex.get("blindspot") == [], "④ 不误报「不在索引」", ex.get("blindspot"))
|
|
223
|
+
triples = sorted((h.get("issue_kind"), h.get("field"), h.get("rule"))
|
|
224
|
+
for h in ex.get("hits") or [])
|
|
225
|
+
_ok(triples == BAD_TRIPLES,
|
|
226
|
+
"④ 按空盘面判定(实测命中集合,不静默放过)", triples)
|
|
227
|
+
_ok(all(h.get("status") == "located" for h in ex.get("hits") or []),
|
|
228
|
+
"④ 命中全部 status=='located'")
|
|
229
|
+
|
|
230
|
+
gh = None
|
|
231
|
+
raised = None
|
|
232
|
+
try:
|
|
233
|
+
gh = locate_mod.locate_ex("ghost", root=root, index=cg.index)
|
|
234
|
+
except BaseException as exc: # noqa: BLE001
|
|
235
|
+
raised = exc
|
|
236
|
+
_ok(raised is None and isinstance(gh, dict)
|
|
237
|
+
and gh.get("blindspot") == ["节点 ghost 不在索引"]
|
|
238
|
+
and gh.get("load") is None and gh.get("hits") == [],
|
|
239
|
+
"④ 相邻分支:不在索引 → blindspot 点名 / load None / hits 空",
|
|
240
|
+
(repr(raised), gh))
|
|
241
|
+
|
|
242
|
+
many = None
|
|
243
|
+
raised = None
|
|
244
|
+
try:
|
|
245
|
+
many = locate_mod.locate_many(node_ids=["bad", "good1"],
|
|
246
|
+
root=root, index=cg.index)
|
|
247
|
+
except BaseException as exc: # noqa: BLE001
|
|
248
|
+
raised = exc
|
|
249
|
+
_ok(raised is None, "④ locate_many 对坏节点不崩", repr(raised))
|
|
250
|
+
many = many if isinstance(many, dict) else {}
|
|
251
|
+
_ok(many.get("nodes") == 2 and many.get("missing") == [],
|
|
252
|
+
"④ locate_many:坏节点计入 nodes,不是 missing",
|
|
253
|
+
(many.get("nodes"), many.get("missing")))
|
|
254
|
+
_ok(any(h.get("issue_kind") == "contradiction"
|
|
255
|
+
and h.get("field") == "content_hash"
|
|
256
|
+
for h in many.get("hits") or []),
|
|
257
|
+
"④ locate_many:坏节点出显式读数(content_hash 矛盾)")
|
|
258
|
+
finally:
|
|
259
|
+
cg.close()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------- ⑤ health
|
|
263
|
+
def _check_health(tmp):
|
|
264
|
+
cg, root = _synth(tmp, "health", ("good1", "good2", "bad"))
|
|
265
|
+
try:
|
|
266
|
+
_corrupt_node(cg, root, "bad")
|
|
267
|
+
h = None
|
|
268
|
+
raised = None
|
|
269
|
+
try:
|
|
270
|
+
h = cg.health()
|
|
271
|
+
except BaseException as exc: # noqa: BLE001
|
|
272
|
+
raised = exc
|
|
273
|
+
_ok(raised is None, "⑤ health() 不崩", repr(raised))
|
|
274
|
+
h = h if isinstance(h, dict) else {}
|
|
275
|
+
_ok(h.get("skipped_unreadable") == 1,
|
|
276
|
+
"⑤ skipped_unreadable==1(1 坏)", h.get("skipped_unreadable"))
|
|
277
|
+
_ok(h.get("total_nodes") == 3, "⑤ total_nodes==3(索引三节点)",
|
|
278
|
+
h.get("total_nodes"))
|
|
279
|
+
_ok(sum(v.get("total", 0)
|
|
280
|
+
for v in (h.get("ccg_by_layer") or {}).values()) == 2,
|
|
281
|
+
"⑤ 良品照常统计(层总计 2)", h.get("ccg_by_layer"))
|
|
282
|
+
finally:
|
|
283
|
+
cg.close()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
_CHECKS = (("census", _check_census), ("restore", _check_restore),
|
|
287
|
+
("preimage", _check_preimage), ("locate", _check_locate),
|
|
288
|
+
("health", _check_health))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _run_all(tmp):
|
|
292
|
+
"""跑全部检查面,返回 ({面名: 失败数}, 通过总数)。
|
|
293
|
+
|
|
294
|
+
面内异常计 1 条失败并继续下一面(守卫不因被测面抛异常整体崩)。
|
|
295
|
+
"""
|
|
296
|
+
out = {}
|
|
297
|
+
passed = 0
|
|
298
|
+
for name, fn in _CHECKS:
|
|
299
|
+
_PASS.clear()
|
|
300
|
+
_FAIL.clear()
|
|
301
|
+
try:
|
|
302
|
+
fn(tmp)
|
|
303
|
+
except BaseException as exc: # noqa: BLE001
|
|
304
|
+
_FAIL.append("面 %s 未捕获异常 %r" % (name, exc))
|
|
305
|
+
out[name] = len(_FAIL)
|
|
306
|
+
passed += len(_PASS)
|
|
307
|
+
return out, passed
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ---------------------------------------------------------------- 定点变异自证
|
|
311
|
+
# 表内每项:holder/attr = monkeypatch 落点;old 必须**逐字**出现在目标函数
|
|
312
|
+
# 当前源码(dedent 后)里且**唯一**;new 为短路形态;face = 应转红的面;
|
|
313
|
+
# expect = 实施期实测的红项数(2026-10-04 本文件首跑,实测构成如下):
|
|
314
|
+
# m1 红 2 = ①不崩 + ①良品集合(①「坏节点不在产物中」在 load 崩掉、产物为空时
|
|
315
|
+
# 仍成立——空集不含 bad,属该断言的固有局限,读数如实记 2);
|
|
316
|
+
# m2 红 3 = ②不抛 + ok == False + error == "corrupt"(trash 字节/未回索引
|
|
317
|
+
# 两条在裸抛下仍成立,不红);
|
|
318
|
+
# m3 红 3 = ③RollbackError + ③非 UnicodeDecodeError + ③code
|
|
319
|
+
# (③相邻分支 preimage_missing 不受影响,不红);
|
|
320
|
+
# m4 红 8 = ④不崩 + 三键 + load 非 None + blindspot 空 + 命中集合 + locate_many
|
|
321
|
+
# 不崩 + nodes/missing + content_hash 读数(④status 与 ghost 邻支两条不红:
|
|
322
|
+
# 空 hits 使 "all()" 恒真、ghost 走「索引无此项」早返回不读盘);
|
|
323
|
+
# m5 红 1 = ⑤skipped_unreadable(total_nodes 与层统计不受计数短路影响)。
|
|
324
|
+
_MUTATIONS = (
|
|
325
|
+
# m1:census.load 去掉 UnicodeDecodeError 捕获(修复前形态)→ ① 红。
|
|
326
|
+
{"n": 1, "face": "census", "holder": census, "attr": "load",
|
|
327
|
+
"old": "except (OSError, UnicodeDecodeError):", "new": "except OSError:",
|
|
328
|
+
"desc": "census.load 丢 UnicodeDecodeError 捕获", "expect": 2},
|
|
329
|
+
# m2:restore 的 except 块改成裸抛(不再落结构化失败)→ ② 红。
|
|
330
|
+
{"n": 2, "face": "restore", "holder": MdCGOS, "attr": "restore",
|
|
331
|
+
"old": "# trash 源损坏:返回结构化失败,源文件原样保留(不删不移)",
|
|
332
|
+
"new": 'raise UnicodeDecodeError("utf-8", b"\\xff", 0, 1, "mut")',
|
|
333
|
+
"desc": "restore 丢 corrupt 结构化返回(改裸抛)", "expect": 3},
|
|
334
|
+
# m3:read_preimage 的捕获子句换成 OSError(UnicodeDecodeError 逃逸)→ ③ 红。
|
|
335
|
+
{"n": 3, "face": "preimage", "holder": rollback, "attr": "read_preimage",
|
|
336
|
+
"old": "except UnicodeDecodeError as exc:",
|
|
337
|
+
"new": "except OSError as exc:",
|
|
338
|
+
"desc": "read_preimage 丢坏字节捕获", "expect": 3},
|
|
339
|
+
# m4:load_node 丢 UnicodeDecodeError 捕获 → ④ 红(其余面不受累)。
|
|
340
|
+
{"n": 4, "face": "locate", "holder": locate_mod, "attr": "load_node",
|
|
341
|
+
"old": "except (OSError, UnicodeDecodeError):", "new": "except OSError:",
|
|
342
|
+
"desc": "locate.load_node 丢 UnicodeDecodeError 捕获", "expect": 8},
|
|
343
|
+
# m5:health 的跳过计数短路 → ⑤ 红。
|
|
344
|
+
{"n": 5, "face": "health", "holder": MdCG, "attr": "health",
|
|
345
|
+
"old": "skipped_unreadable += 1", "new": "pass",
|
|
346
|
+
"desc": "health 丢 skipped_unreadable 计数", "expect": 1},
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _target_src(m):
|
|
351
|
+
return textwrap.dedent(inspect.getsource(getattr(m["holder"], m["attr"])))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _anchor_check():
|
|
355
|
+
"""返回 ANCHOR-MISS 说明列表(空 = 全部在位且唯一)。"""
|
|
356
|
+
bad = []
|
|
357
|
+
for m in _MUTATIONS:
|
|
358
|
+
src = _target_src(m)
|
|
359
|
+
cnt = src.count(m["old"])
|
|
360
|
+
if cnt != 1:
|
|
361
|
+
bad.append("变异 %d [%s] 锚点 %r 在目标源码中出现 %d 次(需恰 1 次)"
|
|
362
|
+
% (m["n"], m["face"], m["old"], cnt))
|
|
363
|
+
return bad
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _mutate_func(func, old, new):
|
|
367
|
+
"""按字面替换重编译函数(源码不落盘)——globals 绑回原模块/类。"""
|
|
368
|
+
src = textwrap.dedent(inspect.getsource(func))
|
|
369
|
+
if src.count(old) != 1:
|
|
370
|
+
raise AssertionError("锚点不唯一:%r(%d 处)" % (old, src.count(old)))
|
|
371
|
+
ns = {}
|
|
372
|
+
exec(compile(src.replace(old, new, 1), "<mut:%s>" % func.__name__, "exec"),
|
|
373
|
+
ns)
|
|
374
|
+
maker = ns[func.__name__]
|
|
375
|
+
nf = types.FunctionType(maker.__code__, func.__globals__, func.__name__,
|
|
376
|
+
maker.__defaults__, maker.__closure__)
|
|
377
|
+
nf.__kwdefaults__ = maker.__kwdefaults__
|
|
378
|
+
nf.__qualname__ = func.__qualname__
|
|
379
|
+
return nf
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _mutate_mode(tmp):
|
|
383
|
+
global _VERBOSE
|
|
384
|
+
_VERBOSE = False
|
|
385
|
+
base, _passed = _run_all(tmp)
|
|
386
|
+
b = sum(base.values())
|
|
387
|
+
print(" 未变异基线:红项=%d%s"
|
|
388
|
+
% (b, "" if b == 0 else " ← 基线即红,变异核验无意义"))
|
|
389
|
+
bad = [] if b == 0 else ["未变异基线即失败"]
|
|
390
|
+
for m in _MUTATIONS:
|
|
391
|
+
live = getattr(m["holder"], m["attr"])
|
|
392
|
+
try:
|
|
393
|
+
setattr(m["holder"], m["attr"], _mutate_func(live, m["old"],
|
|
394
|
+
m["new"]))
|
|
395
|
+
res, _passed = _run_all(tmp)
|
|
396
|
+
finally:
|
|
397
|
+
setattr(m["holder"], m["attr"], live)
|
|
398
|
+
k = sum(res.values())
|
|
399
|
+
faces = [n for n in res if res[n]]
|
|
400
|
+
hit = (k == m["expect"] and faces == [m["face"]])
|
|
401
|
+
print(" 变异 %d [%s] %s → 红项=%d(面=%s)%s"
|
|
402
|
+
% (m["n"], m["face"], m["desc"], k, ",".join(faces) or "-",
|
|
403
|
+
"命中预期" if hit else "**不命中(预期 红项=%s 面=%s)**"
|
|
404
|
+
% (m["expect"], m["face"])))
|
|
405
|
+
if not hit:
|
|
406
|
+
bad.append("变异 %d" % m["n"])
|
|
407
|
+
print("\n定点变异自证:%s"
|
|
408
|
+
% ("PASS(五处受护站点各被对应面抓住,红项数逐处吻合、无跨面误伤)"
|
|
409
|
+
if not bad else "FAIL —— " + "、".join(bad)))
|
|
410
|
+
return 0 if not bad else 1
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ---------------------------------------------------------------- main
|
|
414
|
+
def main():
|
|
415
|
+
if "--list" in sys.argv:
|
|
416
|
+
print("变异表(%d 项):" % len(_MUTATIONS))
|
|
417
|
+
for m in _MUTATIONS:
|
|
418
|
+
print(" %d [%s] %s(expect 红项=%s)" % (m["n"], m["face"],
|
|
419
|
+
m["desc"], m["expect"]))
|
|
420
|
+
return 0
|
|
421
|
+
|
|
422
|
+
anchor_bad = _anchor_check()
|
|
423
|
+
if anchor_bad:
|
|
424
|
+
for b in anchor_bad:
|
|
425
|
+
print(" ANCHOR-MISS " + b)
|
|
426
|
+
print("锚点自检:FAIL(fail-closed,exit 2)——实现改了请同步本文件变异表")
|
|
427
|
+
return 2
|
|
428
|
+
print("坏 UTF-8 读取面守卫(五面)——隔离临时库,不碰工作区与在役库;"
|
|
429
|
+
"锚点自检 PASS")
|
|
430
|
+
|
|
431
|
+
tmp = tempfile.mkdtemp(prefix="corrupt_utf8_guard_")
|
|
432
|
+
print("临时根:%s" % tmp)
|
|
433
|
+
try:
|
|
434
|
+
if "--mutate" in sys.argv:
|
|
435
|
+
rc = _mutate_mode(tmp)
|
|
436
|
+
else:
|
|
437
|
+
res, passed = _run_all(tmp)
|
|
438
|
+
print("== 五面汇总 ==")
|
|
439
|
+
for name, fails in res.items():
|
|
440
|
+
print(" 面 %s:%s" % (name, "绿" if fails == 0
|
|
441
|
+
else "红(%d 项)" % fails))
|
|
442
|
+
red = sum(res.values())
|
|
443
|
+
print("\n读取面守卫:%d 通过,%d 失败" % (passed, red))
|
|
444
|
+
rc = 1 if red else 0
|
|
445
|
+
finally:
|
|
446
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
447
|
+
return rc
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
if __name__ == "__main__":
|
|
451
|
+
sys.exit(main())
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
模块顶层**——只在函数体内(顶层调用会与函数内延迟导入的避环设计冲突)。
|
|
20
20
|
五 键集对拍:对临时 root 上的 `cg(op=info)` 真调一次 MCP 服务,响应必须含
|
|
21
21
|
code_generation / disk_generation / stale_on_disk / restart_required / hint
|
|
22
|
-
|
|
22
|
+
五个新键,且**既有键集合**与读码钉住的 20 键(18 既有 + issue #53-B 扩键
|
|
23
|
+
bucket_scope + PR #54 扩键 skipped_unreadable)逐字一致。
|
|
23
24
|
|
|
24
25
|
安全边界:一切在 `tempfile.mkdtemp()` 临时目录内进行(md_cg 树是复制出来的副本),
|
|
25
26
|
绝不写工作区、绝不写任何在役记忆库、绝不起用真 root 的常驻服务、绝不重启/终止
|
|
@@ -39,13 +40,17 @@ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
39
40
|
if REPO not in sys.path: # 保证 `import md_cg.*` 能定位到当前树的包
|
|
40
41
|
sys.path.insert(0, REPO)
|
|
41
42
|
|
|
42
|
-
#
|
|
43
|
-
# 2026-09-30
|
|
43
|
+
# 钉住的 cg(op=info) 既有键集合(在临时 root 上实跑一次服务所读):
|
|
44
|
+
# 18 键为 2026-09-30 接线前读码名单;bucket_scope 为 issue #53-B(2026-10-03)
|
|
45
|
+
# 的**有意扩键**(分桶读数覆盖面自述,纯增量、与既有键无冲突,与代校验五键
|
|
46
|
+
# 同构的扩键流程);skipped_unreadable 为 PR #54(2026-10-04)的**有意扩键**
|
|
47
|
+
# (health() 跳过损坏不可读节点的显式计数,同为纯增量、与既有键无冲突的扩键
|
|
48
|
+
# 流程)——名单同步至此;其后任何未声明的漂移仍被 extra/lost 两向抓出。
|
|
44
49
|
PINNED_INFO_KEYS = frozenset({
|
|
45
|
-
"action_source", "audit_kinds", "buckets", "
|
|
46
|
-
"external_verifiers", "links", "neg_memory_counts", "ok",
|
|
47
|
-
"
|
|
48
|
-
"write_policy",
|
|
50
|
+
"action_source", "audit_kinds", "buckets", "bucket_scope", "ccg_by_layer",
|
|
51
|
+
"ccg_contract", "external_verifiers", "links", "neg_memory_counts", "ok",
|
|
52
|
+
"os", "reason", "security", "skipped_unreadable", "surface", "theory",
|
|
53
|
+
"tools", "total_nodes", "whoami", "write_policy",
|
|
49
54
|
})
|
|
50
55
|
|
|
51
56
|
NEW_INFO_KEYS = ("code_generation", "disk_generation", "stale_on_disk",
|
|
@@ -325,8 +330,9 @@ def check_info_keys(tmp):
|
|
|
325
330
|
if "重启常驻 MCP 进程" not in (info["hint"] or ""):
|
|
326
331
|
_fail("五·键集", "hint 未含处置指引:%r" % (info["hint"],))
|
|
327
332
|
return
|
|
328
|
-
_ok("五·键集", "五新键齐备、既有 18
|
|
329
|
-
"
|
|
333
|
+
_ok("五·键集", "五新键齐备、既有 20 键(18 + #53-B 扩键 bucket_scope + "
|
|
334
|
+
"PR #54 扩键 skipped_unreadable)一字未动;"
|
|
335
|
+
"代 %s(code==disk,restart_required=False)"
|
|
330
336
|
% info["code_generation"])
|
|
331
337
|
|
|
332
338
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""health() skips a damaged UTF-8 node instead of crashing the health endpoint."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
from md_cg.mdcos import MdCGOS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
root = tempfile.mkdtemp(prefix="health_utf8_guard_")
|
|
14
|
+
cg = MdCGOS(root, autoflush=0)
|
|
15
|
+
try:
|
|
16
|
+
cg.add("good", "可读的健康检查节点", tags=["domain:health"])
|
|
17
|
+
cg.add("bad", "待损坏节点", tags=["domain:health"])
|
|
18
|
+
cg.flush()
|
|
19
|
+
bad_path = os.path.join(root, cg.index["nodes"]["bad"]["path"].replace("/", os.sep))
|
|
20
|
+
with open(bad_path, "wb") as fh:
|
|
21
|
+
fh.write(bytes((0xff, 0xfe, 0xfd)))
|
|
22
|
+
|
|
23
|
+
assert cg.get("bad") is None
|
|
24
|
+
results, _meta = cg.search("可读", k=5, include_neg=True)
|
|
25
|
+
assert any(item[0]["id"] == "good" for item in results), results
|
|
26
|
+
health = cg.health()
|
|
27
|
+
assert health["total_nodes"] == 2, health
|
|
28
|
+
assert sum(v["total"] for v in health["ccg_by_layer"].values()) == 1, health
|
|
29
|
+
print("PASS health skips invalid UTF-8 node")
|
|
30
|
+
finally:
|
|
31
|
+
cg.close()
|
|
32
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
main()
|
|
37
|
+
|
|
@@ -262,30 +262,45 @@ def main():
|
|
|
262
262
|
a4.close()
|
|
263
263
|
|
|
264
264
|
# ---------- ⑥ 性能粗测:无跨进程写入时逐读开销 ----------
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
265
|
+
# 并行豁免 + 3 轮中位数(2026-10-03 外部报告核验新增):
|
|
266
|
+
# · 并行全量下 CPU 争抢使本断言假红(报告机器实测 on 242.0µs / off 35.6µs,
|
|
267
|
+
# 单独跑 3 次 41~42µs 全 PASS——接线态是 syscall 密集面,争抢长尾远重于
|
|
268
|
+
# 空转桩态)⇒ 并行时**明示豁免**(run_tests 经 MDCG_RUN_TESTS_PARALLEL
|
|
269
|
+
# 传信号;串行(--jobs 1,含 CI)照测——语义断言不受本豁免影响);
|
|
270
|
+
# · 串行下采样改 3 轮取中位(抗偶发长尾),阈值口径不变(仍防真回归)。
|
|
271
|
+
_parallel = os.environ.get("MDCG_RUN_TESTS_PARALLEL") == "1"
|
|
272
|
+
if _parallel:
|
|
273
|
+
print("\n【⑥】性能粗测(无跨进程写入,接线态 vs 重载桩空转态)")
|
|
274
|
+
print(" [SKIP] 性能上界断言(并行豁免):CPU 争抢下本断言假红(外部报告"
|
|
275
|
+
"实测 on 242.0µs vs off 35.6µs;单跑 41~42µs)——串行(--jobs 1,含 CI)照测")
|
|
276
|
+
else:
|
|
277
|
+
print("\n【⑥】性能粗测(无跨进程写入,接线态 vs 重载桩空转态;3 轮取中位)")
|
|
278
|
+
N = 300
|
|
279
|
+
a2c = MdCGSecure(root3, principal=_principal())
|
|
280
|
+
|
|
281
|
+
def _probe():
|
|
282
|
+
for _ in range(5):
|
|
283
|
+
a2c.get("own_0")
|
|
284
|
+
t0 = time.perf_counter()
|
|
285
|
+
for _ in range(N):
|
|
286
|
+
a2c.get("own_0")
|
|
287
|
+
return (time.perf_counter() - t0) / N * 1e6
|
|
288
|
+
|
|
289
|
+
on_runs = [_probe() for _ in range(3)]
|
|
290
|
+
orig = a2c._maybe_reload_index
|
|
291
|
+
a2c._maybe_reload_index = lambda: False
|
|
292
|
+
off_runs = [_probe() for _ in range(3)]
|
|
293
|
+
a2c._maybe_reload_index = orig
|
|
294
|
+
a2c.close()
|
|
295
|
+
t_on = sorted(on_runs)[1]
|
|
296
|
+
t_off = sorted(off_runs)[1]
|
|
297
|
+
print(" 接线态 %.1f µs/读(3 轮 %s)· 桩空转态 %.1f µs/读(3 轮 %s)· 增幅 %.1f%%"
|
|
298
|
+
% (t_on, "/".join("%.1f" % v for v in on_runs),
|
|
299
|
+
t_off, "/".join("%.1f" % v for v in off_runs),
|
|
300
|
+
(t_on - t_off) / max(t_off, 1e-9) * 100))
|
|
301
|
+
check("性能上界:接线态(3 轮中位)≤ 桩空转态(3 轮中位)×3 + 30µs(中位抗长尾)",
|
|
302
|
+
t_on <= t_off * 3 + 30.0,
|
|
303
|
+
"on=%.1f off=%.1f" % (t_on, t_off))
|
|
289
304
|
finally:
|
|
290
305
|
for r_ in roots:
|
|
291
306
|
shutil.rmtree(r_, ignore_errors=True)
|