@furongjun1999/dsh-memory 0.4.8 → 0.4.10

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 (232) hide show
  1. package/README.md +54 -26
  2. package/codebuddy/CODEBUDDY.md +196 -195
  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/README.md +1 -1
  7. package/docs/discipline/harnesses.yaml +18 -7
  8. package/docs/discipline/templates/full.md.tmpl +4 -3
  9. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  10. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  11. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  12. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  13. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  22. 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
  23. 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
  24. 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 +47 -45
  25. 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.4.md → 206_v0.5.md} +92 -4
  26. 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
  27. 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
  28. 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
  29. 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 +2 -2
  30. 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 +3 -3
  31. 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 +2 -2
  32. 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 -433
  33. 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
  34. package/dsh/README.md +33 -0
  35. package/dsh/cordis.yml.example +13 -7
  36. package/dsh/hive-mcp-probe.mjs +94 -0
  37. package/dsh/hive-mcp.example.yml +62 -0
  38. package/dsh/update-lingshu.bat +11 -0
  39. package/dsh/update-lingshu.ps1 +337 -0
  40. package/lib/hooks.d.ts +3 -0
  41. package/lib/hooks.js +17 -23
  42. package/lib/index.d.ts +4 -2
  43. package/lib/index.js +29 -6
  44. package/lib/lib/datapath.d.ts +76 -1
  45. package/lib/lib/datapath.js +199 -13
  46. package/lib/lib/mdcg_client.d.ts +40 -3
  47. package/lib/lib/mdcg_client.js +46 -22
  48. package/lib/lib/mutual.js +4 -4
  49. package/lib/lib/token_store.js +4 -5
  50. package/md_cg/audit.py +17 -2
  51. package/md_cg/autonomy.py +86 -15
  52. package/md_cg/backfill.py +36 -1
  53. package/md_cg/backfill_bigdomain.py +34 -0
  54. package/md_cg/bench6_arms.py +28 -1
  55. package/md_cg/bench6_common.py +10 -1
  56. package/md_cg/bench6_competitors.py +6 -1
  57. package/md_cg/bench_axis_domain.py +9 -1
  58. package/md_cg/bench_blind_comp.py +7 -1
  59. package/md_cg/bench_en_atoms_public.py +9 -0
  60. package/md_cg/bench_governance.py +348 -0
  61. package/md_cg/bench_lme_zh.py +16 -1
  62. package/md_cg/bench_locomo.py +2 -1
  63. package/md_cg/bench_locomo_zh.py +16 -1
  64. package/md_cg/bench_locomo_zh_public.py +4 -1
  65. package/md_cg/bench_longmem.py +2 -1
  66. package/md_cg/bench_membench.py +27 -1
  67. package/md_cg/bench_p0.py +4 -1
  68. package/md_cg/bench_progressive.py +13 -1
  69. package/md_cg/bench_role_views.py +238 -0
  70. package/md_cg/bench_task_ab.py +8 -1
  71. package/md_cg/bench_task_ab_llm.py +13 -1
  72. package/md_cg/bench_unified_en.py +6 -1
  73. package/md_cg/bench_zh_mad.py +20 -1
  74. package/md_cg/blindspot_tickets.py +123 -0
  75. package/md_cg/branches.py +12 -1
  76. package/md_cg/build_postings.py +73 -0
  77. package/md_cg/ccgc.py +67 -2
  78. package/md_cg/census.py +5 -1
  79. package/md_cg/chain.py +24 -3
  80. package/md_cg/codeindex.py +134 -17
  81. package/md_cg/coldverify.py +265 -0
  82. package/md_cg/comment_gate.py +338 -0
  83. package/md_cg/cond_compose.py +190 -0
  84. package/md_cg/cond_facts.py +155 -0
  85. package/md_cg/cond_template.json +107 -0
  86. package/md_cg/condition_anchor.py +143 -0
  87. package/md_cg/conformance.py +69 -4
  88. package/md_cg/consistency.py +24 -1
  89. package/md_cg/consolidate.py +53 -2
  90. package/md_cg/corpus.py +4 -0
  91. package/md_cg/crosscheck.py +42 -2
  92. package/md_cg/crypto.py +35 -1
  93. package/md_cg/d_meta.py +310 -0
  94. package/md_cg/datapath.py +201 -26
  95. package/md_cg/docindex.py +122 -1
  96. package/md_cg/eval_common.py +29 -1
  97. package/md_cg/evidence.py +27 -1
  98. package/md_cg/evolution.py +21 -1
  99. package/md_cg/export.py +11 -1
  100. package/md_cg/forgetting.py +23 -1
  101. package/md_cg/fsutil.py +18 -1
  102. package/md_cg/hotcache.py +214 -0
  103. package/md_cg/hyperedge.py +251 -0
  104. package/md_cg/identity.py +18 -1
  105. package/md_cg/insight.py +17 -1
  106. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  107. package/md_cg/lexicon/build_standard_en.py +171 -168
  108. package/md_cg/lexicon/expand_en_zh.py +6 -0
  109. package/md_cg/lifecycle.py +12 -1
  110. package/md_cg/linkref.py +281 -0
  111. package/md_cg/links.py +29 -1
  112. package/md_cg/mcp_server.py +362 -43
  113. package/md_cg/md_whitebox.py +53 -1
  114. package/md_cg/mdcg.py +1003 -27
  115. package/md_cg/mdcos.py +558 -36
  116. package/md_cg/metacognition.py +37 -2
  117. package/md_cg/migrate.py +4 -0
  118. package/md_cg/migrate_aeis.py +221 -213
  119. package/md_cg/migrate_roleplay.py +8 -0
  120. package/md_cg/migrate_wisdom_graph.py +14 -1
  121. package/md_cg/mreview/__main__.py +3 -0
  122. package/md_cg/mreview/bundle.py +8 -0
  123. package/md_cg/mreview/candidates.py +9 -0
  124. package/md_cg/mreview/govern.py +21 -1
  125. package/md_cg/mreview/locate.py +34 -0
  126. package/md_cg/mreview/pipeline.py +29 -1
  127. package/md_cg/mreview/ruleset.py +16 -1
  128. package/md_cg/nodefile.py +233 -3
  129. package/md_cg/pooling.py +23 -1
  130. package/md_cg/postings.py +298 -0
  131. package/md_cg/predict.py +89 -9
  132. package/md_cg/progressive.py +3 -0
  133. package/md_cg/protect.py +14 -1
  134. package/md_cg/protocol.py +372 -0
  135. package/md_cg/provenance.py +262 -0
  136. package/md_cg/reach.py +453 -0
  137. package/md_cg/refindex.py +47 -2
  138. package/md_cg/refine.py +20 -1
  139. package/md_cg/roleviews.py +89 -0
  140. package/md_cg/routing.py +76 -0
  141. package/md_cg/scrub.py +63 -2
  142. package/md_cg/security.py +26 -1
  143. package/md_cg/self_state.py +64 -1
  144. package/md_cg/selfreport.py +151 -0
  145. package/md_cg/semantic/canonical.py +5 -0
  146. package/md_cg/semantic/en_normalizer.py +364 -355
  147. package/md_cg/semantic/zh_en_atoms.py +139 -136
  148. package/md_cg/signer.py +41 -1
  149. package/md_cg/sources.py +583 -547
  150. package/md_cg/statushdr.py +179 -0
  151. package/md_cg/stg.py +59 -18
  152. package/md_cg/subgraph.py +23 -0
  153. package/md_cg/sustain.py +56 -1
  154. package/md_cg/tasks.py +26 -2
  155. package/md_cg/test_autonomy.py +26 -0
  156. package/md_cg/test_bench_governance.py +102 -0
  157. package/md_cg/test_blindspot_tickets.py +166 -0
  158. package/md_cg/test_ccgc.py +10 -0
  159. package/md_cg/test_codeindex.py +338 -0
  160. package/md_cg/test_comment_gate.py +187 -0
  161. package/md_cg/test_cond_compose_anchors.py +76 -0
  162. package/md_cg/test_condition_anchor.py +82 -0
  163. package/md_cg/test_d_meta.py +412 -0
  164. package/md_cg/test_datapath_root.py +188 -0
  165. package/md_cg/test_gain_gate.py +47 -1
  166. package/md_cg/test_hot_cold.py +187 -0
  167. package/md_cg/test_hyperedge.py +245 -0
  168. package/md_cg/test_linkref.py +306 -0
  169. package/md_cg/test_md_access_parity.py +15 -3
  170. package/md_cg/test_mr_m1.py +108 -18
  171. package/md_cg/test_mr_m3.py +8 -1
  172. package/md_cg/test_p26_refindex.py +49 -20
  173. package/md_cg/test_p27_docindex.py +236 -2
  174. package/md_cg/test_p2_mcp.py +1 -1
  175. package/md_cg/test_p31_insight.py +24 -0
  176. package/md_cg/test_p44_md_whitebox.py +14 -1
  177. package/md_cg/test_protocol.py +243 -0
  178. package/md_cg/test_reach.py +378 -0
  179. package/md_cg/test_reach_keys.py +201 -0
  180. package/md_cg/test_reach_meta_exits.py +145 -0
  181. package/md_cg/test_read_clip.py +8 -4
  182. package/md_cg/test_retr_s1.py +340 -0
  183. package/md_cg/test_retr_s1b.py +209 -0
  184. package/md_cg/test_retr_s3.py +194 -0
  185. package/md_cg/test_retr_s4.py +163 -0
  186. package/md_cg/test_retr_s5.py +200 -0
  187. package/md_cg/test_retr_s6.py +157 -0
  188. package/md_cg/test_retr_s7.py +385 -0
  189. package/md_cg/test_retr_s8_time.py +316 -0
  190. package/md_cg/test_retr_s9_edges.py +286 -0
  191. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  192. package/md_cg/test_review_conformance.py +59 -2
  193. package/md_cg/test_role_views.py +354 -0
  194. package/md_cg/test_subproc_encoding.py +188 -0
  195. package/md_cg/test_trust.py +361 -0
  196. package/md_cg/test_units_poll.py +71 -0
  197. package/md_cg/test_v14_fixes.py +397 -0
  198. package/md_cg/test_validity_filter.py +280 -0
  199. package/md_cg/test_wisdom_md_store.py +7 -3
  200. package/md_cg/test_writepipe.py +5 -1
  201. package/md_cg/theory.py +16 -1
  202. package/md_cg/tokens.py +40 -8
  203. package/md_cg/tool_face.py +13 -2
  204. package/md_cg/trust.py +943 -0
  205. package/md_cg/twophase.py +12 -1
  206. package/md_cg/units.py +132 -10
  207. package/md_cg/vision_evidence.py +24 -1
  208. package/md_cg/weights.py +24 -1
  209. package/md_cg/whitebox.py +32 -1
  210. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  211. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  212. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  213. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  214. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  215. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  216. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  217. package/md_cg/writelimit.py +18 -4
  218. package/md_cg/writepipe.py +178 -7
  219. package/package.json +2 -2
  220. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  221. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  222. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  223. package/src/hooks.ts +17 -21
  224. package/src/index.ts +33 -6
  225. package/src/lib/datapath.ts +211 -13
  226. package/src/lib/mdcg_client.ts +64 -25
  227. package/src/lib/mutual.ts +411 -411
  228. package/src/lib/token_store.ts +4 -5
  229. package/zcode/AGENTS.md +196 -195
  230. package/zcode/README.md +4 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  232. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
