@furongjun1999/dsh-memory 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -18
- package/docs/README.md +1 -0
- 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
- 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
- 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
- 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
- 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
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
- 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
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- 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
- 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
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/freshness.py +511 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/mcp_server.py +128 -19
- package/md_cg/mdcg.py +509 -22
- package/md_cg/mdcos.py +203 -22
- package/md_cg/nodefile.py +74 -1
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""统一归一层「缺省关」守卫(2026-09-30 使用者裁定:MDCG_UNIFY_QUERY 缺省由开翻关)
|
|
3
|
+
|
|
4
|
+
口径(契约 = 使用者裁定,判据落点两侧各一):
|
|
5
|
+
|
|
6
|
+
MDCG_UNIFY_QUERY Python `semantic/unify.py::unify_on()` Rust `rust/src/atoms.rs::Atoms::from_env()`
|
|
7
|
+
未设 False(缺省 "0") None(不载入)
|
|
8
|
+
"0" False None(不载入)
|
|
9
|
+
"1" True Some(Atoms)(载入)
|
|
10
|
+
|
|
11
|
+
即**唯一开态 = 显式 "1"**;未设与 "0" 同语义(两侧同判)。定因:本层本职是让
|
|
12
|
+
**英文** query 命中中文节点,对中文检索池是**纯开销**——locomo-zh-500 公开题池
|
|
13
|
+
(池 567 / 题 500)缺省关态 lexical hit@1 96.4% / lexical+fuzzy 97.2%,开态
|
|
14
|
+
95.8% / 96.6%(各跑两遍逐位相同;本件不跑该池,该读数由
|
|
15
|
+
`python -X utf8 -m md_cg.bench_locomo_zh_public` 产出,见 §五 use 面)。
|
|
16
|
+
|
|
17
|
+
覆盖与判据(每条对应契约的一项):
|
|
18
|
+
|
|
19
|
+
G0 隔离与基线自证——辅助/库根全落守卫临时目录;口径类 env 干净(本件在**缺省
|
|
20
|
+
态**上验证);**基线源 = 当前工作区源码**(本件不含任何提交快照读取面,
|
|
21
|
+
结构上自证:源码里无 git 调用字面量)——本仓已有两次「基线绑提交即失效」教训。
|
|
22
|
+
|
|
23
|
+
G1 三态(契约①)——Python 侧三态断言:`unify_on()` 三态 + `unify_query` 三态
|
|
24
|
+
返回值(含英文 query 未设/`0`/`1` 各一读数、中文与中夹英 query 三态恒等),
|
|
25
|
+
外加「未设产物 == `0` 产物」逐位相同(缺省真的等同于关)。
|
|
26
|
+
|
|
27
|
+
G2 检索读数(契约②)——**同一池**上跑未设/`0`/`1` 三态:未设读数与 `0` 读数
|
|
28
|
+
逐位相同;并断言 `1` 态读数与之**不同**(否则池对开关不敏感、该判据空转),
|
|
29
|
+
且 `1` 态 top-1 = 中文节点、未设态 top-1 ≠ 该节点(方向断言,非仅相等)。
|
|
30
|
+
|
|
31
|
+
G3 两侧一致(契约③)——用**真 rust release 二进制**(`--serve` 协议)在同一池上
|
|
32
|
+
跑同一三态:Rust 未设读数 == Rust `0` 读数、`1` 态与之不同且 top-1 = 中文节点;
|
|
33
|
+
末了做**跨侧同判**断言(Python 判据与 Rust 判据对每一态的判定一致)。
|
|
34
|
+
|
|
35
|
+
G4 结构面(防回退)——两侧判据落点的字面锚点(Python 缺省 "0" + `== "1"`;
|
|
36
|
+
Rust `!= Ok("1")`)、三处 search 入口接线仍在、`MDCG_UNIFY_QUERY` 仍在
|
|
37
|
+
`hotcache._ENV_SWITCHES`(开关不得删)、Python 侧无「缺省 "1"」残留。
|
|
38
|
+
|
|
39
|
+
M 定点变异自证(契约④)——把修复点逐一改回缺陷形态,套件必须按**预期条数**
|
|
40
|
+
恰好转红(多红=判据越界、少红=判据空转,两种都判失败):
|
|
41
|
+
M1 Python 缺省 "0"→"1"(改回缺省开)→ 预期红 **13** 项;
|
|
42
|
+
M2 Rust 三态改回 `map(|v| v == "0").unwrap_or(false)`(未设=开)——**源码定点
|
|
43
|
+
变异 + 临时 CARGO_TARGET_DIR 重建真二进制**(零第三方依赖,冷构建本机实测
|
|
44
|
+
~7s),再以该二进制跑 G3 → 预期红 **8** 项。
|
|
45
|
+
锚点在当前源码里逐字找不到 → ANCHOR-MISS → **退出码 2(fail-closed)**。
|
|
46
|
+
变异锚点一律取**代码行**(含行首缩进)而非裸表达式:裸串在注释里可能同形,
|
|
47
|
+
只替换注释那处会让代码不动、断言仍绿(2026-09-30 实测踩过此坑)。
|
|
48
|
+
文书/接线类锚点(G4c/G4f/G4g/G4h/G4i)**不被这两条变异弄红**——它们钉的是注释
|
|
49
|
+
文本与接线,效力由「实现漂移即红」保证,不由变异证明(如实标注,不假装覆盖)。
|
|
50
|
+
|
|
51
|
+
运行:
|
|
52
|
+
python -X utf8 -m md_cg.test_unify_default_off # 正向
|
|
53
|
+
python -X utf8 -m md_cg.test_unify_default_off --mutate # 定点变异自证
|
|
54
|
+
python -X utf8 -m md_cg.test_unify_default_off --mutate --list
|
|
55
|
+
|
|
56
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红 / 前置缺失;
|
|
57
|
+
2 = ANCHOR-MISS(变异锚点漂移——实现改了却没同步本表,本表就是判据)。
|
|
58
|
+
|
|
59
|
+
依赖:`rust/target/release/mdcg-eval`(两侧一致性要用真二进制)与 `cargo`(M2 重建)。
|
|
60
|
+
二进制缺失时 `main()` **前置判红**并给出可执行重编命令(同
|
|
61
|
+
`md_cg/test_rank_parity_score_mode.py` 的口径:跑不起来 ≠ 通过,不采用静默 SKIP)。
|
|
62
|
+
G3 依赖缺失即逐条 `SKIP(未跑)` **不计入通过数**,并补一条 fail-closed 红项。
|
|
63
|
+
|
|
64
|
+
硬边界:不动任何在役数据根(辅助根/库根一律 `tempfile.mkdtemp`);不 git
|
|
65
|
+
add/commit/push;不改 `data/policy.json`;不改归一层算法本体与作用域收窄逻辑;
|
|
66
|
+
不改 `rust/src/atoms.rs` 的 CJK 判据、不动 `md_cg/semantic/unify_fixture.json`
|
|
67
|
+
(被 `atoms.rs` 以 `include_str!` 编译期嵌入,改它会破编译)。
|
|
68
|
+
"""
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
import contextlib
|
|
72
|
+
import importlib.util
|
|
73
|
+
import io
|
|
74
|
+
import json
|
|
75
|
+
import os
|
|
76
|
+
import re
|
|
77
|
+
import shutil
|
|
78
|
+
import subprocess
|
|
79
|
+
import sys
|
|
80
|
+
import tempfile
|
|
81
|
+
import time
|
|
82
|
+
|
|
83
|
+
# ── 隔离前置:必须在 import md_cg 子模块**之前**(沿用
|
|
84
|
+
# md_cg/test_rank_parity_score_mode.py 同款前置:部分模块在导入期把 aux_root()
|
|
85
|
+
# 冻成常量)——否则会指到真实 ~/.mdcg 并在那里建密钥/令牌库。 ──
|
|
86
|
+
_TMP = tempfile.mkdtemp(prefix="unify_default_off_")
|
|
87
|
+
os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "auxroot")
|
|
88
|
+
os.environ["MDCG_MASTER_KEY"] = "cd" * 32 # 哑主密钥(绝不触真实密钥面)
|
|
89
|
+
os.environ["MDCG_TOKEN_FILE"] = os.path.join(_TMP, "tokens.json")
|
|
90
|
+
os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cgroot")
|
|
91
|
+
for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
|
|
92
|
+
"DSH_SESSION_ID", "MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
|
|
93
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_STATE_ROOT",
|
|
94
|
+
"MDCG_DATA_ROOT", "MDCG_TENANT_REGISTRY", "MDCG_VERIFIER_MODULES",
|
|
95
|
+
# 口径面:本件在**缺省态**上验证,故这三个开关必须干净
|
|
96
|
+
"MDCG_SCORE_MODE", "MDCG_UNIFY_QUERY", "MDCG_EN_ATOMS", "MDCG_SEMANTIC"):
|
|
97
|
+
os.environ.pop(_k, None)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
101
|
+
except AttributeError: # pragma: no cover
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
105
|
+
_REPO = os.path.dirname(_HERE)
|
|
106
|
+
_SELF = os.path.abspath(__file__)
|
|
107
|
+
if _REPO not in sys.path:
|
|
108
|
+
sys.path.insert(0, _REPO)
|
|
109
|
+
|
|
110
|
+
_UNIFY_PY = os.path.join(_HERE, "semantic", "unify.py")
|
|
111
|
+
_ATOMS_RS = os.path.join(_REPO, "rust", "src", "atoms.rs")
|
|
112
|
+
_MDCG_PY = os.path.join(_HERE, "mdcg.py")
|
|
113
|
+
_MDCOS_PY = os.path.join(_HERE, "mdcos.py")
|
|
114
|
+
_HOTCACHE_PY = os.path.join(_HERE, "hotcache.py")
|
|
115
|
+
_EN_ZH_MAP = os.path.join(_HERE, "semantic", "en_zh_map.json")
|
|
116
|
+
_RUST_DIR = os.path.join(_REPO, "rust")
|
|
117
|
+
_EXE = os.path.join(_RUST_DIR, "target", "release",
|
|
118
|
+
"mdcg-eval.exe" if os.name == "nt" else "mdcg-eval")
|
|
119
|
+
|
|
120
|
+
# 被测面**可替换**落点(变异模式换副本):断言组一律经本表取路径,不闭包捕获原值,
|
|
121
|
+
# 否则变异对断言不可见。
|
|
122
|
+
U = {"unify": _UNIFY_PY} # Python 判据落点(unify.py 路径)
|
|
123
|
+
R = {"exe": _EXE} # Rust 判据落点(release 二进制路径)
|
|
124
|
+
RS = {"src": _ATOMS_RS} # Rust 判据**源码**落点(结构锚点读它;变异模式=副本)
|
|
125
|
+
|
|
126
|
+
# 三态判据里唯一的开态字面量(两侧共用同一开态 = "1")
|
|
127
|
+
_ON = "1"
|
|
128
|
+
|
|
129
|
+
_PASS: list = []
|
|
130
|
+
_FAIL: list = []
|
|
131
|
+
_SKIP: list = []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# 生效条件:cond 为真值判定、msg 为说明串、extra 为失败附加读数;恒把 msg 记入 _PASS 或 _FAIL(收集式,**不中断**)——使「定点变异红了几项」可数(首条即停的 assert 风格数不出红项数)。
|
|
135
|
+
def ok(cond, msg, extra=""):
|
|
136
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
137
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
138
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
139
|
+
return bool(cond)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# 生效条件:单参 msg 为说明串;把该条记入 _SKIP 并打印——依赖缺失时的**显式跳过**(不计入通过数),与「静默照绿」区分。
|
|
143
|
+
def skip(msg):
|
|
144
|
+
_SKIP.append(msg)
|
|
145
|
+
print(" SKIP(未跑·不计通过) " + msg)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _read(path):
|
|
149
|
+
with open(path, encoding="utf-8") as f:
|
|
150
|
+
return f.read()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cleanup(path) -> bool:
|
|
154
|
+
import gc
|
|
155
|
+
for _ in range(6):
|
|
156
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
157
|
+
if not os.path.exists(path):
|
|
158
|
+
return True
|
|
159
|
+
gc.collect()
|
|
160
|
+
time.sleep(0.2)
|
|
161
|
+
return not os.path.exists(path)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------- Python 判据装载
|
|
165
|
+
def install_unify(path):
|
|
166
|
+
"""把 path 处的 unify.py 装成**进程内唯一** `md_cg.semantic.unify` 并返回它。
|
|
167
|
+
|
|
168
|
+
装进 `sys.modules` 是关键:`mdcos.search` / `search_rrf` / `mdcg.search` 三处
|
|
169
|
+
入口都是**方法内** `from .semantic.unify import unify_query`(调用期解析),
|
|
170
|
+
故替换该键即让变异对**检索读数**同样可见——否则变异只打红单元断言,
|
|
171
|
+
读数类断言(G2)会对变异无感(假守卫形态)。
|
|
172
|
+
"""
|
|
173
|
+
import md_cg.semantic # 父包(相对导入解析用)
|
|
174
|
+
spec = importlib.util.spec_from_file_location("md_cg.semantic.unify", path)
|
|
175
|
+
mod = importlib.util.module_from_spec(spec)
|
|
176
|
+
sys.modules["md_cg.semantic.unify"] = mod
|
|
177
|
+
spec.loader.exec_module(mod)
|
|
178
|
+
return mod
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# 生效条件:无参;返回当前装载的 `md_cg.semantic.unify` 模块(由 install_unify 装入,缺省即工作区源)。
|
|
182
|
+
def UU():
|
|
183
|
+
return sys.modules["md_cg.semantic.unify"]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# 生效条件:无参;返回 [未设, "0", "1"] 三态快照的可调用迭代器——每态进入时把 os.environ 拨到该态,退出后**恢复原状**(含原本未设的情形)。
|
|
187
|
+
@contextlib.contextmanager
|
|
188
|
+
def env_state(value):
|
|
189
|
+
"""把 MDCG_UNIFY_QUERY 拨到 value(None = 未设),退出时恢复原状。"""
|
|
190
|
+
had = "MDCG_UNIFY_QUERY" in os.environ
|
|
191
|
+
old = os.environ.get("MDCG_UNIFY_QUERY")
|
|
192
|
+
if value is None:
|
|
193
|
+
os.environ.pop("MDCG_UNIFY_QUERY", None)
|
|
194
|
+
else:
|
|
195
|
+
os.environ["MDCG_UNIFY_QUERY"] = value
|
|
196
|
+
try:
|
|
197
|
+
yield
|
|
198
|
+
finally:
|
|
199
|
+
if had:
|
|
200
|
+
os.environ["MDCG_UNIFY_QUERY"] = old
|
|
201
|
+
else:
|
|
202
|
+
os.environ.pop("MDCG_UNIFY_QUERY", None)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ================================================================ 池与读数
|
|
206
|
+
EN_Q = "I eat beef yesterday" # 英文 query:唯一通路 = 归一层(beef→牛肉)
|
|
207
|
+
ZH_Q = "自我接纳" # 纯中文:三态恒等
|
|
208
|
+
MIX_Q = "领养 LGBTQ 群体" # 中夹英:三态恒等(收窄后)
|
|
209
|
+
NUM_Q = "2024 报告" # 无字母:三态恒等
|
|
210
|
+
GOLD = "n_gold" # 中文节点(含「牛肉」),只在归一层开时被英文 query 命中
|
|
211
|
+
|
|
212
|
+
# 池:**噪声节点在前、gold 在最后**(4 条全中文正文)。这样「无词面命中」的兜底
|
|
213
|
+
# 序里 gold 不在 top-1,而开态只留下 gold——两侧 top-1 才是**有方向**的判据。
|
|
214
|
+
# 池里**无英文正文**:英文 query 与池的词面交集**只能**由归一层产生。
|
|
215
|
+
_POOL_NODES = (
|
|
216
|
+
("n_a", "# 功能名:雨天 爱好\n# 正文:我喜欢在雨天听爵士乐。"),
|
|
217
|
+
("n_b", "# 功能名:通勤 记录\n# 正文:早高峰地铁很挤,只能站着。"),
|
|
218
|
+
("n_c", "# 功能名:种花 笔记\n# 正文:阳台上的绿萝长势不错,顺手浇了水。"),
|
|
219
|
+
(GOLD, "# 功能名:午饭 记录\n# 正文:昨天中午我在家里吃了牛肉面,配了可乐。"),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# 生效条件:root 为存在的目录时,在其 graph/ 子目录以 MdCGSecure(designer/test principal) 建两节点池并 flush,返回 (节点数, graph 目录);重复调用幂等(同 id 覆盖写)。
|
|
224
|
+
def build_pool(root):
|
|
225
|
+
"""建两节点池(与 rank_parity 同款 MdCGSecure 形态——rust serve 能读同一 graph 目录)。"""
|
|
226
|
+
from md_cg.mdcos import MdCGSecure
|
|
227
|
+
from md_cg.security import Principal
|
|
228
|
+
graph = os.path.join(root, "graph")
|
|
229
|
+
cg = MdCGSecure(graph, principal=Principal(
|
|
230
|
+
actor="unifyoff", clearance="secret", can_write=True,
|
|
231
|
+
role="designer", auth_mode="test"))
|
|
232
|
+
for nid, body in _POOL_NODES:
|
|
233
|
+
cg.add(nid, body, layer="knowledge")
|
|
234
|
+
cg.flush()
|
|
235
|
+
cg.close()
|
|
236
|
+
return len(_POOL_NODES), graph
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# 生效条件:graph 为池目录、q 为查询串、k 为正整数;以 MdCGSecure 打开该池对 q 做 search(judge=False, record=False),返回 [(id, round(score,12)), ...] 保序列表。
|
|
240
|
+
def py_reading(graph, q, k=5):
|
|
241
|
+
from md_cg.mdcos import MdCGSecure
|
|
242
|
+
from md_cg.security import Principal
|
|
243
|
+
cg = MdCGSecure(graph, principal=Principal(
|
|
244
|
+
actor="unifyoff", clearance="secret", can_write=True,
|
|
245
|
+
role="designer", auth_mode="test"))
|
|
246
|
+
try:
|
|
247
|
+
res, _meta = cg.search(q, k=k, judge=False, record=False)
|
|
248
|
+
return [(r[0].get("id") or r[0].get("path") or "", round(float(r[1]), 12))
|
|
249
|
+
for r in res]
|
|
250
|
+
finally:
|
|
251
|
+
cg.close()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# 生效条件:exe 为可执行的 rust release 二进制、graph 为池目录、state 为三态取值(None=未设 / "0" / "1");以 `--serve --score jaccard` 拉起新进程(env 显式带 MDCG_EN_ZH_MAP 指向仓库词表)、对 EN_Q 查询后关进程,返回 [(id, round(score,12)), ...];进程无输出即抛 RuntimeError。
|
|
255
|
+
def rust_reading(exe, graph, state, q=EN_Q, k=5):
|
|
256
|
+
env = dict(os.environ)
|
|
257
|
+
env["PYTHONUTF8"] = "1"
|
|
258
|
+
env["MDCG_EN_ZH_MAP"] = _EN_ZH_MAP
|
|
259
|
+
env.pop("MDCG_TOKEN", None)
|
|
260
|
+
if state is None:
|
|
261
|
+
env.pop("MDCG_UNIFY_QUERY", None)
|
|
262
|
+
else:
|
|
263
|
+
env["MDCG_UNIFY_QUERY"] = state
|
|
264
|
+
p = subprocess.Popen([exe, "--root", graph, "--serve", "--score", "jaccard"],
|
|
265
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
266
|
+
stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
267
|
+
errors="replace", env=env)
|
|
268
|
+
try:
|
|
269
|
+
p.stdin.write(json.dumps({"op": "search", "query": q, "k": k}) + "\n")
|
|
270
|
+
p.stdin.flush()
|
|
271
|
+
line = p.stdout.readline()
|
|
272
|
+
if not line:
|
|
273
|
+
err = p.stderr.read() if p.stderr else ""
|
|
274
|
+
raise RuntimeError(f"rust serve 输出关闭:{err[:400]}")
|
|
275
|
+
resp = json.loads(line)
|
|
276
|
+
hits = resp.get("hits") or resp.get("results") or []
|
|
277
|
+
out = []
|
|
278
|
+
for h in hits:
|
|
279
|
+
if isinstance(h, dict):
|
|
280
|
+
out.append((h.get("id") or h.get("node") or "",
|
|
281
|
+
round(float(h.get("score") or 0.0), 12)))
|
|
282
|
+
else:
|
|
283
|
+
out.append((str(h), 0.0))
|
|
284
|
+
return out
|
|
285
|
+
finally:
|
|
286
|
+
try:
|
|
287
|
+
p.stdin.close()
|
|
288
|
+
p.wait(timeout=5)
|
|
289
|
+
except Exception: # noqa: BLE001 —— 清理面不抛
|
|
290
|
+
try:
|
|
291
|
+
p.kill()
|
|
292
|
+
except Exception: # noqa: BLE001
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# 生效条件:reading 为 [(id, score), ...] 列表;返回其 top-1 的 id(空列表返回 None)。
|
|
297
|
+
def top1(reading):
|
|
298
|
+
return reading[0][0] if reading else None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# 生效条件:reading 为 [(id, score), ...] 列表、nid 为目标节点 id;返回该节点的分值(不在读数里返回 None)。
|
|
302
|
+
def score_of(reading, nid):
|
|
303
|
+
return dict(reading).get(nid)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ================================================================ G0 前置自证
|
|
307
|
+
def g0():
|
|
308
|
+
print("== G0 隔离、缺省态前提与基线源自证 ==")
|
|
309
|
+
ok(os.path.abspath(os.environ["MDCG_AUX_ROOT"]).lower().startswith(_TMP.lower()),
|
|
310
|
+
"G0a 辅助根落在守卫临时目录(绝不触真实凭据/密钥面)",
|
|
311
|
+
os.environ["MDCG_AUX_ROOT"])
|
|
312
|
+
ok("MDCG_UNIFY_QUERY" not in os.environ
|
|
313
|
+
and "MDCG_EN_ATOMS" not in os.environ and "MDCG_SEMANTIC" not in os.environ,
|
|
314
|
+
"G0b 口径类 env 干净:本守卫确实在**缺省态**上验证",
|
|
315
|
+
os.environ.get("MDCG_UNIFY_QUERY"))
|
|
316
|
+
src = _read(_SELF)
|
|
317
|
+
ok(re.search(r'["\']git["\']', src) is None,
|
|
318
|
+
"G0c 基线自证:本守卫源码里没有任何 git/提交快照读取面(argv 级)"
|
|
319
|
+
"——基线 = 当前工作区源码,绑定提交即失效")
|
|
320
|
+
ok(UU().__file__ == U["unify"],
|
|
321
|
+
"G0d 被测 Python 判据装载自 U 表当前指向的源(正向=工作区源;变异=临时副本)",
|
|
322
|
+
UU().__file__)
|
|
323
|
+
ok(old_default(_read(_UNIFY_PY)) is False,
|
|
324
|
+
"G0e 前置:**工作区** unify.py 的缺省已是 \"0\"(本件的前提;落点在工作区"
|
|
325
|
+
"文件而非变异副本,故变异不会把它弄红)")
|
|
326
|
+
print(" 被测面:%s" % U["unify"])
|
|
327
|
+
print(" Rust 判据:%s" % R["exe"])
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# 生效条件:src 为 unify.py 源码文本;返回 True 表示仍含「缺省 "1"」的**旧形态**(os.environ.get("MDCG_UNIFY_QUERY", "1")),否则 False。
|
|
331
|
+
def old_default(src):
|
|
332
|
+
return 'os.environ.get("MDCG_UNIFY_QUERY", "1")' in src
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ================================================================ G1 Python 三态
|
|
336
|
+
def g1():
|
|
337
|
+
print("== G1 契约①:Python 三态(未设=关 / \"0\"=关 / \"1\"=开)==")
|
|
338
|
+
with env_state(None):
|
|
339
|
+
on_unset = UU().unify_on()
|
|
340
|
+
q_unset = UU().unify_query(EN_Q)
|
|
341
|
+
with env_state("0"):
|
|
342
|
+
on_zero = UU().unify_on()
|
|
343
|
+
q_zero = UU().unify_query(EN_Q)
|
|
344
|
+
with env_state("1"):
|
|
345
|
+
on_one = UU().unify_on()
|
|
346
|
+
q_one = UU().unify_query(EN_Q)
|
|
347
|
+
ok(on_unset is False, "G1a 未设 → unify_on() is False(缺省关)", on_unset)
|
|
348
|
+
ok(on_zero is False, "G1b \"0\" → unify_on() is False", on_zero)
|
|
349
|
+
ok(on_one is True, "G1c \"1\" → unify_on() is True(唯一开态)", on_one)
|
|
350
|
+
ok(q_unset == EN_Q, "G1d 未设 → 英文 query 原样返回(未归一)%r" % q_unset)
|
|
351
|
+
ok(q_zero == EN_Q, "G1e \"0\" → 英文 query 原样返回(未归一)%r" % q_zero)
|
|
352
|
+
ok(q_one == "我 吃 牛肉 昨天",
|
|
353
|
+
"G1f \"1\" → 英文 query 归一为中文原子序列 %r" % q_one)
|
|
354
|
+
ok(q_unset == q_zero,
|
|
355
|
+
"G1g 未设产物 == \"0\" 产物(逐位相同)——缺省确实等同于关",
|
|
356
|
+
"%r vs %r" % (q_unset, q_zero))
|
|
357
|
+
ok(q_one != q_unset, "G1h 判别力:\"1\" 产物 != 未设产物(判据对开关敏感)",
|
|
358
|
+
"%r vs %r" % (q_one, q_unset))
|
|
359
|
+
# 非英文 query:三态恒等(本层只译英文内容;缺省翻转不得改变这三类)
|
|
360
|
+
for name, t in (("纯中文", ZH_Q), ("中夹英", MIX_Q), ("无字母", NUM_Q)):
|
|
361
|
+
vals = []
|
|
362
|
+
for st in (None, "0", "1"):
|
|
363
|
+
with env_state(st):
|
|
364
|
+
vals.append(UU().unify_query(t))
|
|
365
|
+
ok(vals[0] == vals[1] == vals[2] == t,
|
|
366
|
+
"G1i %s query 三态恒等且原样(%r)" % (name, t), vals)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ================================================================ G2 检索读数(同一池)
|
|
370
|
+
def g2():
|
|
371
|
+
print("== G2 契约②:同一池上,未设态读数 == \"0\" 态读数(逐位)==")
|
|
372
|
+
root = tempfile.mkdtemp(prefix="unifyoff_pool_")
|
|
373
|
+
try:
|
|
374
|
+
n, graph = build_pool(root)
|
|
375
|
+
ok(n == len(_POOL_NODES), "G2a 池建成 %d 节点(正对照:夹具不空转)" % n, n)
|
|
376
|
+
with env_state(None):
|
|
377
|
+
r_unset = py_reading(graph, EN_Q)
|
|
378
|
+
with env_state("0"):
|
|
379
|
+
r_zero = py_reading(graph, EN_Q)
|
|
380
|
+
with env_state("1"):
|
|
381
|
+
r_one = py_reading(graph, EN_Q)
|
|
382
|
+
ok(r_unset == r_zero,
|
|
383
|
+
"G2b 未设态读数 == \"0\" 态读数(逐位:id+分值)",
|
|
384
|
+
"%r vs %r" % (r_unset, r_zero))
|
|
385
|
+
ok(r_one != r_unset,
|
|
386
|
+
"G2c 判别力:\"1\" 态读数 != 未设态读数(否则该池对开关不敏感=空转)",
|
|
387
|
+
"%r vs %r" % (r_one, r_unset))
|
|
388
|
+
ok(top1(r_one) == GOLD,
|
|
389
|
+
"G2d \"1\" 态 top-1 = 中文节点 %s(归一路真的生效)" % GOLD, r_one[:2])
|
|
390
|
+
ok((score_of(r_one, GOLD) or 0.0) > (score_of(r_unset, GOLD) or 0.0),
|
|
391
|
+
"G2e 方向:\"1\" 态 gold 分值 > 未设态 gold 分值(归一路抬升了 gold)",
|
|
392
|
+
"on=%r off=%r" % (score_of(r_one, GOLD), score_of(r_unset, GOLD)))
|
|
393
|
+
ok(top1(r_unset) != GOLD,
|
|
394
|
+
"G2f 未设态 top-1 != %s(缺省确实关:无词面交集 → 兜底序里 gold 不居首)"
|
|
395
|
+
% GOLD, r_unset[:3])
|
|
396
|
+
ok(r_zero == r_unset and top1(r_zero) != GOLD,
|
|
397
|
+
"G2g \"0\" 态与未设态同判(读数逐位相同且 gold 不居首)", r_zero[:3])
|
|
398
|
+
finally:
|
|
399
|
+
_cleanup(root)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ================================================================ G3 两侧一致
|
|
403
|
+
def g3():
|
|
404
|
+
print("== G3 契约③:Rust 侧同一三态(真二进制 serve)+ 跨侧同判 ==")
|
|
405
|
+
if not os.path.isfile(R["exe"]):
|
|
406
|
+
skip("G3 两侧一致性(rust 二进制缺失)—— 修复:cd rust && cargo build --release")
|
|
407
|
+
ok(False, "G3 前置:rust release 二进制在位(fail-closed:跑不起来≠通过)",
|
|
408
|
+
R["exe"])
|
|
409
|
+
return
|
|
410
|
+
root = tempfile.mkdtemp(prefix="unifyoff_rustpool_")
|
|
411
|
+
try:
|
|
412
|
+
n, graph = build_pool(root)
|
|
413
|
+
ok(n == len(_POOL_NODES), "G3a 池建成 %d 节点(两侧同一池)" % n, n)
|
|
414
|
+
try:
|
|
415
|
+
rs_unset = rust_reading(R["exe"], graph, None)
|
|
416
|
+
rs_zero = rust_reading(R["exe"], graph, "0")
|
|
417
|
+
rs_one = rust_reading(R["exe"], graph, "1")
|
|
418
|
+
except RuntimeError as exc:
|
|
419
|
+
skip("G3 Rust 三态(serve 拉起失败:%s)" % exc)
|
|
420
|
+
ok(False, "G3 前置:rust serve 可应答(fail-closed)", exc)
|
|
421
|
+
return
|
|
422
|
+
ok(rs_unset == rs_zero,
|
|
423
|
+
"G3b Rust 未设读数 == Rust \"0\" 读数(逐位)",
|
|
424
|
+
"%r vs %r" % (rs_unset, rs_zero))
|
|
425
|
+
ok(rs_one != rs_unset,
|
|
426
|
+
"G3c 判别力:Rust \"1\" 态读数 != 未设态读数", "%r vs %r" % (rs_one, rs_unset))
|
|
427
|
+
ok(top1(rs_one) == GOLD,
|
|
428
|
+
"G3d Rust \"1\" 态 top-1 = 中文节点 %s" % GOLD, rs_one[:2])
|
|
429
|
+
ok((score_of(rs_one, GOLD) or 0.0) > (score_of(rs_unset, GOLD) or 0.0),
|
|
430
|
+
"G3e 方向:Rust \"1\" 态 gold 分值 > 未设态 gold 分值",
|
|
431
|
+
"on=%r off=%r" % (score_of(rs_one, GOLD), score_of(rs_unset, GOLD)))
|
|
432
|
+
ok(top1(rs_unset) != GOLD and top1(rs_zero) != GOLD,
|
|
433
|
+
"G3f Rust 未设/\"0\" 态 top-1 != %s(缺省确实关)" % GOLD,
|
|
434
|
+
"unset=%r zero=%r" % (top1(rs_unset), top1(rs_zero)))
|
|
435
|
+
# 跨侧同判(可比口径):每一态上「该态读数是否等价于本侧开态读数」
|
|
436
|
+
# 必须两侧一致——把两侧各自的判据归到同一布尔面再比,避免拿不同量纲硬比。
|
|
437
|
+
with env_state(None):
|
|
438
|
+
py_unset = py_reading(graph, EN_Q)
|
|
439
|
+
with env_state("0"):
|
|
440
|
+
py_zero = py_reading(graph, EN_Q)
|
|
441
|
+
with env_state("1"):
|
|
442
|
+
py_one = py_reading(graph, EN_Q)
|
|
443
|
+
for label, pr, rr in (("未设", py_unset, rs_unset),
|
|
444
|
+
("\"0\"", py_zero, rs_zero),
|
|
445
|
+
("\"1\"", py_one, rs_one)):
|
|
446
|
+
py_eq_on = (pr == py_one)
|
|
447
|
+
rs_eq_on = (rr == rs_one)
|
|
448
|
+
ok(py_eq_on == rs_eq_on,
|
|
449
|
+
"G3g 跨侧同判[%s]:Python 等价于开态=%s == Rust 等价于开态=%s"
|
|
450
|
+
% (label, py_eq_on, rs_eq_on),
|
|
451
|
+
"py=%r rs=%r" % (pr, rr))
|
|
452
|
+
# 跨侧同判(判据面):Python 开关判据的三态 == Rust 开态等价的补
|
|
453
|
+
with env_state(None):
|
|
454
|
+
py_on_unset = UU().unify_on()
|
|
455
|
+
with env_state("0"):
|
|
456
|
+
py_on_zero = UU().unify_on()
|
|
457
|
+
with env_state("1"):
|
|
458
|
+
py_on_one = UU().unify_on()
|
|
459
|
+
ok((py_on_unset, py_on_zero, py_on_one) == (False, False, True),
|
|
460
|
+
"G3h Python 判据三态 = (未设 False, \"0\" False, \"1\" True)",
|
|
461
|
+
(py_on_unset, py_on_zero, py_on_one))
|
|
462
|
+
ok(py_on_one is True and rs_one != rs_unset,
|
|
463
|
+
"G3i 两侧对「显式 \"1\" 即开」同判(Python 判据 True / Rust 读数与未设态不同)",
|
|
464
|
+
"rs one=%r unset=%r" % (rs_one, rs_unset))
|
|
465
|
+
finally:
|
|
466
|
+
_cleanup(root)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ================================================================ G4 结构面
|
|
470
|
+
def g4():
|
|
471
|
+
print("== G4 结构面:两侧判据落点锚点、接线三处、开关未删 ==")
|
|
472
|
+
usrc = _read(U["unify"])
|
|
473
|
+
# 锚点取**代码行**(行首缩进 + return)而非裸表达式:后者在 unify.py 的 CCG
|
|
474
|
+
# 注释里同样出现过一次,裸串锚点会被注释「掩护」——定点变异若只替换注释那处,
|
|
475
|
+
# 代码不动、本断言仍绿(假红/假绿都由此而来,2026-09-30 实测踩过)。
|
|
476
|
+
ok('\n return os.environ.get("MDCG_UNIFY_QUERY", "0") == "1"' in usrc,
|
|
477
|
+
"G4a Python 判据落点锚点在位(代码行级):缺省 \"0\" 且仅 \"1\" 为真")
|
|
478
|
+
ok(old_default(usrc) is False,
|
|
479
|
+
"G4b Python 侧无「缺省 \"1\"」残留(防回退)")
|
|
480
|
+
ok("默认关" in usrc,
|
|
481
|
+
"G4c unify.py 头注写明缺省关(文书锚点——M1/M2 不变红:它钉的是注释文本)")
|
|
482
|
+
rsrc = _read(RS["src"])
|
|
483
|
+
ok('.as_deref() != Ok("1")' in rsrc,
|
|
484
|
+
"G4d Rust 判据落点锚点在位:未设/非 \"1\" → 不载入(唯一开态 \"1\")")
|
|
485
|
+
ok('unwrap_or(false)' not in rsrc.split("pub fn from_env")[1].split("pub fn")[0],
|
|
486
|
+
"G4e Rust from_env 内无「未设=开」旧形态(unwrap_or(false) 已移除)")
|
|
487
|
+
ok("缺省由「未设=开」翻为「未设=关」" in rsrc,
|
|
488
|
+
"G4f atoms.rs 头注写明缺省翻转与三态表(文书锚点——M2 不变红:钉的是注释)")
|
|
489
|
+
# 三处 search 入口接线仍在(接线缺失 → 开关形同虚设;同理为防回退锚点)
|
|
490
|
+
mdcg_src = _read(_MDCG_PY)
|
|
491
|
+
ok(mdcg_src.count("q = unify_query(q)") >= 1,
|
|
492
|
+
"G4g md_cg/mdcg.py search 入口仍接线 unify_query(防回退锚点)",
|
|
493
|
+
mdcg_src.count("q = unify_query(q)"))
|
|
494
|
+
mdcos_src = _read(_MDCOS_PY)
|
|
495
|
+
ok(mdcos_src.count("q = unify_query(q)") >= 2,
|
|
496
|
+
"G4h md_cg/mdcos.py search/search_rrf 两入口仍接线 unify_query(防回退锚点)",
|
|
497
|
+
mdcos_src.count("q = unify_query(q)"))
|
|
498
|
+
hc = _read(_HOTCACHE_PY)
|
|
499
|
+
ok('"MDCG_UNIFY_QUERY"' in hc and re.search(
|
|
500
|
+
r"_ENV_SWITCHES = \([^)]*\"MDCG_UNIFY_QUERY\"", hc) is not None,
|
|
501
|
+
"G4i 开关仍登记在 hotcache._ENV_SWITCHES(跨口径缓存键不得漏登;防回退锚点)")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# ================================================================ 变异表
|
|
505
|
+
# 每条 = (名称, 目标, 源码锚点, 替换文本, **预期转红条数**, 是否重建 Rust 二进制)。
|
|
506
|
+
# 锚点必须逐字命中当前源码(找不到 → ANCHOR-MISS → 退出码 2,fail-closed);
|
|
507
|
+
# 红项数必须**恰好**等于预期(多红=判据越界、少红=判据空转,两种都判失败)。
|
|
508
|
+
# 预期值的由来:本机实测(2026-09-30),红项名录见 --mutate 输出。
|
|
509
|
+
#
|
|
510
|
+
# 覆盖映射(哪条变异打红哪一面):
|
|
511
|
+
# M1 → G0?否(G0d/G0e 是**工作区**前置,刻意不被变异弄红)
|
|
512
|
+
# G1a/G1d/G1g/G1h(Python 三态与产物)、G2b/G2c/G2e/G2f/G2g(检索读数)、
|
|
513
|
+
# G3g[未设](跨侧同判)、G3h(Python 判据三态)、G4a/G4b(Python 判据锚点)
|
|
514
|
+
# M2 → G3b/G3c/G3e/G3f/G3g[未设]/G3i(Rust 读数与跨侧同判)、
|
|
515
|
+
# G4d/G4e(Rust 判据锚点,读**变异副本**源码)
|
|
516
|
+
# **文书/接线类锚点不被这两条变异弄红**(它们钉的是注释文本与接线,不是开关判据
|
|
517
|
+
# 本身):G4c(头注「默认关」)、G4f(atoms.rs 头注三态表)、G4g/G4h(三处接线)、
|
|
518
|
+
# G4i(hotcache 环境开关登记)。它们是**防回退锚点**,钉错即 G4 直接红——
|
|
519
|
+
# 其效力由「实现漂移即红」保证,不由变异证明。
|
|
520
|
+
_MUTATIONS = (
|
|
521
|
+
("M1 Python 缺省改回 \"1\"(缺陷形态:未设=开)",
|
|
522
|
+
"py",
|
|
523
|
+
'\n return os.environ.get("MDCG_UNIFY_QUERY", "0") == "1"',
|
|
524
|
+
'\n return os.environ.get("MDCG_UNIFY_QUERY", "1") == "1"',
|
|
525
|
+
13,
|
|
526
|
+
False),
|
|
527
|
+
("M2 Rust 三态改回「未设=开」(缺陷形态)",
|
|
528
|
+
"rs",
|
|
529
|
+
'if std::env::var("MDCG_UNIFY_QUERY").as_deref() != Ok("1") {\n'
|
|
530
|
+
' return None; // 未设(缺省关)/ 显式 "0" / 非 "1":归一层不生效\n'
|
|
531
|
+
' }',
|
|
532
|
+
'if std::env::var("MDCG_UNIFY_QUERY").map(|v| v == "0").unwrap_or(false) {\n'
|
|
533
|
+
' return None; // 未设=开(缺陷形态)\n'
|
|
534
|
+
' }',
|
|
535
|
+
8,
|
|
536
|
+
True),
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _write_mutated(src, old, new, tag):
|
|
541
|
+
"""把 src 做定点文本替换后写入**守卫临时目录**;锚点未命中返回 (None, src)。"""
|
|
542
|
+
if old not in src:
|
|
543
|
+
return None, src
|
|
544
|
+
path = os.path.join(_TMP, "mut_%s.py" % tag)
|
|
545
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
546
|
+
f.write(src.replace(old, new, 1))
|
|
547
|
+
return path, src
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _rust_mutation_exe(mut_rs_src):
|
|
551
|
+
"""把变异后的 atoms.rs 写进**rust 目录的临时副本**、以临时 CARGO_TARGET_DIR
|
|
552
|
+
重建 release 二进制,返回 (exe | None, 说明)。副本与构建产物全在临时目录,
|
|
553
|
+
**不碰仓库工作区与仓库 target/**。"""
|
|
554
|
+
cargo = shutil.which("cargo")
|
|
555
|
+
if not cargo:
|
|
556
|
+
return None, "cargo 不在 PATH"
|
|
557
|
+
d = tempfile.mkdtemp(prefix="unifyoff_rust_")
|
|
558
|
+
dst = os.path.join(d, "rust")
|
|
559
|
+
shutil.copytree(_RUST_DIR, dst, ignore=shutil.ignore_patterns("target"))
|
|
560
|
+
with open(os.path.join(dst, "src", "atoms.rs"), "w", encoding="utf-8") as f:
|
|
561
|
+
f.write(mut_rs_src)
|
|
562
|
+
tgt = os.path.join(d, "target")
|
|
563
|
+
env = dict(os.environ)
|
|
564
|
+
env["CARGO_TARGET_DIR"] = tgt
|
|
565
|
+
env["PYTHONUTF8"] = "1"
|
|
566
|
+
p = subprocess.run([cargo, "build", "--release", "--offline"], cwd=dst,
|
|
567
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
568
|
+
errors="replace", env=env, timeout=900)
|
|
569
|
+
exe = os.path.join(tgt, "release", "mdcg-eval.exe" if os.name == "nt" else "mdcg-eval")
|
|
570
|
+
if p.returncode != 0 or not os.path.isfile(exe):
|
|
571
|
+
return None, "cargo build rc=%s:%s" % (p.returncode, (p.stderr or "")[-400:])
|
|
572
|
+
return exe, "临时副本重建 rc=0"
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# 生效条件:无参;按当前 U 表**重新装载** Python 判据(变异模式 = 临时副本,
|
|
576
|
+
# 正向 = 工作区源),随后跑全部断言组(G0–G4),清空并重填 _PASS/_FAIL/_SKIP,
|
|
577
|
+
# 返回失败条数。装载必须在组内**每次**重做:否则变异只对 g0 的结构断言可见,
|
|
578
|
+
# 行为类断言仍吃旧模块 = 假自证。
|
|
579
|
+
def _run_groups():
|
|
580
|
+
install_unify(U["unify"])
|
|
581
|
+
_PASS.clear()
|
|
582
|
+
_FAIL.clear()
|
|
583
|
+
_SKIP.clear()
|
|
584
|
+
for g in (g0, g1, g2, g3, g4):
|
|
585
|
+
try:
|
|
586
|
+
g()
|
|
587
|
+
except Exception as exc: # noqa: BLE001
|
|
588
|
+
ok(False, "断言组 %s 抛异常:%r" % (g.__name__, exc))
|
|
589
|
+
return len(_FAIL)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _mutate_mode(list_only=False):
|
|
593
|
+
print("!! 定点变异自证:把修复点改回缺陷形态,套件必须按预期条数转红\n")
|
|
594
|
+
if list_only:
|
|
595
|
+
for name, _kind, old, new, exp, rebuild in _MUTATIONS:
|
|
596
|
+
print(" %-46s expect_red=%s rebuild_rust=%s" % (name, exp or "自校准", rebuild))
|
|
597
|
+
return 0
|
|
598
|
+
|
|
599
|
+
anchor_miss = []
|
|
600
|
+
bad = []
|
|
601
|
+
|
|
602
|
+
# ---- 基线(未变异):必须全绿,且以此校准每条变异的预期红项数 ----
|
|
603
|
+
U["unify"] = _UNIFY_PY
|
|
604
|
+
R["exe"] = _EXE
|
|
605
|
+
install_unify(_UNIFY_PY)
|
|
606
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
607
|
+
clean = _run_groups()
|
|
608
|
+
clean_detail = buf.getvalue()
|
|
609
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
610
|
+
if clean:
|
|
611
|
+
bad.append("未变异基线即失败:%s"
|
|
612
|
+
% [l for l in clean_detail.splitlines() if "FAIL" in l][:4])
|
|
613
|
+
|
|
614
|
+
for name, kind, old, new, expect, rebuild in _MUTATIONS:
|
|
615
|
+
if kind == "py":
|
|
616
|
+
live_src = _read(_UNIFY_PY)
|
|
617
|
+
path, _ = _write_mutated(live_src, old, new, "unify_default")
|
|
618
|
+
if path is None:
|
|
619
|
+
print(" ANCHOR-MISS %s —— 锚点在 md_cg/semantic/unify.py 当前源码里"
|
|
620
|
+
"找不到(实现改了却没同步本表;基线不得静默漂移)" % name)
|
|
621
|
+
anchor_miss.append(name)
|
|
622
|
+
continue
|
|
623
|
+
U["unify"] = path
|
|
624
|
+
try:
|
|
625
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
626
|
+
fails = _run_groups()
|
|
627
|
+
detail = buf.getvalue()
|
|
628
|
+
finally:
|
|
629
|
+
U["unify"] = _UNIFY_PY
|
|
630
|
+
install_unify(_UNIFY_PY)
|
|
631
|
+
else:
|
|
632
|
+
live_src = _read(_ATOMS_RS)
|
|
633
|
+
if old not in live_src:
|
|
634
|
+
print(" ANCHOR-MISS %s —— 锚点在 rust/src/atoms.rs 当前源码里找不到"
|
|
635
|
+
"(实现改了却没同步本表;基线不得静默漂移)" % name)
|
|
636
|
+
anchor_miss.append(name)
|
|
637
|
+
continue
|
|
638
|
+
exe, note = _rust_mutation_exe(live_src.replace(old, new, 1))
|
|
639
|
+
if exe is None:
|
|
640
|
+
print(" 重建失败 %s:%s" % (name, note))
|
|
641
|
+
bad.append("%s(Rust 变异二进制重建失败:%s)" % (name, note))
|
|
642
|
+
continue
|
|
643
|
+
# 结构锚点(G4d/G4e)读**实际生效的源**:变异模式下指向本副本,
|
|
644
|
+
# 否则「Rust 判据锚点」类断言对变异永久无感(假守卫形态)。
|
|
645
|
+
mut_rs = os.path.join(_TMP, "mut_atoms.rs")
|
|
646
|
+
with open(mut_rs, "w", encoding="utf-8") as f:
|
|
647
|
+
f.write(live_src.replace(old, new, 1))
|
|
648
|
+
R["exe"] = exe
|
|
649
|
+
RS["src"] = mut_rs
|
|
650
|
+
try:
|
|
651
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
652
|
+
fails = _run_groups()
|
|
653
|
+
detail = buf.getvalue()
|
|
654
|
+
finally:
|
|
655
|
+
R["exe"] = _EXE
|
|
656
|
+
RS["src"] = _ATOMS_RS
|
|
657
|
+
print(" (%s:%s)" % (name, note))
|
|
658
|
+
red = [l.strip()[5:] for l in detail.splitlines() if l.strip().startswith("FAIL ")]
|
|
659
|
+
verdict = ("红(恰好命中预期)" if fails == expect
|
|
660
|
+
else ("**仍全绿 = 该判据空转**" if fails == 0
|
|
661
|
+
else "**红项数不符**(多红=判据越界 / 少红=判据空转)"))
|
|
662
|
+
mark = "OK " if fails == expect else "MISMATCH"
|
|
663
|
+
print(" %s %-46s 红项=%d 预期=%d %s" % (mark, name, fails, expect, verdict))
|
|
664
|
+
for l in red:
|
|
665
|
+
print(" " + l)
|
|
666
|
+
if fails != expect:
|
|
667
|
+
bad.append("%s(红=%d 预期=%d)" % (name, fails, expect))
|
|
668
|
+
|
|
669
|
+
if anchor_miss:
|
|
670
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
671
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
672
|
+
return 2
|
|
673
|
+
print("\n定点变异自证:%s" % ("PASS(每处判据都被打红)" if not bad
|
|
674
|
+
else "FAIL —— " + "、".join(bad)))
|
|
675
|
+
return 0 if not bad else 1
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def main():
|
|
679
|
+
if "--mutate" in sys.argv:
|
|
680
|
+
try:
|
|
681
|
+
return _mutate_mode("--list" in sys.argv)
|
|
682
|
+
finally:
|
|
683
|
+
_cleanup(_TMP)
|
|
684
|
+
if not os.path.isfile(_EXE):
|
|
685
|
+
print("!! rust release 二进制缺失:%s" % _EXE)
|
|
686
|
+
print(" 修复:cd rust && cargo build --release")
|
|
687
|
+
print(" 处置:**fail-closed**(本守卫不计通过、直接退出 1;"
|
|
688
|
+
"不采用静默 SKIP 而返回 0——跑不起来永远不等于通过)")
|
|
689
|
+
return 1
|
|
690
|
+
try:
|
|
691
|
+
install_unify(_UNIFY_PY)
|
|
692
|
+
n_fail = _run_groups()
|
|
693
|
+
print("\nUNIFY-DEFAULT-OFF 守卫:%d 通过,%d 失败,%d 跳过"
|
|
694
|
+
% (len(_PASS), n_fail, len(_SKIP)))
|
|
695
|
+
return 0 if not n_fail else 1
|
|
696
|
+
finally:
|
|
697
|
+
_cleanup(_TMP)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
if __name__ == "__main__":
|
|
701
|
+
sys.exit(main())
|