@furongjun1999/dsh-memory 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +618 -572
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/README.md +1 -0
  5. package/docs/discipline/harnesses.yaml +31 -0
  6. package/docs/discipline/templates/full.md.tmpl +1 -1
  7. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  8. 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 +418 -0
  9. 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 +441 -0
  10. 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 +398 -0
  11. 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 +498 -0
  12. 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 +669 -0
  13. 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 +683 -0
  14. 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
  15. 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 +205 -0
  16. 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
  17. 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
  18. package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
  19. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
  20. 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 +646 -0
  21. 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_v20.md +283 -0
  22. 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_v21.md +224 -0
  23. 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_v22.md +223 -0
  24. 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_v23.md +293 -0
  25. 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_v24.md +303 -0
  26. 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_v25.md +230 -0
  27. 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
  28. 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 +52 -2
  29. 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
  30. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -0
  31. 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
  32. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
  33. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
  34. 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
  35. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
  36. 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 +220 -0
  37. package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
  38. 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
  39. package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
  40. 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 +460 -460
  41. package/lib/bridge.js +24 -2
  42. package/lib/hooks.d.ts +31 -2
  43. package/lib/hooks.js +219 -16
  44. package/lib/init.js +16 -1
  45. package/lib/lib/prompt_safety.d.ts +52 -1
  46. package/lib/lib/prompt_safety.js +76 -1
  47. package/md_cg/audit.py +652 -379
  48. package/md_cg/bench6_arms.py +1 -1
  49. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  50. package/md_cg/bench_p0.py +1 -1
  51. package/md_cg/branches.py +2 -2
  52. package/md_cg/ccgc.py +1071 -1006
  53. package/md_cg/chain.py +48 -1
  54. package/md_cg/consistency.py +97 -9
  55. package/md_cg/consolidate.py +34 -9
  56. package/md_cg/crypto.py +59 -26
  57. package/md_cg/docindex.py +16 -1
  58. package/md_cg/evolution.py +17 -1
  59. package/md_cg/export.py +1 -1
  60. package/md_cg/forgetting.py +309 -13
  61. package/md_cg/freshness.py +511 -0
  62. package/md_cg/fsutil.py +454 -8
  63. package/md_cg/generation.py +409 -0
  64. package/md_cg/hotcache.py +4 -1
  65. package/md_cg/identity.py +3 -3
  66. package/md_cg/insight.py +24 -3
  67. package/md_cg/linkref.py +1 -1
  68. package/md_cg/logref.py +327 -0
  69. package/md_cg/mcp_server.py +4261 -3818
  70. package/md_cg/mdcg.py +4695 -3547
  71. package/md_cg/mdcos.py +4672 -4159
  72. package/md_cg/mreview/pipeline.py +15 -1
  73. package/md_cg/nodefile.py +713 -576
  74. package/md_cg/protect.py +158 -11
  75. package/md_cg/protocol.py +41 -2
  76. package/md_cg/provenance.py +22 -4
  77. package/md_cg/reach.py +4 -4
  78. package/md_cg/readcache.py +76 -15
  79. package/md_cg/reconcile.py +1 -1
  80. package/md_cg/refindex.py +246 -37
  81. package/md_cg/refine.py +1 -1
  82. package/md_cg/routing.py +32 -5
  83. package/md_cg/run_tests.py +17 -0
  84. package/md_cg/scrub.py +30 -3
  85. package/md_cg/security.py +47 -1
  86. package/md_cg/self_state.py +5 -5
  87. package/md_cg/semantic/canonical.py +22 -0
  88. package/md_cg/semantic/unify.py +73 -22
  89. package/md_cg/semantic/unify_fixture.json +25 -0
  90. package/md_cg/sleep.py +1297 -0
  91. package/md_cg/sources.py +23 -6
  92. package/md_cg/srcindex.py +352 -0
  93. package/md_cg/stg.py +47 -3
  94. package/md_cg/subgraph.py +2 -2
  95. package/md_cg/sustain.py +1436 -1168
  96. package/md_cg/tasks.py +469 -470
  97. package/md_cg/test_auto_defaults.py +424 -0
  98. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  99. package/md_cg/test_b1b2_write_face.py +417 -0
  100. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  101. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  102. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  103. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  104. package/md_cg/test_boundary_hit.py +410 -0
  105. package/md_cg/test_c3_transient_read_negative.py +793 -0
  106. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  107. package/md_cg/test_ccg_form_parity.py +188 -0
  108. package/md_cg/test_ccgc.py +28 -3
  109. package/md_cg/test_en_pipeline.py +22 -13
  110. package/md_cg/test_generation_guard.py +352 -0
  111. package/md_cg/test_govern_directread.py +17 -4
  112. package/md_cg/test_h2_session_view_norm.py +233 -0
  113. package/md_cg/test_h4_sustain_snapshot.py +1387 -0
  114. package/md_cg/test_hive_ingest.py +285 -285
  115. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  116. package/md_cg/test_issue43_default_policy.py +865 -0
  117. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  118. package/md_cg/test_linkref.py +10 -3
  119. package/md_cg/test_lock.py +2 -2
  120. package/md_cg/test_logref.py +1109 -0
  121. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  122. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  123. package/md_cg/test_mr_m2.py +15 -3
  124. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  125. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  126. package/md_cg/test_n176_link_trust.py +221 -0
  127. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  128. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  129. package/md_cg/test_n195_writepath_reload.py +283 -0
  130. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  131. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  132. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  133. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  134. package/md_cg/test_n201_proposal_visibility.py +382 -0
  135. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  136. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  137. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  138. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  139. package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
  140. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  141. package/md_cg/test_n225_nonobject_load.py +1110 -0
  142. package/md_cg/test_n230_dirty_replay.py +375 -0
  143. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  144. package/md_cg/test_neg_condition_hits.py +333 -0
  145. package/md_cg/test_neg_tail_honesty.py +663 -0
  146. package/md_cg/test_none_id_write_guard.py +4 -3
  147. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  148. package/md_cg/test_p1.py +5 -5
  149. package/md_cg/test_p11_consistency.py +161 -12
  150. package/md_cg/test_p26_refindex.py +2 -2
  151. package/md_cg/test_p27_docindex.py +777 -774
  152. package/md_cg/test_p28_refcheck.py +740 -13
  153. package/md_cg/test_p29_session_ingest_export.py +2 -2
  154. package/md_cg/test_p2_mcp.py +12 -1
  155. package/md_cg/test_p2_six_elements.py +463 -0
  156. package/md_cg/test_p30_maintain.py +33 -2
  157. package/md_cg/test_p31_insight.py +1 -0
  158. package/md_cg/test_p3_legacy_closure.py +433 -0
  159. package/md_cg/test_p4_freshness.py +680 -0
  160. package/md_cg/test_p8_subgraph_chain.py +14 -1
  161. package/md_cg/test_p9c_dedup_hints.py +276 -0
  162. package/md_cg/test_policy_required_ccg.py +426 -0
  163. package/md_cg/test_protocol.py +22 -0
  164. package/md_cg/test_rank_parity_score_mode.py +522 -0
  165. package/md_cg/test_read_face_input_gates.py +363 -0
  166. package/md_cg/test_read_face_semantics.py +489 -0
  167. package/md_cg/test_recall_face_guards.py +812 -0
  168. package/md_cg/test_rejected_credential_forms.py +188 -0
  169. package/md_cg/test_rejected_redact.py +146 -0
  170. package/md_cg/test_retr_s1b.py +2 -2
  171. package/md_cg/test_retr_s5.py +20 -7
  172. package/md_cg/test_review_conformance.py +21 -3
  173. package/md_cg/test_security_audit_v21.py +2 -2
  174. package/md_cg/test_semantic_canonical.py +5 -3
  175. package/md_cg/test_server_version.py +67 -0
  176. package/md_cg/test_sleep.py +611 -0
  177. package/md_cg/test_sleep_p1.py +784 -0
  178. package/md_cg/test_srcindex.py +171 -0
  179. package/md_cg/test_tenant_env_override_warn.py +63 -50
  180. package/md_cg/test_time_core_lint.py +968 -0
  181. package/md_cg/test_token_lowercase_form.py +315 -0
  182. package/md_cg/test_unify_default_off.py +701 -0
  183. package/md_cg/test_unify_scope.py +182 -0
  184. package/md_cg/test_v21r1_package_version.py +310 -0
  185. package/md_cg/test_writepipe.py +10 -4
  186. package/md_cg/tokens.py +225 -95
  187. package/md_cg/tool_face.py +4 -4
  188. package/md_cg/trust.py +49 -5
  189. package/md_cg/units.py +204 -6
  190. package/md_cg/weights.py +4 -4
  191. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  192. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  193. package/md_cg/writelimit.py +42 -8
  194. package/md_cg/writepipe.py +651 -554
  195. package/package.json +4 -2
  196. package/skills/plugin.json +1 -1
  197. package/src/bridge.ts +25 -2
  198. package/src/hooks.ts +229 -16
  199. package/src/init.ts +15 -1
  200. package/src/lib/prompt_safety.ts +88 -1
  201. package/utf8_boot.py +237 -0
  202. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,188 @@
