@furongjun1999/dsh-memory 0.7.2 → 0.7.4
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 +57 -47
- package/codebuddy/CODEBUDDY.md +6 -6
- package/docs/README.md +148 -144
- package/docs/discipline/harnesses.yaml +16 -8
- package/docs/discipline/templates/zcode-user.md.tmpl +49 -0
- package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_/345/205/255/345/256/266/350/256/260/345/277/206/347/263/273/347/273/237/345/220/214/345/260/272/345/256/236/346/265/213_v3.1.md +340 -0
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +2 -2
- 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 +1 -1
- 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 +1 -1
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +1 -1
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +2 -2
- 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 +5 -5
- 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 +9 -9
- 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 +4 -4
- 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 +1 -1
- 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 +1 -1
- 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 +3 -3
- 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 +3 -3
- 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 +7 -7
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +2 -2
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +1 -1
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +7 -7
- 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 +10 -10
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +4 -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 +1 -1
- package/docs/eval//345/217/221/345/270/20311_/345/244/226/346/212/245/345/205/255/350/277/236/344/277/256/344/270/216/344/270/226/347/225/214/346/250/241/345/236/213/345/272/225/345/272/247_v1.0.md +91 -0
- package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -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 +2 -2
- package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/270/200/346/211/271_v1.0.md +195 -0
- package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/272/214/346/211/271B6_/346/234/254/346/234/272/350/267/257/345/276/204/347/233/270/345/257/271/345/214/226_v1.0.md +78 -0
- package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +1 -1
- package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +3 -3
- 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 +1 -1
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +1 -1
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v26.md +196 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v27.md +259 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v28.md +217 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v29.md +270 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v30.md +231 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v31.md +281 -0
- package/docs/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -0
- package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +1 -1
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +1 -1
- package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +17 -17
- package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
- package/docs/mdcg//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -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 +45 -42
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
- 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 +71 -1
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +1 -1
- package/docs/plans//345/244/232/344/270/273/344/275/223/344/270/226/347/225/214/346/250/241/345/236/213_/345/257/271/351/275/220/350/257/204/344/274/260/344/270/216/350/220/275/345/234/260/350/256/276/350/256/241_v0.1.md +141 -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 +2 -2
- package/docs/plans//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/350/256/276/350/256/241_v0.1.md +107 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +6 -5
- package/docs//347/247/244_/350/256/260/345/277/206/347/263/273/347/273/237/350/257/204/346/265/213/350/247/204/350/214/203_v2.1.md +225 -0
- package/dsh/cordis-patch-profile-web.example.yml +2 -2
- package/lib/bridge.js +9 -0
- package/lib/hooks.d.ts +8 -1
- package/lib/hooks.js +134 -50
- package/lib/lib/hook_audit.d.ts +55 -0
- package/lib/lib/hook_audit.js +208 -0
- package/lib/lib/mutual.js +5 -1
- package/lib/lib/roleplay_web.js +17 -2
- package/lib/lib/session_state.d.ts +56 -0
- package/lib/lib/session_state.js +124 -0
- package/lib/tools.js +12 -1
- package/md_cg/auditview.py +248 -0
- package/md_cg/bench_e2e_judge.py +4 -2
- package/md_cg/branches.py +29 -3
- package/md_cg/consistency.py +38 -13
- package/md_cg/datapath.py +38 -11
- package/md_cg/forgetting.py +15 -2
- package/md_cg/freshness.py +32 -11
- package/md_cg/ghostref.py +130 -0
- package/md_cg/judgment_manifest.py +7 -0
- package/md_cg/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +122 -12
- package/md_cg/mdcg.py +4923 -4822
- package/md_cg/mdcos.py +109 -21
- package/md_cg/nodefile.py +77 -13
- package/md_cg/readcache.py +29 -4
- package/md_cg/selfreport.py +12 -3
- package/md_cg/sleep.py +129 -13
- package/md_cg/sources.py +1 -1
- package/md_cg/state_events.py +131 -0
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- package/md_cg/sustain.py +1436 -1436
- package/md_cg/test_auditview.py +277 -0
- package/md_cg/test_autonomy_modes.py +2 -1
- package/md_cg/test_boot_openblas_guard.py +197 -0
- package/md_cg/test_ccg_form_parity.py +95 -4
- package/md_cg/test_cons200_scan_selection.py +4 -2
- package/md_cg/test_datapath_device_name.py +62 -3
- package/md_cg/test_generation_guard.py +5 -2
- package/md_cg/test_ghostref.py +182 -0
- package/md_cg/test_interop.py +1 -1
- package/md_cg/test_issue52_scan_condition_first.py +1 -1
- package/md_cg/test_lifecycle_retire_leak.py +408 -0
- package/md_cg/test_lock.py +16 -7
- package/md_cg/test_m3_h9_semantic_guard.py +1 -1
- package/md_cg/test_n230_dirty_replay.py +375 -375
- package/md_cg/test_n238_body_text_parity.py +156 -0
- package/md_cg/test_n276_probe_hoist.py +199 -0
- package/md_cg/test_p4_freshness.py +95 -1
- package/md_cg/test_policy_required_ccg.py +3 -1
- package/md_cg/test_read_face_semantics.py +6 -3
- package/md_cg/test_readcache_prodpath.py +66 -0
- package/md_cg/test_sleep.py +58 -1
- package/md_cg/test_sleep_p1.py +218 -2
- package/md_cg/test_state_event_op.py +514 -0
- package/md_cg/test_state_events.py +178 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/test_time_core_lint.py +8 -1
- package/md_cg/test_writepipe.py +8 -5
- package/md_cg/tokens.py +18 -2
- package/md_cg/writepipe.py +76 -0
- package/package.json +2 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +9 -0
- package/src/hooks.ts +599 -517
- package/src/lib/hook_audit.ts +243 -0
- package/src/lib/mutual.ts +5 -1
- package/src/lib/roleplay_web.ts +17 -2
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
- package/zcode/AGENTS.md +6 -6
|
@@ -1,375 +1,375 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""N230 `_dirty` 重放遮蔽守卫(P0-3:新记录必须能盖住旧记录,旧不得盖新)。
|
|
3
|
-
|
|
4
|
-
缺陷(材料自编 N226,`docs/eval/缺陷挖掘_自主迭代_v25.md:132/:178` 记为**留档未修**):
|
|
5
|
-
|
|
6
|
-
站点 `md_cg/mdcg.py` 的 `_maybe_reload_index`(改动前 :1353-1357)——
|
|
7
|
-
`for nid, e in self._dirty.items(): … idx["nodes"][nid] = e` **无条件**把本实例
|
|
8
|
-
未 flush 的 `_dirty` 条目盖回重载后的索引上,没有任何时序/代际比较。
|
|
9
|
-
|
|
10
|
-
**探针实跑复现(2026-10-01,独立 tempfile 沙箱)**:
|
|
11
|
-
|
|
12
|
-
① A = MdCG(R, autoflush=1000);A.add("x_node", 正文=OLD) ⇒ A._dirty 含 x_node(未 flush)
|
|
13
|
-
② B = MdCG(R, autoflush=1);B.add("x_node", 正文=NEW);B.close() ⇒ 盘面与快照都是 NEW
|
|
14
|
-
③ A._maybe_reload_index() 返回 **True**(确实重载了),但重载后
|
|
15
|
-
A.index["nodes"]["x_node"]["content_hash"] **仍是 OLD**;新建读者实例 C 读到 NEW
|
|
16
|
-
⇒ 索引与盘面撕裂(检索面按旧值给候选/桶/指纹)
|
|
17
|
-
|
|
18
|
-
**修法(本轮)**:给 `_dirty` 引入「**落盘逐字见证**」——标脏时记下节点文件的
|
|
19
|
-
`(st_mtime_ns, st_size)`;重放前再 stat 一次,失配 ⇒ 本实例这条已被盘面更新盖过
|
|
20
|
-
⇒ **跳过重放**(放行盘面)。证不出陈旧(无 path / stat 失败 / 未挂 root)时照旧
|
|
21
|
-
重放——「本实例未落盘写入不因重载从检索面消失」这条既有不变量一并保留。
|
|
22
|
-
tombstone(`None`)语义**一字不改**(`_unstage` 立即 flush,不跨重载存活)。
|
|
23
|
-
|
|
24
|
-
断言分组:
|
|
25
|
-
|
|
26
|
-
G0 前置——站点在位:`_dirty_entry_superseded` / `_note_witness` 各一处;`_dirty.root` 已挂。
|
|
27
|
-
G1 **红基线可出示**(探针三步复现)——旧记录**不得**盖住盘面新记录:A 重载后
|
|
28
|
-
必须收敛到 NEW(= 新建读者实例 C 的读数),且 `_dirty_replay_superseded ≥ 1`。
|
|
29
|
-
G2 不变量保持——他进程只动**别的**节点时,本实例未 flush 的写入必须仍在检索面
|
|
30
|
-
上(`_dirty_replay_superseded == 0`,该条目仍在 index 且哈希不变)。
|
|
31
|
-
G3 tombstone 语义一字不改——`_dirty[nid]=None` 在重载时仍 pop 掉该条目。
|
|
32
|
-
G4 见证判据的判别力(正/负对照)——`_dirty_entry_superseded` 的四条边界逐一断言。
|
|
33
|
-
G5 单点结构(静态)——`for nid, e in self._dirty.items():` 的重放体内**必须**
|
|
34
|
-
经过 `_dirty_entry_superseded`(副本回归/裸赋值回归即红)。
|
|
35
|
-
|
|
36
|
-
运行:python -X utf8 -m md_cg.test_n230_dirty_replay
|
|
37
|
-
python -X utf8 -m md_cg.test_n230_dirty_replay --head-baseline # 红基线自证
|
|
38
|
-
python -X utf8 -m md_cg.test_n230_dirty_replay --mutate # 定点变异自证
|
|
39
|
-
python -X utf8 -m md_cg.test_n230_dirty_replay --mutate --list
|
|
40
|
-
|
|
41
|
-
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
42
|
-
2 = ANCHOR-MISS(变异锚点在当前源码里找不到——实现改了却没同步本表)。
|
|
43
|
-
|
|
44
|
-
**基线纪律**:`--head-baseline` 把 `_dirty_entry_superseded` 换回「恒 False」
|
|
45
|
-
(= 改动前的无条件重放),G1 必须转红;若不红,说明 G1 没有判别力。
|
|
46
|
-
"""
|
|
47
|
-
from __future__ import annotations
|
|
48
|
-
|
|
49
|
-
import contextlib
|
|
50
|
-
import io
|
|
51
|
-
import os
|
|
52
|
-
import shutil
|
|
53
|
-
import sys
|
|
54
|
-
import tempfile
|
|
55
|
-
import types
|
|
56
|
-
|
|
57
|
-
from .mdcg import MdCG
|
|
58
|
-
|
|
59
|
-
_PASS = []
|
|
60
|
-
_FAIL = []
|
|
61
|
-
_SKIP = []
|
|
62
|
-
_SRC = {}
|
|
63
|
-
_TMP = tempfile.mkdtemp(prefix="mdcg_n230_")
|
|
64
|
-
_GEN = [0]
|
|
65
|
-
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
66
|
-
|
|
67
|
-
BODY = "# 功能名:%s\n# 生效条件:全时窗\n# 子功能:s\n# 执行:%s\n" \
|
|
68
|
-
"# 验证方式:test\n# 不适用条件:无\n"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def ok(cond, msg, extra=""):
|
|
72
|
-
(_PASS if cond else _FAIL).append(msg)
|
|
73
|
-
print((" PASS " if cond else " FAIL ") + msg
|
|
74
|
-
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _rel_text(rel: str) -> str:
|
|
78
|
-
if rel in _SRC:
|
|
79
|
-
return _SRC[rel]
|
|
80
|
-
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
81
|
-
return f.read()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def _sandbox(name: str) -> str:
|
|
85
|
-
_GEN[0] += 1
|
|
86
|
-
d = os.path.join(_TMP, "gen%d_%s" % (_GEN[0], name))
|
|
87
|
-
os.makedirs(d, exist_ok=True)
|
|
88
|
-
return d
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _disk_body(root: str, nid: str) -> str:
|
|
92
|
-
p = os.path.join(root, "knowledge", "orphan", nid + ".md")
|
|
93
|
-
return open(p, encoding="utf-8").read()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# ---------------------------------------------------------------- G0
|
|
97
|
-
def g0():
|
|
98
|
-
print("== G0 站点在位(前置)==")
|
|
99
|
-
src = _rel_text("md_cg/mdcg.py")
|
|
100
|
-
ok(src.count("def _dirty_entry_superseded") == 1,
|
|
101
|
-
"G0a mdcg.py 内 `_dirty_entry_superseded` 恰一处定义")
|
|
102
|
-
ok(src.count("def _note_witness") == 1,
|
|
103
|
-
"G0b mdcg.py 内 `_note_witness` 恰一处定义")
|
|
104
|
-
ok("self._dirty.root = self.root" in src,
|
|
105
|
-
"G0c MdCG.__init__ 把数据根交给脏集(见证要靠它拼绝对路径)")
|
|
106
|
-
r = _sandbox("g0")
|
|
107
|
-
cg = MdCG(r, autoflush=1000)
|
|
108
|
-
ok(cg._dirty.root == cg.root and os.path.isabs(cg._dirty.root),
|
|
109
|
-
"G0d 实例构造后 _dirty.root = 数据根", cg._dirty.root)
|
|
110
|
-
ok(cg._dirty_replay_superseded == 0,
|
|
111
|
-
"G0e 读数 _dirty_replay_superseded 初始为 0")
|
|
112
|
-
cg.close()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# ---------------------------------------------------------------- G1
|
|
116
|
-
def _probe_repro():
|
|
117
|
-
"""探针三步复现(返回值供 G1 逐条断言)。"""
|
|
118
|
-
r = _sandbox("g1")
|
|
119
|
-
a = MdCG(r, autoflush=1000)
|
|
120
|
-
a.add("x_node", BODY % ("x", "OLD"), layer="knowledge",
|
|
121
|
-
verification_basis="test")
|
|
122
|
-
witness = dict(a._dirty.staged_stat)
|
|
123
|
-
b = MdCG(r, autoflush=1)
|
|
124
|
-
b.add("x_node", BODY % ("x", "NEW"), layer="knowledge",
|
|
125
|
-
verification_basis="test")
|
|
126
|
-
b.close() # 他进程写出新盘面 + 新快照
|
|
127
|
-
reloaded = a._maybe_reload_index()
|
|
128
|
-
c = MdCG(r, autoflush=1000) # 新建读者实例(只读盘面)
|
|
129
|
-
out = {"root": r, "a": a, "c": c, "reloaded": reloaded,
|
|
130
|
-
"a_hash": (a.index["nodes"].get("x_node") or {}).get("content_hash"),
|
|
131
|
-
"c_hash": (c.index["nodes"].get("x_node") or {}).get("content_hash"),
|
|
132
|
-
"superseded": a._dirty_replay_superseded,
|
|
133
|
-
"witness": witness, "dirty": list(a._dirty),
|
|
134
|
-
"disk": _disk_body(r, "x_node")}
|
|
135
|
-
return out
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def g1():
|
|
139
|
-
print("== G1 红基线可出示:旧记录不得盖住盘面新记录(探针复现)==")
|
|
140
|
-
R = _probe_repro()
|
|
141
|
-
ok(R["witness"].get("x_node") is not None,
|
|
142
|
-
"G1 前置:A 标脏时记下了逐字见证 (mtime_ns, size)", R["witness"])
|
|
143
|
-
ok(R["reloaded"] is True,
|
|
144
|
-
"G1a 前置:重载确已发生(_maybe_reload_index() == True)", R["reloaded"])
|
|
145
|
-
ok("执行:NEW" in R["disk"],
|
|
146
|
-
"G1b 前置:盘面真值是 NEW", R["disk"].splitlines()[3])
|
|
147
|
-
ok(R["c_hash"] is not None and R["a_hash"] == R["c_hash"],
|
|
148
|
-
"G1c **新盖旧**:A 重载后条目 == 新建读者实例 C 的条目(收敛到盘面)",
|
|
149
|
-
{"A": R["a_hash"], "C": R["c_hash"]})
|
|
150
|
-
ok(R["superseded"] >= 1,
|
|
151
|
-
"G1d 可判读数:至少 1 条被判「已被盘面更新盖过」而跳过重放",
|
|
152
|
-
R["superseded"])
|
|
153
|
-
ok(R["a"]._dirty.get("x_node") is not None,
|
|
154
|
-
"G1e 该条仍在 _dirty(本实例确实还没 flush——不是靠 flush 凑的绿)",
|
|
155
|
-
list(R["a"]._dirty))
|
|
156
|
-
R["a"].close(); R["c"].close()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# ---------------------------------------------------------------- G2
|
|
160
|
-
def g2():
|
|
161
|
-
print("== G2 不变量保持:他进程只动别的节点时,未落盘写入不消失 ==")
|
|
162
|
-
r = _sandbox("g2")
|
|
163
|
-
a = MdCG(r, autoflush=1000)
|
|
164
|
-
a.add("y_node", BODY % ("y", "MINE"), layer="knowledge",
|
|
165
|
-
verification_basis="test")
|
|
166
|
-
a_hash = a.index["nodes"]["y_node"]["content_hash"]
|
|
167
|
-
b = MdCG(r, autoflush=1)
|
|
168
|
-
b.add("z_node", BODY % ("z", "OTHER"), layer="knowledge",
|
|
169
|
-
verification_basis="test")
|
|
170
|
-
b.close()
|
|
171
|
-
reloaded = a._maybe_reload_index()
|
|
172
|
-
ok(reloaded is True, "G2 前置:重载确已发生(他进程改了签名)", reloaded)
|
|
173
|
-
ok(a._dirty_replay_superseded == 0,
|
|
174
|
-
"G2a y_node 未被判陈旧(盘面上没人动它)——见证失配才是陈旧",
|
|
175
|
-
a._dirty_replay_superseded)
|
|
176
|
-
e = a.index["nodes"].get("y_node")
|
|
177
|
-
ok(e is not None and e.get("content_hash") == a_hash,
|
|
178
|
-
"G2b **不变量保持**:本实例未 flush 的 y_node 仍在检索面上且哈希不变",
|
|
179
|
-
{"present": e is not None})
|
|
180
|
-
ok(a.index["nodes"].get("z_node") is not None,
|
|
181
|
-
"G2c 他进程的 z_node 也可见(重载不该只保一边)")
|
|
182
|
-
a.close(); b.close()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# ---------------------------------------------------------------- G3
|
|
186
|
-
def g3():
|
|
187
|
-
print("== G3 tombstone 语义一字不改 ==")
|
|
188
|
-
r = _sandbox("g3")
|
|
189
|
-
c = MdCG(r, autoflush=1000)
|
|
190
|
-
c.add("tomb_node", BODY % ("t", "X"), layer="knowledge",
|
|
191
|
-
verification_basis="test")
|
|
192
|
-
c.flush() # 入日志;_dirty 清空
|
|
193
|
-
ok("tomb_node" in c.index["nodes"], "G3 前置:节点已在索引里")
|
|
194
|
-
c.index["nodes"].pop("tomb_node", None)
|
|
195
|
-
c._dirty["tomb_node"] = None # 不经 _unstage(那条路立即 flush)
|
|
196
|
-
ok(c._dirty.get("tomb_node") is None and "tomb_node" in c._dirty,
|
|
197
|
-
"G3a 前置:_dirty 里有该 nid 的 tombstone(值 None)")
|
|
198
|
-
c._index_sig = None # 强制签名失配 → 下次读面必然重载
|
|
199
|
-
ok(c._maybe_reload_index() is True, "G3b 前置:已重载")
|
|
200
|
-
ok("tomb_node" not in c.index["nodes"],
|
|
201
|
-
"G3c tombstone 仍被重放 pop 掉(本修复不碰 tombstone 语义)")
|
|
202
|
-
c.close()
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
# ---------------------------------------------------------------- G4
|
|
206
|
-
def g4():
|
|
207
|
-
print("== G4 见证判据的判别力(正/负对照)==")
|
|
208
|
-
r = _sandbox("g4")
|
|
209
|
-
c = MdCG(r, autoflush=1000)
|
|
210
|
-
c.add("w_node", BODY % ("w", "V1"), layer="knowledge",
|
|
211
|
-
verification_basis="test")
|
|
212
|
-
ok(c._dirty_entry_superseded("w_node") is False,
|
|
213
|
-
"G4a 负对照:标脏后盘面未动 → 不判陈旧(未落盘写入照旧重放)")
|
|
214
|
-
import time as _t
|
|
215
|
-
_t.sleep(0.01)
|
|
216
|
-
with open(os.path.join(r, c._dirty["w_node"]["path"]), "w",
|
|
217
|
-
encoding="utf-8", newline="\n") as f:
|
|
218
|
-
f.write(BODY % ("w", "V2"))
|
|
219
|
-
ok(c._dirty_entry_superseded("w_node") is True,
|
|
220
|
-
"G4b 正对照:盘面被改写 → 判陈旧(旧不得盖新)")
|
|
221
|
-
os.remove(os.path.join(r, c._dirty["w_node"]["path"]))
|
|
222
|
-
ok(c._dirty_entry_superseded("w_node") is True,
|
|
223
|
-
"G4c 正对照:文件被删掉(标脏时在)→ 判陈旧")
|
|
224
|
-
c._dirty.staged_stat.pop("w_node", None)
|
|
225
|
-
ok(c._dirty_entry_superseded("w_node") is False,
|
|
226
|
-
"G4d 无见证(证不出陈旧)→ 不判陈旧——不变量优先于覆盖面")
|
|
227
|
-
c._dirty.staged_stat["nopath"] = (1, 1)
|
|
228
|
-
c._dirty["nopath"] = {"layer": "knowledge"} # 无 path 的条目
|
|
229
|
-
ok(c._dirty_entry_superseded("nopath") is False,
|
|
230
|
-
"G4e 条目无 path → 不判陈旧(无见证可查)")
|
|
231
|
-
ok(c._dirty.root == r and c._dirty.staged_stat.get("w_node") is None,
|
|
232
|
-
"G4f 移除文件的那次 stat 失败**不登记**见证(staged_stat 无该键)")
|
|
233
|
-
c.close()
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# ---------------------------------------------------------------- G5
|
|
237
|
-
def g5():
|
|
238
|
-
print("== G5 单点结构(静态:重放体必须过判据)==")
|
|
239
|
-
src = _rel_text("md_cg/mdcg.py")
|
|
240
|
-
i = src.index("for nid, e in self._dirty.items():")
|
|
241
|
-
body = src[i:i + 900]
|
|
242
|
-
ok("self._dirty_entry_superseded(nid)" in body,
|
|
243
|
-
"G5 重放循环体内经过 `_dirty_entry_superseded`(副本回归即红)")
|
|
244
|
-
ok("superseded += 1" in body and "continue" in body,
|
|
245
|
-
"G5a 判为陈旧即 `continue`(跳过重放,不是只记账)")
|
|
246
|
-
# 可执行行里不得再有无条件覆盖的裸赋值形态
|
|
247
|
-
code = "\n".join(l for l in body.splitlines()
|
|
248
|
-
if not l.strip().startswith("#"))
|
|
249
|
-
ok(code.count('idx["nodes"][nid] = e') == 1,
|
|
250
|
-
"G5b 重放体里 `idx[\"nodes\"][nid] = e` 恰一处(在判据之后)")
|
|
251
|
-
ok("self._dirty_replay_superseded = superseded" in src,
|
|
252
|
-
"G5c 跳过条数落在可判读数 `_dirty_replay_superseded` 上")
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
_GROUPS = (g0, g1, g2, g3, g4, g5)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _run_groups() -> int:
|
|
259
|
-
_PASS.clear()
|
|
260
|
-
_FAIL.clear()
|
|
261
|
-
for g in _GROUPS:
|
|
262
|
-
try:
|
|
263
|
-
g()
|
|
264
|
-
except Exception as exc: # noqa: BLE001
|
|
265
|
-
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
266
|
-
import traceback
|
|
267
|
-
print(traceback.format_exc()[-900:])
|
|
268
|
-
return len(_FAIL)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# ---------------------------------------------------------------- 变异表
|
|
272
|
-
# 锚点 = (名字, rel, old, new)。每次变异必须让套件**转红**:
|
|
273
|
-
# M1 重放体恢复无条件覆盖(= 改动前形态)→ G1c/G1d/G5 红
|
|
274
|
-
# M2 判据恒 False(见证照记但不采信)→ G1c/G1d 红
|
|
275
|
-
# M3 见证完全不登记 → 判据永远拿不到见证 ⇒ 同 M2
|
|
276
|
-
_MUTATIONS = (
|
|
277
|
-
("重放体恢复无条件覆盖(改动前形态)", "md_cg/mdcg.py",
|
|
278
|
-
" if self._dirty_entry_superseded(nid):\n"
|
|
279
|
-
" superseded += 1\n continue",
|
|
280
|
-
" if False:\n superseded += 1\n continue"),
|
|
281
|
-
("判据恒 False(见证不采信)", "md_cg/mdcg.py",
|
|
282
|
-
" return (cur.st_mtime_ns, cur.st_size) != st",
|
|
283
|
-
" return False"),
|
|
284
|
-
("见证完全不登记", "md_cg/mdcg.py",
|
|
285
|
-
" self.staged_stat[k] = (st.st_mtime_ns, st.st_size)",
|
|
286
|
-
" self.staged_stat.pop(k, None)"),
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _exec_module(name: str, rel: str, text: str):
|
|
291
|
-
ns = {"__name__": name, "__package__": "md_cg",
|
|
292
|
-
"__file__": os.path.join(_REPO, rel)}
|
|
293
|
-
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
294
|
-
m = types.ModuleType(name)
|
|
295
|
-
m.__dict__.update(ns)
|
|
296
|
-
return m
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def _mutate_mode(list_only: bool = False) -> int:
|
|
300
|
-
print("!! 定点变异自证:逐条把判据改回「改动前/失效」形态,套件必须转红\n")
|
|
301
|
-
if list_only:
|
|
302
|
-
for name, rel, _o, _n in _MUTATIONS:
|
|
303
|
-
print(" %-34s [%s]" % (name, rel))
|
|
304
|
-
return 0
|
|
305
|
-
anchor_miss, bad = [], []
|
|
306
|
-
with contextlib.redirect_stdout(io.StringIO()):
|
|
307
|
-
clean = _run_groups()
|
|
308
|
-
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
309
|
-
if clean:
|
|
310
|
-
bad.append("未变异基线即失败")
|
|
311
|
-
|
|
312
|
-
for name, rel, old, new in _MUTATIONS:
|
|
313
|
-
src = _rel_text(rel)
|
|
314
|
-
if old not in src:
|
|
315
|
-
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没同步"
|
|
316
|
-
"本表)" % (name, rel))
|
|
317
|
-
anchor_miss.append(name)
|
|
318
|
-
continue
|
|
319
|
-
mut_src = src.replace(old, new, 1)
|
|
320
|
-
_SRC[rel] = mut_src
|
|
321
|
-
try:
|
|
322
|
-
mut = _exec_module("md_cg._n230_mut", rel, mut_src)
|
|
323
|
-
globals()["MdCG"] = mut.MdCG
|
|
324
|
-
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
325
|
-
reds = _run_groups()
|
|
326
|
-
detail = buf.getvalue()
|
|
327
|
-
finally:
|
|
328
|
-
globals()["MdCG"] = _REAL_MdCG
|
|
329
|
-
_SRC.pop(rel, None)
|
|
330
|
-
red_lines = [l for l in detail.splitlines()
|
|
331
|
-
if l.strip().startswith("FAIL ")]
|
|
332
|
-
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
333
|
-
print(" %s %-34s 红项=%d %s"
|
|
334
|
-
% ("OK " if reds else "MISS ", name, reds, verdict))
|
|
335
|
-
for l in red_lines[:5]:
|
|
336
|
-
print(" " + l.strip()[5:])
|
|
337
|
-
if not reds:
|
|
338
|
-
bad.append(name)
|
|
339
|
-
|
|
340
|
-
if anchor_miss:
|
|
341
|
-
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
342
|
-
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
343
|
-
return 2
|
|
344
|
-
print("\n定点变异自证:%s"
|
|
345
|
-
% ("PASS(每处判据都被打红)" if not bad else "FAIL —— " + "、".join(bad)))
|
|
346
|
-
return 0 if not bad else 1
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
_REAL_MdCG = MdCG
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def main() -> int:
|
|
353
|
-
try:
|
|
354
|
-
if "--mutate" in sys.argv:
|
|
355
|
-
return _mutate_mode("--list" in sys.argv)
|
|
356
|
-
if "--head-baseline" in sys.argv:
|
|
357
|
-
print("!! 红基线模式:_dirty_entry_superseded 换回「恒 False」"
|
|
358
|
-
"(= 改动前的无条件重放),G1 应当转红\n")
|
|
359
|
-
MdCG._dirty_entry_superseded = lambda self, nid: False
|
|
360
|
-
n = _run_groups()
|
|
361
|
-
print("\n== 红基线判定:应有失败 ==")
|
|
362
|
-
print("红基线失败数 = %d(>0 才算断言有判别力)" % n)
|
|
363
|
-
for f in _FAIL:
|
|
364
|
-
print(" 红:", f)
|
|
365
|
-
return 0 if n else 1
|
|
366
|
-
n = _run_groups()
|
|
367
|
-
print("\nN230 `_dirty` 重放遮蔽守卫:%d 通过,%d 失败,%d 跳过"
|
|
368
|
-
% (len(_PASS), n, len(_SKIP)))
|
|
369
|
-
return 0 if not n else 1
|
|
370
|
-
finally:
|
|
371
|
-
shutil.rmtree(_TMP, ignore_errors=True)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if __name__ == "__main__":
|
|
375
|
-
sys.exit(main())
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""N230 `_dirty` 重放遮蔽守卫(P0-3:新记录必须能盖住旧记录,旧不得盖新)。
|
|
3
|
+
|
|
4
|
+
缺陷(材料自编 N226,`docs/eval/缺陷挖掘_自主迭代_v25.md:132/:178` 记为**留档未修**):
|
|
5
|
+
|
|
6
|
+
站点 `md_cg/mdcg.py` 的 `_maybe_reload_index`(改动前 :1353-1357)——
|
|
7
|
+
`for nid, e in self._dirty.items(): … idx["nodes"][nid] = e` **无条件**把本实例
|
|
8
|
+
未 flush 的 `_dirty` 条目盖回重载后的索引上,没有任何时序/代际比较。
|
|
9
|
+
|
|
10
|
+
**探针实跑复现(2026-10-01,独立 tempfile 沙箱)**:
|
|
11
|
+
|
|
12
|
+
① A = MdCG(R, autoflush=1000);A.add("x_node", 正文=OLD) ⇒ A._dirty 含 x_node(未 flush)
|
|
13
|
+
② B = MdCG(R, autoflush=1);B.add("x_node", 正文=NEW);B.close() ⇒ 盘面与快照都是 NEW
|
|
14
|
+
③ A._maybe_reload_index() 返回 **True**(确实重载了),但重载后
|
|
15
|
+
A.index["nodes"]["x_node"]["content_hash"] **仍是 OLD**;新建读者实例 C 读到 NEW
|
|
16
|
+
⇒ 索引与盘面撕裂(检索面按旧值给候选/桶/指纹)
|
|
17
|
+
|
|
18
|
+
**修法(本轮)**:给 `_dirty` 引入「**落盘逐字见证**」——标脏时记下节点文件的
|
|
19
|
+
`(st_mtime_ns, st_size)`;重放前再 stat 一次,失配 ⇒ 本实例这条已被盘面更新盖过
|
|
20
|
+
⇒ **跳过重放**(放行盘面)。证不出陈旧(无 path / stat 失败 / 未挂 root)时照旧
|
|
21
|
+
重放——「本实例未落盘写入不因重载从检索面消失」这条既有不变量一并保留。
|
|
22
|
+
tombstone(`None`)语义**一字不改**(`_unstage` 立即 flush,不跨重载存活)。
|
|
23
|
+
|
|
24
|
+
断言分组:
|
|
25
|
+
|
|
26
|
+
G0 前置——站点在位:`_dirty_entry_superseded` / `_note_witness` 各一处;`_dirty.root` 已挂。
|
|
27
|
+
G1 **红基线可出示**(探针三步复现)——旧记录**不得**盖住盘面新记录:A 重载后
|
|
28
|
+
必须收敛到 NEW(= 新建读者实例 C 的读数),且 `_dirty_replay_superseded ≥ 1`。
|
|
29
|
+
G2 不变量保持——他进程只动**别的**节点时,本实例未 flush 的写入必须仍在检索面
|
|
30
|
+
上(`_dirty_replay_superseded == 0`,该条目仍在 index 且哈希不变)。
|
|
31
|
+
G3 tombstone 语义一字不改——`_dirty[nid]=None` 在重载时仍 pop 掉该条目。
|
|
32
|
+
G4 见证判据的判别力(正/负对照)——`_dirty_entry_superseded` 的四条边界逐一断言。
|
|
33
|
+
G5 单点结构(静态)——`for nid, e in self._dirty.items():` 的重放体内**必须**
|
|
34
|
+
经过 `_dirty_entry_superseded`(副本回归/裸赋值回归即红)。
|
|
35
|
+
|
|
36
|
+
运行:python -X utf8 -m md_cg.test_n230_dirty_replay
|
|
37
|
+
python -X utf8 -m md_cg.test_n230_dirty_replay --head-baseline # 红基线自证
|
|
38
|
+
python -X utf8 -m md_cg.test_n230_dirty_replay --mutate # 定点变异自证
|
|
39
|
+
python -X utf8 -m md_cg.test_n230_dirty_replay --mutate --list
|
|
40
|
+
|
|
41
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
42
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到——实现改了却没同步本表)。
|
|
43
|
+
|
|
44
|
+
**基线纪律**:`--head-baseline` 把 `_dirty_entry_superseded` 换回「恒 False」
|
|
45
|
+
(= 改动前的无条件重放),G1 必须转红;若不红,说明 G1 没有判别力。
|
|
46
|
+
"""
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import contextlib
|
|
50
|
+
import io
|
|
51
|
+
import os
|
|
52
|
+
import shutil
|
|
53
|
+
import sys
|
|
54
|
+
import tempfile
|
|
55
|
+
import types
|
|
56
|
+
|
|
57
|
+
from .mdcg import MdCG
|
|
58
|
+
|
|
59
|
+
_PASS = []
|
|
60
|
+
_FAIL = []
|
|
61
|
+
_SKIP = []
|
|
62
|
+
_SRC = {}
|
|
63
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_n230_")
|
|
64
|
+
_GEN = [0]
|
|
65
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
66
|
+
|
|
67
|
+
BODY = "# 功能名:%s\n# 生效条件:全时窗\n# 子功能:s\n# 执行:%s\n" \
|
|
68
|
+
"# 验证方式:test\n# 不适用条件:无\n"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ok(cond, msg, extra=""):
|
|
72
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
73
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
74
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _rel_text(rel: str) -> str:
|
|
78
|
+
if rel in _SRC:
|
|
79
|
+
return _SRC[rel]
|
|
80
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
81
|
+
return f.read()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _sandbox(name: str) -> str:
|
|
85
|
+
_GEN[0] += 1
|
|
86
|
+
d = os.path.join(_TMP, "gen%d_%s" % (_GEN[0], name))
|
|
87
|
+
os.makedirs(d, exist_ok=True)
|
|
88
|
+
return d
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _disk_body(root: str, nid: str) -> str:
|
|
92
|
+
p = os.path.join(root, "knowledge", "orphan", nid + ".md")
|
|
93
|
+
return open(p, encoding="utf-8").read()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------- G0
|
|
97
|
+
def g0():
|
|
98
|
+
print("== G0 站点在位(前置)==")
|
|
99
|
+
src = _rel_text("md_cg/mdcg.py")
|
|
100
|
+
ok(src.count("def _dirty_entry_superseded") == 1,
|
|
101
|
+
"G0a mdcg.py 内 `_dirty_entry_superseded` 恰一处定义")
|
|
102
|
+
ok(src.count("def _note_witness") == 1,
|
|
103
|
+
"G0b mdcg.py 内 `_note_witness` 恰一处定义")
|
|
104
|
+
ok("self._dirty.root = self.root" in src,
|
|
105
|
+
"G0c MdCG.__init__ 把数据根交给脏集(见证要靠它拼绝对路径)")
|
|
106
|
+
r = _sandbox("g0")
|
|
107
|
+
cg = MdCG(r, autoflush=1000)
|
|
108
|
+
ok(cg._dirty.root == cg.root and os.path.isabs(cg._dirty.root),
|
|
109
|
+
"G0d 实例构造后 _dirty.root = 数据根", cg._dirty.root)
|
|
110
|
+
ok(cg._dirty_replay_superseded == 0,
|
|
111
|
+
"G0e 读数 _dirty_replay_superseded 初始为 0")
|
|
112
|
+
cg.close()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------- G1
|
|
116
|
+
def _probe_repro():
|
|
117
|
+
"""探针三步复现(返回值供 G1 逐条断言)。"""
|
|
118
|
+
r = _sandbox("g1")
|
|
119
|
+
a = MdCG(r, autoflush=1000)
|
|
120
|
+
a.add("x_node", BODY % ("x", "OLD"), layer="knowledge",
|
|
121
|
+
verification_basis="test")
|
|
122
|
+
witness = dict(a._dirty.staged_stat)
|
|
123
|
+
b = MdCG(r, autoflush=1)
|
|
124
|
+
b.add("x_node", BODY % ("x", "NEW"), layer="knowledge",
|
|
125
|
+
verification_basis="test")
|
|
126
|
+
b.close() # 他进程写出新盘面 + 新快照
|
|
127
|
+
reloaded = a._maybe_reload_index()
|
|
128
|
+
c = MdCG(r, autoflush=1000) # 新建读者实例(只读盘面)
|
|
129
|
+
out = {"root": r, "a": a, "c": c, "reloaded": reloaded,
|
|
130
|
+
"a_hash": (a.index["nodes"].get("x_node") or {}).get("content_hash"),
|
|
131
|
+
"c_hash": (c.index["nodes"].get("x_node") or {}).get("content_hash"),
|
|
132
|
+
"superseded": a._dirty_replay_superseded,
|
|
133
|
+
"witness": witness, "dirty": list(a._dirty),
|
|
134
|
+
"disk": _disk_body(r, "x_node")}
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def g1():
|
|
139
|
+
print("== G1 红基线可出示:旧记录不得盖住盘面新记录(探针复现)==")
|
|
140
|
+
R = _probe_repro()
|
|
141
|
+
ok(R["witness"].get("x_node") is not None,
|
|
142
|
+
"G1 前置:A 标脏时记下了逐字见证 (mtime_ns, size)", R["witness"])
|
|
143
|
+
ok(R["reloaded"] is True,
|
|
144
|
+
"G1a 前置:重载确已发生(_maybe_reload_index() == True)", R["reloaded"])
|
|
145
|
+
ok("执行:NEW" in R["disk"],
|
|
146
|
+
"G1b 前置:盘面真值是 NEW", R["disk"].splitlines()[3])
|
|
147
|
+
ok(R["c_hash"] is not None and R["a_hash"] == R["c_hash"],
|
|
148
|
+
"G1c **新盖旧**:A 重载后条目 == 新建读者实例 C 的条目(收敛到盘面)",
|
|
149
|
+
{"A": R["a_hash"], "C": R["c_hash"]})
|
|
150
|
+
ok(R["superseded"] >= 1,
|
|
151
|
+
"G1d 可判读数:至少 1 条被判「已被盘面更新盖过」而跳过重放",
|
|
152
|
+
R["superseded"])
|
|
153
|
+
ok(R["a"]._dirty.get("x_node") is not None,
|
|
154
|
+
"G1e 该条仍在 _dirty(本实例确实还没 flush——不是靠 flush 凑的绿)",
|
|
155
|
+
list(R["a"]._dirty))
|
|
156
|
+
R["a"].close(); R["c"].close()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---------------------------------------------------------------- G2
|
|
160
|
+
def g2():
|
|
161
|
+
print("== G2 不变量保持:他进程只动别的节点时,未落盘写入不消失 ==")
|
|
162
|
+
r = _sandbox("g2")
|
|
163
|
+
a = MdCG(r, autoflush=1000)
|
|
164
|
+
a.add("y_node", BODY % ("y", "MINE"), layer="knowledge",
|
|
165
|
+
verification_basis="test")
|
|
166
|
+
a_hash = a.index["nodes"]["y_node"]["content_hash"]
|
|
167
|
+
b = MdCG(r, autoflush=1)
|
|
168
|
+
b.add("z_node", BODY % ("z", "OTHER"), layer="knowledge",
|
|
169
|
+
verification_basis="test")
|
|
170
|
+
b.close()
|
|
171
|
+
reloaded = a._maybe_reload_index()
|
|
172
|
+
ok(reloaded is True, "G2 前置:重载确已发生(他进程改了签名)", reloaded)
|
|
173
|
+
ok(a._dirty_replay_superseded == 0,
|
|
174
|
+
"G2a y_node 未被判陈旧(盘面上没人动它)——见证失配才是陈旧",
|
|
175
|
+
a._dirty_replay_superseded)
|
|
176
|
+
e = a.index["nodes"].get("y_node")
|
|
177
|
+
ok(e is not None and e.get("content_hash") == a_hash,
|
|
178
|
+
"G2b **不变量保持**:本实例未 flush 的 y_node 仍在检索面上且哈希不变",
|
|
179
|
+
{"present": e is not None})
|
|
180
|
+
ok(a.index["nodes"].get("z_node") is not None,
|
|
181
|
+
"G2c 他进程的 z_node 也可见(重载不该只保一边)")
|
|
182
|
+
a.close(); b.close()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------- G3
|
|
186
|
+
def g3():
|
|
187
|
+
print("== G3 tombstone 语义一字不改 ==")
|
|
188
|
+
r = _sandbox("g3")
|
|
189
|
+
c = MdCG(r, autoflush=1000)
|
|
190
|
+
c.add("tomb_node", BODY % ("t", "X"), layer="knowledge",
|
|
191
|
+
verification_basis="test")
|
|
192
|
+
c.flush() # 入日志;_dirty 清空
|
|
193
|
+
ok("tomb_node" in c.index["nodes"], "G3 前置:节点已在索引里")
|
|
194
|
+
c.index["nodes"].pop("tomb_node", None)
|
|
195
|
+
c._dirty["tomb_node"] = None # 不经 _unstage(那条路立即 flush)
|
|
196
|
+
ok(c._dirty.get("tomb_node") is None and "tomb_node" in c._dirty,
|
|
197
|
+
"G3a 前置:_dirty 里有该 nid 的 tombstone(值 None)")
|
|
198
|
+
c._index_sig = None # 强制签名失配 → 下次读面必然重载
|
|
199
|
+
ok(c._maybe_reload_index() is True, "G3b 前置:已重载")
|
|
200
|
+
ok("tomb_node" not in c.index["nodes"],
|
|
201
|
+
"G3c tombstone 仍被重放 pop 掉(本修复不碰 tombstone 语义)")
|
|
202
|
+
c.close()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ---------------------------------------------------------------- G4
|
|
206
|
+
def g4():
|
|
207
|
+
print("== G4 见证判据的判别力(正/负对照)==")
|
|
208
|
+
r = _sandbox("g4")
|
|
209
|
+
c = MdCG(r, autoflush=1000)
|
|
210
|
+
c.add("w_node", BODY % ("w", "V1"), layer="knowledge",
|
|
211
|
+
verification_basis="test")
|
|
212
|
+
ok(c._dirty_entry_superseded("w_node") is False,
|
|
213
|
+
"G4a 负对照:标脏后盘面未动 → 不判陈旧(未落盘写入照旧重放)")
|
|
214
|
+
import time as _t
|
|
215
|
+
_t.sleep(0.01)
|
|
216
|
+
with open(os.path.join(r, c._dirty["w_node"]["path"]), "w",
|
|
217
|
+
encoding="utf-8", newline="\n") as f:
|
|
218
|
+
f.write(BODY % ("w", "V2"))
|
|
219
|
+
ok(c._dirty_entry_superseded("w_node") is True,
|
|
220
|
+
"G4b 正对照:盘面被改写 → 判陈旧(旧不得盖新)")
|
|
221
|
+
os.remove(os.path.join(r, c._dirty["w_node"]["path"]))
|
|
222
|
+
ok(c._dirty_entry_superseded("w_node") is True,
|
|
223
|
+
"G4c 正对照:文件被删掉(标脏时在)→ 判陈旧")
|
|
224
|
+
c._dirty.staged_stat.pop("w_node", None)
|
|
225
|
+
ok(c._dirty_entry_superseded("w_node") is False,
|
|
226
|
+
"G4d 无见证(证不出陈旧)→ 不判陈旧——不变量优先于覆盖面")
|
|
227
|
+
c._dirty.staged_stat["nopath"] = (1, 1)
|
|
228
|
+
c._dirty["nopath"] = {"layer": "knowledge"} # 无 path 的条目
|
|
229
|
+
ok(c._dirty_entry_superseded("nopath") is False,
|
|
230
|
+
"G4e 条目无 path → 不判陈旧(无见证可查)")
|
|
231
|
+
ok(c._dirty.root == r and c._dirty.staged_stat.get("w_node") is None,
|
|
232
|
+
"G4f 移除文件的那次 stat 失败**不登记**见证(staged_stat 无该键)")
|
|
233
|
+
c.close()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------- G5
|
|
237
|
+
def g5():
|
|
238
|
+
print("== G5 单点结构(静态:重放体必须过判据)==")
|
|
239
|
+
src = _rel_text("md_cg/mdcg.py")
|
|
240
|
+
i = src.index("for nid, e in self._dirty.items():")
|
|
241
|
+
body = src[i:i + 900]
|
|
242
|
+
ok("self._dirty_entry_superseded(nid)" in body,
|
|
243
|
+
"G5 重放循环体内经过 `_dirty_entry_superseded`(副本回归即红)")
|
|
244
|
+
ok("superseded += 1" in body and "continue" in body,
|
|
245
|
+
"G5a 判为陈旧即 `continue`(跳过重放,不是只记账)")
|
|
246
|
+
# 可执行行里不得再有无条件覆盖的裸赋值形态
|
|
247
|
+
code = "\n".join(l for l in body.splitlines()
|
|
248
|
+
if not l.strip().startswith("#"))
|
|
249
|
+
ok(code.count('idx["nodes"][nid] = e') == 1,
|
|
250
|
+
"G5b 重放体里 `idx[\"nodes\"][nid] = e` 恰一处(在判据之后)")
|
|
251
|
+
ok("self._dirty_replay_superseded = superseded" in src,
|
|
252
|
+
"G5c 跳过条数落在可判读数 `_dirty_replay_superseded` 上")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
_GROUPS = (g0, g1, g2, g3, g4, g5)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _run_groups() -> int:
|
|
259
|
+
_PASS.clear()
|
|
260
|
+
_FAIL.clear()
|
|
261
|
+
for g in _GROUPS:
|
|
262
|
+
try:
|
|
263
|
+
g()
|
|
264
|
+
except Exception as exc: # noqa: BLE001
|
|
265
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
266
|
+
import traceback
|
|
267
|
+
print(traceback.format_exc()[-900:])
|
|
268
|
+
return len(_FAIL)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------- 变异表
|
|
272
|
+
# 锚点 = (名字, rel, old, new)。每次变异必须让套件**转红**:
|
|
273
|
+
# M1 重放体恢复无条件覆盖(= 改动前形态)→ G1c/G1d/G5 红
|
|
274
|
+
# M2 判据恒 False(见证照记但不采信)→ G1c/G1d 红
|
|
275
|
+
# M3 见证完全不登记 → 判据永远拿不到见证 ⇒ 同 M2
|
|
276
|
+
_MUTATIONS = (
|
|
277
|
+
("重放体恢复无条件覆盖(改动前形态)", "md_cg/mdcg.py",
|
|
278
|
+
" if self._dirty_entry_superseded(nid):\n"
|
|
279
|
+
" superseded += 1\n continue",
|
|
280
|
+
" if False:\n superseded += 1\n continue"),
|
|
281
|
+
("判据恒 False(见证不采信)", "md_cg/mdcg.py",
|
|
282
|
+
" return (cur.st_mtime_ns, cur.st_size) != st",
|
|
283
|
+
" return False"),
|
|
284
|
+
("见证完全不登记", "md_cg/mdcg.py",
|
|
285
|
+
" self.staged_stat[k] = (st.st_mtime_ns, st.st_size)",
|
|
286
|
+
" self.staged_stat.pop(k, None)"),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
291
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
292
|
+
"__file__": os.path.join(_REPO, rel)}
|
|
293
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
294
|
+
m = types.ModuleType(name)
|
|
295
|
+
m.__dict__.update(ns)
|
|
296
|
+
return m
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
300
|
+
print("!! 定点变异自证:逐条把判据改回「改动前/失效」形态,套件必须转红\n")
|
|
301
|
+
if list_only:
|
|
302
|
+
for name, rel, _o, _n in _MUTATIONS:
|
|
303
|
+
print(" %-34s [%s]" % (name, rel))
|
|
304
|
+
return 0
|
|
305
|
+
anchor_miss, bad = [], []
|
|
306
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
307
|
+
clean = _run_groups()
|
|
308
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
309
|
+
if clean:
|
|
310
|
+
bad.append("未变异基线即失败")
|
|
311
|
+
|
|
312
|
+
for name, rel, old, new in _MUTATIONS:
|
|
313
|
+
src = _rel_text(rel)
|
|
314
|
+
if old not in src:
|
|
315
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没同步"
|
|
316
|
+
"本表)" % (name, rel))
|
|
317
|
+
anchor_miss.append(name)
|
|
318
|
+
continue
|
|
319
|
+
mut_src = src.replace(old, new, 1)
|
|
320
|
+
_SRC[rel] = mut_src
|
|
321
|
+
try:
|
|
322
|
+
mut = _exec_module("md_cg._n230_mut", rel, mut_src)
|
|
323
|
+
globals()["MdCG"] = mut.MdCG
|
|
324
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
325
|
+
reds = _run_groups()
|
|
326
|
+
detail = buf.getvalue()
|
|
327
|
+
finally:
|
|
328
|
+
globals()["MdCG"] = _REAL_MdCG
|
|
329
|
+
_SRC.pop(rel, None)
|
|
330
|
+
red_lines = [l for l in detail.splitlines()
|
|
331
|
+
if l.strip().startswith("FAIL ")]
|
|
332
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
333
|
+
print(" %s %-34s 红项=%d %s"
|
|
334
|
+
% ("OK " if reds else "MISS ", name, reds, verdict))
|
|
335
|
+
for l in red_lines[:5]:
|
|
336
|
+
print(" " + l.strip()[5:])
|
|
337
|
+
if not reds:
|
|
338
|
+
bad.append(name)
|
|
339
|
+
|
|
340
|
+
if anchor_miss:
|
|
341
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
342
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
343
|
+
return 2
|
|
344
|
+
print("\n定点变异自证:%s"
|
|
345
|
+
% ("PASS(每处判据都被打红)" if not bad else "FAIL —— " + "、".join(bad)))
|
|
346
|
+
return 0 if not bad else 1
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
_REAL_MdCG = MdCG
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def main() -> int:
|
|
353
|
+
try:
|
|
354
|
+
if "--mutate" in sys.argv:
|
|
355
|
+
return _mutate_mode("--list" in sys.argv)
|
|
356
|
+
if "--head-baseline" in sys.argv:
|
|
357
|
+
print("!! 红基线模式:_dirty_entry_superseded 换回「恒 False」"
|
|
358
|
+
"(= 改动前的无条件重放),G1 应当转红\n")
|
|
359
|
+
MdCG._dirty_entry_superseded = lambda self, nid: False
|
|
360
|
+
n = _run_groups()
|
|
361
|
+
print("\n== 红基线判定:应有失败 ==")
|
|
362
|
+
print("红基线失败数 = %d(>0 才算断言有判别力)" % n)
|
|
363
|
+
for f in _FAIL:
|
|
364
|
+
print(" 红:", f)
|
|
365
|
+
return 0 if n else 1
|
|
366
|
+
n = _run_groups()
|
|
367
|
+
print("\nN230 `_dirty` 重放遮蔽守卫:%d 通过,%d 失败,%d 跳过"
|
|
368
|
+
% (len(_PASS), n, len(_SKIP)))
|
|
369
|
+
return 0 if not n else 1
|
|
370
|
+
finally:
|
|
371
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
sys.exit(main())
|