@furongjun1999/dsh-memory 0.4.8 → 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 (231) 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 +6 -3
  47. package/lib/lib/mdcg_client.js +6 -5
  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_trust.py +361 -0
  195. package/md_cg/test_units_poll.py +71 -0
  196. package/md_cg/test_v14_fixes.py +397 -0
  197. package/md_cg/test_validity_filter.py +280 -0
  198. package/md_cg/test_wisdom_md_store.py +7 -3
  199. package/md_cg/test_writepipe.py +5 -1
  200. package/md_cg/theory.py +16 -1
  201. package/md_cg/tokens.py +40 -8
  202. package/md_cg/tool_face.py +13 -2
  203. package/md_cg/trust.py +943 -0
  204. package/md_cg/twophase.py +12 -1
  205. package/md_cg/units.py +129 -10
  206. package/md_cg/vision_evidence.py +24 -1
  207. package/md_cg/weights.py +24 -1
  208. package/md_cg/whitebox.py +32 -1
  209. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  210. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  211. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  212. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  213. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  214. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  215. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  216. package/md_cg/writelimit.py +18 -4
  217. package/md_cg/writepipe.py +178 -7
  218. package/package.json +2 -2
  219. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  220. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  221. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  222. package/src/hooks.ts +17 -21
  223. package/src/index.ts +33 -6
  224. package/src/lib/datapath.ts +211 -13
  225. package/src/lib/mdcg_client.ts +12 -8
  226. package/src/lib/mutual.ts +411 -411
  227. package/src/lib/token_store.ts +4 -5
  228. package/zcode/AGENTS.md +196 -195
  229. package/zcode/README.md +4 -0
  230. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
package/md_cg/routing.py CHANGED
@@ -29,6 +29,7 @@ _SKELETON_TAIL = re.compile(r"(知识点)?内容\s*[((]按骨架填充[))]\s
29
29
  ORPHAN = "orphan"
30
30
 
31
31
 
32
+ # 生效条件:raw 为真值时按 NFKC 归一化并 strip,最多 3 轮剥离 _INSTANCE_TAIL(无变化即停)、再剥 _SKELETON_TAIL、空白转下划线后返回;raw 为假值(None/空串)或处理后为空串时返回 ORPHAN。
32
33
  def normalize_domain(raw: str) -> str:
33
34
  """把自由文本的域描述归一化成稳定的短键。
34
35
 
@@ -50,6 +51,7 @@ def normalize_domain(raw: str) -> str:
50
51
  return s or ORPHAN
51
52
 
52
53
 
54
+ # 生效条件:tags 为真值时取其中首个 str(t) 以 "domain:" 开头的项,返回其前缀后内容的 normalize_domain 结果;无此标签且 condition_space 为真值时返回 normalize_domain(condition_space.get("observation_position"))(缺键即 None 归一为 ORPHAN);tags 为假值且 condition_space 为假值时返回 ORPHAN。
53
55
  def route_key(condition_space: dict = None, tags=None) -> str:
54
56
  """导出条件路由键。优先级:tags 的 domain: > observation_position 归一化 > orphan。
55
57
 
@@ -65,6 +67,7 @@ def route_key(condition_space: dict = None, tags=None) -> str:
65
67
  return ORPHAN
66
68
 
67
69
 
70
+ # 生效条件:key 等于 ORPHAN 时原样返回 ORPHAN;否则返回 "cond_" + 把 key 中非[\w中文-]字符替换为下划线并截前 24 字符的串 + "_" + key 的 sha256(utf-8)前 8 位十六进制。
68
71
  def bucket_dir(key: str) -> str:
69
72
  """路由键 → 目录名。中文键保留可读前缀 + 短哈希,避免文件系统非法字符/超长。"""
70
73
  if key == ORPHAN:
@@ -115,6 +118,7 @@ BIG_DOMAINS = {
115
118
  }
116
119
 
117
120
 
121
+ # 生效条件:terms 为真值时对 BIG_DOMAINS 每个大域统计命中词数,最高分为 0 或 terms 为假值(None/空)时返回 None,否则返回最高分大域名(并列取 BIG_DOMAINS 迭代序首个最高分)。
118
122
  def big_domain_classify(terms) -> str | None:
119
123
  """阶段 1:14 大域并行打分 → 收敛到 top-1。
120
124
 
@@ -137,6 +141,7 @@ def big_domain_classify(terms) -> str | None:
137
141
  return best[0]
138
142
 
139
143
 
144
+ # 生效条件:terms 为假值(None/空)时返回 {每个 BIG_DOMAINS 域: 0};terms 为真值时返回 {大域: 该域词表命中 terms 中词的个数}。
140
145
  def big_domain_score_breakdown(terms) -> dict:
141
146
  """暴露打分明细,便于审计与回归测试。"""
142
147
  if not terms:
@@ -156,6 +161,7 @@ def big_domain_score_breakdown(terms) -> dict:
156
161
  # 老函数保持不动 → P0/P1 基线可比性不受影响;新函数只供新路径(fuzzy)使用。
157
162
  # ---------------------------------------------------------------------------
158
163
 
164
+ # 生效条件:term 或 word 为假值(None/空串)返回 0.0;相等返回 1.0;word 是 term 子串返回 len(word)/len(term);term 是 word 子串返回 len(term)/len(word);二者无包含关系返回 0.0。
159
165
  def membership(term: str, word: str) -> float:
160
166
  """词 term 对代表词 word 的隶属度(0.0~1.0,越接近 1 越隶属)。
161
167
 
@@ -175,6 +181,7 @@ def membership(term: str, word: str) -> float:
175
181
  return 0.0
176
182
 
177
183
 
184
+ # 生效条件:无入参,恒遍历模块级常量 BIG_DOMAINS 的各域代表词表,返回 {代表词: 包含该词的域个数}。
178
185
  def _build_domain_df() -> dict:
179
186
  """代表词 → 覆盖它的大域数(IDF 的分母)。"""
180
187
  df = {}
@@ -187,6 +194,7 @@ def _build_domain_df() -> dict:
187
194
  _DOMAIN_DF = _build_domain_df()
188
195
 
189
196
 
197
+ # 生效条件:恒返回 math.log(1.0 + len(BIG_DOMAINS) / _DOMAIN_DF.get(word, 1)),即 word 在 _DOMAIN_DF 中取其覆盖域数,缺键(含任何未登记值)时按 _DOMAIN_DF.get 的默认 1 代入。
190
198
  def domain_idf(word: str) -> float:
191
199
  """代表词的区分度权重:log(1 + 大域总数 / 覆盖它的大域数)。
192
200
 
@@ -196,6 +204,7 @@ def domain_idf(word: str) -> float:
196
204
  return math.log(1.0 + len(BIG_DOMAINS) / _DOMAIN_DF.get(word, 1))
197
205
 
198
206
 
207
+ # 生效条件:terms 是 dict 时以其条目(键 str(t) 以 "__" 开头者剔除、值为 float)为词权重并忽略 weights,否则以 wmap.get(t, 1.0) 为权重(weights 为假值即 None/空 dict 时 wmap 为空、全部权重取 1.0,terms 为假值则词集为空);逐域累加「词权重 × 该词在域词表内最大(membership × domain_idf)」,权重 <=0 的词跳过,返回 {大域: round(得分, 6)}。
199
208
  def big_domain_score_weighted(terms, weights=None) -> dict:
200
209
  """阶段 1(分级版):14 大域并行打分,按「隶属度 × IDF」加权。
201
210
 
@@ -226,6 +235,7 @@ def big_domain_score_weighted(terms, weights=None) -> dict:
226
235
  return out
227
236
 
228
237
 
238
+ # 生效条件:以 terms、weights 计算各域加权得分(terms 为假值时各域均为 0.0),得分为空或最高分 <= min_score(默认 0.0)时返回 None,否则返回最高分大域名。
229
239
  def big_domain_classify_weighted(terms, weights=None, min_score: float = 0.0):
230
240
  """阶段 1(分级版)收敛到 top-1;全部低于 min_score → None。"""
231
241
  scores = big_domain_score_weighted(terms, weights)
@@ -237,6 +247,7 @@ def big_domain_classify_weighted(terms, weights=None, min_score: float = 0.0):
237
247
  return best[0]
238
248
 
239
249
 
250
+ # 生效条件:a 或 b 为假值(None/空串)返回 0.0;相等返回 1.0;a 是 b 子串返回 len(a)/len(b);b 是 a 子串返回 len(b)/len(a);否则按二者二元组字符集合的 Jaccard 返回 len(ga & gb)/len(ga | gb),任一集合为空(如单字符键)时返回 0.0。
240
251
  def domain_similarity(a: str, b: str) -> float:
241
252
  """两个归一化域键的相似度(0~1):相同 1.0;包含取长度比;否则二元组 Jaccard。
242
253
 
@@ -258,6 +269,7 @@ def domain_similarity(a: str, b: str) -> float:
258
269
  return len(ga & gb) / len(ga | gb)
259
270
 
260
271
 
272
+ # 生效条件:counts 各值之和为 0(含空 dict 与全 0 计数)时返回 {'ok': True, 'reason': 'empty', 'buckets': 0};否则在最大桶占比 >30%、桶数 >1 且单例桶占比 >50%、期望扫描 >30% 中命中的项写入 problems,返回含 ok(=problems 为空)、buckets、nodes、max_bucket_share、singleton_ratio、expected_scan、problems 的 dict。
261
273
  def bucket_health(counts: dict) -> dict:
262
274
  """分区健康度自检。分桶键一旦退化(巨桶或碎片化),条件路由就是纸面收益,
263
275
  必须在写入侧就能发现,而不是等召回变差才回头查。
@@ -288,3 +300,67 @@ def bucket_health(counts: dict) -> dict:
288
300
  "expected_scan": expected_scan,
289
301
  "problems": problems,
290
302
  }
