@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/backfill.py CHANGED
@@ -1,1328 +1,1328 @@
1
- # -*- coding: utf-8 -*-
2
- """真实库对齐(P32):CCG 回填 + 能力标签注入。
3
-
4
- 为什么是「回填」而不是「重写」
5
- ------------------------------
6
- 迁移入库的历史节点里,条件信息**往往已经在 frontmatter 里**:
7
- `condition_space`(四槽)、`non_applicable_conditions`、`verification_basis`、
8
- `state_attributes.comment.*`。缺的只是把它们渲染成 CCG 正文行(`# 生效条件:…`
9
- 等)。缺了正文行,`judge_qualification` 一律判 BLINDSPOT——节点从「可检索」
10
- 掉到「不可判定」。
11
-
12
- 回填 = 把**已声明的证据**渲染成 CCG 行。它不发明条件,只搬运已有声明。
13
-
14
- 生效条件的来源链(本轮口径修正:观测位置 ≠ 生效条件)
15
- ----------------------------------------------------
16
- 生效条件**只能**来自两处,且优先成文声明:
17
- ① `state_attributes.comment.生效条件`(人/流程写下的成文声明)
18
- ② `nodefile.condition_space_text(frontmatter.condition_space)`(四槽合成)
19
- 旧版曾回退到 `condition_space.observation_position` **单槽**,加前缀「观测位置:」
20
- 冒充生效条件——那是把坐标的一维当成整条生效条件,真实库因此落了 109 条弱等价行。
21
- 该回退**已删除**;四槽不齐 → 不写(部分槽不构成完整条件空间声明),转待补台账。
22
- `fix_conditions_*` 三个函数用于清洗存量:把已落库的单槽冒充行**重渲染**为合规
23
- 合成声明,或**删除并登记待补**——全程留痕、可回滚、幂等。
24
-
25
- 纪律(对齐 consolidate 的固化纪律)
26
- ----------------------------------
27
- · 不猜测:字段只在**有来源**时才写;来源写进留痕 `basis`;无来源 → 跳过并计入
28
- `unfillable`,绝不编造。
29
- · 可预演:`plan()` 只出报表、不改盘;`apply()` 才写。默认只回填「补完即可判定」
30
- 的节点,`partial`(补完仍不全)默认不写,除非显式 `include_partial=True`。
31
- · 可留痕:每次写入记一条 `_backfill.jsonl`(字段 / 写入值 / 依据 / 批次 / 操作者)。
32
- · 可回滚:`rollback()` 按留痕反向应用,且**只在当前值仍等于写入值**时撤销
33
- (防覆盖后续人工修改),否则计入 `conflict` 跳过。
34
- · fail-closed:密文节点一律跳过,**绝不解密回写**。
35
-
36
- 能力标签注入
37
- ------------
38
- `cap:<op>` 标签让路由能返回建议能力名(`mcp_server` 读 frontmatter.tags 的
39
- `cap:` 前缀)。匹配是**关键词启发式**(对标 `tokens.ALL_OPS` 工具名清单),
40
- 不是语义推断:结果带 `matched_by`(命中的关键词)与 `confidence`,调用方据此
41
- 判断可信度。只改 frontmatter.tags,不动正文。
42
- """
43
- from __future__ import annotations
44
-
45
- import hashlib
46
- import os
47
- import time
48
- from collections import OrderedDict
49
-
50
- from . import crypto, nodefile, tokens
51
- from .consolidate import _has_ccg_line, _upsert_ccg_line
52
- from .fsutil import append_jsonl, read_jsonl
53
- from .mdcos import MdCGOS, _ccg_field
54
-
55
- # ---- 常量 ----------------------------------------------------------------
56
-
57
- BACKFILL_LOG = "_backfill.jsonl"
58
-
59
- # 负记忆层是「故意无条件」的(覆盖标记),派生脚手架不该再被回填成事实
60
- SKIP_LAYERS = ("rejected", "unresolved", "goals")
61
- # 推断脚手架 / 概念层不得回填:否则会污染锚点解析(见 predict.anchor_from_description)
62
- SKIP_TAGS = ("gap_hint", "scene", "reconstructed", "concept", "insight")
63
- # 记忆系统**内部脚手架层**(锚点解析 anchor / 自模型修订 self):不是用户知识,永不可回填;
64
- # 被回填成事实会污染锚点解析与自模型(与 SKIP_LAYERS 同源理由)。crosscheck 亦复用之。
65
- INTERNAL_LAYERS = ("anchor", "self")
66
-
67
- # 验证基底枚举 → 可读声明(人/流程声明,不靠模型生成)
68
- BASIS_TEXT = OrderedDict((
69
- ("compiler", "编译器/静态检查通过"),
70
- ("test", "单元测试/回归测试通过"),
71
- ("measurement", "实测数据(benchmark / 采样)"),
72
- ("formal_proof", "形式化证明"),
73
- ("data", "数据/语料统计"),
74
- # 来源一致性档(文科):文科知识非可复现的物理事实,以来源表述一致为足够基底
75
- ("textbook", "依人教版教材表述一致(文科·来源一致性)"),
76
- ("public_kb", "公开知识库条目一致(文科·来源一致性)"),
77
- ("other", "人工评审或离线工序声明"),
78
- ))
79
- BASIS_ENUM_DEFAULT = "other"
80
-
81
- BATCH_DEFAULT = "backfill"
82
-
83
- #: 生效条件的**唯一结构化来源**:condition_space 四槽合成(见 nodefile)
84
- BASIS_CONDITION_SYNTH = "frontmatter.condition_space(四槽合成)"
85
- #: 旧口径残留标记:把单槽 observation_position 冒充成生效条件的留痕 basis
86
- LEGACY_CONDITION_BASIS = "frontmatter.condition_space.observation_position"
87
- #: 存量清洗批次的默认批次名
88
- FIX_BATCH_DEFAULT = "cond_fix"
89
-
90
- #: 槽名 → 人读标签(台账/报告用;与 nodefile.CONDITION_SLOTS 同源)
91
- _SLOT_LABEL = dict(nodefile.CONDITION_SLOTS)
92
-
93
- # 能力标签规则:cap → 关键词(小写,中文原样)。仅保留 ALL_OPS 里真实存在的 op。
94
- _CAP_RULES_RAW = OrderedDict((
95
- ("route", ("路由", "召回", "检索", "rank", "rrf", "route")),
96
- ("export", ("导出", "灾备", "备份", "搬运", "export", "evidence_pack")),
97
- ("ingest", ("摄取", "导入", "摄入", "ingest", "分派")),
98
- ("session", ("会话", "续接", "上下文压缩", "session", "compact")),
99
- ("maintain", ("维护", "重要性", "重算", "快照", "前馈", "模式分离", "recalc")),
100
- ("consolidate", ("固化", "提升", "归纳", "聚类", "升格", "promote", "induce")),
101
- ("insight", ("洞察", "情景重构", "盲区", "归因", "reconstruct", "outlook")),
102
- ("whitebox", ("白箱", "资格判定", "裁决", "四态", "whitebox")),
103
- ("verify", ("验证", "核验", "verdict")),
104
- ("predict", ("预测", "趋势", "外推", "predict")),
105
- ("causal", ("因果", "causal")),
106
- ("metacognition", ("元认知", "metacognition")),
107
- ("self_state", ("自我状态", "状态卡", "self_state")),
108
- ("evolution", ("演化", "evolution", "账本")),
109
- ("sustain", ("维生", "自愈", "心跳", "sustain")),
110
- ("scrub", ("擦除", "去污", "scrub")),
111
- ("protect", ("保护", "私有内容", "protect")),
112
- ("forget", ("遗忘", "失效", "forget")),
113
- ("link", ("蜂群", "对等", "信任", "link")),
114
- ("identity", ("身份", "主体", "identity")),
115
- ("goal", ("目标", "goal")),
116
- ("recent", ("近期事件", "recent")),
117
- ("theory", ("协议版本", "theory")),
118
- ("consistency", ("一致性", "自洽", "consistency")),
119
- ("ref", ("回读", "漂移", "悬空", "code_ref", "doc_ref")),
120
- ("index_code", ("代码索引", "index_code")),
121
- ("index_doc", ("文档索引", "章节索引", "index_doc")),
122
- ))
123
- # 只保留真实 op,未知 op 静默丢弃(避免注入无效能力名)
124
- CAP_RULES = OrderedDict(
125
- (cap, kws) for cap, kws in _CAP_RULES_RAW.items() if cap in tokens.ALL_OPS)
126
-
127
-
128
- # ---- 通用工具 ------------------------------------------------------------
129
-
130
- # 生效条件:x 为 str 时返回 MdCGOS(x),否则原样返回 x。
131
- def _as_cg(x):
132
- """接受 root 路径或已构造的 cg 实例——保持密级隔离与密钥上下文。"""
133
- return MdCGOS(x) if isinstance(x, str) else x
134
-
135
-
136
- # 生效条件:v 为 list/tuple 时返回分号连接的非空元素文本;v 为 None 或空串时返回 "";否则返回 str(v).strip()(v 为 0 或 False 走此支返回 "0"/"False")。
137
- def _as_text(v) -> str:
138
- if isinstance(v, (list, tuple)):
139
- return ";".join(str(x).strip() for x in v if str(x).strip())
140
- if v in (None, ""):
141
- return ""
142
- return str(v).strip()
143
-
144
-
145
- # 生效条件:fm 为假值时按 {} 处理,state_attributes.comment 为 dict 时返回该 dict,否则返回 {}。
146
- def _comment(fm: dict) -> dict:
147
- st = (fm or {}).get("state_attributes")
148
- c = st.get("comment") if isinstance(st, dict) else None
149
- return c if isinstance(c, dict) else {}
150
-
151
-
152
- # 生效条件:fm 的 state_attributes 为 dict 且其下 comment 为 dict 时原样返回该 comment;否则创建并返回空 comment dict(state_attributes 非 dict 时置 fm["state_attributes"]={},comment 非 dict 时置 st["comment"]={})。
153
- def _ensure_comment(fm: dict) -> dict:
154
- st = fm.get("state_attributes")
155
- if not isinstance(st, dict):
156
- st = {}
157
- fm["state_attributes"] = st
158
- c = st.get("comment")
159
- if not isinstance(c, dict):
160
- c = {}
161
- st["comment"] = c
162
- return c
163
-
164
-
165
- # 生效条件:e 的 id 为真值时返回 id,否则返回 e 的 path 基名去掉最后 3 个字符(path 为假值时基名为空,结果空串)。
166
- def _node_id(e: dict) -> str:
167
- return e.get("id") or os.path.basename(e.get("path") or "")[:-3]
168
-
169
-
170
- # 生效条件:fm 的 condition_space 四槽齐全时返回 nodefile.condition_space_text 合成文本,否则返回空串。
171
- def _condition_text(fm: dict) -> str:
172
- """→ 条件空间四槽合成的生效条件声明;四槽不齐 → ""(不冒充)。
173
-
174
- **唯一**的 condition_space → 生效条件 路径。旧版在此回退到
175
- `observation_position` **单槽**加「观测位置:」前缀——那正是
176
- 「观测位置 ≠ 生效条件」的污染源(真实库 109 条),已删除。
177
- """
178
- return nodefile.condition_space_text((fm or {}).get("condition_space"))
179
-
180
-
181
- # 生效条件:s 为真值时返回其 sha1 前 12 位,s 为假值(None/空串等)时对空串取 sha1 前 12 位。
182
- def _sha(s: str) -> str:
183
- return hashlib.sha1((s or "").encode("utf-8")).hexdigest()[:12]
184
-
185
-
186
- # 生效条件:batch 与 nid 经 f-string 拼接后取 sha1 前 12 位;两者为 None 会字符串化为 "None"。
187
- def _entry_id(batch: str, nid: str) -> str:
188
- return hashlib.sha1(f"{batch}|{nid}".encode("utf-8")).hexdigest()[:12]
189
-
190
-
191
- # 生效条件:content 中 strip 后以 # 开头且去掉 # 与空白后、全角或半角冒号前首段等于 field 的整行被删除,其余行保留并 join。
192
- def _remove_ccg_line(content: str, field: str) -> str:
193
- """删掉 `# <字段>:…` 整行(回滚用)。"""
194
- keep = []
195
- for ln in (content or "").split("\n"):
196
- s = ln.strip().lstrip("#").strip()
197
- name = s.split(":")[0].split(":")[0].strip()
198
- if ln.strip().startswith("#") and name == field:
199
- continue
200
- keep.append(ln)
201
- return "\n".join(keep)
202
-
203
-
204
- # 生效条件:cg.root 与模块级常量 BACKFILL_LOG 拼接为返回路径。
205
- def _log_path(cg) -> str:
206
- return os.path.join(cg.root, BACKFILL_LOG)
207
-
208
-
209
- # ---- 字段推导(唯一入口:只搬运已声明的证据) -----------------------------
210
-
211
- # 生效条件:fm 与 content 给定时,仅对 content 中尚无对应 CCG 行的字段(功能名/生效条件/子功能/执行/验证方式/不适用条件)从 fm 的已有声明(state_attributes.comment、frontmatter、verification_basis、或形参 basis_text)取值,值非空且非占位文本才写入 out,假值不写、占位文本只把字段名追加进 placeholder_out(未传该形参时用临时列表)。
212
- def derive_fields(fm: dict, content: str, basis_text: str = None,
213
- placeholder_out: list = None) -> dict:
214
- """按**已有声明**推导可回填字段 → `{field: (value, basis)}`。
215
-
216
- 无来源的字段不出现在结果里(不猜测)。
217
- **占位标记(`骨架锚点`/`内容待填充`)同样不出现**——它不是已声明的事实;
218
- 被丢弃的字段名记入 `placeholder_out`(可选出参),供报表区分
219
- 「无来源」与「待填充」两种缺口。
220
- """
221
- c = _comment(fm)
222
- st = fm.get("state_attributes")
223
- st = st if isinstance(st, dict) else {}
224
- ph = placeholder_out if placeholder_out is not None else []
225
- out = {}
226
-
227
- # 生效条件:field 与 basis 在 value 为真值且 nodefile.is_placeholder_text(value) 为假时写入 out;value 为假值不写入;value 为占位文本时仅把 field 记入 ph。
228
- def _put(field, value, basis):
229
- """有值且非占位标记才写出;占位值只记名,绝不渲染成事实。"""
230
- if not value:
231
- return
232
- if nodefile.is_placeholder_text(value):
233
- ph.append(field)
234
- return
235
- out[field] = (value, basis)
236
-
237
- if not _has_ccg_line(content, "功能名"):
238
- # 优先 state_attributes.name(迁移入库写入的规范功能名),回退 frontmatter.title
239
- raw_name = st.get("name")
240
- v = _as_text(raw_name) if isinstance(raw_name, (str, list, tuple)) else ""
241
- src = "state_attributes.name"
242
- if not v:
243
- v, src = _as_text(fm.get("title")), "frontmatter.title"
244
- _put("功能名", v, src)
245
-
246
- if not _has_ccg_line(content, "生效条件"):
247
- v = _as_text(c.get("生效条件") or c.get("适用条件"))
248
- src = "state_attributes.comment.生效条件"
249
- if not v:
250
- # 结构化来源:整条条件空间声明的合成,**不是** observation_position 单槽
251
- v, src = _condition_text(fm), BASIS_CONDITION_SYNTH
252
- _put("生效条件", v, src)
253
-
254
- if not _has_ccg_line(content, "子功能"):
255
- _put("子功能", _as_text(c.get("子功能") or c.get("子内容")),
256
- "state_attributes.comment.子功能")
257
-
258
- if not _has_ccg_line(content, "执行"):
259
- _put("执行", _as_text(c.get("执行") or c.get("执行方式")),
260
- "state_attributes.comment.执行")
261
-
262
- if not _has_ccg_line(content, "验证方式"):
263
- v = _as_text(c.get("验证方式"))
264
- src = "state_attributes.comment.验证方式"
265
- if not v and nodefile.verification_basis_valid(fm):
266
- vb = fm.get("verification_basis")
267
- v, src = BASIS_TEXT.get(vb, ""), f"frontmatter.verification_basis={vb}"
268
- if not v and basis_text:
269
- v, src = basis_text, "declared.basis_text"
270
- _put("验证方式", v, src)
271
-
272
- if not _has_ccg_line(content, "不适用条件"):
273
- _put("不适用条件",
274
- _as_text(c.get("不适用条件") or fm.get("non_applicable_conditions")),
275
- "state_attributes.comment/frontmatter.non_applicable_conditions")
276
-
277
- return out
278
-
279
-
280
- # ---- 节点筛选 ------------------------------------------------------------
281
-
282
- # 生效条件:cg 无 _readable 可调用时返回 True;有可调用时返回 bool(fn(e)),fn(e) 抛异常时返回 False。
283
- def _readable_guard(cg, e) -> bool:
284
- fn = getattr(cg, "_readable", None)
285
- if not callable(fn):
286
- return True
287
- try:
288
- return bool(fn(e))
289
- except Exception: # noqa: BLE001
290
- return False
291
-
292
-
293
- # 生效条件:仅当 cg 对 e 读出的 fm 非 None、content 未被 crypto.is_encrypted、e['layer'] 不在 SKIP_LAYERS 且 fm.get('tags') 无命中 SKIP_TAGS、ccg_completeness(content)['complete'] 为假时才继续——derive_fields(受 basis_text 影响)过滤掉 content 已有 CCG 行的可写字段为空时按 placeholder_out 是否非空返回 ('placeholder'/'unfillable', None),非空时返回 ('', item)(item 的 id 取 nid、class 依 undeducible 是否为空取 'backfillable' 或 'partial');上述四个前置不满足时依次返回 ('unreadable'/'locked'/'derived'/'present', None)。
294
- def _classify(cg, e, nid, basis_text=None):
295
- """→ (skip_reason, item);skip_reason 非空表示不参与回填。"""
296
- fm, content = cg._read(e)
297
- if fm is None:
298
- return "unreadable", None
299
- if crypto.is_encrypted(content):
300
- return "locked", None
301
- tags = fm.get("tags") or []
302
- if e.get("layer") in SKIP_LAYERS or any(t in SKIP_TAGS for t in tags):
303
- return "derived", None
304
- cpl = nodefile.ccg_completeness(content)
305
- if cpl["complete"]:
306
- return "present", None
307
- ph_fields = []
308
- der = derive_fields(fm, content, basis_text=basis_text,
309
- placeholder_out=ph_fields)
310
- fill = {f: der[f] for f in der if not _has_ccg_line(content, f)}
311
- required_missing = [f for f in nodefile.CCG_REQUIRED
312
- if not _has_ccg_line(content, f)]
313
- undeducible = [f for f in required_missing if f not in der]
314
- if not fill:
315
- # 无可写字段:区分「无来源」(unfillable)与「字段值全是待填充标记」
316
- # (placeholder)——后者是空壳节点,须转待填充工单,而非静默计入无来源。
317
- return ("placeholder" if ph_fields else "unfillable"), None
318
- cls = "backfillable" if not undeducible else "partial"
319
- # 待补台账:生效条件既不在正文、也推不出(四槽不全)→ 记缺失槽名。
320
- # 这不是「无来源」而是**缺证据**:补不动者按裁定判 BLINDSPOT,绝不静默 ACCEPT。
321
- cond_pending = []
322
- if not _has_ccg_line(content, "生效条件") and "生效条件" not in der:
323
- cond_pending = nodefile.condition_space_missing(fm.get("condition_space"))
324
- return "", {
325
- "id": nid, "layer": e.get("layer"), "class": cls,
326
- "fill": {f: {"value": v, "basis": b} for f, (v, b) in fill.items()},
327
- "missing_undeducible": undeducible,
328
- "placeholder_fields": ph_fields,
329
- "conditions_pending": cond_pending,
330
- "before_ratio": cpl["ratio"],
331
- }
332
-
333
-
334
- # 生效条件:x 经 _as_cg 后遍历 cg.index.nodes,按 ids/layer/prefix 过滤,内部层/不可读/locked/derived/present/unfillable/placeholder/partial 且 include_partial 为 False 分别计数跳过,其余标记 entry_id 并计入 targeted,items 受 limit 限制(limit 为 None 或 len(items) < limit 时追加),返回 dry_run rep。
335
- def plan(x, layer=None, limit=None, ids=None, include_partial=False,
336
- basis_text=None, prefix=None) -> dict:
337
- """预演:产出可回填清单,不写盘。
338
-
339
- `prefix`:按 id 前缀收窄范围(真实库用 `kp_`,与核对工单同一边界);
340
- 内部 `anchor`/`self` 层一律出局(计入 `skipped_internal`)。
341
- """
342
- cg = _as_cg(x)
343
- rep = {"root": cg.root, "dry_run": True, "action": "backfill",
344
- "prefix": prefix, "nodes_scanned": 0, "skipped_locked": 0,
345
- "skipped_derived": 0, "skipped_internal": 0,
346
- "skipped_present": 0, "skipped_denied": 0, "unfillable": 0,
347
- "skipped_placeholder": 0, "nodes_with_placeholder": 0,
348
- "skipped_partial": 0, "targeted": 0,
349
- # 待写节点中生效条件仍缺声明的数量(全库台账见 conditions_pending)
350
- "targeted_missing_conditions": 0, "items": []}
351
- want = set(ids) if ids else None
352
- for nid, e in list((cg.index.get("nodes") or {}).items()):
353
- if want is not None and nid not in want:
354
- continue
355
- if prefix and not str(nid).startswith(prefix):
356
- continue
357
- if layer and e.get("layer") != layer:
358
- continue
359
- if e.get("layer") in INTERNAL_LAYERS:
360
- rep["skipped_internal"] += 1
361
- continue
362
- if not _readable_guard(cg, e):
363
- rep["skipped_denied"] += 1
364
- continue
365
- rep["nodes_scanned"] += 1
366
- reason, item = _classify(cg, e, nid, basis_text=basis_text)
367
- if reason == "locked":
368
- rep["skipped_locked"] += 1
369
- continue
370
- if reason == "derived":
371
- rep["skipped_derived"] += 1
372
- continue
373
- if reason == "present":
374
- rep["skipped_present"] += 1
375
- continue
376
- if reason == "unfillable":
377
- rep["unfillable"] += 1
378
- continue
379
- if reason == "placeholder":
380
- rep["skipped_placeholder"] += 1
381
- continue
382
- if item.get("placeholder_fields"):
383
- rep["nodes_with_placeholder"] += 1
384
- if item.get("conditions_pending"):
385
- rep["targeted_missing_conditions"] += 1
386
- if item["class"] == "partial" and not include_partial:
387
- rep["skipped_partial"] += 1
388
- continue
389
- item["entry_id"] = _entry_id("backfill", nid)
390
- rep["targeted"] += 1
391
- if limit is None or len(rep["items"]) < limit:
392
- rep["items"].append(item)
393
- rep["planned_ids"] = [i["id"] for i in rep["items"]]
394
- return rep
395
-
396
-
397
- # ---- 回填写入 / 回滚 / 留痕 ----------------------------------------------
398
-
399
- # 生效条件:x 可被 _as_cg 解释为 CG 句柄时,按 layer/ids/prefix/entry_ids 选定条目做回填并返回含 written 等计数的 rep,其中 limit 非 None 时把写入数截断到该值。
400
- def apply(x, ids=None, entry_ids=None, layer=None, limit=None,
401
- batch=BATCH_DEFAULT, include_partial=False, basis_text=None,
402
- actor=None, prefix=None) -> dict:
403
- """执行回填:逐节点改写 md,写 `_backfill.jsonl` 留痕。"""
404
- cg = _as_cg(x)
405
- batch = batch or BATCH_DEFAULT
406
- p = plan(cg, layer=layer, limit=None, ids=ids, prefix=prefix,
407
- include_partial=include_partial, basis_text=basis_text)
408
- items = p["items"]
409
- if entry_ids:
410
- want = set(entry_ids)
411
- items = [i for i in items if i["entry_id"] in want]
412
- rep = {"root": cg.root, "dry_run": False, "action": "backfill",
413
- "batch": batch, "actor": actor, "planned": len(items),
414
- "written": 0, "skipped_drift": 0, "skipped_locked": 0,
415
- "entry_ids": [], "by_field": {}}
416
- for it in items:
417
- if limit is not None and rep["written"] >= limit:
418
- break
419
- nid = it["id"]
420
- e = cg.index["nodes"].get(nid)
421
- if not e:
422
- rep["skipped_drift"] += 1
423
- continue
424
- fm, content = cg._read(e)
425
- if fm is None or crypto.is_encrypted(content):
426
- rep["skipped_locked"] += 1
427
- continue
428
- # 预演到执行之间节点可能被改动:只写仍缺失的字段
429
- todo = {f: d for f, d in it["fill"].items()
430
- if not _has_ccg_line(content, f)}
431
- if not todo:
432
- rep["skipped_drift"] += 1
433
- continue
434
- comment0 = _comment(fm)
435
- # 记录改写前的真相:rollback 必须「还原」而非「删除」——否则 comment
436
- # 里原有的声明会被误删,导致回填不可重复(回滚不是真逆操作)。
437
- prev_c = {f: comment0[f] for f in todo if f in comment0}
438
- fm_before = {}
439
- if "不适用条件" in todo:
440
- fm_before["non_applicable_conditions"] = {
441
- "had": "non_applicable_conditions" in fm,
442
- "value": fm.get("non_applicable_conditions")}
443
- if "验证方式" in todo and not nodefile.verification_basis_valid(fm):
444
- fm_before["verification_basis"] = {
445
- "had": "verification_basis" in fm,
446
- "value": fm.get("verification_basis")}
447
- comment = _ensure_comment(fm)
448
- wid = _sha(f"{nid}|{batch}|{time.time()}")
449
- applied = {}
450
- for f, d in todo.items():
451
- content = _upsert_ccg_line(content, f, d["value"])
452
- comment[f] = d["value"]
453
- if f == "不适用条件":
454
- fm["non_applicable_conditions"] = [
455
- s.strip() for s in d["value"].split(";") if s.strip()]
456
- if f == "验证方式" and not nodefile.verification_basis_valid(fm):
457
- fm["verification_basis"] = BASIS_ENUM_DEFAULT
458
- applied[f] = {"after": d["value"], "basis": d["basis"],
459
- "before": prev_c.get(f)}
460
- rep["by_field"][f] = rep["by_field"].get(f, 0) + 1
461
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
462
- durable=True)
463
- append_jsonl(_log_path(cg), {
464
- "action": "backfill", "ts": time.time(), "batch": batch,
465
- "actor": actor, "entry_id": it["entry_id"], "write_id": wid,
466
- "node": nid, "layer": e.get("layer"), "fields": applied,
467
- "fm_before": fm_before,
468
- "content_hash_after": _sha(content)})
469
- rep["written"] += 1
470
- rep["entry_ids"].append(it["entry_id"])
471
- if rep["written"]:
472
- cg.rebuild_index()
473
- rep["plan_remaining"] = max(0, p["targeted"] - len(items))
474
- return rep
475
-
476
-
477
- # 生效条件:x 经 _as_cg 定位后读留痕日志,仅对 action=='backfill' 且(batch 为假值则不过滤 batch,否则 rec['batch']==batch)、(entry_ids 为假值则不过滤,否则 rec['entry_id'] 属于该集合)、write_id 未出现在已完成 rollback 集合中、节点命中 cg.index['nodes'] 且 cg._read(e) 的 fm 可读、字段当前 _ccg_field(content, f) 等于留痕 after 的记录执行撤销写回(before 为 None 则删该 comment 键,否则还原原值),无字段可撤销只计 conflict 不写盘,reverted 非空时 rebuild_index,结果汇总进返回的 rep。
478
- def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
479
- """按留痕反向应用:撤销本批次回填(当前值 ≠ 写入值时跳过,防覆盖)。"""
480
- cg = _as_cg(x)
481
- want = set(entry_ids) if entry_ids else None
482
- rep = {"root": cg.root, "action": "backfill_rollback", "actor": actor,
483
- "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
484
- "fields_reverted": 0, "skipped_done": 0}
485
- log = list(read_jsonl(_log_path(cg)) or [])
486
- done = {r.get("write_id") for r in log
487
- if r.get("action") == "backfill_rollback" and r.get("write_id")}
488
- for rec in log:
489
- if rec.get("action") != "backfill":
490
- continue
491
- if rec.get("write_id") and rec.get("write_id") in done:
492
- rep["skipped_done"] += 1
493
- continue
494
- if batch and rec.get("batch") != batch:
495
- continue
496
- if want is not None and rec.get("entry_id") not in want:
497
- continue
498
- nid = rec.get("node")
499
- e = cg.index["nodes"].get(nid)
500
- if not e:
501
- rep["missing"] += 1
502
- continue
503
- fm, content = cg._read(e)
504
- if fm is None:
505
- rep["missing"] += 1
506
- continue
507
- comment = _comment(fm)
508
- reverted, conflicted = {}, []
509
- for f, d in (rec.get("fields") or {}).items():
510
- if _ccg_field(content, f) != d.get("after"):
511
- conflicted.append(f) # 已被后续修改 → 不撤销
512
- continue
513
- content = _remove_ccg_line(content, f)
514
- b = d.get("before")
515
- if b is None:
516
- comment.pop(f, None) # 原本就没有 → 删回「无」
517
- else:
518
- comment[f] = b # 原本有 → 还原原值(非删除)
519
- reverted[f] = d.get("after")
520
- if not reverted:
521
- rep["conflict"] += 1
522
- continue
523
- for k, box in (rec.get("fm_before") or {}).items():
524
- if box.get("had"):
525
- fm[k] = box.get("value")
526
- else:
527
- fm.pop(k, None)
528
- st = fm.get("state_attributes")
529
- if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
530
- and not st["comment"]:
531
- st.pop("comment", None)
532
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
533
- durable=True)
534
- append_jsonl(_log_path(cg), {
535
- "action": "backfill_rollback", "ts": time.time(), "actor": actor,
536
- "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
537
- "write_id": rec.get("write_id"), "node": nid,
538
- "reverted": list(reverted), "conflict": conflicted})
539
- rep["reverted"] += 1
540
- rep["fields_reverted"] += len(reverted)
541
- if conflicted:
542
- rep["conflict"] += 1
543
- if rep["reverted"]:
544
- cg.rebuild_index()
545
- return rep
546
-
547
-
548
- # 生效条件:x 经 _as_cg,action/batch 为真值时过滤对应日志记录;limit 不为 None 且 >=0 时按 recs[-limit:] 截断(limit=0 时切片为全部记录),limit 为 None 或负数时不截断;返回 total/returned/records。
549
- def history(x, limit=100, action=None, batch=None) -> dict:
550
- cg = _as_cg(x)
551
- recs = []
552
- for rec in read_jsonl(_log_path(cg)) or []:
553
- if action and rec.get("action") != action:
554
- continue
555
- if batch and rec.get("batch") != batch:
556
- continue
557
- recs.append(rec)
558
- total = len(recs)
559
- if limit is not None and limit >= 0:
560
- recs = recs[-limit:]
561
- return {"root": cg.root, "total": total, "returned": len(recs),
562
- "records": recs}
563
-
564
-
565
- # ---- 能力标签注入(关键词启发式) ----------------------------------------
566
-
567
- # 候选匹配不得吃进两类「自产词」,否则候选再生、plan 永不收敛:
568
- # ① `fm.tags`:`cap:<op>` 是**注入结果**,回流后 `cap:route` 自匹配关键词
569
- # "route"、`cap:self_state` 自匹配 "self_state"…… 已注入节点会重新成为候选;
570
- # ② `# 验证方式:` 模板行:它是 CCG 五要素的必备行,展开后**几乎全库**命中
571
- # 「验证」,`cap:verify` 遂从能力判断退化为正文模板的副产品
572
- # (evolution 账本实测:预演命中 1630/1813)。
573
- _CAP_TEMPLATE_LINES = ("验证方式",)
574
-
575
-
576
- # 生效条件:content 中去除 # 与空白后、全角或半角冒号前首段命中 _CAP_TEMPLATE_LINES 的行被剔除,其余行保留并 join。
577
- def _cap_body(content: str) -> str:
578
- """正文(供关键词匹配)——剔除 CCG 模板行,防模板词污染候选。"""
579
- keep = []
580
- for line in (content or "").splitlines():
581
- head = line.strip().lstrip("#").strip()
582
- head = head.split(":", 1)[0].split(":", 1)[0].strip()
583
- if head in _CAP_TEMPLATE_LINES:
584
- continue
585
- keep.append(line)
586
- return "\n".join(keep)
587
-
588
-
589
- # 生效条件:当 fm 为可 get 的 frontmatter、e 为带 id 的节点条目、content 为正文文本时,返回 title+e.id+功能名/生效条件/子功能字段+正文前 300 字合并后的小写串(不含 fm.tags)。
590
- def _cap_text(e, fm, content) -> str:
591
- """候选匹配文本:节点标识 + CCG 字段 + 正文(**不含 `fm.tags`**)。"""
592
- parts = [_as_text(fm.get("title")), e.get("id") or ""]
593
- for f in ("功能名", "生效条件", "子功能"):
594
- v = _ccg_field(content, f)
595
- if v:
596
- parts.append(v)
597
- parts.append(_cap_body(content)[:300])
598
- return " ".join(parts).lower()
599
-
600
-
601
- # 生效条件:text 包含 CAP_RULES 中某 cap 的至少一个关键词时,该 cap 以匹配关键词与置信度加入返回,按置信度降序、cap 升序排序;无匹配返回空 hits。
602
- def cap_matches(text: str) -> list:
603
- hits = []
604
- for cap, kws in CAP_RULES.items():
605
- m = [k for k in kws if k in text]
606
- if m:
607
- hits.append({"cap": cap, "matched_by": m,
608
- "confidence": round(min(1.0, 0.4 + 0.2 * len(m)), 3)})
609
- hits.sort(key=lambda h: (-h["confidence"], h["cap"]))
610
- return hits
611
-
612
-
613
- # 生效条件:x 经 _as_cg,遍历 nodes 按 ids/layer 过滤,不可读计 denied,读取失败或加密计 locked,扫描后以 _cap_text 匹配并过滤 confidence >= min_conf 且标签未含 cap:,无命中计 present,有命中则 targeted 并受 limit 限制加入 items,返回 dry_run rep。
614
- def cap_plan(x, layer=None, limit=None, ids=None, min_conf=0.5) -> dict:
615
- """预演:按关键词启发式给出 `cap:<op>` 标签建议(含依据与置信度),不写盘。"""
616
- cg = _as_cg(x)
617
- rep = {"root": cg.root, "dry_run": True, "action": "cap",
618
- "min_conf": min_conf, "nodes_scanned": 0, "skipped_locked": 0,
619
- "skipped_present": 0, "skipped_denied": 0, "targeted": 0,
620
- "items": []}
621
- want = set(ids) if ids else None
622
- for nid, e in list((cg.index.get("nodes") or {}).items()):
623
- if want is not None and nid not in want:
624
- continue
625
- if layer and e.get("layer") != layer:
626
- continue
627
- if not _readable_guard(cg, e):
628
- rep["skipped_denied"] += 1
629
- continue
630
- fm, content = cg._read(e)
631
- if fm is None or crypto.is_encrypted(content):
632
- rep["skipped_locked"] += 1
633
- continue
634
- rep["nodes_scanned"] += 1
635
- have = set(t for t in (fm.get("tags") or []) if isinstance(t, str))
636
- hits = [h for h in cap_matches(_cap_text(e, fm, content))
637
- if h["confidence"] >= min_conf
638
- and f"cap:{h['cap']}" not in have]
639
- if not hits:
640
- rep["skipped_present"] += 1
641
- continue
642
- item = {"id": nid, "layer": e.get("layer"), "caps": hits,
643
- "entry_id": _entry_id("cap", nid)}
644
- rep["targeted"] += 1
645
- if limit is None or len(rep["items"]) < limit:
646
- rep["items"].append(item)
647
- rep["planned_ids"] = [i["id"] for i in rep["items"]]
648
- return rep
649
-
650
-
651
- # 生效条件:x 经 _as_cg,batch 假值回落 BATCH_DEFAULT;先 cap_plan 得 items,按 entry_ids 过滤;逐个检查节点存在、读取可读、无新增标签分别计 drift/locked/drift,成功写 tags 并记日志,limit 非 None 且 written >= limit 时 break;返回 rep。
652
- def cap_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
653
- batch=BATCH_DEFAULT, min_conf=0.5, actor=None) -> dict:
654
- """注入 `cap:<op>` 标签:只改 frontmatter.tags,不动正文。"""
655
- cg = _as_cg(x)
656
- batch = batch or BATCH_DEFAULT
657
- p = cap_plan(cg, layer=layer, limit=None, ids=ids, min_conf=min_conf)
658
- items = p["items"]
659
- if entry_ids:
660
- want = set(entry_ids)
661
- items = [i for i in items if i["entry_id"] in want]
662
- rep = {"root": cg.root, "dry_run": False, "action": "cap", "batch": batch,
663
- "actor": actor, "min_conf": min_conf, "planned": len(items),
664
- "written": 0, "skipped_locked": 0, "skipped_drift": 0,
665
- "tags_added": 0, "entry_ids": []}
666
- for it in items:
667
- if limit is not None and rep["written"] >= limit:
668
- break
669
- nid = it["id"]
670
- e = cg.index["nodes"].get(nid)
671
- if not e:
672
- rep["skipped_drift"] += 1
673
- continue
674
- fm, content = cg._read(e)
675
- if fm is None or crypto.is_encrypted(content):
676
- rep["skipped_locked"] += 1
677
- continue
678
- tags = list(fm.get("tags") or [])
679
- added = [f"cap:{h['cap']}" for h in it["caps"] if f"cap:{h['cap']}" not in tags]
680
- if not added:
681
- rep["skipped_drift"] += 1
682
- continue
683
- fm["tags"] = tags + added
684
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
685
- durable=True)
686
- append_jsonl(_log_path(cg), {
687
- "action": "cap", "ts": time.time(), "batch": batch, "actor": actor,
688
- "entry_id": it["entry_id"], "write_id": _sha(f"cap|{nid}|{time.time()}"),
689
- "node": nid, "layer": e.get("layer"),
690
- "tags_added": added,
691
- "evidence": {h["cap"]: h["matched_by"] for h in it["caps"]},
692
- "confidence": {h["cap"]: h["confidence"] for h in it["caps"]}})
693
- rep["written"] += 1
694
- rep["tags_added"] += len(added)
695
- rep["entry_ids"].append(it["entry_id"])
696
- if rep["written"]:
697
- cg.rebuild_index()
698
- rep["plan_remaining"] = max(0, p["targeted"] - len(items))
699
- return rep
700
-
701
-
702
- # 生效条件:x 经 _as_cg,读取日志中 action=cap 且未回滚的记录,按 batch/entry_ids 过滤,节点存在且原加入标签仍在 tags 中时移除,否则计 conflict/missing,返回 rep。
703
- def cap_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
704
- """撤销 cap 注入:仅移除**仍在 tags 里**的 `cap:` 标签(防覆盖后续修改)。"""
705
- cg = _as_cg(x)
706
- want = set(entry_ids) if entry_ids else None
707
- rep = {"root": cg.root, "action": "cap_rollback", "actor": actor,
708
- "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
709
- "tags_removed": 0, "skipped_done": 0}
710
- log = list(read_jsonl(_log_path(cg)) or [])
711
- done = {r.get("write_id") for r in log
712
- if r.get("action") == "cap_rollback" and r.get("write_id")}
713
- for rec in log:
714
- if rec.get("action") != "cap":
715
- continue
716
- if rec.get("write_id") and rec.get("write_id") in done:
717
- rep["skipped_done"] += 1
718
- continue
719
- if batch and rec.get("batch") != batch:
720
- continue
721
- if want is not None and rec.get("entry_id") not in want:
722
- continue
723
- nid, added = rec.get("node"), rec.get("tags_added") or []
724
- e = cg.index["nodes"].get(nid)
725
- if not e:
726
- rep["missing"] += 1
727
- continue
728
- fm, content = cg._read(e)
729
- if fm is None:
730
- rep["missing"] += 1
731
- continue
732
- tags = list(fm.get("tags") or [])
733
- removed = [t for t in added if t in tags]
734
- if not removed:
735
- rep["conflict"] += 1
736
- continue
737
- fm["tags"] = [t for t in tags if t not in set(removed)]
738
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
739
- durable=True)
740
- append_jsonl(_log_path(cg), {
741
- "action": "cap_rollback", "ts": time.time(), "actor": actor,
742
- "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
743
- "write_id": rec.get("write_id"), "node": nid,
744
- "tags_removed": removed})
745
- rep["reverted"] += 1
746
- rep["tags_removed"] += len(removed)
747
- if rep["reverted"]:
748
- cg.rebuild_index()
749
- return rep
750
-
751
-
752
- # ---- 豁免解除(ccg_exempt 分批摘除) --------------------------------------
753
-
754
- EXEMPT_FLAG = "ccg_exempt"
755
-
756
-
757
- # 生效条件:fm 的 verification_basis 合法且 content 的 ccg_completeness 标记 complete 时返回 (True, []),否则把缺失项放入 miss 返回 ready=False。
758
- def _exempt_ready(fm: dict, content: str):
759
- """摘豁免前置条件 → `(ready, missing)`。
760
-
761
- 硬约束(顺序依赖):证据未补齐就摘豁免,节点会从 DEFER **退化为 BLINDSPOT**,
762
- 比现状更差。故要求「合法 `verification_basis`」+「5 要素齐备(含验证方式行)」
763
- 同时成立才允许摘除。
764
- """
765
- miss = []
766
- if not nodefile.verification_basis_valid(fm):
767
- miss.append("verification_basis")
768
- if not nodefile.ccg_completeness(content).get("complete"):
769
- miss.append("ccg5")
770
- return (not miss), miss
771
-
772
-
773
- # 生效条件:x 经 _as_cg,遍历 nodes 按 ids/prefix/layer 过滤,内部层/不可读/读取失败或加密分别计数跳过,fm 无 EXEMPT_FLAG 计 not_exempt,require_ready 为真且不 ready 时计 unready 并在 want 为 None 时跳过,否则生成 item 受 limit 限制;sample 为真值时按 id 序等距抽取 sample 个。
774
- def exempt_plan(x, layer=None, limit=None, ids=None, require_ready=True,
775
- sample=0, prefix=None) -> dict:
776
- """预演:列出可摘 `ccg_exempt` 的节点(默认要求证据就绪),不写盘。
777
-
778
- `sample=N` 时按 id 序等距抽取 N 个作为验收样本(确定性,重跑同一样本)。
779
- `prefix`:按 id 前缀收窄范围(真实库用 `kp_`);内部 `anchor`/`self` 层出局。
780
- """
781
- cg = _as_cg(x)
782
- rep = {"root": cg.root, "dry_run": True, "action": "exempt",
783
- "require_ready": require_ready, "prefix": prefix, "nodes_scanned": 0,
784
- "skipped_locked": 0, "skipped_denied": 0, "skipped_not_exempt": 0,
785
- "skipped_internal": 0, "skipped_unready": 0, "unready_reasons": {},
786
- "targeted": 0, "items": [], "sample": []}
787
- want = set(ids) if ids else None
788
- for nid, e in list((cg.index.get("nodes") or {}).items()):
789
- if want is not None and nid not in want:
790
- continue
791
- if prefix and not str(nid).startswith(prefix):
792
- continue
793
- if layer and e.get("layer") != layer:
794
- continue
795
- if e.get("layer") in INTERNAL_LAYERS:
796
- rep["skipped_internal"] += 1
797
- continue
798
- if not _readable_guard(cg, e):
799
- rep["skipped_denied"] += 1
800
- continue
801
- fm, content = cg._read(e)
802
- if fm is None or crypto.is_encrypted(content):
803
- rep["skipped_locked"] += 1
804
- continue
805
- rep["nodes_scanned"] += 1
806
- if not fm.get(EXEMPT_FLAG):
807
- rep["skipped_not_exempt"] += 1
808
- continue
809
- ready, miss = _exempt_ready(fm, content)
810
- if require_ready and not ready:
811
- rep["skipped_unready"] += 1
812
- for m in miss:
813
- rep["unready_reasons"][m] = rep["unready_reasons"].get(m, 0) + 1
814
- # 批量:直接过滤(报表已计数);显式点名:入列交由 apply 判定并留
815
- # `exempt_skip` 记录——被点名的节点绝不静默丢弃。
816
- if want is None:
817
- continue
818
- item = {"id": nid, "layer": e.get("layer"), "ready": ready,
819
- "missing": miss, "basis": fm.get("verification_basis"),
820
- "entry_id": _entry_id("exempt", nid)}
821
- rep["targeted"] += 1
822
- if limit is None or len(rep["items"]) < limit:
823
- rep["items"].append(item)
824
- rep["planned_ids"] = [i["id"] for i in rep["items"]]
825
- if sample and rep["items"]:
826
- ids_all = [i["id"] for i in rep["items"]]
827
- k = min(int(sample), len(ids_all))
828
- stride = max(1, len(ids_all) // k)
829
- rep["sample"] = ids_all[::stride][:k]
830
- return rep
831
-
832
-
833
- # 生效条件:x 经 _as_cg,batch 假值回落 BATCH_DEFAULT;先 exempt_plan 得 items,按 entry_ids 过滤;逐个检查节点存在、可读、EXEMPT_FLAG 仍真、require_ready 为真时 _exempt_ready 再次通过;不通过计 skipped_unready 并记 exempt_skip;通过则置 EXEMPT_FLAG=False 写盘记日志;limit 限制 written;返回 rep。
834
- def exempt_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
835
- batch=BATCH_DEFAULT, require_ready=True, actor=None,
836
- prefix=None) -> dict:
837
- """分批摘除 `ccg_exempt`(置 False 而非删键,便于反向还原)。
838
-
839
- 就绪校验在**写入前**再查一次(plan 与 apply 之间可能被改动);
840
- 不满足则计 `skipped_unready` 并留 `exempt_skip` 记录,绝不硬摘。
841
- """
842
- cg = _as_cg(x)
843
- batch = batch or BATCH_DEFAULT
844
- p = exempt_plan(cg, layer=layer, ids=ids, require_ready=require_ready,
845
- prefix=prefix)
846
- items = p["items"]
847
- if entry_ids:
848
- want = set(entry_ids)
849
- items = [i for i in items if i["entry_id"] in want]
850
- rep = {"root": cg.root, "dry_run": False, "action": "exempt", "batch": batch,
851
- "actor": actor, "require_ready": require_ready, "planned": len(items),
852
- "written": 0, "skipped_unready": 0, "skipped_locked": 0,
853
- "skipped_drift": 0, "entry_ids": []}
854
- for it in items:
855
- if limit is not None and rep["written"] >= limit:
856
- break
857
- nid = it["id"]
858
- e = cg.index["nodes"].get(nid)
859
- if not e:
860
- rep["skipped_drift"] += 1
861
- continue
862
- fm, content = cg._read(e)
863
- if fm is None or crypto.is_encrypted(content):
864
- rep["skipped_locked"] += 1
865
- continue
866
- if not fm.get(EXEMPT_FLAG):
867
- rep["skipped_drift"] += 1
868
- continue
869
- ready, miss = _exempt_ready(fm, content)
870
- if require_ready and not ready:
871
- rep["skipped_unready"] += 1
872
- append_jsonl(_log_path(cg), {
873
- "action": "exempt_skip", "ts": time.time(), "batch": batch,
874
- "actor": actor, "node": nid, "missing": miss})
875
- continue
876
- before = fm.get(EXEMPT_FLAG)
877
- fm[EXEMPT_FLAG] = False
878
- wid = _sha(f"exempt|{nid}|{time.time()}")
879
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
880
- durable=True)
881
- append_jsonl(_log_path(cg), {
882
- "action": "exempt", "ts": time.time(), "batch": batch, "actor": actor,
883
- "entry_id": it["entry_id"], "write_id": wid, "node": nid,
884
- "layer": e.get("layer"), "exempt_before": before,
885
- "basis": fm.get("verification_basis")})
886
- rep["written"] += 1
887
- rep["entry_ids"].append(it["entry_id"])
888
- if rep["written"]:
889
- cg.rebuild_index()
890
- rep["plan_remaining"] = max(0, p["targeted"] - len(items))
891
- return rep
892
-
893
-
894
- # 生效条件:当 x 可解析为 cg 时,日志中 action 为 exempt 的记录若其非空 write_id 已存在于既有 action 为 exempt_rollback 的记录 write_id 集合中,则跳过并计入 skipped_done,否则在通过 batch 与 entry_ids 过滤后,节点存在且可读、EXEMPT_FLAG 当前为假时,该记录才被还原并计入 reverted;。
895
- def exempt_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
896
- """反向还原 `ccg_exempt`:仅当当前仍为「已摘」状态时还原,否则计 conflict。"""
897
- cg = _as_cg(x)
898
- want = set(entry_ids) if entry_ids else None
899
- rep = {"root": cg.root, "action": "exempt_rollback", "actor": actor,
900
- "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
901
- "skipped_done": 0}
902
- log = list(read_jsonl(_log_path(cg)) or [])
903
- done = {r.get("write_id") for r in log
904
- if r.get("action") == "exempt_rollback" and r.get("write_id")}
905
- for rec in log:
906
- if rec.get("action") != "exempt":
907
- continue
908
- if rec.get("write_id") and rec.get("write_id") in done:
909
- rep["skipped_done"] += 1
910
- continue
911
- if batch and rec.get("batch") != batch:
912
- continue
913
- if want is not None and rec.get("entry_id") not in want:
914
- continue
915
- nid = rec.get("node")
916
- e = cg.index["nodes"].get(nid)
917
- if not e:
918
- rep["missing"] += 1
919
- continue
920
- fm, content = cg._read(e)
921
- if fm is None:
922
- rep["missing"] += 1
923
- continue
924
- if fm.get(EXEMPT_FLAG):
925
- rep["conflict"] += 1
926
- continue
927
- fm[EXEMPT_FLAG] = rec.get("exempt_before", True)
928
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
929
- durable=True)
930
- append_jsonl(_log_path(cg), {
931
- "action": "exempt_rollback", "ts": time.time(), "actor": actor,
932
- "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
933
- "write_id": rec.get("write_id"), "node": nid,
934
- "exempt_restored": fm.get(EXEMPT_FLAG)})
935
- rep["reverted"] += 1
936
- if rep["reverted"]:
937
- cg.rebuild_index()
938
- return rep
939
-
940
-
941
- # ---- 待补台账 / 存量清洗(生效条件口径修正) ------------------------------
942
- #
943
- # 背景:旧口径把 `condition_space.observation_position` 单槽加前缀「观测位置:」
944
- # 当作生效条件写入(真实库 109 条)。观测位置 ≠ 生效条件——生效条件是**整条**
945
- # 条件空间声明的合成。本段负责三件事:
946
- # ① conditions_pending 只读台账:未声明且四槽推不出的节点
947
- # ② fix_conditions_* 清洗:冒充行重渲染为合规声明,或删除并登记待补
948
- # ③ verify_conditions 只读复算:验收指标 legacy_position == 0
949
-
950
-
951
- # 生效条件:x 经 _as_cg,遍历 nodes 按 prefix/layer 过滤,内部层/不可读/读取失败或加密/derived 层或 SKIP_TAGS 分别计数跳过;扫描后,正文已有生效条件计 declared,derive_fields 可推出计 derivable,否则按 condition_space 缺失槽计 pending 并受 limit 限制收集 items;返回 rep。
952
- def conditions_pending(x, prefix=None, layer=None, limit=None) -> dict:
953
- """只读台账:列出「未声明生效条件、且四槽推不出」的节点及其缺失槽。
954
-
955
- 生效条件已成为必填项(`CCG_REQUIRED`),但存量节点未必有足够声明可补。
956
- 这类节点**不能静默判 ACCEPT**:进本台账等待后续补充;确实补不动者由资格
957
- 判定落 BLINDSPOT(而不是被「常用条件默认省略」掩盖)。本函数不写盘。
958
- """
959
- cg = _as_cg(x)
960
- rep = {"root": cg.root, "dry_run": True, "action": "conditions_pending",
961
- "prefix": prefix, "nodes_scanned": 0, "skipped_locked": 0,
962
- "skipped_denied": 0, "skipped_internal": 0, "skipped_derived": 0,
963
- "declared": 0, "derivable": 0, "pending": 0,
964
- "by_missing": {}, "items": []}
965
- for nid, e in list((cg.index.get("nodes") or {}).items()):
966
- if prefix and not str(nid).startswith(prefix):
967
- continue
968
- if layer and e.get("layer") != layer:
969
- continue
970
- if e.get("layer") in INTERNAL_LAYERS:
971
- rep["skipped_internal"] += 1
972
- continue
973
- if not _readable_guard(cg, e):
974
- rep["skipped_denied"] += 1
975
- continue
976
- fm, content = cg._read(e)
977
- if fm is None or crypto.is_encrypted(content):
978
- rep["skipped_locked"] += 1
979
- continue
980
- if (e.get("layer") in SKIP_LAYERS
981
- or any(t in SKIP_TAGS for t in (fm.get("tags") or []))):
982
- rep["skipped_derived"] += 1
983
- continue
984
- rep["nodes_scanned"] += 1
985
- if _has_ccg_line(content, "生效条件"):
986
- rep["declared"] += 1
987
- continue
988
- if "生效条件" in derive_fields(fm, content):
989
- # 有来源、只是还没接线 → 属于「待回填」,不是缺证据
990
- rep["derivable"] += 1
991
- continue
992
- cs = fm.get("condition_space")
993
- miss = nodefile.condition_space_missing(cs)
994
- if not isinstance(cs, dict) or len(miss) == len(nodefile.CONDITION_SLOTS):
995
- key = "无条件空间声明"
996
- else:
997
- key = "缺" + "+".join(_SLOT_LABEL.get(k, k) for k in miss)
998
- rep["by_missing"][key] = rep["by_missing"].get(key, 0) + 1
999
- rep["pending"] += 1
1000
- if limit is None or len(rep["items"]) < limit:
1001
- rep["items"].append({
1002
- "id": nid, "layer": e.get("layer"), "missing_slots": miss,
1003
- "missing_text": key,
1004
- "fallback": "补写生效条件;确实补不动 → 判 BLINDSPOT"})
1005
- if limit is not None:
1006
- rep["items"] = rep["items"][:limit]
1007
- return rep
1008
-
1009
-
1010
- # 生效条件:cg 的留痕日志中存在 basis 为 LEGACY_CONDITION_BASIS 且未被回滚的 backfill 生效条件写入时,按 node 记录最后一次的 after/original 返回;无匹配返回空。
1011
- def _legacy_condition_records(cg) -> dict:
1012
- """→ {node: {after, original, …}}:我们**自己写下的**单槽冒充行(真源 = 留痕)。
1013
-
1014
- 只有留痕里 `basis == LEGACY_CONDITION_BASIS` 的行才敢自动改——人写下的
1015
- 「观测位置:…」声明不在其列(宁可漏改,不可误改)。已被回滚的写入剔除;
1016
- 同一节点多次写入时以最后一次为准。
1017
- """
1018
- log = list(read_jsonl(_log_path(cg)) or [])
1019
- undone = {r.get("write_id") for r in log
1020
- if r.get("action") == "backfill_rollback" and r.get("write_id")}
1021
- out = {}
1022
- for rec in log:
1023
- if rec.get("action") != "backfill":
1024
- continue
1025
- if rec.get("write_id") and rec.get("write_id") in undone:
1026
- continue
1027
- f = (rec.get("fields") or {}).get("生效条件")
1028
- if not isinstance(f, dict) or f.get("basis") != LEGACY_CONDITION_BASIS:
1029
- continue
1030
- nid = rec.get("node")
1031
- if not nid:
1032
- continue
1033
- out[nid] = {"after": f.get("after"), "original": f.get("before"),
1034
- "write_id": rec.get("write_id"), "batch": rec.get("batch")}
1035
- return out
1036
-
1037
-
1038
- # 生效条件:x 经 _as_cg,从 _legacy_condition_records 取候选,按 prefix 过滤,节点不存在/不可读/读取失败或加密分别计 missing/denied/locked;当前生效条件等于留痕 after 时,四槽合成非空则 rewrite 否则 drop,不等则 conflict;items 受 limit 限制;返回 dry_run rep。
1039
- def fix_conditions_plan(x, prefix=None, limit=None) -> dict:
1040
- """预演:清洗存量单槽冒充行。不写盘。
1041
-
1042
- 逐条判定(都要求「当前值仍等于我们当初写入的值」,否则计 `conflict`、不碰):
1043
- · 四槽齐备 → `rewrite`:改写为 `condition_space_text(cs)` 合成声明;
1044
- · 四槽不全 → `drop`:删掉冒充行并登记待补。补不全的行留着比删掉更危险——
1045
- 它会被当成生效条件读,等于把坐标的一维当成整条声明。
1046
- """
1047
- cg = _as_cg(x)
1048
- rep = {"root": cg.root, "dry_run": True, "action": "fix_conditions",
1049
- "prefix": prefix, "legacy": 0, "targeted": 0, "rewrite": 0,
1050
- "drop": 0, "conflict": 0, "missing": 0, "skipped_locked": 0,
1051
- "skipped_denied": 0, "items": []}
1052
- for nid, rec in _legacy_condition_records(cg).items():
1053
- if prefix and not str(nid).startswith(prefix):
1054
- continue
1055
- e = cg.index["nodes"].get(nid)
1056
- if not e:
1057
- rep["missing"] += 1
1058
- continue
1059
- if not _readable_guard(cg, e):
1060
- rep["skipped_denied"] += 1
1061
- continue
1062
- fm, content = cg._read(e)
1063
- if fm is None or crypto.is_encrypted(content):
1064
- rep["skipped_locked"] += 1
1065
- continue
1066
- rep["legacy"] += 1
1067
- cur = _ccg_field(content, "生效条件") or ""
1068
- if cur != (rec.get("after") or ""):
1069
- rep["conflict"] += 1 # 已被后续改动 → 不碰
1070
- continue
1071
- new = _condition_text(fm)
1072
- act = "rewrite" if new else "drop"
1073
- rep["targeted"] += 1
1074
- rep[act] += 1
1075
- if limit is None or len(rep["items"]) < limit:
1076
- rep["items"].append({
1077
- "id": nid, "layer": e.get("layer"), "action": act,
1078
- "before": cur, "after": new,
1079
- "missing_slots": nodefile.condition_space_missing(
1080
- fm.get("condition_space")),
1081
- "comment_before": (_comment(fm) or {}).get("生效条件"),
1082
- "comment_original": rec.get("original"),
1083
- "entry_id": _entry_id(FIX_BATCH_DEFAULT, nid)})
1084
- rep["planned_ids"] = [i["id"] for i in rep["items"]]
1085
- return rep
1086
-
1087
-
1088
- # 生效条件:x 经 _as_cg,batch 假值回落 FIX_BATCH_DEFAULT;先 fix_conditions_plan 得 items,按 ids/entry_ids 过滤;逐个再验当前值仍等于 before 且 comment_before 一致,否则 skipped_drift;rewrite 时 upsert 生效条件行并写 comment,drop 时删行并还原 comment;写盘记 condition_fix,drop 另记 condition_pending;limit 限制 rewritten+dropped;返回 rep。
1089
- def fix_conditions_apply(x, ids=None, entry_ids=None, prefix=None, limit=None,
1090
- batch=FIX_BATCH_DEFAULT, actor=None) -> dict:
1091
- """执行清洗:`rewrite` 改写为四槽合成声明;`drop` 删行并登记待补。
1092
-
1093
- 写盘与留痕同 `apply`:`_backfill.jsonl` 记 `condition_fix`;`drop` 另记
1094
- `condition_pending`(待补台账的可追溯副本)。幂等:清洗后不再有候选行。
1095
- """
1096
- cg = _as_cg(x)
1097
- batch = batch or FIX_BATCH_DEFAULT
1098
- p = fix_conditions_plan(cg, prefix=prefix, limit=None)
1099
- items = p["items"]
1100
- if ids:
1101
- want = set(ids)
1102
- items = [i for i in items if i["id"] in want]
1103
- if entry_ids:
1104
- want = set(entry_ids)
1105
- items = [i for i in items if i["entry_id"] in want]
1106
- rep = {"root": cg.root, "dry_run": False, "action": "fix_conditions",
1107
- "batch": batch, "actor": actor, "planned": len(items),
1108
- "rewritten": 0, "dropped": 0, "pending_logged": 0,
1109
- "skipped_drift": 0, "skipped_locked": 0, "skipped_denied": 0,
1110
- "entry_ids": []}
1111
- for it in items:
1112
- if limit is not None and (rep["rewritten"] + rep["dropped"]) >= limit:
1113
- break
1114
- nid = it["id"]
1115
- e = cg.index["nodes"].get(nid)
1116
- if not e:
1117
- rep["skipped_drift"] += 1
1118
- continue
1119
- fm, content = cg._read(e)
1120
- if fm is None or crypto.is_encrypted(content):
1121
- rep["skipped_locked"] += 1
1122
- continue
1123
- # 预演 → 执行之间可能被改动:当前值必须仍等于我们当初写入的值
1124
- cur = _ccg_field(content, "生效条件")
1125
- comment_before = (_comment(fm) or {}).get("生效条件")
1126
- if (cur or "") != (it["before"] or "") \
1127
- or comment_before != it["comment_before"]:
1128
- rep["skipped_drift"] += 1
1129
- continue
1130
- comment = _ensure_comment(fm)
1131
- if it["action"] == "rewrite":
1132
- content = _upsert_ccg_line(content, "生效条件", it["after"])
1133
- comment["生效条件"] = it["after"]
1134
- else:
1135
- content = _remove_ccg_line(content, "生效条件")
1136
- # comment 里那份是同一污染的副本 → 还原为**写入前**的原值(真逆操作)
1137
- orig = it["comment_original"]
1138
- if orig in (None, ""):
1139
- comment.pop("生效条件", None)
1140
- else:
1141
- comment["生效条件"] = orig
1142
- st = fm.get("state_attributes")
1143
- if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
1144
- and not st["comment"]:
1145
- st.pop("comment", None)
1146
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
1147
- durable=True)
1148
- append_jsonl(_log_path(cg), {
1149
- "action": "condition_fix", "ts": time.time(), "batch": batch,
1150
- "actor": actor, "entry_id": it["entry_id"],
1151
- "write_id": _sha(f"cond_fix|{nid}|{time.time()}"),
1152
- "node": nid, "layer": e.get("layer"), "fix": it["action"],
1153
- "fields": {"生效条件": {
1154
- "before": it["before"], "after": it["after"],
1155
- "basis": BASIS_CONDITION_SYNTH if it["action"] == "rewrite"
1156
- else "dropped:legacy_position"}},
1157
- "comment_before": comment_before,
1158
- "comment_original": it["comment_original"],
1159
- "missing_slots": it["missing_slots"],
1160
- "content_hash_after": _sha(content)})
1161
- if it["action"] == "rewrite":
1162
- rep["rewritten"] += 1
1163
- else:
1164
- rep["dropped"] += 1
1165
- append_jsonl(_log_path(cg), {
1166
- "action": "condition_pending", "ts": time.time(),
1167
- "batch": batch, "actor": actor, "node": nid,
1168
- "layer": e.get("layer"), "missing_slots": it["missing_slots"],
1169
- "reason": "四槽不可合成:单槽冒充行已删除,等待后续补充;"
1170
- "确实补不动则判 BLINDSPOT"})
1171
- rep["pending_logged"] += 1
1172
- rep["entry_ids"].append(it["entry_id"])
1173
- if rep["rewritten"] or rep["dropped"]:
1174
- cg.rebuild_index()
1175
- rep["plan_remaining"] = max(0, p["targeted"] - len(items))
1176
- return rep
1177
-
1178
-
1179
- # 生效条件:x 经 _as_cg,读取日志中 action=condition_fix 且未回滚的记录,按 batch/entry_ids 过滤,节点存在且当前生效条件等于 after 时,还原 before(空则删行)并还原 comment_before,否则计 conflict/missing;返回 rep。
1180
- def fix_conditions_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
1181
- """按留痕反向应用:恢复被改写的旧值 / 重建被删除的冒充行。
1182
-
1183
- 与 `rollback` 同一条纪律:**只在当前值仍等于写入值**时撤销,
1184
- 否则计 `conflict` 跳过(防覆盖后续人工修改)。
1185
- """
1186
- cg = _as_cg(x)
1187
- want = set(entry_ids) if entry_ids else None
1188
- rep = {"root": cg.root, "action": "fix_conditions_rollback", "actor": actor,
1189
- "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
1190
- "skipped_done": 0}
1191
- log = list(read_jsonl(_log_path(cg)) or [])
1192
- done = {r.get("write_id") for r in log
1193
- if r.get("action") == "fix_conditions_rollback" and r.get("write_id")}
1194
- for rec in log:
1195
- if rec.get("action") != "condition_fix":
1196
- continue
1197
- if rec.get("write_id") and rec.get("write_id") in done:
1198
- rep["skipped_done"] += 1
1199
- continue
1200
- if batch and rec.get("batch") != batch:
1201
- continue
1202
- if want is not None and rec.get("entry_id") not in want:
1203
- continue
1204
- nid = rec.get("node")
1205
- e = cg.index["nodes"].get(nid)
1206
- if not e:
1207
- rep["missing"] += 1
1208
- continue
1209
- fm, content = cg._read(e)
1210
- if fm is None:
1211
- rep["missing"] += 1
1212
- continue
1213
- d = (rec.get("fields") or {}).get("生效条件") or {}
1214
- cur = _ccg_field(content, "生效条件") or ""
1215
- if cur != (d.get("after") or ""):
1216
- rep["conflict"] += 1 # 已被后续改动 → 不撤销
1217
- continue
1218
- before = d.get("before")
1219
- if before in (None, ""):
1220
- content = _remove_ccg_line(content, "生效条件")
1221
- else:
1222
- content = _upsert_ccg_line(content, "生效条件", before)
1223
- comment = _ensure_comment(fm)
1224
- cb = rec.get("comment_before")
1225
- if cb in (None, ""):
1226
- comment.pop("生效条件", None)
1227
- else:
1228
- comment["生效条件"] = cb
1229
- st = fm.get("state_attributes")
1230
- if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
1231
- and not st["comment"]:
1232
- st.pop("comment", None)
1233
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
1234
- durable=True)
1235
- append_jsonl(_log_path(cg), {
1236
- "action": "fix_conditions_rollback", "ts": time.time(),
1237
- "actor": actor, "batch": rec.get("batch"),
1238
- "entry_id": rec.get("entry_id"), "write_id": rec.get("write_id"),
1239
- "node": nid, "restored": before})
1240
- rep["reverted"] += 1
1241
- if rep["reverted"]:
1242
- cg.rebuild_index()
1243
- return rep
1244
-
1245
-
1246
- # 生效条件:x 经 _as_cg,遍历 nodes 按 prefix 过滤、内部层跳过,读取失败或加密跳过;对每个节点当前生效条件行,缺失计 absent,否则按 is_legacy_position_condition、等于 _condition_text 合成、等于 comment、其他分别计数,legacy_ids 受 limit 限制;返回 rep。
1247
- def verify_conditions(x, prefix=None, limit=None) -> dict:
1248
- """复算核对(只读):生效条件行的来源分布 + 弱等价残留计数。
1249
-
1250
- `legacy_position` 必须为 **0**——这是本轮口径修正的验收指标:
1251
- 「观测位置:X」单槽冒充行不得再出现在任何节点里。
1252
- """
1253
- cg = _as_cg(x)
1254
- rep = {"root": cg.root, "dry_run": True, "action": "verify_conditions",
1255
- "prefix": prefix, "nodes_scanned": 0, "absent": 0, "declared": 0,
1256
- "from_synthesis": 0, "from_comment": 0, "legacy_position": 0,
1257
- "other": 0, "by_prefix": {}, "legacy_ids": []}
1258
- for nid, e in list((cg.index.get("nodes") or {}).items()):
1259
- if prefix and not str(nid).startswith(prefix):
1260
- continue
1261
- if e.get("layer") in INTERNAL_LAYERS:
1262
- continue
1263
- fm, content = cg._read(e)
1264
- if fm is None or crypto.is_encrypted(content):
1265
- continue
1266
- rep["nodes_scanned"] += 1
1267
- cur = _ccg_field(content, "生效条件")
1268
- if not cur:
1269
- rep["absent"] += 1
1270
- kind = "absent"
1271
- else:
1272
- rep["declared"] += 1
1273
- synth = _condition_text(fm)
1274
- cmt = _as_text(_comment(fm).get("生效条件"))
1275
- if nodefile.is_legacy_position_condition(cur):
1276
- rep["legacy_position"] += 1
1277
- kind = "legacy_position"
1278
- if limit is None or len(rep["legacy_ids"]) < limit:
1279
- rep["legacy_ids"].append(nid)
1280
- elif synth and cur == synth:
1281
- rep["from_synthesis"] += 1
1282
- kind = "from_synthesis"
1283
- elif cmt and cur == cmt:
1284
- rep["from_comment"] += 1
1285
- kind = "from_comment"
1286
- else:
1287
- rep["other"] += 1
1288
- kind = "other"
1289
- box = rep["by_prefix"].setdefault(str(nid).split("_")[0], {})
1290
- box[kind] = box.get(kind, 0) + 1
1291
- return rep
1292
-
1293
-
1294
- # ---- 统一入口 ------------------------------------------------------------
1295
-
1296
- ACTIONS = ("backfill", "backfill_rollback", "backfill_history",
1297
- "cap", "cap_rollback", "cap_history",
1298
- "exempt", "exempt_rollback", "exempt_history")
1299
-
1300
-
1301
- # 生效条件:按其 action 分派——action=='backfill' 时 kw['apply'] 为真调 apply(x, 去掉 apply 的 kw)、否则调 plan 同参;action=='cap'/'exempt' 同理在 kw['apply'] 为真时调 cap_apply/exempt_apply、否则调 cap_plan/exempt_plan;action=='backfill_rollback'/'cap_rollback'/'exempt_rollback' 分别调 rollback/cap_rollback/exempt_rollback(x, **kw);action=='backfill_history' 调 history(x, **kw),'cap_history'/'exempt_history' 调 history(x, action='cap'/'exempt', **kw);其余 action 值抛 ValueError。
1302
- def run(x, action, **kw) -> dict:
1303
- """`maintain` op 的分派入口:action ∈ ACTIONS。"""
1304
- if action == "backfill":
1305
- if kw.get("apply"):
1306
- return apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1307
- return plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1308
- if action == "backfill_rollback":
1309
- return rollback(x, **kw)
1310
- if action == "backfill_history":
1311
- return history(x, **kw)
1312
- if action == "cap":
1313
- if kw.get("apply"):
1314
- return cap_apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1315
- return cap_plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1316
- if action == "cap_rollback":
1317
- return cap_rollback(x, **kw)
1318
- if action == "cap_history":
1319
- return history(x, action="cap", **kw)
1320
- if action == "exempt":
1321
- if kw.get("apply"):
1322
- return exempt_apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1323
- return exempt_plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1324
- if action == "exempt_rollback":
1325
- return exempt_rollback(x, **kw)
1326
- if action == "exempt_history":
1327
- return history(x, action="exempt", **kw)
1
+ # -*- coding: utf-8 -*-
2
+ """真实库对齐(P32):CCG 回填 + 能力标签注入。
3
+
4
+ 为什么是「回填」而不是「重写」
5
+ ------------------------------
6
+ 迁移入库的历史节点里,条件信息**往往已经在 frontmatter 里**:
7
+ `condition_space`(四槽)、`non_applicable_conditions`、`verification_basis`、
8
+ `state_attributes.comment.*`。缺的只是把它们渲染成 CCG 正文行(`# 生效条件:…`
9
+ 等)。缺了正文行,`judge_qualification` 一律判 BLINDSPOT——节点从「可检索」
10
+ 掉到「不可判定」。
11
+
12
+ 回填 = 把**已声明的证据**渲染成 CCG 行。它不发明条件,只搬运已有声明。
13
+
14
+ 生效条件的来源链(本轮口径修正:观测位置 ≠ 生效条件)
15
+ ----------------------------------------------------
16
+ 生效条件**只能**来自两处,且优先成文声明:
17
+ ① `state_attributes.comment.生效条件`(人/流程写下的成文声明)
18
+ ② `nodefile.condition_space_text(frontmatter.condition_space)`(四槽合成)
19
+ 旧版曾回退到 `condition_space.observation_position` **单槽**,加前缀「观测位置:」
20
+ 冒充生效条件——那是把坐标的一维当成整条生效条件,真实库因此落了 109 条弱等价行。
21
+ 该回退**已删除**;四槽不齐 → 不写(部分槽不构成完整条件空间声明),转待补台账。
22
+ `fix_conditions_*` 三个函数用于清洗存量:把已落库的单槽冒充行**重渲染**为合规
23
+ 合成声明,或**删除并登记待补**——全程留痕、可回滚、幂等。
24
+
25
+ 纪律(对齐 consolidate 的固化纪律)
26
+ ----------------------------------
27
+ · 不猜测:字段只在**有来源**时才写;来源写进留痕 `basis`;无来源 → 跳过并计入
28
+ `unfillable`,绝不编造。
29
+ · 可预演:`plan()` 只出报表、不改盘;`apply()` 才写。默认只回填「补完即可判定」
30
+ 的节点,`partial`(补完仍不全)默认不写,除非显式 `include_partial=True`。
31
+ · 可留痕:每次写入记一条 `_backfill.jsonl`(字段 / 写入值 / 依据 / 批次 / 操作者)。
32
+ · 可回滚:`rollback()` 按留痕反向应用,且**只在当前值仍等于写入值**时撤销
33
+ (防覆盖后续人工修改),否则计入 `conflict` 跳过。
34
+ · fail-closed:密文节点一律跳过,**绝不解密回写**。
35
+
36
+ 能力标签注入
37
+ ------------
38
+ `cap:<op>` 标签让路由能返回建议能力名(`mcp_server` 读 frontmatter.tags 的
39
+ `cap:` 前缀)。匹配是**关键词启发式**(对标 `tokens.ALL_OPS` 工具名清单),
40
+ 不是语义推断:结果带 `matched_by`(命中的关键词)与 `confidence`,调用方据此
41
+ 判断可信度。只改 frontmatter.tags,不动正文。
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import hashlib
46
+ import os
47
+ import time
48
+ from collections import OrderedDict
49
+
50
+ from . import crypto, nodefile, tokens
51
+ from .consolidate import _has_ccg_line, _upsert_ccg_line
52
+ from .fsutil import append_jsonl, read_jsonl
53
+ from .mdcos import MdCGOS, _ccg_field
54
+
55
+ # ---- 常量 ----------------------------------------------------------------
56
+
57
+ BACKFILL_LOG = "_backfill.jsonl"
58
+
59
+ # 负记忆层是「故意无条件」的(覆盖标记),派生脚手架不该再被回填成事实
60
+ SKIP_LAYERS = ("rejected", "unresolved", "goals")
61
+ # 推断脚手架 / 概念层不得回填:否则会污染锚点解析(见 predict.anchor_from_description)
62
+ SKIP_TAGS = ("gap_hint", "scene", "reconstructed", "concept", "insight")
63
+ # 记忆系统**内部脚手架层**(锚点解析 anchor / 自模型修订 self):不是用户知识,永不可回填;
64
+ # 被回填成事实会污染锚点解析与自模型(与 SKIP_LAYERS 同源理由)。crosscheck 亦复用之。
65
+ INTERNAL_LAYERS = ("anchor", "self")
66
+
67
+ # 验证基底枚举 → 可读声明(人/流程声明,不靠模型生成)
68
+ BASIS_TEXT = OrderedDict((
69
+ ("compiler", "编译器/静态检查通过"),
70
+ ("test", "单元测试/回归测试通过"),
71
+ ("measurement", "实测数据(benchmark / 采样)"),
72
+ ("formal_proof", "形式化证明"),
73
+ ("data", "数据/语料统计"),
74
+ # 来源一致性档(文科):文科知识非可复现的物理事实,以来源表述一致为足够基底
75
+ ("textbook", "依人教版教材表述一致(文科·来源一致性)"),
76
+ ("public_kb", "公开知识库条目一致(文科·来源一致性)"),
77
+ ("other", "人工评审或离线工序声明"),
78
+ ))
79
+ BASIS_ENUM_DEFAULT = "other"
80
+
81
+ BATCH_DEFAULT = "backfill"
82
+
83
+ #: 生效条件的**唯一结构化来源**:condition_space 四槽合成(见 nodefile)
84
+ BASIS_CONDITION_SYNTH = "frontmatter.condition_space(四槽合成)"
85
+ #: 旧口径残留标记:把单槽 observation_position 冒充成生效条件的留痕 basis
86
+ LEGACY_CONDITION_BASIS = "frontmatter.condition_space.observation_position"
87
+ #: 存量清洗批次的默认批次名
88
+ FIX_BATCH_DEFAULT = "cond_fix"
89
+
90
+ #: 槽名 → 人读标签(台账/报告用;与 nodefile.CONDITION_SLOTS 同源)
91
+ _SLOT_LABEL = dict(nodefile.CONDITION_SLOTS)
92
+
93
+ # 能力标签规则:cap → 关键词(小写,中文原样)。仅保留 ALL_OPS 里真实存在的 op。
94
+ _CAP_RULES_RAW = OrderedDict((
95
+ ("route", ("路由", "召回", "检索", "rank", "rrf", "route")),
96
+ ("export", ("导出", "灾备", "备份", "搬运", "export", "evidence_pack")),
97
+ ("ingest", ("摄取", "导入", "摄入", "ingest", "分派")),
98
+ ("session", ("会话", "续接", "上下文压缩", "session", "compact")),
99
+ ("maintain", ("维护", "重要性", "重算", "快照", "前馈", "模式分离", "recalc")),
100
+ ("consolidate", ("固化", "提升", "归纳", "聚类", "升格", "promote", "induce")),
101
+ ("insight", ("洞察", "情景重构", "盲区", "归因", "reconstruct", "outlook")),
102
+ ("whitebox", ("白箱", "资格判定", "裁决", "四态", "whitebox")),
103
+ ("verify", ("验证", "核验", "verdict")),
104
+ ("predict", ("预测", "趋势", "外推", "predict")),
105
+ ("causal", ("因果", "causal")),
106
+ ("metacognition", ("元认知", "metacognition")),
107
+ ("self_state", ("自我状态", "状态卡", "self_state")),
108
+ ("evolution", ("演化", "evolution", "账本")),
109
+ ("sustain", ("维生", "自愈", "心跳", "sustain")),
110
+ ("scrub", ("擦除", "去污", "scrub")),
111
+ ("protect", ("保护", "私有内容", "protect")),
112
+ ("forget", ("遗忘", "失效", "forget")),
113
+ ("link", ("蜂群", "对等", "信任", "link")),
114
+ ("identity", ("身份", "主体", "identity")),
115
+ ("goal", ("目标", "goal")),
116
+ ("recent", ("近期事件", "recent")),
117
+ ("theory", ("协议版本", "theory")),
118
+ ("consistency", ("一致性", "自洽", "consistency")),
119
+ ("ref", ("回读", "漂移", "悬空", "code_ref", "doc_ref")),
120
+ ("index_code", ("代码索引", "index_code")),
121
+ ("index_doc", ("文档索引", "章节索引", "index_doc")),
122
+ ))
123
+ # 只保留真实 op,未知 op 静默丢弃(避免注入无效能力名)
124
+ CAP_RULES = OrderedDict(
125
+ (cap, kws) for cap, kws in _CAP_RULES_RAW.items() if cap in tokens.ALL_OPS)
126
+
127
+
128
+ # ---- 通用工具 ------------------------------------------------------------
129
+
130
+ # 生效条件:x 为 str 时返回 MdCGOS(x),否则原样返回 x。
131
+ def _as_cg(x):
132
+ """接受 root 路径或已构造的 cg 实例——保持密级隔离与密钥上下文。"""
133
+ return MdCGOS(x) if isinstance(x, str) else x
134
+
135
+
136
+ # 生效条件:v 为 list/tuple 时返回分号连接的非空元素文本;v 为 None 或空串时返回 "";否则返回 str(v).strip()(v 为 0 或 False 走此支返回 "0"/"False")。
137
+ def _as_text(v) -> str:
138
+ if isinstance(v, (list, tuple)):
139
+ return ";".join(str(x).strip() for x in v if str(x).strip())
140
+ if v in (None, ""):
141
+ return ""
142
+ return str(v).strip()
143
+
144
+
145
+ # 生效条件:fm 为假值时按 {} 处理,state_attributes.comment 为 dict 时返回该 dict,否则返回 {}。
146
+ def _comment(fm: dict) -> dict:
147
+ st = (fm or {}).get("state_attributes")
148
+ c = st.get("comment") if isinstance(st, dict) else None
149
+ return c if isinstance(c, dict) else {}
150
+
151
+
152
+ # 生效条件:fm 的 state_attributes 为 dict 且其下 comment 为 dict 时原样返回该 comment;否则创建并返回空 comment dict(state_attributes 非 dict 时置 fm["state_attributes"]={},comment 非 dict 时置 st["comment"]={})。
153
+ def _ensure_comment(fm: dict) -> dict:
154
+ st = fm.get("state_attributes")
155
+ if not isinstance(st, dict):
156
+ st = {}
157
+ fm["state_attributes"] = st
158
+ c = st.get("comment")
159
+ if not isinstance(c, dict):
160
+ c = {}
161
+ st["comment"] = c
162
+ return c
163
+
164
+
165
+ # 生效条件:e 的 id 为真值时返回 id,否则返回 e 的 path 基名去掉最后 3 个字符(path 为假值时基名为空,结果空串)。
166
+ def _node_id(e: dict) -> str:
167
+ return e.get("id") or os.path.basename(e.get("path") or "")[:-3]
168
+
169
+
170
+ # 生效条件:fm 的 condition_space 四槽齐全时返回 nodefile.condition_space_text 合成文本,否则返回空串。
171
+ def _condition_text(fm: dict) -> str:
172
+ """→ 条件空间四槽合成的生效条件声明;四槽不齐 → ""(不冒充)。
173
+
174
+ **唯一**的 condition_space → 生效条件 路径。旧版在此回退到
175
+ `observation_position` **单槽**加「观测位置:」前缀——那正是
176
+ 「观测位置 ≠ 生效条件」的污染源(真实库 109 条),已删除。
177
+ """
178
+ return nodefile.condition_space_text((fm or {}).get("condition_space"))
179
+
180
+
181
+ # 生效条件:s 为真值时返回其 sha1 前 12 位,s 为假值(None/空串等)时对空串取 sha1 前 12 位。
182
+ def _sha(s: str) -> str:
183
+ return hashlib.sha1((s or "").encode("utf-8")).hexdigest()[:12]
184
+
185
+
186
+ # 生效条件:batch 与 nid 经 f-string 拼接后取 sha1 前 12 位;两者为 None 会字符串化为 "None"。
187
+ def _entry_id(batch: str, nid: str) -> str:
188
+ return hashlib.sha1(f"{batch}|{nid}".encode("utf-8")).hexdigest()[:12]
189
+
190
+
191
+ # 生效条件:content 中 strip 后以 # 开头且去掉 # 与空白后、全角或半角冒号前首段等于 field 的整行被删除,其余行保留并 join。
192
+ def _remove_ccg_line(content: str, field: str) -> str:
193
+ """删掉 `# <字段>:…` 整行(回滚用)。"""
194
+ keep = []
195
+ for ln in (content or "").split("\n"):
196
+ s = ln.strip().lstrip("#").strip()
197
+ name = s.split(":")[0].split(":")[0].strip()
198
+ if ln.strip().startswith("#") and name == field:
199
+ continue
200
+ keep.append(ln)
201
+ return "\n".join(keep)
202
+
203
+
204
+ # 生效条件:cg.root 与模块级常量 BACKFILL_LOG 拼接为返回路径。
205
+ def _log_path(cg) -> str:
206
+ return os.path.join(cg.root, BACKFILL_LOG)
207
+
208
+
209
+ # ---- 字段推导(唯一入口:只搬运已声明的证据) -----------------------------
210
+
211
+ # 生效条件:fm 与 content 给定时,仅对 content 中尚无对应 CCG 行的字段(功能名/生效条件/子功能/执行/验证方式/不适用条件)从 fm 的已有声明(state_attributes.comment、frontmatter、verification_basis、或形参 basis_text)取值,值非空且非占位文本才写入 out,假值不写、占位文本只把字段名追加进 placeholder_out(未传该形参时用临时列表)。
212
+ def derive_fields(fm: dict, content: str, basis_text: str = None,
213
+ placeholder_out: list = None) -> dict:
214
+ """按**已有声明**推导可回填字段 → `{field: (value, basis)}`。
215
+
216
+ 无来源的字段不出现在结果里(不猜测)。
217
+ **占位标记(`骨架锚点`/`内容待填充`)同样不出现**——它不是已声明的事实;
218
+ 被丢弃的字段名记入 `placeholder_out`(可选出参),供报表区分
219
+ 「无来源」与「待填充」两种缺口。
220
+ """
221
+ c = _comment(fm)
222
+ st = fm.get("state_attributes")
223
+ st = st if isinstance(st, dict) else {}
224
+ ph = placeholder_out if placeholder_out is not None else []
225
+ out = {}
226
+
227
+ # 生效条件:field 与 basis 在 value 为真值且 nodefile.is_placeholder_text(value) 为假时写入 out;value 为假值不写入;value 为占位文本时仅把 field 记入 ph。
228
+ def _put(field, value, basis):
229
+ """有值且非占位标记才写出;占位值只记名,绝不渲染成事实。"""
230
+ if not value:
231
+ return
232
+ if nodefile.is_placeholder_text(value):
233
+ ph.append(field)
234
+ return
235
+ out[field] = (value, basis)
236
+
237
+ if not _has_ccg_line(content, "功能名"):
238
+ # 优先 state_attributes.name(迁移入库写入的规范功能名),回退 frontmatter.title
239
+ raw_name = st.get("name")
240
+ v = _as_text(raw_name) if isinstance(raw_name, (str, list, tuple)) else ""
241
+ src = "state_attributes.name"
242
+ if not v:
243
+ v, src = _as_text(fm.get("title")), "frontmatter.title"
244
+ _put("功能名", v, src)
245
+
246
+ if not _has_ccg_line(content, "生效条件"):
247
+ v = _as_text(c.get("生效条件") or c.get("适用条件"))
248
+ src = "state_attributes.comment.生效条件"
249
+ if not v:
250
+ # 结构化来源:整条条件空间声明的合成,**不是** observation_position 单槽
251
+ v, src = _condition_text(fm), BASIS_CONDITION_SYNTH
252
+ _put("生效条件", v, src)
253
+
254
+ if not _has_ccg_line(content, "子功能"):
255
+ _put("子功能", _as_text(c.get("子功能") or c.get("子内容")),
256
+ "state_attributes.comment.子功能")
257
+
258
+ if not _has_ccg_line(content, "执行"):
259
+ _put("执行", _as_text(c.get("执行") or c.get("执行方式")),
260
+ "state_attributes.comment.执行")
261
+
262
+ if not _has_ccg_line(content, "验证方式"):
263
+ v = _as_text(c.get("验证方式"))
264
+ src = "state_attributes.comment.验证方式"
265
+ if not v and nodefile.verification_basis_valid(fm):
266
+ vb = fm.get("verification_basis")
267
+ v, src = BASIS_TEXT.get(vb, ""), f"frontmatter.verification_basis={vb}"
268
+ if not v and basis_text:
269
+ v, src = basis_text, "declared.basis_text"
270
+ _put("验证方式", v, src)
271
+
272
+ if not _has_ccg_line(content, "不适用条件"):
273
+ _put("不适用条件",
274
+ _as_text(c.get("不适用条件") or fm.get("non_applicable_conditions")),
275
+ "state_attributes.comment/frontmatter.non_applicable_conditions")
276
+
277
+ return out
278
+
279
+
280
+ # ---- 节点筛选 ------------------------------------------------------------
281
+
282
+ # 生效条件:cg 无 _readable 可调用时返回 True;有可调用时返回 bool(fn(e)),fn(e) 抛异常时返回 False。
283
+ def _readable_guard(cg, e) -> bool:
284
+ fn = getattr(cg, "_readable", None)
285
+ if not callable(fn):
286
+ return True
287
+ try:
288
+ return bool(fn(e))
289
+ except Exception: # noqa: BLE001
290
+ return False
291
+
292
+
293
+ # 生效条件:仅当 cg 对 e 读出的 fm 非 None、content 未被 crypto.is_encrypted、e['layer'] 不在 SKIP_LAYERS 且 fm.get('tags') 无命中 SKIP_TAGS、ccg_completeness(content)['complete'] 为假时才继续——derive_fields(受 basis_text 影响)过滤掉 content 已有 CCG 行的可写字段为空时按 placeholder_out 是否非空返回 ('placeholder'/'unfillable', None),非空时返回 ('', item)(item 的 id 取 nid、class 依 undeducible 是否为空取 'backfillable' 或 'partial');上述四个前置不满足时依次返回 ('unreadable'/'locked'/'derived'/'present', None)。
294
+ def _classify(cg, e, nid, basis_text=None):
295
+ """→ (skip_reason, item);skip_reason 非空表示不参与回填。"""
296
+ fm, content = cg._read(e)
297
+ if fm is None:
298
+ return "unreadable", None
299
+ if crypto.is_encrypted(content):
300
+ return "locked", None
301
+ tags = fm.get("tags") or []
302
+ if e.get("layer") in SKIP_LAYERS or any(t in SKIP_TAGS for t in tags):
303
+ return "derived", None
304
+ cpl = nodefile.ccg_completeness(content)
305
+ if cpl["complete"]:
306
+ return "present", None
307
+ ph_fields = []
308
+ der = derive_fields(fm, content, basis_text=basis_text,
309
+ placeholder_out=ph_fields)
310
+ fill = {f: der[f] for f in der if not _has_ccg_line(content, f)}
311
+ required_missing = [f for f in nodefile.CCG_REQUIRED
312
+ if not _has_ccg_line(content, f)]
313
+ undeducible = [f for f in required_missing if f not in der]
314
+ if not fill:
315
+ # 无可写字段:区分「无来源」(unfillable)与「字段值全是待填充标记」
316
+ # (placeholder)——后者是空壳节点,须转待填充工单,而非静默计入无来源。
317
+ return ("placeholder" if ph_fields else "unfillable"), None
318
+ cls = "backfillable" if not undeducible else "partial"
319
+ # 待补台账:生效条件既不在正文、也推不出(四槽不全)→ 记缺失槽名。
320
+ # 这不是「无来源」而是**缺证据**:补不动者按裁定判 BLINDSPOT,绝不静默 ACCEPT。
321
+ cond_pending = []
322
+ if not _has_ccg_line(content, "生效条件") and "生效条件" not in der:
323
+ cond_pending = nodefile.condition_space_missing(fm.get("condition_space"))
324
+ return "", {
325
+ "id": nid, "layer": e.get("layer"), "class": cls,
326
+ "fill": {f: {"value": v, "basis": b} for f, (v, b) in fill.items()},
327
+ "missing_undeducible": undeducible,
328
+ "placeholder_fields": ph_fields,
329
+ "conditions_pending": cond_pending,
330
+ "before_ratio": cpl["ratio"],
331
+ }
332
+
333
+
334
+ # 生效条件:x 经 _as_cg 后遍历 cg.index.nodes,按 ids/layer/prefix 过滤,内部层/不可读/locked/derived/present/unfillable/placeholder/partial 且 include_partial 为 False 分别计数跳过,其余标记 entry_id 并计入 targeted,items 受 limit 限制(limit 为 None 或 len(items) < limit 时追加),返回 dry_run rep。
335
+ def plan(x, layer=None, limit=None, ids=None, include_partial=False,
336
+ basis_text=None, prefix=None) -> dict:
337
+ """预演:产出可回填清单,不写盘。
338
+
339
+ `prefix`:按 id 前缀收窄范围(真实库用 `kp_`,与核对工单同一边界);
340
+ 内部 `anchor`/`self` 层一律出局(计入 `skipped_internal`)。
341
+ """
342
+ cg = _as_cg(x)
343
+ rep = {"root": cg.root, "dry_run": True, "action": "backfill",
344
+ "prefix": prefix, "nodes_scanned": 0, "skipped_locked": 0,
345
+ "skipped_derived": 0, "skipped_internal": 0,
346
+ "skipped_present": 0, "skipped_denied": 0, "unfillable": 0,
347
+ "skipped_placeholder": 0, "nodes_with_placeholder": 0,
348
+ "skipped_partial": 0, "targeted": 0,
349
+ # 待写节点中生效条件仍缺声明的数量(全库台账见 conditions_pending)
350
+ "targeted_missing_conditions": 0, "items": []}
351
+ want = set(ids) if ids else None
352
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
353
+ if want is not None and nid not in want:
354
+ continue
355
+ if prefix and not str(nid).startswith(prefix):
356
+ continue
357
+ if layer and e.get("layer") != layer:
358
+ continue
359
+ if e.get("layer") in INTERNAL_LAYERS:
360
+ rep["skipped_internal"] += 1
361
+ continue
362
+ if not _readable_guard(cg, e):
363
+ rep["skipped_denied"] += 1
364
+ continue
365
+ rep["nodes_scanned"] += 1
366
+ reason, item = _classify(cg, e, nid, basis_text=basis_text)
367
+ if reason == "locked":
368
+ rep["skipped_locked"] += 1
369
+ continue
370
+ if reason == "derived":
371
+ rep["skipped_derived"] += 1
372
+ continue
373
+ if reason == "present":
374
+ rep["skipped_present"] += 1
375
+ continue
376
+ if reason == "unfillable":
377
+ rep["unfillable"] += 1
378
+ continue
379
+ if reason == "placeholder":
380
+ rep["skipped_placeholder"] += 1
381
+ continue
382
+ if item.get("placeholder_fields"):
383
+ rep["nodes_with_placeholder"] += 1
384
+ if item.get("conditions_pending"):
385
+ rep["targeted_missing_conditions"] += 1
386
+ if item["class"] == "partial" and not include_partial:
387
+ rep["skipped_partial"] += 1
388
+ continue
389
+ item["entry_id"] = _entry_id("backfill", nid)
390
+ rep["targeted"] += 1
391
+ if limit is None or len(rep["items"]) < limit:
392
+ rep["items"].append(item)
393
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
394
+ return rep
395
+
396
+
397
+ # ---- 回填写入 / 回滚 / 留痕 ----------------------------------------------
398
+
399
+ # 生效条件:x 可被 _as_cg 解释为 CG 句柄时,按 layer/ids/prefix/entry_ids 选定条目做回填并返回含 written 等计数的 rep,其中 limit 非 None 时把写入数截断到该值。
400
+ def apply(x, ids=None, entry_ids=None, layer=None, limit=None,
401
+ batch=BATCH_DEFAULT, include_partial=False, basis_text=None,
402
+ actor=None, prefix=None) -> dict:
403
+ """执行回填:逐节点改写 md,写 `_backfill.jsonl` 留痕。"""
404
+ cg = _as_cg(x)
405
+ batch = batch or BATCH_DEFAULT
406
+ p = plan(cg, layer=layer, limit=None, ids=ids, prefix=prefix,
407
+ include_partial=include_partial, basis_text=basis_text)
408
+ items = p["items"]
409
+ if entry_ids:
410
+ want = set(entry_ids)
411
+ items = [i for i in items if i["entry_id"] in want]
412
+ rep = {"root": cg.root, "dry_run": False, "action": "backfill",
413
+ "batch": batch, "actor": actor, "planned": len(items),
414
+ "written": 0, "skipped_drift": 0, "skipped_locked": 0,
415
+ "entry_ids": [], "by_field": {}}
416
+ for it in items:
417
+ if limit is not None and rep["written"] >= limit:
418
+ break
419
+ nid = it["id"]
420
+ e = cg.index["nodes"].get(nid)
421
+ if not e:
422
+ rep["skipped_drift"] += 1
423
+ continue
424
+ fm, content = cg._read(e)
425
+ if fm is None or crypto.is_encrypted(content):
426
+ rep["skipped_locked"] += 1
427
+ continue
428
+ # 预演到执行之间节点可能被改动:只写仍缺失的字段
429
+ todo = {f: d for f, d in it["fill"].items()
430
+ if not _has_ccg_line(content, f)}
431
+ if not todo:
432
+ rep["skipped_drift"] += 1
433
+ continue
434
+ comment0 = _comment(fm)
435
+ # 记录改写前的真相:rollback 必须「还原」而非「删除」——否则 comment
436
+ # 里原有的声明会被误删,导致回填不可重复(回滚不是真逆操作)。
437
+ prev_c = {f: comment0[f] for f in todo if f in comment0}
438
+ fm_before = {}
439
+ if "不适用条件" in todo:
440
+ fm_before["non_applicable_conditions"] = {
441
+ "had": "non_applicable_conditions" in fm,
442
+ "value": fm.get("non_applicable_conditions")}
443
+ if "验证方式" in todo and not nodefile.verification_basis_valid(fm):
444
+ fm_before["verification_basis"] = {
445
+ "had": "verification_basis" in fm,
446
+ "value": fm.get("verification_basis")}
447
+ comment = _ensure_comment(fm)
448
+ wid = _sha(f"{nid}|{batch}|{time.time()}")
449
+ applied = {}
450
+ for f, d in todo.items():
451
+ content = _upsert_ccg_line(content, f, d["value"])
452
+ comment[f] = d["value"]
453
+ if f == "不适用条件":
454
+ fm["non_applicable_conditions"] = [
455
+ s.strip() for s in d["value"].split(";") if s.strip()]
456
+ if f == "验证方式" and not nodefile.verification_basis_valid(fm):
457
+ fm["verification_basis"] = BASIS_ENUM_DEFAULT
458
+ applied[f] = {"after": d["value"], "basis": d["basis"],
459
+ "before": prev_c.get(f)}
460
+ rep["by_field"][f] = rep["by_field"].get(f, 0) + 1
461
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
462
+ durable=True)
463
+ append_jsonl(_log_path(cg), {
464
+ "action": "backfill", "ts": time.time(), "batch": batch,
465
+ "actor": actor, "entry_id": it["entry_id"], "write_id": wid,
466
+ "node": nid, "layer": e.get("layer"), "fields": applied,
467
+ "fm_before": fm_before,
468
+ "content_hash_after": _sha(content)})
469
+ rep["written"] += 1
470
+ rep["entry_ids"].append(it["entry_id"])
471
+ if rep["written"]:
472
+ cg.rebuild_index()
473
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
474
+ return rep
475
+
476
+
477
+ # 生效条件:x 经 _as_cg 定位后读留痕日志,仅对 action=='backfill' 且(batch 为假值则不过滤 batch,否则 rec['batch']==batch)、(entry_ids 为假值则不过滤,否则 rec['entry_id'] 属于该集合)、write_id 未出现在已完成 rollback 集合中、节点命中 cg.index['nodes'] 且 cg._read(e) 的 fm 可读、字段当前 _ccg_field(content, f) 等于留痕 after 的记录执行撤销写回(before 为 None 则删该 comment 键,否则还原原值),无字段可撤销只计 conflict 不写盘,reverted 非空时 rebuild_index,结果汇总进返回的 rep。
478
+ def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
479
+ """按留痕反向应用:撤销本批次回填(当前值 ≠ 写入值时跳过,防覆盖)。"""
480
+ cg = _as_cg(x)
481
+ want = set(entry_ids) if entry_ids else None
482
+ rep = {"root": cg.root, "action": "backfill_rollback", "actor": actor,
483
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
484
+ "fields_reverted": 0, "skipped_done": 0}
485
+ log = list(read_jsonl(_log_path(cg)) or [])
486
+ done = {r.get("write_id") for r in log
487
+ if r.get("action") == "backfill_rollback" and r.get("write_id")}
488
+ for rec in log:
489
+ if rec.get("action") != "backfill":
490
+ continue
491
+ if rec.get("write_id") and rec.get("write_id") in done:
492
+ rep["skipped_done"] += 1
493
+ continue
494
+ if batch and rec.get("batch") != batch:
495
+ continue
496
+ if want is not None and rec.get("entry_id") not in want:
497
+ continue
498
+ nid = rec.get("node")
499
+ e = cg.index["nodes"].get(nid)
500
+ if not e:
501
+ rep["missing"] += 1
502
+ continue
503
+ fm, content = cg._read(e)
504
+ if fm is None:
505
+ rep["missing"] += 1
506
+ continue
507
+ comment = _comment(fm)
508
+ reverted, conflicted = {}, []
509
+ for f, d in (rec.get("fields") or {}).items():
510
+ if _ccg_field(content, f) != d.get("after"):
511
+ conflicted.append(f) # 已被后续修改 → 不撤销
512
+ continue
513
+ content = _remove_ccg_line(content, f)
514
+ b = d.get("before")
515
+ if b is None:
516
+ comment.pop(f, None) # 原本就没有 → 删回「无」
517
+ else:
518
+ comment[f] = b # 原本有 → 还原原值(非删除)
519
+ reverted[f] = d.get("after")
520
+ if not reverted:
521
+ rep["conflict"] += 1
522
+ continue
523
+ for k, box in (rec.get("fm_before") or {}).items():
524
+ if box.get("had"):
525
+ fm[k] = box.get("value")
526
+ else:
527
+ fm.pop(k, None)
528
+ st = fm.get("state_attributes")
529
+ if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
530
+ and not st["comment"]:
531
+ st.pop("comment", None)
532
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
533
+ durable=True)
534
+ append_jsonl(_log_path(cg), {
535
+ "action": "backfill_rollback", "ts": time.time(), "actor": actor,
536
+ "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
537
+ "write_id": rec.get("write_id"), "node": nid,
538
+ "reverted": list(reverted), "conflict": conflicted})
539
+ rep["reverted"] += 1
540
+ rep["fields_reverted"] += len(reverted)
541
+ if conflicted:
542
+ rep["conflict"] += 1
543
+ if rep["reverted"]:
544
+ cg.rebuild_index()
545
+ return rep
546
+
547
+
548
+ # 生效条件:x 经 _as_cg,action/batch 为真值时过滤对应日志记录;limit 不为 None 且 >=0 时按 recs[-limit:] 截断(limit=0 时切片为全部记录),limit 为 None 或负数时不截断;返回 total/returned/records。
549
+ def history(x, limit=100, action=None, batch=None) -> dict:
550
+ cg = _as_cg(x)
551
+ recs = []
552
+ for rec in read_jsonl(_log_path(cg)) or []:
553
+ if action and rec.get("action") != action:
554
+ continue
555
+ if batch and rec.get("batch") != batch:
556
+ continue
557
+ recs.append(rec)
558
+ total = len(recs)
559
+ if limit is not None and limit >= 0:
560
+ recs = recs[-limit:]
561
+ return {"root": cg.root, "total": total, "returned": len(recs),
562
+ "records": recs}
563
+
564
+
565
+ # ---- 能力标签注入(关键词启发式) ----------------------------------------
566
+
567
+ # 候选匹配不得吃进两类「自产词」,否则候选再生、plan 永不收敛:
568
+ # ① `fm.tags`:`cap:<op>` 是**注入结果**,回流后 `cap:route` 自匹配关键词
569
+ # "route"、`cap:self_state` 自匹配 "self_state"…… 已注入节点会重新成为候选;
570
+ # ② `# 验证方式:` 模板行:它是 CCG 五要素的必备行,展开后**几乎全库**命中
571
+ # 「验证」,`cap:verify` 遂从能力判断退化为正文模板的副产品
572
+ # (evolution 账本实测:预演命中 1630/1813)。
573
+ _CAP_TEMPLATE_LINES = ("验证方式",)
574
+
575
+
576
+ # 生效条件:content 中去除 # 与空白后、全角或半角冒号前首段命中 _CAP_TEMPLATE_LINES 的行被剔除,其余行保留并 join。
577
+ def _cap_body(content: str) -> str:
578
+ """正文(供关键词匹配)——剔除 CCG 模板行,防模板词污染候选。"""
579
+ keep = []
580
+ for line in (content or "").splitlines():
581
+ head = line.strip().lstrip("#").strip()
582
+ head = head.split(":", 1)[0].split(":", 1)[0].strip()
583
+ if head in _CAP_TEMPLATE_LINES:
584
+ continue
585
+ keep.append(line)
586
+ return "\n".join(keep)
587
+
588
+
589
+ # 生效条件:当 fm 为可 get 的 frontmatter、e 为带 id 的节点条目、content 为正文文本时,返回 title+e.id+功能名/生效条件/子功能字段+正文前 300 字合并后的小写串(不含 fm.tags)。
590
+ def _cap_text(e, fm, content) -> str:
591
+ """候选匹配文本:节点标识 + CCG 字段 + 正文(**不含 `fm.tags`**)。"""
592
+ parts = [_as_text(fm.get("title")), e.get("id") or ""]
593
+ for f in ("功能名", "生效条件", "子功能"):
594
+ v = _ccg_field(content, f)
595
+ if v:
596
+ parts.append(v)
597
+ parts.append(_cap_body(content)[:300])
598
+ return " ".join(parts).lower()
599
+
600
+
601
+ # 生效条件:text 包含 CAP_RULES 中某 cap 的至少一个关键词时,该 cap 以匹配关键词与置信度加入返回,按置信度降序、cap 升序排序;无匹配返回空 hits。
602
+ def cap_matches(text: str) -> list:
603
+ hits = []
604
+ for cap, kws in CAP_RULES.items():
605
+ m = [k for k in kws if k in text]
606
+ if m:
607
+ hits.append({"cap": cap, "matched_by": m,
608
+ "confidence": round(min(1.0, 0.4 + 0.2 * len(m)), 3)})
609
+ hits.sort(key=lambda h: (-h["confidence"], h["cap"]))
610
+ return hits
611
+
612
+
613
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 ids/layer 过滤,不可读计 denied,读取失败或加密计 locked,扫描后以 _cap_text 匹配并过滤 confidence >= min_conf 且标签未含 cap:,无命中计 present,有命中则 targeted 并受 limit 限制加入 items,返回 dry_run rep。
614
+ def cap_plan(x, layer=None, limit=None, ids=None, min_conf=0.5) -> dict:
615
+ """预演:按关键词启发式给出 `cap:<op>` 标签建议(含依据与置信度),不写盘。"""
616
+ cg = _as_cg(x)
617
+ rep = {"root": cg.root, "dry_run": True, "action": "cap",
618
+ "min_conf": min_conf, "nodes_scanned": 0, "skipped_locked": 0,
619
+ "skipped_present": 0, "skipped_denied": 0, "targeted": 0,
620
+ "items": []}
621
+ want = set(ids) if ids else None
622
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
623
+ if want is not None and nid not in want:
624
+ continue
625
+ if layer and e.get("layer") != layer:
626
+ continue
627
+ if not _readable_guard(cg, e):
628
+ rep["skipped_denied"] += 1
629
+ continue
630
+ fm, content = cg._read(e)
631
+ if fm is None or crypto.is_encrypted(content):
632
+ rep["skipped_locked"] += 1
633
+ continue
634
+ rep["nodes_scanned"] += 1
635
+ have = set(t for t in (fm.get("tags") or []) if isinstance(t, str))
636
+ hits = [h for h in cap_matches(_cap_text(e, fm, content))
637
+ if h["confidence"] >= min_conf
638
+ and f"cap:{h['cap']}" not in have]
639
+ if not hits:
640
+ rep["skipped_present"] += 1
641
+ continue
642
+ item = {"id": nid, "layer": e.get("layer"), "caps": hits,
643
+ "entry_id": _entry_id("cap", nid)}
644
+ rep["targeted"] += 1
645
+ if limit is None or len(rep["items"]) < limit:
646
+ rep["items"].append(item)
647
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
648
+ return rep
649
+
650
+
651
+ # 生效条件:x 经 _as_cg,batch 假值回落 BATCH_DEFAULT;先 cap_plan 得 items,按 entry_ids 过滤;逐个检查节点存在、读取可读、无新增标签分别计 drift/locked/drift,成功写 tags 并记日志,limit 非 None 且 written >= limit 时 break;返回 rep。
652
+ def cap_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
653
+ batch=BATCH_DEFAULT, min_conf=0.5, actor=None) -> dict:
654
+ """注入 `cap:<op>` 标签:只改 frontmatter.tags,不动正文。"""
655
+ cg = _as_cg(x)
656
+ batch = batch or BATCH_DEFAULT
657
+ p = cap_plan(cg, layer=layer, limit=None, ids=ids, min_conf=min_conf)
658
+ items = p["items"]
659
+ if entry_ids:
660
+ want = set(entry_ids)
661
+ items = [i for i in items if i["entry_id"] in want]
662
+ rep = {"root": cg.root, "dry_run": False, "action": "cap", "batch": batch,
663
+ "actor": actor, "min_conf": min_conf, "planned": len(items),
664
+ "written": 0, "skipped_locked": 0, "skipped_drift": 0,
665
+ "tags_added": 0, "entry_ids": []}
666
+ for it in items:
667
+ if limit is not None and rep["written"] >= limit:
668
+ break
669
+ nid = it["id"]
670
+ e = cg.index["nodes"].get(nid)
671
+ if not e:
672
+ rep["skipped_drift"] += 1
673
+ continue
674
+ fm, content = cg._read(e)
675
+ if fm is None or crypto.is_encrypted(content):
676
+ rep["skipped_locked"] += 1
677
+ continue
678
+ tags = list(fm.get("tags") or [])
679
+ added = [f"cap:{h['cap']}" for h in it["caps"] if f"cap:{h['cap']}" not in tags]
680
+ if not added:
681
+ rep["skipped_drift"] += 1
682
+ continue
683
+ fm["tags"] = tags + added
684
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
685
+ durable=True)
686
+ append_jsonl(_log_path(cg), {
687
+ "action": "cap", "ts": time.time(), "batch": batch, "actor": actor,
688
+ "entry_id": it["entry_id"], "write_id": _sha(f"cap|{nid}|{time.time()}"),
689
+ "node": nid, "layer": e.get("layer"),
690
+ "tags_added": added,
691
+ "evidence": {h["cap"]: h["matched_by"] for h in it["caps"]},
692
+ "confidence": {h["cap"]: h["confidence"] for h in it["caps"]}})
693
+ rep["written"] += 1
694
+ rep["tags_added"] += len(added)
695
+ rep["entry_ids"].append(it["entry_id"])
696
+ if rep["written"]:
697
+ cg.rebuild_index()
698
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
699
+ return rep
700
+
701
+
702
+ # 生效条件:x 经 _as_cg,读取日志中 action=cap 且未回滚的记录,按 batch/entry_ids 过滤,节点存在且原加入标签仍在 tags 中时移除,否则计 conflict/missing,返回 rep。
703
+ def cap_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
704
+ """撤销 cap 注入:仅移除**仍在 tags 里**的 `cap:` 标签(防覆盖后续修改)。"""
705
+ cg = _as_cg(x)
706
+ want = set(entry_ids) if entry_ids else None
707
+ rep = {"root": cg.root, "action": "cap_rollback", "actor": actor,
708
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
709
+ "tags_removed": 0, "skipped_done": 0}
710
+ log = list(read_jsonl(_log_path(cg)) or [])
711
+ done = {r.get("write_id") for r in log
712
+ if r.get("action") == "cap_rollback" and r.get("write_id")}
713
+ for rec in log:
714
+ if rec.get("action") != "cap":
715
+ continue
716
+ if rec.get("write_id") and rec.get("write_id") in done:
717
+ rep["skipped_done"] += 1
718
+ continue
719
+ if batch and rec.get("batch") != batch:
720
+ continue
721
+ if want is not None and rec.get("entry_id") not in want:
722
+ continue
723
+ nid, added = rec.get("node"), rec.get("tags_added") or []
724
+ e = cg.index["nodes"].get(nid)
725
+ if not e:
726
+ rep["missing"] += 1
727
+ continue
728
+ fm, content = cg._read(e)
729
+ if fm is None:
730
+ rep["missing"] += 1
731
+ continue
732
+ tags = list(fm.get("tags") or [])
733
+ removed = [t for t in added if t in tags]
734
+ if not removed:
735
+ rep["conflict"] += 1
736
+ continue
737
+ fm["tags"] = [t for t in tags if t not in set(removed)]
738
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
739
+ durable=True)
740
+ append_jsonl(_log_path(cg), {
741
+ "action": "cap_rollback", "ts": time.time(), "actor": actor,
742
+ "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
743
+ "write_id": rec.get("write_id"), "node": nid,
744
+ "tags_removed": removed})
745
+ rep["reverted"] += 1
746
+ rep["tags_removed"] += len(removed)
747
+ if rep["reverted"]:
748
+ cg.rebuild_index()
749
+ return rep
750
+
751
+
752
+ # ---- 豁免解除(ccg_exempt 分批摘除) --------------------------------------
753
+
754
+ EXEMPT_FLAG = "ccg_exempt"
755
+
756
+
757
+ # 生效条件:fm 的 verification_basis 合法且 content 的 ccg_completeness 标记 complete 时返回 (True, []),否则把缺失项放入 miss 返回 ready=False。
758
+ def _exempt_ready(fm: dict, content: str):
759
+ """摘豁免前置条件 → `(ready, missing)`。
760
+
761
+ 硬约束(顺序依赖):证据未补齐就摘豁免,节点会从 DEFER **退化为 BLINDSPOT**,
762
+ 比现状更差。故要求「合法 `verification_basis`」+「5 要素齐备(含验证方式行)」
763
+ 同时成立才允许摘除。
764
+ """
765
+ miss = []
766
+ if not nodefile.verification_basis_valid(fm):
767
+ miss.append("verification_basis")
768
+ if not nodefile.ccg_completeness(content).get("complete"):
769
+ miss.append("ccg5")
770
+ return (not miss), miss
771
+
772
+
773
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 ids/prefix/layer 过滤,内部层/不可读/读取失败或加密分别计数跳过,fm 无 EXEMPT_FLAG 计 not_exempt,require_ready 为真且不 ready 时计 unready 并在 want 为 None 时跳过,否则生成 item 受 limit 限制;sample 为真值时按 id 序等距抽取 sample 个。
774
+ def exempt_plan(x, layer=None, limit=None, ids=None, require_ready=True,
775
+ sample=0, prefix=None) -> dict:
776
+ """预演:列出可摘 `ccg_exempt` 的节点(默认要求证据就绪),不写盘。
777
+
778
+ `sample=N` 时按 id 序等距抽取 N 个作为验收样本(确定性,重跑同一样本)。
779
+ `prefix`:按 id 前缀收窄范围(真实库用 `kp_`);内部 `anchor`/`self` 层出局。
780
+ """
781
+ cg = _as_cg(x)
782
+ rep = {"root": cg.root, "dry_run": True, "action": "exempt",
783
+ "require_ready": require_ready, "prefix": prefix, "nodes_scanned": 0,
784
+ "skipped_locked": 0, "skipped_denied": 0, "skipped_not_exempt": 0,
785
+ "skipped_internal": 0, "skipped_unready": 0, "unready_reasons": {},
786
+ "targeted": 0, "items": [], "sample": []}
787
+ want = set(ids) if ids else None
788
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
789
+ if want is not None and nid not in want:
790
+ continue
791
+ if prefix and not str(nid).startswith(prefix):
792
+ continue
793
+ if layer and e.get("layer") != layer:
794
+ continue
795
+ if e.get("layer") in INTERNAL_LAYERS:
796
+ rep["skipped_internal"] += 1
797
+ continue
798
+ if not _readable_guard(cg, e):
799
+ rep["skipped_denied"] += 1
800
+ continue
801
+ fm, content = cg._read(e)
802
+ if fm is None or crypto.is_encrypted(content):
803
+ rep["skipped_locked"] += 1
804
+ continue
805
+ rep["nodes_scanned"] += 1
806
+ if not fm.get(EXEMPT_FLAG):
807
+ rep["skipped_not_exempt"] += 1
808
+ continue
809
+ ready, miss = _exempt_ready(fm, content)
810
+ if require_ready and not ready:
811
+ rep["skipped_unready"] += 1
812
+ for m in miss:
813
+ rep["unready_reasons"][m] = rep["unready_reasons"].get(m, 0) + 1
814
+ # 批量:直接过滤(报表已计数);显式点名:入列交由 apply 判定并留
815
+ # `exempt_skip` 记录——被点名的节点绝不静默丢弃。
816
+ if want is None:
817
+ continue
818
+ item = {"id": nid, "layer": e.get("layer"), "ready": ready,
819
+ "missing": miss, "basis": fm.get("verification_basis"),
820
+ "entry_id": _entry_id("exempt", nid)}
821
+ rep["targeted"] += 1
822
+ if limit is None or len(rep["items"]) < limit:
823
+ rep["items"].append(item)
824
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
825
+ if sample and rep["items"]:
826
+ ids_all = [i["id"] for i in rep["items"]]
827
+ k = min(int(sample), len(ids_all))
828
+ stride = max(1, len(ids_all) // k)
829
+ rep["sample"] = ids_all[::stride][:k]
830
+ return rep
831
+
832
+
833
+ # 生效条件:x 经 _as_cg,batch 假值回落 BATCH_DEFAULT;先 exempt_plan 得 items,按 entry_ids 过滤;逐个检查节点存在、可读、EXEMPT_FLAG 仍真、require_ready 为真时 _exempt_ready 再次通过;不通过计 skipped_unready 并记 exempt_skip;通过则置 EXEMPT_FLAG=False 写盘记日志;limit 限制 written;返回 rep。
834
+ def exempt_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
835
+ batch=BATCH_DEFAULT, require_ready=True, actor=None,
836
+ prefix=None) -> dict:
837
+ """分批摘除 `ccg_exempt`(置 False 而非删键,便于反向还原)。
838
+
839
+ 就绪校验在**写入前**再查一次(plan 与 apply 之间可能被改动);
840
+ 不满足则计 `skipped_unready` 并留 `exempt_skip` 记录,绝不硬摘。
841
+ """
842
+ cg = _as_cg(x)
843
+ batch = batch or BATCH_DEFAULT
844
+ p = exempt_plan(cg, layer=layer, ids=ids, require_ready=require_ready,
845
+ prefix=prefix)
846
+ items = p["items"]
847
+ if entry_ids:
848
+ want = set(entry_ids)
849
+ items = [i for i in items if i["entry_id"] in want]
850
+ rep = {"root": cg.root, "dry_run": False, "action": "exempt", "batch": batch,
851
+ "actor": actor, "require_ready": require_ready, "planned": len(items),
852
+ "written": 0, "skipped_unready": 0, "skipped_locked": 0,
853
+ "skipped_drift": 0, "entry_ids": []}
854
+ for it in items:
855
+ if limit is not None and rep["written"] >= limit:
856
+ break
857
+ nid = it["id"]
858
+ e = cg.index["nodes"].get(nid)
859
+ if not e:
860
+ rep["skipped_drift"] += 1
861
+ continue
862
+ fm, content = cg._read(e)
863
+ if fm is None or crypto.is_encrypted(content):
864
+ rep["skipped_locked"] += 1
865
+ continue
866
+ if not fm.get(EXEMPT_FLAG):
867
+ rep["skipped_drift"] += 1
868
+ continue
869
+ ready, miss = _exempt_ready(fm, content)
870
+ if require_ready and not ready:
871
+ rep["skipped_unready"] += 1
872
+ append_jsonl(_log_path(cg), {
873
+ "action": "exempt_skip", "ts": time.time(), "batch": batch,
874
+ "actor": actor, "node": nid, "missing": miss})
875
+ continue
876
+ before = fm.get(EXEMPT_FLAG)
877
+ fm[EXEMPT_FLAG] = False
878
+ wid = _sha(f"exempt|{nid}|{time.time()}")
879
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
880
+ durable=True)
881
+ append_jsonl(_log_path(cg), {
882
+ "action": "exempt", "ts": time.time(), "batch": batch, "actor": actor,
883
+ "entry_id": it["entry_id"], "write_id": wid, "node": nid,
884
+ "layer": e.get("layer"), "exempt_before": before,
885
+ "basis": fm.get("verification_basis")})
886
+ rep["written"] += 1
887
+ rep["entry_ids"].append(it["entry_id"])
888
+ if rep["written"]:
889
+ cg.rebuild_index()
890
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
891
+ return rep
892
+
893
+
894
+ # 生效条件:当 x 可解析为 cg 时,日志中 action 为 exempt 的记录若其非空 write_id 已存在于既有 action 为 exempt_rollback 的记录 write_id 集合中,则跳过并计入 skipped_done,否则在通过 batch 与 entry_ids 过滤后,节点存在且可读、EXEMPT_FLAG 当前为假时,该记录才被还原并计入 reverted;。
895
+ def exempt_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
896
+ """反向还原 `ccg_exempt`:仅当当前仍为「已摘」状态时还原,否则计 conflict。"""
897
+ cg = _as_cg(x)
898
+ want = set(entry_ids) if entry_ids else None
899
+ rep = {"root": cg.root, "action": "exempt_rollback", "actor": actor,
900
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
901
+ "skipped_done": 0}
902
+ log = list(read_jsonl(_log_path(cg)) or [])
903
+ done = {r.get("write_id") for r in log
904
+ if r.get("action") == "exempt_rollback" and r.get("write_id")}
905
+ for rec in log:
906
+ if rec.get("action") != "exempt":
907
+ continue
908
+ if rec.get("write_id") and rec.get("write_id") in done:
909
+ rep["skipped_done"] += 1
910
+ continue
911
+ if batch and rec.get("batch") != batch:
912
+ continue
913
+ if want is not None and rec.get("entry_id") not in want:
914
+ continue
915
+ nid = rec.get("node")
916
+ e = cg.index["nodes"].get(nid)
917
+ if not e:
918
+ rep["missing"] += 1
919
+ continue
920
+ fm, content = cg._read(e)
921
+ if fm is None:
922
+ rep["missing"] += 1
923
+ continue
924
+ if fm.get(EXEMPT_FLAG):
925
+ rep["conflict"] += 1
926
+ continue
927
+ fm[EXEMPT_FLAG] = rec.get("exempt_before", True)
928
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
929
+ durable=True)
930
+ append_jsonl(_log_path(cg), {
931
+ "action": "exempt_rollback", "ts": time.time(), "actor": actor,
932
+ "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
933
+ "write_id": rec.get("write_id"), "node": nid,
934
+ "exempt_restored": fm.get(EXEMPT_FLAG)})
935
+ rep["reverted"] += 1
936
+ if rep["reverted"]:
937
+ cg.rebuild_index()
938
+ return rep
939
+
940
+
941
+ # ---- 待补台账 / 存量清洗(生效条件口径修正) ------------------------------
942
+ #
943
+ # 背景:旧口径把 `condition_space.observation_position` 单槽加前缀「观测位置:」
944
+ # 当作生效条件写入(真实库 109 条)。观测位置 ≠ 生效条件——生效条件是**整条**
945
+ # 条件空间声明的合成。本段负责三件事:
946
+ # ① conditions_pending 只读台账:未声明且四槽推不出的节点
947
+ # ② fix_conditions_* 清洗:冒充行重渲染为合规声明,或删除并登记待补
948
+ # ③ verify_conditions 只读复算:验收指标 legacy_position == 0
949
+
950
+
951
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 prefix/layer 过滤,内部层/不可读/读取失败或加密/derived 层或 SKIP_TAGS 分别计数跳过;扫描后,正文已有生效条件计 declared,derive_fields 可推出计 derivable,否则按 condition_space 缺失槽计 pending 并受 limit 限制收集 items;返回 rep。
952
+ def conditions_pending(x, prefix=None, layer=None, limit=None) -> dict:
953
+ """只读台账:列出「未声明生效条件、且四槽推不出」的节点及其缺失槽。
954
+
955
+ 生效条件已成为必填项(`CCG_REQUIRED`),但存量节点未必有足够声明可补。
956
+ 这类节点**不能静默判 ACCEPT**:进本台账等待后续补充;确实补不动者由资格
957
+ 判定落 BLINDSPOT(而不是被「常用条件默认省略」掩盖)。本函数不写盘。
958
+ """
959
+ cg = _as_cg(x)
960
+ rep = {"root": cg.root, "dry_run": True, "action": "conditions_pending",
961
+ "prefix": prefix, "nodes_scanned": 0, "skipped_locked": 0,
962
+ "skipped_denied": 0, "skipped_internal": 0, "skipped_derived": 0,
963
+ "declared": 0, "derivable": 0, "pending": 0,
964
+ "by_missing": {}, "items": []}
965
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
966
+ if prefix and not str(nid).startswith(prefix):
967
+ continue
968
+ if layer and e.get("layer") != layer:
969
+ continue
970
+ if e.get("layer") in INTERNAL_LAYERS:
971
+ rep["skipped_internal"] += 1
972
+ continue
973
+ if not _readable_guard(cg, e):
974
+ rep["skipped_denied"] += 1
975
+ continue
976
+ fm, content = cg._read(e)
977
+ if fm is None or crypto.is_encrypted(content):
978
+ rep["skipped_locked"] += 1
979
+ continue
980
+ if (e.get("layer") in SKIP_LAYERS
981
+ or any(t in SKIP_TAGS for t in (fm.get("tags") or []))):
982
+ rep["skipped_derived"] += 1
983
+ continue
984
+ rep["nodes_scanned"] += 1
985
+ if _has_ccg_line(content, "生效条件"):
986
+ rep["declared"] += 1
987
+ continue
988
+ if "生效条件" in derive_fields(fm, content):
989
+ # 有来源、只是还没接线 → 属于「待回填」,不是缺证据
990
+ rep["derivable"] += 1
991
+ continue
992
+ cs = fm.get("condition_space")
993
+ miss = nodefile.condition_space_missing(cs)
994
+ if not isinstance(cs, dict) or len(miss) == len(nodefile.CONDITION_SLOTS):
995
+ key = "无条件空间声明"
996
+ else:
997
+ key = "缺" + "+".join(_SLOT_LABEL.get(k, k) for k in miss)
998
+ rep["by_missing"][key] = rep["by_missing"].get(key, 0) + 1
999
+ rep["pending"] += 1
1000
+ if limit is None or len(rep["items"]) < limit:
1001
+ rep["items"].append({
1002
+ "id": nid, "layer": e.get("layer"), "missing_slots": miss,
1003
+ "missing_text": key,
1004
+ "fallback": "补写生效条件;确实补不动 → 判 BLINDSPOT"})
1005
+ if limit is not None:
1006
+ rep["items"] = rep["items"][:limit]
1007
+ return rep
1008
+
1009
+
1010
+ # 生效条件:cg 的留痕日志中存在 basis 为 LEGACY_CONDITION_BASIS 且未被回滚的 backfill 生效条件写入时,按 node 记录最后一次的 after/original 返回;无匹配返回空。
1011
+ def _legacy_condition_records(cg) -> dict:
1012
+ """→ {node: {after, original, …}}:我们**自己写下的**单槽冒充行(真源 = 留痕)。
1013
+
1014
+ 只有留痕里 `basis == LEGACY_CONDITION_BASIS` 的行才敢自动改——人写下的
1015
+ 「观测位置:…」声明不在其列(宁可漏改,不可误改)。已被回滚的写入剔除;
1016
+ 同一节点多次写入时以最后一次为准。
1017
+ """
1018
+ log = list(read_jsonl(_log_path(cg)) or [])
1019
+ undone = {r.get("write_id") for r in log
1020
+ if r.get("action") == "backfill_rollback" and r.get("write_id")}
1021
+ out = {}
1022
+ for rec in log:
1023
+ if rec.get("action") != "backfill":
1024
+ continue
1025
+ if rec.get("write_id") and rec.get("write_id") in undone:
1026
+ continue
1027
+ f = (rec.get("fields") or {}).get("生效条件")
1028
+ if not isinstance(f, dict) or f.get("basis") != LEGACY_CONDITION_BASIS:
1029
+ continue
1030
+ nid = rec.get("node")
1031
+ if not nid:
1032
+ continue
1033
+ out[nid] = {"after": f.get("after"), "original": f.get("before"),
1034
+ "write_id": rec.get("write_id"), "batch": rec.get("batch")}
1035
+ return out
1036
+
1037
+
1038
+ # 生效条件:x 经 _as_cg,从 _legacy_condition_records 取候选,按 prefix 过滤,节点不存在/不可读/读取失败或加密分别计 missing/denied/locked;当前生效条件等于留痕 after 时,四槽合成非空则 rewrite 否则 drop,不等则 conflict;items 受 limit 限制;返回 dry_run rep。
1039
+ def fix_conditions_plan(x, prefix=None, limit=None) -> dict:
1040
+ """预演:清洗存量单槽冒充行。不写盘。
1041
+
1042
+ 逐条判定(都要求「当前值仍等于我们当初写入的值」,否则计 `conflict`、不碰):
1043
+ · 四槽齐备 → `rewrite`:改写为 `condition_space_text(cs)` 合成声明;
1044
+ · 四槽不全 → `drop`:删掉冒充行并登记待补。补不全的行留着比删掉更危险——
1045
+ 它会被当成生效条件读,等于把坐标的一维当成整条声明。
1046
+ """
1047
+ cg = _as_cg(x)
1048
+ rep = {"root": cg.root, "dry_run": True, "action": "fix_conditions",
1049
+ "prefix": prefix, "legacy": 0, "targeted": 0, "rewrite": 0,
1050
+ "drop": 0, "conflict": 0, "missing": 0, "skipped_locked": 0,
1051
+ "skipped_denied": 0, "items": []}
1052
+ for nid, rec in _legacy_condition_records(cg).items():
1053
+ if prefix and not str(nid).startswith(prefix):
1054
+ continue
1055
+ e = cg.index["nodes"].get(nid)
1056
+ if not e:
1057
+ rep["missing"] += 1
1058
+ continue
1059
+ if not _readable_guard(cg, e):
1060
+ rep["skipped_denied"] += 1
1061
+ continue
1062
+ fm, content = cg._read(e)
1063
+ if fm is None or crypto.is_encrypted(content):
1064
+ rep["skipped_locked"] += 1
1065
+ continue
1066
+ rep["legacy"] += 1
1067
+ cur = _ccg_field(content, "生效条件") or ""
1068
+ if cur != (rec.get("after") or ""):
1069
+ rep["conflict"] += 1 # 已被后续改动 → 不碰
1070
+ continue
1071
+ new = _condition_text(fm)
1072
+ act = "rewrite" if new else "drop"
1073
+ rep["targeted"] += 1
1074
+ rep[act] += 1
1075
+ if limit is None or len(rep["items"]) < limit:
1076
+ rep["items"].append({
1077
+ "id": nid, "layer": e.get("layer"), "action": act,
1078
+ "before": cur, "after": new,
1079
+ "missing_slots": nodefile.condition_space_missing(
1080
+ fm.get("condition_space")),
1081
+ "comment_before": (_comment(fm) or {}).get("生效条件"),
1082
+ "comment_original": rec.get("original"),
1083
+ "entry_id": _entry_id(FIX_BATCH_DEFAULT, nid)})
1084
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
1085
+ return rep
1086
+
1087
+
1088
+ # 生效条件:x 经 _as_cg,batch 假值回落 FIX_BATCH_DEFAULT;先 fix_conditions_plan 得 items,按 ids/entry_ids 过滤;逐个再验当前值仍等于 before 且 comment_before 一致,否则 skipped_drift;rewrite 时 upsert 生效条件行并写 comment,drop 时删行并还原 comment;写盘记 condition_fix,drop 另记 condition_pending;limit 限制 rewritten+dropped;返回 rep。
1089
+ def fix_conditions_apply(x, ids=None, entry_ids=None, prefix=None, limit=None,
1090
+ batch=FIX_BATCH_DEFAULT, actor=None) -> dict:
1091
+ """执行清洗:`rewrite` 改写为四槽合成声明;`drop` 删行并登记待补。
1092
+
1093
+ 写盘与留痕同 `apply`:`_backfill.jsonl` 记 `condition_fix`;`drop` 另记
1094
+ `condition_pending`(待补台账的可追溯副本)。幂等:清洗后不再有候选行。
1095
+ """
1096
+ cg = _as_cg(x)
1097
+ batch = batch or FIX_BATCH_DEFAULT
1098
+ p = fix_conditions_plan(cg, prefix=prefix, limit=None)
1099
+ items = p["items"]
1100
+ if ids:
1101
+ want = set(ids)
1102
+ items = [i for i in items if i["id"] in want]
1103
+ if entry_ids:
1104
+ want = set(entry_ids)
1105
+ items = [i for i in items if i["entry_id"] in want]
1106
+ rep = {"root": cg.root, "dry_run": False, "action": "fix_conditions",
1107
+ "batch": batch, "actor": actor, "planned": len(items),
1108
+ "rewritten": 0, "dropped": 0, "pending_logged": 0,
1109
+ "skipped_drift": 0, "skipped_locked": 0, "skipped_denied": 0,
1110
+ "entry_ids": []}
1111
+ for it in items:
1112
+ if limit is not None and (rep["rewritten"] + rep["dropped"]) >= limit:
1113
+ break
1114
+ nid = it["id"]
1115
+ e = cg.index["nodes"].get(nid)
1116
+ if not e:
1117
+ rep["skipped_drift"] += 1
1118
+ continue
1119
+ fm, content = cg._read(e)
1120
+ if fm is None or crypto.is_encrypted(content):
1121
+ rep["skipped_locked"] += 1
1122
+ continue
1123
+ # 预演 → 执行之间可能被改动:当前值必须仍等于我们当初写入的值
1124
+ cur = _ccg_field(content, "生效条件")
1125
+ comment_before = (_comment(fm) or {}).get("生效条件")
1126
+ if (cur or "") != (it["before"] or "") \
1127
+ or comment_before != it["comment_before"]:
1128
+ rep["skipped_drift"] += 1
1129
+ continue
1130
+ comment = _ensure_comment(fm)
1131
+ if it["action"] == "rewrite":
1132
+ content = _upsert_ccg_line(content, "生效条件", it["after"])
1133
+ comment["生效条件"] = it["after"]
1134
+ else:
1135
+ content = _remove_ccg_line(content, "生效条件")
1136
+ # comment 里那份是同一污染的副本 → 还原为**写入前**的原值(真逆操作)
1137
+ orig = it["comment_original"]
1138
+ if orig in (None, ""):
1139
+ comment.pop("生效条件", None)
1140
+ else:
1141
+ comment["生效条件"] = orig
1142
+ st = fm.get("state_attributes")
1143
+ if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
1144
+ and not st["comment"]:
1145
+ st.pop("comment", None)
1146
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
1147
+ durable=True)
1148
+ append_jsonl(_log_path(cg), {
1149
+ "action": "condition_fix", "ts": time.time(), "batch": batch,
1150
+ "actor": actor, "entry_id": it["entry_id"],
1151
+ "write_id": _sha(f"cond_fix|{nid}|{time.time()}"),
1152
+ "node": nid, "layer": e.get("layer"), "fix": it["action"],
1153
+ "fields": {"生效条件": {
1154
+ "before": it["before"], "after": it["after"],
1155
+ "basis": BASIS_CONDITION_SYNTH if it["action"] == "rewrite"
1156
+ else "dropped:legacy_position"}},
1157
+ "comment_before": comment_before,
1158
+ "comment_original": it["comment_original"],
1159
+ "missing_slots": it["missing_slots"],
1160
+ "content_hash_after": _sha(content)})
1161
+ if it["action"] == "rewrite":
1162
+ rep["rewritten"] += 1
1163
+ else:
1164
+ rep["dropped"] += 1
1165
+ append_jsonl(_log_path(cg), {
1166
+ "action": "condition_pending", "ts": time.time(),
1167
+ "batch": batch, "actor": actor, "node": nid,
1168
+ "layer": e.get("layer"), "missing_slots": it["missing_slots"],
1169
+ "reason": "四槽不可合成:单槽冒充行已删除,等待后续补充;"
1170
+ "确实补不动则判 BLINDSPOT"})
1171
+ rep["pending_logged"] += 1
1172
+ rep["entry_ids"].append(it["entry_id"])
1173
+ if rep["rewritten"] or rep["dropped"]:
1174
+ cg.rebuild_index()
1175
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
1176
+ return rep
1177
+
1178
+
1179
+ # 生效条件:x 经 _as_cg,读取日志中 action=condition_fix 且未回滚的记录,按 batch/entry_ids 过滤,节点存在且当前生效条件等于 after 时,还原 before(空则删行)并还原 comment_before,否则计 conflict/missing;返回 rep。
1180
+ def fix_conditions_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
1181
+ """按留痕反向应用:恢复被改写的旧值 / 重建被删除的冒充行。
1182
+
1183
+ 与 `rollback` 同一条纪律:**只在当前值仍等于写入值**时撤销,
1184
+ 否则计 `conflict` 跳过(防覆盖后续人工修改)。
1185
+ """
1186
+ cg = _as_cg(x)
1187
+ want = set(entry_ids) if entry_ids else None
1188
+ rep = {"root": cg.root, "action": "fix_conditions_rollback", "actor": actor,
1189
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
1190
+ "skipped_done": 0}
1191
+ log = list(read_jsonl(_log_path(cg)) or [])
1192
+ done = {r.get("write_id") for r in log
1193
+ if r.get("action") == "fix_conditions_rollback" and r.get("write_id")}
1194
+ for rec in log:
1195
+ if rec.get("action") != "condition_fix":
1196
+ continue
1197
+ if rec.get("write_id") and rec.get("write_id") in done:
1198
+ rep["skipped_done"] += 1
1199
+ continue
1200
+ if batch and rec.get("batch") != batch:
1201
+ continue
1202
+ if want is not None and rec.get("entry_id") not in want:
1203
+ continue
1204
+ nid = rec.get("node")
1205
+ e = cg.index["nodes"].get(nid)
1206
+ if not e:
1207
+ rep["missing"] += 1
1208
+ continue
1209
+ fm, content = cg._read(e)
1210
+ if fm is None:
1211
+ rep["missing"] += 1
1212
+ continue
1213
+ d = (rec.get("fields") or {}).get("生效条件") or {}
1214
+ cur = _ccg_field(content, "生效条件") or ""
1215
+ if cur != (d.get("after") or ""):
1216
+ rep["conflict"] += 1 # 已被后续改动 → 不撤销
1217
+ continue
1218
+ before = d.get("before")
1219
+ if before in (None, ""):
1220
+ content = _remove_ccg_line(content, "生效条件")
1221
+ else:
1222
+ content = _upsert_ccg_line(content, "生效条件", before)
1223
+ comment = _ensure_comment(fm)
1224
+ cb = rec.get("comment_before")
1225
+ if cb in (None, ""):
1226
+ comment.pop("生效条件", None)
1227
+ else:
1228
+ comment["生效条件"] = cb
1229
+ st = fm.get("state_attributes")
1230
+ if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
1231
+ and not st["comment"]:
1232
+ st.pop("comment", None)
1233
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
1234
+ durable=True)
1235
+ append_jsonl(_log_path(cg), {
1236
+ "action": "fix_conditions_rollback", "ts": time.time(),
1237
+ "actor": actor, "batch": rec.get("batch"),
1238
+ "entry_id": rec.get("entry_id"), "write_id": rec.get("write_id"),
1239
+ "node": nid, "restored": before})
1240
+ rep["reverted"] += 1
1241
+ if rep["reverted"]:
1242
+ cg.rebuild_index()
1243
+ return rep
1244
+
1245
+
1246
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 prefix 过滤、内部层跳过,读取失败或加密跳过;对每个节点当前生效条件行,缺失计 absent,否则按 is_legacy_position_condition、等于 _condition_text 合成、等于 comment、其他分别计数,legacy_ids 受 limit 限制;返回 rep。
1247
+ def verify_conditions(x, prefix=None, limit=None) -> dict:
1248
+ """复算核对(只读):生效条件行的来源分布 + 弱等价残留计数。
1249
+
1250
+ `legacy_position` 必须为 **0**——这是本轮口径修正的验收指标:
1251
+ 「观测位置:X」单槽冒充行不得再出现在任何节点里。
1252
+ """
1253
+ cg = _as_cg(x)
1254
+ rep = {"root": cg.root, "dry_run": True, "action": "verify_conditions",
1255
+ "prefix": prefix, "nodes_scanned": 0, "absent": 0, "declared": 0,
1256
+ "from_synthesis": 0, "from_comment": 0, "legacy_position": 0,
1257
+ "other": 0, "by_prefix": {}, "legacy_ids": []}
1258
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
1259
+ if prefix and not str(nid).startswith(prefix):
1260
+ continue
1261
+ if e.get("layer") in INTERNAL_LAYERS:
1262
+ continue
1263
+ fm, content = cg._read(e)
1264
+ if fm is None or crypto.is_encrypted(content):
1265
+ continue
1266
+ rep["nodes_scanned"] += 1
1267
+ cur = _ccg_field(content, "生效条件")
1268
+ if not cur:
1269
+ rep["absent"] += 1
1270
+ kind = "absent"
1271
+ else:
1272
+ rep["declared"] += 1
1273
+ synth = _condition_text(fm)
1274
+ cmt = _as_text(_comment(fm).get("生效条件"))
1275
+ if nodefile.is_legacy_position_condition(cur):
1276
+ rep["legacy_position"] += 1
1277
+ kind = "legacy_position"
1278
+ if limit is None or len(rep["legacy_ids"]) < limit:
1279
+ rep["legacy_ids"].append(nid)
1280
+ elif synth and cur == synth:
1281
+ rep["from_synthesis"] += 1
1282
+ kind = "from_synthesis"
1283
+ elif cmt and cur == cmt:
1284
+ rep["from_comment"] += 1
1285
+ kind = "from_comment"
1286
+ else:
1287
+ rep["other"] += 1
1288
+ kind = "other"
1289
+ box = rep["by_prefix"].setdefault(str(nid).split("_")[0], {})
1290
+ box[kind] = box.get(kind, 0) + 1
1291
+ return rep
1292
+
1293
+
1294
+ # ---- 统一入口 ------------------------------------------------------------
1295
+
1296
+ ACTIONS = ("backfill", "backfill_rollback", "backfill_history",
1297
+ "cap", "cap_rollback", "cap_history",
1298
+ "exempt", "exempt_rollback", "exempt_history")
1299
+
1300
+
1301
+ # 生效条件:按其 action 分派——action=='backfill' 时 kw['apply'] 为真调 apply(x, 去掉 apply 的 kw)、否则调 plan 同参;action=='cap'/'exempt' 同理在 kw['apply'] 为真时调 cap_apply/exempt_apply、否则调 cap_plan/exempt_plan;action=='backfill_rollback'/'cap_rollback'/'exempt_rollback' 分别调 rollback/cap_rollback/exempt_rollback(x, **kw);action=='backfill_history' 调 history(x, **kw),'cap_history'/'exempt_history' 调 history(x, action='cap'/'exempt', **kw);其余 action 值抛 ValueError。
1302
+ def run(x, action, **kw) -> dict:
1303
+ """`maintain` op 的分派入口:action ∈ ACTIONS。"""
1304
+ if action == "backfill":
1305
+ if kw.get("apply"):
1306
+ return apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1307
+ return plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1308
+ if action == "backfill_rollback":
1309
+ return rollback(x, **kw)
1310
+ if action == "backfill_history":
1311
+ return history(x, **kw)
1312
+ if action == "cap":
1313
+ if kw.get("apply"):
1314
+ return cap_apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1315
+ return cap_plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1316
+ if action == "cap_rollback":
1317
+ return cap_rollback(x, **kw)
1318
+ if action == "cap_history":
1319
+ return history(x, action="cap", **kw)
1320
+ if action == "exempt":
1321
+ if kw.get("apply"):
1322
+ return exempt_apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1323
+ return exempt_plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1324
+ if action == "exempt_rollback":
1325
+ return exempt_rollback(x, **kw)
1326
+ if action == "exempt_history":
1327
+ return history(x, action="exempt", **kw)
1328
1328
  raise ValueError(f"未知 backfill action:{action}(允许:{ACTIONS})")