@furongjun1999/dsh-memory 0.6.0 → 0.7.0

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 (202) hide show
  1. package/README.md +618 -572
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/README.md +1 -0
  5. package/docs/discipline/harnesses.yaml +31 -0
  6. package/docs/discipline/templates/full.md.tmpl +1 -1
  7. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
  15. 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
  16. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
  17. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
  18. 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
  19. 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
  20. 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
  21. 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
  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_v21.md +224 -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_v22.md +223 -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_v23.md +293 -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_v24.md +303 -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_v25.md +230 -0
  27. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
  28. 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 +52 -2
  29. 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
  30. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +100 -0
  31. 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
  32. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +57 -9
  33. 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
  34. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
  35. 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
  36. 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
  37. 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
  38. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -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/lib/bridge.js +24 -2
  42. package/lib/hooks.d.ts +31 -2
  43. package/lib/hooks.js +219 -16
  44. package/lib/init.js +16 -1
  45. package/lib/lib/prompt_safety.d.ts +52 -1
  46. package/lib/lib/prompt_safety.js +76 -1
  47. package/md_cg/audit.py +652 -379
  48. package/md_cg/bench6_arms.py +1 -1
  49. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  50. package/md_cg/bench_p0.py +1 -1
  51. package/md_cg/branches.py +2 -2
  52. package/md_cg/ccgc.py +1071 -1006
  53. package/md_cg/chain.py +48 -1
  54. package/md_cg/consistency.py +97 -9
  55. package/md_cg/consolidate.py +34 -9
  56. package/md_cg/crypto.py +59 -26
  57. package/md_cg/docindex.py +16 -1
  58. package/md_cg/evolution.py +17 -1
  59. package/md_cg/export.py +1 -1
  60. package/md_cg/forgetting.py +309 -13
  61. package/md_cg/freshness.py +511 -0
  62. package/md_cg/fsutil.py +454 -8
  63. package/md_cg/generation.py +409 -0
  64. package/md_cg/hotcache.py +4 -1
  65. package/md_cg/identity.py +3 -3
  66. package/md_cg/insight.py +24 -3
  67. package/md_cg/linkref.py +1 -1
  68. package/md_cg/logref.py +327 -0
  69. package/md_cg/mcp_server.py +4261 -3818
  70. package/md_cg/mdcg.py +4695 -3547
  71. package/md_cg/mdcos.py +4672 -4159
  72. package/md_cg/mreview/pipeline.py +15 -1
  73. package/md_cg/nodefile.py +713 -576
  74. package/md_cg/protect.py +158 -11
  75. package/md_cg/protocol.py +41 -2
  76. package/md_cg/provenance.py +22 -4
  77. package/md_cg/reach.py +4 -4
  78. package/md_cg/readcache.py +76 -15
  79. package/md_cg/reconcile.py +1 -1
  80. package/md_cg/refindex.py +246 -37
  81. package/md_cg/refine.py +1 -1
  82. package/md_cg/routing.py +32 -5
  83. package/md_cg/run_tests.py +17 -0
  84. package/md_cg/scrub.py +30 -3
  85. package/md_cg/security.py +47 -1
  86. package/md_cg/self_state.py +5 -5
  87. package/md_cg/semantic/canonical.py +22 -0
  88. package/md_cg/semantic/unify.py +73 -22
  89. package/md_cg/semantic/unify_fixture.json +25 -0
  90. package/md_cg/sleep.py +1297 -0
  91. package/md_cg/sources.py +23 -6
  92. package/md_cg/srcindex.py +352 -0
  93. package/md_cg/stg.py +47 -3
  94. package/md_cg/subgraph.py +2 -2
  95. package/md_cg/sustain.py +1436 -1168
  96. package/md_cg/tasks.py +469 -470
  97. package/md_cg/test_auto_defaults.py +424 -0
  98. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  99. package/md_cg/test_b1b2_write_face.py +417 -0
  100. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  101. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  102. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  103. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  104. package/md_cg/test_boundary_hit.py +410 -0
  105. package/md_cg/test_c3_transient_read_negative.py +793 -0
  106. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  107. package/md_cg/test_ccg_form_parity.py +188 -0
  108. package/md_cg/test_ccgc.py +28 -3
  109. package/md_cg/test_en_pipeline.py +22 -13
  110. package/md_cg/test_generation_guard.py +352 -0
  111. package/md_cg/test_govern_directread.py +17 -4
  112. package/md_cg/test_h2_session_view_norm.py +233 -0
  113. package/md_cg/test_h4_sustain_snapshot.py +1387 -0
  114. package/md_cg/test_hive_ingest.py +285 -285
  115. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  116. package/md_cg/test_issue43_default_policy.py +865 -0
  117. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  118. package/md_cg/test_linkref.py +10 -3
  119. package/md_cg/test_lock.py +2 -2
  120. package/md_cg/test_logref.py +1109 -0
  121. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  122. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  123. package/md_cg/test_mr_m2.py +15 -3
  124. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  125. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  126. package/md_cg/test_n176_link_trust.py +221 -0
  127. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  128. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  129. package/md_cg/test_n195_writepath_reload.py +283 -0
  130. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  131. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  132. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  133. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  134. package/md_cg/test_n201_proposal_visibility.py +382 -0
  135. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  136. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  137. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  138. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  139. package/md_cg/test_n212_n213_n224_generation_gates.py +519 -0
  140. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  141. package/md_cg/test_n225_nonobject_load.py +1110 -0
  142. package/md_cg/test_n230_dirty_replay.py +375 -0
  143. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  144. package/md_cg/test_neg_condition_hits.py +333 -0
  145. package/md_cg/test_neg_tail_honesty.py +663 -0
  146. package/md_cg/test_none_id_write_guard.py +4 -3
  147. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  148. package/md_cg/test_p1.py +5 -5
  149. package/md_cg/test_p11_consistency.py +161 -12
  150. package/md_cg/test_p26_refindex.py +2 -2
  151. package/md_cg/test_p27_docindex.py +777 -774
  152. package/md_cg/test_p28_refcheck.py +740 -13
  153. package/md_cg/test_p29_session_ingest_export.py +2 -2
  154. package/md_cg/test_p2_mcp.py +12 -1
  155. package/md_cg/test_p2_six_elements.py +463 -0
  156. package/md_cg/test_p30_maintain.py +33 -2
  157. package/md_cg/test_p31_insight.py +1 -0
  158. package/md_cg/test_p3_legacy_closure.py +433 -0
  159. package/md_cg/test_p4_freshness.py +680 -0
  160. package/md_cg/test_p8_subgraph_chain.py +14 -1
  161. package/md_cg/test_p9c_dedup_hints.py +276 -0
  162. package/md_cg/test_policy_required_ccg.py +426 -0
  163. package/md_cg/test_protocol.py +22 -0
  164. package/md_cg/test_rank_parity_score_mode.py +522 -0
  165. package/md_cg/test_read_face_input_gates.py +363 -0
  166. package/md_cg/test_read_face_semantics.py +489 -0
  167. package/md_cg/test_recall_face_guards.py +812 -0
  168. package/md_cg/test_rejected_credential_forms.py +188 -0
  169. package/md_cg/test_rejected_redact.py +146 -0
  170. package/md_cg/test_retr_s1b.py +2 -2
  171. package/md_cg/test_retr_s5.py +20 -7
  172. package/md_cg/test_review_conformance.py +21 -3
  173. package/md_cg/test_security_audit_v21.py +2 -2
  174. package/md_cg/test_semantic_canonical.py +5 -3
  175. package/md_cg/test_server_version.py +67 -0
  176. package/md_cg/test_sleep.py +611 -0
  177. package/md_cg/test_sleep_p1.py +784 -0
  178. package/md_cg/test_srcindex.py +171 -0
  179. package/md_cg/test_tenant_env_override_warn.py +63 -50
  180. package/md_cg/test_time_core_lint.py +968 -0
  181. package/md_cg/test_token_lowercase_form.py +315 -0
  182. package/md_cg/test_unify_default_off.py +701 -0
  183. package/md_cg/test_unify_scope.py +182 -0
  184. package/md_cg/test_v21r1_package_version.py +310 -0
  185. package/md_cg/test_writepipe.py +10 -4
  186. package/md_cg/tokens.py +225 -95
  187. package/md_cg/tool_face.py +4 -4
  188. package/md_cg/trust.py +49 -5
  189. package/md_cg/units.py +204 -6
  190. package/md_cg/weights.py +4 -4
  191. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  192. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  193. package/md_cg/writelimit.py +42 -8
  194. package/md_cg/writepipe.py +651 -554
  195. package/package.json +4 -2
  196. package/skills/plugin.json +1 -1
  197. package/src/bridge.ts +25 -2
  198. package/src/hooks.ts +229 -16
  199. package/src/init.ts +15 -1
  200. package/src/lib/prompt_safety.ts +88 -1
  201. package/utf8_boot.py +237 -0
  202. package/zcode/AGENTS.md +10 -10
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",
@@ -31,6 +31,28 @@ from . import zh_en_atoms
31
31
  WIN = 6 # 组合共现窗口(token 距离,探针实证口径)
