@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,545 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""md_cg · issue50-b 守卫:遗忘闸门 DEFER → 审核队列(带原文)+ 双跑去重
|
|
3
|
+
|
|
4
|
+
缺陷(编排侧复核,非推断)
|
|
5
|
+
--------------------------
|
|
6
|
+
`md_cg/mdcos.py::remember_gated` 的 DEFER 出口**只写一行 `_forgetting.jsonl`
|
|
7
|
+
留痕**(node_id/layer/verdict/reason/importance/entropy/actor,**不含正文**),
|
|
8
|
+
不调用任何入队入口——对比 DROP 分支有 `forgetting.record_drop`(留全文 +
|
|
9
|
+
trace_id)。审核队列的条目一律来自**另一条链**(审计/策略闸
|
|
10
|
+
`writepipe._gate_audit` :255、一致性闸 `_gate_consistency` :318 的
|
|
11
|
+
`cg.propose`)。
|
|
12
|
+
|
|
13
|
+
于是 issue50-a(半重复 → DEFER)单落会把半重复从「落成近似重复节点」改成
|
|
14
|
+
「**不落盘且无收件箱**」——把噪声缺陷换成静默不记缺陷。本批补这个入口。
|
|
15
|
+
|
|
16
|
+
本批修复(契约冻结)
|
|
17
|
+
--------------------
|
|
18
|
+
`remember_gated` 的 DEFER 出口接线**既有入队单点** `self.propose`
|
|
19
|
+
(与审计闸/一致性闸同一条链、同一 payload_hash 幂等对账):
|
|
20
|
+
· 「为何待定」写进提案的既有任意槽 `extra.defer_reason`(不新增队列字段);
|
|
21
|
+
· 密级与**调用方声明的其余 meta** 随 `**kw` 一并透传(issue50-c F1 扩面:
|
|
22
|
+
此前只传 `sensitivity`,tags/condition_space/verification_basis/… 全丢);
|
|
23
|
+
· `node_id` 非字符串时兜底成内容派生 id(issue50-c F2,`_nid`);
|
|
24
|
+
· 返回体带 `proposed` = pid(可检索句柄);
|
|
25
|
+
· forgetting 留痕行**形状不变**;
|
|
26
|
+
· **限流 DEFER 不入队**(语义是「先别写」,原文在 recent 时间线);
|
|
27
|
+
· gated=False 旁路、ACCEPT 后 on_conflict=defer 降级**都不入队**。
|
|
28
|
+
|
|
29
|
+
双跑面(现场读码,非照抄)
|
|
30
|
+
--------------------------
|
|
31
|
+
`writepipe.execute` 的 before 链为 linkref→deps→**audit→consistency→gated**,
|
|
32
|
+
任一闸返回 dict 即短路(execute :137-142)⇒ audit 判 DEFER 时 gated 根本不跑,
|
|
33
|
+
audit 判 ACCEPT 时 gated 才跑且 audit 未入队 ⇒ **同一次 `op=write` 至多一条
|
|
34
|
+
队列记录**;`mdcg_remember(gated=true)`(`mcp_server.py:3325`)**绕过整条
|
|
35
|
+
writepipe**,直接调 `remember_gated`,故也不双跑。此外即便双跑,`propose` 的
|
|
36
|
+
`payload_hash = _sig(content)` 幂等对账也会吸收(同内容不长第二条)。
|
|
37
|
+
本守卫的 F 组把「单次调用恰 1 条」与「writepipe 全链恰 1 条」都钉住。
|
|
38
|
+
|
|
39
|
+
断言分组(每组都在**隔离临时根**的合成库上真跑,绝不触在役数据根)
|
|
40
|
+
A 修复面:半重复 DEFER → 队列恰 1 条、正文逐字可取回(jsonl 面 + review_list
|
|
41
|
+
面)、layer/归因/为何待定在位、不落盘、forgetting 留痕行形状不变
|
|
42
|
+
B 幂等:同内容重复写入 → 队列仍 1 条、幂等返回既有 pid
|
|
43
|
+
C 裁决:accept → 正文真的落盘且逐字相等;reject → 不落盘
|
|
44
|
+
D 限流 DEFER 不入队(且反向腿:knowledge 半重复仍入队)
|
|
45
|
+
E gated=False 直写旁路不受影响、不入队
|
|
46
|
+
F 同一次写入只产生 1 条队列记录(含 writepipe 全链)
|
|
47
|
+
|
|
48
|
+
定点变异自证(--mutate)
|
|
49
|
+
------------------------
|
|
50
|
+
每一处判据都配一个定点变异把它打红,且**恰好**命中预期红项数(`_SRC_MUTATIONS`
|
|
51
|
+
的 expected 列;实测计数不符即判 FAIL——照 test_neg_condition_hits.py /
|
|
52
|
+
test_i50a_half_dup_defer.py 同口径)。锚点漂移(实现改了却没同步本表)→ 报
|
|
53
|
+
ANCHOR-MISS 并 **exit 2**(fail-closed);默认模式同样先做锚点自检。
|
|
54
|
+
**不以 git HEAD 为基线源**——基线绑提交即失效(本仓已有两次教训),变异一律
|
|
55
|
+
作用在**当前盘的实现**上。
|
|
56
|
+
|
|
57
|
+
运行:python -X utf8 -m md_cg.test_i50b_defer_to_review_queue
|
|
58
|
+
python -X utf8 -m md_cg.test_i50b_defer_to_review_queue --mutate
|
|
59
|
+
"""
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import contextlib
|
|
63
|
+
import inspect
|
|
64
|
+
import io
|
|
65
|
+
import json
|
|
66
|
+
import os
|
|
67
|
+
import shutil
|
|
68
|
+
import sys
|
|
69
|
+
import tempfile
|
|
70
|
+
|
|
71
|
+
# 三档自治批次②(2026-10-02)**夹具隔离**:本守卫考的不是档位面(档位守卫 =
|
|
72
|
+
# md_cg/test_autonomy_modes.py),故显式置 full 档——回到改动前「动作直落」的
|
|
73
|
+
# 行为,使本文件的断言意图(合并/覆写/软删真的发生)逐条不变;env 键名从唯一
|
|
74
|
+
# 真源表取(本文件不构成第二处字面量)。
|
|
75
|
+
from . import autonomy_modes as _autonomy_modes # noqa: E402
|
|
76
|
+
os.environ[_autonomy_modes.AUTONOMY_ENV_KEYS["mode"]] = "full"
|
|
77
|
+
|
|
78
|
+
from . import writelimit
|
|
79
|
+
from .mdcos import MdCGOS, MdCGSecure
|
|
80
|
+
from .security import Principal
|
|
81
|
+
from .writepipe import default_pipeline
|
|
82
|
+
|
|
83
|
+
_PASS = []
|
|
84
|
+
_FAIL = []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ok(cond, msg, extra=""):
|
|
88
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
89
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
90
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------------------------------------------------------------- 夹具构造
|
|
94
|
+
# 与 test_i50a_half_dup_defer.py 同一套夹具口径:600 个互异汉字作基线正文、
|
|
95
|
+
# 另 600 个互异汉字作全新填充,前 k 字的覆盖比例即重复度(实测 k=360 → dup
|
|
96
|
+
# 0.6078、k=436 → 0.7310、k=503 → 0.8395,三处分别落在 DUP_DROP 侧、中部、
|
|
97
|
+
# 贴近 DUP_MERGE)。
|
|
98
|
+
BASE = "".join(chr(0x4e00 + i) for i in range(600))
|
|
99
|
+
FILL = "".join(chr(0x8000 + i) for i in range(600))
|
|
100
|
+
T_BASE, T_NEW = "基线节点标题", "新写入标题"
|
|
101
|
+
|
|
102
|
+
K_EDGE = 360 # dup ≈ 0.6078(刚好越过 DUP_DROP)
|
|
103
|
+
K_HALF = 436 # dup ≈ 0.7310
|
|
104
|
+
K_TOP = 503 # dup ≈ 0.8395(贴近 DUP_MERGE 但未到)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# 生效条件:title 与 body 给定时按本仓 CCG 六要素模板拼出节点正文(模板行两侧
|
|
108
|
+
# 一致,故不成为重复度的差量来源);无输入校验,参数缺失即抛 TypeError。
|
|
109
|
+
def _doc(title, body):
|
|
110
|
+
return ("# 功能名:%s\n# 生效条件:任意情境\n# 子功能:验收\n"
|
|
111
|
+
"# 执行:直接调用\n# 验证方式:test\n# 不适用条件:无\n%s\n"
|
|
112
|
+
% (title, body))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# 生效条件:k 为整数下标;返回「前 k 字取自 BASE、其余取自 FILL」的新正文。
|
|
116
|
+
def _mixed(k):
|
|
117
|
+
return BASE[:k] + FILL[:len(BASE) - k]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# 生效条件:sess 为真值时作为会话归属,否则用固定串;返回 designer-cli 形态的
|
|
121
|
+
# 本地身份(与 review_cli._cg 同一构造口径:clearance=secret / can_admin /
|
|
122
|
+
# role=designer / auth_mode=local-cli)——队列面的 review_list / review_decide
|
|
123
|
+
# 需要 review op 与 can_admin,本守卫因此不触在役库、只用临时根。
|
|
124
|
+
def _princ(sess="sess-i50b"):
|
|
125
|
+
return Principal(tenant="t1", actor="designer-cli", clearance="secret",
|
|
126
|
+
can_write=True, can_admin=True, role="designer",
|
|
127
|
+
auth_mode="local-cli", session=sess)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# 生效条件:无输入;在系统临时目录下新建隔离根并构造 MdCGSecure(autoflush=1,
|
|
131
|
+
# 与 server/CLI 同款可见性兜底),退出时先 close()(落索引/关分片句柄,免
|
|
132
|
+
# atexit 兜底再去碰已删的根)再无条件递归删除该根(异常路径也删)。
|
|
133
|
+
@contextlib.contextmanager
|
|
134
|
+
def _lib(sess="sess-i50b"):
|
|
135
|
+
root = tempfile.mkdtemp(prefix="i50b_")
|
|
136
|
+
cg = None
|
|
137
|
+
try:
|
|
138
|
+
cg = MdCGSecure(root, principal=_princ(sess), autoflush=1)
|
|
139
|
+
yield cg
|
|
140
|
+
finally:
|
|
141
|
+
with contextlib.suppress(Exception):
|
|
142
|
+
if cg is not None:
|
|
143
|
+
cg.close()
|
|
144
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# 生效条件:cg 有 inbox_log 属性;返回其 JSONL 全记录(文件不存在或空行即空
|
|
148
|
+
# 列表);单行畸形按跳过处理,不抛。
|
|
149
|
+
def _inbox(cg):
|
|
150
|
+
p = getattr(cg, "inbox_log", None)
|
|
151
|
+
if not p or not os.path.exists(p):
|
|
152
|
+
return []
|
|
153
|
+
out = []
|
|
154
|
+
with open(p, encoding="utf-8") as f:
|
|
155
|
+
for line in f:
|
|
156
|
+
line = line.strip()
|
|
157
|
+
if not line:
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
out.append(json.loads(line))
|
|
161
|
+
except ValueError:
|
|
162
|
+
continue
|
|
163
|
+
return out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# 生效条件:cg 有 root 属性;返回 `_forgetting.jsonl` 里 node_id 等于 nid 的
|
|
167
|
+
# 最后一条记录(无则空 dict)。
|
|
168
|
+
def _forget_rec(cg, nid):
|
|
169
|
+
p = os.path.join(cg.root, "_forgetting.jsonl")
|
|
170
|
+
if not os.path.exists(p):
|
|
171
|
+
return {}
|
|
172
|
+
hit = {}
|
|
173
|
+
with open(p, encoding="utf-8") as f:
|
|
174
|
+
for line in f:
|
|
175
|
+
line = line.strip()
|
|
176
|
+
if not line:
|
|
177
|
+
continue
|
|
178
|
+
try:
|
|
179
|
+
rec = json.loads(line)
|
|
180
|
+
except ValueError:
|
|
181
|
+
continue
|
|
182
|
+
if rec.get("node_id") == nid:
|
|
183
|
+
hit = rec
|
|
184
|
+
return hit
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# 生效条件:k 给定时在隔离根建「基线节点 + 一条半重复写入」;产出 (cg, out,
|
|
188
|
+
# content)。基线层与新写层同为 layer(缺省 knowledge:无 writelimit 介入,只
|
|
189
|
+
# 走 forgetting 闸),role 缺省 user(外部惊奇来源),hint 缺省 0.60(插件缺省
|
|
190
|
+
# 形态)。退出时删根。
|
|
191
|
+
@contextlib.contextmanager
|
|
192
|
+
def _half_dup_case(k, nid="w", hint=0.60, layer="knowledge", role="user"):
|
|
193
|
+
root = tempfile.mkdtemp(prefix="i50b_case_")
|
|
194
|
+
cg = None
|
|
195
|
+
try:
|
|
196
|
+
cg = MdCGSecure(root, principal=_princ(), autoflush=1)
|
|
197
|
+
cg.add("base", _doc(T_BASE, BASE), layer=layer, importance=0.5,
|
|
198
|
+
role=role)
|
|
199
|
+
content = _doc(T_NEW, _mixed(k))
|
|
200
|
+
out = cg.remember_gated(nid, content, layer=layer, role=role,
|
|
201
|
+
gated=True, importance_hint=hint)
|
|
202
|
+
yield cg, out, content
|
|
203
|
+
finally:
|
|
204
|
+
with contextlib.suppress(Exception):
|
|
205
|
+
if cg is not None:
|
|
206
|
+
cg.close()
|
|
207
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------- A 修复面
|
|
211
|
+
def g_a():
|
|
212
|
+
print("== A 遗忘 DEFER → 正文进审核队列 ==")
|
|
213
|
+
with _half_dup_case(K_HALF) as (cg, out, content):
|
|
214
|
+
dup = ((out.get("gate") or {}).get("redundancy") or {}).get("max")
|
|
215
|
+
ok(out.get("verdict") == "DEFER" and 0.60 <= (dup or 0) < 0.85,
|
|
216
|
+
"A1 半重复 + hint=0.60 → DEFER(dup=%.4f)" % (dup or -1),
|
|
217
|
+
out.get("verdict"))
|
|
218
|
+
recs = _inbox(cg)
|
|
219
|
+
ok(len(recs) == 1, "A2 审核队列恰 1 条", len(recs))
|
|
220
|
+
rec = recs[0] if recs else {}
|
|
221
|
+
ok(rec.get("id") == "w", "A3 队列条目的 node_id 即本次节点", rec.get("id"))
|
|
222
|
+
ok(rec.get("content") == content,
|
|
223
|
+
"A4 队列面正文与原文**逐字相等**(含换行/空白)")
|
|
224
|
+
ok(rec.get("layer") == "knowledge", "A5 队列条目 layer 随写入",
|
|
225
|
+
rec.get("layer"))
|
|
226
|
+
why = ((rec.get("extra") or {}).get("defer_reason") or "")
|
|
227
|
+
ok(("遗忘闸门" in why and "半重复" in why and "base" in why),
|
|
228
|
+
"A6 「为何待定」在位且含重复对象 id(extra.defer_reason)", why)
|
|
229
|
+
ok(rec.get("sensitivity") == "internal",
|
|
230
|
+
"A7 密级随 sensitivity 透传(不静默降级)", rec.get("sensitivity"))
|
|
231
|
+
ok(cg.get("w") is None, "A8 DEFER 不落盘")
|
|
232
|
+
lg = _forget_rec(cg, "w")
|
|
233
|
+
ok(lg.get("verdict") == "DEFER" and "reason" in lg and "importance" in lg
|
|
234
|
+
and "entropy" in lg and "actor" in lg and "limiter" not in lg,
|
|
235
|
+
"A9 forgetting 留痕行仍在且形状不变(无 limiter 键)", sorted(lg))
|
|
236
|
+
ok(bool(out.get("proposed")) and out.get("proposed") == rec.get("pid"),
|
|
237
|
+
"A10 返回体 proposed == 队列 pid(可检索句柄)",
|
|
238
|
+
(out.get("proposed"), rec.get("pid")))
|
|
239
|
+
vis = [r for r in cg.review_list() if r.get("content") == content]
|
|
240
|
+
ok(len(vis) == 1 and vis[0].get("id") == "w",
|
|
241
|
+
"A11 **队列读面**(review_list)同样能取回该正文")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------- B 幂等
|
|
245
|
+
def g_b():
|
|
246
|
+
print("== B 幂等:同内容重复写入不长第二条 ==")
|
|
247
|
+
with _half_dup_case(K_HALF) as (cg, out1, content):
|
|
248
|
+
pid1 = out1.get("proposed")
|
|
249
|
+
out2 = cg.remember_gated("w2", content, layer="knowledge", role="user",
|
|
250
|
+
gated=True, importance_hint=0.60)
|
|
251
|
+
recs = _inbox(cg)
|
|
252
|
+
ok(out2.get("verdict") == "DEFER",
|
|
253
|
+
"B1 同内容再写(换 node_id)仍判 DEFER", out2.get("verdict"))
|
|
254
|
+
ok(len(recs) == 1, "B2 队列仍 1 条(payload_hash 幂等对账)", len(recs))
|
|
255
|
+
ok(bool(pid1) and out2.get("proposed") == pid1,
|
|
256
|
+
"B3 幂等返回既有 pid(同一单点)", (out2.get("proposed"), pid1))
|
|
257
|
+
ok(len([r for r in recs if r.get("content") == content]) == 1,
|
|
258
|
+
"B4 队列里该内容的记录恰 1 条")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------- C 裁决
|
|
262
|
+
def g_c():
|
|
263
|
+
print("== C 裁决:accept 落盘 / reject 不落盘 ==")
|
|
264
|
+
with _half_dup_case(K_EDGE) as (cg, out, content):
|
|
265
|
+
pid = out.get("proposed")
|
|
266
|
+
acc = cg.review_decide(pid, "accept", reason="i50b 守卫")
|
|
267
|
+
ok(bool(acc.get("ok")), "C1 队列裁定 accept 返回 ok", acc)
|
|
268
|
+
node = cg.get("w")
|
|
269
|
+
ok(node is not None, "C2 accept 后正文真的落盘")
|
|
270
|
+
ok((node or {}).get("content") == content,
|
|
271
|
+
"C3 落盘正文与原文逐字相等")
|
|
272
|
+
with _half_dup_case(K_TOP) as (cg, out, content):
|
|
273
|
+
pid = out.get("proposed")
|
|
274
|
+
rej = cg.review_decide(pid, "reject", reason="i50b 守卫")
|
|
275
|
+
ok(bool(rej.get("ok")), "C4 队列裁定 reject 返回 ok", rej)
|
|
276
|
+
ok(cg.get("w") is None, "C5 reject 后不落盘")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------- D 限流
|
|
280
|
+
def g_d():
|
|
281
|
+
print("== D 限流 DEFER 不入队(语义是先别写,不是内容待定)==")
|
|
282
|
+
with _lib() as cg:
|
|
283
|
+
writelimit._save(cg, {"sigs": {}, "rate": {}})
|
|
284
|
+
for i in range(8):
|
|
285
|
+
cg.remember_gated("rl%d" % i, "盘点条目甲乙丙丁戊%d号" % i,
|
|
286
|
+
layer="contextual", role="user", gated=True)
|
|
287
|
+
rl = cg.remember_gated("rl9", "盘点条目甲乙丙丁戊9号",
|
|
288
|
+
layer="contextual", role="user", gated=True)
|
|
289
|
+
g = rl.get("gate") or {}
|
|
290
|
+
ok(rl.get("verdict") == "DEFER" and "ratelimit" in (g.get("reason") or ""),
|
|
291
|
+
"D1 窗口超量 → 限流 DEFER", g.get("reason"))
|
|
292
|
+
ok(not rl.get("proposed"),
|
|
293
|
+
"D2 限流 DEFER 不带 proposed(不入队)", rl.get("proposed"))
|
|
294
|
+
ok(len(_inbox(cg)) == 0, "D3 队列 0 条")
|
|
295
|
+
lg = _forget_rec(cg, "rl9")
|
|
296
|
+
ok(bool(lg.get("limiter")), "D4 限流留痕照旧(limiter 键在位)",
|
|
297
|
+
sorted(lg))
|
|
298
|
+
cg.add("base", _doc(T_BASE, BASE), layer="knowledge", importance=0.5,
|
|
299
|
+
role="user")
|
|
300
|
+
r2 = cg.remember_gated("w", _doc(T_NEW, _mixed(K_HALF)),
|
|
301
|
+
layer="knowledge", role="user", gated=True,
|
|
302
|
+
importance_hint=0.60)
|
|
303
|
+
ok(r2.get("verdict") == "DEFER" and len(_inbox(cg)) == 1,
|
|
304
|
+
"D5 反向腿:knowledge 半重复 DEFER 仍入队 1 条",
|
|
305
|
+
(r2.get("verdict"), len(_inbox(cg))))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------- E 旁路
|
|
309
|
+
def g_e():
|
|
310
|
+
print("== E gated=False 直写旁路不受影响 ==")
|
|
311
|
+
with _lib() as cg:
|
|
312
|
+
out = cg.remember_gated("byp", _doc(T_NEW, BASE), layer="knowledge",
|
|
313
|
+
role="user", gated=False)
|
|
314
|
+
ok(out.get("verdict") == "ACCEPT" and out.get("bypass") is True,
|
|
315
|
+
"E1 旁路 verdict=ACCEPT / bypass=True", out.get("verdict"))
|
|
316
|
+
ok(cg.get("byp") is not None, "E2 旁路正文真的落盘")
|
|
317
|
+
ok(len(_inbox(cg)) == 0, "E3 旁路不进审核队列")
|
|
318
|
+
ok("proposed" not in out, "E4 旁路返回体无 proposed 键", sorted(out))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ---------------------------------------------------------------- F 双跑
|
|
322
|
+
def g_f():
|
|
323
|
+
print("== F 同一次写入只产生 1 条队列记录(双跑去重)==")
|
|
324
|
+
with _half_dup_case(K_HALF) as (cg, out, content):
|
|
325
|
+
ok(len(_inbox(cg)) == 1, "F1 单次 gated 调用 → 恰 1 条",
|
|
326
|
+
len(_inbox(cg)))
|
|
327
|
+
root = tempfile.mkdtemp(prefix="i50b_wp_")
|
|
328
|
+
try:
|
|
329
|
+
cg = MdCGSecure(root, principal=_princ("sess-wp"), autoflush=1)
|
|
330
|
+
cg.add("p0", _doc(T_BASE, BASE), layer="knowledge", importance=0.5,
|
|
331
|
+
role="user")
|
|
332
|
+
content = _doc(T_NEW, _mixed(K_HALF))
|
|
333
|
+
out = default_pipeline().execute(cg, {
|
|
334
|
+
"node_id": "p0_w", "content": content, "layer": "knowledge",
|
|
335
|
+
"role": "user", "content_kind": "text", "gated": True})
|
|
336
|
+
gate = out.get("gate") or {}
|
|
337
|
+
ok(gate.get("verdict") == "DEFER",
|
|
338
|
+
"F2 writepipe 全链(audit ACCEPT → gated DEFER)判 DEFER",
|
|
339
|
+
gate.get("verdict"))
|
|
340
|
+
recs = _inbox(cg)
|
|
341
|
+
ok(len(recs) == 1,
|
|
342
|
+
"F3 writepipe 全链 → 队列恰 1 条(audit 未在 ACCEPT 时入队、不双跑)",
|
|
343
|
+
len(recs))
|
|
344
|
+
ok(len([r for r in recs if r.get("content") == content]) == 1,
|
|
345
|
+
"F4 队列里该内容的记录恰 1 条")
|
|
346
|
+
ok(bool(gate.get("proposed")),
|
|
347
|
+
"F5 writepipe 返回体保留了入队 pid(gate.proposed)",
|
|
348
|
+
gate.get("proposed"))
|
|
349
|
+
finally:
|
|
350
|
+
with contextlib.suppress(Exception):
|
|
351
|
+
cg.close()
|
|
352
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
_GROUPS = (g_a, g_b, g_c, g_d, g_e, g_f)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _run_fails():
|
|
359
|
+
"""跑全部断言组(静默),返回失败断言名列表——供变异自证复用。"""
|
|
360
|
+
_PASS.clear()
|
|
361
|
+
_FAIL.clear()
|
|
362
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
363
|
+
for g in _GROUPS:
|
|
364
|
+
g()
|
|
365
|
+
return list(_FAIL)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _run_groups():
|
|
369
|
+
return len(_run_fails())
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ---------------------------------------------------------------- 定点变异自证
|
|
373
|
+
# 表内每项 = (说明, 锚点原文, 替换文, 预期红项数)。锚点必须**逐字**出现在当前
|
|
374
|
+
# `MdCGOS.remember_gated` 的源码里;漂移即 ANCHOR-MISS(fail-closed,exit 2)。
|
|
375
|
+
# issue50-c F1 后入队单点把调用方 meta 一并透传(`**kw`);F2 后 node_id 走
|
|
376
|
+
# 内容派生兜底(`_nid`)。锚点随实现同步,判据(红项数)不变。
|
|
377
|
+
_PROPOSE_BLOCK = (' out["proposed"] = self.propose(\n'
|
|
378
|
+
' _nid, content, layer=layer, **kw, '
|
|
379
|
+
'defer_reason=_why)')
|
|
380
|
+
|
|
381
|
+
_SRC_MUTATIONS = (
|
|
382
|
+
# ① 契约指定:DEFER 出口不接线(入队单点整个摘掉)。实测红项 20:
|
|
383
|
+
# A1/A8/A9 三条不依赖队列的判据仍绿,其余 A2-A7/A10/A11(8)+ B2-B4(3)
|
|
384
|
+
# + C1-C4(4;C5「reject 不落盘」在无提案时反而为真)+ D5(1)
|
|
385
|
+
# + F1/F3/F4/F5(4)= 20。
|
|
386
|
+
("DEFER 出口不接线(删掉 propose 调用)",
|
|
387
|
+
_PROPOSE_BLOCK,
|
|
388
|
+
' out["proposed"] = None',
|
|
389
|
+
20),
|
|
390
|
+
# ② 契约指定:入队正文传成空串——「正文可取回」那几条必红。实测红项 5:
|
|
391
|
+
# A4/A11(逐字比对)+ B4 + C3 + F4——**所有「队列恰 1 条」类计数断言不红**
|
|
392
|
+
# (空串也是 1 条),这正是本变异要区分的:它只钉住「正文」而非「条数」。
|
|
393
|
+
("入队正文传成空串(正文可取回必红)",
|
|
394
|
+
_PROPOSE_BLOCK,
|
|
395
|
+
' out["proposed"] = self.propose(\n'
|
|
396
|
+
' _nid, "", layer=layer, **kw, defer_reason=_why)',
|
|
397
|
+
5),
|
|
398
|
+
# ③ 限流 DEFER 也入队(违背「先别写」语义)——D2/D3/D5 必红(实测 3)。
|
|
399
|
+
("限流 DEFER 也入队(违背先别写语义)",
|
|
400
|
+
' else: # DEFER\n'
|
|
401
|
+
' out = {"verdict": "DEFER", "node_id": node_id,\n'
|
|
402
|
+
' "gate": lim}\n'
|
|
403
|
+
' fv = "DEFER"',
|
|
404
|
+
' else: # DEFER\n'
|
|
405
|
+
' out = {"verdict": "DEFER", "node_id": node_id,\n'
|
|
406
|
+
' "gate": lim}\n'
|
|
407
|
+
' fv = "DEFER"\n'
|
|
408
|
+
' out["proposed"] = self.propose(node_id, content,\n'
|
|
409
|
+
' layer=layer)',
|
|
410
|
+
3),
|
|
411
|
+
# ④ gated=False 旁路也入队——E3 必红(实测 1)。
|
|
412
|
+
("gated=False 旁路也入队",
|
|
413
|
+
' if not gated:\n'
|
|
414
|
+
' return {"verdict": "ACCEPT", "bypass": True, "gate": None,',
|
|
415
|
+
' if not gated:\n'
|
|
416
|
+
' self.propose(node_id, content, layer=layer)\n'
|
|
417
|
+
' return {"verdict": "ACCEPT", "bypass": True, "gate": None,',
|
|
418
|
+
1),
|
|
419
|
+
# ⑤ 模拟「双跑」:第二条入队带零宽字符绕开 payload_hash 幂等。**为何要绕**:
|
|
420
|
+
# 纯重复调用会被 propose 自己的对账吸收(第二次不落行),打不红 F——这本身
|
|
421
|
+
# 就是本批「双跑不致双条」的第二道防线(见模块 docstring)。本变异一次写入
|
|
422
|
+
# 长出两条队列记录 ⇒ F1/F3(恰 1 条)与 B2、D5 同类计数据必红(实测 5:
|
|
423
|
+
# A2、B2、D5、F1、F3)。
|
|
424
|
+
("双跑(第二路绕开幂等键)",
|
|
425
|
+
_PROPOSE_BLOCK,
|
|
426
|
+
_PROPOSE_BLOCK + '\n'
|
|
427
|
+
' self.propose(node_id, content + "\\u200b", layer=layer,\n'
|
|
428
|
+
' defer_reason=_why)',
|
|
429
|
+
5),
|
|
430
|
+
# ⑥ DEFER 不再写 forgetting 留痕(违背契约③「留痕行形状不变」)——A9 必红。
|
|
431
|
+
("DEFER 不写 forgetting 留痕",
|
|
432
|
+
' if v != "DROP":',
|
|
433
|
+
' if v not in ("DROP", "DEFER"):',
|
|
434
|
+
1),
|
|
435
|
+
# ⑦ 「为何待定」文案清空——A6 必红。
|
|
436
|
+
("defer_reason 文案清空",
|
|
437
|
+
' _why = "遗忘闸门:" + str(verdict.get("reason") or "待定复核")',
|
|
438
|
+
' _why = ""',
|
|
439
|
+
1),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# 静态锚点(默认模式也自检,fail-closed):
|
|
443
|
+
# · 入队单点必须**仍是 self.propose**(不得被换成另一套入队实现)
|
|
444
|
+
# · DEFER 分支必须在位,且限流 DEFER 出口不得带 propose
|
|
445
|
+
_ANCHORS_REQUIRED = (
|
|
446
|
+
'out["proposed"] = self.propose(',
|
|
447
|
+
'layer=layer, **kw, defer_reason=_why)',
|
|
448
|
+
'elif v == "DEFER":',
|
|
449
|
+
)
|
|
450
|
+
_ANCHORS_BANNED = (
|
|
451
|
+
# 限流 DEFER 出口旁出现 propose ⇒ 违契约④
|
|
452
|
+
'"gate": lim}\n fv = "DEFER"\n out["proposed"]',
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _gated_src():
|
|
457
|
+
return inspect.getsource(MdCGOS.remember_gated)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _anchor_check():
|
|
461
|
+
"""返回 ANCHOR-MISS 说明列表(空 = 全部在位)。"""
|
|
462
|
+
src = _gated_src()
|
|
463
|
+
bad = []
|
|
464
|
+
for name, old, _new, _n in _SRC_MUTATIONS:
|
|
465
|
+
if old not in src:
|
|
466
|
+
bad.append("变异锚点缺失:%s(锚点 %r)" % (name, old[:60]))
|
|
467
|
+
for s in _ANCHORS_REQUIRED:
|
|
468
|
+
if s not in src:
|
|
469
|
+
bad.append("必需锚点缺失:%r" % s)
|
|
470
|
+
for s in _ANCHORS_BANNED:
|
|
471
|
+
if s in src:
|
|
472
|
+
bad.append("禁用锚点回归(限流 DEFER 被接线):%r" % s)
|
|
473
|
+
return bad
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# 生效条件:old 与 new 给定时,取当前 MdCGOS.remember_gated 的源码做字面替换,
|
|
477
|
+
# 以 mdcos 的模块 globals 副本 exec 出新函数;返回该函数对象(不落盘、不改源文件)。
|
|
478
|
+
# 方法体在类里缩进 4 格,故用 `if True:` 前缀承接(不用 dedent,避免空白行被
|
|
479
|
+
# textwrap 归一后与表内锚点错位)。
|
|
480
|
+
def _mutate_src(old, new):
|
|
481
|
+
ns = dict(vars(sys.modules[MdCGOS.__module__]))
|
|
482
|
+
exec(compile("if True:\n" + _gated_src().replace(old, new),
|
|
483
|
+
"i50b_mut.py", "exec"), ns)
|
|
484
|
+
return ns["remember_gated"]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _with_patched(fn, run):
|
|
488
|
+
"""把 MdCGOS.remember_gated 临时换成 fn 跑一遍 run(),finally 原样还原。"""
|
|
489
|
+
live = MdCGOS.remember_gated
|
|
490
|
+
MdCGOS.remember_gated = fn
|
|
491
|
+
try:
|
|
492
|
+
return run()
|
|
493
|
+
finally:
|
|
494
|
+
MdCGOS.remember_gated = live
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _mutate_mode():
|
|
498
|
+
bad = []
|
|
499
|
+
anchor_bad = _anchor_check()
|
|
500
|
+
if anchor_bad:
|
|
501
|
+
for b in anchor_bad:
|
|
502
|
+
print(" ANCHOR-MISS " + b)
|
|
503
|
+
print("\n锚点自检:FAIL(fail-closed,exit 2)")
|
|
504
|
+
return 2
|
|
505
|
+
clean = _run_groups()
|
|
506
|
+
print(" 未变异基线:红项=%d%s"
|
|
507
|
+
% (clean, "" if clean == 0 else " ← 基线即红,变异核验无意义"))
|
|
508
|
+
if clean:
|
|
509
|
+
bad.append("未变异基线即失败")
|
|
510
|
+
for name, old, new, expect in _SRC_MUTATIONS:
|
|
511
|
+
fails = _with_patched(_mutate_src(old, new), _run_fails)
|
|
512
|
+
verdict = ("命中预期" if len(fails) == expect
|
|
513
|
+
else "**红项数不符(预期 %d)**" % expect)
|
|
514
|
+
print(" 变异「%s」→ 红项=%d %s" % (name, len(fails), verdict))
|
|
515
|
+
if len(fails) != expect:
|
|
516
|
+
bad.append(name)
|
|
517
|
+
print(" 实际红项:" + "、".join(fails))
|
|
518
|
+
print("\n定点变异自证:%s"
|
|
519
|
+
% ("PASS(每处判据都有变异钉死,且红项数逐处吻合)"
|
|
520
|
+
if not bad else "FAIL —— " + "、".join(bad)))
|
|
521
|
+
return 0 if not bad else 1
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def main() -> int:
|
|
525
|
+
src = os.path.basename(os.path.abspath(__file__))
|
|
526
|
+
if "--mutate" in sys.argv:
|
|
527
|
+
print("!! 定点变异模式:逐个变异判据,套件应转红且红项数吻合\n")
|
|
528
|
+
return _mutate_mode()
|
|
529
|
+
anchor_bad = _anchor_check()
|
|
530
|
+
if anchor_bad:
|
|
531
|
+
for b in anchor_bad:
|
|
532
|
+
print(" ANCHOR-MISS " + b)
|
|
533
|
+
print("\n锚点自检:FAIL(fail-closed,exit 2)——实现改了请同步变异表/文案")
|
|
534
|
+
return 2
|
|
535
|
+
print("锚点自检:PASS(%s;不以 git HEAD 为基线源)" % src)
|
|
536
|
+
for g in _GROUPS:
|
|
537
|
+
g()
|
|
538
|
+
print("\nissue50-b 守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
|
|
539
|
+
if _FAIL:
|
|
540
|
+
print("失败项:" + "、".join(_FAIL))
|
|
541
|
+
return 0 if not _FAIL else 1
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
sys.exit(main())
|