package/md_cg/nodefile.py CHANGED
@@ -39,6 +39,7 @@ CCG 要素的语义对应(白箱第 1 篇第 17 章):
39
39
  """
40
40
  import hashlib
41
41
  import json
42
+ import re
42
43
  import time
43
44
 
44
45
  _DELIM = "---"
@@ -54,7 +55,7 @@ CCG_REQUIRED = CCG_MARKS
54
55
  # 六要素不是注释/描述,而是**接口契约**——每行在契约里有一个确定角色:
55
56
  # 功能名 = 签名 signature(可执行入口符号)
56
57
  # 生效条件 = 前置条件 precondition(由 condition_space 四槽合成,唯一入口)
57
- # 子功能 = 依赖 dependency
58
+ # 子功能 = 内部子功能分解(自述)**兼** 依赖 dependency(跨节点)
58
59
  # 执行 = 调用 invocation
59
60
  # 验证方式 = 后置条件 postcondition + test
60
61
  # 不适用条件 = 拒绝域 rejection_domain
@@ -62,14 +63,51 @@ CCG_REQUIRED = CCG_MARKS
62
63
  # 的既有硬约束同源:缺前置条件的接口无法判定可否调用,故缺参即编译错误。
63
64
  # 本常量是术语的**唯一真源**;其余模块(如 ccgc.CONTRACT_ROLES)一律引用本处,
64
65
  # 禁止各自再定义一份,防「术语双写法」漂移。
66
+ #
67
+ # 「子功能」的**双语义**(收窄裁定 b · 使用者 2026-09-19)——这是本槽位的既成事实,
68
+ # 不能只看本常量的 "dependency" 四字:理论真源(智能论 3.4 §6章.3 六要素表)把该行
69
+ # 定义为「内部子功能分解(①②③)」= **对自身的描述**;本常量记其契约角色为
70
+ # dependency = **寄生于他者**。同一槽位承载两种语义,**靠形态区分**(不是靠猜):
71
+ # · 自然语言描述本单元内部构成 → **自述**,不构成依赖声明,无需 depends_on;
72
+ # · `@<节点 id>` 显式引用其它单元 → **跨节点依赖声明**,必须落 depends_on。
73
+ # 判据真源 = `declares_dependency`(本模块);闸门 = `ccgc._check_deps` /
74
+ # `writepipe._gate_deps`(两闸共用同一判据,收窄一处即两闸同步)。
75
+ # **不得再退回「值非哨兵即声明」**:CCG 编译产物六要素必含本行,退回即让每个节点
76
+ # 落库后被 E050 永久锁死(test_ccgc V16f 实证),且与理论真源的自述口径直接冲突。
65
77
  CCG_CONTRACT_ROLES = {
66
78
  "功能名": "签名 signature(可执行入口符号)",
67
79
  "生效条件": "前置条件 precondition",
68
- "子功能": "依赖 dependency",
80
+ "子功能": "内部子功能分解 self_decomposition / 依赖 dependency"
81
+ "(双语义,以 @<节点 id> 显式引用为界)",
69
82
  "执行": "调用 invocation",
70
83
  "验证方式": "后置条件 postcondition + test",
71
84
  "不适用条件": "拒绝域 rejection_domain",
72
85
  }
86
+
87
+ # ---- 裁定 C(Phase 0 契约裁决 · 2026-09-17):索引元条件与功能生效条件字段分家 ----
88
+ # 病根:codeindex.render 曾把**机械推导的索引元条件**(本地源码仓 / 全时窗 /
89
+ # AST 工具 / 文件可读)写成「生效条件」行,与「功能生效条件」(人工声明:
90
+ # 这段代码在何种输入/状态下正确)**共用同一字段名**。检索侧
91
+ # mdcos._ccg_field 取首个匹配 → 源码里人工写的生效条件行被永久压制 →
92
+ # 「补了注释」与「没补」在检索结果上不可区分。
93
+ # 处置(沿用 backfill.py 已裁定先例「观测位置 ≠ 生效条件」):
94
+ # · 「生效条件」只承载**功能前置条件**,来源仅限人工/源码声明,
95
+ # **不由 render 合成**(合成即冒充);
96
+ # · 索引元条件改由 INDEX_META_MARK 承载,与 CCG_MARKS **零重名**
97
+ # (机械可判,见 is_ccg_mark)。
98
+ # 权威契约:docs/mdcg/代码评审与条件化注释_契约_v0.1.md
99
+ INDEX_META_MARK = "索引元条件"
100
+
101
+
102
+ # 生效条件:name 无论为何值先经 str().strip() 归一(name 为 None 时即字符串 "None"),归一结果落在模块常量 CCG_MARKS 中即返回 True,否则 False。
103
+ def is_ccg_mark(name: str) -> bool:
104
+ """该字段名是否为 CCG 六要素之一(合成区零重名契约的机械判据)。
105
+
106
+ 给「合成区字段名 ∩ CCG_MARKS = ∅」提供**可执行**判据,而不是靠注释约定:
107
+ test_codeindex 逐行核合成区用到的字段名。
108
+ """
109
+ return str(name).strip() in CCG_MARKS
110
+
73
111
  # 外部验证基底的可取值(frontmatter.verification_basis)
74
112
  #
75
113
  # 分两档(口径:文科宽松、理科严格):
@@ -85,7 +123,156 @@ REPRODUCIBLE_BASIS = ("compiler", "test", "measurement", "formal_proof", "data")
85
123
  #: 来源一致性档:文科断言可用(来源表述一致即可)
86
124
  CONSISTENCY_BASIS = ("textbook", "public_kb")
87
125
 
126
+ # ---- 裁定 D(2026-09-19):可验证记忆单元的**字段名真源** --------------------------
127
+ # 「子功能」的契约角色是**依赖 dependency**(见 CCG_CONTRACT_ROLES),其落字段即
128
+ # `depends_on`——名字与角色对齐(不叫 sub_features,避免同一概念两种写法)。
129
+ # 依赖一旦失效,下游的「还成不成立」就变了,故它与验证态、时间轴同属一层。
130
+ DEPENDS_ON_FIELD = "depends_on"
131
+
132
+ #: 双时间轴:何时开始成立 / 何时不再成立(判定未生效·生效中·已过期)。
133
+ #: `valid_until` 既有(已被 scrub 的过期消费面使用);`valid_from` 本轮补齐。
134
+ VALID_FROM_FIELD = "valid_from"
135
+ VALID_UNTIL_FIELD = "valid_until"
136
+
137
+ #: 双时间轴的**规范键**(2026-09-19 阶段一):新写入落此;历史键 `valid_from` /
138
+ #: `valid_until` 保留为**读取侧回落别名**(存量不迁移、零破坏)。本处只登记字段名;
139
+ #: **行为真源**(取值优先级 `effective_* > valid_* > 其余别名` 与三态判定)见
140
+ #: md_cg/trust.py 的 FROM_ALIASES / UNTIL_ALIASES / validity()。
141
+ EFFECTIVE_FROM_FIELD = "effective_from"
142
+ EFFECTIVE_UNTIL_FIELD = "effective_until"
143
+
144
+ #: 信念时间:体系**何时确认此条**——取代(supersede)/ 审核的排序锚。
145
+ #: **第三类语义**:既不是「尚未开始」也不是「已经结束」,故**不入** scrub 的任一
146
+ #: 键族(并入即把「已确认」误判成「已生效 / 已失效」)。物理隔离守卫见
147
+ #: test_validity_filter.py 的交叉断言。
148
+ BELIEVED_AT_FIELD = "believed_at"
149
+
150
+ #: 过期时刻(写盘冗余:由 effective_until 派生,供审计 / 对账免计算直读)。
151
+ EXPIRED_AT_FIELD = "expired_at"
152
+
153
+ #: 巩固留痕(2026-09-19 阶段一):何时巩固 / 巩固进哪一条(成员 → 概念 id)。
154
+ #: 与 `promoted_at`(层迁移时刻)同族——把「归并产物可溯源」从流程记录升为一等字段。
155
+ #: **行为真源**(写入侧)见 md_cg/consolidate.py 的 induce / promote。
156
+ CONSOLIDATED_AT_FIELD = "consolidated_at"
157
+ CONSOLIDATED_INTO_FIELD = "consolidated_into"
158
+
159
+ #: 归纳来源(概念节点侧):前身成员清单 / 归纳时刻。与 `consolidated_*` 同族——
160
+ #: 成员侧列「巩固进哪一条」,概念侧列「前身是谁」,两侧互查即完整血缘。
161
+ INDUCED_FROM_FIELD = "induced_from"
162
+ INDUCED_AT_FIELD = "induced_at"
163
+
164
+ #: 巩固 / 归纳留痕字段族(单一真源):全族**不进索引白名单**(审计/血缘向,非查询
165
+ #: 热点),故 `add()` 覆写时须**回读节点文件**继承(索引快照里没有这些键)——
166
+ #: 否则概念节点被一次普通覆写(如审核 edit/merge 重写)即丢掉前身清单。
167
+ CONSOLIDATION_FIELDS = (CONSOLIDATED_AT_FIELD, CONSOLIDATED_INTO_FIELD,
168
+ INDUCED_FROM_FIELD, INDUCED_AT_FIELD)
169
+
170
+ #: 验证态字段。**刻意不叫 `state`**——该名已被裁决四态(ACCEPT/REJECT/DEFER/
171
+ #: BLINDSPOT)占用,`lifecycle_state` 的先例同此动机(观测位置不同即命名不同)。
172
+ #: 状态枚举与合法迁移表的**行为真源是 md_cg/trust.py**;本处只登记字段名,
173
+ #: **不复制枚举**——复制即制造第二份真源,与 CCG_CONTRACT_ROLES 的单一真源纪律相悖。
174
+ VERIFICATION_STATE_FIELD = "verification_state"
175
+
176
+ # ---- 「子功能」声明的**值语义**:哨兵与解析 --------------------------------------
177
+ # 行存在 ≠ 有声明:`# 子功能:无` 是合法填充(绝大多数知识节点不依赖任何单元),
178
+ # 若把它也算成「声明了依赖」,全库普通节点都会被 E050 硬拒——闸门立刻沦为噪声源。
179
+ # 故必须区分「行存在」与「构成真实声明」,判据收在这里(单一真源,ccgc/writepipe 共用)。
180
+ DEP_DECL_SENTINELS = ("无依赖", "无", "不适用", "不需要", "none", "n/a", "na", "-", "—")
181
+
182
+
183
+ # 生效条件:value 为 None 或 strip 后为空 → True;strip 后等于 DEP_DECL_SENTINELS 任一项、或为该哨兵加尾部括号说明(如「无(不依赖其他单元)」)→ True;其余 False(不做语义判断);
184
+ def is_dep_sentinel(value) -> bool:
185
+ """纯函数:依赖槽的值是否属于「无依赖」哨兵。
186
+
187
+ 只认**完全相等**或**哨兵+括号说明**两种形态,不做语义猜测——「无法确定」
188
+ 这类真实陈述不得被误判为「无」(宁可多要求一次 depends_on,不可漏掉声明)。
189
+ """
190
+ s = "" if value is None else str(value).strip()
191
+ if not s:
192
+ return True
193
+ for mark in DEP_DECL_SENTINELS:
194
+ if s == mark:
195
+ return True
196
+ for lp, rp in (("(", ")"), ("(", ")")):
197
+ if s.startswith(mark + lp) and s.endswith(rp):
198
+ return True
199
+ return False
200
+
201
+
202
+ # 生效条件:content 中不存在名字等于 field_name 的 `# 字段:` 行 → 返回 None;存在 → 返回首个命中行首个冒号之后的 strip 结果(可为空串);
203
+ def ccg_field_value(content, field_name: str):
204
+ """读取 `# <字段>:<值>` 的值(首个命中行;全/半角冒号兼容)。无该行 → None。
205
+
206
+ 与 `ccgc._upsert_ccg_line` 的解析口径一致(去 `#`→按冒号切名字→名字相等即
207
+ 命中)——即**写入口径与读入口径共用同一条行语义**,避免「写进去读不出」。
208
+ """
209
+ for ln in (content or "").split("\n"):
210
+ s = ln.strip()
211
+ if not s.startswith("#"):
212
+ continue
213
+ if s.lstrip("#").strip().split(":")[0].split(":")[0].strip() != field_name:
214
+ continue
215
+ for p in ("# " + field_name + ":", "# " + field_name + ":"):
216
+ if p in ln:
217
+ return ln.split(p, 1)[1].strip()
218
+ return ""
219
+ return None
220
+
221
+
222
+ # ---- 「子功能」行中的**跨节点引用**:显式标记与提取 -------------------------------
223
+ # 为什么需要它(收窄判据 · 使用者 2026-09-19 裁定 b):「子功能」的契约角色是
224
+ # dependency,但同一个槽里实际承载着两种语义完全不同的内容——
225
+ # ① **自述子功能**:本单元**内部**由哪些子步骤/子模块构成(如「按扩展名把文件
226
+ # 路由到对应摄取器」)。它描述的是自身,不引用任何其它记忆单元。
227
+ # ② **跨节点依赖**:本单元的成立与否**寄生于另一个记忆单元**(如 `@code_xxx`)。
228
+ # 只有它才是失效传播的入口,也只有它才需要落 `depends_on`。
229
+ # 旧判据「值非哨兵即声明依赖」把①一并判成②:CCG 编译产物六要素必含「子功能」
230
+ # 行,于是该节点一旦落库就被永久要求 `depends_on`——第二次 link 必被 E050 硬拒
231
+ # (test_ccgc V16f 实证:合法内容被自己的闸门锁死)。故**收窄判据:只有显式引用
232
+ # 形态才构成跨节点依赖声明**,自然语言自述不算(依赖不是必填元数据,不得默认要求)。
233
+ #
234
+ # 形态约定:`@` 紧跟节点 id(`[A-Za-z0-9_]` 起头,可含 `.`/`-`);`@` 左侧不得是
235
+ # 标识符字符——后半条排除 `user@host`(邮箱等)被误读为引用。中文自然语言里 `@`
236
+ # 极罕见,故该标记本身就是「这是引用」的显式信号,**不依赖语义猜测**(与哨兵
237
+ # 「只认完全相等」同一纪律:能机械判的绝不猜)。
238
+ DEP_REF_MARK = "@"
239
+ DEP_REF_RE = re.compile(r"(?<![A-Za-z0-9_])@([A-Za-z0-9_][A-Za-z0-9_.\-]*)")
88
240
 
241
+
242
+ # 生效条件:value 为 None 时按空串处理 → 返回空列表;为任意值且 str 化后正则无命中 → 空列表;命中 → 按出现顺序返回去重后的 id 列表。
243
+ def dep_refs(value) -> list:
244
+ """从「子功能」行的值中提取**显式跨节点引用**(`@<节点 id>`)→ 去重保序列表。
245
+
246
+ 只做**形态提取**,不校验目标是否存在——「声称依赖」与「目标存在」是两件事,
247
+ 混在一处会让「声称依赖一个并不存在的节点」被静默当成「没声明」(E051 永不触发)。
248
+ """
249
+ if value is None:
250
+ return []
251
+ out = []
252
+ for m in DEP_REF_RE.finditer(str(value)):
253
+ if m.group(1) not in out:
254
+ out.append(m.group(1))
255
+ return out
256
+
257
+
258
+ # 生效条件:content 中「# 子功能:」行缺失 → False;行存在且其值为空或命中依赖哨兵(is_dep_sentinel)→ False;行存在且值含显式跨节点引用(dep_refs 非空)→ True;其余(自然语言自述子功能)→ False;
259
+ def declares_dependency(content) -> bool:
260
+ """「# 子功能:」是否构成**跨节点依赖声明**(行存在 ∧ 值非哨兵 ∧ 含 `@<id>`)。
261
+
262
+ 这是 E050 硬拒的**前置判据真源**:只有**显式声称**依赖某节点,才要求
263
+ `depends_on` 给出可解析目标。行缺失、哨兵、以及**自述子功能**(自然语言描述
264
+ 本单元的内部构成)一律按「未声明」处理——依赖不是必填元数据,把自述当声明会
265
+ 让每个 CCG 节点(六要素必含「子功能」行)永久被要求 depends_on,闸门反过来
266
+ 锁死合法写入。**只有「声称依赖却不落字段」才是契约违规**(声称与落盘不一致,
267
+ 被依赖单元变动时下游无处可传,传播链从源头断掉)。
268
+ """
269
+ val = ccg_field_value(content, "子功能")
270
+ if val is None or is_dep_sentinel(val):
271
+ return False
272
+ return bool(dep_refs(val))
273
+
274
+
275
+ # 生效条件:content 为 None 或空串时按源码的 content or "" 回落空串计算,非空时按其原值编码,一律返回 sha256(utf-8) 十六进制摘要的前 12 位(content 的 frontmatter 不计入口径由调用方保证)。
89
276
  def content_hash(content: str) -> str:
90
277
  """节点正文的**内容指纹**(sha256 前 12 位)——全仓唯一实现。
