@furongjun1999/dsh-memory 0.4.7 → 0.4.9

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 (325) hide show
  1. package/README.md +130 -47
  2. package/codebuddy/CODEBUDDY.md +21 -10
  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/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
  7. package/docs/README.md +111 -0
  8. package/docs/discipline/harnesses.yaml +226 -152
  9. package/docs/discipline/templates/full.md.tmpl +61 -58
  10. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
  11. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
  12. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
  13. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  14. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  15. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  16. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  17. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  18. package/docs/experiments/m4-role-probe/census.py +86 -0
  19. package/docs/experiments/m4-role-probe/probe.py +89 -0
  20. package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
  21. package/docs/experiments/mapped_confidence/post_check.py +75 -0
  22. package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
  23. package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
  31. 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
  32. package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  33. package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
  34. 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
  35. 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
  36. 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 +263 -0
  37. package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
  38. 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.5.md +236 -0
  39. package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
  40. package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
  41. 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
  42. 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
  43. package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
  44. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
  45. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +4 -4
  46. package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
  47. 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
  48. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
  49. package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
  50. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
  51. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
  52. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -0
  53. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
  54. 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 +6 -6
  55. package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
  56. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -0
  57. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
  58. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
  59. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
  60. 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 -405
  61. 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
  62. package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
  63. package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
  64. package/dsh/README.md +34 -1
  65. package/dsh/cordis.yml.example +13 -7
  66. package/dsh/hive-mcp-probe.mjs +94 -0
  67. package/dsh/hive-mcp.example.yml +62 -0
  68. package/dsh/update-lingshu.bat +11 -0
  69. package/dsh/update-lingshu.ps1 +337 -0
  70. package/lib/hooks.d.ts +8 -0
  71. package/lib/hooks.js +27 -24
  72. package/lib/index.d.ts +4 -2
  73. package/lib/index.js +29 -6
  74. package/lib/lib/datapath.d.ts +76 -1
  75. package/lib/lib/datapath.js +199 -13
  76. package/lib/lib/mdcg_client.d.ts +6 -3
  77. package/lib/lib/mdcg_client.js +6 -5
  78. package/lib/lib/mutual.js +4 -4
  79. package/lib/lib/prompt_safety.d.ts +48 -0
  80. package/lib/lib/prompt_safety.js +62 -0
  81. package/lib/lib/token_store.js +4 -5
  82. package/md_cg/audit.py +91 -2
  83. package/md_cg/autonomy.py +86 -15
  84. package/md_cg/backfill.py +36 -1
  85. package/md_cg/backfill_bigdomain.py +34 -0
  86. package/md_cg/bench6_arms.py +28 -1
  87. package/md_cg/bench6_common.py +10 -1
  88. package/md_cg/bench6_competitors.py +6 -1
  89. package/md_cg/bench_axis_domain.py +9 -1
  90. package/md_cg/bench_blind_comp.py +7 -1
  91. package/md_cg/bench_en_atoms_public.py +9 -0
  92. package/md_cg/bench_governance.py +348 -0
  93. package/md_cg/bench_lme_zh.py +16 -1
  94. package/md_cg/bench_locomo.py +2 -1
  95. package/md_cg/bench_locomo_zh.py +16 -1
  96. package/md_cg/bench_locomo_zh_public.py +4 -1
  97. package/md_cg/bench_longmem.py +2 -1
  98. package/md_cg/bench_membench.py +37 -6
  99. package/md_cg/bench_p0.py +4 -1
  100. package/md_cg/bench_progressive.py +61 -10
  101. package/md_cg/bench_role_views.py +238 -0
  102. package/md_cg/bench_task_ab.py +8 -1
  103. package/md_cg/bench_task_ab_llm.py +13 -1
  104. package/md_cg/bench_unified_en.py +6 -1
  105. package/md_cg/bench_zh_mad.py +20 -1
  106. package/md_cg/blindspot_tickets.py +123 -0
  107. package/md_cg/branches.py +286 -0
  108. package/md_cg/build_postings.py +73 -0
  109. package/md_cg/ccgc.py +949 -0
  110. package/md_cg/census.py +5 -1
  111. package/md_cg/chain.py +24 -3
  112. package/md_cg/codeindex.py +134 -17
  113. package/md_cg/coldverify.py +265 -0
  114. package/md_cg/comment_gate.py +338 -0
  115. package/md_cg/cond_compose.py +190 -0
  116. package/md_cg/cond_facts.py +155 -0
  117. package/md_cg/cond_template.json +107 -0
  118. package/md_cg/condition_anchor.py +143 -0
  119. package/md_cg/conformance.py +727 -0
  120. package/md_cg/consistency.py +25 -2
  121. package/md_cg/consolidate.py +53 -2
  122. package/md_cg/corpus.py +5 -1
  123. package/md_cg/crosscheck.py +42 -2
  124. package/md_cg/crypto.py +35 -1
  125. package/md_cg/d_meta.py +310 -0
  126. package/md_cg/datapath.py +201 -26
  127. package/md_cg/docindex.py +122 -1
  128. package/md_cg/eval_common.py +35 -6
  129. package/md_cg/evidence.py +27 -1
  130. package/md_cg/evolution.py +21 -1
  131. package/md_cg/export.py +11 -1
  132. package/md_cg/forgetting.py +34 -4
  133. package/md_cg/fsutil.py +81 -2
  134. package/md_cg/hotcache.py +214 -0
  135. package/md_cg/hyperedge.py +251 -0
  136. package/md_cg/identity.py +20 -3
  137. package/md_cg/insight.py +19 -3
  138. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  139. package/md_cg/lexicon/build_standard_en.py +171 -168
  140. package/md_cg/lexicon/expand_en_zh.py +6 -0
  141. package/md_cg/lifecycle.py +273 -0
  142. package/md_cg/linkref.py +281 -0
  143. package/md_cg/links.py +29 -1
  144. package/md_cg/mcp_server.py +894 -168
  145. package/md_cg/md_whitebox.py +53 -1
  146. package/md_cg/mdcg.py +1205 -41
  147. package/md_cg/mdcos.py +1060 -76
  148. package/md_cg/metacognition.py +39 -4
  149. package/md_cg/migrate.py +4 -0
  150. package/md_cg/migrate_aeis.py +221 -213
  151. package/md_cg/migrate_roleplay.py +8 -0
  152. package/md_cg/migrate_wisdom_graph.py +14 -1
  153. package/md_cg/mreview/__init__.py +25 -0
  154. package/md_cg/mreview/__main__.py +110 -0
  155. package/md_cg/mreview/bundle.py +178 -0
  156. package/md_cg/mreview/candidates.py +262 -0
  157. package/md_cg/mreview/govern.py +694 -0
  158. package/md_cg/mreview/locate.py +939 -0
  159. package/md_cg/mreview/pipeline.py +729 -0
  160. package/md_cg/mreview/rules/duplication.json +21 -0
  161. package/md_cg/mreview/rules/field_coverage.json +54 -0
  162. package/md_cg/mreview/rules/source_license.json +21 -0
  163. package/md_cg/mreview/rules/template_flow.json +21 -0
  164. package/md_cg/mreview/ruleset.py +253 -0
  165. package/md_cg/nodefile.py +269 -1
  166. package/md_cg/pooling.py +23 -1
  167. package/md_cg/postings.py +298 -0
  168. package/md_cg/predict.py +113 -15
  169. package/md_cg/progressive.py +3 -0
  170. package/md_cg/protect.py +18 -5
  171. package/md_cg/protocol.py +372 -0
  172. package/md_cg/provenance.py +262 -0
  173. package/md_cg/reach.py +453 -0
  174. package/md_cg/refindex.py +88 -4
  175. package/md_cg/refine.py +20 -1
  176. package/md_cg/roleviews.py +89 -0
  177. package/md_cg/routing.py +76 -0
  178. package/md_cg/scrub.py +68 -3
  179. package/md_cg/security.py +26 -1
  180. package/md_cg/self_state.py +65 -2
  181. package/md_cg/selfreport.py +151 -0
  182. package/md_cg/semantic/canonical.py +5 -0
  183. package/md_cg/semantic/en_normalizer.py +364 -295
  184. package/md_cg/semantic/zh_en_atoms.py +139 -136
  185. package/md_cg/signer.py +41 -1
  186. package/md_cg/sources.py +583 -547
  187. package/md_cg/statushdr.py +179 -0
  188. package/md_cg/stg.py +59 -18
  189. package/md_cg/subgraph.py +23 -0
  190. package/md_cg/sustain.py +74 -1
  191. package/md_cg/tasks.py +471 -0
  192. package/md_cg/test_action_derive.py +203 -0
  193. package/md_cg/test_audit_rotate.py +270 -0
  194. package/md_cg/test_autonomy.py +26 -0
  195. package/md_cg/test_bench_governance.py +102 -0
  196. package/md_cg/test_blindspot_tickets.py +166 -0
  197. package/md_cg/test_branches.py +249 -0
  198. package/md_cg/test_ccg_perturb.py +1 -1
  199. package/md_cg/test_ccgc.py +433 -0
  200. package/md_cg/test_codeindex.py +338 -0
  201. package/md_cg/test_comment_gate.py +187 -0
  202. package/md_cg/test_cond_compose_anchors.py +76 -0
  203. package/md_cg/test_condition_anchor.py +82 -0
  204. package/md_cg/test_conformance.py +343 -0
  205. package/md_cg/test_d_meta.py +412 -0
  206. package/md_cg/test_datapath_root.py +188 -0
  207. package/md_cg/test_en_pipeline.py +24 -0
  208. package/md_cg/test_gain_gate.py +47 -1
  209. package/md_cg/test_health_scale.py +173 -0
  210. package/md_cg/test_hot_cold.py +187 -0
  211. package/md_cg/test_hyperedge.py +245 -0
  212. package/md_cg/test_identity_attribution.py +6 -0
  213. package/md_cg/test_index_durability.py +224 -0
  214. package/md_cg/test_lifecycle.py +309 -0
  215. package/md_cg/test_linkref.py +306 -0
  216. package/md_cg/test_md_access_parity.py +15 -3
  217. package/md_cg/test_mr_m1.py +769 -0
  218. package/md_cg/test_mr_m2.py +587 -0
  219. package/md_cg/test_mr_m3.py +710 -0
  220. package/md_cg/test_mr_m4.py +485 -0
  221. package/md_cg/test_p1.py +1 -1
  222. package/md_cg/test_p11_consistency.py +1 -1
  223. package/md_cg/test_p12_metacognition.py +2 -2
  224. package/md_cg/test_p16_self_state.py +1 -1
  225. package/md_cg/test_p26_refindex.py +50 -21
  226. package/md_cg/test_p27_docindex.py +258 -4
  227. package/md_cg/test_p28_refcheck.py +1 -1
  228. package/md_cg/test_p29_session_ingest_export.py +1 -1
  229. package/md_cg/test_p2_mcp.py +7 -1
  230. package/md_cg/test_p31_insight.py +24 -0
  231. package/md_cg/test_p38_contextualize.py +1 -1
  232. package/md_cg/test_p39_vision_evidence.py +1 -1
  233. package/md_cg/test_p40_refine_worklist.py +1 -1
  234. package/md_cg/test_p41_evolve_patrol.py +1 -1
  235. package/md_cg/test_p42_provenance.py +1 -1
  236. package/md_cg/test_p43_pooling.py +41 -13
  237. package/md_cg/test_p44_md_whitebox.py +14 -1
  238. package/md_cg/test_protocol.py +243 -0
  239. package/md_cg/test_reach.py +378 -0
  240. package/md_cg/test_reach_keys.py +201 -0
  241. package/md_cg/test_reach_meta_exits.py +145 -0
  242. package/md_cg/test_read_clip.py +141 -0
  243. package/md_cg/test_retr_s1.py +340 -0
  244. package/md_cg/test_retr_s1b.py +209 -0
  245. package/md_cg/test_retr_s3.py +194 -0
  246. package/md_cg/test_retr_s4.py +163 -0
  247. package/md_cg/test_retr_s5.py +200 -0
  248. package/md_cg/test_retr_s6.py +157 -0
  249. package/md_cg/test_retr_s7.py +385 -0
  250. package/md_cg/test_retr_s8_time.py +316 -0
  251. package/md_cg/test_retr_s9_edges.py +286 -0
  252. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  253. package/md_cg/test_review_conformance.py +367 -0
  254. package/md_cg/test_role_views.py +354 -0
  255. package/md_cg/test_tasks.py +409 -0
  256. package/md_cg/test_tool_face.py +189 -0
  257. package/md_cg/test_trust.py +361 -0
  258. package/md_cg/test_twophase.py +286 -0
  259. package/md_cg/test_units_poll.py +71 -0
  260. package/md_cg/test_v14_fixes.py +397 -0
  261. package/md_cg/test_validity_filter.py +280 -0
  262. package/md_cg/test_wisdom_md_store.py +7 -3
  263. package/md_cg/test_writepipe.py +214 -0
  264. package/md_cg/theory.py +17 -2
  265. package/md_cg/tokens.py +134 -12
  266. package/md_cg/tool_face.py +261 -0
  267. package/md_cg/trust.py +943 -0
  268. package/md_cg/twophase.py +232 -0
  269. package/md_cg/units.py +665 -0
  270. package/md_cg/vision_evidence.py +25 -2
  271. package/md_cg/weights.py +25 -2
  272. package/md_cg/whitebox.py +33 -2
  273. package/md_cg/whitebox_kb/data/verify_cache.json +21233 -360
  274. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5124 -0
  275. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  276. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  277. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  278. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  279. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  280. package/md_cg/writelimit.py +37 -7
  281. package/md_cg/writepipe.py +543 -0
  282. package/package.json +2 -2
  283. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  284. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  285. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  286. package/src/hooks.ts +27 -22
  287. package/src/index.ts +33 -6
  288. package/src/lib/datapath.ts +211 -13
  289. package/src/lib/mdcg_client.ts +12 -8
  290. package/src/lib/mutual.ts +411 -411
  291. package/src/lib/prompt_safety.ts +62 -0
  292. package/src/lib/token_store.ts +4 -5
  293. package/zcode/AGENTS.md +21 -10
  294. package/zcode/README.md +4 -0
  295. package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
  296. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
  297. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
  298. /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
  299. /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
  300. /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
  301. /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
  302. /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
  303. /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
  304. /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
  305. /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
  306. /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
  307. /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
  308. /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
  309. /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
  310. /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
  311. /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
  312. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
  313. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
  314. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
  315. /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
  316. /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
  317. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
  318. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
  319. /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
  320. /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
  321. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
  322. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
  323. /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
  324. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
  325. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
