@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,156 @@
1
+ # -*- coding: utf-8 -*-
2
+ """N238 · `_body_text` 剥除判据须与 CCG 形态单点同源(冒号可有可无)。
3
+
4
+ 运行:python -m md_cg.test_n238_body_text_parity
5
+
6
+ 缺陷(本会话实跑复现):
7
+ `consistency._body_text` 的本地判据是「以 `#` 开头**且含冒号**」——
8
+ 无冒号形态的六要素(`# 不适用条件` + 换行 + 值行)不满足该条件,标题行
9
+ 与**值行**双双残留正文。后果:同一条内容
10
+ 带冒号 `# 不适用条件:不得把甲当作乙` → `_body_text == ''`、不命中;
11
+ 无冒号 `# 不适用条件` + `不得把甲当作乙` → body 含该短语 ⇒
12
+ `_ban_hit(body, neg)` 恒 True ⇒ self_negation / conflict_strength=1.0
13
+ ⇒ verdict=REJECT(writepipe 的 auto_flywheel 下合法节点被拦入
14
+ review_queue 并自动建飞轮工单)。
15
+ 「冒号可有可无」已由单点 `nodefile._ccg_heading_rest` / `ccg_mark_present` /
16
+ `ccg_field_value` 定案(`test_ccg_form_parity` 验收),本处是漏网的分叉。
17
+
18
+ 修法:`_body_text` 复用单点判据,剥「标题行在即已声明」的**整段字段**——
19
+ 行内带值(冒号式/前缀式)只剥该行;裸标题连同其后首个非空、非标题行
20
+ (值行,口径同 `nodefile.ccg_field_value`)一并剥除。
21
+
22
+ 本套断言(纯行为,不做源码文本匹配):
23
+ ① 三种书写形态的声明段一律剥净(`_body_text == ''`),且判定结论同值
24
+ ② 判别力自证:禁令短语真出现在**正文**时仍命中 self_negation(不同形态各测)
25
+ ③ 值行剥除的边界(紧跟标题不吞/隔空行取值行/行内冒号不误剥后文)
26
+ ④ 逐要素 × 逐形态机械对拍:只声明该要素 + 一行正文 ⇒ `_body_text == 正文`
27
+ ⑤ 飞轮侧证据:合法无冒号节点不再触发 auto_flywheel 建单;真自否定仍触发
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import os
32
+ import sys
33
+ import tempfile
34
+
35
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
36
+
37
+ from md_cg import consistency, nodefile
38
+
39
+ PASS = FAIL = 0
40
+ FAILS = []
41
+
42
+
43
+ # 生效条件:cond 为真时 PASS 加 1 并打印 [PASS],否则 FAIL 加 1、把 label 记入 FAILS 并打印 [FAIL]。
44
+ def ok(cond, label):
45
+ global PASS, FAIL
46
+ if cond:
47
+ PASS += 1
48
+ print(" [PASS] %s" % label)
49
+ else:
50
+ FAIL += 1
51
+ FAILS.append(label)
52
+ print(" [FAIL] %s" % label)
53
+
54
+
55
+ MARKS = nodefile.CCG_MARKS # 六要素(真源,不另抄一份)
56
+ VALS = {"功能名": "测试条目", "生效条件": "载体/位置:本地源码仓;时间:全时窗",
57
+ "子功能": "演示", "执行": "步骤一", "验证方式": "test",
58
+ "不适用条件": "不得把甲当作乙"}
59
+
60
+
61
+ # 生效条件:form 取 "colon"/"bare"/"mixed",返回按该形态书写的六要素声明段;
62
+ def decls(form: str) -> str:
63
+ out = []
64
+ for i, m in enumerate(MARKS):
65
+ v = VALS[m]
66
+ use_colon = (form == "colon") or (form == "mixed" and i % 2 == 0)
67
+ out.append("# %s:%s\n" % (m, v) if use_colon else "# %s\n%s\n" % (m, v))
68
+ return "".join(out)
69
+
70
+
71
+ class _CG:
72
+ """最小 cg 替身:空索引(无从比对 ⇒ 判定只看新节点自身)+ 飞轮计数。"""
73
+
74
+ def __init__(self, root):
75
+ self.index = {"nodes": {}}
76
+ self.root = root
77
+ self.flywheel_calls = []
78
+
79
+ def flywheel_step(self, payload):
80
+ self.flywheel_calls.append(payload)
81
+ return {"unresolved_id": "unr_test_%d" % len(self.flywheel_calls)}
82
+
83
+
84
+ def main() -> int:
85
+ tmp = tempfile.mkdtemp()
86
+
87
+ print("== ① 三种形态的声明段一律剥净,且判定结论同值 ==")
88
+ verdicts = {}
89
+ for form in ("colon", "bare", "mixed"):
90
+ body = consistency._body_text(decls(form))
91
+ ok(body == "", "①%s:_body_text 剥净(得到 %r)" % (form, body))
92
+ cg = _CG(tmp)
93
+ rec = consistency.check(cg, decls(form), log_write=False)
94
+ verdicts[form] = (rec["verdict"], rec["conflict_strength"],
95
+ tuple((c["type"], c["score"]) for c in rec["conflicts"]))
96
+ ok(not rec["conflicts"] and rec["conflict_strength"] == 0.0,
97
+ "①%s:无硬冲突、strength=0.0(%s)" % (form, verdicts[form][:2]))
98
+ ok(len(set(verdicts.values())) == 1,
99
+ "①同内容三形态判定结论一致(%s)" % (verdicts,))
100
+
101
+ print("== ② 判别力自证:禁令短语真在正文时仍判 self_negation ==")
102
+ for form in ("colon", "bare", "mixed"):
103
+ real = decls(form) + "本流程说明:不得把甲当作乙,否则告警。\n"
104
+ body = consistency._body_text(real)
105
+ ok(consistency._ban_hit(body, ["不得把甲当作乙"]),
106
+ "②%s:正文残留行命中禁令短语" % form)
107
+ rec = consistency.check(_CG(tmp), real, log_write=False)
108
+ ok(rec["verdict"] == "REJECT" and rec["conflict_strength"] == 1.0
109
+ and any(c["type"] == "self_negation" for c in rec["conflicts"]),
110
+ "②%s:仍判 REJECT/self_negation/1.0(%s)"
111
+ % (form, (rec["verdict"], rec["conflict_strength"])))
112
+
113
+ print("== ③ 值行剥除的边界 ==")
114
+ s = "# 功能名\n# 生效条件:x\n"
115
+ ok(consistency._body_text(s) == "" and nodefile.ccg_field_value(s, "功能名") == "",
116
+ "③裸标题后紧跟另一标题:值空、不吞下一要素")
117
+ s = "# 执行\n\n真实值行\n后文行\n"
118
+ ok(consistency._body_text(s) == "后文行\n"
119
+ and nodefile.ccg_field_value(s, "执行") == "真实值行",
120
+ "③裸标题后隔空行取值行:剥值行、留后文(与取值单点同口径)")
121
+ s = "# 生效条件:条件A\n正文第一行\n正文第二行\n"
122
+ ok(consistency._body_text(s) == "正文第一行\n正文第二行\n",
123
+ "③行内冒号式:后续正文不被误剥")
124
+ ok(consistency._body_text("") == "", "③空正文 → 空串")
125
+
126
+ print("== ④ 逐要素 × 逐形态机械对拍(只声明该要素 + 一行正文)==")
127
+ line = "正文一行\n"
128
+ for form in ("colon", "bare", "mixed"):
129
+ for i, m in enumerate(MARKS):
130
+ v = VALS[m]
131
+ use_colon = (form == "colon") or (form == "mixed" and i % 2 == 0)
132
+ one = ("# %s:%s\n" % (m, v)) if use_colon else ("# %s\n%s\n" % (m, v))
133
+ got = consistency._body_text(one + line)
134
+ ok(got == line, "④%s「%s」:剥净该要素段(得到 %r)" % (form, m, got))
135
+
136
+ print("== ⑤ 飞轮侧证据:合法无冒号节点不再自动建单 ==")
137
+ cg_ok = _CG(tmp)
138
+ rec = consistency.check(cg_ok, decls("bare"), auto_flywheel=True, log_write=False)
139
+ ok(rec["verdict"] != "REJECT" and not cg_ok.flywheel_calls,
140
+ "⑤合法无冒号节点:verdict=%s、flywheel_step 调用 %d 次(须 0,无工单)"
141
+ % (rec["verdict"], len(cg_ok.flywheel_calls)))
142
+ cg_bad = _CG(tmp)
143
+ consistency.check(cg_bad, decls("bare") + "本流程说明:不得把甲当作乙。\n",
144
+ auto_flywheel=True, log_write=False)
145
+ ok(len(cg_bad.flywheel_calls) == 1,
146
+ "⑤真自否定仍建单:flywheel_step 调用 1 次(判别力未削弱)")
147
+
148
+ print()
149
+ print("ccg_body_text_n238: PASS=%d FAIL=%d" % (PASS, FAIL))
150
+ if FAILS:
151
+ print("FAILS: " + "; ".join(FAILS))
152
+ return 1 if FAIL else 0
153
+
154
+
155
+ if __name__ == "__main__":
156
+ sys.exit(main())
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """N276 守卫 · 批内单探针(写链读环的索引代际探活只做一次)。
4
+
5
+ 背景(第 32 轮性能面):`MdCG.get` 首行无条件 `_maybe_reload_index()`,
6
+ 而写链读环(`forgetting.redundancy` 240 上限 / `consistency.check` 精比环)
7
+ 在环内逐节点 `cg.get(nid)` —— 每次重算同一个索引签名(快照 stat +
8
+ listdir + 逐分片 stat)。修法:`get` 增可选 `probe=True`(默认零变),
9
+ 两环循环外单探一次、环内 `cg.get(nid, probe=False)`。
10
+
11
+ 覆盖判据(调用计数优先;墙钟只作辅助不作断言):
12
+ C1 `forgetting.redundancy` 环内零重探:探针计数 == 1(修前 == 同层可比
13
+ 节点数,上限 MAX_COMPARE)
14
+ C2 `consistency.check` 精比环零重探:探针计数 == 1(修前 == len(scan_face))
15
+ C3 `get(node_id, probe=False)` 与 `get(node_id)` 返回逐位一致(默认语义零变)
16
+ C4 自证腿:旧签名 `get(nid)` 逐次探针(spy 有判别力;默认 probe=True 未变
17
+ ——若有人把默认改 False 或去掉环外探针,C1/C2/C4 必红)
18
+ C5 可见性不丢:他实例写入后,单探 + `probe=False` 组合读到新节点(不陈旧)
19
+ C6 无探活面载体(测试桩)回落原 `cg.get(nid)`(既有桩零破坏)
20
+ C7 等价性:redundancy/check 在「单探 + probe=False」与「逐节点 probe=True」
21
+ 两形态下输出逐位一致(C7a/C7b)
22
+
23
+ 运行:python -X utf8 -m md_cg.test_n276_probe_hoist
24
+ 退出码:0 = 全绿;1 = 有失败。
25
+ 硬边界:库根一律 tempfile;不动在役数据根;不 git add/commit/push。
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ import shutil
31
+ import sys
32
+ import tempfile
33
+
34
+ _HERE = os.path.dirname(os.path.abspath(__file__))
35
+ _REPO = os.path.dirname(_HERE)
36
+ sys.path.insert(0, _REPO)
37
+
38
+ from md_cg import consistency, forgetting # noqa: E402
39
+ from md_cg.mdcos import MdCGSecure # noqa: E402
40
+ from md_cg.security import Principal # noqa: E402
41
+
42
+ PASS = FAIL = 0
43
+ FAILS = []
44
+
45
+ BODY = ("# 功能名:%s\n# 生效条件:载体=守卫夹具\n# 子功能:占位\n"
46
+ "# 执行:占位\n# 验证方式:compiler 复现\n# 不适用条件:无\n\n"
47
+ "蜂群调度 节点 %s 依赖门禁 收敛 检索\n")
48
+ CONTENT = ("# 功能名:待写样本\n# 生效条件:载体=守卫夹具\n# 子功能:占位\n"
49
+ "# 执行:占位\n# 验证方式:compiler 复现\n# 不适用条件:无\n\n"
50
+ "蜂群调度 待写 依赖门禁 收敛 苹果 章节\n")
51
+
52
+
53
+ def check(name, cond, detail=""):
54
+ global PASS, FAIL
55
+ if cond:
56
+ PASS += 1
57
+ print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
58
+ else:
59
+ FAIL += 1
60
+ FAILS.append(name)
61
+ print(f" [FAIL] {name} · {detail}")
62
+
63
+
64
+ def _principal():
65
+ return Principal(actor="n276", clearance="secret", can_write=True,
66
+ role="designer", auth_mode="test")
67
+
68
+
69
+ def _lib(n=12):
70
+ root = tempfile.mkdtemp(prefix="n276_")
71
+ cg = MdCGSecure(root, principal=_principal())
72
+ for i in range(n):
73
+ cg.add("k_%02d" % i, BODY % (i, i), layer="knowledge")
74
+ cg.flush()
75
+ return root, cg
76
+
77
+
78
+ def _spy(cg):
79
+ """spy `cg._maybe_reload_index`(实例属性;两类 get 与环外探针都经它)。"""
80
+ calls = {"n": 0}
81
+ orig = cg._maybe_reload_index
82
+
83
+ def s():
84
+ calls["n"] += 1
85
+ return orig()
86
+ cg._maybe_reload_index = s
87
+ return calls, orig
88
+
89
+
90
+ def main():
91
+ root, cg = _lib()
92
+ calls, orig_probe = _spy(cg)
93
+ try:
94
+ # ---------- C1 redundancy 环内零重探 ----------
95
+ print("== C1 forgetting.redundancy 环内零重探 ==")
96
+ calls["n"] = 0
97
+ r1 = forgetting.redundancy(cg, CONTENT, layer="knowledge")
98
+ n1 = calls["n"]
99
+ check("C1a redundancy 探针 == 1(修前 == min(同层节点, 240))",
100
+ n1 == 1, f"calls={n1}")
101
+ check("C1b redundancy 正常产出(compared>0)",
102
+ r1.get("compared", 0) > 0, str(r1))
103
+
104
+ # ---------- C4 自证腿:旧签名逐次探针 ----------
105
+ print("== C4 自证腿(旧签名照旧逐次探针)==")
106
+ ids = sorted(cg.index["nodes"].keys())
107
+ calls["n"] = 0
108
+ for nid in ids:
109
+ cg.get(nid)
110
+ n4 = calls["n"]
111
+ check("C4a 旧签名 get(nid) 逐次探针(spy 有判别力)",
112
+ n4 == len(ids), f"calls={n4} ids={len(ids)}")
113
+
114
+ # ---------- C2 check 精比环零重探 ----------
115
+ print("== C2 consistency.check 精比环零重探 ==")
116
+ calls["n"] = 0
117
+ rc = consistency.check(cg, CONTENT, layer="knowledge",
118
+ log_write=False)
119
+ n2 = calls["n"]
120
+ check("C2a check 探针 == 1(修前 == len(scan_face))",
121
+ n2 == 1, f"calls={n2} kept={rc.get('kept')}")
122
+ check("C2b check 正常产出(verdict 在场)",
123
+ isinstance(rc.get("verdict"), str), str(rc.get("verdict")))
124
+
125
+ # ---------- C3 probe=False 与默认逐位一致 ----------
126
+ print("== C3 get(probe=False) 与 get() 逐位一致 ==")
127
+ a = cg.get("k_00")
128
+ b = cg.get("k_00", probe=False)
129
+ check("C3a 两形态返回等值(id/frontmatter/content/path)",
130
+ a is not None and b is not None and a == b)
131
+
132
+ # ---------- C5 可见性不丢(他实例写入) ----------
133
+ print("== C5 他实例写入后单探 + probe=False 可见 ==")
134
+ cgB = MdCGSecure(root, principal=_principal())
135
+ cgB.add("k_new", BODY % ("new", "new"), layer="knowledge")
136
+ cgB.flush()
137
+ cgB.close()
138
+ calls["n"] = 0
139
+ cg._maybe_reload_index() # 循环外单探(生产探针位)
140
+ probe_calls_before_get = calls["n"]
141
+ node = cg.get("k_new", probe=False)
142
+ check("C5a 单探后 probe=False 读到新节点(不陈旧)",
143
+ bool(node) and "new" in (node.get("content") or ""),
144
+ f"node={'yes' if node else 'None'}")
145
+ check("C5b 该组合探针恰好 1 次(外面那次)",
146
+ probe_calls_before_get == 1 and calls["n"] == 1,
147
+ f"calls={calls['n']}")
148
+
149
+ # ---------- C6 无探活面载体回落 ----------
150
+ print("== C6 测试桩回落原 cg.get(nid) ==")
151
+ class _Stub:
152
+ def __init__(self):
153
+ self.index = {"nodes": {"s1": {"path": "knowledge/s1.md",
154
+ "layer": "knowledge"}}}
155
+
156
+ def get(self, nid):
157
+ return {"id": nid, "frontmatter": {},
158
+ "content": "占位 苹果 章节", "path": "knowledge/s1.md"}
159
+ r6 = forgetting.redundancy(_Stub(), CONTENT, layer="knowledge")
160
+ check("C6a 无 _maybe_reload_index 的载体不崩且正常比对",
161
+ r6.get("compared") == 1, str(r6))
162
+
163
+ # ---------- C7 两形态等价 ----------
164
+ print("== C7 单探形态 vs 逐节点探针形态逐位一致 ==")
165
+ r_prod = forgetting.redundancy(cg, CONTENT, layer="knowledge")
166
+ rc_prod = consistency.check(cg, CONTENT, layer="knowledge",
167
+ log_write=False)
168
+ orig_get = cg.get
169
+
170
+ def _always_probe(nid, probe=True):
171
+ return orig_get(nid, probe=True) # 强制回到逐节点探针
172
+ cg.get = _always_probe
173
+ try:
174
+ r_full = forgetting.redundancy(cg, CONTENT, layer="knowledge")
175
+ rc_full = consistency.check(cg, CONTENT, layer="knowledge",
176
+ log_write=False)
177
+ finally:
178
+ cg.get = orig_get
179
+ check("C7a redundancy 两形态逐位一致(max/with/jaccard/compared)",
180
+ r_prod == r_full, f"prod={r_prod} full={r_full}")
181
+ _strip = lambda d: {k: v for k, v in d.items() if k != "t"} # noqa: E731
182
+ check("C7b check 两形态逐位一致(剔动态键 t)",
183
+ _strip(rc_prod) == _strip(rc_full),
184
+ f"prod={_strip(rc_prod).get('verdict')}/{_strip(rc_prod).get('kept')}"
185
+ f" full={_strip(rc_full).get('verdict')}/{_strip(rc_full).get('kept')}")
186
+ finally:
187
+ cg._maybe_reload_index = orig_probe
188
+ cg.close()
189
+ shutil.rmtree(root, ignore_errors=True)
190
+
191
+ print(f"\ntest_n276_probe_hoist: {PASS} 通过 / {FAIL} 失败")
192
+ if FAILS:
193
+ for f in FAILS:
194
+ print(f" - {f}")
195
+ return 0 if FAIL == 0 else 1
196
+
197
+
198
+ if __name__ == "__main__":
199
+ sys.exit(main())
@@ -285,8 +285,14 @@ def g5_same_multiplier_slot():
285
285
  0 < kd - jd <= 12
