@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,1972 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · 三档自治「档位单一入口」守卫(设计 v0.2 §三/§四/§五 · 批次②)
|
|
3
|
+
|
|
4
|
+
覆盖(设计 §十一 验收判据在本批的落点):
|
|
5
|
+
· **逐格矩阵**(3 档 × 5 动作类 A/B/C/D/E)+ 缺省档 = confirm(裁定①);
|
|
6
|
+
· 非法 env 值 fail-closed(报错 + hint 列合法值,**不**静默降级);
|
|
7
|
+
· plan 档最小语义:无计划时「须命中计划步骤」的 A/B/E 一律 forbid
|
|
8
|
+
(计划输入面待后续批次;硬约束③ 计划外零变更);
|
|
9
|
+
· **B/C/D 出单**(三条热路径各一:写链覆写 C / 遗忘与限流合并 B / 工具面删除 D)
|
|
10
|
+
—— 出单且**不落盘**,载荷含动作类·目标·后像·理由;
|
|
11
|
+
· **accept 执行**(B/C/D 三种动作各一)+ **reject 不执行**(原样留痕);
|
|
12
|
+
· edit/merge/noop 对变更单 fail-closed(不静默当已处理);
|
|
13
|
+
· 幂等:同(动作类+目标+后像)不长第二条;不同目标**不撞键**;
|
|
14
|
+
· 存量兼容:旧条目无 `kind` 键一律视为 proposal(零迁移),accept 路径逐位不变;
|
|
15
|
+
· **不动面对拍**(设计 §十一):
|
|
16
|
+
① full 档下闸门对全部动作类返回 allow ⇒ 与改动前**同一条链**(构造性等价
|
|
17
|
+
——本基线**不绑 git HEAD**,本仓已有两次教训);
|
|
18
|
+
② confirm 档下 A 新增 / E 权重 的动作面读数与 full 档**逐位相等**;
|
|
19
|
+
③ 库层(`MdCG.add` / `_write_node` / `MdCGOS.forget`)与合并原语
|
|
20
|
+
(`forgetting.reinforce` / `writelimit.converge_into`)源码里**不得**出现
|
|
21
|
+
档位判据——「直调库层不接线」是 accept 执行桥「越过确认判定」的机制前提。
|
|
22
|
+
|
|
23
|
+
**补强批次(2026-10-02)新增 I/J 两组**(独立复核三条未收口 + 容器栈一门禁红):
|
|
24
|
+
· I 组 **gated 面**(`remember_gated` 的 ACCEPT 分支 = 插件主写入通道):
|
|
25
|
+
- C 覆写(既有 node_id):confirm/plan 档出变更单且不落盘、full 档照旧直落;
|
|
26
|
+
资格在先——受保护节点覆写在 `write_qualify` 处当场拦(不静默入队);
|
|
27
|
+
- A 新增(node_id 不存在):confirm/full 档**逐位零变化**(返回体与落盘节点
|
|
28
|
+
都对拍),plan 档 fail-closed(`autonomy_forbidden`,带 hint);
|
|
29
|
+
- 链面(`op=write` + `gated=true`,`_gate_gated`)与 MCP 工具面
|
|
30
|
+
(`mdcg_remember(gated=true)`)同判据——插件主通道不再旁路确认档;
|
|
31
|
+
- 执行桥 C:目标在 accept 前消失 ⇒ `target_missing` 且**不重建**节点
|
|
32
|
+
(对照片:目标在位时照旧执行);
|
|
33
|
+
· J 组 **容器栈一 SHIM-MISS 修复面**:`test_policy_required_ccg` 的 `_SHIMS`
|
|
34
|
+
登记(writepipe 档位闸的相对 import)+ 该腿 `--head-baseline` 整腿 PASS +
|
|
35
|
+
**负对照**(子进程内把 `_SHIMS` 去掉 `autonomy_modes` ⇒ 必须 SHIM-MISS
|
|
36
|
+
非零退出——J3 的绿不是空转)。
|
|
37
|
+
|
|
38
|
+
**批次③(2026-10-02)新增 K 组**(设计 §四 变更单三字段 · 本批扩展):
|
|
39
|
+
· K 组 **变更单字段完备**:载荷在(动作类/目标/后像/理由/primitive/meta)之上
|
|
40
|
+
的 `before`(前像)/`impact`(影响面)/`rollback`(回滚命令)三键——
|
|
41
|
+
提议时点恒在且占位(None/None/"")、**执行时点**补全(before 指向执行前一刻
|
|
42
|
+
的盘面:第三方改动被保留)、impact 三面(边/索引/聚合行)齐备、
|
|
43
|
+
rollback 为可执行命令串(含 `md_cg.rollback_cli` 与 pid)、decisions.jsonl
|
|
44
|
+
的 `rec["mutation"]` 落盘。深度回滚演练见 `md_cg/test_mutation_rollback.py`。
|
|
45
|
+
|
|
46
|
+
**批次④(2026-10-02)新增 L 组**(设计 §六/§七 准入闸 · 本批扩展):
|
|
47
|
+
· L 组 **准入闸生效面**:`env MDCG_AUTONOMY=full` 而 R1–R4 读数不满足 ⇒
|
|
48
|
+
`settle()` 的实际生效档位回落 confirm 且 `alerts` 逐条报「缺哪条读数」
|
|
49
|
+
(不静默降级);full+满足 ⇒ full(与改动前逐位一致);plan/confirm
|
|
50
|
+
不受读数影响(零 IO 零告警);非法 env 仍 fail-closed;未结算 = 旧行为
|
|
51
|
+
(对拍);读数缓存 TTL 内不重扫(「不得让每次 decide 都全库扫描」);
|
|
52
|
+
只读(结算前后库指纹逐位相同)。四条读数本身的深度判据(门槛边界 /
|
|
53
|
+
窗口边界 3 天·30 天)见独立守卫 `md_cg/test_autonomy_admission.py`。
|
|
54
|
+
|
|
55
|
+
**收官批次(步骤⑤,2026-10-03)新增 M 组**(设计 §十三.5 步骤⑤ / §十一 验收):
|
|
56
|
+
· M 组 **覆盖矩阵未覆盖格补缺**——(a)plan 档热路径四面:写链 C 覆写 /
|
|
57
|
+
gated 面 B 合并(MERGE 落点)/ 限流闸 CONVERGE 落点 / 工具面 D 删除
|
|
58
|
+
(此前 plan 档只有 decide 级判据与 gated A 新增);(b)**E 分数类在档位面**
|
|
59
|
+
的行为读数。
|
|
60
|
+
confirm/full 两档的 A/E 逐位对拍、与旧基线的 `git archive` oracle 对拍、
|
|
61
|
+
端到端回滚演练(跨写链/插件面/执行桥/回滚 CLI)在独立守卫
|
|
62
|
+
`md_cg/test_mode_parity.py`(收官批次另立;覆盖矩阵见收官报告)。
|
|
63
|
+
|
|
64
|
+
**补强批次(v1.2,2026-10-03)扩展 M 组**(独立复核 DEFER 的解除项 U1/U2 + E 面接线):
|
|
65
|
+
· **M1e 直写面三档**(U1):`mdcg_remember` **非 gated 直写分支**
|
|
66
|
+
(`mcp_server.py:3373` 的自注「热写入路径的第二落点」)——plan/confirm 两档
|
|
67
|
+
C 覆写**出单且不落盘**、full 档**直落**(此前该分支两档均无行为断言,复核
|
|
68
|
+
V4' 注入 0 红存活);
|
|
69
|
+
· **M2 按标题意图重编码**:E 面**已接线**(`autonomy_modes.e_gate` 接在
|
|
70
|
+
`weights.recalc` / `freshness.recalc` / `lifecycle.set_state` /
|
|
71
|
+
`lifecycle.backfill` 的 apply 路径)⇒ 断言由「plan 与 confirm 逐位相等
|
|
72
|
+
(现状钉、非背书)」重编码为「**plan 被拦(fail-closed、零写盘)+
|
|
73
|
+
confirm/full 逐位相等**」——**断言意图不变**(同 issue50-a 的 E1 先例:
|
|
74
|
+
改的是判据编码,不是判据意图)。
|
|
75
|
+
|
|
76
|
+
定点变异自证(`--mutate`,与 `md_cg/test_neg_condition_hits.py` 同口径):
|
|
77
|
+
表内每项 = (说明, 目标, 锚点原文, 替换文, 预期红项数)。锚点须**逐字**出现在目标
|
|
78
|
+
函数源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。红项数与实测**逐一相符**
|
|
79
|
+
才算通过——不符即 FAIL(多红=断言越界、少红=该判据无判别力)。
|
|
80
|
+
目标形态三种:`("mod", 模块, 函数名)` / `("cls", 类, 方法名)` 走**源码替换 + exec**
|
|
81
|
+
重装;`("attr", 模块, 属性名)` 是**常量型变异**(把属性换成 `替换文` 求值出的新值,
|
|
82
|
+
用于 `_SHIMS` 这类「判据是数据不是函数体」的落点)。
|
|
83
|
+
|
|
84
|
+
**F1 收口(2026-10-03)——防误删自检**:变异表第③条(矩阵整表坍缩)曾在工作树
|
|
85
|
+
被删(仅剩注释;删除原因无留痕、不归因);本批自 `git show HEAD:` 取回、按现行树
|
|
86
|
+
勘定锚点并实测校准红项数。新增**变异表完整性自检**(`_table_gaps` /
|
|
87
|
+
`_table_integrity_check`:编号无缺口 + 表长与显式声明 `_MUTATION_IDS` 一致;
|
|
88
|
+
缺项 ⇒ fail-closed 退出码 2 并报缺口编号)——今后删条目即机械报错,不再靠人工
|
|
89
|
+
发现;「24 处」由此获得**自动核验载体**(每次运行 F1③ 测例自动核验)。自检的
|
|
90
|
+
判别力由合成源自证测例(F1①–④)+ 内置判别力钉 `_SELFCHECK_MUTATION`
|
|
91
|
+
(剥掉自检开关 ⇒ 自证测例转红 **3**;不占表内编号——表长与声明恒等)钉死。
|
|
92
|
+
三级判据(注释被覆盖 / 序列缺编号 / 表长不符)分别对应删元组留注释、
|
|
93
|
+
注释与元组同删、编号齐而条目缺三种删法。
|
|
94
|
+
|
|
95
|
+
运行:
|
|
96
|
+
python -X utf8 -m md_cg.test_autonomy_modes # 正常跑
|
|
97
|
+
python -X utf8 -m md_cg.test_autonomy_modes --mutate # 定点变异自证
|
|
98
|
+
|
|
99
|
+
沙箱(硬约束):一切读写都在 tempfile.mkdtemp 内(含 MDCG_AUX_ROOT / 主密钥——
|
|
100
|
+
crypto 在导入期求值 MASTER_FILE,故必须在任何 md_cg 子模块 import **之前**设好);
|
|
101
|
+
跑完 rmtree。**绝不碰在役数据根**。档位 env 由夹具按需设置并在 finally 还原。
|
|
102
|
+
边界(如实,2026-10-02 补强批次实测):`rmtree(..., ignore_errors=True)` 在 Windows 上
|
|
103
|
+
可能被未释放的锁句柄挡住,每次跑约残留 1 个**空壳**沙箱目录(实测内容仅
|
|
104
|
+
`<lib>/_index.json` 与 `.lock` 两个文件,全为合成库、零真实数据);补强批次已清理
|
|
105
|
+
本会话累积的 30 个,清理逻辑未改(根因未定,留池登记)。
|
|
106
|
+
"""
|
|
107
|
+
from __future__ import annotations
|
|
108
|
+
|
|
109
|
+
import contextlib
|
|
110
|
+
import inspect
|
|
111
|
+
import io
|
|
112
|
+
import json
|
|
113
|
+
import os
|
|
114
|
+
import re
|
|
115
|
+
import shutil
|
|
116
|
+
import sys
|
|
117
|
+
import tempfile
|
|
118
|
+
import time
|
|
119
|
+
|
|
120
|
+
# ---- 沙箱:必须在任何 md_cg 子模块 import 之前 ------------------------------
|
|
121
|
+
_SANDBOX = tempfile.mkdtemp(prefix="autonomy_modes_sandbox_")
|
|
122
|
+
os.environ["MDCG_AUX_ROOT"] = _SANDBOX
|
|
123
|
+
os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
|
|
124
|
+
os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
|
|
125
|
+
os.environ.pop("MDCG_TEST_LIVE_ROOT", None)
|
|
126
|
+
_OLD_POLICY = os.environ.pop("MDCG_POLICY_FILE", None)
|
|
127
|
+
|
|
128
|
+
from . import autonomy_modes, forgetting, writelimit, writepipe # noqa: E402
|
|
129
|
+
from . import freshness # noqa: E402
|
|
130
|
+
from . import weights # noqa: E402
|
|
131
|
+
from . import lifecycle as _lc # noqa: E402
|
|
132
|
+
from . import mcp_server # noqa: E402
|
|
133
|
+
from . import nodefile as _nf # noqa: E402
|
|
134
|
+
from . import mdcos # noqa: E402
|
|
135
|
+
from . import test_policy_required_ccg as _tpc # noqa: E402
|
|
136
|
+
from .mdcos import MdCGOS, MdCGSecure # noqa: E402
|
|
137
|
+
from .mdcg import MdCG # noqa: E402
|
|
138
|
+
from .security import Principal # noqa: E402
|
|
139
|
+
|
|
140
|
+
#: 档位 env 键名——**不写字面量**:从真源表取(全仓唯一字面量落点在本模块之外的
|
|
141
|
+
#: autonomy_modes.AUTONOMY_ENV_KEYS,G 组把它 grep 钉死)。夹具因此不构成第二处
|
|
142
|
+
#: 字面量,守卫的 grep 判据得以保持严格。
|
|
143
|
+
_ENV_MODE = autonomy_modes.AUTONOMY_ENV_KEYS["mode"]
|
|
144
|
+
|
|
145
|
+
_PASS = []
|
|
146
|
+
_FAIL = []
|
|
147
|
+
_RUN = [0]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ok(cond, msg, extra=""):
|
|
151
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
152
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
153
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _policy():
|
|
157
|
+
"""审计闸要一份可用规则库(required 命中即 ACCEPT,同 test_writepipe 口径)。"""
|
|
158
|
+
path = os.path.join(_SANDBOX, "policy.json")
|
|
159
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
160
|
+
json.dump({"forbidden": ["FORBIDDEN_WORD"], "required": ["PASSED"]}, f)
|
|
161
|
+
os.environ["MDCG_POLICY_FILE"] = path
|
|
162
|
+
return path
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _lib(tag="lib"):
|
|
166
|
+
"""独立合成库(每轮独立子根:变异模式连跑多轮,共用目录会读到上一轮盘面)。"""
|
|
167
|
+
return MdCGOS(os.path.join(_SANDBOX, "%s_r%d" % (tag, _RUN[0])), autoflush=0)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@contextlib.contextmanager
|
|
171
|
+
def _mode(m):
|
|
172
|
+
"""按需设档位,退出还原(夹具专用;不在生产码里读/写档位)。"""
|
|
173
|
+
old = os.environ.get(_ENV_MODE)
|
|
174
|
+
os.environ[_ENV_MODE] = m
|
|
175
|
+
try:
|
|
176
|
+
yield m
|
|
177
|
+
finally:
|
|
178
|
+
if old is None:
|
|
179
|
+
os.environ.pop(_ENV_MODE, None)
|
|
180
|
+
else:
|
|
181
|
+
os.environ[_ENV_MODE] = old
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@contextlib.contextmanager
|
|
185
|
+
def _no_mode():
|
|
186
|
+
"""清掉档位 env(考「缺省 = confirm」)。"""
|
|
187
|
+
old = os.environ.pop(_ENV_MODE, None)
|
|
188
|
+
try:
|
|
189
|
+
yield
|
|
190
|
+
finally:
|
|
191
|
+
if old is not None:
|
|
192
|
+
os.environ[_ENV_MODE] = old
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _pipe():
|
|
196
|
+
"""新建默认链(档位闸是注册期绑定,故每组现建,变异才对写链生效)。"""
|
|
197
|
+
return writepipe.install_default_gates(writepipe.WritePipeline())
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
#: 对拍要剔除的**时钟面/耗时面**键(两次运行必然不同,与档位无关——剔除不算
|
|
201
|
+
#: 放水:对拍的判据是「档位有没有改变行为」,不是「时间戳/耗时是否相同」)。
|
|
202
|
+
#: 收官批次(步骤⑤)追加三项,均为同一族:`batch`(strftime 派生的批号)、
|
|
203
|
+
#: `note`(文案里内嵌 batch 的提示句,与 batch 同源)、`elapsed_ms`
|
|
204
|
+
#: (运行耗时读数,性能面——M 组 E 分数类对拍用)。
|
|
205
|
+
_VOLATILE = ("t", "created_at", "last_access", "time_window", "last_merge_at",
|
|
206
|
+
"batch", "note", "elapsed_ms")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _strip(d):
|
|
210
|
+
"""递归剔除时钟面键,返回可逐位比较的结构。"""
|
|
211
|
+
if isinstance(d, dict):
|
|
212
|
+
return {k: _strip(v) for k, v in d.items() if k not in _VOLATILE}
|
|
213
|
+
if isinstance(d, list):
|
|
214
|
+
return [_strip(x) for x in d]
|
|
215
|
+
return d
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
BODY = ("# 功能名:部署面端口约定\n"
|
|
219
|
+
"# 生效条件:载体/位置:prod 集群;时间:2026-09-30 起;方法:部署清单核对;约束:无\n"
|
|
220
|
+
"# 子功能:登记各服务监听端口\n"
|
|
221
|
+
"# 执行:核对 manifest 的 ports 段\n"
|
|
222
|
+
"# 验证方式:test\n"
|
|
223
|
+
"# 不适用条件:无\n"
|
|
224
|
+
"\n网关监听端口 8080/HTTP;管理面监听端口 8081/HTTP;"
|
|
225
|
+
"指标面监听端口 %s/HTTP;日志面监听端口 9091/HTTP\n")
|
|
226
|
+
PLAIN = "PASSED 探针正文 %s"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# =============================================================== A 组:矩阵
|
|
230
|
+
def g_a():
|
|
231
|
+
print("== A 组:裁决矩阵逐格(3 档 × 5 动作类)==")
|
|
232
|
+
want = {
|
|
233
|
+
"plan": {"A": "forbid", "B": "forbid", "C": "confirm",
|
|
234
|
+
"D": "confirm", "E": "forbid"}, # 无计划 ⇒ 须计划步骤者 forbid
|
|
235
|
+
"confirm": {"A": "allow", "B": "confirm", "C": "confirm",
|
|
236
|
+
"D": "confirm", "E": "allow"}, # 设计 §三 缺省列
|
|
237
|
+
"full": {"A": "allow", "B": "allow", "C": "allow",
|
|
238
|
+
"D": "allow", "E": "allow"},
|
|
239
|
+
}
|
|
240
|
+
for m in autonomy_modes.AUTONOMY_MODES:
|
|
241
|
+
for a in autonomy_modes.ACTION_CLASSES:
|
|
242
|
+
d = autonomy_modes.decide(a, mode_explicit=m)
|
|
243
|
+
ok(d["decision"] == want[m][a] and d["mode"] == m
|
|
244
|
+
and d["action"] == a,
|
|
245
|
+
"A 矩阵格 %s×%s → %s" % (m, a, want[m][a]), d)
|
|
246
|
+
# 矩阵真源 = MATRIX 表(逐格与上表同源,防「表改了判据没改」)
|
|
247
|
+
for m in autonomy_modes.AUTONOMY_MODES:
|
|
248
|
+
for a in autonomy_modes.ACTION_CLASSES:
|
|
249
|
+
cell = autonomy_modes.MATRIX[m][a]
|
|
250
|
+
ok(cell in (autonomy_modes.ALLOW, autonomy_modes.CONFIRM,
|
|
251
|
+
autonomy_modes.PLAN_STEP),
|
|
252
|
+
"A 表内格 %s×%s 是合法格值(%s)" % (m, a, cell), cell)
|
|
253
|
+
d = autonomy_modes.decide("B", mode_explicit="confirm")
|
|
254
|
+
ok(bool(d["hint"]) and "变更单" in d["hint"],
|
|
255
|
+
"A confirm 判定带可读 hint(出变更单 + 裁决入口)", d["hint"][:40])
|
|
256
|
+
with _no_mode():
|
|
257
|
+
ok(autonomy_modes.mode() == "confirm",
|
|
258
|
+
"A 缺省档 = confirm(env 未设)", autonomy_modes.mode())
|
|
259
|
+
ok(autonomy_modes.DEFAULT_MODE == "confirm"
|
|
260
|
+
and autonomy_modes.AUTONOMY_ENV_DEFAULTS["mode"] == "confirm",
|
|
261
|
+
"A 缺省真源 = confirm(表与具名常量同源)")
|
|
262
|
+
with _mode("bogus"):
|
|
263
|
+
try:
|
|
264
|
+
autonomy_modes.mode()
|
|
265
|
+
ok(False, "A 非法 env fail-closed(未报错)")
|
|
266
|
+
except autonomy_modes.AutonomyModeError as e:
|
|
267
|
+
ok("bogus" in str(e) and "plan" in str(e) and "confirm" in str(e)
|
|
268
|
+
and "full" in str(e),
|
|
269
|
+
"A 非法 env fail-closed:报错 + hint 列合法值", str(e)[:50])
|
|
270
|
+
with _mode(" FULL "):
|
|
271
|
+
ok(autonomy_modes.mode() == "full",
|
|
272
|
+
"A 档位名去空白/大小写不敏感(归一后合法即生效)")
|
|
273
|
+
# plan 档:有计划且命中 ⇒ allow;有计划未命中 ⇒ forbid(计划外零变更)
|
|
274
|
+
d = autonomy_modes.decide("B", mode_explicit="plan", plan=["B", "A"])
|
|
275
|
+
ok(d["decision"] == "allow", "A plan 档命中计划步骤 → allow", d)
|
|
276
|
+
d = autonomy_modes.decide("A", mode_explicit="plan", plan={"steps": [{"action": "B"}]})
|
|
277
|
+
ok(d["decision"] == "forbid" and "计划外零变更" in d["hint"],
|
|
278
|
+
"A plan 档未命中计划步骤 → forbid(计划外零变更)", d)
|
|
279
|
+
d = autonomy_modes.decide("A", mode_explicit="plan")
|
|
280
|
+
ok(d["decision"] == "forbid" and "计划输入面待后续批次" in d["hint"],
|
|
281
|
+
"A plan 档无计划 → forbid 且 hint 说明计划输入面待后续批次", d["hint"][:50])
|
|
282
|
+
try:
|
|
283
|
+
autonomy_modes.decide("Z", mode_explicit="confirm")
|
|
284
|
+
ok(False, "A 未知动作类未拒")
|
|
285
|
+
except ValueError:
|
|
286
|
+
ok(True, "A 未知动作类 fail-closed(ValueError)")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# =============================================================== B 组:C 改写
|
|
290
|
+
def g_b():
|
|
291
|
+
print("== B 组:写链 C 改写(cg(op=write) 同 id 覆写)==")
|
|
292
|
+
_policy()
|
|
293
|
+
pipe = _pipe()
|
|
294
|
+
with _mode("confirm"):
|
|
295
|
+
cg = _lib("c")
|
|
296
|
+
nid = "c_b1"
|
|
297
|
+
o1 = pipe.execute(cg, {"content_kind": "text",
|
|
298
|
+
"content": PLAIN % "初版", "layer": "knowledge",
|
|
299
|
+
"node_id": nid})
|
|
300
|
+
ok(o1.get("committed") is True, "B 首次写入(A 新增)直落不拦", o1)
|
|
301
|
+
o2 = pipe.execute(cg, {"content_kind": "text",
|
|
302
|
+
"content": PLAIN % "改写版", "layer": "knowledge",
|
|
303
|
+
"node_id": nid})
|
|
304
|
+
ok(o2.get("moved_to") == "review_queue"
|
|
305
|
+
and o2.get("committed") is False and bool(o2.get("pid")),
|
|
306
|
+
"B C 改写出变更单且不落盘", {k: o2.get(k) for k in
|
|
307
|
+
("moved_to", "committed", "pid")})
|
|
308
|
+
ok("初版" in (cg.get(nid) or {}).get("content", "")
|
|
309
|
+
and "改写版" not in (cg.get(nid) or {}).get("content", ""),
|
|
310
|
+
"B 出单时目标节点正文**未被改写**(逐字比对)")
|
|
311
|
+
pay = o2.get("mutation") or {}
|
|
312
|
+
ok(pay.get("action") == "C" and pay.get("target") == nid
|
|
313
|
+
and pay.get("after") == (PLAIN % "改写版")
|
|
314
|
+
and bool(pay.get("reason")) and pay.get("primitive") == "add",
|
|
315
|
+
"B 载荷齐备:动作类 C · 目标 id · 后像正文 · 理由 · 复现原语", pay)
|
|
316
|
+
rec = [r for r in cg.review_list() if r.get("pid") == (o2.get("pid") or "")]
|
|
317
|
+
ok(len(rec) == 1 and autonomy_modes.order_kind(rec[0]) == "mutation"
|
|
318
|
+
and rec[0].get("id") == nid,
|
|
319
|
+
"B 队列条目 kind=mutation 且 id=目标节点(既有读取面可见)",
|
|
320
|
+
rec[0] if rec else None)
|
|
321
|
+
r = cg.review_decide(o2.get("pid") or "", "accept", reason="守卫")
|
|
322
|
+
ok(r.get("ok") is True, "B accept 返回 ok", r)
|
|
323
|
+
ok("改写版" in (cg.get(nid) or {}).get("content", ""),
|
|
324
|
+
"B accept **执行**改写:覆写落盘走既有 add")
|
|
325
|
+
# 前像式核对:accept 前盘面 == 旧正文(内容级「前像」由出单不落盘保证)
|
|
326
|
+
ok((cg.get(nid) or {}).get("content", "").count("改写版") == 1,
|
|
327
|
+
"B 改写只发生一次(幂等落盘)")
|
|
328
|
+
# full 档:直落(对拍锚:与改动前同一条路)
|
|
329
|
+
with _mode("full"):
|
|
330
|
+
o3 = pipe.execute(cg, {"content_kind": "text",
|
|
331
|
+
"content": PLAIN % "完全访问版",
|
|
332
|
+
"layer": "knowledge", "node_id": nid})
|
|
333
|
+
ok(o3.get("committed") is True and o3.get("moved_to") is None
|
|
334
|
+
and "完全访问版" in (cg.get(nid) or {}).get("content", ""),
|
|
335
|
+
"B full 档 C 改写直落(无单、无 extra 键)", o3)
|
|
336
|
+
# plan 档:A 新增 fail-closed(写链面)
|
|
337
|
+
with _mode("plan"):
|
|
338
|
+
o4 = pipe.execute(cg, {"content_kind": "text",
|
|
339
|
+
"content": PLAIN % "计划档", "layer": "knowledge",
|
|
340
|
+
"node_id": "c_plan_new"})
|
|
341
|
+
ok(o4.get("moved_to") == "autonomy_forbidden"
|
|
342
|
+
and o4.get("committed") is False
|
|
343
|
+
and cg.get("c_plan_new") is None,
|
|
344
|
+
"B plan 档 A 新增 fail-closed(未落盘、未出单)", o4)
|
|
345
|
+
cg.close()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# =============================================================== C 组:B 合并
|
|
349
|
+
def g_c():
|
|
350
|
+
print("== C 组:B 合并(遗忘闸 MERGE + 限流闸 CONVERGE 两落点)==")
|
|
351
|
+
with _mode("confirm"):
|
|
352
|
+
cg = _lib("m")
|
|
353
|
+
cg.add("m_t1", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
354
|
+
cg.flush()
|
|
355
|
+
r1 = cg.remember_gated("m_n1", BODY % "9595", layer="knowledge")
|
|
356
|
+
ok(r1.get("verdict") == "CONFIRM" and r1.get("moved_to") == "review_queue"
|
|
357
|
+
and bool(r1.get("pid")),
|
|
358
|
+
"C 遗忘闸 MERGE → 出变更单且不合并", {k: r1.get(k) for k in
|
|
359
|
+
("verdict", "moved_to", "pid")})
|
|
360
|
+
t = cg.get("m_t1")
|
|
361
|
+
ok("9595" not in (t or {}).get("content", "")
|
|
362
|
+
and not ((t or {}).get("frontmatter") or {}).get("merge_count"),
|
|
363
|
+
"C 出单时目标未被合并(新值与 merge_count 都不动)")
|
|
364
|
+
pay = r1.get("mutation") or {}
|
|
365
|
+
ok(pay.get("action") == "B" and pay.get("target") == "m_t1"
|
|
366
|
+
and pay.get("after") == (BODY % "9595")
|
|
367
|
+
and pay.get("primitive") == "reinforce",
|
|
368
|
+
"C 载荷:动作类 B · 目标 · 后像 · 原语 reinforce", pay)
|
|
369
|
+
r = cg.review_decide(r1.get("pid") or "", "accept", reason="守卫")
|
|
370
|
+
ok(r.get("ok") is True, "C accept 返回 ok", r)
|
|
371
|
+
t = cg.get("m_t1")
|
|
372
|
+
ok("9595" in (t or {}).get("content", "")
|
|
373
|
+
and ((t or {}).get("frontmatter") or {}).get("merge_count") == 1,
|
|
374
|
+
"C accept **执行**合并:reinforce 原语落盘(新值可检索)")
|
|
375
|
+
# reject 不执行
|
|
376
|
+
r2 = cg.remember_gated("m_n2", BODY % "7070", layer="knowledge")
|
|
377
|
+
r = cg.review_decide(r2.get("pid") or "", "reject", reason="守卫:不执行")
|
|
378
|
+
ok(r.get("ok") is True and "7070" not in
|
|
379
|
+
(cg.get("m_t1") or {}).get("content", ""),
|
|
380
|
+
"C reject **不执行**(原样留痕、目标未被改)")
|
|
381
|
+
# edit 对变更单 fail-closed
|
|
382
|
+
r3 = cg.remember_gated("m_n3", BODY % "6060", layer="knowledge")
|
|
383
|
+
r = cg.review_decide(r3.get("pid") or "", "edit", edits={"content": "x"}, reason="守卫")
|
|
384
|
+
ok(r.get("ok") is False and r.get("error") == "mutation_decision_unsupported"
|
|
385
|
+
and bool(r.get("hint")),
|
|
386
|
+
"C edit 对变更单 fail-closed(带 hint,不静默当已处理)", r)
|
|
387
|
+
ok(any(x.get("pid") == (r3.get("pid") or "") for x in cg.review_list()),
|
|
388
|
+
"C 未被自理的变更单仍在 pending(未被静默关闭)")
|
|
389
|
+
# CONVERGE 落点(限流闸 → converge_into)
|
|
390
|
+
r4 = cg.remember_gated("m_x1", "批次107 收官:指标面 9090 正常",
|
|
391
|
+
layer="contextual")
|
|
392
|
+
r5 = cg.remember_gated("m_x2", "批次108 收官:指标面 9595 正常",
|
|
393
|
+
layer="contextual")
|
|
394
|
+
ok(r4.get("verdict") == "ACCEPT", "C CONVERGE 前置:首条 ACCEPT", r4.get("verdict"))
|
|
395
|
+
ok(r5.get("verdict") == "CONFIRM"
|
|
396
|
+
and (r5.get("mutation") or {}).get("primitive") == "converge_into"
|
|
397
|
+
and (r5.get("mutation") or {}).get("target") == "m_x1",
|
|
398
|
+
"C 限流闸 CONVERGE → 出变更单(原语 converge_into、目标=既有节点)",
|
|
399
|
+
r5.get("mutation"))
|
|
400
|
+
ok("9595" not in (cg.get("m_x1") or {}).get("content", ""),
|
|
401
|
+
"C CONVERGE 出单时未并入")
|
|
402
|
+
r = cg.review_decide(r5.get("pid") or "", "accept", reason="守卫")
|
|
403
|
+
ok(r.get("ok") is True and "9595" in (cg.get("m_x1") or {}).get("content", ""),
|
|
404
|
+
"C accept **执行** CONVERGE:converge_into 落盘")
|
|
405
|
+
cg.close()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# =============================================================== D 组:D 删除
|
|
409
|
+
def g_d():
|
|
410
|
+
print("== D 组:D 删除(MCP 工具面 forget_gated)==")
|
|
411
|
+
with _mode("confirm"):
|
|
412
|
+
cg = _lib("d")
|
|
413
|
+
cg.add("d_1", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
414
|
+
cg.add("d_prot", BODY % "8080", layer="self", verification_basis="test")
|
|
415
|
+
cg.flush()
|
|
416
|
+
r1 = cg.forget_gated("d_1", reason="守卫删除")
|
|
417
|
+
ok(r1.get("ok") is True and r1.get("deleted") is False
|
|
418
|
+
and r1.get("moved_to") == "review_queue" and bool(r1.get("pid")),
|
|
419
|
+
"D 删除出变更单且不软删(deleted=False 明示)",
|
|
420
|
+
{k: r1.get(k) for k in ("ok", "deleted", "moved_to", "pid")})
|
|
421
|
+
ok(cg.get("d_1") is not None, "D 出单后节点原样在位")
|
|
422
|
+
pay = r1.get("mutation") or {}
|
|
423
|
+
ok(pay.get("action") == "D" and pay.get("target") == "d_1"
|
|
424
|
+
and pay.get("after") == "" and pay.get("primitive") == "forget",
|
|
425
|
+
"D 载荷:动作类 D · 目标 · 后像为空 · 原语 forget", pay)
|
|
426
|
+
r = cg.review_decide(r1.get("pid") or "", "accept", reason="守卫")
|
|
427
|
+
ok(r.get("ok") is True and cg.get("d_1") is None,
|
|
428
|
+
"D accept **执行**软删(trash + 删除清单,既有 forget 原语)")
|
|
429
|
+
# 资格闸先于档位(纯加严):受保护节点在资格闸就被拦,出单都轮不到
|
|
430
|
+
try:
|
|
431
|
+
cg.forget_gated("d_prot", reason="受保护")
|
|
432
|
+
ok(False, "D 受保护节点未被拦(纯加严被破坏)")
|
|
433
|
+
except Exception as exc: # noqa: BLE001
|
|
434
|
+
ok(type(exc).__name__ in ("ProtectionError", "AccessDenied"),
|
|
435
|
+
"D 受保护节点在资格闸就被拦(档位不参与资格判定、不放宽)",
|
|
436
|
+
type(exc).__name__)
|
|
437
|
+
ok(cg.get("d_prot") is not None, "D 受保护节点仍在位")
|
|
438
|
+
# 资格闸不过时不出单(not_found 原样返回)
|
|
439
|
+
r3 = cg.forget_gated("d_ghost", reason="不存在")
|
|
440
|
+
ok(r3 == {"ok": False, "error": "not_found"},
|
|
441
|
+
"D 资格闸未过 → 原样返回 not_found(不出单)", r3)
|
|
442
|
+
cg.flush()
|
|
443
|
+
# 库层不接线:直调 forget 不受档位影响(脚本通道现状逐位不变)
|
|
444
|
+
cg.add("d_2", BODY % "7070", layer="knowledge", verification_basis="test")
|
|
445
|
+
cg.flush()
|
|
446
|
+
dr = cg.forget("d_2", reason="库层直调")
|
|
447
|
+
ok(dr.get("ok") is True and bool(dr.get("tombstone"))
|
|
448
|
+
and cg.get("d_2") is None,
|
|
449
|
+
"D 库层 forget 直调不受档位影响(软删照旧成功)", dr)
|
|
450
|
+
cg.close()
|
|
451
|
+
# 安全层管理闸在**档位路径**同样生效(本批实修:档位路径曾直调 _forget_apply,
|
|
452
|
+
# 把 `MdCGSecure.forget` 的 can_admin 要求整个跳过——MDCG_CAN_ADMIN=0 的身份
|
|
453
|
+
# 经 mdcg_forget 能把节点删掉;test_p2_mcp §9 实测抓到,见报告)。
|
|
454
|
+
cg_ro = MdCGSecure(os.path.join(_SANDBOX, "ro_r%d" % _RUN[0]),
|
|
455
|
+
principal=Principal(tenant="default", actor="ro",
|
|
456
|
+
role="reader", can_write=True,
|
|
457
|
+
can_admin=False))
|
|
458
|
+
with _mode("confirm"):
|
|
459
|
+
cg_ro.add("ro_1", BODY % "4040", layer="knowledge",
|
|
460
|
+
verification_basis="test")
|
|
461
|
+
cg_ro.flush()
|
|
462
|
+
try:
|
|
463
|
+
cg_ro.forget_gated("ro_1", reason="守卫:无 can_admin")
|
|
464
|
+
ok(False, "D 无 can_admin 的身份经档位路径删除被拒(未报错)")
|
|
465
|
+
except Exception as exc: # noqa: BLE001
|
|
466
|
+
ok(type(exc).__name__ == "AccessDenied"
|
|
467
|
+
and cg_ro.get("ro_1") is not None,
|
|
468
|
+
"D 无 can_admin 的身份经档位路径删除被拒(AccessDenied,节点在位)",
|
|
469
|
+
type(exc).__name__)
|
|
470
|
+
try:
|
|
471
|
+
cg_ro.forget("ro_1", reason="守卫:库层同拒")
|
|
472
|
+
ok(False, "D 无 can_admin 的身份直调库层 forget 同样被拒")
|
|
473
|
+
except Exception as exc: # noqa: BLE001
|
|
474
|
+
ok(type(exc).__name__ == "AccessDenied",
|
|
475
|
+
"D 无 can_admin 的身份直调库层 forget 同样被拒(两入口同一闸)",
|
|
476
|
+
type(exc).__name__)
|
|
477
|
+
cg_ro.close()
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# =============================================================== E 组:幂等
|
|
481
|
+
def g_e():
|
|
482
|
+
print("== E 组:变更单幂等(同动作+目标+后像不长第二条)==")
|
|
483
|
+
with _mode("confirm"):
|
|
484
|
+
cg = _lib("idem")
|
|
485
|
+
cg.add("e_1", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
486
|
+
cg.add("e_2", BODY % "8080", layer="knowledge", verification_basis="test")
|
|
487
|
+
cg.flush()
|
|
488
|
+
k1 = autonomy_modes.mutation_dedup_key("C", "e_1", "X")
|
|
489
|
+
ok(k1 == autonomy_modes.mutation_dedup_key("C", "e_1", "X")
|
|
490
|
+
and k1 != autonomy_modes.mutation_dedup_key("C", "e_2", "X")
|
|
491
|
+
and k1 != autonomy_modes.mutation_dedup_key("B", "e_1", "X")
|
|
492
|
+
and k1 != autonomy_modes.mutation_dedup_key("C", "e_1", "Y"),
|
|
493
|
+
"E 幂等键:同三元恒同、动作/目标/后像任一不同即不同")
|
|
494
|
+
p1 = autonomy_modes.propose_mutation(
|
|
495
|
+
cg, "C", "e_1", after="X", reason="幂等探针", info=True)
|
|
496
|
+
p2 = autonomy_modes.propose_mutation(
|
|
497
|
+
cg, "C", "e_1", after="X", reason="幂等探针", info=True)
|
|
498
|
+
ok(p2.get("dedup") is True and p2.get("pid") == p1.get("pid"),
|
|
499
|
+
"E 同(动作+目标+后像)重复提议不长第二条(幂等返回既有 pid)", p2)
|
|
500
|
+
p3 = autonomy_modes.propose_mutation(
|
|
501
|
+
cg, "C", "e_2", after="X", reason="幂等探针", info=True)
|
|
502
|
+
ok(p3.get("dedup") is not True and p3.get("pid") != p1.get("pid"),
|
|
503
|
+
"E 换目标即长第二条(内容签名不带目标 ⇒ 必须覆盖对账键)", p3)
|
|
504
|
+
pend = [r for r in cg.review_list()
|
|
505
|
+
if autonomy_modes.order_kind(r) == "mutation"]
|
|
506
|
+
ok(len(pend) == 2, "E 三次提议(2 唯一键)→ 队列恰 2 条", len(pend))
|
|
507
|
+
cg.close()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# =============================================================== F 组:存量兼容
|
|
511
|
+
def g_f():
|
|
512
|
+
print("== F 组:存量兼容(缺 kind = proposal,接受路径逐位不变)==")
|
|
513
|
+
with _mode("confirm"):
|
|
514
|
+
cg = _lib("legacy")
|
|
515
|
+
rec = {"pid": "prop_legacy_1", "id": "f_1", "content": "老提案正文",
|
|
516
|
+
"layer": "knowledge", "tags": [], "payload_hash": "h1",
|
|
517
|
+
"extra": {}}
|
|
518
|
+
ok(autonomy_modes.order_kind(rec) == "proposal"
|
|
519
|
+
and autonomy_modes.mutation_view(rec) is None,
|
|
520
|
+
"F 缺 kind 键 ⇒ proposal(零迁移)", autonomy_modes.order_kind(rec))
|
|
521
|
+
pr = cg.propose("f_1", "老提案正文", layer="knowledge", info=True)
|
|
522
|
+
item = [r for r in cg.review_list() if r.get("pid") == pr["pid"]][0]
|
|
523
|
+
ok("kind" not in item,
|
|
524
|
+
"F 普通提案的 rec 形状零变化(不落 kind 键)", sorted(item))
|
|
525
|
+
r = cg.review_decide(pr["pid"], "accept", reason="守卫")
|
|
526
|
+
ok(r.get("ok") is True and r.get("node_id") == "f_1"
|
|
527
|
+
and "老提案正文" in (cg.get("f_1") or {}).get("content", ""),
|
|
528
|
+
"F proposal 的 accept 原路径逐位不变(落盘成功)", r)
|
|
529
|
+
# noop/merge 对 proposal 亦不变
|
|
530
|
+
pr2 = cg.propose("f_2", "老提案正文二", layer="knowledge", info=True)
|
|
531
|
+
r = cg.review_decide(pr2["pid"], "noop", reason="守卫")
|
|
532
|
+
ok(r.get("ok") is True and cg.get("f_2") is None,
|
|
533
|
+
"F proposal 的 noop 仍只留痕不落盘", r)
|
|
534
|
+
cg.close()
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# =============================================================== G 组:单点结构
|
|
538
|
+
def _repo_root():
|
|
539
|
+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _py_files():
|
|
543
|
+
root = _repo_root()
|
|
544
|
+
out = []
|
|
545
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
546
|
+
dirnames[:] = [d for d in dirnames
|
|
547
|
+
if d not in (".git", "__pycache__", "node_modules",
|
|
548
|
+
".venv", "target", "lib")]
|
|
549
|
+
for fn in filenames:
|
|
550
|
+
if fn.endswith(".py"):
|
|
551
|
+
out.append(os.path.join(dirpath, fn))
|
|
552
|
+
return out
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def g_g():
|
|
556
|
+
print("== G 组:单点结构(全仓只有 autonomy_modes 读档位 env)==")
|
|
557
|
+
lit = '"%s"' % _ENV_MODE
|
|
558
|
+
hits = []
|
|
559
|
+
for p in _py_files():
|
|
560
|
+
try:
|
|
561
|
+
with open(p, encoding="utf-8") as f:
|
|
562
|
+
src = f.read()
|
|
563
|
+
except OSError:
|
|
564
|
+
continue
|
|
565
|
+
if lit in src or ("'%s'" % _ENV_MODE) in src:
|
|
566
|
+
hits.append(os.path.relpath(p, _repo_root()).replace("\\", "/"))
|
|
567
|
+
ok(hits == ["md_cg/autonomy_modes.py"],
|
|
568
|
+
"G 全仓 .py 里带引号的档位 env 字面量只出现在唯一入口模块", hits)
|
|
569
|
+
# 库层不接线(accept 执行桥「直越确认判定」的机制前提)
|
|
570
|
+
for owner, name in ((MdCG, "add"), (MdCG, "_write_node"),
|
|
571
|
+
(MdCGOS, "forget")):
|
|
572
|
+
src = inspect.getsource(getattr(owner, name))
|
|
573
|
+
code = "\n".join(l for l in src.splitlines()
|
|
574
|
+
if not l.strip().startswith("#"))
|
|
575
|
+
ok("autonomy_modes" not in code,
|
|
576
|
+
"G 库层 %s.%s 不含档位判据(保持库层语义不变)"
|
|
577
|
+
% (owner.__name__, name), [l for l in code.splitlines()
|
|
578
|
+
if "autonomy_modes" in l])
|
|
579
|
+
for mod, name in ((forgetting, "reinforce"), (writelimit, "converge_into")):
|
|
580
|
+
src = inspect.getsource(getattr(mod, name))
|
|
581
|
+
code = "\n".join(l for l in src.splitlines()
|
|
582
|
+
if not l.strip().startswith("#"))
|
|
583
|
+
ok("autonomy_modes" not in code,
|
|
584
|
+
"G 合并原语 %s.%s 不含档位判据(执行桥直调原语即越过判定)"
|
|
585
|
+
% (mod.__name__, name))
|
|
586
|
+
# 接线点仍在(防「档位不生效」的静态退化)
|
|
587
|
+
wsrc = inspect.getsource(writepipe.install_default_gates)
|
|
588
|
+
ok('"autonomy"' in wsrc or "'autonomy'" in wsrc,
|
|
589
|
+
"G 写链默认链仍注册档位闸(链尾)")
|
|
590
|
+
ok("_gate_autonomy" in inspect.getsource(writepipe)
|
|
591
|
+
and "_autonomy_gate_merge" in inspect.getsource(mdcos),
|
|
592
|
+
"G 档位闸的接线单点存在(写链 + 合并两落点)")
|
|
593
|
+
ok("forget_gated" in inspect.getsource(mdcos)
|
|
594
|
+
and "forget_gated" in open(
|
|
595
|
+
os.path.join(_repo_root(), "md_cg", "mcp_server.py"),
|
|
596
|
+
encoding="utf-8").read(),
|
|
597
|
+
"G 删除档位路径(forget_gated)在库层与 MCP 工具面都在位")
|
|
598
|
+
# 管理闸单点(本批实修:档位路径曾绕过 MdCGSecure 的 can_admin 要求)
|
|
599
|
+
ok("def _forget_admin" in inspect.getsource(mdcos.MdCGSecure)
|
|
600
|
+
and "self._forget_admin()" in inspect.getsource(mdcos.MdCGSecure.forget)
|
|
601
|
+
and "self._forget_admin()"
|
|
602
|
+
in inspect.getsource(mdcos.MdCGSecure.forget_gated),
|
|
603
|
+
"G 管理闸单点:MdCGSecure 的 forget 与 forget_gated 都过 _forget_admin")
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# =============================================================== H 组:不动面对拍
|
|
607
|
+
def g_h():
|
|
608
|
+
print("== H 组:不动面对拍(full 档全动作 / confirm 档 A·E)==")
|
|
609
|
+
for a in autonomy_modes.ACTION_CLASSES:
|
|
610
|
+
d = autonomy_modes.decide(a, mode_explicit="full")
|
|
611
|
+
ok(d["decision"] == "allow" and d["hint"] == "",
|
|
612
|
+
"H full 档 %s → allow 且无 hint(闸放行 = 与改动前同一条链)" % a, d)
|
|
613
|
+
for a in ("A", "E"):
|
|
614
|
+
d = autonomy_modes.decide(a, mode_explicit="confirm")
|
|
615
|
+
ok(d["decision"] == autonomy_modes.decide(a, mode_explicit="full")["decision"]
|
|
616
|
+
and d["hint"] == "",
|
|
617
|
+
"H confirm 档 %s 的动作面读数与 full 档逐位相等(不阻塞)" % a, d)
|
|
618
|
+
_policy()
|
|
619
|
+
runs = {}
|
|
620
|
+
for m in ("confirm", "full"):
|
|
621
|
+
with _mode(m):
|
|
622
|
+
cg = _lib("pair_" + m)
|
|
623
|
+
pipe = _pipe()
|
|
624
|
+
out = pipe.execute(cg, {"content_kind": "text",
|
|
625
|
+
"content": PLAIN % "对拍", "layer": "knowledge",
|
|
626
|
+
"node_id": "h_1"})
|
|
627
|
+
node = cg.get("h_1") or {}
|
|
628
|
+
runs[m] = (out, node)
|
|
629
|
+
cg.close()
|
|
630
|
+
oc, of = _strip(runs["confirm"][0]), _strip(runs["full"][0])
|
|
631
|
+
ok(oc == of, "H A 新增:confirm 档返回体与 full 档**逐位相等**(时钟面除外)",
|
|
632
|
+
{"confirm": oc, "full": of})
|
|
633
|
+
nc, nf = _strip(runs["confirm"][1]), _strip(runs["full"][1])
|
|
634
|
+
ok(nc == nf, "H A 新增:两档落盘节点(含正文与 fm,时钟面除外)逐位相等",
|
|
635
|
+
{"confirm": nc, "full": nf})
|
|
636
|
+
# full 档三动作真执行(与改动前同):C 覆写 / B 合并 / D 软删
|
|
637
|
+
with _mode("full"):
|
|
638
|
+
cg = _lib("full_drive")
|
|
639
|
+
cg.add("f_a", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
640
|
+
cg.flush()
|
|
641
|
+
rc = cg.remember_gated("f_b", BODY % "9595", layer="knowledge")
|
|
642
|
+
ok(rc.get("verdict") == "MERGE"
|
|
643
|
+
and "9595" in (cg.get("f_a") or {}).get("content", ""),
|
|
644
|
+
"H full 档 B 合并直落(无单、verdict=MERGE 与改动前同)",
|
|
645
|
+
rc.get("verdict"))
|
|
646
|
+
dd = cg.forget_gated("f_a", reason="full 档")
|
|
647
|
+
ok(dd.get("ok") is True and dd.get("deleted") is not False
|
|
648
|
+
and dd.get("moved_to") is None and dd.get("tombstone"),
|
|
649
|
+
"H full 档 D 删除直删(无单、带 tombstone 与改动前同)", dd)
|
|
650
|
+
ok(len([r for r in cg.review_list()
|
|
651
|
+
if autonomy_modes.order_kind(r) == "mutation"]) == 0,
|
|
652
|
+
"H full 档全程零变更单(队列无 mutation 条目)")
|
|
653
|
+
cg.close()
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# =============================================================== I 组:gated 面
|
|
657
|
+
#: gated 面覆写探针正文:与库内既有正文**同族但不同值**(`assess` 的 redundancy
|
|
658
|
+
#: 对同 id 自排除 ⇒ 判 ACCEPT 且带 overwrite 读数),故能走到 ACCEPT 分支的档位判定。
|
|
659
|
+
def _mut_entries(recs):
|
|
660
|
+
return [x for x in recs if autonomy_modes.order_kind(x) == "mutation"]
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _submit(script, args, drop_shim=False):
|
|
664
|
+
"""子进程跑 `test_policy_required_ccg` 整腿(隔离:其 sys.path/sys.modules 污染不外溢)。
|
|
665
|
+
|
|
666
|
+
drop_shim=True = 该处判据的**定点变异**,且必须放在「读文件的那一侧」才生效:
|
|
667
|
+
在子进程内先把 `_SHIMS` 去掉 `autonomy_modes`(=未登记的形态),该腿必须
|
|
668
|
+
SHIM-MISS 非零退出——正/负对照成对,证明 J3 的绿不是空转。
|
|
669
|
+
"""
|
|
670
|
+
import subprocess
|
|
671
|
+
code = ("import sys; from md_cg import test_policy_required_ccg as t; "
|
|
672
|
+
+ ('t._SHIMS = tuple(x for x in t._SHIMS'
|
|
673
|
+
' if x != "autonomy_modes"); ' if drop_shim else "")
|
|
674
|
+
+ "sys.exit(t.main())")
|
|
675
|
+
p = subprocess.run([sys.executable, "-X", "utf8", "-c", code] + list(args),
|
|
676
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
677
|
+
errors="replace", cwd=_repo_root(),
|
|
678
|
+
env=dict(os.environ, PYTHONUTF8="1"))
|
|
679
|
+
return {"rc": p.returncode, "out": (p.stdout or "") + (p.stderr or "")}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def g_i():
|
|
683
|
+
print("== I 组:gated 面 C 改写 / A 新增 + 执行桥 C 目标消失(补强批次)==")
|
|
684
|
+
# 每个场景**独立合成库**:遗忘闸的 redundancy 是全库比对(只排除本次 node_id),
|
|
685
|
+
# 同库多节点会把「新写入」判成 MERGE/DEFER 而走不到 ACCEPT 分支的档位判定——
|
|
686
|
+
# 场景隔离是让每条断言确实命中目标分支的前提(不是放水)。
|
|
687
|
+
_policy()
|
|
688
|
+
|
|
689
|
+
# ---- ① confirm 档:C 覆写出单且不落盘 ----
|
|
690
|
+
with _mode("confirm"):
|
|
691
|
+
cg = _lib("gated_c")
|
|
692
|
+
cg.add("i_1", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
693
|
+
cg.flush()
|
|
694
|
+
r1 = cg.remember_gated("i_1", BODY % "9595", layer="knowledge")
|
|
695
|
+
ok(r1.get("verdict") == "CONFIRM"
|
|
696
|
+
and r1.get("moved_to") == "review_queue"
|
|
697
|
+
and r1.get("committed") is False and bool(r1.get("pid")),
|
|
698
|
+
"I gated C 覆写(confirm 档)出变更单且明示未落盘(moved_to=review_queue)",
|
|
699
|
+
{k: r1.get(k) for k in ("verdict", "ok", "committed", "moved_to", "pid")})
|
|
700
|
+
t = cg.get("i_1") or {}
|
|
701
|
+
ok("9090" in t.get("content", "") and "9595" not in t.get("content", ""),
|
|
702
|
+
"I gated C 出单时目标节点正文**未被覆写**(逐字比对)")
|
|
703
|
+
pay = r1.get("mutation") or {}
|
|
704
|
+
ok(pay.get("action") == "C" and pay.get("target") == "i_1"
|
|
705
|
+
and pay.get("after") == (BODY % "9595")
|
|
706
|
+
and pay.get("primitive") == "add" and bool(pay.get("reason")),
|
|
707
|
+
"I gated C 载荷齐备:动作类 C · 目标 · 后像 · 理由 · 复现原语 add", pay)
|
|
708
|
+
rec = [x for x in cg.review_list()
|
|
709
|
+
if x.get("pid") == (r1.get("pid") or "")]
|
|
710
|
+
ok(len(rec) == 1 and autonomy_modes.order_kind(rec[0]) == "mutation"
|
|
711
|
+
and rec[0].get("id") == "i_1",
|
|
712
|
+
"I gated C 队列条目 kind=mutation 且 id=目标(既有读取面可见)",
|
|
713
|
+
rec[0] if rec else None)
|
|
714
|
+
# accept 执行 / reject 不执行
|
|
715
|
+
rr = cg.review_decide(r1.get("pid") or "", "accept", reason="守卫")
|
|
716
|
+
ok(rr.get("ok") is True
|
|
717
|
+
and "9595" in (cg.get("i_1") or {}).get("content", ""),
|
|
718
|
+
"I gated C accept **执行**覆写(走既有 add,正文已换)", rr)
|
|
719
|
+
r_rej = cg.remember_gated("i_1", BODY % "6060", layer="knowledge")
|
|
720
|
+
cg.review_decide(r_rej.get("pid") or "", "reject", reason="守卫:不执行")
|
|
721
|
+
ok("6060" not in (cg.get("i_1") or {}).get("content", ""),
|
|
722
|
+
"I gated C reject **不执行**(目标未被改)")
|
|
723
|
+
cg.close()
|
|
724
|
+
|
|
725
|
+
# ---- ② 资格在先(纯加严):受保护节点覆写在 write_qualify 处当场拦 ----
|
|
726
|
+
with _mode("confirm"):
|
|
727
|
+
cg = _lib("gated_prot")
|
|
728
|
+
cg.add("i_prot", BODY % "8080", layer="self", verification_basis="test")
|
|
729
|
+
cg.flush()
|
|
730
|
+
try:
|
|
731
|
+
cg.remember_gated("i_prot", BODY % "7070", layer="knowledge")
|
|
732
|
+
ok(False, "I gated C 受保护节点覆写未被资格闸拦(纯加严被破坏)")
|
|
733
|
+
except Exception as exc: # noqa: BLE001
|
|
734
|
+
ok(type(exc).__name__ in ("ProtectionError", "AccessDenied"),
|
|
735
|
+
"I gated C 受保护节点在 write_qualify 处当场被拦(资格先于档位、"
|
|
736
|
+
"不静默入队)", type(exc).__name__)
|
|
737
|
+
ok(cg.get("i_prot") is not None and len(_mut_entries(cg.review_list())) == 0,
|
|
738
|
+
"I 受保护覆写零出单(队列无变更单,未被静默入队)")
|
|
739
|
+
cg.close()
|
|
740
|
+
|
|
741
|
+
# ---- ③ plan 档:C 覆写出单(与 confirm 同格) ----
|
|
742
|
+
with _mode("plan"):
|
|
743
|
+
cg = _lib("gated_plan")
|
|
744
|
+
cg.add("i_p", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
745
|
+
cg.flush()
|
|
746
|
+
r = cg.remember_gated("i_p", BODY % "9595", layer="knowledge")
|
|
747
|
+
ok(r.get("verdict") == "CONFIRM" and r.get("moved_to") == "review_queue"
|
|
748
|
+
and bool(r.get("pid"))
|
|
749
|
+
and "9595" not in (cg.get("i_p") or {}).get("content", ""),
|
|
750
|
+
"I gated C 覆写(plan 档)出变更单且不落盘(C 在 plan/confirm 同语义)",
|
|
751
|
+
{k: r.get(k) for k in ("verdict", "moved_to", "pid")})
|
|
752
|
+
cg.close()
|
|
753
|
+
|
|
754
|
+
# ---- ④ plan 档:A 新增 fail-closed(独立库:不得被同库近重复判成 MERGE) ----
|
|
755
|
+
with _mode("plan"):
|
|
756
|
+
cg = _lib("gated_plan_a")
|
|
757
|
+
r2 = cg.remember_gated("i_new", BODY % "7777", layer="knowledge")
|
|
758
|
+
ok(r2.get("verdict") == "FORBID" and r2.get("ok") is False
|
|
759
|
+
and r2.get("moved_to") == "autonomy_forbidden"
|
|
760
|
+
and r2.get("error") == "autonomy_forbid"
|
|
761
|
+
and "计划" in str(r2.get("hint") or "")
|
|
762
|
+
and cg.get("i_new") is None
|
|
763
|
+
and len(_mut_entries(cg.review_list())) == 0,
|
|
764
|
+
"I gated A 新增(plan 档)fail-closed 带 hint、未落盘、未出单",
|
|
765
|
+
{k: r2.get(k) for k in ("verdict", "ok", "moved_to", "error")})
|
|
766
|
+
cg.close()
|
|
767
|
+
|
|
768
|
+
# ---- ⑤ full 档:C 覆写照旧直落(零变更单) ----
|
|
769
|
+
with _mode("full"):
|
|
770
|
+
cg = _lib("gated_full")
|
|
771
|
+
cg.add("i_f", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
772
|
+
cg.flush()
|
|
773
|
+
r = cg.remember_gated("i_f", BODY % "9595", layer="knowledge")
|
|
774
|
+
ok(r.get("verdict") == "ACCEPT" and r.get("written") == "i_f"
|
|
775
|
+
and "9595" in (cg.get("i_f") or {}).get("content", "")
|
|
776
|
+
and len(_mut_entries(cg.review_list())) == 0,
|
|
777
|
+
"I gated C 覆写(full 档)照旧直落:verdict=ACCEPT、正文已换、零变更单",
|
|
778
|
+
{k: r.get(k) for k in ("verdict", "written")})
|
|
779
|
+
cg.close()
|
|
780
|
+
|
|
781
|
+
# ---- ⑥ A 新增:confirm/full 两档**逐位一致**(不动面对拍) ----
|
|
782
|
+
runs = {}
|
|
783
|
+
for m in ("confirm", "full"):
|
|
784
|
+
with _mode(m):
|
|
785
|
+
cg = _lib("gated_pair_" + m)
|
|
786
|
+
r = cg.remember_gated("i_a", PLAIN % "对拍", layer="knowledge")
|
|
787
|
+
runs[m] = (r, _strip(cg.get("i_a") or {}))
|
|
788
|
+
cg.close()
|
|
789
|
+
ok(runs["confirm"][0].get("verdict") == "ACCEPT"
|
|
790
|
+
and runs["full"][0].get("verdict") == "ACCEPT"
|
|
791
|
+
and _strip(runs["confirm"][0]) == _strip(runs["full"][0]),
|
|
792
|
+
"I gated A 新增:confirm 档返回体与 full 档逐位相等(时钟面除外)",
|
|
793
|
+
{"confirm": _strip(runs["confirm"][0]),
|
|
794
|
+
"full": _strip(runs["full"][0])})
|
|
795
|
+
ok(runs["confirm"][1] == runs["full"][1],
|
|
796
|
+
"I gated A 新增:两档落盘节点(正文与 fm)逐位相等(A 面零变化)")
|
|
797
|
+
|
|
798
|
+
# ---- ⑦ 写链面(op=write + gated=true)与 MCP 工具面(插件主写入通道) ----
|
|
799
|
+
with _mode("confirm"):
|
|
800
|
+
cg = _lib("gated_chain")
|
|
801
|
+
pipe = _pipe()
|
|
802
|
+
o1 = pipe.execute(cg, {"content_kind": "text", "content": PLAIN % "甲",
|
|
803
|
+
"layer": "knowledge", "node_id": "i_ch",
|
|
804
|
+
"gated": True})
|
|
805
|
+
ok(o1.get("committed") is True,
|
|
806
|
+
"I gated 链面:A 新增(confirm 档)直落不拦(行为零变化)", o1)
|
|
807
|
+
o2 = pipe.execute(cg, {"content_kind": "text", "content": PLAIN % "乙",
|
|
808
|
+
"layer": "knowledge", "node_id": "i_ch",
|
|
809
|
+
"gated": True})
|
|
810
|
+
ok(o2.get("moved_to") == "review_queue" and o2.get("committed") is False
|
|
811
|
+
and bool((o2.get("gate") or {}).get("pid")),
|
|
812
|
+
"I gated 链面:C 覆写出单且不落盘(_gate_gated 透出 review_queue+pid)",
|
|
813
|
+
{k: o2.get(k) for k in ("moved_to", "committed")})
|
|
814
|
+
ok("甲" in (cg.get("i_ch") or {}).get("content", "")
|
|
815
|
+
and "乙" not in (cg.get("i_ch") or {}).get("content", ""),
|
|
816
|
+
"I gated 链面:出单时链上目标未被覆写(替代执行路径已改判出单)")
|
|
817
|
+
from .mcp_server import _dispatch
|
|
818
|
+
rm = _dispatch(cg, "mdcg_remember",
|
|
819
|
+
{"content": PLAIN % "丙", "node_id": "i_ch",
|
|
820
|
+
"gated": True, "layer": "knowledge"})
|
|
821
|
+
ok(rm.get("moved_to") == "review_queue" and bool(rm.get("pid"))
|
|
822
|
+
and rm.get("committed") is False
|
|
823
|
+
and "丙" not in (cg.get("i_ch") or {}).get("content", ""),
|
|
824
|
+
"I MCP 面 mdcg_remember(gated=true):C 覆写出单且不落盘(插件主通道"
|
|
825
|
+
"不再旁路确认档)",
|
|
826
|
+
{k: rm.get(k) for k in ("verdict", "ok", "moved_to", "pid")})
|
|
827
|
+
cg.close()
|
|
828
|
+
|
|
829
|
+
# ---- ⑧ 执行桥:C 目标在 accept 前消失 ----
|
|
830
|
+
with _mode("confirm"):
|
|
831
|
+
cg = _lib("gated_bridge")
|
|
832
|
+
cg.add("i_b", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
833
|
+
cg.flush()
|
|
834
|
+
rb = cg.remember_gated("i_b", BODY % "9595", layer="knowledge")
|
|
835
|
+
ok(rb.get("verdict") == "CONFIRM" and bool(rb.get("pid")),
|
|
836
|
+
"I 执行桥前置:C 覆写已出单", rb.get("verdict"))
|
|
837
|
+
cg.forget("i_b", reason="守卫:目标消失")
|
|
838
|
+
cg.flush()
|
|
839
|
+
rr = cg.review_decide(rb.get("pid") or "", "accept", reason="守卫")
|
|
840
|
+
ok(rr.get("ok") is False and rr.get("error") == "target_missing"
|
|
841
|
+
and cg.get("i_b") is None and bool(rr.get("hint")),
|
|
842
|
+
"I 执行桥 C 目标消失 → target_missing 且**不重建**节点(fail-closed)",
|
|
843
|
+
{k: rr.get(k) for k in ("ok", "error")})
|
|
844
|
+
ok(any(x.get("pid") == rb.get("pid") for x in cg.review_list()),
|
|
845
|
+
"I 目标消失的单仍在 pending(未被静默记 accepted)")
|
|
846
|
+
cg.add("i_b2", BODY % "9090", layer="knowledge", verification_basis="test")
|
|
847
|
+
cg.flush()
|
|
848
|
+
rb2 = cg.remember_gated("i_b2", BODY % "9595", layer="knowledge")
|
|
849
|
+
rr2 = cg.review_decide(rb2.get("pid") or "", "accept", reason="守卫")
|
|
850
|
+
ok(rr2.get("ok") is True
|
|
851
|
+
and "9595" in (cg.get("i_b2") or {}).get("content", ""),
|
|
852
|
+
"I 对照:目标在位时 accept 照旧执行覆写(判据不是「一律拒」)", rr2)
|
|
853
|
+
cg.close()
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# =============================================================== J 组:SHIM 修复面
|
|
857
|
+
def g_j():
|
|
858
|
+
print("== J 组:容器栈一 SHIM-MISS 修复面(test_policy_required_ccg)==")
|
|
859
|
+
from . import test_policy_required_ccg as _tpc_g
|
|
860
|
+
ok(_tpc_g is _tpc and "autonomy_modes" in _tpc._SHIMS,
|
|
861
|
+
"J SHIM 登记:autonomy_modes 在 _SHIMS(writepipe 档位闸的相对 import "
|
|
862
|
+
"已覆盖)", list(_tpc._SHIMS))
|
|
863
|
+
try:
|
|
864
|
+
_tpc._check_shim_coverage(_tpc._baseline_sources())
|
|
865
|
+
ok(True, "J 基线源相对 import 全覆盖:SHIM-MISS 判据不抛(变异锚点亦在位)")
|
|
866
|
+
except SystemExit as exc: # noqa: BLE001
|
|
867
|
+
ok(False, "J 基线源相对 import 全覆盖:SHIM-MISS 判据不抛(fail-closed)",
|
|
868
|
+
exc)
|
|
869
|
+
got = _submit("md_cg/test_policy_required_ccg.py", ["--head-baseline"])
|
|
870
|
+
ok(got["rc"] == 0 and "红基线符合预期" in got["out"],
|
|
871
|
+
"J --head-baseline 整腿 PASS(退出码 0、红项集合 == 预期 11 项)",
|
|
872
|
+
{"rc": got["rc"], "tail": got["out"][-160:]})
|
|
873
|
+
got2 = _submit("md_cg/test_policy_required_ccg.py", ["--head-baseline"],
|
|
874
|
+
drop_shim=True)
|
|
875
|
+
ok(got2["rc"] != 0 and "SHIM-MISS" in got2["out"],
|
|
876
|
+
"J 负对照(同处定点变异:_SHIMS 去掉 autonomy_modes)⇒ 该腿 SHIM-MISS "
|
|
877
|
+
"非零退出——J3 的绿有判别力", {"rc": got2["rc"]})
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
# =============================================================== K 组:变更单字段
|
|
881
|
+
def g_k():
|
|
882
|
+
print("== K 组:变更单三字段(前像=执行时点 · 影响面 · 回滚命令)==")
|
|
883
|
+
with _mode("confirm"):
|
|
884
|
+
cg = _lib("fields")
|
|
885
|
+
# 边影响面:本节点 part_of → k_p(父)——C 覆写的 add 全量重建 fm 会把
|
|
886
|
+
# 声明式 edges 清空(既有行为),前像/影响面据此可判别。
|
|
887
|
+
cg.add("k_1", PLAIN % "初版", layer="knowledge",
|
|
888
|
+
verification_basis="test",
|
|
889
|
+
edges=[{"target": "k_p", "relation_type": "part_of"}])
|
|
890
|
+
cg.add("k_p", PLAIN % "父", layer="knowledge",
|
|
891
|
+
verification_basis="test")
|
|
892
|
+
cg.flush()
|
|
893
|
+
pay0 = autonomy_modes.mutation_payload("C", "k_1", after=PLAIN % "改写版",
|
|
894
|
+
reason="字段完备探针",
|
|
895
|
+
primitive="add")
|
|
896
|
+
ok(pay0.get("before") is None and pay0.get("impact") is None
|
|
897
|
+
and pay0.get("rollback") == "",
|
|
898
|
+
"K 提议时点载荷三键恒在且占位(before/impact=None、rollback='')",
|
|
899
|
+
{k: pay0.get(k) for k in ("before", "impact", "rollback")})
|
|
900
|
+
pr = autonomy_modes.propose_mutation(cg, "C", "k_1", payload=pay0,
|
|
901
|
+
layer="knowledge", info=True)
|
|
902
|
+
rec0 = [x for x in cg.review_list() if x.get("pid") == pr["pid"]]
|
|
903
|
+
view0 = autonomy_modes.mutation_view(rec0[0]) if rec0 else None
|
|
904
|
+
ok(view0 is not None and view0.get("before") is None
|
|
905
|
+
and view0.get("impact") is None and view0.get("rollback") == "",
|
|
906
|
+
"K 队列条目的 mutation_view 同口径三键(提议时点占位)", view0)
|
|
907
|
+
# 提议→accept 之间:第三方覆写(前像必须=执行时点=该版本)。
|
|
908
|
+
# 第三方自己也带同一条声明式边(add 全量重建 fm 的既有语义:不带即清空)
|
|
909
|
+
# ——使「执行时点影响面」的边读数在案、判据可评估。
|
|
910
|
+
cg.add("k_1", PLAIN % "第三方版", layer="knowledge",
|
|
911
|
+
verification_basis="test",
|
|
912
|
+
edges=[{"target": "k_p", "relation_type": "part_of"}])
|
|
913
|
+
third_p = os.path.join(cg.root, cg.index["nodes"]["k_1"]["path"])
|
|
914
|
+
with open(third_p, "rb") as f:
|
|
915
|
+
third_bytes = f.read()
|
|
916
|
+
r = cg.review_decide(pr["pid"], "accept", reason="K:执行")
|
|
917
|
+
m = r.get("mutation") or {}
|
|
918
|
+
ok(r.get("ok") is True and isinstance(m, dict),
|
|
919
|
+
"K accept 执行返回补全载荷(dict)", {k: r.get(k) for k in
|
|
920
|
+
("ok", "error")})
|
|
921
|
+
pre_rel = m.get("before") or ""
|
|
922
|
+
pre_p = os.path.join(cg.root, pre_rel.replace("/", os.sep)) if pre_rel else ""
|
|
923
|
+
ok(bool(pre_rel) and os.path.isfile(pre_p),
|
|
924
|
+
"K before 非空且指向已落盘快照(执行时点前像)", pre_rel)
|
|
925
|
+
snap_bytes = None
|
|
926
|
+
if pre_p and os.path.isfile(pre_p):
|
|
927
|
+
with open(pre_p, "rb") as f:
|
|
928
|
+
snap_bytes = f.read()
|
|
929
|
+
# 前像缺失(变异「不拍前像」)时这里不 skip——None != third_bytes 即红,
|
|
930
|
+
# 「前像=执行时点」这条判据必须有判别力(不是空转的前提断言)。
|
|
931
|
+
ok(snap_bytes == third_bytes,
|
|
932
|
+
"K 前像=**执行时点**盘面(第三方改动被保留=逐字节等于 accept 前盘面)",
|
|
933
|
+
"前像字节与 accept 前盘面%s" % ("相同" if snap_bytes == third_bytes
|
|
934
|
+
else "不同/前像缺失"))
|
|
935
|
+
snap_text = (open(pre_p, encoding="utf-8").read()
|
|
936
|
+
if (pre_p and os.path.isfile(pre_p)) else "")
|
|
937
|
+
ok("第三方版" in snap_text and "初版" not in snap_text,
|
|
938
|
+
"K 前像不是提议时点版本(含第三方版、不含出单前初版)",
|
|
939
|
+
repr(snap_text[-60:]))
|
|
940
|
+
imp = m.get("impact")
|
|
941
|
+
ok(isinstance(imp, dict)
|
|
942
|
+
and {"edges", "index", "agg_lines"} <= set(imp),
|
|
943
|
+
"K impact 三面齐备(边/索引条目/聚合行)", imp)
|
|
944
|
+
ok(isinstance(imp, dict)
|
|
945
|
+
and (imp.get("edges") or {}).get("parents") == ["k_p"],
|
|
946
|
+
"K impact·边 = 执行时点读数(part_of 父边 k_p 在案)",
|
|
947
|
+
(imp or {}).get("edges"))
|
|
948
|
+
ok(isinstance(imp, dict)
|
|
949
|
+
and isinstance(imp.get("index"), dict)
|
|
950
|
+
and imp["index"].get("present") is True
|
|
951
|
+
and isinstance(imp.get("agg_lines"), list),
|
|
952
|
+
"K impact·索引条目在案 + 聚合行为列表", (imp or {}).get("index"))
|
|
953
|
+
cmd = m.get("rollback") or ""
|
|
954
|
+
ok(isinstance(cmd, str) and "md_cg.rollback_cli" in cmd
|
|
955
|
+
and pr["pid"] in cmd and "utf8" in cmd,
|
|
956
|
+
"K rollback = 可执行回滚命令串(rollback_cli + pid + utf8)", cmd)
|
|
957
|
+
recs = [x for x in cg.decisions() if x.get("pid") == pr["pid"]]
|
|
958
|
+
rex = recs[-1] if recs else {}
|
|
959
|
+
ok(isinstance(rex.get("mutation"), dict)
|
|
960
|
+
and rex["mutation"].get("before") == pre_rel
|
|
961
|
+
and rex["mutation"].get("rollback") == cmd
|
|
962
|
+
and rex["mutation"].get("action") == "C",
|
|
963
|
+
"K 执行记录 rec['mutation'] 落盘(before/rollback/action 与返回体一致)",
|
|
964
|
+
sorted((rex.get("mutation") or {}).keys()))
|
|
965
|
+
ok(isinstance(rex.get("mutation"), dict)
|
|
966
|
+
and rex["mutation"].get("executed") is True
|
|
967
|
+
and rex["mutation"].get("impact") == imp,
|
|
968
|
+
"K rec['mutation'] 亦带 executed 与影响面(同一份补全形态)")
|
|
969
|
+
cg.close()
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
# =============================================================== L 组:准入闸(批次④)
|
|
973
|
+
def g_l():
|
|
974
|
+
"""批次④(设计 §六/§七):准入闸生效面——full 经 R1–R4 读数、回落不静默。
|
|
975
|
+
|
|
976
|
+
深度判据(R1–R4 四条读数本身、门槛边界、窗口边界)见独立守卫
|
|
977
|
+
`md_cg/test_autonomy_admission.py`;本组只钉**生效面接线**:
|
|
978
|
+
full+满足 ⇒ full(逐位一致)/ full+不满足 ⇒ confirm + alerts 报缺 /
|
|
979
|
+
plan·confirm 不受影响(零 IO 零告警)/ 非法 env fail-closed /
|
|
980
|
+
未结算 = 旧行为(对拍)/ 缓存频度 / 只读。
|
|
981
|
+
"""
|
|
982
|
+
print("== L 组:准入闸(full 经读数 / plan·confirm 不受影响 / 回落不静默)==")
|
|
983
|
+
from . import admission as _adm_mod
|
|
984
|
+
from . import nodefile as _nf
|
|
985
|
+
|
|
986
|
+
def _lroot(tag):
|
|
987
|
+
p = os.path.join(_SANDBOX, "l_%s_r%d" % (tag, _RUN[0]))
|
|
988
|
+
os.makedirs(p, exist_ok=True)
|
|
989
|
+
return p
|
|
990
|
+
|
|
991
|
+
def _lwr(root, rel, rows):
|
|
992
|
+
p = os.path.join(root, rel)
|
|
993
|
+
d = os.path.dirname(p)
|
|
994
|
+
if d:
|
|
995
|
+
os.makedirs(d, exist_ok=True)
|
|
996
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
997
|
+
for r in rows:
|
|
998
|
+
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
|
999
|
+
|
|
1000
|
+
def _lmd(root, layer, nid, created_at):
|
|
1001
|
+
p = os.path.join(root, layer, nid + ".md")
|
|
1002
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
1003
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
1004
|
+
f.write(_nf.dumps({"id": nid, "layer": layer,
|
|
1005
|
+
"created_at": created_at}, "# 探针节点\n"))
|
|
1006
|
+
|
|
1007
|
+
def _sat(tag):
|
|
1008
|
+
"""四条读数全满足的合成库(窗口数据用字面量天,不引用被测常量)。"""
|
|
1009
|
+
r = _lroot(tag)
|
|
1010
|
+
now = time.time()
|
|
1011
|
+
_lwr(r, _adm_mod.FORGETTING_LOG, [
|
|
1012
|
+
{"t": now - 3600, "verdict": "DROP"},
|
|
1013
|
+
{"t": now - 3600, "verdict": "MERGE"},
|
|
1014
|
+
{"t": now - 3600, "verdict": "DEFER"}])
|
|
1015
|
+
_lmd(r, "rejected", "rej_1", now - 3600)
|
|
1016
|
+
_lmd(r, "unresolved", "unr_1", now - 3600)
|
|
1017
|
+
_lwr(r, _adm_mod.DEVICE_AUDIT, [
|
|
1018
|
+
{"t": now - 7200, "op": "forget", "id": "z_1"},
|
|
1019
|
+
{"t": now - 3600, "op": "restore", "id": "z_1", "forced": True}])
|
|
1020
|
+
rows = []
|
|
1021
|
+
for k, n_rej in ((1, 1), (2, 3), (3, 4)):
|
|
1022
|
+
mid = now - (k - 0.5) * 10 * 86400.0
|
|
1023
|
+
rows += [{"t": mid, "decision": "reject"} for _ in range(n_rej)]
|
|
1024
|
+
rows += [{"t": mid, "decision": "accept"} for _ in range(10 - n_rej)]
|
|
1025
|
+
_lwr(r, _adm_mod.DECISIONS_LOG, rows)
|
|
1026
|
+
return r
|
|
1027
|
+
|
|
1028
|
+
try:
|
|
1029
|
+
empty, sat = _lroot("empty"), _sat("sat")
|
|
1030
|
+
# L1 full + 读数满足 ⇒ full(改动前行为逐位一致)
|
|
1031
|
+
with _mode("full"):
|
|
1032
|
+
autonomy_modes.reset_settlement()
|
|
1033
|
+
st = autonomy_modes.settle(root=sat, force=True)
|
|
1034
|
+
ok(st["effective"] == "full" and st["alerts"] == []
|
|
1035
|
+
and autonomy_modes.mode() == "full",
|
|
1036
|
+
"L1 full+读数满足 ⇒ 生效档位 full、零告警(与改动前逐位一致)",
|
|
1037
|
+
st["effective"])
|
|
1038
|
+
# L2 full + 读数不满足 ⇒ 回落 confirm + 报缺哪条(不静默降级)
|
|
1039
|
+
with _mode("full"):
|
|
1040
|
+
autonomy_modes.reset_settlement()
|
|
1041
|
+
st = autonomy_modes.settle(root=empty, force=True)
|
|
1042
|
+
ok(st["effective"] == "confirm" and st["configured"] == "full"
|
|
1043
|
+
and st["settled"] is True,
|
|
1044
|
+
"L2 full+读数不满足 ⇒ 实际生效档位回落 confirm(配置仍记 full)",
|
|
1045
|
+
st["effective"])
|
|
1046
|
+
miss = (st.get("admission") or {}).get("missing") or []
|
|
1047
|
+
ok(bool(miss) and [a["reading"] for a in st["alerts"]] == miss,
|
|
1048
|
+
"L2a 不静默降级:alerts 逐条报缺哪条读数(与读数 missing 一致)",
|
|
1049
|
+
st["alerts"])
|
|
1050
|
+
ok(autonomy_modes.mode() == "confirm"
|
|
1051
|
+
and autonomy_modes.decide("D")["decision"] == "confirm",
|
|
1052
|
+
"L2b 回落贯通判定链:mode() 与 decide(D) 同步 confirm",
|
|
1053
|
+
{"mode": autonomy_modes.mode(), "dec": autonomy_modes.decide("D")["decision"]})
|
|
1054
|
+
# L3 plan/confirm 不受读数影响(零 IO 零告警)
|
|
1055
|
+
autonomy_modes.reset_settlement()
|
|
1056
|
+
for m in ("confirm", "plan"):
|
|
1057
|
+
with _mode(m):
|
|
1058
|
+
n0 = _adm_mod.cache_stats()["checks"]
|
|
1059
|
+
st = autonomy_modes.settle(root=empty, force=True)
|
|
1060
|
+
ok(st["effective"] == m and st["admission"] is None
|
|
1061
|
+
and st["alerts"] == []
|
|
1062
|
+
and _adm_mod.cache_stats()["checks"] == n0,
|
|
1063
|
+
"L3 %s 档不受读数影响(零告警、零读数扫描)" % m,
|
|
1064
|
+
st["effective"])
|
|
1065
|
+
# L4 非法 env 仍 fail-closed
|
|
1066
|
+
with _mode("bogus-x"):
|
|
1067
|
+
try:
|
|
1068
|
+
autonomy_modes.settle(root=empty, force=True)
|
|
1069
|
+
ok(False, "L4 非法 env settle 未报错")
|
|
1070
|
+
except autonomy_modes.AutonomyModeError:
|
|
1071
|
+
ok(True, "L4 非法 env fail-closed(settle 抛 AutonomyModeError)")
|
|
1072
|
+
# L5 未结算 = 旧行为(对拍)
|
|
1073
|
+
autonomy_modes.reset_settlement()
|
|
1074
|
+
with _mode("full"):
|
|
1075
|
+
ok(autonomy_modes.mode() == "full" and autonomy_modes.admission_state()["settled"] is False,
|
|
1076
|
+
"L5 未结算时 mode() 返回配置档位(生效面未启用 = 改动前逐位一致)")
|
|
1077
|
+
# L6 缓存频度(「不得让每次 decide 都全库扫描」)
|
|
1078
|
+
autonomy_modes.reset_settlement()
|
|
1079
|
+
_adm_mod.reset_cache()
|
|
1080
|
+
with _mode("full"):
|
|
1081
|
+
autonomy_modes.settle(root=empty, force=True, ttl=60)
|
|
1082
|
+
autonomy_modes.settle(root=empty, ttl=60)
|
|
1083
|
+
autonomy_modes.settle(root=empty, ttl=60)
|
|
1084
|
+
ok(_adm_mod.cache_stats()["checks"] == 1,
|
|
1085
|
+
"L6 读数缓存:TTL 内重复结算不重扫(热路径零 IO)",
|
|
1086
|
+
_adm_mod.cache_stats())
|
|
1087
|
+
# L7 只读(只告警不改数据)
|
|
1088
|
+
fp0 = _adm_mod._fingerprint(sat)
|
|
1089
|
+
autonomy_modes.reset_settlement()
|
|
1090
|
+
with _mode("full"):
|
|
1091
|
+
autonomy_modes.settle(root=sat, force=True)
|
|
1092
|
+
ok(_adm_mod._fingerprint(sat) == fp0 and len(fp0) > 0,
|
|
1093
|
+
"L7 只告警不改数据:结算前后库指纹逐位相同", len(fp0))
|
|
1094
|
+
# L8 结算态可查(含 alerts)
|
|
1095
|
+
autonomy_modes.reset_settlement()
|
|
1096
|
+
with _mode("full"):
|
|
1097
|
+
autonomy_modes.settle(root=empty, force=True)
|
|
1098
|
+
ok(autonomy_modes.admission_state()["effective"] == "confirm"
|
|
1099
|
+
and autonomy_modes.admission_state()["alerts"] != [],
|
|
1100
|
+
"L8 结算态可查:admission_state() 返回回落体与 alerts")
|
|
1101
|
+
finally:
|
|
1102
|
+
autonomy_modes.reset_settlement()
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
# =============================================================== M 组:收官补缺
|
|
1106
|
+
def g_m():
|
|
1107
|
+
"""收官批次(步骤⑤):覆盖矩阵**未覆盖格**补缺(plan 档热路径 + E 分数类)。
|
|
1108
|
+
|
|
1109
|
+
覆盖矩阵(3 档 × A–E × 四套守卫)清点见
|
|
1110
|
+
`docs/eval/三档自治_步骤⑤收官与全链验收_落码记录_v1.0.md`;本组补的是矩阵
|
|
1111
|
+
里此前**无断言**的格:
|
|
1112
|
+
|
|
1113
|
+
· plan 档热路径四面:写链 C 覆写(非 gated)/gated 面 B 合并(MERGE
|
|
1114
|
+
落点)/限流闸 CONVERGE 落点/工具面 D 删除——此前 plan 档只有
|
|
1115
|
+
decide 级判据(A 组)与 gated A 新增(I 组 ④);
|
|
1116
|
+
· E 分数类(权重/生命周期)在档位面的**行为读数**。
|
|
1117
|
+
补强批次(v1.2)扩展:
|
|
1118
|
+
· **M1e**:`mdcg_remember` **非 gated 直写分支**(`mcp_server.py:3373`)
|
|
1119
|
+
三档行为——复核 U1 的解除项(此前 plan/confirm 两档均无断言);
|
|
1120
|
+
· **M2 重编码**:E 面已接档位闸(`e_gate` 接在四处 apply 写盘路径)
|
|
1121
|
+
⇒ 判据由「plan≡confirm(现状钉)」改为「plan 被拦 + confirm/full
|
|
1122
|
+
逐位相等」(**意图不变**,同 issue50-a 的 E1 先例)。
|
|
1123
|
+
|
|
1124
|
+
边界(如实:设计内语义 vs 留池缺口分列,不混淆):
|
|
1125
|
+
· plan 档 **C/D 与 confirm 同格**(需确认 ⇒ 出单,非 forbid)——设计 §三
|
|
1126
|
+
矩阵逐格如此,M1/M1d 钉的就是「出单且不落盘」;
|
|
1127
|
+
· plan 档 **A/B/E 无计划输入即 forbid**(设计 §三 硬约束③)——属设计内
|
|
1128
|
+
语义,**不并入** confirm↔full 不动面对拍范围(对拍锚只含 confirm/full);
|
|
1129
|
+
· **E 面档位闸已接线**(补强批次 v1.2):plan 档 E 类动作
|
|
1130
|
+
(`freshness.recalc` / `weights.recalc` / `lifecycle.set_state` /
|
|
1131
|
+
`lifecycle.backfill` 的 **apply 路径**)fail-closed 且零写盘;
|
|
1132
|
+
confirm/full 两档原链原样(逐位不变)。**未接**的三处入口及理由见
|
|
1133
|
+
`autonomy_modes.e_gate()` docstring(`lifecycle.stamp` 属 B 链/库层
|
|
1134
|
+
`require_transition` 禁接线/分数类回滚面待裁)——**计划输入面**仍留池:
|
|
1135
|
+
现状 `plan=None` ⇒ plan 档 E 一律 fail-closed,「命中计划步骤」的
|
|
1136
|
+
allow 分支待计划输入面接线(`autonomy_modes` 模块 docstring「本批不做」)。
|
|
1137
|
+
|
|
1138
|
+
confirm/full 两档的 A/E 逐位对拍、与旧基线的 oracle 对拍、端到端回滚演练
|
|
1139
|
+
(跨写链/插件面/执行桥/回滚 CLI)在独立守卫 `md_cg/test_mode_parity.py`。
|
|
1140
|
+
"""
|
|
1141
|
+
print("== M 组:收官补缺(plan 档热路径四面 + E 分数类在档位面)==")
|
|
1142
|
+
from . import freshness as _fresh
|
|
1143
|
+
# ---- M1 写链 C 覆写(plan 档,非 gated 面)----
|
|
1144
|
+
with _mode("plan"):
|
|
1145
|
+
cg = _lib("m_plan_c")
|
|
1146
|
+
cg.add("mp_c", PLAIN % "初版", layer="knowledge",
|
|
1147
|
+
verification_basis="test")
|
|
1148
|
+
cg.flush()
|
|
1149
|
+
o = _pipe().execute(cg, {"content_kind": "text", "content": PLAIN % "改写版",
|
|
1150
|
+
"layer": "knowledge", "node_id": "mp_c"})
|
|
1151
|
+
pay = o.get("mutation") or {}
|
|
1152
|
+
ok(o.get("moved_to") == "review_queue" and o.get("committed") is False
|
|
1153
|
+
and bool(o.get("pid")) and pay.get("action") == "C"
|
|
1154
|
+
and pay.get("target") == "mp_c",
|
|
1155
|
+
"M1 写链 C 覆写(plan 档)出变更单且不落盘(C 在 plan/confirm 同格,"
|
|
1156
|
+
"此前写链面 plan 档无断言)",
|
|
1157
|
+
{k: o.get(k) for k in ("moved_to", "committed", "pid")})
|
|
1158
|
+
ok("改写版" not in (cg.get("mp_c") or {}).get("content", "")
|
|
1159
|
+
and "初版" in (cg.get("mp_c") or {}).get("content", ""),
|
|
1160
|
+
"M1 出单时目标正文**未被覆写**(逐字比对)")
|
|
1161
|
+
cg.close()
|
|
1162
|
+
|
|
1163
|
+
# ---- M1b gated 面 B 合并(plan 档,MERGE 落点)----
|
|
1164
|
+
with _mode("plan"):
|
|
1165
|
+
cg = _lib("m_plan_b")
|
|
1166
|
+
cg.add("mp_b", BODY % "9090", layer="knowledge",
|
|
1167
|
+
verification_basis="test")
|
|
1168
|
+
cg.flush()
|
|
1169
|
+
r = cg.remember_gated("mp_bn", BODY % "9595", layer="knowledge")
|
|
1170
|
+
ok(r.get("verdict") == "FORBID"
|
|
1171
|
+
and r.get("moved_to") == "autonomy_forbidden"
|
|
1172
|
+
and r.get("merged_into") == "mp_b"
|
|
1173
|
+
and "计划" in str(r.get("hint") or ""),
|
|
1174
|
+
"M1b gated 面 B 合并(plan 档·MERGE 落点)fail-closed:verdict=FORBID "
|
|
1175
|
+
"+ 计划外零变更 hint(此前无断言)",
|
|
1176
|
+
{k: r.get(k) for k in ("verdict", "moved_to", "merged_into")})
|
|
1177
|
+
t = cg.get("mp_b") or {}
|
|
1178
|
+
ok("9595" not in (t.get("content") or "")
|
|
1179
|
+
and not ((t.get("frontmatter") or {}).get("merge_count"))
|
|
1180
|
+
and len(_mut_entries(cg.review_list())) == 0,
|
|
1181
|
+
"M1b 未合并:目标正文/merge_count 不动、零变更单(未落盘、未出单)",
|
|
1182
|
+
(t.get("frontmatter") or {}).get("merge_count"))
|
|
1183
|
+
cg.close()
|
|
1184
|
+
|
|
1185
|
+
# ---- M1c 限流闸 CONVERGE 落点(plan 档;锚点须在允许档先落)----
|
|
1186
|
+
cg = _lib("m_plan_cv")
|
|
1187
|
+
with _mode("confirm"):
|
|
1188
|
+
r1 = cg.remember_gated("mp_x1", "批次107 收官:指标面 9090 正常",
|
|
1189
|
+
layer="contextual")
|
|
1190
|
+
with _mode("plan"):
|
|
1191
|
+
r2 = cg.remember_gated("mp_x2", "批次108 收官:指标面 9595 正常",
|
|
1192
|
+
layer="contextual")
|
|
1193
|
+
ok(r1.get("verdict") == "ACCEPT" and r2.get("verdict") == "FORBID"
|
|
1194
|
+
and r2.get("moved_to") == "autonomy_forbidden",
|
|
1195
|
+
"M1c 限流闸 CONVERGE(plan 档)fail-closed:verdict=FORBID、"
|
|
1196
|
+
"moved_to=autonomy_forbidden(锚点在 confirm 档落盘、切 plan 后"
|
|
1197
|
+
"第二篇被拦——此前无断言)",
|
|
1198
|
+
{"r1": r1.get("verdict"), "r2": r2.get("verdict")})
|
|
1199
|
+
ok("9595" not in ((cg.get("mp_x1") or {}).get("content") or ""),
|
|
1200
|
+
"M1c CONVERGE 未并入:目标正文不含新值(合并原语未被调用)")
|
|
1201
|
+
cg.close()
|
|
1202
|
+
|
|
1203
|
+
# ---- M1d 工具面 D 删除(plan 档)----
|
|
1204
|
+
with _mode("plan"):
|
|
1205
|
+
cg = _lib("m_plan_d")
|
|
1206
|
+
cg.add("mp_d", BODY % "9090", layer="knowledge",
|
|
1207
|
+
verification_basis="test")
|
|
1208
|
+
cg.flush()
|
|
1209
|
+
r = cg.forget_gated("mp_d", reason="守卫:plan 档删除")
|
|
1210
|
+
ok(r.get("moved_to") == "review_queue" and r.get("deleted") is False
|
|
1211
|
+
and (r.get("mutation") or {}).get("action") == "D"
|
|
1212
|
+
and cg.get("mp_d") is not None,
|
|
1213
|
+
"M1d 工具面 D 删除(plan 档)出变更单且不软删(deleted=False 明示、"
|
|
1214
|
+
"节点原样在位——此前无断言)",
|
|
1215
|
+
{k: r.get(k) for k in ("moved_to", "deleted", "ok")})
|
|
1216
|
+
cg.close()
|
|
1217
|
+
|
|
1218
|
+
# ---- M1e 直写面(mdcg_remember 非 gated)三档:plan/confirm 出单、full 直落 ----
|
|
1219
|
+
# U1 解除项:`mcp_server.py:3373` 的 `_dec = _am.decide(_action)` 所在分支
|
|
1220
|
+
# (`_dispatch` 的 `mdcg_remember` 非 gated 直写面)此前 **plan 与 confirm
|
|
1221
|
+
# 两档均无行为断言**(复核 V4' 注入 0 红存活、不限档位摘除变体亦全绿);
|
|
1222
|
+
# 本组形态同 M1(写链面):出单且不落盘 / 正文逐字未动 / 队列计数 + full 直落。
|
|
1223
|
+
def _direct_probe(mode, tag):
|
|
1224
|
+
with _mode(mode):
|
|
1225
|
+
cg = _lib(tag)
|
|
1226
|
+
cg.add("ud_c", PLAIN % "初版", layer="knowledge",
|
|
1227
|
+
verification_basis="test")
|
|
1228
|
+
cg.flush()
|
|
1229
|
+
o = mcp_server._dispatch(cg, "mdcg_remember",
|
|
1230
|
+
{"content": PLAIN % "改写版",
|
|
1231
|
+
"node_id": "ud_c", "layer": "knowledge"})
|
|
1232
|
+
body = (cg.get("ud_c") or {}).get("content") or ""
|
|
1233
|
+
orders = _mut_entries(cg.review_list())
|
|
1234
|
+
cg.close()
|
|
1235
|
+
return o, body, orders
|
|
1236
|
+
|
|
1237
|
+
o_p, b_p, m_p = _direct_probe("plan", "m_direct_plan")
|
|
1238
|
+
ok(o_p.get("moved_to") == "review_queue" and o_p.get("committed") is False
|
|
1239
|
+
and bool(o_p.get("pid"))
|
|
1240
|
+
and (o_p.get("mutation") or {}).get("action") == "C"
|
|
1241
|
+
and (o_p.get("mutation") or {}).get("target") == "ud_c",
|
|
1242
|
+
"M1e① 直写面 C 覆写(plan 档·mdcg_remember 非 gated)出变更单且不落盘"
|
|
1243
|
+
"(moved_to=review_queue、committed=False、单载荷 action=C/target 齐"
|
|
1244
|
+
"——复核 U1:此前该分支 plan 档无行为断言)",
|
|
1245
|
+
{k: o_p.get(k) for k in ("moved_to", "committed", "pid")})
|
|
1246
|
+
ok("改写版" not in b_p and "初版" in b_p and len(m_p) == 1,
|
|
1247
|
+
"M1e② plan 档出单时目标正文**未被覆写**(逐字比对)且队列恰 1 张",
|
|
1248
|
+
{"orders": len(m_p), "body": b_p[:24]})
|
|
1249
|
+
o_c, b_c, m_c = _direct_probe("confirm", "m_direct_confirm")
|
|
1250
|
+
ok(o_c.get("moved_to") == "review_queue" and o_c.get("committed") is False
|
|
1251
|
+
and bool(o_c.get("pid"))
|
|
1252
|
+
and (o_c.get("mutation") or {}).get("action") == "C"
|
|
1253
|
+
and "改写版" not in b_c and "初版" in b_c and len(m_c) == 1,
|
|
1254
|
+
"M1e③ 直写面 C 覆写(confirm 档)出变更单且不落盘(正文未改、队列 1 张"
|
|
1255
|
+
"——复核 U1:此前该分支 confirm 档亦无断言)",
|
|
1256
|
+
{k: o_c.get(k) for k in ("moved_to", "committed", "pid")})
|
|
1257
|
+
o_f, b_f, m_f = _direct_probe("full", "m_direct_full")
|
|
1258
|
+
ok(o_f.get("ok") is True and o_f.get("moved_to") is None
|
|
1259
|
+
and o_f.get("overwrite_of") == "ud_c" and "改写版" in b_f
|
|
1260
|
+
and "初版" not in b_f,
|
|
1261
|
+
"M1e④ 直写面 C 覆写(full 档)**直落**:ok=True、无单、正文已换"
|
|
1262
|
+
"(overwrite_of 读数齐)",
|
|
1263
|
+
{k: o_f.get(k) for k in ("ok", "moved_to", "overwrite_of")})
|
|
1264
|
+
ok(len(m_f) == 0,
|
|
1265
|
+
"M1e⑤ full 档同面零变更单(直落面不产单——与 confirm 链的分界)",
|
|
1266
|
+
len(m_f))
|
|
1267
|
+
|
|
1268
|
+
# ---- M2 E 分数类在档位面(补强批次 v1.2:按标题意图重编码,意图不变)----
|
|
1269
|
+
# 重编码的根据:E 面**已接档位闸**(autonomy_modes.e_gate 接在
|
|
1270
|
+
# weights.recalc / freshness.recalc / lifecycle.set_state / lifecycle.backfill
|
|
1271
|
+
# 的 apply 写盘路径)。故 M2 的判据由「plan≡confirm(现状钉、非背书)」改为
|
|
1272
|
+
# 「**plan 被拦 + confirm/full 逐位相等**」——断言意图(E 在 plan 档须受
|
|
1273
|
+
# 计划约束、confirm/full 不阻塞)一字未改,改的是编码(同 issue50-a 的 E1
|
|
1274
|
+
# 先例)。**本组不再钉「E 面无接线」**:那正是补强批次要消灭的留池项。
|
|
1275
|
+
def _e_fresh(mode, tag):
|
|
1276
|
+
with _mode(mode):
|
|
1277
|
+
cg = _lib(tag)
|
|
1278
|
+
cg.add("me_1", BODY % "9090", layer="knowledge",
|
|
1279
|
+
verification_basis="test", importance=0.4)
|
|
1280
|
+
cg.add("me_2", PLAIN % "被引", layer="knowledge",
|
|
1281
|
+
verification_basis="test",
|
|
1282
|
+
edges=[{"target": "me_1", "relation_type": "part_of"}])
|
|
1283
|
+
cg.flush()
|
|
1284
|
+
out = _fresh.recalc(cg, apply=True, min_delta=0.0001,
|
|
1285
|
+
now=1700000000.0)
|
|
1286
|
+
fm = dict(((cg.get("me_1") or {}).get("frontmatter") or {}))
|
|
1287
|
+
cg.close()
|
|
1288
|
+
return out, fm
|
|
1289
|
+
|
|
1290
|
+
op_, fp = _e_fresh("plan", "m_e_plan")
|
|
1291
|
+
oc, fc = _e_fresh("confirm", "m_e_conf")
|
|
1292
|
+
of, ff = _e_fresh("full", "m_e_full")
|
|
1293
|
+
ok(op_.get("ok") is False and op_.get("error") == "autonomy_forbidden"
|
|
1294
|
+
and "计划" in str(op_.get("hint") or "")
|
|
1295
|
+
and op_.get("written") == 0,
|
|
1296
|
+
"M2① E 面 plan 档**被拦**:freshness.recalc(apply=True) fail-closed"
|
|
1297
|
+
"(ok=False/error=autonomy_forbidden/带「计划外零变更」hint/written=0)",
|
|
1298
|
+
{k: op_.get(k) for k in ("ok", "error", "written")})
|
|
1299
|
+
ok(fp.get("freshness_weight") is None
|
|
1300
|
+
and fp.get("freshness_source") is None,
|
|
1301
|
+
"M2② plan 档**零写盘**:盘面无 freshness_weight/freshness_source"
|
|
1302
|
+
"(与 confirm 档对照——fail-closed 不是「报错但仍写」)",
|
|
1303
|
+
{k: fp.get(k) for k in ("freshness_weight", "freshness_source")})
|
|
1304
|
+
ok(oc.get("ok") is True and int(oc.get("written") or 0) >= 1
|
|
1305
|
+
and fc.get("freshness_weight") is not None,
|
|
1306
|
+
"M2③ confirm 档**照落**:written≥1 且 freshness_weight 已写盘"
|
|
1307
|
+
"(E 列 confirm = 允许,分数类不阻塞)",
|
|
1308
|
+
{k: oc.get(k) for k in ("ok", "written", "error")})
|
|
1309
|
+
ok(_strip(oc) == _strip(of) and _strip(fc) == _strip(ff)
|
|
1310
|
+
and ff.get("freshness_weight") is not None,
|
|
1311
|
+
"M2④ E 接线**只在 plan 档生效**:confirm 与 full 两档输出**逐位相等**、"
|
|
1312
|
+
"落盘 fm(剔除时钟面)逐位相等(「confirm/full 全线逐位不变」硬约束的"
|
|
1313
|
+
"E 面锚)",
|
|
1314
|
+
{"out_same": _strip(oc) == _strip(of),
|
|
1315
|
+
"fm_diff": [k for k in set(fc) | set(ff)
|
|
1316
|
+
if _strip(fc).get(k) != _strip(ff).get(k)]})
|
|
1317
|
+
with _mode("plan"):
|
|
1318
|
+
cg = _lib("m_e_dry")
|
|
1319
|
+
cg.add("me_1", BODY % "9090", layer="knowledge",
|
|
1320
|
+
verification_basis="test", importance=0.4)
|
|
1321
|
+
cg.flush()
|
|
1322
|
+
dr = _fresh.recalc(cg, apply=False, min_delta=0.0001, now=1700000000.0)
|
|
1323
|
+
cg.close()
|
|
1324
|
+
ok(dr.get("ok") is True and dr.get("dry_run") is True
|
|
1325
|
+
and int(dr.get("changed") or 0) >= 1,
|
|
1326
|
+
"M2⑤ 闸只在 **apply 路径**:plan 档预演(apply=False)照旧出报表"
|
|
1327
|
+
"(零写盘面不接闸——接线面即「变更」面)",
|
|
1328
|
+
{k: dr.get(k) for k in ("ok", "dry_run", "changed")})
|
|
1329
|
+
|
|
1330
|
+
def _e_weights(mode, tag):
|
|
1331
|
+
with _mode(mode):
|
|
1332
|
+
cg = _lib(tag)
|
|
1333
|
+
cg.add("mw_1", BODY % "9090", layer="knowledge",
|
|
1334
|
+
verification_basis="test", importance=0.4)
|
|
1335
|
+
cg.add("mw_2", PLAIN % "被引", layer="knowledge",
|
|
1336
|
+
verification_basis="test",
|
|
1337
|
+
edges=[{"target": "mw_1", "relation_type": "part_of"}])
|
|
1338
|
+
cg.flush()
|
|
1339
|
+
out = weights.recalc(cg, apply=True, min_delta=0.0001)
|
|
1340
|
+
fm = dict(((cg.get("mw_1") or {}).get("frontmatter") or {}))
|
|
1341
|
+
cg.close()
|
|
1342
|
+
return out, fm
|
|
1343
|
+
|
|
1344
|
+
wp, fwp = _e_weights("plan", "m_w_plan")
|
|
1345
|
+
wc, fwc = _e_weights("confirm", "m_w_conf")
|
|
1346
|
+
ok(wp.get("ok") is False and wp.get("error") == "autonomy_forbidden"
|
|
1347
|
+
and fwp.get("importance") == 0.4,
|
|
1348
|
+
"M2⑥ weights.recalc(第二写盘入口)plan 档被拦且 importance 盘面未改"
|
|
1349
|
+
"(接线前实测 0.5→0.57 已写盘——本条即该缺口的机械钉)",
|
|
1350
|
+
{k: wp.get(k) for k in ("ok", "error")})
|
|
1351
|
+
ok(wc.get("ok") is True and int(wc.get("written") or 0) >= 1
|
|
1352
|
+
and fwc.get("importance") != 0.4,
|
|
1353
|
+
"M2⑦ weights.recalc confirm 档照落(importance 已重算写盘)",
|
|
1354
|
+
{"written": wc.get("written"), "imp": fwc.get("importance")})
|
|
1355
|
+
|
|
1356
|
+
def _e_state(mode, tag):
|
|
1357
|
+
with _mode(mode):
|
|
1358
|
+
cg = _lib(tag)
|
|
1359
|
+
cg.add("ml_1", BODY % "9090", layer="knowledge",
|
|
1360
|
+
verification_basis="test")
|
|
1361
|
+
cg.flush()
|
|
1362
|
+
out = _lc.set_state(cg, "ml_1", "converged", reason="守卫",
|
|
1363
|
+
actor="guard")
|
|
1364
|
+
fm = dict(((cg.get("ml_1") or {}).get("frontmatter") or {}))
|
|
1365
|
+
cg.close()
|
|
1366
|
+
return out, fm
|
|
1367
|
+
|
|
1368
|
+
sp, fsp = _e_state("plan", "m_l_plan")
|
|
1369
|
+
sc, fsc = _e_state("confirm", "m_l_conf")
|
|
1370
|
+
ok(sp.get("ok") is False and sp.get("error") == "autonomy_forbidden"
|
|
1371
|
+
and sp.get("changed") is False
|
|
1372
|
+
and fsp.get(_lc.STATE_FIELD) == "active",
|
|
1373
|
+
"M2⑧ lifecycle.set_state(第三写盘入口)plan 档被拦:ok=False + 状态未推进"
|
|
1374
|
+
"(盘面仍 active;资格面在前——非法迁移仍走自己的 code)",
|
|
1375
|
+
{k: sp.get(k) for k in ("ok", "error", "changed", "code")})
|
|
1376
|
+
ok(sc.get("ok") is True and sc.get("changed") is True
|
|
1377
|
+
and fsc.get(_lc.STATE_FIELD) == "converged",
|
|
1378
|
+
"M2⑨ lifecycle.set_state confirm 档照落(状态推进 = converged)",
|
|
1379
|
+
{"changed": sc.get("changed"), "state": fsc.get(_lc.STATE_FIELD)})
|
|
1380
|
+
|
|
1381
|
+
def _e_backfill(mode, tag):
|
|
1382
|
+
"""盘面直造**缺 lifecycle_state** 的节点(backfill 的「缺字段」前提)。"""
|
|
1383
|
+
with _mode(mode):
|
|
1384
|
+
cg = _lib(tag)
|
|
1385
|
+
p = os.path.join(cg.root, "knowledge", "ml_bare.md")
|
|
1386
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
1387
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
1388
|
+
f.write(_nf.dumps({"id": "ml_bare", "layer": "knowledge",
|
|
1389
|
+
"created_at": 1700000000.0}, "# 探针节点\n"))
|
|
1390
|
+
cg.rebuild_index()
|
|
1391
|
+
out = _lc.backfill(cg, apply=True)
|
|
1392
|
+
fm = dict(((cg.get("ml_bare") or {}).get("frontmatter") or {}))
|
|
1393
|
+
cg.close()
|
|
1394
|
+
return out, fm
|
|
1395
|
+
|
|
1396
|
+
bp, fbp = _e_backfill("plan", "m_b_plan")
|
|
1397
|
+
bc, fbc = _e_backfill("confirm", "m_b_conf")
|
|
1398
|
+
ok(bp.get("ok") is False and bp.get("error") == "autonomy_forbidden"
|
|
1399
|
+
and bp.get("backfilled") == 0 and bp.get("missing") == 1
|
|
1400
|
+
and fbp.get(_lc.STATE_FIELD) is None,
|
|
1401
|
+
"M2⑩ lifecycle.backfill(第四写盘入口)plan 档被拦:盘点出 1 个缺字段节点"
|
|
1402
|
+
"仍 fail-closed、零回填(盘面仍无 state 键)",
|
|
1403
|
+
{k: bp.get(k) for k in ("ok", "error", "missing", "backfilled")})
|
|
1404
|
+
ok(bc.get("ok") is True and bc.get("backfilled") == 1
|
|
1405
|
+
and fbc.get(_lc.STATE_FIELD) == "active",
|
|
1406
|
+
"M2⑪ lifecycle.backfill confirm 档照落(缺字段节点显式回填 active)",
|
|
1407
|
+
{k: bc.get(k) for k in ("ok", "backfilled")})
|
|
1408
|
+
ok(autonomy_modes.decide("A", mode_explicit="plan")["decision"] == "forbid"
|
|
1409
|
+
and autonomy_modes.decide("E", mode_explicit="plan")["decision"] == "forbid",
|
|
1410
|
+
"M2⑫ 设计内语义单独钉:plan 档 A/E 无计划即 forbid(计划输入面留池)"
|
|
1411
|
+
"——**不并入** confirm↔full 不动面对拍范围")
|
|
1412
|
+
|
|
1413
|
+
# ---- F1 收口自证(2026-10-03):防误删自检的判别力 ----
|
|
1414
|
+
# (合成源三形态 + 真实表对照;判别力钉=`_SELFCHECK_MUTATION`,不占表内编号)
|
|
1415
|
+
# 合成源=表体最小形态(编号用真实字符);删法各一 + 真实表对照 + 表长兜底。
|
|
1416
|
+
_S_DROP_TUPLE = ("x\n_SRC_MUTATIONS = (\n"
|
|
1417
|
+
" # ① 甲\n (\"a\",),\n"
|
|
1418
|
+
" # ② 乙\n" # ← ② 删元组留注释(F1 原形)
|
|
1419
|
+
" # ③ 丙\n (\"c\",),\n)\n")
|
|
1420
|
+
_S_DROP_BOTH = ("x\n_SRC_MUTATIONS = (\n"
|
|
1421
|
+
" # ① 甲\n (\"a\",),\n"
|
|
1422
|
+
" # ③ 丙\n (\"c\",),\n)\n") # ← ② 注释+元组同删
|
|
1423
|
+
_self_src = io.open(os.path.abspath(__file__), encoding="utf-8").read()
|
|
1424
|
+
ok(_table_gaps(_S_DROP_TUPLE, 2, ("①", "②", "③")) == ["②", "len:2≠3"],
|
|
1425
|
+
"F1① 防误删自检判别力:删元组留注释(F1 原形)⇒ 报缺口编号 ②"
|
|
1426
|
+
"(表长兜底随报——此前该形态无任何机械载体)",
|
|
1427
|
+
_table_gaps(_S_DROP_TUPLE, 2, ("①", "②", "③")))
|
|
1428
|
+
ok(_table_gaps(_S_DROP_BOTH, 2, ("①", "②", "③")) == ["②", "len:2≠3"],
|
|
1429
|
+
"F1② 防误删自检判别力:注释与元组同删 ⇒ 仍报缺口编号 ②(编号序列比对"
|
|
1430
|
+
"判据)",
|
|
1431
|
+
_table_gaps(_S_DROP_BOTH, 2, ("①", "②", "③")))
|
|
1432
|
+
ok(_table_integrity_check() == [],
|
|
1433
|
+
"F1③ 真实表完好(**自动核验载体**):编号 ①–㉔ 无缺口、表长 %d = 声明"
|
|
1434
|
+
"——「24 处」不再只是文档声称,而是每次运行自动核验" % len(_MUTATION_IDS),
|
|
1435
|
+
_table_integrity_check())
|
|
1436
|
+
_n1, _n2 = len(_SRC_MUTATIONS) - 1, len(_SRC_MUTATIONS)
|
|
1437
|
+
ok(_table_gaps(_self_src, _n1) == ["len:%d≠%d" % (_n1, _n2)],
|
|
1438
|
+
"F1④ 表长判据兜底:真实源 + 表长-1 ⇒ 报「len:%d≠%d」(防「编号齐但"
|
|
1439
|
+
"条目缺」的边角形态)" % (_n1, _n2),
|
|
1440
|
+
_table_gaps(_self_src, _n1))
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
_GROUPS = (g_a, g_b, g_c, g_d, g_e, g_f, g_g, g_h, g_i, g_j, g_k, g_l, g_m)
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def _run_groups():
|
|
1447
|
+
"""跑全部断言组(静默),返回失败数——供变异自证复用。"""
|
|
1448
|
+
_PASS.clear()
|
|
1449
|
+
_FAIL.clear()
|
|
1450
|
+
for g in _GROUPS:
|
|
1451
|
+
g()
|
|
1452
|
+
return len(_FAIL)
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
# =============================================================== 定点变异自证
|
|
1456
|
+
# 表内每项 = (说明, 目标, 锚点原文, 替换文, 预期红项数)。锚点须**逐字**出现在
|
|
1457
|
+
# 目标函数源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。
|
|
1458
|
+
# 目标 = ("mod", 模块对象, 函数名) 或 ("cls", 类对象, 方法名)。
|
|
1459
|
+
#
|
|
1460
|
+
#: 本守卫模块对象——「防误删自检」的判别力钉(`_SELFCHECK_MUTATION`)以本模块为属主
|
|
1461
|
+
#: (`sys.modules[__name__]` 是同一对象的现取形态,不写第二处名字字面量;
|
|
1462
|
+
#: 同 `md_cg/test_mode_parity.py` 的 `_SELF` 先例)。
|
|
1463
|
+
_THIS = sys.modules[__name__]
|
|
1464
|
+
_SRC_MUTATIONS = (
|
|
1465
|
+
# ①「档位不生效」——env 读取口恒返回真源缺省(confirm):全档位退化成一个档。
|
|
1466
|
+
# 锚点(批次④改):env 解析单点从 mode() 抽到 _configured_mode()(settle() 与
|
|
1467
|
+
# mode() 共用;表达式文字逐字不变)——变异目标随之挪到该单点。
|
|
1468
|
+
# 红 27(实测 2026-10-03 补强批次 v1.2 校准 20→27):20(收官批次)=9 条
|
|
1469
|
+
# (A 组非法值/归一两条 + full 档直落三条(B/H)+ plan 档 fail-closed 一条
|
|
1470
|
+
# + full 档零变更单一条 + I 组两条)+ L 组 8 条(L1 full+满足失效、L2 结算体
|
|
1471
|
+
# 形状、L2a 缺项告警、L3 plan 档(配置恒 confirm ⇒ plan 也退化)、L4 非法值
|
|
1472
|
+
# fail-closed 失效、L5 未结算对拍、L6 缓存零 IO、L8 结算态——L2b 与 L7 语义
|
|
1473
|
+
# 相符仍绿)+ 收官 M 组 3(M1b「MERGE 落点 FORBID」两条 + M1c「CONVERGE 落点
|
|
1474
|
+
# FORBID」第一条;M1(C 覆写)与 M1d(D 删除)在 plan/confirm 同格 ⇒ 相符仍绿);
|
|
1475
|
+
# 补强批次 v1.2 +7 = M1e④⑤(full 臂退化 ⇒ 直写面不再直落:出单且不落盘、
|
|
1476
|
+
# 非零变更单)+ M2①(plan 档 E 被拦判据失效——退化后照落)+ M2②(plan 零写盘)
|
|
1477
|
+
# + M2⑥(weights plan 被拦)+ M2⑧(set_state plan 被拦)+ M2⑩(backfill plan
|
|
1478
|
+
# 被拦)。M2⑤(预演不接闸)与 M2③④⑦⑨⑪⑫ 语义相符仍绿。
|
|
1479
|
+
("档位不生效(配置档位恒返回表内缺省)", "mod", autonomy_modes,
|
|
1480
|
+
"_configured_mode",
|
|
1481
|
+
' raw = autonomy_env("mode", environ)', ' raw = DEFAULT_MODE', 27),
|
|
1482
|
+
# ②「confirm 也直落」——矩阵的 confirm 格被当成放行(确认形同虚设)。
|
|
1483
|
+
# 红 46(实测 2026-10-03 补强批次 v1.2 校准 43→46):43(收官批次)=
|
|
1484
|
+
# 原 40(批次④)=A 组 6 条矩阵/hint 判据 + B/C/D/E 组全部「出单/不落盘/
|
|
1485
|
+
# 载荷/accept 执行/幂等」判据(确认路径整体消失)+ 补强批次 I 组 14 条 +
|
|
1486
|
+
# L 组 L2b,+收官 M 组 3(M1 两条:写链 C 覆写 plan 档不再出单——直落且正文
|
|
1487
|
+
# 被覆写 + M1d:工具面 D 删除不再出单、直接软删;M1b/M1c 走 FORBID 格而非
|
|
1488
|
+
# CONFIRM 格,相符仍绿);补强批次 +3 = M1e①②③(**plan 档 C 的矩阵格也是
|
|
1489
|
+
# CONFIRM** ⇒ 该格被当放行后直写面 plan/confirm 两臂全部直落:plan 两条
|
|
1490
|
+
# 「出单/正文未改」+ confirm 一条同形)。M2 各条走 PLAN_STEP/ALLOW 格,
|
|
1491
|
+
# 语义相符仍绿。
|
|
1492
|
+
("confirm 也直落(decide 把 confirm 格判成 allow)", "mod", autonomy_modes,
|
|
1493
|
+
"decide", ' if cell == CONFIRM:', ' if False:', 46),
|
|
1494
|
+
# ③ 矩阵整表坍缩成 full 列——「档位不生效」的真源版(判据被绕过、表仍在)。
|
|
1495
|
+
# 红 63(实测 2026-10-03 补强批次 v1.2 校准 55→63):55(收官批次)=
|
|
1496
|
+
# 原 47(批次④)=A 组 15 格 + confirm/plan 相关判据 + 三个落点的出单判据 +
|
|
1497
|
+
# 补强批次 I 组 15 + L 组 L2b,+收官 M 组 8(M1 两条 + M1b 两条 + M1c 两条
|
|
1498
|
+
# (CONVERGE 直接合并)+ M1d 一条 + M2 第三条(plan 档 A/E 的 forbid 语义消失));
|
|
1499
|
+
# 补强批次 +8 = M1e①②③(全档坍缩 full ⇒ 直写面 plan/confirm 也直落:出单/
|
|
1500
|
+
# 正文未改/队列计数三条判据全失)+ M2① ② ⑥ ⑧ ⑩(plan 档 E 五处判据——
|
|
1501
|
+
# 坍缩后 E 直接允许、照落写盘)。M2⑤(预演不接闸)与 M2③④⑦⑨⑪⑫ 相符仍绿。
|
|
1502
|
+
# F1 收口(2026-10-03,本批):本元组曾在工作树被删(仅剩注释,红 63 的形态
|
|
1503
|
+
# 与注释都在、元组没了——删除原因无留痕、不归因);自 `git show HEAD:` 取回
|
|
1504
|
+
# (原 expect 47 = 历史值),按现行树勘定锚点、预期红项数按现行树实测校准。
|
|
1505
|
+
("矩阵整表坍缩成 full(档位不生效·真源版)", "mod", autonomy_modes, "decide",
|
|
1506
|
+
' cell = MATRIX[m][act]',
|
|
1507
|
+
' cell = MATRIX["full"][act]', 63),
|
|
1508
|
+
# ④ 写链档位闸空转——C 改写不再出单(直接覆写)。
|
|
1509
|
+
# 红 8(实测 2026-10-03 收官批次校准 6→8):原 6(批次②)=B 组 C 改写 5 条
|
|
1510
|
+
# (出单/未改写/载荷/队列/accept)+ plan 档 A 新增 fail-closed 一条;
|
|
1511
|
+
# 收官批次 +2 = M 组 M1 两条(plan 档写链 C 覆写的出单与「未覆写」判据)。
|
|
1512
|
+
("写链档位闸不生效(_gate_autonomy 恒放行)", "mod", writepipe,
|
|
1513
|
+
"_gate_autonomy", ' prior = _forgetting.prior_node(cg, nid)',
|
|
1514
|
+
' return None\n prior = _forgetting.prior_node(cg, nid)', 8),
|
|
1515
|
+
# ⑤ 合并档位闸空转——B 立即合并(MERGE/CONVERGE 两落点同闸失效)。
|
|
1516
|
+
# 红 14(实测 2026-10-03 收官批次校准 10→14):原 10(批次②)=C 组 MERGE
|
|
1517
|
+
# 出单 4 条 + C 组 CONVERGE 出单 3 条 + H 组 full 档合并对拍 + F/E 相关 2 条;
|
|
1518
|
+
# 收官批次 +4 = M 组 M1b 两条(plan 档 MERGE 落点的 FORBID 与「未合并」判据)
|
|
1519
|
+
# + M1c 两条(plan 档 CONVERGE 落点的 FORBID 与「未并入」判据)。
|
|
1520
|
+
("合并档位闸不生效(_autonomy_gate_merge 恒放行)", "cls", mdcos.MdCGOS,
|
|
1521
|
+
"_autonomy_gate_merge", ' dec = _am.decide(_am.B_MERGE)',
|
|
1522
|
+
' return None\n dec = _am.decide(_am.B_MERGE)', 14),
|
|
1523
|
+
# ⑥ 删除档位闸空转——D 立即软删(工具面档位形同虚设)。
|
|
1524
|
+
# 红 5(实测 2026-10-03 收官批次校准 4→5):原 4(批次②)=D 组出单/在位/
|
|
1525
|
+
# 载荷/accept 四条;收官批次 +1 = M 组 M1d(plan 档工具面删除的出单/不软删)。
|
|
1526
|
+
("删除档位闸不生效(forget_gated 直调软删)", "cls", mdcos.MdCGOS,
|
|
1527
|
+
"forget_gated", ' dec = autonomy_modes.decide(autonomy_modes.D_DELETE)',
|
|
1528
|
+
' return self._forget_apply(node_id, e, reason)\n'
|
|
1529
|
+
' dec = autonomy_modes.decide(autonomy_modes.D_DELETE)', 5),
|
|
1530
|
+
# ⑦ 执行桥空转——accept「成功了」但动作没做(最危险的形态:汇报不实)。
|
|
1531
|
+
# 锚点(批次③改):提前 return 插在**前像拍摄之前**——空转形态同时废掉
|
|
1532
|
+
# 「执行时点前像/补全」(K 组一并红)。
|
|
1533
|
+
# 红 18(实测 2026-10-02 批次③):B 组 2(accept 执行/幂等)+ C 组 2
|
|
1534
|
+
# (MERGE/CONVERGE 执行)+ D 组 1 + I 组 3 + K 组 10(前像/影响面/
|
|
1535
|
+
# 命令串/执行记录——K 的提议时点占位与 mutation_view 两条在空转下仍绿,
|
|
1536
|
+
# 语义相符)。
|
|
1537
|
+
("accept 不执行(执行桥空转)", "cls", mdcos.MdCGOS, "_mutation_execute",
|
|
1538
|
+
' pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),',
|
|
1539
|
+
' return {"ok": True, "node_id": tgt}\n'
|
|
1540
|
+
' pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),', 18),
|
|
1541
|
+
# ⑧ 幂等键丢掉目标(退回内容签名)——不同目标的同内容变更单撞成一条。
|
|
1542
|
+
# 红 4(实测 2026-10-02 补强批次复测;批次②当时 3):E 组三条幂等判据;
|
|
1543
|
+
# 补强批次 +1 = I 组「对照:目标在位 accept 照旧执行」——执行桥两张
|
|
1544
|
+
# 同内容不同目标的 C 单撞键后,第二张的裁决落到已关闭的 pid 上。
|
|
1545
|
+
("幂等键丢掉目标(内容签名回归)", "mod", autonomy_modes,
|
|
1546
|
+
"mutation_dedup_key",
|
|
1547
|
+
' text = "%s\\x1f%s\\x1f%s" % (act, tgt, after if after is not None else "")',
|
|
1548
|
+
' text = "%s" % (after if after is not None else "")', 4),
|
|
1549
|
+
# ⑨ 类型字段不落 rec——变更单退化成提案(accept 会把它当新写入落盘)。
|
|
1550
|
+
# 红 22(实测 2026-10-03 补强批次 v1.2 校准 20→22):20(批次③)=B 组队列
|
|
1551
|
+
# kind 一条 + C 组 3 条 + D 组 accept 一条 + E 组队列计数一条 + I 组 4 条
|
|
1552
|
+
# (gated C 队列条目/目标消失/pending/对照)+ 批次③ K 组字段判据 10(kind 缺
|
|
1553
|
+
# ⇒ order_kind 判 proposal ⇒ mutation_view 返回 None、accept 走 proposal 链);
|
|
1554
|
+
# 补强批次 +2 = M1e②③(出单条目因缺 kind 被 `order_kind` 判成 proposal ⇒
|
|
1555
|
+
# `_mut_entries` 计数 0≠1——队列计数判据的机械钉)。
|
|
1556
|
+
("kind 不落 rec(变更单退化成提案)", "cls", mdcos.MdCGOS, "propose",
|
|
1557
|
+
' if kind:\n rec["kind"] = str(kind)',
|
|
1558
|
+
' if False:\n rec["kind"] = str(kind)', 22),
|
|
1559
|
+
# ⑩ 档位路径绕过 can_admin 闸——**本批实修缺陷的原形态**(MdCGSecure.forget
|
|
1560
|
+
# 的管理闸原先不在档位路径上;test_p2_mcp §9 实测抓到)。红 2(实测
|
|
1561
|
+
# 2026-10-02):D 组「无 can_admin 经档位路径删除被拒」+ G 组「管理闸单点」。
|
|
1562
|
+
("档位路径绕过 can_admin 闸(实修缺陷形态)", "cls", mdcos.MdCGSecure,
|
|
1563
|
+
"forget_gated", ' self._forget_admin()\n'
|
|
1564
|
+
' return super().forget_gated(node_id, reason, override=override)',
|
|
1565
|
+
' return super().forget_gated(node_id, reason, override=override)', 2),
|
|
1566
|
+
# ⑪ gated 面档位闸空转——`gated=true` 的 C 覆写照旧直接覆写(复核发现 1 的
|
|
1567
|
+
# 原形态:插件主写入通道旁路确认档;I 组 C 面/链面/MCP 面判据整体失效)。
|
|
1568
|
+
("gated 面档位闸不生效(_autonomy_gate_rewrite 恒放行)", "cls", mdcos.MdCGOS,
|
|
1569
|
+
"_autonomy_gate_rewrite",
|
|
1570
|
+
' prior = forgetting.prior_node(self, node_id)',
|
|
1571
|
+
' return None\n prior = forgetting.prior_node(self, node_id)', 15),
|
|
1572
|
+
# ⑫ gated 面档位闸丢掉资格探针——受保护/越权覆写从「当场拒」变成「静默入队」
|
|
1573
|
+
# (放宽既有判据,纯加严被破坏)。
|
|
1574
|
+
("gated 面档位闸跳过资格探针(write_qualify 摘除)", "cls", mdcos.MdCGOS,
|
|
1575
|
+
"_autonomy_gate_rewrite",
|
|
1576
|
+
' self.write_qualify(node_id, target_layer=layer,\n'
|
|
1577
|
+
' override=bool(override), actor=self.actor)',
|
|
1578
|
+
' pass', 2),
|
|
1579
|
+
# ⑬ 执行桥 C 目标消失判据摘除——回到 `self.add` 的 upsert(复核观察 3 的原形态:
|
|
1580
|
+
# 「改写」静默落成「新增同 id 节点」并记 accepted)。
|
|
1581
|
+
("执行桥 C 目标消失判据摘除(upsert 回归)", "cls", mdcos.MdCGOS,
|
|
1582
|
+
"_mutation_execute",
|
|
1583
|
+
' if forgetting.prior_node(self, tgt) is None:',
|
|
1584
|
+
' if False:', 3),
|
|
1585
|
+
# ⑭ SHIM 登记丢失(**常量型变异**,kind="attr")——baseline 假包缺
|
|
1586
|
+
# autonomy_modes(补强一 的原形态:容器栈一 37/1 的那条红)。
|
|
1587
|
+
("SHIM 登记丢失(_SHIMS 去掉 autonomy_modes)", "attr", _tpc, "_SHIMS",
|
|
1588
|
+
'"autonomy_modes")',
|
|
1589
|
+
'tuple(x for x in _SHIMS if x != "autonomy_modes")', 2),
|
|
1590
|
+
# ⑮ 执行桥不拍前像(批次③)——三字段的「执行时点」半支失效:before 空、
|
|
1591
|
+
# rollback 空,回滚句柄不存在、影响面缺失。
|
|
1592
|
+
# 红 8(实测):K 组前像 3 条 + impact 3 条 + rollback 1 条 + 记录落盘 1 条
|
|
1593
|
+
# (K 的「executed 与影响面同形态」一条在 pre=None 时仍绿——mutation_executed
|
|
1594
|
+
# 保留原占位,语义相符)。
|
|
1595
|
+
("执行桥不拍前像(执行时点前像整支失效)", "cls", mdcos.MdCGOS,
|
|
1596
|
+
"_mutation_execute",
|
|
1597
|
+
' pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),\n'
|
|
1598
|
+
' reason="变更单执行时点前像(%s→%s,pid=%s)"\n'
|
|
1599
|
+
' % (act, tgt, item.get("pid") or "?"))',
|
|
1600
|
+
' pre = None', 8),
|
|
1601
|
+
# ⑯ 执行时点补全摘除(批次③)——accept 返回/落盘的都是提议时点占位载荷
|
|
1602
|
+
# (三字段保持 None/"",executed/kind 也不补)。
|
|
1603
|
+
# 红 9(实测):K 组字段判据除「提议时点三键占位」外整体红(语义相符)。
|
|
1604
|
+
("执行时点补全摘除(mutation_executed 空转)", "mod", autonomy_modes,
|
|
1605
|
+
"mutation_executed",
|
|
1606
|
+
' out = dict(payload or {})\n out["kind"] = KIND_MUTATION',
|
|
1607
|
+
' return dict(payload or {})\n'
|
|
1608
|
+
' out = dict(payload or {})\n out["kind"] = KIND_MUTATION', 9),
|
|
1609
|
+
# ⑰ 补强批次 v1.2·U1——**直写面档位闸摘除**(不限档位放行):`mdcg_remember`
|
|
1610
|
+
# 非 gated 直写分支的档位判定被短路 ⇒ plan/confirm 两档 C 覆写从「出单且
|
|
1611
|
+
# 不落盘」变「直落且正文被覆写」(复核 V4' 的**摘除形态**;接入前该注入在
|
|
1612
|
+
# 现行树 0 红存活——M1e 即为解除项)。
|
|
1613
|
+
# 红 3(实测):M1e①②(plan 档:出单体 + 正文未改/队列 1 张,直落后两条皆
|
|
1614
|
+
# 不成立)+ M1e③(confirm 档同形)。M1e④⑤(full 直落)语义相符仍绿。
|
|
1615
|
+
("直写面档位闸摘除(mdcg_remember 非 gated 分支放行)", "mod", mcp_server,
|
|
1616
|
+
"_dispatch",
|
|
1617
|
+
' _dec = _am.decide(_action)\n'
|
|
1618
|
+
' if _dec["decision"] != _am.ALLOW:',
|
|
1619
|
+
' _dec = {"mode": "", "action": _action, "action_name": "",\n'
|
|
1620
|
+
' "decision": _am.ALLOW, "hint": ""}\n'
|
|
1621
|
+
' if False:', 3),
|
|
1622
|
+
# ⑱ 补强批次 v1.2·U1——**直写面 plan 档旁路**(复核 V4' 的**追加式注入形态**,
|
|
1623
|
+
# 逐字复刻其 payload):判据行之后追加「plan 档 C 覆写放行」——只有 plan 档
|
|
1624
|
+
# 转红(confirm 档不受影响)。两条形态成对:⑰ 证「闸在,不限档位摘除必红」,
|
|
1625
|
+
# ⑱ 证「plan 档单点放行必红」。
|
|
1626
|
+
# 红 2(实测):M1e①②。
|
|
1627
|
+
("直写面 plan 档旁路(复核 V4' 注入形态)", "mod", mcp_server, "_dispatch",
|
|
1628
|
+
' _dec = _am.decide(_action)',
|
|
1629
|
+
' _dec = _am.decide(_action)\n'
|
|
1630
|
+
' if _am.mode() == "plan" and _action == _am.C_REWRITE '
|
|
1631
|
+
'and _dec["decision"] == _am.CONFIRM:\n'
|
|
1632
|
+
' _dec = dict(_dec, decision=_am.ALLOW)', 2),
|
|
1633
|
+
# ⑲ 补强批次 v1.2·E 面——**闸无差别阻塞**(「E 接线只在 plan 档生效」的反向
|
|
1634
|
+
# 形态):`e_gate` 恒判 FORBID ⇒ confirm/full 两档的 E 写盘一并被拦。
|
|
1635
|
+
# 红 5(实测):M2③(confirm 照落)+ M2④(confirm↔full 对拍)+ M2⑦(weights
|
|
1636
|
+
# confirm)+ M2⑨(set_state confirm)+ M2⑪(backfill confirm);plan 档判据
|
|
1637
|
+
# (M2①②⑥⑧⑩)语义相符仍绿——**这一分布就是「只在 plan 档生效」的机械证据**。
|
|
1638
|
+
("E 面闸无差别阻塞(confirm/full 也被拦)", "mod", autonomy_modes, "e_gate",
|
|
1639
|
+
' return decide(E_WEIGHT, plan=plan, environ=environ)',
|
|
1640
|
+
' return dict(decide(E_WEIGHT, plan=plan, environ=environ),\n'
|
|
1641
|
+
' decision=FORBID)', 5),
|
|
1642
|
+
# ⑳ 补强批次 v1.2·E 面——**接线摘除·freshness.recalc**(apply 路径的档位闸
|
|
1643
|
+
# 短路)⇒ plan 档 E 照跑写盘(即复核实测的 importance/freshness 写盘形态)。
|
|
1644
|
+
# 红 2(实测):M2①②。
|
|
1645
|
+
("E 面接线摘除·freshness.recalc", "mod", freshness, "recalc",
|
|
1646
|
+
' from . import autonomy_modes as _am\n'
|
|
1647
|
+
' _dec = _am.e_gate()\n'
|
|
1648
|
+
' if _dec["decision"] != _am.ALLOW:\n'
|
|
1649
|
+
' return _am.forbidden_result(_dec, action="freshness", '
|
|
1650
|
+
'dry_run=False,\n'
|
|
1651
|
+
' written=0, batch=batch)',
|
|
1652
|
+
' pass', 2),
|
|
1653
|
+
# ㉑ 补强批次 v1.2·E 面——**接线摘除·weights.recalc**(第二写盘入口)。
|
|
1654
|
+
# 红 1(实测):M2⑥。
|
|
1655
|
+
("E 面接线摘除·weights.recalc", "mod", weights, "recalc",
|
|
1656
|
+
' from . import autonomy_modes as _am\n'
|
|
1657
|
+
' _dec = _am.e_gate()\n'
|
|
1658
|
+
' if _dec["decision"] != _am.ALLOW:\n'
|
|
1659
|
+
' return _am.forbidden_result(_dec, action="importance", '
|
|
1660
|
+
'dry_run=False,\n'
|
|
1661
|
+
' written=0, batch=batch)',
|
|
1662
|
+
' pass', 1),
|
|
1663
|
+
# ㉒ 补强批次 v1.2·E 面——**接线摘除·lifecycle.set_state**(唯一推进入口,
|
|
1664
|
+
# 含 `MdCG.set_state` 委托面)。
|
|
1665
|
+
# 红 1(实测):M2⑧。
|
|
1666
|
+
("E 面接线摘除·lifecycle.set_state", "mod", _lc, "set_state",
|
|
1667
|
+
' from . import autonomy_modes as _am\n'
|
|
1668
|
+
' _dec = _am.e_gate()\n'
|
|
1669
|
+
' if _dec["decision"] != _am.ALLOW:\n'
|
|
1670
|
+
' out = _am.forbidden_result(_dec, node_id=node_id, changed=False)\n'
|
|
1671
|
+
' out.update({"from": src, "to": dst, "code": "autonomy_forbidden"})\n'
|
|
1672
|
+
' return out',
|
|
1673
|
+
' pass', 1),
|
|
1674
|
+
# ㉓ 补强批次 v1.2·E 面——**接线摘除·lifecycle.backfill**(第二写盘入口)。
|
|
1675
|
+
# 红 1(实测):M2⑩。
|
|
1676
|
+
("E 面接线摘除·lifecycle.backfill", "mod", _lc, "backfill",
|
|
1677
|
+
' from . import autonomy_modes as _am\n'
|
|
1678
|
+
' _dec = _am.e_gate()\n'
|
|
1679
|
+
' if _dec["decision"] != _am.ALLOW:\n'
|
|
1680
|
+
' return _am.forbidden_result(_dec, dry_run=False, scanned=len(nodes),\n'
|
|
1681
|
+
' missing=len(missing), backfilled=0)',
|
|
1682
|
+
' pass', 1),
|
|
1683
|
+
# ㉔ 补强批次 v1.2·E 面——**闸越过 apply 边界**(接在预演面之前):在
|
|
1684
|
+
# `freshness.recalc` 函数体首行插入同一判定 ⇒ plan 档**预演**(apply=False)
|
|
1685
|
+
# 也被拦——「闸只在 apply 路径」这条判据(M2⑤)的判别形态。
|
|
1686
|
+
# 红 1(实测):M2⑤(apply 路径的 plan 判据仍绿——注入块与真闸的返回体同形,
|
|
1687
|
+
# 语义相符)。
|
|
1688
|
+
("E 面闸越过 apply 边界(预演面也被拦)", "mod", freshness, "recalc",
|
|
1689
|
+
' nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}',
|
|
1690
|
+
' from . import autonomy_modes as _am\n'
|
|
1691
|
+
' _dec0 = _am.e_gate()\n'
|
|
1692
|
+
' if _dec0["decision"] != _am.ALLOW:\n'
|
|
1693
|
+
' return _am.forbidden_result(_dec0, action="freshness",\n'
|
|
1694
|
+
' dry_run=not apply, written=0)\n'
|
|
1695
|
+
' nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}', 1),
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
#: 「防误删自检」的判别力钉(F1 收口,2026-10-03)——**不占表内编号**:表长恒 =
|
|
1699
|
+
#: `_MUTATION_IDS` 的长度(复核口径「24 处」与实存一致);它是自检体系的判别力
|
|
1700
|
+
#: 证明,形态与表内条目**同口径**:源码替换 + exec 重装 + 红项数比照,锚点同样
|
|
1701
|
+
#: 过 `_anchor_check`(漂移即 ANCHOR-MISS、fail-closed)。
|
|
1702
|
+
#: 剥掉 `_table_gaps` 的自检开关(恒判「完好」)⇒ 三条合成源自证测例(F1①②④)
|
|
1703
|
+
#: 转红——证明「删条目即机械报错」不是空转。红 3(实测);F1③(真实表期望 [])
|
|
1704
|
+
#: 语义相符仍绿。
|
|
1705
|
+
_SELFCHECK_MUTATION = (
|
|
1706
|
+
"防误删自检开关剥除(_table_gaps 恒判完好)", "mod", _THIS, "_table_gaps",
|
|
1707
|
+
'def _table_gaps(text, entries_count, ids=_MUTATION_IDS):\n'
|
|
1708
|
+
' """变异表完整性判定(纯函数):返回缺口说明列表(空 = 完好)。',
|
|
1709
|
+
'def _table_gaps(text, entries_count, ids=_MUTATION_IDS):\n'
|
|
1710
|
+
' return []\n'
|
|
1711
|
+
' """变异表完整性判定(纯函数):返回缺口说明列表(空 = 完好)。', 3)
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
# ---- 变异表完整性自检(F1 收口:防误删,2026-10-03)------------------------
|
|
1715
|
+
# 由来:F1 复查发现第③条元组被删(仅剩注释)而无人察觉(数字只存文档/注释、
|
|
1716
|
+
# 无自动核验载体)——本自检把「编号无缺口 + 表长与显式声明一致」变成机械判据:
|
|
1717
|
+
# 缺项 ⇒ fail-closed 退出码 2 并报缺口编号(不再靠人工发现)。与 `_anchor_check`
|
|
1718
|
+
# 同层接入(正常运行与 --mutate 均先行执行),配判别力钉 `_SELFCHECK_MUTATION`
|
|
1719
|
+
# (剥掉自检开关 ⇒ 合成源自证测例转红 3)证明它有判别力。
|
|
1720
|
+
# 基线源=**当前工作区文件**,不绑 git HEAD。
|
|
1721
|
+
#
|
|
1722
|
+
# 判据(`_table_gaps`,纯函数——自证测例以合成源调它):
|
|
1723
|
+
# a. 编号注释被下一**不同**编号注释覆盖(= 删元组留注释,F1 原形)⇒ 报该编号
|
|
1724
|
+
# (同编号在块内的复提不算——⑱ 块先例);
|
|
1725
|
+
# b. 编号注释序列与声明不一致(缺/重复;含「注释与元组同删」的真删形态)⇒
|
|
1726
|
+
# 报缺者;
|
|
1727
|
+
# c. len(_SRC_MUTATIONS) ≠ 声明长度 ⇒ 报「len:23≠24」兜底(防共用注释等边角)。
|
|
1728
|
+
_MUTATION_IDS = ("①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩",
|
|
1729
|
+
"⑪", "⑫", "⑬", "⑭", "⑮", "⑯", "⑰", "⑱", "⑲", "⑳",
|
|
1730
|
+
"㉑", "㉒", "㉓", "㉔")
|
|
1731
|
+
|
|
1732
|
+
_ANNOT_RE = re.compile(r"^ # ([①-⑳㉑-㉔])")
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def _table_lines(text):
|
|
1736
|
+
"""取 `_SRC_MUTATIONS = (` 到首个独立 `)` 行之间的表体行(当前源,非 git 基线)。"""
|
|
1737
|
+
out, on = [], False
|
|
1738
|
+
for line in text.split("\n"):
|
|
1739
|
+
if not on:
|
|
1740
|
+
if line.startswith("_SRC_MUTATIONS = ("):
|
|
1741
|
+
on = True
|
|
1742
|
+
continue
|
|
1743
|
+
if line.rstrip() == ")":
|
|
1744
|
+
break
|
|
1745
|
+
out.append(line)
|
|
1746
|
+
return out
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
def _table_gaps(text, entries_count, ids=_MUTATION_IDS):
|
|
1750
|
+
"""变异表完整性判定(纯函数):返回缺口说明列表(空 = 完好)。"""
|
|
1751
|
+
annot, gaps, pending = [], [], None
|
|
1752
|
+
for line in _table_lines(text):
|
|
1753
|
+
m = _ANNOT_RE.match(line)
|
|
1754
|
+
if m:
|
|
1755
|
+
nid = m.group(1)
|
|
1756
|
+
if pending is None:
|
|
1757
|
+
pending = nid
|
|
1758
|
+
annot.append(nid)
|
|
1759
|
+
elif nid != pending:
|
|
1760
|
+
gaps.append(pending) # a. 上一编号注释未被元组消费
|
|
1761
|
+
pending = nid
|
|
1762
|
+
annot.append(nid)
|
|
1763
|
+
# nid == pending:同一条目注释块内的复提(如 ⑱ 块内「⑱ 证…」行)——
|
|
1764
|
+
# 不构成新条目注释、不覆盖 pending(否则会把该块判成「缺条目」)。
|
|
1765
|
+
continue
|
|
1766
|
+
if pending is not None and line.startswith(" ("):
|
|
1767
|
+
pending = None # 元组消费其上最近的编号注释
|
|
1768
|
+
if pending is not None:
|
|
1769
|
+
gaps.append(pending) # 表尾仍有未消费的编号注释
|
|
1770
|
+
for i in ids:
|
|
1771
|
+
if i not in annot:
|
|
1772
|
+
gaps.append(i) # b. 编号缺(含注释与元组同删的形态)
|
|
1773
|
+
elif annot.count(i) > 1:
|
|
1774
|
+
gaps.append(i) # b. 编号重复
|
|
1775
|
+
if entries_count != len(ids):
|
|
1776
|
+
gaps.append("len:%d≠%d" % (entries_count, len(ids))) # c. 表长兜底
|
|
1777
|
+
seen, uniq = set(), []
|
|
1778
|
+
for g in gaps:
|
|
1779
|
+
if g not in seen:
|
|
1780
|
+
seen.add(g)
|
|
1781
|
+
uniq.append(g)
|
|
1782
|
+
return uniq
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def _table_integrity_check():
|
|
1786
|
+
"""变异表完整性自检(真实表/当前工作区源):返回缺口说明列表(空 = 完好)。"""
|
|
1787
|
+
with io.open(os.path.abspath(__file__), encoding="utf-8") as f:
|
|
1788
|
+
text = f.read()
|
|
1789
|
+
return _table_gaps(text, len(_SRC_MUTATIONS))
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
def _fn_src(target):
|
|
1793
|
+
"""变异锚点所在源码:`mod`/`cls` = 目标函数源码;`attr` = 属主源码(模块整体)。"""
|
|
1794
|
+
kind, owner, name = target
|
|
1795
|
+
if kind == "attr":
|
|
1796
|
+
return inspect.getsource(owner)
|
|
1797
|
+
return inspect.getsource(getattr(owner, name))
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
def _strip_indent(text: str, n: int) -> str:
|
|
1801
|
+
"""按 n 列去缩进(首 n 列全空白的行才截,其余行 lstrip——防把空行/续行搞错)。"""
|
|
1802
|
+
if not n:
|
|
1803
|
+
return text
|
|
1804
|
+
out = []
|
|
1805
|
+
for line in text.split("\n"):
|
|
1806
|
+
if line[:n].strip() == "":
|
|
1807
|
+
out.append(line[n:])
|
|
1808
|
+
else:
|
|
1809
|
+
out.append(line.lstrip())
|
|
1810
|
+
return "\n".join(out)
|
|
1811
|
+
|
|
1812
|
+
|
|
1813
|
+
def _anchor_check():
|
|
1814
|
+
"""返回 ANCHOR-MISS 说明列表(空 = 全部在位)——含自检判别力钉。"""
|
|
1815
|
+
bad = []
|
|
1816
|
+
for name, _kind, owner, fname, old, _new, _n in (_SRC_MUTATIONS
|
|
1817
|
+
+ (_SELFCHECK_MUTATION,)):
|
|
1818
|
+
if old not in _fn_src((_kind, owner, fname)):
|
|
1819
|
+
bad.append("变异锚点缺失:%r @%s.%s" % (old[:40], owner.__name__, fname))
|
|
1820
|
+
return bad
|
|
1821
|
+
|
|
1822
|
+
|
|
1823
|
+
@contextlib.contextmanager
|
|
1824
|
+
def _patched(target, old, new):
|
|
1825
|
+
"""把目标函数按字面替换变异后安装/还原(不落盘、不改源文件)。
|
|
1826
|
+
|
|
1827
|
+
接线点一律经**模块属性/类属性**在调用时解析(`_am.decide(...)` /
|
|
1828
|
+
`self._autonomy_gate_merge(...)`),故 setattr 后全部出口同闸生效;
|
|
1829
|
+
写链的档位闸是注册期绑定,故夹具每组现建 pipeline(见 `_pipe`)。
|
|
1830
|
+
"""
|
|
1831
|
+
kind, owner, name = target
|
|
1832
|
+
if kind == "attr":
|
|
1833
|
+
# 常量型变异:把属主(模块/类)的该属性换成 `new` 求值出的新值。
|
|
1834
|
+
# 为什么需要它:SHIM 登记(`_SHIMS` 元组)是**数据**不是函数体,源码替换
|
|
1835
|
+
# 无从下手;而「登记丢失」正是补强批次修的那处判据,必须有定点变异钉死。
|
|
1836
|
+
live = getattr(owner, name)
|
|
1837
|
+
setattr(owner, name, eval(new, dict(vars(owner)))) # noqa: S307
|
|
1838
|
+
try:
|
|
1839
|
+
yield
|
|
1840
|
+
finally:
|
|
1841
|
+
setattr(owner, name, live)
|
|
1842
|
+
return
|
|
1843
|
+
# 方法源码自带类内缩进(def 行 4 空格、体内 8 空格)→ 必须先按 def 行的缩进
|
|
1844
|
+
# 整块去缩进才可在模块级 exec;锚点/替换文按**同一**缩进量规整后匹配,
|
|
1845
|
+
# 故表里写的是「源码原文形态」(与 _anchor_check 的判据同一份字面量)。
|
|
1846
|
+
src = _fn_src(target)
|
|
1847
|
+
cut = len(src) - len(src.lstrip(" "))
|
|
1848
|
+
src2 = _strip_indent(src, cut)
|
|
1849
|
+
old2 = _strip_indent(old, cut)
|
|
1850
|
+
new2 = _strip_indent(new, cut)
|
|
1851
|
+
if old2 not in src2:
|
|
1852
|
+
raise AssertionError("变异锚点在去缩进后仍不匹配:%r" % old[:40])
|
|
1853
|
+
src = src2.replace(old2, new2)
|
|
1854
|
+
if kind == "mod":
|
|
1855
|
+
ns = dict(vars(owner))
|
|
1856
|
+
ns["__name__"] = owner.__name__
|
|
1857
|
+
else:
|
|
1858
|
+
ns = dict(vars(sys.modules[owner.__module__]))
|
|
1859
|
+
ns["__name__"] = owner.__module__
|
|
1860
|
+
exec(compile(src, "am_mut.py", "exec"), ns)
|
|
1861
|
+
live = getattr(owner, name)
|
|
1862
|
+
setattr(owner, name, ns[name])
|
|
1863
|
+
try:
|
|
1864
|
+
yield
|
|
1865
|
+
finally:
|
|
1866
|
+
setattr(owner, name, live)
|
|
1867
|
+
|
|
1868
|
+
|
|
1869
|
+
def _run_one_mutation(item):
|
|
1870
|
+
"""执行单条定点变异并比照红项数;返回 None(命中预期)或条目名(不符)。"""
|
|
1871
|
+
name, kind, owner, fname, old, new, expect = item
|
|
1872
|
+
_RUN[0] += 1
|
|
1873
|
+
try:
|
|
1874
|
+
with _patched((kind, owner, fname), old, new), \
|
|
1875
|
+
contextlib.redirect_stdout(io.StringIO()):
|
|
1876
|
+
fails = _run_groups()
|
|
1877
|
+
except Exception as exc: # noqa: BLE001 —— 变异把路径打断也算「红」
|
|
1878
|
+
fails = -1
|
|
1879
|
+
print(" 变异「%s」→ 断言链抛异常 %s: %s(判 FAIL)"
|
|
1880
|
+
% (name, type(exc).__name__, str(exc)[:80]))
|
|
1881
|
+
verdict = ("命中预期" if fails == expect
|
|
1882
|
+
else "**红项数不符(预期 %d)**" % expect)
|
|
1883
|
+
print(" 变异「%s」→ 红项=%d %s" % (name, fails, verdict))
|
|
1884
|
+
for f in _FAIL[:6]:
|
|
1885
|
+
print(" 红:", f)
|
|
1886
|
+
if len(_FAIL) > 6:
|
|
1887
|
+
print(" …(余 %d 项)" % (len(_FAIL) - 6))
|
|
1888
|
+
return None if fails == expect else name
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
def _mutate_mode():
|
|
1892
|
+
bad = []
|
|
1893
|
+
anchor_bad = _anchor_check()
|
|
1894
|
+
gap_bad = _table_integrity_check()
|
|
1895
|
+
if anchor_bad or gap_bad:
|
|
1896
|
+
for b in anchor_bad:
|
|
1897
|
+
print(" ANCHOR-MISS " + b)
|
|
1898
|
+
for b in gap_bad:
|
|
1899
|
+
print(" 变异表缺口:" + b)
|
|
1900
|
+
print("\n锚点/完整性自检:FAIL(fail-closed,exit 2)")
|
|
1901
|
+
return 2
|
|
1902
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
1903
|
+
clean = _run_groups()
|
|
1904
|
+
print(" 未变异基线:红项=%d%s"
|
|
1905
|
+
% (clean, "" if clean == 0 else " ← 基线即红,变异核验无意义"))
|
|
1906
|
+
if clean:
|
|
1907
|
+
bad.append("未变异基线即失败")
|
|
1908
|
+
_RUN[0] = 10 ** 6 # 变异轮用独立子根(防读到上一轮盘面/限流状态)
|
|
1909
|
+
print(" 表内条目:%d 处(编号 %s–%s;防误删自检保证无缺口、表长与声明一致)"
|
|
1910
|
+
% (len(_SRC_MUTATIONS), _MUTATION_IDS[0], _MUTATION_IDS[-1]))
|
|
1911
|
+
for item in _SRC_MUTATIONS:
|
|
1912
|
+
r = _run_one_mutation(item)
|
|
1913
|
+
if r:
|
|
1914
|
+
bad.append(r)
|
|
1915
|
+
# 防误删自检的判别力钉(不占表内编号——表长与声明恒等,见 _SELFCHECK_MUTATION)
|
|
1916
|
+
r = _run_one_mutation(_SELFCHECK_MUTATION)
|
|
1917
|
+
if r:
|
|
1918
|
+
bad.append(r)
|
|
1919
|
+
print("\n定点变异自证:%s"
|
|
1920
|
+
% ("PASS(每处判据都有变异钉死,且红项数逐处吻合)" if not bad
|
|
1921
|
+
else "FAIL —— " + "、".join(bad)))
|
|
1922
|
+
return 0 if not bad else 1
|
|
1923
|
+
|
|
1924
|
+
|
|
1925
|
+
def _print_reds():
|
|
1926
|
+
for f in _FAIL:
|
|
1927
|
+
print(" 红:", f)
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
def main() -> int:
|
|
1931
|
+
src = os.path.basename(os.path.abspath(__file__))
|
|
1932
|
+
if "--mutate" in sys.argv:
|
|
1933
|
+
print("!! 定点变异模式:逐个变异判据,套件应转红且红项数吻合\n")
|
|
1934
|
+
try:
|
|
1935
|
+
return _mutate_mode()
|
|
1936
|
+
finally:
|
|
1937
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
1938
|
+
anchor_bad = _anchor_check()
|
|
1939
|
+
gap_bad = _table_integrity_check()
|
|
1940
|
+
if anchor_bad or gap_bad:
|
|
1941
|
+
for b in anchor_bad:
|
|
1942
|
+
print(" ANCHOR-MISS " + b)
|
|
1943
|
+
for b in gap_bad:
|
|
1944
|
+
print(" 变异表缺口:" + b)
|
|
1945
|
+
print("\n锚点/完整性自检:FAIL(fail-closed,exit 2)"
|
|
1946
|
+
"——实现/变异表改了请同步")
|
|
1947
|
+
return 2
|
|
1948
|
+
print("锚点自检:PASS(%s;不以 git HEAD 为基线源)" % src)
|
|
1949
|
+
print("变异表完整性自检:PASS(编号 %s–%s 无缺口、表长 %d = 声明——"
|
|
1950
|
+
"删条目即 fail-closed,F1 收口)"
|
|
1951
|
+
% (_MUTATION_IDS[0], _MUTATION_IDS[-1], len(_MUTATION_IDS)))
|
|
1952
|
+
try:
|
|
1953
|
+
for g in _GROUPS:
|
|
1954
|
+
g()
|
|
1955
|
+
finally:
|
|
1956
|
+
_RUN[0] += 1
|
|
1957
|
+
print("\n三档自治档位守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
|
|
1958
|
+
if _FAIL:
|
|
1959
|
+
_print_reds()
|
|
1960
|
+
return 0 if not _FAIL else 1
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
if __name__ == "__main__":
|
|
1964
|
+
try:
|
|
1965
|
+
rc = main()
|
|
1966
|
+
finally:
|
|
1967
|
+
shutil.rmtree(_SANDBOX, ignore_errors=True)
|
|
1968
|
+
os.environ.pop("MDCG_POLICY_FILE", None)
|
|
1969
|
+
if _OLD_POLICY is not None:
|
|
1970
|
+
os.environ["MDCG_POLICY_FILE"] = _OLD_POLICY
|
|
1971
|
+
os.environ.pop(_ENV_MODE, None)
|
|
1972
|
+
sys.exit(rc)
|