@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.
@@ -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
- 五个新键,且**既有键集合**与改动前读码钉住的 18 键逐字一致。
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
- # 改动前读码钉住的 cg(op=info) 既有键集合(在临时 root 上实跑一次服务所读,
43
- # 2026-09-30 接线前):本守卫要证明这 18 键一字未动。
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", "ccg_by_layer", "ccg_contract",
46
- "external_verifiers", "links", "neg_memory_counts", "ok", "os", "reason",
47
- "security", "surface", "theory", "tools", "total_nodes", "whoami",
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 键一字未动;代 %s(code==disk,"
329
- "restart_required=False)"
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
- print("\n【⑥】性能粗测(无跨进程写入,接线态 vs 重载桩空转态)")
266
- N = 300
267
- a2c = MdCGSecure(root3, principal=_principal())
268
- for _ in range(5):
269
- a2c.get("own_0")
270
- t0 = time.perf_counter()
271
- for _ in range(N):
272
- a2c.get("own_0")
273
- t_on = (time.perf_counter() - t0) / N * 1e6
274
- orig = a2c._maybe_reload_index
275
- a2c._maybe_reload_index = lambda: False
276
- for _ in range(5):
277
- a2c.get("own_0")
278
- t0 = time.perf_counter()
279
- for _ in range(N):
280
- a2c.get("own_0")
281
- t_off = (time.perf_counter() - t0) / N * 1e6
282
- a2c._maybe_reload_index = orig
283
- a2c.close()
284
- print(" 接线态 %.1f µs/读 · 桩空转态 %.1f µs/读 · 增幅 %.1f%%"
285
- % (t_on, t_off, (t_on - t_off) / max(t_off, 1e-9) * 100))
286
- check("性能上界:接线态 ≤ 桩空转态×3 + 30µs(stat 微秒级;宽松防抖)",
287
- t_on <= t_off * 3 + 30.0,
288
- "on=%.1f off=%.1f" % (t_on, t_off))
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)