@furongjun1999/dsh-memory 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -18
- package/docs/README.md +1 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
- package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
- package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
- package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
- package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
- package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
- package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
- package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +527 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +191 -26
- package/md_cg/mdcg.py +594 -39
- package/md_cg/mdcos.py +869 -38
- package/md_cg/nodefile.py +74 -1
- package/md_cg/protect.py +79 -4
- package/md_cg/provenance.py +1 -1
- package/md_cg/review_cli.py +31 -2
- package/md_cg/rollback.py +411 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_autonomy_admission.py +1098 -0
- package/md_cg/test_autonomy_modes.py +1972 -0
- package/md_cg/test_b1_auto_id_multiproc.py +7 -0
- package/md_cg/test_b1b2_write_face.py +7 -0
- package/md_cg/test_b3_merge_keeps_content.py +7 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_i50a_half_dup_defer.py +408 -0
- package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
- package/md_cg/test_i50c_meta_passthrough.py +608 -0
- package/md_cg/test_i50d_importance_source.py +531 -0
- package/md_cg/test_i50e_readside_protection.py +656 -0
- package/md_cg/test_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -0
- package/md_cg/test_linkref.py +7 -0
- package/md_cg/test_mode_parity.py +1393 -0
- package/md_cg/test_mutation_rollback.py +805 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9_forget_protect.py +7 -0
- package/md_cg/test_p9c_dedup_hints.py +7 -0
- package/md_cg/test_policy_required_ccg.py +5 -1
- package/md_cg/test_protocol.py +7 -0
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/writepipe.py +101 -2
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""issue #52 守卫:stg「条件先行于限额」+「截断可观测」(第 1/2 层)。
|
|
3
|
+
|
|
4
|
+
背景(外部报告 bixianzuose,2026-10-03 核实成立):`md_cg/stg.py::_scan` 旧实现
|
|
5
|
+
`list(cg.index["nodes"].items())[:max_scan]`(max_scan 默认 5000)在**条件过滤
|
|
6
|
+
之前**按 id 字典序切片 ⇒ 库超 5000 节点后:
|
|
7
|
+
(a) 条件命中(本会话记忆/layer/时间窗)落在切片外即被**永久排除**(在役库实测
|
|
8
|
+
`timeline(session=…)` 默认口径 count=25 且全为 code_*,被切 204 条含 15 条
|
|
9
|
+
mem_*;今天新归档的记忆一条都召不回);
|
|
10
|
+
(b) 选面按 id 字典序(无意义序)——新写入节点恰在索引尾部必被切;
|
|
11
|
+
(c) 截断完全静默(返回体无任何标记)——「读不全」与「读不到」不可区分;
|
|
12
|
+
(d) 被排除集合随索引重排漂移(运行期 dict 追加 vs 重建后 sorted 写盘)。
|
|
13
|
+
三接口 timeline/anchors/consistency 共用 `_scan` 同病;MCP 面 `_stg_call` 不透传
|
|
14
|
+
max_scan(永远默认口径);DSH 侧 hooks.ts:423 auto-recall 直连 timeline 默认口径。
|
|
15
|
+
|
|
16
|
+
本批修法(设计者裁定 2026-10-03,原文):「遇到更多的检索节点,应该要建立索引,
|
|
17
|
+
明确检索条件,和不适用条件,而不是扩大节点数」⇒ 只落「条件先行 + 截断可观测」
|
|
18
|
+
两层。【禁止面】max_scan 数值不放大;默认不改 None(全量);时间倒序只作**截断
|
|
19
|
+
发生时的兜底选序**,不作条件先行的替代。第 3 层(session/layer/time 结构索引 +
|
|
20
|
+
条件资格进 stg)另立 `docs/plans/stg条件化与结构索引_设计_v0.1.md`。
|
|
21
|
+
|
|
22
|
+
断言分组(每组判据都有定点变异把它打红,见 `_MUTATIONS`):
|
|
23
|
+
G1 条件先行:条件命中不再被索引序切片吞掉——含 >5000 的**真实规模**场景
|
|
24
|
+
(只带 index 的最小 cg 替身,5001 条目走快照路径)与「追加序 / 重建序」两形态;
|
|
25
|
+
G2 截断可观测:truncated/scanned/kept/hint 在场且取值正确(hint 含「细化生效
|
|
26
|
+
条件/不适用条件」「条件索引」语义);count 恒为条件命中总数(在役对照
|
|
27
|
+
25→229 的合成等价);MCP 面 stg 透传同一批读数;
|
|
28
|
+
G3 兜底选序:命中集超限时按时间倒序保留近期(无时间区间者先被截);
|
|
29
|
+
G4 语义红线:max_scan 默认 5000 不动、旧调用签名兼容、desc/limit/session="*"
|
|
30
|
+
既有语义不变、_sec 可见性单点先于截断(不占 kept 名额)、旧快照回退读文件
|
|
31
|
+
路径在、layer 过滤在;
|
|
32
|
+
G5 结构:`_scan` 内不再有索引序切片(AST 判据,非源文本在场)。
|
|
33
|
+
|
|
34
|
+
判别力自证(与 `md_cg/test_neg_condition_hits.py` 同口径):
|
|
35
|
+
· `--branch-baseline`:逐处**定点变异**(源码级,含契约点名的三处:条件过滤移回
|
|
36
|
+
截断之后 / 去掉 truncated 上报 / 截断选序改回字典序,另加 _scan 切片回退、
|
|
37
|
+
默认值改全量、_sec 闸绕过、快照回退删分支、layer 过滤失效共八处)——每处必须
|
|
38
|
+
**恰好**打红预期条数的断言(多一条少一条都报红),且**所有**断言至少被一处
|
|
39
|
+
变异打红(一条都没被打红的=空转,直接判 FAIL)。锚点漂移报 ANCHOR-MISS 且
|
|
40
|
+
**退出码 2**(fail-closed,不静默失效)。
|
|
41
|
+
· `--legacy-baseline`:把「修前形态」(切片先于条件 + 无截断上报)在运行时源码上
|
|
42
|
+
原地重建,断言必须转红——证明本守卫抓得住 #52 这个缺陷本身,而非只抓得住
|
|
43
|
+
我们自造的变异。
|
|
44
|
+
· 基线源=**运行时源码**(`inspect.getsource(stg)`),**不是 git HEAD**——基线绑
|
|
45
|
+
提交即失效(本仓已有两次教训);锚点都写在 `_MUTATIONS` 里,实现改动致锚点
|
|
46
|
+
漂移时会当场报 ANCHOR-MISS。
|
|
47
|
+
|
|
48
|
+
运行:python -X utf8 -m md_cg.test_issue52_scan_condition_first
|
|
49
|
+
python -X utf8 -m md_cg.test_issue52_scan_condition_first --branch-baseline
|
|
50
|
+
python -X utf8 -m md_cg.test_issue52_scan_condition_first --legacy-baseline
|
|
51
|
+
"""
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import ast
|
|
55
|
+
import contextlib
|
|
56
|
+
import inspect
|
|
57
|
+
import io
|
|
58
|
+
import os
|
|
59
|
+
import shutil
|
|
60
|
+
import sys
|
|
61
|
+
import tempfile
|
|
62
|
+
import types
|
|
63
|
+
|
|
64
|
+
from . import stg as _stg
|
|
65
|
+
from .mdcos import MdCGOS, MdCGSecure
|
|
66
|
+
from .security import Principal
|
|
67
|
+
|
|
68
|
+
S = _stg # 当前被测 STG 模块(变异模式 = exec 出的变异体命名空间)
|
|
69
|
+
_CUR_SRC = None # 变异模式下当前的变异体源码(G5 的 AST 判据取它)
|
|
70
|
+
_PASS = []
|
|
71
|
+
_FAIL = []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def ok(cond, label, extra=""):
|
|
75
|
+
(_PASS if cond else _FAIL).append(label)
|
|
76
|
+
print((" PASS " if cond else " FAIL ") + label +
|
|
77
|
+
((" ← " + str(extra)[:200]) if (extra and not cond) else ""))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@contextlib.contextmanager
|
|
81
|
+
def _quiet():
|
|
82
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
83
|
+
yield
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _ids(body):
|
|
87
|
+
return [i["id"] for i in (body.get("items") or [])]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------- 合成库(系统临时目录)
|
|
91
|
+
# 硬边界:不动任何在役数据根——全部库建在 tempfile.mkdtemp 下,用完即删。
|
|
92
|
+
|
|
93
|
+
def _mk_small(root, name, rebuild=False):
|
|
94
|
+
"""8 节点:6 填充(session=other-sess,id 字典序在前)+ 2 目标(session=target-sess,
|
|
95
|
+
id 字典序在后)。max_scan=3 的显式小值=「库超 5000」的等价放大(id 序尾部必被切)。"""
|
|
96
|
+
cg = MdCGOS(os.path.join(root, name))
|
|
97
|
+
for i in range(6):
|
|
98
|
+
cg.add("a%02d" % i, "# 功能名:填充 %d\n# 正文:填充节点 %d" % (i, i),
|
|
99
|
+
layer="knowledge", temporal=1000.0 + i, session="other-sess")
|
|
100
|
+
for i in range(2):
|
|
101
|
+
cg.add("zz_target_%d" % i,
|
|
102
|
+
"# 功能名:目标 %d\n# 正文:目标节点 %d" % (i, i),
|
|
103
|
+
layer="knowledge", temporal=5000.0 + i, session="target-sess")
|
|
104
|
+
cg.flush()
|
|
105
|
+
if rebuild:
|
|
106
|
+
cg.rebuild_index() # 索引序=字典序(sorted 写盘)
|
|
107
|
+
return cg
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _mk_layers(root, name):
|
|
111
|
+
"""5 节点:3 条 anchor 层 + 2 条 knowledge 层(同会话,id 前缀区分层序)。"""
|
|
112
|
+
cg = MdCGOS(os.path.join(root, name))
|
|
113
|
+
for i in range(3):
|
|
114
|
+
cg.add("anc_%d" % i, "# 功能名:锚 %d\n# 正文:锚节点 %d" % (i, i),
|
|
115
|
+
layer="anchor", temporal=3000.0 + i, session="sess_L")
|
|
116
|
+
for i in range(2):
|
|
117
|
+
cg.add("kno_%d" % i, "# 功能名:知 %d\n# 正文:知识节点 %d" % (i, i),
|
|
118
|
+
layer="knowledge", temporal=2000.0 + i, session="sess_L")
|
|
119
|
+
cg.flush()
|
|
120
|
+
return cg
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _mk_trunc(root, name):
|
|
124
|
+
"""13 节点:3 填充(session=other-sess)+ 10 命中(session=sess_T,temporal 递增,
|
|
125
|
+
id 形如 t00..t09——字典序与时间序同向,故「字典序切片」保最旧、「时间倒序」保最新,
|
|
126
|
+
两侧可判别)。"""
|
|
127
|
+
cg = MdCGOS(os.path.join(root, name))
|
|
128
|
+
for i in range(3):
|
|
129
|
+
cg.add("a_fill_%d" % i, "# 功能名:填充 %d\n# 正文:填充" % i,
|
|
130
|
+
layer="knowledge", temporal=100.0 + i, session="other-sess")
|
|
131
|
+
for i in range(10):
|
|
132
|
+
cg.add("t%02d" % i, "# 功能名:命中 %d\n# 正文:命中节点 %d" % (i, i),
|
|
133
|
+
layer="knowledge", temporal=1000.0 + i, session="sess_T")
|
|
134
|
+
cg.flush()
|
|
135
|
+
return cg
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _mk_anchor_bbox(root, name):
|
|
139
|
+
"""10 节点:4 条无时间(仅 spatial bbox)+ 6 条有 temporal(bbox 同名)。
|
|
140
|
+
空间查询 bbox=[0,0,100,100] 时 10 条全是条件命中;截断时无时间者应先被截。
|
|
141
|
+
注:add() 对未给 time_window 的节点会以**写入时刻**自动填充(_interval 注释
|
|
142
|
+
记载的既有行为),故 4 条「无时间」节点须把索引快照里的 time_window 键摘掉
|
|
143
|
+
——快照路径由此得到 iv=None(幂等:重复跑无副作用)。"""
|
|
144
|
+
cg = MdCGOS(os.path.join(root, name))
|
|
145
|
+
for i in range(4):
|
|
146
|
+
cg.add("a_notime_%d" % i, "# 功能名:无时间 %d\n# 正文:无时间节点" % i,
|
|
147
|
+
layer="knowledge", spatial={"bbox": [1, 1, 9, 9]}, session="sess_B")
|
|
148
|
+
for i in range(6):
|
|
149
|
+
cg.add("z_timed_%d" % i, "# 功能名:有时间 %d\n# 正文:有时间节点" % i,
|
|
150
|
+
layer="knowledge", temporal=2000.0 + i,
|
|
151
|
+
spatial={"bbox": [1, 1, 9, 9]}, session="sess_B")
|
|
152
|
+
cg.flush()
|
|
153
|
+
for i in range(4):
|
|
154
|
+
cg.index["nodes"]["a_notime_%d" % i].pop("time_window", None)
|
|
155
|
+
return cg
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _mk_cons_recency(root, name):
|
|
159
|
+
"""10 节点:7 条自洽(temporal=i)+ 3 条**最新**且时间倒置(condition_space.time_window
|
|
160
|
+
[9000,8000])。consistency 截断到 3 时,保留的应是最近 3 条 ⇒ issues 恰为这 3 条。"""
|
|
161
|
+
cg = MdCGOS(os.path.join(root, name))
|
|
162
|
+
for i in range(7):
|
|
163
|
+
cg.add("c%02d" % i, "# 功能名:自洽 %d\n# 正文:自洽节点 %d" % (i, i),
|
|
164
|
+
layer="knowledge", temporal=1000.0 + i, session="sess_C")
|
|
165
|
+
for i in range(7, 10):
|
|
166
|
+
cg.add("c%02d" % i, "# 功能名:倒置 %d\n# 正文:倒置节点 %d" % (i, i),
|
|
167
|
+
layer="knowledge", session="sess_C",
|
|
168
|
+
condition_space={"time_window": [9000 + i, 8000 + i]})
|
|
169
|
+
cg.flush()
|
|
170
|
+
return cg
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _mk_live(root, name, n_hits=229, n_fill=3):
|
|
174
|
+
"""在役对照的合成等价:229 条 session=sess_live 命中 + 3 条异会话填充。"""
|
|
175
|
+
cg = MdCGOS(os.path.join(root, name))
|
|
176
|
+
for i in range(n_fill):
|
|
177
|
+
cg.add("a_fill_%d" % i, "# 功能名:填充 %d\n# 正文:填充" % i,
|
|
178
|
+
layer="knowledge", temporal=1.0 + i, session="other-sess")
|
|
179
|
+
for i in range(n_hits):
|
|
180
|
+
cg.add("m%04d" % i, "# 功能名:在役对照 %d\n# 正文:对照节点 %d" % (i, i),
|
|
181
|
+
layer="knowledge", temporal=1000.0 + i, session="sess_live")
|
|
182
|
+
cg.flush()
|
|
183
|
+
return cg
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _mk_secure(root, name):
|
|
187
|
+
"""可见性红线库:3 条 internal(跨会话共享)+ 2 条 private(绑定 sess_A)。
|
|
188
|
+
读侧是 sess_B / clearance=internal / 非 admin ⇒ 只应看见 3 条。"""
|
|
189
|
+
root = os.path.join(root, name)
|
|
190
|
+
wr = Principal(actor="i52wr", clearance="private", can_write=True,
|
|
191
|
+
can_admin=False, role="designer", session="sess_A",
|
|
192
|
+
harness="test-harness")
|
|
193
|
+
w = MdCGSecure(root, principal=wr)
|
|
194
|
+
for i in range(3):
|
|
195
|
+
w.add("v_pub_%d" % i, "# 功能名:共享 %d\n# 正文:共享节点 %d" % (i, i),
|
|
196
|
+
layer="knowledge", temporal=4000.0 + i, session="sess_A")
|
|
197
|
+
for i in range(2):
|
|
198
|
+
w.add("v_priv_%d" % i, "# 功能名:私有 %d\n# 正文:私有节点 %d" % (i, i),
|
|
199
|
+
layer="knowledge", temporal=4500.0 + i, session="sess_A",
|
|
200
|
+
sensitivity="private")
|
|
201
|
+
w.flush()
|
|
202
|
+
rd = Principal(actor="i52rd", clearance="internal", can_write=False,
|
|
203
|
+
can_admin=False, role="designer", session="sess_B",
|
|
204
|
+
harness="test-harness")
|
|
205
|
+
return MdCGSecure(root, principal=rd)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _mk_snapshot_legacy(root, name):
|
|
209
|
+
"""旧快照形态库:构造后把索引条目的 temporal/spatial 键摘掉 ⇒ 走「回退读文件」路径。"""
|
|
210
|
+
cg = MdCGOS(os.path.join(root, name))
|
|
211
|
+
cg.add("a_fill", "# 功能名:填充\n# 正文:填充节点", layer="knowledge",
|
|
212
|
+
temporal=900.0, session="other-sess")
|
|
213
|
+
for i in range(2):
|
|
214
|
+
cg.add("zz_target_%d" % i, "# 功能名:目标 %d\n# 正文:目标" % i,
|
|
215
|
+
layer="knowledge", temporal=5000.0 + i, session="target-sess")
|
|
216
|
+
cg.flush()
|
|
217
|
+
for e in list(cg.index["nodes"].values()): # 快照迭代(H-4(a):裸迭代即红)
|
|
218
|
+
e.pop("temporal", None)
|
|
219
|
+
e.pop("spatial", None)
|
|
220
|
+
return cg
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class _IndexOnly:
|
|
224
|
+
"""只带 index/_read/get 的最小 cg 替身——驱动 stg 的**快照路径**(不落盘、不建库)。
|
|
225
|
+
|
|
226
|
+
用途:把「库超 max_scan(默认 5000)」做到**真实规模**(5001 个索引条目,今日
|
|
227
|
+
新写入的目标节点在 id 字典序尾部)而不写 5001 个文件。除 `_read`(只被预览调用)
|
|
228
|
+
外,走的是与真库逐位相同的 stg 代码路径:`_scan` 全部命中快照分支(条目带
|
|
229
|
+
temporal 键,不触发回退读文件)。
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(self, entries):
|
|
233
|
+
self.index = {"nodes": entries}
|
|
234
|
+
|
|
235
|
+
def get(self, nid):
|
|
236
|
+
e = self.index["nodes"].get(nid)
|
|
237
|
+
return {"frontmatter": dict(e), "content": ""} if e else None
|
|
238
|
+
|
|
239
|
+
def _read(self, e):
|
|
240
|
+
return (dict(e), "")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _mk_index_only(n_bulk=5000):
|
|
244
|
+
"""5000 条旧节点(sess_other,id 形如 k00000 字典序在前)+ 1 条今日新记忆
|
|
245
|
+
(sess_today,id=zz_new_mem 字典序在**尾部**)。"""
|
|
246
|
+
entries = {}
|
|
247
|
+
for i in range(n_bulk):
|
|
248
|
+
entries["k%05d" % i] = {"layer": "knowledge", "path": "knowledge/k%d.md" % i,
|
|
249
|
+
"temporal": 1000.0 + i, "spatial": None,
|
|
250
|
+
"session": "sess_other", "time_window": None}
|
|
251
|
+
entries["zz_new_mem"] = {"layer": "knowledge", "path": "knowledge/zz_new_mem.md",
|
|
252
|
+
"temporal": 10 ** 6, "spatial": None,
|
|
253
|
+
"session": "sess_today", "time_window": None}
|
|
254
|
+
return _IndexOnly(entries)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _mk_index_mcp(n_hits=5100, n_fill=100):
|
|
258
|
+
"""MCP 面截断场景:5100 条 sess_mcp 命中(默认 max_scan=5000 下命中集超限)
|
|
259
|
+
+ 100 条异会话填充(id 字典序在前——M1 类切片在这些条目上先消耗限额)。"""
|
|
260
|
+
entries = {}
|
|
261
|
+
for i in range(n_fill):
|
|
262
|
+
entries["a_fill_%03d" % i] = {"layer": "knowledge",
|
|
263
|
+
"path": "knowledge/af%d.md" % i,
|
|
264
|
+
"temporal": 10.0 + i, "spatial": None,
|
|
265
|
+
"session": "sess_other", "time_window": None}
|
|
266
|
+
for i in range(n_hits):
|
|
267
|
+
entries["m%05d" % i] = {"layer": "knowledge", "path": "knowledge/m%d.md" % i,
|
|
268
|
+
"temporal": 1000.0 + i, "spatial": None,
|
|
269
|
+
"session": "sess_mcp", "time_window": None}
|
|
270
|
+
return _IndexOnly(entries)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class _Fixtures:
|
|
274
|
+
"""一次构建、多轮复用(变异核验要重跑 N 轮)——库面与变异面正交(变异只改 stg 源码)。
|
|
275
|
+
|
|
276
|
+
库都是只读消费(唯一例外 _mk_snapshot_legacy 的摘键是幂等的,第二次跑无副作用)。
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self):
|
|
280
|
+
self.tmp = tempfile.mkdtemp(prefix="mdcg_i52_")
|
|
281
|
+
self.small = _mk_small(self.tmp, "small") # 追加序
|
|
282
|
+
self.small_rb = _mk_small(self.tmp, "small_rb", rebuild=True) # 重建序
|
|
283
|
+
self.layers = _mk_layers(self.tmp, "layers")
|
|
284
|
+
self.trunc = _mk_trunc(self.tmp, "trunc")
|
|
285
|
+
self.abox = _mk_anchor_bbox(self.tmp, "abox")
|
|
286
|
+
self.cons = _mk_cons_recency(self.tmp, "cons")
|
|
287
|
+
self.live = _mk_live(self.tmp, "live")
|
|
288
|
+
self.secure = _mk_secure(self.tmp, "secure")
|
|
289
|
+
self.snap = _mk_snapshot_legacy(self.tmp, "snap")
|
|
290
|
+
self.index_only = _mk_index_only()
|
|
291
|
+
self.index_mcp = _mk_index_mcp()
|
|
292
|
+
|
|
293
|
+
def close(self):
|
|
294
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------- G1 条件先行
|
|
298
|
+
def g1_condition_first(fx):
|
|
299
|
+
print("== G1 条件先行:条件命中不再被索引序切片吞掉 ==")
|
|
300
|
+
tl = S.timeline(fx.small, session="target-sess", max_scan=3)
|
|
301
|
+
ok(tl.get("count") == 2 and _ids(tl) == ["zz_target_1", "zz_target_0"],
|
|
302
|
+
"A1 运行期追加序:会话条件命中不被切片吞掉(count=2、两条目标都在;修前 count=0)",
|
|
303
|
+
(tl.get("count"), _ids(tl)))
|
|
304
|
+
|
|
305
|
+
tl_rb = S.timeline(fx.small_rb, session="target-sess", max_scan=3)
|
|
306
|
+
ok(_ids(tl_rb) == ["zz_target_1", "zz_target_0"] and _ids(tl_rb) == _ids(tl),
|
|
307
|
+
"A2 重建索引(字典序写盘)后逐位一致——被排除集合不随索引重排漂移",
|
|
308
|
+
_ids(tl_rb))
|
|
309
|
+
|
|
310
|
+
tl_cross = S.timeline(fx.small, max_scan=3)
|
|
311
|
+
ok(tl_cross.get("count") == 8,
|
|
312
|
+
"A3 跨会话视图 count=全库 8(修前被静默切为 3)", tl_cross.get("count"))
|
|
313
|
+
|
|
314
|
+
an = S.anchors(fx.small, time_window=[4900, 5100], max_scan=3)
|
|
315
|
+
ok(an.get("count") == 2 and set(_ids(an)) == {"zz_target_0", "zz_target_1"},
|
|
316
|
+
"A4 anchors 时间窗条件命中同样先于限额(count=2;修前 count=0)",
|
|
317
|
+
(an.get("count"), _ids(an)))
|
|
318
|
+
|
|
319
|
+
co = S.consistency(fx.small, max_scan=3)
|
|
320
|
+
ok(co.get("scanned") == 8,
|
|
321
|
+
"A5 consistency 遍历面=全库 8(修前 scanned=3,截断静默)", co.get("scanned"))
|
|
322
|
+
|
|
323
|
+
ok(S.timeline(fx.layers, layer="anchor", max_scan=3).get("count") == 3
|
|
324
|
+
and S.timeline(fx.layers, layer="knowledge", max_scan=3).get("count") == 2,
|
|
325
|
+
"A6 layer 面条件命中不被切片吞掉(anchor 3 / knowledge 2)",
|
|
326
|
+
(S.timeline(fx.layers, layer="anchor", max_scan=3).get("count"),
|
|
327
|
+
S.timeline(fx.layers, layer="knowledge", max_scan=3).get("count")))
|
|
328
|
+
|
|
329
|
+
# A7:**真实规模**(5001 索引条目,默认 max_scan=5000)——今日新记忆在 id 字典序尾部
|
|
330
|
+
tl_big = S.timeline(fx.index_only, session="sess_today")
|
|
331
|
+
ok(tl_big.get("count") == 1 and _ids(tl_big) == ["zz_new_mem"],
|
|
332
|
+
"A7 库超 5000:字典序尾部的今日新记忆仍召得回(修前 count=0——#52 的临床面)",
|
|
333
|
+
(tl_big.get("count"), _ids(tl_big)))
|
|
334
|
+
ok(tl_big.get("scanned") == 5001 and tl_big.get("kept") == 1
|
|
335
|
+
and tl_big.get("truncated") is False,
|
|
336
|
+
"A8 库超 5000:遍历读数=5001、命中未超限不标记截断",
|
|
337
|
+
{k: tl_big.get(k) for k in ("scanned", "kept", "truncated")})
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------- G2 截断可观测
|
|
341
|
+
def g2_truncation_reads(fx):
|
|
342
|
+
print("== G2 截断可观测:truncated/scanned/kept/hint + count 语义 ==")
|
|
343
|
+
tl = S.timeline(fx.trunc, session="sess_T", max_scan=4)
|
|
344
|
+
ok(tl.get("truncated") is True, "B1 命中集超限 ⇒ truncated=True(修前无此键)",
|
|
345
|
+
tl.get("truncated"))
|
|
346
|
+
ok(tl.get("kept") == 4 and tl.get("scanned") == 13,
|
|
347
|
+
"B2 读数:scanned=候选 13 / kept=截断后 4",
|
|
348
|
+
(tl.get("scanned"), tl.get("kept")))
|
|
349
|
+
hint = tl.get("hint") or ""
|
|
350
|
+
ok(bool(hint) and "细化生效条件" in hint and "不适用条件" in hint
|
|
351
|
+
and "条件索引" in hint,
|
|
352
|
+
"B3 截断附可操作 hint(细化生效条件/不适用条件 or 建立条件索引,禁止静默)",
|
|
353
|
+
hint)
|
|
354
|
+
ok(tl.get("count") == 10,
|
|
355
|
+
"B4 count=条件命中总数 10(不再受切片影响;修前 count=3)", tl.get("count"))
|
|
356
|
+
|
|
357
|
+
tl0 = S.timeline(fx.small, session="target-sess", max_scan=3)
|
|
358
|
+
ok(tl0.get("truncated") is False and "hint" not in tl0,
|
|
359
|
+
"B5 未超限:truncated=False 且不附 hint(逐位兼容)",
|
|
360
|
+
{k: tl0.get(k) for k in ("truncated", "hint")})
|
|
361
|
+
|
|
362
|
+
an = S.anchors(fx.abox, bbox=[0, 0, 100, 100], max_scan=3)
|
|
363
|
+
ok(an.get("truncated") is True and an.get("kept") == 3 and an.get("count") == 10,
|
|
364
|
+
"B6 anchors:truncated/kept/count 三读数在场且正确",
|
|
365
|
+
{k: an.get(k) for k in ("truncated", "kept", "count")})
|
|
366
|
+
|
|
367
|
+
co = S.consistency(fx.cons, max_scan=3)
|
|
368
|
+
ok(co.get("truncated") is True and co.get("kept") == 3 and co.get("scanned") == 10,
|
|
369
|
+
"B7 consistency:truncated/kept/scanned 三读数在场且正确",
|
|
370
|
+
{k: co.get(k) for k in ("truncated", "kept", "scanned")})
|
|
371
|
+
|
|
372
|
+
tl_live = S.timeline(fx.live, session="sess_live", max_scan=25)
|
|
373
|
+
ok(tl_live.get("count") == 229 and tl_live.get("kept") == 25
|
|
374
|
+
and tl_live.get("truncated") is True,
|
|
375
|
+
"B8 在役对照合成等价:count=229(修前 25)、kept=25、truncated=True",
|
|
376
|
+
{k: tl_live.get(k) for k in ("count", "kept", "truncated")})
|
|
377
|
+
|
|
378
|
+
# MCP 面:默认口径(max_scan 不在 MCP 入参面)下的截断——5100 条命中 > 5000,
|
|
379
|
+
# 读数随返回体自动透传(本层不裁剪)。入参里带 max_scan 本批也无效(契约不加)。
|
|
380
|
+
from .mcp_server import _stg_call
|
|
381
|
+
mcp = _stg_call(fx.index_mcp, {"op": "timeline", "session": "sess_mcp",
|
|
382
|
+
"limit": 3, "max_scan": 2})
|
|
383
|
+
ok(mcp.get("count") == 5100 and mcp.get("truncated") is True
|
|
384
|
+
and mcp.get("kept") == 5000 and mcp.get("scanned") == 5200
|
|
385
|
+
and bool(mcp.get("hint")) and len(mcp.get("items") or []) == 3,
|
|
386
|
+
"B9 MCP 面 stg:默认口径超限的截断读数随返回体透传(kept=5000 而非入参 2)",
|
|
387
|
+
{k: mcp.get(k) for k in ("count", "truncated", "kept", "scanned")})
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------- G3 兜底选序
|
|
391
|
+
def g3_recency(fx):
|
|
392
|
+
print("== G3 截断兜底选序:时间倒序保留近期(非主修法,只作超限时选序)==")
|
|
393
|
+
tl = S.timeline(fx.trunc, session="sess_T", max_scan=4)
|
|
394
|
+
ok(_ids(tl) == ["t09", "t08", "t07", "t06"],
|
|
395
|
+
"C1 timeline 超限保留最近 4 条(字典序切片会保最旧的 t00..t03)", _ids(tl))
|
|
396
|
+
|
|
397
|
+
an = S.anchors(fx.abox, bbox=[0, 0, 100, 100], max_scan=3)
|
|
398
|
+
ok(all(h.get("time") is not None for h in (an.get("items") or []))
|
|
399
|
+
and _ids(an) == ["z_timed_5", "z_timed_4", "z_timed_3"],
|
|
400
|
+
"C2 anchors 超限:无时间区间者先被截,保留最近 3 条定时命中",
|
|
401
|
+
[(h.get("id"), h.get("time")) for h in (an.get("items") or [])])
|
|
402
|
+
|
|
403
|
+
co = S.consistency(fx.cons, max_scan=3)
|
|
404
|
+
ok({i["id"] for i in (co.get("items") or [])} == {"c07", "c08", "c09"},
|
|
405
|
+
"C3 consistency 超限后检查的是最近 3 条(issues 恰为这 3 条倒置节点)",
|
|
406
|
+
[i["id"] for i in (co.get("items") or [])])
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# ---------------------------------------------------------------- G4 语义红线
|
|
410
|
+
def g4_redlines(fx):
|
|
411
|
+
print("== G4 语义红线:默认值/签名兼容/既有语义/可见性/回退路径 ==")
|
|
412
|
+
for fn, label in ((S.timeline, "D1a timeline"), (S.anchors, "D1b anchors"),
|
|
413
|
+
(S.consistency, "D1c consistency")):
|
|
414
|
+
d = inspect.signature(fn).parameters["max_scan"].default
|
|
415
|
+
ok(d == 5000, "%s max_scan 默认 5000 保留(禁止调大/禁止默认 None)" % label, d)
|
|
416
|
+
|
|
417
|
+
out = S.timeline(fx.small, limit=5)
|
|
418
|
+
ok("count" in out and "items" in out and out.get("truncated") is False
|
|
419
|
+
and isinstance(out.get("scanned"), int),
|
|
420
|
+
"D2a 旧调用签名兼容:不传 max_scan 正常返回且读数在场",
|
|
421
|
+
{k: out.get(k) for k in ("count", "truncated", "scanned")})
|
|
422
|
+
an = S.anchors(fx.small, time_window=[1000, 6000], limit=5)
|
|
423
|
+
ok(an.get("count") == 8 and an.get("truncated") is False,
|
|
424
|
+
"D2b anchors 旧调用兼容(count=8、未截断)",
|
|
425
|
+
{k: an.get(k) for k in ("count", "truncated")})
|
|
426
|
+
co = S.consistency(fx.small, limit=5)
|
|
427
|
+
ok(co.get("scanned") == 8 and isinstance(co.get("kept"), int)
|
|
428
|
+
and co.get("truncated") is False,
|
|
429
|
+
"D2c consistency 旧调用兼容(scanned=8、读数在场)",
|
|
430
|
+
{k: co.get(k) for k in ("scanned", "kept", "truncated")})
|
|
431
|
+
|
|
432
|
+
# D3 _sec 可见性单点:先于一切(含截断)——不可见节点不占 kept 名额、不泄露 id
|
|
433
|
+
sec = S.timeline(fx.secure, max_scan=2)
|
|
434
|
+
ok(sec.get("scanned") == 3, "D3a 可见性先于遍历读数:候选只有 3 条共享档",
|
|
435
|
+
sec.get("scanned"))
|
|
436
|
+
ok(sec.get("count") == 3, "D3b 命中数=可见面 3(私有档不进条件命中集)",
|
|
437
|
+
sec.get("count"))
|
|
438
|
+
ok(not ({"v_priv_0", "v_priv_1"} & set(_ids(sec))),
|
|
439
|
+
"D3c 绑定档 id 不出现在返回体(不泄露存在性)", _ids(sec))
|
|
440
|
+
ok(sec.get("kept") == 2 and sec.get("truncated") is True,
|
|
441
|
+
"D3d 截断名额只给可见候选(可见性判定不得重排为截断之后)",
|
|
442
|
+
{k: sec.get(k) for k in ("kept", "truncated")})
|
|
443
|
+
|
|
444
|
+
# D4 旧索引快照(无 temporal/spatial 键)回退读文件路径保持
|
|
445
|
+
snap = S.timeline(fx.snap, session="target-sess", max_scan=3)
|
|
446
|
+
ok(snap.get("count") == 2 and set(_ids(snap)) == {"zz_target_0", "zz_target_1"},
|
|
447
|
+
"D4 旧快照缺键回退读文件路径保持(命中仍可见)",
|
|
448
|
+
(snap.get("count"), _ids(snap)))
|
|
449
|
+
|
|
450
|
+
# D5 layer 过滤在(_scan 内,条件面之一)
|
|
451
|
+
ok(S.timeline(fx.layers, layer="anchor", max_scan=3).get("count") == 3
|
|
452
|
+
and S.timeline(fx.layers, layer="knowledge", max_scan=3).get("count") == 2,
|
|
453
|
+
"D5 layer 过滤仍生效(anchor 3 / knowledge 2)",
|
|
454
|
+
(S.timeline(fx.layers, layer="anchor", max_scan=3).get("count"),
|
|
455
|
+
S.timeline(fx.layers, layer="knowledge", max_scan=3).get("count")))
|
|
456
|
+
|
|
457
|
+
# D6 既有 desc/limit/session 语义逐位不变
|
|
458
|
+
z = S.timeline(fx.small, session="target-sess", limit=0, max_scan=3)
|
|
459
|
+
ok(z.get("items") == [] and z.get("count") == 2,
|
|
460
|
+
"D6a limit=0 取空 items 但 count 仍为条件命中总数", (z.get("items"), z.get("count")))
|
|
461
|
+
asc = S.timeline(fx.small, session="target-sess", desc=False, max_scan=3)
|
|
462
|
+
ok(_ids(asc) == ["zz_target_0", "zz_target_1"],
|
|
463
|
+
"D6b desc=False 升序((start,end,id) 序不变)", _ids(asc))
|
|
464
|
+
star = S.timeline(fx.small, session="*", max_scan=3)
|
|
465
|
+
ok(star.get("count") == 8 and star.get("count") == S.timeline(
|
|
466
|
+
fx.small, max_scan=3).get("count"),
|
|
467
|
+
"D6c session=\"*\" 与缺省同义(跨会话读全)", star.get("count"))
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# ---------------------------------------------------------------- G5 结构
|
|
471
|
+
def g5_structure(fx):
|
|
472
|
+
print("== G5 结构:_scan 内不再有索引序切片(AST 判据) ==")
|
|
473
|
+
src = _CUR_SRC if _CUR_SRC is not None else inspect.getsource(_stg)
|
|
474
|
+
fn = None
|
|
475
|
+
for node in ast.parse(src).body:
|
|
476
|
+
if isinstance(node, ast.FunctionDef) and node.name == "_scan":
|
|
477
|
+
fn = node
|
|
478
|
+
break
|
|
479
|
+
slices = [] if fn is None else [
|
|
480
|
+
n for n in ast.walk(fn)
|
|
481
|
+
if isinstance(n, ast.Subscript) and isinstance(n.slice, ast.Slice)]
|
|
482
|
+
ok(fn is not None and not slices,
|
|
483
|
+
"E1 _scan 存在且内无切片子表达式(旧 `list(...items())[:max_scan]` 回退即红;"
|
|
484
|
+
"定义消失同样红——不静默空转)",
|
|
485
|
+
(fn is None, [getattr(s, "lineno", "?") for s in slices]))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
_GROUPS = (g1_condition_first, g2_truncation_reads, g3_recency,
|
|
489
|
+
g4_redlines, g5_structure)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _run_groups(fx) -> list:
|
|
493
|
+
"""跑全部分组,返回失败标签列表(静默模式复用;单组异常记为 EXC 标签)。"""
|
|
494
|
+
_PASS.clear()
|
|
495
|
+
_FAIL.clear()
|
|
496
|
+
for g in _GROUPS:
|
|
497
|
+
try:
|
|
498
|
+
g(fx)
|
|
499
|
+
except Exception as e: # noqa: BLE001 变异可能炸——记标签不中断
|
|
500
|
+
label = "EXC:%s:%s" % (g.__name__, type(e).__name__)
|
|
501
|
+
_FAIL.append(label)
|
|
502
|
+
print(" FAIL " + label + " ← " + str(e)[:160])
|
|
503
|
+
return list(_FAIL)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# ---------------------------------------------------------------- 定点变异表
|
|
507
|
+
# 每条 = (名字, [(旧源码, 新源码), ...], 预期打红条数)。
|
|
508
|
+
# 锚点缺失 ⇒ ANCHOR-MISS + 退出码 2(fail-closed);预期条数不符 ⇒ 报红(「恰好」语义)。
|
|
509
|
+
_M1 = [
|
|
510
|
+
(" items = []\n for n in _scan(cg, layer=layer, nodes=pairs):",
|
|
511
|
+
" items = []\n for n in _scan(cg, layer=layer, nodes=pairs)[:max_scan]:"),
|
|
512
|
+
(" hits = []\n for n in _scan(cg, layer=layer, nodes=pairs):",
|
|
513
|
+
" hits = []\n for n in _scan(cg, layer=layer, nodes=pairs)[:max_scan]:"),
|
|
514
|
+
(" cand = list(_scan(cg, layer=layer, nodes=pairs))",
|
|
515
|
+
" cand = list(_scan(cg, layer=layer, nodes=pairs))[:max_scan]"),
|
|
516
|
+
]
|
|
517
|
+
_M2 = [
|
|
518
|
+
(" out.update({\"scanned\": scanned, \"kept\": kept, \"truncated\": truncated})\n"
|
|
519
|
+
" if truncated:\n"
|
|
520
|
+
" out[\"hint\"] = _CAP_HINT % (hits, max_scan, kept)\n",
|
|
521
|
+
" pass # 变异:去掉读数与 hint 上报\n"),
|
|
522
|
+
]
|
|
523
|
+
_M3 = [
|
|
524
|
+
("_cap_hits(items, max_scan, _tl_recent)",
|
|
525
|
+
"_cap_hits(items, max_scan, (lambda x: ()))"),
|
|
526
|
+
("_cap_hits(hits, max_scan, _an_recent)",
|
|
527
|
+
"_cap_hits(hits, max_scan, (lambda x: ()))"),
|
|
528
|
+
("_cap_hits(cand, max_scan, lambda n: _co_recent(n, time_axis))",
|
|
529
|
+
"_cap_hits(cand, max_scan, (lambda n: ()))"),
|
|
530
|
+
]
|
|
531
|
+
# 维护记录:2026-10-03 第 3 层(stgidx 批)改造 _scan 结构后,同步 ①/②/⑥/⑦/⑧ 五组锚
|
|
532
|
+
# ——ANCHOR-MISS 即是该信号(实现改动致锚漂移时当场报红 + 退出码 2,维护者按本表同步)。
|
|
533
|
+
_MUTATIONS = (
|
|
534
|
+
("①条件过滤移回截断之后(三接口候选集切片)", _M1, 24),
|
|
535
|
+
("②去掉 truncated 上报(读数/hint 一并消失)", _M2, 15),
|
|
536
|
+
("③截断选序改回字典序", _M3, 3),
|
|
537
|
+
("④_scan 恢复索引序切片(回退 5000)",
|
|
538
|
+
[(" for nid, e in list(cg.index[\"nodes\"].items()):",
|
|
539
|
+
" for nid, e in list(cg.index[\"nodes\"].items())[:5000]:")], 4),
|
|
540
|
+
("⑤max_scan 默认改 None(全量,契约禁止面)",
|
|
541
|
+
[("max_scan=5000", "max_scan=None")], 4),
|
|
542
|
+
("⑥_sec 可见性闸绕过",
|
|
543
|
+
[(" if _sec is not None and not _sec(e):\n return None",
|
|
544
|
+
" if False:\n return None")], 3),
|
|
545
|
+
("⑦去掉旧快照回退读文件分支",
|
|
546
|
+
[(" fm, _content = cg._read(e)",
|
|
547
|
+
" fm, _content = None, None")], 1),
|
|
548
|
+
("⑧layer 过滤失效",
|
|
549
|
+
[(" if layer and e.get(\"layer\") != layer:\n return None",
|
|
550
|
+
" if False:\n return None")], 2),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
_LEGACY_REPLS = _M1 + _M2 # 修前形态重建:切片先于条件 + 无截断上报
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _build_mutant(replacements):
|
|
557
|
+
"""在**运行时源码**上做替换并 exec 成模块命名空间。
|
|
558
|
+
|
|
559
|
+
返回 (attr_ns, raw_ns, src);锚点缺失返回 (None, None, None)。attr_ns 供 S.x
|
|
560
|
+
取用(函数各自的 globals 仍是 raw_ns ⇒ 变异体内部自洽)。
|
|
561
|
+
"""
|
|
562
|
+
src = _CUR_SRC if _CUR_SRC is not None else inspect.getsource(_stg)
|
|
563
|
+
for old, new in replacements:
|
|
564
|
+
if old not in src:
|
|
565
|
+
return None, None, None
|
|
566
|
+
src = src.replace(old, new)
|
|
567
|
+
ns = {"__name__": "md_cg._stg_mutant", "__package__": "md_cg",
|
|
568
|
+
"__file__": getattr(_stg, "__file__", "<stg>")}
|
|
569
|
+
exec(compile(src, "<stg-mutant>", "exec"), ns) # noqa: S102 测试内变异自证
|
|
570
|
+
return types.SimpleNamespace(**ns), ns, src
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _activate(mod, raw_ns, src):
|
|
574
|
+
"""把变异体的可调用面临时挂上真源模块(_stg_call 等经属性取用 → 变异可见)。"""
|
|
575
|
+
global S, _CUR_SRC
|
|
576
|
+
saved = {}
|
|
577
|
+
for k in list(_stg.__dict__):
|
|
578
|
+
if k.startswith("__") or k not in raw_ns:
|
|
579
|
+
continue
|
|
580
|
+
saved[k] = _stg.__dict__[k]
|
|
581
|
+
setattr(_stg, k, raw_ns[k])
|
|
582
|
+
S = mod
|
|
583
|
+
_CUR_SRC = src
|
|
584
|
+
return saved
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _deactivate(saved):
|
|
588
|
+
global S, _CUR_SRC
|
|
589
|
+
for k, v in saved.items():
|
|
590
|
+
setattr(_stg, k, v)
|
|
591
|
+
S = _stg
|
|
592
|
+
_CUR_SRC = None
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _branch_baseline(fx) -> int:
|
|
596
|
+
print("!! 定点变异自证:逐处变异,断言须**恰好**打红预期条数;锚点漂移=ANCHOR-MISS+退出码 2\n")
|
|
597
|
+
with _quiet():
|
|
598
|
+
clean_red = _run_groups(fx)
|
|
599
|
+
all_labels = set(_PASS) | set(_FAIL)
|
|
600
|
+
if clean_red:
|
|
601
|
+
print(" 未变异基线即失败:%s" % clean_red)
|
|
602
|
+
return 1
|
|
603
|
+
print(" 未变异基线:%d 条断言全绿" % len(all_labels))
|
|
604
|
+
reddened = set()
|
|
605
|
+
bad = []
|
|
606
|
+
for name, repls, expect in _MUTATIONS:
|
|
607
|
+
mod, raw_ns, src = _build_mutant(repls)
|
|
608
|
+
if mod is None:
|
|
609
|
+
print(" ANCHOR-MISS %s —— 变异锚点漂移(实现改了却没同步 _MUTATIONS)" % name)
|
|
610
|
+
return 2
|
|
611
|
+
saved = _activate(mod, raw_ns, src)
|
|
612
|
+
try:
|
|
613
|
+
with _quiet():
|
|
614
|
+
reds = _run_groups(fx)
|
|
615
|
+
finally:
|
|
616
|
+
_deactivate(saved)
|
|
617
|
+
reddened |= set(reds)
|
|
618
|
+
hit = len(reds) == expect
|
|
619
|
+
print(" 变异「%s」→ 打红 %d 条(预期 %d)%s"
|
|
620
|
+
% (name, len(reds), expect, "OK" if hit else "**不符**"))
|
|
621
|
+
if not hit:
|
|
622
|
+
bad.append("%s:红 %d ≠ 预期 %d" % (name, len(reds), expect))
|
|
623
|
+
for r in sorted(reds):
|
|
624
|
+
print(" red: " + r)
|
|
625
|
+
never = sorted(all_labels - reddened)
|
|
626
|
+
if never:
|
|
627
|
+
print(" **空转断言(无任何变异能打红)**:%s" % "、".join(never))
|
|
628
|
+
bad.append("空转断言 %d 条" % len(never))
|
|
629
|
+
print("\n判别力自证:%s" % ("PASS(八处变异各自恰好命中,且无空转断言)"
|
|
630
|
+
if not bad else "FAIL —— " + ";".join(bad)))
|
|
631
|
+
return 0 if not bad else 1
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _legacy_baseline(fx) -> int:
|
|
635
|
+
print("!! 修前形态重建(切片先于条件 + 无截断上报):断言应当转红\n")
|
|
636
|
+
mod, raw_ns, src = _build_mutant(_LEGACY_REPLS)
|
|
637
|
+
if mod is None:
|
|
638
|
+
print(" ANCHOR-MISS —— 修前形态重建的锚点漂移")
|
|
639
|
+
return 2
|
|
640
|
+
saved = _activate(mod, raw_ns, src)
|
|
641
|
+
try:
|
|
642
|
+
with _quiet():
|
|
643
|
+
reds = _run_groups(fx)
|
|
644
|
+
finally:
|
|
645
|
+
_deactivate(saved)
|
|
646
|
+
print(" 修前形态下打红 %d 条:%s" % (len(reds), "、".join(sorted(reds))))
|
|
647
|
+
core = {l for l in reds if l.startswith("A1 ") or l.startswith("A3 ")}
|
|
648
|
+
miss = {"A1/A3(条件先行的核心断言)"} if len(core) < 2 else set()
|
|
649
|
+
print("\n修前形态判定:%s" % ("PASS(核心断言转红——本守卫抓得住 #52 本身)"
|
|
650
|
+
if not miss else "FAIL —— 核心断言未转红:%s" % miss))
|
|
651
|
+
return 0 if not miss else 1
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def main() -> int:
|
|
655
|
+
if "--branch-baseline" in sys.argv:
|
|
656
|
+
fx = _Fixtures()
|
|
657
|
+
try:
|
|
658
|
+
return _branch_baseline(fx)
|
|
659
|
+
finally:
|
|
660
|
+
fx.close()
|
|
661
|
+
if "--legacy-baseline" in sys.argv:
|
|
662
|
+
fx = _Fixtures()
|
|
663
|
+
try:
|
|
664
|
+
return _legacy_baseline(fx)
|
|
665
|
+
finally:
|
|
666
|
+
fx.close()
|
|
667
|
+
fx = _Fixtures()
|
|
668
|
+
try:
|
|
669
|
+
_run_groups(fx)
|
|
670
|
+
finally:
|
|
671
|
+
fx.close()
|
|
672
|
+
print("\nissue52 守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
|
|
673
|
+
return 0 if not _FAIL else 1
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
if __name__ == "__main__":
|
|
677
|
+
sys.exit(main())
|