32
32
  _ATOMS_ZH = None
33
33
 
34
+ # 中文段判据(单点,2026-09-30 检索归一层作用域收窄时立):
35
+ # 中文 = CJK 统一表意文字基本区 U+4E00–U+9FFF。与 en_normalizer 分词字符类
36
+ # `[A-Za-z\u4e00-\u9fff]` 的中文区间同区间;rust/src/text.rs::is_zh 是同判据的
37
+ # Rust 侧单点(两侧归一层共用同一判据;改动须两侧同步,否则 rank_parity 漂移)。
38
+ # 消费方:unify.py::unify_query(中文段逐字保留)、rust/src/atoms.rs::Atoms::unify。
39
+ ZH_LO, ZH_HI = "\u4e00", "\u9fff"
40
+
41
+
42
+ # 生效条件:单参 ch 为长度恰为 1 的 str 时返回 ZH_LO <= ch <= ZH_HI(CJK 基本区);长度 != 1(空串/多字符串)一律返回 False(2026-09-30 清理批次把边界收紧为「长度恰为 1」——此前只在 ch 为空串时为假,多字符串按字典序比较可能被误判为真)。
43
+ def is_zh_char(ch):
44
+ """中文段判据(单点):CJK 统一表意文字基本区 U+4E00–U+9FFF。
45
+
46
+ 边界约定(2026-09-30 清理批次乙2):**仅长度恰为 1 的串可为真**——
47
+ 空串与长度 >1 的串一律 False。此前只有空串为假(链式比较
48
+ `ZH_LO <= ch` 对空串即假),而多字符串按字典序比较:`is_zh_char("中文")`
49
+ 判真(首字符在区间即真)=判据外溢。两侧调用点清点结果:本函数只有
50
+ `unify.py::_runs` 与 `test_unify_scope.py::zh_runs` 两处消费,均逐字符
51
+ 传入 ⇒ 本收紧对存量行为零变更;Rust 侧 `text.rs::is_zh` 收 `char`,
52
+ 天然无空/多字符二态,边界语义两侧一致(docstring 已互指)。
53
+ """
54
+ return len(ch) == 1 and ZH_LO <= ch <= ZH_HI
55
+
34
56
 
