@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
@@ -100,6 +100,7 @@ CONDITION_MARK = "〔来源限定〕"
100
100
 
101
101
  # ---- 赛道与来源执照 ------------------------------------------------------
102
102
 
103
+ # 生效条件:给定 fm,若显式 track/discipline_type 命中枚举则返回对应赛道;否则用相关元数据与正文 CCG 字段匹配提示词,返回 humanities/science/undetermined。
103
104
  def classify_track(fm: dict, content: str = "") -> str:
104
105
  """判定节点赛道:`humanities` / `science` / `undetermined`。
105
106
 
@@ -133,11 +134,13 @@ def classify_track(fm: dict, content: str = "") -> str:
133
134
  return "undetermined"
134
135
 
135
136
 
137
+ # 生效条件:给定 track,返回 SOURCE_POLICY 中映射的策略名;未知 track 返回空串。
136
138
  def source_policy(track: str) -> str:
137
139
  """赛道 → 来源策略名(空串表示不可判定,应 DEFER)。"""
138
140
  return SOURCE_POLICY.get(track or "", "")
139
141
 
140
142
 
143
+ # 生效条件:给定 track,若为 science 返回 REPRODUCIBLE_BASIS,若为 humanities 返回 CONSISTENCY_BASIS,否则返回 ()。
141
144
  def allowed_basis(track: str) -> tuple:
142
145
  if track == "science":
143
146
  return tuple(nodefile.REPRODUCIBLE_BASIS)
@@ -146,15 +149,18 @@ def allowed_basis(track: str) -> tuple:
146
149
  return ()
147
150
 
148
151
 
152
+ # 生效条件:给定 track 与 basis,当 basis 非空且其字符串形式属于 allowed_basis(track) 时返回 True,否则 False。
149
153
  def basis_licensed(track: str, basis) -> bool:
150
154
  """来源执照:理科要可复现证据,文科要来源一致性;赛道未定一律不发放。"""
151
155
  return bool(basis) and str(basis) in allowed_basis(track)
152
156
 
153
157
 
158
+ # 生效条件:给定 field,返回 FIELD_NORMALIZE 映射值;未知字段返回空串。
154
159
  def normalize_field(field) -> str:
155
160
  return FIELD_NORMALIZE.get(str(field or "").strip(), "")
156
161
 
157
162
 
163
+ # 生效条件:给定 v,若为 None 返回 [];否则将单值或列表转为去除空白后非空字符串的列表。
158
164
  def _as_source(v) -> list:
159
165
  if v is None:
160
166
  return []
@@ -164,6 +170,7 @@ def _as_source(v) -> list:
164
170
 
165
171
  # ---- B 型识别与条件化改写 ------------------------------------------------
166
172
 
173
+ # 生效条件:给定 text,若含 VALUATION_MARKERS 或匹配 VALUATION_PATTERNS 则返回 B_CLAIM,否则 A_CLAIM。
167
174
  def claim_type(text) -> str:
168
175
  """`A_fact`(事实性)或 `B_valuation`(评价性断言)。"""
169
176
  s = str(text or "")
@@ -176,10 +183,12 @@ def claim_type(text) -> str:
176
183
  return A_CLAIM
177
184
 
178
185
 
186
+ # 生效条件:给定 text,返回其去除首尾空白后是否以 CONDITION_MARK 开头。
179
187
  def is_conditioned(text) -> bool:
180
188
  return str(text or "").strip().startswith(CONDITION_MARK)
181
189
 
182
190
 
191
+ # 生效条件:给定 text、label、source,若 text 非空且 label 非空且 source 解析后非空,则返回带 CONDITION_MARK 的来源限定表述;已条件化原样返回;否则 None。
183
192
  def conditioned_claim(text, label, source):
184
193
  """把评价性断言改写为**带来源限定的条件表述**;缺来源/标签则返回 `None`(不写)。
185
194
 
@@ -198,6 +207,7 @@ def conditioned_claim(text, label, source):
198
207
  return f"{CONDITION_MARK}据{label}({src})的表述:{body}"
199
208
 
200
209
 
210
+ # 生效条件:给定 fm 与 content,提取 CCG 声明字段、comment 值与正文长句,返回断言列表,每项含 text/type/where/field。
201
211
  def extract_claims(fm: dict, content: str) -> list:
202
212
  """提取可核对断言:CCG 声明字段 + comment 值 + 正文长句。
203
213
 
@@ -206,6 +216,7 @@ def extract_claims(fm: dict, content: str) -> list:
206
216
  """
207
217
  out, seen = [], set()
208
218
 
219
+ # 生效条件:仅当 str(text or "").strip() 得到的 s 长度 >= 4、s 不在 seen 中、且 nodefile.is_placeholder_text(s) 为假时,把 {text: s, type: claim_type(s), where, field} 追加进 out 并把 s 加入 seen,否则直接返回(field 默认 "")。
209
220
  def _push(text, where, field=""):
210
221
  s = str(text or "").strip()
211
222
  if len(s) < 4 or s in seen or nodefile.is_placeholder_text(s):
@@ -240,6 +251,7 @@ def extract_claims(fm: dict, content: str) -> list:
240
251
  return out
241
252
 
242
253
 
254
+ # 生效条件:给定 fm、content、claim、new_text,按 claim.where 定位并在唯一匹配时替换断言返回 (content, True),否则返回 (content, False)。
243
255
  def _rewrite_claim(fm: dict, content: str, claim: dict, new_text: str):
244
256
  """节点内定位并替换一条断言 → `(content, ok)`;定位不唯一则 fail-closed 不动。"""
245
257
  where, field = claim.get("where"), claim.get("field")
