@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
@@ -1,694 +1,695 @@
1
- # -*- coding: utf-8 -*-
2
- """记忆评审流水线 · 级 4:M4 落库治理(确定性部分 · rule 驱动 · 永不删除)。
3
-
4
- 真源:`docs/记忆评审系统_立项设计与施工交接_20260915.md` §3 第 4 级 + §6 验收 + §7 负面清单。
5
- 本模块只做**机械可裁决**的治理动作(回填等);LLM 语义类(合并 / 降权)不在其列——
6
- 那属 M2 建议 + designer 终裁,本轮不落地。
7
-
8
- **规则驱动,引擎不写死**:回填哪个字段、作用于哪些层,不写在本文件里,从 M1 规则库
9
- (`rules/*.json`,数据真源)读——`R-ROLE-MISSING` 的 `matcher.layer` 定作用域、
10
- `mechanical[check=field_absent].field` 定字段。改靶子 = 改数据,引擎冻结。
11
-
12
- **来源链(只搬运已声明证据)**:
13
-
14
- ① `tags` `role:<值>` 标签——显式成文声明;
15
- ② `map` `frontmatter.writer` → role 的**声明映射表**(`WRITER_ROLE_MAP`)。
16
- 当前**故意为空**:库内无任何成文映射能把 writer(`MDCG_ACTOR`:
17
- dsh-memory / codebuddy / zcode / designer-cli…)落到
18
- `mdcos.ALL_ROLES`;`scripts/review_cli.py` 的
19
- `Principal(role="designer")` 属**权限角色**(`tokens.ROLE_SPECS`),
20
- 与节点 role 是两套词汇,混填即污染域。故留空 + 通道保留,待终裁补表;
21
- ③ `layer_default` 层默认投影——真源 = `mdcos.MdCGOS.add` 成文语义「默认 role=None
22
- (知识)」,即缺省来源 = knowledge。该档是「隐含默认的显式化」,
23
- 断言强度高于 ①②,故单独分档上报(`by_source`);只认显式成文声明时
24
- 用 `sources=("tags","map")` 关掉它。
25
-
26
- **域封闭**:写入值必须 ∈ `mdcos.ALL_ROLES`;域外值一律不写,记 `*_out_of_domain` 且
27
- **停止降级**(显式声明了域外值 ≠ 无声明,回退到默认档会把待裁决冲突悄悄抹平)。
28
- `tool-output/command/edit` 属工作角色(`WORK_ROLES`,默认不参与正排,见
29
- `mdcos._candidates`)。故 ② `map` 档**显式拒绝**工作角色(命中即
30
- `map_role_is_work_role`,不降级)——映射表是本引擎的推断,把知识节点推成工作角色
31
- 等于亲手把它从默认召回里摘掉;① `tags` 档是**写入者成文声明**,予以尊重(治理层
32
- 不替作者改主意)。
33
-
34
- **五纪律**(对齐 `backfill`):不猜测(无来源 → `unfillable` + 原因分布,绝不编造)/
35
- 可预演(`role_plan` 只出报表,`role_apply` 写前重查,已有值计 `skipped_drift`)/
36
- 可留痕(`_govern.jsonl`:前后值 / 依据 / 来源档 / 批次 / 操作者 / 规则 id)/
37
- 可回滚(只在当前值 == 写入值时撤销,否则 `conflict`;原本无键 → 删键还原)/
38
- fail-closed(密文与不可读节点跳过,绝不解密回写;靶规则缺失抛错)。
39
- §7 负面清单:只写 frontmatter 一个字段,不动正文、永不删除。
40
- """
41
- from __future__ import annotations
42
-
43
- import argparse
44
- import json
45
- import os
46
- import sys
47
- import time
48
-
49
- from .. import crypto
50
- from ..backfill import _as_cg, _entry_id, _readable_guard, _sha
51
- from ..fsutil import append_jsonl, read_jsonl
52
- from ..mdcos import ALL_ROLES, WORK_ROLES
53
- from .ruleset import _as_rules, _blank
54
-
55
- #: 留痕文件名(与 `backfill._backfill.jsonl` 分立:治理动作须能独立审计)
56
- GOVERN_LOG = "_govern.jsonl"
57
-
58
- #: 默认靶规则;`field_absent` 检查名(与 `ruleset.MECH_CHECKS` 同名)
59
- RULE_ROLE = "R-ROLE-MISSING"
60
- CHECK_FIELD_ABSENT = "field_absent"
61
-
62
- #: 来源档
63
- SOURCE_TAGS = "tags"
64
- SOURCE_MAP = "map"
65
- SOURCE_LAYER = "layer_default"
66
- ALL_SOURCES = (SOURCE_TAGS, SOURCE_MAP, SOURCE_LAYER)
67
- #: 默认只开「成文声明」两档;③ `layer_default` **必须显式开启**。
68
- #: 依据(2026-09-16 真实库实测):writer/tags 面无任何成文声明,故默认档在真实库上
69
- #: 产出 `targeted=0 / unfillable=11120`(原因逐一列出)——这是**诚实结论**,而不是
70
- #: 用 ③ 把指标刷绿:role"覆盖≥90%"若由常量填满即失去判据意义,且 ③ 会关掉 M1 的
71
- #: `field_absent`,使 M2 的 `role_inferable` 语义判定**永不发生**——「字段有值」冒充
72
- #: 「来源已知」。终裁开启前请先读 `LAYER_DEFAULT_WARNING`。
73
- DEFAULT_SOURCES = (SOURCE_TAGS, SOURCE_MAP)
74
-
75
- #: 开启 ③ `layer_default` 前**必读**的终裁提示(`role_plan`/CLI 在 `sources` 含 ③ 时透出)。
76
- #: 它填的是**常量**而非**来源**:`role_ratio_kn` 会立刻跳到 100%(层内几乎全是 knowledge),
77
- #: 但 ① M1 的 `R-ROLE-MISSING`(`field_absent`)随之永不命中——规则失去发现能力;
78
- #: ② M2 的 `role_inferable` 语义判定(LLM 从 writer/tags/正文判 role)永无输入——
79
- #: 即以「字段有值」冒充「来源已知」。③ 若 writer 有声明而库内无法映射(当前真实库
80
- #: 全部如此),本档会把该冲突抹平成「已治理」,掩盖待裁决项。
81
- LAYER_DEFAULT_WARNING = (
82
- "开启 layer_default 是把『来源未知』填成常量 knowledge:role 覆盖率立刻达标,"
83
- "但 M1 field_absent 与 M2 role_inferable 同时失效(字段有值冒充来源已知),"
84
- "并以层默认抹平 writer→role 待裁决冲突。须 designer 终裁、显式传 "
85
- "sources=tags,map,layer_default、留痕署名后方可使用。")
86
-
87
- #: 标签前缀:`role:<值>` 为成文 role 声明
88
- ROLE_TAG_PREFIX = "role:"
89
-
90
- #: `writer` → role 的**声明映射表**(见模块 docstring ②:当前故意为空)
91
- WRITER_ROLE_MAP: dict = {}
92
-
93
- #: 层默认投影表:真源 = `mdcos.MdCGOS.add` docstring「默认 role=None(知识)」。
94
- #: 只有层名本身就在 `ALL_ROLES` 里时才投影(knowledge 是唯一的一个)。
95
- LAYER_DEFAULT_ROLE = {"knowledge": "knowledge"}
96
-
97
- BASIS_TAG = "frontmatter.tags(role:<值> 显式声明)"
98
- BASIS_LAYER = "mdcos.MdCGOS.add 默认语义(role=None ⇒ knowledge)"
99
-
100
- REASON_NO_SOURCE = "no_source_declared"
101
- REASON_TAG_OOD = "tag_role_out_of_domain"
102
- REASON_MAP_OOD = "map_role_out_of_domain"
103
- REASON_MAP_WORK = "map_role_is_work_role"
104
- REASON_WRITER_NO_MAP = "writer_no_mapping"
105
-
106
- ACTION_ROLE = "role"
107
- ACTION_ROLE_ROLLBACK = "role_rollback"
108
- BATCH_DEFAULT = "m4-role"
109
-
110
- ACTIONS = ("role", "role_rollback", "role_history", "role_stats")
111
-
112
-
113
- # ---- 通用工具 ------------------------------------------------------------
114
-
115
- # 生效条件:传入 cg 即返回 os.path.join(cg.root, GOVERN_LOG),无分支。
116
- def _log_path(cg) -> str:
117
- return os.path.join(cg.root, GOVERN_LOG)
118
-
119
-
120
- # 生效条件:fm 为假值(None/{})时按 {} 处理,其 .get("tags") 为假值(None/空串/空列表)时回落 [],否则对该值逐项过滤,只保留 isinstance(t, str) 的元素。
121
- def _tags(fm: dict) -> list:
122
- return [t for t in ((fm or {}).get("tags") or []) if isinstance(t, str)]
123
-
124
-
125
- # 生效条件:detail 为假值(None/空串)时返回 kind,否则返回 "%s:%s" % (kind, detail)。
126
- def _reason_key(kind: str, detail: str = None) -> str:
127
- return kind if not detail else "%s:%s" % (kind, detail)
128
-
129
-
130
- # 生效条件:box 缺 key 时以 box.get(key, 0) 取 0 再加 1 写回 box[key];key 已存在(含值为非数值)时直接对现值 +1。
131
- def _bump(box: dict, key: str) -> None:
132
- box[key] = box.get(key, 0) + 1
133
-
134
-
135
- # ---- 靶子定位(rule 驱动)------------------------------------------------
136
-
137
- # 生效条件:rules/rules_dir 经 _as_rules 得到的列表中存在 id == rule_id 的项、且该项 matcher.layer 非空、mechanical 中存在 check == CHECK_FIELD_ABSENT 且 field 为真的项时,返回 {'rule_id','title','field','layers','severity','remedy','llm'};该项不存在或 layers/field 为空则抛 ValueError。
138
- def role_rule(*, rules=None, rules_dir=None, rule_id: str = RULE_ROLE) -> dict:
139
- """从 M1 规则库取「role 回填」靶规则 → `{field, layers, remedy, …}`。
140
-
141
- 规则库是靶子的**唯一真源**:缺规则 / 缺 `matcher.layer` / 缺
142
- `field_absent.field` 一律抛 ValueError(fail-closed)——规则消失不是
143
- 「没有要治理的东西」,而是治理靶子失效,必须显式红灯而非静默空跑。
144
- """
145
- rl = _as_rules(rules, rules_dir)
146
- hit = next((r for r in rl if r.get("id") == rule_id), None)
147
- if hit is None:
148
- raise ValueError("规则库无 %s(可用:%s)"
149
- % (rule_id, [r.get("id") for r in rl]))
150
- layers = list((hit.get("matcher") or {}).get("layer") or [])
151
- field = None
152
- for spec in hit.get("mechanical") or []:
153
- if spec.get("check") == CHECK_FIELD_ABSENT and spec.get("field"):
154
- field = spec["field"]
155
- break
156
- if not layers or not field:
157
- raise ValueError("规则 %s 未声明 matcher.layer / %s.field——无法定位回填靶"
158
- % (rule_id, CHECK_FIELD_ABSENT))
159
- return {"rule_id": rule_id, "title": hit.get("title"), "field": field,
160
- "layers": layers, "severity": hit.get("severity"),
161
- "remedy": hit.get("remedy"), "llm": list(hit.get("llm") or [])}
162
-
163
-
164
- # ---- 取值推导(唯一入口:只搬运已声明的证据)------------------------------
165
-
166
- # 生效条件:按 sources 逐档判定——含 SOURCE_TAGS 时取 fm 的 tags 内首个 ROLE_TAG_PREFIX 前缀标签,其值属 ALL_ROLES 则返回 (值, BASIS_TAG, SOURCE_TAGS, None),值域外则返回 (None,None,None, REASON_TAG_OOD 原因);tags 未定出时含 SOURCE_MAP 则以小写 writer 取 role_map 值,属 WORK_ROLES 返回 REASON_MAP_WORK 原因、属 ALL_ROLES 返回 (值,"WRITER_ROLE_MAP[writer]",SOURCE_MAP,None)、否则返回 REASON_MAP_OOD 原因(命中域外值即返回、不降级);再否且含 SOURCE_LAYER 且 LAYER_DEFAULT_ROLE 命中 e["layer"] 时返回 (值, BASIS_LAYER, SOURCE_LAYER, None);全部未命中则 writer 非空返回 REASON_WRITER_NO_MAP:writer,writer 为空返回 REASON_NO_SOURCE。
167
- def _candidate_role(e, fm, *, sources, role_map) -> tuple:
168
- """→ `(role, basis, source, reason)`:有来源则 reason=None;无来源则 role=None。
169
-
170
- 按声明强度优先:`tags`(成文)> `map`(声明表)> `layer_default`(隐含默认显式化)。
171
- 命中**域外值**时立即返回且**不降级**:显式声明了域外值 ≠ 无声明——继续回退到更弱的
172
- 来源会把一个待 designer 裁决的冲突悄悄抹平。
173
- """
174
- if SOURCE_TAGS in sources:
175
- for t in _tags(fm):
176
- if t.startswith(ROLE_TAG_PREFIX):
177
- v = t[len(ROLE_TAG_PREFIX):].strip().lower()
178
- if v in ALL_ROLES:
179
- return v, BASIS_TAG, SOURCE_TAGS, None
180
- return None, None, None, _reason_key(REASON_TAG_OOD, v or "空")
181
- if SOURCE_MAP in sources:
182
- w = str(fm.get("writer") or "").strip().lower()
183
- v = str((role_map or {}).get(w) or "").strip().lower() if w else ""
184
- if v:
185
- if v in WORK_ROLES:
186
- # 映射表是**我们的推断**:把 knowledge 层节点推成工作角色会让它在默认
187
- # 召回中被剔除(`_candidates` 默认排除 WORK_ROLES)——故意不做,红灯。
188
- return None, None, None, _reason_key(REASON_MAP_WORK, v)
189
- if v in ALL_ROLES:
190
- return v, "WRITER_ROLE_MAP[%s]" % w, SOURCE_MAP, None
191
- return None, None, None, _reason_key(REASON_MAP_OOD, v)
192
- if SOURCE_LAYER in sources:
193
- v = LAYER_DEFAULT_ROLE.get(str(e.get("layer") or ""))
194
- if v:
195
- return v, BASIS_LAYER, SOURCE_LAYER, None
196
- w = str(fm.get("writer") or "").strip()
197
- return None, None, None, (_reason_key(REASON_WRITER_NO_MAP, w) if w
198
- else REASON_NO_SOURCE)
199
-
200
-
201
- # 生效条件:按序判定——_readable_guard(cg, e) 为假返回 ("denied", None);cg._read(e) 的 fm 为 None 返回 ("unreadable", None);crypto.is_encrypted(content) 为真返回 ("locked", None);fm.get("role") 非空白返回 ("present", None);否则 _candidate_role(e, fm, sources=sources, role_map=role_map) 的 role 为假返回 ("unfillable", {id,layer,reason,writer}),role 为真返回 ("", {id,layer,role,source,basis,before,had_key,writer})。
202
- def _classify(cg, e, nid, *, sources, role_map) -> tuple:
203
- """单条裁决 → `(skip_reason, item|gap)`;skip_reason 为空串表示可回填。"""
204
- if not _readable_guard(cg, e):
205
- return "denied", None
206
- fm, content = cg._read(e)
207
- if fm is None:
208
- return "unreadable", None
209
- if crypto.is_encrypted(content):
210
- return "locked", None
211
- if not _blank(fm.get("role")):
212
- return "present", None
213
- role, basis, src, reason = _candidate_role(e, fm, sources=sources,
214
- role_map=role_map)
215
- if not role:
216
- return "unfillable", {"id": nid, "layer": e.get("layer"),
217
- "reason": reason, "writer": fm.get("writer")}
218
- return "", {"id": nid, "layer": e.get("layer"), "role": role, "source": src,
219
- "basis": basis, "before": fm.get("role"),
220
- "had_key": "role" in fm, "writer": fm.get("writer")}
221
-
222
-
223
- # 生效条件:want 由必填 layers 决定;layer 为真值时须属 layers 否则抛 ValueError,且命中后 want 收窄为 {layer};ids 为真值时只保留白名单内 nid、为假值(None/[])时不过滤;prefix 为真值时只保留 nid 以之开头者;仅 str(e.get("layer")) 属 want 的条目按 nid 升序进入返回列表。
224
- def _iter_scope(cg, *, layers, layer=None, prefix=None, ids=None) -> list:
225
- """按规则作用域遍历索引条目 → `[(nid, entry)]`(只读、nid 稳定序)。
226
-
227
- `layer` 收窄只能收窄到规则已声明的层内——跨层即抛错(同 `ruleset` 的
228
- 「执行时按同一条件过滤,绝不跨层套用」)。
229
- """
230
- want = set(layers)
231
- if layer:
232
- if layer not in want:
233
- raise ValueError("layer=%s 不在靶规则作用域 %s 内(绝不跨层套用)"
234
- % (layer, sorted(want)))
235
- want = {layer}
236
- idw = set(ids) if ids else None
237
- out = []
238
- for nid in sorted((cg.index.get("nodes") or {})):
239
- if idw is not None and nid not in idw:
240
- continue
241
- if prefix and not nid.startswith(prefix):
242
- continue
243
- e = cg.index["nodes"][nid]
244
- if str(e.get("layer")) not in want:
245
- continue
246
- out.append((nid, e))
247
- return out
248
-
249
-
250
- # ---- 预演 -----------------------------------------------------------------
251
-
252
- # 生效条件:以 x 经 _as_cg 得 cg 并先经 role_rule(rules,rules_dir,rule_id) 取规则(缺规则/缺 layer/缺 field 时该步抛 ValueError),role_map 非空时的条目以小写去空白键合并进 WRITER_ROLE_MAP 副本,再遍历 _iter_scope(cg, layers=rule["layers"], layer=layer, prefix=prefix, ids=ids) 逐条 _classify 后返回不写盘的 rep;每条 item 仅在 limit is None 或 len(rep["items"]) < limit 时追加(limit=0 时 items 为空但 targeted 仍累加),sample 为真且 planned_ids 非空时以 max(1, len(planned_ids)//int(sample)) 为步长取前 int(sample) 项。
253
- def role_plan(x, layer=None, limit=None, ids=None, prefix=None, *,
254
- sources=DEFAULT_SOURCES, role_map=None, rule_id=RULE_ROLE,
255
- rules=None, rules_dir=None, sample=0) -> dict:
256
- """预演:产出可回填清单 + 不可回填原因分布,**不写盘**。
257
-
258
- `sources` 见模块 docstring 的三档来源;`role_map` 覆盖 `WRITER_ROLE_MAP`
259
- (designer 终裁补声明表的入口,不改库)。
260
- """
261
- cg = _as_cg(x)
262
- rule = role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id)
263
- rm = dict(WRITER_ROLE_MAP)
264
- rm.update({str(k).strip().lower(): v for k, v in (role_map or {}).items()})
265
- rep = {"root": cg.root, "dry_run": True, "action": ACTION_ROLE,
266
- "rule": rule, "sources": list(sources), "role_map": rm,
267
- "layer": layer, "prefix": prefix,
268
- "nodes_scanned": 0, "skipped_out_of_scope": 0,
269
- "skipped_locked": 0, "skipped_denied": 0, "skipped_unreadable": 0,
270
- "skipped_present": 0, "unfillable": 0, "unfillable_by_reason": {},
271
- "targeted": 0, "by_source": {}, "items": []}
272
- scope_layers = set(rule["layers"])
273
- idw = set(ids) if ids else None
274
- for nid in sorted((cg.index.get("nodes") or {})):
275
- if idw is not None and nid not in idw:
276
- continue
277
- if prefix and not str(nid).startswith(prefix):
278
- continue
279
- e = cg.index["nodes"][nid]
280
- if str(e.get("layer")) not in scope_layers:
281
- rep["skipped_out_of_scope"] += 1
282
- for nid, e in _iter_scope(cg, layers=rule["layers"], layer=layer,
283
- prefix=prefix, ids=ids):
284
- rep["nodes_scanned"] += 1
285
- skip, item = _classify(cg, e, nid, sources=sources, role_map=rm)
286
- if skip == "unfillable":
287
- rep["unfillable"] += 1
288
- # 原因键带明细(如 writer_no_mapping:dsh-memory)——聚合即得逐来源分布
289
- _bump(rep["unfillable_by_reason"], str(item["reason"]))
290
- continue
291
- if skip:
292
- rep["skipped_%s" % skip] = rep.get("skipped_%s" % skip, 0) + 1
293
- continue
294
- item["entry_id"] = _entry_id(ACTION_ROLE, nid)
295
- item["ref"] = e.get("ref")
296
- rep["targeted"] += 1
297
- _bump(rep["by_source"], item["source"])
298
- if limit is None or len(rep["items"]) < limit:
299
- rep["items"].append(item)
300
- rep["planned_ids"] = [i["id"] for i in rep["items"]]
301
- if sample and rep["planned_ids"]:
302
- step = max(1, len(rep["planned_ids"]) // int(sample))
303
- rep["sample"] = rep["planned_ids"][::step][:int(sample)]
304
- rep["rule_layers"] = sorted(scope_layers)
305
- return rep
306
-
307
-
308
- # ---- 执行 / 回滚 / 留痕 / 对照 -------------------------------------------
309
-
310
- # 生效条件:x 经 _as_cg,batch 为假值时回落 BATCH_DEFAULT;先调 role_plan 取 items,entry_ids 为真值时按 entry_id 收窄;逐项处理:索引无该节点或 _read 得 fm 为 None → skipped_missing,_readable_guard 为假 → skipped_denied,crypto.is_encrypted(content) 为真 → skipped_locked,fm["role"] 非空 → skipped_drift,否则写回 role 并 append_jsonl 留痕、written 递增;written 非零时 cg.rebuild_index(),返回含 plan_remaining 的 rep。
311
- def role_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
312
- batch=BATCH_DEFAULT, sources=DEFAULT_SOURCES, role_map=None,
313
- rule_id=RULE_ROLE, rules=None, rules_dir=None, actor=None,
314
- prefix=None) -> dict:
315
- """执行回填:逐节点改写 md 的 role 字段,写 `_govern.jsonl` 留痕。
316
-
317
- 写前**重查**一次(预演 → 执行之间节点可能被改动):已有 role 值 → `skipped_drift`
318
- 一律不动;密文 / 不可读 → `skipped_locked` / `skipped_missing`,**绝不解密回写**。
319
- `ids` / `prefix` / `entry_ids` 三重收窄,`entry_ids` 优先(工单式精确执行)。
320
- """
321
- cg = _as_cg(x)
322
- batch = batch or BATCH_DEFAULT
323
- p = role_plan(cg, layer=layer, limit=limit, ids=ids, prefix=prefix,
324
- sources=sources, role_map=role_map, rule_id=rule_id,
325
- rules=rules, rules_dir=rules_dir)
326
- items = p["items"]
327
- if entry_ids:
328
- want = set(entry_ids)
329
- items = [i for i in items if i["entry_id"] in want]
330
- rep = {"root": cg.root, "dry_run": False, "action": ACTION_ROLE,
331
- "batch": batch, "actor": actor, "rule_id": rule_id,
332
- "field": p["rule"]["field"], "sources": list(sources),
333
- "planned": len(items), "written": 0, "by_source": {},
334
- "skipped_drift": 0, "skipped_locked": 0, "skipped_missing": 0,
335
- "skipped_denied": 0, "entry_ids": [],
336
- "unfillable": p["unfillable"],
337
- "unfillable_by_reason": p["unfillable_by_reason"]}
338
- for it in items:
339
- nid = it["id"]
340
- e = (cg.index.get("nodes") or {}).get(nid)
341
- if e is None:
342
- rep["skipped_missing"] += 1
343
- continue
344
- if not _readable_guard(cg, e):
345
- rep["skipped_denied"] += 1
346
- continue
347
- fm, content = cg._read(e)
348
- if fm is None:
349
- rep["skipped_missing"] += 1
350
- continue
351
- if crypto.is_encrypted(content):
352
- rep["skipped_locked"] += 1
353
- continue
354
- if not _blank(fm.get("role")):
355
- rep["skipped_drift"] += 1
356
- continue
357
- fm_before = {"role": fm.get("role"), "had_key": "role" in fm}
358
- fm["role"] = it["role"]
359
- ts = time.time()
360
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
361
- durable=True)
362
- wid = _sha("%s|%s|%.6f" % (nid, batch, ts))
363
- append_jsonl(_log_path(cg), {
364
- "action": ACTION_ROLE, "ts": ts, "batch": batch, "actor": actor,
365
- "entry_id": it["entry_id"], "write_id": wid, "node": nid,
366
- "layer": e.get("layer"), "rule_id": rule_id, "field": "role",
367
- "value": it["role"], "source": it["source"], "basis": it["basis"],
368
- "writer": it.get("writer"), "fm_before": fm_before,
369
- "content_hash_after": _sha(content)})
370
- rep["written"] += 1
371
- _bump(rep["by_source"], it["source"])
372
- rep["entry_ids"].append(it["entry_id"])
373
- if rep["written"]:
374
- cg.rebuild_index()
375
- rep["plan_remaining"] = max(0, p["targeted"] - len(items))
376
- return rep
377
-
378
-
379
- # 生效条件:只处理 action == ACTION_ROLE 的留痕,且 batch 为真时要求 rec.get("batch") == batch(batch 为 None/假值时不按批次过滤)、entry_ids 为真时要求 rec.get("entry_id") ∈ set(entry_ids);rec 的 write_id 已在既有 ACTION_ROLE_ROLLBACK 记录中 → skipped_done,节点不在索引或 read 不到或加密 → missing,当前 fm.get("role") 为空白或与 rec.get("value") 不等 → conflict 不覆盖,相等时按 fm_before.get("had_key") 恢复 fm_before["role"] 或删除 role 键;reverted 非 0 时 cg.rebuild_index()。
380
- def role_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
381
- """按留痕反向应用:撤销 role 回填。
382
-
383
- **只在当前值仍等于当初写入值时**撤销;否则计 `conflict` 不覆盖(此后可能已有
384
- 人工修正 / 后续批次写入)。原本无该键 → 删键还原(不是写空串)。
385
- """
386
- cg = _as_cg(x)
387
- want = set(entry_ids) if entry_ids else None
388
- rep = {"root": cg.root, "action": ACTION_ROLE_ROLLBACK, "actor": actor,
389
- "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
390
- "skipped_done": 0, "entry_ids": []}
391
- log = list(read_jsonl(_log_path(cg)) or [])
392
- done = {r.get("write_id") for r in log
393
- if r.get("action") == ACTION_ROLE_ROLLBACK and r.get("write_id")}
394
- for rec in log:
395
- if rec.get("action") != ACTION_ROLE:
396
- continue
397
- if batch and rec.get("batch") != batch:
398
- continue
399
- if want is not None and rec.get("entry_id") not in want:
400
- continue
401
- wid = rec.get("write_id")
402
- if wid and wid in done:
403
- rep["skipped_done"] += 1
404
- continue
405
- nid = rec.get("node")
406
- e = (cg.index.get("nodes") or {}).get(nid)
407
- if e is None:
408
- rep["missing"] += 1
409
- continue
410
- fm, content = cg._read(e)
411
- if fm is None or crypto.is_encrypted(content):
412
- rep["missing"] += 1
413
- continue
414
- if _blank(fm.get("role")) or str(fm.get("role")) != str(rec.get("value")):
415
- rep["conflict"] += 1
416
- continue
417
- before = rec.get("fm_before") or {}
418
- if before.get("had_key"):
419
- fm["role"] = before.get("role")
420
- else:
421
- fm.pop("role", None)
422
- cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
423
- durable=True)
424
- append_jsonl(_log_path(cg), {
425
- "action": ACTION_ROLE_ROLLBACK, "ts": time.time(), "actor": actor,
426
- "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
427
- "write_id": wid, "node": nid, "field": "role",
428
- "restored": before.get("role"),
429
- "had_key": bool(before.get("had_key")), "reason": "rollback"})
430
- rep["reverted"] += 1
431
- rep["entry_ids"].append(rec.get("entry_id"))
432
- if rep["reverted"]:
433
- cg.rebuild_index()
434
- return rep
435
-
436
-
437
- # 生效条件:action 为真时只保留 r.get("action") == action 的记录、batch 为真时只保留 r.get("batch") == batch 的记录,total/by_action 统计的是过滤后的 recs 全量;records 在 limit 为假值(0/None)时返回全部 recs,否则返回 recs[-int(limit):]。
438
- def history(x, limit=100, action=None, batch=None) -> dict:
439
- """读 `_govern.jsonl` 留痕(治理动作的可审计面)。"""
440
- cg = _as_cg(x)
441
- recs = [r for r in (read_jsonl(_log_path(cg)) or [])
442
- if (not action or r.get("action") == action)
443
- and (not batch or r.get("batch") == batch)]
444
- tail = recs[-int(limit):] if limit else recs
445
- by_action = {}
446
- for r in recs:
447
- _bump(by_action, str(r.get("action")))
448
- return {"root": cg.root, "action": "role_history", "total": len(recs),
449
- "by_action": by_action, "records": tail}
450
-
451
-
452
- # 生效条件:x 经 _as_cg 且 role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id) 命中并声明了 matcher.layer 与 field_absent.field 时,返回以 conformance.load_index(cg.root) 的节点为口径的统计(by_layer 计全部节点、by_value 只计 role 非空白者、target_met 由 role_ratio_kn >= float(THRESHOLDS["role_coverage_min"]) 决定、gap_to_target 取 max(0.0, thr-kn));规则缺失或声明不全时在 role_rule 处抛 ValueError。
453
- def role_stats(x, *, rule_id=RULE_ROLE, rules=None, rules_dir=None) -> dict:
454
- """只读对照:role 覆盖率 + 取值分布(治理前后量化用)。
455
-
456
- 判据**不复刻**——直接复用 `conformance`(真源:`_coverage_metrics` 的
457
- knowledge 层口径 `role_ratio_kn` 与 `THRESHOLDS["role_coverage_min"]`),
458
- 本函数只补「逐层 / 逐值」分布,供 M4 复跑报告引用。
459
- """
460
- from .. import conformance as CONF # 延迟导入:读侧判据真源,按需加载
461
- cg = _as_cg(x)
462
- rule = role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id)
463
- nodes = CONF.load_index(cg.root)
464
- cov = CONF._coverage_metrics(nodes)
465
- thr = float(CONF.THRESHOLDS["role_coverage_min"])
466
- by_layer, by_value = {}, {}
467
- for e in nodes.values():
468
- _bump(by_layer, str(e.get("layer")))
469
- if not _blank(e.get("role")):
470
- _bump(by_value, str(e.get("role")))
471
- kn = float(cov.get("role_ratio_kn") or 0.0)
472
- return {"root": cg.root, "action": "role_stats", "rule_id": rule_id,
473
- "layers": rule["layers"], "nodes": cov.get("nodes"),
474
- "knowledge": cov.get("knowledge"), "role_ratio": cov.get("role_ratio"),
475
- "role_ratio_kn": cov.get("role_ratio_kn"), "threshold": thr,
476
- "target_met": kn >= thr, "gap_to_target": max(0.0, thr - kn),
477
- "by_layer": by_layer, "by_value": by_value}
478
-
479
-
480
- # ---- 统一入口 ------------------------------------------------------------
481
-
482
- # 生效条件:action 不在 ACTIONS 内时抛 ValueError;action == ACTION_ROLE 时以 kw.pop("apply", False) 为真调用 role_apply(x, **kw)、为假(含缺该键)调用 role_plan(x, **kw);其余情况以 {ACTION_ROLE_ROLLBACK: role_rollback, "role_history": history, "role_stats": role_stats}[action] 取 fn 并返回 fn(x, **kw)。
483
- def run(x, action, **kw) -> dict:
484
- """`role`(`apply=True` 则执行)/ `role_rollback` / `role_history` / `role_stats`。"""
485
- if action not in ACTIONS:
486
- raise ValueError("未知 action=%s(可用:%s)" % (action, list(ACTIONS)))
487
- if action == ACTION_ROLE:
488
- if kw.pop("apply", False):
489
- return role_apply(x, **kw)
490
- return role_plan(x, **kw)
491
- fn = {ACTION_ROLE_ROLLBACK: role_rollback, "role_history": history,
492
- "role_stats": role_stats}[action]
493
- return fn(x, **kw)
494
-
495
-
496
- # ---- 命令行 --------------------------------------------------------------
497
- #
498
- # 形态约定:`--root` / `--rules-dir` / `--json` 均为**子命令参数**(`parents=[common]`),
499
- # 须写在子命令**之后**:`govern plan --root <root>`。写在子命令之前会被顶层解析器
500
- # 当作位置参数吃掉(`scripts/review_cli.py` 的同型坑,2026-09-15 实测更正)。
501
-
502
- # 生效条件:s 为假值(None/空串)时直接返回 DEFAULT_SOURCES;否则按 "," 切分并 strip 丢弃空段,任一段不在 ALL_SOURCES 内即抛 ValueError,切分后 vals 为空(如 s=",")也抛 ValueError,其余返回该非空元组。
503
- def _parse_sources(s) -> tuple:
504
- """CLI 侧来源档解析:逗号分隔,值域封闭(非法即抛,不静默降级)。"""
505
- if not s:
506
- return DEFAULT_SOURCES
507
- vals = tuple(v.strip() for v in str(s).split(",") if v.strip())
508
- bad = [v for v in vals if v not in ALL_SOURCES]
509
- if bad:
510
- raise ValueError("未知来源档 %s(可用:%s)" % (bad, list(ALL_SOURCES)))
511
- if not vals:
512
- raise ValueError("sources 为空(缺省即 %s)" % ",".join(DEFAULT_SOURCES))
513
- return vals
514
-
515
-
516
- # 生效条件:无参数,调用即返回 argparse.ArgumentParser(add_help=False),其中已含 --root(default=None)、--rules-dir(default=None)、--json(store_true) 三项,无分支。
517
- def _common_parser() -> argparse.ArgumentParser:
518
- ap = argparse.ArgumentParser(add_help=False)
519
- ap.add_argument("--root", default=None, help="真源根(缺省读环境变量 MDCG_ROOT)")
520
- ap.add_argument("--rules-dir", default=None, help="规则库目录(缺省 mreview/rules)")
521
- ap.add_argument("--json", action="store_true", help="输出 JSON(缺省人读摘要)")
522
- return ap
523
-
524
-
525
- # 生效条件:传入 p 即向它注册 --layer/--prefix/--ids/--limit(type=int)/--sources 五个参数,无分支且无返回值。
526
- def _add_scope_args(p) -> None:
527
- p.add_argument("--layer", help="收窄到该层(须在规则 matcher 层之内)")
528
- p.add_argument("--prefix", help="只处理该 id 前缀")
529
- p.add_argument("--ids", help="逗号分隔的节点 id 白名单")
530
- p.add_argument("--limit", type=int, help="本次最多处理条数")
531
- p.add_argument("--sources",
532
- help="来源档逗号分隔:tags,map[,layer_default](缺省 %s)"
533
- % ",".join(DEFAULT_SOURCES))
534
-
535
-
536
- # 生效条件:传入 a 后返回 {'layer': a.layer, 'prefix': a.prefix, 'limit': a.limit, 'ids': [...], 'sources': _parse_sources(a.sources), 'rules_dir': a.rules_dir},其中 ids 由 a.ids(为假值即 None/空串)按 "," 切分去空段、结果为空则回落 None,sources 由 a.sources 经 _parse_sources 解析(假值 → DEFAULT_SOURCES,非法值抛 ValueError)。
537
- def _scope_kw(a) -> dict:
538
- return {"layer": a.layer, "prefix": a.prefix, "limit": a.limit,
539
- "ids": [i.strip() for i in (a.ids or "").split(",") if i.strip()] or None,
540
- "sources": _parse_sources(a.sources), "rules_dir": a.rules_dir}
541
-
542
-
543
- # 生效条件:SOURCE_LAYER 在 sources 内时向 sys.stderr 打印 "警告:" + LAYER_DEFAULT_WARNING,否则不输出。
544
- def _warn_layer_default(sources) -> None:
545
- if SOURCE_LAYER in sources:
546
- print("警告:" + LAYER_DEFAULT_WARNING, file=sys.stderr)
547
-
548
-
549
- # 生效条件:传入 msg 即向 sys.stderr 打印该 msg 并返回 2,无分支。
550
- def _die(msg: str) -> int:
551
- print(msg, file=sys.stderr)
552
- return 2
553
-
554
-
555
- def main(argv=None) -> int:
556
- ap = argparse.ArgumentParser(
557
- prog="python -m md_cg.mreview.govern",
558
- description="记忆评审 M4 · 落库治理(确定性部分:rule 驱动 · 永不删除)")
559
- sub = ap.add_subparsers(dest="cmd")
560
- common = _common_parser()
561
-
562
- p = sub.add_parser("plan", parents=[common], help="预演(只出报表,不写盘)")
563
- _add_scope_args(p)
564
- p.add_argument("--sample", type=int, default=0, help="抽样条数(人工核对样板)")
565
-
566
- p = sub.add_parser("apply", parents=[common], help="执行回填(写盘 + 留痕 + 可回滚)")
567
- _add_scope_args(p)
568
- p.add_argument("--batch", default=BATCH_DEFAULT, help="批次名(留痕 / 回滚锚点)")
569
- p.add_argument("--actor", default=None, help="执行者署名(留痕)")
570
- p.add_argument("--entry-id", action="append", default=[],
571
- help="只执行该条目(可多次;工单式精确执行)")
572
- p.add_argument("--yes", action="store_true", help="显式确认写盘(缺省拒绝)")
573
-
574
- p = sub.add_parser("rollback", parents=[common], help="按留痕回滚")
575
- p.add_argument("--batch", default=None, help="只回滚该批次")
576
- p.add_argument("--entry-id", action="append", default=[], help="只回滚该条目(可多次)")
577
- p.add_argument("--actor", default=None, help="执行者署名(留痕)")
578
-
579
- p = sub.add_parser("history", parents=[common], help="读 `_govern.jsonl` 留痕")
580
- p.add_argument("--limit", type=int, default=100)
581
- p.add_argument("--batch", default=None)
582
- p.add_argument("--action", default=None)
583
-
584
- sub.add_parser("stats", parents=[common],
585
- help="覆盖率对照(复用 conformance 判据)")
586
-
587
- a = ap.parse_args(argv)
588
- if not a.cmd:
589
- # `--root` 是**子命令**参数(`parents=[common]`):无子命令时顶层 Namespace
590
- # 根本没有该属性——须先判 cmd 再取 root,否则 `govern` 裸跑即 AttributeError。
591
- ap.print_help()
592
- return 2
593
- root = a.root or os.environ.get("MDCG_ROOT")
594
- if not root:
595
- return _die("需要 --root 或环境变量 MDCG_ROOT(fail-closed)")
596
-
597
- if a.cmd == "plan":
598
- try:
599
- kw = _scope_kw(a)
600
- except ValueError as ex:
601
- return _die(str(ex))
602
- _warn_layer_default(kw["sources"])
603
- try:
604
- rep = role_plan(root, sample=a.sample, **kw)
605
- except ValueError as ex:
606
- return _die("预演失败:%s" % ex)
607
- if a.json:
608
- print(json.dumps(rep, ensure_ascii=False, indent=2))
609
- return 0
610
- print("[M4 预演·只读] root=%s" % rep["root"])
611
- print(" 靶规则 %s|层 %s|字段 %s|来源档 %s"
612
- % (rep["rule"]["rule_id"], ",".join(rep["rule_layers"]),
613
- rep["rule"]["field"], ",".join(rep["sources"])))
614
- print(" 扫描 %d|可回填 %d|不可回填 %d|域外 %d|已锁 %d|已有值 %d|无权限 %d"
615
- % (rep["nodes_scanned"], rep["targeted"], rep["unfillable"],
616
- rep["skipped_out_of_scope"], rep["skipped_locked"],
617
- rep["skipped_present"], rep["skipped_denied"]))
618
- for k, v in sorted(rep["by_source"].items()):
619
- print(" 来源 %-16s %d" % (k, v))
620
- for k, v in sorted(rep["unfillable_by_reason"].items(), key=lambda kv: -kv[1]):
621
- print(" 挡下 %-32s %d" % (k, v))
622
- if rep.get("sample"):
623
- print(" 抽样:%s" % ", ".join(rep["sample"]))
624
- return 0
625
-
626
- if a.cmd == "apply":
627
- if not a.yes:
628
- return _die("apply 会写盘:先跑 plan 核对清单,再以 --yes 显式确认(fail-closed)")
629
- try:
630
- kw = _scope_kw(a)
631
- except ValueError as ex:
632
- return _die(str(ex))
633
- _warn_layer_default(kw["sources"])
634
- try:
635
- rep = role_apply(root, batch=a.batch, actor=a.actor,
636
- entry_ids=list(a.entry_id) or None, **kw)
637
- except ValueError as ex:
638
- return _die("执行失败:%s" % ex)
639
- if a.json:
640
- print(json.dumps(rep, ensure_ascii=False, indent=2))
641
- return 0
642
- print("[M4 执行] batch=%s actor=%s" % (rep["batch"], rep["actor"]))
643
- print(" 写入 %d|漂移跳过 %d|密文跳过 %d|缺失 %d|无权限 %d|余量 %d"
644
- % (rep["written"], rep["skipped_drift"], rep["skipped_locked"],
645
- rep["skipped_missing"], rep["skipped_denied"], rep["plan_remaining"]))
646
- for k, v in sorted(rep["by_source"].items()):
647
- print(" 来源 %-16s %d" % (k, v))
648
- return 0
649
-
650
- if a.cmd == "rollback":
651
- if not a.batch and not a.entry_id:
652
- return _die("rollback 需要 --batch 或 --entry-id(防全量误回滚)")
653
- rep = role_rollback(root, batch=a.batch, actor=a.actor,
654
- entry_ids=list(a.entry_id) or None)
655
- if a.json:
656
- print(json.dumps(rep, ensure_ascii=False, indent=2))
657
- return 0
658
- print("[M4 回滚] batch=%s|还原 %d|冲突跳过 %d|缺失 %d|已回滚 %d"
659
- % (rep["batch"], rep["reverted"], rep["conflict"], rep["missing"],
660
- rep["skipped_done"]))
661
- return 0
662
-
663
- if a.cmd == "history":
664
- rep = history(root, limit=a.limit, action=a.action, batch=a.batch)
665
- if a.json:
666
- print(json.dumps(rep, ensure_ascii=False, indent=2))
667
- return 0
668
- print("[M4 留痕] 命中 %d 条|分布 %s" % (rep["total"], rep["by_action"]))
669
- for r in rep["records"]:
670
- print(" %-14s %-10s %-22s %s=%s"
671
- % (time.strftime("%m-%d %H:%M:%S", time.localtime(r.get("ts") or 0)),
672
- r.get("action"), str(r.get("node")), r.get("field"),
673
- r.get("value", r.get("restored"))))
674
- return 0
675
-
676
- try:
677
- rep = role_stats(root)
678
- except ValueError as ex:
679
- return _die("对照失败:%s" % ex)
680
- if a.json:
681
- print(json.dumps(rep, ensure_ascii=False, indent=2))
682
- return 0
683
- print("[M4 对照] root=%s 判据源=conformance" % rep["root"])
684
- print(" 节点 %s|knowledge %s|role 覆盖 %s|knowledge 层覆盖 %s(阈值 %.2f,%s)"
685
- % (rep["nodes"], rep["knowledge"], rep["role_ratio"],
686
- rep["role_ratio_kn"], rep["threshold"],
687
- "达标" if rep["target_met"] else "未达标,缺口 %.4f" % rep["gap_to_target"]))
688
- print(" 逐层 %s" % rep["by_layer"])
689
- print(" 逐值 %s" % rep["by_value"])
690
- return 0
691
-
692
-
693
- if __name__ == "__main__":
1
+ # -*- coding: utf-8 -*-
2
+ """记忆评审流水线 · 级 4:M4 落库治理(确定性部分 · rule 驱动 · 永不删除)。
3
+
4
+ 真源:`docs/记忆评审系统_立项设计与施工交接_20260915.md` §3 第 4 级 + §6 验收 + §7 负面清单。
5
+ 本模块只做**机械可裁决**的治理动作(回填等);LLM 语义类(合并 / 降权)不在其列——
6
+ 那属 M2 建议 + designer 终裁,本轮不落地。
7
+
8
+ **规则驱动,引擎不写死**:回填哪个字段、作用于哪些层,不写在本文件里,从 M1 规则库
9
+ (`rules/*.json`,数据真源)读——`R-ROLE-MISSING` 的 `matcher.layer` 定作用域、
10
+ `mechanical[check=field_absent].field` 定字段。改靶子 = 改数据,引擎冻结。
11
+
12
+ **来源链(只搬运已声明证据)**:
13
+
14
+ ① `tags` `role:<值>` 标签——显式成文声明;
15
+ ② `map` `frontmatter.writer` → role 的**声明映射表**(`WRITER_ROLE_MAP`)。
16
+ 当前**故意为空**:库内无任何成文映射能把 writer(`MDCG_ACTOR`:
17
+ dsh-memory / codebuddy / zcode / designer-cli…)落到
18
+ `mdcos.ALL_ROLES`;`scripts/review_cli.py` 的
19
+ `Principal(role="designer")` 属**权限角色**(`tokens.ROLE_SPECS`),
20
+ 与节点 role 是两套词汇,混填即污染域。故留空 + 通道保留,待终裁补表;
21
+ ③ `layer_default` 层默认投影——真源 = `mdcos.MdCGOS.add` 成文语义「默认 role=None
22
+ (知识)」,即缺省来源 = knowledge。该档是「隐含默认的显式化」,
23
+ 断言强度高于 ①②,故单独分档上报(`by_source`);只认显式成文声明时
24
+ 用 `sources=("tags","map")` 关掉它。
25
+
26
+ **域封闭**:写入值必须 ∈ `mdcos.ALL_ROLES`;域外值一律不写,记 `*_out_of_domain` 且
27
+ **停止降级**(显式声明了域外值 ≠ 无声明,回退到默认档会把待裁决冲突悄悄抹平)。
28
+ `tool-output/command/edit` 属工作角色(`WORK_ROLES`,默认不参与正排,见
29
+ `mdcos._candidates`)。故 ② `map` 档**显式拒绝**工作角色(命中即
30
+ `map_role_is_work_role`,不降级)——映射表是本引擎的推断,把知识节点推成工作角色
31
+ 等于亲手把它从默认召回里摘掉;① `tags` 档是**写入者成文声明**,予以尊重(治理层
32
+ 不替作者改主意)。
33
+
34
+ **五纪律**(对齐 `backfill`):不猜测(无来源 → `unfillable` + 原因分布,绝不编造)/
35
+ 可预演(`role_plan` 只出报表,`role_apply` 写前重查,已有值计 `skipped_drift`)/
36
+ 可留痕(`_govern.jsonl`:前后值 / 依据 / 来源档 / 批次 / 操作者 / 规则 id)/
37
+ 可回滚(只在当前值 == 写入值时撤销,否则 `conflict`;原本无键 → 删键还原)/
38
+ fail-closed(密文与不可读节点跳过,绝不解密回写;靶规则缺失抛错)。
39
+ §7 负面清单:只写 frontmatter 一个字段,不动正文、永不删除。
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import argparse
44
+ import json
45
+ import os
46
+ import sys
47
+ import time
48
+
49
+ from .. import crypto
50
+ from ..backfill import _as_cg, _entry_id, _readable_guard, _sha
51
+ from ..fsutil import append_jsonl, read_jsonl
52
+ from ..mdcos import ALL_ROLES, WORK_ROLES
53
+ from .ruleset import _as_rules, _blank
54
+ from ..readcache import direct_read
55
+
56
+ #: 留痕文件名(与 `backfill._backfill.jsonl` 分立:治理动作须能独立审计)
57
+ GOVERN_LOG = "_govern.jsonl"
58
+
59
+ #: 默认靶规则;`field_absent` 检查名(与 `ruleset.MECH_CHECKS` 同名)
60
+ RULE_ROLE = "R-ROLE-MISSING"
61
+ CHECK_FIELD_ABSENT = "field_absent"
62
+
63
+ #: 来源档
64
+ SOURCE_TAGS = "tags"
65
+ SOURCE_MAP = "map"
66
+ SOURCE_LAYER = "layer_default"
67
+ ALL_SOURCES = (SOURCE_TAGS, SOURCE_MAP, SOURCE_LAYER)
68
+ #: 默认只开「成文声明」两档;③ `layer_default` **必须显式开启**。
69
+ #: 依据(2026-09-16 真实库实测):writer/tags 面无任何成文声明,故默认档在真实库上
70
+ #: 产出 `targeted=0 / unfillable=11120`(原因逐一列出)——这是**诚实结论**,而不是
71
+ #: 用 ③ 把指标刷绿:role"覆盖≥90%"若由常量填满即失去判据意义,且 ③ 会关掉 M1 的
72
+ #: `field_absent`,使 M2 的 `role_inferable` 语义判定**永不发生**——「字段有值」冒充
73
+ #: 「来源已知」。终裁开启前请先读 `LAYER_DEFAULT_WARNING`。
74
+ DEFAULT_SOURCES = (SOURCE_TAGS, SOURCE_MAP)
75
+
76
+ #: 开启 ③ `layer_default` 前**必读**的终裁提示(`role_plan`/CLI 在 `sources` 含 ③ 时透出)。
77
+ #: 它填的是**常量**而非**来源**:`role_ratio_kn` 会立刻跳到 100%(层内几乎全是 knowledge),
78
+ #: 但 ① M1 的 `R-ROLE-MISSING`(`field_absent`)随之永不命中——规则失去发现能力;
79
+ #: ② M2 的 `role_inferable` 语义判定(LLM 从 writer/tags/正文判 role)永无输入——
80
+ #: 即以「字段有值」冒充「来源已知」。③ 若 writer 有声明而库内无法映射(当前真实库
81
+ #: 全部如此),本档会把该冲突抹平成「已治理」,掩盖待裁决项。
82
+ LAYER_DEFAULT_WARNING = (
83
+ "开启 layer_default 是把『来源未知』填成常量 knowledge:role 覆盖率立刻达标,"
84
+ "但 M1 field_absent 与 M2 role_inferable 同时失效(字段有值冒充来源已知),"
85
+ "并以层默认抹平 writer→role 待裁决冲突。须 designer 终裁、显式传 "
86
+ "sources=tags,map,layer_default、留痕署名后方可使用。")
87
+
88
+ #: 标签前缀:`role:<值>` 为成文 role 声明
89
+ ROLE_TAG_PREFIX = "role:"
90
+
91
+ #: `writer` → role 的**声明映射表**(见模块 docstring ②:当前故意为空)
92
+ WRITER_ROLE_MAP: dict = {}
93
+
94
+ #: 层默认投影表:真源 = `mdcos.MdCGOS.add` docstring「默认 role=None(知识)」。
95
+ #: 只有层名本身就在 `ALL_ROLES` 里时才投影(knowledge 是唯一的一个)。
96
+ LAYER_DEFAULT_ROLE = {"knowledge": "knowledge"}
97
+
98
+ BASIS_TAG = "frontmatter.tags(role:<值> 显式声明)"
99
+ BASIS_LAYER = "mdcos.MdCGOS.add 默认语义(role=None ⇒ knowledge)"
100
+
101
+ REASON_NO_SOURCE = "no_source_declared"
102
+ REASON_TAG_OOD = "tag_role_out_of_domain"
103
+ REASON_MAP_OOD = "map_role_out_of_domain"
104
+ REASON_MAP_WORK = "map_role_is_work_role"
105
+ REASON_WRITER_NO_MAP = "writer_no_mapping"
106
+
107
+ ACTION_ROLE = "role"
108
+ ACTION_ROLE_ROLLBACK = "role_rollback"
109
+ BATCH_DEFAULT = "m4-role"
110
+
111
+ ACTIONS = ("role", "role_rollback", "role_history", "role_stats")
112
+
113
+
114
+ # ---- 通用工具 ------------------------------------------------------------
115
+
116
+ # 生效条件:传入 cg 即返回 os.path.join(cg.root, GOVERN_LOG),无分支。
117
+ def _log_path(cg) -> str:
118
+ return os.path.join(cg.root, GOVERN_LOG)
119
+
120
+
121
+ # 生效条件:fm 为假值(None/{})时按 {} 处理,其 .get("tags") 为假值(None/空串/空列表)时回落 [],否则对该值逐项过滤,只保留 isinstance(t, str) 的元素。
122
+ def _tags(fm: dict) -> list:
123
+ return [t for t in ((fm or {}).get("tags") or []) if isinstance(t, str)]
124
+
125
+
126
+ # 生效条件:detail 为假值(None/空串)时返回 kind,否则返回 "%s:%s" % (kind, detail)。
127
+ def _reason_key(kind: str, detail: str = None) -> str:
128
+ return kind if not detail else "%s:%s" % (kind, detail)
129
+
130
+
131
+ # 生效条件:box 缺 key 时以 box.get(key, 0) 取 0 再加 1 写回 box[key];key 已存在(含值为非数值)时直接对现值 +1。
132
+ def _bump(box: dict, key: str) -> None:
133
+ box[key] = box.get(key, 0) + 1
134
+
135
+
136
+ # ---- 靶子定位(rule 驱动)------------------------------------------------
137
+
138
+ # 生效条件:rules/rules_dir 经 _as_rules 得到的列表中存在 id == rule_id 的项、且该项 matcher.layer 非空、mechanical 中存在 check == CHECK_FIELD_ABSENT 且 field 为真的项时,返回 {'rule_id','title','field','layers','severity','remedy','llm'};该项不存在或 layers/field 为空则抛 ValueError。
139
+ def role_rule(*, rules=None, rules_dir=None, rule_id: str = RULE_ROLE) -> dict:
140
+ """从 M1 规则库取「role 回填」靶规则 → `{field, layers, remedy, …}`。
141
+
142
+ 规则库是靶子的**唯一真源**:缺规则 / 缺 `matcher.layer` / 缺
143
+ `field_absent.field` 一律抛 ValueError(fail-closed)——规则消失不是
144
+ 「没有要治理的东西」,而是治理靶子失效,必须显式红灯而非静默空跑。
145
+ """
146
+ rl = _as_rules(rules, rules_dir)
147
+ hit = next((r for r in rl if r.get("id") == rule_id), None)
148
+ if hit is None:
149
+ raise ValueError("规则库无 %s(可用:%s)"
150
+ % (rule_id, [r.get("id") for r in rl]))
151
+ layers = list((hit.get("matcher") or {}).get("layer") or [])
152
+ field = None
153
+ for spec in hit.get("mechanical") or []:
154
+ if spec.get("check") == CHECK_FIELD_ABSENT and spec.get("field"):
155
+ field = spec["field"]
156
+ break
157
+ if not layers or not field:
158
+ raise ValueError("规则 %s 未声明 matcher.layer / %s.field——无法定位回填靶"
159
+ % (rule_id, CHECK_FIELD_ABSENT))
160
+ return {"rule_id": rule_id, "title": hit.get("title"), "field": field,
161
+ "layers": layers, "severity": hit.get("severity"),
162
+ "remedy": hit.get("remedy"), "llm": list(hit.get("llm") or [])}
163
+
164
+
165
+ # ---- 取值推导(唯一入口:只搬运已声明的证据)------------------------------
166
+
167
+ # 生效条件:按 sources 逐档判定——含 SOURCE_TAGS 时取 fm 的 tags 内首个 ROLE_TAG_PREFIX 前缀标签,其值属 ALL_ROLES 则返回 (值, BASIS_TAG, SOURCE_TAGS, None),值域外则返回 (None,None,None, REASON_TAG_OOD 原因);tags 未定出时含 SOURCE_MAP 则以小写 writer 取 role_map 值,属 WORK_ROLES 返回 REASON_MAP_WORK 原因、属 ALL_ROLES 返回 (值,"WRITER_ROLE_MAP[writer]",SOURCE_MAP,None)、否则返回 REASON_MAP_OOD 原因(命中域外值即返回、不降级);再否且含 SOURCE_LAYER 且 LAYER_DEFAULT_ROLE 命中 e["layer"] 时返回 (值, BASIS_LAYER, SOURCE_LAYER, None);全部未命中则 writer 非空返回 REASON_WRITER_NO_MAP:writer,writer 为空返回 REASON_NO_SOURCE。
168
+ def _candidate_role(e, fm, *, sources, role_map) -> tuple:
169
+ """→ `(role, basis, source, reason)`:有来源则 reason=None;无来源则 role=None。
170
+
171
+ 按声明强度优先:`tags`(成文)> `map`(声明表)> `layer_default`(隐含默认显式化)。
172
+ 命中**域外值**时立即返回且**不降级**:显式声明了域外值 ≠ 无声明——继续回退到更弱的
173
+ 来源会把一个待 designer 裁决的冲突悄悄抹平。
174
+ """
175
+ if SOURCE_TAGS in sources:
176
+ for t in _tags(fm):
177
+ if t.startswith(ROLE_TAG_PREFIX):
178
+ v = t[len(ROLE_TAG_PREFIX):].strip().lower()
179
+ if v in ALL_ROLES:
180
+ return v, BASIS_TAG, SOURCE_TAGS, None
181
+ return None, None, None, _reason_key(REASON_TAG_OOD, v or "空")
182
+ if SOURCE_MAP in sources:
183
+ w = str(fm.get("writer") or "").strip().lower()
184
+ v = str((role_map or {}).get(w) or "").strip().lower() if w else ""
185
+ if v:
186
+ if v in WORK_ROLES:
187
+ # 映射表是**我们的推断**:把 knowledge 层节点推成工作角色会让它在默认
188
+ # 召回中被剔除(`_candidates` 默认排除 WORK_ROLES)——故意不做,红灯。
189
+ return None, None, None, _reason_key(REASON_MAP_WORK, v)
190
+ if v in ALL_ROLES:
191
+ return v, "WRITER_ROLE_MAP[%s]" % w, SOURCE_MAP, None
192
+ return None, None, None, _reason_key(REASON_MAP_OOD, v)
193
+ if SOURCE_LAYER in sources:
194
+ v = LAYER_DEFAULT_ROLE.get(str(e.get("layer") or ""))
195
+ if v:
196
+ return v, BASIS_LAYER, SOURCE_LAYER, None
197
+ w = str(fm.get("writer") or "").strip()
198
+ return None, None, None, (_reason_key(REASON_WRITER_NO_MAP, w) if w
199
+ else REASON_NO_SOURCE)
200
+
201
+
202
+ # 生效条件:按序判定——_readable_guard(cg, e) 为假返回 ("denied", None);direct_read(cg, e) 的 fm 为 None 返回 ("unreadable", None);crypto.is_encrypted(content) 为真返回 ("locked", None);fm.get("role") 非空白返回 ("present", None);否则 _candidate_role(e, fm, sources=sources, role_map=role_map) 的 role 为假返回 ("unfillable", {id,layer,reason,writer}),role 为真返回 ("", {id,layer,role,source,basis,before,had_key,writer})。
203
+ def _classify(cg, e, nid, *, sources, role_map) -> tuple:
204
+ """单条裁决 → `(skip_reason, item|gap)`;skip_reason 为空串表示可回填。"""
205
+ if not _readable_guard(cg, e):
206
+ return "denied", None
207
+ fm, content = direct_read(cg, e)
208
+ if fm is None:
209
+ return "unreadable", None
210
+ if crypto.is_encrypted(content):
211
+ return "locked", None
212
+ if not _blank(fm.get("role")):
213
+ return "present", None
214
+ role, basis, src, reason = _candidate_role(e, fm, sources=sources,
215
+ role_map=role_map)
216
+ if not role:
217
+ return "unfillable", {"id": nid, "layer": e.get("layer"),
218
+ "reason": reason, "writer": fm.get("writer")}
219
+ return "", {"id": nid, "layer": e.get("layer"), "role": role, "source": src,
220
+ "basis": basis, "before": fm.get("role"),
221
+ "had_key": "role" in fm, "writer": fm.get("writer")}
222
+
223
+
224
+ # 生效条件:want 由必填 layers 决定;layer 为真值时须属 layers 否则抛 ValueError,且命中后 want 收窄为 {layer};ids 为真值时只保留白名单内 nid、为假值(None/[])时不过滤;prefix 为真值时只保留 nid 以之开头者;仅 str(e.get("layer")) 属 want 的条目按 nid 升序进入返回列表。
225
+ def _iter_scope(cg, *, layers, layer=None, prefix=None, ids=None) -> list:
226
+ """按规则作用域遍历索引条目 → `[(nid, entry)]`(只读、nid 稳定序)。
227
+
228
+ `layer` 收窄只能收窄到规则已声明的层内——跨层即抛错(同 `ruleset` 的
229
+ 「执行时按同一条件过滤,绝不跨层套用」)。
230
+ """
231
+ want = set(layers)
232
+ if layer:
233
+ if layer not in want:
234
+ raise ValueError("layer=%s 不在靶规则作用域 %s 内(绝不跨层套用)"
235
+ % (layer, sorted(want)))
236
+ want = {layer}
237
+ idw = set(ids) if ids else None
238
+ out = []
239
+ for nid in sorted((cg.index.get("nodes") or {})):
240
+ if idw is not None and nid not in idw:
241
+ continue
242
+ if prefix and not nid.startswith(prefix):
243
+ continue
244
+ e = cg.index["nodes"][nid]
245
+ if str(e.get("layer")) not in want:
246
+ continue
247
+ out.append((nid, e))
248
+ return out
249
+
250
+
251
+ # ---- 预演 -----------------------------------------------------------------
252
+
253
+ # 生效条件:以 x 经 _as_cg 得 cg 并先经 role_rule(rules,rules_dir,rule_id) 取规则(缺规则/缺 layer/缺 field 时该步抛 ValueError),role_map 非空时的条目以小写去空白键合并进 WRITER_ROLE_MAP 副本,再遍历 _iter_scope(cg, layers=rule["layers"], layer=layer, prefix=prefix, ids=ids) 逐条 _classify 后返回不写盘的 rep;每条 item 仅在 limit is None 或 len(rep["items"]) < limit 时追加(limit=0 时 items 为空但 targeted 仍累加),sample 为真且 planned_ids 非空时以 max(1, len(planned_ids)//int(sample)) 为步长取前 int(sample) 项。
254
+ def role_plan(x, layer=None, limit=None, ids=None, prefix=None, *,
255
+ sources=DEFAULT_SOURCES, role_map=None, rule_id=RULE_ROLE,
256
+ rules=None, rules_dir=None, sample=0) -> dict:
257
+ """预演:产出可回填清单 + 不可回填原因分布,**不写盘**。
258
+
259
+ `sources` 见模块 docstring 的三档来源;`role_map` 覆盖 `WRITER_ROLE_MAP`
260
+ (designer 终裁补声明表的入口,不改库)。
261
+ """
262
+ cg = _as_cg(x)
263
+ rule = role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id)
264
+ rm = dict(WRITER_ROLE_MAP)
265
+ rm.update({str(k).strip().lower(): v for k, v in (role_map or {}).items()})
266
+ rep = {"root": cg.root, "dry_run": True, "action": ACTION_ROLE,
267
+ "rule": rule, "sources": list(sources), "role_map": rm,
268
+ "layer": layer, "prefix": prefix,
269
+ "nodes_scanned": 0, "skipped_out_of_scope": 0,
270
+ "skipped_locked": 0, "skipped_denied": 0, "skipped_unreadable": 0,
271
+ "skipped_present": 0, "unfillable": 0, "unfillable_by_reason": {},
272
+ "targeted": 0, "by_source": {}, "items": []}
273
+ scope_layers = set(rule["layers"])
274
+ idw = set(ids) if ids else None
275
+ for nid in sorted((cg.index.get("nodes") or {})):
276
+ if idw is not None and nid not in idw:
277
+ continue
278
+ if prefix and not str(nid).startswith(prefix):
279
+ continue
280
+ e = cg.index["nodes"][nid]
281
+ if str(e.get("layer")) not in scope_layers:
282
+ rep["skipped_out_of_scope"] += 1
283
+ for nid, e in _iter_scope(cg, layers=rule["layers"], layer=layer,
284
+ prefix=prefix, ids=ids):
285
+ rep["nodes_scanned"] += 1
286
+ skip, item = _classify(cg, e, nid, sources=sources, role_map=rm)
287
+ if skip == "unfillable":
288
+ rep["unfillable"] += 1
289
+ # 原因键带明细(如 writer_no_mapping:dsh-memory)——聚合即得逐来源分布
290
+ _bump(rep["unfillable_by_reason"], str(item["reason"]))
291
+ continue
292
+ if skip:
293
+ rep["skipped_%s" % skip] = rep.get("skipped_%s" % skip, 0) + 1
294
+ continue
295
+ item["entry_id"] = _entry_id(ACTION_ROLE, nid)
296
+ item["ref"] = e.get("ref")
297
+ rep["targeted"] += 1
298
+ _bump(rep["by_source"], item["source"])
299
+ if limit is None or len(rep["items"]) < limit:
300
+ rep["items"].append(item)
301
+ rep["planned_ids"] = [i["id"] for i in rep["items"]]
302
+ if sample and rep["planned_ids"]:
303
+ step = max(1, len(rep["planned_ids"]) // int(sample))
304
+ rep["sample"] = rep["planned_ids"][::step][:int(sample)]
305
+ rep["rule_layers"] = sorted(scope_layers)
306
+ return rep
307
+
308
+
309
+ # ---- 执行 / 回滚 / 留痕 / 对照 -------------------------------------------
310
+
311
+ # 生效条件:x 经 _as_cg,batch 为假值时回落 BATCH_DEFAULT;先调 role_plan 取 items,entry_ids 为真值时按 entry_id 收窄;逐项处理:索引无该节点或 _read 得 fm 为 None → skipped_missing,_readable_guard 为假 → skipped_denied,crypto.is_encrypted(content) 为真 → skipped_locked,fm["role"] 非空 → skipped_drift,否则写回 role 并 append_jsonl 留痕、written 递增;written 非零时 cg.rebuild_index(),返回含 plan_remaining 的 rep。
312
+ def role_apply(x, ids=None, entry_ids=None, layer=None, limit=None,
313
+ batch=BATCH_DEFAULT, sources=DEFAULT_SOURCES, role_map=None,
314
+ rule_id=RULE_ROLE, rules=None, rules_dir=None, actor=None,
315
+ prefix=None) -> dict:
316
+ """执行回填:逐节点改写 md 的 role 字段,写 `_govern.jsonl` 留痕。
317
+
318
+ 写前**重查**一次(预演 → 执行之间节点可能被改动):已有 role 值 → `skipped_drift`
319
+ 一律不动;密文 / 不可读 → `skipped_locked` / `skipped_missing`,**绝不解密回写**。
320
+ `ids` / `prefix` / `entry_ids` 三重收窄,`entry_ids` 优先(工单式精确执行)。
321
+ """
322
+ cg = _as_cg(x)
323
+ batch = batch or BATCH_DEFAULT
324
+ p = role_plan(cg, layer=layer, limit=limit, ids=ids, prefix=prefix,
325
+ sources=sources, role_map=role_map, rule_id=rule_id,
326
+ rules=rules, rules_dir=rules_dir)
327
+ items = p["items"]
328
+ if entry_ids:
329
+ want = set(entry_ids)
330
+ items = [i for i in items if i["entry_id"] in want]
331
+ rep = {"root": cg.root, "dry_run": False, "action": ACTION_ROLE,
332
+ "batch": batch, "actor": actor, "rule_id": rule_id,
333
+ "field": p["rule"]["field"], "sources": list(sources),
334
+ "planned": len(items), "written": 0, "by_source": {},
335
+ "skipped_drift": 0, "skipped_locked": 0, "skipped_missing": 0,
336
+ "skipped_denied": 0, "entry_ids": [],
337
+ "unfillable": p["unfillable"],
338
+ "unfillable_by_reason": p["unfillable_by_reason"]}
339
+ for it in items:
340
+ nid = it["id"]
341
+ e = (cg.index.get("nodes") or {}).get(nid)
342
+ if e is None:
343
+ rep["skipped_missing"] += 1
344
+ continue
345
+ if not _readable_guard(cg, e):
346
+ rep["skipped_denied"] += 1
347
+ continue
348
+ fm, content = direct_read(cg, e)
349
+ if fm is None:
350
+ rep["skipped_missing"] += 1
351
+ continue
352
+ if crypto.is_encrypted(content):
353
+ rep["skipped_locked"] += 1
354
+ continue
355
+ if not _blank(fm.get("role")):
356
+ rep["skipped_drift"] += 1
357
+ continue
358
+ fm_before = {"role": fm.get("role"), "had_key": "role" in fm}
359
+ fm["role"] = it["role"]
360
+ ts = time.time()
361
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
362
+ durable=True)
363
+ wid = _sha("%s|%s|%.6f" % (nid, batch, ts))
364
+ append_jsonl(_log_path(cg), {
365
+ "action": ACTION_ROLE, "ts": ts, "batch": batch, "actor": actor,
366
+ "entry_id": it["entry_id"], "write_id": wid, "node": nid,
367
+ "layer": e.get("layer"), "rule_id": rule_id, "field": "role",
368
+ "value": it["role"], "source": it["source"], "basis": it["basis"],
369
+ "writer": it.get("writer"), "fm_before": fm_before,
370
+ "content_hash_after": _sha(content)})
371
+ rep["written"] += 1
372
+ _bump(rep["by_source"], it["source"])
373
+ rep["entry_ids"].append(it["entry_id"])
374
+ if rep["written"]:
375
+ cg.rebuild_index()
376
+ rep["plan_remaining"] = max(0, p["targeted"] - len(items))
377
+ return rep
378
+
379
+
380
+ # 生效条件:只处理 action == ACTION_ROLE 的留痕,且 batch 为真时要求 rec.get("batch") == batch(batch 为 None/假值时不按批次过滤)、entry_ids 为真时要求 rec.get("entry_id") ∈ set(entry_ids);rec 的 write_id 已在既有 ACTION_ROLE_ROLLBACK 记录中 → skipped_done,节点不在索引或 read 不到或加密 → missing,当前 fm.get("role") 为空白或与 rec.get("value") 不等 → conflict 不覆盖,相等时按 fm_before.get("had_key") 恢复 fm_before["role"] 或删除 role 键;reverted 非 0 时 cg.rebuild_index()。
381
+ def role_rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
382
+ """按留痕反向应用:撤销 role 回填。
383
+
384
+ **只在当前值仍等于当初写入值时**撤销;否则计 `conflict` 不覆盖(此后可能已有
385
+ 人工修正 / 后续批次写入)。原本无该键 → 删键还原(不是写空串)。
386
+ """
387
+ cg = _as_cg(x)
388
+ want = set(entry_ids) if entry_ids else None
389
+ rep = {"root": cg.root, "action": ACTION_ROLE_ROLLBACK, "actor": actor,
390
+ "batch": batch, "reverted": 0, "conflict": 0, "missing": 0,
391
+ "skipped_done": 0, "entry_ids": []}
392
+ log = list(read_jsonl(_log_path(cg)) or [])
393
+ done = {r.get("write_id") for r in log
394
+ if r.get("action") == ACTION_ROLE_ROLLBACK and r.get("write_id")}
395
+ for rec in log:
396
+ if rec.get("action") != ACTION_ROLE:
397
+ continue
398
+ if batch and rec.get("batch") != batch:
399
+ continue
400
+ if want is not None and rec.get("entry_id") not in want:
401
+ continue
402
+ wid = rec.get("write_id")
403
+ if wid and wid in done:
404
+ rep["skipped_done"] += 1
405
+ continue
406
+ nid = rec.get("node")
407
+ e = (cg.index.get("nodes") or {}).get(nid)
408
+ if e is None:
409
+ rep["missing"] += 1
410
+ continue
411
+ fm, content = direct_read(cg, e)
412
+ if fm is None or crypto.is_encrypted(content):
413
+ rep["missing"] += 1
414
+ continue
415
+ if _blank(fm.get("role")) or str(fm.get("role")) != str(rec.get("value")):
416
+ rep["conflict"] += 1
417
+ continue
418
+ before = rec.get("fm_before") or {}
419
+ if before.get("had_key"):
420
+ fm["role"] = before.get("role")
421
+ else:
422
+ fm.pop("role", None)
423
+ cg._write_node(nid, os.path.join(cg.root, e["path"]), fm, content,
424
+ durable=True)
425
+ append_jsonl(_log_path(cg), {
426
+ "action": ACTION_ROLE_ROLLBACK, "ts": time.time(), "actor": actor,
427
+ "batch": rec.get("batch"), "entry_id": rec.get("entry_id"),
428
+ "write_id": wid, "node": nid, "field": "role",
429
+ "restored": before.get("role"),
430
+ "had_key": bool(before.get("had_key")), "reason": "rollback"})
431
+ rep["reverted"] += 1
432
+ rep["entry_ids"].append(rec.get("entry_id"))
433
+ if rep["reverted"]:
434
+ cg.rebuild_index()
435
+ return rep
436
+
437
+
438
+ # 生效条件:action 为真时只保留 r.get("action") == action 的记录、batch 为真时只保留 r.get("batch") == batch 的记录,total/by_action 统计的是过滤后的 recs 全量;records 在 limit 为假值(0/None)时返回全部 recs,否则返回 recs[-int(limit):]。
439
+ def history(x, limit=100, action=None, batch=None) -> dict:
440
+ """读 `_govern.jsonl` 留痕(治理动作的可审计面)。"""
441
+ cg = _as_cg(x)
442
+ recs = [r for r in (read_jsonl(_log_path(cg)) or [])
443
+ if (not action or r.get("action") == action)
444
+ and (not batch or r.get("batch") == batch)]
445
+ tail = recs[-int(limit):] if limit else recs
446
+ by_action = {}
447
+ for r in recs:
448
+ _bump(by_action, str(r.get("action")))
449
+ return {"root": cg.root, "action": "role_history", "total": len(recs),
450
+ "by_action": by_action, "records": tail}
451
+
452
+
453
+ # 生效条件:x 经 _as_cg 且 role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id) 命中并声明了 matcher.layer 与 field_absent.field 时,返回以 conformance.load_index(cg.root) 的节点为口径的统计(by_layer 计全部节点、by_value 只计 role 非空白者、target_met 由 role_ratio_kn >= float(THRESHOLDS["role_coverage_min"]) 决定、gap_to_target 取 max(0.0, thr-kn));规则缺失或声明不全时在 role_rule 处抛 ValueError。
454
+ def role_stats(x, *, rule_id=RULE_ROLE, rules=None, rules_dir=None) -> dict:
455
+ """只读对照:role 覆盖率 + 取值分布(治理前后量化用)。
456
+
457
+ 判据**不复刻**——直接复用 `conformance`(真源:`_coverage_metrics` 的
458
+ knowledge 层口径 `role_ratio_kn` 与 `THRESHOLDS["role_coverage_min"]`),
459
+ 本函数只补「逐层 / 逐值」分布,供 M4 复跑报告引用。
460
+ """
461
+ from .. import conformance as CONF # 延迟导入:读侧判据真源,按需加载
462
+ cg = _as_cg(x)
463
+ rule = role_rule(rules=rules, rules_dir=rules_dir, rule_id=rule_id)
464
+ nodes = CONF.load_index(cg.root)
465
+ cov = CONF._coverage_metrics(nodes)
466
+ thr = float(CONF.THRESHOLDS["role_coverage_min"])
467
+ by_layer, by_value = {}, {}
468
+ for e in nodes.values():
469
+ _bump(by_layer, str(e.get("layer")))
470
+ if not _blank(e.get("role")):
471
+ _bump(by_value, str(e.get("role")))
472
+ kn = float(cov.get("role_ratio_kn") or 0.0)
473
+ return {"root": cg.root, "action": "role_stats", "rule_id": rule_id,
474
+ "layers": rule["layers"], "nodes": cov.get("nodes"),
475
+ "knowledge": cov.get("knowledge"), "role_ratio": cov.get("role_ratio"),
476
+ "role_ratio_kn": cov.get("role_ratio_kn"), "threshold": thr,
477
+ "target_met": kn >= thr, "gap_to_target": max(0.0, thr - kn),
478
+ "by_layer": by_layer, "by_value": by_value}
479
+
480
+
481
+ # ---- 统一入口 ------------------------------------------------------------
482
+
483
+ # 生效条件:action 不在 ACTIONS 内时抛 ValueError;action == ACTION_ROLE 时以 kw.pop("apply", False) 为真调用 role_apply(x, **kw)、为假(含缺该键)调用 role_plan(x, **kw);其余情况以 {ACTION_ROLE_ROLLBACK: role_rollback, "role_history": history, "role_stats": role_stats}[action] 取 fn 并返回 fn(x, **kw)。
484
+ def run(x, action, **kw) -> dict:
485
+ """`role`(`apply=True` 则执行)/ `role_rollback` / `role_history` / `role_stats`。"""
486
+ if action not in ACTIONS:
487
+ raise ValueError("未知 action=%s(可用:%s)" % (action, list(ACTIONS)))
488
+ if action == ACTION_ROLE:
489
+ if kw.pop("apply", False):
490
+ return role_apply(x, **kw)
491
+ return role_plan(x, **kw)
492
+ fn = {ACTION_ROLE_ROLLBACK: role_rollback, "role_history": history,
493
+ "role_stats": role_stats}[action]
494
+ return fn(x, **kw)
495
+
496
+
497
+ # ---- 命令行 --------------------------------------------------------------
498
+ #
499
+ # 形态约定:`--root` / `--rules-dir` / `--json` 均为**子命令参数**(`parents=[common]`),
500
+ # 须写在子命令**之后**:`govern plan --root <root>`。写在子命令之前会被顶层解析器
501
+ # 当作位置参数吃掉(`scripts/review_cli.py` 的同型坑,2026-09-15 实测更正)。
502
+
503
+ # 生效条件:s 为假值(None/空串)时直接返回 DEFAULT_SOURCES;否则按 "," 切分并 strip 丢弃空段,任一段不在 ALL_SOURCES 内即抛 ValueError,切分后 vals 为空(如 s=",")也抛 ValueError,其余返回该非空元组。
504
+ def _parse_sources(s) -> tuple:
505
+ """CLI 侧来源档解析:逗号分隔,值域封闭(非法即抛,不静默降级)。"""
506
+ if not s:
507
+ return DEFAULT_SOURCES
508
+ vals = tuple(v.strip() for v in str(s).split(",") if v.strip())
509
+ bad = [v for v in vals if v not in ALL_SOURCES]
510
+ if bad:
511
+ raise ValueError("未知来源档 %s(可用:%s)" % (bad, list(ALL_SOURCES)))
512
+ if not vals:
513
+ raise ValueError("sources 为空(缺省即 %s)" % ",".join(DEFAULT_SOURCES))
514
+ return vals
515
+
516
+
517
+ # 生效条件:无参数,调用即返回 argparse.ArgumentParser(add_help=False),其中已含 --root(default=None)、--rules-dir(default=None)、--json(store_true) 三项,无分支。
518
+ def _common_parser() -> argparse.ArgumentParser:
519
+ ap = argparse.ArgumentParser(add_help=False)
520
+ ap.add_argument("--root", default=None, help="真源根(缺省读环境变量 MDCG_ROOT)")
521
+ ap.add_argument("--rules-dir", default=None, help="规则库目录(缺省 mreview/rules)")
522
+ ap.add_argument("--json", action="store_true", help="输出 JSON(缺省人读摘要)")
523
+ return ap
524
+
525
+
526
+ # 生效条件:传入 p 即向它注册 --layer/--prefix/--ids/--limit(type=int)/--sources 五个参数,无分支且无返回值。
527
+ def _add_scope_args(p) -> None:
528
+ p.add_argument("--layer", help="收窄到该层(须在规则 matcher 层之内)")
529
+ p.add_argument("--prefix", help="只处理该 id 前缀")
530
+ p.add_argument("--ids", help="逗号分隔的节点 id 白名单")
531
+ p.add_argument("--limit", type=int, help="本次最多处理条数")
532
+ p.add_argument("--sources",
533
+ help="来源档逗号分隔:tags,map[,layer_default](缺省 %s)"
534
+ % ",".join(DEFAULT_SOURCES))
535
+
536
+
537
+ # 生效条件:传入 a 后返回 {'layer': a.layer, 'prefix': a.prefix, 'limit': a.limit, 'ids': [...], 'sources': _parse_sources(a.sources), 'rules_dir': a.rules_dir},其中 ids 由 a.ids(为假值即 None/空串)按 "," 切分去空段、结果为空则回落 None,sources 由 a.sources 经 _parse_sources 解析(假值 → DEFAULT_SOURCES,非法值抛 ValueError)。
538
+ def _scope_kw(a) -> dict:
539
+ return {"layer": a.layer, "prefix": a.prefix, "limit": a.limit,
540
+ "ids": [i.strip() for i in (a.ids or "").split(",") if i.strip()] or None,
541
+ "sources": _parse_sources(a.sources), "rules_dir": a.rules_dir}
542
+
543
+
544
+ # 生效条件:SOURCE_LAYER 在 sources 内时向 sys.stderr 打印 "警告:" + LAYER_DEFAULT_WARNING,否则不输出。
545
+ def _warn_layer_default(sources) -> None:
546
+ if SOURCE_LAYER in sources:
547
+ print("警告:" + LAYER_DEFAULT_WARNING, file=sys.stderr)
548
+
549
+
550
+ # 生效条件:传入 msg 即向 sys.stderr 打印该 msg 并返回 2,无分支。
551
+ def _die(msg: str) -> int:
552
+ print(msg, file=sys.stderr)
553
+ return 2
554
+
555
+
556
+ def main(argv=None) -> int:
557
+ ap = argparse.ArgumentParser(
558
+ prog="python -m md_cg.mreview.govern",
559
+ description="记忆评审 M4 · 落库治理(确定性部分:rule 驱动 · 永不删除)")
560
+ sub = ap.add_subparsers(dest="cmd")
561
+ common = _common_parser()
562
+
563
+ p = sub.add_parser("plan", parents=[common], help="预演(只出报表,不写盘)")
564
+ _add_scope_args(p)
565
+ p.add_argument("--sample", type=int, default=0, help="抽样条数(人工核对样板)")
566
+
567
+ p = sub.add_parser("apply", parents=[common], help="执行回填(写盘 + 留痕 + 可回滚)")
568
+ _add_scope_args(p)
569
+ p.add_argument("--batch", default=BATCH_DEFAULT, help="批次名(留痕 / 回滚锚点)")
570
+ p.add_argument("--actor", default=None, help="执行者署名(留痕)")
571
+ p.add_argument("--entry-id", action="append", default=[],
572
+ help="只执行该条目(可多次;工单式精确执行)")
573
+ p.add_argument("--yes", action="store_true", help="显式确认写盘(缺省拒绝)")
574
+
575
+ p = sub.add_parser("rollback", parents=[common], help="按留痕回滚")
576
+ p.add_argument("--batch", default=None, help="只回滚该批次")
577
+ p.add_argument("--entry-id", action="append", default=[], help="只回滚该条目(可多次)")
578
+ p.add_argument("--actor", default=None, help="执行者署名(留痕)")
579
+
580
+ p = sub.add_parser("history", parents=[common], help="读 `_govern.jsonl` 留痕")
581
+ p.add_argument("--limit", type=int, default=100)
582
+ p.add_argument("--batch", default=None)
583
+ p.add_argument("--action", default=None)
584
+
585
+ sub.add_parser("stats", parents=[common],
586
+ help="覆盖率对照(复用 conformance 判据)")
587
+
588
+ a = ap.parse_args(argv)
589
+ if not a.cmd:
590
+ # `--root` 是**子命令**参数(`parents=[common]`):无子命令时顶层 Namespace
591
+ # 根本没有该属性——须先判 cmd 再取 root,否则 `govern` 裸跑即 AttributeError。
592
+ ap.print_help()
593
+ return 2
594
+ root = a.root or os.environ.get("MDCG_ROOT")
595
+ if not root:
596
+ return _die("需要 --root 或环境变量 MDCG_ROOT(fail-closed)")
597
+
598
+ if a.cmd == "plan":
599
+ try:
600
+ kw = _scope_kw(a)
601
+ except ValueError as ex:
602
+ return _die(str(ex))
603
+ _warn_layer_default(kw["sources"])
604
+ try:
605
+ rep = role_plan(root, sample=a.sample, **kw)
606
+ except ValueError as ex:
607
+ return _die("预演失败:%s" % ex)
608
+ if a.json:
609
+ print(json.dumps(rep, ensure_ascii=False, indent=2))
610
+ return 0
611
+ print("[M4 预演·只读] root=%s" % rep["root"])
612
+ print(" 靶规则 %s|层 %s|字段 %s|来源档 %s"
613
+ % (rep["rule"]["rule_id"], ",".join(rep["rule_layers"]),
614
+ rep["rule"]["field"], ",".join(rep["sources"])))
615
+ print(" 扫描 %d|可回填 %d|不可回填 %d|域外 %d|已锁 %d|已有值 %d|无权限 %d"
616
+ % (rep["nodes_scanned"], rep["targeted"], rep["unfillable"],
617
+ rep["skipped_out_of_scope"], rep["skipped_locked"],
618
+ rep["skipped_present"], rep["skipped_denied"]))
619
+ for k, v in sorted(rep["by_source"].items()):
620
+ print(" 来源 %-16s %d" % (k, v))
621
+ for k, v in sorted(rep["unfillable_by_reason"].items(), key=lambda kv: -kv[1]):
622
+ print(" 挡下 %-32s %d" % (k, v))
623
+ if rep.get("sample"):
624
+ print(" 抽样:%s" % ", ".join(rep["sample"]))
625
+ return 0
626
+
627
+ if a.cmd == "apply":
628
+ if not a.yes:
629
+ return _die("apply 会写盘:先跑 plan 核对清单,再以 --yes 显式确认(fail-closed)")
630
+ try:
631
+ kw = _scope_kw(a)
632
+ except ValueError as ex:
633
+ return _die(str(ex))
634
+ _warn_layer_default(kw["sources"])
635
+ try:
636
+ rep = role_apply(root, batch=a.batch, actor=a.actor,
637
+ entry_ids=list(a.entry_id) or None, **kw)
638
+ except ValueError as ex:
639
+ return _die("执行失败:%s" % ex)
640
+ if a.json:
641
+ print(json.dumps(rep, ensure_ascii=False, indent=2))
642
+ return 0
643
+ print("[M4 执行] batch=%s actor=%s" % (rep["batch"], rep["actor"]))
644
+ print(" 写入 %d|漂移跳过 %d|密文跳过 %d|缺失 %d|无权限 %d|余量 %d"
645
+ % (rep["written"], rep["skipped_drift"], rep["skipped_locked"],
646
+ rep["skipped_missing"], rep["skipped_denied"], rep["plan_remaining"]))
647
+ for k, v in sorted(rep["by_source"].items()):
648
+ print(" 来源 %-16s %d" % (k, v))
649
+ return 0
650
+
651
+ if a.cmd == "rollback":
652
+ if not a.batch and not a.entry_id:
653
+ return _die("rollback 需要 --batch 或 --entry-id(防全量误回滚)")
654
+ rep = role_rollback(root, batch=a.batch, actor=a.actor,
655
+ entry_ids=list(a.entry_id) or None)
656
+ if a.json:
657
+ print(json.dumps(rep, ensure_ascii=False, indent=2))
658
+ return 0
659
+ print("[M4 回滚] batch=%s|还原 %d|冲突跳过 %d|缺失 %d|已回滚 %d"
660
+ % (rep["batch"], rep["reverted"], rep["conflict"], rep["missing"],
661
+ rep["skipped_done"]))
662
+ return 0
663
+
664
+ if a.cmd == "history":
665
+ rep = history(root, limit=a.limit, action=a.action, batch=a.batch)
666
+ if a.json:
667
+ print(json.dumps(rep, ensure_ascii=False, indent=2))
668
+ return 0
669
+ print("[M4 留痕] 命中 %d 条|分布 %s" % (rep["total"], rep["by_action"]))
670
+ for r in rep["records"]:
671
+ print(" %-14s %-10s %-22s %s=%s"
672
+ % (time.strftime("%m-%d %H:%M:%S", time.localtime(r.get("ts") or 0)),
673
+ r.get("action"), str(r.get("node")), r.get("field"),
674
+ r.get("value", r.get("restored"))))
675
+ return 0
676
+
677
+ try:
678
+ rep = role_stats(root)
679
+ except ValueError as ex:
680
+ return _die("对照失败:%s" % ex)
681
+ if a.json:
682
+ print(json.dumps(rep, ensure_ascii=False, indent=2))
683
+ return 0
684
+ print("[M4 对照] root=%s 判据源=conformance" % rep["root"])
685
+ print(" 节点 %s|knowledge %s|role 覆盖 %s|knowledge 层覆盖 %s(阈值 %.2f,%s)"
686
+ % (rep["nodes"], rep["knowledge"], rep["role_ratio"],
687
+ rep["role_ratio_kn"], rep["threshold"],
688
+ "达标" if rep["target_met"] else "未达标,缺口 %.4f" % rep["gap_to_target"]))
689
+ print(" 逐层 %s" % rep["by_layer"])
690
+ print(" 逐值 %s" % rep["by_value"])
691
+ return 0
692
+
693
+
694
+ if __name__ == "__main__":
694
695
  sys.exit(main())