@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
@@ -0,0 +1,224 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · 索引持久化守卫(跨进程可见性 · 进程退出兜底)
3
+
4
+ 运行:python -m md_cg.test_index_durability
5
+
6
+ 背景(2026-09-16 取证,首例=审核裁决通路):
7
+ 节点 .md 由 `add()` **立即写盘**,但索引条目先进内存 `_dirty`,靠 `flush()` /
8
+ `close()` 落分片日志。一次性进程(CLI / 脚本)写入 1~2 条、远未达 autoflush(64)
9
+ 阈值即退出 → 日志无记录;而**已有 `_index.json` 的根重开时不重扫目录**
10
+ (`_load_index`:快照优先,仅重放日志),于是节点「**在盘上但索引无条目**」,
11
+ 其它进程与重载后的长驻进程都检索不到,只能靠某次全量 `rebuild_index()` 偶然救回。
12
+ 表象极易被误判为「写入丢失」——故本测试以**索引可见性**(而非 `get()`)为判据。
13
+
14
+ 固化三处修复 + 一处反证(防测试空转):
15
+ ① 库层进程退出兜底(`mdcg._LIVE_CGS` + atexit):子进程不显式收尾也能落盘;
16
+ ② `review_decide` 统一收尾:目标节点 **与** 审计记录节点双落盘
17
+ (accept/edit 写前者、`_record_decision` 写后者;reject 路径**只**写后者);
18
+ ③ `review_cli` 显式 `close()`:真实缺陷现场(一次性 CLI 裁决)端到端;
19
+ ④ 反证组:清空兜底登记 → 复现「索引不可见」,证明 ①② 确在生效。
20
+
21
+ `_index.json` 快照是「贴真实库形态」的必要前提:空 root 首开会全扫目录,
22
+ 恰好掩盖本缺陷(这也是首版复现实验失败的教训)。
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import os
27
+ import shutil
28
+ import subprocess
29
+ import sys
30
+ import tempfile
31
+
32
+ from .mdcos import MdCGOS
33
+
34
+ PASS = FAIL = 0
35
+ FAILS = []
36
+
37
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38
+
39
+ CCG = ("# 功能名:{n}\n# 生效条件:{c}\n# 子功能:{s}\n# 执行:{e}\n"
40
+ "# 验证方式:test\n# 不适用条件:{neg}\n\n{body}\n")
41
+
42
+
43
+ def check(name, cond, detail=""):
44
+ global PASS, FAIL
45
+ if cond:
46
+ PASS += 1
47
+ print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
48
+ else:
49
+ FAIL += 1
50
+ FAILS.append(name)
51
+ print(f" [FAIL] {name} · {detail}")
52
+
53
+
54
+ def mk(n, cond, body, neg="其它"):
55
+ return CCG.format(n=n, c=cond, s=body[:20], e=body, neg=neg, body=body)
56
+
57
+
58
+ def _child_env():
59
+ """子进程环境:显式 UTF-8 + PYTHONPATH 指向本仓(纪律 15:不经 Windows shell)。"""
60
+ env = dict(os.environ)
61
+ env["PYTHONUTF8"] = "1"
62
+ env["PYTHONIOENCODING"] = "utf-8"
63
+ env["PYTHONPATH"] = REPO + os.pathsep + env.get("PYTHONPATH", "")
64
+ return env
65
+
66
+
67
+ def _run_child(code):
68
+ """以子进程执行一段脚本(argv 列表,shell=False)。"""
69
+ fd, path = tempfile.mkstemp(suffix=".py", prefix="_idxguard_")
70
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
71
+ f.write(code)
72
+ try:
73
+ return subprocess.run([sys.executable, path], capture_output=True,
74
+ text=True, encoding="utf-8", errors="replace",
75
+ env=_child_env(), cwd=REPO)
76
+ finally:
77
+ try:
78
+ os.remove(path)
79
+ except OSError:
80
+ pass
81
+
82
+
83
+ def _seed(root, nid="seed"):
84
+ """建一个「已有快照」的库形态(缺陷成立的必要前提)。"""
85
+ cg = MdCGOS(root, actor="seed")
86
+ cg.add(nid, mk("种子节点", "问种子", "仅用于建索引快照"),
87
+ verification_basis="test")
88
+ cg.flush()
89
+ cg.rebuild_index()
90
+ cg.close()
91
+
92
+
93
+ def _visible(root, nid):
94
+ """索引可见性判据:**新实例**(模拟其它进程 / 重载后的长驻进程)索引里是否有该条目。
95
+
96
+ 刻意不用 `get()`:它夹带密级/密钥可读性判据,会把「索引缺失」与「读不到」混为一谈。
97
+ """
98
+ probe = MdCGOS(root, actor="probe")
99
+ try:
100
+ return nid in (probe.index.get("nodes") or {})
101
+ finally:
102
+ probe.close()
103
+
104
+
105
+ CHILD_BODY = (
106
+ "import sys\n"
107
+ "sys.path.insert(0, {repo!r})\n"
108
+ "from md_cg.mdcos import MdCGOS\n"
109
+ "cg = MdCGOS({root!r}, actor='child', autoflush=64)\n"
110
+ "cg.add({nid!r}, {content!r}, verification_basis='test')\n"
111
+ )
112
+
113
+
114
+ def main():
115
+ roots = []
116
+ try:
117
+ # ---------------- ① 进程退出兜底(atexit) ----------------
118
+ print("\n【①】库层进程退出兜底:子进程不显式收尾")
119
+ root_a = tempfile.mkdtemp(prefix="idxdur_a_")
120
+ roots.append(root_a)
121
+ _seed(root_a)
122
+ r = _run_child(CHILD_BODY.format(
123
+ repo=REPO, root=root_a, nid="n_atexit",
124
+ content=mk("兜底测试节点", "问兜底", "子进程写一条后直接退出")) +
125
+ "# 故意不 close:验证进程退出兜底(atexit)\n")
126
+ check("子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
127
+ check("兜底生效:新进程索引可见", _visible(root_a, "n_atexit"))
128
+
129
+ # ---------------- ④ 反证组(防测试空转) ----------------
130
+ print("\n【④】反证组:清空兜底登记 → 复现缺陷")
131
+ root_b = tempfile.mkdtemp(prefix="idxdur_b_")
132
+ roots.append(root_b)
133
+ _seed(root_b)
134
+ r = _run_child(CHILD_BODY.format(
135
+ repo=REPO, root=root_b, nid="n_nohook",
136
+ content=mk("反证测试节点", "问反证", "清空兜底登记后退出")) +
137
+ "# 反证:摘掉兜底登记(等价于修复前形态)\n"
138
+ "from md_cg.mdcg import _LIVE_CGS\n"
139
+ "_LIVE_CGS.clear()\n")
140
+ check("反证子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
141
+ check("反证:索引不可见(缺陷可复现,证明兜底是有效变量)",
142
+ not _visible(root_b, "n_nohook"))
143
+
144
+ # ---------------- ③ 显式 close 对照(原有正路不回归) ----------------
145
+ print("\n【③】显式收尾对照:close() 仍是正路")
146
+ root_c = tempfile.mkdtemp(prefix="idxdur_c_")
147
+ roots.append(root_c)
148
+ _seed(root_c)
149
+ r = _run_child(CHILD_BODY.format(
150
+ repo=REPO, root=root_c, nid="n_close",
151
+ content=mk("显式收尾节点", "问收尾", "子进程写一条后 close")) +
152
+ "cg.close()\n")
153
+ check("显式 close 子进程正常退出", r.returncode == 0, (r.stderr or "")[:200])
154
+ check("显式 close:新进程索引可见", _visible(root_c, "n_close"))
155
+
156
+ # ---------------- ② review_decide 通路统一收尾 ----------------
157
+ print("\n【②】审核裁决通路:目标节点 + 审计记录节点双落盘")
158
+ root_d = tempfile.mkdtemp(prefix="idxdur_d_")
159
+ roots.append(root_d)
160
+ _seed(root_d, nid="seed_d")
161
+ osd = MdCGOS(root_d, actor="designer")
162
+ pid = osd.propose("n_acc", mk("受审节点", "问受审", "审核落盘的目标节点"),
163
+ verify={"kind": "test", "assertions": ["断言通过"]})
164
+ out = osd.review_decide(pid, "accept", reason="测试判据")
165
+ check("accept 裁决成功", bool(out.get("ok")), str(out)[:160])
166
+ rec_nid = out.get("record_node_id")
167
+ probe = MdCGOS(root_d, actor="probe")
168
+ try:
169
+ check("accept:目标节点索引可见(新实例)",
170
+ out.get("node_id") in probe.index["nodes"], str(out.get("node_id")))
171
+ check("accept:审计记录节点索引可见(新实例)",
172
+ bool(rec_nid) and rec_nid in probe.index["nodes"], str(rec_nid))
173
+ check("accept:review_records 可列出(复核面不空)",
174
+ any(x["id"] == rec_nid for x in probe.review_records(pid=pid)))
175
+ finally:
176
+ probe.close()
177
+
178
+ pid2 = osd.propose("n_rej", mk("被拒节点", "问被拒", "reject 只记裁决不落目标节点"))
179
+ out2 = osd.review_decide(pid2, "reject", reason="不通过")
180
+ check("reject 裁决成功", bool(out2.get("ok")), str(out2)[:160])
181
+ rec2 = out2.get("record_node_id")
182
+ check("reject:审计记录节点索引可见(该路径只写它)",
183
+ bool(rec2) and _visible(root_d, rec2), str(rec2))
184
+ osd.close()
185
+
186
+ # ---------------- ③ review_cli 端到端(真实缺陷现场) ----------------
187
+ print("\n【③】review_cli 端到端(一次性 CLI 裁决)")
188
+ root_e = tempfile.mkdtemp(prefix="idxdur_e_")
189
+ roots.append(root_e)
190
+ _seed(root_e, nid="seed_e")
191
+ ose = MdCGOS(root_e, actor="designer")
192
+ pid_e = ose.propose("n_cli", mk("CLI 裁决节点", "问 CLI",
193
+ "review_cli 落盘的目标节点"))
194
+ ose.close()
195
+ r = subprocess.run(
196
+ [sys.executable, os.path.join(REPO, "scripts", "review_cli.py"),
197
+ "accept", pid_e, "--root", root_e, "--reason", "端到端守卫测试"],
198
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
199
+ env=_child_env(), cwd=REPO)
200
+ check("review_cli 退出码 0", r.returncode == 0,
201
+ (r.stdout or r.stderr or "")[:200])
202
+ check("review_cli:目标节点索引可见(新进程)", _visible(root_e, "n_cli"))
203
+ probe = MdCGOS(root_e, actor="probe")
204
+ try:
205
+ recs = probe.review_records(pid=pid_e)
206
+ check("review_cli:审计记录节点索引可见",
207
+ bool(recs) and all(x["id"] in probe.index["nodes"] for x in recs),
208
+ str([x["id"] for x in recs]))
209
+ finally:
210
+ probe.close()
211
+ finally:
212
+ for r_ in roots:
213
+ shutil.rmtree(r_, ignore_errors=True)
214
+
215
+ print("\n" + "=" * 64)
216
+ print(f"PASS={PASS} FAIL={FAIL}")
217
+ if FAILS:
218
+ print("失败项:" + ";".join(FAILS))
219
+ return 0 if FAIL == 0 else 1
220
+
221
+
222
+ if __name__ == "__main__":
223
+ sys.exit(main())
224
+
@@ -0,0 +1,309 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_lifecycle · 节点显式生命周期状态机(Pi 可学习优点 ②)
3
+
4
+ 背景:状态散落四处各说各话——`writelimit` 用 tag `tidy:converged`、`forgetting`
5
+ 用 importance 升降、`protect` 用 `fm.protected`、`_evolution/ledger.md` 记账本;
6
+ **没有任何地方校验「这一步迁移合不合法」**。本测试覆盖唯一真源 `lifecycle.py`
7
+ 及其四条收口路径。
8
+
9
+ 覆盖:① 纯函数裁决 ② 索引快照带状态(内存+磁盘) ③ 合法逐级迁移(历史+审计)
10
+ ④ 非法迁移拒绝(负路由,且拒绝不留痕) ⑤ 幂等 ⑥ 受保护豁免降级 / 回升不受限
11
+ ⑦ add 全量重建 fm 时的状态继承 ⑧ 存量回填 backfill ⑨ 收口点(writelimit /
12
+ forgetting) ⑩ 边界(节点不存在 / 永不删除)
13
+
14
+ 运行:python -m md_cg.test_lifecycle
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import shutil
21
+ import tempfile
22
+
23
+ from . import lifecycle, writelimit
24
+ from .forgetting import reinforce
25
+ from .mdcos import MdCGOS
26
+
27
+ _root = tempfile.mkdtemp(prefix="mdcg_life_")
28
+ cg = MdCGOS(_root)
29
+
30
+ _ok = 0
31
+ _fail = []
32
+
33
+
34
+ def check(name, cond, detail=""):
35
+ global _ok
36
+ if cond:
37
+ _ok += 1
38
+ print("[ok] " + name)
39
+ else:
40
+ _fail.append(name)
41
+ print("[FAIL] %s · %s" % (name, str(detail)[:240]))
42
+
43
+
44
+ def _fm(nid):
45
+ return ((cg.get(nid) or {}).get("frontmatter")) or {}
46
+
47
+
48
+ def _idx(nid):
49
+ return (cg.index.get("nodes") or {}).get(nid) or {}
50
+
51
+
52
+ def _disk_index():
53
+ """读**磁盘** `_index.json`——「免读文件可查」的硬验收面。"""
54
+ p = os.path.join(cg.root, "_index.json")
55
+ if not os.path.exists(p):
56
+ return {}
57
+ with open(p, encoding="utf-8", errors="replace") as f:
58
+ return json.load(f)
59
+
60
+
61
+ def _index_log():
62
+ """读**增量索引日志**(ShardedLog 分片)——`flush()` 的实际落点(`_index.json`
63
+ 是快照,由 `rebuild_index()` 写)。"""
64
+ out = []
65
+ d = os.path.join(cg.root, "_index_log")
66
+ if not os.path.isdir(d):
67
+ return out
68
+ for fn in sorted(os.listdir(d)):
69
+ p = os.path.join(d, fn)
70
+ if not os.path.isfile(p):
71
+ continue
72
+ with open(p, encoding="utf-8", errors="replace") as f:
73
+ for line in f:
74
+ if not line.strip():
75
+ continue
76
+ try:
77
+ out.append(json.loads(line))
78
+ except Exception:
79
+ pass
80
+ return out
81
+
82
+
83
+ def _audit():
84
+ p = os.path.join(cg.root, lifecycle.AUDIT_FILE)
85
+ out = []
86
+ if os.path.exists(p):
87
+ with open(p, encoding="utf-8", errors="replace") as f:
88
+ for line in f:
89
+ if line.strip():
90
+ out.append(json.loads(line))
91
+ return out
92
+
93
+
94
+ # ------------------------------------------------------------ ① 纯函数裁决
95
+ check("①1 缺字段 / 未知值 / 非 dict → active(存量兼容,不猜测)",
96
+ lifecycle.state_of({}) == "active"
97
+ and lifecycle.state_of({lifecycle.STATE_FIELD: "frozen"}) == "active"
98
+ and lifecycle.state_of(None) == "active")
99
+ check("①2 状态集内值原样返回",
100
+ all(lifecycle.state_of({lifecycle.STATE_FIELD: s}) == s
101
+ for s in lifecycle.STATES))
102
+ check("①3 合法迁移对可通行(逐级降级 + 逐级回升)",
103
+ lifecycle.can_transition("active", "converged")
104
+ and lifecycle.can_transition("converged", "demoted")
105
+ and lifecycle.can_transition("demoted", "archived")
106
+ and lifecycle.can_transition("archived", "active"))
107
+ check("①4 跳级迁移一律不通",
108
+ not lifecycle.can_transition("active", "archived")
109
+ and not lifecycle.can_transition("converged", "archived")
110
+ and not lifecycle.can_transition("archived", "demoted"))
111
+ check("①5 同状态幂等可通行", lifecycle.can_transition("demoted", "demoted"))
112
+ check("①6 check 未知状态 → unknown_state",
113
+ lifecycle.check("active", "frozen")[:2] == (False, "unknown_state"))
114
+ check("①7 check 同状态 → noop",
115
+ lifecycle.check("active", "active")[:2] == (True, "noop"))
116
+ check("①8 check 跳级 → illegal_transition",
117
+ lifecycle.check("active", "archived")[:2]
118
+ == (False, "illegal_transition"))
119
+ check("①9 check 受保护降级 → protected;override 放行",
120
+ lifecycle.check("active", "demoted", protected=True)[:2]
121
+ == (False, "protected")
122
+ and lifecycle.check("active", "demoted", protected=True,
123
+ override=True)[:2] == (True, "ok"))
124
+ check("①10 is_downgrade 方向正确(回升不算降级)",
125
+ lifecycle.is_downgrade("active", "archived")
126
+ and not lifecycle.is_downgrade("archived", "active"))
127
+
128
+ # ------------------------------------------------------------ ② 新节点与索引
129
+ n1 = cg.add("lc_n1", "生命周期状态机测试节点甲", layer="knowledge")
130
+ check("②1 新节点**显式**落 active(不再依赖缺省约定)",
131
+ _fm(n1).get(lifecycle.STATE_FIELD) == "active", _fm(n1))
132
+ check("②2 索引快照带状态(内存)",
133
+ _idx(n1).get(lifecycle.STATE_FIELD) == "active", _idx(n1))
134
+ cg.flush()
135
+ check("②3 增量日志(_index_log)带状态",
136
+ any((r.get("e") or {}).get(lifecycle.STATE_FIELD) == "active"
137
+ for r in _index_log() if r.get("id") == n1), _index_log())
138
+ cg.rebuild_index()
139
+ check("②4 磁盘 _index.json 快照带状态(免读文件即可回答「这节点什么状态」)",
140
+ ((_disk_index().get("nodes") or {}).get(n1) or {}
141
+ ).get(lifecycle.STATE_FIELD) == "active")
142
+
143
+ # ------------------------------------------------------------ ③ 合法逐级迁移
144
+ r1 = cg.set_state(n1, "converged", reason="同构聚合定型", actor="test")
145
+ check("③1 active→converged 通过且落盘",
146
+ r1.get("ok") and r1.get("changed")
147
+ and _fm(n1).get(lifecycle.STATE_FIELD) == "converged", r1)
148
+ check("③2 索引同步到 converged",
149
+ _idx(n1).get(lifecycle.STATE_FIELD) == "converged")
150
+ r2 = cg.set_state(n1, "demoted", reason="弱化", actor="test")
151
+ r3 = cg.set_state(n1, "archived", reason="归档", actor="test")
152
+ check("③3 converged→demoted→archived 逐级通过",
153
+ r2.get("ok") and r3.get("ok")
154
+ and _fm(n1).get(lifecycle.STATE_FIELD) == "archived")
155
+ h1 = _fm(n1).get(lifecycle.HISTORY_FIELD) or []
156
+ check("③4 迁移历史逐条留痕(from/to/reason/actor)",
157
+ len(h1) == 3 and h1[-1]["from"] == "demoted"
158
+ and h1[-1]["to"] == "archived" and h1[-1]["reason"] == "归档"
159
+ and h1[-1]["actor"] == "test", h1)
160
+ check("③5 审计 _lifecycle.jsonl 留痕 3 条",
161
+ len([a for a in _audit() if a.get("node_id") == n1]) == 3, _audit())
162
+ check("③6 永不删除:archived 后节点文件仍在且可读",
163
+ bool(cg.get(n1))
164
+ and os.path.exists(os.path.join(cg.root, cg.get(n1)["path"])))
165
+
166
+ # ------------------------------------------------------------ ④ 非法迁移拒绝
167
+ n2 = cg.add("lc_n2", "生命周期状态机测试节点乙", layer="knowledge")
168
+ bad = cg.set_state(n2, "archived", reason="跳级归档", actor="test")
169
+ check("④1 跳级 active→archived 被拒(负路由,不抛)",
170
+ (not bad.get("ok")) and bad.get("error") == "illegal_transition", bad)
171
+ check("④2 拒绝后 frontmatter 未被改动(不留痕)",
172
+ _fm(n2).get(lifecycle.STATE_FIELD) in (None, "active")
173
+ and not (_fm(n2).get(lifecycle.HISTORY_FIELD) or []), _fm(n2))
174
+ check("④3 拒绝后索引未被改动",
175
+ _idx(n2).get(lifecycle.STATE_FIELD) in (None, "active"))
176
+ check("④4 未知状态被拒",
177
+ cg.set_state(n2, "frozen").get("error") == "unknown_state")
178
+ check("④5 节点不存在如实上报(不静默创建)",
179
+ cg.set_state("lc_missing", "converged").get("error") == "node_not_found")
180
+ try:
181
+ lifecycle.require_transition("active", "archived")
182
+ _hard = False
183
+ except lifecycle.TransitionError:
184
+ _hard = True
185
+ check("④6 require_transition 硬拒(写路径抛异常)", _hard)
186
+ try:
187
+ cg.add("lc_n2", "覆写并试图跳级归档", state="archived")
188
+ _add_hard = False
189
+ except lifecycle.TransitionError:
190
+ _add_hard = True
191
+ check("④7 add 覆写显式非法状态 → 硬拒", _add_hard)
192
+ check("④8 硬拒后磁盘内容未被改写(校验先于写盘)",
193
+ _fm(n2).get(lifecycle.STATE_FIELD) in (None, "active"))
194
+
195
+ # ------------------------------------------------------------ ⑤ 幂等
196
+ h_before = len(_fm(n1).get(lifecycle.HISTORY_FIELD) or [])
197
+ aud_before = len([a for a in _audit() if a.get("node_id") == n1])
198
+ same = cg.set_state(n1, "archived", reason="重复归档", actor="test")
199
+ check("⑤1 同状态幂等(noop / changed=False)",
200
+ same.get("ok") and same.get("changed") is False
201
+ and same.get("code") == "noop", same)
202
+ check("⑤2 幂等不刷历史",
203
+ len(_fm(n1).get(lifecycle.HISTORY_FIELD) or []) == h_before)
204
+ check("⑤3 幂等不写审计",
205
+ len([a for a in _audit() if a.get("node_id") == n1]) == aud_before)
206
+
207
+ # ------------------------------------------------------------ ⑥ 受保护豁免降级
208
+ n3 = cg.add("lc_n3", "受保护节点丙", layer="knowledge", importance=0.8)
209
+ check("⑥0 新建即自动受保护(importance≥0.7)",
210
+ _fm(n3).get("protected") is True, _fm(n3))
211
+ prot = cg.set_state(n3, "demoted", reason="试图降级", actor="test")
212
+ check("⑥1 受保护节点降级被拒 → protected 码",
213
+ (not prot.get("ok")) and prot.get("error") == "protected", prot)
214
+ check("⑥2 拒绝后状态未变(保护 = 不可遗忘)",
215
+ _fm(n3).get(lifecycle.STATE_FIELD) == "active")
216
+ ov = cg.set_state(n3, "demoted", reason="显式放行", actor="test",
217
+ override=True)
218
+ check("⑥3 override=True 放行降级",
219
+ ov.get("ok") and _fm(n3).get(lifecycle.STATE_FIELD) == "demoted", ov)
220
+ up = cg.set_state(n3, "active", reason="回升不受限", actor="test")
221
+ check("⑥4 回升不受保护限制(无需 override)",
222
+ up.get("ok") and _fm(n3).get(lifecycle.STATE_FIELD) == "active")
223
+
224
+ # ------------------------------------------------------------ ⑦ add 状态继承
225
+ n4 = cg.add("lc_n4", "状态继承节点丁", layer="knowledge")
226
+ cg.set_state(n4, "converged", reason="定型", actor="test")
227
+ cg.add("lc_n4", "覆写内容(裸 add,不带状态)", layer="knowledge")
228
+ check("⑦1 裸 add 覆写**继承**旧状态(不静默打回 active)",
229
+ _fm(n4).get(lifecycle.STATE_FIELD) == "converged", _fm(n4))
230
+ check("⑦2 继承后索引仍是 converged",
231
+ _idx(n4).get(lifecycle.STATE_FIELD) == "converged")
232
+ cg.add("lc_n4", "覆写并合法回升", layer="knowledge", state="active")
233
+ check("⑦3 显式合法迁移放行(converged→active)",
234
+ _fm(n4).get(lifecycle.STATE_FIELD) == "active")
235
+ check("⑦4 新节点显式状态从 active 起算(active→converged 合法)",
236
+ _fm(cg.add("lc_n5", "显式定型节点", layer="knowledge",
237
+ state="converged")).get(lifecycle.STATE_FIELD) == "converged")
238
+
239
+ # ------------------------------------------------------------ ⑧ 存量回填
240
+ node4 = cg.get(n4)
241
+ fm4 = dict(node4["frontmatter"])
242
+ fm4.pop(lifecycle.STATE_FIELD, None) # 造「旧库缺字段」形态
243
+ cg._write_node(n4, os.path.join(cg.root, node4["path"]), fm4,
244
+ node4.get("content") or "")
245
+ _idx(n4).pop(lifecycle.STATE_FIELD, None)
246
+ check("⑧0 旧库形态成立(字段已摘除)",
247
+ lifecycle.STATE_FIELD not in _fm(n4))
248
+ pre = lifecycle.backfill(cg, apply=False)
249
+ check("⑧1 dry-run 盘点缺失且不改盘",
250
+ pre.get("dry_run") and n4 in (pre.get("planned") or [])
251
+ and lifecycle.STATE_FIELD not in _fm(n4), pre)
252
+ app = lifecycle.backfill(cg, apply=True)
253
+ check("⑧2 apply 回填为显式 active",
254
+ app.get("backfilled", 0) >= 1
255
+ and _fm(n4).get(lifecycle.STATE_FIELD) == "active", app)
256
+ check("⑧3 回填幂等(第二次无缺失)",
257
+ lifecycle.backfill(cg, apply=True).get("backfilled") == 0)
258
+ check("⑧4 缺字段节点在回填前也按 active 工作(state_of 缺省)",
259
+ lifecycle.state_of({}) == "active")
260
+
261
+ # ------------------------------------------------------------ ⑨ 收口点
262
+ n6 = cg.add("lc_n6", "同构聚合成员节点", layer="contextual", importance=0.4)
263
+ node6 = cg.get(n6)
264
+ e6 = {"id": n6, "path": node6["path"],
265
+ "frontmatter": dict(node6["frontmatter"]),
266
+ "content": node6.get("content") or ""}
267
+ writelimit._demote(cg, e6)
268
+ check("⑨1 tidy 降权 → lifecycle_state=converged",
269
+ _fm(n6).get(lifecycle.STATE_FIELD) == "converged", _fm(n6))
270
+ check("⑨2 兼容别名 tidy:converged tag 保留(历史数据不破)",
271
+ "tidy:converged" in (_fm(n6).get("tags") or []))
272
+ check("⑨3 降权后索引同步 converged",
273
+ _idx(n6).get(lifecycle.STATE_FIELD) == "converged")
274
+ _imp_before = float(_fm(n6).get("importance") or 0)
275
+ reinforce(cg, n6)
276
+ check("⑨4 MERGE 强化 → 逐级回升 active",
277
+ _fm(n6).get(lifecycle.STATE_FIELD) == "active", _fm(n6))
278
+ check("⑨5 回升与 importance 强化同批落盘(+delta)",
279
+ float(_fm(n6).get("importance") or 0) > _imp_before)
280
+ h6 = _fm(n6).get(lifecycle.HISTORY_FIELD) or []
281
+ check("⑨6 两处收口共用同一迁移历史(active→converged→active 两条,同一 actor 面)",
282
+ len(h6) == 2 and (h6[0]["from"], h6[0]["to"]) == ("active", "converged")
283
+ and (h6[1]["from"], h6[1]["to"]) == ("converged", "active")
284
+ and h6[0]["actor"] == "sustain_tidy"
285
+ and h6[1]["actor"] == "forgetting:reinforce", h6)
286
+ n7 = cg.add("lc_n7", "普通节点庚", layer="knowledge")
287
+ h7_before = len(_fm(n7).get(lifecycle.HISTORY_FIELD) or [])
288
+ reinforce(cg, n7)
289
+ check("⑨7 强化 active 节点:状态幂等(不刷历史、不误降)",
290
+ _fm(n7).get(lifecycle.STATE_FIELD) == "active"
291
+ and len(_fm(n7).get(lifecycle.HISTORY_FIELD) or []) == h7_before)
292
+
293
+ # ------------------------------------------------------------ ⑩ 边界
294
+ check("⑩1 状态字段名与裁决四态 state 不撞名(消歧)",
295
+ lifecycle.STATE_FIELD == "lifecycle_state"
296
+ and lifecycle.STATE_FIELD != "state")
297
+ check("⑩2 set_state 返回体恒带 from/to/code(可审计)",
298
+ set(("from", "to", "code")) <= set(cg.set_state(n7, "active").keys()))
299
+ check("⑩3 迁移历史滚动上限(不无界膨胀)",
300
+ lifecycle.HISTORY_KEEP > 0
301
+ and len(_fm(n1).get(lifecycle.HISTORY_FIELD) or [])
302
+ <= lifecycle.HISTORY_KEEP)
303
+
304
+ print("=" * 58)
305
+ print("test_lifecycle: %d 通过 / %d 失败" % (_ok, len(_fail)))
306
+ if _fail:
307
+ print("失败项:" + "、".join(_fail))
308
+ shutil.rmtree(cg.root, ignore_errors=True)
309
+ raise SystemExit(1 if _fail else 0)