@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
@@ -25,8 +25,14 @@
25
25
  python -X utf8 -m md_cg.bench_e2e_locomo_qa --translate # 翻译 5882 turn + 500 问句
26
26
  python -X utf8 -m md_cg.bench_e2e_locomo_qa # 全量 500 题 × 2 臂
27
27
  python -X utf8 -m md_cg.bench_e2e_locomo_qa --quick # 冒烟 3 题(须先完成翻译)
28
- 环境:DEEPSEEK_API_KEY;建议 MDCG_UNIFY_QUERY=0(中文问句含英文名时批次15归一有
29
- 形态缺陷,见 docs/eval/端到端干扰池评测_v1.1 §4)。
28
+ 环境:DEEPSEEK_API_KEY;归一层**缺省关**(2026-09-30 使用者裁定:本层本职是让英文
29
+ query 命中中文节点,对中文检索池是纯开销)——批次15 归一曾把中文问句的中文段
30
+ 逐字切开(中文问句含英文名时的形态缺陷,见 docs/eval/端到端干扰池评测_v1.1 §4),
31
+ 该缺陷已由 2026-09-30 作用域收窄(批次 26:只译英文内容、中文段原样)修掉:
32
+ locomo-zh-500 定点实测 旧口径 lexical hit@1 94.4% / lexical,fuzzy 87.2% →
33
+ 收窄后 95.8% / 96.6%;而缺省关态为 96.4% / 97.2%(本脚本缺省即此态)。
34
+ `MDCG_UNIFY_QUERY=1` 保留为**英文对照口径**(归一层显式开,95.8% / 96.6%),
35
+ 不再是缺省值(口径真源 docs/hive/检索算法口径对照_v0.1.md)。
30
36
  """
31
37
  from __future__ import annotations
32
38
 
@@ -271,7 +277,8 @@ def main(argv=None):
271
277
  cfg = {"model": a.model, "base": a.base_url, "key": key,
272
278
  "timeout": a.timeout, "workers": a.workers}
273
279
  print(f"[locomo_qa] 归一层开关 MDCG_UNIFY_QUERY="
274
- f"{os.environ.get('MDCG_UNIFY_QUERY', '1')}(建议 0,见模块头注)")
280
+ f"{os.environ.get('MDCG_UNIFY_QUERY', '0')}(缺省关;=1 为英文对照口径,"
281
+ f"见模块头注)")
275
282
 
276
283
  ids, txts = load_corpus()
277
284
  answers = json.load(open(ANSWERS, encoding="utf-8"))
@@ -353,7 +360,7 @@ def main(argv=None):
353
360
  errs = sum(1 for r in rows if r.get("error"))
354
361
  print(f"判分失败 {errs} 行\n")
355
362
  summary = {"meta": {"model": a.model, "n_q": len(qs), "k": a.k,
356
- "unify": os.environ.get("MDCG_UNIFY_QUERY", "1")}}
363
+ "unify": os.environ.get("MDCG_UNIFY_QUERY", "0")}}
357
364
  for arm in ("retrieval", "full_context"):
358
365
  summary[arm] = report([r for r in rows if r["arm"] == arm], arm)
359
366
  out = os.path.join(a.data_root, "results",
package/md_cg/chain.py CHANGED
@@ -24,6 +24,8 @@
24
24
  """
25
25
  from __future__ import annotations
26
26
 
27
+ import os
28
+
27
29
  # 边类型 → 传播权重 base(AEIS《激活引擎 v0.1》第 14 行)
