@furongjun1999/dsh-memory 0.4.7 → 0.4.8

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 (172) hide show
  1. package/README.md +92 -37
  2. package/codebuddy/CODEBUDDY.md +195 -185
  3. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
  4. package/docs/README.md +111 -0
  5. package/docs/discipline/harnesses.yaml +215 -152
  6. package/docs/discipline/templates/full.md.tmpl +60 -58
  7. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
  8. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
  9. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
  10. package/docs/experiments/m4-role-probe/census.py +86 -0
  11. package/docs/experiments/m4-role-probe/probe.py +89 -0
  12. package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
  13. package/docs/experiments/mapped_confidence/post_check.py +75 -0
  14. package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
  15. package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
  16. package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
  17. package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +9 -9
  18. package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
  19. 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 +261 -0
  20. package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
  21. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.4.md +148 -0
  22. package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
  23. package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
  24. package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
  25. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
  26. 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 +4 -4
  27. package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
  28. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
  29. package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
  30. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
  31. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
  32. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -0
  33. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
  34. package/docs/{ → theory/}/347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
  35. package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
  36. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -0
  37. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
  38. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
  39. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -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 +34 -6
  41. package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
  42. package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
  43. package/dsh/README.md +1 -1
  44. package/lib/hooks.d.ts +5 -0
  45. package/lib/hooks.js +10 -1
  46. package/lib/lib/prompt_safety.d.ts +48 -0
  47. package/lib/lib/prompt_safety.js +62 -0
  48. package/md_cg/audit.py +76 -2
  49. package/md_cg/bench_membench.py +10 -5
  50. package/md_cg/bench_progressive.py +48 -9
  51. package/md_cg/branches.py +275 -0
  52. package/md_cg/ccgc.py +884 -0
  53. package/md_cg/conformance.py +662 -0
  54. package/md_cg/consistency.py +1 -1
  55. package/md_cg/corpus.py +1 -1
  56. package/md_cg/eval_common.py +6 -5
  57. package/md_cg/forgetting.py +11 -3
  58. package/md_cg/fsutil.py +63 -1
  59. package/md_cg/identity.py +2 -2
  60. package/md_cg/insight.py +2 -2
  61. package/md_cg/lifecycle.py +262 -0
  62. package/md_cg/mcp_server.py +547 -140
  63. package/md_cg/mdcg.py +206 -18
  64. package/md_cg/mdcos.py +512 -50
  65. package/md_cg/metacognition.py +2 -2
  66. package/md_cg/mreview/__init__.py +25 -0
  67. package/md_cg/mreview/__main__.py +107 -0
  68. package/md_cg/mreview/bundle.py +170 -0
  69. package/md_cg/mreview/candidates.py +253 -0
  70. package/md_cg/mreview/govern.py +674 -0
  71. package/md_cg/mreview/locate.py +905 -0
  72. package/md_cg/mreview/pipeline.py +701 -0
  73. package/md_cg/mreview/rules/duplication.json +21 -0
  74. package/md_cg/mreview/rules/field_coverage.json +54 -0
  75. package/md_cg/mreview/rules/source_license.json +21 -0
  76. package/md_cg/mreview/rules/template_flow.json +21 -0
  77. package/md_cg/mreview/ruleset.py +238 -0
  78. package/md_cg/nodefile.py +38 -0
  79. package/md_cg/predict.py +24 -6
  80. package/md_cg/protect.py +4 -4
  81. package/md_cg/refindex.py +41 -2
  82. package/md_cg/scrub.py +5 -1
  83. package/md_cg/self_state.py +1 -1
  84. package/md_cg/semantic/en_normalizer.py +85 -25
  85. package/md_cg/sustain.py +18 -0
  86. package/md_cg/tasks.py +447 -0
  87. package/md_cg/test_action_derive.py +203 -0
  88. package/md_cg/test_audit_rotate.py +270 -0
  89. package/md_cg/test_branches.py +249 -0
  90. package/md_cg/test_ccg_perturb.py +1 -1
  91. package/md_cg/test_ccgc.py +423 -0
  92. package/md_cg/test_conformance.py +343 -0
  93. package/md_cg/test_en_pipeline.py +24 -0
  94. package/md_cg/test_health_scale.py +173 -0
  95. package/md_cg/test_identity_attribution.py +6 -0
  96. package/md_cg/test_index_durability.py +224 -0
  97. package/md_cg/test_lifecycle.py +309 -0
  98. package/md_cg/test_mr_m1.py +679 -0
  99. package/md_cg/test_mr_m2.py +587 -0
  100. package/md_cg/test_mr_m3.py +703 -0
  101. package/md_cg/test_mr_m4.py +485 -0
  102. package/md_cg/test_p1.py +1 -1
  103. package/md_cg/test_p11_consistency.py +1 -1
  104. package/md_cg/test_p12_metacognition.py +2 -2
  105. package/md_cg/test_p16_self_state.py +1 -1
  106. package/md_cg/test_p26_refindex.py +1 -1
  107. package/md_cg/test_p27_docindex.py +22 -2
  108. package/md_cg/test_p28_refcheck.py +1 -1
  109. package/md_cg/test_p29_session_ingest_export.py +1 -1
  110. package/md_cg/test_p2_mcp.py +7 -1
  111. package/md_cg/test_p38_contextualize.py +1 -1
  112. package/md_cg/test_p39_vision_evidence.py +1 -1
  113. package/md_cg/test_p40_refine_worklist.py +1 -1
  114. package/md_cg/test_p41_evolve_patrol.py +1 -1
  115. package/md_cg/test_p42_provenance.py +1 -1
  116. package/md_cg/test_p43_pooling.py +41 -13
  117. package/md_cg/test_read_clip.py +137 -0
  118. package/md_cg/test_review_conformance.py +310 -0
  119. package/md_cg/test_tasks.py +409 -0
  120. package/md_cg/test_tool_face.py +189 -0
  121. package/md_cg/test_twophase.py +286 -0
  122. package/md_cg/test_writepipe.py +210 -0
  123. package/md_cg/theory.py +1 -1
  124. package/md_cg/tokens.py +95 -5
  125. package/md_cg/tool_face.py +250 -0
  126. package/md_cg/twophase.py +221 -0
  127. package/md_cg/units.py +546 -0
  128. package/md_cg/vision_evidence.py +1 -1
  129. package/md_cg/weights.py +1 -1
  130. package/md_cg/whitebox.py +1 -1
  131. package/md_cg/whitebox_kb/data/verify_cache.json +28 -0
  132. package/md_cg/whitebox_kb/data/verify_savings.jsonl +46 -0
  133. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  134. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  135. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
  136. package/md_cg/writelimit.py +19 -3
  137. package/md_cg/writepipe.py +372 -0
  138. package/package.json +1 -1
  139. package/src/hooks.ts +10 -1
  140. package/src/lib/prompt_safety.ts +62 -0
  141. package/zcode/AGENTS.md +195 -185
  142. package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
  143. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
  144. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
  145. /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
  146. /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
  147. /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
  148. /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
  149. /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
  150. /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
  151. /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
  152. /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
  153. /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
  154. /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
  155. /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
  156. /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
  157. /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
  158. /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
  159. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
  160. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
  161. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
  162. /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
  163. /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
  164. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
  165. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
  166. /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
  167. /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
  168. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
  169. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
  170. /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
  171. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
  172. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
