@furongjun1999/dsh-memory 0.4.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (504) hide show
  1. package/README.md +16 -16
  2. package/codebuddy/CODEBUDDY.md +11 -3
  3. package/codebuddy/README.md +92 -90
  4. package/codebuddy/mcp.json +27 -27
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -169
  6. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -144
  7. package/docs/README.md +142 -111
  8. package/docs/discipline/harnesses.yaml +244 -226
  9. package/docs/discipline/templates/full.md.tmpl +61 -61
  10. package/docs/discipline/templates/rules.mdc.tmpl +68 -0
  11. package/docs/discipline/templates/skill.md.tmpl +23 -23
  12. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md +299 -299
  13. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md +239 -239
  14. package/docs/eval//345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md +174 -174
  15. package/docs/eval//346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md +223 -223
  16. package/docs/{mdcg → hive}//344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md +165 -160
  17. package/docs/hive//344/273/244/347/211/214/350/257/255/344/271/211/344/277/256/346/255/243_/344/273/262/350/243/201/344/275/215_v0.1.md +76 -0
  18. package/docs/{mdcg → hive}//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.5.md +236 -236
  19. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -43
  20. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +85 -0
  21. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -102
  22. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +47 -0
  23. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -503
  24. package/docs/hive//350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +85 -85
  25. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +191 -0
  26. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +90 -0
  27. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -216
  28. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
  29. package/docs/mdcg/lingshu_tutorial.html +14449 -14449
  30. package/docs/mdcg/release_v0.4.11.md +49 -0
  31. package/docs/mdcg/release_v0.4.5.md +55 -55
  32. package/docs/mdcg/tool_table_v0.3.0.md +117 -117
  33. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -600
  34. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  35. package/docs/mdcg//345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +275 -275
  36. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
  37. package/docs/mdcg//346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +130 -130
  38. package/docs/mdcg//347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md +278 -278
  39. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -172
  40. package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
  41. package/docs/mdcg//350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +491 -491
  42. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md +340 -340
  43. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +588 -588
  44. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -176
  45. package/docs/plans//345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +81 -81
  46. package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
  47. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -242
  48. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -129
  49. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -134
  50. package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  51. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -145
  52. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +144 -144
  53. package/docs/theory//347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +198 -198
  54. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -221
  55. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -237
  56. package/docs//345/217/221/345/270/203/344/273/266/345/233/236/346/272/257/350/257/264/346/230/216_v0.1.md +82 -82
  57. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +28 -2
  58. package/dsh/README.md +82 -82
  59. package/dsh/cordis.yml.example +139 -139
  60. package/dsh/update-lingshu.bat +11 -11
  61. package/lib/hooks.js +36 -2
  62. package/lib/lib/roleplay_web.js +427 -427
  63. package/md_cg/__init__.py +7 -7
  64. package/md_cg/audit.py +368 -368
  65. package/md_cg/autonomy.py +287 -287
  66. package/md_cg/backfill.py +1327 -1327
  67. package/md_cg/backfill_bigdomain.py +34 -34
  68. package/md_cg/bench6_arms.py +410 -410
  69. package/md_cg/bench6_common.py +230 -230
  70. package/md_cg/bench6_competitors.py +212 -212
  71. package/md_cg/bench_axis_domain.py +257 -257
  72. package/md_cg/bench_blind_comp.py +308 -308
  73. package/md_cg/bench_en_atoms_public.py +230 -230
  74. package/md_cg/bench_governance.py +348 -348
  75. package/md_cg/bench_lme_zh.py +410 -410
  76. package/md_cg/bench_locomo.py +121 -121
  77. package/md_cg/bench_locomo_zh.py +450 -450
  78. package/md_cg/bench_locomo_zh_public.py +147 -147
  79. package/md_cg/bench_longmem.py +112 -112
  80. package/md_cg/bench_membench.py +632 -632
  81. package/md_cg/bench_p0.py +149 -149
  82. package/md_cg/bench_progressive.py +287 -287
  83. package/md_cg/bench_role_views.py +238 -238
  84. package/md_cg/bench_task_ab.py +243 -243
  85. package/md_cg/bench_task_ab_llm.py +408 -408
  86. package/md_cg/bench_unified_en.py +204 -204
  87. package/md_cg/bench_zh_mad.py +601 -601
  88. package/md_cg/blindspot_tickets.py +123 -123
  89. package/md_cg/branches.py +285 -285
  90. package/md_cg/build_postings.py +73 -73
  91. package/md_cg/ccgc.py +1005 -948
  92. package/md_cg/census.py +132 -132
  93. package/md_cg/chain.py +300 -300
  94. package/md_cg/codeindex.py +531 -531
  95. package/md_cg/coldverify.py +292 -292
  96. package/md_cg/comment_gate.py +337 -337
  97. package/md_cg/cond_compose.py +190 -190
  98. package/md_cg/cond_facts.py +154 -154
  99. package/md_cg/cond_template.json +106 -106
  100. package/md_cg/condition_anchor.py +142 -142
  101. package/md_cg/conformance.py +726 -726
  102. package/md_cg/consistency.py +717 -717
  103. package/md_cg/consolidate.py +1536 -1439
  104. package/md_cg/corpus.py +110 -110
  105. package/md_cg/crosscheck.py +1097 -1097
  106. package/md_cg/crypto.py +437 -437
  107. package/md_cg/d_meta.py +310 -310
  108. package/md_cg/datapath.py +334 -334
  109. package/md_cg/docindex.py +473 -473
  110. package/md_cg/eval_common.py +575 -575
  111. package/md_cg/evidence.py +580 -580
  112. package/md_cg/evolution.py +477 -477
  113. package/md_cg/export.py +220 -220
  114. package/md_cg/forgetting.py +581 -581
  115. package/md_cg/fsutil.py +329 -329
  116. package/md_cg/hotcache.py +238 -214
  117. package/md_cg/hyperedge.py +251 -251
  118. package/md_cg/identity.py +390 -390
  119. package/md_cg/insight.py +500 -500
  120. package/md_cg/interop.py +199 -0
  121. package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
  122. package/md_cg/lexicon/build_standard_en.py +171 -171
  123. package/md_cg/lexicon/expand_en_zh.py +211 -211
  124. package/md_cg/lifecycle.py +272 -272
  125. package/md_cg/linkref.py +280 -280
  126. package/md_cg/links.py +622 -622
  127. package/md_cg/mcp_server.py +129 -32
  128. package/md_cg/md_whitebox.py +345 -345
  129. package/md_cg/mdcg.py +176 -117
  130. package/md_cg/mdcos.py +79 -13
  131. package/md_cg/metacognition.py +591 -591
  132. package/md_cg/migrate.py +119 -119
  133. package/md_cg/migrate_aeis.py +221 -221
  134. package/md_cg/migrate_roleplay.py +293 -293
  135. package/md_cg/migrate_wisdom_graph.py +360 -360
  136. package/md_cg/mreview/__init__.py +25 -25
  137. package/md_cg/mreview/__main__.py +110 -110
  138. package/md_cg/mreview/bundle.py +178 -178
  139. package/md_cg/mreview/candidates.py +262 -262
  140. package/md_cg/mreview/govern.py +693 -693
  141. package/md_cg/mreview/locate.py +939 -939
  142. package/md_cg/mreview/pipeline.py +728 -728
  143. package/md_cg/mreview/rules/duplication.json +21 -21
  144. package/md_cg/mreview/rules/field_coverage.json +54 -54
  145. package/md_cg/mreview/rules/source_license.json +21 -21
  146. package/md_cg/mreview/rules/template_flow.json +21 -21
  147. package/md_cg/mreview/ruleset.py +252 -252
  148. package/md_cg/nodefile.py +575 -575
  149. package/md_cg/pooling.py +484 -472
  150. package/md_cg/postings.py +298 -298
  151. package/md_cg/predict.py +1100 -1100
  152. package/md_cg/progressive.py +123 -123
  153. package/md_cg/protect.py +272 -272
  154. package/md_cg/protocol/md_cg_gate.proto +33 -33
  155. package/md_cg/protocol.py +372 -372
  156. package/md_cg/provenance.py +582 -582
  157. package/md_cg/reach.py +453 -453
  158. package/md_cg/readcache.py +85 -0
  159. package/md_cg/refindex.py +833 -833
  160. package/md_cg/refine.py +604 -604
  161. package/md_cg/roleviews.py +89 -89
  162. package/md_cg/routing.py +365 -365
  163. package/md_cg/scrub.py +852 -852
  164. package/md_cg/security.py +274 -274
  165. package/md_cg/self_state.py +1029 -1029
  166. package/md_cg/selfreport.py +151 -151
  167. package/md_cg/semantic/__init__.py +10 -10
  168. package/md_cg/semantic/canonical.py +122 -122
  169. package/md_cg/semantic/en_normalizer.py +364 -364
  170. package/md_cg/semantic/en_zh_map.json +28694 -0
  171. package/md_cg/semantic/export_en_zh_map.py +64 -0
  172. package/md_cg/semantic/unify.py +45 -0
  173. package/md_cg/semantic/zh_en_atoms.py +139 -139
  174. package/md_cg/signer.py +562 -562
  175. package/md_cg/sources.py +815 -582
  176. package/md_cg/statushdr.py +179 -179
  177. package/md_cg/stg.py +48 -37
  178. package/md_cg/subgraph.py +729 -729
  179. package/md_cg/sustain.py +1138 -1138
  180. package/md_cg/tasks.py +470 -470
  181. package/md_cg/test_action_derive.py +203 -203
  182. package/md_cg/test_audit_rotate.py +270 -270
  183. package/md_cg/test_autonomy.py +143 -143
  184. package/md_cg/test_bench_governance.py +102 -102
  185. package/md_cg/test_blindspot_tickets.py +166 -166
  186. package/md_cg/test_branches.py +249 -249
  187. package/md_cg/test_ccg_perturb.py +184 -184
  188. package/md_cg/test_ccgc.py +433 -433
  189. package/md_cg/test_census_prune.py +81 -81
  190. package/md_cg/test_cond_compose_anchors.py +76 -76
  191. package/md_cg/test_cond_match.py +165 -165
  192. package/md_cg/test_condition_anchor.py +81 -81
  193. package/md_cg/test_d_meta.py +412 -412
  194. package/md_cg/test_datapath_root.py +199 -199
  195. package/md_cg/test_en_pipeline.py +166 -166
  196. package/md_cg/test_gain_gate.py +212 -212
  197. package/md_cg/test_health_scale.py +173 -173
  198. package/md_cg/test_hive_ingest.py +285 -0
  199. package/md_cg/test_hot_cold.py +215 -215
  200. package/md_cg/test_hyperedge.py +245 -245
  201. package/md_cg/test_i26_empty_first_write.py +116 -0
  202. package/md_cg/test_i27_e041_identity.py +128 -0
  203. package/md_cg/test_i28_hotcache_prodpath.py +122 -0
  204. package/md_cg/test_identity_attribution.py +147 -147
  205. package/md_cg/test_index_durability.py +224 -224
  206. package/md_cg/test_interop.py +93 -0
  207. package/md_cg/test_lifecycle.py +309 -309
  208. package/md_cg/test_linkref.py +306 -306
  209. package/md_cg/test_lock.py +43 -43
  210. package/md_cg/test_md_access_parity.py +255 -255
  211. package/md_cg/test_md_writepath.py +345 -345
  212. package/md_cg/test_mdstore_search_parity.py +160 -0
  213. package/md_cg/test_mr_m2.py +587 -587
  214. package/md_cg/test_mr_m3.py +710 -710
  215. package/md_cg/test_mr_m4.py +485 -485
  216. package/md_cg/test_p0.py +250 -250
  217. package/md_cg/test_p1.py +316 -316
  218. package/md_cg/test_p10_identity.py +173 -173
  219. package/md_cg/test_p11_consistency.py +233 -233
  220. package/md_cg/test_p12_metacognition.py +212 -212
  221. package/md_cg/test_p13_encryption.py +241 -241
  222. package/md_cg/test_p14_sustain.py +249 -249
  223. package/md_cg/test_p15_scrub.py +280 -280
  224. package/md_cg/test_p16_self_state.py +301 -301
  225. package/md_cg/test_p17_predict.py +354 -354
  226. package/md_cg/test_p18_whitebox.py +171 -171
  227. package/md_cg/test_p19_migrate_roleplay.py +149 -149
  228. package/md_cg/test_p20_evolution.py +315 -315
  229. package/md_cg/test_p21_tokens.py +293 -270
  230. package/md_cg/test_p22_theory.py +175 -175
  231. package/md_cg/test_p23_links.py +311 -311
  232. package/md_cg/test_p24_evidence.py +227 -227
  233. package/md_cg/test_p25_weights.py +156 -156
  234. package/md_cg/test_p26_refindex.py +416 -416
  235. package/md_cg/test_p27_docindex.py +765 -765
  236. package/md_cg/test_p28_refcheck.py +305 -305
  237. package/md_cg/test_p29_session_ingest_export.py +354 -333
  238. package/md_cg/test_p3.py +11 -2
  239. package/md_cg/test_p30_maintain.py +330 -330
  240. package/md_cg/test_p31_insight.py +534 -534
  241. package/md_cg/test_p32_backfill.py +298 -298
  242. package/md_cg/test_p33_ccg_wiring.py +293 -293
  243. package/md_cg/test_p34_crosscheck.py +331 -331
  244. package/md_cg/test_p35_conditioned_claim.py +252 -252
  245. package/md_cg/test_p36_kp_align.py +230 -230
  246. package/md_cg/test_p37_condition_space.py +248 -248
  247. package/md_cg/test_p38_concurrent_flush.py +102 -0
  248. package/md_cg/test_p38_contextualize.py +273 -273
  249. package/md_cg/test_p39_verify_flow.py +113 -0
  250. package/md_cg/test_p39_vision_evidence.py +369 -369
  251. package/md_cg/test_p40_refine_worklist.py +241 -241
  252. package/md_cg/test_p41_evolve_patrol.py +224 -224
  253. package/md_cg/test_p42_provenance.py +269 -269
  254. package/md_cg/test_p43_pooling.py +412 -398
  255. package/md_cg/test_p44_md_whitebox.py +231 -231
  256. package/md_cg/test_p45_session_identity.py +219 -219
  257. package/md_cg/test_p46_unit_scope.py +272 -272
  258. package/md_cg/test_p47_session_view.py +281 -0
  259. package/md_cg/test_p4_fuzzy.py +223 -223
  260. package/md_cg/test_p5_semantic.py +226 -226
  261. package/md_cg/test_p6_consolidate.py +440 -387
  262. package/md_cg/test_p7_goals_recent.py +202 -202
  263. package/md_cg/test_p8_subgraph_chain.py +200 -200
  264. package/md_cg/test_p9_forget_protect.py +231 -231
  265. package/md_cg/test_predict_beta.py +135 -135
  266. package/md_cg/test_preflight_failclosed.py +100 -100
  267. package/md_cg/test_progressive.py +146 -146
  268. package/md_cg/test_protocol.py +243 -243
  269. package/md_cg/test_reach.py +378 -378
  270. package/md_cg/test_reach_keys.py +201 -201
  271. package/md_cg/test_read_clip.py +141 -141
  272. package/md_cg/test_readcache_prodpath.py +155 -0
  273. package/md_cg/test_retr_gates_prodpath.py +140 -0
  274. package/md_cg/test_retr_s1.py +340 -340
  275. package/md_cg/test_retr_s1b.py +209 -209
  276. package/md_cg/test_retr_s3.py +194 -194
  277. package/md_cg/test_retr_s4.py +163 -163
  278. package/md_cg/test_retr_s5.py +200 -200
  279. package/md_cg/test_retr_s6.py +157 -157
  280. package/md_cg/test_retr_s7.py +384 -384
  281. package/md_cg/test_retr_s8_time.py +369 -316
  282. package/md_cg/test_retr_s9_edges.py +286 -286
  283. package/md_cg/test_retr_s9_entity_ctx.py +175 -175
  284. package/md_cg/test_review_conformance.py +367 -367
  285. package/md_cg/test_role_views.py +354 -354
  286. package/md_cg/test_sem_noise.py +242 -242
  287. package/md_cg/test_semantic_canonical.py +241 -241
  288. package/md_cg/test_subproc_encoding.py +192 -192
  289. package/md_cg/test_sustain_mutual.py +153 -153
  290. package/md_cg/test_tasks.py +409 -409
  291. package/md_cg/test_tool_face.py +189 -189
  292. package/md_cg/test_transfer.py +180 -180
  293. package/md_cg/test_trust.py +361 -361
  294. package/md_cg/test_twophase.py +286 -286
  295. package/md_cg/test_v14_fixes.py +397 -397
  296. package/md_cg/test_validity_filter.py +280 -280
  297. package/md_cg/test_verify_answer.py +138 -138
  298. package/md_cg/test_wisdom_md_store.py +292 -292
  299. package/md_cg/test_writelimit.py +197 -197
  300. package/md_cg/test_writepipe.py +214 -214
  301. package/md_cg/theory.py +273 -273
  302. package/md_cg/tokens.py +677 -663
  303. package/md_cg/tool_face.py +260 -260
  304. package/md_cg/trust.py +986 -950
  305. package/md_cg/twophase.py +231 -231
  306. package/md_cg/units.py +667 -667
  307. package/md_cg/vision_evidence.py +666 -666
  308. package/md_cg/weights.py +624 -624
  309. package/md_cg/whitebox.py +527 -527
  310. package/md_cg/whitebox_kb/__init__.py +37 -37
  311. package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
  312. package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
  313. package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
  314. package/md_cg/whitebox_kb/engine.py +310 -310
  315. package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  316. package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
  317. package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
  318. package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
  319. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  320. package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
  321. package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
  322. package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
  323. package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
  324. package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
  325. package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
  326. package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
  327. package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
  328. package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
  329. package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
  330. package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
  331. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
  332. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
  333. package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
  334. package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
  335. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
  336. package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
  337. package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
  338. package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
  339. package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
  340. package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
  341. package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
  342. package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
  343. package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
  344. package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
  345. package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
  346. package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
  347. package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
  348. package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
  349. package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
  350. package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
  351. package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
  352. package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
  353. package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
  354. package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
  355. package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
  356. package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
  357. package/md_cg/writelimit.py +356 -356
  358. package/md_cg/writepipe.py +550 -542
  359. package/package.json +97 -96
  360. package/skills/plugin.json +54 -54
  361. package/skills/skills/designer-perspective/SKILL.md +158 -158
  362. package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
  363. package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
  364. package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
  365. package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
  366. package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
  367. package/skills/skills/designer-perspective/scripts/designer.py +545 -545
  368. package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
  369. package/skills/skills/designer-perspective/tests/selftest.py +61 -61
  370. package/skills/skills/lingshu-browser/SKILL.md +60 -60
  371. package/skills/skills/lingshu-compiler/SKILL.md +56 -56
  372. package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
  373. package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
  374. package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
  375. package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
  376. package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
  377. package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
  378. package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
  379. package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
  380. package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
  381. package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
  382. package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
  383. package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
  384. package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
  385. package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
  386. package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
  387. package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
  388. package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
  389. package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
  390. package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
  391. package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
  392. package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
  393. package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
  394. package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
  395. package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
  396. package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
  397. package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
  398. package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
  399. package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
  400. package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
  401. package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
  402. package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
  403. package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
  404. package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
  405. package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
  406. package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
  407. package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
  408. package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
  409. package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
  410. package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
  411. package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
  412. package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
  413. package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
  414. package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
  415. package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
  416. package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
  417. package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
  418. package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
  419. package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
  420. package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
  421. package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
  422. package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
  423. package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
  424. package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
  425. package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
  426. package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
  427. package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
  428. package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
  429. package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
  430. package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
  431. package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
  432. package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
  433. package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
  434. package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
  435. package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
  436. package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
  437. package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
  438. package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
  439. package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
  440. package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
  441. package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
  442. package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
  443. package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
  444. package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
  445. package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
  446. package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
  447. package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
  448. package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
  449. package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
  450. package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
  451. package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
  452. package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
  453. package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
  454. package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
  455. package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
  456. package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
  457. package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
  458. package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
  459. package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
  460. package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
  461. package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
  462. package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
  463. package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
  464. package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
  465. package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
  466. package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
  467. package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
  468. package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
  469. package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
  470. package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
  471. package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
  472. package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
  473. package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
  474. package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
  475. package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
  476. package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
  477. package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
  478. package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
  479. package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
  480. package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
  481. package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
  482. package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
  483. package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
  484. package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
  485. package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
  486. package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
  487. package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
  488. package/skills/skills/lingshu-graph/SKILL.md +63 -63
  489. package/skills/skills/lingshu-net/SKILL.md +48 -48
  490. package/skills/skills/lingshu-os/SKILL.md +64 -64
  491. package/skills/skills/lingshu-pylang/SKILL.md +71 -71
  492. package/src/bridge.ts +401 -401
  493. package/src/hooks.ts +38 -2
  494. package/src/lib/datapath.ts +326 -326
  495. package/src/lib/mdcg_client.ts +413 -413
  496. package/src/lib/mutual.ts +428 -428
  497. package/src/lib/prompt_safety.ts +62 -62
  498. package/src/lib/python_path.ts +71 -71
  499. package/src/lib/roleplay_web.ts +932 -932
  500. package/src/lib/token_store.ts +192 -192
  501. package/src/tools.ts +212 -212
  502. package/zcode/AGENTS.md +11 -3
  503. package/zcode/README.md +41 -41
  504. /package/docs/{mdcg → hive}//344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md" +0 -0
