@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
@@ -47,7 +47,9 @@ import math
47
47
  import os
48
48
  import time
49
49
 
50
- from . import lifecycle, nodefile
50
+ from . import lifecycle, nodefile, protect
51
+ from . import crypto
52
+ from . import security as _security
51
53
  from .fsutil import append_jsonl, atomic_write, publish, read_jsonl
52
54
  from .mdcg import bigrams
53
55
 
@@ -77,6 +79,10 @@ DETERMINISTIC_BASIS = ("data", "measurement", "compiler", "test", "formal_proof"
77
79
 
78
80
  LOG_FILE = "_forgetting.jsonl"
79
81
 
82
+ # 合并面「新正文去向」/「归属并列」的常量(B3 / H3,2026-09-30)
83
+ MERGE_SOURCES_KEEP = 20 # fm.merge_sources 只保留最近 N 条「新写入方归属」
84
+ AGG_MARK = "聚合" # 聚合行标记(与 writelimit.converge_into 同款)
85
+
80
86
 
81
87
  # ---------------------------------------------------------------- 三问
82
88
 
@@ -126,6 +132,48 @@ def payload(content):
126
132
  return "".join(out)
127
133
 
128
134
 
135
+ # 生效条件:node_id 为真值且 cg.index 的 nodes 中已有该 id 时返回该 id(本次写入是覆写既有同 id 节点);node_id 为空、cg 无 index 或取 index 抛异常、id 不在索引中时返回 None。
136
+ def prior_node(cg, node_id):
137
+ """本次写入是否为**覆写既有同 id 节点**——返回既有 id 或 None(存在性判据单点)。
138
+
139
+ P-9b(a) 根因(探针实测):同 id 覆写时 `redundancy(exclude=node_id)` 自排除
140
+ (「不与自身比正文」本身正确),但该层若只此一个节点 ⇒ `compared=0`、
141
+ `duplicate_with=None`、`duplicate_ratio=0.0`——裁决里「覆写」与「全新写入」
142
+ **完全不可区分**(审计面全 null,静默洞)。存在性判据收敛在此一处:`assess`
143
+ 的 entropy 标注与直写路径的返回体共用,不各写一份。
144
+
145
+ 边界(如实):判据读 `cg.index`,与 `redundancy` 同一数据面(同代际假设);
146
+ index 陈旧时最坏是漏标一次覆写提示,不影响任何落盘行为。
147
+ """
148
+ if not node_id:
149
+ return None
150
+ try:
151
+ nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
152
+ except Exception:
153
+ return None
154
+ return node_id if node_id in nodes else None
155
+
156
+
157
+ # 生效条件:node_id 为真值且 cg.get(node_id) 返回真值时返回覆盖度(_coverage(bigrams(payload(content)), bigrams(payload(既有正文))),1.0=逐字同构的重写);node_id 为空、节点取不到、既有正文为空时返回 None。
158
+ def self_coverage(cg, node_id, content):
159
+ """「本次正文 vs 同 id 既有正文」的覆盖度(1.0 = 内容等同的重写)。
160
+
161
+ 与 `redundancy` 共用同一套骨架(payload/bigrams/_coverage),不另写一份
162
+ 重复度算法;差别只在比较对象是**自己**(故不适用 exclude)。用途:同 id
163
+ 覆写时回答「这次是空写还是真改了」(P-9b(a) 提示的主体信息)。
164
+ """
165
+ try:
166
+ node = cg.get(node_id)
167
+ except Exception:
168
+ node = None
169
+ if not node:
170
+ return None
171
+ body = bigrams(payload(node.get("content") or ""))
172
+ if not body:
173
+ return None
174
+ return round(_coverage(bigrams(payload(content)), body), 4)
175
+
176
+
129
177
  # 生效条件:content 经 payload/bigrams 得空集合时直接返回零值 best(max=0.0、with=None、compared=0);否则遍历 cg.index 的 nodes,跳过 nid==exclude,layer 为真值时只比较 str(layer 字段 or "")==layer 的节点,cg.get(nid) 抛异常/返回假值、或该节点 content 的 bigrams 为空则跳过,每计入一个节点后若 n>=limit 立即 break(故 limit 为 0 或负数时只比较首项即停),返回覆盖度最大者 best(无覆盖度提升时不更新 with/jaccard,compared 为实际计入数)。
130
178
  def redundancy(cg, content, layer="contextual", exclude=None, limit=MAX_COMPARE):
131
179
  """Q1 重复?——新内容被既有同层节点覆盖的最大比例。"""
@@ -188,15 +236,39 @@ def importance_score(hint, novelty, kind, content):
188
236
  return {"score": round(max(0.0, min(1.0, s)), 4), "from": "heuristic"}
189
237
 
190
238
 
191
- # 生效条件:以 source_kind(role,verification_basis) 的 kind 与 redundancy(cg,content,layer=layer,exclude=node_id) 的 red["max"] 为输入,按 if/elif 顺序取首个命中分支——imp["score"]≥PROTECT_IMPORTANCE→"ACCEPT";否则 kind=="internal_deterministic" 且 red["max"]≥DUP_DROP→"DROP";否则 red["max"]≥DUP_MERGE→"MERGE";否则 red["max"]≥DUP_DROP 且 imp["score"]<IMPORTANCE_MIN→"DEFER";否则 imp["score"]≥IMPORTANCE_MIN→"ACCEPT";否则 novelty≥NOVELTY_MIN→"ACCEPT";否则→"DEFER"。
239
+ # 生效条件:以 source_kind(role,verification_basis) 的 kind 与 redundancy(cg,content,layer=layer,exclude=node_id) 的 red["max"] 为输入,按 if/elif 顺序取首个命中分支——imp["score"]≥PROTECT_IMPORTANCE→"ACCEPT";否则 kind=="internal_deterministic" 且 red["max"]≥DUP_DROP→"DROP";否则 red["max"]≥DUP_MERGE→"MERGE";否则 red["max"]≥DUP_DROP 且 imp["score"]<IMPORTANCE_MIN→"DEFER";否则 imp["score"]≥IMPORTANCE_MIN→"ACCEPT";否则 novelty≥NOVELTY_MIN→"ACCEPT";否则→"DEFER"。返回体附带 P-9b 可见性字段:entropy.overwrite_of / entropy.overwrite_ratio(同 id 覆写时非 null)、dedup_skipped(**仅 PROTECT 分支**非 null,标注「因保护优先未走去重」+ 去重判据读数)——两者都不改 verdict、不改落盘行为。
192
240
  def assess(cg, content, layer="contextual", role=None, verification_basis=None,
193
241
  importance_hint=None, node_id=None):
194
- """三问 → 四态裁决。返回完整判据(可审计,不只给结论)。"""
242
+ """三问 → 四态裁决。返回完整判据(可审计,不只给结论)。
243
+
244
+ P-9b(2026-09-30):本函数此前有两处**静默洞**(探针实测),都只补可见性、
245
+ 不动裁决语义:
246
+ (a) 同 id 覆写:`redundancy(exclude=node_id)` 自排除 ⇒ 该层只此一节点时
247
+ compared=0、dup 字段全 null,「覆写」与「全新写入」不可区分。处置:
248
+ entropy 增 `overwrite_of`(既有 id)+ `overwrite_ratio`(新正文对既有
249
+ 正文的覆盖度,1.0=空写)——判据单点在 `prior_node`/`self_coverage`。
250
+ (b) importance≥PROTECT_IMPORTANCE 的保护优先分支**排在冗余判定之前** ⇒
251
+ 高重要度的近重复内容直接 ACCEPT,绕过 MERGE/DROP 而审计面只剩一句
252
+ 「触发不可遗忘保护」。处置:该分支返回 `dedup_skipped`
253
+ (reason=protect_importance + duplicate_with/duplicate_ratio/compared +
254
+ overwrite_of),并在 reason 文案里显式写出「因保护优先未走去重」。
255
+ **为何不把 PROTECT 挪到冗余判定之后**(依据):① 该分支语义是「不可
256
+ 遗忘保护」(PROTECT_IMPORTANCE 与 writelimit.py 模块头注 :9-12 的
257
+ 「importance_hint≥0.7 保护优先」是跨模块同一口径,且 writelimit 是
258
+ 文档化设计不许动);挪位后高重要度内容会被 DROP(内部确定性来源)或
259
+ MERGE 并入低重要度节点,**削弱的正是保护本身**,且造成两模块对同一
260
+ 阈值的语义分叉;② DROP 分支的伤害(重要内容被丢弃)不可逆,MERGE 会把
261
+ 关键正文并入他节点、检索归属漂移——风险高于收益;③ 缺陷本体是「绕过
262
+ 去重而不可见」而非「保护存在」,标注即可消除静默且零回归面
263
+ (test_p9_forget_protect.py 的「importance_hint≥0.7→ACCEPT」逐字不变)。
264
+ """
195
265
  kind = source_kind(role, verification_basis)
196
266
  red = redundancy(cg, content, layer=layer, exclude=node_id)
197
267
  novelty = round(1.0 - red["max"], 4)
198
268
  bits = round(self_information(red["max"]), 4)
199
269
  imp = importance_score(importance_hint, novelty, kind, content)
270
+ # P-9b(a):同 id 覆写的存在性/覆盖度(不与自身比的 redundancy 之外的另一读法)
271
+ prior = prior_node(cg, node_id)
200
272
  entropy = {
201
273
  "source_kind": kind,
202
274
  "novelty": novelty,
@@ -205,11 +277,38 @@ def assess(cg, content, layer="contextual", role=None, verification_basis=None,
205
277
  "duplicate_with": red["with"],
206
278
  "duplicate_ratio": round(red["max"], 4),
207
279
  "compared": red["compared"],
280
+ # 非 null = 本次是覆写既有同 id 节点(原先此处无法与全新写入区分)
281
+ "overwrite_of": prior,
282
+ "overwrite_ratio": (self_coverage(cg, prior, content)
283
+ if prior is not None else None),
208
284
  }
209
285
 
286
+ dedup_skipped = None
210
287
  if imp["score"] >= PROTECT_IMPORTANCE:
288
+ # P-9b(b):保护优先分支先于冗余判定——近重复/覆写时不静默,给出去重读数
289
+ due = []
290
+ if prior is not None:
291
+ due.append("覆写既有同 id 节点 %s(正文覆盖度 %s)"
292
+ % (prior, "—" if entropy["overwrite_ratio"] is None
293
+ else "%.2f" % entropy["overwrite_ratio"]))
294
+ if red["with"] and red["max"] >= DUP_MERGE:
295
+ due.append("与 %s 重复度 %.2f≥%.2f(够 MERGE 阈值)"
296
+ % (red["with"], red["max"], DUP_MERGE))
211
297
  verdict, why = "ACCEPT", (f"重要度 {imp['score']:.2f}≥{PROTECT_IMPORTANCE}"
212
298
  f"(触发不可遗忘保护)")
299
+ dedup_skipped = {
300
+ "reason": "protect_importance",
301
+ "detail": ("保护优先分支排在冗余判定之前:本次未走 MERGE/DROP"
302
+ + ("(" + ";".join(due) + ")" if due else
303
+ "(重复度 %.2f<%.2f 且非覆写,无近重复)"
304
+ % (red["max"], DUP_MERGE))),
305
+ "duplicate_with": red["with"] if red["max"] >= DUP_MERGE else None,
306
+ "duplicate_ratio": round(red["max"], 4),
307
+ "compared": red["compared"],
308
+ "overwrite_of": prior,
309
+ }
310
+ if due:
311
+ why += ";⚠ 因保护优先未走去重(" + ";".join(due) + ")"
213
312
  elif kind == "internal_deterministic" and red["max"] >= DUP_DROP:
214
313
  verdict, why = "DROP", (f"确定性内部产生且冗余 {red['max']:.2f}≥{DUP_DROP}"
215
314
  f"(低熵噪音,不编码)")
@@ -228,7 +327,9 @@ def assess(cg, content, layer="contextual", role=None, verification_basis=None,
228
327
  verdict, why = "DEFER", "重要度与新信息均不足判据(待定)"
229
328
 
230
329
  return {"verdict": verdict, "reason": why, "redundancy": red,
231
- "importance": imp, "entropy": entropy}
330
+ "importance": imp, "entropy": entropy,
331
+ # P-9b(b) 可见性字段:仅 PROTECT 分支非 null(其余分支本来就去重/裁决)
332
+ "dedup_skipped": dedup_skipped}
232
333
 
233
334
 
234
335
  # ---------------------------------------------------------------- 落库动作
@@ -243,12 +344,151 @@ def log(cg, rec):
243
344
  return rec
244
345
 
245
346
 
246
- # 生效条件:cg 与 node_id 必填,delta 默认 0.05;cg.get(node_id) 抛异常或返回假值时返回 None;imp 跨过 PROTECT_IMPORTANCE 即写 protected。
247
- def reinforce(cg, node_id, delta=0.05):
347
+ # 生效条件:body 与 content 必传(None 按空串);把 content 逐行空白归一去重、剔除已出现在 body 中的行;delta 为空(零新增信息)时返回 (None, None, None),否则返回 (追加后的正文, 追加行, 行号)。
348
+ def aggregate_line(body, content, stamp=None):
349
+ """MERGE 聚合行(**单点**):新正文里「目标正文还没有的」行 → 追加一行。
350
+
351
+ B3(2026-09-30)根因:近重复合并(`forgetting.reinforce`,MERGE)与同构
352
+ 聚合(`writelimit.converge_into`,CONVERGE)都只改 frontmatter
353
+ (importance/merge_count),新正文**逐字节丢弃**——「近重复但关键值不同」
354
+ 的第二次写入在库里 0 命中(探针实测:新值 9595 全库 0 命中、节点未落盘)。
355
+ 本函数是「新正文怎么进目标正文」的**唯一实现点**:两处落库动作共用,
356
+ 第二处委托调用(硬约束:单点优先),不各自再写一份追加口径。
357
+
358
+ 与 `converge_into` 旧口径(`" ".join(content.split())[:80]`)的两点差别,
359
+ 均为**收窄信息损失**而非放宽判据:
360
+ · 不截断——旧口径把新值丢在 80 字外即整体丢失(P-3 实测:端口 9090
361
+ 变体在库中 0 次落盘),而「新值必须在库里可检索」正是本修的目标;
362
+ · 只追加 delta——旧口径无条件追加,逐字重复的再写也会让目标正文无限
363
+ 增长。按「归一化行是否已出现在目标正文(含已追加的聚合行)中」判,
364
+ 纯重复零追加(不增长),带新信息的变体各追加一次(新值原样落盘,
365
+ 可被 read/search 命中)。**幂等性靠子串判据**:聚合行自带
366
+ `- 【聚合 …】` 前缀,故比对用的是「归一化后的整段目标正文」而非
367
+ 逐行集合——否则第二次同文再写会因前缀差异重复追加。
368
+ """
369
+ have = " ".join((body or "").split())
370
+ delta, seen = [], set()
371
+ for l in (content or "").splitlines():
372
+ s = " ".join(l.split())
373
+ if not s or s in seen or s in have:
374
+ continue
375
+ seen.add(s)
376
+ delta.append(s)
377
+ if not delta:
378
+ return None, None, None
379
+ stamp = stamp or time.strftime("%m-%d %H:%M", time.localtime())
380
+ line = f"- 【{AGG_MARK} {stamp}】" + ";".join(delta)
381
+ new_body = ((body or "") + "\n" if body else "") + line
382
+ return new_body, line, len(new_body.splitlines())
383
+
384
+
385
+ # 生效条件:session 与 actor 均为假值(None/空串等)时返回 None 且不改 fm;否则把 {"session","actor","t"} 追加进 fm["merge_sources"](只保留最近 MERGE_SOURCES_KEEP 条)并返回该条。
386
+ def note_source(fm, session=None, actor=None, t=None):
387
+ """H3 归属并列(**单点**):把「新写入方」的归属记进目标 frontmatter。
388
+
389
+ 跨会话合并的处置选择与依据(探针读数:合并判定完全不看会话——`redundancy`
390
+ 按同层全量比、`writelimit.check` 的键是 `f"{layer}:{role}"`,两处都无会话维;
391
+ 实测 B 会话的近重复被并入 A 会话节点,B 的内容与归属双双消失):
392
+ · **不动合并判定**(不按会话切分)。依据:同一事实在另一会话再次出现
393
+ 仍是「又一次见到」= 确认信号,正是冗余闸门的既有语义;按会话切分会让
394
+ 同一事实在每个会话各长一个节点,与「噪声丢掉、骨架留下」的目的相悖。
395
+ · **把双方归属并列落 fm**:目标自己的 `fm.session` 原样保留,新写入方
396
+ 记进 `fm.merge_sources`(保序、有界)。召回侧据此可识别「这条记忆被
397
+ 哪些会话/主体确认过」——当前会话的新内容不再无名无姓地消失。
398
+ """
399
+ if not session and not actor:
400
+ return None
401
+ src = {"session": None if session is None else str(session),
402
+ "actor": None if actor is None else str(actor),
403
+ "t": float(t if t is not None else time.time())}
404
+ rows = list(fm.get("merge_sources") or [])
405
+ rows.append(src)
406
+ fm["merge_sources"] = rows[-MERGE_SOURCES_KEEP:]
407
+ return src
408
+
409
+
410
+ # 生效条件:cg 与 node_id 必传,content 为本次被丢弃的正文,target 为近重复目标,verdict 为 forgetting.assess 的判据字典;恒写一条 verdict=="DROP" 的留痕并返回「去向单」(含 trace_id / 全文或摘要 / 检索入口);密级属 crypto.ENCRYPTED_LEVELS 时 dropped_content 为 None 且 content_redacted 为 True(明文不落留痕)。
411
+ def record_drop(cg, content, node_id, target=None, verdict=None, layer=None,
412
+ sensitivity=None, session=None, actor=None):
413
+ """DROP 去向留痕(**单点**,B3-④):被丢的新正文「去哪了」。
414
+
415
+ 为什么需要它:`assess` 的 `:214` 分支(internal_deterministic 且
416
+ dup≥DUP_DROP,0.60)**先于** MERGE 分支命中,于是「近重复但关键值不同」的
417
+ 确定性内部写入走 DROP,连既有节点都不强化——新正文只在日志里留一个
418
+ 「低熵噪音,不编码」,正文本身零去向(探针实测:全库 0 命中)。
419
+
420
+ 处置:**不追加进目标正文**(该分支的既有语义是「例行输出不该污染既有
421
+ 记忆」,不能靠追加把它变相强化成 MERGE),改为把新正文**全文**与去向目标
422
+ 一并落进 `_forgetting.jsonl`,返回单给出可检索的句柄(trace_id / 文件 /
423
+ sha1)。密级为加密级时不落明文(与写面 `mdcg._check_sensitivity_landing`
424
+ 的「加密级却明文落盘」fail-closed 同口径)——只留摘要与长度,返回单显式
425
+ 声明这一处取舍,不假装全文可检索。
426
+ """
427
+ flat = " ".join((content or "").split())
428
+ digest = hashlib.sha1(flat.encode("utf-8")).hexdigest()[:12] if flat else ""
429
+ enc = bool(flat) and str(sensitivity or "").strip().lower() \
430
+ in crypto.ENCRYPTED_LEVELS
431
+ t = time.time()
432
+ trace_id = f"drop-{node_id}-{int(t * 1000)}"
433
+ log(cg, {"t": t, "node_id": node_id, "layer": layer, "verdict": "DROP",
434
+ "reason": (verdict or {}).get("reason", ""),
435
+ "importance": (verdict or {}).get("importance"),
436
+ "entropy": (verdict or {}).get("entropy"),
437
+ "duplicate_with": target, "actor": actor, "session": session,
438
+ "trace_id": trace_id, "content_sha1": digest,
439
+ "content_chars": len(flat),
440
+ "dropped_content": None if enc else (flat or None),
441
+ "content_redacted": enc})
442
+ sink = {"verdict": "DROP", "duplicate_with": target, "trace_id": trace_id,
443
+ "kept_in": LOG_FILE,
444
+ "retrieval": (f"{LOG_FILE} 内 trace_id={trace_id}"
445
+ f"(forgetting_history 读回;节点未落盘,"
446
+ f"按 N229 读可见性过滤仅治理面可见)"),
447
+ "content_sha1": digest, "content_chars": len(flat),
448
+ "content_kept": bool(flat) and not enc}
449
+ if enc:
450
+ sink["redacted_reason"] = ("密级为加密级:明文不入留痕(与写面"
451
+ "「加密级却明文落盘」fail-closed 同口径)")
452
+ return sink
453
+
454
+
455
+ # 生效条件:cg 与 node_id 必填,delta 默认 0.05,content 默认 None(不传即与改动前逐位一致:只改 frontmatter、正文原样写回);cg.get(node_id) 抛异常或返回假值时返回 None;随后以节点真层/敏感度过 protect.guard_overwrite(层闸 + 保护闸,override 为真时先快照 + 审计);imp 跨过 PROTECT_IMPORTANCE 即写 protected;content 非 None 时按 forgetting.aggregate_line 把「目标正文没有的」行追加为聚合行(零新增信息则不追加),session/actor 非空时按 forgetting.note_source 并列记归属;写盘后同步内存索引条目并**标脏**(cg._dirty[node_id]=e,推进读缓存代际与索引增量日志);返回体带 content_sink(新正文去向:节点/行/行号)与 source(归属)。
456
+ def reinforce(cg, node_id, delta=0.05, override=False, actor=None,
457
+ content=None, session=None):
248
458
  """MERGE 的落库动作:不新增节点,把「又一次见到」折算成既有节点的强化。
249
459
 
250
460
  重要性 +delta,merge_count +1;一旦跨过 0.7 自动打上保护标记
251
461
  (对齐「importance 提升(保护:不可遗忘…且受保护标记)」)。
462
+
463
+ B3(2026-09-30):本函数此前**没有 content 槽**——MERGE 时新正文 100%
464
+ 丢弃(写回的是 `node.get("content")`,即既有正文),调用方手上正握着
465
+ 新正文却无处可传。现加 `content=None`(**缺省行为与改动前逐位一致**:
466
+ 不传即正文原样写回,既有调用零破坏);MERGE 时按 `aggregate_line`
467
+ (单点,与 `writelimit.converge_into` 共用)把新正文里目标还没有的行
468
+ 追加为聚合行,`merge_count` 口径不变。注意**不动 importance 之外的既有
469
+ 语义**:聚合行是「又一次见到 + 新值留痕」,不是新增节点、不改层、不改
470
+ 密级、不绕保护闸(`guard_overwrite` 仍在本函数内、写盘之前,见 :278)。
471
+
472
+ H3(2026-09-30):`session` 槽把「新写入方归属」并列记进目标
473
+ frontmatter(`note_source`,单点)——跨会话合并时目标自己的
474
+ `fm.session` 保留,新写入方记进 `fm.merge_sources`。
475
+
476
+ C-1(2026-09-29):写盘后**标脏**(`cg._dirty[node_id] = e`,对照
477
+ `md_cg/mdcg.py` 的 update_tags / verify 直写分支先例)——不标脏时
478
+ `_dirty.path_gen` 不推进,默认开(`MDCG_READ_CACHE=1`)的读缓存会把写盘前
479
+ 旧 fm 判新鲜,同进程「reinforce 后读」永久拿到旧 importance(FI-M03 实测)。
480
+
481
+ N214(2026-09-28,同族未接线写点):本函数此前**裸调 `cg._write_node`**
482
+ 覆写既有节点,两个入口(`mdcos.remember_gated` 的 MERGE 分支 :3380、
483
+ `mdcos.maintain(action="prefeed", write=True)` 的 reinforce 分支 :2783)
484
+ 全程无 principal 层闸、无保护闸、无快照无审计——同一身份对**同层**的 `add` 被
485
+ `AccessDenied`,而此路照样覆写(持 write 的 reflect 令牌以 `layer='self'`
486
+ 发 `mdcg_remember(gated=true)`:`writelimit.check` 因形参层≠contextual 放行、
487
+ `assess` 按形参层比得重复度 1.0 → MERGE → 改写 self 层节点的
488
+ importance/merge_count 并自动打保护位)。落盘前统一过
489
+ `protect.guard_overwrite`(单点,对照 `MdCGSecure.add` 的层闸 + `MdCG.add`
490
+ 的保护闸),`override=True` 时先快照进 `_protected_history` 并写
491
+ `_protected_audit.jsonl`。
252
492
  """
253
493
  try:
254
494
  node = cg.get(node_id)
@@ -257,6 +497,9 @@ def reinforce(cg, node_id, delta=0.05):
257
497
  if not node:
258
498
  return None
259
499
  fm = node.get("frontmatter") or {}
500
+ protect.guard_overwrite(cg, node_id, layer=fm.get("layer"),
501
+ sensitivity=fm.get("sensitivity"),
502
+ override=override, actor=actor)
260
503
  imp = min(1.0, float(fm.get("importance") or 0.5) + delta)
261
504
  fm["importance"] = imp
262
505
  fm["merge_count"] = int(fm.get("merge_count") or 0) + 1
@@ -271,8 +514,16 @@ def reinforce(cg, node_id, delta=0.05):
271
514
  # 幂等 no-op(不写字段、不留痕)。protected 只豁免**降级**,回升不受限。
272
515
  lifecycle.stamp(fm, "active", reason="MERGE 重复强化(回升)",
273
516
  actor="forgetting:reinforce")
517
+ # B3:新正文去向(content 缺省 None → new_body 为 None → 正文原样写回,
518
+ # 与改动前逐位一致)。聚合行只补「目标还没有的」行,纯重复零追加。
519
+ new_body, line, line_no = (None, None, None)
520
+ if content is not None:
521
+ new_body, line, line_no = aggregate_line(node.get("content"), content)
522
+ # H3:归属并列(新写入方 → fm.merge_sources;目标自己的 fm.session 不动)
523
+ source = note_source(fm, session=session, actor=actor)
274
524
  cg._write_node(node_id, os.path.join(cg.root, node["path"]),
275
- fm, node.get("content") or "")
525
+ fm, new_body if new_body is not None
526
+ else (node.get("content") or ""))
276
527
  e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id)