package/md_cg/mdcg.py CHANGED
@@ -20,10 +20,13 @@ import os
20
20
  import re
21
21
  import json
22
22
  import time
23
+ import atexit
24
+ import weakref
23
25
  import hashlib
24
26
  import threading
25
27
 
26
- from . import nodefile, protect, routing, subgraph, chain, provenance, pooling
28
+ from . import (nodefile, protect, routing, subgraph, chain, provenance, pooling,
29
+ lifecycle)
27
30
  from .fsutil import (FileLock, ShardedLog, atomic_write, append_jsonl,
28
31
  read_jsonl, sweep_stale_temps)
29
32
 
@@ -57,6 +60,69 @@ DEFAULT_RECENT_WINDOW = 200
57
60
  # 全量回退的读取上限,对齐 sqlite 版 `ORDER BY importance DESC, created_at DESC LIMIT 500`
58
61
  GLOBAL_CAP = 500
59
62
 
63
+ # ---------- 索引持久化 · 进程退出兜底(2026-09-16 取证) ----------
64
+ # 脏索引(`_dirty`)靠调用方显式 flush()/close() 落分片日志。一次性脚本/CLI
65
+ # (review_cli、backfill、migrate_* 等)写入 1~2 条后直接退出,未达 autoflush(64)
66
+ # 阈值 → 日志无记录;而**已有 `_index.json` 的根重开时不重扫目录**,于是节点
67
+ # 「在盘上但索引无条目」:其它进程与重载后的长驻进程都检索不到,只能靠某次全量
68
+ # rebuild_index() 偶然救回(首例=审核裁决通路,见 mdcos.review_decide)。
69
+ # 兜底语义(刻意收窄):**正常进程退出时**对所有存活实例补一次 close()——
70
+ # 幂等(`_dirty` 为空即 no-op)、不改变正常路径行为、不替代显式收尾
71
+ # (异常退出 / 被 kill 仍须调用方自己保证)。WeakSet 不阻止实例回收。
72
+ _LIVE_CGS = weakref.WeakSet()
73
+ _ATEXIT_HOOK = None
74
+
75
+
76
+ def _atexit_flush_all():
77
+ """进程退出兜底:把仍存活实例的脏索引落盘(异常吞掉——退出路径不该再抛)。"""
78
+ for cg in list(_LIVE_CGS):
79
+ try:
80
+ cg.close()
81
+ except Exception: # noqa: BLE001
82
+ pass
83
+
84
+
85
+ def _register_atexit_hook():
86
+ global _ATEXIT_HOOK
87
+ if _ATEXIT_HOOK is None:
88
+ atexit.register(_atexit_flush_all)
89
+ _ATEXIT_HOOK = _atexit_flush_all
90
+ return _ATEXIT_HOOK
91
+
92
+
93
+ def cut_by_relevance(docs, scored, total, pools=None, key_of=None, stat=None):
94
+ """候选截断:**先按相关度排序,再截断到 `total`**(截断依据=相关度)。
95
+
96
+ 动机(真库实测 2026-09-16,11521 节点 / cap=500):原实现是「LIKE 命中集
97
+ 沿 `entries`(目录枚举序)取前 `total` 条 → 再打分」——命中集超上限时,
98
+ 等价于用**写入顺序抽签**决定谁进候选池。实测 40 个抽样查询中 27 个命中数
99
+ 超 cap(最差 10963/11073 ≈ 99%),被现状丢弃的候选平均分 0.650、被保留的
100
+ 平均分 0.340,gold 进 top10 由 19/40 降至 29/40。
101
+
102
+ 本函数把 cap 语义从「候选生成上限」降级为「输出上限」:候选质量责任交还
103
+ 排序(与 `mdcos._lexical` 同源先例一致),**cap 值本身不变**。
104
+
105
+ `docs` 与 `scored` 必须同序同长(`MdCG._score` 输出序=输入序)。不一致时
106
+ 不猜:退回原序截断,并在 `stat["cut_order"]` 标 `insert_fallback` 供审计。
107
+ 分池生效时在**分数序**内按池额度截断(池内原序=分数序)。
108
+ 返回 `(picked, report)`。
109
+ """
110
+ if len(scored) != len(docs):
111
+ if stat is not None:
112
+ stat["cut_order"] = "insert_fallback"
113
+ return pooling.cut_report(docs, total, pools=pools,
114
+ key_of=key_of or pooling.doc_key)
115
+ order = sorted(
116
+ range(len(docs)),
117
+ key=lambda i: (-float(scored[i][1]),
118
+ -float(scored[i][0]["frontmatter"].get("importance") or 0),
119
+ -float(scored[i][0]["frontmatter"].get("created_at") or 0)))
120
+ ranked = [docs[i] for i in order]
121
+ if stat is not None:
122
+ stat["cut_order"] = "relevance"
123
+ return pooling.cut_report(ranked, total, pools=pools,
124
+ key_of=key_of or pooling.doc_key)
125
+
60
126
  # 条件论「观测时间」栏的默认观测窗口(秒):调用方未提供 time_window 时,
