@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,201 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_reach_keys.py · 新鲜度键与落盘的**编码无歧义**验收(r11 复核反例固化)。
3
+
4
+ 背景:_node_key 曾用 ';'/'|' 拼接 tags/edges,导致 ["a;b"] 与 ["a","b"] 生成同键;
5
+ 倒排曾用 ',' 与 chr(31) 拼接路径。本测试把编码层的不变性固化:
6
+ ① tags 含分隔符不碰撞 ② edges 含分隔符不碰撞
7
+ ③ 倒排落地/重载对含控制字符的路径原样保留 ④ TTL<=0 回落默认(不得静默关闭兜底)
8
+ ⑤ hash_complete 按原始 content_hash 判定
9
+
10
+ 运行:python -m md_cg.test_reach_keys
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import tempfile
17
+ import time
18
+
19
+ from . import reach
20
+
21
+ PASS = FAIL = 0
22
+
23
+
24
+ def ok(cond, label, detail=""):
25
+ global PASS, FAIL
26
+ PASS += int(bool(cond))
27
+ FAIL += int(not cond)
28
+ print(f'[{"✓" if cond else "✘"}] {label}' + (f" · {detail}" if detail else ""))
29
+
30
+
31
+ def _fake(root, **entry):
32
+ base = {"path": "knowledge/a.md", "content_hash": "H1", "tags": [], "edges": []}
33
+ base.update(entry)
34
+
35
+ class _CG:
36
+ def __init__(self):
37
+ self.root = root
38
+ self.index = {"nodes": {"n1": dict(base)}}
39
+ def _read(self, e):
40
+ return {}, ""
41
+ def _open_content(self, node_id, fm, content):
42
+ return content
43
+ return _CG()
44
+
45
+
46
+ def main():
47
+ root = tempfile.mkdtemp(prefix="reach_keys_")
48
+ # reach 为显式 opt-in(契约 §7:不在默认路径上启用新行为),本测试须自行开启
49
+ os.environ["MDCG_REACH"] = "1"
50
+ os.environ["MDCG_REACH_INDEX"] = os.path.join(root, "idx.json")
51
+ # r12 复核:无 path 的条目不计入覆盖判定(与 build 口径一致)
52
+ class _CG3:
53
+ def __init__(self):
54
+ self.root = root
55
+ self.index = {"nodes": {"n1": {"path": "knowledge/a.md", "content_hash": "H1"},
56
+ "n2": {"content_hash": None}}}
57
+ def _read(self, e):
58
+ return {}, ""
59
+ def _open_content(self, node_id, fm, content):
60
+ return content
61
+ ok(reach._hash_complete(_CG3()) is True,
62
+ "⑤ 无 path 条目不计入 hash 覆盖(与 build 口径一致)")
63
+
64
+ print("md_cg reach 键编码验收 · 无歧义 / TTL 兜底 / 落盘往返")
65
+ print("=" * 66)
66
+ # ① tags 分隔符不碰撞
67
+ k1 = reach._node_key(_fake(root, tags=["a;b"]).index["nodes"]["n1"])
68
+ k2 = reach._node_key(_fake(root, tags=["a", "b"]).index["nodes"]["n1"])
69
+ ok(k1 != k2, "① tags 含分号不碰撞", f"{k1[:26]} vs {k2[:26]}")
70
+ k3 = reach._node_key(_fake(root, tags=["a|b"]).index["nodes"]["n1"])
71
+ k4 = reach._node_key(_fake(root, tags=["a", "b"]).index["nodes"]["n1"])
72
+ ok(k3 != k4, "① tags 含竖线不碰撞")
73
+ # ② edges 分隔符不碰撞
74
+ e1 = reach._node_key(_fake(root, edges=[{"target": "a|b", "type": ""}]).index["nodes"]["n1"])
75
+ e2 = reach._node_key(_fake(root, edges=[{"target": "a", "type": "b"}]).index["nodes"]["n1"])
76
+ ok(e1 != e2, "② edges 含竖线不碰撞")
77
+ e3 = reach._node_key(_fake(root, edges=[{"target": "a;b", "type": "c"}]).index["nodes"]["n1"])
78
+ e4 = reach._node_key(_fake(root, edges=[{"target": "a", "type": "b;c"}]).index["nodes"]["n1"])
79
+ ok(e3 != e4, "② edges 含分号不碰撞")
80
+ # ③ 倒排落盘对控制字符路径原样往返
81
+ idx = reach.ReachIndex(root)
82
+ weird = "knowledge/a" + chr(31) + "b.md"
83
+ idx.post = {"召": [weird]}
84
+ idx.adj = {}
85
+ idx.hashes = {}
86
+ idx.save()
87
+ back = reach.ReachIndex(root)
88
+ back.load()
89
+ ok(back.post.get("召") == [weird], "③ 含控制字符路径经落盘/重载原样保留",
90
+ str(back.post.get("召")))
91
+ # ④ TTL<=0 → 回落默认(不得静默关闭)
92
+ class _CG2:
93
+ def __init__(self):
94
+ self.root = root
95
+ self.index = {"nodes": {"n1": {"path": "knowledge/a.md", "content_hash": "H1",
96
+ "tags": [], "edges": []}}}
97
+ cg = _CG2()
98
+ os.environ["MDCG_REACH_TTL"] = "0"
99
+ idx2 = reach.ReachIndex(root)
100
+ idx2.built_at = time.time() - 4000
101
+ saved = reach._CACHE
102
+ reach._CACHE = {}
103
+ ok(reach._ttl() == 600.0, "④ TTL=0 经 _ttl() 回落 600s", str(reach._ttl()))
104
+ os.environ["MDCG_REACH_TTL"] = "-5"
105
+ ok(reach._ttl() == 600.0, "④ TTL 负值同样回落默认")
106
+ os.environ["MDCG_REACH_TTL"] = "abc"
107
+ ok(reach._ttl() == 600.0, "④ TTL 非法文本回落默认")
108
+ os.environ["MDCG_REACH_TTL"] = "30"
109
+ ok(reach._ttl() == 30.0, "④ 正数 TTL 原样生效")
110
+ os.environ["MDCG_REACH_TTL"] = "inf"
111
+ ok(reach._ttl() == 600.0, "④ TTL=inf 回落默认(否则过期判断永假)", str(reach._ttl()))
112
+ os.environ["MDCG_REACH_TTL"] = "1e999"
113
+ ok(reach._ttl() == 600.0, "④ TTL=1e999(溢出为 inf)回落默认")
114
+ os.environ["MDCG_REACH_TTL"] = "nan"
115
+ ok(reach._ttl() == 600.0, "④ TTL=nan 回落默认")
116
+ os.environ["MDCG_REACH_TTL"] = "0"
117
+ reach._CACHE = saved
118
+ # ⑤ hash_complete 按原始 content_hash 判定
119
+ a = reach.ReachIndex(root); a.hash_complete = None
120
+ b = reach.ReachIndex(root); b.hash_complete = None
121
+ a.build(_fake(root, content_hash="H1"))
122
+ b.build(_fake(root, content_hash=None))
123
+ ok(a.hash_complete is True and b.hash_complete is False,
124
+ "⑤ hash_complete 按原始 content_hash", f"a={a.hash_complete} b={b.hash_complete}")
125
+ # r13 复核:真实磁盘新鲜度路径(不手写 _CACHE)
126
+ os.environ["MDCG_REACH_TTL"] = "0"
127
+ cg2 = _CG3()
128
+ idx_a = reach.ReachIndex(root)
129
+ idx_a.build(cg2)
130
+ idx_a.save()
131
+ idx_b = reach.ReachIndex(root)
132
+ idx_b.load()
133
+ ok(reach._disk_fresh(idx_b, cg2) is True, "⑤ 真实磁盘缓存:同库判新鲜")
134
+ cg3 = _CG3()
135
+ cg3.index["nodes"]["n1"]["content_hash"] = "H2"
136
+ ok(reach._disk_fresh(idx_b, cg3) is False, "⑤ 真实磁盘缓存:content_hash 变即判不新鲜")
137
+ # r13 复核:落盘失败(只读根)时不得入进程缓存(类级替换 save 注入 OSError)
138
+ saved_save = reach.ReachIndex.save
139
+ def _boom(self):
140
+ raise OSError('readonly')
141
+ reach.ReachIndex.save = _boom
142
+ try:
143
+ reach._CACHE.clear()
144
+ reach._index(cg2, force=True)
145
+ ok(len(reach._CACHE) == 0, '⑤ 落盘失败时不入进程缓存(下次调用重建)', str(list(reach._CACHE)))
146
+ finally:
147
+ reach.ReachIndex.save = saved_save
148
+ # r15 复核:库内缺 content_hash 时,即使本次刚重建也必须回退(不得收敛)
149
+ class _CG5:
150
+ def __init__(self):
151
+ self.root = root
152
+ self.index = {"nodes": {"n1": {"path": "knowledge/a.md", "tags": [], "edges": []}}}
153
+ def _read(self, e):
154
+ return {}, ""
155
+ def _open_content(self, node_id, fm, content):
156
+ return content
157
+ os.environ["MDCG_REACH_INDEX"] = os.path.join(root, "idx2.json")
158
+ reach._CACHE.clear()
159
+ got_h, meta_h = reach.narrow(_CG5(), [{"path": "knowledge/a.md"}], ["abcd"], {"abcd"}, None, q="abcd")
160
+ ok(got_h is None and meta_h.get("reach") == "hash_incomplete",
161
+ "⑤ 缺 content_hash 时一律回退不收敛", str(meta_h)[:80])
162
+ # r17 复核:库内含不可读节点时,磁盘缓存仍须判新鲜(口径与 build 的剔除对齐)
163
+ class _CG6:
164
+ def __init__(self):
165
+ self.root = root
166
+ self.index = {"nodes": {"n1": {"path": "knowledge/a.md", "content_hash": "H1"},
167
+ "n2": {"path": "knowledge/locked.md", "content_hash": "H2"}}}
168
+ def _read(self, e):
169
+ if e.get("path", "").endswith("locked.md"):
170
+ return None, None # 不可读(模拟解密失败)
171
+ return {}, ""
172
+ def _open_content(self, node_id, fm, content):
173
+ return content
174
+ os.environ["MDCG_REACH_INDEX"] = os.path.join(root, "idx3.json")
175
+ cg6 = _CG6()
176
+ i6 = reach.ReachIndex(root)
177
+ i6.build(cg6)
178
+ i6.save()
179
+ i6b = reach.ReachIndex(root)
180
+ i6b.load()
181
+ ok(reach._disk_fresh(i6b, cg6) is True,
182
+ "⑤ 含不可读节点时磁盘缓存仍判新鲜(跨进程可复用)",
183
+ "hashes=%d" % len(i6.hashes))
184
+ cg6.index["nodes"]["n1"]["content_hash"] = "H9"
185
+ ok(reach._disk_fresh(i6b, cg6) is False, "⑤ 已缓存节点 hash 变即判不新鲜")
186
+ # r18 复核:hash 不完整时——首次调用必须暴露首建成本;第二次调用不得再全库重建
187
+ os.environ["MDCG_REACH_INDEX"] = os.path.join(root, "idx4.json")
188
+ reach._CACHE.clear()
189
+ _cg7 = _CG5()
190
+ _g1, m1 = reach.narrow(_cg7, [{"path": "knowledge/a.md"}], ["abcd"], {"abcd"}, None, q="abcd")
191
+ ok(_g1 is None and m1.get("reach") == "hash_incomplete" and m1.get("reach_build_docs") is not None,
192
+ "⑤ hash 不完整:首次调用暴露首建成本", str(m1)[:90])
193
+ _g2, m2 = reach.narrow(_cg7, [{"path": "knowledge/a.md"}], ["abcd"], {"abcd"}, None, q="abcd")
194
+ ok(_g2 is None and m2.get("reach") == "hash_incomplete" and "reach_build_docs" not in m2,
195
+ "⑤ hash 不完整:第二次调用复用缓存(不再全库重建)", str(m2)[:90])
196
+ print(f"\n===== 键编码验收:{PASS}/{PASS + FAIL} 通过 =====")
197
+ return 0 if FAIL == 0 else 1
198
+
199
+
200
+ if __name__ == "__main__":
201
+ raise SystemExit(main())
@@ -0,0 +1,145 @@
1
+ # -*- coding: utf-8 -*-
2
+ """narrow() 各退化出口必须如实暴露首建 / 部分重建成本。
3
+
4
+ 历史取证:r19 复核发现 no_tokens / empty_converge 吞掉 reach_build_docs;
5
+ r32 发现 index_unavailable 连部分读盘成本也丢了、且旧测试用 sem=True 使 set(True)
6
+ 异常被吞掉;r34 发现只计「已入倒排」会漏计「读了很多但都不可读」的情形、且模块级状态不安全。
7
+ """
8
+ import os
9
+ import sys
10
+ import tempfile
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+ from md_cg import reach # noqa: E402
14
+
15
+
16
+ class StubIdx:
17
+ hash_complete = True
18
+ sem = [] # 必须是真容器:曾写 sem=True,set(True) 报错被吞掉 → 语义分支未真实验证(r32 复核)
19
+ built_now = True
20
+ built_docs = 7
21
+ built_reads = 9
22
+ hashes = {}
23
+ built_at = 0.0
24
+
25
+ def postings(self, toks):
26
+ return {"x"}
27
+
28
+
29
+ class ReadStubCG:
30
+ """按 plan 依次返回 cg._read 结果,plan 耗尽后抛错(模拟重建中途失败)。"""
31
+
32
+ def __init__(self, root, nodes, plan):
33
+ self.root = root
34
+ self.index = {"nodes": nodes}
35
+ self._plan = list(plan)
36
+ self.calls = 0
37
+
38
+ def _read(self, entry):
39
+ self.calls += 1
40
+ if not self._plan:
41
+ raise RuntimeError("simulated rebuild failure")
42
+ return self._plan.pop(0)
43
+
44
+ def _open_content(self, nid, fm, c):
45
+ return c
46
+
47
+
48
+ def _tmp_root():
49
+ return tempfile.mkdtemp(prefix="reach_meta_exits_")
50
+
51
+
52
+ def _use(path):
53
+ os.environ["MDCG_REACH_INDEX"] = os.path.join(path, "_reach_index.json")
54
+
55
+
56
+ def main():
57
+ os.environ["MDCG_REACH"] = "1"
58
+ import md_cg.mdcg as mdcg_mod
59
+ orig_with, orig_sem = reach._index_with_meta, mdcg_mod.semantic_on
60
+ mdcg_mod.semantic_on = lambda: True
61
+ reach._index_with_meta = lambda cg, force=False: (StubIdx(), {})
62
+ try:
63
+ # ① 无 token → no_tokens,必须带首建成本
64
+ _, m1 = reach.narrow(object(), [], [], set(), q="")
65
+ assert m1.get("reach") == "no_tokens", m1
66
+ assert m1.get("reach_build_docs") == 7, "no_tokens 出口漏首建成本:%r" % (m1,)
67
+ # ② 语义空收敛 → empty_converge,同样必须带首建成本
68
+ _, m2 = reach.narrow(object(), [], ["甲乙"], {"甲", "乙"}, q="甲乙")
69
+ assert m2.get("reach") == "empty_converge", m2
70
+ assert m2.get("reach_build_docs") == 7, "empty_converge 出口漏首建成本:%r" % (m2,)
71
+
72
+ # ③ 未重建时不得虚构成本
73
+ class Cold(StubIdx):
74
+ built_now = False
75
+
76
+ reach._index_with_meta = lambda cg, force=False: (Cold(), {})
77
+ _, m3 = reach.narrow(object(), [], [], set(), q="")
78
+ assert "reach_build_docs" not in m3 and "reach_build_reads" not in m3, m3
79
+
80
+ # ④ 语义分支确实生效:sem 为真容器时,无词面命中的语义节点也必须入池
81
+ class SemIdx(StubIdx):
82
+ sem = ["p/sem"]
83
+
84
+ reach._index_with_meta = lambda cg, force=False: (SemIdx(), {})
85
+ got4, m4 = reach.narrow(object(), [{"path": "p/sem"}], ["甲乙"], {"甲", "乙"}, q="甲乙")
86
+ assert m4.get("reach") == "converged", m4
87
+ assert [e["path"] for e in got4] == ["p/sem"], "sem 分支未真实生效:%r" % (got4,)
88
+
89
+ # ⑤ fresh_overflow:未索引节点超上限 → 整段回退,且同时暴露 reach_fresh_nodes 与首建成本
90
+ reach._index_with_meta = lambda cg, force=False: (StubIdx(), {})
91
+ many = [{"path": "x"}] + [{"path": "p/f%04d" % i} for i in range(reach._FRESH_MAX + 1)]
92
+ got5, m5 = reach.narrow(object(), many, ["甲乙"], {"甲", "乙"}, q="甲乙")
93
+ assert got5 is None and m5.get("reach") == "fresh_overflow", m5
94
+ assert m5.get("reach_fresh_nodes") == reach._FRESH_MAX + 1, m5
95
+ assert m5.get("reach_build_docs") == 7, "fresh_overflow 出口漏首建成本:%r" % (m5,)
96
+
97
+ # ⑥ r34 反例:读了很多节点但都不可读(None)后失败 → 已读盘量必须如实计数
98
+ reach._index_with_meta = orig_with
99
+ r1 = _tmp_root()
100
+ _use(r1)
101
+ cg_a = ReadStubCG(r1, {"a": {"path": "pa"}, "b": {"path": "pb"}, "c": {"path": "pc"}},
102
+ [(None, None), (None, None)])
103
+ idx_a, fail_a = reach._index_with_meta(cg_a)
104
+ assert idx_a is None, idx_a
105
+ assert fail_a.get("reach_build_reads") == 2, "不可读读盘未计数:%r" % (fail_a,)
106
+ assert fail_a.get("reach_build_docs") == 0 and fail_a.get("reach_build_partial") is True, fail_a
107
+ # 端到端:narrow 的 index_unavailable 出口也必须带这些字段
108
+ # (每次调用各自独立:本例用**新** stub 与新根,否则上一次调用已把 plan 耗尽)
109
+ r4 = _tmp_root()
110
+ _use(r4)
111
+ cg_a2 = ReadStubCG(r4, {"a": {"path": "pa"}, "b": {"path": "pb"}, "c": {"path": "pc"}},
112
+ [(None, None), (None, None)])
113
+ got6, m6 = reach.narrow(cg_a2, [], ["甲乙"], {"甲", "乙"}, q="甲乙")
114
+ assert got6 is None and m6.get("reach") == "index_unavailable", m6
115
+ assert m6.get("reach_build_reads") == 2 and m6.get("reach_build_partial") is True, m6
116
+
117
+ # ⑦ 部分成功:第一个节点已入倒排后失败 → docs 与 reads 都要如实
118
+ r2 = _tmp_root()
119
+ _use(r2)
120
+ cg_b = ReadStubCG(r2, {"a": {"path": "pa"}, "b": {"path": "pb"}}, [({"id": "a"}, "hello")])
121
+ idx_b, fail_b = reach._index_with_meta(cg_b)
122
+ assert idx_b is None, idx_b
123
+ assert fail_b.get("reach_build_docs") == 1 and fail_b.get("reach_build_reads") == 1, fail_b
124
+ assert fail_b.get("reach_build_partial") is True, fail_b
125
+
126
+ # ⑧ 成功路径:无 fail 元数据;首建成本同时暴露读盘数与入索引数(各调用各自独立容器)
127
+ r3 = _tmp_root()
128
+ _use(r3)
129
+ cg_c = ReadStubCG(r3, {"a": {"path": "pa", "content_hash": "h"}}, [({"id": "a"}, "hello")])
130
+ idx_c, fail_c = reach._index_with_meta(cg_c)
131
+ assert idx_c is not None and fail_c == {}, (idx_c, fail_c)
132
+ bm = reach._build_meta(idx_c)
133
+ assert bm.get("reach_build_docs") == 1 and bm.get("reach_build_reads") == 1, bm
134
+ # 重复调用不得把上次成本带下来
135
+ idx_d, fail_d = reach._index_with_meta(cg_c)
136
+ assert fail_d == {}, fail_d
137
+ finally:
138
+ reach._index_with_meta, mdcg_mod.semantic_on = orig_with, orig_sem
139
+ os.environ.pop("MDCG_REACH_INDEX", None)
140
+ print("ALL PASS test_reach_meta_exits")
141
+ return 0
142
+
143
+
144
+ if __name__ == "__main__":
145
+ sys.exit(main())
@@ -0,0 +1,141 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · Pi⑦② read 截断 + 续读提示(_clip_text / _node_view / schema 落点)
3
+
4
+ 验收三件事:
5
+ ① 单次 read 返回受「2000 行 / 50KB」双阈值约束,且**不静默丢内容**——
6
+ 截断必带 total_lines / total_bytes / next_offset / note;
7
+ ② 续读按 offset 连续取回可拼回原文(不丢不重,可裁决的等价性判据);
8
+ ③ 小内容行为兼容——旧四键(id/path/frontmatter/content)不丢、content 原样;
9
+ 验证态 `verification_state` 为**有意的 additive 透出**(不参与排序),
10
+ 故 B1 断言「旧键 ⊆ 新键 ∧ 验证态在场」而非「键集逐一相等」。
11
+ 真源:protocol.py 的 read.node.required 已把 `verification_state` 列为必需键。
12
+
13
+ 运行:python -m md_cg.test_read_clip
14
+ """
15
+
16
+ import os
17
+ import sys
18
+
19
+ from .mcp_server import KERNEL_TOOLS, READ_MAX_BYTES, READ_MAX_LINES, \
20
+ _cg_call, _clip_text, _node_view
21
+
22
+ _ok = 0
23
+ _bad = []
24
+
25
+
26
+ def check(name, cond, detail=""):
27
+ global _ok
28
+ if cond:
29
+ _ok += 1
30
+ print(f" [ok] {name}" + (f" · {detail}" if detail else ""))
31
+ else:
32
+ _bad.append(name)
33
+ print(f" [FAIL] {name} · {detail}")
34
+
35
+
36
+ class _FakeCG:
37
+ """只实现 read 面所需的最小取节点契约。"""
38
+
39
+ def __init__(self, nodes):
40
+ self._nodes = nodes
41
+
42
+ def get(self, nid, *a, **k):
43
+ return self._nodes.get(nid)
44
+
45
+
46
+ def main():
47
+ print("[A] _clip_text 双阈值与续读语义")
48
+ small = "第一行\n第二行\n第三行"
49
+ c = _clip_text(small)
50
+ check("A1 小文本原样返回(note 空串、无截断位点)",
51
+ c["text"] == small and c["truncated"] is False
52
+ and c["next_offset"] is None and c["total_lines"] == 3
53
+ and c["note"] == "", str(c)[:160])
54
+
55
+ many = "\n".join(f"line-{i:05d}" for i in range(READ_MAX_LINES + 500))
56
+ c = _clip_text(many)
57
+ check("A2 行数上限:超 2000 行截断且给出续读位点",
58
+ c["truncated"] is True and c["returned_lines"] == READ_MAX_LINES
59
+ and c["next_offset"] == READ_MAX_LINES + 1
60
+ and str(READ_MAX_LINES) in c["note"], c["note"])
61
+
62
+ wide = "\n".join("x" * 60 for _ in range(3000)) # 约定 180KB > 50KB
63
+ c = _clip_text(wide)
64
+ check("A3 字节上限生效(行未满 2000 但字节先到顶)",
65
+ c["truncated"] is True and c["returned_lines"] < READ_MAX_LINES
66
+ and len(c["text"].encode("utf-8")) <= READ_MAX_BYTES
67
+ and str(READ_MAX_BYTES) in c["note"], f"{c['returned_lines']}行/{c['total_bytes']}B")
68
+
69
+ # 续读拼接等价性:按 next_offset 连续取回,拼回必须与原文逐字一致
70
+ got, off, hops = [], 0, 0
71
+ while True:
72
+ c = _clip_text(many, offset=off)
73
+ got.append(c["text"])
74
+ hops += 1
75
+ if not c["truncated"]:
76
+ break
77
+ off = c["next_offset"]
78
+ if hops > 10:
79
+ break
80
+ check("A4 续读拼接 == 原文(不丢不重)",
81
+ "\n".join(got) == many and hops == 2, f"hops={hops}")
82
+
83
+ c = _clip_text("a\nb", offset=99)
84
+ check("A5 offset 越界:返回空并如实说明(不假装有内容)",
85
+ c["text"] == "" and c["truncated"] is False and "超出总行数" in c["note"],
86
+ c["note"])
87
+
88
+ c = _clip_text("y" * (READ_MAX_BYTES + 100))
89
+ check("A6 单行超字节上限也保底返回一行(不返回空)",
90
+ c["returned_lines"] == 1 and c["truncated"] is False
91
+ and len(c["text"]) == READ_MAX_BYTES + 100, str(c["returned_lines"]))
92
+
93
+ print("[B] _node_view 兼容与截断字段")
94
+ node = {"id": "n1", "path": "/p/n1.md", "frontmatter": {"layer": "knowledge"},
95
+ "content": "短内容"}
96
+ v = _node_view(node)
97
+ check("B1 旧四键不丢(零回归)+ 验证态 additive 透出",
98
+ {"id", "path", "frontmatter", "content"} <= set(v)
99
+ and v["content"] == "短内容"
100
+ and v["verification_state"] == "unverified", str(sorted(v)))
101
+ check("B2 空节点维持 None(旧契约)", _node_view(None) is None)
102
+
103
+ big = dict(node, content=many)
104
+ v = _node_view(big)
105
+ check("B3 大节点截断 + 审计字段(truncated/next_offset/note)",
106
+ v["truncated"] is True and v["next_offset"] == READ_MAX_LINES + 1
107
+ and v["content_lines"] == READ_MAX_LINES + 500
108
+ and v["content"].split("\n")[-1] == f"line-{READ_MAX_LINES - 1:05d}",
109
+ str({k: v[k] for k in ("truncated", "next_offset", "content_lines")}))
110
+ v2 = _node_view(big, offset=v["next_offset"])
111
+ check("B4 按 next_offset 续读可取回尾部且拼回原文",
112
+ "\n".join([v["content"], v2["content"]]) == many
113
+ and v2["offset"] == READ_MAX_LINES + 1, str(v2["offset"]))
114
+
115
+ print("[C] 工具面落点(schema + 分发透传)")
116
+ cg = next(t for t in KERNEL_TOOLS if t["name"] == "cg")
117
+ props = cg["inputSchema"]["properties"]
118
+ check("C1 cg schema 暴露 offset 且描述含续读语义",
119
+ props["offset"]["type"] == "integer" and "next_offset" in props["offset"]["description"])
120
+ check("C2 read 面描述声明截断与续读(调用方可知情)",
121
+ "next_offset" in cg["description"] and "截断" in cg["description"])
122
+
123
+ fcg = _FakeCG({"n1": node})
124
+ out = _cg_call(fcg, {"op": "read", "node_id": "n1"})
125
+ check("C3 未传 offset:行为与旧版一致",
126
+ out["content"] == "短内容" and "truncated" not in out, str(out)[:160])
127
+ fcg = _FakeCG({"n1": big})
128
+ out = _cg_call(fcg, {"op": "read", "node_id": "n1", "offset": READ_MAX_LINES + 1})
129
+ check("C4 offset 经 _cg_call 透传到节点视图(真实续读通路)",
130
+ out["offset"] == READ_MAX_LINES + 1 and out["truncated"] is False
131
+ and out["content"].startswith(f"line-{READ_MAX_LINES:05d}")
132
+ and out["content"].endswith("line-02499"), str(out["offset"]))
133
+
134
+ print(f"\n结果:{_ok} 通过 / {len(_bad)} 失败")
135
+ if _bad:
136
+ print("失败项:" + "、".join(_bad))
137
+ return 1 if _bad else 0
138
+
139
+
140
+ if __name__ == "__main__":
141
+ sys.exit(main())