@@ -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()
@@ -9,12 +9,12 @@
9
9
  · `docs/白箱智能系列·第五篇:174-180`
10
10
  「把经历兑换成结构…整理完之后记忆库变小了,但信息量反而更可用——
11
11
  噪音被扔掉了,骨架被留下。」
12
- · AEIS 工具表 `docs/tool_table_v0.3.0.md:15-19`
12
+ · AEIS 工具表 `docs/mdcg/tool_table_v0.3.0.md:15-19`
13
13
  `prefeed`(H1 新奇检测 → 高新奇输入当场强化编码)、
14
14
  `pattern_separation`(H3 扫描相似节点对)、
15
15
  `nightly_cleanup`(知识层夜间整理、无边孤岛降级)。
16
16
  本模块 = 这三件事的**写入侧前置版**:不等夜间整理,写之前就裁决。
17
- · `docs/智能的公理化基石.md:758-763` —— **诚实边界**
17
+ · `docs/theory/智能的公理化基石.md:758-763` —— **诚实边界**
18
18
  「信息差与热力学熵之间只能进行结构类比,不应宣称数学同构」。
19
19
  故本模块一律称「自信息代理 / 惊奇度」,**不称香农熵**,也不做熵的物理断言。
20
20
 
@@ -47,7 +47,7 @@ import math
47
47
  import os
