@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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
③ N226 `md_cg/provenance.py:427`(`_node_digest` 直读 cg.index)+`539-543`:
|
|
17
17
|
`cg(op="edges", expand_nodes=true)` 的端点摘要与边拓扑零可见性闸,两端都
|
|
18
18
|
不可见的边照返回、`aggregates.sample` 亦回 `child->parent`(同库同身份的
|
|
19
|
-
`stg.timeline` stg.py:
|
|
19
|
+
`stg.timeline` stg.py:144/238 早已接线 `_readable`——本处是漏网出口)。
|
|
20
20
|
④ N227 `md_cg/evolution.py:265/332` × `mdcos.py:3261`(health_os 挂
|
|
21
21
|
evolution.summary)× `mcp_server.py:3071-3072/2125/3288-3292`:演化台账
|
|
22
22
|
(`_evolution/ledger.md`)出口无可见性/身份闸,且工具面粗粒度 op 映射
|
|
@@ -362,11 +362,14 @@ def main():
|
|
|
362
362
|
check("3c 阳性对照:共享档(internal/S1)仍在候选中(闸未误伤)",
|
|
363
363
|
_ctrl is not None and any(e is _ctrl for e in _cands),
|
|
364
364
|
"cand=%d" % len(_cands), live=True)
|
|
365
|
-
# ③d 第三序面(本批首手实测:本条同时钉住 _open_content
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
|
|
365
|
+
# ③d 第三序面(本批首手实测:本条同时钉住 _open_content 的解密出口闸)。
|
|
366
|
+
# **N230(2026-10-01,P0-3)改判**:本组原 setup 断言「A 内存条目仍是本地未
|
|
367
|
+
# 落盘版本(`_dirty` 重放遮蔽盘上真值)」,即把**旧盖新**当既定行为钉住。
|
|
368
|
+
# N230 修复后该遮蔽不再发生——重载取回的是他进程写下的更新条目,本实例这条
|
|
369
|
+
# 陈旧 `_dirty` 被逐字见证判为陈旧、**跳过重放**(新记录必须能盖住旧记录)。
|
|
370
|
+
# 故 setup 断言改为「收敛到盘面真值(private/S2)」;下游两条(`get` 同闸拒绝、
|
|
371
|
+
# 正文水合不泄明文)语义不变,且在新语义下更强:索引侧密级也已是 private。
|
|
372
|
+
print("\n【③d】他进程更新条目必须盖过本实例陈旧 _dirty 条目(N230)")
|
|
370
373
|
a6 = _reflect(R6, "S1")
|
|
371
374
|
a6.add("n213_d", INTERNAL_BODY, layer="contextual") # 故意不 flush
|
|
372
375
|
b6 = _reflect(R6, "S2")
|
|
@@ -375,9 +378,12 @@ def main():
|
|
|
375
378
|
b6.flush()
|
|
376
379
|
a6._maybe_reload_index()
|
|
377
380
|
_e6 = a6.index["nodes"].get("n213_d") or {}
|
|
378
|
-
check("3d setup:A
|
|
379
|
-
_e6.get("sensitivity") == "
|
|
380
|
-
|
|
381
|
+
check("3d setup:A 重载后条目**收敛到盘面真值**(N230:旧不得盖新)",
|
|
382
|
+
_e6.get("sensitivity") == "private" and _e6.get("session") == "S2"
|
|
383
|
+
and a6._dirty_replay_superseded >= 1,
|
|
384
|
+
"sens=%r sess=%r superseded=%r"
|
|
385
|
+
% (_e6.get("sensitivity"), _e6.get("session"),
|
|
386
|
+
getattr(a6, "_dirty_replay_superseded", None)))
|
|
381
387
|
check("3d A.get 同闸拒绝",
|
|
382
388
|
a6.get("n213_d") is None,
|
|
383
389
|
repr((a6.get("n213_d") or {}).get("content") or "")[:50], live=True)
|
|
@@ -98,6 +98,13 @@ for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
|
|
|
98
98
|
"PYTHONIOENCODING", "PYTHONUTF8"):
|
|
99
99
|
os.environ.pop(_k, None)
|
|
100
100
|
|
|
101
|
+
# 三档自治批次②(2026-10-02)**夹具隔离**:本守卫考的不是档位面(档位守卫 =
|
|
102
|
+
# md_cg/test_autonomy_modes.py),故显式置 full 档——回到改动前「动作直落」的
|
|
103
|
+
# 行为,使本文件的断言意图(合并/覆写/软删真的发生)逐条不变;env 键名从唯一
|
|
104
|
+
# 真源表取(本文件不构成第二处字面量)。
|
|
105
|
+
from . import autonomy_modes as _autonomy_modes # noqa: E402
|
|
106
|
+
os.environ[_autonomy_modes.AUTONOMY_ENV_KEYS["mode"]] = "full"
|
|
107
|
+
|
|
101
108
|
from . import forgetting, mcp_server, protect, scrub, tokens, writelimit # noqa: E402
|
|
102
109
|
from .datapath import aux_root # noqa: E402
|
|
103
110
|
from .mdcos import MdCGSecure # noqa: E402
|
|
@@ -0,0 +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())
|
package/md_cg/test_p2_mcp.py
CHANGED
|
@@ -12,6 +12,14 @@ import subprocess
|
|
|
12
12
|
import sys
|
|
13
13
|
import tempfile
|
|
14
14
|
|
|
15
|
+
# 三档自治批次②(2026-10-02)**夹具隔离**:本守卫考的不是档位面(档位守卫 =
|
|
16
|
+
# md_cg/test_autonomy_modes.py),故显式置 full 档——回到改动前「动作直落」的
|
|
17
|
+
# 行为(本套的 forget/restore/review_decide 用例都假定软删真的发生),使断言
|
|
18
|
+
# 意图逐条不变。子进程继承本进程 environ,故被拉起的 MCP server 同档。
|
|
19
|
+
# env 键名从唯一真源表取(本文件不构成第二处字面量)。
|
|
20
|
+
from md_cg import autonomy_modes as _autonomy_modes
|
|
21
|
+
os.environ[_autonomy_modes.AUTONOMY_ENV_KEYS["mode"]] = "full"
|
|
22
|
+
|
|
15
23
|
PASS = FAIL = 0
|
|
16
24
|
FAILS = []
|
|
17
25
|
|