@@ -269,6 +281,7 @@ def _rewrite_claim(fm: dict, content: str, claim: dict, new_text: str):
269
281
 
270
282
  # ---- 工单 ----------------------------------------------------------------
271
283
 
284
+ # 生效条件:给定 fm 与 content,返回缺失项列表:verification_basis 无效则加入该名,正文无 "# 验证方式" 行则加入该名。
272
285
  def _need(fm: dict, content: str) -> list:
273
286
  need = []
274
287
  if not nodefile.verification_basis_valid(fm):
@@ -278,6 +291,7 @@ def _need(fm: dict, content: str) -> list:
278
291
  return need
279
292
 
280
293
 
294
+ # 生效条件:给定 nid、e、fm、content,返回含 id、layer、track、claims、need、source_policy 的工单行字典。
281
295
  def _worklist_row(nid: str, e: dict, fm: dict, content: str) -> dict:
282
296
  track = classify_track(fm, content)
283
297
  return {
@@ -290,6 +304,7 @@ def _worklist_row(nid: str, e: dict, fm: dict, content: str) -> dict:
290
304
  }
291
305
 
292
306
 
307
+ # 生效条件:给定 fm 与 content,若正文或 comment 中声明的执行字段为占位文本则返回 True;未声明执行时以正文整体占位判定。
293
308
  def _is_placeholder_shell(fm: dict, content: str) -> bool:
294
309
  """空壳判定:核心可执行内容未被填充 → 禁止接线(不得把「待填充」固化成事实)。
295
310
 
@@ -305,6 +320,7 @@ def _is_placeholder_shell(fm: dict, content: str) -> bool:
305
320
  return nodefile.is_placeholder_text(content)
306
321
 
307
322
 
323
+ # 生效条件:给定 cg,逐节点按 layer/ids/prefix 过滤后产出状态为 skip(internal/denied/locked/derived/present/placeholder/unreadable 等)或 row 的扫描结果。
308
324
  def _scan(cg, layer=None, ids=None, prefix=None):
309
325
  """逐节点产出扫描结果:`{"status", "reason"?, "id", "row"?}`。
310
326
 
@@ -355,6 +371,7 @@ _SKIP_KEY = {"locked": "skipped_locked", "derived": "skipped_derived",
355
371
  "unreadable": "skipped_unreadable", "internal": "skipped_internal"}
356
372
 
357
373
 
374
+ # 生效条件:给定 x(路径或 MdCGOS),只读扫描并生成缺 verification_basis 或 "# 验证方式" 的节点工单,返回统计 rep。
358
375
  def build_worklist(x, layer=None, limit=None, ids=None, prefix=None) -> dict:
359
376
  """生成核对工单(只读):缺 `verification_basis`/`验证方式` 的节点入列。
360
377
 
@@ -418,6 +435,7 @@ _VERIFY_TEMPLATE = """你是独立**验证单元**(verify)。对下列候选
418
435
  硬约束:你只能否决(drop)或存疑(defer),**不得新增候选、不得改写 value**。"""
419
436
 
420
437
 
438
+ # 生效条件:给定 row、fm、content,用 row 的 track/source_policy/need/claims 与 fm 标题、content 前 1200 字符填充反思模板并返回字符串。
421
439
  def reflect_prompt(row: dict, fm: dict, content: str) -> str:
422
440
  claims = "\n".join(f"- [{c['type']}] {c['text']}" for c in (row.get("claims") or []))
423
441
  return _REFLECT_TEMPLATE.format(
@@ -428,6 +446,7 @@ def reflect_prompt(row: dict, fm: dict, content: str) -> str:
428
446
  body=(content or "")[:1200])
429
447
 
430
448
 
449
+ # 生效条件:给定 row 与 rows,把候选字段、值、依据、来源序列化为 JSON 并填充验证模板返回字符串。
431
450
  def verify_prompt(row: dict, rows: list) -> str:
432
451
  cands = [{"field": r.get("field"), "value": r.get("value"),
433
452
  "basis": r.get("basis"), "source": r.get("source")} for r in rows]
@@ -436,6 +455,7 @@ def verify_prompt(row: dict, rows: list) -> str:
436
455
  candidates=json.dumps(cands, ensure_ascii=False))
437
456
 
438
457
 
458
+ # 生效条件:raw 经 str(raw or "") 得 s 后,want_list 为真时先试 s 首个 "[" 至末个 "]"、再试首个 "{" 至末个 "}"(want_list 假值时只试花括号),区间可被 json.loads 解析且结果为 list 时原样返回该 list;结果为 dict 时按 rows/items/verdicts/candidates/data 顺序取首个 obj.get(key) 为 list 的 obj[key],都不满足则返回 [obj],非 list/dict 或区间缺失、解析抛 ValueError 时继续下一组括号,全部落空(含 raw 为假值使 s 为空串)返回 []。
439
459
  def _extract_json(raw, want_list=True):
440
460
  """从模型输出里抽取 JSON(容忍代码围栏与前后废话)。"""
441
461
  s = str(raw or "")
@@ -458,6 +478,7 @@ def _extract_json(raw, want_list=True):
458
478
  return []
459
479
 
460
480
 
481
+ # 生效条件:给定 item,若为 dict 则规范化 field/value/basis/source/verdict/reason 后返回字典,否则返回 {}。
461
482
  def _norm_row(item) -> dict:
462
483
  if not isinstance(item, dict):
463
484
  return {}
@@ -471,6 +492,7 @@ def _norm_row(item) -> dict:
471
492
  }
472
493
 
473
494
 
495
+ # 生效条件:给定 raw,解析 JSON 行并保留 field 为“验证方式”或“verification_basis”(统一为“验证方式”)的行,返回列表。
474
496
  def parse_reflect_rows(raw) -> list:
475
497
  out = []
476
498
  for item in _extract_json(raw, want_list=True):
@@ -482,6 +504,7 @@ def parse_reflect_rows(raw) -> list:
482
504
  return out
483
505
 
484
506
 
507
+ # 生效条件:遍历 _extract_json(raw, want_list=False)(只认花括号 JSON)的结果,仅当 item 经 _norm_row 后为真且 r["field"] 非空时产出 {field,value,verdict,reason} 四键行,否则跳过(raw 无可解析花括号对象时 out 为空列表)。
485
508
  def parse_verify_rows(raw) -> list:
486
509
  out = []
487
510
  for item in _extract_json(raw, want_list=False):
@@ -494,6 +517,7 @@ def parse_verify_rows(raw) -> list:
494
517
 
495
518
  # ---- 白箱闸门与双单元折叠 ------------------------------------------------
496
519
 
520
+ # 生效条件:逐行处理 rows,仅当 normalize_field(r.get("field")) 落在 WRITABLE_FIELDS、basis_licensed(track, r.get("basis")) 为真、r.get("source") 为真、且 r.get("value") or BASIS_TEXT.get(str(r.get("basis")), "") 非空时进入 kept(附 verdict="accept"),否则该行带对应 reason 进入 gated。
497
521
  def gate_rows(rows: list, track: str) -> tuple:
498
522
  """零模型白箱闸门:字段越界 / 来源执照不通过 / 无来源 → 一律降级为 defer。