286
286
  and all(l.strip() for l in seg[jd + 1:kd]),
287
287
  f"pooling 行={jd} 乘子行={kd}")
288
+ # N274:乘子行改为「调用跨两行」形态(循环外提升后追加
289
+ # now/gamma/enabled 关键字透传)——判据性质不变(仍要求乘子**直接
290
+ # 乘在 raw 上**、同一乘子位、以 [0] 取乘子),仅匹配形态同步为
291
+ # 跨行;并顺带收紧为「行首即 raw = raw * …」。
288
292
  check("乘子直接乘在 raw 上(同一乘子位,不新开乘子链)",
289
- "raw * freshness.entry_weight(e, fm=fm)[0]" in seg[kd])
293
+ seg[kd].strip().startswith("raw = raw * freshness.entry_weight(")
294
+ and "[0]" in (seg[kd] + seg[kd + 1]), # 单行/跨行两形态皆可
295
+ f"kd 行={seg[kd]!r} 次行={seg[kd + 1]!r}")
290
296
  # 行为:同内容两节点,仅 created_at 不同 → 分数比 == 乘子比
291
297
  scored = _score_pair(cg)
292
298
  m_new = F.entry_weight(cg.index["nodes"]["k_new"], now=REF)[0]
@@ -577,6 +583,93 @@ def g12_rust_parity_e2e():
577
583
  shutil.rmtree(root, ignore_errors=True)