303
+
304
+
305
+ # ---------------------------------------------------------------------------
306
+ # 节点侧大域标签(S1 大域先验收敛的前置元数据)
307
+ #
308
+ # 为何需要:`big_domain_classify` 一直只作用于 **query 侧**(terms),节点侧没有域字段,
309
+ # 于是「14 大域并行打分」算完只能写进 meta(审计偏差 4:先验算出来却当成报告)。
310
+ # 要让大域先验真正参与候选收敛,节点必须在写入时固化自己的域。
311
+ # 口径:与查询侧同一个分类器(BIG_DOMAINS + big_domain_classify),保证两侧同构。
312
+ # ---------------------------------------------------------------------------
313
+
314
+ # 域词取词:中文(≥2 字)与拉丁词(≥2 字)。词表命中用「包含关系」判定,
315
+ # 故 2 字以上的切口足以覆盖 1~4 字代表词(如「工程」命中「工程师」)。
316
+ DOMAIN_TERM_RE = re.compile(r"[\u4e00-\u9fff]{2,}|[A-Za-z][A-Za-z0-9_]{1,}")
317
+
318
+
319
+ # 生效条件:text 为假值(None/空串)时返回 [];否则以 DOMAIN_TERM_RE 取词、按出现顺序去重后返回,
320
+ # 最多 limit 个(limit 为假值/负数时按空处理,返回 [])。
321
+ def domain_terms(text, limit: int = 400) -> list:
322
+ """从节点正文/标题抽「域词」(供 big_domain_classify 使用)。
323
+
324
+ 只做最小分词:中文按 ≥2 字连续片段、拉丁按 [A-Za-z][A-Za-z0-9_]+ 取词;
325
+ 去重保序,最多 limit 个(默认 400,避免长文把分类器拖慢)。
326
+ """
327
+ if not text or not limit or limit < 1:
328
+ return []
329
+ out, seen = [], set()
330
+ for m in DOMAIN_TERM_RE.finditer(str(text)):
331
+ w = m.group(0)
332
+ if w in seen:
333
+ continue
334
+ seen.add(w)
335
+ out.append(w)
336
+ if len(out) >= limit:
337
+ break
338
+ return out
339
+
340
+
341
+ # 生效条件:bucket 为假值或等于 ORPHAN 时返回空串;否则去掉 "cond_" 前缀、并当末段为 8 位十六进制哈希时剥掉该段,返回剩余的可读键。
342
+ def bucket_key_readable(bucket: str) -> str:
343
+ """桶目录名 → 可读键(S1b query 侧桶推断用):'cond_感知系统_d94e90d2' → '感知系统'。
344
+
345
+ 与 `bucket_dir` 互逆(丢哈希段);`orphan`/空值返回空串(S1b 不把 orphan 当键,orphan 恒作兜底)。
346
+ """
347
+ if not bucket or bucket == ORPHAN:
348
+ return ""
349
+ s = str(bucket)
350
+ if s.startswith("cond_"):
351
+ s = s[len("cond_"):]
352
+ head, sep, tail = s.rpartition("_")
353
+ if sep and len(tail) == 8 and all(c in "0123456789abcdef" for c in tail):
354
+ s = head
355
+ return s
356
+
357
+
358
+ # 生效条件:text 为假值或取不到任何域词时返回 None;否则返回 big_domain_classify(domain_terms(text))
359
+ # 的结果(无有效域信号时亦为 None,调用方据此决定是否落域字段)。
360
+ def classify_text(text, limit: int = 400):
361
+ """正文 → 大域名(节点侧域标签真源)。与查询侧共用 big_domain_classify。
362
+
363
+ 返回 None 表示「无有效域信号」——此时**不写** big_domain 字段,
364
+ 该节点留在 ORPHAN/兜底池(S1 收敛时必须能被兜底召回,见契约 §3 S1 不变量)。
365
+ """
366
+ return big_domain_classify(domain_terms(text, limit))
package/md_cg/scrub.py CHANGED
@@ -71,6 +71,9 @@ STRATUM_WEIGHTS = {"stale": 0.20, "low_conf": 0.20, "disputed": 0.15,
71
71
  CONTAMINATION = {
72
72
  "contradiction": ("high", ("weaken", "demote")),
73
73
  "expired": ("high", ("weaken", "demote")),
74
+ # 未生效(双时间轴另一侧,2026-09-19):**不是错误**——只提示「此刻不适用」,
75
+ # 故 severity=info、处置仅 hint(不 weaken / 不 demote / 不删)。
76
+ "not_yet": ("info", ("hint",)),
74
77
  "duplicate": ("medium", ("hint",)),
75
78
  "orphan_noise": ("low", ("weaken", "demote")),
76
79
  "unverified": ("info", ("hint",)),
@@ -82,10 +85,12 @@ SEVERITY_ORDER = {"high": 3, "medium": 2, "low": 1, "info": 0}
82
85
  # 工具
83
86
  # --------------------------------------------------------------------------
84
87
 
88
+ # 生效条件:当 cg 的 index 为真且其 "nodes" 为真时返回该值,否则(cg 无 index、index 为假值、缺 "nodes" 或 "nodes" 为假值)返回 {};
85
89
  def _nodes(cg) -> dict:
86
90
  return (getattr(cg, "index", None) or {}).get("nodes") or {}
87
91
 
88
92
 
93
+ # 生效条件:cg 与 nid 传入后,若 cache 非 None 且 nid 已在 cache 中则直接返回 cache[nid](即使其值为假值);否则尝试 cg.get(nid),异常或返回假值时按空节点处理,再取其中的 "frontmatter" 真值,若为假值则用 {};cache 非 None 时把结果写入 cache[nid] 后返回;
89
94
  def _fm_of(cg, nid, cache=None) -> dict:
90
95
  """取节点 frontmatter(带可选缓存)。不可读(缺密钥 / 已删)→ {}。"""
91
96
  if cache is not None and nid in cache:
@@ -101,6 +106,7 @@ def _fm_of(cg, nid, cache=None) -> dict:
101
106
  return fm
102
107
 
103
108
 
109
+ # 生效条件:cg.access_counts() 调用成功时返回其结果(访问次数, 最后访问时间);调用抛出任何 Exception 时返回 ({}, {});
104
110
  def _access(cg):
105
111
  """(访问次数, 最后访问时间):读访问日志,未 compact 的也算。"""
106
112
  try:
@@ -109,6 +115,7 @@ def _access(cg):
109
115
  return {}, {}
110
116
 
111
117
 
118
+ # 生效条件:`from . import chain` 成功且 chain.adjacency(cg) 正常返回时返回该 dict,导入或调用抛任何异常时返回 {}。
112
119
  def _adjacency(cg) -> dict:
113
120
  try:
114
121
  from . import chain
@@ -117,6 +124,7 @@ def _adjacency(cg) -> dict:
117
124
  return {}
118
125
 
119
126
 
127
+ # 生效条件:float(ts or 0) 抛 TypeError/ValueError 时返回 0.0,转换后为 0(含 ts 为 0/空串/None 等假值)时返回 0.0,否则返回 (now - ts)/86400.0。
120
128
  def _days(ts, now) -> float:
121
129
  try:
122
130
  ts = float(ts or 0)
@@ -125,6 +133,7 @@ def _days(ts, now) -> float:
125
133
  return (now - ts) / 86400.0 if ts else 0.0
126
134
 
127
135
 
136
+ # 生效条件:调用即返回带 t 与 op 的 rec;写 append_jsonl(os.path.join(cg.root, SCRUB_LOG), rec) 抛任何异常都被吞掉,不影响返回值。
128
137
  def _log(cg, op: str, **rec) -> dict:
129
138
  rec = dict(rec, t=time.time(), op=op)
130
139
  try:
@@ -134,6 +143,7 @@ def _log(cg, op: str, **rec) -> dict:
134
143
  return rec
135
144
 
136
145
 
146
+ # 生效条件:仅当 read_jsonl(cg.root 下 SCRUB_LOG) 的记录 op=="decontaminate" 且 ok 为真时,把 (rec.get("node_id"), rec.get("kind")) 收进返回集合;无此类记录返回空集。
137
147
  def _handled(cg) -> set:
138
148
  """已处置过的 (node_id, kind):保证去污染幂等(审计即状态)。"""
139
149
  out = set()
@@ -147,6 +157,7 @@ def _handled(cg) -> set:
147
157
  # ① 记忆抽查
148
158
  # --------------------------------------------------------------------------
149
159
 
160
+ # 生效条件:当 cg 的节点/访问/邻接数据可取时,对每个「layer 不在 SELF_LAYERS」的节点(源码仅以 `if layer in SELF_LAYERS: continue` 排除,未要求 layer 非空)按 now、stale_days、unverified_days 判定入池——last 访问时间距今≥stale_days 时入 stale,last 为假值且 created_at 距今≥stale_days 时入 stale,访问计数 acc≥3 入 hot,邻接度 deg==0 且 layer=="contextual" 入 orphan,evidence_count≤0 且 layer=="knowledge" 且 age_d≥unverified_days 入 unverified,evidence_count>0 且 reads<int(max_reads) 且 _fm_of 返回非空前台时按 negative_evidence>0 入 disputed、按 confidence<LOW_CONF(confidence 缺键回落 0.6)入 low_conf(max_reads 为 0 时 int(max_reads)=0,reads<0 恒假,故 disputed/low_conf 不产生),每个节点无条件入 random 池,最后按各池 key 排序返回 pools(片段仅见排序段,未展示抽样阶段);多分支无法一句话覆盖全部分支。
150
161
  def _pool_candidates(cg, *, now, stale_days, unverified_days, max_reads=200):
151
162
  """构造各层候选池(不读文件的部分先用索引 + 访问日志)。"""
152
163
  nodes = _nodes(cg)
@@ -210,6 +221,7 @@ def _pool_candidates(cg, *, now, stale_days, unverified_days, max_reads=200):
210
221
  return pools
211
222
 
212
223
 
224
+ # 生效条件:strategy=="random" 时直接返回 {"random": int(n)};strategy=="risk" 时按剔除 hot/random 后的 STRATUM_WEIGHTS 权重分配;其它策略名走全权重分配,两者都把 int(n) 余量补进已存在的 random 键否则补进 stale。
213
225
  def _quota(n: int, strategy: str) -> dict:
214
226
  if strategy == "random":
215
227
  return {"random": int(n)}
@@ -231,6 +243,7 @@ def _quota(n: int, strategy: str) -> dict:
231
243
  return out
232
244
 
233
245
 
246
+ # 生效条件:k<=0 或 pool 为假值(空池)时返回 [],否则取池前 k*3 项后用 random.Random(f"{seed}:{stratum}") 稳定洗牌并返回前 k 项(池长不足 k*3 时对全池洗牌)。
234
247
  def _pick(pool, k: int, seed, stratum: str):
235
248
  """从池中取 k 个:风险最高的 3k 个入池,再按 seed 稳定洗牌。"""
236
249
  if k <= 0 or not pool:
@@ -242,6 +255,7 @@ def _pick(pool, k: int, seed, stratum: str):
242
255
  return cand[:k]
243
256
 
244
257
 
258
+ # 生效条件:strategy 经 str(strategy or "stratified").lower() 后属于 stratified/risk/random 时返回带分层标签的样本(seed 为 None 时取 0,n 为 0 时 picked 为空),否则抛 ValueError。
245
259
  def sample(cg, n: int = DEFAULT_SAMPLE, *, strategy: str = "stratified",
246
260
  seed=None, stale_days: float = STALE_DAYS,
247
261
  unverified_days: float = UNVERIFIED_DAYS,
@@ -284,12 +298,14 @@ def sample(cg, n: int = DEFAULT_SAMPLE, *, strategy: str = "stratified",
284
298
  # ② 联想
285
299
  # --------------------------------------------------------------------------
286
300
 
301
+ # 生效条件:a 或 b 为假值(空集)时返回 0.0,否则返回 len(a & b)/len(a | b)。
287
302
  def _jaccard(a: set, b: set) -> float:
288
303
  if not a or not b:
289
304
  return 0.0
290
305
  return len(a & b) / len(a | b)
291
306
 
292
307
 
308
+ # 生效条件:对 node_id 汇总关系链(chain.walk 出边与入边,max_depth=int(hops))、子图层级(subgraph.expand 命中项与父索引逐级上溯 int(hops) 层)以及 lexical 为真时的词法近邻(只在 _nodes 前 int(max_scan) 个节点内比 bigram、sim≥min_sim 者),返回按 weight 降序的 items[:int(limit)]。
293
309
  def associate(cg, node_id: str, *, hops: int = DEFAULT_HOPS, limit: int = 30,
294
310
  lexical: bool = True, min_sim: float = 0.25,
295
311
  max_scan: int = MAX_ASSOC_SCAN) -> dict:
@@ -300,6 +316,7 @@ def associate(cg, node_id: str, *, hops: int = DEFAULT_HOPS, limit: int = 30,
300
316
  """
301
317
  related = {}
302
318
 
319
+ # 生效条件:nid 为真且 nid != node_id 时,构造 rec={'node_id':nid,'via':via,'weight':round(float(weight),4),'depth':int(depth)} 并并入 extra;仅当 related 中 nid 不存在或 rec['weight'] 大于已有 weight 时更新 related[nid];nid 为假或等于 node_id 时直接返回;
303
320
  def put(nid, via, weight, depth=1, **extra):
304
321
  if not nid or nid == node_id:
305
322
  return
@@ -385,13 +402,19 @@ def associate(cg, node_id: str, *, hops: int = DEFAULT_HOPS, limit: int = 30,
385
402
  # ③ 去污染:确定性判据
386
403
  # --------------------------------------------------------------------------
387
404
 
388
- _EXPIRY_KEYS = ("valid_until", "expires_at", "expire_at", "expiry", "deadline")
405
+ # 已结束键族(2026-09-19 阶段一:补规范名 `effective_until` 与冗余时刻 `expired_at`)。
406
+ # 纪律:`believed_at`(信念时间)**两族都不入**——它既不是「已结束」也不是「尚未开始」,
407
+ # 而是「体系何时确认此条」的取代/审核锚。键族真源见 md_cg/trust.py(FROM_ALIASES/UNTIL_ALIASES),
408
+ # 两侧新增键须同步(交叉守卫 test_validity_filter)。
409
+ _EXPIRY_KEYS = ("effective_until", "valid_until", "expires_at", "expire_at",
410
+ "expiry", "deadline", "expired_at")
389
411
  _SKIP_KEYS = ("功能名", "执行", "条件", "来源", "标签", "状态", "备注", "标题",
390
412
  "描述", "name", "id", "title", "layer", "tags", "说明")
391
413
  _NEG_WORDS = ("禁止", "不得", "不要", "不能", "切勿", "避免", "不应", "不可")
392
414
  _POS_WORDS = ("应当", "建议", "必须", "需要", "可以", "允许", "推荐", "宜")
393
415
 
394
416
 
417
+ # 生效条件:v 为 bool 返回 None;v 为 int/float 时仅 f>1e8 返回 f 否则 None;str(v or "").strip() 为空返回 None;否则按 "%Y-%m-%dT%H:%M:%S"/"%Y-%m-%d %H:%M:%S"/"%Y-%m-%d" 依次取前 19/19/10 字符尝试解析,全失败后再试 float(s),>1e8 返回否则 None,float 亦失败返回 None。
395
418
  def _to_ts(v):
396
419
  """宽松时间解析:秒级时间戳 / ISO / `YYYY-MM-DD`。无法识别 → None。"""
397
420
  if isinstance(v, bool):
@@ -415,6 +438,7 @@ def _to_ts(v):
415
438
  return None
416
439
 
417
440
 
441
+ # 生效条件:对 (content or "").splitlines() 的每行去 # 后,先试中文全角 ":"、无则试 ":",切出的键长度在 2–12、值非空且键不在 _SKIP_KEYS 时以 setdefault 记录(每键只留首次出现),无合格行返回 {}。
418
442
  def _kv_pairs(content) -> dict:
419
443
  """抽 `键:值` 对(中文/英文冒号),跳过结构字段。"""
420
444
  out = {}
@@ -432,12 +456,14 @@ def _kv_pairs(content) -> dict:
432
456
  return out
433
457
 
434
458
 
459
+ # 生效条件:c 取 content or "",含 _NEG_WORDS 任一返回 -1 分量、含 _POS_WORDS 任一返回 +1 分量,结果为两者之和(都不含时为 0)。
435
460
  def _polarity(content) -> int:
436
461
  c = content or ""
437
462
  return (-1 if any(w in c for w in _NEG_WORDS) else 0) + \
438
463
  (1 if any(w in c for w in _POS_WORDS) else 0)
439
464
 
440
465
 
466
+ # 生效条件:按 _EXPIRY_KEYS 顺序遍历 fm,返回首个满足「键在 fm 中且 _to_ts 非 None 且 ts<now」的 (k, fm.get(k));该键解析为 None 或 ts≥now 时继续检查后续键,全不满足返回 None。
441
467
  def _expired(fm, now):
442
468
  for k in _EXPIRY_KEYS:
443
469
  if k in fm:
@@ -447,6 +473,25 @@ def _expired(fm, now):
447
473
  return None
448
474
 
449
475
 
476
+ # 未生效键(双时间轴的起点,2026-09-19 · 真源 md_cg/trust.py)。
477
+ # 纪律一:`valid_from` **绝不并入 `_EXPIRY_KEYS`**——两者语义相反(「尚未开始」vs
478
+ # 「已经结束」),并入会让「未来才生效」被误判为「已失效」并触发 weaken/demote。
479
+ # 纪律二:`believed_at`(信念时间)同上,**两族都不入**——第三类语义(体系何时确认)。
480
+ _NOT_YET_KEYS = ("valid_from", "valid_since", "effective_from", "starts_at")
481
+
482
+
483
+ # 生效条件:按 _NOT_YET_KEYS 顺序遍历 fm,返回首个满足「键在 fm 中且 _to_ts 非 None 且 ts>now」的 (k, fm.get(k));全不满足返回 None。
484
+ def _not_yet(fm, now):
485
+ """宽松判定「尚未生效」(与 `_expired` 同口径,方向相反)。"""
486
+ for k in _NOT_YET_KEYS:
487
+ if k in fm:
488
+ ts = _to_ts(fm.get(k))
489
+ if ts is not None and ts > now:
490
+ return k, fm.get(k)
491
+ return None
492
+
493
+
494
+ # 生效条件:对 content 取 kv_pairs、bigrams 和 polarity,遍历 related(若 related 为假值则视为空)的前 int(max_compare) 个 r,以 r["node_id"] 调 cg.get;若某 oid 节点可读非空,先在其 content 与 content 的共同键中找到值不同者并返回 {'with':oid,'via':r.get('via'),'why':'同键不同值:...'};否则若极性乘积 <0 且双方 bigram 非空,且共享 bigram 数 >=3 且 ratio>=0.15,返回 {'with':oid,'via':r.get('via'),'why':'极性相反且共享内容:...'};全部遍历完无命中则返回 None;
450
495
  def _contradiction(cg, content, related, max_compare=10):
451
496
  """与同族节点比对:同键不同值 / 极性相反且共享 bigram。"""
452
497
  kv_a = _kv_pairs(content)
@@ -476,6 +521,7 @@ def _contradiction(cg, content, related, max_compare=10):
476
521
  return None
477
522
 
478
523
 
524
+ # 生效条件:ids 在 node_ids 为 None 时取索引全部键、为 str 时取单元素列表、否则取 list(node_ids),limit 为真值时截断为前 int(limit) 个;跳过 layer 在 SELF_LAYERS 的节点和 cg.get 取不到正文的节点,对剩余每个节点按 min_severity 门限累加 expired/unverified/orphan_noise/duplicate/contradiction,返回 ok=无 high 且无 medium 的结果。
479
525
  def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
480
526
  limit=None, unverified_days: float = UNVERIFIED_DAYS,
481
527
  lexical: bool = True, min_sim: float = 0.15) -> dict:
@@ -512,6 +558,7 @@ def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
512
558
  content = node.get("content") or ""
513
559
  age_d = _days(fm.get("created_at") or e.get("created_at"), now)
514
560
 
561
+ # 生效条件:kind 是 CONTAMINATION 的键(否则 KeyError)且 CONTAMINATION[kind] 取出的 sev 在 SEVERITY_ORDER 中的值(缺键按 0)不小于闭包变量 min_severity 在 SEVERITY_ORDER 中的值(缺键按 0)时,把 {node_id, layer, kind, severity, detail, fix} 用 **extra 覆盖更新后追加到闭包 issues;sev 的值更小则直接 return 不追加(该函数无返回值)。
515
562
  def add(kind, detail, **extra):
516
563
  sev, actions = CONTAMINATION[kind]
517
564
  if SEVERITY_ORDER.get(sev, 0) < SEVERITY_ORDER.get(min_severity, 0):
@@ -525,6 +572,12 @@ def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
525
572
  if hit:
526
573
  add("expired", f"时效已过:{hit[0]}={hit[1]}", evidence=hit[0])
527
574
 
575
+ # 双时间轴另一侧(2026-09-19):尚未生效**不是错误**——只提示「此刻不适用」,
576
+ # 处置由 CONTAMINATION["not_yet"] 定为 info 级 + 仅 hint(不 weaken / 不 demote)。
577
+ ny = _not_yet(fm, now)
578
+ if ny:
579
+ add("not_yet", f"尚未生效:{ny[0]}={ny[1]}", evidence=ny[0])
580
+
528
581
  if (layer == "knowledge" and age_d >= unverified_days
529
582
  and int(e.get("evidence_count") or 0) <= 0):
530
583
  add("unverified", f"knowledge 层 {age_d:.0f} 天零验证")
@@ -562,6 +615,7 @@ def audit(cg, node_ids=None, *, hops: int = 1, min_severity: str = "info",
562
615
  "t": now}
563
616
 
564
617
 
618
+ # 生效条件:循环 max(1, int(times)) 次调用 cg.verify(nid, "scrub:"+kind+":"+str(detail)[:120], "weakened"),返回最后一次调用结果(times≤0 时按 1 次执行)。
565
619
  def _weaken(cg, nid, kind, detail, times=1):
566
620
  res = None
567
621
  for _ in range(max(1, int(times))):
@@ -569,6 +623,7 @@ def _weaken(cg, nid, kind, detail, times=1):
569
623
  return res
570
624
 
571
625
 
626
+ # 生效条件:kinds 为真且 issue 的 kind 不在其中则跳过;kind 为 duplicate/unverified 记 hint,已出现在 _handled 记 skip,protect.is_protected 为真且 override 为假记 skip_protected,dry_run 为真记 planned;仅 dry_run 为假时对余下 issue 执行 _weaken,并在 demote 为真、severity 为 high 或 low 且当前层非 contextual 时降级到 contextual,返回计数与 actions。
572
627
  def decontaminate(cg, node_ids=None, *, kinds=None, dry_run: bool = True,
573
628
  min_severity: str = "medium", hops: int = 1, actor=None,
574
629
  override: bool = False, weaken_times: int = 1,
@@ -650,6 +705,7 @@ def decontaminate(cg, node_ids=None, *, kinds=None, dry_run: bool = True,
650
705
  # ④ 校准偏差
651
706
  # --------------------------------------------------------------------------
652
707
 
708
+ # 生效条件:对 cg 中每个「layer 不在 SELF_LAYERS」且 evidence_count≥int(min_evidence)(min_evidence=0 时该比较恒假而不早退)的节点,若 protect.is_protected 为假或 override 为真,且 cg.get(nid) 未抛异常并返回真值节点,则取 fm.get("confidence", 0.6)(缺键才回落 0.6,键存在为 None/假值不回落)为 old,算出 round(max(0.0, min(0.99, old+float(offset))),4),与 old 差<1e-9 时跳过,否则写 fm["confidence"] 与 fm["calibration"] 并调用 cg._write_node 成功时 adjusted+1(写回异常被吞掉不计数),返回 (adjusted, skipped),其中 skipped 只累计「layer 属 SELF_LAYERS」或被 protect 拦下且非 override 的节点。
653
709
  def _apply_offset(cg, offset, *, override=False, min_evidence=1):
654
710
  nodes = _nodes(cg)
655
711
  adjusted = skipped = 0
@@ -686,6 +742,7 @@ def _apply_offset(cg, offset, *, override=False, min_evidence=1):
686
742
  return adjusted, skipped
687
743
 
688
744
 
745
+ # 生效条件:cg 和 apply/override/actor/max_offset/min_evidence 传入后,若 metacognition.calibration(cg) 返回 ok 假,则返回 {'ok':False,'reason':cal.get('reason') or 'insufficient_data',...};若 ok 真,则用 gap=float(cal.get('gap') or 0.0) 和 max_offset 计算 offset=round(max(-max_offset,min(max_offset,-gap)),4),对 cal.get('bins') or [] 中 accuracy 非 None 的 bin 生成 bins_bias 并排序;仅当 apply 为真且 abs(offset)>1e-9 时调用 _apply_offset(cg,offset,override=override,min_evidence=min_evidence) 并写日志,最后返回 ok True 及 verdict/gap/建议 offset 等字段;
689
746
  def calibrate(cg, *, apply: bool = False, override: bool = False, actor=None,
690
747
  max_offset: float = MAX_OFFSET, min_evidence: int = 1) -> dict:
691
748
  """校准偏差:自报置信 vs 实测正确率 → 偏置建议(`apply=True` 才写回)。"""
@@ -734,6 +791,7 @@ def calibrate(cg, *, apply: bool = False, override: bool = False, actor=None,
734
791
  # 一轮完整自净 + 审计
735
792
  # --------------------------------------------------------------------------
736
793
 
794
+ # 生效条件:cg 与 n/seed/dry_run/hops/strategy/apply_calibration/actor 传入后,按 n 与 strategy、seed 调 sample;对 sample 结果前 8 个 node_id 按 hops 调 associate;按 dry_run/hops/actor 调 decontaminate;按 apply_calibration/actor 调 calibrate;若 decontaminate 的 audit.issues 中存在 severity 为 high 或 medium 的项则 out.ok 为 False,否则为 True,并返回含 sample/associate/audit/decontaminate/calibration/t 的 out;
737
795
  def sweep(cg, *, n: int = DEFAULT_SAMPLE, seed=None, dry_run: bool = True,
738
796
  hops: int = DEFAULT_HOPS, strategy: str = "stratified",
739
797
  apply_calibration: bool = False, actor=None) -> dict:
@@ -758,11 +816,13 @@ def sweep(cg, *, n: int = DEFAULT_SAMPLE, seed=None, dry_run: bool = True,
758
816
  return out
759
817
 
760
818
 
819
+ # 生效条件:返回 cg.root 下 SCRUB_LOG 的全部记录条数 n 与 recs[-int(limit):](limit=0 时切片为 recs[0:] 即返回全部记录)。
761
820
  def history(cg, limit: int = 100) -> dict:
762
821
  recs = list(read_jsonl(os.path.join(cg.root, SCRUB_LOG)))
763
822
  return {"n": len(recs), "records": recs[-int(limit):]}
764
823
 
765
824
 
825
+ # 生效条件:读取 cg.root 下 SCRUB_LOG 的 JSONL 记录,过滤 op=="sweep" 得 sweeps、op=="decontaminate" 且 ok 为真得 decs;last 为 sweeps 最后一项或 None;返回 {'sweeps':len(sweeps),'decontaminated':len(decs),'last_sweep':last 的 t/n_issues/n_high_medium/applied/dry_run/calibration 或 None};
766
826
  def summary(cg) -> dict:
767
827
  """给 health_os / 自维持循环用的只读摘要。"""
768
828
  recs = list(read_jsonl(os.path.join(cg.root, SCRUB_LOG)))
@@ -779,6 +839,7 @@ def summary(cg) -> dict:
779
839
  if last else None)}
780
840
 
781
841
 
842
+ # 生效条件:无入参调用即返回固定的 actions 列表、STRATA 列表、CONTAMINATION 映射(每项取 severity 与 actions)与 STALE_DAYS/UNVERIFIED_DAYS/LOW_CONF/ORPHAN_IMPORTANCE/MAX_OFFSET 阈值字典。
782
843
  def catalog() -> dict:
783
844
  return {"actions": ["sample", "associate", "audit", "decontaminate",
784
845
  "calibrate", "sweep", "history", "summary", "catalog"],
@@ -789,4 +850,4 @@ def catalog() -> dict:
789
850
  "unverified_days": UNVERIFIED_DAYS,
790
851
  "low_conf": LOW_CONF,
791
852
  "orphan_importance": ORPHAN_IMPORTANCE,
792
- "max_offset": MAX_OFFSET}}
853
+ "max_offset": MAX_OFFSET}}
package/md_cg/security.py CHANGED
@@ -28,6 +28,7 @@ class AccessDenied(Exception):
28
28
  """权限拒绝(读/写/管理)。"""
29
29
 
30
30
 
31
+ # 生效条件:形参 level 为模块级常量 SENSITIVITY_ORDER 中的元素时返回其下标,否则抛 AccessDenied。
31
32
  def _rank(level: str) -> int:
32
33
  try:
33
34
  return SENSITIVITY_ORDER.index(level)
@@ -35,6 +36,7 @@ def _rank(level: str) -> int:
35
36
  raise AccessDenied(f"未知敏感度/密级:{level}") from None
36
37
 
37
38
 
39
+ # 生效条件:全部形参均可省略、clearance 默认取模块常量 DEFAULT_SENSITIVITY,构造时先经 _rank(clearance) 校验,随后 session 为假值(含空串)回落为 sess_+uuid 十二位、role 假值回落 "system"、expires_at 为假值(含 0)置 None、layers_allow/ops_allow 为 None 时保持 None 否则转 tuple、theory_ok 经 bool() 转换。
38
40
  class Principal:
39
41
  """调用方身份(一次会话一个)。
40
42
 
@@ -65,6 +67,7 @@ class Principal:
65
67
  theory_version —— 当前声明的协议版本(审计与 whoami 用)
66
68
  """
67
69
 
70
+ # 生效条件:clearance 须为模块级常量 SENSITIVITY_ORDER 成员(否则 _rank 抛 AccessDenied),session 为假值(含 None/空串)时生成 sess_ 随机串,expires_at 为假值(含 None/0)时存 None 否则 float(expires_at),layers_allow/ops_allow 为 None 时存 None 否则 tuple 化。
68
71
  def __init__(self, tenant: str = "default", actor: str = "system",
69
72
  clearance: str = DEFAULT_SENSITIVITY, can_write: bool = True,
70
73
  can_admin: bool = False, session: str = None,
@@ -99,31 +102,38 @@ class Principal:
99
102
 
100
103
  # ---------- 基础判定 ----------
101
104
 
105
+ # 生效条件:形参 sensitivity 与 self.clearance 均可被 _rank 映射到模块级常量 SENSITIVITY_ORDER 中时,返回前者排名是否不高于后者;任一不在其中则 _rank 抛 AccessDenied。
102
106
  def allows(self, sensitivity: str) -> bool:
103
107
  """clearance 是否覆盖该敏感度(可读/可写)。"""
104
108
  return _rank(sensitivity) <= _rank(self.clearance)
105
109
 
110
+ # 生效条件:无 required 形参或模块级常量前置,仅当 self.expires_at(来自 __init__ 的 expires_at)不为 None 且 time.time() 大于它时返回 True,否则返回 False。
106
111
  def expired(self) -> bool:
107
112
  return self.expires_at is not None and time.time() > self.expires_at
108
113
 
109
114
  @staticmethod
115
+ # 生效条件:allow 为 None 时返回 True(未声明=不限制),allow 非 None 时返回 "'*' in allow 或 name in allow" 的布尔结果。
110
116
  def _in_scope(allow, name: str) -> bool:
111
117
  if allow is None: # 未声明 = 不限制(兼容直接构造)
112
118
  return True
113
119
  return "*" in allow or name in allow
114
120
 
121
+ # 生效条件:layer 为假值(None/空串)时以 "knowledge" 参与判定,返回 self._in_scope(self.layers_allow, layer or "knowledge") 的结果。
115
122
  def allows_layer(self, layer: str) -> bool:
116
123
  return self._in_scope(self.layers_allow, layer or "knowledge")
117
124
 
125
+ # 生效条件:op 为假值(None/空串)时以 "" 参与,先经 strip().lower() 归一化,返回 self._in_scope(self.ops_allow, (op or "").strip().lower()) 的结果。
118
126
  def allows_op(self, op: str) -> bool:
119
127
  return self._in_scope(self.ops_allow, (op or "").strip().lower())
120
128
 
121
129
  # ---------- 强制校验(越权即 AccessDenied) ----------
122
130
 
131
+ # 生效条件:无 required 形参或模块级常量前置,当 self.expired()(来自 __init__ 的 expires_at 与当前时间比较)为 True 时抛 AccessDenied,否则无操作返回 None。
123
132
  def _require_live(self):
124
133
  if self.expired():
125
134
  raise AccessDenied(f"actor={self.actor} 令牌已过期")
126
135
 
136
+ # 生效条件:先调用 self._require_live()(过期则抛 AccessDenied);再要求 self.allows_op(op) 为 True,否则抛 AccessDenied。
127
137
  def require_op(self, op: str):
128
138
  self._require_live()
129
139
  if not self.allows_op(op):
@@ -131,6 +141,7 @@ class Principal:
131
141
  f"角色 {self.role} 无权执行 op={op}(作用域 "
132
142
  f"{list(self.ops_allow) if self.ops_allow is not None else '不限'})")
133
143
 
144
+ # 生效条件:依次要求 self._require_live() 未抛异常、self.theory_ok 为 True、self.can_write 为 True、self.allows(sensitivity) 为 True;任一不满足则抛 AccessDenied。
134
145
  def require_write(self, sensitivity: str):
135
146
  self._require_live()
136
147
  if not self.theory_ok:
@@ -143,6 +154,7 @@ class Principal:
143
154
  raise AccessDenied(
144
155
  f"写入敏感度 {sensitivity} 超出 clearance {self.clearance}")
145
156
 
157
+ # 生效条件:先要求 self.require_write(sensitivity) 未抛异常;随后将形参 layer 为假值(None/空串)时按 "knowledge" 处理,并要求 self.allows_layer(layer) 为 True,否则抛 AccessDenied。
146
158
  def require_layer_write(self, layer: str, sensitivity: str):
147
159
  """写层校验:密级 + 层白名单双闸门(核心私有内容不可越权修改)。"""
148
160
  self.require_write(sensitivity)
@@ -152,6 +164,7 @@ class Principal:
152
164
  f"角色 {self.role} 无权写入 {layer} 层"
153
165
  f"(可写层 {list(self.layers_allow) if self.layers_allow is not None else '不限'})")
154
166
 
167
+ # 生效条件:要求 self._require_live() 未抛异常、self.theory_ok 为 True、self.can_admin 为 True 时通过;否则抛 AccessDenied(消息用形参 op 标记操作)。
155
168
  def require_admin(self, op: str):
156
169
  self._require_live()
157
170
  if not self.theory_ok:
@@ -160,6 +173,7 @@ class Principal:
160
173
  if not self.can_admin:
161
174
  raise AccessDenied(f"actor={self.actor} 无管理权限({op})")
162
175
 
176
+ # 生效条件:无 required 形参或模块级常量前置,返回包含 self 各属性(tenant/actor/clearance/can_write/can_admin/session/harness/unit/role/token_id/parent/auth_mode/expires_at/theory_ok/theory_version 及 layers_allow/ops_allow)的 dict;其中 layers_allow/ops_allow 为 None 时值为 None,否则 list 化。
163
177
  def as_dict(self):
164
178
  return {"tenant": self.tenant, "actor": self.actor,
165
179
  "clearance": self.clearance, "can_write": self.can_write,
@@ -175,12 +189,14 @@ class Principal:
175
189
  "ops_allow": (None if self.ops_allow is None
176
190
  else list(self.ops_allow))}
177
191
 
192
+ # 生效条件:无前置;仅返回 tenant/actor/role/clearance/write/admin 的短摘要用于日志与排障,不含 token、密钥材料与能力白名单明细;
178
193
  def __repr__(self):
179
194
  return (f"Principal(tenant={self.tenant!r}, actor={self.actor!r}, "
180
195
  f"role={self.role!r}, clearance={self.clearance!r}, "
181
196
  f"write={self.can_write}, admin={self.can_admin})")
182
197
 
183
198
 
199
+ # 生效条件:path 为 None 时落至 os.path.expanduser("~") 下的 .mdcg/_tenants.json,path 非 None(含空串)时按传入值使用,并在构造内以 self._load() 的返回填充 self.data。
184
200
  class TenantRegistry:
185
201
  """租户注册表:tenant → {root, clearance_cap, description}。
186
202
 
@@ -188,12 +204,14 @@ class TenantRegistry:
188
204
  设计意图:私有租户的 root 指向仓库外目录,开源仓库里只放 public 租户的根。
189
205
  """
190
206
 
207
+ # 生效条件:形参 path 为 None 时取 ~/.mdcg/_tenants.json,否则取 path;self.data 初始化为 _load() 结果(self.path 经 os.path.exists 为真且 JSON 解析为 dict 时取该 dict,否则回落 {"schema":1,"tenants":{}})。
191
208
  def __init__(self, path: str = None):
192
209
  if path is None:
193
210
  path = os.path.join(os.path.expanduser("~"), ".mdcg", "_tenants.json")
194
211
  self.path = path
195
212
  self.data = self._load()
196
213
 
214
+ # 生效条件:当 self.path 经 os.path.exists 为真且内容可解析为 dict 时返回该 dict;否则(os.path.exists 为假、非 dict、JSON 解析失败或 OSError)返回 {"schema":1,"tenants":{}}。
197
215
  def _load(self):
198
216
  if os.path.exists(self.path):
199
217
  try:
@@ -205,6 +223,7 @@ class TenantRegistry:
205
223
  pass
206
224
  return {"schema": 1, "tenants": {}}
207
225
 
226
+ # 生效条件:无 required 形参或模块级常量前置,将 self.data 以 JSON 写入 self.path + ".tmp",随后 os.replace 到 self.path;目录名称为空时用 "." 创建。
208
227
  def _save(self):
209
228
  os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True)
210
229
  tmp = self.path + ".tmp"
@@ -212,6 +231,7 @@ class TenantRegistry:
212
231
  json.dump(self.data, f, ensure_ascii=False, indent=1)
213
232
  os.replace(tmp, self.path)
214
233
 
234
+ # 生效条件:形参 clearance_cap 须为模块级常量 SENSITIVITY_ORDER 成员(否则 _rank 抛 AccessDenied);形参 tenant/root 提供后写入 self.data["tenants"](要求 self.data 含 "tenants" 键,否则 KeyError),_save 成功则返回新登记项。
215
235
  def register(self, tenant: str, root: str, clearance_cap: str = DEFAULT_SENSITIVITY,
216
236
  description: str = ""):
217
237
  _rank(clearance_cap)
@@ -224,20 +244,25 @@ class TenantRegistry:
224
244
  self._save()
225
245
  return self.data["tenants"][tenant]
226
246
 
247
+ # 生效条件:self.data 含 "tenants" 键(否则 KeyError),且该映射中存在形参 tenant 时返回其值,否则返回 None(.get 缺键回落 None,键存在值为 None 也返回 None)。
227
248
  def get(self, tenant: str):
228
249
  return self.data["tenants"].get(tenant)
229
250
 
251
+ # 生效条件:self.get(tenant) 返回真值(非 None/空 dict 等)时返回 t["root"](若 t 无 "root" 键则 KeyError);返回假值时返回 None。
230
252
  def root_of(self, tenant: str):
231
253
  t = self.get(tenant)
232
254
  return t["root"] if t else None
233
255
 
256
+ # 生效条件:self.get(tenant) 返回真值时返回 t["clearance_cap"](缺键则 KeyError);返回假值时返回模块级常量 DEFAULT_SENSITIVITY。
234
257
  def cap_of(self, tenant: str):
235
258
  t = self.get(tenant)
236
259
  return t["clearance_cap"] if t else DEFAULT_SENSITIVITY
237
260
 
261
+ # 生效条件:self.data 含 "tenants" 键时返回其浅拷贝 dict(self.data["tenants"]);该键缺失时按 self.data["tenants"] 取值会 KeyError,无默认回落。
238
262
  def all(self):
239
263
  return dict(self.data["tenants"])
240
264
 
265
+ # 生效条件:cap 由 cap_of(tenant) 决定(形参 tenant 未注册时取模块级常量 DEFAULT_SENSITIVITY);clearance 为假值(含 None/空串)时 want 取 cap,否则先取 clearance,再在 _rank(want) > _rank(cap) 时夹紧为 cap;actor 为假值时取 tenant;返回 Principal(...)。
241
266
  def principal_for(self, tenant: str, actor: str = None, clearance: str = None,
242
267
  can_write: bool = True, can_admin: bool = False,
243
268
  session: str = None) -> Principal:
@@ -247,4 +272,4 @@ class TenantRegistry:
247
272
  if _rank(want) > _rank(cap):
248
273
  want = cap
249
274
  return Principal(tenant=tenant, actor=actor or tenant, clearance=want,
250
- can_write=can_write, can_admin=can_admin, session=session)
275
+ can_write=can_write, can_admin=can_admin, session=session)