@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.
- package/README.md +16 -16
- package/codebuddy/CODEBUDDY.md +11 -3
- package/codebuddy/README.md +92 -90
- package/codebuddy/mcp.json +27 -27
- 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
- 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
- package/docs/README.md +142 -111
- package/docs/discipline/harnesses.yaml +244 -226
- package/docs/discipline/templates/full.md.tmpl +61 -61
- package/docs/discipline/templates/rules.mdc.tmpl +68 -0
- package/docs/discipline/templates/skill.md.tmpl +23 -23
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
- package/docs/mdcg/lingshu_tutorial.html +14449 -14449
- package/docs/mdcg/release_v0.4.11.md +49 -0
- package/docs/mdcg/release_v0.4.5.md +55 -55
- package/docs/mdcg/tool_table_v0.3.0.md +117 -117
- 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
- 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
- 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
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
- 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
- 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
- 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
- package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
- 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
- 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
- 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
- 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
- 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
- package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
- 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
- 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
- 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
- package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/dsh/README.md +82 -82
- package/dsh/cordis.yml.example +139 -139
- package/dsh/update-lingshu.bat +11 -11
- package/lib/hooks.js +36 -2
- package/lib/lib/roleplay_web.js +427 -427
- package/md_cg/__init__.py +7 -7
- package/md_cg/audit.py +368 -368
- package/md_cg/autonomy.py +287 -287
- package/md_cg/backfill.py +1327 -1327
- package/md_cg/backfill_bigdomain.py +34 -34
- package/md_cg/bench6_arms.py +410 -410
- package/md_cg/bench6_common.py +230 -230
- package/md_cg/bench6_competitors.py +212 -212
- package/md_cg/bench_axis_domain.py +257 -257
- package/md_cg/bench_blind_comp.py +308 -308
- package/md_cg/bench_en_atoms_public.py +230 -230
- package/md_cg/bench_governance.py +348 -348
- package/md_cg/bench_lme_zh.py +410 -410
- package/md_cg/bench_locomo.py +121 -121
- package/md_cg/bench_locomo_zh.py +450 -450
- package/md_cg/bench_locomo_zh_public.py +147 -147
- package/md_cg/bench_longmem.py +112 -112
- package/md_cg/bench_membench.py +632 -632
- package/md_cg/bench_p0.py +149 -149
- package/md_cg/bench_progressive.py +287 -287
- package/md_cg/bench_role_views.py +238 -238
- package/md_cg/bench_task_ab.py +243 -243
- package/md_cg/bench_task_ab_llm.py +408 -408
- package/md_cg/bench_unified_en.py +204 -204
- package/md_cg/bench_zh_mad.py +601 -601
- package/md_cg/blindspot_tickets.py +123 -123
- package/md_cg/branches.py +285 -285
- package/md_cg/build_postings.py +73 -73
- package/md_cg/ccgc.py +1005 -948
- package/md_cg/census.py +132 -132
- package/md_cg/chain.py +300 -300
- package/md_cg/codeindex.py +531 -531
- package/md_cg/coldverify.py +292 -292
- package/md_cg/comment_gate.py +337 -337
- package/md_cg/cond_compose.py +190 -190
- package/md_cg/cond_facts.py +154 -154
- package/md_cg/cond_template.json +106 -106
- package/md_cg/condition_anchor.py +142 -142
- package/md_cg/conformance.py +726 -726
- package/md_cg/consistency.py +717 -717
- package/md_cg/consolidate.py +1536 -1439
- package/md_cg/corpus.py +110 -110
- package/md_cg/crosscheck.py +1097 -1097
- package/md_cg/crypto.py +437 -437
- package/md_cg/d_meta.py +310 -310
- package/md_cg/datapath.py +334 -334
- package/md_cg/docindex.py +473 -473
- package/md_cg/eval_common.py +575 -575
- package/md_cg/evidence.py +580 -580
- package/md_cg/evolution.py +477 -477
- package/md_cg/export.py +220 -220
- package/md_cg/forgetting.py +581 -581
- package/md_cg/fsutil.py +329 -329
- package/md_cg/hotcache.py +238 -214
- package/md_cg/hyperedge.py +251 -251
- package/md_cg/identity.py +390 -390
- package/md_cg/insight.py +500 -500
- package/md_cg/interop.py +199 -0
- package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
- package/md_cg/lexicon/build_standard_en.py +171 -171
- package/md_cg/lexicon/expand_en_zh.py +211 -211
- package/md_cg/lifecycle.py +272 -272
- package/md_cg/linkref.py +280 -280
- package/md_cg/links.py +622 -622
- package/md_cg/mcp_server.py +129 -32
- package/md_cg/md_whitebox.py +345 -345
- package/md_cg/mdcg.py +176 -117
- package/md_cg/mdcos.py +79 -13
- package/md_cg/metacognition.py +591 -591
- package/md_cg/migrate.py +119 -119
- package/md_cg/migrate_aeis.py +221 -221
- package/md_cg/migrate_roleplay.py +293 -293
- package/md_cg/migrate_wisdom_graph.py +360 -360
- package/md_cg/mreview/__init__.py +25 -25
- package/md_cg/mreview/__main__.py +110 -110
- package/md_cg/mreview/bundle.py +178 -178
- package/md_cg/mreview/candidates.py +262 -262
- package/md_cg/mreview/govern.py +693 -693
- package/md_cg/mreview/locate.py +939 -939
- package/md_cg/mreview/pipeline.py +728 -728
- package/md_cg/mreview/rules/duplication.json +21 -21
- package/md_cg/mreview/rules/field_coverage.json +54 -54
- package/md_cg/mreview/rules/source_license.json +21 -21
- package/md_cg/mreview/rules/template_flow.json +21 -21
- package/md_cg/mreview/ruleset.py +252 -252
- package/md_cg/nodefile.py +575 -575
- package/md_cg/pooling.py +484 -472
- package/md_cg/postings.py +298 -298
- package/md_cg/predict.py +1100 -1100
- package/md_cg/progressive.py +123 -123
- package/md_cg/protect.py +272 -272
- package/md_cg/protocol/md_cg_gate.proto +33 -33
- package/md_cg/protocol.py +372 -372
- package/md_cg/provenance.py +582 -582
- package/md_cg/reach.py +453 -453
- package/md_cg/readcache.py +85 -0
- package/md_cg/refindex.py +833 -833
- package/md_cg/refine.py +604 -604
- package/md_cg/roleviews.py +89 -89
- package/md_cg/routing.py +365 -365
- package/md_cg/scrub.py +852 -852
- package/md_cg/security.py +274 -274
- package/md_cg/self_state.py +1029 -1029
- package/md_cg/selfreport.py +151 -151
- package/md_cg/semantic/__init__.py +10 -10
- package/md_cg/semantic/canonical.py +122 -122
- package/md_cg/semantic/en_normalizer.py +364 -364
- package/md_cg/semantic/en_zh_map.json +28694 -0
- package/md_cg/semantic/export_en_zh_map.py +64 -0
- package/md_cg/semantic/unify.py +45 -0
- package/md_cg/semantic/zh_en_atoms.py +139 -139
- package/md_cg/signer.py +562 -562
- package/md_cg/sources.py +815 -582
- package/md_cg/statushdr.py +179 -179
- package/md_cg/stg.py +48 -37
- package/md_cg/subgraph.py +729 -729
- package/md_cg/sustain.py +1138 -1138
- package/md_cg/tasks.py +470 -470
- package/md_cg/test_action_derive.py +203 -203
- package/md_cg/test_audit_rotate.py +270 -270
- package/md_cg/test_autonomy.py +143 -143
- package/md_cg/test_bench_governance.py +102 -102
- package/md_cg/test_blindspot_tickets.py +166 -166
- package/md_cg/test_branches.py +249 -249
- package/md_cg/test_ccg_perturb.py +184 -184
- package/md_cg/test_ccgc.py +433 -433
- package/md_cg/test_census_prune.py +81 -81
- package/md_cg/test_cond_compose_anchors.py +76 -76
- package/md_cg/test_cond_match.py +165 -165
- package/md_cg/test_condition_anchor.py +81 -81
- package/md_cg/test_d_meta.py +412 -412
- package/md_cg/test_datapath_root.py +199 -199
- package/md_cg/test_en_pipeline.py +166 -166
- package/md_cg/test_gain_gate.py +212 -212
- package/md_cg/test_health_scale.py +173 -173
- package/md_cg/test_hive_ingest.py +285 -0
- package/md_cg/test_hot_cold.py +215 -215
- package/md_cg/test_hyperedge.py +245 -245
- package/md_cg/test_i26_empty_first_write.py +116 -0
- package/md_cg/test_i27_e041_identity.py +128 -0
- package/md_cg/test_i28_hotcache_prodpath.py +122 -0
- package/md_cg/test_identity_attribution.py +147 -147
- package/md_cg/test_index_durability.py +224 -224
- package/md_cg/test_interop.py +93 -0
- package/md_cg/test_lifecycle.py +309 -309
- package/md_cg/test_linkref.py +306 -306
- package/md_cg/test_lock.py +43 -43
- package/md_cg/test_md_access_parity.py +255 -255
- package/md_cg/test_md_writepath.py +345 -345
- package/md_cg/test_mdstore_search_parity.py +160 -0
- package/md_cg/test_mr_m2.py +587 -587
- package/md_cg/test_mr_m3.py +710 -710
- package/md_cg/test_mr_m4.py +485 -485
- package/md_cg/test_p0.py +250 -250
- package/md_cg/test_p1.py +316 -316
- package/md_cg/test_p10_identity.py +173 -173
- package/md_cg/test_p11_consistency.py +233 -233
- package/md_cg/test_p12_metacognition.py +212 -212
- package/md_cg/test_p13_encryption.py +241 -241
- package/md_cg/test_p14_sustain.py +249 -249
- package/md_cg/test_p15_scrub.py +280 -280
- package/md_cg/test_p16_self_state.py +301 -301
- package/md_cg/test_p17_predict.py +354 -354
- package/md_cg/test_p18_whitebox.py +171 -171
- package/md_cg/test_p19_migrate_roleplay.py +149 -149
- package/md_cg/test_p20_evolution.py +315 -315
- package/md_cg/test_p21_tokens.py +293 -270
- package/md_cg/test_p22_theory.py +175 -175
- package/md_cg/test_p23_links.py +311 -311
- package/md_cg/test_p24_evidence.py +227 -227
- package/md_cg/test_p25_weights.py +156 -156
- package/md_cg/test_p26_refindex.py +416 -416
- package/md_cg/test_p27_docindex.py +765 -765
- package/md_cg/test_p28_refcheck.py +305 -305
- package/md_cg/test_p29_session_ingest_export.py +354 -333
- package/md_cg/test_p3.py +11 -2
- package/md_cg/test_p30_maintain.py +330 -330
- package/md_cg/test_p31_insight.py +534 -534
- package/md_cg/test_p32_backfill.py +298 -298
- package/md_cg/test_p33_ccg_wiring.py +293 -293
- package/md_cg/test_p34_crosscheck.py +331 -331
- package/md_cg/test_p35_conditioned_claim.py +252 -252
- package/md_cg/test_p36_kp_align.py +230 -230
- package/md_cg/test_p37_condition_space.py +248 -248
- package/md_cg/test_p38_concurrent_flush.py +102 -0
- package/md_cg/test_p38_contextualize.py +273 -273
- package/md_cg/test_p39_verify_flow.py +113 -0
- package/md_cg/test_p39_vision_evidence.py +369 -369
- package/md_cg/test_p40_refine_worklist.py +241 -241
- package/md_cg/test_p41_evolve_patrol.py +224 -224
- package/md_cg/test_p42_provenance.py +269 -269
- package/md_cg/test_p43_pooling.py +412 -398
- package/md_cg/test_p44_md_whitebox.py +231 -231
- package/md_cg/test_p45_session_identity.py +219 -219
- package/md_cg/test_p46_unit_scope.py +272 -272
- package/md_cg/test_p47_session_view.py +281 -0
- package/md_cg/test_p4_fuzzy.py +223 -223
- package/md_cg/test_p5_semantic.py +226 -226
- package/md_cg/test_p6_consolidate.py +440 -387
- package/md_cg/test_p7_goals_recent.py +202 -202
- package/md_cg/test_p8_subgraph_chain.py +200 -200
- package/md_cg/test_p9_forget_protect.py +231 -231
- package/md_cg/test_predict_beta.py +135 -135
- package/md_cg/test_preflight_failclosed.py +100 -100
- package/md_cg/test_progressive.py +146 -146
- package/md_cg/test_protocol.py +243 -243
- package/md_cg/test_reach.py +378 -378
- package/md_cg/test_reach_keys.py +201 -201
- package/md_cg/test_read_clip.py +141 -141
- package/md_cg/test_readcache_prodpath.py +155 -0
- package/md_cg/test_retr_gates_prodpath.py +140 -0
- package/md_cg/test_retr_s1.py +340 -340
- package/md_cg/test_retr_s1b.py +209 -209
- package/md_cg/test_retr_s3.py +194 -194
- package/md_cg/test_retr_s4.py +163 -163
- package/md_cg/test_retr_s5.py +200 -200
- package/md_cg/test_retr_s6.py +157 -157
- package/md_cg/test_retr_s7.py +384 -384
- package/md_cg/test_retr_s8_time.py +369 -316
- package/md_cg/test_retr_s9_edges.py +286 -286
- package/md_cg/test_retr_s9_entity_ctx.py +175 -175
- package/md_cg/test_review_conformance.py +367 -367
- package/md_cg/test_role_views.py +354 -354
- package/md_cg/test_sem_noise.py +242 -242
- package/md_cg/test_semantic_canonical.py +241 -241
- package/md_cg/test_subproc_encoding.py +192 -192
- package/md_cg/test_sustain_mutual.py +153 -153
- package/md_cg/test_tasks.py +409 -409
- package/md_cg/test_tool_face.py +189 -189
- package/md_cg/test_transfer.py +180 -180
- package/md_cg/test_trust.py +361 -361
- package/md_cg/test_twophase.py +286 -286
- package/md_cg/test_v14_fixes.py +397 -397
- package/md_cg/test_validity_filter.py +280 -280
- package/md_cg/test_verify_answer.py +138 -138
- package/md_cg/test_wisdom_md_store.py +292 -292
- package/md_cg/test_writelimit.py +197 -197
- package/md_cg/test_writepipe.py +214 -214
- package/md_cg/theory.py +273 -273
- package/md_cg/tokens.py +677 -663
- package/md_cg/tool_face.py +260 -260
- package/md_cg/trust.py +986 -950
- package/md_cg/twophase.py +231 -231
- package/md_cg/units.py +667 -667
- package/md_cg/vision_evidence.py +666 -666
- package/md_cg/weights.py +624 -624
- package/md_cg/whitebox.py +527 -527
- package/md_cg/whitebox_kb/__init__.py +37 -37
- package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
- package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
- package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
- package/md_cg/whitebox_kb/engine.py +310 -310
- package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
- package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
- package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
- package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
- package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
- package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
- package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
- package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
- package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
- package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
- package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
- package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
- package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
- package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
- package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
- package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
- package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
- package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
- package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
- package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
- package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
- package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
- package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
- package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
- package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
- package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
- package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
- package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
- package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
- package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
- package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
- package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
- package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
- package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
- package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
- package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
- package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
- package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
- package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
- package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
- package/md_cg/writelimit.py +356 -356
- package/md_cg/writepipe.py +550 -542
- package/package.json +97 -96
- package/skills/plugin.json +54 -54
- package/skills/skills/designer-perspective/SKILL.md +158 -158
- package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
- package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
- package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
- package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
- package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
- package/skills/skills/designer-perspective/scripts/designer.py +545 -545
- package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
- package/skills/skills/designer-perspective/tests/selftest.py +61 -61
- package/skills/skills/lingshu-browser/SKILL.md +60 -60
- package/skills/skills/lingshu-compiler/SKILL.md +56 -56
- package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
- package/skills/skills/lingshu-graph/SKILL.md +63 -63
- package/skills/skills/lingshu-net/SKILL.md +48 -48
- package/skills/skills/lingshu-os/SKILL.md +64 -64
- package/skills/skills/lingshu-pylang/SKILL.md +71 -71
- package/src/bridge.ts +401 -401
- package/src/hooks.ts +38 -2
- package/src/lib/datapath.ts +326 -326
- package/src/lib/mdcg_client.ts +413 -413
- package/src/lib/mutual.ts +428 -428
- package/src/lib/prompt_safety.ts +62 -62
- package/src/lib/python_path.ts +71 -71
- package/src/lib/roleplay_web.ts +932 -932
- package/src/lib/token_store.ts +192 -192
- package/src/tools.ts +212 -212
- package/zcode/AGENTS.md +11 -3
- package/zcode/README.md +41 -41
- /package/docs/{mdcg → hive}//344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md" +0 -0
package/md_cg/mreview/govern.py
CHANGED
|
@@ -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())
|