@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
@@ -84,18 +84,22 @@ _STATE_RE = re.compile(r"```json state\s*\n(.*?)\n```", re.S)
84
84
  # 路径 / 追加
85
85
  # --------------------------------------------------------------------------
86
86
 
87
+ # 生效条件:cg 可解析出 root 时,返回 os.path.join(cg.root, 模块常量 EVOLUTION_DIR),无守卫分支。
87
88
  def evolution_dir(cg) -> str:
88
89
  return os.path.join(cg.root, EVOLUTION_DIR)
89
90
 
90
91
 
92
+ # 生效条件:cg 可解析出 root 时,返回 evolution_dir(cg) 与模块常量 LEDGER_NAME 的 os.path.join 结果。
91
93
  def ledger_path(cg) -> str:
92
94
  return os.path.join(evolution_dir(cg), LEDGER_NAME)
93
95
 
94
96
 
97
+ # 生效条件:无入参,任何一次调用都返回 "evo-" + time.strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex 的前 4 位。
95
98
  def new_entry_id() -> str:
96
99
  return "evo-" + time.strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:4]
97
100
 
98
101
 
102
+ # 生效条件:cg 与 text 给定时,在 FileLock(ledger_path(cg)) 内取旧内容(读不到或空则回退 _HEADER,不以换行结尾则补一个换行)并 atomic_write 写入 old + text,无需 text 非空。
99
103
  def _append(cg, text: str):
100
104
  """向账本追加一条(读-改-写 + 跨进程锁,崩溃不留半截文件)。"""
101
105
  p = ledger_path(cg)
@@ -112,6 +116,7 @@ def _append(cg, text: str):
112
116
  atomic_write(p, old + text)
113
117
 
114
118
 
119
+ # 生效条件:ledger_path(cg) 对应路径不存在时返回 "",否则以 encoding="utf-8"、errors="replace" 打开并返回 f.read() 的全部内容。
115
120
  def read_ledger(cg) -> str:
116
121
  p = ledger_path(cg)
117
122
  if not os.path.exists(p):
@@ -124,7 +129,9 @@ def read_ledger(cg) -> str:
124
129
  # 渲染 / 解析
125
130
  # --------------------------------------------------------------------------
126
131
 
132
+ # 生效条件:field、before、after 任意取值下都返回 f"{field} {_short(before)}→{_short(after)}"。
127
133
  def _fmt_change(field, before, after) -> str:
134
+ # 生效条件:v 为 list/tuple 返回 f"{len(v)}条",为 dict 返回 f"{len(v)}键",为 None 返回 "—",为 float 返回 f"{v:g}",其余类型返回 str(v)。
128
135
  def _short(v):
129
136
  if isinstance(v, (list, tuple)):
130
137
  return f"{len(v)}条"
@@ -138,6 +145,7 @@ def _fmt_change(field, before, after) -> str:
138
145
  return f"{field} {_short(before)}→{_short(after)}"
139
146
 
140
147
 
148
+ # 生效条件:before 与 after 中任一为假值(None、{} 等)时该侧按 {} 参与比较,返回 STATE_FIELDS 中 b.get(k) != a.get(k) 的字段对应的 _fmt_change(k, b.get(k), a.get(k)) 列表。
141
149
  def diff(before, after):
142
150
  """状态差异 → 人类可读列表(只比认知状态字段)。"""
143
151
  b, a = before or {}, after or {}
@@ -145,6 +153,7 @@ def diff(before, after):
145
153
  for k in STATE_FIELDS if b.get(k) != a.get(k)]
146
154
 
147
155
 
156
+ # 生效条件:entry 含键 'entry_id'(缺失即 KeyError)时,按 _BULLET_ORDER 输出值不属 None/""/[]/{} 的字段行(node_id 假值显示 '—'),并在 entry.get("state") 为真值时追加 json 状态块,返回以 "\n" 连接的文本。
148
157
  def _fmt(entry: dict) -> str:
149
158
  lines = [f"## {entry['entry_id']} · `{entry.get('node_id') or '—'}`", ""]
150
159
  for k in _BULLET_ORDER:
@@ -162,6 +171,7 @@ def _fmt(entry: dict) -> str:
162
171
  return "\n".join(lines)
163
172
 
164
173
 
174
+ # 生效条件:text 中 _ENTRY_RE 匹配到 0 个条目时返回 [],匹配到则按各匹配区间解析为字典列表(node 为空或 "—" 时 node_id 置 None,state 段 json.loads 抛 ValueError 时 state 置 None,kind 缺省回落模块常量 KIND_CONDITION_GAP)。
165
175
  def _parse(text: str):
166
176
  out = []
167
177
  marks = list(_ENTRY_RE.finditer(text))
@@ -190,6 +200,7 @@ def _parse(text: str):
190
200
  # 认知状态
191
201
  # --------------------------------------------------------------------------
192
202
 
203
+ # 生效条件:cg.get(node_id) 为假值(含 None)时返回 None,否则返回 frontmatter 中属于 STATE_FIELDS 且值不为 None 的字段("layer" 缺失时用 node.get("path") 首段补上),保留 []/{}/"" 等显式空值。
193
204
  def state_of(cg, node_id):
