@furongjun1999/dsh-memory 0.6.1 → 0.7.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 (105) hide show
  1. package/README.md +49 -18
  2. package/docs/README.md +1 -0
  3. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
  4. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
  5. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
  6. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
  7. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
  8. package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
  9. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
  10. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
  11. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
  12. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
  13. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
  14. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
  15. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
  16. 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
  17. 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
  18. package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
  19. 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
  20. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
  21. 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
  22. 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 +32 -2
  23. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  24. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  25. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  26. 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
  27. package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
  28. 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
  29. package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
  30. package/md_cg/admission.py +718 -0
  31. package/md_cg/autonomy_modes.py +642 -0
  32. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  33. package/md_cg/chain.py +47 -0
  34. package/md_cg/consistency.py +133 -8
  35. package/md_cg/forgetting.py +38 -6
  36. package/md_cg/freshness.py +527 -0
  37. package/md_cg/generation.py +409 -0
  38. package/md_cg/hotcache.py +4 -1
  39. package/md_cg/lifecycle.py +30 -2
  40. package/md_cg/mcp_server.py +191 -26
  41. package/md_cg/mdcg.py +594 -39
  42. package/md_cg/mdcos.py +869 -38
  43. package/md_cg/nodefile.py +74 -1
  44. package/md_cg/protect.py +79 -4
  45. package/md_cg/provenance.py +1 -1
  46. package/md_cg/review_cli.py +31 -2
  47. package/md_cg/rollback.py +411 -0
  48. package/md_cg/rollback_cli.py +104 -0
  49. package/md_cg/semantic/canonical.py +22 -0
  50. package/md_cg/semantic/unify.py +73 -22
  51. package/md_cg/semantic/unify_fixture.json +25 -0
  52. package/md_cg/sleep.py +1297 -0
  53. package/md_cg/stg.py +400 -50
  54. package/md_cg/stgidx.py +281 -0
  55. package/md_cg/sustain.py +146 -9
  56. package/md_cg/test_auto_defaults.py +424 -0
  57. package/md_cg/test_autonomy_admission.py +1098 -0
  58. package/md_cg/test_autonomy_modes.py +1972 -0
  59. package/md_cg/test_b1_auto_id_multiproc.py +7 -0
  60. package/md_cg/test_b1b2_write_face.py +7 -0
  61. package/md_cg/test_b3_merge_keeps_content.py +7 -0
  62. package/md_cg/test_boundary_hit.py +410 -0
  63. package/md_cg/test_cons200_scan_selection.py +791 -0
  64. package/md_cg/test_en_pipeline.py +22 -13
  65. package/md_cg/test_generation_guard.py +352 -0
  66. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  67. package/md_cg/test_i50a_half_dup_defer.py +408 -0
  68. package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
  69. package/md_cg/test_i50c_meta_passthrough.py +608 -0
  70. package/md_cg/test_i50d_importance_source.py +531 -0
  71. package/md_cg/test_i50e_readside_protection.py +656 -0
  72. package/md_cg/test_issue39_utf8_stdio.py +9 -1
  73. package/md_cg/test_issue52_scan_condition_first.py +677 -0
  74. package/md_cg/test_linkref.py +7 -0
  75. package/md_cg/test_mode_parity.py +1393 -0
  76. package/md_cg/test_mutation_rollback.py +805 -0
  77. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
  78. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  79. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
  80. package/md_cg/test_n230_dirty_replay.py +375 -0
  81. package/md_cg/test_p2_mcp.py +8 -0
  82. package/md_cg/test_p2_six_elements.py +463 -0
  83. package/md_cg/test_p3_legacy_closure.py +433 -0
  84. package/md_cg/test_p4_freshness.py +680 -0
  85. package/md_cg/test_p8_subgraph_chain.py +14 -1
  86. package/md_cg/test_p9_forget_protect.py +7 -0
  87. package/md_cg/test_p9c_dedup_hints.py +7 -0
  88. package/md_cg/test_policy_required_ccg.py +5 -1
  89. package/md_cg/test_protocol.py +7 -0
  90. package/md_cg/test_rank_parity_score_mode.py +6 -0
  91. package/md_cg/test_semantic_canonical.py +5 -3
  92. package/md_cg/test_sleep.py +611 -0
  93. package/md_cg/test_sleep_p1.py +784 -0
  94. package/md_cg/test_stgidx_index_parity.py +1020 -0
  95. package/md_cg/test_time_core_lint.py +968 -0
  96. package/md_cg/test_unify_default_off.py +701 -0
  97. package/md_cg/test_unify_scope.py +182 -0
  98. package/md_cg/test_writelimit.py +36 -9
  99. package/md_cg/test_writepipe.py +5 -2
  100. package/md_cg/weights.py +15 -1
  101. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  102. package/md_cg/writepipe.py +101 -2
  103. package/package.json +3 -2
  104. package/skills/plugin.json +1 -1
  105. package/utf8_boot.py +237 -0
package/md_cg/mdcos.py CHANGED
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
 
21
21
  import hashlib
22
22
  import json
23
+ import math
23
24
  import os
24
25
  import re
25
26
  import time
@@ -30,11 +31,13 @@ from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT
30
31
  TIER_BUCKET_SCAN, TIER_GLOBAL_LIKE, TIER_GLOBAL_SCAN,
31
32
  GLOBAL_CAP, expand_query_terms_weighted,
32
33
  expand_query_terms_llm, en_zh_bigrams, semantic_on,
33
- cut_by_relevance, apply_retrieval_gates, NEG_ROUTE_LAYERS)
34
+ cut_by_relevance, apply_retrieval_gates, NEG_ROUTE_LAYERS,
35
+ # P2-3(§5.4):六要素后两行索引键的召回判据单点(mdcg 侧)
36
+ index_key_hits, boundary_mark)
34
37
  from . import (nodefile, routing, chain, subgraph, forgetting, protect,
35
38
  identity, consistency, metacognition, crypto, sustain,
36
39
  self_state, predict, evolution, weights, pooling,
37
- writelimit, reach, trust, roleviews)
40
+ writelimit, reach, trust, roleviews, autonomy_modes)
38
41
  from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
39
42
  read_jsonl_tail, count_jsonl, publish)
