@furongjun1999/dsh-memory 0.6.0 → 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 (170) hide show
  1. package/README.md +588 -572
  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/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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  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_v24.md +303 -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_v25.md +230 -0
  23. 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
  24. 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
  25. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
  26. 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
  27. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. package/lib/bridge.js +24 -2
  35. package/lib/hooks.d.ts +31 -2
  36. package/lib/hooks.js +219 -16
  37. package/lib/init.js +16 -1
  38. package/lib/lib/prompt_safety.d.ts +52 -1
  39. package/lib/lib/prompt_safety.js +76 -1
  40. package/md_cg/audit.py +652 -379
  41. package/md_cg/bench6_arms.py +1 -1
  42. package/md_cg/bench_p0.py +1 -1
  43. package/md_cg/branches.py +2 -2
  44. package/md_cg/ccgc.py +1071 -1006
  45. package/md_cg/chain.py +1 -1
  46. package/md_cg/consistency.py +97 -9
  47. package/md_cg/consolidate.py +34 -9
  48. package/md_cg/crypto.py +59 -26
  49. package/md_cg/docindex.py +16 -1
  50. package/md_cg/evolution.py +17 -1
  51. package/md_cg/export.py +1 -1
  52. package/md_cg/forgetting.py +309 -13
  53. package/md_cg/fsutil.py +454 -8
  54. package/md_cg/identity.py +3 -3
  55. package/md_cg/insight.py +24 -3
  56. package/md_cg/linkref.py +1 -1
  57. package/md_cg/logref.py +327 -0
  58. package/md_cg/mcp_server.py +4152 -3818
  59. package/md_cg/mdcg.py +4208 -3547
  60. package/md_cg/mdcos.py +4491 -4159
  61. package/md_cg/mreview/pipeline.py +15 -1
  62. package/md_cg/nodefile.py +639 -575
  63. package/md_cg/protect.py +158 -11
  64. package/md_cg/protocol.py +41 -2
  65. package/md_cg/provenance.py +22 -4
  66. package/md_cg/reach.py +4 -4
  67. package/md_cg/readcache.py +76 -15
  68. package/md_cg/reconcile.py +1 -1
  69. package/md_cg/refindex.py +246 -37
  70. package/md_cg/refine.py +1 -1
  71. package/md_cg/routing.py +32 -5
  72. package/md_cg/run_tests.py +17 -0
  73. package/md_cg/scrub.py +30 -3
  74. package/md_cg/security.py +47 -1
  75. package/md_cg/self_state.py +5 -5
  76. package/md_cg/sources.py +23 -6
  77. package/md_cg/srcindex.py +352 -0
  78. package/md_cg/stg.py +47 -3
  79. package/md_cg/subgraph.py +2 -2
  80. package/md_cg/sustain.py +1299 -1168
  81. package/md_cg/tasks.py +469 -470
  82. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  83. package/md_cg/test_b1b2_write_face.py +417 -0
  84. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  85. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  86. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  87. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  88. package/md_cg/test_c3_transient_read_negative.py +793 -0
  89. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  90. package/md_cg/test_ccg_form_parity.py +188 -0
  91. package/md_cg/test_ccgc.py +28 -3
  92. package/md_cg/test_govern_directread.py +17 -4
  93. package/md_cg/test_h2_session_view_norm.py +233 -0
  94. package/md_cg/test_h4_sustain_snapshot.py +1377 -0
  95. package/md_cg/test_hive_ingest.py +285 -285
  96. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  97. package/md_cg/test_issue43_default_policy.py +865 -0
  98. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  99. package/md_cg/test_linkref.py +10 -3
  100. package/md_cg/test_lock.py +2 -2
  101. package/md_cg/test_logref.py +1109 -0
  102. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  103. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  104. package/md_cg/test_mr_m2.py +15 -3
  105. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  106. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  107. package/md_cg/test_n176_link_trust.py +221 -0
  108. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  109. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  110. package/md_cg/test_n195_writepath_reload.py +283 -0
  111. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  112. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  113. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  114. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  115. package/md_cg/test_n201_proposal_visibility.py +382 -0
  116. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  117. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  118. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  119. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  120. package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
  121. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  122. package/md_cg/test_n225_nonobject_load.py +1110 -0
  123. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  124. package/md_cg/test_neg_condition_hits.py +333 -0
  125. package/md_cg/test_neg_tail_honesty.py +663 -0
  126. package/md_cg/test_none_id_write_guard.py +4 -3
  127. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  128. package/md_cg/test_p1.py +5 -5
  129. package/md_cg/test_p11_consistency.py +161 -12
  130. package/md_cg/test_p26_refindex.py +2 -2
  131. package/md_cg/test_p27_docindex.py +777 -774
  132. package/md_cg/test_p28_refcheck.py +740 -13
  133. package/md_cg/test_p29_session_ingest_export.py +2 -2
  134. package/md_cg/test_p2_mcp.py +12 -1
  135. package/md_cg/test_p30_maintain.py +33 -2
  136. package/md_cg/test_p31_insight.py +1 -0
  137. package/md_cg/test_p9c_dedup_hints.py +276 -0
  138. package/md_cg/test_policy_required_ccg.py +426 -0
  139. package/md_cg/test_protocol.py +22 -0
  140. package/md_cg/test_rank_parity_score_mode.py +516 -0
  141. package/md_cg/test_read_face_input_gates.py +363 -0
  142. package/md_cg/test_read_face_semantics.py +489 -0
  143. package/md_cg/test_recall_face_guards.py +812 -0
  144. package/md_cg/test_rejected_credential_forms.py +188 -0
  145. package/md_cg/test_rejected_redact.py +146 -0
  146. package/md_cg/test_retr_s1b.py +2 -2
  147. package/md_cg/test_retr_s5.py +20 -7
  148. package/md_cg/test_review_conformance.py +21 -3
  149. package/md_cg/test_security_audit_v21.py +2 -2
  150. package/md_cg/test_server_version.py +67 -0
  151. package/md_cg/test_srcindex.py +171 -0
  152. package/md_cg/test_tenant_env_override_warn.py +63 -50
  153. package/md_cg/test_token_lowercase_form.py +315 -0
  154. package/md_cg/test_v21r1_package_version.py +310 -0
  155. package/md_cg/test_writepipe.py +10 -4
  156. package/md_cg/tokens.py +225 -95
  157. package/md_cg/tool_face.py +4 -4
  158. package/md_cg/trust.py +49 -5
  159. package/md_cg/units.py +204 -6
  160. package/md_cg/weights.py +4 -4
  161. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  162. package/md_cg/writelimit.py +42 -8
  163. package/md_cg/writepipe.py +651 -554
  164. package/package.json +3 -2
  165. package/skills/plugin.json +1 -1
  166. package/src/bridge.ts +25 -2
  167. package/src/hooks.ts +229 -16
  168. package/src/init.ts +15 -1
  169. package/src/lib/prompt_safety.ts +88 -1
  170. package/zcode/AGENTS.md +10 -10