578
584
 
579
585
 
586
+ def g13_hot_loop_reads():
587
+ """N274:freshness 读取点全部提升到 `_score` 循环外(调用计数判据)。
588
+
589
+ 判据=**与文档数无关**:同一 `_score` 调用于 2 节点与 20 节点两批文档
590
+ 时,`enabled`/`freshness_now`/`resolve_gamma`/`temporal_gamma` 的调用
591
+ 计数相同。修前(8000 池实测,2026-10-05)为每节点 7 次(enabled 4×N
592
+ + now/γ/temporal 各 N);墙钟只作辅助,不作判据。
593
+ """
594
+ print("== G13 freshness 读取提升到 _score 循环外(N274)==")
595
+ import md_cg.mdcos as MC
596
+ root, cg = _lib()
597
+ c = {"enabled": 0, "now": 0, "gamma": 0, "temporal": 0}
598
+ orig = {n: getattr(F, n) for n in ("enabled", "freshness_now",
599
+ "resolve_gamma")}
600
+ orig_tg = MC.temporal_gamma
601
+
602
+ def _wrap(fn, key):
603
+ def _w(*a, **k):
604
+ c[key] += 1
605
+ return fn(*a, **k)
606
+ return _w
607
+
608
+ F.enabled = _wrap(orig["enabled"], "enabled")
609
+ F.freshness_now = _wrap(orig["freshness_now"], "now")
610
+ F.resolve_gamma = _wrap(orig["resolve_gamma"], "gamma")
611
+ MC.temporal_gamma = _wrap(orig_tg, "temporal")
612
+ try:
613
+ def _mk_docs(n):
614
+ return [({"path": "knowledge/k%d.md" % i},
615
+ {"id": "k%d" % i, "tags": [],
616
+ "created_at": REF - i * DAY, "access_count": 0,
617
+ "last_access": 0, "importance": 0.5},
618
+ BODY.format(n="k%d" % i, t="k%d 正文 苹果 章节" % i))
619
+ for i in range(n)]
620
+
621
+ def _count(docs):
622
+ for k in c:
623
+ c[k] = 0
624
+ cg._score(docs, "苹果 章节",
625
+ M.bigrams(M.normalize_en("苹果 章节")))
626
+ return dict(c)
627
+
628
+ c2 = _count(_mk_docs(2))
629
+ c20 = _count(_mk_docs(20))
630
+ # 判据面=**env 读取三面**(enabled / freshness_now / temporal_gamma
631
+ # ——后者是 γ 的实际读取点:resolve_gamma 的 early-return 分支不碰
632
+ # temporal_gamma,故「temporal_gamma 常数」即「γ 的 env 读取归零」)。
633
+ # 诚实边界:resolve_gamma 的**函数调用**仍每节点一次(early-return
634
+ # 直通已解析的 float,~0.15µs/节点、不读 env)——保留它是因为
635
+ # resolve_gamma 还承担「非法 gamma 回落缺省」的单点边界语义,跳过
636
+ # 调用会改变该边界。
637
+ check("env 读取面与文档数无关(enabled/now/temporal;修前各 N×)",
638
+ {k: c2[k] for k in ("enabled", "now", "temporal")}
639
+ == {k: c20[k] for k in ("enabled", "now", "temporal")},
640
+ f"c2={c2} c20={c20}")
641
+ check("enabled() 常数级(修前 = 4×N)", c20["enabled"] <= 2, str(c20))
642
+ check("freshness_now() 常数级(修前 = 1×N)", c20["now"] <= 1, str(c20))
643
+ check("temporal_gamma() 常数级(修前 = 1×N;γ 的实际读取点)",
644
+ c20["temporal"] <= 1, str(c20))
645
+ # 自证腿(常驻):全缺省签名的旧路径仍自算,四个 spy 数得到——证明
646
+ # 上面的「零/常数」不是 spy 失效造成的假绿(判别力在位)。
647
+ for k in c:
648
+ c[k] = 0
649
+ F.entry_weight({"created_at": REF, "importance": 0.5})
650
+ check("自证腿:全缺省签名仍自算(四路 spy 各 ≥1,判别力在位)",
651
+ c["enabled"] >= 1 and c["now"] >= 1 and c["gamma"] >= 1
652
+ and c["temporal"] >= 1, str(c))
653
+ # 语义等价:显式透传与自算逐位同值;关闭恒 1.0(三面同口径)
654
+ e0 = {"created_at": REF - 30 * DAY, "importance": 0.5}
655
+ check("显式 enabled+now+γ 透传与自算同值(逐位一致)",
656
+ F.entry_weight(e0, now=REF)[0]
657
+ == F.entry_weight(e0, now=REF, gamma=F.resolve_gamma(None),
658
+ enabled=True)[0])
659
+ check("enabled=False 恒 1.0(entry/decay/refresh 三面同口径)",
660
+ F.entry_weight(e0, now=REF, enabled=False)[0] == 1.0
661
+ and F.decay_factor(0.023, 100.0, enabled=False) == 1.0
662
+ and F.refresh_factor(0, 0, 0.9, enabled=False) == 1.0)
663
+ finally:
664
+ F.enabled = orig["enabled"]
665
+ F.freshness_now = orig["freshness_now"]
666
+ F.resolve_gamma = orig["resolve_gamma"]
667
+ MC.temporal_gamma = orig_tg
668
+ cg.close()
669
+ import shutil
670
+ shutil.rmtree(root, ignore_errors=True)
671
+
672
+
580
673
  def _mut_floor_zero():
