@furongjun1999/dsh-memory 0.4.8 → 0.4.10

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 (232) hide show
  1. package/README.md +54 -26
  2. package/codebuddy/CODEBUDDY.md +196 -195
  3. package/codebuddy/README.md +13 -1
  4. package/codebuddy/mcp.json +9 -0
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
  6. package/docs/README.md +1 -1
  7. package/docs/discipline/harnesses.yaml +18 -7
  8. package/docs/discipline/templates/full.md.tmpl +4 -3
  9. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  10. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  11. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  12. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  13. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  14. package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
  15. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
  16. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -0
  17. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
  18. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
  19. 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 +503 -0
  20. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
  21. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  22. package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
  23. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -0
  24. 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 +47 -45
  25. 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 → 206_v0.5.md} +92 -4
  26. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
  27. package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
  28. package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
  29. 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 +2 -2
  30. 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
  31. 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 +2 -2
  32. 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 +434 -433
  33. package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
  34. package/dsh/README.md +33 -0
  35. package/dsh/cordis.yml.example +13 -7
  36. package/dsh/hive-mcp-probe.mjs +94 -0
  37. package/dsh/hive-mcp.example.yml +62 -0
  38. package/dsh/update-lingshu.bat +11 -0
  39. package/dsh/update-lingshu.ps1 +337 -0
  40. package/lib/hooks.d.ts +3 -0
  41. package/lib/hooks.js +17 -23
  42. package/lib/index.d.ts +4 -2
  43. package/lib/index.js +29 -6
  44. package/lib/lib/datapath.d.ts +76 -1
  45. package/lib/lib/datapath.js +199 -13
  46. package/lib/lib/mdcg_client.d.ts +40 -3
  47. package/lib/lib/mdcg_client.js +46 -22
  48. package/lib/lib/mutual.js +4 -4
  49. package/lib/lib/token_store.js +4 -5
  50. package/md_cg/audit.py +17 -2
  51. package/md_cg/autonomy.py +86 -15
  52. package/md_cg/backfill.py +36 -1
  53. package/md_cg/backfill_bigdomain.py +34 -0
  54. package/md_cg/bench6_arms.py +28 -1
  55. package/md_cg/bench6_common.py +10 -1
  56. package/md_cg/bench6_competitors.py +6 -1
  57. package/md_cg/bench_axis_domain.py +9 -1
  58. package/md_cg/bench_blind_comp.py +7 -1
  59. package/md_cg/bench_en_atoms_public.py +9 -0
  60. package/md_cg/bench_governance.py +348 -0
  61. package/md_cg/bench_lme_zh.py +16 -1
  62. package/md_cg/bench_locomo.py +2 -1
  63. package/md_cg/bench_locomo_zh.py +16 -1
  64. package/md_cg/bench_locomo_zh_public.py +4 -1
  65. package/md_cg/bench_longmem.py +2 -1
  66. package/md_cg/bench_membench.py +27 -1
  67. package/md_cg/bench_p0.py +4 -1
  68. package/md_cg/bench_progressive.py +13 -1
  69. package/md_cg/bench_role_views.py +238 -0
  70. package/md_cg/bench_task_ab.py +8 -1
  71. package/md_cg/bench_task_ab_llm.py +13 -1
  72. package/md_cg/bench_unified_en.py +6 -1
  73. package/md_cg/bench_zh_mad.py +20 -1
  74. package/md_cg/blindspot_tickets.py +123 -0
  75. package/md_cg/branches.py +12 -1
  76. package/md_cg/build_postings.py +73 -0
  77. package/md_cg/ccgc.py +67 -2
  78. package/md_cg/census.py +5 -1
  79. package/md_cg/chain.py +24 -3
  80. package/md_cg/codeindex.py +134 -17
  81. package/md_cg/coldverify.py +265 -0
  82. package/md_cg/comment_gate.py +338 -0
  83. package/md_cg/cond_compose.py +190 -0
  84. package/md_cg/cond_facts.py +155 -0
  85. package/md_cg/cond_template.json +107 -0
  86. package/md_cg/condition_anchor.py +143 -0
  87. package/md_cg/conformance.py +69 -4
  88. package/md_cg/consistency.py +24 -1
  89. package/md_cg/consolidate.py +53 -2
  90. package/md_cg/corpus.py +4 -0
  91. package/md_cg/crosscheck.py +42 -2
  92. package/md_cg/crypto.py +35 -1
  93. package/md_cg/d_meta.py +310 -0
  94. package/md_cg/datapath.py +201 -26
  95. package/md_cg/docindex.py +122 -1
  96. package/md_cg/eval_common.py +29 -1
  97. package/md_cg/evidence.py +27 -1
  98. package/md_cg/evolution.py +21 -1
  99. package/md_cg/export.py +11 -1
  100. package/md_cg/forgetting.py +23 -1
  101. package/md_cg/fsutil.py +18 -1
  102. package/md_cg/hotcache.py +214 -0
  103. package/md_cg/hyperedge.py +251 -0
  104. package/md_cg/identity.py +18 -1
  105. package/md_cg/insight.py +17 -1
  106. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  107. package/md_cg/lexicon/build_standard_en.py +171 -168
  108. package/md_cg/lexicon/expand_en_zh.py +6 -0
  109. package/md_cg/lifecycle.py +12 -1
  110. package/md_cg/linkref.py +281 -0
  111. package/md_cg/links.py +29 -1
  112. package/md_cg/mcp_server.py +362 -43
  113. package/md_cg/md_whitebox.py +53 -1
  114. package/md_cg/mdcg.py +1003 -27
  115. package/md_cg/mdcos.py +558 -36
  116. package/md_cg/metacognition.py +37 -2
  117. package/md_cg/migrate.py +4 -0
  118. package/md_cg/migrate_aeis.py +221 -213
  119. package/md_cg/migrate_roleplay.py +8 -0
  120. package/md_cg/migrate_wisdom_graph.py +14 -1
  121. package/md_cg/mreview/__main__.py +3 -0
  122. package/md_cg/mreview/bundle.py +8 -0
  123. package/md_cg/mreview/candidates.py +9 -0
  124. package/md_cg/mreview/govern.py +21 -1
  125. package/md_cg/mreview/locate.py +34 -0
  126. package/md_cg/mreview/pipeline.py +29 -1
  127. package/md_cg/mreview/ruleset.py +16 -1
  128. package/md_cg/nodefile.py +233 -3
  129. package/md_cg/pooling.py +23 -1
  130. package/md_cg/postings.py +298 -0
  131. package/md_cg/predict.py +89 -9
  132. package/md_cg/progressive.py +3 -0
  133. package/md_cg/protect.py +14 -1
  134. package/md_cg/protocol.py +372 -0
  135. package/md_cg/provenance.py +262 -0
  136. package/md_cg/reach.py +453 -0
  137. package/md_cg/refindex.py +47 -2
  138. package/md_cg/refine.py +20 -1
  139. package/md_cg/roleviews.py +89 -0
  140. package/md_cg/routing.py +76 -0
  141. package/md_cg/scrub.py +63 -2
  142. package/md_cg/security.py +26 -1
  143. package/md_cg/self_state.py +64 -1
  144. package/md_cg/selfreport.py +151 -0
  145. package/md_cg/semantic/canonical.py +5 -0
  146. package/md_cg/semantic/en_normalizer.py +364 -355
  147. package/md_cg/semantic/zh_en_atoms.py +139 -136
  148. package/md_cg/signer.py +41 -1
  149. package/md_cg/sources.py +583 -547
  150. package/md_cg/statushdr.py +179 -0
  151. package/md_cg/stg.py +59 -18
  152. package/md_cg/subgraph.py +23 -0
  153. package/md_cg/sustain.py +56 -1
  154. package/md_cg/tasks.py +26 -2
  155. package/md_cg/test_autonomy.py +26 -0
  156. package/md_cg/test_bench_governance.py +102 -0
  157. package/md_cg/test_blindspot_tickets.py +166 -0
  158. package/md_cg/test_ccgc.py +10 -0
  159. package/md_cg/test_codeindex.py +338 -0
  160. package/md_cg/test_comment_gate.py +187 -0
  161. package/md_cg/test_cond_compose_anchors.py +76 -0
  162. package/md_cg/test_condition_anchor.py +82 -0
  163. package/md_cg/test_d_meta.py +412 -0
  164. package/md_cg/test_datapath_root.py +188 -0
  165. package/md_cg/test_gain_gate.py +47 -1
  166. package/md_cg/test_hot_cold.py +187 -0
  167. package/md_cg/test_hyperedge.py +245 -0
  168. package/md_cg/test_linkref.py +306 -0
  169. package/md_cg/test_md_access_parity.py +15 -3
  170. package/md_cg/test_mr_m1.py +108 -18
  171. package/md_cg/test_mr_m3.py +8 -1
  172. package/md_cg/test_p26_refindex.py +49 -20
  173. package/md_cg/test_p27_docindex.py +236 -2
  174. package/md_cg/test_p2_mcp.py +1 -1
  175. package/md_cg/test_p31_insight.py +24 -0
  176. package/md_cg/test_p44_md_whitebox.py +14 -1
  177. package/md_cg/test_protocol.py +243 -0
  178. package/md_cg/test_reach.py +378 -0
  179. package/md_cg/test_reach_keys.py +201 -0
  180. package/md_cg/test_reach_meta_exits.py +145 -0
  181. package/md_cg/test_read_clip.py +8 -4
  182. package/md_cg/test_retr_s1.py +340 -0
  183. package/md_cg/test_retr_s1b.py +209 -0
  184. package/md_cg/test_retr_s3.py +194 -0
  185. package/md_cg/test_retr_s4.py +163 -0
  186. package/md_cg/test_retr_s5.py +200 -0
  187. package/md_cg/test_retr_s6.py +157 -0
  188. package/md_cg/test_retr_s7.py +385 -0
  189. package/md_cg/test_retr_s8_time.py +316 -0
  190. package/md_cg/test_retr_s9_edges.py +286 -0
  191. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  192. package/md_cg/test_review_conformance.py +59 -2
  193. package/md_cg/test_role_views.py +354 -0
  194. package/md_cg/test_subproc_encoding.py +188 -0
  195. package/md_cg/test_trust.py +361 -0
  196. package/md_cg/test_units_poll.py +71 -0
  197. package/md_cg/test_v14_fixes.py +397 -0
  198. package/md_cg/test_validity_filter.py +280 -0
  199. package/md_cg/test_wisdom_md_store.py +7 -3
  200. package/md_cg/test_writepipe.py +5 -1
  201. package/md_cg/theory.py +16 -1
  202. package/md_cg/tokens.py +40 -8
  203. package/md_cg/tool_face.py +13 -2
  204. package/md_cg/trust.py +943 -0
  205. package/md_cg/twophase.py +12 -1
  206. package/md_cg/units.py +132 -10
  207. package/md_cg/vision_evidence.py +24 -1
  208. package/md_cg/weights.py +24 -1
  209. package/md_cg/whitebox.py +32 -1
  210. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  211. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  212. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  213. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  214. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  215. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  216. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  217. package/md_cg/writelimit.py +18 -4
  218. package/md_cg/writepipe.py +178 -7
  219. package/package.json +2 -2
  220. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  221. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  222. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  223. package/src/hooks.ts +17 -21
  224. package/src/index.ts +33 -6
  225. package/src/lib/datapath.ts +211 -13
  226. package/src/lib/mdcg_client.ts +64 -25
  227. package/src/lib/mutual.ts +411 -411
  228. package/src/lib/token_store.ts +4 -5
  229. package/zcode/AGENTS.md +196 -195
  230. package/zcode/README.md +4 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  232. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
