@furongjun1999/dsh-memory 0.4.11 → 0.5.1

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 (580) hide show
  1. package/README.md +552 -465
  2. package/codebuddy/CODEBUDDY.md +11 -3
  3. package/codebuddy/README.md +92 -90
  4. package/codebuddy/mcp.json +27 -27
  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 -169
  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 -144
  7. package/docs/README.md +143 -111
  8. package/docs/discipline/harnesses.yaml +244 -226
  9. package/docs/discipline/templates/full.md.tmpl +61 -61
  10. package/docs/discipline/templates/rules.mdc.tmpl +68 -0
  11. package/docs/discipline/templates/skill.md.tmpl +23 -23
  12. package/docs/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 +299 -299
  13. package/docs/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 +239 -239
  14. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  15. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  16. package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
  17. package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
  18. 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 +174 -174
  19. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
  20. 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 +223 -223
  21. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
  22. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
  23. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v1.md +156 -0
  24. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v10.md +210 -0
  25. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v11.md +227 -0
  26. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v12.md +203 -0
  27. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v13.md +233 -0
  28. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v14.md +191 -0
  29. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v15.md +213 -0
  30. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v16.md +214 -0
  31. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v17.md +199 -0
  32. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v2.md +156 -0
  33. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v3.md +152 -0
  34. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v4.md +128 -0
  35. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v5.md +114 -0
  36. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v6.md +192 -0
  37. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v7.md +187 -0
  38. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v8.md +207 -0
  39. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v9.md +203 -0
  40. package/docs/{mdcg → hive}//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 +165 -160
  41. package/docs/hive//344/273/244/347/211/214/350/257/255/344/271/211/344/277/256/346/255/243_/344/273/262/350/243/201/344/275/215_v0.1.md +76 -0
  42. package/docs/{mdcg → hive}//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 -236
  43. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
  44. 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 -43
  45. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +85 -0
  46. 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 -102
  47. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +47 -0
  48. 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 +519 -503
  49. 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 +85 -85
  50. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +191 -0
  51. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +90 -0
  52. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  53. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
  54. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -216
  55. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
  56. package/docs/mdcg/lingshu_tutorial.html +14449 -14449
  57. package/docs/mdcg/release_v0.4.11.md +49 -0
  58. package/docs/mdcg/release_v0.4.5.md +55 -55
  59. package/docs/mdcg/tool_table_v0.3.0.md +117 -117
  60. 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 -600
  61. 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 +40 -40
  62. 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 +275 -275
  63. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
  64. 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 +130 -130
  65. 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 +278 -278
  66. 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 -172
  67. package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
  68. 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 +491 -491
  69. 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 +340 -340
  70. 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 +588 -588
  71. 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 -176
  72. 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 +81 -81
  73. package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
  74. 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 -242
  75. package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
  76. 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 -129
  77. 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 -134
  78. package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  79. 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 -145
  80. 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 +144 -144
  81. 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 +198 -198
  82. 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 -221
  83. 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 -237
  84. package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
  85. package/docs//345/217/221/345/270/203/344/273/266/345/233/236/346/272/257/350/257/264/346/230/216_v0.1.md +82 -82
  86. 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 +28 -2
  87. package/dsh/README.md +82 -82
  88. package/dsh/cordis.yml.example +139 -139
  89. package/dsh/update-lingshu.bat +11 -11
  90. package/lib/bridge.d.ts +9 -0
  91. package/lib/bridge.js +35 -0
  92. package/lib/hooks.js +36 -2
  93. package/lib/index.js +7 -1
  94. package/lib/lib/roleplay_web.js +116 -29
  95. package/lib/lib/token_store.d.ts +7 -1
  96. package/lib/lib/token_store.js +12 -3
  97. package/md_cg/__init__.py +7 -7
  98. package/md_cg/audit.py +379 -368
  99. package/md_cg/autonomy.py +287 -287
  100. package/md_cg/backfill.py +1328 -1327
  101. package/md_cg/backfill_bigdomain.py +34 -34
  102. package/md_cg/backfill_bucket_zh.py +35 -0
  103. package/md_cg/bench6_arms.py +410 -410
  104. package/md_cg/bench6_common.py +230 -230
  105. package/md_cg/bench6_competitors.py +212 -212
  106. package/md_cg/bench_axis_domain.py +257 -257
  107. package/md_cg/bench_blind_comp.py +308 -308
  108. package/md_cg/bench_e2e_judge.py +532 -0
  109. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  110. package/md_cg/bench_e2e_qa.py +256 -0
  111. package/md_cg/bench_en_atoms_public.py +230 -230
  112. package/md_cg/bench_governance.py +348 -348
  113. package/md_cg/bench_lme_zh.py +410 -410
  114. package/md_cg/bench_locomo.py +121 -121
  115. package/md_cg/bench_locomo_zh.py +450 -450
  116. package/md_cg/bench_locomo_zh_public.py +147 -147
  117. package/md_cg/bench_longmem.py +112 -112
  118. package/md_cg/bench_membench.py +632 -632
  119. package/md_cg/bench_p0.py +149 -149
  120. package/md_cg/bench_progressive.py +287 -287
  121. package/md_cg/bench_role_views.py +238 -238
  122. package/md_cg/bench_task_ab.py +243 -243
  123. package/md_cg/bench_task_ab_llm.py +408 -408
  124. package/md_cg/bench_unified_en.py +204 -204
  125. package/md_cg/bench_zh_mad.py +601 -601
  126. package/md_cg/blindspot_tickets.py +123 -123
  127. package/md_cg/branches.py +301 -285
  128. package/md_cg/build_postings.py +73 -73
  129. package/md_cg/ccgc.py +1006 -948
  130. package/md_cg/census.py +132 -132
  131. package/md_cg/chain.py +315 -300
  132. package/md_cg/codeindex.py +531 -531
  133. package/md_cg/coldverify.py +292 -292
  134. package/md_cg/comment_gate.py +337 -337
  135. package/md_cg/cond_compose.py +190 -190
  136. package/md_cg/cond_facts.py +154 -154
  137. package/md_cg/cond_template.json +106 -106
  138. package/md_cg/condition_anchor.py +142 -142
  139. package/md_cg/conformance.py +726 -726
  140. package/md_cg/consistency.py +717 -717
  141. package/md_cg/consolidate.py +1537 -1439
  142. package/md_cg/corpus.py +110 -110
  143. package/md_cg/crosscheck.py +1098 -1097
  144. package/md_cg/crypto.py +3 -1
  145. package/md_cg/d_meta.py +310 -310
  146. package/md_cg/datapath.py +78 -18
  147. package/md_cg/docindex.py +473 -473
  148. package/md_cg/eval_common.py +575 -575
  149. package/md_cg/evidence.py +4 -2
  150. package/md_cg/evolution.py +477 -477
  151. package/md_cg/export.py +222 -220
  152. package/md_cg/forgetting.py +581 -581
  153. package/md_cg/fsutil.py +377 -329
  154. package/md_cg/hotcache.py +48 -7
  155. package/md_cg/hyperedge.py +251 -251
  156. package/md_cg/identity.py +390 -390
  157. package/md_cg/insight.py +500 -500
  158. package/md_cg/interop.py +338 -0
  159. package/md_cg/judgment_manifest.py +177 -0
  160. package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
  161. package/md_cg/lexicon/build_standard_en.py +171 -171
  162. package/md_cg/lexicon/expand_en_zh.py +211 -211
  163. package/md_cg/lifecycle.py +272 -272
  164. package/md_cg/linkref.py +280 -280
  165. package/md_cg/links.py +140 -107
  166. package/md_cg/mcp_server.py +403 -55
  167. package/md_cg/md_whitebox.py +345 -345
  168. package/md_cg/mdcg.py +783 -233
  169. package/md_cg/mdcos.py +500 -70
  170. package/md_cg/metacognition.py +591 -591
  171. package/md_cg/migrate.py +119 -119
  172. package/md_cg/migrate_aeis.py +221 -221
  173. package/md_cg/migrate_roleplay.py +293 -293
  174. package/md_cg/migrate_wisdom_graph.py +360 -360
  175. package/md_cg/mreview/__init__.py +25 -25
  176. package/md_cg/mreview/__main__.py +110 -110
  177. package/md_cg/mreview/bundle.py +178 -178
  178. package/md_cg/mreview/candidates.py +262 -262
  179. package/md_cg/mreview/govern.py +694 -693
  180. package/md_cg/mreview/locate.py +939 -939
  181. package/md_cg/mreview/pipeline.py +728 -728
  182. package/md_cg/mreview/rules/duplication.json +21 -21
  183. package/md_cg/mreview/rules/field_coverage.json +54 -54
  184. package/md_cg/mreview/rules/source_license.json +21 -21
  185. package/md_cg/mreview/rules/template_flow.json +21 -21
  186. package/md_cg/mreview/ruleset.py +252 -252
  187. package/md_cg/nodefile.py +575 -575
  188. package/md_cg/pooling.py +484 -472
  189. package/md_cg/postings.py +300 -298
  190. package/md_cg/predict.py +1100 -1100
  191. package/md_cg/progressive.py +123 -123
  192. package/md_cg/protect.py +272 -272
  193. package/md_cg/protocol/md_cg_gate.proto +33 -33
  194. package/md_cg/protocol.py +372 -372
  195. package/md_cg/provenance.py +582 -582
  196. package/md_cg/reach.py +453 -453
  197. package/md_cg/readcache.py +143 -0
  198. package/md_cg/reconcile.py +228 -0
  199. package/md_cg/refindex.py +833 -833
  200. package/md_cg/refine.py +604 -604
  201. package/md_cg/review_cli.py +170 -0
  202. package/md_cg/roleviews.py +89 -89
  203. package/md_cg/routing.py +393 -365
  204. package/md_cg/run_tests.py +211 -0
  205. package/md_cg/scrub.py +13 -3
  206. package/md_cg/security.py +128 -18
  207. package/md_cg/self_state.py +1029 -1029
  208. package/md_cg/selfreport.py +152 -151
  209. package/md_cg/semantic/__init__.py +10 -10
  210. package/md_cg/semantic/canonical.py +122 -122
  211. package/md_cg/semantic/en_normalizer.py +364 -364
  212. package/md_cg/semantic/en_zh_map.json +28694 -0
  213. package/md_cg/semantic/export_en_zh_map.py +64 -0
  214. package/md_cg/semantic/unify.py +45 -0
  215. package/md_cg/semantic/zh_en_atoms.py +139 -139
  216. package/md_cg/signer.py +7 -4
  217. package/md_cg/sources.py +816 -582
  218. package/md_cg/statushdr.py +179 -179
  219. package/md_cg/stg.py +54 -37
  220. package/md_cg/subgraph.py +729 -729
  221. package/md_cg/sustain.py +35 -5
  222. package/md_cg/tasks.py +470 -470
  223. package/md_cg/test_access_hints.py +147 -0
  224. package/md_cg/test_action_derive.py +203 -203
  225. package/md_cg/test_audit_rotate.py +270 -270
  226. package/md_cg/test_autonomy.py +143 -143
  227. package/md_cg/test_bench_governance.py +102 -102
  228. package/md_cg/test_blindspot_tickets.py +166 -166
  229. package/md_cg/test_branch_discard_tombstone.py +136 -0
  230. package/md_cg/test_branches.py +13 -3
  231. package/md_cg/test_ccg_perturb.py +184 -184
  232. package/md_cg/test_ccgc.py +433 -433
  233. package/md_cg/test_census_prune.py +81 -81
  234. package/md_cg/test_chain_read_isolate.py +168 -0
  235. package/md_cg/test_cond_compose_anchors.py +76 -76
  236. package/md_cg/test_cond_match.py +165 -165
  237. package/md_cg/test_condition_anchor.py +81 -81
  238. package/md_cg/test_d_meta.py +412 -412
  239. package/md_cg/test_datapath_device_name.py +203 -0
  240. package/md_cg/test_datapath_root.py +199 -199
  241. package/md_cg/test_emit_negtail_cache.py +156 -0
  242. package/md_cg/test_en_pipeline.py +22 -2
  243. package/md_cg/test_gain_gate.py +212 -212
  244. package/md_cg/test_govern_directread.py +421 -0
  245. package/md_cg/test_health_scale.py +173 -173
  246. package/md_cg/test_hive_ingest.py +285 -0
  247. package/md_cg/test_hot_cold.py +215 -215
  248. package/md_cg/test_hyperedge.py +245 -245
  249. package/md_cg/test_i26_empty_first_write.py +116 -0
  250. package/md_cg/test_i27_e041_identity.py +128 -0
  251. package/md_cg/test_i28_hotcache_prodpath.py +122 -0
  252. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  253. package/md_cg/test_identity_attribution.py +96 -15
  254. package/md_cg/test_index_durability.py +17 -3
  255. package/md_cg/test_interop.py +95 -0
  256. package/md_cg/test_interop_judgment.py +228 -0
  257. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  258. package/md_cg/test_lifecycle.py +309 -309
  259. package/md_cg/test_linkref.py +306 -306
  260. package/md_cg/test_links_concurrent_write.py +188 -0
  261. package/md_cg/test_lock.py +43 -43
  262. package/md_cg/test_md_access_parity.py +255 -255
  263. package/md_cg/test_md_writepath.py +345 -345
  264. package/md_cg/test_mdstore_search_parity.py +160 -0
  265. package/md_cg/test_merge_upsert.py +168 -0
  266. package/md_cg/test_mr_m2.py +587 -587
  267. package/md_cg/test_mr_m3.py +710 -710
  268. package/md_cg/test_mr_m4.py +485 -485
  269. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  270. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  271. package/md_cg/test_n131_merge_gate.py +205 -0
  272. package/md_cg/test_p0.py +250 -250
  273. package/md_cg/test_p1.py +316 -316
  274. package/md_cg/test_p10_identity.py +173 -173
  275. package/md_cg/test_p11_consistency.py +233 -233
  276. package/md_cg/test_p12_metacognition.py +212 -212
  277. package/md_cg/test_p13_encryption.py +241 -241
  278. package/md_cg/test_p14_sustain.py +249 -249
  279. package/md_cg/test_p15_scrub.py +280 -280
  280. package/md_cg/test_p16_self_state.py +301 -301
  281. package/md_cg/test_p17_predict.py +354 -354
  282. package/md_cg/test_p18_whitebox.py +171 -171
  283. package/md_cg/test_p19_migrate_roleplay.py +149 -149
  284. package/md_cg/test_p1x_ref_root.py +160 -0
  285. package/md_cg/test_p20_evolution.py +315 -315
  286. package/md_cg/test_p21_tokens.py +293 -270
  287. package/md_cg/test_p22_theory.py +175 -175
  288. package/md_cg/test_p23_links.py +311 -311
  289. package/md_cg/test_p24_evidence.py +227 -227
  290. package/md_cg/test_p25_weights.py +156 -156
  291. package/md_cg/test_p26_refindex.py +416 -416
  292. package/md_cg/test_p27_docindex.py +16 -7
  293. package/md_cg/test_p28_refcheck.py +305 -305
  294. package/md_cg/test_p29_session_ingest_export.py +354 -333
  295. package/md_cg/test_p2_mcp.py +3 -0
  296. package/md_cg/test_p3.py +11 -2
  297. package/md_cg/test_p30_maintain.py +330 -330
  298. package/md_cg/test_p31_insight.py +534 -534
  299. package/md_cg/test_p32_backfill.py +7 -1
  300. package/md_cg/test_p33_ccg_wiring.py +293 -293
  301. package/md_cg/test_p34_crosscheck.py +331 -331
  302. package/md_cg/test_p35_conditioned_claim.py +252 -252
  303. package/md_cg/test_p36_kp_align.py +230 -230
  304. package/md_cg/test_p37_condition_space.py +248 -248
  305. package/md_cg/test_p38_concurrent_flush.py +102 -0
  306. package/md_cg/test_p38_contextualize.py +273 -273
  307. package/md_cg/test_p39_verify_flow.py +153 -0
  308. package/md_cg/test_p39_vision_evidence.py +369 -369
  309. package/md_cg/test_p40_refine_worklist.py +241 -241
  310. package/md_cg/test_p41_evolve_patrol.py +224 -224
  311. package/md_cg/test_p42_provenance.py +269 -269
  312. package/md_cg/test_p43_pooling.py +412 -398
  313. package/md_cg/test_p44_md_whitebox.py +231 -231
  314. package/md_cg/test_p45_session_identity.py +219 -219
  315. package/md_cg/test_p46_unit_scope.py +272 -272
  316. package/md_cg/test_p47_session_view.py +316 -0
  317. package/md_cg/test_p4_fuzzy.py +223 -223
  318. package/md_cg/test_p5_semantic.py +226 -226
  319. package/md_cg/test_p6_consolidate.py +440 -387
  320. package/md_cg/test_p7_goals_recent.py +202 -202
  321. package/md_cg/test_p8_subgraph_chain.py +200 -200
  322. package/md_cg/test_p9_forget_protect.py +231 -231
  323. package/md_cg/test_predict_beta.py +135 -135
  324. package/md_cg/test_preflight_failclosed.py +100 -100
  325. package/md_cg/test_progressive.py +146 -146
  326. package/md_cg/test_propose_tail_index.py +157 -0
  327. package/md_cg/test_protocol.py +243 -243
  328. package/md_cg/test_reach.py +378 -378
  329. package/md_cg/test_reach_keys.py +201 -201
  330. package/md_cg/test_read_clip.py +141 -141
  331. package/md_cg/test_read_scope_b27.py +277 -0
  332. package/md_cg/test_readcache_default_on.py +168 -0
  333. package/md_cg/test_readcache_precise_inval.py +270 -0
  334. package/md_cg/test_readcache_prodpath.py +203 -0
  335. package/md_cg/test_reconcile_v0.py +294 -0
  336. package/md_cg/test_retr_gates_prodpath.py +140 -0
  337. package/md_cg/test_retr_s1.py +344 -340
  338. package/md_cg/test_retr_s1b.py +276 -209
  339. package/md_cg/test_retr_s3.py +194 -194
  340. package/md_cg/test_retr_s4.py +163 -163
  341. package/md_cg/test_retr_s5.py +200 -200
  342. package/md_cg/test_retr_s6.py +157 -157
  343. package/md_cg/test_retr_s7.py +392 -384
  344. package/md_cg/test_retr_s8_time.py +369 -316
  345. package/md_cg/test_retr_s9_edges.py +286 -286
  346. package/md_cg/test_retr_s9_entity_ctx.py +9 -3
  347. package/md_cg/test_retr_score_once.py +208 -0
  348. package/md_cg/test_review_conformance.py +367 -367
  349. package/md_cg/test_review_onepass.py +170 -0
  350. package/md_cg/test_role_views.py +354 -354
  351. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  352. package/md_cg/test_security_audit.py +155 -0
  353. package/md_cg/test_security_audit_b26.py +161 -0
  354. package/md_cg/test_security_audit_v21.py +250 -0
  355. package/md_cg/test_sem_noise.py +242 -242
  356. package/md_cg/test_semantic_canonical.py +16 -2
  357. package/md_cg/test_session_isolation.py +168 -0
  358. package/md_cg/test_snapshot_autoclose.py +187 -0
  359. package/md_cg/test_subproc_encoding.py +192 -192
  360. package/md_cg/test_sustain_mutual.py +153 -153
  361. package/md_cg/test_tail_watermark_race.py +208 -0
  362. package/md_cg/test_tasks.py +409 -409
  363. package/md_cg/test_tenant_env_override_warn.py +139 -0
  364. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  365. package/md_cg/test_tool_face.py +189 -189
  366. package/md_cg/test_transfer.py +180 -180
  367. package/md_cg/test_trust.py +361 -361
  368. package/md_cg/test_twophase.py +286 -286
  369. package/md_cg/test_v14_fixes.py +38 -20
  370. package/md_cg/test_validity_filter.py +280 -280
  371. package/md_cg/test_verify_answer.py +138 -138
  372. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  373. package/md_cg/test_wisdom_md_store.py +292 -292
  374. package/md_cg/test_writelimit.py +197 -197
  375. package/md_cg/test_writepipe.py +214 -214
  376. package/md_cg/theory.py +6 -3
  377. package/md_cg/tokens.py +85 -14
  378. package/md_cg/tool_face.py +260 -260
  379. package/md_cg/trust.py +986 -950
  380. package/md_cg/twophase.py +231 -231
  381. package/md_cg/units.py +668 -667
  382. package/md_cg/vision_evidence.py +667 -666
  383. package/md_cg/weights.py +624 -624
  384. package/md_cg/whitebox.py +527 -527
  385. package/md_cg/whitebox_kb/__init__.py +37 -37
  386. package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
  387. package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
  388. package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
  389. package/md_cg/whitebox_kb/engine.py +310 -310
  390. package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  391. package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
  392. package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
  393. package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
  394. package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
  395. package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
  396. package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
  397. package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
  398. package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
  399. package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
  400. package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
  401. package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
  402. package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
  403. package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
  404. package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
  405. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
  406. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
  407. package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
  408. package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
  409. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
  410. package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
  411. package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
  412. package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
  413. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  414. package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
  415. package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
  416. package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
  417. package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
  418. package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
  419. package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
  420. package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
  421. package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
  422. package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
  423. package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
  424. package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
  425. package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
  426. package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
  427. package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
  428. package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
  429. package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
  430. package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
  431. package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
  432. package/md_cg/writelimit.py +356 -356
  433. package/md_cg/writepipe.py +20 -8
  434. package/package.json +101 -96
  435. package/skills/plugin.json +54 -54
  436. package/skills/skills/designer-perspective/SKILL.md +158 -158
  437. package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
  438. package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
  439. package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
  440. package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
  441. package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
  442. package/skills/skills/designer-perspective/scripts/designer.py +545 -545
  443. package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
  444. package/skills/skills/designer-perspective/tests/selftest.py +61 -61
  445. package/skills/skills/lingshu-browser/SKILL.md +60 -60
  446. package/skills/skills/lingshu-compiler/SKILL.md +56 -56
  447. package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
  448. package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
  449. package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
  450. package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
  451. package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
  452. package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
  453. package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
  454. package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
  455. package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
  456. package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
  457. package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
  458. package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
  459. package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
  460. package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
  461. package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
  462. package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
  463. package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
  464. package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
  465. package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
  466. package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
  467. package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
  468. package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
  469. package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
  470. package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
  471. package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
  472. package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
  473. package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
  474. package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
  475. package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
  476. package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
  477. package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
  478. package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
  479. package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
  480. package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
  481. package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
  482. package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
  483. package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
  484. package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
  485. package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
  486. package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
  487. package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
  488. package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
  489. package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
  490. package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
  491. package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
  492. package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
  493. package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
  494. package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
  495. package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
  496. package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
  497. package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
  498. package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
  499. package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
  500. package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
  501. package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
  502. package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
  503. package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
  504. package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
  505. package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
  506. package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
  507. package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
  508. package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
  509. package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
  510. package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
  511. package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
  512. package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
  513. package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
  514. package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
  515. package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
  516. package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
  517. package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
  518. package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
  519. package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
  520. package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
  521. package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
  522. package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
  523. package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
  524. package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
  525. package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
  526. package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
  527. package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
  528. package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
  529. package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
  530. package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
  531. package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
  532. package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
  533. package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
  534. package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
  535. package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
  536. package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
  537. package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
  538. package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
  539. package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
  540. package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
  541. package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
  542. package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
  543. package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
  544. package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
  545. package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
  546. package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
  547. package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
  548. package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
  549. package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
  550. package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
  551. package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
  552. package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
  553. package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
  554. package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
  555. package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
  556. package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
  557. package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
  558. package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
  559. package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
  560. package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
  561. package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
  562. package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
  563. package/skills/skills/lingshu-graph/SKILL.md +63 -63
  564. package/skills/skills/lingshu-net/SKILL.md +48 -48
  565. package/skills/skills/lingshu-os/SKILL.md +64 -64
  566. package/skills/skills/lingshu-pylang/SKILL.md +71 -71
  567. package/src/bridge.ts +33 -0
  568. package/src/hooks.ts +38 -2
  569. package/src/index.ts +526 -518
  570. package/src/lib/datapath.ts +326 -326
  571. package/src/lib/mdcg_client.ts +413 -413
  572. package/src/lib/mutual.ts +428 -428
  573. package/src/lib/prompt_safety.ts +62 -62
  574. package/src/lib/python_path.ts +71 -71
  575. package/src/lib/roleplay_web.ts +116 -29
  576. package/src/lib/token_store.ts +13 -3
  577. package/src/tools.ts +212 -212
  578. package/zcode/AGENTS.md +11 -3
  579. package/zcode/README.md +41 -41
  580. /package/docs/{mdcg → hive}//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" +0 -0
