@furongjun1999/dsh-memory 0.7.1 → 0.7.3

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.
Files changed (125) hide show
  1. package/README.md +58 -48
  2. package/codebuddy/CODEBUDDY.md +9 -9
  3. package/docs/README.md +148 -144
  4. package/docs/discipline/harnesses.yaml +16 -8
  5. package/docs/discipline/templates/zcode-user.md.tmpl +49 -0
  6. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_/345/205/255/345/256/266/350/256/260/345/277/206/347/263/273/347/273/237/345/220/214/345/260/272/345/256/236/346/265/213_v3.1.md +340 -0
  7. package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +2 -2
  8. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  9. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +1 -1
  10. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +1 -1
  11. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +2 -2
  12. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +5 -5
  13. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +9 -9
  14. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +4 -4
  15. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +1 -1
  16. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +1 -1
  17. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +3 -3
  18. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +3 -3
  19. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +7 -7
  20. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +2 -2
  21. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +1 -1
  22. package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +7 -7
  23. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +10 -10
  24. package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +4 -0
  25. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +1 -1
  26. 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
  27. 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
  28. package/docs/eval//345/217/221/345/270/20311_/345/244/226/346/212/245/345/205/255/350/277/236/344/277/256/344/270/216/344/270/226/347/225/214/346/250/241/345/236/213/345/272/225/345/272/247_v1.0.md +91 -0
  29. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +2 -2
  30. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/270/200/346/211/271_v1.0.md +195 -0
  31. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/272/214/346/211/271B6_/346/234/254/346/234/272/350/267/257/345/276/204/347/233/270/345/257/271/345/214/226_v1.0.md +78 -0
  32. 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 +1 -1
  33. 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 +3 -3
  34. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
  35. package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +1 -1
  36. 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_v26.md +196 -0
  37. 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_v27.md +259 -0
  38. 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_v28.md +217 -0
  39. 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_v29.md +270 -0
  40. 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_v30.md +231 -0
  41. 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_v31.md +281 -0
  42. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +1 -1
  43. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
  44. 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 +1 -1
  45. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +17 -17
  46. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +1 -1
  47. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
  48. 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 +42 -41
  49. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
  50. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +71 -1
  51. package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +1 -1
  52. package/docs/plans//345/244/232/344/270/273/344/275/223/344/270/226/347/225/214/346/250/241/345/236/213_/345/257/271/351/275/220/350/257/204/344/274/260/344/270/216/350/220/275/345/234/260/350/256/276/350/256/241_v0.1.md +141 -0
  53. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +2 -2
  54. package/docs/plans//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/350/256/276/350/256/241_v0.1.md +107 -0
  55. 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 +9 -8
  56. package/docs//347/247/244_/350/256/260/345/277/206/347/263/273/347/273/237/350/257/204/346/265/213/350/247/204/350/214/203_v2.1.md +225 -0
  57. package/dsh/cordis-patch-profile-web.example.yml +2 -2
  58. package/lib/bridge.js +9 -0
  59. package/lib/hooks.d.ts +8 -1
  60. package/lib/hooks.js +94 -32
  61. package/lib/lib/hook_audit.d.ts +55 -0
  62. package/lib/lib/hook_audit.js +208 -0
  63. package/lib/lib/mutual.js +5 -1
  64. package/lib/lib/roleplay_web.js +17 -2
  65. package/md_cg/auditview.py +248 -0
  66. package/md_cg/bench6_arms.py +10 -6
  67. package/md_cg/bench_e2e_judge.py +4 -2
  68. package/md_cg/branches.py +29 -3
  69. package/md_cg/census.py +2 -1
  70. package/md_cg/consistency.py +38 -13
  71. package/md_cg/datapath.py +38 -11
  72. package/md_cg/forgetting.py +15 -2
  73. package/md_cg/freshness.py +32 -11
  74. package/md_cg/ghostref.py +130 -0
  75. package/md_cg/judgment_manifest.py +7 -0
  76. package/md_cg/mcp_server.py +58 -1
  77. package/md_cg/mdcg.py +4923 -4764
  78. package/md_cg/mdcos.py +40 -15
  79. package/md_cg/mreview/locate.py +1 -1
  80. package/md_cg/nodefile.py +77 -13
  81. package/md_cg/readcache.py +29 -4
  82. package/md_cg/rollback.py +12 -3
  83. package/md_cg/routing.py +10 -1
  84. package/md_cg/selfreport.py +12 -3
  85. package/md_cg/sleep.py +129 -13
  86. package/md_cg/sources.py +1 -1
  87. package/md_cg/state_events.py +131 -0
  88. package/md_cg/sustain.py +1436 -1436
  89. package/md_cg/test_auditview.py +277 -0
  90. package/md_cg/test_autonomy_modes.py +2 -1
  91. package/md_cg/test_boot_openblas_guard.py +197 -0
  92. package/md_cg/test_ccg_form_parity.py +95 -4
  93. package/md_cg/test_cons200_scan_selection.py +4 -2
  94. package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
  95. package/md_cg/test_datapath_device_name.py +62 -3
  96. package/md_cg/test_generation_guard.py +20 -11
  97. package/md_cg/test_ghostref.py +182 -0
  98. package/md_cg/test_health_corrupt_utf8.py +37 -0
  99. package/md_cg/test_index_crossprocess_reload.py +39 -24
  100. package/md_cg/test_interop.py +1 -1
  101. package/md_cg/test_issue52_scan_condition_first.py +1 -1
  102. package/md_cg/test_issue53_cipher_health_hash.py +385 -0
  103. package/md_cg/test_lock.py +16 -7
  104. package/md_cg/test_m3_h9_semantic_guard.py +1 -1
  105. package/md_cg/test_n230_dirty_replay.py +375 -375
  106. package/md_cg/test_n238_body_text_parity.py +156 -0
  107. package/md_cg/test_n276_probe_hoist.py +199 -0
  108. package/md_cg/test_p4_freshness.py +95 -1
  109. package/md_cg/test_policy_required_ccg.py +3 -1
  110. package/md_cg/test_readcache_prodpath.py +66 -0
  111. package/md_cg/test_sleep.py +58 -1
  112. package/md_cg/test_sleep_p1.py +218 -2
  113. package/md_cg/test_state_events.py +178 -0
  114. package/md_cg/test_time_core_lint.py +8 -1
  115. package/md_cg/test_writepipe.py +8 -5
  116. package/md_cg/tokens.py +11 -2
  117. package/md_cg/writepipe.py +76 -0
  118. package/package.json +2 -2
  119. package/skills/plugin.json +1 -1
  120. package/src/bridge.ts +9 -0
  121. package/src/hooks.ts +576 -517
  122. package/src/lib/hook_audit.ts +243 -0
  123. package/src/lib/mutual.ts +5 -1
  124. package/src/lib/roleplay_web.ts +17 -2
  125. package/zcode/AGENTS.md +9 -9