40
43
  from .security import (Principal, TenantRegistry, AccessDenied,
@@ -64,6 +67,37 @@ DEFAULT_BUDGET = 1200 # recall 默认 token 预算
64
67
  # 0 = 关闭截断,回到"超大一律跳过"的旧行为。
65
68
  DEFAULT_MAX_ITEM_TOKENS = 250
66
69
 
70
+ # ---- P3-temporal:时间路的核与口径(设计稿 §6.3,裁定 12)-------------------
71
+ # 核**必须**来自唯一权威 `md_cg/whitebox_kb/aeis_core/time_core.py::cred_factor`
72
+ # (该模块 docstring 逐字点名 `exp(-t/τ)` 为 bug);本模块**不写任何指数核**。
73
+ #: γ(衰减率)可配 env —— 与 `MDCG_SPREAD_DECAY` 同款:缺失/非法即回落缺省。
74
+ TEMPORAL_GAMMA_ENV = "MDCG_TEMPORAL_GAMMA"
75
+ #: 半衰期真源 = `links.DECAY_DAYS`(30 天,`md_cg/links.py:53`)——不复制字面量,
76
+ #: 由 `temporal_gamma()` 现取;此处只写明「γ 缺省 = ln2 / 半衰期」这一换算。
77
+ #: 单位口径(**最易错处**):γ 是「每天」的速率,故 Δt 必须**以天为单位**喂入。
78
+ TEMPORAL_DT_UNIT = "day"
79
+ #: 分数下限(floor):`cred_factor(gamma, dt, floor, 1.0)` 的 floor。
80
+ #: 取值理由:0.5**10 ≈ 9.77e-4 < 1e-3 —— 30 天半衰期下 300 天(10 个半衰期)
81
+ #: 之后衰减已落到 1e-3 以下,继续区分无信息量;floor=1e-3 让**老节点/缺
82
+ #: created_at 的节点**仍带非零分进入本路,而不是被乘成 0 后不可召回
83
+ #:(读数见 `md_cg/test_time_core_lint.py` 的 G4 floor 组)。
84
+ TEMPORAL_SCORE_FLOOR = 0.001
85
+
86
+
87
+ # 生效条件:environ(缺省 os.environ)里 TEMPORAL_GAMMA_ENV 为真值且 float() 可解析且 > 0 时返回该值;缺失/空串/不可解析/非正数一律回落 `math.log(2)/links.DECAY_DAYS`(缺省半衰期 30 天)。
88
+ def temporal_gamma(environ=None) -> float:
89
+ """时间邻近度衰减率 γ(**每天**)的唯一读取点。"""
90
+ raw = (environ or os.environ).get(TEMPORAL_GAMMA_ENV)
91
+ if raw:
92
+ try:
93
+ g = float(raw)
94
+ except (TypeError, ValueError):
95
+ g = 0.0
96
+ if g > 0:
97
+ return g
98
+ from .links import DECAY_DAYS # 半衰期唯一真源(links.py:53,30 天)
99
+ return math.log(2.0) / float(DECAY_DAYS)
100
+
67
101
 
68
102
  # 生效条件:text 为假值(None/空串)时返回 0,否则按「CJK 0.6/字 + 其余 /4」计算并返回 int(cjk*0.6 + other/4) + 1。
69
103
  def est_tokens(text: str) -> int:
@@ -846,8 +880,9 @@ class MdCGOS(MdCG):
846
880
  详见 _candidates。
847
881
  """
848
882
  q = (query or "").strip()
849
- # 批次 15 统一口径(unify.py):任意语言 query → 标准原子序列;
850
- # 与 MdCG.search/search_rrf 三入口同口径(MDCG_UNIFY_QUERY=0 可关)
883
+ # 批次 15 统一口径(unify.py,2026-09-30 收窄作用域):只译**英文内容**
884
+ # → 标准原子序列,中文段逐字原样(不再整条归一);与
885
+ # MdCG.search/search_rrf 三入口同口径(归一层缺省关,显式 =1 才开)
851
886
  from .semantic.unify import unify_query
852
887
  q = unify_query(q)
853
888
  if not q:
@@ -940,7 +975,9 @@ class MdCGOS(MdCG):
940
975
  if in_bucket:
941
976
  docs = self._read_many(in_bucket, stat)
942
977
  # 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
943
- hits = [d for d in docs if self._like(d[2], d[1], terms)
978
+ hits = [d for d in docs
979
+ if self._like(d[2], d[1], terms,
980
+ index_key_hits(d[0], terms, q))
944
981
  or (semantic_on() and d[1].get("semantic"))]
945
982
  out = try_stage(hits, TIER_BUCKET_LIKE)
946
983
  if out:
@@ -966,7 +1003,8 @@ class MdCGOS(MdCG):
966
1003
  docs_r = self._read_many(reach_entries, stat)
967
1004
  _dif = set(_rstat.get("reach_diffused_paths") or ())
968
1005
  hits_r = [d for d in docs_r
969
- if self._like(d[2], d[1], terms)
1006
+ if self._like(d[2], d[1], terms,
1007
+ index_key_hits(d[0], terms, q))
970
1008
  or (semantic_on() and d[1].get("semantic"))
971
1009
  or d[0].get("path") in _dif] # 图扩散补召回:无词面命中也放行进打分
972
1010
  stat["pre_cap"] = len(hits_r) # 与 T2 同序:截断**前**的候选数
@@ -998,7 +1036,9 @@ class MdCGOS(MdCG):
998
1036
  # 截断依据=相关度(同 MdCG.search:cap 值不变,改的是拿什么排序)
999
1037
  docs_all = self._read_many(entries, stat)
1000
1038
  # 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
1001
- hits = [d for d in docs_all if self._like(d[2], d[1], terms)
1039
+ hits = [d for d in docs_all
1040
+ if self._like(d[2], d[1], terms,
1041
+ index_key_hits(d[0], terms, q))
1002
1042
  or (semantic_on() and d[1].get("semantic"))]
1003
1043
  stat["pre_cap"] = len(hits)
1004
1044
  stat["cap"] = GLOBAL_CAP
@@ -1046,7 +1086,9 @@ class MdCGOS(MdCG):
1046
1086
  docs = self._read_many(entries, stat)
1047
1087
  # 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池——
1048
1088
  # 语义摘要=检索面(设想核心),否则摘要层只在 LIKE 全空时生效
1049
- hits = [d for d in docs if self._like(d[2], d[1], terms)
1089
+ hits = [d for d in docs
1090
+ if self._like(d[2], d[1], terms,
1091
+ index_key_hits(d[0], terms, query))
1050
1092
  or (semantic_on() and d[1].get("semantic"))]
1051
1093
  if not hits:
1052
1094
  # 兜底池(LIKE 全空 = 无相关度信号):截断依据=importance/created_at
@@ -1158,7 +1200,8 @@ class MdCGOS(MdCG):
1158
1200
  # 生效条件:当 seeds 非空且 seed_map 非空时,用 relation_types 或 CHAIN_TYPES_DEFAULT、max_depth 或 MAX_DEPTH_DEFAULT、decay 调用 chain.expand_from_seeds,仅保留 best 中仍存在于 entries(按 id(e))的节点,读取成功者加入 scored 并按分数降序返回 (scored, prov);seeds/seed_map/best 为空返回 ([], {});
1159
1201
  def _path_chain(self, query, entries, seeds, context=None,
1160
1202
  relation_types=None, max_depth=None, decay=0.9):
1161
- """关系链路径:沿 causal/sequential/applies_to 边**多跳**扩散。
1203
+ """关系链路径(= 设计稿 §6.2 的**因果路**):沿 causal/sequential/
1204
+ applies_to 边**多跳**扩散。
1162
1205
 
1163
1206
  理论依据:`causal` = 条件依赖因果(A 是 B 成立的条件),
1164
1207
  `dex_chain` 沿 causal 边正向展开、每步标注条件,**链 = 条件序列**。
@@ -1170,6 +1213,13 @@ class MdCGOS(MdCG):
1170
1213
  本路按边类型权重(causal .85 / sequential .60 …)、逐跳乘边置信度、
1171
1214
  默认 5 跳、visited 剪枝——「检索使用关系链」的落地。
1172
1215
 
1216
+ **边类型集可配(P3-causal)**:relation_types 缺省不再写死
1217
+ `CHAIN_TYPES_DEFAULT`,而走 `chain.chain_types_from_env()`(env
1218
+ `MDCG_CHAIN_TYPES`,缺省仍 = `CHAIN_TYPES_DEFAULT`)。动因:叙事/
1219
+ 文档语料(按章切出的 doc_ref 库)里**没有 causal 边**,只有
1220
+ `reference` 与父章节结构边 `part_of`——写死即本路在这类库上恒空。
1221
+ 未登记进 `EDGE_WEIGHTS` 的形态被读取点剔除(不静默走未知边)。
1222
+
1173
1223
  返回 (scored, prov);prov[nid] 带该节点**最强链**的节点序列与条件序列,
1174
1224
  使「为什么召回它」可审计。
1175
1225
  """
@@ -1182,7 +1232,8 @@ class MdCGOS(MdCG):
1182
1232
  seed_map[nid] = float(s)
1183
1233
  if not seed_map:
1184
1234
  return [], {}
1185
- rels = tuple(relation_types) if relation_types else chain.CHAIN_TYPES_DEFAULT
1235
+ rels = (tuple(relation_types) if relation_types
1236
+ else chain.chain_types_from_env())
1186
1237
  depth = chain.MAX_DEPTH_DEFAULT if max_depth is None else max_depth
1187
1238
  best = chain.expand_from_seeds(self, seed_map, relation_types=rels,
1188
1239
  max_depth=depth, decay=decay)
@@ -1209,6 +1260,65 @@ class MdCGOS(MdCG):
1209
1260
  scored.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
1210
1261
  return scored, prov
1211
1262
 
1263
+ def _path_temporal(self, entries, q_start, q_end, start_op, end_op,
1264
+ now=None):
1265
+ """时间路(P3-temporal,设计稿 §6.3):按**时间邻近度**排序的排名项。
1266
+
1267
+ 口径(§〇.3-12 已裁,逐条钉在此处):
1268
+
1269
+ * **核走唯一权威**:`time_core.cred_factor(gamma, dt, floor, 1.0)`
1270
+ ——本模块**不写任何指数核**(`exp(-t/τ)`、`×(1-factor)`、EMA 保持率
1271
+ 均是 time_core docstring 点名的 bug 形态)。
1272
+ * **γ 缺省 = ln2 / 30 天**(半衰期 30 天,与 `links.py:53` 同刻度),
1273
+ env `MDCG_TEMPORAL_GAMMA` 可覆盖;唯一读取点 `temporal_gamma()`。
1274
+ * **Δt 取 `created_at`**(写入时刻,主链必有)。
1275
+ * **单位必须显式是「天」**:γ 是「每天」的速率,Δt 先除以 86400 再喂
1276
+ ——喂秒会让衰减快 86400 倍(守卫 `test_time_core_lint.py` G3 组有该反证)。
1277
+ * **加 floor**(`TEMPORAL_SCORE_FLOOR`):老节点/缺 `created_at` 的节点
1278
+ 不被乘成 0 而永不可召回。
1279
+ * **不做多跳扩散**:时间天然「1 跳」,本路是**排名项**(与 S4 层级加成
1280
+ 同类),不是扩散路。
1281
+ * **种子不来自词法命中**:时间类问句常词面零重叠,故种子取
1282
+ **时间算子/区间命中集**——`entries`(已过 S1/S2 门控 ∧ 已过
1283
+ `trust.filter_by_time` 的索引标量过滤),**不 `_scan` 全库**、
1284
+ 不读正文即可算分(`created_at` 在索引快照里)。
1285
+
1286
+ ⚠ **理论边界标注**:理论原文 `exp(-γ·t)`(`docs/theory/智能论3.4.md:2122-2125`
1287
+ DEV-005)说的是 **confidence 衰减**;把它用于「时间邻近度」是**工程口径
1288
+ 的类比迁移**,不是理论原话——同一核形状、不同语义(新近度权重)。
1289
+ """
1290
+ if not any(x is not None for x in (q_start, q_end, start_op, end_op)):
1291
+ return [] # 无时间算子/区间 → 本路不启用(默认查询零变更)
1292
+ from .whitebox_kb.aeis_core import time_core as _tc # 懒导入(唯一权威)
1293
+ gamma = temporal_gamma()
1294
+ ref = q_end if q_end is not None else q_start
1295
+ if ref is None:
1296
+ ref = now if now is not None else time.time()
1297
+ out = []
1298
+ for e in entries:
1299
+ try:
1300
+ created = float(e.get("created_at") or 0.0)
1301
+ except (TypeError, ValueError):
1302
+ created = 0.0
1303
+ if created <= 0:
1304
+ # 写入时刻缺失:不猜测时刻,判「距参照无穷远」——floor 兜住
1305
+ #(仍可召回,不因缺字段整条消失)。
1306
+ dt_days = float("inf")
1307
+ else:
1308
+ # **单位=天**:86400 秒/天,显式换算(γ 是每天速率)
1309
+ dt_days = abs(created - float(ref)) / 86400.0
1310
+ score = _tc.cred_factor(gamma, dt_days, TEMPORAL_SCORE_FLOOR, 1.0)
1311
+ fm, c = self._read(e)
1312
+ if c is None:
1313
+ continue
1314
+ c = self._open_content(fm.get("id"), fm, c)
1315
+ if c is None:
1316
+ continue
1317
+ out.append(({"id": fm.get("id") or e["path"], "frontmatter": fm,
1318
+ "content": c, "path": e["path"]}, round(score, 6)))
1319
+ out.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
1320
+ return out
1321
+
1212
1322
  # 生效条件:以 expand or expand_query_terms_weighted 作扩展函数并对 query 调用(结果为假值按 {} 处理),从其中 pop "__source__"(缺键为 "whitebox")得 source,tw 为空返回 ([], source),否则对 entries 中存在 _weighted_coverage>0 的条目按 0.6·cov+0.3·aff+0.1·ctx_aff(context 非 None 时以 route_key(ctx, ctx.get("tags")) 得 ctx_domain,context 非 dict 时用 {})打分,返回 (out, source)。
1213
1323
  def _path_fuzzy(self, query, entries, context=None, expand=None):
1214
1324
  """模糊路径(分级隶属度):返回 (scored, source)。
@@ -1343,7 +1453,9 @@ class MdCGOS(MdCG):
1343
1453
  # 生效条件:query 去空白为空→empty_query、候选为空→no_candidates;否则按 paths 各路召回后融合(fusion=="max" 取各路最大贡献、否则求和),recall_only 中的路只以 0 分补池不参与打分,judge 与 judge_ranking 同时为真时对 fused 前 max(k*2,10) 条做资格裁决(REJECT/BLINDSPOT 剔除、DEFER 降权 0.5),否则直接取 fused 前 k;validity 真值时候选层剔除已过期节点,且 query 缓存按 validity 分键不串口径。
1344
1454
  def search_rrf(self, query: str, k: int = 20, layer: str = None,
1345
1455
  context=None, roles=None, include_work: bool = False,
1346
- judge: bool = True, paths=("lexical", "bucket", "entity", "graph"),
1456
+ judge: bool = True,
1457
+ paths=("lexical", "bucket", "entity", "graph", "chain",
1458
+ "temporal"),
1347
1459
  record: bool = True, query_expand=None,
1348
1460
  path_weights=None, recall_only=None, fusion: str = "sum",
1349
1461
  goal_text=None, judge_ranking: bool = False,
@@ -1358,14 +1470,28 @@ class MdCGOS(MdCG):
1358
1470
  多路共同确认的记忆排在单路命中之前(对齐 noema 的 Fusion Recall)。
1359
1471
  meta 含 per_path(各路的候选数与来源),可审计。
1360
1472
 
1361
- paths: 基线四路(词法/条件桶/实体/图扩展)+ 两条**显式启用**的增量路:
1473
+ paths: 基线四路(词法/条件桶/实体/图扩展)+ 增量路:
1362
1474
  "fuzzy" 词表驱动(同义词组分级隶属度 + 大域 IDF)
1363
1475
  "semantic" 条件结构驱动(CCG 生效条件 + condition_space 四槽;
1364
1476
  不适用条件被整词命中即从本路剔除——条件级负路由)
1365
1477
  "goal" 目标定向(第 5 篇第 3 章):以活跃目标文本(或显式
1366
1478
  goal_text)扩展查询,给「与当前目标相关」的记忆加权;
1367
1479
  无活跃目标时本路为空,等价于未启用。
1368
- 缺省 ("lexical","bucket","entity","graph"),既有行为完全不变。
1480
+ "chain" **因果路**(P3-causal,设计稿 §6.2):以词法/实体命中为
1481
+ 种子沿 edges 多跳扩散(复用 `md_cg/chain.py`:14 类边权、
1482
+ MAX_DEPTH 5、decay 0.9)。**代价口径(P3 遗留 ⑥ 如实
1483
+ 降级,2026-10-01):不读正文,但邻接构建为 O(N) 索引
1484
+ 条目级**(`chain.adjacency` 遍历全部索引节点;冷建代价
1485
+ 读数见 `scripts/p2p4_probe.py` R7)——原表述「不走全表」
1486
+ 与实测不符,已改正。
1487
+ "temporal" **时间路**(P3-temporal,设计稿 §6.3):按时间邻近度排序
1488
+ 的**排名项**,核走 `time_core.cred_factor`;无时间算子/
1489
+ 区间时恒空。
1490
+ 缺省 = ("lexical","bucket","entity","graph","chain","temporal")
1491
+ —— 后两路按裁定 9「全进默认检索」进缺省集;**每路可单独关**:
1492
+ 显式传不含该路的 paths 即关(既有调用方口径一字不变)。
1493
+ 两条新路在**无 edges / 无时间参数**的库上自然为空(零多算),
1494
+ 故纯词法查询的返回与改动前一致。
1369
1495
  path_weights: {路名: 权重};缺省全部 1.0 → 与既有等权 RRF 完全一致。
1370
1496
  用于压低**同质路**的贡献:等权融合下,两路对同一批候选给出不一致
1371
1497
  排序时,RRF 会双重奖励「两路都靠前」的干扰项,把强路的 top-1 挤掉。
@@ -1375,6 +1501,8 @@ class MdCGOS(MdCG):
1375
1501
  sum 奖励「多路共识」,但会系统性低估**单路独有**候选:当强路漏掉目标、
1376
1502
  弱路捞到时,目标的单路贡献必然低于任何「两路都有排名」的干扰项。
1377
1503
  max 只认「最好的一次排名」,不奖励共识,适合「任一路捞到即可」的召回。
1504
+ 因果路是**单路独有召回型**,MCP 面(`mdcg_recall`)按其缺省 max
1505
+ (先例 `mcp_server.py` 的 fuzzy/semantic/goal 同款表达式)。
1378
1506
  judge_ranking: 白箱终排(证据防火墙,显式启用)。融合排序只产生候选
1379
1507
  (语义负责「不要漏」),资格裁决决定最终优先级(白箱负责「不要错」):
1380
1508
  REJECT / BLINDSPOT 剔除,DEFER 降权 ×0.5,ACCEPT 保位。候选池取
@@ -1399,8 +1527,9 @@ class MdCGOS(MdCG):
1399
1527
  本入口的对应物是 reach——不在本函数内。
1400
1528
  """
1401
1529
  q = (query or "").strip()
1402
- # 批次 15 统一口径(unify.py):任意语言 query → 标准原子序列;
1403
- # 归一后的 q 进热路径缓存键(归一确定 → 缓存不串味)
1530
+ # 批次 15 统一口径(unify.py,2026-09-30 收窄作用域):只译**英文内容**
1531
+ # → 标准原子序列(中文段逐字原样);归一后的 q 进热路径缓存键
1532
+ # (归一确定 → 缓存不串味)
1404
1533
  from .semantic.unify import unify_query
1405
1534
  q = unify_query(q)
1406
1535
  if not q:
@@ -1479,6 +1608,13 @@ class MdCGOS(MdCG):
1479
1608
  ranked = {} # path -> [(node, score)]
1480
1609
  fuzzy_source = None
1481
1610
  chain_prov = {}
1611
+ # P3-temporal:时间路的参照窗(与 `_candidates` 同一真源 `trust.check_time_args`
1612
+ # /`trust.parse_time`;未启用时留 None → 本路恒空,默认查询零变更)。
1613
+ # 只读索引标量(created_at 在索引快照里),不 `_scan` 全库、不读正文。
1614
+ _t_en, _t_ax, _t_why = trust.check_time_args(
1615
+ start_time, end_time, start_operator, end_operator, time_axis)
1616
+ _t_qs, _t_qe = ((trust.parse_time(start_time), trust.parse_time(end_time))
1617
+ if _t_en else (None, None))
1482
1618
  if "lexical" in paths:
1483
1619
  ranked["lexical"] = self._lexical(q, entries, stat)
1484
1620
  if "bucket" in paths:
@@ -1490,6 +1626,12 @@ class MdCGOS(MdCG):
1490
1626
  if "chain" in paths:
1491
1627
  seeds = (ranked.get("lexical") or []) + (ranked.get("entity") or [])
1492
1628
  ranked["chain"], chain_prov = self._path_chain(q, entries, seeds, context)
1629
+ if "temporal" in paths:
1630
+ # P3-temporal:时间路(排名项)。时间算子/区间未启用时恒空——默认
1631
+ # 查询(无时间参数)零变更;启用时种子取「已过 S1/S2 门控 ∧ 已过
1632
+ # 时间算子过滤」的 entries(索引标量直读,不 _scan 全库)。
1633
+ ranked["temporal"] = self._path_temporal(
1634
+ entries, _t_qs, _t_qe, start_operator, end_operator)
1493
1635
  if "fuzzy" in paths:
1494
1636
  ranked["fuzzy"], fuzzy_source = self._path_fuzzy(
1495
1637
  q, entries, context, expand=query_expand)
@@ -1582,12 +1724,50 @@ class MdCGOS(MdCG):
1582
1724
  fused = fused_all[:k]
1583
1725
 
1584
1726
  results = []
1727
+ # P2-2(§5.2):默认召回路径上的**拒绝域双语义**——与 `MdCG._emit` 同款
1728
+ # 接线(同一个判据函数 `MdCG.judge_with_boundary`,此处不另写一份):
1729
+ # 边界命中 ⇒ 卡片带 `boundary_hit` 标记(可召回可展示);资格不受影响;
1730
+ # 边界命中数与资格 REJECT 数**分开计数**(boundary_counts)。
1731
+ boundary_counts = {"hit": 0, "terms": 0,
1732
+ "counts": {"accept": 0, "reject": 0, "defer": 0,
1733
+ "blindspot": 0}}
1585
1734
  for nid, fs in fused:
1586
1735
  node, s = node_by_id[nid]
1587
- qual = (quals.get(nid)
1588
- or (self.judge_qualification(node, q, context) if judge
1589
- else {"state": None, "reason": "judge_disabled"}))
1736
+ if quals.get(nid):
1737
+ # judge_ranking(白箱终排)已算过资格 → 不重复判定,边界标记
1738
+ # 单独取(同一个 P0-4 判据函数 `MdCG.boundary_hit`)
1739
+ qual = quals[nid]
1740
+ _b = boundary_mark(node, q, context)
1741
+ _c = {"accept": 0, "reject": 0, "defer": 0, "blindspot": 0}
1742
+ _st = str(qual.get("state") or "").lower()
1743
+ if _st in _c:
1744
+ _c[_st] = 1
1745
+ if _b.get("hit"):
1746
+ boundary_counts["hit"] += 1
1747
+ boundary_counts["terms"] += len(_b.get("terms") or [])
1748
+ node["boundary_hit"] = True
1749
+ node["boundary_marker"] = _b.get("marker")
1750
+ node["boundary_terms"] = list(_b.get("terms") or [])
1751
+ for _k in _c:
1752
+ boundary_counts["counts"][_k] += _c[_k]
1753
+ elif judge:
1754
+ _jw = MdCG.judge_with_boundary(node, q, context)
1755
+ qual = dict(_jw["qualification"])
1756
+ _b = boundary_mark(node, q, context)
1757
+ if _b.get("hit"):
1758
+ boundary_counts["hit"] += 1
1759
+ boundary_counts["terms"] += len(_b.get("terms") or [])
1760
+ node["boundary_hit"] = True
1761
+ node["boundary_marker"] = _b.get("marker")
1762
+ node["boundary_terms"] = list(_b.get("terms") or [])
1763
+ for _k in ("accept", "reject", "defer", "blindspot"):
1764
+ boundary_counts["counts"][_k] += int(
1765
+ (_jw.get("counts") or {}).get(_k) or 0)
1766
+ else:
1767
+ qual = {"state": None, "reason": "judge_disabled"}
1590
1768
  results.append((node, round(fs, 6), qual, prov.get(nid, [])))
1769
+ _bnd_meta = ({"boundary": boundary_counts}
1770
+ if boundary_counts["hit"] else {})
1591
1771
  if record and results:
1592
1772
  self.record_access([r[0]["id"] for r in results], "RRF")
1593
1773
  # 热路径:写 query 结果缓存 —— **同样受 `_time_on` 约束**。
@@ -1603,7 +1783,7 @@ class MdCGOS(MdCG):
1603
1783
  "expand_source": fuzzy_source,
1604
1784
  "goal_used": goal_used,
1605
1785
  "provenance": prov, **_tf_meta,
1606
- **_gates_meta}, k=k, layer=layer,
1786
+ **_gates_meta, **_bnd_meta}, k=k, layer=layer,
1607
1787
  session=session, branch=branch, validity=validity,