91
278
 
@@ -102,6 +289,7 @@ def content_hash(content: str) -> str:
102
289
  return hashlib.sha256((content or "").encode("utf-8")).hexdigest()[:12]
103
290
 
104
291
 
292
+ # 生效条件:frontmatter(任意 dict,键按 sorted 输出)与 content(任意 str)均无前置校验即生效;content 不以 "\n" 结尾时补一个换行,返回 "---" + 各键的 "{k}: {json.dumps(v, ensure_ascii=False)}" 行 + "---" 行 + 正文。
105
293
  def dumps(frontmatter: dict, content: str) -> str:
106
294
  lines = [_DELIM]
107
295
  for k in sorted(frontmatter):
@@ -113,6 +301,7 @@ def dumps(frontmatter: dict, content: str) -> str:
113
301
  return "\n".join(lines) + "\n" + body
114
302
 
115
303
 
304
+ # 生效条件:text 以 "---\n" 开头且其后存在 "\n---\n" 时才解析——head 段内含 ":" 的行按首个 ":" 拆键值(json.loads 成功取值、抛 ValueError 则保留原字符串),不含 ":" 的行跳过,返回 (fm, content);不满足上述两个起始条件时返回 ({}, text)。
116
305
  def loads(text: str):
117
306
  """返回 (frontmatter dict, content str)。非法格式返回 ({}, 原文)。"""
