@furongjun1999/dsh-memory 0.5.1 → 0.6.1

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 (187) hide show
  1. package/README.md +588 -552
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/discipline/harnesses.yaml +31 -0
  5. package/docs/discipline/templates/full.md.tmpl +1 -1
  6. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  7. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
  8. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
  9. package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
  10. package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
  11. package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
  12. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
  13. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
  14. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
  15. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
  16. package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
  17. package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
  18. package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
  19. package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
  20. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
  21. package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
  22. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v18.md +183 -0
  23. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v19.md +207 -0
  24. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
  25. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
  26. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
  27. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
  28. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
  29. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
  30. 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 +20 -0
  31. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
  32. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
  33. 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 +40 -40
  34. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
  35. 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 +7 -2
  36. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
  37. package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
  38. package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
  39. package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
  40. 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 +460 -460
  41. package/dsh/cordis-patch-profile-web.example.yml +35 -0
  42. package/lib/bridge.js +24 -2
  43. package/lib/cli.d.ts +3 -0
  44. package/lib/cli.js +66 -0
  45. package/lib/hooks.d.ts +31 -2
  46. package/lib/hooks.js +219 -16
  47. package/lib/init.d.ts +88 -0
  48. package/lib/init.js +302 -0
  49. package/lib/lib/prompt_safety.d.ts +52 -1
  50. package/lib/lib/prompt_safety.js +76 -1
  51. package/md_cg/audit.py +652 -379
  52. package/md_cg/bench6_arms.py +1 -1
  53. package/md_cg/bench_p0.py +1 -1
  54. package/md_cg/branches.py +2 -2
  55. package/md_cg/ccgc.py +1071 -1006
  56. package/md_cg/chain.py +1 -1
  57. package/md_cg/consistency.py +97 -9
  58. package/md_cg/consolidate.py +34 -9
  59. package/md_cg/crypto.py +59 -26
  60. package/md_cg/docindex.py +16 -1
  61. package/md_cg/evidence.py +585 -582
  62. package/md_cg/evolution.py +17 -1
  63. package/md_cg/export.py +1 -1
  64. package/md_cg/forgetting.py +309 -13
  65. package/md_cg/fsutil.py +454 -8
  66. package/md_cg/identity.py +3 -3
  67. package/md_cg/insight.py +24 -3
  68. package/md_cg/linkref.py +1 -1
  69. package/md_cg/logref.py +327 -0
  70. package/md_cg/mcp_server.py +4152 -3818
  71. package/md_cg/mdcg.py +4208 -3462
  72. package/md_cg/mdcos.py +4491 -4137
  73. package/md_cg/mreview/pipeline.py +15 -1
  74. package/md_cg/nodefile.py +639 -575
  75. package/md_cg/protect.py +158 -11
  76. package/md_cg/protocol.py +41 -2
  77. package/md_cg/provenance.py +22 -4
  78. package/md_cg/reach.py +4 -4
  79. package/md_cg/readcache.py +76 -15
  80. package/md_cg/reconcile.py +1 -1
  81. package/md_cg/refindex.py +246 -37
  82. package/md_cg/refine.py +1 -1
  83. package/md_cg/review_cli.py +49 -4
  84. package/md_cg/routing.py +32 -5
  85. package/md_cg/run_tests.py +17 -0
  86. package/md_cg/scrub.py +30 -3
  87. package/md_cg/security.py +47 -1
  88. package/md_cg/self_state.py +5 -5
  89. package/md_cg/sources.py +23 -6
  90. package/md_cg/srcindex.py +352 -0
  91. package/md_cg/stg.py +47 -3
  92. package/md_cg/subgraph.py +2 -2
  93. package/md_cg/sustain.py +1299 -1168
  94. package/md_cg/tasks.py +469 -470
  95. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  96. package/md_cg/test_b1b2_write_face.py +417 -0
  97. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  98. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  99. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  100. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  101. package/md_cg/test_c3_transient_read_negative.py +793 -0
  102. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  103. package/md_cg/test_ccg_form_parity.py +188 -0
  104. package/md_cg/test_ccgc.py +28 -3
  105. package/md_cg/test_govern_directread.py +17 -4
  106. package/md_cg/test_h2_session_view_norm.py +233 -0
  107. package/md_cg/test_h4_sustain_snapshot.py +1377 -0
  108. package/md_cg/test_hive_ingest.py +285 -285
  109. package/md_cg/test_index_crossprocess_reload.py +301 -0
  110. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  111. package/md_cg/test_issue43_default_policy.py +865 -0
  112. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  113. package/md_cg/test_linkref.py +10 -3
  114. package/md_cg/test_lock.py +2 -2
  115. package/md_cg/test_logref.py +1109 -0
  116. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  117. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  118. package/md_cg/test_mr_m2.py +15 -3
  119. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  120. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  121. package/md_cg/test_n176_link_trust.py +221 -0
  122. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  123. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  124. package/md_cg/test_n195_writepath_reload.py +283 -0
  125. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  126. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  127. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  128. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  129. package/md_cg/test_n201_proposal_visibility.py +382 -0
  130. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  131. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  132. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  133. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  134. package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
  135. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  136. package/md_cg/test_n225_nonobject_load.py +1110 -0
  137. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  138. package/md_cg/test_neg_condition_hits.py +333 -0
  139. package/md_cg/test_neg_tail_honesty.py +663 -0
  140. package/md_cg/test_none_id_write_guard.py +166 -0
  141. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  142. package/md_cg/test_p1.py +5 -5
  143. package/md_cg/test_p11_consistency.py +161 -12
  144. package/md_cg/test_p26_refindex.py +2 -2
  145. package/md_cg/test_p27_docindex.py +777 -774
  146. package/md_cg/test_p28_refcheck.py +740 -13
  147. package/md_cg/test_p29_session_ingest_export.py +2 -2
  148. package/md_cg/test_p2_mcp.py +12 -1
  149. package/md_cg/test_p30_maintain.py +33 -2
  150. package/md_cg/test_p31_insight.py +1 -0
  151. package/md_cg/test_p9c_dedup_hints.py +276 -0
  152. package/md_cg/test_policy_required_ccg.py +426 -0
  153. package/md_cg/test_protocol.py +22 -0
  154. package/md_cg/test_rank_parity_score_mode.py +516 -0
  155. package/md_cg/test_read_face_input_gates.py +363 -0
  156. package/md_cg/test_read_face_semantics.py +489 -0
  157. package/md_cg/test_recall_face_guards.py +812 -0
  158. package/md_cg/test_rejected_credential_forms.py +188 -0
  159. package/md_cg/test_rejected_redact.py +146 -0
  160. package/md_cg/test_retr_s1b.py +2 -2
  161. package/md_cg/test_retr_s5.py +20 -7
  162. package/md_cg/test_review_cli_attribution.py +177 -0
  163. package/md_cg/test_review_cli_visibility.py +235 -0
  164. package/md_cg/test_review_conformance.py +21 -3
  165. package/md_cg/test_security_audit_v21.py +2 -2
  166. package/md_cg/test_server_version.py +67 -0
  167. package/md_cg/test_srcindex.py +171 -0
  168. package/md_cg/test_tenant_env_override_warn.py +63 -50
  169. package/md_cg/test_token_lowercase_form.py +315 -0
  170. package/md_cg/test_v21r1_package_version.py +310 -0
  171. package/md_cg/test_writepipe.py +10 -4
  172. package/md_cg/tokens.py +225 -95
  173. package/md_cg/tool_face.py +4 -4
  174. package/md_cg/trust.py +49 -5
  175. package/md_cg/units.py +204 -6
  176. package/md_cg/weights.py +4 -4
  177. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  178. package/md_cg/writelimit.py +42 -8
  179. package/md_cg/writepipe.py +651 -554
  180. package/package.json +8 -2
  181. package/skills/plugin.json +1 -1
  182. package/src/bridge.ts +25 -2
  183. package/src/cli.ts +65 -0
  184. package/src/hooks.ts +229 -16
  185. package/src/init.ts +361 -0
  186. package/src/lib/prompt_safety.ts +88 -1
  187. package/zcode/AGENTS.md +10 -10