499
523
 
@@ -520,6 +544,7 @@ def gate_rows(rows: list, track: str) -> tuple:
520
544
  return kept, gated
521
545
 
522
546
 
547
+ # 生效条件:按 (r.get("id"), normalize_field(r.get("field")) or r.get("field"), r.get("value")) 分组后,组内缺 unit==REFLECT_UNIT 或 unit==VERIFY_UNIT 的行时进 deferred,否则 verify 侧出现 verdict=="drop" 即进 dropped(veto 优先),再否则仅当 reflect 与 verify 各存在 verdict=="accept" 时才进 accepted(附 units),其余进 deferred。
523
548
  def fold_verdicts(rows: list) -> tuple:
524
549
  """把两单元裁决折叠为可落库结论 → `(accepted, deferred, dropped)`。
525
550
 
@@ -562,6 +587,7 @@ def fold_verdicts(rows: list) -> tuple:
562
587
  return accepted, deferred, dropped
563
588
 
564
589
 
590
+ # 生效条件:当 rows 中 unit==REFLECT_UNIT 与 unit==VERIFY_UNIT 的执行者经 str(x.get("actor") or "") 后存在相同的非空值(空串被 discard)时返回 True,否则返回 False。
565
591
  def detect_self_verify(rows: list) -> bool:
566
592
  """同一执行者同时充当反思与验证 = 自证(禁止)。"""
567
593
  r = {str(x.get("actor") or "") for x in rows if x.get("unit") == REFLECT_UNIT}
@@ -573,6 +599,7 @@ def detect_self_verify(rows: list) -> bool:
573
599
 
574
600
  # ---- 落库写入 ------------------------------------------------------------
575
601
 
602
+ # 生效条件:verdicts 为 None 时返回 None;verdicts 为 dict 时对每个键值把 (rs or []) 中的 dict 元素收为 {str(nid): [...]};否则遍历 verdicts or [],仅当元素为 dict 且 str(r.get("id") or "") 非空时按该 id 追加到对应列表。
576
603
  def _norm_verdicts(verdicts):
577
604
  """外部裁决(子代理落盘)→ `{id: [rows]}`。"""
578
605
  if verdicts is None:
@@ -591,6 +618,7 @@ def _norm_verdicts(verdicts):
591
618
  return out
592
619
 
593
620
 
621
+ # 生效条件:accepted 非空(取 accepted[0])时,先以 basis=str(a.get("basis") or BASIS_ENUM_DEFAULT) 与 value=str(a.get("value") or BASIS_TEXT.get(basis, "")).strip() 写「验证方式」行与 comment,之后才在 nodefile.verification_basis_valid(fm) 为真时把 basis 换成 fm.get("verification_basis")、否则把该 basis 写入 fm["verification_basis"];condition_claims 为真时仅对 row.get("claims") 中 type==B_CLAIM 且未被 is_conditioned 的条目做条件化改写,返回含 fm_before、content_hash_before 等留痕的 dict。
594
622
  def _apply_node(cg, nid, e, fm, content, accepted, row, batch, actor,
595
623
  condition_claims=True):
596
624
  """把一个节点的已接受结论写入 md,返回留痕记录(含回滚所需现场)。"""
@@ -664,6 +692,7 @@ def _apply_node(cg, nid, e, fm, content, accepted, row, batch, actor,
664
692
 
665
693
  # ---- 主流程 --------------------------------------------------------------
666
694
 
695
+ # 生效条件:reflect_fn(reflect_prompt(scan_row, fm, content)) 经 parse_reflect_rows 得到非空候选时返回 (rrows, vrows),rrows 为空则返回 ([], []);verify_fn 为 None 时 vrows 为空列表,非 None 时由 parse_verify_rows(verify_fn(verify_prompt(scan_row, rrows))) 生成、每行 value 为 c.get("value") or rrows 中同 field 的 value、再回落 ""。
667
696
  def _rows_for(scan_row, fm, content, reflect_fn, verify_fn, r_actor, v_actor):
668
697
  """调用两单元子代理,返回合并后的裁决行(reflect + verify)。"""
669
698
  prompt = reflect_prompt(scan_row, fm, content)
@@ -684,6 +713,7 @@ def _rows_for(scan_row, fm, content, reflect_fn, verify_fn, r_actor, v_actor):
684
713
  return rrows, vrows
685
714
 
686
715
 
716
+ # 生效条件:对 pre 中每个 r,str(r.get("unit") or REFLECT_UNIT).strip().lower() 等于 VERIFY_UNIT 时进 vrows,否则(含 unit 缺失回落到 REFLECT_UNIT 及任何其他取值)进 rrows,两组行均覆盖 id=nid、unit、track=track。
687
717
  def _rows_from_verdicts(nid, track, pre):
688
718
  """从外部裁决中拆出 (reflect, verify) 两组行。"""
689
719
  rrows, vrows = [], []
@@ -694,6 +724,7 @@ def _rows_from_verdicts(nid, track, pre):
694
724
  return rrows, vrows
695
725
 
696
726
 
727
+ # 生效条件:x 经 _as_cg 解析且 batch = batch or CROSSCHECK_BATCH 后逐节点扫描,裁决来源按 vmap(verdicts 归一化后非 None)→ reflect_fn 非 None → 二者皆无记 no_reflect 三条分支取行;allow_self_verify=False 时同执行者自证记 self_verify_disallowed,再经 gate_rows 闸门与 require_verify 后 fold_verdicts,仅 apply=True 才 _apply_node 写盘并在有写入时 cg.rebuild_index;limit 非 None 且已达标数 >= limit 时用 continue 跳过(非终止)。
697
728
  def crosscheck(x, layer=None, limit=None, ids=None, reflect_fn=None,
698
729
  verify_fn=None, verdicts=None, apply=False,
699
730
  batch=CROSSCHECK_BATCH, actor=None, require_verify=True,
@@ -721,9 +752,11 @@ def crosscheck(x, layer=None, limit=None, ids=None, reflect_fn=None,
721
752
  "placeholder_ids": [], "undetermined": 0,
722
753
  "reasons": {}, "samples": [], "entry_ids": []}
723
754
 
755
+ # 生效条件:无条件执行 rep["reasons"][reason] = rep["reasons"].get(reason, 0) + 1(reason 缺键时按 .get 的第二参数 0 起算),返回 None。
724
756
  def _bump(reason):
725
757
  rep["reasons"][reason] = rep["reasons"].get(reason, 0) + 1
726
758
 
759
+ # 生效条件:仅当外层 verbose 为真且 len(rep["samples"]) < 20 时把 {kind, id: nid, detail} 追加进 rep["samples"],否则不追加(已达 20 条即停止采样)。
727
760
  def _sample(kind, nid, detail=""):
728
761
  if verbose and len(rep["samples"]) < 20:
729
762
  rep["samples"].append({"kind": kind, "id": nid, "detail": detail})
@@ -828,10 +861,12 @@ def crosscheck(x, layer=None, limit=None, ids=None, reflect_fn=None,
828
861
 
829
862
  # ---- 留痕查询 / 回滚 -----------------------------------------------------
830
863
 
864
+ # 生效条件:无条件返回 os.path.join(cg.root, CROSSCHECK_LOG)(以 cg.root 与常量 CROSSCHECK_LOG 拼接,无分支)。
831
865
  def _log_path(cg) -> str:
832
866
  return os.path.join(cg.root, CROSSCHECK_LOG)
833
867
 
834
868
 
869
+ # 生效条件:box 非 dict 时返回 False;box 为 dict 且 key=="comment_verification" 时按 box.get("had") 为真则把 comment 的「验证方式」设为 box.get("value")、否则删除该键并返回 True;其他 key 时 had 为真赋 fm[key]=value、否则 fm.pop(key, None) 并返回 True。
835
870
  def _reattach(fm: dict, content: str, box: dict, key: str):
836
871
  """把 `fm_before[key]` 现场还原到 fm,返回是否发生还原。"""
837
872
  if not isinstance(box, dict):
@@ -851,6 +886,7 @@ def _reattach(fm: dict, content: str, box: dict, key: str):
851
886
  return True
852
887
 
853
888
 
889
+ # 生效条件:仅当 str(c.get("after") or "") 非空,且分别满足 where=="ccg" 且 field 真值且 _ccg_field(content, field).strip()==after.strip()(用 before 覆盖该行)、where=="comment" 且 field 真值且 comment 该 field 为含 after 的 list 或 str(v or "").strip()==after.strip()(改为 before)、where=="body" 且 after 出现在 content 中(替换首个匹配)时返回 (content, True);其余情形(含 where 为其他值、字段缺失、当前值不等于写入值)返回 (content, False)。
854
890
  def _rewind_claim(fm: dict, content: str, c: dict):
855
891
  """撤销一条条件化改写(仅当前值 == 写入值时才动)→ `(content, ok)`。"""
856
892
  where, field = c.get("where"), c.get("field")
@@ -880,6 +916,7 @@ def _rewind_claim(fm: dict, content: str, c: dict):
880
916
  return content, False
881
917
 
882
918
 
919
+ # 生效条件:x 经 _as_cg 后,对 read_jsonl(_log_path(cg)) 中 action=="crosscheck"、batch 为 None 或等于参数 batch、且 entry_ids 为假值不做 id 过滤(为真值时仅取 entry_id 在集合中的)的记录逐条处理:node 缺失或已处理则跳过,索引无该 node 或 cg._read 得 fm 为 None 或 crypto.is_encrypted(content) 为真时 skipped_drift 加一,write_id 双方非空且不等时 conflict 加一,否则撤销 claims_conditioned、在当前「验证方式」行非空且等于 rec 的 verification_value 时撤销该行、再按 fm_before 还原,reverted 为空则 conflict 加一,非空则写回节点、追加 crosscheck_rollback 日志、reverted 与 entry_ids 加一,最终 reverted 非零时 cg.rebuild_index(),返回 rep;
883
920
  def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
884
921
  """按留痕反向应用:撤销核对写入(当前值 ≠ 写入值时跳过,计入 conflict)。"""
885
922
  cg = _as_cg(x)
@@ -945,6 +982,7 @@ def rollback(x, batch=None, entry_ids=None, actor=None) -> dict:
945
982
  return rep
946
983
 
947
984
 
985
+ # 生效条件:遍历 _log_path(cg) 的记录时,action 为真值只留 rec.get("action")==action 的行、batch 为真值只留 rec.get("batch")==batch 的行;limit 非 None 且 limit>=0 时按 recs[-limit:] 截取(limit 为 0 时 [-0:] 即整表不被削减),否则保留全部;返回 {'root','total','returned','records'}。
948
986
  def history(x, limit=100, action=None, batch=None) -> dict:
949
987
  cg = _as_cg(x)
950
988
  recs = []
@@ -963,6 +1001,7 @@ def history(x, limit=100, action=None, batch=None) -> dict:
963
1001
 
964
1002
  # ---- 权限与 CLI ----------------------------------------------------------
965
1003
 
1004
+ # 生效条件:principal 为 None 时返回 False;否则仅当 principal.expired() 为假、principal.can_write 为真、且 principal.allows_layer("knowledge") 为真时返回 True,期间任一步抛 Exception 亦返回 False。
966
1005
  def can_write_knowledge(principal) -> bool:
967
1006
  """落 knowledge 层必须持有可写该层的令牌(designer 派生);否则 fail-closed。"""
968
1007
  if principal is None:
@@ -975,6 +1014,7 @@ def can_write_knowledge(principal) -> bool:
975
1014
  return False
976
1015
 
977
1016
 
1017
+ # 生效条件:path 为假值(空串/None)返回 None;path 不存在则 raise SystemExit;已存在且读取文本 strip 后为空串返回 [],非空时整段 json.loads 成功即返回该值,抛 ValueError 时按行解析(跳过空行与 "//" 开头行)返回行列表。
978
1018
  def _load_verdicts(path: str):
979
1019
  if not path:
980
1020
  return None
@@ -996,6 +1036,7 @@ def _load_verdicts(path: str):
996
1036
  return rows
997
1037
 
998
1038
 
1039
+ # 生效条件:argv(为 None 时由 argparse 读 sys.argv)解析后按 --action 分派——worklist 调 build_worklist,history 调 history(--limit 默认 None,为 None 时传 100),rollback 在 can_write_knowledge(principal) 为假时抛 SystemExit 否则调 rollback,crosscheck 在 --apply 为真且 can_write_knowledge(principal) 为假时抛 SystemExit 否则调 crosscheck;--token(默认 os.environ.get("MDCG_TOKEN") or "")为真值时先 tokens.verify_token 校验、失败抛 SystemExit;最后打印 rep 并返回 0;
999
1040
  def _cli(argv=None) -> int:
1000
1041
  ap = argparse.ArgumentParser(
1001
1042
  prog="python -m md_cg.crosscheck",
@@ -1054,5 +1095,4 @@ def _cli(argv=None) -> int:
1054
1095
 
1055
1096
 
1056
1097
  if __name__ == "__main__": # pragma: no cover
1057
- sys.exit(_cli())
1058
-
1098
+ sys.exit(_cli())
package/md_cg/crypto.py CHANGED
@@ -68,20 +68,24 @@ MASTER_FILE = os.path.join(os.path.expanduser("~"), ".mdcg", "master.key")
68
68
  SCRYPT_N, SCRYPT_R, SCRYPT_P, SCRYPT_DKLEN = 2 ** 14, 8, 1, 32
69
69
 
70
70
 
71
+ # 生效条件:不适用(无必需形参与模块级常量)
71
72
  class CryptoError(Exception):
72
73
  """加解密 / 密钥相关错误。"""
73
74
 
74
75
 
76
+ # 生效条件:不适用(无必需形参与模块级常量)
75
77
  class LockedError(CryptoError):
76
78
  """无密钥或身份不符——内容不可读(fail-closed,绝不降级为明文)。"""
77
79
 
78
80
 
79
81
  # ---- ChaCha20(RFC 8439 §2.3)--------------------------------------------
80
82
 
83
+ # 生效条件:x、n 为入参,返回 ((x << n) & 0xFFFFFFFF) | (x >> (32 - n));源码未校验 x、n 类型或范围。
81
84
  def _rotl32(x, n):
82
85
  return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))
83
86
 
84
87
 
88
+ # 生效条件:s、a、b、c、d 为入参,依次读写 s[a]、s[b]、s[c]、s[d] 并按源码顺序做加法、异或、_rotl32 更新;源码未校验 s 元素类型或索引范围。
85
89
  def _quarter_round(s, a, b, c, d):
86
90
  s[a] = (s[a] + s[b]) & 0xFFFFFFFF
87
91
  s[d] = _rotl32(s[d] ^ s[a], 16)
@@ -93,6 +97,7 @@ def _quarter_round(s, a, b, c, d):
93
97
  s[b] = _rotl32(s[b] ^ s[c], 7)
94
98
 
95
99
 
100
+ # 生效条件:key、counter、nonce 为入参,按 const + key 解包 + counter 低 32 位 + nonce 解包构造 state,执行 10 次双轮后返回 16 个 32 位小端打包的 64 字节块。
96
101
  def _chacha_block(key, counter, nonce):
97
102
  """生成 64 字节密钥流块。"""
98
103
  const = b"expand 32-byte k"
@@ -113,6 +118,7 @@ def _chacha_block(key, counter, nonce):
113
118
  return struct.pack("<16I", *[(w[i] + st[i]) & 0xFFFFFFFF for i in range(16)])
114
119
 
115
120
 
121
+ # 生效条件:key、counter、nonce、data 为入参,对 data 从 0 到 len(data) 步长 64 分块,每块用 _chacha_block(key, counter + i//64, nonce) 生成密钥流并逐字节异或,返回等长 bytes;data 为空时返回 b""。
116
122
  def _chacha20_xor(key, counter, nonce, data):
117
123
  out = bytearray(len(data))
118
124
  for i in range(0, len(data), 64):
@@ -124,6 +130,7 @@ def _chacha20_xor(key, counter, nonce, data):
124
130
 
125
131
  # ---- Poly1305(RFC 8439 §2.5)-------------------------------------------
126
132
 
133
+ # 生效条件:key、msg 为入参,取 key[:16] 掩码得 r、key[16:] 得 s,msg 按 16 字节分块加 b"\x01" 累加,返回 (acc + s) 低 128 位的 16 字节小端;源码未校验 key 长度或 msg 类型。
127
134
  def _poly1305(key, msg):
128
135
  r = int.from_bytes(key[:16], "little") & 0x0FFFFFFC0FFFFFFC0FFFFFFC0FFFFFFF
129
136
  s = int.from_bytes(key[16:], "little")
@@ -135,10 +142,12 @@ def _poly1305(key, msg):
135
142
  return ((acc + s) & ((1 << 128) - 1)).to_bytes(16, "little")
136
143
 
137
144
 
145
+ # 生效条件:b 为入参,返回 b"\x00" * ((16 - len(b) % 16) % 16);b 长度为 16 的倍数(含空)时返回 b""。
138
146
  def _pad16(b):
139
147
  return b"\x00" * ((16 - len(b) % 16) % 16)
140
148
 
141
149
 
150
+ # 生效条件:otk、aad、ct 为入参,返回 _poly1305(otk, aad + _pad16(aad) + ct + _pad16(ct) + struct.pack("<Q", len(aad)) + struct.pack("<Q", len(ct))) 的结果。
142
151
  def _aead_mac(otk, aad, ct):
143
152
  return _poly1305(otk, aad + _pad16(aad) + ct + _pad16(ct)
144
153
  + struct.pack("<Q", len(aad)) + struct.pack("<Q", len(ct)))
@@ -146,6 +155,7 @@ def _aead_mac(otk, aad, ct):
146
155
 
147
156
  # ---- AEAD:ChaCha20-Poly1305(RFC 8439 §2.8)-----------------------------
148
157
 
158
+ # 生效条件:key、nonce、plaintext、aad=b"" 为入参;len(key) != KEY_LEN 或 len(nonce) != NONCE_LEN 时抛 CryptoError;否则以 _chacha_block(key,0,nonce)[:32] 为 otk、_chacha20_xor(key,1,nonce,plaintext) 为 ct,返回 (ct, _aead_mac(otk, aad, ct))。
149
159
  def aead_encrypt(key, nonce, plaintext, aad=b""):
150
160
  """返回 (ciphertext, tag)。key=32B / nonce=12B。"""
151
161
  if len(key) != KEY_LEN:
@@ -157,6 +167,7 @@ def aead_encrypt(key, nonce, plaintext, aad=b""):
157
167
  return ct, _aead_mac(otk, aad, ct)
158
168
 
159
169
 
170
+ # 生效条件:key、nonce、ct、tag、aad=b"" 为入参;len(key) != KEY_LEN 或 len(nonce) != NONCE_LEN 时抛 CryptoError;否则算 otk,若 hmac.compare_digest(_aead_mac(otk,aad,ct), tag) 为假抛 CryptoError,为真返回 _chacha20_xor(key,1,nonce,ct)。
160
171
  def aead_decrypt(key, nonce, ct, tag, aad=b""):
161
172
  """验签后解密;失败抛 CryptoError(不返回任何明文)。"""
162
173
  if len(key) != KEY_LEN:
@@ -171,20 +182,24 @@ def aead_decrypt(key, nonce, ct, tag, aad=b""):
171
182
 
172
183
  # ---- KDF / 主密钥(KEK)--------------------------------------------------
173
184
 
185
+ # 生效条件:b 为入参,返回 base64.b64encode(b).decode("ascii") 得到的字符串。
174
186
  def _b64e(b):
175
187
  return base64.b64encode(b).decode("ascii")
176
188
 
177
189
 
190
+ # 生效条件:s 为入参,返回 base64.b64decode(str(s).encode("ascii")) 的结果;默认 validate=False,非字母字符被丢弃,可能返回空字节或部分数据,源码未校验 s 合法性。
178
191
  def _b64d(s):
179
192
  return base64.b64decode(str(s).encode("ascii"))
180
193
 
181
194
 
195
+ # 生效条件:passphrase、salt 为入参,将 str(passphrase).encode("utf-8") 作为口令、salt 作为盐,按 SCRYPT_N、SCRYPT_R、SCRYPT_P、SCRYPT_DKLEN 调 hashlib.scrypt 返回 KEK。
182
196
  def scrypt_kek(passphrase, salt):
183
197
  """口令 → KEK(scrypt)。用于「人类口令」场景,避免直接存放原始密钥。"""
184
198
  return hashlib.scrypt(str(passphrase).encode("utf-8"), salt=salt,
185
199
  n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P, dklen=SCRYPT_DKLEN)
186
200
 
187
201
 
202
+ # 生效条件:master_file=None、env_var=MASTER_ENV、create=True 为可选入参;从 os.environ.get(env_var) 读取并 strip,若 raw 非空则长度 64 走 bytes.fromhex、否则 _b64d,解码失败抛 CryptoError;否则 path = master_file or MASTER_FILE,若 os.path.exists(path) 为真则读取并 _b64d;若 create 为假返回 None;否则生成 KEY_LEN 随机密钥写入 path 并返回 key。
188
203
  def load_master_key(master_file=None, env_var=MASTER_ENV, create=True):
189
204
  """KEK 来源:环境变量 → 主密钥文件(可选自动生成);都没有返回 None。"""
190
205
  raw = (os.environ.get(env_var) or "").strip()
@@ -210,22 +225,26 @@ def load_master_key(master_file=None, env_var=MASTER_ENV, create=True):
210
225
  return key
211
226
 
212
227
 
228
+ # 生效条件:kek 为入参,返回 hashlib.sha256(b"mdcg-kek|" + kek).hexdigest()[:16];源码未校验 kek 类型。
213
229
  def kek_fingerprint(kek):
214
230
  return hashlib.sha256(b"mdcg-kek|" + kek).hexdigest()[:16]
215
231
 
216
232
 
217
233
  # ---- 身份一致性(AAD 绑定)----------------------------------------------
218
234
 
235
+ # 生效条件:tenant、actor 为入参,返回 hashlib.sha256(f"mdcg-id|v{ENVELOPE_VERSION}|{tenant}|{actor}".encode("utf-8")).hexdigest()[:16];ENVELOPE_VERSION 为模块级常量。
219
236
  def identity_fingerprint(tenant, actor):
220
237
  """身份指纹:tenant + actor 的确定性摘要(不泄露原文)。"""
221
238
  raw = f"mdcg-id|v{ENVELOPE_VERSION}|{tenant}|{actor}".encode("utf-8")
222
239
  return hashlib.sha256(raw).hexdigest()[:16]
223
240
 
224
241
 
242
+ # 生效条件:tenant、actor 为入参,返回 f"mdcg-dek|v{ENVELOPE_VERSION}|{tenant}|{actor}".encode("utf-8")。
225
243
  def _dek_aad(tenant, actor):
226
244
  return f"mdcg-dek|v{ENVELOPE_VERSION}|{tenant}|{actor}".encode("utf-8")
227
245
 
228
246
 
247
+ # 生效条件:node_id、tenant、actor 为入参,返回 f"mdcg-node|v{ENVELOPE_VERSION}|{tenant}|{actor}|{node_id}".encode("utf-8")。
229
248
  def _node_aad(node_id, tenant, actor):
230
249
  return (f"mdcg-node|v{ENVELOPE_VERSION}|{tenant}|{actor}|{node_id}"
231
250
  .encode("utf-8"))
@@ -233,10 +252,12 @@ def _node_aad(node_id, tenant, actor):
233
252
 
234
253
  # ---- 密钥库(DEK 信封)---------------------------------------------------
235
254
 
255
+ # 生效条件:root 为入参,返回 os.path.join(root, KEYS_FILE);KEYS_FILE 为模块级常量。
236
256
  def keys_path(root):
237
257
  return os.path.join(root, KEYS_FILE)
238
258
 
239
259
 
260
+ # 生效条件:root 为入参;若 os.path.exists(keys_path(root)) 为假,返回 {"v": ENVELOPE_VERSION, "alg": ALG, "envelopes": {}};否则尝试 json.load,若结果为 dict 且其 "envelopes" 为 dict 则返回该 dict;若 json 解析 ValueError 或 OSError 则返回同样的空结构。
240
261
  def _load_keys(root):
241
262
  p = keys_path(root)
242
263
  if not os.path.exists(p):
@@ -251,16 +272,19 @@ def _load_keys(root):
251
272
  return {"v": ENVELOPE_VERSION, "alg": ALG, "envelopes": {}}
252
273
 
253
274
 
275
+ # 生效条件:root、data 为入参,将 data 经 json.dumps(data, ensure_ascii=False, indent=1) 后由 atomic_write 写入 keys_path(root)。
254
276
  def _save_keys(root, data):
255
277
  from .fsutil import atomic_write
256
278
  atomic_write(keys_path(root),
257
279
  json.dumps(data, ensure_ascii=False, indent=1))
258
280
 
259
281
 
282
+ # 生效条件:tenant、actor 为入参,返回 f"{tenant}|{actor}"。
260
283
  def _envelope_key(tenant, actor):
261
284
  return f"{tenant}|{actor}"
262
285
 
263
286
 
287
+ # 生效条件:root、kek、tenant、actor、clearance="private"、rotate=False 为入参;若 kek 为假抛 LockedError;否则加载 keys,若 _envelope_key(tenant, actor) 已在 envelopes 中且 rotate 为假则返回 unwrap_dek(root, kek, tenant, actor, clearance);否则生成新 DEK 与 nonce,用 aead_encrypt(kek, nonce, dek, _dek_aad(tenant, actor)) 包裹后写入 envelopes[k] 并保存,返回 dek。
264
288
  def provision_dek(root, kek, tenant, actor, clearance="private", rotate=False):
265
289
  """为 (tenant, actor) 生成 / 取回 DEK,用 KEK 包裹后存入 `_keys.json`。
