@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
@@ -1,576 +1,576 @@
1
- # -*- coding: utf-8 -*-
2
- """公开基准评测公共层:LongMemEval-S / LoCoMo → 统一格式 → 机判指标。
3
-
4
- 评测口径(诚实条款,报告必须原样声明):
5
- * hit@K 为「证据命中」机判:Top-K 至少含一条证据 turn(LongMemEval 用
6
- answer_session_ids 映射的证据集,LoCoMo 用 qrels 标注),非 LLM-judge,
7
- 与论文口径(LLM 判 answer 文本正确)不可直接比较,仅用于系统间同口径对比。
8
- * 负例拒答(refusal):Top-1 融合分 < 阈值线判为拒答。线 = 该数据集正例
9
- hit@1 题 Top-1 分的 10 分位(各数据集独立校准),报告附校准明细与敏感性。
10
- * LongMemEval-S 实测无 abstention 题(该类仅 M 版有),负例组以 LoCoMo
11
- adversarial + 自建 bench 噪声层承担。
12
-
13
- 统一题格式(两个数据集转换后一致;转换产物为 data/external/ 下的本地派生副本):
14
- {"qid", "qtype", "question", "answer", "evidence_turns": [turn_id, ...]}
15
- 统一语料行:{"id", "text", ...}(建库时日期/说话人内联进文本供词法路召回)
16
-
17
- 被测 API:MdCGOS(root).add / .search_rrf(四路 RRF:lexical,bucket,entity,graph)。
18
- 评测库独立于主库:_md_cg_eval_longmem / _md_cg_eval_locomo(.gitignore 已盖)。
19
-
20
- 双口径(评测设计核心,报告须分栏声明):
21
- 口径 A legacy —— 原始 turn 直接入库(corpus.marks=False 同形的「旧库迁移
22
- 对照物」)。此形态下 entity 路无 tags 恒空、负路由无条件
23
- 结构恒不触发、judge 走 legacy 分支:测的是检索底座下界。
24
- 口径 B calibrated —— 确定性白箱标定器把 turn 加工为 CCG 五要素 md 条目
25
- (生效/不适用条件、子功能、执行、验证方式 + tags +
26
- condition_space),再入库。测的是记忆系统完整形态
27
- (写入标定管线 + 四路召回 + 负路由 + 四态)。
28
-
29
- 标定红线(诚实条款):
30
- * 确定性:标定器是纯规则函数,零 LLM、零第三方依赖,同输入必同输出,
31
- 标定器源码随报告公开,第三方可重放;
32
- * 盲于查询集:标定只读 turn 自身字段(说话人/时间/正文句式),不读任何
33
- question/answer 字段——否则即数据泄漏,评测作废。
34
- """
35
- import json
36
- import os
37
- import random
38
- import sys
39
- import time
40
-
41
- HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
- sys.path.insert(0, HERE)
43
-
44
- EXT = os.path.join(HERE, "data", "external")
45
- RESULTS = os.path.join(EXT, "eval_results")
46
- ROOT_LM = os.path.join(HERE, "_md_cg_eval_longmem")
47
- ROOT_LC = os.path.join(HERE, "_md_cg_eval_locomo")
48
-
49
- LM_Q = os.path.join(EXT, "longmemeval", "lme_s_questions.jsonl")
50
- LM_H = os.path.join(EXT, "longmemeval", "lme_s_haystack.jsonl")
51
- LC_Q = os.path.join(EXT, "locomo", "locomo_questions.jsonl")
52
- LC_CORPUS = os.path.join(EXT, "locomo", "locomo_corpus.jsonl")
53
-
54
- ROOT_LM_CAL = ROOT_LM + "_cal" # 口径 B 独立库(标定形态)
55
- ROOT_LC_CAL = ROOT_LC + "_cal"
56
-
57
- PATHS = ("lexical", "bucket", "entity", "graph") # 引擎默认四路 RRF
58
- PATHS_CAL = PATHS + ("semantic",) # 口径 B:显式启用条件结构路(负路由所在)
59
-
60
-
61
- # 生效条件:仅评测口径调用(主库不调用);必须双改 md_cg.mdcg 与 md_cg.mdcos 两份 from-import 的同名值(漏改其一即静默失效),置为 10**9;无返回值、不落盘;
62
- def unlock_global_cap():
63
- """评测口径:解除 GLOBAL_CAP 截断(bench_membench patch_lexical_full 同法)。
64
-
65
- 【2026-09-16 后语义更新】截断依据已由「插入序」改为「相关度」(`mdcg
66
- .cut_by_relevance` 与 `mdcos._lexical` 均先全量打分再排序截断,cap 值仍
67
- 500)——插入序 lottery 已消除。评测仍解除 cap 的理由只剩一条:避免
68
- 「按相关度截掉尾部」在长语料上压低召回天花板(相近似 turn 数千条时,
69
- 排 501 名之后仍可能是证据)——评测要测**排序质量**而非截断策略。
70
- mdcg 与 mdcos 各持一份 from-import 值,必须双改(test_p43_pooling 先例)。
71
- """
72
- import md_cg.mdcg as m
73
- import md_cg.mdcos as mo
74
- m.GLOBAL_CAP = 10 ** 9
75
- mo.GLOBAL_CAP = 10 ** 9
76
-
77
-
78
- # 生效条件:仅评测口径调用(短条目语料下 jaccard 会退化);把 md_cg.mdcg.SCORE_MODE 置为 "jaccard"(mdcos 读同一份、无需双改);无返回值、不落盘;
79
- def use_jaccard():
80
- """评测口径:词法打分切 jaccard(对称归一化,长度自惩罚)。
81
-
82
- 默认 legacy(|qb∩db|/|qb|)只归一化查询侧——长 turn 语料里证据被长文档
83
- 挤出 Top-5,LongMemEval-S 上 precise/temporal/reference 的 hit@1 全为 0%。
84
- 切 jaccard 后同口径实测:precise 0%→8.97% / temporal 0%→6.77% /
85
- interference 1.28%→20.51% / reference 0%→3.76%(与 Rust
86
- `mdcg-eval --score jaccard` 及既有 trec_longmem_jac.json 逐位一致)。
87
-
88
- 只改 mdcg 一处即可(不像 GLOBAL_CAP 被 mdcos from-import 成副本):
89
- `lexical_sim` 在自身模块内引用全局 SCORE_MODE,且 mdcos 的 MdCGOS 继承
90
- MdCG._score,读的是同一份。
91
-
92
- **仅作评测口径**:主库(短条目)不切——短文档上 jaccard 会退化
93
- (test_p17_predict G4:可预测锚点由因果起点 a 错配到语义邻居 x)。
94
- """
95
- import md_cg.mdcg as m
96
- m.SCORE_MODE = "jaccard"
97
-
98
-
99
- # 生效条件:对 path 调用 open(path, encoding="utf-8") 后逐行读取,仅对 strip 后非空的 line yield json.loads(line);
100
- def iter_jsonl(path):
101
- with open(path, encoding="utf-8") as f:
102
- for line in f:
103
- line = line.strip()
104
- if line:
105
- yield json.loads(line)
106
-
107
-
108
- # 生效条件:ds 等于 "lm" 时 path=LM_Q,否则 path=LC_Q;先由 iter_jsonl(path) 取全部 rows,再仅当 qtypes 为真值时保留 r["qtype"] 在 qtypes 中的行(qtypes 为 None/空串/空容器等假值则不过滤);
109
- def load_questions(ds, qtypes=None):
110
- """ds: 'lm' | 'lc'。qtypes 过滤题型(None=全部)。"""
111
- path = LM_Q if ds == "lm" else LC_Q
112
- rows = list(iter_jsonl(path))
113
- if qtypes:
114
- rows = [r for r in rows if r["qtype"] in qtypes]
115
- return rows
116
-
117
-
118
- # 生效条件:对 rows/n(seed 默认 7),若 not n 或 n >= len(rows) 则原样返回 rows,否则返回 random.Random(seed).sample(rows, n);
119
- def sample_questions(rows, n, seed=7):
120
- if not n or n >= len(rows):
121
- return rows
122
- return random.Random(seed).sample(rows, n)
123
-
124
-
125
- # ------------------------------------------------------------------ 建库
126
-
127
- # 生效条件:对 t,若 t.get("speaker") 为真值则返回 f"{t['speaker']}: {t['text']} [{t['date']}]",否则返回 f"{t['text']} [{t['date']}]";
128
- def lm_turn_text(t):
129
- """LongMemEval turn → 入库文本(日期内联,词法路可召回时间词)。"""
130
- return f"{t['speaker']}: {t['text']} [{t['date']}]" if t.get("speaker") \
131
- else f"{t['text']} [{t['date']}]"
132
-
133
-
134
- # 生效条件:对 r,若 r.get("title") 为真值则返回 f"{r['text']} [{r['title']}]",否则返回 r["text"];
135
- def lc_turn_text(r):
136
- """LoCoMo turn → 入库文本(title 含 Data time 时间戳,内联保序)。"""
137
- return f"{r['text']} [{r['title']}]" if r.get("title") else r["text"]
138
-
139
-
140
- # ---------------- 白箱标定器(口径 B;纯规则、确定性、盲于查询集) ----------------
141
-
142
- # 句首功能词表:`[A-Z][a-z]+` 实体近似会把句首大写词误抽为实体,予以排除。
143
- # 词表固定随报告公开——它决定实体抽取边界,属标定器配置的一部分。
144
- _STOP_HEADS = frozenset(
145
- "I We You They He She It This That There Then So But And Or What When Where "
146
- "Why How Did Do Does Have Has Had Will Would Can Could Should My Your Our His "
147
- "Her Its Their In On At To For With From About After Before During Is Am Are "
148
- "Was Were The A An That's Let's Yeah Okay Yes No Well Now Just Really Maybe "
149
- "Let Get Got Know Think Want Need Make Take Say Said Tell Ask See Look Come "
150
- "Go Going If Because When While How's What's Where's Who's".split())
151
-
152
- # 强否定标记:原文含它才生成不适用条件(弱否定不生成,宁缺勿造——
153
- # forgetting.payload 已证明模板骨架会虚高重复度且无判别力)。
154
- _NEG_MARKS = ("n't", " not ", " never ", "nobody", "nothing", "no one", "none of")
155
-
156
-
157
- # 生效条件:对 text(limit 默认 4),按正则收集非 _STOP_HEADS 开头的大写词与 4 位年/时刻/日期数字并去重后,返回前 limit 项;
158
- def _extract_entities(text, limit=4):
159
- """确定性实体近似:连续大写词(人名/地名)+ 日期/时间数字。只读 turn 正文。
160
- 纯数字编号("1.")不是实体——只保留年份/时刻/日期形态,否则列表编号会
161
- 批量污染 tags 与生效条件行(实测教训)。"""
162
- import re
163
- ents = []
164
- for m in re.findall(r"[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?", text):
165
- if m.split()[0] not in _STOP_HEADS and m not in ents:
166
- ents.append(m)
167
- for m in re.findall(r"\d{4}|\d{1,2}:\d{2}|\d{1,2}/\d{1,2}(?:/\d{2,4})?", text):
168
- if m not in ents:
169
- ents.append(m)
170
- return ents[:limit]
171
-
172
-
173
- # 生效条件:对 title,若 title 为假值则返回空串,否则按 YYYY-MM-DD、英文星期日期、d/d/d 三个正则顺序取首个匹配串,无匹配返回空串;
174
- def _date_tag(title):
175
- """从 title 抽规范化日期串做 tag。整段 title 入 tags 会污染实体路
176
- (_path_entity 的 `query in t` 方向:长 tag 是误报源)。"""
177
- if not title:
178
- return ""
179
- import re
180
- m = (re.search(r"\d{4}-\d{2}-\d{2}", title)
181
- or re.search(r"[A-Za-z]+day\s+\d{1,2}\s+[A-Za-z]+,?\s*\d{4}", title)
182
- or re.search(r"\d{1,2}/\d{1,2}/\d{2,4}", title))
183
- return m.group(0) if m else ""
184
-
185
-
186
- # 生效条件:r 为语料 turn 行(缺 text/role 等键时按空串处理);date_key/title_key 为 None 时对应日期取空串;返回 (CCG 五要素正文, tags, condition_space) 三元组,仅当原文出现强否定标记才生成不适用条件、否则显式写「(无)」;
187
- def calibrate_turn(r, date_key=None, title_key=None):
188
- """turn 行 → (CCG 五要素正文, tags, condition_space)。
189
-
190
- v2 归因修正:自建语料 100% 召回的机制条件是「查询-证据词面同源」
191
- (对照实验:节点特异词在查询中出现 → 100%;全同质问法 → 11.9%)。
192
- 因此——
193
- 1. 特异词(说话人/日期/实体)必须进五要素**每一行**(词法路对全文打
194
- Jaccard:条件行的特异词是净增益,同质模板词是净稀释——v1 抽样
195
- 3.3% < legacy 16% 的根源,与 corpus.body 把 dom/point/aspect 内嵌
196
- 每一行同构);
197
- 2. 模板骨架压到最短(corpus.body 固定尾巴仅 12 字,v1 有 30+ 字同质词);
198
- 3. 五要素行承担条件结构职责(semantic 路/judge 读),不承诺解决
199
- 查询-证据表述鸿沟——那是公开数据集词法召回的真实天花板。
200
- tags 用裸词(说话人/日期/实体):_path_entity 的匹配是 `t in query or
201
- query in t`,带 `ent:` 前缀的 tag 在自然语言查询下永不命中(实测教训,
202
- corpus 的 `domain:` 桶 tag 是给 bucket/条件路由用的,两者用途不同)。
203
- 不适用条件仅当原文含强否定标记时生成(对 adversarial「是否说过」类问题
204
- 有真实判别力);否则显式写「(无)」。
205
- """
206
- import re
207
- text = str(r.get("text") or "").strip()
208
- speaker = str(r.get("speaker") or "").strip()
209
- date = str(r.get(date_key) or "").strip() if date_key else ""
210
- title = str(r.get(title_key) or "").strip() if title_key else ""
211
- ents = [e for e in _extract_entities(text) if e != speaker]
212
- who = speaker or (next((e for e in ents if not e[0].isdigit()), None) or "匿名")
213
- dtag = date or _date_tag(title)
214
- low = f" {text.lower()} "
215
- neg = next((m for m in _NEG_MARKS if m in low), None)
216
- when = f"于{dtag}" if dtag else ""
217
- ents_head = "/".join(ents[:2])
218
-
219
- body = (
220
- f"# 功能名:{who}·会话记忆{when}\n"
221
- f"# 生效条件:查询涉及「{who}」{when}"
222
- + (f"所述「{ents_head}」" if ents_head else "") + "时生效\n"
223
- f"# 子功能:{who}的会话条目{when}\n"
224
- f"# 执行:{who}会话原文回溯\n"
225
- f"# 验证方式:data\n"
226
- f"# 不适用条件:"
227
- + (f"断言与原文相反(含{neg.strip()}语义)" if neg else "(无)") + "\n\n"
228
- + text
229
- )
230
- tags = [t for t in (who, dtag) if t] + ents[:3]
231
- cond_space = {
232
- "observation_position": f"{who} 的会话陈述" + (f"({dtag})" if dtag else ""),
233
- "observation_tool": "会话记录",
234
- "existence_constraint": "公开",
235
- }
236
- return body, tags, cond_space
237
-
238
-
239
- # 生效条件:任意 t 与可选 ctx(默认 None)传入即原样转调 calibrate_turn(t, date_key="date", ctx=ctx),返回其结果;
240
- def calibrate_lm_turn(t, ctx=None):
241
- return calibrate_turn(t, date_key="date", ctx=ctx)
242
-
243
-
244
- # 生效条件:任意 r 与可选 ctx(默认 None)传入即原样转调 calibrate_turn(r, title_key="title", ctx=ctx),返回其结果;
245
- def calibrate_lc_turn(r, ctx=None):
246
- return calibrate_turn(r, title_key="title", ctx=ctx)
247
-
248
-
249
- # ============ 写入时加工管线(v3)——「查询时的泛化提前到写入时完成」 ============
250
- # 外部对标结论:检索只是链路最后一跳;完整系统在写入时做实体规范化/意图抽象/
251
- # 指代消解,把语料加工成可沿链行走的形态。以下三步全部是**确定性规则近似**
252
- # (纯标准库、盲于查询集、随报告公开词表),不引入任何模型依赖。
253
-
254
- # 生效条件:对 corpus_path(min_df 默认 3),逐行取 text=str(r.get("text") or "")、spk=str(r.get("speaker") or "").strip(),以 {spk}(spk 真值)或空集合并入正文大写词首词(排除 _STOP_HEADS)更新 df 计数,最后返回计数 >= min_df 的词集合;
255
- def build_canon(corpus_path, min_df=3):
256
- """离线实体规范化:全语料扫一遍,聚合大写词/人名的文档频次(df)。
257
- df≥min_df 的是 canonical 实体(会话成员 + 高频专名)——单 turn 局部抽取
258
- 看不见「Alice 在 200 条 turn 里反复出现」这个全库事实,这正是写入时
259
- (offline)加工区别于查询时(online)匹配的价值所在。"""
260
- from collections import Counter
261
- df = Counter()
262
- import re as _re
263
- for r in iter_jsonl(corpus_path):
264
- text = str(r.get("text") or "")
265
- spk = str(r.get("speaker") or "").strip()
266
- ws = {spk} if spk else set()
267
- ws |= set(_re.findall(r"[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?", text))
268
- ws = {w.split()[0] for w in ws if w.split()[0] not in _STOP_HEADS}
269
- df.update(ws)
270
- return {w for w, c in df.items() if c >= min_df}
271
-
272
-
273
- # 意图槽位(离线意图抽象的确定性近似):英文动词短语原样入条件行——
274
- # 保持词面同源(查询 "What did Alice plan" 与条件行 "plan" 直接 bigram 命中),
275
- # 不翻译成中文(同义词表是中文,翻译即断链)。三类槽覆盖 LoCoMo 问题分布:
276
- # 计划/过去事件/偏好。
277
- _INTENT_HEADS = ("plan", "going to", "thinking of", "thinking about", "want to",
278
- "will be", "need to", "have to", "decide", "suggest",
279
- "remember", "talked about", "talk about", "mention",
280
- "last time", "tell you", "told you", "hear",
281
- "prefer", "favorite", "love", "hate", "enjoy")
282
-
283
-
284
- # 生效条件:对 text 小写并前后加空格得到 low,返回 _INTENT_HEADS 中满足 f" {h}" 出现在 low 的项;
285
- def _intent_of(text):
286
- low = f" {text.lower()} "
287
- return [h for h in _INTENT_HEADS if f" {h}" in low]
288
-
289
-
290
- # 生效条件:r 为语料 turn 行;ctx 为写入时全库视图(canon/intents/last_canon),ctx 为 None 时按空视图处理并退化为 v2 行为;返回 (CCG 五要素正文, tags, condition_space) 三元组;
291
- def calibrate_turn(r, date_key=None, title_key=None, ctx=None):
292
- """turn 行 → (CCG 五要素正文, tags, condition_space)。
293
-
294
- v3(写入时加工管线):在 v2(特异词进每一行)之上叠加三步离线加工,
295
- ctx 携带全库视图:
296
- - ctx["canon"]:canonical 实体表(build_canon 产物)。条目涉及的
297
- canonical 实体进生效条件行——查询以人名开头时,词法/实体路沿
298
- 规范化实体直接落到「该人名相关条目」集合(链路第一跳)。
299
- - ctx["intents"]:意图槽位短语进子功能行——意图抽象的词面同源版。
300
- - ctx["last_canon"]:前一条的 canonical 实体(写入时序状态)。本条
301
- 无任何实体而前条有 → 条件行附「承接 {last}」——保守指代消解:
302
- 只附加假设、不删原文(判别力交给 RRF 多路共识,虚报风险可控)。
303
- tags 用裸词(说话人/日期/实体):_path_entity 的匹配是 `t in query or
304
- query in t`,带 `ent:` 前缀的 tag 在自然语言查询下永不命中(实测教训)。
305
- 不适用条件仅当原文含强否定标记时生成;否则显式写「(无)」。
306
- """
307
- import re
308
- text = str(r.get("text") or "").strip()
309
- speaker = str(r.get("speaker") or "").strip()
310
- date = str(r.get(date_key) or "").strip() if date_key else ""
311
- title = str(r.get(title_key) or "").strip() if title_key else ""
312
- canon = (ctx or {}).get("canon") or frozenset()
313
- ctx_ents = _extract_entities(text)
314
- canon_hits = [e for e in dict.fromkeys(
315
- [speaker] if speaker else []) if e in canon]
316
- if canon:
317
- local = re.findall(r"[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?", text)
318
- canon_hits += [w for w in dict.fromkeys(w.split()[0] for w in local)
319
- if w in canon and w not in canon_hits]
320
- else:
321
- canon_hits += [e for e in ctx_ents if e != speaker]
322
- last = (ctx or {}).get("last_canon")
323
- if not canon_hits and last:
324
- canon_hits = list(last)
325
- who = speaker or (next((e for e in canon_hits if not e[0].isdigit()), None)
326
- or "匿名")
327
- dtag = date or _date_tag(title)
328
- low = f" {text.lower()} "
329
- neg = next((m for m in _NEG_MARKS if m in low), None)
330
- when = f"于{dtag}" if dtag else ""
331
- ent_head = "/".join(canon_hits[:3])
332
- intents = _intent_of(text)
333
-
334
- body = (
335
- f"# 功能名:{who}·会话记忆{when}\n"
336
- f"# 生效条件:查询涉及「{ent_head or who}」{when}时生效\n"
337
- f"# 子功能:{who}的会话条目"
338
- + (f"(意图:{'/'.join(intents[:3])})" if intents else "") + "\n"
339
- f"# 执行:{who}会话原文回溯\n"
340
- f"# 验证方式:data\n"
341
- f"# 不适用条件:"
342
- + (f"断言与原文相反(含{neg.strip()}语义)" if neg else "(无)") + "\n\n"
343
- + text
344
- )
345
- tags = [t for t in (who, dtag) if t] + canon_hits[:3]
346
- cond_space = {
347
- "observation_position": f"{who} 的会话陈述" + (f"({dtag})" if dtag else ""),
348
- "observation_tool": "会话记录",
349
- "existence_constraint": "公开",
350
- }
351
- return body, tags, cond_space
352
-
353
-
354
- # 生效条件:rebuild=True 且 os.path.isdir(root) 时先 rmtree(root);cg_cls 为假值(如 None)时回落 MdCGOS(root, autoflush=500);语料行数 n_rows ≤ cg.index["nodes"] 现有节点数(n_rows=0 的空语料也满足)时直接复用返回 cg;否则逐行写入——calib_of 为真走标定口径 B(先 build_canon,calib_of(r, {"canon": canon, "last_canon": last_canon}) 取 body/tags/condition_space),calib_of 为假(含 None)走 legacy 口径 A(用 text_of(r)),id 已在 cg.index["nodes"] 的行跳过,verbose 为真时每 20000 行打印进度,循环后 cg.flush() 再返回 cg;
355
- def build_eval_cg(cg_cls, root, corpus_path, text_of, src_tag, verbose=True,
356
- calib_of=None, rebuild=False):
357
- """幂等建库:节点数已达语料行数则直接复用。返回 cg(不 flush 句柄)。
358
-
359
- calib_of=None → 口径 A(legacy 裸文本);传入 calibrate_*_turn → 口径 B
360
- (五要素标定条目 + tags + condition_space)。两口径用不同 root,互不污染。
361
- rebuild=True 删库重建(标定器改版后必须重建——幂等按节点数判断,识别不了
362
- 同节点数的旧版标定条目)。
363
- v3:标定口径先全语料 build_canon(离线实体规范化),写入循环维护
364
- last_canon(时序状态,供保守指代承接)——两遍扫描,仍 O(N)。
365
- """
366
- from md_cg.mdcos import MdCGOS
367
- if rebuild and os.path.isdir(root):
368
- import shutil
369
- shutil.rmtree(root, ignore_errors=True)
370
- n_rows = sum(1 for _ in iter_jsonl(corpus_path))
371
- cg = cg_cls(root, autoflush=500) if cg_cls else MdCGOS(root, autoflush=500)
372
- have = len(cg.index["nodes"])
373
- if have >= n_rows:
374
- if verbose:
375
- print(f" 建库复用 {root}:{have} 节点(语料 {n_rows} 行)")
376
- return cg
377
- canon = build_canon(corpus_path) if calib_of else frozenset()
378
- if verbose and calib_of:
379
- print(f" 写入时加工:canonical 实体 {len(canon)} 个(df≥3)")
380
- if verbose:
381
- print(f" 建库 {root}:已有 {have},语料 {n_rows} 行,开始写入 "
382
- f"({'标定口径 B' if calib_of else 'legacy 口径 A'})…")
383
- t0 = time.time()
384
- last_canon = None
385
- for i, r in enumerate(iter_jsonl(corpus_path), 1):
386
- if r["id"] in cg.index["nodes"]:
387
- continue
388
- if calib_of:
389
- body, tags, cond_space = calib_of(r, {"canon": canon,
390
- "last_canon": last_canon})
391
- cg.add(r["id"], body, layer="contextual", tags=tags,
392
- condition_space=cond_space, eval_src=src_tag,
393
- verification_basis="data")
394
- hits = [t for t in tags[1:] if not t.replace(".", "")
395
- .replace(":", "").isdigit()]
396
- last_canon = hits or last_canon
397
- else:
398
- cg.add(r["id"], text_of(r), layer="contextual",
399
- eval_src=src_tag, verification_basis="data")
400
- if verbose and i % 20000 == 0:
401
- print(f" … {i}/{n_rows}({time.time() - t0:.0f}s)")
402
- cg.flush()
403
- if verbose:
404
- print(f" 建库完成:{len(cg.index['nodes'])} 节点,{time.time() - t0:.0f}s")
405
- return cg
406
-
407
-
408
- # 生效条件:对传入 cg,用闭包 cache(键为 entry["path"])包装其原 cg._read 并赋回 cg._read,返回该 cache;仅当 p = entry["path"] 不在 cache 时调用 orig(entry) 并缓存其(含假值)结果,p 已在 cache 中时直接返回缓存值;
409
- def install_read_cache(cg):
410
- """评测只读:节点文件读进内存,避免逐次检索重复磁盘 I/O。"""
411
- cache = {}
412
- orig = cg._read
413
-
414
- # 生效条件:entry["path"] 未在闭包 cache 中时调用 orig(entry) 存入并返回,已在 cache 中则直接返回缓存值(cache 与原 _read 由外层 install_read_cache 提供);
415
- def _cached(entry):
416
- p = entry["path"]
417
- if p not in cache:
418
- cache[p] = orig(entry)
419
- return cache[p]
420
-
421
- cg._read = _cached
422
- return cache
423
-
424
-
425
- # ------------------------------------------------------------------ 检索与指标
426
-
427
- # 生效条件:对 cg/query(k 默认 5、paths 默认 PATHS、judge 默认 False),若 fusion 为真值则加入 kw["fusion"],若 path_weights 为真值则加入 kw["path_weights"],若 context is not None(含空 dict/空串)则加入 kw["context"],再调用 cg.search_rrf(...);
428
- def run_query(cg, query, k=5, paths=PATHS, judge=False, fusion=None,
429
- path_weights=None, context=None):
430
- """单查询,返回 (结果四元组列表, meta)。
431
- fusion/path_weights 覆盖融合方式与路权重(按路拆解归因用)。
432
-
433
- context 是**条件约束(bucket)路可被开启的唯一入口**:mdcos._path_bucket 在
434
- `context is None` 时直接 `return []`(mdcos.py:435),而 search_rrf 的默认
435
- paths 里恰恰含 "bucket"——即「名义四路、实际三路」。传 context 之前,全部
436
- 既有四路 RRF 口径的评测都未曾开启过条件路由(先例:四轴消融 + Rust 空壳)。
437
- """
438
- kw = {}
439
- if fusion:
440
- kw["fusion"] = fusion
441
- if path_weights:
442
- kw["path_weights"] = path_weights
443
- if context is not None:
444
- kw["context"] = context
445
- return cg.search_rrf(query, k=k, paths=paths, judge=judge, record=False, **kw)
446
-
447
-
448
- # 生效条件:当 evidence 为真值且 res 中某 r 的 r[0]["id"] 属于 evidence 时返回首个 1-based 排名,否则(evidence 为假值或未命中)返回 0;
449
- def first_evidence_rank(res, evidence):
450
- """首个证据 turn 的排名(1-based;未命中 0)。"""
451
- if not evidence:
452
- return 0
453
- for i, r in enumerate(res, 1):
454
- if r[0]["id"] in evidence:
455
- return i
456
- return 0
457
-
458
-
459
- # 生效条件:对 questions 中每个 it,若 context_of 为真值则 ctx=context_of(it) 否则 ctx=None;以 k/paths/judge/fusion/path_weights 调用 run_query,按 evidence_turns 算 first_evidence_rank,收集 qid/qtype/rank/top1_score/n_res 行,返回 rows;
460
- def evaluate_group(cg, questions, k=5, paths=PATHS, judge=False, verbose=True,
461
- fusion=None, path_weights=None, context_of=None):
462
- """一组题 → per-question 明细行。负例组传 judge=False(拒答看分数线)。
463
-
464
- context_of: 可调用 `q -> context dict`(返回 None 表示该题不下发情境)。
465
- 缺省 None → 全组不传 context(**既有行为完全不变**)。传入即开启条件路由,
466
- 是「条件约束路的净效应」单变量实验的开关。
467
- """
468
- rows = []
469
- t0 = time.time()
470
- for i, it in enumerate(questions, 1):
471
- ctx = context_of(it) if context_of else None
472
- res, meta = run_query(cg, it["question"], k=k, paths=paths, judge=judge,
473
- fusion=fusion, path_weights=path_weights,
474
- context=ctx)
475
- rank = first_evidence_rank(res, set(it.get("evidence_turns") or []))
476
- rows.append({
477
- "qid": it["qid"], "qtype": it["qtype"],
478
- "rank": rank, "top1_score": res[0][1] if res else 0.0,
479
- "n_res": len(res),
480
- })
481
- if verbose and i % 100 == 0:
482
- print(f" 查询 {i}/{len(questions)}({time.time() - t0:.0f}s)")
483
- return rows
484
-
485
-
486
- # 生效条件:当 rows 为真值时按 k(默认 5)计算 hit@1、hit@k、MRR、score_p10/p50 及 by_qtype 指标,否则返回 {"n": 0};
487
- def summarize(rows, k=5):
488
- """hit@1 / hit@K / MRR + 分题型。score_p10 供拒答线校准。"""
489
- if not rows:
490
- return {"n": 0}
491
- hit1 = sum(1 for r in rows if r["rank"] == 1)
492
- hitk = sum(1 for r in rows if 0 < r["rank"] <= k)
493
- mrr = sum(1.0 / r["rank"] for r in rows if r["rank"]) / len(rows)
494
- top1 = sorted(r["top1_score"] for r in rows)
495
- out = {
496
- "n": len(rows),
497
- "hit@1": hit1 / len(rows),
498
- f"hit@{k}": hitk / len(rows),
499
- "mrr": mrr,
500
- "score_p10": top1[max(0, len(top1) // 10)],
501
- "score_p50": top1[len(top1) // 2],
502
- }
503
- by = {}
504
- for r in rows:
505
- by.setdefault(r["qtype"], []).append(r)
506
- out["by_qtype"] = {
507
- qt: {
508
- "n": len(g),
509
- "hit@1": sum(1 for r in g if r["rank"] == 1) / len(g),
510
- f"hit@{k}": sum(1 for r in g if 0 < r["rank"] <= k) / len(g),
511
- "mrr": sum(1.0 / r["rank"] for r in g if r["rank"]) / len(g),
512
- } for qt, g in sorted(by.items())
513
- }
514
- return out
515
-
516
-
517
- # 生效条件:当 rows 非空时按 r["top1_score"] < line 统计拒答数并返回拒绝率,否则返回 {"n": 0};
518
- def refusal_metrics(rows, line):
519
- """负例组:Top-1 分 < line 记为拒答。返回拒答率与明细统计。"""
520
- n = len(rows)
521
- if not n:
522
- return {"n": 0}
523
- ref = sum(1 for r in rows if r["top1_score"] < line)
524
- return {"n": n, "refusal_rate": ref / n,
525
- "refused": ref, "line": line}
526
-
527
-
528
- # 生效条件:当 rows 非空时返回 r["top1_score"] < line 的行占比,否则返回 0.0;
529
- def false_refusal_rate(rows, line):
530
- """正例误杀(被错误拒答的正例比例):正例 Top-1 分 < line。"""
531
- n = len(rows)
532
- if not n:
533
- return 0.0
534
- return sum(1 for r in rows if r["top1_score"] < line) / n
535
-
536
-
537
- # 生效条件:当 pos_rows 中存在 rank==1 的行时,返回这些行 top1_score 升序后 max(0, len//10) 索引处的值,否则返回 0.0;
538
- def calibrate_line(pos_rows):
539
- """拒答线 = 正例 hit@1 题 Top-1 分的 10 分位。"""
540
- scores = sorted(r["top1_score"] for r in pos_rows if r["rank"] == 1)
541
- if not scores:
542
- return 0.0
543
- return scores[max(0, len(scores) // 10)]
544
-
545
-
546
- # 生效条件:当 name/payload 给出时,确保 RESULTS 目录(os.makedirs(RESULTS, exist_ok=True)),将 payload 以 ensure_ascii=False、indent=2 写入 RESULTS/name 并返回 path;
547
- def save_result(name, payload):
548
- """评测结果 JSON 落盘(报告数据源)。"""
549
- os.makedirs(RESULTS, exist_ok=True)
550
- path = os.path.join(RESULTS, name)
551
- with open(path, "w", encoding="utf-8") as f:
552
- json.dump(payload, f, ensure_ascii=False, indent=2)
553
- print(f" 结果 → {path}")
554
- return path
555
-
556
-
557
- # 生效条件:对 groups(k 默认 5),若某组 s 的 s.get("n") 为假(缺 n 或 n=0)则打印 0 行并跳过,否则按 s["hit@1"]、s[f"hit@{k}"]、s["mrr"] 打印并遍历 s.get("by_qtype", {}) 输出子行;
558
- def print_table(title, groups, k=5):
559
- """groups: {组名: summarize 输出}。"""
560
- print(f"\n== {title}(证据命中口径,k={k})==")
561
- print(f"{'组':<28}{'n':>6}{'hit@1':>9}{f'hit@{k}':>9}{'MRR':>8}")
562
- print("-" * 62)
563
- for name, s in groups.items():
564
- if not s.get("n"):
565
- print(f"{name:<28}{0:>6} -")
566
- continue
567
- print(f"{name:<28}{s['n']:>6}{s['hit@1']:>9.1%}"
568
- f"{s[f'hit@{k}']:>9.1%}{s['mrr']:>8.3f}")
569
- for qt, st in s.get("by_qtype", {}).items():
570
- print(f" ├ {qt:<24}{st['n']:>6}{st['hit@1']:>9.1%}"
571
- f"{st[f'hit@{k}']:>9.1%}{st['mrr']:>8.3f}")
572
-
573
-
574
- # 生效条件:对任意 x,返回 f"{x * 100:.1f}%" 的百分比字符串;
575
- def pct(x):
1
+ # -*- coding: utf-8 -*-
2
+ """公开基准评测公共层:LongMemEval-S / LoCoMo → 统一格式 → 机判指标。
3
+
4
+ 评测口径(诚实条款,报告必须原样声明):
5
+ * hit@K 为「证据命中」机判:Top-K 至少含一条证据 turn(LongMemEval 用
6
+ answer_session_ids 映射的证据集,LoCoMo 用 qrels 标注),非 LLM-judge,
7
+ 与论文口径(LLM 判 answer 文本正确)不可直接比较,仅用于系统间同口径对比。
8
+ * 负例拒答(refusal):Top-1 融合分 < 阈值线判为拒答。线 = 该数据集正例
9
+ hit@1 题 Top-1 分的 10 分位(各数据集独立校准),报告附校准明细与敏感性。
10
+ * LongMemEval-S 实测无 abstention 题(该类仅 M 版有),负例组以 LoCoMo
11
+ adversarial + 自建 bench 噪声层承担。
12
+
13
+ 统一题格式(两个数据集转换后一致;转换产物为 data/external/ 下的本地派生副本):
14
+ {"qid", "qtype", "question", "answer", "evidence_turns": [turn_id, ...]}
15
+ 统一语料行:{"id", "text", ...}(建库时日期/说话人内联进文本供词法路召回)
16
+
17
+ 被测 API:MdCGOS(root).add / .search_rrf(四路 RRF:lexical,bucket,entity,graph)。
18
+ 评测库独立于主库:_md_cg_eval_longmem / _md_cg_eval_locomo(.gitignore 已盖)。
19
+
20
+ 双口径(评测设计核心,报告须分栏声明):
21
+ 口径 A legacy —— 原始 turn 直接入库(corpus.marks=False 同形的「旧库迁移
22
+ 对照物」)。此形态下 entity 路无 tags 恒空、负路由无条件
23
+ 结构恒不触发、judge 走 legacy 分支:测的是检索底座下界。
24
+ 口径 B calibrated —— 确定性白箱标定器把 turn 加工为 CCG 五要素 md 条目
25
+ (生效/不适用条件、子功能、执行、验证方式 + tags +
26
+ condition_space),再入库。测的是记忆系统完整形态
27
+ (写入标定管线 + 四路召回 + 负路由 + 四态)。
28
+
29
+ 标定红线(诚实条款):
30
+ * 确定性:标定器是纯规则函数,零 LLM、零第三方依赖,同输入必同输出,
31
+ 标定器源码随报告公开,第三方可重放;
32
+ * 盲于查询集:标定只读 turn 自身字段(说话人/时间/正文句式),不读任何
33
+ question/answer 字段——否则即数据泄漏,评测作废。
34
+ """
35
+ import json
36
+ import os
37
+ import random
38
+ import sys
39
+ import time
40
+
41
+ HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
+ sys.path.insert(0, HERE)
43
+
44
+ EXT = os.path.join(HERE, "data", "external")
45
+ RESULTS = os.path.join(EXT, "eval_results")
46
+ ROOT_LM = os.path.join(HERE, "_md_cg_eval_longmem")
47
+ ROOT_LC = os.path.join(HERE, "_md_cg_eval_locomo")
48
+
49
+ LM_Q = os.path.join(EXT, "longmemeval", "lme_s_questions.jsonl")
50
+ LM_H = os.path.join(EXT, "longmemeval", "lme_s_haystack.jsonl")
51
+ LC_Q = os.path.join(EXT, "locomo", "locomo_questions.jsonl")
52
+ LC_CORPUS = os.path.join(EXT, "locomo", "locomo_corpus.jsonl")
53
+
54
+ ROOT_LM_CAL = ROOT_LM + "_cal" # 口径 B 独立库(标定形态)
55
+ ROOT_LC_CAL = ROOT_LC + "_cal"
56
+
57
+ PATHS = ("lexical", "bucket", "entity", "graph") # 引擎默认四路 RRF
58
+ PATHS_CAL = PATHS + ("semantic",) # 口径 B:显式启用条件结构路(负路由所在)
59
+
60
+
61
+ # 生效条件:仅评测口径调用(主库不调用);必须双改 md_cg.mdcg 与 md_cg.mdcos 两份 from-import 的同名值(漏改其一即静默失效),置为 10**9;无返回值、不落盘;
62
+ def unlock_global_cap():
63
+ """评测口径:解除 GLOBAL_CAP 截断(bench_membench patch_lexical_full 同法)。
64
+
65
+ 【2026-09-16 后语义更新】截断依据已由「插入序」改为「相关度」(`mdcg
66
+ .cut_by_relevance` 与 `mdcos._lexical` 均先全量打分再排序截断,cap 值仍
67
+ 500)——插入序 lottery 已消除。评测仍解除 cap 的理由只剩一条:避免
68
+ 「按相关度截掉尾部」在长语料上压低召回天花板(相近似 turn 数千条时,
69
+ 排 501 名之后仍可能是证据)——评测要测**排序质量**而非截断策略。
70
+ mdcg 与 mdcos 各持一份 from-import 值,必须双改(test_p43_pooling 先例)。
71
+ """
72
+ import md_cg.mdcg as m
73
+ import md_cg.mdcos as mo
74
+ m.GLOBAL_CAP = 10 ** 9
75
+ mo.GLOBAL_CAP = 10 ** 9
76
+
77
+
78
+ # 生效条件:仅评测口径调用(短条目语料下 jaccard 会退化);把 md_cg.mdcg.SCORE_MODE 置为 "jaccard"(mdcos 读同一份、无需双改);无返回值、不落盘;
79
+ def use_jaccard():
80
+ """评测口径:词法打分切 jaccard(对称归一化,长度自惩罚)。
81
+
82
+ 默认 legacy(|qb∩db|/|qb|)只归一化查询侧——长 turn 语料里证据被长文档
83
+ 挤出 Top-5,LongMemEval-S 上 precise/temporal/reference 的 hit@1 全为 0%。
84
+ 切 jaccard 后同口径实测:precise 0%→8.97% / temporal 0%→6.77% /
85
+ interference 1.28%→20.51% / reference 0%→3.76%(与 Rust
86
+ `mdcg-eval --score jaccard` 及既有 trec_longmem_jac.json 逐位一致)。
87
+
88
+ 只改 mdcg 一处即可(不像 GLOBAL_CAP 被 mdcos from-import 成副本):
89
+ `lexical_sim` 在自身模块内引用全局 SCORE_MODE,且 mdcos 的 MdCGOS 继承
90
+ MdCG._score,读的是同一份。
91
+
92
+ **仅作评测口径**:主库(短条目)不切——短文档上 jaccard 会退化
93
+ (test_p17_predict G4:可预测锚点由因果起点 a 错配到语义邻居 x)。
94
+ """
95
+ import md_cg.mdcg as m
96
+ m.SCORE_MODE = "jaccard"
97
+
98
+
99
+ # 生效条件:对 path 调用 open(path, encoding="utf-8") 后逐行读取,仅对 strip 后非空的 line yield json.loads(line);
100
+ def iter_jsonl(path):
101
+ with open(path, encoding="utf-8") as f:
102
+ for line in f:
103
+ line = line.strip()
104
+ if line:
105
+ yield json.loads(line)
106
+
107
+
108
+ # 生效条件:ds 等于 "lm" 时 path=LM_Q,否则 path=LC_Q;先由 iter_jsonl(path) 取全部 rows,再仅当 qtypes 为真值时保留 r["qtype"] 在 qtypes 中的行(qtypes 为 None/空串/空容器等假值则不过滤);
109
+ def load_questions(ds, qtypes=None):
110
+ """ds: 'lm' | 'lc'。qtypes 过滤题型(None=全部)。"""
111
+ path = LM_Q if ds == "lm" else LC_Q
112
+ rows = list(iter_jsonl(path))
113
+ if qtypes:
114
+ rows = [r for r in rows if r["qtype"] in qtypes]
115
+ return rows
116
+
117
+
118
+ # 生效条件:对 rows/n(seed 默认 7),若 not n 或 n >= len(rows) 则原样返回 rows,否则返回 random.Random(seed).sample(rows, n);
119
+ def sample_questions(rows, n, seed=7):
120
+ if not n or n >= len(rows):
121
+ return rows
122
+ return random.Random(seed).sample(rows, n)
123
+
124
+
125
+ # ------------------------------------------------------------------ 建库
126
+
127
+ # 生效条件:对 t,若 t.get("speaker") 为真值则返回 f"{t['speaker']}: {t['text']} [{t['date']}]",否则返回 f"{t['text']} [{t['date']}]";
128
+ def lm_turn_text(t):
129
+ """LongMemEval turn → 入库文本(日期内联,词法路可召回时间词)。"""
130
+ return f"{t['speaker']}: {t['text']} [{t['date']}]" if t.get("speaker") \
131
+ else f"{t['text']} [{t['date']}]"
132
+
133
+
134
+ # 生效条件:对 r,若 r.get("title") 为真值则返回 f"{r['text']} [{r['title']}]",否则返回 r["text"];
135
+ def lc_turn_text(r):
136
+ """LoCoMo turn → 入库文本(title 含 Data time 时间戳,内联保序)。"""
137
+ return f"{r['text']} [{r['title']}]" if r.get("title") else r["text"]
138
+
139
+
140
+ # ---------------- 白箱标定器(口径 B;纯规则、确定性、盲于查询集) ----------------
141
+
142
+ # 句首功能词表:`[A-Z][a-z]+` 实体近似会把句首大写词误抽为实体,予以排除。
143
+ # 词表固定随报告公开——它决定实体抽取边界,属标定器配置的一部分。
144
+ _STOP_HEADS = frozenset(
145
+ "I We You They He She It This That There Then So But And Or What When Where "
146
+ "Why How Did Do Does Have Has Had Will Would Can Could Should My Your Our His "
147
+ "Her Its Their In On At To For With From About After Before During Is Am Are "
148
+ "Was Were The A An That's Let's Yeah Okay Yes No Well Now Just Really Maybe "
149
+ "Let Get Got Know Think Want Need Make Take Say Said Tell Ask See Look Come "
150
+ "Go Going If Because When While How's What's Where's Who's".split())
151
+
152
+ # 强否定标记:原文含它才生成不适用条件(弱否定不生成,宁缺勿造——
153
+ # forgetting.payload 已证明模板骨架会虚高重复度且无判别力)。
154
+ _NEG_MARKS = ("n't", " not ", " never ", "nobody", "nothing", "no one", "none of")
155
+
156
+
157
+ # 生效条件:对 text(limit 默认 4),按正则收集非 _STOP_HEADS 开头的大写词与 4 位年/时刻/日期数字并去重后,返回前 limit 项;
158
+ def _extract_entities(text, limit=4):
159
+ """确定性实体近似:连续大写词(人名/地名)+ 日期/时间数字。只读 turn 正文。
160
+ 纯数字编号("1.")不是实体——只保留年份/时刻/日期形态,否则列表编号会
161
+ 批量污染 tags 与生效条件行(实测教训)。"""
162
+ import re
163
+ ents = []
164
+ for m in re.findall(r"[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?", text):
165
+ if m.split()[0] not in _STOP_HEADS and m not in ents:
166
+ ents.append(m)
167
+ for m in re.findall(r"\d{4}|\d{1,2}:\d{2}|\d{1,2}/\d{1,2}(?:/\d{2,4})?", text):
168
+ if m not in ents:
169
+ ents.append(m)
170
+ return ents[:limit]
171
+
172
+
173
+ # 生效条件:对 title,若 title 为假值则返回空串,否则按 YYYY-MM-DD、英文星期日期、d/d/d 三个正则顺序取首个匹配串,无匹配返回空串;
174
+ def _date_tag(title):
175
+ """从 title 抽规范化日期串做 tag。整段 title 入 tags 会污染实体路
176
+ (_path_entity 的 `query in t` 方向:长 tag 是误报源)。"""
177
+ if not title:
178
+ return ""
179
+ import re
180
+ m = (re.search(r"\d{4}-\d{2}-\d{2}", title)
181
+ or re.search(r"[A-Za-z]+day\s+\d{1,2}\s+[A-Za-z]+,?\s*\d{4}", title)
182
+ or re.search(r"\d{1,2}/\d{1,2}/\d{2,4}", title))
183
+ return m.group(0) if m else ""
184
+
185
+
186
+ # 生效条件:r 为语料 turn 行(缺 text/role 等键时按空串处理);date_key/title_key 为 None 时对应日期取空串;返回 (CCG 五要素正文, tags, condition_space) 三元组,仅当原文出现强否定标记才生成不适用条件、否则显式写「(无)」;
187
+ def calibrate_turn(r, date_key=None, title_key=None):
188
+ """turn 行 → (CCG 五要素正文, tags, condition_space)。
189
+
190
+ v2 归因修正:自建语料 100% 召回的机制条件是「查询-证据词面同源」
191
+ (对照实验:节点特异词在查询中出现 → 100%;全同质问法 → 11.9%)。
192
+ 因此——
193
+ 1. 特异词(说话人/日期/实体)必须进五要素**每一行**(词法路对全文打
194
+ Jaccard:条件行的特异词是净增益,同质模板词是净稀释——v1 抽样
195
+ 3.3% < legacy 16% 的根源,与 corpus.body 把 dom/point/aspect 内嵌
196
+ 每一行同构);
197
+ 2. 模板骨架压到最短(corpus.body 固定尾巴仅 12 字,v1 有 30+ 字同质词);
198
+ 3. 五要素行承担条件结构职责(semantic 路/judge 读),不承诺解决
199
+ 查询-证据表述鸿沟——那是公开数据集词法召回的真实天花板。
200
+ tags 用裸词(说话人/日期/实体):_path_entity 的匹配是 `t in query or
201
+ query in t`,带 `ent:` 前缀的 tag 在自然语言查询下永不命中(实测教训,
202
+ corpus 的 `domain:` 桶 tag 是给 bucket/条件路由用的,两者用途不同)。
203
+ 不适用条件仅当原文含强否定标记时生成(对 adversarial「是否说过」类问题
204
+ 有真实判别力);否则显式写「(无)」。
205
+ """
206
+ import re
207
+ text = str(r.get("text") or "").strip()
208
+ speaker = str(r.get("speaker") or "").strip()
209
+ date = str(r.get(date_key) or "").strip() if date_key else ""
210
+ title = str(r.get(title_key) or "").strip() if title_key else ""
211
+ ents = [e for e in _extract_entities(text) if e != speaker]
212
+ who = speaker or (next((e for e in ents if not e[0].isdigit()), None) or "匿名")
213
+ dtag = date or _date_tag(title)
214
+ low = f" {text.lower()} "
215
+ neg = next((m for m in _NEG_MARKS if m in low), None)
216
+ when = f"于{dtag}" if dtag else ""
217
+ ents_head = "/".join(ents[:2])
218
+
219
+ body = (
220
+ f"# 功能名:{who}·会话记忆{when}\n"
221
+ f"# 生效条件:查询涉及「{who}」{when}"
222
+ + (f"所述「{ents_head}」" if ents_head else "") + "时生效\n"
223
+ f"# 子功能:{who}的会话条目{when}\n"
224
+ f"# 执行:{who}会话原文回溯\n"
225
+ f"# 验证方式:data\n"
226
+ f"# 不适用条件:"
227
+ + (f"断言与原文相反(含{neg.strip()}语义)" if neg else "(无)") + "\n\n"
228
+ + text
229
+ )
230
+ tags = [t for t in (who, dtag) if t] + ents[:3]
231
+ cond_space = {
232
+ "observation_position": f"{who} 的会话陈述" + (f"({dtag})" if dtag else ""),
233
+ "observation_tool": "会话记录",
234
+ "existence_constraint": "公开",
235
+ }
236
+ return body, tags, cond_space
237
+
238
+
239
+ # 生效条件:任意 t 与可选 ctx(默认 None)传入即原样转调 calibrate_turn(t, date_key="date", ctx=ctx),返回其结果;
240
+ def calibrate_lm_turn(t, ctx=None):
241
+ return calibrate_turn(t, date_key="date", ctx=ctx)
242
+
243
+
244
+ # 生效条件:任意 r 与可选 ctx(默认 None)传入即原样转调 calibrate_turn(r, title_key="title", ctx=ctx),返回其结果;
245
+ def calibrate_lc_turn(r, ctx=None):
246
+ return calibrate_turn(r, title_key="title", ctx=ctx)
247
+
248
+
249
+ # ============ 写入时加工管线(v3)——「查询时的泛化提前到写入时完成」 ============
250
+ # 外部对标结论:检索只是链路最后一跳;完整系统在写入时做实体规范化/意图抽象/
251
+ # 指代消解,把语料加工成可沿链行走的形态。以下三步全部是**确定性规则近似**
252
+ # (纯标准库、盲于查询集、随报告公开词表),不引入任何模型依赖。
253
+
254
+ # 生效条件:对 corpus_path(min_df 默认 3),逐行取 text=str(r.get("text") or "")、spk=str(r.get("speaker") or "").strip(),以 {spk}(spk 真值)或空集合并入正文大写词首词(排除 _STOP_HEADS)更新 df 计数,最后返回计数 >= min_df 的词集合;
255
+ def build_canon(corpus_path, min_df=3):
256
+ """离线实体规范化:全语料扫一遍,聚合大写词/人名的文档频次(df)。
257
+ df≥min_df 的是 canonical 实体(会话成员 + 高频专名)——单 turn 局部抽取
258
+ 看不见「Alice 在 200 条 turn 里反复出现」这个全库事实,这正是写入时
259
+ (offline)加工区别于查询时(online)匹配的价值所在。"""
260
+ from collections import Counter
261
+ df = Counter()
262
+ import re as _re
263
+ for r in iter_jsonl(corpus_path):
264
+ text = str(r.get("text") or "")
265
+ spk = str(r.get("speaker") or "").strip()
266
+ ws = {spk} if spk else set()
267
+ ws |= set(_re.findall(r"[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?", text))
268
+ ws = {w.split()[0] for w in ws if w.split()[0] not in _STOP_HEADS}
269
+ df.update(ws)
270
+ return {w for w, c in df.items() if c >= min_df}
271
+
272
+
273
+ # 意图槽位(离线意图抽象的确定性近似):英文动词短语原样入条件行——
274
+ # 保持词面同源(查询 "What did Alice plan" 与条件行 "plan" 直接 bigram 命中),
275
+ # 不翻译成中文(同义词表是中文,翻译即断链)。三类槽覆盖 LoCoMo 问题分布:
276
+ # 计划/过去事件/偏好。
277
+ _INTENT_HEADS = ("plan", "going to", "thinking of", "thinking about", "want to",
278
+ "will be", "need to", "have to", "decide", "suggest",
279
+ "remember", "talked about", "talk about", "mention",
280
+ "last time", "tell you", "told you", "hear",
281
+ "prefer", "favorite", "love", "hate", "enjoy")
282
+
283
+
284
+ # 生效条件:对 text 小写并前后加空格得到 low,返回 _INTENT_HEADS 中满足 f" {h}" 出现在 low 的项;
285
+ def _intent_of(text):
286
+ low = f" {text.lower()} "
287
+ return [h for h in _INTENT_HEADS if f" {h}" in low]
288
+
289
+
290
+ # 生效条件:r 为语料 turn 行;ctx 为写入时全库视图(canon/intents/last_canon),ctx 为 None 时按空视图处理并退化为 v2 行为;返回 (CCG 五要素正文, tags, condition_space) 三元组;
291
+ def calibrate_turn(r, date_key=None, title_key=None, ctx=None):
292
+ """turn 行 → (CCG 五要素正文, tags, condition_space)。
293
+
294
+ v3(写入时加工管线):在 v2(特异词进每一行)之上叠加三步离线加工,
295
+ ctx 携带全库视图:
296
+ - ctx["canon"]:canonical 实体表(build_canon 产物)。条目涉及的
297
+ canonical 实体进生效条件行——查询以人名开头时,词法/实体路沿
298
+ 规范化实体直接落到「该人名相关条目」集合(链路第一跳)。
299
+ - ctx["intents"]:意图槽位短语进子功能行——意图抽象的词面同源版。
300
+ - ctx["last_canon"]:前一条的 canonical 实体(写入时序状态)。本条
301
+ 无任何实体而前条有 → 条件行附「承接 {last}」——保守指代消解:
302
+ 只附加假设、不删原文(判别力交给 RRF 多路共识,虚报风险可控)。
303
+ tags 用裸词(说话人/日期/实体):_path_entity 的匹配是 `t in query or
304
+ query in t`,带 `ent:` 前缀的 tag 在自然语言查询下永不命中(实测教训)。
305
+ 不适用条件仅当原文含强否定标记时生成;否则显式写「(无)」。
306
+ """
307
+ import re
308
+ text = str(r.get("text") or "").strip()
309
+ speaker = str(r.get("speaker") or "").strip()
310
+ date = str(r.get(date_key) or "").strip() if date_key else ""
311
+ title = str(r.get(title_key) or "").strip() if title_key else ""
312
+ canon = (ctx or {}).get("canon") or frozenset()
313
+ ctx_ents = _extract_entities(text)
314
+ canon_hits = [e for e in dict.fromkeys(
315
+ [speaker] if speaker else []) if e in canon]
316
+ if canon:
317
+ local = re.findall(r"[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?", text)
318
+ canon_hits += [w for w in dict.fromkeys(w.split()[0] for w in local)
319
+ if w in canon and w not in canon_hits]
320
+ else:
321
+ canon_hits += [e for e in ctx_ents if e != speaker]
322
+ last = (ctx or {}).get("last_canon")
323
+ if not canon_hits and last:
324
+ canon_hits = list(last)
325
+ who = speaker or (next((e for e in canon_hits if not e[0].isdigit()), None)
326
+ or "匿名")
327
+ dtag = date or _date_tag(title)
328
+ low = f" {text.lower()} "
329
+ neg = next((m for m in _NEG_MARKS if m in low), None)
330
+ when = f"于{dtag}" if dtag else ""
331
+ ent_head = "/".join(canon_hits[:3])
332
+ intents = _intent_of(text)
333
+
334
+ body = (
335
+ f"# 功能名:{who}·会话记忆{when}\n"
336
+ f"# 生效条件:查询涉及「{ent_head or who}」{when}时生效\n"
337
+ f"# 子功能:{who}的会话条目"
338
+ + (f"(意图:{'/'.join(intents[:3])})" if intents else "") + "\n"
339
+ f"# 执行:{who}会话原文回溯\n"
340
+ f"# 验证方式:data\n"
341
+ f"# 不适用条件:"
342
+ + (f"断言与原文相反(含{neg.strip()}语义)" if neg else "(无)") + "\n\n"
343
+ + text
344
+ )
345
+ tags = [t for t in (who, dtag) if t] + canon_hits[:3]
346
+ cond_space = {
347
+ "observation_position": f"{who} 的会话陈述" + (f"({dtag})" if dtag else ""),
348
+ "observation_tool": "会话记录",
349
+ "existence_constraint": "公开",
350
+ }
351
+ return body, tags, cond_space
352
+
353
+
354
+ # 生效条件:rebuild=True 且 os.path.isdir(root) 时先 rmtree(root);cg_cls 为假值(如 None)时回落 MdCGOS(root, autoflush=500);语料行数 n_rows ≤ cg.index["nodes"] 现有节点数(n_rows=0 的空语料也满足)时直接复用返回 cg;否则逐行写入——calib_of 为真走标定口径 B(先 build_canon,calib_of(r, {"canon": canon, "last_canon": last_canon}) 取 body/tags/condition_space),calib_of 为假(含 None)走 legacy 口径 A(用 text_of(r)),id 已在 cg.index["nodes"] 的行跳过,verbose 为真时每 20000 行打印进度,循环后 cg.flush() 再返回 cg;
355
+ def build_eval_cg(cg_cls, root, corpus_path, text_of, src_tag, verbose=True,
356
+ calib_of=None, rebuild=False):
357
+ """幂等建库:节点数已达语料行数则直接复用。返回 cg(不 flush 句柄)。
358
+
359
+ calib_of=None → 口径 A(legacy 裸文本);传入 calibrate_*_turn → 口径 B
360
+ (五要素标定条目 + tags + condition_space)。两口径用不同 root,互不污染。
361
+ rebuild=True 删库重建(标定器改版后必须重建——幂等按节点数判断,识别不了
362
+ 同节点数的旧版标定条目)。
363
+ v3:标定口径先全语料 build_canon(离线实体规范化),写入循环维护
364
+ last_canon(时序状态,供保守指代承接)——两遍扫描,仍 O(N)。
365
+ """
366
+ from md_cg.mdcos import MdCGOS
367
+ if rebuild and os.path.isdir(root):
368
+ import shutil
369
+ shutil.rmtree(root, ignore_errors=True)
370
+ n_rows = sum(1 for _ in iter_jsonl(corpus_path))
371
+ cg = cg_cls(root, autoflush=500) if cg_cls else MdCGOS(root, autoflush=500)
372
+ have = len(cg.index["nodes"])
373
+ if have >= n_rows:
374
+ if verbose:
375
+ print(f" 建库复用 {root}:{have} 节点(语料 {n_rows} 行)")
376
+ return cg
377
+ canon = build_canon(corpus_path) if calib_of else frozenset()
378
+ if verbose and calib_of:
379
+ print(f" 写入时加工:canonical 实体 {len(canon)} 个(df≥3)")
380
+ if verbose:
381
+ print(f" 建库 {root}:已有 {have},语料 {n_rows} 行,开始写入 "
382
+ f"({'标定口径 B' if calib_of else 'legacy 口径 A'})…")
383
+ t0 = time.time()
384
+ last_canon = None
385
+ for i, r in enumerate(iter_jsonl(corpus_path), 1):
386
+ if r["id"] in cg.index["nodes"]:
387
+ continue
388
+ if calib_of:
389
+ body, tags, cond_space = calib_of(r, {"canon": canon,
390
+ "last_canon": last_canon})
391
+ cg.add(r["id"], body, layer="contextual", tags=tags,
392
+ condition_space=cond_space, eval_src=src_tag,
393
+ verification_basis="data")
394
+ hits = [t for t in tags[1:] if not t.replace(".", "")
395
+ .replace(":", "").isdigit()]
396
+ last_canon = hits or last_canon
397
+ else:
398
+ cg.add(r["id"], text_of(r), layer="contextual",
399
+ eval_src=src_tag, verification_basis="data")
400
+ if verbose and i % 20000 == 0:
401
+ print(f" … {i}/{n_rows}({time.time() - t0:.0f}s)")
402
+ cg.flush()
403
+ if verbose:
404
+ print(f" 建库完成:{len(cg.index['nodes'])} 节点,{time.time() - t0:.0f}s")
405
+ return cg
406
+
407
+
408
+ # 生效条件:对传入 cg,用闭包 cache(键为 entry["path"])包装其原 cg._read 并赋回 cg._read,返回该 cache;仅当 p = entry["path"] 不在 cache 时调用 orig(entry) 并缓存其(含假值)结果,p 已在 cache 中时直接返回缓存值;
409
+ def install_read_cache(cg):
410
+ """评测只读:节点文件读进内存,避免逐次检索重复磁盘 I/O。"""
411
+ cache = {}
412
+ orig = cg._read
413
+
414
+ # 生效条件:entry["path"] 未在闭包 cache 中时调用 orig(entry) 存入并返回,已在 cache 中则直接返回缓存值(cache 与原 _read 由外层 install_read_cache 提供);
415
+ def _cached(entry):
416
+ p = entry["path"]
417
+ if p not in cache:
418
+ cache[p] = orig(entry)
419
+ return cache[p]
420
+
421
+ cg._read = _cached
422
+ return cache
423
+
424
+
425
+ # ------------------------------------------------------------------ 检索与指标
426
+
427
+ # 生效条件:对 cg/query(k 默认 5、paths 默认 PATHS、judge 默认 False),若 fusion 为真值则加入 kw["fusion"],若 path_weights 为真值则加入 kw["path_weights"],若 context is not None(含空 dict/空串)则加入 kw["context"],再调用 cg.search_rrf(...);
428
+ def run_query(cg, query, k=5, paths=PATHS, judge=False, fusion=None,
429
+ path_weights=None, context=None):
430
+ """单查询,返回 (结果四元组列表, meta)。
431
+ fusion/path_weights 覆盖融合方式与路权重(按路拆解归因用)。
432
+
433
+ context 是**条件约束(bucket)路可被开启的唯一入口**:mdcos._path_bucket 在
434
+ `context is None` 时直接 `return []`(mdcos.py:435),而 search_rrf 的默认
435
+ paths 里恰恰含 "bucket"——即「名义四路、实际三路」。传 context 之前,全部
436
+ 既有四路 RRF 口径的评测都未曾开启过条件路由(先例:四轴消融 + Rust 空壳)。
437
+ """
438
+ kw = {}
439
+ if fusion:
440
+ kw["fusion"] = fusion
441
+ if path_weights:
442
+ kw["path_weights"] = path_weights
443
+ if context is not None:
444
+ kw["context"] = context
445
+ return cg.search_rrf(query, k=k, paths=paths, judge=judge, record=False, **kw)
446
+
447
+
448
+ # 生效条件:当 evidence 为真值且 res 中某 r 的 r[0]["id"] 属于 evidence 时返回首个 1-based 排名,否则(evidence 为假值或未命中)返回 0;
449
+ def first_evidence_rank(res, evidence):
450
+ """首个证据 turn 的排名(1-based;未命中 0)。"""
451
+ if not evidence:
452
+ return 0
453
+ for i, r in enumerate(res, 1):
454
+ if r[0]["id"] in evidence:
455
+ return i
456
+ return 0
457
+
458
+
459
+ # 生效条件:对 questions 中每个 it,若 context_of 为真值则 ctx=context_of(it) 否则 ctx=None;以 k/paths/judge/fusion/path_weights 调用 run_query,按 evidence_turns 算 first_evidence_rank,收集 qid/qtype/rank/top1_score/n_res 行,返回 rows;
460
+ def evaluate_group(cg, questions, k=5, paths=PATHS, judge=False, verbose=True,
461
+ fusion=None, path_weights=None, context_of=None):
462
+ """一组题 → per-question 明细行。负例组传 judge=False(拒答看分数线)。
463
+
464
+ context_of: 可调用 `q -> context dict`(返回 None 表示该题不下发情境)。
465
+ 缺省 None → 全组不传 context(**既有行为完全不变**)。传入即开启条件路由,
466
+ 是「条件约束路的净效应」单变量实验的开关。
467
+ """
468
+ rows = []
469
+ t0 = time.time()
470
+ for i, it in enumerate(questions, 1):
471
+ ctx = context_of(it) if context_of else None
472
+ res, meta = run_query(cg, it["question"], k=k, paths=paths, judge=judge,
473
+ fusion=fusion, path_weights=path_weights,
474
+ context=ctx)
475
+ rank = first_evidence_rank(res, set(it.get("evidence_turns") or []))
476
+ rows.append({
477
+ "qid": it["qid"], "qtype": it["qtype"],
478
+ "rank": rank, "top1_score": res[0][1] if res else 0.0,
479
+ "n_res": len(res),
480
+ })
481
+ if verbose and i % 100 == 0:
482
+ print(f" 查询 {i}/{len(questions)}({time.time() - t0:.0f}s)")
483
+ return rows
484
+
485
+
486
+ # 生效条件:当 rows 为真值时按 k(默认 5)计算 hit@1、hit@k、MRR、score_p10/p50 及 by_qtype 指标,否则返回 {"n": 0};
487
+ def summarize(rows, k=5):
488
+ """hit@1 / hit@K / MRR + 分题型。score_p10 供拒答线校准。"""
489
+ if not rows:
490
+ return {"n": 0}
491
+ hit1 = sum(1 for r in rows if r["rank"] == 1)
492
+ hitk = sum(1 for r in rows if 0 < r["rank"] <= k)
493
+ mrr = sum(1.0 / r["rank"] for r in rows if r["rank"]) / len(rows)
494
+ top1 = sorted(r["top1_score"] for r in rows)
495
+ out = {
496
+ "n": len(rows),
497
+ "hit@1": hit1 / len(rows),
498
+ f"hit@{k}": hitk / len(rows),
499
+ "mrr": mrr,
500
+ "score_p10": top1[max(0, len(top1) // 10)],
501
+ "score_p50": top1[len(top1) // 2],
502
+ }
503
+ by = {}
504
+ for r in rows:
505
+ by.setdefault(r["qtype"], []).append(r)
506
+ out["by_qtype"] = {
507
+ qt: {
508
+ "n": len(g),
509
+ "hit@1": sum(1 for r in g if r["rank"] == 1) / len(g),
510
+ f"hit@{k}": sum(1 for r in g if 0 < r["rank"] <= k) / len(g),
511
+ "mrr": sum(1.0 / r["rank"] for r in g if r["rank"]) / len(g),
512
+ } for qt, g in sorted(by.items())
513
+ }
514
+ return out
515
+
516
+
517
+ # 生效条件:当 rows 非空时按 r["top1_score"] < line 统计拒答数并返回拒绝率,否则返回 {"n": 0};
518
+ def refusal_metrics(rows, line):
519
+ """负例组:Top-1 分 < line 记为拒答。返回拒答率与明细统计。"""
520
+ n = len(rows)
521
+ if not n:
522
+ return {"n": 0}
523
+ ref = sum(1 for r in rows if r["top1_score"] < line)
524
+ return {"n": n, "refusal_rate": ref / n,
525
+ "refused": ref, "line": line}
526
+
527
+
528
+ # 生效条件:当 rows 非空时返回 r["top1_score"] < line 的行占比,否则返回 0.0;
529
+ def false_refusal_rate(rows, line):
530
+ """正例误杀(被错误拒答的正例比例):正例 Top-1 分 < line。"""
531
+ n = len(rows)
532
+ if not n:
533
+ return 0.0
534
+ return sum(1 for r in rows if r["top1_score"] < line) / n
535
+
536
+
537
+ # 生效条件:当 pos_rows 中存在 rank==1 的行时,返回这些行 top1_score 升序后 max(0, len//10) 索引处的值,否则返回 0.0;
538
+ def calibrate_line(pos_rows):
539
+ """拒答线 = 正例 hit@1 题 Top-1 分的 10 分位。"""
540
+ scores = sorted(r["top1_score"] for r in pos_rows if r["rank"] == 1)
541
+ if not scores:
542
+ return 0.0
543
+ return scores[max(0, len(scores) // 10)]
544
+
545
+
546
+ # 生效条件:当 name/payload 给出时,确保 RESULTS 目录(os.makedirs(RESULTS, exist_ok=True)),将 payload 以 ensure_ascii=False、indent=2 写入 RESULTS/name 并返回 path;
547
+ def save_result(name, payload):
548
+ """评测结果 JSON 落盘(报告数据源)。"""
549
+ os.makedirs(RESULTS, exist_ok=True)
550
+ path = os.path.join(RESULTS, name)
551
+ with open(path, "w", encoding="utf-8") as f:
552
+ json.dump(payload, f, ensure_ascii=False, indent=2)
553
+ print(f" 结果 → {path}")
554
+ return path
555
+
556
+
557
+ # 生效条件:对 groups(k 默认 5),若某组 s 的 s.get("n") 为假(缺 n 或 n=0)则打印 0 行并跳过,否则按 s["hit@1"]、s[f"hit@{k}"]、s["mrr"] 打印并遍历 s.get("by_qtype", {}) 输出子行;
558
+ def print_table(title, groups, k=5):
559
+ """groups: {组名: summarize 输出}。"""
560
+ print(f"\n== {title}(证据命中口径,k={k})==")
561
+ print(f"{'组':<28}{'n':>6}{'hit@1':>9}{f'hit@{k}':>9}{'MRR':>8}")
562
+ print("-" * 62)
563
+ for name, s in groups.items():
564
+ if not s.get("n"):
565
+ print(f"{name:<28}{0:>6} -")
566
+ continue
567
+ print(f"{name:<28}{s['n']:>6}{s['hit@1']:>9.1%}"
568
+ f"{s[f'hit@{k}']:>9.1%}{s['mrr']:>8.3f}")
569
+ for qt, st in s.get("by_qtype", {}).items():
570
+ print(f" ├ {qt:<24}{st['n']:>6}{st['hit@1']:>9.1%}"
571
+ f"{st[f'hit@{k}']:>9.1%}{st['mrr']:>8.3f}")
572
+
573
+
574
+ # 生效条件:对任意 x,返回 f"{x * 100:.1f}%" 的百分比字符串;
575
+ def pct(x):
576
576
  return f"{x * 100:.1f}%"