@furongjun1999/dsh-memory 0.4.11 → 0.5.0

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