266
290
 
@@ -288,6 +312,7 @@ def provision_dek(root, kek, tenant, actor, clearance="private", rotate=False):
288
312
  return dek
289
313
 
290
314
 
315
+ # 生效条件:root、kek、tenant、actor、clearance=None 为入参;若 kek 为假抛 LockedError;否则取 _load_keys(root)["envelopes"] 中 _envelope_key(tenant, actor) 的信封,无则抛 LockedError;若 env["id_fp"] 不等于 identity_fingerprint(tenant, actor) 抛 LockedError;否则从 env["ct"] 解出 raw,按 TAG_LEN 切出 ct/tag,调 aead_decrypt;若 aead_decrypt 抛 CryptoError 则转抛 LockedError;clearance 形参默认 None 但源码未在条件中使用。
291
316
  def unwrap_dek(root, kek, tenant, actor, clearance=None):
292
317
  """解出 DEK;身份指纹不符 / KEK 不对 / 无信封 → LockedError。"""
293
318
  if not kek:
@@ -306,16 +331,19 @@ def unwrap_dek(root, kek, tenant, actor, clearance=None):
306
331
  raise LockedError(f"身份 / 主密钥不匹配:{e}") from e
307
332
 
308
333
 
334
+ # 生效条件:root、tenant、actor 为入参,返回 _envelope_key(tenant, actor) in (_load_keys(root).get("envelopes") or {}) 的布尔结果。
309
335
  def has_envelope(root, tenant, actor):