@@ -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,6 +19,14 @@ paths.json 直值面)改走该辅助。非 Windows 平台 ``\\\\.\\aux`` 是
19
19
  承接面:``mcp_server.main()`` 启动早期对 root/aux 各探一次,ValueError →
20
20
  stderr 告警 + return 2(对照 `_md_cg_` 前缀守卫先例「启动即拒绝,不带病运行」)。
21
21
 
22
+ issue #57 加固(判据与 OS 行为解耦):较新 Windows 构建的 GetFullPathNameW 不再把
23
+ 「<目录>\\aux」吞成 ``\\.\aux``,前缀判据随之失效(报告人红单:aux/con/com1/lpt1
24
+ 漏判、nul 未漏,本机旧构建仍吞、复现不了该红)——``md_cg/datapath.py`` 新增
25
+ ``_RESERVED_DEVICE_NAMES`` 与纯函数 ``_reserved_device_tail``(末段直判),
26
+ ``_abs_host_path`` 条件改为「设备前缀判据 或 末段直判」两因并置。§7 谓词表直调
27
+ 纯函数(平台无关闭集);§8 把模块所用 ``os.path.abspath`` 临时换成恒等函数、模拟
28
+ 报告人系统复现漏判现场——删掉「或末段直判」后 §8 在 Windows 上必转红(变异自证锚点)。
29
+
22
30
  隔离纪律:四个覆盖键(MDCG_STATE_ROOT/MDCG_DATA_ROOT/MDCG_ROOT/MDCG_AUX_ROOT)
