@furongjun1999/dsh-memory 0.7.3 → 0.7.5
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 +631 -629
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/discipline/templates/zcode-user.md.tmpl +4 -1
- 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/217/221/345/270/20313_/344/270/212/344/270/213/346/226/207/350/207/252/347/256/241/347/220/206/346/234/272/345/210/266_v1.0.md +114 -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/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 -43
- 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 +80 -1
- package/docs/plans//347/201/265/346/236/242/350/272/253/344/275/223/303/227/350/204/221_/344/270/226/347/225/214/346/250/241/345/236/213/345/257/271/346/216/245/350/256/276/350/256/241_v0.1.md +76 -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 +461 -461
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.2.md +121 -0
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.3.md +117 -0
- package/lib/hooks.d.ts +34 -0
- package/lib/hooks.js +205 -18
- package/lib/index.js +22 -1
- package/lib/lib/mdcg_client.d.ts +37 -0
- package/lib/lib/mdcg_client.js +44 -0
- 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/audit.py +7 -1
- package/md_cg/bench_governance.py +4 -1
- package/md_cg/bench_locomo_zh.py +12 -2
- package/md_cg/bench_zh_mad.py +12 -2
- package/md_cg/interop.py +25 -9
- package/md_cg/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +64 -11
- package/md_cg/mdcos.py +78 -9
- package/md_cg/sleep.py +223 -103
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- package/md_cg/sustain.py +150 -4
- package/md_cg/test_lifecycle_retire_leak.py +408 -0
- package/md_cg/test_read_face_semantics.py +6 -3
- package/md_cg/test_state_event_op.py +514 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/test_sustain_bounded.py +624 -0
- package/md_cg/tokens.py +8 -1
- package/md_cg/units.py +4 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +217 -17
- package/src/index.ts +22 -1
- package/src/lib/mdcg_client.ts +48 -0
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
- package/zcode/AGENTS.md +5 -5
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""test_lifecycle_retire_leak · 退役纪律守卫(archived 不参与默认检索)
|
|
3
|
+
|
|
4
|
+
现场(编排侧复现的对外缺陷):写 v1 → 写矛盾 v2 → 把 v2 走**合法迁移表**
|
|
5
|
+
降到 `archived` → 查。`frontmatter.lifecycle_state` 与索引条目都已正确为
|
|
6
|
+
`archived`,而 `cg.search` 与 `stg.timeline` **仍返回 v2**——`lifecycle.py`
|
|
7
|
+
宣称「archived 已归档:不再参与默认检索」,而**读面从未实现该过滤**
|
|
8
|
+
(标称 vs 实现缺口)。
|
|
9
|
+
|
|
10
|
+
判据单点:`lifecycle.is_archived`(fail-open:缺键/非法值 → active)。
|
|
11
|
+
消费面(三个,判据只此一份):①cg **检索面** `MdCGOS._candidates`;
|
|
12
|
+
②stg 扫描面 `stg._scan_one`(`_scan` 的全量遍历与索引子集两条分支共用的
|
|
13
|
+
逐条实现单点);③会话续接**注入面** `MdCGOS.session_recall` 的四个取数段
|
|
14
|
+
(会话要点 `_session_notes` + 活跃目标 `goals` + 任务台账 `tasks` +
|
|
15
|
+
未解问题 `unresolved`;`goals`/`tasks` 经 `_retired_by_id` 按 id 回查索引)。
|
|
16
|
+
|
|
17
|
+
覆盖(R1–R4):
|
|
18
|
+
R1 复现序列:写 v1/v2 → demoted → archived → search / recall / search_rrf /
|
|
19
|
+
`_candidates` 与 timeline / anchors / consistency 候选面(scanned 读数)
|
|
20
|
+
均不返回 v2,v1 照常返回;R1.11 图扩展/因果路不绕过;R1.12 会话续接
|
|
21
|
+
注入面(要点摘要 + 未解问题正文);R1.13 续接包台账段(活跃目标 + 任务卡);
|
|
22
|
+
R2 不误剔:converged 与 demoted(**降权轴**)仍返回;**缺 lifecycle 键**
|
|
23
|
+
节点照常返回(fail-open);
|
|
24
|
+
R3 退役不删除:archived 期间 `cg.get` 可得、`op=audit` 面可得;
|
|
25
|
+
`set_state(archived→active)` 后读面(含续接面)重新可见;
|
|
26
|
+
R4 面完整性:read/recall/search/route 全链(含 `mcp_server._cg_call`
|
|
27
|
+
抽查一层)+ `op=session` 全链 archived 不出现。
|
|
28
|
+
|
|
29
|
+
运行:python -X utf8 -m md_cg.test_lifecycle_retire_leak
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
import shutil
|
|
35
|
+
import tempfile
|
|
36
|
+
|
|
37
|
+
from . import lifecycle, stg
|
|
38
|
+
from . import mcp_server
|
|
39
|
+
from . import tasks as _tasks
|
|
40
|
+
from .mdcos import MdCGOS
|
|
41
|
+
|
|
42
|
+
_root = tempfile.mkdtemp(prefix="mdcg_retire_")
|
|
43
|
+
cg = MdCGOS(_root)
|
|
44
|
+
|
|
45
|
+
_ok = 0
|
|
46
|
+
_fail = []
|
|
47
|
+
|
|
48
|
+
#: 查询串(固定字面量,逐字出现在各节点正文里——词面命中不靠猜测)。
|
|
49
|
+
Q = "退役泄漏守卫查询槽位"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _body(tag, verdict):
|
|
53
|
+
"""CCG 六要素完整的节点正文(缺要素会被判 BLINDSPOT,污染可见性读数)。"""
|
|
54
|
+
return ("# 功能名:退役纪律守卫·%s\n"
|
|
55
|
+
"# 生效条件:隔离 temp root 内执行退役纪律守卫序列时\n"
|
|
56
|
+
"# 子功能:①登记被测事实 ②退役后的读面/时空面可见性读数\n"
|
|
57
|
+
"# 执行:cg.add 一次(退役节点另经 set_state 逐级降级)\n"
|
|
58
|
+
"# 验证方式:读面 search/recall/search_rrf 与 stg 面的可见性断言\n"
|
|
59
|
+
"# 不适用条件:在役真实记忆库内执行\n"
|
|
60
|
+
"正文:%s = %s" % (tag, Q, verdict))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check(name, cond, detail=""):
|
|
64
|
+
global _ok
|
|
65
|
+
if cond:
|
|
66
|
+
_ok += 1
|
|
67
|
+
print("[ok] " + name)
|
|
68
|
+
else:
|
|
69
|
+
_fail.append(name)
|
|
70
|
+
print("[FAIL] %s · %s" % (name, str(detail)[:240]))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _search_ids(q=Q):
|
|
74
|
+
res, _meta = cg.search(q, k=50, record=False)
|
|
75
|
+
return {str(n.get("id")) for n, _s, _q in res}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _rrf_ids(q=Q):
|
|
79
|
+
"""RRF 路(`cg.recall` 缺省走的就是它)——含图扩展/因果路。"""
|
|
80
|
+
res, _meta = cg.search_rrf(q, k=50, record=False)
|
|
81
|
+
return {str(r[0].get("id")) for r in res}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _recall_ids(q=Q):
|
|
85
|
+
pack = cg.recall(q, budget_tokens=4000, k=50)
|
|
86
|
+
return {str(it.get("id")) for it in (pack.get("pack") or [])}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _entry_nid(e):
|
|
90
|
+
"""索引条目 → 节点 id:条目**不带 id 键**(只带 path),按文件名取
|
|
91
|
+
(与 `MdCGOS._path_graph` 的 `e["path"].split("/")[-1][:-3]` 同口径)。"""
|
|
92
|
+
p = str((e or {}).get("path") or "")
|
|
93
|
+
return p.replace("\\", "/").split("/")[-1][:-3]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cand_ids():
|
|
97
|
+
"""读面单点 `_candidates` 的候选池 id 集合(cg 全部读 op 的公共候选面)。"""
|
|
98
|
+
return {_entry_nid(e) for e in cg._candidates()}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _tl():
|
|
102
|
+
return stg.timeline(cg, limit=10 ** 6, max_scan=10 ** 9)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _an():
|
|
106
|
+
return stg.anchors(cg, time_window=[1.0, 10.0 ** 11], limit=10 ** 6,
|
|
107
|
+
max_scan=10 ** 9)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _co():
|
|
111
|
+
return stg.consistency(cg, limit=10 ** 6, max_scan=10 ** 9)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _tl_ids(rep=None):
|
|
115
|
+
rep = _tl() if rep is None else rep
|
|
116
|
+
return {str(it.get("id")) for it in (rep.get("items") or [])}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _an_ids(rep=None):
|
|
120
|
+
rep = _an() if rep is None else rep
|
|
121
|
+
return {str(it.get("id")) for it in (rep.get("items") or [])}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _scan_subset_ids():
|
|
125
|
+
"""索引子集分支(`_scan(nodes=...)`):第 3 层 flag 开时的取数面。"""
|
|
126
|
+
pairs = list((cg.index.get("nodes") or {}).items())
|
|
127
|
+
return {str(n.get("id")) for n in stg._scan(cg, nodes=pairs)}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _fm(nid):
|
|
131
|
+
return ((cg.get(nid) or {}).get("frontmatter")) or {}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _idx(nid):
|
|
135
|
+
return (cg.index.get("nodes") or {}).get(nid) or {}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------ R1 复现序列
|
|
139
|
+
n1 = cg.add("rl_v1", _body("旧事实 v1", "甲地(初始登记)"), layer="knowledge",
|
|
140
|
+
verification_basis="test", importance=0.4)
|
|
141
|
+
n2 = cg.add("rl_v2", _body("新事实 v2(与 v1 矛盾)", "乙地(订正:甲地作废)"),
|
|
142
|
+
layer="knowledge", verification_basis="test", importance=0.4)
|
|
143
|
+
|
|
144
|
+
check("R1.0 写入后两节点都在读面与时空面(前提成立,非空断言)",
|
|
145
|
+
n1 in _search_ids() and n2 in _search_ids()
|
|
146
|
+
and n1 in _tl_ids() and n2 in _tl_ids(),
|
|
147
|
+
(sorted(_search_ids()), sorted(_tl_ids())))
|
|
148
|
+
|
|
149
|
+
scanned_tl_before = _tl().get("scanned")
|
|
150
|
+
scanned_co_before = _co().get("scanned")
|
|
151
|
+
|
|
152
|
+
_r1 = cg.set_state(n2, "demoted", reason="守卫:先降权", actor="test")
|
|
153
|
+
_r2 = cg.set_state(n2, "archived", reason="守卫:归档", actor="test")
|
|
154
|
+
check("R1.1 退役走合法迁移表(demoted→archived)且状态落盘为 archived",
|
|
155
|
+
_r1.get("ok") and _r2.get("ok")
|
|
156
|
+
and lifecycle.state_of(_fm(n2)) == "archived", (_r1, _r2, _fm(n2)))
|
|
157
|
+
|
|
158
|
+
check("R1.2 退役后 cg 读面 search 不返回 v2、v1 照常返回",
|
|
159
|
+
n2 not in _search_ids() and n1 in _search_ids(), sorted(_search_ids()))
|
|
160
|
+
check("R1.3 退役后 cg 读面 recall(预算装包,走 RRF)不返回 v2、v1 照常返回",
|
|
161
|
+
n2 not in _recall_ids() and n1 in _recall_ids(), sorted(_recall_ids()))
|
|
162
|
+
check("R1.4 退役后 cg 读面 search_rrf 不返回 v2、v1 照常返回",
|
|
163
|
+
n2 not in _rrf_ids() and n1 in _rrf_ids(), sorted(_rrf_ids()))
|
|
164
|
+
check("R1.5 退役后 _candidates 候选池不含 v2(读面单点)、含 v1",
|
|
165
|
+
n2 not in _cand_ids() and n1 in _cand_ids(), sorted(_cand_ids()))
|
|
166
|
+
check("R1.6 退役后 stg.timeline 不返回 v2、v1 照常返回",
|
|
167
|
+
n2 not in _tl_ids() and n1 in _tl_ids(), sorted(_tl_ids()))
|
|
168
|
+
check("R1.7 退役后 stg.anchors 不返回 v2、v1 照常返回",
|
|
169
|
+
n2 not in _an_ids() and n1 in _an_ids(), sorted(_an_ids()))
|
|
170
|
+
|
|
171
|
+
scanned_tl_after = _tl().get("scanned")
|
|
172
|
+
scanned_co_after = _co().get("scanned")
|
|
173
|
+
check("R1.8 stg 候选遍历读数随退役下降(timeline scanned 少 1)",
|
|
174
|
+
scanned_tl_after == scanned_tl_before - 1,
|
|
175
|
+
(scanned_tl_before, scanned_tl_after))
|
|
176
|
+
check("R1.9 consistency 候选面同点覆盖(scanned 少 1——archived 不进候选)",
|
|
177
|
+
scanned_co_after == scanned_co_before - 1,
|
|
178
|
+
(scanned_co_before, scanned_co_after))
|
|
179
|
+
check("R1.10 索引子集分支(_scan(nodes=...) 第 3 层取数面)逐位一致——不因取数面漏过滤",
|
|
180
|
+
n2 not in _scan_subset_ids() and n1 in _scan_subset_ids(),
|
|
181
|
+
sorted(_scan_subset_ids()))
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------------------------ R2 不误剔
|
|
184
|
+
n3 = cg.add("rl_converged", _body("已定型 converged", "丙地"),
|
|
185
|
+
layer="knowledge", verification_basis="test", importance=0.4)
|
|
186
|
+
n4 = cg.add("rl_demoted", _body("已降权 demoted", "丁地"),
|
|
187
|
+
layer="knowledge", verification_basis="test", importance=0.4)
|
|
188
|
+
cg.set_state(n3, "converged", reason="守卫:定型", actor="test")
|
|
189
|
+
cg.set_state(n4, "demoted", reason="守卫:降权", actor="test")
|
|
190
|
+
|
|
191
|
+
check("R2.1 converged(已定型=降权轴)仍参与默认检索",
|
|
192
|
+
lifecycle.state_of(_fm(n3)) == "converged"
|
|
193
|
+
and n3 in _cand_ids() and n3 in _search_ids() and n3 in _tl_ids(),
|
|
194
|
+
(_fm(n3).get(lifecycle.STATE_FIELD), sorted(_search_ids())))
|
|
195
|
+
check("R2.2 demoted(已降权=降权轴)仍参与默认检索",
|
|
196
|
+
lifecycle.state_of(_fm(n4)) == "demoted"
|
|
197
|
+
and n4 in _cand_ids() and n4 in _search_ids() and n4 in _tl_ids(),
|
|
198
|
+
(_fm(n4).get(lifecycle.STATE_FIELD), sorted(_search_ids())))
|
|
199
|
+
|
|
200
|
+
# 缺 lifecycle 键(旧库形态):盘上 fm 与索引条目都摘掉该键
|
|
201
|
+
n5 = cg.add("rl_legacy", _body("旧库缺键形态", "戊地"), layer="knowledge",
|
|
202
|
+
verification_basis="test", importance=0.4)
|
|
203
|
+
_node5 = cg.get(n5)
|
|
204
|
+
_fm5 = dict(_node5["frontmatter"])
|
|
205
|
+
_fm5.pop(lifecycle.STATE_FIELD, None)
|
|
206
|
+
cg._write_node(n5, os.path.join(cg.root, _node5["path"]), _fm5,
|
|
207
|
+
_node5.get("content") or "")
|
|
208
|
+
(cg.index.get("nodes") or {}).get(n5, {}).pop(lifecycle.STATE_FIELD, None)
|
|
209
|
+
check("R2.3 旧库形态成立(fm 与索引条目均无 lifecycle 键)",
|
|
210
|
+
lifecycle.STATE_FIELD not in _fm(n5)
|
|
211
|
+
and lifecycle.STATE_FIELD not in _idx(n5), (_fm(n5), _idx(n5)))
|
|
212
|
+
check("R2.4 fail-open:缺 lifecycle 键的节点照常参与默认检索(存量零迁移)",
|
|
213
|
+
n5 in _cand_ids() and n5 in _search_ids() and n5 in _tl_ids(),
|
|
214
|
+
sorted(_search_ids()))
|
|
215
|
+
check("R2.5 is_archived 判据单点:缺键/非法值/非 dict → False;archived → True",
|
|
216
|
+
lifecycle.is_archived({}) is False
|
|
217
|
+
and lifecycle.is_archived({lifecycle.STATE_FIELD: "frozen"}) is False
|
|
218
|
+
and lifecycle.is_archived(None) is False
|
|
219
|
+
and lifecycle.is_archived({lifecycle.STATE_FIELD: "archived"}) is True
|
|
220
|
+
and lifecycle.is_archived(_idx(n2)) is True
|
|
221
|
+
and lifecycle.is_archived({lifecycle.STATE_FIELD: "demoted"}) is False)
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------ R3 退役不删除
|
|
224
|
+
_node2 = cg.get(n2) or {}
|
|
225
|
+
check("R3.1 archived 期间 cg.get 直读可得(frontmatter + 正文 + 文件仍在)",
|
|
226
|
+
bool(_node2) and bool(_node2.get("content"))
|
|
227
|
+
and os.path.exists(os.path.join(cg.root, _node2.get("path") or "")),
|
|
228
|
+
_node2)
|
|
229
|
+
_aud = mcp_server._cg_call(cg, {"op": "audit", "node_id": n2})
|
|
230
|
+
check("R3.2 archived 期间 op=audit 审计面照常可达(不被退役闸拦住)",
|
|
231
|
+
isinstance(_aud, dict) and not _aud.get("error"), _aud)
|
|
232
|
+
check("R3.3 显式恢复 archived→active 走合法迁移表且成功",
|
|
233
|
+
cg.set_state(n2, "active", reason="守卫:显式恢复",
|
|
234
|
+
actor="test").get("ok")
|
|
235
|
+
and lifecycle.state_of(_fm(n2)) == "active", _fm(n2))
|
|
236
|
+
check("R3.4 恢复后读面/时空面重新可见(退役可逆)",
|
|
237
|
+
n2 in _cand_ids() and n2 in _search_ids() and n2 in _tl_ids()
|
|
238
|
+
and n2 in _an_ids(), (sorted(_search_ids()), sorted(_tl_ids())))
|
|
239
|
+
# 复原到 archived,供 R4 用
|
|
240
|
+
cg.set_state(n2, "demoted", reason="守卫:复测降权", actor="test")
|
|
241
|
+
cg.set_state(n2, "archived", reason="守卫:复测归档", actor="test")
|
|
242
|
+
|
|
243
|
+
# ------------------------------------------------------------ R4 面完整性
|
|
244
|
+
_call_read = mcp_server._cg_call(cg, {"op": "read", "query": Q, "k": 50})
|
|
245
|
+
_call_recall = mcp_server._cg_call(cg, {"op": "read", "query": Q,
|
|
246
|
+
"budget_tokens": 4000, "k": 50})
|
|
247
|
+
_call_route = mcp_server._cg_call(cg, {"op": "route", "intent": Q, "k": 50})
|
|
248
|
+
_read_ids = {str(r.get("node", {}).get("id"))
|
|
249
|
+
for r in (_call_read.get("results") or [])}
|
|
250
|
+
_pack_ids = {str(it.get("id")) for it in (_call_recall.get("pack") or [])}
|
|
251
|
+
_route_ids = {str(k.get("id")) for k in (_call_route.get("knowledge") or [])}
|
|
252
|
+
check("R4.1 op=read(检索分支)全链:archived 不出现、v1 出现",
|
|
253
|
+
n2 not in _read_ids and n1 in _read_ids,
|
|
254
|
+
(_call_read.get("meta"), sorted(_read_ids)))
|
|
255
|
+
check("R4.2 op=read(预算召回分支→recall/RRF)全链:archived 不出现、v1 出现",
|
|
256
|
+
n2 not in _pack_ids and n1 in _pack_ids,
|
|
257
|
+
(_call_recall.get("meta"), sorted(_pack_ids)))
|
|
258
|
+
check("R4.3 op=route 全链:archived 不出现、v1 出现",
|
|
259
|
+
n2 not in _route_ids and n1 in _route_ids,
|
|
260
|
+
(_call_route.get("meta"), sorted(_route_ids)))
|
|
261
|
+
check("R4.4 直读面(read+node_id 分支)不受限:archived 仍可显式取回",
|
|
262
|
+
mcp_server._cg_call(cg, {"op": "read", "node_id": n2}) is not None)
|
|
263
|
+
check("R4.5 四条读面口径同源(都经 _candidates 单点)",
|
|
264
|
+
n2 not in _cand_ids() and n2 not in _search_ids()
|
|
265
|
+
and n2 not in _rrf_ids() and n2 not in _recall_ids())
|
|
266
|
+
|
|
267
|
+
# ------------------------------------------------------------ R1.11 图扩展/因果路不绕过读面
|
|
268
|
+
# `MdCGSecure.search_rrf` 的二次过滤注释宣称「图扩展会绕过 _candidates」——
|
|
269
|
+
# 本腿坐实:graph/chain 两路的扩散目标**受 entries 约束**(`_path_graph` 的
|
|
270
|
+
# by_id、`_path_chain` 的 allowed 都取自 `_candidates`),故 archived 目标
|
|
271
|
+
# 进不了融合池。判据分两段:先证「扩展真的能到达目标」(非空断言,反空转),
|
|
272
|
+
# 再证「目标退役后到达不了」。
|
|
273
|
+
g1 = cg.add("rl_graph_v1", _body("图扩展种子", "己地"), layer="knowledge",
|
|
274
|
+
verification_basis="test", importance=0.4,
|
|
275
|
+
edges=[{"target": "rl_graph_v2", "type": "derived_from"}])
|
|
276
|
+
g2 = cg.add("rl_graph_v2", _body("图扩展目标(待退役)", "庚地"),
|
|
277
|
+
layer="knowledge", verification_basis="test", importance=0.4)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _graph_ids():
|
|
281
|
+
res, _meta = cg.search_rrf(Q, k=50, record=False,
|
|
282
|
+
paths=("lexical", "graph", "chain"))
|
|
283
|
+
return {str(r[0].get("id")) for r in res}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
check("R1.11a 前提:graph/chain 扩散确实能到达边目标(非空断言,防本腿空转)",
|
|
287
|
+
g1 in _graph_ids() and g2 in _graph_ids(), sorted(_graph_ids()))
|
|
288
|
+
cg.set_state(g2, "demoted", reason="守卫:图路降权", actor="test")
|
|
289
|
+
cg.set_state(g2, "archived", reason="守卫:图路归档", actor="test")
|
|
290
|
+
check("R1.11b 目标退役后图扩展路不返回它、种子照常返回(图扩展不绕过 _candidates)",
|
|
291
|
+
g2 not in _graph_ids() and g1 in _graph_ids(), sorted(_graph_ids()))
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------ R1.12 会话续接注入面(复核打回补)
|
|
294
|
+
# 独立复核 2026-10-06:`cg(op=session, action=recall)` 自持取数面(不经 _candidates),
|
|
295
|
+
# 修前把 archived 节点的要点摘要与未解问题正文照旧回给调用方——同属「退役不参与
|
|
296
|
+
# 默认注入」的遗漏出口。本腿两段:先证在役时确能回出(非空断言,反空转),再证
|
|
297
|
+
# 退役后回不出;并留一条在役对照(防「一刀切全清空」的假绿)。
|
|
298
|
+
_SN = "sess_retire_guard"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _sess_pack(session=_SN):
|
|
302
|
+
return cg.session_recall(session=session, limit=50, recent_limit=50,
|
|
303
|
+
budget_tokens=20000)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _sess_ids(p=None):
|
|
307
|
+
p = _sess_pack() if p is None else p
|
|
308
|
+
ids = {str(n.get("id")) for n in (p.get("notes") or [])}
|
|
309
|
+
ids |= {str(u.get("id")) for u in (p.get("unresolved") or [])}
|
|
310
|
+
ids |= {str(g.get("id")) for g in (p.get("goals") or [])}
|
|
311
|
+
ids |= {str(t.get("id")) for t in ((p.get("tasks") or {}).get("active") or [])}
|
|
312
|
+
ids |= {str(t.get("id")) for t in ((p.get("tasks") or {}).get("done") or [])}
|
|
313
|
+
return ids
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
_note1 = cg.session_note("守卫会话要点甲:退役后不应再回给调用方",
|
|
317
|
+
session=_SN, importance=0.6)["id"]
|
|
318
|
+
_note2 = cg.session_note("守卫会话要点乙:仍在役(对照,须照常回出)",
|
|
319
|
+
session=_SN, importance=0.6)["id"]
|
|
320
|
+
_unr1 = cg.add_unresolved("守卫未解问题:退役后是否仍被注入?",
|
|
321
|
+
known_clues="守卫线索", goal="守卫目标")
|
|
322
|
+
check("R1.12a 前提:会话要点与未解问题都确在续接包里(非空断言,防本腿空转)",
|
|
323
|
+
{_note1, _note2, _unr1} <= _sess_ids(), sorted(_sess_ids()))
|
|
324
|
+
for _nid in (_note1, _unr1):
|
|
325
|
+
cg.set_state(_nid, "demoted", reason="守卫:续接面降权", actor="test")
|
|
326
|
+
cg.set_state(_nid, "archived", reason="守卫:续接面归档", actor="test")
|
|
327
|
+
check("R1.12b 退役后续接包不再回该要点与未解问题(注入面剔除);在役对照仍在",
|
|
328
|
+
_sess_ids().isdisjoint({_note1, _unr1}) and _note2 in _sess_ids(),
|
|
329
|
+
sorted(_sess_ids()))
|
|
330
|
+
cg.set_state(_note1, "active", reason="守卫:续接面恢复", actor="test")
|
|
331
|
+
check("R3.5 恢复后续接包重新回该要点(退役可逆,同面)",
|
|
332
|
+
_note1 in _sess_ids(), sorted(_sess_ids()))
|
|
333
|
+
cg.set_state(_note1, "demoted", reason="守卫:复测降权", actor="test")
|
|
334
|
+
cg.set_state(_note1, "archived", reason="守卫:复测归档", actor="test")
|
|
335
|
+
|
|
336
|
+
# ------------------------------------------------- R1.13 续接包台账段(复核打回补)
|
|
337
|
+
# 独立复核 2026-10-06 第二轮回执:同一续接包中 notes/未解段已剔而 goals/tasks 段
|
|
338
|
+
# 仍把 archived 节点注回调用方(与本文件 §5 判据字面冲突)。本腿三段:前提(真能
|
|
339
|
+
# 带回)→ 退役后剔除(含 `*_total` 口径)→ 恢复后重新带回;并留在役对照防假绿。
|
|
340
|
+
_goal1 = cg.add_goal("守卫目标甲:退役后不应再进续接包", priority=0.4)
|
|
341
|
+
_goal2 = cg.add_goal("守卫目标乙:仍在役(对照)", priority=0.3)
|
|
342
|
+
_task1 = _tasks.upsert(cg, "守卫任务卡甲", plan="退役后不应再进续接包")["node_id"]
|
|
343
|
+
_task2 = _tasks.upsert(cg, "守卫任务卡乙", plan="仍在役(对照)")["node_id"]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _pack_ledger(p=None):
|
|
347
|
+
p = _sess_pack() if p is None else p
|
|
348
|
+
ids = {str(g.get("id")) for g in (p.get("goals") or [])}
|
|
349
|
+
ids |= {str(t.get("id"))
|
|
350
|
+
for t in ((p.get("tasks") or {}).get("active") or [])}
|
|
351
|
+
return ids, (p.get("tasks") or {})
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
check("R1.13a 前提:活跃目标与任务卡都确在续接包里(非空断言,防本腿空转)",
|
|
355
|
+
{_goal1, _goal2, _task1, _task2} <= _pack_ledger()[0],
|
|
356
|
+
sorted(_pack_ledger()[0]))
|
|
357
|
+
# 任务卡以 importance=0.8 建(tasks.DEFAULT_IMPORTANCE)⇒ 自动 protected——
|
|
358
|
+
# 受保护节点降级须显式 override=True(lifecycle 的「保护 = 不可遗忘」);这里
|
|
359
|
+
# 顺带把退役结果断言出来,避免「退役没成功却当成已退役」的假绿。
|
|
360
|
+
_led_ret = []
|
|
361
|
+
for _nid in (_goal1, _task1):
|
|
362
|
+
_led_ret.append(cg.set_state(_nid, "demoted", reason="守卫:台账段降权",
|
|
363
|
+
actor="test", override=True))
|
|
364
|
+
_led_ret.append(cg.set_state(_nid, "archived", reason="守卫:台账段归档",
|
|
365
|
+
actor="test", override=True))
|
|
366
|
+
check("R1.13b0 台账两节点退役成功(任务卡默认 protected,降级走 override=True)",
|
|
367
|
+
all(r.get("ok") for r in _led_ret)
|
|
368
|
+
and all(lifecycle.state_of(_fm(_n)) == "archived"
|
|
369
|
+
for _n in (_goal1, _task1)), _led_ret)
|
|
370
|
+
_led_ids, _led_tasks = _pack_ledger()
|
|
371
|
+
check("R1.13b 退役后台账段不再回该目标与任务卡;在役对照仍在;total 取剔除后计数",
|
|
372
|
+
_led_ids.isdisjoint({_goal1, _task1})
|
|
373
|
+
and {_goal2, _task2} <= _led_ids
|
|
374
|
+
and _led_tasks.get("active_total") == 1
|
|
375
|
+
and _led_tasks.get("done_total") == 0,
|
|
376
|
+
(sorted(_led_ids), _led_tasks.get("active_total"),
|
|
377
|
+
_led_tasks.get("done_total")))
|
|
378
|
+
check("R1.13c 管理面不受限对照:cg(op=goal/task, action=list) 仍可见 archived 台账",
|
|
379
|
+
any(g.get("id") == _goal1 for g in
|
|
380
|
+
(mcp_server._cg_call(cg, {"op": "goal", "action": "list"})
|
|
381
|
+
.get("goals") or []))
|
|
382
|
+
and any(t.get("id") == _task1 for t in
|
|
383
|
+
(mcp_server._cg_call(cg, {"op": "task", "action": "list"})
|
|
384
|
+
.get("tasks") or [])),
|
|
385
|
+
mcp_server._cg_call(cg, {"op": "goal", "action": "list"}))
|
|
386
|
+
cg.set_state(_goal1, "active", reason="守卫:台账段恢复", actor="test")
|
|
387
|
+
check("R3.6 恢复后台账段重新回该目标(退役可逆,同面)",
|
|
388
|
+
_goal1 in _pack_ledger()[0], sorted(_pack_ledger()[0]))
|
|
389
|
+
cg.set_state(_goal1, "demoted", reason="守卫:复测降权", actor="test")
|
|
390
|
+
cg.set_state(_goal1, "archived", reason="守卫:复测归档", actor="test")
|
|
391
|
+
# ------------------------------------------------------------ R4.6 会话续接面全链抽查
|
|
392
|
+
_call_sess = mcp_server._cg_call(cg, {"op": "session", "action": "recall",
|
|
393
|
+
"session": _SN, "limit": 50,
|
|
394
|
+
"recent_limit": 50,
|
|
395
|
+
"budget_tokens": 20000})
|
|
396
|
+
_sess_call_ids = {str(n.get("id")) for n in (_call_sess.get("notes") or [])}
|
|
397
|
+
_sess_call_ids |= {str(u.get("id"))
|
|
398
|
+
for u in (_call_sess.get("unresolved") or [])}
|
|
399
|
+
check("R4.6 op=session(action=recall)全链:archived 要点/未解问题不出现、在役对照出现",
|
|
400
|
+
_sess_call_ids.isdisjoint({_note1, _unr1}) and _note2 in _sess_call_ids,
|
|
401
|
+
sorted(_sess_call_ids))
|
|
402
|
+
|
|
403
|
+
print("=" * 58)
|
|
404
|
+
print("test_lifecycle_retire_leak: %d 通过 / %d 失败" % (_ok, len(_fail)))
|
|
405
|
+
if _fail:
|
|
406
|
+
print("失败项:" + "、".join(_fail))
|
|
407
|
+
shutil.rmtree(cg.root, ignore_errors=True)
|
|
408
|
+
raise SystemExit(1 if _fail else 0)
|
|
@@ -356,9 +356,12 @@ def group_m4(S):
|
|
|
356
356
|
isinstance(o, dict) and _g(o, "action_source") in
|
|
357
357
|
("explicit", "sig", "default", "none"),
|
|
358
358
|
str(o)[:110])
|
|
359
|
-
# 抛错面钉住(既有 fail-closed,非本闸范围):只允许 verify(缺 verdict
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
# 抛错面钉住(既有 fail-closed,非本闸范围):只允许 verify(缺 verdict)
|
|
360
|
+
# 与 state_event(缺 subject/slot——P3 写口的 fail-closed,缺参即
|
|
361
|
+
# ValueError 原样上抛,见 md_cg/test_state_event_op.py E 组)两支;
|
|
362
|
+
# 本闸只管读面 action 推导,写口抛错面在此**登记**(仍「新增抛错即红」)。
|
|
363
|
+
check("M4-8 抛错面只有既有的 verify 缺裁决与 state_event 缺参两支(新增抛错即红)",
|
|
364
|
+
{r[0] for r in raised} <= {"verify", "state_event"}, raised)
|
|
362
365
|
if raised:
|
|
363
366
|
print(" · 已知非 dict 返回:%s" % raised)
|
|
364
367
|
finally:
|