package/md_cg/routing.py CHANGED
@@ -269,27 +269,54 @@ def domain_similarity(a: str, b: str) -> float:
269
269
  return len(ga & gb) / len(ga | gb)
270
270
 
271
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。
273
- def bucket_health(counts: dict) -> dict:
272
+ # 生效条件:counts 各值之和为 0(含空 dict 与全 0 计数)时——total_nodes 为假值(缺省 None 或 <=0)返回 {'ok': True, 'reason': 'empty', 'buckets': 0}(与加 total_nodes 参数之前逐位一致),total_nodes > 0 返回 {'ok': False, 'reason': 'empty_buckets', 'buckets': 0, 'nodes': total_nodes, 'problems': [...]};否则在「桶数 >1 且最大桶占比 >30%」、「桶数 >1 且单例桶占比 >50%」、「桶数 ==1(单桶,条件路由不可用)」、桶数 >1 时「期望扫描 >30%」中命中的项写入 problems,返回含 ok(=problems 为空)、buckets、nodes、max_bucket_share、singleton_ratio、expected_scan、problems 的 dict。
273
+ def bucket_health(counts: dict, total_nodes: int = None) -> dict:
274
274
  """分区健康度自检。分桶键一旦退化(巨桶或碎片化),条件路由就是纸面收益,
275
275
  必须在写入侧就能发现,而不是等召回变差才回头查。
276
276
 
277
277
  counts: {bucket_dir: node_count}
278
+ total_nodes: 调用方(`MdCG.health`)持有的**库内节点总数**,唯一用途是把
279
+ 「真空库(无节点、无桶)」与「有节点但分桶表为空(分区信息缺失)」分开。
280
+ **缺省 None ⇒ 不做该比对**:空 counts 的返回与加本参数之前逐位一致,
281
+ 故既有调用方(test_p0 / bench_axis_domain / bench6_arms,只传 counts)
282
+ 行为一字不变——M3③ 的向后兼容面。
283
+
284
+ 单桶(nb == 1)是「从未分区」而不是「分区退化」:巨桶判据(top/n == 1.0)与
285
+ 期望扫描判据(同样恒 1.0)在 nb==1 时必触发且互为同一事实,此前把这类库报成
286
+ 「巨桶 / 分区失效」是假话(M3①②)。现只给一条如实读数——条件路由本就不可用;
287
+ ok 仍为 False(单桶库的一次查询确实是全量扫描,判据未放宽)。
278
288
  """
