@furongjun1999/dsh-memory 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -18
- package/docs/README.md +1 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/freshness.py +511 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/mcp_server.py +128 -19
- package/md_cg/mdcg.py +509 -22
- package/md_cg/mdcos.py +203 -22
- package/md_cg/nodefile.py +74 -1
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
package/md_cg/sustain.py
CHANGED
|
@@ -70,6 +70,31 @@ DEFAULT_HEAL_INTERVAL = 300.0 # 自愈巡检 5min
|
|
|
70
70
|
DEFAULT_SCRUB_INTERVAL = 3600.0 # 记忆自净(抽查/去污染/校准)1h
|
|
71
71
|
DEFAULT_EVOLVE_INTERVAL = 7200.0 # 演化巡检(固化/重要性候选盘点)2h;只读
|
|
72
72
|
DEFAULT_TIDY_INTERVAL = 21600.0 # 整理巡检(contextual 同构组聚合)6h
|
|
73
|
+
|
|
74
|
+
# ---- 四档 `auto_*` 缺省的**单一真源**(P0-2,2026-10-01)--------------------
|
|
75
|
+
# 为什么要有这张表:同一组缺省此前在**三处**各写一份——op 路径
|
|
76
|
+
# (mcp_server.py 的 `_sustain_call` start 分支)、env 路径(`_start_sustain`)
|
|
77
|
+
# 与 `SustainLoop.__init__` 形参。三份必然漂移,且已经漂了:`auto_tidy` 在
|
|
78
|
+
# op 路径是 `False`、在 env 路径是 `"1"`(True)——同一个 `(root,name)` 走哪条
|
|
79
|
+
# 入口得到相反的整理语义,是**对外可见的缺省不一致**。
|
|
80
|
+
# 纪律:改缺省只改这里;调用点只许经 `auto_default` / `auto_from_env` /
|
|
81
|
+
# `auto_from_args` 读取,**不得再写第二处字面量**(守卫 test_auto_defaults.py 钉死)。
|
|
82
|
+
#
|
|
83
|
+
# ⚠ 本轮**对外可见的缺省变更**(P0-2 裁决值 = 开):`auto_tidy` 由 op 路径原
|
|
84
|
+
# 字面量 `False` 收敛为 `True`,取 env 路径(生产路径:常驻 serve 自启)既有值
|
|
85
|
+
# ——该动作确定性、永不删除节点、可逆可审计(见 `_tick_tidy` 说明),op 路径的
|
|
86
|
+
# `False` 是唯一错位项。**opt-out:`MDCG_AUTO_TIDY=0`(env 路径)/显式传
|
|
87
|
+
# `auto_tidy=false`(op 路径——显式入参仍优先于本表)。**
|
|
88
|
+
AUTO_DEFAULTS = {"auto_heal": True, "auto_scrub": False,
|
|
89
|
+
"auto_evolve": False, "auto_tidy": True}
|
|
90
|
+
#: 各 `auto_*` 的 env 覆盖键(env 路径入口照此读;即各档的 opt-out 名)。
|
|
91
|
+
AUTO_ENVS = {"auto_heal": "MDCG_SUSTAIN_AUTOHEAL",
|
|
92
|
+
"auto_scrub": "MDCG_AUTO_SCRUB",
|
|
93
|
+
"auto_evolve": "MDCG_AUTO_EVOLVE",
|
|
94
|
+
"auto_tidy": "MDCG_AUTO_TIDY"}
|
|
95
|
+
#: 关断字面量——与两入口既有口径逐字一致的三写法(`0` / `false` / `False`)。
|
|
96
|
+
AUTO_OFF_VALUES = ("0", "false", "False")
|
|
97
|
+
|
|
73
98
|
DEFAULT_WARN_FACTOR = 2.5 # 2.5× 心跳间隔 → 警告
|
|
74
99
|
DEFAULT_DEAD_FACTOR = 3.5 # 3.5× → 失联
|
|
75
100
|
DEFAULT_WORKING_FACTOR = 2.0 # 任务执行中阈值 ×2
|
|
@@ -78,6 +103,39 @@ ACCESS_LOG_COMPACT_LINES = 500 # 访问日志超过该行数即折叠(否
|
|
|
78
103
|
_POLL = 0.2 # 循环轮询步长(常驻进程 CPU 可忽略)
|
|
79
104
|
|
|
80
105
|
|
|
106
|
+
# 生效条件:name 为 AUTO_DEFAULTS 的键时返回该档缺省的 bool(真源表取值,无副作用);键不存在时抛 KeyError(不做静默回落——拼错档名即为编程错误)。
|
|
107
|
+
def auto_default(name: str) -> bool:
|
|
108
|
+
"""`auto_*` 缺省的真源读取(无环境、无入参)。"""
|
|
109
|
+
return bool(AUTO_DEFAULTS[name])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# 生效条件:name 为 AUTO_DEFAULTS 的键时,从 environ(缺省 os.environ)按 AUTO_ENVS[name] 取名取值,缺键时回落「真源缺省对应的字面量」(真值→"1"、假值→"0");取值经 str() 后不属于 AUTO_OFF_VALUES 即为真。返回 bool。
|
|
113
|
+
def auto_from_env(name: str, environ=None) -> bool:
|
|
114
|
+
"""env 路径(常驻 serve 自启)的 `auto_*` 读取器。
|
|
115
|
+
|
|
116
|
+
与改动前的逐处字面量**同义**:`os.environ.get(<键>, <默认>) not in
|
|
117
|
+
("0", "false", "False")`——默认字面量由真源表推出,不再各写一份。
|
|
118
|
+
"""
|
|
119
|
+
env = os.environ if environ is None else environ
|
|
120
|
+
return str(env.get(AUTO_ENVS[name],
|
|
121
|
+
"1" if AUTO_DEFAULTS[name] else "0")) not in AUTO_OFF_VALUES
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# 生效条件:args 为 dict 且含 name 键时返回 bool(args[name])(显式传 None 亦为 False——与改动前 `bool(a.get(name, <默认>))` 逐字同义);args 非 dict 或缺该键时回落 auto_default(name)。
|
|
125
|
+
def auto_from_args(name: str, args, environ=None) -> bool:
|
|
126
|
+
"""op 路径(工具面 `sustain action=start`)的 `auto_*` 读取器。
|
|
127
|
+
|
|
128
|
+
判据是**键在不在**而不是值真假:`{"auto_tidy": False}` 与
|
|
129
|
+
`{"auto_tidy": None}` 都按「显式给了」处理(前者关、后者按 bool(None)=False
|
|
130
|
+
关),缺键才回落真源缺省——与改动前 `a.get(name, default)` 的语义一字不差。
|
|
131
|
+
`environ` 仅为签名对齐 `auto_from_env`(op 路径不读 env;保留位以免调用点
|
|
132
|
+
两边形参不一致)。
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(args, dict) and name in args:
|
|
135
|
+
return bool(args[name])
|
|
136
|
+
return auto_default(name)
|
|
137
|
+
|
|
138
|
+
|
|
81
139
|
# --------------------------------------------------------------------------
|
|
82
140
|
# 心跳
|
|
83
141
|
# --------------------------------------------------------------------------
|
|
@@ -944,7 +1002,7 @@ def watermarks(cg) -> dict:
|
|
|
944
1002
|
# 常驻循环
|
|
945
1003
|
# --------------------------------------------------------------------------
|
|
946
1004
|
|
|
947
|
-
# 生效条件:传入 cg 即构造实例并把 self.cg 指向它,name/beat_interval/heal_interval/auto_heal/scrub_interval/auto_scrub/evolve_interval/auto_evolve/tidy_interval/auto_tidy 用各默认值(DEFAULT_* 与
|
|
1005
|
+
# 生效条件:传入 cg 即构造实例并把 self.cg 指向它,name/beat_interval/heal_interval/auto_heal/scrub_interval/auto_scrub/evolve_interval/auto_evolve/tidy_interval/auto_tidy 用各默认值(DEFAULT_* 与 AUTO_DEFAULTS 真源表)经 float()/bool() 落为 self 属性,ledger 为假值(默认 None)时回落 SessionLedger(cg.root),d 经 net_dir(d) 赋值,其余运行态字段初始化为 False/None/空列表/空 Event/Lock
|
|
948
1006
|
class SustainLoop:
|
|
949
1007
|
"""常驻自维持循环:后台线程周期心跳 + 周期巡检 + 必要时自愈。
|
|
950
1008
|
|
|
@@ -952,18 +1010,31 @@ class SustainLoop:
|
|
|
952
1010
|
让对端立刻看到「正常下线」而不是「失联」。
|
|
953
1011
|
"""
|
|
954
1012
|
|
|
955
|
-
# 生效条件:传入 cg 时按 name 与各 DEFAULT_*
|
|
1013
|
+
# 生效条件:传入 cg 时按 name 与各 DEFAULT_* / AUTO_DEFAULTS 真源表默认值初始化——self.d=net_dir(d)(d 假值时回落 MDCG_SUSTAIN_DIR/~/ .mdcg/sustain)、self.ledger=ledger or SessionLedger(cg.root)(ledger 假值时新建),beat/heal/scrub/evolve/tidy 间隔 float() 化、auto_heal/auto_scrub/auto_evolve/auto_tidy bool() 化后存为实例属性;
|
|
956
1014
|
def __init__(self, cg, name: str = "md_cg", *,
|
|
957
1015
|
beat_interval: float = DEFAULT_BEAT_INTERVAL,
|
|
958
1016
|
heal_interval: float = DEFAULT_HEAL_INTERVAL,
|
|
959
|
-
|
|
1017
|
+
# 四档 `auto_*` 缺省取自**单一真源**(P0-2):本形参默认值与
|
|
1018
|
+
# 两个入口读取的是同一张表,`SustainLoop(cg)` 与
|
|
1019
|
+
# `sustain action=start` / `_start_sustain` 三面同值。
|
|
1020
|
+
auto_heal: bool = AUTO_DEFAULTS["auto_heal"], d: str = None,
|
|
960
1021
|
ledger: SessionLedger = None,
|
|
961
1022
|
scrub_interval: float = DEFAULT_SCRUB_INTERVAL,
|
|
962
|
-
auto_scrub: bool =
|
|
1023
|
+
auto_scrub: bool = AUTO_DEFAULTS["auto_scrub"],
|
|
963
1024
|
evolve_interval: float = DEFAULT_EVOLVE_INTERVAL,
|
|
964
|
-
auto_evolve: bool =
|
|
1025
|
+
auto_evolve: bool = AUTO_DEFAULTS["auto_evolve"],
|
|
965
1026
|
tidy_interval: float = DEFAULT_TIDY_INTERVAL,
|
|
966
|
-
auto_tidy: bool =
|
|
1027
|
+
auto_tidy: bool = AUTO_DEFAULTS["auto_tidy"],
|
|
1028
|
+
# 第六档:睡眠周期(§三 九步 / §4.7)。四个缺省一律取自
|
|
1029
|
+
# `md_cg/sleep.py` 的 **env 表单一真源**(SLEEP_ENV_DEFAULTS +
|
|
1030
|
+
# sleep_env 族读取器)——本处**不写第二份缺省字面量**;两个
|
|
1031
|
+
# 入口(`_start_sustain` / op 路径)同样只经那些读取器。
|
|
1032
|
+
sleep_interval: float = None,
|
|
1033
|
+
auto_sleep: bool = None,
|
|
1034
|
+
sleep_merge: str = None,
|
|
1035
|
+
sleep_window: str = None,
|
|
1036
|
+
sleep_scrub_apply: bool = None):
|
|
1037
|
+
from . import sleep as _sleep
|
|
967
1038
|
self.cg = cg
|
|
968
1039
|
self.name = name
|
|
969
1040
|
self.beat_interval = float(beat_interval)
|
|
@@ -977,6 +1048,22 @@ class SustainLoop:
|
|
|
977
1048
|
self.auto_evolve = bool(auto_evolve)
|
|
978
1049
|
self.tidy_interval = float(tidy_interval)
|
|
979
1050
|
self.auto_tidy = bool(auto_tidy)
|
|
1051
|
+
# 第六档睡眠周期:值全来自 sleep 模块的真源读取器(缺省见 §4.7 表)。
|
|
1052
|
+
self.sleep_interval = float(_sleep.sleep_interval()
|
|
1053
|
+
if sleep_interval is None
|
|
1054
|
+
else sleep_interval)
|
|
1055
|
+
self.auto_sleep = bool(_sleep.sleep_enabled() if auto_sleep is None
|
|
1056
|
+
else auto_sleep)
|
|
1057
|
+
self.sleep_merge = (_sleep.sleep_merge_mode() if sleep_merge is None
|
|
1058
|
+
else str(sleep_merge))
|
|
1059
|
+
self.sleep_window = (_sleep.sleep_window() if sleep_window is None
|
|
1060
|
+
else str(sleep_window))
|
|
1061
|
+
self.sleep_scrub_apply = bool(_sleep.sleep_scrub_apply()
|
|
1062
|
+
if sleep_scrub_apply is None
|
|
1063
|
+
else sleep_scrub_apply)
|
|
1064
|
+
self.last_sleep = None
|
|
1065
|
+
self.sleeps = []
|
|
1066
|
+
self.sleep_round = 0
|
|
980
1067
|
self.task_running = False
|
|
981
1068
|
self.beats = 0
|
|
982
1069
|
self.last_beat = None
|
|
@@ -1032,13 +1119,14 @@ class SustainLoop:
|
|
|
1032
1119
|
clear_stamp(self.name, self.d)
|
|
1033
1120
|
return self
|
|
1034
1121
|
|
|
1035
|
-
# 生效条件:self._stop 未置位期间轮询,按 beat_interval/heal_interval/scrub_interval/evolve_interval/tidy_interval 到期分别执行 beat 与 _tick_heal/_tick_scrub/_tick_evolve/_tick_tidy,各 tick 抛出的异常被吞掉不中断循环,末尾以 _stop.wait(_POLL) 休眠;
|
|
1122
|
+
# 生效条件:self._stop 未置位期间轮询,按 beat_interval/heal_interval/scrub_interval/evolve_interval/tidy_interval/**sleep_interval(第六档)** 到期分别执行 beat 与 _tick_heal/_tick_scrub/_tick_evolve/_tick_tidy/_tick_sleep,各 tick 抛出的异常被吞掉不中断循环,末尾以 _stop.wait(_POLL) 休眠;
|
|
1036
1123
|
def _run(self):
|
|
1037
1124
|
next_beat = time.time() + self.beat_interval
|
|
1038
1125
|
next_heal = time.time() + self.heal_interval
|
|
1039
1126
|
next_scrub = time.time() + self.scrub_interval
|
|
1040
1127
|
next_evolve = time.time() + self.evolve_interval
|
|
1041
1128
|
next_tidy = time.time() + self.tidy_interval
|
|
1129
|
+
next_sleep = time.time() + self.sleep_interval
|
|
1042
1130
|
while not self._stop.is_set():
|
|
1043
1131
|
now = time.time()
|
|
1044
1132
|
if now >= next_beat:
|
|
@@ -1071,13 +1159,20 @@ class SustainLoop:
|
|
|
1071
1159
|
except Exception:
|
|
1072
1160
|
pass # 整理巡检失败不中断常驻
|
|
1073
1161
|
next_tidy = now + self.tidy_interval
|
|
1162
|
+
if now >= next_sleep:
|
|
1163
|
+
try:
|
|
1164
|
+
self._tick_sleep()
|
|
1165
|
+
except Exception:
|
|
1166
|
+
pass # 睡眠周期失败不中断常驻
|
|
1167
|
+
next_sleep = now + self.sleep_interval
|
|
1074
1168
|
self._stop.wait(_POLL)
|
|
1075
1169
|
|
|
1076
|
-
# 生效条件:以 apply=self.auto_tidy 调 writelimit.tidy_contextual(self.cg, actor="sustain_tidy"),把 t/scanned/groups/members/applied_count/auto_tidy 记入 self.last_tidy 与 tidys(仅保留最近 20 条),随后调 _tick_conformance();auto_tidy
|
|
1170
|
+
# 生效条件:以 apply=self.auto_tidy 调 writelimit.tidy_contextual(self.cg, actor="sustain_tidy"),把 t/scanned/groups/members/applied_count/auto_tidy 记入 self.last_tidy 与 tidys(仅保留最近 20 条),随后调 _tick_conformance();auto_tidy 为假时只盘点不落盘;
|
|
1077
1171
|
def _tick_tidy(self):
|
|
1078
1172
|
"""整理巡检(contextual 流水治理·读侧):同构组聚合 + 成员降权。
|
|
1079
1173
|
|
|
1080
|
-
确定性动作、永不删节点;`auto_tidy
|
|
1174
|
+
确定性动作、永不删节点;`auto_tidy` 为假时只盘点不落盘。缺省值取自
|
|
1175
|
+
模块级真源 `AUTO_DEFAULTS`(P0-2;缺省为 True,opt-out 见该表注释)。
|
|
1081
1176
|
治理对象:单日批次流水(「批次247收官记忆」×163 那类同模板写入)
|
|
1082
1177
|
—— 写入侧限流(writelimit.check)拦增量,本巡检收敛存量。
|
|
1083
1178
|
"""
|
|
@@ -1109,6 +1204,41 @@ class SustainLoop:
|
|
|
1109
1204
|
self.last_conformance = {"ok": False, "verdict": "BLINDSPOT",
|
|
1110
1205
|
"error": f"{type(e).__name__}: {e}"}
|
|
1111
1206
|
|
|
1207
|
+
# 生效条件:self.sleep_round 自增 1 后以 enabled=self.auto_sleep / merge_mode=self.sleep_merge / scrub_apply=self.sleep_scrub_apply / window=self.sleep_window / round_index=self.sleep_round 调 sleep.run_cycle(self.cg),把 t/batch/round/candidates/merged/skipped/conflicts/九步名与其 skipped 明细/auto_sleep/merge_mode 记入 last_sleep 与 sleeps(保留最近 20 条);auto_sleep 为假时 run_cycle 只记账不迭代;
|
|
1208
|
+
def _tick_sleep(self):
|
|
1209
|
+
"""睡眠周期(第六档 tick):§3.1 九步显式化 + §4.4 副本迭代与周期合并。
|
|
1210
|
+
|
|
1211
|
+
**只在副本上迭代**(物化 → 影子迭代 → 对账四闸 → 语义重放 + git 合并),
|
|
1212
|
+
主库真源面在非合并阶段逐字节不变。四个开关全取 `md_cg/sleep.py` 的 §4.7
|
|
1213
|
+
env 表真源:`MDCG_SLEEP`(总开关,缺省开)、`MDCG_SLEEP_MERGE`(缺省
|
|
1214
|
+
auto=自动走四阶段,冲突仍挂起)、`MDCG_SLEEP_WINDOW`(缺省 23:00-07:00,
|
|
1215
|
+
**窗口外只记账不迭代**)、`MDCG_SLEEP_SCRUB_APPLY`(缺省 **关**——第④步
|
|
1216
|
+
缺省只在副本上盘点、不落盘)。
|
|
1217
|
+
|
|
1218
|
+
⑤权重刷新与衰减 / ⑥索引重建两步本轮是**显式 no-op 占位**(台账里标
|
|
1219
|
+
`skipped: "未接线"`),故本轮**不动检索读数**。
|
|
1220
|
+
"""
|
|
1221
|
+
from . import sleep as _sleep
|
|
1222
|
+
self.sleep_round += 1
|
|
1223
|
+
r = _sleep.run_cycle(self.cg, enabled=self.auto_sleep,
|
|
1224
|
+
merge_mode=self.sleep_merge,
|
|
1225
|
+
scrub_apply=self.sleep_scrub_apply,
|
|
1226
|
+
window=self.sleep_window,
|
|
1227
|
+
round_index=self.sleep_round)
|
|
1228
|
+
steps = list(r.get("steps") or [])
|
|
1229
|
+
rec = {"t": r.get("t"), "batch": r.get("batch"), "round": r.get("round"),
|
|
1230
|
+
"candidates": r.get("candidates"),
|
|
1231
|
+
"merged": r.get("merged"), "skipped": r.get("skipped"),
|
|
1232
|
+
"conflicts": r.get("conflicts"),
|
|
1233
|
+
"steps": [s.get("step") for s in steps],
|
|
1234
|
+
"steps_skipped": ["%s:%s" % (s.get("step"), s.get("skipped"))
|
|
1235
|
+
for s in steps if s.get("skipped")],
|
|
1236
|
+
"auto_sleep": self.auto_sleep, "merge_mode": self.sleep_merge}
|
|
1237
|
+
self.last_sleep = rec
|
|
1238
|
+
with self._lock:
|
|
1239
|
+
self.sleeps.append(rec)
|
|
1240
|
+
self.sleeps = self.sleeps[-20:]
|
|
1241
|
+
|
|
1112
1242
|
# 生效条件:恒以 evolution_candidates(self.cg) 只读盘点并记入 last_evolve 与 evolves(保留最近 20 条);仅当 self.auto_evolve 为真且 ev["importance_drift"]["n"] 为真时才额外执行 weights.recalc(self.cg, apply=True, actor="sustain_evolve"),其异常写入 rec["applied"];
|
|
1113
1243
|
def _tick_evolve(self):
|
|
1114
1244
|
"""演化巡检(G7):盘点固化/重要性候选 —— 让「有能力」变成「有驱动」。
|
|
@@ -1206,6 +1336,13 @@ class SustainLoop:
|
|
|
1206
1336
|
"evolves": self.evolves[-5:],
|
|
1207
1337
|
"last_tidy": self.last_tidy,
|
|
1208
1338
|
"tidys": self.tidys[-5:],
|
|
1339
|
+
"sleep_interval": self.sleep_interval,
|
|
1340
|
+
"auto_sleep": self.auto_sleep,
|
|
1341
|
+
"sleep_merge": self.sleep_merge,
|
|
1342
|
+
"sleep_window": self.sleep_window,
|
|
1343
|
+
"sleep_scrub_apply": self.sleep_scrub_apply,
|
|
1344
|
+
"last_sleep": self.last_sleep,
|
|
1345
|
+
"sleeps": self.sleeps[-5:],
|
|
1209
1346
|
"last_conformance": self.last_conformance,
|
|
1210
1347
|
"peers": peers(self.d),
|
|
1211
1348
|
"sessions": self.ledger.summary()}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""`auto_*` 四档缺省「单一真源」守卫(P0-2,2026-10-01)。
|
|
3
|
+
|
|
4
|
+
契约(设计稿 `docs/plans/睡眠与自迭代_功能优化设计_v0.4.md` §八 P0-2 / §4.7):
|
|
5
|
+
|
|
6
|
+
同一 `(root, name)` 的两条入口必须得到**同一组** `auto_*` 取值;
|
|
7
|
+
缺省只许有**一份定义**(`sustain.AUTO_DEFAULTS` + 三个读取器),
|
|
8
|
+
两个调用点不得再写第二处字面量。
|
|
9
|
+
|
|
10
|
+
改动前实测(探针读数,逐条引 `path:line`):
|
|
11
|
+
|
|
12
|
+
op 路径(工具面 `sustain action=start`) `auto_tidy=bool(a.get("auto_tidy", False))`
|
|
13
|
+
→ **False**(mcp_server.py:1639)
|
|
14
|
+
env 路径(常驻 serve 自启 `_start_sustain`)`auto_tidy=os.environ.get("MDCG_AUTO_TIDY","1") not in (...)`
|
|
15
|
+
→ **True**(mcp_server.py:3595-3596)
|
|
16
|
+
另三项 auto_heal/auto_scrub/auto_evolve 两入口一致(True / False / False);
|
|
17
|
+
`SustainLoop.__init__` 形参另有一份 `auto_tidy=False`(sustain.py:966)——
|
|
18
|
+
即「单一真源尚未成立,全仓 auto_tidy 缺省字面量至少两处」。
|
|
19
|
+
|
|
20
|
+
**本轮对外可见的缺省变更(裁决值 = 开)**:`auto_tidy` 缺省由 False 收敛为
|
|
21
|
+
**True**(取 env 路径既有值);opt-out = `MDCG_AUTO_TIDY=0`(env 路径)/
|
|
22
|
+
显式 `auto_tidy=false`(op 路径)。另三项**逐项未变**(G4 钉死,防「顺手改多了」)。
|
|
23
|
+
|
|
24
|
+
断言分组:
|
|
25
|
+
|
|
26
|
+
G0 隔离与基线自证——库根/状态根/辅助根/心跳目录全落守卫临时目录(绝不碰在役
|
|
27
|
+
库与 `~/.mdcg`);断言真源表与三个读取器在位。
|
|
28
|
+
G1 真源表与读取器——四项值、键集、env 键映射、关断字面量固定;`auto_default`
|
|
29
|
+
逐项一致;`auto_from_env` 的缺省字面量**由表推出**(真值→"1"、假值→"0",
|
|
30
|
+
与改动前两处字面量同义)。
|
|
31
|
+
G2 三面同源读数(端到端,真起循环)——env 路径(`_start_sustain`)、op 路径
|
|
32
|
+
(`_sustain_call` 的 start 分支)、构造面(`SustainLoop(cg)`)三面在同一档上
|
|
33
|
+
取值相同且**都等于真源表**;并验两侧 opt-out / 显式入参仍优先。
|
|
34
|
+
G3 单一真源(静态)——带引号字面量 `"MDCG_AUTO_TIDY"` 在生产源码里恰 1 处
|
|
35
|
+
(在 `AUTO_ENVS`);`mcp_server.py` 内 `a.get("auto_*"` /
|
|
36
|
+
`os.environ.get("MDCG_AUTO_*"` / `os.environ.get("MDCG_SUSTAIN_AUTOHEAL"` 零命中;
|
|
37
|
+
两入口各恰好一处读取器调用。(`md_cg/test_*.py` 不在判据面内:本守卫自己的
|
|
38
|
+
变异表就含这些字面量,属红基线工具面。)
|
|
39
|
+
G4 对外可见变更的披露面——`auto_tidy` 缺省为 True 且 op 路径无参即得 True;
|
|
40
|
+
另三项缺省与本轮前逐项相同;opt-out 名取自 `AUTO_ENVS` 而非硬编码。
|
|
41
|
+
|
|
42
|
+
运行:python -X utf8 -m md_cg.test_auto_defaults
|
|
43
|
+
python -X utf8 -m md_cg.test_auto_defaults --mutate # 定点变异自证
|
|
44
|
+
python -X utf8 -m md_cg.test_auto_defaults --mutate --list # 只列变异表
|
|
45
|
+
|
|
46
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
47
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到——实现改了却没同步本表,
|
|
48
|
+
锚点漂移必须硬失败,不得静默跳过)。
|
|
49
|
+
|
|
50
|
+
**基线纪律**:本守卫**不以 git HEAD 为基线源**——「改动前形态」由在当前工作区
|
|
51
|
+
源码上做**定点文本变异**(`_MUTATIONS`)复现,锚点漂移即退出码 2。
|
|
52
|
+
"""
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import contextlib
|
|
56
|
+
import io
|
|
57
|
+
import os
|
|
58
|
+
import shutil
|
|
59
|
+
import sys
|
|
60
|
+
import tempfile
|
|
61
|
+
import types
|
|
62
|
+
|
|
63
|
+
from . import mcp_server as MCP
|
|
64
|
+
from . import sustain
|
|
65
|
+
|
|
66
|
+
_PASS = []
|
|
67
|
+
_FAIL = []
|
|
68
|
+
_SKIP = []
|
|
69
|
+
#: 变异模式下供静态组读取的**源码文本**(rel → text);未变异时读盘。
|
|
70
|
+
_SRC = {}
|
|
71
|
+
|
|
72
|
+
_REAL_SUSTAIN = sustain
|
|
73
|
+
_REAL_MCP = MCP
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ok(cond, msg, extra=""):
|
|
77
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
78
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
79
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def skip(msg):
|
|
83
|
+
_SKIP.append(msg)
|
|
84
|
+
print(" SKIP " + msg)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _repo_root() -> str:
|
|
88
|
+
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _rel_text(rel: str) -> str:
|
|
92
|
+
"""源码文本:变异模式下取变异后的文本,否则读盘。"""
|
|
93
|
+
if rel in _SRC:
|
|
94
|
+
return _SRC[rel]
|
|
95
|
+
with open(os.path.join(_repo_root(), rel), encoding="utf-8") as f:
|
|
96
|
+
return f.read()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _md_cg_py_texts():
|
|
100
|
+
d = os.path.join(_repo_root(), "md_cg")
|
|
101
|
+
return {"md_cg/" + fn: _rel_text("md_cg/" + fn)
|
|
102
|
+
for fn in sorted(os.listdir(d)) if fn.endswith(".py")}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------- 沙箱
|
|
106
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_autodef_")
|
|
107
|
+
_SAVED = {}
|
|
108
|
+
_GEN = [0]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _sandbox_env():
|
|
112
|
+
"""把四条根全钉进守卫临时目录;原值留待还原(绝不碰在役库 / ~/.mdcg)。"""
|
|
113
|
+
for k in ("MDCG_ROOT", "MDCG_AUX_ROOT", "MDCG_STATE_ROOT", "MDCG_SUSTAIN_DIR",
|
|
114
|
+
"MDCG_SUBSTAIN", "MDCG_SUSTAIN", "MDCG_SUSTAIN_NAME",
|
|
115
|
+
"MDCG_AUTO_TIDY", "MDCG_AUTO_SCRUB", "MDCG_AUTO_EVOLVE",
|
|
116
|
+
"MDCG_SUSTAIN_AUTOHEAL"):
|
|
117
|
+
_SAVED[k] = os.environ.get(k)
|
|
118
|
+
os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cg")
|
|
119
|
+
os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "aux")
|
|
120
|
+
os.environ["MDCG_STATE_ROOT"] = os.path.join(_TMP, "state")
|
|
121
|
+
os.environ["MDCG_SUSTAIN_DIR"] = os.path.join(_TMP, "sustain")
|
|
122
|
+
os.environ["MDCG_SUBSTAIN"] = "0"
|
|
123
|
+
os.environ.pop("MDCG_SUSTAIN", None)
|
|
124
|
+
for k in ("MDCG_AUTO_TIDY", "MDCG_AUTO_SCRUB", "MDCG_AUTO_EVOLVE",
|
|
125
|
+
"MDCG_SUSTAIN_AUTOHEAL"):
|
|
126
|
+
os.environ.pop(k, None)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _restore_env():
|
|
130
|
+
for k, v in _SAVED.items():
|
|
131
|
+
if v is None:
|
|
132
|
+
os.environ.pop(k, None)
|
|
133
|
+
else:
|
|
134
|
+
os.environ[k] = v
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _cg(name="cg"):
|
|
138
|
+
"""每轮 `_run_groups` 用**全新根**:`sustain.ensure_loop` 按 (root,name) 复用
|
|
139
|
+
实例,同一根跨轮复用会让变异轮读到上一轮的循环(假绿)。"""
|
|
140
|
+
from .mdcos import MdCGOS
|
|
141
|
+
root = os.path.join(_TMP, "gen%d" % _GEN[0], name)
|
|
142
|
+
os.makedirs(root, exist_ok=True)
|
|
143
|
+
return MdCGOS(root)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------- G0
|
|
147
|
+
def g0():
|
|
148
|
+
print("== G0 隔离与基线自证 ==")
|
|
149
|
+
ok(hasattr(_REAL_SUSTAIN, "AUTO_DEFAULTS"),
|
|
150
|
+
"G0a sustain.AUTO_DEFAULTS 存在(缺省真源表)")
|
|
151
|
+
ok(all(hasattr(_REAL_SUSTAIN, n)
|
|
152
|
+
for n in ("auto_default", "auto_from_env", "auto_from_args")),
|
|
153
|
+
"G0b 三个读取器(auto_default / auto_from_env / auto_from_args)均在位")
|
|
154
|
+
ok(os.environ["MDCG_ROOT"].startswith(_TMP)
|
|
155
|
+
and os.environ["MDCG_AUX_ROOT"].startswith(_TMP),
|
|
156
|
+
"G0c 沙箱:库根与辅助根都在守卫临时目录内", os.environ["MDCG_ROOT"])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------- G1
|
|
160
|
+
_EXPECT = {"auto_heal": True, "auto_scrub": False,
|
|
161
|
+
"auto_evolve": False, "auto_tidy": True}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def g1():
|
|
165
|
+
print("== G1 真源表与读取器 ==")
|
|
166
|
+
S = globals()["sustain"]
|
|
167
|
+
ok(dict(S.AUTO_DEFAULTS) == _EXPECT,
|
|
168
|
+
"G1 真源表四项与裁决值一致(含 auto_tidy=True——本轮唯一的对外可见变更)",
|
|
169
|
+
S.AUTO_DEFAULTS)
|
|
170
|
+
ok(set(S.AUTO_ENVS) == set(S.AUTO_DEFAULTS),
|
|
171
|
+
"G1a env 键映射与真源表同键集", sorted(S.AUTO_ENVS))
|
|
172
|
+
ok(S.AUTO_ENVS["auto_tidy"] == "MDCG_AUTO_TIDY",
|
|
173
|
+
"G1b auto_tidy 的 env 键名 = MDCG_AUTO_TIDY(opt-out 名)")
|
|
174
|
+
ok(tuple(S.AUTO_OFF_VALUES) == ("0", "false", "False"),
|
|
175
|
+
"G1c 关断字面量与改动前两入口逐字一致(0 / false / False)",
|
|
176
|
+
S.AUTO_OFF_VALUES)
|
|
177
|
+
for k, v in _EXPECT.items():
|
|
178
|
+
ok(S.auto_default(k) is v,
|
|
179
|
+
"G1d auto_default(%s) 与表一致 = %r" % (k, v))
|
|
180
|
+
for k, v in _EXPECT.items():
|
|
181
|
+
ok(S.auto_from_env(k, {}) is v,
|
|
182
|
+
"G1e auto_from_env(%s) 空环境下回落真源缺省 %r(默认字面量由表推出)"
|
|
183
|
+
% (k, v))
|
|
184
|
+
for k in _EXPECT:
|
|
185
|
+
for off in ("0", "false", "False", "FALSE ", ""):
|
|
186
|
+
got = S.auto_from_env(k, {S.AUTO_ENVS[k]: off})
|
|
187
|
+
want = off not in S.AUTO_OFF_VALUES
|
|
188
|
+
ok(got is want,
|
|
189
|
+
"G1f auto_from_env(%s) 取 %r → %r(关断口径不变)" % (k, off, want),
|
|
190
|
+
got)
|
|
191
|
+
ok(S.auto_from_env("auto_tidy", {"MDCG_AUTO_TIDY": "1"}) is True,
|
|
192
|
+
"G1g 显式 1 仍为开(开态仍可达)")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------- G2
|
|
196
|
+
def g2():
|
|
197
|
+
print("== G2 三面同源读数(端到端,真起循环)==")
|
|
198
|
+
S = globals()["sustain"]
|
|
199
|
+
M = globals()["MCP"]
|
|
200
|
+
cg = _cg("cg_g2")
|
|
201
|
+
os.environ.pop("MDCG_AUTO_TIDY", None)
|
|
202
|
+
os.environ["MDCG_SUSTAIN_NAME"] = "env_edge"
|
|
203
|
+
lp = M._start_sustain(cg)
|
|
204
|
+
ok(lp is not None, "G2 env 路径确已起循环(前置)")
|
|
205
|
+
ok(lp.auto_tidy is True,
|
|
206
|
+
"G2a env 路径(_start_sustain)无 env 覆盖时 auto_tidy=True(生产路径既有值)",
|
|
207
|
+
lp.auto_tidy)
|
|
208
|
+
os.environ["MDCG_AUTO_TIDY"] = "0"
|
|
209
|
+
os.environ["MDCG_SUSTAIN_NAME"] = "env_off"
|
|
210
|
+
lp2 = M._start_sustain(cg)
|
|
211
|
+
ok(lp2 is not None and lp2.auto_tidy is False,
|
|
212
|
+
"G2b env 路径 opt-out:MDCG_AUTO_TIDY=0 → auto_tidy=False",
|
|
213
|
+
(lp2.auto_tidy if lp2 else None))
|
|
214
|
+
os.environ.pop("MDCG_AUTO_TIDY", None)
|
|
215
|
+
ti = {"beat_interval": 3600, "heal_interval": 3600, "scrub_interval": 3600,
|
|
216
|
+
"evolve_interval": 3600, "tidy_interval": 3600}
|
|
217
|
+
r = M._sustain_call(cg, dict({"action": "start", "name": "op_default"}, **ti))
|
|
218
|
+
opl = S.get_loop(cg, "op_default")
|
|
219
|
+
ok("loop" in r and opl is not None, "G2c op 路径确已起循环(前置)", r)
|
|
220
|
+
ok(opl is not None and opl.auto_tidy is True,
|
|
221
|
+
"G2d op 路径(sustain action=start)不传参时 auto_tidy=True——**本轮的缺省变更**",
|
|
222
|
+
(opl.auto_tidy if opl else None))
|
|
223
|
+
M._sustain_call(cg, dict({"action": "start", "name": "op_off",
|
|
224
|
+
"auto_tidy": False}, **ti))
|
|
225
|
+
opl2 = S.get_loop(cg, "op_off")
|
|
226
|
+
ok(opl2 is not None and opl2.auto_tidy is False,
|
|
227
|
+
"G2e op 路径显式 auto_tidy=false → False(显式入参优先于真源表)",
|
|
228
|
+
(opl2.auto_tidy if opl2 else None))
|
|
229
|
+
M._sustain_call(cg, dict({"action": "start", "name": "op_face"}, **ti))
|
|
230
|
+
env_face = S.get_loop(cg, "env_edge")
|
|
231
|
+
op_face = S.get_loop(cg, "op_face")
|
|
232
|
+
ctor_face = S.SustainLoop(cg, name="ctor_face",
|
|
233
|
+
d=os.environ["MDCG_SUSTAIN_DIR"])
|
|
234
|
+
ok(env_face is not None and op_face is not None,
|
|
235
|
+
"G2f 前置:env / op 两面循环都在册")
|
|
236
|
+
for k in _EXPECT:
|
|
237
|
+
trio = (getattr(env_face, k), getattr(op_face, k), getattr(ctor_face, k))
|
|
238
|
+
ok(trio[0] == trio[1] == trio[2] == _EXPECT[k],
|
|
239
|
+
"G2g 三面同值 %s:env/op/构造 = %r,真源 = %r" % (k, trio, _EXPECT[k]))
|
|
240
|
+
S.stop_all()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------- G3
|
|
244
|
+
def g3():
|
|
245
|
+
print("== G3 单一真源(静态)==")
|
|
246
|
+
texts = _md_cg_py_texts()
|
|
247
|
+
prod = {rel: t for rel, t in texts.items()
|
|
248
|
+
if not os.path.basename(rel).startswith("test_")}
|
|
249
|
+
quoted = sorted((rel, t.count('"MDCG_AUTO_TIDY"')) for rel, t in prod.items()
|
|
250
|
+
if t.count('"MDCG_AUTO_TIDY"'))
|
|
251
|
+
ok(quoted == [("md_cg/sustain.py", 1)],
|
|
252
|
+
"G3 带引号字面量 \"MDCG_AUTO_TIDY\" 在生产源码里恰 1 处(在 AUTO_ENVS)",
|
|
253
|
+
quoted)
|
|
254
|
+
pats = ('os.environ.get("MDCG_AUTO_TIDY"', 'a.get("auto_tidy"',
|
|
255
|
+
'a.get("auto_heal"', 'a.get("auto_scrub"', 'a.get("auto_evolve"',
|
|
256
|
+
'os.environ.get("MDCG_AUTO_SCRUB"',
|
|
257
|
+
'os.environ.get("MDCG_AUTO_EVOLVE"',
|
|
258
|
+
'os.environ.get("MDCG_SUSTAIN_AUTOHEAL"')
|
|
259
|
+
bad = [(rel, p, t.count(p)) for rel, t in prod.items()
|
|
260
|
+
for p in pats if t.count(p)]
|
|
261
|
+
ok(not bad, "G3a 生产源码里零命中「缺省字面量」形态(^ 见 detail)", bad)
|
|
262
|
+
s = _rel_text("md_cg/sustain.py")
|
|
263
|
+
ok("AUTO_DEFAULTS = {" in s, "G3b 真源表在 sustain.py 定义")
|
|
264
|
+
ok(s.count("auto_tidy: bool = AUTO_DEFAULTS[") == 1,
|
|
265
|
+
"G3c SustainLoop 形参默认值引用真源表(不写第二处字面量)")
|
|
266
|
+
m = _rel_text("md_cg/mcp_server.py")
|
|
267
|
+
ok(m.count('auto_tidy=sustain.auto_from_args("auto_tidy", a)') == 1,
|
|
268
|
+
"G3d op 路径恰一处经 auto_from_args 读 auto_tidy")
|
|
269
|
+
ok(m.count('auto_tidy=sustain.auto_from_env("auto_tidy")') == 1,
|
|
270
|
+
"G3e env 路径恰一处经 auto_from_env 读 auto_tidy")
|
|
271
|
+
ok('"auto_tidy", False' not in m and 'auto_tidy=False)' not in m,
|
|
272
|
+
"G3f mcp_server.py 内不再有 auto_tidy 的 False 缺省字面量")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------- G4
|
|
276
|
+
def g4():
|
|
277
|
+
print("== G4 变更披露面(防顺手改多了)==")
|
|
278
|
+
S = globals()["sustain"]
|
|
279
|
+
ok(S.AUTO_DEFAULTS["auto_tidy"] is True,
|
|
280
|
+
"G4 裁决值 = 开:auto_tidy 真源缺省为 True")
|
|
281
|
+
ok(S.AUTO_DEFAULTS["auto_heal"] is True
|
|
282
|
+
and S.AUTO_DEFAULTS["auto_scrub"] is False
|
|
283
|
+
and S.AUTO_DEFAULTS["auto_evolve"] is False,
|
|
284
|
+
"G4a 另三项缺省与本轮前逐项相同(heal=True / scrub=False / evolve=False)",
|
|
285
|
+
S.AUTO_DEFAULTS)
|
|
286
|
+
ok(S.AUTO_ENVS["auto_tidy"] == "MDCG_AUTO_TIDY",
|
|
287
|
+
"G4b opt-out env 名(自真源读)= MDCG_AUTO_TIDY")
|
|
288
|
+
ok(S.auto_from_args("auto_tidy", {"auto_tidy": None}) is False,
|
|
289
|
+
"G4c 显式传 None(键在)→ False,与改动前 bool(a.get(name, default)) 同义")
|
|
290
|
+
ok(S.auto_from_args("auto_tidy", {"other": 1}) is True,
|
|
291
|
+
"G4d 缺键才回落真源缺省(True)")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
_GROUPS = (g0, g1, g2, g3, g4)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------- 变异表
|
|
298
|
+
# 锚点 = (名字, 模式, rel, old, new, 预期红项数下限)。
|
|
299
|
+
# "mod" = 改**可执行实现**(exec 成模块并换进 `sys.modules`,供 `from . import` 面读到)
|
|
300
|
+
# "src" = 只改**源码文本**(静态组 G3 读 `_SRC`,不 exec)
|
|
301
|
+
# 「改动前形态」的复现即在这里:
|
|
302
|
+
# M1 把真源表里的 auto_tidy 改回 False(= 旧的 op 路径字面量)
|
|
303
|
+
# M2 让 op 路径读取器不回落真源(= 旧的「另一处字面量」)
|
|
304
|
+
# M3 把 env 路径改回旧的手写 env 读取(= 旧 env 路径形态 + 第二处字面量)
|
|
305
|
+
_MUTATIONS = (
|
|
306
|
+
("真源表 auto_tidy 改回 False(旧 op 路径值)", "mod", "md_cg/sustain.py",
|
|
307
|
+
'"auto_evolve": False, "auto_tidy": True}',
|
|
308
|
+
'"auto_evolve": False, "auto_tidy": False}', 1),
|
|
309
|
+
("op 路径读取器不回落真源(旧「另一处字面量」)", "mod", "md_cg/sustain.py",
|
|
310
|
+
" return bool(args[name])\n return auto_default(name)",
|
|
311
|
+
" return bool(args[name])\n return False", 1),
|
|
312
|
+
("env 路径改回手写 env 读取(第二处 MDCG_AUTO_TIDY 字面量)", "src",
|
|
313
|
+
"md_cg/mcp_server.py",
|
|
314
|
+
'auto_tidy=sustain.auto_from_env("auto_tidy"))',
|
|
315
|
+
'auto_tidy=os.environ.get("MDCG_AUTO_TIDY", "1") not in ("0", "false", "False"))',
|
|
316
|
+
1),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _run_groups() -> int:
|
|
321
|
+
_GEN[0] += 1
|
|
322
|
+
_PASS.clear()
|
|
323
|
+
_FAIL.clear()
|
|
324
|
+
for g in _GROUPS:
|
|
325
|
+
try:
|
|
326
|
+
g()
|
|
327
|
+
except Exception as exc: # noqa: BLE001
|
|
328
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
329
|
+
import traceback
|
|
330
|
+
print(traceback.format_exc()[-800:])
|
|
331
|
+
for mod in (globals().get("sustain"), _REAL_SUSTAIN):
|
|
332
|
+
try:
|
|
333
|
+
mod.stop_all()
|
|
334
|
+
except Exception: # noqa: BLE001
|
|
335
|
+
pass
|
|
336
|
+
return len(_FAIL)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
340
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
341
|
+
"__file__": os.path.join(_repo_root(), rel)}
|
|
342
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
343
|
+
m = types.ModuleType(name)
|
|
344
|
+
m.__dict__.update(ns)
|
|
345
|
+
return m
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
349
|
+
print("!! 定点变异自证:逐条把「单一真源」改回改动前形态,套件必须转红\n")
|
|
350
|
+
if list_only:
|
|
351
|
+
for name, kind, rel, _o, _n, exp in _MUTATIONS:
|
|
352
|
+
print(" %-46s [%s] expect_red>=%d (%s)" % (name, kind, exp, rel))
|
|
353
|
+
return 0
|
|
354
|
+
anchor_miss, bad = [], []
|
|
355
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
356
|
+
clean = _run_groups()
|
|
357
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
358
|
+
if clean:
|
|
359
|
+
bad.append("未变异基线即失败")
|
|
360
|
+
|
|
361
|
+
for name, kind, rel, old, new, expect in _MUTATIONS:
|
|
362
|
+
src = _rel_text(rel)
|
|
363
|
+
if old not in src:
|
|
364
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没同步"
|
|
365
|
+
"本表;锚点漂移不得静默)" % (name, rel))
|
|
366
|
+
anchor_miss.append(name)
|
|
367
|
+
continue
|
|
368
|
+
mut_src = src.replace(old, new, 1)
|
|
369
|
+
_SRC[rel] = mut_src
|
|
370
|
+
live = None
|
|
371
|
+
try:
|
|
372
|
+
if kind == "mod":
|
|
373
|
+
live = sys.modules["md_cg.sustain"]
|
|
374
|
+
mut = _exec_module("md_cg._autodef_mut", rel, mut_src)
|
|
375
|
+
sys.modules["md_cg.sustain"] = mut
|
|
376
|
+
globals()["sustain"] = mut
|
|
377
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
378
|
+
reds = _run_groups()
|
|
379
|
+
detail = buf.getvalue()
|
|
380
|
+
finally:
|
|
381
|
+
if live is not None:
|
|
382
|
+
sys.modules["md_cg.sustain"] = live
|
|
383
|
+
globals()["sustain"] = _REAL_SUSTAIN
|
|
384
|
+
_SRC.pop(rel, None)
|
|
385
|
+
red_lines = [l for l in detail.splitlines()
|
|
386
|
+
if l.strip().startswith("FAIL ")]
|
|
387
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
388
|
+
mark = "OK " if reds >= expect else "MISMATCH"
|
|
389
|
+
print(" %s %-46s 红项=%d 预期>=%d %s"
|
|
390
|
+
% (mark, name, reds, expect, verdict))
|
|
391
|
+
for l in red_lines[:6]:
|
|
392
|
+
print(" " + l.strip()[5:])
|
|
393
|
+
if reds < expect:
|
|
394
|
+
bad.append("%s(红=%d 预期>=%d)" % (name, reds, expect))
|
|
395
|
+
|
|
396
|
+
if anchor_miss:
|
|
397
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
398
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
399
|
+
return 2
|
|
400
|
+
print("\n定点变异自证:%s"
|
|
401
|
+
% ("PASS(每处判据都被打红)" if not bad else "FAIL —— " + "、".join(bad)))
|
|
402
|
+
return 0 if not bad else 1
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def main() -> int:
|
|
406
|
+
_sandbox_env()
|
|
407
|
+
try:
|
|
408
|
+
if "--mutate" in sys.argv:
|
|
409
|
+
return _mutate_mode("--list" in sys.argv)
|
|
410
|
+
n = _run_groups()
|
|
411
|
+
print("\nauto_* 缺省单一真源守卫:%d 通过,%d 失败,%d 跳过"
|
|
412
|
+
% (len(_PASS), n, len(_SKIP)))
|
|
413
|
+
return 0 if not n else 1
|
|
414
|
+
finally:
|
|
415
|
+
try:
|
|
416
|
+
_REAL_SUSTAIN.stop_all()
|
|
417
|
+
except Exception: # noqa: BLE001
|
|
418
|
+
pass
|
|
419
|
+
_restore_env()
|
|
420
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
if __name__ == "__main__":
|
|
424
|
+
sys.exit(main())
|