@furongjun1999/dsh-memory 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -18
- package/docs/README.md +1 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
- package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
- package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
- package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
- package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
- package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
- package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
- package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +527 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +191 -26
- package/md_cg/mdcg.py +594 -39
- package/md_cg/mdcos.py +869 -38
- package/md_cg/nodefile.py +74 -1
- package/md_cg/protect.py +79 -4
- package/md_cg/provenance.py +1 -1
- package/md_cg/review_cli.py +31 -2
- package/md_cg/rollback.py +411 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_autonomy_admission.py +1098 -0
- package/md_cg/test_autonomy_modes.py +1972 -0
- package/md_cg/test_b1_auto_id_multiproc.py +7 -0
- package/md_cg/test_b1b2_write_face.py +7 -0
- package/md_cg/test_b3_merge_keeps_content.py +7 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_i50a_half_dup_defer.py +408 -0
- package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
- package/md_cg/test_i50c_meta_passthrough.py +608 -0
- package/md_cg/test_i50d_importance_source.py +531 -0
- package/md_cg/test_i50e_readside_protection.py +656 -0
- package/md_cg/test_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -0
- package/md_cg/test_linkref.py +7 -0
- package/md_cg/test_mode_parity.py +1393 -0
- package/md_cg/test_mutation_rollback.py +805 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9_forget_protect.py +7 -0
- package/md_cg/test_p9c_dedup_hints.py +7 -0
- package/md_cg/test_policy_required_ccg.py +5 -1
- package/md_cg/test_protocol.py +7 -0
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/writepipe.py +101 -2
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""睡眠周期 git 机制守卫(P0-1:根级锁 + 影子工作树 + 独立 git 目录)。
|
|
3
|
+
|
|
4
|
+
契约(设计稿 `docs/plans/睡眠与自迭代_功能优化设计_v0.4.md` §八 P0-1 / §4.1 /
|
|
5
|
+
§4.2 / §4.4 / §4.5):
|
|
6
|
+
|
|
7
|
+
① 迭代期间主库真源面**逐字节不变**(8 个 LAYERS 目录下全部 .md 的内容哈希集合对拍);
|
|
8
|
+
② git 历史里 `_keys.json` / `_access.log` / `_index.json` / `*.tmp` / `*.lock`
|
|
9
|
+
**零命中**;版本库追踪集合 **⊆ 8 个 LAYERS 目录** 且 **⊇ 其下全部 .md**;
|
|
10
|
+
③ 两进程同时触发睡眠**只有一个拿到根级锁**(要有可判读数);锁文件落
|
|
11
|
+
`state_root` 侧而非数据根;**绝不在数据根内建 `.git`**;
|
|
12
|
+
④ 回滚**只提供 `revert`**(不提供抹历史的强推档)。
|
|
13
|
+
|
|
14
|
+
断言分组:
|
|
15
|
+
|
|
16
|
+
G0 隔离与落点——沙箱四根全落守卫临时目录;锁文件在 `state_root` 侧、**不在数据根**;
|
|
17
|
+
影子落 `state_root()/sleep/shadow`。
|
|
18
|
+
G1 静态形态(字面量面)——`sleep.py` 内 `add -A` / `add .` / `checkout` /
|
|
19
|
+
`reset --hard` **零命中**;`--git-dir=` 与 `--work-tree=` 显式形态在位;
|
|
20
|
+
`LAYERS` 自 `md_cg.mdcg` **单点导入**(对象同一,非第二份副本)。
|
|
21
|
+
G2 物化——主库真源面哈希集合前后**逐字节相同**;影子挂上且有 `.git`;
|
|
22
|
+
数据根内**无 `.git`**(目录与文件都无);独立 git 目录在 `state_root` 侧;
|
|
23
|
+
根里planted 的密钥/临时/锁件**不在追踪集合**里;两条机械判据全绿。
|
|
24
|
+
G3 对账 + 提交(在**影子**上)——提交确实发生;主库真源面**仍逐字节不变**;
|
|
25
|
+
影子里的新增 .md 入册;影子里 planted 的密钥/临时/锁件**不入册**
|
|
26
|
+
(历史判据仍零命中)。
|
|
27
|
+
G4 合并——`--no-ff` 合回 main;主库**只多出应写的那一个 .md**(根里的 planted
|
|
28
|
+
非白名单件原样不动、仍不在追踪集合);两条机械判据仍全绿。
|
|
29
|
+
G5 回滚——`revert` 出一个反向提交:主库真源面回到合并前;**历史不丢**
|
|
30
|
+
(合并提交与被还原路径仍在 `log --all --name-only` 里)。
|
|
31
|
+
G6 根级锁跨进程唯一——父进程持锁时另起**子进程**调 `materialize` 必须
|
|
32
|
+
`busy=True` 且不取得锁;父进程释放后同一子进程脚本转 `ok=True`。
|
|
33
|
+
G7 判据自身有判别力(正对照)——改动一个 .md 后 `face_delta` 必须报 changed;
|
|
34
|
+
给根里加一个非白名单 .md 之外的件不改变判据(负对照)。
|
|
35
|
+
|
|
36
|
+
运行:python -X utf8 -m md_cg.test_sleep
|
|
37
|
+
python -X utf8 -m md_cg.test_sleep --mutate # 定点变异自证
|
|
38
|
+
python -X utf8 -m md_cg.test_sleep --mutate --list # 只列变异表
|
|
39
|
+
|
|
40
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
41
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到)。
|
|
42
|
+
|
|
43
|
+
**基线纪律**:不以 git HEAD 为基线源——「改动前形态」由在当前工作区源码上做
|
|
44
|
+
**定点文本变异**(`_MUTATIONS`)复现,锚点漂移即退出码 2。
|
|
45
|
+
"""
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import contextlib
|
|
49
|
+
import io
|
|
50
|
+
import json
|
|
51
|
+
import os
|
|
52
|
+
import shutil
|
|
53
|
+
import subprocess
|
|
54
|
+
import sys
|
|
55
|
+
import tempfile
|
|
56
|
+
import types
|
|
57
|
+
|
|
58
|
+
from . import mdcg as M
|
|
59
|
+
from . import sleep as SL
|
|
60
|
+
from .fsutil import FileLock
|
|
61
|
+
|
|
62
|
+
_PASS = []
|
|
63
|
+
_FAIL = []
|
|
64
|
+
_SKIP = []
|
|
65
|
+
_SRC = {}
|
|
66
|
+
#: 变异核验上下文:{"mutsrc": <变异源码落盘路径>}——G6 的子进程据此载入变异模块
|
|
67
|
+
#: (子进程读真源码,不共享父进程内存里的变异;不给它这份落盘件,跨进程那条
|
|
68
|
+
#: 断言就对「不校验锁」的变异失明——本机实测过一次)。
|
|
69
|
+
_MUT_CTX = {}
|
|
70
|
+
_GEN = [0]
|
|
71
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_sleep_")
|
|
72
|
+
_SAVED = {}
|
|
73
|
+
_REAL_SL = SL
|
|
74
|
+
|
|
75
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
76
|
+
|
|
77
|
+
#: 安放在数据根里的**非白名单件**(§4.5 判据一逐一覆盖):密钥面 / 运行态 /
|
|
78
|
+
#: 派生面 / 临时件 / 锁件。
|
|
79
|
+
DECOYS = {"_keys.json": '{"k":"v"}',
|
|
80
|
+
"_access.log": "a\tb\n",
|
|
81
|
+
"_index.json": '{"schema":1}',
|
|
82
|
+
"junk.tmp": "tmp",
|
|
83
|
+
"other.lock": "lock"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ok(cond, msg, extra=""):
|
|
87
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
88
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
89
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _rel_text(rel: str) -> str:
|
|
93
|
+
if rel in _SRC:
|
|
94
|
+
return _SRC[rel]
|
|
95
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
96
|
+
return f.read()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _sandbox_env():
|
|
100
|
+
for k in ("MDCG_ROOT", "MDCG_AUX_ROOT", "MDCG_STATE_ROOT",
|
|
101
|
+
"MDCG_SLEEP_GITDIR", "MDCG_SLEEP_SHADOW"):
|
|
102
|
+
_SAVED[k] = os.environ.get(k)
|
|
103
|
+
os.environ.pop(k, None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _restore_env():
|
|
107
|
+
for k, v in _SAVED.items():
|
|
108
|
+
if v is None:
|
|
109
|
+
os.environ.pop(k, None)
|
|
110
|
+
else:
|
|
111
|
+
os.environ[k] = v
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _fixture():
|
|
115
|
+
"""建一个沙箱数据面:8 个 LAYERS 目录(其中 7 个为空——专门压 pathspec 白名单
|
|
116
|
+
必须滤空目录这条)+ 若干 .md + 五个非白名单件。返回 (gen 根, 数据根, 状态根)。"""
|
|
117
|
+
_GEN[0] += 1
|
|
118
|
+
gen = os.path.join(_TMP, "gen%d" % _GEN[0])
|
|
119
|
+
root = os.path.join(gen, "data", "mdcg")
|
|
120
|
+
state = os.path.join(gen, "state")
|
|
121
|
+
for L in M.LAYERS:
|
|
122
|
+
os.makedirs(os.path.join(root, L), exist_ok=True)
|
|
123
|
+
for nid, layer in (("n_a", "knowledge"), ("n_b", "knowledge"),
|
|
124
|
+
("n_c", "contextual")):
|
|
125
|
+
p = os.path.join(root, layer, nid + ".md")
|
|
126
|
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
|
127
|
+
with open(p, "w", encoding="utf-8", newline="\n") as f:
|
|
128
|
+
f.write("# 功能名:%s\n# 生效条件:全时窗\n# 子功能:s\n# 执行:e\n"
|
|
129
|
+
"# 验证方式:test\n# 不适用条件:无\n" % nid)
|
|
130
|
+
for name, body in DECOYS.items():
|
|
131
|
+
with open(os.path.join(root, name), "w", encoding="utf-8", newline="\n") as f:
|
|
132
|
+
f.write(body)
|
|
133
|
+
os.makedirs(state, exist_ok=True)
|
|
134
|
+
os.environ["MDCG_STATE_ROOT"] = state
|
|
135
|
+
os.environ["MDCG_ROOT"] = root
|
|
136
|
+
os.environ["MDCG_AUX_ROOT"] = os.path.join(gen, "aux")
|
|
137
|
+
return gen, root, state
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------- G0
|
|
141
|
+
def g0():
|
|
142
|
+
print("== G0 隔离与落点 ==")
|
|
143
|
+
S = globals()["SL"]
|
|
144
|
+
gen, root, state = _fixture()
|
|
145
|
+
lp = S.lock_path()
|
|
146
|
+
ok(lp == os.path.join(state, "sleep", "sleep"),
|
|
147
|
+
"G0a 锁文件在 state_root 侧:<state_root>/sleep/sleep(.lock)", lp)
|
|
148
|
+
ok(not lp.startswith(root + os.sep),
|
|
149
|
+
"G0b 锁文件**不在数据根内**(否则会进真源面观测面)", lp)
|
|
150
|
+
ok(S.shadow_dir() == os.path.join(state, "sleep", "shadow"),
|
|
151
|
+
"G0c 影子落 state_root()/sleep/shadow(§4.7 缺省)", S.shadow_dir())
|
|
152
|
+
ok(S.git_dir() == os.path.join(state, "sleep", "lib.git"),
|
|
153
|
+
"G0d 独立 git 目录落 state_root()/sleep/lib.git", S.git_dir())
|
|
154
|
+
ok(S.SLEEP_DIRNAME == "sleep" and S.GITDIR_DIRNAME == "lib.git"
|
|
155
|
+
and S.SHADOW_DIRNAME == "shadow",
|
|
156
|
+
"G0e 三个落点常量与 §4.1 的布局逐字一致")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------- G1
|
|
160
|
+
def g1():
|
|
161
|
+
print("== G1 静态形态(字面量面)==")
|
|
162
|
+
s = _rel_text("md_cg/sleep.py")
|
|
163
|
+
for lit in ("add -A", "add .", "checkout", "reset --hard"):
|
|
164
|
+
ok(s.count(lit) == 0,
|
|
165
|
+
"G1 `%s` 在 sleep.py 内零命中(不含注释里的引述)" % lit,
|
|
166
|
+
s.count(lit))
|
|
167
|
+
ok(s.count("--git-dir=") >= 1 and s.count("--work-tree=") >= 1,
|
|
168
|
+
"G1a git 调用一律 --git-dir + --work-tree 显式形态",
|
|
169
|
+
(s.count("--git-dir="), s.count("--work-tree=")))
|
|
170
|
+
ok("from .mdcg import LAYERS" in s,
|
|
171
|
+
"G1b LAYERS 自 md_cg.mdcg **单点导入**(不硬编码第二份白名单)")
|
|
172
|
+
import re as _re
|
|
173
|
+
ok(_re.search(r'\["git", "--git-dir=" \+ git_dir_path, "--work-tree=" \+ work_tree\]', s),
|
|
174
|
+
"G1c 唯一 git 出口的 argv 形态逐字可读(--git-dir / --work-tree 并列)")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------- G2
|
|
178
|
+
def _paths(root):
|
|
179
|
+
return globals()["SL"].source_face_hashes(root)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def g2():
|
|
183
|
+
print("== G2 物化(持根级锁)==")
|
|
184
|
+
S = globals()["SL"]
|
|
185
|
+
gen, root, state = _fixture()
|
|
186
|
+
G = os.path.join(state, "sleep", "lib.git")
|
|
187
|
+
SH = os.path.join(state, "sleep", "shadow")
|
|
188
|
+
before = S.source_face_hashes(root)
|
|
189
|
+
ok(len(before) == 3, "G2 前置:真源面 3 个 .md", sorted(before))
|
|
190
|
+
m = S.materialize(root=root, git_dir_path=G, shadow=SH, ts="G2T1")
|
|
191
|
+
ok(m.get("ok") is True,
|
|
192
|
+
"G2a 物化成功(ok=True)", {k: m.get(k) for k in
|
|
193
|
+
("ok", "busy", "error", "hint")})
|
|
194
|
+
ok(m.get("busy") is False and m.get("lock_held") is True,
|
|
195
|
+
"G2b 物化在**已取得**根级锁下完成(lock_held=True)", m.get("lock"))
|
|
196
|
+
ok(m["source_face_delta"] == {"added": [], "removed": [], "changed": []},
|
|
197
|
+
"G2c 物化全程**主库真源面逐字节不变**(内容哈希集合三键全空)",
|
|
198
|
+
m["source_face_delta"])
|
|
199
|
+
ok(S.source_face_hashes(root) == before,
|
|
200
|
+
"G2d 物化后重算真源面哈希与物化前**逐位相同**")
|
|
201
|
+
ok(os.path.isdir(SH) and os.path.exists(os.path.join(SH, ".git")),
|
|
202
|
+
"G2e 影子已挂上(git worktree,含 .git 文件)", SH)
|
|
203
|
+
wg = S.worktree_git_dir(SH)
|
|
204
|
+
ok(wg.startswith(os.path.join(G, "worktrees")) and os.path.isdir(wg),
|
|
205
|
+
"G2e2 影子有**专属** gitdir(<lib.git>/worktrees/<名>)——对账提交靠它把"
|
|
206
|
+
"提交落到 sleep 分支而不是 main", wg)
|
|
207
|
+
ok(not os.path.exists(os.path.join(root, ".git")),
|
|
208
|
+
"G2f **数据根内无 .git**(目录与文件都没有)")
|
|
209
|
+
ok(os.path.isdir(G) and G.startswith(state),
|
|
210
|
+
"G2g 版本库在 state_root 侧(独立 git 目录)", G)
|
|
211
|
+
ok(m["branch"].startswith("sleep/"),
|
|
212
|
+
"G2h 影子分支名形如 sleep/<时间戳>", m["branch"])
|
|
213
|
+
ok(S.pathspecs(root) == ["knowledge", "contextual"],
|
|
214
|
+
"G2i pathspec 白名单 = 盘上非空的 LAYERS 目录(空目录被滤掉)",
|
|
215
|
+
S.pathspecs(root))
|
|
216
|
+
# 两条机械判据
|
|
217
|
+
hi = S.audit_history(root, git_dir_path=G)
|
|
218
|
+
tr = S.audit_tracking(root, git_dir_path=G)
|
|
219
|
+
ok(hi["ok"] and not hi["hits"],
|
|
220
|
+
"G2j 判据一:历史里 _keys.json/_access.log/_index.json/*.tmp/*.lock 零命中",
|
|
221
|
+
hi["hits"])
|
|
222
|
+
ok(tr["ok"] and not tr["outside_layers"] and not tr["missing_md"],
|
|
223
|
+
"G2k 判据二:追踪集合 ⊆ LAYERS 目录 且 ⊇ 其下全部 .md",
|
|
224
|
+
{"outside": tr["outside_layers"], "missing": tr["missing_md"]})
|
|
225
|
+
# 安放在根里的非白名单件仍原样在盘上、且不在追踪集合
|
|
226
|
+
for name in DECOYS:
|
|
227
|
+
ok(os.path.exists(os.path.join(root, name)),
|
|
228
|
+
"G2l 非白名单件 %s 仍在数据根(物化不搬它)" % name)
|
|
229
|
+
tracked = S.tracked_paths(root, git_dir_path=G)
|
|
230
|
+
ok(not any(p.rsplit("/", 1)[-1] in DECOYS for p in tracked),
|
|
231
|
+
"G2m 非白名单件**不在**追踪集合里", tracked)
|
|
232
|
+
ok(sorted(tracked) == sorted(before.keys()),
|
|
233
|
+
"G2n 追踪集合 = 真源面 .md 全集(刚好相等)",
|
|
234
|
+
(sorted(tracked), sorted(before)))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------- G3
|
|
238
|
+
def g3():
|
|
239
|
+
print("== G3 对账 + 提交(在影子上)==")
|
|
240
|
+
S = globals()["SL"]
|
|
241
|
+
gen, root, state = _fixture()
|
|
242
|
+
G = os.path.join(state, "sleep", "lib.git")
|
|
243
|
+
SH = os.path.join(state, "sleep", "shadow")
|
|
244
|
+
S.materialize(root=root, git_dir_path=G, shadow=SH, ts="G3T1")
|
|
245
|
+
# 模拟 P1 在影子上的迭代改动 + 影子里也安放非白名单件
|
|
246
|
+
newrel = "knowledge/n_new.md"
|
|
247
|
+
with open(os.path.join(SH, "knowledge", "n_new.md"), "w",
|
|
248
|
+
encoding="utf-8", newline="\n") as f:
|
|
249
|
+
f.write("# 功能名:n_new\n# 生效条件:全时窗\n# 子功能:s\n# 执行:e\n"
|
|
250
|
+
"# 验证方式:test\n# 不适用条件:无\n")
|
|
251
|
+
for name, body in DECOYS.items():
|
|
252
|
+
with open(os.path.join(SH, name), "w", encoding="utf-8", newline="\n") as f:
|
|
253
|
+
f.write(body)
|
|
254
|
+
before_root = S.source_face_hashes(root)
|
|
255
|
+
rc = S.reconcile_and_commit(root=root, git_dir_path=G, shadow=SH)
|
|
256
|
+
ok(rc.get("ok") is True and rc.get("committed") is True,
|
|
257
|
+
"G3 影子上的改动被提交(一次调用 = 一个提交)",
|
|
258
|
+
{k: rc.get(k) for k in ("ok", "committed", "reason", "error")})
|
|
259
|
+
ok(rc["source_face_delta"] == {"added": [], "removed": [], "changed": []},
|
|
260
|
+
"G3a 提交阶段**主库真源面仍逐字节不变**(迭代段不碰主库)",
|
|
261
|
+
rc["source_face_delta"])
|
|
262
|
+
ok(S.source_face_hashes(root) == before_root,
|
|
263
|
+
"G3b 提交后重算主库真源面哈希与之前逐位相同")
|
|
264
|
+
wg = rc["worktree_git_dir"]
|
|
265
|
+
sh_tracked = S.tracked_paths(SH, git_dir_path=wg)
|
|
266
|
+
ok(newrel in sh_tracked, "G3c 影子里的新 .md 已入册(白名单内)", sh_tracked)
|
|
267
|
+
ok(not any(p.rsplit("/", 1)[-1] in DECOYS for p in sh_tracked),
|
|
268
|
+
"G3d 影子里安放的非白名单件**不入册**(白名单是唯一入口)", sh_tracked)
|
|
269
|
+
hi = S.audit_history(root, git_dir_path=G)
|
|
270
|
+
ok(hi["ok"], "G3e 提交后历史判据仍零命中(影子里的密钥/锁/临时件进不去)",
|
|
271
|
+
hi["hits"])
|
|
272
|
+
cur = SL._git_ok(G, root, "rev-parse", "--abbrev-ref", "HEAD").strip()
|
|
273
|
+
ok(cur == "main", "G3f 主库工作树 HEAD 仍在 main(物化/提交不动主线)", cur)
|
|
274
|
+
# 关键:提交必须落在 sleep 分支上,main 仍在基线(否则「影子迭代 + 周期合并」
|
|
275
|
+
# 变成「直接改主线」——本机实测过这个错法:--git-dir 取主那份即落到 main)
|
|
276
|
+
n_main = SL._git_ok(G, root, "rev-list", "--count", "main").strip()
|
|
277
|
+
n_br = SL._git_ok(G, root, "rev-list", "--count", rc["branch"]).strip()
|
|
278
|
+
ok(n_main == "1" and n_br == "2",
|
|
279
|
+
"G3h 提交落在 sleep 分支(main 仍 = 基线 1 个提交;分支 = 2)",
|
|
280
|
+
(n_main, n_br, rc["branch"]))
|
|
281
|
+
ok(SL._git_ok(G, root, "rev-parse", "main").strip()
|
|
282
|
+
== SL._git_ok(G, root, "rev-parse", "main~0").strip()
|
|
283
|
+
and SL._git_ok(G, root, "rev-parse", rc["branch"]).strip()
|
|
284
|
+
!= SL._git_ok(G, root, "rev-parse", "main").strip(),
|
|
285
|
+
"G3i sleep 分支与 main 指向不同提交(分支真的前进了)")
|
|
286
|
+
# 无改动再来一次 → 零提交(Δ 为空整轮跳过)
|
|
287
|
+
rc2 = S.reconcile_and_commit(root=root, git_dir_path=G, shadow=SH)
|
|
288
|
+
ok(rc2.get("committed") is False and "Δ 为空" in (rc2.get("reason") or ""),
|
|
289
|
+
"G3g 重复对账=零提交(Δ 为空则整轮跳过)", rc2.get("reason"))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ---------------------------------------------------------------- G4
|
|
293
|
+
def g4():
|
|
294
|
+
print("== G4 合并(持根级锁,写主库的只有应写那部分)==")
|
|
295
|
+
S = globals()["SL"]
|
|
296
|
+
gen, root, state = _fixture()
|
|
297
|
+
G = os.path.join(state, "sleep", "lib.git")
|
|
298
|
+
SH = os.path.join(state, "sleep", "shadow")
|
|
299
|
+
m = S.materialize(root=root, git_dir_path=G, shadow=SH, ts="G4T1")
|
|
300
|
+
branch = m["branch"]
|
|
301
|
+
body = ("# 功能名:n_merged\n# 生效条件:全时窗\n# 子功能:s\n# 执行:e\n"
|
|
302
|
+
"# 验证方式:test\n# 不适用条件:无\n")
|
|
303
|
+
with open(os.path.join(SH, "knowledge", "n_merged.md"), "w",
|
|
304
|
+
encoding="utf-8", newline="\n") as f:
|
|
305
|
+
f.write(body)
|
|
306
|
+
S.reconcile_and_commit(root=root, git_dir_path=G, shadow=SH)
|
|
307
|
+
before = S.source_face_hashes(root)
|
|
308
|
+
mg = S.merge(root=root, git_dir_path=G, branch=branch)
|
|
309
|
+
ok(mg.get("ok") is True, "G4 合并成功(--no-ff 合回 main)",
|
|
310
|
+
{k: mg.get(k) for k in ("ok", "error", "conflict", "hint")})
|
|
311
|
+
ok(mg["main_written"] == ["knowledge/n_merged.md"],
|
|
312
|
+
"G4a 主库**只多出应写的那一个 .md**(main_written 逐项)",
|
|
313
|
+
mg["main_written"])
|
|
314
|
+
after = S.source_face_hashes(root)
|
|
315
|
+
ok(after.get("knowledge/n_merged.md") is not None
|
|
316
|
+
and all(after.get(k) == v for k, v in before.items())
|
|
317
|
+
and len(after) == len(before) + 1,
|
|
318
|
+
"G4b 合并后主库真源面 = 合并前 + 恰好一个新件(其余逐位不变)",
|
|
319
|
+
S.face_delta(before, after))
|
|
320
|
+
got = open(os.path.join(root, "knowledge", "n_merged.md"),
|
|
321
|
+
encoding="utf-8").read()
|
|
322
|
+
ok(got == body, "G4c 合并进来的内容与影子里写下的逐字节相同")
|
|
323
|
+
for name, want in DECOYS.items():
|
|
324
|
+
p = os.path.join(root, name)
|
|
325
|
+
ok(os.path.exists(p) and open(p, encoding="utf-8").read() == want,
|
|
326
|
+
"G4d 数据根里的非白名单件 %s 原样未被合并触碰" % name)
|
|
327
|
+
tracked = S.tracked_paths(root, git_dir_path=G)
|
|
328
|
+
ok(not any(p.rsplit("/", 1)[-1] in DECOYS for p in tracked)
|
|
329
|
+
and set(tracked) == set(after),
|
|
330
|
+
"G4e 合并后追踪集合 = 真源面 .md 全集(非白名单件仍不可达)",
|
|
331
|
+
(sorted(set(tracked) - set(after)), sorted(set(after) - set(tracked))))
|
|
332
|
+
hi = S.audit_history(root, git_dir_path=G)
|
|
333
|
+
tr = S.audit_tracking(root, git_dir_path=G)
|
|
334
|
+
ok(hi["ok"] and tr["ok"],
|
|
335
|
+
"G4f 合并后两条机械判据仍全绿",
|
|
336
|
+
{"history": hi["hits"], "outside": tr["outside_layers"],
|
|
337
|
+
"missing": tr["missing_md"]})
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------- G5
|
|
341
|
+
def g5():
|
|
342
|
+
print("== G5 回滚(只 revert,历史不丢)==")
|
|
343
|
+
S = globals()["SL"]
|
|
344
|
+
gen, root, state = _fixture()
|
|
345
|
+
G = os.path.join(state, "sleep", "lib.git")
|
|
346
|
+
SH = os.path.join(state, "sleep", "shadow")
|
|
347
|
+
m = S.materialize(root=root, git_dir_path=G, shadow=SH, ts="G5T1")
|
|
348
|
+
with open(os.path.join(SH, "knowledge", "n_rev.md"), "w",
|
|
349
|
+
encoding="utf-8", newline="\n") as f:
|
|
350
|
+
f.write("# 功能名:n_rev\n# 生效条件:全时窗\n# 子功能:s\n# 执行:e\n"
|
|
351
|
+
"# 验证方式:test\n# 不适用条件:无\n")
|
|
352
|
+
S.reconcile_and_commit(root=root, git_dir_path=G, shadow=SH)
|
|
353
|
+
before_merge = S.source_face_hashes(root)
|
|
354
|
+
mg = S.merge(root=root, git_dir_path=G, branch=m["branch"])
|
|
355
|
+
ok(mg.get("ok") is True, "G5 前置:合并成功")
|
|
356
|
+
hist_before = set(S.history_paths(root, git_dir_path=G))
|
|
357
|
+
rv = S.revert(mg["commit"], root, git_dir_path=G)
|
|
358
|
+
ok(rv.get("ok") is True, "G5a revert 成功(反向提交)",
|
|
359
|
+
{k: rv.get(k) for k in ("ok", "error", "conflict", "hint")})
|
|
360
|
+
ok(rv.get("merge_commit") is True,
|
|
361
|
+
"G5b merge commit 自动 -m 1(撤销该次合并引入的变更)", rv.get("merge_commit"))
|
|
362
|
+
ok(S.source_face_hashes(root) == before_merge,
|
|
363
|
+
"G5c 回滚后主库真源面 == 合并前(逐位)",
|
|
364
|
+
S.face_delta(before_merge, S.source_face_hashes(root)))
|
|
365
|
+
hist_after = set(S.history_paths(root, git_dir_path=G))
|
|
366
|
+
ok("knowledge/n_rev.md" in hist_after,
|
|
367
|
+
"G5d **历史不丢**:被回滚的路径仍在 log --all --name-only 里",
|
|
368
|
+
sorted(hist_after - hist_before))
|
|
369
|
+
ok(hist_after >= hist_before,
|
|
370
|
+
"G5e 历史是 append-only:回滚只增不减(revert 自身也进历史)")
|
|
371
|
+
n_commits = int(SL._git_ok(G, root, "rev-list", "--count", "HEAD").strip())
|
|
372
|
+
ok(n_commits >= 4,
|
|
373
|
+
"G5f 提交数 >= 4(基线 / 对账 / 合并 / 回滚各一个 —— 实测 %s)" % n_commits)
|
|
374
|
+
ok(S.audit_history(root, git_dir_path=G)["ok"],
|
|
375
|
+
"G5g 回滚后历史判据仍零命中")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------- G6
|
|
379
|
+
_CHILD_SRC = '''# -*- coding: utf-8 -*-
|
|
380
|
+
import json, sys, types
|
|
381
|
+
sys.path.insert(0, r"%(repo)s")
|
|
382
|
+
import md_cg # 真包(datapath/fsutil/mdcg 走真源)
|
|
383
|
+
_mut = r"%(mutsrc)s"
|
|
384
|
+
if _mut:
|
|
385
|
+
# 变异核验:把(变异后的)机制源码当 md_cg.sleep 载入——G6 才吃得下变异
|
|
386
|
+
with open(_mut, encoding="utf-8") as _f:
|
|
387
|
+
_src = _f.read()
|
|
388
|
+
_ns = {"__name__": "md_cg.sleep", "__package__": "md_cg", "__file__": _mut}
|
|
389
|
+
exec(compile(_src, "sleep_mut.py", "exec"), _ns)
|
|
390
|
+
_m = types.ModuleType("md_cg.sleep")
|
|
391
|
+
_m.__dict__.update(_ns)
|
|
392
|
+
sys.modules["md_cg.sleep"] = _m
|
|
393
|
+
sleep = _m
|
|
394
|
+
else:
|
|
395
|
+
from md_cg import sleep
|
|
396
|
+
r = sleep.materialize(root=r"%(root)s", git_dir_path=r"%(git)s",
|
|
397
|
+
shadow=r"%(shadow)s", ts="CHILD", timeout=0.6)
|
|
398
|
+
print("RESULT " + json.dumps({k: r.get(k) for k in
|
|
399
|
+
("ok", "busy", "lock_held", "phase", "lock", "reason")},
|
|
400
|
+
ensure_ascii=False))
|
|
401
|
+
'''
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _run_child(gen, root, G, SH, mutsrc: str = ""):
|
|
405
|
+
p = os.path.join(gen, "child_probe.py")
|
|
406
|
+
with open(p, "w", encoding="utf-8", newline="\n") as f:
|
|
407
|
+
f.write(_CHILD_SRC % {"repo": _REPO.replace("\\", "\\\\"),
|
|
408
|
+
"root": root.replace("\\", "\\\\"),
|
|
409
|
+
"git": G.replace("\\", "\\\\"),
|
|
410
|
+
"shadow": SH.replace("\\", "\\\\"),
|
|
411
|
+
"mutsrc": mutsrc.replace("\\", "\\\\")})
|
|
412
|
+
env = dict(os.environ, PYTHONUTF8="1")
|
|
413
|
+
r = subprocess.run([sys.executable, "-X", "utf8", p], capture_output=True,
|
|
414
|
+
text=True, encoding="utf-8", errors="replace",
|
|
415
|
+
env=env, shell=False)
|
|
416
|
+
for line in (r.stdout or "").splitlines():
|
|
417
|
+
if line.startswith("RESULT "):
|
|
418
|
+
return json.loads(line[len("RESULT "):]), r
|
|
419
|
+
return None, r
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def g6():
|
|
423
|
+
print("== G6 根级锁跨进程唯一(两进程同时触发只有一个能进)==")
|
|
424
|
+
S = globals()["SL"]
|
|
425
|
+
gen, root, state = _fixture()
|
|
426
|
+
G = os.path.join(state, "sleep", "lib.git")
|
|
427
|
+
SH = os.path.join(state, "sleep", "shadow")
|
|
428
|
+
lp = S.lock_path()
|
|
429
|
+
mutsrc = _MUT_CTX.get("mutsrc", "")
|
|
430
|
+
with FileLock(lp, timeout=5.0, strict=False) as lk:
|
|
431
|
+
ok(lk.acquired is True, "G6 前置:父进程已持根级锁", lp)
|
|
432
|
+
got, r = _run_child(gen, root, G, SH, mutsrc)
|
|
433
|
+
ok(got is not None,
|
|
434
|
+
"G6a 子进程跑通(前置:能取到读数)", (r.stdout or "")[-200:])
|
|
435
|
+
if got:
|
|
436
|
+
ok(got["busy"] is True and got["lock_held"] is False
|
|
437
|
+
and got["ok"] is False,
|
|
438
|
+
"G6b 父进程持锁期间,子进程 materialize → busy=True(拿不到锁)",
|
|
439
|
+
got)
|
|
440
|
+
ok(os.path.exists(lp + ".lock"),
|
|
441
|
+
"G6c 锁件实际落点 = <state_root>/sleep/sleep.lock", lp + ".lock")
|
|
442
|
+
ok(not (lp + ".lock").startswith(root + os.sep),
|
|
443
|
+
"G6d 锁件不在数据根内")
|
|
444
|
+
# 释放后同一子进程脚本必须能进
|
|
445
|
+
got2, r2 = _run_child(gen, root, G, SH, mutsrc)
|
|
446
|
+
ok(got2 is not None and got2["busy"] is False and got2["ok"] is True,
|
|
447
|
+
"G6e 父进程释放后,同一子进程转 ok=True(锁可达、非死锁)",
|
|
448
|
+
got2 if got2 else (r2.stdout or "")[-200:])
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ---------------------------------------------------------------- G7
|
|
452
|
+
def g7():
|
|
453
|
+
print("== G7 判据自身的判别力(正/负对照)==")
|
|
454
|
+
S = globals()["SL"]
|
|
455
|
+
gen, root, state = _fixture()
|
|
456
|
+
a = S.source_face_hashes(root)
|
|
457
|
+
# 正对照:改一个 .md 的正文 → changed 必报
|
|
458
|
+
p = os.path.join(root, "knowledge", "n_a.md")
|
|
459
|
+
with open(p, "a", encoding="utf-8", newline="\n") as f:
|
|
460
|
+
f.write("补一行\n")
|
|
461
|
+
b = S.source_face_hashes(root)
|
|
462
|
+
d = S.face_delta(a, b)
|
|
463
|
+
ok(d["changed"] == ["knowledge/n_a.md"] and not d["added"] and not d["removed"],
|
|
464
|
+
"G7 正对照:真源面内容改动被 face_delta 报为 changed", d)
|
|
465
|
+
# 负对照:动一个非白名单件 → 真源面哈希集合**不变**(判据面不掺派生/密钥面)
|
|
466
|
+
with open(os.path.join(root, "_keys.json"), "w", encoding="utf-8") as f:
|
|
467
|
+
f.write('{"k":"v2"}')
|
|
468
|
+
with open(os.path.join(root, "junk.tmp"), "w", encoding="utf-8") as f:
|
|
469
|
+
f.write("tmp2")
|
|
470
|
+
c = S.source_face_hashes(root)
|
|
471
|
+
ok(c == b, "G7a 负对照:动密钥/临时件**不改变**真源面哈希集合(判据面被钉死)")
|
|
472
|
+
# 正对照:新增 .md → added;删除 .md → removed
|
|
473
|
+
for L in ("knowledge", "anchor"):
|
|
474
|
+
os.makedirs(os.path.join(root, L), exist_ok=True)
|
|
475
|
+
with open(os.path.join(root, "anchor", "n_d.md"), "w", encoding="utf-8",
|
|
476
|
+
newline="\n") as f:
|
|
477
|
+
f.write("x")
|
|
478
|
+
d1 = S.face_delta(c, S.source_face_hashes(root))
|
|
479
|
+
ok(d1["added"] == ["anchor/n_d.md"] and not d1["removed"],
|
|
480
|
+
"G7b 正对照:新增 .md 被报为 added", d1)
|
|
481
|
+
with_p = S.source_face_hashes(root)
|
|
482
|
+
os.remove(os.path.join(root, "anchor", "n_d.md"))
|
|
483
|
+
d2 = S.face_delta(with_p, S.source_face_hashes(root))
|
|
484
|
+
ok(d2["removed"] == ["anchor/n_d.md"] and not d2["added"],
|
|
485
|
+
"G7c 正对照:删除 .md 被报为 removed", d2)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
_GROUPS = (g0, g1, g2, g3, g4, g5, g6, g7)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _run_groups() -> int:
|
|
492
|
+
_GEN[0] += 1 # 每轮换 gen:沙箱与状态根全新,互不串味
|
|
493
|
+
_PASS.clear()
|
|
494
|
+
_FAIL.clear()
|
|
495
|
+
for g in _GROUPS:
|
|
496
|
+
try:
|
|
497
|
+
g()
|
|
498
|
+
except Exception as exc: # noqa: BLE001
|
|
499
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
500
|
+
import traceback
|
|
501
|
+
print(traceback.format_exc()[-900:])
|
|
502
|
+
return len(_FAIL)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ---------------------------------------------------------------- 变异表
|
|
506
|
+
# 锚点 = (名字, rel, old, new)。每次变异必须让套件**转红**:
|
|
507
|
+
# M1 白名单不滤空目录(旧形态:把 LAYERS 目录名整表喂给 add)→ add 整体失败
|
|
508
|
+
# M2 add 退回整树形态(不带 pathspec)→ 数据根里的密钥/临时/锁件被入册
|
|
509
|
+
# M3 materialize 不校验锁(旧形态:无根级锁)→ 跨进程唯一性判据失效
|
|
510
|
+
# M4 影子改落 state_root 根(不在 sleep/ 下)→ 落点判据失效
|
|
511
|
+
# M5 锁文件改落数据根 → 「锁不在数据根」判据失效
|
|
512
|
+
_MUTATIONS = (
|
|
513
|
+
("白名单不滤空目录", "md_cg/sleep.py",
|
|
514
|
+
" if files:\n out.append(layer)\n break",
|
|
515
|
+
" out.append(layer)\n break"),
|
|
516
|
+
("add 退回整树形态(不带 pathspec)", "md_cg/sleep.py",
|
|
517
|
+
' _git_ok(git_dir_path, work_tree, "add", "--", *ps)',
|
|
518
|
+
' _git_ok(git_dir_path, work_tree, "add", "-A")'),
|
|
519
|
+
("materialize 不校验根级锁", "md_cg/sleep.py",
|
|
520
|
+
' if not lk.acquired:\n return _busy("materialize", timeout)',
|
|
521
|
+
' if False:\n return _busy("materialize", timeout)'),
|
|
522
|
+
("影子改落 state_root 根", "md_cg/sleep.py",
|
|
523
|
+
" return os.path.abspath(v) if v else os.path.join(sleep_root(), SHADOW_DIRNAME)",
|
|
524
|
+
" return os.path.abspath(v) if v else os.path.join(state_root(), SHADOW_DIRNAME)"),
|
|
525
|
+
("锁文件改落数据根", "md_cg/sleep.py",
|
|
526
|
+
" return os.path.join(sleep_root(), LOCK_NAME)",
|
|
527
|
+
" return os.path.join(mdcg_root(), LOCK_NAME)"),
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
532
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
533
|
+
"__file__": os.path.join(_REPO, rel)}
|
|
534
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
535
|
+
m = types.ModuleType(name)
|
|
536
|
+
m.__dict__.update(ns)
|
|
537
|
+
return m
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
541
|
+
print("!! 定点变异自证:逐条把机制改回「改动前/错误」形态,套件必须转红\n")
|
|
542
|
+
if list_only:
|
|
543
|
+
for name, rel, _o, _n in _MUTATIONS:
|
|
544
|
+
print(" %-34s [%s]" % (name, rel))
|
|
545
|
+
return 0
|
|
546
|
+
anchor_miss, bad = [], []
|
|
547
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
548
|
+
clean = _run_groups()
|
|
549
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
550
|
+
if clean:
|
|
551
|
+
bad.append("未变异基线即失败")
|
|
552
|
+
|
|
553
|
+
for name, rel, old, new in _MUTATIONS:
|
|
554
|
+
src = _rel_text(rel)
|
|
555
|
+
if old not in src:
|
|
556
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没同步"
|
|
557
|
+
"本表)" % (name, rel))
|
|
558
|
+
anchor_miss.append(name)
|
|
559
|
+
continue
|
|
560
|
+
mut_src = src.replace(old, new, 1)
|
|
561
|
+
_SRC[rel] = mut_src
|
|
562
|
+
mfile = os.path.join(_TMP, "sleep_mut_%d.py" % len(_MUT_CTX))
|
|
563
|
+
with open(mfile, "w", encoding="utf-8", newline="\n") as f:
|
|
564
|
+
f.write(mut_src)
|
|
565
|
+
_MUT_CTX["mutsrc"] = mfile
|
|
566
|
+
try:
|
|
567
|
+
mut = _exec_module("md_cg._sleep_mut", rel, mut_src)
|
|
568
|
+
globals()["SL"] = mut
|
|
569
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
570
|
+
reds = _run_groups()
|
|
571
|
+
detail = buf.getvalue()
|
|
572
|
+
finally:
|
|
573
|
+
globals()["SL"] = _REAL_SL
|
|
574
|
+
_SRC.pop(rel, None)
|
|
575
|
+
_MUT_CTX.pop("mutsrc", None)
|
|
576
|
+
reds_lines = [l for l in detail.splitlines()
|
|
577
|
+
if l.strip().startswith("FAIL ")]
|
|
578
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
579
|
+
print(" %s %-34s 红项=%d %s"
|
|
580
|
+
% ("OK " if reds else "MISS ", name, reds, verdict))
|
|
581
|
+
for l in reds_lines[:5]:
|
|
582
|
+
print(" " + l.strip()[5:])
|
|
583
|
+
if not reds:
|
|
584
|
+
bad.append(name)
|
|
585
|
+
|
|
586
|
+
if anchor_miss:
|
|
587
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
588
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
589
|
+
return 2
|
|
590
|
+
print("\n定点变异自证:%s"
|
|
591
|
+
% ("PASS(每处机制都有一条断言把它钉死)" if not bad
|
|
592
|
+
else "FAIL —— " + "、".join(bad)))
|
|
593
|
+
return 0 if not bad else 1
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def main() -> int:
|
|
597
|
+
_sandbox_env()
|
|
598
|
+
try:
|
|
599
|
+
if "--mutate" in sys.argv:
|
|
600
|
+
return _mutate_mode("--list" in sys.argv)
|
|
601
|
+
n = _run_groups()
|
|
602
|
+
print("\n睡眠 git 机制守卫:%d 通过,%d 失败,%d 跳过"
|
|
603
|
+
% (len(_PASS), n, len(_SKIP)))
|
|
604
|
+
return 0 if not n else 1
|
|
605
|
+
finally:
|
|
606
|
+
_restore_env()
|
|
607
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
if __name__ == "__main__":
|
|
611
|
+
sys.exit(main())
|