279
289
  n = sum(counts.values())
290
+ nb = len(counts)
280
291
  if not n:
292
+ # 库里有节点却零个桶 ⇒ 分桶表与节点面不一致(分区信息缺失),
293
+ # 不能报 ok=True 冒充健康(M3③)。total_nodes 缺省时不作此判断。
294
+ if total_nodes and total_nodes > 0:
295
+ return {
296
+ "ok": False,
297
+ "reason": "empty_buckets",
298
+ "buckets": 0,
299
+ "nodes": int(total_nodes),
300
+ "problems": [f"分桶表为空但库内有 {int(total_nodes)} 个节点:"
301
+ "分区信息缺失,条件路由不可用(非真空库)"],
302
+ }
281
303
  return {"ok": True, "reason": "empty", "buckets": 0}
282
- nb = len(counts)
283
304
  top = max(counts.values())
284
305
  singles = sum(1 for v in counts.values() if v == 1)
285
306
  # 期望扫描占比:随机取一节点的情境去路由,命中桶的期望大小占全库比例
286
307
  expected_scan = sum(v * v for v in counts.values()) / n / n
287
308
  problems = []
288
- if top / n > 0.30:
309
+ if nb > 1 and top / n > 0.30:
289
310
  problems.append(f"巨桶:最大桶占 {top / n:.1%}(>30% 视为分区失效)")
