@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.
- package/README.md +49 -18
- package/docs/README.md +1 -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.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/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/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/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/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/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/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//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/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/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +527 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +191 -26
- package/md_cg/mdcg.py +594 -39
- package/md_cg/mdcos.py +869 -38
- package/md_cg/nodefile.py +74 -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 +411 -0
- package/md_cg/rollback_cli.py +104 -0
- 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/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -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_boundary_hit.py +410 -0
- package/md_cg/test_cons200_scan_selection.py +791 -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_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_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -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_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_mcp.py +8 -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_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_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_stgidx_index_parity.py +1020 -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/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/writepipe.py +101 -2
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 变更单回滚面守卫(三档自治 · 设计 v0.2 §四/§十一 · 批次③)
|
|
3
|
+
|
|
4
|
+
覆盖(设计 §十一 验收判据在本批的落点):
|
|
5
|
+
· **回滚实测**(§十一 明文「每种破坏性动作各演一次『落 → 回滚 → 逐字节比对
|
|
6
|
+
前像』」):C 改写(含「目标被覆写后回滚」——第三方改动被保留)· B 合并
|
|
7
|
+
(聚合行定向剥离)· D 删除(trash + 删除清单 restore)三条各一演,全部
|
|
8
|
+
经**回滚命令串实测演练**(subprocess 执行字段里的命令串,不是进程内直调);
|
|
9
|
+
· **红项面**(§十一):回滚后不得残留——索引条目 / 边 / 聚合行三面与前像
|
|
10
|
+
时点影响面逐位对拍(`residual` 三键 + 逐字节比对);
|
|
11
|
+
· **前像=执行时点**(§四 明文):C 场景把「提议→accept 之间目标被第三方
|
|
12
|
+
覆写」演出来——回滚后盘面 == 执行前一刻(第三方版本),**不是**提议时点
|
|
13
|
+
版本;T 组另有「最旧快照」判别场景(预置一张提议时点旧快照,实现若取最旧
|
|
14
|
+
即转红);
|
|
15
|
+
· **受保护节点既有快照行为一字不变**(约束):P 组把 `protect.snapshot()` /
|
|
16
|
+
`guard_write|guard_forget` 的既有读数(快照数、审计 action、命名形态)钉死,
|
|
17
|
+
并以定点变异(把前像挂钩塞进 `_allow`)证明这组判据有判别力;
|
|
18
|
+
· 结构性单点:前像拍摄入口(`protect.snapshot_preimage`)、执行时点补全
|
|
19
|
+
(`autonomy_modes.mutation_executed`)、回滚命令构造(`rollback.command_for`)
|
|
20
|
+
全仓唯一;
|
|
21
|
+
· 边界 fail-closed:非 dict 载荷 / 缺前像 / 未知动作类 / 前像文件缺失 /
|
|
22
|
+
未执行(reject)变更单,一律拒绝回滚(不猜一个前像)。
|
|
23
|
+
|
|
24
|
+
**定点变异自证(`--mutate`,与 `md_cg/test_neg_condition_hits.py` 同口径)**:
|
|
25
|
+
表内每项 = (说明, 目标, 锚点原文, 替换文, 预期红项数)。锚点须**逐字**出现在
|
|
26
|
+
目标源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。红项数与实测**逐一相符**
|
|
27
|
+
才算通过——不符即 FAIL(多红=断言越界、少红=该判据无判别力)。
|
|
28
|
+
**基线源=工作区源码 + 本表,绝不取 `git HEAD`**(本仓已有两次教训:基线绑
|
|
29
|
+
提交即失效)。
|
|
30
|
+
|
|
31
|
+
沙箱(硬约束):一切读写都在 tempfile.mkdtemp 内(含 MDCG_AUX_ROOT / 主密钥 /
|
|
32
|
+
policy——crypto 在导入期求值 MASTER_FILE,故必须在任何 md_cg 子模块 import
|
|
33
|
+
**之前**设好);跑完 rmtree。**绝不碰在役数据根**。回滚命令串实测演练走
|
|
34
|
+
subprocess(shlex 切分命令串、`python` 令牌换成本进程解释器、shell=False、
|
|
35
|
+
显式 UTF-8)——演练的是**字段里的那条命令串本身**。
|
|
36
|
+
|
|
37
|
+
运行:
|
|
38
|
+
python -X utf8 -m md_cg.test_mutation_rollback # 正常跑
|
|
39
|
+
python -X utf8 -m md_cg.test_mutation_rollback --mutate # 定点变异自证
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import contextlib
|
|
44
|
+
import inspect
|
|
45
|
+
import io
|
|
46
|
+
import json
|
|
47
|
+
import os
|
|
48
|
+
import re
|
|
49
|
+
import shlex
|
|
50
|
+
import shutil
|
|
51
|
+
import subprocess
|
|
52
|
+
import sys
|
|
53
|
+
import tempfile
|
|
54
|
+
|
|
55
|
+
# ---- 沙箱:必须在任何 md_cg 子模块 import 之前 ------------------------------
|
|
56
|
+
_SANDBOX = tempfile.mkdtemp(prefix="mutation_rollback_sandbox_")
|
|
57
|
+
os.environ["MDCG_AUX_ROOT"] = _SANDBOX
|
|
58
|
+
os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
|
|
59
|
+
os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
|
|
60
|
+
os.environ.pop("MDCG_TEST_LIVE_ROOT", None)
|
|
61
|
+
_OLD_POLICY = os.environ.pop("MDCG_POLICY_FILE", None)
|
|
62
|
+
|
|
63
|
+
from . import autonomy_modes, forgetting, nodefile, protect, rollback # noqa: E402
|
|
64
|
+
from . import writepipe # noqa: E402
|
|
65
|
+
from . import mdcos # noqa: E402
|
|
66
|
+
from .mdcos import MdCGOS # noqa: E402
|
|
67
|
+
|
|
68
|
+
_ENV_MODE = autonomy_modes.AUTONOMY_ENV_KEYS["mode"]
|
|
69
|
+
|
|
70
|
+
_PASS = []
|
|
71
|
+
_FAIL = []
|
|
72
|
+
_RUN = [0]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def ok(cond, msg, extra=""):
|
|
76
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
77
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
78
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _policy():
|
|
82
|
+
path = os.path.join(_SANDBOX, "policy.json")
|
|
83
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
84
|
+
json.dump({"forbidden": ["FORBIDDEN_WORD"], "required": ["PASSED"]}, f)
|
|
85
|
+
os.environ["MDCG_POLICY_FILE"] = path
|
|
86
|
+
return path
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _lib(tag="lib"):
|
|
90
|
+
"""独立合成库(每轮独立子根:变异模式连跑多轮,共用目录会读到上一轮盘面)。"""
|
|
91
|
+
return MdCGOS(os.path.join(_SANDBOX, "%s_r%d" % (tag, _RUN[0])), autoflush=0)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@contextlib.contextmanager
|
|
95
|
+
def _mode(m):
|
|
96
|
+
old = os.environ.get(_ENV_MODE)
|
|
97
|
+
os.environ[_ENV_MODE] = m
|
|
98
|
+
try:
|
|
99
|
+
yield m
|
|
100
|
+
finally:
|
|
101
|
+
if old is None:
|
|
102
|
+
os.environ.pop(_ENV_MODE, None)
|
|
103
|
+
else:
|
|
104
|
+
os.environ[_ENV_MODE] = old
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _pipe():
|
|
108
|
+
return writepipe.install_default_gates(writepipe.WritePipeline())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _repo_root():
|
|
112
|
+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _pre_path(cg, rel):
|
|
116
|
+
return os.path.join(cg.root, str(rel).replace("/", os.sep))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _bytes(path):
|
|
120
|
+
with open(path, "rb") as f:
|
|
121
|
+
return f.read()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _disk_path(cg, nid):
|
|
125
|
+
return os.path.join(cg.root, cg.index["nodes"][nid]["path"])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _audit_rows(cg):
|
|
129
|
+
p = os.path.join(cg.root, protect.AUDIT_FILE)
|
|
130
|
+
if not os.path.exists(p):
|
|
131
|
+
return []
|
|
132
|
+
out = []
|
|
133
|
+
with open(p, encoding="utf-8") as f:
|
|
134
|
+
for line in f:
|
|
135
|
+
line = line.strip()
|
|
136
|
+
if line:
|
|
137
|
+
out.append(json.loads(line))
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _run_command(cmd):
|
|
142
|
+
"""**实测演练回滚命令串**:切分命令串 → `python` 换本进程解释器 → subprocess。
|
|
143
|
+
|
|
144
|
+
shlex 用 posix=False:Windows 路径里的反斜杠在 posix 模式会被当转义字符
|
|
145
|
+
(把 `C:\\Users\\...` 拆坏);posix=False 下引号原样保留,故手工剥引号。
|
|
146
|
+
只替换首令牌 `python`(守卫进程内的解释器才可靠——与命令串里写的 `python`
|
|
147
|
+
同义);其余令牌逐字来自字段,不重写(演练的是字段里的那条串本身)。
|
|
148
|
+
"""
|
|
149
|
+
argv = shlex.split(cmd, posix=False)
|
|
150
|
+
argv = [a[1:-1] if len(a) >= 2 and a[0] == a[-1] == '"' else a for a in argv]
|
|
151
|
+
if not argv:
|
|
152
|
+
# 命令串为空(载荷缺 rollback——变异/未执行场景):如实报失败体,
|
|
153
|
+
# 由断言判红(不抛异常打断链条)。
|
|
154
|
+
return {"rc": -1, "out": "", "err": "empty rollback command", "json": None}
|
|
155
|
+
if argv[0] == "python":
|
|
156
|
+
argv[0] = sys.executable
|
|
157
|
+
p = subprocess.run(argv, capture_output=True, text=True, encoding="utf-8",
|
|
158
|
+
errors="replace", cwd=_repo_root(),
|
|
159
|
+
env=dict(os.environ, PYTHONUTF8="1"))
|
|
160
|
+
out_json = None
|
|
161
|
+
try:
|
|
162
|
+
out_json = json.loads((p.stdout or "").strip().splitlines()[-1])
|
|
163
|
+
except Exception:
|
|
164
|
+
out_json = None
|
|
165
|
+
return {"rc": p.returncode, "out": p.stdout or "", "err": p.stderr or "",
|
|
166
|
+
"json": out_json}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _refresh(cg):
|
|
170
|
+
"""CLI 子进程回滚后刷新父进程索引(读面代际感知:子进程经 _index_log
|
|
171
|
+
增量日志落盘,重载后父进程才看见「节点回来了/条目换了」)。"""
|
|
172
|
+
cg._maybe_reload_index()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
BODY = ("# 功能名:部署面端口约定\n"
|
|
176
|
+
"# 生效条件:载体/位置:prod 集群;时间:2026-09-30 起;方法:部署清单核对;约束:无\n"
|
|
177
|
+
"# 子功能:登记各服务监听端口\n"
|
|
178
|
+
"# 执行:核对 manifest 的 ports 段\n"
|
|
179
|
+
"# 验证方式:test\n"
|
|
180
|
+
"# 不适用条件:无\n"
|
|
181
|
+
"\n网关监听端口 8080/HTTP;管理面监听端口 8081/HTTP;"
|
|
182
|
+
"指标面监听端口 %s/HTTP;日志面监听端口 9091/HTTP\n")
|
|
183
|
+
PLAIN = "PASSED 探针正文 %s"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _snapshot_entry(cg, nid, rel):
|
|
187
|
+
"""按快照文件重算索引条目(与 _node_entry 同源)——回滚后索引对拍的基准。
|
|
188
|
+
|
|
189
|
+
path 用**节点自身的盘面路径**:`_node_entry` 的 path/bucket 从文件位置派生
|
|
190
|
+
(与 fm/content 无关),快照文件在 `_protected_history/` 下、派生出的
|
|
191
|
+
path/bucket 是快照自己的——对拍两边的 path 必须同为节点盘面位置,其余
|
|
192
|
+
字段(tags/importance/edges/created_at…)才由 fm/content 决定。
|
|
193
|
+
rel 为空(前像缺失——变异场景)时返回 None,由调用方判 False(不抛)。
|
|
194
|
+
"""
|
|
195
|
+
if not rel:
|
|
196
|
+
return None
|
|
197
|
+
p = _disk_path(cg, nid)
|
|
198
|
+
with open(_pre_path(cg, rel), encoding="utf-8") as f:
|
|
199
|
+
fm, c = nodefile.loads(f.read())
|
|
200
|
+
return cg._node_entry(p, str(fm.get("layer") or "knowledge"), fm, c)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# =============================================================== P 组:前像面
|
|
204
|
+
def g_p():
|
|
205
|
+
print("== P 组:执行时点前像面(同面推广 · 受保护既有行为一字不变)==")
|
|
206
|
+
_policy()
|
|
207
|
+
cg = _lib("pre")
|
|
208
|
+
cg.add("p_1", PLAIN % "甲", layer="knowledge", verification_basis="test")
|
|
209
|
+
cg.flush()
|
|
210
|
+
snap1 = protect.snapshot_preimage(cg, "p_1", action="C", pid="prop_probe")
|
|
211
|
+
ok(bool(snap1)
|
|
212
|
+
and re.match(r"^_protected_history/p_1/\d{8}-\d{6}-\d{6}\.md$", snap1),
|
|
213
|
+
"P snapshot_preimage 返回同面路径(_protected_history/<id>/<ts>-<微秒>.md)",
|
|
214
|
+
snap1)
|
|
215
|
+
sp = _pre_path(cg, snap1)
|
|
216
|
+
dp = _disk_path(cg, "p_1")
|
|
217
|
+
ok(os.path.isfile(sp) and _bytes(sp) == _bytes(dp),
|
|
218
|
+
"P 前像与节点盘面逐字节相同(拍的就是执行前一刻)")
|
|
219
|
+
fm_s, c_s = nodefile.loads(open(sp, encoding="utf-8").read())
|
|
220
|
+
ok(fm_s.get("id") == "p_1" and c_s == cg.get("p_1")["content"],
|
|
221
|
+
"P 前像与 snapshot 同格式(nodefile 可解析、正文可读回)")
|
|
222
|
+
rows = [r for r in _audit_rows(cg)
|
|
223
|
+
if r.get("action") == "preimage" and r.get("node_id") == "p_1"]
|
|
224
|
+
ok(len(rows) == 1 and "C" in str(rows[0].get("reason"))
|
|
225
|
+
and "prop_probe" in str(rows[0].get("reason")),
|
|
226
|
+
"P 前像审计 action=preimage 且 reason 交代动作类/pid", rows[:1])
|
|
227
|
+
# 同秒两次前像不互踩(微秒后缀)
|
|
228
|
+
cg.add("p_1", PLAIN % "乙", layer="knowledge", verification_basis="test")
|
|
229
|
+
snap2 = protect.snapshot_preimage(cg, "p_1", action="C", pid="prop_probe2")
|
|
230
|
+
ok(bool(snap1) and bool(snap2) and snap1 != snap2
|
|
231
|
+
and os.path.isfile(_pre_path(cg, snap1)) and os.path.isfile(_pre_path(cg, snap2)),
|
|
232
|
+
"P 同一节点同秒两次前像=两个文件(不互踩,早先的回滚句柄不被覆盖)",
|
|
233
|
+
(snap1, snap2))
|
|
234
|
+
ok(_bytes(_pre_path(cg, snap2)) != _bytes(_pre_path(cg, snap1)),
|
|
235
|
+
"P 两张前像各自拍住各自时点的盘面(中间那次写入进了第二张)")
|
|
236
|
+
# 既有 snapshot() 形态钉(普通节点)
|
|
237
|
+
s0 = protect.snapshot(cg, "p_1")
|
|
238
|
+
ok(bool(s0) and re.match(r"^_protected_history/p_1/\d{8}-\d{6}\.md$", s0),
|
|
239
|
+
"P 既有 protect.snapshot 命名形态不变(秒级名,无微秒后缀)", s0)
|
|
240
|
+
rows_s = [r for r in _audit_rows(cg)
|
|
241
|
+
if r.get("action") == "snapshot" and r.get("node_id") == "p_1"]
|
|
242
|
+
ok(len(rows_s) == 1 and str(rows_s[0].get("reason")).startswith("显式快照("),
|
|
243
|
+
"P 既有 snapshot 审计形态不变(action=snapshot、reason「显式快照(…)」)",
|
|
244
|
+
rows_s[:1])
|
|
245
|
+
cg.close()
|
|
246
|
+
|
|
247
|
+
# 受保护节点:guard 路径既有行为(不经执行桥)逐条钉死
|
|
248
|
+
cg = _lib("prot")
|
|
249
|
+
cg.add("p_s", PLAIN % "自我版", layer="self", verification_basis="test")
|
|
250
|
+
cg.flush()
|
|
251
|
+
protect.guard_write(cg, "p_s", override=True, actor="guard-probe")
|
|
252
|
+
hist = protect.history(cg, "p_s")
|
|
253
|
+
acts = sorted(r.get("action") for r in _audit_rows(cg)
|
|
254
|
+
if r.get("node_id") == "p_s")
|
|
255
|
+
ok(len(hist) == 1, "P guard_write override:快照恰好 1 张(既有行为一字不变)",
|
|
256
|
+
hist)
|
|
257
|
+
ok(acts == ["override_write", "snapshot"],
|
|
258
|
+
"P guard_write override:审计面 snapshot+override_write 各一条", acts)
|
|
259
|
+
try:
|
|
260
|
+
protect.guard_forget(cg, "p_s", override=True, actor="guard-probe")
|
|
261
|
+
got_f = True
|
|
262
|
+
except Exception as exc: # noqa: BLE001
|
|
263
|
+
got_f = "exc=%s" % type(exc).__name__
|
|
264
|
+
acts2 = sorted(r.get("action") for r in _audit_rows(cg)
|
|
265
|
+
if r.get("node_id") == "p_s")
|
|
266
|
+
# 文件数上限 2、下限 1:guard 的秒级名在**同秒**两次看守间会互相覆盖
|
|
267
|
+
# (探针实测:同秒二次 snapshot 返回同一路径)——这是既有行为,判据按
|
|
268
|
+
# 审计面钉(两次 snapshot 各留一条,确定性),文件数只钉区间。
|
|
269
|
+
ok(got_f is True and 1 <= len(protect.history(cg, "p_s")) <= 2
|
|
270
|
+
and acts2 == ["override_forget", "override_write", "snapshot", "snapshot"],
|
|
271
|
+
"P guard_forget override:快照 + override_forget 留痕(既有口径,"
|
|
272
|
+
"同秒覆盖属既有行为)", (got_f, protect.history(cg, "p_s"), acts2))
|
|
273
|
+
cg.close()
|
|
274
|
+
|
|
275
|
+
# 执行桥对受保护节点(override 载荷):前像另拍 + guard 快照仍在
|
|
276
|
+
with _mode("confirm"):
|
|
277
|
+
cg = _lib("bridge_prot")
|
|
278
|
+
cg.add("p_b", PLAIN % "自我版", layer="self", verification_basis="test")
|
|
279
|
+
cg.flush()
|
|
280
|
+
pay = autonomy_modes.mutation_payload(
|
|
281
|
+
"C", "p_b", after=PLAIN % "新自我版", reason="守卫:执行桥+受保护",
|
|
282
|
+
primitive="add", meta={"override": True})
|
|
283
|
+
pr = autonomy_modes.propose_mutation(cg, "C", "p_b", payload=pay,
|
|
284
|
+
layer="self", info=True)
|
|
285
|
+
r = cg.review_decide(pr["pid"], "accept", reason="守卫")
|
|
286
|
+
m = r.get("mutation") or {}
|
|
287
|
+
ok(r.get("ok") is True and bool(m.get("before")),
|
|
288
|
+
"P 执行桥 accept(受保护+override)成功且前像在案",
|
|
289
|
+
{k: r.get(k) for k in ("ok", "error")})
|
|
290
|
+
hist2 = protect.history(cg, "p_b")
|
|
291
|
+
micro = [h for h in hist2 if re.search(r"\d{8}-\d{6}-\d{6}\.md$", h)]
|
|
292
|
+
plain = [h for h in hist2 if re.search(r"\d{8}-\d{6}\.md$", h)]
|
|
293
|
+
ok(len(hist2) == 2 and len(micro) == 1 and len(plain) == 1,
|
|
294
|
+
"P 执行桥对受保护节点:前像(微秒名)另拍、guard 快照(秒级名)原样"
|
|
295
|
+
"仍在(推广不破坏既有)", hist2)
|
|
296
|
+
rows_p = [rr for rr in _audit_rows(cg) if rr.get("node_id") == "p_b"]
|
|
297
|
+
ok(sorted(set(str(rr.get("action")) for rr in rows_p))
|
|
298
|
+
== ["override_write", "preimage", "snapshot"],
|
|
299
|
+
"P 审计面并存 preimage + snapshot + override_write(三类各司其职)",
|
|
300
|
+
sorted(r.get("action") for r in rows_p))
|
|
301
|
+
cg.close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# =============================================================== Q 组:C 改写
|
|
305
|
+
def g_q():
|
|
306
|
+
print("== Q 组:C 改写回滚(目标被覆写后回滚 → 执行时点前像)==")
|
|
307
|
+
_policy()
|
|
308
|
+
with _mode("confirm"):
|
|
309
|
+
cg = _lib("c")
|
|
310
|
+
cg.add("q_1", PLAIN % "初版", layer="knowledge",
|
|
311
|
+
verification_basis="test",
|
|
312
|
+
edges=[{"target": "q_p", "relation_type": "part_of"}])
|
|
313
|
+
cg.add("q_p", PLAIN % "父", layer="knowledge",
|
|
314
|
+
verification_basis="test")
|
|
315
|
+
cg.flush()
|
|
316
|
+
pipe = _pipe()
|
|
317
|
+
o1 = pipe.execute(cg, {"content_kind": "text", "content": PLAIN % "改写版",
|
|
318
|
+
"layer": "knowledge", "node_id": "q_1"})
|
|
319
|
+
ok(o1.get("moved_to") == "review_queue" and bool(o1.get("pid")),
|
|
320
|
+
"Q 写链出单(C 改写)", {k: o1.get(k) for k in ("moved_to", "pid")})
|
|
321
|
+
# 提议→accept 之间:第三方覆写(带同一条边)——前像必须=此刻
|
|
322
|
+
cg.add("q_1", PLAIN % "第三方版", layer="knowledge",
|
|
323
|
+
verification_basis="test",
|
|
324
|
+
edges=[{"target": "q_p", "relation_type": "part_of"}])
|
|
325
|
+
third_bytes = _bytes(_disk_path(cg, "q_1"))
|
|
326
|
+
d = cg.review_decide(o1["pid"], "accept", reason="守卫")
|
|
327
|
+
m = d.get("mutation") or {}
|
|
328
|
+
ok(d.get("ok") is True and bool(m.get("before")),
|
|
329
|
+
"Q accept 执行且执行记录带前像", {k: d.get(k) for k in ("ok", "error")})
|
|
330
|
+
pre_p = _pre_path(cg, m.get("before")) if m.get("before") else None
|
|
331
|
+
ok(bool(pre_p) and os.path.isfile(pre_p) and _bytes(pre_p) == third_bytes,
|
|
332
|
+
"Q 前像=**执行时点**(=第三方覆写后的盘面,逐字节)")
|
|
333
|
+
ok((((m.get("impact") or {}).get("edges") or {}).get("parents")
|
|
334
|
+
== ["q_p"]),
|
|
335
|
+
"Q 影响面·边 = 执行时点读数(part_of 父边在案)",
|
|
336
|
+
(m.get("impact") or {}).get("edges"))
|
|
337
|
+
# —— 落 ——:执行后正文=改写版、边被 add 全量重建清空(既有行为)
|
|
338
|
+
ok("改写版" in cg.get("q_1")["content"]
|
|
339
|
+
and "第三方版" not in cg.get("q_1")["content"],
|
|
340
|
+
"Q 落:accept 覆写生效(正文已换)")
|
|
341
|
+
ok((((rollback.collect_impact(cg, "q_1").get("edges")) or {})
|
|
342
|
+
.get("parents") or []) == [],
|
|
343
|
+
"Q 落:C 覆写的 add 全量重建把声明式边清空(既有行为,回滚须恢复)")
|
|
344
|
+
# —— 回滚(进程内统一入口;父进程内的变异可打红本段判据)——
|
|
345
|
+
rr = rollback.rollback_mutation(cg, m, reason="Q")
|
|
346
|
+
ok(rr.get("ok") is True, "Q 回滚:统一入口 ok",
|
|
347
|
+
{k: rr.get(k) for k in ("ok", "error")})
|
|
348
|
+
ok(rr.get("bytes_equal_preimage") is True,
|
|
349
|
+
"Q 回滚:逐字节比对前像 = True", rr.get("bytes_equal_preimage"))
|
|
350
|
+
res = rr.get("residual") or {}
|
|
351
|
+
ok(res.get("agg_lines_restored") is True
|
|
352
|
+
and res.get("edges_restored") is True
|
|
353
|
+
and res.get("index_restored") is True,
|
|
354
|
+
"Q 回滚:残留核对三面全回(索引/边/聚合行)", res)
|
|
355
|
+
ok(_bytes(_disk_path(cg, "q_1")) == third_bytes,
|
|
356
|
+
"Q 回滚后盘面逐字节 == 前像(第三方版本字节)")
|
|
357
|
+
ok("初版" not in cg.get("q_1")["content"],
|
|
358
|
+
"Q 撤销的是本变更(未一并抹掉第三方改动,也没回到提议时点初版)")
|
|
359
|
+
e_now = cg.index["nodes"]["q_1"]
|
|
360
|
+
e_ref = _snapshot_entry(cg, "q_1", m.get("before"))
|
|
361
|
+
ok(e_now == e_ref,
|
|
362
|
+
"Q 索引不残留:回滚后条目 == 前像重算条目(逐位)", (e_now, e_ref))
|
|
363
|
+
ok(rollback.collect_impact(cg, "q_1")["edges"]["parents"] == ["q_p"],
|
|
364
|
+
"Q 边恢复:回滚后 part_of 父边回案(边面无本变更残留)")
|
|
365
|
+
# —— 命令串实测演练(CLI subprocess;此刻为幂等重放)——
|
|
366
|
+
got = _run_command(m.get("rollback") or "")
|
|
367
|
+
_refresh(cg)
|
|
368
|
+
out = got.get("json") or {}
|
|
369
|
+
ok(got["rc"] == 0 and out.get("ok") is True
|
|
370
|
+
and out.get("bytes_equal_preimage") is True,
|
|
371
|
+
"Q 回滚命令串实测演练:rc=0、ok=True、逐字节比对前像=True",
|
|
372
|
+
{"rc": got["rc"], "err": (got.get("err") or "")[-160:]})
|
|
373
|
+
ok(_bytes(_disk_path(cg, "q_1")) == third_bytes,
|
|
374
|
+
"Q 命令串演练后盘面仍 == 前像(独立进程回滚一致)")
|
|
375
|
+
cg.close()
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# =============================================================== R 组:B 合并
|
|
379
|
+
def g_r():
|
|
380
|
+
print("== R 组:B 合并回滚(聚合行定向剥离)==")
|
|
381
|
+
_policy()
|
|
382
|
+
with _mode("confirm"):
|
|
383
|
+
cg = _lib("m")
|
|
384
|
+
cg.add("r_t", BODY % "8081", layer="knowledge",
|
|
385
|
+
verification_basis="test")
|
|
386
|
+
cg.flush()
|
|
387
|
+
pre_bytes = _bytes(_disk_path(cg, "r_t"))
|
|
388
|
+
r1 = cg.remember_gated("r_n", BODY % "9595", layer="knowledge")
|
|
389
|
+
ok(r1.get("verdict") == "CONFIRM" and bool(r1.get("pid")),
|
|
390
|
+
"R 出单(B 合并)", r1.get("verdict"))
|
|
391
|
+
d = cg.review_decide(r1["pid"], "accept", reason="守卫")
|
|
392
|
+
m = d.get("mutation") or {}
|
|
393
|
+
ok(d.get("ok") is True, "R accept 执行")
|
|
394
|
+
cur = cg.get("r_t")
|
|
395
|
+
fm1 = cur["frontmatter"]
|
|
396
|
+
ok(forgetting.AGG_MARK in cur["content"]
|
|
397
|
+
and fm1.get("merge_count") == 1,
|
|
398
|
+
"R 落:聚合行已追加、merge_count=1(既有原语落盘)")
|
|
399
|
+
ok((m.get("impact") or {}).get("agg_lines") == [],
|
|
400
|
+
"R 影响面:执行时点无聚合行(agg_lines=[])",
|
|
401
|
+
(m.get("impact") or {}).get("agg_lines"))
|
|
402
|
+
# —— 回滚(进程内统一入口:定向剥离)——
|
|
403
|
+
rr = rollback.rollback_mutation(cg, m, reason="R")
|
|
404
|
+
ok(rr.get("ok") is True and rr.get("method") == "strip_aggregate_lines"
|
|
405
|
+
and rr.get("stripped_ok") is True and bool(rr.get("removed_lines")),
|
|
406
|
+
"R 回滚走**定向剥离**且剥离精确(stripped_ok、removed 非空)",
|
|
407
|
+
{k: rr.get(k) for k in ("ok", "method", "stripped_ok",
|
|
408
|
+
"removed_lines")})
|
|
409
|
+
ok(rr.get("bytes_equal_preimage") is True,
|
|
410
|
+
"R 回滚:逐字节比对前像 = True")
|
|
411
|
+
ok(_bytes(_disk_path(cg, "r_t")) == pre_bytes,
|
|
412
|
+
"R 回滚后盘面逐字节 == 前像(合并前正文)")
|
|
413
|
+
cur2 = cg.get("r_t")
|
|
414
|
+
ok(forgetting.AGG_MARK not in cur2["content"],
|
|
415
|
+
"R 聚合行不残留(本次合并的聚合行已剥净)")
|
|
416
|
+
ok(cur2["frontmatter"].get("merge_count") in (None, 0)
|
|
417
|
+
and float(cur2["frontmatter"].get("importance") or 0) == 0.5,
|
|
418
|
+
"R fm 回退:merge_count/importance 回前像值(全量取前像)",
|
|
419
|
+
{k: cur2["frontmatter"].get(k) for k in ("merge_count", "importance")})
|
|
420
|
+
ok((rr.get("residual") or {}).get("agg_lines_restored") is True,
|
|
421
|
+
"R 残留核对:聚合行面对齐前像时点读数",
|
|
422
|
+
(rr.get("residual") or {}))
|
|
423
|
+
# —— 命令串实测演练(CLI;幂等重放)——
|
|
424
|
+
got = _run_command(m.get("rollback") or "")
|
|
425
|
+
_refresh(cg)
|
|
426
|
+
out = got.get("json") or {}
|
|
427
|
+
ok(got["rc"] == 0 and out.get("ok") is True
|
|
428
|
+
and out.get("bytes_equal_preimage") is True
|
|
429
|
+
and _bytes(_disk_path(cg, "r_t")) == pre_bytes,
|
|
430
|
+
"R 回滚命令串实测演练:rc=0、ok=True、盘面仍 == 前像",
|
|
431
|
+
{"rc": got["rc"], "err": (got.get("err") or "")[-160:]})
|
|
432
|
+
cg.close()
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# =============================================================== S 组:D 删除
|
|
436
|
+
def g_s():
|
|
437
|
+
print("== S 组:D 删除回滚(trash + 删除清单 restore 接进统一口径)==")
|
|
438
|
+
_policy()
|
|
439
|
+
with _mode("confirm"):
|
|
440
|
+
cg = _lib("d")
|
|
441
|
+
cg.add("s_1", PLAIN % "初版", layer="knowledge",
|
|
442
|
+
verification_basis="test", tags=["t1"], importance=0.62)
|
|
443
|
+
cg.flush()
|
|
444
|
+
pre_bytes = _bytes(_disk_path(cg, "s_1"))
|
|
445
|
+
r1 = cg.forget_gated("s_1", reason="守卫删除")
|
|
446
|
+
ok(r1.get("moved_to") == "review_queue" and bool(r1.get("pid")),
|
|
447
|
+
"S 出单(D 删除)", {k: r1.get(k) for k in ("moved_to", "pid")})
|
|
448
|
+
d = cg.review_decide(r1["pid"], "accept", reason="守卫")
|
|
449
|
+
m = d.get("mutation") or {}
|
|
450
|
+
ok(d.get("ok") is True and cg.get("s_1") is None
|
|
451
|
+
and cg.is_tombstoned("s_1"),
|
|
452
|
+
"S 落:软删生效(节点离位 + 删除清单在案)")
|
|
453
|
+
ok(os.path.isfile(os.path.join(cg.root, "trash", "s_1.md")),
|
|
454
|
+
"S trash 原件在案(软删=移动,非销毁)")
|
|
455
|
+
# —— 回滚(进程内统一入口:restore + 前像校准)——
|
|
456
|
+
rr = rollback.rollback_mutation(cg, m, reason="S")
|
|
457
|
+
ok(rr.get("ok") is True, "S 回滚:统一入口 ok",
|
|
458
|
+
{k: rr.get(k) for k in ("ok", "error")})
|
|
459
|
+
ok(rr.get("calibrated") is True
|
|
460
|
+
and rr.get("restore_bytes_equal") is False,
|
|
461
|
+
"S 读数如实:restore 原样输出与快照有字节差(探针实测多 "
|
|
462
|
+
"sensitivity:null 键)→ 已按前像校准(不假装 restore 逐字节)",
|
|
463
|
+
{k: rr.get(k) for k in ("calibrated", "restore_bytes_equal")})
|
|
464
|
+
ok(rr.get("bytes_equal_preimage") is True
|
|
465
|
+
and _bytes(_disk_path(cg, "s_1")) == pre_bytes,
|
|
466
|
+
"S 回滚后盘面逐字节 == 前像")
|
|
467
|
+
ok(cg.get("s_1") is not None
|
|
468
|
+
and (PLAIN % "初版") in (cg.get("s_1")["content"] or ""),
|
|
469
|
+
"S 节点复位(读面可见、正文回前像;盘面逐字节已由上一断言钉死)")
|
|
470
|
+
res = rr.get("residual") or {}
|
|
471
|
+
ok(res.get("index_restored") is True
|
|
472
|
+
and res.get("edges_restored") is True
|
|
473
|
+
and res.get("agg_lines_restored") is True,
|
|
474
|
+
"S 残留核对:索引/边/聚合行三面全回(索引条目 present/path 复位)", res)
|
|
475
|
+
# —— 命令串实测演练(CLI;幂等重放走 replayed 分支)——
|
|
476
|
+
got = _run_command(m.get("rollback") or "")
|
|
477
|
+
_refresh(cg)
|
|
478
|
+
out = got.get("json") or {}
|
|
479
|
+
ok(got["rc"] == 0 and out.get("ok") is True
|
|
480
|
+
and out.get("replayed") is True
|
|
481
|
+
and _bytes(_disk_path(cg, "s_1")) == pre_bytes,
|
|
482
|
+
"S 回滚命令串实测演练:rc=0、幂等重放(replayed=True)、盘面仍 == 前像",
|
|
483
|
+
{"rc": got["rc"], "err": (got.get("err") or "")[-160:]})
|
|
484
|
+
cg.close()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# =============================================================== T 组:单点/边界/时点
|
|
488
|
+
def g_t():
|
|
489
|
+
print("== T 组:单点结构 · 边界 fail-closed · 时点判别 ==")
|
|
490
|
+
# 单点:前像拍摄入口全仓唯一(protect 定义 + rollback 调用)
|
|
491
|
+
hits = []
|
|
492
|
+
for dirpath, dirnames, filenames in os.walk(_repo_root()):
|
|
493
|
+
dirnames[:] = [d for d in dirnames
|
|
494
|
+
if d not in (".git", "__pycache__", "node_modules",
|
|
495
|
+
".venv", "target", "lib")]
|
|
496
|
+
for fn in filenames:
|
|
497
|
+
if not fn.endswith(".py") or fn.startswith("test_"):
|
|
498
|
+
continue # 生产码单点:测试/守卫自身的调用与字面量不算第二实现
|
|
499
|
+
p = os.path.join(dirpath, fn)
|
|
500
|
+
src = open(p, encoding="utf-8", errors="replace").read()
|
|
501
|
+
if "snapshot_preimage(" in src:
|
|
502
|
+
hits.append(os.path.relpath(p, _repo_root()).replace("\\", "/"))
|
|
503
|
+
ok(sorted(hits) == ["md_cg/protect.py", "md_cg/rollback.py"],
|
|
504
|
+
"T 前像拍摄入口单点(snapshot_preimage 只出现在 protect 定义 + rollback 调用)",
|
|
505
|
+
hits)
|
|
506
|
+
hits2 = []
|
|
507
|
+
for dirpath, dirnames, filenames in os.walk(_repo_root()):
|
|
508
|
+
dirnames[:] = [d for d in dirnames
|
|
509
|
+
if d not in (".git", "__pycache__", "node_modules",
|
|
510
|
+
".venv", "target", "lib")]
|
|
511
|
+
for fn in filenames:
|
|
512
|
+
if not fn.endswith(".py") or fn.startswith("test_"):
|
|
513
|
+
continue # 同上:只看生产码
|
|
514
|
+
p = os.path.join(dirpath, fn)
|
|
515
|
+
src = open(p, encoding="utf-8", errors="replace").read()
|
|
516
|
+
if "mutation_executed(" in src:
|
|
517
|
+
hits2.append(os.path.relpath(p, _repo_root()).replace("\\", "/"))
|
|
518
|
+
ok(sorted(hits2) == ["md_cg/autonomy_modes.py", "md_cg/mdcos.py"],
|
|
519
|
+
"T 执行时点补全单点(mutation_executed 只在 autonomy_modes 定义 + mdcos 调用)",
|
|
520
|
+
hits2)
|
|
521
|
+
cmd = rollback.command_for("/tmp/x root", "prop_abc")
|
|
522
|
+
ok(isinstance(cmd, str) and "md_cg.rollback_cli" in cmd and "utf8" in cmd
|
|
523
|
+
and '--root "/tmp/x root"' in cmd and "--pid prop_abc" in cmd,
|
|
524
|
+
"T 回滚命令构造单点(形态含 rollback_cli/utf8/引号包 root/pid)", cmd)
|
|
525
|
+
# 边界 fail-closed
|
|
526
|
+
r = rollback.rollback_mutation(None, None)
|
|
527
|
+
ok(r.get("ok") is False and r.get("error") == "mutation_missing",
|
|
528
|
+
"T 非 dict 载荷 fail-closed(mutation_missing)", r)
|
|
529
|
+
r = rollback.rollback_mutation(None, {"action": "C", "target": "x"})
|
|
530
|
+
ok(r.get("ok") is False and r.get("error") == "rollback_gap"
|
|
531
|
+
and bool(r.get("hint")),
|
|
532
|
+
"T 缺前像 fail-closed(rollback_gap:不许凭猜测出来源)", r)
|
|
533
|
+
r = rollback.rollback_mutation(None, {"action": "E", "target": "x",
|
|
534
|
+
"before": "_protected_history/x/1.md"})
|
|
535
|
+
ok(r.get("ok") is False and r.get("error") == "rollback_action_unknown",
|
|
536
|
+
"T 未知动作类 fail-closed(E 分数类不属本批)", r)
|
|
537
|
+
# 时点判别场景:预置一张「提议时点」旧快照(手工写:初版字节),
|
|
538
|
+
# 实现若取最旧快照(=提议时点)即回滚到初版 → 本组断言转红。
|
|
539
|
+
with _mode("confirm"):
|
|
540
|
+
cg = _lib("point")
|
|
541
|
+
cg.add("t_1", PLAIN % "初版", layer="knowledge",
|
|
542
|
+
verification_basis="test")
|
|
543
|
+
cg.flush()
|
|
544
|
+
old_dir = os.path.join(cg.root, protect.HISTORY_DIR, "t_1")
|
|
545
|
+
os.makedirs(old_dir, exist_ok=True)
|
|
546
|
+
old_rel = "_protected_history/t_1/20000101-000000.md"
|
|
547
|
+
with open(_pre_path(cg, old_rel), "wb") as f:
|
|
548
|
+
f.write(_bytes(_disk_path(cg, "t_1"))) # 提议时点版本(初版)
|
|
549
|
+
pay = autonomy_modes.mutation_payload("C", "t_1",
|
|
550
|
+
after=PLAIN % "改写版",
|
|
551
|
+
reason="时点判别", primitive="add")
|
|
552
|
+
pr = autonomy_modes.propose_mutation(cg, "C", "t_1", payload=pay,
|
|
553
|
+
layer="knowledge", info=True)
|
|
554
|
+
cg.add("t_1", PLAIN % "第三方版", layer="knowledge",
|
|
555
|
+
verification_basis="test")
|
|
556
|
+
third_bytes = _bytes(_disk_path(cg, "t_1"))
|
|
557
|
+
d = cg.review_decide(pr["pid"], "accept", reason="守卫")
|
|
558
|
+
m = d.get("mutation") or {}
|
|
559
|
+
ok((m.get("before") or "") != old_rel,
|
|
560
|
+
"T 执行记录前像**不是**最旧的那张(新拍的执行时点快照)",
|
|
561
|
+
m.get("before"))
|
|
562
|
+
rr = rollback.rollback_mutation(cg, m, reason="T 时点判别")
|
|
563
|
+
ok(rr.get("ok") is True and rr.get("bytes_equal_preimage") is True,
|
|
564
|
+
"T 时点判别场景回滚成功且逐字节==执行时点前像",
|
|
565
|
+
{k: rr.get(k) for k in ("ok", "bytes_equal_preimage")})
|
|
566
|
+
ok(_bytes(_disk_path(cg, "t_1")) == third_bytes
|
|
567
|
+
and _bytes(_disk_path(cg, "t_1"))
|
|
568
|
+
!= _bytes(_pre_path(cg, old_rel)),
|
|
569
|
+
"T 回滚到**执行时点**(第三方版)而非最旧快照(初版)——"
|
|
570
|
+
"「前像=执行时点」有判别力")
|
|
571
|
+
cg.close()
|
|
572
|
+
# 未执行的变更单不可回滚(reject 后 CLI fail-closed)
|
|
573
|
+
with _mode("confirm"):
|
|
574
|
+
cg = _lib("unexec")
|
|
575
|
+
cg.add("u_1", PLAIN % "甲", layer="knowledge",
|
|
576
|
+
verification_basis="test")
|
|
577
|
+
cg.flush()
|
|
578
|
+
pay = autonomy_modes.mutation_payload("C", "u_1", after=PLAIN % "乙",
|
|
579
|
+
reason="未执行", primitive="add")
|
|
580
|
+
pr = autonomy_modes.propose_mutation(cg, "C", "u_1", payload=pay,
|
|
581
|
+
layer="knowledge", info=True)
|
|
582
|
+
cg.review_decide(pr["pid"], "reject", reason="守卫:不执行")
|
|
583
|
+
got = _run_command(rollback.command_for(cg.root, pr["pid"]))
|
|
584
|
+
out = got.get("json") or {}
|
|
585
|
+
ok(got["rc"] == 1 and out.get("ok") is False
|
|
586
|
+
and out.get("error") == "decision_not_found",
|
|
587
|
+
"T 未执行(reject)的变更单:回滚命令 fail-closed(无前像可依)",
|
|
588
|
+
{"rc": got["rc"], "json": out})
|
|
589
|
+
cg.close()
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
_GROUPS = (g_p, g_q, g_r, g_s, g_t)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _run_groups():
|
|
596
|
+
_PASS.clear()
|
|
597
|
+
_FAIL.clear()
|
|
598
|
+
for g in _GROUPS:
|
|
599
|
+
g()
|
|
600
|
+
return len(_FAIL)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
# =============================================================== 定点变异自证
|
|
604
|
+
# 表内每项 = (说明, 目标, 锚点原文, 替换文, 预期红项数)。锚点须**逐字**出现在
|
|
605
|
+
# 目标函数源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。基线源=**工作区
|
|
606
|
+
# 源码 + 本表**(绝不取 git HEAD——基线绑提交即失效,本仓已有两次教训)。
|
|
607
|
+
_SRC_MUTATIONS = (
|
|
608
|
+
# ① 执行桥不拍前像——回滚句柄不存在(回滚命令 read 不到 before、回滚
|
|
609
|
+
# gap;影响面/命令串亦缺)。红 30(实测 2026-10-02):P 组 3 条(受保护
|
|
610
|
+
# 面执行桥判据)+ Q 组 12 条 + R 组 7 条 + S 组 6 条 + T 组 2 条——三条
|
|
611
|
+
# 动作的「前像在案/回滚成功/逐字节/残留/命令串演练」判据整体转红。
|
|
612
|
+
("执行桥不拍前像(执行时点前像整支失效)", "cls", mdcos.MdCGOS,
|
|
613
|
+
"_mutation_execute",
|
|
614
|
+
' pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),\n'
|
|
615
|
+
' reason="变更单执行时点前像(%s→%s,pid=%s)"\n'
|
|
616
|
+
' % (act, tgt, item.get("pid") or "?"))',
|
|
617
|
+
' pre = None', 30),
|
|
618
|
+
# ② 前像取最旧快照(=「提议时点」错误形态)——T 组时点判别场景须转红。
|
|
619
|
+
# 红 2(实测):T「前像不是最旧那张」+ T「回滚到执行时点(非最旧)」;
|
|
620
|
+
# 其余场景无预置旧快照(_hist 空 → 照旧新拍),语义相符。
|
|
621
|
+
("前像取最旧快照(提议时点语义回归)", "mod", rollback, "preimage",
|
|
622
|
+
' rel = protect.snapshot_preimage(cg, target, action=act, pid=pid,\n'
|
|
623
|
+
' reason=reason)',
|
|
624
|
+
' _hist = protect.history(cg, target)\n'
|
|
625
|
+
' rel = _hist[0] if _hist else protect.snapshot_preimage(\n'
|
|
626
|
+
' cg, target, action=act, pid=pid, reason=reason)', 2),
|
|
627
|
+
# ③ 定向剥离恒等空转——聚合行剥离失效(回滚仍以快照正文兜底,故逐字节
|
|
628
|
+
# 不断;剥离读数与 fm 回退判据须红)。红 1(实测):R「走定向剥离且剥离
|
|
629
|
+
# 精确」——stripped_ok False/method=preimage/removed 空 三条件同挂一条判据。
|
|
630
|
+
("定向剥离恒等空转(strip_aggregate_lines 不剥)", "mod", rollback,
|
|
631
|
+
"strip_aggregate_lines",
|
|
632
|
+
' if not ref or not text.startswith(ref):\n return text, []',
|
|
633
|
+
' return text, []', 1),
|
|
634
|
+
# ④ 回滚不落盘(_land 空转)——回滚「报成功了」但盘面没回(汇报不实)。
|
|
635
|
+
# 红 13(实测):Q 5 条(逐字节/残留/盘面/索引对拍/边恢复)+ R 4 条
|
|
636
|
+
# + S 4 条(逐字节/复位/残留/命令串演练后盘面)。
|
|
637
|
+
("回滚不落盘(_land 空转)", "mod", rollback, "_land",
|
|
638
|
+
' cg._write_node(node_id, path, fm, content)', ' return', 13),
|
|
639
|
+
# ⑤ D 回滚不做前像校准——restore 的字节差(sensitivity:null)留在盘面。
|
|
640
|
+
# 红 2(实测):S「读数如实(calibrated/restore_bytes_equal)」+ S
|
|
641
|
+
# 「回滚后盘面逐字节==前像」。
|
|
642
|
+
("D 回滚不做前像校准(restore 字节差残留)", "mod", rollback,
|
|
643
|
+
"_rollback_delete",
|
|
644
|
+
' if not restore_bytes_equal:', ' if False:', 2),
|
|
645
|
+
# ⑥ 前像挂钩被塞进受保护路径(_allow 多拍一张)——受保护节点既有快照行为
|
|
646
|
+
# 被污染。红 4(实测):P「guard_write 快照恰好 1 张」+ P「审计面各一条」
|
|
647
|
+
# + P「guard_forget 同款口径」+ P「执行桥场景 history 恰 2 条(微秒/秒级
|
|
648
|
+
# 各一)」——四条的读数都被多出的一张打乱。
|
|
649
|
+
("受保护路径被污染(_allow 多拍前像)", "mod", protect, "_allow",
|
|
650
|
+
' snap = snapshot(cg, node_id)',
|
|
651
|
+
' snap = snapshot(cg, node_id)\n'
|
|
652
|
+
' snapshot_preimage(cg, node_id, action="guard", pid=action)', 4),
|
|
653
|
+
# ⑦ 前像文件名去微秒(同秒互踩)——同一节点同秒两次前像互相覆盖,
|
|
654
|
+
# 早先的回滚句柄被踩掉。红 4(实测):P「同秒两次=两个文件」+ P「两张
|
|
655
|
+
# 各拍各自时点」+ P「既有 snapshot 命名形态不变」(微秒名与秒级名的
|
|
656
|
+
# 判别被打乱)+ P「执行桥场景微秒/秒级各一条」。
|
|
657
|
+
("前像名去微秒(同秒互踩)", "mod", protect, "snapshot_preimage",
|
|
658
|
+
' ts = (time.strftime("%Y%m%d-%H%M%S", time.localtime())\n'
|
|
659
|
+
' + "-%06d" % (time.time_ns() // 1000 % 1000000))',
|
|
660
|
+
' ts = time.strftime("%Y%m%d-%H%M%S", time.localtime())', 4),
|
|
661
|
+
# ⑧ 执行记录不落 mutation(_record_decision 摘除)——回滚 CLI 读不到载荷
|
|
662
|
+
# (「回滚命令」的读取源断链)。红 3(实测):Q/R/S 三条「命令串实测
|
|
663
|
+
# 演练」——CLI 报 decision_not_found 非零退出;进程内入口不受影响
|
|
664
|
+
# (直接持有载荷),语义相符。
|
|
665
|
+
("执行记录不落 mutation(rec['mutation'] 摘除)", "cls", mdcos.MdCGOS,
|
|
666
|
+
"_record_decision",
|
|
667
|
+
' _mut = result.get("mutation")\n'
|
|
668
|
+
' if isinstance(_mut, dict):\n'
|
|
669
|
+
' rec["mutation"] = _mut',
|
|
670
|
+
' _mut = result.get("mutation")\n'
|
|
671
|
+
' if False:\n'
|
|
672
|
+
' rec["mutation"] = _mut', 3),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _fn_src(target):
|
|
677
|
+
kind, owner, name = target
|
|
678
|
+
if kind == "attr":
|
|
679
|
+
return inspect.getsource(owner)
|
|
680
|
+
return inspect.getsource(getattr(owner, name))
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _strip_indent(text, n):
|
|
684
|
+
if not n:
|
|
685
|
+
return text
|
|
686
|
+
out = []
|
|
687
|
+
for line in text.split("\n"):
|
|
688
|
+
if line[:n].strip() == "":
|
|
689
|
+
out.append(line[n:])
|
|
690
|
+
else:
|
|
691
|
+
out.append(line.lstrip())
|
|
692
|
+
return "\n".join(out)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _anchor_check():
|
|
696
|
+
bad = []
|
|
697
|
+
for name, _kind, owner, fname, old, _new, _n in _SRC_MUTATIONS:
|
|
698
|
+
if old not in _fn_src((_kind, owner, fname)):
|
|
699
|
+
bad.append("变异锚点缺失:%r @%s.%s" % (old[:40], owner.__name__, fname))
|
|
700
|
+
return bad
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
@contextlib.contextmanager
|
|
704
|
+
def _patched(target, old, new):
|
|
705
|
+
kind, owner, name = target
|
|
706
|
+
src = _fn_src(target)
|
|
707
|
+
cut = len(src) - len(src.lstrip(" "))
|
|
708
|
+
src2 = _strip_indent(src, cut)
|
|
709
|
+
old2 = _strip_indent(old, cut)
|
|
710
|
+
new2 = _strip_indent(new, cut)
|
|
711
|
+
if old2 not in src2:
|
|
712
|
+
raise AssertionError("变异锚点在去缩进后仍不匹配:%r" % old[:40])
|
|
713
|
+
src = src2.replace(old2, new2)
|
|
714
|
+
if kind == "mod":
|
|
715
|
+
ns = dict(vars(owner))
|
|
716
|
+
ns["__name__"] = owner.__name__
|
|
717
|
+
else:
|
|
718
|
+
ns = dict(vars(sys.modules[owner.__module__]))
|
|
719
|
+
ns["__name__"] = owner.__module__
|
|
720
|
+
exec(compile(src, "mr_mut.py", "exec"), ns)
|
|
721
|
+
live = getattr(owner, name)
|
|
722
|
+
setattr(owner, name, ns[name])
|
|
723
|
+
try:
|
|
724
|
+
yield
|
|
725
|
+
finally:
|
|
726
|
+
setattr(owner, name, live)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _mutate_mode():
|
|
730
|
+
bad = []
|
|
731
|
+
anchor_bad = _anchor_check()
|
|
732
|
+
if anchor_bad:
|
|
733
|
+
for b in anchor_bad:
|
|
734
|
+
print(" ANCHOR-MISS " + b)
|
|
735
|
+
print("\n锚点自检:FAIL(fail-closed,exit 2)")
|
|
736
|
+
return 2
|
|
737
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
738
|
+
clean = _run_groups()
|
|
739
|
+
print(" 未变异基线:红项=%d%s"
|
|
740
|
+
% (clean, "" if clean == 0 else " ← 基线即红,变异核验无意义"))
|
|
741
|
+
if clean:
|
|
742
|
+
bad.append("未变异基线即失败")
|
|
743
|
+
_RUN[0] = 10 ** 6
|
|
744
|
+
for name, kind, owner, fname, old, new, expect in _SRC_MUTATIONS:
|
|
745
|
+
_RUN[0] += 1
|
|
746
|
+
try:
|
|
747
|
+
with _patched((kind, owner, fname), old, new), \
|
|
748
|
+
contextlib.redirect_stdout(io.StringIO()):
|
|
749
|
+
fails = _run_groups()
|
|
750
|
+
except Exception as exc: # noqa: BLE001 —— 变异把路径打断也算「红」
|
|
751
|
+
fails = -1
|
|
752
|
+
print(" 变异「%s」→ 断言链抛异常 %s: %s(判 FAIL)"
|
|
753
|
+
% (name, type(exc).__name__, str(exc)[:80]))
|
|
754
|
+
verdict = ("命中预期" if fails == expect
|
|
755
|
+
else "**红项数不符(预期 %d)**" % expect)
|
|
756
|
+
print(" 变异「%s」→ 红项=%d %s" % (name, fails, verdict))
|
|
757
|
+
for f in _FAIL[:6]:
|
|
758
|
+
print(" 红:", f)
|
|
759
|
+
if len(_FAIL) > 6:
|
|
760
|
+
print(" …(余 %d 项)" % (len(_FAIL) - 6))
|
|
761
|
+
if fails != expect:
|
|
762
|
+
bad.append(name)
|
|
763
|
+
print("\n定点变异自证:%s"
|
|
764
|
+
% ("PASS(每处判据都有变异钉死,且红项数逐处吻合)" if not bad
|
|
765
|
+
else "FAIL —— " + "、".join(bad)))
|
|
766
|
+
return 0 if not bad else 1
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def main() -> int:
|
|
770
|
+
src = os.path.basename(os.path.abspath(__file__))
|
|
771
|
+
if "--mutate" in sys.argv:
|
|
772
|
+
print("!! 定点变异模式:逐个变异判据,套件应转红且红项数吻合\n")
|
|
773
|
+
try:
|
|
774
|
+
return _mutate_mode()
|
|
775
|
+
finally:
|
|
776
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
777
|
+
anchor_bad = _anchor_check()
|
|
778
|
+
if anchor_bad:
|
|
779
|
+
for b in anchor_bad:
|
|
780
|
+
print(" ANCHOR-MISS " + b)
|
|
781
|
+
print("\n锚点自检:FAIL(fail-closed,exit 2)——实现改了请同步变异表")
|
|
782
|
+
return 2
|
|
783
|
+
print("锚点自检:PASS(%s;不以 git HEAD 为基线源)" % src)
|
|
784
|
+
try:
|
|
785
|
+
for g in _GROUPS:
|
|
786
|
+
g()
|
|
787
|
+
finally:
|
|
788
|
+
_RUN[0] += 1
|
|
789
|
+
print("\n变更单回滚面守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
|
|
790
|
+
if _FAIL:
|
|
791
|
+
for f in _FAIL:
|
|
792
|
+
print(" 红:", f)
|
|
793
|
+
return 0 if not _FAIL else 1
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
if __name__ == "__main__":
|
|
797
|
+
try:
|
|
798
|
+
rc = main()
|
|
799
|
+
finally:
|
|
800
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
801
|
+
os.environ.pop("MDCG_POLICY_FILE", None)
|
|
802
|
+
if _OLD_POLICY is not None:
|
|
803
|
+
os.environ["MDCG_POLICY_FILE"] = _OLD_POLICY
|
|
804
|
+
os.environ.pop(_ENV_MODE, None)
|
|
805
|
+
sys.exit(rc)
|