61
127
  # 以写入时刻为锚开一个 1 小时窗口(与 AEIS 既有约定一致)。
62
128
  OBSERVATION_WINDOW_SEC = 3600.0
@@ -350,8 +416,10 @@ def normalize_en(text: str) -> str:
350
416
  # **评测侧**改用 eval_common.use_jaccard() 显式注入——长 turn 语料上
351
417
  # precise 0%→8.97% / temporal 0%→6.77% / interference 1.28%→20.51% /
352
418
  # reference 0%→3.76%(与 Rust mdcg-eval --score jaccard 逐位一致)。
353
- # 主库不切:短条目语料上 jaccard 会退化(test_p17_predict G4——可预测锚点
354
- # 由因果起点 a 错配到语义邻居 x),收益随文档长度单调增长。
419
+ # 主库不切:收益随文档长度单调增长,短条目语料上无增益(该语料本无长度偏置)。
420
+ # 另注:test_p17_predict G4 曾出现的「可预测锚点由因果起点 a 错配到语义邻居 x」
421
+ # 已由 predict.anchor_from_description 的**可起推资格**修复(2026-09-16,出边
422
+ # 非空优先),锚点漂移不再是切换打分口径的理由;口径切换仍按上列数据收益决定。
355
423
  SCORE_MODES = ("legacy", "jaccard")
