@furongjun1999/dsh-memory 0.4.11 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/codebuddy/CODEBUDDY.md +11 -3
- package/codebuddy/README.md +92 -90
- package/codebuddy/mcp.json +27 -27
- package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -169
- package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -144
- package/docs/README.md +142 -111
- package/docs/discipline/harnesses.yaml +244 -226
- package/docs/discipline/templates/full.md.tmpl +61 -61
- package/docs/discipline/templates/rules.mdc.tmpl +68 -0
- package/docs/discipline/templates/skill.md.tmpl +23 -23
- package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md +299 -299
- package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md +239 -239
- package/docs/eval//345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md +174 -174
- package/docs/eval//346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md +223 -223
- package/docs/{mdcg → hive}//344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md +165 -160
- package/docs/hive//344/273/244/347/211/214/350/257/255/344/271/211/344/277/256/346/255/243_/344/273/262/350/243/201/344/275/215_v0.1.md +76 -0
- package/docs/{mdcg → hive}//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.5.md +236 -236
- package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -43
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +85 -0
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -102
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +47 -0
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -503
- package/docs/hive//350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +85 -85
- package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +191 -0
- package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +90 -0
- package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -216
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +646 -646
- package/docs/mdcg/lingshu_tutorial.html +14449 -14449
- package/docs/mdcg/release_v0.4.11.md +49 -0
- package/docs/mdcg/release_v0.4.5.md +55 -55
- package/docs/mdcg/tool_table_v0.3.0.md +117 -117
- package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -600
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +275 -275
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +54 -0
- package/docs/mdcg//346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +130 -130
- package/docs/mdcg//347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md +278 -278
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -172
- package/docs/mdcg//347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +270 -270
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +491 -491
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md +340 -340
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +588 -588
- package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -176
- package/docs/plans//345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +81 -81
- package/docs/swarm//350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +704 -704
- package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -242
- package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -129
- package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -134
- package/docs/theory//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
- package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -145
- package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +144 -144
- package/docs/theory//347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +198 -198
- package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -221
- package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -237
- package/docs//345/217/221/345/270/203/344/273/266/345/233/236/346/272/257/350/257/264/346/230/216_v0.1.md +82 -82
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +28 -2
- package/dsh/README.md +82 -82
- package/dsh/cordis.yml.example +139 -139
- package/dsh/update-lingshu.bat +11 -11
- package/lib/hooks.js +36 -2
- package/lib/lib/roleplay_web.js +427 -427
- package/md_cg/__init__.py +7 -7
- package/md_cg/audit.py +368 -368
- package/md_cg/autonomy.py +287 -287
- package/md_cg/backfill.py +1327 -1327
- package/md_cg/backfill_bigdomain.py +34 -34
- package/md_cg/bench6_arms.py +410 -410
- package/md_cg/bench6_common.py +230 -230
- package/md_cg/bench6_competitors.py +212 -212
- package/md_cg/bench_axis_domain.py +257 -257
- package/md_cg/bench_blind_comp.py +308 -308
- package/md_cg/bench_en_atoms_public.py +230 -230
- package/md_cg/bench_governance.py +348 -348
- package/md_cg/bench_lme_zh.py +410 -410
- package/md_cg/bench_locomo.py +121 -121
- package/md_cg/bench_locomo_zh.py +450 -450
- package/md_cg/bench_locomo_zh_public.py +147 -147
- package/md_cg/bench_longmem.py +112 -112
- package/md_cg/bench_membench.py +632 -632
- package/md_cg/bench_p0.py +149 -149
- package/md_cg/bench_progressive.py +287 -287
- package/md_cg/bench_role_views.py +238 -238
- package/md_cg/bench_task_ab.py +243 -243
- package/md_cg/bench_task_ab_llm.py +408 -408
- package/md_cg/bench_unified_en.py +204 -204
- package/md_cg/bench_zh_mad.py +601 -601
- package/md_cg/blindspot_tickets.py +123 -123
- package/md_cg/branches.py +285 -285
- package/md_cg/build_postings.py +73 -73
- package/md_cg/ccgc.py +1005 -948
- package/md_cg/census.py +132 -132
- package/md_cg/chain.py +300 -300
- package/md_cg/codeindex.py +531 -531
- package/md_cg/coldverify.py +292 -292
- package/md_cg/comment_gate.py +337 -337
- package/md_cg/cond_compose.py +190 -190
- package/md_cg/cond_facts.py +154 -154
- package/md_cg/cond_template.json +106 -106
- package/md_cg/condition_anchor.py +142 -142
- package/md_cg/conformance.py +726 -726
- package/md_cg/consistency.py +717 -717
- package/md_cg/consolidate.py +1536 -1439
- package/md_cg/corpus.py +110 -110
- package/md_cg/crosscheck.py +1097 -1097
- package/md_cg/crypto.py +437 -437
- package/md_cg/d_meta.py +310 -310
- package/md_cg/datapath.py +334 -334
- package/md_cg/docindex.py +473 -473
- package/md_cg/eval_common.py +575 -575
- package/md_cg/evidence.py +580 -580
- package/md_cg/evolution.py +477 -477
- package/md_cg/export.py +220 -220
- package/md_cg/forgetting.py +581 -581
- package/md_cg/fsutil.py +329 -329
- package/md_cg/hotcache.py +238 -214
- package/md_cg/hyperedge.py +251 -251
- package/md_cg/identity.py +390 -390
- package/md_cg/insight.py +500 -500
- package/md_cg/interop.py +199 -0
- package/md_cg/lexicon/build_cedict_en_zh.py +329 -329
- package/md_cg/lexicon/build_standard_en.py +171 -171
- package/md_cg/lexicon/expand_en_zh.py +211 -211
- package/md_cg/lifecycle.py +272 -272
- package/md_cg/linkref.py +280 -280
- package/md_cg/links.py +622 -622
- package/md_cg/mcp_server.py +129 -32
- package/md_cg/md_whitebox.py +345 -345
- package/md_cg/mdcg.py +176 -117
- package/md_cg/mdcos.py +79 -13
- package/md_cg/metacognition.py +591 -591
- package/md_cg/migrate.py +119 -119
- package/md_cg/migrate_aeis.py +221 -221
- package/md_cg/migrate_roleplay.py +293 -293
- package/md_cg/migrate_wisdom_graph.py +360 -360
- package/md_cg/mreview/__init__.py +25 -25
- package/md_cg/mreview/__main__.py +110 -110
- package/md_cg/mreview/bundle.py +178 -178
- package/md_cg/mreview/candidates.py +262 -262
- package/md_cg/mreview/govern.py +693 -693
- package/md_cg/mreview/locate.py +939 -939
- package/md_cg/mreview/pipeline.py +728 -728
- package/md_cg/mreview/rules/duplication.json +21 -21
- package/md_cg/mreview/rules/field_coverage.json +54 -54
- package/md_cg/mreview/rules/source_license.json +21 -21
- package/md_cg/mreview/rules/template_flow.json +21 -21
- package/md_cg/mreview/ruleset.py +252 -252
- package/md_cg/nodefile.py +575 -575
- package/md_cg/pooling.py +484 -472
- package/md_cg/postings.py +298 -298
- package/md_cg/predict.py +1100 -1100
- package/md_cg/progressive.py +123 -123
- package/md_cg/protect.py +272 -272
- package/md_cg/protocol/md_cg_gate.proto +33 -33
- package/md_cg/protocol.py +372 -372
- package/md_cg/provenance.py +582 -582
- package/md_cg/reach.py +453 -453
- package/md_cg/readcache.py +85 -0
- package/md_cg/refindex.py +833 -833
- package/md_cg/refine.py +604 -604
- package/md_cg/roleviews.py +89 -89
- package/md_cg/routing.py +365 -365
- package/md_cg/scrub.py +852 -852
- package/md_cg/security.py +274 -274
- package/md_cg/self_state.py +1029 -1029
- package/md_cg/selfreport.py +151 -151
- package/md_cg/semantic/__init__.py +10 -10
- package/md_cg/semantic/canonical.py +122 -122
- package/md_cg/semantic/en_normalizer.py +364 -364
- package/md_cg/semantic/en_zh_map.json +28694 -0
- package/md_cg/semantic/export_en_zh_map.py +64 -0
- package/md_cg/semantic/unify.py +45 -0
- package/md_cg/semantic/zh_en_atoms.py +139 -139
- package/md_cg/signer.py +562 -562
- package/md_cg/sources.py +815 -582
- package/md_cg/statushdr.py +179 -179
- package/md_cg/stg.py +48 -37
- package/md_cg/subgraph.py +729 -729
- package/md_cg/sustain.py +1138 -1138
- package/md_cg/tasks.py +470 -470
- package/md_cg/test_action_derive.py +203 -203
- package/md_cg/test_audit_rotate.py +270 -270
- package/md_cg/test_autonomy.py +143 -143
- package/md_cg/test_bench_governance.py +102 -102
- package/md_cg/test_blindspot_tickets.py +166 -166
- package/md_cg/test_branches.py +249 -249
- package/md_cg/test_ccg_perturb.py +184 -184
- package/md_cg/test_ccgc.py +433 -433
- package/md_cg/test_census_prune.py +81 -81
- package/md_cg/test_cond_compose_anchors.py +76 -76
- package/md_cg/test_cond_match.py +165 -165
- package/md_cg/test_condition_anchor.py +81 -81
- package/md_cg/test_d_meta.py +412 -412
- package/md_cg/test_datapath_root.py +199 -199
- package/md_cg/test_en_pipeline.py +166 -166
- package/md_cg/test_gain_gate.py +212 -212
- package/md_cg/test_health_scale.py +173 -173
- package/md_cg/test_hive_ingest.py +285 -0
- package/md_cg/test_hot_cold.py +215 -215
- package/md_cg/test_hyperedge.py +245 -245
- package/md_cg/test_i26_empty_first_write.py +116 -0
- package/md_cg/test_i27_e041_identity.py +128 -0
- package/md_cg/test_i28_hotcache_prodpath.py +122 -0
- package/md_cg/test_identity_attribution.py +147 -147
- package/md_cg/test_index_durability.py +224 -224
- package/md_cg/test_interop.py +93 -0
- package/md_cg/test_lifecycle.py +309 -309
- package/md_cg/test_linkref.py +306 -306
- package/md_cg/test_lock.py +43 -43
- package/md_cg/test_md_access_parity.py +255 -255
- package/md_cg/test_md_writepath.py +345 -345
- package/md_cg/test_mdstore_search_parity.py +160 -0
- package/md_cg/test_mr_m2.py +587 -587
- package/md_cg/test_mr_m3.py +710 -710
- package/md_cg/test_mr_m4.py +485 -485
- package/md_cg/test_p0.py +250 -250
- package/md_cg/test_p1.py +316 -316
- package/md_cg/test_p10_identity.py +173 -173
- package/md_cg/test_p11_consistency.py +233 -233
- package/md_cg/test_p12_metacognition.py +212 -212
- package/md_cg/test_p13_encryption.py +241 -241
- package/md_cg/test_p14_sustain.py +249 -249
- package/md_cg/test_p15_scrub.py +280 -280
- package/md_cg/test_p16_self_state.py +301 -301
- package/md_cg/test_p17_predict.py +354 -354
- package/md_cg/test_p18_whitebox.py +171 -171
- package/md_cg/test_p19_migrate_roleplay.py +149 -149
- package/md_cg/test_p20_evolution.py +315 -315
- package/md_cg/test_p21_tokens.py +293 -270
- package/md_cg/test_p22_theory.py +175 -175
- package/md_cg/test_p23_links.py +311 -311
- package/md_cg/test_p24_evidence.py +227 -227
- package/md_cg/test_p25_weights.py +156 -156
- package/md_cg/test_p26_refindex.py +416 -416
- package/md_cg/test_p27_docindex.py +765 -765
- package/md_cg/test_p28_refcheck.py +305 -305
- package/md_cg/test_p29_session_ingest_export.py +354 -333
- package/md_cg/test_p3.py +11 -2
- package/md_cg/test_p30_maintain.py +330 -330
- package/md_cg/test_p31_insight.py +534 -534
- package/md_cg/test_p32_backfill.py +298 -298
- package/md_cg/test_p33_ccg_wiring.py +293 -293
- package/md_cg/test_p34_crosscheck.py +331 -331
- package/md_cg/test_p35_conditioned_claim.py +252 -252
- package/md_cg/test_p36_kp_align.py +230 -230
- package/md_cg/test_p37_condition_space.py +248 -248
- package/md_cg/test_p38_concurrent_flush.py +102 -0
- package/md_cg/test_p38_contextualize.py +273 -273
- package/md_cg/test_p39_verify_flow.py +113 -0
- package/md_cg/test_p39_vision_evidence.py +369 -369
- package/md_cg/test_p40_refine_worklist.py +241 -241
- package/md_cg/test_p41_evolve_patrol.py +224 -224
- package/md_cg/test_p42_provenance.py +269 -269
- package/md_cg/test_p43_pooling.py +412 -398
- package/md_cg/test_p44_md_whitebox.py +231 -231
- package/md_cg/test_p45_session_identity.py +219 -219
- package/md_cg/test_p46_unit_scope.py +272 -272
- package/md_cg/test_p47_session_view.py +281 -0
- package/md_cg/test_p4_fuzzy.py +223 -223
- package/md_cg/test_p5_semantic.py +226 -226
- package/md_cg/test_p6_consolidate.py +440 -387
- package/md_cg/test_p7_goals_recent.py +202 -202
- package/md_cg/test_p8_subgraph_chain.py +200 -200
- package/md_cg/test_p9_forget_protect.py +231 -231
- package/md_cg/test_predict_beta.py +135 -135
- package/md_cg/test_preflight_failclosed.py +100 -100
- package/md_cg/test_progressive.py +146 -146
- package/md_cg/test_protocol.py +243 -243
- package/md_cg/test_reach.py +378 -378
- package/md_cg/test_reach_keys.py +201 -201
- package/md_cg/test_read_clip.py +141 -141
- package/md_cg/test_readcache_prodpath.py +155 -0
- package/md_cg/test_retr_gates_prodpath.py +140 -0
- package/md_cg/test_retr_s1.py +340 -340
- package/md_cg/test_retr_s1b.py +209 -209
- package/md_cg/test_retr_s3.py +194 -194
- package/md_cg/test_retr_s4.py +163 -163
- package/md_cg/test_retr_s5.py +200 -200
- package/md_cg/test_retr_s6.py +157 -157
- package/md_cg/test_retr_s7.py +384 -384
- package/md_cg/test_retr_s8_time.py +369 -316
- package/md_cg/test_retr_s9_edges.py +286 -286
- package/md_cg/test_retr_s9_entity_ctx.py +175 -175
- package/md_cg/test_review_conformance.py +367 -367
- package/md_cg/test_role_views.py +354 -354
- package/md_cg/test_sem_noise.py +242 -242
- package/md_cg/test_semantic_canonical.py +241 -241
- package/md_cg/test_subproc_encoding.py +192 -192
- package/md_cg/test_sustain_mutual.py +153 -153
- package/md_cg/test_tasks.py +409 -409
- package/md_cg/test_tool_face.py +189 -189
- package/md_cg/test_transfer.py +180 -180
- package/md_cg/test_trust.py +361 -361
- package/md_cg/test_twophase.py +286 -286
- package/md_cg/test_v14_fixes.py +397 -397
- package/md_cg/test_validity_filter.py +280 -280
- package/md_cg/test_verify_answer.py +138 -138
- package/md_cg/test_wisdom_md_store.py +292 -292
- package/md_cg/test_writelimit.py +197 -197
- package/md_cg/test_writepipe.py +214 -214
- package/md_cg/theory.py +273 -273
- package/md_cg/tokens.py +677 -663
- package/md_cg/tool_face.py +260 -260
- package/md_cg/trust.py +986 -950
- package/md_cg/twophase.py +231 -231
- package/md_cg/units.py +667 -667
- package/md_cg/vision_evidence.py +666 -666
- package/md_cg/weights.py +624 -624
- package/md_cg/whitebox.py +527 -527
- package/md_cg/whitebox_kb/__init__.py +37 -37
- package/md_cg/whitebox_kb/aeis_core/__init__.py +42 -42
- package/md_cg/whitebox_kb/aeis_core/semantic.py +280 -280
- package/md_cg/whitebox_kb/aeis_core/textutil.py +13 -13
- package/md_cg/whitebox_kb/engine.py +310 -310
- package/md_cg/whitebox_kb/seed_knowledge//346/231/272/350/203/275/350/256/2723.4.md +5260 -5260
- package/md_cg/whitebox_kb/wisdom/browser_units.py +2631 -2631
- package/md_cg/whitebox_kb/wisdom/causal_discover.py +432 -432
- package/md_cg/whitebox_kb/wisdom/chat_engine.py +1506 -1506
- package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
- package/md_cg/whitebox_kb/wisdom/compiler_code_units.py +3033 -3033
- package/md_cg/whitebox_kb/wisdom/condition_algebra.py +112 -112
- package/md_cg/whitebox_kb/wisdom/condition_frame.py +315 -315
- package/md_cg/whitebox_kb/wisdom/condition_kb.py +108 -108
- package/md_cg/whitebox_kb/wisdom/conflict_map.json +4445 -4445
- package/md_cg/whitebox_kb/wisdom/core/lexer.py +512 -512
- package/md_cg/whitebox_kb/wisdom/core/name_checker.py +1023 -1023
- package/md_cg/whitebox_kb/wisdom/cspmn.py +258 -258
- package/md_cg/whitebox_kb/wisdom/csre.py +264 -264
- package/md_cg/whitebox_kb/wisdom/danmaku_audit.py +252 -252
- package/md_cg/whitebox_kb/wisdom/distilled_condition_units.json +6417 -6417
- package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902b.json +1950 -1950
- package/md_cg/whitebox_kb/wisdom/docs/WB-EVAL-20260902c.json +1296 -1296
- package/md_cg/whitebox_kb/wisdom/docs/whitebox_capability_graph_demo.json +59 -59
- package/md_cg/whitebox_kb/wisdom/graph_db_units.py +3089 -3089
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +318 -318
- package/md_cg/whitebox_kb/wisdom/md_access.py +470 -470
- package/md_cg/whitebox_kb/wisdom/md_store.py +276 -251
- package/md_cg/whitebox_kb/wisdom/migrate_wisdom.py +476 -476
- package/md_cg/whitebox_kb/wisdom/navigate.py +248 -248
- package/md_cg/whitebox_kb/wisdom/neural_retrieve.py +224 -224
- package/md_cg/whitebox_kb/wisdom/os_units.py +2735 -2735
- package/md_cg/whitebox_kb/wisdom/pattern_separation.py +376 -376
- package/md_cg/whitebox_kb/wisdom/prereq_map.json +364 -364
- package/md_cg/whitebox_kb/wisdom/python_code_units.py +2766 -2766
- package/md_cg/whitebox_kb/wisdom/role_solidified.json +7 -7
- package/md_cg/whitebox_kb/wisdom/route_memory.py +244 -244
- package/md_cg/whitebox_kb/wisdom/scene_reconstruction.py +148 -148
- package/md_cg/whitebox_kb/wisdom/snr_report.json +40 -40
- package/md_cg/whitebox_kb/wisdom/test_code_compose_domains.py +7541 -7541
- package/md_cg/whitebox_kb/wisdom/test_compiler_self_bootstrap.py +354 -354
- package/md_cg/whitebox_kb/wisdom/test_ecosystem_assembly.py +158 -158
- package/md_cg/whitebox_kb/wisdom/test_ecosystem_demos.py +193 -193
- package/md_cg/whitebox_kb/wisdom/test_graph_db.py +292 -292
- package/md_cg/whitebox_kb/wisdom/test_python_self_bootstrap.py +160 -160
- package/md_cg/whitebox_kb/wisdom/trigger_words_index.json +4366 -4366
- package/md_cg/whitebox_kb/wisdom/verifier.py +1297 -1297
- package/md_cg/writelimit.py +356 -356
- package/md_cg/writepipe.py +550 -542
- package/package.json +97 -96
- package/skills/plugin.json +54 -54
- package/skills/skills/designer-perspective/SKILL.md +158 -158
- package/skills/skills/designer-perspective/references/01-observation-position.md +66 -66
- package/skills/skills/designer-perspective/references/02-structure-recognition.md +62 -62
- package/skills/skills/designer-perspective/references/03-direction-judgment.md +55 -55
- package/skills/skills/designer-perspective/references/04-qualification-verdict.md +72 -72
- package/skills/skills/designer-perspective/references/05-condition-attribution.md +74 -74
- package/skills/skills/designer-perspective/scripts/designer.py +545 -545
- package/skills/skills/designer-perspective/tests/cases.jsonl +17 -17
- package/skills/skills/designer-perspective/tests/selftest.py +61 -61
- package/skills/skills/lingshu-browser/SKILL.md +60 -60
- package/skills/skills/lingshu-compiler/SKILL.md +56 -56
- package/skills/skills/lingshu-compiler/units/analyze-type-infer/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/check-name-real/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-assign/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-expr-tree/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-full-pipeline/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-func-def/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-if-then/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-logic-expr/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-recursive/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-scope/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-type-check/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compile-while/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0010c4bf/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0355bffb/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0361708a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-054a0414/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-05a1691a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-05eeed1e/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0622a1f6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-08b54217/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0a62b70c/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0ab24d00/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0e093688/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0e82b966/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0ee9b9b9/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-0f3787e6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-1028685f/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-11897630/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-16661b9b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-1f722303/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-25be1262/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2a76ba07/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2dbea54a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2df52f16/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2ec2c9d2/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-2f8c8f39/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-38378ac7/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-39457d2e/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-39fb5926/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-47f4fbfa/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4a8cd1f1/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4b230b6b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4cbbda95/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-4d5a68ff/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-56dc9bdd/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-57e76ebe/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-61ff016b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-63eae588/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-64c4224b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-66377955/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-674ab4f6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6af76fd6/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6d979db2/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6de326c0/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-6f1c0eea/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-724d6c9b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-7690177f/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-7b229a7d/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-7f471b3a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-815cad08/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-83c61634/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-8c798c81/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-8dd747ac/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-90324d0a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-94b8d72d/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-94f12231/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-95937c16/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-98a5625b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-98b4c42f/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-98e3894a/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-9bdfe4b8/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-9d9b4e83/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-9f7be5ad/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-a6daf076/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-a7995a0c/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-a8399248/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-aabbd099/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-afe169d8/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b0678bda/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b09bd196/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b1396e23/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-b9b31ce0/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-c10264a7/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-cb1e8e4b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-ccafd438/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-cda9c262/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-ce648068/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-cf5776a4/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-d974e5d3/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-e3979fd3/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-eb1cf2b5/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-ecb30d5b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-f8c8b24b/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-f99fedbe/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-fa8ff5f7/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/compiler-fe1b058d/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/lex-chinese-program/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/lex-dao-de-jing/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/lex-nine-chapters/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-arithmetic/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-array-ops/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-closure-call/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-closure-create/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-compare/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-cond-jump/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-cond-space/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-exception/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-func-call/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-loop-run/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-profiling/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-refcount/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-run-loop/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-short-circuit/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-stack-guard/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-stack-ops/SKILL.md +45 -45
- package/skills/skills/lingshu-compiler/units/vm-trust-accum/SKILL.md +45 -45
- package/skills/skills/lingshu-graph/SKILL.md +63 -63
- package/skills/skills/lingshu-net/SKILL.md +48 -48
- package/skills/skills/lingshu-os/SKILL.md +64 -64
- package/skills/skills/lingshu-pylang/SKILL.md +71 -71
- package/src/bridge.ts +401 -401
- package/src/hooks.ts +38 -2
- package/src/lib/datapath.ts +326 -326
- package/src/lib/mdcg_client.ts +413 -413
- package/src/lib/mutual.ts +428 -428
- package/src/lib/prompt_safety.ts +62 -62
- package/src/lib/python_path.ts +71 -71
- package/src/lib/roleplay_web.ts +932 -932
- package/src/lib/token_store.ts +192 -192
- package/src/tools.ts +212 -212
- package/zcode/AGENTS.md +11 -3
- package/zcode/README.md +41 -41
- /package/docs/{mdcg → hive}//344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md" +0 -0
package/md_cg/provenance.py
CHANGED
|
@@ -1,583 +1,583 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""派生溯源(G8):新增节点常态化建链 + 悬空可检出。
|
|
3
|
-
|
|
4
|
-
回答「这个节点**从哪来**」——与 `md_cg.links` 刻意分层:
|
|
5
|
-
· `links.py` = 跨节点信任(「我信你多少」,P_trust,落 `~/.mdcg/_links.json`);
|
|
6
|
-
· 本模块 = 节点派生关系(「它由谁派生」,落 `<root>/_link.jsonl`)。
|
|
7
|
-
两者都叫「链接」,但一个管**信任状态**、一个管**演进血缘**,不可混用。
|
|
8
|
-
|
|
9
|
-
存储形态(对齐「md 单一真相源 + 派生索引可重建」):
|
|
10
|
-
· 权威声明在节点 frontmatter(`derived_from` / `derived_relation`);
|
|
11
|
-
· `<root>/_link.jsonl` 是 **append-only 派生台账**(快查用,可由 frontmatter 重建);
|
|
12
|
-
· 建链失败写 `<root>/_link.jsonl.fail`(降级留痕)。
|
|
13
|
-
|
|
14
|
-
三条纪律(对齐 G8 裁定 §六):
|
|
15
|
-
1. **只对新增节点常态化建链,历史不回填**——`rebuild_ledger` 只重放 frontmatter
|
|
16
|
-
里**已经声明**的关系,不为历史节点发明任何边(当前库历史声明为 0 → 重建为空);
|
|
17
|
-
2. **建链失败不得阻断写入**——`record()` 永不抛(best-effort),失败降级为告警 +
|
|
18
|
-
失败台账留痕,节点写入照常提交;
|
|
19
|
-
3. **巡检只读**——`check()` 检出悬空边(目标/子节点不在索引内)但**不自动删边**,
|
|
20
|
-
关系事实去留由人处置。
|
|
21
|
-
|
|
22
|
-
零第三方依赖。
|
|
23
|
-
"""
|
|
24
|
-
from __future__ import annotations
|
|
25
|
-
|
|
26
|
-
import json
|
|
27
|
-
import os
|
|
28
|
-
import time
|
|
29
|
-
|
|
30
|
-
from . import trust as _trust
|
|
31
|
-
from .fsutil import FileLock, append_jsonl, atomic_write, read_jsonl
|
|
32
|
-
|
|
33
|
-
LEDGER_NAME = "_link.jsonl"
|
|
34
|
-
FAIL_SUFFIX = ".fail"
|
|
35
|
-
LEDGER_ENV = "MDCG_LINK_FILE"
|
|
36
|
-
SCHEMA = 1
|
|
37
|
-
|
|
38
|
-
#: 允许的派生关系(显式枚举,避免「自由字符串」把血缘写成噪声)
|
|
39
|
-
RELATIONS = ("derived_from", "split_from", "extracted_from",
|
|
40
|
-
"merged_from", "refined_from", "source")
|
|
41
|
-
DEFAULT_RELATION = "derived_from"
|
|
42
|
-
|
|
43
|
-
#: frontmatter 里承载派生声明的字段(写路径只读这两处,不猜)
|
|
44
|
-
FM_FIELD = "derived_from"
|
|
45
|
-
FM_REL_FIELD = "derived_relation"
|
|
46
|
-
|
|
47
|
-
_LOCK_TIMEOUT = 2.0
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class ProvenanceError(Exception):
|
|
51
|
-
"""派生溯源错误。写路径侧一律由 `record()` 兜住,不向上抛。"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# --------------------------------------------------------------------------
|
|
55
|
-
# 路径 / 规范化
|
|
56
|
-
# --------------------------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
# 生效条件:path 为真值时返回 path;否则 os.environ.get(LEDGER_ENV) 为非空真值时返回该环境变量值;否则返回 os.path.join(root, LEDGER_NAME)。
|
|
59
|
-
def ledger_file(root: str, path: str = None) -> str:
|
|
60
|
-
"""台账路径:显式 → MDCG_LINK_FILE → <root>/_link.jsonl。"""
|
|
61
|
-
return path or os.environ.get(LEDGER_ENV) or os.path.join(root, LEDGER_NAME)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# 生效条件:传入 root(path 为真值则以其为准,否则由 ledger_file 的回落决定落点)时返回 ledger_file(root, path) 结果拼接 FAIL_SUFFIX。
|
|
65
|
-
def fail_log_file(root: str, path: str = None) -> str:
|
|
66
|
-
"""降级留痕路径(台账写不进时的「本该建的边」)。"""
|
|
67
|
-
return ledger_file(root, path) + FAIL_SUFFIX
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# 生效条件:value 为 None 返回 [];否则 list/tuple/set 逐个元素、其他类型视作单元素,元素经 str(x).strip() 后非空且未出现过才保留(重复只留首次)。
|
|
71
|
-
def as_list(value) -> list:
|
|
72
|
-
"""把单值 / 序列统一成去重、去空白的字符串列表。"""
|
|
73
|
-
if value is None:
|
|
74
|
-
return []
|
|
75
|
-
items = list(value) if isinstance(value, (list, tuple, set)) else [value]
|
|
76
|
-
out = []
|
|
77
|
-
for x in items:
|
|
78
|
-
s = str(x).strip()
|
|
79
|
-
if s and s not in out:
|
|
80
|
-
out.append(s)
|
|
81
|
-
return out
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# 生效条件:rel 为假值(None/空串)或 str(rel).strip().lower() 后为空白的串(如 " ")时 r 回落 default(默认常量 DEFAULT_RELATION);r 不在 RELATIONS 内(含回落后的 default 本身非法)即抛 ProvenanceError,否则返回该小写串。
|
|
85
|
-
def normalize_relation(rel, default: str = DEFAULT_RELATION) -> str:
|
|
86
|
-
"""严格校验关系名;非法抛 `ProvenanceError`(显式 API 用)。"""
|
|
87
|
-
r = str(rel or "").strip().lower()
|
|
88
|
-
if not r:
|
|
89
|
-
r = default
|
|
90
|
-
if r not in RELATIONS:
|
|
91
|
-
raise ProvenanceError(f"未知派生关系:{rel}(允许:{RELATIONS})")
|
|
92
|
-
return r
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# 生效条件:normalize_relation(rel, default) 抛 ProvenanceError(含 rel 为假值/仅空白且 default 非法时)则原样返回 default;否则返回该调用的返回值。
|
|
96
|
-
def coerce_relation(rel, default: str = DEFAULT_RELATION) -> str:
|
|
97
|
-
"""宽松兜底:非法关系名回退默认值(**写路径用,保证永不阻断写入**)。"""
|
|
98
|
-
try:
|
|
99
|
-
return normalize_relation(rel, default)
|
|
100
|
-
except ProvenanceError:
|
|
101
|
-
return default
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# 生效条件:child 或 parent 为假值或仅空白使 str(x or "").strip() 为空、或二者 strip 后相等(自环)时返回 None;否则返回含 schema/t/child/parent/rel/batch/actor 的 dict,note 为真值时才附上并截断到 200 字符。
|
|
105
|
-
def make_edge(child, parent, *, relation=DEFAULT_RELATION, batch=None,
|
|
106
|
-
actor="system", note=None, t=None):
|
|
107
|
-
"""构造一条派生边;自环 / 空端点返回 None(**不产生无意义边**)。"""
|
|
108
|
-
c, p = str(child or "").strip(), str(parent or "").strip()
|
|
109
|
-
if not c or not p or c == p:
|
|
110
|
-
return None
|
|
111
|
-
edge = {"schema": SCHEMA,
|
|
112
|
-
"t": float(t if t is not None else time.time()),
|
|
113
|
-
"child": c, "parent": p, "rel": coerce_relation(relation),
|
|
114
|
-
"batch": batch, "actor": actor}
|
|
115
|
-
if note:
|
|
116
|
-
edge["note"] = str(note)[:200]
|
|
117
|
-
return edge
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# 生效条件:对 as_list(parents) 的每个父项调用 make_edge,只保留返回非 None 的边,全部被丢弃时返回空列表。
|
|
121
|
-
def edges_for(child, parents, **kw) -> list:
|
|
122
|
-
"""`(child, [parents]) → [edge]`:空端点 / 自环自动丢弃。"""
|
|
123
|
-
out = []
|
|
124
|
-
for p in as_list(parents):
|
|
125
|
-
e = make_edge(child, p, **kw)
|
|
126
|
-
if e:
|
|
127
|
-
out.append(e)
|
|
128
|
-
return out
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# --------------------------------------------------------------------------
|
|
132
|
-
# 写:台账追加(record 为写路径唯一入口,永不抛)
|
|
133
|
-
# --------------------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
# 生效条件:edges 为 None/空/全为假元素时返回 0;否则在 FileLock(p, timeout=_LOCK_TIMEOUT) 内逐条 append_jsonl,抛 OSError 时转抛 ProvenanceError,成功返回 len(edges)。
|
|
136
|
-
def append(root: str, edges, *, path: str = None) -> int:
|
|
137
|
-
"""台账追加(加锁串行,防 Windows 并发交错丢边)。IO 失败抛 `ProvenanceError`。"""
|
|
138
|
-
edges = [e for e in (edges or []) if e]
|
|
139
|
-
if not edges:
|
|
140
|
-
return 0
|
|
141
|
-
p = ledger_file(root, path)
|
|
142
|
-
try:
|
|
143
|
-
os.makedirs(os.path.dirname(os.path.abspath(p)) or ".", exist_ok=True)
|
|
144
|
-
with FileLock(p, timeout=_LOCK_TIMEOUT):
|
|
145
|
-
for e in edges:
|
|
146
|
-
append_jsonl(p, e)
|
|
147
|
-
except OSError as exc:
|
|
148
|
-
raise ProvenanceError(f"派生台账写入失败:{p}({exc})") from exc
|
|
149
|
-
return len(edges)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# 生效条件:给定 root/path/child/parents/relation/reason/code 即恒返回 {'ok': False, 'added': 0, 'edges': [], 'degraded': True, 'degrade_code': code, 'reason': reason},其中写 fail_log_file 台账的异常被 except Exception 吞掉。
|
|
153
|
-
def _degrade(root: str, path, child, parents, relation, reason,
|
|
154
|
-
code: str) -> dict:
|
|
155
|
-
"""降级留痕:把「本该建的边」记进 .fail 台账(自身也 best-effort)。"""
|
|
156
|
-
rec = {"t": time.time(), "code": code, "child": str(child or ""),
|
|
157
|
-
"parents": as_list(parents), "rel": str(relation or ""),
|
|
158
|
-
"reason": str(reason)[:300]}
|
|
159
|
-
try:
|
|
160
|
-
append_jsonl(fail_log_file(root, path), rec)
|
|
161
|
-
except Exception: # noqa: BLE001
|
|
162
|
-
pass
|
|
163
|
-
return {"ok": False, "added": 0, "edges": [], "degraded": True,
|
|
164
|
-
"degrade_code": code, "reason": reason}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
# 生效条件:edges_for 抛异常时经 _degrade(code='bad_edge') 返回;edges 为空时返回 {'ok': True, 'added': 0, 'edges': [], 'reason': 'no_parents'};append 抛 ProvenanceError 或其他异常时经 _degrade(code='ledger_io') 返回;其余返回 {'ok': True, 'added': n, 'edges': edges, 'ledger': …},恒不向外抛。
|
|
168
|
-
def record(root: str, child, parents, *, relation=DEFAULT_RELATION,
|
|
169
|
-
batch=None, actor="system", note=None, path: str = None) -> dict:
|
|
170
|
-
"""写路径建链入口:**永不抛**(G8 硬约束:建链失败不得阻断节点写入)。
|
|
171
|
-
|
|
172
|
-
返回 `{ok, added, edges, ...}`;失败时 `ok=False` + `degraded=True` 且已写
|
|
173
|
-
`.fail` 留痕。调用方**不得**因本函数返回 False 而回滚节点。
|
|
174
|
-
"""
|
|
175
|
-
try:
|
|
176
|
-
edges = edges_for(child, parents, relation=relation, batch=batch,
|
|
177
|
-
actor=actor, note=note)
|
|
178
|
-
except Exception as exc: # noqa: BLE001
|
|
179
|
-
return _degrade(root, path, child, parents, relation,
|
|
180
|
-
f"{type(exc).__name__}: {exc}", "bad_edge")
|
|
181
|
-
if not edges:
|
|
182
|
-
return {"ok": True, "added": 0, "edges": [], "reason": "no_parents"}
|
|
183
|
-
try:
|
|
184
|
-
n = append(root, edges, path=path)
|
|
185
|
-
except ProvenanceError as exc:
|
|
186
|
-
return _degrade(root, path, child, parents, relation, str(exc),
|
|
187
|
-
"ledger_io")
|
|
188
|
-
except Exception as exc: # noqa: BLE001
|
|
189
|
-
return _degrade(root, path, child, parents, relation,
|
|
190
|
-
f"{type(exc).__name__}: {exc}", "ledger_io")
|
|
191
|
-
return {"ok": True, "added": n, "edges": edges,
|
|
192
|
-
"ledger": ledger_file(root, path)}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
# --------------------------------------------------------------------------
|
|
196
|
-
# 读:台账 / 索引 / 悬空巡检
|
|
197
|
-
# --------------------------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
# 生效条件:遍历 read_jsonl(ledger_file(root, path)),仅收录 isinstance(r, dict) 且 r.get("child") 与 r.get("parent") 均为真值(键缺失或值为假即丢弃)的记录。
|
|
200
|
-
def load(root: str, *, path: str = None) -> list:
|
|
201
|
-
"""读台账(跳过坏行;只取有端点的记录)。"""
|
|
202
|
-
out = []
|
|
203
|
-
for r in read_jsonl(ledger_file(root, path)):
|
|
204
|
-
if isinstance(r, dict) and r.get("child") and r.get("parent"):
|
|
205
|
-
out.append(r)
|
|
206
|
-
return out
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# 生效条件:rows 中每行按 (r.get("child"), r.get("parent"), r.get("rel")) 三元组判重,仅首次出现的行保留,按原顺序返回去重列表。
|
|
210
|
-
def _dedupe(rows):
|
|
211
|
-
out, seen = [], set()
|
|
212
|
-
for r in rows:
|
|
213
|
-
key = (r.get("child"), r.get("parent"), r.get("rel"))
|
|
214
|
-
if key in seen:
|
|
215
|
-
continue
|
|
216
|
-
seen.add(key)
|
|
217
|
-
out.append(r)
|
|
218
|
-
return out
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
# 生效条件:child/parent/relation/batch 各为真值时才做对应等值过滤(假值或 None 不过滤),去重后 limit 为真值时返回 out[:int(limit)],limit 为 None 或 0 时返回全部 out。
|
|
222
|
-
def edges(root: str, *, child=None, parent=None, relation=None, batch=None,
|
|
223
|
-
limit: int = None, path: str = None) -> list:
|
|
224
|
-
"""按端点 / 关系 / 批次过滤台账边(只读,去重,保持写入顺序)。"""
|
|
225
|
-
out = []
|
|
226
|
-
for r in load(root, path=path):
|
|
227
|
-
if child and r.get("child") != child:
|
|
228
|
-
continue
|
|
229
|
-
if parent and r.get("parent") != parent:
|
|
230
|
-
continue
|
|
231
|
-
if relation and r.get("rel") != relation:
|
|
232
|
-
continue
|
|
233
|
-
if batch and r.get("batch") != batch:
|
|
234
|
-
continue
|
|
235
|
-
out.append(r)
|
|
236
|
-
out = _dedupe(out)
|
|
237
|
-
return out[:int(limit)] if limit else out
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
# 生效条件:以 (getattr(cg, "index", None) or {}).get("nodes") or {} 遍历(缺失时视作空);prefix 为真值时仅保留 str(nid).startswith(prefix) 的节点,输出经 _dedupe 去重。
|
|
241
|
-
def index_edges(cg, *, prefix: str = None) -> list:
|
|
242
|
-
"""从**索引快照**恢复派生边(零读文件)——台账丢失/未重建时的只读兜底。"""
|
|
243
|
-
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
244
|
-
out = []
|
|
245
|
-
for nid, e in nodes.items():
|
|
246
|
-
if prefix and not str(nid).startswith(prefix):
|
|
247
|
-
continue
|
|
248
|
-
rel = coerce_relation((e or {}).get(FM_REL_FIELD))
|
|
249
|
-
for p in as_list((e or {}).get(FM_FIELD)):
|
|
250
|
-
out.append({"schema": SCHEMA, "child": nid, "parent": p, "rel": rel,
|
|
251
|
-
"batch": (e or {}).get("derived_batch"), "actor": None,
|
|
252
|
-
"origin": "index"})
|
|
253
|
-
return _dedupe(out)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
# 生效条件:给定 cg 即返回 _dedupe(load(cg.root, path=path) + index_edges(cg)),台账记录在前、按边去重。
|
|
257
|
-
def all_edges(cg, *, path: str = None) -> list:
|
|
258
|
-
"""台账 ∪ 索引声明(台账优先,按边去重)。"""
|
|
259
|
-
return _dedupe(load(cg.root, path=path) + index_edges(cg))
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# 生效条件:include_index 为真值时取 all_edges(cg, path=path)、为假时取 load(cg.root, path=path),对 child/parent 不在 cg 索引节点键集合中的边记为 dangling 并列出 missing;返回 ok=not dangling,dangling 仅取 int(limit)(默认 20)项,不写盘。
|
|
263
|
-
def check(cg, *, path: str = None, limit: int = 20,
|
|
264
|
-
include_index: bool = True) -> dict:
|
|
265
|
-
"""只读巡检:检出**悬空派生边**(端点不在索引内)。**不删边、不写盘**。
|
|
266
|
-
|
|
267
|
-
`ok=False` 仅表示「有悬空」,不代表巡检失败;`checked=True` 恒成立。
|
|
268
|
-
"""
|
|
269
|
-
known = set((getattr(cg, "index", None) or {}).get("nodes") or {})
|
|
270
|
-
rows = all_edges(cg, path=path) if include_index else load(cg.root, path=path)
|
|
271
|
-
dangling = []
|
|
272
|
-
for e in rows:
|
|
273
|
-
missing = []
|
|
274
|
-
if e.get("child") not in known:
|
|
275
|
-
missing.append("child")
|
|
276
|
-
if e.get("parent") not in known:
|
|
277
|
-
missing.append("parent")
|
|
278
|
-
if missing:
|
|
279
|
-
dangling.append({"child": e.get("child"), "parent": e.get("parent"),
|
|
280
|
-
"rel": e.get("rel"), "missing": missing,
|
|
281
|
-
"batch": e.get("batch"), "t": e.get("t"),
|
|
282
|
-
"origin": e.get("origin") or "ledger"})
|
|
283
|
-
dangling.sort(key=lambda r: (r.get("child") or "", r.get("parent") or ""))
|
|
284
|
-
ledger = ledger_file(cg.root, path)
|
|
285
|
-
return {"ok": not dangling, "checked": True, "root": cg.root,
|
|
286
|
-
"ledger": ledger, "ledger_exists": os.path.exists(ledger),
|
|
287
|
-
"edges": len(rows), "nodes": len(known),
|
|
288
|
-
"dangling": dangling[:int(limit)], "dangling_count": len(dangling),
|
|
289
|
-
"readonly": True,
|
|
290
|
-
"note": "只读巡检:悬空边仅检出并报告,不自动删除(关系事实由人处置)"}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
# 生效条件:apply 为假值(默认 False)时只返回 dry_run 报表(written=0、sample=es[:5]);apply 为真值时把 index_edges(cg) 的边写入 ledger_file(cg.root, path) 并返回 written=len(es)。
|
|
294
|
-
def rebuild_ledger(cg, *, apply: bool = False, path: str = None) -> dict:
|
|
295
|
-
"""按 frontmatter 重建台账——**只重放已声明的边,不发明任何边**。
|
|
296
|
-
|
|
297
|
-
历史节点未声明派生关系 → 重建结果为空,正合「历史不回填」。
|
|
298
|
-
默认 dry_run(只出报表)。
|
|
299
|
-
"""
|
|
300
|
-
es = index_edges(cg)
|
|
301
|
-
if not apply:
|
|
302
|
-
return {"ok": True, "dry_run": True, "edges": len(es),
|
|
303
|
-
"written": 0, "sample": es[:5],
|
|
304
|
-
"note": "预演:未写盘;只重放 frontmatter 已声明的关系"}
|
|
305
|
-
body = "".join(json.dumps(e, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
306
|
-
for e in es)
|
|
307
|
-
p = ledger_file(cg.root, path)
|
|
308
|
-
atomic_write(p, body)
|
|
309
|
-
return {"ok": True, "dry_run": False, "edges": len(es),
|
|
310
|
-
"written": len(es), "ledger": p}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
# 生效条件:check(cg, path=path, limit=3) 成功时返回 edges/dangling/ledger/exists/sample(悬空边 child->parent);该调用抛任何异常时被 except Exception 吞掉并返回 {}。
|
|
314
|
-
def summary(cg, *, path: str = None) -> dict:
|
|
315
|
-
"""轻量摘要(只读;失败不抛,避免拖垮 health_os / 常驻循环)。"""
|
|
316
|
-
try:
|
|
317
|
-
rep = check(cg, path=path, limit=3)
|
|
318
|
-
return {"edges": rep["edges"], "dangling": rep["dangling_count"],
|
|
319
|
-
"ledger": rep["ledger"], "exists": rep["ledger_exists"],
|
|
320
|
-
"sample": [f"{r['child']}->{r['parent']}"
|
|
321
|
-
for r in rep["dangling"]]}
|
|
322
|
-
except Exception: # noqa: BLE001
|
|
323
|
-
return {}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
# 生效条件:root 为真值时 ledger 字段取 ledger_file(root),否则取常量 LEDGER_NAME;其余自描述字段(SCHEMA、RELATIONS、DEFAULT_RELATION、FM_FIELD/FM_REL_FIELD 等)恒定返回。
|
|
327
|
-
def catalog(root: str = None) -> dict:
|
|
328
|
-
"""自描述(供 MCP catalog / 人工核对)。"""
|
|
329
|
-
return {
|
|
330
|
-
"layer": "派生溯源(G8)",
|
|
331
|
-
"question": "这个节点从哪来(演进血缘)",
|
|
332
|
-
"ledger": ledger_file(root) if root else LEDGER_NAME,
|
|
333
|
-
"schema": SCHEMA,
|
|
334
|
-
"relations": list(RELATIONS),
|
|
335
|
-
"default_relation": DEFAULT_RELATION,
|
|
336
|
-
"fm_fields": [FM_FIELD, FM_REL_FIELD],
|
|
337
|
-
"discipline": {"incremental_only": True, "no_backfill": True,
|
|
338
|
-
"never_block_write": True, "patrol_readonly": True},
|
|
339
|
-
"distinct_from": ("links.py = 跨节点信任 P_trust(_links.json);"
|
|
340
|
-
"本层 = 节点派生关系(_link.jsonl)"),
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
# --------------------------------------------------------------------------
|
|
345
|
-
# 读:三元组反查原语(阶段二 4.2 · find_entity_contexts 式)
|
|
346
|
-
# --------------------------------------------------------------------------
|
|
347
|
-
# 术语映射(**同一件事,勿新造第二套字段**):三元组 `subject / predicate /
|
|
348
|
-
# object` 在本层就是派生边的 `child / rel / parent`——即节点写入时声明的
|
|
349
|
-
# `derived_from` 关系。不另建 subject/predicate/object 参数字面量,理由有二:
|
|
350
|
-
# ① `cg` 工具面是**扁平 schema**,`subject` 已被 `identity`(`subject:<id>`
|
|
351
|
-
# 主体语义)与 `link.evidence`(证据主体)占用,同名异义会把两处口径搅在一起;
|
|
352
|
-
# ② `edges()` 已是唯一谓词载体(child/parent/relation/batch 四键),反查只是它的
|
|
353
|
-
# **只读超集**——另造一套参数必然分叉。
|
|
354
|
-
# 与 `edges()` 的三处**有意**差异(不是漂移):
|
|
355
|
-
# · 时间轴缺省 `observed`(边只有记录时刻 `t`,见 FIND_DEFAULT_AXIS);
|
|
356
|
-
# · 默认排序 `desc`(按 `t` 新→旧)且 `limit` 缺省 50、上限 500(分页原语,
|
|
357
|
-
# 不给「静默全量倾倒」);
|
|
358
|
-
# · 加 `aggregation`(分页前全集分桶)与 `expand_nodes`(端点摘要,索引级零读盘)。
|
|
359
|
-
|
|
360
|
-
#: 排序方向(封闭枚举,拒收未知名——与 `trust.TIME_OPERATORS` 同风格)
|
|
361
|
-
ORDERINGS = ("desc", "asc")
|
|
362
|
-
#: 聚合维度(封闭枚举):按谓词 / 对象端 / 主体端分桶
|
|
363
|
-
AGGREGATIONS = ("by_relation", "by_parent", "by_child")
|
|
364
|
-
#: 聚合维度 → 边字段(谓词在边上叫 `rel`;聚合名沿用三元组术语命名)
|
|
365
|
-
_AGG_FIELD = {"by_relation": "rel", "by_parent": "parent", "by_child": "child"}
|
|
366
|
-
#: 反查缺省时间轴:派生边只有一个时刻字段 `t`(写入时刻,观察轴),
|
|
367
|
-
#: **效力轴字段根本不存在**。与 `mdcg.search` 缺省 `effective` **有意不同**:
|
|
368
|
-
#: 那里 `effective_from/until` 是可选声明(多数节点没写),沿用 fail-open 不会
|
|
369
|
-
#: 出错;这里若缺省 `effective`,则「给了时间条件却恒不过滤」——把静默 no-op
|
|
370
|
-
#: 当成了「没有匹配」,属无法复算的错答。
|
|
371
|
-
FIND_DEFAULT_AXIS = "observed"
|
|
372
|
-
DEFAULT_FIND_LIMIT = 50
|
|
373
|
-
MAX_FIND_LIMIT = 500
|
|
374
|
-
#: `expand_nodes=True` 时透出的索引字段白名单(只读索引快照,**零读节点文件**)
|
|
375
|
-
EXPAND_FIELDS = ("layer", "tags", "importance", "writer", "session",
|
|
376
|
-
"derived_from", "derived_relation", "derived_batch",
|
|
377
|
-
"temporal", "time_window", "condition_space")
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
# 生效条件:value 为 None 或 str(value).strip() 为空时返回 "desc";小写后命中 ORDERINGS 返回该值;否则抛 ProvenanceError。
|
|
381
|
-
def _ordering_of(value) -> str:
|
|
382
|
-
"""排序方向归一 → `"desc"` / `"asc"`;未知 → `ProvenanceError`(fail-closed)。"""
|
|
383
|
-
if value is None or not str(value).strip():
|
|
384
|
-
return "desc"
|
|
385
|
-
v = str(value).strip().lower()
|
|
386
|
-
if v not in ORDERINGS:
|
|
387
|
-
raise ProvenanceError(f"未知 ordering {value!r}(允许:{ORDERINGS})")
|
|
388
|
-
return v
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
# 生效条件:value 为 None 或 str(value).strip() 为空时返回 None(= 不聚合);小写后命中 AGGREGATIONS 返回该值;否则抛 ProvenanceError。
|
|
392
|
-
def _aggregation_of(value):
|
|
393
|
-
"""聚合维度归一 → `None` / `AGGREGATIONS` 之一;未知 → `ProvenanceError`。"""
|
|
394
|
-
if value is None or not str(value).strip():
|
|
395
|
-
return None
|
|
396
|
-
v = str(value).strip().lower()
|
|
397
|
-
if v not in AGGREGATIONS:
|
|
398
|
-
raise ProvenanceError(f"未知 aggregation {value!r}(允许:{AGGREGATIONS})")
|
|
399
|
-
return v
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
# 生效条件:axis 为 "observed" 且 edge.get("t") 可经 trust.parse_time 解析时返回 (t, t, False);axis 非 observed 或 t 不可解析/缺失时返回 (None, None, True)。
|
|
403
|
-
def _edge_window(edge, axis: str):
|
|
404
|
-
"""边的轴窗口 → `(start, end, missing)`(与 `trust.time_window_of` **同形**)。
|
|
405
|
-
|
|
406
|
-
observed 轴:`t`(写入时刻)→ `(t, t, False)`;`t` 缺失(`index_edges` 兜底边
|
|
407
|
-
不带时间)→ `(None, None, True)`。其余轴一律 `(None, None, True)`——边没有效力轴
|
|
408
|
-
声明可读,如实报「不可判定」,由**轴策略**处置(observed fail-closed 剔除并计入
|
|
409
|
-
`axis_missing`;effective fail-open 保留),不在这里悄悄换轴。
|
|
410
|
-
"""
|
|
411
|
-
if str(axis) == "observed":
|
|
412
|
-
t = _trust.parse_time((edge or {}).get("t"))
|
|
413
|
-
if t is None:
|
|
414
|
-
return None, None, True
|
|
415
|
-
return t, t, False
|
|
416
|
-
return None, None, True
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
# 生效条件:start_operator 与 end_operator 均为 None 时返回 "overlap",否则返回 "endpoint"。
|
|
420
|
-
def _mode_of(start_operator, end_operator) -> str:
|
|
421
|
-
"""时间过滤模式(与 `trust.window_match` 的显式分叉口径同源,不另立判据)。"""
|
|
422
|
-
return "endpoint" if (start_operator is not None or end_operator is not None) \
|
|
423
|
-
else "overlap"
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
# 生效条件:nid 不在 (cg.index or {}).get("nodes") or {} 的 dict 条目中(含 cg.index 缺失、条目非 dict)时返回 {'id': nid, 'present': False};否则返回 {'id','present':True} 并附 EXPAND_FIELDS 中值非 None 的字段。
|
|
427
|
-
def _node_digest(cg, nid) -> dict:
|
|
428
|
-
"""端点摘要(只读索引快照,**零读节点文件**);端点缺失 → `present=False`。"""
|
|
429
|
-
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
430
|
-
e = nodes.get(nid)
|
|
431
|
-
if not isinstance(e, dict):
|
|
432
|
-
return {"id": nid, "present": False}
|
|
433
|
-
d = {"id": nid, "present": True}
|
|
434
|
-
for k in EXPAND_FIELDS:
|
|
435
|
-
if e.get(k) is not None:
|
|
436
|
-
d[k] = e[k]
|
|
437
|
-
return d
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
# 生效条件:child/parent/relation/batch 各为真值(str(x).strip() 非空)时归一为过滤条件,relation 经 normalize_relation 非法即抛 ProvenanceError;ordering/aggregation 经 _ordering_of/_aggregation_of 非法即抛;offset 为负或 limit<=0 或 limit>MAX_FIND_LIMIT 即抛;时间五参经 trust.check_time_args 校验,why 非空即抛 ProvenanceError;返回 {'ok': True, 'readonly': True, 'op': 'edges', 'triple', 'total', 'matched', 'returned', 'offset', 'limit', 'ordering', 'aggregation', 'edges', 'aggregates', 'nodes_expanded', 'time_filter', 'ledger', 'distinct_from', 'note'},其中 total=谓词过滤后条数、matched=时间过滤后条数(dropped+matched==total)、edges=按 t 排序后 offset:offset+limit 切片(expand_nodes 时每边附 child_node/parent_node 摘要)、aggregates 为分页前全集分桶(未请求为 None)。
|
|
441
|
-
def find_edges(cg, *, child=None, parent=None, relation=None, batch=None,
|
|
442
|
-
start_time=None, end_time=None, start_operator=None,
|
|
443
|
-
end_operator=None, time_axis=None, ordering=None, offset=0,
|
|
444
|
-
limit=None, aggregation=None, expand_nodes: bool = False,
|
|
445
|
-
path: str = None) -> dict:
|
|
446
|
-
"""三元组反查(阶段二 4.2):按**任意端 / 谓词 / 时间**反查派生边(只读)。
|
|
447
|
-
|
|
448
|
-
`subject/predicate/object` ≡ `child/rel/parent`(见本节术语映射注释)。
|
|
449
|
-
|
|
450
|
-
谓词(`child` / `parent` / `relation` / `batch`)与 `edges()` **同源同义**:
|
|
451
|
-
给了就等值过滤、不给就不过滤。时间条件走 `trust.check_time_args`
|
|
452
|
-
(**与检索共用的唯一校验点**,本层不另写一套),比较语义由
|
|
453
|
-
`trust.window_match` 提供(不给 operator = 区间重叠;给 operator = 端点比较)。
|
|
454
|
-
|
|
455
|
-
fail-closed 清单(宁可报错,不静默降级):
|
|
456
|
-
· `relation` 非 `RELATIONS`(经 `normalize_relation`);
|
|
457
|
-
· `ordering` / `aggregation` 非各自枚举;
|
|
458
|
-
· `offset < 0`;`limit <= 0` 或 `> MAX_FIND_LIMIT`(**不把 0/负数当「全部」**);
|
|
459
|
-
· 时间五参非法(未知轴 / 未知算子 / 给了算子缺时间 / start > end)。
|
|
460
|
-
|
|
461
|
-
分页与聚合的次序是刻意的:**聚合基于分页前全集**(`matched`),
|
|
462
|
-
否则「先切页再聚合」会给出随 offset 漂移的分桶——不可复算。
|
|
463
|
-
审计块 `time_filter.dropped + matched == total` 由本函数保证。
|
|
464
|
-
"""
|
|
465
|
-
# ---- 谓词归一(空/空白 = 不约束,与 edges() 同口径) ------------------
|
|
466
|
-
c_f = str(child).strip() if child is not None and str(child).strip() else None
|
|
467
|
-
p_f = str(parent).strip() if parent is not None and str(parent).strip() else None
|
|
468
|
-
b_f = str(batch).strip() if batch is not None and str(batch).strip() else None
|
|
469
|
-
r_f = normalize_relation(relation) if (relation is not None
|
|
470
|
-
and str(relation).strip()) else None
|
|
471
|
-
# ---- 排序 / 分页 / 聚合 入参校验 --------------------------------------
|
|
472
|
-
ord_v = _ordering_of(ordering)
|
|
473
|
-
agg_v = _aggregation_of(aggregation)
|
|
474
|
-
try:
|
|
475
|
-
off = int(offset or 0)
|
|
476
|
-
except (TypeError, ValueError) as exc:
|
|
477
|
-
raise ProvenanceError(f"offset 非法:{offset!r}") from exc
|
|
478
|
-
if off < 0:
|
|
479
|
-
raise ProvenanceError(f"offset 不能为负:{off}")
|
|
480
|
-
lim = DEFAULT_FIND_LIMIT if limit is None else int(limit)
|
|
481
|
-
if lim <= 0:
|
|
482
|
-
raise ProvenanceError(f"limit 必须为正整数(0/负数不当「全部」):{limit!r}")
|
|
483
|
-
if lim > MAX_FIND_LIMIT:
|
|
484
|
-
raise ProvenanceError(f"limit 超上限 {MAX_FIND_LIMIT}:{lim}")
|
|
485
|
-
# ---- 时间算子:复用唯一校验点,缺省轴按本层语义补 observed -----------
|
|
486
|
-
enabled, axis0, why = _trust.check_time_args(
|
|
487
|
-
start_time=start_time, end_time=end_time,
|
|
488
|
-
start_operator=start_operator, end_operator=end_operator,
|
|
489
|
-
time_axis=time_axis)
|
|
490
|
-
if why:
|
|
491
|
-
raise ProvenanceError(why)
|
|
492
|
-
axis = FIND_DEFAULT_AXIS if time_axis is None else axis0
|
|
493
|
-
q_s = _trust.parse_time(start_time) if enabled else None
|
|
494
|
-
q_e = _trust.parse_time(end_time) if enabled else None
|
|
495
|
-
|
|
496
|
-
rows = all_edges(cg, path=path)
|
|
497
|
-
cand = []
|
|
498
|
-
for e in rows:
|
|
499
|
-
if c_f and e.get("child") != c_f:
|
|
500
|
-
continue
|
|
501
|
-
if p_f and e.get("parent") != p_f:
|
|
502
|
-
continue
|
|
503
|
-
if r_f and e.get("rel") != r_f:
|
|
504
|
-
continue
|
|
505
|
-
if b_f and e.get("batch") != b_f:
|
|
506
|
-
continue
|
|
507
|
-
cand.append(e)
|
|
508
|
-
total = len(cand)
|
|
509
|
-
|
|
510
|
-
# ---- 时间过滤(候选层:与 trust.filter_by_time 同策略) ---------------
|
|
511
|
-
dropped = missing = 0
|
|
512
|
-
kept = []
|
|
513
|
-
for e in cand:
|
|
514
|
-
if not enabled:
|
|
515
|
-
kept.append(e)
|
|
516
|
-
continue
|
|
517
|
-
cs, ce, miss = _edge_window(e, axis)
|
|
518
|
-
if miss:
|
|
519
|
-
if axis == "observed": # 观察轴 fail-closed
|
|
520
|
-
dropped += 1
|
|
521
|
-
missing += 1
|
|
522
|
-
continue
|
|
523
|
-
kept.append(e) # 效力轴 fail-open(无效力声明可读)
|
|
524
|
-
continue
|
|
525
|
-
if _trust.window_match(cs, ce, q_s, q_e, start_operator, end_operator):
|
|
526
|
-
kept.append(e)
|
|
527
|
-
else:
|
|
528
|
-
dropped += 1
|
|
529
|
-
if dropped + len(kept) != total: # 审计不变式(可复算)
|
|
530
|
-
raise ProvenanceError(
|
|
531
|
-
f"审计不变式破缺:dropped({dropped}) + kept({len(kept)}) != total({total})")
|
|
532
|
-
|
|
533
|
-
# ---- 排序(t 缺失按 0 计,确定性次级键防抖) --------------------------
|
|
534
|
-
kept.sort(key=lambda e: (float(e.get("t") or 0.0),
|
|
535
|
-
str(e.get("child") or ""),
|
|
536
|
-
str(e.get("parent") or "")),
|
|
537
|
-
reverse=(ord_v == "desc"))
|
|
538
|
-
page = kept[off:off + lim]
|
|
539
|
-
if expand_nodes:
|
|
540
|
-
page = [dict(e) for e in page]
|
|
541
|
-
for e in page:
|
|
542
|
-
e["child_node"] = _node_digest(cg, e.get("child"))
|
|
543
|
-
e["parent_node"] = _node_digest(cg, e.get("parent"))
|
|
544
|
-
|
|
545
|
-
# ---- 聚合(分页前全集;无分页漂移) ----------------------------------
|
|
546
|
-
aggregates = None
|
|
547
|
-
if agg_v:
|
|
548
|
-
field = _AGG_FIELD[agg_v]
|
|
549
|
-
buckets = {}
|
|
550
|
-
for e in kept:
|
|
551
|
-
k = str(e.get(field) or "")
|
|
552
|
-
b = buckets.get(k)
|
|
553
|
-
if b is None:
|
|
554
|
-
b = buckets[k] = {"key": k, "count": 0, "t_min": None,
|
|
555
|
-
"t_max": None, "sample": []}
|
|
556
|
-
b["count"] += 1
|
|
557
|
-
t = e.get("t")
|
|
558
|
-
if t is not None:
|
|
559
|
-
t = float(t)
|
|
560
|
-
b["t_min"] = t if b["t_min"] is None else min(b["t_min"], t)
|
|
561
|
-
b["t_max"] = t if b["t_max"] is None else max(b["t_max"], t)
|
|
562
|
-
if len(b["sample"]) < 3:
|
|
563
|
-
b["sample"].append(f"{e.get('child')}->{e.get('parent')}"
|
|
564
|
-
f"({e.get('rel')})")
|
|
565
|
-
aggregates = sorted(buckets.values(), key=lambda b: (-b["count"], b["key"]))
|
|
566
|
-
|
|
567
|
-
return {"ok": True, "readonly": True, "op": "edges",
|
|
568
|
-
"triple": {"child": c_f, "relation": r_f, "parent": p_f, "batch": b_f},
|
|
569
|
-
"total": total, "matched": len(kept), "returned": len(page),
|
|
570
|
-
"offset": off, "limit": lim, "ordering": ord_v,
|
|
571
|
-
"aggregation": agg_v, "aggregates": aggregates,
|
|
572
|
-
"nodes_expanded": bool(expand_nodes),
|
|
573
|
-
"edges": page,
|
|
574
|
-
"time_filter": _trust.time_filter_meta(
|
|
575
|
-
axis=axis, mode=_mode_of(start_operator, end_operator),
|
|
576
|
-
start=start_time, end=end_time,
|
|
577
|
-
start_operator=start_operator, end_operator=end_operator,
|
|
578
|
-
dropped=dropped, axis_missing=missing, applied=bool(enabled)),
|
|
579
|
-
"ledger": ledger_file(cg.root, path),
|
|
580
|
-
"distinct_from": "links.py = 跨节点信任 P_trust(_links.json)",
|
|
581
|
-
"note": ("只读:台账 ∪ 索引声明(台账优先,按边去重);"
|
|
582
|
-
"聚合基于分页前全集;边时刻字段为 t(观察轴),"
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""派生溯源(G8):新增节点常态化建链 + 悬空可检出。
|
|
3
|
+
|
|
4
|
+
回答「这个节点**从哪来**」——与 `md_cg.links` 刻意分层:
|
|
5
|
+
· `links.py` = 跨节点信任(「我信你多少」,P_trust,落 `~/.mdcg/_links.json`);
|
|
6
|
+
· 本模块 = 节点派生关系(「它由谁派生」,落 `<root>/_link.jsonl`)。
|
|
7
|
+
两者都叫「链接」,但一个管**信任状态**、一个管**演进血缘**,不可混用。
|
|
8
|
+
|
|
9
|
+
存储形态(对齐「md 单一真相源 + 派生索引可重建」):
|
|
10
|
+
· 权威声明在节点 frontmatter(`derived_from` / `derived_relation`);
|
|
11
|
+
· `<root>/_link.jsonl` 是 **append-only 派生台账**(快查用,可由 frontmatter 重建);
|
|
12
|
+
· 建链失败写 `<root>/_link.jsonl.fail`(降级留痕)。
|
|
13
|
+
|
|
14
|
+
三条纪律(对齐 G8 裁定 §六):
|
|
15
|
+
1. **只对新增节点常态化建链,历史不回填**——`rebuild_ledger` 只重放 frontmatter
|
|
16
|
+
里**已经声明**的关系,不为历史节点发明任何边(当前库历史声明为 0 → 重建为空);
|
|
17
|
+
2. **建链失败不得阻断写入**——`record()` 永不抛(best-effort),失败降级为告警 +
|
|
18
|
+
失败台账留痕,节点写入照常提交;
|
|
19
|
+
3. **巡检只读**——`check()` 检出悬空边(目标/子节点不在索引内)但**不自动删边**,
|
|
20
|
+
关系事实去留由人处置。
|
|
21
|
+
|
|
22
|
+
零第三方依赖。
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import time
|
|
29
|
+
|
|
30
|
+
from . import trust as _trust
|
|
31
|
+
from .fsutil import FileLock, append_jsonl, atomic_write, read_jsonl
|
|
32
|
+
|
|
33
|
+
LEDGER_NAME = "_link.jsonl"
|
|
34
|
+
FAIL_SUFFIX = ".fail"
|
|
35
|
+
LEDGER_ENV = "MDCG_LINK_FILE"
|
|
36
|
+
SCHEMA = 1
|
|
37
|
+
|
|
38
|
+
#: 允许的派生关系(显式枚举,避免「自由字符串」把血缘写成噪声)
|
|
39
|
+
RELATIONS = ("derived_from", "split_from", "extracted_from",
|
|
40
|
+
"merged_from", "refined_from", "source")
|
|
41
|
+
DEFAULT_RELATION = "derived_from"
|
|
42
|
+
|
|
43
|
+
#: frontmatter 里承载派生声明的字段(写路径只读这两处,不猜)
|
|
44
|
+
FM_FIELD = "derived_from"
|
|
45
|
+
FM_REL_FIELD = "derived_relation"
|
|
46
|
+
|
|
47
|
+
_LOCK_TIMEOUT = 2.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProvenanceError(Exception):
|
|
51
|
+
"""派生溯源错误。写路径侧一律由 `record()` 兜住,不向上抛。"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --------------------------------------------------------------------------
|
|
55
|
+
# 路径 / 规范化
|
|
56
|
+
# --------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
# 生效条件:path 为真值时返回 path;否则 os.environ.get(LEDGER_ENV) 为非空真值时返回该环境变量值;否则返回 os.path.join(root, LEDGER_NAME)。
|
|
59
|
+
def ledger_file(root: str, path: str = None) -> str:
|
|
60
|
+
"""台账路径:显式 → MDCG_LINK_FILE → <root>/_link.jsonl。"""
|
|
61
|
+
return path or os.environ.get(LEDGER_ENV) or os.path.join(root, LEDGER_NAME)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# 生效条件:传入 root(path 为真值则以其为准,否则由 ledger_file 的回落决定落点)时返回 ledger_file(root, path) 结果拼接 FAIL_SUFFIX。
|
|
65
|
+
def fail_log_file(root: str, path: str = None) -> str:
|
|
66
|
+
"""降级留痕路径(台账写不进时的「本该建的边」)。"""
|
|
67
|
+
return ledger_file(root, path) + FAIL_SUFFIX
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# 生效条件:value 为 None 返回 [];否则 list/tuple/set 逐个元素、其他类型视作单元素,元素经 str(x).strip() 后非空且未出现过才保留(重复只留首次)。
|
|
71
|
+
def as_list(value) -> list:
|
|
72
|
+
"""把单值 / 序列统一成去重、去空白的字符串列表。"""
|
|
73
|
+
if value is None:
|
|
74
|
+
return []
|
|
75
|
+
items = list(value) if isinstance(value, (list, tuple, set)) else [value]
|
|
76
|
+
out = []
|
|
77
|
+
for x in items:
|
|
78
|
+
s = str(x).strip()
|
|
79
|
+
if s and s not in out:
|
|
80
|
+
out.append(s)
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# 生效条件:rel 为假值(None/空串)或 str(rel).strip().lower() 后为空白的串(如 " ")时 r 回落 default(默认常量 DEFAULT_RELATION);r 不在 RELATIONS 内(含回落后的 default 本身非法)即抛 ProvenanceError,否则返回该小写串。
|
|
85
|
+
def normalize_relation(rel, default: str = DEFAULT_RELATION) -> str:
|
|
86
|
+
"""严格校验关系名;非法抛 `ProvenanceError`(显式 API 用)。"""
|
|
87
|
+
r = str(rel or "").strip().lower()
|
|
88
|
+
if not r:
|
|
89
|
+
r = default
|
|
90
|
+
if r not in RELATIONS:
|
|
91
|
+
raise ProvenanceError(f"未知派生关系:{rel}(允许:{RELATIONS})")
|
|
92
|
+
return r
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# 生效条件:normalize_relation(rel, default) 抛 ProvenanceError(含 rel 为假值/仅空白且 default 非法时)则原样返回 default;否则返回该调用的返回值。
|
|
96
|
+
def coerce_relation(rel, default: str = DEFAULT_RELATION) -> str:
|
|
97
|
+
"""宽松兜底:非法关系名回退默认值(**写路径用,保证永不阻断写入**)。"""
|
|
98
|
+
try:
|
|
99
|
+
return normalize_relation(rel, default)
|
|
100
|
+
except ProvenanceError:
|
|
101
|
+
return default
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# 生效条件:child 或 parent 为假值或仅空白使 str(x or "").strip() 为空、或二者 strip 后相等(自环)时返回 None;否则返回含 schema/t/child/parent/rel/batch/actor 的 dict,note 为真值时才附上并截断到 200 字符。
|
|
105
|
+
def make_edge(child, parent, *, relation=DEFAULT_RELATION, batch=None,
|
|
106
|
+
actor="system", note=None, t=None):
|
|
107
|
+
"""构造一条派生边;自环 / 空端点返回 None(**不产生无意义边**)。"""
|
|
108
|
+
c, p = str(child or "").strip(), str(parent or "").strip()
|
|
109
|
+
if not c or not p or c == p:
|
|
110
|
+
return None
|
|
111
|
+
edge = {"schema": SCHEMA,
|
|
112
|
+
"t": float(t if t is not None else time.time()),
|
|
113
|
+
"child": c, "parent": p, "rel": coerce_relation(relation),
|
|
114
|
+
"batch": batch, "actor": actor}
|
|
115
|
+
if note:
|
|
116
|
+
edge["note"] = str(note)[:200]
|
|
117
|
+
return edge
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# 生效条件:对 as_list(parents) 的每个父项调用 make_edge,只保留返回非 None 的边,全部被丢弃时返回空列表。
|
|
121
|
+
def edges_for(child, parents, **kw) -> list:
|
|
122
|
+
"""`(child, [parents]) → [edge]`:空端点 / 自环自动丢弃。"""
|
|
123
|
+
out = []
|
|
124
|
+
for p in as_list(parents):
|
|
125
|
+
e = make_edge(child, p, **kw)
|
|
126
|
+
if e:
|
|
127
|
+
out.append(e)
|
|
128
|
+
return out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# --------------------------------------------------------------------------
|
|
132
|
+
# 写:台账追加(record 为写路径唯一入口,永不抛)
|
|
133
|
+
# --------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
# 生效条件:edges 为 None/空/全为假元素时返回 0;否则在 FileLock(p, timeout=_LOCK_TIMEOUT) 内逐条 append_jsonl,抛 OSError 时转抛 ProvenanceError,成功返回 len(edges)。
|
|
136
|
+
def append(root: str, edges, *, path: str = None) -> int:
|
|
137
|
+
"""台账追加(加锁串行,防 Windows 并发交错丢边)。IO 失败抛 `ProvenanceError`。"""
|
|
138
|
+
edges = [e for e in (edges or []) if e]
|
|
139
|
+
if not edges:
|
|
140
|
+
return 0
|
|
141
|
+
p = ledger_file(root, path)
|
|
142
|
+
try:
|
|
143
|
+
os.makedirs(os.path.dirname(os.path.abspath(p)) or ".", exist_ok=True)
|
|
144
|
+
with FileLock(p, timeout=_LOCK_TIMEOUT):
|
|
145
|
+
for e in edges:
|
|
146
|
+
append_jsonl(p, e)
|
|
147
|
+
except OSError as exc:
|
|
148
|
+
raise ProvenanceError(f"派生台账写入失败:{p}({exc})") from exc
|
|
149
|
+
return len(edges)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# 生效条件:给定 root/path/child/parents/relation/reason/code 即恒返回 {'ok': False, 'added': 0, 'edges': [], 'degraded': True, 'degrade_code': code, 'reason': reason},其中写 fail_log_file 台账的异常被 except Exception 吞掉。
|
|
153
|
+
def _degrade(root: str, path, child, parents, relation, reason,
|
|
154
|
+
code: str) -> dict:
|
|
155
|
+
"""降级留痕:把「本该建的边」记进 .fail 台账(自身也 best-effort)。"""
|
|
156
|
+
rec = {"t": time.time(), "code": code, "child": str(child or ""),
|
|
157
|
+
"parents": as_list(parents), "rel": str(relation or ""),
|
|
158
|
+
"reason": str(reason)[:300]}
|
|
159
|
+
try:
|
|
160
|
+
append_jsonl(fail_log_file(root, path), rec)
|
|
161
|
+
except Exception: # noqa: BLE001
|
|
162
|
+
pass
|
|
163
|
+
return {"ok": False, "added": 0, "edges": [], "degraded": True,
|
|
164
|
+
"degrade_code": code, "reason": reason}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# 生效条件:edges_for 抛异常时经 _degrade(code='bad_edge') 返回;edges 为空时返回 {'ok': True, 'added': 0, 'edges': [], 'reason': 'no_parents'};append 抛 ProvenanceError 或其他异常时经 _degrade(code='ledger_io') 返回;其余返回 {'ok': True, 'added': n, 'edges': edges, 'ledger': …},恒不向外抛。
|
|
168
|
+
def record(root: str, child, parents, *, relation=DEFAULT_RELATION,
|
|
169
|
+
batch=None, actor="system", note=None, path: str = None) -> dict:
|
|
170
|
+
"""写路径建链入口:**永不抛**(G8 硬约束:建链失败不得阻断节点写入)。
|
|
171
|
+
|
|
172
|
+
返回 `{ok, added, edges, ...}`;失败时 `ok=False` + `degraded=True` 且已写
|
|
173
|
+
`.fail` 留痕。调用方**不得**因本函数返回 False 而回滚节点。
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
edges = edges_for(child, parents, relation=relation, batch=batch,
|
|
177
|
+
actor=actor, note=note)
|
|
178
|
+
except Exception as exc: # noqa: BLE001
|
|
179
|
+
return _degrade(root, path, child, parents, relation,
|
|
180
|
+
f"{type(exc).__name__}: {exc}", "bad_edge")
|
|
181
|
+
if not edges:
|
|
182
|
+
return {"ok": True, "added": 0, "edges": [], "reason": "no_parents"}
|
|
183
|
+
try:
|
|
184
|
+
n = append(root, edges, path=path)
|
|
185
|
+
except ProvenanceError as exc:
|
|
186
|
+
return _degrade(root, path, child, parents, relation, str(exc),
|
|
187
|
+
"ledger_io")
|
|
188
|
+
except Exception as exc: # noqa: BLE001
|
|
189
|
+
return _degrade(root, path, child, parents, relation,
|
|
190
|
+
f"{type(exc).__name__}: {exc}", "ledger_io")
|
|
191
|
+
return {"ok": True, "added": n, "edges": edges,
|
|
192
|
+
"ledger": ledger_file(root, path)}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --------------------------------------------------------------------------
|
|
196
|
+
# 读:台账 / 索引 / 悬空巡检
|
|
197
|
+
# --------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
# 生效条件:遍历 read_jsonl(ledger_file(root, path)),仅收录 isinstance(r, dict) 且 r.get("child") 与 r.get("parent") 均为真值(键缺失或值为假即丢弃)的记录。
|
|
200
|
+
def load(root: str, *, path: str = None) -> list:
|
|
201
|
+
"""读台账(跳过坏行;只取有端点的记录)。"""
|
|
202
|
+
out = []
|
|
203
|
+
for r in read_jsonl(ledger_file(root, path)):
|
|
204
|
+
if isinstance(r, dict) and r.get("child") and r.get("parent"):
|
|
205
|
+
out.append(r)
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# 生效条件:rows 中每行按 (r.get("child"), r.get("parent"), r.get("rel")) 三元组判重,仅首次出现的行保留,按原顺序返回去重列表。
|
|
210
|
+
def _dedupe(rows):
|
|
211
|
+
out, seen = [], set()
|
|
212
|
+
for r in rows:
|
|
213
|
+
key = (r.get("child"), r.get("parent"), r.get("rel"))
|
|
214
|
+
if key in seen:
|
|
215
|
+
continue
|
|
216
|
+
seen.add(key)
|
|
217
|
+
out.append(r)
|
|
218
|
+
return out
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# 生效条件:child/parent/relation/batch 各为真值时才做对应等值过滤(假值或 None 不过滤),去重后 limit 为真值时返回 out[:int(limit)],limit 为 None 或 0 时返回全部 out。
|
|
222
|
+
def edges(root: str, *, child=None, parent=None, relation=None, batch=None,
|
|
223
|
+
limit: int = None, path: str = None) -> list:
|
|
224
|
+
"""按端点 / 关系 / 批次过滤台账边(只读,去重,保持写入顺序)。"""
|
|
225
|
+
out = []
|
|
226
|
+
for r in load(root, path=path):
|
|
227
|
+
if child and r.get("child") != child:
|
|
228
|
+
continue
|
|
229
|
+
if parent and r.get("parent") != parent:
|
|
230
|
+
continue
|
|
231
|
+
if relation and r.get("rel") != relation:
|
|
232
|
+
continue
|
|
233
|
+
if batch and r.get("batch") != batch:
|
|
234
|
+
continue
|
|
235
|
+
out.append(r)
|
|
236
|
+
out = _dedupe(out)
|
|
237
|
+
return out[:int(limit)] if limit else out
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# 生效条件:以 (getattr(cg, "index", None) or {}).get("nodes") or {} 遍历(缺失时视作空);prefix 为真值时仅保留 str(nid).startswith(prefix) 的节点,输出经 _dedupe 去重。
|
|
241
|
+
def index_edges(cg, *, prefix: str = None) -> list:
|
|
242
|
+
"""从**索引快照**恢复派生边(零读文件)——台账丢失/未重建时的只读兜底。"""
|
|
243
|
+
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
244
|
+
out = []
|
|
245
|
+
for nid, e in nodes.items():
|
|
246
|
+
if prefix and not str(nid).startswith(prefix):
|
|
247
|
+
continue
|
|
248
|
+
rel = coerce_relation((e or {}).get(FM_REL_FIELD))
|
|
249
|
+
for p in as_list((e or {}).get(FM_FIELD)):
|
|
250
|
+
out.append({"schema": SCHEMA, "child": nid, "parent": p, "rel": rel,
|
|
251
|
+
"batch": (e or {}).get("derived_batch"), "actor": None,
|
|
252
|
+
"origin": "index"})
|
|
253
|
+
return _dedupe(out)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# 生效条件:给定 cg 即返回 _dedupe(load(cg.root, path=path) + index_edges(cg)),台账记录在前、按边去重。
|
|
257
|
+
def all_edges(cg, *, path: str = None) -> list:
|
|
258
|
+
"""台账 ∪ 索引声明(台账优先,按边去重)。"""
|
|
259
|
+
return _dedupe(load(cg.root, path=path) + index_edges(cg))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# 生效条件:include_index 为真值时取 all_edges(cg, path=path)、为假时取 load(cg.root, path=path),对 child/parent 不在 cg 索引节点键集合中的边记为 dangling 并列出 missing;返回 ok=not dangling,dangling 仅取 int(limit)(默认 20)项,不写盘。
|
|
263
|
+
def check(cg, *, path: str = None, limit: int = 20,
|
|
264
|
+
include_index: bool = True) -> dict:
|
|
265
|
+
"""只读巡检:检出**悬空派生边**(端点不在索引内)。**不删边、不写盘**。
|
|
266
|
+
|
|
267
|
+
`ok=False` 仅表示「有悬空」,不代表巡检失败;`checked=True` 恒成立。
|
|
268
|
+
"""
|
|
269
|
+
known = set((getattr(cg, "index", None) or {}).get("nodes") or {})
|
|
270
|
+
rows = all_edges(cg, path=path) if include_index else load(cg.root, path=path)
|
|
271
|
+
dangling = []
|
|
272
|
+
for e in rows:
|
|
273
|
+
missing = []
|
|
274
|
+
if e.get("child") not in known:
|
|
275
|
+
missing.append("child")
|
|
276
|
+
if e.get("parent") not in known:
|
|
277
|
+
missing.append("parent")
|
|
278
|
+
if missing:
|
|
279
|
+
dangling.append({"child": e.get("child"), "parent": e.get("parent"),
|
|
280
|
+
"rel": e.get("rel"), "missing": missing,
|
|
281
|
+
"batch": e.get("batch"), "t": e.get("t"),
|
|
282
|
+
"origin": e.get("origin") or "ledger"})
|
|
283
|
+
dangling.sort(key=lambda r: (r.get("child") or "", r.get("parent") or ""))
|
|
284
|
+
ledger = ledger_file(cg.root, path)
|
|
285
|
+
return {"ok": not dangling, "checked": True, "root": cg.root,
|
|
286
|
+
"ledger": ledger, "ledger_exists": os.path.exists(ledger),
|
|
287
|
+
"edges": len(rows), "nodes": len(known),
|
|
288
|
+
"dangling": dangling[:int(limit)], "dangling_count": len(dangling),
|
|
289
|
+
"readonly": True,
|
|
290
|
+
"note": "只读巡检:悬空边仅检出并报告,不自动删除(关系事实由人处置)"}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# 生效条件:apply 为假值(默认 False)时只返回 dry_run 报表(written=0、sample=es[:5]);apply 为真值时把 index_edges(cg) 的边写入 ledger_file(cg.root, path) 并返回 written=len(es)。
|
|
294
|
+
def rebuild_ledger(cg, *, apply: bool = False, path: str = None) -> dict:
|
|
295
|
+
"""按 frontmatter 重建台账——**只重放已声明的边,不发明任何边**。
|
|
296
|
+
|
|
297
|
+
历史节点未声明派生关系 → 重建结果为空,正合「历史不回填」。
|
|
298
|
+
默认 dry_run(只出报表)。
|
|
299
|
+
"""
|
|
300
|
+
es = index_edges(cg)
|
|
301
|
+
if not apply:
|
|
302
|
+
return {"ok": True, "dry_run": True, "edges": len(es),
|
|
303
|
+
"written": 0, "sample": es[:5],
|
|
304
|
+
"note": "预演:未写盘;只重放 frontmatter 已声明的关系"}
|
|
305
|
+
body = "".join(json.dumps(e, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
306
|
+
for e in es)
|
|
307
|
+
p = ledger_file(cg.root, path)
|
|
308
|
+
atomic_write(p, body)
|
|
309
|
+
return {"ok": True, "dry_run": False, "edges": len(es),
|
|
310
|
+
"written": len(es), "ledger": p}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# 生效条件:check(cg, path=path, limit=3) 成功时返回 edges/dangling/ledger/exists/sample(悬空边 child->parent);该调用抛任何异常时被 except Exception 吞掉并返回 {}。
|
|
314
|
+
def summary(cg, *, path: str = None) -> dict:
|
|
315
|
+
"""轻量摘要(只读;失败不抛,避免拖垮 health_os / 常驻循环)。"""
|
|
316
|
+
try:
|
|
317
|
+
rep = check(cg, path=path, limit=3)
|
|
318
|
+
return {"edges": rep["edges"], "dangling": rep["dangling_count"],
|
|
319
|
+
"ledger": rep["ledger"], "exists": rep["ledger_exists"],
|
|
320
|
+
"sample": [f"{r['child']}->{r['parent']}"
|
|
321
|
+
for r in rep["dangling"]]}
|
|
322
|
+
except Exception: # noqa: BLE001
|
|
323
|
+
return {}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# 生效条件:root 为真值时 ledger 字段取 ledger_file(root),否则取常量 LEDGER_NAME;其余自描述字段(SCHEMA、RELATIONS、DEFAULT_RELATION、FM_FIELD/FM_REL_FIELD 等)恒定返回。
|
|
327
|
+
def catalog(root: str = None) -> dict:
|
|
328
|
+
"""自描述(供 MCP catalog / 人工核对)。"""
|
|
329
|
+
return {
|
|
330
|
+
"layer": "派生溯源(G8)",
|
|
331
|
+
"question": "这个节点从哪来(演进血缘)",
|
|
332
|
+
"ledger": ledger_file(root) if root else LEDGER_NAME,
|
|
333
|
+
"schema": SCHEMA,
|
|
334
|
+
"relations": list(RELATIONS),
|
|
335
|
+
"default_relation": DEFAULT_RELATION,
|
|
336
|
+
"fm_fields": [FM_FIELD, FM_REL_FIELD],
|
|
337
|
+
"discipline": {"incremental_only": True, "no_backfill": True,
|
|
338
|
+
"never_block_write": True, "patrol_readonly": True},
|
|
339
|
+
"distinct_from": ("links.py = 跨节点信任 P_trust(_links.json);"
|
|
340
|
+
"本层 = 节点派生关系(_link.jsonl)"),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# --------------------------------------------------------------------------
|
|
345
|
+
# 读:三元组反查原语(阶段二 4.2 · find_entity_contexts 式)
|
|
346
|
+
# --------------------------------------------------------------------------
|
|
347
|
+
# 术语映射(**同一件事,勿新造第二套字段**):三元组 `subject / predicate /
|
|
348
|
+
# object` 在本层就是派生边的 `child / rel / parent`——即节点写入时声明的
|
|
349
|
+
# `derived_from` 关系。不另建 subject/predicate/object 参数字面量,理由有二:
|
|
350
|
+
# ① `cg` 工具面是**扁平 schema**,`subject` 已被 `identity`(`subject:<id>`
|
|
351
|
+
# 主体语义)与 `link.evidence`(证据主体)占用,同名异义会把两处口径搅在一起;
|
|
352
|
+
# ② `edges()` 已是唯一谓词载体(child/parent/relation/batch 四键),反查只是它的
|
|
353
|
+
# **只读超集**——另造一套参数必然分叉。
|
|
354
|
+
# 与 `edges()` 的三处**有意**差异(不是漂移):
|
|
355
|
+
# · 时间轴缺省 `observed`(边只有记录时刻 `t`,见 FIND_DEFAULT_AXIS);
|
|
356
|
+
# · 默认排序 `desc`(按 `t` 新→旧)且 `limit` 缺省 50、上限 500(分页原语,
|
|
357
|
+
# 不给「静默全量倾倒」);
|
|
358
|
+
# · 加 `aggregation`(分页前全集分桶)与 `expand_nodes`(端点摘要,索引级零读盘)。
|
|
359
|
+
|
|
360
|
+
#: 排序方向(封闭枚举,拒收未知名——与 `trust.TIME_OPERATORS` 同风格)
|
|
361
|
+
ORDERINGS = ("desc", "asc")
|
|
362
|
+
#: 聚合维度(封闭枚举):按谓词 / 对象端 / 主体端分桶
|
|
363
|
+
AGGREGATIONS = ("by_relation", "by_parent", "by_child")
|
|
364
|
+
#: 聚合维度 → 边字段(谓词在边上叫 `rel`;聚合名沿用三元组术语命名)
|
|
365
|
+
_AGG_FIELD = {"by_relation": "rel", "by_parent": "parent", "by_child": "child"}
|
|
366
|
+
#: 反查缺省时间轴:派生边只有一个时刻字段 `t`(写入时刻,观察轴),
|
|
367
|
+
#: **效力轴字段根本不存在**。与 `mdcg.search` 缺省 `effective` **有意不同**:
|
|
368
|
+
#: 那里 `effective_from/until` 是可选声明(多数节点没写),沿用 fail-open 不会
|
|
369
|
+
#: 出错;这里若缺省 `effective`,则「给了时间条件却恒不过滤」——把静默 no-op
|
|
370
|
+
#: 当成了「没有匹配」,属无法复算的错答。
|
|
371
|
+
FIND_DEFAULT_AXIS = "observed"
|
|
372
|
+
DEFAULT_FIND_LIMIT = 50
|
|
373
|
+
MAX_FIND_LIMIT = 500
|
|
374
|
+
#: `expand_nodes=True` 时透出的索引字段白名单(只读索引快照,**零读节点文件**)
|
|
375
|
+
EXPAND_FIELDS = ("layer", "tags", "importance", "writer", "session",
|
|
376
|
+
"derived_from", "derived_relation", "derived_batch",
|
|
377
|
+
"temporal", "time_window", "condition_space")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# 生效条件:value 为 None 或 str(value).strip() 为空时返回 "desc";小写后命中 ORDERINGS 返回该值;否则抛 ProvenanceError。
|
|
381
|
+
def _ordering_of(value) -> str:
|
|
382
|
+
"""排序方向归一 → `"desc"` / `"asc"`;未知 → `ProvenanceError`(fail-closed)。"""
|
|
383
|
+
if value is None or not str(value).strip():
|
|
384
|
+
return "desc"
|
|
385
|
+
v = str(value).strip().lower()
|
|
386
|
+
if v not in ORDERINGS:
|
|
387
|
+
raise ProvenanceError(f"未知 ordering {value!r}(允许:{ORDERINGS})")
|
|
388
|
+
return v
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# 生效条件:value 为 None 或 str(value).strip() 为空时返回 None(= 不聚合);小写后命中 AGGREGATIONS 返回该值;否则抛 ProvenanceError。
|
|
392
|
+
def _aggregation_of(value):
|
|
393
|
+
"""聚合维度归一 → `None` / `AGGREGATIONS` 之一;未知 → `ProvenanceError`。"""
|
|
394
|
+
if value is None or not str(value).strip():
|
|
395
|
+
return None
|
|
396
|
+
v = str(value).strip().lower()
|
|
397
|
+
if v not in AGGREGATIONS:
|
|
398
|
+
raise ProvenanceError(f"未知 aggregation {value!r}(允许:{AGGREGATIONS})")
|
|
399
|
+
return v
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# 生效条件:axis 为 "observed" 且 edge.get("t") 可经 trust.parse_time 解析时返回 (t, t, False);axis 非 observed 或 t 不可解析/缺失时返回 (None, None, True)。
|
|
403
|
+
def _edge_window(edge, axis: str):
|
|
404
|
+
"""边的轴窗口 → `(start, end, missing)`(与 `trust.time_window_of` **同形**)。
|
|
405
|
+
|
|
406
|
+
observed 轴:`t`(写入时刻)→ `(t, t, False)`;`t` 缺失(`index_edges` 兜底边
|
|
407
|
+
不带时间)→ `(None, None, True)`。其余轴一律 `(None, None, True)`——边没有效力轴
|
|
408
|
+
声明可读,如实报「不可判定」,由**轴策略**处置(observed fail-closed 剔除并计入
|
|
409
|
+
`axis_missing`;effective fail-open 保留),不在这里悄悄换轴。
|
|
410
|
+
"""
|
|
411
|
+
if str(axis) == "observed":
|
|
412
|
+
t = _trust.parse_time((edge or {}).get("t"))
|
|
413
|
+
if t is None:
|
|
414
|
+
return None, None, True
|
|
415
|
+
return t, t, False
|
|
416
|
+
return None, None, True
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# 生效条件:start_operator 与 end_operator 均为 None 时返回 "overlap",否则返回 "endpoint"。
|
|
420
|
+
def _mode_of(start_operator, end_operator) -> str:
|
|
421
|
+
"""时间过滤模式(与 `trust.window_match` 的显式分叉口径同源,不另立判据)。"""
|
|
422
|
+
return "endpoint" if (start_operator is not None or end_operator is not None) \
|
|
423
|
+
else "overlap"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# 生效条件:nid 不在 (cg.index or {}).get("nodes") or {} 的 dict 条目中(含 cg.index 缺失、条目非 dict)时返回 {'id': nid, 'present': False};否则返回 {'id','present':True} 并附 EXPAND_FIELDS 中值非 None 的字段。
|
|
427
|
+
def _node_digest(cg, nid) -> dict:
|
|
428
|
+
"""端点摘要(只读索引快照,**零读节点文件**);端点缺失 → `present=False`。"""
|
|
429
|
+
nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
|
|
430
|
+
e = nodes.get(nid)
|
|
431
|
+
if not isinstance(e, dict):
|
|
432
|
+
return {"id": nid, "present": False}
|
|
433
|
+
d = {"id": nid, "present": True}
|
|
434
|
+
for k in EXPAND_FIELDS:
|
|
435
|
+
if e.get(k) is not None:
|
|
436
|
+
d[k] = e[k]
|
|
437
|
+
return d
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# 生效条件:child/parent/relation/batch 各为真值(str(x).strip() 非空)时归一为过滤条件,relation 经 normalize_relation 非法即抛 ProvenanceError;ordering/aggregation 经 _ordering_of/_aggregation_of 非法即抛;offset 为负或 limit<=0 或 limit>MAX_FIND_LIMIT 即抛;时间五参经 trust.check_time_args 校验,why 非空即抛 ProvenanceError;返回 {'ok': True, 'readonly': True, 'op': 'edges', 'triple', 'total', 'matched', 'returned', 'offset', 'limit', 'ordering', 'aggregation', 'edges', 'aggregates', 'nodes_expanded', 'time_filter', 'ledger', 'distinct_from', 'note'},其中 total=谓词过滤后条数、matched=时间过滤后条数(dropped+matched==total)、edges=按 t 排序后 offset:offset+limit 切片(expand_nodes 时每边附 child_node/parent_node 摘要)、aggregates 为分页前全集分桶(未请求为 None)。
|
|
441
|
+
def find_edges(cg, *, child=None, parent=None, relation=None, batch=None,
|
|
442
|
+
start_time=None, end_time=None, start_operator=None,
|
|
443
|
+
end_operator=None, time_axis=None, ordering=None, offset=0,
|
|
444
|
+
limit=None, aggregation=None, expand_nodes: bool = False,
|
|
445
|
+
path: str = None) -> dict:
|
|
446
|
+
"""三元组反查(阶段二 4.2):按**任意端 / 谓词 / 时间**反查派生边(只读)。
|
|
447
|
+
|
|
448
|
+
`subject/predicate/object` ≡ `child/rel/parent`(见本节术语映射注释)。
|
|
449
|
+
|
|
450
|
+
谓词(`child` / `parent` / `relation` / `batch`)与 `edges()` **同源同义**:
|
|
451
|
+
给了就等值过滤、不给就不过滤。时间条件走 `trust.check_time_args`
|
|
452
|
+
(**与检索共用的唯一校验点**,本层不另写一套),比较语义由
|
|
453
|
+
`trust.window_match` 提供(不给 operator = 区间重叠;给 operator = 端点比较)。
|
|
454
|
+
|
|
455
|
+
fail-closed 清单(宁可报错,不静默降级):
|
|
456
|
+
· `relation` 非 `RELATIONS`(经 `normalize_relation`);
|
|
457
|
+
· `ordering` / `aggregation` 非各自枚举;
|
|
458
|
+
· `offset < 0`;`limit <= 0` 或 `> MAX_FIND_LIMIT`(**不把 0/负数当「全部」**);
|
|
459
|
+
· 时间五参非法(未知轴 / 未知算子 / 给了算子缺时间 / start > end)。
|
|
460
|
+
|
|
461
|
+
分页与聚合的次序是刻意的:**聚合基于分页前全集**(`matched`),
|
|
462
|
+
否则「先切页再聚合」会给出随 offset 漂移的分桶——不可复算。
|
|
463
|
+
审计块 `time_filter.dropped + matched == total` 由本函数保证。
|
|
464
|
+
"""
|
|
465
|
+
# ---- 谓词归一(空/空白 = 不约束,与 edges() 同口径) ------------------
|
|
466
|
+
c_f = str(child).strip() if child is not None and str(child).strip() else None
|
|
467
|
+
p_f = str(parent).strip() if parent is not None and str(parent).strip() else None
|
|
468
|
+
b_f = str(batch).strip() if batch is not None and str(batch).strip() else None
|
|
469
|
+
r_f = normalize_relation(relation) if (relation is not None
|
|
470
|
+
and str(relation).strip()) else None
|
|
471
|
+
# ---- 排序 / 分页 / 聚合 入参校验 --------------------------------------
|
|
472
|
+
ord_v = _ordering_of(ordering)
|
|
473
|
+
agg_v = _aggregation_of(aggregation)
|
|
474
|
+
try:
|
|
475
|
+
off = int(offset or 0)
|
|
476
|
+
except (TypeError, ValueError) as exc:
|
|
477
|
+
raise ProvenanceError(f"offset 非法:{offset!r}") from exc
|
|
478
|
+
if off < 0:
|
|
479
|
+
raise ProvenanceError(f"offset 不能为负:{off}")
|
|
480
|
+
lim = DEFAULT_FIND_LIMIT if limit is None else int(limit)
|
|
481
|
+
if lim <= 0:
|
|
482
|
+
raise ProvenanceError(f"limit 必须为正整数(0/负数不当「全部」):{limit!r}")
|
|
483
|
+
if lim > MAX_FIND_LIMIT:
|
|
484
|
+
raise ProvenanceError(f"limit 超上限 {MAX_FIND_LIMIT}:{lim}")
|
|
485
|
+
# ---- 时间算子:复用唯一校验点,缺省轴按本层语义补 observed -----------
|
|
486
|
+
enabled, axis0, why = _trust.check_time_args(
|
|
487
|
+
start_time=start_time, end_time=end_time,
|
|
488
|
+
start_operator=start_operator, end_operator=end_operator,
|
|
489
|
+
time_axis=time_axis)
|
|
490
|
+
if why:
|
|
491
|
+
raise ProvenanceError(why)
|
|
492
|
+
axis = FIND_DEFAULT_AXIS if time_axis is None else axis0
|
|
493
|
+
q_s = _trust.parse_time(start_time) if enabled else None
|
|
494
|
+
q_e = _trust.parse_time(end_time) if enabled else None
|
|
495
|
+
|
|
496
|
+
rows = all_edges(cg, path=path)
|
|
497
|
+
cand = []
|
|
498
|
+
for e in rows:
|
|
499
|
+
if c_f and e.get("child") != c_f:
|
|
500
|
+
continue
|
|
501
|
+
if p_f and e.get("parent") != p_f:
|
|
502
|
+
continue
|
|
503
|
+
if r_f and e.get("rel") != r_f:
|
|
504
|
+
continue
|
|
505
|
+
if b_f and e.get("batch") != b_f:
|
|
506
|
+
continue
|
|
507
|
+
cand.append(e)
|
|
508
|
+
total = len(cand)
|
|
509
|
+
|
|
510
|
+
# ---- 时间过滤(候选层:与 trust.filter_by_time 同策略) ---------------
|
|
511
|
+
dropped = missing = 0
|
|
512
|
+
kept = []
|
|
513
|
+
for e in cand:
|
|
514
|
+
if not enabled:
|
|
515
|
+
kept.append(e)
|
|
516
|
+
continue
|
|
517
|
+
cs, ce, miss = _edge_window(e, axis)
|
|
518
|
+
if miss:
|
|
519
|
+
if axis == "observed": # 观察轴 fail-closed
|
|
520
|
+
dropped += 1
|
|
521
|
+
missing += 1
|
|
522
|
+
continue
|
|
523
|
+
kept.append(e) # 效力轴 fail-open(无效力声明可读)
|
|
524
|
+
continue
|
|
525
|
+
if _trust.window_match(cs, ce, q_s, q_e, start_operator, end_operator):
|
|
526
|
+
kept.append(e)
|
|
527
|
+
else:
|
|
528
|
+
dropped += 1
|
|
529
|
+
if dropped + len(kept) != total: # 审计不变式(可复算)
|
|
530
|
+
raise ProvenanceError(
|
|
531
|
+
f"审计不变式破缺:dropped({dropped}) + kept({len(kept)}) != total({total})")
|
|
532
|
+
|
|
533
|
+
# ---- 排序(t 缺失按 0 计,确定性次级键防抖) --------------------------
|
|
534
|
+
kept.sort(key=lambda e: (float(e.get("t") or 0.0),
|
|
535
|
+
str(e.get("child") or ""),
|
|
536
|
+
str(e.get("parent") or "")),
|
|
537
|
+
reverse=(ord_v == "desc"))
|
|
538
|
+
page = kept[off:off + lim]
|
|
539
|
+
if expand_nodes:
|
|
540
|
+
page = [dict(e) for e in page]
|
|
541
|
+
for e in page:
|
|
542
|
+
e["child_node"] = _node_digest(cg, e.get("child"))
|
|
543
|
+
e["parent_node"] = _node_digest(cg, e.get("parent"))
|
|
544
|
+
|
|
545
|
+
# ---- 聚合(分页前全集;无分页漂移) ----------------------------------
|
|
546
|
+
aggregates = None
|
|
547
|
+
if agg_v:
|
|
548
|
+
field = _AGG_FIELD[agg_v]
|
|
549
|
+
buckets = {}
|
|
550
|
+
for e in kept:
|
|
551
|
+
k = str(e.get(field) or "")
|
|
552
|
+
b = buckets.get(k)
|
|
553
|
+
if b is None:
|
|
554
|
+
b = buckets[k] = {"key": k, "count": 0, "t_min": None,
|
|
555
|
+
"t_max": None, "sample": []}
|
|
556
|
+
b["count"] += 1
|
|
557
|
+
t = e.get("t")
|
|
558
|
+
if t is not None:
|
|
559
|
+
t = float(t)
|
|
560
|
+
b["t_min"] = t if b["t_min"] is None else min(b["t_min"], t)
|
|
561
|
+
b["t_max"] = t if b["t_max"] is None else max(b["t_max"], t)
|
|
562
|
+
if len(b["sample"]) < 3:
|
|
563
|
+
b["sample"].append(f"{e.get('child')}->{e.get('parent')}"
|
|
564
|
+
f"({e.get('rel')})")
|
|
565
|
+
aggregates = sorted(buckets.values(), key=lambda b: (-b["count"], b["key"]))
|
|
566
|
+
|
|
567
|
+
return {"ok": True, "readonly": True, "op": "edges",
|
|
568
|
+
"triple": {"child": c_f, "relation": r_f, "parent": p_f, "batch": b_f},
|
|
569
|
+
"total": total, "matched": len(kept), "returned": len(page),
|
|
570
|
+
"offset": off, "limit": lim, "ordering": ord_v,
|
|
571
|
+
"aggregation": agg_v, "aggregates": aggregates,
|
|
572
|
+
"nodes_expanded": bool(expand_nodes),
|
|
573
|
+
"edges": page,
|
|
574
|
+
"time_filter": _trust.time_filter_meta(
|
|
575
|
+
axis=axis, mode=_mode_of(start_operator, end_operator),
|
|
576
|
+
start=start_time, end=end_time,
|
|
577
|
+
start_operator=start_operator, end_operator=end_operator,
|
|
578
|
+
dropped=dropped, axis_missing=missing, applied=bool(enabled)),
|
|
579
|
+
"ledger": ledger_file(cg.root, path),
|
|
580
|
+
"distinct_from": "links.py = 跨节点信任 P_trust(_links.json)",
|
|
581
|
+
"note": ("只读:台账 ∪ 索引声明(台账优先,按边去重);"
|
|
582
|
+
"聚合基于分页前全集;边时刻字段为 t(观察轴),"
|
|
583
583
|
"无效力轴字段——真实时间条件请用缺省 time_axis=observed")}
|