23
31
  与 DSH_HOME 以 os.environ 副本注入、finally 逐键还原;探针路径全部是
24
32
  tempfile.mkdtemp 下**不存在**的纯字符串(或正常临时子目录),不创建任何含
@@ -40,10 +48,12 @@ except AttributeError:
40
48
  pass
41
49
 
42
50
  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)
51
+ ENV_STATE_ROOT, _reserved_device_tail, aux_root,
52
+ data_root, mdcg_root, state_root)
45
53
 
46
- FLOOR = 8 # 断言总数下限(防假绿)
54
+ # 断言总数下限(防假绿):含 [7] 谓词表 35 条 + [8] 现场模拟 2 条,
55
+ # 按新增断言数上调后仍留 ≥8 条余量。
56
+ FLOOR = 50
47
57
  PASS, FAIL = 0, 0
48
58
 
49
59
  #: 守卫注入/还原面(含 DSH_HOME——state_root 的第二档,一并与真实环境隔离)
@@ -182,8 +192,57 @@ def main():
182
192
  "_probe_root" in mwin and "_probe_aux" in mwin
183
193
  and "except ValueError" in mwin and "return 2" in mwin,
184
194
  "main() 首段缺承接受理")
195
+ check("5g _abs_host_path 源窗口含 _reserved_device_tail( 调用"
196
+ "(末段直判与设备前缀判据并置,issue #57)",
197
+ "_reserved_device_tail(" in _func_window(src, "_abs_host_path"))
198
+
199
+ # ---- 7. 末段直判谓词表(issue #57):_reserved_device_tail 纯函数窗口 ----
200
+ # 谓词表取自实测闭集:会被吞的形态必须全判真(含变形 aux.txt/aux.tar.gz/
201
+ # "aux."/"aux "(尾空格)/aux..b 与绝对路径末段/带尾分隔符形态);不吞的
202
+ # 形态必须全判假——com0/lpt0/com10/lpt10 数字越界、auxx/auxiliary 非设备名、
203
+ # ".aux" 名字以点开头不拆扩展名。纯函数不触盘、无平台分支,Windows/POSIX 同判。
204
+ print("[7] 末段直判谓词表(_reserved_device_tail,纯字符串·平台无关)")
205
+ for case in ("aux", "AUX", "Aux", "aux.txt", "aux.tar.gz", "aux.",
206
+ "aux ", "aux..b", "con", "prn", "nul", "com1", "com9",
207
+ "lpt1", "lpt9", "com\u00b9", "com\u00b2", "com\u00b3",
208
+ "lpt\u00b9", "lpt\u00b2", "lpt\u00b3", "conin$", "conout$",
209
+ "D:\\probe\\aux", "D:\\probe\\aux\\", "D:\\probe\\aux.txt"):
210
+ check(f"7·真·{case!r}: 判为保留设备名末段(须拦)",
211
+ _reserved_device_tail(case), "判为 False(漏拦)")
212
+ for case in ("com0", "lpt0", "com10", "lpt10", "auxx", ".aux",
213
+ "auxiliary", "", "D:\\probe\\com0"):
214
+ check(f"7·假·{case!r}: 不判保留设备名末段(须放行)",
215
+ not _reserved_device_tail(case), "判为 True(过拦)")
216
+
217
+ # ---- 8. issue #57 现场模拟:abspath 不改写绝对路径(新 Windows 构建)----
218
+ # 报告人系统(较新 Windows 构建)的 GetFullPathNameW 不再把「<目录>\aux」
219
+ # 吞成 \\.\aux——既有前缀判据(r.startswith("\\\\.\\"))随之失效(本机旧
220
+ # 构建仍吞、复现不了该红,故以恒等替换模拟)。本段把 datapath 模块所用的
221
+ # os.path.abspath 临时换成恒等函数(保存/还原,finally),模拟「abspath
222
+ # 原样返回绝对路径」的报告人行为,再经 _probe_reject 探 aux_root。
223
+ # 变异自证锚点:删掉 _abs_host_path 条件里的 `or _reserved_device_tail(r)`
224
+ # 后,本段在 Windows 上必须转红(r 不以 \\.\ 开头 → 不抛 → 8a/8b 失败),
225
+ # 而 [7] 谓词表保持全绿(证明末段判定是承重件而非装饰)。
226
+ print("[8] issue #57 现场模拟:abspath 恒等(新构建不吞设备名)")
227
+ _abspath_orig = os.path.abspath
228
+ try:
229
+ os.path.abspath = lambda p: p
230
+ raised, msg = _probe_reject(aux_root, ENV_AUX_ROOT, "aux", tmp)
231
+ finally:
232
+ os.path.abspath = _abspath_orig
233
+ if os.name == "nt":
234
+ check("8a abspath 不改写时 aux_root() 仍抛 ValueError(末段直判承重)",
235
+ raised, f"msg={msg[:160]!r}")
236
+ check("8b 该路径下消息仍含覆盖键名 MDCG_AUX_ROOT",
237
+ ENV_AUX_ROOT in msg, f"msg={msg[:160]!r}")
238
+ else:
239
+ check("8a 非 Windows 不判定:abspath 恒等后仍不抛(平台闸未越界)",
240
+ not raised, f"msg={msg[:160]!r}")
241
+ check("8b 非 Windows 不判定:不产生拒因(消息为空)",
242
+ msg == "", f"msg={msg[:160]!r}")
185
243
 