118
307
  if not text.startswith(_DELIM + "\n"):
@@ -135,6 +324,7 @@ def loads(text: str):
135
324
  return fm, content
136
325
 
137
326
 
327
+ # 生效条件:content 中出现 "# {mark}:" 或 "# {mark}:"(中/英文冒号)即把该 mark 计入 present 与 required_present;complete 为 required_present 覆盖全部 CCG_REQUIRED、all_present 为 present 覆盖全部 CCG_MARKS,ratio = len(required_present)/len(CCG_REQUIRED),四键连同两个清单一起返回。
138
328
  def ccg_completeness(content: str) -> dict:
139
329
  """CCG 要素齐全度——白箱可审计性的量化指标。
140
330
 
@@ -157,6 +347,7 @@ def ccg_completeness(content: str) -> dict:
157
347
  }
158
348
 
159
349
 
350
+ # 生效条件:fm 的 "verification_basis" 缺键或取值为 None 时(.get 回落 None)直接返回 False;取值非 None 时,仅当该值属于模块常量 VERIFICATION_BASIS 才返回 True。
160
351
  def verification_basis_valid(fm: dict) -> bool:
161
352
  """frontmatter.verification_basis 是否落在可接受枚举里。"""
162
353
  vb = fm.get("verification_basis")
@@ -173,6 +364,7 @@ def verification_basis_valid(fm: dict) -> bool:
173
364
  PLACEHOLDER_MARKERS = ("骨架锚点", "内容待填充", "待填充", "骨架节点")
174
365
 
175
366
 
367
+ # 生效条件:value 为 None 或 str(value).strip() 为空串时返回 True;否则仅当去空白后的字符串包含 PLACEHOLDER_MARKERS 中任一标记词时返回 True,其余返回 False(不做语义判断)。
176
368
  def is_placeholder_text(value) -> bool:
177
369
  """占位标记的纯函数判定:空值或含占位标记 → 不可渲染为事实。
