@furongjun1999/dsh-memory 0.7.2 → 0.7.4

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 (129) hide show
  1. package/README.md +57 -47
  2. package/codebuddy/CODEBUDDY.md +6 -6
  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/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
  27. package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -0
  28. 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
  29. 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
  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/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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  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_v27.md +259 -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_v28.md +217 -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_v29.md +270 -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_v30.md +231 -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_v31.md +281 -0
  41. package/docs/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -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//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -0
  49. 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 +45 -42
  50. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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 +6 -5
  57. 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
  58. package/dsh/cordis-patch-profile-web.example.yml +2 -2
  59. package/lib/bridge.js +9 -0
  60. package/lib/hooks.d.ts +8 -1
  61. package/lib/hooks.js +134 -50
  62. package/lib/lib/hook_audit.d.ts +55 -0
  63. package/lib/lib/hook_audit.js +208 -0
  64. package/lib/lib/mutual.js +5 -1
  65. package/lib/lib/roleplay_web.js +17 -2
  66. package/lib/lib/session_state.d.ts +56 -0
  67. package/lib/lib/session_state.js +124 -0
  68. package/lib/tools.js +12 -1
  69. package/md_cg/auditview.py +248 -0
  70. package/md_cg/bench_e2e_judge.py +4 -2
  71. package/md_cg/branches.py +29 -3
  72. package/md_cg/consistency.py +38 -13
  73. package/md_cg/datapath.py +38 -11
  74. package/md_cg/forgetting.py +15 -2
  75. package/md_cg/freshness.py +32 -11
  76. package/md_cg/ghostref.py +130 -0
  77. package/md_cg/judgment_manifest.py +7 -0
  78. package/md_cg/lifecycle.py +38 -0
  79. package/md_cg/mcp_server.py +122 -12
  80. package/md_cg/mdcg.py +4923 -4822
  81. package/md_cg/mdcos.py +109 -21
  82. package/md_cg/nodefile.py +77 -13
  83. package/md_cg/readcache.py +29 -4
  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/state_slots.py +221 -0
  89. package/md_cg/stg.py +68 -3
  90. package/md_cg/sustain.py +1436 -1436
  91. package/md_cg/test_auditview.py +277 -0
  92. package/md_cg/test_autonomy_modes.py +2 -1
  93. package/md_cg/test_boot_openblas_guard.py +197 -0
  94. package/md_cg/test_ccg_form_parity.py +95 -4
  95. package/md_cg/test_cons200_scan_selection.py +4 -2
  96. package/md_cg/test_datapath_device_name.py +62 -3
  97. package/md_cg/test_generation_guard.py +5 -2
  98. package/md_cg/test_ghostref.py +182 -0
  99. package/md_cg/test_interop.py +1 -1
  100. package/md_cg/test_issue52_scan_condition_first.py +1 -1
  101. package/md_cg/test_lifecycle_retire_leak.py +408 -0
  102. package/md_cg/test_lock.py +16 -7
  103. package/md_cg/test_m3_h9_semantic_guard.py +1 -1
  104. package/md_cg/test_n230_dirty_replay.py +375 -375
  105. package/md_cg/test_n238_body_text_parity.py +156 -0
  106. package/md_cg/test_n276_probe_hoist.py +199 -0
  107. package/md_cg/test_p4_freshness.py +95 -1
  108. package/md_cg/test_policy_required_ccg.py +3 -1
  109. package/md_cg/test_read_face_semantics.py +6 -3
  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_event_op.py +514 -0
  114. package/md_cg/test_state_events.py +178 -0
  115. package/md_cg/test_state_slots.py +691 -0
  116. package/md_cg/test_time_core_lint.py +8 -1
  117. package/md_cg/test_writepipe.py +8 -5
  118. package/md_cg/tokens.py +18 -2
  119. package/md_cg/writepipe.py +76 -0
  120. package/package.json +2 -2
  121. package/skills/plugin.json +1 -1
  122. package/src/bridge.ts +9 -0
  123. package/src/hooks.ts +599 -517
  124. package/src/lib/hook_audit.ts +243 -0
  125. package/src/lib/mutual.ts +5 -1
  126. package/src/lib/roleplay_web.ts +17 -2
  127. package/src/lib/session_state.ts +127 -0
  128. package/src/tools.ts +12 -1
  129. package/zcode/AGENTS.md +6 -6