35
57
  # 生效条件:模块级常量 `_ATOMS_ZH` 为 None 时按 `__file__` 所在目录的 atoms.json 取 `data.get("atoms", [])` 各项 `zh` 建集合并缓存,非 None 时直接返回 `_ATOMS_ZH`。
36
58
  def atoms_zh():
@@ -1,45 +1,96 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """灵枢 · 检索 query 统一归一层(批次 15,2026-09-23 使用者口径定案)
4
-
5
- 口径:**统一翻译为中文 → 归一化到标准中文集(atoms.json)→ 检索**。
6
- 任何语言的 query 先归一为标准原子序列(空格 join)再进检索各路:
7
- - 英文:en_normalizer(时态还原→停用词剔除→en→zh 语素映射)→
8
- segment 展开为标准原子(「牛肉」→「牛 肉」,与 doc 侧 fm.semantic
9
- 原子形态同构);
10
- - 中文:segment 贪心最长匹配归一到 atoms.json 标准概念;
11
- - 纯中文 query **原样返回**——segment+join+bigrams(去空白) 后与原文
12
- 等价,归一只对跨语 query 有信息增益(beef 与「牛肉」零共享字符,
13
- 形态归一救不了跨语),纯中文零变更纪律;
3
+ """灵枢 · 检索 query 统一归一层(批次 15 立;2026-09-30 使用者裁定收窄作用域)
4
+
5
+ 口径:**只对英文内容做翻译归一,中文内容原样不动**。
6
+ query 先按「中文段 / 非中文段」切开,只有**非中文段里的英文**进归一链路:
7
+ - 非中文段(含 ASCII 字母):en_normalizer(时态还原→停用词剔除→en→zh 语素
8
+ 映射)→ segment 展开为标准原子(「牛肉」→「牛 肉」,与 doc 侧
9
+ fm.semantic 原子形态同构);
10
+ - 中文段(CJK 统一表意文字基本区 U+4E00–U+9FFF,判据单点 =
11
+ `canonical.is_zh_char`):**逐字保留、绝不切分、绝不送 segment**;
12
+ - 非英文段(数字/标点等,段内无 ASCII 字母):原样保留(不是英文即不翻译);
13
+ - 两段按原顺序以单空格 join,空白(段内/段间)一律折叠为单空格;
14
14
  - 专名/未登录词原样保留(词表边界 unknown_keep,非错误)。