356
424
  SCORE_MODE = os.environ.get("MDCG_SCORE_MODE") or "legacy"
357
425
 
@@ -493,6 +561,9 @@ class MdCG:
493
561
  self._log = None
494
562
  self.index = self._load_index()
495
563
  sweep_stale_temps(self.root)
564
+ # 进程退出兜底登记(见模块级 _LIVE_CGS):一次性脚本漏收尾时索引仍能落盘。
565
+ _LIVE_CGS.add(self)
566
+ _register_atexit_hook()
496
567
 
497
568
  # ---------- 索引(派生物,可重建) ----------
498
569
 
@@ -510,7 +581,13 @@ class MdCG:
510
581
  idx = {"schema": SCHEMA, "nodes": self._scan_nodes(), "buckets": {}}
511
582
  for rec in ShardedLog.read_all(self.index_log_dir):
512
583
  nid, e = rec.get("id"), rec.get("e")
513
- if nid and e:
584
+ if not nid:
585
+ continue
586
+ if e is None:
587
+ # 删除记录(tombstone):删除必须能重放,否则已删节点会在下次
588
+ # 启动时从旧记录里复活成**幽灵条目**(索引有条目、文件不存在)。
589
+ idx["nodes"].pop(nid, None)
590
+ else:
514
591
  idx["nodes"][nid] = e
515
592
  idx["buckets"] = self._count_buckets(idx["nodes"])
516
593
  return idx
@@ -537,7 +614,11 @@ class MdCG:
537
614
  pass
538
615
  for rec in ShardedLog.read_all(self.index_log_dir):
539
616
  nid, e = rec.get("id"), rec.get("e")
540
- if nid and e:
617
+ if not nid:
618
+ continue
619
+ if e is None: # 删除记录(tombstone),见 _load_index
620
+ idx["nodes"].pop(nid, None)
621
+ else:
541
622
  idx["nodes"][nid] = e