310
336
  return _envelope_key(tenant, actor) in (
311
337
  _load_keys(root).get("envelopes") or {})
312
338
 
313
339
 
340
+ # 生效条件:root、kek、tenant、actor、clearance="private" 为入参,返回 provision_dek(root, kek, tenant, actor, clearance=clearance, rotate=True)。
314
341
  def rotate_dek(root, kek, tenant, actor, clearance="private"):
315
342
  return provision_dek(root, kek, tenant, actor, clearance=clearance,
316
343
  rotate=True)
317
344
 
318
345
 
346
+ # 生效条件:root 为入参,遍历 _load_keys(root).get("envelopes") or {} 的每项,按 "|" partition 出 tenant/actor,收集 id_fp、clearance、created_at 后返回列表;无信封时返回 []。
319
347
  def envelopes(root):
320
348
  """信封清单(不含密钥材料):供运维审计「谁被签发了密钥」。"""
321
349
  out = []
@@ -329,10 +357,12 @@ def envelopes(root):
329
357
 
330
358
  # ---- 节点正文封装 --------------------------------------------------------
331
359
 
360
+ # 生效条件:content 为入参;bool(content) 为假(如空串或 None)时返回 False;否则返回 content.lstrip().startswith(ENC_PREFIX)。
332
361
  def is_encrypted(content):
