@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.
Files changed (48) hide show
  1. package/README.md +48 -18
  2. package/docs/README.md +1 -0
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  9. 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
  10. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  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
  12. 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
  13. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  14. package/md_cg/chain.py +47 -0
  15. package/md_cg/freshness.py +511 -0
  16. package/md_cg/generation.py +409 -0
  17. package/md_cg/hotcache.py +4 -1
  18. package/md_cg/mcp_server.py +128 -19
  19. package/md_cg/mdcg.py +509 -22
  20. package/md_cg/mdcos.py +203 -22
  21. package/md_cg/nodefile.py +74 -1
  22. package/md_cg/semantic/canonical.py +22 -0
  23. package/md_cg/semantic/unify.py +73 -22
  24. package/md_cg/semantic/unify_fixture.json +25 -0
  25. package/md_cg/sleep.py +1297 -0
  26. package/md_cg/sustain.py +146 -9
  27. package/md_cg/test_auto_defaults.py +424 -0
  28. package/md_cg/test_boundary_hit.py +410 -0
  29. package/md_cg/test_en_pipeline.py +22 -13
  30. package/md_cg/test_generation_guard.py +352 -0
  31. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  32. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  33. package/md_cg/test_n230_dirty_replay.py +375 -0
  34. package/md_cg/test_p2_six_elements.py +463 -0
  35. package/md_cg/test_p3_legacy_closure.py +433 -0
  36. package/md_cg/test_p4_freshness.py +680 -0
  37. package/md_cg/test_p8_subgraph_chain.py +14 -1
  38. package/md_cg/test_rank_parity_score_mode.py +6 -0
  39. package/md_cg/test_semantic_canonical.py +5 -3
  40. package/md_cg/test_sleep.py +611 -0
  41. package/md_cg/test_sleep_p1.py +784 -0
  42. package/md_cg/test_time_core_lint.py +968 -0
  43. package/md_cg/test_unify_default_off.py +701 -0
  44. package/md_cg/test_unify_scope.py +182 -0
  45. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  46. package/package.json +3 -2
  47. package/skills/plugin.json +1 -1
  48. 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_* 与 False/True)经 float()/bool() 落为 self 属性,ledger 为假值(默认 None)时回落 SessionLedger(cg.root),d 经 net_dir(d) 赋值,其余运行态字段初始化为 False/None/空列表/空 Event/Lock
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_* 默认值初始化——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() 化后存为实例属性;
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
- auto_heal: bool = True, d: str = None,
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 = False,
1023
+ auto_scrub: bool = AUTO_DEFAULTS["auto_scrub"],
963
1024
  evolve_interval: float = DEFAULT_EVOLVE_INTERVAL,
964
- auto_evolve: bool = False,
1025
+ auto_evolve: bool = AUTO_DEFAULTS["auto_evolve"],
965
1026
  tidy_interval: float = DEFAULT_TIDY_INTERVAL,
966
- auto_tidy: bool = False):
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=False(默认)时只盘点不落盘;
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=False`(默认)只盘点不落盘。
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())