15
15
 
16
- 开关:MDCG_UNIFY_QUERY 默认 "1"(2026-09-23 使用者拍板口径转正);
17
- "=0" 显式关回退。两侧同步:rust/mcdg-eval 读同一 env + 同一份
18
- atoms.json(scripts/rank_parity.py --dataset 对拍在两侧同开关态进行,
19
- A/B 差异归档为转正证据)。
16
+ 为什么收窄(2026-09-30 定因,实测于 locomo-zh-500 公开题池 500 题 / 567 池、
17
+ eval_common jaccard + GLOBAL_CAP 解除、k=5):旧口径「query 含任一 ASCII 字母
18
+ 即整条归一」会把**中夹英** query 的中文部分逐字切开(「自我接纳」→
19
+ 「自 我 接 纳」、「慈善跑」→「慈 善 跑」);500 题里含 ASCII 字母的 145 条
20
+ **全部**也含中文(无纯英文 query)。单点实测:整条归一 lexical hit@1 = 94.4% /
21
+ lexical,fuzzy = 87.2%;只译英文片段 lexical = 95.8% / lexical,fuzzy = 96.6%;
22
+ 归一层全关 lexical = 96.4% / lexical,fuzzy = 97.2%。
23
+
24
+ 开关:MDCG_UNIFY_QUERY **默认关**(2026-09-30 使用者裁定,缺省由 "1" 翻为 "0")。
25
+ 定因:本层本职是让**英文** query 命中中文节点,对中文检索池是**纯开销**——
26
+ locomo-zh-500 公开题池(池 567 / 题 500)缺省关态 lexical hit@1 96.4% /
27
+ lexical+fuzzy 97.2%,开态 95.8% / 96.6%(各跑两遍逐位相同)。
28
+
29
+ 三态判据(两侧同语义,唯一开态 = 显式 "1"):
30
+
31
+ MDCG_UNIFY_QUERY Python unify_on() Rust Atoms::from_env()
32
+ 未设 False None(不载入)
33
+ "0" False None(不载入)
34
+ "1" True Some(Atoms)(载入)
35
+
36
+ 显式 "1" 仍能开回(英文对照路要用);两侧同源 en_zh_map.json
37
+ (scripts/rank_parity.py --dataset 对拍在两侧同开关态进行)。
20
38
 
21
39
  归一失败(semantic 模块缺失等)静默原样返回——与 en_zh_terms 同降级
22
40
  风格,不阻断检索主链路。