333
362
  return bool(content) and content.lstrip().startswith(ENC_PREFIX)
334
363
 
335
364
 
365
+ # 生效条件:content、dek、node_id、tenant、actor 为入参,生成 NONCE_LEN 随机 nonce,将 str(content).encode("utf-8") 以 aead_encrypt(dek, nonce, ..., _node_aad(node_id, tenant, actor)) 加密,返回 f"{ENC_PREFIX}{_b64e(nonce + tag + ct)}{ENC_SUFFIX}"。
336
366
  def seal_node(content, dek, node_id, tenant, actor):
337
367
  """明文 → 密文标记块(正文整体加密;frontmatter 不在此处处理)。"""
338
368
  nonce = secrets.token_bytes(NONCE_LEN)
@@ -341,6 +371,7 @@ def seal_node(content, dek, node_id, tenant, actor):
341
371
  return f"{ENC_PREFIX}{_b64e(nonce + tag + ct)}{ENC_SUFFIX}"
342
372
 
343
373
 
374
+ # 生效条件:content、dek、node_id、tenant、actor 为入参;若 is_encrypted(content) 为假则原样返回 content;否则 strip 后切掉 ENC_PREFIX/ENC_SUFFIX,base64 解出 raw,按 NONCE_LEN、TAG_LEN 切出 nonce/tag/ct,调 aead_decrypt(dek, nonce, ct, tag, _node_aad(node_id, tenant, actor)) 并 utf-8 解码返回;aead_decrypt 失败抛 CryptoError。
344
375
  def open_node(content, dek, node_id, tenant, actor):