@@ -0,0 +1,408 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_lifecycle_retire_leak · 退役纪律守卫(archived 不参与默认检索)
3
+
4
+ 现场(编排侧复现的对外缺陷):写 v1 → 写矛盾 v2 → 把 v2 走**合法迁移表**
5
+ 降到 `archived` → 查。`frontmatter.lifecycle_state` 与索引条目都已正确为
6
+ `archived`,而 `cg.search` 与 `stg.timeline` **仍返回 v2**——`lifecycle.py`
7
+ 宣称「archived 已归档:不再参与默认检索」,而**读面从未实现该过滤**
8
+ (标称 vs 实现缺口)。
9
+
10
+ 判据单点:`lifecycle.is_archived`(fail-open:缺键/非法值 → active)。
11
+ 消费面(三个,判据只此一份):①cg **检索面** `MdCGOS._candidates`;
12
+ ②stg 扫描面 `stg._scan_one`(`_scan` 的全量遍历与索引子集两条分支共用的
13
+ 逐条实现单点);③会话续接**注入面** `MdCGOS.session_recall` 的四个取数段
14
+ (会话要点 `_session_notes` + 活跃目标 `goals` + 任务台账 `tasks` +
15
+ 未解问题 `unresolved`;`goals`/`tasks` 经 `_retired_by_id` 按 id 回查索引)。
16
+
17
+ 覆盖(R1–R4):
18
+ R1 复现序列:写 v1/v2 → demoted → archived → search / recall / search_rrf /
19
+ `_candidates` 与 timeline / anchors / consistency 候选面(scanned 读数)
20
+ 均不返回 v2,v1 照常返回;R1.11 图扩展/因果路不绕过;R1.12 会话续接
21
+ 注入面(要点摘要 + 未解问题正文);R1.13 续接包台账段(活跃目标 + 任务卡);
22
+ R2 不误剔:converged 与 demoted(**降权轴**)仍返回;**缺 lifecycle 键**
23
+ 节点照常返回(fail-open);
24
+ R3 退役不删除:archived 期间 `cg.get` 可得、`op=audit` 面可得;
25
+ `set_state(archived→active)` 后读面(含续接面)重新可见;
26
+ R4 面完整性:read/recall/search/route 全链(含 `mcp_server._cg_call`
27
+ 抽查一层)+ `op=session` 全链 archived 不出现。
28
+
29
+ 运行:python -X utf8 -m md_cg.test_lifecycle_retire_leak
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import os
34
+ import shutil
35
+ import tempfile
36
+
37
+ from . import lifecycle, stg
38
+ from . import mcp_server
39
+ from . import tasks as _tasks
40
+ from .mdcos import MdCGOS
41
+
42
+ _root = tempfile.mkdtemp(prefix="mdcg_retire_")
43
+ cg = MdCGOS(_root)
44
+
45
+ _ok = 0
46
+ _fail = []
47
+
48
+ #: 查询串(固定字面量,逐字出现在各节点正文里——词面命中不靠猜测)。
49
+ Q = "退役泄漏守卫查询槽位"
50
+
51
+
52
+ def _body(tag, verdict):
53
+ """CCG 六要素完整的节点正文(缺要素会被判 BLINDSPOT,污染可见性读数)。"""
54
+ return ("# 功能名:退役纪律守卫·%s\n"
55
+ "# 生效条件:隔离 temp root 内执行退役纪律守卫序列时\n"
56
+ "# 子功能:①登记被测事实 ②退役后的读面/时空面可见性读数\n"
57
+ "# 执行:cg.add 一次(退役节点另经 set_state 逐级降级)\n"
58
+ "# 验证方式:读面 search/recall/search_rrf 与 stg 面的可见性断言\n"
59
+ "# 不适用条件:在役真实记忆库内执行\n"
60
+ "正文:%s = %s" % (tag, Q, verdict))
61
+
62
+
63
+ def check(name, cond, detail=""):
64
+ global _ok
65
+ if cond:
66
+ _ok += 1
67
+ print("[ok] " + name)
68
+ else:
69
+ _fail.append(name)
70
+ print("[FAIL] %s · %s" % (name, str(detail)[:240]))
71
+
72
+
73
+ def _search_ids(q=Q):
74
+ res, _meta = cg.search(q, k=50, record=False)
75
+ return {str(n.get("id")) for n, _s, _q in res}
76
+
77
+
78
+ def _rrf_ids(q=Q):
79
+ """RRF 路(`cg.recall` 缺省走的就是它)——含图扩展/因果路。"""
80
+ res, _meta = cg.search_rrf(q, k=50, record=False)
81
+ return {str(r[0].get("id")) for r in res}
82
+
83
+
84
+ def _recall_ids(q=Q):
85
+ pack = cg.recall(q, budget_tokens=4000, k=50)
86
+ return {str(it.get("id")) for it in (pack.get("pack") or [])}
87
+
88
+
89
+ def _entry_nid(e):
90
+ """索引条目 → 节点 id:条目**不带 id 键**(只带 path),按文件名取
91
+ (与 `MdCGOS._path_graph` 的 `e["path"].split("/")[-1][:-3]` 同口径)。"""
92
+ p = str((e or {}).get("path") or "")
93
+ return p.replace("\\", "/").split("/")[-1][:-3]
94
+
95
+
96
+ def _cand_ids():
97
+ """读面单点 `_candidates` 的候选池 id 集合(cg 全部读 op 的公共候选面)。"""
98
+ return {_entry_nid(e) for e in cg._candidates()}
99
+
100
+
101
+ def _tl():
102
+ return stg.timeline(cg, limit=10 ** 6, max_scan=10 ** 9)
103
+
104
+
105
+ def _an():
106
+ return stg.anchors(cg, time_window=[1.0, 10.0 ** 11], limit=10 ** 6,
107
+ max_scan=10 ** 9)
108
+
109
+
110
+ def _co():
111
+ return stg.consistency(cg, limit=10 ** 6, max_scan=10 ** 9)
112
+
113
+
114
+ def _tl_ids(rep=None):
115
+ rep = _tl() if rep is None else rep
116
+ return {str(it.get("id")) for it in (rep.get("items") or [])}
117
+
118
+
119
+ def _an_ids(rep=None):
120
+ rep = _an() if rep is None else rep
121
+ return {str(it.get("id")) for it in (rep.get("items") or [])}
122
+
123
+
124
+ def _scan_subset_ids():
125
+ """索引子集分支(`_scan(nodes=...)`):第 3 层 flag 开时的取数面。"""
126
+ pairs = list((cg.index.get("nodes") or {}).items())
127
+ return {str(n.get("id")) for n in stg._scan(cg, nodes=pairs)}
128
+
129
+
130
+ def _fm(nid):
131
+ return ((cg.get(nid) or {}).get("frontmatter")) or {}
132
+
133
+
134
+ def _idx(nid):
135
+ return (cg.index.get("nodes") or {}).get(nid) or {}
136
+
137
+
138
+ # ------------------------------------------------------------ R1 复现序列
139
+ n1 = cg.add("rl_v1", _body("旧事实 v1", "甲地(初始登记)"), layer="knowledge",
140
+ verification_basis="test", importance=0.4)
141
+ n2 = cg.add("rl_v2", _body("新事实 v2(与 v1 矛盾)", "乙地(订正:甲地作废)"),
142
+ layer="knowledge", verification_basis="test", importance=0.4)
143
+
144
+ check("R1.0 写入后两节点都在读面与时空面(前提成立,非空断言)",
145
+ n1 in _search_ids() and n2 in _search_ids()
146
+ and n1 in _tl_ids() and n2 in _tl_ids(),
147
+ (sorted(_search_ids()), sorted(_tl_ids())))
148
+
149
+ scanned_tl_before = _tl().get("scanned")
150
+ scanned_co_before = _co().get("scanned")
151
+
152
+ _r1 = cg.set_state(n2, "demoted", reason="守卫:先降权", actor="test")
153
+ _r2 = cg.set_state(n2, "archived", reason="守卫:归档", actor="test")
154
+ check("R1.1 退役走合法迁移表(demoted→archived)且状态落盘为 archived",
155
+ _r1.get("ok") and _r2.get("ok")
156
+ and lifecycle.state_of(_fm(n2)) == "archived", (_r1, _r2, _fm(n2)))
157
+
158
+ check("R1.2 退役后 cg 读面 search 不返回 v2、v1 照常返回",
159
+ n2 not in _search_ids() and n1 in _search_ids(), sorted(_search_ids()))
160
+ check("R1.3 退役后 cg 读面 recall(预算装包,走 RRF)不返回 v2、v1 照常返回",
161
+ n2 not in _recall_ids() and n1 in _recall_ids(), sorted(_recall_ids()))
162
+ check("R1.4 退役后 cg 读面 search_rrf 不返回 v2、v1 照常返回",
163
+ n2 not in _rrf_ids() and n1 in _rrf_ids(), sorted(_rrf_ids()))
164
+ check("R1.5 退役后 _candidates 候选池不含 v2(读面单点)、含 v1",
165
+ n2 not in _cand_ids() and n1 in _cand_ids(), sorted(_cand_ids()))
166
+ check("R1.6 退役后 stg.timeline 不返回 v2、v1 照常返回",
167
+ n2 not in _tl_ids() and n1 in _tl_ids(), sorted(_tl_ids()))
168
+ check("R1.7 退役后 stg.anchors 不返回 v2、v1 照常返回",
169
+ n2 not in _an_ids() and n1 in _an_ids(), sorted(_an_ids()))
170
+
171
+ scanned_tl_after = _tl().get("scanned")
172
+ scanned_co_after = _co().get("scanned")
173
+ check("R1.8 stg 候选遍历读数随退役下降(timeline scanned 少 1)",
174
+ scanned_tl_after == scanned_tl_before - 1,
175
+ (scanned_tl_before, scanned_tl_after))
176
+ check("R1.9 consistency 候选面同点覆盖(scanned 少 1——archived 不进候选)",
177
+ scanned_co_after == scanned_co_before - 1,
178
+ (scanned_co_before, scanned_co_after))
179
+ check("R1.10 索引子集分支(_scan(nodes=...) 第 3 层取数面)逐位一致——不因取数面漏过滤",
180
+ n2 not in _scan_subset_ids() and n1 in _scan_subset_ids(),
181
+ sorted(_scan_subset_ids()))
182
+
183
+ # ------------------------------------------------------------ R2 不误剔
184
+ n3 = cg.add("rl_converged", _body("已定型 converged", "丙地"),
185
+ layer="knowledge", verification_basis="test", importance=0.4)
186
+ n4 = cg.add("rl_demoted", _body("已降权 demoted", "丁地"),
187
+ layer="knowledge", verification_basis="test", importance=0.4)
188
+ cg.set_state(n3, "converged", reason="守卫:定型", actor="test")
189
+ cg.set_state(n4, "demoted", reason="守卫:降权", actor="test")
190
+
191
+ check("R2.1 converged(已定型=降权轴)仍参与默认检索",
192
+ lifecycle.state_of(_fm(n3)) == "converged"
193
+ and n3 in _cand_ids() and n3 in _search_ids() and n3 in _tl_ids(),
194
+ (_fm(n3).get(lifecycle.STATE_FIELD), sorted(_search_ids())))
195
+ check("R2.2 demoted(已降权=降权轴)仍参与默认检索",
196
+ lifecycle.state_of(_fm(n4)) == "demoted"
197
+ and n4 in _cand_ids() and n4 in _search_ids() and n4 in _tl_ids(),
198
+ (_fm(n4).get(lifecycle.STATE_FIELD), sorted(_search_ids())))
199
+
200
+ # 缺 lifecycle 键(旧库形态):盘上 fm 与索引条目都摘掉该键
201
+ n5 = cg.add("rl_legacy", _body("旧库缺键形态", "戊地"), layer="knowledge",
202
+ verification_basis="test", importance=0.4)
203
+ _node5 = cg.get(n5)
204
+ _fm5 = dict(_node5["frontmatter"])
205
+ _fm5.pop(lifecycle.STATE_FIELD, None)
206
+ cg._write_node(n5, os.path.join(cg.root, _node5["path"]), _fm5,
207
+ _node5.get("content") or "")
208
+ (cg.index.get("nodes") or {}).get(n5, {}).pop(lifecycle.STATE_FIELD, None)
209
+ check("R2.3 旧库形态成立(fm 与索引条目均无 lifecycle 键)",
210
+ lifecycle.STATE_FIELD not in _fm(n5)
211
+ and lifecycle.STATE_FIELD not in _idx(n5), (_fm(n5), _idx(n5)))
212
+ check("R2.4 fail-open:缺 lifecycle 键的节点照常参与默认检索(存量零迁移)",
213
+ n5 in _cand_ids() and n5 in _search_ids() and n5 in _tl_ids(),
214
+ sorted(_search_ids()))
215
+ check("R2.5 is_archived 判据单点:缺键/非法值/非 dict → False;archived → True",
216
+ lifecycle.is_archived({}) is False
217
+ and lifecycle.is_archived({lifecycle.STATE_FIELD: "frozen"}) is False
218
+ and lifecycle.is_archived(None) is False
219
+ and lifecycle.is_archived({lifecycle.STATE_FIELD: "archived"}) is True
220
+ and lifecycle.is_archived(_idx(n2)) is True
221
+ and lifecycle.is_archived({lifecycle.STATE_FIELD: "demoted"}) is False)
222
+
223
+ # ------------------------------------------------------------ R3 退役不删除
224
+ _node2 = cg.get(n2) or {}
225
+ check("R3.1 archived 期间 cg.get 直读可得(frontmatter + 正文 + 文件仍在)",
226
+ bool(_node2) and bool(_node2.get("content"))
227
+ and os.path.exists(os.path.join(cg.root, _node2.get("path") or "")),
228
+ _node2)
229
+ _aud = mcp_server._cg_call(cg, {"op": "audit", "node_id": n2})
230
+ check("R3.2 archived 期间 op=audit 审计面照常可达(不被退役闸拦住)",
231
+ isinstance(_aud, dict) and not _aud.get("error"), _aud)
232
+ check("R3.3 显式恢复 archived→active 走合法迁移表且成功",
233
+ cg.set_state(n2, "active", reason="守卫:显式恢复",
234
+ actor="test").get("ok")
235
+ and lifecycle.state_of(_fm(n2)) == "active", _fm(n2))
236
+ check("R3.4 恢复后读面/时空面重新可见(退役可逆)",
237
+ n2 in _cand_ids() and n2 in _search_ids() and n2 in _tl_ids()
238
+ and n2 in _an_ids(), (sorted(_search_ids()), sorted(_tl_ids())))
239
+ # 复原到 archived,供 R4 用
240
+ cg.set_state(n2, "demoted", reason="守卫:复测降权", actor="test")
241
+ cg.set_state(n2, "archived", reason="守卫:复测归档", actor="test")
242
+
243
+ # ------------------------------------------------------------ R4 面完整性
244
+ _call_read = mcp_server._cg_call(cg, {"op": "read", "query": Q, "k": 50})
245
+ _call_recall = mcp_server._cg_call(cg, {"op": "read", "query": Q,
246
+ "budget_tokens": 4000, "k": 50})
247
+ _call_route = mcp_server._cg_call(cg, {"op": "route", "intent": Q, "k": 50})
248
+ _read_ids = {str(r.get("node", {}).get("id"))
249
+ for r in (_call_read.get("results") or [])}
250
+ _pack_ids = {str(it.get("id")) for it in (_call_recall.get("pack") or [])}
251
+ _route_ids = {str(k.get("id")) for k in (_call_route.get("knowledge") or [])}
252
+ check("R4.1 op=read(检索分支)全链:archived 不出现、v1 出现",
253
+ n2 not in _read_ids and n1 in _read_ids,
254
+ (_call_read.get("meta"), sorted(_read_ids)))
255
+ check("R4.2 op=read(预算召回分支→recall/RRF)全链:archived 不出现、v1 出现",
256
+ n2 not in _pack_ids and n1 in _pack_ids,
257
+ (_call_recall.get("meta"), sorted(_pack_ids)))
258
+ check("R4.3 op=route 全链:archived 不出现、v1 出现",
259
+ n2 not in _route_ids and n1 in _route_ids,
260
+ (_call_route.get("meta"), sorted(_route_ids)))
261
+ check("R4.4 直读面(read+node_id 分支)不受限:archived 仍可显式取回",
262
+ mcp_server._cg_call(cg, {"op": "read", "node_id": n2}) is not None)
263
+ check("R4.5 四条读面口径同源(都经 _candidates 单点)",
264
+ n2 not in _cand_ids() and n2 not in _search_ids()
265
+ and n2 not in _rrf_ids() and n2 not in _recall_ids())
266
+
267
+ # ------------------------------------------------------------ R1.11 图扩展/因果路不绕过读面
268
+ # `MdCGSecure.search_rrf` 的二次过滤注释宣称「图扩展会绕过 _candidates」——
269
+ # 本腿坐实:graph/chain 两路的扩散目标**受 entries 约束**(`_path_graph` 的
270
+ # by_id、`_path_chain` 的 allowed 都取自 `_candidates`),故 archived 目标
271
+ # 进不了融合池。判据分两段:先证「扩展真的能到达目标」(非空断言,反空转),
272
+ # 再证「目标退役后到达不了」。
273
+ g1 = cg.add("rl_graph_v1", _body("图扩展种子", "己地"), layer="knowledge",
274
+ verification_basis="test", importance=0.4,
275
+ edges=[{"target": "rl_graph_v2", "type": "derived_from"}])
276
+ g2 = cg.add("rl_graph_v2", _body("图扩展目标(待退役)", "庚地"),
277
+ layer="knowledge", verification_basis="test", importance=0.4)
278
+
279
+
280
+ def _graph_ids():
281
+ res, _meta = cg.search_rrf(Q, k=50, record=False,
282
+ paths=("lexical", "graph", "chain"))
283
+ return {str(r[0].get("id")) for r in res}
284
+
285
+
286
+ check("R1.11a 前提:graph/chain 扩散确实能到达边目标(非空断言,防本腿空转)",
287
+ g1 in _graph_ids() and g2 in _graph_ids(), sorted(_graph_ids()))
288
+ cg.set_state(g2, "demoted", reason="守卫:图路降权", actor="test")
289
+ cg.set_state(g2, "archived", reason="守卫:图路归档", actor="test")
290
+ check("R1.11b 目标退役后图扩展路不返回它、种子照常返回(图扩展不绕过 _candidates)",
291
+ g2 not in _graph_ids() and g1 in _graph_ids(), sorted(_graph_ids()))
292
+
293
+ # ------------------------------------------------------------ R1.12 会话续接注入面(复核打回补)
294
+ # 独立复核 2026-10-06:`cg(op=session, action=recall)` 自持取数面(不经 _candidates),
295
+ # 修前把 archived 节点的要点摘要与未解问题正文照旧回给调用方——同属「退役不参与
296
+ # 默认注入」的遗漏出口。本腿两段:先证在役时确能回出(非空断言,反空转),再证
297
+ # 退役后回不出;并留一条在役对照(防「一刀切全清空」的假绿)。
298
+ _SN = "sess_retire_guard"
299
+
300
+
301
+ def _sess_pack(session=_SN):
302
+ return cg.session_recall(session=session, limit=50, recent_limit=50,
303
+ budget_tokens=20000)
304
+
305
+
306
+ def _sess_ids(p=None):
307
+ p = _sess_pack() if p is None else p
308
+ ids = {str(n.get("id")) for n in (p.get("notes") or [])}
309
+ ids |= {str(u.get("id")) for u in (p.get("unresolved") or [])}
310
+ ids |= {str(g.get("id")) for g in (p.get("goals") or [])}
311
+ ids |= {str(t.get("id")) for t in ((p.get("tasks") or {}).get("active") or [])}
312
+ ids |= {str(t.get("id")) for t in ((p.get("tasks") or {}).get("done") or [])}
313
+ return ids
314
+
315
+
316
+ _note1 = cg.session_note("守卫会话要点甲:退役后不应再回给调用方",
317
+ session=_SN, importance=0.6)["id"]
318
+ _note2 = cg.session_note("守卫会话要点乙:仍在役(对照,须照常回出)",
319
+ session=_SN, importance=0.6)["id"]
320
+ _unr1 = cg.add_unresolved("守卫未解问题:退役后是否仍被注入?",
321
+ known_clues="守卫线索", goal="守卫目标")
322
+ check("R1.12a 前提:会话要点与未解问题都确在续接包里(非空断言,防本腿空转)",
323
+ {_note1, _note2, _unr1} <= _sess_ids(), sorted(_sess_ids()))
324
+ for _nid in (_note1, _unr1):
325
+ cg.set_state(_nid, "demoted", reason="守卫:续接面降权", actor="test")
326
+ cg.set_state(_nid, "archived", reason="守卫:续接面归档", actor="test")
327
+ check("R1.12b 退役后续接包不再回该要点与未解问题(注入面剔除);在役对照仍在",
328
+ _sess_ids().isdisjoint({_note1, _unr1}) and _note2 in _sess_ids(),
329
+ sorted(_sess_ids()))
330
+ cg.set_state(_note1, "active", reason="守卫:续接面恢复", actor="test")
331
+ check("R3.5 恢复后续接包重新回该要点(退役可逆,同面)",
332
+ _note1 in _sess_ids(), sorted(_sess_ids()))
333
+ cg.set_state(_note1, "demoted", reason="守卫:复测降权", actor="test")
334
+ cg.set_state(_note1, "archived", reason="守卫:复测归档", actor="test")
335
+
336
+ # ------------------------------------------------- R1.13 续接包台账段(复核打回补)
337
+ # 独立复核 2026-10-06 第二轮回执:同一续接包中 notes/未解段已剔而 goals/tasks 段
338
+ # 仍把 archived 节点注回调用方(与本文件 §5 判据字面冲突)。本腿三段:前提(真能
339
+ # 带回)→ 退役后剔除(含 `*_total` 口径)→ 恢复后重新带回;并留在役对照防假绿。
340
+ _goal1 = cg.add_goal("守卫目标甲:退役后不应再进续接包", priority=0.4)
341
+ _goal2 = cg.add_goal("守卫目标乙:仍在役(对照)", priority=0.3)
342
+ _task1 = _tasks.upsert(cg, "守卫任务卡甲", plan="退役后不应再进续接包")["node_id"]
343
+ _task2 = _tasks.upsert(cg, "守卫任务卡乙", plan="仍在役(对照)")["node_id"]
344
+
345
+
346
+ def _pack_ledger(p=None):
347
+ p = _sess_pack() if p is None else p
348
+ ids = {str(g.get("id")) for g in (p.get("goals") or [])}
349
+ ids |= {str(t.get("id"))
350
+ for t in ((p.get("tasks") or {}).get("active") or [])}
351
+ return ids, (p.get("tasks") or {})
352
+
353
+
354
+ check("R1.13a 前提:活跃目标与任务卡都确在续接包里(非空断言,防本腿空转)",
355
+ {_goal1, _goal2, _task1, _task2} <= _pack_ledger()[0],
356
+ sorted(_pack_ledger()[0]))
357
+ # 任务卡以 importance=0.8 建(tasks.DEFAULT_IMPORTANCE)⇒ 自动 protected——
358
+ # 受保护节点降级须显式 override=True(lifecycle 的「保护 = 不可遗忘」);这里
359
+ # 顺带把退役结果断言出来,避免「退役没成功却当成已退役」的假绿。
360
+ _led_ret = []
361
+ for _nid in (_goal1, _task1):
362
+ _led_ret.append(cg.set_state(_nid, "demoted", reason="守卫:台账段降权",
363
+ actor="test", override=True))
364
+ _led_ret.append(cg.set_state(_nid, "archived", reason="守卫:台账段归档",
365
+ actor="test", override=True))
366
+ check("R1.13b0 台账两节点退役成功(任务卡默认 protected,降级走 override=True)",
367
+ all(r.get("ok") for r in _led_ret)
368
+ and all(lifecycle.state_of(_fm(_n)) == "archived"
369
+ for _n in (_goal1, _task1)), _led_ret)
370
+ _led_ids, _led_tasks = _pack_ledger()
371
+ check("R1.13b 退役后台账段不再回该目标与任务卡;在役对照仍在;total 取剔除后计数",
372
+ _led_ids.isdisjoint({_goal1, _task1})
373
+ and {_goal2, _task2} <= _led_ids
374
+ and _led_tasks.get("active_total") == 1
375
+ and _led_tasks.get("done_total") == 0,
376
+ (sorted(_led_ids), _led_tasks.get("active_total"),
377
+ _led_tasks.get("done_total")))
378
+ check("R1.13c 管理面不受限对照:cg(op=goal/task, action=list) 仍可见 archived 台账",
379
+ any(g.get("id") == _goal1 for g in
380
+ (mcp_server._cg_call(cg, {"op": "goal", "action": "list"})
381
+ .get("goals") or []))
382
+ and any(t.get("id") == _task1 for t in
383
+ (mcp_server._cg_call(cg, {"op": "task", "action": "list"})
384
+ .get("tasks") or [])),
385
+ mcp_server._cg_call(cg, {"op": "goal", "action": "list"}))
386
+ cg.set_state(_goal1, "active", reason="守卫:台账段恢复", actor="test")
387
+ check("R3.6 恢复后台账段重新回该目标(退役可逆,同面)",
388
+ _goal1 in _pack_ledger()[0], sorted(_pack_ledger()[0]))
389
+ cg.set_state(_goal1, "demoted", reason="守卫:复测降权", actor="test")
390
+ cg.set_state(_goal1, "archived", reason="守卫:复测归档", actor="test")
391
+ # ------------------------------------------------------------ R4.6 会话续接面全链抽查
392
+ _call_sess = mcp_server._cg_call(cg, {"op": "session", "action": "recall",
393
+ "session": _SN, "limit": 50,
394
+ "recent_limit": 50,
395
+ "budget_tokens": 20000})
396
+ _sess_call_ids = {str(n.get("id")) for n in (_call_sess.get("notes") or [])}
397
+ _sess_call_ids |= {str(u.get("id"))
398
+ for u in (_call_sess.get("unresolved") or [])}
399
+ check("R4.6 op=session(action=recall)全链:archived 要点/未解问题不出现、在役对照出现",
400
+ _sess_call_ids.isdisjoint({_note1, _unr1}) and _note2 in _sess_call_ids,
401
+ sorted(_sess_call_ids))
402
+
403
+ print("=" * 58)
404
+ print("test_lifecycle_retire_leak: %d 通过 / %d 失败" % (_ok, len(_fail)))
405
+ if _fail:
406
+ print("失败项:" + "、".join(_fail))
407
+ shutil.rmtree(cg.root, ignore_errors=True)
408
+ raise SystemExit(1 if _fail else 0)
@@ -6,33 +6,42 @@ N 个进程各做 M 次「锁内 读-加一-写」。若锁真互斥,最终值
6
6
  import os