581
674
  return _patch(F, "SCORE_FLOOR", 0.0)
582
675
 
@@ -641,6 +734,7 @@ def _collect():
641
734
  g10_weights_autoprotect_kept()
642
735
  g11_rust_parity()
643
736
  g12_rust_parity_e2e()
737
+ g13_hot_loop_reads()
644
738
  return list(FAILS)
645
739
 
646
740
 
@@ -109,9 +109,11 @@ _BASELINE_MUTATIONS = {
109
109
  # autonomy_modes(2026-10-02 补,三档自治批次②):writepipe.py 新增链尾档位闸
110
110
  # `_gate_autonomy`,其函数内 `from . import autonomy_modes as _am` 被本文件的
111
111
  # AST 相对-import 扫描命中——不登记则假包缺模块(SHIM-MISS 红,容器栈一实测)。
112
+ # ghostref(2026-10-05 补,A2 幽灵引用批次):writepipe.py 新增 `_gate_ghostref`,
113
+ # 其函数内 `from . import ghostref as _ghostref` 同款被扫描命中——同规则登记。
112
114
  _SHIMS = ("twophase", "trust", "linkref", "mcp_server", "mdcg", "forgetting",
113
115
  "units", "coldverify", "hotcache", "hyperedge", "nodefile",
114
- "autonomy_modes")
116
+ "autonomy_modes", "ghostref")
115
117
  _SHIM_TPL = ("import md_cg.%s as _m\n"
116
118
  "globals().update({k: v for k, v in vars(_m).items()\n"
117
119
  " if not k.startswith('__')})\n")