178
370
 
@@ -185,6 +377,7 @@ def is_placeholder_text(value) -> bool:
185
377
  return any(m in s for m in PLACEHOLDER_MARKERS)
186
378
 
187
379
 
380
+ # 生效条件:content 中含 "# 不适用条件:"(全角冒号)或 "# 不适用条件:"(半角冒号)即返回 True,两者都不出现返回 False。
188
381
  def has_non_applicable(content: str) -> bool:
189
382
  """是否声明了不适用条件——REJECT 路径成立的必要条件。
190
383
 
@@ -198,6 +391,7 @@ def has_non_applicable(content: str) -> bool:
198
391
  NEG_FIELD = "不适用条件"
199
392
 
200
393
 
394
+ # 生效条件:content 为 None 时按 "" 处理;逐行 strip 后,仅当该行以 "#" 开头且 lstrip("#").strip() 又以 NEG_FIELD 开头时丢弃该行,其余行原样保留(保留原缩进),返回保留行的 "\n".join。
201
395
  def positive_body(content: str) -> str:
202
396
  """剥离 `# 不适用条件:` 行后的正文——负条件不作召回键。
203
397
 
@@ -253,6 +447,7 @@ FULL_TIME_WINDOW_TEXT = "全时窗(任意时刻成立)"
253
447
  LEGACY_POSITION_PREFIX = "观测位置:"
254
448
 
255
449
 
450
+ # 生效条件:value 为 list/tuple 时用「、」连接各元素 str().strip() 后非空的部分(空元素跳过);value 为 None 时返回 "";其余类型返回 str(value).strip()。
256
451
  def _as_slot_text(value) -> str:
257
452
  """槽值 → 单行文本;列表值用「、」连接(「;」留给槽间分隔,不可混用)。"""
258
453
  if isinstance(value, (list, tuple)):
@@ -262,6 +457,7 @@ def _as_slot_text(value) -> str:
262
457
  return str(value).strip()
263
458
 
264
459
 
460
+ # 生效条件:value 可被 value[0]、value[1] 取下标并 float 化,且 lo <= FULL_TIME_WINDOW_MIN 同时 hi >= FULL_TIME_WINDOW_MAX 时返回 True;取值或 float 转换抛 TypeError/ValueError/IndexError/KeyError 时返回 False。
265
461
  def is_full_time_window(value) -> bool:
266
462
  """时间窗是否覆盖全时窗(任意时刻成立)。解析不了 → False(不冒充已声明)。"""
267
463
  try:
@@ -271,6 +467,7 @@ def is_full_time_window(value) -> bool:
271
467
  return lo <= FULL_TIME_WINDOW_MIN and hi >= FULL_TIME_WINDOW_MAX
272
468
 
273
469
 
470
+ # 生效条件:float(value) 能被 time.gmtime 接受时按 UTC 返回 "%Y-%m-%d %H:%M";转换或格式化抛 TypeError/ValueError/OSError/OverflowError 时返回 ""。
274
471
  def _fmt_ts(value) -> str:
275
472
  """unix 时间戳 → 「YYYY-MM-DD HH:MM」(UTC);解析不了 → ""。"""
276
473
  try:
