@furongjun1999/dsh-memory 0.6.1 → 0.7.1

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 (105) hide show
  1. package/README.md +49 -18
  2. package/docs/README.md +1 -0
  3. 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 +223 -0
  4. 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 +340 -0
  5. 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 +437 -0
  6. 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 +488 -0
  7. 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 +428 -0
  8. package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
  9. 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 +306 -0
  10. 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 +58 -0
  11. 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 +360 -0
  12. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
  13. 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 +439 -0
  14. 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 +1337 -0
  15. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
  16. 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 +460 -0
  17. 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 +97 -0
  18. 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 +66 -0
  19. 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 +458 -0
  20. 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 +250 -0
  21. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
  22. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
  23. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  24. 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
  25. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  26. 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 +183 -0
  27. package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
  28. 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 +547 -0
  29. package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
  30. package/md_cg/admission.py +718 -0
  31. package/md_cg/autonomy_modes.py +642 -0
  32. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  33. package/md_cg/chain.py +47 -0
  34. package/md_cg/consistency.py +133 -8
  35. package/md_cg/forgetting.py +38 -6
  36. package/md_cg/freshness.py +527 -0
  37. package/md_cg/generation.py +409 -0
  38. package/md_cg/hotcache.py +4 -1
  39. package/md_cg/lifecycle.py +30 -2
  40. package/md_cg/mcp_server.py +191 -26
  41. package/md_cg/mdcg.py +594 -39
  42. package/md_cg/mdcos.py +869 -38
  43. package/md_cg/nodefile.py +74 -1
  44. package/md_cg/protect.py +79 -4
  45. package/md_cg/provenance.py +1 -1
  46. package/md_cg/review_cli.py +31 -2
  47. package/md_cg/rollback.py +411 -0
  48. package/md_cg/rollback_cli.py +104 -0
  49. package/md_cg/semantic/canonical.py +22 -0
  50. package/md_cg/semantic/unify.py +73 -22
  51. package/md_cg/semantic/unify_fixture.json +25 -0
  52. package/md_cg/sleep.py +1297 -0
  53. package/md_cg/stg.py +400 -50
  54. package/md_cg/stgidx.py +281 -0
  55. package/md_cg/sustain.py +146 -9
  56. package/md_cg/test_auto_defaults.py +424 -0
  57. package/md_cg/test_autonomy_admission.py +1098 -0
  58. package/md_cg/test_autonomy_modes.py +1972 -0
  59. package/md_cg/test_b1_auto_id_multiproc.py +7 -0
  60. package/md_cg/test_b1b2_write_face.py +7 -0
  61. package/md_cg/test_b3_merge_keeps_content.py +7 -0
  62. package/md_cg/test_boundary_hit.py +410 -0
  63. package/md_cg/test_cons200_scan_selection.py +791 -0
  64. package/md_cg/test_en_pipeline.py +22 -13
  65. package/md_cg/test_generation_guard.py +352 -0
  66. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  67. package/md_cg/test_i50a_half_dup_defer.py +408 -0
  68. package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
  69. package/md_cg/test_i50c_meta_passthrough.py +608 -0
  70. package/md_cg/test_i50d_importance_source.py +531 -0
  71. package/md_cg/test_i50e_readside_protection.py +656 -0
  72. package/md_cg/test_issue39_utf8_stdio.py +9 -1
  73. package/md_cg/test_issue52_scan_condition_first.py +677 -0
  74. package/md_cg/test_linkref.py +7 -0
  75. package/md_cg/test_mode_parity.py +1393 -0
  76. package/md_cg/test_mutation_rollback.py +805 -0
  77. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
  78. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  79. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
  80. package/md_cg/test_n230_dirty_replay.py +375 -0
  81. package/md_cg/test_p2_mcp.py +8 -0
  82. package/md_cg/test_p2_six_elements.py +463 -0
  83. package/md_cg/test_p3_legacy_closure.py +433 -0
  84. package/md_cg/test_p4_freshness.py +680 -0
  85. package/md_cg/test_p8_subgraph_chain.py +14 -1
  86. package/md_cg/test_p9_forget_protect.py +7 -0
  87. package/md_cg/test_p9c_dedup_hints.py +7 -0
  88. package/md_cg/test_policy_required_ccg.py +5 -1
  89. package/md_cg/test_protocol.py +7 -0
  90. package/md_cg/test_rank_parity_score_mode.py +6 -0
  91. package/md_cg/test_semantic_canonical.py +5 -3
  92. package/md_cg/test_sleep.py +611 -0
  93. package/md_cg/test_sleep_p1.py +784 -0
  94. package/md_cg/test_stgidx_index_parity.py +1020 -0
  95. package/md_cg/test_time_core_lint.py +968 -0
  96. package/md_cg/test_unify_default_off.py +701 -0
  97. package/md_cg/test_unify_scope.py +182 -0
  98. package/md_cg/test_writelimit.py +36 -9
  99. package/md_cg/test_writepipe.py +5 -2
  100. package/md_cg/weights.py +15 -1
  101. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  102. package/md_cg/writepipe.py +101 -2
  103. package/package.json +3 -2
  104. package/skills/plugin.json +1 -1
  105. package/utf8_boot.py +237 -0