345
376
  """密文标记块 → 明文;未加密原样返回;失败抛 CryptoError。"""
346
377
  if not is_encrypted(content):
@@ -357,6 +388,7 @@ def open_node(content, dek, node_id, tenant, actor):
357
388
 
358
389
  # ---- 审计(payload-free)-------------------------------------------------
359
390
 
391
+ # 生效条件:root、rec 为入参,复制 rec,若未提供 ts 则设 time.time(),设 payload_free=True,尝试 append_jsonl(os.path.join(root, AUDIT_FILE), rec);OSError 时静默忽略。
360
392
  def audit(root, rec):
361
393
  from .fsutil import append_jsonl
362
394
  rec = dict(rec)
@@ -368,6 +400,7 @@ def audit(root, rec):
368
400
  pass
369
401
 
370
402
 
403
+ # 生效条件:root 为入参,返回 list(read_jsonl(os.path.join(root, AUDIT_FILE))) 得到的记录列表。
371
404
  def audit_records(root):
372
405
  from .fsutil import read_jsonl
373
406
  return list(read_jsonl(os.path.join(root, AUDIT_FILE)))
@@ -375,6 +408,7 @@ def audit_records(root):
375
408
 
376
409
  # ---- 自描述 --------------------------------------------------------------
377
410
 
411
+ # 生效条件:无入参,返回包含 module="crypto"、ALG、ENCRYPTED_LEVELS、MASTER_ENV、KEYS_FILE 等模块级常量的自描述字典。
378
412
  def catalog():
379
413
  """自描述:加密范围、密钥层级、身份一致性、威胁模型(供 MCP 对照)。"""
380
414
  return {
@@ -401,4 +435,4 @@ def catalog():
401
435
  "not_covered": ["本地内存取证", "侧信道(纯 Python 实现的固有限制)"],
402
436
  },
403
437
  "fail_closed": "无 KEK 时拒绝写入 private/secret,不静默降级为明文",
404
- }
438
+ }