@furongjun1999/dsh-memory 0.7.0 → 0.7.2
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 +8 -6
- package/codebuddy/CODEBUDDY.md +5 -5
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/docs/eval//345/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -0
- 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
- 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 +25 -25
- 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
- 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
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +4 -4
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench6_arms.py +10 -6
- package/md_cg/census.py +2 -1
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +17 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +63 -7
- package/md_cg/mdcg.py +157 -31
- package/md_cg/mdcos.py +675 -19
- package/md_cg/mreview/locate.py +1 -1
- package/md_cg/protect.py +79 -4
- package/md_cg/provenance.py +1 -1
- package/md_cg/review_cli.py +31 -2
- package/md_cg/rollback.py +420 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/routing.py +10 -1
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/test_autonomy_admission.py +1098 -0
- package/md_cg/test_autonomy_modes.py +1972 -0
- package/md_cg/test_b1_auto_id_multiproc.py +7 -0
- package/md_cg/test_b1b2_write_face.py +7 -0
- package/md_cg/test_b3_merge_keeps_content.py +7 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
- package/md_cg/test_generation_guard.py +15 -9
- package/md_cg/test_health_corrupt_utf8.py +37 -0
- package/md_cg/test_i50a_half_dup_defer.py +408 -0
- package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
- package/md_cg/test_i50c_meta_passthrough.py +608 -0
- package/md_cg/test_i50d_importance_source.py +531 -0
- package/md_cg/test_i50e_readside_protection.py +656 -0
- package/md_cg/test_index_crossprocess_reload.py +39 -24
- package/md_cg/test_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -0
- package/md_cg/test_issue53_cipher_health_hash.py +385 -0
- package/md_cg/test_linkref.py +7 -0
- package/md_cg/test_mode_parity.py +1393 -0
- package/md_cg/test_mutation_rollback.py +805 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p9_forget_protect.py +7 -0
- package/md_cg/test_p9c_dedup_hints.py +7 -0
- package/md_cg/test_policy_required_ccg.py +5 -1
- package/md_cg/test_protocol.py +7 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/writepipe.py +101 -2
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/zcode/AGENTS.md +5 -5
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 三档自治「准入读数 R1–R4 与准入闸」守卫(设计 v0.2 §六/§七/§十一 · 批次④)。
|
|
3
|
+
|
|
4
|
+
覆盖(设计 §十一「R1–R4 各有独立断言与一组门槛刚过/刚不过的边界用例,
|
|
5
|
+
含窗口边界 3 天/30 天」在本批的落点):
|
|
6
|
+
|
|
7
|
+
A 组 **R1 判断非退化**(30 天窗):三态齐 + 单一占比 <90% 的满足判据;
|
|
8
|
+
门槛边界——恰 90%(刚不过)/ 89.9%(刚过)/ 三态缺失 / **30 天窗口
|
|
9
|
+
边界 ±60s**(窗外不计入)/ 缺数据面;
|
|
10
|
+
B 组 **R2 会说「不」**(短期 3 天 + 长期 30 天):3 天内各 ≥1(刚过=恰好 1)
|
|
11
|
+
/ 3 天外 30 天内(短期刚不过,同时钉长期读数照记)/ **3 天窗口边界
|
|
12
|
+
±60s** / 无 created_at 不计窗(如实);短期满足 ⇒ 长期自动满足
|
|
13
|
+
(窗口嵌套——长期判据的独立判别力在「短期不足」侧,见 B2);
|
|
14
|
+
C 组 **R3 破坏可逆且「回过」**(30 天窗,状态推断见 admission docstring):
|
|
15
|
+
D 面真跑 forget→restore(刚过=1 次)/ 只删不回(刚不过)/ 只有 restore
|
|
16
|
+
无更早 forget 不计(证据链要成对)/ C 面真跑 出单→accept→回滚
|
|
17
|
+
(盘面逐字节==执行时点前像)/ 执行不回滚(盘面≠前像,刚不过)/
|
|
18
|
+
**30 天窗口边界 ±60s**(窗外 restore 不计);
|
|
19
|
+
D 组 **R4 判断收敛**(3×10 天子窗):0.40/0.30/0.10 严格下降且最近 <20%
|
|
20
|
+
(刚过)/ 恰 20%(刚不过)/ 持平(非严格下降,刚不过)/ 样本不足 ⇒
|
|
21
|
+
不可判(ok=None,不假装)/ **10 天子窗归属边界** / edit 与 reject 同权;
|
|
22
|
+
E 组 **准入闸**(`autonomy_modes.settle`/`mode`):full+不满足 ⇒ 实际生效
|
|
23
|
+
档位回落 confirm 且 alerts 报「缺哪条读数」(不静默)/ full+满足 ⇒ full
|
|
24
|
+
(与改动前逐位一致)/ plan·confirm 不受读数影响(零 IO 零告警)/ 非法
|
|
25
|
+
env fail-closed / 未结算=旧行为(对拍)/ 缓存频度(TTL 内不重扫——
|
|
26
|
+
「不得让每次 decide 都全库扫描」的机械证明)/ decide 端到端看回落 /
|
|
27
|
+
只读(结算前后库指纹不变);
|
|
28
|
+
F 组 **结构单点与只读**:数据面常量与真源同值 / 显式 root 不调 default_root
|
|
29
|
+
/ CLI 形态(够格 rc=0、不够格 rc=1、--json 可读)/ check 幂等且零写入;
|
|
30
|
+
G 组 **补强批次(v1.1)**:数值容错——`_ts`/`_fm_ts` 逐型(400 位整数 /
|
|
31
|
+
`1e400`(=inf) / -inf / NaN / 字符串数字 / 负数 / None ⇒ None 不抛;
|
|
32
|
+
合法浮点/整数/可表示大数逐位不变)+ 四条读数与 `check()` 对含溢出行的
|
|
33
|
+
数据面不崩、`undated` 计数正确 + 含溢出行库的 `settle` 容错生效(回落
|
|
34
|
+
confirm 而非结算失败态);结算异常 fail-closed——从未结算 + 异常 ⇒
|
|
35
|
+
失败态(error 非空、alerts 报「读数不可得」)+ `mode()` 回落 confirm
|
|
36
|
+
(收口 fail-open)/ 已成功结算 + 异常 ⇒ 不升不降(保持上次 effective)
|
|
37
|
+
且失败态可观测 / 失败态与「未调用过 settle」可区分 / 「未调用过 settle
|
|
38
|
+
⇒ mode()=env」对拍保持 / 失败态路径零写入。
|
|
39
|
+
|
|
40
|
+
**定点变异自证**(`--mutate`,与 `md_cg/test_neg_condition_hits.py` /
|
|
41
|
+
`md_cg/test_autonomy_modes.py` 同口径):表内每项 = (说明, 目标, 锚点原文,
|
|
42
|
+
替换文, 预期红项数)。锚点须**逐字**出现在目标函数源码里;漂移即
|
|
43
|
+
ANCHOR-MISS(fail-closed,exit 2)。红项数与实测**逐一相符**才算通过。
|
|
44
|
+
补强批次新增 4 处(容错摘除 / 失败态放行 full / 失败态总是 confirm /
|
|
45
|
+
结算异常不捕获),并校准既有 ⑫/⑭ 的红项数(G 组判据并入)。
|
|
46
|
+
|
|
47
|
+
沙箱(硬约束):一切读写都在 tempfile.mkdtemp 内(含 MDCG_AUX_ROOT / 主密钥
|
|
48
|
+
——crypto 在导入期求值 MASTER_FILE,故必须在任何 md_cg 子模块 import **之前**
|
|
49
|
+
设好);跑完 rmtree。**绝不碰在役数据根**:本守卫所有读数/结算都传显式合成
|
|
50
|
+
root;F2 另钉「显式 root 不触 default_root」。档位 env 由夹具按需设置并在
|
|
51
|
+
finally 还原;结算态(settle)每组用完即 reset_settlement。
|
|
52
|
+
|
|
53
|
+
运行:
|
|
54
|
+
python -X utf8 -m md_cg.test_autonomy_admission # 正常跑
|
|
55
|
+
python -X utf8 -m md_cg.test_autonomy_admission --mutate # 定点变异自证
|
|
56
|
+
"""
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import contextlib
|
|
60
|
+
import inspect
|
|
61
|
+
import io
|
|
62
|
+
import json
|
|
63
|
+
import os
|
|
64
|
+
import shutil
|
|
65
|
+
import subprocess
|
|
66
|
+
import sys
|
|
67
|
+
import tempfile
|
|
68
|
+
import time
|
|
69
|
+
|
|
70
|
+
# ---- 沙箱:必须在任何 md_cg 子模块 import 之前 ------------------------------
|
|
71
|
+
_SANDBOX = tempfile.mkdtemp(prefix="autonomy_admission_sandbox_")
|
|
72
|
+
os.environ["MDCG_AUX_ROOT"] = _SANDBOX
|
|
73
|
+
os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
|
|
74
|
+
os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
|
|
75
|
+
os.environ.pop("MDCG_TEST_LIVE_ROOT", None)
|
|
76
|
+
_OLD_POLICY = os.environ.pop("MDCG_POLICY_FILE", None)
|
|
77
|
+
|
|
78
|
+
from . import admission as adm # noqa: E402
|
|
79
|
+
from . import autonomy_modes as am # noqa: E402
|
|
80
|
+
from . import forgetting, mdcos, nodefile, protect # noqa: E402
|
|
81
|
+
from . import rollback as rb # noqa: E402
|
|
82
|
+
from .mdcos import MdCGOS # noqa: E402
|
|
83
|
+
|
|
84
|
+
_ENV_MODE = am.AUTONOMY_ENV_KEYS["mode"]
|
|
85
|
+
DAY = 86400.0 # 字面量:用例数据不引用被测常量(否则常量变异会带着数据一起漂移、判别力流失)
|
|
86
|
+
NOW = time.time()
|
|
87
|
+
|
|
88
|
+
_PASS = []
|
|
89
|
+
_FAIL = []
|
|
90
|
+
_RUN = [0]
|
|
91
|
+
|
|
92
|
+
BODY = ("# 功能名:部署面端口约定\n"
|
|
93
|
+
"# 生效条件:载体/位置:prod 集群;时间:2026-09-30 起;方法:部署清单核对;约束:无\n"
|
|
94
|
+
"# 子功能:登记各服务监听端口\n"
|
|
95
|
+
"# 执行:核对 manifest 的 ports 段\n"
|
|
96
|
+
"# 验证方式:test\n"
|
|
97
|
+
"# 不适用条件:无\n"
|
|
98
|
+
"\n网关监听端口 8080/HTTP;管理面监听端口 8081/HTTP;"
|
|
99
|
+
"指标面监听端口 %s/HTTP;日志面监听端口 9091/HTTP\n")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def ok(cond, msg, extra=""):
|
|
103
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
104
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
105
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _root(tag="lib"):
|
|
109
|
+
"""独立合成库根(每轮独立子根:变异模式连跑多轮,共用目录会读到旧盘面)。"""
|
|
110
|
+
p = os.path.join(_SANDBOX, "%s_r%d" % (tag, _RUN[0]))
|
|
111
|
+
os.makedirs(p, exist_ok=True)
|
|
112
|
+
return p
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _wr_jsonl(root, rel, rows):
|
|
116
|
+
"""写 JSONL 数据面(合成库内;只被守卫调用,生产码不写)。"""
|
|
117
|
+
p = os.path.join(root, rel)
|
|
118
|
+
d = os.path.dirname(p)
|
|
119
|
+
if d:
|
|
120
|
+
os.makedirs(d, exist_ok=True)
|
|
121
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
122
|
+
for r in rows:
|
|
123
|
+
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
124
|
+
return p
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _mk_layer(root, layer, nid, created_at=None):
|
|
128
|
+
"""造一个层节点 md(fm 含 id/layer/created_at——R2 的判窗面)。"""
|
|
129
|
+
p = os.path.join(root, layer, nid + ".md")
|
|
130
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
131
|
+
fm = {"id": nid, "layer": layer}
|
|
132
|
+
if created_at is not None:
|
|
133
|
+
fm["created_at"] = created_at
|
|
134
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
135
|
+
f.write(nodefile.dumps(fm, "# 探针节点\n"))
|
|
136
|
+
return p
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _rows_r4(windows):
|
|
140
|
+
"""windows = [(n_reject, n_accept)] 按 W1(最近)/W2/W3 顺序 → decisions 行。"""
|
|
141
|
+
rows = []
|
|
142
|
+
for k, (n_rej, n_acc) in enumerate(windows, start=1):
|
|
143
|
+
mid = NOW - (k - 0.5) * 10 * DAY
|
|
144
|
+
rows += [{"t": mid, "decision": "reject"} for _ in range(n_rej)]
|
|
145
|
+
rows += [{"t": mid, "decision": "accept"} for _ in range(n_acc)]
|
|
146
|
+
return rows
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _satisfying(tag):
|
|
150
|
+
"""四条读数全满足的合成库(E 组「full+满足 ⇒ full」用;F1 也复用它)。"""
|
|
151
|
+
r = _root(tag)
|
|
152
|
+
_wr_jsonl(r, adm.FORGETTING_LOG, [
|
|
153
|
+
{"t": NOW - 3600, "verdict": "DROP"},
|
|
154
|
+
{"t": NOW - 3600, "verdict": "MERGE"},
|
|
155
|
+
{"t": NOW - 3600, "verdict": "DEFER"}])
|
|
156
|
+
_mk_layer(r, "rejected", "rej_ok", NOW - 3600)
|
|
157
|
+
_mk_layer(r, "unresolved", "unr_ok", NOW - 3600)
|
|
158
|
+
_wr_jsonl(r, adm.DEVICE_AUDIT, [
|
|
159
|
+
{"t": NOW - 7200, "op": "forget", "id": "z_1"},
|
|
160
|
+
{"t": NOW - 3600, "op": "restore", "id": "z_1", "forced": True}])
|
|
161
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, _rows_r4([(1, 9), (3, 7), (4, 6)]))
|
|
162
|
+
return r
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@contextlib.contextmanager
|
|
166
|
+
def _mode(m):
|
|
167
|
+
"""按需设档位,退出还原(夹具专用;不在生产码里读/写档位)。"""
|
|
168
|
+
old = os.environ.get(_ENV_MODE)
|
|
169
|
+
os.environ[_ENV_MODE] = m
|
|
170
|
+
try:
|
|
171
|
+
yield m
|
|
172
|
+
finally:
|
|
173
|
+
if old is None:
|
|
174
|
+
os.environ.pop(_ENV_MODE, None)
|
|
175
|
+
else:
|
|
176
|
+
os.environ[_ENV_MODE] = old
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _mk_cg(root):
|
|
180
|
+
return MdCGOS(root, autoflush=1)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# =============================================================== A 组:R1
|
|
184
|
+
def g_a():
|
|
185
|
+
print("== A 组:R1 判断非退化(30 天窗 · 门槛刚过/刚不过/窗口边界)==")
|
|
186
|
+
r = _root("r1_ok")
|
|
187
|
+
_wr_jsonl(r, adm.FORGETTING_LOG, [
|
|
188
|
+
{"t": NOW - 3600, "verdict": "DROP"},
|
|
189
|
+
{"t": NOW - 3600, "verdict": "MERGE"},
|
|
190
|
+
{"t": NOW - 3600, "verdict": "DEFER"}])
|
|
191
|
+
d = adm.r1_non_degenerate(r, now=NOW)
|
|
192
|
+
ok(d["ok"] is True and d["total"] == 3 and d["missing_verdicts"] == []
|
|
193
|
+
and d["top_share"] < 0.90,
|
|
194
|
+
"A1 三态齐且单一占比 33%% → 满足(门槛刚过侧)", d["detail"])
|
|
195
|
+
|
|
196
|
+
r = _root("r1_edge90")
|
|
197
|
+
_wr_jsonl(r, adm.FORGETTING_LOG,
|
|
198
|
+
[{"t": NOW - 3600, "verdict": "DROP"}] * 18
|
|
199
|
+
+ [{"t": NOW - 3600, "verdict": "MERGE"}]
|
|
200
|
+
+ [{"t": NOW - 3600, "verdict": "DEFER"}])
|
|
201
|
+
d = adm.r1_non_degenerate(r, now=NOW)
|
|
202
|
+
ok(d["ok"] is False and abs((d["top_share"] or 0) - 0.90) < 1e-9,
|
|
203
|
+
"A2 单一 verdict 恰 90%% → 不满足(阈值严格小于,刚不过)", d["detail"])
|
|
204
|
+
|
|
205
|
+
r = _root("r1_edge899")
|
|
206
|
+
_wr_jsonl(r, adm.FORGETTING_LOG,
|
|
207
|
+
[{"t": NOW - 3600, "verdict": "DROP"}] * 899
|
|
208
|
+
+ [{"t": NOW - 3600, "verdict": "MERGE"}] * 51
|
|
209
|
+
+ [{"t": NOW - 3600, "verdict": "DEFER"}] * 50)
|
|
210
|
+
d = adm.r1_non_degenerate(r, now=NOW)
|
|
211
|
+
ok(d["ok"] is True and abs((d["top_share"] or 0) - 0.899) < 1e-9,
|
|
212
|
+
"A3 单一 verdict 89.9%% → 满足(刚过)", d["detail"])
|
|
213
|
+
|
|
214
|
+
r = _root("r1_short3")
|
|
215
|
+
_wr_jsonl(r, adm.FORGETTING_LOG,
|
|
216
|
+
[{"t": NOW - 3600, "verdict": "DROP"}] * 5
|
|
217
|
+
+ [{"t": NOW - 3600, "verdict": "MERGE"}] * 5)
|
|
218
|
+
d = adm.r1_non_degenerate(r, now=NOW)
|
|
219
|
+
ok(d["ok"] is False and d["missing_verdicts"] == ["DEFER"],
|
|
220
|
+
"A4 三态不全(缺 DEFER)→ 不满足(占比再低也不行)", d["detail"])
|
|
221
|
+
|
|
222
|
+
r = _root("r1_border")
|
|
223
|
+
_wr_jsonl(r, adm.FORGETTING_LOG, [
|
|
224
|
+
{"t": NOW - 30 * DAY - 60, "verdict": "DROP"}, # 窗外
|
|
225
|
+
{"t": NOW - 30 * DAY + 60, "verdict": "MERGE"}, # 窗内
|
|
226
|
+
{"t": NOW - 30 * DAY + 120, "verdict": "DEFER"}]) # 窗内
|
|
227
|
+
d = adm.r1_non_degenerate(r, now=NOW)
|
|
228
|
+
ok(d["total"] == 2 and "DROP" not in d["by_verdict"]
|
|
229
|
+
and d["summary_all"]["total"] == 3,
|
|
230
|
+
"A5 30 天窗口边界 ±60s:窗外一条不计入窗口、窗内两条计入(全量仍 3)",
|
|
231
|
+
{"win": d["by_verdict"], "all": d["summary_all"]})
|
|
232
|
+
|
|
233
|
+
r = _root("r1_missing")
|
|
234
|
+
d = adm.r1_non_degenerate(r, now=NOW)
|
|
235
|
+
ok(d["ok"] is False and adm.FORGETTING_LOG in d["detail"],
|
|
236
|
+
"A6 缺数据面(无 _forgetting.jsonl)→ 不满足且 detail 指名缺哪个文件",
|
|
237
|
+
d["detail"])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# =============================================================== B 组:R2
|
|
241
|
+
def g_b():
|
|
242
|
+
print("== B 组:R2 会说「不」(3 天/30 天 · 门槛刚过/刚不过/窗口边界)==")
|
|
243
|
+
r = _root("r2_ok")
|
|
244
|
+
_mk_layer(r, "rejected", "rej_1", NOW - 3600)
|
|
245
|
+
_mk_layer(r, "unresolved", "unr_1", NOW - 3600)
|
|
246
|
+
d = adm.r2_says_no(r, now=NOW)
|
|
247
|
+
ok(d["ok"] is True and d["layers"]["rejected"]["recent"] == 1
|
|
248
|
+
and d["layers"]["unresolved"]["recent"] == 1,
|
|
249
|
+
"B1 两层 3 天内各恰好 1 条 → 满足(门槛刚过侧)", d["detail"])
|
|
250
|
+
|
|
251
|
+
r = _root("r2_short_no")
|
|
252
|
+
_mk_layer(r, "rejected", "rej_1", NOW - 4 * DAY)
|
|
253
|
+
_mk_layer(r, "unresolved", "unr_1", NOW - 3600)
|
|
254
|
+
d = adm.r2_says_no(r, now=NOW)
|
|
255
|
+
ok(d["ok"] is False and d["short_missing"] == ["rejected"]
|
|
256
|
+
and d["layers"]["rejected"]["total_30d"] == 1,
|
|
257
|
+
"B2 短期刚不过:rejected 最近一条在 3 天外(30 天内)→ 不满足;"
|
|
258
|
+
"长期读数照记(=1,长期判据的独立判别面)", d["detail"])
|
|
259
|
+
|
|
260
|
+
r = _root("r2_border")
|
|
261
|
+
_mk_layer(r, "rejected", "rej_in", NOW - 3 * DAY + 60)
|
|
262
|
+
_mk_layer(r, "rejected", "rej_out", NOW - 3 * DAY - 60)
|
|
263
|
+
_mk_layer(r, "unresolved", "unr_1", NOW - 3600)
|
|
264
|
+
d = adm.r2_says_no(r, now=NOW)
|
|
265
|
+
ok(d["layers"]["rejected"]["recent"] == 1
|
|
266
|
+
and d["layers"]["rejected"]["total_30d"] == 2 and d["ok"] is True,
|
|
267
|
+
"B3 3 天窗口边界 ±60s:窗外一条不计入短期、窗内一条计入(30 天总量仍 2)",
|
|
268
|
+
d["layers"]["rejected"])
|
|
269
|
+
|
|
270
|
+
r = _root("r2_border_no")
|
|
271
|
+
_mk_layer(r, "rejected", "rej_out", NOW - 3 * DAY - 60)
|
|
272
|
+
_mk_layer(r, "unresolved", "unr_1", NOW - 3600)
|
|
273
|
+
d = adm.r2_says_no(r, now=NOW)
|
|
274
|
+
ok(d["ok"] is False and d["layers"]["rejected"]["recent"] == 0,
|
|
275
|
+
"B4 3 天窗口边界刚不过:唯一 rejected 在窗外(60s)→ 短期 0 条不满足",
|
|
276
|
+
d["layers"]["rejected"])
|
|
277
|
+
|
|
278
|
+
r = _root("r2_undated")
|
|
279
|
+
_mk_layer(r, "rejected", "rej_nodate", None)
|
|
280
|
+
_mk_layer(r, "unresolved", "unr_1", NOW - 3600)
|
|
281
|
+
d = adm.r2_says_no(r, now=NOW)
|
|
282
|
+
ok(d["ok"] is False and d["layers"]["rejected"]["undated"] == 1
|
|
283
|
+
and d["layers"]["rejected"]["recent"] == 0,
|
|
284
|
+
"B5 无 created_at 的节点不计窗(如实计入 undated;不猜文件 mtime)",
|
|
285
|
+
d["layers"]["rejected"])
|
|
286
|
+
|
|
287
|
+
r = _root("r2_long_old")
|
|
288
|
+
_mk_layer(r, "rejected", "rej_old", NOW - 40 * DAY)
|
|
289
|
+
_mk_layer(r, "unresolved", "unr_1", NOW - 3600)
|
|
290
|
+
d = adm.r2_says_no(r, now=NOW)
|
|
291
|
+
ok(d["ok"] is False and d["layers"]["rejected"]["total_30d"] == 0,
|
|
292
|
+
"B6 40 天前的节点不计入 30 天总量(长期窗边界外)",
|
|
293
|
+
d["layers"]["rejected"])
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# =============================================================== C 组:R3
|
|
297
|
+
def g_c():
|
|
298
|
+
print("== C 组:R3 破坏可逆且「回过」(D 面/C 面真跑 · 30 天窗口边界)==")
|
|
299
|
+
# C1 D 面正向:真跑 forget→restore(1 次真实回滚 = 门槛刚过)
|
|
300
|
+
r = _root("r3_d_ok")
|
|
301
|
+
cg = _mk_cg(r)
|
|
302
|
+
cg.add("d_1", BODY % "D1", layer="knowledge", verification_basis="test")
|
|
303
|
+
cg.flush()
|
|
304
|
+
cg.forget("d_1", reason="探针删除")
|
|
305
|
+
cg.restore("d_1", force=True)
|
|
306
|
+
cg.flush()
|
|
307
|
+
res = cg.close()
|
|
308
|
+
# 真跑的时间戳 = 执行时刻(略晚于模块加载时的 NOW)——窗口「现在」取
|
|
309
|
+
# NOW+3600(未来一点),使真跑记录确定落在 30 天窗内。
|
|
310
|
+
d = adm.r3_reversible(r, now=NOW + 3600)
|
|
311
|
+
ok(d["ok"] is True and d["d_restores"] >= 1 and d["count"] >= 1,
|
|
312
|
+
"C1 D 面:forget→restore 实测 ⇒ 30 天内 ≥1 次真实回滚(门槛刚过侧)",
|
|
313
|
+
d["detail"])
|
|
314
|
+
|
|
315
|
+
# C2 D 面刚不过:只删不回
|
|
316
|
+
r = _root("r3_d_no")
|
|
317
|
+
cg = _mk_cg(r)
|
|
318
|
+
cg.add("d_2", BODY % "D2", layer="knowledge", verification_basis="test")
|
|
319
|
+
cg.flush()
|
|
320
|
+
cg.forget("d_2", reason="探针删除")
|
|
321
|
+
cg.flush()
|
|
322
|
+
cg.close()
|
|
323
|
+
d = adm.r3_reversible(r, now=NOW)
|
|
324
|
+
ok(d["ok"] is False and d["d_restores"] == 0,
|
|
325
|
+
"C2 D 面刚不过:只删不回(无 restore)→ 0 次证据、不满足", d["detail"])
|
|
326
|
+
|
|
327
|
+
# C3 证据链成对:只有 restore、无更早 forget → 不计
|
|
328
|
+
r = _root("r3_restore_only")
|
|
329
|
+
_wr_jsonl(r, adm.DEVICE_AUDIT,
|
|
330
|
+
[{"t": NOW - 3600, "op": "restore", "id": "x_9",
|
|
331
|
+
"forced": True}])
|
|
332
|
+
d = adm.r3_reversible(r, now=NOW)
|
|
333
|
+
ok(d["ok"] is False and d["count"] == 0,
|
|
334
|
+
"C3 只有 restore 无更早 forget → 不计(「回过」须成对:删过才谈得上回)",
|
|
335
|
+
d["detail"])
|
|
336
|
+
|
|
337
|
+
# C4 C 面正向:出单 → accept 执行 → 回滚(盘面逐字节回到执行时点前像)
|
|
338
|
+
r = _root("r3_c_ok")
|
|
339
|
+
cg = _mk_cg(r)
|
|
340
|
+
cg.add("c_1", BODY % "C1", layer="knowledge", verification_basis="test")
|
|
341
|
+
cg.flush()
|
|
342
|
+
pay = am.mutation_payload(am.C_REWRITE, "c_1", after=BODY % "C1b",
|
|
343
|
+
reason="探针改写")
|
|
344
|
+
pid = am.propose_mutation(cg, am.C_REWRITE, "c_1", payload=pay)
|
|
345
|
+
cg.flush()
|
|
346
|
+
res = cg.review_decide(pid, "accept")
|
|
347
|
+
recs = [x for x in cg.decisions()
|
|
348
|
+
if x.get("pid") == pid and x.get("decision") == "accept"]
|
|
349
|
+
rbout = rb.rollback_mutation(cg, recs[-1]["mutation"], reason="探针回滚")
|
|
350
|
+
cg.flush()
|
|
351
|
+
cg.close()
|
|
352
|
+
d = adm.r3_reversible(r, now=NOW + 3600)
|
|
353
|
+
ok(res.get("ok") is True and rbout.get("ok") is True
|
|
354
|
+
and rbout.get("bytes_equal_preimage") is True
|
|
355
|
+
and d["ok"] is True and d["cb_pairs"] >= 1,
|
|
356
|
+
"C4 C 面:accept 执行后回滚(逐字节==执行时点前像)⇒ 证据成立、满足",
|
|
357
|
+
d["detail"])
|
|
358
|
+
|
|
359
|
+
# C5 C 面刚不过:执行不回滚(盘面 ≠ 前像)
|
|
360
|
+
r = _root("r3_c_no")
|
|
361
|
+
cg = _mk_cg(r)
|
|
362
|
+
cg.add("c_2", BODY % "C2", layer="knowledge", verification_basis="test")
|
|
363
|
+
cg.flush()
|
|
364
|
+
pay = am.mutation_payload(am.C_REWRITE, "c_2", after=BODY % "C2b",
|
|
365
|
+
reason="探针改写")
|
|
366
|
+
pid = am.propose_mutation(cg, am.C_REWRITE, "c_2", payload=pay)
|
|
367
|
+
cg.flush()
|
|
368
|
+
cg.review_decide(pid, "accept")
|
|
369
|
+
cg.flush()
|
|
370
|
+
cg.close()
|
|
371
|
+
d = adm.r3_reversible(r, now=NOW + 3600)
|
|
372
|
+
ok(d["ok"] is False and d["cb_pairs"] == 0 and d["candidates"] >= 1,
|
|
373
|
+
"C5 C 面刚不过:执行后未回滚(盘面≠前像)→ 0 次证据、不满足", d["detail"])
|
|
374
|
+
|
|
375
|
+
# C6 30 天窗口边界:窗外 restore 不计、窗内成对计入
|
|
376
|
+
r = _root("r3_border")
|
|
377
|
+
_wr_jsonl(r, adm.DEVICE_AUDIT, [
|
|
378
|
+
{"t": NOW - 31 * DAY, "op": "forget", "id": "old_1"},
|
|
379
|
+
{"t": NOW - 30 * DAY - 60, "op": "restore",
|
|
380
|
+
"id": "old_1", "forced": True}, # 窗外
|
|
381
|
+
{"t": NOW - 7200, "op": "forget", "id": "new_1"},
|
|
382
|
+
{"t": NOW - 3600, "op": "restore", "id": "new_1",
|
|
383
|
+
"forced": True}]) # 窗内
|
|
384
|
+
d = adm.r3_reversible(r, now=NOW)
|
|
385
|
+
ok(d["count"] == 1 and d["evidence"][0]["node_id"] == "new_1",
|
|
386
|
+
"C6 30 天窗口边界 ±60s:窗外 restore 不计、窗内成对计入(恰 1 条)",
|
|
387
|
+
d["evidence"])
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# =============================================================== D 组:R4
|
|
391
|
+
def g_d():
|
|
392
|
+
print("== D 组:R4 判断收敛(3×10 天子窗 · 门槛刚过/刚不过/不可判)==")
|
|
393
|
+
r = _root("r4_ok")
|
|
394
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, _rows_r4([(1, 9), (3, 7), (4, 6)]))
|
|
395
|
+
d = adm.r4_converging(r, now=NOW)
|
|
396
|
+
ok(d["ok"] is True and d["rates_asc"][0] > d["rates_asc"][1] > d["rates_asc"][2]
|
|
397
|
+
and d["rates_asc"][2] == 0.10,
|
|
398
|
+
"D1 0.40/0.30/0.10 严格下降且最近 <20%% → 满足(门槛刚过侧)", d["detail"])
|
|
399
|
+
|
|
400
|
+
r = _root("r4_edge20")
|
|
401
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, _rows_r4([(2, 8), (4, 6), (5, 5)]))
|
|
402
|
+
d = adm.r4_converging(r, now=NOW)
|
|
403
|
+
ok(d["ok"] is False and d["rates_asc"][2] == 0.20,
|
|
404
|
+
"D2 最近子窗恰 20%% → 不满足(阈值严格小于,刚不过)", d["detail"])
|
|
405
|
+
|
|
406
|
+
r = _root("r4_flat")
|
|
407
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, _rows_r4([(1, 9), (1, 9), (3, 7)]))
|
|
408
|
+
d = adm.r4_converging(r, now=NOW)
|
|
409
|
+
ok(d["ok"] is False and d["rates_asc"][1] == d["rates_asc"][2] == 0.10,
|
|
410
|
+
"D3 子窗持平(0.30/0.10/0.10,最近 <20% 但不严格递减)→ 不满足"
|
|
411
|
+
"(「逐窗下降」= 严格递减,平坦不算下降)", d["detail"])
|
|
412
|
+
|
|
413
|
+
r = _root("r4_indet")
|
|
414
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, _rows_r4([(0, 0), (3, 7), (4, 6)]))
|
|
415
|
+
d = adm.r4_converging(r, now=NOW)
|
|
416
|
+
ok(d["ok"] is None,
|
|
417
|
+
"D4 最近子窗无裁定样本 → 不可判(ok=None,如实;不假装收敛/不收敛)",
|
|
418
|
+
d["detail"])
|
|
419
|
+
|
|
420
|
+
r = _root("r4_border")
|
|
421
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, [
|
|
422
|
+
{"t": NOW - 10 * DAY - 60, "decision": "reject"}, # W2
|
|
423
|
+
{"t": NOW - 10 * DAY, "decision": "accept"}, # W2 边界
|
|
424
|
+
{"t": NOW - 10 * DAY + 60, "decision": "reject"}, # W1
|
|
425
|
+
])
|
|
426
|
+
d = adm.r4_converging(r, now=NOW)
|
|
427
|
+
ok(d["windows"][1]["total"] == 2 and d["windows"][0]["total"] == 1,
|
|
428
|
+
"D5 10 天子窗归属边界:恰在边界线上的记录归远窗(lo< t ≤ hi),"
|
|
429
|
+
"±60s 分属两窗", {"W1": d["windows"][0], "W2": d["windows"][1]})
|
|
430
|
+
|
|
431
|
+
r = _root("r4_edit")
|
|
432
|
+
rows = _rows_r4([(0, 9), (3, 7), (4, 6)])
|
|
433
|
+
rows.append({"t": NOW - 5 * DAY, "decision": "edit"})
|
|
434
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, rows)
|
|
435
|
+
d = adm.r4_converging(r, now=NOW)
|
|
436
|
+
ok(d["ok"] is True and abs(d["rates_asc"][2] - 0.10) < 1e-9,
|
|
437
|
+
"D6 edit 与 reject 同权计入驳回率(最近窗 1 edit/10 = 10%)",
|
|
438
|
+
d["rates_asc"])
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# =============================================================== E 组:准入闸
|
|
442
|
+
def g_e():
|
|
443
|
+
print("== E 组:准入闸(settle/mode:full 走读数、plan/confirm 不受影响)==")
|
|
444
|
+
try:
|
|
445
|
+
# E1 full + 读数满足 ⇒ 实际生效 full(与改动前逐位一致)
|
|
446
|
+
sat = _satisfying("adm_sat")
|
|
447
|
+
chk = adm.check(sat, now=NOW)
|
|
448
|
+
with _mode("full"):
|
|
449
|
+
am.reset_settlement()
|
|
450
|
+
st = am.settle(root=sat, force=True)
|
|
451
|
+
ok(chk["ok"] is True and st["effective"] == "full"
|
|
452
|
+
and st["settled"] is True and st["alerts"] == []
|
|
453
|
+
and am.mode() == "full",
|
|
454
|
+
"E1 full+读数满足 ⇒ 生效档位 full、零告警(与改动前逐位一致)",
|
|
455
|
+
{"chk_ok": chk["ok"], "eff": st["effective"],
|
|
456
|
+
"alerts": st["alerts"][:2]})
|
|
457
|
+
|
|
458
|
+
# E2 full + 读数不满足 ⇒ 回落 confirm + alerts 报缺哪条(不静默)
|
|
459
|
+
empty = _root("adm_empty")
|
|
460
|
+
am.reset_settlement()
|
|
461
|
+
with _mode("full"):
|
|
462
|
+
st = am.settle(root=empty, force=True)
|
|
463
|
+
missing = st["admission"]["missing"]
|
|
464
|
+
ok(st["effective"] == "confirm" and st["settled"] is True
|
|
465
|
+
and st["configured"] == "full",
|
|
466
|
+
"E2 full+读数不满足 ⇒ 实际生效档位回落 confirm(配置仍记 full)",
|
|
467
|
+
{"eff": st["effective"], "missing": missing})
|
|
468
|
+
ok([a["reading"] for a in st["alerts"]] == missing and missing,
|
|
469
|
+
"E2a 告警可观测:alerts 逐条报「缺哪条读数」(与读数 missing 逐一相同,"
|
|
470
|
+
"不静默降级)", st["alerts"])
|
|
471
|
+
ok(am.mode() == "confirm",
|
|
472
|
+
"E2b 生效面贯通:回落后 mode() 返回 confirm(同一入口全出口一致)",
|
|
473
|
+
am.mode())
|
|
474
|
+
ok(all(a.get("detail") for a in st["alerts"]),
|
|
475
|
+
"E2c alerts 每条带可读 detail(报缺口而非只报编号)",
|
|
476
|
+
st["alerts"][:1])
|
|
477
|
+
ok(am.admission_state()["alerts"] == st["alerts"]
|
|
478
|
+
and am.admission_state()["effective"] == "confirm",
|
|
479
|
+
"E2d 结算态可查:admission_state() 返回同一结算体(含 alerts)")
|
|
480
|
+
|
|
481
|
+
# E3 confirm 不受读数影响(零 IO、零告警)
|
|
482
|
+
am.reset_settlement()
|
|
483
|
+
before = adm.cache_stats()["checks"]
|
|
484
|
+
with _mode("confirm"):
|
|
485
|
+
st = am.settle(root=empty, force=True)
|
|
486
|
+
ok(st["effective"] == "confirm" and st["admission"] is None
|
|
487
|
+
and st["alerts"] == [] and st["settled"] is False
|
|
488
|
+
and am.mode() == "confirm",
|
|
489
|
+
"E3 confirm 档不受读数影响(admission=None、零告警、零读数扫描)",
|
|
490
|
+
st)
|
|
491
|
+
ok(adm.cache_stats()["checks"] == before,
|
|
492
|
+
"E3a confirm 结算零 IO(读数扫描计数不变)",
|
|
493
|
+
adm.cache_stats())
|
|
494
|
+
|
|
495
|
+
# E4 plan 不受读数影响
|
|
496
|
+
am.reset_settlement()
|
|
497
|
+
with _mode("plan"):
|
|
498
|
+
st = am.settle(root=empty, force=True)
|
|
499
|
+
ok(st["effective"] == "plan" and st["admission"] is None
|
|
500
|
+
and st["alerts"] == [] and am.mode() == "plan",
|
|
501
|
+
"E4 plan 档不受读数影响(零告警、零读数扫描)", st)
|
|
502
|
+
|
|
503
|
+
# E5 非法 env 仍 fail-closed
|
|
504
|
+
with _mode("bogus-garbage"):
|
|
505
|
+
try:
|
|
506
|
+
am.settle(root=empty, force=True)
|
|
507
|
+
ok(False, "E5 非法 env settle 未报错")
|
|
508
|
+
except am.AutonomyModeError as e:
|
|
509
|
+
ok("bogus-garbage" in str(e) and "full" in str(e),
|
|
510
|
+
"E5 非法 env fail-closed:settle 抛 AutonomyModeError"
|
|
511
|
+
"(hint 列合法值)", str(e)[:50])
|
|
512
|
+
|
|
513
|
+
# E6 未结算 = 旧行为(对拍)
|
|
514
|
+
am.reset_settlement()
|
|
515
|
+
with _mode("full"):
|
|
516
|
+
ok(am.mode() == "full" and am.admission_state()["settled"] is False,
|
|
517
|
+
"E6 未结算时 mode() 返回配置档位(生效面未启用 = 改动前逐位一致)",
|
|
518
|
+
am.mode())
|
|
519
|
+
|
|
520
|
+
# E7 缓存频度:TTL 内不重扫(「不得让每次 decide 都全库扫描」)
|
|
521
|
+
am.reset_settlement()
|
|
522
|
+
adm.reset_cache()
|
|
523
|
+
with _mode("full"):
|
|
524
|
+
am.settle(root=empty, force=True, ttl=60)
|
|
525
|
+
am.settle(root=empty, ttl=60)
|
|
526
|
+
am.settle(root=empty, ttl=60)
|
|
527
|
+
c1 = adm.cache_stats()["checks"]
|
|
528
|
+
# 确定过期(ttl=-1;不用 0——Windows time.time() 刻度 ~15.6ms,
|
|
529
|
+
# 连续调用可能同刻度,差为 0 时 0<=0 仍命中缓存,判据会含糊)
|
|
530
|
+
am.settle(root=empty, ttl=-1)
|
|
531
|
+
c2 = adm.cache_stats()["checks"]
|
|
532
|
+
ok(c1 == 1 and c2 == 2,
|
|
533
|
+
"E7 读数缓存:TTL=60s 内三次结算只扫一次;确定过期(ttl=-1)才重扫"
|
|
534
|
+
"(热路径 mode()/decide() 零 IO)", {"checks": c1, "after_exp": c2})
|
|
535
|
+
|
|
536
|
+
# E8 decide 端到端:回落在判定链上可见
|
|
537
|
+
am.reset_settlement()
|
|
538
|
+
with _mode("full"):
|
|
539
|
+
am.settle(root=empty, force=True)
|
|
540
|
+
dec = am.decide("D")
|
|
541
|
+
ok(dec["decision"] == "confirm" and dec["mode"] == "confirm",
|
|
542
|
+
"E8 full+不满足:decide(D) 判 confirm(回落在判定链上生效)", dec)
|
|
543
|
+
am.reset_settlement()
|
|
544
|
+
with _mode("full"):
|
|
545
|
+
am.settle(root=sat, force=True)
|
|
546
|
+
dec = am.decide("D")
|
|
547
|
+
ok(dec["decision"] == "allow" and dec["mode"] == "full",
|
|
548
|
+
"E8a full+满足:decide(D) 判 allow(与改动前一致)", dec)
|
|
549
|
+
|
|
550
|
+
# E9 只读:结算前后库指纹不变(「只告警不改数据」)
|
|
551
|
+
fp0 = adm._fingerprint(sat)
|
|
552
|
+
am.reset_settlement()
|
|
553
|
+
with _mode("full"):
|
|
554
|
+
am.settle(root=sat, force=True)
|
|
555
|
+
fp1 = adm._fingerprint(sat)
|
|
556
|
+
ok(fp0 == fp1 and len(fp0) > 0,
|
|
557
|
+
"E9 只告警不改数据:settle(含读数扫描)前后库指纹逐位相同", len(fp0))
|
|
558
|
+
finally:
|
|
559
|
+
am.reset_settlement()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# =============================================================== F 组:结构
|
|
563
|
+
def g_f():
|
|
564
|
+
print("== F 组:结构单点与只读(常量同源 / 不触 default_root / CLI)==")
|
|
565
|
+
ok(adm.R1_MAX_SHARE == 0.90 and adm.R4_MAX_RATE == 0.20
|
|
566
|
+
and adm.WINDOW_SHORT_D == 3 and adm.WINDOW_LONG_D == 30
|
|
567
|
+
and adm.R4_SUBWINDOW_D == 10 and adm.R4_SUBWINDOWS == 3
|
|
568
|
+
and adm.R3_MIN_ROLLBACKS == 1 and adm.R2_MIN_RECENT == 1
|
|
569
|
+
and adm.R2_MIN_TOTAL == 1,
|
|
570
|
+
"F0 门槛/窗口常量与设计 §七 表同值(0.90 / 0.20 / 3 天 / 30 天 / "
|
|
571
|
+
"3×10 天 / 各 1 次——守卫钉规格的字面量面)",
|
|
572
|
+
{"share": adm.R1_MAX_SHARE, "rate": adm.R4_MAX_RATE,
|
|
573
|
+
"short_d": adm.WINDOW_SHORT_D, "long_d": adm.WINDOW_LONG_D})
|
|
574
|
+
ok(adm.FORGETTING_LOG == forgetting.LOG_FILE
|
|
575
|
+
and adm.PROTECT_AUDIT == protect.AUDIT_FILE
|
|
576
|
+
and adm.HISTORY_DIR == protect.HISTORY_DIR
|
|
577
|
+
and adm.DECISIONS_LOG == os.path.join(mdcos.MdCGOS.HIPPOCAMPUS,
|
|
578
|
+
"decisions.jsonl")
|
|
579
|
+
and adm.AUDIT_ARCHIVE == mdcos.MdCGOS.AUDIT_ARCHIVE,
|
|
580
|
+
"F1 数据面常量与真源同值(forgetting/protect/mdcos 单点)",
|
|
581
|
+
{"adm": adm.FORGETTING_LOG, "src": forgetting.LOG_FILE})
|
|
582
|
+
|
|
583
|
+
r = _root("f_no_default")
|
|
584
|
+
live = adm.default_root
|
|
585
|
+
adm.default_root = lambda: (_ for _ in ()).throw(
|
|
586
|
+
AssertionError("显式 root 路径不应触发 default_root()"))
|
|
587
|
+
try:
|
|
588
|
+
d1 = adm.check(r, now=NOW)
|
|
589
|
+
d2 = adm.gate(r, now=NOW, force=True)
|
|
590
|
+
am.reset_settlement()
|
|
591
|
+
with _mode("full"):
|
|
592
|
+
st = am.settle(root=r, force=True)
|
|
593
|
+
ok(d1["ok"] is False and d2["ok"] is False
|
|
594
|
+
and st["effective"] == "confirm",
|
|
595
|
+
"F2 显式 root 的读数/结算不触 default_root(沙箱隔离:不碰在役根)",
|
|
596
|
+
None)
|
|
597
|
+
finally:
|
|
598
|
+
adm.default_root = live
|
|
599
|
+
am.reset_settlement()
|
|
600
|
+
|
|
601
|
+
empty = _root("f_cli_no")
|
|
602
|
+
r = _root("f_cli_ok")
|
|
603
|
+
_wr_jsonl(r, adm.FORGETTING_LOG, [
|
|
604
|
+
{"t": NOW - 3600, "verdict": "DROP"},
|
|
605
|
+
{"t": NOW - 3600, "verdict": "MERGE"},
|
|
606
|
+
{"t": NOW - 3600, "verdict": "DEFER"}])
|
|
607
|
+
_mk_layer(r, "rejected", "rej_1", NOW - 3600)
|
|
608
|
+
_mk_layer(r, "unresolved", "unr_1", NOW - 3600)
|
|
609
|
+
_wr_jsonl(r, adm.DEVICE_AUDIT, [
|
|
610
|
+
{"t": NOW - 7200, "op": "forget", "id": "z_1"},
|
|
611
|
+
{"t": NOW - 3600, "op": "restore", "id": "z_1", "forced": True}])
|
|
612
|
+
_wr_jsonl(r, adm.DECISIONS_LOG, _rows_r4([(1, 9), (3, 7), (4, 6)]))
|
|
613
|
+
repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
614
|
+
json_out = os.path.join(_SANDBOX, "cli_out_r%d.json" % _RUN[0])
|
|
615
|
+
|
|
616
|
+
def _cli(root):
|
|
617
|
+
p = subprocess.run(
|
|
618
|
+
[sys.executable, "-X", "utf8", "-m", "md_cg.admission",
|
|
619
|
+
"--root", root, "--json", json_out],
|
|
620
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
621
|
+
cwd=repo, env=dict(os.environ, PYTHONUTF8="1"))
|
|
622
|
+
return p
|
|
623
|
+
|
|
624
|
+
p_no = _cli(empty)
|
|
625
|
+
p_ok = _cli(r)
|
|
626
|
+
ok(p_no.returncode == 1 and "不够格" in (p_no.stdout or ""),
|
|
627
|
+
"F3 CLI:不满足库 rc=1 且打印「不够格」(全入口可答「它今天够格吗」)",
|
|
628
|
+
(p_no.stdout or "")[:80])
|
|
629
|
+
ok(p_ok.returncode == 0 and "够格" in (p_ok.stdout or ""),
|
|
630
|
+
"F4 CLI:满足库 rc=0 且打印「够格」", (p_ok.stdout or "")[:80])
|
|
631
|
+
try:
|
|
632
|
+
with open(json_out, encoding="utf-8") as f:
|
|
633
|
+
rep = json.load(f)
|
|
634
|
+
ok(len(rep.get("readings") or []) == 4 and rep.get("ok") is True,
|
|
635
|
+
"F4a CLI --json 落盘完整读数体(四条读数齐备)",
|
|
636
|
+
list(rep.keys())[:8])
|
|
637
|
+
except (OSError, ValueError) as e:
|
|
638
|
+
ok(False, "F4a CLI --json 输出不可读:%r" % e)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# =============================================================== G 组:补强批次
|
|
642
|
+
def g_g():
|
|
643
|
+
"""补强批次(v1.1):数值容错(不可表示 ⇒ 缺时间戳)+ 结算异常 fail-closed。
|
|
644
|
+
|
|
645
|
+
语义出处:`admission.py` 模块 docstring「补强批次」段(口径与 `_stream_jsonl`
|
|
646
|
+
「坏行跳过不抛」一致)+ `autonomy_modes.settle` docstring(失败态两格:
|
|
647
|
+
从未结算 ⇒ 回落 confirm;已结算 ⇒ 不升不降)。跑完必 reset_settlement。
|
|
648
|
+
"""
|
|
649
|
+
print("== G 组:补强批次(溢出/坏值容错 · settle 异常 fail-closed)==")
|
|
650
|
+
BIG = int("9" * 400) # 400 位十进制整数:float() 溢出(复核 uncovered 原形态)
|
|
651
|
+
|
|
652
|
+
def _safe(fn):
|
|
653
|
+
"""可崩调用包装:返回 (结果, 异常串)——异常由断言判红,不让整组中断。"""
|
|
654
|
+
try:
|
|
655
|
+
return fn(), None
|
|
656
|
+
except Exception as e: # noqa: BLE001
|
|
657
|
+
return None, "%s: %s" % (type(e).__name__, e)
|
|
658
|
+
|
|
659
|
+
# G1 逐型读数:不可表示值一律 None(不抛);合法值逐位不变(独立 oracle)
|
|
660
|
+
ts_bad = []
|
|
661
|
+
for name, v, want in (
|
|
662
|
+
("400 位整数", BIG, None),
|
|
663
|
+
("1e400(=inf)", float("1e400"), None),
|
|
664
|
+
("-inf", float("-inf"), None),
|
|
665
|
+
("NaN", float("nan"), None),
|
|
666
|
+
("字符串数字", "1700000000", None),
|
|
667
|
+
("负数", -5, None),
|
|
668
|
+
("None", None, None),
|
|
669
|
+
("合法浮点", 1700000000.5, 1700000000.5),
|
|
670
|
+
("合法整数", 1700000000, 1700000000.0),
|
|
671
|
+
("10**300(可表示)", 10 ** 300, 1e300)):
|
|
672
|
+
got, err = _safe(lambda v=v: adm._ts({"t": v}))
|
|
673
|
+
if err is not None or got != want:
|
|
674
|
+
ts_bad.append("%s: %r(want %r)" % (name, err or got, want))
|
|
675
|
+
ok(not ts_bad,
|
|
676
|
+
"G1 `_ts` 逐型:400 位整数/±inf/NaN 视同缺时间戳(None,不抛);"
|
|
677
|
+
"字符串数字/负数/None 同;合法浮点/整数/可表示大数逐位不变", ts_bad)
|
|
678
|
+
|
|
679
|
+
fm_bad = []
|
|
680
|
+
for name, v, want in (
|
|
681
|
+
("400 位整数", BIG, None),
|
|
682
|
+
("1e400(=inf)", float("1e400"), None),
|
|
683
|
+
("NaN", float("nan"), None),
|
|
684
|
+
("字符串数字", "1700000000", None),
|
|
685
|
+
("负数", -5, None),
|
|
686
|
+
("None", None, None),
|
|
687
|
+
("合法浮点", 1700000000.5, 1700000000.5)):
|
|
688
|
+
got, err = _safe(lambda v=v: adm._fm_ts({"created_at": v}))
|
|
689
|
+
if err is not None or got != want:
|
|
690
|
+
fm_bad.append("%s: %r(want %r)" % (name, err or got, want))
|
|
691
|
+
ok(not fm_bad,
|
|
692
|
+
"G1a `_fm_ts` 逐型:同一数值闸(不可表示 ⇒ None 不抛;合法值逐位不变)",
|
|
693
|
+
fm_bad)
|
|
694
|
+
|
|
695
|
+
# G2 四条读数 + check:含溢出/±inf 行的数据面不崩、undated 计数正确
|
|
696
|
+
r = _root("g_ovf")
|
|
697
|
+
_wr_jsonl(r, adm.FORGETTING_LOG, [
|
|
698
|
+
{"t": BIG, "verdict": "DROP"}, # 不可表示 ⇒ 视同缺时间戳
|
|
699
|
+
{"t": float("inf"), "verdict": "MERGE"}, # ±inf 同归缺
|
|
700
|
+
{"t": NOW - 3600, "verdict": "DEFER"}]) # 合法一条照常计窗
|
|
701
|
+
d1, e1 = _safe(lambda: adm.r1_non_degenerate(r, now=NOW))
|
|
702
|
+
ok(e1 is None and d1 is not None,
|
|
703
|
+
"G2 R1 对含溢出/±inf 行的数据面不崩(补强前:OverflowError 直接抛出)", e1)
|
|
704
|
+
ok(e1 is None and d1 is not None and d1["undated"] == 2 and d1["total"] == 1,
|
|
705
|
+
"G2a R1 undated 计数正确:两条坏行视同缺时间戳(undated=2)、仅合法一条计窗",
|
|
706
|
+
{"undated": (d1 or {}).get("undated"), "total": (d1 or {}).get("total")})
|
|
707
|
+
|
|
708
|
+
r2 = _root("g_ovf_r2")
|
|
709
|
+
_mk_layer(r2, "rejected", "rej_big", BIG) # frontmatter 大整数(nodefile 往返为 int)
|
|
710
|
+
_mk_layer(r2, "rejected", "rej_ok", NOW - 3600)
|
|
711
|
+
_mk_layer(r2, "unresolved", "unr_ok", NOW - 3600)
|
|
712
|
+
d2, e2 = _safe(lambda: adm.r2_says_no(r2, now=NOW))
|
|
713
|
+
ok(e2 is None and d2 is not None,
|
|
714
|
+
"G2b R2 对 created_at 为大整数的节点不崩(同一溢出点 `_fm_ts`)", e2)
|
|
715
|
+
ok(e2 is None and d2 is not None
|
|
716
|
+
and d2["layers"]["rejected"]["undated"] == 1
|
|
717
|
+
and d2["layers"]["rejected"]["recent"] == 1,
|
|
718
|
+
"G2c R2 undated 正确:大整数 created_at 计入 undated、合法一条照常计窗",
|
|
719
|
+
(d2 or {}).get("layers", {}).get("rejected"))
|
|
720
|
+
|
|
721
|
+
r3 = _root("g_ovf_r3")
|
|
722
|
+
_wr_jsonl(r3, adm.DEVICE_AUDIT, [
|
|
723
|
+
{"t": NOW - 7200, "op": "forget", "id": "g_1"}, # 合法删行
|
|
724
|
+
{"t": BIG, "op": "restore", "id": "g_1",
|
|
725
|
+
"forced": True}]) # 坏行 ⇒ 缺时间戳
|
|
726
|
+
d3, e3 = _safe(lambda: adm.r3_reversible(r3, now=NOW))
|
|
727
|
+
ok(e3 is None and d3 is not None and d3["ok"] is False and d3["count"] == 0,
|
|
728
|
+
"G2d R3 对含溢出 t 的 _audit 行不崩:坏行视同缺时间戳 ⇒ 不成对、不计证据",
|
|
729
|
+
e3)
|
|
730
|
+
|
|
731
|
+
r4 = _root("g_ovf_r4")
|
|
732
|
+
_wr_jsonl(r4, adm.DECISIONS_LOG,
|
|
733
|
+
_rows_r4([(1, 9), (3, 7), (4, 6)]) + [{"t": BIG, "decision": "accept"}])
|
|
734
|
+
d4, e4 = _safe(lambda: adm.r4_converging(r4, now=NOW))
|
|
735
|
+
ok(e4 is None and d4 is not None and d4["ok"] is True,
|
|
736
|
+
"G2e R4 对含溢出 t 的裁定行不崩:坏行不计窗(三子窗样本与收敛判定不受扰)",
|
|
737
|
+
e4)
|
|
738
|
+
|
|
739
|
+
dc, ec = _safe(lambda: adm.check(r, now=NOW))
|
|
740
|
+
ok(ec is None and dc is not None and len(dc["readings"]) == 4
|
|
741
|
+
and "R1" in dc["missing"],
|
|
742
|
+
"G2f check() 对含溢出行的数据面不崩:四条读数齐备、缺口如实列出", ec)
|
|
743
|
+
|
|
744
|
+
# G3 settle 异常语义:失败态 + fail-closed(两格)+ 不静默 + 可区分
|
|
745
|
+
sat = _satisfying("g_sat")
|
|
746
|
+
|
|
747
|
+
def _boom(*_a, **_k):
|
|
748
|
+
raise RuntimeError("探针注入:读数不可得")
|
|
749
|
+
|
|
750
|
+
live_gate = adm.gate
|
|
751
|
+
try:
|
|
752
|
+
# G3 从未结算 + 异常 ⇒ 回落 confirm(收口 fail-open)
|
|
753
|
+
am.reset_settlement()
|
|
754
|
+
adm.gate = _boom
|
|
755
|
+
with _mode("full"):
|
|
756
|
+
st, e3 = _safe(lambda: am.settle(root=sat, force=True))
|
|
757
|
+
adm.gate = live_gate
|
|
758
|
+
ok(e3 is None and st is not None and st["effective"] == "confirm"
|
|
759
|
+
and st["configured"] == "full" and st["settled"] is True,
|
|
760
|
+
"G3 从未结算 + settle 异常 ⇒ 写失败态:effective 回落 confirm(非 full)"
|
|
761
|
+
"——收口「未结算 + 崩溃 ⇒ full 照常生效」的 fail-open",
|
|
762
|
+
{"eff": (st or {}).get("effective"), "err": e3})
|
|
763
|
+
ok(st is not None and bool(st.get("error")) and st["alerts"]
|
|
764
|
+
and st["alerts"][0].get("reading") == "settle_error"
|
|
765
|
+
and "读数不可得" in (st["alerts"][0].get("detail") or ""),
|
|
766
|
+
"G3a 失败态不静默:error 非空 + alerts 报「读数不可得」(不静默降级)",
|
|
767
|
+
{"error": (st or {}).get("error"), "alerts": (st or {}).get("alerts")})
|
|
768
|
+
with _mode("full"):
|
|
769
|
+
ok(am.mode() == "confirm",
|
|
770
|
+
"G3b 生效面贯通:异常回落后 mode()=confirm(full 不照常生效)",
|
|
771
|
+
am.mode())
|
|
772
|
+
st_a = am.admission_state()
|
|
773
|
+
ok(bool(st_a.get("error")) and st_a.get("configured") == "full"
|
|
774
|
+
and st_a.get("settled") is True,
|
|
775
|
+
"G3c 失败态与「未调用过 settle」可区分(configured=full + error 非空)",
|
|
776
|
+
{k: st_a.get(k) for k in ("configured", "settled", "error")})
|
|
777
|
+
|
|
778
|
+
# G3d 已成功结算(full+满足)后异常 ⇒ 不升不降(保持上次 effective)
|
|
779
|
+
am.reset_settlement()
|
|
780
|
+
with _mode("full"):
|
|
781
|
+
st0, e0 = _safe(lambda: am.settle(root=sat, force=True))
|
|
782
|
+
ok(e0 is None and st0 is not None and st0["effective"] == "full"
|
|
783
|
+
and st0["error"] is None,
|
|
784
|
+
"G3d0 前置:合法面成功结算 ⇒ full、零错误(合法数据面结算逐位不变)",
|
|
785
|
+
(st0 or {}).get("effective"))
|
|
786
|
+
adm.gate = _boom
|
|
787
|
+
with _mode("full"):
|
|
788
|
+
st1, e1b = _safe(lambda: am.settle(root=sat, force=True))
|
|
789
|
+
adm.gate = live_gate
|
|
790
|
+
ok(e1b is None and st1 is not None
|
|
791
|
+
and (st1 or {}).get("effective") == (st0 or {}).get("effective") == "full"
|
|
792
|
+
and bool((st1 or {}).get("error")),
|
|
793
|
+
"G3d 已成功结算后异常 ⇒ 不升不降:effective 保持上次成功结算值(full)"
|
|
794
|
+
"且失败态仍可观测(error 非空)",
|
|
795
|
+
{"eff0": (st0 or {}).get("effective"),
|
|
796
|
+
"eff1": (st1 or {}).get("effective"),
|
|
797
|
+
"error": (st1 or {}).get("error")})
|
|
798
|
+
with _mode("full"):
|
|
799
|
+
ok(am.mode() == "full" and bool(am.admission_state().get("error")),
|
|
800
|
+
"G3e 不升不降贯通:mode() 仍 full,失败态可查(无静默)",
|
|
801
|
+
{"mode": am.mode(), "error": am.admission_state().get("error")})
|
|
802
|
+
|
|
803
|
+
# G4 未调用过 settle 的进程:mode()=env(既有对拍锚,不得破)
|
|
804
|
+
am.reset_settlement()
|
|
805
|
+
with _mode("full"):
|
|
806
|
+
st_u = am.admission_state()
|
|
807
|
+
ok(am.mode() == "full" and st_u["configured"] is None
|
|
808
|
+
and st_u["settled"] is False,
|
|
809
|
+
"G4 未调用过 settle:mode()=env(full)逐位不变、结算态为空"
|
|
810
|
+
"(对拍锚,不得破)", am.mode())
|
|
811
|
+
|
|
812
|
+
# G5 失败态路径零写入(只告警不改数据)
|
|
813
|
+
fp0 = adm._fingerprint(sat)
|
|
814
|
+
am.reset_settlement()
|
|
815
|
+
adm.gate = _boom
|
|
816
|
+
with _mode("full"):
|
|
817
|
+
_safe(lambda: am.settle(root=sat, force=True))
|
|
818
|
+
adm.gate = live_gate
|
|
819
|
+
ok(adm._fingerprint(sat) == fp0 and len(fp0) > 0,
|
|
820
|
+
"G5 失败态路径只告警不改数据:结算异常前后库指纹逐位相同", len(fp0))
|
|
821
|
+
|
|
822
|
+
# G6 含溢出行的库拿真 gate 结算:容错生效 ⇒ 读数正常判不满足(回落
|
|
823
|
+
# confirm、非结算失败态);补强前该库 settle 直接崩(fail-open)
|
|
824
|
+
r6 = _root("g_ovf_settle")
|
|
825
|
+
_wr_jsonl(r6, adm.FORGETTING_LOG, [{"t": BIG, "verdict": "DROP"}])
|
|
826
|
+
am.reset_settlement()
|
|
827
|
+
with _mode("full"):
|
|
828
|
+
st6, e6 = _safe(lambda: am.settle(root=r6, force=True))
|
|
829
|
+
ok(e6 is None and st6 is not None and st6["effective"] == "confirm"
|
|
830
|
+
and st6.get("error") is None,
|
|
831
|
+
"G6 含溢出行的库 settle:容错生效(读数正常判不满足 ⇒ 回落 confirm、"
|
|
832
|
+
"非结算失败态;补强前直接崩)",
|
|
833
|
+
{"eff": (st6 or {}).get("effective"), "err": e6})
|
|
834
|
+
finally:
|
|
835
|
+
adm.gate = live_gate
|
|
836
|
+
am.reset_settlement()
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
_GROUPS = (g_a, g_b, g_c, g_d, g_e, g_f, g_g)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _run_groups() -> int:
|
|
843
|
+
"""跑全部断言组(静默),返回失败数——供变异自证复用。"""
|
|
844
|
+
_PASS.clear()
|
|
845
|
+
_FAIL.clear()
|
|
846
|
+
for g in _GROUPS:
|
|
847
|
+
g()
|
|
848
|
+
return len(_FAIL)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
# =============================================================== 定点变异自证
|
|
852
|
+
# 表内每项 = (说明, 目标, 锚点原文, 替换文, 预期红项数)。锚点须**逐字**出现在
|
|
853
|
+
# 目标函数源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。
|
|
854
|
+
# 目标 = ("mod", 模块对象, 函数名) 或 ("attr", 模块对象, 属性名)(常量型变异,
|
|
855
|
+
# 与 test_autonomy_modes.py 同口径)。
|
|
856
|
+
_SRC_MUTATIONS = (
|
|
857
|
+
# ① R1 占比阈值放宽——「恰 90% 不满足」的判据失去判别力。
|
|
858
|
+
# 红 2:A2(恰 90% 转满足)+ F0(常量与设计规格同值断言——阈值声明被改)。
|
|
859
|
+
("R1 占比阈值放宽(0.90→1.01)", "attr", adm, "R1_MAX_SHARE",
|
|
860
|
+
"R1_MAX_SHARE = 0.90", "1.01", 2),
|
|
861
|
+
# ② R1 三态判据摘除——「缺 DEFER」仍判满足。
|
|
862
|
+
("R1 三态判据摘除(missing_verdicts 恒空)", "mod", adm,
|
|
863
|
+
"r1_non_degenerate",
|
|
864
|
+
' missing_verdicts = [v for v, n in seen.items() if n < R1_MIN_ONE]',
|
|
865
|
+
' missing_verdicts = []', 1),
|
|
866
|
+
# ③ R1 窗口失效——窗外记录混入窗口统计。
|
|
867
|
+
("R1 30 天窗失效(窗外记录混入)", "mod", adm, "r1_non_degenerate",
|
|
868
|
+
' if 0 <= (now - t) <= WINDOW_LONG_D * DAY:',
|
|
869
|
+
' if True:', 1),
|
|
870
|
+
# ④ R2 组合判据只剩长期——「3 天外 30 天内」被误判满足。
|
|
871
|
+
# 红 2:B2 与 B4 都在钉「短期不足 ⇒ 不满足」(长期满足不能救)。
|
|
872
|
+
("R2 组合判据只剩长期(short_ok 摘除)", "mod", adm, "r2_says_no",
|
|
873
|
+
' ok = bool(short_ok and long_ok)', ' ok = bool(long_ok)', 2),
|
|
874
|
+
# ⑤ R2 短期窗口缩短——3 天窗边界的窗外记录混入。
|
|
875
|
+
# 红 2:B3(3 天边界用例:边界数据是字面量,不随常量漂移)+ F0。
|
|
876
|
+
("R2 短期窗口缩短(3 天→1 天)", "attr", adm, "WINDOW_SHORT_D",
|
|
877
|
+
"WINDOW_SHORT_D = 3", "1", 2),
|
|
878
|
+
# ⑥ R3 D 面「更早 forget」判据摘除——只有 restore 也被当「回过」。
|
|
879
|
+
("R3 D 面成对判据摘除(无更早 forget 也计入)", "mod", adm,
|
|
880
|
+
"r3_reversible",
|
|
881
|
+
' if ft is not None and ft < t and lo <= t <= now:',
|
|
882
|
+
' ft = ft if ft is not None else t\n'
|
|
883
|
+
' if lo <= t <= now:', 1),
|
|
884
|
+
# ⑦ R3 C/B 面「盘面==前像」判据摘除——执行未回滚也被当「回过」。
|
|
885
|
+
("R3 C/B 面盘面判据摘除(未回滚也计入)", "mod", adm, "r3_reversible",
|
|
886
|
+
' if a != b:\n continue',
|
|
887
|
+
' if False:\n continue', 1),
|
|
888
|
+
# ⑧ R3 30 天窗失效——窗外 restore 混入。
|
|
889
|
+
("R3 30 天窗失效(窗外 restore 混入)", "mod", adm, "r3_reversible",
|
|
890
|
+
' if ft is not None and ft < t and lo <= t <= now:',
|
|
891
|
+
' if ft is not None and ft < t:', 1),
|
|
892
|
+
# ⑨ R4 严格下降放宽为不增——持平被误判收敛。
|
|
893
|
+
("R4 严格下降放宽为不增(持平也判下降)", "mod", adm, "r4_converging",
|
|
894
|
+
' strictly_down = rates_asc[0] > rates_asc[1] > rates_asc[2]',
|
|
895
|
+
' strictly_down = rates_asc[0] >= rates_asc[1] >= rates_asc[2]', 1),
|
|
896
|
+
# ⑩ R4 最近窗阈值放宽——「恰 20%」被误判满足。
|
|
897
|
+
# 红 2:D2(恰 20% 转满足)+ F0(常量与设计规格同值断言)。
|
|
898
|
+
("R4 最近窗阈值放宽(0.20→1.01)", "attr", adm, "R4_MAX_RATE",
|
|
899
|
+
"R4_MAX_RATE = 0.20", "1.01", 2),
|
|
900
|
+
# ⑪ R4 不可判分支摘除——样本不足被当成「不收敛」(非如实)。
|
|
901
|
+
("R4 不可判分支摘除(样本不足判 False)", "mod", adm, "r4_converging",
|
|
902
|
+
' ok = None\n detail = ("30 天内子窗样本不足',
|
|
903
|
+
' ok = False\n detail = ("30 天内子窗样本不足', 1),
|
|
904
|
+
# ⑫ 准入闸恒放行——full+不满足仍报 full(放权失控形态)。
|
|
905
|
+
# 红 7(补强批次校准 6→7):E2/E2a/E2b/E2d(回落与告警整组失效)+ E8
|
|
906
|
+
# (decide 判 allow 而非 confirm)+ F2(显式 root 结算不再回落)+ G6
|
|
907
|
+
# (含溢出行的库在恒放行下报 full 而非 confirm)。E2c(alerts detail 非空)
|
|
908
|
+
# 在 alerts=[] 时空列表全真、语义相符不红。
|
|
909
|
+
("准入闸恒放行(settle 不查读数)", "mod", am, "settle",
|
|
910
|
+
' if r["ok"]:', ' if True:', 7),
|
|
911
|
+
# ⑬ 准入闸告警静默——回落但 alerts 为空(静默降级形态)。
|
|
912
|
+
("准入闸告警静默(alerts 恒空)", "mod", am, "settle",
|
|
913
|
+
' alerts = [{"reading": rd["id"], "ok": rd["ok"], "detail": rd["detail"]}\n'
|
|
914
|
+
' for rd in r["readings"] if rd["ok"] is not True]',
|
|
915
|
+
' alerts = []', 1),
|
|
916
|
+
# ⑭ 生效档位折算摘除——mode() 恒返回配置档位(回落不可见)。
|
|
917
|
+
# 红 3(补强批次校准 2→3):E2b + E8 + G3b(异常回落后 mode() 应 confirm,
|
|
918
|
+
# 折算摘除后恒返回配置档位 full)。
|
|
919
|
+
("生效档位折算摘除(mode 不读结算态)", "mod", am, "mode",
|
|
920
|
+
' if (m == MODE_FULL and environ is None\n'
|
|
921
|
+
' and _SETTLEMENT["configured"] == MODE_FULL\n'
|
|
922
|
+
' and _SETTLEMENT["effective"]):',
|
|
923
|
+
' if False:', 3),
|
|
924
|
+
# ⑮ 读数缓存失效——每次结算都全扫(「不得每次 decide 全库扫描」被破坏)。
|
|
925
|
+
("读数缓存失效(gate 每次重算)", "mod", adm, "gate",
|
|
926
|
+
' if ent is not None and not force and (t - ent["t"]) <= ttl:',
|
|
927
|
+
' if False:', 1),
|
|
928
|
+
# ⑯ 补强批次:数值容错摘除(`_as_ts` 不再兜 float(v) 的溢出/非有限值)
|
|
929
|
+
# ——含 400 位整数 t 的数据面回到「读数抛 OverflowError」的旧形态。
|
|
930
|
+
# 红 10(补强批次实测):G1/G1a(逐型)+ G2/G2a/G2b/G2c/G2d/G2e/G2f
|
|
931
|
+
# (四条读数与 check 不崩/undated)+ G6(溢出库 settle 转失败态)。
|
|
932
|
+
("数值容错摘除(_as_ts 不再兜溢出/非有限值)", "mod", adm, "_as_ts",
|
|
933
|
+
' try:\n'
|
|
934
|
+
' f = float(v)\n'
|
|
935
|
+
' except (OverflowError, ValueError, TypeError):\n'
|
|
936
|
+
' return None',
|
|
937
|
+
' f = float(v)', 10),
|
|
938
|
+
# ⑰ 补强批次:结算失败态放行为 full(异常当满足,fail-open 新形态)。
|
|
939
|
+
# 红 2(补强批次实测):G3(未结算格 effective 非 confirm)+ G3b
|
|
940
|
+
# (mode() 非 confirm)。
|
|
941
|
+
("结算失败态放行为 full(异常当满足)", "mod", am, "settle",
|
|
942
|
+
' eff = prev or MODE_CONFIRM', ' eff = MODE_FULL', 2),
|
|
943
|
+
# ⑱ 补强批次:结算失败态总是回落 confirm——「已成功结算 ⇒ 不升不降」
|
|
944
|
+
# 被破(已结算 full 的进程被无据降档)。
|
|
945
|
+
# 红 2(补强批次实测):G3d(effective 不再保持 full)+ G3e(mode() 不再 full)。
|
|
946
|
+
("结算失败态总是回落 confirm(不升不降被破)", "mod", am, "settle",
|
|
947
|
+
' eff = prev or MODE_CONFIRM', ' eff = MODE_CONFIRM', 2),
|
|
948
|
+
# ⑲ 补强批次:结算异常不捕获(回到旧 fail-open 形态:异常传播、结算态不
|
|
949
|
+
# 更新 ⇒ 未结算进程 mode() 照常 full)。
|
|
950
|
+
# 红 6(补强批次实测):G3/G3a/G3b/G3c(未结算格失败态整组缺)+ G3d/G3e
|
|
951
|
+
# (已结算格失败态缺;G3d0/G4/G5/G6 语义相符仍绿)。
|
|
952
|
+
("结算异常不捕获(旧 fail-open 形态)", "mod", am, "settle",
|
|
953
|
+
' except Exception as exc:', ' except () as exc:', 6),
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _fn_src(target):
|
|
958
|
+
"""变异锚点所在源码:`mod` = 目标函数源码;`attr` = 属主源码(模块整体)。"""
|
|
959
|
+
kind, owner, name = target
|
|
960
|
+
if kind == "attr":
|
|
961
|
+
return inspect.getsource(owner)
|
|
962
|
+
return inspect.getsource(getattr(owner, name))
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _strip_indent(text: str, n: int) -> str:
|
|
966
|
+
"""按 n 列去缩进(首 n 列全空白的行才截,其余行 lstrip)。"""
|
|
967
|
+
if not n:
|
|
968
|
+
return text
|
|
969
|
+
out = []
|
|
970
|
+
for line in text.split("\n"):
|
|
971
|
+
if line[:n].strip() == "":
|
|
972
|
+
out.append(line[n:])
|
|
973
|
+
else:
|
|
974
|
+
out.append(line.lstrip())
|
|
975
|
+
return "\n".join(out)
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def _anchor_check():
|
|
979
|
+
"""返回 ANCHOR-MISS 说明列表(空 = 全部在位)。"""
|
|
980
|
+
bad = []
|
|
981
|
+
for name, kind, owner, name_or_f, old, _new, _n in _SRC_MUTATIONS:
|
|
982
|
+
if old not in _fn_src((kind, owner, name_or_f)):
|
|
983
|
+
bad.append("变异锚点缺失:%r @%s.%s"
|
|
984
|
+
% (old[:40], owner.__name__, name_or_f))
|
|
985
|
+
return bad
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
@contextlib.contextmanager
|
|
989
|
+
def _patched(target, old, new):
|
|
990
|
+
"""把目标函数按字面替换变异后安装/还原(不落盘、不改源文件)。"""
|
|
991
|
+
kind, owner, name = target
|
|
992
|
+
if kind == "attr":
|
|
993
|
+
live = getattr(owner, name)
|
|
994
|
+
setattr(owner, name, eval(new, dict(vars(owner)))) # noqa: S307
|
|
995
|
+
try:
|
|
996
|
+
yield
|
|
997
|
+
finally:
|
|
998
|
+
setattr(owner, name, live)
|
|
999
|
+
return
|
|
1000
|
+
src = _fn_src(target)
|
|
1001
|
+
cut = len(src) - len(src.lstrip(" "))
|
|
1002
|
+
src2 = _strip_indent(src, cut)
|
|
1003
|
+
old2 = _strip_indent(old, cut)
|
|
1004
|
+
new2 = _strip_indent(new, cut)
|
|
1005
|
+
if old2 not in src2:
|
|
1006
|
+
raise AssertionError("变异锚点在去缩进后仍不匹配:%r" % old[:40])
|
|
1007
|
+
src = src2.replace(old2, new2)
|
|
1008
|
+
if kind == "mod":
|
|
1009
|
+
ns = dict(vars(owner))
|
|
1010
|
+
ns["__name__"] = owner.__name__
|
|
1011
|
+
else:
|
|
1012
|
+
ns = dict(vars(sys.modules[owner.__module__]))
|
|
1013
|
+
ns["__name__"] = owner.__module__
|
|
1014
|
+
exec(compile(src, "adm_mut.py", "exec"), ns)
|
|
1015
|
+
live = getattr(owner, name)
|
|
1016
|
+
setattr(owner, name, ns[name])
|
|
1017
|
+
try:
|
|
1018
|
+
yield
|
|
1019
|
+
finally:
|
|
1020
|
+
setattr(owner, name, live)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _mutate_mode():
|
|
1024
|
+
bad = []
|
|
1025
|
+
anchor_bad = _anchor_check()
|
|
1026
|
+
if anchor_bad:
|
|
1027
|
+
for b in anchor_bad:
|
|
1028
|
+
print(" ANCHOR-MISS " + b)
|
|
1029
|
+
print("\n锚点自检:FAIL(fail-closed,exit 2)")
|
|
1030
|
+
return 2
|
|
1031
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
1032
|
+
clean = _run_groups()
|
|
1033
|
+
print(" 未变异基线:红项=%d%s"
|
|
1034
|
+
% (clean, "" if clean == 0 else " ← 基线即红,变异核验无意义"))
|
|
1035
|
+
if clean:
|
|
1036
|
+
bad.append("未变异基线即失败")
|
|
1037
|
+
_RUN[0] = 10 ** 6 # 变异轮用独立子根(防读到上一轮盘面)
|
|
1038
|
+
for name, kind, owner, name_or_f, old, new, expect in _SRC_MUTATIONS:
|
|
1039
|
+
_RUN[0] += 1
|
|
1040
|
+
try:
|
|
1041
|
+
with _patched((kind, owner, name_or_f), old, new), \
|
|
1042
|
+
contextlib.redirect_stdout(io.StringIO()):
|
|
1043
|
+
fails = _run_groups()
|
|
1044
|
+
except Exception as exc: # noqa: BLE001 —— 变异把路径打断也算「红」
|
|
1045
|
+
fails = -1
|
|
1046
|
+
print(" 变异「%s」→ 断言链抛异常 %s: %s(判 FAIL)"
|
|
1047
|
+
% (name, type(exc).__name__, str(exc)[:80]))
|
|
1048
|
+
verdict = ("命中预期" if fails == expect
|
|
1049
|
+
else "**红项数不符(预期 %d)**" % expect)
|
|
1050
|
+
print(" 变异「%s」→ 红项=%d %s" % (name, fails, verdict))
|
|
1051
|
+
for f in _FAIL[:8]:
|
|
1052
|
+
print(" 红:", f)
|
|
1053
|
+
if len(_FAIL) > 8:
|
|
1054
|
+
print(" …(余 %d 项)" % (len(_FAIL) - 8))
|
|
1055
|
+
if fails != expect:
|
|
1056
|
+
bad.append(name)
|
|
1057
|
+
print("\n定点变异自证:%s"
|
|
1058
|
+
% ("PASS(每处判据都有变异钉死,且红项数逐处吻合)" if not bad
|
|
1059
|
+
else "FAIL —— " + "、".join(bad)))
|
|
1060
|
+
return 0 if not bad else 1
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def main() -> int:
|
|
1064
|
+
src = os.path.basename(os.path.abspath(__file__))
|
|
1065
|
+
if "--mutate" in sys.argv:
|
|
1066
|
+
print("!! 定点变异模式:逐个变异判据,套件应转红且红项数吻合\n")
|
|
1067
|
+
try:
|
|
1068
|
+
return _mutate_mode()
|
|
1069
|
+
finally:
|
|
1070
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
1071
|
+
anchor_bad = _anchor_check()
|
|
1072
|
+
if anchor_bad:
|
|
1073
|
+
for b in anchor_bad:
|
|
1074
|
+
print(" ANCHOR-MISS " + b)
|
|
1075
|
+
print("\n锚点自检:FAIL(fail-closed,exit 2)——实现改了请同步变异表")
|
|
1076
|
+
return 2
|
|
1077
|
+
print("锚点自检:PASS(%s;不以 git HEAD 为基线源)" % src)
|
|
1078
|
+
try:
|
|
1079
|
+
for g in _GROUPS:
|
|
1080
|
+
g()
|
|
1081
|
+
finally:
|
|
1082
|
+
_RUN[0] += 1
|
|
1083
|
+
print("\n准入读数与准入闸守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
|
|
1084
|
+
if _FAIL:
|
|
1085
|
+
for f in _FAIL:
|
|
1086
|
+
print(" 红:", f)
|
|
1087
|
+
return 0 if not _FAIL else 1
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
if __name__ == "__main__":
|
|
1091
|
+
try:
|
|
1092
|
+
rc = main()
|
|
1093
|
+
finally:
|
|
1094
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
1095
|
+
os.environ.pop(_ENV_MODE, None)
|
|
1096
|
+
if _OLD_POLICY is not None:
|
|
1097
|
+
os.environ["MDCG_POLICY_FILE"] = _OLD_POLICY
|
|
1098
|
+
sys.exit(rc)
|