@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
@@ -0,0 +1,769 @@
1
+ # -*- coding: utf-8 -*-
2
+ """M1 机械层自测:候选生成 → 捆绑 → 规则装配(确定性、零写入)。
3
+
4
+ 真源:docs/记忆评审系统_立项设计与施工交接_20260915.md §3 前三级 / §5 D3 / §6。
5
+ 纪律(吸取 test_p27【9】教训):**自备数据源**——主测面 = tempfile 合成库;
6
+ 真源库仅在**存在时**做集成复核(不存在即 SKIP,外部 clone 下同样全绿)。
7
+
8
+ 覆盖:A 级1 四源/分源开关/同口径交叉核对/轮巡不重不漏/确定性
9
+ B 级2 模板一包一评/包号确定性/降级/正文回读/切分/血缘/主题
10
+ C 级3 规则库加载与非法库 fail-closed/matcher 语义/六检查器/数据化零代码改动
11
+ D CLI run/--out/--full/零写入 E 集成:真源存在才跑(审计基线口径)
12
+
13
+ 运行:python -m md_cg.test_mr_m1
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import collections
18
+ import hashlib
19
+ import io
20
+ import json
21
+ import os
22
+ import sys
23
+ import tempfile
24
+ import shutil
25
+ from contextlib import redirect_stdout
26
+
27
+ from . import conformance as CF
28
+ from .mreview import __main__ as CLI
29
+ from .mreview import bundle as BD
30
+ from .mreview import candidates as CD
31
+ from .mreview import ruleset as RS
32
+
33
+ PASS = FAIL = 0
34
+ FAILS = []
35
+
36
+
37
+ def ok(cond, label):
38
+ global PASS, FAIL
39
+ if cond:
40
+ PASS += 1
41
+ else:
42
+ FAIL += 1
43
+ FAILS.append(label)
44
+ print(f" FAIL {label}")
45
+
46
+
47
+ # ---------------------------- 合成库 ----------------------------
48
+
49
+ def node(nid, layer="knowledge", **kw):
50
+ rec = {"id": nid, "layer": layer, "path": f"{layer}/{nid}.md",
51
+ "content": f"内容 {nid}", "content_hash": f"hash_{nid}", "tags": []}
52
+ rec.update(kw)
53
+ return rec
54
+
55
+
56
+ def mkroot(root, nodes, *, access=(), inbox=(), decisions=(), forgetting=(),
57
+ contents=None, write_files=True):
58
+ """合成认知图根:索引 + 闸门三日志 + 节点盘文件。"""
59
+ os.makedirs(root, exist_ok=True)
60
+ with open(os.path.join(root, "_index.json"), "w", encoding="utf-8") as f:
61
+ json.dump({"nodes": nodes}, f, ensure_ascii=False)
62
+ hp = os.path.join(root, "hippocampus")
63
+ os.makedirs(hp, exist_ok=True)
64
+ for name, rows in (("inbox.jsonl", inbox), ("decisions.jsonl", decisions)):
65
+ with open(os.path.join(hp, name), "w", encoding="utf-8") as f:
66
+ for r in rows:
67
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
68
+ with open(os.path.join(root, CF.ACCESS_LOG), "w", encoding="utf-8") as f:
69
+ for r in access:
70
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
71
+ with open(os.path.join(root, CD.FORGET_LOG), "w", encoding="utf-8") as f:
72
+ for r in forgetting:
73
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
74
+ if write_files:
75
+ for r in nodes.values():
76
+ if not r.get("path"):
77
+ continue
78
+ p = os.path.join(root, r["path"])
79
+ os.makedirs(os.path.dirname(p), exist_ok=True)
80
+ with open(p, "w", encoding="utf-8") as f:
81
+ f.write((contents or {}).get(r["id"], r.get("content", "")))
82
+ return root
83
+
84
+
85
+ # 运行态目录:由常驻进程(MCP server 等)按自身节拍 flush 增量日志,与 M1 无关。
86
+ # 取证(第4条):静置 6s 真源无变化,跑 M1 前后 _index_log/ 无新文件、逐字节不变;
87
+ # 目录内文件名带写入者 pid(14916/16072/20184/6708),非本进程 pid。
88
+ VOLATILE_DIRS = ("_index_log",)
89
+
90
+ # 运行态文件(2026-09-16 取证补充):真源被多 harness 共享时,外部写入者按自身
91
+ # 节拍追加审计/重建引用索引。取证:真源 `_audit.jsonl` 尾部 actor="zcode"
92
+ # (session=sess_775ac68e480e)连续 add code_*/cond_* —— 并发跑套件期间静置
93
+ # 60s 无变化、非常驻写者,写入随外部 agent 活动发生。E10 若把它们计入指纹,
94
+ # 断言对象就变成「全环境静置」而非「本进程零写入」→ 误报。故显式排除,
95
+ # 并以 E10b/E10c(本进程 pid 与 actor 归因)承担「本进程零写入」的可裁决性。
96
+ VOLATILE_FILES = ("_audit.jsonl", "_refindex.json")
97
+ #
98
+ # 运行态面扩充(2026-09-19 取证,第4条):`VOLATILE_FILES` 只列了审计/引用索引,
99
+ # 漏了同属常驻进程节拍面的状态留痕,致 E10 的降级判据失效——取证:跑 M1 期间
100
+ # `_crypto.jsonl`(crypto.AUDIT_FILE,尾部 actor="codebuddy" 的 open_failed)与
101
+ # `_sustain.jsonl`(sustain.SUSTAIN_LOG,尾部 op="heal"/action="flush_index"/
102
+ # pid=24788)mtime 与 `_audit.jsonl` 同为 20:31:28~30(同秒改写),且**审计行数
103
+ # 未增**(仅内容刷新)→ 「审计新增行」不足以判定外部写入,须以指纹面为准。
104
+ RUN_STATE_FILES = ("_audit.jsonl", "_refindex.json", "_crypto.jsonl",
105
+ "_sustain.jsonl")
106
+
107
+ #: M1 侧可能出现的审计 actor(CLI/管线)。当前 M1 为纯读链路(generate→bundle→
108
+ #: assemble 零写入),故预期新增行恒为空;留作「若将来引入写入」的兜底归因面。
109
+ _M1_ACTORS = ("mreview", "m1", "cli", "test_mr_m1")
110
+
111
+
112
+ def _run_state_probe(root):
113
+ """易变面指纹——「本次运行期间外部写入是否发生」的直接证据。
114
+
115
+ 与 `snapshot(exclude_files=VOLATILE_FILES)` 互补:这里**只取**运行态面
116
+ (常驻进程按节拍 flush 的心跳/审计/加密留痕),逐文件 md5;两时刻差异
117
+ 非空即外部写入存在。取证见 RUN_STATE_FILES 上方(同秒改写但审计行数未增)。
118
+ """
119
+ out = {}
120
+ for fn in RUN_STATE_FILES:
121
+ try:
122
+ with open(os.path.join(root, fn), "rb") as f:
123
+ out[fn] = hashlib.md5(f.read()).hexdigest()
124
+ except OSError:
125
+ continue
126
+ return out
127
+
128
+
129
+ def _run_state_diff(before, after):
130
+ return sorted(k for k in set(before) | set(after)
131
+ if before.get(k) != after.get(k))
132
+
133
+
134
+ def _external_actors(root, tail=200):
135
+ """环境级「外部写入者存在」证据——与本次观测窗口无关。
136
+
137
+ E10 原判据(仅「本次窗口内运行态面变更」)实测**不稳定**:外部写入按心跳
138
+ 节拍间歇发生,是否落在观测窗口内是运气——2026-09-19 实测同一条命令连跑两次,
139
+ 一次 `external` 非空(E10 降级)、一次为空(E10 走断言且恰好通过),而
140
+ E11 的偏离两次都在。故补环境级判据:审计尾部含非 `_M1_ACTORS` 的 actor,
141
+ 即证明该库被本进程之外的写者持续写入(共享前提成立),偏离可归因外部增长。
142
+ """
143
+ p = os.path.join(root, "_audit.jsonl")
144
+ try:
145
+ with open(p, "rb") as f:
146
+ f.seek(0, os.SEEK_END)
147
+ f.seek(max(0, f.tell() - 65536)) # 尾部 64KB ≈ 数百行,避免全量载入
148
+ chunk = f.read().decode("utf-8", "replace")
149
+ except OSError:
150
+ return []
151
+ actors = set()
152
+ for ln in chunk.splitlines()[-tail:]:
153
+ if not ln.strip():
154
+ continue
155
+ try:
156
+ a = json.loads(ln).get("actor")
157
+ except Exception:
158
+ continue # 首行可能被窗口截断,跳过不炸
159
+ if a and a not in _M1_ACTORS:
160
+ actors.add(str(a))
161
+ return sorted(actors)
162
+
163
+
164
+ def _audit_lines(root):
165
+ """审计行数(不解析内容,只做增量基数;轮转致行数下降时由调用方判空)。"""
166
+ p = os.path.join(root, "_audit.jsonl")
167
+ try:
168
+ with open(p, "rb") as f:
169
+ return sum(1 for _ in f)
170
+ except OSError:
171
+ return 0
172
+
173
+
174
+ def _audit_new_since(root, base_lines):
175
+ """返回审计新增行(解析失败行跳过);轮转(行数下降)视为无可归因新增。"""
176
+ p = os.path.join(root, "_audit.jsonl")
177
+ try:
178
+ with open(p, encoding="utf-8", errors="replace") as f:
179
+ lines = f.readlines()
180
+ except OSError:
181
+ return []
182
+ if len(lines) <= base_lines:
183
+ return []
184
+ out = []
185
+ for ln in lines[base_lines:]:
186
+ try:
187
+ out.append(json.loads(ln))
188
+ except Exception: # noqa: BLE001
189
+ continue
190
+ return out
191
+
192
+
193
+ def _is_atomic_tmp(fn):
194
+ """原子写的中间态文件(`X.md.tmp-<rand>` / `.<name>.tmp-*`)。
195
+
196
+ 取证(2026-09-16):真源被外部写入者(`_audit.jsonl` 尾部 actor="zcode")
197
+ 以「写 .tmp 再 rename」方式更新,快照恰在窗口内会撞 FileNotFoundError,
198
+ 或把同一份件的两个中间态算成差异——两者都与「M1 是否写入」无关。
199
+ """
200
+ return ".tmp-" in fn or fn.startswith(".") and ".md.tmp" in fn
201
+
202
+
203
+ def snapshot(root, *, exclude_dirs=(), exclude_files=()):
204
+ """记忆数据面指纹;排除面必须显式声明且带取证理由(见上方常量)。
205
+
206
+ 容错:外部写入者的原子写中间态与并发删除**跳过不记**(读取失败即剔除,
207
+ 宁可漏记一次中间态,也不把环境噪声冒充成 M1 写入)。
208
+ """
209
+ ex = set(exclude_dirs)
210
+ exf = set(exclude_files)
211
+ out = {}
212
+ for dp, dns, fns in os.walk(root):
213
+ dns[:] = [d for d in dns if d not in ex]
214
+ for fn in fns:
215
+ if fn in exf or _is_atomic_tmp(fn):
216
+ continue
217
+ p = os.path.join(dp, fn)
218
+ try:
219
+ with open(p, "rb") as f:
220
+ out[os.path.relpath(p, root)] = hashlib.md5(f.read()).hexdigest()
221
+ except OSError:
222
+ continue
223
+ return out
224
+
225
+
226
+ def _diff(before, after):
227
+ ch = sorted(k for k in set(before) & set(after) if before[k] != after[k])
228
+ return "改=%s 增=%s 删=%s" % (ch[:5], sorted(set(after) - set(before))[:5],
229
+ sorted(set(before) - set(after))[:5])
230
+
231
+
232
+ # 引擎默认 issue_kind(规则 JSON 未显式声明 issue_kind 时的契约值)
233
+ _DEFAULT_KIND = {"field_absent": "missing_field", "evidence_zero": "missing_field",
234
+ "field_ratio_below": "package_ratio", "dup_hash_group": "dup_content",
235
+ "template_flow_digits_only": "template_flow", "basis_licensed": "weak_source"}
236
+
237
+
238
+ def declared_kinds():
239
+ """规则库声明的 issue_kind 全集——机械分类不得越出此界。"""
240
+ ks = set()
241
+ for r in RS.load_rules()["rules"]:
242
+ for s in (r.get("mechanical") or []):
243
+ ks.add(s.get("issue_kind") or _DEFAULT_KIND.get(s.get("check"), s.get("check")))
244
+ return ks
245
+
246
+
247
+ def rows_of(bundles):
248
+ return [e for b in bundles for e in b["entries"]]
249
+
250
+
251
+ # A 组语料:knowledge 4(k1 三缺、k3/k4 同指纹)、contextual 1、self 1(legacy 边)
252
+ A_NODES = {
253
+ "k1": node("k1", role=None, evidence_count=0, verification_basis="", tags=["doc:note"]),
254
+ "k2": node("k2", role="knowledge-card", evidence_count=2, verification_basis="test"),
255
+ "k3": node("k3", role="knowledge-card", evidence_count=1, verification_basis="test",
256
+ content_hash="dup"),
257
+ "k4": node("k4", role="knowledge-card", evidence_count=1, verification_basis="test",
258
+ content_hash="dup"),
259
+ "c1": node("c1", layer="contextual"),
260
+ "s1": node("s1", layer="self", importance=0.9,
261
+ edges=[{"type": "part_of", "target": "k2"}]),
262
+ }
263
+ A_ACCESS = ({"t": 1, "ids": ["k2", "c1"], "tier": "core"},)
264
+ TMPL = "# 功能名:批次%d收官记忆\n\n内容各不相同 %d"
265
+
266
+
267
+ def _count(items, kind):
268
+ return sum(1 for i in items if i.get("issue_kind") == kind)
269
+
270
+
271
+ # ---------------------------- A 级 1:候选生成 ----------------------------
272
+
273
+ def phase_a(tmp):
274
+ print("[A] 级 1 候选生成")
275
+ rA = mkroot(os.path.join(tmp, "a"), dict(A_NODES), access=A_ACCESS)
276
+ nodesA = dict(A_NODES)
277
+
278
+ gen = CD.generate(rA, nodes=nodesA, now=0.0, patrol_offset=0)
279
+ kinds = {c["key"]: c["issue_kinds"] for c in gen["candidates"]}
280
+ ok(gen["by_source"]["assertion"] > 0 and gen["total"] > 0, "A1 断言源产出候选")
281
+ ok({"missing_field", "mixed_layer", "unanalyzed"} <= set(kinds.get("k1", [])),
282
+ f"A2 k1 三问题聚合一条候选(实得 {kinds.get('k1')})")
283
+ ok("dup_content" in kinds.get("k3", []) and "dup_content" in kinds.get("k4", []),
284
+ "A3 同指纹两条都进候选")
285
+
286
+ items = CD._assertion_items(nodesA)
287
+ kn = {n: r for n, r in nodesA.items() if str(r.get("layer")) == "knowledge"}
288
+ cov = CF._coverage_metrics(nodesA)
289
+ n_role = _count([i for i in items if "role" in i["evidence"]], "missing_field")
290
+ n_ev = _count([i for i in items if "evidence_count" in i["evidence"]], "missing_field")
291
+ ok(n_role == round((1 - cov["role_ratio_kn"]) * len(kn)),
292
+ f"A4 role 条目数 == 覆盖率反推(实得 {n_role})")
293
+ ok(n_ev == round((1 - cov["evidence_ratio_kn"]) * len(kn)),
294
+ f"A5 evidence 条目数 == 覆盖率反推(实得 {n_ev})")
295
+ ok(_count(items, "mixed_layer") == cov["mixed_layer"],
296
+ "A6 混层条目数 == conformance.stratum.mixed_ratio 分子")
297
+ ok(_count(items, "dup_content") == CF._dup_metrics(nodesA)["nodes"],
298
+ "A7 重复条目数 == conformance.dup 涉及节点数")
299
+ ok(_count(items, "unanalyzed") == CF.unanalyzed(nodesA)["count"],
300
+ "A8 unanalyzed 条目数 == conformance 派生判据")
301
+ ok(_count(items, "unanalyzed") == 3,
302
+ f"A9 unanalyzed 全库口径展开=3(实得 {_count(items, 'unanalyzed')})")
303
+
304
+ only = CD.generate(rA, sources=("assertion",), nodes=nodesA, now=0.0)
305
+ ok(set(only["by_source"]) == {"assertion"}, "A10 分源开关生效(只跑断言源)")
306
+
307
+ cold = CD.generate(rA, sources=("cold",), nodes=nodesA, now=0.0, cold_limit=10)
308
+ cids = {c["node_id"] for c in cold["candidates"]}
309
+ ok("s1" in cids and "k2" not in cids and "c1" not in cids,
310
+ f"A11 冷节点=未触达且 importance≥0.6(实得 {sorted(cids)})")
311
+ ok(all(c["issue_kinds"] == ["cold"] for c in cold["candidates"]), "A12 冷节点候选仅带 cold")
312
+
313
+ p8 = {f"p{i}": node(f"p{i}", importance=0.5) for i in range(8)}
314
+ w0, w1 = CD._patrol_items(p8, 2, 0), CD._patrol_items(p8, 2, 1)
315
+ s0, s1 = {x["node_id"] for x in w0}, {x["node_id"] for x in w1}
316
+ ok(len(s0) == 2 and len(s1) == 2 and not (s0 & s1), "A13 轮巡相邻窗口不重")
317
+ ok({x["node_id"] for x in CD._patrol_items(p8, 2, 0)} == s0, "A14 轮巡同 offset 可复现")
318
+ ok({x["node_id"] for x in CD._patrol_items(p8, 2, 4)} == s0,
319
+ "A15 轮巡整除时环绕回起点(窗口不重不漏闭合)")
320
+ ok(len({x["node_id"] for x in CD._patrol_items(p8, 3, 2)}) == 3,
321
+ "A16 轮巡非整除窗口内不重(环绕补足)")
322
+ ok(all(x["node_id"] in p8 for x in CD._patrol_items(p8, 2, 1)), "A17 轮巡条目不越界")
323
+
324
+ rD = mkroot(os.path.join(tmp, "defer"), dict(A_NODES), access=A_ACCESS,
325
+ inbox=[{"pid": "p1", "id": "k1", "layer": "knowledge",
326
+ "content": "提案正文", "tags": ["doc:x"], "payload_hash": "h1"},
327
+ {"pid": "p2", "id": "k2", "layer": "knowledge"}],
328
+ decisions=[{"pid": "p2", "decision": "accept"}],
329
+ forgetting=[{"verdict": "DEFER", "node_id": "ghost", "reason": "证据不足"},
330
+ {"verdict": "DEFER", "node_id": "k2", "reason": "冲突"},
331
+ {"verdict": "DROP", "node_id": "k3", "reason": "无关"}])
332
+ dq = CD.generate(rD, sources=("defer_queue",), nodes=dict(A_NODES), now=0.0)
333
+ pids = {c.get("proposal_id") for c in dq["candidates"]}
334
+ ok("p1" in pids and "p2" not in pids,
335
+ f"A18 被 decisions 覆盖的提案不入候选(实得 {sorted(x for x in pids if x)})")
336
+ ok(any(c["node_id"] == "k2" and c["origin"] == "node" for c in dq["candidates"]),
337
+ "A19 遗忘日志 DEFER 且节点在库 → 入候选")
338
+ ok(any(s["reason"] == "node_not_landed" for s in dq["skipped"]),
339
+ "A20 未落盘 DEFER 计入 skipped 不冒充候选")
340
+ ok("k3" not in {c["node_id"] for c in dq["candidates"]}, "A21 非 DEFER 裁决不入候选")
341
+
342
+ g1 = CD.generate(rA, nodes=nodesA, now=0.0, patrol_offset=0)
343
+ g2 = CD.generate(rA, nodes=nodesA, now=0.0, patrol_offset=0)
344
+ ok([c["key"] for c in g1["candidates"]] == [c["key"] for c in g2["candidates"]],
345
+ "A22 候选顺序确定性")
346
+ ok(json.dumps(g1, ensure_ascii=False, sort_keys=True)
347
+ == json.dumps(g2, ensure_ascii=False, sort_keys=True), "A23 候选全量逐字节确定")
348
+ pri = [c["priority"] for c in g1["candidates"]]
349
+ ok(pri == sorted(pri), "A24 候选按优先级排序(assertion<defer<cold<patrol)")
350
+
351
+ wr = CD.generate(rA, nodes=nodesA, now=0.0, with_report=True)
352
+ rep = CF.check(rA, check_paths=False)
353
+ ok(wr["assertions"]["verdict"] == rep["verdict"], "A25 with_report verdict 与 conformance 同源")
354
+ ok(wr["report"] == CF.metrics_of(rep), "A26 with_report 指标与 metrics_of 同源")
355
+ ok(set(CD.SOURCES) == {"assertion", "defer_queue", "cold", "patrol"}, "A27 四源齐备")
356
+ ok("report" not in gen and "report" in wr, "A28 报告 opt-in(默认不产出)")
357
+
358
+
359
+ # ---------------------------- B 级 2:捆绑 ----------------------------
360
+
361
+ def _cand(key, tags=(), kinds=("cold",), content=""):
362
+ return {"key": key, "node_id": key, "tags": list(tags),
363
+ "issue_kinds": list(kinds), "evidence": [], "content": content}
364
+
365
+
366
+ def phase_b(tmp):
367
+ print("[B] 级 2 捆绑")
368
+ bnodes = {f"t{i}": node(f"t{i}", tags=["batch:report"]) for i in range(3)}
369
+ bcands = [_cand(f"t{i}", ["batch:report"], content=TMPL % (i, i)) for i in range(3)]
370
+ rB = mkroot(os.path.join(tmp, "b"), bnodes, access=())
371
+ bd = BD.bundle(bcands, nodes=bnodes, root=rB)
372
+ ok(len(bd["bundles"]) == 1 and bd["bundles"][0]["group_kind"] == "template",
373
+ f"B1 同模板组一包一评(实得 {[b['group_kind'] for b in bd['bundles']]})")
374
+ ok(bd["bundles"][0]["size"] == 3, "B2 同模板三条并入一包")
375
+ ok(bd["bundles"][0]["group_sig_kind"] == "strong", "B3 标签题命中强模板签名 t:")
376
+ bd2 = BD.bundle(bcands, nodes=bnodes, root=rB)
377
+ ok(json.dumps(bd, ensure_ascii=False, sort_keys=True)
378
+ == json.dumps(bd2, ensure_ascii=False, sort_keys=True), "B4 包号与内容逐字节确定")
379
+ ok(len({b["bundle_id"] for b in bd["bundles"]}) == len(bd["bundles"]), "B5 包号唯一")
380
+
381
+ one = BD.bundle(bcands[:1], nodes=bnodes, root=rB)
382
+ ok(len(one["bundles"]) == 1 and one["bundles"][0]["group_kind"] == "batch",
383
+ "B6 单条模板组降级并入 batch(不误聚)")
384
+
385
+ lnodes = {"l1": node("l1", branch_id="br1"), "l2": node("l2", branch_id="br1"),
386
+ "l3": node("l3", branched_from="br1")}
387
+ rL = mkroot(os.path.join(tmp, "lin"), lnodes, access=(), write_files=False)
388
+ lbd = BD.bundle([_cand(k, content="") for k in lnodes], nodes=lnodes, root=rL)
389
+ ok(len(lbd["bundles"]) == 1 and lbd["bundles"][0]["group_kind"] == "lineage",
390
+ f"B7 同血缘成一组(实得 {[b['group_kind'] for b in lbd['bundles']]})")
391
+
392
+ tnodes = {"q1": node("q1", tags=["goal:alpha"]), "q2": node("q2", tags=["goal:beta"]),
393
+ "q3": node("q3", tags=["vision:gamma"]), "q4": node("q4", tags=["vision:delta"])}
394
+ rT = mkroot(os.path.join(tmp, "topic"), tnodes, access=(), write_files=False)
395
+ tbd = BD.bundle([_cand(k, tnodes[k]["tags"], content="") for k in tnodes],
396
+ nodes=tnodes, root=rT)
397
+ ok(len(tbd["bundles"]) == 2 and all(b["group_kind"] == "topic" for b in tbd["bundles"]),
398
+ f"B8 不同主题前缀分属不同 topic 组(实得 {[b['group_kind'] for b in tbd['bundles']]})")
399
+ ok(sorted(tbd["stats"]["by_group_kind"].items()) == [("topic", 2)],
400
+ f"B8b 同主题两条合包、异主题分属(实得 {tbd['stats']['by_group_kind']})")
401
+
402
+ big = {f"x{i}": node(f"x{i}") for i in range(60)}
403
+ bigc = [_cand(k, content=TMPL % (i, i)) for i, k in enumerate(big)]
404
+ rG = mkroot(os.path.join(tmp, "big"), big, access=())
405
+ gb = BD.bundle(bigc, nodes=big, root=rG, max_per_bundle=50)
406
+ ok(len(gb["bundles"]) == 2 and gb["stats"]["largest"] == 50,
407
+ f"B9 超 max_per_bundle 切分(实得 {[b['size'] for b in gb['bundles']]})")
408
+ ok(sum(b["size"] for b in gb["bundles"]) == 60, "B10 切分后条目总数守恒")
409
+
410
+ cnodes = {"z1": node("z1", content="盘上正文甲")}
411
+ zbd = BD.bundle([_cand("z1", content="")], nodes=cnodes,
412
+ root=mkroot(os.path.join(tmp, "cont"), cnodes, access=()))
413
+ ex = zbd["bundles"][0]["entries"][0]["excerpt"]
414
+ ok(ex and "盘上正文甲" in ex, "B11 索引无正文时按 path 回读盘上正文")
415
+ ok(zbd["content_missing"] == 0, "B12 回读成功 → content_missing=0")
416
+
417
+ rN = mkroot(os.path.join(tmp, "nocont"), cnodes, access=(), write_files=False)
418
+ nbd = BD.bundle([_cand("z1", content="")], nodes=cnodes, root=rN)
419
+ ok(nbd["content_missing"] == 1 and nbd["bundles"][0]["entries"][0]["excerpt"] is None,
420
+ "B13 读盘失败 → content_missing 如实计数、excerpt 置 None")
421
+
422
+ e0 = zbd["bundles"][0]["entries"][0]
423
+ ok({"ref", "node_id", "layer", "role", "evidence_count", "verification_basis",
424
+ "issue_kinds"} <= set(e0), "B14 条目字段面含机械检查所需全部字段")
425
+ ok(all(r in {c["key"] for c in bcands} for b in bd["bundles"] for r in b["refs"]),
426
+ "B15 包内 refs ⊆ 候选集(不凭空造条目)")
427
+ ok(bd["stats"]["bundles"] == len(bd["bundles"])
428
+ and bd["stats"]["candidates"] == len(bcands), "B16 stats 与实际一致")
429
+
430
+
431
+ # ---------------------------- C 级 3:规则装配 ----------------------------
432
+
433
+ def _mkpkg(entries, kind="batch"):
434
+ return {"bundle_id": "bT", "group_kind": kind, "group_key": "g", "entries": entries}
435
+
436
+
437
+ def _entry(ref, layer="knowledge", role="knowledge-card", ev=1, basis="test",
438
+ kinds=(), excerpt="", tags=(), chash=""):
439
+ return {"ref": ref, "node_id": ref, "layer": layer, "role": role, "evidence_count": ev,
440
+ "verification_basis": basis, "issue_kinds": list(kinds), "excerpt": excerpt,
441
+ "content_hash": chash, "tags": list(tags), "importance": 0.5,
442
+ "writer": "w", "session": "s"}
443
+
444
+
445
+ def phase_c(tmp):
446
+ print("[C] 级 3 规则装配")
447
+ rl = RS.load_rules()
448
+ ids = [r["id"] for r in rl["rules"]]
449
+ ok(rl["count"] == 7, f"C1 仓内规则库 7 条(实得 {rl['count']})")
450
+ ok(set(ids) == {"R-DUP-HASH-GROUP", "R-ROLE-MISSING", "R-EVIDENCE-ZERO",
451
+ "R-BASIS-MISSING", "R-BASIS-RATIO-PACKAGE", "R-BASIS-LICENSE",
452
+ "R-TEMPLATE-FLOW"}, f"C2 规则 id 集合正确(实得 {sorted(ids)})")
453
+ ok(all(r["severity"] in RS.SEVERITIES for r in rl["rules"]), "C3 severity 枚举合法")
454
+ ok(sorted(RS.MECH_CHECKS) == ["basis_licensed", "dup_hash_group", "evidence_zero",
455
+ "field_absent", "field_ratio_below",
456
+ "template_flow_digits_only"],
457
+ "C4 六检查器注册表齐备(引擎面冻结)")
458
+ ok(all(r.get("title") for r in rl["rules"]), "C5 规则元信息齐备")
459
+
460
+ bad = os.path.join(tmp, "badrules")
461
+ os.makedirs(bad, exist_ok=True)
462
+
463
+ def _bad(doc):
464
+ for fn in os.listdir(bad):
465
+ os.remove(os.path.join(bad, fn))
466
+ with open(os.path.join(bad, "r.json"), "w", encoding="utf-8") as f:
467
+ json.dump(doc, f, ensure_ascii=False)
468
+ try:
469
+ RS.load_rules(bad)
470
+ return ""
471
+ except ValueError as e:
472
+ return str(e)
473
+
474
+ m = _bad({"rules": [{"id": "X", "matcher": {}}]})
475
+ ok("缺字段" in m, f"C6 规则缺 severity → ValueError 不静默降级({m})")
476
+ m = _bad({"rules": [{"id": "X", "matcher": {}, "severity": "FATAL"}]})
477
+ ok("severity" in m, "C7 severity 非法 → ValueError")
478
+ m = _bad({"rules": [{"id": "X", "matcher": {}, "severity": "INFO"},
479
+ {"id": "X", "matcher": {}, "severity": "INFO"}]})
480
+ ok("重复" in m, "C8 规则 id 重复 → ValueError")
481
+ m = _bad({"rules": [{"id": "X", "matcher": {}, "severity": "INFO",
482
+ "mechanical": [{"check": "no_such_check"}]}]})
483
+ ok("未知" in m, "C9 未知检查器 → ValueError(引擎白名单 fail-closed)")
484
+ ok(_bad({"rules": []}) == "", "C10 空规则库不炸(容错)")
485
+
486
+ # matcher / scope
487
+ asm = RS.assemble(_mkpkg([_entry("a", role=None, ev=0)]), rules=rl)
488
+ kinds = collections.Counter(i["issue_kind"] for i in asm["mechanical"])
489
+ ok(kinds["missing_field"] >= 1, "C11 field_absent(role) 命中")
490
+ ok(any(i.get("field") == "role" and i.get("rule_id") == "R-ROLE-MISSING"
491
+ for i in asm["mechanical"]),
492
+ "C12 issue 携带 field 与 rule_id 定位")
493
+ ok(all(i["ref"] == "a" for i in asm["mechanical"]
494
+ if i["issue_kind"] != "package_ratio"), "C13 条目级 issue 指回 ref")
495
+
496
+ ctx = RS.assemble(_mkpkg([_entry("c", layer="contextual", role=None, ev=0)]), rules=rl)
497
+ ok(not any(i["issue_kind"] == "missing_field" for i in ctx["mechanical"]),
498
+ "C14 layer=knowledge 规则不跨层套用 contextual 条目")
499
+
500
+ pkg = _mkpkg([_entry("d1", basis=""), _entry("d2", basis="test")])
501
+ d = RS.assemble(pkg, rules=rl)
502
+ ok(any(i["issue_kind"] == "package_ratio" and i.get("ref") is None
503
+ for i in d["mechanical"]),
504
+ "C15 field_ratio_below 产出包级 issue(ref=None)")
505
+
506
+ dup = _mkpkg([_entry("e1", chash="H"), _entry("e2", chash="H"), _entry("e3", chash="H")])
507
+ dd = RS.assemble(dup, rules=rl)
508
+ n = sum(1 for i in dd["mechanical"] if i["issue_kind"] == "dup_content")
509
+ ok(n == 2, f"C16 dup_hash_group 除首条外逐条(3 条同指纹 → 2,实得 {n})")
510
+ ok(any(i.get("ref") is not None and "e1" in i.get("evidence", "")
511
+ for i in dd["mechanical"] if i["issue_kind"] == "dup_content"),
512
+ "C17 重复 issue 证据点名组首,供 M2 一次裁决")
513
+ ok(not any(i["issue_kind"] == "dup_content" for i in
514
+ RS.assemble(_mkpkg([_entry("u1", chash="A"), _entry("u2", chash="B")]),
515
+ rules=rl)["mechanical"]),
516
+ "C17b 指纹不同 → 不误报重复")
517
+
518
+ tf = _mkpkg([_entry("f1", excerpt="# 功能名:批次1收官记忆\n证据 1 条"),
519
+ _entry("f2", excerpt="# 功能名:批次2收官记忆\n证据 2 条")])
520
+ tt = RS.assemble(tf, rules=rl)
521
+ ok(any(i["issue_kind"] == "template_flow" for i in tt["mechanical"]), "C18 去数字骨架相同 → 流水命中")
522
+ tf2 = _mkpkg([_entry("f1", excerpt="# 功能名:批次1收官记忆"),
523
+ _entry("f2", excerpt="# 另一件事:完全不同正文结构")])
524
+ ok(not any(i["issue_kind"] == "template_flow"
525
+ for i in RS.assemble(tf2, rules=rl)["mechanical"]), "C19 骨架不同 → 不误报")
526
+
527
+ sci = _mkpkg([_entry("g1", basis="textbook", tags=["数学"])])
528
+ ok(any(i["issue_kind"] == "weak_source" for i in RS.assemble(sci, rules=rl)["mechanical"]),
529
+ "C20 science 赛道 + textbook 基底 → 来源执照报 weak_source")
530
+ hum = _mkpkg([_entry("g2", basis="textbook", tags=["文学"])])
531
+ ok(not any(i["issue_kind"] == "weak_source"
532
+ for i in RS.assemble(hum, rules=rl)["mechanical"]), "C21 humanities 同基底 → 不报")
533
+ und = _mkpkg([_entry("g3", basis="textbook", tags=["zzz"])])
534
+ ok(any(i["issue_kind"] == "weak_source" for i in RS.assemble(und, rules=rl)["mechanical"]),
535
+ "C22 赛道未定 → 机械面一律报(crosscheck「一律不发放」语义,供 M2 复核)")
536
+
537
+ # 规则数据化:新增一条自定义规则,引擎零改动即生效
538
+ cust = os.path.join(tmp, "custrules")
539
+ os.makedirs(cust, exist_ok=True)
540
+ RS.load_rules() # 预热
541
+ src = RS.RULES_DIR
542
+ shutil.copytree(src, cust, dirs_exist_ok=True)
543
+ with open(os.path.join(cust, "zz_custom.json"), "w", encoding="utf-8") as f:
544
+ json.dump({"spec_version": 1, "domain": "custom", "rules": [
545
+ {"id": "R-CUSTOM-ROLE", "title": "自定规则", "severity": "INFO",
546
+ "matcher": {"layer": ["knowledge"]},
547
+ "mechanical": [{"check": "field_absent", "field": "role",
548
+ "issue_kind": "custom_role"}],
549
+ "llm": [], "remedy": "noop"}]}, f, ensure_ascii=False)
550
+ rl2 = RS.load_rules(cust)
551
+ ok(rl2["count"] == 8, f"C23 规则库热加一条 → 8 条(实得 {rl2['count']})")
552
+ a2 = RS.assemble(_mkpkg([_entry("h1", role=None)]), rules=rl2)
553
+ ok(any(i["issue_kind"] == "custom_role" for i in a2["mechanical"]),
554
+ "C24 新规则零代码改动即生效(G2 数据化)")
555
+
556
+ # assemble_all 汇总口径
557
+ res = {"candidates": [{"key": "a"}, {"key": "b"}],
558
+ "bundles": [{"bundle_id": "b1", "group_kind": "batch", "group_key": "g",
559
+ "entries": [_entry("a", role=None)]},
560
+ {"bundle_id": "b2", "group_kind": "batch", "group_key": "g",
561
+ "entries": [_entry("b", ev=0)]}],
562
+ "stats": {}}
563
+ allr = RS.assemble_all(res, rules=rl)
564
+ tot = sum(len(p["mechanical"]) for p in allr["packages"])
565
+ ok(allr["stats"]["mechanical_total"] == tot, "C25 assemble_all 汇总 == 各包之和")
566
+ ok(allr["stats"]["llm_checks"] == sum(len(p["llm"]) for p in allr["packages"]),
567
+ "C26 llm 计数同口径")
568
+ ok(allr["packages"][0]["bundle_id"] == "b1"
569
+ and allr["stats"]["bundles"] == 2, "C27 包序与包数稳定")
570
+ ok(allr["stats"]["rules"] == [r["id"] for r in rl["rules"]],
571
+ "C28 汇总内嵌生效规则清单(可复算)")
572
+ ok(all(p["llm"] == [] or all("scope_refs" in c for c in p["llm"])
573
+ for p in allr["packages"]), "C29 LLM 检查清单带 scope_refs 供 M2 圈定范围")
574
+ ok(all(not p["matched"] or all({"id", "severity", "scope", "remedy"} <= set(m)
575
+ for m in p["matched"]) for p in allr["packages"]),
576
+ "C30 matched 记录规则命中理由/范围/处方")
577
+
578
+ a_list = RS.assemble(_mkpkg([_entry("m1", role=None)]), rules=rl["rules"])
579
+ a_dict = RS.assemble(_mkpkg([_entry("m1", role=None)]), rules=rl)
580
+ ok(a_list == a_dict, "C31 rules 入参防呆:列表与 load_rules 结果等价")
581
+ try:
582
+ RS.assemble(_mkpkg([_entry("m2")]), rules={"count": 0})
583
+ ok(False, "C32 规则入参形态错误 → 明确报错不静默")
584
+ except ValueError as e:
585
+ ok("rules" in str(e), "C32 规则入参形态错误 → 明确报错不静默")
586
+
587
+
588
+ # ---------------------------- D CLI ----------------------------
589
+
590
+ def phase_d(tmp):
591
+ print("[D] CLI")
592
+ rC = mkroot(os.path.join(tmp, "cli"), dict(A_NODES), access=A_ACCESS)
593
+ d = CLI.run(rC)
594
+ ok({"root", "candidates", "bundles", "bundle_stats", "assemble_stats",
595
+ "mechanical_by_rule", "llm_checks"} <= set(d),
596
+ f"D1 run 返回结构齐备(实得 {sorted(d)})")
597
+ ok(d["candidates"]["total"] > 0 and d["bundle_stats"]["bundles"] > 0,
598
+ "D2 run 产出候选与包")
599
+ ok(d["assemble_stats"]["mechanical_total"] >= 1 and d["mechanical_by_rule"],
600
+ "D3 run 含规则装配结果(逐规则计数)")
601
+ ok(d["bundle_stats"]["candidates"] == d["candidates"]["total"], "D4 捆绑条目数与候选同口径")
602
+
603
+ out = os.path.join(tmp, "m1.json")
604
+ rc = CLI.main(["--root", rC, "--out", out])
605
+ ok(rc == 0, f"D5 main 正常退出码 0(实得 {rc})")
606
+ ok(os.path.exists(out), "D6 --out 落盘产物存在")
607
+ with open(out, encoding="utf-8") as f:
608
+ jd = json.load(f)
609
+ ok(jd["candidates"]["total"] == d["candidates"]["total"], "D7 落盘内容与 run 同源")
610
+ ok(not ({"candidates_full", "bundles_full", "packages_full"} & set(jd)),
611
+ "D8 默认产物只含摘要不含逐条明细")
612
+
613
+ s = io.StringIO()
614
+ with redirect_stdout(s):
615
+ CLI.main(["--root", rC, "--full", "--out", os.path.join(tmp, "full.json")])
616
+ ok("机械检查" in s.getvalue() and "包" in s.getvalue(), "D9 摘要含机械聚合与包清单")
617
+ with open(os.path.join(tmp, "full.json"), encoding="utf-8") as f:
618
+ jf = json.load(f)
619
+ ok({"candidates_full", "bundles_full", "packages_full"} <= set(jf),
620
+ "D10 --full 追加三类明细")
621
+
622
+ before = snapshot(rC)
623
+ CD.generate(rC)
624
+ BD.bundle(CD.generate(rC)["candidates"], root=rC)
625
+ CLI.run(rC)
626
+ ok(snapshot(rC) == before, "D11 全链路零写入(库指纹不变)")
627
+
628
+
629
+ # ---------------------------- E 集成(真源存在才跑) ----------------------------
630
+
631
+ def _real_root():
632
+ cands = [os.environ.get("MDCG_ROOT")]
633
+ try:
634
+ from .datapath import mdcg_root
635
+ cands.append(mdcg_root())
636
+ except Exception:
637
+ pass
638
+ cands.append("D:/Program Files/2_ai/AEIS/data/mdcg")
639
+ for r in cands:
640
+ if r and os.path.exists(os.path.join(r, "_index.json")):
641
+ return r
642
+ return None
643
+
644
+
645
+ def phase_e():
646
+ print("[E] 集成(真源存在才跑)")
647
+ root = _real_root()
648
+ if not root:
649
+ print(" SKIP 真源库不可用(外部 clone 环境)——非失败")
650
+ return
651
+ before = snapshot(root, exclude_dirs=VOLATILE_DIRS, exclude_files=VOLATILE_FILES)
652
+ vol_before = _run_state_probe(root)
653
+ audit_before = _audit_lines(root)
654
+ d = CLI.run(root, cold_limit=200)
655
+ nodes = CF.load_index(root) or {}
656
+ bb = d["bundle_stats"]
657
+ ok(bb["candidates"] == d["candidates"]["total"] and bb["candidates"] > 0,
658
+ "E1 捆绑条目数与候选同口径")
659
+ ok(8000 <= d["candidates"]["total"] <= 20000,
660
+ f"E2 真源欠账规模与基线 11195 同量级(实得 {d['candidates']['total']})")
661
+ ok(bb["bundles"] <= 3000, f"E3 包数在可评审规模内(实得 {bb['bundles']})")
662
+ ok(len(nodes) > 0 and d["candidates"]["total"] <= len(nodes) * 4,
663
+ "E4 候选量级与真源节点数相容")
664
+ asm = d["assemble_stats"]
665
+ ok(asm["mechanical_total"] > 0, "E5 真源机械 issue 非空")
666
+ ok(set(asm["mechanical_by_kind"]) <= declared_kinds(),
667
+ f"E6 机械分类 ⊆ 规则库声明(实得 {sorted(asm['mechanical_by_kind'])})")
668
+ ok({"missing_field", "dup_content", "weak_source"} <= set(asm["mechanical_by_kind"]),
669
+ f"E6b 真源三类核心 issue 必现(实得 {sorted(asm['mechanical_by_kind'])})")
670
+ ok(all(k in d["candidates"]["by_source"] for k in CD.SOURCES), "E7 四源统计齐备")
671
+ rep = d.get("assertions") or {}
672
+ ok(rep.get("verdict") in ("PASS", "WARN", "FAIL"),
673
+ f"E8 断言集 verdict 合法({rep.get('verdict')})")
674
+ ok(d["llm_checks"] and all(isinstance(t, (list, tuple)) and len(t) == 2
675
+ for t in d["llm_checks"]), "E9 LLM 待检清单可枚举(供 M2 排期)")
676
+ after = snapshot(root, exclude_dirs=VOLATILE_DIRS, exclude_files=VOLATILE_FILES)
677
+ external = _run_state_diff(vol_before, _run_state_probe(root))
678
+ ext_actors = _external_actors(root)
679
+ # 「外部写入存在」= 本次窗口内确有变更 **或** 环境级存在非本进程写入者。
680
+ # 只用前者不稳定(节拍间歇,见 _external_actors 注释),只用后者过于宽泛,
681
+ # 取或并逐条报告证据来源。
682
+ shared_writes = bool(external or ext_actors)
683
+ logs = (os.listdir(os.path.join(root, "_index_log"))
684
+ if os.path.isdir(os.path.join(root, "_index_log")) else [])
685
+ _pidpref = str(os.getpid()) + "-"
686
+ ok(not any(s.startswith(_pidpref) for s in logs),
687
+ "E10b M1 未在真源留下本进程增量日志(写入者归属外部)")
688
+ # E10c:把「本进程零写入」从「全环境静置」里分离出来——审计新增行逐条按
689
+ # actor/pid 归因,只认本进程的行(VOLATILE_FILES 已排除的外部写入不背锅)。
690
+ new_audit = _audit_new_since(root, audit_before)
691
+ mine = [r for r in new_audit
692
+ if r.get("actor") in _M1_ACTORS or str(r.get("pid") or "") == str(os.getpid())]
693
+ ok(not mine, "E10c 审计新增行归属本进程为 0(实得 %d,新增 %d 行全部外部)"
694
+ % (len(mine), len(new_audit)))
695
+ if shared_writes:
696
+ # 环境共享前提不成立:E10 的字节级不变已不可归因于 M1,明确降级为
697
+ # 「不适用」并报告证据——不静默通过,也不把环境噪声判成本进程违规。
698
+ # 判据取证(2026-09-19,第4条):旧判据「审计新增行非空」漏判——实测外部
699
+ # 写入(_crypto/_sustain 被同秒改写)发生而同刻审计行数未增,E10 因此误报;
700
+ # 单用「本次窗口变更」又随心跳节拍不稳定(见 _external_actors 注释)。
701
+ print(" E10 不适用:真源被外部写入者并发写入(运行态文件本次变更 %s;"
702
+ "审计尾部外部 actor %s;新增审计 %d 行)"
703
+ "——本进程零写入由 E10b/E10c 承担"
704
+ % (external or ["-"], ext_actors or ["-"], len(new_audit)))
705
+ else:
706
+ ok(after == before, "E10 真源只读(记忆数据面指纹不变," + _diff(before, after) + ")")
707
+
708
+ # ---- E11~ 审计基线复现(§6 M1 验收口径)----
709
+ # 基线(2026-09-15 审计 node_26b0973a):混层 57.9% / role 空 99.99% /
710
+ # evidence_count 仅 6 条 / 精确重复 90 条 / 欠账 11195。库在增长,用容差区间。
711
+ # 降级判据(2026-09-19,与 E10 同构):E11~E16 断言的是**数据面快照统计量**,
712
+ # 其分子分母均由外部写入者推动——mixed_ratio = knowledge 层带 doc∪code 标签数 /
713
+ # knowledge 总数,新增一个 code_*/doc_* 节点即同时推高分子分母;比率变化方向有
714
+ # 闭式判据:Δratio 的符号 = 新增批次混层占比 p − 当前 ratio。实测 p≈96.6%
715
+ # ≫ 0.579,故比率单调上行、容差区间必被击穿(现场 0.6302 越上界 1.03pp)。
716
+ # 外部写入存在(shared_writes)时该区间已不描述本进程可控面 → 明确降级为
717
+ # 「不适用」并报告现场值(不静默通过,也不把外部增长判成本进程违规)。
718
+ # 边界如实标注:隔离/冻结库(无外部写入者)下仍照常判红——此时偏离即基线过期,
719
+ # 须人工重定基线,不豁免;这也意味着共享库上本组断言长期呈「不适用」,若需恢复
720
+ # 可裁决性,应改趋势判据(与本地上次快照比不劣化),属验收口径变更待定夺。
721
+ cov = CF.metrics_of(CF.check(root, check_paths=False))
722
+ kn = [k for k, v in nodes.items() if (v.get("layer") or "") == "knowledge"]
723
+ n_ev = sum(1 for k in kn if CF._as_int(nodes[k].get("evidence_count")) > 0)
724
+ if shared_writes:
725
+ print(" E11~E16 不适用:审计基线为 2026-09-15 快照,真源被外部写入者"
726
+ "并发写入(本次变更 %s;审计尾部外部 actor %s)→ 数据面统计量由外部"
727
+ "写入推动;现场值 混层 %.4f / role_ratio_kn %.6f / evidence %d / "
728
+ "dup %d 组 / knowledge %d / reach %.4f"
729
+ % (external or ["-"], ext_actors or ["-"],
730
+ cov["mixed_ratio"], cov["role_ratio_kn"], n_ev,
731
+ cov["dup_groups"], len(kn), cov["reach_ratio"]))
732
+ else:
733
+ ok(0.55 <= cov["mixed_ratio"] <= 0.62,
734
+ f"E11 复现混层率 57.9%(实得 {cov['mixed_ratio']:.4f})")
735
+ ok(cov["role_ratio_kn"] < 0.001,
736
+ f"E12 复现 role 近乎全空(有 role 占比 {cov['role_ratio_kn']:.6f})")
737
+ ok(n_ev == 6, f"E13 复现 evidence_count 仅 6 条(实得 {n_ev})")
738
+ ok(85 <= cov["dup_groups"] <= 105,
739
+ f"E14 复现精确重复 ~90 组(实得 {cov['dup_groups']})")
740
+ ok(10000 <= len(kn) <= 13000,
741
+ f"E15 复现 knowledge 存量 11195 量级(实得 {len(kn)})")
742
+ ok(cov["reach_ratio"] < 0.15,
743
+ f"E16 复现召回率低(reach_ratio {cov['reach_ratio']:.4f})")
744
+ # 边界如实标注(不猜测、不硬凑):基线「同模板冗余 233 条」无固化判据函数——实测两候选
745
+ # 口径均不等(writelimit.template_signature 全量分组冗余 1949;本引擎 template_flow
746
+ # 严格「只换数字」口径 13),故不纳入机械断言,留待 M3 定位模块抽样核对后固化。
747
+
748
+
749
+ # ---------------------------- main ----------------------------
750
+
751
+ def main():
752
+ tmp = tempfile.mkdtemp(prefix="mrev_")
753
+ try:
754
+ for ph in (phase_a, phase_b, phase_c, phase_d):
755
+ ph(tmp)
756
+ phase_e()
757
+ finally:
758
+ shutil.rmtree(tmp, ignore_errors=True)
759
+ print(f"\n== M1 机械层自测:{PASS} 通过 / {FAIL} 失败 ==")
760
+ if FAILS:
761
+ for f in FAILS:
762
+ print(" - " + f)
763
+ return 1
764
+ return 0
765
+
766
+
767
+ if __name__ == "__main__":
768
+ sys.exit(main())
769
+