@furongjun1999/dsh-memory 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +49 -18
  2. package/docs/README.md +1 -0
  3. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
  4. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
  5. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
  6. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
  7. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
  8. package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
  9. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
  10. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
  11. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
  12. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
  13. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
  14. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
  15. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
  16. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
  17. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
  18. package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
  19. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
  20. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
  21. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
  22. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
  23. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  24. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  25. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  26. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
  27. package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
  28. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
  29. package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
  30. package/md_cg/admission.py +718 -0
  31. package/md_cg/autonomy_modes.py +642 -0
  32. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  33. package/md_cg/chain.py +47 -0
  34. package/md_cg/consistency.py +133 -8
  35. package/md_cg/forgetting.py +38 -6
  36. package/md_cg/freshness.py +527 -0
  37. package/md_cg/generation.py +409 -0
  38. package/md_cg/hotcache.py +4 -1
  39. package/md_cg/lifecycle.py +30 -2
  40. package/md_cg/mcp_server.py +191 -26
  41. package/md_cg/mdcg.py +594 -39
  42. package/md_cg/mdcos.py +869 -38
  43. package/md_cg/nodefile.py +74 -1
  44. package/md_cg/protect.py +79 -4
  45. package/md_cg/provenance.py +1 -1
  46. package/md_cg/review_cli.py +31 -2
  47. package/md_cg/rollback.py +411 -0
  48. package/md_cg/rollback_cli.py +104 -0
  49. package/md_cg/semantic/canonical.py +22 -0
  50. package/md_cg/semantic/unify.py +73 -22
  51. package/md_cg/semantic/unify_fixture.json +25 -0
  52. package/md_cg/sleep.py +1297 -0
  53. package/md_cg/stg.py +400 -50
  54. package/md_cg/stgidx.py +281 -0
  55. package/md_cg/sustain.py +146 -9
  56. package/md_cg/test_auto_defaults.py +424 -0
  57. package/md_cg/test_autonomy_admission.py +1098 -0
  58. package/md_cg/test_autonomy_modes.py +1972 -0
  59. package/md_cg/test_b1_auto_id_multiproc.py +7 -0
  60. package/md_cg/test_b1b2_write_face.py +7 -0
  61. package/md_cg/test_b3_merge_keeps_content.py +7 -0
  62. package/md_cg/test_boundary_hit.py +410 -0
  63. package/md_cg/test_cons200_scan_selection.py +791 -0
  64. package/md_cg/test_en_pipeline.py +22 -13
  65. package/md_cg/test_generation_guard.py +352 -0
  66. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  67. package/md_cg/test_i50a_half_dup_defer.py +408 -0
  68. package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
  69. package/md_cg/test_i50c_meta_passthrough.py +608 -0
  70. package/md_cg/test_i50d_importance_source.py +531 -0
  71. package/md_cg/test_i50e_readside_protection.py +656 -0
  72. package/md_cg/test_issue39_utf8_stdio.py +9 -1
  73. package/md_cg/test_issue52_scan_condition_first.py +677 -0
  74. package/md_cg/test_linkref.py +7 -0
  75. package/md_cg/test_mode_parity.py +1393 -0
  76. package/md_cg/test_mutation_rollback.py +805 -0
  77. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
  78. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  79. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
  80. package/md_cg/test_n230_dirty_replay.py +375 -0
  81. package/md_cg/test_p2_mcp.py +8 -0
  82. package/md_cg/test_p2_six_elements.py +463 -0
  83. package/md_cg/test_p3_legacy_closure.py +433 -0
  84. package/md_cg/test_p4_freshness.py +680 -0
  85. package/md_cg/test_p8_subgraph_chain.py +14 -1
  86. package/md_cg/test_p9_forget_protect.py +7 -0
  87. package/md_cg/test_p9c_dedup_hints.py +7 -0
  88. package/md_cg/test_policy_required_ccg.py +5 -1
  89. package/md_cg/test_protocol.py +7 -0
  90. package/md_cg/test_rank_parity_score_mode.py +6 -0
  91. package/md_cg/test_semantic_canonical.py +5 -3
  92. package/md_cg/test_sleep.py +611 -0
  93. package/md_cg/test_sleep_p1.py +784 -0
  94. package/md_cg/test_stgidx_index_parity.py +1020 -0
  95. package/md_cg/test_time_core_lint.py +968 -0
  96. package/md_cg/test_unify_default_off.py +701 -0
  97. package/md_cg/test_unify_scope.py +182 -0
  98. package/md_cg/test_writelimit.py +36 -9
  99. package/md_cg/test_writepipe.py +5 -2
  100. package/md_cg/weights.py +15 -1
  101. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  102. package/md_cg/writepipe.py +101 -2
  103. package/package.json +3 -2
  104. package/skills/plugin.json +1 -1
  105. package/utf8_boot.py +237 -0
