@furongjun1999/dsh-memory 0.4.7 → 0.4.9
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 +130 -47
- package/codebuddy/CODEBUDDY.md +21 -10
- package/codebuddy/README.md +13 -1
- package/codebuddy/mcp.json +9 -0
- 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 -0
- 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 -0
- package/docs/README.md +111 -0
- package/docs/discipline/harnesses.yaml +226 -152
- package/docs/discipline/templates/full.md.tmpl +61 -58
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
- package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
- package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
- package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
- package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
- package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
- package/docs/experiments/m4-role-probe/census.py +86 -0
- package/docs/experiments/m4-role-probe/probe.py +89 -0
- package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
- package/docs/experiments/mapped_confidence/post_check.py +75 -0
- package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
- package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
- package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
- 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 -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 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
- package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -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 -0
- 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 +14 -2
- package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
- package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
- package/docs/{ → mdcg/}/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 +1 -1
- package/docs/mdcg//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_/345/245/221/347/272/246_v0.1.md +82 -0
- 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 -0
- 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 +263 -0
- 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 +1 -1
- package/docs/mdcg//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 -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 +2 -2
- package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
- 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 -0
- package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
- package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
- 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 +1 -1
- 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 +4 -4
- package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
- package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
- 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 -0
- package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
- 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 -0
- 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 -0
- 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 -0
- 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 -0
- 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 +6 -6
- 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 +1 -1
- 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 -0
- 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 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -0
- 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 +434 -405
- package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
- package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
- package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
- package/dsh/README.md +34 -1
- package/dsh/cordis.yml.example +13 -7
- package/dsh/hive-mcp-probe.mjs +94 -0
- package/dsh/hive-mcp.example.yml +62 -0
- package/dsh/update-lingshu.bat +11 -0
- package/dsh/update-lingshu.ps1 +337 -0
- package/lib/hooks.d.ts +8 -0
- package/lib/hooks.js +27 -24
- package/lib/index.d.ts +4 -2
- package/lib/index.js +29 -6
- package/lib/lib/datapath.d.ts +76 -1
- package/lib/lib/datapath.js +199 -13
- package/lib/lib/mdcg_client.d.ts +6 -3
- package/lib/lib/mdcg_client.js +6 -5
- package/lib/lib/mutual.js +4 -4
- package/lib/lib/prompt_safety.d.ts +48 -0
- package/lib/lib/prompt_safety.js +62 -0
- package/lib/lib/token_store.js +4 -5
- package/md_cg/audit.py +91 -2
- package/md_cg/autonomy.py +86 -15
- package/md_cg/backfill.py +36 -1
- package/md_cg/backfill_bigdomain.py +34 -0
- package/md_cg/bench6_arms.py +28 -1
- package/md_cg/bench6_common.py +10 -1
- package/md_cg/bench6_competitors.py +6 -1
- package/md_cg/bench_axis_domain.py +9 -1
- package/md_cg/bench_blind_comp.py +7 -1
- package/md_cg/bench_en_atoms_public.py +9 -0
- package/md_cg/bench_governance.py +348 -0
- package/md_cg/bench_lme_zh.py +16 -1
- package/md_cg/bench_locomo.py +2 -1
- package/md_cg/bench_locomo_zh.py +16 -1
- package/md_cg/bench_locomo_zh_public.py +4 -1
- package/md_cg/bench_longmem.py +2 -1
- package/md_cg/bench_membench.py +37 -6
- package/md_cg/bench_p0.py +4 -1
- package/md_cg/bench_progressive.py +61 -10
- package/md_cg/bench_role_views.py +238 -0
- package/md_cg/bench_task_ab.py +8 -1
- package/md_cg/bench_task_ab_llm.py +13 -1
- package/md_cg/bench_unified_en.py +6 -1
- package/md_cg/bench_zh_mad.py +20 -1
- package/md_cg/blindspot_tickets.py +123 -0
- package/md_cg/branches.py +286 -0
- package/md_cg/build_postings.py +73 -0
- package/md_cg/ccgc.py +949 -0
- package/md_cg/census.py +5 -1
- package/md_cg/chain.py +24 -3
- package/md_cg/codeindex.py +134 -17
- package/md_cg/coldverify.py +265 -0
- package/md_cg/comment_gate.py +338 -0
- package/md_cg/cond_compose.py +190 -0
- package/md_cg/cond_facts.py +155 -0
- package/md_cg/cond_template.json +107 -0
- package/md_cg/condition_anchor.py +143 -0
- package/md_cg/conformance.py +727 -0
- package/md_cg/consistency.py +25 -2
- package/md_cg/consolidate.py +53 -2
- package/md_cg/corpus.py +5 -1
- package/md_cg/crosscheck.py +42 -2
- package/md_cg/crypto.py +35 -1
- package/md_cg/d_meta.py +310 -0
- package/md_cg/datapath.py +201 -26
- package/md_cg/docindex.py +122 -1
- package/md_cg/eval_common.py +35 -6
- package/md_cg/evidence.py +27 -1
- package/md_cg/evolution.py +21 -1
- package/md_cg/export.py +11 -1
- package/md_cg/forgetting.py +34 -4
- package/md_cg/fsutil.py +81 -2
- package/md_cg/hotcache.py +214 -0
- package/md_cg/hyperedge.py +251 -0
- package/md_cg/identity.py +20 -3
- package/md_cg/insight.py +19 -3
- package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
- package/md_cg/lexicon/build_standard_en.py +171 -168
- package/md_cg/lexicon/expand_en_zh.py +6 -0
- package/md_cg/lifecycle.py +273 -0
- package/md_cg/linkref.py +281 -0
- package/md_cg/links.py +29 -1
- package/md_cg/mcp_server.py +894 -168
- package/md_cg/md_whitebox.py +53 -1
- package/md_cg/mdcg.py +1205 -41
- package/md_cg/mdcos.py +1060 -76
- package/md_cg/metacognition.py +39 -4
- package/md_cg/migrate.py +4 -0
- package/md_cg/migrate_aeis.py +221 -213
- package/md_cg/migrate_roleplay.py +8 -0
- package/md_cg/migrate_wisdom_graph.py +14 -1
- package/md_cg/mreview/__init__.py +25 -0
- package/md_cg/mreview/__main__.py +110 -0
- package/md_cg/mreview/bundle.py +178 -0
- package/md_cg/mreview/candidates.py +262 -0
- package/md_cg/mreview/govern.py +694 -0
- package/md_cg/mreview/locate.py +939 -0
- package/md_cg/mreview/pipeline.py +729 -0
- package/md_cg/mreview/rules/duplication.json +21 -0
- package/md_cg/mreview/rules/field_coverage.json +54 -0
- package/md_cg/mreview/rules/source_license.json +21 -0
- package/md_cg/mreview/rules/template_flow.json +21 -0
- package/md_cg/mreview/ruleset.py +253 -0
- package/md_cg/nodefile.py +269 -1
- package/md_cg/pooling.py +23 -1
- package/md_cg/postings.py +298 -0
- package/md_cg/predict.py +113 -15
- package/md_cg/progressive.py +3 -0
- package/md_cg/protect.py +18 -5
- package/md_cg/protocol.py +372 -0
- package/md_cg/provenance.py +262 -0
- package/md_cg/reach.py +453 -0
- package/md_cg/refindex.py +88 -4
- package/md_cg/refine.py +20 -1
- package/md_cg/roleviews.py +89 -0
- package/md_cg/routing.py +76 -0
- package/md_cg/scrub.py +68 -3
- package/md_cg/security.py +26 -1
- package/md_cg/self_state.py +65 -2
- package/md_cg/selfreport.py +151 -0
- package/md_cg/semantic/canonical.py +5 -0
- package/md_cg/semantic/en_normalizer.py +364 -295
- package/md_cg/semantic/zh_en_atoms.py +139 -136
- package/md_cg/signer.py +41 -1
- package/md_cg/sources.py +583 -547
- package/md_cg/statushdr.py +179 -0
- package/md_cg/stg.py +59 -18
- package/md_cg/subgraph.py +23 -0
- package/md_cg/sustain.py +74 -1
- package/md_cg/tasks.py +471 -0
- package/md_cg/test_action_derive.py +203 -0
- package/md_cg/test_audit_rotate.py +270 -0
- package/md_cg/test_autonomy.py +26 -0
- package/md_cg/test_bench_governance.py +102 -0
- package/md_cg/test_blindspot_tickets.py +166 -0
- package/md_cg/test_branches.py +249 -0
- package/md_cg/test_ccg_perturb.py +1 -1
- package/md_cg/test_ccgc.py +433 -0
- package/md_cg/test_codeindex.py +338 -0
- package/md_cg/test_comment_gate.py +187 -0
- package/md_cg/test_cond_compose_anchors.py +76 -0
- package/md_cg/test_condition_anchor.py +82 -0
- package/md_cg/test_conformance.py +343 -0
- package/md_cg/test_d_meta.py +412 -0
- package/md_cg/test_datapath_root.py +188 -0
- package/md_cg/test_en_pipeline.py +24 -0
- package/md_cg/test_gain_gate.py +47 -1
- package/md_cg/test_health_scale.py +173 -0
- package/md_cg/test_hot_cold.py +187 -0
- package/md_cg/test_hyperedge.py +245 -0
- package/md_cg/test_identity_attribution.py +6 -0
- package/md_cg/test_index_durability.py +224 -0
- package/md_cg/test_lifecycle.py +309 -0
- package/md_cg/test_linkref.py +306 -0
- package/md_cg/test_md_access_parity.py +15 -3
- package/md_cg/test_mr_m1.py +769 -0
- package/md_cg/test_mr_m2.py +587 -0
- package/md_cg/test_mr_m3.py +710 -0
- package/md_cg/test_mr_m4.py +485 -0
- package/md_cg/test_p1.py +1 -1
- package/md_cg/test_p11_consistency.py +1 -1
- package/md_cg/test_p12_metacognition.py +2 -2
- package/md_cg/test_p16_self_state.py +1 -1
- package/md_cg/test_p26_refindex.py +50 -21
- package/md_cg/test_p27_docindex.py +258 -4
- package/md_cg/test_p28_refcheck.py +1 -1
- package/md_cg/test_p29_session_ingest_export.py +1 -1
- package/md_cg/test_p2_mcp.py +7 -1
- package/md_cg/test_p31_insight.py +24 -0
- package/md_cg/test_p38_contextualize.py +1 -1
- package/md_cg/test_p39_vision_evidence.py +1 -1
- package/md_cg/test_p40_refine_worklist.py +1 -1
- package/md_cg/test_p41_evolve_patrol.py +1 -1
- package/md_cg/test_p42_provenance.py +1 -1
- package/md_cg/test_p43_pooling.py +41 -13
- package/md_cg/test_p44_md_whitebox.py +14 -1
- package/md_cg/test_protocol.py +243 -0
- package/md_cg/test_reach.py +378 -0
- package/md_cg/test_reach_keys.py +201 -0
- package/md_cg/test_reach_meta_exits.py +145 -0
- package/md_cg/test_read_clip.py +141 -0
- package/md_cg/test_retr_s1.py +340 -0
- package/md_cg/test_retr_s1b.py +209 -0
- package/md_cg/test_retr_s3.py +194 -0
- package/md_cg/test_retr_s4.py +163 -0
- package/md_cg/test_retr_s5.py +200 -0
- package/md_cg/test_retr_s6.py +157 -0
- package/md_cg/test_retr_s7.py +385 -0
- package/md_cg/test_retr_s8_time.py +316 -0
- package/md_cg/test_retr_s9_edges.py +286 -0
- package/md_cg/test_retr_s9_entity_ctx.py +175 -0
- package/md_cg/test_review_conformance.py +367 -0
- package/md_cg/test_role_views.py +354 -0
- package/md_cg/test_tasks.py +409 -0
- package/md_cg/test_tool_face.py +189 -0
- package/md_cg/test_trust.py +361 -0
- package/md_cg/test_twophase.py +286 -0
- package/md_cg/test_units_poll.py +71 -0
- package/md_cg/test_v14_fixes.py +397 -0
- package/md_cg/test_validity_filter.py +280 -0
- package/md_cg/test_wisdom_md_store.py +7 -3
- package/md_cg/test_writepipe.py +214 -0
- package/md_cg/theory.py +17 -2
- package/md_cg/tokens.py +134 -12
- package/md_cg/tool_face.py +261 -0
- package/md_cg/trust.py +943 -0
- package/md_cg/twophase.py +232 -0
- package/md_cg/units.py +665 -0
- package/md_cg/vision_evidence.py +25 -2
- package/md_cg/weights.py +25 -2
- package/md_cg/whitebox.py +33 -2
- package/md_cg/whitebox_kb/data/verify_cache.json +21233 -360
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +5124 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
- package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
- package/md_cg/writelimit.py +37 -7
- package/md_cg/writepipe.py +543 -0
- package/package.json +2 -2
- package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
- package/skills/skills/designer-perspective/scripts/designer.py +17 -1
- package/skills/skills/designer-perspective/tests/selftest.py +3 -1
- package/src/hooks.ts +27 -22
- package/src/index.ts +33 -6
- package/src/lib/datapath.ts +211 -13
- package/src/lib/mdcg_client.ts +12 -8
- package/src/lib/mutual.ts +411 -411
- package/src/lib/prompt_safety.ts +62 -0
- package/src/lib/token_store.ts +4 -5
- package/zcode/AGENTS.md +21 -10
- package/zcode/README.md +4 -0
- package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
- /package/docs/{AGI → 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" +0 -0
- /package/docs/{AGI → 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" +0 -0
- /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" +0 -0
- /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" +0 -0
- /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
- /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
- /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
- /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
- /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
- /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
- /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
- /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
- /package/docs/{ → mdcg/}/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" +0 -0
- /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" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
- /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" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
- /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" +0 -0
- /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
- /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
package/md_cg/mdcos.py
CHANGED
|
@@ -23,17 +23,20 @@ import json
|
|
|
23
23
|
import os
|
|
24
24
|
import re
|
|
25
25
|
import time
|
|
26
|
+
import uuid
|
|
26
27
|
|
|
27
28
|
from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT,
|
|
28
29
|
STATE_REJECT, STATE_DEFER, STATE_BLINDSPOT, TIER_BUCKET_LIKE,
|
|
29
30
|
TIER_BUCKET_SCAN, TIER_GLOBAL_LIKE, TIER_GLOBAL_SCAN,
|
|
30
31
|
GLOBAL_CAP, expand_query_terms_weighted,
|
|
31
|
-
expand_query_terms_llm, en_zh_bigrams, semantic_on
|
|
32
|
+
expand_query_terms_llm, en_zh_bigrams, semantic_on,
|
|
33
|
+
cut_by_relevance)
|
|
32
34
|
from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
33
35
|
identity, consistency, metacognition, crypto, sustain,
|
|
34
36
|
self_state, predict, evolution, weights, pooling,
|
|
35
|
-
writelimit)
|
|
36
|
-
from .fsutil import FileLock, atomic_write, append_jsonl, read_jsonl
|
|
37
|
+
writelimit, reach, trust, roleviews)
|
|
38
|
+
from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
|
|
39
|
+
count_jsonl)
|
|
37
40
|
from .security import (Principal, TenantRegistry, AccessDenied,
|
|
38
41
|
SENSITIVITY_ORDER, DEFAULT_SENSITIVITY, _rank)
|
|
39
42
|
|
|
@@ -61,6 +64,7 @@ DEFAULT_BUDGET = 1200 # recall 默认 token 预算
|
|
|
61
64
|
DEFAULT_MAX_ITEM_TOKENS = 250
|
|
62
65
|
|
|
63
66
|
|
|
67
|
+
# 生效条件:text 为假值(None/空串)时返回 0,否则按「CJK 0.6/字 + 其余 /4」计算并返回 int(cjk*0.6 + other/4) + 1。
|
|
64
68
|
def est_tokens(text: str) -> int:
|
|
65
69
|
"""确定性 token 估算:CJK 0.6/字 + 其余 /4(与白箱 adapter 口径一致)。"""
|
|
66
70
|
if not text:
|
|
@@ -71,6 +75,7 @@ def est_tokens(text: str) -> int:
|
|
|
71
75
|
return int(cjk * 0.6 + other / 4) + 1
|
|
72
76
|
|
|
73
77
|
|
|
78
|
+
# 生效条件:text 非空且 max_tokens > 0 时返回 est_tokens 口径 ≤ max_tokens 的摘录,text 为空或 max_tokens ≤ 0 时返回空串 '',est_tokens(text) ≤ max_tokens 时原样返回 text。
|
|
74
79
|
def excerpt_tokens(text: str, max_tokens: int) -> str:
|
|
75
80
|
"""按 est_tokens 口径截取正文前 max_tokens 的摘录(用于 recall 的单条上限)。
|
|
76
81
|
|
|
@@ -100,10 +105,12 @@ def excerpt_tokens(text: str, max_tokens: int) -> str:
|
|
|
100
105
|
return out
|
|
101
106
|
|
|
102
107
|
|
|
108
|
+
# 生效条件:text 为可调用 .strip().encode("utf-8") 的字符串时,返回 hashlib.sha1(text.strip().encode("utf-8")).hexdigest()[:n],n 默认 12 只决定摘要截取长度。
|
|
103
109
|
def _sig(text: str, n: int = 12) -> str:
|
|
104
110
|
return hashlib.sha1(text.strip().encode("utf-8")).hexdigest()[:n]
|
|
105
111
|
|
|
106
112
|
|
|
113
|
+
# 生效条件:term 与 text 均非空时,term 整词出现在 text 中返回 1.0;否则仅当 term 长度 n≥2 且存在长度 L 满足 2≤L<n 的最长命中子串时返回 0.5*L/n;term 或 text 为空、term 长度 <2、或无此类命中子串时返回 0.0。
|
|
107
114
|
def _term_degree(term: str, text: str) -> float:
|
|
108
115
|
"""词在文本中的分级命中(0~1):整词出现 1.0;否则取最长命中子串的长度比 × 0.5。
|
|
109
116
|
|
|
@@ -126,6 +133,7 @@ def _term_degree(term: str, text: str) -> float:
|
|
|
126
133
|
return 0.0
|
|
127
134
|
|
|
128
135
|
|
|
136
|
+
# 生效条件:tw 为 {词: 权重} 映射(None 视作空),只累加 t 不以 "__" 开头且权重 w > 0 的项,返回 num/den;tw 无有效项(den 为 0)时返回 0.0,text 任意(转交 _term_degree)。
|
|
129
137
|
def _weighted_coverage(tw: dict, text: str) -> float:
|
|
130
138
|
"""词权 × 分级命中的加权覆盖率 ∈ [0,1]。"""
|
|
131
139
|
num = den = 0.0
|
|
@@ -147,6 +155,7 @@ def _weighted_coverage(tw: dict, text: str) -> float:
|
|
|
147
155
|
# 条件空间重合率是《激活引擎》cond_match 已被认证的度量。
|
|
148
156
|
# ---------------------------------------------------------------------------
|
|
149
157
|
|
|
158
|
+
# 生效条件:content 中某行以 "#" 开头、含 name、且以 ":" 或 ":" partition 出的 head.strip() 恰等于 name 时返回该行 val.strip()(首个命中即返回);无此行使返回空串 '',content 为 None/空按空串处理。
|
|
150
159
|
def _ccg_field(content: str, name: str) -> str:
|
|
151
160
|
"""取 CCG 正文中 `# <name>:` 那一行的值(确定性扫描,无正则回溯风险)。"""
|
|
152
161
|
for line in (content or "").splitlines():
|
|
@@ -162,6 +171,7 @@ def _ccg_field(content: str, name: str) -> str:
|
|
|
162
171
|
return ""
|
|
163
172
|
|
|
164
173
|
|
|
174
|
+
# 生效条件:当 fm 为 dict 且 content 为字符串时,返回从 CCG 正文、state_attributes.comment 与 non_applicable_conditions 三处合并去重后的 (生效条件列表, 不适用条件列表)。
|
|
165
175
|
def _declared_conditions(fm: dict, content: str):
|
|
166
176
|
"""节点声明的条件证据 → (生效条件列表, 不适用条件列表)。
|
|
167
177
|
|
|
@@ -172,6 +182,7 @@ def _declared_conditions(fm: dict, content: str):
|
|
|
172
182
|
"""
|
|
173
183
|
pos, neg = [], []
|
|
174
184
|
|
|
185
|
+
# 生效条件:当 bucket 与 val 传入且 str(val).strip() 得到的 s 非空、s 尚不在 bucket 中时,将 s 追加到 bucket;s 为空或已存在时不追加;
|
|
175
186
|
def _push(bucket, val):
|
|
176
187
|
s = str(val).strip()
|
|
177
188
|
if s and s not in bucket:
|
|
@@ -192,6 +203,7 @@ def _declared_conditions(fm: dict, content: str):
|
|
|
192
203
|
return pos, neg
|
|
193
204
|
|
|
194
205
|
|
|
206
|
+
# 生效条件:neg_texts 非空且 tw 中存在长度 ≥ 2、权重 ≥ min_weight(默认 0.6)的词,其对该拼接 blob 的 _term_degree ≥ 0.5 时返回 True;neg_texts 为空或无此类词时返回 False。
|
|
195
207
|
def _neg_hit(tw: dict, neg_texts, min_weight: float = 0.6) -> bool:
|
|
196
208
|
"""不适用条件是否被查询词**整词**命中——条件级负路由。
|
|
197
209
|
|
|
@@ -209,6 +221,7 @@ def _neg_hit(tw: dict, neg_texts, min_weight: float = 0.6) -> bool:
|
|
|
209
221
|
return False
|
|
210
222
|
|
|
211
223
|
|
|
224
|
+
# 生效条件:a 与 b 均为可取 a[0]、a[1](b[0]、b[1])并能 float() 的两元素窗口,否则(TypeError/ValueError/IndexError/KeyError)返回 0.0;交叠 hi > lo 时返回 min(1.0,(hi-lo)/span),span ≤ 0 返回 1.0,hi ≤ lo 返回 0.0。
|
|
212
225
|
def _window_overlap(a, b) -> float:
|
|
213
226
|
"""两个时间窗 [t1,t2] 的交叠比(0~1)= 交叠长度 / 较短窗长度。"""
|
|
214
227
|
try:
|
|
@@ -223,6 +236,7 @@ def _window_overlap(a, b) -> float:
|
|
|
223
236
|
return 1.0 if span <= 0 else min(1.0, (hi - lo) / span)
|
|
224
237
|
|
|
225
238
|
|
|
239
|
+
# 生效条件:cs 为映射;仅当 cs["observation_position"] 有值才把 q_domain 与它的 domain_similarity 以权重 2.0 计入,仅当 observation_tool/existence_constraint 有值才以 tw 取最佳 _term_degree 并按 1.0 计入,仅当 cs["time_window"] 与 ctx_tw 同时有值才按 1.0 计入 _window_overlap;缺信息槽不进分母,den 为 0 时返回 0.0,否则返回加权平均。
|
|
226
240
|
def _slot_overlap(tw: dict, cs: dict, q_domain=None, ctx_tw=None) -> float:
|
|
227
241
|
"""condition_space 四槽的加权重合度 ∈ [0,1]。
|
|
228
242
|
|
|
@@ -250,6 +264,7 @@ def _slot_overlap(tw: dict, cs: dict, q_domain=None, ctx_tw=None) -> float:
|
|
|
250
264
|
return (sum(w * v for w, v in parts) / den) if den else 0.0
|
|
251
265
|
|
|
252
266
|
|
|
267
|
+
# 生效条件:verify 为真值时返回 (norm, _sig(norm)) 二元组,norm 为 json.dumps(verify, sort_keys=True, ensure_ascii=False, separators=(",",":"), default=str);verify 为假值(None/空)时返回 ('', '')。
|
|
253
268
|
def _verify_norm(verify):
|
|
254
269
|
"""判据规范化 + 指纹:判据由 propose 声明,指纹不一致即视为被改写。"""
|
|
255
270
|
if not verify:
|
|
@@ -260,21 +275,48 @@ def _verify_norm(verify):
|
|
|
260
275
|
return norm, _sig(norm)
|
|
261
276
|
|
|
262
277
|
|
|
278
|
+
# 生效条件:无 required 形参;当 os.environ 中 MDCG_REDTEAM_REQUIRED 去空白小写后为 "1"/"true"/"yes" 时返回 True,否则返回 False。
|
|
263
279
|
def _redteam_required():
|
|
264
280
|
return os.environ.get("MDCG_REDTEAM_REQUIRED", "").strip().lower() \
|
|
265
281
|
in ("1", "true", "yes")
|
|
266
282
|
|
|
267
283
|
|
|
284
|
+
# ---- 审核队列裁决动作(治理层)· 2026-09-19 阶段一 ----------------------------
|
|
285
|
+
# **命名空间隔离**:`noop` 一词在 lifecycle.py / trust.py 中已被占用为**状态迁移
|
|
286
|
+
# 结果码**(`check`/`stamp` 返回的 code,语义「同状态、不写盘」);这里的 noop 是
|
|
287
|
+
# **审核队列裁决动作**(语义「已评估、判定不改变任何现有记忆」)。沿用
|
|
288
|
+
# `lifecycle_state` 改名先例(同结构两义必生歧义):两处**不共用常量、不互相
|
|
289
|
+
# import**,靠各自的常量名与返回体区分——迁移结果码出现在 `(ok, code, why)`,
|
|
290
|
+
# 裁决动作出现在 `review_decide` 的入参与 decisions.jsonl 的 `decision` 字段。
|
|
291
|
+
DECISION_ACCEPT = "accept"
|
|
292
|
+
DECISION_REJECT = "reject"
|
|
293
|
+
DECISION_EDIT = "edit"
|
|
294
|
+
DECISION_MERGE = "merge"
|
|
295
|
+
DECISION_NOOP = "noop"
|
|
296
|
+
|
|
297
|
+
#: 合法裁决动作全集——`review_decide` 的唯一校验源(原先硬编码在函数体,与
|
|
298
|
+
#: status 映射、工具面 schema、CLI 子命令三处各写一份,加动作必漏)。
|
|
299
|
+
DECISION_ACTIONS = (DECISION_ACCEPT, DECISION_REJECT, DECISION_EDIT,
|
|
300
|
+
DECISION_MERGE, DECISION_NOOP)
|
|
301
|
+
|
|
302
|
+
#: 终态裁决状态集(写进 decisions.jsonl 的 `status` 字段):三者都把提案**关闭**。
|
|
303
|
+
#: `needs_reapproval` 刻意不在列——红队打回后仍待再审批,必须继续可见。
|
|
304
|
+
TERMINAL_DECISION_STATUS = ("accepted", "rejected", "noop")
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# 生效条件:以任意 root 构造时按其拼接 audit_log/hippocampus/trash 等路径并 makedirs 创建 hippocampus 与 trash_dir(exist_ok=True),autoflush 透传父类、actor 存入 self.actor;
|
|
268
308
|
class MdCGOS(MdCG):
|
|
269
309
|
"""MdCG + 记忆 OS 七项能力。"""
|
|
270
310
|
|
|
271
311
|
HIPPOCAMPUS = "hippocampus"
|
|
272
312
|
TRASH = "trash"
|
|
273
313
|
|
|
314
|
+
# 生效条件:传入 root 时以 super().__init__(root, autoflush=autoflush) 初始化父类,把 actor 存入 self.actor,并按 root(及类常量 AUDIT_ARCHIVE/HIPPOCAMPUS/TRASH)拼出 audit_log/audit_archive/hippocampus/inbox_log/decisions_log/trash_dir 等路径,同时 makedirs(hippocampus/trash_dir, exist_ok=True)。
|
|
274
315
|
def __init__(self, root: str, autoflush: int = 64, actor: str = "system"):
|
|
275
316
|
super().__init__(root, autoflush=autoflush)
|
|
276
317
|
self.actor = actor
|
|
277
318
|
self.audit_log = os.path.join(self.root, "_audit.jsonl")
|
|
319
|
+
self.audit_archive = os.path.join(self.root, self.AUDIT_ARCHIVE)
|
|
278
320
|
self.deletions_log = os.path.join(self.root, "_deletions.jsonl")
|
|
279
321
|
self.hippocampus = os.path.join(self.root, self.HIPPOCAMPUS)
|
|
280
322
|
self.inbox_log = os.path.join(self.hippocampus, "inbox.jsonl")
|
|
@@ -282,24 +324,242 @@ class MdCGOS(MdCG):
|
|
|
282
324
|
self.trash_dir = os.path.join(self.root, self.TRASH)
|
|
283
325
|
os.makedirs(self.hippocampus, exist_ok=True)
|
|
284
326
|
os.makedirs(self.trash_dir, exist_ok=True)
|
|
327
|
+
self._audit_writes = 0 # 进程内写入计数(轮转探测节流,稳态零 stat)
|
|
328
|
+
self._audit_index = None # 归档索引缓存(懒加载)
|
|
285
329
|
|
|
286
330
|
# ================= 6. payload-free 审计 =================
|
|
287
331
|
|
|
332
|
+
# 生效条件:当 op 与 node_id 传入时,构造含 t/op/id/actor/session 并合并 meta 的记录,尝试轮转后追加到 self.audit_log;追加过程中的 OSError 被吞掉;
|
|
288
333
|
def _audit(self, op: str, node_id: str, **meta):
|
|
289
334
|
"""只记事件与载荷哈希,绝不记录内容(payload-free)。"""
|
|
290
335
|
rec = {"t": time.time(), "op": op, "id": node_id, "actor": self.actor,
|
|
291
336
|
"session": getattr(self, "session", None)}
|
|
292
337
|
rec.update(meta)
|
|
293
338
|
try:
|
|
339
|
+
self._rotate_audit_if_needed()
|
|
294
340
|
append_jsonl(self.audit_log, rec)
|
|
295
341
|
except OSError:
|
|
296
342
|
pass
|
|
297
343
|
|
|
298
|
-
|
|
299
|
-
|
|
344
|
+
# 生效条件:limit 为 None 时返回 audit_archive 各分片与 audit_log 依 paths 顺序 extend 的全部记录;limit 非 None(含 0)时从 reversed(paths) 读取并在 len(out) >= limit 时停止,返回 out[-limit:](limit=0 时立即返回空列表);
|
|
345
|
+
def audit_records(self, limit: int = None):
|
|
346
|
+
"""审计记录读取——轮转后跨分片按时间序(旧片在前)合并。
|
|
347
|
+
|
|
348
|
+
默认保持全量语义(既有调用方零改动);limit=N 取尾部 N 条,供巡检使用
|
|
349
|
+
——有界日志不该被读成新的 O(n) 全量。
|
|
350
|
+
"""
|
|
351
|
+
paths = [os.path.join(self.audit_archive, n) for n in self._audit_shards()]
|
|
352
|
+
paths.append(self.audit_log)
|
|
353
|
+
if limit is None:
|
|
354
|
+
out = []
|
|
355
|
+
for p in paths:
|
|
356
|
+
out.extend(read_jsonl(p))
|
|
357
|
+
return out
|
|
358
|
+
out = []
|
|
359
|
+
for p in reversed(paths): # 从最新往回读,读满 limit 即停
|
|
360
|
+
if len(out) >= limit:
|
|
361
|
+
break
|
|
362
|
+
out = list(read_jsonl(p)) + out
|
|
363
|
+
return out[-limit:]
|
|
364
|
+
|
|
365
|
+
# ---------- 审计日志分片轮转(治本:给无上界增长装上界) ----------
|
|
366
|
+
|
|
367
|
+
# 生效条件:当 self.audit_archive 可被 os.listdir 列出时,返回其中以 "_audit." 开头且以 ".jsonl" 结尾的名字升序列表;listdir 抛 OSError 时返回 [];
|
|
368
|
+
def _audit_shards(self):
|
|
369
|
+
"""归档分片名,序号零填充 ⇒ 字典序 == 时间序。"""
|
|
370
|
+
try:
|
|
371
|
+
names = os.listdir(self.audit_archive)
|
|
372
|
+
except OSError:
|
|
373
|
+
return []
|
|
374
|
+
return sorted(n for n in names
|
|
375
|
+
if n.startswith("_audit.") and n.endswith(".jsonl"))
|
|
376
|
+
|
|
377
|
+
# 生效条件:当 self._audit_index 为 None 时,尝试读取 audit_archive 中 AUDIT_INDEX 并过滤 value 为 dict,读取失败或数据非 dict 时 idx 为空字典;随后把 self._audit_index 设为 idx 并返回;非 None 时不重读直接返回;
|
|
378
|
+
def _load_audit_index(self) -> dict:
|
|
379
|
+
if self._audit_index is None:
|
|
380
|
+
idx = {}
|
|
381
|
+
try:
|
|
382
|
+
with open(os.path.join(self.audit_archive, self.AUDIT_INDEX),
|
|
383
|
+
"r", encoding="utf-8") as f:
|
|
384
|
+
data = json.load(f)
|
|
385
|
+
if isinstance(data, dict):
|
|
386
|
+
idx = {k: v for k, v in data.items() if isinstance(v, dict)}
|
|
387
|
+
except (OSError, ValueError):
|
|
388
|
+
idx = {} # 索引缺失/损坏 → 空起步,自愈补数
|
|
389
|
+
self._audit_index = idx
|
|
390
|
+
return self._audit_index
|
|
391
|
+
|
|
392
|
+
# 生效条件:当 idx 传入时,尝试创建 self.audit_archive 并把 idx 的 JSON(ensure_ascii=False, indent=1, sort_keys=True)原子写入 AUDIT_INDEX;OSError 被吞掉;最后把 self._audit_index 设为 idx,无返回值;
|
|
393
|
+
def _save_audit_index(self, idx: dict):
|
|
394
|
+
try:
|
|
395
|
+
os.makedirs(self.audit_archive, exist_ok=True)
|
|
396
|
+
atomic_write(os.path.join(self.audit_archive, self.AUDIT_INDEX),
|
|
397
|
+
json.dumps(idx, ensure_ascii=False, indent=1, sort_keys=True))
|
|
398
|
+
except OSError:
|
|
399
|
+
pass
|
|
400
|
+
self._audit_index = idx
|
|
401
|
+
|
|
402
|
+
# 生效条件:当 AUDIT_ROTATE_BYTES > 0 且 _audit_writes 自增后能被 AUDIT_PROBE_EVERY 整除,且 audit_log 的 getsize 不小于 AUDIT_ROTATE_BYTES 时,调用 rotate_audit;AUDIT_ROTATE_BYTES<=0、未到探测间隔或 getsize 不足/OSError 时直接返回;
|
|
403
|
+
def _rotate_audit_if_needed(self):
|
|
404
|
+
"""写前闸门:活动日志达阈值即切分(把 stat 摊到 1/AUDIT_PROBE_EVERY)。
|
|
405
|
+
|
|
406
|
+
为什么不逐条 stat:写路径要付的是「每次写入税」,而活动文件由本进程与同
|
|
407
|
+
root 其它进程共同增长,故按固定间隔读一次真实元数据校准——既不漏轮转,
|
|
408
|
+
也不把 O(1) 纪律反向变成 O(n) 写入开销。
|
|
409
|
+
"""
|
|
410
|
+
limit = self.AUDIT_ROTATE_BYTES
|
|
411
|
+
if limit <= 0:
|
|
412
|
+
return
|
|
413
|
+
self._audit_writes = (self._audit_writes or 0) + 1
|
|
414
|
+
if self._audit_writes % self.AUDIT_PROBE_EVERY:
|
|
415
|
+
return
|
|
416
|
+
try:
|
|
417
|
+
if os.path.getsize(self.audit_log) < limit:
|
|
418
|
+
return
|
|
419
|
+
except OSError:
|
|
420
|
+
return
|
|
421
|
+
self.rotate_audit()
|
|
422
|
+
|
|
423
|
+
# 生效条件:当 reason(默认 "size")传入时,在 FileLock 下若 audit_log 的 size >= AUDIT_ROTATE_BYTES 则 os.replace 为 _audit.%06d.jsonl 归档并更新索引/剪枝/自述留痕后返回 {"shard","bytes","events","pruned"};size 不足、getsize OSError 或 os.replace 失败时返回 None;
|
|
424
|
+
def rotate_audit(self, reason: str = "size"):
|
|
425
|
+
"""把活动审计日志切分为归档分片(os.replace 原子,不重写一个字节)。
|
|
426
|
+
|
|
427
|
+
语义边界(诚实面):
|
|
428
|
+
· 分片内容与轮转前**逐行一致**(rename 不动字节);最新记录始终在活动文件
|
|
429
|
+
`_audit.jsonl` 中——读尾部取最新记录的调用方不受轮转影响;
|
|
430
|
+
· 并发由 FileLock + 「rename 前复检大小 / 失败即返回 None」兜住:抢输的
|
|
431
|
+
进程不重复切分,也不丢记录(记录要么在旧片、要么在活动文件);
|
|
432
|
+
· 保留策略只淘汰**分片**,且淘汰名单写进审计(不静默丢证据)。
|
|
433
|
+
"""
|
|
434
|
+
with FileLock(os.path.join(self.root, "_audit.rotate.lock"), timeout=5.0):
|
|
435
|
+
try:
|
|
436
|
+
size = os.path.getsize(self.audit_log)
|
|
437
|
+
except OSError:
|
|
438
|
+
return None
|
|
439
|
+
if size < self.AUDIT_ROTATE_BYTES:
|
|
440
|
+
return None # 已被并发写者轮转
|
|
441
|
+
os.makedirs(self.audit_archive, exist_ok=True)
|
|
442
|
+
name = "_audit.%06d.jsonl" % self._next_shard_seq()
|
|
443
|
+
dst = os.path.join(self.audit_archive, name)
|
|
444
|
+
try:
|
|
445
|
+
os.replace(self.audit_log, dst)
|
|
446
|
+
except OSError:
|
|
447
|
+
return None # 抢输(文件已被移走)→ 让位,不报错
|
|
448
|
+
scale = self._audit_count_shard(dst, size) # 分档:有界扫描 / 只读量级
|
|
449
|
+
events = scale["events"]
|
|
450
|
+
idx = self._load_audit_index()
|
|
451
|
+
idx[name] = {"bytes": size, "events": events, "exact": scale["exact"],
|
|
452
|
+
"t": round(time.time(), 3), "reason": reason}
|
|
453
|
+
self._save_audit_index(idx)
|
|
454
|
+
pruned = self._prune_audit_shards()
|
|
455
|
+
self._audit_writes = 0
|
|
456
|
+
try: # 自述留痕:轮转本身可审计
|
|
457
|
+
append_jsonl(self.audit_log,
|
|
458
|
+
{"t": time.time(), "op": "audit_rotate", "id": name,
|
|
459
|
+
"actor": self.actor, "bytes": size, "events": events,
|
|
460
|
+
"pruned": pruned, "reason": reason,
|
|
461
|
+
"session": getattr(self, "session", None)})
|
|
462
|
+
except OSError:
|
|
463
|
+
pass
|
|
464
|
+
return {"shard": name, "bytes": size, "events": events, "pruned": pruned}
|
|
465
|
+
|
|
466
|
+
# 生效条件:当 path 与 size 传入时,size 为 None 则先 getsize(OSError 返回 bytes/events 0 exact True);size <= AUDIT_COUNT_MAX_BYTES 时返回 count_jsonl(path) 精确条数 exact True;超过时返回 _log_scale(path) 的事件数 exact False;
|
|
467
|
+
def _audit_count_shard(self, path, size: int = None) -> dict:
|
|
468
|
+
"""分片条数读数:有界分片给精确值,超大历史分片只给量级。
|
|
469
|
+
|
|
470
|
+
为什么分档:阈值内的分片扫描是**有界**代价(≤ AUDIT_COUNT_MAX_BYTES);但
|
|
471
|
+
历史遗留的超大文件(本机首个分片即 4.0 GB)若在轮转/体检路径上全量解析,
|
|
472
|
+
就会把「一次调用堵死整条通道」原样复现——故超阈值退回 _log_scale 元数据
|
|
473
|
+
口径并如实标注 exact=False(要精确值走离线工具,不在写路径上付 O(n))。
|
|
474
|
+
"""
|
|
475
|
+
if size is None:
|
|
476
|
+
try:
|
|
477
|
+
size = os.path.getsize(path)
|
|
478
|
+
except OSError:
|
|
479
|
+
return {"bytes": 0, "events": 0, "exact": True}
|
|
480
|
+
if size <= self.AUDIT_COUNT_MAX_BYTES:
|
|
481
|
+
return {"bytes": size, "events": count_jsonl(path), "exact": True}
|
|
482
|
+
s = self._log_scale(path)
|
|
483
|
+
return {"bytes": size, "events": s["events"], "exact": False}
|
|
484
|
+
|
|
485
|
+
# 生效条件:在已用 root 构造的实例上遍历 _audit_shards() 的分片名,对 `_audit.<n>.jsonl` 形式中 int(n) 成功的取最大值 top(解析失败 continue、无可解析项时 top=0),返回 top+1。
|
|
486
|
+
def _next_shard_seq(self) -> int:
|
|
487
|
+
top = 0
|
|
488
|
+
for n in self._audit_shards():
|
|
489
|
+
try:
|
|
490
|
+
top = max(top, int(n[len("_audit."):-len(".jsonl")]))
|
|
491
|
+
except ValueError:
|
|
492
|
+
continue
|
|
493
|
+
return top + 1
|
|
494
|
+
|
|
495
|
+
# 生效条件:当 AUDIT_KEEP_SHARDS > 0 且分片数超过该值时,计算 gone=shards[:-keep] 并逐个尝试 os.remove、成功则从索引 pop(OSError 则 continue),最后保存索引并返回 gone;keep<=0 或 gone 为空时返回 [];
|
|
496
|
+
def _prune_audit_shards(self):
|
|
497
|
+
"""保留最近 AUDIT_KEEP_SHARDS 个分片、淘汰更旧的(≤0 表示不淘汰)。"""
|
|
498
|
+
keep = self.AUDIT_KEEP_SHARDS
|
|
499
|
+
if keep <= 0:
|
|
500
|
+
return []
|
|
501
|
+
shards = self._audit_shards()
|
|
502
|
+
gone = shards[:-keep] if len(shards) > keep else []
|
|
503
|
+
if not gone:
|
|
504
|
+
return []
|
|
505
|
+
idx = self._load_audit_index()
|
|
506
|
+
for n in gone:
|
|
507
|
+
try:
|
|
508
|
+
os.remove(os.path.join(self.audit_archive, n))
|
|
509
|
+
except OSError:
|
|
510
|
+
continue # 删不掉就留着:不假装已淘汰
|
|
511
|
+
idx.pop(n, None)
|
|
512
|
+
self._save_audit_index(idx)
|
|
513
|
+
return gone
|
|
514
|
+
|
|
515
|
+
# 生效条件:在已用 root 构造的实例上以 _log_scale(audit_log) 取活动读数为 active,对磁盘现有分片 present 中未登记者按 _audit_count_shard 采纳进 idx(OSError 则 continue,有采纳才写回索引),返回 active 并附 shards/shard_bytes/shard_events(均只统计 k in present)/oversized(非 exact 分片数)/total_bytes/total_events(active["events"] 为 None 时取 None)/total_exact/rotate_bytes=self.AUDIT_ROTATE_BYTES/keep_shards=self.AUDIT_KEEP_SHARDS。
|
|
516
|
+
def audit_scale(self) -> dict:
|
|
517
|
+
"""审计面量级读数(O(1) 稳态):活动文件实时读数 + 归档分片索引缓存。
|
|
518
|
+
|
|
519
|
+
为什么不逐片重数(第 4 条:指标与代价匹配):轮转后「总计」= 活动 + 各分片,
|
|
520
|
+
逐片全量数会把体检摊成 O(分片总字节)。分片是**封存文件**(内容不再变),故
|
|
521
|
+
封存时数一次、落索引缓存即得稳态 O(1);目录里出现未登记分片(外部手工放入)
|
|
522
|
+
时自愈补数一次——绝不假装与磁盘一致。
|
|
523
|
+
"""
|
|
524
|
+
active = self._log_scale(self.audit_log)
|
|
525
|
+
present = self._audit_shards()
|
|
526
|
+
idx = self._load_audit_index()
|
|
527
|
+
healed = False
|
|
528
|
+
for n in present:
|
|
529
|
+
if n in idx:
|
|
530
|
+
continue
|
|
531
|
+
p = os.path.join(self.audit_archive, n)
|
|
532
|
+
try:
|
|
533
|
+
sc = self._audit_count_shard(p) # 分档:有界扫描 / 只读量级
|
|
534
|
+
idx[n] = {"bytes": sc["bytes"], "events": sc["events"],
|
|
535
|
+
"exact": sc["exact"], "t": round(time.time(), 3),
|
|
536
|
+
"reason": "adopted"}
|
|
537
|
+
healed = True
|
|
538
|
+
except OSError:
|
|
539
|
+
continue
|
|
540
|
+
if healed:
|
|
541
|
+
self._save_audit_index(idx)
|
|
542
|
+
shard_bytes = sum(int(v.get("bytes") or 0) for k, v in idx.items() if k in present)
|
|
543
|
+
shard_events = sum(int(v.get("events") or 0) for k, v in idx.items() if k in present)
|
|
544
|
+
oversized = sum(1 for n in present if not idx.get(n, {}).get("exact", True))
|
|
545
|
+
out = dict(active)
|
|
546
|
+
out.update({
|
|
547
|
+
"shards": len(present),
|
|
548
|
+
"shard_bytes": shard_bytes,
|
|
549
|
+
"shard_events": shard_events,
|
|
550
|
+
"oversized": oversized, # 非精确分片数(历史遗留超大文件)
|
|
551
|
+
"total_bytes": active["bytes"] + shard_bytes,
|
|
552
|
+
"total_events": (active["events"] + shard_events
|
|
553
|
+
if active["events"] is not None else None),
|
|
554
|
+
"total_exact": bool(active["exact"]) and oversized == 0,
|
|
555
|
+
"rotate_bytes": self.AUDIT_ROTATE_BYTES,
|
|
556
|
+
"keep_shards": self.AUDIT_KEEP_SHARDS,
|
|
557
|
+
})
|
|
558
|
+
return out
|
|
300
559
|
|
|
301
560
|
# ================= 2. role 分层索引 =================
|
|
302
561
|
|
|
562
|
+
# 生效条件:当 node_id 与 content 传入时,先若有 role 非 None 则放入 kw,调用父类 add 得 nid;仅当索引中 e 非 None 且 role 非 None 时把 role 写入索引并标记 dirty;随后记 audit 并返回 nid;
|
|
303
563
|
def add(self, node_id: str, content: str, layer: str = "knowledge",
|
|
304
564
|
role: str = None, **kw) -> str:
|
|
305
565
|
"""在父类 add 之上:写入 role(frontmatter + 索引),默认 role=None(知识)。"""
|
|
@@ -314,33 +574,88 @@ class MdCGOS(MdCG):
|
|
|
314
574
|
payload_hash=_sig(content))
|
|
315
575
|
return nid
|
|
316
576
|
|
|
577
|
+
# 生效条件:在已用 root 构造的实例上遍历 index["nodes"],恒剔除 layer 为 rejected/unresolved/goals 的节点,session 为真值而 e["session"] 不等于它时剔除,e["branch_id"] 不在 (None, branch) 时剔除(branch=None 时只留 branch_id 为 None 者),validity 为真值而 trust.is_expired(e) 为真时剔除(**只排已过期,not_yet 保留**),layer 为真值而 layer 不等时剔除,roles 不为 None 时仅留 role 落在 roles 内的节点;view 为真值时 role 维度裁决权移交 roleviews.matches(receipt=工作角色白名单须绕过默认剔除才可达,非法 view ValueError),view 为假值且 include_work 为假时剔除 WORK_ROLES 角色,时间算子启用时按 time_axis 轴过滤(效力轴不可判定 fail-open、观察轴不可判定 fail-closed 并入 self._time_filter_stat),其余收集进 out 返回。
|
|
317
578
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
318
|
-
session=None
|
|
579
|
+
session=None, branch=None, validity=None,
|
|
580
|
+
start_time=None, end_time=None, start_operator=None,
|
|
581
|
+
end_operator=None, time_axis=None, view=None):
|
|
319
582
|
"""候选池:按层 + role 过滤。默认剔除工作角色(工具输出/命令/编辑)。
|
|
320
583
|
|
|
321
584
|
session:会话归属过滤(frontmatter.session,写入时自动落盘)——
|
|
322
585
|
多会话共用一个 root 时,按它区分「本会话记忆 / 其他会话记忆」。
|
|
586
|
+
branch:分支可见性(记忆演化分支④)——None(默认)时分支实验
|
|
587
|
+
节点全部隐身(实验不污染主支检索);branch=<id> 时主支 + 该分支
|
|
588
|
+
可见、其他分支仍隐身。
|
|
589
|
+
validity:时效过滤(**显式启用**,缺省 None 不过滤)——真值时剔除
|
|
590
|
+
**已过期**(valid_until 已过 / expired)节点;**未生效(valid_from
|
|
591
|
+
未到 / not_yet)一律保留**,因其与「已失效」语义相反(scrub 纪律
|
|
592
|
+
「valid_from 绝不并入 _EXPIRY_KEYS」),预约/计划类记忆在生效前仍可召回。
|
|
593
|
+
判定走 trust.validity 唯一真源;时间轴缺失/端点不可解析 → 不过滤(不猜测)。
|
|
594
|
+
start_time/end_time/start_operator/end_operator/time_axis:时间算子
|
|
595
|
+
(阶段二 4.1,**显式启用**)——按 `time_axis` 轴把候选收敛到查询时间窗内。
|
|
596
|
+
轴未指定 → 回落 `effective`;轴/算子非法、孤 operator、start>end 一律
|
|
597
|
+
`ValueError`(fail-open 只针对**节点缺字段**,不针对**调用方误用**)。
|
|
598
|
+
效力轴缺字段 fail-open(保留);观察轴缺字段 fail-closed(剔除)。
|
|
599
|
+
view:角色化读取视图(第四阶段 6.1,**显式启用**,缺省 None 零变更)
|
|
600
|
+
——roleviews.ROLE_VIEWS 规则表:main/verifier 声明 content_kind/
|
|
601
|
+
layer 资格并沿用「剔除工作角色」默认口径;receipt=工作角色白名单
|
|
602
|
+
(恰是默认候选池剔除的补集,须接管 role 维度裁决才可达)。非法
|
|
603
|
+
view ValueError(fail-closed 只针对调用方误用);roles 显式参数
|
|
604
|
+
仍优先于 view。
|
|
323
605
|
"""
|
|
606
|
+
now = time.time() if validity else None
|
|
324
607
|
out = []
|
|
325
608
|
for e in self.index["nodes"].values():
|
|
326
609
|
if e.get("layer") in ("rejected", "unresolved", "goals"):
|
|
327
610
|
continue # 负记忆走覆盖标记;目标只做定向,都不进正排
|
|
328
611
|
if session and e.get("session") != session:
|
|
329
612
|
continue
|
|
613
|
+
if e.get("branch_id") not in (None, branch):
|
|
614
|
+
continue
|
|
615
|
+
if validity and trust.is_expired(e, now=now):
|
|
616
|
+
continue
|
|
330
617
|
if layer and e.get("layer") != layer:
|
|
331
618
|
continue
|
|
332
619
|
r = e.get("role")
|
|
333
620
|
if roles is not None:
|
|
334
621
|
if r not in roles:
|
|
335
622
|
continue
|
|
623
|
+
elif view is not None:
|
|
624
|
+
# 角色化读取视图(第四阶段 6.1):view 非空时接管 role 维度
|
|
625
|
+
# 裁决权(receipt=工作角色白名单,须绕过默认剔除才可达),
|
|
626
|
+
# 并叠加 content_kind/layer 资格;非法 view ValueError。
|
|
627
|
+
if not roleviews.matches(e, view):
|
|
628
|
+
continue
|
|
336
629
|
elif not include_work and r in WORK_ROLES:
|
|
337
630
|
continue
|
|
338
631
|
out.append(e)
|
|
632
|
+
# ---- 时间算子(阶段二 4.1):候选层**唯一**过滤点 ----
|
|
633
|
+
# 与 validity 各司其职,互不替代:validity=「是否已失效」(默认关、只排过期);
|
|
634
|
+
# 时间算子=「是否落在查询时间窗内」(轴 + 算子显式启用,双端可空=无界)。
|
|
635
|
+
en, ax, why = trust.check_time_args(start_time, end_time, start_operator,
|
|
636
|
+
end_operator, time_axis)
|
|
637
|
+
# 该属性供调用方(search / search_rrf)取审计块;紧接调用后即读,
|
|
638
|
+
# 无跨调用竞态(库按实例串行使用)。
|
|
639
|
+
self._time_filter_stat = None
|
|
640
|
+
if not en:
|
|
641
|
+
if why:
|
|
642
|
+
raise ValueError(why) # fail-closed:误用不静默降级
|
|
643
|
+
return out
|
|
644
|
+
qs, qe = trust.parse_time(start_time), trust.parse_time(end_time)
|
|
645
|
+
out, _dropped, _missing = trust.filter_by_time(
|
|
646
|
+
out, ax, qs, qe, start_operator, end_operator)
|
|
647
|
+
self._time_filter_stat = trust.time_filter_meta(
|
|
648
|
+
axis=ax,
|
|
649
|
+
mode="endpoint" if (start_operator or end_operator) else "overlap",
|
|
650
|
+
start=qs, end=qe, start_operator=start_operator,
|
|
651
|
+
end_operator=end_operator, dropped=_dropped,
|
|
652
|
+
axis_missing=_missing, applied=True)
|
|
339
653
|
return out
|
|
340
654
|
|
|
341
655
|
# ================= 资格判定(legacy 记忆豁免) =================
|
|
342
656
|
|
|
343
657
|
@staticmethod
|
|
658
|
+
# 生效条件:node_dict 的 frontmatter 中 ccg_exempt 为真且 content 去空白后非空时返回 DEFER 记录,否则以 (node_dict, query, context) 转 MdCG.judge_qualification;
|
|
344
659
|
def judge_qualification(node_dict, query: str, context=None):
|
|
345
660
|
"""在父类四态之上支持 legacy 记忆(迁移进来的自由文本)。
|
|
346
661
|
|
|
@@ -358,6 +673,7 @@ class MdCGOS(MdCG):
|
|
|
358
673
|
|
|
359
674
|
# ================= 检索(含 role 过滤 + 四路召回) =================
|
|
360
675
|
|
|
676
|
+
# 生效条件:当 terms 传入时,仅对 layer 为 rejected/unresolved 且 _read 返回 fm 为真、_open_content 后 content 含任一 term 的节点收集到 out;否则跳过;
|
|
361
677
|
def _neg_coverage(self, terms):
|
|
362
678
|
"""负记忆覆盖:查询词是否已被 rejected/unresolved 覆盖。"""
|
|
363
679
|
out = []
|
|
@@ -372,24 +688,38 @@ class MdCGOS(MdCG):
|
|
|
372
688
|
out.append(e)
|
|
373
689
|
return out
|
|
374
690
|
|
|
691
|
+
# 生效条件:query strip 后为空即返回 ([], {"tier": None, "reason": "empty_query", "scanned": 0});pool_cfg 由 pooling.resolve(pooling.from_env(pools)) 解析;_candidates 为空即返回 no_candidates;其余与 MdCG.search 同构(T0–T3 阶梯 + 资格判定),并按 roles/include_work 默认剔除工具输出与命令类 role,view 真值时移交 role 维度裁决权给 roleviews.matches(见 _candidates),validity 真值时候选层剔除已过期节点;
|
|
375
692
|
def search(self, query: str, layer: str = None, k: int = 20,
|
|
376
693
|
context=None, min_results: int = 1, record: bool = True,
|
|
377
694
|
include_neg: bool = True, judge: bool = True,
|
|
378
695
|
roles=None, include_work: bool = False, pools=None,
|
|
379
|
-
session=None
|
|
696
|
+
session=None, branch=None, validity=None,
|
|
697
|
+
start_time=None, end_time=None, start_operator=None,
|
|
698
|
+
end_operator=None, time_axis=None, view=None):
|
|
380
699
|
"""在父类语义之上加 role 过滤(默认剔除工具输出/命令/编辑)。
|
|
381
700
|
|
|
382
701
|
返回 (results, meta),与 MdCG.search 完全同构(T0–T3 阶梯 + 资格判定)。
|
|
383
702
|
pools:§七 召回分池(None=关闭原行为 / True=内置表 / dict=自定义表)。
|
|
703
|
+
validity:时效过滤(显式启用,缺省不过滤)——只排已过期,未生效保留;
|
|
704
|
+
view:角色化读取视图(第四阶段 6.1,显式启用,缺省 None 零变更);
|
|
705
|
+
详见 _candidates。
|
|
384
706
|
"""
|
|
385
707
|
q = (query or "").strip()
|
|
386
708
|
if not q:
|
|
387
709
|
return [], {"tier": None, "reason": "empty_query", "scanned": 0}
|
|
388
710
|
pool_cfg = pooling.resolve(pooling.from_env(pools))
|
|
389
711
|
entries = self._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
390
|
-
session=session
|
|
712
|
+
session=session, branch=branch, validity=validity,
|
|
713
|
+
start_time=start_time, end_time=end_time,
|
|
714
|
+
start_operator=start_operator,
|
|
715
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
716
|
+
view=view)
|
|
717
|
+
_tf = getattr(self, "_time_filter_stat", None)
|
|
391
718
|
if not entries:
|
|
392
|
-
|
|
719
|
+
_m = {"tier": None, "reason": "no_candidates", "scanned": 0}
|
|
720
|
+
if _tf:
|
|
721
|
+
_m["time_filter"] = _tf
|
|
722
|
+
return [], _m
|
|
393
723
|
|
|
394
724
|
terms = expand_query_terms(q)
|
|
395
725
|
# qb 与 _score 文档侧口径对齐(文档侧已是 normalize_en 小写化),
|
|
@@ -397,6 +727,13 @@ class MdCGOS(MdCG):
|
|
|
397
727
|
# + 英→中语素 bigram 补充(与 mdcg.search/_lexical 打分口径同步)
|
|
398
728
|
qb = bigrams(normalize_en(q)) | en_zh_bigrams(q)
|
|
399
729
|
stat = {"scanned": 0, "query": q}
|
|
730
|
+
# 时间算子审计(阶段二 4.1):候选层过滤在 `_candidates` 已完成,此处把
|
|
731
|
+
# 审计块交给父类 `_emit` 落进 meta —— 与 search_rrf 的 `stat["time_filter"]`
|
|
732
|
+
# 同构。缺此步则过滤**确实生效但审计不可见**:调用方无法区分「未启用过滤」
|
|
733
|
+
# 与「启用了、但候选全 fail-open 保留」,属静默(审计不可见即不可裁决)。
|
|
734
|
+
# 仅在启用时落键,保默认关时 meta 键集合不变。
|
|
735
|
+
if _tf:
|
|
736
|
+
stat["time_filter"] = _tf
|
|
400
737
|
route_bucket = None
|
|
401
738
|
if context is not None:
|
|
402
739
|
ctx = context if isinstance(context, dict) else {}
|
|
@@ -405,6 +742,7 @@ class MdCGOS(MdCG):
|
|
|
405
742
|
big_scores = routing.big_domain_score_breakdown(terms)
|
|
406
743
|
neg_coverage = self._neg_coverage(terms) if include_neg else []
|
|
407
744
|
|
|
745
|
+
# 生效条件:对 docs 调 self._score 后,若其中分数 >0 的条数达到(search 作用域内的)min_results 就返回 self._emit(scored, k, tier, stat, route_bucket, record, len(docs), ...),否则返回 None。
|
|
408
746
|
def try_stage(docs, tier):
|
|
409
747
|
scored = self._score(docs, q, qb, pool_cfg)
|
|
410
748
|
valid = sum(1 for _, s in scored if s > 0)
|
|
@@ -428,31 +766,76 @@ class MdCGOS(MdCG):
|
|
|
428
766
|
if out:
|
|
429
767
|
return out
|
|
430
768
|
|
|
769
|
+
# T1'(reach):大域收敛 → 条件门控 → 图扩散(2026-09-18 新增)。
|
|
770
|
+
# 层级仍报 TIER_GLOBAL_LIKE(它就是「全量 LIKE」阶段的收敛实现,test_p0 契约
|
|
771
|
+
# 「无 context 走全量阶梯」不因优化而变);收敛与否由 meta.reach* 字段区分。
|
|
772
|
+
# 收敛集是 LIKE 命中集与词法打分>0 集合的**超集**(必要条件倒排,见
|
|
773
|
+
# md_cg/test_reach.py 包含性断言),故本阶段不可能丢召回;任一不适用条件
|
|
774
|
+
# (MDCG_REACH 未开启 / 单字符 term / 索引不可用 / 收敛集为空)整段跳过,由下方全量阶段兜底。
|
|
775
|
+
# 默认关(契约 §7「不在默认路径上启用新行为」):整段不进入 → meta 键集合与改动前逐字节一致。
|
|
776
|
+
reach_entries, _rstat = None, {}
|
|
777
|
+
if reach.enabled():
|
|
778
|
+
reach_entries, _rstat = reach.narrow(self, entries, terms, qb, context, q=q)
|
|
779
|
+
if _rstat:
|
|
780
|
+
stat.update(_rstat)
|
|
781
|
+
if reach_entries is not None:
|
|
782
|
+
_pre_scan = int(stat.get("scanned") or 0) # 收敛阶段读盘基线(供回退审计)
|
|
783
|
+
docs_r = self._read_many(reach_entries, stat)
|
|
784
|
+
_dif = set(_rstat.get("reach_diffused_paths") or ())
|
|
785
|
+
hits_r = [d for d in docs_r
|
|
786
|
+
if self._like(d[2], d[1], terms)
|
|
787
|
+
or (semantic_on() and d[1].get("semantic"))
|
|
788
|
+
or d[0].get("path") in _dif] # 图扩散补召回:无词面命中也放行进打分
|
|
789
|
+
stat["pre_cap"] = len(hits_r) # 与 T2 同序:截断**前**的候选数
|
|
790
|
+
stat["cap"] = GLOBAL_CAP
|
|
791
|
+
# 与 T2 **无条件**同序调用(不可按 cap 短路:cut_by_relevance 还会写
|
|
792
|
+
# 池账 pool_taken/cands/lost,短路会让 meta.pools.taken 缺失 → test_p43(13) 红)
|
|
793
|
+
hits_r, _rep = cut_by_relevance(hits_r, self._score(hits_r, q, qb, pool_cfg),
|
|
794
|
+
GLOBAL_CAP, pools=pool_cfg,
|
|
795
|
+
key_of=pooling.doc_key, stat=stat)
|
|
796
|
+
pooling.record_audit(stat, _rep)
|
|
797
|
+
out = try_stage(hits_r, TIER_GLOBAL_LIKE)
|
|
798
|
+
if out:
|
|
799
|
+
return out
|
|
800
|
+
# 收敛阶段未产出结果 → 继续走下方全量 T2/T3;此时必须**改写 reach 审计**,
|
|
801
|
+
# 否则 meta.reach 谎报 converged、A3 也会把「收敛读+全量读」双计当成收敛(r14 复核取证)
|
|
802
|
+
stat["reach"] = "reverted" # 已回退:本次结果不是收敛路径产出的
|
|
803
|
+
stat["reach_reverted"] = True
|
|
804
|
+
# 收敛阶段确实读过盘 → 如实暴露其读盘量,A3 可据此扣减(**不**回滚 scanned:
|
|
805
|
+
# 累计读盘是事实;也**不**清除 reach_build_docs:首建成本真实发生过,r16 复核取证)
|
|
806
|
+
stat["reach_reverted_docs"] = int(stat.get("scanned") or 0) - _pre_scan
|
|
807
|
+
for _k in ("reach_seed", "reach_diffused", "reach_hops", "reach_diffused_paths",
|
|
808
|
+
"reach_fresh_nodes"):
|
|
809
|
+
stat.pop(_k, None)
|
|
810
|
+
|
|
811
|
+
# 截断依据=相关度(同 MdCG.search:cap 值不变,改的是拿什么排序)
|
|
431
812
|
docs_all = self._read_many(entries, stat)
|
|
432
813
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
433
814
|
hits = [d for d in docs_all if self._like(d[2], d[1], terms)
|
|
434
815
|
or (semantic_on() and d[1].get("semantic"))]
|
|
435
816
|
stat["pre_cap"] = len(hits)
|
|
436
817
|
stat["cap"] = GLOBAL_CAP
|
|
437
|
-
hits, _rep =
|
|
438
|
-
|
|
818
|
+
hits, _rep = cut_by_relevance(hits, self._score(hits, q, qb, pool_cfg),
|
|
819
|
+
GLOBAL_CAP, pools=pool_cfg,
|
|
820
|
+
key_of=pooling.doc_key, stat=stat)
|
|
439
821
|
pooling.record_audit(stat, _rep)
|
|
440
822
|
out = try_stage(hits, TIER_GLOBAL_LIKE)
|
|
441
823
|
if out:
|
|
442
824
|
return out
|
|
443
825
|
|
|
444
|
-
docs_all.sort(key=lambda d: (-float(d[1].get("importance") or 0),
|
|
445
|
-
-float(d[1].get("created_at") or 0)))
|
|
446
826
|
stat["pre_cap"] = len(docs_all)
|
|
447
827
|
stat["cap"] = GLOBAL_CAP
|
|
448
|
-
picked, _rep =
|
|
449
|
-
|
|
828
|
+
picked, _rep = cut_by_relevance(docs_all,
|
|
829
|
+
self._score(docs_all, q, qb, pool_cfg),
|
|
830
|
+
GLOBAL_CAP, pools=pool_cfg,
|
|
831
|
+
key_of=pooling.doc_key, stat=stat)
|
|
450
832
|
pooling.record_audit(stat, _rep)
|
|
451
833
|
scored = self._score(picked, q, qb, pool_cfg)
|
|
452
834
|
return self._emit(scored, k, TIER_GLOBAL_SCAN, stat, route_bucket,
|
|
453
835
|
record, len(picked), judge,
|
|
454
836
|
context, neg_coverage, big_domain, big_scores, pool_cfg)
|
|
455
837
|
|
|
838
|
+
# 生效条件:当 query、entries、stat 传入时,先以 terms/qb 从 entries 读取文档并保留 LIKE 命中或(semantic_on 且 frontmatter.semantic)的项;若 hits 为空则按 importance/created_at 降序取前 GLOBAL_CAP 作为兜底;随后对 hits 打分并按相关度与 importance 降序排序,超过 GLOBAL_CAP 时截断并更新 stat,否则返回全部 scored;
|
|
456
839
|
def _lexical(self, query, entries, stat):
|
|
457
840
|
"""词法路径:LIKE 预筛 + 二元组相似度(口径见 mdcg.SCORE_MODE)。
|
|
458
841
|
|
|
@@ -460,7 +843,9 @@ class MdCGOS(MdCG):
|
|
|
460
843
|
LIKE 命中集沿 entries(目录枚举序)排列,插入序截断会让本路候选池
|
|
461
844
|
随写入顺序漂移、不可复算,并可能把与查询最相关的节点随机丢弃。
|
|
462
845
|
LIKE 全空时兜底池改用 importance/created_at 序(与 search 主路径同口径),
|
|
463
|
-
不再取插入序前 CAP
|
|
846
|
+
不再取插入序前 CAP。返回序**恒为相关度降序**——原实现仅在超 cap 时
|
|
847
|
+
排序,≤cap 时直接返回 entries 枚举序,下游 seed/融合会拿到无语义依据
|
|
848
|
+
的顺序。
|
|
464
849
|
"""
|
|
465
850
|
terms = expand_query_terms(query)
|
|
466
851
|
# qb 与文档侧 normalize_en 口径对齐(同 MdCGOS.search,防大小写断裂)
|
|
@@ -472,19 +857,29 @@ class MdCGOS(MdCG):
|
|
|
472
857
|
hits = [d for d in docs if self._like(d[2], d[1], terms)
|
|
473
858
|
or (semantic_on() and d[1].get("semantic"))]
|
|
474
859
|
if not hits:
|
|
860
|
+
# 兜底池(LIKE 全空 = 无相关度信号):截断依据=importance/created_at
|
|
861
|
+
# 序,确定可复算;此时 bigram 部分匹配不足以定序(共现噪声),
|
|
862
|
+
# 故本路不做「先全量打分再截断」。
|
|
475
863
|
hits = sorted(
|
|
476
864
|
docs, key=lambda d: (-float(d[1].get("importance") or 0),
|
|
477
|
-
-float(d[1].get("created_at") or 0)
|
|
865
|
+
-float(d[1].get("created_at") or 0),
|
|
866
|
+
str(d[0].get("id") or ""))
|
|
478
867
|
)[:GLOBAL_CAP]
|
|
868
|
+
stat["cut_order"] = "importance_fallback"
|
|
479
869
|
scored = self._score(hits, query, qb)
|
|
870
|
+
# 恒按相关度排序(原实现仅超 cap 时排序 → ≤cap 时返回 entries 枚举序,
|
|
871
|
+
# 下游 seed/融合拿到无语义依据的顺序,不可复算)
|
|
872
|
+
scored.sort(key=lambda x: (-x[1],
|
|
873
|
+
-float(x[0]["frontmatter"].get("importance") or 0),
|
|
874
|
+
str(x[0].get("id") or "")))
|
|
480
875
|
if len(scored) > GLOBAL_CAP:
|
|
481
876
|
stat["pre_cap"] = len(scored)
|
|
482
877
|
stat["cap"] = GLOBAL_CAP
|
|
483
|
-
|
|
484
|
-
-float(x[0]["frontmatter"].get("importance") or 0)))
|
|
878
|
+
stat["cut_order"] = "relevance"
|
|
485
879
|
return scored[:GLOBAL_CAP]
|
|
486
880
|
return scored
|
|
487
881
|
|
|
882
|
+
# 生效条件:context 为 None 时返回 [];否则用 routing.bucket_dir(routing.route_key(ctx, ctx.get("tags"))) 取桶(context 非 dict 时 ctx 按 {} 处理),entries 中 bucket 等于该桶的经 self._read_many + self._score(docs, query, bigrams(query)) 返回,无命中则返回 []。
|
|
488
883
|
def _path_bucket(self, query, entries, context):
|
|
489
884
|
"""条件桶路径:命中路由桶的节点优先。"""
|
|
490
885
|
if context is None:
|
|
@@ -498,6 +893,7 @@ class MdCGOS(MdCG):
|
|
|
498
893
|
docs = self._read_many(inb, stat)
|
|
499
894
|
return self._score(docs, query, bigrams(query))
|
|
500
895
|
|
|
896
|
+
# 生效条件:当 query 与 entries 传入时,仅当 entry 的 tags 中存在长度 >=2 且 t 在 query 中或 query 在 t 中的项时,读取并追加该节点及分数 1.0;否则跳过;返回 out;
|
|
501
897
|
def _path_entity(self, query, entries):
|
|
502
898
|
"""实体路径:tags 命中。(返回节点字典,与 _lexical 同构)"""
|
|
503
899
|
out = []
|
|
@@ -514,6 +910,7 @@ class MdCGOS(MdCG):
|
|
|
514
910
|
"content": c, "path": e["path"]}, 1.0))
|
|
515
911
|
return out
|
|
516
912
|
|
|
913
|
+
# 生效条件:当 seeds 非空时,仅取前 5 个种子,从每个种子节点的 frontmatter.edges 取 target(dict 取 target,否则 str(edge)),若 target 在 entries 映射中且不在 seed_ids 中则读取并追加分数 s*0.5;seeds 为空返回 [];depth 默认 1 但本段未使用;
|
|
517
914
|
def _path_graph(self, query, entries, seeds, depth=1):
|
|
518
915
|
"""图扩展路径:从词法种子沿 edges 一跳扩展。"""
|
|
519
916
|
if not seeds:
|
|
@@ -542,6 +939,7 @@ class MdCGOS(MdCG):
|
|
|
542
939
|
"content": c, "path": e["path"]}, s * 0.5))
|
|
543
940
|
return out
|
|
544
941
|
|
|
942
|
+
# 生效条件:当 seeds 非空且 seed_map 非空时,用 relation_types 或 CHAIN_TYPES_DEFAULT、max_depth 或 MAX_DEPTH_DEFAULT、decay 调用 chain.expand_from_seeds,仅保留 best 中仍存在于 entries(按 id(e))的节点,读取成功者加入 scored 并按分数降序返回 (scored, prov);seeds/seed_map/best 为空返回 ([], {});
|
|
545
943
|
def _path_chain(self, query, entries, seeds, context=None,
|
|
546
944
|
relation_types=None, max_depth=None, decay=0.9):
|
|
547
945
|
"""关系链路径:沿 causal/sequential/applies_to 边**多跳**扩散。
|
|
@@ -592,9 +990,10 @@ class MdCGOS(MdCG):
|
|
|
592
990
|
prov[out_id] = {"chain": info["chain"]["nodes"],
|
|
593
991
|
"conditions": info["conditions"],
|
|
594
992
|
"depth": info["depth"]}
|
|
595
|
-
scored.sort(key=lambda x: -x[1])
|
|
993
|
+
scored.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
|
|
596
994
|
return scored, prov
|
|
597
995
|
|
|
996
|
+
# 生效条件:以 expand or expand_query_terms_weighted 作扩展函数并对 query 调用(结果为假值按 {} 处理),从其中 pop "__source__"(缺键为 "whitebox")得 source,tw 为空返回 ([], source),否则对 entries 中存在 _weighted_coverage>0 的条目按 0.6·cov+0.3·aff+0.1·ctx_aff(context 非 None 时以 route_key(ctx, ctx.get("tags")) 得 ctx_domain,context 非 dict 时用 {})打分,返回 (out, source)。
|
|
598
997
|
def _path_fuzzy(self, query, entries, context=None, expand=None):
|
|
599
998
|
"""模糊路径(分级隶属度):返回 (scored, source)。
|
|
600
999
|
|
|
@@ -611,7 +1010,8 @@ class MdCGOS(MdCG):
|
|
|
611
1010
|
return [], source
|
|
612
1011
|
dom_scores = routing.big_domain_score_weighted(tw)
|
|
613
1012
|
dom_total = sum(dom_scores.values()) or 1.0
|
|
614
|
-
top_domains = sorted(dom_scores.items(),
|
|
1013
|
+
top_domains = sorted(dom_scores.items(),
|
|
1014
|
+
key=lambda kv: (-kv[1], str(kv[0])))[:3]
|
|
615
1015
|
ctx_domain = None
|
|
616
1016
|
if context is not None:
|
|
617
1017
|
ctx = context if isinstance(context, dict) else {}
|
|
@@ -680,6 +1080,7 @@ class MdCGOS(MdCG):
|
|
|
680
1080
|
"content": c, "path": e["path"]}, round(score, 6)))
|
|
681
1081
|
return out
|
|
682
1082
|
|
|
1083
|
+
# 生效条件:当 query 与 entries 传入时,目标文本 gt 取 goal_text(非 None)或 self.goal_text(),goal_text 为空串则 gt 为空并返回 ([], "");strip 后为空返回 ([], "");扩展词表为空返回 ([], "");否则跳过 goals 层节点,仅保留目标词覆盖 >0 的条目,按 0.7*cov+0.3*域亲和封顶 1.0 打分,返回 (out, gt);
|
|
683
1084
|
def _path_goal(self, query, entries, context=None, goal_text=None):
|
|
684
1085
|
"""目标定向路(白箱第 5 篇第 3 章「目标」):用当前目标给召回定向。
|
|
685
1086
|
|
|
@@ -703,7 +1104,8 @@ class MdCGOS(MdCG):
|
|
|
703
1104
|
return [], ""
|
|
704
1105
|
dom_scores = routing.big_domain_score_weighted(tw)
|
|
705
1106
|
dom_total = sum(dom_scores.values()) or 1.0
|
|
706
|
-
top_domains = sorted(dom_scores.items(),
|
|
1107
|
+
top_domains = sorted(dom_scores.items(),
|
|
1108
|
+
key=lambda kv: (-kv[1], str(kv[0])))[:3]
|
|
707
1109
|
stat = {"scanned": 0}
|
|
708
1110
|
out = []
|
|
709
1111
|
for e, fm, c in self._read_many(entries, stat):
|
|
@@ -722,13 +1124,17 @@ class MdCGOS(MdCG):
|
|
|
722
1124
|
"content": c, "path": e["path"]}, round(score, 6)))
|
|
723
1125
|
return out, gt
|
|
724
1126
|
|
|
1127
|
+
# 生效条件:query 去空白为空→empty_query、候选为空→no_candidates;否则按 paths 各路召回后融合(fusion=="max" 取各路最大贡献、否则求和),recall_only 中的路只以 0 分补池不参与打分,judge 与 judge_ranking 同时为真时对 fused 前 max(k*2,10) 条做资格裁决(REJECT/BLINDSPOT 剔除、DEFER 降权 0.5),否则直接取 fused 前 k;validity 真值时候选层剔除已过期节点,且 query 缓存按 validity 分键不串口径。
|
|
725
1128
|
def search_rrf(self, query: str, k: int = 20, layer: str = None,
|
|
726
1129
|
context=None, roles=None, include_work: bool = False,
|
|
727
1130
|
judge: bool = True, paths=("lexical", "bucket", "entity", "graph"),
|
|
728
1131
|
record: bool = True, query_expand=None,
|
|
729
1132
|
path_weights=None, recall_only=None, fusion: str = "sum",
|
|
730
1133
|
goal_text=None, judge_ranking: bool = False,
|
|
731
|
-
session=None
|
|
1134
|
+
session=None, branch=None, validity=None,
|
|
1135
|
+
early_stop_threshold=None,
|
|
1136
|
+
start_time=None, end_time=None, start_operator=None,
|
|
1137
|
+
end_operator=None, time_axis=None, view=None):
|
|
732
1138
|
"""并行多路召回 + RRF 融合。返回 (results, meta)。
|
|
733
1139
|
|
|
734
1140
|
每路各自排序 → Reciprocal Rank Fusion:
|
|
@@ -758,16 +1164,66 @@ class MdCGOS(MdCG):
|
|
|
758
1164
|
REJECT / BLINDSPOT 剔除,DEFER 降权 ×0.5,ACCEPT 保位。候选池取
|
|
759
1165
|
fused 前 max(k*2,10) 再裁决补位。语义联系可以是认知噪声(等权 RRF
|
|
760
1166
|
双重奖励「多路都靠前」的干扰项),唯有条件证据可授予优先级。
|
|
1167
|
+
validity: 时效过滤(显式启用,缺省不过滤)。**只排已过期**,未生效
|
|
1168
|
+
(valid_from 未到)保留——两者语义相反(scrub 纪律「valid_from 绝不
|
|
1169
|
+
并入 _EXPIRY_KEYS」)。详见 _candidates。
|
|
1170
|
+
start_time/end_time/start_operator/end_operator/time_axis:时间算子
|
|
1171
|
+
(阶段二 4.1,显式启用;语义与 fail-closed 规则见 _candidates)。
|
|
1172
|
+
启用时**绕过热路径缓存**(缓存键不含时间参数,复用会串味)。
|
|
1173
|
+
view:角色化读取视图(第四阶段 6.1,显式启用,缺省 None 零变更)。
|
|
1174
|
+
view **进热路径缓存键**(不同视图候选资格不同,不入键会跨视图
|
|
1175
|
+
串结果);候选层语义与 search 一致,详见 _candidates。
|
|
761
1176
|
"""
|
|
762
1177
|
q = (query or "").strip()
|
|
763
1178
|
if not q:
|
|
764
1179
|
return [], {"tier": None, "reason": "empty_query", "paths": {}}
|
|
1180
|
+
# 热路径:query 结果缓存命中即返回(不改 RRF 核心)
|
|
1181
|
+
# 时间算子**显式启用时绕开缓存**:缓存键不含时间参数,命中会返回
|
|
1182
|
+
# 未按本次窗口过滤的结果(静默错答比慢更贵)。
|
|
1183
|
+
_time_on = any(x is not None for x in (start_time, end_time,
|
|
1184
|
+
start_operator, end_operator))
|
|
1185
|
+
from . import hotcache as _hc
|
|
1186
|
+
hc = _hc.get(self)
|
|
1187
|
+
# 口径参数**整体入键**(v14 缺陷 C 修复):include_work/roles 改变候选
|
|
1188
|
+
# 资格、paths/fusion/judge_ranking/goal_text 等改变排序——不入键会让
|
|
1189
|
+
# 默认查询命中工作角色口径的缓存(资格泄漏),方向与时间算子同属
|
|
1190
|
+
# 「静默错答」。清单真源 = hotcache._KEYED_EXTRA。
|
|
1191
|
+
_cache_extra = {
|
|
1192
|
+
"include_work": bool(include_work), "roles": roles,
|
|
1193
|
+
"paths": tuple(paths or ()), "path_weights": path_weights,
|
|
1194
|
+
"recall_only": recall_only, "fusion": fusion,
|
|
1195
|
+
"judge": bool(judge), "judge_ranking": bool(judge_ranking),
|
|
1196
|
+
"goal_text": goal_text, "context": context,
|
|
1197
|
+
"early_stop_threshold": early_stop_threshold,
|
|
1198
|
+
}
|
|
1199
|
+
# 不可稳定进键的参数(自定义可调用 query_expand):非默认即**绕行**
|
|
1200
|
+
# 缓存(读+写双侧闭合)——fail-closed,宁可不用缓存也不串味。
|
|
1201
|
+
_bypass = query_expand is not None
|
|
1202
|
+
if hc is not None and not _time_on and not _bypass:
|
|
1203
|
+
cached = hc.get_query(q, k=k, layer=layer, session=session,
|
|
1204
|
+
branch=branch, validity=validity, view=view,
|
|
1205
|
+
extra=_cache_extra)
|
|
1206
|
+
if cached is not None:
|
|
1207
|
+
_results, _meta = cached
|
|
1208
|
+
_meta["cached"] = True
|
|
1209
|
+
return _results, _meta
|
|
765
1210
|
entries = self._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
766
|
-
session=session
|
|
1211
|
+
session=session, branch=branch, validity=validity,
|
|
1212
|
+
start_time=start_time, end_time=end_time,
|
|
1213
|
+
start_operator=start_operator,
|
|
1214
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
1215
|
+
view=view)
|
|
1216
|
+
_tf = getattr(self, "_time_filter_stat", None)
|
|
767
1217
|
if not entries:
|
|
768
|
-
|
|
1218
|
+
_m = {"tier": None, "reason": "no_candidates", "paths": {}}
|
|
1219
|
+
if _tf:
|
|
1220
|
+
_m["time_filter"] = _tf
|
|
1221
|
+
return [], _m
|
|
769
1222
|
|
|
770
1223
|
stat = {"scanned": 0}
|
|
1224
|
+
if _tf:
|
|
1225
|
+
stat["time_filter"] = _tf
|
|
1226
|
+
_tf_meta = {"time_filter": _tf} if _tf else {}
|
|
771
1227
|
ranked = {} # path -> [(node, score)]
|
|
772
1228
|
fuzzy_source = None
|
|
773
1229
|
chain_prov = {}
|
|
@@ -797,8 +1253,11 @@ class MdCGOS(MdCG):
|
|
|
797
1253
|
per_path = {}
|
|
798
1254
|
ro = set(recall_only or ())
|
|
799
1255
|
for name, scored in ranked.items():
|
|
1256
|
+
# 终键 nid:此处 rank 直接进 RRF(w/(K+rank)),并列若回落输入序
|
|
1257
|
+
# 会改变融合分——同分同 importance 必须由 nid 定序才可复算。
|
|
800
1258
|
scored = sorted(scored, key=lambda x: (-x[1],
|
|
801
|
-
-float(x[0]["frontmatter"].get("importance") or 0)
|
|
1259
|
+
-float(x[0]["frontmatter"].get("importance") or 0),
|
|
1260
|
+
str(x[0].get("id") or "")))
|
|
802
1261
|
per_path[name] = len(scored)
|
|
803
1262
|
if name in ro:
|
|
804
1263
|
continue
|
|
@@ -828,11 +1287,32 @@ class MdCGOS(MdCG):
|
|
|
828
1287
|
for scored in ranked.values():
|
|
829
1288
|
for node, s in scored:
|
|
830
1289
|
node_by_id.setdefault(node["id"], (node, s))
|
|
831
|
-
fused_all = sorted(rrf.items(), key=lambda kv: -kv[1])
|
|
1290
|
+
fused_all = sorted(rrf.items(), key=lambda kv: (-kv[1], kv[0]))
|
|
1291
|
+
|
|
1292
|
+
# 温路径早停判据(2026-09-19 热温冷分层):
|
|
1293
|
+
# top-1 RRF 分 >= threshold 且 top-k 全部来自 >=2 路共识 → 提前返回
|
|
1294
|
+
# 不改 RRF 核心算法,只在融合后判断
|
|
1295
|
+
early_stopped = False
|
|
1296
|
+
if early_stop_threshold is not None and len(fused_all) >= k:
|
|
1297
|
+
top1_score = fused_all[0][1]
|
|
1298
|
+
if top1_score >= early_stop_threshold:
|
|
1299
|
+
# 检查 top-k 是否全部来自多路共识(prov 中 >=2 路)
|
|
1300
|
+
topk_ids = [nid for nid, _ in fused_all[:k]]
|
|
1301
|
+
multi_consensus = all(
|
|
1302
|
+
len({p["path"] for p in prov.get(nid, [])}) >= 2
|
|
1303
|
+
for nid in topk_ids
|
|
1304
|
+
)
|
|
1305
|
+
if multi_consensus:
|
|
1306
|
+
early_stopped = True
|
|
832
1307
|
|
|
833
1308
|
quals = {}
|
|
834
1309
|
filtered = 0
|
|
835
|
-
if
|
|
1310
|
+
if early_stopped:
|
|
1311
|
+
# 早停:直接取 top-k,不做 judge_ranking
|
|
1312
|
+
fused = fused_all[:k]
|
|
1313
|
+
judge = False # 早停时跳过资格判定
|
|
1314
|
+
judge_ranking = False
|
|
1315
|
+
elif judge and judge_ranking:
|
|
836
1316
|
# 证据防火墙:语义/词法融合产生候选(不要漏),资格授予优先级(不要错)
|
|
837
1317
|
kept = []
|
|
838
1318
|
for nid, fs in fused_all[:max(k * 2, 10)]:
|
|
@@ -844,7 +1324,7 @@ class MdCGOS(MdCG):
|
|
|
844
1324
|
filtered += 1
|
|
845
1325
|
continue
|
|
846
1326
|
kept.append((nid, fs if st == STATE_ACCEPT else fs * 0.5))
|
|
847
|
-
kept.sort(key=lambda x: -x[1])
|
|
1327
|
+
kept.sort(key=lambda x: (-x[1], x[0]))
|
|
848
1328
|
fused = kept[:k]
|
|
849
1329
|
else:
|
|
850
1330
|
fused = fused_all[:k]
|
|
@@ -858,23 +1338,42 @@ class MdCGOS(MdCG):
|
|
|
858
1338
|
results.append((node, round(fs, 6), qual, prov.get(nid, [])))
|
|
859
1339
|
if record and results:
|
|
860
1340
|
self.record_access([r[0]["id"] for r in results], "RRF")
|
|
1341
|
+
# 热路径:写 query 结果缓存 —— **同样受 `_time_on` 约束**。
|
|
1342
|
+
# 缓存键不含时间参数,把被时间过滤的结果写进去,会让后续**默认查询**
|
|
1343
|
+
# 命中那个子集(串味方向与「读」相反,但同样是静默错答:少了 4 条
|
|
1344
|
+
# 却看不出原因)。绕行必须读+写双侧闭合。
|
|
1345
|
+
if hc is not None and results and not _time_on and not _bypass:
|
|
1346
|
+
hc.put_query(q, results, {"tier": "RRF", "scanned": stat["scanned"],
|
|
1347
|
+
"paths": per_path, "fused": len(results),
|
|
1348
|
+
"judge_ranking": bool(judge and judge_ranking),
|
|
1349
|
+
"judge_filtered": filtered,
|
|
1350
|
+
"early_stopped": early_stopped,
|
|
1351
|
+
"expand_source": fuzzy_source,
|
|
1352
|
+
"goal_used": goal_used,
|
|
1353
|
+
"provenance": prov, **_tf_meta}, k=k, layer=layer,
|
|
1354
|
+
session=session, branch=branch, validity=validity,
|
|
1355
|
+
view=view, extra=_cache_extra)
|
|
861
1356
|
return results, {"tier": "RRF", "scanned": stat["scanned"],
|
|
862
1357
|
"paths": per_path, "fused": len(results),
|
|
863
1358
|
"judge_ranking": bool(judge and judge_ranking),
|
|
864
1359
|
"judge_filtered": filtered,
|
|
1360
|
+
"early_stopped": early_stopped,
|
|
865
1361
|
"expand_source": fuzzy_source,
|
|
866
1362
|
"goal_used": goal_used,
|
|
867
|
-
"provenance": prov}
|
|
1363
|
+
"provenance": prov, **_tf_meta}
|
|
868
1364
|
|
|
869
1365
|
# ================= 7. budget-driven pack =================
|
|
870
1366
|
|
|
1367
|
+
# 生效条件:query(配合 use_rrf 取 items)逐条按 budget_tokens 与 max_item_tokens 装包:若 used+t > budget_tokens,则 max_item_tokens 为真且 room=budget_tokens-used 不小于 min_excerpt(max_item_tokens 为真时取 max(1, min(50, max_item_tokens // 5)),否则为 0)时按 keep=min(max_item_tokens, room) 摘录,摘录后 est_tokens<=0 则该条以 excerpt_empty 进 skipped 并 continue;否则以 oversize_or_over_budget 进 skipped 并 continue;未超预算则计入 used 并 append,include_recent 为真时再按 left=budget_tokens-used 追加 recent_limit 条近期事件(逐条 est_tokens 不超过 left 才计入),最终返回含 pack/tokens_used/budget/skipped/recent/meta 的 dict;validity 真值时向候选层透传时效过滤(只排已过期)。
|
|
871
1368
|
def recall(self, query: str, budget_tokens: int = DEFAULT_BUDGET, k: int = 20,
|
|
872
1369
|
layer: str = None, context=None, roles=None,
|
|
873
1370
|
include_work: bool = False, judge: bool = True, use_rrf: bool = True,
|
|
874
1371
|
paths=None, query_expand=None, fusion=None,
|
|
875
1372
|
goal_text=None, include_recent=False, recent_limit: int = 10,
|
|
876
|
-
judge_ranking: bool = False, session=None,
|
|
877
|
-
max_item_tokens: int = DEFAULT_MAX_ITEM_TOKENS
|
|
1373
|
+
judge_ranking: bool = False, session=None, branch=None,
|
|
1374
|
+
validity=None, max_item_tokens: int = DEFAULT_MAX_ITEM_TOKENS,
|
|
1375
|
+
start_time=None, end_time=None, start_operator=None,
|
|
1376
|
+
end_operator=None, time_axis=None, view=None):
|
|
878
1377
|
"""按 token 预算装包:装到预算花完为止。
|
|
879
1378
|
|
|
880
1379
|
装包策略(2026-09-14 调整):
|
|
@@ -893,10 +1392,17 @@ class MdCGOS(MdCG):
|
|
|
893
1392
|
include_recent=True 时,把近期事件窗口(第 5 篇第 3 章)附在包后,
|
|
894
1393
|
保证当前任务的连续性;它不参与 RRF 正排,但计入 token 预算。
|
|
895
1394
|
返回 {pack: [...], tokens_used, budget, skipped: [...], recent: [...], meta}
|
|
1395
|
+
validity:时效过滤(显式启用,缺省不过滤)——只排已过期,未生效保留。
|
|
1396
|
+
start_time/end_time/start_operator/end_operator/time_axis:时间算子
|
|
1397
|
+
(显式启用)——按 time_axis 轴把候选收敛到查询时间窗内;轴未指定回落
|
|
1398
|
+
effective;误用 fail-closed(ValueError);节点缺字段按轴策略处置。
|
|
1399
|
+
view:角色化读取视图(第四阶段 6.1,显式启用,缺省 None 零变更),
|
|
1400
|
+
透传 search_rrf/search 候选层。
|
|
896
1401
|
"""
|
|
897
1402
|
if use_rrf:
|
|
898
1403
|
kw = dict(k=k, layer=layer, context=context, roles=roles,
|
|
899
|
-
include_work=include_work, judge=judge, session=session
|
|
1404
|
+
include_work=include_work, judge=judge, session=session,
|
|
1405
|
+
view=view)
|
|
900
1406
|
if paths is not None:
|
|
901
1407
|
kw["paths"] = tuple(paths)
|
|
902
1408
|
if query_expand is not None:
|
|
@@ -906,11 +1412,26 @@ class MdCGOS(MdCG):
|
|
|
906
1412
|
if goal_text is not None:
|
|
907
1413
|
kw["goal_text"] = goal_text
|
|
908
1414
|
kw["judge_ranking"] = judge_ranking
|
|
1415
|
+
if branch is not None:
|
|
1416
|
+
kw["branch"] = branch
|
|
1417
|
+
if validity:
|
|
1418
|
+
kw["validity"] = validity
|
|
1419
|
+
for _k, _v in (("start_time", start_time), ("end_time", end_time),
|
|
1420
|
+
("start_operator", start_operator),
|
|
1421
|
+
("end_operator", end_operator),
|
|
1422
|
+
("time_axis", time_axis)):
|
|
1423
|
+
if _v is not None: # 时间算子透传(None = 未启用,不入参)
|
|
1424
|
+
kw[_k] = _v
|
|
909
1425
|
results, meta = self.search_rrf(query, **kw)
|
|
910
1426
|
items = [(r[0], r[1], r[2], r[3]) for r in results]
|
|
911
1427
|
else:
|
|
912
1428
|
res, meta = self.search(query, layer=layer, k=k, context=context,
|
|
913
|
-
judge=judge, session=session
|
|
1429
|
+
judge=judge, session=session, branch=branch,
|
|
1430
|
+
validity=validity,
|
|
1431
|
+
start_time=start_time, end_time=end_time,
|
|
1432
|
+
start_operator=start_operator,
|
|
1433
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
1434
|
+
view=view)
|
|
914
1435
|
items = [(r[0], r[1], r[2], []) for r in res]
|
|
915
1436
|
|
|
916
1437
|
pack, skipped, used = [], [], 0
|
|
@@ -964,6 +1485,7 @@ class MdCGOS(MdCG):
|
|
|
964
1485
|
|
|
965
1486
|
# ================= 1. Fix pairs 自动挖掘 =================
|
|
966
1487
|
|
|
1488
|
+
# 生效条件:当 events 传入时,兼容显式 {error,fix} 对与含 text 的事件序列;后者在错误行长度 >= min_len 后向后 lookahead 条内寻找 _FIX_RE 命中的行配对并 break;去重后为每对 add 知识节点和 add_rejected,返回 pairs/rejected_ids/knowledge_ids;
|
|
967
1489
|
def mine_fix_pairs(self, events, lookahead: int = 4, min_len: int = 6):
|
|
968
1490
|
"""从行为日志挖掘「错误 → 修复」对。
|
|
969
1491
|
|
|
@@ -1032,27 +1554,72 @@ class MdCGOS(MdCG):
|
|
|
1032
1554
|
|
|
1033
1555
|
# ================= 4. 审核队列(inbox → decisions) =================
|
|
1034
1556
|
|
|
1557
|
+
# 生效条件:当 node_id 与 content 传入时,在 strict 锁内按 payload_hash 查重;命中同内容提案(无论 pending/accepted/rejected,已裁决优先 break)时幂等返回既有 pid(info=True 返回 dedup 字典),未命中则生成新 pid 入队并返回 pid(info=True 返回 dedup False 字典);
|
|
1035
1558
|
def propose(self, node_id: str, content: str, layer: str = "knowledge",
|
|
1036
|
-
tags=None, condition_space=None, verify=None,
|
|
1559
|
+
tags=None, condition_space=None, verify=None,
|
|
1560
|
+
info: bool = False, **kw):
|
|
1037
1561
|
"""把一个候选记忆放入海马体 inbox,等待审核(不直接持久化)。
|
|
1038
1562
|
|
|
1039
1563
|
verify —— 验收判据(内联声明,裁决阶段只读),形如:
|
|
1040
1564
|
{"kind": "code", "assertions": ["pytest -k foo 通过"], "cmd": "..."}
|
|
1041
1565
|
判据指纹随提案落盘,复核者只能按原判据裁决,不能放宽标准。
|
|
1566
|
+
|
|
1567
|
+
幂等对账(两段式):入队动作在 strict 锁内「查重 → 入队」原子完成。
|
|
1568
|
+
对账键 = payload_hash(内容签名 _sig(content)),同内容提案若已存在
|
|
1569
|
+
(无论 pending 还是已裁决 accepted/rejected)→ 幂等返回既有 pid,
|
|
1570
|
+
不再入队。语义:重试与崩溃恢复无害——入队落盘成功而响应丢失时
|
|
1571
|
+
(MCP 客户端超时重试的真正机制),重试对账命中既有记录并返回原 pid。
|
|
1572
|
+
|
|
1573
|
+
返回值:info=False(默认)返回 pid 字符串(保形,存量调用零变化);
|
|
1574
|
+
info=True 返回 {"pid", "dedup", "dup_of", "dup_status"}。
|
|
1042
1575
|
"""
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1576
|
+
phash = _sig(content)
|
|
1577
|
+
with FileLock(self.inbox_log, strict=True):
|
|
1578
|
+
st = self._pid_status()
|
|
1579
|
+
dup = None
|
|
1580
|
+
for r in read_jsonl(self.inbox_log):
|
|
1581
|
+
# 存量记录无 payload_hash 字段 → 现算兼容(对账覆盖旧账)
|
|
1582
|
+
rh = r.get("payload_hash") or _sig(r.get("content") or "")
|
|
1583
|
+
if rh != phash:
|
|
1584
|
+
continue
|
|
1585
|
+
s = st.get(r.get("pid")) or {}
|
|
1586
|
+
dup = {"pid": r.get("pid"),
|
|
1587
|
+
"status": s.get("status") or "pending",
|
|
1588
|
+
"node_id": r.get("id")}
|
|
1589
|
+
if dup["status"] in ("accepted", "rejected"):
|
|
1590
|
+
break # 已裁决的最有信息量,优先返回
|
|
1591
|
+
if dup:
|
|
1592
|
+
self._audit("propose_dedup", node_id, dup_of=dup["pid"],
|
|
1593
|
+
dup_status=dup["status"], payload_hash=phash)
|
|
1594
|
+
if info:
|
|
1595
|
+
return {"pid": dup["pid"], "dedup": True,
|
|
1596
|
+
"dup_of": dup["pid"], "dup_status": dup["status"]}
|
|
1597
|
+
return dup["pid"]
|
|
1598
|
+
# 唯一性取证(2026-09-16):旧式 _sig(node_id + time.time()) 只带
|
|
1599
|
+
# 「节点 id + 时间戳」,无进程熵——多 worker 在同一时刻用相同
|
|
1600
|
+
# node_id 入队即产生 pid 碰撞(test_review_conformance【8】「pid
|
|
1601
|
+
# 互不重复」偶发红,4 进程同用 w-node-K 时命中)。幂等由
|
|
1602
|
+
# payload_hash 对账保证、与 pid 取值无关,故此处补熵是
|
|
1603
|
+
# 只增不减契约(20 条互异由 test_review_conformance【8c】守卫)。
|
|
1604
|
+
pid = "prop_" + _sig(node_id + str(time.time())
|
|
1605
|
+
+ uuid.uuid4().hex)
|
|
1606
|
+
_norm, vhash = _verify_norm(verify)
|
|
1607
|
+
rec = {"t": time.time(), "pid": pid, "id": node_id, "content": content,
|
|
1608
|
+
"layer": layer, "tags": list(tags or []),
|
|
1609
|
+
"condition_space": condition_space or {},
|
|
1610
|
+
"payload_hash": phash,
|
|
1611
|
+
"verify": verify or {}, "verify_hash": vhash,
|
|
1612
|
+
"extra": kw, "actor": self.actor,
|
|
1613
|
+
"session": getattr(self, "session", None)}
|
|
1614
|
+
append_jsonl(self.inbox_log, rec)
|
|
1052
1615
|
self._audit("propose", node_id, pid=pid, layer=layer,
|
|
1053
|
-
payload_hash=
|
|
1616
|
+
payload_hash=phash, verify_hash=vhash)
|
|
1617
|
+
if info:
|
|
1618
|
+
return {"pid": pid, "dedup": False,
|
|
1619
|
+
"dup_of": None, "dup_status": None}
|
|
1054
1620
|
return pid
|
|
1055
1621
|
|
|
1622
|
+
# 生效条件:当 self.decisions_log 可被 read_jsonl 读取时,仅 r.get("pid") 为真值的记录写入 st[r["pid"]],后出现记录覆盖先出现记录,返回 pid → 最新一条裁决记录的字典;
|
|
1056
1623
|
def _pid_status(self):
|
|
1057
1624
|
"""pid → 最新一条裁决记录(多轮再审批时取最后一轮)。"""
|
|
1058
1625
|
st = {}
|
|
@@ -1061,18 +1628,20 @@ class MdCGOS(MdCG):
|
|
|
1061
1628
|
st[r["pid"]] = r
|
|
1062
1629
|
return st
|
|
1063
1630
|
|
|
1631
|
+
# 生效条件:实例已构建(内部先读 _pid_status());返回其 status ∈ TERMINAL_DECISION_STATUS(accepted/rejected/noop)的 pid 集合,status=="needs_reapproval" 视为未关闭、不计入;
|
|
1064
1632
|
def _closed_pids(self):
|
|
1065
1633
|
"""已被终态裁决关闭的 pid(needs_reapproval 仍视为打开)。"""
|
|
1066
1634
|
return {pid for pid, r in self._pid_status().items()
|
|
1067
|
-
if r.get("status") in
|
|
1635
|
+
if r.get("status") in TERMINAL_DECISION_STATUS}
|
|
1068
1636
|
|
|
1637
|
+
# 生效条件:在已用 root 构造的实例上遍历 self.inbox_log 记录,其 pid 在 _pid_status() 中 status ∈ TERMINAL_DECISION_STATUS 时跳过,其余复制该记录并写入 status=s.get("status") or "pending"、round=int(s.get("round") or 0)、issues=list(s.get("issues") or []) 后返回 out。
|
|
1069
1638
|
def review_list(self):
|
|
1070
1639
|
"""待审核候选(含被红队打回、待再审批的条目)。"""
|
|
1071
1640
|
st = self._pid_status()
|
|
1072
1641
|
out = []
|
|
1073
1642
|
for r in read_jsonl(self.inbox_log):
|
|
1074
1643
|
s = st.get(r.get("pid")) or {}
|
|
1075
|
-
if s.get("status") in
|
|
1644
|
+
if s.get("status") in TERMINAL_DECISION_STATUS:
|
|
1076
1645
|
continue
|
|
1077
1646
|
rec = dict(r)
|
|
1078
1647
|
rec["status"] = s.get("status") or "pending"
|
|
@@ -1081,6 +1650,27 @@ class MdCGOS(MdCG):
|
|
|
1081
1650
|
out.append(rec)
|
|
1082
1651
|
return out
|
|
1083
1652
|
|
|
1653
|
+
# 生效条件:实例已构建(内部读 decisions_log、_pid_status()、review_list()、_closed_pids());返回 records=decisions 记录总数、by_decision=按 decision 值分组计数(decision 为假值时归入 "unknown")、proposals=有裁决记录的 pid 数、pending=review_list() 长度、closed=_closed_pids() 长度、noop=by_decision 中 noop 计数(缺省 0)、terminal_status=终态状态集清单;
|
|
1654
|
+
def review_stats(self):
|
|
1655
|
+
"""裁决动作分布统计(含 NOOP)。
|
|
1656
|
+
|
|
1657
|
+
存在意义:NOOP(已评估、判定不改变任何现有记忆)若只落在 jsonl 里、
|
|
1658
|
+
没有统计出口,则「这条候选被评估过」在治理面不可见——与「静默忽略」
|
|
1659
|
+
等价。口径=decisions.jsonl 逐条记录(权威源,不另建统计文件);队列
|
|
1660
|
+
视图复用 review_list/_closed_pids,避免在此复制第二份终态判定逻辑。
|
|
1661
|
+
"""
|
|
1662
|
+
by = {}
|
|
1663
|
+
for r in read_jsonl(self.decisions_log):
|
|
1664
|
+
d = str(r.get("decision") or "").strip() or "unknown"
|
|
1665
|
+
by[d] = by.get(d, 0) + 1
|
|
1666
|
+
return {"records": sum(by.values()), "by_decision": by,
|
|
1667
|
+
"proposals": len(self._pid_status()),
|
|
1668
|
+
"pending": len(self.review_list()),
|
|
1669
|
+
"closed": len(self._closed_pids()),
|
|
1670
|
+
"noop": by.get(DECISION_NOOP, 0),
|
|
1671
|
+
"terminal_status": list(TERMINAL_DECISION_STATUS)}
|
|
1672
|
+
|
|
1673
|
+
# 生效条件:当 pid 传入时,从 decisions_log 读取并仅保留 r.get("pid")==pid 的记录,映射为含 round/decision/status/redteam/issues/t/actor/record_node_id/record_hash 的列表返回;
|
|
1084
1674
|
def review_rounds(self, pid: str):
|
|
1085
1675
|
"""某提案的裁决轮次历史(红队打回 → 修复 → 再审批,可追溯)。"""
|
|
1086
1676
|
return [{"round": r.get("round"), "decision": r.get("decision"),
|
|
@@ -1098,6 +1688,7 @@ class MdCGOS(MdCG):
|
|
|
1098
1688
|
AUDIT_TAG = "review-record"
|
|
1099
1689
|
|
|
1100
1690
|
@staticmethod
|
|
1691
|
+
# 生效条件:从 rec 取 pid/round/decision/status/redteam_verdict/issues/verify_hash/reason/actor/t 十键(缺键回落 None)做 sort_keys 紧凑 JSON 序列化后返回 sha1 hexdigest;
|
|
1101
1692
|
def _record_hash(rec):
|
|
1102
1693
|
"""裁决记录指纹:外部审计方按同规则重算即可验证未被篡改。"""
|
|
1103
1694
|
keys = ("pid", "round", "decision", "status", "redteam_verdict",
|
|
@@ -1108,9 +1699,11 @@ class MdCGOS(MdCG):
|
|
|
1108
1699
|
return hashlib.sha1(norm.encode("utf-8")).hexdigest()
|
|
1109
1700
|
|
|
1110
1701
|
@staticmethod
|
|
1702
|
+
# 生效条件:对任意 pid 取其 '_' 分段末段、round_no 经 int() 转换后拼成 f"rev_{末段}_r{轮次}";
|
|
1111
1703
|
def _audit_node_id(pid: str, round_no) -> str:
|
|
1112
1704
|
return f"rev_{pid.split('_')[-1]}_r{int(round_no)}"
|
|
1113
1705
|
|
|
1706
|
+
# 生效条件:当 rec 提供 record_node_id/record_hash/pid/round/decision/status/redteam_verdict 等键时(缺键会 KeyError),构造审核裁决 body,并调用 self.add 以 layer="self"、override=True、role=AUDIT_ROLE 等写入,返回 nid;若 self 有 principal 则额外传 sensitivity=principal.clearance;
|
|
1114
1707
|
def _write_review_record(self, rec):
|
|
1115
1708
|
"""把一轮裁决写成 md 记忆节点(self 层)——外部来源可读、可复核。"""
|
|
1116
1709
|
nid = rec["record_node_id"]
|
|
@@ -1148,6 +1741,7 @@ class MdCGOS(MdCG):
|
|
|
1148
1741
|
audit_source="hippocampus/decisions.jsonl", **extra)
|
|
1149
1742
|
return nid
|
|
1150
1743
|
|
|
1744
|
+
# 生效条件:当 pid/item/decision/status/reason/round_no/rt_verdict/issues/vhash/result 传入时,构造 rec 并写 record_hash,把 result 中 ok/node_id/error/state 保留;在 strict 锁内追加 decisions_log;随后尝试写 md 审核记录,成功补 record_node_id/record_hash,异常则补 record_error 并 audit review_record_failed;最后 audit review_decide 并返回 result;
|
|
1151
1745
|
def _record_decision(self, pid, item, decision, status, reason, round_no,
|
|
1152
1746
|
rt_verdict, issues, vhash, result):
|
|
1153
1747
|
"""落盘一轮裁决:jsonl(权威)+ md 审计节点(可复核、供外部审计)。"""
|
|
@@ -1160,7 +1754,10 @@ class MdCGOS(MdCG):
|
|
|
1160
1754
|
rec["record_hash"] = self._record_hash(rec)
|
|
1161
1755
|
rec["result"] = {k: v for k, v in result.items()
|
|
1162
1756
|
if k in ("ok", "node_id", "error", "state")}
|
|
1163
|
-
|
|
1757
|
+
# 裁决记录不能丢:strict 锁内追加(并发裁决不交错;
|
|
1758
|
+
# 丢一条裁决会让提案回 pending → 重复落盘,比让裁决者等一下代价大)
|
|
1759
|
+
with FileLock(self.decisions_log, strict=True):
|
|
1760
|
+
append_jsonl(self.decisions_log, rec)
|
|
1164
1761
|
# md 审计节点写失败不影响裁决(jsonl 仍是权威来源)
|
|
1165
1762
|
try:
|
|
1166
1763
|
self._write_review_record(rec)
|
|
@@ -1176,6 +1773,45 @@ class MdCGOS(MdCG):
|
|
|
1176
1773
|
record_node=rec["record_node_id"])
|
|
1177
1774
|
return result
|
|
1178
1775
|
|
|
1776
|
+
# 生效条件:当 pid、phash、decision 传入且 phash 非空时,在 strict 锁内对 inbox_log 中同 phash、非自身、且 _pid_status 尚无记录的纯 pending 兄弟提案写入 rejected 裁决,返回 closed 列表;phash 为空返回 [];
|
|
1777
|
+
def _cascade_dedup(self, pid: str, phash: str, decision: str):
|
|
1778
|
+
"""主提案终态裁决后,同 payload_hash 的其余纯 pending 提案自动出清。
|
|
1779
|
+
|
|
1780
|
+
兄弟提案多为超时重试的重复入队产物(存量账本里已有实例):主提案已
|
|
1781
|
+
裁决后,兄弟再走一遍裁决只会重复落盘/重复入库。只在 decisions 锁内
|
|
1782
|
+
「查兄弟最新状态 → 未有任何裁决记录才出清」,与并发裁决互斥,不会
|
|
1783
|
+
覆盖兄弟自己的 accept。needs_reapproval 的兄弟有独立红队轮次历史,
|
|
1784
|
+
不动(留人工处置)。级联裁决的权威源同为本文件(jsonl),
|
|
1785
|
+
不写 md 审计节点(可能批量,self 层只留 jsonl + audit 簿记)。
|
|
1786
|
+
"""
|
|
1787
|
+
if not phash:
|
|
1788
|
+
return []
|
|
1789
|
+
closed = []
|
|
1790
|
+
with FileLock(self.decisions_log, strict=True):
|
|
1791
|
+
st = self._pid_status()
|
|
1792
|
+
for r in read_jsonl(self.inbox_log):
|
|
1793
|
+
bpid = r.get("pid")
|
|
1794
|
+
if not bpid or bpid == pid or bpid in st:
|
|
1795
|
+
continue # 自己 / 已有裁决记录(含 needs_reapproval)跳过
|
|
1796
|
+
rh = r.get("payload_hash") or _sig(r.get("content") or "")
|
|
1797
|
+
if rh != phash:
|
|
1798
|
+
continue
|
|
1799
|
+
rec = {"t": time.time(), "pid": bpid, "decision": "reject",
|
|
1800
|
+
"status": "rejected", "round": 1,
|
|
1801
|
+
"redteam_verdict": "absent", "issues": [],
|
|
1802
|
+
"verify_hash": r.get("verify_hash") or "",
|
|
1803
|
+
"reason": f"cascade_dedup: 同内容提案已由 {pid} "
|
|
1804
|
+
f"{decision}(自动出清重复入队,无需再裁决)",
|
|
1805
|
+
"actor": self.actor, "target_id": r.get("id"),
|
|
1806
|
+
"record_node_id": self._audit_node_id(bpid, 1)}
|
|
1807
|
+
rec["record_hash"] = self._record_hash(rec)
|
|
1808
|
+
append_jsonl(self.decisions_log, rec)
|
|
1809
|
+
closed.append(bpid)
|
|
1810
|
+
for b in closed:
|
|
1811
|
+
self._audit("review_cascade", b, cascade_of=pid, decision=decision)
|
|
1812
|
+
return closed
|
|
1813
|
+
|
|
1814
|
+
# 生效条件:遍历 self.index["nodes"];仅取 layer=="self" 且 tags 含 AUDIT_TAG 的节点,pid 为真值时再要求 tags 含 f"pid:{pid}";按 (created_at, id) 升序返回不含正文的条目;
|
|
1179
1815
|
def review_records(self, pid: str = None):
|
|
1180
1816
|
"""列出裁决记录节点(self 层 / audit 标签),供外部来源审计。"""
|
|
1181
1817
|
out = []
|
|
@@ -1192,6 +1828,7 @@ class MdCGOS(MdCG):
|
|
|
1192
1828
|
out.sort(key=lambda r: (r.get("created_at") or 0, r["id"]))
|
|
1193
1829
|
return out
|
|
1194
1830
|
|
|
1831
|
+
# 生效条件:当 node_id 传入时,若 self.get(node_id) 无节点返回 ok False error node_not_found;否则从 frontmatter 取 pid/round,在 decisions_log 找 pid 且 int(round or 0) 匹配的记录;找不到返回 source_record_missing;找到则用 _record_hash(src) 与 frontmatter.record_hash 比较,返回 ok=bool(expected) and actual==expected 及明细;
|
|
1195
1832
|
def verify_review_record(self, node_id: str):
|
|
1196
1833
|
"""复核一条裁决记录节点:重算 record_hash 与 decisions.jsonl 比对。
|
|
1197
1834
|
|
|
@@ -1218,22 +1855,30 @@ class MdCGOS(MdCG):
|
|
|
1218
1855
|
"verify_hash": src.get("verify_hash"),
|
|
1219
1856
|
"source": "hippocampus/decisions.jsonl"}
|
|
1220
1857
|
|
|
1858
|
+
# 生效条件:decision 须为 DECISION_ACTIONS("accept"/"reject"/"edit"/"merge"/"noop")之一(否则 raise ValueError),inbox_log 中须有 pid 匹配记录(否则 {'ok': False, 'error': 'pid_not_found'}),且 pid 不在 self._closed_pids() 中(否则 'already_decided');edits 为真值且含 "verify"、或 redteam 为真值且含 "verify" 时返回 'verify_readonly';last_status=="needs_reapproval" 时须 redteam.verdict 归一化为 "pass" 且 round_no>last_round(否则 'reapproval_required' / 'round_not_advanced');rt_v=="reject" 或(decision=="reject" 且 rt_issues 非空)时记 needs_reapproval 不落节点;decision=="accept" 且 rt_v 为空且 _redteam_required() 为真时返回 'redteam_required';其余 accept/edit 按 item(edit 时用 edits.get 覆盖 content/tags/layer)add+flush 落节点,merge 须 merge_into 或 item.extra.merge_into 指向的节点存在(否则 'merge_target_not_found')后追加内容并 rebuild_index,reject 与 noop 只记裁决(status 分别为 "rejected"/"noop");最后统一 _record_decision + _cascade_dedup + flush 后返回 result。
|
|
1221
1859
|
def review_decide(self, pid: str, decision: str, edits: dict = None,
|
|
1222
1860
|
merge_into: str = None, reason: str = "",
|
|
1223
1861
|
redteam: dict = None, issues=None):
|
|
1224
|
-
"""审核裁决:accept / reject / edit / merge。
|
|
1862
|
+
"""审核裁决:accept / reject / edit / merge / noop。
|
|
1225
1863
|
|
|
1226
1864
|
accept → 按 inbox 原样写入
|
|
1227
1865
|
reject → 丢弃(只记裁决,不落节点)
|
|
1228
1866
|
edit → 用 edits 覆盖 content/tags/layer 后写入
|
|
1229
1867
|
merge → 合并进已有节点 merge_into(内容追加 + 不适用条件并集)
|
|
1868
|
+
noop → 已评估、判定**不改变任何现有记忆**:只留痕(decisions.jsonl +
|
|
1869
|
+
审计记录节点)并关闭提案,不落业务节点、不进负记忆
|
|
1870
|
+
|
|
1871
|
+
noop 与 reject 的区别是语义而非路径:reject 是「否掉这条候选」,noop 是
|
|
1872
|
+
「评估过了、无需改动」。二者都不写目标节点,故 noop 不可借道绕过 accept
|
|
1873
|
+
的写入门控(它根本不写)。此处 noop 是**裁决动作**,与 lifecycle/trust 中
|
|
1874
|
+
同名的**状态迁移结果码**分属两层(见模块常量区注释)。
|
|
1230
1875
|
|
|
1231
1876
|
两条验证纪律(借自任务分级协议的验证端):
|
|
1232
1877
|
1. 判据只读:verify 由 propose 声明,裁决阶段传入不同判据 → verify_readonly。
|
|
1233
1878
|
2. 红队门控 + 再审批:redteam.verdict=reject(或带 issues 的 reject)不落库,
|
|
1234
1879
|
该 pid 转 needs_reapproval;修复后必须带 round 递增的 pass 再审批。
|
|
1235
1880
|
"""
|
|
1236
|
-
if decision not in
|
|
1881
|
+
if decision not in DECISION_ACTIONS:
|
|
1237
1882
|
raise ValueError(f"未知裁决:{decision}")
|
|
1238
1883
|
item = next((r for r in read_jsonl(self.inbox_log)
|
|
1239
1884
|
if r.get("pid") == pid), None)
|
|
@@ -1283,7 +1928,12 @@ class MdCGOS(MdCG):
|
|
|
1283
1928
|
|
|
1284
1929
|
result = {"pid": pid, "decision": decision, "round": round_no,
|
|
1285
1930
|
"redteam": rt_v or "absent"}
|
|
1286
|
-
if decision ==
|
|
1931
|
+
if decision == DECISION_NOOP:
|
|
1932
|
+
# 已评估、判定不改变任何现有记忆:不落业务节点、不进负记忆,唯一产物
|
|
1933
|
+
# 是下面 _record_decision 写的留痕(jsonl + 审计记录节点)。放在最前,
|
|
1934
|
+
# 免得将来有人往 accept/edit 分支加副作用时把它卷进去。
|
|
1935
|
+
result["ok"] = True
|
|
1936
|
+
elif decision == "reject":
|
|
1287
1937
|
result["ok"] = True
|
|
1288
1938
|
elif decision in ("accept", "edit"):
|
|
1289
1939
|
content = item["content"]
|
|
@@ -1299,6 +1949,12 @@ class MdCGOS(MdCG):
|
|
|
1299
1949
|
extra["verify_hash"] = expect
|
|
1300
1950
|
nid = self.add(item["id"], content, layer=layer, tags=tags,
|
|
1301
1951
|
condition_space=item.get("condition_space"), **extra)
|
|
1952
|
+
# 索引增量收尾(2026-09-16 取证):add 只把条目放进本进程内存 _dirty,
|
|
1953
|
+
# 未达 autoflush(64) 阈值时进程退出即永久丢失——裁决进程(review_cli /
|
|
1954
|
+
# MCP op=review)通常只写 1~2 条,不 flush 则「节点在盘上但索引无条目」,
|
|
1955
|
+
# 其他进程与重载后的长驻进程都检索不到,只能靠某次全量 rebuild 偶然救回。
|
|
1956
|
+
# merge 分支绕开 add 直写节点文件,故其收尾同为索引重建(同因不同法)。
|
|
1957
|
+
self.flush()
|
|
1302
1958
|
result.update(ok=True, node_id=nid)
|
|
1303
1959
|
else: # merge
|
|
1304
1960
|
target = merge_into or (item.get("extra") or {}).get("merge_into")
|
|
@@ -1319,16 +1975,34 @@ class MdCGOS(MdCG):
|
|
|
1319
1975
|
self.rebuild_index()
|
|
1320
1976
|
result.update(ok=True, node_id=target)
|
|
1321
1977
|
|
|
1322
|
-
|
|
1978
|
+
# status 映射统一出口:reject→"rejected"、noop→"noop"(同为终态,见
|
|
1979
|
+
# TERMINAL_DECISION_STATUS)、其余→"accepted"。
|
|
1980
|
+
result = self._record_decision(
|
|
1323
1981
|
pid, item, decision,
|
|
1324
|
-
"rejected"
|
|
1982
|
+
{DECISION_REJECT: "rejected", DECISION_NOOP: DECISION_NOOP}
|
|
1983
|
+
.get(decision, "accepted"),
|
|
1325
1984
|
reason, round_no, rt_v, rt_issues, expect, result)
|
|
1985
|
+
# 级联出清:主提案已终态,同内容兄弟提案(重复入队产物)自动关闭
|
|
1986
|
+
casc = self._cascade_dedup(
|
|
1987
|
+
pid, item.get("payload_hash") or _sig(item.get("content") or ""),
|
|
1988
|
+
decision)
|
|
1989
|
+
if casc:
|
|
1990
|
+
result["cascade_closed"] = casc
|
|
1991
|
+
# 裁决链路统一收尾(2026-09-16 取证):上面 accept/edit 分支的 flush 只覆盖
|
|
1992
|
+
# 目标节点,而 `_record_decision → _write_review_record → add` 在这里又写了一个
|
|
1993
|
+
# **审计记录节点**(self 层),reject 路径更是**只**写它——两者同样停留在内存
|
|
1994
|
+
# `_dirty` 中。不在此收尾则「审计记录在盘上但索引无条目」,复核方与其它进程都
|
|
1995
|
+
# 检索不到(与目标节点同因,只是漏点不同)。flush 幂等(`_dirty` 空即返回)。
|
|
1996
|
+
self.flush()
|
|
1997
|
+
return result
|
|
1326
1998
|
|
|
1999
|
+
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.decisions_log)),记录内容取决于 decisions_log 可读结果;
|
|
1327
2000
|
def decisions(self):
|
|
1328
2001
|
return list(read_jsonl(self.decisions_log))
|
|
1329
2002
|
|
|
1330
2003
|
# ================= 5. tombstone + 恢复时删除检查 =================
|
|
1331
2004
|
|
|
2005
|
+
# 生效条件:当 node_id 传入且索引中存在该节点时,先经 protect.guard_forget(override=override) 保护检查,随后尝试把源路径 os.replace 到 trash_dir/{node_id}.md;OSError 返回 ok False error 字符串;成功则写 deletions_log、_unstage、缓存失效、audit,并返回 ok True/id/tombstone;索引无此节点返回 not_found;
|
|
1332
2006
|
def forget(self, node_id: str, reason: str = "", override: bool = False):
|
|
1333
2007
|
"""软删除:节点文件移入 trash/,写入删除清单(payload-free)。
|
|
1334
2008
|
|
|
@@ -1352,19 +2026,23 @@ class MdCGOS(MdCG):
|
|
|
1352
2026
|
"actor": self.actor,
|
|
1353
2027
|
"trash": os.path.relpath(dst, self.root)
|
|
1354
2028
|
.replace("\\", "/")})
|
|
1355
|
-
|
|
1356
|
-
|
|
2029
|
+
# 摘索引必须**落盘**(写删除记录):只 pop 内存会让条目在下次启动
|
|
2030
|
+
# 重放 _index_log 时复活成幽灵条目(索引有条目、文件已进 trash/)。
|
|
2031
|
+
self._unstage(node_id)
|
|
1357
2032
|
subgraph.invalidate_cache(self)
|
|
1358
2033
|
chain.invalidate_cache(self)
|
|
1359
2034
|
self._audit("forget", node_id, reason=reason, payload_hash=h)
|
|
1360
2035
|
return {"ok": True, "id": node_id, "tombstone": h}
|
|
1361
2036
|
|
|
2037
|
+
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.deletions_log)),记录内容取决于 deletions_log 可读结果;
|
|
1362
2038
|
def deletions(self):
|
|
1363
2039
|
return list(read_jsonl(self.deletions_log))
|
|
1364
2040
|
|
|
2041
|
+
# 生效条件:当 node_id 传入时,返回 deletions_log 中是否存在 r.get("id")==node_id 的布尔值;
|
|
1365
2042
|
def is_tombstoned(self, node_id: str):
|
|
1366
2043
|
return any(r.get("id") == node_id for r in read_jsonl(self.deletions_log))
|
|
1367
2044
|
|
|
2045
|
+
# 生效条件:当 node_id 传入时,若 deletions_log 中存在该 id 且 force=False 返回 tombstoned 拒绝;否则检查 trash_dir/{node_id}.md,os.path.exists 为 False 返回 not_in_trash;可打开则读取并 self.add(override=True...) 恢复、移除 trash 源、audit,返回 ok True/id/forced=bool(force);
|
|
1368
2046
|
def restore(self, node_id: str, force: bool = False):
|
|
1369
2047
|
"""恢复:若在删除清单中且未 force → 拒绝(恢复时删除检查)。"""
|
|
1370
2048
|
tomb = [r for r in read_jsonl(self.deletions_log) if r.get("id") == node_id]
|
|
@@ -1398,6 +2076,7 @@ class MdCGOS(MdCG):
|
|
|
1398
2076
|
SESSION_TAG = "session"
|
|
1399
2077
|
|
|
1400
2078
|
@staticmethod
|
|
2079
|
+
# 生效条件:对任意 session 与 summary(None 分别按空串处理,summary 另去首尾空白)取 sha1 前 12 位拼成 "sess_{sig}",同 (session, summary) 得同 id;
|
|
1401
2080
|
def _session_node_id(session, summary):
|
|
1402
2081
|
"""会话要点节点 id:同 (session, summary) → 同 id(幂等覆盖,不新增)。"""
|
|
1403
2082
|
sig = hashlib.sha1(
|
|
@@ -1406,6 +2085,7 @@ class MdCGOS(MdCG):
|
|
|
1406
2085
|
return f"sess_{sig}"
|
|
1407
2086
|
|
|
1408
2087
|
@staticmethod
|
|
2088
|
+
# 生效条件:content 中「执行」字段真值时返回其前 500 字符,否则返回首个非空行去空白后前 500 字符,全空或无行时返回空串;
|
|
1409
2089
|
def _session_digest(content):
|
|
1410
2090
|
"""从会话节点正文取一行摘要(`# 执行:` 优先,否则首个非空行)。"""
|
|
1411
2091
|
v = _ccg_field(content, "执行")
|
|
@@ -1416,6 +2096,7 @@ class MdCGOS(MdCG):
|
|
|
1416
2096
|
return ln.strip()[:500]
|
|
1417
2097
|
return ""
|
|
1418
2098
|
|
|
2099
|
+
# 生效条件:当 summary 传入且 strip 后非空时,session 按显式入参、self.session、日期依次回落;conditions 为假值时回落默认条件;用 SESSION_TAG 与 session 标签调用 add,返回含 ok/id/session/layer/basis/tokens 的字典;summary 为空则 raise ValueError;
|
|
1419
2100
|
def session_note(self, summary, session=None, tags=None, layer="contextual",
|
|
1420
2101
|
importance=0.6, sensitivity=None, conditions=None,
|
|
1421
2102
|
basis="data"):
|
|
@@ -1454,6 +2135,7 @@ class MdCGOS(MdCG):
|
|
|
1454
2135
|
return {"ok": True, "id": nid, "session": session, "layer": layer,
|
|
1455
2136
|
"basis": basis, "tokens": est_tokens(content)}
|
|
1456
2137
|
|
|
2138
|
+
# 生效条件:当 session 传入且为真时仅保留 tags 含 f"session:{session}" 的项;保留 tags 含 SESSION_TAG 或任一以 "session:" 开头的索引节点,_read 的 content 为 None 则跳过;按 created_at 降序后返回前 max(1, int(limit or 5)) 条,limit 为假值(含 0/None)按 5 处理;
|
|
1457
2139
|
def _session_notes(self, session=None, limit=5):
|
|
1458
2140
|
"""按时间倒序取会话要点(索引过滤 + 惰性回读摘要)。只读,不写盘。"""
|
|
1459
2141
|
out = []
|
|
@@ -1473,21 +2155,29 @@ class MdCGOS(MdCG):
|
|
|
1473
2155
|
"layer": e.get("layer"), "tags": tags,
|
|
1474
2156
|
"summary": self._session_digest(content),
|
|
1475
2157
|
})
|
|
1476
|
-
out.sort(key=lambda n: -n["created_at"])
|
|
2158
|
+
out.sort(key=lambda n: (-n["created_at"], str(n.get("id") or "")))
|
|
1477
2159
|
return out[:max(1, int(limit or 5))]
|
|
1478
2160
|
|
|
2161
|
+
# 生效条件:limit 经 max(1,min(int(limit or 5),50))、budget_tokens 经 max(200,int(budget_tokens or 1200)) 归一后逐段取数(include_state 为真才取 self_state),每段异常只把段名追加进 degraded,再由 while 循环按预算交替裁 recent/notes 尾部、任务段最后才裁并置 tasks_truncated。
|
|
1479
2162
|
def session_recall(self, session=None, limit=5, recent_limit=10,
|
|
1480
2163
|
budget_tokens=1200, include_state=True):
|
|
1481
2164
|
"""按需恢复:一次调用返回「可续接的上下文包」(替代 hook 自动注入)。
|
|
1482
2165
|
|
|
1483
|
-
内容 = 最近会话要点 + 活跃目标 +
|
|
1484
|
-
|
|
1485
|
-
|
|
2166
|
+
内容 = 最近会话要点 + 活跃目标 + **任务台账(进行中 + 近期完成)** + 近期事件
|
|
2167
|
+
+ 未解问题 (+ 自我状态卡)。纯只读、无副作用;返回体受 budget_tokens 约束
|
|
2168
|
+
(超出即裁剪并显式上报)。无 hook 的载体应在会话开始时显式调用本 op 一次。
|
|
2169
|
+
|
|
2170
|
+
任务段(2026-09-16 新增)是「忘记已实现的工程」的直接解药:新会话开机即见
|
|
2171
|
+
「还在做的」与「刚做完的」,不必先想到去查。任务属结构层、跨会话稳定,
|
|
2172
|
+
故**不按 session 过滤**——工程台账跟着工程走,不跟着会话走。
|
|
1486
2173
|
"""
|
|
1487
2174
|
limit = max(1, min(int(limit or 5), 50))
|
|
1488
2175
|
budget = max(200, int(budget_tokens or 1200))
|
|
1489
2176
|
pack = {"ok": True, "session": session, "source": "session_recall",
|
|
1490
|
-
"notes": [], "goals": [],
|
|
2177
|
+
"notes": [], "goals": [],
|
|
2178
|
+
"tasks": {"active": [], "done": [], "active_total": 0,
|
|
2179
|
+
"done_total": 0},
|
|
2180
|
+
"recent": [], "unresolved": [],
|
|
1491
2181
|
"degraded": []}
|
|
1492
2182
|
# ① 会话要点
|
|
1493
2183
|
try:
|
|
@@ -1501,6 +2191,22 @@ class MdCGOS(MdCG):
|
|
|
1501
2191
|
for g in self.active_goals(limit=5)]
|
|
1502
2192
|
except Exception: # noqa: BLE001
|
|
1503
2193
|
pack["degraded"].append("goals")
|
|
2194
|
+
# ②.5 任务台账(structural 层)——见 docstring:不按 session 过滤
|
|
2195
|
+
try:
|
|
2196
|
+
from . import tasks as _tasks
|
|
2197
|
+
ts = _tasks.session_tasks(self, active_limit=5, done_limit=5)
|
|
2198
|
+
pack["tasks"] = {
|
|
2199
|
+
"active": [{"id": t["id"], "name": t["name"], "status": t["status"],
|
|
2200
|
+
"plan": (t.get("plan") or "")[:300],
|
|
2201
|
+
"updated_at": t.get("updated_at")}
|
|
2202
|
+
for t in ts["active"]],
|
|
2203
|
+
"done": [{"id": t["id"], "name": t["name"], "status": t["status"],
|
|
2204
|
+
"result": (t.get("result") or "")[:300],
|
|
2205
|
+
"updated_at": t.get("updated_at")}
|
|
2206
|
+
for t in ts["done"]],
|
|
2207
|
+
"active_total": ts["active_total"], "done_total": ts["done_total"]}
|
|
2208
|
+
except Exception: # noqa: BLE001
|
|
2209
|
+
pack["degraded"].append("tasks")
|
|
1504
2210
|
# ③ 近期事件(原始滚动窗口)
|
|
1505
2211
|
try:
|
|
1506
2212
|
evs = self.recent_events(limit=max(1, int(recent_limit or 10)))
|
|
@@ -1528,21 +2234,33 @@ class MdCGOS(MdCG):
|
|
|
1528
2234
|
pack["self_state"] = _ss.summary(self, session=session)
|
|
1529
2235
|
except Exception: # noqa: BLE001
|
|
1530
2236
|
pack["degraded"].append("self_state")
|
|
1531
|
-
# ⑥ 预算裁剪:交替丢 recent / notes
|
|
2237
|
+
# ⑥ 预算裁剪:交替丢 recent / notes 尾部;任务段**最后才让位**
|
|
2238
|
+
# (任务台账是结构性结论,事件流水是易失过程——先丢过程),
|
|
2239
|
+
# 被裁的事实在 tasks_truncated 里显式上报,不静默丢。
|
|
2240
|
+
tasks_trimmed = False
|
|
1532
2241
|
pack["tokens"] = est_tokens(json.dumps(pack, ensure_ascii=False))
|
|
1533
|
-
while pack["tokens"] > budget and (pack["recent"] or pack["notes"]
|
|
1534
|
-
|
|
2242
|
+
while pack["tokens"] > budget and (pack["recent"] or pack["notes"]
|
|
2243
|
+
or pack["tasks"]["active"]
|
|
2244
|
+
or pack["tasks"]["done"]):
|
|
2245
|
+
if pack["recent"] and len(pack["recent"]) >= len(pack["notes"]):
|
|
1535
2246
|
pack["recent"].pop()
|
|
1536
|
-
|
|
2247
|
+
elif pack["notes"]:
|
|
1537
2248
|
pack["notes"].pop()
|
|
2249
|
+
elif pack["tasks"]["done"] or pack["tasks"]["active"]:
|
|
2250
|
+
(pack["tasks"]["done"] or pack["tasks"]["active"]).pop()
|
|
2251
|
+
tasks_trimmed = True
|
|
2252
|
+
else:
|
|
2253
|
+
pack["recent"].pop()
|
|
1538
2254
|
pack["tokens"] = est_tokens(json.dumps(pack, ensure_ascii=False))
|
|
1539
2255
|
pack["budget_tokens"] = budget
|
|
1540
2256
|
pack["truncated"] = pack["tokens"] > budget
|
|
1541
|
-
pack["
|
|
1542
|
-
|
|
2257
|
+
pack["tasks_truncated"] = tasks_trimmed
|
|
2258
|
+
pack["note"] = ("只读上下文包:会话要点 + 目标 + 任务台账(进行中/近期完成)"
|
|
2259
|
+
"+ 近期事件 + 未解问题(+自我状态卡)。库侧替代 hook 自动注入;"
|
|
1543
2260
|
"会话开始时显式调用本 op 一次即可续接。")
|
|
1544
2261
|
return pack
|
|
1545
2262
|
|
|
2263
|
+
# 生效条件:以 self.recent_events(limit=max(1, int(limit or 40))) 取事件(limit 为 0/None/"" 等假值时回落 40,异常时 evs=[]),session 为真值时按 (r.get("meta") or {}).get("session")==session 过滤,正文按行去重收集长度≥8 的要点、user 行优先排序后取前 max(1, int(max_points or 8))(max_points 假值时回落 8)拼 summary,note 为真值时再调 session_note 写入并在 out 附 written_id。
|
|
1546
2264
|
def session_compact(self, session=None, limit=40, max_points=8,
|
|
1547
2265
|
note=False, importance=0.5):
|
|
1548
2266
|
"""上下文压缩摘要:把会话近期事件压成要点(可选写入会话节点)。
|
|
@@ -1600,6 +2318,7 @@ class MdCGOS(MdCG):
|
|
|
1600
2318
|
"refine", "refine_gate", "refine_history",
|
|
1601
2319
|
"refine_calibrate")
|
|
1602
2320
|
|
|
2321
|
+
# 生效条件:当 content 传入时,先调用 forgetting.prefeed 得到裁决 vd;write=False(默认)返回预演 out(written/reinforced 为 None);write=True 时按 vd["decision"] 为 "write" 则用 node_id 或 _prefeed_id(content)、importance_hint 为 None 时取 0.5 调用 add 并写 written;为 "reinforce" 且有 duplicate_with 则调用 forgetting.reinforce 写 reinforced;discard/defer 不写不并;最后返回 out;
|
|
1603
2322
|
def prefeed(self, content, layer="contextual", role=None,
|
|
1604
2323
|
verification_basis=None, importance_hint=None, node_id=None,
|
|
1605
2324
|
write=False, tags=None, conditions=None):
|
|
@@ -1635,6 +2354,7 @@ class MdCGOS(MdCG):
|
|
|
1635
2354
|
"discard": "已丢弃(不写)", "defer": "留待复核(不写不并)"}.get(dec, "")
|
|
1636
2355
|
return out
|
|
1637
2356
|
|
|
2357
|
+
# 生效条件:act 取 str(action or "stat") 去空白并 lower 后按分支分派(importance / longterm / prefeed / separate / stat 等);未识别 act 走 stat 兜底;只读与写层 action 库层不鉴权,apply 类批量改写由 MCP 分发层 require_admin 把守;
|
|
1638
2358
|
def maintain(self, action="stat", layer=None, limit=None, apply=False,
|
|
1639
2359
|
min_delta=None, max_rows=None, force=False, keep=None,
|
|
1640
2360
|
mode=None, snapshot_id=None, batch=None, entry_ids=None,
|
|
@@ -1773,6 +2493,19 @@ class MdCGOS(MdCG):
|
|
|
1773
2493
|
min_jaccard=min_jaccard,
|
|
1774
2494
|
min_cluster=common["min_cluster"])
|
|
1775
2495
|
return refine.history(self, limit=limit or 100, batch=batch)
|
|
2496
|
+
if act in ("comment_gate", "comment_gate_gate", "comment_gate_verdict"):
|
|
2497
|
+
# 环二:代码符号「条件化注释」抽样闸门(复用 refine 范式,对象换成 code_ 节点)。
|
|
2498
|
+
# plan 出工单(只读);apply 只落抽检留痕(**不改节点**)但决定扩批放行;
|
|
2499
|
+
# gate 复算通过率。绝不盲跑全量。权威实现见 md_cg/comment_gate.py。
|
|
2500
|
+
from . import comment_gate as _cg_gate
|
|
2501
|
+
who = actor or getattr(self, "actor", "maintain")
|
|
2502
|
+
canon = ("comment_gate" if act == "comment_gate"
|
|
2503
|
+
else "comment_gate_verdict")
|
|
2504
|
+
return _cg_gate.run(
|
|
2505
|
+
self, canon, ids=ids,
|
|
2506
|
+
n=(extra.get("sample_n") or extra.get("n")), seed=extra.get("seed"),
|
|
2507
|
+
apply=apply, verdicts=extra.get("verdicts"), actor=who,
|
|
2508
|
+
note=extra.get("reason"), batch=batch)
|
|
1776
2509
|
if act == "stat":
|
|
1777
2510
|
nodes = self.index.get("nodes") or {}
|
|
1778
2511
|
by_layer, imp_sum, protected, missing_basis = {}, 0.0, 0, 0
|
|
@@ -1802,6 +2535,7 @@ class MdCGOS(MdCG):
|
|
|
1802
2535
|
"note": "只读盘点;apply 类动作需管理权限(require_admin)。"}
|
|
1803
2536
|
raise ValueError(f"maintain 未知 action:{act}(可选 {list(self.MAINTAIN_ACTIONS)})")
|
|
1804
2537
|
|
|
2538
|
+
# 生效条件:action 为假值时回落 "promote",随后按 str(action or "promote").strip().lower() 分派到 promote、promote_rollback|rollback、promote_history、induce、contextualize、contextualize_rollback|relayer_rollback、contextualize_history 各路实现,全部不匹配时抛 ValueError。
|
|
1805
2539
|
def consolidate_run(self, action="promote", **kw):
|
|
1806
2540
|
"""离线固化面(P1:promote;P2:induce/run)。"""
|
|
1807
2541
|
from . import consolidate
|
|
@@ -1859,8 +2593,12 @@ class MdCGOS(MdCG):
|
|
|
1859
2593
|
# 三者都不进默认召回热路径,只在显式调用时工作。
|
|
1860
2594
|
|
|
1861
2595
|
INSIGHT_ACTIONS = ("window", "record", "verify", "list", "report",
|
|
1862
|
-
"reconstruct", "learn", "outlook", "catalog"
|
|
2596
|
+
"reconstruct", "learn", "outlook", "catalog",
|
|
2597
|
+
"fork", "branch_rewrite", "branch_search",
|
|
2598
|
+
"branch_merge", "branch_discard", "branches",
|
|
2599
|
+
"tickets")
|
|
1863
2600
|
|
|
2601
|
+
# 生效条件:act 取 str(action or "outlook") 去空白并 lower;只读分支 window/list/report/reconstruct/outlook/catalog,记账分支 record/verify(写 contextual 层),落库分支 learn(apply)/reconstruct(apply) 与六分支 act(fork/branch_rewrite/branch_search/branch_merge/branch_discard/branches);落库与分支类由 _insight_call 的 require_admin 把守;
|
|
1864
2602
|
def insight(self, action="outlook", **kw):
|
|
1865
2603
|
"""洞察条件层 + 情景重构 + 盲区学习 + 结构洞察(P2)。
|
|
1866
2604
|
|
|
@@ -1909,6 +2647,34 @@ class MdCGOS(MdCG):
|
|
|
1909
2647
|
max_nodes=(kw.get("max_nodes") or subgraph.RECON_MAX_NODES),
|
|
1910
2648
|
neighbors=(True if kw.get("neighbors") is None
|
|
1911
2649
|
else bool(kw.get("neighbors"))))
|
|
2650
|
+
if act in ("fork", "branch_rewrite", "branch_search", "branch_merge",
|
|
2651
|
+
"branch_discard", "branches"):
|
|
2652
|
+
# 记忆演化分支(Pi 移植④):fork / 分支改写(唯一正路,归属强制
|
|
2653
|
+
# 继承)/ 分支内检索 / 溯源合并 / 教训归档冷收 / 盘点。
|
|
2654
|
+
# discard 属写操作(MCP 分发层 require_admin)。
|
|
2655
|
+
from . import branches as _br
|
|
2656
|
+
if act == "fork":
|
|
2657
|
+
return _br.fork(self, kw.get("node_ids") or kw.get("ids"),
|
|
2658
|
+
branch_id=kw.get("branch_id"),
|
|
2659
|
+
note=kw.get("note"))
|
|
2660
|
+
if act == "branch_rewrite":
|
|
2661
|
+
return _br.rewrite(self,
|
|
2662
|
+
kw.get("node_id") or kw.get("pid") or "",
|
|
2663
|
+
kw.get("content") or "",
|
|
2664
|
+
tags=kw.get("tags"),
|
|
2665
|
+
importance=kw.get("importance"))
|
|
2666
|
+
if act == "branch_search":
|
|
2667
|
+
return _br.search(self,
|
|
2668
|
+
kw.get("content") or kw.get("query") or "",
|
|
2669
|
+
kw.get("branch_id") or "")
|
|
2670
|
+
if act == "branch_merge":
|
|
2671
|
+
return _br.merge(self, kw.get("branch_id"),
|
|
2672
|
+
reason=kw.get("reason"))
|
|
2673
|
+
if act == "branch_discard":
|
|
2674
|
+
return _br.discard(self, kw.get("branch_id"),
|
|
2675
|
+
summary=kw.get("content")
|
|
2676
|
+
or kw.get("summary") or "")
|
|
2677
|
+
return _br.list_branches(self)
|
|
1912
2678
|
if act == "explore":
|
|
1913
2679
|
# 信息差驱动自主探索(opt-in):提案 → 五态验证 → 回写 gap_hint
|
|
1914
2680
|
from . import autonomy
|
|
@@ -1916,6 +2682,18 @@ class MdCGOS(MdCG):
|
|
|
1916
2682
|
limit=(kw.get("limit") or 3),
|
|
1917
2683
|
window=(kw.get("window") or 200),
|
|
1918
2684
|
actor=actor)
|
|
2685
|
+
if act == "tickets":
|
|
2686
|
+
# 盲区消解票据(阶段三 §5.4,opt-in):盲区 → 四类任务卡
|
|
2687
|
+
# (research/prototype/grilling/task)经 tasks.upsert 落库挂图。
|
|
2688
|
+
# apply=True 属批量落库(MCP 分发层 require_admin 把守)。
|
|
2689
|
+
from . import blindspot_tickets as _bt
|
|
2690
|
+
types = kw.get("types")
|
|
2691
|
+
return _bt.make_tickets(
|
|
2692
|
+
self,
|
|
2693
|
+
types=(list(types) if isinstance(types, (list, tuple)) else None),
|
|
2694
|
+
limit=(kw.get("limit") or 10),
|
|
2695
|
+
min_blindspot=(kw.get("min_blindspot") or 0),
|
|
2696
|
+
apply=bool(kw.get("apply")), actor=actor)
|
|
1919
2697
|
if act == "learn":
|
|
1920
2698
|
lkw = {k: kw[k] for k in ("blindspot_id", "limit", "horizon",
|
|
1921
2699
|
"max_branches") if kw.get(k) is not None}
|
|
@@ -1947,14 +2725,83 @@ class MdCGOS(MdCG):
|
|
|
1947
2725
|
|
|
1948
2726
|
# ================= 健康度(并入 OS 指标) =================
|
|
1949
2727
|
|
|
2728
|
+
AUDIT_COUNT_MAX_BYTES = 64 << 20 # 超过此规模不再全量精确计数(O(1) 体检纪律)
|
|
2729
|
+
# ---- 审计日志分片轮转(治本面,2026-09-16)----
|
|
2730
|
+
# 元数据读数(_log_scale)只让「指标与代价错配」不再显形,**有界性的来源是轮转**:
|
|
2731
|
+
# 单文件 ≤ AUDIT_ROTATE_BYTES、分片数 ≤ AUDIT_KEEP_SHARDS ⇒ 单片读取代价与总量
|
|
2732
|
+
# 都有上界,体检/getsize 不再随运行时长线性劣化。
|
|
2733
|
+
AUDIT_ROTATE_BYTES = 64 << 20 # 活动日志轮转阈值(≤0 关闭轮转,退回无上界)
|
|
2734
|
+
AUDIT_KEEP_SHARDS = 8 # 归档分片保留数(≤0 不淘汰;淘汰必留审计痕)
|
|
2735
|
+
AUDIT_ARCHIVE = "_audit_archive" # 分片归档目录(不在 LAYERS 内,不参与节点索引)
|
|
2736
|
+
AUDIT_INDEX = "_index.json" # 归档索引:分片 bytes/events 缓存(稳态 O(1))
|
|
2737
|
+
AUDIT_PROBE_EVERY = 32 # 每 N 次写入探测一次大小(把写入税摊到 1/N)
|
|
2738
|
+
|
|
2739
|
+
# 生效条件:当 path 传入时,先 os.stat;OSError 返回 {'bytes':0,'mtime':None,'events':0,'exact':True};size <= AUDIT_COUNT_MAX_BYTES 时返回 count_jsonl(path) 精确条数 exact True;否则读取末尾 est_sample 字节估算 events、exact False 并附 note;
|
|
2740
|
+
def _log_scale(self, path, est_sample=256 << 10):
|
|
2741
|
+
"""日志量级读数(O(1)):以元数据为主,条数只在与规模相称时才精确。
|
|
2742
|
+
|
|
2743
|
+
为什么不去全量数条数(2026-09-16 使用者裁定「python 审查 4GB 数据是个
|
|
2744
|
+
不明智的选择」+ 第 4 条现场取证):
|
|
2745
|
+
|
|
2746
|
+
信息需求 = 量级(这条日志多大、多久没动)→ O(1) 元数据即可满足;
|
|
2747
|
+
实际代价 = 全量解析/扫描 → O(n) 磁盘 IO(本机 4.0 GB 实测数十秒);
|
|
2748
|
+
且日志**无上界增长**(实测 ~2 条/s 持续写入、累计 ~21.5 M 条 / 4.0 GB)。
|
|
2749
|
+
指标与代价错配,而「无上界」使任何「每次体检全量扫」的实现随运行时长
|
|
2750
|
+
线性劣化——4 GB 只是让错配显形,不是错配本身。
|
|
2751
|
+
|
|
2752
|
+
故:有界规模内给精确行数(count_jsonl 流式、内存恒定);超阈值只读元数据
|
|
2753
|
+
+ 尾部采样估算,**显式标注 exact=False 不假装精确**。要精确值走离线
|
|
2754
|
+
count_jsonl 或审计日志分片轮转(属方向性决策,待定夺)。
|
|
2755
|
+
"""
|
|
2756
|
+
try:
|
|
2757
|
+
st = os.stat(path)
|
|
2758
|
+
except OSError:
|
|
2759
|
+
return {"bytes": 0, "mtime": None, "events": 0, "exact": True}
|
|
2760
|
+
size = st.st_size
|
|
2761
|
+
out = {"bytes": size, "mtime": st.st_mtime}
|
|
2762
|
+
if size <= self.AUDIT_COUNT_MAX_BYTES:
|
|
2763
|
+
out.update({"events": count_jsonl(path), "exact": True})
|
|
2764
|
+
return out
|
|
2765
|
+
est = None
|
|
2766
|
+
try:
|
|
2767
|
+
with open(path, "rb") as f:
|
|
2768
|
+
f.seek(max(0, size - est_sample))
|
|
2769
|
+
tail = f.read()
|
|
2770
|
+
lines = [ln for ln in tail.split(b"\n") if ln.strip()]
|
|
2771
|
+
if lines:
|
|
2772
|
+
est = int(size / (len(tail) / float(len(lines))))
|
|
2773
|
+
except OSError:
|
|
2774
|
+
pass
|
|
2775
|
+
out.update({
|
|
2776
|
+
"events": est, "exact": False,
|
|
2777
|
+
"note": ("超过 %.0f MB 不给全量精确计数(O(n) 磁盘 IO 与量级体检不匹配);"
|
|
2778
|
+
"events 为尾部 %d KB 采样的估算值"
|
|
2779
|
+
% (self.AUDIT_COUNT_MAX_BYTES / 1048576.0, est_sample >> 10))})
|
|
2780
|
+
return out
|
|
2781
|
+
|
|
2782
|
+
# 生效条件:在已用 root 构造的实例上以 self.health() 为基底,附加 os 面读数(_role_counts、review_list/review_records 长度、_log_scale(deletions_log)["events"]、audit_scale 的 events/exact/bytes/shards/total_bytes/total_events/total_exact/oversized、last_d_records 长度、goals 的 total 与 active_goals(limit=0)、recent_events(limit=0)、protect_stats 去掉 ids/immutable_ids、forgetting/identity/consistency/metacognition/self_state/evolution 的 summary)后返回 h。
|
|
1950
2783
|
def health_os(self):
|
|
1951
2784
|
h = self.health()
|
|
2785
|
+
audit = self.audit_scale()
|
|
1952
2786
|
h["os"] = {
|
|
1953
2787
|
"roles": self._role_counts(),
|
|
1954
2788
|
"review_pending": len(self.review_list()),
|
|
1955
2789
|
"review_records": len(self.review_records()),
|
|
1956
|
-
|
|
1957
|
-
|
|
2790
|
+
# 审计/墓碑面走 _log_scale(O(1) 量级读数)而非 list(read_jsonl(...))
|
|
2791
|
+
# 也不再全量流式数行:物化读让只读体检把整条通道拖死(实测 4.0 GB
|
|
2792
|
+
# 日志 → RSS 5 GB+、数十分钟不返回),而全量流式扫仍要 O(n) 磁盘 IO
|
|
2793
|
+
# 且日志无上界增长(~2 条/s)→ 随运行时长线性劣化。见 _log_scale。
|
|
2794
|
+
"tombstones": self._log_scale(self.deletions_log)["events"],
|
|
2795
|
+
"audit_events": audit["events"], # 超阈值时为估算值
|
|
2796
|
+
"audit_events_exact": audit["exact"], # False = 上面是估算,非精确
|
|
2797
|
+
"audit_bytes": audit["bytes"], # 量级看这个(O(1) 精确)
|
|
2798
|
+
"audit_events_note": audit.get("note"),
|
|
2799
|
+
# 轮转面(有界性证据):活动文件 ≤rotate_bytes、分片 ≤keep_shards
|
|
2800
|
+
"audit_shards": audit["shards"], # 归档分片数
|
|
2801
|
+
"audit_total_bytes": audit["total_bytes"], # 活动 + 全部分片
|
|
2802
|
+
"audit_total_events": audit["total_events"], # 全量条数(分片走索引缓存)
|
|
2803
|
+
"audit_total_exact": audit["total_exact"], # False = 含超大分片的估算
|
|
2804
|
+
"audit_oversized": audit["oversized"], # 超大历史分片数(待离线切分)
|
|
1958
2805
|
"reflections": len(self.last_d_records()),
|
|
1959
2806
|
# 七件套覆盖度(第 5 篇):目标槽 + 近期事件窗口
|
|
1960
2807
|
"goals": {"total": len(self.list_goals()),
|
|
@@ -1977,6 +2824,7 @@ class MdCGOS(MdCG):
|
|
|
1977
2824
|
}
|
|
1978
2825
|
return h
|
|
1979
2826
|
|
|
2827
|
+
# 生效条件:无前置;遍历 self.index["nodes"] 按 e.get("role") 或 "(none)" 计数,返回 role → 计数 dict(不过滤、不排序);
|
|
1980
2828
|
def _role_counts(self):
|
|
1981
2829
|
c = {}
|
|
1982
2830
|
for e in self.index["nodes"].values():
|
|
@@ -1991,23 +2839,28 @@ class MdCGOS(MdCG):
|
|
|
1991
2839
|
|
|
1992
2840
|
# ============ 8. 嵌套子图 + 关系链(结构要素的可递归化 / 因果链=条件链)============
|
|
1993
2841
|
|
|
2842
|
+
# 生效条件:当 node_id 传入时,以 max_depth(默认 None)原样调用 subgraph.expand 并返回其结果;本函数不改变参数;
|
|
1994
2843
|
def subgraph_expand(self, node_id, max_depth=None):
|
|
1995
2844
|
"""递归展开嵌套子图:`max_depth=None` 数据驱动(展开到自然耗尽)。"""
|
|
1996
2845
|
return subgraph.expand(self, node_id, max_depth=max_depth)
|
|
1997
2846
|
|
|
2847
|
+
# 生效条件:当 node_id 传入时,以 max_depth(默认 None)原样调用 subgraph.flatten 并返回其结果;本函数不改变参数;
|
|
1998
2848
|
def subgraph_flatten(self, node_id, max_depth=None):
|
|
1999
2849
|
"""摊平为「节点 + 对称父子边」(part_of / parent_of)。"""
|
|
2000
2850
|
return subgraph.flatten(self, node_id, max_depth=max_depth)
|
|
2001
2851
|
|
|
2852
|
+
# 生效条件:当 limit 传入时,以 limit(默认 50)调用 subgraph.validate 并返回其结果;本函数不改变参数;
|
|
2002
2853
|
def subgraph_validate(self, limit=50):
|
|
2003
2854
|
"""树一致性:多父 / 环 / 悬空 / 自环(不一致 → 该层判定应退回 DEFER)。"""
|
|
2004
2855
|
return subgraph.validate(self, limit=limit)
|
|
2005
2856
|
|
|
2857
|
+
# 生效条件:无输入形参;调用 subgraph.roots(self) 并返回其结果;
|
|
2006
2858
|
def subgraph_roots(self):
|
|
2007
2859
|
return subgraph.roots(self)
|
|
2008
2860
|
|
|
2009
2861
|
# ---- 主动遗忘(写入侧三问闸门)+ 写保护盘点 ----
|
|
2010
2862
|
|
|
2863
|
+
# 生效条件:kw 中 gated 为假值时旁路直接 ACCEPT 写入并返回 bypass;否则 writelimit.check 非 None 时按 CONVERGE→MERGE 并经 converge_into 并入 target、DROP/DEFER 只记 forgetting 日志,无限流拦截时按 forgetting.assess 的四态处理(ACCEPT 走 add,ConsistencyError 或 written 为 None 转 DEFER;MERGE 走 reinforce;DROP/DEFER 不落库只留痕)。
|
|
2011
2864
|
def remember_gated(self, node_id, content, layer="contextual", **kw):
|
|
2012
2865
|
"""写入情景层记忆前的**主动遗忘闸门**:三问 → 四态。
|
|
2013
2866
|
|
|
@@ -2096,46 +2949,56 @@ class MdCGOS(MdCG):
|
|
|
2096
2949
|
"actor": self.actor})
|
|
2097
2950
|
return out
|
|
2098
2951
|
|
|
2952
|
+
# 生效条件:当 limit 传入时,以 limit(默认 100)调用 forgetting.history 并返回其结果;本函数不改变参数;
|
|
2099
2953
|
def forgetting_history(self, limit=100):
|
|
2100
2954
|
"""遗忘裁决留痕:为什么没记住,与为什么记住同样可查。"""
|
|
2101
2955
|
return forgetting.history(self, limit=limit)
|
|
2102
2956
|
|
|
2957
|
+
# 生效条件:无输入形参;调用 protect.stats(self) 并返回其结果;
|
|
2103
2958
|
def protect_stats(self):
|
|
2104
2959
|
"""写保护面盘点:受保护节点数、分层分布、自动保护命中数。"""
|
|
2105
2960
|
return protect.stats(self)
|
|
2106
2961
|
|
|
2107
2962
|
# ---- 身份特征识别(智能论 v3.4 位置效应 + 扮演论三接口)----
|
|
2108
2963
|
|
|
2964
|
+
# 生效条件:当 subject_id 与 text 传入时,以 subject_id、text 及 **kw 调用 identity.observe 并返回其结果;
|
|
2109
2965
|
def identity_observe(self, subject_id, text, **kw):
|
|
2110
2966
|
"""memory 接口:记录主体行为证据(供位置效应推断)。"""
|
|
2111
2967
|
return identity.observe(self, subject_id, text, **kw)
|
|
2112
2968
|
|
|
2969
|
+
# 生效条件:当 subject_id 与 text 传入时,以 subject_id、text 及 **kw 调用 identity.set_anchor 并返回其结果;
|
|
2113
2970
|
def identity_anchor(self, subject_id, text, **kw):
|
|
2114
2971
|
"""anchor 接口:写身份锚点(不可遗忘;role/user 不得进 self 层)。"""
|
|
2115
2972
|
return identity.set_anchor(self, subject_id, text, **kw)
|
|
2116
2973
|
|
|
2974
|
+
# 生效条件:当 subject_id 与 trait 传入时,以 subject_id、trait 及 **kw 调用 identity.add_trait 并返回其结果;
|
|
2117
2975
|
def identity_trait(self, subject_id, trait, **kw):
|
|
2118
2976
|
"""values 接口:写条件触发的特征 / 特化价值观(落结构层)。"""
|
|
2119
2977
|
return identity.add_trait(self, subject_id, trait, **kw)
|
|
2120
2978
|
|
|
2979
|
+
# 生效条件:当 subject_id 传入时,调用 identity.profile(self, subject_id) 并返回其结果;
|
|
2121
2980
|
def identity_profile(self, subject_id):
|
|
2122
2981
|
"""主体画像:身份锚点 + 位置效应 + 条件特征(不止「用户画像」)。"""
|
|
2123
2982
|
return identity.profile(self, subject_id)
|
|
2124
2983
|
|
|
2984
|
+
# 生效条件:当 limit 传入时,以 limit(默认 0)调用 identity.positions 并返回其结果;本函数不改变 limit;
|
|
2125
2985
|
def identity_positions(self, limit=0):
|
|
2126
2986
|
"""所有主体的位置效应分布(谁在记录/反思/验证/输出/维生)。"""
|
|
2127
2987
|
return identity.positions(self, limit=limit)
|
|
2128
2988
|
|
|
2989
|
+
# 生效条件:当 limit 传入时,以 limit(默认 100)调用 identity.history 并返回其结果;本函数不改变 limit;
|
|
2129
2990
|
def identity_history(self, limit=100):
|
|
2130
2991
|
"""身份操作留痕。"""
|
|
2131
2992
|
return identity.history(self, limit=limit)
|
|
2132
2993
|
|
|
2994
|
+
# 生效条件:无输入形参;调用 identity.catalog() 并返回其结果;
|
|
2133
2995
|
def identity_catalog(self):
|
|
2134
2996
|
"""自描述:位置效应表 + 扮演论三接口。"""
|
|
2135
2997
|
return identity.catalog()
|
|
2136
2998
|
|
|
2137
2999
|
# ---- 节点间自动冲突检测(三级决策:情绪 → 反思 → 递归反思)----
|
|
2138
3000
|
|
|
3001
|
+
# 生效条件:当 content 传入时,以 layer/condition_space/non_applicable_conditions/tags/exclude/limit/depth/auto_flywheel 的传入值或默认值(limit=consistency.MAX_SCAN、depth=consistency.MAX_DEPTH、auto_flywheel=False)调用 consistency.check 并返回其结果;
|
|
2139
3002
|
def check_consistency(self, content, layer=None, condition_space=None,
|
|
2140
3003
|
non_applicable_conditions=None, tags=None,
|
|
2141
3004
|
exclude=None, limit=consistency.MAX_SCAN,
|
|
@@ -2151,20 +3014,24 @@ class MdCGOS(MdCG):
|
|
|
2151
3014
|
exclude=exclude, limit=limit, depth=depth,
|
|
2152
3015
|
auto_flywheel=auto_flywheel)
|
|
2153
3016
|
|
|
3017
|
+
# 生效条件:当 limit 传入时,以 limit(默认 100)调用 consistency.history 并返回其结果;本函数不改变 limit;
|
|
2154
3018
|
def consistency_history(self, limit=100):
|
|
2155
3019
|
"""冲突判定留痕:为什么冲突 / 为什么放行。"""
|
|
2156
3020
|
return consistency.history(self, limit=limit)
|
|
2157
3021
|
|
|
3022
|
+
# 生效条件:无输入形参;调用 consistency.summary(self) 并返回其结果;
|
|
2158
3023
|
def consistency_stats(self):
|
|
2159
3024
|
"""冲突面汇总(供 health / 运维审计)。"""
|
|
2160
3025
|
return consistency.summary(self)
|
|
2161
3026
|
|
|
3027
|
+
# 生效条件:无输入形参;调用 consistency.catalog() 并返回其结果;
|
|
2162
3028
|
def consistency_catalog(self):
|
|
2163
3029
|
"""自描述:三级决策 + 四态 + 递归约束(供协议对照验证)。"""
|
|
2164
3030
|
return consistency.catalog()
|
|
2165
3031
|
|
|
2166
3032
|
# ============ 独立元认知(观察自身认知的二阶单元,不参与裁决)============
|
|
2167
3033
|
|
|
3034
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 50)转调 metacognition.report(self, window=window) 并返回其结果。
|
|
2168
3035
|
def metacognition_report(self, window=50):
|
|
2169
3036
|
"""元认知报告:轨迹 / 校准 / 盲区 / 信任 + 确定性建议。
|
|
2170
3037
|
|
|
@@ -2174,46 +3041,61 @@ class MdCGOS(MdCG):
|
|
|
2174
3041
|
"""
|
|
2175
3042
|
return metacognition.report(self, window=window)
|
|
2176
3043
|
|
|
3044
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 50)转调 metacognition.trace(self, window=window) 并返回其结果。
|
|
2177
3045
|
def metacognition_trace(self, window=50):
|
|
2178
3046
|
"""信息差轨迹 D(t) → dD/dt(方向)→ d²D/dt²(情绪)。"""
|
|
2179
3047
|
return metacognition.trace(self, window=window)
|
|
2180
3048
|
|
|
3049
|
+
# 生效条件:在已用 root 构造的实例上以 max_scan(缺省 2000)转调 metacognition.calibration(self, max_scan=max_scan) 并返回其结果。
|
|
2181
3050
|
def metacognition_calibration(self, max_scan=2000):
|
|
2182
3051
|
"""自信校准:期望正确率 vs 实际验证通过率(过度自信 / 过度保守)。"""
|
|
2183
3052
|
return metacognition.calibration(self, max_scan=max_scan)
|
|
2184
3053
|
|
|
3054
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 20)、window(缺省 200)转调 metacognition.blindspots(self, limit=limit, window=window) 并返回其结果。
|
|
2185
3055
|
def metacognition_blindspots(self, limit=20, window=200):
|
|
2186
3056
|
"""盲区地图:反复 BLINDSPOT 的查询邻域 + 未解问题清单。"""
|
|
2187
3057
|
return metacognition.blindspots(self, limit=limit, window=window)
|
|
2188
3058
|
|
|
3059
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 100)转调 metacognition.trust(self, window=window) 并返回其结果。
|
|
2189
3060
|
def metacognition_trust(self, window=100):
|
|
2190
3061
|
"""P_gap(信息差置信)+ P_trust(验证稳定置信)+ d²T/dt²(情感)。"""
|
|
2191
3062
|
return metacognition.trust(self, window=window)
|
|
2192
3063
|
|
|
3064
|
+
# 生效条件:传入 query,并以 k(缺省 5)、min_sim(缺省 0.25)转调 metacognition.self_check(self, query, k=k, min_sim=min_sim) 并返回其结果。
|
|
2193
3065
|
def self_check(self, query, k=5, min_sim=0.25):
|
|
2194
3066
|
"""元认知闸门:回答前先自问「我对这件事的认知状态如何」。"""
|
|
2195
3067
|
return metacognition.self_check(self, query, k=k, min_sim=min_sim)
|
|
2196
3068
|
|
|
3069
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 100)转调 metacognition.history(self, limit=limit) 并返回其结果。
|
|
2197
3070
|
def metacognition_history(self, limit=100):
|
|
2198
3071
|
"""元认知留痕(倒序)。"""
|
|
2199
3072
|
return metacognition.history(self, limit=limit)
|
|
2200
3073
|
|
|
3074
|
+
# 生效条件:在已用 root 构造的实例上转调 metacognition.summary(self) 并返回其结果。
|
|
2201
3075
|
def metacognition_summary(self):
|
|
2202
3076
|
"""一句话元认知状态(供 health / 面板)。"""
|
|
2203
3077
|
return metacognition.summary(self)
|
|
2204
3078
|
|
|
3079
|
+
# 生效条件:不带 self 转调 metacognition.catalog() 并返回其结果。
|
|
2205
3080
|
def metacognition_catalog(self):
|
|
2206
3081
|
"""自描述:观测面 + 理论出处 + 独立性约束。"""
|
|
2207
3082
|
return metacognition.catalog()
|
|
2208
3083
|
|
|
3084
|
+
# 生效条件:在已用 root 构造的实例上以 window(缺省 200)转调 metacognition.d_meta_face(self, window=window) 并返回其结果。
|
|
3085
|
+
def metacognition_d_meta(self, window=200):
|
|
3086
|
+
"""D_meta 观测面:边界压力向量(三代理各自 [0,1],不合成单值)。"""
|
|
3087
|
+
return metacognition.d_meta_face(self, window=window)
|
|
3088
|
+
|
|
2209
3089
|
# ============ 自我状态层(薄自我 + 富索引)============
|
|
2210
3090
|
# self 层只放状态卡(单例)+ 关系节点;九项自我信息只登记当前值与指针,
|
|
2211
3091
|
# 具体任务/人物/会话/时间/信任的细节由认知图按五维索引连接(不搬运内容)。
|
|
2212
3092
|
|
|
3093
|
+
# 生效条件:在已用 root 构造的实例上以 subject(缺省为模块级常量 self_state.DEFAULT_SUBJECT)转调 self_state.snapshot(self, subject) 并返回其结果。
|
|
2213
3094
|
def self_state_snapshot(self, subject=self_state.DEFAULT_SUBJECT):
|
|
2214
3095
|
"""读自我状态卡(薄):信息差/信任/情绪/情感/短期记忆/重要性/身份/关系。"""
|
|
2215
3096
|
return self_state.snapshot(self, subject)
|
|
2216
3097
|
|
|
3098
|
+
# 生效条件:subject 缺省为 self_state.DEFAULT_SUBJECT,且当 kw 中 session 为假值(缺键/None/"")时置为 getattr(self, "session", None),随后以 (**kw) 转调 self_state.refresh(self, subject, **kw) 并返回其结果。
|
|
2217
3099
|
def self_state_refresh(self, subject=self_state.DEFAULT_SUBJECT, **kw):
|
|
2218
3100
|
"""刷新状态卡:聚合九项自我信息 → 写卡 + 版本链留痕(幂等)。
|
|
2219
3101
|
|
|
@@ -2223,44 +3105,54 @@ class MdCGOS(MdCG):
|
|
|
2223
3105
|
kw["session"] = getattr(self, "session", None)
|
|
2224
3106
|
return self_state.refresh(self, subject, **kw)
|
|
2225
3107
|
|
|
3108
|
+
# 生效条件:subject 缺省为模块级常量 self_state.DEFAULT_SUBJECT,以 (**kw) 转调 self_state.bootstrap(self, subject, **kw) 并返回其结果。
|
|
2226
3109
|
def self_state_bootstrap(self, subject=self_state.DEFAULT_SUBJECT, **kw):
|
|
2227
3110
|
"""会话启动加载:状态卡 + 关系 + 最近留痕 + 五维索引(跨会话自我续接)。"""
|
|
2228
3111
|
return self_state.bootstrap(self, subject, **kw)
|
|
2229
3112
|
|
|
3113
|
+
# 生效条件:传入 frm 与 to,以 (**kw) 转调 self_state.relate(self, frm, to, **kw) 并返回其结果。
|
|
2230
3114
|
def self_state_relate(self, frm, to, **kw):
|
|
2231
3115
|
"""写一条有向关系(自我 ↔ 其他智能),reciprocal=True 时双向。"""
|
|
2232
3116
|
return self_state.relate(self, frm, to, **kw)
|
|
2233
3117
|
|
|
3118
|
+
# 生效条件:以 subject(缺省 None)与 direction(缺省 "both")转调 self_state.relations(self, subject=subject, direction=direction) 并返回其结果。
|
|
2234
3119
|
def self_state_relations(self, subject=None, direction="both"):
|
|
2235
3120
|
"""列出关系节点(按 subject 过滤出/入)。"""
|
|
2236
3121
|
return self_state.relations(self, subject=subject, direction=direction)
|
|
2237
3122
|
|
|
3123
|
+
# 生效条件:传入 dim 与 value,以 (**kw) 转调 self_state.index(self, dim, value, **kw) 并返回其结果。
|
|
2238
3124
|
def self_state_index(self, dim, value, **kw):
|
|
2239
3125
|
"""按五维索引(task/person/session/time/trust)反查具体详情节点。"""
|
|
2240
3126
|
return self_state.index(self, dim, value, **kw)
|
|
2241
3127
|
|
|
3128
|
+
# 生效条件:以 subject(缺省为模块级常量 self_state.DEFAULT_SUBJECT)转调 self_state.dimensions(self, subject) 并返回其结果。
|
|
2242
3129
|
def self_state_dimensions(self, subject=self_state.DEFAULT_SUBJECT):
|
|
2243
3130
|
"""状态卡登记的五维索引标签。"""
|
|
2244
3131
|
return self_state.dimensions(self, subject)
|
|
2245
3132
|
|
|
3133
|
+
# 生效条件:以 subject(缺省为模块级常量 self_state.DEFAULT_SUBJECT)与 (**kw) 转调 self_state.audit(self, subject, **kw) 并返回其结果。
|
|
2246
3134
|
def self_state_audit(self, subject=self_state.DEFAULT_SUBJECT, **kw):
|
|
2247
3135
|
"""自我信息一致性审计:单例/版本链/时序/派生自洽/跨面一致/身份/关系/保护/索引。"""
|
|
2248
3136
|
return self_state.audit(self, subject, **kw)
|
|
2249
3137
|
|
|
3138
|
+
# 生效条件:以 limit(缺省 100)、subject(缺省 None)转调 self_state.history(self, limit=limit, subject=subject) 并返回其结果。
|
|
2250
3139
|
def self_state_history(self, limit=100, subject=None):
|
|
2251
3140
|
"""自我状态留痕(倒序,含版本链 hash)。"""
|
|
2252
3141
|
return self_state.history(self, limit=limit, subject=subject)
|
|
2253
3142
|
|
|
3143
|
+
# 生效条件:subject 缺省为模块级常量 self_state.DEFAULT_SUBJECT,session 为假值(None/"")时回落 getattr(self, "session", None),随后转调 self_state.summary(self, subject, session=session or getattr(self, "session", None)) 并返回其结果。
|
|
2254
3144
|
def self_state_summary(self, subject=self_state.DEFAULT_SUBJECT,
|
|
2255
3145
|
session=None):
|
|
2256
3146
|
"""一句话自我状态(供 health / 面板;session= 会话归因切片)。"""
|
|
2257
3147
|
return self_state.summary(self, subject,
|
|
2258
3148
|
session=session or getattr(self, "session", None))
|
|
2259
3149
|
|
|
3150
|
+
# 生效条件:不带 self 转调 self_state.catalog() 并返回其结果。
|
|
2260
3151
|
def self_state_catalog(self):
|
|
2261
3152
|
"""自描述:九项自我信息 + 五维索引 + 审计规则。"""
|
|
2262
3153
|
return self_state.catalog()
|
|
2263
3154
|
|
|
3155
|
+
# 生效条件:传入 node_id,relation_types 为假值(None/""等)时回落模块级常量 chain.CAUSAL_TYPES,连同 max_depth(缺省 chain.MAX_DEPTH_DEFAULT)、direction(缺省 "out")、max_chains(缺省 50)、sort(缺省 "strength")转调 chain.walk 并返回其结果。
|
|
2264
3156
|
def causal_chain(self, node_id, relation_types=None,
|
|
2265
3157
|
max_depth=chain.MAX_DEPTH_DEFAULT, direction="out",
|
|
2266
3158
|
max_chains=50, sort="strength"):
|
|
@@ -2273,12 +3165,14 @@ class MdCGOS(MdCG):
|
|
|
2273
3165
|
max_depth=max_depth, direction=direction,
|
|
2274
3166
|
max_chains=max_chains, sort=sort)
|
|
2275
3167
|
|
|
3168
|
+
# 生效条件:传入 node_id,以 (**kw) 转调 chain.explain(self, node_id, **kw) 并返回其结果。
|
|
2276
3169
|
def explain_chain(self, node_id, **kw):
|
|
2277
3170
|
"""人类可读链式解释:「什么条件下 → 发生什么」。"""
|
|
2278
3171
|
return chain.explain(self, node_id, **kw)
|
|
2279
3172
|
|
|
2280
3173
|
# ============ 生成式预测 / 因果推理 ============
|
|
2281
3174
|
|
|
3175
|
+
# 生效条件:在已用 root 构造的实例上按原值透传 start_id/blindspot_id/horizon(缺省 predict.HORIZON_DEFAULT)/max_branches(缺省 predict.MAX_BRANCHES_DEFAULT)/sort(缺省 "composite")/limit(缺省 0)/semantic(缺省 True)调用 predict.routes 并返回其结果。
|
|
2282
3176
|
def predict_routes(self, start_id=None, blindspot_id=None,
|
|
2283
3177
|
horizon=predict.HORIZON_DEFAULT,
|
|
2284
3178
|
max_branches=predict.MAX_BRANCHES_DEFAULT,
|
|
@@ -2289,6 +3183,7 @@ class MdCGOS(MdCG):
|
|
|
2289
3183
|
max_branches=max_branches, sort=sort,
|
|
2290
3184
|
limit=limit, semantic=semantic)
|
|
2291
3185
|
|
|
3186
|
+
# 生效条件:传入 predicted_node_id 与 actual_node_id,并以 hit(缺省 None)/note(缺省 "")/actor(缺省 "predict")/sync_self(缺省 True)转调 predict.feedback 并返回其结果。
|
|
2292
3187
|
def predict_feedback(self, predicted_node_id, actual_node_id=None,
|
|
2293
3188
|
hit=None, note="", actor="predict", sync_self=True):
|
|
2294
3189
|
"""预测反馈(D-006):命中 → 边置信度 +0.05;未命中 → 登记 rejected。
|
|
@@ -2300,18 +3195,22 @@ class MdCGOS(MdCG):
|
|
|
2300
3195
|
hit=hit, note=note, actor=actor,
|
|
2301
3196
|
sync_self=sync_self)
|
|
2302
3197
|
|
|
3198
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 20)转调 predict.stats(self, limit=limit) 并返回其结果。
|
|
2303
3199
|
def predict_stats(self, limit=20):
|
|
2304
3200
|
"""预测统计:调用数 / 路线数 / 命中率 / 动态阈值。"""
|
|
2305
3201
|
return predict.stats(self, limit=limit)
|
|
2306
3202
|
|
|
3203
|
+
# 生效条件:不带 self 转调 predict.catalog() 并返回其结果。
|
|
2307
3204
|
def predict_catalog(self):
|
|
2308
3205
|
"""自描述:D-001~D-006 决策、权重、校准参数、与 AEIS 的差异。"""
|
|
2309
3206
|
return predict.catalog()
|
|
2310
3207
|
|
|
3208
|
+
# 生效条件:传入 a_id 与 b_id,以 max_depth(缺省 5)转调 predict.causal_path(self, a_id, b_id, max_depth=max_depth) 并返回其结果。
|
|
2311
3209
|
def causal_path(self, a_id, b_id, max_depth=5):
|
|
2312
3210
|
"""因果路径推理:A 能否沿因果/时序边到达 B(伪因果防护的完整语义)。"""
|
|
2313
3211
|
return predict.causal_path(self, a_id, b_id, max_depth=max_depth)
|
|
2314
3212
|
|
|
3213
|
+
# 生效条件:传入 a_id 与 b_id,转调 predict.causal_gate(self, a_id, b_id) 并返回其结果。
|
|
2315
3214
|
def causal_gate(self, a_id, b_id):
|
|
2316
3215
|
"""D-002 伪因果过滤门 → (准入?, 理由)。"""
|
|
2317
3216
|
return predict.causal_gate(self, a_id, b_id)
|
|
@@ -2319,6 +3218,7 @@ class MdCGOS(MdCG):
|
|
|
2319
3218
|
# ============ 演化账本(md 载体:规律 + 状态,可回滚)============
|
|
2320
3219
|
# 每一次修改 = 对一条缺失条件的补充;记录的是认知规律与状态,不是实现。
|
|
2321
3220
|
|
|
3221
|
+
# 生效条件:在已用 root 构造的实例上转调 evolution.record,其中 kind 为假值(None/"")时回落模块级常量 evolution.KIND_CONDITION_GAP,**extra 为空字典时传 extra=None,node_id/pattern/missing/action/evidence/source/before/after 原值透传。
|
|
2322
3222
|
def evolution_record(self, node_id=None, pattern="", missing="", action="",
|
|
2323
3223
|
evidence="", source="", kind=None, before=None,
|
|
2324
3224
|
after=None, **extra):
|
|
@@ -2329,36 +3229,44 @@ class MdCGOS(MdCG):
|
|
|
2329
3229
|
kind=kind or evolution.KIND_CONDITION_GAP,
|
|
2330
3230
|
before=before, after=after, extra=extra or None)
|
|
2331
3231
|
|
|
3232
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 50)、node_id(缺省 None)、kind(缺省 None)转调 evolution.entries,返回 {"entries": ...}。
|
|
2332
3233
|
def evolution_entries(self, limit=50, node_id=None, kind=None):
|
|
2333
3234
|
"""账本条目(倒序)。"""
|
|
2334
3235
|
return {"entries": evolution.entries(
|
|
2335
3236
|
self, limit=limit, node_id=node_id, kind=kind)}
|
|
2336
3237
|
|
|
3238
|
+
# 生效条件:传入 entry_id,转调 evolution.show(self, entry_id),返回 {"entry": ...}。
|
|
2337
3239
|
def evolution_show(self, entry_id):
|
|
2338
3240
|
"""单条演化条目。"""
|
|
2339
3241
|
return {"entry": evolution.show(self, entry_id)}
|
|
2340
3242
|
|
|
3243
|
+
# 生效条件:传入 node_id,以 limit(缺省 50)转调 evolution.history(self, node_id, limit=limit) 并返回其结果。
|
|
2341
3244
|
def evolution_history(self, node_id, limit=50):
|
|
2342
3245
|
"""某节点的演化史(倒序)。"""
|
|
2343
3246
|
return evolution.history(self, node_id, limit=limit)
|
|
2344
3247
|
|
|
3248
|
+
# 生效条件:在已用 root 构造的实例上以 limit(缺省 10)转调 evolution.patterns(self, limit=limit) 并返回其结果。
|
|
2345
3249
|
def evolution_patterns(self, limit=10):
|
|
2346
3250
|
"""规律统计:哪一维条件反复缺失、由谁触发、哪些规律重复出现。"""
|
|
2347
3251
|
return evolution.patterns(self, limit=limit)
|
|
2348
3252
|
|
|
3253
|
+
# 生效条件:在已用 root 构造的实例上转调 evolution.summary(self) 并返回其结果。
|
|
2349
3254
|
def evolution_summary(self):
|
|
2350
3255
|
"""一句话演化状态(供 health / 面板)。"""
|
|
2351
3256
|
return evolution.summary(self)
|
|
2352
3257
|
|
|
3258
|
+
# 生效条件:传入 entry_id,以 dry_run(缺省 False)、note(缺省 "")转调 evolution.rollback 并返回其结果(含 note 为空时原样传空串)。
|
|
2353
3259
|
def evolution_rollback(self, entry_id, dry_run=False, note=""):
|
|
2354
3260
|
"""把某条演化撤回其 before 状态;撤销本身也记一条条目。"""
|
|
2355
3261
|
return evolution.rollback(self, entry_id, dry_run=dry_run, note=note)
|
|
2356
3262
|
|
|
3263
|
+
# 生效条件:不带 self 转调 evolution.catalog() 并返回其结果。
|
|
2357
3264
|
def evolution_catalog(self):
|
|
2358
3265
|
"""自描述:载体 + 原则 + 字段 + 可回滚范围。"""
|
|
2359
3266
|
return evolution.catalog()
|
|
2360
3267
|
|
|
2361
3268
|
|
|
3269
|
+
# 生效条件:以 root 构造并把 **kw 透传父类,principal 为假值(None)时回落新建 Principal()、否则用传入的 principal,其 actor 作为 actor 传给父类,master_key 原样交给 _init_crypto;
|
|
2362
3270
|
class MdCGSecure(MdCGOS):
|
|
2363
3271
|
"""带权限的记忆 OS:租户 + 密级(clearance)× 节点敏感度(sensitivity)。
|
|
2364
3272
|
|
|
@@ -2369,6 +3277,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2369
3277
|
· 审计带 tenant/actor/session(可追溯到哪个会话做了什么)
|
|
2370
3278
|
"""
|
|
2371
3279
|
|
|
3280
|
+
# 生效条件:传入 root 时 principal 为假值(None 等)则新建 Principal(),以 self.principal.actor 作 actor 调父类 MdCGOS.__init__(root, actor=..., **kw),再设 self.session = self.principal.session 并执行 self._init_crypto(master_key)(master_key 缺省 None)。
|
|
2372
3281
|
def __init__(self, root: str, principal: Principal = None,
|
|
2373
3282
|
master_key=None, **kw):
|
|
2374
3283
|
self.principal = principal or Principal()
|
|
@@ -2381,6 +3290,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2381
3290
|
|
|
2382
3291
|
# ---------- 私有内容加密(密钥即访问权 + 身份一致性识别)----------
|
|
2383
3292
|
|
|
3293
|
+
# 生效条件:master_key 非 None 时取 self._resolve_master_key(master_key)、为 None 时取 crypto.load_master_key();kek 为假值时置 _crypto_error="no_master_key" 且 self.kek=self.dek=None 并返回;否则置 self.kek 并调 crypto.provision_dek(self.root, kek, tenant, actor, clearance=...) 得 dek;捕获 crypto.CryptoError 或 OSError 时置 _crypto_error=str(e) 且 self.kek=self.dek=None。
|
|
2384
3294
|
def _init_crypto(self, master_key=None):
|
|
2385
3295
|
"""解析 KEK(显式 → 环境变量 → 仓库外主密钥文件),签发本身份 DEK。
|
|
2386
3296
|
|
|
@@ -2405,6 +3315,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2405
3315
|
self.kek = self.dek = None
|
|
2406
3316
|
|
|
2407
3317
|
@staticmethod
|
|
3318
|
+
# 生效条件:master_key 为 bytes/bytearray 时直接取字节,否则转 str 去空白后若该串是存在路径则读文件内容,len==64 走 bytes.fromhex、否则走 base64 解码,所得长度不等于 crypto.KEY_LEN 时抛 CryptoError;
|
|
2408
3319
|
def _resolve_master_key(master_key):
|
|
2409
3320
|
"""接受 32B bytes / 64 位 hex / base64 / 密钥文件路径。"""
|
|
2410
3321
|
if isinstance(master_key, (bytes, bytearray)):
|
|
@@ -2423,17 +3334,20 @@ class MdCGSecure(MdCGOS):
|
|
|
2423
3334
|
raise crypto.CryptoError("主密钥须为 32 字节")
|
|
2424
3335
|
return k
|
|
2425
3336
|
|
|
3337
|
+
# 生效条件:调用 self._init_crypto(master_key);master_key 为 None 时回落环境变量与主密钥文件,密钥缺失或长度非 32 字节抛 crypto.CryptoError;成功后返回 crypto_status();
|
|
2426
3338
|
def unlock(self, master_key=None):
|
|
2427
3339
|
"""运行时解锁(显式密钥 / 重新加载环境变量或主密钥文件)。"""
|
|
2428
3340
|
self._init_crypto(master_key)
|
|
2429
3341
|
return self.crypto_status()
|
|
2430
3342
|
|
|
3343
|
+
# 生效条件:无前置;丢弃内存中的 self.kek / self.dek 并置 _crypto_error="locked"(已落盘密文不受影响);返回 crypto_status();
|
|
2431
3344
|
def lock(self):
|
|
2432
3345
|
"""锁定:丢弃内存中的密钥(已落盘密文不受影响)。"""
|
|
2433
3346
|
self.kek = self.dek = None
|
|
2434
3347
|
self._crypto_error = "locked"
|
|
2435
3348
|
return self.crypto_status()
|
|
2436
3349
|
|
|
3350
|
+
# 生效条件:调用 crypto_status 时,返回当前加密状态字典;unlocked 取决于 self.dek is not None,kek_fp 在 self.kek 为假值时取 None,否则取 crypto.kek_fingerprint(self.kek)。
|
|
2437
3351
|
def crypto_status(self):
|
|
2438
3352
|
"""当前加密状态(不含密钥材料)。"""
|
|
2439
3353
|
return {
|
|
@@ -2449,6 +3363,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2449
3363
|
"envelopes": crypto.envelopes(self.root),
|
|
2450
3364
|
}
|
|
2451
3365
|
|
|
3366
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,若 sens 不在 crypto.ENCRYPTED_LEVELS 或 content 已加密则原样返回 content;否则 self.dek 为假值时写 seal_denied 审计并抛 crypto.LockedError,有 dek 时 seal_node 并写 seal 审计后返回密文。
|
|
2452
3367
|
def _seal_content(self, node_id, content, sensitivity=None):
|
|
2453
3368
|
"""私有内容(private / secret)写入前加密;无密钥 → fail-closed。"""
|
|
2454
3369
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -2469,6 +3384,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2469
3384
|
"actor": self.principal.actor})
|
|
2470
3385
|
return sealed
|
|
2471
3386
|
|
|
3387
|
+
# 生效条件:content 为 None 或非加密时原样返回 content;加密但 self.dek 为假值时写 read_locked 审计并返回 None;crypto.open_node 抛 CryptoError 时写 open_failed 审计并返回 None,成功则返回明文。
|
|
2472
3388
|
def _open_content(self, node_id, fm, content):
|
|
2473
3389
|
"""密文解封;无密钥 / 身份不符 → None(不可读),失败留审计。"""
|
|
2474
3390
|
if content is None or not crypto.is_encrypted(content):
|
|
@@ -2491,6 +3407,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2491
3407
|
|
|
2492
3408
|
# ---------- 索引:把 role / sensitivity / 写入归属一并索引 ----------
|
|
2493
3409
|
|
|
3410
|
+
# 生效条件:对传入的 kw 生效——writer/session 缺键时分别落 self.principal.actor 与 self.session,self.principal.harness 为真值时 harness 缺键才落该值,已存在的键一律不覆盖。
|
|
2494
3411
|
def _attribution(self, kw):
|
|
2495
3412
|
"""写入归属注入(归因维度,不参与授权)。
|
|
2496
3413
|
|
|
@@ -2504,6 +3421,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2504
3421
|
kw.setdefault("harness", self.principal.harness)
|
|
2505
3422
|
return kw
|
|
2506
3423
|
|
|
3424
|
+
# 生效条件:在 super()._scan_nodes() 结果上逐节点重新 _read,仅当读出的 fm 为真值时把 role、sensitivity(假值回落 DEFAULT_SENSITIVITY)、writer、session、content_kind(角色化读取视图的候选资格维度,第四阶段 6.1)写回该条目。
|
|
2507
3425
|
def _scan_nodes(self):
|
|
2508
3426
|
nodes = super()._scan_nodes()
|
|
2509
3427
|
for nid, e in nodes.items():
|
|
@@ -2513,8 +3431,10 @@ class MdCGSecure(MdCGOS):
|
|
|
2513
3431
|
e["sensitivity"] = fm.get("sensitivity") or DEFAULT_SENSITIVITY
|
|
2514
3432
|
e["writer"] = fm.get("writer")
|
|
2515
3433
|
e["session"] = fm.get("session")
|
|
3434
|
+
e["content_kind"] = fm.get("content_kind")
|
|
2516
3435
|
return nodes
|
|
2517
3436
|
|
|
3437
|
+
# 生效条件:仅当 self.index["nodes"] 中已存在 nid 时,把该条目的 sensitivity 置为 sens 并标记进 self._dirty;nid 不存在则不做任何事、无返回。
|
|
2518
3438
|
def _index_sensitivity(self, nid, sens):
|
|
2519
3439
|
e = self.index["nodes"].get(nid)
|
|
2520
3440
|
if e is not None:
|
|
@@ -2523,6 +3443,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2523
3443
|
|
|
2524
3444
|
# ---------- 写:权限校验 ----------
|
|
2525
3445
|
|
|
3446
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 _rank(sens) 并 principal.require_layer_write(layer, sens),再 _attribution(kw) 后转 super().add,最后按下发的 nid 调 _index_sensitivity 并返回 nid。
|
|
2526
3447
|
def add(self, node_id: str, content: str, layer: str = "knowledge",
|
|
2527
3448
|
sensitivity: str = None, **kw) -> str:
|
|
2528
3449
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -2533,6 +3454,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2533
3454
|
self._index_sensitivity(nid, sens)
|
|
2534
3455
|
return nid
|
|
2535
3456
|
|
|
3457
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write("rejected", sens) 与 _attribution(kw) 后转 super().add_rejected,最后按下发的 nid 调 _index_sensitivity 并返回 nid。
|
|
2536
3458
|
def add_rejected(self, hypothesis: str, reason: str, sensitivity: str = None, **kw) -> str:
|
|
2537
3459
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
2538
3460
|
self.principal.require_layer_write("rejected", sens)
|
|
@@ -2541,6 +3463,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2541
3463
|
self._index_sensitivity(nid, sens)
|
|
2542
3464
|
return nid
|
|
2543
3465
|
|
|
3466
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write("unresolved", sens) 与 _attribution(kw) 后转 super().add_unresolved(question, known_clues, goal),最后调 _index_sensitivity 并返回 nid。
|
|
2544
3467
|
def add_unresolved(self, question: str, known_clues: str = "", goal: str = "",
|
|
2545
3468
|
sensitivity: str = None, **kw) -> str:
|
|
2546
3469
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -2550,11 +3473,13 @@ class MdCGSecure(MdCGOS):
|
|
|
2550
3473
|
self._index_sensitivity(nid, sens)
|
|
2551
3474
|
return nid
|
|
2552
3475
|
|
|
3476
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 principal.require_layer_write(kw.get("layer") or "contextual", sens),再转 super().propose(node_id, content, sensitivity=sens) 并返回其结果。
|
|
2553
3477
|
def propose(self, node_id: str, content: str, sensitivity: str = None, **kw):
|
|
2554
3478
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
2555
3479
|
self.principal.require_layer_write(kw.get("layer") or "contextual", sens)
|
|
2556
3480
|
return super().propose(node_id, content, sensitivity=sens, **kw)
|
|
2557
3481
|
|
|
3482
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,先 _rank(sens) 并 principal.require_layer_write("goals", sens),再转 super().add_goal,最后按 gid 调 _index_sensitivity 并返回 gid。
|
|
2558
3483
|
def add_goal(self, goal: str, sensitivity: str = None, **kw) -> str:
|
|
2559
3484
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
2560
3485
|
_rank(sens)
|
|
@@ -2563,10 +3488,12 @@ class MdCGSecure(MdCGOS):
|
|
|
2563
3488
|
self._index_sensitivity(gid, sens)
|
|
2564
3489
|
return gid
|
|
2565
3490
|
|
|
3491
|
+
# 生效条件:先 principal.require_layer_write("goals", DEFAULT_SENSITIVITY),再转 super().set_goal_status(node_id, status)。
|
|
2566
3492
|
def set_goal_status(self, node_id: str, status: str):
|
|
2567
3493
|
self.principal.require_layer_write("goals", DEFAULT_SENSITIVITY)
|
|
2568
3494
|
return super().set_goal_status(node_id, status)
|
|
2569
3495
|
|
|
3496
|
+
# 生效条件:sens 取 sensitivity or DEFAULT_SENSITIVITY,经 _rank(sens) 与 principal.require_write(sens) 后把 m 基于 meta 复制并 setdefault tenant/session、harness 与 unit 为真值时补入,再强制 m["sensitivity"]=sens,text 经 _seal_content("_recent", text, sens) 后连 tags=tags 一起转 super().remember_event(window 为 None 时不传该参,否则带上 window)。
|
|
2570
3497
|
def remember_event(self, role: str, text: str, tags=None, meta=None,
|
|
2571
3498
|
window=None, sensitivity: str = None):
|
|
2572
3499
|
sens = sensitivity or DEFAULT_SENSITIVITY
|
|
@@ -2587,6 +3514,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2587
3514
|
|
|
2588
3515
|
# ---------- 读:密级过滤 ----------
|
|
2589
3516
|
|
|
3517
|
+
# 生效条件:e 的 sensitivity 为假值时先 _read 回填为 fm 的 sensitivity or DEFAULT_SENSITIVITY,再以 self.principal.allows(sens) 的布尔结果为准。
|
|
2590
3518
|
def _readable(self, e) -> bool:
|
|
2591
3519
|
sens = e.get("sensitivity")
|
|
2592
3520
|
if not sens:
|
|
@@ -2597,6 +3525,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2597
3525
|
e["sensitivity"] = sens
|
|
2598
3526
|
return self.principal.allows(sens)
|
|
2599
3527
|
|
|
3528
|
+
# 生效条件:先以 limit=None 取 super().list_goals(status=status) 的全量,再只保留 index 中 _readable(e) 为真的目标,limit 为真值时返回 keep[:limit]、否则返回全部 keep。
|
|
2600
3529
|
def list_goals(self, status=None, limit=None):
|
|
2601
3530
|
"""读隔离:只返回当前 clearance 可见的目标(active_goals/goal_text 同源过滤)。"""
|
|
2602
3531
|
out = super().list_goals(status=status, limit=None)
|
|
@@ -2607,6 +3536,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2607
3536
|
keep.append(g)
|
|
2608
3537
|
return keep[:limit] if limit else keep
|
|
2609
3538
|
|
|
3539
|
+
# 生效条件:先以 limit=0 取全量,逐条跳过 meta.tenant 与 self.principal.tenant 不一致的、以及 principal.allows(meta 的 sensitivity or DEFAULT_SENSITIVITY) 为假的,文本加密时解封失败(None)也跳过,最后 limit 为真值返回 keep[:limit]、否则返回全部 keep。
|
|
2610
3540
|
def recent_events(self, limit=20, roles=None, since=None, newest_first=True):
|
|
2611
3541
|
"""读隔离:只返回本 tenant 且当前 clearance 可见的近期事件。"""
|
|
2612
3542
|
out = super().recent_events(limit=0, roles=roles, since=since,
|
|
@@ -2627,46 +3557,96 @@ class MdCGSecure(MdCGOS):
|
|
|
2627
3557
|
keep.append(r)
|
|
2628
3558
|
return keep[:limit] if limit else keep
|
|
2629
3559
|
|
|
3560
|
+
# 生效条件:先 principal.require_admin("clear_recent"),再转 super().clear_recent()。
|
|
2630
3561
|
def clear_recent(self):
|
|
2631
3562
|
self.principal.require_admin("clear_recent")
|
|
2632
3563
|
return super().clear_recent()
|
|
2633
3564
|
|
|
3565
|
+
# 生效条件:在 super()._candidates(layer=layer, roles=roles, include_work=include_work, session=session, branch=branch, validity=validity, start_time=start_time, end_time=end_time, start_operator=start_operator, end_operator=end_operator, time_axis=time_axis, view=view) 的结果上,只保留 self._readable(e) 为真的条目(时间算子在父类候选层**单点已过滤**,此处只叠加读可见性、不重复判一次——重复判会让 dropped 计数与 meta 脱钩)。
|
|
2634
3566
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
2635
|
-
session=None
|
|
3567
|
+
session=None, branch=None, validity=None,
|
|
3568
|
+
start_time=None, end_time=None, start_operator=None,
|
|
3569
|
+
end_operator=None, time_axis=None, view=None):
|
|
2636
3570
|
out = super()._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
2637
|
-
session=session
|
|
3571
|
+
session=session, branch=branch, validity=validity,
|
|
3572
|
+
start_time=start_time, end_time=end_time,
|
|
3573
|
+
start_operator=start_operator,
|
|
3574
|
+
end_operator=end_operator, time_axis=time_axis,
|
|
3575
|
+
view=view)
|
|
2638
3576
|
return [e for e in out if self._readable(e)]
|
|
2639
3577
|
|
|
3578
|
+
# 生效条件:在 super()._neg_coverage(terms) 的结果上,只保留 self._readable(e) 为真的条目。
|
|
2640
3579
|
def _neg_coverage(self, terms):
|
|
2641
3580
|
return [e for e in super()._neg_coverage(terms) if self._readable(e)]
|
|
2642
3581
|
|
|
3582
|
+
# 生效条件:node_id 在 self.index["nodes"] 中存在且 self._readable(e) 为假时返回 None,否则转 super().get(node_id)。
|
|
2643
3583
|
def get(self, node_id: str):
|
|
2644
3584
|
e = self.index["nodes"].get(node_id)
|
|
2645
3585
|
if e is not None and not self._readable(e):
|
|
2646
3586
|
return None # 读隔离:不可见即不存在
|
|
2647
3587
|
return super().get(node_id)
|
|
2648
3588
|
|
|
3589
|
+
# 生效条件:把 *a/**kw 原样转给 super().search_rrf 后,仅保留其结果中每条以 self.index["nodes"].get(结果节点 id) 为索引(索引缺该 id 时用结果节点自身)经 self._readable 判为可见的条目,且 kw["validity"] 为真时该条目经 trust.is_expired 判为未过期者,kw["view"] 为真值时该条目经 roleviews.matches 判为满足视图资格(非法 view ValueError),再返回。
|
|
2649
3590
|
def search_rrf(self, *a, **kw):
|
|
2650
|
-
"""RRF 路径里的图扩展会绕过 _candidates,这里显式再过滤一次。
|
|
3591
|
+
"""RRF 路径里的图扩展会绕过 _candidates,这里显式再过滤一次。
|
|
3592
|
+
|
|
3593
|
+
时效过滤(validity)同属候选资格:图扩展会把过期节点重新带回结果,
|
|
3594
|
+
故必须在此与读可见性一并二次过滤,否则过期节点经扩散路径绕过 _candidates。
|
|
3595
|
+
角色化读取视图(第四阶段 6.1)同理:view 声明的候选资格对图扩展
|
|
3596
|
+
扩散路径同样生效,在此一并兜底。非法 view 由 roleviews.matches 的
|
|
3597
|
+
fail-closed 直接抛出(与候选层同口径,不会静默放行)。
|
|
3598
|
+
时间算子(阶段二 4.1)同理由此二次过滤——判据复用 `trust.filter_by_time`
|
|
3599
|
+
的同一实现口径(`window_matches_node`),**不另写一套轴/算子判断**。
|
|
3600
|
+
边界:本层计数不并入 `meta["time_filter"]`(该块以父类候选层为准),
|
|
3601
|
+
二次过滤只做「不放进结果」的兜底,差额如实不记账。
|
|
3602
|
+
"""
|
|
2651
3603
|
res, meta = super().search_rrf(*a, **kw)
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
3604
|
+
validity = kw.get("validity")
|
|
3605
|
+
view = kw.get("view")
|
|
3606
|
+
now = time.time() if validity else None
|
|
3607
|
+
_en_t, _ax_t, _why_t = trust.check_time_args(
|
|
3608
|
+
kw.get("start_time"), kw.get("end_time"), kw.get("start_operator"),
|
|
3609
|
+
kw.get("end_operator"), kw.get("time_axis"))
|
|
3610
|
+
keep = []
|
|
3611
|
+
for r in res:
|
|
3612
|
+
e = self.index["nodes"].get(r[0]["id"], r[0])
|
|
3613
|
+
if not self._readable(e):
|
|
3614
|
+
continue
|
|
3615
|
+
if view is not None and not roleviews.matches(e, view):
|
|
3616
|
+
continue
|
|
3617
|
+
if validity and trust.is_expired(e, now=now):
|
|
3618
|
+
continue
|
|
3619
|
+
if _en_t:
|
|
3620
|
+
_m, _miss = trust.window_matches_node(
|
|
3621
|
+
e, _ax_t, trust.parse_time(kw.get("start_time")),
|
|
3622
|
+
trust.parse_time(kw.get("end_time")),
|
|
3623
|
+
kw.get("start_operator"), kw.get("end_operator"))
|
|
3624
|
+
if _miss:
|
|
3625
|
+
if _ax_t == "observed":
|
|
3626
|
+
continue # 观察轴 fail-closed(与候选层同策略)
|
|
3627
|
+
elif not _m:
|
|
3628
|
+
continue
|
|
3629
|
+
keep.append(r)
|
|
3630
|
+
return keep, meta
|
|
2655
3631
|
|
|
2656
3632
|
# ---------- 管理:需 can_admin ----------
|
|
2657
3633
|
|
|
3634
|
+
# 生效条件:先 principal.require_admin("forget"),再转 super().forget(node_id, reason, override=override)。
|
|
2658
3635
|
def forget(self, node_id: str, reason: str = "", override: bool = False):
|
|
2659
3636
|
self.principal.require_admin("forget")
|
|
2660
3637
|
return super().forget(node_id, reason, override=override)
|
|
2661
3638
|
|
|
3639
|
+
# 生效条件:先 principal.require_admin("restore"),再转 super().restore(node_id, force=force)。
|
|
2662
3640
|
def restore(self, node_id: str, force: bool = False):
|
|
2663
3641
|
self.principal.require_admin("restore")
|
|
2664
3642
|
return super().restore(node_id, force=force)
|
|
2665
3643
|
|
|
3644
|
+
# 生效条件:先 principal.require_admin("review_decide"),再把 *a/**kw 原样转给 super().review_decide。
|
|
2666
3645
|
def review_decide(self, *a, **kw):
|
|
2667
3646
|
self.principal.require_admin("review_decide")
|
|
2668
3647
|
return super().review_decide(*a, **kw)
|
|
2669
3648
|
|
|
3649
|
+
# 生效条件:先 principal.require_admin("evolution_rollback"),再转 super().evolution_rollback(entry_id, dry_run=dry_run, note=note)。
|
|
2670
3650
|
def evolution_rollback(self, entry_id, dry_run=False, note=""):
|
|
2671
3651
|
"""回滚是管理操作:撤回结构变更 → 需 can_admin。"""
|
|
2672
3652
|
self.principal.require_admin("evolution_rollback")
|
|
@@ -2674,6 +3654,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2674
3654
|
|
|
2675
3655
|
# ---------- 身份/审计 ----------
|
|
2676
3656
|
|
|
3657
|
+
# 生效条件:无入参,返回 principal.as_dict、root、按 SENSITIVITY_ORDER 中 p.allows 为真筛出的 readable_sensitivities、index 中 _readable 为真的 nodes_visible 与 nodes_total、encryption(crypto_status);tokens.role_spec 可用时另补 role_label/duty/forbidden,导入或取值异常则跳过。
|
|
2677
3658
|
def whoami(self):
|
|
2678
3659
|
p = self.principal
|
|
2679
3660
|
out = {"principal": p.as_dict(), "root": self.root,
|
|
@@ -2693,6 +3674,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2693
3674
|
pass
|
|
2694
3675
|
return out
|
|
2695
3676
|
|
|
3677
|
+
# 生效条件:对 meta 先 setdefault tenant/session/clearance,并在 principal.harness、principal.unit 为真值时补入同名键,再转 super()._audit(op, node_id, **meta)。
|
|
2696
3678
|
def _audit(self, op, node_id, **meta):
|
|
2697
3679
|
meta.setdefault("tenant", self.principal.tenant)
|
|
2698
3680
|
meta.setdefault("session", self.principal.session)
|
|
@@ -2704,6 +3686,7 @@ class MdCGSecure(MdCGOS):
|
|
|
2704
3686
|
meta.setdefault("unit", self.principal.unit)
|
|
2705
3687
|
super()._audit(op, node_id, **meta)
|
|
2706
3688
|
|
|
3689
|
+
# 生效条件:在 super().health_os() 结果上写入 os.sustain(sustain.summary(self))与 security(principal 的 tenant/clearance、_sensitivity_counts、crypto_status)后返回。
|
|
2707
3690
|
def health_os(self):
|
|
2708
3691
|
h = super().health_os()
|
|
2709
3692
|
# 持续性自维持(常驻 / 心跳 / 会话续接):只读摘要,不做巡检
|
|
@@ -2716,9 +3699,10 @@ class MdCGSecure(MdCGOS):
|
|
|
2716
3699
|
}
|
|
2717
3700
|
return h
|
|
2718
3701
|
|
|
3702
|
+
# 生效条件:对 self.index["nodes"] 遍历生效——按每条 e 的 sensitivity(假值回落 DEFAULT_SENSITIVITY)累计计数并返回该字典。
|
|
2719
3703
|
def _sensitivity_counts(self):
|
|
2720
3704
|
c = {}
|
|
2721
3705
|
for e in self.index["nodes"].values():
|
|
2722
3706
|
s = e.get("sensitivity") or DEFAULT_SENSITIVITY
|
|
2723
3707
|
c[s] = c.get(s, 0) + 1
|
|
2724
|
-
return c
|
|
3708
|
+
return c
|