@@ -77,6 +77,28 @@ def count_opens(fn):
77
77
  builtins.open = real
78
78
 
79
79
 
80
+ def count_pb(fn):
81
+ """打桩 md_cg.nodefile.positive_body 计数(N273 召回键派生物调用面)。
82
+
83
+ 计数面覆盖直调与钩子内直调(`MdCG._positive_body` 默认实现即调它)——
84
+ readcache 缓存版命中时该函数零调用,「未缓存实现」下每候选一次。
85
+ """
86
+ import md_cg.nodefile as _nf
87
+ n = 0
88
+ real = _nf.positive_body
89
+
90
+ def _spy(content):
91
+ nonlocal n
92
+ n += 1
93
+ return real(content)
94
+
95
+ _nf.positive_body = _spy
96
+ try:
97
+ return fn(), n
98
+ finally:
99
+ _nf.positive_body = real
100
+
101
+
80
102
  def main():
81
103
  root = tempfile.mkdtemp(prefix="mdcg_readcache_")
82
104
  os.environ.pop("MDCG_READ_CACHE", None)
@@ -148,9 +170,53 @@ def main():
148
170
  check("P6a 热缓存后二次检索派生物零重算(identity 不变)",
149
171
  same and len(snap) > 0, f"snap={len(snap)}")
