@furongjun1999/dsh-memory 0.4.7 → 0.4.9

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