542
623
  idx["buckets"] = self._count_buckets(idx["nodes"])
543
624
  atomic_write(self.index_path, json.dumps(idx, ensure_ascii=False))
@@ -596,12 +677,19 @@ class MdCG:
596
677
  "created_at": fm.get("created_at", 0),
597
678
  "verification_basis": fm.get("verification_basis"),
598
679
  "has_neg_conditions": nodefile.has_non_applicable(content),
599
- "content_hash": hashlib.sha256(
600
- content.encode("utf-8")).hexdigest()[:12],
680
+ # 内容指纹走 nodefile 的唯一实现(两段式对账依赖同一算法)
681
+ "content_hash": nodefile.content_hash(content),
601
682
  # 时空字段入索引快照:STG 查询免读文件(大域/目录索引的延伸)
602
683
  "temporal": fm.get("temporal"),
603
684
  "spatial": fm.get("spatial"),
604
685
  "time_window": (fm.get("condition_space") or {}).get("time_window"),
686
+ # 生命周期状态(② 显式状态机):索引入快照 → 免读文件可查,
687
+ # 写入路径也因此无需读盘就能校验迁移合法性。重建口径与
688
+ # _stage 一致(旧库无该字段 → None → state_of 视为 active)。
689
+ lifecycle.STATE_FIELD: fm.get(lifecycle.STATE_FIELD),
690
+ # 记忆演化分支(④):重建口径与 _stage 一致
691
+ "branch_id": fm.get("branch_id"),
692
+ "branched_from": fm.get("branched_from"),
605
693
  "evidence_count": fm.get("evidence_count", 0),
606
694
  # 嵌套子图 / 关系边入索引快照:递归展开与链式遍历免读文件
607
695
  "subgraph": fm.get("subgraph"),
@@ -722,6 +810,41 @@ class MdCG:
722
810
  "evidence_count": 0, "positive_evidence": 0, "negative_evidence": 0,
723
811
  }
724
812
  fm.update(extra)
813
+ # 生命周期状态(② 显式状态机,真源 `lifecycle.py`):add 是**全量重建
814
+ # fm** 而非增量更新,故必须显式处理状态——否则已定型/已降权节点会被静默
815
+ # 打回 active(与 ④ rewrite 必须重传 branch_id 同构的坑)。口径:
816
+ # · 新节点:显式落 active(不再依赖「缺省即 active」的隐式约定);
817
+ # · 覆写既有节点:**继承旧状态**;显式传入 state 时走迁移裁决,非法迁移
818
+ # **拒绝**(负路由:抛 TransitionError,与非法层名同风格)。
819
+ # 取旧状态优先读**索引快照**(免读盘);索引缺该键(升级前的旧库)才回读
820
+ # 节点文件兜底——不兜底会把存量 converged/demoted 节点误判成 active。
821
+ prev_entry = (self.index.get("nodes") or {}).get(node_id)
822
+ prev_state = lifecycle.state_of(prev_entry)
823
+ if prev_entry and lifecycle.STATE_FIELD not in prev_entry:
824
+ prev_state = lifecycle.state_of(
825
+ (self.get(node_id) or {}).get("frontmatter"))
826
+ # 显式入口兼容两种写法:`state=`(调用方直觉)与 `lifecycle_state=`。
827
+ # `state` 必须 **pop 掉**——该键名已属裁决四态(ACCEPT/REJECT/DEFER/
828
+ # BLINDSPOT),落进 frontmatter 只会在读面制造同名歧义;归一到真字段名后
829
+ # 再走同一裁决。两者同时给出时以 `lifecycle_state=` 为准(字段名更明确)。
830
+ _alias = fm.pop("state", None)
831
+ want_state = fm.get(lifecycle.STATE_FIELD)
832
+ if want_state is None:
833
+ want_state = _alias
834
+ if want_state is None:
835
+ fm[lifecycle.STATE_FIELD] = prev_state
836
+ else:
837
+ # 受保护判定取**索引快照**(兼具两层含义):本次 fm 里的保护标记、以及
838
+ # 节点**既有**的保护(快照带 protected/immutable)。只看本次 fm 会漏掉
839
+ # 「既有受保护节点被覆写时降级」——保护 = 不可遗忘,不因一次 add 失守。
840
+ # 注:该参数只在降级迁移上生效(check 内按方向判定),回升不受限。
841
+ _pe = prev_entry or {}
842
+ lifecycle.require_transition(
843
+ prev_state, want_state,
844
+ protected=bool(fm.get("protected") or fm.get("immutable")
845
+ or _pe.get("protected") or _pe.get("immutable")),
846
+ override=override)
847
+ fm[lifecycle.STATE_FIELD] = want_state
725
848
  # G8 派生溯源:把「来源声明」写进 frontmatter(单一真相源),台账为派生物。
