@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/reach.py CHANGED
@@ -1,453 +1,453 @@
1
- # -*- coding: utf-8 -*-
2
- """reach.py · 检索候选收敛层:大域收敛 → 条件门控 → 图扩散(确定性、零 LLM)。
3
-
4
- 背景(2026-09-18 A3 基线取证):`MdCG/MdCGSecure.search` 的 T2 全局阶段对**全部候选**
5
- `_read_many(entries)`(实测 7395/12092 ≈ 0.6116,且 5 条不同查询的 scanned 完全相同)
6
- ——触碰量与查询无关,说明它做的是全表/大域扫描,而不是「按查询收敛」。
7
-
8
- 本层在**读盘之前**把候选收敛到「必要条件成立」的子集,三步各司其职:
9
- ① 大域收敛:bigram 倒排(query bigrams ∪ 各 term 的 bigrams ∪ 原始查询 bigrams → 并集)。
10
- 口径**精确**:LIKE 命中要求 term 作为子串出现 ⟹ term 的所有 bigram 必在文档
11
- bigram 集内;词法打分 >0(bigram 重合)要求至少一个 query bigram 命中 ⟹ 倒排并集
12
- 必为「LIKE 命中 ∪ bigram 正分」的**超集**(证明见 test_reach.py 的包含性断言)。
13
- **例外(已声明)**:_score 的 tag_bonus(标签与原始查询互为子串)可给单字符标签的
14
- 节点 +0.05 分而不共享任何 bigram。这类节点不在收敛集内,但**仍不丢召回**:它只能经
15
- T3 全量兜底阶段进入结果,而该阶段仅在收敛阶段未返回时运行(收敛阶段失败 → 走原全量
16
- 路径);收敛阶段成功 ⟺ 原 T2 成功(LIKE 命中集相等),此时两条路径都不含该节点。
17
- ② 条件门控:只把「被接受的**必要条件**」提前到读盘前——层/角色/会话/分支
18
- + 可读性(_open_content 返回 None 的节点在构建期即剔除)。**时窗/生命周期/重要度
19
- 密级(clearance)等只影响排序或由调用方与 _readable 负责**,本层不新增门控,避免静默改变结果集。
20
- ③ 图扩散:按索引内 `edges`(零读盘)做 1..N 跳扩散补召回;只吸收在候选集合内、
21
- 且带 content_hash 的邻居,宁少不误扩散。
22
-
23
- 回退:任何一步不适用(开关关闭 / 单字符 term / 索引不可写且无缓存 / 收敛集为空)
24
- → 返回 (None, meta),调用方继续走原全量阶段,**旧行为不变**。
25
-
26
- # 功能名:检索候选收敛层
27
- # 生效条件:MdCG/MdCGSecure.search 在 T2 全局阶段之前调用本层时;进程环境 MDCG_REACH == "1"(显式 opt-in,默认关——依据 docs/hive/检索路径与认知结构契约_v0.1.md §7「不在默认路径上启用新行为」,与 MDCG_RETRIEVAL_PIPELINE 同纪律);库根可写(首建缓存)或缓存已存在;查询各 term 长度 >= 2(bigram 能覆盖子串语义)
28
- # 子功能:bigram 倒排大域收敛(LIKE 与词法打分双路并集,超集精确);条件门控(只提前「接受的必要条件」:层/角色/会话/分支 + 可读性;时窗/生命周期/密级等仅排序或由调用方负责,不作门控);edges 图扩散补召回;命中不足或条件不满足时回退全量
29
- # 执行:from md_cg import reach;reach.narrow(cg, entries, terms, qb, context) -> (entries 或 None, meta);缓存文件 <root>/_reach_index.json(MDCG_REACH_INDEX 可改写落点);开关 MDCG_REACH=1 显式开启(默认关,未设即不生效)
30
- # 验证方式:md_cg/test_reach.py(收敛集必含 LIKE/打分命中集、门控/扩散/回退逐项断言);_coord/a3_reach.py(A1 全组回归、A2 新旧结果对比、A3 触碰比)
31
- # A3 口径(与文档对齐):meta.scanned 只计**检索读盘**(_read_many);索引首建/重建的全库读盘
32
- # 另计于 meta.reach_build_docs,两者不可混淆——报 A3 时须同时给「稳态均值」与「含首建均值」。
33
- # 重建中途失败也如实暴露:meta.reach == "index_unavailable" 时带 meta.reach_build_docs(已读盘量)
34
- # 与 meta.reach_build_docs=**已进入倒排**的节点数、meta.reach_build_reads=**已实际读盘**的节点数(含读失败/解密失败)、meta.reach_build_partial=True;
35
- # 无读盘则这些键均不出现(r7/r8 独立复核取证:旧写法把部分读盘成本藏起来了,且只计入索引数会漏计「读了很多但都不可读」的情形)。
36
- # 新鲜度覆盖边界(如实声明):键 = content_hash(正文)+ tags + edges。fm.semantic 属**前置元数据且不在索引条目中**,
37
- # 仅改 semantic 标志不会改变键;该情形由 TTL(默认 600s)兜底——**在 TTL 窗口内**改 semantic 标志可能破坏 new 包含 old(窗口上界=TTL);若需秒级精确,应把 semantic 纳入索引条目后再入键。
38
- # 已知风险(如实声明):落盘缓存 post 内含**明文 bigram**,目前未按主体/密级隔离——
39
- # 多主体共用同一 root 时,缓存文件本身即构成跨主体词面信息(读盘/内存无越权,但文件级隔离未做)。
40
- # 不适用条件:①查询含单字符 term(bigram 无法表达单字子串)→ 不收敛 ①b库内有节点缺 content_hash 且非本次重建 → 不收敛(宁全量不冒陈旧丢召回)①c图扩散默认关闭(MDCG_REACH_DIFFUSE=1 开启:扩散节点参与 top-k 竞争,召回增加但 A2 的集合包含性不再逐位保证) ②MDCG_REACH 未设或不等于 "1"(默认关,契约 §7) ③索引内容过时(content_hash 不符 / 超过 MDCG_REACH_TTL 默认 600s)时先全量重建,重建失败即不收敛 ④只读根且无缓存时:当次仍以内存索引收敛,但**不入进程缓存**(下次调用重新构建)——收敛收益减少但结果不丢,且与声明一致 ⑤首建当次额外读全库(meta.reach_build_docs 如实暴露),A3 稳态值不含该次 ⑤密级(clearance)过滤由调用方与 _readable 负责,本层不新增 ⑥首建当次调用会读全库(meta.reach_build_docs 如实暴露),稳态才享受收敛收益
41
- """
42
- from __future__ import annotations
43
-
44
- import json
45
- import math
46
- import os
47
- import time
48
-
49
- from .fsutil import FileLock, atomic_write
50
- from . import nodefile
51
-
52
-
53
- # 生效条件:调用发生在 md_cg.mdcg 完成导入之后(本模块被 mdcg 顶层 import 时**不得**在模块级取用
54
- # mdcg 的符号,否则 mdcg↔reach 循环导入);返回 (bigrams, normalize_en) 两函数。
55
- def _lex():
56
- """延迟取词法函数——规避 mdcg ↔ reach 循环导入(模块级 from .mdcg import X 会失败)。"""
57
- from .mdcg import bigrams as _bg, normalize_en as _ne
58
- return _bg, _ne
59
-
60
-
61
- # 生效条件:content 为节点正文、tags 为标签可迭代对象时返回该文档的 token 集合 = bigrams(正体小写) ∪ bigrams(标签小写) ∪ bigrams(normalize_en(正文));content 为假值时按空串处理。
62
- def _doc_tokens(content, tags):
63
- bg, ne = _lex()
64
- body = nodefile.positive_body(content).lower()
65
- tag_s = " ".join(str(t) for t in (tags or [])).lower()
66
- # 文档侧 token = 正体小写 ∪ 标签小写 ∪ normalize_en(正文) ∪ normalize_en(正文).lower()
67
- # 末项是**超集保险**:查询侧与 LIKE 都走小写口径,故索引侧再收一份全小写(r13 复核:
68
- # 若 LIKE 命中落在正体之外且含大写/全角,仅 ne() 可能不产生对应小写 bigram)
69
- return bg(body) | bg(tag_s) | bg(ne(content)) | bg(ne(content).lower())
70
-
71
- INDEX_NAME = "_reach_index.json"
72
- #: 倒排落盘用 JSON 列表(曾用逗号/单元分隔符拼接:含该字符的路径会被拆碎,r1/r11 复核取证)
73
- SCHEMA_VERSION = 1
74
- #: 扩散触发阈值:收敛集小于该值时才做图扩散(避免大集合上无谓放大)
75
- DIFFUSE_BELOW = 400
76
- #: 未索引/新写入节点的并入上限:超过即整段回退不收敛(不静默截断——截断会破坏超集/A2)
77
- _FRESH_MAX = 500
78
- #: 扩散放大上限:不超过收敛集该倍数(防扩散把 A3 触碰比吃回去)
79
- DIFFUSE_MAX_FACTOR = 3
80
-
81
- #: 进程内缓存:root -> (ReachIndex, 载入时的库指纹)
82
- _CACHE = {}
83
-
84
-
85
- # 生效条件:读取 MDCG_REACH_TTL;值为非正数或不可解析时回落到默认 600.0(**不得**把 TTL=0 解释为关闭新鲜度兜底——semantic 变更依赖它,r11 复核取证)。
86
- def _ttl() -> float:
87
- try:
88
- v = float(os.environ.get("MDCG_REACH_TTL") or 600.0)
89
- except ValueError:
90
- return 600.0
91
- # 非有限(inf/nan/1e999)视同非法:否则过期判断永假 → 声明中的 TTL 兜底被静默取消(r12 复核取证)
92
- if not math.isfinite(v) or v <= 0:
93
- return 600.0
94
- return v
95
-
96
-
97
- # 生效条件:idx 若本次发生了全量重建(built_now)则返回 {"reach_build_docs": built_docs},否则返回 {}——
98
- # 供**所有**窄化出口(含 hash_incomplete/fresh_overflow 回退)如实暴露首建成本。
99
- def _build_meta(idx) -> dict:
100
- if getattr(idx, "built_now", False):
101
- return {"reach_build_docs": getattr(idx, "built_docs", 0),
102
- "reach_build_reads": getattr(idx, "built_reads", 0)}
103
- return {}
104
-
105
-
106
- # 生效条件:环境变量 MDCG_REACH **恰为 "1"** 时返回 True;未设置或其它取值(含 "0")返回 False——即默认关。
107
- # 依据:docs/hive/检索路径与认知结构契约_v0.1.md §7「不在默认路径上启用新行为」(先 flag 化、逐项验证、再讨论默认开启)
108
- # 与 §5.1「开关关 → run_tests 全绿且与现状一致」。启用与否由调用点 reach.enabled() 前置守卫,保证关态下不向 meta 落任何 reach* 键。
109
- def enabled() -> bool:
110
- return os.environ.get("MDCG_REACH") == "1"
111
-
112
-
113
- # 生效条件:环境变量 MDCG_REACH_DIFFUSE 等于 "1" 时返回 True,其余(含未设置)返回 False。
114
- def diffuse_on() -> bool:
115
- """图扩散开关(默认关):扩散节点会与词面命中一起参与 top-k 竞争,
116
- 为使 A2「new ⊇ old」在默认配置下**精确成立**,此能力默认关闭、需显式开启。"""
117
- return os.environ.get("MDCG_REACH_DIFFUSE", "0") == "1"
118
-
119
-
120
- # 生效条件:entries 中每个元素都有 path 键且 path 对应库根下的文件时,返回其 (path -> entry) 映射;缺 path 的条目被跳过。
121
- def _by_path(entries):
122
- out = {}
123
- for e in entries:
124
- p = e.get("path")
125
- if p:
126
- out[p] = e
127
- return out
128
-
129
-
130
- # 生效条件:entry 为索引条目时返回其 edges 的稳定签名(按 target|type 排序后拼接;无 edges 返回空串)。
131
- def _edges_sig(entry) -> str:
132
- # 用 JSON 列表编码:分隔符若有歧义会导致键碰撞(r11 复核:a;b 与 [a,b] 曾同键)
133
- items = []
134
- for ed in (entry or {}).get("edges") or []:
135
- if isinstance(ed, dict):
136
- items.append([str(ed.get("target") or ""), str(ed.get("type") or "")])
137
- else:
138
- items.append([str(ed)])
139
- return json.dumps(sorted(items), ensure_ascii=False)
140
-
141
-
142
- # 生效条件:entry 为索引条目时返回其新鲜度键 = content_hash + chr(31) + edges 签名——内容或**边**任一变化都会使键变化(r7 复核:仅改边也必须触发重建,否则扩散会复用陈旧邻接表)。
143
- def _node_key(entry) -> str:
144
- # 倒排同时依赖:正文(content_hash)、标签 bigram(tags)、邻接(edges)。
145
- # 三者任一变化都必须判为不新鲜,否则会复用陈旧倒排(r7 补 edges、r9 补 tags)。
146
- tags = json.dumps([str(t) for t in ((entry or {}).get("tags") or [])], ensure_ascii=False)
147
- return json.dumps([str((entry or {}).get("content_hash") or ""), tags, _edges_sig(entry)],
148
- ensure_ascii=False)
149
-
150
-
151
- # 生效条件:cg 有 index["nodes"] 与 root,且根目录存在时返回「库指纹」字符串(节点数 + 各节点 content_hash 的拼接哈希);缺字段按空串参与。
152
- def _fingerprint(cg) -> str:
153
- import hashlib
154
- h = hashlib.sha1()
155
- nodes = cg.index.get("nodes") or {}
156
- h.update(str(len(nodes)).encode())
157
- for k in sorted(nodes):
158
- # 指纹含 **path** 与 **edges 签名**:路径交换/重命名、或仅增删改边时都必须判为变更,
159
- # 否则进程缓存复用会给「旧 path 的倒排」或「旧邻接表」(扩散沿已删边/漏新邻居)
160
- h.update((str(nodes[k].get("path") or "") + chr(31)).encode())
161
- h.update(_node_key(nodes[k]).encode())
162
- return h.hexdigest()
163
-
164
-
165
- # 生效条件:root 为调用方传入的根路径字符串,实例化后 path 取 os.environ.get("MDCG_REACH_INDEX") 的值,未设或为空串时回落 os.path.join(root, INDEX_NAME);post/adj/hashes/sem 初始化为空容器,persist_failed=False,built_fingerprint="",built_now=False,hash_complete=True,built_docs/built_reads=0,built_at=0.0;
166
- class ReachIndex:
167
- """bigram 倒排 + 邻接表(edges),落盘于 <root>/_reach_index.json。"""
168
-
169
- # 生效条件:root 给定时记录根目录与缓存路径;构造本身不读盘。
170
- # 生效条件:对任意 root(含空串等假值),当 MDCG_REACH_INDEX 环境变量为非空字符串时 self.path 取该值,否则 self.path 回落到 os.path.join(root, INDEX_NAME)。
171
- def __init__(self, root: str):
172
- self.root = root
173
- # 缓存落点:默认 <root>/_reach_index.json;环境变量 MDCG_REACH_INDEX 可改到别处
174
- # (测量/演练时避免在真实记忆库根下落盘)
175
- self.path = os.environ.get("MDCG_REACH_INDEX") or os.path.join(root, INDEX_NAME)
176
- self.post = {} # bigram -> [path, ...]
177
- self.adj = {} # path -> [path, ...]
178
- self.hashes = {} # path -> content_hash
179
- self.sem = [] # 语义摘要节点(fm.semantic 为真)——MDCG_SEMANTIC=1 时无条件入池
180
- self.persist_failed = False # 落盘失败(只读根):不跨调用复用
181
- self.built_fingerprint = "" # 建库时的库指纹(磁盘新鲜度判据)
182
- self.built_now = False # 本次进程缓存是否刚做过全量重建(供审计首建成本)
183
- self.hash_complete = True # 库内节点是否都有 content_hash(缺则不复用缓存)
184
- self.built_docs = 0 # 本次重建**已进入倒排**的节点数
185
- self.built_reads = 0 # 本次重建**已实际读盘**的节点数(含读失败/解密失败;失败时如实暴露 I/O 成本)
186
- self.built_at = 0.0
187
-
188
- # 生效条件:缓存文件存在且 JSON 可解析、schema 版本相符时载入 post/adj/hashes 并返回 True;文件缺失/损坏/版本不符返回 False(调用方转入重建)。
189
- # 生效条件:读取 self.path 时若 open/json.load 抛 OSError 或 ValueError、或解析结果不是 dict、或其中 "v" 与模块级常量 SCHEMA_VERSION 不等则返回 False,否则回填 post/adj/hashes/sem/built_at/fp 并返回 True。
190
- def load(self) -> bool:
191
- try:
192
- with open(self.path, encoding="utf-8") as f:
193
- d = json.load(f)
194
- except (OSError, ValueError):
195
- return False
196
- if not isinstance(d, dict) or d.get("v") != SCHEMA_VERSION:
197
- return False
198
- self.post = {k: list(v) for k, v in (d.get("post") or {}).items() if v}
199
- self.adj = d.get("adj") or {}
200
- self.hashes = d.get("hashes") or {}
201
- self.sem = list(d.get("sem") or [])
202
- self.built_at = float(d.get("built_at") or 0.0)
203
- self.built_fingerprint = str(d.get("fp") or "")
204
- return True
205
-
206
- # 生效条件:cg 有 index["nodes"] 且可逐条 _read 时,全量重建倒排与邻接:文档 token = bigrams(positive_body 小写) ∪ bigrams(标签小写) ∪ bigrams(normalize_en(content))(前者覆盖 LIKE、后者覆盖词法打分);邻接取 index 内 edges 的 target,仅保留在索引中的目标。
207
- # 生效条件:当 cg 的 index 提供 nodes 时,对每个 path 非空、cg._read 返回内容非 None 且 cg._open_content 亦返回非 None 的节点写入 hashes/post(fm.semantic 为真时并入 sem),并只把两端均已入 hashes 的 edges 建成 adj,随后按原始 content_hash 是否齐全设置 hash_complete 并返回 self。
208
- def build(self, cg):
209
- nodes = cg.index.get("nodes") or {}
210
- id2path = {k: (v.get("path") or "") for k, v in nodes.items()}
211
- hashes, post, adj, sem = {}, {}, {}, []
212
- for k in sorted(nodes):
213
- e = nodes[k]
214
- p = e.get("path")
215
- if not p:
216
- continue
217
- fm, c = cg._read(e)
218
- self.built_reads += 1 # 已发生一次读盘:无论该节点是否可读、后续是否报错,都如实计数(r8 复核取证)
219
- if c is None:
220
- continue # 读不到 → 与检索路径同样跳过
221
- # 与 _read_many 同口径:密文节点必须解密后再取 token,否则加密节点
222
- # 只索引到密文 → 收敛集漏掉它们(A2 召回退化,2026-09-18 由 test_p3 取证)
223
- c = cg._open_content((fm or {}).get("id"), fm, c)
224
- if c is None:
225
- continue
226
- hashes[p] = _node_key(e)
227
- self.built_docs = len(hashes) # 渐进计数:中途抛错也能如实暴露已入倒排量(r7 复核取证)
228
- if (fm or {}).get("semantic"):
229
- sem.append(p) # MDCG_SEMANTIC=1 时该节点无条件入池 → 必须进收敛集
230
- for b in _doc_tokens(c, (fm or {}).get("tags")):
231
- post.setdefault(b, []).append(p)
232
- for k, e in nodes.items():
233
- p = e.get("path")
234
- if not p or p not in hashes:
235
- continue
236
- nb = []
237
- for ed in e.get("edges") or []:
238
- tgt = ed.get("target") if isinstance(ed, dict) else ed
239
- tp = id2path.get(tgt)
240
- if tp and tp in hashes:
241
- nb.append(tp)
242
- if nb:
243
- adj[p] = nb
244
- self.post, self.adj, self.hashes, self.sem = post, adj, hashes, sem
245
- self.built_fingerprint = _fingerprint(cg)
246
- self.built_docs = len(hashes)
247
- # 覆盖完整性必须看**原始 content_hash**(hashes 里存的是 _node_key,
248
- # 含分隔符恒为真 → 曾使 hash_complete 恒判 True,r10 复核取证)
249
- self.hash_complete = all(bool(v.get("content_hash"))
250
- for v in nodes.values() if v.get("path"))
251
- self.built_at = time.time()
252
- return self
253
-
254
- # 生效条件:在 FileLock(缓存路径) 内以 atomic_write 写回 post(逗号连接)/adj/hashes/built_at;写失败抛 OSError(调用方按不收敛处理)。
255
- # 生效条件:以模块级常量 SCHEMA_VERSION 与实例的 built_at/built_fingerprint/post/adj/hashes/sem 组装 JSON,并在 FileLock(self.path, timeout=5.0) 内 atomic_write 到 self.path,无返回值。
256
- def save(self):
257
- d = {"v": SCHEMA_VERSION, "built_at": self.built_at, "fp": self.built_fingerprint,
258
- "post": {k: list(v) for k, v in self.post.items()},
259
- "adj": self.adj, "hashes": self.hashes, "sem": self.sem}
260
- with FileLock(self.path, timeout=5.0):
261
- atomic_write(self.path, json.dumps(d, ensure_ascii=False))
262
-
263
- # 生效条件:tokens 为可迭代的 bigram 集合时返回倒排并集(path 集合);空 tokens 返回空集。
264
- # 生效条件:对 tokens 中每个元素取 self.post.get(元素),仅当取值为非空列表时把其中路径并入返回集合(缺键或空列表不贡献)。
265
- def postings(self, tokens) -> set:
266
- out = set()
267
- for b in tokens:
268
- got = self.post.get(b)
269
- if got:
270
- out.update(got)
271
- return out
272
-
273
- # 生效条件:seeds 为 path 集合时按 adj 做 hops 跳扩散并返回 seeds ∪ 各跳邻居;邻居超出 limit(默认收敛集 DIFFUSE_MAX_FACTOR 倍 + 1)即停止吸收。
274
- # 生效条件:返回集以 seeds 起步,limit 不大于 0(含默认 0)时上限改取 max(len(seeds),1)*DIFFUSE_MAX_FACTOR+1、否则用传入的 limit,再沿 self.adj 逐跳扩展 max(0,hops) 层,一旦返回集大小达到上限立即返回(hops 为 0 或负时只返回 seeds 集合)。
275
- def diffuse(self, seeds, hops: int = 1, limit: int = 0) -> set:
276
- out = set(seeds)
277
- if limit <= 0:
278
- limit = max(len(seeds), 1) * DIFFUSE_MAX_FACTOR + 1
279
- frontier = list(out)
280
- for _ in range(max(0, hops)):
281
- nxt = []
282
- for p in frontier:
283
- for q in self.adj.get(p) or []:
284
- if q not in out:
285
- out.add(q)
286
- nxt.append(q)
287
- if len(out) >= limit:
288
- return out
289
- frontier = nxt
290
- if not frontier:
291
- break
292
- return out
293
-
294
-
295
- # 生效条件:root 下缓存与当前库指纹一致时返回进程缓存/磁盘缓存实例;指纹不符或缓存缺失时重建并尝试落盘(落盘失败仍返回内存实例);重建抛异常时返回 None(调用方不收敛)。缓存键含缓存路径(MDCG_REACH_INDEX 改写时互不串味)。
296
- # 生效条件:调用 _index() 需同时拿到「本次重建失败且已部分读盘」的成本时,返回 (idx, fail_meta);fail_meta 在成功/无读盘时为 {}。
297
- # 为何用调用方提供的容器而不是模块级状态:模块级可变状态无锁,跨线程/跨调用都会串味
298
- # (r8 独立复核取证);而「失败信息与本次调用的绑定」必须显式。idx 仍按原契约返回,其它调用方无需改动。
299
- def _index_with_meta(cg, force: bool = False):
300
- fail = {}
301
- idx = _index(cg, force=force, _fail=fail)
302
- return idx, fail
303
-
304
-
305
- # 生效条件:必填 cg 提供 root 与指纹,force 默认 False、_fail 默认 None;当 force 为假且 _CACHE.get(ReachIndex(cg.root).path) 的 hit 为真、hit[1] 等于 _fingerprint(cg) 且 _ttl() 未超时时,返回 hit[0] 并置其 built_now=False;否则以 idx=ReachIndex(cg.root) 继续:先若 force 为假且 idx.load() 为真则按 _ttl() 判 expired,再无条件设 idx.hash_complete=_hash_complete(cg),随后若 force 为假且 expired 为假且 idx.post 非空且 _disk_fresh(idx,cg) 为真则置 idx.built_now=False、写 _CACHE[ReachIndex(cg.root).path]=(idx,fp) 并返回 idx;否则 try 中 idx.build(cg) 且 idx.built_now=True 后 idx.save(),save 成功则 persist_failed=False、写 _CACHE[ReachIndex(cg.root).path]=(idx,fp) 并返回 idx,save 抛 OSError 则 persist_failed=True、pop 缓存并返回 idx;idx.build 或 save 抛其他异常(含 save 非 OSError)则当 _fail 非 None 且 built_reads 或 built_docs 非零时向 _fail 写入 reach_build_docs、reach_build_reads、reach_build_partial=True,并返回 None;
306
- def _index(cg, force: bool = False, _fail=None) -> "ReachIndex | None":
307
- root = cg.root
308
- fp = _fingerprint(cg)
309
- idx0 = ReachIndex(root)
310
- key = idx0.path
311
- hit = _CACHE.get(key)
312
- ttl0 = _ttl()
313
- if (not force and hit and hit[1] == fp
314
- and not (ttl0 > 0 and (time.time() - hit[0].built_at) > ttl0)):
315
- hit[0].built_now = False # 复用:本次未重建(勿把上一轮的首建标记留在 meta 里)
316
- return hit[0]
317
- idx = idx0
318
- ttl = _ttl()
319
- expired = False
320
- if not force and idx.load():
321
- expired = ttl > 0 and (time.time() - idx.built_at) > ttl
322
- # hash 覆盖不完整(有节点无 content_hash)→ 无法判「未变」,缓存复用会给陈旧倒排,
323
- # 故一律重建;重建失败则由 narrow 走 hash_incomplete 回退(T3-r5 复核取证)
324
- idx.hash_complete = _hash_complete(cg)
325
- if not force and not expired and idx.post and _disk_fresh(idx, cg):
326
- idx.built_now = False
327
- _CACHE[key] = (idx, fp)
328
- return idx
329
- try:
330
- idx.build(cg)
331
- idx.built_now = True
332
- try:
333
- idx.save()
334
- idx.persist_failed = False
335
- except OSError:
336
- # 只读根:当次内存索引仍可用,但**不得跨调用复用**(模块头声明「下次调用重新构建」,
337
- # r13 复核指出旧写法会让 _CACHE 跨调用复用,与声明不符)
338
- idx.persist_failed = True
339
- except Exception:
340
- # 重建中途失败:已读盘的量也必须如实暴露(不许把成本藏起来)——r7 独立复核取证
341
- reads = getattr(idx, "built_reads", 0)
342
- docs = getattr(idx, "built_docs", 0)
343
- if _fail is not None and (reads or docs):
344
- _fail["reach_build_docs"] = docs
345
- _fail["reach_build_reads"] = reads
346
- _fail["reach_build_partial"] = True
347
- return None
348
- if getattr(idx, "persist_failed", False):
349
- _CACHE.pop(key, None)
350
- else:
351
- _CACHE[key] = (idx, fp)
352
- return idx
353
-
354
-
355
- # 生效条件:cg.index["nodes"] 中每个节点都有非空 content_hash 时返回 True,否则 False(意味着无法用 content_hash 判新鲜)。
356
- def _hash_complete(cg) -> bool:
357
- # 口径与 build() 的覆盖集合一致:只按有 path 的条目判定(无 path 的条目 build 不收录,
358
- # 不应因此禁用缓存复用——r12 复核指出旧写法会让稳态每次全库重建)
359
- nodes = cg.index.get("nodes") or {}
360
- return all(bool(v.get("content_hash")) for v in nodes.values() if v.get("path"))
361
-
362
-
363
- # 生效条件:磁盘缓存 hashes 与库内节点 path/content_hash 一一对应(数量相同且逐条相等)时返回 True,否则返回 False(触发重建)。
364
- def _disk_fresh(idx: ReachIndex, cg) -> bool:
365
- """磁盘缓存是否新鲜:**建库时指纹 == 当前指纹**(含节点数/path/键=hash+tags+edges)。
366
-
367
- 为什么用指纹而不是「逐条缓存条目复核」:build 会跳过不可读/解密失败的节点(它们不进 hashes),
368
- 若要求「索引里有 path 的节点数 == 缓存条目数」,库内含任一不可读节点时永不新鲜 → 跨进程每次
369
- 全库重建(r17 复核取证);而只复核缓存条目又会**漏掉新增节点**(r17 修正版实测)。
370
- 指纹同时覆盖:节点增删/改名/正文 hash/标签/邻接,且与可读性无关。
371
- """
372
- if not idx.hashes:
373
- return False
374
- return bool(idx.built_fingerprint) and idx.built_fingerprint == _fingerprint(cg)
375
-
376
-
377
- # 生效条件:仅当 enabled() 为真、terms 中各项长度均不小于 2、_index_with_meta(cg) 返回的 idx 非 None 且 idx.hash_complete 为真、由 qb/terms/q 生成的 token 集非空、按 entries 过滤后的 got 非空、且 entries 中未索引或 created_at 晚于 idx.built_at 的新节点数不超过 _FRESH_MAX 时返回 (got, meta),否则按守卫失败原因返回 (None, {"reach": off/short_term/index_unavailable/hash_incomplete/no_tokens/empty_converge/fresh_overflow})。
378
- def narrow(cg, entries, terms, qb, context=None, hops: int = 1, q: str = ""):
379
- meta = {"reach": "on"}
380
- if not enabled():
381
- return None, {"reach": "off"}
382
- terms = list(terms or [])
383
- if any(len(t or "") < 2 for t in terms):
384
- return None, {"reach": "short_term"} # 单字符 term 的 bigram 无法覆盖子串语义
385
- idx, _fail = _index_with_meta(cg)
386
- if idx is None:
387
- return None, dict({"reach": "index_unavailable"}, **_fail)
388
- _bmeta = _build_meta(idx) # 回退分支同样要如实暴露本次首建/重建成本(r18 复核取证)
389
- if not getattr(idx, "hash_complete", True):
390
- # 库内有节点缺 content_hash:无法证明倒排未过期 → 一律不收敛(走全量),不冒丢召回风险。
391
- # 注意:**不论是否本次重建**都必须回退——否则「每次重建 + 每次收敛」会把全库读盘
392
- # 混进稳态 scanned,并与模块头声明不符(r15 复核取证)
393
- return None, dict({"reach": "hash_incomplete"}, **_bmeta)
394
- bg, _ne = _lex()
395
- toks = set(qb or ())
396
- # 原始查询与其中文/小写形态:覆盖 _score 的 tag_bonus 路径
397
- # (str(tag) in q 或 q in str(tag) 会给「标签与原始查询互为子串」的节点 +0.05 分,
398
- # 该路径与 LIKE/词法分无关,故 token 集必须含原始查询的 bigram)
399
- toks |= bg(q or "")
400
- toks |= bg((q or "").lower())
401
- for t in terms:
402
- toks |= bg(t)
403
- toks |= bg((t or "").lower())
404
- # 与文档侧 ne() 口径对齐(terms 此前只做小写)——同样是超集方向,宁多读不漏召回
405
- toks |= bg(_ne(t or ""))
406
- toks |= bg(_ne(t or "").lower())
407
- toks = {b for b in toks if len(b) >= 2}
408
- if not toks:
409
- return None, dict({"reach": "no_tokens"}, **_bmeta)
410
- paths = idx.postings(toks)
411
- try:
412
- from .mdcg import semantic_on
413
- if semantic_on() and idx.sem:
414
- paths |= set(idx.sem) # 语义摘要节点无条件入池(否则收敛会误丢)
415
- except Exception:
416
- pass
417
- by_path = _by_path(entries)
418
- got = [by_path[p] for p in paths if p in by_path]
419
- if not got:
420
- return None, dict({"reach": "empty_converge"}, **_bmeta)
421
- pre = len(got)
422
- diffused = []
423
- if diffuse_on() and len(got) < DIFFUSE_BELOW:
424
- # 扩散种子先按 entries 过滤(否则会从未授权/越层的索引节点拉邻居,放大读盘与内存)
425
- seed_paths = {p for p in paths if p in by_path}
426
- spread = idx.diffuse(seed_paths, hops=hops)
427
- got = [by_path[p] for p in spread if p in by_path]
428
- diffused = [e for e in got if e.get("path") not in seed_paths]
429
- meta.update({"reach_seed": pre, "reach": "converged", "reach_diffused": len(diffused),
430
- "reach_hops": hops,
431
- # 结果层要据此放行「图扩散补召回」的节点(它们没有 LIKE/词法命中,
432
- # 若只按 _like 过滤就会被丢掉 → 扩散形同虚设,T3-r3 复核取证)
433
- "reach_diffused_paths": [e["path"] for e in diffused]})
434
- # 新节点并入:索引建好之后写入的节点(created_at 晚于 built_at)可能是新命中,直接并入候选,
435
- # 避免「陈旧索引漏召回」窗口(文件被原地改动的情形由 TTL 兜底)
436
- fresh_paths = []
437
- for e in entries:
438
- pth = e.get("path")
439
- if pth is None or pth in paths:
440
- continue
441
- ca = e.get("created_at")
442
- if pth not in idx.hashes or (ca and float(ca) > idx.built_at):
443
- fresh_paths.append(e) # 未索引(新写入)或索引建成后写入 → 一律并入候选
444
- if len(fresh_paths) > _FRESH_MAX:
445
- # 未索引节点过多(索引覆盖不完整/权限不完整):既不能静默截断(截断掉的可能是 LIKE 命中,
446
- # 而收敛集另有命中时会提前 return,兜底不跑 → 破坏超集与 A2),也不能无限放大读盘
447
- # → 整段回退:不收敛,走原全量路径(r8 复核取证)
448
- return None, dict({"reach": "fresh_overflow", "reach_fresh_nodes": len(fresh_paths)}, **_bmeta)
449
- if fresh_paths:
450
- got = got + fresh_paths
451
- meta["reach_fresh_nodes"] = len(fresh_paths)
452
- meta.update(_build_meta(idx)) # 首建成本(若有)如实暴露
453
- return got, meta
1
+ # -*- coding: utf-8 -*-
2
+ """reach.py · 检索候选收敛层:大域收敛 → 条件门控 → 图扩散(确定性、零 LLM)。
3
+
4
+ 背景(2026-09-18 A3 基线取证):`MdCG/MdCGSecure.search` 的 T2 全局阶段对**全部候选**
5
+ `_read_many(entries)`(实测 7395/12092 ≈ 0.6116,且 5 条不同查询的 scanned 完全相同)
6
+ ——触碰量与查询无关,说明它做的是全表/大域扫描,而不是「按查询收敛」。
7
+
8
+ 本层在**读盘之前**把候选收敛到「必要条件成立」的子集,三步各司其职:
9
+ ① 大域收敛:bigram 倒排(query bigrams ∪ 各 term 的 bigrams ∪ 原始查询 bigrams → 并集)。
10
+ 口径**精确**:LIKE 命中要求 term 作为子串出现 ⟹ term 的所有 bigram 必在文档
11
+ bigram 集内;词法打分 >0(bigram 重合)要求至少一个 query bigram 命中 ⟹ 倒排并集
12
+ 必为「LIKE 命中 ∪ bigram 正分」的**超集**(证明见 test_reach.py 的包含性断言)。
13
+ **例外(已声明)**:_score 的 tag_bonus(标签与原始查询互为子串)可给单字符标签的
14
+ 节点 +0.05 分而不共享任何 bigram。这类节点不在收敛集内,但**仍不丢召回**:它只能经
15
+ T3 全量兜底阶段进入结果,而该阶段仅在收敛阶段未返回时运行(收敛阶段失败 → 走原全量
16
+ 路径);收敛阶段成功 ⟺ 原 T2 成功(LIKE 命中集相等),此时两条路径都不含该节点。
17
+ ② 条件门控:只把「被接受的**必要条件**」提前到读盘前——层/角色/会话/分支
18
+ + 可读性(_open_content 返回 None 的节点在构建期即剔除)。**时窗/生命周期/重要度
19
+ 密级(clearance)等只影响排序或由调用方与 _readable 负责**,本层不新增门控,避免静默改变结果集。
20
+ ③ 图扩散:按索引内 `edges`(零读盘)做 1..N 跳扩散补召回;只吸收在候选集合内、
21
+ 且带 content_hash 的邻居,宁少不误扩散。
22
+
23
+ 回退:任何一步不适用(开关关闭 / 单字符 term / 索引不可写且无缓存 / 收敛集为空)
24
+ → 返回 (None, meta),调用方继续走原全量阶段,**旧行为不变**。
25
+
26
+ # 功能名:检索候选收敛层
27
+ # 生效条件:MdCG/MdCGSecure.search 在 T2 全局阶段之前调用本层时;进程环境 MDCG_REACH == "1"(显式 opt-in,默认关——依据 docs/hive/检索路径与认知结构契约_v0.1.md §7「不在默认路径上启用新行为」,与 MDCG_RETRIEVAL_PIPELINE 同纪律);库根可写(首建缓存)或缓存已存在;查询各 term 长度 >= 2(bigram 能覆盖子串语义)
28
+ # 子功能:bigram 倒排大域收敛(LIKE 与词法打分双路并集,超集精确);条件门控(只提前「接受的必要条件」:层/角色/会话/分支 + 可读性;时窗/生命周期/密级等仅排序或由调用方负责,不作门控);edges 图扩散补召回;命中不足或条件不满足时回退全量
29
+ # 执行:from md_cg import reach;reach.narrow(cg, entries, terms, qb, context) -> (entries 或 None, meta);缓存文件 <root>/_reach_index.json(MDCG_REACH_INDEX 可改写落点);开关 MDCG_REACH=1 显式开启(默认关,未设即不生效)
30
+ # 验证方式:md_cg/test_reach.py(收敛集必含 LIKE/打分命中集、门控/扩散/回退逐项断言);_coord/a3_reach.py(A1 全组回归、A2 新旧结果对比、A3 触碰比)
31
+ # A3 口径(与文档对齐):meta.scanned 只计**检索读盘**(_read_many);索引首建/重建的全库读盘
32
+ # 另计于 meta.reach_build_docs,两者不可混淆——报 A3 时须同时给「稳态均值」与「含首建均值」。
33
+ # 重建中途失败也如实暴露:meta.reach == "index_unavailable" 时带 meta.reach_build_docs(已读盘量)
34
+ # 与 meta.reach_build_docs=**已进入倒排**的节点数、meta.reach_build_reads=**已实际读盘**的节点数(含读失败/解密失败)、meta.reach_build_partial=True;
35
+ # 无读盘则这些键均不出现(r7/r8 独立复核取证:旧写法把部分读盘成本藏起来了,且只计入索引数会漏计「读了很多但都不可读」的情形)。
36
+ # 新鲜度覆盖边界(如实声明):键 = content_hash(正文)+ tags + edges。fm.semantic 属**前置元数据且不在索引条目中**,
37
+ # 仅改 semantic 标志不会改变键;该情形由 TTL(默认 600s)兜底——**在 TTL 窗口内**改 semantic 标志可能破坏 new 包含 old(窗口上界=TTL);若需秒级精确,应把 semantic 纳入索引条目后再入键。
38
+ # 已知风险(如实声明):落盘缓存 post 内含**明文 bigram**,目前未按主体/密级隔离——
39
+ # 多主体共用同一 root 时,缓存文件本身即构成跨主体词面信息(读盘/内存无越权,但文件级隔离未做)。
40
+ # 不适用条件:①查询含单字符 term(bigram 无法表达单字子串)→ 不收敛 ①b库内有节点缺 content_hash 且非本次重建 → 不收敛(宁全量不冒陈旧丢召回)①c图扩散默认关闭(MDCG_REACH_DIFFUSE=1 开启:扩散节点参与 top-k 竞争,召回增加但 A2 的集合包含性不再逐位保证) ②MDCG_REACH 未设或不等于 "1"(默认关,契约 §7) ③索引内容过时(content_hash 不符 / 超过 MDCG_REACH_TTL 默认 600s)时先全量重建,重建失败即不收敛 ④只读根且无缓存时:当次仍以内存索引收敛,但**不入进程缓存**(下次调用重新构建)——收敛收益减少但结果不丢,且与声明一致 ⑤首建当次额外读全库(meta.reach_build_docs 如实暴露),A3 稳态值不含该次 ⑤密级(clearance)过滤由调用方与 _readable 负责,本层不新增 ⑥首建当次调用会读全库(meta.reach_build_docs 如实暴露),稳态才享受收敛收益
41
+ """
42
+ from __future__ import annotations
43
+
44
+ import json
45
+ import math
46
+ import os
47
+ import time
48
+
49
+ from .fsutil import FileLock, atomic_write
50
+ from . import nodefile
51
+
52
+
53
+ # 生效条件:调用发生在 md_cg.mdcg 完成导入之后(本模块被 mdcg 顶层 import 时**不得**在模块级取用
54
+ # mdcg 的符号,否则 mdcg↔reach 循环导入);返回 (bigrams, normalize_en) 两函数。
55
+ def _lex():
56
+ """延迟取词法函数——规避 mdcg ↔ reach 循环导入(模块级 from .mdcg import X 会失败)。"""
57
+ from .mdcg import bigrams as _bg, normalize_en as _ne
58
+ return _bg, _ne
59
+
60
+
61
+ # 生效条件:content 为节点正文、tags 为标签可迭代对象时返回该文档的 token 集合 = bigrams(正体小写) ∪ bigrams(标签小写) ∪ bigrams(normalize_en(正文));content 为假值时按空串处理。
62
+ def _doc_tokens(content, tags):
63
+ bg, ne = _lex()
64
+ body = nodefile.positive_body(content).lower()
65
+ tag_s = " ".join(str(t) for t in (tags or [])).lower()
66
+ # 文档侧 token = 正体小写 ∪ 标签小写 ∪ normalize_en(正文) ∪ normalize_en(正文).lower()
67
+ # 末项是**超集保险**:查询侧与 LIKE 都走小写口径,故索引侧再收一份全小写(r13 复核:
68
+ # 若 LIKE 命中落在正体之外且含大写/全角,仅 ne() 可能不产生对应小写 bigram)
69
+ return bg(body) | bg(tag_s) | bg(ne(content)) | bg(ne(content).lower())
70
+
71
+ INDEX_NAME = "_reach_index.json"
72
+ #: 倒排落盘用 JSON 列表(曾用逗号/单元分隔符拼接:含该字符的路径会被拆碎,r1/r11 复核取证)
73
+ SCHEMA_VERSION = 1
74
+ #: 扩散触发阈值:收敛集小于该值时才做图扩散(避免大集合上无谓放大)
75
+ DIFFUSE_BELOW = 400
76
+ #: 未索引/新写入节点的并入上限:超过即整段回退不收敛(不静默截断——截断会破坏超集/A2)
77
+ _FRESH_MAX = 500
78
+ #: 扩散放大上限:不超过收敛集该倍数(防扩散把 A3 触碰比吃回去)
79
+ DIFFUSE_MAX_FACTOR = 3
80
+
81
+ #: 进程内缓存:root -> (ReachIndex, 载入时的库指纹)
82
+ _CACHE = {}
83
+
84
+
85
+ # 生效条件:读取 MDCG_REACH_TTL;值为非正数或不可解析时回落到默认 600.0(**不得**把 TTL=0 解释为关闭新鲜度兜底——semantic 变更依赖它,r11 复核取证)。
86
+ def _ttl() -> float:
87
+ try:
88
+ v = float(os.environ.get("MDCG_REACH_TTL") or 600.0)
89
+ except ValueError:
90
+ return 600.0
91
+ # 非有限(inf/nan/1e999)视同非法:否则过期判断永假 → 声明中的 TTL 兜底被静默取消(r12 复核取证)
92
+ if not math.isfinite(v) or v <= 0:
93
+ return 600.0
94
+ return v
95
+
96
+
97
+ # 生效条件:idx 若本次发生了全量重建(built_now)则返回 {"reach_build_docs": built_docs},否则返回 {}——
98
+ # 供**所有**窄化出口(含 hash_incomplete/fresh_overflow 回退)如实暴露首建成本。
99
+ def _build_meta(idx) -> dict:
100
+ if getattr(idx, "built_now", False):
101
+ return {"reach_build_docs": getattr(idx, "built_docs", 0),
102
+ "reach_build_reads": getattr(idx, "built_reads", 0)}
103
+ return {}
104
+
105
+
106
+ # 生效条件:环境变量 MDCG_REACH **恰为 "1"** 时返回 True;未设置或其它取值(含 "0")返回 False——即默认关。
107
+ # 依据:docs/hive/检索路径与认知结构契约_v0.1.md §7「不在默认路径上启用新行为」(先 flag 化、逐项验证、再讨论默认开启)
108
+ # 与 §5.1「开关关 → run_tests 全绿且与现状一致」。启用与否由调用点 reach.enabled() 前置守卫,保证关态下不向 meta 落任何 reach* 键。
109
+ def enabled() -> bool:
110
+ return os.environ.get("MDCG_REACH") == "1"
111
+
112
+
113
+ # 生效条件:环境变量 MDCG_REACH_DIFFUSE 等于 "1" 时返回 True,其余(含未设置)返回 False。
114
+ def diffuse_on() -> bool:
115
+ """图扩散开关(默认关):扩散节点会与词面命中一起参与 top-k 竞争,
116
+ 为使 A2「new ⊇ old」在默认配置下**精确成立**,此能力默认关闭、需显式开启。"""
117
+ return os.environ.get("MDCG_REACH_DIFFUSE", "0") == "1"
118
+
119
+
120
+ # 生效条件:entries 中每个元素都有 path 键且 path 对应库根下的文件时,返回其 (path -> entry) 映射;缺 path 的条目被跳过。
121
+ def _by_path(entries):
122
+ out = {}
123
+ for e in entries:
124
+ p = e.get("path")
125
+ if p:
126
+ out[p] = e
127
+ return out
128
+
129
+
130
+ # 生效条件:entry 为索引条目时返回其 edges 的稳定签名(按 target|type 排序后拼接;无 edges 返回空串)。
131
+ def _edges_sig(entry) -> str:
132
+ # 用 JSON 列表编码:分隔符若有歧义会导致键碰撞(r11 复核:a;b 与 [a,b] 曾同键)
133
+ items = []
134
+ for ed in (entry or {}).get("edges") or []:
135
+ if isinstance(ed, dict):
136
+ items.append([str(ed.get("target") or ""), str(ed.get("type") or "")])
137
+ else:
138
+ items.append([str(ed)])
139
+ return json.dumps(sorted(items), ensure_ascii=False)
140
+
141
+
142
+ # 生效条件:entry 为索引条目时返回其新鲜度键 = content_hash + chr(31) + edges 签名——内容或**边**任一变化都会使键变化(r7 复核:仅改边也必须触发重建,否则扩散会复用陈旧邻接表)。
143
+ def _node_key(entry) -> str:
144
+ # 倒排同时依赖:正文(content_hash)、标签 bigram(tags)、邻接(edges)。
145
+ # 三者任一变化都必须判为不新鲜,否则会复用陈旧倒排(r7 补 edges、r9 补 tags)。
146
+ tags = json.dumps([str(t) for t in ((entry or {}).get("tags") or [])], ensure_ascii=False)
147
+ return json.dumps([str((entry or {}).get("content_hash") or ""), tags, _edges_sig(entry)],
148
+ ensure_ascii=False)
149
+
150
+
151
+ # 生效条件:cg 有 index["nodes"] 与 root,且根目录存在时返回「库指纹」字符串(节点数 + 各节点 content_hash 的拼接哈希);缺字段按空串参与。
152
+ def _fingerprint(cg) -> str:
153
+ import hashlib
154
+ h = hashlib.sha1()
155
+ nodes = cg.index.get("nodes") or {}
156
+ h.update(str(len(nodes)).encode())
157
+ for k in sorted(nodes):
158
+ # 指纹含 **path** 与 **edges 签名**:路径交换/重命名、或仅增删改边时都必须判为变更,
159
+ # 否则进程缓存复用会给「旧 path 的倒排」或「旧邻接表」(扩散沿已删边/漏新邻居)
160
+ h.update((str(nodes[k].get("path") or "") + chr(31)).encode())
161
+ h.update(_node_key(nodes[k]).encode())
162
+ return h.hexdigest()
163
+
164
+
165
+ # 生效条件:root 为调用方传入的根路径字符串,实例化后 path 取 os.environ.get("MDCG_REACH_INDEX") 的值,未设或为空串时回落 os.path.join(root, INDEX_NAME);post/adj/hashes/sem 初始化为空容器,persist_failed=False,built_fingerprint="",built_now=False,hash_complete=True,built_docs/built_reads=0,built_at=0.0;
166
+ class ReachIndex:
167
+ """bigram 倒排 + 邻接表(edges),落盘于 <root>/_reach_index.json。"""
168
+
169
+ # 生效条件:root 给定时记录根目录与缓存路径;构造本身不读盘。
170
+ # 生效条件:对任意 root(含空串等假值),当 MDCG_REACH_INDEX 环境变量为非空字符串时 self.path 取该值,否则 self.path 回落到 os.path.join(root, INDEX_NAME)。
171
+ def __init__(self, root: str):
172
+ self.root = root
173
+ # 缓存落点:默认 <root>/_reach_index.json;环境变量 MDCG_REACH_INDEX 可改到别处
174
+ # (测量/演练时避免在真实记忆库根下落盘)
175
+ self.path = os.environ.get("MDCG_REACH_INDEX") or os.path.join(root, INDEX_NAME)
176
+ self.post = {} # bigram -> [path, ...]
177
+ self.adj = {} # path -> [path, ...]
178
+ self.hashes = {} # path -> content_hash
179
+ self.sem = [] # 语义摘要节点(fm.semantic 为真)——MDCG_SEMANTIC=1 时无条件入池
180
+ self.persist_failed = False # 落盘失败(只读根):不跨调用复用
181
+ self.built_fingerprint = "" # 建库时的库指纹(磁盘新鲜度判据)
182
+ self.built_now = False # 本次进程缓存是否刚做过全量重建(供审计首建成本)
183
+ self.hash_complete = True # 库内节点是否都有 content_hash(缺则不复用缓存)
184
+ self.built_docs = 0 # 本次重建**已进入倒排**的节点数
185
+ self.built_reads = 0 # 本次重建**已实际读盘**的节点数(含读失败/解密失败;失败时如实暴露 I/O 成本)
186
+ self.built_at = 0.0
187
+
188
+ # 生效条件:缓存文件存在且 JSON 可解析、schema 版本相符时载入 post/adj/hashes 并返回 True;文件缺失/损坏/版本不符返回 False(调用方转入重建)。
189
+ # 生效条件:读取 self.path 时若 open/json.load 抛 OSError 或 ValueError、或解析结果不是 dict、或其中 "v" 与模块级常量 SCHEMA_VERSION 不等则返回 False,否则回填 post/adj/hashes/sem/built_at/fp 并返回 True。
190
+ def load(self) -> bool:
191
+ try:
192
+ with open(self.path, encoding="utf-8") as f:
193
+ d = json.load(f)
194
+ except (OSError, ValueError):
195
+ return False
196
+ if not isinstance(d, dict) or d.get("v") != SCHEMA_VERSION:
197
+ return False
198
+ self.post = {k: list(v) for k, v in (d.get("post") or {}).items() if v}
199
+ self.adj = d.get("adj") or {}
200
+ self.hashes = d.get("hashes") or {}
201
+ self.sem = list(d.get("sem") or [])
202
+ self.built_at = float(d.get("built_at") or 0.0)
203
+ self.built_fingerprint = str(d.get("fp") or "")
204
+ return True
205
+
206
+ # 生效条件:cg 有 index["nodes"] 且可逐条 _read 时,全量重建倒排与邻接:文档 token = bigrams(positive_body 小写) ∪ bigrams(标签小写) ∪ bigrams(normalize_en(content))(前者覆盖 LIKE、后者覆盖词法打分);邻接取 index 内 edges 的 target,仅保留在索引中的目标。
207
+ # 生效条件:当 cg 的 index 提供 nodes 时,对每个 path 非空、cg._read 返回内容非 None 且 cg._open_content 亦返回非 None 的节点写入 hashes/post(fm.semantic 为真时并入 sem),并只把两端均已入 hashes 的 edges 建成 adj,随后按原始 content_hash 是否齐全设置 hash_complete 并返回 self。
208
+ def build(self, cg):
209
+ nodes = cg.index.get("nodes") or {}
210
+ id2path = {k: (v.get("path") or "") for k, v in nodes.items()}
211
+ hashes, post, adj, sem = {}, {}, {}, []
212
+ for k in sorted(nodes):
213
+ e = nodes[k]
214
+ p = e.get("path")
215
+ if not p:
216
+ continue
217
+ fm, c = cg._read(e)
218
+ self.built_reads += 1 # 已发生一次读盘:无论该节点是否可读、后续是否报错,都如实计数(r8 复核取证)
219
+ if c is None:
220
+ continue # 读不到 → 与检索路径同样跳过
221
+ # 与 _read_many 同口径:密文节点必须解密后再取 token,否则加密节点
222
+ # 只索引到密文 → 收敛集漏掉它们(A2 召回退化,2026-09-18 由 test_p3 取证)
223
+ c = cg._open_content((fm or {}).get("id"), fm, c)
224
+ if c is None:
225
+ continue
226
+ hashes[p] = _node_key(e)
227
+ self.built_docs = len(hashes) # 渐进计数:中途抛错也能如实暴露已入倒排量(r7 复核取证)
228
+ if (fm or {}).get("semantic"):
229
+ sem.append(p) # MDCG_SEMANTIC=1 时该节点无条件入池 → 必须进收敛集
230
+ for b in _doc_tokens(c, (fm or {}).get("tags")):
231
+ post.setdefault(b, []).append(p)
232
+ for k, e in nodes.items():
233
+ p = e.get("path")
234
+ if not p or p not in hashes:
235
+ continue
236
+ nb = []
237
+ for ed in e.get("edges") or []:
238
+ tgt = ed.get("target") if isinstance(ed, dict) else ed
239
+ tp = id2path.get(tgt)
240
+ if tp and tp in hashes:
241
+ nb.append(tp)
242
+ if nb:
243
+ adj[p] = nb
244
+ self.post, self.adj, self.hashes, self.sem = post, adj, hashes, sem
245
+ self.built_fingerprint = _fingerprint(cg)
246
+ self.built_docs = len(hashes)
247
+ # 覆盖完整性必须看**原始 content_hash**(hashes 里存的是 _node_key,
248
+ # 含分隔符恒为真 → 曾使 hash_complete 恒判 True,r10 复核取证)
249
+ self.hash_complete = all(bool(v.get("content_hash"))
250
+ for v in nodes.values() if v.get("path"))
251
+ self.built_at = time.time()
252
+ return self
253
+
254
+ # 生效条件:在 FileLock(缓存路径) 内以 atomic_write 写回 post(逗号连接)/adj/hashes/built_at;写失败抛 OSError(调用方按不收敛处理)。
255
+ # 生效条件:以模块级常量 SCHEMA_VERSION 与实例的 built_at/built_fingerprint/post/adj/hashes/sem 组装 JSON,并在 FileLock(self.path, timeout=5.0) 内 atomic_write 到 self.path,无返回值。
256
+ def save(self):
257
+ d = {"v": SCHEMA_VERSION, "built_at": self.built_at, "fp": self.built_fingerprint,
258
+ "post": {k: list(v) for k, v in self.post.items()},
259
+ "adj": self.adj, "hashes": self.hashes, "sem": self.sem}
260
+ with FileLock(self.path, timeout=5.0):
261
+ atomic_write(self.path, json.dumps(d, ensure_ascii=False))
262
+
263
+ # 生效条件:tokens 为可迭代的 bigram 集合时返回倒排并集(path 集合);空 tokens 返回空集。
264
+ # 生效条件:对 tokens 中每个元素取 self.post.get(元素),仅当取值为非空列表时把其中路径并入返回集合(缺键或空列表不贡献)。
265
+ def postings(self, tokens) -> set:
266
+ out = set()
267
+ for b in tokens:
268
+ got = self.post.get(b)
269
+ if got:
270
+ out.update(got)
271
+ return out
272
+
273
+ # 生效条件:seeds 为 path 集合时按 adj 做 hops 跳扩散并返回 seeds ∪ 各跳邻居;邻居超出 limit(默认收敛集 DIFFUSE_MAX_FACTOR 倍 + 1)即停止吸收。
274
+ # 生效条件:返回集以 seeds 起步,limit 不大于 0(含默认 0)时上限改取 max(len(seeds),1)*DIFFUSE_MAX_FACTOR+1、否则用传入的 limit,再沿 self.adj 逐跳扩展 max(0,hops) 层,一旦返回集大小达到上限立即返回(hops 为 0 或负时只返回 seeds 集合)。
275
+ def diffuse(self, seeds, hops: int = 1, limit: int = 0) -> set:
276
+ out = set(seeds)
277
+ if limit <= 0:
278
+ limit = max(len(seeds), 1) * DIFFUSE_MAX_FACTOR + 1
279
+ frontier = list(out)
280
+ for _ in range(max(0, hops)):
281
+ nxt = []
282
+ for p in frontier:
283
+ for q in self.adj.get(p) or []:
284
+ if q not in out:
285
+ out.add(q)
286
+ nxt.append(q)
287
+ if len(out) >= limit:
288
+ return out
289
+ frontier = nxt
290
+ if not frontier:
291
+ break
292
+ return out
293
+
294
+
295
+ # 生效条件:root 下缓存与当前库指纹一致时返回进程缓存/磁盘缓存实例;指纹不符或缓存缺失时重建并尝试落盘(落盘失败仍返回内存实例);重建抛异常时返回 None(调用方不收敛)。缓存键含缓存路径(MDCG_REACH_INDEX 改写时互不串味)。
296
+ # 生效条件:调用 _index() 需同时拿到「本次重建失败且已部分读盘」的成本时,返回 (idx, fail_meta);fail_meta 在成功/无读盘时为 {}。
297
+ # 为何用调用方提供的容器而不是模块级状态:模块级可变状态无锁,跨线程/跨调用都会串味
298
+ # (r8 独立复核取证);而「失败信息与本次调用的绑定」必须显式。idx 仍按原契约返回,其它调用方无需改动。
299
+ def _index_with_meta(cg, force: bool = False):
300
+ fail = {}
301
+ idx = _index(cg, force=force, _fail=fail)
302
+ return idx, fail
303
+
304
+
305
+ # 生效条件:必填 cg 提供 root 与指纹,force 默认 False、_fail 默认 None;当 force 为假且 _CACHE.get(ReachIndex(cg.root).path) 的 hit 为真、hit[1] 等于 _fingerprint(cg) 且 _ttl() 未超时时,返回 hit[0] 并置其 built_now=False;否则以 idx=ReachIndex(cg.root) 继续:先若 force 为假且 idx.load() 为真则按 _ttl() 判 expired,再无条件设 idx.hash_complete=_hash_complete(cg),随后若 force 为假且 expired 为假且 idx.post 非空且 _disk_fresh(idx,cg) 为真则置 idx.built_now=False、写 _CACHE[ReachIndex(cg.root).path]=(idx,fp) 并返回 idx;否则 try 中 idx.build(cg) 且 idx.built_now=True 后 idx.save(),save 成功则 persist_failed=False、写 _CACHE[ReachIndex(cg.root).path]=(idx,fp) 并返回 idx,save 抛 OSError 则 persist_failed=True、pop 缓存并返回 idx;idx.build 或 save 抛其他异常(含 save 非 OSError)则当 _fail 非 None 且 built_reads 或 built_docs 非零时向 _fail 写入 reach_build_docs、reach_build_reads、reach_build_partial=True,并返回 None;
306
+ def _index(cg, force: bool = False, _fail=None) -> "ReachIndex | None":
307
+ root = cg.root
308
+ fp = _fingerprint(cg)
309
+ idx0 = ReachIndex(root)
310
+ key = idx0.path
311
+ hit = _CACHE.get(key)
312
+ ttl0 = _ttl()
313
+ if (not force and hit and hit[1] == fp
314
+ and not (ttl0 > 0 and (time.time() - hit[0].built_at) > ttl0)):
315
+ hit[0].built_now = False # 复用:本次未重建(勿把上一轮的首建标记留在 meta 里)
316
+ return hit[0]
317
+ idx = idx0
318
+ ttl = _ttl()
319
+ expired = False
320
+ if not force and idx.load():
321
+ expired = ttl > 0 and (time.time() - idx.built_at) > ttl
322
+ # hash 覆盖不完整(有节点无 content_hash)→ 无法判「未变」,缓存复用会给陈旧倒排,
323
+ # 故一律重建;重建失败则由 narrow 走 hash_incomplete 回退(T3-r5 复核取证)
324
+ idx.hash_complete = _hash_complete(cg)
325
+ if not force and not expired and idx.post and _disk_fresh(idx, cg):
326
+ idx.built_now = False
327
+ _CACHE[key] = (idx, fp)
328
+ return idx
329
+ try:
330
+ idx.build(cg)
331
+ idx.built_now = True
332
+ try:
333
+ idx.save()
334
+ idx.persist_failed = False
335
+ except OSError:
336
+ # 只读根:当次内存索引仍可用,但**不得跨调用复用**(模块头声明「下次调用重新构建」,
337
+ # r13 复核指出旧写法会让 _CACHE 跨调用复用,与声明不符)
338
+ idx.persist_failed = True
339
+ except Exception:
340
+ # 重建中途失败:已读盘的量也必须如实暴露(不许把成本藏起来)——r7 独立复核取证
341
+ reads = getattr(idx, "built_reads", 0)
342
+ docs = getattr(idx, "built_docs", 0)
343
+ if _fail is not None and (reads or docs):
344
+ _fail["reach_build_docs"] = docs
345
+ _fail["reach_build_reads"] = reads
346
+ _fail["reach_build_partial"] = True
347
+ return None
348
+ if getattr(idx, "persist_failed", False):
349
+ _CACHE.pop(key, None)
350
+ else:
351
+ _CACHE[key] = (idx, fp)
352
+ return idx
353
+
354
+
355
+ # 生效条件:cg.index["nodes"] 中每个节点都有非空 content_hash 时返回 True,否则 False(意味着无法用 content_hash 判新鲜)。
356
+ def _hash_complete(cg) -> bool:
357
+ # 口径与 build() 的覆盖集合一致:只按有 path 的条目判定(无 path 的条目 build 不收录,
358
+ # 不应因此禁用缓存复用——r12 复核指出旧写法会让稳态每次全库重建)
359
+ nodes = cg.index.get("nodes") or {}
360
+ return all(bool(v.get("content_hash")) for v in nodes.values() if v.get("path"))
361
+
362
+
363
+ # 生效条件:磁盘缓存 hashes 与库内节点 path/content_hash 一一对应(数量相同且逐条相等)时返回 True,否则返回 False(触发重建)。
364
+ def _disk_fresh(idx: ReachIndex, cg) -> bool:
365
+ """磁盘缓存是否新鲜:**建库时指纹 == 当前指纹**(含节点数/path/键=hash+tags+edges)。
366
+
367
+ 为什么用指纹而不是「逐条缓存条目复核」:build 会跳过不可读/解密失败的节点(它们不进 hashes),
368
+ 若要求「索引里有 path 的节点数 == 缓存条目数」,库内含任一不可读节点时永不新鲜 → 跨进程每次
369
+ 全库重建(r17 复核取证);而只复核缓存条目又会**漏掉新增节点**(r17 修正版实测)。
370
+ 指纹同时覆盖:节点增删/改名/正文 hash/标签/邻接,且与可读性无关。
371
+ """
372
+ if not idx.hashes:
373
+ return False
374
+ return bool(idx.built_fingerprint) and idx.built_fingerprint == _fingerprint(cg)
375
+
376
+
377
+ # 生效条件:仅当 enabled() 为真、terms 中各项长度均不小于 2、_index_with_meta(cg) 返回的 idx 非 None 且 idx.hash_complete 为真、由 qb/terms/q 生成的 token 集非空、按 entries 过滤后的 got 非空、且 entries 中未索引或 created_at 晚于 idx.built_at 的新节点数不超过 _FRESH_MAX 时返回 (got, meta),否则按守卫失败原因返回 (None, {"reach": off/short_term/index_unavailable/hash_incomplete/no_tokens/empty_converge/fresh_overflow})。
378
+ def narrow(cg, entries, terms, qb, context=None, hops: int = 1, q: str = ""):
379
+ meta = {"reach": "on"}
380
+ if not enabled():
381
+ return None, {"reach": "off"}
382
+ terms = list(terms or [])
383
+ if any(len(t or "") < 2 for t in terms):
384
+ return None, {"reach": "short_term"} # 单字符 term 的 bigram 无法覆盖子串语义
385
+ idx, _fail = _index_with_meta(cg)
386
+ if idx is None:
387
+ return None, dict({"reach": "index_unavailable"}, **_fail)
388
+ _bmeta = _build_meta(idx) # 回退分支同样要如实暴露本次首建/重建成本(r18 复核取证)
389
+ if not getattr(idx, "hash_complete", True):
390
+ # 库内有节点缺 content_hash:无法证明倒排未过期 → 一律不收敛(走全量),不冒丢召回风险。
391
+ # 注意:**不论是否本次重建**都必须回退——否则「每次重建 + 每次收敛」会把全库读盘
392
+ # 混进稳态 scanned,并与模块头声明不符(r15 复核取证)
393
+ return None, dict({"reach": "hash_incomplete"}, **_bmeta)
394
+ bg, _ne = _lex()
395
+ toks = set(qb or ())
396
+ # 原始查询与其中文/小写形态:覆盖 _score 的 tag_bonus 路径
397
+ # (str(tag) in q 或 q in str(tag) 会给「标签与原始查询互为子串」的节点 +0.05 分,
398
+ # 该路径与 LIKE/词法分无关,故 token 集必须含原始查询的 bigram)
399
+ toks |= bg(q or "")
400
+ toks |= bg((q or "").lower())
401
+ for t in terms:
402
+ toks |= bg(t)
403
+ toks |= bg((t or "").lower())
404
+ # 与文档侧 ne() 口径对齐(terms 此前只做小写)——同样是超集方向,宁多读不漏召回
405
+ toks |= bg(_ne(t or ""))
406
+ toks |= bg(_ne(t or "").lower())
407
+ toks = {b for b in toks if len(b) >= 2}
408
+ if not toks:
409
+ return None, dict({"reach": "no_tokens"}, **_bmeta)
410
+ paths = idx.postings(toks)
411
+ try:
412
+ from .mdcg import semantic_on
413
+ if semantic_on() and idx.sem:
414
+ paths |= set(idx.sem) # 语义摘要节点无条件入池(否则收敛会误丢)
415
+ except Exception:
416
+ pass
417
+ by_path = _by_path(entries)
418
+ got = [by_path[p] for p in paths if p in by_path]
419
+ if not got:
420
+ return None, dict({"reach": "empty_converge"}, **_bmeta)
421
+ pre = len(got)
422
+ diffused = []
423
+ if diffuse_on() and len(got) < DIFFUSE_BELOW:
424
+ # 扩散种子先按 entries 过滤(否则会从未授权/越层的索引节点拉邻居,放大读盘与内存)
425
+ seed_paths = {p for p in paths if p in by_path}
426
+ spread = idx.diffuse(seed_paths, hops=hops)
427
+ got = [by_path[p] for p in spread if p in by_path]
428
+ diffused = [e for e in got if e.get("path") not in seed_paths]
429
+ meta.update({"reach_seed": pre, "reach": "converged", "reach_diffused": len(diffused),
430
+ "reach_hops": hops,
431
+ # 结果层要据此放行「图扩散补召回」的节点(它们没有 LIKE/词法命中,
432
+ # 若只按 _like 过滤就会被丢掉 → 扩散形同虚设,T3-r3 复核取证)
433
+ "reach_diffused_paths": [e["path"] for e in diffused]})
434
+ # 新节点并入:索引建好之后写入的节点(created_at 晚于 built_at)可能是新命中,直接并入候选,
435
+ # 避免「陈旧索引漏召回」窗口(文件被原地改动的情形由 TTL 兜底)
436
+ fresh_paths = []
437
+ for e in entries:
438
+ pth = e.get("path")
439
+ if pth is None or pth in paths:
440
+ continue
441
+ ca = e.get("created_at")
442
+ if pth not in idx.hashes or (ca and float(ca) > idx.built_at):
443
+ fresh_paths.append(e) # 未索引(新写入)或索引建成后写入 → 一律并入候选
444
+ if len(fresh_paths) > _FRESH_MAX:
445
+ # 未索引节点过多(索引覆盖不完整/权限不完整):既不能静默截断(截断掉的可能是 LIKE 命中,
446
+ # 而收敛集另有命中时会提前 return,兜底不跑 → 破坏超集与 A2),也不能无限放大读盘
447
+ # → 整段回退:不收敛,走原全量路径(r8 复核取证)
448
+ return None, dict({"reach": "fresh_overflow", "reach_fresh_nodes": len(fresh_paths)}, **_bmeta)
449
+ if fresh_paths:
450
+ got = got + fresh_paths
451
+ meta["reach_fresh_nodes"] = len(fresh_paths)
452
+ meta.update(_build_meta(idx)) # 首建成本(若有)如实暴露
453
+ return got, meta