290
311
  if nb > 1 and singles / nb > 0.50:
291
312
  problems.append(f"碎片化:单例桶占 {singles / nb:.1%}(>50% 说明键含实例级字段)")
292
- if expected_scan > 0.30:
313
+ if nb == 1:
314
+ # 巨桶/期望扫描两条在 nb==1 时只是同一事实的两种写法,合并为一条如实读数。
315
+ # expected_scan 的**数值**仍在返回 dict 里(下游读的是数:test_p0 :92、
316
+ # bench_axis_domain :159),只有文案不再把「未分区」说成「巨桶退化」。
317
+ problems.append(f"单桶:全库仅 1 个分桶({n} 个节点同桶),"
318
+ "条件路由本就不可用(从未分区,非分区退化)")
319
+ elif expected_scan > 0.30:
293
320
  problems.append(f"路由无效:期望扫描 {expected_scan:.1%}(接近全量)")
294
321
  return {
295
322
  "ok": not problems,
@@ -38,6 +38,23 @@ import sys
38
38
  import tempfile
39
39
  import time
40
40
 
41
+ # ---------------------------------------------------------------- 入口自保证 UTF-8
42
+ # 约束(工作纪律第 15 条):本调用必须在**任何文件/库 I/O 之前**——utf8_boot.ensure_utf8
43
+ # 在解释器未开 UTF-8 模式时以相同 argv 重启自身(-X utf8),早于它的任何 open/stdio
44
+ # 读写都走 locale 编码(Windows 中文机 = cp936:裸 open 抛 UnicodeDecodeError、中文写
45
+ # 落 GBK 字节)。本文件是包内全量入口(安装态下 README 的门禁命令),必须最先保证。
46
+ # 仓库根入 sys.path 的形态照 hive/exec.py::_md_cg_import 的最小写法(助手在仓根,
47
+ # 不是 md_cg 包目录)。
48
+ # 被 import(本模块非 __main__)时助手只置子进程继承面、绝不重启/退出——F6:静默重启
49
+ # 会吞掉调用方输入。
50
+ _UTF8_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
51
+ if _UTF8_ROOT not in sys.path:
52
+ sys.path.insert(0, _UTF8_ROOT)
53
+ from utf8_boot import ensure_utf8 # noqa: E402
54
+
55
+ ensure_utf8(__file__)
56
+
57
+
41
58
  _REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
59
  #: 逐用例日志目录(重定向目标;默认临时目录,避免污染仓)
43
60
  _LOG_DIR = os.environ.get("MDCG_TESTLOG_DIR") or os.path.join(
package/md_cg/scrub.py CHANGED
@@ -715,8 +715,8 @@ def decontaminate(cg, node_ids=None, *, kinds=None, dry_run: bool = True,
715
715
  # ④ 校准偏差
716
716
  # --------------------------------------------------------------------------
717
717
 
718
- # 生效条件:对 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 的节点。
719
- def _apply_offset(cg, offset, *, override=False, min_evidence=1):
718
+ # 生效条件:对 cg 中每个「layer 不在 SELF_LAYERS」且 evidence_count≥int(min_evidence)(min_evidence=0 时该比较恒假而不早退)的节点,若 protect.is_protected 为假或 override 为真,且 cg.get(nid) 未抛异常并返回真值节点,则先以节点真层/敏感度过 protect.guard_overwrite(层闸 + 保护闸,override 为真时先快照 + 审计,越权抛 AccessDenied 且整批中止),再取 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,成功后置 cg._dirty[nid]=e(**标脏**:推进读缓存代际与索引增量日志)并 adjusted+1(写回异常被吞掉不计数),返回 (adjusted, skipped),其中 skipped 只累计「layer 属 SELF_LAYERS」或被 protect 拦下且非 override 的节点。
719
+ def _apply_offset(cg, offset, *, override=False, min_evidence=1, actor=None):
720
720
  nodes = _nodes(cg)
721
721
  adjusted = skipped = 0
722
722
  for nid, e in nodes.items():
@@ -736,6 +736,20 @@ def _apply_offset(cg, offset, *, override=False, min_evidence=1):
736
736
  if not node:
737
737
  continue
738
738
  fm = node.get("frontmatter") or {}
739
+ # N222(2026-09-28,同族未接线写点):本函数是 calibrate 落库动作,此前
740
+ # 直调 `cg._write_node` 改写**全库** `layer∉(self,anchor)` 且
741
+ # `evidence_count≥1` 节点的 confidence/calibration——不过 principal 层写
742
+ # 白名单、不过引擎级保护闸、`override` 形参直通(含 importance≥0.7 的
743
+ # 不可遗忘节点),无快照无审计。持 sustain 令牌(`tokens.py:190`
744
+ # `layers_allow=('self',)`)即可改 knowledge 层节点置信度(实测
745
+ # `n_adjusted=5`,0.8→0.95;`override=True` 再 +1)。落盘前统一过
746
+ # `protect.guard_overwrite`:层/敏感度取**节点 fm 真值**(不信索引缓存),
747
+ # 解析与层闸由 `protect.require_layer` 单点承担,越权即 AccessDenied
748
+ # 整批中止(fail-closed,不静默跳过——否则越权面被伪装成「无可校准节点」);
749
+ # `override=True` 走同一闸的显式放行分支(快照 + `_protected_audit.jsonl`)。
750
+ protect.guard_overwrite(cg, nid, layer=fm.get("layer"),
751
+ sensitivity=fm.get("sensitivity"),
752
+ override=override, actor=actor)
739
753
  old = float(fm.get("confidence", 0.6))
740
754
  new = round(max(0.0, min(0.99, old + float(offset))), 4)
741
755
  if abs(new - old) < 1e-9:
@@ -746,6 +760,18 @@ def _apply_offset(cg, offset, *, override=False, min_evidence=1):
746
760
  try:
747
761
  cg._write_node(nid, os.path.join(cg.root, e["path"]), fm,
748
762
  node.get("content") or "")
763
+ # 标脏(N133 修复,对照先例 md_cg/mdcg.py 的 update_tags / verify
764
+ # 直写分支 `self._dirty[node_id] = e`):本函数原先写盘后**完全不
765
+ # 碰索引**(条目字段集不含 confidence,无需同步),但正因如此
766
+ # path_gen 不推进 ⇒ 读缓存(默认开)把写盘前的旧 fm 判新鲜——
767
+ # 同进程「calibrate 后读」拿旧 confidence(其余两处同族写点
768
+ # forgetting.reinforce / insight.verify 同批补标脏)。标脏同时
769
+ # 让本写进 `_dirty → flush → _index_log` 重放。**不下沉进
770
+ # `_write_node`**:该口另有「只对账索引不落盘」的调用方,下沉会
771
+ # 凭空产生写入代际与自重载。
772
+ _dirty = getattr(cg, "_dirty", None)
773
+ if isinstance(_dirty, dict):
774
+ _dirty[nid] = e
749
775
  adjusted += 1
750
776
  except Exception: # noqa: BLE001
751
777
  pass
@@ -777,7 +803,8 @@ def calibrate(cg, *, apply: bool = False, override: bool = False, actor=None,
777
803
  adjusted = skipped = 0
778
804
  if apply and abs(offset) > 1e-9:
779
805
  adjusted, skipped = _apply_offset(cg, offset, override=override,
780
- min_evidence=min_evidence)
806
+ min_evidence=min_evidence,
807
+ actor=actor)
781
808
  _log(cg, "calibrate", ok=True, offset=offset, gap=gap,
782
809
  adjusted=adjusted, actor=actor)
783
810
  blind = 0
package/md_cg/security.py CHANGED
@@ -382,4 +382,50 @@ def can_read_restricted(p) -> bool:
382
382
  return False
383
383
  if getattr(p, "can_admin", False):
384
384
  return True
385
- return getattr(p, "role", "") in _RESTRICTED_READER_ROLES
385
+ return getattr(p, "role", "") in _RESTRICTED_READER_ROLES
386
+
387
+
388
+ # --------------------------------------------------------------------------
389
+ # 读可见性可选钩子(跨层唯一实现,批次 68)
390
+ # --------------------------------------------------------------------------
391
+ # 为什么放在本模块:可见性单点 `_readable` 定义在**子类** `MdCGSecure`
392
+ # (mdcos.py:4028),而需要它的读数出口散在**下层/旁路模块**——trust(验证态
393
+ # 台账/describe)、provenance(派生边端点摘要与拓扑)、evolution(演化账本
394
+ # 自由文本)、forgetting(遗忘留痕 jsonl)。这些模块若 `from .mdcos import` 会
395
+ # 成环(mdcos 反向导入它们),故单点放在两侧都依赖的 security.py:
396
+ # · `visible_to(cg, entry)` —— 条目级判定(口径 = `_readable`);
397
+ # · `node_visible(cg, node_id)` —— 节点 id 级判定(索引取条目 + 缺席处置)。
398
+ # 口径与仓内既有三处可选钩子逐条一致:`mdcos._note_visible`(及本函数的
399
+ # 委派)、`backfill._readable_guard`、`linkref._known_ids`、`stg._scan/_preview`
400
+ # ——「无钩子(纯 MdCGOS,无身份/密级模型)= 不限制;有钩子 = 按判据;
401
+ # 判据异常 = 不可见(fail-closed);宁可少读,不可 fail-open 泄漏」。
402
+ # 生效条件:cg 有可调用的 _readable 时返回 bool(判定结果),判定抛异常返回 False;无该属性/不可调用(纯 MdCGOS)时返回 True。
403
+ def visible_to(cg, entry) -> bool:
404
+ """条目级读可见性(cg 无 `_readable` 钩子时不设限)。"""
405
+ fn = getattr(cg, "_readable", None)
406
+ if not callable(fn):
407
+ return True
408
+ try:
409
+ return bool(fn(entry))
410
+ except Exception: # noqa: BLE001 —— 判据异常=不可见
411
+ return False
412
+
413
+
414
+ # 生效条件:cg 无可调用 _readable 时返回 True;有钩子且索引条目为 dict 时返回 bool(visible_to(cg, 条目))(判定异常 False);索引无该条目/条目非 dict 时返回 bool(principal.can_admin)(有身份模型但不可证可见→fail-closed,设计者不受限,否则删除后的治理留痕反被清空)。
415
+ def node_visible(cg, node_id) -> bool:
416
+ """节点 id 级读可见性(索引取条目;**缺席 = 不可证可见 → fail-closed**)。
417
+
418
+ 缺席的两种实情:① 节点从未存在/拼错 id;② 节点已被 forget(索引无条目)。
419
+ 有身份模型时两者都不得被「当作可见」——否则 `_forgetting.jsonl` /
420
+ `trust` 台账 / 派生边摘要就成了「已删内容」的旁路读出(N205/N226/N229)。
421
+ 例外只给 can_admin(设计者=全局观测与治理本职):否则删除后的裁决留痕
422
+ 对唯一需要它的人也不可见(与 `_readable` 的 can_admin 豁免同款)。
423
+ """
424
+ fn = getattr(cg, "_readable", None)
425
+ if not callable(fn):
426
+ return True # 无身份模型:无「越权」可言
427
+ e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
428
+ if isinstance(e, dict):
429
+ return visible_to(cg, e)
430
+ p = getattr(cg, "principal", None)
431
+ return bool(p is not None and getattr(p, "can_admin", False))
@@ -355,7 +355,7 @@ def _relation_counts(cg, subject):
355
355
  slug = _slug(subject)
356
356
  nodes = ((getattr(cg, "index", None) or {}).get("nodes") or {})
357
357
  out = inn = 0
358
- for nid, e in nodes.items():
358
+ for nid, e in list(nodes.items()):
359
359
  tags = set(e.get("tags") or [])
360
360
  if TAG_RELATION not in tags and not nid.startswith(RELATION_PREFIX):
361
361
  continue
@@ -656,7 +656,7 @@ def relations(cg, subject=None, direction="both"):
656
656
  nodes = ((getattr(cg, "index", None) or {}).get("nodes") or {})
657
657
  slug = _slug(subject) if subject else None
658
658
  out = []
659
- for nid, e in nodes.items():
659
+ for nid, e in list(nodes.items()):
660
660
  tags = set(e.get("tags") or [])
661
661
  if TAG_RELATION not in tags and not nid.startswith(RELATION_PREFIX):
662
662
  continue
@@ -692,7 +692,7 @@ def index(cg, dim, value, limit=50, with_content=False):
692
692
  "allowed": list(DIMENSIONS)}
693
693
  tag = dim_tag(d, value)
694
694
  nodes = ((getattr(cg, "index", None) or {}).get("nodes") or {})
695
- hits = [(nid, e) for nid, e in nodes.items()
695
+ hits = [(nid, e) for nid, e in list(nodes.items())
696
696
  if tag in (e.get("tags") or [])]
697
697
  hits.sort(key=lambda kv: (-float(kv[1].get("importance") or 0),
698
698
  -float(kv[1].get("created_at") or 0),
@@ -740,7 +740,7 @@ def audit(cg, subject=DEFAULT_SUBJECT, window=RECENT_WINDOW):
740
740
 
741
741
  # 1 单例:同 subject 的状态卡只能有一张
742
742
  slug = _slug(subject)
743
- cards = [n for n, e in nodes.items()
743
+ cards = [n for n, e in list(nodes.items())
744
744
  if TAG_STATE in (e.get("tags") or [])
745
745
  and f"state_subject:{slug}" in (e.get("tags") or [])]
746
746
  if len(cards) > 1:
@@ -878,7 +878,7 @@ def audit(cg, subject=DEFAULT_SUBJECT, window=RECENT_WINDOW):
878
878
  continue
879
879
  for v in (vals or []):
880
880
  tag = dim_tag(d, v)
881
- holders = [n for n, e in nodes.items()
881
+ holders = [n for n, e in list(nodes.items())
882
882
  if tag in (e.get("tags") or [])]
883
883
  if not holders:
884
884
  issues.append(_issue("dimension_orphan", "warn",
package/md_cg/sources.py CHANGED
@@ -273,8 +273,17 @@ class HiveJobsSource(Source):
273
273
  result.json 终态 → **权威确认事件**(progress 可能因强杀缺失 final):
274
274
  done → 「任务完成(job=…)」;error/timeout/killed → 「任务失败(job=…)」
275
275
 
276
- 排序:job_id 名升序 = 时间升序(job.rs 命名保证 h<unix_ms>_<pid>),
277
- 跨 job 全序成立;seq 由本源按枚举顺序递增(ingest 去重键 = (session, seq))。
276
+ 排序:**job_id 名升序**(`sorted(listdir)`,确定性),**不是**时间序——
277
+ 契约 v2(`docs/plans/全中文编码与蜂巢任务标识契约_v2.0.md` §四.5)起 id 改为
278
+ 语义四槽 `h_<身份>_<任务>_<单元>_<编号>`,名序不再等于提交时序:旧形态
279
+ `h<unix_ms>_<pid>` 才是「名序 = 时间序」的**巧合代理**。据实订正(C2:本仓
280
+ 禁止把代理当结构保证)——本源的时序由来源行自带的 ts 承载,排序只求
281
+ **跨 job 全序且确定**(seq 由本源按枚举顺序递增,ingest 去重键 = (session, seq),
282
+ 名序的确定性保证该键跨轮稳定)。
283
+ 诚实边界(**已发现、未在本批动码**):本源的 `_jobs()` 若需要**真时间序**,
284
+ 应改走与 Rust 侧同判据的 created_ts 真值(`hive/src/job.rs::list_jobs_by_created`
285
+ 的语义,Python 侧对应 `mcp_server._list_jobs_by_created`)——契约 §四.5 的消费者
286
+ 清单未列本源,且改动牵动 md_cg 摄入链语义与其守卫,故留待单独裁决,不静默改。
278
287
  解析失败的行/条目计入 self.skipped,不终杀批次。
279
288
  """
280
289
 
@@ -626,11 +635,14 @@ class FileDispatcher:
626
635
 
627
636
  # ---- stat:看水位与支持面 ----
628
637
 
629
- # 生效条件:from . import refindex 与 refindex.Ledger(self.cg.root).stat() 均不抛异常时返回该 stat 结果,抛任何异常时返回 {}。
638
+ # 生效条件:from . import refindex 与 refindex.Ledger(self.cg.root).summary() 均不抛异常时返回该 summary 结果,抛任何异常时返回 {}。
630
639
  def _ledger_stat(self):
640
+ # `Ledger` 只有 `summary()`、**没有** `stat()`:此前写 `.stat()` 撞
641
+ # AttributeError 被下面的 except 吞成 `{}`,于是 `ingest stat` 的水位面
642
+ # 永远是空且无人知晓(唯一实现面是 `refindex.Ledger.summary`)。
631
643
  try:
632
644
  from . import refindex
633
- return refindex.Ledger(self.cg.root).stat()
645
+ return refindex.Ledger(self.cg.root).summary()
634
646
  except Exception: # noqa: BLE001
635
647
  return {}
636
648
 
@@ -702,7 +714,7 @@ class FileDispatcher:
702
714
  "counts": counts,
703
715
  "note": "预演:仅统计各链文件数,未做任何写入"}
704
716
 
705
- # 生效条件:root 非目录时返回 ok=False 的「目录不存在」;dry_run 为真时返回 _dry_dir(root);否则对 doc_ref/code_ref 两链各以 patterns/max_files/max_items/incremental/ledger 调 refindex.index_dir 与 add_items,并把 root 下 **/*.jsonl 前 max_files 个逐个 ingest_jsonl 后返回 out。
717
+ # 生效条件:root 非目录时返回 ok=False 的「目录不存在」;dry_run 为真时返回 _dry_dir(root);否则对 doc_ref/code_ref 两链各以 patterns/max_files/max_items/incremental/ledger 调 refindex.index_dir(commit=False) 与 add_items 后 ledger.save(),并把 root 下 **/*.jsonl 前 max_files 个逐个 ingest_jsonl 后返回 out。
706
718
  def ingest_dir(self, root, layer=None, sensitivity=None, patterns=None,
707
719
  max_files=500, max_items=2000, incremental=False,
708
720
  dry_run=False):
@@ -716,13 +728,18 @@ class FileDispatcher:
716
728
  for ref_kind, key in (("doc_ref", "doc"), ("code_ref", "code")):
717
729
  items, errors, stats = refindex.index_dir(
718
730
  root, kind=ref_kind, patterns=patterns, max_files=max_files,
719
- max_items=max_items, incremental=incremental, ledger=ledger)
731
+ max_items=max_items, incremental=incremental, ledger=ledger,
732
+ commit=False)
720
733
  ids, sens = refindex.add_items(self.cg, items, kind=ref_kind,
721
734
  root=root, layer=layer,
722
735
  sensitivity=sensitivity)
736
+ # 写序(同 op=index_code/index_doc):节点先落盘,水位随后——
737
+ # 中途被杀只会留下「节点新 + 水位旧」,下次增量重切,不会静默漏漂移。
738
+ ledger.save()
723
739
  out["chains"][key] = {
724
740
  "indexed": len(ids), "errors": len(errors),
725
741
  "files": stats.get("files"), "truncated": stats.get("truncated"),
742
+ "empty_scan": stats.get("empty_scan"),
726
743
  "skipped_unchanged": stats.get("skipped_unchanged", 0),
727
744
  "skipped_suffixes": stats.get("skipped_suffixes", []),
728
745
  "sensitivity": sens}