package/md_cg/refine.py CHANGED
@@ -1,605 +1,605 @@
1
- # -*- coding: utf-8 -*-
2
- """G6:node_ 结构提炼 · 小样本抽检工单与扩批闸门(只读预演,纯标准库)。
3
-
4
- 缺口(G6):`node_*`(P37 双子管线派生记忆)自带 `ccg_exempt=true`,正文只有
5
- 「功能名 / 生效条件」两行,`子功能 / 执行 / 不适用条件` 均未声明(实测 1324 条)。
6
- 若直接对全量跑归纳提炼(`consolidate.induce`),一旦**提炼口径**失准,就会把
7
- 模板化词面当成「共性条件」固化成概念节点,污染单一真相源。
8
-
9
- 裁定单条件 A:**先抽 20 条做提炼预演,人工核对提炼口径,绝不盲跑全量**。
10
-
11
- 本模块只做三件事(零 LLM、零第三方依赖,且**不改写任何节点**):
12
- 1. `plan` —— 确定性分层抽样出 20 条工单(原文摘录 / 字段缺口 / 口径声明);
13
- 2. `preview` —— 在抽检样本上跑与 `consolidate.induce` **同源的**聚类口径,
14
- 输出候选概念 + 每条共性条件的可追溯来源 + **样本内文档频率**
15
- (df 高 = 模板/通用水位线,不得当作区分性共性);
16
- 3. `apply`/`gate` —— 把抽检批次与人工裁决落 `_refine.jsonl` 留痕,并据此
17
- 计算「是否允许扩大批次」。
18
-
19
- 纪律与诚实边界:
20
- · 提炼执行仍由 `consolidate.induce` 承担(产出恒 `inferred`);本模块不实现
21
- 第二套提炼器,避免口径分叉——`preview` 复用同一套 `_jaccard/_common_terms`。
22
- · 抽检若取「20 条节点」,在库内实测**可能产不出候选**(`min_cluster=3` 时
23
- 20 条随机样本 0 簇);此时如实报 `candidates=0` 并**拒绝放行扩批**,
24
- 由 `sample_adequacy` 提示「按候选为抽检单位」或上调样本量。
25
- · 人工裁决必须由库外人类给出;本模块只做算术与留痕,**不代替裁决**。
26
- · 共性条件的去模板判据只用**词面统计**,不做语义猜测(宁可漏判,不可误判)。
27
- """
28
- from __future__ import annotations
29
-
30
- import hashlib
31
- import math
32
- import os
33
- import time
34
-
35
- from . import consolidate, nodefile
36
- from .fsutil import append_jsonl, read_jsonl
37
- from .mdcos import MdCGOS
38
-
39
- REFINE_LOG = "_refine.jsonl"
40
-
41
- #: 抽检对象 id 前缀(P37 派生记忆)
42
- PREFIX_DEFAULT = "node_"
43
- #: 裁定单条件 A 规定的抽检条数
44
- SAMPLE_N = 20
45
- #: 抽样种子:同 seed ⇒ 同样本(可复算、可复核)
46
- SAMPLE_SEED = "g6-sample-v1"
47
-
48
- MIN_JACCARD = consolidate.INDUCE_MIN_JACCARD
49
- MIN_CLUSTER = consolidate.INDUCE_MIN_CLUSTER
50
- MAX_TERMS = consolidate.INDUCE_MAX_TERMS
51
-
52
- #: 共性条件在**抽检样本**内的文档频率 ≥ 该值 → 判为模板/通用水位线,不具区分性
53
- GENERIC_DF = 0.80
54
- #: 工单正文摘录上限(超出即标 truncated;完整原文请 `cg read`)
55
- EXCERPT_MAX = 4000
56
- #: 扩批通过率闸门:人工核对「忠实」比例下限
57
- GATE_MIN_PASS_RATE = 0.90
58
-
59
- #: 样本量校准:目标产出候选数 / 有界爬坡上限(样本量非唯一杠杆,故设上限)
60
- CALIBRATE_TARGET = 20
61
- CALIBRATE_CAP = 200
62
-
63
- #: 人工核对裁决键(与工单 review_items 一一对应)
64
- VERDICT_KEYS = ("faithful", "added_info")
65
-
66
- #: 提炼口径声明——人工核对的就是这份口径,不是某一次的输出
67
- SPEC = {
68
- "method": "consolidate.induce 同源口径:bigram-jaccard 贪心聚类(确定性、零 LLM)",
69
- "min_cluster": MIN_CLUSTER,
70
- "min_jaccard": MIN_JACCARD,
71
- "fields": ["功能名", "生效条件", "子功能", "执行", "不适用条件"],
72
- "evidence": "inferred(未经验证,不得直接当事实使用)",
73
- "common_condition_source": "成员 positive_body 的词面统计(不做语义推断)",
74
- "review_items": [
75
- {"key": "faithful",
76
- "ask": "每条共性条件是否能在**全部**成员原文中原样找到"
77
- "(grounding_gap 为空即无缺口)"},
78
- {"key": "added_info",
79
- "ask": "概念是否引入了成员原文之外的词(外部知识/编造)——命中即整批否决"},
80
- {"key": "discriminating",
81
- "ask": "共性条件是否为区分性特征;df≥%.2f 的模板词不得充当共性"
82
- % GENERIC_DF},
83
- {"key": "member_consistency",
84
- "ask": "成员是否真属同一范畴(无强行拼团)"},
85
- ],
86
- "gate": {"min_pass_rate": GATE_MIN_PASS_RATE,
87
- "require_review_all": True,
88
- "fail_on_added_info": True},
89
- "honest_limits": [
90
- "本模块不执行提炼写入;扩批需 `consolidate.induce` 另开批次执行并留痕",
91
- "人工裁决由库外人类给出,本模块不代替裁决",
92
- ],
93
- }
94
-
95
- # ---- 通用工具 -------------------------------------------------------------
96
-
97
- # 生效条件:x 为 str 时返回 MdCGOS(x),否则原样返回 x;
98
- def _as_cg(x):
99
- return MdCGOS(x) if isinstance(x, str) else x
100
-
101
-
102
- # 生效条件:cg 具 "root" 属性时以该值为根、否则以 str(cg) 为根,与模块常量 REFINE_LOG 拼接返回;
103
- def _log_path(cg) -> str:
104
- root = cg.root if hasattr(cg, "root") else str(cg)
105
- return os.path.join(root, REFINE_LOG)
106
-
107
-
108
- # 生效条件:read_jsonl(_log_path(cg)) 返回假值时按空列表处理,仅保留 action 字段等于 "refine" 的记录;
109
- def _read_log(cg) -> list:
110
- return [r for r in (read_jsonl(_log_path(cg)) or [])
111
- if r.get("action") == "refine"]
112
-
113
-
114
- # 生效条件:cg 无 index 或 index["nodes"] 为假值时以空字典查找;nodes.get(nid) 为假值时返回空字典 {};
115
- def _entry(cg, nid):
116
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
117
- return nodes.get(nid) or {}
118
-
119
-
120
- # 生效条件:取 _entry(cg, nid) 的 path(假值回落空串)的父目录 basename;该 basename 为假值时返回 "(root)";
121
- def _family(cg, nid):
122
- """家族 = 节点所在 `cond_*` 目录名(无则 `(root)`)。"""
123
- p = str(_entry(cg, nid).get("path") or "").replace("\\", "/")
124
- d = os.path.basename(os.path.dirname(p))
125
- return d or "(root)"
126
-
127
-
128
- # 生效条件:对传入的 parts 逐项 str 后以 "|" 连接并 UTF-8 编码,返回 sha1 的 hexdigest;
129
- def _sha(*parts) -> str:
130
- h = hashlib.sha1("|".join(str(p) for p in parts).encode("utf-8"))
131
- return h.hexdigest()
132
-
133
-
134
- # 生效条件:遍历 cg.index 的 nodes,仅收 str(nid) 以 prefix 开头且条目 protected 为假值的 nid 进 ids(排序后返回),protected 为真值的计入 protected 计数;
135
- def _pool(cg, prefix) -> tuple:
136
- """抽检池:id 以 prefix 开头、非受保护节点(与 induce 的池口径一致)。"""
137
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
138
- ids, protected = [], 0
139
- for nid, e in nodes.items():
140
- if not str(nid).startswith(prefix):
141
- continue
142
- if (e or {}).get("protected"):
143
- protected += 1
144
- continue
145
- ids.append(nid)
146
- ids.sort()
147
- return ids, protected
148
-
149
-
150
- # 生效条件:cg 的 id 池经 prefix(假值回落 PREFIX_DEFAULT)过滤后,n 为 None 用 SAMPLE_N、否则 int(n),seed 假值回落 SAMPLE_SEED;pool 为空或 n<=0 时返回 ([], meta),否则按家族分层最大余数分配并在层内按 _sha(seed, id) 升序取前 k 个返回 (picked, meta);
151
- def sample_ids(cg, n=None, seed=None, prefix=None) -> tuple:
152
- """确定性分层抽样(家族=层,家族内按 sha1(seed|id) 排序)。
153
-
154
- 分层 + 最大余数分配:主家族按规模拿到多数名额,稀有家族亦保留席位(n 足够时)。
155
- 返回 `(ids, meta)`;同 `seed` 必得同样本——抽检可复核、可复算。
156
- """
157
- n = SAMPLE_N if n is None else int(n)
158
- seed = seed or SAMPLE_SEED
159
- prefix = prefix or PREFIX_DEFAULT
160
- pool, protected = _pool(cg, prefix)
161
- fam = {}
162
- for nid in pool:
163
- fam.setdefault(_family(cg, nid), []).append(nid)
164
- meta = {"prefix": prefix, "seed": seed, "requested": int(n),
165
- "pool": len(pool), "skipped_protected": protected,
166
- "families": len(fam), "sampled": 0, "strata": {}}
167
- if not pool or n <= 0:
168
- return [], meta
169
- n = min(int(n), len(pool))
170
- names = sorted(fam)
171
- raw = {f: n * len(fam[f]) / float(len(pool)) for f in names}
172
- alloc = {f: int(math.floor(raw[f])) for f in names}
173
- rest = n - sum(alloc.values())
174
- for f in sorted(names, key=lambda x: (-(raw[x] - alloc[x]), x))[:rest]:
175
- alloc[f] += 1
176
- picked, strata = [], {}
177
- for f in names:
178
- k = int(alloc[f])
179
- if k <= 0:
180
- continue
181
- rank = sorted(fam[f], key=lambda x: _sha(seed, x))
182
- take = rank[:k]
183
- picked.extend(take)
184
- strata[f] = {"pool": len(fam[f]), "picked": len(take)}
185
- picked.sort()
186
- meta["sampled"] = len(picked)
187
- meta["strata"] = strata
188
- return picked, meta
189
-
190
-
191
- # 生效条件:对 cg 中 nid 对应节点(cg.get(nid) 为假值时用空字典)生成字段;content 长度大于 EXCERPT_MAX 时 body 截断且 body_truncated 为 True,否则 body 为全 content;
192
- def _item(cg, nid) -> dict:
193
- node = cg.get(nid) or {}
194
- fm = node.get("frontmatter") or {}
195
- content = node.get("content") or ""
196
- comp = nodefile.ccg_completeness(content)
197
- e = _entry(cg, nid)
198
- truncated = len(content) > EXCERPT_MAX
199
- return {
200
- "id": nid, "family": _family(cg, nid), "layer": e.get("layer"),
201
- "tags": list(e.get("tags") or []), "importance": e.get("importance"),
202
- "edges": len(fm.get("edges") or []),
203
- "ccg_exempt": bool(fm.get("ccg_exempt")),
204
- "ccg_present": comp["present"],
205
- "ccg_missing": [m for m in nodefile.CCG_MARKS if m not in comp["present"]],
206
- "comment": fm.get("comment"),
207
- "body": content[:EXCERPT_MAX], "body_len": len(content),
208
- "body_truncated": truncated,
209
- "source_sha": _sha(content)[:16],
210
- }
211
-
212
-
213
- # ---- 预演:与 induce 同源的聚类口径 ---------------------------------------
214
-
215
- # 生效条件:pool 中能取到 grams 的 nid 进入 cache;按 keys 顺序贪心,与当前 a 的 _jaccard >= float(min_jaccard) 且未分配的后续 b 并入组,组大小达到 int(min_cluster) 才成组,返回 (cache, keys, groups);
216
- def _cluster(cg, pool, min_jaccard, min_cluster) -> tuple:
217
- """贪心聚类(与 `consolidate.induce_memories` 同源)。返回 (cache, keys, groups)。"""
218
- from . import subgraph
219
- cache = {}
220
- for nid in pool:
221
- got = subgraph._node_terms_and_grams(cg, nid)
222
- if got and got.get("grams"):
223
- cache[nid] = got
224
- keys = sorted(cache)
225
- assigned, groups = set(), []
226
- for i, a in enumerate(keys):
227
- if a in assigned:
228
- continue
229
- ga = cache[a]["grams"]
230
- grp = [b for b in keys[i + 1:]
231
- if b not in assigned
232
- and consolidate._jaccard(ga, cache[b]["grams"]) >= float(min_jaccard)]
233
- if len(grp) + 1 < int(min_cluster):
234
- continue
235
- members = [a] + grp
236
- assigned.update(members)
237
- groups.append(members)
238
- return cache, keys, groups
239
-
240
-
241
- # 生效条件:terms 为空时对每个 t 返回 df[t]=0;否则对 keys 中每个 k 的 cache[k]["pos"] 统计各 term 出现次数,返回 df;
242
- def _term_df(cache, keys, terms) -> dict:
243
- """词面在抽检样本内的文档频率(不含语义推断)。"""
244
- df = {t: 0 for t in terms}
245
- if not terms:
246
- return df
247
- for k in keys:
248
- pos = set(cache[k]["pos"])
249
- for t in terms:
250
- if t in pos:
251
- df[t] += 1
252
- return df
253
-
254
-
255
- # 生效条件:x 转为 cg;min_jaccard 为 None 取 MIN_JACCARD、否则 float(min_jaccard),min_cluster 为 None 取 MIN_CLUSTER、否则 int(min_cluster);ids 为真值时 pool 为显式 ids 去重排序且 meta 标记 explicit_ids=True,否则 pool/meta 来自 sample_ids(cg, n=n, seed=seed, prefix=prefix);对 pool 聚类后,require_conditions 为真且某组 common 为空时跳过该组并累计 skipped_no_cond;返回含 candidates 与 sample_adequacy 的只读预演字典;
256
- def preview(x, ids=None, n=None, seed=None, prefix=None,
257
- min_jaccard=None, min_cluster=None, require_conditions=True) -> dict:
258
- """提炼预演(只读):抽样 → 同源聚类 → 共性条件 + 来源 + 泛化度标记。"""
259
- cg = _as_cg(x)
260
- min_j = MIN_JACCARD if min_jaccard is None else float(min_jaccard)
261
- min_c = MIN_CLUSTER if min_cluster is None else int(min_cluster)
262
- if ids:
263
- pool = sorted({str(i) for i in ids})
264
- meta = {"prefix": prefix or PREFIX_DEFAULT, "seed": seed or SAMPLE_SEED,
265
- "requested": len(pool), "pool": len(pool),
266
- "skipped_protected": 0, "families": 0, "sampled": len(pool),
267
- "strata": {}, "explicit_ids": True}
268
- else:
269
- pool, meta = sample_ids(cg, n=n, seed=seed, prefix=prefix)
270
-
271
- cache, keys, groups = _cluster(cg, pool, min_j, min_c)
272
- base = max(1, len(keys))
273
- cands, skipped_no_cond, all_terms = [], 0, set()
274
- for members in groups:
275
- common = consolidate._common_terms([cache[m]["pos"] for m in members])
276
- if require_conditions and not common:
277
- skipped_no_cond += 1
278
- continue
279
- neg = consolidate._union_terms([cache[m]["neg"] for m in members])
280
- all_terms.update(common)
281
- cands.append({"concept_id": consolidate._concept_id(members),
282
- "members": members, "size": len(members),
283
- "common_conditions": common,
284
- "non_applicable": neg})
285
-
286
- df = _term_df(cache, keys, sorted(all_terms))
287
- for c in cands:
288
- members = c["members"]
289
- src = {t: [m for m in members if t in cache[m]["pos"]]
290
- for t in c["common_conditions"]}
291
- gap = [t for t, ms in src.items() if len(ms) < len(members)]
292
- generic = [{"term": t, "df": df.get(t, 0),
293
- "ratio": round(df.get(t, 0) / float(base), 4)}
294
- for t in c["common_conditions"]
295
- if df.get(t, 0) / float(base) >= GENERIC_DF]
296
- c.update({
297
- "condition_sources": src,
298
- "grounding_gap": gap,
299
- "generic_conditions": generic,
300
- "discriminating": [t for t in c["common_conditions"]
301
- if df.get(t, 0) / float(base) < GENERIC_DF],
302
- "reason": ("%d 条记忆内容相近且共享条件「%s」→ 归纳为概念"
303
- % (len(members),
304
- "、".join(c["common_conditions"][:MAX_TERMS]) or "无")),
305
- })
306
- generic_only = sum(1 for c in cands if not c["discriminating"])
307
- adequacy = "ok" if cands else "insufficient"
308
- return {
309
- "root": cg.root, "dry_run": True, "readonly": True,
310
- "action": "refine_preview", "op": "maintain",
311
- "prefix": meta["prefix"], "seed": meta["seed"],
312
- "requested": meta["requested"], "pool": meta["pool"],
313
- "skipped_protected": meta["skipped_protected"],
314
- "families": meta["families"], "strata": meta["strata"],
315
- "sampled": meta["sampled"], "sample": pool,
316
- "indexed": len(keys), "min_cluster": min_c, "min_jaccard": min_j,
317
- "require_conditions": bool(require_conditions),
318
- "clusters": len(cands), "candidates": cands,
319
- "skipped_no_condition": skipped_no_cond,
320
- "candidates_generic_only": generic_only,
321
- "df_basis": "sampled", "generic_df_threshold": GENERIC_DF,
322
- "sample_adequacy": adequacy,
323
- "note": ("预演:未写盘、未改任何节点;候选供人工核对提炼口径"
324
- if cands else
325
- "抽检样本不足以产出候选(样本量或抽检单位需校准),不得据此扩批"),
326
- }
327
-
328
-
329
- # ---- 工单 ---------------------------------------------------------------
330
-
331
- # 生效条件:x 转为 cg;prefix 假值回落 PREFIX_DEFAULT,seed 假值回落 SAMPLE_SEED;ids 为真值时 sample 为显式 ids 去重排序且 real_pool=len(sample)、skipped=0、families=0,否则 sample/meta 来自 sample_ids(cg, n=n, seed=seed, prefix=prefix) 并取 meta["pool"]/meta["skipped_protected"]/meta["families"];items 为 sample 的 _item,preview 以 sample 为显式 ids 调用,返回只读工单;
332
- def plan(x, ids=None, n=None, seed=None, prefix=None,
333
- min_jaccard=None, min_cluster=None) -> dict:
334
- """抽检工单(只读):确定性样本 + 原文摘录 + 字段缺口 + 口径声明 + 预演。"""
335
- cg = _as_cg(x)
336
- prefix = prefix or PREFIX_DEFAULT
337
- seed = seed or SAMPLE_SEED
338
- if ids:
339
- sample = sorted({str(i) for i in ids})
340
- real_pool, skipped, families = len(sample), 0, 0
341
- else:
342
- sample, meta = sample_ids(cg, n=n, seed=seed, prefix=prefix)
343
- real_pool, skipped, families = (meta["pool"], meta["skipped_protected"],
344
- meta["families"])
345
- items = [_item(cg, nid) for nid in sample]
346
- pv = preview(cg, ids=sample, prefix=prefix,
347
- min_jaccard=min_jaccard, min_cluster=min_cluster)
348
- strata = {}
349
- for it in items:
350
- k = it["family"]
351
- s = strata.setdefault(k, {"picked": 0, "missing_fields": {},
352
- "ccg_exempt": 0})
353
- s["picked"] += 1
354
- if it["ccg_exempt"]:
355
- s["ccg_exempt"] += 1
356
- for m in it["ccg_missing"]:
357
- s["missing_fields"][m] = s["missing_fields"].get(m, 0) + 1
358
- return {
359
- "root": cg.root, "dry_run": True, "readonly": True,
360
- "action": "refine", "op": "maintain",
361
- "prefix": prefix, "seed": seed,
362
- "requested": n if n is not None else (len(sample) if ids else SAMPLE_N),
363
- "pool": real_pool, "skipped_protected": skipped,
364
- "sampled": len(items), "families": families,
365
- "sample": sample, "sample_sha": _sha(*sample)[:16],
366
- "strata": strata, "items": items,
367
- "worklist": items, "spec": SPEC,
368
- "preview": {"clusters": pv["clusters"], "candidates": pv["candidates"],
369
- "min_cluster": pv["min_cluster"],
370
- "min_jaccard": pv["min_jaccard"],
371
- "skipped_no_condition": pv["skipped_no_condition"],
372
- "candidates_generic_only": pv["candidates_generic_only"],
373
- "sample_adequacy": pv["sample_adequacy"],
374
- "df_basis": pv["df_basis"],
375
- "generic_df_threshold": pv["generic_df_threshold"]},
376
- "gate_rule": SPEC["gate"],
377
- "note": ("抽检工单(只读):供人工核对提炼口径;核对通过前不得扩批。"
378
- "行 `apply=true` 只落抽检留痕,不改任何节点。"),
379
- }
380
-
381
-
382
- # 生效条件:x 转为 cg;target 为 None 取 CALIBRATE_TARGET、否则 int(target),cap 为 None 取 CALIBRATE_CAP、否则 int(cap),初始 n=min(SAMPLE_N if n0 is None else int(n0), cap);循环以 n 调 preview(cg, n=n, seed=seed, prefix=prefix, min_jaccard=min_jaccard, min_cluster=min_cluster) 并记录 step,直到 pv["clusters"] >= target 则 chosen=step 并 break,或 n >= min(cap, pool or cap) 则 break;chosen 为 None 时 blocked=True 并按 steps 汇总 recommend,否则 recommend 基于 chosen;返回只读校准报告;
383
- def calibrate(x, target=None, cap=None, n0=None, seed=None, prefix=None,
384
- min_jaccard=None, min_cluster=None) -> dict:
385
- """样本量校准(只读):20 条起步有界爬坡,直到产出 `target` 个候选或触顶。
386
-
387
- 目的是回答「20 条工单为什么交不出候选」,并给出**有界**的替代样本量;
388
- 若爬坡到顶仍无候选、或候选共性条件全为高 df 模板词,则如实报
389
- `blocked` 并指出真正的瓶颈是**提炼口径**而非样本量——不得据此扩批。
390
- """
391
- cg = _as_cg(x)
392
- target = CALIBRATE_TARGET if target is None else int(target)
393
- cap = CALIBRATE_CAP if cap is None else int(cap)
394
- n = min(SAMPLE_N if n0 is None else int(n0), cap)
395
- steps, chosen, pool = [], None, None
396
- while True:
397
- pv = preview(cg, n=n, seed=seed, prefix=prefix,
398
- min_jaccard=min_jaccard, min_cluster=min_cluster)
399
- pool = pv["pool"]
400
- step = {"n": n, "sampled": pv["sampled"], "clusters": pv["clusters"],
401
- "candidates_generic_only": pv["candidates_generic_only"],
402
- "discriminating_terms": sorted(
403
- {t for c in pv["candidates"] for t in c["discriminating"]})[:12]}
404
- steps.append(step)
405
- if pv["clusters"] >= target:
406
- chosen = step
407
- break
408
- top = min(cap, pool or cap)
409
- if n >= top:
410
- break
411
- n = min(n * 2, top)
412
- blocked = chosen is None
413
- saw_clusters = any(s["clusters"] for s in steps)
414
- total_c = sum(s["clusters"] for s in steps)
415
- gen_only = sum(s["candidates_generic_only"] for s in steps)
416
- if not blocked:
417
- rec = ("抽检样本量校准为 n=%d(产出 %d 个候选);仍须人工核对提炼口径,"
418
- "核对通过前不得扩批" % (chosen["n"], chosen["clusters"]))
419
- else:
420
- parts = ["爬坡至 cap=%d 仍只产出 %d 个候选(< target=%d),样本量不足以"
421
- "直接支撑抽检" % (cap, steps[-1]["clusters"], target)]
422
- if saw_clusters:
423
- if gen_only >= total_c:
424
- parts.append("且已产出候选的共性条件**全部**为高 df 模板词(无区分性)")
425
- else:
426
- parts.append("且候选中 %d/%d 的共性条件全为高 df 模板词,"
427
- "其余『区分性』项形如整行条件文本(含时间戳),"
428
- "属归一化缺失导致的伪区分性" % (gen_only, total_c))
429
- parts.append("须先修正提炼口径(剔除模板词 + 归一化/裁剪整行文本)"
430
- "或以「候选」为抽检单位重新校准;当前状态不得扩批")
431
- rec = ";".join(parts)
432
- return {"ok": True, "action": "refine_calibrate", "op": "maintain",
433
- "root": cg.root, "dry_run": True, "readonly": True,
434
- "target": target, "cap": cap, "pool": pool,
435
- "steps": steps, "chosen": chosen, "blocked": blocked,
436
- "sample_adequacy": "ok" if not blocked else "insufficient",
437
- "recommend": rec, "note": "只读校准:未写盘、未改任何节点。"}
438
-
439
-
440
- # ---- 留痕与扩批闸门 ------------------------------------------------------
441
-
442
- # 生效条件:candidates 中取 concept_id 为真值的 id 列表;verdicts 中为 dict 且 concept_id 非空且首次出现的条目计入 reviewed 并累加其中 faithful/added_info 的真值计数;ids 非空时按 reviewed < len(ids) → awaiting_human_review、added 非零 → added_info_detected、faithful_rate < GATE_MIN_PASS_RATE → faithful_rate_below_gate、否则 ok 且 expand_allowed=True;ids 为空时 reason="no_candidates" 且 expand_allowed=False;
443
- def _verdict_stats(verdicts, candidates) -> dict:
444
- ids = [c.get("concept_id") for c in candidates if c.get("concept_id")]
445
- seen, faithful, added = set(), 0, 0
446
- for v in verdicts or []:
447
- if not isinstance(v, dict):
448
- continue
449
- cid = v.get("concept_id")
450
- if not cid or cid in seen:
451
- continue
452
- seen.add(cid)
453
- faithful += 1 if v.get("faithful") else 0
454
- added += 1 if v.get("added_info") else 0
455
- reviewed = len(seen)
456
- rate = (faithful / float(reviewed)) if reviewed else 0.0
457
- expand, reason = False, "no_candidates"
458
- if ids:
459
- if reviewed < len(ids):
460
- reason = "awaiting_human_review"
461
- elif added:
462
- reason = "added_info_detected"
463
- elif rate < GATE_MIN_PASS_RATE:
464
- reason = "faithful_rate_below_gate"
465
- else:
466
- reason = "ok"
467
- expand = True
468
- return {"candidates": len(ids), "reviewed": reviewed,
469
- "faithful": faithful, "added_info": added,
470
- "faithful_rate": round(rate, 4),
471
- "min_pass_rate": GATE_MIN_PASS_RATE,
472
- "expand_allowed": expand, "reason": reason}
473
-
474
-
475
- # 生效条件:x 转为 cg;以 ids/n/seed/prefix/min_jaccard/min_cluster 调 plan 得到 p,batch 假值(含 None/空串)回落当前时间字符串;从 p["preview"]["candidates"] 与 verdicts 经 _verdict_stats 得 stats;把含 batch/actor/prefix/seed/sample/verdicts/gate/note 的 record 追加写入 _log_path(cg),返回 rep;
476
- def apply(x, batch=None, actor=None, verdicts=None, note=None,
477
- ids=None, n=None, seed=None, prefix=None,
478
- min_jaccard=None, min_cluster=None) -> dict:
479
- """落抽检批次 + 人工裁决到 `_refine.jsonl`。**不改写任何节点**。"""
480
- cg = _as_cg(x)
481
- p = plan(cg, ids=ids, n=n, seed=seed, prefix=prefix,
482
- min_jaccard=min_jaccard, min_cluster=min_cluster)
483
- batch = batch or time.strftime("%Y%m%d-%H%M%S")
484
- cands = p["preview"]["candidates"]
485
- stats = _verdict_stats(verdicts, cands)
486
- record = {
487
- "t": time.time(), "action": "refine", "batch": batch, "actor": actor,
488
- "prefix": p["prefix"], "seed": p["seed"], "requested": p["requested"],
489
- "pool": p["pool"], "sampled": p["sampled"],
490
- "sample": p["sample"], "sample_sha": p["sample_sha"],
491
- "sample_adequacy": p["preview"]["sample_adequacy"],
492
- "min_cluster": p["preview"].get("min_cluster"),
493
- "candidates": [{"concept_id": c["concept_id"], "members": c["members"],
494
- "common_conditions": c["common_conditions"],
495
- "grounding_gap": c["grounding_gap"],
496
- "discriminating": c["discriminating"]}
497
- for c in cands],
498
- "verdicts": list(verdicts or []), "gate": stats, "note": note,
499
- }
500
- append_jsonl(_log_path(cg), record)
501
- rep = {"ok": True, "action": "refine", "op": "maintain", "batch": batch,
502
- "root": cg.root, "sampled": record["sampled"],
503
- "candidates": stats["candidates"], "reviewed": stats["reviewed"],
504
- "gate": stats, "log": REFINE_LOG,
505
- "note": ("抽检留痕已落盘(未改任何节点);"
506
- + ("闸门放行扩批(仍需另开 induce 批次并留痕)"
507
- if stats["expand_allowed"] else
508
- "闸门未放行:" + stats["reason"]))}
509
- return rep
510
-
511
-
512
- # 生效条件:x 转为 cg;batch 为真值时只保留 _read_log(cg) 中 batch 字段相等的记录,过滤后为空时返回 batches=0、expand_allowed=False、reason="no_batch";否则取最后一条记录,以其 verdicts 与 candidates(假值转空列表)经 _verdict_stats 复算并返回 expand_allowed/reason 等;
513
- def gate(x, batch=None) -> dict:
514
- """扩批闸门:读留痕复算通过率(不写盘)。"""
515
- cg = _as_cg(x)
516
- recs = _read_log(cg)
517
- if batch:
518
- recs = [r for r in recs if r.get("batch") == batch]
519
- if not recs:
520
- return {"ok": True, "action": "refine_gate", "op": "maintain",
521
- "root": cg.root, "batch": batch, "batches": 0,
522
- "expand_allowed": False, "reason": "no_batch"}
523
- rec = recs[-1]
524
- stats = _verdict_stats(rec.get("verdicts"), rec.get("candidates") or [])
525
- return {"ok": True, "action": "refine_gate", "op": "maintain",
526
- "root": cg.root, "batch": rec.get("batch"),
527
- "batches": len({r.get("batch") for r in recs}),
528
- "sampled": rec.get("sampled"),
529
- "sample_adequacy": rec.get("sample_adequacy"),
530
- "gate": stats, "expand_allowed": stats["expand_allowed"],
531
- "reason": stats["reason"],
532
- "note": ("闸门放行:可另开 induce 批次扩大提炼规模并逐批留痕"
533
- if stats["expand_allowed"] else
534
- "闸门未放行,禁止扩大批次;不得盲跑全量。")}
535
-
536
-
537
- # 生效条件:batch 为真值时只保留该批次记录,total 先记为过滤后条数;limit 非 None 且 >=0 时以 recs[-int(limit):] 截尾(limit=0 因 [-0:] 等价 [0:] 仍返回全部记录),limit 为 None 或负数时不截断;
538
- def history(x, limit=100, batch=None) -> dict:
539
- cg = _as_cg(x)
540
- recs = _read_log(cg)
541
- if batch:
542
- recs = [r for r in recs if r.get("batch") == batch]
543
- total = len(recs)
544
- if limit is not None and limit >= 0:
545
- recs = recs[-int(limit):]
546
- return {"root": cg.root, "total": total, "returned": len(recs),
547
- "action": "refine", "records": recs}
548
-
549
-
550
- # ---- CLI(真实库抽检/预演用;MCP 侧走 maintain action) -------------------
551
-
552
- # 生效条件:解析 argv(None 时由 argparse 取 sys.argv)得到 root/n/seed/prefix/min-jaccard/min-cluster/report 等参数;a.calibrate 为真时调 calibrate 并打印 target/cap/pool/blocked/sample_adequacy/recommend 及每步摘要;否则 a.preview 为真选 preview、为假选 plan,调用后打印 thin 摘要与候选并返回 0;
553
- def _main(argv=None) -> int:
554
- import argparse
555
- import json
556
- ap = argparse.ArgumentParser(description="G6 提炼抽检(默认只预演,不写盘)")
557
- ap.add_argument("root", help="认知图库根")
558
- ap.add_argument("--n", type=int, default=SAMPLE_N, help="抽检条数(默认 20)")
559
- ap.add_argument("--seed", default=SAMPLE_SEED, help="抽样种子(同 seed 同样本)")
560
- ap.add_argument("--prefix", default=PREFIX_DEFAULT, help="id 前缀(默认 node_)")
561
- ap.add_argument("--min-jaccard", type=float, default=None)
562
- ap.add_argument("--min-cluster", type=int, default=None)
563
- ap.add_argument("--preview", action="store_true", help="只出预演(不出工单正文)")
564
- ap.add_argument("--calibrate", action="store_true",
565
- help="只做样本量校准(20 条起步有界爬坡)")
566
- ap.add_argument("--report", default=None, help="把汇总 JSON 另存一份")
567
- a = ap.parse_args(argv)
568
- if a.calibrate:
569
- rep = calibrate(a.root, seed=a.seed, prefix=a.prefix,
570
- min_jaccard=a.min_jaccard, min_cluster=a.min_cluster)
571
- if a.report:
572
- with open(a.report, "w", encoding="utf-8") as f:
573
- json.dump(rep, f, ensure_ascii=False, indent=2)
574
- print(json.dumps({k: rep[k] for k in
575
- ("target", "cap", "pool", "blocked",
576
- "sample_adequacy", "recommend")},
577
- ensure_ascii=False, indent=2))
578
- for s in rep["steps"]:
579
- print(" - n=%4d clusters=%2d generic_only=%2d discriminating=%s"
580
- % (s["n"], s["clusters"], s["candidates_generic_only"],
581
- s["discriminating_terms"][:6]))
582
- return 0
583
- fn = preview if a.preview else plan
584
- rep = fn(a.root, n=a.n, seed=a.seed, prefix=a.prefix,
585
- min_jaccard=a.min_jaccard, min_cluster=a.min_cluster)
586
- if a.report:
587
- with open(a.report, "w", encoding="utf-8") as f:
588
- json.dump(rep, f, ensure_ascii=False, indent=2)
589
- pv = rep.get("preview") if "preview" in rep else rep
590
- thin = {k: rep.get(k) for k in ("action", "prefix", "seed", "requested",
591
- "pool", "sampled", "families")}
592
- thin.update({k: pv.get(k) for k in ("clusters", "sample_adequacy",
593
- "candidates_generic_only")})
594
- print(json.dumps(thin, ensure_ascii=False, indent=2))
595
- for c in (pv.get("candidates") or []):
596
- print(" - %s size=%d common=%s generic=%s gap=%s"
597
- % (c["concept_id"], c["size"], c["common_conditions"][:4],
598
- [g["term"] for g in c.get("generic_conditions", [])],
599
- c.get("grounding_gap")))
600
- return 0
601
-
602
-
603
- if __name__ == "__main__":
604
- import sys
1
+ # -*- coding: utf-8 -*-
2
+ """G6:node_ 结构提炼 · 小样本抽检工单与扩批闸门(只读预演,纯标准库)。
3
+
4
+ 缺口(G6):`node_*`(P37 双子管线派生记忆)自带 `ccg_exempt=true`,正文只有
5
+ 「功能名 / 生效条件」两行,`子功能 / 执行 / 不适用条件` 均未声明(实测 1324 条)。
6
+ 若直接对全量跑归纳提炼(`consolidate.induce`),一旦**提炼口径**失准,就会把
7
+ 模板化词面当成「共性条件」固化成概念节点,污染单一真相源。
8
+
9
+ 裁定单条件 A:**先抽 20 条做提炼预演,人工核对提炼口径,绝不盲跑全量**。
10
+
11
+ 本模块只做三件事(零 LLM、零第三方依赖,且**不改写任何节点**):
12
+ 1. `plan` —— 确定性分层抽样出 20 条工单(原文摘录 / 字段缺口 / 口径声明);
13
+ 2. `preview` —— 在抽检样本上跑与 `consolidate.induce` **同源的**聚类口径,
14
+ 输出候选概念 + 每条共性条件的可追溯来源 + **样本内文档频率**
15
+ (df 高 = 模板/通用水位线,不得当作区分性共性);
16
+ 3. `apply`/`gate` —— 把抽检批次与人工裁决落 `_refine.jsonl` 留痕,并据此
17
+ 计算「是否允许扩大批次」。
18
+
19
+ 纪律与诚实边界:
20
+ · 提炼执行仍由 `consolidate.induce` 承担(产出恒 `inferred`);本模块不实现
21
+ 第二套提炼器,避免口径分叉——`preview` 复用同一套 `_jaccard/_common_terms`。
22
+ · 抽检若取「20 条节点」,在库内实测**可能产不出候选**(`min_cluster=3` 时
23
+ 20 条随机样本 0 簇);此时如实报 `candidates=0` 并**拒绝放行扩批**,
24
+ 由 `sample_adequacy` 提示「按候选为抽检单位」或上调样本量。
25
+ · 人工裁决必须由库外人类给出;本模块只做算术与留痕,**不代替裁决**。
26
+ · 共性条件的去模板判据只用**词面统计**,不做语义猜测(宁可漏判,不可误判)。
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import hashlib
31
+ import math
32
+ import os
33
+ import time
34
+
35
+ from . import consolidate, nodefile
36
+ from .fsutil import append_jsonl, read_jsonl
37
+ from .mdcos import MdCGOS
38
+
39
+ REFINE_LOG = "_refine.jsonl"
40
+
41
+ #: 抽检对象 id 前缀(P37 派生记忆)
42
+ PREFIX_DEFAULT = "node_"
43
+ #: 裁定单条件 A 规定的抽检条数
44
+ SAMPLE_N = 20
45
+ #: 抽样种子:同 seed ⇒ 同样本(可复算、可复核)
46
+ SAMPLE_SEED = "g6-sample-v1"
47
+
48
+ MIN_JACCARD = consolidate.INDUCE_MIN_JACCARD
49
+ MIN_CLUSTER = consolidate.INDUCE_MIN_CLUSTER
50
+ MAX_TERMS = consolidate.INDUCE_MAX_TERMS
51
+
52
+ #: 共性条件在**抽检样本**内的文档频率 ≥ 该值 → 判为模板/通用水位线,不具区分性
53
+ GENERIC_DF = 0.80
54
+ #: 工单正文摘录上限(超出即标 truncated;完整原文请 `cg read`)
55
+ EXCERPT_MAX = 4000
56
+ #: 扩批通过率闸门:人工核对「忠实」比例下限
57
+ GATE_MIN_PASS_RATE = 0.90
58
+
59
+ #: 样本量校准:目标产出候选数 / 有界爬坡上限(样本量非唯一杠杆,故设上限)
60
+ CALIBRATE_TARGET = 20
61
+ CALIBRATE_CAP = 200
62
+
63
+ #: 人工核对裁决键(与工单 review_items 一一对应)
64
+ VERDICT_KEYS = ("faithful", "added_info")
65
+
66
+ #: 提炼口径声明——人工核对的就是这份口径,不是某一次的输出
67
+ SPEC = {
68
+ "method": "consolidate.induce 同源口径:bigram-jaccard 贪心聚类(确定性、零 LLM)",
69
+ "min_cluster": MIN_CLUSTER,
70
+ "min_jaccard": MIN_JACCARD,
71
+ "fields": ["功能名", "生效条件", "子功能", "执行", "不适用条件"],
72
+ "evidence": "inferred(未经验证,不得直接当事实使用)",
73
+ "common_condition_source": "成员 positive_body 的词面统计(不做语义推断)",
74
+ "review_items": [
75
+ {"key": "faithful",
76
+ "ask": "每条共性条件是否能在**全部**成员原文中原样找到"
77
+ "(grounding_gap 为空即无缺口)"},
78
+ {"key": "added_info",
79
+ "ask": "概念是否引入了成员原文之外的词(外部知识/编造)——命中即整批否决"},
80
+ {"key": "discriminating",
81
+ "ask": "共性条件是否为区分性特征;df≥%.2f 的模板词不得充当共性"
82
+ % GENERIC_DF},
83
+ {"key": "member_consistency",
84
+ "ask": "成员是否真属同一范畴(无强行拼团)"},
85
+ ],
86
+ "gate": {"min_pass_rate": GATE_MIN_PASS_RATE,
87
+ "require_review_all": True,
88
+ "fail_on_added_info": True},
89
+ "honest_limits": [
90
+ "本模块不执行提炼写入;扩批需 `consolidate.induce` 另开批次执行并留痕",
91
+ "人工裁决由库外人类给出,本模块不代替裁决",
92
+ ],
93
+ }
94
+
95
+ # ---- 通用工具 -------------------------------------------------------------
96
+
97
+ # 生效条件:x 为 str 时返回 MdCGOS(x),否则原样返回 x;
98
+ def _as_cg(x):
99
+ return MdCGOS(x) if isinstance(x, str) else x
100
+
101
+
102
+ # 生效条件:cg 具 "root" 属性时以该值为根、否则以 str(cg) 为根,与模块常量 REFINE_LOG 拼接返回;
103
+ def _log_path(cg) -> str:
104
+ root = cg.root if hasattr(cg, "root") else str(cg)
105
+ return os.path.join(root, REFINE_LOG)
106
+
107
+
108
+ # 生效条件:read_jsonl(_log_path(cg)) 返回假值时按空列表处理,仅保留 action 字段等于 "refine" 的记录;
109
+ def _read_log(cg) -> list:
110
+ return [r for r in (read_jsonl(_log_path(cg)) or [])
111
+ if r.get("action") == "refine"]
112
+
113
+
114
+ # 生效条件:cg 无 index 或 index["nodes"] 为假值时以空字典查找;nodes.get(nid) 为假值时返回空字典 {};
115
+ def _entry(cg, nid):
116
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
117
+ return nodes.get(nid) or {}
118
+
119
+
120
+ # 生效条件:取 _entry(cg, nid) 的 path(假值回落空串)的父目录 basename;该 basename 为假值时返回 "(root)";
121
+ def _family(cg, nid):
122
+ """家族 = 节点所在 `cond_*` 目录名(无则 `(root)`)。"""
123
+ p = str(_entry(cg, nid).get("path") or "").replace("\\", "/")
124
+ d = os.path.basename(os.path.dirname(p))
125
+ return d or "(root)"
126
+
127
+
128
+ # 生效条件:对传入的 parts 逐项 str 后以 "|" 连接并 UTF-8 编码,返回 sha1 的 hexdigest;
129
+ def _sha(*parts) -> str:
130
+ h = hashlib.sha1("|".join(str(p) for p in parts).encode("utf-8"))
131
+ return h.hexdigest()
132
+
133
+
134
+ # 生效条件:遍历 cg.index 的 nodes,仅收 str(nid) 以 prefix 开头且条目 protected 为假值的 nid 进 ids(排序后返回),protected 为真值的计入 protected 计数;
135
+ def _pool(cg, prefix) -> tuple:
136
+ """抽检池:id 以 prefix 开头、非受保护节点(与 induce 的池口径一致)。"""
137
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
138
+ ids, protected = [], 0
139
+ for nid, e in nodes.items():
140
+ if not str(nid).startswith(prefix):
141
+ continue
142
+ if (e or {}).get("protected"):
143
+ protected += 1
144
+ continue
145
+ ids.append(nid)
146
+ ids.sort()
147
+ return ids, protected
148
+
149
+
150
+ # 生效条件:cg 的 id 池经 prefix(假值回落 PREFIX_DEFAULT)过滤后,n 为 None 用 SAMPLE_N、否则 int(n),seed 假值回落 SAMPLE_SEED;pool 为空或 n<=0 时返回 ([], meta),否则按家族分层最大余数分配并在层内按 _sha(seed, id) 升序取前 k 个返回 (picked, meta);
151
+ def sample_ids(cg, n=None, seed=None, prefix=None) -> tuple:
152
+ """确定性分层抽样(家族=层,家族内按 sha1(seed|id) 排序)。
153
+
154
+ 分层 + 最大余数分配:主家族按规模拿到多数名额,稀有家族亦保留席位(n 足够时)。
155
+ 返回 `(ids, meta)`;同 `seed` 必得同样本——抽检可复核、可复算。
156
+ """
157
+ n = SAMPLE_N if n is None else int(n)
158
+ seed = seed or SAMPLE_SEED
159
+ prefix = prefix or PREFIX_DEFAULT
160
+ pool, protected = _pool(cg, prefix)
161
+ fam = {}
162
+ for nid in pool:
163
+ fam.setdefault(_family(cg, nid), []).append(nid)
164
+ meta = {"prefix": prefix, "seed": seed, "requested": int(n),
165
+ "pool": len(pool), "skipped_protected": protected,
166
+ "families": len(fam), "sampled": 0, "strata": {}}
167
+ if not pool or n <= 0:
168
+ return [], meta
169
+ n = min(int(n), len(pool))
170
+ names = sorted(fam)
171
+ raw = {f: n * len(fam[f]) / float(len(pool)) for f in names}
172
+ alloc = {f: int(math.floor(raw[f])) for f in names}
173
+ rest = n - sum(alloc.values())
174
+ for f in sorted(names, key=lambda x: (-(raw[x] - alloc[x]), x))[:rest]:
175
+ alloc[f] += 1
176
+ picked, strata = [], {}
177
+ for f in names:
178
+ k = int(alloc[f])
179
+ if k <= 0:
180
+ continue
181
+ rank = sorted(fam[f], key=lambda x: _sha(seed, x))
182
+ take = rank[:k]
183
+ picked.extend(take)
184
+ strata[f] = {"pool": len(fam[f]), "picked": len(take)}
185
+ picked.sort()
186
+ meta["sampled"] = len(picked)
187
+ meta["strata"] = strata
188
+ return picked, meta
189
+
190
+
191
+ # 生效条件:对 cg 中 nid 对应节点(cg.get(nid) 为假值时用空字典)生成字段;content 长度大于 EXCERPT_MAX 时 body 截断且 body_truncated 为 True,否则 body 为全 content;
192
+ def _item(cg, nid) -> dict:
193
+ node = cg.get(nid) or {}
194
+ fm = node.get("frontmatter") or {}
195
+ content = node.get("content") or ""
196
+ comp = nodefile.ccg_completeness(content)
197
+ e = _entry(cg, nid)
198
+ truncated = len(content) > EXCERPT_MAX
199
+ return {
200
+ "id": nid, "family": _family(cg, nid), "layer": e.get("layer"),
201
+ "tags": list(e.get("tags") or []), "importance": e.get("importance"),
202
+ "edges": len(fm.get("edges") or []),
203
+ "ccg_exempt": bool(fm.get("ccg_exempt")),
204
+ "ccg_present": comp["present"],
205
+ "ccg_missing": [m for m in nodefile.CCG_MARKS if m not in comp["present"]],
206
+ "comment": fm.get("comment"),
207
+ "body": content[:EXCERPT_MAX], "body_len": len(content),
208
+ "body_truncated": truncated,
209
+ "source_sha": _sha(content)[:16],
210
+ }
211
+
212
+
213
+ # ---- 预演:与 induce 同源的聚类口径 ---------------------------------------
214
+
215
+ # 生效条件:pool 中能取到 grams 的 nid 进入 cache;按 keys 顺序贪心,与当前 a 的 _jaccard >= float(min_jaccard) 且未分配的后续 b 并入组,组大小达到 int(min_cluster) 才成组,返回 (cache, keys, groups);
216
+ def _cluster(cg, pool, min_jaccard, min_cluster) -> tuple:
217
+ """贪心聚类(与 `consolidate.induce_memories` 同源)。返回 (cache, keys, groups)。"""
218
+ from . import subgraph
219
+ cache = {}
220
+ for nid in pool:
221
+ got = subgraph._node_terms_and_grams(cg, nid)
222
+ if got and got.get("grams"):
223
+ cache[nid] = got
224
+ keys = sorted(cache)
225
+ assigned, groups = set(), []
226
+ for i, a in enumerate(keys):
227
+ if a in assigned:
228
+ continue
229
+ ga = cache[a]["grams"]
230
+ grp = [b for b in keys[i + 1:]
231
+ if b not in assigned
232
+ and consolidate._jaccard(ga, cache[b]["grams"]) >= float(min_jaccard)]
233
+ if len(grp) + 1 < int(min_cluster):
234
+ continue
235
+ members = [a] + grp
236
+ assigned.update(members)
237
+ groups.append(members)
238
+ return cache, keys, groups
239
+
240
+
241
+ # 生效条件:terms 为空时对每个 t 返回 df[t]=0;否则对 keys 中每个 k 的 cache[k]["pos"] 统计各 term 出现次数,返回 df;
242
+ def _term_df(cache, keys, terms) -> dict:
243
+ """词面在抽检样本内的文档频率(不含语义推断)。"""
244
+ df = {t: 0 for t in terms}
245
+ if not terms:
246
+ return df
247
+ for k in keys:
248
+ pos = set(cache[k]["pos"])
249
+ for t in terms:
250
+ if t in pos:
251
+ df[t] += 1
252
+ return df
253
+
254
+
255
+ # 生效条件:x 转为 cg;min_jaccard 为 None 取 MIN_JACCARD、否则 float(min_jaccard),min_cluster 为 None 取 MIN_CLUSTER、否则 int(min_cluster);ids 为真值时 pool 为显式 ids 去重排序且 meta 标记 explicit_ids=True,否则 pool/meta 来自 sample_ids(cg, n=n, seed=seed, prefix=prefix);对 pool 聚类后,require_conditions 为真且某组 common 为空时跳过该组并累计 skipped_no_cond;返回含 candidates 与 sample_adequacy 的只读预演字典;
256
+ def preview(x, ids=None, n=None, seed=None, prefix=None,
257
+ min_jaccard=None, min_cluster=None, require_conditions=True) -> dict:
258
+ """提炼预演(只读):抽样 → 同源聚类 → 共性条件 + 来源 + 泛化度标记。"""
259
+ cg = _as_cg(x)
260
+ min_j = MIN_JACCARD if min_jaccard is None else float(min_jaccard)
261
+ min_c = MIN_CLUSTER if min_cluster is None else int(min_cluster)
262
+ if ids:
263
+ pool = sorted({str(i) for i in ids})
264
+ meta = {"prefix": prefix or PREFIX_DEFAULT, "seed": seed or SAMPLE_SEED,
265
+ "requested": len(pool), "pool": len(pool),
266
+ "skipped_protected": 0, "families": 0, "sampled": len(pool),
267
+ "strata": {}, "explicit_ids": True}
268
+ else:
269
+ pool, meta = sample_ids(cg, n=n, seed=seed, prefix=prefix)
270
+
271
+ cache, keys, groups = _cluster(cg, pool, min_j, min_c)
272
+ base = max(1, len(keys))
273
+ cands, skipped_no_cond, all_terms = [], 0, set()
274
+ for members in groups:
275
+ common = consolidate._common_terms([cache[m]["pos"] for m in members])
276
+ if require_conditions and not common:
277
+ skipped_no_cond += 1
278
+ continue
279
+ neg = consolidate._union_terms([cache[m]["neg"] for m in members])
280
+ all_terms.update(common)
281
+ cands.append({"concept_id": consolidate._concept_id(members),
282
+ "members": members, "size": len(members),
283
+ "common_conditions": common,
284
+ "non_applicable": neg})
285
+
286
+ df = _term_df(cache, keys, sorted(all_terms))
287
+ for c in cands:
288
+ members = c["members"]
289
+ src = {t: [m for m in members if t in cache[m]["pos"]]
290
+ for t in c["common_conditions"]}
291
+ gap = [t for t, ms in src.items() if len(ms) < len(members)]
292
+ generic = [{"term": t, "df": df.get(t, 0),
293
+ "ratio": round(df.get(t, 0) / float(base), 4)}
294
+ for t in c["common_conditions"]
295
+ if df.get(t, 0) / float(base) >= GENERIC_DF]
296
+ c.update({
297
+ "condition_sources": src,
298
+ "grounding_gap": gap,
299
+ "generic_conditions": generic,
300
+ "discriminating": [t for t in c["common_conditions"]
301
+ if df.get(t, 0) / float(base) < GENERIC_DF],
302
+ "reason": ("%d 条记忆内容相近且共享条件「%s」→ 归纳为概念"
303
+ % (len(members),
304
+ "、".join(c["common_conditions"][:MAX_TERMS]) or "无")),
305
+ })
306
+ generic_only = sum(1 for c in cands if not c["discriminating"])
307
+ adequacy = "ok" if cands else "insufficient"
308
+ return {
309
+ "root": cg.root, "dry_run": True, "readonly": True,
310
+ "action": "refine_preview", "op": "maintain",
311
+ "prefix": meta["prefix"], "seed": meta["seed"],
312
+ "requested": meta["requested"], "pool": meta["pool"],
313
+ "skipped_protected": meta["skipped_protected"],
314
+ "families": meta["families"], "strata": meta["strata"],
315
+ "sampled": meta["sampled"], "sample": pool,
316
+ "indexed": len(keys), "min_cluster": min_c, "min_jaccard": min_j,
317
+ "require_conditions": bool(require_conditions),
318
+ "clusters": len(cands), "candidates": cands,
319
+ "skipped_no_condition": skipped_no_cond,
320
+ "candidates_generic_only": generic_only,
321
+ "df_basis": "sampled", "generic_df_threshold": GENERIC_DF,
322
+ "sample_adequacy": adequacy,
323
+ "note": ("预演:未写盘、未改任何节点;候选供人工核对提炼口径"
324
+ if cands else
325
+ "抽检样本不足以产出候选(样本量或抽检单位需校准),不得据此扩批"),
326
+ }
327
+
328
+
329
+ # ---- 工单 ---------------------------------------------------------------
330
+
331
+ # 生效条件:x 转为 cg;prefix 假值回落 PREFIX_DEFAULT,seed 假值回落 SAMPLE_SEED;ids 为真值时 sample 为显式 ids 去重排序且 real_pool=len(sample)、skipped=0、families=0,否则 sample/meta 来自 sample_ids(cg, n=n, seed=seed, prefix=prefix) 并取 meta["pool"]/meta["skipped_protected"]/meta["families"];items 为 sample 的 _item,preview 以 sample 为显式 ids 调用,返回只读工单;
332
+ def plan(x, ids=None, n=None, seed=None, prefix=None,
333
+ min_jaccard=None, min_cluster=None) -> dict:
334
+ """抽检工单(只读):确定性样本 + 原文摘录 + 字段缺口 + 口径声明 + 预演。"""
335
+ cg = _as_cg(x)
336
+ prefix = prefix or PREFIX_DEFAULT
337
+ seed = seed or SAMPLE_SEED
338
+ if ids:
339
+ sample = sorted({str(i) for i in ids})
340
+ real_pool, skipped, families = len(sample), 0, 0
341
+ else:
342
+ sample, meta = sample_ids(cg, n=n, seed=seed, prefix=prefix)
343
+ real_pool, skipped, families = (meta["pool"], meta["skipped_protected"],
344
+ meta["families"])
345
+ items = [_item(cg, nid) for nid in sample]
346
+ pv = preview(cg, ids=sample, prefix=prefix,
347
+ min_jaccard=min_jaccard, min_cluster=min_cluster)
348
+ strata = {}
349
+ for it in items:
350
+ k = it["family"]
351
+ s = strata.setdefault(k, {"picked": 0, "missing_fields": {},
352
+ "ccg_exempt": 0})
353
+ s["picked"] += 1
354
+ if it["ccg_exempt"]:
355
+ s["ccg_exempt"] += 1
356
+ for m in it["ccg_missing"]:
357
+ s["missing_fields"][m] = s["missing_fields"].get(m, 0) + 1
358
+ return {
359
+ "root": cg.root, "dry_run": True, "readonly": True,
360
+ "action": "refine", "op": "maintain",
361
+ "prefix": prefix, "seed": seed,
362
+ "requested": n if n is not None else (len(sample) if ids else SAMPLE_N),
363
+ "pool": real_pool, "skipped_protected": skipped,
364
+ "sampled": len(items), "families": families,
365
+ "sample": sample, "sample_sha": _sha(*sample)[:16],
366
+ "strata": strata, "items": items,
367
+ "worklist": items, "spec": SPEC,
368
+ "preview": {"clusters": pv["clusters"], "candidates": pv["candidates"],
369
+ "min_cluster": pv["min_cluster"],
370
+ "min_jaccard": pv["min_jaccard"],
371
+ "skipped_no_condition": pv["skipped_no_condition"],
372
+ "candidates_generic_only": pv["candidates_generic_only"],
373
+ "sample_adequacy": pv["sample_adequacy"],
374
+ "df_basis": pv["df_basis"],
375
+ "generic_df_threshold": pv["generic_df_threshold"]},
376
+ "gate_rule": SPEC["gate"],
377
+ "note": ("抽检工单(只读):供人工核对提炼口径;核对通过前不得扩批。"
378
+ "行 `apply=true` 只落抽检留痕,不改任何节点。"),
379
+ }
380
+
381
+
382
+ # 生效条件:x 转为 cg;target 为 None 取 CALIBRATE_TARGET、否则 int(target),cap 为 None 取 CALIBRATE_CAP、否则 int(cap),初始 n=min(SAMPLE_N if n0 is None else int(n0), cap);循环以 n 调 preview(cg, n=n, seed=seed, prefix=prefix, min_jaccard=min_jaccard, min_cluster=min_cluster) 并记录 step,直到 pv["clusters"] >= target 则 chosen=step 并 break,或 n >= min(cap, pool or cap) 则 break;chosen 为 None 时 blocked=True 并按 steps 汇总 recommend,否则 recommend 基于 chosen;返回只读校准报告;
383
+ def calibrate(x, target=None, cap=None, n0=None, seed=None, prefix=None,
384
+ min_jaccard=None, min_cluster=None) -> dict:
385
+ """样本量校准(只读):20 条起步有界爬坡,直到产出 `target` 个候选或触顶。
386
+
387
+ 目的是回答「20 条工单为什么交不出候选」,并给出**有界**的替代样本量;
388
+ 若爬坡到顶仍无候选、或候选共性条件全为高 df 模板词,则如实报
389
+ `blocked` 并指出真正的瓶颈是**提炼口径**而非样本量——不得据此扩批。
390
+ """
391
+ cg = _as_cg(x)
392
+ target = CALIBRATE_TARGET if target is None else int(target)
393
+ cap = CALIBRATE_CAP if cap is None else int(cap)
394
+ n = min(SAMPLE_N if n0 is None else int(n0), cap)
395
+ steps, chosen, pool = [], None, None
396
+ while True:
397
+ pv = preview(cg, n=n, seed=seed, prefix=prefix,
398
+ min_jaccard=min_jaccard, min_cluster=min_cluster)
399
+ pool = pv["pool"]
400
+ step = {"n": n, "sampled": pv["sampled"], "clusters": pv["clusters"],
401
+ "candidates_generic_only": pv["candidates_generic_only"],
402
+ "discriminating_terms": sorted(
403
+ {t for c in pv["candidates"] for t in c["discriminating"]})[:12]}
404
+ steps.append(step)
405
+ if pv["clusters"] >= target:
406
+ chosen = step
407
+ break
408
+ top = min(cap, pool or cap)
409
+ if n >= top:
410
+ break
411
+ n = min(n * 2, top)
412
+ blocked = chosen is None
413
+ saw_clusters = any(s["clusters"] for s in steps)
414
+ total_c = sum(s["clusters"] for s in steps)
415
+ gen_only = sum(s["candidates_generic_only"] for s in steps)
416
+ if not blocked:
417
+ rec = ("抽检样本量校准为 n=%d(产出 %d 个候选);仍须人工核对提炼口径,"
418
+ "核对通过前不得扩批" % (chosen["n"], chosen["clusters"]))
419
+ else:
420
+ parts = ["爬坡至 cap=%d 仍只产出 %d 个候选(< target=%d),样本量不足以"
421
+ "直接支撑抽检" % (cap, steps[-1]["clusters"], target)]
422
+ if saw_clusters:
423
+ if gen_only >= total_c:
424
+ parts.append("且已产出候选的共性条件**全部**为高 df 模板词(无区分性)")
425
+ else:
426
+ parts.append("且候选中 %d/%d 的共性条件全为高 df 模板词,"
427
+ "其余『区分性』项形如整行条件文本(含时间戳),"
428
+ "属归一化缺失导致的伪区分性" % (gen_only, total_c))
429
+ parts.append("须先修正提炼口径(剔除模板词 + 归一化/裁剪整行文本)"
430
+ "或以「候选」为抽检单位重新校准;当前状态不得扩批")
431
+ rec = ";".join(parts)
432
+ return {"ok": True, "action": "refine_calibrate", "op": "maintain",
433
+ "root": cg.root, "dry_run": True, "readonly": True,
434
+ "target": target, "cap": cap, "pool": pool,
435
+ "steps": steps, "chosen": chosen, "blocked": blocked,
436
+ "sample_adequacy": "ok" if not blocked else "insufficient",
437
+ "recommend": rec, "note": "只读校准:未写盘、未改任何节点。"}
438
+
439
+
440
+ # ---- 留痕与扩批闸门 ------------------------------------------------------
441
+
442
+ # 生效条件:candidates 中取 concept_id 为真值的 id 列表;verdicts 中为 dict 且 concept_id 非空且首次出现的条目计入 reviewed 并累加其中 faithful/added_info 的真值计数;ids 非空时按 reviewed < len(ids) → awaiting_human_review、added 非零 → added_info_detected、faithful_rate < GATE_MIN_PASS_RATE → faithful_rate_below_gate、否则 ok 且 expand_allowed=True;ids 为空时 reason="no_candidates" 且 expand_allowed=False;
443
+ def _verdict_stats(verdicts, candidates) -> dict:
444
+ ids = [c.get("concept_id") for c in candidates if c.get("concept_id")]
445
+ seen, faithful, added = set(), 0, 0
446
+ for v in verdicts or []:
447
+ if not isinstance(v, dict):
448
+ continue
449
+ cid = v.get("concept_id")
450
+ if not cid or cid in seen:
451
+ continue
452
+ seen.add(cid)
453
+ faithful += 1 if v.get("faithful") else 0
454
+ added += 1 if v.get("added_info") else 0
455
+ reviewed = len(seen)
456
+ rate = (faithful / float(reviewed)) if reviewed else 0.0
457
+ expand, reason = False, "no_candidates"
458
+ if ids:
459
+ if reviewed < len(ids):
460
+ reason = "awaiting_human_review"
461
+ elif added:
462
+ reason = "added_info_detected"
463
+ elif rate < GATE_MIN_PASS_RATE:
464
+ reason = "faithful_rate_below_gate"
465
+ else:
466
+ reason = "ok"
467
+ expand = True
468
+ return {"candidates": len(ids), "reviewed": reviewed,
469
+ "faithful": faithful, "added_info": added,
470
+ "faithful_rate": round(rate, 4),
471
+ "min_pass_rate": GATE_MIN_PASS_RATE,
472
+ "expand_allowed": expand, "reason": reason}
473
+
474
+
475
+ # 生效条件:x 转为 cg;以 ids/n/seed/prefix/min_jaccard/min_cluster 调 plan 得到 p,batch 假值(含 None/空串)回落当前时间字符串;从 p["preview"]["candidates"] 与 verdicts 经 _verdict_stats 得 stats;把含 batch/actor/prefix/seed/sample/verdicts/gate/note 的 record 追加写入 _log_path(cg),返回 rep;
476
+ def apply(x, batch=None, actor=None, verdicts=None, note=None,
477
+ ids=None, n=None, seed=None, prefix=None,
478
+ min_jaccard=None, min_cluster=None) -> dict:
479
+ """落抽检批次 + 人工裁决到 `_refine.jsonl`。**不改写任何节点**。"""
480
+ cg = _as_cg(x)
481
+ p = plan(cg, ids=ids, n=n, seed=seed, prefix=prefix,
482
+ min_jaccard=min_jaccard, min_cluster=min_cluster)
483
+ batch = batch or time.strftime("%Y%m%d-%H%M%S")
484
+ cands = p["preview"]["candidates"]
485
+ stats = _verdict_stats(verdicts, cands)
486
+ record = {
487
+ "t": time.time(), "action": "refine", "batch": batch, "actor": actor,
488
+ "prefix": p["prefix"], "seed": p["seed"], "requested": p["requested"],
489
+ "pool": p["pool"], "sampled": p["sampled"],
490
+ "sample": p["sample"], "sample_sha": p["sample_sha"],
491
+ "sample_adequacy": p["preview"]["sample_adequacy"],
492
+ "min_cluster": p["preview"].get("min_cluster"),
493
+ "candidates": [{"concept_id": c["concept_id"], "members": c["members"],
494
+ "common_conditions": c["common_conditions"],
495
+ "grounding_gap": c["grounding_gap"],
496
+ "discriminating": c["discriminating"]}
497
+ for c in cands],
498
+ "verdicts": list(verdicts or []), "gate": stats, "note": note,
499
+ }
500
+ append_jsonl(_log_path(cg), record)
501
+ rep = {"ok": True, "action": "refine", "op": "maintain", "batch": batch,
502
+ "root": cg.root, "sampled": record["sampled"],
503
+ "candidates": stats["candidates"], "reviewed": stats["reviewed"],
504
+ "gate": stats, "log": REFINE_LOG,
505
+ "note": ("抽检留痕已落盘(未改任何节点);"
506
+ + ("闸门放行扩批(仍需另开 induce 批次并留痕)"
507
+ if stats["expand_allowed"] else
508
+ "闸门未放行:" + stats["reason"]))}
509
+ return rep
510
+
511
+
512
+ # 生效条件:x 转为 cg;batch 为真值时只保留 _read_log(cg) 中 batch 字段相等的记录,过滤后为空时返回 batches=0、expand_allowed=False、reason="no_batch";否则取最后一条记录,以其 verdicts 与 candidates(假值转空列表)经 _verdict_stats 复算并返回 expand_allowed/reason 等;
513
+ def gate(x, batch=None) -> dict:
514
+ """扩批闸门:读留痕复算通过率(不写盘)。"""
515
+ cg = _as_cg(x)
516
+ recs = _read_log(cg)
517
+ if batch:
518
+ recs = [r for r in recs if r.get("batch") == batch]
519
+ if not recs:
520
+ return {"ok": True, "action": "refine_gate", "op": "maintain",
521
+ "root": cg.root, "batch": batch, "batches": 0,
522
+ "expand_allowed": False, "reason": "no_batch"}
523
+ rec = recs[-1]
524
+ stats = _verdict_stats(rec.get("verdicts"), rec.get("candidates") or [])
525
+ return {"ok": True, "action": "refine_gate", "op": "maintain",
526
+ "root": cg.root, "batch": rec.get("batch"),
527
+ "batches": len({r.get("batch") for r in recs}),
528
+ "sampled": rec.get("sampled"),
529
+ "sample_adequacy": rec.get("sample_adequacy"),
530
+ "gate": stats, "expand_allowed": stats["expand_allowed"],
531
+ "reason": stats["reason"],
532
+ "note": ("闸门放行:可另开 induce 批次扩大提炼规模并逐批留痕"
533
+ if stats["expand_allowed"] else
534
+ "闸门未放行,禁止扩大批次;不得盲跑全量。")}
535
+
536
+
537
+ # 生效条件:batch 为真值时只保留该批次记录,total 先记为过滤后条数;limit 非 None 且 >=0 时以 recs[-int(limit):] 截尾(limit=0 因 [-0:] 等价 [0:] 仍返回全部记录),limit 为 None 或负数时不截断;
538
+ def history(x, limit=100, batch=None) -> dict:
539
+ cg = _as_cg(x)
540
+ recs = _read_log(cg)
541
+ if batch:
542
+ recs = [r for r in recs if r.get("batch") == batch]
543
+ total = len(recs)
544
+ if limit is not None and limit >= 0:
545
+ recs = recs[-int(limit):]
546
+ return {"root": cg.root, "total": total, "returned": len(recs),
547
+ "action": "refine", "records": recs}
548
+
549
+
550
+ # ---- CLI(真实库抽检/预演用;MCP 侧走 maintain action) -------------------
551
+
552
+ # 生效条件:解析 argv(None 时由 argparse 取 sys.argv)得到 root/n/seed/prefix/min-jaccard/min-cluster/report 等参数;a.calibrate 为真时调 calibrate 并打印 target/cap/pool/blocked/sample_adequacy/recommend 及每步摘要;否则 a.preview 为真选 preview、为假选 plan,调用后打印 thin 摘要与候选并返回 0;
553
+ def _main(argv=None) -> int:
554
+ import argparse
555
+ import json
556
+ ap = argparse.ArgumentParser(description="G6 提炼抽检(默认只预演,不写盘)")
557
+ ap.add_argument("root", help="认知图库根")
558
+ ap.add_argument("--n", type=int, default=SAMPLE_N, help="抽检条数(默认 20)")
559
+ ap.add_argument("--seed", default=SAMPLE_SEED, help="抽样种子(同 seed 同样本)")
560
+ ap.add_argument("--prefix", default=PREFIX_DEFAULT, help="id 前缀(默认 node_)")
561
+ ap.add_argument("--min-jaccard", type=float, default=None)
562
+ ap.add_argument("--min-cluster", type=int, default=None)
563
+ ap.add_argument("--preview", action="store_true", help="只出预演(不出工单正文)")
564
+ ap.add_argument("--calibrate", action="store_true",
565
+ help="只做样本量校准(20 条起步有界爬坡)")
566
+ ap.add_argument("--report", default=None, help="把汇总 JSON 另存一份")
567
+ a = ap.parse_args(argv)
568
+ if a.calibrate:
569
+ rep = calibrate(a.root, seed=a.seed, prefix=a.prefix,
570
+ min_jaccard=a.min_jaccard, min_cluster=a.min_cluster)
571
+ if a.report:
572
+ with open(a.report, "w", encoding="utf-8") as f:
573
+ json.dump(rep, f, ensure_ascii=False, indent=2)
574
+ print(json.dumps({k: rep[k] for k in
575
+ ("target", "cap", "pool", "blocked",
576
+ "sample_adequacy", "recommend")},
577
+ ensure_ascii=False, indent=2))
578
+ for s in rep["steps"]:
579
+ print(" - n=%4d clusters=%2d generic_only=%2d discriminating=%s"
580
+ % (s["n"], s["clusters"], s["candidates_generic_only"],
581
+ s["discriminating_terms"][:6]))
582
+ return 0
583
+ fn = preview if a.preview else plan
584
+ rep = fn(a.root, n=a.n, seed=a.seed, prefix=a.prefix,
585
+ min_jaccard=a.min_jaccard, min_cluster=a.min_cluster)
586
+ if a.report:
587
+ with open(a.report, "w", encoding="utf-8") as f:
588
+ json.dump(rep, f, ensure_ascii=False, indent=2)
589
+ pv = rep.get("preview") if "preview" in rep else rep
590
+ thin = {k: rep.get(k) for k in ("action", "prefix", "seed", "requested",
591
+ "pool", "sampled", "families")}
592
+ thin.update({k: pv.get(k) for k in ("clusters", "sample_adequacy",
593
+ "candidates_generic_only")})
594
+ print(json.dumps(thin, ensure_ascii=False, indent=2))
595
+ for c in (pv.get("candidates") or []):
596
+ print(" - %s size=%d common=%s generic=%s gap=%s"
597
+ % (c["concept_id"], c["size"], c["common_conditions"][:4],
598
+ [g["term"] for g in c.get("generic_conditions", [])],
599
+ c.get("grounding_gap")))
600
+ return 0
601
+
602
+
603
+ if __name__ == "__main__":
604
+ import sys
605
605
  sys.exit(_main())