package/md_cg/subgraph.py CHANGED
@@ -1,729 +1,729 @@
1
- # -*- coding: utf-8 -*-
2
- """嵌套子图(nested subgraph):结构要素的可递归表示 + flatten。
3
-
4
- 对齐 AEIS《认知图写入纪律 v1.0》四要素中的第三项:
5
- subgraph = 内部子内容(可嵌套:子节点 + 边),角色=结构(可检索/可递归)。
6
-
7
- 与 AEIS 的两处**有意差异**(都有工程理由,不是简化):
8
-
9
- 1) 存储形态:AEIS 把子节点**内联**在父节点 `subgraph.nodes` 里
10
- (`aeis/image_semantics_cg.py:37-57`)。md_cg 的检索粒度是「节点 = 文件」
11
- (RRF 按节点召回),内联子节点无法被任何检索路单独命中,会违背「子图可检索」
12
- 这一规范本身。故 md_cg 采用**引用式**:`subgraph.nodes` 存子节点 id,
13
- 子节点各自是独立 `.md`,可被 lexical/entity/chain 等路独立召回。
14
- 同时兼容内联写法(`{"id": ...}` 对象)以承接 AEIS 语料。
15
-
16
- 2) 递归深度:AEIS 明确「深度上限不是协议常数,数据驱动,= 可分性条件的自然耗尽」
17
- (`子部件提取_理论稿_v0.4.md:146-150`)。故 `max_depth=None` 表示一直展开到
18
- 自然耗尽(无子节点);`max_depth=k` 只是工程硬截断,会置 `truncated=True`。
19
-
20
- 父边唯一(每个节点至多一个 `part_of` 父)——`子部件提取_理论稿_v0.4.md:105`;
21
- 违反者由 `validate()` 报 `multi_parent`,对齐「树形不一致 → 退回 DEFER」。
22
- """
23
- from __future__ import annotations
24
-
25
- import hashlib
26
- import os
27
- import re
28
- import time
29
-
30
- from .fsutil import append_jsonl
31
-
32
- MAX_DEPTH_HARD = 64 # 工程硬截断上限(防止畸形数据把遍历拖爆)
33
- MAX_NODES_DEFAULT = 2000 # 单次展开的节点上限
34
-
35
- # ---- 模式分离(maintain.separate)参数 -----------------------------------
36
- MAINTAIN_LOG = "_maintain.jsonl"
37
- SEP_MIN_JACCARD = 0.55 # 内容相似度下限(越高=越像,才值得谈分离)
38
- SEP_MAX_COND_OVERLAP = 0.35 # 条件重合上限(越高=条件越同,就该合并而非分离)
39
- SEP_MAX_NODES = 400 # 单次扫描节点上限(防 O(N²) 读盘爆炸)
40
- SEP_MAX_PAIRS = 50 # 单次返回/落库的候选对上限
41
-
42
-
43
- # 生效条件:fm 为假值或 fm.get("subgraph") 取值为假(None/[]/{}/空串)时直接返回 {'nodes': [], 'edges': []};subgraph 为 list/tuple 时其元素作节点、边记空列表,为 dict 时节点取 sg.get("nodes")(缺键或假值回落 [])、边取 sg.get("edges")(同样回落),为其他类型仍返回空;节点项为 dict 时取 n.get("id")、否则取元素本身,nid 为 None 跳过,仅当 str(nid).strip() 非空且未出现过才追加;边只保留 isinstance(e, dict) 的项;
44
- def declared(fm):
45
- """规范化 `frontmatter.subgraph` → `{"nodes": [id...], "edges": [edge...]}`。
46
-
47
- 兼容三种写法:
48
- {"nodes": ["a", "b"], "edges": [...]} 引用式(md_cg 主用)
49
- ["a", "b"] 纯 id 列表
50
- {"nodes": [{"id": "a"}, ...]} 内联式(AEIS 原样,只取 id)
51
- """
52
- sg = (fm or {}).get("subgraph")
53
- if not sg:
54
- return {"nodes": [], "edges": []}
55
- if isinstance(sg, (list, tuple)):
56
- raw_nodes, raw_edges = list(sg), []
57
- elif isinstance(sg, dict):
58
- raw_nodes, raw_edges = list(sg.get("nodes") or []), list(sg.get("edges") or [])
59
- else:
60
- return {"nodes": [], "edges": []}
61
- nodes = []
62
- for n in raw_nodes:
63
- nid = n.get("id") if isinstance(n, dict) else n
64
- if nid is None:
65
- continue
66
- nid = str(nid).strip()
67
- if nid and nid not in nodes:
68
- nodes.append(nid)
69
- return {"nodes": nodes, "edges": [e for e in raw_edges if isinstance(e, dict)]}
70
-
71
-
72
- # 生效条件:当 getattr(cg,"index",None) 的 nodes 字典中存在 nid(entry 非 None)且 "subgraph" in entry 时,返回 {'id': nid, 'subgraph': entry.get("subgraph"), 'edges': entry.get("edges") or []}(subgraph 值为 None 也照样返回);否则改走 cg.get(nid),返回 (node or {}).get("frontmatter") or {},节点不存在或 frontmatter 为假值时得空 dict;
73
- def _fm(cg, nid):
74
- """取节点 frontmatter:优先索引快照(免 IO),快照缺字段时回退读文件。"""
75
- entry = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid)
76
- if entry is not None and "subgraph" in entry:
77
- return {"id": nid, "subgraph": entry.get("subgraph"),
78
- "edges": entry.get("edges") or []}
79
- node = cg.get(nid)
80
- return (node or {}).get("frontmatter") or {}
81
-
82
-
83
- # --------------------------------------------------------------------------
84
- # 迁移边适配:源 sqlite 认知图 → md_cg 本地边
85
- # --------------------------------------------------------------------------
86
- # 源库的 `hierarchical` 方向是 **source=父、target=子**(白箱
87
- # `wisdom-book-cloud.db` 全量实测:`source_is_parent=2832`、`target_is_parent=0`)。
88
- # md_cg 的父边约定与其**相反**:`part_of` 表示「本节点是子、target 是父」
89
- # (见下方 children_index)。若把源库 hierarchical 原样搬进来,树会整体倒置;
90
- # 故迁移时翻译成语义等价的 `contains`(本节点是父、target 是子)。
91
- SRC_REL_MAP = {"hierarchical": "contains"}
92
-
93
-
94
- # 生效条件:rel 先经 str(rel or "").strip().lower() 归一(None/空串得 ""),返回 {'target': str(tgt), 'relation_type': SRC_REL_MAP.get(rel, rel), 'confidence': confidence, 'verified': verified}——归一后的 rel 命中模块常量 SRC_REL_MAP 时用映射值、未命中(含 rel 为空串)时原样保留,confidence/verified 直接透传默认 1.0/0 的实参不做校验;
95
- def normalize_edge(tgt, rel, confidence=1.0, verified=0):
96
- """源库边 → md_cg 本地边(迁移器专用)。做两件事:
97
-
98
- 1) 键名统一为 `relation_type`。`children_index` / `parents_index` 只认
99
- `relation_type` / `relation`;早期迁移器写的 `"type"` 会被静默忽略
100
- (`chain.edge_rel` 兼容 `type`,但 `subgraph` 不兼容),使迁入的图不可遍历。
101
- 2) 方向按 `SRC_REL_MAP` 翻译(源库 hierarchical = source 是父)。
102
- """
103
- rel = str(rel or "").strip().lower()
104
- return {"target": str(tgt), "relation_type": SRC_REL_MAP.get(rel, rel),
105
- "confidence": confidence, "verified": verified}
106
-
107
-
108
- # 生效条件:cg 的 `_subgraph_children` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 收子节点、并按边 rel 为 part_of/hierarchical(target 记为父、当前 pid 记为子)或 parent_of/contains(当前 pid 记为父、target 记为子,target 为 None 则该边跳过)补全 parent_id→[child_id] 索引,回写 cg._subgraph_children 后返回 idx;
109
- def children_index(cg):
110
- """全局正查:parent_id → [child_id...](声明式 ∪ 边式,一次 O(N) 后缓存)。"""
111
- idx = getattr(cg, "_subgraph_children", None)
112
- if idx is not None:
113
- return idx
114
- idx = {}
115
- for pid in list(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys()):
116
- fm = _fm(cg, pid)
117
- for ch in declared(fm)["nodes"]:
118
- idx.setdefault(pid, [])
119
- if ch not in idx[pid]:
120
- idx[pid].append(ch)
121
- for e in (fm.get("edges") or []):
122
- if not isinstance(e, dict):
123
- continue
124
- # 兼容历史语料:早期迁移器把关系类型写在 `type` 键(`migrate.py:43`、
125
- # `migrate_aeis.py:99`),写侧已按 `normalize_edge` 修正,读侧一并容错。
126
- rel = str(e.get("relation_type") or e.get("relation") or e.get("type")
127
- or "").strip().lower()
128
- tgt = e.get("target") or e.get("target_id")
129
- if tgt is None:
130
- continue
131
- tgt = str(tgt).strip()
132
- if rel in ("part_of", "hierarchical"): # 本节点是子,target 是父
133
- idx.setdefault(tgt, [])
134
- if pid not in idx[tgt]:
135
- idx[tgt].append(pid)
136
- elif rel in ("parent_of", "contains"): # 本节点是父,target 是子
137
- idx.setdefault(pid, [])
138
- if tgt not in idx[pid]:
139
- idx[pid].append(tgt)
140
- try:
141
- cg._subgraph_children = idx
142
- except Exception:
143
- pass
144
- return idx
145
-
146
-
147
- # 生效条件:返回 list(children_index(cg).get(nid) or []),即 nid 在索引中无键或对应值为空(None/空列表)时得空列表,否则返回其子 id 的浅拷贝列表;
148
- def children(cg, nid):
149
- """直接子节点 id 列表:`subgraph.nodes` 声明 ∪ 边式父子关系。"""
150
- return list(children_index(cg).get(nid) or [])
151
-
152
-
153
- # 生效条件:cg 的 `_subgraph_parents` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 把 pid 记为每个子节点的父、并按边 rel 为 part_of/hierarchical(target 记为 pid 的父)或 parent_of/contains(pid 记为 target 的父,target 为 None 则该边跳过)补全 child_id→[parent_id] 索引,回写 cg._subgraph_parents 后返回 idx;
154
- def parents_index(cg):
155
- """全局反查:child_id → [parent_id...](供 children/validate 复用,一次 O(N))。"""
156
- idx = getattr(cg, "_subgraph_parents", None)
157
- if idx is not None:
158
- return idx
159
- idx = {}
160
- for pid in list(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys()):
161
- for ch in declared(_fm(cg, pid))["nodes"]:
162
- idx.setdefault(ch, [])
163
- if pid not in idx[ch]:
164
- idx[ch].append(pid)
165
- for e in (_fm(cg, pid).get("edges") or []):
166
- if not isinstance(e, dict):
167
- continue
168
- rel = str(e.get("relation_type") or e.get("relation") or e.get("type")
169
- or "").strip().lower()
170
- tgt = e.get("target") or e.get("target_id")
171
- if tgt is None:
172
- continue
173
- tgt = str(tgt).strip()
174
- # 与 children_index 逐字对称,只是方向取反:
175
- # part_of / hierarchical → 本节点是子,故 target 是本节点的父
176
- # parent_of / contains → 本节点是父,故本节点是 target 的父
177
- if rel in ("part_of", "hierarchical"):
178
- idx.setdefault(pid, [])
179
- if tgt not in idx[pid]:
180
- idx[pid].append(tgt)
181
- elif rel in ("parent_of", "contains"):
182
- idx.setdefault(tgt, [])
183
- if pid not in idx[tgt]:
184
- idx[tgt].append(pid)
185
- try:
186
- cg._subgraph_parents = idx
187
- except Exception:
188
- pass
189
- return idx
190
-
191
-
192
- # 生效条件:无条件执行——把 cg 的 _subgraph_parents 与 _subgraph_children 依次 setattr 为 None(setattr 抛异常被 except 吞掉后继续下一个),无返回值,与 cg 是否已建有缓存无关;
193
- def invalidate_cache(cg):
194
- """写入/删除节点后调用,丢弃父子正查/反查缓存。"""
195
- for attr in ("_subgraph_parents", "_subgraph_children"):
196
- try:
197
- setattr(cg, attr, None)
198
- except Exception:
199
- pass
200
-
201
-
202
- # 生效条件:取 parents_index(cg).get(nid) or [],列表非空时返回其首元素,键缺失或值为空/假(None、[])时返回 None;多父时只返回第一个,不在此处报歧义;
203
- def parent_of(cg, nid):
204
- """唯一父(多父时返回第一个并置 `ambiguous` 标记由 validate 报出)。"""
205
- ps = parents_index(cg).get(nid) or []
206
- return ps[0] if ps else None
207
-
208
-
209
- # 生效条件:max_depth 非 None 时先 max(0, min(int(max_depth), MAX_DEPTH_HARD)) 收敛;以 nid 为根迭代 DFS,已入 seen 的节点跳过,len(seen) >= max_nodes(max_nodes 传 0 时首次循环即成立)置 truncated=True 并 break,depth >= max_depth 且 children 非空时置 truncated=True 并 continue,否则对 children(cg,cur) 逆序、对未出现过的 (ch,cur) 追加 part_of 边并压栈;返回 {'root','nodes','paths','edges','n_nodes','n_edges','truncated'};
210
- def expand(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
211
- """递归展开子树(迭代 DFS,防递归深度爆栈)。
212
-
213
- 返回 `{"root","nodes","paths","edges","n_nodes","n_edges","truncated"}`;
214
- `paths` 为 `节点 id → "根/子/孙"` 层级路径(flatten 后仍可定位来源)。
215
- 边为 `part_of(child→parent)`,与 AEIS flatten 的方向一致。
216
- """
217
- if max_depth is not None:
218
- max_depth = max(0, min(int(max_depth), MAX_DEPTH_HARD))
219
- seen, edges, truncated = {}, [], False
220
- seen_edges = set()
221
- stack = [(nid, 0, nid)]
222
- while stack:
223
- cur, depth, path = stack.pop()
224
- if cur in seen:
225
- continue
226
- if len(seen) >= max_nodes:
227
- truncated = True
228
- break
229
- seen[cur] = path
230
- if max_depth is not None and depth >= max_depth:
231
- if children(cg, cur):
232
- truncated = True
233
- continue
234
- kids = children(cg, cur)
235
- for ch in reversed(kids):
236
- key = (ch, cur)
237
- if key not in seen_edges:
238
- seen_edges.add(key)
239
- edges.append({"source": ch, "target": cur,
240
- "relation_type": "part_of",
241
- "confidence": 1.0, "verified": 0})
242
- if ch not in seen:
243
- stack.append((ch, depth + 1, f"{path}/{ch}"))
244
- return {"root": nid, "nodes": list(seen.keys()), "paths": seen,
245
- "edges": edges, "n_nodes": len(seen), "n_edges": len(edges),
246
- "truncated": truncated}
247
-
248
-
249
- # 生效条件:先调用 expand(cg, nid, max_depth=max_depth, max_nodes=max_nodes),对其中每条边复制一份并追加 source/target 互换、relation_type="parent_of"、confidence/verified 沿用原边 .get(…,1.0/0) 的反向边;返回 root=nid、nodes=ex["nodes"]、paths=ex["paths"]、n_nodes=ex["n_nodes"]、n_edges=双边后条数、truncated=ex["truncated"];
250
- def flatten(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
251
- """把嵌套子图摊平成「节点 + 边」,父子生成**对称双边**。
252
-
253
- 对齐 AEIS `flatten_image_semantics_graph`(`image_semantics_cg.py:151-173`):
254
- 每个有父的节点同时产出 `part_of(child→parent)` 与 `parent_of(parent→child)`。
255
- 检索侧因此既能「从父找子」(下钻),也能「从子找父」(溯源)。
256
- """
257
- ex = expand(cg, nid, max_depth=max_depth, max_nodes=max_nodes)
258
- edges = []
259
- for e in ex["edges"]:
260
- edges.append(dict(e))
261
- edges.append({"source": e["target"], "target": e["source"],
262
- "relation_type": "parent_of",
263
- "confidence": e.get("confidence", 1.0),
264
- "verified": e.get("verified", 0)})
265
- return {"root": nid, "nodes": ex["nodes"], "edges": edges, "paths": ex["paths"],
266
- "n_nodes": ex["n_nodes"], "n_edges": len(edges),
267
- "truncated": ex["truncated"]}
268
-
269
-
270
- # 生效条件:known 取 cg.index 的 nodes 键集合,仅当 parents_index(cg).get(nid) or [] 为空(键缺失或空列表)的 nid 才视为树根,返回其排序后的 id 列表;
271
- def roots(cg):
272
- """无父节点(树根)的 id 列表。"""
273
- known = set(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys())
274
- pmap = parents_index(cg)
275
- return sorted(nid for nid in known if not (pmap.get(nid) or []))
276
-
277
-
278
- # 生效条件:known 取 cg.index 的 nodes 键列表,max_scan 为真值时截断为 known[:int(max_scan)];逐 pid 遍历 declared(_fm(cg,pid))["nodes"]:ch==pid 记 self_loop 并 continue(不再进入悬空/多父判断),ch 不在 known_set 记 dangling_child,随后 parent 中已有 ch 且 prev != pid 记 multi_parent、否则写 parent[ch]=pid;再沿 parent 指针上溯,遇本路径已访问节点记 cycle;_issue 仅在 len(issues) < limit*4 时追加(limit 为 0 时 0<0 不成立,不追加任何 issue);返回 scanned、issues=len(issues)、items=issues[:limit]、truncated=len(issues)>limit;
279
- def validate(cg, limit=50, max_scan=None):
280
- """树一致性校验:自环 / 悬空子节点 / 多父 / 环。
281
-
282
- 对齐 AEIS:父边唯一、拓扑无矛盾、树形不一致 → 该层判定退回 DEFER
283
- (`子部件提取_理论稿_v0.4.md:105-107`)。
284
- """
285
- known = list(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys())
286
- if max_scan:
287
- known = known[:int(max_scan)]
288
- known_set = set(known)
289
- parent, issues = {}, []
290
-
291
- # 生效条件:仅当 len(issues) < limit*4 时把 kw 追加进 issues(限流防报告膨胀);无返回值,只改外层 issues;
292
- def _issue(**kw):
293
- if len(issues) < limit * 4:
294
- issues.append(kw)
295
-
296
- for pid in known:
297
- for ch in declared(_fm(cg, pid))["nodes"]:
298
- if ch == pid:
299
- _issue(id=pid, issue="self_loop", child=ch)
300
- continue
301
- if ch not in known_set:
302
- _issue(id=pid, issue="dangling_child", child=ch)
303
- prev = parent.get(ch)
304
- if prev is not None and prev != pid:
305
- _issue(id=ch, issue="multi_parent", parents=sorted({prev, pid}))
306
- else:
307
- parent[ch] = pid
308
-
309
- # 环检测:沿 parent 指针上溯,遇本路径已访问节点即成环
310
- for nid in known:
311
- path, cur = [], nid
312
- while cur is not None:
313
- if cur in path:
314
- _issue(id=cur, issue="cycle", path=path[path.index(cur):] + [cur])
315
- break
316
- path.append(cur)
317
- cur = parent.get(cur)
318
-
319
- return {"scanned": len(known), "issues": len(issues),
320
- "items": issues[:limit], "truncated": len(issues) > limit}
321
-
322
-
323
- # ==========================================================================
324
- # 模式分离(maintain.separate)
325
- # ==========================================================================
326
- #
327
- # 问题:两条记忆**内容高度相似但生效条件不同**。若放任不管,检索会把它们混为
328
- # 一体——按 A 的条件召回、却拿到 B 的结论。海马体的做法是 pattern separation:
329
- # 把相似但情境不同的表征拆开,各挂各的条件线索。
330
- #
331
- # 本层的工程兑现(保守、可逆、幂等):
332
- # · 不动正文(不制造第二份真相),只加**对称分离边** `distinct_from`;
333
- # · 边携带 reason + 判定分量,供后续重构/审计追溯;
334
- # · 条件**相同**的相似对**不是**分离候选(那是重复,该走 MERGE)。
335
-
336
- # 生效条件:cg.get(nid) 为假值(None/{})时返回 None;否则取 fm=node.get("frontmatter") or {}、content=node.get("content") or "",返回 {'grams': bigrams(forgetting.payload(content)), 'pos','neg': consistency.condition_terms(fm, content) 的结果, 'layer': fm.get("layer")}——grams 为空也照常返回该字典,不返回 None;
337
- def _node_terms_and_grams(cg, nid):
338
- """读取节点并返回 (payload 二元组, 正条件词面, 负条件词面);不可读返回 None。"""
339
- from . import consistency, forgetting
340
- from .mdcg import bigrams # 惰性导入:避开 mdcg↔subgraph 循环
341
- node = cg.get(nid)
342
- if not node:
343
- return None
344
- fm = node.get("frontmatter") or {}
345
- content = node.get("content") or ""
346
- grams = bigrams(forgetting.payload(content))
347
- pos, neg = consistency.condition_terms(fm, content)
348
- return {"grams": grams, "pos": pos, "neg": neg, "layer": fm.get("layer")}
349
-
350
-
351
- # 生效条件:pool 初取 cg.index 的 nodes 键,layer 为假值(None/"")时不按层过滤、否则只留 e.get("layer")==layer 的 nid,ids 为真值时只留 str(x) 属于该集合的 nid;pool 排序后 truncated=len(pool)>int(max_nodes)、并截为前 int(max_nodes) 个;只有 _node_terms_and_grams 返回非 None 且 grams 非空的节点进 cache;对 cache 键两两比较——无公共 bigram 跳过、jaccard 小于 float(min_jaccard) 跳过、condition_distinct 的 distinct 为假或 overlap > float(max_cond_overlap) 跳过,其余成候选并按 (-jaccard,a,b) 排序;返回 scanned、compared、candidates=out[:int(limit)]、total_candidates、truncated、note;
352
- def separation_pairs(cg, layer=None, ids=None, max_nodes=SEP_MAX_NODES,
353
- min_jaccard=SEP_MIN_JACCARD,
354
- max_cond_overlap=SEP_MAX_COND_OVERLAP,
355
- limit=SEP_MAX_PAIRS):
356
- """找出「内容高度相似、条件却不同」的节点对(模式分离候选)。
357
-
358
- 返回 {scanned, compared, candidates:[{a,b,jaccard,cond_overlap,distinct,reason}],
359
- truncated, note}。只读,不写盘。
360
- """
361
- from . import consistency
362
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
363
- pool = [nid for nid, e in nodes.items()
364
- if (not layer or e.get("layer") == layer)]
365
- if ids:
366
- want = {str(x) for x in ids}
367
- pool = [nid for nid in pool if nid in want]
368
- pool.sort()
369
- truncated = len(pool) > int(max_nodes)
370
- pool = pool[:int(max_nodes)]
371
- cache = {}
372
- for nid in pool:
373
- got = _node_terms_and_grams(cg, nid)
374
- if got and got["grams"]:
375
- cache[nid] = got
376
- keys = sorted(cache.keys())
377
- out, compared = [], 0
378
- for i in range(len(keys)):
379
- gi = cache[keys[i]]["grams"]
380
- for j in range(i + 1, len(keys)):
381
- gj = cache[keys[j]]["grams"]
382
- inter = len(gi & gj)
383
- if not inter:
384
- continue
385
- compared += 1
386
- jac = inter / float(len(gi | gj) or 1)
387
- if jac < float(min_jaccard):
388
- continue
389
- a, b = keys[i], keys[j]
390
- cd = consistency.condition_distinct(
391
- cache[a]["pos"], cache[a]["neg"],
392
- cache[b]["pos"], cache[b]["neg"])
393
- if not cd["distinct"] or cd["overlap"] > float(max_cond_overlap):
394
- continue # 条件相同 → 是重复,不是分离
395
- out.append({
396
- "a": a, "b": b,
397
- "a_layer": cache[a]["layer"], "b_layer": cache[b]["layer"],
398
- "jaccard": round(jac, 4), "cond_overlap": cd["overlap"],
399
- "reason": (f"内容相似 {jac:.2f} 但条件重合仅 {cd['overlap']:.2f}"
400
- f"(相似而不同情境,需分离)"),
401
- "a_conditions": sorted(cache[a]["pos"] | cache[a]["neg"])[:6],
402
- "b_conditions": sorted(cache[b]["pos"] | cache[b]["neg"])[:6],
403
- })
404
- out.sort(key=lambda x: (-x["jaccard"], x["a"], x["b"]))
405
- return {"scanned": len(keys), "compared": compared,
406
- "candidates": out[:int(limit)],
407
- "total_candidates": len(out), "truncated": truncated,
408
- "note": "只读候选;apply=True 才写分离边"}
409
-
410
-
411
- # 生效条件:batch 为假值(None 或空串)时回落到 time.strftime("%Y%m%d-%H%M%S");按 (a,b)、(b,a) 两向处理:cg.get(src) 为假值跳过,dst 已出现在 consistency.separation_targets(fm) 中跳过,否则往 fm["edges"] 追加 distinct_from 边、向 fm["pattern_separated_from"] 追加 dst,按 os.path.join(cg.root, e.get("path") or f"{src}.md") 调 cg._write_node 写盘并追加 MAINTAIN_LOG 行,src 记入 written;返回 {'a','b','written','batch'};
412
- def mark_separated(cg, a, b, reason="", actor="maintain", batch=None):
413
- """写入 (a↔b) 对称 `distinct_from` 边(幂等:已存在则不重复写)。"""
414
- from . import consistency
415
- batch = batch or time.strftime("%Y%m%d-%H%M%S")
416
- written = []
417
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
418
- for src, dst in ((a, b), (b, a)):
419
- node = cg.get(src)
420
- if not node:
421
- continue
422
- fm = node.get("frontmatter") or {}
423
- edges = list(fm.get("edges") or [])
424
- if dst in consistency.separation_targets(fm):
425
- continue
426
- edges.append({"target": dst, "relation_type": consistency.SEPARATION_REL,
427
- "reason": reason, "created_at": time.time(),
428
- "confidence": 1.0, "verified": 0})
429
- fm["edges"] = edges
430
- sep = list(fm.get("pattern_separated_from") or [])
431
- if dst not in sep:
432
- sep.append(dst)
433
- fm["pattern_separated_from"] = sep
434
- e = nodes.get(src) or {}
435
- path = os.path.join(cg.root, e.get("path") or f"{src}.md")
436
- cg._write_node(src, path, fm, node.get("content") or "")
437
- if e:
438
- e["edges"] = edges
439
- written.append(src)
440
- append_jsonl(os.path.join(cg.root, MAINTAIN_LOG), {
441
- "t": time.time(), "action": "separate", "batch": batch,
442
- "from": src, "to": dst, "reason": reason, "actor": actor})
443
- return {"a": a, "b": b, "written": written, "batch": batch}
444
-
445
-
446
- # 生效条件:pairs 为 None 时调 separation_pairs 取 candidates 与 meta(scanned/compared/truncated/total_candidates),否则 cands=list(pairs) 且 meta 中 scanned/compared 记 0、truncated 记 False、total_candidates=len(cands);apply 为真值时先生成统一 batch=time.strftime(...),逐个候选调 mark_separated(..., batch=batch),written 非空且 cg 有 rebuild_index 时调 cg.rebuild_index()、否则调 invalidate_cache(cg),apply 为假值时不写盘;返回 ok、action='separate'、dry_run=not apply、layer、candidates、written、written_count、elapsed_ms、log 与 meta、note;
447
- def separate_run(cg, layer=None, pairs=None, apply=False, ids=None,
448
- min_jaccard=SEP_MIN_JACCARD, limit=SEP_MAX_PAIRS,
449
- actor="maintain"):
450
- """模式分离:扫描相似但条件不同的节点对,按需写入对称分离边。
451
-
452
- apply=False(默认)只出候选报表(对应计划「可预演」)。
453
- """
454
- t0 = time.time()
455
- if pairs is None:
456
- rep = separation_pairs(cg, layer=layer, ids=ids,
457
- min_jaccard=min_jaccard, limit=limit)
458
- cands = rep["candidates"]
459
- meta = {k: rep[k] for k in ("scanned", "compared", "truncated",
460
- "total_candidates")}
461
- else:
462
- cands = list(pairs)
463
- meta = {"scanned": 0, "compared": 0, "truncated": False,
464
- "total_candidates": len(cands)}
465
- written = []
466
- if apply:
467
- batch = time.strftime("%Y%m%d-%H%M%S")
468
- for c in cands:
469
- written.append(mark_separated(cg, c["a"], c["b"],
470
- reason=c.get("reason", ""),
471
- actor=actor, batch=batch))
472
- if written and hasattr(cg, "rebuild_index"):
473
- cg.rebuild_index() # 边写盘后重建索引 + 清子图缓存
474
- else:
475
- invalidate_cache(cg)
476
- return {"ok": True, "action": "separate", "dry_run": not apply,
477
- "layer": layer, "candidates": cands, "written": written,
478
- "written_count": len(written), "elapsed_ms": int((time.time() - t0) * 1000),
479
- "log": MAINTAIN_LOG, **meta,
480
- "note": ("dry-run:未写盘;apply=True 才写分离边" if not apply
481
- else f"已写 {len(written)} 对对称分离边")}
482
-
483
-
484
- # ==========================================================================
485
- # 情景重构(insight.reconstruct)
486
- # ==========================================================================
487
- #
488
- # 问题:记忆被检索回来时,往往只剩一条「结论」,它当时**为什么成立**(生效条件 /
489
- # 不适用条件 / 同行情境)已经散落在相邻节点里。情景重构做的就是:由给定线索
490
- # (词面 / 节点 id)反推当时的**条件空间**,把散落的场景要素重新聚在一起。
491
- #
492
- # 与检索(read/route)的区别:检索按「当前查询」做资格判定;重构按「线索」做
493
- # **条件空间复原**,输出条件结构而非排序结果,用于回答「这件事成立于什么条件」。
494
- #
495
- # 诚实边界:
496
- # · 重构是条件空间的**近似重建**,不是事件回放(输出显式标注);
497
- # · 定位不到任何锚点时判 blindspot,**不得凭空编造条件空间**。
498
-
499
- RECON_MIN_SCORE = 0.15 # 词面命中下限(低于此不算锚点)
500
- RECON_MAX_NODES = 80 # 单次扫描节点上限(防 O(N) 读盘爆炸)
501
- RECON_MAX_ANCHORS = 12 # 锚点上限
502
- RECON_NEIGHBOR_LIMIT = 24 # 每锚点取的邻居上限
503
- RECON_COMMON_SHARE = 0.5 # 共同条件判定:≥ 半数锚点共享
504
- RECON_SCENE_PREFIX = "scene_"
505
-
506
-
507
- # 生效条件:clues 为 None 时返回 [];clues 为 str 时包装成单元素列表;逐项取 str(c or "").strip().lower(),空串跳过,整串去重入 out,再按 re.split(r"[\s,,、;;/|]+") 切词、去重后追加;返回 out;
508
- def _clue_terms(clues):
509
- """线索归一化:字符串 / 列表 → 去重词面(保留整串 + 切分后的词)。"""
510
- if clues is None:
511
- return []
512
- if isinstance(clues, str):
513
- clues = [clues]
514
- out = []
515
- for c in clues:
516
- s = str(c or "").strip().lower()
517
- if not s:
518
- continue
519
- if s not in out:
520
- out.append(s)
521
- for w in re.split(r"[\s,,、;;/|]+", s):
522
- w = w.strip()
523
- if w and w not in out:
524
- out.append(w)
525
- return out
526
-
527
-
528
- # 生效条件:term_sets 为假值(None/[])时返回 [];否则对每个集合的 set(s or ()) 统计词面出现次数,need = max(2, int(len(term_sets)*RECON_COMMON_SHARE) 向上取整),返回出现次数 >= need 的词面排序列表(阈值取自模块常量 RECON_COMMON_SHARE);
529
- def _shared_terms(term_sets):
530
- """出现在 ≥ RECON_COMMON_SHARE 比例集合中的词面(且至少 2 个集合共享)。"""
531
- if not term_sets:
532
- return []
533
- cnt = {}
534
- for s in term_sets:
535
- for t in set(s or ()):
536
- cnt[t] = cnt.get(t, 0) + 1
537
- need = max(2, int(len(term_sets) * RECON_COMMON_SHARE) + (1 if len(term_sets) * RECON_COMMON_SHARE % 1 else 0))
538
- return sorted(t for t, c in cnt.items() if c >= need)
539
-
540
-
541
- # 生效条件:按 term_sets 原顺序遍历每个集合的 sorted(s or ()),首次出现的词面追加到 seen,返回 seen[:limit](limit 默认 99,传 0 时切片为空列表,无 or 回落);
542
- def _uniq_terms(term_sets, limit=99):
543
- seen = []
544
- for s in term_sets:
545
- for t in sorted(s or ()):
546
- if t not in seen:
547
- seen.append(t)
548
- return seen[:limit]
549
-
550
-
551
- # 生效条件:bigrams(" ".join(clue_terms)) 为空时返回 [];否则遍历 pool,cg.get(nid) 为假值、或 forgetting.payload 后 bigrams 为空、或与 clue_grams 无交集时跳过,否则 jac=inter/len(并集 or 1)、exact 为 clue_terms 中非空且作为子串出现在 body 的个数,score=jac+0.05*exact,仅 score >= RECON_MIN_SCORE 才收录,最终按 (-score, nid) 排序返回;
552
- def _anchor_scores(cg, pool, clue_terms):
553
- """按词面重合给候选锚点打分(返回 [(nid, score, shared, exact)],降序)。"""
554
- from . import forgetting
555
- from .mdcg import bigrams
556
- clue_grams = bigrams(" ".join(clue_terms))
557
- if not clue_grams:
558
- return []
559
- scored = []
560
- for nid in pool:
561
- node = cg.get(nid)
562
- if not node:
563
- continue
564
- body = forgetting.payload(node.get("content") or "")
565
- grams = bigrams(body)
566
- if not grams:
567
- continue
568
- inter = len(grams & clue_grams)
569
- if not inter:
570
- continue
571
- jac = inter / float(len(grams | clue_grams) or 1)
572
- exact = sum(1 for t in clue_terms if t and t in body)
573
- score = jac + 0.05 * exact
574
- if score >= RECON_MIN_SCORE:
575
- scored.append((nid, round(score, 4), inter, exact))
576
- scored.sort(key=lambda x: (-x[1], x[0]))
577
- return scored
578
-
579
-
580
- # 生效条件:对 anchor_ids 中每个 nid,cg.get(nid) 为假值跳过,否则取 fm 与 consistency.condition_terms 得 pos/neg,per[nid] 记为 {"effective": sorted(pos)[:8], "non_applicable": sorted(neg)[:8]};返回 common=_shared_terms(pos_sets)、individual=_uniq_terms(pos_sets) 中不在 common 的项、non_applicable=_uniq_terms(neg_sets)、declared=list(declared_terms or [])、per_anchor=per;
581
- def _condition_space(cg, anchor_ids, declared_terms=None):
582
- """合成锚点群的条件空间:共同条件 / 个别条件 / 不适用条件 / 逐锚点明细。"""
583
- from . import consistency
584
- pos_sets, neg_sets, per = [], [], {}
585
- for nid in anchor_ids:
586
- node = cg.get(nid)
587
- if not node:
588
- continue
589
- fm = node.get("frontmatter") or {}
590
- pos, neg = consistency.condition_terms(fm, node.get("content") or "")
591
- pos_sets.append(pos)
592
- neg_sets.append(neg)
593
- per[nid] = {"effective": sorted(pos)[:8], "non_applicable": sorted(neg)[:8]}
594
- common = _shared_terms(pos_sets)
595
- individual = [t for t in _uniq_terms(pos_sets) if t not in common]
596
- return {"common": common, "individual": individual,
597
- "non_applicable": _uniq_terms(neg_sets),
598
- "declared": list(declared_terms or []), "per_anchor": per}
599
-
600
-
601
- # 生效条件:以 cg.index["nodes"] 为节点池(layer 为真时只保留 layer 等于该值的节点,池按 max_nodes 截断),由 clues 词项与 ids 收集锚点并截取 limit 个,锚点为空则返回 status="blindspot" 且 condition_space=None,否则返回 status="reconstructed"(neighbors 为真时按链邻接补边并在 max_nodes 内补节点,apply 为真且算出的 scene_id 尚不在 nodes 中时再写入该情境节点并记日志);
602
- def reconstruct_scene(cg, clues=None, ids=None, conditions=None, layer=None,
603
- max_nodes=RECON_MAX_NODES, limit=RECON_MAX_ANCHORS,
604
- neighbors=True, apply=False, actor="insight"):
605
- """情景重构:由线索反推条件空间,还原记忆成立的场景。
606
-
607
- 只读(apply=False 默认);apply=True 额外落一个 `scene_<hash>` 情境节点,
608
- 携带 `reconstructed_from` 锚点与 inferred 边,便于追溯与撤销。
609
-
610
- 返回 status ∈ {"reconstructed", "blindspot"};blindspot 时条件空间为 None,
611
- 不编造(对应计划「诚实边界」)。
612
- """
613
- from . import chain
614
- t0 = time.time()
615
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
616
- clue_terms = _clue_terms(clues)
617
- declared = [str(c).strip().lower() for c in (conditions or []) if str(c).strip()]
618
- want_ids = [str(x) for x in (ids or []) if str(x).strip()]
619
-
620
- # 线索里若直接写了节点 id,也算显式锚点
621
- for t in clue_terms:
622
- if t in nodes and t not in want_ids:
623
- want_ids.append(t)
624
-
625
- pool = [nid for nid, e in nodes.items()
626
- if (not layer or (e or {}).get("layer") == layer)]
627
- truncated = len(pool) > int(max_nodes)
628
- pool.sort()
629
- pool = pool[:int(max_nodes)]
630
-
631
- anchors, missing = [], []
632
- for nid in want_ids:
633
- if nid in nodes:
634
- anchors.append({"node_id": nid, "score": 1.0, "source": "explicit"})
635
- else:
636
- missing.append(nid)
637
- if clue_terms:
638
- have = {a["node_id"] for a in anchors}
639
- for nid, score, inter, exact in _anchor_scores(cg, pool, clue_terms):
640
- if nid in have:
641
- continue
642
- anchors.append({"node_id": nid, "score": score, "source": "lexical",
643
- "shared_bigrams": inter, "exact_hits": exact})
644
- have.add(nid)
645
- anchors = anchors[:int(limit)]
646
-
647
- base = {"ok": True, "action": "reconstruct", "op": "insight",
648
- "clues": clue_terms, "requested_ids": want_ids, "layer": layer,
649
- "truncated": truncated, "scanned": len(pool)}
650
-
651
- if not anchors:
652
- base.update({
653
- "status": "blindspot", "anchors": [], "nodes": [], "edges": [],
654
- "condition_space": None,
655
- "missing": missing or (["锚点"] if clue_terms else ["线索"]),
656
- "elapsed_ms": int((time.time() - t0) * 1000),
657
- "note": "线索未能定位任何锚点:判 blindspot,不编造条件空间(诚实边界)"})
658
- return base
659
-
660
- anchor_ids = [a["node_id"] for a in anchors]
661
- node_ids, edges = list(anchor_ids), []
662
- if neighbors:
663
- adj = {}
664
- try:
665
- adj = chain.adjacency(cg)
666
- except Exception:
667
- adj = {}
668
- seen = set(node_ids)
669
- for nid in anchor_ids:
670
- outs = (adj.get(nid) or [])[:RECON_NEIGHBOR_LIMIT]
671
- for tgt, e in outs:
672
- rel = chain.edge_rel(e)
673
- edges.append({"from": nid, "to": tgt, "relation": rel,
674
- "evidence": (e or {}).get("evidence")})
675
- if tgt not in seen and len(node_ids) < int(max_nodes):
676
- seen.add(tgt)
677
- node_ids.append(tgt)
678
-
679
- cs = _condition_space(cg, anchor_ids, declared_terms=declared)
680
- separated = [nid for nid in anchor_ids
681
- if any(e.get("relation") == "distinct_from" for e in edges
682
- if e.get("from") == nid)]
683
- with_conditions = sum(1 for nid in anchor_ids
684
- if (cs["per_anchor"].get(nid) or {}).get("effective"))
685
- confidence = round(
686
- min(1.0, (sum(a["score"] for a in anchors) / float(len(anchors)))
687
- * (0.5 + 0.5 * with_conditions / float(len(anchor_ids)))), 4)
688
-
689
- out = dict(base)
690
- out.update({
691
- "status": "reconstructed", "anchors": anchors, "nodes": node_ids,
692
- "edges": edges, "condition_space": cs,
693
- "signals": {"anchors": len(anchors), "with_conditions": with_conditions,
694
- "separated": separated},
695
- "confidence": confidence,
696
- "elapsed_ms": int((time.time() - t0) * 1000),
697
- "note": "条件空间的近似重建(非事件回放);common=共同生效条件,"
698
- "individual=个别条件,non_applicable=不适用条件"})
699
-
700
- if apply:
701
- sid = RECON_SCENE_PREFIX + hashlib.sha1(
702
- "|".join(sorted(node_ids)).encode("utf-8")).hexdigest()[:10]
703
- written = False
704
- if sid not in nodes:
705
- common_txt = ";".join(cs["common"][:6]) or "(未提炼出共同条件)"
706
- neg_txt = ";".join(cs["non_applicable"][:6]) or "(未判定)"
707
- body = (
708
- "# 功能名:情景重构:%s\n"
709
- "# 生效条件:%s\n"
710
- "# 子功能:由 %d 个锚点复原的条件空间(锚点:%s)\n"
711
- "# 执行:由 insight.reconstruct 反推(inferred 近似重建,非事件回放)\n"
712
- "# 验证方式:待验证(推断结果,需人工/实践核对后方可升格)\n"
713
- "# 不适用条件:%s\n"
714
- % ("、".join(clue_terms[:6]) or "线索", common_txt, len(anchor_ids),
715
- "、".join(anchor_ids), neg_txt))
716
- cg.add(sid, body, layer="contextual",
717
- tags=["scene", "reconstructed"], importance=0.4,
718
- verification_basis="other", reconstructed_from=list(anchor_ids),
719
- clues=list(clue_terms), confidence=confidence,
720
- actor=actor)
721
- append_jsonl(os.path.join(cg.root, MAINTAIN_LOG), {
722
- "t": time.time(), "action": "reconstruct", "scene": sid,
723
- "anchors": list(anchor_ids), "clues": clue_terms,
724
- "common": cs["common"], "confidence": confidence, "actor": actor})
725
- invalidate_cache(cg)
726
- written = True
727
- out["scene_id"] = sid
728
- out["scene_written"] = written
729
- return out
1
+ # -*- coding: utf-8 -*-
2
+ """嵌套子图(nested subgraph):结构要素的可递归表示 + flatten。
3
+
4
+ 对齐 AEIS《认知图写入纪律 v1.0》四要素中的第三项:
5
+ subgraph = 内部子内容(可嵌套:子节点 + 边),角色=结构(可检索/可递归)。
6
+
7
+ 与 AEIS 的两处**有意差异**(都有工程理由,不是简化):
8
+
9
+ 1) 存储形态:AEIS 把子节点**内联**在父节点 `subgraph.nodes` 里
10
+ (`aeis/image_semantics_cg.py:37-57`)。md_cg 的检索粒度是「节点 = 文件」
11
+ (RRF 按节点召回),内联子节点无法被任何检索路单独命中,会违背「子图可检索」
12
+ 这一规范本身。故 md_cg 采用**引用式**:`subgraph.nodes` 存子节点 id,
13
+ 子节点各自是独立 `.md`,可被 lexical/entity/chain 等路独立召回。
14
+ 同时兼容内联写法(`{"id": ...}` 对象)以承接 AEIS 语料。
15
+
16
+ 2) 递归深度:AEIS 明确「深度上限不是协议常数,数据驱动,= 可分性条件的自然耗尽」
17
+ (`子部件提取_理论稿_v0.4.md:146-150`)。故 `max_depth=None` 表示一直展开到
18
+ 自然耗尽(无子节点);`max_depth=k` 只是工程硬截断,会置 `truncated=True`。
19
+
20
+ 父边唯一(每个节点至多一个 `part_of` 父)——`子部件提取_理论稿_v0.4.md:105`;
21
+ 违反者由 `validate()` 报 `multi_parent`,对齐「树形不一致 → 退回 DEFER」。
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import os
27
+ import re
28
+ import time
29
+
30
+ from .fsutil import append_jsonl
31
+
32
+ MAX_DEPTH_HARD = 64 # 工程硬截断上限(防止畸形数据把遍历拖爆)
33
+ MAX_NODES_DEFAULT = 2000 # 单次展开的节点上限
34
+
35
+ # ---- 模式分离(maintain.separate)参数 -----------------------------------
36
+ MAINTAIN_LOG = "_maintain.jsonl"
37
+ SEP_MIN_JACCARD = 0.55 # 内容相似度下限(越高=越像,才值得谈分离)
38
+ SEP_MAX_COND_OVERLAP = 0.35 # 条件重合上限(越高=条件越同,就该合并而非分离)
39
+ SEP_MAX_NODES = 400 # 单次扫描节点上限(防 O(N²) 读盘爆炸)
40
+ SEP_MAX_PAIRS = 50 # 单次返回/落库的候选对上限
41
+
42
+
43
+ # 生效条件:fm 为假值或 fm.get("subgraph") 取值为假(None/[]/{}/空串)时直接返回 {'nodes': [], 'edges': []};subgraph 为 list/tuple 时其元素作节点、边记空列表,为 dict 时节点取 sg.get("nodes")(缺键或假值回落 [])、边取 sg.get("edges")(同样回落),为其他类型仍返回空;节点项为 dict 时取 n.get("id")、否则取元素本身,nid 为 None 跳过,仅当 str(nid).strip() 非空且未出现过才追加;边只保留 isinstance(e, dict) 的项;
44
+ def declared(fm):
45
+ """规范化 `frontmatter.subgraph` → `{"nodes": [id...], "edges": [edge...]}`。
46
+
47
+ 兼容三种写法:
48
+ {"nodes": ["a", "b"], "edges": [...]} 引用式(md_cg 主用)
49
+ ["a", "b"] 纯 id 列表
50
+ {"nodes": [{"id": "a"}, ...]} 内联式(AEIS 原样,只取 id)
51
+ """
52
+ sg = (fm or {}).get("subgraph")
53
+ if not sg:
54
+ return {"nodes": [], "edges": []}
55
+ if isinstance(sg, (list, tuple)):
56
+ raw_nodes, raw_edges = list(sg), []
57
+ elif isinstance(sg, dict):
58
+ raw_nodes, raw_edges = list(sg.get("nodes") or []), list(sg.get("edges") or [])
59
+ else:
60
+ return {"nodes": [], "edges": []}
61
+ nodes = []
62
+ for n in raw_nodes:
63
+ nid = n.get("id") if isinstance(n, dict) else n
64
+ if nid is None:
65
+ continue
66
+ nid = str(nid).strip()
67
+ if nid and nid not in nodes:
68
+ nodes.append(nid)
69
+ return {"nodes": nodes, "edges": [e for e in raw_edges if isinstance(e, dict)]}
70
+
71
+
72
+ # 生效条件:当 getattr(cg,"index",None) 的 nodes 字典中存在 nid(entry 非 None)且 "subgraph" in entry 时,返回 {'id': nid, 'subgraph': entry.get("subgraph"), 'edges': entry.get("edges") or []}(subgraph 值为 None 也照样返回);否则改走 cg.get(nid),返回 (node or {}).get("frontmatter") or {},节点不存在或 frontmatter 为假值时得空 dict;
73
+ def _fm(cg, nid):
74
+ """取节点 frontmatter:优先索引快照(免 IO),快照缺字段时回退读文件。"""
75
+ entry = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid)
76
+ if entry is not None and "subgraph" in entry:
77
+ return {"id": nid, "subgraph": entry.get("subgraph"),
78
+ "edges": entry.get("edges") or []}
79
+ node = cg.get(nid)
80
+ return (node or {}).get("frontmatter") or {}
81
+
82
+
83
+ # --------------------------------------------------------------------------
84
+ # 迁移边适配:源 sqlite 认知图 → md_cg 本地边
85
+ # --------------------------------------------------------------------------
86
+ # 源库的 `hierarchical` 方向是 **source=父、target=子**(白箱
87
+ # `wisdom-book-cloud.db` 全量实测:`source_is_parent=2832`、`target_is_parent=0`)。
88
+ # md_cg 的父边约定与其**相反**:`part_of` 表示「本节点是子、target 是父」
89
+ # (见下方 children_index)。若把源库 hierarchical 原样搬进来,树会整体倒置;
90
+ # 故迁移时翻译成语义等价的 `contains`(本节点是父、target 是子)。
91
+ SRC_REL_MAP = {"hierarchical": "contains"}
92
+
93
+
94
+ # 生效条件:rel 先经 str(rel or "").strip().lower() 归一(None/空串得 ""),返回 {'target': str(tgt), 'relation_type': SRC_REL_MAP.get(rel, rel), 'confidence': confidence, 'verified': verified}——归一后的 rel 命中模块常量 SRC_REL_MAP 时用映射值、未命中(含 rel 为空串)时原样保留,confidence/verified 直接透传默认 1.0/0 的实参不做校验;
95
+ def normalize_edge(tgt, rel, confidence=1.0, verified=0):
96
+ """源库边 → md_cg 本地边(迁移器专用)。做两件事:
97
+
98
+ 1) 键名统一为 `relation_type`。`children_index` / `parents_index` 只认
99
+ `relation_type` / `relation`;早期迁移器写的 `"type"` 会被静默忽略
100
+ (`chain.edge_rel` 兼容 `type`,但 `subgraph` 不兼容),使迁入的图不可遍历。
101
+ 2) 方向按 `SRC_REL_MAP` 翻译(源库 hierarchical = source 是父)。
102
+ """
103
+ rel = str(rel or "").strip().lower()
104
+ return {"target": str(tgt), "relation_type": SRC_REL_MAP.get(rel, rel),
105
+ "confidence": confidence, "verified": verified}
106
+
107
+
108
+ # 生效条件:cg 的 `_subgraph_children` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 收子节点、并按边 rel 为 part_of/hierarchical(target 记为父、当前 pid 记为子)或 parent_of/contains(当前 pid 记为父、target 记为子,target 为 None 则该边跳过)补全 parent_id→[child_id] 索引,回写 cg._subgraph_children 后返回 idx;
109
+ def children_index(cg):
110
+ """全局正查:parent_id → [child_id...](声明式 ∪ 边式,一次 O(N) 后缓存)。"""
111
+ idx = getattr(cg, "_subgraph_children", None)
112
+ if idx is not None:
113
+ return idx
114
+ idx = {}
115
+ for pid in list(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys()):
116
+ fm = _fm(cg, pid)
117
+ for ch in declared(fm)["nodes"]:
118
+ idx.setdefault(pid, [])
119
+ if ch not in idx[pid]:
120
+ idx[pid].append(ch)
121
+ for e in (fm.get("edges") or []):
122
+ if not isinstance(e, dict):
123
+ continue
124
+ # 兼容历史语料:早期迁移器把关系类型写在 `type` 键(`migrate.py:43`、
125
+ # `migrate_aeis.py:99`),写侧已按 `normalize_edge` 修正,读侧一并容错。
126
+ rel = str(e.get("relation_type") or e.get("relation") or e.get("type")
127
+ or "").strip().lower()
128
+ tgt = e.get("target") or e.get("target_id")
129
+ if tgt is None:
130
+ continue
131
+ tgt = str(tgt).strip()
132
+ if rel in ("part_of", "hierarchical"): # 本节点是子,target 是父
133
+ idx.setdefault(tgt, [])
134
+ if pid not in idx[tgt]:
135
+ idx[tgt].append(pid)
136
+ elif rel in ("parent_of", "contains"): # 本节点是父,target 是子
137
+ idx.setdefault(pid, [])
138
+ if tgt not in idx[pid]:
139
+ idx[pid].append(tgt)
140
+ try:
141
+ cg._subgraph_children = idx
142
+ except Exception:
143
+ pass
144
+ return idx
145
+
146
+
147
+ # 生效条件:返回 list(children_index(cg).get(nid) or []),即 nid 在索引中无键或对应值为空(None/空列表)时得空列表,否则返回其子 id 的浅拷贝列表;
148
+ def children(cg, nid):
149
+ """直接子节点 id 列表:`subgraph.nodes` 声明 ∪ 边式父子关系。"""
150
+ return list(children_index(cg).get(nid) or [])
151
+
152
+
153
+ # 生效条件:cg 的 `_subgraph_parents` 不为 None 时原样返回该缓存字典,否则遍历 cg.index["nodes"] 的键,用 declared(fm)["nodes"] 把 pid 记为每个子节点的父、并按边 rel 为 part_of/hierarchical(target 记为 pid 的父)或 parent_of/contains(pid 记为 target 的父,target 为 None 则该边跳过)补全 child_id→[parent_id] 索引,回写 cg._subgraph_parents 后返回 idx;
154
+ def parents_index(cg):
155
+ """全局反查:child_id → [parent_id...](供 children/validate 复用,一次 O(N))。"""
156
+ idx = getattr(cg, "_subgraph_parents", None)
157
+ if idx is not None:
158
+ return idx
159
+ idx = {}
160
+ for pid in list(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys()):
161
+ for ch in declared(_fm(cg, pid))["nodes"]:
162
+ idx.setdefault(ch, [])
163
+ if pid not in idx[ch]:
164
+ idx[ch].append(pid)
165
+ for e in (_fm(cg, pid).get("edges") or []):
166
+ if not isinstance(e, dict):
167
+ continue
168
+ rel = str(e.get("relation_type") or e.get("relation") or e.get("type")
169
+ or "").strip().lower()
170
+ tgt = e.get("target") or e.get("target_id")
171
+ if tgt is None:
172
+ continue
173
+ tgt = str(tgt).strip()
174
+ # 与 children_index 逐字对称,只是方向取反:
175
+ # part_of / hierarchical → 本节点是子,故 target 是本节点的父
176
+ # parent_of / contains → 本节点是父,故本节点是 target 的父
177
+ if rel in ("part_of", "hierarchical"):
178
+ idx.setdefault(pid, [])
179
+ if tgt not in idx[pid]:
180
+ idx[pid].append(tgt)
181
+ elif rel in ("parent_of", "contains"):
182
+ idx.setdefault(tgt, [])
183
+ if pid not in idx[tgt]:
184
+ idx[tgt].append(pid)
185
+ try:
186
+ cg._subgraph_parents = idx
187
+ except Exception:
188
+ pass
189
+ return idx
190
+
191
+
192
+ # 生效条件:无条件执行——把 cg 的 _subgraph_parents 与 _subgraph_children 依次 setattr 为 None(setattr 抛异常被 except 吞掉后继续下一个),无返回值,与 cg 是否已建有缓存无关;
193
+ def invalidate_cache(cg):
194
+ """写入/删除节点后调用,丢弃父子正查/反查缓存。"""
195
+ for attr in ("_subgraph_parents", "_subgraph_children"):
196
+ try:
197
+ setattr(cg, attr, None)
198
+ except Exception:
199
+ pass
200
+
201
+
202
+ # 生效条件:取 parents_index(cg).get(nid) or [],列表非空时返回其首元素,键缺失或值为空/假(None、[])时返回 None;多父时只返回第一个,不在此处报歧义;
203
+ def parent_of(cg, nid):
204
+ """唯一父(多父时返回第一个并置 `ambiguous` 标记由 validate 报出)。"""
205
+ ps = parents_index(cg).get(nid) or []
206
+ return ps[0] if ps else None
207
+
208
+
209
+ # 生效条件:max_depth 非 None 时先 max(0, min(int(max_depth), MAX_DEPTH_HARD)) 收敛;以 nid 为根迭代 DFS,已入 seen 的节点跳过,len(seen) >= max_nodes(max_nodes 传 0 时首次循环即成立)置 truncated=True 并 break,depth >= max_depth 且 children 非空时置 truncated=True 并 continue,否则对 children(cg,cur) 逆序、对未出现过的 (ch,cur) 追加 part_of 边并压栈;返回 {'root','nodes','paths','edges','n_nodes','n_edges','truncated'};
210
+ def expand(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
211
+ """递归展开子树(迭代 DFS,防递归深度爆栈)。
212
+
213
+ 返回 `{"root","nodes","paths","edges","n_nodes","n_edges","truncated"}`;
214
+ `paths` 为 `节点 id → "根/子/孙"` 层级路径(flatten 后仍可定位来源)。
215
+ 边为 `part_of(child→parent)`,与 AEIS flatten 的方向一致。
216
+ """
217
+ if max_depth is not None:
218
+ max_depth = max(0, min(int(max_depth), MAX_DEPTH_HARD))
219
+ seen, edges, truncated = {}, [], False
220
+ seen_edges = set()
221
+ stack = [(nid, 0, nid)]
222
+ while stack:
223
+ cur, depth, path = stack.pop()
224
+ if cur in seen:
225
+ continue
226
+ if len(seen) >= max_nodes:
227
+ truncated = True
228
+ break
229
+ seen[cur] = path
230
+ if max_depth is not None and depth >= max_depth:
231
+ if children(cg, cur):
232
+ truncated = True
233
+ continue
234
+ kids = children(cg, cur)
235
+ for ch in reversed(kids):
236
+ key = (ch, cur)
237
+ if key not in seen_edges:
238
+ seen_edges.add(key)
239
+ edges.append({"source": ch, "target": cur,
240
+ "relation_type": "part_of",
241
+ "confidence": 1.0, "verified": 0})
242
+ if ch not in seen:
243
+ stack.append((ch, depth + 1, f"{path}/{ch}"))
244
+ return {"root": nid, "nodes": list(seen.keys()), "paths": seen,
245
+ "edges": edges, "n_nodes": len(seen), "n_edges": len(edges),
246
+ "truncated": truncated}
247
+
248
+
249
+ # 生效条件:先调用 expand(cg, nid, max_depth=max_depth, max_nodes=max_nodes),对其中每条边复制一份并追加 source/target 互换、relation_type="parent_of"、confidence/verified 沿用原边 .get(…,1.0/0) 的反向边;返回 root=nid、nodes=ex["nodes"]、paths=ex["paths"]、n_nodes=ex["n_nodes"]、n_edges=双边后条数、truncated=ex["truncated"];
250
+ def flatten(cg, nid, max_depth=None, max_nodes=MAX_NODES_DEFAULT):
251
+ """把嵌套子图摊平成「节点 + 边」,父子生成**对称双边**。
252
+
253
+ 对齐 AEIS `flatten_image_semantics_graph`(`image_semantics_cg.py:151-173`):
254
+ 每个有父的节点同时产出 `part_of(child→parent)` 与 `parent_of(parent→child)`。
255
+ 检索侧因此既能「从父找子」(下钻),也能「从子找父」(溯源)。
256
+ """
257
+ ex = expand(cg, nid, max_depth=max_depth, max_nodes=max_nodes)
258
+ edges = []
259
+ for e in ex["edges"]:
260
+ edges.append(dict(e))
261
+ edges.append({"source": e["target"], "target": e["source"],
262
+ "relation_type": "parent_of",
263
+ "confidence": e.get("confidence", 1.0),
264
+ "verified": e.get("verified", 0)})
265
+ return {"root": nid, "nodes": ex["nodes"], "edges": edges, "paths": ex["paths"],
266
+ "n_nodes": ex["n_nodes"], "n_edges": len(edges),
267
+ "truncated": ex["truncated"]}
268
+
269
+
270
+ # 生效条件:known 取 cg.index 的 nodes 键集合,仅当 parents_index(cg).get(nid) or [] 为空(键缺失或空列表)的 nid 才视为树根,返回其排序后的 id 列表;
271
+ def roots(cg):
272
+ """无父节点(树根)的 id 列表。"""
273
+ known = set(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys())
274
+ pmap = parents_index(cg)
275
+ return sorted(nid for nid in known if not (pmap.get(nid) or []))
276
+
277
+
278
+ # 生效条件:known 取 cg.index 的 nodes 键列表,max_scan 为真值时截断为 known[:int(max_scan)];逐 pid 遍历 declared(_fm(cg,pid))["nodes"]:ch==pid 记 self_loop 并 continue(不再进入悬空/多父判断),ch 不在 known_set 记 dangling_child,随后 parent 中已有 ch 且 prev != pid 记 multi_parent、否则写 parent[ch]=pid;再沿 parent 指针上溯,遇本路径已访问节点记 cycle;_issue 仅在 len(issues) < limit*4 时追加(limit 为 0 时 0<0 不成立,不追加任何 issue);返回 scanned、issues=len(issues)、items=issues[:limit]、truncated=len(issues)>limit;
279
+ def validate(cg, limit=50, max_scan=None):
280
+ """树一致性校验:自环 / 悬空子节点 / 多父 / 环。
281
+
282
+ 对齐 AEIS:父边唯一、拓扑无矛盾、树形不一致 → 该层判定退回 DEFER
283
+ (`子部件提取_理论稿_v0.4.md:105-107`)。
284
+ """
285
+ known = list(((getattr(cg, "index", None) or {}).get("nodes") or {}).keys())
286
+ if max_scan:
287
+ known = known[:int(max_scan)]
288
+ known_set = set(known)
289
+ parent, issues = {}, []
290
+
291
+ # 生效条件:仅当 len(issues) < limit*4 时把 kw 追加进 issues(限流防报告膨胀);无返回值,只改外层 issues;
292
+ def _issue(**kw):
293
+ if len(issues) < limit * 4:
294
+ issues.append(kw)
295
+
296
+ for pid in known:
297
+ for ch in declared(_fm(cg, pid))["nodes"]:
298
+ if ch == pid:
299
+ _issue(id=pid, issue="self_loop", child=ch)
300
+ continue
301
+ if ch not in known_set:
302
+ _issue(id=pid, issue="dangling_child", child=ch)
303
+ prev = parent.get(ch)
304
+ if prev is not None and prev != pid:
305
+ _issue(id=ch, issue="multi_parent", parents=sorted({prev, pid}))
306
+ else:
307
+ parent[ch] = pid
308
+
309
+ # 环检测:沿 parent 指针上溯,遇本路径已访问节点即成环
310
+ for nid in known:
311
+ path, cur = [], nid
312
+ while cur is not None:
313
+ if cur in path:
314
+ _issue(id=cur, issue="cycle", path=path[path.index(cur):] + [cur])
315
+ break
316
+ path.append(cur)
317
+ cur = parent.get(cur)
318
+
319
+ return {"scanned": len(known), "issues": len(issues),
320
+ "items": issues[:limit], "truncated": len(issues) > limit}
321
+
322
+
323
+ # ==========================================================================
324
+ # 模式分离(maintain.separate)
325
+ # ==========================================================================
326
+ #
327
+ # 问题:两条记忆**内容高度相似但生效条件不同**。若放任不管,检索会把它们混为
328
+ # 一体——按 A 的条件召回、却拿到 B 的结论。海马体的做法是 pattern separation:
329
+ # 把相似但情境不同的表征拆开,各挂各的条件线索。
330
+ #
331
+ # 本层的工程兑现(保守、可逆、幂等):
332
+ # · 不动正文(不制造第二份真相),只加**对称分离边** `distinct_from`;
333
+ # · 边携带 reason + 判定分量,供后续重构/审计追溯;
334
+ # · 条件**相同**的相似对**不是**分离候选(那是重复,该走 MERGE)。
335
+
336
+ # 生效条件:cg.get(nid) 为假值(None/{})时返回 None;否则取 fm=node.get("frontmatter") or {}、content=node.get("content") or "",返回 {'grams': bigrams(forgetting.payload(content)), 'pos','neg': consistency.condition_terms(fm, content) 的结果, 'layer': fm.get("layer")}——grams 为空也照常返回该字典,不返回 None;
337
+ def _node_terms_and_grams(cg, nid):
338
+ """读取节点并返回 (payload 二元组, 正条件词面, 负条件词面);不可读返回 None。"""
339
+ from . import consistency, forgetting
340
+ from .mdcg import bigrams # 惰性导入:避开 mdcg↔subgraph 循环
341
+ node = cg.get(nid)
342
+ if not node:
343
+ return None
344
+ fm = node.get("frontmatter") or {}
345
+ content = node.get("content") or ""
346
+ grams = bigrams(forgetting.payload(content))
347
+ pos, neg = consistency.condition_terms(fm, content)
348
+ return {"grams": grams, "pos": pos, "neg": neg, "layer": fm.get("layer")}
349
+
350
+
351
+ # 生效条件:pool 初取 cg.index 的 nodes 键,layer 为假值(None/"")时不按层过滤、否则只留 e.get("layer")==layer 的 nid,ids 为真值时只留 str(x) 属于该集合的 nid;pool 排序后 truncated=len(pool)>int(max_nodes)、并截为前 int(max_nodes) 个;只有 _node_terms_and_grams 返回非 None 且 grams 非空的节点进 cache;对 cache 键两两比较——无公共 bigram 跳过、jaccard 小于 float(min_jaccard) 跳过、condition_distinct 的 distinct 为假或 overlap > float(max_cond_overlap) 跳过,其余成候选并按 (-jaccard,a,b) 排序;返回 scanned、compared、candidates=out[:int(limit)]、total_candidates、truncated、note;
352
+ def separation_pairs(cg, layer=None, ids=None, max_nodes=SEP_MAX_NODES,
353
+ min_jaccard=SEP_MIN_JACCARD,
354
+ max_cond_overlap=SEP_MAX_COND_OVERLAP,
355
+ limit=SEP_MAX_PAIRS):
356
+ """找出「内容高度相似、条件却不同」的节点对(模式分离候选)。
357
+
358
+ 返回 {scanned, compared, candidates:[{a,b,jaccard,cond_overlap,distinct,reason}],
359
+ truncated, note}。只读,不写盘。
360
+ """
361
+ from . import consistency
362
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
363
+ pool = [nid for nid, e in nodes.items()
364
+ if (not layer or e.get("layer") == layer)]
365
+ if ids:
366
+ want = {str(x) for x in ids}
367
+ pool = [nid for nid in pool if nid in want]
368
+ pool.sort()
369
+ truncated = len(pool) > int(max_nodes)
370
+ pool = pool[:int(max_nodes)]
371
+ cache = {}
372
+ for nid in pool:
373
+ got = _node_terms_and_grams(cg, nid)
374
+ if got and got["grams"]:
375
+ cache[nid] = got
376
+ keys = sorted(cache.keys())
377
+ out, compared = [], 0
378
+ for i in range(len(keys)):
379
+ gi = cache[keys[i]]["grams"]
380
+ for j in range(i + 1, len(keys)):
381
+ gj = cache[keys[j]]["grams"]
382
+ inter = len(gi & gj)
383
+ if not inter:
384
+ continue
385
+ compared += 1
386
+ jac = inter / float(len(gi | gj) or 1)
387
+ if jac < float(min_jaccard):
388
+ continue
389
+ a, b = keys[i], keys[j]
390
+ cd = consistency.condition_distinct(
391
+ cache[a]["pos"], cache[a]["neg"],
392
+ cache[b]["pos"], cache[b]["neg"])
393
+ if not cd["distinct"] or cd["overlap"] > float(max_cond_overlap):
394
+ continue # 条件相同 → 是重复,不是分离
395
+ out.append({
396
+ "a": a, "b": b,
397
+ "a_layer": cache[a]["layer"], "b_layer": cache[b]["layer"],
398
+ "jaccard": round(jac, 4), "cond_overlap": cd["overlap"],
399
+ "reason": (f"内容相似 {jac:.2f} 但条件重合仅 {cd['overlap']:.2f}"
400
+ f"(相似而不同情境,需分离)"),
401
+ "a_conditions": sorted(cache[a]["pos"] | cache[a]["neg"])[:6],
402
+ "b_conditions": sorted(cache[b]["pos"] | cache[b]["neg"])[:6],
403
+ })
404
+ out.sort(key=lambda x: (-x["jaccard"], x["a"], x["b"]))
405
+ return {"scanned": len(keys), "compared": compared,
406
+ "candidates": out[:int(limit)],
407
+ "total_candidates": len(out), "truncated": truncated,
408
+ "note": "只读候选;apply=True 才写分离边"}
409
+
410
+
411
+ # 生效条件:batch 为假值(None 或空串)时回落到 time.strftime("%Y%m%d-%H%M%S");按 (a,b)、(b,a) 两向处理:cg.get(src) 为假值跳过,dst 已出现在 consistency.separation_targets(fm) 中跳过,否则往 fm["edges"] 追加 distinct_from 边、向 fm["pattern_separated_from"] 追加 dst,按 os.path.join(cg.root, e.get("path") or f"{src}.md") 调 cg._write_node 写盘并追加 MAINTAIN_LOG 行,src 记入 written;返回 {'a','b','written','batch'};
412
+ def mark_separated(cg, a, b, reason="", actor="maintain", batch=None):
413
+ """写入 (a↔b) 对称 `distinct_from` 边(幂等:已存在则不重复写)。"""
414
+ from . import consistency
415
+ batch = batch or time.strftime("%Y%m%d-%H%M%S")
416
+ written = []
417
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
418
+ for src, dst in ((a, b), (b, a)):
419
+ node = cg.get(src)
420
+ if not node:
421
+ continue
422
+ fm = node.get("frontmatter") or {}
423
+ edges = list(fm.get("edges") or [])
424
+ if dst in consistency.separation_targets(fm):
425
+ continue
426
+ edges.append({"target": dst, "relation_type": consistency.SEPARATION_REL,
427
+ "reason": reason, "created_at": time.time(),
428
+ "confidence": 1.0, "verified": 0})
429
+ fm["edges"] = edges
430
+ sep = list(fm.get("pattern_separated_from") or [])
431
+ if dst not in sep:
432
+ sep.append(dst)
433
+ fm["pattern_separated_from"] = sep
434
+ e = nodes.get(src) or {}
435
+ path = os.path.join(cg.root, e.get("path") or f"{src}.md")
436
+ cg._write_node(src, path, fm, node.get("content") or "")
437
+ if e:
438
+ e["edges"] = edges
439
+ written.append(src)
440
+ append_jsonl(os.path.join(cg.root, MAINTAIN_LOG), {
441
+ "t": time.time(), "action": "separate", "batch": batch,
442
+ "from": src, "to": dst, "reason": reason, "actor": actor})
443
+ return {"a": a, "b": b, "written": written, "batch": batch}
444
+
445
+
446
+ # 生效条件:pairs 为 None 时调 separation_pairs 取 candidates 与 meta(scanned/compared/truncated/total_candidates),否则 cands=list(pairs) 且 meta 中 scanned/compared 记 0、truncated 记 False、total_candidates=len(cands);apply 为真值时先生成统一 batch=time.strftime(...),逐个候选调 mark_separated(..., batch=batch),written 非空且 cg 有 rebuild_index 时调 cg.rebuild_index()、否则调 invalidate_cache(cg),apply 为假值时不写盘;返回 ok、action='separate'、dry_run=not apply、layer、candidates、written、written_count、elapsed_ms、log 与 meta、note;
447
+ def separate_run(cg, layer=None, pairs=None, apply=False, ids=None,
448
+ min_jaccard=SEP_MIN_JACCARD, limit=SEP_MAX_PAIRS,
449
+ actor="maintain"):
450
+ """模式分离:扫描相似但条件不同的节点对,按需写入对称分离边。
451
+
452
+ apply=False(默认)只出候选报表(对应计划「可预演」)。
453
+ """
454
+ t0 = time.time()
455
+ if pairs is None:
456
+ rep = separation_pairs(cg, layer=layer, ids=ids,
457
+ min_jaccard=min_jaccard, limit=limit)
458
+ cands = rep["candidates"]
459
+ meta = {k: rep[k] for k in ("scanned", "compared", "truncated",
460
+ "total_candidates")}
461
+ else:
462
+ cands = list(pairs)
463
+ meta = {"scanned": 0, "compared": 0, "truncated": False,
464
+ "total_candidates": len(cands)}
465
+ written = []
466
+ if apply:
467
+ batch = time.strftime("%Y%m%d-%H%M%S")
468
+ for c in cands:
469
+ written.append(mark_separated(cg, c["a"], c["b"],
470
+ reason=c.get("reason", ""),
471
+ actor=actor, batch=batch))
472
+ if written and hasattr(cg, "rebuild_index"):
473
+ cg.rebuild_index() # 边写盘后重建索引 + 清子图缓存
474
+ else:
475
+ invalidate_cache(cg)
476
+ return {"ok": True, "action": "separate", "dry_run": not apply,
477
+ "layer": layer, "candidates": cands, "written": written,
478
+ "written_count": len(written), "elapsed_ms": int((time.time() - t0) * 1000),
479
+ "log": MAINTAIN_LOG, **meta,
480
+ "note": ("dry-run:未写盘;apply=True 才写分离边" if not apply
481
+ else f"已写 {len(written)} 对对称分离边")}
482
+
483
+
484
+ # ==========================================================================
485
+ # 情景重构(insight.reconstruct)
486
+ # ==========================================================================
487
+ #
488
+ # 问题:记忆被检索回来时,往往只剩一条「结论」,它当时**为什么成立**(生效条件 /
489
+ # 不适用条件 / 同行情境)已经散落在相邻节点里。情景重构做的就是:由给定线索
490
+ # (词面 / 节点 id)反推当时的**条件空间**,把散落的场景要素重新聚在一起。
491
+ #
492
+ # 与检索(read/route)的区别:检索按「当前查询」做资格判定;重构按「线索」做
493
+ # **条件空间复原**,输出条件结构而非排序结果,用于回答「这件事成立于什么条件」。
494
+ #
495
+ # 诚实边界:
496
+ # · 重构是条件空间的**近似重建**,不是事件回放(输出显式标注);
497
+ # · 定位不到任何锚点时判 blindspot,**不得凭空编造条件空间**。
498
+
499
+ RECON_MIN_SCORE = 0.15 # 词面命中下限(低于此不算锚点)
500
+ RECON_MAX_NODES = 80 # 单次扫描节点上限(防 O(N) 读盘爆炸)
501
+ RECON_MAX_ANCHORS = 12 # 锚点上限
502
+ RECON_NEIGHBOR_LIMIT = 24 # 每锚点取的邻居上限
503
+ RECON_COMMON_SHARE = 0.5 # 共同条件判定:≥ 半数锚点共享
504
+ RECON_SCENE_PREFIX = "scene_"
505
+
506
+
507
+ # 生效条件:clues 为 None 时返回 [];clues 为 str 时包装成单元素列表;逐项取 str(c or "").strip().lower(),空串跳过,整串去重入 out,再按 re.split(r"[\s,,、;;/|]+") 切词、去重后追加;返回 out;
508
+ def _clue_terms(clues):
509
+ """线索归一化:字符串 / 列表 → 去重词面(保留整串 + 切分后的词)。"""
510
+ if clues is None:
511
+ return []
512
+ if isinstance(clues, str):
513
+ clues = [clues]
514
+ out = []
515
+ for c in clues:
516
+ s = str(c or "").strip().lower()
517
+ if not s:
518
+ continue
519
+ if s not in out:
520
+ out.append(s)
521
+ for w in re.split(r"[\s,,、;;/|]+", s):
522
+ w = w.strip()
523
+ if w and w not in out:
524
+ out.append(w)
525
+ return out
526
+
527
+
528
+ # 生效条件:term_sets 为假值(None/[])时返回 [];否则对每个集合的 set(s or ()) 统计词面出现次数,need = max(2, int(len(term_sets)*RECON_COMMON_SHARE) 向上取整),返回出现次数 >= need 的词面排序列表(阈值取自模块常量 RECON_COMMON_SHARE);
529
+ def _shared_terms(term_sets):
530
+ """出现在 ≥ RECON_COMMON_SHARE 比例集合中的词面(且至少 2 个集合共享)。"""
531
+ if not term_sets:
532
+ return []
533
+ cnt = {}
534
+ for s in term_sets:
535
+ for t in set(s or ()):
536
+ cnt[t] = cnt.get(t, 0) + 1
537
+ need = max(2, int(len(term_sets) * RECON_COMMON_SHARE) + (1 if len(term_sets) * RECON_COMMON_SHARE % 1 else 0))
538
+ return sorted(t for t, c in cnt.items() if c >= need)
539
+
540
+
541
+ # 生效条件:按 term_sets 原顺序遍历每个集合的 sorted(s or ()),首次出现的词面追加到 seen,返回 seen[:limit](limit 默认 99,传 0 时切片为空列表,无 or 回落);
542
+ def _uniq_terms(term_sets, limit=99):
543
+ seen = []
544
+ for s in term_sets:
545
+ for t in sorted(s or ()):
546
+ if t not in seen:
547
+ seen.append(t)
548
+ return seen[:limit]
549
+
550
+
551
+ # 生效条件:bigrams(" ".join(clue_terms)) 为空时返回 [];否则遍历 pool,cg.get(nid) 为假值、或 forgetting.payload 后 bigrams 为空、或与 clue_grams 无交集时跳过,否则 jac=inter/len(并集 or 1)、exact 为 clue_terms 中非空且作为子串出现在 body 的个数,score=jac+0.05*exact,仅 score >= RECON_MIN_SCORE 才收录,最终按 (-score, nid) 排序返回;
552
+ def _anchor_scores(cg, pool, clue_terms):
553
+ """按词面重合给候选锚点打分(返回 [(nid, score, shared, exact)],降序)。"""
554
+ from . import forgetting
555
+ from .mdcg import bigrams
556
+ clue_grams = bigrams(" ".join(clue_terms))
557
+ if not clue_grams:
558
+ return []
559
+ scored = []
560
+ for nid in pool:
561
+ node = cg.get(nid)
562
+ if not node:
563
+ continue
564
+ body = forgetting.payload(node.get("content") or "")
565
+ grams = bigrams(body)
566
+ if not grams:
567
+ continue
568
+ inter = len(grams & clue_grams)
569
+ if not inter:
570
+ continue
571
+ jac = inter / float(len(grams | clue_grams) or 1)
572
+ exact = sum(1 for t in clue_terms if t and t in body)
573
+ score = jac + 0.05 * exact
574
+ if score >= RECON_MIN_SCORE:
575
+ scored.append((nid, round(score, 4), inter, exact))
576
+ scored.sort(key=lambda x: (-x[1], x[0]))
577
+ return scored
578
+
579
+
580
+ # 生效条件:对 anchor_ids 中每个 nid,cg.get(nid) 为假值跳过,否则取 fm 与 consistency.condition_terms 得 pos/neg,per[nid] 记为 {"effective": sorted(pos)[:8], "non_applicable": sorted(neg)[:8]};返回 common=_shared_terms(pos_sets)、individual=_uniq_terms(pos_sets) 中不在 common 的项、non_applicable=_uniq_terms(neg_sets)、declared=list(declared_terms or [])、per_anchor=per;
581
+ def _condition_space(cg, anchor_ids, declared_terms=None):
582
+ """合成锚点群的条件空间:共同条件 / 个别条件 / 不适用条件 / 逐锚点明细。"""
583
+ from . import consistency
584
+ pos_sets, neg_sets, per = [], [], {}
585
+ for nid in anchor_ids:
586
+ node = cg.get(nid)
587
+ if not node:
588
+ continue
589
+ fm = node.get("frontmatter") or {}
590
+ pos, neg = consistency.condition_terms(fm, node.get("content") or "")
591
+ pos_sets.append(pos)
592
+ neg_sets.append(neg)
593
+ per[nid] = {"effective": sorted(pos)[:8], "non_applicable": sorted(neg)[:8]}
594
+ common = _shared_terms(pos_sets)
595
+ individual = [t for t in _uniq_terms(pos_sets) if t not in common]
596
+ return {"common": common, "individual": individual,
597
+ "non_applicable": _uniq_terms(neg_sets),
598
+ "declared": list(declared_terms or []), "per_anchor": per}
599
+
600
+
601
+ # 生效条件:以 cg.index["nodes"] 为节点池(layer 为真时只保留 layer 等于该值的节点,池按 max_nodes 截断),由 clues 词项与 ids 收集锚点并截取 limit 个,锚点为空则返回 status="blindspot" 且 condition_space=None,否则返回 status="reconstructed"(neighbors 为真时按链邻接补边并在 max_nodes 内补节点,apply 为真且算出的 scene_id 尚不在 nodes 中时再写入该情境节点并记日志);
602
+ def reconstruct_scene(cg, clues=None, ids=None, conditions=None, layer=None,
603
+ max_nodes=RECON_MAX_NODES, limit=RECON_MAX_ANCHORS,
604
+ neighbors=True, apply=False, actor="insight"):
605
+ """情景重构:由线索反推条件空间,还原记忆成立的场景。
606
+
607
+ 只读(apply=False 默认);apply=True 额外落一个 `scene_<hash>` 情境节点,
608
+ 携带 `reconstructed_from` 锚点与 inferred 边,便于追溯与撤销。
609
+
610
+ 返回 status ∈ {"reconstructed", "blindspot"};blindspot 时条件空间为 None,
611
+ 不编造(对应计划「诚实边界」)。
612
+ """
613
+ from . import chain
614
+ t0 = time.time()
615
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
616
+ clue_terms = _clue_terms(clues)
617
+ declared = [str(c).strip().lower() for c in (conditions or []) if str(c).strip()]
618
+ want_ids = [str(x) for x in (ids or []) if str(x).strip()]
619
+
620
+ # 线索里若直接写了节点 id,也算显式锚点
621
+ for t in clue_terms:
622
+ if t in nodes and t not in want_ids:
623
+ want_ids.append(t)
624
+
625
+ pool = [nid for nid, e in nodes.items()
626
+ if (not layer or (e or {}).get("layer") == layer)]
627
+ truncated = len(pool) > int(max_nodes)
628
+ pool.sort()
629
+ pool = pool[:int(max_nodes)]
630
+
631
+ anchors, missing = [], []
632
+ for nid in want_ids:
633
+ if nid in nodes:
634
+ anchors.append({"node_id": nid, "score": 1.0, "source": "explicit"})
635
+ else:
636
+ missing.append(nid)
637
+ if clue_terms:
638
+ have = {a["node_id"] for a in anchors}
639
+ for nid, score, inter, exact in _anchor_scores(cg, pool, clue_terms):
640
+ if nid in have:
641
+ continue
642
+ anchors.append({"node_id": nid, "score": score, "source": "lexical",
643
+ "shared_bigrams": inter, "exact_hits": exact})
644
+ have.add(nid)
645
+ anchors = anchors[:int(limit)]
646
+
647
+ base = {"ok": True, "action": "reconstruct", "op": "insight",
648
+ "clues": clue_terms, "requested_ids": want_ids, "layer": layer,
649
+ "truncated": truncated, "scanned": len(pool)}
650
+
651
+ if not anchors:
652
+ base.update({
653
+ "status": "blindspot", "anchors": [], "nodes": [], "edges": [],
654
+ "condition_space": None,
655
+ "missing": missing or (["锚点"] if clue_terms else ["线索"]),
656
+ "elapsed_ms": int((time.time() - t0) * 1000),
657
+ "note": "线索未能定位任何锚点:判 blindspot,不编造条件空间(诚实边界)"})
658
+ return base
659
+
660
+ anchor_ids = [a["node_id"] for a in anchors]
661
+ node_ids, edges = list(anchor_ids), []
662
+ if neighbors:
663
+ adj = {}
664
+ try:
665
+ adj = chain.adjacency(cg)
666
+ except Exception:
667
+ adj = {}
668
+ seen = set(node_ids)
669
+ for nid in anchor_ids:
670
+ outs = (adj.get(nid) or [])[:RECON_NEIGHBOR_LIMIT]
671
+ for tgt, e in outs:
672
+ rel = chain.edge_rel(e)
673
+ edges.append({"from": nid, "to": tgt, "relation": rel,
674
+ "evidence": (e or {}).get("evidence")})
675
+ if tgt not in seen and len(node_ids) < int(max_nodes):
676
+ seen.add(tgt)
677
+ node_ids.append(tgt)
678
+
679
+ cs = _condition_space(cg, anchor_ids, declared_terms=declared)
680
+ separated = [nid for nid in anchor_ids
681
+ if any(e.get("relation") == "distinct_from" for e in edges
682
+ if e.get("from") == nid)]
683
+ with_conditions = sum(1 for nid in anchor_ids
684
+ if (cs["per_anchor"].get(nid) or {}).get("effective"))
685
+ confidence = round(
686
+ min(1.0, (sum(a["score"] for a in anchors) / float(len(anchors)))
687
+ * (0.5 + 0.5 * with_conditions / float(len(anchor_ids)))), 4)
688
+
689
+ out = dict(base)
690
+ out.update({
691
+ "status": "reconstructed", "anchors": anchors, "nodes": node_ids,
692
+ "edges": edges, "condition_space": cs,
693
+ "signals": {"anchors": len(anchors), "with_conditions": with_conditions,
694
+ "separated": separated},
695
+ "confidence": confidence,
696
+ "elapsed_ms": int((time.time() - t0) * 1000),
697
+ "note": "条件空间的近似重建(非事件回放);common=共同生效条件,"
698
+ "individual=个别条件,non_applicable=不适用条件"})
699
+
700
+ if apply:
701
+ sid = RECON_SCENE_PREFIX + hashlib.sha1(
702
+ "|".join(sorted(node_ids)).encode("utf-8")).hexdigest()[:10]
703
+ written = False
704
+ if sid not in nodes:
705
+ common_txt = ";".join(cs["common"][:6]) or "(未提炼出共同条件)"
706
+ neg_txt = ";".join(cs["non_applicable"][:6]) or "(未判定)"
707
+ body = (
708
+ "# 功能名:情景重构:%s\n"
709
+ "# 生效条件:%s\n"
710
+ "# 子功能:由 %d 个锚点复原的条件空间(锚点:%s)\n"
711
+ "# 执行:由 insight.reconstruct 反推(inferred 近似重建,非事件回放)\n"
712
+ "# 验证方式:待验证(推断结果,需人工/实践核对后方可升格)\n"
713
+ "# 不适用条件:%s\n"
714
+ % ("、".join(clue_terms[:6]) or "线索", common_txt, len(anchor_ids),
715
+ "、".join(anchor_ids), neg_txt))
716
+ cg.add(sid, body, layer="contextual",
717
+ tags=["scene", "reconstructed"], importance=0.4,
718
+ verification_basis="other", reconstructed_from=list(anchor_ids),
719
+ clues=list(clue_terms), confidence=confidence,
720
+ actor=actor)
721
+ append_jsonl(os.path.join(cg.root, MAINTAIN_LOG), {
722
+ "t": time.time(), "action": "reconstruct", "scene": sid,
723
+ "anchors": list(anchor_ids), "clues": clue_terms,
724
+ "common": cs["common"], "confidence": confidence, "actor": actor})
725
+ invalidate_cache(cg)
726
+ written = True
727
+ out["scene_id"] = sid
728
+ out["scene_written"] = written
729
+ return out