186
244
  # ---- 6. 守卫自检 floor ----
245
+ # [6] 后置于 [7][8] 之后:FLOOR 随新增断言数上调,须统计全部断言。
187
246
  print("[6] 守卫自检")
188
247
  check(f"6a 断言总数 ≥ {FLOOR}(防探针面失效假绿)",
189
248
  PASS + FAIL >= FLOOR, f"total={PASS + FAIL}")
@@ -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",
@@ -130,7 +135,7 @@ _STALE_SNIPPET = (
130
135
  "import io, json, os\n"
131
136
  "from md_cg import generation as g\n"
132
137
  "target = os.path.join(os.path.dirname(g.__file__), 'fsutil.py')\n"
133
- "raw = io.open(target, encoding='utf-8').read()\n"
138
+ "raw = io.open(target, encoding='utf-8', newline='').read()\n"
134
139
  "clean = g.is_stale()\n"
135
140
  "io.open(target, 'w', encoding='utf-8', newline='').write(raw + '\\n# 守卫临时改动\\n')\n"
136
141
  "modified = g.is_stale()\n"
@@ -172,8 +177,11 @@ def check_verify_and_mutation(tmp):
172
177
  _ok("一·全绿", "verify_delegations() 通过,且副本与当前树同代 %s" % real_fp)
173
178
 
174
179
  # 二:定点变异——改名 mdcg.py 里的 mint_auto_id 定义
180
+ # 读写都 newline=""(字节忠实):恢复必须逐字节复原——默认读会把 CRLF
181
+ # 压成 LF、再以 newline="" 写回即静默改变文件(2026-10-05 复核定因:
182
+ # 该非对称曾使「三·判别力」在 CRLF 工作区下必红)。
175
183
  mdcg_path = os.path.join(tmp, "md_cg", "mdcg.py")
176
- with open(mdcg_path, encoding="utf-8") as fh:
184
+ with open(mdcg_path, encoding="utf-8", newline="") as fh:
177
185
  original = fh.read()
178
186
  mutated = original.replace("def mint_auto_id(", "def mint_auto_id_renamed(", 1)
179
187
  if mutated == original:
@@ -325,8 +333,9 @@ def check_info_keys(tmp):
325
333
  if "重启常驻 MCP 进程" not in (info["hint"] or ""):
326
334
  _fail("五·键集", "hint 未含处置指引:%r" % (info["hint"],))
327
335
  return
328
- _ok("五·键集", "五新键齐备、既有 18 键一字未动;代 %s(code==disk,"
329
- "restart_required=False)"
336
+ _ok("五·键集", "五新键齐备、既有 20 键(18 + #53-B 扩键 bucket_scope + "
337
+ "PR #54 扩键 skipped_unreadable)一字未动;"
338
+ "代 %s(code==disk,restart_required=False)"
330
339
  % info["code_generation"])
331
340
 
332
341