package/md_cg/sustain.py CHANGED
@@ -80,16 +80,19 @@ _POLL = 0.2 # 循环轮询步长(常驻进程 CPU 可忽
80
80
  # 心跳
81
81
  # --------------------------------------------------------------------------
82
82
 
83
+ # 生效条件:d 为真值时返回 d,d 为 None/空串时回落 MDCG_SUSTAIN_DIR,该环境变量也未设或为空串时返回 os.path.join(os.path.expanduser("~"), ".mdcg", "sustain");
83
84
  def net_dir(d: str = None) -> str:
84
85
  """心跳戳目录:显式 → MDCG_SUSTAIN_DIR → ~/.mdcg/sustain(仓库外)。"""
85
86
  return (d or os.environ.get("MDCG_SUSTAIN_DIR")
86
87
  or os.path.join(os.path.expanduser("~"), ".mdcg", "sustain"))
87
88
 
88
89
 
90
+ # 生效条件:给定必填 name,返回 net_dir(d) 下 heartbeat.<name>.stamp 的拼接路径。
89
91
  def stamp_path(name: str, d: str = None) -> str:
90
92
  return os.path.join(net_dir(d), f"heartbeat.{name}.stamp")
91
93
 
92
94
 
95
+ # 生效条件:name 必填,组装含模块常量 STAMP_VERSION 与 task_running 的戳记录、原子写入 stamp_path(name,d) 后返回该 rec。
93
96
  def write_stamp(name: str, d: str = None, **extra) -> dict:
94
97
  """写一次心跳(原子替换)。extra 会并入戳内容(None 值丢弃)。"""
