@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
@@ -0,0 +1,408 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · issue50-a 守卫:半重复 → DEFER(与重要度解耦)
3
+
4
+ 缺陷(编排侧改前实测,非推断)
5
+ ------------------------------
6
+ 主动遗忘闸门 `forgetting.assess` 的「半重复且不重要 → DEFER」分支(改动前
7
+ :318-321)条件为 `red["max"] >= DUP_DROP and imp["score"] < IMPORTANCE_MIN`
8
+ ——**结构性不可达**:
9
+
10
+ · 用户角色属外部惊奇来源(`SOURCE_WEIGHT["external_surprising"]=1.00`),
11
+ 启发式重要度 = 0.5·novelty + 0.3·1.00 + 0.2·lf ⇒ novelty→1 时基线恰为
12
+ **0.30 = IMPORTANCE_MIN**,而比较用的是 `>=`(差一步就够不着);
13
+ · 插件缺省还显式传 `hint=0.6`(≥0.30)。
14
+
15
+ 实测:重复度 0.7324 / 0.7606 的半重复内容一律走分支⑤判 ACCEPT;在役库 215 条
16
+ 留痕 ACCEPT 201 / MERGE 14,**DROP 与 DEFER 皆 0**——本应是「变更确认」输入端的
17
+ 那条回路从不产生输入。
18
+
19
+ 本批修复(契约冻结,只此一条)
20
+ ------------------------------
21
+ 分支④改为**与重要度无关**:`red["max"] >= DUP_DROP`(且未触发保护)→ DEFER。
22
+ 分支序不变:①保护 ACCEPT → ②内部确定性 DROP → ③冗余≥0.85 MERGE → ④半重复
23
+ DEFER → ⑤重要度 ACCEPT → ⑥新信息 ACCEPT → ⑦其余 DEFER。常量、其余分支的
24
+ 判据与文案、writelimit、`mdcos.remember_gated` 的落库动作均不动。
25
+
26
+ 断言分组(每组都在**隔离临时根**的合成库上真跑,绝不触在役数据根)
27
+ F 夹具自检:构造出的重复度确实落在各区(防「测试面打滑」——测的不是想测的)
28
+ A 修复面:dup∈[0.60,0.85) 且 role=user 非保护 → DEFER(hint=0.60 与启发式
29
+ hint=None 两路都必须 DEFER——后者正是改前误判 ACCEPT 的那一路);
30
+ hint=0.9 的半重复 → ACCEPT(保护优先不受影响);reason 文案如实描述新判据
31
+ B 窗口边界(④ 不许外溢):dup≥0.85 仍 MERGE(③ 先);dup<0.60 仍 ACCEPT
32
+ C 先后次序:role=tool-output 且 dup∈[0.60,0.85) → DROP(② 先于 ④)
33
+ E 不动面:①③⑤⑥ 的判据/文案与五个判据常量原样
34
+
35
+ 定点变异自证(--mutate)
36
+ ------------------------
37
+ 每一处判据都配一个定点变异把它打红,且**恰好**命中预期红项数
38
+ (`_SRC_MUTATIONS` / `_CONST_MUTATIONS` 的 expected 列;实测计数不符即判 FAIL
39
+ ——照 test_neg_condition_hits.py 的 --branch-baseline 同口径)。锚点漂移(实现
40
+ 改了却没同步本表)→ 报 ANCHOR-MISS 并 **exit 2**(fail-closed);默认模式同样
41
+ 先做锚点自检。**不以 git HEAD 为基线源**——基线绑提交即失效(本仓已有两次教训),
42
+ 变异一律作用在**当前盘的实现**上。
43
+
44
+ 运行:python -X utf8 -m md_cg.test_i50a_half_dup_defer
45
+ python -X utf8 -m md_cg.test_i50a_half_dup_defer --mutate
46
+ """
47
+ from __future__ import annotations
48
+
49
+ import contextlib
50
+ import inspect
51
+ import io
52
+ import os
53
+ import shutil
54
+ import sys
55
+ import tempfile
56
+
57
+ from . import forgetting
58
+ from .mdcos import MdCGOS
59
+
60
+ _PASS = []
61
+ _FAIL = []
62
+
63
+
64
+ def ok(cond, msg, extra=""):
65
+ (_PASS if cond else _FAIL).append(msg)
66
+ print((" PASS " if cond else " FAIL ") + msg
67
+ + ((" ← " + str(extra)) if (extra and not cond) else ""))
68
+
69
+
70
+ def _line(r):
71
+ """一行读数:verdict · reason · dup · importance(报告面逐行可核对)。"""
72
+ return ("%s · %s · dup=%.4f · imp=%s/%s"
73
+ % (r["verdict"], r["reason"], r["redundancy"]["max"],
74
+ r["importance"]["score"], r["importance"]["from"]))
75
+
76
+
77
+ # ---------------------------------------------------------------- 夹具构造
78
+ # 600 个互异汉字作基线正文、另 600 个互异汉字作全新填充——两者的 bigram 集合
79
+ # 几乎不交,于是「前 k 字的覆盖比例」可以线性地调出目标重复度(实测 k=360 →
80
+ # dup 0.6074、k=436 → 0.7302、k=455 → 0.7609、k=503 → 0.8384、k=579 → 0.9612)。
81
+ BASE = "".join(chr(0x4e00 + i) for i in range(600))
82
+ FILL = "".join(chr(0x8000 + i) for i in range(600))
83
+ TITLE_BASE, TITLE_NEW = "基线节点标题", "新写入标题"
84
+
85
+ K_LO = 355 # dup ≈ 0.5994(刚好落在 DUP_DROP 之下)
86
+ K_EDGE = 360 # dup ≈ 0.6074(刚好越过 DUP_DROP)
87
+ K_HALF1 = 436 # dup ≈ 0.7302
88
+ K_HALF2 = 455 # dup ≈ 0.7609
89
+ K_TOP = 503 # dup ≈ 0.8384(贴近 DUP_MERGE 但未到)
90
+ K_NEAR_MID = 541 # dup ≈ 0.8998
91
+ K_NEAR = 579 # dup ≈ 0.9612
92
+
93
+
94
+ # 生效条件:title 与 body 给定时按本仓 CCG 六要素模板拼出节点正文(模板行两侧
95
+ # 一致,故不成为重复度的差量来源);无输入校验,参数缺失即抛 TypeError。
96
+ def _doc(title, body):
97
+ return ("# 功能名:%s\n# 生效条件:任意情境\n# 子功能:验收\n"
98
+ "# 执行:直接调用\n# 验证方式:test\n# 不适用条件:无\n%s\n"
99
+ % (title, body))
100
+
101
+
102
+ # 生效条件:k 为整数下标;返回「前 k 字取自 BASE、其余取自 FILL」的新正文。
103
+ def _mixed(k):
104
+ return BASE[:k] + FILL[:len(BASE) - k]
105
+
106
+
107
+ # 生效条件:在**隔离临时根**上真建一个基线节点(层 contextual),再对 _mixed(k)
108
+ # 的正文调 forgetting.assess(走 forgetting.assess 属性查表,**不**本地绑定,
109
+ # 否则变异自证打不进去);same_title 为真时新正文复用基线标题(精确重复用)。
110
+ # 返回 assess 的完整判据字典;临时根在 finally 里删除,绝不触在役数据根。
111
+ def _case(k, role="user", hint=None, same_title=False):
112
+ root = tempfile.mkdtemp(prefix="i50a_")
113
+ try:
114
+ cg = MdCGOS(root)
115
+ cg.add("n1", _doc(TITLE_BASE, BASE), layer="contextual", importance=0.5)
116
+ title = TITLE_BASE if same_title else TITLE_NEW
117
+ return forgetting.assess(cg, _doc(title, _mixed(k)), layer="contextual",
118
+ role=role, verification_basis=None,
119
+ importance_hint=hint, node_id=None)
120
+ finally:
121
+ shutil.rmtree(root, ignore_errors=True)
122
+
123
+
124
+ # ---------------------------------------------------------------- F 夹具自检
125
+ def g_f():
126
+ print("== F 夹具自检(构造出的重复度确实落在预期区)==")
127
+ d = {}
128
+ for name, k, same in (("fresh", 0, False), ("lo", K_LO, False),
129
+ ("edge", K_EDGE, False), ("top", K_TOP, False),
130
+ ("near", K_NEAR, False), ("exact", 600, True)):
131
+ d[name] = _case(k, same_title=same)["redundancy"]["max"]
132
+ ok(d["fresh"] < 0.60, "F1 全新内容 dup<0.60(%.4f)" % d["fresh"])
133
+ ok(d["lo"] < 0.60, "F2 边界下侧 dup<0.60(%.4f,恰好卡在 DUP_DROP 之下)"
134
+ % d["lo"])
135
+ ok(0.60 <= d["edge"] < 0.85, "F3 边界上侧 dup∈[0.60,0.85)(%.4f)"
136
+ % d["edge"])
137
+ ok(0.60 <= d["top"] < 0.85, "F4 贴近 DUP_MERGE 仍 ∈[0.60,0.85)(%.4f)"
138
+ % d["top"])
139
+ ok(d["near"] >= 0.85, "F5 近重复 dup≥0.85(%.4f)" % d["near"])
140
+ ok(d["exact"] == 1.0, "F6 精确重复 dup==1.0(%.4f)" % d["exact"])
141
+
142
+
143
+ # ---------------------------------------------------------------- A 修复面
144
+ def g_a():
145
+ print("== A 修复面:半重复且未触发保护 → DEFER(与重要度无关)==")
146
+ r = _case(K_EDGE, hint=0.60)
147
+ ok(r["verdict"] == "DEFER",
148
+ "A1 边界上侧 dup=%.4f + hint=0.60 → DEFER" % r["redundancy"]["max"],
149
+ _line(r))
150
+ r_h1 = _case(K_HALF1, hint=0.60)
151
+ ok(r_h1["verdict"] == "DEFER",
152
+ "A2 dup=%.4f + hint=0.60 → DEFER(改前走⑤误判 ACCEPT)"
153
+ % r_h1["redundancy"]["max"], _line(r_h1))
154
+ r_h1n = _case(K_HALF1, hint=None)
155
+ ok(r_h1n["verdict"] == "DEFER",
156
+ "A3 dup=%.4f + hint=None(启发式 %.4f≥%.2f)→ 仍 DEFER"
157
+ % (r_h1n["redundancy"]["max"], r_h1n["importance"]["score"],
158
+ forgetting.IMPORTANCE_MIN), _line(r_h1n))
159
+ r_h2 = _case(K_HALF2, hint=0.60)
160
+ ok(r_h2["verdict"] == "DEFER",
161
+ "A4 dup=%.4f + hint=0.60 → DEFER" % r_h2["redundancy"]["max"], _line(r_h2))
162
+ r_top = _case(K_TOP, hint=0.60)
163
+ ok(r_top["verdict"] == "DEFER",
164
+ "A5 dup=%.4f(贴近 DUP_MERGE)+ hint=0.60 → DEFER"
165
+ % r_top["redundancy"]["max"], _line(r_top))
166
+ r_p = _case(K_HALF1, hint=0.9)
167
+ ok(r_p["verdict"] == "ACCEPT",
168
+ "A6 半重复 + hint=0.9 → ACCEPT(① 保护优先不受本批影响)", _line(r_p))
169
+ ok(r_p.get("dedup_skipped") is not None,
170
+ "A7 保护优先分支的去重读数仍在(dedup_skipped 非空)",
171
+ r_p.get("dedup_skipped"))
172
+ why = r_h1["reason"]
173
+ ok("半重复" in why and "未触发不可遗忘保护" in why and "重要度" not in why,
174
+ "A8 分支④ 文案如实描述新判据(半重复/未触发保护,不再称重要度不足)",
175
+ why)
176
+
177
+
178
+ # ---------------------------------------------------------------- B 窗口边界
179
+ def g_b():
180
+ print("== B 窗口边界:④ 不许外溢 ==")
181
+ r = _case(K_NEAR, hint=0.60)
182
+ ok(r["verdict"] == "MERGE" and "强化既有" in r["reason"],
183
+ "B1 dup=%.4f≥0.85 → MERGE(③ 仍先于 ④,分支③ 文案原样)"
184
+ % r["redundancy"]["max"], _line(r))
185
+ r = _case(K_NEAR_MID, hint=None)
186
+ ok(r["verdict"] == "MERGE",
187
+ "B2 dup=%.4f≥0.85 → MERGE" % r["redundancy"]["max"], _line(r))
188
+ r = _case(600, same_title=True)
189
+ ok(r["verdict"] == "MERGE",
190
+ "B3 精确重复(dup=1.0)→ MERGE", _line(r))
191
+ r = _case(K_LO, hint=0.60)
192
+ ok(r["verdict"] == "ACCEPT",
193
+ "B4 dup=%.4f<0.60 → ACCEPT(④ 不外溢到 DUP_DROP 之下)"
194
+ % r["redundancy"]["max"], _line(r))
195
+ r = _case(0, hint=None)
196
+ ok(r["verdict"] == "ACCEPT", "B5 全新内容(dup=%.4f)→ ACCEPT"
197
+ % r["redundancy"]["max"], _line(r))
198
+
199
+
200
+ # ---------------------------------------------------------------- C 先后次序
201
+ def g_c():
202
+ print("== C 先后次序:分支② 仍先于 ④ ==")
203
+ r = _case(K_EDGE, role="tool-output")
204
+ ok(r["verdict"] == "DROP",
205
+ "C1 tool-output + dup=%.4f∈[0.60,0.85) → DROP(② 先于 ④)"
206
+ % r["redundancy"]["max"], _line(r))
207
+ r = _case(K_TOP, role="tool-output")
208
+ ok(r["verdict"] == "DROP" and "低熵噪音" in r["reason"],
209
+ "C2 贴近 DUP_MERGE 仍 DROP,分支② 文案原样", _line(r))
210
+ r = _case(0, role="tool-output")
211
+ ok(r["verdict"] == "ACCEPT",
212
+ "C3 反向腿:tool-output 全新内容仍 ACCEPT(C 组不是恒判 DROP)", _line(r))
213
+
214
+
215
+ # ---------------------------------------------------------------- E 不动面
216
+ def g_e():
217
+ print("== E 不动面:①③⑤⑥ 的判据/文案与五个常量原样 ==")
218
+ r = _case(K_NEAR, hint=0.9)
219
+ ok(r["verdict"] == "ACCEPT" and "触发不可遗忘保护" in r["reason"],
220
+ "E1 ① 保护优先仍最先(dup=0.96 + hint=0.9 → ACCEPT,未被 ③ 抢走)",
221
+ _line(r))
222
+ r = _case(0, hint=0.35)
223
+ ok(r["verdict"] == "ACCEPT" and "重要度" in r["reason"],
224
+ "E2 ⑤ 重要度≥IMPORTANCE_MIN → ACCEPT,文案原样", _line(r))
225
+ r = _case(0, role="command", hint=0.1)
226
+ ok(r["verdict"] == "ACCEPT" and "新信息" in r["reason"],
227
+ "E3 ⑥ 新信息≥NOVELTY_MIN → ACCEPT,文案原样", _line(r))
228
+ ok(forgetting.DUP_MERGE == 0.85 and forgetting.DUP_DROP == 0.60
229
+ and forgetting.NOVELTY_MIN == 0.15 and forgetting.IMPORTANCE_MIN == 0.30
230
+ and forgetting.PROTECT_IMPORTANCE == 0.70,
231
+ "E4 五个判据常量原样(本批不改任何常量值)",
232
+ (forgetting.DUP_MERGE, forgetting.DUP_DROP, forgetting.NOVELTY_MIN,
233
+ forgetting.IMPORTANCE_MIN, forgetting.PROTECT_IMPORTANCE))
234
+
235
+
236
+ _GROUPS = (g_f, g_a, g_b, g_c, g_e)
237
+
238
+
239
+ def _run_groups():
240
+ """跑全部断言组(静默),返回失败数——供变异自证复用。"""
241
+ _PASS.clear()
242
+ _FAIL.clear()
243
+ for g in _GROUPS:
244
+ g()
245
+ return len(_FAIL)
246
+
247
+
248
+ # ---------------------------------------------------------------- 定点变异自证
249
+ # 表内每项 = (说明, 锚点原文, 替换文, 预期红项数)。锚点必须**逐字**出现在
250
+ # 当前实现的 assess 源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。
251
+ _SRC_MUTATIONS = (
252
+ # 契约指定的那处:把分支④ 的条件改回「且重要度不足」——半重复重新被重要度
253
+ # 拦住 → A1..A5 全部落回分支⑤ ACCEPT,A8 文案也随之变成「重要度 …」
254
+ ("④ 分支加回重要度合取(改回缺陷形态)",
255
+ 'elif red["max"] >= DUP_DROP:',
256
+ 'elif red["max"] >= DUP_DROP and imp["score"] < IMPORTANCE_MIN:',
257
+ 6),
258
+ # 整条删掉分支④——半重复无出口,落回分支⑤
259
+ ("④ 分支整条删除", 'elif red["max"] >= DUP_DROP:', 'elif False:', 6),
260
+ # 整条删掉分支③——dup≥0.85 不再 MERGE 而是落到 ④:B 组「≥0.85→MERGE」
261
+ # 那端的判别力由本变异钉死
262
+ ("③ 分支整条删除(≥0.85 不再 MERGE)",
263
+ 'elif red["max"] >= DUP_MERGE:', 'elif False:', 3),
264
+ # ① 保护分支失能(阈值抬到不可能达到)——保护面与半重复面同时暴露
265
+ ("① 保护分支失能(PROTECT_IMPORTANCE 抬到 1.1)",
266
+ 'imp["score"] >= PROTECT_IMPORTANCE', 'imp["score"] >= 1.1', 3),
267
+ )
268
+ # 常量变异:直接改模块属性(assess 在调用时按模块 globals 取名,故立刻生效)
269
+ # 表内 = (说明, 常量名, 变异值, 预期红项数)。**每条常量变异的红项数都含 E4
270
+ # +1**——E4 断言的正是「五个常量原样」,常量一动它就必红,这是设计使然。
271
+ _CONST_MUTATIONS = (
272
+ # 契约指定的那处:DUP_DROP 抬到 0.9。**实测与契约的预期不同**(见回报):
273
+ # 分支③(0.85) 在 ④ 之前,抬高 DUP_DROP 只会让 ④ 的窗口变成空集 ⇒ 半重复
274
+ # 落回 ⑤(A 组红),而 B 组的「≥0.85→MERGE」与「<0.60→ACCEPT」两端都不受影响。
275
+ ("DUP_DROP 抬到 0.9(契约指定;④ 窗口变空)", "DUP_DROP", 0.9, 9),
276
+ # 反方向:DUP_DROP 降到 0.30 —— ④ 的入口边界下移,dup<0.60 的「不许外溢」
277
+ # 断言(B4)必红。这才是 B 组在该常量上的判别力所在。
278
+ ("DUP_DROP 降到 0.30(④ 的入口边界下移)", "DUP_DROP", 0.30, 2),
279
+ ("DUP_MERGE 降到 0.60(③ 抢在 ④ 之前)", "DUP_MERGE", 0.60, 7),
280
+ ("IMPORTANCE_MIN 抬到 1.1(⑤ 失能)", "IMPORTANCE_MIN", 1.1, 2),
281
+ ("NOVELTY_MIN 抬到 1.1(⑥ 失能)", "NOVELTY_MIN", 1.1, 2),
282
+ )
283
+
284
+ # 静态锚点(默认模式也自检,fail-closed):
285
+ # · 缺陷形态的合取**不得**出现在 assess 源码里(回归即 ANCHOR-MISS)
286
+ # · 新文案的两个词必须在位
287
+ _ANCHORS_BANNED = ("and imp[\"score\"] < IMPORTANCE_MIN",)
288
+ _ANCHORS_REQUIRED = ("半重复", "未触发不可遗忘保护", "待定复核")
289
+
290
+
291
+ def _assess_src():
292
+ return inspect.getsource(forgetting.assess)
293
+
294
+
295
+ def _code_face():
296
+ """assess 的**可执行面**(剥掉函数 docstring 与整行注释)。
297
+
298
+ 与 test_neg_condition_hits.py 的 G6a 同口径:说明性文字里**引述**被删掉的
299
+ 旧实现是允许的(正是要记下它为何被换掉),判别力只压在可执行行上。
300
+ """
301
+ import ast
302
+ src = _assess_src()
303
+ lines = src.splitlines()
304
+ fn = ast.parse(src).body[0]
305
+ skip = set()
306
+ if (fn.body and isinstance(fn.body[0], ast.Expr)
307
+ and isinstance(fn.body[0].value, ast.Constant)
308
+ and isinstance(fn.body[0].value.value, str)):
309
+ doc = fn.body[0]
310
+ for i in range(doc.lineno, (doc.end_lineno or doc.lineno) + 1):
311
+ skip.add(i)
312
+ return [(i, l) for i, l in enumerate(lines, 1)
313
+ if i not in skip and not l.strip().startswith("#")]
314
+
315
+
316
+ def _anchor_check():
317
+ """返回 ANCHOR-MISS 说明列表(空 = 全部在位)。"""
318
+ src = _assess_src()
319
+ code = "\n".join(l for _i, l in _code_face())
320
+ bad = []
321
+ for name, old, _new, _n in _SRC_MUTATIONS:
322
+ if old not in src:
323
+ bad.append("变异锚点缺失:%s(锚点 %r)" % (name, old))
324
+ for s in _ANCHORS_BANNED:
325
+ if s in code:
326
+ bad.append("缺陷形态回归:assess 可执行行里仍有 %r" % s)
327
+ for s in _ANCHORS_REQUIRED:
328
+ if s not in code:
329
+ bad.append("新文案锚点缺失:assess 可执行行里找不到 %r" % s)
330
+ return bad
331
+
332
+
333
+ # 生效条件:old 与 new 给定时,取当前 forgetting.assess 的源码做字面替换并以
334
+ # forgetting 的模块 globals 副本 exec 出新函数;返回该函数对象(不落盘、不改源文件)。
335
+ def _mutate_src(old, new):
336
+ ns = dict(vars(forgetting))
337
+ exec(compile(_assess_src().replace(old, new), "i50a_mut.py", "exec"), ns)
338
+ return ns["assess"]
339
+
340
+
341
+ def _with_patched(fn, run):
342
+ """把 forgetting.assess 临时换成 fn 跑一遍 run(),finally 原样还原。"""
343
+ live = forgetting.assess
344
+ forgetting.assess = fn
345
+ try:
346
+ with contextlib.redirect_stdout(io.StringIO()):
347
+ return run()
348
+ finally:
349
+ forgetting.assess = live
350
+
351
+
352
+ def _mutate_mode():
353
+ bad = []
354
+ anchor_bad = _anchor_check()
355
+ if anchor_bad:
356
+ for b in anchor_bad:
357
+ print(" ANCHOR-MISS " + b)
358
+ print("\n锚点自检:FAIL(fail-closed,exit 2)")
359
+ return 2
360
+ with contextlib.redirect_stdout(io.StringIO()):
361
+ clean = _run_groups()
362
+ print(" 未变异基线:红项=%d%s" % (clean, "" if clean == 0 else " ← 基线即红,变异核验无意义"))
363
+ if clean:
364
+ bad.append("未变异基线即失败")
365
+ for name, old, new, expect in _SRC_MUTATIONS:
366
+ fails = _with_patched(_mutate_src(old, new), _run_groups)
367
+ verdict = "命中预期" if fails == expect else "**红项数不符(预期 %d)**" % expect
368
+ print(" 变异「%s」→ 红项=%d %s" % (name, fails, verdict))
369
+ if fails != expect:
370
+ bad.append(name)
371
+ for name, const, val, expect in _CONST_MUTATIONS:
372
+ live = getattr(forgetting, const)
373
+ try:
374
+ setattr(forgetting, const, val)
375
+ fails = _with_patched(forgetting.assess, _run_groups)
376
+ finally:
377
+ setattr(forgetting, const, live)
378
+ verdict = "命中预期" if fails == expect else "**红项数不符(预期 %d)**" % expect
379
+ print(" 变异「%s」→ 红项=%d %s" % (name, fails, verdict))
380
+ if fails != expect:
381
+ bad.append(name)
382
+ print("\n定点变异自证:%s" % ("PASS(每处判据都有变异钉死,且红项数逐处吻合)"
383
+ if not bad else "FAIL —— " + "、".join(bad)))
384
+ return 0 if not bad else 1
385
+
386
+
387
+ def main() -> int:
388
+ src = os.path.basename(os.path.abspath(__file__))
389
+ if "--mutate" in sys.argv:
390
+ print("!! 定点变异模式:逐个变异判据,套件应转红且红项数吻合\n")
391
+ return _mutate_mode()
392
+ anchor_bad = _anchor_check()
393
+ if anchor_bad:
394
+ for b in anchor_bad:
395
+ print(" ANCHOR-MISS " + b)
396
+ print("\n锚点自检:FAIL(fail-closed,exit 2)——实现改了请同步变异表/文案")
397
+ return 2
398
+ print("锚点自检:PASS(%s;不以 git HEAD 为基线源)" % src)
399
+ for g in _GROUPS:
400
+ g()
401
+ print("\nissue50-a 守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
402
+ if _FAIL:
403
+ print("失败项:" + "、".join(_FAIL))
404
+ return 0 if not _FAIL else 1
405
+
406
+
407
+ if __name__ == "__main__":
408
+ sys.exit(main())