48
48
  import time
49
49
 
50
- from . import nodefile
50
+ from . import lifecycle, nodefile
51
51
  from .fsutil import append_jsonl, atomic_write, read_jsonl
52
52
  from .mdcg import bigrams
53
53
 
@@ -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
 
@@ -256,11 +265,19 @@ def reinforce(cg, node_id, delta=0.05):
256
265
  fm["protected"] = True
257
266
  fm["protection_reason"] = (f"importance={imp:.2f}≥{PROTECT_IMPORTANCE}"
258
267
  f"(重复强化)")
268
+ # ② 显式状态机收口(2026-09-16):MERGE 的语义是「又一次见到」= **重新激活**
269
+ # 信号——已降权(demoted)/已定型(converged)的节点经状态机**逐级回升**到
270
+ # active(archived→active 亦合法,归档节点被再次见到即恢复参与);active 为
271
+ # 幂等 no-op(不写字段、不留痕)。protected 只豁免**降级**,回升不受限。
272
+ lifecycle.stamp(fm, "active", reason="MERGE 重复强化(回升)",
273
+ actor="forgetting:reinforce")
259
274
  cg._write_node(node_id, os.path.join(cg.root, node["path"]),
260
275
  fm, node.get("content") or "")
261
276
  e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
262
277
  if e is not None:
263
278
  e["importance"] = imp
279
+ if fm.get(lifecycle.STATE_FIELD):
280
+ e[lifecycle.STATE_FIELD] = fm[lifecycle.STATE_FIELD]
264
281
  if fm.get("protected"):
265
282
  e["protected"] = True
266
283
  e["protection_reason"] = fm["protection_reason"]
@@ -268,6 +285,7 @@ def reinforce(cg, node_id, delta=0.05):
268
285
  "merge_count": fm["merge_count"], "protected": bool(fm.get("protected"))}
269
286
 
270
287
 
288
+ # 生效条件:cg 必填,limit 默认 100;日志路径不存在时返回 [];否则返回 out[-limit:],limit=0 时 -0 退化为 out[0:] 即全量。
271
289
  def history(cg, limit=100):
272
290
  """读取遗忘留痕(最近 limit 条)。"""
273
291
  p = os.path.join(cg.root, LOG_FILE)
@@ -288,6 +306,7 @@ def history(cg, limit=100):
288
306
  return out[-limit:]
289
307
 
290
308
 
309
+ # 生效条件:cg 必填;日志路径不存在返回 {"total": 0, "by_verdict": {}};否则流式累计行数与 verdict 分布。
291
310
  def summary(cg):
292
311
  """遗忘留痕聚合(流式,不把全量日志读进内存):总数 + 四态分布。"""