@@ -13,13 +13,14 @@
13
13
  MDCG_EN_ATOMS=1 显式开启;纯中文 query 零触发;
14
14
  mdcos 四路 RRF 的 lexical 路同源复用 expand_query_terms,自动受益。
15
15
 
16
- ⚠ 与**统一归一层**的关系(2026-09-24 澄清,曾造成本件红灯):
17
- `semantic/unify.py::unify_query` 是**另一个、且默认开启**的开关
18
- (MDCG_UNIFY_QUERY,默认 "1",2026-09-23 口径转正):检索入口把任意语言的
19
- query 先归一成标准中文原子序列——英文 query 因此在**词法路**上就能命中中文
20
- 节点(与 en_zh_terms 默认关并不矛盾:一个在 query 侧归一,一个在召回词扩展侧)。
21
- 故本件的「跨语词面零交集 ⇒ 召回必须归零」只在**两个开关都关**时成立;
22
- 默认态断言相应改为「命中」,两个态都钉住(改口径必红)。
16
+ ⚠ 与**统一归一层**的关系(2026-09-24 澄清,曾造成本件红灯;2026-09-30 缺省翻关):
17
+ `semantic/unify.py::unify_query` 是**另一个**开关,缺省**关**(MDCG_UNIFY_QUERY,
18
+ 2026-09-30 使用者裁定由「默认 "1"」翻为「未设即关、显式 "1" 才开」):显式开启时
19
+ 检索入口把任意语言的 query 先归一成标准中文原子序列——英文 query 因此在**词法路**
20
+ 上就能命中中文节点(与 en_zh_terms 默认关并不矛盾:一个在 query 侧归一,一个在召回
21
+ 词扩展侧)。故「跨语词面零交集 ⇒ 召回必须归零」在**缺省态**(两个开关都不设)与
22
+ 「两个开关都关」两种形态下都成立;命中只在显式 `MDCG_UNIFY_QUERY=1` 时断言。
23
+ 三态都钉住(改口径必红)。
23
24
 
24
25
  运行:python -m md_cg.test_en_pipeline