95
98
  rec = {"v": STAMP_VERSION, "name": name, "ts": time.time(),
@@ -104,6 +107,7 @@ def write_stamp(name: str, d: str = None, **extra) -> dict:
104
107
  return rec
105
108
 
106
109
 
110
+ # 生效条件:stamp_path(name, d) 所得路径上 os.path.exists 为真且 json.load 结果为 dict 时,返回该 rec 并附加 age=max(0.0, time.time() - float(rec.get("ts") or 0));该路径不可读、json.load 抛 ValueError/OSError 或 rec 非 dict 时返回 None;
107
111
  def read_stamp(name: str, d: str = None):
108
112
  """读心跳戳并附 age(秒);不存在 / 损坏 → None。"""
109
113
  p = stamp_path(name, d)
@@ -120,6 +124,7 @@ def read_stamp(name: str, d: str = None):
120
124
  return rec
121
125
 
122
126
 
127
+ # 生效条件:对 stamp_path(name,d) 执行 os.remove,OSError 被静默忽略,无返回值。
123
128
  def clear_stamp(name: str, d: str = None):
124
129
  try:
125
130
  os.remove(stamp_path(name, d))
@@ -127,6 +132,7 @@ def clear_stamp(name: str, d: str = None):
127
132
  pass
128
133
 
129
134
 
135
+ # 生效条件:age 为 None 返回 'absent';否则以 factor=working if task_running else 1.0 比较 age 与 interval*warn*factor、interval*dead*factor,分别返回 'ok'/'warning'/'dead'。
130
136
  def judge(age, *, interval: float = DEFAULT_BEAT_INTERVAL,
131
137
  task_running: bool = False, warn: float = DEFAULT_WARN_FACTOR,
132
138
  dead: float = DEFAULT_DEAD_FACTOR,
@@ -146,6 +152,7 @@ def judge(age, *, interval: float = DEFAULT_BEAT_INTERVAL,
146
152
  return "dead"
147
153
 
148
154
 
155
+ # 生效条件:net_dir(d) 可列为目录时遍历 heartbeat.*.stamp 并返回含 age 与 state 的记录列表,否则返回 []。
149
156
  def peers(d: str = None):
150
157
  """列出本机所有心跳戳(含状态)。"""
151
158
  base = net_dir(d)
@@ -171,6 +178,7 @@ DEFAULT_RESTART_COOLDOWN = 300.0 # 同一对端两次拉起的最小间隔(
171
178
  DEFAULT_FRESH_TIMEOUT = 60.0 # 拉起后等待对端戳变新鲜的窗口
172
179
 
173
180
 
181
+ # 生效条件:pid 可转 int 且 >0 时按探活(os.name=='nt' 用 OpenProcess+WaitForSingleObject,否则 os.kill(pid,0))返回 True,转 int 失败或 pid<=0 或探活失败返回 False。
174
182
  def pid_alive(pid) -> bool:
175
183
  """进程探活(零第三方依赖):Windows=OpenProcess+WaitForSingleObject;
176
184
  posix=os.kill(pid,0)。pid 无效 / 已退出 / 权限外 → False(诚实)。"""
@@ -199,6 +207,7 @@ def pid_alive(pid) -> bool:
199
207
  return False
200
208
 
201
209
 
210
+ # 生效条件:cmd 为 argv 列表时以 stdout/stderr=DEVNULL、不经 shell 的 subprocess.Popen 拉起(nt 附加 DETACHED|NEW_GROUP creationflags),返回该 Popen 对象。
202
211
  def _default_spawner(cmd):
203
212
  """分离式拉起:argv 列表、不经 shell、输出弃置(Windows 完全脱离父控制台)。"""
204
213
  import subprocess
@@ -209,6 +218,7 @@ def _default_spawner(cmd):
209
218
  stderr=subprocess.DEVNULL, **kw)
210
219
 
211
220
 
221
+ # 生效条件:root 与 peer 必填,扫描 root/SUSTAIN_LOG(模块常量)返回 op='mutual'、action='restart' 且 detail 以 peer 开头的记录中最大 t,无匹配返回 0.0。
212
222
  def _last_mutual_restart(root: str, peer: str) -> float:
213
223
  """_sustain.jsonl 里该对端最近一次互维拉起时间(无 → 0.0,审计即状态)。"""
214
224
  last = 0.0
@@ -227,6 +237,7 @@ def _last_mutual_restart(root: str, peer: str) -> float:
227
237
  return last
228
238
 
229
239
 
240
+ # 生效条件:cg 与 peer 必填,读对端戳判状态:ok/warning 直接返回 action='none';dead/absent 时依次按 pid_alive、restart_cooldown、restart_cmd 有无走 alive_but_stale/restart_cooldown/dead_unhandled/拉起并验戳,返回 out。
230
241
  def mutual_watch(cg, peer: str, *, restart_cmd=None, d: str = None,
231
242
  interval: float = DEFAULT_BEAT_INTERVAL,
232
243
  restart_cooldown: float = DEFAULT_RESTART_COOLDOWN,
@@ -294,6 +305,7 @@ def mutual_watch(cg, peer: str, *, restart_cmd=None, d: str = None,
294
305
  return out
295
306
 
296
307
 
308
+ # 生效条件:cg 与 peer 必填,分别按 name 与 peer 读戳判状态,仅当自身与对端状态均属 ('dead','absent') 时返回 ok=False、both_dead=True。
297
309
  def mutual_status(cg, peer: str, *, d: str = None, name: str = "md_cg",
298
310
  interval: float = DEFAULT_BEAT_INTERVAL) -> dict:
299
311
  """互维状态 + 双亡检测:自己也失联时互维不可信 → 显式外部告警语义。"""
@@ -316,6 +328,7 @@ def mutual_status(cg, peer: str, *, d: str = None, name: str = "md_cg",
316
328
  # 诊断(只读)
317
329
  # --------------------------------------------------------------------------
318
330
 
331
+ # 生效条件:root 必填,对模块常量 LAYERS 各层 os.walk 统计以 .md 结尾的文件名个数并返回 n(不解析正文)。
319
332
  def _count_node_files(root: str) -> int:
320
333
  """轻量统计磁盘节点数(只数文件名,不解析正文)。"""
321
334
  n = 0
@@ -325,6 +338,7 @@ def _count_node_files(root: str) -> int:
325
338
  return n
326
339
 
327
340
 
341
+ # 生效条件:root 与 older_than 必填,返回 root 及其 LAYERS 各层下以 '.' 开头、名含 '.tmp-' 且 mtime 早于 now-older_than 的路径列表。
328
342
  def _list_stale_temps(root: str, older_than: float):
329
343
  now, out = time.time(), []
330
344
  bases = [root] + [os.path.join(root, l) for l in LAYERS]
@@ -343,6 +357,7 @@ def _list_stale_temps(root: str, older_than: float):
343
357
  return out
344
358
 
345
359
 
360
+ # 生效条件:root 必填,返回 root 顶层以 .jsonl/.log 结尾且 ends_mid_line 为真的路径列表;root 非目录返回 []。
346
361
  def _half_line_logs(root: str):
347
362
  """顶层行式日志里末尾不是换行的(写者被杀留下的半截记录)。"""
348
363
  out = []
@@ -357,6 +372,7 @@ def _half_line_logs(root: str):
357
372
  return out
358
373
 
359
374
 
375
+ # 生效条件:root/_index_log 可列为目录时返回其中 .log 结尾的文件名列表,否则返回 []。
360
376
  def _index_log_shards(root: str):
361
377
  d = os.path.join(root, "_index_log")
362
378
  if not os.path.isdir(d):
@@ -364,6 +380,7 @@ def _index_log_shards(root: str):
364
380
  return [f for f in os.listdir(d) if f.endswith(".log")]
365
381
 
366
382
 
383
+ # 生效条件:cg 具备 crypto_status 且其返回 unlocked 为假时,返回 cg.index['nodes'] 中 sensitivity 属 crypto.ENCRYPTED_LEVELS 的条目数;属性缺失、调用异常或已解锁时返回 0。
367
384
  def _locked_nodes(cg) -> int:
368
385
  """加密引擎在「未解锁」状态下被隔离的私有节点数(缺密钥 = 权限事实)。"""
369
386
  if not hasattr(cg, "crypto_status"):
@@ -388,6 +405,7 @@ EVOLVE_FIXES = {"ccg_backlog": "consolidate_run",
388
405
  "importance_drift": "importance"}
389
406
 
390
407
 
408
+ # 生效条件:nodes 与 top 必填,遍历索引条目统计缺 verification_basis 或 has_neg_conditions 的候补数 n、no_basis、no_neg 并取最多 top 个 sample,返回含 proxy=True 与 EVOLVE_FIXES['ccg_backlog'] 的字典。
391
409
  def _ccg_backlog(nodes: dict, top: int) -> dict:
392
410
  """固化候补量(**索引代理指标**:零读节点文件、确定性、O(N))。
393
411
 
@@ -412,6 +430,7 @@ def _ccg_backlog(nodes: dict, top: int) -> dict:
412
430
  "proxy": True, "fix": EVOLVE_FIXES["ccg_backlog"]}
413
431
 
414
432
 
433
+ # 生效条件:cg 必填,取 cg.index['nodes'](layer 非空时按 layer 过滤),经 weights.recalc(apply=False) 与 _ccg_backlog 汇总,返回 ok=True、readonly=True、dry_run=True 的候选盘点字典。
415
434
  def evolution_candidates(cg, *, layer: str = None, top: int = 8,
416
435
  min_delta: float = None) -> dict:
417
436
  """演化候选盘点(G7):**只读、零读节点文件、确定性、不写盘**。
@@ -446,6 +465,7 @@ def evolution_candidates(cg, *, layer: str = None, top: int = 8,
446
465
  "重要性重算为确定性动作(有 rollback)")}
447
466
 
448
467
 
468
+ # 生效条件:给定 cg 后只读汇总(nodes 取自 cg.index、disk 计数、refindex.check_refs),stale_temp_age 传入 _list_stale_temps;check_heartbeat / check_evolution(evolve_top) / check_provenance(provenance_top) 为真时分别追加对应 issue,返回 ok = 无 severity=="warning" 的 issue 连同 stats。
449
469
  def diagnose(cg, *, name: str = "md_cg", stale_temp_age: float = STALE_TEMP_AGE,
450
470
  check_heartbeat: bool = True, check_evolution: bool = True,
451
471
  evolve_top: int = 5, check_provenance: bool = True,
@@ -575,17 +595,20 @@ def diagnose(cg, *, name: str = "md_cg", stale_temp_age: float = STALE_TEMP_AGE,
575
595
  # 自愈
576
596
  # --------------------------------------------------------------------------
577
597
 
598
+ # 生效条件:path 必填,以 'ab' 模式向该路径追加写入单个换行 b'\n',无返回值。
578
599
  def _seal_half_line(path: str):
579
600
  with open(path, "ab") as f:
580
601
  f.write(b"\n")
581
602
 
582
603
 
604
+ # 生效条件:root/op/action 必填,向 root/SUSTAIN_LOG(模块常量)追加一行含 t/op/action/detail(截断 200)/pid 的 JSONL,无返回值。
583
605
  def _audit(root: str, op: str, action: str, detail: str = ""):
584
606
  append_jsonl(os.path.join(root, SUSTAIN_LOG),
585
607
  {"t": time.time(), "op": op, "action": action,
586
608
  "detail": str(detail)[:200], "pid": os.getpid()})
587
609
 
588
610
 
611
+ # 生效条件:按 diagnose(cg, name=name, stale_temp_age=stale_temp_age) 的 issues code 集合分派——命中 index_drift/index_orphan 重建索引、ref_stale/ref_dangling 按 ref 重建源索引、index_log_backlog 合并索引分片、stale_temps 清理陈旧临时文件、half_line_logs 修补半截日志;ccg_backlog 仅 allow_evolve=True 且 reflect_fn 非 None 时才 consolidate(否则记 needs_llm),importance_drift 仅 allow_evolve=True 时才重算重要性(否则记 evolve_disabled);dry_run=True 时各动作只记入 actions 不落盘,返回含 after["ok"]、dry_run、actions、before/after 的 stats 与 t 的 dict;
589
612
  def heal(cg, *, name: str = "md_cg", dry_run: bool = False,
590
613
  stale_temp_age: float = STALE_TEMP_AGE,
591
614
  allow_evolve: bool = False, reflect_fn=None, verify_fn=None) -> dict:
@@ -599,6 +622,7 @@ def heal(cg, *, name: str = "md_cg", dry_run: bool = False,
599
622
  root = cg.root
600
623
  actions = []
601
624
 
625
+ # 生效条件:闭包 dry_run 为真时向 actions 追加 {"code": code, "detail": detail, "applied": False} 并返回;否则调用 fn(),成功追加 applied=True/ok=True,抛异常时追加 applied=True/ok=False 与 error,最后执行 _audit(root, "heal", code, detail);
602
626
  def act(code: str, detail: str, fn):
603
627
  if dry_run:
604
628
  actions.append({"code": code, "detail": detail, "applied": False})
@@ -624,6 +648,7 @@ def heal(cg, *, name: str = "md_cg", dry_run: bool = False,
624
648
  if "stale_temps" in codes:
625
649
  paths = _list_stale_temps(root, stale_temp_age)
626
650
 
651
+ # 生效条件:对闭包变量 paths 中每个路径尝试 os.remove(p),单个路径的 OSError 被吞掉后继续处理后续路径;
627
652
  def _sweep():
628
653
  for p in paths:
629
654
  try:
@@ -643,6 +668,7 @@ def heal(cg, *, name: str = "md_cg", dry_run: bool = False,
643
668
  if allow_evolve and reflect_fn is not None:
644
669
  from . import consolidate as _cd
645
670
 
671
+ # 生效条件:调用时返回 _cd.consolidate(cg.root, apply=True, reflect_fn=reflect_fn, verify_fn=verify_fn);
646
672
  def _consolidate():
647
673
  return _cd.consolidate(cg.root, apply=True,
648
674
  reflect_fn=reflect_fn,
@@ -655,6 +681,7 @@ def heal(cg, *, name: str = "md_cg", dry_run: bool = False,
655
681
  if allow_evolve:
656
682
  from . import weights
657
683
 
684
+ # 生效条件:调用时返回 weights.recalc(cg, apply=True, actor="sustain_evolve");
658
685
  def _importance():
659
686
  return weights.recalc(cg, apply=True, actor="sustain_evolve")
660
687
  act("importance", "重算结构重要性(确定性;可 rollback)", _importance)
@@ -674,6 +701,7 @@ def heal(cg, *, name: str = "md_cg", dry_run: bool = False,
674
701
  # 会话水位(重启续接)
675
702
  # --------------------------------------------------------------------------
676
703
 
704
+ # 生效条件:以 root 实例化后账本固定指向 os.path.join(root, LEDGER_FILE),其 _load/_save/note/resume_point/sessions/summary 均以该路径为唯一读写对象;
677
705
  class SessionLedger:
678
706
  """会话水位账本:<root>/_sessions.json。
679
707
 
@@ -681,10 +709,12 @@ class SessionLedger:
681
709
  这里记「某个会话发生过什么」——重启后按会话给出续接点。
682
710
  """
683
711
 
712
+ # 生效条件:传入 root 时置 self.root=root 且 self.path=os.path.join(root, LEDGER_FILE),不做其他校验;
684
713
  def __init__(self, root: str):
685
714
  self.root = root
686
715
  self.path = os.path.join(root, LEDGER_FILE)
687
716
 
717
+ # 生效条件:self.path(root/LEDGER_FILE)可读且 json 结果为 dict、且 d.get("sessions") 也是 dict 时返回该 d;self.path 不可读、json.load 抛 ValueError/OSError、或 d 非 dict / sessions 非 dict 时返回 {"schema": 1, "sessions": {}};
688
718
  def _load(self):
689
719
  if os.path.exists(self.path):
690
720
  try:
@@ -696,9 +726,11 @@ class SessionLedger:
696
726
  pass
697
727
  return {"schema": 1, "sessions": {}}
698
728
 
729
+ # 生效条件:传入 d 时执行 atomic_write(self.path, json.dumps(d, ensure_ascii=False, indent=1)),自身无返回值;
699
730
  def _save(self, d):
700
731
  atomic_write(self.path, json.dumps(d, ensure_ascii=False, indent=1))
701
732
 
733
+ # 生效条件:以 session 为键 setdefault 会话记录后写 events=int(s.get("events") or 0)+int(n)(n 默认 1,n=0 时计数不变),t 非 None 时写 last_t 且 first_t 为空时一并写入,seq 非 None 时写 last_seq,actor 为真值时写 actor,最后写 updated_at 并 _save,返回 dict(s, session=session);
702
734
  def note(self, session: str, *, t=None, seq=None, n: int = 1,
703
735
  actor: str = None) -> dict:
704
736
  """记一笔会话活动(事件计数 + 最后 (t, seq) 水位)。"""
@@ -718,6 +750,7 @@ class SessionLedger:
718
750
  self._save(d)
719
751
  return dict(s, session=session)
720
752
 
753
+ # 生效条件:_load()["sessions"].get(session) 为假值(键缺失或值为空 dict)时返回 {"session": session, "resume": None, "events": 0};否则返回 {"session": session, "events": s.get("events", 0), "resume": {"t": s.get("last_t"), "seq": s.get("last_seq")}, "last_t": s.get("last_t"), "actor": s.get("actor")};
721
754
  def resume_point(self, session: str) -> dict:
722
755
  """重启续接点:(t, seq) 之后的事件才是新的。"""
723
756
  s = self._load()["sessions"].get(session)
@@ -727,9 +760,11 @@ class SessionLedger:
727
760
  "resume": {"t": s.get("last_t"), "seq": s.get("last_seq")},
728
761
  "last_t": s.get("last_t"), "actor": s.get("actor")}
729
762
 
763
+ # 生效条件:无参调用时返回 dict(self._load()["sessions"]) 的浅拷贝,账本缺失/损坏时 _load 回落默认值故此处为 {};
730
764
  def sessions(self) -> dict:
731
765
  return dict(self._load()["sessions"])
732
766
 
767
+ # 生效条件:取 d=_load()["sessions"] 后返回 {"sessions": len(d), "events": sum(int(v.get("events") or 0)), "latest": max(v.get("last_t") or 0) if d else None},d 为空时 latest 为 None;
733
768
  def summary(self) -> dict:
734
769
  d = self._load()["sessions"]
735
770
  return {"sessions": len(d),
@@ -738,6 +773,7 @@ class SessionLedger:
738
773
  if d else None}
739
774
 
740
775
 
776
+ # 生效条件:os.path.exists(os.path.join(cg.root, "_sources.json")) 为真且 json.load 成功时返回 dict((d or {}).get("sources") or {})(d 或 sources 为假值即回落 {});该路径不可读或 json.load 抛 ValueError/OSError 时返回 {};
741
777
  def watermarks(cg) -> dict:
742
778
  """源视角水位快照(读 sources.Ingestor 的 _sources.json)。"""
743
779
  p = os.path.join(cg.root, "_sources.json")
@@ -755,6 +791,7 @@ def watermarks(cg) -> dict:
755
791
  # 常驻循环
756
792
  # --------------------------------------------------------------------------
757
793
 
794
+ # 生效条件:传入 cg 即构造实例并把 self.cg 指向它,name/beat_interval/heal_interval/auto_heal/scrub_interval/auto_scrub/evolve_interval/auto_evolve/tidy_interval/auto_tidy 用各默认值(DEFAULT_* 与 False/True)经 float()/bool() 落为 self 属性,ledger 为假值(默认 None)时回落 SessionLedger(cg.root),d 经 net_dir(d) 赋值,其余运行态字段初始化为 False/None/空列表/空 Event/Lock
758
795
  class SustainLoop:
759
796
  """常驻自维持循环:后台线程周期心跳 + 周期巡检 + 必要时自愈。
760
797
 
@@ -762,6 +799,7 @@ class SustainLoop:
762
799
  让对端立刻看到「正常下线」而不是「失联」。
763
800
  """
764
801
 
802
+ # 生效条件:传入 cg 时按 name 与各 DEFAULT_* 默认值初始化——self.d=net_dir(d)(d 假值时回落 MDCG_SUSTAIN_DIR/~/ .mdcg/sustain)、self.ledger=ledger or SessionLedger(cg.root)(ledger 假值时新建),beat/heal/scrub/evolve/tidy 间隔 float() 化、auto_heal/auto_scrub/auto_evolve/auto_tidy bool() 化后存为实例属性;
765
803
  def __init__(self, cg, name: str = "md_cg", *,
766
804
  beat_interval: float = DEFAULT_BEAT_INTERVAL,
767
805
  heal_interval: float = DEFAULT_HEAL_INTERVAL,
@@ -806,6 +844,7 @@ class SustainLoop:
806
844
 
807
845
  # ---- 心跳 ----
808
846
 
847
+ # 生效条件:task_running 非 None 时先置 self.task_running=bool(task_running),再 write_stamp(self.name, self.d, task_running=self.task_running, root=self.cg.root, uptime=... if self._started_at else 0.0),beats 自增并记 last_beat=rec["ts"],返回该 rec;
809
848
  def beat(self, task_running: bool = None) -> dict:
810
849
  if task_running is not None:
811
850
  self.task_running = bool(task_running)
@@ -819,6 +858,7 @@ class SustainLoop:
819
858
 
820
859
  # ---- 生命周期 ----
821
860
 
861
+ # 生效条件:不适用(无必需形参与模块级常量)
822
862
  def start(self):
823
863
  if self._th is not None and self._th.is_alive():
824
864
  return self
@@ -830,6 +870,7 @@ class SustainLoop:
830
870
  self._th.start()
831
871
  return self
832
872
 
873
+ # 生效条件:调用时置 _stop 事件,self._th 非 None 时 join(timeout)(默认 3.0)后置为 None,随后 clear_stamp(self.name, self.d),返回 self;
833
874
  def stop(self, timeout: float = 3.0):
834
875
  self._stop.set()
835
876
  if self._th is not None:
@@ -838,6 +879,7 @@ class SustainLoop:
838
879
  clear_stamp(self.name, self.d)
839
880
  return self
840
881
 
882
+ # 生效条件:self._stop 未置位期间轮询,按 beat_interval/heal_interval/scrub_interval/evolve_interval/tidy_interval 到期分别执行 beat 与 _tick_heal/_tick_scrub/_tick_evolve/_tick_tidy,各 tick 抛出的异常被吞掉不中断循环,末尾以 _stop.wait(_POLL) 休眠;
841
883
  def _run(self):
842
884
  next_beat = time.time() + self.beat_interval
843
885
  next_heal = time.time() + self.heal_interval
@@ -878,6 +920,7 @@ class SustainLoop:
878
920
  next_tidy = now + self.tidy_interval
879
921
  self._stop.wait(_POLL)
880
922
 
923
+ # 生效条件:以 apply=self.auto_tidy 调 writelimit.tidy_contextual(self.cg, actor="sustain_tidy"),把 t/scanned/groups/members/applied_count/auto_tidy 记入 self.last_tidy 与 tidys(仅保留最近 20 条),随后调 _tick_conformance();auto_tidy=False(默认)时只盘点不落盘;
881
924
  def _tick_tidy(self):
882
925
  """整理巡检(contextual 流水治理·读侧):同构组聚合 + 成员降权。
883
926
 
@@ -898,6 +941,7 @@ class SustainLoop:
898
941
  self.tidys = self.tidys[-20:]
899
942
  self._tick_conformance()
900
943
 
944
+ # 生效条件:把 conformance.report_summary(self.cg) 的结论写入 self.last_conformance;该调用抛异常时写入 {"ok": False, "verdict": "BLINDSPOT", "error": "<类型名>: <消息>"};
901
945
  def _tick_conformance(self):
902
946
  """数据健康不变量断言集(Pi⑤)——与 tidy 同节奏,**只取结论**。
903
947
 
@@ -912,6 +956,7 @@ class SustainLoop:
912
956
  self.last_conformance = {"ok": False, "verdict": "BLINDSPOT",
913
957
  "error": f"{type(e).__name__}: {e}"}
914
958
 
959
+ # 生效条件:恒以 evolution_candidates(self.cg) 只读盘点并记入 last_evolve 与 evolves(保留最近 20 条);仅当 self.auto_evolve 为真且 ev["importance_drift"]["n"] 为真时才额外执行 weights.recalc(self.cg, apply=True, actor="sustain_evolve"),其异常写入 rec["applied"];
915
960
  def _tick_evolve(self):
916
961
  """演化巡检(G7):盘点固化/重要性候选 —— 让「有能力」变成「有驱动」。
917
962
 
@@ -938,6 +983,7 @@ class SustainLoop:
938
983
  self.evolves.append(rec)
939
984
  self.evolves = self.evolves[-20:]
940
985
 
986
+ # 生效条件:以 dry_run=not self.auto_scrub 调 scrub.sweep(self.cg),把 t/ok/n_issues/n_high_medium/applied/dry_run 记入 last_scrub 与 scrubs(保留最近 20 条);auto_scrub=False(默认)时 dry_run=True 只读巡检;
941
987
  def _tick_scrub(self):
942
988
  """记忆自净:抽查 → 联想 → 去污染 → 校准偏差。
943
989
 
@@ -956,6 +1002,7 @@ class SustainLoop:
956
1002
  self.scrubs.append(rec)
957
1003
  self.scrubs = self.scrubs[-20:]
958
1004
 
1005
+ # 生效条件:先 diagnose(self.cg, name=self.name) 并将 ok 与 issues code 记入 last_diagnose;仅当 self.auto_heal 为真且 rep["ok"] 为假时才调 heal(self.cg, name=self.name),其 actions 非空时把各 action 的 code 记入 heals(保留最近 20 条);
959
1006
  def _tick_heal(self):
960
1007
  rep = diagnose(self.cg, name=self.name)
961
1008
  self.last_diagnose = {"t": time.time(), "ok": rep["ok"],
@@ -971,6 +1018,7 @@ class SustainLoop:
971
1018
 
972
1019
  # ---- 状态 ----
973
1020
 
1021
+ # 生效条件:以 read_stamp(self.name, self.d) 判定 state(有戳走 judge(age, interval=self.beat_interval, task_running=...),无戳为 "stopped"),返回含 name/running/pid/uptime(无 _started_at 时为 0.0)/beats/last_beat/各 interval 与 auto_* 开关/heals[-5:]/evolves[-5:]/tidys[-5:]/peers(self.d)/ledger.summary() 的 dict;
974
1022
  def status(self) -> dict:
975
1023
  st = read_stamp(self.name, self.d)
976
1024
  state = (judge(st["age"], interval=self.beat_interval,
@@ -1006,6 +1054,7 @@ _LOOPS = {}
1006
1054
  _LOOPS_LOCK = threading.Lock()
1007
1055
 
1008
1056
 
1057
+ # 生效条件:cg 必填,以 (os.path.abspath(cg.root), name) 为键在模块级 _LOOPS 中复用已有实例或新建 SustainLoop 并返回该实例。
1009
1058
  def ensure_loop(cg, name: str = "md_cg", **kw) -> SustainLoop:
1010
1059
  """按 (root, name) 复用同一个常驻循环(避免重复线程 / 重复戳)。"""
1011
1060
  key = (os.path.abspath(cg.root), name)
@@ -1017,10 +1066,12 @@ def ensure_loop(cg, name: str = "md_cg", **kw) -> SustainLoop:
1017
1066
  return lp
1018
1067
 
1019
1068
 
1069
+ # 生效条件:cg 必填,返回 _LOOPS[(os.path.abspath(cg.root), name)],该键未登记时返回 None。
1020
1070
  def get_loop(cg, name: str = "md_cg"):
1021
1071
  return _LOOPS.get((os.path.abspath(cg.root), name))
1022
1072
 
1023
1073
 
1074
+ # 生效条件:模块级 _LOOPS 非空时清空该表并对每个循环调用 stop()(异常被吞),无返回值。
1024
1075
  def stop_all():
1025
1076
  """测试 / 进程退出用:停掉本进程创建的所有常驻循环。"""
1026
1077
  with _LOOPS_LOCK:
@@ -1033,6 +1084,7 @@ def stop_all():
1033
1084
  pass
1034
1085
 
1035
1086
 
1087
+ # 生效条件:以 read_stamp(name) 有戳时返回 heartbeat={'age': round(st["age"],1), 'state': judge(...), 'pid', 'task_running'}、无戳时 {'state':'absent'},loop 取 get_loop(cg, name) 有值时给 running/beats/state、无值时 {'running': False},并附带 SessionLedger(cg.root).summary()、scrub_summary(cg)、evolve_summary(cg)、provenance_summary(cg);
1036
1088
  def summary(cg, name: str = "md_cg") -> dict:
1037
1089
  """并入 health_os 的轻量摘要(只读戳与计数,不做巡检)。"""
1038
1090
  st = read_stamp(name)
@@ -1054,6 +1106,7 @@ def summary(cg, name: str = "md_cg") -> dict:
1054
1106
  }
1055
1107
 
1056
1108
 
1109
+ # 生效条件:cg 必填,可导入 provenance 且其 summary(cg) 调用成功时返回该摘要,异常时返回 {}。
1057
1110
  def provenance_summary(cg) -> dict:
1058
1111
  """派生溯源摘要(G8,只读;失败不抛,避免拖垮 health_os)。"""
1059
1112
  try:
@@ -1063,6 +1116,7 @@ def provenance_summary(cg) -> dict:
1063
1116
  return {}
1064
1117
 
1065
1118
 
1119
+ # 生效条件:cg 必填,evolution_candidates(cg, top=3) 成功时返回 candidates/ccg_backlog/importance_drift 计数摘要,异常时返回 {}。
1066
1120
  def evolve_summary(cg) -> dict:
1067
1121
  """演化巡检摘要(只读;失败不抛,避免拖垮 health_os)。"""
1068
1122
  try:
@@ -1075,10 +1129,11 @@ def evolve_summary(cg) -> dict:
1075
1129
  return {}
1076
1130
 
1077
1131
 
1132
+ # 生效条件:cg 必填,可导入 scrub 且其 summary(cg) 调用成功时返回该摘要,异常时返回 {}。
1078
1133
  def scrub_summary(cg) -> dict:
1079
1134
  """记忆自净摘要(惰性导入,避免模块加载顺序耦合)。"""
1080
1135
  try:
1081
1136
  from . import scrub
1082
1137
  return scrub.summary(cg)
1083
1138
  except Exception: # noqa: BLE001
1084
- return {}
1139
+ return {}
package/md_cg/tasks.py CHANGED
@@ -75,6 +75,7 @@ _ILLEGAL_RE = re.compile(r"[^0-9A-Za-z\u4e00-\u9fff_.-]+")
75
75
 
76
76
  # ---------------------------------------------------------------- 命名与解析
77
77
 
78
+ # 生效条件:name 为 None/空串或 strip 后为空时返回空串,否则把 `/`、`\`、`..` 及 _ILLEGAL_RE 命中字符折叠为 `-`、压缩连续 `-` 并去首尾 `-.` 后取前 64 字符再去首尾 `-.` 返回;
78
79
  def slugify(name: str) -> str:
79
80
  """任务名 → 语义 slug(稳定标识;同 slug 即同任务)。
80
81
 
@@ -91,6 +92,7 @@ def slugify(name: str) -> str:
91
92
  return s[:64].strip("-.")
92
93
 
93
94
 
95
+ # 生效条件:name(`name or ""` 后 strip)先剥掉已有的 TASK_PREFIX 再 slugify,结果非空且被 _SLUG_RE.match 命中时返回 TASK_PREFIX + slug;name 为假值或归一化后为空、不匹配时返回 ""。
94
96
  def task_node_id(name: str) -> str:
95
97
  """任务名或节点 id → 规范节点 id(`task_<slug>`)。非法名返回空串。"""
96
98
  s = (name or "").strip()
@@ -102,15 +104,18 @@ def task_node_id(name: str) -> str:
102
104
  return TASK_PREFIX + slug
103
105
 
104
106
 
107
+ # 生效条件:v 为 None 或 str(v).strip() 为 "" 时返回 False,否则返回 True。
105
108
  def _has(v) -> bool:
106
109
  """「本次调用是否提供了该字段」——空串/None 一律视为未提供(不静默清空)。"""
107
110
  return v is not None and str(v).strip() != ""
108
111
 
109
112
 
113
+ # 生效条件:无必需形参且无模块级常量约束,恒返回 time.strftime("%Y-%m-%d %H:%M") 的当前时间文本。
110
114
  def _today() -> str:
111
115
  return time.strftime("%Y-%m-%d %H:%M")
112
116
 
113
117
 
118
+ # 生效条件:content(`content or ""` 后 splitlines)中有某行 strip 后以 `# ` + field + `:` 开头时,返回该行该前缀之后的去空白内容;content 为假值或无此匹配行时返回 ""。
114
119
  def _field_line(content: str, field: str) -> str:
115
120
  """从正文抽 `# <字段>:` 行的值(与 `mdcos._ccg_field` 同源口径,
116
121
  本模块不反向 import mdcos,避免包内循环依赖)。"""
@@ -122,6 +127,7 @@ def _field_line(content: str, field: str) -> str:
122
127
  return ""
123
128
 
124
129
 
130
+ # 生效条件:(content or "") 的行中 strip 后以 "## " 开头者成为节名 s[3:].strip() 并切换当前节,其余行累入当前节,返回各节内容以 "\n" join 后 strip 的字典;无任何标题行时仅返回 {"__body__": 全篇 strip};content 为 None/空串时返回 {"__body__": ""}。
125
131
  def sections(content: str) -> dict:
126
132
  """正文 → `{节名: 节内容}`;无标题部分归入 `__body__`。"""
127
133
  out: dict = {"__body__": []}
@@ -136,6 +142,7 @@ def sections(content: str) -> dict:
136
142
  return {k: "\n".join(v).strip() for k, v in out.items()}
137
143
 
138
144
 
145
+ # 生效条件:t = (text or "").strip(),t 为 ""/"(无)"/"(未填)" 时返回 "",否则 len(t) <= n(默认 200)时返回 t,超出时返回 t[:n].rstrip() + "…"。
139
146
  def _brief(text: str, n: int = 200) -> str:
140
147
  t = (text or "").strip()
141
148
  if t in ("", "(无)", "(未填)"):
@@ -143,6 +150,7 @@ def _brief(text: str, n: int = 200) -> str:
143
150
  return t if len(t) <= n else t[:n].rstrip() + "…"
144
151
 
145
152
 
153
+ # 生效条件:以 (old_text or "").strip() 为 base(base 为「(无)」或「(未填)」时置空),_has(change) 判定为假时返回 base or "(无)",为真时拼出 `- [今日] change.strip()`,base 非空返回 base+"\n"+该行再 strip,base 为空只返回该行;
146
154
  def _append_change(old_text: str, change: str) -> str:
147
155
  """「计划变更」节追加一行(累积式,不覆盖历史)。"""
148
156
  base = (old_text or "").strip()
@@ -154,6 +162,7 @@ def _append_change(old_text: str, change: str) -> str:
154
162
  return (base + "\n" + line).strip() if base else line
155
163
 
156
164
 
165
+ # 生效条件:name 为必需形参(`name or ""` 后 strip 填 `# 功能名:` 行);condition、goal、acceptance、boundary、plan、changes、result 各经 _has 判定,未提供时分别落「无条件」「(未填:任务目标待补)」「other」「任务转 done/dropped 终态后不再作为进行中任务参与装配」「(未填)」「(无)」与空串;status 经 STATUS_ZH.get(status, status) 映射、未命中时原样输出,note 经 _has 为真时以 `|` 拼在执行行后。
157
166
  def render(name: str, *, condition: str = "", goal: str = "", status: str = "active",
158
167
  note: str = "", acceptance: str = "", boundary: str = "",
159
168
  plan: str = "", changes: str = "", result: str = "") -> str:
@@ -184,6 +193,7 @@ def render(name: str, *, condition: str = "", goal: str = "", status: str = "act
184
193
 
185
194
  # ---------------------------------------------------------------- 读写
186
195
 
196
+ # 生效条件:nid 为假值(None/空串)时返回 None;否则 cg.get(nid) 命中且其 frontmatter.layer == "structural"、TASK_TAG 在 frontmatter.tags(`or []`)中时,返回 {id, fm, content, path, sec}(sec 为 sections(content));记录缺失或层/标签不符时返回 None。
187
197
  def _read_task(cg, nid: str):
188
198
  """读回任务卡;层或标签不符一律视为不存在(防串号:别的节点占用了同 id)。"""
189
199
  if not nid:
@@ -199,6 +209,7 @@ def _read_task(cg, nid: str):
199
209
  "path": rec.get("path"), "sec": sections(content)}
200
210
 
201
211
 
212
+ # 生效条件:以 layer="structural"、tags=list(tags)、importance=float(importance)、override=True、task_name=name、task_status=status、task_updated_at=time.time()、actor=actor or "task" 等构成 kw,extra 为真值时经 kw.update(extra) 追加覆盖,随后调用 cg.add(nid, content, **kw) 并返回其结果;
202
213
  def _write(cg, nid: str, name: str, content: str, status: str, tags,
203
214
  importance: float, actor, extra: dict = None):
204
215
  """唯一写盘点——走 `cg.add`(权限闸 / 归属注入全部复用既有链路)。
@@ -217,6 +228,7 @@ def _write(cg, nid: str, name: str, content: str, status: str, tags,
217
228
  return cg.add(nid, content, **kw)
218
229
 
219
230
 
231
+ # 生效条件:cg 具有 _audit 属性(getattr(cg, "_audit", None) 非 None)时以 (op, nid, **meta) 调用它,且其中抛出的任何异常被吞掉;cg 无该属性时不调用,两种路径均不返回内容。
220
232
  def _audit(cg, op: str, nid: str, **meta) -> None:
221
233
  """生命周期留痕;审计失败绝不阻断主流程(与 branches._audit 同哲学)。"""
222
234
  a = getattr(cg, "_audit", None)
@@ -228,6 +240,7 @@ def _audit(cg, op: str, nid: str, **meta) -> None:
228
240
  pass
229
241
 
230
242
 
243
+ # 生效条件:_read_task(cg, nid) 命中任务卡时返回摘要条目——id 取 rec['id'],name 取 fm.task_name 或(缺失/假值时)rec['id'],status 取 fm.task_status 或 "active",plan/changes/result 取对应节的 _brief 摘要(缺节回落 ""),并带 created_at/updated_at/path;未命中时返回 None。
231
244
  def _entry(cg, nid: str):
232
245
  """节点 id → 对外任务条目(摘要形态;全字段查 `get_task`)。"""
233
246
  rec = _read_task(cg, nid)
@@ -247,6 +260,7 @@ def _entry(cg, nid: str):
247
260
 
248
261
  # ---------------------------------------------------------------- 写操作
249
262
 
263
+ # 生效条件:slugify(name) 为空串或不匹配 _SLUG_RE 时返回 {'ok': False, 含 slug 的非法名 error};否则 st = str(status if _has(status) else (旧卡 task_status or "active")).strip().lower(),st 不在 TASK_STATUSES 时返回未知状态错误,st == "done" 且合并后结果节 _has(new_res) 为假时返回「转 done 必须填结果」拒收,其余情况渲染写入并返回按 old 是否为 None 区分新建/更新的 out。
250
264
  def upsert(cg, name: str, *, plan: str = None, status: str = None,
251
265
  result: str = None, condition: str = None, goal: str = None,
252
266
  acceptance: str = None, boundary: str = None, change: str = None,
@@ -313,12 +327,14 @@ def upsert(cg, name: str, *, plan: str = None, status: str = None,
313
327
  return out
314
328
 
315
329
 
330
+ # 生效条件:content 中 `# 执行:` 行的值含 "|" 时返回第一个 "|" 之后去空白的内容;content 为假值、无该行或该行值不含 "|" 时返回 ""。
316
331
  def _exec_note(content: str) -> str:
317
332
  """从 `# 执行:` 行取 `|` 之后的进度说明。"""
318
333
  val = _field_line(content, "执行")
319
334
  return val.split("|", 1)[1].strip() if "|" in val else ""
320
335
 
321
336
 
337
+ # 生效条件:node_id 经 task_node_id 得到非空 nid 且 _read_task 命中该卡时,以旧卡 task_name(缺失/假值回落 nid)、status、result、note、actor 转调 upsert 并返回其结果;node_id 非法时返回 ok=False「非法任务标识」,卡不存在时返回 ok=False「任务不存在」。
322
338
  def set_status(cg, node_id: str, status: str, *, result: str = None,
323
339
  note: str = None, actor: str = None) -> dict:
324
340
  """任务状态迁移(进行中/受阻/完成/放弃)。迁 done 且无结果 → 拒收。"""
@@ -332,6 +348,7 @@ def set_status(cg, node_id: str, status: str, *, result: str = None,
332
348
  result=result, note=note, actor=actor)
333
349
 
334
350
 
351
+ # 生效条件:change 经 _has 判定为已提供、且 node_id 经 task_node_id 非空、_read_task 命中该卡时,以旧卡 task_name(缺失/假值回落 nid)与 change、actor 转调 upsert;change 为 None/空串/纯空白时返回 ok=False「计划变更内容为空」,node_id 非法或卡不存在时返回 ok=False 对应错误。
335
352
  def plan_add(cg, node_id: str, change: str, *, actor: str = None) -> dict:
336
353
  """计划变更追加——执行中发现的错误/新问题/偏差,累积进「计划变更」节。"""
337
354
  if not _has(change):
@@ -347,6 +364,7 @@ def plan_add(cg, node_id: str, change: str, *, actor: str = None) -> dict:
347
364
 
348
365
  # ---------------------------------------------------------------- 读操作
349
366
 
367
+ # 生效条件:node_id 经 task_node_id 得到的 nid 对应一张 _read_task 命中的任务卡时返回 ok=True 的全字段(condition/goal/acceptance/boundary 由 _field_line 抽取、note 由 _exec_note 抽取、plan/changes/result 取节原文不截断、tags 取 fm.tags 或 []);否则返回 ok=False,error 中的标识取 nid 或原 node_id。
350
368
  def get_task(cg, node_id: str) -> dict:
351
369
  """单卡全字段读回(计划/变更/结果不截断)。"""
352
370
  nid = task_node_id(node_id)
@@ -370,6 +388,7 @@ def get_task(cg, node_id: str) -> dict:
370
388
  "path": rec["path"]}
371
389
 
372
390
 
391
+ # 生效条件:status 经 `str(status or "").strip().lower()` 为非空且不在 TASK_STATUSES 时返回 ok=False 未知状态;否则收集 cg.index["nodes"] 中各 _entry 可读任务、按 updated_at 倒序,status 非空时再按该状态过滤,total 为过滤后截断前的条数,limit 为真值时取前 max(int(limit), 1) 条——limit 为 None、0 或空串(假值)时不截断而返回全量 filtered 列表。
373
392
  def list_tasks(cg, status: str = None, limit: int = None) -> dict:
374
393
  """任务清单(按 updated_at 倒序);status 可选 active/blocked/done/dropped。"""
375
394
  st = str(status or "").strip().lower() or None
@@ -389,6 +408,7 @@ def list_tasks(cg, status: str = None, limit: int = None) -> dict:
389
408
  return {"ok": True, "count": len(items), "total": total, "tasks": items}
390
409
 
391
410
 
411
+ # 生效条件:遍历 cg.index["nodes"],把 _entry 可读且 status 为 active/blocked 的归入 active、status 为 done 的归入 done,各自按 updated_at 倒序;返回 active[:max(int(active_limit), 0)] 与 done[:max(int(done_limit), 0)](active_limit/done_limit 传 0 或负数时对应列表为空)以及两组截断前的 active_total/done_total。
392
412
  def session_tasks(cg, active_limit: int = 5, done_limit: int = 5) -> dict:
393
413
  """会话装配用:进行中(active/blocked)+ 近期完成(done,按 updated_at 倒序)。
394
414
 
@@ -404,7 +424,10 @@ def session_tasks(cg, active_limit: int = 5, done_limit: int = 5) -> dict:
404
424
  active.append(t)
405
425
  elif t["status"] == "done":
406
426
  done.append(t)
407
- key = lambda x: -(x.get("updated_at") or 0) # noqa: E731
427
+ # 终键 id:同 updated_at 并列时定序,否则顺序回落到 cg.index["nodes"]
428
+ # 的物理序(增量路径=写入序,重建路径=nid 序)。
429
+ key = lambda x: (-(x.get("updated_at") or 0), # noqa: E731
430
+ str(x.get("id") or ""))
408
431
  active.sort(key=key)
409
432
  done.sort(key=key)
410
433
  return {"ok": True,
@@ -413,6 +436,7 @@ def session_tasks(cg, active_limit: int = 5, done_limit: int = 5) -> dict:
413
436
  "active_total": len(active), "done_total": len(done)}
414
437
 
415
438
 
439
+ # 生效条件:name 经 task_node_id 得 nid,exists 为同 id 任务卡是否被 _read_task 命中;k 经 `int(k or 5)` 再 max(…, 1)(k 为 None/0/空串回落 5,负数取 1)得到 kk,cg.search 抛异常时按无结果处理,只保留元素长度 ≥3、frontmatter.tags 含 TASK_TAG 且 node.id != nid 的条目,返回 out[:kk] 与 note。
416
440
  def find_similar(cg, name: str, k: int = 5) -> dict:
417
441
  """同族任务提示(**只提示,不自动合并**——是否同一任务由调用方裁决)。
418
442
 
@@ -444,4 +468,4 @@ def find_similar(cg, name: str, k: int = 5) -> dict:
444
468
  "score": round(float(score), 4),
445
469
  "qualification": (qual or {}).get("state") if isinstance(qual, dict) else None})
446
470
  return {"ok": True, "node_id": nid, "exists": exact, "similar": out[:kk],
447
- "note": "仅提示疑似同族任务,不自动合并——终裁权在调用方"}
471
+ "note": "仅提示疑似同族任务,不自动合并——终裁权在调用方"}
@@ -7,6 +7,8 @@
7
7
  提案携带证据明细
8
8
  ③ 全链路:提案 → learn_blindspots 五态终判 → 留痕 _explore.jsonl
9
9
  ④ 幂等:重复 explore 终态一致、gap 回写不重复(gap_<sha1(bid)> 幂等)
10
+ ⑤ D_meta 排序加分:只进 sort_score 与证据,不改 score、不改资格
11
+ (零信号查询仍不提案;加分每轮一次、循环内复用)
10
12
 
11
13
  运行:python -m md_cg.test_autonomy
12
14
  """
@@ -103,6 +105,30 @@ def main():
103
105
  "④b gap 回写幂等(第二次 written=None,节点已存在)")
104
106
  else:
105
107
  ok(True, "④b本终态无回写动作,幂等性由判定确定性保证")
108
+
109
+ # ---------- ⑤ D_meta 排序加分(只影响排序,不改资格) ----------
110
+ root5 = os.path.join(tmp, "root5")
111
+ os.makedirs(root5, exist_ok=True)
112
+ cg5 = MdCGOS(root5)
113
+ for i in range(50):
114
+ append_jsonl(cg5.recent_log, {"i": i})
115
+ _sig(cg5, 1.0, "零信号查询", 0.0, {})
116
+ _sig(cg5, 2.0, "反应堆冷却方案", 0.9, {"BLINDSPOT": 2})
117
+ pr5 = autonomy.proposals(cg5, limit=3)
118
+ ps5 = pr5["proposals"]
119
+ ok(len(ps5) == 1 and pr5["n_signals"] == 2,
120
+ "⑤零信号查询仍不提案、n_signals 按原始信号计(加分不改资格)")
121
+ p0 = ps5[0]
122
+ ok(p0["score"] == round(autonomy.W_D2 * p0["d2_abs"]
123
+ + autonomy.W_BLINDSPOT * p0["blindspot"]
124
+ + autonomy.W_DEFER * p0["defer"], 4),
125
+ "⑤bscore 仍是纯 ΔD 定价(口径零变更)")
126
+ ok(p0["sort_score"] == round(p0["score"] + p0["d_meta_bonus"], 4)
127
+ and p0["d_meta_bonus"] == pr5["d_meta"]["bonus"],
128
+ "⑤c排序键 = ΔD + D_meta 加分(每轮一次、循环内复用)")
129
+ ok(p0["d_meta_bonus"] > 0 and "D_meta" in p0["reason"]
130
+ and "不合成" in p0["reason"],
131
+ "⑤d加分生效且 reason 追加证据并声明不合成(可审计)")
106
132
  finally:
107
133
  shutil.rmtree(tmp, ignore_errors=True)
108
134