150
172
 
173
+ # P8 `_positive_body` 派生物缓存(N273,`_like` 召回键面)——与 P6
174
+ # 同形判定:调用计数为主判据(修前实现每候选一次、8000 池实测
175
+ # 8000 次/查询),identity 快照为辅。
176
+ print("== P8 召回键派生物缓存(_positive_body,N273)==")
177
+ pbc = getattr(cg, "_positive_body_cache", {})
178
+ snap_pb = {k: v[1] for k, v in pbc.items()}
179
+ check("P8pre 召回键派生物缓存已装载", len(snap_pb) > 0,
180
+ f"entries={len(snap_pb)}")
181
+ (_res8, n_pb), = (count_pb(lambda: cg.search("蜂群调度")),)
182
+ check("P8a 热态二次检索 _positive_body 零重调(修前 = 每候选一次)",
183
+ n_pb == 0, f"calls={n_pb}")
184
+ pbc2 = getattr(cg, "_positive_body_cache", {})
185
+ same_pb = all(pbc2[k][1] is snap_pb[k] for k in snap_pb if k in pbc2)
186
+ check("P8b 热态二次检索派生物零重算(identity 不变)",
187
+ same_pb and len(snap_pb) > 0, f"snap={len(snap_pb)}")
188
+ # 写 1 节点 → 脏集精确失效:仅新 path 重算(旧条目零重算)
189
+ cg.add("pb_new", "# 功能名:召回键样本\n# 正文:蜂群调度 新增节点 独有标记PB\n"
190
+ "# 不适用条件:仅当 排除场景PB 时", layer="knowledge")
191
+ cg.flush()
192
+ (_res8c, n_pb8), = (count_pb(lambda: cg.search("蜂群调度")),)
193
+ check("P8c 写 1 节点后仅该节点重算(脏集精确失效)", n_pb8 == 1,
194
+ f"calls={n_pb8}")
195
+ check("P8c2 写后新内容可检索(不陈旧)",
196
+ any(x[0].get("id") == "pb_new" for x in _res8c[0]),
197
+ f"ids={[x[0].get('id') for x in _res8c[0]][:5]}")
198
+ # P8d 红基线自证腿:摘回**未缓存**实现(类上原方法)跑同一查询,
199
+ # 调用计数必须 >0——证明 P8a 的「零重调」判据对「无缓存层」形态有
200
+ # 判别力(不依赖 git 回退的常驻自证)。
201
+ from md_cg.mdcg import MdCG as _MdCG
202
+ _saved_pb = cg._positive_body
203
+ cg._positive_body = _MdCG.__dict__["_positive_body"].__get__(
204
+ cg, type(cg))
205
+ try:
206
+ (_res8d, n_pb_uncached), = (count_pb(lambda: cg.search("蜂群调度")),)
207
+ finally:
208
+ cg._positive_body = _saved_pb
209
+ check("P8d 红基线自证:未缓存实现下同查询调用计数 >0",
210
+ n_pb_uncached > 0, f"calls={n_pb_uncached}")
211
+ check("P8e 恢复缓存版后再次零重调",
212
+ count_pb(lambda: cg.search("蜂群调度"))[1] == 0,
213
+ "缓存版未复原")
214
+
151
215
  # P5 clear 兜底(破坏性,放最后)