@@ -279,6 +476,7 @@ def _fmt_ts(value) -> str:
279
476
  return ""
280
477
 
281
478
 
479
+ # 生效条件:先判 is_full_time_window(value) 为真则直接返回 FULL_TIME_WINDOW_TEXT(不落裸数组);否则取 value[0]、value[1] 的 UTC 文本(下标/取值抛 TypeError/IndexError/KeyError 返回 ""),两者有任一为空串也返回 "",均非空时返回 "{lo}~{hi}(UTC)"。
282
480
  def time_window_text(value) -> str:
283
481
  """时间窗 → 可读文本。
284
482
 
@@ -296,6 +494,7 @@ def time_window_text(value) -> str:
296
494
  return f"{lo}~{hi}(UTC)"
297
495
 
298
496
 
497
+ # 生效条件:cs 为非 dict 时返回 "";否则取 cs.get(key)(缺 key 键得 None),key == "time_window" 走时间窗文本、其余键走槽值文本,结果为假值或经 is_placeholder_text 判为占位时返回 "",否则返回该文本。
299
498
  def condition_slot_text(cs, key: str) -> str:
300
499
  """单槽 → 可读值;缺失/空/待填充占位 → ""(不冒充已声明)。"""
301
500
  if not isinstance(cs, dict):
@@ -307,6 +506,7 @@ def condition_slot_text(cs, key: str) -> str:
307
506
  return s
308
507
 
309
508
 
509
+ # 生效条件:cs(任意值,非 dict 由 condition_slot_text 兜为 "")下,仅把 CONDITION_SLOTS 中 condition_slot_text(cs, key) 返回非空串的槽按固定顺序收集为 (槽名, 标签, 文本) 列表,缺失槽不写入。
310
510
  def condition_space_slots(cs) -> list:
311
511
  """→ [(槽名, 标签, 文本)],只含**已声明**的槽(缺失槽不写)。"""
312
512
  out = []
@@ -317,12 +517,14 @@ def condition_space_slots(cs) -> list:
317
517
  return out
318
518
 
319
519
 
520
+ # 生效条件:cs(任意值)下,以 condition_space_slots(cs) 实际产出的槽名为已声明集合 have,返回 CONDITION_SLOTS 中不在 have 里的键名列表(已声明槽不计)。
320
521
  def condition_space_missing(cs) -> list:
321
522
  """缺失槽名清单(待补台账用);已声明槽不计。"""
322
523
  have = {k for k, _l, _t in condition_space_slots(cs)}
323
524
  return [k for k, _l in CONDITION_SLOTS if k not in have]
324
525
 
325
526
 
527
+ # 生效条件:cs 任意值;require_full(默认 True)为真且 condition_space_slots(cs) 的槽数不等于 len(CONDITION_SLOTS) 时返回 "";否则按固定顺序用「;」连接已声明槽的 "{label}:{text}"(require_full 为 False 时仅部分槽也照渲,无槽则返回 "")。
326
528
  def condition_space_text(cs, require_full: bool = True) -> str:
327
529
  """condition_space → 单行生效条件声明。**唯一合成入口**(纯函数,无 IO)。
328
530
 
@@ -337,10 +539,38 @@ def condition_space_text(cs, require_full: bool = True) -> str:
337
539
  return ";".join(f"{label}:{text}" for _k, label, text in slots)
338
540
 
339
541
 
542
+ # 生效条件:text 为 None 时按 "" 处理;去空白后的字符串以 LEGACY_POSITION_PREFIX 开头且长度严格大于该前缀长度时返回 True,其余(含仅等于前缀本身、空串)返回 False。
340
543
  def is_legacy_position_condition(text) -> bool:
341
544
  """文本是否为「观测位置:X」形态的单槽冒充(旧口径残留)。
342
545
 
343
546
  只认前缀形态,不做语义猜测:用于审计与迁移定位,不参与正常渲染。
344
547
  """
345
548
  s = "" if text is None else str(text).strip()
346
- return s.startswith(LEGACY_POSITION_PREFIX) and len(s) > len(LEGACY_POSITION_PREFIX)
549
+ return s.startswith(LEGACY_POSITION_PREFIX) and len(s) > len(LEGACY_POSITION_PREFIX)
550
+
551
+
552
+ # 生效条件:text 为假值(None/空串)时按空文本处理返回 [];否则按「;/;」拆槽、槽内含「:」或「:」时取首个分隔符之后的内容,再按「,,、/()()」切短语,丢弃长度 <2、纯数字及命中时间维哨兵关键词的短语并去重后返回 out;
553
+ def cond_terms(text: str) -> list[str]:
554
+ """生效条件声明 → 匹配短语列表(确定性切分,无语义猜测)。
555
+
556
+ 切分规则:按槽分隔「;/;」拆槽(condition_space_text 以「;」连四槽)
557
+ → 每槽剥「槽标签:」前缀(载体/位置、时间、方法、约束等标签是通用词,
558
+ 参与命中必误判)→ 槽内按「,,、/()」切短语 → 丢弃长度 <2、纯数字、
559
+ 全时窗哨兵短语(全时窗 = 时间维无信息量,不因其未命中而降级)。
560
+ """
561
+ out, seen = [], set()
562
+ for slot in re.split(r"[;;]", str(text or "")):
563
+ if ":" in slot:
564
+ slot = slot.split(":", 1)[1]
565
+ elif ":" in slot:
566
+ slot = slot.split(":", 1)[1]
567
+ for seg in re.split(r"[,,、/()()]", slot):
568
+ seg = seg.strip()
569
+ if len(seg) < 2 or seg.isdigit():
570
+ continue
571
+ if "全时窗" in seg or "任意时刻" in seg:
572
+ continue
573
+ if seg not in seen:
574
+ seen.add(seg)
575
+ out.append(seg)
576
+ return out
package/md_cg/pooling.py CHANGED
@@ -61,6 +61,7 @@ class PoolError(ValueError):
61
61
  # 分类 / 校验 / 计划
62
62
  # --------------------------------------------------------------------------
63
63
 
64
+ # 生效条件:pools 为 None 或 False 时返回 None;pools is True 时先替换为模块常量 WEIGHTS 再交 validate;pools 为其他值(含 dict)时直接交 validate(pools)。
64
65
  def resolve(pools):
65
66
  """把 `pools` 参数解成生效配置:None→关闭;True→内置表;dict→校验后副本。"""
66
67
  if pools is None or pools is False:
@@ -70,6 +71,7 @@ def resolve(pools):
70
71
  return validate(pools)
71
72
 
72
73
 
74
+ # 生效条件:entry 为假值(含 None)时按 entry or {} 处理,其 "layer" 去假值后经 str() 属模块常量 NEG_LAYERS → 返回 POOL_NEGATIVE;否则 node_id 去假值转 str 后以模块常量 INDEX_PREFIXES 起始,或 entry 的 "tags"(假值按 [])小写后任一元素恰为 index/artifact/code_index/doc_index → 返回 POOL_INDEX;其余 → POOL_KNOWLEDGE。
73
75
  def pool_of(node_id, entry=None) -> str:
74
76
  """节点归池(确定性、只看 id 前缀 / 层 / 标签,不读文件)。"""
75
77
  e = entry or {}
@@ -84,6 +86,7 @@ def pool_of(node_id, entry=None) -> str:
84
86
  return POOL_KNOWLEDGE
85
87
 
86
88
 
89
+ # 生效条件:pools 非 dict、缺 POOL_ORDER 中任一池、pools[p] or {} 不是 dict、float(spec.get("cap_ratio")) 或 float(spec.get("weight", 1.0)) 抛 TypeError/ValueError、ratio<=0、weight<=0、或各池 cap_ratio 之和与 1.0 之差超过 _RATIO_EPS 时抛 PoolError,全部通过才返回各项为 float 的 out(desc 经 spec.get("desc") or WEIGHTS[p]["desc"] 补齐)。
87
90
  def validate(pools) -> dict:
88
91
  """校验并归一化权重表(**硬约束:额度之和必须恰为 1.0**)。"""
89
92
  if not isinstance(pools, dict):
@@ -114,6 +117,7 @@ def validate(pools) -> dict:
114
117
  return out
115
118
 
116
119
 
120
+ # 生效条件:total 先 int(total),total<=0 时各池返回 0;total < len(POOL_ORDER) 时把 total 全给 POOL_KNOWLEDGE、其余池为 0;否则每池保底 1、余量按 left*float(pools[p]["cap_ratio"]) 取 floor 后,余数按小数部分降序(并列按 POOL_ORDER 序)补 1,返回额度之和恰为 total 的 out。
117
121
  def caps(total: int, pools) -> dict:
118
122
  """按比例分配额度;**各池额度之和恰等于 total**。
119
123
 
@@ -141,6 +145,7 @@ def caps(total: int, pools) -> dict:
141
145
  return out
142
146
 
143
147
 
148
+ # 生效条件:total 给出后,pools 为 None/False 等使 resolve(pools) 返回假值时返回 {'enabled': False, 'total': int(total), 'note': ...};cfg 为真时用 caps(total, cfg) 并逐池取 cfg[p]["weight"]/cfg[p]["cap_ratio"],返回启用态计划。
144
149
  def plan(total: int, pools=None) -> dict:
145
150
  """分池计划(供审计 / A/B 复算):额度 + 系数 + 是否启用。"""
146
151
  cfg = resolve(pools)
@@ -155,6 +160,7 @@ def plan(total: int, pools=None) -> dict:
155
160
  "index_prefixes": list(INDEX_PREFIXES)}
156
161
 
157
162
 
163
+ # 生效条件:pools 为 None/False 使 resolve(pools) 返回假值时返回 1.0;cfg 为真时返回 float(cfg[pool_of(node_id, entry)]["weight"]),其中 entry 缺省为 None。
158
164
  def weight_of(node_id, entry=None, pools=None) -> float:
159
165
  """节点的打分乘数(未启用分池 → 1.0,保证原行为)。"""
160
166
  cfg = resolve(pools)
@@ -163,6 +169,7 @@ def weight_of(node_id, entry=None, pools=None) -> float:
163
169
  return float(cfg[pool_of(node_id, entry)]["weight"])
164
170
 
165
171
 
172
+ # 生效条件:docs 与 total 给出后无条件调用 take(docs, total, pools=pools, key_of=key_of) 并只返回其第 0 项,pools 与 key_of 缺省为 None 原样透传。
166
173
  def allocate(docs, total: int, *, pools=None, key_of=None) -> list:
167
174
  """分池截断(只要结果;需要各池实取数用 `take()`)。未启用 → 平截(原行为)。
168
175
 
@@ -172,6 +179,7 @@ def allocate(docs, total: int, *, pools=None, key_of=None) -> list:
172
179
  return take(docs, total, pools=pools, key_of=key_of)[0]
173
180
 
174
181
 
182
+ # 生效条件:cfg 为真时先 quota=caps(total, cfg),遍历 docs 时 key_of 为真则用 key_of(d) 解出 (nid, entry)、否则把 d 解包为 (nid, entry),按 pool_of(nid, entry) 入 buckets;backflow 为真时以 total 减去各池 min(len(buckets[p]), quota[p]) 得 left,按 POOL_ORDER 只对尚有 room 的池补 quota 到 left 用尽为止,返回 (buckets, quota)。
175
183
  def _bucketize(docs, total: int, cfg, key_of, backflow: bool):
176
184
  """归池 + 分额 + 回流 → `(buckets, quota)`(`take` 与 `cut_report` 共用)。"""
177
185
  quota = caps(total, cfg)
@@ -193,6 +201,7 @@ def _bucketize(docs, total: int, cfg, key_of, backflow: bool):
193
201
  return buckets, quota
194
202
 
195
203
 
204
+ # 生效条件:docs 经 list(docs or [])(None/空容器→[]),pools 为 None/False 使 resolve(pools) 返回假值时返回 (docs[:int(total)], {});cfg 为真时转为 cut_report(docs, total, pools=pools, key_of=key_of, backflow=backflow) 并返回 (picked, report["taken"])。
196
205
  def take(docs, total: int, *, pools=None, key_of=None, backflow: bool = True):
197
206
  """分池截断并**回报各池实取数** → `(picked, taken)`。
198
207
 
@@ -210,6 +219,7 @@ def take(docs, total: int, *, pools=None, key_of=None, backflow: bool = True):
210
219
  return picked, report["taken"]
211
220
 
212
221
 
222
+ # 生效条件:docs 经 list(docs or [])、total 经 int(total),pools 为 None/False 使 resolve(pools) 返回假值时返回 (docs[:total], {"enabled": False});cfg 为真时用 _bucketize(docs, total, cfg, key_of, backflow),按 POOL_ORDER 逐池取 buckets[p][:quota[p]] 拼接 picked 并记录 taken/cands/lost,返回启用态完整 report。
213
223
  def cut_report(docs, total: int, *, pools=None, key_of=None,
214
224
  backflow: bool = True):
215
225
  """同 `take`,但回报**完整池账** → `(picked, report)`。
@@ -235,11 +245,13 @@ def cut_report(docs, total: int, *, pools=None, key_of=None,
235
245
  "cands": cands, "taken": taken, "lost": lost}
236
246
 
237
247
 
248
+ # 生效条件:docs 与 total 给出后无条件调用 take(docs, total, pools=pools, key_of=doc_key) 并只返回其第 0 项,pools 缺省为 None 原样透传。
238
249
  def cut(docs, total: int, *, pools=None) -> list:
239
250
  """检索 T2/T3 截断点专用:`docs = [(entry, fm, content)]`(只取结果)。"""
240
251
  return take(docs, total, pools=pools, key_of=doc_key)[0]
241
252
 
242
253
 
254
+ # 生效条件:report 为真值且 report.get("enabled") 为真时,把 dict(report["taken"])/dict(report["cands"])/dict(report["lost"]) 写入 stat 的 pool_taken/pool_cands/pool_lost;否则一个键都不写,始终返回 stat。
243
255
  def record_audit(stat: dict, report) -> dict:
244
256
  """把 `cut_report` 的池账落进检索 `stat`(**关闭态不写** → 不伪造池账)。
245
257
 
@@ -252,6 +264,7 @@ def record_audit(stat: dict, report) -> dict:
252
264
  return stat
253
265
 
254
266
 
267
+ # 生效条件:d[1].get("id") 取到真值(非 None/空串等假值)时以其为 node_id,否则回落 d[0]["path"],并总是把 d[0] 作为 entry 返回;d[0] 无 "path" 键时在回落分支抛 KeyError。
255
268
  def doc_key(d):
256
269
  """检索文档三元组 `(entry, fm, content)` → `(node_id, entry)`。"""
257
270
  return (d[1].get("id") or d[0]["path"]), d[0]
@@ -261,6 +274,7 @@ def doc_key(d):
261
274
  # 口径冻结与复测(只读)
262
275
  # --------------------------------------------------------------------------
263
276
 
277
+ # 生效条件:xs 先按 float 排序,xs 为空时返回 0.0;否则取 idx=max(0, min(len(xs)-1, ceil(q*len(xs))-1)) 并返回 xs[idx],q 本身未做取值范围校验。
264
278
  def _pct(xs, q):
265
279
  """分位数(最近秩法,确定性;零依赖)。"""
266
280
  xs = sorted(float(x) for x in xs)
@@ -270,6 +284,7 @@ def _pct(xs, q):
270
284
  return xs[idx]
271
285
 
272
286
 
287
+ # 生效条件:ids=sorted((cg.index.get("nodes") or {}).keys()) 为空时返回 [];否则 n 经 max(1, int(n))(n=0 会变成 1)、step=max(1, len(ids)//max(1,int(n))),对 ids[::step][:max(1,int(n))] 逐个 cg._read(entry),读失败或无 content 则 continue,首个以 # 开头的行按「含全角冒号取其后、否则去 # 与空格」生成 q,q 非空才追加,返回 out。
273
288
  def bench_queries(cg, n: int = 50) -> list:
274
289
  """从索引**确定性**取样查询词(按 id 排序等距抽,取节点标题行)。
275
290
 
@@ -308,10 +323,12 @@ DELTA_KEYS = ("p95_scanned", "p50_scanned", "mean_candidates",
308
323
  "index_share", "knowledge_share", "pool_lost_rate")
309
324
 
310
325
 
326
+ # 生效条件:xs 为真值(非空容器)时返回 sum(xs)/len(xs),xs 为假值(空容器或 None)时返回 0.0。
311
327
  def _mean(xs):
312
328
  return (sum(xs) / len(xs)) if xs else 0.0
313
329
 
314
330
 
331
+ # 生效条件:res 为假值(None/空列表)时返回空 dict;否则对每个 r 取 node=r[0] or {},以 pool_of(node.get("id"), node.get("frontmatter")) 归池并累加计数,返回 out。
315
332
  def _pool_counts(res) -> dict:
316
333
  """结果集的归池构成(对分池**敏感**的口径:索引类是否吃满召回)。"""
317
334
  out = {}
@@ -322,6 +339,7 @@ def _pool_counts(res) -> dict:
322
339
  return out
323
340
 
324
341
 
342
+ # 生效条件:queries 为真值时用 list(queries)、为假值(None/空列表)时改用 bench_queries(cg, n=n_queries);逐 q 调 cg.search(q, k=int(k), record=False, judge=judge, pools=pools) 且该调用抛异常则跳过该 q;meta.get("pre_cap") 为 None 时回落 meta.get("candidates")/cap,仅 cap 为真且 pre>cap 才计入截断与损失,且 pl.get("lost") 与 pl.get("cands") 均非空才计入 pool_lost_rate。
325
343
  def measure(cg, queries=None, *, k: int = 20, pools=None, judge: bool = False,
326
344
  n_queries: int = 50) -> dict:
327
345
  """同口径跑一遍(**只读**):系统成本口径 + 结果构成口径 + 池级截断损失。
@@ -387,12 +405,14 @@ def measure(cg, queries=None, *, k: int = 20, pools=None, judge: bool = False,
387
405
  "同口径前后对比只看方向与幅度,不当绝对结论")}
388
406
 
389
407
 
408
+ # 生效条件:调用即从 md_cg.mdcg 取 GLOBAL_CAP(getattr 缺省 0),取到假值(0/None/空串)时经 or 0 回落 0,返回 int(...)。
390
409
  def meta_cap(cg) -> int:
391
410
  """读当前全局额度(避免硬编码漂移)。"""
392
411
  from . import mdcg
393
412
  return int(getattr(mdcg, "GLOBAL_CAP", 0) or 0)
394
413
 
395
414
 
415
+ # 生效条件:queries 为真值时用 list(queries)、为假值(None/空列表)时用 bench_queries(cg, n=n_queries);before 恒以 pools=None 调用 measure,after 在 pools 为 None 时用模块常量 WEIGHTS、pools 显式(含 False)时原样传入;delta 只统计 DELTA_KEYS 中 before/after 两侧均为 int/float 的键,其余不参与 Δ 计算。
396
416
  def compare(cg, queries=None, *, k: int = 20, pools=None, n_queries: int = 50) -> dict:
397
417
  """§七 要求的「同口径复测」:关闭态 vs 启用态 一并给出 + 差值 + 副作用归因。"""
398
418
  qs = list(queries) if queries else bench_queries(cg, n=n_queries)
@@ -418,6 +438,7 @@ def compare(cg, queries=None, *, k: int = 20, pools=None, n_queries: int = 50) -
418
438
  "只说明该口径测不到池内重分配")}
419
439
 
420
440
 
441
+ # 生效条件:无入参,调用即返回由模块常量 POOL_ORDER/WEIGHTS/INDEX_PREFIXES/NEG_LAYERS/ENV_SWITCH 组装的自描述 dict,cap_ratio_sum 为 round(sum(WEIGHTS[p]["cap_ratio"] for p in POOL_ORDER), 12)。
421
442
  def catalog() -> dict:
422
443
  """自描述(供 MCP / 人工核对)。"""
423
444
  return {"layer": "召回分池与降权(§七)",
@@ -441,6 +462,7 @@ def catalog() -> dict:
441
462
  }
442
463
 
443
464
 
465
+ # 生效条件:pools 非 None 且非 False 时原样返回 pools;pools 为 None 或 False 时读 os.environ.get(ENV_SWITCH),缺失/空串经 or "" 归空串并 strip().lower(),属于 ("1","on","true","yes","y") 则返回 True,否则返回 None。
444
466
  def from_env(pools=None):
445
467
  """载体侧开关:`pools` 显式给出时优先;否则读 `MDCG_POOLING`(默认关)。"""
446
468
  if pools is not None and pools is not False:
@@ -448,4 +470,4 @@ def from_env(pools=None):
448
470
  v = (os.environ.get(ENV_SWITCH) or "").strip().lower()
449
471
  if v in ("1", "on", "true", "yes", "y"):
450
472
  return True
451
- return None
473
+ return None