@furongjun1999/dsh-memory 0.4.11 → 0.5.1

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 (580) hide show
  1. package/README.md +552 -465
  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 +143 -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/bench_lingshu_self/bench_self.py +140 -0
  15. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  16. package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
  17. package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
  18. 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
  19. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
  20. 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
  21. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +100 -0
  22. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +197 -0
  23. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v1.md +156 -0
  24. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v10.md +210 -0
  25. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v11.md +227 -0
  26. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v12.md +203 -0
  27. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v13.md +233 -0
  28. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v14.md +191 -0
  29. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v15.md +213 -0
  30. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v16.md +214 -0
  31. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v17.md +199 -0
  32. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v2.md +156 -0
  33. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v3.md +152 -0
  34. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v4.md +128 -0
  35. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v5.md +114 -0
  36. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v6.md +192 -0
  37. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v7.md +187 -0
  38. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v8.md +207 -0
  39. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v9.md +203 -0
  40. 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
  41. 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
  42. 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
  43. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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 +519 -503
  49. 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
  50. 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
  51. 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
  52. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  53. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
  54. 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
  55. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
  56. package/docs/mdcg/lingshu_tutorial.html +14449 -14449
  57. package/docs/mdcg/release_v0.4.11.md +49 -0
  58. package/docs/mdcg/release_v0.4.5.md +55 -55
  59. package/docs/mdcg/tool_table_v0.3.0.md +117 -117
  60. 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
  61. 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
  62. 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
  63. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
  64. 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
  65. 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
  66. 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
  67. package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
  74. 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
  75. package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
  76. 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
  77. 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
  78. package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
  85. 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
  86. 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
  87. package/dsh/README.md +82 -82
  88. package/dsh/cordis.yml.example +139 -139
  89. package/dsh/update-lingshu.bat +11 -11
  90. package/lib/bridge.d.ts +9 -0
  91. package/lib/bridge.js +35 -0
  92. package/lib/hooks.js +36 -2
  93. package/lib/index.js +7 -1
  94. package/lib/lib/roleplay_web.js +116 -29
  95. package/lib/lib/token_store.d.ts +7 -1
  96. package/lib/lib/token_store.js +12 -3
  97. package/md_cg/__init__.py +7 -7
  98. package/md_cg/audit.py +379 -368
  99. package/md_cg/autonomy.py +287 -287
  100. package/md_cg/backfill.py +1328 -1327
  101. package/md_cg/backfill_bigdomain.py +34 -34
  102. package/md_cg/backfill_bucket_zh.py +35 -0
  103. package/md_cg/bench6_arms.py +410 -410
  104. package/md_cg/bench6_common.py +230 -230
  105. package/md_cg/bench6_competitors.py +212 -212
  106. package/md_cg/bench_axis_domain.py +257 -257
  107. package/md_cg/bench_blind_comp.py +308 -308
  108. package/md_cg/bench_e2e_judge.py +532 -0
  109. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  110. package/md_cg/bench_e2e_qa.py +256 -0
  111. package/md_cg/bench_en_atoms_public.py +230 -230
  112. package/md_cg/bench_governance.py +348 -348
  113. package/md_cg/bench_lme_zh.py +410 -410
  114. package/md_cg/bench_locomo.py +121 -121
  115. package/md_cg/bench_locomo_zh.py +450 -450
  116. package/md_cg/bench_locomo_zh_public.py +147 -147
  117. package/md_cg/bench_longmem.py +112 -112
  118. package/md_cg/bench_membench.py +632 -632
  119. package/md_cg/bench_p0.py +149 -149
  120. package/md_cg/bench_progressive.py +287 -287
  121. package/md_cg/bench_role_views.py +238 -238
  122. package/md_cg/bench_task_ab.py +243 -243
  123. package/md_cg/bench_task_ab_llm.py +408 -408
  124. package/md_cg/bench_unified_en.py +204 -204
  125. package/md_cg/bench_zh_mad.py +601 -601
  126. package/md_cg/blindspot_tickets.py +123 -123
  127. package/md_cg/branches.py +301 -285
  128. package/md_cg/build_postings.py +73 -73
  129. package/md_cg/ccgc.py +1006 -948
  130. package/md_cg/census.py +132 -132
  131. package/md_cg/chain.py +315 -300
  132. package/md_cg/codeindex.py +531 -531
  133. package/md_cg/coldverify.py +292 -292
  134. package/md_cg/comment_gate.py +337 -337
  135. package/md_cg/cond_compose.py +190 -190
  136. package/md_cg/cond_facts.py +154 -154
  137. package/md_cg/cond_template.json +106 -106
  138. package/md_cg/condition_anchor.py +142 -142
  139. package/md_cg/conformance.py +726 -726
  140. package/md_cg/consistency.py +717 -717
  141. package/md_cg/consolidate.py +1537 -1439
  142. package/md_cg/corpus.py +110 -110
  143. package/md_cg/crosscheck.py +1098 -1097
  144. package/md_cg/crypto.py +3 -1
  145. package/md_cg/d_meta.py +310 -310
  146. package/md_cg/datapath.py +78 -18
  147. package/md_cg/docindex.py +473 -473
  148. package/md_cg/eval_common.py +575 -575
  149. package/md_cg/evidence.py +4 -2
  150. package/md_cg/evolution.py +477 -477
  151. package/md_cg/export.py +222 -220
  152. package/md_cg/forgetting.py +581 -581
  153. package/md_cg/fsutil.py +377 -329
  154. package/md_cg/hotcache.py +48 -7
  155. package/md_cg/hyperedge.py +251 -251
  156. package/md_cg/identity.py +390 -390
  157. package/md_cg/insight.py +500 -500
  158. package/md_cg/interop.py +338 -0
  159. package/md_cg/judgment_manifest.py +177 -0
  160. package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
  161. package/md_cg/lexicon/build_standard_en.py +171 -171
  162. package/md_cg/lexicon/expand_en_zh.py +211 -211
  163. package/md_cg/lifecycle.py +272 -272
  164. package/md_cg/linkref.py +280 -280
  165. package/md_cg/links.py +140 -107
  166. package/md_cg/mcp_server.py +403 -55
  167. package/md_cg/md_whitebox.py +345 -345
  168. package/md_cg/mdcg.py +783 -233
  169. package/md_cg/mdcos.py +500 -70
  170. package/md_cg/metacognition.py +591 -591
  171. package/md_cg/migrate.py +119 -119
  172. package/md_cg/migrate_aeis.py +221 -221
  173. package/md_cg/migrate_roleplay.py +293 -293
  174. package/md_cg/migrate_wisdom_graph.py +360 -360
  175. package/md_cg/mreview/__init__.py +25 -25
  176. package/md_cg/mreview/__main__.py +110 -110
  177. package/md_cg/mreview/bundle.py +178 -178
  178. package/md_cg/mreview/candidates.py +262 -262
  179. package/md_cg/mreview/govern.py +694 -693
  180. package/md_cg/mreview/locate.py +939 -939
  181. package/md_cg/mreview/pipeline.py +728 -728
  182. package/md_cg/mreview/rules/duplication.json +21 -21
  183. package/md_cg/mreview/rules/field_coverage.json +54 -54
  184. package/md_cg/mreview/rules/source_license.json +21 -21
  185. package/md_cg/mreview/rules/template_flow.json +21 -21
  186. package/md_cg/mreview/ruleset.py +252 -252
  187. package/md_cg/nodefile.py +575 -575
  188. package/md_cg/pooling.py +484 -472
  189. package/md_cg/postings.py +300 -298
  190. package/md_cg/predict.py +1100 -1100
  191. package/md_cg/progressive.py +123 -123
  192. package/md_cg/protect.py +272 -272
  193. package/md_cg/protocol/md_cg_gate.proto +33 -33
  194. package/md_cg/protocol.py +372 -372
  195. package/md_cg/provenance.py +582 -582
  196. package/md_cg/reach.py +453 -453
  197. package/md_cg/readcache.py +143 -0
  198. package/md_cg/reconcile.py +228 -0
  199. package/md_cg/refindex.py +833 -833
  200. package/md_cg/refine.py +604 -604
  201. package/md_cg/review_cli.py +170 -0
  202. package/md_cg/roleviews.py +89 -89
  203. package/md_cg/routing.py +393 -365
  204. package/md_cg/run_tests.py +211 -0
  205. package/md_cg/scrub.py +13 -3
  206. package/md_cg/security.py +128 -18
  207. package/md_cg/self_state.py +1029 -1029
  208. package/md_cg/selfreport.py +152 -151
  209. package/md_cg/semantic/__init__.py +10 -10
  210. package/md_cg/semantic/canonical.py +122 -122
  211. package/md_cg/semantic/en_normalizer.py +364 -364
  212. package/md_cg/semantic/en_zh_map.json +28694 -0
  213. package/md_cg/semantic/export_en_zh_map.py +64 -0
  214. package/md_cg/semantic/unify.py +45 -0
  215. package/md_cg/semantic/zh_en_atoms.py +139 -139
  216. package/md_cg/signer.py +7 -4
  217. package/md_cg/sources.py +816 -582
  218. package/md_cg/statushdr.py +179 -179
  219. package/md_cg/stg.py +54 -37
  220. package/md_cg/subgraph.py +729 -729
  221. package/md_cg/sustain.py +35 -5
  222. package/md_cg/tasks.py +470 -470
  223. package/md_cg/test_access_hints.py +147 -0
  224. package/md_cg/test_action_derive.py +203 -203
  225. package/md_cg/test_audit_rotate.py +270 -270
  226. package/md_cg/test_autonomy.py +143 -143
  227. package/md_cg/test_bench_governance.py +102 -102
  228. package/md_cg/test_blindspot_tickets.py +166 -166
  229. package/md_cg/test_branch_discard_tombstone.py +136 -0
  230. package/md_cg/test_branches.py +13 -3
  231. package/md_cg/test_ccg_perturb.py +184 -184
  232. package/md_cg/test_ccgc.py +433 -433
  233. package/md_cg/test_census_prune.py +81 -81
  234. package/md_cg/test_chain_read_isolate.py +168 -0
  235. package/md_cg/test_cond_compose_anchors.py +76 -76
  236. package/md_cg/test_cond_match.py +165 -165
  237. package/md_cg/test_condition_anchor.py +81 -81
  238. package/md_cg/test_d_meta.py +412 -412
  239. package/md_cg/test_datapath_device_name.py +203 -0
  240. package/md_cg/test_datapath_root.py +199 -199
  241. package/md_cg/test_emit_negtail_cache.py +156 -0
  242. package/md_cg/test_en_pipeline.py +22 -2
  243. package/md_cg/test_gain_gate.py +212 -212
  244. package/md_cg/test_govern_directread.py +421 -0
  245. package/md_cg/test_health_scale.py +173 -173
  246. package/md_cg/test_hive_ingest.py +285 -0
  247. package/md_cg/test_hot_cold.py +215 -215
  248. package/md_cg/test_hyperedge.py +245 -245
  249. package/md_cg/test_i26_empty_first_write.py +116 -0
  250. package/md_cg/test_i27_e041_identity.py +128 -0
  251. package/md_cg/test_i28_hotcache_prodpath.py +122 -0
  252. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  253. package/md_cg/test_identity_attribution.py +96 -15
  254. package/md_cg/test_index_durability.py +17 -3
  255. package/md_cg/test_interop.py +95 -0
  256. package/md_cg/test_interop_judgment.py +228 -0
  257. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  258. package/md_cg/test_lifecycle.py +309 -309
  259. package/md_cg/test_linkref.py +306 -306
  260. package/md_cg/test_links_concurrent_write.py +188 -0
  261. package/md_cg/test_lock.py +43 -43
  262. package/md_cg/test_md_access_parity.py +255 -255
  263. package/md_cg/test_md_writepath.py +345 -345
  264. package/md_cg/test_mdstore_search_parity.py +160 -0
  265. package/md_cg/test_merge_upsert.py +168 -0
  266. package/md_cg/test_mr_m2.py +587 -587
  267. package/md_cg/test_mr_m3.py +710 -710
  268. package/md_cg/test_mr_m4.py +485 -485
  269. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  270. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  271. package/md_cg/test_n131_merge_gate.py +205 -0
  272. package/md_cg/test_p0.py +250 -250
  273. package/md_cg/test_p1.py +316 -316
  274. package/md_cg/test_p10_identity.py +173 -173
  275. package/md_cg/test_p11_consistency.py +233 -233
  276. package/md_cg/test_p12_metacognition.py +212 -212
  277. package/md_cg/test_p13_encryption.py +241 -241
  278. package/md_cg/test_p14_sustain.py +249 -249
  279. package/md_cg/test_p15_scrub.py +280 -280
  280. package/md_cg/test_p16_self_state.py +301 -301
  281. package/md_cg/test_p17_predict.py +354 -354
  282. package/md_cg/test_p18_whitebox.py +171 -171
  283. package/md_cg/test_p19_migrate_roleplay.py +149 -149
  284. package/md_cg/test_p1x_ref_root.py +160 -0
  285. package/md_cg/test_p20_evolution.py +315 -315
  286. package/md_cg/test_p21_tokens.py +293 -270
  287. package/md_cg/test_p22_theory.py +175 -175
  288. package/md_cg/test_p23_links.py +311 -311
  289. package/md_cg/test_p24_evidence.py +227 -227
  290. package/md_cg/test_p25_weights.py +156 -156
  291. package/md_cg/test_p26_refindex.py +416 -416
  292. package/md_cg/test_p27_docindex.py +16 -7
  293. package/md_cg/test_p28_refcheck.py +305 -305
  294. package/md_cg/test_p29_session_ingest_export.py +354 -333
  295. package/md_cg/test_p2_mcp.py +3 -0
  296. package/md_cg/test_p3.py +11 -2
  297. package/md_cg/test_p30_maintain.py +330 -330
  298. package/md_cg/test_p31_insight.py +534 -534
  299. package/md_cg/test_p32_backfill.py +7 -1
  300. package/md_cg/test_p33_ccg_wiring.py +293 -293
  301. package/md_cg/test_p34_crosscheck.py +331 -331
  302. package/md_cg/test_p35_conditioned_claim.py +252 -252
  303. package/md_cg/test_p36_kp_align.py +230 -230
  304. package/md_cg/test_p37_condition_space.py +248 -248
  305. package/md_cg/test_p38_concurrent_flush.py +102 -0
  306. package/md_cg/test_p38_contextualize.py +273 -273
  307. package/md_cg/test_p39_verify_flow.py +153 -0
  308. package/md_cg/test_p39_vision_evidence.py +369 -369
  309. package/md_cg/test_p40_refine_worklist.py +241 -241
  310. package/md_cg/test_p41_evolve_patrol.py +224 -224
  311. package/md_cg/test_p42_provenance.py +269 -269
  312. package/md_cg/test_p43_pooling.py +412 -398
  313. package/md_cg/test_p44_md_whitebox.py +231 -231
  314. package/md_cg/test_p45_session_identity.py +219 -219
  315. package/md_cg/test_p46_unit_scope.py +272 -272
  316. package/md_cg/test_p47_session_view.py +316 -0
  317. package/md_cg/test_p4_fuzzy.py +223 -223
  318. package/md_cg/test_p5_semantic.py +226 -226
  319. package/md_cg/test_p6_consolidate.py +440 -387
  320. package/md_cg/test_p7_goals_recent.py +202 -202
  321. package/md_cg/test_p8_subgraph_chain.py +200 -200
  322. package/md_cg/test_p9_forget_protect.py +231 -231
  323. package/md_cg/test_predict_beta.py +135 -135
  324. package/md_cg/test_preflight_failclosed.py +100 -100
  325. package/md_cg/test_progressive.py +146 -146
  326. package/md_cg/test_propose_tail_index.py +157 -0
  327. package/md_cg/test_protocol.py +243 -243
  328. package/md_cg/test_reach.py +378 -378
  329. package/md_cg/test_reach_keys.py +201 -201
  330. package/md_cg/test_read_clip.py +141 -141
  331. package/md_cg/test_read_scope_b27.py +277 -0
  332. package/md_cg/test_readcache_default_on.py +168 -0
  333. package/md_cg/test_readcache_precise_inval.py +270 -0
  334. package/md_cg/test_readcache_prodpath.py +203 -0
  335. package/md_cg/test_reconcile_v0.py +294 -0
  336. package/md_cg/test_retr_gates_prodpath.py +140 -0
  337. package/md_cg/test_retr_s1.py +344 -340
  338. package/md_cg/test_retr_s1b.py +276 -209
  339. package/md_cg/test_retr_s3.py +194 -194
  340. package/md_cg/test_retr_s4.py +163 -163
  341. package/md_cg/test_retr_s5.py +200 -200
  342. package/md_cg/test_retr_s6.py +157 -157
  343. package/md_cg/test_retr_s7.py +392 -384
  344. package/md_cg/test_retr_s8_time.py +369 -316
  345. package/md_cg/test_retr_s9_edges.py +286 -286
  346. package/md_cg/test_retr_s9_entity_ctx.py +9 -3
  347. package/md_cg/test_retr_score_once.py +208 -0
  348. package/md_cg/test_review_conformance.py +367 -367
  349. package/md_cg/test_review_onepass.py +170 -0
  350. package/md_cg/test_role_views.py +354 -354
  351. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  352. package/md_cg/test_security_audit.py +155 -0
  353. package/md_cg/test_security_audit_b26.py +161 -0
  354. package/md_cg/test_security_audit_v21.py +250 -0
  355. package/md_cg/test_sem_noise.py +242 -242
  356. package/md_cg/test_semantic_canonical.py +16 -2
  357. package/md_cg/test_session_isolation.py +168 -0
  358. package/md_cg/test_snapshot_autoclose.py +187 -0
  359. package/md_cg/test_subproc_encoding.py +192 -192
  360. package/md_cg/test_sustain_mutual.py +153 -153
  361. package/md_cg/test_tail_watermark_race.py +208 -0
  362. package/md_cg/test_tasks.py +409 -409
  363. package/md_cg/test_tenant_env_override_warn.py +139 -0
  364. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  365. package/md_cg/test_tool_face.py +189 -189
  366. package/md_cg/test_transfer.py +180 -180
  367. package/md_cg/test_trust.py +361 -361
  368. package/md_cg/test_twophase.py +286 -286
  369. package/md_cg/test_v14_fixes.py +38 -20
  370. package/md_cg/test_validity_filter.py +280 -280
  371. package/md_cg/test_verify_answer.py +138 -138
  372. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  373. package/md_cg/test_wisdom_md_store.py +292 -292
  374. package/md_cg/test_writelimit.py +197 -197
  375. package/md_cg/test_writepipe.py +214 -214
  376. package/md_cg/theory.py +6 -3
  377. package/md_cg/tokens.py +85 -14
  378. package/md_cg/tool_face.py +260 -260
  379. package/md_cg/trust.py +986 -950
  380. package/md_cg/twophase.py +231 -231
  381. package/md_cg/units.py +668 -667
  382. package/md_cg/vision_evidence.py +667 -666
  383. package/md_cg/weights.py +624 -624
  384. package/md_cg/whitebox.py +527 -527
  385. package/md_cg/whitebox_kb/__init__.py +37 -37
  386. package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
  387. package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
  388. package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
  389. package/md_cg/whitebox_kb/engine.py +310 -310
  390. package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  391. package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
  392. package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
  393. package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
  394. package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
  395. package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
  396. package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
  397. package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
  398. package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
  399. package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
  400. package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
  401. package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
  402. package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
  403. package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
  404. package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
  405. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
  406. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
  407. package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
  408. package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
  409. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
  410. package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
  411. package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
  412. package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
  413. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  414. package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
  415. package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
  416. package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
  417. package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
  418. package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
  419. package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
  420. package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
  421. package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
  422. package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
  423. package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
  424. package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
  425. package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
  426. package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
  427. package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
  428. package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
  429. package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
  430. package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
  431. package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
  432. package/md_cg/writelimit.py +356 -356
  433. package/md_cg/writepipe.py +20 -8
  434. package/package.json +101 -96
  435. package/skills/plugin.json +54 -54
  436. package/skills/skills/designer-perspective/SKILL.md +158 -158
  437. package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
  438. package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
  439. package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
  440. package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
  441. package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
  442. package/skills/skills/designer-perspective/scripts/designer.py +545 -545
  443. package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
  444. package/skills/skills/designer-perspective/tests/selftest.py +61 -61
  445. package/skills/skills/lingshu-browser/SKILL.md +60 -60
  446. package/skills/skills/lingshu-compiler/SKILL.md +56 -56
  447. package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
  448. package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
  449. package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
  450. package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
  451. package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
  452. package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
  453. package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
  454. package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
  455. package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
  456. package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
  457. package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
  458. package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
  459. package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
  460. package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
  461. package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
  462. package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
  463. package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
  464. package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
  465. package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
  466. package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
  467. package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
  468. package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
  469. package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
  470. package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
  471. package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
  472. package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
  473. package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
  474. package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
  475. package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
  476. package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
  477. package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
  478. package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
  479. package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
  480. package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
  481. package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
  482. package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
  483. package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
  484. package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
  485. package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
  486. package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
  487. package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
  488. package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
  489. package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
  490. package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
  491. package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
  492. package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
  493. package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
  494. package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
  495. package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
  496. package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
  497. package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
  498. package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
  499. package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
  500. package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
  501. package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
  502. package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
  503. package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
  504. package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
  505. package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
  506. package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
  507. package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
  508. package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
  509. package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
  510. package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
  511. package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
  512. package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
  513. package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
  514. package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
  515. package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
  516. package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
  517. package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
  518. package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
  519. package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
  520. package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
  521. package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
  522. package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
  523. package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
  524. package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
  525. package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
  526. package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
  527. package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
  528. package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
  529. package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
  530. package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
  531. package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
  532. package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
  533. package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
  534. package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
  535. package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
  536. package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
  537. package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
  538. package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
  539. package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
  540. package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
  541. package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
  542. package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
  543. package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
  544. package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
  545. package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
  546. package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
  547. package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
  548. package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
  549. package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
  550. package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
  551. package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
  552. package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
  553. package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
  554. package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
  555. package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
  556. package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
  557. package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
  558. package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
  559. package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
  560. package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
  561. package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
  562. package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
  563. package/skills/skills/lingshu-graph/SKILL.md +63 -63
  564. package/skills/skills/lingshu-net/SKILL.md +48 -48
  565. package/skills/skills/lingshu-os/SKILL.md +64 -64
  566. package/skills/skills/lingshu-pylang/SKILL.md +71 -71
  567. package/src/bridge.ts +33 -0
  568. package/src/hooks.ts +38 -2
  569. package/src/index.ts +526 -518
  570. package/src/lib/datapath.ts +326 -326
  571. package/src/lib/mdcg_client.ts +413 -413
  572. package/src/lib/mutual.ts +428 -428
  573. package/src/lib/prompt_safety.ts +62 -62
  574. package/src/lib/python_path.ts +71 -71
  575. package/src/lib/roleplay_web.ts +116 -29
  576. package/src/lib/token_store.ts +13 -3
  577. package/src/tools.ts +212 -212
  578. package/zcode/AGENTS.md +11 -3
  579. package/zcode/README.md +41 -41
  580. /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,1329 @@
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
+ from .readcache import direct_read
55
+
56
+ # ---- 常量 ----------------------------------------------------------------
57
+
58
+ BACKFILL_LOG = "_backfill.jsonl"
59
+
60
+ # 负记忆层是「故意无条件」的(覆盖标记),派生脚手架不该再被回填成事实
61
+ SKIP_LAYERS = ("rejected", "unresolved", "goals")
62
+ # 推断脚手架 / 概念层不得回填:否则会污染锚点解析(见 predict.anchor_from_description)
63
+ SKIP_TAGS = ("gap_hint", "scene", "reconstructed", "concept", "insight")
64
+ # 记忆系统**内部脚手架层**(锚点解析 anchor / 自模型修订 self):不是用户知识,永不可回填;
65
+ # 被回填成事实会污染锚点解析与自模型(与 SKIP_LAYERS 同源理由)。crosscheck 亦复用之。
66
+ INTERNAL_LAYERS = ("anchor", "self")
67
+
68
+ # 验证基底枚举 → 可读声明(人/流程声明,不靠模型生成)
69
+ BASIS_TEXT = OrderedDict((
70
+ ("compiler", "编译器/静态检查通过"),
71
+ ("test", "单元测试/回归测试通过"),
72
+ ("measurement", "实测数据(benchmark / 采样)"),
73
+ ("formal_proof", "形式化证明"),
74
+ ("data", "数据/语料统计"),
75
+ # 来源一致性档(文科):文科知识非可复现的物理事实,以来源表述一致为足够基底
76
+ ("textbook", "依人教版教材表述一致(文科·来源一致性)"),
77
+ ("public_kb", "公开知识库条目一致(文科·来源一致性)"),
78
+ ("other", "人工评审或离线工序声明"),
79
+ ))
80
+ BASIS_ENUM_DEFAULT = "other"
81
+
82
+ BATCH_DEFAULT = "backfill"
83
+
84
+ #: 生效条件的**唯一结构化来源**:condition_space 四槽合成(见 nodefile)
85
+ BASIS_CONDITION_SYNTH = "frontmatter.condition_space(四槽合成)"
86
+ #: 旧口径残留标记:把单槽 observation_position 冒充成生效条件的留痕 basis
87
+ LEGACY_CONDITION_BASIS = "frontmatter.condition_space.observation_position"
88
+ #: 存量清洗批次的默认批次名
89
+ FIX_BATCH_DEFAULT = "cond_fix"
90
+
91
+ #: 槽名 → 人读标签(台账/报告用;与 nodefile.CONDITION_SLOTS 同源)
92
+ _SLOT_LABEL = dict(nodefile.CONDITION_SLOTS)
93
+
94
+ # 能力标签规则:cap → 关键词(小写,中文原样)。仅保留 ALL_OPS 里真实存在的 op。
95
+ _CAP_RULES_RAW = OrderedDict((
96
+ ("route", ("路由", "召回", "检索", "rank", "rrf", "route")),
97
+ ("export", ("导出", "灾备", "备份", "搬运", "export", "evidence_pack")),
98
+ ("ingest", ("摄取", "导入", "摄入", "ingest", "分派")),
99
+ ("session", ("会话", "续接", "上下文压缩", "session", "compact")),
100
+ ("maintain", ("维护", "重要性", "重算", "快照", "前馈", "模式分离", "recalc")),
101
+ ("consolidate", ("固化", "提升", "归纳", "聚类", "升格", "promote", "induce")),
102
+ ("insight", ("洞察", "情景重构", "盲区", "归因", "reconstruct", "outlook")),
103
+ ("whitebox", ("白箱", "资格判定", "裁决", "四态", "whitebox")),
104
+ ("verify", ("验证", "核验", "verdict")),
105
+ ("predict", ("预测", "趋势", "外推", "predict")),
106
+ ("causal", ("因果", "causal")),
107
+ ("metacognition", ("元认知", "metacognition")),
108
+ ("self_state", ("自我状态", "状态卡", "self_state")),
109
+ ("evolution", ("演化", "evolution", "账本")),
110
+ ("sustain", ("维生", "自愈", "心跳", "sustain")),
111
+ ("scrub", ("擦除", "去污", "scrub")),
112
+ ("protect", ("保护", "私有内容", "protect")),
113
+ ("forget", ("遗忘", "失效", "forget")),
114
+ ("link", ("蜂群", "对等", "信任", "link")),
115
+ ("identity", ("身份", "主体", "identity")),
116
+ ("goal", ("目标", "goal")),
117
+ ("recent", ("近期事件", "recent")),
118
+ ("theory", ("协议版本", "theory")),
119
+ ("consistency", ("一致性", "自洽", "consistency")),
120
+ ("ref", ("回读", "漂移", "悬空", "code_ref", "doc_ref")),
121
+ ("index_code", ("代码索引", "index_code")),
122
+ ("index_doc", ("文档索引", "章节索引", "index_doc")),
123
+ ))
124
+ # 只保留真实 op,未知 op 静默丢弃(避免注入无效能力名)
125
+ CAP_RULES = OrderedDict(
126
+ (cap, kws) for cap, kws in _CAP_RULES_RAW.items() if cap in tokens.ALL_OPS)
127
+
128
+
129
+ # ---- 通用工具 ------------------------------------------------------------
130
+
131
+ # 生效条件:x 为 str 时返回 MdCGOS(x),否则原样返回 x。
132
+ def _as_cg(x):
133
+ """接受 root 路径或已构造的 cg 实例——保持密级隔离与密钥上下文。"""
134
+ return MdCGOS(x) if isinstance(x, str) else x
135
+
136
+
137
+ # 生效条件:v 为 list/tuple 时返回分号连接的非空元素文本;v 为 None 或空串时返回 "";否则返回 str(v).strip()(v 为 0 或 False 走此支返回 "0"/"False")。
138
+ def _as_text(v) -> str:
139
+ if isinstance(v, (list, tuple)):
140
+ return ";".join(str(x).strip() for x in v if str(x).strip())
141
+ if v in (None, ""):
142
+ return ""
143
+ return str(v).strip()
144
+
145
+
146
+ # 生效条件:fm 为假值时按 {} 处理,state_attributes.comment 为 dict 时返回该 dict,否则返回 {}。
147
+ def _comment(fm: dict) -> dict:
148
+ st = (fm or {}).get("state_attributes")
149
+ c = st.get("comment") if isinstance(st, dict) else None
150
+ return c if isinstance(c, dict) else {}
151
+
152
+
153
+ # 生效条件:fm 的 state_attributes 为 dict 且其下 comment 为 dict 时原样返回该 comment;否则创建并返回空 comment dict(state_attributes 非 dict 时置 fm["state_attributes"]={},comment 非 dict 时置 st["comment"]={})。
154
+ def _ensure_comment(fm: dict) -> dict:
155
+ st = fm.get("state_attributes")
156
+ if not isinstance(st, dict):
157
+ st = {}
158
+ fm["state_attributes"] = st
159
+ c = st.get("comment")
160
+ if not isinstance(c, dict):
161
+ c = {}
162
+ st["comment"] = c
163
+ return c
164
+
165
+
166
+ # 生效条件:e 的 id 为真值时返回 id,否则返回 e 的 path 基名去掉最后 3 个字符(path 为假值时基名为空,结果空串)。
167
+ def _node_id(e: dict) -> str:
168
+ return e.get("id") or os.path.basename(e.get("path") or "")[:-3]
169
+
170
+
171
+ # 生效条件:fm 的 condition_space 四槽齐全时返回 nodefile.condition_space_text 合成文本,否则返回空串。
172
+ def _condition_text(fm: dict) -> str:
173
+ """→ 条件空间四槽合成的生效条件声明;四槽不齐 → ""(不冒充)。
174
+
175
+ **唯一**的 condition_space → 生效条件 路径。旧版在此回退到
176
+ `observation_position` **单槽**加「观测位置:」前缀——那正是
177
+ 「观测位置 ≠ 生效条件」的污染源(真实库 109 条),已删除。
178
+ """
179
+ return nodefile.condition_space_text((fm or {}).get("condition_space"))
180
+
181
+
182
+ # 生效条件:s 为真值时返回其 sha1 前 12 位,s 为假值(None/空串等)时对空串取 sha1 前 12 位。
183
+ def _sha(s: str) -> str:
184
+ return hashlib.sha1((s or "").encode("utf-8")).hexdigest()[:12]
185
+
186
+
187
+ # 生效条件:batch 与 nid 经 f-string 拼接后取 sha1 前 12 位;两者为 None 会字符串化为 "None"。
188
+ def _entry_id(batch: str, nid: str) -> str:
189
+ return hashlib.sha1(f"{batch}|{nid}".encode("utf-8")).hexdigest()[:12]
190
+
191
+
192
+ # 生效条件:content 中 strip 后以 # 开头且去掉 # 与空白后、全角或半角冒号前首段等于 field 的整行被删除,其余行保留并 join。
193
+ def _remove_ccg_line(content: str, field: str) -> str:
194
+ """删掉 `# <字段>:…` 整行(回滚用)。"""
195
+ keep = []
196
+ for ln in (content or "").split("\n"):
197
+ s = ln.strip().lstrip("#").strip()
198
+ name = s.split(":")[0].split(":")[0].strip()
199
+ if ln.strip().startswith("#") and name == field:
200
+ continue
201
+ keep.append(ln)
202
+ return "\n".join(keep)
203
+
204
+
205
+ # 生效条件:cg.root 与模块级常量 BACKFILL_LOG 拼接为返回路径。
206
+ def _log_path(cg) -> str:
207
+ return os.path.join(cg.root, BACKFILL_LOG)
208
+
209
+
210
+ # ---- 字段推导(唯一入口:只搬运已声明的证据) -----------------------------
211
+
212
+ # 生效条件:fm 与 content 给定时,仅对 content 中尚无对应 CCG 行的字段(功能名/生效条件/子功能/执行/验证方式/不适用条件)从 fm 的已有声明(state_attributes.comment、frontmatter、verification_basis、或形参 basis_text)取值,值非空且非占位文本才写入 out,假值不写、占位文本只把字段名追加进 placeholder_out(未传该形参时用临时列表)。
213
+ def derive_fields(fm: dict, content: str, basis_text: str = None,
214
+ placeholder_out: list = None) -> dict:
215
+ """按**已有声明**推导可回填字段 → `{field: (value, basis)}`。
216
+
217
+ 无来源的字段不出现在结果里(不猜测)。
218
+ **占位标记(`骨架锚点`/`内容待填充`)同样不出现**——它不是已声明的事实;
219
+ 被丢弃的字段名记入 `placeholder_out`(可选出参),供报表区分
220
+ 「无来源」与「待填充」两种缺口。
221
+ """
222
+ c = _comment(fm)
223
+ st = fm.get("state_attributes")
224
+ st = st if isinstance(st, dict) else {}
225
+ ph = placeholder_out if placeholder_out is not None else []
226
+ out = {}
227
+
228
+ # 生效条件:field 与 basis 在 value 为真值且 nodefile.is_placeholder_text(value) 为假时写入 out;value 为假值不写入;value 为占位文本时仅把 field 记入 ph。
229
+ def _put(field, value, basis):
230
+ """有值且非占位标记才写出;占位值只记名,绝不渲染成事实。"""
231
+ if not value:
232
+ return
233
+ if nodefile.is_placeholder_text(value):
234
+ ph.append(field)
235
+ return
236
+ out[field] = (value, basis)
237
+
238
+ if not _has_ccg_line(content, "功能名"):
239
+ # 优先 state_attributes.name(迁移入库写入的规范功能名),回退 frontmatter.title
240
+ raw_name = st.get("name")
241
+ v = _as_text(raw_name) if isinstance(raw_name, (str, list, tuple)) else ""
242
+ src = "state_attributes.name"
243
+ if not v:
244
+ v, src = _as_text(fm.get("title")), "frontmatter.title"
245
+ _put("功能名", v, src)
246
+
247
+ if not _has_ccg_line(content, "生效条件"):
248
+ v = _as_text(c.get("生效条件") or c.get("适用条件"))
249
+ src = "state_attributes.comment.生效条件"
250
+ if not v:
251
+ # 结构化来源:整条条件空间声明的合成,**不是** observation_position 单槽
252
+ v, src = _condition_text(fm), BASIS_CONDITION_SYNTH
253
+ _put("生效条件", v, src)
254
+
255
+ if not _has_ccg_line(content, "子功能"):
256
+ _put("子功能", _as_text(c.get("子功能") or c.get("子内容")),
257
+ "state_attributes.comment.子功能")
258
+
259
+ if not _has_ccg_line(content, "执行"):
260
+ _put("执行", _as_text(c.get("执行") or c.get("执行方式")),
261
+ "state_attributes.comment.执行")
262
+
263
+ if not _has_ccg_line(content, "验证方式"):
264
+ v = _as_text(c.get("验证方式"))
265
+ src = "state_attributes.comment.验证方式"
266
+ if not v and nodefile.verification_basis_valid(fm):
267
+ vb = fm.get("verification_basis")
268
+ v, src = BASIS_TEXT.get(vb, ""), f"frontmatter.verification_basis={vb}"
269
+ if not v and basis_text:
270
+ v, src = basis_text, "declared.basis_text"
271
+ _put("验证方式", v, src)
272
+
273
+ if not _has_ccg_line(content, "不适用条件"):
274
+ _put("不适用条件",
275
+ _as_text(c.get("不适用条件") or fm.get("non_applicable_conditions")),
276
+ "state_attributes.comment/frontmatter.non_applicable_conditions")
277
+
278
+ return out
279
+
280
+
281
+ # ---- 节点筛选 ------------------------------------------------------------
282
+
283
+ # 生效条件:cg 无 _readable 可调用时返回 True;有可调用时返回 bool(fn(e)),fn(e) 抛异常时返回 False。
284
+ def _readable_guard(cg, e) -> bool:
285
+ fn = getattr(cg, "_readable", None)
286
+ if not callable(fn):
287
+ return True
288
+ try:
289
+ return bool(fn(e))
290
+ except Exception: # noqa: BLE001
291
+ return False
292
+
293
+
294
+ # 生效条件:仅当 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)。
295
+ def _classify(cg, e, nid, basis_text=None):
296
+ """→ (skip_reason, item);skip_reason 非空表示不参与回填。"""
297
+ fm, content = direct_read(cg, e)
298
+ if fm is None:
299
+ return "unreadable", None
300
+ if crypto.is_encrypted(content):
301
+ return "locked", None
302
+ tags = fm.get("tags") or []
303
+ if e.get("layer") in SKIP_LAYERS or any(t in SKIP_TAGS for t in tags):
304
+ return "derived", None
305
+ cpl = nodefile.ccg_completeness(content)
306
+ if cpl["complete"]:
307
+ return "present", None
308
+ ph_fields = []
309
+ der = derive_fields(fm, content, basis_text=basis_text,
310
+ placeholder_out=ph_fields)
311
+ fill = {f: der[f] for f in der if not _has_ccg_line(content, f)}
312
+ required_missing = [f for f in nodefile.CCG_REQUIRED
313
+ if not _has_ccg_line(content, f)]
314
+ undeducible = [f for f in required_missing if f not in der]
315
+ if not fill:
316
+ # 无可写字段:区分「无来源」(unfillable)与「字段值全是待填充标记」
317
+ # (placeholder)——后者是空壳节点,须转待填充工单,而非静默计入无来源。
318
+ return ("placeholder" if ph_fields else "unfillable"), None
319
+ cls = "backfillable" if not undeducible else "partial"
320
+ # 待补台账:生效条件既不在正文、也推不出(四槽不全)→ 记缺失槽名。
321
+ # 这不是「无来源」而是**缺证据**:补不动者按裁定判 BLINDSPOT,绝不静默 ACCEPT。
322
+ cond_pending = []
323
+ if not _has_ccg_line(content, "生效条件") and "生效条件" not in der:
324
+ cond_pending = nodefile.condition_space_missing(fm.get("condition_space"))
325
+ return "", {
326
+ "id": nid, "layer": e.get("layer"), "class": cls,
327
+ "fill": {f: {"value": v, "basis": b} for f, (v, b) in fill.items()},
328
+ "missing_undeducible": undeducible,
329
+ "placeholder_fields": ph_fields,
330
+ "conditions_pending": cond_pending,
331
+ "before_ratio": cpl["ratio"],
332
+ }
333
+
334
+
335
+ # 生效条件: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。
336
+ def plan(x, layer=None, limit=None, ids=None, include_partial=False,
337
+ basis_text=None, prefix=None) -> dict:
338
+ """预演:产出可回填清单,不写盘。
339
+
340
+ `prefix`:按 id 前缀收窄范围(真实库用 `kp_`,与核对工单同一边界);
341
+ 内部 `anchor`/`self` 层一律出局(计入 `skipped_internal`)。
342
+ """
343
+ cg = _as_cg(x)
344
+ rep = {"root": cg.root, "dry_run": True, "action": "backfill",
345
+ "prefix": prefix, "nodes_scanned": 0, "skipped_locked": 0,
346
+ "skipped_derived": 0, "skipped_internal": 0,
347
+ "skipped_present": 0, "skipped_denied": 0, "unfillable": 0,
348
+ "skipped_placeholder": 0, "nodes_with_placeholder": 0,
349
+ "skipped_partial": 0, "targeted": 0,
350
+ # 待写节点中生效条件仍缺声明的数量(全库台账见 conditions_pending)
351
+ "targeted_missing_conditions": 0, "items": []}
352
+ want = set(ids) if ids else None
353
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
354
+ if want is not None and nid not in want:
355
+ continue
356
+ if prefix and not str(nid).startswith(prefix):
357
+ continue
358
+ if layer and e.get("layer") != layer:
359
+ continue
360
+ if e.get("layer") in INTERNAL_LAYERS:
361
+ rep["skipped_internal"] += 1
362
+ continue
363
+ if not _readable_guard(cg, e):
364
+ rep["skipped_denied"] += 1
365
+ continue
366
+ rep["nodes_scanned"] += 1
367
+ reason, item = _classify(cg, e, nid, basis_text=basis_text)
368
+ if reason == "locked":
369
+ rep["skipped_locked"] += 1
370
+ continue
371
+ if reason == "derived":
372
+ rep["skipped_derived"] += 1
373
+ continue
374
+ if reason == "present":
375
+ rep["skipped_present"] += 1
376
+ continue
377
+ if reason == "unfillable":
378
+ rep["unfillable"] += 1
379
+ continue
380
+ if reason == "placeholder":
381
+ rep["skipped_placeholder"] += 1
382
+ continue
383
+ if item.get("placeholder_fields"):
384
+ rep["nodes_with_placeholder"] += 1
385
+ if item.get("conditions_pending"):
386
+ rep["targeted_missing_conditions"] += 1
387
+ if item["class"] == "partial" and not include_partial:
388
+ rep["skipped_partial"] += 1
389
+ continue
390
+ item["entry_id"] = _entry_id("backfill", nid)
391
+ rep["targeted"] += 1
392
+ if limit is None or len(rep["items"]) < limit:
393
+ rep["items"].append(item)
394
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
395
+ return rep
396
+
397
+
398
+ # ---- 回填写入 / 回滚 / 留痕 ----------------------------------------------
399
+
400
+ # 生效条件:x 可被 _as_cg 解释为 CG 句柄时,按 layer/ids/prefix/entry_ids 选定条目做回填并返回含 written 等计数的 rep,其中 limit 非 None 时把写入数截断到该值。
401
+ def apply(x, ids=None, entry_ids=None, layer=None, limit=None,
402
+ batch=BATCH_DEFAULT, include_partial=False, basis_text=None,
403
+ actor=None, prefix=None) -> dict:
404
+ """执行回填:逐节点改写 md,写 `_backfill.jsonl` 留痕。"""
405
+ cg = _as_cg(x)
406
+ batch = batch or BATCH_DEFAULT
407
+ p = plan(cg, layer=layer, limit=None, ids=ids, prefix=prefix,
408
+ include_partial=include_partial, basis_text=basis_text)
409
+ items = p["items"]
410
+ if entry_ids:
411
+ want = set(entry_ids)
412
+ items = [i for i in items if i["entry_id"] in want]
413
+ rep = {"root": cg.root, "dry_run": False, "action": "backfill",
414
+ "batch": batch, "actor": actor, "planned": len(items),
415
+ "written": 0, "skipped_drift": 0, "skipped_locked": 0,
416
+ "entry_ids": [], "by_field": {}}
417
+ for it in items:
418
+ if limit is not None and rep["written"] >= limit:
419
+ break
420
+ nid = it["id"]
421
+ e = cg.index["nodes"].get(nid)
422
+ if not e:
423
+ rep["skipped_drift"] += 1
424
+ continue
425
+ fm, content = direct_read(cg, e)
426
+ if fm is None or crypto.is_encrypted(content):
427
+ rep["skipped_locked"] += 1
428
+ continue
429
+ # 预演到执行之间节点可能被改动:只写仍缺失的字段
430
+ todo = {f: d for f, d in it["fill"].items()
431
+ if not _has_ccg_line(content, f)}
432
+ if not todo:
433
+ rep["skipped_drift"] += 1
434
+ continue
435
+ comment0 = _comment(fm)
436
+ # 记录改写前的真相:rollback 必须「还原」而非「删除」——否则 comment
437
+ # 里原有的声明会被误删,导致回填不可重复(回滚不是真逆操作)。
438
+ prev_c = {f: comment0[f] for f in todo if f in comment0}
439
+ fm_before = {}
440
+ if "不适用条件" in todo:
441
+ fm_before["non_applicable_conditions"] = {
442
+ "had": "non_applicable_conditions" in fm,
443
+ "value": fm.get("non_applicable_conditions")}
444
+ if "验证方式" in todo and not nodefile.verification_basis_valid(fm):
445
+ fm_before["verification_basis"] = {
446
+ "had": "verification_basis" in fm,
447
+ "value": fm.get("verification_basis")}
448
+ comment = _ensure_comment(fm)
449
+ wid = _sha(f"{nid}|{batch}|{time.time()}")
450
+ applied = {}
451
+ for f, d in todo.items():
452
+ content = _upsert_ccg_line(content, f, d["value"])
453
+ comment[f] = d["value"]
454
+ if f == "不适用条件":
455
+ fm["non_applicable_conditions"] = [
456
+ s.strip() for s in d["value"].split(";") if s.strip()]
457
+ if f == "验证方式" and not nodefile.verification_basis_valid(fm):
458
+ fm["verification_basis"] = BASIS_ENUM_DEFAULT
459
+ applied[f] = {"after": d["value"], "basis": d["basis"],
460
+ "before": prev_c.get(f)}
461
+ rep["by_field"][f] = rep["by_field"].get(f, 0) + 1
462
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
463
+ durable=True)
464
+ append_jsonl(_log_path(cg), {
465
+ "action": "backfill", "ts": time.time(), "batch": batch,
466
+ "actor": actor, "entry_id": it["entry_id"], "write_id": wid,
467
+ "node": nid, "layer": e.get("layer"), "fields": applied,
468
+ "fm_before": fm_before,
469
+ "content_hash_after": _sha(content)})
470
+ rep["written"] += 1
471
+ rep["entry_ids"].append(it["entry_id"])
472
+ if rep["written"]:
473
+ cg.rebuild_index()
474
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
475
+ return rep
476
+
477
+
478
+ # 生效条件:x 经 _as_cg 定位后读留痕日志,仅对 action=='backfill' 且(batch 为假值则不过滤 batch,否则 rec['batch']==batch)、(entry_ids 为假值则不过滤,否则 rec['entry_id'] 属于该集合)、write_id 未出现在已完成 rollback 集合中、节点命中 cg.index['nodes'] 且 direct_read(cg, e) 的 fm 可读、字段当前 _ccg_field(content, f) 等于留痕 after 的记录执行撤销写回(before 为 None 则删该 comment 键,否则还原原值),无字段可撤销只计 conflict 不写盘,reverted 非空时 rebuild_index,结果汇总进返回的 rep。
479
+ def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
480
+ """按留痕反向应用:撤销本批次回填(当前值 ≠ 写入值时跳过,防覆盖)。"""
481
+ cg = _as_cg(x)
482
+ want = set(entry_ids) if entry_ids else None
483
+ rep = {"root": cg.root, "action": "backfill_rollback", "actor": actor,
484
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
485
+ "fields_reverted": 0, "skipped_done": 0}
486
+ log = list(read_jsonl(_log_path(cg)) or [])
487
+ done = {r.get("write_id") for r in log
488
+ if r.get("action") == "backfill_rollback" and r.get("write_id")}
489
+ for rec in log:
490
+ if rec.get("action") != "backfill":
491
+ continue
492
+ if rec.get("write_id") and rec.get("write_id") in done:
493
+ rep["skipped_done"] += 1
494
+ continue
495
+ if batch and rec.get("batch") != batch:
496
+ continue
497
+ if want is not None and rec.get("entry_id") not in want:
498
+ continue
499
+ nid = rec.get("node")
500
+ e = cg.index["nodes"].get(nid)
501
+ if not e:
502
+ rep["missing"] += 1
503
+ continue
504
+ fm, content = direct_read(cg, e)
505
+ if fm is None:
506
+ rep["missing"] += 1
507
+ continue
508
+ comment = _comment(fm)
509
+ reverted, conflicted = {}, []
510
+ for f, d in (rec.get("fields") or {}).items():
511
+ if _ccg_field(content, f) != d.get("after"):
512
+ conflicted.append(f) # 已被后续修改 → 不撤销
513
+ continue
514
+ content = _remove_ccg_line(content, f)
515
+ b = d.get("before")
516
+ if b is None:
517
+ comment.pop(f, None) # 原本就没有 → 删回「无」
518
+ else:
519
+ comment[f] = b # 原本有 → 还原原值(非删除)
520
+ reverted[f] = d.get("after")
521
+ if not reverted:
522
+ rep["conflict"] += 1
523
+ continue
524
+ for k, box in (rec.get("fm_before") or {}).items():
525
+ if box.get("had"):
526
+ fm[k] = box.get("value")
527
+ else:
528
+ fm.pop(k, None)
529
+ st = fm.get("state_attributes")
530
+ if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
531
+ and not st["comment"]:
532
+ st.pop("comment", None)
533
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
534
+ durable=True)
535
+ append_jsonl(_log_path(cg), {
536
+ "action": "backfill_rollback", "ts": time.time(), "actor": actor,
537
+ "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
538
+ "write_id": rec.get("write_id"), "node": nid,
539
+ "reverted": list(reverted), "conflict": conflicted})
540
+ rep["reverted"] += 1
541
+ rep["fields_reverted"] += len(reverted)
542
+ if conflicted:
543
+ rep["conflict"] += 1
544
+ if rep["reverted"]:
545
+ cg.rebuild_index()
546
+ return rep
547
+
548
+
549
+ # 生效条件:x 经 _as_cg,action/batch 为真值时过滤对应日志记录;limit 不为 None 且 >=0 时按 recs[-limit:] 截断(limit=0 时切片为全部记录),limit 为 None 或负数时不截断;返回 total/returned/records。
550
+ def history(x, limit=100, action=None, batch=None) -> dict:
551
+ cg = _as_cg(x)
552
+ recs = []
553
+ for rec in read_jsonl(_log_path(cg)) or []:
554
+ if action and rec.get("action") != action:
555
+ continue
556
+ if batch and rec.get("batch") != batch:
557
+ continue
558
+ recs.append(rec)
559
+ total = len(recs)
560
+ if limit is not None and limit >= 0:
561
+ recs = recs[-limit:]
562
+ return {"root": cg.root, "total": total, "returned": len(recs),
563
+ "records": recs}
564
+
565
+
566
+ # ---- 能力标签注入(关键词启发式) ----------------------------------------
567
+
568
+ # 候选匹配不得吃进两类「自产词」,否则候选再生、plan 永不收敛:
569
+ # ① `fm.tags`:`cap:<op>` 是**注入结果**,回流后 `cap:route` 自匹配关键词
570
+ # "route"、`cap:self_state` 自匹配 "self_state"…… 已注入节点会重新成为候选;
571
+ # ② `# 验证方式:` 模板行:它是 CCG 五要素的必备行,展开后**几乎全库**命中
572
+ # 「验证」,`cap:verify` 遂从能力判断退化为正文模板的副产品
573
+ # (evolution 账本实测:预演命中 1630/1813)。
574
+ _CAP_TEMPLATE_LINES = ("验证方式",)
575
+
576
+
577
+ # 生效条件:content 中去除 # 与空白后、全角或半角冒号前首段命中 _CAP_TEMPLATE_LINES 的行被剔除,其余行保留并 join。
578
+ def _cap_body(content: str) -> str:
579
+ """正文(供关键词匹配)——剔除 CCG 模板行,防模板词污染候选。"""
580
+ keep = []
581
+ for line in (content or "").splitlines():
582
+ head = line.strip().lstrip("#").strip()
583
+ head = head.split(":", 1)[0].split(":", 1)[0].strip()
584
+ if head in _CAP_TEMPLATE_LINES:
585
+ continue
586
+ keep.append(line)
587
+ return "\n".join(keep)
588
+
589
+
590
+ # 生效条件:当 fm 为可 get 的 frontmatter、e 为带 id 的节点条目、content 为正文文本时,返回 title+e.id+功能名/生效条件/子功能字段+正文前 300 字合并后的小写串(不含 fm.tags)。
591
+ def _cap_text(e, fm, content) -> str:
592
+ """候选匹配文本:节点标识 + CCG 字段 + 正文(**不含 `fm.tags`**)。"""
593
+ parts = [_as_text(fm.get("title")), e.get("id") or ""]
594
+ for f in ("功能名", "生效条件", "子功能"):
595
+ v = _ccg_field(content, f)
596
+ if v:
597
+ parts.append(v)
598
+ parts.append(_cap_body(content)[:300])
599
+ return " ".join(parts).lower()
600
+
601
+
602
+ # 生效条件:text 包含 CAP_RULES 中某 cap 的至少一个关键词时,该 cap 以匹配关键词与置信度加入返回,按置信度降序、cap 升序排序;无匹配返回空 hits。
603
+ def cap_matches(text: str) -> list:
604
+ hits = []
605
+ for cap, kws in CAP_RULES.items():
606
+ m = [k for k in kws if k in text]
607
+ if m:
608
+ hits.append({"cap": cap, "matched_by": m,
609
+ "confidence": round(min(1.0, 0.4 + 0.2 * len(m)), 3)})
610
+ hits.sort(key=lambda h: (-h["confidence"], h["cap"]))
611
+ return hits
612
+
613
+
614
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 ids/layer 过滤,不可读计 denied,读取失败或加密计 locked,扫描后以 _cap_text 匹配并过滤 confidence >= min_conf 且标签未含 cap:,无命中计 present,有命中则 targeted 并受 limit 限制加入 items,返回 dry_run rep。
615
+ def cap_plan(x, layer=None, limit=None, ids=None, min_conf=0.5) -> dict:
616
+ """预演:按关键词启发式给出 `cap:<op>` 标签建议(含依据与置信度),不写盘。"""
617
+ cg = _as_cg(x)
618
+ rep = {"root": cg.root, "dry_run": True, "action": "cap",
619
+ "min_conf": min_conf, "nodes_scanned": 0, "skipped_locked": 0,
620
+ "skipped_present": 0, "skipped_denied": 0, "targeted": 0,
621
+ "items": []}
622
+ want = set(ids) if ids else None
623
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
624
+ if want is not None and nid not in want:
625
+ continue
626
+ if layer and e.get("layer") != layer:
627
+ continue
628
+ if not _readable_guard(cg, e):
629
+ rep["skipped_denied"] += 1
630
+ continue
631
+ fm, content = direct_read(cg, e)
632
+ if fm is None or crypto.is_encrypted(content):
633
+ rep["skipped_locked"] += 1
634
+ continue
635
+ rep["nodes_scanned"] += 1
636
+ have = set(t for t in (fm.get("tags") or []) if isinstance(t, str))
637
+ hits = [h for h in cap_matches(_cap_text(e, fm, content))
638
+ if h["confidence"] >= min_conf
639
+ and f"cap:{h['cap']}" not in have]
640
+ if not hits:
641
+ rep["skipped_present"] += 1
642
+ continue
643
+ item = {"id": nid, "layer": e.get("layer"), "caps": hits,
644
+ "entry_id": _entry_id("cap", nid)}
645
+ rep["targeted"] += 1
646
+ if limit is None or len(rep["items"]) < limit:
647
+ rep["items"].append(item)
648
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
649
+ return rep
650
+
651
+
652
+ # 生效条件:x 经 _as_cg,batch 假值回落 BATCH_DEFAULT;先 cap_plan 得 items,按 entry_ids 过滤;逐个检查节点存在、读取可读、无新增标签分别计 drift/locked/drift,成功写 tags 并记日志,limit 非 None 且 written >= limit 时 break;返回 rep。
653
+ def cap_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
654
+ batch=BATCH_DEFAULT, min_conf=0.5, actor=None) -> dict:
655
+ """注入 `cap:<op>` 标签:只改 frontmatter.tags,不动正文。"""
656
+ cg = _as_cg(x)
657
+ batch = batch or BATCH_DEFAULT
658
+ p = cap_plan(cg, layer=layer, limit=None, ids=ids, min_conf=min_conf)
659
+ items = p["items"]
660
+ if entry_ids:
661
+ want = set(entry_ids)
662
+ items = [i for i in items if i["entry_id"] in want]
663
+ rep = {"root": cg.root, "dry_run": False, "action": "cap", "batch": batch,
664
+ "actor": actor, "min_conf": min_conf, "planned": len(items),
665
+ "written": 0, "skipped_locked": 0, "skipped_drift": 0,
666
+ "tags_added": 0, "entry_ids": []}
667
+ for it in items:
668
+ if limit is not None and rep["written"] >= limit:
669
+ break
670
+ nid = it["id"]
671
+ e = cg.index["nodes"].get(nid)
672
+ if not e:
673
+ rep["skipped_drift"] += 1
674
+ continue
675
+ fm, content = direct_read(cg, e)
676
+ if fm is None or crypto.is_encrypted(content):
677
+ rep["skipped_locked"] += 1
678
+ continue
679
+ tags = list(fm.get("tags") or [])
680
+ added = [f"cap:{h['cap']}" for h in it["caps"] if f"cap:{h['cap']}" not in tags]
681
+ if not added:
682
+ rep["skipped_drift"] += 1
683
+ continue
684
+ fm["tags"] = tags + added
685
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
686
+ durable=True)
687
+ append_jsonl(_log_path(cg), {
688
+ "action": "cap", "ts": time.time(), "batch": batch, "actor": actor,
689
+ "entry_id": it["entry_id"], "write_id": _sha(f"cap|{nid}|{time.time()}"),
690
+ "node": nid, "layer": e.get("layer"),
691
+ "tags_added": added,
692
+ "evidence": {h["cap"]: h["matched_by"] for h in it["caps"]},
693
+ "confidence": {h["cap"]: h["confidence"] for h in it["caps"]}})
694
+ rep["written"] += 1
695
+ rep["tags_added"] += len(added)
696
+ rep["entry_ids"].append(it["entry_id"])
697
+ if rep["written"]:
698
+ cg.rebuild_index()
699
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
700
+ return rep
701
+
702
+
703
+ # 生效条件:x 经 _as_cg,读取日志中 action=cap 且未回滚的记录,按 batch/entry_ids 过滤,节点存在且原加入标签仍在 tags 中时移除,否则计 conflict/missing,返回 rep。
704
+ def cap_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
705
+ """撤销 cap 注入:仅移除**仍在 tags 里**的 `cap:` 标签(防覆盖后续修改)。"""
706
+ cg = _as_cg(x)
707
+ want = set(entry_ids) if entry_ids else None
708
+ rep = {"root": cg.root, "action": "cap_rollback", "actor": actor,
709
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
710
+ "tags_removed": 0, "skipped_done": 0}
711
+ log = list(read_jsonl(_log_path(cg)) or [])
712
+ done = {r.get("write_id") for r in log
713
+ if r.get("action") == "cap_rollback" and r.get("write_id")}
714
+ for rec in log:
715
+ if rec.get("action") != "cap":
716
+ continue
717
+ if rec.get("write_id") and rec.get("write_id") in done:
718
+ rep["skipped_done"] += 1
719
+ continue
720
+ if batch and rec.get("batch") != batch:
721
+ continue
722
+ if want is not None and rec.get("entry_id") not in want:
723
+ continue
724
+ nid, added = rec.get("node"), rec.get("tags_added") or []
725
+ e = cg.index["nodes"].get(nid)
726
+ if not e:
727
+ rep["missing"] += 1
728
+ continue
729
+ fm, content = direct_read(cg, e)
730
+ if fm is None:
731
+ rep["missing"] += 1
732
+ continue
733
+ tags = list(fm.get("tags") or [])
734
+ removed = [t for t in added if t in tags]
735
+ if not removed:
736
+ rep["conflict"] += 1
737
+ continue
738
+ fm["tags"] = [t for t in tags if t not in set(removed)]
739
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
740
+ durable=True)
741
+ append_jsonl(_log_path(cg), {
742
+ "action": "cap_rollback", "ts": time.time(), "actor": actor,
743
+ "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
744
+ "write_id": rec.get("write_id"), "node": nid,
745
+ "tags_removed": removed})
746
+ rep["reverted"] += 1
747
+ rep["tags_removed"] += len(removed)
748
+ if rep["reverted"]:
749
+ cg.rebuild_index()
750
+ return rep
751
+
752
+
753
+ # ---- 豁免解除(ccg_exempt 分批摘除) --------------------------------------
754
+
755
+ EXEMPT_FLAG = "ccg_exempt"
756
+
757
+
758
+ # 生效条件:fm 的 verification_basis 合法且 content 的 ccg_completeness 标记 complete 时返回 (True, []),否则把缺失项放入 miss 返回 ready=False。
759
+ def _exempt_ready(fm: dict, content: str):
760
+ """摘豁免前置条件 → `(ready, missing)`。
761
+
762
+ 硬约束(顺序依赖):证据未补齐就摘豁免,节点会从 DEFER **退化为 BLINDSPOT**,
763
+ 比现状更差。故要求「合法 `verification_basis`」+「5 要素齐备(含验证方式行)」
764
+ 同时成立才允许摘除。
765
+ """
766
+ miss = []
767
+ if not nodefile.verification_basis_valid(fm):
768
+ miss.append("verification_basis")
769
+ if not nodefile.ccg_completeness(content).get("complete"):
770
+ miss.append("ccg5")
771
+ return (not miss), miss
772
+
773
+
774
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 ids/prefix/layer 过滤,内部层/不可读/读取失败或加密分别计数跳过,fm 无 EXEMPT_FLAG 计 not_exempt,require_ready 为真且不 ready 时计 unready 并在 want 为 None 时跳过,否则生成 item 受 limit 限制;sample 为真值时按 id 序等距抽取 sample 个。
775
+ def exempt_plan(x, layer=None, limit=None, ids=None, require_ready=True,
776
+ sample=0, prefix=None) -> dict:
777
+ """预演:列出可摘 `ccg_exempt` 的节点(默认要求证据就绪),不写盘。
778
+
779
+ `sample=N` 时按 id 序等距抽取 N 个作为验收样本(确定性,重跑同一样本)。
780
+ `prefix`:按 id 前缀收窄范围(真实库用 `kp_`);内部 `anchor`/`self` 层出局。
781
+ """
782
+ cg = _as_cg(x)
783
+ rep = {"root": cg.root, "dry_run": True, "action": "exempt",
784
+ "require_ready": require_ready, "prefix": prefix, "nodes_scanned": 0,
785
+ "skipped_locked": 0, "skipped_denied": 0, "skipped_not_exempt": 0,
786
+ "skipped_internal": 0, "skipped_unready": 0, "unready_reasons": {},
787
+ "targeted": 0, "items": [], "sample": []}
788
+ want = set(ids) if ids else None
789
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
790
+ if want is not None and nid not in want:
791
+ continue
792
+ if prefix and not str(nid).startswith(prefix):
793
+ continue
794
+ if layer and e.get("layer") != layer:
795
+ continue
796
+ if e.get("layer") in INTERNAL_LAYERS:
797
+ rep["skipped_internal"] += 1
798
+ continue
799
+ if not _readable_guard(cg, e):
800
+ rep["skipped_denied"] += 1
801
+ continue
802
+ fm, content = direct_read(cg, e)
803
+ if fm is None or crypto.is_encrypted(content):
804
+ rep["skipped_locked"] += 1
805
+ continue
806
+ rep["nodes_scanned"] += 1
807
+ if not fm.get(EXEMPT_FLAG):
808
+ rep["skipped_not_exempt"] += 1
809
+ continue
810
+ ready, miss = _exempt_ready(fm, content)
811
+ if require_ready and not ready:
812
+ rep["skipped_unready"] += 1
813
+ for m in miss:
814
+ rep["unready_reasons"][m] = rep["unready_reasons"].get(m, 0) + 1
815
+ # 批量:直接过滤(报表已计数);显式点名:入列交由 apply 判定并留
816
+ # `exempt_skip` 记录——被点名的节点绝不静默丢弃。
817
+ if want is None:
818
+ continue
819
+ item = {"id": nid, "layer": e.get("layer"), "ready": ready,
820
+ "missing": miss, "basis": fm.get("verification_basis"),
821
+ "entry_id": _entry_id("exempt", nid)}
822
+ rep["targeted"] += 1
823
+ if limit is None or len(rep["items"]) < limit:
824
+ rep["items"].append(item)
825
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
826
+ if sample and rep["items"]:
827
+ ids_all = [i["id"] for i in rep["items"]]
828
+ k = min(int(sample), len(ids_all))
829
+ stride = max(1, len(ids_all) // k)
830
+ rep["sample"] = ids_all[::stride][:k]
831
+ return rep
832
+
833
+
834
+ # 生效条件: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。
835
+ def exempt_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
836
+ batch=BATCH_DEFAULT, require_ready=True, actor=None,
837
+ prefix=None) -> dict:
838
+ """分批摘除 `ccg_exempt`(置 False 而非删键,便于反向还原)。
839
+
840
+ 就绪校验在**写入前**再查一次(plan 与 apply 之间可能被改动);
841
+ 不满足则计 `skipped_unready` 并留 `exempt_skip` 记录,绝不硬摘。
842
+ """
843
+ cg = _as_cg(x)
844
+ batch = batch or BATCH_DEFAULT
845
+ p = exempt_plan(cg, layer=layer, ids=ids, require_ready=require_ready,
846
+ prefix=prefix)
847
+ items = p["items"]
848
+ if entry_ids:
849
+ want = set(entry_ids)
850
+ items = [i for i in items if i["entry_id"] in want]
851
+ rep = {"root": cg.root, "dry_run": False, "action": "exempt", "batch": batch,
852
+ "actor": actor, "require_ready": require_ready, "planned": len(items),
853
+ "written": 0, "skipped_unready": 0, "skipped_locked": 0,
854
+ "skipped_drift": 0, "entry_ids": []}
855
+ for it in items:
856
+ if limit is not None and rep["written"] >= limit:
857
+ break
858
+ nid = it["id"]
859
+ e = cg.index["nodes"].get(nid)
860
+ if not e:
861
+ rep["skipped_drift"] += 1
862
+ continue
863
+ fm, content = direct_read(cg, e)
864
+ if fm is None or crypto.is_encrypted(content):
865
+ rep["skipped_locked"] += 1
866
+ continue
867
+ if not fm.get(EXEMPT_FLAG):
868
+ rep["skipped_drift"] += 1
869
+ continue
870
+ ready, miss = _exempt_ready(fm, content)
871
+ if require_ready and not ready:
872
+ rep["skipped_unready"] += 1
873
+ append_jsonl(_log_path(cg), {
874
+ "action": "exempt_skip", "ts": time.time(), "batch": batch,
875
+ "actor": actor, "node": nid, "missing": miss})
876
+ continue
877
+ before = fm.get(EXEMPT_FLAG)
878
+ fm[EXEMPT_FLAG] = False
879
+ wid = _sha(f"exempt|{nid}|{time.time()}")
880
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
881
+ durable=True)
882
+ append_jsonl(_log_path(cg), {
883
+ "action": "exempt", "ts": time.time(), "batch": batch, "actor": actor,
884
+ "entry_id": it["entry_id"], "write_id": wid, "node": nid,
885
+ "layer": e.get("layer"), "exempt_before": before,
886
+ "basis": fm.get("verification_basis")})
887
+ rep["written"] += 1
888
+ rep["entry_ids"].append(it["entry_id"])
889
+ if rep["written"]:
890
+ cg.rebuild_index()
891
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
892
+ return rep
893
+
894
+
895
+ # 生效条件:当 x 可解析为 cg 时,日志中 action 为 exempt 的记录若其非空 write_id 已存在于既有 action 为 exempt_rollback 的记录 write_id 集合中,则跳过并计入 skipped_done,否则在通过 batch 与 entry_ids 过滤后,节点存在且可读、EXEMPT_FLAG 当前为假时,该记录才被还原并计入 reverted;。
896
+ def exempt_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
897
+ """反向还原 `ccg_exempt`:仅当当前仍为「已摘」状态时还原,否则计 conflict。"""
898
+ cg = _as_cg(x)
899
+ want = set(entry_ids) if entry_ids else None
900
+ rep = {"root": cg.root, "action": "exempt_rollback", "actor": actor,
901
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
902
+ "skipped_done": 0}
903
+ log = list(read_jsonl(_log_path(cg)) or [])
904
+ done = {r.get("write_id") for r in log
905
+ if r.get("action") == "exempt_rollback" and r.get("write_id")}
906
+ for rec in log:
907
+ if rec.get("action") != "exempt":
908
+ continue
909
+ if rec.get("write_id") and rec.get("write_id") in done:
910
+ rep["skipped_done"] += 1
911
+ continue
912
+ if batch and rec.get("batch") != batch:
913
+ continue
914
+ if want is not None and rec.get("entry_id") not in want:
915
+ continue
916
+ nid = rec.get("node")
917
+ e = cg.index["nodes"].get(nid)
918
+ if not e:
919
+ rep["missing"] += 1
920
+ continue
921
+ fm, content = direct_read(cg, e)
922
+ if fm is None:
923
+ rep["missing"] += 1
924
+ continue
925
+ if fm.get(EXEMPT_FLAG):
926
+ rep["conflict"] += 1
927
+ continue
928
+ fm[EXEMPT_FLAG] = rec.get("exempt_before", True)
929
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
930
+ durable=True)
931
+ append_jsonl(_log_path(cg), {
932
+ "action": "exempt_rollback", "ts": time.time(), "actor": actor,
933
+ "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
934
+ "write_id": rec.get("write_id"), "node": nid,
935
+ "exempt_restored": fm.get(EXEMPT_FLAG)})
936
+ rep["reverted"] += 1
937
+ if rep["reverted"]:
938
+ cg.rebuild_index()
939
+ return rep
940
+
941
+
942
+ # ---- 待补台账 / 存量清洗(生效条件口径修正) ------------------------------
943
+ #
944
+ # 背景:旧口径把 `condition_space.observation_position` 单槽加前缀「观测位置:」
945
+ # 当作生效条件写入(真实库 109 条)。观测位置 ≠ 生效条件——生效条件是**整条**
946
+ # 条件空间声明的合成。本段负责三件事:
947
+ # ① conditions_pending 只读台账:未声明且四槽推不出的节点
948
+ # ② fix_conditions_* 清洗:冒充行重渲染为合规声明,或删除并登记待补
949
+ # ③ verify_conditions 只读复算:验收指标 legacy_position == 0
950
+
951
+
952
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 prefix/layer 过滤,内部层/不可读/读取失败或加密/derived 层或 SKIP_TAGS 分别计数跳过;扫描后,正文已有生效条件计 declared,derive_fields 可推出计 derivable,否则按 condition_space 缺失槽计 pending 并受 limit 限制收集 items;返回 rep。
953
+ def conditions_pending(x, prefix=None, layer=None, limit=None) -> dict:
954
+ """只读台账:列出「未声明生效条件、且四槽推不出」的节点及其缺失槽。
955
+
956
+ 生效条件已成为必填项(`CCG_REQUIRED`),但存量节点未必有足够声明可补。
957
+ 这类节点**不能静默判 ACCEPT**:进本台账等待后续补充;确实补不动者由资格
958
+ 判定落 BLINDSPOT(而不是被「常用条件默认省略」掩盖)。本函数不写盘。
959
+ """
960
+ cg = _as_cg(x)
961
+ rep = {"root": cg.root, "dry_run": True, "action": "conditions_pending",
962
+ "prefix": prefix, "nodes_scanned": 0, "skipped_locked": 0,
963
+ "skipped_denied": 0, "skipped_internal": 0, "skipped_derived": 0,
964
+ "declared": 0, "derivable": 0, "pending": 0,
965
+ "by_missing": {}, "items": []}
966
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
967
+ if prefix and not str(nid).startswith(prefix):
968
+ continue
969
+ if layer and e.get("layer") != layer:
970
+ continue
971
+ if e.get("layer") in INTERNAL_LAYERS:
972
+ rep["skipped_internal"] += 1
973
+ continue
974
+ if not _readable_guard(cg, e):
975
+ rep["skipped_denied"] += 1
976
+ continue
977
+ fm, content = direct_read(cg, e)
978
+ if fm is None or crypto.is_encrypted(content):
979
+ rep["skipped_locked"] += 1
980
+ continue
981
+ if (e.get("layer") in SKIP_LAYERS
982
+ or any(t in SKIP_TAGS for t in (fm.get("tags") or []))):
983
+ rep["skipped_derived"] += 1
984
+ continue
985
+ rep["nodes_scanned"] += 1
986
+ if _has_ccg_line(content, "生效条件"):
987
+ rep["declared"] += 1
988
+ continue
989
+ if "生效条件" in derive_fields(fm, content):
990
+ # 有来源、只是还没接线 → 属于「待回填」,不是缺证据
991
+ rep["derivable"] += 1
992
+ continue
993
+ cs = fm.get("condition_space")
994
+ miss = nodefile.condition_space_missing(cs)
995
+ if not isinstance(cs, dict) or len(miss) == len(nodefile.CONDITION_SLOTS):
996
+ key = "无条件空间声明"
997
+ else:
998
+ key = "缺" + "+".join(_SLOT_LABEL.get(k, k) for k in miss)
999
+ rep["by_missing"][key] = rep["by_missing"].get(key, 0) + 1
1000
+ rep["pending"] += 1
1001
+ if limit is None or len(rep["items"]) < limit:
1002
+ rep["items"].append({
1003
+ "id": nid, "layer": e.get("layer"), "missing_slots": miss,
1004
+ "missing_text": key,
1005
+ "fallback": "补写生效条件;确实补不动 → 判 BLINDSPOT"})
1006
+ if limit is not None:
1007
+ rep["items"] = rep["items"][:limit]
1008
+ return rep
1009
+
1010
+
1011
+ # 生效条件:cg 的留痕日志中存在 basis 为 LEGACY_CONDITION_BASIS 且未被回滚的 backfill 生效条件写入时,按 node 记录最后一次的 after/original 返回;无匹配返回空。
1012
+ def _legacy_condition_records(cg) -> dict:
1013
+ """→ {node: {after, original, …}}:我们**自己写下的**单槽冒充行(真源 = 留痕)。
1014
+
1015
+ 只有留痕里 `basis == LEGACY_CONDITION_BASIS` 的行才敢自动改——人写下的
1016
+ 「观测位置:…」声明不在其列(宁可漏改,不可误改)。已被回滚的写入剔除;
1017
+ 同一节点多次写入时以最后一次为准。
1018
+ """
1019
+ log = list(read_jsonl(_log_path(cg)) or [])
1020
+ undone = {r.get("write_id") for r in log
1021
+ if r.get("action") == "backfill_rollback" and r.get("write_id")}
1022
+ out = {}
1023
+ for rec in log:
1024
+ if rec.get("action") != "backfill":
1025
+ continue
1026
+ if rec.get("write_id") and rec.get("write_id") in undone:
1027
+ continue
1028
+ f = (rec.get("fields") or {}).get("生效条件")
1029
+ if not isinstance(f, dict) or f.get("basis") != LEGACY_CONDITION_BASIS:
1030
+ continue
1031
+ nid = rec.get("node")
1032
+ if not nid:
1033
+ continue
1034
+ out[nid] = {"after": f.get("after"), "original": f.get("before"),
1035
+ "write_id": rec.get("write_id"), "batch": rec.get("batch")}
1036
+ return out
1037
+
1038
+
1039
+ # 生效条件:x 经 _as_cg,从 _legacy_condition_records 取候选,按 prefix 过滤,节点不存在/不可读/读取失败或加密分别计 missing/denied/locked;当前生效条件等于留痕 after 时,四槽合成非空则 rewrite 否则 drop,不等则 conflict;items 受 limit 限制;返回 dry_run rep。
1040
+ def fix_conditions_plan(x, prefix=None, limit=None) -> dict:
1041
+ """预演:清洗存量单槽冒充行。不写盘。
1042
+
1043
+ 逐条判定(都要求「当前值仍等于我们当初写入的值」,否则计 `conflict`、不碰):
1044
+ · 四槽齐备 → `rewrite`:改写为 `condition_space_text(cs)` 合成声明;
1045
+ · 四槽不全 → `drop`:删掉冒充行并登记待补。补不全的行留着比删掉更危险——
1046
+ 它会被当成生效条件读,等于把坐标的一维当成整条声明。
1047
+ """
1048
+ cg = _as_cg(x)
1049
+ rep = {"root": cg.root, "dry_run": True, "action": "fix_conditions",
1050
+ "prefix": prefix, "legacy": 0, "targeted": 0, "rewrite": 0,
1051
+ "drop": 0, "conflict": 0, "missing": 0, "skipped_locked": 0,
1052
+ "skipped_denied": 0, "items": []}
1053
+ for nid, rec in _legacy_condition_records(cg).items():
1054
+ if prefix and not str(nid).startswith(prefix):
1055
+ continue
1056
+ e = cg.index["nodes"].get(nid)
1057
+ if not e:
1058
+ rep["missing"] += 1
1059
+ continue
1060
+ if not _readable_guard(cg, e):
1061
+ rep["skipped_denied"] += 1
1062
+ continue
1063
+ fm, content = direct_read(cg, e)
1064
+ if fm is None or crypto.is_encrypted(content):
1065
+ rep["skipped_locked"] += 1
1066
+ continue
1067
+ rep["legacy"] += 1
1068
+ cur = _ccg_field(content, "生效条件") or ""
1069
+ if cur != (rec.get("after") or ""):
1070
+ rep["conflict"] += 1 # 已被后续改动 → 不碰
1071
+ continue
1072
+ new = _condition_text(fm)
1073
+ act = "rewrite" if new else "drop"
1074
+ rep["targeted"] += 1
1075
+ rep[act] += 1
1076
+ if limit is None or len(rep["items"]) < limit:
1077
+ rep["items"].append({
1078
+ "id": nid, "layer": e.get("layer"), "action": act,
1079
+ "before": cur, "after": new,
1080
+ "missing_slots": nodefile.condition_space_missing(
1081
+ fm.get("condition_space")),
1082
+ "comment_before": (_comment(fm) or {}).get("生效条件"),
1083
+ "comment_original": rec.get("original"),
1084
+ "entry_id": _entry_id(FIX_BATCH_DEFAULT, nid)})
1085
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
1086
+ return rep
1087
+
1088
+
1089
+ # 生效条件: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。
1090
+ def fix_conditions_apply(x, ids=None, entry_ids=None, prefix=None, limit=None,
1091
+ batch=FIX_BATCH_DEFAULT, actor=None) -> dict:
1092
+ """执行清洗:`rewrite` 改写为四槽合成声明;`drop` 删行并登记待补。
1093
+
1094
+ 写盘与留痕同 `apply`:`_backfill.jsonl` 记 `condition_fix`;`drop` 另记
1095
+ `condition_pending`(待补台账的可追溯副本)。幂等:清洗后不再有候选行。
1096
+ """
1097
+ cg = _as_cg(x)
1098
+ batch = batch or FIX_BATCH_DEFAULT
1099
+ p = fix_conditions_plan(cg, prefix=prefix, limit=None)
1100
+ items = p["items"]
1101
+ if ids:
1102
+ want = set(ids)
1103
+ items = [i for i in items if i["id"] in want]
1104
+ if entry_ids:
1105
+ want = set(entry_ids)
1106
+ items = [i for i in items if i["entry_id"] in want]
1107
+ rep = {"root": cg.root, "dry_run": False, "action": "fix_conditions",
1108
+ "batch": batch, "actor": actor, "planned": len(items),
1109
+ "rewritten": 0, "dropped": 0, "pending_logged": 0,
1110
+ "skipped_drift": 0, "skipped_locked": 0, "skipped_denied": 0,
1111
+ "entry_ids": []}
1112
+ for it in items:
1113
+ if limit is not None and (rep["rewritten"] + rep["dropped"]) >= limit:
1114
+ break
1115
+ nid = it["id"]
1116
+ e = cg.index["nodes"].get(nid)
1117
+ if not e:
1118
+ rep["skipped_drift"] += 1
1119
+ continue
1120
+ fm, content = direct_read(cg, e)
1121
+ if fm is None or crypto.is_encrypted(content):
1122
+ rep["skipped_locked"] += 1
1123
+ continue
1124
+ # 预演 → 执行之间可能被改动:当前值必须仍等于我们当初写入的值
1125
+ cur = _ccg_field(content, "生效条件")
1126
+ comment_before = (_comment(fm) or {}).get("生效条件")
1127
+ if (cur or "") != (it["before"] or "") \
1128
+ or comment_before != it["comment_before"]:
1129
+ rep["skipped_drift"] += 1
1130
+ continue
1131
+ comment = _ensure_comment(fm)
1132
+ if it["action"] == "rewrite":
1133
+ content = _upsert_ccg_line(content, "生效条件", it["after"])
1134
+ comment["生效条件"] = it["after"]
1135
+ else:
1136
+ content = _remove_ccg_line(content, "生效条件")
1137
+ # comment 里那份是同一污染的副本 → 还原为**写入前**的原值(真逆操作)
1138
+ orig = it["comment_original"]
1139
+ if orig in (None, ""):
1140
+ comment.pop("生效条件", None)
1141
+ else:
1142
+ comment["生效条件"] = orig
1143
+ st = fm.get("state_attributes")
1144
+ if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
1145
+ and not st["comment"]:
1146
+ st.pop("comment", None)
1147
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
1148
+ durable=True)
1149
+ append_jsonl(_log_path(cg), {
1150
+ "action": "condition_fix", "ts": time.time(), "batch": batch,
1151
+ "actor": actor, "entry_id": it["entry_id"],
1152
+ "write_id": _sha(f"cond_fix|{nid}|{time.time()}"),
1153
+ "node": nid, "layer": e.get("layer"), "fix": it["action"],
1154
+ "fields": {"生效条件": {
1155
+ "before": it["before"], "after": it["after"],
1156
+ "basis": BASIS_CONDITION_SYNTH if it["action"] == "rewrite"
1157
+ else "dropped:legacy_position"}},
1158
+ "comment_before": comment_before,
1159
+ "comment_original": it["comment_original"],
1160
+ "missing_slots": it["missing_slots"],
1161
+ "content_hash_after": _sha(content)})
1162
+ if it["action"] == "rewrite":
1163
+ rep["rewritten"] += 1
1164
+ else:
1165
+ rep["dropped"] += 1
1166
+ append_jsonl(_log_path(cg), {
1167
+ "action": "condition_pending", "ts": time.time(),
1168
+ "batch": batch, "actor": actor, "node": nid,
1169
+ "layer": e.get("layer"), "missing_slots": it["missing_slots"],
1170
+ "reason": "四槽不可合成:单槽冒充行已删除,等待后续补充;"
1171
+ "确实补不动则判 BLINDSPOT"})
1172
+ rep["pending_logged"] += 1
1173
+ rep["entry_ids"].append(it["entry_id"])
1174
+ if rep["rewritten"] or rep["dropped"]:
1175
+ cg.rebuild_index()
1176
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
1177
+ return rep
1178
+
1179
+
1180
+ # 生效条件:x 经 _as_cg,读取日志中 action=condition_fix 且未回滚的记录,按 batch/entry_ids 过滤,节点存在且当前生效条件等于 after 时,还原 before(空则删行)并还原 comment_before,否则计 conflict/missing;返回 rep。
1181
+ def fix_conditions_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
1182
+ """按留痕反向应用:恢复被改写的旧值 / 重建被删除的冒充行。
1183
+
1184
+ 与 `rollback` 同一条纪律:**只在当前值仍等于写入值**时撤销,
1185
+ 否则计 `conflict` 跳过(防覆盖后续人工修改)。
1186
+ """
1187
+ cg = _as_cg(x)
1188
+ want = set(entry_ids) if entry_ids else None
1189
+ rep = {"root": cg.root, "action": "fix_conditions_rollback", "actor": actor,
1190
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
1191
+ "skipped_done": 0}
1192
+ log = list(read_jsonl(_log_path(cg)) or [])
1193
+ done = {r.get("write_id") for r in log
1194
+ if r.get("action") == "fix_conditions_rollback" and r.get("write_id")}
1195
+ for rec in log:
1196
+ if rec.get("action") != "condition_fix":
1197
+ continue
1198
+ if rec.get("write_id") and rec.get("write_id") in done:
1199
+ rep["skipped_done"] += 1
1200
+ continue
1201
+ if batch and rec.get("batch") != batch:
1202
+ continue
1203
+ if want is not None and rec.get("entry_id") not in want:
1204
+ continue
1205
+ nid = rec.get("node")
1206
+ e = cg.index["nodes"].get(nid)
1207
+ if not e:
1208
+ rep["missing"] += 1
1209
+ continue
1210
+ fm, content = direct_read(cg, e)
1211
+ if fm is None:
1212
+ rep["missing"] += 1
1213
+ continue
1214
+ d = (rec.get("fields") or {}).get("生效条件") or {}
1215
+ cur = _ccg_field(content, "生效条件") or ""
1216
+ if cur != (d.get("after") or ""):
1217
+ rep["conflict"] += 1 # 已被后续改动 → 不撤销
1218
+ continue
1219
+ before = d.get("before")
1220
+ if before in (None, ""):
1221
+ content = _remove_ccg_line(content, "生效条件")
1222
+ else:
1223
+ content = _upsert_ccg_line(content, "生效条件", before)
1224
+ comment = _ensure_comment(fm)
1225
+ cb = rec.get("comment_before")
1226
+ if cb in (None, ""):
1227
+ comment.pop("生效条件", None)
1228
+ else:
1229
+ comment["生效条件"] = cb
1230
+ st = fm.get("state_attributes")
1231
+ if isinstance(st, dict) and isinstance(st.get("comment"), dict) \
1232
+ and not st["comment"]:
1233
+ st.pop("comment", None)
1234
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
1235
+ durable=True)
1236
+ append_jsonl(_log_path(cg), {
1237
+ "action": "fix_conditions_rollback", "ts": time.time(),
1238
+ "actor": actor, "batch": rec.get("batch"),
1239
+ "entry_id": rec.get("entry_id"), "write_id": rec.get("write_id"),
1240
+ "node": nid, "restored": before})
1241
+ rep["reverted"] += 1
1242
+ if rep["reverted"]:
1243
+ cg.rebuild_index()
1244
+ return rep
1245
+
1246
+
1247
+ # 生效条件:x 经 _as_cg,遍历 nodes 按 prefix 过滤、内部层跳过,读取失败或加密跳过;对每个节点当前生效条件行,缺失计 absent,否则按 is_legacy_position_condition、等于 _condition_text 合成、等于 comment、其他分别计数,legacy_ids 受 limit 限制;返回 rep。
1248
+ def verify_conditions(x, prefix=None, limit=None) -> dict:
1249
+ """复算核对(只读):生效条件行的来源分布 + 弱等价残留计数。
1250
+
1251
+ `legacy_position` 必须为 **0**——这是本轮口径修正的验收指标:
1252
+ 「观测位置:X」单槽冒充行不得再出现在任何节点里。
1253
+ """
1254
+ cg = _as_cg(x)
1255
+ rep = {"root": cg.root, "dry_run": True, "action": "verify_conditions",
1256
+ "prefix": prefix, "nodes_scanned": 0, "absent": 0, "declared": 0,
1257
+ "from_synthesis": 0, "from_comment": 0, "legacy_position": 0,
1258
+ "other": 0, "by_prefix": {}, "legacy_ids": []}
1259
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
1260
+ if prefix and not str(nid).startswith(prefix):
1261
+ continue
1262
+ if e.get("layer") in INTERNAL_LAYERS:
1263
+ continue
1264
+ fm, content = direct_read(cg, e)
1265
+ if fm is None or crypto.is_encrypted(content):
1266
+ continue
1267
+ rep["nodes_scanned"] += 1
1268
+ cur = _ccg_field(content, "生效条件")
1269
+ if not cur:
1270
+ rep["absent"] += 1
1271
+ kind = "absent"
1272
+ else:
1273
+ rep["declared"] += 1
1274
+ synth = _condition_text(fm)
1275
+ cmt = _as_text(_comment(fm).get("生效条件"))
1276
+ if nodefile.is_legacy_position_condition(cur):
1277
+ rep["legacy_position"] += 1
1278
+ kind = "legacy_position"
1279
+ if limit is None or len(rep["legacy_ids"]) < limit:
1280
+ rep["legacy_ids"].append(nid)
1281
+ elif synth and cur == synth:
1282
+ rep["from_synthesis"] += 1
1283
+ kind = "from_synthesis"
1284
+ elif cmt and cur == cmt:
1285
+ rep["from_comment"] += 1
1286
+ kind = "from_comment"
1287
+ else:
1288
+ rep["other"] += 1
1289
+ kind = "other"
1290
+ box = rep["by_prefix"].setdefault(str(nid).split("_")[0], {})
1291
+ box[kind] = box.get(kind, 0) + 1
1292
+ return rep
1293
+
1294
+
1295
+ # ---- 统一入口 ------------------------------------------------------------
1296
+
1297
+ ACTIONS = ("backfill", "backfill_rollback", "backfill_history",
1298
+ "cap", "cap_rollback", "cap_history",
1299
+ "exempt", "exempt_rollback", "exempt_history")
1300
+
1301
+
1302
+ # 生效条件:按其 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。
1303
+ def run(x, action, **kw) -> dict:
1304
+ """`maintain` op 的分派入口:action ∈ ACTIONS。"""
1305
+ if action == "backfill":
1306
+ if kw.get("apply"):
1307
+ return apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1308
+ return plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1309
+ if action == "backfill_rollback":
1310
+ return rollback(x, **kw)
1311
+ if action == "backfill_history":
1312
+ return history(x, **kw)
1313
+ if action == "cap":
1314
+ if kw.get("apply"):
1315
+ return cap_apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1316
+ return cap_plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1317
+ if action == "cap_rollback":
1318
+ return cap_rollback(x, **kw)
1319
+ if action == "cap_history":
1320
+ return history(x, action="cap", **kw)
1321
+ if action == "exempt":
1322
+ if kw.get("apply"):
1323
+ return exempt_apply(x, **{k: v for k, v in kw.items() if k != "apply"})
1324
+ return exempt_plan(x, **{k: v for k, v in kw.items() if k != "apply"})
1325
+ if action == "exempt_rollback":
1326
+ return exempt_rollback(x, **kw)
1327
+ if action == "exempt_history":
1328
+ return history(x, action="exempt", **kw)
1328
1329
  raise ValueError(f"未知 backfill action:{action}(允许:{ACTIONS})")