194
205
  """抽取节点的认知状态(可回滚字段),节点不存在返回 None。
195
206
 
@@ -209,6 +220,7 @@ def state_of(cg, node_id):
209
220
  # 记录
210
221
  # --------------------------------------------------------------------------
211
222
 
223
+ # 生效条件:pattern.strip() 非空且 kind 属于模块常量 KINDS 时追加并返回条目 e(pattern 为空白或 kind 未知抛 ValueError;action 去空白后为空且 extra 为真时取 extra.pop("change", "") 作 action;before/after 任一 non-None 时 state 记入二者 or {};extra 中值不属 None/""/[]/{} 的键并入 e)。
212
224
  def record(cg, node_id=None, pattern="", missing="", action="", evidence="",
213
225
  source="", kind=KIND_CONDITION_GAP, before=None, after=None,
214
226
  extra=None):
@@ -249,6 +261,7 @@ def record(cg, node_id=None, pattern="", missing="", action="", evidence="",
249
261
  # 查询
250
262
  # --------------------------------------------------------------------------
251
263
 
264
+ # 生效条件:cg 给定时返回 _parse(read_ledger(cg)) 的列表——node_id 为真值则只留该节点记录、kind 为真值则只留该类型记录、newest_first 为真值则 reverse、limit 为真值时截断为前 int(limit) 条(limit 为 0/None 等假值时不截断)。
252
265
  def entries(cg, limit=None, node_id=None, kind=None, newest_first=True):
253
266
  recs = _parse(read_ledger(cg))
254
267
  if node_id:
@@ -262,6 +275,7 @@ def entries(cg, limit=None, node_id=None, kind=None, newest_first=True):
262
275
  return recs
263
276
 
264
277
 
278
+ # 生效条件:遍历 entries(cg, limit=0)(limit=0 为假值故不截断,覆盖全部记录),命中 r.get("entry_id") == entry_id 时返回该记录,否则返回 None。
265
279
  def show(cg, entry_id):
266
280
  for r in entries(cg, limit=0):
267
281
  if r.get("entry_id") == entry_id:
@@ -269,10 +283,12 @@ def show(cg, entry_id):
269
283
  return None
270
284
 
271
285
 
286
+ # 生效条件:cg 与 node_id 给定时返回 {"node_id": node_id, "entries": entries(cg, limit=limit, node_id=node_id)}——node_id 为假值时 entries 不按节点过滤,limit 默认 50 为真值故截断,传 0 等假值则不截断。
272
287
  def history(cg, node_id, limit=50):
273
288
  return {"node_id": node_id, "entries": entries(cg, limit=limit, node_id=node_id)}
274
289
 
275
290
 
291
+ # 生效条件:cg 给定时返回统计字典,其中 recs 取 entries(cg, limit=0) 全量、real 剔除 kind == 模块常量 KIND_ROLLBACK(missing 为空的条目不计入 by_missing、kind 缺省回落 KIND_CONDITION_GAP、source 缺省回落 "unknown"),top_patterns 取按 count 降序 pattern 升序排序后的前 int(limit) 项(limit 为 0 时切片为空列表)。
276
292
  def patterns(cg, limit=10):
277
293
  """规律统计:哪一维条件反复缺失、由谁触发、哪些规律重复出现。
278
294
 
@@ -301,6 +317,7 @@ def patterns(cg, limit=10):
301
317
  "by_source": by_source, "top_patterns": top[:int(limit)]}
302
318
 
303
319
 
320
+ # 生效条件:cg 给定时返回固定结构字典——missing_top 取 patterns(cg) 的 by_missing 中计数最大项的键(by_missing 为空则 "(暂无)"),top_pattern 取 top_patterns[0]["pattern"](为空则 "(暂无)"),recent 取 entries(cg, limit=5)。
304
321
  def summary(cg):
305
322
  p = patterns(cg)
