@furongjun1999/dsh-memory 0.4.11 → 0.5.0

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 (504) hide show
  1. package/README.md +16 -16
  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 +142 -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//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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -503
  24. 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
  25. 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
  26. 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
  27. 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
  28. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
  29. package/docs/mdcg/lingshu_tutorial.html +14449 -14449
  30. package/docs/mdcg/release_v0.4.11.md +49 -0
  31. package/docs/mdcg/release_v0.4.5.md +55 -55
  32. package/docs/mdcg/tool_table_v0.3.0.md +117 -117
  33. 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
  34. 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
  35. 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
  36. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
  37. 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
  38. 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
  39. 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
  40. package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
  47. 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
  48. 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
  49. 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
  50. package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. package/dsh/README.md +82 -82
  59. package/dsh/cordis.yml.example +139 -139
  60. package/dsh/update-lingshu.bat +11 -11
  61. package/lib/hooks.js +36 -2
  62. package/lib/lib/roleplay_web.js +427 -427
  63. package/md_cg/__init__.py +7 -7
  64. package/md_cg/audit.py +368 -368
  65. package/md_cg/autonomy.py +287 -287
  66. package/md_cg/backfill.py +1327 -1327
  67. package/md_cg/backfill_bigdomain.py +34 -34
  68. package/md_cg/bench6_arms.py +410 -410
  69. package/md_cg/bench6_common.py +230 -230
  70. package/md_cg/bench6_competitors.py +212 -212
  71. package/md_cg/bench_axis_domain.py +257 -257
  72. package/md_cg/bench_blind_comp.py +308 -308
  73. package/md_cg/bench_en_atoms_public.py +230 -230
  74. package/md_cg/bench_governance.py +348 -348
  75. package/md_cg/bench_lme_zh.py +410 -410
  76. package/md_cg/bench_locomo.py +121 -121
  77. package/md_cg/bench_locomo_zh.py +450 -450
  78. package/md_cg/bench_locomo_zh_public.py +147 -147
  79. package/md_cg/bench_longmem.py +112 -112
  80. package/md_cg/bench_membench.py +632 -632
  81. package/md_cg/bench_p0.py +149 -149
  82. package/md_cg/bench_progressive.py +287 -287
  83. package/md_cg/bench_role_views.py +238 -238
  84. package/md_cg/bench_task_ab.py +243 -243
  85. package/md_cg/bench_task_ab_llm.py +408 -408
  86. package/md_cg/bench_unified_en.py +204 -204
  87. package/md_cg/bench_zh_mad.py +601 -601
  88. package/md_cg/blindspot_tickets.py +123 -123
  89. package/md_cg/branches.py +285 -285
  90. package/md_cg/build_postings.py +73 -73
  91. package/md_cg/ccgc.py +1005 -948
  92. package/md_cg/census.py +132 -132
  93. package/md_cg/chain.py +300 -300
  94. package/md_cg/codeindex.py +531 -531
  95. package/md_cg/coldverify.py +292 -292
  96. package/md_cg/comment_gate.py +337 -337
  97. package/md_cg/cond_compose.py +190 -190
  98. package/md_cg/cond_facts.py +154 -154
  99. package/md_cg/cond_template.json +106 -106
  100. package/md_cg/condition_anchor.py +142 -142
  101. package/md_cg/conformance.py +726 -726
  102. package/md_cg/consistency.py +717 -717
  103. package/md_cg/consolidate.py +1536 -1439
  104. package/md_cg/corpus.py +110 -110
  105. package/md_cg/crosscheck.py +1097 -1097
  106. package/md_cg/crypto.py +437 -437
  107. package/md_cg/d_meta.py +310 -310
  108. package/md_cg/datapath.py +334 -334
  109. package/md_cg/docindex.py +473 -473
  110. package/md_cg/eval_common.py +575 -575
  111. package/md_cg/evidence.py +580 -580
  112. package/md_cg/evolution.py +477 -477
  113. package/md_cg/export.py +220 -220
  114. package/md_cg/forgetting.py +581 -581
  115. package/md_cg/fsutil.py +329 -329
  116. package/md_cg/hotcache.py +238 -214
  117. package/md_cg/hyperedge.py +251 -251
  118. package/md_cg/identity.py +390 -390
  119. package/md_cg/insight.py +500 -500
  120. package/md_cg/interop.py +199 -0
  121. package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
  122. package/md_cg/lexicon/build_standard_en.py +171 -171
  123. package/md_cg/lexicon/expand_en_zh.py +211 -211
  124. package/md_cg/lifecycle.py +272 -272
  125. package/md_cg/linkref.py +280 -280
  126. package/md_cg/links.py +622 -622
  127. package/md_cg/mcp_server.py +129 -32
  128. package/md_cg/md_whitebox.py +345 -345
  129. package/md_cg/mdcg.py +176 -117
  130. package/md_cg/mdcos.py +79 -13
  131. package/md_cg/metacognition.py +591 -591
  132. package/md_cg/migrate.py +119 -119
  133. package/md_cg/migrate_aeis.py +221 -221
  134. package/md_cg/migrate_roleplay.py +293 -293
  135. package/md_cg/migrate_wisdom_graph.py +360 -360
  136. package/md_cg/mreview/__init__.py +25 -25
  137. package/md_cg/mreview/__main__.py +110 -110
  138. package/md_cg/mreview/bundle.py +178 -178
  139. package/md_cg/mreview/candidates.py +262 -262
  140. package/md_cg/mreview/govern.py +693 -693
  141. package/md_cg/mreview/locate.py +939 -939
  142. package/md_cg/mreview/pipeline.py +728 -728
  143. package/md_cg/mreview/rules/duplication.json +21 -21
  144. package/md_cg/mreview/rules/field_coverage.json +54 -54
  145. package/md_cg/mreview/rules/source_license.json +21 -21
  146. package/md_cg/mreview/rules/template_flow.json +21 -21
  147. package/md_cg/mreview/ruleset.py +252 -252
  148. package/md_cg/nodefile.py +575 -575
  149. package/md_cg/pooling.py +484 -472
  150. package/md_cg/postings.py +298 -298
  151. package/md_cg/predict.py +1100 -1100
  152. package/md_cg/progressive.py +123 -123
  153. package/md_cg/protect.py +272 -272
  154. package/md_cg/protocol/md_cg_gate.proto +33 -33
  155. package/md_cg/protocol.py +372 -372
  156. package/md_cg/provenance.py +582 -582
  157. package/md_cg/reach.py +453 -453
  158. package/md_cg/readcache.py +85 -0
  159. package/md_cg/refindex.py +833 -833
  160. package/md_cg/refine.py +604 -604
  161. package/md_cg/roleviews.py +89 -89
  162. package/md_cg/routing.py +365 -365
  163. package/md_cg/scrub.py +852 -852
  164. package/md_cg/security.py +274 -274
  165. package/md_cg/self_state.py +1029 -1029
  166. package/md_cg/selfreport.py +151 -151
  167. package/md_cg/semantic/__init__.py +10 -10
  168. package/md_cg/semantic/canonical.py +122 -122
  169. package/md_cg/semantic/en_normalizer.py +364 -364
  170. package/md_cg/semantic/en_zh_map.json +28694 -0
  171. package/md_cg/semantic/export_en_zh_map.py +64 -0
  172. package/md_cg/semantic/unify.py +45 -0
  173. package/md_cg/semantic/zh_en_atoms.py +139 -139
  174. package/md_cg/signer.py +562 -562
  175. package/md_cg/sources.py +815 -582
  176. package/md_cg/statushdr.py +179 -179
  177. package/md_cg/stg.py +48 -37
  178. package/md_cg/subgraph.py +729 -729
  179. package/md_cg/sustain.py +1138 -1138
  180. package/md_cg/tasks.py +470 -470
  181. package/md_cg/test_action_derive.py +203 -203
  182. package/md_cg/test_audit_rotate.py +270 -270
  183. package/md_cg/test_autonomy.py +143 -143
  184. package/md_cg/test_bench_governance.py +102 -102
  185. package/md_cg/test_blindspot_tickets.py +166 -166
  186. package/md_cg/test_branches.py +249 -249
  187. package/md_cg/test_ccg_perturb.py +184 -184
  188. package/md_cg/test_ccgc.py +433 -433
  189. package/md_cg/test_census_prune.py +81 -81
  190. package/md_cg/test_cond_compose_anchors.py +76 -76
  191. package/md_cg/test_cond_match.py +165 -165
  192. package/md_cg/test_condition_anchor.py +81 -81
  193. package/md_cg/test_d_meta.py +412 -412
  194. package/md_cg/test_datapath_root.py +199 -199
  195. package/md_cg/test_en_pipeline.py +166 -166
  196. package/md_cg/test_gain_gate.py +212 -212
  197. package/md_cg/test_health_scale.py +173 -173
  198. package/md_cg/test_hive_ingest.py +285 -0
  199. package/md_cg/test_hot_cold.py +215 -215
  200. package/md_cg/test_hyperedge.py +245 -245
  201. package/md_cg/test_i26_empty_first_write.py +116 -0
  202. package/md_cg/test_i27_e041_identity.py +128 -0
  203. package/md_cg/test_i28_hotcache_prodpath.py +122 -0
  204. package/md_cg/test_identity_attribution.py +147 -147
  205. package/md_cg/test_index_durability.py +224 -224
  206. package/md_cg/test_interop.py +93 -0
  207. package/md_cg/test_lifecycle.py +309 -309
  208. package/md_cg/test_linkref.py +306 -306
  209. package/md_cg/test_lock.py +43 -43
  210. package/md_cg/test_md_access_parity.py +255 -255
  211. package/md_cg/test_md_writepath.py +345 -345
  212. package/md_cg/test_mdstore_search_parity.py +160 -0
  213. package/md_cg/test_mr_m2.py +587 -587
  214. package/md_cg/test_mr_m3.py +710 -710
  215. package/md_cg/test_mr_m4.py +485 -485
  216. package/md_cg/test_p0.py +250 -250
  217. package/md_cg/test_p1.py +316 -316
  218. package/md_cg/test_p10_identity.py +173 -173
  219. package/md_cg/test_p11_consistency.py +233 -233
  220. package/md_cg/test_p12_metacognition.py +212 -212
  221. package/md_cg/test_p13_encryption.py +241 -241
  222. package/md_cg/test_p14_sustain.py +249 -249
  223. package/md_cg/test_p15_scrub.py +280 -280
  224. package/md_cg/test_p16_self_state.py +301 -301
  225. package/md_cg/test_p17_predict.py +354 -354
  226. package/md_cg/test_p18_whitebox.py +171 -171
  227. package/md_cg/test_p19_migrate_roleplay.py +149 -149
  228. package/md_cg/test_p20_evolution.py +315 -315
  229. package/md_cg/test_p21_tokens.py +293 -270
  230. package/md_cg/test_p22_theory.py +175 -175
  231. package/md_cg/test_p23_links.py +311 -311
  232. package/md_cg/test_p24_evidence.py +227 -227
  233. package/md_cg/test_p25_weights.py +156 -156
  234. package/md_cg/test_p26_refindex.py +416 -416
  235. package/md_cg/test_p27_docindex.py +765 -765
  236. package/md_cg/test_p28_refcheck.py +305 -305
  237. package/md_cg/test_p29_session_ingest_export.py +354 -333
  238. package/md_cg/test_p3.py +11 -2
  239. package/md_cg/test_p30_maintain.py +330 -330
  240. package/md_cg/test_p31_insight.py +534 -534
  241. package/md_cg/test_p32_backfill.py +298 -298
  242. package/md_cg/test_p33_ccg_wiring.py +293 -293
  243. package/md_cg/test_p34_crosscheck.py +331 -331
  244. package/md_cg/test_p35_conditioned_claim.py +252 -252
  245. package/md_cg/test_p36_kp_align.py +230 -230
  246. package/md_cg/test_p37_condition_space.py +248 -248
  247. package/md_cg/test_p38_concurrent_flush.py +102 -0
  248. package/md_cg/test_p38_contextualize.py +273 -273
  249. package/md_cg/test_p39_verify_flow.py +113 -0
  250. package/md_cg/test_p39_vision_evidence.py +369 -369
  251. package/md_cg/test_p40_refine_worklist.py +241 -241
  252. package/md_cg/test_p41_evolve_patrol.py +224 -224
  253. package/md_cg/test_p42_provenance.py +269 -269
  254. package/md_cg/test_p43_pooling.py +412 -398
  255. package/md_cg/test_p44_md_whitebox.py +231 -231
  256. package/md_cg/test_p45_session_identity.py +219 -219
  257. package/md_cg/test_p46_unit_scope.py +272 -272
  258. package/md_cg/test_p47_session_view.py +281 -0
  259. package/md_cg/test_p4_fuzzy.py +223 -223
  260. package/md_cg/test_p5_semantic.py +226 -226
  261. package/md_cg/test_p6_consolidate.py +440 -387
  262. package/md_cg/test_p7_goals_recent.py +202 -202
  263. package/md_cg/test_p8_subgraph_chain.py +200 -200
  264. package/md_cg/test_p9_forget_protect.py +231 -231
  265. package/md_cg/test_predict_beta.py +135 -135
  266. package/md_cg/test_preflight_failclosed.py +100 -100
  267. package/md_cg/test_progressive.py +146 -146
  268. package/md_cg/test_protocol.py +243 -243
  269. package/md_cg/test_reach.py +378 -378
  270. package/md_cg/test_reach_keys.py +201 -201
  271. package/md_cg/test_read_clip.py +141 -141
  272. package/md_cg/test_readcache_prodpath.py +155 -0
  273. package/md_cg/test_retr_gates_prodpath.py +140 -0
  274. package/md_cg/test_retr_s1.py +340 -340
  275. package/md_cg/test_retr_s1b.py +209 -209
  276. package/md_cg/test_retr_s3.py +194 -194
  277. package/md_cg/test_retr_s4.py +163 -163
  278. package/md_cg/test_retr_s5.py +200 -200
  279. package/md_cg/test_retr_s6.py +157 -157
  280. package/md_cg/test_retr_s7.py +384 -384
  281. package/md_cg/test_retr_s8_time.py +369 -316
  282. package/md_cg/test_retr_s9_edges.py +286 -286
  283. package/md_cg/test_retr_s9_entity_ctx.py +175 -175
  284. package/md_cg/test_review_conformance.py +367 -367
  285. package/md_cg/test_role_views.py +354 -354
  286. package/md_cg/test_sem_noise.py +242 -242
  287. package/md_cg/test_semantic_canonical.py +241 -241
  288. package/md_cg/test_subproc_encoding.py +192 -192
  289. package/md_cg/test_sustain_mutual.py +153 -153
  290. package/md_cg/test_tasks.py +409 -409
  291. package/md_cg/test_tool_face.py +189 -189
  292. package/md_cg/test_transfer.py +180 -180
  293. package/md_cg/test_trust.py +361 -361
  294. package/md_cg/test_twophase.py +286 -286
  295. package/md_cg/test_v14_fixes.py +397 -397
  296. package/md_cg/test_validity_filter.py +280 -280
  297. package/md_cg/test_verify_answer.py +138 -138
  298. package/md_cg/test_wisdom_md_store.py +292 -292
  299. package/md_cg/test_writelimit.py +197 -197
  300. package/md_cg/test_writepipe.py +214 -214
  301. package/md_cg/theory.py +273 -273
  302. package/md_cg/tokens.py +677 -663
  303. package/md_cg/tool_face.py +260 -260
  304. package/md_cg/trust.py +986 -950
  305. package/md_cg/twophase.py +231 -231
  306. package/md_cg/units.py +667 -667
  307. package/md_cg/vision_evidence.py +666 -666
  308. package/md_cg/weights.py +624 -624
  309. package/md_cg/whitebox.py +527 -527
  310. package/md_cg/whitebox_kb/__init__.py +37 -37
  311. package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
  312. package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
  313. package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
  314. package/md_cg/whitebox_kb/engine.py +310 -310
  315. package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  316. package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
  317. package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
  318. package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
  319. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  320. package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
  321. package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
  322. package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
  323. package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
  324. package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
  325. package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
  326. package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
  327. package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
  328. package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
  329. package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
  330. package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
  331. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
  332. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
  333. package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
  334. package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
  335. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
  336. package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
  337. package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
  338. package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
  339. package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
  340. package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
  341. package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
  342. package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
  343. package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
  344. package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
  345. package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
  346. package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
  347. package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
  348. package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
  349. package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
  350. package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
  351. package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
  352. package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
  353. package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
  354. package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
  355. package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
  356. package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
  357. package/md_cg/writelimit.py +356 -356
  358. package/md_cg/writepipe.py +550 -542
  359. package/package.json +97 -96
  360. package/skills/plugin.json +54 -54
  361. package/skills/skills/designer-perspective/SKILL.md +158 -158
  362. package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
  363. package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
  364. package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
  365. package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
  366. package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
  367. package/skills/skills/designer-perspective/scripts/designer.py +545 -545
  368. package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
  369. package/skills/skills/designer-perspective/tests/selftest.py +61 -61
  370. package/skills/skills/lingshu-browser/SKILL.md +60 -60
  371. package/skills/skills/lingshu-compiler/SKILL.md +56 -56
  372. package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
  373. package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
  374. package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
  375. package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
  376. package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
  377. package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
  378. package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
  379. package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
  380. package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
  381. package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
  382. package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
  383. package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
  384. package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
  385. package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
  386. package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
  387. package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
  388. package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
  389. package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
  390. package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
  391. package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
  392. package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
  393. package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
  394. package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
  395. package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
  396. package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
  397. package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
  398. package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
  399. package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
  400. package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
  401. package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
  402. package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
  403. package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
  404. package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
  405. package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
  406. package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
  407. package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
  408. package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
  409. package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
  410. package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
  411. package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
  412. package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
  413. package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
  414. package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
  415. package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
  416. package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
  417. package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
  418. package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
  419. package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
  420. package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
  421. package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
  422. package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
  423. package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
  424. package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
  425. package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
  426. package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
  427. package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
  428. package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
  429. package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
  430. package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
  431. package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
  432. package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
  433. package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
  434. package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
  435. package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
  436. package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
  437. package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
  438. package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
  439. package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
  440. package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
  441. package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
  442. package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
  443. package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
  444. package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
  445. package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
  446. package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
  447. package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
  448. package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
  449. package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
  450. package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
  451. package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
  452. package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
  453. package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
  454. package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
  455. package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
  456. package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
  457. package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
  458. package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
  459. package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
  460. package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
  461. package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
  462. package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
  463. package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
  464. package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
  465. package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
  466. package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
  467. package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
  468. package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
  469. package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
  470. package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
  471. package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
  472. package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
  473. package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
  474. package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
  475. package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
  476. package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
  477. package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
  478. package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
  479. package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
  480. package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
  481. package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
  482. package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
  483. package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
  484. package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
  485. package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
  486. package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
  487. package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
  488. package/skills/skills/lingshu-graph/SKILL.md +63 -63
  489. package/skills/skills/lingshu-net/SKILL.md +48 -48
  490. package/skills/skills/lingshu-os/SKILL.md +64 -64
  491. package/skills/skills/lingshu-pylang/SKILL.md +71 -71
  492. package/src/bridge.ts +401 -401
  493. package/src/hooks.ts +38 -2
  494. package/src/lib/datapath.ts +326 -326
  495. package/src/lib/mdcg_client.ts +413 -413
  496. package/src/lib/mutual.ts +428 -428
  497. package/src/lib/prompt_safety.ts +62 -62
  498. package/src/lib/python_path.ts +71 -71
  499. package/src/lib/roleplay_web.ts +932 -932
  500. package/src/lib/token_store.ts +192 -192
  501. package/src/tools.ts +212 -212
  502. package/zcode/AGENTS.md +11 -3
  503. package/zcode/README.md +41 -41
  504. /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