293
312
  p = os.path.join(cg.root, LOG_FILE)
@@ -331,6 +350,7 @@ VERIFIED_BASES = ("formal_proof", "compiler", "test", "textbook", "public_kb")
331
350
  TIER_WORKING = 0.40
332
351
 
333
352
 
353
+ # 生效条件:e 必填;protected 为真、importance>=PROTECT_IMPORTANCE、或 evidence_count>=3 且 verification_basis 在 VERIFIED_BASES → "longterm";importance>=TIER_WORKING 或 vb 在 VERIFIED_BASES → "working";否则 "candidate"。
334
354
  def _tier_of(e: dict) -> str:
335
355
  """索引快照 → 分层:longterm(长期)/ working(工作)/ candidate(候选待评估)。"""
336
356
  imp = float(e.get("importance", 0.5) or 0.5)
@@ -343,19 +363,23 @@ def _tier_of(e: dict) -> str:
343
363
  return "candidate"
344
364
 
345
365
 
366
+ # 生效条件:e 必填;e["edges"] 为假值(缺失/空列表)且 e["subgraph"] 为假值时返回 True,否则 False。
346
367
  def _is_island(e: dict) -> bool:
347
368
  """无边孤岛:既无出边也无子图声明(夜间整理的首要候选)。"""
348
369
  return (not (e.get("edges") or [])) and (not e.get("subgraph"))
349
370
 
350
371
 
372
+ # 生效条件:cg 必填且提供 cg.root,恒返回 os.path.join(cg.root, LONGTERM_DIR)。
351
373
  def longterm_dir(cg) -> str:
352
374
  return os.path.join(cg.root, LONGTERM_DIR)
353
375
 
354
376
 
377
+ # 生效条件:cg 必填,恒返回 longterm_dir(cg) 下的 "current.json" 路径。
355
378
  def current_path(cg) -> str:
356
379
  return os.path.join(longterm_dir(cg), "current.json")
357
380
 
358
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 在源码中未被引用)。
359
383
  def longterm_assess(cg, apply=False, out=None, layer=None, keep=LONGTERM_KEEP,
360
384
  max_rows=None, force=False, actor="maintain"):