1608
1788
  view=view, extra=_cache_extra)
1609
1789
  return results, {"tier": "RRF", "scanned": stat["scanned"],
@@ -1614,7 +1794,7 @@ class MdCGOS(MdCG):
1614
1794
  "expand_source": fuzzy_source,
1615
1795
  "goal_used": goal_used,
1616
1796
  "provenance": prov, **_tf_meta,
1617
- **_gates_meta}
1797
+ **_gates_meta, **_bnd_meta}
1618
1798
 
1619
1799
  # ================= 7. budget-driven pack =================
1620
1800
 
@@ -1638,11 +1818,12 @@ class MdCGOS(MdCG):
1638
1818
  动机:旧策略是「跳过超大、继续试更小的」——预算紧张时形成**逆向淘汰**,
1639
1819
  越有价值的详实条目越容易被排除(实测 budget=1500 时 16 条被刷、只装 2 条)。
1640
1820
 
1641
- paths/query_expand 缺省时行为与既有完全一致(默认四路、纯白箱扩展);
1642
- 显式传 paths 才启用新路,例如
1821
+ paths/query_expand 缺省时行为 = 既有四路 + P3 两条图检索路(chain/temporal,
1822
+ 按裁定 9 进缺省集;无 edges / 无时间参数时自然为空);
1823
+ 显式传 paths 才启用第 5/6 路,例如
1643
1824
  ("lexical","bucket","entity","graph","fuzzy") 词表驱动
1644
1825
  ("lexical","bucket","entity","graph","semantic") 条件结构驱动
1645
- (… 七路全开 ) 三者叠加
1826
+ (… 八路全开 ) 叠加
1646
1827
  include_recent=True 时,把近期事件窗口(第 5 篇第 3 章)附在包后,
1647
1828
  保证当前任务的连续性;它不参与 RRF 正排,但计入 token 预算。
1648
1829
  返回 {pack: [...], tokens_used, budget, skipped: [...], recent: [...], meta}
@@ -1831,10 +2012,11 @@ class MdCGOS(MdCG):
1831
2012
 
1832
2013
  # ================= 4. 审核队列(inbox → decisions) =================
1833
2014
 
1834
- # 生效条件:当 node_id 与 content 传入时,在 strict 锁内按 payload_hash 查重;命中同内容提案(无论 pending/accepted/rejected,已裁决优先 break)时幂等返回既有 pid(info=True 返回 dedup 字典),未命中则生成新 pid 入队并返回 pid(info=True 返回 dedup False 字典);
2015
+ # 生效条件:当 node_id 与 content 传入时,在 strict 锁内按 payload_hash(dedup_key 非空时以它为对账键、否则 _sig(content))查重;命中同键提案(无论 pending/accepted/rejected,已裁决优先 break)时幂等返回既有 pid(info=True 返回 dedup 字典),未命中则生成新 pid 入队并返回 pid(info=True 返回 dedup False 字典);kind 非空时原样落 rec 顶层 kind 键(缺省不落键 = 存量条目形状逐位不变);
1835
2016
  def propose(self, node_id: str, content: str, layer: str = "knowledge",
1836
2017
  tags=None, condition_space=None, verify=None,
1837
- info: bool = False, **kw):
2018
+ info: bool = False, kind: str = None, dedup_key: str = None,
2019
+ **kw):
1838
2020
  """把一个候选记忆放入海马体 inbox,等待审核(不直接持久化)。
1839
2021
 
1840
2022
  verify —— 验收判据(内联声明,裁决阶段只读),形如:
@@ -1847,10 +2029,22 @@ class MdCGOS(MdCG):
1847
2029
  不再入队。语义:重试与崩溃恢复无害——入队落盘成功而响应丢失时
1848
2030
  (MCP 客户端超时重试的真正机制),重试对账命中既有记录并返回原 pid。
1849
2031
 
2032
+ kind / dedup_key(三档自治批次②,设计 §四 两个**可选**槽,缺省即
2033
+ 存量行为逐位不变):
2034
+ · `kind` = 队列条目**类型字段**("proposal" 存量 / "mutation" 变更单)。
2035
+ 缺省 None **不落键**——存量与新普通提案的 rec 形状一字不变;读取方
2036
+ 经 `autonomy_modes.order_kind(rec)` 判类型,缺键一律视为 proposal
2037
+ (零迁移)。
2038
+ · `dedup_key` = 幂等对账键的**显式覆盖**(缺省 None → 原 `_sig(content)`)。
2039
+ 变更单必须覆盖:内容签名不带目标,「同一份新正文并入节点 X / 并入
2040
+ 节点 Y」会撞成同一条(`autonomy_modes.mutation_dedup_key` 把动作类与
2041
+ 目标并进键面)。键仍是 sha1 十六进制——`_inbox_phash_index` 与
2042
+ `_cascade_dedup` 只做字符串比对,无需认识构造。
2043
+
1850
2044
  返回值:info=False(默认)返回 pid 字符串(保形,存量调用零变化);
1851
2045
  info=True 返回 {"pid", "dedup", "dup_of", "dup_status"}。
1852
2046
  """
1853
- phash = _sig(content)
2047
+ phash = dedup_key or _sig(content)
1854
2048
  with FileLock(self.inbox_log, strict=True):
1855
2049
  # 增量对账(issue #32):原实现每条全量 read_jsonl(inbox) +
1856
2050
  # read_jsonl(decisions)(锁内 O(M+D)/条、批量 O(M²),旧格式行
@@ -1893,6 +2087,11 @@ class MdCGOS(MdCG):
1893
2087
  "verify": verify or {}, "verify_hash": vhash,
1894
2088
  "extra": kw, "actor": self.actor,
1895
2089
  "session": getattr(self, "session", None)}
2090
+ # 三档自治批次②(设计 §四):类型字段**只在显式给定时落键**——
2091
+ # 缺省不落,存量与新普通提案的 rec 形状逐位不变(零回归),
2092
+ # 读取方按「缺键 = proposal」判(零迁移)。
2093
+ if kind:
2094
+ rec["kind"] = str(kind)
1896
2095
  append_jsonl(self.inbox_log, rec)
1897
2096
  self._audit("propose", node_id, pid=pid, layer=layer,
1898
2097
  payload_hash=phash, verify_hash=vhash)
@@ -2161,7 +2360,7 @@ class MdCGOS(MdCG):
2161
2360
  audit_source="hippocampus/decisions.jsonl", **extra)
2162
2361
  return nid
2163
2362
 
2164
- # 生效条件:当 pid/item/decision/status/reason/round_no/rt_verdict/issues/vhash/result 传入时,构造 rec 并写 record_hash,把 result 中 ok/node_id/error/state 保留;在 strict 锁内追加 decisions_log;随后尝试写 md 审核记录,成功补 record_node_id/record_hash,异常则补 record_error 并 audit review_record_failed;最后 audit review_decide 并返回 result;
2363
+ # 生效条件:当 pid/item/decision/status/reason/round_no/rt_verdict/issues/vhash/result 传入时,构造 rec 并写 record_hash,把 result 中 ok/node_id/error/state 保留(result.mutation 为 dict 时另落 rec["mutation"]=变更单执行时点载荷,proposal 链无此键、rec 形状逐位不变);在 strict 锁内追加 decisions_log;随后尝试写 md 审核记录,成功补 record_node_id/record_hash,异常则补 record_error 并 audit review_record_failed;最后 audit review_decide 并返回 result;
2165
2364
  def _record_decision(self, pid, item, decision, status, reason, round_no,
2166
2365
  rt_verdict, issues, vhash, result):
2167
2366
  """落盘一轮裁决:jsonl(权威)+ md 审计节点(可复核、供外部审计)。"""
@@ -2174,6 +2373,13 @@ class MdCGOS(MdCG):
2174
2373
  rec["record_hash"] = self._record_hash(rec)
2175
2374
  rec["result"] = {k: v for k, v in result.items()
2176
2375
  if k in ("ok", "node_id", "error", "state")}