25
26
  """
@@ -143,14 +144,22 @@ try:
143
144
  ids = [r[0].get("id") for r in res]
144
145
  del os.environ["MDCG_EN_ATOMS"]
145
146
  ok(len(res) >= 1 and ids[0] == "n_beef", "开启:英文 query top1 召回中文牛肉节点 %s" % ids)
146
- # 默认(两个开关都不设):**统一归一层**(MDCG_UNIFY_QUERY 默认 "1")把英文
147
- # query 归一成中文原子序列 → 词法路直达中文节点。这是 2026-09-23 使用者拍板的
148
- # 口径转正(semantic/unify.py),与旧的「跨语词面零交集」契约相反:
149
- # 旧断言已按新口径改写——默认态断言「命中」,unify=0 才断言「归零」。
147
+ # 2026-09-30 使用者裁定:统一归一层缺省由开翻为关(semantic/unify.py)。
148
+ # 故「两个开关都不设」= **两个都关** → 词面零交集,召回必须归零;
149
+ # 命中只在**显式** MDCG_UNIFY_QUERY=1 时才成立(英文对照路要用)。
150
+ # (旧断言「缺省态命中」对应的是 2026-09-23 的「默认开」口径,已随缺省翻转改写。)
150
151
  res_u, _mu = cg.search("I ate beef yesterday", judge=False)
151
152
  ids_u = [r[0].get("id") for r in res_u]
152
- ok(len(res_u) >= 1 and ids_u[0] == "n_beef",
153
- "默认(unify 开):英文 query 经统一归一后 top1 命中中文节点 %s" % ids_u)
153
+ ok(not res_u or all(r[1] <= 0 for r in res_u),
154
+ "缺省(unify 缺省关 + EN_ATOMS 关):词面零重叠召回归零 %s" % ids_u)
155
+ os.environ["MDCG_UNIFY_QUERY"] = "1"
156
+ try:
157
+ res1, _m1 = cg.search("I ate beef yesterday", judge=False)
158
+ ids1 = [r[0].get("id") for r in res1]
159
+ finally:
160
+ os.environ.pop("MDCG_UNIFY_QUERY", None)
161
+ ok(len(res1) >= 1 and ids1[0] == "n_beef",
162
+ "显式 unify=1:英文 query 经统一归一后 top1 命中中文节点 %s" % ids1)
154
163
  os.environ["MDCG_UNIFY_QUERY"] = "0"
155
164
  try:
156
165
  res0, _m0 = cg.search("I ate beef yesterday", judge=False) # 关归一+EN_ATOMS 关
@@ -0,0 +1,352 @@
1
+ # -*- coding: utf-8 -*-
2
+ """守卫:代校验单点(`md_cg/generation.py`)与它的两处接线
3
+
4
+ 用法(仓根直跑,退出码 0 = 全绿):
5
+
6
+ python -X utf8 -m md_cg.test_generation_guard
7
+
8
+ 守的是「长驻 MCP 进程跨代混合快照」这条已定因缺陷的处置(使用者裁定方案 b:
9
+ 把「升级后须重启长驻 MCP 进程」写成运维前提 + 加一道代校验)。五项:
10
+
11
+ 一 全绿:`verify_delegations()` 对**当前树**(逐字节临时副本)必须通过,
12
+ 并打印扫到的委托项数与目标模块数。
13
+ 二 定点变异自证:在临时副本里把 `md_cg/mdcg.py` 的 `mint_auto_id` 定义改名,
14
+ `verify_delegations()` 必须抛 `GenerationError` 且消息**逐字点名**目标子模块
15
+ 与名字(证明它不是「恒绿的摆设」)。
16
+ 三 判别力自证:临时副本里改一个源文件后 `is_stale()` 必须 True,恢复后必须 False
17
+ (证明指纹认内容不认 mtime)。
18
+ 四 AST 断言:`mcp_server.py` 里 `generation.verify_delegations(` 的调用**不在
19
+ 模块顶层**——只在函数体内(顶层调用会与函数内延迟导入的避环设计冲突)。
20
+ 五 键集对拍:对临时 root 上的 `cg(op=info)` 真调一次 MCP 服务,响应必须含
21
+ code_generation / disk_generation / stale_on_disk / restart_required / hint
22
+ 五个新键,且**既有键集合**与改动前读码钉住的 18 键逐字一致。
23
+
24
+ 安全边界:一切在 `tempfile.mkdtemp()` 临时目录内进行(md_cg 树是复制出来的副本),
25
+ 绝不写工作区、绝不写任何在役记忆库、绝不起用真 root 的常驻服务、绝不重启/终止
26
+ 任何在跑的进程——本守卫只**新起**短命子进程(argv 列表 + shell=False + UTF-8)。
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import ast
31
+ import json
32
+ import os
33
+ import shutil
34
+ import subprocess
35
+ import sys
36
+ import tempfile
37
+
38
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
39
+ if REPO not in sys.path: # 保证 `import md_cg.*` 能定位到当前树的包
40
+ sys.path.insert(0, REPO)
41
+
42
+ # 改动前读码钉住的 cg(op=info) 既有键集合(在临时 root 上实跑一次服务所读,
43
+ # 2026-09-30 接线前):本守卫要证明这 18 键一字未动。
44
+ 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",
49
+ })
50
+
51
+ NEW_INFO_KEYS = ("code_generation", "disk_generation", "stale_on_disk",
52
+ "restart_required", "hint")
53
+
54
+ _FAILED = []
55
+
56
+
57
+ def _ok(tag, text):
58
+ print("[PASS] %s —— %s" % (tag, text))
59
+
60
+
61
+ def _fail(tag, text):
62
+ _FAILED.append(tag)
63
+ print("[FAIL] %s —— %s" % (tag, text))
64
+
65
+
66
+ def _child_env(root):
67
+ """子进程环境:显式 UTF-8 + 全临时 root(绝不碰在役库)。"""
68
+ env = dict(os.environ)
69
+ env["PYTHONUTF8"] = "1"
70
+ env["PYTHONIOENCODING"] = "utf-8"
71
+ env["PYTHONPATH"] = root
72
+ env["MDCG_ROOT"] = os.path.join(root, "_guard_mem")
73
+ env["MDCG_AUX_ROOT"] = os.path.join(root, "_guard_aux")
74
+ env["DSH_HOME"] = root
75
+ env.pop("MDCG_TOKEN", None)
76
+ return env
77
+
78
+
79
+ def _run(args, cwd, env, timeout=600):
80
+ """argv 列表 + shell=False + capture_output 的显式 UTF-8 调用(第 15 条)。"""
81
+ return subprocess.run(args, cwd=cwd, env=env, capture_output=True, text=True,
82
+ encoding="utf-8", errors="replace", shell=False,
83
+ timeout=timeout)
84
+
85
+
86
+ def _copy_tree(dst_root):
87
+ """把当前树的 md_cg 复制成临时副本,返回副本包目录。
88
+
89
+ 连仓根一级模块(`utf8_boot.py`——md_cg 各入口在文件 I/O 前用它自保证 UTF-8)
90
+ 一并复制:否则副本里 `import md_cg.mcp_server` 会因缺 utf8_boot 而失败,把
91
+ 指向它的延迟导入统统降级成「非致命不可用」,副本就不再忠实于当前树。
92
+ """
93
+ shutil.copytree(os.path.join(REPO, "md_cg"),
94
+ os.path.join(dst_root, "md_cg"),
95
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
96
+ for name in os.listdir(REPO):
97
+ if name.endswith(".py"):
98
+ shutil.copy2(os.path.join(REPO, name), os.path.join(dst_root, name))
99
+ return os.path.join(dst_root, "md_cg")
100
+
101
+
102
+ _VERIFY_SNIPPET = (
103
+ "import json\n"
104
+ "from md_cg import generation as g\n"
105
+ "r = g.verify_delegations()\n"
106
+ "print(json.dumps({'ok': r['ok'], 'delegations': r['delegations'],\n"
107
+ " 'target_modules': r['target_modules'],\n"
108
+ " 'resolved': r['resolved'],\n"
109
+ " 'scanned_files': r['scanned_files'],\n"
110
+ " 'unavailable': len(r['unavailable']),\n"
111
+ " 'generation': g.STARTUP_FINGERPRINT[:12]}, ensure_ascii=False))\n"
112
+ )
113
+
114
+ _MUTATION_SNIPPET = (
115
+ "import json, sys\n"
116
+ "from md_cg import generation as g\n"
117
+ "try:\n"
118
+ " g.verify_delegations()\n"
119
+ "except g.GenerationError as e:\n"
120
+ " print(json.dumps({'raised': 'GenerationError', 'msg': str(e)},\n"
121
+ " ensure_ascii=False))\n"
122
+ "except Exception as e:\n"
123
+ " print(json.dumps({'raised': type(e).__name__, 'msg': str(e)},\n"
124
+ " ensure_ascii=False))\n"
125
+ "else:\n"
126
+ " print(json.dumps({'raised': None}, ensure_ascii=False))\n"
127
+ )
128
+
129
+ _STALE_SNIPPET = (
130
+ "import io, json, os\n"
131
+ "from md_cg import generation as g\n"
132
+ "target = os.path.join(os.path.dirname(g.__file__), 'fsutil.py')\n"
133
+ "raw = io.open(target, encoding='utf-8').read()\n"
134
+ "clean = g.is_stale()\n"
135
+ "io.open(target, 'w', encoding='utf-8', newline='').write(raw + '\\n# 守卫临时改动\\n')\n"
136
+ "modified = g.is_stale()\n"
137
+ "io.open(target, 'w', encoding='utf-8', newline='').write(raw)\n"
138
+ "restored = g.is_stale()\n"
139
+ "print(json.dumps({'clean': clean, 'modified': modified,\n"
140
+ " 'restored': restored, 'fp': g.fingerprint()[:12]},\n"
141
+ " ensure_ascii=False))\n"
142
+ )
143
+
144
+
145
+ # 一 + 二 + 三:临时副本上的三项自证
146
+ def check_verify_and_mutation(tmp):
147
+ print("\n== 一 全绿(当前树的临时副本)+ 二 定点变异 + 三 判别力 ==")
148
+ _copy_tree(tmp)
149
+ env = _child_env(tmp)
150
+
151
+ real = _run([sys.executable, "-X", "utf8", "-c", _VERIFY_SNIPPET], tmp, env)
152
+ if real.returncode != 0:
153
+ _fail("一·全绿", "子进程退出码 %s:%s" % (real.returncode,
154
+ (real.stderr or "")[-500:]))
155
+ return
156
+ data = json.loads(real.stdout.strip().splitlines()[-1])
157
+ print(" 扫描文件 %d 个 / 委托项 %d 项 / 目标模块 %d 个 / 已解析 %d 项 / "
158
+ "可选缺失 %d 项 / 副本代 %s"
159
+ % (data["scanned_files"], data["delegations"], data["target_modules"],
160
+ data["resolved"], data["unavailable"], data["generation"]))
161
+ if not data["ok"]:
162
+ _fail("一·全绿", "verify_delegations() 在副本上未通过")
163
+ return
164
+
165
+ # 副本与当前树同形自证:现树的指纹(本进程内只读计算)必须等于副本自报的代
166
+ from md_cg import generation as _real_gen # 只读:只算哈希
167
+ real_fp = _real_gen.fingerprint()[:12]
168
+ if real_fp != data["generation"]:
169
+ _fail("一·全绿", "副本代 %s 与当前树代 %s 不同——副本不忠实,结论不成立"
170
+ % (data["generation"], real_fp))
171
+ return
172
+ _ok("一·全绿", "verify_delegations() 通过,且副本与当前树同代 %s" % real_fp)
173
+
174
+ # 二:定点变异——改名 mdcg.py 里的 mint_auto_id 定义
175
+ mdcg_path = os.path.join(tmp, "md_cg", "mdcg.py")
176
+ with open(mdcg_path, encoding="utf-8") as fh:
177
+ original = fh.read()
178
+ mutated = original.replace("def mint_auto_id(", "def mint_auto_id_renamed(", 1)
179
+ if mutated == original:
180
+ _fail("二·定点变异", "定位不到 def mint_auto_id( ——变异未生效,本条不成立")
181
+ return
182
+ with open(mdcg_path, "w", encoding="utf-8", newline="") as fh:
183
+ fh.write(mutated)
184
+ try:
185
+ mut = _run([sys.executable, "-X", "utf8", "-c", _MUTATION_SNIPPET], tmp, env)
186
+ finally:
187
+ with open(mdcg_path, "w", encoding="utf-8", newline="") as fh:
188
+ fh.write(original)
189
+ if mut.returncode != 0:
190
+ _fail("二·定点变异", "子进程退出码 %s:%s" % (mut.returncode,
191
+ (mut.stderr or "")[-500:]))
192
+ return
193
+ got = json.loads(mut.stdout.strip().splitlines()[-1])
194
+ if got.get("raised") != "GenerationError":
195
+ _fail("二·定点变异", "期望 GenerationError,实得 %r" % (got.get("raised"),))
196
+ return
197
+ msg = got.get("msg") or ""
198
+ if "md_cg.mdcg" not in msg or "mint_auto_id" not in msg:
199
+ _fail("二·定点变异", "异常消息未逐字点名子模块/名字:%r" % msg[:400])
200
+ return
201
+ print(" 异常消息首行:%s" % msg.splitlines()[0])
202
+ _ok("二·定点变异", "去掉 mint_auto_id 定义后抛出 GenerationError,消息点名 "
203
+ "md_cg.mdcg + mint_auto_id")
204
+
205
+ # 三:判别力——同一进程内改盘 → is_stale True;恢复 → False
206
+ st = _run([sys.executable, "-X", "utf8", "-c", _STALE_SNIPPET], tmp, env)
207
+ if st.returncode != 0:
208
+ _fail("三·判别力", "子进程退出码 %s:%s" % (st.returncode,
209
+ (st.stderr or "")[-500:]))
210
+ return
211
+ s = json.loads(st.stdout.strip().splitlines()[-1])
212
+ if (s["clean"], s["modified"], s["restored"]) != (False, True, False):
213
+ _fail("三·判别力", "期望 (False, True, False),实得 %r"
214
+ % ((s["clean"], s["modified"], s["restored"]),))
215
+ return
216
+ if s["fp"] != real_fp:
217
+ _fail("三·判别力", "恢复后指纹 %s ≠ 原代 %s(恢复不彻底)" % (s["fp"], real_fp))
218
+ return
219
+ _ok("三·判别力", "改一个源文件后 is_stale()=True,恢复后 False(代 %s 复原)"
220
+ % s["fp"])
221
+
222
+
223
+ # 四:AST 断言——verify_delegations 的调用不在 mcp_server.py 的模块顶层
224
+ def check_call_site_not_toplevel():
225
+ print("\n== 四 AST 断言:调用点不在模块顶层 ==")
226
+ path = os.path.join(REPO, "md_cg", "mcp_server.py")
227
+ with open(path, encoding="utf-8") as fh:
228
+ tree = ast.parse(fh.read(), path)
229
+
230
+ found = [] # (lineno, depth, 是否在 main 内)
231
+
232
+ class _V(ast.NodeVisitor):
233
+ def __init__(self):
234
+ self.depth = 0
235
+ self.func = []
236
+
237
+ def _fn(self, node):
238
+ self.depth += 1
239
+ self.func.append(node.name)
240
+ self.generic_visit(node)
241
+ self.func.pop()
242
+ self.depth -= 1
243
+
244
+ visit_FunctionDef = _fn
245
+ visit_AsyncFunctionDef = _fn
246
+
247
+ def visit_Call(self, node):
248
+ f = node.func
249
+ if isinstance(f, ast.Attribute) and f.attr == "verify_delegations":
250
+ found.append((node.lineno, self.depth,
251
+ self.func[-1] if self.func else None))
252
+ self.generic_visit(node)
253
+
254
+ _V().visit(tree)
255
+ if not found:
256
+ _fail("四·调用点", "mcp_server.py 里找不到 *.verify_delegations(...) 调用"
257
+ "——接线缺失")
258
+ return
259
+ top = [f for f in found if f[1] == 0]
260
+ if top:
261
+ _fail("四·调用点", "存在模块顶层调用(第 %s 行)——会与函数内延迟导入的"
262
+ "避环设计冲突" % ", ".join(str(t[0]) for t in top))
263
+ return
264
+ _ok("四·调用点", "%d 处调用全在函数体内:%s"
265
+ % (len(found), ", ".join("%s:%d" % (f[2], f[0]) for f in found)))
266
+
267
+
268
+ # 五:键集对拍——对临时 root 上真起的 MCP 服务调 cg(op=info)
269
+ def check_info_keys(tmp):
270
+ print("\n== 五 键集对拍:cg(op=info) 真调(临时 root) ==")
271
+ env = _child_env(tmp)
272
+ # 本条要验的是**工作区里这套接线**(不是副本),故包来源显式钉到当前树:
273
+ # 记忆 root / aux / DSH_HOME 仍是临时目录(隔离),只有代码取自当前树。
274
+ env["PYTHONPATH"] = REPO
275
+ lines = [
276
+ {"jsonrpc": "2.0", "id": 1, "method": "initialize",
277
+ "params": {"protocolVersion": "2024-11-05", "capabilities": {},
278
+ "clientInfo": {"name": "generation-guard", "version": "0"}}},
279
+ {"jsonrpc": "2.0", "id": 2, "method": "tools/call",
280
+ "params": {"name": "cg", "arguments": {"op": "info"}}},
281
+ {"jsonrpc": "2.0", "id": 3, "method": "shutdown", "params": {}},
282
+ ]
283
+ payload = "\n".join(json.dumps(x, ensure_ascii=False) for x in lines) + "\n"
284
+ r = subprocess.run([sys.executable, "-X", "utf8", "-m", "md_cg.mcp_server"],
285
+ cwd=REPO, env=env, input=payload, capture_output=True,
286
+ text=True, encoding="utf-8", errors="replace",
287
+ shell=False, timeout=600)
288
+ if r.returncode != 0:
289
+ _fail("五·键集", "mcp_server 退出码 %s(代校验接线可能把启动打死了):%s"
290
+ % (r.returncode, (r.stderr or "")[-700:]))
291
+ return
292
+ info = None
293
+ for line in r.stdout.splitlines():
294
+ line = line.strip()
295
+ if not line:
296
+ continue
297
+ try:
298
+ obj = json.loads(line)
299
+ except ValueError:
300
+ continue
301
+ if obj.get("id") == 2 and "result" in obj:
302
+ info = json.loads(obj["result"]["content"][0]["text"])
303
+ if info is None:
304
+ _fail("五·键集", "没取到 cg(op=info) 响应:stdout=%s" % (r.stdout or "")[:400])
305
+ return
306
+ keys = set(info)
307
+ missing = [k for k in NEW_INFO_KEYS if k not in keys]
308
+ if missing:
309
+ _fail("五·键集", "缺新键:%s" % missing)
310
+ return
311
+ extra = sorted(keys - PINNED_INFO_KEYS - set(NEW_INFO_KEYS))
312
+ lost = sorted(PINNED_INFO_KEYS - keys)
313
+ if extra or lost:
314
+ _fail("五·键集", "既有键集被改动:多出 %s / 丢失 %s" % (extra, lost))
315
+ return
316
+ if info["code_generation"] != info["disk_generation"]:
317
+ _fail("五·键集", "刚起的进程自报跨代:code=%s disk=%s"
318
+ % (info["code_generation"], info["disk_generation"]))
319
+ return
320
+ if info["stale_on_disk"] is not False or info["restart_required"] is not False:
321
+ _fail("五·键集", "刚起的进程 stale_on_disk/restart_required 应为 False,"
322
+ "实得 %r/%r" % (info["stale_on_disk"],
323
+ info["restart_required"]))
324
+ return
325
+ if "重启常驻 MCP 进程" not in (info["hint"] or ""):
326
+ _fail("五·键集", "hint 未含处置指引:%r" % (info["hint"],))
327
+ return
328
+ _ok("五·键集", "五新键齐备、既有 18 键一字未动;代 %s(code==disk,"
329
+ "restart_required=False)"
330
+ % info["code_generation"])
331
+
332
+
333
+ def main():
334
+ print("代校验守卫(generation guard)——临时目录内进行,不碰工作区与在役库")
335
+ tmp = tempfile.mkdtemp(prefix="mdcg_gen_guard_")
336
+ print("临时 root:%s" % tmp)
337
+ try:
338
+ check_verify_and_mutation(tmp)
339
+ check_call_site_not_toplevel()
340
+ check_info_keys(tmp)
341
+ finally:
342
+ shutil.rmtree(tmp, ignore_errors=True)
343
+ print("")
344
+ if _FAILED:
345
+ print("结果:%d 项红 —— %s" % (len(_FAILED), "、".join(_FAILED)))
346
+ return 1
347
+ print("结果:全绿(0 红)")
348
+ return 0
349
+
350
+
351
+ if __name__ == "__main__":
352
+ sys.exit(main())
@@ -33,7 +33,8 @@ sustain 的后台巡检线程与请求线程**共享同一个 MdCG 实例与同
33
33
  G4b 真线程 · 默认 switchinterval;G4c 真线程 · 1e-6(放大窗口)。
34
34
  ③ 判定结果逐位不变(独立 oracle 对照,不靠推理)—— G3(现实现 vs 全域
35
35
  「回缺陷副本」的 30+ 条只读路径输出逐字节相同)+ G3b(**手算** oracle)
36
- ④ 记账面 `_lock` 覆盖范围**未扩**(本批不改并发契约)—— G0
36
+ ④ 记账面 `_lock` 覆盖范围(P1 起:五处 = tidys/evolves/scrubs/heals/
37
+ **sleeps**,第六档睡眠周期新增 `sleeps` 一项)—— G0
37
38
 
38
39
  运行:
39
40
  python -X utf8 -m md_cg.test_h4_sustain_snapshot # 正向
@@ -700,12 +701,21 @@ def _build_fixture(tag: str, n: int = 3):
700
701
  # ═══════════════════════════════════════════════ 断言组
701
702
 
702
703
  def g0():
703
- """记账面未扩:`with self._lock:` 仍恰 4 处(本批不改并发契约)。"""
704
+ """记账面:第六档睡眠周期新增 `sleeps` ⇒ `with self._lock:` 由 4 处变
705
+ **5 处**(P1,2026-10-01:`_tick_sleep` 与既有五档同款记账)。判据仍钉死
706
+ 「每处记账面都真的在锁内」——新增那处不得是没锁的共享可变面。"""
704
707
  with open(os.path.join(_REPO, _RELPATH), encoding="utf-8") as f:
705
708
  src = f.read()
706
709
  n = src.count("with self._lock:")
707
- ok(n == 4, "G0 记账面未扩:`with self._lock:` 仍恰 4 处(tidys/evolves/"
708
- "scrubs/heals)", f"实测 {n} 处")
710
+ ok(n == 5, "G0 记账面:`with self._lock:` 恰 5 处(tidys/evolves/scrubs/"
711
+ "heals/**sleeps**)", f"实测 {n} 处")
712
+ for face in ("self.tidys.append", "self.evolves.append",
713
+ "self.scrubs.append", "self.heals.append",
714
+ "self.sleeps.append"):
715
+ m = re.search(r"with self\._lock:\n(?:[ \t]+.*\n)*?[ \t]+%s"
716
+ % re.escape(face), src)
717
+ ok(m is not None,
718
+ "G0a 记账面 %s 在 `with self._lock:` 内(受锁保护)" % face)
709
719
 
710
720
 
711
721
  def _override_for(relpath_hint: str = None):