@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
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_state_slots · L2 状态槽位投影守卫(P2 批:事件是源、槽位是投影、查询走 stg)。
|
|
3
|
+
|
|
4
|
+
契约:`docs/plans/语义时空图补全_世界模型功能端_设计_v0.1.md` §2/§3(六签收点已批准)。
|
|
5
|
+
|
|
6
|
+
断言分组:
|
|
7
|
+
|
|
8
|
+
A 现值/退役——多事件链(含撤回后再来新值**复活**)→ value/state 逐点断言;
|
|
9
|
+
tier 透传(含 retired 态仍取最后有值事件)与缺档位 'unspecified'(不编造)。
|
|
10
|
+
B 区间——t_to/seq_to 被下一事件闭合、末条不闭合(None);retired 态
|
|
11
|
+
t_from=最后有值事件、t_to=撤回事件(且是**最后一条**撤回)。
|
|
12
|
+
C 变迁史——history 序=落盘序、字段原样(七字段、逐条与台账记录逐字段相等)、
|
|
13
|
+
evidence/actor 回链;history=False 不带 history;尾条与现值互证(history↔value)。
|
|
14
|
+
D 多主体多槽位隔离 + (subject,slot) 字典序稳定排序 + subject/slot 过滤
|
|
15
|
+
+ include_retired 过滤。
|
|
16
|
+
E 空台账 → [](不报错);单事件边界;孤立撤回(起端无可溯源 → None,不编造)。
|
|
17
|
+
F stg.state_chain——开关关 → disabled 返回体(**不抛**、不伪装成空结果);
|
|
18
|
+
开 → 返回体形态(count/items/truncated/kept)与 limit<命中数时 truncated=True。
|
|
19
|
+
G CLI——进程内调 state_slots._main(隔离 env + temp root):--status 与
|
|
20
|
+
--subject/--slot/--include-retired/--no-history 输出可解析且与 project/status
|
|
21
|
+
逐值一致(env 根与 --root 显式两路一致)。
|
|
22
|
+
H 不建第二真源——project 一次 → append 新事件 → 再 project 结果即变(无缓存),
|
|
23
|
+
前次返回对象不被就地改写,且投影全程 zero 落盘(root 下文件集合不变)。
|
|
24
|
+
I 坏行可观测——台账写一行坏 JSON → project 不崩、BAD_ROWS 递增、status 同源读数
|
|
25
|
+
(沿 `md_cg/state_events.py` 既有口径)。
|
|
26
|
+
J MCP 面接线(`mcp_server._stg_call` / TOOLS schema)——缺 op 与未知 op 的
|
|
27
|
+
允许枚举含 state_chain、缺省 limit=50、开关关/开经 MCP 面同形、schema 参数与
|
|
28
|
+
描述到位(本组是对 A–I 之外的**接线面**补充,A–I 契约不变)。
|
|
29
|
+
|
|
30
|
+
隔离:全部落临时目录(`tempfile.mkdtemp`);env 里的根变量与投影开关全程置空或指向
|
|
31
|
+
临时根,**绝不动在役库**(本文件不含任何真实数据根路径字面量)。
|
|
32
|
+
|
|
33
|
+
运行:python -X utf8 -m md_cg.test_state_slots
|
|
34
|
+
python -X utf8 -m md_cg.test_state_slots --mutate # 定点变异自证
|
|
35
|
+
python -X utf8 -m md_cg.test_state_slots --mutate --list # 只列变异表
|
|
36
|
+
|
|
37
|
+
退出码(fail-closed):0 = 全绿 / 变异自证 PASS;1 = 有断言失败 / 变异未按预期转红;
|
|
38
|
+
2 = ANCHOR-MISS(变异锚点在当前源码里找不到——实现改了却没同步本表即硬失败)。
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import contextlib
|
|
43
|
+
import io
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import shutil
|
|
47
|
+
import sys
|
|
48
|
+
import tempfile
|
|
49
|
+
import types
|
|
50
|
+
|
|
51
|
+
from . import state_events
|
|
52
|
+
from . import state_slots
|
|
53
|
+
from . import stg
|
|
54
|
+
from .mdcos import MdCGOS
|
|
55
|
+
|
|
56
|
+
_PASS = []
|
|
57
|
+
_FAIL = []
|
|
58
|
+
_GEN = [0]
|
|
59
|
+
_TMP = tempfile.mkdtemp(prefix="mdcg_ss_")
|
|
60
|
+
_SAVED = {}
|
|
61
|
+
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
62
|
+
#: 被测模块的**可变引用**(定点变异把全局换成变异副本,组内一律经 _ss()/_sg() 取用)。
|
|
63
|
+
SS = state_slots
|
|
64
|
+
SG = stg
|
|
65
|
+
_REAL_SS = state_slots
|
|
66
|
+
_REAL_SG = stg
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def ok(cond, msg, extra=""):
|
|
70
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
71
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
72
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _ss():
|
|
76
|
+
return globals()["SS"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sg():
|
|
80
|
+
return globals()["SG"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# 生效条件:无入参;把 MDCG_ROOT/MDCG_STATE_ROOT/MDCG_AUX_ROOT/MDCG_DATA_ROOT/MDCG_STG_STATE 的现值存入 _SAVED 并逐个从 os.environ 移除(隔离:任何经 env 取根的路径都不得落到在役库)。
|
|
84
|
+
def _sandbox_env():
|
|
85
|
+
for k in ("MDCG_ROOT", "MDCG_STATE_ROOT", "MDCG_AUX_ROOT", "MDCG_DATA_ROOT",
|
|
86
|
+
"MDCG_STG_STATE"):
|
|
87
|
+
_SAVED[k] = os.environ.get(k)
|
|
88
|
+
os.environ.pop(k, None)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# 生效条件:无入参;把 _SAVED 逐个还原(原值 None 则移除键),恢复进程原有 env 形态。
|
|
92
|
+
def _restore_env():
|
|
93
|
+
for k, v in _SAVED.items():
|
|
94
|
+
if v is None:
|
|
95
|
+
os.environ.pop(k, None)
|
|
96
|
+
else:
|
|
97
|
+
os.environ[k] = v
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# 生效条件:无入参;在 _TMP 下新建自增代次目录并返回其上的 MdCGOS 实例(每调用一个新根:组间与变异轮间零串味;root 全在临时目录内)。
|
|
101
|
+
def _fresh_cg():
|
|
102
|
+
_GEN[0] += 1
|
|
103
|
+
return MdCGOS(os.path.join(_TMP, "gen%d" % _GEN[0], "root"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# 生效条件:cg 为沙箱实例、subject/slot 为过滤值、include_retired/history 为投影参数时,返回该过滤下**首个**单元(无命中返回 None)。
|
|
107
|
+
def _one(cg, subject, slot, include_retired=False, history=True):
|
|
108
|
+
us = _ss().project(cg, subject=subject, slot=slot,
|
|
109
|
+
include_retired=include_retired, history=history)
|
|
110
|
+
return us[0] if us else None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# 生效条件:argv 为 _main 的入参列表时——在 stdout 重定向下进程内调用 state_slots._main,返回 (退出码, 捕获文本);_main 抛异常时不吞(交由调用组记红)。
|
|
114
|
+
def _run_main(argv):
|
|
115
|
+
buf = io.StringIO()
|
|
116
|
+
with contextlib.redirect_stdout(buf):
|
|
117
|
+
rc = _ss()._main(argv)
|
|
118
|
+
return rc, buf.getvalue()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# 生效条件:txt 为 CLI 输出文本时返回 json.loads(txt);解析失败抛原异常(CLI 输出必须可解析,不吞)。
|
|
122
|
+
def _load(txt):
|
|
123
|
+
return json.loads(txt)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ---------------------------------------------------------------- A
|
|
127
|
+
# 生效条件:无入参;在自有沙箱根上按「启用→迁移→撤回→复活→带档位迁移→再撤回」推进甲/住所槽位,逐点断言 value/state/tier/event_count(落点见组内文案)。
|
|
128
|
+
def gA():
|
|
129
|
+
print("== A 现值 / 退役(撤回后复活) ==")
|
|
130
|
+
cg = _fresh_cg()
|
|
131
|
+
state_events.append(cg, "甲", "住所", old=None, new="别墅", kind="enablement",
|
|
132
|
+
seq=1, evidence="u1", actor="user")
|
|
133
|
+
u1 = _one(cg, "甲", "住所")
|
|
134
|
+
ok(u1 and u1["value"] == "别墅" and u1["state"] == "active",
|
|
135
|
+
"A1 首值生效(active / 别墅)", u1)
|
|
136
|
+
state_events.append(cg, "甲", "住所", old="别墅", new="小屋", kind="migration",
|
|
137
|
+
seq=2, evidence="u2")
|
|
138
|
+
u2 = _one(cg, "甲", "住所")
|
|
139
|
+
ok(u2 and u2["value"] == "小屋" and u2["state"] == "active",
|
|
140
|
+
"A2 迁移后现值=新值(active / 小屋)", u2)
|
|
141
|
+
state_events.append(cg, "甲", "住所", old="小屋", new=None, kind="retraction",
|
|
142
|
+
seq=3, evidence="u3")
|
|
143
|
+
u3 = _one(cg, "甲", "住所", include_retired=True)
|
|
144
|
+
ok(u3 and u3["value"] is None and u3["state"] == "retired",
|
|
145
|
+
"A3 撤回:value=None 且 state=retired", u3)
|
|
146
|
+
ok(_one(cg, "甲", "住所") is None,
|
|
147
|
+
"A3b 缺省投影剔除退役单元(include_retired=False)")
|
|
148
|
+
state_events.append(cg, "甲", "住所", old=None, new="船屋", kind="enablement",
|
|
149
|
+
seq=4, evidence="u4")
|
|
150
|
+
u4 = _one(cg, "甲", "住所")
|
|
151
|
+
ok(u4 and u4["value"] == "船屋" and u4["state"] == "active",
|
|
152
|
+
"A4 撤回后新值复活(active / 船屋;事件序自然处理)", u4)
|
|
153
|
+
ok(u4 and u4["event_count"] == 4,
|
|
154
|
+
"A5 event_count=4(退役段一并计入该槽位)", u4 and u4["event_count"])
|
|
155
|
+
ok(u4 and u4["tier"] == state_slots.TIER_UNSPECIFIED,
|
|
156
|
+
"A6 无 tier → 'unspecified'(不编造档位)", u4 and u4["tier"])
|
|
157
|
+
state_events.append(cg, "甲", "住所", old="船屋", new="岸上公寓",
|
|
158
|
+
kind="migration", seq=5, evidence="u5",
|
|
159
|
+
extra={"tier": "一手"})
|
|
160
|
+
u5 = _one(cg, "甲", "住所")
|
|
161
|
+
ok(u5 and u5["tier"] == "一手",
|
|
162
|
+
"A7 extra.tier 透传(append(extra=…) 并入行顶层)", u5 and u5["tier"])
|
|
163
|
+
state_events.append(cg, "甲", "住所", old="岸上公寓", new=None,
|
|
164
|
+
kind="retraction", seq=6, evidence="u6")
|
|
165
|
+
u6 = _one(cg, "甲", "住所", include_retired=True)
|
|
166
|
+
ok(u6 and u6["state"] == "retired" and u6["tier"] == "一手",
|
|
167
|
+
"A8 retired 态 tier 仍取**最后有值**事件(一手)", u6 and (u6["state"], u6["tier"]))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------- B
|
|
171
|
+
# 生效条件:无入参;在自有沙箱根上构造「有值→撤回」与「末条有值」两类单元及双撤回链,断言 t_from/t_to/seq_from/seq_to 的闭合形态(落点见组内文案)。
|
|
172
|
+
def gB():
|
|
173
|
+
print("== B 区间(闭合 / 未闭合 / retired 闭合点) ==")
|
|
174
|
+
cg = _fresh_cg()
|
|
175
|
+
r1 = state_events.append(cg, "乙", "住所", old=None, new="X",
|
|
176
|
+
kind="enablement", seq=10, evidence="b1")
|
|
177
|
+
r2 = state_events.append(cg, "乙", "住所", old="X", new=None,
|
|
178
|
+
kind="retraction", seq=11, evidence="b2")
|
|
179
|
+
u = _one(cg, "乙", "住所", include_retired=True)
|
|
180
|
+
ok(u and u["state"] == "retired" and u["value"] is None,
|
|
181
|
+
"B1 撤回后 retired / value=None", u)
|
|
182
|
+
ok(u and u["t_from"] == r1["t"] and u["seq_from"] == 10,
|
|
183
|
+
"B2 retired 态 t_from/seq_from =最后有值事件(r1)",
|
|
184
|
+
u and (u["t_from"], r1["t"], u["seq_from"]))
|
|
185
|
+
ok(u and u["t_to"] == r2["t"] and u["seq_to"] == 11,
|
|
186
|
+
"B3 retired 态 t_to/seq_to =撤回事件(闭合点=作废时刻)",
|
|
187
|
+
u and (u["t_to"], r2["t"], u["seq_to"]))
|
|
188
|
+
r3 = state_events.append(cg, "乙", "房贷", old=None, new="有",
|
|
189
|
+
kind="enablement", seq=12, evidence="b3")
|
|
190
|
+
v = _one(cg, "乙", "房贷")
|
|
191
|
+
ok(v and v["t_from"] == r3["t"] and v["seq_from"] == 12
|
|
192
|
+
and v["t_to"] is None and v["seq_to"] is None,
|
|
193
|
+
"B4 台账末条(active):t_from/seq_from 追溯本条、t_to/seq_to=None(未闭合)",
|
|
194
|
+
v and (v["t_from"], v["seq_from"], v["t_to"], v["seq_to"]))
|
|
195
|
+
r4 = state_events.append(cg, "乙", "房贷", old="有", new=None,
|
|
196
|
+
kind="retraction", seq=13, evidence="b4")
|
|
197
|
+
w = _one(cg, "乙", "房贷", include_retired=True)
|
|
198
|
+
ok(w and w["t_to"] == r4["t"] and w["seq_to"] == 13,
|
|
199
|
+
"B5 闭合点随撤回事件推进(t_to=撤回事件 t)", w and (w["t_to"], r4["t"]))
|
|
200
|
+
# 双撤回链:闭合点是**最后一条**撤回(=使该槽位进入 retired 的那条),
|
|
201
|
+
# 不是「有值事件的下一条」——两读法在此分道(用 seq 判,不用 t:
|
|
202
|
+
# 连续两次 append 可能落同一时钟刻度,拿时间戳判「不等」会假红)。
|
|
203
|
+
cg2 = _fresh_cg()
|
|
204
|
+
state_events.append(cg2, "己", "槽", old=None, new="A", kind="enablement",
|
|
205
|
+
seq=14, evidence="b5")
|
|
206
|
+
r6 = state_events.append(cg2, "己", "槽", old="A", new=None,
|
|
207
|
+
kind="retraction", seq=15, evidence="b6")
|
|
208
|
+
r7 = state_events.append(cg2, "己", "槽", old="A", new=None,
|
|
209
|
+
kind="retraction", seq=16, evidence="b7")
|
|
210
|
+
z = _one(cg2, "己", "槽", include_retired=True)
|
|
211
|
+
ok(z and z["t_to"] == r7["t"] and z["seq_to"] == 16
|
|
212
|
+
and z["seq_from"] == 14,
|
|
213
|
+
"B6 双撤回:t_to/seq_to =最后一条撤回(进入 retired 的那条;"
|
|
214
|
+
"非有值事件的下一条 seq=15)",
|
|
215
|
+
z and (z["t_to"], r6["t"], r7["t"], z["seq_to"]))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------- C
|
|
219
|
+
# 生效条件:无入参;在自有沙箱根上写三条带 evidence/actor 的事件,断言 history 的序、字段集、逐字段原样、回链、与现值互证及 history=False 的键省略(落点见组内文案)。
|
|
220
|
+
def gC():
|
|
221
|
+
print("== C 变迁史(序 / 字段 / 回链 / history=False) ==")
|
|
222
|
+
cg = _fresh_cg()
|
|
223
|
+
state_events.append(cg, "丙", "烤箱", old=None, new="未启用",
|
|
224
|
+
kind="enablement", seq=21, evidence="用户侧 r0021",
|
|
225
|
+
actor="user")
|
|
226
|
+
state_events.append(cg, "丙", "烤箱", old="未启用", new="已启用",
|
|
227
|
+
kind="enablement", seq=22, evidence="AI 侧 r0022",
|
|
228
|
+
actor="assistant")
|
|
229
|
+
state_events.append(cg, "丙", "烤箱", old="已启用", new="已拆",
|
|
230
|
+
kind="migration", seq=23, evidence="r0023")
|
|
231
|
+
recs = state_events.read(cg, subject="丙", slot="烤箱")
|
|
232
|
+
u = _one(cg, "丙", "烤箱")
|
|
233
|
+
ok(u and [h["seq"] for h in u["history"]] == [21, 22, 23],
|
|
234
|
+
"C1 history 序=落盘序(21/22/23)",
|
|
235
|
+
u and [h["seq"] for h in u["history"]])
|
|
236
|
+
ok(u and len(u["history"]) == u["event_count"] == 3,
|
|
237
|
+
"C2 history 条数=event_count=3",
|
|
238
|
+
u and (len(u["history"]), u["event_count"]))
|
|
239
|
+
fields = set(state_slots._HIST_FIELDS)
|
|
240
|
+
ok(u and all(set(h) == fields for h in u["history"]),
|
|
241
|
+
"C3 每条 history 恰为七字段(不发明字段)",
|
|
242
|
+
u and [sorted(h) for h in u["history"]][:1])
|
|
243
|
+
ok(u and len(recs) == 3 and all(
|
|
244
|
+
h[k] == rec.get(k) for h, rec in zip(u["history"], recs)
|
|
245
|
+
for k in fields),
|
|
246
|
+
"C4 字段原样(逐条与台账记录逐字段相等)")
|
|
247
|
+
ok(u and [h["evidence"] for h in u["history"]]
|
|
248
|
+
== ["用户侧 r0021", "AI 侧 r0022", "r0023"]
|
|
249
|
+
and [h["actor"] for h in u["history"]] == ["user", "assistant", ""],
|
|
250
|
+
"C5 evidence/actor 回链(可回查原文)",
|
|
251
|
+
u and [(h["evidence"], h["actor"]) for h in u["history"]])
|
|
252
|
+
ok(u and u["history"][-1]["new"] == u["value"],
|
|
253
|
+
"C6 尾条 new 与现值互证(history↔value)",
|
|
254
|
+
u and (u["history"][-1]["new"], u["value"]))
|
|
255
|
+
u_nh = _one(cg, "丙", "烤箱", history=False)
|
|
256
|
+
ok(u_nh and "history" not in u_nh,
|
|
257
|
+
"C7 history=False 时单元不带 history 键", u_nh and sorted(u_nh))
|
|
258
|
+
ok(u_nh and u_nh["value"] == u["value"]
|
|
259
|
+
and u_nh["event_count"] == u["event_count"]
|
|
260
|
+
and u_nh["seq_from"] == u["seq_from"],
|
|
261
|
+
"C8 history=False 只影响 history 键(其余读数逐位一致)")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------- D
|
|
265
|
+
# 生效条件:无入参;在自有沙箱根上铺三主体×两槽位(其中乙/房贷退役),断言字典序排序、include_retired 过滤、subject/slot 过滤与多主体同槽位互不串味(落点见组内文案)。
|
|
266
|
+
def gD():
|
|
267
|
+
print("== D 多主体多槽位 / 排序 / 过滤 ==")
|
|
268
|
+
cg = _fresh_cg()
|
|
269
|
+
for i, (s, k, v) in enumerate([("甲", "住所", "别墅"), ("甲", "房贷", "有"),
|
|
270
|
+
("乙", "住所", "帐篷"), ("乙", "房贷", "有"),
|
|
271
|
+
("丙", "住所", "船屋")]):
|
|
272
|
+
state_events.append(cg, s, k, old=None, new=v, kind="enablement",
|
|
273
|
+
seq=100 + i, evidence="d%d" % i)
|
|
274
|
+
state_events.append(cg, "乙", "房贷", old="有", new=None,
|
|
275
|
+
kind="retraction", seq=200, evidence="d200")
|
|
276
|
+
pairs = [(u["subject"], u["slot"])
|
|
277
|
+
for u in _ss().project(cg, include_retired=True)]
|
|
278
|
+
# 字典序(码位):丙(4E19) < 乙(4E59) < 甲(7532);住所(4F4F) < 房贷(623F)。
|
|
279
|
+
expect = [("丙", "住所"), ("乙", "住所"), ("乙", "房贷"),
|
|
280
|
+
("甲", "住所"), ("甲", "房贷")]
|
|
281
|
+
ok(pairs == expect, "D1 按 (subject, slot) 字典序稳定排序(含退役单元)", pairs)
|
|
282
|
+
live = _ss().project(cg)
|
|
283
|
+
ok(len(live) == 4 and all(u["state"] == "active" for u in live),
|
|
284
|
+
"D2 include_retired=False 剔除退役单元(5→4,留存全 active)",
|
|
285
|
+
[(u["subject"], u["slot"], u["state"]) for u in live])
|
|
286
|
+
sub = _ss().project(cg, subject="乙")
|
|
287
|
+
ok([(u["subject"], u["slot"]) for u in sub] == [("乙", "住所")],
|
|
288
|
+
"D3 subject 过滤(缺省剔除乙 的退役单元)", sub)
|
|
289
|
+
sub2 = _ss().project(cg, subject="乙", include_retired=True)
|
|
290
|
+
ok([(u["subject"], u["slot"]) for u in sub2]
|
|
291
|
+
== [("乙", "住所"), ("乙", "房贷")],
|
|
292
|
+
"D4 subject 过滤 + include_retired=True(两单元,字典序)",
|
|
293
|
+
[(u["subject"], u["slot"]) for u in sub2])
|
|
294
|
+
sl = _ss().project(cg, slot="住所")
|
|
295
|
+
ok([(u["subject"], u["slot"]) for u in sl]
|
|
296
|
+
== [("丙", "住所"), ("乙", "住所"), ("甲", "住所")],
|
|
297
|
+
"D5 slot 过滤(跨主体,字典序)",
|
|
298
|
+
[(u["subject"], u["slot"]) for u in sl])
|
|
299
|
+
ok(_ss().project(cg, subject="丁") == [],
|
|
300
|
+
"D6 未登记主体 → [](空过滤不报错)")
|
|
301
|
+
m = {(u["subject"], u["slot"]): u["value"]
|
|
302
|
+
for u in _ss().project(cg, include_retired=True)}
|
|
303
|
+
ok(m[("甲", "住所")] == "别墅" and m[("乙", "住所")] == "帐篷"
|
|
304
|
+
and m[("丙", "住所")] == "船屋" and m[("乙", "房贷")] is None,
|
|
305
|
+
"D7 多主体同槽位互不串味(各自现值/退役独立)", m)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------- E
|
|
309
|
+
# 生效条件:无入参;在自有沙箱根上断言空台账 → []与 status 全 0、单事件边界各读数、孤立撤回(起端无可溯源)的 None 语义(落点见组内文案)。
|
|
310
|
+
def gE():
|
|
311
|
+
print("== E 空台账 / 单事件边界 ==")
|
|
312
|
+
cg = _fresh_cg()
|
|
313
|
+
ok(_ss().project(cg) == [], "E1 空台账 → [](不报错)")
|
|
314
|
+
ok(_ss().project(cg, subject="甲", slot="住所") == [],
|
|
315
|
+
"E2 空台账 + 过滤 → []")
|
|
316
|
+
st = _ss().status(cg)
|
|
317
|
+
ok(st["events"] == 0 and st["subjects"] == 0 and st["slots"] == 0
|
|
318
|
+
and st["retired"] == 0 and isinstance(st["bad_rows"], int),
|
|
319
|
+
"E3 空台账 status 全 0(bad_rows 为 int 读数)", st)
|
|
320
|
+
r = state_events.append(cg, "甲", "住所", old=None, new="唯一值",
|
|
321
|
+
kind="enablement", seq=7, evidence="e7")
|
|
322
|
+
us = _ss().project(cg)
|
|
323
|
+
ok(len(us) == 1, "E4 单事件 → 恰一个单元", len(us))
|
|
324
|
+
u = us[0]
|
|
325
|
+
ok(u["value"] == "唯一值" and u["state"] == "active"
|
|
326
|
+
and u["event_count"] == 1 and u["t_from"] == r["t"]
|
|
327
|
+
and u["seq_from"] == 7 and u["t_to"] is None and u["seq_to"] is None,
|
|
328
|
+
"E5 单事件边界读数(active / t_from=本条 / t_to=None / count=1)", u)
|
|
329
|
+
ok(len(u["history"]) == 1 and u["history"][0]["seq"] == 7,
|
|
330
|
+
"E6 单事件 history 恰一条")
|
|
331
|
+
ok(u["alternatives"] == [] and u["blindspots"] == [],
|
|
332
|
+
"E7 alternatives/blindspots 结构在场且恒空(口径后置,不臆造)", u)
|
|
333
|
+
cg2 = _fresh_cg()
|
|
334
|
+
r2 = state_events.append(cg2, "戊", "槽", old="悬空旧值", new=None,
|
|
335
|
+
kind="retraction", seq=8, evidence="e8")
|
|
336
|
+
u2 = _one(cg2, "戊", "槽", include_retired=True)
|
|
337
|
+
ok(u2 and u2["state"] == "retired" and u2["value"] is None
|
|
338
|
+
and u2["t_from"] is None and u2["seq_from"] is None
|
|
339
|
+
and u2["t_to"] == r2["t"],
|
|
340
|
+
"E8 孤立撤回:起端无可溯源 → t_from/seq_from=None(不编造);t_to=该撤回事件",
|
|
341
|
+
u2)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------- F
|
|
345
|
+
# 生效条件:无入参;在自有沙箱根上先按开关关断言 disabled 返回体(不抛、不伪装空结果),再置 MDCG_STG_STATE=1 断言开臂形态、items 与 project 逐位一致、截断标记与参数透传;组末 finally 复开关(不留 env 污染)。
|
|
346
|
+
def gF():
|
|
347
|
+
print("== F stg.state_chain(开关关 / 开 + 截断) ==")
|
|
348
|
+
cg = _fresh_cg()
|
|
349
|
+
for i, (s, k, v) in enumerate([("甲", "住所", "别墅"), ("甲", "房贷", "有"),
|
|
350
|
+
("乙", "住所", "帐篷")]):
|
|
351
|
+
state_events.append(cg, s, k, old=None, new=v, kind="enablement",
|
|
352
|
+
seq=300 + i, evidence="f%d" % i)
|
|
353
|
+
os.environ.pop("MDCG_STG_STATE", None)
|
|
354
|
+
try:
|
|
355
|
+
off = _sg().state_chain(cg) # 关臂:**不抛**
|
|
356
|
+
ok(isinstance(off, dict) and off.get("error") == "disabled",
|
|
357
|
+
"F1 开关关 → disabled 体(不抛异常)", off)
|
|
358
|
+
ok("MDCG_STG_STATE" in (off.get("hint") or ""),
|
|
359
|
+
"F2 hint 点名开关名(可操作提示)", off.get("hint"))
|
|
360
|
+
ok("items" not in off and "count" not in off,
|
|
361
|
+
"F3 关臂不伪装成空结果(无 items/count 键)", sorted(off))
|
|
362
|
+
except Exception as exc: # noqa: BLE001
|
|
363
|
+
ok(False, "F1 开关关 → disabled 体(不抛异常)", repr(exc))
|
|
364
|
+
os.environ["MDCG_STG_STATE"] = "1"
|
|
365
|
+
try:
|
|
366
|
+
on = _sg().state_chain(cg, limit=50)
|
|
367
|
+
ok(on.get("count") == 3 and on.get("kept") == 3
|
|
368
|
+
and on.get("truncated") is False and len(on.get("items") or []) == 3,
|
|
369
|
+
"F4 开臂:count/kept/items 一致、未截断",
|
|
370
|
+
{k: on.get(k) for k in ("count", "kept", "truncated")})
|
|
371
|
+
ok(on.get("items") == _ss().project(cg),
|
|
372
|
+
"F5 items 与 state_slots.project 逐位一致(投影单点,不复制逻辑)")
|
|
373
|
+
ok(on.get("subject") is None and on.get("slot") is None
|
|
374
|
+
and on.get("include_retired") is False and on.get("limit") == 50,
|
|
375
|
+
"F6 返回体回带查询条件(subject/slot/include_retired/limit)",
|
|
376
|
+
{k: on.get(k) for k in ("subject", "slot", "include_retired", "limit")})
|
|
377
|
+
tr = _sg().state_chain(cg, limit=2)
|
|
378
|
+
ok(tr.get("count") == 3 and tr.get("kept") == 2
|
|
379
|
+
and tr.get("truncated") is True,
|
|
380
|
+
"F7 limit(2) < 命中数(3) → truncated=True(截断可观测)",
|
|
381
|
+
{k: tr.get(k) for k in ("count", "kept", "truncated")})
|
|
382
|
+
ok(tr.get("items") == _ss().project(cg)[:2],
|
|
383
|
+
"F8 截断取前 limit 条(序不变)")
|
|
384
|
+
flt = _sg().state_chain(cg, subject="甲", include_retired=True,
|
|
385
|
+
history=False, limit=1)
|
|
386
|
+
ok(flt.get("count") == 2 and flt.get("kept") == 1
|
|
387
|
+
and flt.get("truncated") is True
|
|
388
|
+
and "history" not in (flt.get("items") or [{}])[0],
|
|
389
|
+
"F9 参数透传(subject/include_retired/history)+ 截断同时可观测",
|
|
390
|
+
{k: flt.get(k) for k in ("count", "kept", "truncated")})
|
|
391
|
+
finally:
|
|
392
|
+
os.environ.pop("MDCG_STG_STATE", None)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ---------------------------------------------------------------- G
|
|
396
|
+
# 生效条件:无入参;在自有沙箱根上把 MDCG_ROOT 指向该根(隔离),进程内调用 _main 的 --status / --subject / --include-retired / --no-history / --root / 缺省六态,断言输出可解析且与 status/project 逐值一致;组末 finally 还原 MDCG_ROOT。
|
|
397
|
+
def gG():
|
|
398
|
+
print("== G CLI(--status / --subject;进程内 + 隔离 env) ==")
|
|
399
|
+
cg = _fresh_cg()
|
|
400
|
+
root = cg.root
|
|
401
|
+
state_events.append(cg, "甲", "住所", old=None, new="别墅",
|
|
402
|
+
kind="enablement", seq=41, evidence="g1")
|
|
403
|
+
state_events.append(cg, "甲", "住所", old="别墅", new=None,
|
|
404
|
+
kind="retraction", seq=42, evidence="g2")
|
|
405
|
+
state_events.append(cg, "乙", "住所", old=None, new="帐篷",
|
|
406
|
+
kind="enablement", seq=43, evidence="g3")
|
|
407
|
+
saved = os.environ.get("MDCG_ROOT")
|
|
408
|
+
os.environ["MDCG_ROOT"] = root # 隔离:CLI 的缺省根=本沙箱
|
|
409
|
+
try:
|
|
410
|
+
rc, txt = _run_main(["--status"])
|
|
411
|
+
ok(rc == 0, "G1 --status 退出码 0", rc)
|
|
412
|
+
got = _load(txt)
|
|
413
|
+
ok(got == _ss().status(cg),
|
|
414
|
+
"G2 --status 输出可解析且与 status() 逐值一致(缺省根走 env)",
|
|
415
|
+
(txt[:120], got))
|
|
416
|
+
rc, txt = _run_main(["--subject", "乙"])
|
|
417
|
+
ok(rc == 0 and _load(txt) == _ss().project(cg, subject="乙"),
|
|
418
|
+
"G3 --subject 输出与 project(subject=…) 逐值一致", txt[:160])
|
|
419
|
+
rc, txt = _run_main(["--subject", "甲", "--slot", "住所",
|
|
420
|
+
"--include-retired"])
|
|
421
|
+
ok(rc == 0
|
|
422
|
+
and _load(txt) == _ss().project(cg, subject="甲", slot="住所",
|
|
423
|
+
include_retired=True)
|
|
424
|
+
and _load(txt)[0]["state"] == "retired",
|
|
425
|
+
"G4 --subject + --slot + --include-retired(退役单元可见)", txt[:200])
|
|
426
|
+
rc, txt = _run_main(["--subject", "乙", "--no-history"])
|
|
427
|
+
ok(rc == 0
|
|
428
|
+
and _load(txt) == _ss().project(cg, subject="乙", history=False),
|
|
429
|
+
"G5 --no-history 不带 history(与 project(history=False) 一致)")
|
|
430
|
+
rc, txt = _run_main(["--status", "--root", root])
|
|
431
|
+
ok(rc == 0 and _load(txt) == _ss().status(cg),
|
|
432
|
+
"G6 --root 显式给定与 env 缺省两路同值")
|
|
433
|
+
rc, txt = _run_main([])
|
|
434
|
+
ok(rc == 0 and _load(txt) == _ss().project(cg),
|
|
435
|
+
"G7 缺省(无 --status/--subject)打印全量投影")
|
|
436
|
+
finally:
|
|
437
|
+
if saved is None:
|
|
438
|
+
os.environ.pop("MDCG_ROOT", None)
|
|
439
|
+
else:
|
|
440
|
+
os.environ["MDCG_ROOT"] = saved
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ---------------------------------------------------------------- H
|
|
444
|
+
# 生效条件:无入参;在自有沙箱根上断言「追加一条事件 → 再投影即变」(无缓存)、前次返回对象不被就地改写、以及投影调用(project/status/无 history)后 root 下文件集合不变(零落盘)。
|
|
445
|
+
def gH():
|
|
446
|
+
print("== H 不建第二真源(无缓存 + 零落盘) ==")
|
|
447
|
+
cg = _fresh_cg()
|
|
448
|
+
root = cg.root
|
|
449
|
+
state_events.append(cg, "己", "住所", old=None, new="旧值",
|
|
450
|
+
kind="enablement", seq=51, evidence="h1")
|
|
451
|
+
u1 = _one(cg, "己", "住所")
|
|
452
|
+
ok(u1 and u1["value"] == "旧值" and u1["event_count"] == 1,
|
|
453
|
+
"H1 首次投影=旧值(count=1)", u1)
|
|
454
|
+
before = sorted(os.listdir(root))
|
|
455
|
+
state_events.append(cg, "己", "住所", old="旧值", new="新值",
|
|
456
|
+
kind="migration", seq=52, evidence="h2")
|
|
457
|
+
u2 = _one(cg, "己", "住所")
|
|
458
|
+
ok(u2 and u2["value"] == "新值" and u2["event_count"] == 2,
|
|
459
|
+
"H2 台账追加一条 → 再 project 结果即变(查询时现算,无缓存)", u2)
|
|
460
|
+
ok(u1["value"] == "旧值" and u1["event_count"] == 1,
|
|
461
|
+
"H3 前次返回的单元对象未被就地改写(无跨调用共享可变状态)", u1)
|
|
462
|
+
_one(cg, "己", "住所")
|
|
463
|
+
_ss().status(cg)
|
|
464
|
+
_one(cg, "己", "住所", history=False)
|
|
465
|
+
after = sorted(os.listdir(root))
|
|
466
|
+
ok(after == before,
|
|
467
|
+
"H4 投影零落盘:root 下文件集合不变(不建物化表/缓存文件)",
|
|
468
|
+
(after, before))
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# ---------------------------------------------------------------- I
|
|
472
|
+
# 生效条件:无入参;在自有沙箱根上追加一行坏 JSON,断言 project 不崩且好行照常、BAD_ROWS 恰 +1(一次 read 记一次)、status.bad_rows 与 state_events.BAD_ROWS 同源、坏行不计入 events/subjects/slots。
|
|
473
|
+
def gI():
|
|
474
|
+
print("== I 坏行可观测(沿 state_events 口径) ==")
|
|
475
|
+
cg = _fresh_cg()
|
|
476
|
+
state_events.append(cg, "庚", "住所", old=None, new="好值",
|
|
477
|
+
kind="enablement", seq=61, evidence="i1")
|
|
478
|
+
p = state_events.ledger_path(cg)
|
|
479
|
+
with open(p, "a", encoding="utf-8") as f:
|
|
480
|
+
f.write("{坏行 json\n")
|
|
481
|
+
n0 = state_events.BAD_ROWS
|
|
482
|
+
us = _ss().project(cg) # 不崩
|
|
483
|
+
ok(len(us) == 1 and us[0]["value"] == "好值",
|
|
484
|
+
"I1 坏行不中断:好行照常投影", us)
|
|
485
|
+
ok(state_events.BAD_ROWS == n0 + 1,
|
|
486
|
+
"I2 BAD_ROWS 递增 +1(一次 read 记一次,同一坏行每次计)",
|
|
487
|
+
state_events.BAD_ROWS - n0)
|
|
488
|
+
st = _ss().status(cg)
|
|
489
|
+
ok(st["bad_rows"] == state_events.BAD_ROWS,
|
|
490
|
+
"I3 status.bad_rows = state_events.BAD_ROWS(同源读数,不另计数)",
|
|
491
|
+
(st["bad_rows"], state_events.BAD_ROWS))
|
|
492
|
+
ok(st["events"] == 1 and st["subjects"] == 1 and st["slots"] == 1,
|
|
493
|
+
"I4 坏行不计入 events/subjects/slots", st)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---------------------------------------------------------------- J
|
|
497
|
+
# 生效条件:无入参;在自有沙箱根上经 mcp_server._stg_call 断言:缺 op / 未知 op 的允许枚举含 state_chain、缺省 limit=50 与 history 缺省 true、关臂 disabled 同形、开臂与直调逐位一致、TOOLS 里 stg 的 description 与 inputSchema(subject/slot/include_retired/history + op 枚举)到位;组末 finally 复开关。
|
|
498
|
+
def gJ():
|
|
499
|
+
print("== J MCP 面接线(_stg_call / TOOLS schema) ==")
|
|
500
|
+
from . import mcp_server as MS
|
|
501
|
+
cg = _fresh_cg()
|
|
502
|
+
state_events.append(cg, "辛", "住所", old=None, new="甲板", kind="enablement",
|
|
503
|
+
seq=71, evidence="j1")
|
|
504
|
+
state_events.append(cg, "辛", "住所", old="甲板", new="舱室",
|
|
505
|
+
kind="migration", seq=72, evidence="j2")
|
|
506
|
+
os.environ.pop("MDCG_STG_STATE", None)
|
|
507
|
+
try:
|
|
508
|
+
off = MS._stg_call(cg, {"op": "state_chain"})
|
|
509
|
+
ok(off.get("error") == "disabled",
|
|
510
|
+
"J1 经 MCP 面(_stg_call)开关关 → disabled 体(不抛)", off)
|
|
511
|
+
finally:
|
|
512
|
+
os.environ.pop("MDCG_STG_STATE", None)
|
|
513
|
+
e1 = e2 = None
|
|
514
|
+
try:
|
|
515
|
+
MS._stg_call(cg, {"op": ""})
|
|
516
|
+
except ValueError as exc:
|
|
517
|
+
e1 = str(exc)
|
|
518
|
+
try:
|
|
519
|
+
MS._stg_call(cg, {"op": "not_an_op"})
|
|
520
|
+
except ValueError as exc:
|
|
521
|
+
e2 = str(exc)
|
|
522
|
+
ok(e1 is not None and "state_chain" in e1,
|
|
523
|
+
"J2 缺 op 的允许枚举含 state_chain(fail-closed 且提示可操作)", e1)
|
|
524
|
+
ok(e2 is not None and "state_chain" in e2,
|
|
525
|
+
"J3 未知 op 的允许枚举含 state_chain", e2)
|
|
526
|
+
os.environ["MDCG_STG_STATE"] = "1"
|
|
527
|
+
try:
|
|
528
|
+
on = MS._stg_call(cg, {"op": "state_chain", "subject": "辛"})
|
|
529
|
+
direct = _sg().state_chain(cg, subject="辛")
|
|
530
|
+
ok(on == direct, "J4 经 MCP 面与直调逐位一致(本层不裁剪/不改写)")
|
|
531
|
+
ok(on.get("limit") == 50 and on.get("count") == 1
|
|
532
|
+
and "history" in (on.get("items") or [{}])[0],
|
|
533
|
+
"J5 缺省 limit=50、history 缺省 true(逐参缺省与文档一致)",
|
|
534
|
+
{k: on.get(k) for k in ("limit", "count")})
|
|
535
|
+
tr = MS._stg_call(cg, {"op": "state_chain", "limit": 1,
|
|
536
|
+
"history": False})
|
|
537
|
+
ok(tr.get("truncated") is False and tr.get("kept") == 1
|
|
538
|
+
and "history" not in (tr.get("items") or [{}])[0],
|
|
539
|
+
"J6 history=false 透传(不附带变迁史)", {k: tr.get(k) for k in
|
|
540
|
+
("count", "kept", "truncated")})
|
|
541
|
+
finally:
|
|
542
|
+
os.environ.pop("MDCG_STG_STATE", None)
|
|
543
|
+
# stg 属 kernel 面(KERNEL_TOOLS),故按 ALL_TOOLS 查——只查 TOOLS 会假红。
|
|
544
|
+
stg_tool = next((t for t in MS.ALL_TOOLS if t.get("name") == "stg"), None)
|
|
545
|
+
desc = (stg_tool or {}).get("description") or ""
|
|
546
|
+
props = ((stg_tool or {}).get("inputSchema") or {}).get("properties") or {}
|
|
547
|
+
ok("op=state_chain" in desc and "MDCG_STG_STATE" in desc,
|
|
548
|
+
"J7 TOOLS 的 stg description 含 op=state_chain 与开关名", desc[-90:])
|
|
549
|
+
ok("state_chain" in (props.get("op") or {}).get("description", ""),
|
|
550
|
+
"J8 op 参数枚举含 state_chain(调用契约不落后于实现)",
|
|
551
|
+
(props.get("op") or {}).get("description"))
|
|
552
|
+
ok(all(k in props for k in ("subject", "slot", "include_retired",
|
|
553
|
+
"history")),
|
|
554
|
+
"J9 inputSchema 增参数 subject/slot/include_retired/history 到位",
|
|
555
|
+
sorted(props))
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
_GROUPS = (gA, gB, gC, gD, gE, gF, gG, gH, gI, gJ)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# 生效条件:无入参;逐组执行 _GROUPS(每组自行建沙箱根),组内抛异常记为一条失败并把栈尾打印出来(不中断其余组),返回失败条数。
|
|
562
|
+
def _run_groups() -> int:
|
|
563
|
+
_PASS.clear()
|
|
564
|
+
_FAIL.clear()
|
|
565
|
+
for g in _GROUPS:
|
|
566
|
+
try:
|
|
567
|
+
g()
|
|
568
|
+
except Exception as exc: # noqa: BLE001
|
|
569
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
570
|
+
import traceback
|
|
571
|
+
print(traceback.format_exc()[-900:])
|
|
572
|
+
return len(_FAIL)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# ---------------------------------------------------------------- 变异表
|
|
576
|
+
# 锚点 = (名字, rel, ((old, new), …))。每条必须让套件**转红**(红项原文由 --mutate 打印):
|
|
577
|
+
# M1 现值改取首值(不随事件推进)→ A2/A4 与 C6(history↔value 互证)红
|
|
578
|
+
# M2 撤回不清空现值且 state 恒 active → A3 与 B1 红
|
|
579
|
+
# M3 t_to/seq_to 恒 None(不闭合)→ B3/B6 红
|
|
580
|
+
# M4 删开关判断(MDCG_STG_STATE 失去作用)→ F1/F2/F3 红
|
|
581
|
+
_MUTATIONS = (
|
|
582
|
+
("现值改取首值(不随事件推进)", "md_cg/state_slots.py",
|
|
583
|
+
((' value = rec.get("new")\n last_valued = i',
|
|
584
|
+
' value = recs[0].get("new") if value is None else value\n'
|
|
585
|
+
' last_valued = i'),)),
|
|
586
|
+
("撤回不清空现值且 state 恒 active", "md_cg/state_slots.py",
|
|
587
|
+
((' value = None # 撤回:值作废、无新值',
|
|
588
|
+
' value = value # 变异:撤回不清空现值'),
|
|
589
|
+
(' retired = _is_void(last) # state:最后一个事件的形态决定',
|
|
590
|
+
' retired = False # 变异:state 恒 active'))),
|
|
591
|
+
("t_to/seq_to 恒 None(不闭合)", "md_cg/state_slots.py",
|
|
592
|
+
((' if nxt is not None and 0 <= nxt < len(recs):\n'
|
|
593
|
+
' t_to, seq_to = recs[nxt].get("t"), recs[nxt].get("seq")\n'
|
|
594
|
+
' else:\n'
|
|
595
|
+
' t_to, seq_to = None, None',
|
|
596
|
+
' t_to, seq_to = None, None'),)),
|
|
597
|
+
("删开关判断(MDCG_STG_STATE 失效)", "md_cg/stg.py",
|
|
598
|
+
((' if not _flag_on(_STATE_ENV):\n'
|
|
599
|
+
' return {"error": "disabled",',
|
|
600
|
+
' if False:\n'
|
|
601
|
+
' return {"error": "disabled",'),)),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# 生效条件:rel 为仓根相对路径时返回该文件的当前源码文本(每次从盘读,不缓存——变异基线必须是**当前工作区**实现)。
|
|
606
|
+
def _rel_text(rel: str) -> str:
|
|
607
|
+
with open(os.path.join(_REPO, rel), encoding="utf-8") as f:
|
|
608
|
+
return f.read()
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
# 生效条件:name/rel/text 给定时——把 text 以 __package__="md_cg"、__file__=<仓内 rel 路径> 执行成命名空间并包成新模块对象返回(不注册 sys.modules、不触发 __main__;语法错误原样抛)。
|
|
612
|
+
def _exec_module(name: str, rel: str, text: str):
|
|
613
|
+
ns = {"__name__": name, "__package__": "md_cg",
|
|
614
|
+
"__file__": os.path.join(_REPO, rel)}
|
|
615
|
+
exec(compile(text, rel, "exec"), ns) # noqa: S102 —— 基线自证用
|
|
616
|
+
m = types.ModuleType(name)
|
|
617
|
+
m.__dict__.update(ns)
|
|
618
|
+
return m
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# 生效条件:list_only 为真时只打印变异表并返回 0;否则先跑未变异基线(必须 0 失败),再逐条在**内存副本**上注入变异(逐锚点 replace,锚点缺失记 ANCHOR-MISS)后重跑整套分组——有红项记 OK、无红项记 MISS(空转即失败);全部锚点命中有红项时返回 0、否则 1;有 ANCHOR-MISS 时返回 2(fail-closed)。
|
|
622
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
623
|
+
print("!! 定点变异自证:逐条把机制改回「错误/缺陷」形态,套件必须转红\n")
|
|
624
|
+
if list_only:
|
|
625
|
+
for name, rel, _pairs in _MUTATIONS:
|
|
626
|
+
print(" %-34s [%s]" % (name, rel))
|
|
627
|
+
return 0
|
|
628
|
+
anchor_miss, bad = [], []
|
|
629
|
+
with contextlib.redirect_stdout(buf0 := io.StringIO()):
|
|
630
|
+
clean = _run_groups()
|
|
631
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
632
|
+
if clean:
|
|
633
|
+
bad.append("未变异基线即失败")
|
|
634
|
+
for l in buf0.getvalue().splitlines(): # 基线红项逐条打印(可定位)
|
|
635
|
+
if l.strip().startswith("FAIL "):
|
|
636
|
+
print(" " + l.strip()[5:])
|
|
637
|
+
for name, rel, pairs in _MUTATIONS:
|
|
638
|
+
mut_src, miss = _rel_text(rel), []
|
|
639
|
+
for old, new in pairs:
|
|
640
|
+
if old not in mut_src:
|
|
641
|
+
miss.append(old.splitlines()[0].strip()[:48])
|
|
642
|
+
continue
|
|
643
|
+
mut_src = mut_src.replace(old, new, 1)
|
|
644
|
+
if miss:
|
|
645
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 源码里找不到(实现改了却没"
|
|
646
|
+
"同步本表):%s" % (name, rel, miss))
|
|
647
|
+
anchor_miss.append(name)
|
|
648
|
+
continue
|
|
649
|
+
kind = "SS" if rel.endswith("state_slots.py") else "SG"
|
|
650
|
+
try:
|
|
651
|
+
globals()[kind] = _exec_module("md_cg._ss_mut", rel, mut_src)
|
|
652
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
653
|
+
reds = _run_groups()
|
|
654
|
+
detail = buf.getvalue()
|
|
655
|
+
finally:
|
|
656
|
+
globals()["SS"], globals()["SG"] = _REAL_SS, _REAL_SG
|
|
657
|
+
reds_lines = [l for l in detail.splitlines()
|
|
658
|
+
if l.strip().startswith("FAIL ")]
|
|
659
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
660
|
+
print(" %s %-34s 红项=%d %s"
|
|
661
|
+
% ("OK " if reds else "MISS ", name, reds, verdict))
|
|
662
|
+
for l in reds_lines[:6]:
|
|
663
|
+
print(" " + l.strip()[5:])
|
|
664
|
+
if not reds:
|
|
665
|
+
bad.append(name)
|
|
666
|
+
if anchor_miss:
|
|
667
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
668
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
669
|
+
return 2
|
|
670
|
+
print("\n定点变异自证:%s"
|
|
671
|
+
% ("PASS(每处机制都有断言把它钉死)" if not bad
|
|
672
|
+
else "FAIL —— " + "、".join(bad)))
|
|
673
|
+
return 0 if not bad else 1
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def main() -> int:
|
|
677
|
+
_sandbox_env()
|
|
678
|
+
try:
|
|
679
|
+
if "--mutate" in sys.argv:
|
|
680
|
+
return _mutate_mode("--list" in sys.argv)
|
|
681
|
+
n = _run_groups()
|
|
682
|
+
print("\n状态槽位投影守卫:%d 通过 / %d 失败" % (len(_PASS), n))
|
|
683
|
+
print("SUMMARY: md_cg.test_state_slots 通过=%d 失败=%d" % (len(_PASS), n))
|
|
684
|
+
return 0 if not n else 1
|
|
685
|
+
finally:
|
|
686
|
+
_restore_env()
|
|
687
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
if __name__ == "__main__":
|
|
691
|
+
sys.exit(main())
|