361
385
  """评估后分层落盘:生成一个可回溯的长期记忆断面。
@@ -432,6 +456,7 @@ def longterm_assess(cg, apply=False, out=None, layer=None, keep=LONGTERM_KEEP,
432
456
  }
433
457
 
434
458
 
459
+ # 生效条件:cg 与 keep 必填;keep<=0 时不删除任何断面返回 [];否则删除除最近 keep 个 .jsonl 外的旧断面。
435
460
  def _prune(cg, keep):
436
461
  """只保留最近 keep 个断面文件(按文件名时间前缀排序)。"""
437
462
  d = longterm_dir(cg)
@@ -449,6 +474,7 @@ def _prune(cg, keep):
449
474
  return removed
450
475
 
451
476
 
477
+ # 生效条件:cg 必填,limit 默认 20;目录不可读返回 [];否则新的在前逐个 append,因先 append 后判 len(out)>=limit,limit=0 时仍返回 1 条快照。
452
478
  def longterm_list(cg, limit=20):
453
479
  """列出历史断面(新的在前):{snapshot_id, path, ts, total, tiers}。"""
454
480
  d = longterm_dir(cg)
@@ -473,6 +499,7 @@ def longterm_list(cg, limit=20):
473
499
  return {"current": cur, "snapshots": out}
474
500
 
475
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。
476
503
  def longterm_show(cg, snapshot_id=None):
477
504
  """读取某个断面的分层统计(不载全量行,只聚合)。"""
478
505
  d = longterm_dir(cg)
@@ -516,6 +543,7 @@ def longterm_show(cg, snapshot_id=None):
516
543
  # 写入**之前**的新奇检测:重复项并入既有(MERGE),而非新增;无关噪音丢弃;
517
544
  # 有歧义的半重复留痕待复核。这是「写入侧前置」的落库动作,比夜间整理更早一步。
518
545
 
546
+ # 生效条件:cg 与 content 必填;恒经 assess 得四态并映射 decision(ACCEPT→write 等),落留痕后返回 ok=True,不写任何节点。
519
547
  def prefeed(cg, content, layer="contextual", role=None, verification_basis=None,
520
548
  importance_hint=None, node_id=None):
521
549
  """前馈裁决(不写盘):返回四态 + 判据,并留痕 `_forgetting.jsonl`。