@@ -0,0 +1,1393 @@
1
+ # -*- coding: utf-8 -*-
2
+ """md_cg · 三档自治「不动面对拍 + 端到端回滚演练」守卫(设计 v0.2 §十一 · 收官批次⑤)
3
+
4
+ 本守卫承载设计 §十一 验收判据的**两路对拍**与**跨面回滚演练**(§十三.5 步骤⑤):
5
+
6
+ **P 组 · 切档对拍(同一合成库 confirm↔full)**——非破坏性动作 **A 新增**
7
+ (写链面)与 **E 权重与生命周期**(`freshness.recalc` / `weights.recalc`,批
8
+ ③留池第 7 条「E 分数类」在档位面的行为读数)在切档前后输出**逐位不变**:
9
+ · A:两臂各写两次**首写(A 新增,不同新 id、同正文)**——第一次都在 confirm
10
+ (两臂同起点锚),第二次分属 full(切档臂)与 confirm(对照臂):比的就是
11
+ 「同一无歧义操作在切档前后是否同值」。**同 id 重放不比较**(第二次是覆写,
12
+ confirm/full 语义本就分叉,不属 A/E 对拍范围);
13
+ · E:同一库里「落 → **既有 rollback**(设计 §四 第 4 条:分数类用各模块
14
+ 既有 rollback)→ 切档 → 再落」两轮读数与落盘 fm 逐位相等;
15
+ · 「切档本身零写入」:库盘面指纹(全文件 sha256)跨 env 切换逐位相同;
16
+ · 「切档锚不空转」:切到目标档后 `autonomy_modes.mode()` 读回该档(防
17
+ 「切了个寂寞」——定点变异①实测打红的就是这条(full 臂)+ R4 四条直落判据)。
18
+ **plan 档不并入本对拍**(无计划时 A/E 的 forbid 是设计内语义,单独钉在
19
+ `test_autonomy_modes.g_m` 的 M2 组)。
20
+
21
+ **Q 组 · 与改动前基线的 oracle 对拍(`git archive HEAD` 只读导出)**——
22
+ 步骤③ 复核同款装置:把 HEAD 树只读导出到系统临时目录作 oracle,同一观测
23
+ 脚本(`_RUNNER`,守卫生成)对**两棵树**各跑一遍,**A/E 全线 + full 档全线**
24
+ 逐位比对,打印 SAME/DIFF 清单(报告引用)。
25
+ · **基线口径(防「基线绑提交即失效」,本仓已有两次教训)**:oracle 是
26
+ **每次运行现导出**的 HEAD 树(参照物),本守卫**不含任何某一提交派生的
27
+ 字面量期望值**——判据是「同一脚本在两棵树上的输出是否逐位一致」,两棵树
28
+ 都由 git 与工作区现状现取;
29
+ · **判别力=负对照**:对导出的 HEAD 树副本**定点注入**写链 A 面差异(把
30
+ A 新增也判成需确认),对拍必须报 DIFF **且 DIFF 项恰好命中预期集合**
31
+ (SAME 集合不变)——对拍机制不空转的机械证明;
32
+ · **已知返回体扩展的定向豁免(cons200 批,2026-10-03 同步)**:写链 out 的
33
+ consistency 读数新增 kept/truncated 两键(契约面:选面读数+截断可观测,
34
+ 见 `md_cg/test_cons200_scan_selection.py`),Q 组对拍对这两键**定向豁免**、
35
+ **旧键仍逐位对拍**,并以「新键在场」断言钉住(`_KNOWN_EXT_KEYS` /
36
+ `_norm_known_ext`)——有意扩展不误报 DIFF,真行为差异不因归一化被漏掉;
37
+ · git 不可用 / 导出失败 / 导出树缺 md_cg ⇒ `ORACLE-MISS` 打印后退出码
38
+ **2**(fail-closed,不静默跳过)。
39
+
40
+ **R 组 · 端到端回滚演练(跨面,含子进程 CLI 实测)**——不是守卫级单呼:
41
+ · confirm 档三条链各一遍(**三个不同入口面**):C 改写经**写链**
42
+ (`cg(op=write)`)/ B 合并经**插件面**(`mdcg_remember(gated=true)`)/
43
+ D 删除经**工具面**(`cg(op=forget)`)——每条链「出单 → 裁决 accept
44
+ (**执行桥**)→ 回滚 **`md_cg.rollback_cli` 子进程**实测 → 逐字节比对
45
+ 前像 → 残差核对(索引/边/聚合行三面)+索引条目与前像重算逐位对拍」;
46
+ · full 档三条链各一遍:**直落**(无单、零变更单)→ 统一回滚入口回滚 →
47
+ 逐字节比对前像 + 残差三面;
48
+ · **空池口径(补强批次 v1.2·U2 收口)**:一切「逐字节 == 前像」判据一律
49
+ **非空前置**(`bool(pre_b)` 门槛)——两侧皆空(`b""`)时**判红、不静默
50
+ 判绿**(与 `_bytes` 的「缺失 ⇒ 判不相等」声明一字一致);R4 三条 full 链
51
+ 另加**执行时点前像 + 盘面快照双非空取证**(收口前复核探针实测「R 组红 3 /
52
+ 绿 30」——R2、R3-逐字节、R4 三条的第三合取项在两侧皆空时静默判绿)。
53
+ 判别力由定点变异「`_bytes` 恒空」承担(收口后这些判据必须全部转红)。
54
+ · 边界(如实登记,非背书):full 档直落**不产变更单、不自动拍前像**
55
+ ——设计 §六 义务①「一切破坏性动作先留前像」在 full 直落路径**未接线**
56
+ (留池;收官报告登记)。故 full 档链的「回滚」=演练侧按执行时点拍前像
57
+ (`protect.snapshot_preimage` 单点,与执行桥同一函数)后走统一回滚入口
58
+ ——证明**回滚原语对 full 档直落形态可用**;生产全自动前像待接线。
59
+
60
+ 定点变异自证(`--mutate`,与 `md_cg/test_neg_condition_hits.py` 同口径):表内
61
+ 每项 = (说明, 目标, 锚点原文, 替换文, 预期红项数)。锚点须**逐字**出现在目标函数
62
+ 源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。红项数与实测**逐一相符**才算
63
+ 通过——不符即 FAIL(多红=断言越界、少红=该判据无判别力)。
64
+
65
+ **F1 收口(2026-10-03)——防误删自检**(与 `md_cg/test_autonomy_modes.py`
66
+ **两守卫对齐**):新增**变异表完整性自检**(`_table_gaps` /
67
+ `_table_integrity_check`:编号无缺口 + 表长与显式声明 `_MUTATION_IDS` 一致;
68
+ 缺项 ⇒ fail-closed 退出码 2 并报缺口编号)——今后删条目即机械报错,不再靠人工
69
+ 发现。判别力由 G 组 `g_s` 合成源自证测例(F1①–④,随 `--mutate` 每轮同跑)+
70
+ 内置判别力钉 `_SELFCHECK_MUTATION`(剥掉自检开关 ⇒ 自证测例转红 3)钉死。
71
+
72
+ 沙箱(硬约束):一切读写都在 tempfile.mkdtemp 内(含 MDCG_AUX_ROOT / 主密钥 /
73
+ policy——crypto 在导入期求值 MASTER_FILE,故必须在任何 md_cg 子模块 import
74
+ **之前**设好);oracle 导出树、观测脚本、合成库全在系统临时区;跑完 rmtree。
75
+ **绝不碰在役数据根**。档位 env 由夹具按需设置并在 finally 还原。
76
+
77
+ 运行:
78
+ python -X utf8 -m md_cg.test_mode_parity # 正常跑(含 oracle 路由)
79
+ python -X utf8 -m md_cg.test_mode_parity --no-oracle # 跳过 oracle 路由(本地调试)
80
+ python -X utf8 -m md_cg.test_mode_parity --mutate # 定点变异自证
81
+ """
82
+ from __future__ import annotations
83
+
84
+ import contextlib
85
+ import copy
86
+ import hashlib
87
+ import inspect
88
+ import io
89
+ import json
90
+ import os
91
+ import re
92
+ import shlex
93
+ import shutil
94
+ import subprocess
95
+ import sys
96
+ import tarfile
97
+ import tempfile
98
+ import time
99
+
100
+ # ---- 沙箱:必须在任何 md_cg 子模块 import 之前 ------------------------------
101
+ _SANDBOX = tempfile.mkdtemp(prefix="mode_parity_sandbox_")
102
+ os.environ["MDCG_AUX_ROOT"] = _SANDBOX
103
+ os.environ["MDCG_ROOT"] = os.path.join(_SANDBOX, "root")
104
+ os.environ["MDCG_MASTER_KEY"] = os.urandom(32).hex()
105
+ os.environ.pop("MDCG_TEST_LIVE_ROOT", None)
106
+ _OLD_POLICY = os.environ.pop("MDCG_POLICY_FILE", None)
107
+
108
+ from . import autonomy_modes, forgetting, nodefile, protect, rollback # noqa: E402
109
+ from . import writepipe, freshness, weights # noqa: E402
110
+ from . import mcp_server # noqa: E402
111
+ from .mdcos import MdCGOS # noqa: E402
112
+
113
+ _ENV_MODE = autonomy_modes.AUTONOMY_ENV_KEYS["mode"]
114
+
115
+ _PASS = []
116
+ _FAIL = []
117
+ _RUN = [0]
118
+
119
+
120
+ def ok(cond, msg, extra=""):
121
+ (_PASS if cond else _FAIL).append(msg)
122
+ print((" PASS " if cond else " FAIL ") + msg
123
+ + ((" ← " + str(extra)) if (extra and not cond) else ""))
124
+
125
+
126
+ def _policy():
127
+ path = os.path.join(_SANDBOX, "policy.json")
128
+ with open(path, "w", encoding="utf-8") as f:
129
+ json.dump({"forbidden": ["FORBIDDEN_WORD"], "required": ["PASSED"]}, f)
130
+ os.environ["MDCG_POLICY_FILE"] = path
131
+ return path
132
+
133
+
134
+ def _lib(tag="lib"):
135
+ """独立合成库(每轮独立子根:变异模式连跑多轮,共用目录会读到上一轮盘面)。"""
136
+ return MdCGOS(os.path.join(_SANDBOX, "%s_r%d" % (tag, _RUN[0])), autoflush=0)
137
+
138
+
139
+ @contextlib.contextmanager
140
+ def _mode(m):
141
+ old = os.environ.get(_ENV_MODE)
142
+ os.environ[_ENV_MODE] = m
143
+ try:
144
+ yield m
145
+ finally:
146
+ if old is None:
147
+ os.environ.pop(_ENV_MODE, None)
148
+ else:
149
+ os.environ[_ENV_MODE] = old
150
+
151
+
152
+ def _pipe():
153
+ return writepipe.install_default_gates(writepipe.WritePipeline())
154
+
155
+
156
+ def _repo_root():
157
+ return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
158
+
159
+
160
+ def _pre_path(cg, rel):
161
+ return os.path.join(cg.root, str(rel).replace("/", os.sep))
162
+
163
+
164
+ def _bytes(path):
165
+ """读字节:路径缺失/不可读 ⇒ `b""`(判据按「不相等」处理,**不抛**——
166
+ 定点变异会把前像/落点打断,判红由断言承担,不让异常打断整组)。"""
167
+ if not path:
168
+ return b""
169
+ try:
170
+ with open(path, "rb") as f:
171
+ return f.read()
172
+ except OSError:
173
+ return b""
174
+
175
+
176
+ def _disk_path(cg, nid):
177
+ """节点盘面绝对路径;条目缺失 ⇒ None(同一 fail-closed 口径,不抛)。"""
178
+ e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid) or {}
179
+ p = e.get("path")
180
+ return os.path.join(cg.root, p) if p else None
181
+
182
+
183
+ def _refresh(cg):
184
+ cg._maybe_reload_index()
185
+
186
+
187
+ def _run_cli(cmd):
188
+ """**实测演练回滚命令串**(子进程 CLI):切分 → `python` 换本进程解释器。
189
+
190
+ shlex 用 posix=False(Windows 路径的反斜杠在 posix 模式会被当转义字符);
191
+ 只替换首令牌 `python`,其余令牌逐字来自字段(演练的是字段里的那条串)。
192
+ """
193
+ argv = shlex.split(cmd, posix=False)
194
+ argv = [a[1:-1] if len(a) >= 2 and a[0] == a[-1] == '"' else a for a in argv]
195
+ if not argv:
196
+ return {"rc": -1, "out": "", "err": "empty rollback command", "json": None}
197
+ if argv[0] == "python":
198
+ argv[0] = sys.executable
199
+ p = subprocess.run(argv, capture_output=True, text=True, encoding="utf-8",
200
+ errors="replace", cwd=_repo_root(),
201
+ env=dict(os.environ, PYTHONUTF8="1"))
202
+ try:
203
+ out_json = json.loads((p.stdout or "").strip().splitlines()[-1])
204
+ except Exception: # noqa: BLE001
205
+ out_json = None
206
+ return {"rc": p.returncode, "out": p.stdout or "", "err": p.stderr or "",
207
+ "json": out_json}
208
+
209
+
210
+ def _fingerprint(root):
211
+ """库盘面指纹:root 下全部文件 (相对路径, sha256)——「零写入」判据的单点。"""
212
+ out = {}
213
+ for dirpath, dirnames, filenames in os.walk(root):
214
+ dirnames[:] = [d for d in dirnames if d != "__pycache__"]
215
+ for fn in filenames:
216
+ p = os.path.join(dirpath, fn)
217
+ rel = os.path.relpath(p, root).replace("\\", "/")
218
+ try:
219
+ with open(p, "rb") as f:
220
+ out[rel] = hashlib.sha256(f.read()).hexdigest()
221
+ except OSError:
222
+ out[rel] = "<unreadable>"
223
+ return out
224
+
225
+
226
+ def _entry_from_preimage(cg, nid, rel):
227
+ """按前像文件重算索引条目(与 _node_entry 同源)——回滚后索引对拍的基准。
228
+
229
+ 前像缺失/不可读 ⇒ None(调用方判 False ⇒ 红;不抛——变异场景专用口径)。
230
+ """
231
+ if not rel or not _disk_path(cg, nid):
232
+ return None
233
+ try:
234
+ with open(_pre_path(cg, rel), encoding="utf-8") as f:
235
+ fm, c = nodefile.loads(f.read())
236
+ except OSError:
237
+ return None
238
+ return cg._node_entry(_disk_path(cg, nid), str(fm.get("layer") or "knowledge"),
239
+ fm, c)
240
+
241
+
242
+ def _mut_entries(cg):
243
+ return [r for r in cg.review_list()
244
+ if autonomy_modes.order_kind(r) == "mutation"]
245
+
246
+
247
+ def _pims(cg, nid):
248
+ """protect 审计面里某节点的 `action=="preimage"` 行(只读,不抛)。"""
249
+ p = os.path.join(cg.root, protect.AUDIT_FILE)
250
+ out = []
251
+ try:
252
+ with open(p, encoding="utf-8") as f:
253
+ for line in f:
254
+ line = line.strip()
255
+ if not line:
256
+ continue
257
+ try:
258
+ r = json.loads(line)
259
+ except ValueError:
260
+ continue
261
+ if r.get("action") == "preimage" and r.get("node_id") == nid:
262
+ out.append(r)
263
+ except OSError:
264
+ return []
265
+ return out
266
+
267
+
268
+ #: 对拍要剔除的**时钟面/耗时面**键——两次运行必然不同,与档位/版本无关。
269
+ #: 剔除不算放水:对拍的判据是「档位(或版本)有没有改变行为」,不是「时间戳
270
+ #: 与耗时是否相同」。与 `test_autonomy_modes._VOLATILE` 同族同口径。
271
+ _VOLATILE = ("t", "created_at", "last_access", "time_window", "last_merge_at",
272
+ "batch", "note", "elapsed_ms", "updated_at")
273
+ #: 聚合行的时间戳标记(`- 【聚合 月-日 时:分】…`,`forgetting.aggregate_line`
274
+ #: 生成)——同一族的时钟面文本,跨运行必然不同,比对前掩码为 `<ts>`。
275
+ _AGG_RE = re.compile(r"【聚合 [^】]*】")
276
+ #: 提案 id 的**熵面**掩码:`propose` 的 pid = `prop_` + 内容签名(node_id +
277
+ #: `time.time()` + `uuid4().hex`)(`mdcos.propose` 实读)——含时钟与随机熵,
278
+ #: 两次运行必然不同(`test_review_conformance【8】` 以「20 条互异」钉它)。
279
+ #: pid 是**身份面**不是行为面:对拍比的是「档位/版本有没有改变行为」,故掩码。
280
+ _PID_RE = re.compile(r"prop_[0-9a-f]{8,}")
281
+
282
+
283
+ def _strip(d):
284
+ """递归剔除时钟面键 + 掩码聚合行时间戳/pid 熵面,返回可逐位比较的结构。"""
285
+ if isinstance(d, dict):
286
+ return {k: _strip(v) for k, v in d.items() if k not in _VOLATILE}
287
+ if isinstance(d, list):
288
+ return [_strip(x) for x in d]
289
+ if isinstance(d, str):
290
+ return _PID_RE.sub("prop_<id>", _AGG_RE.sub("【聚合 <ts>】", d))
291
+ return d
292
+
293
+
294
+ BODY = ("# 功能名:部署面端口约定\n"
295
+ "# 生效条件:载体/位置:prod 集群;时间:2026-09-30 起;方法:部署清单核对;约束:无\n"
296
+ "# 子功能:登记各服务监听端口\n"
297
+ "# 执行:核对 manifest 的 ports 段\n"
298
+ "# 验证方式:test\n"
299
+ "# 不适用条件:无\n"
300
+ "\n网关监听端口 8080/HTTP;管理面监听端口 8081/HTTP;"
301
+ "指标面监听端口 %s/HTTP;日志面监听端口 9091/HTTP\n")
302
+ PLAIN = "PASSED 探针正文 %s"
303
+
304
+
305
+ # =========================================== P 组:切档对拍(同库 confirm↔full)
306
+ def g_p():
307
+ print("== P 组:切档对拍(同一合成库 confirm↔full · 非破坏性动作 A/E)==")
308
+ _policy()
309
+ # ---- P1 A 新增:切档臂(confirm→full) vs 同档对照臂(confirm→confirm)----
310
+ # 两臂各写两次**首写**(不同新 id、同正文):第一次都在 confirm(两臂同
311
+ # 起点锚),第二次分属 full(切档臂)与 confirm(对照臂)——比的就是「同一
312
+ # 无歧义操作(A 新增)在切档前后是否同值」。**不比较同 id 重放**:同 id
313
+ # 第二次是覆写(C 改写),confirm/full 语义本就分叉,不属本对拍范围。
314
+ arms = {}
315
+ for tag, second in (("p_no2full", "full"), ("p_no2no", "confirm")):
316
+ cg = _lib(tag)
317
+ pipe = _pipe()
318
+ with _mode("confirm"):
319
+ o1 = pipe.execute(cg, {"content_kind": "text", "content": PLAIN % "对拍",
320
+ "layer": "knowledge", "node_id": "p_a1"})
321
+ fp_run = _fingerprint(cg.root)
322
+ with _mode(second):
323
+ fp_sw = _fingerprint(cg.root)
324
+ ok(fp_run == fp_sw,
325
+ "P 切档本身零写入:库盘面指纹跨 confirm→%s 切换逐位相同"
326
+ "(%d 文件)" % (second, len(fp_run)),
327
+ sorted(set(fp_run) ^ set(fp_sw))[:4])
328
+ ok(autonomy_modes.mode() == second,
329
+ "P 切档锚不空转:切到 %s 后 mode() 读回 %s(命中控值)"
330
+ % (second, second), autonomy_modes.mode())
331
+ o2 = pipe.execute(cg, {"content_kind": "text", "content": PLAIN % "对拍",
332
+ "layer": "knowledge", "node_id": "p_a2"})
333
+ node = _strip(cg.get("p_a2") or {})
334
+ arms[tag] = (_strip(o1), _strip(o2), node)
335
+ cg.close()
336
+ ok(arms["p_no2full"][0] == arms["p_no2no"][0],
337
+ "P1 两臂同起点锚:confirm 档首写(A 新增)读数**逐位相等**"
338
+ "(两库结构等价 ⇒ 后续比对可比)",
339
+ {"switch": arms["p_no2full"][0].get("committed"),
340
+ "control": arms["p_no2no"][0].get("committed")})
341
+ ok(arms["p_no2full"][1] == arms["p_no2no"][1],
342
+ "P1 A 新增(切档核心判据):**full 档**首写读数(切档臂)与 **confirm 档**"
343
+ "首写读数(对照臂)逐位相等——切档前后 A 逐位不变")
344
+ ok(arms["p_no2full"][2] == arms["p_no2no"][2],
345
+ "P1 落盘节点(正文与 fm,时钟面除外):切档臂与对照臂逐位相等")
346
+
347
+ # ---- P2 E 权重与生命周期:落 → 既有 rollback → 切档 → 再落 ----
348
+ for name, mod, kw in (("freshness", freshness, {"now": 1700000000.0}),
349
+ ("weights", weights, {})):
350
+ cg = _lib("p_e_" + name)
351
+ with _mode("confirm"):
352
+ cg.add("pe_1", BODY % "9090", layer="knowledge",
353
+ verification_basis="test", importance=0.4)
354
+ cg.add("pe_2", PLAIN % "被引", layer="knowledge",
355
+ verification_basis="test",
356
+ edges=[{"target": "pe_1", "relation_type": "part_of"}])
357
+ cg.flush()
358
+ r1 = mod.recalc(cg, apply=True, min_delta=0.0001, **kw)
359
+ fm1 = _strip(dict((cg.get("pe_1") or {}).get("frontmatter") or {}))
360
+ rb = mod.rollback(cg) # 既有回滚(设计 §四 第 4 条)
361
+ fp_run = _fingerprint(cg.root)
362
+ with _mode("full"):
363
+ fp_sw = _fingerprint(cg.root)
364
+ r2 = mod.recalc(cg, apply=True, min_delta=0.0001, **kw)
365
+ fm2 = _strip(dict((cg.get("pe_1") or {}).get("frontmatter") or {}))
366
+ ok(rb.get("ok") is True,
367
+ "P2 %s 前置:既有 rollback 生效(两轮同起点;reverted=%s)"
368
+ % (name, rb.get("reverted")), rb)
369
+ ok(fp_run == fp_sw,
370
+ "P2 %s 切档本身零写入:库盘面指纹跨切换逐位相同" % name)
371
+ ok(_strip(r1) == _strip(r2),
372
+ "P2 %s 读数:confirm 档与 full 档**逐位相等**(E 分数类不阻塞——"
373
+ "设计 §三 confirm/full 列 E=允许)" % name, _strip(r1).get("written"))
374
+ ok(fm1 == fm2,
375
+ "P2 %s 落盘 fm(剔除时钟面):confirm 档与 full 档逐位相等" % name,
376
+ {k: (fm1.get(k), fm2.get(k)) for k in sorted(set(fm1) | set(fm2))
377
+ if fm1.get(k) != fm2.get(k)})
378
+ cg.close()
379
+
380
+
381
+ # ================================== Q 组:与改动前基线的 oracle 对拍(HEAD 导出)
382
+ #: 观测脚本(**守卫生成、跑在系统临时区**):对给定源码树跑固定场景,输出
383
+ #: 规范化 JSON。同一份脚本跑「HEAD 导出树」与「工作树」两遍,逐项对拍。
384
+ _RUNNER = r'''# -*- coding: utf-8 -*-
385
+ """对拍观测脚本(守卫生成,非仓内文件):argv = 源码树 库根 输出JSON。"""
386
+ import json
387
+ import os
388
+ import re
389
+ import sys
390
+ import time
391
+
392
+ REPO, LIB, OUT = sys.argv[1], sys.argv[2], sys.argv[3]
393
+ SB = os.path.dirname(os.path.abspath(LIB))
394
+ os.environ["MDCG_AUX_ROOT"] = SB
395
+ os.environ["MDCG_ROOT"] = os.path.join(SB, "root")
396
+ os.environ["MDCG_MASTER_KEY"] = "0" * 64
397
+ os.environ.pop("MDCG_TEST_LIVE_ROOT", None)
398
+ _POL = os.path.join(SB, "policy.json")
399
+ with open(_POL, "w", encoding="utf-8") as f:
400
+ json.dump({"forbidden": ["FORBIDDEN_WORD"], "required": ["PASSED"]}, f)
401
+ os.environ["MDCG_POLICY_FILE"] = _POL
402
+ sys.path.insert(0, REPO)
403
+
404
+ import md_cg # noqa: E402
405
+ from md_cg import autonomy_modes as am, writepipe, forgetting, nodefile # noqa: E402
406
+ from md_cg import freshness, weights # noqa: E402
407
+ from md_cg import mcp_server # noqa: E402
408
+ from md_cg.mdcos import MdCGOS # noqa: E402
409
+
410
+ ENV = am.AUTONOMY_ENV_KEYS["mode"]
411
+ VOL = ("t", "created_at", "last_access", "time_window", "last_merge_at",
412
+ "batch", "note", "elapsed_ms", "updated_at")
413
+ AGG = re.compile(r"【聚合 [^】]*】")
414
+ PID = re.compile(r"prop_[0-9a-f]{8,}") # pid 含时钟+随机熵(身份面,掩码)
415
+ TS0 = 1700000000.0 # 固定观测时刻(E 分数类确定性的锚)
416
+ BODY = ("# 功能名:部署面端口约定\n"
417
+ "# 生效条件:载体/位置:prod 集群;时间:2026-09-30 起;方法:部署清单核对;约束:无\n"
418
+ "# 子功能:登记各服务监听端口\n"
419
+ "# 执行:核对 manifest 的 ports 段\n"
420
+ "# 验证方式:test\n"
421
+ "# 不适用条件:无\n"
422
+ "\n网关监听端口 8080/HTTP;管理面监听端口 8081/HTTP;"
423
+ "指标面监听端口 %s/HTTP;日志面监听端口 9091/HTTP\n")
424
+ PLAIN = "PASSED 探针正文 %s"
425
+
426
+
427
+ def strip(d):
428
+ if isinstance(d, dict):
429
+ return {k: strip(v) for k, v in d.items() if k not in VOL}
430
+ if isinstance(d, list):
431
+ return [strip(x) for x in d]
432
+ if isinstance(d, str):
433
+ return PID.sub("prop_<id>", AGG.sub("【聚合 <ts>】", d))
434
+ return d
435
+
436
+
437
+ def pipe():
438
+ return writepipe.install_default_gates(writepipe.WritePipeline())
439
+
440
+
441
+ def lib(tag):
442
+ return MdCGOS(os.path.join(LIB, tag), autoflush=0)
443
+
444
+
445
+ def mk_node(root, layer, nid, extra=None, content=None):
446
+ """直写节点盘面(fm 时刻为**字面量**——E 分数类跨运行确定性的前提)。"""
447
+ d = os.path.join(root, layer)
448
+ os.makedirs(d, exist_ok=True)
449
+ fm = {"id": nid, "layer": layer, "created_at": TS0 - 86400.0,
450
+ "last_access": TS0 - 7200.0, "access_count": 3, "importance": 0.4}
451
+ fm.update(extra or {})
452
+ with open(os.path.join(d, nid + ".md"), "w", encoding="utf-8") as f:
453
+ f.write(nodefile.dumps(fm, content if content is not None
454
+ else ("# 探针节点\n\n" + nid + "\n")))
455
+ return root
456
+
457
+
458
+ # 档位 env 清场:键名经**真源表**取(`autonomy_modes.AUTONOMY_ENV_KEYS`),
459
+ # 不引第二处字面量——`test_autonomy_modes` G 组把这条钉死(首轮实测踩中)。
460
+ os.environ.pop(am.AUTONOMY_ENV_KEYS["mode"], None)
461
+
462
+ items = {}
463
+ prov = {"md_cg": os.path.dirname(os.path.abspath(md_cg.__file__))}
464
+ if os.path.abspath(prov["md_cg"]) != os.path.abspath(os.path.join(REPO, "md_cg")):
465
+ raise SystemExit("观测脚本 import 到了非目标树的 md_cg:%s" % prov["md_cg"])
466
+
467
+ # 0. 纯函数面:缺省档 / 矩阵 / plan 档判定
468
+ os.environ.pop(ENV, None)
469
+ items["mode_default"] = {"mode": am.mode()}
470
+ items["matrix"] = {m: {a: am.MATRIX[m][a] for a in am.ACTION_CLASSES}
471
+ for m in am.AUTONOMY_MODES}
472
+ items["plan_decide"] = {a: am.decide(a, mode_explicit="plan")["decision"]
473
+ for a in am.ACTION_CLASSES}
474
+
475
+ # 1. A 新增(confirm 档写链面)
476
+ os.environ[ENV] = "confirm"
477
+ cg = lib("a")
478
+ out = pipe().execute(cg, {"content_kind": "text", "content": PLAIN % "对拍",
479
+ "layer": "knowledge", "node_id": "q_a"})
480
+ items["A_add"] = {"out": strip(out), "node": strip(cg.get("q_a") or {})}
481
+ cg.close()
482
+
483
+ # 2. 插件面 A 新增(mdcg_remember gated=true)
484
+ cg = lib("plg")
485
+ rm = mcp_server._dispatch(cg, "mdcg_remember",
486
+ {"content": PLAIN % "插件面", "node_id": "q_pl",
487
+ "gated": True, "layer": "knowledge"})
488
+ items["plg_A"] = {"out": strip(rm), "node": strip(cg.get("q_pl") or {})}
489
+ cg.close()
490
+
491
+ # 3. E 权重与生命周期(confirm 档:weights.recalc / freshness.recalc)
492
+ cg = lib("ew")
493
+ mk_node(cg.root, "knowledge", "e_a", {"verification_basis": "test"})
494
+ mk_node(cg.root, "knowledge", "e_b",
495
+ {"verification_basis": "test",
496
+ "edges": [{"target": "e_a", "relation_type": "part_of"}]})
497
+ cg.flush()
498
+ rw = weights.recalc(cg, apply=True, min_delta=0.0001)
499
+ items["E_weights"] = {"report": strip(rw),
500
+ "fm": strip(dict((cg.get("e_a") or {}).get("frontmatter") or {}))}
501
+ cg.close()
502
+
503
+ cg = lib("ef")
504
+ mk_node(cg.root, "knowledge", "f_a", {"verification_basis": "test"})
505
+ cg.flush()
506
+ rf = freshness.recalc(cg, apply=True, min_delta=0.0001, now=TS0)
507
+ items["E_fresh"] = {"report": strip(rf),
508
+ "fm": strip(dict((cg.get("f_a") or {}).get("frontmatter") or {}))}
509
+ cg.close()
510
+
511
+ # 4. full 档全线:A 新增(独立库)/ C 覆写 · B 合并 · D 删除(另一独立库——
512
+ # 负对照注入的写链 A 面差异不会级联到本库,DIFF 集合才可精确断言)
513
+ os.environ[ENV] = "full"
514
+ cg = lib("full_a")
515
+ items["full_A"] = {"out": strip(pipe().execute(cg, {
516
+ "content_kind": "text", "content": PLAIN % "全访问A", "layer": "knowledge",
517
+ "node_id": "fa"}))}
518
+ cg.close()
519
+
520
+ cg = lib("full_bcd")
521
+ pa = pipe()
522
+ cg.add("fc", PLAIN % "初版", layer="knowledge", verification_basis="test")
523
+ cg.flush()
524
+ items["full_C"] = {"out": strip(pa.execute(cg, {
525
+ "content_kind": "text", "content": PLAIN % "全访问C", "layer": "knowledge",
526
+ "node_id": "fc"})),
527
+ "content": strip((cg.get("fc") or {}).get("content"))}
528
+ cg.add("fb", BODY % "9090", layer="knowledge", verification_basis="test")
529
+ cg.flush()
530
+ rb = cg.remember_gated("fb_n", BODY % "9595", layer="knowledge")
531
+ items["full_B"] = {"verdict": rb.get("verdict"),
532
+ "content": strip((cg.get("fb") or {}).get("content")),
533
+ "merge_count": ((cg.get("fb") or {}).get("frontmatter")
534
+ or {}).get("merge_count")}
535
+ rd = cg.forget_gated("fb", reason="对拍探针")
536
+ items["full_D"] = {"out": strip(rd), "gone": cg.get("fb") is None,
537
+ "tombstoned": cg.is_tombstoned("fb"),
538
+ "trash": os.path.isfile(os.path.join(cg.root, "trash", "fb.md")),
539
+ "mut_orders": len([r for r in cg.review_list()
540
+ if am.order_kind(r) == "mutation"])}
541
+ cg.close()
542
+
543
+ cg = lib("full_ew")
544
+ mk_node(cg.root, "knowledge", "g_a", {"verification_basis": "test"})
545
+ cg.flush()
546
+ items["full_E_weights"] = {"report": strip(weights.recalc(cg, apply=True,
547
+ min_delta=0.0001)),
548
+ "fm": strip(dict((cg.get("g_a") or {}).get("frontmatter")
549
+ or {}))}
550
+ cg.close()
551
+ cg = lib("full_ef")
552
+ mk_node(cg.root, "knowledge", "h_a", {"verification_basis": "test"})
553
+ cg.flush()
554
+ items["full_E_fresh"] = {"report": strip(freshness.recalc(cg, apply=True,
555
+ min_delta=0.0001,
556
+ now=TS0)),
557
+ "fm": strip(dict((cg.get("h_a") or {}).get("frontmatter")
558
+ or {}))}
559
+ cg.close()
560
+
561
+ # 5. plan 档热路径四面(读数面——对拍口径下两棵树必须一致)
562
+ os.environ[ENV] = "plan"
563
+ cg = lib("plan")
564
+ cg.add("pn", PLAIN % "初版", layer="knowledge", verification_basis="test")
565
+ cg.flush()
566
+ items["plan_C"] = {"out": strip(pipe().execute(cg, {
567
+ "content_kind": "text", "content": PLAIN % "改写版", "layer": "knowledge",
568
+ "node_id": "pn"})),
569
+ "content": strip((cg.get("pn") or {}).get("content"))}
570
+ items["plan_D"] = {"out": strip(cg.forget_gated("pn", reason="对拍探针")),
571
+ "in_place": cg.get("pn") is not None}
572
+ cg.close()
573
+
574
+ with open(OUT, "w", encoding="utf-8") as f:
575
+ json.dump({"provenance": prov, "items": items}, f, ensure_ascii=False,
576
+ sort_keys=True)
577
+ print("RUNNER-OK", len(items))
578
+ '''
579
+
580
+
581
+ def _git_archive_head(dest):
582
+ """`git archive HEAD` 只读导出整树到 dest(返回 None 成功 / 错误串失败)。"""
583
+ try:
584
+ p = subprocess.run(["git", "archive", "--format=tar", "HEAD"],
585
+ capture_output=True, cwd=_repo_root(),
586
+ env=dict(os.environ, PYTHONUTF8="1"))
587
+ except OSError as exc: # noqa: BLE001
588
+ return "git 不可用:%s" % exc
589
+ if p.returncode != 0:
590
+ return "git archive 失败(rc=%s):%s" % (
591
+ p.returncode, (p.stderr or b"").decode("utf-8", "replace")[:200])
592
+ os.makedirs(dest, exist_ok=True)
593
+ try:
594
+ with tarfile.open(fileobj=io.BytesIO(p.stdout)) as tf:
595
+ try:
596
+ tf.extractall(dest, filter="data")
597
+ except TypeError: # Python < 3.12 无 filter 形参
598
+ tf.extractall(dest)
599
+ except (tarfile.TarError, OSError) as exc: # noqa: BLE001
600
+ return "解包失败:%s" % exc
601
+ if not os.path.isfile(os.path.join(dest, "md_cg", "mdcos.py")):
602
+ return "导出树缺 md_cg/mdcos.py(非自洽树)"
603
+ return None
604
+
605
+
606
+ def _run_scenario(repo, tag):
607
+ """对给定源码树跑观测脚本,返回 (items, provenance);失败返回 (None, 错误串)。"""
608
+ lib_root = os.path.join(_SANDBOX, "scn_%s_r%d" % (tag, _RUN[0]))
609
+ out_json = os.path.join(_SANDBOX, "scn_%s_r%d.json" % (tag, _RUN[0]))
610
+ script = os.path.join(_SANDBOX, "parity_runner.py")
611
+ with open(script, "w", encoding="utf-8") as f:
612
+ f.write(_RUNNER)
613
+ env = {k: v for k, v in os.environ.items()
614
+ if not k.startswith("MDCG_")}
615
+ env.update({"PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"})
616
+ p = subprocess.run([sys.executable, "-X", "utf8", script, repo, lib_root,
617
+ out_json],
618
+ capture_output=True, text=True, encoding="utf-8",
619
+ errors="replace", cwd=_SANDBOX, env=env)
620
+ if p.returncode != 0 or not os.path.isfile(out_json):
621
+ return None, ("rc=%s\n%s\n%s" % (p.returncode, (p.stdout or "")[-400:],
622
+ (p.stderr or "")[-800:]))
623
+ with open(out_json, encoding="utf-8") as f:
624
+ blob = json.load(f)
625
+ return blob, ""
626
+
627
+
628
+ #: 负对照的**定点差异**注入(改的是导出树副本,不是工作区):
629
+ #: 把写链 A 新增也判成需确认——若对拍无判别力,这条差异会被漏掉。
630
+ _NEG_ANCHOR = " dec = _am.decide(action)"
631
+ _NEG_INJECT = (' dec = _am.decide(action)\n'
632
+ ' if action == _am.A_ADD:\n'
633
+ ' dec = dict(dec, decision=_am.CONFIRM)')
634
+
635
+ #: cons200(2026-10-03)批的**已知返回体扩展**:写链 out 的 consistency 读数
636
+ #: 新增 kept/truncated 两键(契约面=选面读数+截断可观测,实现与守卫见
637
+ #: `md_cg/consistency.py` / `md_cg/test_cons200_scan_selection.py`)。Q 组对拍
638
+ #: 口径据此**定向**归一化:只豁免这两个新键,**旧键(verdict/strength/reason/…)
639
+ #: 仍逐位对拍**——有意的扩展不误报成 DIFF,真行为差异也不因归一化被漏掉;
640
+ #: 新键另以「在场」断言钉住(删新键让对拍变绿 ⇒ 该断言转红)。同步口径
641
+ #: 2026-10-03,与「守卫随实现同批同步」惯例一致(issue52 批同款先例)。
642
+ _KNOWN_EXT_KEYS = ("kept", "truncated")
643
+
644
+
645
+ # 生效条件:item 为观测脚本产出的对拍项(dict,任意形态)时返回其**深拷贝**,且当 item["out"]["consistency"] 为 dict 时从中剔除 _KNOWN_EXT_KEYS 各键;原项一字不动,非 dict 的 out/consistency 形态原样返回。
646
+ def _norm_known_ext(item):
647
+ """Q 组对拍归一化:只剔「本批已知返回体扩展」键(深拷贝,不动原项)。"""
648
+ it = copy.deepcopy(item)
649
+ cvd = (it.get("out") or {}).get("consistency") if isinstance(it.get("out"), dict) else None
650
+ if isinstance(cvd, dict):
651
+ for k in _KNOWN_EXT_KEYS:
652
+ cvd.pop(k, None)
653
+ return it
654
+
655
+
656
+ def g_q():
657
+ print("== Q 组:与改动前基线的 oracle 对拍(git archive HEAD 只读导出)==")
658
+ head_tree = os.path.join(_SANDBOX, "tree_head")
659
+ err = _git_archive_head(head_tree)
660
+ if err:
661
+ print(" ORACLE-MISS " + err)
662
+ print("\noracle 自检:FAIL(fail-closed,exit 2)")
663
+ raise SystemExit(2)
664
+ ok(True, "Q oracle 装置:git archive HEAD 只读导出到系统临时区(%s)"
665
+ % ("md_cg/mdcos.py 在位"))
666
+
667
+ cur, e1 = _run_scenario(_repo_root(), "cur")
668
+ ok(cur is not None, "Q 观测脚本对**工作树**跑通(同一脚本、同一场景)", e1)
669
+ ora, e2 = _run_scenario(head_tree, "head")
670
+ ok(ora is not None, "Q 观测脚本对 **HEAD 导出树**跑通(同款装置,步骤③同口径)", e2)
671
+ if cur is None or ora is None:
672
+ print("\noracle 对拍:FAIL(观测脚本未跑通,fail-closed)")
673
+ raise SystemExit(2)
674
+ ok(ora["provenance"]["md_cg"].replace("\\", "/")
675
+ .endswith("tree_head/md_cg"),
676
+ "Q 探针自证:HEAD 路确实 import 了导出树(不是工作树)",
677
+ ora["provenance"])
678
+ names = sorted(cur["items"])
679
+ same, diff = [], []
680
+ for n in names:
681
+ (same if _norm_known_ext(cur["items"][n])
682
+ == _norm_known_ext(ora["items"][n]) else diff).append(n)
683
+ print(" SAME(%d): %s" % (len(same), "、".join(same)))
684
+ print(" DIFF(%d): %s" % (len(diff), "、".join(diff) or "(无)"))
685
+ ok(diff == [],
686
+ "Q 不动面对拍:A/E 全线 + full 档全线 + plan 档读数——工作树与改动前"
687
+ "基线(HEAD 导出树)**逐位一致**(%d 项 SAME / %d 项 DIFF;cons200 批"
688
+ "已知扩展键 %s 已定向豁免,见 _KNOWN_EXT_KEYS)"
689
+ % (len(same), len(diff), "、".join(_KNOWN_EXT_KEYS)), diff)
690
+ cvd_names = [n for n in names
691
+ if isinstance(cur["items"][n].get("out"), dict)
692
+ and isinstance(cur["items"][n]["out"].get("consistency"), dict)]
693
+ ok(bool(cvd_names) and all(
694
+ {"kept", "truncated"} <= set(cur["items"][n]["out"]["consistency"])
695
+ for n in cvd_names),
696
+ "Q 已知扩展在场:写链 out 的 consistency 读数(%s)均含本批新增键 "
697
+ "kept/truncated(cons200 契约面;归一化只豁免这两键,旧键仍逐位对拍)"
698
+ % "、".join(cvd_names), cvd_names)
699
+ ok(len(names) >= 10,
700
+ "Q 对拍项覆盖:A/E 全线(A_add/plg_A/E_weights/E_fresh)+ full 档全线"
701
+ "(full_A/full_C/full_B/full_D/full_E_weights/full_E_fresh)+ 矩阵/缺省/"
702
+ " plan 面,共 %d 项" % len(names), names)
703
+
704
+ # ---- 负对照(定点差异注入:判据必须报 DIFF 且恰好命中预期项)----
705
+ wf = os.path.join(head_tree, "md_cg", "writepipe.py")
706
+ src = open(wf, encoding="utf-8").read()
707
+ if _NEG_ANCHOR not in src:
708
+ print(" ANCHOR-MISS 负对照锚点缺失:%r" % _NEG_ANCHOR)
709
+ print("\noracle 自检:FAIL(fail-closed,exit 2)")
710
+ raise SystemExit(2)
711
+ with open(wf, "w", encoding="utf-8") as f:
712
+ f.write(src.replace(_NEG_ANCHOR, _NEG_INJECT, 1))
713
+ mut, e3 = _run_scenario(head_tree, "mut")
714
+ ok(mut is not None, "Q 负对照观测脚本跑通(导出树副本内注入 A 面差异)", e3)
715
+ if mut is not None:
716
+ nd = sorted(n for n in names
717
+ if mut["items"][n] != ora["items"][n])
718
+ ok(nd == ["A_add", "full_A"],
719
+ "Q 负对照(对拍有判别力):导出树副本内定点注入「写链 A 新增也需"
720
+ "确认」⇒ DIFF 项**恰好**为写链 A 面两项(A_add / full_A)",
721
+ nd)
722
+
723
+
724
+ # ==================================== R 组:端到端回滚演练(跨面 · 含子进程 CLI)
725
+ def g_r():
726
+ print("== R 组:端到端回滚演练(confirm 出单链 / full 直落链 · 跨面)==")
727
+ _policy()
728
+ drill = {} # 动作类 → 读数(跨面证据的汇总面)
729
+
730
+ # ---- R1 confirm 档 C 改写:写链面 → 执行桥 → 回滚 CLI 子进程 ----
731
+ with _mode("confirm"):
732
+ cg = _lib("e2e_c")
733
+ cg.add("rc_1", PLAIN % "初版", layer="knowledge",
734
+ verification_basis="test", tags=["e2e"],
735
+ edges=[{"target": "rc_p", "relation_type": "part_of"}])
736
+ cg.add("rc_p", PLAIN % "父", layer="knowledge",
737
+ verification_basis="test")
738
+ cg.flush()
739
+ o = _pipe().execute(cg, {"content_kind": "text", "content": PLAIN % "改写版",
740
+ "layer": "knowledge", "node_id": "rc_1"})
741
+ ok(o.get("moved_to") == "review_queue" and bool(o.get("pid")),
742
+ "R1 写链面出单(C 改写):moved_to=review_queue + pid",
743
+ {k: o.get(k) for k in ("moved_to", "pid")})
744
+ d = cg.review_decide(o["pid"], "accept", reason="演练")
745
+ m = d.get("mutation") or {}
746
+ ok(d.get("ok") is True and bool(m.get("before")) and bool(m.get("rollback"))
747
+ and bool(m.get("impact")),
748
+ "R1 执行桥 accept:前像/影响面/回滚命令三字段补全",
749
+ {k: m.get(k) for k in ("before", "rollback")})
750
+ ok("改写版" in (cg.get("rc_1") or {}).get("content", ""),
751
+ "R1 落:覆写生效(正文已换)")
752
+ pre_rel = m.get("before") or ""
753
+ pre_b = _bytes(_pre_path(cg, pre_rel))
754
+ got = _run_cli(m.get("rollback") or "")
755
+ _refresh(cg)
756
+ out = got.get("json") or {}
757
+ ok(got["rc"] == 0 and out.get("ok") is True
758
+ and out.get("bytes_equal_preimage") is True,
759
+ "R1 回滚 CLI 子进程实测:rc=0、ok=True、逐字节比对前像=True",
760
+ {"rc": got["rc"], "err": (got.get("err") or "")[-160:]})
761
+ ok(bool(pre_b) and _bytes(_disk_path(cg, "rc_1")) == pre_b,
762
+ "R1 回滚后盘面逐字节 == 前像(第三方改动面由前像时点语义保证)")
763
+ res = out.get("residual") or {}
764
+ ok(res.get("index_restored") is True and res.get("edges_restored") is True
765
+ and res.get("agg_lines_restored") is True,
766
+ "R1 残差核对三面全回(索引/边/聚合行)", res)
767
+ ok(_entry_from_preimage(cg, "rc_1", pre_rel) is not None
768
+ and cg.index["nodes"].get("rc_1")
769
+ == _entry_from_preimage(cg, "rc_1", pre_rel),
770
+ "R1 索引条目回齐:回滚后条目 == 前像重算条目(逐位)")
771
+ # CLI 腿负对照(输入侧判别力):不存在的 pid ⇒ fail-closed 非零退出
772
+ bad = _run_cli(rollback.command_for(cg.root, "prop_deadbeef"))
773
+ bout = bad.get("json") or {}
774
+ ok(bad["rc"] == 1 and bout.get("ok") is False
775
+ and bout.get("error") == "decision_not_found",
776
+ "R1 CLI 负对照:不存在的 pid ⇒ rc=1 + decision_not_found"
777
+ "(CLI 腿不是无脑报成功)", {"rc": bad["rc"], "json": bout})
778
+ ok(rollback.collect_impact(cg, "rc_1")["edges"]["parents"] == ["rc_p"],
779
+ "R1 边回齐:声明式 part_of 父边回案")
780
+ drill["C"] = {"face": "写链(cg op=write)", "cli_rc": got["rc"],
781
+ "bytes": bool(pre_b)
782
+ and _bytes(_disk_path(cg, "rc_1")) == pre_b,
783
+ "residual": res}
784
+
785
+ # ---- R2 confirm 档 B 合并:插件面(mdcg_remember gated)→ 回滚 CLI ----
786
+ with _mode("confirm"):
787
+ cg = _lib("e2e_b")
788
+ cg.add("rb_t", BODY % "8081", layer="knowledge",
789
+ verification_basis="test")
790
+ cg.flush()
791
+ pre_b = _bytes(_disk_path(cg, "rb_t"))
792
+ rb = mcp_server._dispatch(cg, "mdcg_remember",
793
+ {"content": BODY % "9595", "node_id": "rb_n",
794
+ "gated": True, "layer": "knowledge"})
795
+ ok(rb.get("verdict") == "CONFIRM" and rb.get("moved_to") == "review_queue"
796
+ and bool(rb.get("pid")),
797
+ "R2 插件面出单(B 合并):verdict=CONFIRM + review_queue + pid",
798
+ {k: rb.get(k) for k in ("verdict", "moved_to", "pid")})
799
+ d = cg.review_decide(rb["pid"], "accept", reason="演练")
800
+ m = d.get("mutation") or {}
801
+ ok(d.get("ok") is True
802
+ and forgetting.AGG_MARK in (cg.get("rb_t") or {}).get("content", "")
803
+ and ((cg.get("rb_t") or {}).get("frontmatter") or {})
804
+ .get("merge_count") == 1,
805
+ "R2 落:聚合行已追加、merge_count=1(reinforce 原语)")
806
+ got = _run_cli(m.get("rollback") or "")
807
+ _refresh(cg)
808
+ out = got.get("json") or {}
809
+ ok(got["rc"] == 0 and out.get("ok") is True
810
+ and out.get("bytes_equal_preimage") is True,
811
+ "R2 回滚 CLI 子进程实测:rc=0、ok=True、逐字节比对前像=True",
812
+ {"rc": got["rc"], "err": (got.get("err") or "")[-160:]})
813
+ ok(bool(pre_b) and _bytes(_disk_path(cg, "rb_t")) == pre_b,
814
+ "R2 回滚后盘面逐字节 == 前像(合并前正文;**非空前置**——两侧皆空"
815
+ "不再静默判绿,U2 收口)", {"pre_b_len": len(pre_b)})
816
+ ok(forgetting.AGG_MARK not in (cg.get("rb_t") or {}).get("content", "")
817
+ and not ((cg.get("rb_t") or {}).get("frontmatter") or {})
818
+ .get("merge_count"),
819
+ "R2 聚合行/merge_count 不残留(定向剥离 + fm 全量取前像)")
820
+ res = out.get("residual") or {}
821
+ ok(res.get("index_restored") is True and res.get("edges_restored") is True
822
+ and res.get("agg_lines_restored") is True,
823
+ "R2 残差核对三面全回(索引/边/聚合行)", res)
824
+ drill["B"] = {"face": "插件面(mdcg_remember gated)", "cli_rc": got["rc"],
825
+ "bytes": bool(pre_b)
826
+ and _bytes(_disk_path(cg, "rb_t")) == pre_b,
827
+ "residual": res}
828
+
829
+ # ---- R3 confirm 档 D 删除:工具面(cg op=forget)→ 回滚 CLI ----
830
+ with _mode("confirm"):
831
+ cg = _lib("e2e_d")
832
+ cg.add("rd_1", PLAIN % "初版", layer="knowledge",
833
+ verification_basis="test", tags=["e2e"], importance=0.62)
834
+ cg.flush()
835
+ pre_b = _bytes(_disk_path(cg, "rd_1"))
836
+ r3 = mcp_server._dispatch(cg, "cg", {"op": "forget", "node_id": "rd_1",
837
+ "reason": "演练"})
838
+ ok(r3.get("moved_to") == "review_queue" and r3.get("deleted") is False
839
+ and bool(r3.get("pid")),
840
+ "R3 工具面出单(D 删除):review_queue + deleted=False + pid",
841
+ {k: r3.get(k) for k in ("moved_to", "deleted", "pid")})
842
+ d = cg.review_decide(r3["pid"], "accept", reason="演练")
843
+ m = d.get("mutation") or {}
844
+ ok(d.get("ok") is True and cg.get("rd_1") is None
845
+ and cg.is_tombstoned("rd_1"),
846
+ "R3 落:软删生效(节点离位 + 删除清单在案)")
847
+ got = _run_cli(m.get("rollback") or "")
848
+ _refresh(cg)
849
+ out = got.get("json") or {}
850
+ ok(got["rc"] == 0 and out.get("ok") is True
851
+ and out.get("bytes_equal_preimage") is True,
852
+ "R3 回滚 CLI 子进程实测:rc=0、ok=True、逐字节比对前像=True",
853
+ {"rc": got["rc"], "calibrated": out.get("calibrated"),
854
+ "err": (got.get("err") or "")[-160:]})
855
+ ok(bool(pre_b) and _bytes(_disk_path(cg, "rd_1")) == pre_b,
856
+ "R3 回滚后盘面逐字节 == 前像(restore + 前像校准;**非空前置**——"
857
+ "同 R1/R3-节点复位口径,U2 收口)", {"pre_b_len": len(pre_b)})
858
+ res = out.get("residual") or {}
859
+ ok(res.get("index_restored") is True and res.get("edges_restored") is True
860
+ and res.get("agg_lines_restored") is True,
861
+ "R3 残差核对三面全回(索引/边/聚合行)", res)
862
+ ok(bool(pre_b) and cg.index["nodes"].get("rd_1") is not None
863
+ and _bytes(_disk_path(cg, "rd_1")) == pre_b,
864
+ "R3 节点复位:索引条目在案且盘面 == 前像(逐字节)")
865
+ drill["D"] = {"face": "工具面(cg op=forget)", "cli_rc": got["rc"],
866
+ "bytes": bool(pre_b)
867
+ and _bytes(_disk_path(cg, "rd_1")) == pre_b,
868
+ "residual": res}
869
+
870
+ # ---- R4 full 档直落链:B/C/D 各一遍(直落 → 统一回滚入口 → 逐字节)----
871
+ with _mode("full"):
872
+ cg = _lib("e2e_full")
873
+ # C 改写(写链面直落)
874
+ cg.add("fc_1", PLAIN % "初版", layer="knowledge",
875
+ verification_basis="test",
876
+ edges=[{"target": "fc_p", "relation_type": "part_of"}])
877
+ cg.add("fc_p", PLAIN % "父", layer="knowledge",
878
+ verification_basis="test")
879
+ cg.add("fb_t", BODY % "9090", layer="knowledge",
880
+ verification_basis="test")
881
+ cg.add("fd_1", PLAIN % "初版", layer="knowledge",
882
+ verification_basis="test")
883
+ cg.flush()
884
+ # 演练侧按**执行时点**拍前像(与执行桥同一单点 protect.snapshot_preimage)
885
+ pre_c = protect.snapshot_preimage(cg, "fc_1", action="C", pid="drill_c")
886
+ imp_c = rollback.collect_impact(cg, "fc_1")
887
+ pre_b = _bytes(_disk_path(cg, "fc_1"))
888
+ pre_blob = _bytes(_pre_path(cg, pre_c)) if pre_c else b""
889
+ ok(bool(pre_c) and bool(pre_blob) and bool(pre_b),
890
+ "R4 前置取证·C 链:执行时点前像**与**盘面快照均非空(前像 %d 字节、"
891
+ "盘面 %d 字节)——「空池静默判绿」的同族缺口一并收口(U2)"
892
+ % (len(pre_blob), len(pre_b)),
893
+ {"pre_c": pre_c, "pre_b_len": len(pre_b)})
894
+ o_c = _pipe().execute(cg, {"content_kind": "text",
895
+ "content": PLAIN % "全访问版",
896
+ "layer": "knowledge", "node_id": "fc_1"})
897
+ ok(o_c.get("committed") is True and o_c.get("moved_to") is None
898
+ and "全访问版" in (cg.get("fc_1") or {}).get("content", ""),
899
+ "R4 full 档 C 直落(写链面):无单、committed=True、正文已换",
900
+ {k: o_c.get(k) for k in ("committed", "moved_to")})
901
+ rr_c = rollback.rollback_mutation(
902
+ cg, {"action": "C", "target": "fc_1", "before": pre_c,
903
+ "impact": imp_c, "rollback": ""}, reason="演练", actor="drill")
904
+ ok(bool(pre_b) and rr_c.get("ok") is True
905
+ and rr_c.get("bytes_equal_preimage") is True
906
+ and _bytes(_disk_path(cg, "fc_1")) == pre_b,
907
+ "R4 full 档 C 回滚(统一回滚入口):逐字节 == 执行时点前像"
908
+ "(**非空前置**——U2 收口)",
909
+ {k: rr_c.get(k) for k in ("ok", "bytes_equal_preimage", "error")})
910
+ ok((rr_c.get("residual") or {}).get("index_restored") is True
911
+ and (rr_c.get("residual") or {}).get("edges_restored") is True,
912
+ "R4 full 档 C 残差核对:索引/边回齐", rr_c.get("residual"))
913
+ # B 合并(插件面直落)
914
+ pre_m = protect.snapshot_preimage(cg, "fb_t", action="B", pid="drill_b")
915
+ imp_m = rollback.collect_impact(cg, "fb_t")
916
+ pre_mb = _bytes(_disk_path(cg, "fb_t"))
917
+ pre_blob = _bytes(_pre_path(cg, pre_m)) if pre_m else b""
918
+ ok(bool(pre_m) and bool(pre_blob) and bool(pre_mb),
919
+ "R4 前置取证·B 链:执行时点前像**与**盘面快照均非空(前像 %d 字节、"
920
+ "盘面 %d 字节)——「空池静默判绿」的同族缺口一并收口(U2)"
921
+ % (len(pre_blob), len(pre_mb)),
922
+ {"pre_m": pre_m, "pre_mb_len": len(pre_mb)})
923
+ r_m = mcp_server._dispatch(cg, "mdcg_remember",
924
+ {"content": BODY % "9595", "node_id": "fb_n",
925
+ "gated": True, "layer": "knowledge"})
926
+ ok(r_m.get("verdict") == "MERGE" and r_m.get("moved_to") is None
927
+ and forgetting.AGG_MARK in (cg.get("fb_t") or {}).get("content", ""),
928
+ "R4 full 档 B 直落(插件面):verdict=MERGE、聚合行已追加、无单",
929
+ r_m.get("verdict"))
930
+ rr_m = rollback.rollback_mutation(
931
+ cg, {"action": "B", "target": "fb_t", "before": pre_m,
932
+ "impact": imp_m, "rollback": ""}, reason="演练", actor="drill")
933
+ ok(bool(pre_mb) and rr_m.get("ok") is True
934
+ and rr_m.get("bytes_equal_preimage") is True
935
+ and _bytes(_disk_path(cg, "fb_t")) == pre_mb,
936
+ "R4 full 档 B 回滚(定向剥离):逐字节 == 执行时点前像"
937
+ "(**非空前置**——U2 收口)",
938
+ {k: rr_m.get(k) for k in ("ok", "bytes_equal_preimage", "method")})
939
+ ok((rr_m.get("residual") or {}).get("agg_lines_restored") is True
940
+ and (rr_m.get("residual") or {}).get("index_restored") is True,
941
+ "R4 full 档 B 残差核对:聚合行/索引回齐", rr_m.get("residual"))
942
+ # D 删除(工具面直落)
943
+ pre_d = protect.snapshot_preimage(cg, "fd_1", action="D", pid="drill_d")
944
+ imp_d = rollback.collect_impact(cg, "fd_1")
945
+ pre_db = _bytes(_disk_path(cg, "fd_1"))
946
+ pre_blob = _bytes(_pre_path(cg, pre_d)) if pre_d else b""
947
+ ok(bool(pre_d) and bool(pre_blob) and bool(pre_db),
948
+ "R4 前置取证·D 链:执行时点前像**与**盘面快照均非空(前像 %d 字节、"
949
+ "盘面 %d 字节)——「空池静默判绿」的同族缺口一并收口(U2)"
950
+ % (len(pre_blob), len(pre_db)),
951
+ {"pre_d": pre_d, "pre_db_len": len(pre_db)})
952
+ r_d = mcp_server._dispatch(cg, "cg", {"op": "forget", "node_id": "fd_1",
953
+ "reason": "演练"})
954
+ ok(r_d.get("ok") is True and r_d.get("moved_to") is None
955
+ and r_d.get("deleted") is not False and cg.get("fd_1") is None,
956
+ "R4 full 档 D 直落(工具面):软删生效、无单",
957
+ {k: r_d.get(k) for k in ("ok", "deleted", "moved_to")})
958
+ rr_d = rollback.rollback_mutation(
959
+ cg, {"action": "D", "target": "fd_1", "before": pre_d,
960
+ "impact": imp_d, "rollback": ""}, reason="演练", actor="drill")
961
+ ok(bool(pre_db) and rr_d.get("ok") is True
962
+ and rr_d.get("bytes_equal_preimage") is True
963
+ and _bytes(_disk_path(cg, "fd_1")) == pre_db,
964
+ "R4 full 档 D 回滚(restore + 前像校准):逐字节 == 执行时点前像"
965
+ "(**非空前置**——U2 收口)",
966
+ {k: rr_d.get(k) for k in ("ok", "bytes_equal_preimage", "method")})
967
+ ok((rr_d.get("residual") or {}).get("index_restored") is True
968
+ and (rr_d.get("residual") or {}).get("edges_restored") is True
969
+ and (rr_d.get("residual") or {}).get("agg_lines_restored") is True,
970
+ "R4 full 档 D 残差核对三面全回", rr_d.get("residual"))
971
+ ok(len(_mut_entries(cg)) == 0,
972
+ "R4 full 档全程零变更单(直落面不产单——与 confirm 链的分界)",
973
+ len(_mut_entries(cg)))
974
+ # 边界**现状钉**(留池项,非背书):full 档直落路径不自动拍前像——
975
+ # 三节点的 preimage 审计行各**恰好 1 条且均来自演练侧**(pid=drill_*)。
976
+ # 若将来把「一切破坏性动作先留前像」(设计 §六 义务①)接到 full 直落
977
+ # 路径,本断言转红、须随之更新(这正是接线批次的入口判据)。
978
+ pims = {n: _pims(cg, n) for n in ("fc_1", "fb_t", "fd_1")}
979
+ ok(all(len(v) >= 1 and all("drill_" in str(r.get("reason")) for r in v)
980
+ for v in pims.values()),
981
+ "R4 现状钉:full 档 C/B/D 直落路径**不自动**拍前像——三节点的 "
982
+ "preimage 审计行全部来自演练侧(pid=drill_*);设计 §六 义务① 在 "
983
+ "full 直落路径未接线(留池,非背书):若将来接线,本断言转红、"
984
+ "须随之更新", {n: len(v) for n, v in pims.items()})
985
+ cg.close()
986
+
987
+ # ---- R5 跨面汇总面(三条链 × 两个档位齐备)----
988
+ faces = {k: v["face"] for k, v in drill.items()}
989
+ ok(sorted(drill) == ["B", "C", "D"]
990
+ and len(set(faces.values())) == 3
991
+ and all(v["cli_rc"] == 0 and v["bytes"] is True
992
+ and all(v["residual"].values()) for v in drill.values()),
993
+ "R5 跨面演练齐备:C/B/D 三条链各经**不同入口面**(写链/插件面/工具面)"
994
+ "+ 执行桥 + 回滚 CLI 子进程,逐字节与残差三面全真",
995
+ {"faces": faces,
996
+ "readings": {k: {"rc": v["cli_rc"], "bytes": v["bytes"]}
997
+ for k, v in drill.items()}})
998
+
999
+
1000
+ def g_s():
1001
+ """F1 收口(2026-10-03):变异表完整性自检的判别力自证(合成源)。
1002
+
1003
+ 与 `md_cg/test_autonomy_modes.py` 的 F1 组**两守卫对齐**(同款判据、同款
1004
+ 测例形态);真实表断言即「表长 8 = 声明」的**自动核验载体**。
1005
+ """
1006
+ _S_DROP_TUPLE = ("x\n_SRC_MUTATIONS = (\n"
1007
+ " # ① 甲\n (\"a\",),\n"
1008
+ " # ② 乙\n" # ← ② 删元组留注释(F1 原形)
1009
+ " # ③ 丙\n (\"c\",),\n)\n")
1010
+ _S_DROP_BOTH = ("x\n_SRC_MUTATIONS = (\n"
1011
+ " # ① 甲\n (\"a\",),\n"
1012
+ " # ③ 丙\n (\"c\",),\n)\n") # ← ② 注释+元组同删
1013
+ _self_src = io.open(os.path.abspath(__file__), encoding="utf-8").read()
1014
+ ok(_table_gaps(_S_DROP_TUPLE, 2, ("①", "②", "③")) == ["②", "len:2≠3"],
1015
+ "F1① 防误删自检判别力(mp):删元组留注释 ⇒ 报缺口编号 ②"
1016
+ "(表长兜底随报)",
1017
+ _table_gaps(_S_DROP_TUPLE, 2, ("①", "②", "③")))
1018
+ ok(_table_gaps(_S_DROP_BOTH, 2, ("①", "②", "③")) == ["②", "len:2≠3"],
1019
+ "F1② 防误删自检判别力(mp):注释与元组同删 ⇒ 仍报缺口编号 ②",
1020
+ _table_gaps(_S_DROP_BOTH, 2, ("①", "②", "③")))
1021
+ ok(_table_integrity_check() == [],
1022
+ "F1③ 真实表完好(**自动核验载体**,mp):编号 ①–%s 无缺口、表长 %d = 声明"
1023
+ % (_MUTATION_IDS[-1], len(_MUTATION_IDS)),
1024
+ _table_integrity_check())
1025
+ _n1, _n2 = len(_SRC_MUTATIONS) - 1, len(_SRC_MUTATIONS)
1026
+ ok(_table_gaps(_self_src, _n1) == ["len:%d≠%d" % (_n1, _n2)],
1027
+ "F1④ 表长判据兜底(mp):真实源 + 表长-1 ⇒ 报「len:%d≠%d」" % (_n1, _n2),
1028
+ _table_gaps(_self_src, _n1))
1029
+
1030
+
1031
+ _GROUPS = (g_p, g_q, g_r, g_s)
1032
+
1033
+
1034
+ def _run_groups():
1035
+ """跑全部断言组(静默),返回失败数——供变异自证复用。"""
1036
+ _PASS.clear()
1037
+ _FAIL.clear()
1038
+ for g in _GROUPS:
1039
+ g()
1040
+ return len(_FAIL)
1041
+
1042
+
1043
+ # =============================================================== 定点变异自证
1044
+ #: 本守卫模块对象——`_bytes` 这类**守卫自身**的判据也须有定点变异(补强批次
1045
+ #: v1.2·U2 的「空池注入」),故变异表允许以本模块为属主(`sys.modules[__name__]`
1046
+ #: 是同一对象的现取形态,不写第二处名字字面量)。
1047
+ _SELF = sys.modules[__name__]
1048
+
1049
+ # 表内每项 = (说明, 目标, 锚点原文, 替换文, 预期红项数)。锚点须**逐字**出现在
1050
+ # 目标函数源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。
1051
+ # 目标形态:("mod", 模块, 函数名) / ("cls", 类, 方法名)。
1052
+ _SRC_MUTATIONS = (
1053
+ # ①「档位不生效」——env 读取口恒返回真源缺省(confirm):切档锚与
1054
+ # full 档直落面同时失效(设计 §十一 明文「注入『档位不生效』必红」)。
1055
+ # 红 5(实测 2026-10-03):P 组「切档锚不空转·full」(切了 full 但 mode()
1056
+ # 读回 confirm)+ R4 四条(full 档 C/B/D 直落三条——退化后变出单/入库形态
1057
+ # + R4「全程零变更单」)。对照臂(confirm→confirm)与 P 组对拍本体在退化下
1058
+ # 双臂同值(语义相符,不红);E 面无档位判据 ⇒ P2 不受影响(同因)。
1059
+ ("档位不生效(配置档位恒返回表内缺省)", "mod", autonomy_modes,
1060
+ "_configured_mode",
1061
+ ' raw = autonomy_env("mode", environ)', ' raw = DEFAULT_MODE', 5),
1062
+ # ② 执行桥空转——accept「成功了」但动作没做(汇报不实的最危险形态):
1063
+ # 三条 confirm 链的「落」判据与回滚面整体失效。
1064
+ # 红 13(实测 2026-10-03):R1 六条(前像补全/落/CLI 读数/逐字节/残差/
1065
+ # 索引条目)+ R2 三条(落/CLI 读数/残差)+ R3 三条(落/CLI 读数/残差)
1066
+ # + R5 跨面汇总(读数面按实测值断言,不写死)。语义相符仍绿:R1「边回齐」
1067
+ # (边本就未动)、R1 CLI 负对照(期望失败)、R2/R3「回滚后盘面逐字节 ==
1068
+ # 前像」(未发生变更 ⇒ 与前像同值)。
1069
+ ("accept 不执行(执行桥空转)", "cls", MdCGOS, "_mutation_execute",
1070
+ ' pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),',
1071
+ ' return {"ok": True, "node_id": tgt}\n'
1072
+ ' pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),', 13),
1073
+ # ③ 回滚不落盘(_land 空转)——回滚「报成功了」但盘面没回。
1074
+ # 红 5(实测 2026-10-03):**只有 R4 五条**(进程内统一回滚入口的 C/B/D
1075
+ # 逐字节与 C/B 残差)。R1/R2/R3(confirm 链)不受影响——回滚经 **CLI 子进程**
1076
+ # 执行,进程内变异不跨进程到达子进程(如实登记:CLI 腿的判别力由 ②/④ 这类
1077
+ # **状态面**变异与 R1 的 CLI 负对照承担,不由本变异承担)。
1078
+ ("回滚不落盘(_land 空转)", "mod", rollback, "_land",
1079
+ ' cg._write_node(node_id, path, fm, content)', ' return', 5),
1080
+ # ④ 执行记录不落 mutation(rec["mutation"] 摘除)——回滚 CLI 读不到载荷
1081
+ # (「回滚命令」的读取源断链):confirm 三条链的 CLI 后置判据整体失效。
1082
+ # 红 14(实测 2026-10-03):R1 五条(CLI 读数/逐字节/残差/索引条目/边回齐)
1083
+ # + R2 四条(CLI 读数/逐字节/聚合行不残留/残差)+ R3 四条(CLI 读数/逐字节/
1084
+ # 残差/节点复位)+ R5 跨面汇总。R4(进程内入口直接持有载荷)与 CLI 负对照
1085
+ # 不受影响。
1086
+ ("执行记录不落 mutation(rec['mutation'] 摘除)", "cls", MdCGOS,
1087
+ "_record_decision",
1088
+ ' _mut = result.get("mutation")\n'
1089
+ ' if isinstance(_mut, dict):\n'
1090
+ ' rec["mutation"] = _mut',
1091
+ ' _mut = result.get("mutation")\n'
1092
+ ' if False:\n'
1093
+ ' rec["mutation"] = _mut', 14),
1094
+ # ⑤ 写链「full 档放行分支」失效(full 档 A/C 也出单)——切档对拍必须
1095
+ # 抓住它(若对拍无判别力,这条差异会被漏掉)。
1096
+ # 红 4(实测 2026-10-03):P1 两条(切档核心判据「full 首写 == confirm 首写」
1097
+ # + 落盘节点对拍)+ R4 两条(full 档 C 直落无单/正文已换 + 全程零变更单)。
1098
+ ("写链 full 档放行分支失效(full 也出单)", "mod", writepipe,
1099
+ "_gate_autonomy",
1100
+ ' if dec["decision"] == _am.ALLOW:\n return None',
1101
+ ' if dec["decision"] == _am.ALLOW and dec["mode"] != "full":\n'
1102
+ ' return None', 4),
1103
+ # ⑥ E 分数类被档位闸阻塞(freshness 面)——「非破坏性动作 E 不阻塞」的
1104
+ # 反向形态:E 类动作路径被塞进档位判据,**full 档**分支被拦 ⇒ P2 的
1105
+ # confirm↔full 两轮读数与落盘 fm 分叉。(注:E 面在 confirm/full 的
1106
+ # 矩阵格都是「允许」,故注入须按**档名**分叉才能在同档对比中显形——
1107
+ # 这正是「无档位判据」判据的判别形态。)
1108
+ # 红 2(实测 2026-10-03):P2 freshness 的读数对拍 + 落盘 fm 对拍。
1109
+ ("E 面被档位闸阻塞(freshness.recalc 按档名分叉)", "mod",
1110
+ freshness, "recalc",
1111
+ ' ref = freshness_now() if now is None else float(now)',
1112
+ ' from . import autonomy_modes as _am\n'
1113
+ ' if _am.mode() == "full":\n'
1114
+ ' return {"ok": False, "error": "mode_blocked"}\n'
1115
+ ' ref = freshness_now() if now is None else float(now)', 2),
1116
+ # ⑦ E 分数类被档位闸阻塞(weights 面)——同上,另一族(importance 轴)。
1117
+ # 红 2(实测 2026-10-03):P2 weights 的读数对拍 + 落盘 fm 对拍。
1118
+ ("E 面被档位闸阻塞(weights.recalc 按档名分叉)", "mod",
1119
+ weights, "recalc",
1120
+ ' nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}\n'
1121
+ ' indeg = coverage_index(cg)',
1122
+ ' from . import autonomy_modes as _am\n'
1123
+ ' if _am.mode() == "full":\n'
1124
+ ' return {"ok": False, "error": "mode_blocked"}\n'
1125
+ ' nodes = (getattr(cg, "index", None) or {}).get("nodes") or {}\n'
1126
+ ' indeg = coverage_index(cg)', 2),
1127
+ # ⑧ 补强批次 v1.2·U2——**空池注入**(复核探针形态):守卫的字节读取单点
1128
+ # `_bytes` 恒返回空字节 ⇒ 一切「逐字节比对前像」的合取项两侧皆空。
1129
+ # U2 收口前(复核探针实测「R 组红 3 / 绿 30」):R1-逐字节与 R3-节点复位
1130
+ # 因带 `bool(pre_b)` 门槛判红,而 **R2「回滚后盘面逐字节」、R3 首个逐字节、
1131
+ # R4 三条的第三合取项**在两侧皆空时**静默判绿**——同组两种口径并存。
1132
+ # 收口后(非空前置统一 + R4 三条非空取证)⇒ **红 11**(实测 2026-10-03):
1133
+ # = R1-逐字节 + R2-逐字节 + R3-逐字节 + R3-节点复位 + R4 C/B/D 三条
1134
+ # + R4 C/B/D 三条前置取证 + R5 跨面汇总(drill 读数面按实测值断言)。
1135
+ ("空池注入(_bytes 恒返回空字节·U2 判别力)", "mod", _SELF, "_bytes",
1136
+ ' with open(path, "rb") as f:\n return f.read()',
1137
+ ' return b""', 11),
1138
+ )
1139
+
1140
+ #: 「防误删自检」的判别力钉(F1 收口,2026-10-03)——**不占表内编号**:表长恒 =
1141
+ #: `_MUTATION_IDS` 的长度(复核口径与实存一致);它是自检体系的判别力证明,
1142
+ #: 形态与表内条目**同口径**:源码替换 + exec 重装 + 红项数比照,锚点同样过
1143
+ #: `_anchor_check`(漂移即 ANCHOR-MISS、fail-closed)。与
1144
+ #: `md_cg/test_autonomy_modes.py` 的同名钉**两守卫对齐**。
1145
+ #: 剥掉 `_table_gaps` 的自检开关(恒判「完好」)⇒ 三条合成源自证测例(F1①②④)
1146
+ #: 转红。红 3(实测);F1③(真实表期望 [])语义相符仍绿。
1147
+ _SELFCHECK_MUTATION = (
1148
+ "防误删自检开关剥除(_table_gaps 恒判完好)", "mod", _SELF, "_table_gaps",
1149
+ 'def _table_gaps(text, entries_count, ids=_MUTATION_IDS):\n'
1150
+ ' """变异表完整性判定(纯函数):返回缺口说明列表(空 = 完好)。',
1151
+ 'def _table_gaps(text, entries_count, ids=_MUTATION_IDS):\n'
1152
+ ' return []\n'
1153
+ ' """变异表完整性判定(纯函数):返回缺口说明列表(空 = 完好)。', 3)
1154
+
1155
+
1156
+ # ---- 变异表完整性自检(F1 收口:防误删,2026-10-03)------------------------
1157
+ # 与 `md_cg/test_autonomy_modes.py` 同款(两守卫对齐):把「编号无缺口 + 表长与
1158
+ # 显式声明一致」变成机械判据——缺项 ⇒ fail-closed 退出码 2 并报缺口编号,不再
1159
+ # 靠人工发现。与 `_anchor_check` 同层接入(正常运行与 --mutate 均先行执行),
1160
+ # 配内置判别力钉 `_SELFCHECK_MUTATION`(剥掉自检开关 ⇒ 自证测例转红)。
1161
+ # 基线源=**当前工作区文件**,不绑 git HEAD(本仓已有两次教训)。
1162
+ #
1163
+ # 判据(`_table_gaps`,纯函数——自证测例以合成源调它):
1164
+ # a. 编号注释被下一**不同**编号注释覆盖(= 删元组留注释,F1 原形)⇒ 报该编号
1165
+ # (同编号在块内的复提不算);
1166
+ # b. 编号注释序列与声明不一致(缺/重复;含「注释与元组同删」的真删形态)⇒
1167
+ # 报缺者;
1168
+ # c. len(_SRC_MUTATIONS) ≠ 声明长度 ⇒ 报「len:N≠M」兜底。
1169
+ _MUTATION_IDS = ("①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧")
1170
+
1171
+ _ANNOT_RE = re.compile(r"^ # ([①-⑳㉑-㉔])")
1172
+
1173
+
1174
+ def _table_lines(text):
1175
+ """取 `_SRC_MUTATIONS = (` 到首个独立 `)` 行之间的表体行(当前源,非 git 基线)。"""
1176
+ out, on = [], False
1177
+ for line in text.split("\n"):
1178
+ if not on:
1179
+ if line.startswith("_SRC_MUTATIONS = ("):
1180
+ on = True
1181
+ continue
1182
+ if line.rstrip() == ")":
1183
+ break
1184
+ out.append(line)
1185
+ return out
1186
+
1187
+
1188
+ def _table_gaps(text, entries_count, ids=_MUTATION_IDS):
1189
+ """变异表完整性判定(纯函数):返回缺口说明列表(空 = 完好)。"""
1190
+ annot, gaps, pending = [], [], None
1191
+ for line in _table_lines(text):
1192
+ m = _ANNOT_RE.match(line)
1193
+ if m:
1194
+ nid = m.group(1)
1195
+ if pending is None:
1196
+ pending = nid
1197
+ annot.append(nid)
1198
+ elif nid != pending:
1199
+ gaps.append(pending) # a. 上一编号注释未被元组消费
1200
+ pending = nid
1201
+ annot.append(nid)
1202
+ # nid == pending:同一条目注释块内的复提——不覆盖 pending。
1203
+ continue
1204
+ if pending is not None and line.startswith(" ("):
1205
+ pending = None # 元组消费其上最近的编号注释
1206
+ if pending is not None:
1207
+ gaps.append(pending) # 表尾仍有未消费的编号注释
1208
+ for i in ids:
1209
+ if i not in annot:
1210
+ gaps.append(i) # b. 编号缺(含注释与元组同删的形态)
1211
+ elif annot.count(i) > 1:
1212
+ gaps.append(i) # b. 编号重复
1213
+ if entries_count != len(ids):
1214
+ gaps.append("len:%d≠%d" % (entries_count, len(ids))) # c. 表长兜底
1215
+ seen, uniq = set(), []
1216
+ for g in gaps:
1217
+ if g not in seen:
1218
+ seen.add(g)
1219
+ uniq.append(g)
1220
+ return uniq
1221
+
1222
+
1223
+ def _table_integrity_check():
1224
+ """变异表完整性自检(真实表/当前工作区源):返回缺口说明列表(空 = 完好)。"""
1225
+ with io.open(os.path.abspath(__file__), encoding="utf-8") as f:
1226
+ text = f.read()
1227
+ return _table_gaps(text, len(_SRC_MUTATIONS))
1228
+
1229
+
1230
+ def _fn_src(target):
1231
+ kind, owner, name = target
1232
+ return inspect.getsource(getattr(owner, name))
1233
+
1234
+
1235
+ def _strip_indent(text, n):
1236
+ if not n:
1237
+ return text
1238
+ out = []
1239
+ for line in text.split("\n"):
1240
+ if line[:n].strip() == "":
1241
+ out.append(line[n:])
1242
+ else:
1243
+ out.append(line.lstrip())
1244
+ return "\n".join(out)
1245
+
1246
+
1247
+ def _anchor_check():
1248
+ bad = []
1249
+ for name, _kind, owner, fname, old, _new, _n in (_SRC_MUTATIONS
1250
+ + (_SELFCHECK_MUTATION,)):
1251
+ if old not in _fn_src((_kind, owner, fname)):
1252
+ bad.append("变异锚点缺失:%r @%s.%s" % (old[:40], owner.__name__, fname))
1253
+ return bad
1254
+
1255
+
1256
+ @contextlib.contextmanager
1257
+ def _patched(target, old, new):
1258
+ kind, owner, name = target
1259
+ src = _fn_src(target)
1260
+ cut = len(src) - len(src.lstrip(" "))
1261
+ src2 = _strip_indent(src, cut)
1262
+ old2 = _strip_indent(old, cut)
1263
+ new2 = _strip_indent(new, cut)
1264
+ if old2 not in src2:
1265
+ raise AssertionError("变异锚点在去缩进后仍不匹配:%r" % old[:40])
1266
+ src = src2.replace(old2, new2)
1267
+ if kind == "mod":
1268
+ ns = dict(vars(owner))
1269
+ ns["__name__"] = owner.__name__
1270
+ else:
1271
+ try:
1272
+ ns = dict(vars(sys.modules[owner.__module__]))
1273
+ ns["__name__"] = owner.__module__
1274
+ except KeyError:
1275
+ ns = dict(vars(owner))
1276
+ exec(compile(src, "mp_mut.py", "exec"), ns)
1277
+ live = getattr(owner, name)
1278
+ setattr(owner, name, ns[name])
1279
+ try:
1280
+ yield
1281
+ finally:
1282
+ setattr(owner, name, live)
1283
+
1284
+
1285
+ def _run_one_mutation(item):
1286
+ """执行单条定点变异并比照红项数;返回 None(命中预期)或条目名(不符)。"""
1287
+ name, kind, owner, fname, old, new, expect = item
1288
+ _RUN[0] += 1
1289
+ try:
1290
+ with _patched((kind, owner, fname), old, new), \
1291
+ contextlib.redirect_stdout(io.StringIO()):
1292
+ fails = _run_groups()
1293
+ except SystemExit as exc: # oracle 路由 fail-closed:算「红」但报明
1294
+ fails = -1
1295
+ print(" 变异「%s」→ oracle 路由 fail-closed(exit %s),判 FAIL"
1296
+ % (name, getattr(exc, "code", "?")))
1297
+ except Exception as exc: # noqa: BLE001
1298
+ fails = -1
1299
+ print(" 变异「%s」→ 断言链抛异常 %s: %s(判 FAIL)"
1300
+ % (name, type(exc).__name__, str(exc)[:80]))
1301
+ verdict = ("命中预期" if fails == expect
1302
+ else "**红项数不符(预期 %d)**" % expect)
1303
+ print(" 变异「%s」→ 红项=%d %s" % (name, fails, verdict))
1304
+ for f in _FAIL[:6]:
1305
+ print(" 红:", f)
1306
+ if len(_FAIL) > 6:
1307
+ print(" …(余 %d 项)" % (len(_FAIL) - 6))
1308
+ return None if fails == expect else name
1309
+
1310
+
1311
+ def _mutate_mode():
1312
+ bad = []
1313
+ anchor_bad = _anchor_check()
1314
+ gap_bad = _table_integrity_check()
1315
+ if anchor_bad or gap_bad:
1316
+ for b in anchor_bad:
1317
+ print(" ANCHOR-MISS " + b)
1318
+ for b in gap_bad:
1319
+ print(" 变异表缺口:" + b)
1320
+ print("\n锚点/完整性自检:FAIL(fail-closed,exit 2)")
1321
+ return 2
1322
+ with contextlib.redirect_stdout(io.StringIO()):
1323
+ clean = _run_groups()
1324
+ print(" 未变异基线:红项=%d%s"
1325
+ % (clean, "" if clean == 0 else " ← 基线即红,变异核验无意义"))
1326
+ if clean:
1327
+ bad.append("未变异基线即失败")
1328
+ _RUN[0] = 10 ** 6 # 变异轮用独立子根(防读到上一轮盘面)
1329
+ print(" 表内条目:%d 处(编号 %s–%s;防误删自检保证无缺口、表长与声明一致)"
1330
+ % (len(_SRC_MUTATIONS), _MUTATION_IDS[0], _MUTATION_IDS[-1]))
1331
+ for item in _SRC_MUTATIONS:
1332
+ r = _run_one_mutation(item)
1333
+ if r:
1334
+ bad.append(r)
1335
+ # 防误删自检的判别力钉(不占表内编号——表长与声明恒等,见 _SELFCHECK_MUTATION)
1336
+ r = _run_one_mutation(_SELFCHECK_MUTATION)
1337
+ if r:
1338
+ bad.append(r)
1339
+ print("\n定点变异自证:%s"
1340
+ % ("PASS(每处判据都有变异钉死,且红项数逐处吻合)" if not bad
1341
+ else "FAIL —— " + "、".join(bad)))
1342
+ return 0 if not bad else 1
1343
+
1344
+
1345
+ def main() -> int:
1346
+ src = os.path.basename(os.path.abspath(__file__))
1347
+ if "--mutate" in sys.argv:
1348
+ print("!! 定点变异模式:逐个变异判据,套件应转红且红项数吻合\n")
1349
+ try:
1350
+ return _mutate_mode()
1351
+ finally:
1352
+ shutil.rmtree(_SANDBOX, ignore_errors=True)
1353
+ anchor_bad = _anchor_check()
1354
+ gap_bad = _table_integrity_check()
1355
+ if anchor_bad or gap_bad:
1356
+ for b in anchor_bad:
1357
+ print(" ANCHOR-MISS " + b)
1358
+ for b in gap_bad:
1359
+ print(" 变异表缺口:" + b)
1360
+ print("\n锚点/完整性自检:FAIL(fail-closed,exit 2)"
1361
+ "——实现/变异表改了请同步")
1362
+ return 2
1363
+ print("锚点自检:PASS(%s;不以 git HEAD 为冻结基线——oracle 每次现导出)"
1364
+ % src)
1365
+ print("变异表完整性自检:PASS(编号 %s–%s 无缺口、表长 %d = 声明——"
1366
+ "删条目即 fail-closed,F1 收口)"
1367
+ % (_MUTATION_IDS[0], _MUTATION_IDS[-1], len(_MUTATION_IDS)))
1368
+ groups = _GROUPS
1369
+ if "--no-oracle" in sys.argv:
1370
+ groups = tuple(g for g in _GROUPS if g is not g_q)
1371
+ print("(--no-oracle:跳过 oracle 对拍路由——本地调试用,不构成验收读数)")
1372
+ try:
1373
+ for g in groups:
1374
+ g()
1375
+ finally:
1376
+ _RUN[0] += 1
1377
+ print("\n不动面对拍与回滚演练守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
1378
+ if _FAIL:
1379
+ for f in _FAIL:
1380
+ print(" 红:", f)
1381
+ return 0 if not _FAIL else 1
1382
+
1383
+
1384
+ if __name__ == "__main__":
1385
+ try:
1386
+ rc = main()
1387
+ finally:
1388
+ shutil.rmtree(_SANDBOX, ignore_errors=True)
1389
+ os.environ.pop("MDCG_POLICY_FILE", None)
1390
+ if _OLD_POLICY is not None:
1391
+ os.environ["MDCG_POLICY_FILE"] = _OLD_POLICY
1392
+ os.environ.pop(_ENV_MODE, None)
1393
+ sys.exit(rc)