41
+
42
+ 两侧同构:rust/src/atoms.rs::Atoms::unify 同语义、同中文判据;逐位对拍 fixture
43
+ = `md_cg/semantic/unify_fixture.json`(本侧守卫 md_cg/test_unify_scope.py 与
44
+ Rust 单测 unify_mixed_fixture_matches_python 钉同一份期望值)。
23
45
  """
24
46
  import os
25
47
  import re
26
48
 
27
- # 生效条件:无 required 形参,锚定环境变量名 MDCG_UNIFY_QUERY;当 os.environ.get("MDCG_UNIFY_QUERY", "1") == "1" 时返回 True,否则返回 False。
49
+ from .canonical import is_zh_char
50
+
51
+ # 生效条件:无 required 形参,锚定环境变量名 MDCG_UNIFY_QUERY;**未设**时取缺省 "0"(2026-09-30 使用者裁定:缺省关),当 os.environ.get("MDCG_UNIFY_QUERY", "0") == "1" 时返回 True,其余(未设 / "0" / 任何非 "1" 取值)返回 False。
28
52
  def unify_on() -> bool:
29
- """统一归一层开关(默认开=口径定案转正;=0 显式关回退)。"""
30
- return os.environ.get("MDCG_UNIFY_QUERY", "1") == "1"
53
+ """统一归一层开关(**默认关**;显式 =1 才开,=0/未设一律关)。"""
54
+ return os.environ.get("MDCG_UNIFY_QUERY", "0") == "1"
55
+
56
+
57
+ # 生效条件:单参 text 为任意字符串(空串返回空列表),按 canonical.is_zh_char 逐字符判中文段,相邻同判据字符归并为一个连续段,返回 [(是否中文段, 段文本), ...] 的保序列表。
58
+ def _runs(text):
59
+ """按中文判据切连续段:[(True, "中文段"), (False, "非中文段"), ...]。"""
60
+ out = []
61
+ cur = None
62
+ buf = []
63
+ for ch in text:
64
+ k = is_zh_char(ch)
65
+ if buf and k != cur:
66
+ out.append((cur, "".join(buf)))
67
+ buf = []
68
+ cur = k
69
+ buf.append(ch)
70
+ if buf:
71
+ out.append((cur, "".join(buf)))
72
+ return out
31
73
 
32
74
 
33
- # 生效条件:text 为 None/空串或开关关闭或不含 [A-Za-z] 时原样返回;否则经 canonical.query_atoms 归一为标准原子序列并以单空格 join 返回(归一产物为空或抛异常时原样返回)。
75
+ # 生效条件:text 为 None/空串、或开关 MDCG_UNIFY_QUERY 取值不为 "1"(含**未设**——缺省 "0",2026-09-30 起关)、或 strip 后不含 [A-Za-z] 时,原样返回 text(未 strip 的入参);否则按中文段(canonical.is_zh_char)切分——中文段与不含 ASCII 字母的非中文段逐字保留、含 ASCII 字母的非中文段经 canonical.query_atoms 归一——各段折空白后按原顺序单空格 join;归一产物为空(trim 后)或抛异常时原样返回 text。
34
76
  def unify_query(text):
35
- """检索入口统一归一:任意语言 query → 标准原子序列。"""
77
+ """检索入口统一归一:只译英文内容,中文段逐字原样。"""
36
78
  t = (text or "").strip()
37
79
  if not t or not unify_on() or not re.search(r"[A-Za-z]", t):
38
80
  return text
39
81
  try:
40
82
  from .canonical import query_atoms
41
- atoms = query_atoms(t)
83
+ parts = []
84
+ for is_zh, run in _runs(t):
85
+ if is_zh or not re.search(r"[A-Za-z]", run):
86
+ seg = run # 中文段 / 非英文段:逐字保留
87
+ else:
88
+ atoms = query_atoms(run) # 非中文段里的英文:既有链路
89
+ seg = " ".join(a for a in atoms if a)
90
+ seg = " ".join(seg.split()) # 段内空白折叠为单空格
91
+ if seg:
92
+ parts.append(seg)
42
93
  except Exception:
43
94
  return text
44
- u = " ".join(a for a in atoms if a).strip()
95
+ u = " ".join(parts)
45
96
  return u if u else text
@@ -0,0 +1,25 @@
1
+ {
2
+ "_note": "统一归一层(unify_query / Atoms::unify)作用域收窄(2026-09-30 使用者裁定:只对英文内容做翻译归一,中文内容原样不动)的逐位对拍 fixture。两侧同源钉死:Python 守卫 md_cg/test_unify_scope.py 与 Rust 单测 rust/src/atoms.rs::tests::unify_mixed_fixture_matches_python 都读本文件(Rust 侧 include_str! 编译期嵌入)。任一侧实现漂移即红。含「空产物回退」例(入参含首尾空白且归一产物为空 → 两侧逐位回退**原入参本身**,2026-09-30 清理批次乙1 钉住;Python 侧语义为 unify_query 尾行 `return text`)。",
3
+ "_caveat": "非中文段一律用「两侧处理同构」的形态:全小写英文词(或命中 en_zh_map 的形)。两侧**既有非对称**共三条(非 2026-09-30 收窄引入,亦非本清理批次引入——HEAD 对照实测同分歧),故不入本 fixture:①未登录大写英文词——Python 保留原 case(`领养 LGBTQ 群体`→原样、`AI 记忆 系统`→原样),Rust 经 normalize_en 一律 lower(→`领养 lgbtq 群体`/`ai 记忆 系统`);②命中词表的专名——Python 双原子保留原词+译词(`Paris 报告`→`Paris 巴 黎 报告`),Rust 只出译词(→`巴 黎 报告`);③字母与数字同段——Python 丢数字(`beef 2024 报告`→`牛肉 报告`),Rust 保数字(→`牛肉 2024 报告`)。**真源 = `docs/hive/检索算法口径对照_v0.1.md` 正文「两侧既有非对称」表**(含两侧原始读数、fixture 为何避开、以及「统一到哪一侧」的待裁定项);本文件只钉两侧同构形态。",
4
+ "cases": [
5
+ {"name": "纯英·跨语归一冒烟", "in": "I eat beef yesterday", "out": "我 吃 牛肉 昨天"},
6
+ {"name": "纯英·单动词时态还原", "in": "wrote", "out": "写"},
7
+ {"name": "纯英·多词", "in": "compiler tests", "out": "编 辑 家 测 试"},
8
+ {"name": "纯中·复合词不切", "in": "自我接纳", "out": "自我接纳"},
9
+ {"name": "纯中·旧口径会切开的两字三字词", "in": "慈善跑", "out": "慈善跑"},
10
+ {"name": "纯中·带空格", "in": "蜂群调度 依赖门禁", "out": "蜂群调度 依赖门禁"},
11
+ {"name": "中夹英·英文在首", "in": "beef 报告", "out": "牛肉 报告"},
12
+ {"name": "中夹英·未登录英文片段+中文段逐字保留", "in": "领养 xyzzy 群体", "out": "领养 xyzzy 群体"},
13
+ {"name": "英夹中·英文译中文段原样", "in": "beef 牛肉", "out": "牛肉 牛肉"},
14
+ {"name": "前后空白·折叠", "in": " beef 报告 ", "out": "牛肉 报告"},
15
+ {"name": "空产物回退·原样返回原入参(不 trim)", "in": " the is ", "out": " the is "},
16
+ {"name": "多空格·折叠为单空格", "in": "beef 报告", "out": "牛肉 报告"},
17
+ {"name": "数字·非英文段原样", "in": "2024 报告", "out": "2024 报告"},
18
+ {"name": "数字+中文+英文混合", "in": "2024 年 beef 报告", "out": "2024 年 牛肉 报告"},
19
+ {"name": "半角标点·非英文段原样", "in": "beef 阅读,写作", "out": "牛肉 阅读 , 写作"},
20
+ {"name": "全角标点·非英文段原样", "in": "报告。很好 beef", "out": "报告 。 很好 牛肉"},
21
+ {"name": "未登录词·unknown_keep", "in": "xyzzy 报告", "out": "xyzzy 报告"},
22
+ {"name": "单字母词 I", "in": "I 支持", "out": "我 支持"},
23
+ {"name": "英中粘连·无空格混合", "in": "beef报告", "out": "牛肉 报告"}
24
+ ]
25
+ }