277
528
  if e is not None:
278
529
  e["importance"] = imp
@@ -281,13 +532,55 @@ def reinforce(cg, node_id, delta=0.05):
281
532
  if fm.get("protected"):
282
533
  e["protected"] = True
283
534
  e["protection_reason"] = fm["protection_reason"]
535
+ # H3 索引镜像(2026-09-30):merge_sources 同款镜像(对照组
536
+ # `writelimit.converge_into` :257-258 的 `entry["merge_sources"]`)。
537
+ # 此前本段只同步 importance / lifecycle state / protected,盘面 fm 已
538
+ # 有新写入方归属而同进程索引条目读到 None,直到索引重载——与
539
+ # 「写盘后标脏、同进程读到新值」的既有纪律(C-1 / N133 同族,见下方
540
+ # 标脏注释)不一致:CONVERGE 落点(converge_into)与 MERGE 落点
541
+ # (reinforce)的读面行为必须同口径。
542
+ if fm.get("merge_sources"):
543
+ e["merge_sources"] = fm["merge_sources"]
544
+ # 标脏(N133 修复,对照先例 mdcg.py 的 update_tags / verify 直写分支
545
+ # `self._dirty[node_id] = e`):只改内存 entry 时 path_gen 不推进,
546
+ # 读缓存(默认开,MDCG_READ_CACHE=1)按 `_fresh` 把写盘前的旧 fm 判
547
+ # 为新鲜——同进程「写后读」永久拿到旧 importance(FI-M03 实测)。
548
+ # 标脏同时让本写进 `_dirty → flush → _index_log` 重放(重启后索引
549
+ # 与盘面不再漂移)。**不把标脏下沉进 `_write_node`**:该函数另有
550
+ # 「只对账索引、不落盘」的调用方(backfill 对账支路等),下沉会让
551
+ # 它们凭空产生一次写入代际与自重载;此处按既有 48 处显式标脏同款
552
+ # 补,口径与 `_sync_edge_entry` 一致。
553
+ _dirty = getattr(cg, "_dirty", None)
554
+ if isinstance(_dirty, dict):
555
+ _dirty[node_id] = e
284
556
  return {"node_id": node_id, "importance": imp,
285
- "merge_count": fm["merge_count"], "protected": bool(fm.get("protected"))}
557
+ "merge_count": fm["merge_count"], "protected": bool(fm.get("protected")),
558
+ # 新正文去向(②):写进了哪个节点/哪一行——调用方不必猜。
559
+ "content_sink": {
560
+ "node_id": node_id,
561
+ "action": ("appended" if line else
562
+ ("already_covered" if content is not None
563
+ else "not_provided")),
564
+ "line": line, "line_no": line_no,
565
+ "chars": len(new_body if new_body is not None
566
+ else (node.get("content") or ""))},
567
+ "source": source}
568
+
569
+
570
+ # 生效条件:cg 必填,limit 默认 100;日志路径不存在时返回 [];否则逐行 json.loads 后按行所属 node_id 做读可见性过滤(security.node_visible 为假的行整条剔除),再返回 out[-limit:](limit=0 时 -0 退化为 out[0:] 即全量)。
571
+ def history(cg, limit=100):
572
+ """读取遗忘留痕(最近 limit 条,**按节点读可见性过滤**)。
286
573
 
574
+ N229(2026-09-28):此前逐行 json.loads 原文返回(actor/verdict/reason/
575
+ importance/layer/duplicate_with…),零可见性、零归属过滤 ⇒ 任何持 read op
576
+ 的身份(含无令牌 guest 经 `mdcg_forgetting_history`)读走全库他人的写入
577
+ 闸留痕,并可借 `duplicate_with` 枚举 private 目标 id。口径与库读面统一:
578
+ 逐行按 `security.node_visible`(跨层单点)判定行所属节点对本身份是否可见,
579
+ 不可见/已删(索引无条目)→ 整条不出;设计者豁免(治理面本职)。
287
580
 
288
- # 生效条件:cg 必填,limit 默认 100;日志路径不存在时返回 [];否则返回 out[-limit:],limit=0 时 -0 退化为 out[0:] 即全量。
289
- def history(cg, limit=100):
290
- """读取遗忘留痕(最近 limit 条)。"""
581
+ 如实标注:行内 `duplicate_with` 指向的**另一**节点不做二次判定(那是
582
+ 「去重目标」,与行所属节点不同源)——本轮按行所属节点收口,见报告残留。
583
+ """
291
584
  p = os.path.join(cg.root, LOG_FILE)
292
585
  if not os.path.exists(p):
293
586
  return []
@@ -298,9 +591,12 @@ def history(cg, limit=100):
298
591
  line = line.strip()
299
592
  if line:
300
593
  try:
301
- out.append(__import__("json").loads(line))
594
+ r = __import__("json").loads(line)
302
595
  except Exception:
303
596
  continue
597
+ if not _security.node_visible(cg, (r or {}).get("node_id")):
598
+ continue
599
+ out.append(r)
304
600
  except Exception:
305
601
  return []
306
602
  return out[-limit:]
@@ -388,7 +684,7 @@ def longterm_assess(cg, apply=False, out=None, layer=None, keep=LONGTERM_KEEP,
388
684
  `current.json` 指针。幂等:断面内容相同则跳过重写(除非 force=True)。
389
685
  """
390
686
  nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}
391
- ids = sorted(nid for nid, e in nodes.items()
687
+ ids = sorted(nid for nid, e in list(nodes.items())
392
688
  if not layer or e.get("layer") == layer)
393
689
  if max_rows:
394
690
  ids = ids[:int(max_rows)]