@furongjun1999/dsh-memory 0.4.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (504) hide show
  1. package/README.md +16 -16
  2. package/codebuddy/CODEBUDDY.md +11 -3
  3. package/codebuddy/README.md +92 -90
  4. package/codebuddy/mcp.json +27 -27
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -169
  6. package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -144
  7. package/docs/README.md +142 -111
  8. package/docs/discipline/harnesses.yaml +244 -226
  9. package/docs/discipline/templates/full.md.tmpl +61 -61
  10. package/docs/discipline/templates/rules.mdc.tmpl +68 -0
  11. package/docs/discipline/templates/skill.md.tmpl +23 -23
  12. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md +299 -299
  13. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md +239 -239
  14. package/docs/eval//345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md +174 -174
  15. package/docs/eval//346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md +223 -223
  16. package/docs/{mdcg → hive}//344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md +165 -160
  17. package/docs/hive//344/273/244/347/211/214/350/257/255/344/271/211/344/277/256/346/255/243_/344/273/262/350/243/201/344/275/215_v0.1.md +76 -0
  18. package/docs/{mdcg → hive}//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.5.md +236 -236
  19. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -43
  20. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +85 -0
  21. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -102
  22. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +47 -0
  23. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -503
  24. package/docs/hive//350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +85 -85
  25. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +191 -0
  26. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +90 -0
  27. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -216
  28. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
  29. package/docs/mdcg/lingshu_tutorial.html +14449 -14449
  30. package/docs/mdcg/release_v0.4.11.md +49 -0
  31. package/docs/mdcg/release_v0.4.5.md +55 -55
  32. package/docs/mdcg/tool_table_v0.3.0.md +117 -117
  33. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -600
  34. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  35. package/docs/mdcg//345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +275 -275
  36. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
  37. package/docs/mdcg//346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +130 -130
  38. package/docs/mdcg//347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md +278 -278
  39. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -172
  40. package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
  41. package/docs/mdcg//350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +491 -491
  42. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md +340 -340
  43. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +588 -588
  44. package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -176
  45. package/docs/plans//345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +81 -81
  46. package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
  47. package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -242
  48. package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -129
  49. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -134
  50. package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  51. package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -145
  52. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +144 -144
  53. package/docs/theory//347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +198 -198
  54. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -221
  55. package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -237
  56. package/docs//345/217/221/345/270/203/344/273/266/345/233/236/346/272/257/350/257/264/346/230/216_v0.1.md +82 -82
  57. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +28 -2
  58. package/dsh/README.md +82 -82
  59. package/dsh/cordis.yml.example +139 -139
  60. package/dsh/update-lingshu.bat +11 -11
  61. package/lib/hooks.js +36 -2
  62. package/lib/lib/roleplay_web.js +427 -427
  63. package/md_cg/__init__.py +7 -7
  64. package/md_cg/audit.py +368 -368
  65. package/md_cg/autonomy.py +287 -287
  66. package/md_cg/backfill.py +1327 -1327
  67. package/md_cg/backfill_bigdomain.py +34 -34
  68. package/md_cg/bench6_arms.py +410 -410
  69. package/md_cg/bench6_common.py +230 -230
  70. package/md_cg/bench6_competitors.py +212 -212
  71. package/md_cg/bench_axis_domain.py +257 -257
  72. package/md_cg/bench_blind_comp.py +308 -308
  73. package/md_cg/bench_en_atoms_public.py +230 -230
  74. package/md_cg/bench_governance.py +348 -348
  75. package/md_cg/bench_lme_zh.py +410 -410
  76. package/md_cg/bench_locomo.py +121 -121
  77. package/md_cg/bench_locomo_zh.py +450 -450
  78. package/md_cg/bench_locomo_zh_public.py +147 -147
  79. package/md_cg/bench_longmem.py +112 -112
  80. package/md_cg/bench_membench.py +632 -632
  81. package/md_cg/bench_p0.py +149 -149
  82. package/md_cg/bench_progressive.py +287 -287
  83. package/md_cg/bench_role_views.py +238 -238
  84. package/md_cg/bench_task_ab.py +243 -243
  85. package/md_cg/bench_task_ab_llm.py +408 -408
  86. package/md_cg/bench_unified_en.py +204 -204
  87. package/md_cg/bench_zh_mad.py +601 -601
  88. package/md_cg/blindspot_tickets.py +123 -123
  89. package/md_cg/branches.py +285 -285
  90. package/md_cg/build_postings.py +73 -73
  91. package/md_cg/ccgc.py +1005 -948
  92. package/md_cg/census.py +132 -132
  93. package/md_cg/chain.py +300 -300
  94. package/md_cg/codeindex.py +531 -531
  95. package/md_cg/coldverify.py +292 -292
  96. package/md_cg/comment_gate.py +337 -337
  97. package/md_cg/cond_compose.py +190 -190
  98. package/md_cg/cond_facts.py +154 -154
  99. package/md_cg/cond_template.json +106 -106
  100. package/md_cg/condition_anchor.py +142 -142
  101. package/md_cg/conformance.py +726 -726
  102. package/md_cg/consistency.py +717 -717
  103. package/md_cg/consolidate.py +1536 -1439
  104. package/md_cg/corpus.py +110 -110
  105. package/md_cg/crosscheck.py +1097 -1097
  106. package/md_cg/crypto.py +437 -437
  107. package/md_cg/d_meta.py +310 -310
  108. package/md_cg/datapath.py +334 -334
  109. package/md_cg/docindex.py +473 -473
  110. package/md_cg/eval_common.py +575 -575
  111. package/md_cg/evidence.py +580 -580
  112. package/md_cg/evolution.py +477 -477
  113. package/md_cg/export.py +220 -220
  114. package/md_cg/forgetting.py +581 -581
  115. package/md_cg/fsutil.py +329 -329
  116. package/md_cg/hotcache.py +238 -214
  117. package/md_cg/hyperedge.py +251 -251
  118. package/md_cg/identity.py +390 -390
  119. package/md_cg/insight.py +500 -500
  120. package/md_cg/interop.py +199 -0
  121. package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
  122. package/md_cg/lexicon/build_standard_en.py +171 -171
  123. package/md_cg/lexicon/expand_en_zh.py +211 -211
  124. package/md_cg/lifecycle.py +272 -272
  125. package/md_cg/linkref.py +280 -280
  126. package/md_cg/links.py +622 -622
  127. package/md_cg/mcp_server.py +129 -32
  128. package/md_cg/md_whitebox.py +345 -345
  129. package/md_cg/mdcg.py +176 -117
  130. package/md_cg/mdcos.py +79 -13
  131. package/md_cg/metacognition.py +591 -591
  132. package/md_cg/migrate.py +119 -119
  133. package/md_cg/migrate_aeis.py +221 -221
  134. package/md_cg/migrate_roleplay.py +293 -293
  135. package/md_cg/migrate_wisdom_graph.py +360 -360
  136. package/md_cg/mreview/__init__.py +25 -25
  137. package/md_cg/mreview/__main__.py +110 -110
  138. package/md_cg/mreview/bundle.py +178 -178
  139. package/md_cg/mreview/candidates.py +262 -262
  140. package/md_cg/mreview/govern.py +693 -693
  141. package/md_cg/mreview/locate.py +939 -939
  142. package/md_cg/mreview/pipeline.py +728 -728
  143. package/md_cg/mreview/rules/duplication.json +21 -21
  144. package/md_cg/mreview/rules/field_coverage.json +54 -54
  145. package/md_cg/mreview/rules/source_license.json +21 -21
  146. package/md_cg/mreview/rules/template_flow.json +21 -21
  147. package/md_cg/mreview/ruleset.py +252 -252
  148. package/md_cg/nodefile.py +575 -575
  149. package/md_cg/pooling.py +484 -472
  150. package/md_cg/postings.py +298 -298
  151. package/md_cg/predict.py +1100 -1100
  152. package/md_cg/progressive.py +123 -123
  153. package/md_cg/protect.py +272 -272
  154. package/md_cg/protocol/md_cg_gate.proto +33 -33
  155. package/md_cg/protocol.py +372 -372
  156. package/md_cg/provenance.py +582 -582
  157. package/md_cg/reach.py +453 -453
  158. package/md_cg/readcache.py +85 -0
  159. package/md_cg/refindex.py +833 -833
  160. package/md_cg/refine.py +604 -604
  161. package/md_cg/roleviews.py +89 -89
  162. package/md_cg/routing.py +365 -365
  163. package/md_cg/scrub.py +852 -852
  164. package/md_cg/security.py +274 -274
  165. package/md_cg/self_state.py +1029 -1029
  166. package/md_cg/selfreport.py +151 -151
  167. package/md_cg/semantic/__init__.py +10 -10
  168. package/md_cg/semantic/canonical.py +122 -122
  169. package/md_cg/semantic/en_normalizer.py +364 -364
  170. package/md_cg/semantic/en_zh_map.json +28694 -0
  171. package/md_cg/semantic/export_en_zh_map.py +64 -0
  172. package/md_cg/semantic/unify.py +45 -0
  173. package/md_cg/semantic/zh_en_atoms.py +139 -139
  174. package/md_cg/signer.py +562 -562
  175. package/md_cg/sources.py +815 -582
  176. package/md_cg/statushdr.py +179 -179
  177. package/md_cg/stg.py +48 -37
  178. package/md_cg/subgraph.py +729 -729
  179. package/md_cg/sustain.py +1138 -1138
  180. package/md_cg/tasks.py +470 -470
  181. package/md_cg/test_action_derive.py +203 -203
  182. package/md_cg/test_audit_rotate.py +270 -270
  183. package/md_cg/test_autonomy.py +143 -143
  184. package/md_cg/test_bench_governance.py +102 -102
  185. package/md_cg/test_blindspot_tickets.py +166 -166
  186. package/md_cg/test_branches.py +249 -249
  187. package/md_cg/test_ccg_perturb.py +184 -184
  188. package/md_cg/test_ccgc.py +433 -433
  189. package/md_cg/test_census_prune.py +81 -81
  190. package/md_cg/test_cond_compose_anchors.py +76 -76
  191. package/md_cg/test_cond_match.py +165 -165
  192. package/md_cg/test_condition_anchor.py +81 -81
  193. package/md_cg/test_d_meta.py +412 -412
  194. package/md_cg/test_datapath_root.py +199 -199
  195. package/md_cg/test_en_pipeline.py +166 -166
  196. package/md_cg/test_gain_gate.py +212 -212
  197. package/md_cg/test_health_scale.py +173 -173
  198. package/md_cg/test_hive_ingest.py +285 -0
  199. package/md_cg/test_hot_cold.py +215 -215
  200. package/md_cg/test_hyperedge.py +245 -245
  201. package/md_cg/test_i26_empty_first_write.py +116 -0
  202. package/md_cg/test_i27_e041_identity.py +128 -0
  203. package/md_cg/test_i28_hotcache_prodpath.py +122 -0
  204. package/md_cg/test_identity_attribution.py +147 -147
  205. package/md_cg/test_index_durability.py +224 -224
  206. package/md_cg/test_interop.py +93 -0
  207. package/md_cg/test_lifecycle.py +309 -309
  208. package/md_cg/test_linkref.py +306 -306
  209. package/md_cg/test_lock.py +43 -43
  210. package/md_cg/test_md_access_parity.py +255 -255
  211. package/md_cg/test_md_writepath.py +345 -345
  212. package/md_cg/test_mdstore_search_parity.py +160 -0
  213. package/md_cg/test_mr_m2.py +587 -587
  214. package/md_cg/test_mr_m3.py +710 -710
  215. package/md_cg/test_mr_m4.py +485 -485
  216. package/md_cg/test_p0.py +250 -250
  217. package/md_cg/test_p1.py +316 -316
  218. package/md_cg/test_p10_identity.py +173 -173
  219. package/md_cg/test_p11_consistency.py +233 -233
  220. package/md_cg/test_p12_metacognition.py +212 -212
  221. package/md_cg/test_p13_encryption.py +241 -241
  222. package/md_cg/test_p14_sustain.py +249 -249
  223. package/md_cg/test_p15_scrub.py +280 -280
  224. package/md_cg/test_p16_self_state.py +301 -301
  225. package/md_cg/test_p17_predict.py +354 -354
  226. package/md_cg/test_p18_whitebox.py +171 -171
  227. package/md_cg/test_p19_migrate_roleplay.py +149 -149
  228. package/md_cg/test_p20_evolution.py +315 -315
  229. package/md_cg/test_p21_tokens.py +293 -270
  230. package/md_cg/test_p22_theory.py +175 -175
  231. package/md_cg/test_p23_links.py +311 -311
  232. package/md_cg/test_p24_evidence.py +227 -227
  233. package/md_cg/test_p25_weights.py +156 -156
  234. package/md_cg/test_p26_refindex.py +416 -416
  235. package/md_cg/test_p27_docindex.py +765 -765
  236. package/md_cg/test_p28_refcheck.py +305 -305
  237. package/md_cg/test_p29_session_ingest_export.py +354 -333
  238. package/md_cg/test_p3.py +11 -2
  239. package/md_cg/test_p30_maintain.py +330 -330
  240. package/md_cg/test_p31_insight.py +534 -534
  241. package/md_cg/test_p32_backfill.py +298 -298
  242. package/md_cg/test_p33_ccg_wiring.py +293 -293
  243. package/md_cg/test_p34_crosscheck.py +331 -331
  244. package/md_cg/test_p35_conditioned_claim.py +252 -252
  245. package/md_cg/test_p36_kp_align.py +230 -230
  246. package/md_cg/test_p37_condition_space.py +248 -248
  247. package/md_cg/test_p38_concurrent_flush.py +102 -0
  248. package/md_cg/test_p38_contextualize.py +273 -273
  249. package/md_cg/test_p39_verify_flow.py +113 -0
  250. package/md_cg/test_p39_vision_evidence.py +369 -369
  251. package/md_cg/test_p40_refine_worklist.py +241 -241
  252. package/md_cg/test_p41_evolve_patrol.py +224 -224
  253. package/md_cg/test_p42_provenance.py +269 -269
  254. package/md_cg/test_p43_pooling.py +412 -398
  255. package/md_cg/test_p44_md_whitebox.py +231 -231
  256. package/md_cg/test_p45_session_identity.py +219 -219
  257. package/md_cg/test_p46_unit_scope.py +272 -272
  258. package/md_cg/test_p47_session_view.py +281 -0
  259. package/md_cg/test_p4_fuzzy.py +223 -223
  260. package/md_cg/test_p5_semantic.py +226 -226
  261. package/md_cg/test_p6_consolidate.py +440 -387
  262. package/md_cg/test_p7_goals_recent.py +202 -202
  263. package/md_cg/test_p8_subgraph_chain.py +200 -200
  264. package/md_cg/test_p9_forget_protect.py +231 -231
  265. package/md_cg/test_predict_beta.py +135 -135
  266. package/md_cg/test_preflight_failclosed.py +100 -100
  267. package/md_cg/test_progressive.py +146 -146
  268. package/md_cg/test_protocol.py +243 -243
  269. package/md_cg/test_reach.py +378 -378
  270. package/md_cg/test_reach_keys.py +201 -201
  271. package/md_cg/test_read_clip.py +141 -141
  272. package/md_cg/test_readcache_prodpath.py +155 -0
  273. package/md_cg/test_retr_gates_prodpath.py +140 -0
  274. package/md_cg/test_retr_s1.py +340 -340
  275. package/md_cg/test_retr_s1b.py +209 -209
  276. package/md_cg/test_retr_s3.py +194 -194
  277. package/md_cg/test_retr_s4.py +163 -163
  278. package/md_cg/test_retr_s5.py +200 -200
  279. package/md_cg/test_retr_s6.py +157 -157
  280. package/md_cg/test_retr_s7.py +384 -384
  281. package/md_cg/test_retr_s8_time.py +369 -316
  282. package/md_cg/test_retr_s9_edges.py +286 -286
  283. package/md_cg/test_retr_s9_entity_ctx.py +175 -175
  284. package/md_cg/test_review_conformance.py +367 -367
  285. package/md_cg/test_role_views.py +354 -354
  286. package/md_cg/test_sem_noise.py +242 -242
  287. package/md_cg/test_semantic_canonical.py +241 -241
  288. package/md_cg/test_subproc_encoding.py +192 -192
  289. package/md_cg/test_sustain_mutual.py +153 -153
  290. package/md_cg/test_tasks.py +409 -409
  291. package/md_cg/test_tool_face.py +189 -189
  292. package/md_cg/test_transfer.py +180 -180
  293. package/md_cg/test_trust.py +361 -361
  294. package/md_cg/test_twophase.py +286 -286
  295. package/md_cg/test_v14_fixes.py +397 -397
  296. package/md_cg/test_validity_filter.py +280 -280
  297. package/md_cg/test_verify_answer.py +138 -138
  298. package/md_cg/test_wisdom_md_store.py +292 -292
  299. package/md_cg/test_writelimit.py +197 -197
  300. package/md_cg/test_writepipe.py +214 -214
  301. package/md_cg/theory.py +273 -273
  302. package/md_cg/tokens.py +677 -663
  303. package/md_cg/tool_face.py +260 -260
  304. package/md_cg/trust.py +986 -950
  305. package/md_cg/twophase.py +231 -231
  306. package/md_cg/units.py +667 -667
  307. package/md_cg/vision_evidence.py +666 -666
  308. package/md_cg/weights.py +624 -624
  309. package/md_cg/whitebox.py +527 -527
  310. package/md_cg/whitebox_kb/__init__.py +37 -37
  311. package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
  312. package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
  313. package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
  314. package/md_cg/whitebox_kb/engine.py +310 -310
  315. package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
  316. package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
  317. package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
  318. package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
  319. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  320. package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
  321. package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
  322. package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
  323. package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
  324. package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
  325. package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
  326. package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
  327. package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
  328. package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
  329. package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
  330. package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
  331. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
  332. package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
  333. package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
  334. package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
  335. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
  336. package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
  337. package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
  338. package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
  339. package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
  340. package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
  341. package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
  342. package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
  343. package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
  344. package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
  345. package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
  346. package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
  347. package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
  348. package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
  349. package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
  350. package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
  351. package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
  352. package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
  353. package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
  354. package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
  355. package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
  356. package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
  357. package/md_cg/writelimit.py +356 -356
  358. package/md_cg/writepipe.py +550 -542
  359. package/package.json +97 -96
  360. package/skills/plugin.json +54 -54
  361. package/skills/skills/designer-perspective/SKILL.md +158 -158
  362. package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
  363. package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
  364. package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
  365. package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
  366. package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
  367. package/skills/skills/designer-perspective/scripts/designer.py +545 -545
  368. package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
  369. package/skills/skills/designer-perspective/tests/selftest.py +61 -61
  370. package/skills/skills/lingshu-browser/SKILL.md +60 -60
  371. package/skills/skills/lingshu-compiler/SKILL.md +56 -56
  372. package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
  373. package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
  374. package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
  375. package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
  376. package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
  377. package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
  378. package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
  379. package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
  380. package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
  381. package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
  382. package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
  383. package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
  384. package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
  385. package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
  386. package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
  387. package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
  388. package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
  389. package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
  390. package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
  391. package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
  392. package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
  393. package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
  394. package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
  395. package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
  396. package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
  397. package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
  398. package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
  399. package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
  400. package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
  401. package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
  402. package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
  403. package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
  404. package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
  405. package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
  406. package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
  407. package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
  408. package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
  409. package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
  410. package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
  411. package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
  412. package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
  413. package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
  414. package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
  415. package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
  416. package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
  417. package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
  418. package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
  419. package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
  420. package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
  421. package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
  422. package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
  423. package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
  424. package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
  425. package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
  426. package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
  427. package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
  428. package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
  429. package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
  430. package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
  431. package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
  432. package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
  433. package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
  434. package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
  435. package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
  436. package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
  437. package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
  438. package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
  439. package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
  440. package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
  441. package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
  442. package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
  443. package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
  444. package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
  445. package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
  446. package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
  447. package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
  448. package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
  449. package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
  450. package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
  451. package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
  452. package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
  453. package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
  454. package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
  455. package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
  456. package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
  457. package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
  458. package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
  459. package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
  460. package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
  461. package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
  462. package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
  463. package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
  464. package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
  465. package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
  466. package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
  467. package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
  468. package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
  469. package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
  470. package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
  471. package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
  472. package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
  473. package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
  474. package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
  475. package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
  476. package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
  477. package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
  478. package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
  479. package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
  480. package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
  481. package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
  482. package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
  483. package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
  484. package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
  485. package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
  486. package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
  487. package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
  488. package/skills/skills/lingshu-graph/SKILL.md +63 -63
  489. package/skills/skills/lingshu-net/SKILL.md +48 -48
  490. package/skills/skills/lingshu-os/SKILL.md +64 -64
  491. package/skills/skills/lingshu-pylang/SKILL.md +71 -71
  492. package/src/bridge.ts +401 -401
  493. package/src/hooks.ts +38 -2
  494. package/src/lib/datapath.ts +326 -326
  495. package/src/lib/mdcg_client.ts +413 -413
  496. package/src/lib/mutual.ts +428 -428
  497. package/src/lib/prompt_safety.ts +62 -62
  498. package/src/lib/python_path.ts +71 -71
  499. package/src/lib/roleplay_web.ts +932 -932
  500. package/src/lib/token_store.ts +192 -192
  501. package/src/tools.ts +212 -212
  502. package/zcode/AGENTS.md +11 -3
  503. package/zcode/README.md +41 -41
  504. /package/docs/{mdcg → hive}//344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md" +0 -0
@@ -1,694 +1,694 @@
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
+
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__":
694
694
  sys.exit(main())