@furongjun1999/dsh-memory 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/README.md +130 -47
  2. package/codebuddy/CODEBUDDY.md +21 -10
  3. package/codebuddy/README.md +13 -1
  4. package/codebuddy/mcp.json +9 -0
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
  6. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
  7. package/docs/README.md +111 -0
  8. package/docs/discipline/harnesses.yaml +226 -152
  9. package/docs/discipline/templates/full.md.tmpl +61 -58
  10. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
  11. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
  12. package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
  13. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  14. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  15. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  16. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  17. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  18. package/docs/experiments/m4-role-probe/census.py +86 -0
  19. package/docs/experiments/m4-role-probe/probe.py +89 -0
  20. package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
  21. package/docs/experiments/mapped_confidence/post_check.py +75 -0
  22. package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
  23. package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
  24. package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
  25. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
  26. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -0
  27. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
  28. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
  29. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -0
  30. package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
  31. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
  32. package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  33. package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
  34. package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
  35. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -0
  36. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +263 -0
  37. package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
  38. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.5.md +236 -0
  39. package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
  40. package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
  41. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
  42. package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
  43. package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
  44. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
  45. package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +4 -4
  46. package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
  47. package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
  48. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
  49. package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
  50. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
  51. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
  52. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -0
  53. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
  54. package/docs/{ → theory/}/347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +6 -6
  55. package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
  56. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -0
  57. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
  58. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
  59. package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
  60. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +434 -405
  61. package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
  62. package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
  63. package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
  64. package/dsh/README.md +34 -1
  65. package/dsh/cordis.yml.example +13 -7
  66. package/dsh/hive-mcp-probe.mjs +94 -0
  67. package/dsh/hive-mcp.example.yml +62 -0
  68. package/dsh/update-lingshu.bat +11 -0
  69. package/dsh/update-lingshu.ps1 +337 -0
  70. package/lib/hooks.d.ts +8 -0
  71. package/lib/hooks.js +27 -24
  72. package/lib/index.d.ts +4 -2
  73. package/lib/index.js +29 -6
  74. package/lib/lib/datapath.d.ts +76 -1
  75. package/lib/lib/datapath.js +199 -13
  76. package/lib/lib/mdcg_client.d.ts +6 -3
  77. package/lib/lib/mdcg_client.js +6 -5
  78. package/lib/lib/mutual.js +4 -4
  79. package/lib/lib/prompt_safety.d.ts +48 -0
  80. package/lib/lib/prompt_safety.js +62 -0
  81. package/lib/lib/token_store.js +4 -5
  82. package/md_cg/audit.py +91 -2
  83. package/md_cg/autonomy.py +86 -15
  84. package/md_cg/backfill.py +36 -1
  85. package/md_cg/backfill_bigdomain.py +34 -0
  86. package/md_cg/bench6_arms.py +28 -1
  87. package/md_cg/bench6_common.py +10 -1
  88. package/md_cg/bench6_competitors.py +6 -1
  89. package/md_cg/bench_axis_domain.py +9 -1
  90. package/md_cg/bench_blind_comp.py +7 -1
  91. package/md_cg/bench_en_atoms_public.py +9 -0
  92. package/md_cg/bench_governance.py +348 -0
  93. package/md_cg/bench_lme_zh.py +16 -1
  94. package/md_cg/bench_locomo.py +2 -1
  95. package/md_cg/bench_locomo_zh.py +16 -1
  96. package/md_cg/bench_locomo_zh_public.py +4 -1
  97. package/md_cg/bench_longmem.py +2 -1
  98. package/md_cg/bench_membench.py +37 -6
  99. package/md_cg/bench_p0.py +4 -1
  100. package/md_cg/bench_progressive.py +61 -10
  101. package/md_cg/bench_role_views.py +238 -0
  102. package/md_cg/bench_task_ab.py +8 -1
  103. package/md_cg/bench_task_ab_llm.py +13 -1
  104. package/md_cg/bench_unified_en.py +6 -1
  105. package/md_cg/bench_zh_mad.py +20 -1
  106. package/md_cg/blindspot_tickets.py +123 -0
  107. package/md_cg/branches.py +286 -0
  108. package/md_cg/build_postings.py +73 -0
  109. package/md_cg/ccgc.py +949 -0
  110. package/md_cg/census.py +5 -1
  111. package/md_cg/chain.py +24 -3
  112. package/md_cg/codeindex.py +134 -17
  113. package/md_cg/coldverify.py +265 -0
  114. package/md_cg/comment_gate.py +338 -0
  115. package/md_cg/cond_compose.py +190 -0
  116. package/md_cg/cond_facts.py +155 -0
  117. package/md_cg/cond_template.json +107 -0
  118. package/md_cg/condition_anchor.py +143 -0
  119. package/md_cg/conformance.py +727 -0
  120. package/md_cg/consistency.py +25 -2
  121. package/md_cg/consolidate.py +53 -2
  122. package/md_cg/corpus.py +5 -1
  123. package/md_cg/crosscheck.py +42 -2
  124. package/md_cg/crypto.py +35 -1
  125. package/md_cg/d_meta.py +310 -0
  126. package/md_cg/datapath.py +201 -26
  127. package/md_cg/docindex.py +122 -1
  128. package/md_cg/eval_common.py +35 -6
  129. package/md_cg/evidence.py +27 -1
  130. package/md_cg/evolution.py +21 -1
  131. package/md_cg/export.py +11 -1
  132. package/md_cg/forgetting.py +34 -4
  133. package/md_cg/fsutil.py +81 -2
  134. package/md_cg/hotcache.py +214 -0
  135. package/md_cg/hyperedge.py +251 -0
  136. package/md_cg/identity.py +20 -3
  137. package/md_cg/insight.py +19 -3
  138. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  139. package/md_cg/lexicon/build_standard_en.py +171 -168
  140. package/md_cg/lexicon/expand_en_zh.py +6 -0
  141. package/md_cg/lifecycle.py +273 -0
  142. package/md_cg/linkref.py +281 -0
  143. package/md_cg/links.py +29 -1
  144. package/md_cg/mcp_server.py +894 -168
  145. package/md_cg/md_whitebox.py +53 -1
  146. package/md_cg/mdcg.py +1205 -41
  147. package/md_cg/mdcos.py +1060 -76
  148. package/md_cg/metacognition.py +39 -4
  149. package/md_cg/migrate.py +4 -0
  150. package/md_cg/migrate_aeis.py +221 -213
  151. package/md_cg/migrate_roleplay.py +8 -0
  152. package/md_cg/migrate_wisdom_graph.py +14 -1
  153. package/md_cg/mreview/__init__.py +25 -0
  154. package/md_cg/mreview/__main__.py +110 -0
  155. package/md_cg/mreview/bundle.py +178 -0
  156. package/md_cg/mreview/candidates.py +262 -0
  157. package/md_cg/mreview/govern.py +694 -0
  158. package/md_cg/mreview/locate.py +939 -0
  159. package/md_cg/mreview/pipeline.py +729 -0
  160. package/md_cg/mreview/rules/duplication.json +21 -0
  161. package/md_cg/mreview/rules/field_coverage.json +54 -0
  162. package/md_cg/mreview/rules/source_license.json +21 -0
  163. package/md_cg/mreview/rules/template_flow.json +21 -0
  164. package/md_cg/mreview/ruleset.py +253 -0
  165. package/md_cg/nodefile.py +269 -1
  166. package/md_cg/pooling.py +23 -1
  167. package/md_cg/postings.py +298 -0
  168. package/md_cg/predict.py +113 -15
  169. package/md_cg/progressive.py +3 -0
  170. package/md_cg/protect.py +18 -5
  171. package/md_cg/protocol.py +372 -0
  172. package/md_cg/provenance.py +262 -0
  173. package/md_cg/reach.py +453 -0
  174. package/md_cg/refindex.py +88 -4
  175. package/md_cg/refine.py +20 -1
  176. package/md_cg/roleviews.py +89 -0
  177. package/md_cg/routing.py +76 -0
  178. package/md_cg/scrub.py +68 -3
  179. package/md_cg/security.py +26 -1
  180. package/md_cg/self_state.py +65 -2
  181. package/md_cg/selfreport.py +151 -0
  182. package/md_cg/semantic/canonical.py +5 -0
  183. package/md_cg/semantic/en_normalizer.py +364 -295
  184. package/md_cg/semantic/zh_en_atoms.py +139 -136
  185. package/md_cg/signer.py +41 -1
  186. package/md_cg/sources.py +583 -547
  187. package/md_cg/statushdr.py +179 -0
  188. package/md_cg/stg.py +59 -18
  189. package/md_cg/subgraph.py +23 -0
  190. package/md_cg/sustain.py +74 -1
  191. package/md_cg/tasks.py +471 -0
  192. package/md_cg/test_action_derive.py +203 -0
  193. package/md_cg/test_audit_rotate.py +270 -0
  194. package/md_cg/test_autonomy.py +26 -0
  195. package/md_cg/test_bench_governance.py +102 -0
  196. package/md_cg/test_blindspot_tickets.py +166 -0
  197. package/md_cg/test_branches.py +249 -0
  198. package/md_cg/test_ccg_perturb.py +1 -1
  199. package/md_cg/test_ccgc.py +433 -0
  200. package/md_cg/test_codeindex.py +338 -0
  201. package/md_cg/test_comment_gate.py +187 -0
  202. package/md_cg/test_cond_compose_anchors.py +76 -0
  203. package/md_cg/test_condition_anchor.py +82 -0
  204. package/md_cg/test_conformance.py +343 -0
  205. package/md_cg/test_d_meta.py +412 -0
  206. package/md_cg/test_datapath_root.py +188 -0
  207. package/md_cg/test_en_pipeline.py +24 -0
  208. package/md_cg/test_gain_gate.py +47 -1
  209. package/md_cg/test_health_scale.py +173 -0
  210. package/md_cg/test_hot_cold.py +187 -0
  211. package/md_cg/test_hyperedge.py +245 -0
  212. package/md_cg/test_identity_attribution.py +6 -0
  213. package/md_cg/test_index_durability.py +224 -0
  214. package/md_cg/test_lifecycle.py +309 -0
  215. package/md_cg/test_linkref.py +306 -0
  216. package/md_cg/test_md_access_parity.py +15 -3
  217. package/md_cg/test_mr_m1.py +769 -0
  218. package/md_cg/test_mr_m2.py +587 -0
  219. package/md_cg/test_mr_m3.py +710 -0
  220. package/md_cg/test_mr_m4.py +485 -0
  221. package/md_cg/test_p1.py +1 -1
  222. package/md_cg/test_p11_consistency.py +1 -1
  223. package/md_cg/test_p12_metacognition.py +2 -2
  224. package/md_cg/test_p16_self_state.py +1 -1
  225. package/md_cg/test_p26_refindex.py +50 -21
  226. package/md_cg/test_p27_docindex.py +258 -4
  227. package/md_cg/test_p28_refcheck.py +1 -1
  228. package/md_cg/test_p29_session_ingest_export.py +1 -1
  229. package/md_cg/test_p2_mcp.py +7 -1
  230. package/md_cg/test_p31_insight.py +24 -0
  231. package/md_cg/test_p38_contextualize.py +1 -1
  232. package/md_cg/test_p39_vision_evidence.py +1 -1
  233. package/md_cg/test_p40_refine_worklist.py +1 -1
  234. package/md_cg/test_p41_evolve_patrol.py +1 -1
  235. package/md_cg/test_p42_provenance.py +1 -1
  236. package/md_cg/test_p43_pooling.py +41 -13
  237. package/md_cg/test_p44_md_whitebox.py +14 -1
  238. package/md_cg/test_protocol.py +243 -0
  239. package/md_cg/test_reach.py +378 -0
  240. package/md_cg/test_reach_keys.py +201 -0
  241. package/md_cg/test_reach_meta_exits.py +145 -0
  242. package/md_cg/test_read_clip.py +141 -0
  243. package/md_cg/test_retr_s1.py +340 -0
  244. package/md_cg/test_retr_s1b.py +209 -0
  245. package/md_cg/test_retr_s3.py +194 -0
  246. package/md_cg/test_retr_s4.py +163 -0
  247. package/md_cg/test_retr_s5.py +200 -0
  248. package/md_cg/test_retr_s6.py +157 -0
  249. package/md_cg/test_retr_s7.py +385 -0
  250. package/md_cg/test_retr_s8_time.py +316 -0
  251. package/md_cg/test_retr_s9_edges.py +286 -0
  252. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  253. package/md_cg/test_review_conformance.py +367 -0
  254. package/md_cg/test_role_views.py +354 -0
  255. package/md_cg/test_tasks.py +409 -0
  256. package/md_cg/test_tool_face.py +189 -0
  257. package/md_cg/test_trust.py +361 -0
  258. package/md_cg/test_twophase.py +286 -0
  259. package/md_cg/test_units_poll.py +71 -0
  260. package/md_cg/test_v14_fixes.py +397 -0
  261. package/md_cg/test_validity_filter.py +280 -0
  262. package/md_cg/test_wisdom_md_store.py +7 -3
  263. package/md_cg/test_writepipe.py +214 -0
  264. package/md_cg/theory.py +17 -2
  265. package/md_cg/tokens.py +134 -12
  266. package/md_cg/tool_face.py +261 -0
  267. package/md_cg/trust.py +943 -0
  268. package/md_cg/twophase.py +232 -0
  269. package/md_cg/units.py +665 -0
  270. package/md_cg/vision_evidence.py +25 -2
  271. package/md_cg/weights.py +25 -2
  272. package/md_cg/whitebox.py +33 -2
  273. package/md_cg/whitebox_kb/data/verify_cache.json +21233 -360
  274. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5124 -0
  275. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  276. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  277. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  278. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  279. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  280. package/md_cg/writelimit.py +37 -7
  281. package/md_cg/writepipe.py +543 -0
  282. package/package.json +2 -2
  283. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  284. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  285. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  286. package/src/hooks.ts +27 -22
  287. package/src/index.ts +33 -6
  288. package/src/lib/datapath.ts +211 -13
  289. package/src/lib/mdcg_client.ts +12 -8
  290. package/src/lib/mutual.ts +411 -411
  291. package/src/lib/prompt_safety.ts +62 -0
  292. package/src/lib/token_store.ts +4 -5
  293. package/zcode/AGENTS.md +21 -10
  294. package/zcode/README.md +4 -0
  295. package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
  296. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
  297. /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
  298. /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
  299. /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
  300. /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
  301. /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
  302. /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
  303. /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
  304. /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
  305. /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
  306. /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
  307. /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
  308. /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
  309. /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
  310. /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
  311. /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
  312. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
  313. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
  314. /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
  315. /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
  316. /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
  317. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
  318. /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
  319. /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
  320. /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
  321. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
  322. /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
  323. /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
  324. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
  325. /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
package/md_cg/reach.py ADDED
@@ -0,0 +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