1
+ # -*- coding: utf-8 -*-
2
+ """CCG 六要素「形态口径一致性」验收(2026-09-28)。
3
+
4
+ 运行:python -m md_cg.test_ccg_form_parity
5
+
6
+ 为什么需要这一套(本套的由来,是一个真实的口径分叉):
7
+ 六要素标题存在两种书写形态——带冒号 `# 生效条件:<值>` 与不带冒号
8
+ `# 生效条件` 后换行接值。写入闸门 `data/policy.json` 的必需正则
9
+ `(?m)^#\\s*生效条件` **不要求冒号**,而检索面判据 `nodefile.ccg_completeness`
10
+ 原先**要求冒号**才计入「已声明」。二者对同一条正文给出相反结论:归档过闸
11
+ (ACCEPT)后到检索路由被判 BLINDSPOT「CCG 要素不全」——归档看着写成了,
12
+ 检索面却当它没声明。存量实测 6905 件含六要素节点中 88 件处于该状态。
13
+
14
+ 判据收敛在**单点** `nodefile.ccg_mark_present` / `nodefile.ccg_field_value`
15
+ (冒号可有可无),`mdcos._ccg_field`、`ccgc._has_ccg_line`、
16
+ `consolidate._has_ccg_line`、`tasks._field_line` 一律委托该单点。
17
+
18
+ 本套的六组断言:
19
+ ① 两形态都判齐(形态不改变「齐不齐」)
20
+ ② 缺要素仍判不全(放宽的是标点,不是要求——判别力自证,两种形态各测)
21
+ ③ 写入闸门与检索面**结论一致**(读真源 data/policy.json 复算,直接钉住分叉)
22
+ ④ 取值面同源:无冒号的「值」= 标题后首个非空非标题行
23
+ ⑤ 四个副本函数与单点结论一致(防未来再长出第五、第六套口径)
24
+ ⑥ 写读闭环:无冒号节点经 upsert 后取得到新值(不出现「写进去读不出」)
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import io
29
+ import json
30
+ import os
31
+ import re
32
+ import sys
33
+
34
+ from . import ccgc, consolidate, nodefile, tasks
35
+ from .mdcos import _ccg_field
36
+
37
+ PASS = FAIL = 0
38
+ FAILS = []
39
+
40
+
41
+ def ok(cond, label):
42
+ global PASS, FAIL
43
+ if cond:
44
+ PASS += 1
45
+ print(" [PASS] %s" % label)
46
+ else:
47
+ FAIL += 1
48
+ FAILS.append(label)
49
+ print(" [FAIL] %s" % label)
50
+
51
+
52
+ MARKS = ("功能名", "生效条件", "子功能", "执行", "验证方式", "不适用条件")
53
+
54
+ # ① 带冒号形态(既有惯例形态)
55
+ COLON = "".join("# %s:值_%s\n" % (m, m) for m in MARKS)
56
+ # ① 不带冒号形态(policy 正则接受,标题行 + 换行接值)
57
+ NONCOLON = "".join("# %s\n值_%s\n" % (m, m) for m in MARKS)
58
+ # ① 混形态(一半带冒号一半不带)
59
+ MIXED = "".join(("# %s:值_%s\n" if i % 2 == 0 else "# %s\n值_%s\n") % (m, m)
60
+ for i, m in enumerate(MARKS))
61
+
62
+
63
+ def _drop(body: str, mark: str) -> str:
64
+ """删掉某要素的标题行(连带其值行)——用于②的判别力自证。"""
65
+ out = []
66
+ skip_next = False
67
+ for ln in body.splitlines(True):
68
+ if skip_next:
69
+ skip_next = False
70
+ continue
71
+ if re.match(r"^#\s*%s\s*[::]?\s*$" % re.escape(mark), ln.rstrip("\n")):
72
+ skip_next = True
73
+ continue
74
+ if ln.startswith("# %s:" % mark) or ln.startswith("# %s:" % mark):
75
+ continue
76
+ out.append(ln)
77
+ return "".join(out)
78
+
79
+
80
+ def _policy_required() -> list:
81
+ """读**真源**策略文件的必需正则(相对本模块定位,不依赖 cwd)。"""
82
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
83
+ path = os.path.join(root, "data", "policy.json")
84
+ with io.open(path, encoding="utf-8") as fh:
85
+ return [re.compile(p) for p in json.load(fh)["required"]]
86
+
87
+
88
+ def main() -> int:
89
+ print("== ① 两形态都判齐 ==")
90
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON), ("混形态", MIXED)):
91
+ comp = nodefile.ccg_completeness(body)
92
+ ok(comp["complete"] and sorted(comp["required_present"]) == sorted(MARKS),
93
+ "①%s:六要素判齐(present=%d/%d)" % (name, len(comp["required_present"]), len(MARKS)))
94
+ ok(abs(comp["ratio"] - 1.0) < 1e-9, "①%s:ratio=1.0(%s)" % (name, comp["ratio"]))
95
+
96
+ print("== ② 缺要素仍判不全(放宽的是标点,不是要求)==")
97
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON)):
98
+ for mark in MARKS:
99
+ broken = _drop(body, mark)
100
+ comp = nodefile.ccg_completeness(broken)
101
+ ok(not comp["complete"] and mark not in comp["required_present"],
102
+ "②%s 缺「%s」→ 判不全" % (name, mark))
103
+ ok(not nodefile.ccg_completeness("")["complete"], "②空正文 → 判不全")
104
+ ok(not nodefile.ccg_completeness("# 生效条件\n只要这一条\n")["complete"],
105
+ "②只声明一条 → 判不全(不冒充齐全)")
106
+
107
+ print("== ③ 写入闸门与检索面结论一致(读真源 policy 复算)==")
108
+ pats = _policy_required()
109
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON), ("混形态", MIXED),
110
+ ("空正文", ""), ("只一条", "# 生效条件:x\n")):
111
+ gate = all(p.search(body) for p in pats)
112
+ retr = nodefile.ccg_completeness(body)["complete"]
113
+ ok(gate == retr, "③%s:闸门=%s 检索面=%s(必须相等)" % (name, gate, retr))
114
+ for mark in MARKS:
115
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON)):
116
+ broken = _drop(body, mark)
117
+ gate = all(p.search(broken) for p in pats)
118
+ retr = nodefile.ccg_completeness(broken)["complete"]
119
+ ok(gate == retr and not retr,
120
+ "③%s 缺「%s」:两侧同判不全" % (name, mark))
121
+ # 边界形态:二级标题 / 缩进标题 / 无空格 —— 两侧必须同判(宽松方向也要一致)。
122
+ # 注意按**该要素自身**的那条必需正则比对(拿「全六条」的结论比「单条」判据
123
+ # 是错的口径——空正文与二级标题会因两侧同假而蒙对,暴露不出真差异)。
124
+ for name, line in (("二级标题", "## 生效条件:x\n"),
125
+ ("缩进标题", " # 生效条件:x\n"),
126
+ ("无空格", "#生效条件:x\n"),
127
+ ("制表符", "#\t生效条件:x\n")):
128
+ pat = next(p for p in pats if "生效条件" in p.pattern)
129
+ gate = bool(pat.search(line))
130
+ present = nodefile.ccg_mark_present(line, "生效条件")
131
+ ok(gate == present,
132
+ "③%s(%r):闸门=%s 判据=%s(必须相等)" % (name, line.strip(), gate, present))
133
+ # 全形态 × 全要素的逐条一致性(比「整体 complete」更细,能定位到具体要素)
134
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON), ("混形态", MIXED)):
135
+ for mark in MARKS:
136
+ pat = next(p for p in pats if mark in p.pattern)
137
+ ok(bool(pat.search(body)) == nodefile.ccg_mark_present(body, mark),
138
+ "③%s 逐要素一致:「%s」" % (name, mark))
139
+
140
+ print("== ④ 取值面:无冒号的值 = 标题后首个非空非标题行 ==")
141
+ ok(nodefile.ccg_field_value(COLON, "功能名") == "值_功能名",
142
+ "④带冒号:取行内值")
143
+ ok(nodefile.ccg_field_value(NONCOLON, "功能名") == "值_功能名",
144
+ "④无冒号:取下一行值")
145
+ ok(nodefile.ccg_field_value("# 执行\n\n真实值行\n后文\n", "执行") == "真实值行",
146
+ "④无冒号:跳过空行取首个非空行")
147
+ ok(nodefile.ccg_field_value("# 功能名\n# 生效条件:x\n", "功能名") == "",
148
+ "④无冒号且下一行又是标题:取空串而非吞掉下一个要素")
149
+ ok(nodefile.ccg_field_value(COLON, "不存在的字段") is None,
150
+ "④无该字段行 → None")
151
+
152
+ print("== ⑤ 四个副本与单点结论一致(防再长出口径分叉)==")
153
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON)):
154
+ for mark in ("功能名", "生效条件", "执行"):
155
+ single = nodefile.ccg_mark_present(body, mark)
156
+ ok(ccgc._has_ccg_line(body, mark) == single,
157
+ "⑤ccgc._has_ccg_line(%s,%s) 与单点一致" % (name, mark))
158
+ ok(consolidate._has_ccg_line(body, mark) == single,
159
+ "⑤consolidate._has_ccg_line(%s,%s) 与单点一致" % (name, mark))
160
+ ok(bool(_ccg_field(body, mark)) == single or not single,
161
+ "⑤mdcos._ccg_field(%s,%s) 与单点一致" % (name, mark))
162
+ ok(tasks._field_line(body, mark) == (nodefile.ccg_field_value(body, mark) or ""),
163
+ "⑤tasks._field_line(%s,%s) 与单点同值" % (name, mark))
164
+
165
+ print("== ⑥ 写读闭环:无冒号节点 upsert 后取得到新值 ==")
166
+ for enc, upsert in (("ccgc", ccgc._upsert_ccg_line), ("consolidate", consolidate._upsert_ccg_line)):
167
+ after = upsert(NONCOLON, "生效条件", "新条件")
168
+ ok(_ccg_field(after, "生效条件") == "新条件",
169
+ "⑥%s:无冒号节点 upsert 后 mdcos 读出「新条件」" % enc)
170
+ ok(nodefile.ccg_field_value(after, "生效条件") == "新条件",
171
+ "⑥%s:同一次写单点读出「新条件」" % enc)
172
+ ok(nodefile.ccg_completeness(after)["complete"],
173
+ "⑥%s:upsert 后仍判齐" % enc)
174
+ try:
175
+ upsert(NONCOLON, "生效条件", "多行\n注入")
176
+ ok(False, "⑥%s:值含换行应被拒(N208)" % enc)
177
+ except ValueError:
178
+ ok(True, "⑥%s:值含换行被拒(N208 fail-closed)" % enc)
179
+
180
+ print()
181
+ print("ccg_form_parity: PASS=%d FAIL=%d" % (PASS, FAIL))
182
+ if FAILS:
183
+ print("FAILS: " + "; ".join(FAILS))
184
+ return 1 if FAIL else 0
185
+
186
+
187
+ if __name__ == "__main__":
188
+ sys.exit(main())
@@ -23,7 +23,7 @@ import tempfile
23
23
  import threading
24
24
  import time
25
25
 
26
- from . import audit, backfill, ccgc, nodefile, units
26
+ from . import audit, backfill, ccgc, nodefile, tokens, units
27
27
  from .mdcos import MdCGOS
28
28
 
29
29
  PASS = FAIL = 0
@@ -126,6 +126,13 @@ def _import_mcp():
126
126
  def main():
127
127
  global PASS, FAIL
128
128
  tmp = tempfile.mkdtemp(prefix="mdcg_ccgc_")
129
+ # N176 契约:落库准入须令牌验证方——本测试自备哑令牌环境(临时 MDCG_TOKEN_FILE)
130
+ _old_tf = os.environ.get(tokens.TOKEN_FILE_ENV)
131
+ os.environ[tokens.TOKEN_FILE_ENV] = os.path.join(tmp, "tokens.json")
132
+ tk_reviewer = tokens.issue("designer", actor="designer",
133
+ path=os.environ[tokens.TOKEN_FILE_ENV])["token"]
134
+ tk_reviewer_ext = tokens.issue("verify", actor="external-reviewer",
135
+ path=os.environ[tokens.TOKEN_FILE_ENV])["token"]
129
136
  try:
130
137
  root = os.path.join(tmp, "root")
131
138
  os.makedirs(root, exist_ok=True)
@@ -192,8 +199,11 @@ def main():
192
199
  a_defer = ccgc.attest("tgt", ccgc.DEFER, "designer", "agent:a1", cg=cg)
193
200
  ok((not a_defer.ok) and "E042" in a_defer.error, "V7b DEFER 不构成签章")
194
201
  a_ok = ccgc.attest("tgt", ccgc.ACCEPT, "designer", "agent:a1",
195
- evidence="设计者核对原文一致", cg=cg)
202
+ evidence="设计者核对原文一致", cg=cg,
203
+ verifier_token=tk_reviewer)
196
204
  ok(a_ok.ok and bool(a_ok.token), "V7c 编外验证方 ACCEPT → 有效签章")
205
+ ok(a_ok.verifier_identity == "token", "V7d 令牌验证方 identity=token"
206
+ "(N176:self-reported 不再构成落库准入)")
197
207
 
198
208
  # ---------- V8 无章不写 ----------
199
209
  l0 = ccgc.link(r, None, cg=cg, apply=True)
@@ -283,7 +293,7 @@ def main():
283
293
  lz = ccgc.link_pending(cg, "tgt", apply=True, actor="agent:a1")
284
294
  ok(lz.ok and lz.written == 6, "V16f 读回原件+签章 → 落库成功")
285
295
  ok(not os.path.isfile(sv["path"]), "V16g 落库成功即清 pending(暂存非存档)")
286
- ok(not any(ccgc.PENDING_DIR in str(k) for k in (cg.index.get("nodes") or {})),
296
+ ok(not any(ccgc.PENDING_DIR in str(k) for k in (list(cg.index.get("nodes") or {}))),
287
297
  "V16h pending 不进索引(冷区对检索隐身)")
288
298
 
289
299
  # ---------- V17 闸门:只认编外裁决,绝不假装通过 ----------
@@ -396,6 +406,17 @@ def main():
396
406
  ok(c5["state"] == units.HIVE and c5["verdict"] == "ACCEPT" and c5["passed"] is True,
397
407
  "V20g review 取回裁决:ok/verdict/passed 三者分离")
398
408
  ok(bool((c5.get("attest") or {}).get("ok")), "V20h 拿到裁决即自动签章")
409
+ c5b = mcp._ccg_call(cg, {"ccg": {"action": "link", "node_id": "tgt",
410
+ "apply": True}})
411
+ ok((not c5b["ok"]) and "E052" in str((c5b.get("link") or {}).get("errors")),
412
+ "V20n 无令牌自报签章 → link 拒绝(N176 E052,不落 knowledge 层)")
413
+ c5c = mcp._ccg_call(cg, {"ccg": {"action": "attest", "node_id": "tgt",
414
+ "verdict": "ACCEPT",
415
+ "verifier": "external-reviewer",
416
+ "verifier_token": tk_reviewer_ext,
417
+ "evidence": "令牌验证方复核"}})
418
+ ok(c5c["ok"] and (c5c.get("attest") or {}).get("verifier_identity") == "token",
419
+ "V20o 令牌验证方重签章 → identity=token(挂回 pending 待落库)")
399
420
  c7 = mcp._ccg_call(cg, {"ccg": {"action": "review", "node_id": "tgt",
400
421
  "jobs": jobs, "blocking": False}})
401
422
  ok(c7["state"] == units.HIVE and c7["verdict"] is None and c7["passed"] is False,
@@ -421,6 +442,10 @@ def main():
421
442
  os.environ[units.ENV_EXE] = _old_exe
422
443
 
423
444
  finally:
445
+ if _old_tf is None:
446
+ os.environ.pop(tokens.TOKEN_FILE_ENV, None)
447
+ else:
448
+ os.environ[tokens.TOKEN_FILE_ENV] = _old_tf
424
449
  print()
425
450
  print("ccgc: PASS=%d FAIL=%d" % (PASS, FAIL))
426
451
  if FAILS:
@@ -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())