7
7
  import sys
8
8
  import subprocess
9
+ import tempfile
9
10
 
10
11
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
12
  from md_cg.fsutil import FileLock, atomic_write
12
13
 
13
- TARGET = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_locktest.txt")
14
+ # 落点改**系统临时目录 + PID 唯一化**(2026-10-05 复核建议 6):原落包目录
15
+ # `md_cg/_locktest.txt`——并发跑测试/共享仓库检出(copytree、CI 并行)时
16
+ # 瞬态文件被无余量扫描撞上(复核两次实测 WinError 2),且不被 .gitignore
17
+ # 覆盖易误提交。路径在**父进程**唯一化后经 argv 传给 worker 子进程——
18
+ # 子进程是独立 PID,必须显式继承同一路径(v1 初改曾按 PID 各自计算 ⇒
19
+ # 父子读不同文件、计数归零的实测教训)。判据不变:N 进程对同一文件加锁。
20
+ TARGET = os.path.join(tempfile.gettempdir(),
21
+ "mdcg_locktest_%d.txt" % os.getpid())
14
22
 
15
23
 
16
- def worker(m):
24
+ def worker(m, target):
17
25
  for _ in range(m):
18
- with FileLock(TARGET) as lk:
26
+ with FileLock(target) as lk:
19
27
  if not lk.acquired:
20
28
  print("LOCK_TIMEOUT", file=sys.stderr)
21
29
  try:
22
- with open(TARGET, encoding="utf-8") as f:
30
+ with open(target, encoding="utf-8") as f:
23
31
  v = int(f.read().strip() or 0)
24
32
  except (OSError, ValueError):
25
33
  v = 0
26
- atomic_write(TARGET, str(v + 1))
34
+ atomic_write(target, str(v + 1))
27
35
 
28
36
 
29
37
  if __name__ == "__main__":
30
38
  if len(sys.argv) > 1 and sys.argv[1] == "--worker":
31
- worker(int(sys.argv[2]))
39
+ worker(int(sys.argv[2]), sys.argv[3])
32
40
  sys.exit(0)
33
41
  N, M = 6, 50
34
42
  atomic_write(TARGET, "0")
35
- ps = [subprocess.Popen([sys.executable, os.path.abspath(__file__), "--worker", str(M)])
43
+ ps = [subprocess.Popen([sys.executable, os.path.abspath(__file__), "--worker",
44
+ str(M), TARGET])
36
45
  for _ in range(N)]
37
46
  [p.wait() for p in ps]
38
47
  with open(TARGET, encoding="utf-8") as f:
@@ -4,7 +4,7 @@
4
4
  # 生效条件:md_cg/routing.py 的 `bucket_health(counts, total_nodes=None)`、md_cg/mdcg.py
5
5
  # 的 `MdCG.health` 呼叫点(把节点总数传进判据)、md_cg/protect.py 的 `mark`
6
6
  # 三处修复在位时成立;沙箱条件:所有库根一律 tempfile.mkdtemp,绝不触在役库
7
- # (D:/program/dsh-memory-main 的 in-service 库)与在役服务;本文件只读源码,
7
+ # (<仓根> 的 in-service 库)与在役服务;本文件只读源码,
8
8
  # 变异时只写目标文件且在 finally 里按原始字节复原。
9
9
  # 子功能:钉的是**消费方看得见的目标语义**,不是站点文本——
10
10
  # A M3 假警报方向:「从未分区」的库(单桶)不得被读成「分区退化」——读数里不得