@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
package/md_cg/trust.py CHANGED
@@ -1,950 +1,986 @@
1
- # -*- coding: utf-8 -*-
2
- """md_cg · 可验证记忆单元(验证态状态机 + 依赖图 + 双时间轴)
3
-
4
- 把「可信度」从散文式描述,变成**显式、可查询、可传播、可输出**的结构化状态。
5
- 三件事:
6
-
7
- 1. **验证态持续化**——`verification_state` ∈ (unverified / verified /
8
- expired / doubted / rechecking),随验证动作与依赖变动**流转**,不是一次性
9
- 布尔开关;状态落 `frontmatter.verification_state`(节点 md 是唯一真源),
10
- 索引快照同名透出(免读文件可查)。
11
- 2. **依赖声明与失效传播**——单元显式声明它依赖谁(`depends_on`);被依赖单元
12
- 被修改/证伪时,**直接下游立即同步标为 doubted**(一跳,低成本);**多跳**
13
- 交由 `propagate()` 后台巡检重算,不阻塞写入与查询。
14
- 3. **双时间轴**——既有「何时不再成立」(`valid_until`)之外补「何时开始成立」
15
- (`valid_from`),使时效区间可判定:未生效 / 生效中 / 已过期。
16
-
17
- 与 `lifecycle.py` 刻意分层(同构实现、**正交语义**,禁跨套复用常量):
18
- · `lifecycle.py` = 节点**生命周期**(active/converged/demoted/archived);
19
- · 本模块 = 节点**验证态**(unverified/verified/expired/doubted/rechecking)。
20
- 两者都是「单点裁决 + 单点推进 + 硬拒入口」,但状态集不共享、`_RANK` 不共享。
21
-
22
- 与 `provenance.py` 刻意分层:
23
- · `provenance.py` = 节点**派生血缘**(「它由谁派生」,`_link.jsonl`);
24
- · 本模块 = 节点**依赖与证据强度**(「它靠谁成立、现在还成不成立」,
25
- `_trust.jsonl`)。血缘是既成事实,依赖是可失效假设——不可混用。
26
-
27
- 三条纪律(对齐 G8 裁定与 lifecycle 口径):
28
- 1. **台账失败降级不阻断写**——`mark_dependents()` 永不抛,写不进只留痕告警;
29
- 2. **历史不回填**——存量缺 `verification_state` 视为 `unverified`,`backfill()`
30
- 只在显式调用时把「缺省」变「显式」;
31
- 3. **巡检只读**——`patrol()` 检出悬空依赖/过期节点但**不自动改状态、不删节点**。
32
-
33
- 命名避让(既有先例):字段**不叫 `state`**——该名已被裁决四态
34
- (ACCEPT/REJECT/DEFER/BLINDSPOT)占用,`lifecycle_state` 的先例同此动机。
35
-
36
- 零第三方依赖(D-005)。
37
- """
38
- from __future__ import annotations
39
-
40
- import os
41
- import time
42
-
43
- from .fsutil import append_jsonl, read_jsonl
44
-
45
- #: 验证态全集。语义:
46
- #: unverified 未验证(默认;存量缺字段即视为它)
47
- #: verified 已验证(有验证方式与证据,且依赖未动)
48
- #: doubted 存疑(**依赖的地基动了**——异常的新定义)
49
- #: rechecking 重新验证中(复核进行态)
50
- #: expired 已过期(超出时效区间,或证据整体失效)
51
- STATES = ("unverified", "verified", "doubted", "rechecking", "expired")
52
-
53
- #: frontmatter 字段名(索引快照同名透出)。刻意避开 `state`——与 lifecycle 同动机。
54
- STATE_FIELD = "verification_state"
55
- #: 验证履历字段(frontmatter 侧历史,滚动保留最近 HISTORY_KEEP 条)
56
- HISTORY_FIELD = "verification_history"
57
- HISTORY_KEEP = 20
58
- #: 依赖声明字段(CCG「子功能」槽的落字段)
59
- DEPS_FIELD = "depends_on"
60
- #: 双时间轴(**效力时间**)字段。2026-09-19 阶段一:规范名迁移到
61
- #: `effective_from` / `effective_until`(**新写入落规范键**),历史名
62
- #: `valid_from` / `valid_until` 保留为**读取侧回落别名**(存量不迁移、零破坏)。
63
- FROM_FIELD = "valid_from"
64
- UNTIL_FIELD = "valid_until"
65
- #: 规范名(新写入落此;读取时优先于同名族旧键)
66
- EFFECTIVE_FROM_FIELD = "effective_from"
67
- EFFECTIVE_UNTIL_FIELD = "effective_until"
68
- #: 信念时间(体系**何时确认此条**):取代/审核的锚。
69
- #: **不是效力语义**——它既不是「已结束」也不是「尚未开始」,
70
- #: 故绝不并入 `scrub._EXPIRY_KEYS`(已结束族)或 `scrub._NOT_YET_KEYS`(未生效族)。
71
- BELIEVED_FIELD = "believed_at"
72
- #: 过期时刻(写盘冗余:由 `effective_until` 派生落盘,供审计/对账直读)。
73
- EXPIRED_FIELD = "expired_at"
74
-
75
- #: 端点取值优先级(**规范键优先、别名回落**,2026-09-19 阶段一)。
76
- #: 与 `scrub._NOT_YET_KEYS` / `scrub._EXPIRY_KEYS` 同键族——两处共用同一套别名,
77
- #: 任一侧新增键须同步另一侧(交叉守卫测试 test_validity_filter 守住)。
78
- FROM_ALIASES = (EFFECTIVE_FROM_FIELD, FROM_FIELD, "valid_since", "starts_at")
79
- UNTIL_ALIASES = (EFFECTIVE_UNTIL_FIELD, UNTIL_FIELD,
80
- "expires_at", "expire_at", "expiry", "deadline")
81
- #: 验证态迁移审计(append-only,与 _lifecycle.jsonl / _maintain.jsonl 同风格)
82
- AUDIT_FILE = "_trust.jsonl"
83
-
84
- #: 降级序位(用于「受保护豁免降级」判定)。verified 最高,doubted/expired 更低。
85
- _RANK = {"verified": 0, "rechecking": 1, "unverified": 1, "doubted": 2, "expired": 3}
86
-
87
- #: 合法迁移表。刻意不收录的(即拒绝的):
88
- #: verified → unverified(回落未验证:证据已存在,只应「存疑」而非「清零」)
89
- #: doubted → expired(跳级:存疑未经复核不得直接判过期)
90
- #: 注:`unverified → doubted` **收录**——两条路都真实到达这里:①上游变动波及一个
91
- #: 从未验证过的下游(「地基动了」对未验证者同样成立);②未验证节点收到**负证据**
92
- #: (证据削弱本身就是存疑的依据,比停在「未验证」更准确)。
93
- TRANSITIONS = frozenset({
94
- ("unverified", "verified"),
95
- ("unverified", "doubted"),
96
- ("unverified", "rechecking"),
97
- ("unverified", "expired"),
98
- ("verified", "doubted"),
99
- ("verified", "rechecking"),
100
- ("verified", "expired"),
101
- ("doubted", "rechecking"),
102
- ("doubted", "verified"),
103
- ("rechecking", "verified"),
104
- ("rechecking", "doubted"),
105
- ("rechecking", "unverified"),
106
- ("rechecking", "expired"),
107
- ("expired", "rechecking"),
108
- ("expired", "verified"),
109
- })
110
-
111
- #: 依赖边上限(单节点直接下游数);超出即拒收,防「依赖声明写成噪声」。
112
- MAX_DEPS = 32
113
- #: 多跳传播的节点上限(对齐 subgraph.MAX_NODES_DEFAULT 的量级)
114
- MAX_NODES_DEFAULT = 500
115
-
116
-
117
- class TrustError(Exception):
118
- """非法验证态迁移 / 依赖声明非法(写路径用 `require_transition` 直接抛)。"""
119
-
120
- def __init__(self, src, dst, code, reason):
121
- super().__init__(f"非法验证态迁移 {src}→{dst}({code}):{reason}")
122
- self.src, self.dst, self.code, self.reason = src, dst, code, reason
123
-
124
-
125
- # ---------------------------------------------------------------- 纯函数裁决
126
-
127
- def state_of(fm) -> str:
128
- """frontmatter → 验证态;缺字段或未知值 → "unverified"(存量兼容,不猜测)。"""
129
- if not isinstance(fm, dict):
130
- return "unverified"
131
- s = fm.get(STATE_FIELD)
132
- return s if s in STATES else "unverified"
133
-
134
-
135
- def is_downgrade(src: str, dst: str) -> bool:
136
- """是否向「更低可信度」迁移(verified < unverified/rechecking < doubted < expired)。"""
137
- return _RANK.get(dst, 0) > _RANK.get(src, 0)
138
-
139
-
140
- def can_transition(src, dst) -> bool:
141
- """迁移是否合法(含幂等;未知 src 按 unverified 处理)。"""
142
- src = src if src in STATES else "unverified"
143
- if dst not in STATES:
144
- return False
145
- if src == dst:
146
- return True # 幂等:同状态是 no-op
147
- return (src, dst) in TRANSITIONS
148
-
149
-
150
- def check(src, dst, protected: bool = False, override: bool = False):
151
- """迁移合法性裁决 → `(ok, code, reason)`。**唯一裁决点**(纯函数,无 IO)。
152
-
153
- code 取值:`ok` / `noop`(同状态,不写盘)/ `unknown_state` /
154
- `illegal_transition` / `protected`。
155
- """
156
- src = src if src in STATES else "unverified"
157
- if dst not in STATES:
158
- return False, "unknown_state", f"未知验证态 {dst!r}(允许:{STATES})"
159
- if src == dst:
160
- return True, "noop", "验证态未变(幂等,不写盘)"
161
- if (src, dst) not in TRANSITIONS:
162
- legal = "、".join(f"{a}→{b}" for a, b in sorted(TRANSITIONS))
163
- return False, "illegal_transition", (
164
- f"{src}→{dst} 不在合法迁移表内(允许:{legal})——"
165
- "验证态只沿「验证/存疑/复核/过期」四条路径流转,跨级迁移一律拒绝")
166
- if is_downgrade(src, dst) and protected and not override:
167
- return False, "protected", (
168
- f"受保护节点不接受降级迁移 {src}→{dst}(保护 = 不可轻弃);"
169
- "确需降级请显式 override=True")
170
- return True, "ok", f"{src}→{dst} 合法"
171
-
172
-
173
- def require_transition(src, dst, protected: bool = False, override: bool = False):
174
- """合法则返回 code,非法则抛 `TrustError`(写路径的硬拒入口)。"""
175
- ok, code, why = check(src, dst, protected=protected, override=override)
176
- if not ok:
177
- raise TrustError(src if src in STATES else "unverified", dst, code, why)
178
- return code
179
-
180
-
181
- def stamp(fm: dict, dst: str, reason: str = None, actor: str = None,
182
- evidence: str = None, method: str = None, trigger: str = None,
183
- override: bool = False):
184
- """在给定 frontmatter 上**就地**推进验证态(无 IO)→ `(ok, code, why)`。
185
-
186
- 与 `set_state()` 共用同一裁决点(`check`),供**需与其他字段同批落盘**的
187
- 收口点使用(写入时一次写盘改依赖/时间轴/验证态)。
188
- """
189
- src = state_of(fm)
190
- prot = bool(fm.get("protected") or fm.get("immutable"))
191
- ok, code, why = check(src, dst, protected=prot, override=override)
192
- if not ok or code == "noop":
193
- return ok, code, why
194
- fm[STATE_FIELD] = dst
195
- rec = {"at": time.time(), "from": src, "to": dst, "reason": reason,
196
- "actor": actor, "trigger": trigger}
197
- if method:
198
- rec["method"] = str(method)[:120]
199
- if evidence:
200
- rec["evidence"] = str(evidence)[:300]
201
- hist = list(fm.get(HISTORY_FIELD) or [])
202
- hist.append(rec)
203
- fm[HISTORY_FIELD] = hist[-HISTORY_KEEP:]
204
- if method:
205
- fm["verification_method"] = str(method)[:120]
206
- if evidence:
207
- fm["verification_evidence"] = str(evidence)[:300]
208
- fm["verified_at"] = rec["at"]
209
- return True, code, why
210
-
211
-
212
- # ---------------------------------------------------------------- 双时间轴
213
-
214
- def parse_time(value):
215
- """时间值 → epoch 秒;不可解析返回 None(**不猜测**)。
216
-
217
- 接受:数字(epoch 秒)/ 日期或日期时间字符串(ISO 8601,宽松:空格分隔、
218
- `Z` 后缀、日期精度都容忍)。纯标准库实现。
219
- """
220
- if value is None or value == "":
221
- return None
222
- if isinstance(value, (int, float)):
223
- return float(value)
224
- s = str(value).strip()
225
- if not s:
226
- return None
227
- try:
228
- return float(s)
229
- except ValueError:
230
- pass
231
- t = s.replace("/", "-").replace("T", " ")
232
- if t.endswith("Z"):
233
- t = t[:-1].strip()
234
- fmts = ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d")
235
- for f in fmts:
236
- try:
237
- return time.mktime(time.strptime(t, f))
238
- except ValueError:
239
- continue
240
- return None
241
-
242
-
243
- def first_endpoint(fm, keys):
244
- """按 `keys` 优先级取首个**可解析**端点 → `(epoch, key)`;全链无值 → `(None, None)`。
245
-
246
- 单点定义「规范键优先、别名回落」的取值口径:某键**存在但不可解析**时继续回落
247
- (而非按 None 定案)——「写坏了的时间值」不得遮蔽同族另一个合法键。
248
- """
249
- if not isinstance(fm, dict):
250
- return None, None
251
- for k in keys:
252
- if k in fm:
253
- ts = parse_time(fm.get(k))
254
- if ts is not None:
255
- return ts, k
256
- return None, None
257
-
258
-
259
- def validity(fm, now: float = None):
260
- """双时间轴判定 → `(kind, start, end)`。
261
-
262
- kind ∈ `unknown`(无时间轴约束)/ `not_yet`(未生效)/ `active`(生效中)/
263
- `expired`(已过期)。任一端点不可解析 → 该端点按 None 处理(不猜测、不误判)。
264
-
265
- 端点取值(2026-09-19 阶段一):**规范键优先、别名回落**——
266
- 起点 `effective_from` > `valid_from` > `valid_since` > `starts_at`;
267
- 终点 `effective_until` > `valid_until` > `expires_at` > `expire_at` > `expiry` > `deadline`。
268
- `believed_at`(信念时间)**不参与**本判定:它是「体系何时确认此条」(取代/审核的锚),
269
- 不是效力端点——并入任一方向都会把「已确认」误判成「已生效/已失效」。
270
- """
271
- if not isinstance(fm, dict):
272
- return "unknown", None, None
273
- start, _sk = first_endpoint(fm, FROM_ALIASES)
274
- end, _ek = first_endpoint(fm, UNTIL_ALIASES)
275
- if start is None and end is None:
276
- return "unknown", None, None
277
- t = time.time() if now is None else float(now)
278
- if start is not None and t < start:
279
- return "not_yet", start, end
280
- if end is not None and t > end:
281
- return "expired", start, end
282
- return "active", start, end
283
-
284
-
285
- def believed_at(fm) -> float:
286
- """信念时间(体系**何时确认此条**)→ epoch 秒;缺字段/不可解析 → None(不猜测)。
287
-
288
- 用途:取代(supersede)/ 审核的排序锚——「谁更晚被确认」是判定新旧的正路;
289
- **不得**拿 `valid_from`/`valid_until` 代替(那是事实在任务世界里何时有效,
290
- 与体系何时知道它无关)。
291
- """
292
- if not isinstance(fm, dict):
293
- return None
294
- return parse_time(fm.get(BELIEVED_FIELD))
295
-
296
-
297
- def is_expired(fm, now: float = None) -> bool:
298
- """`validity` 的布尔快捷:是否「**已过期**」。
299
-
300
- 只判 `expired`——`not_yet`(尚未生效)**不算**:两者语义相反
301
- (见 `scrub._NOT_YET_KEYS` 纪律「`valid_from` 绝不并入 `_EXPIRY_KEYS`」),
302
- 「尚未开始」不等于「已失效」。无时间轴 / 端点不可解析 → False(不猜测、不误杀)。
303
- """
304
- return validity(fm, now=now)[0] == "expired"
305
-
306
-
307
- def time_window_msg(fm, now: float = None) -> str:
308
- """时效判定的一句话(空串表示无时间轴约束)。**点名实际命中的键**(含别名)。"""
309
- kind, start, end = validity(fm, now=now)
310
- if kind == "unknown":
311
- return ""
312
- if kind == "not_yet":
313
- _ts, k = first_endpoint(fm, FROM_ALIASES)
314
- return f"未生效({k or FROM_FIELD} 未到)"
315
- if kind == "expired":
316
- _ts, k = first_endpoint(fm, UNTIL_ALIASES)
317
- return f"已过期({k or UNTIL_FIELD} 已过)"
318
- return "时效内"
319
-
320
-
321
- # ---------------------------------------------------------------- 时间算子(阶段二 4.1)
322
-
323
- #: 时间轴(**封闭枚举**)。两条轴物理隔离、语义不可互换(同 `believed_at` 的隔离纪律):
324
- #: effective 效力轴——这条事实**何时开始/不再成立**(`effective_from/until` 及别名)
325
- #: observed 观察轴——这条记忆**何时被观测/事件何时发生**(`temporal` / `time_window`)
326
- #: 不设**隐式**默认轴:入口在「启用时间算子但未指定轴」时回落 `effective`
327
- #: (与既有 `validity=` 语义连续),但轴本身永远由调用方显式决定。
328
- TIME_AXES = ("effective", "observed")
329
-
330
- #: 时间算子(**封闭枚举**,拒收未知名):候选轴端点 与 查询端点 的比较关系。
331
- #: 缺省(不给 operator)= **区间重叠**语义,见 `window_match`。
332
- TIME_OPERATORS = ("gt", "gte", "eq", "lte", "lt")
333
-
334
- #: 观察轴字段名(与 `stg._interval` 同源口径,**不新增第二套解析**)。
335
- OBSERVED_TIME_FIELD = "temporal"
336
- OBSERVED_WINDOW_FIELD = "time_window"
337
-
338
-
339
- def time_axis_of(axis) -> str:
340
- """轴名归一 → `"effective"` / `"observed"`;`None` → `"effective"`;
341
- 其余(含 `"believed"`)→ `ValueError`(**fail-closed,不静默降级**)。
342
-
343
- 与 `_stg_call`「不做签名推导——猜错会静默返回错误视图,比报错更贵」同风格:
344
- 轴写错时必须报错,因为静默按另一条轴过滤会产出**无法复算**的结果集。
345
- """
346
- if axis is None:
347
- return "effective"
348
- a = str(axis).strip().lower()
349
- if a in TIME_AXES:
350
- return a
351
- raise ValueError(f"未知 time_axis {axis!r}(允许:{TIME_AXES})")
352
-
353
-
354
- def time_window_of(fm, axis: str = "effective"):
355
- """按轴取候选时间窗口 → `(start, end)`;不可判定 → `(None, None)`(不猜测)。
356
-
357
- · `effective`:效力轴,走 `first_endpoint(FROM_ALIASES/UNTIL_ALIASES)`
358
- (规范键优先、别名回落,与 `validity` **同源**);
359
- 缺字段/不可解析 → 该侧 `None`(= 无界,与 `validity` 的「不误判」口径一致)。
360
- · `observed`:观察轴,与 `stg._interval` **同源口径**——`temporal`(事件时刻)优先,
361
- 缺失才回退 `condition_space.time_window`(观测窗);任一端不可解析 → `(None, None)`
362
- (与 `_interval` 的「整体不可用」语义一致)。
363
-
364
- `believed_at` **永不参与**(`BELIEVED_FIELD` 不出现于任何轴)。
365
- """
366
- if not isinstance(fm, dict):
367
- return None, None
368
- if time_axis_of(axis) == "observed":
369
- t = fm.get(OBSERVED_TIME_FIELD)
370
- if t is not None:
371
- ts = parse_time(t)
372
- if ts is not None:
373
- return ts, ts
374
- # 兼容两种载体形态:**fm**(`condition_space.time_window`)与**索引扁平快照**
375
- # (`time_window` 平铺在 entry 上,见 `mdcg._scan_nodes`/`_stage`)。
376
- # 只读嵌套会让「按 entry 过滤」的路径永不命中(同类坑:S2 时间门控,
377
- # mdcg.py L639 已记「直接读 cs 会让门控永不生效」)。
378
- cs = fm.get("condition_space") or {}
379
- tw = fm.get(OBSERVED_WINDOW_FIELD)
380
- if tw is None and isinstance(cs, dict):
381
- tw = cs.get(OBSERVED_WINDOW_FIELD)
382
- if isinstance(tw, (list, tuple)) and len(tw) == 2:
383
- s, e = parse_time(tw[0]), parse_time(tw[1])
384
- if s is None or e is None:
385
- return None, None
386
- return s, e
387
- return None, None
388
- start, _sk = first_endpoint(fm, FROM_ALIASES)
389
- end, _ek = first_endpoint(fm, UNTIL_ALIASES)
390
- return start, end
391
-
392
-
393
- def _op_ok(cand, q, op) -> bool:
394
- """单个端点比较;任一端不可解析 → False(缺字段的处置归**轴策略**,此处不猜测)。"""
395
- if cand is None or q is None:
396
- return False
397
- if op == "gt":
398
- return cand > q
399
- if op == "gte":
400
- return cand >= q
401
- if op == "eq":
402
- return cand == q
403
- if op == "lte":
404
- return cand <= q
405
- if op == "lt":
406
- return cand < q
407
- return False # 未知算子:入口已 fail-closed,此处保守拒
408
-
409
-
410
- def window_match(cand_start, cand_end, q_start=None, q_end=None,
411
- start_op: str = None, end_op: str = None) -> bool:
412
- """候选窗口与查询窗口是否匹配(§1.2 B1/B2 的**唯一实现点**,纯函数)。
413
-
414
- 两种模式由「是否给 operator」**显式分叉**(不允许隐式混用——混用会产出
415
- 「无法复算」的过滤,违反白箱):
416
-
417
- · **重叠模式**(`start_op`/`end_op` 均为 `None`):候选窗口与查询窗口有交集
418
- 即命中(记忆窗口是**区间**不是点)。查询端点缺省 = 该侧**无界**(不隐含 now)。
419
- · **端点模式**(至少给一个 operator):对**显式启用的侧**做 `op(cand端, q端)`;
420
- 一侧未给 operator 但**给了该侧查询端点**时,用 B1 缺省(起点 `gte` / 终点 `lte`);
421
- 该侧查询端点也没有 → **不约束该侧**。
422
-
423
- 「未给 operator 且未给查询端点 → 不约束」是刻意的:若一律回落到 B1 缺省再比较,
424
- `_op_ok(cand, None, op)` 恒伪,于是 `start_operator="gte" + start_time=T`
425
- (单端算子,合法调用)会静默返回**空集**——把「只筛起点」误答成「没有匹配」。
426
- 端点模式下候选的该侧端点不可解析 → `False`(该侧无法比较,不猜)。
427
-
428
- 候选窗口两端皆不可解析 → `False`(该节点「无时间轴可判」)——是否因此剔除
429
- 由**轴的策略**决定(效力轴 fail-open / 观察轴 fail-closed,见候选过滤处)。
430
- """
431
- cs, ce = parse_time(cand_start), parse_time(cand_end)
432
- if cs is None and ce is None:
433
- return False
434
- qs, qe = parse_time(q_start), parse_time(q_end)
435
- if start_op is None and end_op is None:
436
- if qs is not None and ce is not None and ce < qs:
437
- return False
438
- if qe is not None and cs is not None and cs > qe:
439
- return False
440
- return True
441
- if start_op is not None or qs is not None:
442
- if not _op_ok(cs, qs, start_op or "gte"):
443
- return False
444
- if end_op is not None or qe is not None:
445
- if not _op_ok(ce, qe, end_op or "lte"):
446
- return False
447
- return True
448
-
449
-
450
- def check_time_args(start_time=None, end_time=None, start_operator=None,
451
- end_operator=None, time_axis=None):
452
- """时间算子入参 fail-closed 校验 → `(enabled, axis, why)`。
453
-
454
- **入口(`mdcg.search` / `entity_contexts`)与库层共用的唯一校验点**
455
- (避免两处各写一套、口径漂移)。三则误用一律拒(`why` 非空即应抛
456
- `ValueError`,不静默忽略——与 `_stg_call` 同风格):
457
-
458
- 1. 只给 operator 而不给对应的 `start_time`/`end_time`;
459
- 2. `time_axis` 非 `{"effective","observed"}`(含 `"believed"`);
460
- 3. `start_time > end_time`。
461
-
462
- 未启用(五参全 `None`)→ `(False, None, "")`(默认路径零变更)。
463
- """
464
- if time_axis is not None and str(time_axis).strip().lower() not in TIME_AXES:
465
- return False, None, (f"未知 time_axis {time_axis!r}(允许:{TIME_AXES})——"
466
- "轴必须显式且合法,静默按另一条轴过滤会产出无法复算的结果")
467
- for op, val, nm in ((start_operator, start_time, "start_operator/start_time"),
468
- (end_operator, end_time, "end_operator/end_time")):
469
- if op is None:
470
- continue
471
- if str(op).strip().lower() not in TIME_OPERATORS:
472
- return False, None, (f"未知算子 {op!r}(允许:{TIME_OPERATORS})")
473
- if val is None:
474
- return False, None, f"给了 {nm.split('/')[0]} 但缺 {nm.split('/')[1]}(不猜默认值)"
475
- s, e = parse_time(start_time), parse_time(end_time)
476
- if start_time is not None and s is None:
477
- return False, None, f"start_time 不可解析:{start_time!r}"
478
- if end_time is not None and e is None:
479
- return False, None, f"end_time 不可解析:{end_time!r}"
480
- if s is not None and e is not None and s > e:
481
- return False, None, f"start_time({s}) > end_time({e}):空窗口,拒绝"
482
- enabled = any(x is not None for x in (start_time, end_time,
483
- start_operator, end_operator))
484
- if not enabled:
485
- return False, None, ""
486
- return True, time_axis_of(time_axis), ""
487
-
488
-
489
- def window_matches_node(fm, axis, q_start, q_end, start_op=None, end_op=None):
490
- """候选节点按轴取窗后做 `window_match` → `(matched, missing)`。
491
-
492
- `missing=True` 表示该节点在**该轴上不可判定**(窗口两端皆 `None`)——
493
- 调用方据此执行轴策略:效力轴 fail-open(保留)、观察轴 fail-closed
494
- (剔除并计入 `axis_missing`)。策略按**保证强度**定,不按「一致好看」定:
495
- 效力轴字段是可选声明,观察轴字段由写入侧保证存在。
496
- """
497
- cs, ce = time_window_of(fm, axis)
498
- if cs is None and ce is None:
499
- return False, True
500
- return window_match(cs, ce, q_start, q_end, start_op, end_op), False
501
-
502
-
503
- def time_filter_meta(axis=None, mode="overlap", start=None, end=None,
504
- start_operator=None, end_operator=None, dropped=0,
505
- axis_missing=0, applied=False) -> dict:
506
- """`meta["time_filter"]` 审计块(**五键齐备**,可复算):
507
-
508
- `dropped + 存活数 == 候选数` 由调用方保证;`axis_missing` 单独记账,
509
- 使「观察轴缺字段被剔除」的条数可查(静默放行会把数据异常藏起来)。
510
- """
511
- return {"axis": axis, "mode": mode, "start": start, "end": end,
512
- "start_operator": start_operator, "end_operator": end_operator,
513
- "dropped": int(dropped), "axis_missing": int(axis_missing),
514
- "applied": bool(applied)}
515
-
516
-
517
- def filter_by_time(entries, axis, q_start, q_end, start_op=None, end_op=None):
518
- """按轴过滤候选 → `(kept, dropped, axis_missing)`。**候选层唯一过滤点**。
519
-
520
- 轴策略按**字段保证强度**定(不按「行为一致好看」定):
521
- · `effective` 效力轴:`effective_from/until` 是**可选声明**(多数节点没有)
522
- → 不可判定一律 fail-open(保留)——否则「没写时效 = 被过滤掉」,
523
- 会把沉默当否认;
524
- · `observed` 观察轴:`temporal` / `time_window` 由**写入侧保证**存在
525
- (add 缺省填观测窗)→ 不可判定一律 fail-closed(剔除并计入
526
- `axis_missing`,使数据异常可查而非静默放行)。
527
- """
528
- kept, dropped, missing = [], 0, 0
529
- for e in entries:
530
- m, miss = window_matches_node(e, axis, q_start, q_end, start_op, end_op)
531
- if miss:
532
- if axis == "observed":
533
- dropped += 1
534
- missing += 1
535
- else:
536
- kept.append(e)
537
- continue
538
- if m:
539
- kept.append(e)
540
- else:
541
- dropped += 1
542
- return kept, dropped, missing
543
-
544
-
545
- # ---------------------------------------------------------------- 依赖声明
546
-
547
- def as_deps(value) -> list:
548
- """把单值 / 序列 / 逗号串统一成**去重、排序**的节点 id 列表(上限 MAX_DEPS)。
549
-
550
- 容错口径(写路径的入参形态不由本模块决定,收窄会误伤):
551
- · `None` / 空串 / 纯空白项 → 丢弃(不是目标,也不是「声明了依赖」);
552
- · 半角/全角逗号串 → 切分(`"a, b"` 与 `["a", "b"]` 等价);
553
- · 排序的动机:依赖是**集合语义**(声明顺序不承载信息),排序让 fm 与
554
- 索引快照对同一组依赖恒等——比较/去重/审计不再依赖声明顺序。
555
- """
556
- if value is None:
557
- return []
558
- items = list(value) if isinstance(value, (list, tuple, set)) else [value]
559
- out = []
560
- for x in items:
561
- if x is None:
562
- continue
563
- for s in str(x).replace(",", ",").split(","):
564
- s = s.strip()
565
- if s and s not in out:
566
- out.append(s)
567
- out.sort()
568
- return out[:MAX_DEPS]
569
-
570
-
571
- def deps_of(cg, nid: str) -> list:
572
- """读某节点声明的依赖(**零读文件**:索引快照缺失才回读节点)。"""
573
- e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid) or {}
574
- deps = as_deps(e.get(DEPS_FIELD))
575
- if deps:
576
- return deps
577
- node = cg.get(nid)
578
- if not node:
579
- return []
580
- return as_deps((node.get("frontmatter") or {}).get(DEPS_FIELD))
581
-
582
-
583
- def invalidate_cache(cg) -> None:
584
- """作废反查缓存(写/删节点后调用;下次读时增量重建)。"""
585
- try:
586
- cg.__dict__.pop("_trust_dependents", None)
587
- except Exception: # noqa: BLE001
588
- pass
589
-
590
-
591
- def dependents_index(cg, refresh: bool = False) -> dict:
592
- """反查索引:`{被依赖节点: [依赖它的节点…]}`(「谁依赖我」)。
593
-
594
- 缓存挂在 cg 实例上(与 `subgraph` 的缓存化做法同构);`refresh=True` 强制重建。
595
- """
596
- if not refresh:
597
- cached = getattr(cg, "_trust_dependents", None)
598
- if isinstance(cached, dict):
599
- return cached
600
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
601
- idx = {}
602
- for nid, e in nodes.items():
603
- for p in as_deps((e or {}).get(DEPS_FIELD)):
604
- idx.setdefault(p, []).append(nid)
605
- for k in idx:
606
- idx[k].sort()
607
- try:
608
- cg._trust_dependents = idx
609
- except Exception: # noqa: BLE001
610
- pass
611
- return idx
612
-
613
-
614
- def _record(cg, rec: dict) -> None:
615
- """台账追加(best-effort:失败静默,**绝不阻断写路径**)。"""
616
- try:
617
- append_jsonl(os.path.join(cg.root, AUDIT_FILE), rec)
618
- except Exception: # noqa: BLE001
619
- pass
620
-
621
-
622
- # ---------------------------------------------------------------- 推进
623
-
624
- def _sync_index(cg, node_id, fm) -> None:
625
- """把验证态同步进索引快照(免读文件可查);无索引实现时静默跳过。"""
626
- idx = getattr(cg, "index", None)
627
- if not isinstance(idx, dict):
628
- return
629
- e = (idx.get("nodes") or {}).get(node_id)
630
- if e is None:
631
- return
632
- e[STATE_FIELD] = fm.get(STATE_FIELD)
633
- dirty = getattr(cg, "_dirty", None)
634
- if isinstance(dirty, dict):
635
- dirty[node_id] = e
636
- flush = getattr(cg, "flush", None)
637
- if callable(flush):
638
- flush()
639
-
640
-
641
- def set_state(cg, node_id: str, dst: str, reason: str = None, actor: str = None,
642
- evidence: str = None, method: str = None, trigger: str = None,
643
- override: bool = False) -> dict:
644
- """推进一个节点的验证态(**唯一推进入口**)。
645
-
646
- 非法迁移不走异常而是返回 `{"ok": False, "error": <code>, ...}`(负路由)。
647
- 幂等迁移返回 `changed=False` 且不写盘。
648
- """
649
- node = cg.get(node_id)
650
- if not node:
651
- return {"ok": False, "error": "node_not_found", "node_id": node_id}
652
- fm = dict(node.get("frontmatter") or {})
653
- src = state_of(fm)
654
- ok, code, why = stamp(fm, dst, reason=reason, actor=actor, evidence=evidence,
655
- method=method, trigger=trigger, override=override)
656
- base = {"node_id": node_id, "from": src, "to": dst, "code": code}
657
- if not ok:
658
- return {**base, "ok": False, "error": code, "reason": why}
659
- if code == "noop":
660
- return {**base, "ok": True, "changed": False, "reason": why}
661
- at = fm[HISTORY_FIELD][-1]["at"]
662
- cg._write_node(node_id, os.path.join(cg.root, node["path"]), fm,
663
- node.get("content") or "")
664
- _sync_index(cg, node_id, fm)
665
- _record(cg, {"t": at, "action": "set_state", "node_id": node_id,
666
- "from": src, "to": dst, "reason": reason, "actor": actor,
667
- "method": method, "trigger": trigger, "override": bool(override)})
668
- if fm.get(DEPS_FIELD):
669
- invalidate_cache(cg)
670
- return {**base, "ok": True, "changed": True, "reason": why, "at": at}
671
-
672
-
673
- # ---------------------------------------------------------------- 失效传播
674
-
675
- def mark_dependents(cg, node_id: str, reason: str = None, *, apply: bool = True,
676
- actor: str = "system", trigger: str = None,
677
- depth: int = 1, override: bool = False) -> dict:
678
- """**一跳同步传播**:把直接下游标为 `doubted`(异常的新定义)。
679
-
680
- 根因口径:异常不再仅是「内容错」,而是「**它所依赖的地基动了**」——被依赖
681
- 单元被修改/证伪时,直接下游立刻产生存疑标记,无需等全库巡检。
682
-
683
- **永不抛**(对齐 provenance G8 纪律:传播失败不得阻断写入)。`apply=False`
684
- 只预演;同状态下游是 no-op(幂等,不写盘、不留痕)。
685
- """
686
- try:
687
- dep_map = dependents_index(cg)
688
- kids = list(dep_map.get(node_id, []))
689
- kids.sort()
690
- if not kids:
691
- return {"ok": True, "changed": 0, "dependents": [], "apply": bool(apply),
692
- "reason": "no_dependents"}
693
- reason = reason or f"上游 {node_id} 变动"
694
- planned = [k for k in kids
695
- if state_of((cg.index or {}).get("nodes", {}).get(k) or {})
696
- != "doubted"]
697
- if not apply:
698
- return {"ok": True, "dry_run": True, "changed": 0,
699
- "dependents": kids, "planned": len(planned)}
700
- done, skipped = [], []
701
- for k in kids:
702
- r = set_state(cg, k, "doubted", reason=reason, actor=actor,
703
- trigger=trigger or f"dep_changed:{node_id}",
704
- override=override)
705
- if r.get("ok") and r.get("changed"):
706
- done.append(k)
707
- elif r.get("ok"):
708
- skipped.append(k) # 已是 doubted:幂等
709
- else:
710
- skipped.append(f"{k}({r.get('error')})")
711
- return {"ok": True, "changed": len(done), "dependents": kids,
712
- "updated": done, "skipped": skipped, "apply": True,
713
- "reason": reason}
714
- except Exception as exc: # noqa: BLE001
715
- _record(cg, {"t": time.time(), "action": "mark_dependents_degraded",
716
- "node_id": node_id, "error": f"{type(exc).__name__}: {exc}",
717
- "actor": actor})
718
- return {"ok": False, "degraded": True, "changed": 0,
719
- "error": f"{type(exc).__name__}: {exc}"}
720
-
721
-
722
- def propagate(cg, *, apply: bool = False, max_nodes: int = MAX_NODES_DEFAULT,
723
- actor: str = "patrol") -> dict:
724
- """**多跳异步重算**(巡检面,不阻塞写入/查询)。
725
-
726
- BFS 从所有 `expired` / `rechecking` / 显式根源节点出发,把可达下游标
727
- `doubted`(**只降不升**:复核通过须由人显式 `set_state(verified)`)。
728
- 默认 `apply=False` 只出报表;`max_nodes` 封顶防大库爆炸。
729
-
730
- `doubted` 中间节点的语义(2026-09-20 v14 缺陷 B 修复):**穿过,但不重复
731
- 标记**。已 doubted 说明它已被标记,无需再标(幂等);但它**不是传播终点**
732
- ——「地基动了」的波及范围必须继续往其下游走。旧实现把 doubted 直接
733
- `continue`(当作终点),于是「先 `mark_dependents` 预演直接下游、再
734
- `propagate` 正式传播」这一最常见序列会使多跳传播**整体失效**
735
- (`reachable`/`updated` 全 0)且**无任何报错**。穿过的节点在
736
- `passed_doubted` 中如实透出(可审计)。
737
-
738
- **`reachable` / `updated` 的口径**(2026-09-20 v15-4 澄清,勿读成「波及集
739
- 大小」):两者计的是「本次**需要新标记**的节点数」——`apply=False` 时 =
740
- 计划标记数(`planned` 的长度),`apply=True` 时 = 实际标记成功数(`done` 的
741
- 长度)。**穿过的 `doubted` 中继不计入**(它早已被标记,本次幂等跳过)。故
742
- 「本次的完整波及集」= `reachable`(或 `updated`)+ `passed_doubted`,只读
743
- 前者会低估。`roots` 是传播起点(`expired`/`rechecking`),不属于波及集。
744
- """
745
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
746
- dep_map = dependents_index(cg)
747
- roots = []
748
- for nid, e in nodes.items():
749
- st = (e or {}).get(STATE_FIELD)
750
- if st in ("expired", "rechecking"):
751
- roots.append(nid)
752
- roots.sort()
753
- seen, waves, passed = set(roots), [], set()
754
- frontier, depth = list(roots), 0
755
- while frontier and len(seen) < int(max_nodes):
756
- depth += 1
757
- nxt = []
758
- for nid in frontier:
759
- for k in dep_map.get(nid, []):
760
- if k in seen:
761
- continue
762
- seen.add(k)
763
- if state_of(nodes.get(k) or {}) == "doubted":
764
- passed.add(k) # 穿过但不重复标记(不是终点)
765
- nxt.append(k)
766
- if len(seen) >= int(max_nodes):
767
- break
768
- for k in nxt:
769
- if k in passed:
770
- continue # 已 doubted:无需再标(幂等)
771
- waves.append({"node_id": k, "depth": depth,
772
- "via": sorted(n for n in dep_map if k in dep_map[n]
773
- and n in seen)})
774
- frontier = nxt
775
- if not apply:
776
- return {"ok": True, "dry_run": True, "roots": roots[:50],
777
- "roots_count": len(roots), "reachable": len(waves),
778
- "planned": waves[:50], "passed_doubted": sorted(passed)[:20],
779
- "max_nodes": int(max_nodes),
780
- "note": "预演:未改盘;只降不升(复核须显式)"}
781
- done, skipped = [], []
782
- for w in waves:
783
- r = set_state(cg, w["node_id"], "doubted",
784
- reason="多跳失效传播(上游时间轴/证据失效)",
785
- actor=actor, trigger=f"propagate:d{w['depth']}")
786
- if r.get("ok") and r.get("changed"):
787
- done.append(w["node_id"])
788
- elif not r.get("ok"):
789
- skipped.append(f"{w['node_id']}({r.get('error')})")
790
- return {"ok": True, "dry_run": False, "roots": roots[:50],
791
- "roots_count": len(roots), "updated": done, "updated_count": len(done),
792
- "passed_doubted": sorted(passed)[:20],
793
- "skipped": skipped[:20], "max_nodes": int(max_nodes)}
794
-
795
-
796
- # ---------------------------------------------------------------- 巡检 / 回填
797
-
798
- # 生效条件:cg 传入即只读遍历 index 快照并返回悬空/未生效/已过期/存疑四类计数与样本,不写盘、不改状态、不抛(index 缺失按空库处理);
799
- def patrol(cg, *, limit: int = 20, now: float = None) -> dict:
800
- """只读巡检:悬空依赖 / 时间轴失效 / 存疑积压。**不改状态、不删节点**。
801
-
802
- 命名纪律(负记忆,2026-09-19 实测):本函数**不得叫 `check`**——裁决层已有
803
- `check(src, dst, ...)`(唯一裁决点)。同模块内同名二次定义会**静默遮蔽**前者,
804
- `stamp()` 随即调到巡检版并抛 `TypeError`(现场:`set_verification` 整条链断)。
805
- 两者语义分属不同层次(纯函数裁决 vs 全库只读巡检),名字必须分开。
806
- """
807
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
808
- known = set(nodes)
809
- dangling, expired, not_yet, doubted = [], [], [], []
810
- for nid, e in sorted(nodes.items()):
811
- for p in as_deps((e or {}).get(DEPS_FIELD)):
812
- if p not in known:
813
- dangling.append({"node_id": nid, "missing": p})
814
- st = (e or {}).get(STATE_FIELD)
815
- if st == "doubted":
816
- doubted.append(nid)
817
- # 时间轴按索引可判定字段粗判(精确判定需 fm,巡检读面只给候选)
818
- kind = _index_validity(e, now=now)
819
- if kind == "expired":
820
- expired.append(nid)
821
- elif kind == "not_yet":
822
- not_yet.append(nid)
823
- return {
824
- "ok": not dangling,
825
- "checked": True, "root": cg.root, "nodes": len(known),
826
- "dangling": dangling[:int(limit)], "dangling_count": len(dangling),
827
- "expired": expired[:int(limit)], "expired_count": len(expired),
828
- "not_yet": not_yet[:int(limit)], "not_yet_count": len(not_yet),
829
- "doubted": doubted[:int(limit)], "doubted_count": len(doubted),
830
- "readonly": True,
831
- "note": "只读巡检:悬空依赖/时效仅检出并报告,不自动改状态(处置由人/巡检动作发起)"}
832
-
833
-
834
- def _index_validity(e, now: float = None) -> str:
835
- """索引快照口径的时效粗判(索引缺时间字段时返回 unknown,零读文件)。"""
836
- if not isinstance(e, dict):
837
- return "unknown"
838
- return validity(e, now=now)[0]
839
-
840
-
841
- def backfill(cg, apply: bool = False, limit: int = 5000) -> dict:
842
- """存量回填:把缺 `verification_state` 的节点显式补成 `unverified`(幂等)。
843
-
844
- 缺字段本就按 unverified 解释(`state_of`),故回填**不是让功能工作的前提**,
845
- 而是把「缺省」变成「显式」——索引/审计里从此可直接读到验证态。默认只盘点。
846
- """
847
- nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
848
- missing = []
849
- for nid, e in list(nodes.items()):
850
- if len(missing) >= limit:
851
- break
852
- if (e or {}).get(STATE_FIELD) in STATES:
853
- continue
854
- missing.append(nid)
855
- missing.sort()
856
- if not apply:
857
- return {"ok": True, "dry_run": True, "scanned": len(nodes),
858
- "missing": len(missing), "planned": missing[:50]}
859
- done, failed = [], []
860
- for nid in missing:
861
- node = cg.get(nid)
862
- if not node:
863
- failed.append(nid)
864
- continue
865
- fm = dict(node.get("frontmatter") or {})
866
- fm[STATE_FIELD] = state_of(fm)
867
- cg._write_node(nid, os.path.join(cg.root, node["path"]), fm,
868
- node.get("content") or "")
869
- _sync_index(cg, nid, fm)
870
- done.append(nid)
871
- return {"ok": True, "dry_run": False, "scanned": len(nodes),
872
- "missing": len(missing), "backfilled": len(done),
873
- "planned": done[:50], "failed": failed[:20]}
874
-
875
-
876
- def describe(cg, node_id: str, now: float = None) -> dict:
877
- """单节点验证态全貌(供 op=status / 状态头渲染)。只读、失败不抛。"""
878
- try:
879
- node = cg.get(node_id)
880
- except Exception: # noqa: BLE001
881
- node = None
882
- e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
883
- if not node and not e:
884
- return {"ok": False, "error": "node_not_found", "node_id": node_id}
885
- fm = (node or {}).get("frontmatter") or {}
886
- deps = as_deps(fm.get(DEPS_FIELD)) or as_deps((e or {}).get(DEPS_FIELD))
887
- kind, start, end = validity(fm if fm else (e or {}), now=now)
888
- dep_map = dependents_index(cg)
889
- hist = list(fm.get(HISTORY_FIELD) or [])[-5:]
890
- return {"ok": True, "node_id": node_id,
891
- "verification_state": state_of(fm if fm else (e or {})),
892
- "verification_basis": fm.get("verification_basis"),
893
- "verification_method": fm.get("verification_method"),
894
- "verified_at": fm.get("verified_at"),
895
- "depends_on": deps,
896
- "depended_by": dep_map.get(node_id, []),
897
- "validity": {"kind": kind, "valid_from": fm.get(FROM_FIELD),
898
- "valid_until": fm.get(UNTIL_FIELD),
899
- "start": start, "end": end},
900
- "history": hist,
901
- "lifecycle_state": fm.get("lifecycle_state"),
902
- "protected": bool(fm.get("protected") or fm.get("immutable"))}
903
-
904
-
905
- def summary(cg) -> dict:
906
- """轻量摘要(只读;失败不抛,避免拖垮 health / 常驻循环)。"""
907
- try:
908
- rep = patrol(cg, limit=3)
909
- return {"dangling": rep["dangling_count"], "expired": rep["expired_count"],
910
- "not_yet": rep["not_yet_count"], "doubted": rep["doubted_count"],
911
- "nodes": rep["nodes"], "sample": [
912
- f"{r['node_id']}->?{r['missing']}" for r in rep["dangling"]]}
913
- except Exception: # noqa: BLE001
914
- return {}
915
-
916
-
917
- def load_ledger(cg, *, node_id: str = None, limit: int = None) -> list:
918
- """读验证态台账(跳过坏行;可按节点过滤)。"""
919
- path = os.path.join(cg.root, AUDIT_FILE)
920
- out = []
921
- for r in read_jsonl(path):
922
- if not isinstance(r, dict):
923
- continue
924
- if node_id and r.get("node_id") != node_id:
925
- continue
926
- out.append(r)
927
- return out[:int(limit)] if limit else out
928
-
929
-
930
- def catalog(root: str = None) -> dict:
931
- """自描述(供 MCP catalog / 人工核对)。"""
932
- return {
933
- "layer": "可验证记忆单元",
934
- "question": "它还成不成立(验证态 + 依赖 + 时效)",
935
- "ledger": os.path.join(root, AUDIT_FILE) if root else AUDIT_FILE,
936
- "states": list(STATES),
937
- "state_field": STATE_FIELD,
938
- "history_field": HISTORY_FIELD,
939
- "deps_field": DEPS_FIELD,
940
- "time_fields": [FROM_FIELD, UNTIL_FIELD],
941
- "transitions": sorted(f"{a}→{b}" for a, b in TRANSITIONS),
942
- "max_deps": MAX_DEPS,
943
- "max_nodes_default": MAX_NODES_DEFAULT,
944
- "discipline": {"never_block_write": True, "no_backfill": True,
945
- "patrol_readonly": True, "one_hop_sync": True,
946
- "multi_hop_async": True, "only_downgrade": True},
947
- "distinct_from": ("lifecycle.py = 节点生命周期(lifecycle_state);"
948
- "provenance.py = 派生血缘(_link.jsonl);"
949
- "本层 = 验证态与依赖(verification_state/_trust.jsonl)"),
950
- }
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · 可验证记忆单元(验证态状态机 + 依赖图 + 双时间轴)
3
+
4
+ 把「可信度」从散文式描述,变成**显式、可查询、可传播、可输出**的结构化状态。
5
+ 三件事:
6
+
7
+ 1. **验证态持续化**——`verification_state` ∈ (unverified / verified /
8
+ expired / doubted / rechecking),随验证动作与依赖变动**流转**,不是一次性
9
+ 布尔开关;状态落 `frontmatter.verification_state`(节点 md 是唯一真源),
10
+ 索引快照同名透出(免读文件可查)。
11
+ 2. **依赖声明与失效传播**——单元显式声明它依赖谁(`depends_on`);被依赖单元
12
+ 被修改/证伪时,**直接下游立即同步标为 doubted**(一跳,低成本);**多跳**
13
+ 交由 `propagate()` 后台巡检重算,不阻塞写入与查询。
14
+ 3. **双时间轴**——既有「何时不再成立」(`valid_until`)之外补「何时开始成立」
15
+ (`valid_from`),使时效区间可判定:未生效 / 生效中 / 已过期。
16
+
17
+ 与 `lifecycle.py` 刻意分层(同构实现、**正交语义**,禁跨套复用常量):
18
+ · `lifecycle.py` = 节点**生命周期**(active/converged/demoted/archived);
19
+ · 本模块 = 节点**验证态**(unverified/verified/expired/doubted/rechecking)。
20
+ 两者都是「单点裁决 + 单点推进 + 硬拒入口」,但状态集不共享、`_RANK` 不共享。
21
+
22
+ 与 `provenance.py` 刻意分层:
23
+ · `provenance.py` = 节点**派生血缘**(「它由谁派生」,`_link.jsonl`);
24
+ · 本模块 = 节点**依赖与证据强度**(「它靠谁成立、现在还成不成立」,
25
+ `_trust.jsonl`)。血缘是既成事实,依赖是可失效假设——不可混用。
26
+
27
+ 三条纪律(对齐 G8 裁定与 lifecycle 口径):
28
+ 1. **台账失败降级不阻断写**——`mark_dependents()` 永不抛,写不进只留痕告警;
29
+ 2. **历史不回填**——存量缺 `verification_state` 视为 `unverified`,`backfill()`
30
+ 只在显式调用时把「缺省」变「显式」;
31
+ 3. **巡检只读**——`patrol()` 检出悬空依赖/过期节点但**不自动改状态、不删节点**。
32
+
33
+ 命名避让(既有先例):字段**不叫 `state`**——该名已被裁决四态
34
+ (ACCEPT/REJECT/DEFER/BLINDSPOT)占用,`lifecycle_state` 的先例同此动机。
35
+
36
+ 零第三方依赖(D-005)。
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import os
41
+ import time
42
+
43
+ from .fsutil import append_jsonl, read_jsonl
44
+
45
+ #: 验证态全集。语义:
46
+ #: unverified 未验证(默认;存量缺字段即视为它)
47
+ #: verified 已验证(有验证方式与证据,且依赖未动)
48
+ #: doubted 存疑(**依赖的地基动了**——异常的新定义)
49
+ #: rechecking 重新验证中(复核进行态)
50
+ #: expired 已过期(超出时效区间,或证据整体失效)
51
+ STATES = ("unverified", "verified", "doubted", "rechecking", "expired")
52
+
53
+ #: frontmatter 字段名(索引快照同名透出)。刻意避开 `state`——与 lifecycle 同动机。
54
+ STATE_FIELD = "verification_state"
55
+ #: 验证履历字段(frontmatter 侧历史,滚动保留最近 HISTORY_KEEP 条)
56
+ HISTORY_FIELD = "verification_history"
57
+ HISTORY_KEEP = 20
58
+ #: 依赖声明字段(CCG「子功能」槽的落字段)
59
+ DEPS_FIELD = "depends_on"
60
+ #: 双时间轴(**效力时间**)字段。2026-09-19 阶段一:规范名迁移到
61
+ #: `effective_from` / `effective_until`(**新写入落规范键**),历史名
62
+ #: `valid_from` / `valid_until` 保留为**读取侧回落别名**(存量不迁移、零破坏)。
63
+ FROM_FIELD = "valid_from"
64
+ UNTIL_FIELD = "valid_until"
65
+ #: 规范名(新写入落此;读取时优先于同名族旧键)
66
+ EFFECTIVE_FROM_FIELD = "effective_from"
67
+ EFFECTIVE_UNTIL_FIELD = "effective_until"
68
+ #: 信念时间(体系**何时确认此条**):取代/审核的锚。
69
+ #: **不是效力语义**——它既不是「已结束」也不是「尚未开始」,
70
+ #: 故绝不并入 `scrub._EXPIRY_KEYS`(已结束族)或 `scrub._NOT_YET_KEYS`(未生效族)。
71
+ BELIEVED_FIELD = "believed_at"
72
+ #: 过期时刻(写盘冗余:由 `effective_until` 派生落盘,供审计/对账直读)。
73
+ EXPIRED_FIELD = "expired_at"
74
+
75
+ #: 端点取值优先级(**规范键优先、别名回落**,2026-09-19 阶段一)。
76
+ #: 与 `scrub._NOT_YET_KEYS` / `scrub._EXPIRY_KEYS` 同键族——两处共用同一套别名,
77
+ #: 任一侧新增键须同步另一侧(交叉守卫测试 test_validity_filter 守住)。
78
+ FROM_ALIASES = (EFFECTIVE_FROM_FIELD, FROM_FIELD, "valid_since", "starts_at")
79
+ UNTIL_ALIASES = (EFFECTIVE_UNTIL_FIELD, UNTIL_FIELD,
80
+ "expires_at", "expire_at", "expiry", "deadline")
81
+ #: 验证态迁移审计(append-only,与 _lifecycle.jsonl / _maintain.jsonl 同风格)
82
+ AUDIT_FILE = "_trust.jsonl"
83
+
84
+ #: 降级序位(用于「受保护豁免降级」判定)。verified 最高,doubted/expired 更低。
85
+ _RANK = {"verified": 0, "rechecking": 1, "unverified": 1, "doubted": 2, "expired": 3}
86
+
87
+ #: 合法迁移表。刻意不收录的(即拒绝的):
88
+ #: verified → unverified(回落未验证:证据已存在,只应「存疑」而非「清零」)
89
+ #: doubted → expired(跳级:存疑未经复核不得直接判过期)
90
+ #: 注:`unverified → doubted` **收录**——两条路都真实到达这里:①上游变动波及一个
91
+ #: 从未验证过的下游(「地基动了」对未验证者同样成立);②未验证节点收到**负证据**
92
+ #: (证据削弱本身就是存疑的依据,比停在「未验证」更准确)。
93
+ TRANSITIONS = frozenset({
94
+ ("unverified", "verified"),
95
+ ("unverified", "doubted"),
96
+ ("unverified", "rechecking"),
97
+ ("unverified", "expired"),
98
+ ("verified", "doubted"),
99
+ ("verified", "rechecking"),
100
+ ("verified", "expired"),
101
+ ("doubted", "rechecking"),
102
+ ("doubted", "verified"),
103
+ ("rechecking", "verified"),
104
+ ("rechecking", "doubted"),
105
+ ("rechecking", "unverified"),
106
+ ("rechecking", "expired"),
107
+ ("expired", "rechecking"),
108
+ ("expired", "verified"),
109
+ })
110
+
111
+ #: 依赖边上限(单节点直接下游数);超出即拒收,防「依赖声明写成噪声」。
112
+ MAX_DEPS = 32
113
+ #: 多跳传播的节点上限(对齐 subgraph.MAX_NODES_DEFAULT 的量级)
114
+ MAX_NODES_DEFAULT = 500
115
+
116
+
117
+ class TrustError(Exception):
118
+ """非法验证态迁移 / 依赖声明非法(写路径用 `require_transition` 直接抛)。"""
119
+
120
+ def __init__(self, src, dst, code, reason):
121
+ super().__init__(f"非法验证态迁移 {src}→{dst}({code}):{reason}")
122
+ self.src, self.dst, self.code, self.reason = src, dst, code, reason
123
+
124
+
125
+ # ---------------------------------------------------------------- 纯函数裁决
126
+
127
+ def state_of(fm) -> str:
128
+ """frontmatter → 验证态;缺字段或未知值 → "unverified"(存量兼容,不猜测)。"""
129
+ if not isinstance(fm, dict):
130
+ return "unverified"
131
+ s = fm.get(STATE_FIELD)
132
+ return s if s in STATES else "unverified"
133
+
134
+
135
+ def is_downgrade(src: str, dst: str) -> bool:
136
+ """是否向「更低可信度」迁移(verified < unverified/rechecking < doubted < expired)。"""
137
+ return _RANK.get(dst, 0) > _RANK.get(src, 0)
138
+
139
+
140
+ def can_transition(src, dst) -> bool:
141
+ """迁移是否合法(含幂等;未知 src 按 unverified 处理)。"""
142
+ src = src if src in STATES else "unverified"
143
+ if dst not in STATES:
144
+ return False
145
+ if src == dst:
146
+ return True # 幂等:同状态是 no-op
147
+ return (src, dst) in TRANSITIONS
148
+
149
+
150
+ def check(src, dst, protected: bool = False, override: bool = False):
151
+ """迁移合法性裁决 → `(ok, code, reason)`。**唯一裁决点**(纯函数,无 IO)。
152
+
153
+ code 取值:`ok` / `noop`(同状态,不写盘)/ `unknown_state` /
154
+ `illegal_transition` / `protected`。
155
+ """
156
+ src = src if src in STATES else "unverified"
157
+ if dst not in STATES:
158
+ return False, "unknown_state", f"未知验证态 {dst!r}(允许:{STATES})"
159
+ if src == dst:
160
+ return True, "noop", "验证态未变(幂等,不写盘)"
161
+ if (src, dst) not in TRANSITIONS:
162
+ legal = "、".join(f"{a}→{b}" for a, b in sorted(TRANSITIONS))
163
+ return False, "illegal_transition", (
164
+ f"{src}→{dst} 不在合法迁移表内(允许:{legal})——"
165
+ "验证态只沿「验证/存疑/复核/过期」四条路径流转,跨级迁移一律拒绝")
166
+ if is_downgrade(src, dst) and protected and not override:
167
+ return False, "protected", (
168
+ f"受保护节点不接受降级迁移 {src}→{dst}(保护 = 不可轻弃);"
169
+ "确需降级请显式 override=True")
170
+ return True, "ok", f"{src}→{dst} 合法"
171
+
172
+
173
+ def require_transition(src, dst, protected: bool = False, override: bool = False):
174
+ """合法则返回 code,非法则抛 `TrustError`(写路径的硬拒入口)。"""
175
+ ok, code, why = check(src, dst, protected=protected, override=override)
176
+ if not ok:
177
+ raise TrustError(src if src in STATES else "unverified", dst, code, why)
178
+ return code
179
+
180
+
181
+ def stamp(fm: dict, dst: str, reason: str = None, actor: str = None,
182
+ evidence: str = None, method: str = None, trigger: str = None,
183
+ override: bool = False):
184
+ """在给定 frontmatter 上**就地**推进验证态(无 IO)→ `(ok, code, why)`。
185
+
186
+ 与 `set_state()` 共用同一裁决点(`check`),供**需与其他字段同批落盘**的
187
+ 收口点使用(写入时一次写盘改依赖/时间轴/验证态)。
188
+ """
189
+ src = state_of(fm)
190
+ prot = bool(fm.get("protected") or fm.get("immutable"))
191
+ ok, code, why = check(src, dst, protected=prot, override=override)
192
+ if not ok or code == "noop":
193
+ return ok, code, why
194
+ fm[STATE_FIELD] = dst
195
+ rec = {"at": time.time(), "from": src, "to": dst, "reason": reason,
196
+ "actor": actor, "trigger": trigger}
197
+ if method:
198
+ rec["method"] = str(method)[:120]
199
+ if evidence:
200
+ rec["evidence"] = str(evidence)[:300]
201
+ hist = list(fm.get(HISTORY_FIELD) or [])
202
+ hist.append(rec)
203
+ fm[HISTORY_FIELD] = hist[-HISTORY_KEEP:]
204
+ if method:
205
+ fm["verification_method"] = str(method)[:120]
206
+ if evidence:
207
+ fm["verification_evidence"] = str(evidence)[:300]
208
+ fm["verified_at"] = rec["at"]
209
+ return True, code, why
210
+
211
+
212
+ # ---------------------------------------------------------------- 双时间轴
213
+
214
+ #: 单位归一判据:epoch **秒**到公元 5138 年仍 < 1e11,而 epoch **毫秒**自 1973-03
215
+ #: 起已 > 1e11 —— 量级差 3 个数量级,故「>= 1e11」判为毫秒。
216
+ #: 受保护的既有值:全时窗哨兵 `9999999999.0`(nodefile.FULL_TIME_WINDOW_MAX)
217
+ #: 与秒级 `created_at` 一律**不受影响**(test_trust 的「数字直通」断言同此)。
218
+ _MS_EPOCH_THRESHOLD = 1e11
219
+
220
+
221
+ def epoch_seconds(value):
222
+ """时间**数值** → epoch 秒(单位归一的**唯一实现点**);非数值 → None。
223
+
224
+ 为什么需要它(issue #23 根因):图内规范单位是**秒**——`created_at`、
225
+ `mdcg._write` 的默认观测窗、全时窗哨兵、`parse_time` 全是秒;但摄取链
226
+ (`sources.DSHSessionSource` / `JsonlSource`)历史上产出**毫秒**。毫秒值一旦
227
+ 进入 `condition_space.time_window`,`stg(op=timeline)` 的倒序排序被 1e3 倍放大
228
+ 顶到最前,摄取节点**永久占据头部**(auto-recall 取到的「最新 N 条」全是它们);
229
+ 同一根因还让 S2 时间门控把秒节点的区间判成不相交而误剔除。
230
+
231
+ 口径(不猜测):数值 `>= _MS_EPOCH_THRESHOLD` → `/1000.0`;其余原样 float。
232
+ 于是「已落库的历史毫秒数据」**无需改盘**即可参与排序与区间比较
233
+ (读取侧归一,零迁移)。
234
+
235
+ 不适用条件:日期/日期时间**字符串**(走 `parse_time`);`spatial.bbox` 等
236
+ 非时间的数值;全时窗哨兵判定(`nodefile.is_full_time_window` 按「两侧极值
237
+ 恰好覆盖」判,不做单位换算)。
238
+ """
239
+ if not isinstance(value, (int, float)):
240
+ return None
241
+ v = float(value)
242
+ return v / 1000.0 if v >= _MS_EPOCH_THRESHOLD else v
243
+
244
+
245
+ def parse_time(value):
246
+ """时间值 → epoch 秒;不可解析返回 None(**不猜测**)。
247
+
248
+ 接受:数字(epoch 秒;**毫秒自动归一**,见 `epoch_seconds`)/ 数字串
249
+ (同左)/ 日期或日期时间字符串(ISO 8601,宽松:空格分隔、`Z` 后缀、
250
+ 日期精度都容忍)。纯标准库实现。
251
+
252
+ 单位归一单点落在此处:它是全系统时间数值的**唯一读入口**
253
+ (时间算子 / `validity` / `provenance` 边 / 效力轴端点 / `stg._interval`),
254
+ 故历史毫秒数据在**所有**读取面上一次生效,不再逐调用点打补丁。
255
+ """
256
+ if value is None or value == "":
257
+ return None
258
+ if isinstance(value, (int, float)):
259
+ return epoch_seconds(value)
260
+ s = str(value).strip()
261
+ if not s:
262
+ return None
263
+ try:
264
+ return epoch_seconds(float(s))
265
+ except ValueError:
266
+ pass
267
+ t = s.replace("/", "-").replace("T", " ")
268
+ if t.endswith("Z"):
269
+ t = t[:-1].strip()
270
+ fmts = ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d")
271
+ for f in fmts:
272
+ try:
273
+ return time.mktime(time.strptime(t, f))
274
+ except ValueError:
275
+ continue
276
+ return None
277
+
278
+
279
+ def first_endpoint(fm, keys):
280
+ """按 `keys` 优先级取首个**可解析**端点 → `(epoch, key)`;全链无值 → `(None, None)`。
281
+
282
+ 单点定义「规范键优先、别名回落」的取值口径:某键**存在但不可解析**时继续回落
283
+ (而非按 None 定案)——「写坏了的时间值」不得遮蔽同族另一个合法键。
284
+ """
285
+ if not isinstance(fm, dict):
286
+ return None, None
287
+ for k in keys:
288
+ if k in fm:
289
+ ts = parse_time(fm.get(k))
290
+ if ts is not None:
291
+ return ts, k
292
+ return None, None
293
+
294
+
295
+ def validity(fm, now: float = None):
296
+ """双时间轴判定 → `(kind, start, end)`。
297
+
298
+ kind ∈ `unknown`(无时间轴约束)/ `not_yet`(未生效)/ `active`(生效中)/
299
+ `expired`(已过期)。任一端点不可解析 → 该端点按 None 处理(不猜测、不误判)。
300
+
301
+ 端点取值(2026-09-19 阶段一):**规范键优先、别名回落**——
302
+ 起点 `effective_from` > `valid_from` > `valid_since` > `starts_at`;
303
+ 终点 `effective_until` > `valid_until` > `expires_at` > `expire_at` > `expiry` > `deadline`。
304
+ `believed_at`(信念时间)**不参与**本判定:它是「体系何时确认此条」(取代/审核的锚),
305
+ 不是效力端点——并入任一方向都会把「已确认」误判成「已生效/已失效」。
306
+ """
307
+ if not isinstance(fm, dict):
308
+ return "unknown", None, None
309
+ start, _sk = first_endpoint(fm, FROM_ALIASES)
310
+ end, _ek = first_endpoint(fm, UNTIL_ALIASES)
311
+ if start is None and end is None:
312
+ return "unknown", None, None
313
+ t = time.time() if now is None else float(now)
314
+ if start is not None and t < start:
315
+ return "not_yet", start, end
316
+ if end is not None and t > end:
317
+ return "expired", start, end
318
+ return "active", start, end
319
+
320
+
321
+ def believed_at(fm) -> float:
322
+ """信念时间(体系**何时确认此条**)→ epoch 秒;缺字段/不可解析 → None(不猜测)。
323
+
324
+ 用途:取代(supersede)/ 审核的排序锚——「谁更晚被确认」是判定新旧的正路;
325
+ **不得**拿 `valid_from`/`valid_until` 代替(那是事实在任务世界里何时有效,
326
+ 与体系何时知道它无关)。
327
+ """
328
+ if not isinstance(fm, dict):
329
+ return None
330
+ return parse_time(fm.get(BELIEVED_FIELD))
331
+
332
+
333
+ def is_expired(fm, now: float = None) -> bool:
334
+ """`validity` 的布尔快捷:是否「**已过期**」。
335
+
336
+ 只判 `expired`——`not_yet`(尚未生效)**不算**:两者语义相反
337
+ (见 `scrub._NOT_YET_KEYS` 纪律「`valid_from` 绝不并入 `_EXPIRY_KEYS`」),
338
+ 「尚未开始」不等于「已失效」。无时间轴 / 端点不可解析 → False(不猜测、不误杀)。
339
+ """
340
+ return validity(fm, now=now)[0] == "expired"
341
+
342
+
343
+ def time_window_msg(fm, now: float = None) -> str:
344
+ """时效判定的一句话(空串表示无时间轴约束)。**点名实际命中的键**(含别名)。"""
345
+ kind, start, end = validity(fm, now=now)
346
+ if kind == "unknown":
347
+ return ""
348
+ if kind == "not_yet":
349
+ _ts, k = first_endpoint(fm, FROM_ALIASES)
350
+ return f"未生效({k or FROM_FIELD} 未到)"
351
+ if kind == "expired":
352
+ _ts, k = first_endpoint(fm, UNTIL_ALIASES)
353
+ return f"已过期({k or UNTIL_FIELD} 已过)"
354
+ return "时效内"
355
+
356
+
357
+ # ---------------------------------------------------------------- 时间算子(阶段二 4.1)
358
+
359
+ #: 时间轴(**封闭枚举**)。两条轴物理隔离、语义不可互换(同 `believed_at` 的隔离纪律):
360
+ #: effective 效力轴——这条事实**何时开始/不再成立**(`effective_from/until` 及别名)
361
+ #: observed 观察轴——这条记忆**何时被观测/事件何时发生**(`temporal` / `time_window`)
362
+ #: 不设**隐式**默认轴:入口在「启用时间算子但未指定轴」时回落 `effective`
363
+ #: (与既有 `validity=` 语义连续),但轴本身永远由调用方显式决定。
364
+ TIME_AXES = ("effective", "observed")
365
+
366
+ #: 时间算子(**封闭枚举**,拒收未知名):候选轴端点 与 查询端点 的比较关系。
367
+ #: 缺省(不给 operator)= **区间重叠**语义,见 `window_match`。
368
+ TIME_OPERATORS = ("gt", "gte", "eq", "lte", "lt")
369
+
370
+ #: 观察轴字段名(与 `stg._interval` 同源口径,**不新增第二套解析**)。
371
+ OBSERVED_TIME_FIELD = "temporal"
372
+ OBSERVED_WINDOW_FIELD = "time_window"
373
+
374
+
375
+ def time_axis_of(axis) -> str:
376
+ """轴名归一 → `"effective"` / `"observed"`;`None` → `"effective"`;
377
+ 其余(含 `"believed"`)→ `ValueError`(**fail-closed,不静默降级**)。
378
+
379
+ 与 `_stg_call`「不做签名推导——猜错会静默返回错误视图,比报错更贵」同风格:
380
+ 轴写错时必须报错,因为静默按另一条轴过滤会产出**无法复算**的结果集。
381
+ """
382
+ if axis is None:
383
+ return "effective"
384
+ a = str(axis).strip().lower()
385
+ if a in TIME_AXES:
386
+ return a
387
+ raise ValueError(f"未知 time_axis {axis!r}(允许:{TIME_AXES})")
388
+
389
+
390
+ def time_window_of(fm, axis: str = "effective"):
391
+ """按轴取候选时间窗口 → `(start, end)`;不可判定 → `(None, None)`(不猜测)。
392
+
393
+ · `effective`:效力轴,走 `first_endpoint(FROM_ALIASES/UNTIL_ALIASES)`
394
+ (规范键优先、别名回落,与 `validity` **同源**);
395
+ 缺字段/不可解析 → 该侧 `None`(= 无界,与 `validity` 的「不误判」口径一致)。
396
+ · `observed`:观察轴,与 `stg._interval` **同源口径**——`temporal`(事件时刻)优先,
397
+ 缺失才回退 `condition_space.time_window`(观测窗);任一端不可解析 → `(None, None)`
398
+ (与 `_interval` 的「整体不可用」语义一致)。
399
+
400
+ `believed_at` **永不参与**(`BELIEVED_FIELD` 不出现于任何轴)。
401
+ """
402
+ if not isinstance(fm, dict):
403
+ return None, None
404
+ if time_axis_of(axis) == "observed":
405
+ t = fm.get(OBSERVED_TIME_FIELD)
406
+ if t is not None:
407
+ ts = parse_time(t)
408
+ if ts is not None:
409
+ return ts, ts
410
+ # 兼容两种载体形态:**fm**(`condition_space.time_window`)与**索引扁平快照**
411
+ # (`time_window` 平铺在 entry 上,见 `mdcg._scan_nodes`/`_stage`)。
412
+ # 只读嵌套会让「按 entry 过滤」的路径永不命中(同类坑:S2 时间门控,
413
+ # mdcg.py L639 已记「直接读 cs 会让门控永不生效」)。
414
+ cs = fm.get("condition_space") or {}
415
+ tw = fm.get(OBSERVED_WINDOW_FIELD)
416
+ if tw is None and isinstance(cs, dict):
417
+ tw = cs.get(OBSERVED_WINDOW_FIELD)
418
+ if isinstance(tw, (list, tuple)) and len(tw) == 2:
419
+ s, e = parse_time(tw[0]), parse_time(tw[1])
420
+ if s is None or e is None:
421
+ return None, None
422
+ return s, e
423
+ return None, None
424
+ start, _sk = first_endpoint(fm, FROM_ALIASES)
425
+ end, _ek = first_endpoint(fm, UNTIL_ALIASES)
426
+ return start, end
427
+
428
+
429
+ def _op_ok(cand, q, op) -> bool:
430
+ """单个端点比较;任一端不可解析 → False(缺字段的处置归**轴策略**,此处不猜测)。"""
431
+ if cand is None or q is None:
432
+ return False
433
+ if op == "gt":
434
+ return cand > q
435
+ if op == "gte":
436
+ return cand >= q
437
+ if op == "eq":
438
+ return cand == q
439
+ if op == "lte":
440
+ return cand <= q
441
+ if op == "lt":
442
+ return cand < q
443
+ return False # 未知算子:入口已 fail-closed,此处保守拒
444
+
445
+
446
+ def window_match(cand_start, cand_end, q_start=None, q_end=None,
447
+ start_op: str = None, end_op: str = None) -> bool:
448
+ """候选窗口与查询窗口是否匹配(§1.2 B1/B2 的**唯一实现点**,纯函数)。
449
+
450
+ 两种模式由「是否给 operator」**显式分叉**(不允许隐式混用——混用会产出
451
+ 「无法复算」的过滤,违反白箱):
452
+
453
+ · **重叠模式**(`start_op`/`end_op` 均为 `None`):候选窗口与查询窗口有交集
454
+ 即命中(记忆窗口是**区间**不是点)。查询端点缺省 = 该侧**无界**(不隐含 now)。
455
+ · **端点模式**(至少给一个 operator):对**显式启用的侧**做 `op(cand端, q端)`;
456
+ 一侧未给 operator 但**给了该侧查询端点**时,用 B1 缺省(起点 `gte` / 终点 `lte`);
457
+ 该侧查询端点也没有 → **不约束该侧**。
458
+
459
+ 「未给 operator 且未给查询端点 → 不约束」是刻意的:若一律回落到 B1 缺省再比较,
460
+ `_op_ok(cand, None, op)` 恒伪,于是 `start_operator="gte" + start_time=T`
461
+ (单端算子,合法调用)会静默返回**空集**——把「只筛起点」误答成「没有匹配」。
462
+ 端点模式下候选的该侧端点不可解析 → `False`(该侧无法比较,不猜)。
463
+
464
+ 候选窗口两端皆不可解析 → `False`(该节点「无时间轴可判」)——是否因此剔除
465
+ 由**轴的策略**决定(效力轴 fail-open / 观察轴 fail-closed,见候选过滤处)。
466
+ """
467
+ cs, ce = parse_time(cand_start), parse_time(cand_end)
468
+ if cs is None and ce is None:
469
+ return False
470
+ qs, qe = parse_time(q_start), parse_time(q_end)
471
+ if start_op is None and end_op is None:
472
+ if qs is not None and ce is not None and ce < qs:
473
+ return False
474
+ if qe is not None and cs is not None and cs > qe:
475
+ return False
476
+ return True
477
+ if start_op is not None or qs is not None:
478
+ if not _op_ok(cs, qs, start_op or "gte"):
479
+ return False
480
+ if end_op is not None or qe is not None:
481
+ if not _op_ok(ce, qe, end_op or "lte"):
482
+ return False
483
+ return True
484
+
485
+
486
+ def check_time_args(start_time=None, end_time=None, start_operator=None,
487
+ end_operator=None, time_axis=None):
488
+ """时间算子入参 fail-closed 校验 → `(enabled, axis, why)`。
489
+
490
+ **入口(`mdcg.search` / `entity_contexts`)与库层共用的唯一校验点**
491
+ (避免两处各写一套、口径漂移)。三则误用一律拒(`why` 非空即应抛
492
+ `ValueError`,不静默忽略——与 `_stg_call` 同风格):
493
+
494
+ 1. 只给 operator 而不给对应的 `start_time`/`end_time`;
495
+ 2. `time_axis` 非 `{"effective","observed"}`(含 `"believed"`);
496
+ 3. `start_time > end_time`。
497
+
498
+ 未启用(五参全 `None`)→ `(False, None, "")`(默认路径零变更)。
499
+ """
500
+ if time_axis is not None and str(time_axis).strip().lower() not in TIME_AXES:
501
+ return False, None, (f"未知 time_axis {time_axis!r}(允许:{TIME_AXES})——"
502
+ "轴必须显式且合法,静默按另一条轴过滤会产出无法复算的结果")
503
+ for op, val, nm in ((start_operator, start_time, "start_operator/start_time"),
504
+ (end_operator, end_time, "end_operator/end_time")):
505
+ if op is None:
506
+ continue
507
+ if str(op).strip().lower() not in TIME_OPERATORS:
508
+ return False, None, (f"未知算子 {op!r}(允许:{TIME_OPERATORS})")
509
+ if val is None:
510
+ return False, None, f"给了 {nm.split('/')[0]} 但缺 {nm.split('/')[1]}(不猜默认值)"
511
+ s, e = parse_time(start_time), parse_time(end_time)
512
+ if start_time is not None and s is None:
513
+ return False, None, f"start_time 不可解析:{start_time!r}"
514
+ if end_time is not None and e is None:
515
+ return False, None, f"end_time 不可解析:{end_time!r}"
516
+ if s is not None and e is not None and s > e:
517
+ return False, None, f"start_time({s}) > end_time({e}):空窗口,拒绝"
518
+ enabled = any(x is not None for x in (start_time, end_time,
519
+ start_operator, end_operator))
520
+ if not enabled:
521
+ return False, None, ""
522
+ return True, time_axis_of(time_axis), ""
523
+
524
+
525
+ def window_matches_node(fm, axis, q_start, q_end, start_op=None, end_op=None):
526
+ """候选节点按轴取窗后做 `window_match` → `(matched, missing)`。
527
+
528
+ `missing=True` 表示该节点在**该轴上不可判定**(窗口两端皆 `None`)——
529
+ 调用方据此执行轴策略:效力轴 fail-open(保留)、观察轴 fail-closed
530
+ (剔除并计入 `axis_missing`)。策略按**保证强度**定,不按「一致好看」定:
531
+ 效力轴字段是可选声明,观察轴字段由写入侧保证存在。
532
+ """
533
+ cs, ce = time_window_of(fm, axis)
534
+ if cs is None and ce is None:
535
+ return False, True
536
+ return window_match(cs, ce, q_start, q_end, start_op, end_op), False
537
+
538
+
539
+ def time_filter_meta(axis=None, mode="overlap", start=None, end=None,
540
+ start_operator=None, end_operator=None, dropped=0,
541
+ axis_missing=0, applied=False) -> dict:
542
+ """`meta["time_filter"]` 审计块(**五键齐备**,可复算):
543
+
544
+ `dropped + 存活数 == 候选数` 由调用方保证;`axis_missing` 单独记账,
545
+ 使「观察轴缺字段被剔除」的条数可查(静默放行会把数据异常藏起来)。
546
+ """
547
+ return {"axis": axis, "mode": mode, "start": start, "end": end,
548
+ "start_operator": start_operator, "end_operator": end_operator,
549
+ "dropped": int(dropped), "axis_missing": int(axis_missing),
550
+ "applied": bool(applied)}
551
+
552
+
553
+ def filter_by_time(entries, axis, q_start, q_end, start_op=None, end_op=None):
554
+ """按轴过滤候选 → `(kept, dropped, axis_missing)`。**候选层唯一过滤点**。
555
+
556
+ 轴策略按**字段保证强度**定(不按「行为一致好看」定):
557
+ · `effective` 效力轴:`effective_from/until` 是**可选声明**(多数节点没有)
558
+ → 不可判定一律 fail-open(保留)——否则「没写时效 = 被过滤掉」,
559
+ 会把沉默当否认;
560
+ · `observed` 观察轴:`temporal` / `time_window` 由**写入侧保证**存在
561
+ (add 缺省填观测窗)→ 不可判定一律 fail-closed(剔除并计入
562
+ `axis_missing`,使数据异常可查而非静默放行)。
563
+ """
564
+ kept, dropped, missing = [], 0, 0
565
+ for e in entries:
566
+ m, miss = window_matches_node(e, axis, q_start, q_end, start_op, end_op)
567
+ if miss:
568
+ if axis == "observed":
569
+ dropped += 1
570
+ missing += 1
571
+ else:
572
+ kept.append(e)
573
+ continue
574
+ if m:
575
+ kept.append(e)
576
+ else:
577
+ dropped += 1
578
+ return kept, dropped, missing
579
+
580
+
581
+ # ---------------------------------------------------------------- 依赖声明
582
+
583
+ def as_deps(value) -> list:
584
+ """把单值 / 序列 / 逗号串统一成**去重、排序**的节点 id 列表(上限 MAX_DEPS)。
585
+
586
+ 容错口径(写路径的入参形态不由本模块决定,收窄会误伤):
587
+ · `None` / 空串 / 纯空白项 → 丢弃(不是目标,也不是「声明了依赖」);
588
+ · 半角/全角逗号串 → 切分(`"a, b"` 与 `["a", "b"]` 等价);
589
+ · 排序的动机:依赖是**集合语义**(声明顺序不承载信息),排序让 fm 与
590
+ 索引快照对同一组依赖恒等——比较/去重/审计不再依赖声明顺序。
591
+ """
592
+ if value is None:
593
+ return []
594
+ items = list(value) if isinstance(value, (list, tuple, set)) else [value]
595
+ out = []
596
+ for x in items:
597
+ if x is None:
598
+ continue
599
+ for s in str(x).replace(",", ",").split(","):
600
+ s = s.strip()
601
+ if s and s not in out:
602
+ out.append(s)
603
+ out.sort()
604
+ return out[:MAX_DEPS]
605
+
606
+
607
+ def deps_of(cg, nid: str) -> list:
608
+ """读某节点声明的依赖(**零读文件**:索引快照缺失才回读节点)。"""
609
+ e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid) or {}
610
+ deps = as_deps(e.get(DEPS_FIELD))
611
+ if deps:
612
+ return deps
613
+ node = cg.get(nid)
614
+ if not node:
615
+ return []
616
+ return as_deps((node.get("frontmatter") or {}).get(DEPS_FIELD))
617
+
618
+
619
+ def invalidate_cache(cg) -> None:
620
+ """作废反查缓存(写/删节点后调用;下次读时增量重建)。"""
621
+ try:
622
+ cg.__dict__.pop("_trust_dependents", None)
623
+ except Exception: # noqa: BLE001
624
+ pass
625
+
626
+
627
+ def dependents_index(cg, refresh: bool = False) -> dict:
628
+ """反查索引:`{被依赖节点: [依赖它的节点…]}`(「谁依赖我」)。
629
+
630
+ 缓存挂在 cg 实例上(与 `subgraph` 的缓存化做法同构);`refresh=True` 强制重建。
631
+ """
632
+ if not refresh:
633
+ cached = getattr(cg, "_trust_dependents", None)
634
+ if isinstance(cached, dict):
635
+ return cached
636
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
637
+ idx = {}
638
+ for nid, e in nodes.items():
639
+ for p in as_deps((e or {}).get(DEPS_FIELD)):
640
+ idx.setdefault(p, []).append(nid)
641
+ for k in idx:
642
+ idx[k].sort()
643
+ try:
644
+ cg._trust_dependents = idx
645
+ except Exception: # noqa: BLE001
646
+ pass
647
+ return idx
648
+
649
+
650
+ def _record(cg, rec: dict) -> None:
651
+ """台账追加(best-effort:失败静默,**绝不阻断写路径**)。"""
652
+ try:
653
+ append_jsonl(os.path.join(cg.root, AUDIT_FILE), rec)
654
+ except Exception: # noqa: BLE001
655
+ pass
656
+
657
+
658
+ # ---------------------------------------------------------------- 推进
659
+
660
+ def _sync_index(cg, node_id, fm) -> None:
661
+ """把验证态同步进索引快照(免读文件可查);无索引实现时静默跳过。"""
662
+ idx = getattr(cg, "index", None)
663
+ if not isinstance(idx, dict):
664
+ return
665
+ e = (idx.get("nodes") or {}).get(node_id)
666
+ if e is None:
667
+ return
668
+ e[STATE_FIELD] = fm.get(STATE_FIELD)
669
+ dirty = getattr(cg, "_dirty", None)
670
+ if isinstance(dirty, dict):
671
+ dirty[node_id] = e
672
+ flush = getattr(cg, "flush", None)
673
+ if callable(flush):
674
+ flush()
675
+
676
+
677
+ def set_state(cg, node_id: str, dst: str, reason: str = None, actor: str = None,
678
+ evidence: str = None, method: str = None, trigger: str = None,
679
+ override: bool = False) -> dict:
680
+ """推进一个节点的验证态(**唯一推进入口**)。
681
+
682
+ 非法迁移不走异常而是返回 `{"ok": False, "error": <code>, ...}`(负路由)。
683
+ 幂等迁移返回 `changed=False` 且不写盘。
684
+ """
685
+ node = cg.get(node_id)
686
+ if not node:
687
+ return {"ok": False, "error": "node_not_found", "node_id": node_id}
688
+ fm = dict(node.get("frontmatter") or {})
689
+ src = state_of(fm)
690
+ ok, code, why = stamp(fm, dst, reason=reason, actor=actor, evidence=evidence,
691
+ method=method, trigger=trigger, override=override)
692
+ base = {"node_id": node_id, "from": src, "to": dst, "code": code}
693
+ if not ok:
694
+ return {**base, "ok": False, "error": code, "reason": why}
695
+ if code == "noop":
696
+ return {**base, "ok": True, "changed": False, "reason": why}
697
+ at = fm[HISTORY_FIELD][-1]["at"]
698
+ cg._write_node(node_id, os.path.join(cg.root, node["path"]), fm,
699
+ node.get("content") or "")
700
+ _sync_index(cg, node_id, fm)
701
+ _record(cg, {"t": at, "action": "set_state", "node_id": node_id,
702
+ "from": src, "to": dst, "reason": reason, "actor": actor,
703
+ "method": method, "trigger": trigger, "override": bool(override)})
704
+ if fm.get(DEPS_FIELD):
705
+ invalidate_cache(cg)
706
+ return {**base, "ok": True, "changed": True, "reason": why, "at": at}
707
+
708
+
709
+ # ---------------------------------------------------------------- 失效传播
710
+
711
+ def mark_dependents(cg, node_id: str, reason: str = None, *, apply: bool = True,
712
+ actor: str = "system", trigger: str = None,
713
+ depth: int = 1, override: bool = False) -> dict:
714
+ """**一跳同步传播**:把直接下游标为 `doubted`(异常的新定义)。
715
+
716
+ 根因口径:异常不再仅是「内容错」,而是「**它所依赖的地基动了**」——被依赖
717
+ 单元被修改/证伪时,直接下游立刻产生存疑标记,无需等全库巡检。
718
+
719
+ **永不抛**(对齐 provenance G8 纪律:传播失败不得阻断写入)。`apply=False`
720
+ 只预演;同状态下游是 no-op(幂等,不写盘、不留痕)。
721
+ """
722
+ try:
723
+ dep_map = dependents_index(cg)
724
+ kids = list(dep_map.get(node_id, []))
725
+ kids.sort()
726
+ if not kids:
727
+ return {"ok": True, "changed": 0, "dependents": [], "apply": bool(apply),
728
+ "reason": "no_dependents"}
729
+ reason = reason or f"上游 {node_id} 变动"
730
+ planned = [k for k in kids
731
+ if state_of((cg.index or {}).get("nodes", {}).get(k) or {})
732
+ != "doubted"]
733
+ if not apply:
734
+ return {"ok": True, "dry_run": True, "changed": 0,
735
+ "dependents": kids, "planned": len(planned)}
736
+ done, skipped = [], []
737
+ for k in kids:
738
+ r = set_state(cg, k, "doubted", reason=reason, actor=actor,
739
+ trigger=trigger or f"dep_changed:{node_id}",
740
+ override=override)
741
+ if r.get("ok") and r.get("changed"):
742
+ done.append(k)
743
+ elif r.get("ok"):
744
+ skipped.append(k) # 已是 doubted:幂等
745
+ else:
746
+ skipped.append(f"{k}({r.get('error')})")
747
+ return {"ok": True, "changed": len(done), "dependents": kids,
748
+ "updated": done, "skipped": skipped, "apply": True,
749
+ "reason": reason}
750
+ except Exception as exc: # noqa: BLE001
751
+ _record(cg, {"t": time.time(), "action": "mark_dependents_degraded",
752
+ "node_id": node_id, "error": f"{type(exc).__name__}: {exc}",
753
+ "actor": actor})
754
+ return {"ok": False, "degraded": True, "changed": 0,
755
+ "error": f"{type(exc).__name__}: {exc}"}
756
+
757
+
758
+ def propagate(cg, *, apply: bool = False, max_nodes: int = MAX_NODES_DEFAULT,
759
+ actor: str = "patrol") -> dict:
760
+ """**多跳异步重算**(巡检面,不阻塞写入/查询)。
761
+
762
+ BFS 从所有 `expired` / `rechecking` / 显式根源节点出发,把可达下游标
763
+ `doubted`(**只降不升**:复核通过须由人显式 `set_state(verified)`)。
764
+ 默认 `apply=False` 只出报表;`max_nodes` 封顶防大库爆炸。
765
+
766
+ `doubted` 中间节点的语义(2026-09-20 v14 缺陷 B 修复):**穿过,但不重复
767
+ 标记**。已 doubted 说明它已被标记,无需再标(幂等);但它**不是传播终点**
768
+ ——「地基动了」的波及范围必须继续往其下游走。旧实现把 doubted 直接
769
+ `continue`(当作终点),于是「先 `mark_dependents` 预演直接下游、再
770
+ `propagate` 正式传播」这一最常见序列会使多跳传播**整体失效**
771
+ (`reachable`/`updated` 全 0)且**无任何报错**。穿过的节点在
772
+ `passed_doubted` 中如实透出(可审计)。
773
+
774
+ **`reachable` / `updated` 的口径**(2026-09-20 v15-4 澄清,勿读成「波及集
775
+ 大小」):两者计的是「本次**需要新标记**的节点数」——`apply=False` 时 =
776
+ 计划标记数(`planned` 的长度),`apply=True` 时 = 实际标记成功数(`done` 的
777
+ 长度)。**穿过的 `doubted` 中继不计入**(它早已被标记,本次幂等跳过)。故
778
+ 「本次的完整波及集」= `reachable`(或 `updated`)+ `passed_doubted`,只读
779
+ 前者会低估。`roots` 是传播起点(`expired`/`rechecking`),不属于波及集。
780
+ """
781
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
782
+ dep_map = dependents_index(cg)
783
+ roots = []
784
+ for nid, e in nodes.items():
785
+ st = (e or {}).get(STATE_FIELD)
786
+ if st in ("expired", "rechecking"):
787
+ roots.append(nid)
788
+ roots.sort()
789
+ seen, waves, passed = set(roots), [], set()
790
+ frontier, depth = list(roots), 0
791
+ while frontier and len(seen) < int(max_nodes):
792
+ depth += 1
793
+ nxt = []
794
+ for nid in frontier:
795
+ for k in dep_map.get(nid, []):
796
+ if k in seen:
797
+ continue
798
+ seen.add(k)
799
+ if state_of(nodes.get(k) or {}) == "doubted":
800
+ passed.add(k) # 穿过但不重复标记(不是终点)
801
+ nxt.append(k)
802
+ if len(seen) >= int(max_nodes):
803
+ break
804
+ for k in nxt:
805
+ if k in passed:
806
+ continue # 已 doubted:无需再标(幂等)
807
+ waves.append({"node_id": k, "depth": depth,
808
+ "via": sorted(n for n in dep_map if k in dep_map[n]
809
+ and n in seen)})
810
+ frontier = nxt
811
+ if not apply:
812
+ return {"ok": True, "dry_run": True, "roots": roots[:50],
813
+ "roots_count": len(roots), "reachable": len(waves),
814
+ "planned": waves[:50], "passed_doubted": sorted(passed)[:20],
815
+ "max_nodes": int(max_nodes),
816
+ "note": "预演:未改盘;只降不升(复核须显式)"}
817
+ done, skipped = [], []
818
+ for w in waves:
819
+ r = set_state(cg, w["node_id"], "doubted",
820
+ reason="多跳失效传播(上游时间轴/证据失效)",
821
+ actor=actor, trigger=f"propagate:d{w['depth']}")
822
+ if r.get("ok") and r.get("changed"):
823
+ done.append(w["node_id"])
824
+ elif not r.get("ok"):
825
+ skipped.append(f"{w['node_id']}({r.get('error')})")
826
+ return {"ok": True, "dry_run": False, "roots": roots[:50],
827
+ "roots_count": len(roots), "updated": done, "updated_count": len(done),
828
+ "passed_doubted": sorted(passed)[:20],
829
+ "skipped": skipped[:20], "max_nodes": int(max_nodes)}
830
+
831
+
832
+ # ---------------------------------------------------------------- 巡检 / 回填
833
+
834
+ # 生效条件:cg 传入即只读遍历 index 快照并返回悬空/未生效/已过期/存疑四类计数与样本,不写盘、不改状态、不抛(index 缺失按空库处理);
835
+ def patrol(cg, *, limit: int = 20, now: float = None) -> dict:
836
+ """只读巡检:悬空依赖 / 时间轴失效 / 存疑积压。**不改状态、不删节点**。
837
+
838
+ 命名纪律(负记忆,2026-09-19 实测):本函数**不得叫 `check`**——裁决层已有
839
+ `check(src, dst, ...)`(唯一裁决点)。同模块内同名二次定义会**静默遮蔽**前者,
840
+ `stamp()` 随即调到巡检版并抛 `TypeError`(现场:`set_verification` 整条链断)。
841
+ 两者语义分属不同层次(纯函数裁决 vs 全库只读巡检),名字必须分开。
842
+ """
843
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
844
+ known = set(nodes)
845
+ dangling, expired, not_yet, doubted = [], [], [], []
846
+ for nid, e in sorted(nodes.items()):
847
+ for p in as_deps((e or {}).get(DEPS_FIELD)):
848
+ if p not in known:
849
+ dangling.append({"node_id": nid, "missing": p})
850
+ st = (e or {}).get(STATE_FIELD)
851
+ if st == "doubted":
852
+ doubted.append(nid)
853
+ # 时间轴按索引可判定字段粗判(精确判定需 fm,巡检读面只给候选)
854
+ kind = _index_validity(e, now=now)
855
+ if kind == "expired":
856
+ expired.append(nid)
857
+ elif kind == "not_yet":
858
+ not_yet.append(nid)
859
+ return {
860
+ "ok": not dangling,
861
+ "checked": True, "root": cg.root, "nodes": len(known),
862
+ "dangling": dangling[:int(limit)], "dangling_count": len(dangling),
863
+ "expired": expired[:int(limit)], "expired_count": len(expired),
864
+ "not_yet": not_yet[:int(limit)], "not_yet_count": len(not_yet),
865
+ "doubted": doubted[:int(limit)], "doubted_count": len(doubted),
866
+ "readonly": True,
867
+ "note": "只读巡检:悬空依赖/时效仅检出并报告,不自动改状态(处置由人/巡检动作发起)"}
868
+
869
+
870
+ def _index_validity(e, now: float = None) -> str:
871
+ """索引快照口径的时效粗判(索引缺时间字段时返回 unknown,零读文件)。"""
872
+ if not isinstance(e, dict):
873
+ return "unknown"
874
+ return validity(e, now=now)[0]
875
+
876
+
877
+ def backfill(cg, apply: bool = False, limit: int = 5000) -> dict:
878
+ """存量回填:把缺 `verification_state` 的节点显式补成 `unverified`(幂等)。
879
+
880
+ 缺字段本就按 unverified 解释(`state_of`),故回填**不是让功能工作的前提**,
881
+ 而是把「缺省」变成「显式」——索引/审计里从此可直接读到验证态。默认只盘点。
882
+ """
883
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
884
+ missing = []
885
+ for nid, e in list(nodes.items()):
886
+ if len(missing) >= limit:
887
+ break
888
+ if (e or {}).get(STATE_FIELD) in STATES:
889
+ continue
890
+ missing.append(nid)
891
+ missing.sort()
892
+ if not apply:
893
+ return {"ok": True, "dry_run": True, "scanned": len(nodes),
894
+ "missing": len(missing), "planned": missing[:50]}
895
+ done, failed = [], []
896
+ for nid in missing:
897
+ node = cg.get(nid)
898
+ if not node:
899
+ failed.append(nid)
900
+ continue
901
+ fm = dict(node.get("frontmatter") or {})
902
+ fm[STATE_FIELD] = state_of(fm)
903
+ cg._write_node(nid, os.path.join(cg.root, node["path"]), fm,
904
+ node.get("content") or "")
905
+ _sync_index(cg, nid, fm)
906
+ done.append(nid)
907
+ return {"ok": True, "dry_run": False, "scanned": len(nodes),
908
+ "missing": len(missing), "backfilled": len(done),
909
+ "planned": done[:50], "failed": failed[:20]}
910
+
911
+
912
+ def describe(cg, node_id: str, now: float = None) -> dict:
913
+ """单节点验证态全貌(供 op=status / 状态头渲染)。只读、失败不抛。"""
914
+ try:
915
+ node = cg.get(node_id)
916
+ except Exception: # noqa: BLE001
917
+ node = None
918
+ e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
919
+ if not node and not e:
920
+ return {"ok": False, "error": "node_not_found", "node_id": node_id}
921
+ fm = (node or {}).get("frontmatter") or {}
922
+ deps = as_deps(fm.get(DEPS_FIELD)) or as_deps((e or {}).get(DEPS_FIELD))
923
+ kind, start, end = validity(fm if fm else (e or {}), now=now)
924
+ dep_map = dependents_index(cg)
925
+ hist = list(fm.get(HISTORY_FIELD) or [])[-5:]
926
+ return {"ok": True, "node_id": node_id,
927
+ "verification_state": state_of(fm if fm else (e or {})),
928
+ "verification_basis": fm.get("verification_basis"),
929
+ "verification_method": fm.get("verification_method"),
930
+ "verified_at": fm.get("verified_at"),
931
+ "depends_on": deps,
932
+ "depended_by": dep_map.get(node_id, []),
933
+ "validity": {"kind": kind, "valid_from": fm.get(FROM_FIELD),
934
+ "valid_until": fm.get(UNTIL_FIELD),
935
+ "start": start, "end": end},
936
+ "history": hist,
937
+ "lifecycle_state": fm.get("lifecycle_state"),
938
+ "protected": bool(fm.get("protected") or fm.get("immutable"))}
939
+
940
+
941
+ def summary(cg) -> dict:
942
+ """轻量摘要(只读;失败不抛,避免拖垮 health / 常驻循环)。"""
943
+ try:
944
+ rep = patrol(cg, limit=3)
945
+ return {"dangling": rep["dangling_count"], "expired": rep["expired_count"],
946
+ "not_yet": rep["not_yet_count"], "doubted": rep["doubted_count"],
947
+ "nodes": rep["nodes"], "sample": [
948
+ f"{r['node_id']}->?{r['missing']}" for r in rep["dangling"]]}
949
+ except Exception: # noqa: BLE001
950
+ return {}
951
+
952
+
953
+ def load_ledger(cg, *, node_id: str = None, limit: int = None) -> list:
954
+ """读验证态台账(跳过坏行;可按节点过滤)。"""
955
+ path = os.path.join(cg.root, AUDIT_FILE)
956
+ out = []
957
+ for r in read_jsonl(path):
958
+ if not isinstance(r, dict):
959
+ continue
960
+ if node_id and r.get("node_id") != node_id:
961
+ continue
962
+ out.append(r)
963
+ return out[:int(limit)] if limit else out
964
+
965
+
966
+ def catalog(root: str = None) -> dict:
967
+ """自描述(供 MCP catalog / 人工核对)。"""
968
+ return {
969
+ "layer": "可验证记忆单元",
970
+ "question": "它还成不成立(验证态 + 依赖 + 时效)",
971
+ "ledger": os.path.join(root, AUDIT_FILE) if root else AUDIT_FILE,
972
+ "states": list(STATES),
973
+ "state_field": STATE_FIELD,
974
+ "history_field": HISTORY_FIELD,
975
+ "deps_field": DEPS_FIELD,
976
+ "time_fields": [FROM_FIELD, UNTIL_FIELD],
977
+ "transitions": sorted(f"{a}→{b}" for a, b in TRANSITIONS),
978
+ "max_deps": MAX_DEPS,
979
+ "max_nodes_default": MAX_NODES_DEFAULT,
980
+ "discipline": {"never_block_write": True, "no_backfill": True,
981
+ "patrol_readonly": True, "one_hop_sync": True,
982
+ "multi_hop_async": True, "only_downgrade": True},
983
+ "distinct_from": ("lifecycle.py = 节点生命周期(lifecycle_state);"
984
+ "provenance.py = 派生血缘(_link.jsonl);"
985
+ "本层 = 验证态与依赖(verification_state/_trust.jsonl)"),
986
+ }