2376
+ # 三档自治③(设计 §四):变更单的**执行时点载荷**(含前像/影响面/
2377
+ # 回滚命令)随裁决记录落盘——回滚 CLI 的读取源(rec["mutation"])。
2378
+ # proposal 链 result 无 mutation 键 ⇒ rec 形状逐位不变;record_hash
2379
+ # 只算固定十键(_record_hash),不受本键影响。
2380
+ _mut = result.get("mutation")
2381
+ if isinstance(_mut, dict):
2382
+ rec["mutation"] = _mut
2177
2383
  # 裁决记录不能丢:strict 锁内追加(并发裁决不交错;
2178
2384
  # 丢一条裁决会让提案回 pending → 重复落盘,比让裁决者等一下代价大)
2179
2385
  with FileLock(self.decisions_log, strict=True):
@@ -2279,7 +2485,158 @@ class MdCGOS(MdCG):
2279
2485
  "verify_hash": src.get("verify_hash"),
2280
2486
  "source": "hippocampus/decisions.jsonl"}
2281
2487
 
2282
- # 生效条件:decision 须为 DECISION_ACTIONS("accept"/"reject"/"edit"/"merge"/"noop")之一(否则 raise ValueError),inbox_log 中须有 pid 匹配记录(否则 {'ok': False, 'error': 'pid_not_found'}),且 pid 不在 self._closed_pids() 中(否则 'already_decided');edits 为真值且含 "verify"、或 redteam 为真值且含 "verify" 时返回 'verify_readonly';last_status=="needs_reapproval" 时须 redteam.verdict 归一化为 "pass" 且 round_no>last_round(否则 'reapproval_required' / 'round_not_advanced');rt_v=="reject" 或(decision=="reject" 且 rt_issues 非空)时记 needs_reapproval 不落节点;decision=="accept" 且 rt_v 为空且 _redteam_required() 为真时返回 'redteam_required';其余 accept/edit 按 item(edit 时用 edits.get 覆盖 content/tags/layer)add+flush 落节点,merge 须 merge_into 或 item.extra.merge_into 指向的节点存在(否则 'merge_target_not_found'),且写入前经与 add 同款双闸(N131,2026-09-25:principal 在位先 require_layer_write(目标层, 目标敏感度)——与 MdCGSecure.add 同序同错型;再 protect.guard_write(override=override)——self/anchor 层、immutable 标记,拒绝抛 ProtectionError/AccessDenied 且提案留 pending 不落库)后追加内容并定向索引 upsert(_node_entry 同源条目写入 index/_dirty)+flush,reject 与 noop 只记裁决(status 分别为 "rejected"/"noop");最后统一 _record_decision + _cascade_dedup + flush 后返回 result。
2488
+ # 生效条件:item 为 kind=mutation 的队列记录时先经 rollback.preimage 在**执行时点**留前像(目标在位时拍快照 + 采影响面 + 生成回滚命令;目标缺失返回 None 走既有错误路径;拍摄失败抛 PreimageError 被专捕成 preimage_unavailable 错误体),再按载荷 extra.mutation 分派——B:primitive 为 "converge_into" 走 writelimit.converge_into、否则走 forgetting.reinforce(两者都以 rec.actor 为动作发起者、载荷 meta 的 session/override 复现原槽);C:目标经 forgetting.prior_node 判定——目标已不存在即返回 {"ok": False, "error": "target_missing", ...}(fail-closed,不重建节点、不记 accepted),存在才 self.add(目标, 后像, layer=rec.layer, sensitivity=rec.sensitivity, override, **载荷 meta) 覆写落盘;D:self.forget(目标, override) 软删;动作类不在 {B,C,D} 或原语返回失败(ok 为 False / None / target_missing)时返回 {"ok": False, "error": "mutation_execute_failed"/"mutation_action_unknown", ...} 且**不改动任何节点**;成功返回 {"ok": True, "mutation": <执行时点载荷:原载荷 + kind/executed + before/impact/rollback>, "node_id": ...};
2489
+ def _mutation_execute(self, item, reason: str = "", override: bool = False):
2490
+ """变更单 accept 的执行桥:**显式越过确认判定**,直调对应动作的底层原语。
2491
+
2492
+ 为什么这样就是「越过」(而不是绕过闸门):三档自治的确认判定只长在
2493
+ **热路径入口**(写链 `writepipe._gate_autonomy`、遗忘合并闸
2494
+ `_autonomy_gate_merge`、工具面 `forget_gated`);本桥调的是
2495
+ `MdCG.add` / `forgetting.reinforce` / `writelimit.converge_into` /
2496
+ `MdCGOS.forget`——即**库层原语**,它们历来不含档位判定(设计 §三 硬约束
2497
+ 「直调库层不接线,保持库层语义不变」)。故「已确认」这件事由调用点
2498
+ (本桥)保证,不靠传一个 `already_confirmed=True` 之类的旗标穿透各层
2499
+ (旗标会变成可被伪造的旁路)。
2500
+
2501
+ 保护闸不绕:`add`/`reinforce`/`converge_into` 内部的
2502
+ `protect.guard_write|guard_overwrite` 与 `forget` 的 `guard_forget`
2503
+ 仍是落盘必经之路——受保护节点在确认后依然被拦(拦下即返回失败、
2504
+ 裁决不落盘、提案保持 pending)。
2505
+
2506
+ 失败语义(如实):动作未发生 ⇒ 返回 ok=False 且**不记裁决**;调用方
2507
+ (`review_decide`)据此提前返回,提案维持 pending 可见。
2508
+
2509
+ 补强(2026-10-02,复核观察 3):**C 改写的目标在 accept 时已消失**此前
2510
+ 走 `self.add` 的 upsert——静默把「改写」落地成「新增同 id 节点」并记
2511
+ accepted(单子声称 C、盘面却是 A,前像被清空且无失败信号)。现与 B/D
2512
+ 同口径:目标不存在 ⇒ `{"ok": False, "error": "target_missing"}`,
2513
+ **不重建节点**、不记裁决、提案保持 pending。判据复用存在性单点
2514
+ `forgetting.prior_node`(与出单时的 C/A 判定同一单点)。
2515
+
2516
+ 批次③(2026-10-02,设计 §四)——**执行时点三字段补全**:动作落盘**之前**
2517
+ 经 `rollback.preimage()` 拍前像(`protect.snapshot_preimage`,写
2518
+ `_protected_history` 同面)+ 采影响面(边/索引条目/聚合行)+ 生成回滚
2519
+ 命令;成功返回的 `mutation` 是**补全后的执行时点载荷**
2520
+ (`autonomy_modes.mutation_executed`),由 `_record_decision` 随裁决
2521
+ 落进 decisions.jsonl 的 `rec["mutation"]`(回滚 CLI 的读取源)。
2522
+ 前像**必须**是执行时点(不是提议时点):从提议到 accept 之间目标可能
2523
+ 被第三方改动,回滚只撤销本变更本身(设计 §四 明文)。拍摄失败
2524
+ (目标在位而快照未落)⇒ `preimage_unavailable` fail-closed,不执行
2525
+ ——「一切破坏性动作先留前像」(设计 §六 义务)。
2526
+ """
2527
+ from . import autonomy_modes as _am
2528
+ from . import rollback as _rb
2529
+ try:
2530
+ slot = (item.get("extra") or {}).get(_am.MUTATION_SLOT) or {}
2531
+ except AttributeError:
2532
+ slot = {}
2533
+ act = str(slot.get("action") or "").strip().upper()
2534
+ tgt = slot.get("target") or item.get("id")
2535
+ after = slot.get("after")
2536
+ if after is None:
2537
+ after = item.get("content") or ""
2538
+ meta = dict(slot.get("meta") or {})
2539
+ actor = item.get("actor") or getattr(self, "actor", None)
2540
+ session = meta.pop("session", None)
2541
+ _ovr = bool(meta.pop("override", False)) or bool(override)
2542
+ layer = item.get("layer") or "knowledge"
2543
+ sens = item.get("sensitivity")
2544
+ pre = None
2545
+ try:
2546
+ # ---- 执行时点补全(三档自治③ · 设计 §四)----
2547
+ # 前像(执行时点快照引用)+ 影响面 + 回滚命令——在**动作落盘之前**:
2548
+ # 从提议到 accept 之间目标可能被第三方改动,前像必须反映「执行前
2549
+ # 一刻」的盘面,回滚才只撤销本变更本身。目标不存在时返回 None
2550
+ # (动作不会发生,走既有 target_missing/not_found 错误路径);
2551
+ # 目标在位而拍摄失败 ⇒ PreimageError ⇒ 下方专捕 fail-closed。
2552
+ pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),
2553
+ reason="变更单执行时点前像(%s→%s,pid=%s)"
2554
+ % (act, tgt, item.get("pid") or "?"))
2555
+ if act == _am.B_MERGE:
2556
+ if str(slot.get("primitive") or "") == "converge_into":
2557
+ res = writelimit.converge_into(self, tgt, after,
2558
+ override=_ovr, actor=actor,
2559
+ session=session)
2560
+ else:
2561
+ res = forgetting.reinforce(self, tgt, override=_ovr,
2562
+ actor=actor, content=after,
2563
+ session=session)
2564
+ node_id = tgt
2565
+ elif act == _am.C_REWRITE:
2566
+ # 补强(2026-10-02,复核观察 3):C 的**目标节点在 accept 执行时
2567
+ # 已不存在** ⇒ fail-closed,**不得**静默 `self.add` upsert 成
2568
+ # 新增。旧形态:目标出单后被删除,重放把「改写」落成「同名新
2569
+ # 节点」并记 accepted——变更单声称执行了 C 改写,盘面却凭空多出
2570
+ # 一个节点(前像被清空,且无任何失败信号)。
2571
+ # 判据复用**存在性单点** `forgetting.prior_node`(与出单时的
2572
+ # C/A 判定同一单点),错误名与 B/D 原语同口径
2573
+ # (`writelimit.converge_into` 的 {"ok": False,
2574
+ # "error": "target_missing"})。
2575
+ if forgetting.prior_node(self, tgt) is None:
2576
+ return {"ok": False, "error": "target_missing",
2577
+ "kind": _am.KIND_MUTATION, "action": act,
2578
+ "target": tgt,
2579
+ "detail": "变更单 C 改写:目标节点在 accept 执行时"
2580
+ "已不存在(出单后被删除/移走)——"
2581
+ "fail-closed 未执行任何动作、未重建节点。",
2582
+ "hint": "目标节点 %s 已不存在:C 改写不得静默转成"
2583
+ "「新增同 id 节点」。请核对目标是否已被删除"
2584
+ "(trash/ 可 restore)后重新提议——裁决"
2585
+ "**未落盘**,提案保持 pending。" % tgt}
2586
+ node_id = self.add(tgt, after, layer=layer, sensitivity=sens,
2587
+ override=_ovr, **meta)
2588
+ res = {"ok": True, "node_id": node_id}
2589
+ elif act == _am.D_DELETE:
2590
+ res = self.forget(tgt, reason=reason or slot.get("reason") or "",
2591
+ override=_ovr)
2592
+ node_id = tgt
2593
+ else:
2594
+ return {"ok": False, "error": "mutation_action_unknown",
2595
+ "kind": _am.KIND_MUTATION, "action": act or None,
2596
+ "detail": "变更单载荷的动作类缺失或不在 %s——fail-closed"
2597
+ "未执行任何动作、提案保持 pending。"
2598
+ % "/".join(_am.ACTION_CLASSES)}
2599
+ except _rb.PreimageError as exc:
2600
+ # 执行时点前像未落盘(目标在位而快照失败)——「一切破坏性动作先留
2601
+ # 前像」(设计 §四/§六),fail-closed 不执行:动作**未发生** ⇒
2602
+ # 如实返回失败,不落裁决、提案保持 pending。
2603
+ return {"ok": False, "error": "preimage_unavailable",
2604
+ "kind": _am.KIND_MUTATION, "action": act, "target": tgt,
2605
+ "detail": str(exc),
2606
+ "hint": "变更单未执行:执行时点前像未落盘(设计 §四:一切"
2607
+ "破坏性动作先留前像)——裁决**未落盘**,提案保持 "
2608
+ "pending;排查 _protected_history 目录可写性后"
2609
+ "重新裁决。"}
2610
+ except Exception as exc: # noqa: BLE001
2611
+ # 保护闸(ProtectionError/AccessDenied)、冲突闸(ConsistencyError)、
2612
+ # 路径闸(ValueError)等一律走这里:动作**未发生** ⇒ 如实返回失败,
2613
+ # 不落裁决、提案保持 pending(把异常原样抛给 CLI 会让裁决进程崩在
2614
+ # 「已执行一半」的歧义态上,且提案去向不可见)。
2615
+ return {"ok": False, "error": "mutation_execute_failed",
2616
+ "kind": _am.KIND_MUTATION, "action": act, "target": tgt,
2617
+ "detail": "%s: %s" % (type(exc).__name__, exc),
2618
+ "hint": "变更单未执行:目标节点 %s 的动作被既有闸门拦下或原语"
2619
+ "失败——裁决**未落盘**,提案保持 pending;"
2620
+ "受保护节点/冲突需先按既有通道处置(override 或"
2621
+ "修文)后重新裁决。" % tgt}
2622
+ if not isinstance(res, dict) or res.get("ok") is False:
2623
+ return {"ok": False, "error": "mutation_execute_failed",
2624
+ "kind": _am.KIND_MUTATION, "action": act, "target": tgt,
2625
+ "detail": res,
2626
+ "hint": "变更单未执行:目标节点 %s 的动作原语未成功"
2627
+ "(受保护/目标缺失/同 id 冲突等)——裁决**未落盘**,"
2628
+ "提案保持 pending,修好后可重新裁决。" % tgt}
2629
+ # 执行时点载荷(**唯一补全点**,autonomy_modes.mutation_executed):
2630
+ # 原载荷(动作类/目标/后像/理由/primitive/meta)+ kind/executed +
2631
+ # before/impact/rollback——_record_decision 随裁决落 decisions.jsonl
2632
+ # 的 rec["mutation"](回滚 CLI 据此读回前像与动作类)。
2633
+ mpay = _am.mutation_executed(
2634
+ slot, before=(pre or {}).get("before"),
2635
+ impact=(pre or {}).get("impact"),
2636
+ rollback=(pre or {}).get("rollback"))
2637
+ return {"ok": True, "node_id": node_id, "mutation": mpay}
2638
+
2639
+ # 生效条件:decision 须为 DECISION_ACTIONS("accept"/"reject"/"edit"/"merge"/"noop")之一(否则 raise ValueError),inbox_log 中须有 pid 匹配记录(否则 {'ok': False, 'error': 'pid_not_found'}),且 pid 不在 self._closed_pids() 中(否则 'already_decided');edits 为真值且含 "verify"、或 redteam 为真值且含 "verify" 时返回 'verify_readonly';last_status=="needs_reapproval" 时须 redteam.verdict 归一化为 "pass" 且 round_no>last_round(否则 'reapproval_required' / 'round_not_advanced');rt_v=="reject" 或(decision=="reject" 且 rt_issues 非空)时记 needs_reapproval 不落节点;decision=="accept" 且 rt_v 为空且 _redteam_required() 为真时返回 'redteam_required';其余 accept/edit 按 item(edit 时用 edits.get 覆盖 content/tags/layer)add+flush 落节点,merge 须 merge_into 或 item.extra.merge_into 指向的节点存在(否则 'merge_target_not_found'),且写入前经与 add 同款双闸(N131,2026-09-25:principal 在位先 require_layer_write(目标层, 目标敏感度)——与 MdCGSecure.add 同序同错型;再 protect.guard_write(override=override)——self/anchor 层、immutable 标记,拒绝抛 ProtectionError/AccessDenied 且提案留 pending 不落库)后追加内容并定向索引 upsert(_node_entry 同源条目写入 index/_dirty)+flush,reject 与 noop 只记裁决(status 分别为 "rejected"/"noop");kind=mutation 的变更单(三档自治批次②)不走上述分支——accept 经 _mutation_execute 执行对应动作(失败即返回错误、不记裁决、提案保持 pending)、reject 只留痕,其余裁决 fail-closed 返回 mutation_decision_unsupported;最后统一 _record_decision + _cascade_dedup + flush 后返回 result。
2283
2640
  def review_decide(self, pid: str, decision: str, edits: dict = None,
2284
2641
  merge_into: str = None, reason: str = "",
2285
2642
  redteam: dict = None, issues=None,
@@ -2298,6 +2655,14 @@ class MdCGOS(MdCG):
2298
2655
  的写入门控(它根本不写)。此处 noop 是**裁决动作**,与 lifecycle/trust 中
2299
2656
  同名的**状态迁移结果码**分属两层(见模块常量区注释)。
2300
2657
 
2658
+ **变更单**(`kind=mutation`,三档自治批次② · 设计 §四):本批只定义
2659
+ accept/reject——accept = 执行对应动作(B 合并原语 / C 覆写落盘走既有
2660
+ add / D 软删走既有 forget),reject = 原样留痕不执行;edit/merge/noop
2661
+ 对变更单**未定义** ⇒ fail-closed 报错带 hint(不静默当已处理)。
2662
+ 执行失败 ⇒ **不记裁决**、提案保持 pending(与 merge 的
2663
+ `merge_target_not_found` 同款:不落一条「accepted 而动作没做」)。
2664
+ 存量 `kind` 缺键的提案(= proposal)在本方法里**逐位不变**。
2665
+
2301
2666
  两条验证纪律(借自任务分级协议的验证端):
2302
2667
  1. 判据只读:verify 由 propose 声明,裁决阶段传入不同判据 → verify_readonly。
2303
2668
  2. 红队门控 + 再审批:redteam.verdict=reject(或带 issues 的 reject)不落库,
@@ -2353,6 +2718,14 @@ class MdCGOS(MdCG):
2353
2718
  return {"ok": False, "error": "redteam_required",
2354
2719
  "detail": "MDCG_REDTEAM_REQUIRED=1:accept 必须带红队 pass"}
2355
2720
 
2721
+ # 三档自治批次②(设计 §四):**变更单**(kind=mutation)走**独立执行桥**
2722
+ # ——在既有前置闸(已判/verify_readonly/再审批/红队门控)之后、既有
2723
+ # accept/edit/merge/noop 链之前。proposal(含存量无 kind 条目)一律不
2724
+ # 进此支,下面整条链对它逐位不变。
2725
+ if autonomy_modes.order_kind(item) == autonomy_modes.KIND_MUTATION:
2726
+ return self._decide_mutation(pid, item, decision, reason, override,
2727
+ round_no, rt_v, rt_issues, expect)
2728
+
2356
2729
  result = {"pid": pid, "decision": decision, "round": round_no,
2357
2730
  "redteam": rt_v or "absent"}
2358
2731
  if decision == DECISION_NOOP:
@@ -2363,6 +2736,11 @@ class MdCGOS(MdCG):
2363
2736
  elif decision == "reject":
2364
2737
  result["ok"] = True
2365
2738
  elif decision in ("accept", "edit"):
2739
+ # issue50-c F1 对口(2026-10-01,本分支**零行为改动**):accept 早就是
2740
+ # 「队列记录 → 节点 fm」的映射通道(`rec.tags` / `rec.condition_space`
2741
+ # / `**extra`,下方 :2553/:2573)。`remember_gated` 的 DEFER 出口把调用方
2742
+ # 声明的 meta 随入队一并落进这三槽之后,DEFER→accept 与直接 ACCEPT 的
2743
+ # 节点 fm 即等价——映射不需要新增第二套实现,本分支职责与判据不变。
2366
2744
  content = item["content"]
2367
2745
  tags = list(item.get("tags") or [])
2368
2746
  layer = item.get("layer") or "knowledge"
@@ -2446,6 +2824,18 @@ class MdCGOS(MdCG):
2446
2824
  self.flush()
2447
2825
  result.update(ok=True, node_id=target)
2448
2826
 
2827
+ return self._finish_decision(pid, item, decision, reason, round_no,
2828
+ rt_v, rt_issues, expect, result)
2829
+
2830
+ # 生效条件:result 为已定好的裁决返回体时,按其 pid/item/decision/reason/round_no/rt_v/rt_issues/expect 调 _record_decision(status 映射统一出口:reject→"rejected"、noop→"noop"、其余→"accepted")落盘 jsonl + 审计节点,再按 item 的 payload_hash(缺失时 _sig(content))调 _cascade_dedup 级联出清(有 closed 时写 result["cascade_closed"]),最后 flush 并返回 result;
2831
+ def _finish_decision(self, pid, item, decision, reason, round_no, rt_v,
2832
+ rt_issues, expect, result):
2833
+ """裁决**统一收尾**(单点):proposal 链与变更单执行桥共用同一收尾。
2834
+
2835
+ 为何抽出来:三档自治批次② 的变更单走独立执行桥(`_decide_mutation`),
2836
+ 但「落 decisions.jsonl + 写审计节点 + 级联出清 + flush」这几步必须与原
2837
+ 路径**同一条实现**——各写一份必然漂移(本仓已两次教训)。
2838
+ """
2449
2839
  # status 映射统一出口:reject→"rejected"、noop→"noop"(同为终态,见
2450
2840
  # TERMINAL_DECISION_STATUS)、其余→"accepted"。
2451
2841
  result = self._record_decision(
@@ -2467,6 +2857,40 @@ class MdCGOS(MdCG):
2467
2857
  self.flush()
2468
2858
  return result
2469
2859
 
2860
+ # 生效条件:item 为 kind=mutation 的队列记录——decision 为 accept 时经 _mutation_execute 执行对应动作(返回 ok=False 时**原样返回该错误**、不落裁决、提案保持 pending),为 reject 时只置 ok=True 留痕;decision 非此二者时返回 {"ok": False, "error": "mutation_decision_unsupported", ...} 带 hint(fail-closed,不落裁决);通过者交 _finish_decision 收尾后返回其结果;
2861
+ def _decide_mutation(self, pid, item, decision, reason, override, round_no,
2862
+ rt_v, rt_issues, expect):
2863
+ """变更单的裁决分派(三档自治批次② · 设计 §四):accept 执行 / reject 留痕。
2864
+
2865
+ 本批只定义 accept/reject 两态;其余裁决(edit/merge/noop)**fail-closed
2866
+ 报错带 hint**,不静默当作已处理——否则一条被 noop 的变更单会伪装成
2867
+ 「评估过、无需改动」,而实际动作从未发生过(设计 §三 硬约束③ 的同精神:
2868
+ 没发生的事不许汇报成发生了)。
2869
+ """
2870
+ if decision not in (DECISION_ACCEPT, DECISION_REJECT):
2871
+ return {"ok": False, "error": "mutation_decision_unsupported",
2872
+ "kind": autonomy_modes.KIND_MUTATION,
2873
+ "decision": decision,
2874
+ "hint": "变更单(kind=mutation)本批只定义 accept/reject:"
2875
+ "accept=执行对应动作(B reinforce/converge、"
2876
+ "C 覆写落盘、D 软删),reject=原样留痕不执行;"
2877
+ "edit/merge/noop 对变更单未定义——fail-closed "
2878
+ "未处理,提案保持 pending(未静默关闭)。"}
2879
+ result = {"pid": pid, "decision": decision, "round": round_no,
2880
+ "redteam": rt_v or "absent"}
2881
+ if decision == DECISION_ACCEPT:
2882
+ mres = self._mutation_execute(item, reason=reason, override=override)
2883
+ if not mres.get("ok"):
2884
+ # 执行失败 = 动作未发生:**不落裁决**(提案保持 pending,与 merge
2885
+ # 分支的 merge_target_not_found 同款——落一条 accepted 而动作没做,
2886
+ # 就是「汇报不实」)。
2887
+ return mres
2888
+ result.update(mres)
2889
+ else:
2890
+ result["ok"] = True
2891
+ return self._finish_decision(pid, item, decision, reason, round_no,
2892
+ rt_v, rt_issues, expect, result)
2893
+
2470
2894
  # 生效条件:无输入形参;调用即返回 list(read_jsonl(self.decisions_log)),记录内容取决于 decisions_log 可读结果;
2471
2895
  def decisions(self):
2472
2896
  return list(read_jsonl(self.decisions_log))
@@ -2474,11 +2898,17 @@ class MdCGOS(MdCG):
2474
2898
  # ================= 5. tombstone + 恢复时删除检查 =================
2475
2899
 
2476
2900
  # 生效条件:当 node_id 传入且索引中存在该节点时,先经 protect.guard_forget(override=override) 保护检查,随后尝试把源路径 os.replace 到 trash_dir/{node_id}.md;OSError 返回 ok False error 字符串;成功则写 deletions_log、_unstage、缓存失效、audit,并返回 ok True/id/tombstone;索引无此节点返回 not_found;
2477
- def forget(self, node_id: str, reason: str = "", override: bool = False):
2478
- """软删除:节点文件移入 trash/,写入删除清单(payload-free)。
2901
+ def _forget_qualify(self, node_id: str, override: bool = False):
2902
+ """删除的**既有资格前置闸**(单点):索引代际感知 → not_found → 写保护。
2479
2903
 
2480
- 写保护:受保护节点(self/anchor 层、protected 标记、importance≥0.7)
2481
- 不可遗忘——需显式 override=True,且旧版本先快照、动作全程留痕。
2904
+ 为什么抽出来:三档自治(设计 §三 硬约束②「判定必须在既有资格闸通过
2905
+ 之后」)需要在**不重复、不重排、不放宽**这三道既有判据的前提下,先让
2906
+ 资格闸跑完再问档位。判据只此一份——`forget`(库层原路径)与
2907
+ `forget_gated`(工具面档位路径)共用,不各写一份必然漂移。
2908
+
2909
+ 返回 (entry, None) 表示资格通过(entry = 索引条目);(None, err) 表示
2910
+ 资格未过(err 即既有 forget 的返回体;保护闸抛出的异常照旧外抛,不在
2911
+ 此吞掉——调用方看到的语义与改动前逐位一致)。
2482
2912
  """
2483
2913
  # N195(2026-09-28):删除面与写面同源,先做索引代际感知。此前直读
2484
2914
  # 本进程内存索引——他进程刚写入的 anchor/self 节点在本进程索引中
@@ -2487,8 +2917,28 @@ class MdCGOS(MdCG):
2487
2917
  self._maybe_reload_index()
2488
2918
  e = self.index["nodes"].get(node_id)
2489
2919
  if not e:
2490
- return {"ok": False, "error": "not_found"}
2920
+ return None, {"ok": False, "error": "not_found"}
2491
2921
  protect.guard_forget(self, node_id, override=override, actor=self.actor)
2922
+ return e, None
2923
+
2924
+ def forget(self, node_id: str, reason: str = "", override: bool = False):
2925
+ """软删除:节点文件移入 trash/,写入删除清单(payload-free)。
2926
+
2927
+ 写保护:受保护节点(self/anchor 层、protected 标记、importance≥0.7)
2928
+ 不可遗忘——需显式 override=True,且旧版本先快照、动作全程留痕。
2929
+
2930
+ **库层语义不变**(三档自治批次②边界):本方法不读档位、不出变更单——
2931
+ 档位判定在工具面 `forget_gated`(MCP `cg(op=forget)` / `mdcg_forget`),
2932
+ 脚本通道(`scripts/dsh_log_index.py` 等直调本方法)现状逐位不变。
2933
+ """
2934
+ e, err = self._forget_qualify(node_id, override=override)
2935
+ if err is not None:
2936
+ return err
2937
+ return self._forget_apply(node_id, e, reason)
2938
+
2939
+ # 生效条件:e 为 _forget_qualify 通过后的索引条目时执行既有软删体(os.replace 进 trash_dir、写 deletions_log、_unstage、缓存失效、audit);publish 抛 OSError 时返回 {"ok": False, "error": 异常字符串};成功返回 {"ok": True, "id", "tombstone"};
2940
+ def _forget_apply(self, node_id: str, e: dict, reason: str = ""):
2941
+ """软删**执行体**(单点):与改动前的 `forget` 体逐位一致。"""
2492
2942
  src = os.path.join(self.root, e["path"])
2493
2943
  fm, content = self._read(e)
2494
2944
  h = _sig(content or "", 16)
@@ -2510,6 +2960,43 @@ class MdCGOS(MdCG):
2510
2960
  self._audit("forget", node_id, reason=reason, payload_hash=h)
2511
2961
  return {"ok": True, "id": node_id, "tombstone": h}
2512
2962
 
2963
+ # 生效条件:先跑既有资格前置闸 _forget_qualify(未过即原样返回其 err,含 not_found 与 protect.guard_forget 抛出);资格通过后以 autonomy_modes.decide(D_DELETE) 判定——ALLOW 走 _forget_apply 软删并返回其原返回体;FORBID 返回 ok=False/moved_to="autonomy_forbidden" 终态;CONFIRM 经 autonomy_modes.propose_mutation 出变更单(目标 node_id、后像为空、复现参数 override 入载荷)并返回 ok=True/committed=False/deleted=False/moved_to="review_queue" 的终态(**不软删**);
2964
+ def forget_gated(self, node_id: str, reason: str = "", override: bool = False):
2965
+ """软删的**档位路径**(三档自治批次② · 设计 §三 D 删除):MCP 工具面专用。
2966
+
2967
+ 位置(硬约束②「纯加严」):`protect.guard_forget` 等既有资格判据跑完
2968
+ 之后才问档位——档位**不参与**资格判定、也**不放宽**任何判据:受保护
2969
+ 节点在这里照旧被 `ProtectionError` 拦下(不是先出单再拦)。
2970
+
2971
+ 边界(如实):脚本通道直调 `MdCGOS.forget`(库层原路径)**不接**本档位
2972
+ ——本批只覆盖 MCP 工具面两个落点(`cg(op=forget)` / `mdcg_forget`)。
2973
+ """
2974
+ e, err = self._forget_qualify(node_id, override=override)
2975
+ if err is not None:
2976
+ return err
2977
+ dec = autonomy_modes.decide(autonomy_modes.D_DELETE)
2978
+ if dec["decision"] == autonomy_modes.ALLOW:
2979
+ return self._forget_apply(node_id, e, reason)
2980
+ aut = {"mode": dec["mode"], "action": dec["action"],
2981
+ "action_name": dec["action_name"], "decision": dec["decision"]}
2982
+ if dec["decision"] == autonomy_modes.FORBID:
2983
+ return {"ok": False, "id": node_id, "deleted": False,
2984
+ "moved_to": "autonomy_forbidden", "error": "autonomy_forbid",
2985
+ "autonomy": aut, "hint": dec["hint"]}
2986
+ pay = autonomy_modes.mutation_payload(
2987
+ autonomy_modes.D_DELETE, node_id, after="",
2988
+ reason=reason or "删除(MCP forget 工具面)", primitive="forget",
2989
+ meta={"override": bool(override)})
2990
+ pr = autonomy_modes.propose_mutation(
2991
+ self, autonomy_modes.D_DELETE, node_id, payload=pay,
2992
+ layer=e.get("layer") or "knowledge",
2993
+ sensitivity=e.get("sensitivity"), info=True)
2994
+ return {"ok": True, "id": node_id, "committed": False, "deleted": False,
2995
+ "moved_to": "review_queue", "pid": pr["pid"], "mutation": pay,
2996
+ "autonomy": aut,
2997
+ "hint": "%s 本次未删除(节点原样在位):变更单已入审核队列"
2998
+ "(pid=%s,目标 %s)。" % (dec["hint"], pr["pid"], node_id)}
2999
+
2513
3000
  # 生效条件:无输入形参;调用即返回 list(read_jsonl(self.deletions_log)),记录内容取决于 deletions_log 可读结果;
2514
3001
  def deletions(self):
2515
3002
  return list(read_jsonl(self.deletions_log))
@@ -3412,7 +3899,176 @@ class MdCGOS(MdCG):
3412
3899
 
3413
3900
  # ---- 主动遗忘(写入侧三问闸门)+ 写保护盘点 ----
3414
3901
 
3415
- # 生效条件:kw 中 gated 为假值时旁路直接 ACCEPT 写入并返回 bypass;否则 writelimit.check 非 None 时按 CONVERGE→MERGE 并经 converge_into(带 session 归属)并入 target、DROP/DEFER 只记 forgetting 日志,无限流拦截时按 forgetting.assess 的四态处理(ACCEPT 走 add;MERGE 走 reinforce(带 content 与 session,返回体带 content_sink 去向);DROP 经 forgetting.record_drop 落去向留痕并带 dropped 去向单;DEFER 不落库只留痕)。
3902
+ # 生效条件:以 autonomy_modes.decide(B_MERGE) 判定——ALLOW 返回 None(调用方照旧执行合并原语,merge 裁决本身一字不改);FORBID 返回 verdict="FORBID"/moved_to="autonomy_forbidden" 的终态;CONFIRM 经 autonomy_modes.propose_mutation 出变更单(目标 tgt、后像 content、复现参数 override/session 入载荷 meta)并返回 verdict="CONFIRM"/moved_to="review_queue" 的终态(**不调用任何合并原语、不落盘**);
3903
+ def _autonomy_gate_merge(self, node_id, tgt, content, kw, primitive,
3904
+ reason="", gate=None):
3905
+ """三档自治档位在 **B 合并**出口的判定单点(设计 §三:变更确认档需确认)。
3906
+
3907
+ 为什么是单点:合并的**两个落点**(`writelimit.check` 的 CONVERGE →
3908
+ `converge_into`、`forgetting.assess` 的 MERGE → `reinforce`)都必须
3909
+ 过同一判据——两处各写一份必然漂移;且判定必须晚于**各自的裁决**
3910
+ (限流闸 / 遗忘三问),档位不参与裁决(纯加严)。
3911
+
3912
+ 变更单载荷:动作类 B · 目标 tgt · 后像 = 本次新内容 · 理由 · 复现参数
3913
+ (override/session——merge 原语的两个非默认槽,裁决 accept 时按此重放)。
3914
+ 提议者/会话由 propose 的库端快照自动归属(`actor`/`session` 字段)。
3915
+ """
3916
+ from . import autonomy_modes as _am
3917
+ # 资格在先(纯加严):合并落盘时 reinforce/converge_into 内部的
3918
+ # guard_overwrite 若会拒(受保护/越权),必须在**出单之前**照样拒——
3919
+ # 否则一次本该报错的合并会变成「静默入队」。
3920
+ self.write_qualify(tgt, override=bool(kw.get("override")),
3921
+ actor=self.actor)
3922
+ dec = _am.decide(_am.B_MERGE)
3923
+ if dec["decision"] == _am.ALLOW:
3924
+ return None
3925
+ aut = {"mode": dec["mode"], "action": dec["action"],
3926
+ "action_name": dec["action_name"], "decision": dec["decision"]}
3927
+ if dec["decision"] == _am.FORBID:
3928
+ # 形态对齐原 MERGE 出口(gate / merged_into 两键照带):调用方按
3929
+ # 「gate.reason」取值读判断依据的既有读法不因本闸断裂。
3930
+ return {"verdict": "FORBID", "node_id": node_id, "gate": gate,
3931
+ "merged_into": tgt, "moved_to": "autonomy_forbidden",
3932
+ "autonomy": aut, "hint": dec["hint"]}
3933
+ # 队列的层闸(MdCGSecure.propose)按**目标节点的真层**校验:合并落点
3934
+ # 本来就是目标层,用调用方声明的层会让闸与落点错位(issue50 前车:
3935
+ # 校验层与落盘层错位会造死提案)。
3936
+ tlayer = ((self.index.get("nodes") or {}).get(tgt) or {}).get("layer") \
3937
+ or ((self.get(tgt) or {}).get("frontmatter") or {}).get("layer") \
3938
+ or "contextual"
3939
+ pay = _am.mutation_payload(
3940
+ _am.B_MERGE, tgt, after=content,
3941
+ reason=reason or ("档位确认(变更确认档):新写入内容并入既有节点 %s"
3942
+ "(原语 %s)" % (tgt, primitive)),
3943
+ primitive=primitive,
3944
+ meta={"override": bool(kw.get("override")),
3945
+ "session": _writer_session(self, kw)})
3946
+ pr = _am.propose_mutation(self, _am.B_MERGE, tgt, payload=pay,
3947
+ layer=tlayer,
3948
+ sensitivity=kw.get("sensitivity"), info=True)
3949
+ return {"verdict": "CONFIRM", "node_id": node_id, "gate": gate,
3950
+ "merged_into": tgt, "moved_to": "review_queue",
3951
+ "pid": pr["pid"], "mutation": pay, "autonomy": aut,
3952
+ "hint": "%s 本次未合并:变更单已入审核队列(pid=%s,目标 %s,"
3953
+ "新内容随单)。" % (dec["hint"], pr["pid"], tgt)}
3954
+
3955
+ # 生效条件:以 forgetting.prior_node 判「同 id 已存在」(存在=C 改写、不存在=A 新增)后交 autonomy_modes.decide 判定——ALLOW 返回 None(调用方照旧 add 落盘,逐位不变);FORBID 返回 ok=False/verdict="FORBID"/moved_to="autonomy_forbidden" 的终态(fail-closed,不落盘、不出单);CONFIRM 先经 self.write_qualify 跑既有资格探针(受保护/越权覆写照旧当场抛出、不静默入队)再经 autonomy_modes.propose_mutation 出变更单并返回 ok=True/verdict="CONFIRM"/committed=False/moved_to="review_queue" 的终态(**不落盘**);既有节点(C 改写)confirm/plan 两档出单、full 档放行;新节点(A 新增)confirm/full 两档放行、plan 档 FORBID(计划外零变更);
3956
+ def _autonomy_gate_rewrite(self, node_id, content, layer, kw,
3957
+ override=False, reason="", gate=None):
3958
+ """三档自治档位在 **gated 面「C 改写 / A 新增」**出口的判定单点。
3959
+
3960
+ 为什么要它(补强批次 2026-10-02,复核发现 1/2):`gated=true` 的写入
3961
+ 在上游 `writepipe._gate_gated` 是**替代执行路径**(`remember_gated`
3962
+ 自行落盘后直接返回终态,链尾 `_gate_autonomy` 根本不执行)——而
3963
+ `remember_gated` 的 ACCEPT 分支此前**无任何档位判据**、直调 `self.add`:
3964
+ 缺省(confirm)档对**既有 node_id 的覆写**直接落盘(不出变更单)、
3965
+ 计划档对**新 node_id** 也照样写入。二者都与设计 §三(C=需确认、
3966
+ 计划外零变更)相抵。本方法是该面**唯一**的档位判定点(与写链
3967
+ `writepipe._gate_autonomy`、合并面 `_autonomy_gate_merge` 同口径)。
3968
+
3969
+ 位置(纯加严,设计 §三 硬约束②):遗忘闸四态裁决(`forgetting.assess`)
3970
+ 之后、`self.add` 落盘之前;档位**不参与**裁决、也不放宽任何既有判据。
3971
+
3972
+ 动作类判定:`forgetting.prior_node(self, node_id)` 同 id 存在 ⇒
3973
+ **C 改写**;不存在 ⇒ **A 新增**——判据复用同一单点,不另写一份存在性判据。
3974
+
3975
+ 资格在先:既有节点出单**之前**先 `self.write_qualify`(与 add 内部同一批
3976
+ protect 单点,只判不写)——受保护/越权覆写照旧当场 `ProtectionError`/
3977
+ `AccessDenied`,不会变成「静默入队」(放宽既有判据)。
3978
+
3979
+ 载荷 meta(复现原动作):取**写链同一份白名单** `writepipe.
3980
+ _AUTONOMY_META_KEYS`(延迟导入,单一真源),并补 gated 面独有的 fm 扩展键
3981
+ `role`——直写路径 `self.add(..., **kw)` 会把 role 经 add 的 `**extra`
3982
+ 落 fm,重放不带就会「出单→确认→写」与直写两条路径 fm 不等价。
3983
+ 边界(如实):`kw` 里的 `session` 不在本白名单内、且执行桥会把它
3984
+ 收进 B 原语的 session 槽(C 面不复现)——当前两个 gated 调用点
3985
+ (`writepipe._gate_gated` / `mcp_server.mdcg_remember`)都不传 session。
3986
+
3987
+ 返回形态:出单 = `ok=True/committed=False/moved_to="review_queue"`
3988
+ 且带 pid(口径同写链 `_gate_autonomy`);FORBID = `ok=False/
3989
+ moved_to="autonomy_forbidden"`(口径同写链 A 面);ALLOW = `None`
3990
+ (调用方原路落盘,行为逐位不变)。
3991
+ """
3992
+ from . import autonomy_modes as _am
3993
+ # 载荷 meta 白名单的单一真源在写链(写面唯一定义处);延迟导入防循环。
3994
+ from . import writepipe as _wp
3995
+ prior = forgetting.prior_node(self, node_id)
3996
+ action = _am.C_REWRITE if prior is not None else _am.A_ADD
3997
+ if prior is not None:
3998
+ # 资格在先(纯加严):同 `_autonomy_gate_merge` / `writepipe.
3999
+ # _gate_autonomy`——覆写的层闸/写保护闸住在 `self.add` 内部,档位
4000
+ # 先出单会把一次本该报错的覆写变成「静默入队」(放宽既有判据)。
4001
+ self.write_qualify(node_id, target_layer=layer,
4002
+ override=bool(override), actor=self.actor)
4003
+ dec = _am.decide(action)
4004
+ if dec["decision"] == _am.ALLOW:
4005
+ return None
4006
+ aut = {"mode": dec["mode"], "action": dec["action"],
4007
+ "action_name": dec["action_name"], "decision": dec["decision"]}
4008
+ if dec["decision"] == _am.FORBID:
4009
+ return {"ok": False, "verdict": "FORBID", "node_id": node_id,
4010
+ "committed": False, "gate": gate,
4011
+ "moved_to": "autonomy_forbidden", "error": "autonomy_forbid",
4012
+ "autonomy": aut, "hint": dec["hint"]}
4013
+ meta = {k: kw[k] for k in _wp._AUTONOMY_META_KEYS
4014
+ if kw.get(k) is not None}
4015
+ if kw.get("role") is not None:
4016
+ meta["role"] = kw["role"]
4017
+ if override:
4018
+ meta["override"] = True
4019
+ pay = _am.mutation_payload(
4020
+ action, node_id, after=content,
4021
+ reason=reason or ("gated 面覆写(gated=true / remember_gated 对既有"
4022
+ "节点 %s 的 %s)" % (node_id, _am.ACTION_NAMES[action])),
4023
+ primitive="add", meta=meta)
4024
+ pr = _am.propose_mutation(self, action, node_id, payload=pay,
4025
+ layer=layer,
4026
+ sensitivity=kw.get("sensitivity"), info=True)
4027
+ return {"ok": True, "verdict": "CONFIRM", "node_id": node_id,
4028
+ "committed": False, "gate": gate, "moved_to": "review_queue",
4029
+ "pid": pr["pid"], "mutation": pay, "autonomy": aut,
4030
+ "hint": "%s 本次未落盘:变更单已入审核队列(pid=%s,目标 %s)。"
4031
+ % (dec["hint"], pr["pid"], node_id)}
4032
+
4033
+ # 生效条件:node_id 不在索引中(新节点)时返回 None(新写入面没有「既有资格闸」,其层闸由落盘面把守);在索引中时以 override=False 跑 protect.guard_overwrite(既有节点覆写前的统一双闸:principal 层闸按**既有节点真层** + 引擎级写保护闸)与(target_layer 非空且与真层不同时)protect.guard_move(受保护节点移出保护层的降级闸),任一拒绝抛出的 AccessDenied 一律照抛、ProtectionError 仅在 override 为假时照抛(override 为真时放行——真实写入那一次会带授权并写快照,本探针**不写快照**);全部通过返回 None;本函数不落盘、不改索引、不写任何快照;
4034
+ def write_qualify(self, node_id, target_layer=None, override=False,
4035
+ actor=None):
4036
+ """既有节点写面的**资格探针**(三档自治:档位判定必须晚于既有资格闸)。
4037
+
4038
+ 为什么要它(纯加严,设计 §三 硬约束②):写面落盘时的两道资格闸
4039
+ ——`protect.guard_overwrite`(层闸 + 写保护闸)与 `protect.guard_move`
4040
+ (降级闸)——住在 `MdCG.add` **内部**。若档位闸在 add 之前就判「出变更
4041
+ 单」,一次本该被拒的越权/受保护覆写就从 `AccessDenied`/`ProtectionError`
4042
+ 变成「静默入队」——那是**放宽既有判据**(未授权者凭空获得一个排单出口)。
4043
+ 故档位判定前先用**同一批单点**跑一遍资格:判据只有一份(protect 的那些
4044
+ 单点),这里只负责按 add 的次序与范围调用它们。
4045
+
4046
+ 无副作用边界(如实):override=False 时 guard_write/guard_move 只判不写;
4047
+ override=True 的情形**不由本探针**产生快照——快照归真实写入那一次
4048
+ (`_allow` → `_protected_history`)。故本探针不得被当作「授权检查」以外的
4049
+ 用途(它不建目录、不落文件、不动索引)。
4050
+ """
4051
+ e = (self.index.get("nodes") or {}).get(node_id)
4052
+ if not e:
4053
+ return None
4054
+ if target_layer and str(e.get("layer") or "") \
4055
+ and str(e.get("layer")) != str(target_layer):
4056
+ try:
4057
+ protect.guard_move(self, node_id, target_layer, override=False,
4058
+ actor=actor)
4059
+ except protect.ProtectionError:
4060
+ if not override:
4061
+ raise
4062
+ try:
4063
+ # 层闸按**既有节点真层**解析(layer=None ⇒ protect._resolve_target
4064
+ # 取索引/ fm 的真层)——与 add 的 N221 跨层覆写同口径(源层在先)。
4065
+ protect.guard_overwrite(self, node_id, override=False, actor=actor)
4066
+ except protect.ProtectionError:
4067
+ if not override:
4068
+ raise
4069
+ return None
4070
+
4071
+ # 生效条件:kw 中 gated 为假值时旁路直接 ACCEPT 写入并返回 bypass;否则 writelimit.check 非 None 时按 CONVERGE→MERGE 并经 converge_into(带 session 归属)并入 target、DROP/DEFER 只记 forgetting 日志,无限流拦截时按 forgetting.assess 的四态处理(ACCEPT **先过档位闸 `_autonomy_gate_rewrite`**——既有 node_id ⇒ C 改写(confirm/plan 出单且不落盘、full 放行)、不存在 ⇒ A 新增(confirm/full 放行、plan fail-closed `autonomy_forbidden`),放行才走 add——issue50-d:落盘 importance 恒等于裁决 imp["score"](hint 或启发式)并落 importance_source("hint"|"heuristic"),add 的自动保护位只认显式声明;MERGE 走 reinforce(带 content 与 session,返回体带 content_sink 去向);DROP 经 forgetting.record_drop 落去向留痕并带 dropped 去向单;DEFER 不落盘、经既有入队单点 self.propose 把**正文 + 声明 meta(**kw)+「为何待定」**送进审核队列(返回体带 proposed=pid;node_id 非字符串时兜底成内容派生 id 并写 proposed_id_fallback),并照旧只记 forgetting 留痕)。
3416
4072
  def remember_gated(self, node_id, content, layer="contextual", **kw):
3417
4073
  """写入情景层记忆前的**主动遗忘闸门**:三问 → 四态。
3418
4074
 
@@ -3424,6 +4080,34 @@ class MdCGOS(MdCG):
3424
4080
  DROP 丢弃(低熵噪音)/ DEFER 待定(不写)。
3425
4081
  四种结果都写进 `_forgetting.jsonl`,可审计。
3426
4082
 
4083
+ issue50-b(2026-10-01):DEFER 此前**只留一行痕**——不落盘、无收件箱。
4084
+ issue50-a 把半重复判 DEFER 后,这条出口的语义(「待定**复核**」)与
4085
+ 实际(静默不记)分叉:噪声缺陷会换成「不落盘且无收件箱」缺陷。故
4086
+ DEFER 出口接线**既有入队单点** `self.propose`(与审计/策略闸
4087
+ `writepipe._gate_audit`、一致性闸 `_gate_consistency` 同一条链、同一
4088
+ payload_hash 幂等对账):正文随提案进审核队列,裁决 accept 即落盘;
4089
+ 「为何待定」写进提案记录的 `extra.defer_reason`(propose 的既有任意槽,
4090
+ 不新增队列字段/队列类型)。
4091
+
4092
+ issue50-c F1(2026-10-01,扩面修):入队**只带 content/layer/sensitivity**,
4093
+ 调用方声明的其余 meta 全丢——accept 只能从队列记录取值,故「同一声明」下
4094
+ DEFER→accept 落盘的节点比直接 ACCEPT 少 tags/condition_space/
4095
+ verification_basis/non_applicable_conditions/derived_from/relation/role
4096
+ (实测 tags=[] 而对面有值、derived_from 缺失、bucket_zh 因 tags 空而不生成)。
4097
+ 处置:入队时把本闸收到的 meta 一并落进队列记录(`**kw` → `self.propose`,
4098
+ 口径与直接 ACCEPT 的 `add(**kw)` 同源),accept 分支既有的
4099
+ `tags=/condition_space=/**extra` 映射即把它写回节点 fm——**两条路径元数据
4100
+ 等价**,且 review_decide 无需改动。
4101
+
4102
+ issue50-c F2(2026-10-01):`node_id=None` 时 propose 造 pid 的
4103
+ `node_id + str(…)` 抛 TypeError;按仓内既有形态(`forgetting._prefeed_id`)
4104
+ 兜底成内容派生 id,不崩且非静默(返回体 `proposed_id_fallback`)。
4105
+
4106
+ 接线只覆盖**本闸自己**的 DEFER——
4107
+ writelimit 限流 DEFER 在上方已 return(其语义是「先别写」,原文在
4108
+ recent 时间线,不是内容待定),gated=False 旁路与 ACCEPT 后因
4109
+ on_conflict=defer 降级的 DEFER(`out["conflict"]`)都不在此列。
4110
+
3427
4111
  B3(2026-09-30):MERGE 分支此前**不把 content 往下传**(`reinforce`
3428
4112
  也没有该槽)——近重复的第二次写入,其新正文 100% 丢弃,只有既有节点
3429
4113
  被强化;现传 content + session,新正文按目标缺的行追加为聚合行落盘,
@@ -3431,6 +4115,26 @@ class MdCGOS(MdCG):
3431
4115
  来源,`assess` :214 先于 MERGE)把新正文全文与去向记进 `_forgetting.jsonl`
3432
4116
  并返回 `dropped` 去向单(trace_id/sha1/检索入口)。两条分支都有处置。
3433
4117
  H3:合并时把写入方会话归属并列落目标 fm(`fm.merge_sources`)。
4118
+
4119
+ issue50-d(2026-10-02):ACCEPT 分支**落盘=裁决值**——此前「hint 非
4120
+ None 才透传」的口径把启发式裁决值(0.84/0.71 量级,隔离沙箱实证)
4121
+ 全部丢在闸门上,落盘一律默认 0.5;现无条件落 `imp["score"]` 并按
4122
+ `imp["from"]` 落 `importance_source`("hint"|"heuristic",进 fm 与
4123
+ 索引条目)。`add` 的自动保护位只认显式声明:heuristic 过线只落分、
4124
+ 不打保护位——由此审计面文案(assess 保护分支按 from 分叉)、落盘分、
4125
+ 保护位三者首次同源一致。外部基准(hive-memory-bench v1.0)测得 L3
4126
+ 使用端损耗 3-4/19:本批让重要度轴带电(排序次级键从此拿到真分)。
4127
+
4128
+ 补强批次(2026-10-02,复核发现 1/2 的收口):ACCEPT 分支此前**无任何
4129
+ 档位判据**、直调 `self.add`——`gated=true`(插件自动记忆主写入通道)
4130
+ 在缺省 confirm 档对既有 node_id 直接覆写(不出变更单)、在计划档对新
4131
+ node_id 也照样写入。现该分支落盘前先经 `_autonomy_gate_rewrite`
4132
+ (gated 面档位判定单点,与写链 `writepipe._gate_autonomy` 同口径、
4133
+ 同一存在性单点 `forgetting.prior_node`):C 改写 confirm/plan 出单、
4134
+ full 放行;A 新增 confirm/full 放行(行为逐位不变)、plan fail-closed。
4135
+ 出单前先跑 `write_qualify` 资格探针——受保护/越权覆写照旧当场拒,
4136
+ 不因「改为入队」而放宽既有判据(纯加严)。出单/拒绝都写一行
4137
+ `_forgetting.jsonl`(verdict=CONFIRM/FORBID + hint),可审计。
3434
4138
  """
3435
4139
  role = kw.get("role")
3436
4140
  vb = kw.get("verification_basis")
@@ -3462,6 +4166,18 @@ class MdCGOS(MdCG):
3462
4166
  if lim is not None:
3463
4167
  if lim["verdict"] == "CONVERGE":
3464
4168
  tgt = lim["target"]
4169
+ # 三档自治批次②:档位判定在**限流闸裁决之后、合并原语之前**。
4170
+ _ag = self._autonomy_gate_merge(node_id, tgt, content, kw,
4171
+ primitive="converge_into",
4172
+ gate=lim)
4173
+ if _ag is not None:
4174
+ forgetting.log(self, {"t": time.time(), "node_id": node_id,
4175
+ "layer": layer,
4176
+ "verdict": _ag["verdict"],
4177
+ "reason": _ag["hint"],
4178
+ "limiter": lim.get("limiter"),
4179
+ "actor": self.actor})
4180
+ return _ag
3465
4181
  out = {"verdict": "MERGE", "node_id": node_id,
3466
4182
  "merged_into": tgt, "gate": lim,
3467
4183
  "converged": writelimit.converge_into(
@@ -3489,8 +4205,39 @@ class MdCGOS(MdCG):
3489
4205
  v = verdict["verdict"]
3490
4206
  out = {"verdict": v, "node_id": node_id, "gate": verdict}
3491
4207
  if v == "ACCEPT":
3492
- if hint is not None and "importance" not in kw:
3493
- kw["importance"] = hint
4208
+ # issue50-d(2026-10-02):**落盘=裁决值**。此前「hint 非 None 才
4209
+ # 透传」的口径把启发式裁决值全部丢在闸门上——隔离沙箱实证:
4210
+ # u_heur_new 裁决 0.837/落盘 0.5、t_heur 裁决 0.711/落盘 0.5,落盘
4211
+ # 重要度不携带任何区分信息(启发式全 0.5、插件 hint 通道全 0.6),
4212
+ # 排序/使用端拿不到真分。现无条件落 `imp["score"]`(hint 或启发式,
4213
+ # 均为 assess 实际使用的裁决值,含 0~1 裁剪),并按裁决来源落
4214
+ # `importance_source`("hint"|"heuristic")进 fm 与索引条目——
4215
+ # add 的自动保护位据此只认显式声明(heuristic 过线只落分、不打
4216
+ # 保护位,与裁决②一致;writelimit 的保护豁免仍读原始 hint,不受
4217
+ # 本批影响)。
4218
+ kw["importance"] = verdict["importance"]["score"]
4219
+ kw["importance_source"] = verdict["importance"]["from"]
4220
+ # 三档自治批次②补强(2026-10-02):**gated 面的 C 改写 / A 新增档位
4221
+ # 判定**。此前本分支直调 `self.add`——`gated=true` 的覆写在确认档
4222
+ # 不落任何档位判定就覆写既有节点(复核发现 1:插件自动记忆的主写入
4223
+ # 通道可旁路确认档),计划档的新增也不被拦(发现 2)。判定与写链
4224
+ # `writepipe._gate_autonomy` **同口径**、同一存在性单点
4225
+ # (`forgetting.prior_node`):既有 node_id ⇒ C 改写(confirm/plan
4226
+ # 出单、full 放行)、不存在 ⇒ A 新增(confirm/full 放行、plan
4227
+ # fail-closed);出单前先跑 write_qualify 资格探针(受保护/越权覆写
4228
+ # 照旧当场拒,不静默入队)。位置在 importance 落盘值写入之后,故
4229
+ # 变更单载荷带的是**裁决值**(issue50-d 同源口径)。
4230
+ _ag = self._autonomy_gate_rewrite(node_id, content, layer, kw,
4231
+ override=override, gate=verdict)
4232
+ if _ag is not None:
4233
+ forgetting.log(self, {"t": time.time(), "node_id": node_id,
4234
+ "layer": layer,
4235
+ "verdict": _ag["verdict"],
4236
+ "reason": _ag["hint"],
4237
+ "importance": verdict["importance"],
4238
+ "entropy": verdict["entropy"],
4239
+ "actor": self.actor})
4240
+ return _ag
3494
4241
  try:
3495
4242
  out["written"] = self.add(node_id, content, layer=layer,
3496
4243
  override=override,
@@ -3508,6 +4255,24 @@ class MdCGOS(MdCG):
3508
4255
  writelimit.record_accepted(self, node_id, content)
3509
4256
  elif v == "MERGE":
3510
4257
  tgt = verdict["redundancy"]["with"]
4258
+ # 三档自治批次②:档位判定在**遗忘闸四态裁决之后、合并原语之前**
4259
+ # (纯加严——裁决仍归 forgetting.assess,档位只决定「立即合并」还是
4260
+ # 「出变更单」)。合并对象为空(tgt 假值)时不上报变更单:原路径
4261
+ # 的 reinforce 也只在 tgt 为真时执行,档位不得把「无事发生」变成
4262
+ # 「一张凭空指向空目标的单」。
4263
+ if tgt:
4264
+ _ag = self._autonomy_gate_merge(node_id, tgt, content, kw,
4265
+ primitive="reinforce",
4266
+ gate=verdict)
4267
+ if _ag is not None:
4268
+ forgetting.log(self, {"t": time.time(), "node_id": node_id,
4269
+ "layer": layer,
4270
+ "verdict": _ag["verdict"],
4271
+ "reason": _ag["hint"],
4272
+ "importance": verdict["importance"],
4273
+ "entropy": verdict["entropy"],
4274
+ "actor": self.actor})
4275
+ return _ag
3511
4276
  out["merged_into"] = tgt
3512
4277
  # B3 ②(2026-09-30):把**新正文**与写入方归属一并传下去。此前只传
3513
4278
  # tgt——调用方手上正握着 content 与 node_id 却不往下传,reinforce 写回
@@ -3531,6 +4296,56 @@ class MdCGOS(MdCG):
3531
4296
  self, content, node_id=node_id, target=tgt, verdict=verdict,
3532
4297
  layer=layer, sensitivity=kw.get("sensitivity"),
3533
4298
  session=_writer_session(self, kw), actor=self.actor)
4299
+ elif v == "DEFER":
4300
+ # issue50-b(2026-10-01):遗忘闸门的 DEFER 此前只写一行
4301
+ # `_forgetting.jsonl` 留痕——**不落盘且无收件箱**(对比 DROP 分支
4302
+ # 有 `forgetting.record_drop` 留全文 + trace_id)。第①条(issue50-a)
4303
+ # 把半重复从「落成近似重复节点」改成 DEFER 之后,这条出口就会把
4304
+ # 噪声缺陷换成「静默不记」缺陷。处置:接线**既有入队单点**
4305
+ # `self.propose`——与审计/策略闸(`writepipe._gate_audit`)、一致性闸
4306
+ # (`_gate_consistency`)**同一条入队链**(不新增第二套入队实现),
4307
+ # 复用其 payload_hash 幂等对账(同内容重复写入不长第二条)。
4308
+ # 只覆盖**本闸自己的** DEFER——writelimit 限流 DEFER 在上方已 return
4309
+ # (语义是「先别写」,原文在 recent 时间线,不是内容待定);
4310
+ # gated=False 旁路与 ACCEPT 后 on_conflict=defer 的降级 DEFER 都不走
4311
+ # 本分支(后者发生在 `if v == "ACCEPT"` 内,v 就地改写)。
4312
+ # 「为何待定」写进提案的既有任意槽 extra(不新增队列字段/队列类型,
4313
+ # 见 propose 的 `**kw → extra`);密级随 sensitivity 一并透传,避免
4314
+ # 声明 private 在此静默降级 internal(B2 同一漏传族)。
4315
+ #
4316
+ # issue50-c F1(2026-10-01,扩面修):此前入队只带 content/layer/
4317
+ # sensitivity,**调用方声明的其余 meta 全丢**(tags/condition_space/
4318
+ # verification_basis/non_applicable_conditions/derived_from/relation/
4319
+ # role…)。accept 只能从队列记录取值,于是「同一声明」下
4320
+ # DEFER→accept 落盘的节点比「直接 ACCEPT」少掉这些键(实测 tags=[]
4321
+ # 而对面有值、derived_from 缺失、role 缺失、bucket_zh 因 tags 空而
4322
+ # 不生成)——同一份声明的两条落地路径元数据不等价。处置:把本闸收到
4323
+ # 的那组 meta **随入队一并落进队列记录**(`**kw` 交给 `self.propose`:
4324
+ # 其形参 `tags`/`condition_space`/`sensitivity`/`verify` 落 rec 专属槽,
4325
+ # 其余键落 `extra`——与直接 ACCEPT 走 `add(**kw)` 的口径同源,不新增
4326
+ # 队列字段/队列类型);accept 分支早已把 `rec.tags`/`rec.condition_space`
4327
+ # /`**extra` 映射回 `add`(mdcos.py:2573),故无需改 review_decide。
4328
+ #
4329
+ # issue50-c F2(2026-10-01):`node_id=None` 时上文 `**kw` 透传依旧,
4330
+ # 但 propose 造 pid 用 `node_id + str(time.time()) + …`——None + str
4331
+ # 抛 TypeError。本闸的 ACCEPT/MERGE/DROP 三个出口都会经 `add` 对
4332
+ # 非字符串 node_id 抛 ValueError(fail-closed 既有行为),唯独 DEFER
4333
+ # 出口的入队单点会以 TypeError 崩出。处置:按仓内既有形态兜底成
4334
+ # **内容派生 id**(`forgetting._prefeed_id` 同款——forgetting.py:879
4335
+ # 对「无 id 候选」即用 `pre_<sha1[:12]>`,同一语义的单点复用),
4336
+ # 使「DEFER 有收件箱」在缺 id 时同样成立、不崩;兜底事实写进返回体
4337
+ # `proposed_id_fallback`(非静默——调用方可判、可检索),**不进
4338
+ # forgetting 留痕行**(该行形状由 issue50-b 冻结)。
4339
+ _why = "遗忘闸门:" + str(verdict.get("reason") or "待定复核")
4340
+ _dup_with = (verdict.get("redundancy") or {}).get("with")
4341
+ if _dup_with:
4342
+ _why += "(重复对象 %s)" % _dup_with
4343
+ _nid = node_id if isinstance(node_id, str) and node_id else \
4344
+ forgetting._prefeed_id(content)
4345
+ out["proposed"] = self.propose(
4346
+ _nid, content, layer=layer, **kw, defer_reason=_why)
4347
+ if _nid != node_id:
4348
+ out["proposed_id_fallback"] = _nid
3534
4349
  # DROP 的去向留痕已由 record_drop 单点落盘(含全文/trace_id),此处只补
3535
4350
  # ACCEPT/MERGE/DEFER 的裁决留痕,避免同一次裁决出现两行。
3536
4351
  if v != "DROP":
@@ -4373,11 +5188,27 @@ class MdCGSecure(MdCGOS):
4373
5188
 
4374
5189
  # ---------- 管理:需 can_admin ----------
4375
5190
 
4376
- # 生效条件:先 principal.require_admin("forget"),再转 super().forget(node_id, reason, override=override)。
4377
- def forget(self, node_id: str, reason: str = "", override: bool = False):
5191
+ # 生效条件:principal 在位时调用 principal.require_admin("forget")(越权抛 AccessDenied),返回 None;
5192
+ def _forget_admin(self):
5193
+ """删除面的**管理权限闸单点**(三档自治批次②:两个入口共用一份口径)。
5194
+
5195
+ 为什么抽出来:`forget`(既有)与 `forget_gated`(档位路径)都必须先过
5196
+ 这道闸——`MdCGSecure` 的 can_admin 要求原先只写在 `forget` 里,档位路径
5197
+ 若直调 `_forget_apply` 就把它整个跳过了(实测:`MDCG_CAN_ADMIN=0` 的身份
5198
+ 经 `mdcg_forget` 能把节点删掉)。判据只此一份,两个入口都走它。
5199
+ """
4378
5200
  self.principal.require_admin("forget")
5201
+
5202
+ # 生效条件:先 _forget_admin(),再转 super().forget(node_id, reason, override=override);
5203
+ def forget(self, node_id: str, reason: str = "", override: bool = False):
5204
+ self._forget_admin()
4379
5205
  return super().forget(node_id, reason, override=override)
4380
5206
 
5207
+ # 生效条件:先 _forget_admin()(与 forget 同序同错型——管理闸在资格闸与档位判定**之前**),再转 super().forget_gated(node_id, reason, override=override);三档自治批次② 的 D 删除工具面路径由此把 can_admin 要求带进档位路径;
5208
+ def forget_gated(self, node_id: str, reason: str = "", override: bool = False):
5209
+ self._forget_admin()
5210
+ return super().forget_gated(node_id, reason, override=override)
5211
+
4381
5212
  # 生效条件:先 principal.require_admin("restore"),再转 super().restore(node_id, force=force)。
4382
5213
  def restore(self, node_id: str, force: bool = False):
4383
5214
  self.principal.require_admin("restore")