726
849
  # 只声明事实、不做校验式拒绝——关系名非法仅回退默认值,不阻断写入。
727
850
  parents = provenance.as_list(derived_from)
@@ -759,6 +882,11 @@ class MdCG:
759
882
  "writer": fm.get("writer"),
760
883
  "session": fm.get("session"),
761
884
  "harness": fm.get("harness"),
885
+ # 记忆演化分支(④):fork 副本带分支归属与溯源主支
886
+ "branch_id": fm.get("branch_id"),
887
+ "branched_from": fm.get("branched_from"),
888
+ # 生命周期状态(②):索引快照透出 → 免读文件可查(与 _scan_nodes 同口径)
889
+ lifecycle.STATE_FIELD: fm.get(lifecycle.STATE_FIELD),
762
890
  })
763
891
  subgraph.invalidate_cache(self)
764
892
  chain.invalidate_cache(self)
@@ -771,6 +899,32 @@ class MdCG:
771
899
  actor=extra.get("actor"))
772
900
  return node_id
773
901
 
902
+ def set_state(self, node_id: str, dst: str, reason: str = None,
903
+ actor: str = None, override: bool = False) -> dict:
904
+ """节点生命周期状态推进(② 显式状态机:**唯一推进入口**,见 lifecycle.py)。
905
+
906
+ 非法迁移**拒绝**(返回 `ok=False` + `error` 机器码,不抛异常——负路由);
907
+ 受保护节点(`protected`/`immutable`)不接受降级,需 `override=True`。
908
+ 状态与迁移历史落 frontmatter,索引快照同步,审计追加 `_lifecycle.jsonl`。
909
+ """
910
+ return lifecycle.set_state(self, node_id, dst, reason=reason,
911
+ actor=actor, override=override)
912
+
913
+ def reconcile_writes(self, apply: bool = True, limit: int = 2000) -> dict:
914
+ """写入两段式对账(③ 两段式提交):把崩溃遗留的半途写入**补账或标记**。
915
+
916
+ 幂等(补出的 outcome 与正常 outcome 同形,二次扫描无未配对项);
917
+ `apply=False` 为只读盘点。返回 `{"unpaired","committed","interrupted",
918
+ "applied","details"}`;账本为 root 下 `_write_2pc.jsonl`(append-only)。
919
+ """
920
+ from . import twophase # 延迟导入:与写路径解耦,避免模块环
921
+ return twophase.reconcile(self, apply=apply, limit=limit)
922
+
923
+ def pending_writes(self, limit: int = 200) -> list:
924
+ """未结清的写入意图(有 intent 无 outcome)——只读,不改账本。"""
925
+ from . import twophase
926
+ return twophase.pending(self, limit=limit)
927
+
774
928
  # ------------------------------------------------------------------
775
929
  # 边域窄原语(写路径收口:白箱工具写 md 真源的唯一正路)。
776
930
  #
@@ -1077,6 +1231,26 @@ class MdCG:
1077
1231
  if len(self._dirty) >= self.autoflush:
1078
1232
  self.flush()
1079
1233
 
1234
+ def _unstage(self, node_id):
1235
+ """摘除索引条目并**持久化**——与 `_stage` 对称的删除原语。
1236
+
1237
+ 只 pop 内存索引是不够的:`close()` 注释里记过同构的坑(已有
1238
+ `_index.json` 的根重开不重扫目录),于是「删掉的条目」会在
1239
+ `_index_log` 重放时复活成**幽灵条目**(索引有条目、节点文件不存在)。
1240
+ 幽灵条目的代价:检索白跑候选、`ref action=prune` 因 `cg.get` 取不回
1241
+ 而够不着它,`check` 的 dangling 永不归零(本机实测累积数百条)。
1242
+ """
1243
+ e = self.index["nodes"].pop(node_id, None)
1244
+ if e and e.get("bucket"):
1245
+ b = e["bucket"]
1246
+ left = self.index["buckets"].get(b, 0) - 1
1247
+ if left > 0:
1248
+ self.index["buckets"][b] = left
1249
+ else:
1250
+ self.index["buckets"].pop(b, None)
1251
+ self._dirty[node_id] = None # None = 删除记录,随 flush 落分片日志
1252
+ self.flush() # 删除不可延迟到 autoflush 阈值
1253
+
1080
1254
  # ---------- 内容封装钩子(默认恒等;MdCGSecure 覆盖为「私有内容加密」)----
1081
1255
 