28
30
  EDGE_WEIGHTS = {
29
31
  "causal": 0.85,
@@ -52,11 +54,44 @@ CAUSAL_TYPES = ("causal",)
52
54
  # 否则 causal 链会被「提到过」这类无向弱关联稀释(模式分离)。
53
55
  CHAIN_TYPES_DEFAULT = ("causal", "sequential", "applies_to")
54
56
 
57
+ #: 边类型集可配(P3-causal,2026-10-01):逗号分隔,大小写不敏感。
58
+ #: 动因(设计稿 §6.2 / 交接单)——**叙事/文档语料里没有 causal 边**:按章切出来
59
+ #: 的 doc_ref 库里只有 `reference`(弱权重 0.5,上方注释明写刻意不入默认集)与
60
+ #: 父章节结构边(`part_of`)。边类型集若写死,因果路在这类库上**恒空**,等于
61
+ #: 没接。故做成 env 可配,缺省仍是 `CHAIN_TYPES_DEFAULT`(缺省行为一字不变)。
62
+ #: 约束(不得违反):本处**只从既有 `EDGE_WEIGHTS` 里挑子集**——不新造边类型、
63
+ #: 不写第二份权重表;未登记的形态一律剔除(剔除即回落缺省,而不是按
64
+ #: `DEFAULT_EDGE_WEIGHT` 静默走一条没人登记过的边)。
65
+ #:
66
+ #: **可配面边界(P3 遗留 ④ 如实降级声明,2026-10-01)**:契约 S3 的不变量原文是
67
+ #: 「边方向/关系类型权重可配」,实测只有前一半做成了可配,后一半**不可配**:
68
+ #: · **类型集**:可配(本 env);
69
+ #: · **权重**:沿用**唯一表** `EDGE_WEIGHTS`(仓内「真源唯一」纪律——禁止
70
+ #: 第二份边权表;要调权重改表,不引入第二处口径);
71
+ #: · **方向**:固定为**依赖方向(出边)**(`walk(direction="out")` 为缺省,
72
+ #: `adjacency` 建的是出邻接;`direction="in"` 存在但不在检索路上用)。
73
+ #: ⇒ 对 S3 ④ 属**部分满足**,不得表述为「全满足」。
74
+ CHAIN_TYPES_ENV = "MDCG_CHAIN_TYPES"
75
+
55
76
  MAX_DEPTH_DEFAULT = 5
56
77
  MAX_DEPTH_HARD = 64
57
78
  MAX_NODES_DEFAULT = 500
58
79
 
59
80
 
81
+ # 生效条件:environ(缺省 os.environ)里 CHAIN_TYPES_ENV 为真值时按逗号切分、逐项 strip().lower()、只保留 EDGE_WEIGHTS 里登记过的形态;结果为空(未设 / 全是不认识的名字 / 只有空项)时回落 CHAIN_TYPES_DEFAULT。返回小写元组(顺序 = env 声明序)。
82
+ def chain_types_from_env(environ=None):
83
+ """关系链边类型集的**唯一读取点**(检索 chain 路与显式调用方共用)。
84
+
85
+ 形态与 `MDCG_SPREAD_DECAY` 同款:env 缺失 / 非法 → 回落缺省,不抛异常。
86
+ """
87
+ raw = (environ or os.environ).get(CHAIN_TYPES_ENV)
88
+ if not raw:
89
+ return CHAIN_TYPES_DEFAULT
90
+ kept = tuple(t for t in (str(x).strip().lower() for x in str(raw).split(","))
91
+ if t in EDGE_WEIGHTS)
92
+ return kept or CHAIN_TYPES_DEFAULT
93
+
94
+
60
95
  # 生效条件:edge 为 dict 时按 relation_type→relation→type 顺序取首个真值、非 dict 时 rel 记为 None,两者统一返回 str(rel or "").strip().lower()——键缺失或全为假值时得空串。
61
96
  def edge_rel(edge):
62
97
  """取边的 relation_type(兼容 dict / 字符串两种写法),统一小写。"""
@@ -130,6 +165,18 @@ def adjacency(cg, include_hierarchy=True):
130
165
  来源两处:
131
166
  1. 节点 frontmatter.edges(关系边,含 relation_type)
132
167
  2. 节点 frontmatter.subgraph.nodes(层级边,合成 part_of)
168
+
169
+ **代价口径(P3 遗留 ⑥ 如实降级声明,2026-10-01)**:本函数**不读正文**
170
+ (`subgraph._fm` 优先走索引快照,条目带 `subgraph` 键即零 IO),但其构建是
171
+ **O(N) 索引条目级**——`for nid in nodes` 遍历的是**全部索引节点**
172
+ (`cg.index["nodes"]`),不是「只碰种子邻域」。故契约 S3「不走全表」这一句
173
+ 对**本函数不成立**,正确表述是「不读正文;邻接构建为 O(N) 索引条目级」。
174
+ 代价读数(`scripts/p2p4_probe.py` R7,400 条目确定性小库):冷建 ~0.24ms、
175
+ 缓存命中 ~1µs;缓存键 `(include_hierarchy, 闸存在位)` 存于 `cg._chain_adj`,
176
+ 写入/删除后由 `invalidate_cache` 作废。
177
+ (可选硬化路径 (b1):在索引快照的**边集合**上增量构建邻接——本轮未做,
178
+ 如需做见 P3 遗留 B 的二选一。)
179
+
133
180
  只读索引快照,不读文件正文;结果缓存在 `cg._chain_adj`。
134
181
 
135
182
  读隔离(2026-09-25):cg._chain_visible(nid) 谓词在位(MdCGSecure 注入,
@@ -59,10 +59,21 @@ from .mdcg import expand_query_terms_weighted
59
59
  LOG_FILE = "_consistency.jsonl"
60
60
 
61
61
  MAX_SCAN = 200 # 单次检测最多比对的既有节点数(防 O(N) 爆炸)
62
+ # cons200(2026-10-03):本值是**相关面/保底面各自的限额**,不是
63
+ # 「索引序前 N」——数值一字未动(契约禁止面);超限走可观测
64
+ # (truncated/kept/hint),修法是选面与预筛,不是调大本值。
62
65
  MAX_DEPTH = 3 # L2 递归深度上限(对齐 :273)
63
66
  MAX_NODES = 60 # L2 递归展开节点上限(对齐 :273)
64
67
  MIN_GAIN = 0.15 # 信息增益门槛:候选空间减少比例低于此值即停(对齐 :273)
65
68
 
69
+ # 截断可操作提示(cons200:与 stg `_CAP_HINT` 同口径精神——禁止静默)——文案必须
70
+ # 含「细化生效条件/不适用条件」与「建立条件索引」语义;数值放大(调 limit/MAX_SCAN)
71
+ # 不是修法,故明文劝阻。
72
+ _SCAN_HINT = ("候选 %s 条超过单次检测限额 limit=%s,已按相关性保留 %s 条"
73
+ "(截断不静默):请细化生效条件/不适用条件收窄候选集,"
74
+ "或按设计稿建立条件索引(docs/plans/stg条件化与结构索引_设计_v0.1.md)"
75
+ "——不要调大 limit/MAX_SCAN 数值。")
76
+
66
77
  CLASH_HIGH = 0.6 # 条件互相覆盖阈值 → 明确互斥
67
78
  CLASH_LOW = 0.35 # 条件部分覆盖阈值 → 待定
68
79
  SELF_NEGATION = 0.5 # 自否定阈值:负条件被自身正文强命中
@@ -303,6 +314,53 @@ def _slot_text(content):
303
314
  _ccg_field(content or "", "子功能"))
304
315
 
305
316
 
317
+ # --------------------------------------------------------------------------
318
+ # 选面支持件(cons200):相关性预筛打分(快照级,不读盘)
319
+ # --------------------------------------------------------------------------
320
+
321
+ # 生效条件:e 为索引快照条目、nid 为节点 id、tw_pos/tw_neg 为 {词: 权重}(可为空)、cov 为 _weighted_coverage 同签名函数、disc 为 _is_discipline 的预判结果(None 时按 `_is_discipline({"tags": e.get("tags")}, nid)` 现算)——只读条目内既有键(tags/rejection_terms),恒返回整数二元组 (rank, score):disc 真 → (3, 0);否则 rejection_terms 与 tw_pos/tw_neg 有整词子串交集 → (2, int(round(1e4 * max(双方 _cov)) ));仅 rejection_terms 非空 → (1, 0);其余 → (0, 0);不读盘、不解析正文、不调用 cg.get。
322
+ def _sift_score(e, nid, tw_pos, tw_neg, cov, disc=None):
323
+ """候选相关性打分(**只排序,不裁决**)——选面的廉价预筛单点。
324
+
325
+ 为什么需要它(cons200 = 设计稿签收点 5):修前选面是 `list(nodes.items())`
326
+ 的索引序前 MAX_SCAN——库超限后 id 字典序靠后的记忆节点(`mem_*`)几乎从不
327
+ 进入比对面;而在役库前 200 名基本全是 `code_*` 索引节点。修后按**相关性**
328
+ (本函数)排序取前 limit,让「可能与本内容构成条件冲突」的候选优先入面。
329
+
330
+ 信号与廉价性(全部来自索引快照既有键,零读盘、零正文解析):
331
+ · `tags`(`_is_discipline` 单点判纪律)——纪律违反是硬冲突(REJECT),
332
+ 且其命中走 `_ban_hit` 子串判定、不走词权,故纪律候选 rank 最高(3);
333
+ · `rejection_terms`(**不适用条件**的索引词项,由既有单点
334
+ `mdcg.rejection_index_terms` 生成——不新造第二套解析):与本次内容的
335
+ 条件词面(tw_pos/tw_neg)做整词交集(`in` 子串,C 速度),命中即
336
+ rank 2 并以 `_weighted_coverage`(既有单点)给连续分;非空但无交集
337
+ rank 1(弱迹象:该节点确有负条件声明)。
338
+ 诚实边界(不假装全知):本键只覆盖**负条件面**(新正↔旧负 / 新负↔旧负)。
339
+ 「旧节点只声明生效条件(e_pos)」的比对面在快照里**无字段可判**(`_declared`
340
+ 还要读 content 的 `# 生效条件:` 行)——故本函数不做任何剔除,只做排序;
341
+ 信息不足的候选按 rank 0 落在相关面尾部、并在候选超限时由**保底面**
342
+ (修前扫描面 ∩ 候选,逐位保留)兜底,二者共同保证「修后检出 ⊇ 修前」。
343
+ """
344
+ if disc is None:
345
+ disc = _is_discipline({"tags": e.get("tags")}, nid)
346
+ if disc:
347
+ return 3, 0
348
+ rt = e.get("rejection_terms") or []
349
+ if not rt:
350
+ return 0, 0
351
+ blob = " ".join(str(t) for t in rt)
352
+ hit = False
353
+ if tw_pos:
354
+ hit = any(str(t) in blob for t in tw_pos)
355
+ if not hit and tw_neg:
356
+ hit = any(str(t) in blob for t in tw_neg)
357
+ if not hit:
358
+ return 1, 0
359
+ s1 = cov(tw_pos, blob) if tw_pos else 0.0
360
+ s2 = cov(tw_neg, blob) if tw_neg else 0.0
361
+ return 2, int(round(10000 * max(s1, s2)))
362
+
363
+
306
364
  # --------------------------------------------------------------------------
307
365
  # L0 情绪通道(信息差二阶变化)
308
366
  # --------------------------------------------------------------------------
@@ -426,7 +484,7 @@ def _recursive_reflect(cg, seeds, tw_pos, tw_neg, max_depth=MAX_DEPTH,
426
484
  # 主入口:三级决策
427
485
  # --------------------------------------------------------------------------
428
486
 
429
- # 生效条件:以 cg.index.nodes 为既有节点、content(假值按 "")经 _new_terms 得 pos/neg 并算 tw_pos/tw_neg(non_applicable_conditions 入参先按 mdcos._is_null_condition 剔除空值语义哨兵——⑧ 漏斗单点),按循环中 hard(自否定或纪律命中)→ divergences(同条件槽且 concl < CONCLUSION_SAME)→ strength ≥ CLASH_HIGH → strength ≥ CLASH_LOW → comparable==0 且 (pos or neg) → 否则 ACCEPT 的顺序定 verdict;DEFER 且 int(depth)>0 且 emo["bias"] != "approaching" 时调 _recursive_reflect 补 recursion,auto_flywheel 且 verdict == REJECT 时加 unresolved_id,最后 log 并返回 rec;
487
+ # 生效条件:以 cg.index.nodes 为既有节点、content(假值按 "")经 _new_terms 得 pos/neg 并算 tw_pos/tw_neg(non_applicable_conditions 入参先按 mdcos._is_null_condition 剔除空值语义哨兵——⑧ 漏斗单点);选面(cons200;收口轮①加面内例外):快照级预筛剔「tw_pos 与 tw_neg 皆空(新节点未声明可展开条件)时的非纪律候选」,且**剔除永不触及修前扫描面**(过滤后索引序前 limit 内候选一律保留——面内例外;「修后检出 ⊇ 修前」由此成为与「条目 tags 与盘面 fm.tags 同源」无关的结构保证),候选超 limit 时精比面=相关面(_sift_score 序)前 limit ∪ 保底面(过滤后索引序前 limit ∩ 候选),记 scanned(候选数)/kept(精比数)/truncated(scanned>kept)/hint;按循环中 hard(自否定或纪律命中)→ divergences(同条件槽且 concl < CONCLUSION_SAME)→ strength ≥ CLASH_HIGH → strength ≥ CLASH_LOW → comparable==0 且 (pos or neg) → 否则 ACCEPT 的顺序定 verdict;DEFER 且 int(depth)>0 且 emo["bias"] != "approaching" 时调 _recursive_reflect 补 recursion,auto_flywheel 且 verdict == REJECT 时加 unresolved_id,最后 log 并返回 rec;
430
488
  def check(cg, content, layer=None, condition_space=None,
431
489
  non_applicable_conditions=None, tags=None, exclude=None,
432
490
  limit=MAX_SCAN, depth=MAX_DEPTH, auto_flywheel=False,
@@ -436,6 +494,7 @@ def check(cg, content, layer=None, condition_space=None,
436
494
  返回完整判据(可审计):
437
495
  verdict / reason / conflict_strength / emotional / conflicts[] /
438
496
  recursion{} / missing[] / unresolved_id(若触发飞轮)
497
+ + 选面读数 scanned / kept / truncated(超限时另附 hint)
439
498
 
440
499
  verdict:
441
500
  ACCEPT 无冲突,或新节点未声明条件(无从冲突)
@@ -443,6 +502,36 @@ def check(cg, content, layer=None, condition_space=None,
443
502
  DEFER 条件互斥但可能可分辨(交给 L2 递归或飞轮)
444
503
  BLINDSPOT 有条件声明,但既有节点全无声明 → 无法建立比对路径(不假装确定)
445
504
 
505
+ 选面(cons200,2026-10-03;设计稿签收点 5;收口轮① 2026-10-03 复核 DEFER
506
+ 后补面内例外):
507
+ 修前=`list(nodes.items())` 索引序前 limit 并静默 break——在役库 17882
508
+ 节点 ⇒ 检测面 ≈1.1%,且 id 字典序最前的 `code_*` 索引节点占满名额,
509
+ 真正相关的记忆节点(`mem_*` 在字典序后段)几乎从不进入比对面。修后:
510
+ · 预筛(快照级、不读盘):`_sift_score` 只读条目既有键(tags /
511
+ rejection_terms),不做任何「信息不足」的剔除——唯一剔除档是
512
+ **可证无产出且不落修前扫描面**的「tw_pos 与 tw_neg 皆空(新节点
513
+ 未声明可展开条件)时的非纪律候选」(c1/c2/c3/c4 与分歧判定都以
514
+ tw_* 为乘子;纪律违反走 `_ban_hit` 子串、不看词权,故纪律候选
515
+ 一律保留)。**面内例外(收口轮①)**:剔除永不触及修前扫描面
516
+ (过滤后索引序前 limit 内的候选一律保留进候选面、落保底面通道)
517
+ ——「修后检出 ⊇ 修前」由此是**与「条目 tags 与盘面 fm.tags
518
+ 同源」无关的结构保证**(预筛读条目快照,而精比读盘面;非同源
519
+ 形态下面内候选仍进精比面、由盘面判据裁决)。
520
+ · 精比面=相关面(`_sift_score` 序)前 limit ∪ **保底面**(过滤后
521
+ 索引序前 limit ∩ 候选,逐位=修前扫描面)。保底面不撤是「修后检出
522
+ ⊇ 修前」的结构性保证(不是概率);两面上限各 limit(≤2×MAX_SCAN)。
523
+ · 可观测:`scanned`=预筛后候选数、`kept`=实际精比数、`truncated`=
524
+ scanned>kept(截断不静默,附 `hint` 给细化条件/建索引方向)。
525
+ 诚实边界:`rejection_terms` 只覆盖负条件面;「旧节点仅声明生效条件
526
+ (e_pos,正文 `# 生效条件:` 行)」在快照里无字段可判——此类节点在
527
+ 候选超限且不落修前扫描面/保底面时可能不被精比(由 truncated 可观测)。
528
+ 非同源形态(盘面 tags 与索引条目不一致,非合作写者直改 `.md` 不重建
529
+ 索引)分两段:**面内**(修前扫描面内)由面内例外保底、精比读盘面判定,
530
+ 修后仍检出(不构成回归);**面外残余**(条目未标纪律、盘面是纪律的
531
+ 节点落修前扫描面之外)修前亦检不出——不构成回归,属已声明边界
532
+ (预筛读索引条目快照;协作写者协议下条目与盘面同源,无此形态)。
533
+ 判据本身(阈值/覆盖计算/四态路由)一字未动——本批只改**选面与读数**。
534
+
446
535
  non_applicable_conditions(⑧ 2026-09-30):入参里的**空值语义哨兵**
447
536
  (`["无"]` / `["(无)"]` / `[""]`…)在本函数的入口统一剔除——判据复用
448
537
  `mdcos._is_null_condition`(仓内唯一哨兵表,**不新造第二张词表**)。
@@ -475,7 +564,7 @@ def check(cg, content, layer=None, condition_space=None,
475
564
  n_fn, n_sb = _slot_text(content)
476
565
 
477
566
  conflicts, hard, divergences = [], [], []
478
- strength, scanned, comparable = 0.0, 0, 0
567
+ strength, comparable = 0.0, 0
479
568
 
480
569
  # ---- L1-a 自否定:自己的负条件排除自己的生效条件/正文 ----
481
570
  body = _body_text(content)
@@ -491,16 +580,47 @@ def check(cg, content, layer=None, condition_space=None,
491
580
  strength = max(strength, round(_cov(tw_neg, body), 4))
492
581
 
493
582
  # ---- L1-b 与既有节点的条件级比对(反题) ----
583
+ # 选面(cons200):预筛(快照级,不读盘)→ 候选面 → 相关面 ∪ 保底面。
494
584
  nodes = ((getattr(cg, "index", None) or {}).get("nodes") or {})
495
- seeds = []
496
- for nid, e in list(nodes.items()):
585
+ order = list(nodes.items()) # 索引序快照(H-4(a):快照后再迭代)
586
+ lim = max(0, int(limit))
587
+ no_terms = not (tw_pos or tw_neg) # 新节点未声明可展开条件
588
+ cand = [] # (rank, score, in_before, nid, e)
589
+ fidx = 0
590
+ for nid, e in order:
497
591
  if exclude and nid == exclude:
498
592
  continue
499
593
  if layer and e.get("layer") != layer:
500
594
  continue
501
- if scanned >= int(limit):
502
- break
503
- scanned += 1
595
+ in_before = fidx < lim # 是否在修前扫描面内(过滤后前 limit)
596
+ fidx += 1
597
+ disc = _is_discipline({"tags": e.get("tags")}, nid)
598
+ # 预筛唯一剔除档(可证无产出;其余一律放行——宁多勿漏):
599
+ # 新节点未声明可展开条件时,非纪律候选在精比中产出恒为零
600
+ # (c1/c2/c3/c4 与分歧判定均以 tw_pos/tw_neg 为乘子;纪律违反走
601
+ # _ban_hit 子串判定、不看词权——故纪律候选一律保留)。
602
+ # 收口轮①(复核 DEFER 后补):**面内例外**——剔除永不触及修前扫描面
603
+ # (过滤后索引序前 limit 内的候选一律保留),使「修后检出 ⊇ 修前」
604
+ # 成为与「条目 tags 与盘面 fm.tags 同源」无关的结构保证(非同源形态
605
+ # 下面内候选仍进精比面、由盘面判据裁决);面外非同源残余见 check
606
+ # docstring 诚实边界(修前亦检不出,不构成回归)。
607
+ if no_terms and not disc and not in_before:
608
+ continue
609
+ rank, score = _sift_score(e, nid, tw_pos, tw_neg, _cov, disc)
610
+ cand.append((rank, score, in_before, nid, e))
611
+ # 相关面:rank/score 降序 + id 升序终键(确定性;与索引物理序无关)
612
+ rel = sorted(cand, key=lambda t: (-t[0], -t[1], str(t[3])))[:lim]
613
+ rel_ids = {t[3] for t in rel}
614
+ # 精比面=相关面 ∪ 保底面(过滤后索引序前 limit ∩ 候选)——保底面即
615
+ # 修前扫描面;其逐位保留是「修后检出 ⊇ 修前」的结构性保证(非概率),
616
+ # 两面上限各 limit(≤ 2×MAX_SCAN;MAX_SCAN 数值未动)。
617
+ scan_face = [(t[3], t[4]) for t in rel]
618
+ scan_face += [(t[3], t[4]) for t in cand if t[2] and t[3] not in rel_ids]
619
+ scanned = len(cand) # 预筛后候选数(截断前)
620
+ kept = len(scan_face) # 实际精比数(相关面 ∪ 保底面)
621
+ truncated = scanned > kept
622
+ seeds = []
623
+ for nid, e in scan_face:
504
624
  node = cg.get(nid) or {}
505
625
  fm = node.get("frontmatter") or {}
506
626
  body = node.get("content") or ""
@@ -630,9 +750,14 @@ def check(cg, content, layer=None, condition_space=None,
630
750
  rec = {"t": time.time(), "layer": layer, "verdict": verdict,
631
751
  "reason": reason, "conflict_strength": round(strength, 4),
632
752
  "emotional": emo, "conflicts": allc, "recursion": recursion,
633
- "missing": missing, "scanned": scanned, "comparable": comparable,
753
+ "missing": missing, "scanned": scanned, "kept": kept,
754
+ "truncated": truncated, "comparable": comparable,
634
755
  "pos": pos[:6], "neg": neg[:6],
635
756
  "actor": getattr(cg, "actor", "unknown")}
757
+ # 截断不静默(与 stg `_with_scan_reads` 同口径精神):候选有被限额丢弃时
758
+ # 附可操作 hint(细化条件 / 建索引方向;数值放大不是修法)。
759
+ if truncated:
760
+ rec["hint"] = _SCAN_HINT % (scanned, lim, kept)
636
761
 
637
762
  # ---- 冲突自动触发飞轮(误差 → 补条件 → 结构更新) ----
638
763
  # H11⑥(2026-09-30):**只对真冲突(REJECT)建单**。改前是
@@ -33,7 +33,7 @@
33
33
  噪音,不该去强化既有记忆(否则例行日志
34
34
  会把普通记忆刷成高重要性)
35
35
  3) 冗余 ≥0.85 → MERGE ← 外部/未知来源的重复 = 又一次确认,强化
36
- 4) 半重复 且 不重要 → DEFER
36
+ 4) 半重复 且 未触发保护 → DEFER ← 与重要度无关:半重复是「变更确认」的输入端
37
37
  5) 重要度 ≥0.30 → ACCEPT
38
38
  6) 新信息 ≥0.15 → ACCEPT
39
39
  7) 其余 → DEFER
@@ -236,7 +236,7 @@ def importance_score(hint, novelty, kind, content):
236
236
  return {"score": round(max(0.0, min(1.0, s)), 4), "from": "heuristic"}
237
237
 
238
238
 
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、不改落盘行为。
239
+ # 生效条件:以 source_kind(role,verification_basis) 的 kind 与 redundancy(cg,content,layer=layer,exclude=node_id) 的 red["max"] 为输入,按 if/elif 顺序取首个命中分支——imp["score"]≥PROTECT_IMPORTANCE→"ACCEPT"(reason 按 imp["from"] 分叉:hint→「触发不可遗忘保护」,heuristic→「未落保护——保护须显式声明」,issue50-d);否则 kind=="internal_deterministic" 且 red["max"]≥DUP_DROP→"DROP";否则 red["max"]≥DUP_MERGE→"MERGE";否则 red["max"]≥DUP_DROP→"DEFER"(**与 imp 无关**);否则 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、不改落盘行为。
240
240
  def assess(cg, content, layer="contextual", role=None, verification_basis=None,
241
241
  importance_hint=None, node_id=None):
242
242
  """三问 → 四态裁决。返回完整判据(可审计,不只给结论)。
@@ -261,6 +261,27 @@ def assess(cg, content, layer="contextual", role=None, verification_basis=None,
261
261
  关键正文并入他节点、检索归属漂移——风险高于收益;③ 缺陷本体是「绕过
262
262
  去重而不可见」而非「保护存在」,标注即可消除静默且零回归面
263
263
  (test_p9_forget_protect.py 的「importance_hint≥0.7→ACCEPT」逐字不变)。
264
+
265
+ issue50-d(2026-10-02):保护分支的 reason 文案按 **imp["from"] 分叉**——
266
+ 改前启发式路径(不传 hint)过线时也声称「触发不可遗忘保护」,但落盘面
267
+ importance=0.5(默认)且无保护位(隔离沙箱直读 frontmatter 实证:
268
+ u_heur_new 裁决 0.837/落盘 0.5;t_heur 裁决 0.711/落盘 0.5)——审计面在
269
+ 声称未发生的事。处置:from=="hint" 且 ≥0.7 维持原文案(add 的自动保护位
270
+ 对显式声明真落 protected 位,文案与落盘一致);from=="heuristic" 且 ≥0.7
271
+ 改为如实文案「启发式 …(未落保护——保护须显式声明)」。裁决面 verdict、
272
+ 分支顺序、常量均不变;落盘口径的修复在 remember_gated ACCEPT 分支
273
+ (mdcos.py:落盘=裁决值 + importance_source 标注来源)。
274
+
275
+ issue50-a(2026-10-01):分支 ④「半重复 → DEFER」此前写作
276
+ `red["max"] >= DUP_DROP and imp["score"] < IMPORTANCE_MIN`,**结构性不可达**:
277
+ 用户角色属外部惊奇来源(`SOURCE_WEIGHT["external_surprising"]=1.00`),启发式
278
+ 重要度 = 0.5·novelty + 0.3·1.00 + 0.2·lf ⇒ 基线恰为 0.30 = IMPORTANCE_MIN,
279
+ 且比较用 `>=`;插件侧还显式传 `hint=0.6`(同样 ≥0.30)。实测:重复度 0.7324 /
280
+ 0.7606 的半重复内容一律走分支 ⑤ 判 ACCEPT,`_forgetting.jsonl` 的 DEFER 计数为
281
+ 0(在役库 215 条留痕:ACCEPT 201 / MERGE 14)——本应是「变更确认」输入端的
282
+ 那条回路从不产生输入。处置:**删去 `and imp["score"] < IMPORTANCE_MIN`**,
283
+ 半重复(且未触发保护)一律 DEFER,与重要度解耦。分支顺序与其余六条分支的
284
+ 判据/文案均不变,保护优先(①)仍最先,故保护面零回归。
264
285
  """
265
286
  kind = source_kind(role, verification_basis)
266
287
  red = redundancy(cg, content, layer=layer, exclude=node_id)
@@ -294,8 +315,19 @@ def assess(cg, content, layer="contextual", role=None, verification_basis=None,
294
315
  if red["with"] and red["max"] >= DUP_MERGE:
295
316
  due.append("与 %s 重复度 %.2f≥%.2f(够 MERGE 阈值)"
296
317
  % (red["with"], red["max"], DUP_MERGE))
297
- verdict, why = "ACCEPT", (f"重要度 {imp['score']:.2f}≥{PROTECT_IMPORTANCE}"
298
- f"(触发不可遗忘保护)")
318
+ # issue50-d(2026-10-02):文案按来源分叉——`imp["from"]` 就在手上的
319
+ # imp 里,单点可判。显式 hint 维持「触发不可遗忘保护」(add 的自动
320
+ # 保护位会真落 protected 位,文案与落盘一致);启发式过线**只落分、
321
+ # 不打保护位**(issue50-d 裁决②:保护须显式声明),文案改成如实
322
+ # 表述——改前启发式 0.84 也声称「触发不可遗忘保护」,而落盘面
323
+ # importance=0.5 且无保护位(隔离沙箱实证),审计面在声称未发生的事。
324
+ # 分支顺序与 verdict 不变(硬边界:issue50-a 的那行除外,本批不动)。
325
+ if imp["from"] == "hint":
326
+ verdict, why = "ACCEPT", (f"重要度 {imp['score']:.2f}≥{PROTECT_IMPORTANCE}"
327
+ f"(触发不可遗忘保护)")
328
+ else:
329
+ verdict, why = "ACCEPT", (f"启发式 {imp['score']:.2f}≥{PROTECT_IMPORTANCE}"
330
+ f"(未落保护——保护须显式声明)")
299
331
  dedup_skipped = {
300
332
  "reason": "protect_importance",
301
333
  "detail": ("保护优先分支排在冗余判定之前:本次未走 MERGE/DROP"
@@ -315,9 +347,9 @@ def assess(cg, content, layer="contextual", role=None, verification_basis=None,
315
347
  elif red["max"] >= DUP_MERGE:
316
348
  verdict, why = "MERGE", (f"重复度 {red['max']:.2f}≥{DUP_MERGE}"
317
349
  f"(并入 {red['with']},强化既有)")
318
- elif red["max"] >= DUP_DROP and imp["score"] < IMPORTANCE_MIN:
350
+ elif red["max"] >= DUP_DROP:
319
351
  verdict, why = "DEFER", (f"半重复 {red['max']:.2f}∈[{DUP_DROP},{DUP_MERGE})"
320
- f" 且重要度 {imp['score']:.2f}<{IMPORTANCE_MIN}"
352
+ f" 且未触发不可遗忘保护"
321
353
  f"(待定复核)")
322
354
  elif imp["score"] >= IMPORTANCE_MIN:
323
355
  verdict, why = "ACCEPT", f"重要度 {imp['score']:.2f}≥{IMPORTANCE_MIN}"