@@ -230,12 +230,12 @@ def main():
230
230
  _devs = [e["t"] for e in sources.DSHSessionSource(sess).events()]
231
231
  check("源适配层:DSH 13 位毫秒 time 归一为 epoch 秒",
232
232
  bool(_devs) and all(1e9 < t < 1e10 for t in _devs), str(_devs))
233
- _tw_flat = [x for e in cg.index["nodes"].values()
233
+ _tw_flat = [x for e in list(cg.index["nodes"].values())
234
234
  if isinstance(e.get("time_window"), (list, tuple))
235
235
  for x in e["time_window"]]
236
236
  check("端到端:摄取节点落库 time_window 无 13 位毫秒外溢(旧症状已消失)",
237
237
  bool(_tw_flat)
238
- and all(0 <= x < sources.trust._MS_EPOCH_THRESHOLD for x in _tw_flat),
238
+ and all(0 <= x < sources.trust._MS_EPOCH_THRESHOLD for x in list(_tw_flat)),
239
239
  str(sorted(set(_tw_flat))[:6]))
240
240
 
241
241
  d1 = call_tool(cg, "cg", {"op": "ingest", "action": "dir",
@@ -257,7 +257,18 @@ def main():
257
257
 
258
258
  # 11. 基元暴露面(kernel):默认只暴露 cg / stg 两个认知基元
259
259
  print("\n【11】基元暴露面(kernel 默认)")
260
- kc = McpClient(root, actor="kernel", extra_env={"MDCG_MCP_SURFACE": "kernel"})
260
+ # 迁移(issue #43,2026-09-29):未设 MDCG_POLICY_FILE 时不再等于「无规则
261
+ # 可判」——修后回落到**包内默认** data/policy.json,下方「未验证的写入」
262
+ # 因此由 DEFER 变 REJECT。本断言考的是 **review_queue 通路**(非 ACCEPT/
263
+ # REJECT 出口)仍可达且不落盘,与策略来源无关,故显式给一份**空规则库**
264
+ # 保持原口径;「默认安装的真实行为(REJECT)」由
265
+ # md_cg/test_issue43_default_policy.py 钉住。
266
+ _empty_pol = os.path.join(root, "policy_empty.json")
267
+ with open(_empty_pol, "w", encoding="utf-8") as f:
268
+ json.dump({"forbidden": [], "required": []}, f)
269
+ kc = McpClient(root, actor="kernel",
270
+ extra_env={"MDCG_MCP_SURFACE": "kernel",
271
+ "MDCG_POLICY_FILE": _empty_pol})
261
272
  try:
262
273
  kc.send("initialize", {"protocolVersion": "2024-11-05", "capabilities": {},
263
274
  "clientInfo": {"name": "k", "version": "0"}})
@@ -7,6 +7,7 @@
7
7
  ③ maintain.importance:dry-run 报表 → apply → 留痕 → rollback 复原
8
8
  ④ maintain.longterm:dry-run → 断面落盘 → 幂等 → list/show
9
9
  ⑤ maintain.prefeed:预演不写 → write 时 ACCEPT 新增 / MERGE 并入不新增
10
+ (2026-09-30 补强:MERGE 落点的新正文聚合去向 + 新写入方归属并列落 fm)
10
11
  ⑥ maintain.separate:相似而条件不同 → 候选 → apply 写对称边 → 幂等
11
12
  ⑦ consolidate.promote:dry-run 分流 → 迁移 → 追溯 → 回滚
12
13
  ⑧ 权限分档:写层可 stat/prefeed、不可 importance/separate/rollback
@@ -18,6 +19,7 @@
18
19
  """
19
20
  from __future__ import annotations
20
21
 
22
+ import json
21
23
  import os
22
24
  import shutil
23
25
  import sys
@@ -39,11 +41,13 @@ SEP_B = ("# 功能名:连接池选择\n# 生效条件:多租户在线服务\
39
41
  "# 不适用条件:嵌入式设备\n")
40
42
  PROMO_HOT = ("# 功能名:重试退避策略\n# 生效条件:网络抖动且请求幂等\n"
41
43
  "# 子功能:在瞬时故障下限制重试次数\n# 执行:指数退避重试至多三次\n"
44
+ "# 验证方式:编译器/静态检查通过\n"
42
45
  "# 不适用条件:非幂等写操作\n")
43
46
  PROMO_COLD = ("# 功能名:日志轮转策略\n# 生效条件:磁盘占用超过阈值\n"
44
47
  "# 子功能:按大小切分历史日志\n# 执行:达到上限即滚动归档\n"
48
+ "# 验证方式:编译器/静态检查通过\n"
45
49
  "# 不适用条件:只读挂载卷\n")
46
- # 反复命中但四要素不全(缺「不适用条件」)
50
+ # 反复命中但六要素不全(缺「不适用条件」+「验证方式」)
47
51
  PROMO_BAD = ("# 功能名:缓存穿透保护\n# 生效条件:高频查询未命中\n"
48
52
  "# 子功能:用空值占位挡住穿透\n# 执行:写空对象并设短 TTL\n")
49
53
  NOVEL = ("# 观察:本机固定使用 Windows 与 cmd\n# 生效条件:本机开发环境\n"
@@ -201,6 +205,33 @@ def main():
201
205
  p2.get("decision") == "reinforce" and bool(p2.get("reinforced")),
202
206
  f"decision={p2.get('decision')}")
203
207
  check("MERGE 后节点数不变", len(cg.index["nodes"]) == n1)
208
+ # 2026-09-30 补强:prefeed(write=True) 的 MERGE 落点接线——mdcos.py:2891
209
+ # 调 forgetting.reinforce 时传 `content=content, session=_writer_session(self)`。
210
+ # 复核员实测抽掉该接线后各守卫静默全绿(此前无断言覆盖)。断言打在
211
+ # 可观察结果上:新正文按聚合口径进目标正文 + 新写入方归属并列落目标 fm。
212
+ _prev_sess = getattr(cg, "session", None)
213
+ cg.session = "sess_p30"
214
+ NOVEL2 = NOVEL.replace("cmd.exe", "cmd.exe/powershell")
215
+ p3 = call_tool(cg, "cg", {"op": "maintain", "action": "prefeed",
216
+ "content": NOVEL2, "layer": "contextual",
217
+ "write": True})
218
+ rein3 = p3.get("reinforced") or {}
219
+ tgt3 = rein3.get("node_id")
220
+ full3 = (cg.get(tgt3) or {}) if tgt3 else {}
221
+ sink3 = rein3.get("content_sink") or {}
222
+ check("prefeed 落点①:近重复判 MERGE,新正文按聚合口径进目标正文",
223
+ p3.get("decision") == "reinforce" and bool(tgt3)
224
+ and "powershell" in (full3.get("content") or "")
225
+ and sink3.get("action") == "appended"
226
+ and "powershell" in (sink3.get("line") or ""),
227
+ f"decision={p3.get('decision')} sink={sink3}")
228
+ srcs3 = [(x.get("session"), x.get("actor")) for x in
229
+ ((full3.get("frontmatter") or {}).get("merge_sources") or [])]
230
+ check("prefeed 落点②:新写入方 session/actor 归属并列落目标 fm",
231
+ bool(srcs3) and srcs3[-1] == ("sess_p30", "p30"),
232
+ json.dumps((full3.get("frontmatter") or {}).get("merge_sources"),
233
+ ensure_ascii=False))
234
+ cg.session = _prev_sess
204
235
 
205
236
  # ---------------------------------------------- ⑥ separate
206
237
  print("\n【6】maintain.separate:候选 → 对称边 → 幂等")
@@ -242,7 +273,7 @@ def main():
242
273
  r1 = call_tool(cg, "cg", {"op": "consolidate", "action": "promote"})
243
274
  check("dry-run 未写盘", r1.get("dry_run") and r1.get("written") == 0)
244
275
  check("命中热节点", r1.get("targeted", 0) >= 1, f"targeted={r1.get('targeted')}")
245
- check("剔除四要素不全", r1.get("skipped_incomplete", 0) >= 1)
276
+ check("剔除六要素不全", r1.get("skipped_incomplete", 0) >= 1)
246
277
  check("剔除不热节点", r1.get("skipped_not_hot", 0) >= 1)
247
278
  r2 = call_tool(cg, "cg", {"op": "consolidate", "action": "promote",
248
279
  "apply": True})
@@ -80,6 +80,7 @@ REC_B = ("# 功能名:压缩前先落盘检查点乙\n# 生效条件:%s\n"
80
80
  # ---- 盲区学习夹具 ---------------------------------------------------------
81
81
  LEARN_KB = ("# 功能名:离线缓存决策知识\n# 生效条件:单机离线环境\n"
82
82
  "# 子功能:提供离线时的缓存选型判据\n# 执行:比对候选实现并给出结论\n"
83
+ "# 验证方式:编译器/静态检查通过\n"
83
84
  "# 不适用条件:需要强一致分布式场景\n")
84
85
  LEARN_ANCHOR = ("# 功能名:离线缓存锚点条目 zkv7\n# 生效条件:单机离线环境\n"
85
86
  "# 子功能:作为离线缓存话题的检索入口 zkv7\n"
@@ -0,0 +1,276 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · P-9b 去重可见性守卫:直写 dup_of 提示 + gated 路径两处静默洞
3
+
4
+ 背景(探针实测,2026-09-30)
5
+ ---------------------------
6
+ ① **直写**(`mdcg_remember(gated=false)` 与 `cg(op=write, gated=false)` 的链尾
7
+ 执行器)此前返回体**只有 id**——「同内容以异 id 再写一次」在盘面上变两个
8
+ 节点,而调用方拿不到任何提示(`add` 只回 id 字符串)。
9
+ 这是**文档化现状**(`writelimit.py` 模块头注 :9-12:限流/同构聚合只作用
10
+ contextual 层,knowledge 等手动纪律写入不受限)⇒ **不许改写入语义**。
11
+ ② gated 路径(默认路径)有两处**静默洞**:
12
+ (a) 同 id 覆写:`redundancy(exclude=node_id)` 自排除 ⇒ 该层只此一节点时
13
+ `compared=0`、`duplicate_with=None`、`duplicate_ratio=0.0`——「覆写」与
14
+ 「全新写入」在裁决里完全不可区分(审计面全 null);
15
+ (b) `importance≥PROTECT_IMPORTANCE(0.7)` 的「保护优先」分支**排在冗余判定
16
+ 之前** ⇒ 高重要度内容的重复写直接 ACCEPT,绕过 MERGE/DROP(实测:dup=1.0
17
+ 且节点数 +1),审计面只剩一句「触发不可遗忘保护」。
18
+
19
+ 本批处置(依据见 `forgetting.assess` 的 docstring 与 `md_cg/mcp_server.py` 的
20
+ 分支注释):**只补可见性读数,不动任何落盘行为与 verdict**——
21
+ · 直写:返回体附 `dup_of`/`dup_ratio`/`dup_compared`/`dup_hint` +
22
+ `overwrite_of`/`overwrite_ratio`;
23
+ · gated (a):`gate.entropy.overwrite_of`/`overwrite_ratio` 非 null;
24
+ · gated (b):该分支返回 `gate.dedup_skipped`(reason=protect_importance +
25
+ 去重判据读数),reason 文案显式写出「因保护优先未走去重」。
26
+
27
+ 为何 (b) 选「标注」而不是「把 PROTECT 挪到冗余判定之后」:挪位会削弱
28
+ 「不可遗忘保护」(高重要度内容将可被 DROP/并入他节点),与 PROTECT_IMPORTANCE
29
+ 及 `writelimit.py` 头注的跨模块「保护优先」口径分叉,且 DROP 不可逆——缺陷本体
30
+ 是「静默」而非「保护」,标注即可消除且零回归面。本测 ⑤ **钉住这个决定**:
31
+ 高重要度近重复的 verdict 必须仍是 ACCEPT(若被挪位 ⇒ 变 MERGE ⇒ 立刻红)。
32
+
33
+ 判据来源:现成字段 `gate.entropy.duplicate_with/.duplicate_ratio/.compared` 与
34
+ `gate.redundancy.with/.max`(不新增重复度算法;新增读数复用 `forgetting` 的
35
+ 同一实现 redundancy / prior_node / self_coverage)。
36
+
37
+ 运行:python -m md_cg.test_p9c_dedup_hints
38
+ """
39
+ from __future__ import annotations
40
+
41
+ import json
42
+ import os
43
+ import shutil
44
+ import tempfile
45
+ import traceback
46
+
47
+ from . import forgetting, writepipe
48
+ from .mdcos import MdCGOS, MdCGSecure
49
+ from .security import Principal
50
+
51
+ PASS = 0
52
+ FAIL = 0
53
+ FAILS = []
54
+
55
+
56
+ def check(name, cond, detail=""):
57
+ global PASS, FAIL
58
+ if cond:
59
+ PASS += 1
60
+ print(f" [PASS] {name}" + (f" · {detail}" if detail else ""))
61
+ else:
62
+ FAIL += 1
63
+ FAILS.append(name)
64
+ print(f" [FAIL] {name} · {detail}")
65
+
66
+
67
+ def ccg(title, body):
68
+ return (f"# 功能名:{title}\n# 生效条件:任意情境\n# 子功能:验收\n"
69
+ f"# 执行:直接调用\n# 验证方式:test\n# 不适用条件:无\n{body}\n")
70
+
71
+
72
+ # 直写路径 A(mdcg_remember gated=false)用:无策略闸,正文无需 PASSED
73
+ BODY = ccg("编译缓存清理", "清理编译缓存目录释放磁盘空间,超过 2GB 时按 mtime 删除最旧分片")
74
+ # 直写路径 B(cg(op=write))用:必须过 audit 闸(策略 required 含 PASSED)
75
+ BODY_PASSED = ccg("编译缓存清理", "PASSED 清理编译缓存目录释放磁盘空间,超过 2GB 时删除最旧分片")
76
+
77
+
78
+ def _mk_secure(root):
79
+ p = Principal(tenant="default", actor="t_designer", role="designer",
80
+ can_write=True, can_admin=True)
81
+ return MdCGSecure(root, principal=p)
82
+
83
+
84
+ def _policy(tmp, required=("PASSED",), forbidden=()):
85
+ path = os.path.join(tmp, "policy_p9c.json")
86
+ with open(path, "w", encoding="utf-8") as f:
87
+ json.dump({"forbidden": list(forbidden), "required": list(required)}, f)
88
+ return path
89
+
90
+
91
+ def _run(tmp):
92
+ from . import mcp_server
93
+
94
+ # ============ A. 直写路径:mdcg_remember(gated=false) ============
95
+ print("\n[A] 直写路径 A(mdcg_remember gated=false):同内容异 id 的提示")
96
+ cg = _mk_secure(os.path.join(tmp, "a"))
97
+ cg.add("k_base", BODY, layer="knowledge", importance=0.5)
98
+ n_before = len(cg.index["nodes"])
99
+ res = mcp_server._dispatch(cg, "mdcg_remember",
100
+ {"content": BODY, "node_id": "k_dup",
101
+ "gated": False, "layer": "knowledge",
102
+ "content_kind": "text"})
103
+ check("A1 直写仍成功落盘(不改写入语义)",
104
+ res.get("ok") is True and cg.get("k_dup") is not None, repr(res)[:160])
105
+ check("A2 盘面仍新增节点(去重语义未被改动)",
106
+ len(cg.index["nodes"]) == n_before + 1,
107
+ f"{n_before} -> {len(cg.index['nodes'])}")
108
+ check("A3 返回体带 dup_of 指向已存在节点(修复面)",
109
+ res.get("dup_of") == "k_base", repr(res)[:200])
110
+ check("A4 dup_ratio 为现成重复度读数(≥ DUP_MERGE)",
111
+ isinstance(res.get("dup_ratio"), float)
112
+ and res["dup_ratio"] >= forgetting.DUP_MERGE, repr(res.get("dup_ratio")))
113
+ check("A5 dup_hint 如实说明「直写不去重」的文档化现状",
114
+ isinstance(res.get("dup_hint"), str) and "直写" in res["dup_hint"]
115
+ and "writelimit" in res["dup_hint"], repr(res.get("dup_hint"))[:160])
116
+
117
+ # 无重复内容:不得凭空报 dup(判据要有判别力)
118
+ res2 = mcp_server._dispatch(cg, "mdcg_remember",
119
+ {"content": ccg("奇点事故复盘", "首次观测到未知内存泄漏,与既有记忆无重叠"),
120
+ "node_id": "k_fresh", "gated": False,
121
+ "layer": "knowledge", "content_kind": "text"})
122
+ check("A6 无重复时不报 dup_of(避免假提示)",
123
+ res2.get("ok") is True and "dup_of" not in res2, repr(res2)[:160])
124
+
125
+ # 同 id 覆写:overwrite_of / overwrite_ratio
126
+ res3 = mcp_server._dispatch(cg, "mdcg_remember",
127
+ {"content": BODY, "node_id": "k_base",
128
+ "gated": False, "layer": "knowledge",
129
+ "content_kind": "text"})
130
+ check("A7 同 id 覆写回 overwrite_of(原本无从区分覆写与新建)",
131
+ res3.get("overwrite_of") == "k_base", repr(res3)[:200])
132
+ check("A8 同 id 同内容覆写:overwrite_ratio ≈ 1.0(空写可见)",
133
+ res3.get("overwrite_ratio") == 1.0, repr(res3.get("overwrite_ratio")))
134
+ res4 = mcp_server._dispatch(cg, "mdcg_remember",
135
+ {"content": ccg("编译缓存清理", "清理编译缓存目录释放磁盘空间,超过 8GB 时按 mtime 删除最旧分片"),
136
+ "node_id": "k_base", "gated": False,
137
+ "layer": "knowledge", "content_kind": "text"})
138
+ check("A9 同 id 改内容覆写:overwrite_ratio < 1.0(真改可见)",
139
+ isinstance(res4.get("overwrite_ratio"), float)
140
+ and res4["overwrite_ratio"] < 1.0, repr(res4.get("overwrite_ratio")))
141
+
142
+ # ============ B. 直写路径:cg(op=write) 链尾执行器 ============
143
+ print("\n[B] 直写路径 B(cg(op=write, gated=false) 链尾执行器)")
144
+ old_policy = os.environ.get("MDCG_POLICY_FILE")
145
+ os.environ["MDCG_POLICY_FILE"] = _policy(tmp)
146
+ try:
147
+ pipe = writepipe.install_default_gates(writepipe.WritePipeline())
148
+ cgb = _mk_secure(os.path.join(tmp, "b"))
149
+ cgb.add("w_base", BODY_PASSED, layer="knowledge", importance=0.5)
150
+ out = pipe.execute(cgb, {"content_kind": "text", "content": BODY_PASSED,
151
+ "gated": False, "layer": "knowledge"})
152
+ check("B1 直写经链:committed + verdict 不变",
153
+ out.get("committed") is True and out.get("verdict") is not None,
154
+ repr(out)[:200])
155
+ check("B2 经链直写也带 dup_of(第二处落盘点同样接线上)",
156
+ out.get("dup_of") == "w_base", repr(out)[:200])
157
+ check("B3 经链直写 dup_ratio 读数存在",
158
+ isinstance(out.get("dup_ratio"), float)
159
+ and out["dup_ratio"] >= forgetting.DUP_MERGE, repr(out.get("dup_ratio")))
160
+ out2 = pipe.execute(cgb, {"content_kind": "text",
161
+ "content": BODY_PASSED, "gated": False,
162
+ "node_id": "w_base", "layer": "knowledge"})
163
+ check("B4 经链直写覆写读数存在",
164
+ out2.get("overwrite_of") == "w_base"
165
+ and out2.get("overwrite_ratio") == 1.0, repr(out2)[:200])
166
+ # 无重复不报 dup
167
+ fresh = ccg("奇点事故复盘二", "PASSED 首次观测到未知泄漏,与既有记忆无重叠")
168
+ out3 = pipe.execute(cgb, {"content_kind": "text", "content": fresh,
169
+ "gated": False, "layer": "knowledge"})
170
+ check("B5 经链直写无重复时不报 dup_of",
171
+ out3.get("committed") is True and "dup_of" not in out3,
172
+ repr(out3)[:160])
173
+ finally:
174
+ if old_policy is None:
175
+ os.environ.pop("MDCG_POLICY_FILE", None)
176
+ else:
177
+ os.environ["MDCG_POLICY_FILE"] = old_policy
178
+
179
+ # ============ C. gated 洞 (a):同 id 覆写的裁决不再全 null ============
180
+ print("\n[C] gated (a):同 id 覆写 → entropy.overwrite_of(不再静默全 null)")
181
+ cg3 = MdCGOS(os.path.join(tmp, "c"))
182
+ cg3.add("k_ov", BODY, layer="knowledge", importance=0.5)
183
+ r = cg3.remember_gated("k_ov", BODY, layer="knowledge", role="user")
184
+ ent = r["gate"]["entropy"]
185
+ check("C1 覆写情形 entropy.overwrite_of 指向既有 id",
186
+ ent.get("overwrite_of") == "k_ov", repr(ent)[:220])
187
+ check("C2 覆写情形 overwrite_ratio 有值(1.0=逐字同构的重写)",
188
+ ent.get("overwrite_ratio") == 1.0, repr(ent.get("overwrite_ratio")))
189
+ check("C3 并未改动去重判据本身:自排除仍在(duplicate_with 依旧 None、compared 0)",
190
+ ent.get("duplicate_with") is None and ent.get("compared") == 0,
191
+ repr({k: ent.get(k) for k in ("duplicate_with", "compared")}))
192
+ check("C4 verdict 不变(覆写语义未动)", r["verdict"] == "ACCEPT",
193
+ f"{r['verdict']} · {r['gate']['reason']}")
194
+ check("C5 同 id 异内容覆写:overwrite_ratio < 1.0",
195
+ forgetting.assess(cg3, BODY.replace("2GB", "8GB"), layer="knowledge",
196
+ role="user", node_id="k_ov")["entropy"]["overwrite_ratio"] < 1.0,
197
+ str(forgetting.assess(cg3, BODY.replace("2GB", "8GB"), layer="knowledge",
198
+ role="user", node_id="k_ov")["entropy"]["overwrite_ratio"]))
199
+
200
+ # ============ D. gated 洞 (b):保护优先不静默 ============
201
+ print("\n[D] gated (b):importance≥0.7 的近重复 → dedup_skipped 标注")
202
+ cg4 = MdCGOS(os.path.join(tmp, "d"))
203
+ cg4.add("c_dup", BODY, layer="contextual", importance=0.5)
204
+ n_before = len(cg4.index["nodes"])
205
+ r2 = cg4.remember_gated("c_new", BODY, layer="contextual", role="user",
206
+ importance_hint=0.9)
207
+ g = r2["gate"]
208
+ dk = g.get("dedup_skipped")
209
+ check("D1 保护优先分支**仍** ACCEPT(保护语义不变——未被挪到冗余判定之后)",
210
+ r2["verdict"] == "ACCEPT", f"{r2['verdict']} · {g['reason']}")
211
+ check("D2 该分支不再静默:dedup_skipped 存在且 reason=protect_importance",
212
+ isinstance(dk, dict) and dk.get("reason") == "protect_importance",
213
+ repr(dk)[:220])
214
+ check("D3 标注带现成去重读数(duplicate_with 指向既有节点)",
215
+ isinstance(dk, dict) and dk.get("duplicate_with") == "c_dup"
216
+ and dk.get("duplicate_ratio", 0) >= forgetting.DUP_MERGE,
217
+ repr(dk)[:220])
218
+ check("D4 reason 文案显式写出「因保护优先未走去重」",
219
+ "保护优先" in g["reason"] and "未走去重" in g["reason"], g["reason"])
220
+ check("D5 落盘行为不变:仍新建节点(保护优先本就不并入)",
221
+ len(cg4.index["nodes"]) == n_before + 1,
222
+ f"{n_before} -> {len(cg4.index['nodes'])}")
223
+
224
+ # 保护优先 + 无近重复:仍标注(结构不变量),但如实说「无近重复」
225
+ r3 = cg4.remember_gated("c_fresh", ccg("奇点事故复盘三", "PASSED 首次观测到未知泄漏,与既有记忆无重叠"),
226
+ layer="contextual", role="user", importance_hint=0.9)
227
+ dk3 = r3["gate"].get("dedup_skipped")
228
+ check("D6 保护优先 + 无近重复:仍带 dedup_skipped 且 duplicate_with 为 None",
229
+ isinstance(dk3, dict) and dk3.get("duplicate_with") is None,
230
+ repr(dk3)[:220])
231
+
232
+ # ============ E. 结构不变量:dedup_skipped 只在 PROTECT 分支出现 ============
233
+ print("\n[E] 结构不变量:dedup_skipped 仅 PROTECT 分支(不多不少)")
234
+ cg5 = MdCGOS(os.path.join(tmp, "e"))
235
+ cg5.add("p_dup", BODY, layer="contextual", importance=0.5)
236
+ cases = [
237
+ ("保护优先(0.9)+ 近重复", BODY, 0.9, True),
238
+ ("保护优先(0.7 边界)+ 全新", ccg("边界探针", "PASSED 全新的边界用例内容"), 0.7, True),
239
+ ("非保护(0.6)+ 近重复", BODY, 0.6, False),
240
+ ("非保护(0.1)+ 近重复", BODY, 0.1, False),
241
+ ]
242
+ for name, content, hint, expect in cases:
243
+ g5 = forgetting.assess(cg5, content, layer="contextual", role="user",
244
+ importance_hint=hint, node_id=None)
245
+ got = g5.get("dedup_skipped") is not None
246
+ check(f"E · {name}", got is expect,
247
+ f"dedup_skipped={'有' if got else '无'}(期望 {'有' if expect else '无'})"
248
+ f" · verdict={g5['verdict']}")
249
+ check("E · entropy 恒带 overwrite_of 键(无覆写时为 None,不新增缺键)",
250
+ "overwrite_of" in g5["entropy"] and g5["entropy"]["overwrite_of"] is None,
251
+ str(g5["entropy"].get("overwrite_of")))
252
+ check("E · prior_node 单点:空 id / 不存在 id 一律 None",
253
+ forgetting.prior_node(cg5, None) is None
254
+ and forgetting.prior_node(cg5, "") is None
255
+ and forgetting.prior_node(cg5, "不存在的_id") is None
256
+ and forgetting.prior_node(cg5, "p_dup") == "p_dup")
257
+
258
+
259
+ def main():
260
+ tmp = tempfile.mkdtemp(prefix="mdcg_p9c_")
261
+ try:
262
+ _run(tmp)
263
+ except Exception:
264
+ traceback.print_exc()
265
+ globals()["FAIL"] += 1
266
+ FAILS.append("未捕获异常")
267
+ finally:
268
+ shutil.rmtree(tmp, ignore_errors=True)
269
+ print(f"\n==== P9c 结果:{PASS} 通过 / {FAIL} 失败 ====")
270
+ if FAILS:
271
+ print("失败项:" + "、".join(FAILS))
272
+ return 1 if FAIL else 0
273
+
274
+
275
+ if __name__ == "__main__":
276
+ raise SystemExit(main())