306
323
  missing_top = (max(p["by_missing"].items(), key=lambda kv: kv[1])[0]
@@ -322,6 +339,7 @@ def summary(cg):
322
339
  # 回滚
323
340
  # --------------------------------------------------------------------------
324
341
 
342
+ # 生效条件:cg.get(node_id) 为假时返回 ([], [{"field": "*", "reason": "节点不存在"}]),否则先处理 target 中的 "layer"(为真且不同于当前 path 首段时经 cg._move_layer 迁移并 appended "layer",抛异常则记入 skipped,相同/为假时直接 appended "layer"),再按 STATE_FIELDS 处理:k 在 remove 中且存在于 frontmatter 则 pop 并 appended "-k",k 在 target 中且值相同则 appended k、不同则改写并在 changed 时经 cg._write_node 回写,最终返回 (applied, skipped)(若层迁移后 node 取不回则提前返回)。
325
343
  def _apply_state(cg, node_id, target, remove=()):
326
344
  """把目标状态写回节点。返回 (applied, skipped)。
327
345
 
@@ -375,6 +393,7 @@ def _apply_state(cg, node_id, target, remove=()):
375
393
  return applied, skipped
376
394
 
377
395
 
396
+ # 生效条件:cg 与 entry_id 定位 show(cg, entry_id) 后逐项判定——条目不存在返回未找到错误、kind == 模块常量 KIND_ROLLBACK 返回拒回滚错误、state 的 before 为假值返回无回滚状态错误、node_id 为假值返回未绑定节点错误、state_of 为 None 返回节点不可读错误;dry_run 为真值时返回含 current/target/would_remove/would_change 的预览;否则经 _apply_state(target, remove=after 中不属 before 且非 "layer" 的字段) 回写、尝试 cg.rebuild_index()(异常静默)并 record 一条 KIND_ROLLBACK 条目后返回 ok=True 结果。
378
397
  def rollback(cg, entry_id, dry_run=False, note=""):
379
398
  """把某条演化撤回其 before 状态,并记一条 rollback 条目(撤销不可静默)。"""
380
399
  src = show(cg, entry_id)
@@ -423,6 +442,7 @@ def rollback(cg, entry_id, dry_run=False, note=""):
423
442
  # 自描述
424
443
  # --------------------------------------------------------------------------
425
444
 
445
+ # 生效条件:无入参,任何调用都返回含 module/schema/ledger/carrier/principles/fields/kinds/state_fields/condition_dims/rollback/actions 的固定字典(kinds、state_fields、condition_dims 分别来自模块常量 KINDS、STATE_FIELDS、CONDITION_DIMS)。
426
446
  def catalog():
427
447
  return {
428
448
  "module": "evolution",
@@ -455,4 +475,4 @@ def catalog():
455
475
  },
456
476
  "actions": ["record", "entries", "show", "history", "patterns",
457
477
  "summary", "rollback", "catalog"],
458
- }
478
+ }
package/md_cg/export.py CHANGED
@@ -27,12 +27,14 @@ _ROW_KEYS = ("id", "layer", "path", "tags", "importance", "confidence",
27
27
  "created_at", "edges", "protected", "sensitivity", "content")
28
28
 
29
29
 
30
+ # 生效条件:给定 cg 与 kind 即返回 os.path.join(cg.root, f"export_{kind}_{当前 %Y%m%d_%H%M%S 时间戳}.jsonl"),无任何前置校验或分支。
30
31
  def _default_out(cg, kind: str) -> str:
31
32
  """默认导出路径:`<root>/export_<kind>_<ts>.jsonl`(可搬运、可灾备)。"""
32
33
  ts = time.strftime("%Y%m%d_%H%M%S")
33
34
  return os.path.join(cg.root, f"export_{kind}_{ts}.jsonl")
34
35
 
35
36
 
37
+ # 生效条件:cg.get(nid) 为 None 时返回 None;否则以 fm = node.get("frontmatter") or {}(缺键或假值回落空 dict)与 entry 组装行,layer 取 fm 的 layer、为假值时回落 entry.get("layer"),include_content 为真值时追加 content = node.get("content") or "",最终只保留 _ROW_KEYS 中实际存在的键。
36
38
  def _row(cg, nid: str, entry: dict, include_content: bool = True):
37
39
  """索引条目 → 导出行(回读节点拿到 frontmatter + 正文)。
38
40
 
@@ -62,6 +64,7 @@ def _row(cg, nid: str, entry: dict, include_content: bool = True):
62
64
  return {k: row[k] for k in _ROW_KEYS if k in row}
63
65
 
64
66
 
67
+ # 生效条件:源为 cg.index 的 nodes(缺 "nodes" 键或假值回落空 dict)——ids 为真值时只取其中确实在 nodes 里的 id,否则取全部——按 (float(created_at or 0), id) 排序后逐个产出同时满足 layer(为真时须 (e.layer or "") == layer)、tag(为真时须在 e.tags or [] 中)、since/until(非 None 时按 float 比较 created_at)的条目,limit 为真值且已产出 n 条并 n >= int(limit) 时停止(limit 为 0 或 None 不设上限)。
65
68
  def _iter_entries(cg, layer=None, since=None, until=None, tag=None,
66
69
  ids=None, limit=None):
67
70
  """按条件遍历索引条目(不读文件,保证筛选阶段零 IO)。
@@ -91,6 +94,7 @@ def _iter_entries(cg, layer=None, since=None, until=None, tag=None,
91
94
  return
92
95
 
93
96
 
97
+ # 生效条件:将 entries 逐条经 _row 转换后写入 out_path(先 makedirs 其父目录、写 out_path + ".tmp"、结束后 os.replace 为 out_path),_row 返回 None 的条目只累加 skipped_unreadable 而不写入,其余写入并累加 written 及 by_layer(row 的 layer 缺键或假值记为 "?");include_content 透传给 _row 决定行是否含正文;返回含 ok/out/written/skipped_unreadable/by_layer/bytes/elapsed_ms 的统计 dict。
94
98
  def _write_jsonl(cg, out_path: str, entries, include_content: bool = True):
95
99
  """流式写 JSONL(tmp + 原子改名)。返回统计 dict。"""
96
100
  out_path = os.path.abspath(out_path)
@@ -120,6 +124,7 @@ def _write_jsonl(cg, out_path: str, entries, include_content: bool = True):
120
124
  "elapsed_ms": round((time.time() - t0) * 1000, 1)}
121
125
 
122
126
 
127
+ # 生效条件:out 为假值(None/空串)时回落为 _default_out(cg, "graph"),以 _iter_entries(cg, layer=layer, limit=limit) 为条目流调用 _write_jsonl(include_content=include_content),再补 action="graph" 与 note 后返回该结果 dict。
123
128
  def export_graph(cg, out: str = None, layer=None, limit=None,
124
129
  include_content: bool = True):
125
130
  """全库导出(默认含正文)。"""
@@ -133,6 +138,7 @@ def export_graph(cg, out: str = None, layer=None, limit=None,
133
138
  return res
134
139
 
135
140
 
141
+ # 生效条件:ids 先按 [str(i) for i in (ids or []) if str(i).strip()] 规整,规整结果为空(ids 为 None/空容器/全空白项)时返回 {'ok': False, 'error': 'ids 不能为空'};否则 out 为假值时回落为 _default_out(cg, "nodes"),以 _iter_entries(cg, ids=ids) 为条目流调用 _write_jsonl(include_content=include_content),再补 action="nodes"、requested=len(ids)、missing=索引 keys 与 ids 的差集排序后返回。
136
142
  def export_nodes(cg, ids, out: str = None, include_content: bool = True):
137
143
  """按 id 列表导出(顺序 = 传入顺序)。"""
138
144
  ids = [str(i) for i in (ids or []) if str(i).strip()]
@@ -147,6 +153,7 @@ def export_nodes(cg, ids, out: str = None, include_content: bool = True):
147
153
  return res
148
154
 
149
155
 
156
+ # 生效条件:out 为假值(None/空串)时回落为 _default_out(cg, "slice"),以 _iter_entries(cg, layer=layer, since=since, until=until, tag=tag, limit=limit) 为条目流调用 _write_jsonl(include_content=include_content),再补 action="slice" 与记录 layer/since/until/tag 的 filter 后返回。
150
157
  def export_slice(cg, out: str = None, layer=None, since=None, until=None,
151
158
  tag=None, limit=None, include_content: bool = True):
152
159
  """按层 / 时间窗 / 标签切片导出(有界,便于增量搬运)。"""
@@ -159,6 +166,7 @@ def export_slice(cg, out: str = None, layer=None, since=None, until=None,
159
166
  return res
160
167
 
161
168
 
169
+ # 生效条件:只读 cg.index 的 nodes(缺 "nodes" 键或假值回落空 dict),逐条统计 layer(缺键或假值记 "?")、verification_basis(缺键或假值记 "(未声明)")、tags 计数(按计数降序取前 15)、protected 与 has_neg_conditions 为真值的条目数,以及 created_at 为真值时的 min/max(全为 0 或缺键时二者均为 None),返回含 total/by_layer/by_verification_basis/protected/with_non_applicable/top_tags/time_range/note 的 dict。
162
170
  def export_stat(cg):
163
171
  """导出前体检:层分布 / 验证基底 / 标签 Top / 时间范围。**只读索引,零 IO**。"""
164
172
  nodes = (cg.index.get("nodes") or {})
@@ -180,7 +188,8 @@ def export_stat(cg):
180
188
  if ca:
181
189
  t_min = ca if t_min is None else min(t_min, ca)
182
190
  t_max = ca if t_max is None else max(t_max, ca)
183
- top_tags = sorted(by_tag.items(), key=lambda kv: -kv[1])[:15]
191
+ top_tags = sorted(by_tag.items(),
192
+ key=lambda kv: (-kv[1], str(kv[0])))[:15]
184
193
  return {"ok": True, "action": "stat", "total": len(nodes),
185
194
  "by_layer": by_layer, "by_verification_basis": by_basis,
186
195
  "protected": protected, "with_non_applicable": with_neg,
@@ -190,6 +199,7 @@ def export_stat(cg):
190
199
  "如需密级分布请用 graph 导出后统计。")}
191
200
 
192
201
 
202
+ # 生效条件:act = (action or "graph").strip().lower()(action 为 None/空串等假值时取 "graph")——act 为 "graph"/"nodes"/"slice"/"stat" 时分别转调 export_graph/export_nodes/export_slice/export_stat(out、layer、limit、since、until、tag、ids 取自 kw 对应键,include_content 取 kw.get("include_content", True)),其它 act 值抛 ValueError。
193
203
  def run(cg, action: str = "graph", **kw):
194
204
  """export op 唯一入口。"""
195
205
  act = (action or "graph").strip().lower()
@@ -80,6 +80,7 @@ LOG_FILE = "_forgetting.jsonl"
80
80
 
81
81
  # ---------------------------------------------------------------- 三问
82
82
 
83
+ # 生效条件:role 与 verification_basis 各自经 str(x or "").strip().lower() 后按序判——role 命中模块常量 EXTERNAL_ROLES 返回 "external_surprising";否则 role 命中 INTERNAL_ROLES、或两者都不命中前者时 verification_basis 命中 DETERMINISTIC_BASIS,返回 "internal_deterministic";否则 role 为 "assistant"/"agent" 返回 "self_generated";全不命中返回 "unknown"。
83
84
  def source_kind(role=None, verification_basis=None):
84
85
  """Q3 的来源面:内部确定性产生 vs 外部惊奇来源。"""
85
86
  r = str(role or "").strip().lower()
@@ -95,6 +96,7 @@ def source_kind(role=None, verification_basis=None):
95
96
  return "unknown"
96
97
 
97
98
 
99
+ # 生效条件:new_grams 为空集(假值)时返回 0.0;非空时返回 len(new_grams & body_grams)/len(new_grams)。
98
100
  def _coverage(new_grams, body_grams):
99
101
  if not new_grams:
100
102
  return 0.0
@@ -107,6 +109,7 @@ def _coverage(new_grams, body_grams):
107
109
  _TEMPLATE_LABELS = ("功能名", "生效条件", "子功能", "执行", "验证方式", "不适用条件")
108
110
 
109
111
 
112
+ # 生效条件:content 为 None 或假值时按 "" 处理,结果为空串;否则逐行剥离 "#" 与 _TEMPLATE_LABELS 标签后以 "" 直接拼接。
110
113
  def payload(content):
111
114
  """剥离 CCG 固定标签后的**内容骨架**(保留字段值,丢弃字段名与标记)。"""
112
115
  out = []
@@ -123,6 +126,7 @@ def payload(content):
123
126
  return "".join(out)
124
127
 
125
128
 
129
+ # 生效条件:content 经 payload/bigrams 得空集合时直接返回零值 best(max=0.0、with=None、compared=0);否则遍历 cg.index 的 nodes,跳过 nid==exclude,layer 为真值时只比较 str(layer 字段 or "")==layer 的节点,cg.get(nid) 抛异常/返回假值、或该节点 content 的 bigrams 为空则跳过,每计入一个节点后若 n>=limit 立即 break(故 limit 为 0 或负数时只比较首项即停),返回覆盖度最大者 best(无覆盖度提升时不更新 with/jaccard,compared 为实际计入数)。
126
130
  def redundancy(cg, content, layer="contextual", exclude=None, limit=MAX_COMPARE):
127
131
  """Q1 重复?——新内容被既有同层节点覆盖的最大比例。"""
128
132
  new = bigrams(payload(content))
@@ -157,6 +161,7 @@ def redundancy(cg, content, layer="contextual", exclude=None, limit=MAX_COMPARE)
157
161
  return best
158
162
 
159
163
 
164
+ # 生效条件:dup 必填并转 float;dup=0 时 p 取 EPS,返回 -log2(EPS) 这一有限大值;dup>=1 时返回 0.0。
160
165
  def self_information(dup):
161
166
  """Q3 的自信息代理:I = -log2(min(1, dup + ε)),单位 bit。
162
167
 
@@ -167,6 +172,7 @@ def self_information(dup):
167
172
  return -math.log(p, 2.0)
168
173
 
169
174
 
175
+ # 生效条件:hint 非 None 且可转 float(含 hint=0)时返回 from="hint" 的裁剪分数;否则用 novelty、SOURCE_WEIGHT.get(kind, SOURCE_WEIGHT["unknown"])、len(content)/200 三因子启发式。
170
176
  def importance_score(hint, novelty, kind, content):
171
177
  """Q2 重要?——显式 hint 优先,否则启发式(对齐 longterm_snapshot 四因子简化版)。"""
172
178
  if hint is not None:
@@ -182,6 +188,7 @@ def importance_score(hint, novelty, kind, content):
182
188
  return {"score": round(max(0.0, min(1.0, s)), 4), "from": "heuristic"}
183
189
 
184
190
 
191
+ # 生效条件:以 source_kind(role,verification_basis) 的 kind 与 redundancy(cg,content,layer=layer,exclude=node_id) 的 red["max"] 为输入,按 if/elif 顺序取首个命中分支——imp["score"]≥PROTECT_IMPORTANCE→"ACCEPT";否则 kind=="internal_deterministic" 且 red["max"]≥DUP_DROP→"DROP";否则 red["max"]≥DUP_MERGE→"MERGE";否则 red["max"]≥DUP_DROP 且 imp["score"]<IMPORTANCE_MIN→"DEFER";否则 imp["score"]≥IMPORTANCE_MIN→"ACCEPT";否则 novelty≥NOVELTY_MIN→"ACCEPT";否则→"DEFER"。
185
192
  def assess(cg, content, layer="contextual", role=None, verification_basis=None,
186
193
  importance_hint=None, node_id=None):
187
194
  """三问 → 四态裁决。返回完整判据(可审计,不只给结论)。"""
@@ -226,6 +233,7 @@ def assess(cg, content, layer="contextual", role=None, verification_basis=None,
226
233
 
227
234
  # ---------------------------------------------------------------- 落库动作
228
235
 
236
+ # 生效条件:cg 与 rec 必填;append_jsonl 写 cg.root/LOG_FILE 抛任意异常时被吞掉,仍返回 rec。
229
237
  def log(cg, rec):
230
238
  """裁决留痕(append-only)。DROP/DEFER 也留痕——否则遗忘变黑箱。"""
231
239
  try:
@@ -235,6 +243,7 @@ def log(cg, rec):
235
243
  return rec
236
244
 
237
245
 
246
+ # 生效条件:cg 与 node_id 必填,delta 默认 0.05;cg.get(node_id) 抛异常或返回假值时返回 None;imp 跨过 PROTECT_IMPORTANCE 即写 protected。
238
247
  def reinforce(cg, node_id, delta=0.05):
239
248
  """MERGE 的落库动作:不新增节点,把「又一次见到」折算成既有节点的强化。
240
249
 
@@ -276,6 +285,7 @@ def reinforce(cg, node_id, delta=0.05):
276
285
  "merge_count": fm["merge_count"], "protected": bool(fm.get("protected"))}
277
286
 
278
287
 
288
+ # 生效条件:cg 必填,limit 默认 100;日志路径不存在时返回 [];否则返回 out[-limit:],limit=0 时 -0 退化为 out[0:] 即全量。
279
289
  def history(cg, limit=100):
280
290
  """读取遗忘留痕(最近 limit 条)。"""
281
291
  p = os.path.join(cg.root, LOG_FILE)
@@ -296,6 +306,7 @@ def history(cg, limit=100):
296
306
  return out[-limit:]
297
307
 
298
308
 
309
+ # 生效条件:cg 必填;日志路径不存在返回 {"total": 0, "by_verdict": {}};否则流式累计行数与 verdict 分布。
299
310
  def summary(cg):
300
311
  """遗忘留痕聚合(流式,不把全量日志读进内存):总数 + 四态分布。"""
301
312
  p = os.path.join(cg.root, LOG_FILE)
@@ -339,6 +350,7 @@ VERIFIED_BASES = ("formal_proof", "compiler", "test", "textbook", "public_kb")
339
350
  TIER_WORKING = 0.40
340
351
 
341
352
 
353
+ # 生效条件:e 必填;protected 为真、importance>=PROTECT_IMPORTANCE、或 evidence_count>=3 且 verification_basis 在 VERIFIED_BASES → "longterm";importance>=TIER_WORKING 或 vb 在 VERIFIED_BASES → "working";否则 "candidate"。
342
354
  def _tier_of(e: dict) -> str:
343
355
  """索引快照 → 分层:longterm(长期)/ working(工作)/ candidate(候选待评估)。"""
344
356
  imp = float(e.get("importance", 0.5) or 0.5)
@@ -351,19 +363,23 @@ def _tier_of(e: dict) -> str:
351
363
  return "candidate"
352
364
 
353
365
 
366
+ # 生效条件:e 必填;e["edges"] 为假值(缺失/空列表)且 e["subgraph"] 为假值时返回 True,否则 False。
354
367
  def _is_island(e: dict) -> bool:
355
368
  """无边孤岛:既无出边也无子图声明(夜间整理的首要候选)。"""
356
369
  return (not (e.get("edges") or [])) and (not e.get("subgraph"))
357
370
 
358
371
 
372
+ # 生效条件:cg 必填且提供 cg.root,恒返回 os.path.join(cg.root, LONGTERM_DIR)。
359
373
  def longterm_dir(cg) -> str:
360
374
  return os.path.join(cg.root, LONGTERM_DIR)
361
375
 
362
376
 
377
+ # 生效条件:cg 必填,恒返回 longterm_dir(cg) 下的 "current.json" 路径。
363
378
  def current_path(cg) -> str:
364
379
  return os.path.join(longterm_dir(cg), "current.json")
365
380
 
366
381
 
382
+ # 生效条件:apply 为真且由 cg.index 的 nodes(layer 为假值时不过滤、为真时仅取 layer 字段相等者,max_rows 为真值时先取 ids[:int(max_rows)])算出的 snapshot_id 与 current.json 所记 snapshot_id 不同或其记录的 path 文件不存在(same 为假)时,才写断面文件、原子更新 current 指针、执行 _prune 并追加维护日志;apply 为假时只返回 dry_run=True 的统计(out 与 force 在源码中未被引用)。
367
383
  def longterm_assess(cg, apply=False, out=None, layer=None, keep=LONGTERM_KEEP,
368
384
  max_rows=None, force=False, actor="maintain"):
369
385
  """评估后分层落盘:生成一个可回溯的长期记忆断面。
@@ -440,6 +456,7 @@ def longterm_assess(cg, apply=False, out=None, layer=None, keep=LONGTERM_KEEP,
440
456
  }
441
457
 
442
458
 
459
+ # 生效条件:cg 与 keep 必填;keep<=0 时不删除任何断面返回 [];否则删除除最近 keep 个 .jsonl 外的旧断面。
443
460
  def _prune(cg, keep):
444
461
  """只保留最近 keep 个断面文件(按文件名时间前缀排序)。"""
445
462
  d = longterm_dir(cg)
@@ -457,6 +474,7 @@ def _prune(cg, keep):
457
474
  return removed
458
475
 
459
476
 
477
+ # 生效条件:cg 必填,limit 默认 20;目录不可读返回 [];否则新的在前逐个 append,因先 append 后判 len(out)>=limit,limit=0 时仍返回 1 条快照。
460
478
  def longterm_list(cg, limit=20):
461
479
  """列出历史断面(新的在前):{snapshot_id, path, ts, total, tiers}。"""
462
480
  d = longterm_dir(cg)
@@ -481,6 +499,7 @@ def longterm_list(cg, limit=20):
481
499
  return {"current": cur, "snapshots": out}
482
500
 
483
501
 
502
+ # 生效条件:longterm_dir(cg) 不可列出(OSError)时返回 {"ok":False,"error":"no_snapshot"};否则在倒序文件名中取首个满足 snapshot_id 为 None 或为其子串的 .jsonl(snapshot_id="" 与任意文件名匹配),无匹配返回 {"ok":False,"error":"snapshot_not_found"};命中则逐行聚合该文件(空行与 json.loads 抛 ValueError 的行跳过),返回 file/total/tiers/by_layer/islands。
484
503
  def longterm_show(cg, snapshot_id=None):
485
504
  """读取某个断面的分层统计(不载全量行,只聚合)。"""
486
505
  d = longterm_dir(cg)
@@ -524,6 +543,7 @@ def longterm_show(cg, snapshot_id=None):
524
543
  # 写入**之前**的新奇检测:重复项并入既有(MERGE),而非新增;无关噪音丢弃;
525
544
  # 有歧义的半重复留痕待复核。这是「写入侧前置」的落库动作,比夜间整理更早一步。
526
545
 
546
+ # 生效条件:cg 与 content 必填;恒经 assess 得四态并映射 decision(ACCEPT→write 等),落留痕后返回 ok=True,不写任何节点。
527
547
  def prefeed(cg, content, layer="contextual", role=None, verification_basis=None,
528
548
  importance_hint=None, node_id=None):
529
549
  """前馈裁决(不写盘):返回四态 + 判据,并留痕 `_forgetting.jsonl`。
@@ -548,13 +568,15 @@ def prefeed(cg, content, layer="contextual", role=None, verification_basis=None,
548
568
  return {"ok": True, "action": "prefeed", **rec}
549
569
 
550
570
 
571
+ # 生效条件:content 为 None 或假值时按 "" 计算,恒返回 "pre_"+sha1(content).hexdigest()[:12]。
551
572
  def _prefeed_id(content):
552
573
  return "pre_" + hashlib.sha1((content or "").encode("utf-8")).hexdigest()[:12]
553
574
 
554
575
 
576
+ # 生效条件:cg 必填,limit 默认 100;action 为假值(None/空串)时不过滤,真值只留该 action;返回 recs[-int(limit):],limit=0 时退化为全量。
555
577
  def maintain_history(cg, limit=100, action=None):
556
578
  """维护留痕(`_maintain.jsonl` 最近 limit 条),可按 action 过滤。"""
557
579
  recs = list(read_jsonl(os.path.join(cg.root, MAINTAIN_LOG)))
558
580
  if action:
559
581
  recs = [r for r in recs if r.get("action") == action]
560
- return recs[-int(limit):]
582
+ return recs[-int(limit):]
package/md_cg/fsutil.py CHANGED
@@ -28,6 +28,7 @@ _RENAME_TRIES = 20
28
28
  _RENAME_WAIT = 0.005
29
29
 
30
30
 
31
+ # 生效条件:tmp 与 path 给定后循环至多 _RENAME_TRIES 次调用 os.replace(tmp, path),成功即返回;仅捕获 PermissionError,非最后一次则 time.sleep(_RENAME_WAIT) 重试,最后一次仍 PermissionError 则抛出。
31
32
  def _publish(tmp: str, path: str):
32
33
  """把临时文件 rename 到位,Windows 上短重试。"""
33
34
  for i in range(_RENAME_TRIES):
@@ -40,6 +41,7 @@ def _publish(tmp: str, path: str):
40
41
  time.sleep(_RENAME_WAIT)
41
42
 
42
43
 
44
+ # 生效条件:path 与 data 给定时取 path 所在目录 d 建目录,用 tempfile.mkstemp 在 d 内建临时文件按 encoding 写入 data,durable 为真才 flush+os.fsync(假值不 fsync),再经 _publish(tmp, path) 替换;任一步失败时 finally 里若 tmp 仍非 None 且 os.path.exists(tmp) 为真则 os.remove(OSError 忽略)。
43
45
  def atomic_write(path: str, data: str, encoding: str = "utf-8", durable: bool = False):
44
46
  """整文件替换。临时文件与目标同目录(保证同一文件系统,rename 才原子),
45
47
  临时名唯一(并发写者不共享),失败即清理而不是留在可能刚写满的磁盘上。
@@ -71,6 +73,7 @@ def atomic_write(path: str, data: str, encoding: str = "utf-8", durable: bool =
71
73
  pass
72
74
 
73
75
 
76
+ # 生效条件:os.path.isdir(d) 为假时直接返回;否则对 d 下名字以 "." 开头且含 ".tmp-" 的条目,当 now - os.path.getmtime(p) > older_than 时 os.remove(p)(OSError 忽略),其余条目不动。
74
77
  def sweep_stale_temps(d: str, older_than: float = 3600):
75
78
  """清理被杀死的进程留下的唯一命名临时文件(它们不会被下一个写者复用清掉)。"""
76
79
  if not os.path.isdir(d):
@@ -87,6 +90,7 @@ def sweep_stale_temps(d: str, older_than: float = 3600):
87
90
  pass
88
91
 
89
92
 
93
+ # 生效条件:path 加 ".lock" 后缀作为锁文件,进入时按 timeout 秒内以 poll 间隔轮询获取 OS 级排它锁(IS_WIN 用 msvcrt.locking 锁首字节,否则 fcntl.flock),超时仍未获锁时 strict 为真抛 TimeoutError、否则返回自身放行。
90
94
  class FileLock:
91
95
  """跨进程排它锁(OS 级)。
92
96
 
@@ -102,6 +106,7 @@ class FileLock:
102
106
  好过静默放行后退化为无锁并发(丢一条提案/裁决比让写入者等一下代价大)。
103
107
  """
104
108
 
109
+ # 生效条件:path 加 ".lock" 后缀存入 self.path,timeout、poll、strict 原样保存,并置 self._f = None、self.acquired = False。
105
110
  def __init__(self, path: str, timeout: float = 10.0, poll: float = 0.01,
106
111
  strict: bool = False):
107
112
  self.path = path + ".lock"
@@ -111,6 +116,7 @@ class FileLock:
111
116
  self._f = None
112
117
  self.acquired = False
113
118
 
119
+ # 生效条件:先按 self.path 建父目录并 open(self.path, "a+b"),再在 self.timeout 到期前每 self.poll 秒尝试加锁(IS_WIN 用 msvcrt.locking(LK_NBLCK),否则 fcntl.flock(LOCK_EX|LOCK_NB));成功即置 self.acquired=True 并返回 self;OSError 的 errno 不在 (EACCES, EAGAIN, EDEADLK) 时直接 raise,超时后 self.strict 为真抛 TimeoutError、否则返回 self 放行。
114
120
  def __enter__(self):
115
121
  os.makedirs(os.path.dirname(os.path.abspath(self.path)) or ".", exist_ok=True)
116
122
  self._f = open(self.path, "a+b")
@@ -134,6 +140,7 @@ class FileLock:
134
140
  return self # 放行,不阻断写路径
135
141
  time.sleep(self.poll)
136
142
 
143
+ # 生效条件:self.acquired 为真时按 IS_WIN 用 msvcrt.locking(LK_UNLCK) 或 fcntl.flock(LOCK_UN) 解锁(OSError 被吞掉);finally 中只要 self._f 为真就 close,随后 self._f=None、self.acquired=False。
137
144
  def __exit__(self, *exc):
138
145
  try:
139
146
  if self.acquired:
@@ -151,6 +158,7 @@ class FileLock:
151
158
  self.acquired = False
152
159
 
153
160
 
161
+ # 生效条件:os.path.getsize(path) 为 0 时返回 False;否则二进制打开 path 并从 size-1 处读 1 字节,返回 f.read(1) != b"\n";getsize/open/seek/read 抛 OSError 时返回 False。
154
162
  def ends_mid_line(path: str) -> bool:
155
163
  """行式日志的最后一字节是否不是换行——即上一个写者被杀死留下的半截记录。
156
164
  追加者若不先补一个换行,新记录会粘在这行上,两条都解析不出来。"""
@@ -165,6 +173,7 @@ def ends_mid_line(path: str) -> bool:
165
173
  return False
166
174
 
167
175
 
176
+ # 生效条件:record 序列化为 json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n";若 ends_mid_line(path) 为真则在行首再补一个 "\n";随后建父目录并以 O_CREAT|O_WRONLY|O_APPEND、权限 0o600 打开 path 写入该行 UTF-8 字节后关闭。
168
177
  def append_jsonl(path: str, record: dict):
169
178
  """向 append-only 日志追加一条记录(best-effort 语义)。
170
179
 
@@ -187,6 +196,7 @@ def append_jsonl(path: str, record: dict):
187
196
  os.close(fd)
188
197
 
189
198
 
199
+ # 生效条件:os.path.exists(path) 为假时生成器直接结束不产出;否则逐行 strip,空行跳过,json.loads 成功则 yield 该对象,抛 ValueError 的行跳过,其余异常不捕获。
190
200
  def read_jsonl(path: str):
191
201
  """读 append-only 日志,跳过被截断/粘连的坏行(不因自身簿记而失败)。"""
192
202
  if not os.path.exists(path):
@@ -205,6 +215,7 @@ def read_jsonl(path: str):
205
215
  _COUNT_CACHE = {} # abspath -> (bytes_scanned, mtime_ns, lines)
206
216
 
207
217
 
218
+ # 生效条件:os.stat(os.path.abspath(path or "")) 抛 OSError 时返回 0;缓存命中且已扫字节数与 mtime_ns 均与 stat 一致时直接返回缓存计数;若缓存已扫字节 < 当前 size 且 mtime_ns 不同则从该偏移起按 chunk 分块累计 b"\n" 个数并加上缓存值;读文件抛 OSError 时返回 total or 0(已累计值为假则返回 0)。
208
219
  def count_jsonl(path: str, chunk: int = 1 << 20) -> int:
209
220
  """数 append-only 日志的行数——**流式计数、不物化**(内存 O(1))。
210
221
 
@@ -255,6 +266,7 @@ def count_jsonl(path: str, chunk: int = 1 << 20) -> int:
255
266
  return total
256
267
 
257
268
 
269
+ # 生效条件:directory 经 abspath 后作为分片目录并 makedirs(exist_ok=True),实例分片文件名由 os.getpid() 与 uuid.uuid4().hex[:8] 拼成 "{pid}-{hex8}.log",append 时各写者只写自己这一分片,从而不共享写入点。
258
270
  class ShardedLog:
259
271
  """每写者独占一个分片的 append-only 日志——不能丢记录时用它。
260
272
 
@@ -263,6 +275,7 @@ class ShardedLog:
263
275
  代价是读取要合并 N 个分片,靠记录里的单调序号 (t, seq) 恢复全局写入顺序。
264
276
  """
265
277
 
278
+ # 生效条件:directory 经 abspath 存入 self.dir 并 makedirs(exist_ok=True),self.path 为 self.dir 下 "{os.getpid()}-{uuid.uuid4().hex[:8]}.log",并置 self._seq = 0、self._fh = None。
266
279
  def __init__(self, directory: str):
267
280
  self.dir = os.path.abspath(directory)
268
281
  os.makedirs(self.dir, exist_ok=True)
@@ -271,6 +284,7 @@ class ShardedLog:
271
284
  self._seq = 0
272
285
  self._fh = None
273
286
 
287
+ # 生效条件:self._seq 先自增 1,record 被 dict(record, _t=time.time(), _s=self._seq) 复制;self._fh 为 None 时以 "a"、encoding="utf-8"、newline="\n" 打开 self.path,随后写入 json.dumps(ensure_ascii=False, separators=(",", ":")) + "\n" 并 flush。
274
288
  def append(self, record: dict):
275
289
  self._seq += 1
276
290
  record = dict(record, _t=time.time(), _s=self._seq)
@@ -280,12 +294,14 @@ class ShardedLog:
280
294
  separators=(",", ":")) + "\n")
281
295
  self._fh.flush()
282
296
 
297
+ # 生效条件:幂等;self._fh 为真值时 flush 并关闭句柄、再把 self._fh 置 None,为 None 时直接返回不报错;
283
298
  def close(self):
284
299
  if self._fh:
285
300
  self._fh.close()
286
301
  self._fh = None
287
302
 
288
303
  @staticmethod
304
+ # 生效条件:directory 是目录时,按 sorted(os.listdir(directory)) 顺序对每个以 ".log" 结尾的文件调用 read_jsonl 汇总记录,再按每条记录 r.get("_t", 0)、r.get("_s", 0)(缺键取 0)排序后返回全部记录;directory 不是目录时直接返回 []。
289
305
  def read_all(directory: str):
290
306
  """按全局写入顺序回放所有分片。"""
291
307
  if not os.path.isdir(directory):
@@ -299,6 +315,7 @@ class ShardedLog:
299
315
  return recs
300
316
 
301
317
  @staticmethod
318
+ # 生效条件:directory 是目录时,遍历 os.listdir(directory),对以 ".log" 结尾且不满足「keep 为真值且 os.path.abspath(p) == keep」的条目调用 os.remove(keep 为 None/空串等假值时该排除条件恒不成立,所有 ".log" 条目都会被删),删除时的 OSError 被忽略;directory 不是目录时直接返回。
302
319
  def clear(directory: str, keep: str = None):
303
320
  """合并进快照后清理分片。keep 用于保留当前进程正在写的那个。"""
304
321
  if not os.path.isdir(directory):
@@ -310,4 +327,4 @@ class ShardedLog:
310
327
  try:
311
328
  os.remove(p)
312
329
  except OSError:
313
- pass
330
+ pass