@@ -540,13 +568,15 @@ def prefeed(cg, content, layer="contextual", role=None, verification_basis=None,
540
568
  return {"ok": True, "action": "prefeed", **rec}
541
569
 
542
570
 
571
+ # 生效条件:content 为 None 或假值时按 "" 计算,恒返回 "pre_"+sha1(content).hexdigest()[:12]。
543
572
  def _prefeed_id(content):
544
573
  return "pre_" + hashlib.sha1((content or "").encode("utf-8")).hexdigest()[:12]
545
574
 
546
575
 
576
+ # 生效条件:cg 必填,limit 默认 100;action 为假值(None/空串)时不过滤,真值只留该 action;返回 recs[-int(limit):],limit=0 时退化为全量。
547
577
  def maintain_history(cg, limit=100, action=None):
548
578
  """维护留痕(`_maintain.jsonl` 最近 limit 条),可按 action 过滤。"""
549
579
  recs = list(read_jsonl(os.path.join(cg.root, MAINTAIN_LOG)))
550
580
  if action:
551
581
  recs = [r for r in recs if r.get("action") == action]
552
- 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
 
@@ -96,15 +100,23 @@ class FileLock:
96
100
 
97
101
  超时后放弃并放行(best-effort):写被拒绝的代价大于一次竞态——这与
98
102
  deja-vu 对 usage 日志锁的取舍一致("a racing write beats a lost injection")。
103
+
104
+ strict=True 反转该取舍:超时抛 TimeoutError 而非放行。适用于**不能丢的写**
105
+ (审核队列 inbox/decisions、裁决记录)——锁竞争失败时显式报错让调用方重试,
106
+ 好过静默放行后退化为无锁并发(丢一条提案/裁决比让写入者等一下代价大)。
99
107
  """
100
108
 
101
- def __init__(self, path: str, timeout: float = 10.0, poll: float = 0.01):
109
+ # 生效条件:path 加 ".lock" 后缀存入 self.path,timeout、poll、strict 原样保存,并置 self._f = None、self.acquired = False。
110
+ def __init__(self, path: str, timeout: float = 10.0, poll: float = 0.01,
111
+ strict: bool = False):
102
112
  self.path = path + ".lock"
103
113
  self.timeout = timeout
104
114
  self.poll = poll
115
+ self.strict = strict
105
116
  self._f = None
106
117
  self.acquired = False
107
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 放行。
108
120
  def __enter__(self):
109
121
  os.makedirs(os.path.dirname(os.path.abspath(self.path)) or ".", exist_ok=True)
110
122
  self._f = open(self.path, "a+b")
@@ -122,9 +134,13 @@ class FileLock:
122
134
  if e.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
123
135
  raise
124
136
  if time.time() > deadline:
137
+ if self.strict:
138
+ raise TimeoutError(
139
+ f"FileLock 超时未获锁(strict):{self.path}")
125
140
  return self # 放行,不阻断写路径
126
141
  time.sleep(self.poll)
127
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。
128
144
  def __exit__(self, *exc):
129
145
  try:
130
146
  if self.acquired:
@@ -142,6 +158,7 @@ class FileLock:
142
158
  self.acquired = False
143
159
 
144
160
 
161
+ # 生效条件:os.path.getsize(path) 为 0 时返回 False;否则二进制打开 path 并从 size-1 处读 1 字节,返回 f.read(1) != b"\n";getsize/open/seek/read 抛 OSError 时返回 False。
145
162
  def ends_mid_line(path: str) -> bool:
146
163
  """行式日志的最后一字节是否不是换行——即上一个写者被杀死留下的半截记录。
147
164
  追加者若不先补一个换行,新记录会粘在这行上,两条都解析不出来。"""
@@ -156,6 +173,7 @@ def ends_mid_line(path: str) -> bool:
156
173
  return False
157
174
 
158
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 字节后关闭。
159
177
  def append_jsonl(path: str, record: dict):
160
178
  """向 append-only 日志追加一条记录(best-effort 语义)。
161
179
 
@@ -178,6 +196,7 @@ def append_jsonl(path: str, record: dict):
178
196
  os.close(fd)
179
197
 
180
198
 
199
+ # 生效条件:os.path.exists(path) 为假时生成器直接结束不产出;否则逐行 strip,空行跳过,json.loads 成功则 yield 该对象,抛 ValueError 的行跳过,其余异常不捕获。
181
200
  def read_jsonl(path: str):
182
201
  """读 append-only 日志,跳过被截断/粘连的坏行(不因自身簿记而失败)。"""
183
202
  if not os.path.exists(path):
@@ -193,6 +212,61 @@ def read_jsonl(path: str):
193
212
  continue
194
213
 
195
214
 
215
+ _COUNT_CACHE = {} # abspath -> (bytes_scanned, mtime_ns, lines)
216
+
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)。
219
+ def count_jsonl(path: str, chunk: int = 1 << 20) -> int:
220
+ """数 append-only 日志的行数——**流式计数、不物化**(内存 O(1))。
221
+
222
+ 存在的唯一理由:`len(list(read_jsonl(p)))` 是**危险的默认写法**。它把整份
223
+ 日志解析成对象列表,日志一长就是灾难——本机实测(2026-09-16):审计日志
224
+ 4.3 GB,一次 health/盘点调用解析速率 ~1 MB/s、RSS 涨到 5 GB+ 仍在涨、
225
+ 数十分钟不返回;调用方超时重试又在别的进程里再排一次队,最终把整条 MCP
226
+ 通道堵死(一个只读的「体检」把服务打死,代价与收益完全不成比例)。
227
+ 这里只数字节里的换行:不解析、不驻留,速度只受磁盘限制;进程内按
228
+ (已扫字节数, mtime) 缓存,文件只增长时只扫新增字节(O(增量))。
229
+
230
+ 语义边界(诚实声明):数的是**换行符**,不是 JSON 记录——
231
+ ① 完整写入的日志(`append_jsonl` 每条尾带 `\\n`)换行数 == 记录数,与
232
+ `len(list(read_jsonl(p)))` 逐位相等(test_health_scale ⑦ 守卫);
233
+ ② 末尾**未终止的半截行**不计入(下界,最多差 1 行;见 append_jsonl 的
234
+ ends_mid_line 修补分支——并发交错被杀的进程会留下这种尾巴);
235
+ ③ 非法 JSON 行计入行数而 `read_jsonl` 会跳过——健康度是量级指标,不做
236
+ 逐行校验,逐行解析正是上面那场事故的根因。
237
+ """
238
+ key = os.path.abspath(path or "")
239
+ try:
240
+ st = os.stat(key)
241
+ except OSError:
242
+ return 0
243
+ start, total = 0, 0
244
+ prev = _COUNT_CACHE.get(key)
245
+ if prev:
246
+ p_scan, p_mtime, p_count = prev
247
+ if p_scan == st.st_size and p_mtime == st.st_mtime_ns:
248
+ return p_count # 完全未变:零 IO
249
+ if p_scan < st.st_size and p_mtime != st.st_mtime_ns:
250
+ start, total = p_scan, p_count # 只增长:扫增量
251
+ scanned, lines = start, 0
252
+ try:
253
+ with open(key, "rb") as f:
254
+ if start:
255
+ f.seek(start)
256
+ while True:
257
+ buf = f.read(chunk)
258
+ if not buf:
259
+ break
260
+ scanned += len(buf)
261
+ lines += buf.count(b"\n")
262
+ except OSError:
263
+ return total or 0
264
+ total += lines
265
+ _COUNT_CACHE[key] = (scanned, st.st_mtime_ns, total)
266
+ return total
267
+
268
+
269
+ # 生效条件:directory 经 abspath 后作为分片目录并 makedirs(exist_ok=True),实例分片文件名由 os.getpid() 与 uuid.uuid4().hex[:8] 拼成 "{pid}-{hex8}.log",append 时各写者只写自己这一分片,从而不共享写入点。
196
270
  class ShardedLog:
197
271
  """每写者独占一个分片的 append-only 日志——不能丢记录时用它。
198
272
 
@@ -201,6 +275,7 @@ class ShardedLog:
201
275
  代价是读取要合并 N 个分片,靠记录里的单调序号 (t, seq) 恢复全局写入顺序。
202
276
  """
203
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。
204
279
  def __init__(self, directory: str):
205
280
  self.dir = os.path.abspath(directory)
206
281
  os.makedirs(self.dir, exist_ok=True)
@@ -209,6 +284,7 @@ class ShardedLog:
209
284
  self._seq = 0
210
285
  self._fh = None
211
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。
212
288
  def append(self, record: dict):
213
289
  self._seq += 1
214
290
  record = dict(record, _t=time.time(), _s=self._seq)
@@ -218,12 +294,14 @@ class ShardedLog:
218
294
  separators=(",", ":")) + "\n")
219
295
  self._fh.flush()
220
296
 
297
+ # 生效条件:幂等;self._fh 为真值时 flush 并关闭句柄、再把 self._fh 置 None,为 None 时直接返回不报错;
221
298
  def close(self):
222
299
  if self._fh:
223
300
  self._fh.close()
224
301
  self._fh = None
225
302
 
226
303
  @staticmethod
304
+ # 生效条件:directory 是目录时,按 sorted(os.listdir(directory)) 顺序对每个以 ".log" 结尾的文件调用 read_jsonl 汇总记录,再按每条记录 r.get("_t", 0)、r.get("_s", 0)(缺键取 0)排序后返回全部记录;directory 不是目录时直接返回 []。
227
305
  def read_all(directory: str):
228
306
  """按全局写入顺序回放所有分片。"""
229
307
  if not os.path.isdir(directory):
@@ -237,6 +315,7 @@ class ShardedLog:
237
315
  return recs
238
316
 
239
317
  @staticmethod
318
+ # 生效条件:directory 是目录时,遍历 os.listdir(directory),对以 ".log" 结尾且不满足「keep 为真值且 os.path.abspath(p) == keep」的条目调用 os.remove(keep 为 None/空串等假值时该排除条件恒不成立,所有 ".log" 条目都会被删),删除时的 OSError 被忽略;directory 不是目录时直接返回。
240
319
  def clear(directory: str, keep: str = None):
241
320
  """合并进快照后清理分片。keep 用于保留当前进程正在写的那个。"""
242
321
  if not os.path.isdir(directory):
@@ -248,4 +327,4 @@ class ShardedLog:
248
327
  try:
249
328
  os.remove(p)
250
329
  except OSError:
251
- pass
330
+ pass