1082
1256
  def _seal_content(self, node_id: str, content: str,
@@ -1247,7 +1421,7 @@ class MdCG:
1247
1421
  def search(self, query: str, layer: str = None, k: int = 20,
1248
1422
  context=None, min_results: int = 1, record: bool = True,
1249
1423
  include_neg: bool = True, judge: bool = True, pools=None,
1250
- session=None):
1424
+ session=None, branch=None):
1251
1425
  """返回 (results, meta)。results = [(node_dict, score, qualification)]。
1252
1426
 
1253
1427
  meta 含 tier(性能层级)、scanned(读取节点数)、bucket(路由桶)、candidates。
@@ -1275,7 +1449,10 @@ class MdCG:
1275
1449
  # 默认排除掉负记忆层的节点进入正排打分,仅作为「覆盖标记」用
1276
1450
  entries = [e for e in self.index["nodes"].values()
1277
1451
  if (not layer or e["layer"] == layer)
1278
- and (not session or e.get("session") == session)]
1452
+ and (not session or e.get("session") == session)
1453
+ # 分支实验场:默认(branch=None)分支节点全部隐身;
1454
+ # branch=<id> 时主支 + 本分支可见、其他分支仍隐身
1455
+ and e.get("branch_id") in (None, branch)]
1279
1456
  if not entries:
1280
1457
  return [], {"tier": None, "reason": "no_candidates", "scanned": 0}
1281
1458
 
@@ -1334,26 +1511,30 @@ class MdCG:
1334
1511
  return out
1335
1512
 
1336
1513
  # T2:跨桶 LIKE(§七 截断点:分池截断,索引类不再挤掉知识类)
1514
+ # 截断依据=相关度(先全量打分再排序截断):命中集沿 entries(目录枚举序)
1515
+ # 排列,原来「取前 GLOBAL_CAP 条再打分」等价于用写入顺序抽签决定谁进
1516
+ # 候选池。cap 值不变,变的只是拿什么排序(见 cut_by_relevance)。
1337
1517
  docs_all = self._read_many(entries, stat)
1338
1518
  # 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
1339
1519
  hits = [d for d in docs_all if self._like(d[2], d[1], terms)
1340
1520
  or (semantic_on() and d[1].get("semantic"))]
1341
1521
  stat["pre_cap"] = len(hits)
1342
1522
  stat["cap"] = GLOBAL_CAP
1343
- hits, _rep = pooling.cut_report(hits, GLOBAL_CAP, pools=pool_cfg,
1344
- key_of=pooling.doc_key)
1523
+ hits, _rep = cut_by_relevance(hits, self._score(hits, q, qb, pool_cfg),
1524
+ GLOBAL_CAP, pools=pool_cfg,
1525
+ key_of=pooling.doc_key, stat=stat)
1345
1526
  pooling.record_audit(stat, _rep)
1346
1527
  out = try_stage(hits, TIER_GLOBAL_LIKE)
1347
1528
  if out:
1348
1529
  return out
1349
1530
 
1350
- # T3:全量兜底(同为分池截断点)
1351
- docs_all.sort(key=lambda d: (-float(d[1].get("importance") or 0),
1352
- -float(d[1].get("created_at") or 0)))
1531
+ # T3:全量兜底(同为分池截断点;截断依据同为相关度,importance 作次级键)
1353
1532
  stat["pre_cap"] = len(docs_all)
1354
1533
  stat["cap"] = GLOBAL_CAP
1355
- picked, _rep = pooling.cut_report(docs_all, GLOBAL_CAP, pools=pool_cfg,
1356
- key_of=pooling.doc_key)
1534
+ picked, _rep = cut_by_relevance(docs_all,
1535
+ self._score(docs_all, q, qb, pool_cfg),
1536
+ GLOBAL_CAP, pools=pool_cfg,
1537
+ key_of=pooling.doc_key, stat=stat)
1357
1538
  pooling.record_audit(stat, _rep)
1358
1539
  scored = self._score(picked, q, qb, pool_cfg)
1359
1540
  return self._emit(scored, k, TIER_GLOBAL_SCAN, stat, route_bucket,
@@ -1461,6 +1642,8 @@ class MdCG:
1461
1642
  "candidates": candidates,
1462
1643
  # §七 分池审计:截断前候选数 / 全局额度 / 生效分池计划
1463
1644
  "pre_cap": stat.get("pre_cap"), "cap": stat.get("cap"),
1645
+ # 截断依据审计:relevance=先打分再排序截断(现行)
1646
+ "cut_order": stat.get("cut_order"),
1464
1647
  "pools": pool_plan,
1465
1648
  "covered_neg": [nc["path"] for nc in neg_coverage],
1466
1649
  # 阶段 1 大域收敛结果 + 完整打分明细(白箱可审计)
@@ -1599,8 +1782,7 @@ class MdCG:
1599
1782
  verification_basis="test")
1600
1783
  # 从原位置删除(节点进入 rejected 层)
1601
1784
  os.remove(os.path.join(self.root, node["path"]))
1602
- self.index["nodes"].pop(node_id, None)
1603
- self._dirty.pop(node_id, None)
1785
+ self._unstage(node_id) # 同上:索引删除必须可重放(防幽灵条目)
1604
1786
  return {"action": "falsified", "new_id": None,
1605
1787
  "evidence_count": None, "demoted": None}
1606
1788
  # confirmed/weakened:调整 confidence(白箱第 5 篇:
@@ -1622,6 +1804,12 @@ class MdCG:
1622
1804
  demoted = self._move_layer(
1623
1805
  node_id, "contextual",
1624
1806
  reason=f"confidence {fm['confidence']} < {DEMOTE_CONFIDENCE}")
1807
+ # 层降级 = 生命周期降级(②):knowledge→contextual 即 state→demoted。
1808
+ # 受保护节点已被 `guard_move` 拦在上一层(走不到这里);此处仍按负路由
1809
+ # 柔和处理——ok=False 只说明状态不可降,不回滚已完成的层迁移。
1810
+ self.set_state(node_id, "demoted",
1811
+ reason=f"confidence {fm['confidence']} < {DEMOTE_CONFIDENCE}",
1812
+ actor="verify:weakened")
1625
1813
  else:
1626
1814
  self._write_node(node_id, os.path.join(self.root, node["path"]),
1627
1815
  fm, node["content"])