152
216
  n = rc.clear(cg)
153
217
  check("P5 clear 返回清空条目数", n > 0, f"cleared={n}")
218
+ check("P5b clear 一并清空召回键派生物缓存",
219
+ len(getattr(cg, "_positive_body_cache", {})) == 0)
154
220
 
155
221
  # P7 写代际单调(批次 23,issue #31 D-4 / v20 复现场景,能红 len 代际)
156
222
  print("== P7 flush 后同量写入不回退(D-4 陈旧读)==")
@@ -11,6 +11,10 @@
11
11
  `state_root` 侧而非数据根;**绝不在数据根内建 `.git`**;
12
12
  ④ 回滚**只提供 `revert`**(不提供抹历史的强推档)。
13
13
 
14
+ ⚠ **口径更新(2026-10-06 · issue #60)**:① 的「逐字节不变」是**物化窗口内的读数**
15
+ (`materialize.face_stable`),**不再是「跳过迭代」的闸**——窗口内漂移由 ③ 对账段
16
+ 的并发闸按「主库优先」逐项裁决;`materialize.ok` 只说「影子挂上了没」(见 G8)。
17
+
14
18
  断言分组:
15
19
 
16
20
  G0 隔离与落点——沙箱四根全落守卫临时目录;锁文件在 `state_root` 侧、**不在数据根**;
@@ -32,6 +36,9 @@
32
36
  `busy=True` 且不取得锁;父进程释放后同一子进程脚本转 `ok=True`。
33
37
  G7 判据自身有判别力(正对照)——改动一个 .md 后 `face_delta` 必须报 changed;
34
38
  给根里加一个非白名单 .md 之外的件不改变判据(负对照)。
39
+ G8 物化窗口内漂移(issue #60)——`ok` = **物化动作成功**(挂上影子即 True),
40
+ `face_stable` = 真源面窗口内逐字节未变(原 ok 的真值);漂移时
41
+ `source_face_delta.added` 照旧逐项报出,`baseline_face` 照旧记录。
35
42
 
36
43
  运行:python -X utf8 -m md_cg.test_sleep
37
44
  python -X utf8 -m md_cg.test_sleep --mutate # 定点变异自证
@@ -485,7 +492,57 @@ def g7():
485
492
  "G7c 正对照:删除 .md 被报为 removed", d2)
486
493
 
487
494
 
488
- _GROUPS = (g0, g1, g2, g3, g4, g5, g6, g7)
495
+ # ---------------------------------------------------------------- G8
496
+ def g8():
497
+ print("== G8 物化窗口内漂移:ok=物化动作成功、face_stable=真源面未变(issue #60)==")
498
+ S = globals()["SL"]
499
+ gen, root, state = _fixture()
500
+ G = os.path.join(state, "sleep", "lib.git")
501
+ SH = os.path.join(state, "sleep", "shadow")
502
+ # 补丁要打在**函数实际读取的命名空间**上(`materialize.__globals__`),而不是
503
+ # 模块属性:变异自证时 `SL` 是 exec 出来的模块,其函数读 exec 命名空间,而模块
504
+ # 对象的 `__dict__` 只是它的副本——打在模块属性上,变异后的模块内部调用看不到
505
+ # 补丁(本机实测:该断言会在变异下空转,反而误红)。真源码下两者是同一个 dict。
506
+ ns = S.materialize.__globals__
507
+ real = ns["source_face_hashes"]
508
+ seen = {"n": 0}
509
+
510
+ def _drift(r):
511
+ """只在物化窗口的**第二次**取样(= after)注入一条新增路径。"""
512
+ seen["n"] += 1
513
+ d = dict(real(r))
514
+ if seen["n"] == 2:
515
+ d["knowledge/n_drift.md"] = "0" * 64
516
+ return d
517
+
518
+ try:
519
+ ns["source_face_hashes"] = _drift
520
+ m = S.materialize(root=root, git_dir_path=G, shadow=SH, ts="G8T1")
521
+ finally:
522
+ ns["source_face_hashes"] = real
523
+ ok(m.get("ok") is True and m.get("busy") is False,
524
+ "G8a **物化成功即 ok=True**(不再因窗口内真源面漂移自我否决)",
525
+ {k: m.get(k) for k in ("ok", "busy", "error", "hint")})
526
+ ok(m.get("face_stable") is False,
527
+ "G8b face_stable=False(窗口内确有漂移——原 ok 的真值迁到该读数)",
528
+ m.get("face_stable"))
529
+ ok(m["source_face_delta"]["added"] == ["knowledge/n_drift.md"]
530
+ and not m["source_face_delta"]["removed"]
531
+ and not m["source_face_delta"]["changed"],
532
+ "G8c source_face_delta.added 逐项报出漂移路径(原读数保留)",
533
+ m["source_face_delta"])
534
+ ok(bool(m.get("baseline_face")) and os.path.isdir(SH)
535
+ and os.path.exists(os.path.join(SH, ".git")),
536
+ "G8d 漂移不阻断挂影子:baseline_face 已记录、影子在位",
537
+ (bool(m.get("baseline_face")), SH))
538
+ # 负对照:窗口内无漂移 ⇒ face_stable=True(该读数不是恒假)
539
+ m2 = S.materialize(root=root, git_dir_path=G, shadow=SH, ts="G8T2")
540
+ ok(m2.get("ok") is True and m2.get("face_stable") is True,
541
+ "G8e 负对照:窗口内无漂移 ⇒ face_stable=True",
542
+ {k: m2.get(k) for k in ("ok", "face_stable", "busy")})
543
+
544
+
545
+ _GROUPS = (g0, g1, g2, g3, g4, g5, g6, g7, g8)
489
546
 
490
547
 
491
548
  def _run_groups() -> int: