@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
package/md_cg/mdcos.py
CHANGED
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
import hashlib
|
|
22
22
|
import json
|
|
23
|
+
import math
|
|
23
24
|
import os
|
|
24
25
|
import re
|
|
25
26
|
import time
|
|
@@ -30,11 +31,13 @@ from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT
|
|
|
30
31
|
TIER_BUCKET_SCAN, TIER_GLOBAL_LIKE, TIER_GLOBAL_SCAN,
|
|
31
32
|
GLOBAL_CAP, expand_query_terms_weighted,
|
|
32
33
|
expand_query_terms_llm, en_zh_bigrams, semantic_on,
|
|
33
|
-
cut_by_relevance, apply_retrieval_gates, NEG_ROUTE_LAYERS
|
|
34
|
+
cut_by_relevance, apply_retrieval_gates, NEG_ROUTE_LAYERS,
|
|
35
|
+
# P2-3(§5.4):六要素后两行索引键的召回判据单点(mdcg 侧)
|
|
36
|
+
index_key_hits, boundary_mark)
|
|
34
37
|
from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
35
38
|
identity, consistency, metacognition, crypto, sustain,
|
|
36
39
|
self_state, predict, evolution, weights, pooling,
|
|
37
|
-
writelimit, reach, trust, roleviews)
|
|
40
|
+
writelimit, reach, trust, roleviews, autonomy_modes)
|
|
38
41
|
from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
|
|
39
42
|
read_jsonl_tail, count_jsonl, publish)
|
|
40
43
|
from .security import (Principal, TenantRegistry, AccessDenied,
|
|
@@ -64,6 +67,37 @@ DEFAULT_BUDGET = 1200 # recall 默认 token 预算
|
|
|
64
67
|
# 0 = 关闭截断,回到"超大一律跳过"的旧行为。
|
|
65
68
|
DEFAULT_MAX_ITEM_TOKENS = 250
|
|
66
69
|
|
|
70
|
+
# ---- P3-temporal:时间路的核与口径(设计稿 §6.3,裁定 12)-------------------
|
|
71
|
+
# 核**必须**来自唯一权威 `md_cg/whitebox_kb/aeis_core/time_core.py::cred_factor`
|
|
72
|
+
# (该模块 docstring 逐字点名 `exp(-t/τ)` 为 bug);本模块**不写任何指数核**。
|
|
73
|
+
#: γ(衰减率)可配 env —— 与 `MDCG_SPREAD_DECAY` 同款:缺失/非法即回落缺省。
|
|
74
|
+
TEMPORAL_GAMMA_ENV = "MDCG_TEMPORAL_GAMMA"
|
|
75
|
+
#: 半衰期真源 = `links.DECAY_DAYS`(30 天,`md_cg/links.py:53`)——不复制字面量,
|
|
76
|
+
#: 由 `temporal_gamma()` 现取;此处只写明「γ 缺省 = ln2 / 半衰期」这一换算。
|
|
77
|
+
#: 单位口径(**最易错处**):γ 是「每天」的速率,故 Δt 必须**以天为单位**喂入。
|
|
78
|
+
TEMPORAL_DT_UNIT = "day"
|
|
79
|
+
#: 分数下限(floor):`cred_factor(gamma, dt, floor, 1.0)` 的 floor。
|
|
80
|
+
#: 取值理由:0.5**10 ≈ 9.77e-4 < 1e-3 —— 30 天半衰期下 300 天(10 个半衰期)
|
|
81
|
+
#: 之后衰减已落到 1e-3 以下,继续区分无信息量;floor=1e-3 让**老节点/缺
|
|
82
|
+
#: created_at 的节点**仍带非零分进入本路,而不是被乘成 0 后不可召回
|
|
83
|
+
#:(读数见 `md_cg/test_time_core_lint.py` 的 G4 floor 组)。
|
|
84
|
+
TEMPORAL_SCORE_FLOOR = 0.001
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# 生效条件:environ(缺省 os.environ)里 TEMPORAL_GAMMA_ENV 为真值且 float() 可解析且 > 0 时返回该值;缺失/空串/不可解析/非正数一律回落 `math.log(2)/links.DECAY_DAYS`(缺省半衰期 30 天)。
|
|
88
|
+
def temporal_gamma(environ=None) -> float:
|
|
89
|
+
"""时间邻近度衰减率 γ(**每天**)的唯一读取点。"""
|
|
90
|
+
raw = (environ or os.environ).get(TEMPORAL_GAMMA_ENV)
|
|
91
|
+
if raw:
|
|
92
|
+
try:
|
|
93
|
+
g = float(raw)
|
|
94
|
+
except (TypeError, ValueError):
|
|
95
|
+
g = 0.0
|
|
96
|
+
if g > 0:
|
|
97
|
+
return g
|
|
98
|
+
from .links import DECAY_DAYS # 半衰期唯一真源(links.py:53,30 天)
|
|
99
|
+
return math.log(2.0) / float(DECAY_DAYS)
|
|
100
|
+
|
|
67
101
|
|
|
68
102
|
# 生效条件:text 为假值(None/空串)时返回 0,否则按「CJK 0.6/字 + 其余 /4」计算并返回 int(cjk*0.6 + other/4) + 1。
|
|
69
103
|
def est_tokens(text: str) -> int:
|
|
@@ -846,8 +880,9 @@ class MdCGOS(MdCG):
|
|
|
846
880
|
详见 _candidates。
|
|
847
881
|
"""
|
|
848
882
|
q = (query or "").strip()
|
|
849
|
-
# 批次 15 统一口径(unify.py
|
|
850
|
-
#
|
|
883
|
+
# 批次 15 统一口径(unify.py,2026-09-30 收窄作用域):只译**英文内容**
|
|
884
|
+
# → 标准原子序列,中文段逐字原样(不再整条归一);与
|
|
885
|
+
# MdCG.search/search_rrf 三入口同口径(归一层缺省关,显式 =1 才开)
|
|
851
886
|
from .semantic.unify import unify_query
|
|
852
887
|
q = unify_query(q)
|
|
853
888
|
if not q:
|
|
@@ -940,7 +975,9 @@ class MdCGOS(MdCG):
|
|
|
940
975
|
if in_bucket:
|
|
941
976
|
docs = self._read_many(in_bucket, stat)
|
|
942
977
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
943
|
-
hits = [d for d in docs
|
|
978
|
+
hits = [d for d in docs
|
|
979
|
+
if self._like(d[2], d[1], terms,
|
|
980
|
+
index_key_hits(d[0], terms, q))
|
|
944
981
|
or (semantic_on() and d[1].get("semantic"))]
|
|
945
982
|
out = try_stage(hits, TIER_BUCKET_LIKE)
|
|
946
983
|
if out:
|
|
@@ -966,7 +1003,8 @@ class MdCGOS(MdCG):
|
|
|
966
1003
|
docs_r = self._read_many(reach_entries, stat)
|
|
967
1004
|
_dif = set(_rstat.get("reach_diffused_paths") or ())
|
|
968
1005
|
hits_r = [d for d in docs_r
|
|
969
|
-
if self._like(d[2], d[1], terms
|
|
1006
|
+
if self._like(d[2], d[1], terms,
|
|
1007
|
+
index_key_hits(d[0], terms, q))
|
|
970
1008
|
or (semantic_on() and d[1].get("semantic"))
|
|
971
1009
|
or d[0].get("path") in _dif] # 图扩散补召回:无词面命中也放行进打分
|
|
972
1010
|
stat["pre_cap"] = len(hits_r) # 与 T2 同序:截断**前**的候选数
|
|
@@ -998,7 +1036,9 @@ class MdCGOS(MdCG):
|
|
|
998
1036
|
# 截断依据=相关度(同 MdCG.search:cap 值不变,改的是拿什么排序)
|
|
999
1037
|
docs_all = self._read_many(entries, stat)
|
|
1000
1038
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
1001
|
-
hits = [d for d in docs_all
|
|
1039
|
+
hits = [d for d in docs_all
|
|
1040
|
+
if self._like(d[2], d[1], terms,
|
|
1041
|
+
index_key_hits(d[0], terms, q))
|
|
1002
1042
|
or (semantic_on() and d[1].get("semantic"))]
|
|
1003
1043
|
stat["pre_cap"] = len(hits)
|
|
1004
1044
|
stat["cap"] = GLOBAL_CAP
|
|
@@ -1046,7 +1086,9 @@ class MdCGOS(MdCG):
|
|
|
1046
1086
|
docs = self._read_many(entries, stat)
|
|
1047
1087
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池——
|
|
1048
1088
|
# 语义摘要=检索面(设想核心),否则摘要层只在 LIKE 全空时生效
|
|
1049
|
-
hits = [d for d in docs
|
|
1089
|
+
hits = [d for d in docs
|
|
1090
|
+
if self._like(d[2], d[1], terms,
|
|
1091
|
+
index_key_hits(d[0], terms, query))
|
|
1050
1092
|
or (semantic_on() and d[1].get("semantic"))]
|
|
1051
1093
|
if not hits:
|
|
1052
1094
|
# 兜底池(LIKE 全空 = 无相关度信号):截断依据=importance/created_at
|
|
@@ -1158,7 +1200,8 @@ class MdCGOS(MdCG):
|
|
|
1158
1200
|
# 生效条件:当 seeds 非空且 seed_map 非空时,用 relation_types 或 CHAIN_TYPES_DEFAULT、max_depth 或 MAX_DEPTH_DEFAULT、decay 调用 chain.expand_from_seeds,仅保留 best 中仍存在于 entries(按 id(e))的节点,读取成功者加入 scored 并按分数降序返回 (scored, prov);seeds/seed_map/best 为空返回 ([], {});
|
|
1159
1201
|
def _path_chain(self, query, entries, seeds, context=None,
|
|
1160
1202
|
relation_types=None, max_depth=None, decay=0.9):
|
|
1161
|
-
"""
|
|
1203
|
+
"""关系链路径(= 设计稿 §6.2 的**因果路**):沿 causal/sequential/
|
|
1204
|
+
applies_to 边**多跳**扩散。
|
|
1162
1205
|
|
|
1163
1206
|
理论依据:`causal` = 条件依赖因果(A 是 B 成立的条件),
|
|
1164
1207
|
`dex_chain` 沿 causal 边正向展开、每步标注条件,**链 = 条件序列**。
|
|
@@ -1170,6 +1213,13 @@ class MdCGOS(MdCG):
|
|
|
1170
1213
|
本路按边类型权重(causal .85 / sequential .60 …)、逐跳乘边置信度、
|
|
1171
1214
|
默认 5 跳、visited 剪枝——「检索使用关系链」的落地。
|
|
1172
1215
|
|
|
1216
|
+
**边类型集可配(P3-causal)**:relation_types 缺省不再写死
|
|
1217
|
+
`CHAIN_TYPES_DEFAULT`,而走 `chain.chain_types_from_env()`(env
|
|
1218
|
+
`MDCG_CHAIN_TYPES`,缺省仍 = `CHAIN_TYPES_DEFAULT`)。动因:叙事/
|
|
1219
|
+
文档语料(按章切出的 doc_ref 库)里**没有 causal 边**,只有
|
|
1220
|
+
`reference` 与父章节结构边 `part_of`——写死即本路在这类库上恒空。
|
|
1221
|
+
未登记进 `EDGE_WEIGHTS` 的形态被读取点剔除(不静默走未知边)。
|
|
1222
|
+
|
|
1173
1223
|
返回 (scored, prov);prov[nid] 带该节点**最强链**的节点序列与条件序列,
|
|
1174
1224
|
使「为什么召回它」可审计。
|
|
1175
1225
|
"""
|
|
@@ -1182,7 +1232,8 @@ class MdCGOS(MdCG):
|
|
|
1182
1232
|
seed_map[nid] = float(s)
|
|
1183
1233
|
if not seed_map:
|
|
1184
1234
|
return [], {}
|
|
1185
|
-
rels = tuple(relation_types) if relation_types
|
|
1235
|
+
rels = (tuple(relation_types) if relation_types
|
|
1236
|
+
else chain.chain_types_from_env())
|
|
1186
1237
|
depth = chain.MAX_DEPTH_DEFAULT if max_depth is None else max_depth
|
|
1187
1238
|
best = chain.expand_from_seeds(self, seed_map, relation_types=rels,
|
|
1188
1239
|
max_depth=depth, decay=decay)
|
|
@@ -1209,6 +1260,65 @@ class MdCGOS(MdCG):
|
|
|
1209
1260
|
scored.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
|
|
1210
1261
|
return scored, prov
|
|
1211
1262
|
|
|
1263
|
+
def _path_temporal(self, entries, q_start, q_end, start_op, end_op,
|
|
1264
|
+
now=None):
|
|
1265
|
+
"""时间路(P3-temporal,设计稿 §6.3):按**时间邻近度**排序的排名项。
|
|
1266
|
+
|
|
1267
|
+
口径(§〇.3-12 已裁,逐条钉在此处):
|
|
1268
|
+
|
|
1269
|
+
* **核走唯一权威**:`time_core.cred_factor(gamma, dt, floor, 1.0)`
|
|
1270
|
+
——本模块**不写任何指数核**(`exp(-t/τ)`、`×(1-factor)`、EMA 保持率
|
|
1271
|
+
均是 time_core docstring 点名的 bug 形态)。
|
|
1272
|
+
* **γ 缺省 = ln2 / 30 天**(半衰期 30 天,与 `links.py:53` 同刻度),
|
|
1273
|
+
env `MDCG_TEMPORAL_GAMMA` 可覆盖;唯一读取点 `temporal_gamma()`。
|
|
1274
|
+
* **Δt 取 `created_at`**(写入时刻,主链必有)。
|
|
1275
|
+
* **单位必须显式是「天」**:γ 是「每天」的速率,Δt 先除以 86400 再喂
|
|
1276
|
+
——喂秒会让衰减快 86400 倍(守卫 `test_time_core_lint.py` G3 组有该反证)。
|
|
1277
|
+
* **加 floor**(`TEMPORAL_SCORE_FLOOR`):老节点/缺 `created_at` 的节点
|
|
1278
|
+
不被乘成 0 而永不可召回。
|
|
1279
|
+
* **不做多跳扩散**:时间天然「1 跳」,本路是**排名项**(与 S4 层级加成
|
|
1280
|
+
同类),不是扩散路。
|
|
1281
|
+
* **种子不来自词法命中**:时间类问句常词面零重叠,故种子取
|
|
1282
|
+
**时间算子/区间命中集**——`entries`(已过 S1/S2 门控 ∧ 已过
|
|
1283
|
+
`trust.filter_by_time` 的索引标量过滤),**不 `_scan` 全库**、
|
|
1284
|
+
不读正文即可算分(`created_at` 在索引快照里)。
|
|
1285
|
+
|
|
1286
|
+
⚠ **理论边界标注**:理论原文 `exp(-γ·t)`(`docs/theory/智能论3.4.md:2122-2125`
|
|
1287
|
+
DEV-005)说的是 **confidence 衰减**;把它用于「时间邻近度」是**工程口径
|
|
1288
|
+
的类比迁移**,不是理论原话——同一核形状、不同语义(新近度权重)。
|
|
1289
|
+
"""
|
|
1290
|
+
if not any(x is not None for x in (q_start, q_end, start_op, end_op)):
|
|
1291
|
+
return [] # 无时间算子/区间 → 本路不启用(默认查询零变更)
|
|
1292
|
+
from .whitebox_kb.aeis_core import time_core as _tc # 懒导入(唯一权威)
|
|
1293
|
+
gamma = temporal_gamma()
|
|
1294
|
+
ref = q_end if q_end is not None else q_start
|
|
1295
|
+
if ref is None:
|
|
1296
|
+
ref = now if now is not None else time.time()
|
|
1297
|
+
out = []
|
|
1298
|
+
for e in entries:
|
|
1299
|
+
try:
|
|
1300
|
+
created = float(e.get("created_at") or 0.0)
|
|
1301
|
+
except (TypeError, ValueError):
|
|
1302
|
+
created = 0.0
|
|
1303
|
+
if created <= 0:
|
|
1304
|
+
# 写入时刻缺失:不猜测时刻,判「距参照无穷远」——floor 兜住
|
|
1305
|
+
#(仍可召回,不因缺字段整条消失)。
|
|
1306
|
+
dt_days = float("inf")
|
|
1307
|
+
else:
|
|
1308
|
+
# **单位=天**:86400 秒/天,显式换算(γ 是每天速率)
|
|
1309
|
+
dt_days = abs(created - float(ref)) / 86400.0
|
|
1310
|
+
score = _tc.cred_factor(gamma, dt_days, TEMPORAL_SCORE_FLOOR, 1.0)
|
|
1311
|
+
fm, c = self._read(e)
|
|
1312
|
+
if c is None:
|
|
1313
|
+
continue
|
|
1314
|
+
c = self._open_content(fm.get("id"), fm, c)
|
|
1315
|
+
if c is None:
|
|
1316
|
+
continue
|
|
1317
|
+
out.append(({"id": fm.get("id") or e["path"], "frontmatter": fm,
|
|
1318
|
+
"content": c, "path": e["path"]}, round(score, 6)))
|
|
1319
|
+
out.sort(key=lambda x: (-x[1], str(x[0].get("id") or "")))
|
|
1320
|
+
return out
|
|
1321
|
+
|
|
1212
1322
|
# 生效条件:以 expand or expand_query_terms_weighted 作扩展函数并对 query 调用(结果为假值按 {} 处理),从其中 pop "__source__"(缺键为 "whitebox")得 source,tw 为空返回 ([], source),否则对 entries 中存在 _weighted_coverage>0 的条目按 0.6·cov+0.3·aff+0.1·ctx_aff(context 非 None 时以 route_key(ctx, ctx.get("tags")) 得 ctx_domain,context 非 dict 时用 {})打分,返回 (out, source)。
|
|
1213
1323
|
def _path_fuzzy(self, query, entries, context=None, expand=None):
|
|
1214
1324
|
"""模糊路径(分级隶属度):返回 (scored, source)。
|
|
@@ -1343,7 +1453,9 @@ class MdCGOS(MdCG):
|
|
|
1343
1453
|
# 生效条件:query 去空白为空→empty_query、候选为空→no_candidates;否则按 paths 各路召回后融合(fusion=="max" 取各路最大贡献、否则求和),recall_only 中的路只以 0 分补池不参与打分,judge 与 judge_ranking 同时为真时对 fused 前 max(k*2,10) 条做资格裁决(REJECT/BLINDSPOT 剔除、DEFER 降权 0.5),否则直接取 fused 前 k;validity 真值时候选层剔除已过期节点,且 query 缓存按 validity 分键不串口径。
|
|
1344
1454
|
def search_rrf(self, query: str, k: int = 20, layer: str = None,
|
|
1345
1455
|
context=None, roles=None, include_work: bool = False,
|
|
1346
|
-
judge: bool = True,
|
|
1456
|
+
judge: bool = True,
|
|
1457
|
+
paths=("lexical", "bucket", "entity", "graph", "chain",
|
|
1458
|
+
"temporal"),
|
|
1347
1459
|
record: bool = True, query_expand=None,
|
|
1348
1460
|
path_weights=None, recall_only=None, fusion: str = "sum",
|
|
1349
1461
|
goal_text=None, judge_ranking: bool = False,
|
|
@@ -1358,14 +1470,28 @@ class MdCGOS(MdCG):
|
|
|
1358
1470
|
多路共同确认的记忆排在单路命中之前(对齐 noema 的 Fusion Recall)。
|
|
1359
1471
|
meta 含 per_path(各路的候选数与来源),可审计。
|
|
1360
1472
|
|
|
1361
|
-
paths: 基线四路(词法/条件桶/实体/图扩展)+
|
|
1473
|
+
paths: 基线四路(词法/条件桶/实体/图扩展)+ 增量路:
|
|
1362
1474
|
"fuzzy" 词表驱动(同义词组分级隶属度 + 大域 IDF)
|
|
1363
1475
|
"semantic" 条件结构驱动(CCG 生效条件 + condition_space 四槽;
|
|
1364
1476
|
不适用条件被整词命中即从本路剔除——条件级负路由)
|
|
1365
1477
|
"goal" 目标定向(第 5 篇第 3 章):以活跃目标文本(或显式
|
|
1366
1478
|
goal_text)扩展查询,给「与当前目标相关」的记忆加权;
|
|
1367
1479
|
无活跃目标时本路为空,等价于未启用。
|
|
1368
|
-
|
|
1480
|
+
"chain" **因果路**(P3-causal,设计稿 §6.2):以词法/实体命中为
|
|
1481
|
+
种子沿 edges 多跳扩散(复用 `md_cg/chain.py`:14 类边权、
|
|
1482
|
+
MAX_DEPTH 5、decay 0.9)。**代价口径(P3 遗留 ⑥ 如实
|
|
1483
|
+
降级,2026-10-01):不读正文,但邻接构建为 O(N) 索引
|
|
1484
|
+
条目级**(`chain.adjacency` 遍历全部索引节点;冷建代价
|
|
1485
|
+
读数见 `scripts/p2p4_probe.py` R7)——原表述「不走全表」
|
|
1486
|
+
与实测不符,已改正。
|
|
1487
|
+
"temporal" **时间路**(P3-temporal,设计稿 §6.3):按时间邻近度排序
|
|
1488
|
+
的**排名项**,核走 `time_core.cred_factor`;无时间算子/
|
|
1489
|
+
区间时恒空。
|
|
1490
|
+
缺省 = ("lexical","bucket","entity","graph","chain","temporal")
|
|
1491
|
+
—— 后两路按裁定 9「全进默认检索」进缺省集;**每路可单独关**:
|
|
1492
|
+
显式传不含该路的 paths 即关(既有调用方口径一字不变)。
|
|
1493
|
+
两条新路在**无 edges / 无时间参数**的库上自然为空(零多算),
|
|
1494
|
+
故纯词法查询的返回与改动前一致。
|
|
1369
1495
|
path_weights: {路名: 权重};缺省全部 1.0 → 与既有等权 RRF 完全一致。
|
|
1370
1496
|
用于压低**同质路**的贡献:等权融合下,两路对同一批候选给出不一致
|
|
1371
1497
|
排序时,RRF 会双重奖励「两路都靠前」的干扰项,把强路的 top-1 挤掉。
|
|
@@ -1375,6 +1501,8 @@ class MdCGOS(MdCG):
|
|
|
1375
1501
|
sum 奖励「多路共识」,但会系统性低估**单路独有**候选:当强路漏掉目标、
|
|
1376
1502
|
弱路捞到时,目标的单路贡献必然低于任何「两路都有排名」的干扰项。
|
|
1377
1503
|
max 只认「最好的一次排名」,不奖励共识,适合「任一路捞到即可」的召回。
|
|
1504
|
+
因果路是**单路独有召回型**,MCP 面(`mdcg_recall`)按其缺省 max
|
|
1505
|
+
(先例 `mcp_server.py` 的 fuzzy/semantic/goal 同款表达式)。
|
|
1378
1506
|
judge_ranking: 白箱终排(证据防火墙,显式启用)。融合排序只产生候选
|
|
1379
1507
|
(语义负责「不要漏」),资格裁决决定最终优先级(白箱负责「不要错」):
|
|
1380
1508
|
REJECT / BLINDSPOT 剔除,DEFER 降权 ×0.5,ACCEPT 保位。候选池取
|
|
@@ -1399,8 +1527,9 @@ class MdCGOS(MdCG):
|
|
|
1399
1527
|
本入口的对应物是 reach——不在本函数内。
|
|
1400
1528
|
"""
|
|
1401
1529
|
q = (query or "").strip()
|
|
1402
|
-
# 批次 15 统一口径(unify.py
|
|
1403
|
-
#
|
|
1530
|
+
# 批次 15 统一口径(unify.py,2026-09-30 收窄作用域):只译**英文内容**
|
|
1531
|
+
# → 标准原子序列(中文段逐字原样);归一后的 q 进热路径缓存键
|
|
1532
|
+
# (归一确定 → 缓存不串味)
|
|
1404
1533
|
from .semantic.unify import unify_query
|
|
1405
1534
|
q = unify_query(q)
|
|
1406
1535
|
if not q:
|
|
@@ -1479,6 +1608,13 @@ class MdCGOS(MdCG):
|
|
|
1479
1608
|
ranked = {} # path -> [(node, score)]
|
|
1480
1609
|
fuzzy_source = None
|
|
1481
1610
|
chain_prov = {}
|
|
1611
|
+
# P3-temporal:时间路的参照窗(与 `_candidates` 同一真源 `trust.check_time_args`
|
|
1612
|
+
# /`trust.parse_time`;未启用时留 None → 本路恒空,默认查询零变更)。
|
|
1613
|
+
# 只读索引标量(created_at 在索引快照里),不 `_scan` 全库、不读正文。
|
|
1614
|
+
_t_en, _t_ax, _t_why = trust.check_time_args(
|
|
1615
|
+
start_time, end_time, start_operator, end_operator, time_axis)
|
|
1616
|
+
_t_qs, _t_qe = ((trust.parse_time(start_time), trust.parse_time(end_time))
|
|
1617
|
+
if _t_en else (None, None))
|
|
1482
1618
|
if "lexical" in paths:
|
|
1483
1619
|
ranked["lexical"] = self._lexical(q, entries, stat)
|
|
1484
1620
|
if "bucket" in paths:
|
|
@@ -1490,6 +1626,12 @@ class MdCGOS(MdCG):
|
|
|
1490
1626
|
if "chain" in paths:
|
|
1491
1627
|
seeds = (ranked.get("lexical") or []) + (ranked.get("entity") or [])
|
|
1492
1628
|
ranked["chain"], chain_prov = self._path_chain(q, entries, seeds, context)
|
|
1629
|
+
if "temporal" in paths:
|
|
1630
|
+
# P3-temporal:时间路(排名项)。时间算子/区间未启用时恒空——默认
|
|
1631
|
+
# 查询(无时间参数)零变更;启用时种子取「已过 S1/S2 门控 ∧ 已过
|
|
1632
|
+
# 时间算子过滤」的 entries(索引标量直读,不 _scan 全库)。
|
|
1633
|
+
ranked["temporal"] = self._path_temporal(
|
|
1634
|
+
entries, _t_qs, _t_qe, start_operator, end_operator)
|
|
1493
1635
|
if "fuzzy" in paths:
|
|
1494
1636
|
ranked["fuzzy"], fuzzy_source = self._path_fuzzy(
|
|
1495
1637
|
q, entries, context, expand=query_expand)
|
|
@@ -1582,12 +1724,50 @@ class MdCGOS(MdCG):
|
|
|
1582
1724
|
fused = fused_all[:k]
|
|
1583
1725
|
|
|
1584
1726
|
results = []
|
|
1727
|
+
# P2-2(§5.2):默认召回路径上的**拒绝域双语义**——与 `MdCG._emit` 同款
|
|
1728
|
+
# 接线(同一个判据函数 `MdCG.judge_with_boundary`,此处不另写一份):
|
|
1729
|
+
# 边界命中 ⇒ 卡片带 `boundary_hit` 标记(可召回可展示);资格不受影响;
|
|
1730
|
+
# 边界命中数与资格 REJECT 数**分开计数**(boundary_counts)。
|
|
1731
|
+
boundary_counts = {"hit": 0, "terms": 0,
|
|
1732
|
+
"counts": {"accept": 0, "reject": 0, "defer": 0,
|
|
1733
|
+
"blindspot": 0}}
|
|
1585
1734
|
for nid, fs in fused:
|
|
1586
1735
|
node, s = node_by_id[nid]
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1736
|
+
if quals.get(nid):
|
|
1737
|
+
# judge_ranking(白箱终排)已算过资格 → 不重复判定,边界标记
|
|
1738
|
+
# 单独取(同一个 P0-4 判据函数 `MdCG.boundary_hit`)
|
|
1739
|
+
qual = quals[nid]
|
|
1740
|
+
_b = boundary_mark(node, q, context)
|
|
1741
|
+
_c = {"accept": 0, "reject": 0, "defer": 0, "blindspot": 0}
|
|
1742
|
+
_st = str(qual.get("state") or "").lower()
|
|
1743
|
+
if _st in _c:
|
|
1744
|
+
_c[_st] = 1
|
|
1745
|
+
if _b.get("hit"):
|
|
1746
|
+
boundary_counts["hit"] += 1
|
|
1747
|
+
boundary_counts["terms"] += len(_b.get("terms") or [])
|
|
1748
|
+
node["boundary_hit"] = True
|
|
1749
|
+
node["boundary_marker"] = _b.get("marker")
|
|
1750
|
+
node["boundary_terms"] = list(_b.get("terms") or [])
|
|
1751
|
+
for _k in _c:
|
|
1752
|
+
boundary_counts["counts"][_k] += _c[_k]
|
|
1753
|
+
elif judge:
|
|
1754
|
+
_jw = MdCG.judge_with_boundary(node, q, context)
|
|
1755
|
+
qual = dict(_jw["qualification"])
|
|
1756
|
+
_b = boundary_mark(node, q, context)
|
|
1757
|
+
if _b.get("hit"):
|
|
1758
|
+
boundary_counts["hit"] += 1
|
|
1759
|
+
boundary_counts["terms"] += len(_b.get("terms") or [])
|
|
1760
|
+
node["boundary_hit"] = True
|
|
1761
|
+
node["boundary_marker"] = _b.get("marker")
|
|
1762
|
+
node["boundary_terms"] = list(_b.get("terms") or [])
|
|
1763
|
+
for _k in ("accept", "reject", "defer", "blindspot"):
|
|
1764
|
+
boundary_counts["counts"][_k] += int(
|
|
1765
|
+
(_jw.get("counts") or {}).get(_k) or 0)
|
|
1766
|
+
else:
|
|
1767
|
+
qual = {"state": None, "reason": "judge_disabled"}
|
|
1590
1768
|
results.append((node, round(fs, 6), qual, prov.get(nid, [])))
|
|
1769
|
+
_bnd_meta = ({"boundary": boundary_counts}
|
|
1770
|
+
if boundary_counts["hit"] else {})
|
|
1591
1771
|
if record and results:
|
|
1592
1772
|
self.record_access([r[0]["id"] for r in results], "RRF")
|
|
1593
1773
|
# 热路径:写 query 结果缓存 —— **同样受 `_time_on` 约束**。
|
|
@@ -1603,7 +1783,7 @@ class MdCGOS(MdCG):
|
|
|
1603
1783
|
"expand_source": fuzzy_source,
|
|
1604
1784
|
"goal_used": goal_used,
|
|
1605
1785
|
"provenance": prov, **_tf_meta,
|
|
1606
|
-
**_gates_meta}, k=k, layer=layer,
|
|
1786
|
+
**_gates_meta, **_bnd_meta}, k=k, layer=layer,
|
|
1607
1787
|
session=session, branch=branch, validity=validity,
|
|
1608
1788
|
view=view, extra=_cache_extra)
|
|
1609
1789
|
return results, {"tier": "RRF", "scanned": stat["scanned"],
|
|
@@ -1614,7 +1794,7 @@ class MdCGOS(MdCG):
|
|
|
1614
1794
|
"expand_source": fuzzy_source,
|
|
1615
1795
|
"goal_used": goal_used,
|
|
1616
1796
|
"provenance": prov, **_tf_meta,
|
|
1617
|
-
**_gates_meta}
|
|
1797
|
+
**_gates_meta, **_bnd_meta}
|
|
1618
1798
|
|
|
1619
1799
|
# ================= 7. budget-driven pack =================
|
|
1620
1800
|
|
|
@@ -1638,11 +1818,12 @@ class MdCGOS(MdCG):
|
|
|
1638
1818
|
动机:旧策略是「跳过超大、继续试更小的」——预算紧张时形成**逆向淘汰**,
|
|
1639
1819
|
越有价值的详实条目越容易被排除(实测 budget=1500 时 16 条被刷、只装 2 条)。
|
|
1640
1820
|
|
|
1641
|
-
paths/query_expand
|
|
1642
|
-
|
|
1821
|
+
paths/query_expand 缺省时行为 = 既有四路 + P3 两条图检索路(chain/temporal,
|
|
1822
|
+
按裁定 9 进缺省集;无 edges / 无时间参数时自然为空);
|
|
1823
|
+
显式传 paths 才启用第 5/6 路,例如
|
|
1643
1824
|
("lexical","bucket","entity","graph","fuzzy") 词表驱动
|
|
1644
1825
|
("lexical","bucket","entity","graph","semantic") 条件结构驱动
|
|
1645
|
-
(…
|
|
1826
|
+
(… 八路全开 ) 叠加
|
|
1646
1827
|
include_recent=True 时,把近期事件窗口(第 5 篇第 3 章)附在包后,
|
|
1647
1828
|
保证当前任务的连续性;它不参与 RRF 正排,但计入 token 预算。
|
|
1648
1829
|
返回 {pack: [...], tokens_used, budget, skipped: [...], recent: [...], meta}
|
|
@@ -1831,10 +2012,11 @@ class MdCGOS(MdCG):
|
|
|
1831
2012
|
|
|
1832
2013
|
# ================= 4. 审核队列(inbox → decisions) =================
|
|
1833
2014
|
|
|
1834
|
-
# 生效条件:当 node_id 与 content 传入时,在 strict 锁内按 payload_hash
|
|
2015
|
+
# 生效条件:当 node_id 与 content 传入时,在 strict 锁内按 payload_hash(dedup_key 非空时以它为对账键、否则 _sig(content))查重;命中同键提案(无论 pending/accepted/rejected,已裁决优先 break)时幂等返回既有 pid(info=True 返回 dedup 字典),未命中则生成新 pid 入队并返回 pid(info=True 返回 dedup False 字典);kind 非空时原样落 rec 顶层 kind 键(缺省不落键 = 存量条目形状逐位不变);
|
|
1835
2016
|
def propose(self, node_id: str, content: str, layer: str = "knowledge",
|
|
1836
2017
|
tags=None, condition_space=None, verify=None,
|
|
1837
|
-
info: bool = False,
|
|
2018
|
+
info: bool = False, kind: str = None, dedup_key: str = None,
|
|
2019
|
+
**kw):
|
|
1838
2020
|
"""把一个候选记忆放入海马体 inbox,等待审核(不直接持久化)。
|
|
1839
2021
|
|
|
1840
2022
|
verify —— 验收判据(内联声明,裁决阶段只读),形如:
|
|
@@ -1847,10 +2029,22 @@ class MdCGOS(MdCG):
|
|
|
1847
2029
|
不再入队。语义:重试与崩溃恢复无害——入队落盘成功而响应丢失时
|
|
1848
2030
|
(MCP 客户端超时重试的真正机制),重试对账命中既有记录并返回原 pid。
|
|
1849
2031
|
|
|
2032
|
+
kind / dedup_key(三档自治批次②,设计 §四 两个**可选**槽,缺省即
|
|
2033
|
+
存量行为逐位不变):
|
|
2034
|
+
· `kind` = 队列条目**类型字段**("proposal" 存量 / "mutation" 变更单)。
|
|
2035
|
+
缺省 None **不落键**——存量与新普通提案的 rec 形状一字不变;读取方
|
|
2036
|
+
经 `autonomy_modes.order_kind(rec)` 判类型,缺键一律视为 proposal
|
|
2037
|
+
(零迁移)。
|
|
2038
|
+
· `dedup_key` = 幂等对账键的**显式覆盖**(缺省 None → 原 `_sig(content)`)。
|
|
2039
|
+
变更单必须覆盖:内容签名不带目标,「同一份新正文并入节点 X / 并入
|
|
2040
|
+
节点 Y」会撞成同一条(`autonomy_modes.mutation_dedup_key` 把动作类与
|
|
2041
|
+
目标并进键面)。键仍是 sha1 十六进制——`_inbox_phash_index` 与
|
|
2042
|
+
`_cascade_dedup` 只做字符串比对,无需认识构造。
|
|
2043
|
+
|
|
1850
2044
|
返回值:info=False(默认)返回 pid 字符串(保形,存量调用零变化);
|
|
1851
2045
|
info=True 返回 {"pid", "dedup", "dup_of", "dup_status"}。
|
|
1852
2046
|
"""
|
|
1853
|
-
phash = _sig(content)
|
|
2047
|
+
phash = dedup_key or _sig(content)
|
|
1854
2048
|
with FileLock(self.inbox_log, strict=True):
|
|
1855
2049
|
# 增量对账(issue #32):原实现每条全量 read_jsonl(inbox) +
|
|
1856
2050
|
# read_jsonl(decisions)(锁内 O(M+D)/条、批量 O(M²),旧格式行
|
|
@@ -1893,6 +2087,11 @@ class MdCGOS(MdCG):
|
|
|
1893
2087
|
"verify": verify or {}, "verify_hash": vhash,
|
|
1894
2088
|
"extra": kw, "actor": self.actor,
|
|
1895
2089
|
"session": getattr(self, "session", None)}
|
|
2090
|
+
# 三档自治批次②(设计 §四):类型字段**只在显式给定时落键**——
|
|
2091
|
+
# 缺省不落,存量与新普通提案的 rec 形状逐位不变(零回归),
|
|
2092
|
+
# 读取方按「缺键 = proposal」判(零迁移)。
|
|
2093
|
+
if kind:
|
|
2094
|
+
rec["kind"] = str(kind)
|
|
1896
2095
|
append_jsonl(self.inbox_log, rec)
|
|
1897
2096
|
self._audit("propose", node_id, pid=pid, layer=layer,
|
|
1898
2097
|
payload_hash=phash, verify_hash=vhash)
|
|
@@ -2161,7 +2360,7 @@ class MdCGOS(MdCG):
|
|
|
2161
2360
|
audit_source="hippocampus/decisions.jsonl", **extra)
|
|
2162
2361
|
return nid
|
|
2163
2362
|
|
|
2164
|
-
# 生效条件:当 pid/item/decision/status/reason/round_no/rt_verdict/issues/vhash/result 传入时,构造 rec 并写 record_hash,把 result 中 ok/node_id/error/state
|
|
2363
|
+
# 生效条件:当 pid/item/decision/status/reason/round_no/rt_verdict/issues/vhash/result 传入时,构造 rec 并写 record_hash,把 result 中 ok/node_id/error/state 保留(result.mutation 为 dict 时另落 rec["mutation"]=变更单执行时点载荷,proposal 链无此键、rec 形状逐位不变);在 strict 锁内追加 decisions_log;随后尝试写 md 审核记录,成功补 record_node_id/record_hash,异常则补 record_error 并 audit review_record_failed;最后 audit review_decide 并返回 result;
|
|
2165
2364
|
def _record_decision(self, pid, item, decision, status, reason, round_no,
|
|
2166
2365
|
rt_verdict, issues, vhash, result):
|
|
2167
2366
|
"""落盘一轮裁决:jsonl(权威)+ md 审计节点(可复核、供外部审计)。"""
|
|
@@ -2174,6 +2373,13 @@ class MdCGOS(MdCG):
|
|
|
2174
2373
|
rec["record_hash"] = self._record_hash(rec)
|
|
2175
2374
|
rec["result"] = {k: v for k, v in result.items()
|
|
2176
2375
|
if k in ("ok", "node_id", "error", "state")}
|
|
2376
|
+
# 三档自治③(设计 §四):变更单的**执行时点载荷**(含前像/影响面/
|
|
2377
|
+
# 回滚命令)随裁决记录落盘——回滚 CLI 的读取源(rec["mutation"])。
|
|
2378
|
+
# proposal 链 result 无 mutation 键 ⇒ rec 形状逐位不变;record_hash
|
|
2379
|
+
# 只算固定十键(_record_hash),不受本键影响。
|
|
2380
|
+
_mut = result.get("mutation")
|
|
2381
|
+
if isinstance(_mut, dict):
|
|
2382
|
+
rec["mutation"] = _mut
|
|
2177
2383
|
# 裁决记录不能丢:strict 锁内追加(并发裁决不交错;
|
|
2178
2384
|
# 丢一条裁决会让提案回 pending → 重复落盘,比让裁决者等一下代价大)
|
|
2179
2385
|
with FileLock(self.decisions_log, strict=True):
|
|
@@ -2279,7 +2485,158 @@ class MdCGOS(MdCG):
|
|
|
2279
2485
|
"verify_hash": src.get("verify_hash"),
|
|
2280
2486
|
"source": "hippocampus/decisions.jsonl"}
|
|
2281
2487
|
|
|
2282
|
-
# 生效条件:
|
|
2488
|
+
# 生效条件:item 为 kind=mutation 的队列记录时先经 rollback.preimage 在**执行时点**留前像(目标在位时拍快照 + 采影响面 + 生成回滚命令;目标缺失返回 None 走既有错误路径;拍摄失败抛 PreimageError 被专捕成 preimage_unavailable 错误体),再按载荷 extra.mutation 分派——B:primitive 为 "converge_into" 走 writelimit.converge_into、否则走 forgetting.reinforce(两者都以 rec.actor 为动作发起者、载荷 meta 的 session/override 复现原槽);C:目标经 forgetting.prior_node 判定——目标已不存在即返回 {"ok": False, "error": "target_missing", ...}(fail-closed,不重建节点、不记 accepted),存在才 self.add(目标, 后像, layer=rec.layer, sensitivity=rec.sensitivity, override, **载荷 meta) 覆写落盘;D:self.forget(目标, override) 软删;动作类不在 {B,C,D} 或原语返回失败(ok 为 False / None / target_missing)时返回 {"ok": False, "error": "mutation_execute_failed"/"mutation_action_unknown", ...} 且**不改动任何节点**;成功返回 {"ok": True, "mutation": <执行时点载荷:原载荷 + kind/executed + before/impact/rollback>, "node_id": ...};
|
|
2489
|
+
def _mutation_execute(self, item, reason: str = "", override: bool = False):
|
|
2490
|
+
"""变更单 accept 的执行桥:**显式越过确认判定**,直调对应动作的底层原语。
|
|
2491
|
+
|
|
2492
|
+
为什么这样就是「越过」(而不是绕过闸门):三档自治的确认判定只长在
|
|
2493
|
+
**热路径入口**(写链 `writepipe._gate_autonomy`、遗忘合并闸
|
|
2494
|
+
`_autonomy_gate_merge`、工具面 `forget_gated`);本桥调的是
|
|
2495
|
+
`MdCG.add` / `forgetting.reinforce` / `writelimit.converge_into` /
|
|
2496
|
+
`MdCGOS.forget`——即**库层原语**,它们历来不含档位判定(设计 §三 硬约束
|
|
2497
|
+
「直调库层不接线,保持库层语义不变」)。故「已确认」这件事由调用点
|
|
2498
|
+
(本桥)保证,不靠传一个 `already_confirmed=True` 之类的旗标穿透各层
|
|
2499
|
+
(旗标会变成可被伪造的旁路)。
|
|
2500
|
+
|
|
2501
|
+
保护闸不绕:`add`/`reinforce`/`converge_into` 内部的
|
|
2502
|
+
`protect.guard_write|guard_overwrite` 与 `forget` 的 `guard_forget`
|
|
2503
|
+
仍是落盘必经之路——受保护节点在确认后依然被拦(拦下即返回失败、
|
|
2504
|
+
裁决不落盘、提案保持 pending)。
|
|
2505
|
+
|
|
2506
|
+
失败语义(如实):动作未发生 ⇒ 返回 ok=False 且**不记裁决**;调用方
|
|
2507
|
+
(`review_decide`)据此提前返回,提案维持 pending 可见。
|
|
2508
|
+
|
|
2509
|
+
补强(2026-10-02,复核观察 3):**C 改写的目标在 accept 时已消失**此前
|
|
2510
|
+
走 `self.add` 的 upsert——静默把「改写」落地成「新增同 id 节点」并记
|
|
2511
|
+
accepted(单子声称 C、盘面却是 A,前像被清空且无失败信号)。现与 B/D
|
|
2512
|
+
同口径:目标不存在 ⇒ `{"ok": False, "error": "target_missing"}`,
|
|
2513
|
+
**不重建节点**、不记裁决、提案保持 pending。判据复用存在性单点
|
|
2514
|
+
`forgetting.prior_node`(与出单时的 C/A 判定同一单点)。
|
|
2515
|
+
|
|
2516
|
+
批次③(2026-10-02,设计 §四)——**执行时点三字段补全**:动作落盘**之前**
|
|
2517
|
+
经 `rollback.preimage()` 拍前像(`protect.snapshot_preimage`,写
|
|
2518
|
+
`_protected_history` 同面)+ 采影响面(边/索引条目/聚合行)+ 生成回滚
|
|
2519
|
+
命令;成功返回的 `mutation` 是**补全后的执行时点载荷**
|
|
2520
|
+
(`autonomy_modes.mutation_executed`),由 `_record_decision` 随裁决
|
|
2521
|
+
落进 decisions.jsonl 的 `rec["mutation"]`(回滚 CLI 的读取源)。
|
|
2522
|
+
前像**必须**是执行时点(不是提议时点):从提议到 accept 之间目标可能
|
|
2523
|
+
被第三方改动,回滚只撤销本变更本身(设计 §四 明文)。拍摄失败
|
|
2524
|
+
(目标在位而快照未落)⇒ `preimage_unavailable` fail-closed,不执行
|
|
2525
|
+
——「一切破坏性动作先留前像」(设计 §六 义务)。
|
|
2526
|
+
"""
|
|
2527
|
+
from . import autonomy_modes as _am
|
|
2528
|
+
from . import rollback as _rb
|
|
2529
|
+
try:
|
|
2530
|
+
slot = (item.get("extra") or {}).get(_am.MUTATION_SLOT) or {}
|
|
2531
|
+
except AttributeError:
|
|
2532
|
+
slot = {}
|
|
2533
|
+
act = str(slot.get("action") or "").strip().upper()
|
|
2534
|
+
tgt = slot.get("target") or item.get("id")
|
|
2535
|
+
after = slot.get("after")
|
|
2536
|
+
if after is None:
|
|
2537
|
+
after = item.get("content") or ""
|
|
2538
|
+
meta = dict(slot.get("meta") or {})
|
|
2539
|
+
actor = item.get("actor") or getattr(self, "actor", None)
|
|
2540
|
+
session = meta.pop("session", None)
|
|
2541
|
+
_ovr = bool(meta.pop("override", False)) or bool(override)
|
|
2542
|
+
layer = item.get("layer") or "knowledge"
|
|
2543
|
+
sens = item.get("sensitivity")
|
|
2544
|
+
pre = None
|
|
2545
|
+
try:
|
|
2546
|
+
# ---- 执行时点补全(三档自治③ · 设计 §四)----
|
|
2547
|
+
# 前像(执行时点快照引用)+ 影响面 + 回滚命令——在**动作落盘之前**:
|
|
2548
|
+
# 从提议到 accept 之间目标可能被第三方改动,前像必须反映「执行前
|
|
2549
|
+
# 一刻」的盘面,回滚才只撤销本变更本身。目标不存在时返回 None
|
|
2550
|
+
# (动作不会发生,走既有 target_missing/not_found 错误路径);
|
|
2551
|
+
# 目标在位而拍摄失败 ⇒ PreimageError ⇒ 下方专捕 fail-closed。
|
|
2552
|
+
pre = _rb.preimage(self, act, tgt, pid=item.get("pid"),
|
|
2553
|
+
reason="变更单执行时点前像(%s→%s,pid=%s)"
|
|
2554
|
+
% (act, tgt, item.get("pid") or "?"))
|
|
2555
|
+
if act == _am.B_MERGE:
|
|
2556
|
+
if str(slot.get("primitive") or "") == "converge_into":
|
|
2557
|
+
res = writelimit.converge_into(self, tgt, after,
|
|
2558
|
+
override=_ovr, actor=actor,
|
|
2559
|
+
session=session)
|
|
2560
|
+
else:
|
|
2561
|
+
res = forgetting.reinforce(self, tgt, override=_ovr,
|
|
2562
|
+
actor=actor, content=after,
|
|
2563
|
+
session=session)
|
|
2564
|
+
node_id = tgt
|
|
2565
|
+
elif act == _am.C_REWRITE:
|
|
2566
|
+
# 补强(2026-10-02,复核观察 3):C 的**目标节点在 accept 执行时
|
|
2567
|
+
# 已不存在** ⇒ fail-closed,**不得**静默 `self.add` upsert 成
|
|
2568
|
+
# 新增。旧形态:目标出单后被删除,重放把「改写」落成「同名新
|
|
2569
|
+
# 节点」并记 accepted——变更单声称执行了 C 改写,盘面却凭空多出
|
|
2570
|
+
# 一个节点(前像被清空,且无任何失败信号)。
|
|
2571
|
+
# 判据复用**存在性单点** `forgetting.prior_node`(与出单时的
|
|
2572
|
+
# C/A 判定同一单点),错误名与 B/D 原语同口径
|
|
2573
|
+
# (`writelimit.converge_into` 的 {"ok": False,
|
|
2574
|
+
# "error": "target_missing"})。
|
|
2575
|
+
if forgetting.prior_node(self, tgt) is None:
|
|
2576
|
+
return {"ok": False, "error": "target_missing",
|
|
2577
|
+
"kind": _am.KIND_MUTATION, "action": act,
|
|
2578
|
+
"target": tgt,
|
|
2579
|
+
"detail": "变更单 C 改写:目标节点在 accept 执行时"
|
|
2580
|
+
"已不存在(出单后被删除/移走)——"
|
|
2581
|
+
"fail-closed 未执行任何动作、未重建节点。",
|
|
2582
|
+
"hint": "目标节点 %s 已不存在:C 改写不得静默转成"
|
|
2583
|
+
"「新增同 id 节点」。请核对目标是否已被删除"
|
|
2584
|
+
"(trash/ 可 restore)后重新提议——裁决"
|
|
2585
|
+
"**未落盘**,提案保持 pending。" % tgt}
|
|
2586
|
+
node_id = self.add(tgt, after, layer=layer, sensitivity=sens,
|
|
2587
|
+
override=_ovr, **meta)
|
|
2588
|
+
res = {"ok": True, "node_id": node_id}
|
|
2589
|
+
elif act == _am.D_DELETE:
|
|
2590
|
+
res = self.forget(tgt, reason=reason or slot.get("reason") or "",
|
|
2591
|
+
override=_ovr)
|
|
2592
|
+
node_id = tgt
|
|
2593
|
+
else:
|
|
2594
|
+
return {"ok": False, "error": "mutation_action_unknown",
|
|
2595
|
+
"kind": _am.KIND_MUTATION, "action": act or None,
|
|
2596
|
+
"detail": "变更单载荷的动作类缺失或不在 %s——fail-closed"
|
|
2597
|
+
"未执行任何动作、提案保持 pending。"
|
|
2598
|
+
% "/".join(_am.ACTION_CLASSES)}
|
|
2599
|
+
except _rb.PreimageError as exc:
|
|
2600
|
+
# 执行时点前像未落盘(目标在位而快照失败)——「一切破坏性动作先留
|
|
2601
|
+
# 前像」(设计 §四/§六),fail-closed 不执行:动作**未发生** ⇒
|
|
2602
|
+
# 如实返回失败,不落裁决、提案保持 pending。
|
|
2603
|
+
return {"ok": False, "error": "preimage_unavailable",
|
|
2604
|
+
"kind": _am.KIND_MUTATION, "action": act, "target": tgt,
|
|
2605
|
+
"detail": str(exc),
|
|
2606
|
+
"hint": "变更单未执行:执行时点前像未落盘(设计 §四:一切"
|
|
2607
|
+
"破坏性动作先留前像)——裁决**未落盘**,提案保持 "
|
|
2608
|
+
"pending;排查 _protected_history 目录可写性后"
|
|
2609
|
+
"重新裁决。"}
|
|
2610
|
+
except Exception as exc: # noqa: BLE001
|
|
2611
|
+
# 保护闸(ProtectionError/AccessDenied)、冲突闸(ConsistencyError)、
|
|
2612
|
+
# 路径闸(ValueError)等一律走这里:动作**未发生** ⇒ 如实返回失败,
|
|
2613
|
+
# 不落裁决、提案保持 pending(把异常原样抛给 CLI 会让裁决进程崩在
|
|
2614
|
+
# 「已执行一半」的歧义态上,且提案去向不可见)。
|
|
2615
|
+
return {"ok": False, "error": "mutation_execute_failed",
|
|
2616
|
+
"kind": _am.KIND_MUTATION, "action": act, "target": tgt,
|
|
2617
|
+
"detail": "%s: %s" % (type(exc).__name__, exc),
|
|
2618
|
+
"hint": "变更单未执行:目标节点 %s 的动作被既有闸门拦下或原语"
|
|
2619
|
+
"失败——裁决**未落盘**,提案保持 pending;"
|
|
2620
|
+
"受保护节点/冲突需先按既有通道处置(override 或"
|
|
2621
|
+
"修文)后重新裁决。" % tgt}
|
|
2622
|
+
if not isinstance(res, dict) or res.get("ok") is False:
|
|
2623
|
+
return {"ok": False, "error": "mutation_execute_failed",
|
|
2624
|
+
"kind": _am.KIND_MUTATION, "action": act, "target": tgt,
|
|
2625
|
+
"detail": res,
|
|
2626
|
+
"hint": "变更单未执行:目标节点 %s 的动作原语未成功"
|
|
2627
|
+
"(受保护/目标缺失/同 id 冲突等)——裁决**未落盘**,"
|
|
2628
|
+
"提案保持 pending,修好后可重新裁决。" % tgt}
|
|
2629
|
+
# 执行时点载荷(**唯一补全点**,autonomy_modes.mutation_executed):
|
|
2630
|
+
# 原载荷(动作类/目标/后像/理由/primitive/meta)+ kind/executed +
|
|
2631
|
+
# before/impact/rollback——_record_decision 随裁决落 decisions.jsonl
|
|
2632
|
+
# 的 rec["mutation"](回滚 CLI 据此读回前像与动作类)。
|
|
2633
|
+
mpay = _am.mutation_executed(
|
|
2634
|
+
slot, before=(pre or {}).get("before"),
|
|
2635
|
+
impact=(pre or {}).get("impact"),
|
|
2636
|
+
rollback=(pre or {}).get("rollback"))
|
|
2637
|
+
return {"ok": True, "node_id": node_id, "mutation": mpay}
|
|
2638
|
+
|
|
2639
|
+
# 生效条件:decision 须为 DECISION_ACTIONS("accept"/"reject"/"edit"/"merge"/"noop")之一(否则 raise ValueError),inbox_log 中须有 pid 匹配记录(否则 {'ok': False, 'error': 'pid_not_found'}),且 pid 不在 self._closed_pids() 中(否则 'already_decided');edits 为真值且含 "verify"、或 redteam 为真值且含 "verify" 时返回 'verify_readonly';last_status=="needs_reapproval" 时须 redteam.verdict 归一化为 "pass" 且 round_no>last_round(否则 'reapproval_required' / 'round_not_advanced');rt_v=="reject" 或(decision=="reject" 且 rt_issues 非空)时记 needs_reapproval 不落节点;decision=="accept" 且 rt_v 为空且 _redteam_required() 为真时返回 'redteam_required';其余 accept/edit 按 item(edit 时用 edits.get 覆盖 content/tags/layer)add+flush 落节点,merge 须 merge_into 或 item.extra.merge_into 指向的节点存在(否则 'merge_target_not_found'),且写入前经与 add 同款双闸(N131,2026-09-25:principal 在位先 require_layer_write(目标层, 目标敏感度)——与 MdCGSecure.add 同序同错型;再 protect.guard_write(override=override)——self/anchor 层、immutable 标记,拒绝抛 ProtectionError/AccessDenied 且提案留 pending 不落库)后追加内容并定向索引 upsert(_node_entry 同源条目写入 index/_dirty)+flush,reject 与 noop 只记裁决(status 分别为 "rejected"/"noop");kind=mutation 的变更单(三档自治批次②)不走上述分支——accept 经 _mutation_execute 执行对应动作(失败即返回错误、不记裁决、提案保持 pending)、reject 只留痕,其余裁决 fail-closed 返回 mutation_decision_unsupported;最后统一 _record_decision + _cascade_dedup + flush 后返回 result。
|
|
2283
2640
|
def review_decide(self, pid: str, decision: str, edits: dict = None,
|
|
2284
2641
|
merge_into: str = None, reason: str = "",
|
|
2285
2642
|
redteam: dict = None, issues=None,
|
|
@@ -2298,6 +2655,14 @@ class MdCGOS(MdCG):
|
|
|
2298
2655
|
的写入门控(它根本不写)。此处 noop 是**裁决动作**,与 lifecycle/trust 中
|
|
2299
2656
|
同名的**状态迁移结果码**分属两层(见模块常量区注释)。
|
|
2300
2657
|
|
|
2658
|
+
**变更单**(`kind=mutation`,三档自治批次② · 设计 §四):本批只定义
|
|
2659
|
+
accept/reject——accept = 执行对应动作(B 合并原语 / C 覆写落盘走既有
|
|
2660
|
+
add / D 软删走既有 forget),reject = 原样留痕不执行;edit/merge/noop
|
|
2661
|
+
对变更单**未定义** ⇒ fail-closed 报错带 hint(不静默当已处理)。
|
|
2662
|
+
执行失败 ⇒ **不记裁决**、提案保持 pending(与 merge 的
|
|
2663
|
+
`merge_target_not_found` 同款:不落一条「accepted 而动作没做」)。
|
|
2664
|
+
存量 `kind` 缺键的提案(= proposal)在本方法里**逐位不变**。
|
|
2665
|
+
|
|
2301
2666
|
两条验证纪律(借自任务分级协议的验证端):
|
|
2302
2667
|
1. 判据只读:verify 由 propose 声明,裁决阶段传入不同判据 → verify_readonly。
|
|
2303
2668
|
2. 红队门控 + 再审批:redteam.verdict=reject(或带 issues 的 reject)不落库,
|
|
@@ -2353,6 +2718,14 @@ class MdCGOS(MdCG):
|
|
|
2353
2718
|
return {"ok": False, "error": "redteam_required",
|
|
2354
2719
|
"detail": "MDCG_REDTEAM_REQUIRED=1:accept 必须带红队 pass"}
|
|
2355
2720
|
|
|
2721
|
+
# 三档自治批次②(设计 §四):**变更单**(kind=mutation)走**独立执行桥**
|
|
2722
|
+
# ——在既有前置闸(已判/verify_readonly/再审批/红队门控)之后、既有
|
|
2723
|
+
# accept/edit/merge/noop 链之前。proposal(含存量无 kind 条目)一律不
|
|
2724
|
+
# 进此支,下面整条链对它逐位不变。
|
|
2725
|
+
if autonomy_modes.order_kind(item) == autonomy_modes.KIND_MUTATION:
|
|
2726
|
+
return self._decide_mutation(pid, item, decision, reason, override,
|
|
2727
|
+
round_no, rt_v, rt_issues, expect)
|
|
2728
|
+
|
|
2356
2729
|
result = {"pid": pid, "decision": decision, "round": round_no,
|
|
2357
2730
|
"redteam": rt_v or "absent"}
|
|
2358
2731
|
if decision == DECISION_NOOP:
|
|
@@ -2363,6 +2736,11 @@ class MdCGOS(MdCG):
|
|
|
2363
2736
|
elif decision == "reject":
|
|
2364
2737
|
result["ok"] = True
|
|
2365
2738
|
elif decision in ("accept", "edit"):
|
|
2739
|
+
# issue50-c F1 对口(2026-10-01,本分支**零行为改动**):accept 早就是
|
|
2740
|
+
# 「队列记录 → 节点 fm」的映射通道(`rec.tags` / `rec.condition_space`
|
|
2741
|
+
# / `**extra`,下方 :2553/:2573)。`remember_gated` 的 DEFER 出口把调用方
|
|
2742
|
+
# 声明的 meta 随入队一并落进这三槽之后,DEFER→accept 与直接 ACCEPT 的
|
|
2743
|
+
# 节点 fm 即等价——映射不需要新增第二套实现,本分支职责与判据不变。
|
|
2366
2744
|
content = item["content"]
|
|
2367
2745
|
tags = list(item.get("tags") or [])
|
|
2368
2746
|
layer = item.get("layer") or "knowledge"
|
|
@@ -2446,6 +2824,18 @@ class MdCGOS(MdCG):
|
|
|
2446
2824
|
self.flush()
|
|
2447
2825
|
result.update(ok=True, node_id=target)
|
|
2448
2826
|
|
|
2827
|
+
return self._finish_decision(pid, item, decision, reason, round_no,
|
|
2828
|
+
rt_v, rt_issues, expect, result)
|
|
2829
|
+
|
|
2830
|
+
# 生效条件:result 为已定好的裁决返回体时,按其 pid/item/decision/reason/round_no/rt_v/rt_issues/expect 调 _record_decision(status 映射统一出口:reject→"rejected"、noop→"noop"、其余→"accepted")落盘 jsonl + 审计节点,再按 item 的 payload_hash(缺失时 _sig(content))调 _cascade_dedup 级联出清(有 closed 时写 result["cascade_closed"]),最后 flush 并返回 result;
|
|
2831
|
+
def _finish_decision(self, pid, item, decision, reason, round_no, rt_v,
|
|
2832
|
+
rt_issues, expect, result):
|
|
2833
|
+
"""裁决**统一收尾**(单点):proposal 链与变更单执行桥共用同一收尾。
|
|
2834
|
+
|
|
2835
|
+
为何抽出来:三档自治批次② 的变更单走独立执行桥(`_decide_mutation`),
|
|
2836
|
+
但「落 decisions.jsonl + 写审计节点 + 级联出清 + flush」这几步必须与原
|
|
2837
|
+
路径**同一条实现**——各写一份必然漂移(本仓已两次教训)。
|
|
2838
|
+
"""
|
|
2449
2839
|
# status 映射统一出口:reject→"rejected"、noop→"noop"(同为终态,见
|
|
2450
2840
|
# TERMINAL_DECISION_STATUS)、其余→"accepted"。
|
|
2451
2841
|
result = self._record_decision(
|
|
@@ -2467,6 +2857,40 @@ class MdCGOS(MdCG):
|
|
|
2467
2857
|
self.flush()
|
|
2468
2858
|
return result
|
|
2469
2859
|
|
|
2860
|
+
# 生效条件:item 为 kind=mutation 的队列记录——decision 为 accept 时经 _mutation_execute 执行对应动作(返回 ok=False 时**原样返回该错误**、不落裁决、提案保持 pending),为 reject 时只置 ok=True 留痕;decision 非此二者时返回 {"ok": False, "error": "mutation_decision_unsupported", ...} 带 hint(fail-closed,不落裁决);通过者交 _finish_decision 收尾后返回其结果;
|
|
2861
|
+
def _decide_mutation(self, pid, item, decision, reason, override, round_no,
|
|
2862
|
+
rt_v, rt_issues, expect):
|
|
2863
|
+
"""变更单的裁决分派(三档自治批次② · 设计 §四):accept 执行 / reject 留痕。
|
|
2864
|
+
|
|
2865
|
+
本批只定义 accept/reject 两态;其余裁决(edit/merge/noop)**fail-closed
|
|
2866
|
+
报错带 hint**,不静默当作已处理——否则一条被 noop 的变更单会伪装成
|
|
2867
|
+
「评估过、无需改动」,而实际动作从未发生过(设计 §三 硬约束③ 的同精神:
|
|
2868
|
+
没发生的事不许汇报成发生了)。
|
|
2869
|
+
"""
|
|
2870
|
+
if decision not in (DECISION_ACCEPT, DECISION_REJECT):
|
|
2871
|
+
return {"ok": False, "error": "mutation_decision_unsupported",
|
|
2872
|
+
"kind": autonomy_modes.KIND_MUTATION,
|
|
2873
|
+
"decision": decision,
|
|
2874
|
+
"hint": "变更单(kind=mutation)本批只定义 accept/reject:"
|
|
2875
|
+
"accept=执行对应动作(B reinforce/converge、"
|
|
2876
|
+
"C 覆写落盘、D 软删),reject=原样留痕不执行;"
|
|
2877
|
+
"edit/merge/noop 对变更单未定义——fail-closed "
|
|
2878
|
+
"未处理,提案保持 pending(未静默关闭)。"}
|
|
2879
|
+
result = {"pid": pid, "decision": decision, "round": round_no,
|
|
2880
|
+
"redteam": rt_v or "absent"}
|
|
2881
|
+
if decision == DECISION_ACCEPT:
|
|
2882
|
+
mres = self._mutation_execute(item, reason=reason, override=override)
|
|
2883
|
+
if not mres.get("ok"):
|
|
2884
|
+
# 执行失败 = 动作未发生:**不落裁决**(提案保持 pending,与 merge
|
|
2885
|
+
# 分支的 merge_target_not_found 同款——落一条 accepted 而动作没做,
|
|
2886
|
+
# 就是「汇报不实」)。
|
|
2887
|
+
return mres
|
|
2888
|
+
result.update(mres)
|
|
2889
|
+
else:
|
|
2890
|
+
result["ok"] = True
|
|
2891
|
+
return self._finish_decision(pid, item, decision, reason, round_no,
|
|
2892
|
+
rt_v, rt_issues, expect, result)
|
|
2893
|
+
|
|
2470
2894
|
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.decisions_log)),记录内容取决于 decisions_log 可读结果;
|
|
2471
2895
|
def decisions(self):
|
|
2472
2896
|
return list(read_jsonl(self.decisions_log))
|
|
@@ -2474,11 +2898,17 @@ class MdCGOS(MdCG):
|
|
|
2474
2898
|
# ================= 5. tombstone + 恢复时删除检查 =================
|
|
2475
2899
|
|
|
2476
2900
|
# 生效条件:当 node_id 传入且索引中存在该节点时,先经 protect.guard_forget(override=override) 保护检查,随后尝试把源路径 os.replace 到 trash_dir/{node_id}.md;OSError 返回 ok False error 字符串;成功则写 deletions_log、_unstage、缓存失效、audit,并返回 ok True/id/tombstone;索引无此节点返回 not_found;
|
|
2477
|
-
def
|
|
2478
|
-
"""
|
|
2901
|
+
def _forget_qualify(self, node_id: str, override: bool = False):
|
|
2902
|
+
"""删除的**既有资格前置闸**(单点):索引代际感知 → not_found → 写保护。
|
|
2479
2903
|
|
|
2480
|
-
|
|
2481
|
-
|
|
2904
|
+
为什么抽出来:三档自治(设计 §三 硬约束②「判定必须在既有资格闸通过
|
|
2905
|
+
之后」)需要在**不重复、不重排、不放宽**这三道既有判据的前提下,先让
|
|
2906
|
+
资格闸跑完再问档位。判据只此一份——`forget`(库层原路径)与
|
|
2907
|
+
`forget_gated`(工具面档位路径)共用,不各写一份必然漂移。
|
|
2908
|
+
|
|
2909
|
+
返回 (entry, None) 表示资格通过(entry = 索引条目);(None, err) 表示
|
|
2910
|
+
资格未过(err 即既有 forget 的返回体;保护闸抛出的异常照旧外抛,不在
|
|
2911
|
+
此吞掉——调用方看到的语义与改动前逐位一致)。
|
|
2482
2912
|
"""
|
|
2483
2913
|
# N195(2026-09-28):删除面与写面同源,先做索引代际感知。此前直读
|
|
2484
2914
|
# 本进程内存索引——他进程刚写入的 anchor/self 节点在本进程索引中
|
|
@@ -2487,8 +2917,28 @@ class MdCGOS(MdCG):
|
|
|
2487
2917
|
self._maybe_reload_index()
|
|
2488
2918
|
e = self.index["nodes"].get(node_id)
|
|
2489
2919
|
if not e:
|
|
2490
|
-
return {"ok": False, "error": "not_found"}
|
|
2920
|
+
return None, {"ok": False, "error": "not_found"}
|
|
2491
2921
|
protect.guard_forget(self, node_id, override=override, actor=self.actor)
|
|
2922
|
+
return e, None
|
|
2923
|
+
|
|
2924
|
+
def forget(self, node_id: str, reason: str = "", override: bool = False):
|
|
2925
|
+
"""软删除:节点文件移入 trash/,写入删除清单(payload-free)。
|
|
2926
|
+
|
|
2927
|
+
写保护:受保护节点(self/anchor 层、protected 标记、importance≥0.7)
|
|
2928
|
+
不可遗忘——需显式 override=True,且旧版本先快照、动作全程留痕。
|
|
2929
|
+
|
|
2930
|
+
**库层语义不变**(三档自治批次②边界):本方法不读档位、不出变更单——
|
|
2931
|
+
档位判定在工具面 `forget_gated`(MCP `cg(op=forget)` / `mdcg_forget`),
|
|
2932
|
+
脚本通道(`scripts/dsh_log_index.py` 等直调本方法)现状逐位不变。
|
|
2933
|
+
"""
|
|
2934
|
+
e, err = self._forget_qualify(node_id, override=override)
|
|
2935
|
+
if err is not None:
|
|
2936
|
+
return err
|
|
2937
|
+
return self._forget_apply(node_id, e, reason)
|
|
2938
|
+
|
|
2939
|
+
# 生效条件:e 为 _forget_qualify 通过后的索引条目时执行既有软删体(os.replace 进 trash_dir、写 deletions_log、_unstage、缓存失效、audit);publish 抛 OSError 时返回 {"ok": False, "error": 异常字符串};成功返回 {"ok": True, "id", "tombstone"};
|
|
2940
|
+
def _forget_apply(self, node_id: str, e: dict, reason: str = ""):
|
|
2941
|
+
"""软删**执行体**(单点):与改动前的 `forget` 体逐位一致。"""
|
|
2492
2942
|
src = os.path.join(self.root, e["path"])
|
|
2493
2943
|
fm, content = self._read(e)
|
|
2494
2944
|
h = _sig(content or "", 16)
|
|
@@ -2510,6 +2960,43 @@ class MdCGOS(MdCG):
|
|
|
2510
2960
|
self._audit("forget", node_id, reason=reason, payload_hash=h)
|
|
2511
2961
|
return {"ok": True, "id": node_id, "tombstone": h}
|
|
2512
2962
|
|
|
2963
|
+
# 生效条件:先跑既有资格前置闸 _forget_qualify(未过即原样返回其 err,含 not_found 与 protect.guard_forget 抛出);资格通过后以 autonomy_modes.decide(D_DELETE) 判定——ALLOW 走 _forget_apply 软删并返回其原返回体;FORBID 返回 ok=False/moved_to="autonomy_forbidden" 终态;CONFIRM 经 autonomy_modes.propose_mutation 出变更单(目标 node_id、后像为空、复现参数 override 入载荷)并返回 ok=True/committed=False/deleted=False/moved_to="review_queue" 的终态(**不软删**);
|
|
2964
|
+
def forget_gated(self, node_id: str, reason: str = "", override: bool = False):
|
|
2965
|
+
"""软删的**档位路径**(三档自治批次② · 设计 §三 D 删除):MCP 工具面专用。
|
|
2966
|
+
|
|
2967
|
+
位置(硬约束②「纯加严」):`protect.guard_forget` 等既有资格判据跑完
|
|
2968
|
+
之后才问档位——档位**不参与**资格判定、也**不放宽**任何判据:受保护
|
|
2969
|
+
节点在这里照旧被 `ProtectionError` 拦下(不是先出单再拦)。
|
|
2970
|
+
|
|
2971
|
+
边界(如实):脚本通道直调 `MdCGOS.forget`(库层原路径)**不接**本档位
|
|
2972
|
+
——本批只覆盖 MCP 工具面两个落点(`cg(op=forget)` / `mdcg_forget`)。
|
|
2973
|
+
"""
|
|
2974
|
+
e, err = self._forget_qualify(node_id, override=override)
|
|
2975
|
+
if err is not None:
|
|
2976
|
+
return err
|
|
2977
|
+
dec = autonomy_modes.decide(autonomy_modes.D_DELETE)
|
|
2978
|
+
if dec["decision"] == autonomy_modes.ALLOW:
|
|
2979
|
+
return self._forget_apply(node_id, e, reason)
|
|
2980
|
+
aut = {"mode": dec["mode"], "action": dec["action"],
|
|
2981
|
+
"action_name": dec["action_name"], "decision": dec["decision"]}
|
|
2982
|
+
if dec["decision"] == autonomy_modes.FORBID:
|
|
2983
|
+
return {"ok": False, "id": node_id, "deleted": False,
|
|
2984
|
+
"moved_to": "autonomy_forbidden", "error": "autonomy_forbid",
|
|
2985
|
+
"autonomy": aut, "hint": dec["hint"]}
|
|
2986
|
+
pay = autonomy_modes.mutation_payload(
|
|
2987
|
+
autonomy_modes.D_DELETE, node_id, after="",
|
|
2988
|
+
reason=reason or "删除(MCP forget 工具面)", primitive="forget",
|
|
2989
|
+
meta={"override": bool(override)})
|
|
2990
|
+
pr = autonomy_modes.propose_mutation(
|
|
2991
|
+
self, autonomy_modes.D_DELETE, node_id, payload=pay,
|
|
2992
|
+
layer=e.get("layer") or "knowledge",
|
|
2993
|
+
sensitivity=e.get("sensitivity"), info=True)
|
|
2994
|
+
return {"ok": True, "id": node_id, "committed": False, "deleted": False,
|
|
2995
|
+
"moved_to": "review_queue", "pid": pr["pid"], "mutation": pay,
|
|
2996
|
+
"autonomy": aut,
|
|
2997
|
+
"hint": "%s 本次未删除(节点原样在位):变更单已入审核队列"
|
|
2998
|
+
"(pid=%s,目标 %s)。" % (dec["hint"], pr["pid"], node_id)}
|
|
2999
|
+
|
|
2513
3000
|
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.deletions_log)),记录内容取决于 deletions_log 可读结果;
|
|
2514
3001
|
def deletions(self):
|
|
2515
3002
|
return list(read_jsonl(self.deletions_log))
|
|
@@ -3412,7 +3899,176 @@ class MdCGOS(MdCG):
|
|
|
3412
3899
|
|
|
3413
3900
|
# ---- 主动遗忘(写入侧三问闸门)+ 写保护盘点 ----
|
|
3414
3901
|
|
|
3415
|
-
#
|
|
3902
|
+
# 生效条件:以 autonomy_modes.decide(B_MERGE) 判定——ALLOW 返回 None(调用方照旧执行合并原语,merge 裁决本身一字不改);FORBID 返回 verdict="FORBID"/moved_to="autonomy_forbidden" 的终态;CONFIRM 经 autonomy_modes.propose_mutation 出变更单(目标 tgt、后像 content、复现参数 override/session 入载荷 meta)并返回 verdict="CONFIRM"/moved_to="review_queue" 的终态(**不调用任何合并原语、不落盘**);
|
|
3903
|
+
def _autonomy_gate_merge(self, node_id, tgt, content, kw, primitive,
|
|
3904
|
+
reason="", gate=None):
|
|
3905
|
+
"""三档自治档位在 **B 合并**出口的判定单点(设计 §三:变更确认档需确认)。
|
|
3906
|
+
|
|
3907
|
+
为什么是单点:合并的**两个落点**(`writelimit.check` 的 CONVERGE →
|
|
3908
|
+
`converge_into`、`forgetting.assess` 的 MERGE → `reinforce`)都必须
|
|
3909
|
+
过同一判据——两处各写一份必然漂移;且判定必须晚于**各自的裁决**
|
|
3910
|
+
(限流闸 / 遗忘三问),档位不参与裁决(纯加严)。
|
|
3911
|
+
|
|
3912
|
+
变更单载荷:动作类 B · 目标 tgt · 后像 = 本次新内容 · 理由 · 复现参数
|
|
3913
|
+
(override/session——merge 原语的两个非默认槽,裁决 accept 时按此重放)。
|
|
3914
|
+
提议者/会话由 propose 的库端快照自动归属(`actor`/`session` 字段)。
|
|
3915
|
+
"""
|
|
3916
|
+
from . import autonomy_modes as _am
|
|
3917
|
+
# 资格在先(纯加严):合并落盘时 reinforce/converge_into 内部的
|
|
3918
|
+
# guard_overwrite 若会拒(受保护/越权),必须在**出单之前**照样拒——
|
|
3919
|
+
# 否则一次本该报错的合并会变成「静默入队」。
|
|
3920
|
+
self.write_qualify(tgt, override=bool(kw.get("override")),
|
|
3921
|
+
actor=self.actor)
|
|
3922
|
+
dec = _am.decide(_am.B_MERGE)
|
|
3923
|
+
if dec["decision"] == _am.ALLOW:
|
|
3924
|
+
return None
|
|
3925
|
+
aut = {"mode": dec["mode"], "action": dec["action"],
|
|
3926
|
+
"action_name": dec["action_name"], "decision": dec["decision"]}
|
|
3927
|
+
if dec["decision"] == _am.FORBID:
|
|
3928
|
+
# 形态对齐原 MERGE 出口(gate / merged_into 两键照带):调用方按
|
|
3929
|
+
# 「gate.reason」取值读判断依据的既有读法不因本闸断裂。
|
|
3930
|
+
return {"verdict": "FORBID", "node_id": node_id, "gate": gate,
|
|
3931
|
+
"merged_into": tgt, "moved_to": "autonomy_forbidden",
|
|
3932
|
+
"autonomy": aut, "hint": dec["hint"]}
|
|
3933
|
+
# 队列的层闸(MdCGSecure.propose)按**目标节点的真层**校验:合并落点
|
|
3934
|
+
# 本来就是目标层,用调用方声明的层会让闸与落点错位(issue50 前车:
|
|
3935
|
+
# 校验层与落盘层错位会造死提案)。
|
|
3936
|
+
tlayer = ((self.index.get("nodes") or {}).get(tgt) or {}).get("layer") \
|
|
3937
|
+
or ((self.get(tgt) or {}).get("frontmatter") or {}).get("layer") \
|
|
3938
|
+
or "contextual"
|
|
3939
|
+
pay = _am.mutation_payload(
|
|
3940
|
+
_am.B_MERGE, tgt, after=content,
|
|
3941
|
+
reason=reason or ("档位确认(变更确认档):新写入内容并入既有节点 %s"
|
|
3942
|
+
"(原语 %s)" % (tgt, primitive)),
|
|
3943
|
+
primitive=primitive,
|
|
3944
|
+
meta={"override": bool(kw.get("override")),
|
|
3945
|
+
"session": _writer_session(self, kw)})
|
|
3946
|
+
pr = _am.propose_mutation(self, _am.B_MERGE, tgt, payload=pay,
|
|
3947
|
+
layer=tlayer,
|
|
3948
|
+
sensitivity=kw.get("sensitivity"), info=True)
|
|
3949
|
+
return {"verdict": "CONFIRM", "node_id": node_id, "gate": gate,
|
|
3950
|
+
"merged_into": tgt, "moved_to": "review_queue",
|
|
3951
|
+
"pid": pr["pid"], "mutation": pay, "autonomy": aut,
|
|
3952
|
+
"hint": "%s 本次未合并:变更单已入审核队列(pid=%s,目标 %s,"
|
|
3953
|
+
"新内容随单)。" % (dec["hint"], pr["pid"], tgt)}
|
|
3954
|
+
|
|
3955
|
+
# 生效条件:以 forgetting.prior_node 判「同 id 已存在」(存在=C 改写、不存在=A 新增)后交 autonomy_modes.decide 判定——ALLOW 返回 None(调用方照旧 add 落盘,逐位不变);FORBID 返回 ok=False/verdict="FORBID"/moved_to="autonomy_forbidden" 的终态(fail-closed,不落盘、不出单);CONFIRM 先经 self.write_qualify 跑既有资格探针(受保护/越权覆写照旧当场抛出、不静默入队)再经 autonomy_modes.propose_mutation 出变更单并返回 ok=True/verdict="CONFIRM"/committed=False/moved_to="review_queue" 的终态(**不落盘**);既有节点(C 改写)confirm/plan 两档出单、full 档放行;新节点(A 新增)confirm/full 两档放行、plan 档 FORBID(计划外零变更);
|
|
3956
|
+
def _autonomy_gate_rewrite(self, node_id, content, layer, kw,
|
|
3957
|
+
override=False, reason="", gate=None):
|
|
3958
|
+
"""三档自治档位在 **gated 面「C 改写 / A 新增」**出口的判定单点。
|
|
3959
|
+
|
|
3960
|
+
为什么要它(补强批次 2026-10-02,复核发现 1/2):`gated=true` 的写入
|
|
3961
|
+
在上游 `writepipe._gate_gated` 是**替代执行路径**(`remember_gated`
|
|
3962
|
+
自行落盘后直接返回终态,链尾 `_gate_autonomy` 根本不执行)——而
|
|
3963
|
+
`remember_gated` 的 ACCEPT 分支此前**无任何档位判据**、直调 `self.add`:
|
|
3964
|
+
缺省(confirm)档对**既有 node_id 的覆写**直接落盘(不出变更单)、
|
|
3965
|
+
计划档对**新 node_id** 也照样写入。二者都与设计 §三(C=需确认、
|
|
3966
|
+
计划外零变更)相抵。本方法是该面**唯一**的档位判定点(与写链
|
|
3967
|
+
`writepipe._gate_autonomy`、合并面 `_autonomy_gate_merge` 同口径)。
|
|
3968
|
+
|
|
3969
|
+
位置(纯加严,设计 §三 硬约束②):遗忘闸四态裁决(`forgetting.assess`)
|
|
3970
|
+
之后、`self.add` 落盘之前;档位**不参与**裁决、也不放宽任何既有判据。
|
|
3971
|
+
|
|
3972
|
+
动作类判定:`forgetting.prior_node(self, node_id)` 同 id 存在 ⇒
|
|
3973
|
+
**C 改写**;不存在 ⇒ **A 新增**——判据复用同一单点,不另写一份存在性判据。
|
|
3974
|
+
|
|
3975
|
+
资格在先:既有节点出单**之前**先 `self.write_qualify`(与 add 内部同一批
|
|
3976
|
+
protect 单点,只判不写)——受保护/越权覆写照旧当场 `ProtectionError`/
|
|
3977
|
+
`AccessDenied`,不会变成「静默入队」(放宽既有判据)。
|
|
3978
|
+
|
|
3979
|
+
载荷 meta(复现原动作):取**写链同一份白名单** `writepipe.
|
|
3980
|
+
_AUTONOMY_META_KEYS`(延迟导入,单一真源),并补 gated 面独有的 fm 扩展键
|
|
3981
|
+
`role`——直写路径 `self.add(..., **kw)` 会把 role 经 add 的 `**extra`
|
|
3982
|
+
落 fm,重放不带就会「出单→确认→写」与直写两条路径 fm 不等价。
|
|
3983
|
+
边界(如实):`kw` 里的 `session` 不在本白名单内、且执行桥会把它
|
|
3984
|
+
收进 B 原语的 session 槽(C 面不复现)——当前两个 gated 调用点
|
|
3985
|
+
(`writepipe._gate_gated` / `mcp_server.mdcg_remember`)都不传 session。
|
|
3986
|
+
|
|
3987
|
+
返回形态:出单 = `ok=True/committed=False/moved_to="review_queue"`
|
|
3988
|
+
且带 pid(口径同写链 `_gate_autonomy`);FORBID = `ok=False/
|
|
3989
|
+
moved_to="autonomy_forbidden"`(口径同写链 A 面);ALLOW = `None`
|
|
3990
|
+
(调用方原路落盘,行为逐位不变)。
|
|
3991
|
+
"""
|
|
3992
|
+
from . import autonomy_modes as _am
|
|
3993
|
+
# 载荷 meta 白名单的单一真源在写链(写面唯一定义处);延迟导入防循环。
|
|
3994
|
+
from . import writepipe as _wp
|
|
3995
|
+
prior = forgetting.prior_node(self, node_id)
|
|
3996
|
+
action = _am.C_REWRITE if prior is not None else _am.A_ADD
|
|
3997
|
+
if prior is not None:
|
|
3998
|
+
# 资格在先(纯加严):同 `_autonomy_gate_merge` / `writepipe.
|
|
3999
|
+
# _gate_autonomy`——覆写的层闸/写保护闸住在 `self.add` 内部,档位
|
|
4000
|
+
# 先出单会把一次本该报错的覆写变成「静默入队」(放宽既有判据)。
|
|
4001
|
+
self.write_qualify(node_id, target_layer=layer,
|
|
4002
|
+
override=bool(override), actor=self.actor)
|
|
4003
|
+
dec = _am.decide(action)
|
|
4004
|
+
if dec["decision"] == _am.ALLOW:
|
|
4005
|
+
return None
|
|
4006
|
+
aut = {"mode": dec["mode"], "action": dec["action"],
|
|
4007
|
+
"action_name": dec["action_name"], "decision": dec["decision"]}
|
|
4008
|
+
if dec["decision"] == _am.FORBID:
|
|
4009
|
+
return {"ok": False, "verdict": "FORBID", "node_id": node_id,
|
|
4010
|
+
"committed": False, "gate": gate,
|
|
4011
|
+
"moved_to": "autonomy_forbidden", "error": "autonomy_forbid",
|
|
4012
|
+
"autonomy": aut, "hint": dec["hint"]}
|
|
4013
|
+
meta = {k: kw[k] for k in _wp._AUTONOMY_META_KEYS
|
|
4014
|
+
if kw.get(k) is not None}
|
|
4015
|
+
if kw.get("role") is not None:
|
|
4016
|
+
meta["role"] = kw["role"]
|
|
4017
|
+
if override:
|
|
4018
|
+
meta["override"] = True
|
|
4019
|
+
pay = _am.mutation_payload(
|
|
4020
|
+
action, node_id, after=content,
|
|
4021
|
+
reason=reason or ("gated 面覆写(gated=true / remember_gated 对既有"
|
|
4022
|
+
"节点 %s 的 %s)" % (node_id, _am.ACTION_NAMES[action])),
|
|
4023
|
+
primitive="add", meta=meta)
|
|
4024
|
+
pr = _am.propose_mutation(self, action, node_id, payload=pay,
|
|
4025
|
+
layer=layer,
|
|
4026
|
+
sensitivity=kw.get("sensitivity"), info=True)
|
|
4027
|
+
return {"ok": True, "verdict": "CONFIRM", "node_id": node_id,
|
|
4028
|
+
"committed": False, "gate": gate, "moved_to": "review_queue",
|
|
4029
|
+
"pid": pr["pid"], "mutation": pay, "autonomy": aut,
|
|
4030
|
+
"hint": "%s 本次未落盘:变更单已入审核队列(pid=%s,目标 %s)。"
|
|
4031
|
+
% (dec["hint"], pr["pid"], node_id)}
|
|
4032
|
+
|
|
4033
|
+
# 生效条件:node_id 不在索引中(新节点)时返回 None(新写入面没有「既有资格闸」,其层闸由落盘面把守);在索引中时以 override=False 跑 protect.guard_overwrite(既有节点覆写前的统一双闸:principal 层闸按**既有节点真层** + 引擎级写保护闸)与(target_layer 非空且与真层不同时)protect.guard_move(受保护节点移出保护层的降级闸),任一拒绝抛出的 AccessDenied 一律照抛、ProtectionError 仅在 override 为假时照抛(override 为真时放行——真实写入那一次会带授权并写快照,本探针**不写快照**);全部通过返回 None;本函数不落盘、不改索引、不写任何快照;
|
|
4034
|
+
def write_qualify(self, node_id, target_layer=None, override=False,
|
|
4035
|
+
actor=None):
|
|
4036
|
+
"""既有节点写面的**资格探针**(三档自治:档位判定必须晚于既有资格闸)。
|
|
4037
|
+
|
|
4038
|
+
为什么要它(纯加严,设计 §三 硬约束②):写面落盘时的两道资格闸
|
|
4039
|
+
——`protect.guard_overwrite`(层闸 + 写保护闸)与 `protect.guard_move`
|
|
4040
|
+
(降级闸)——住在 `MdCG.add` **内部**。若档位闸在 add 之前就判「出变更
|
|
4041
|
+
单」,一次本该被拒的越权/受保护覆写就从 `AccessDenied`/`ProtectionError`
|
|
4042
|
+
变成「静默入队」——那是**放宽既有判据**(未授权者凭空获得一个排单出口)。
|
|
4043
|
+
故档位判定前先用**同一批单点**跑一遍资格:判据只有一份(protect 的那些
|
|
4044
|
+
单点),这里只负责按 add 的次序与范围调用它们。
|
|
4045
|
+
|
|
4046
|
+
无副作用边界(如实):override=False 时 guard_write/guard_move 只判不写;
|
|
4047
|
+
override=True 的情形**不由本探针**产生快照——快照归真实写入那一次
|
|
4048
|
+
(`_allow` → `_protected_history`)。故本探针不得被当作「授权检查」以外的
|
|
4049
|
+
用途(它不建目录、不落文件、不动索引)。
|
|
4050
|
+
"""
|
|
4051
|
+
e = (self.index.get("nodes") or {}).get(node_id)
|
|
4052
|
+
if not e:
|
|
4053
|
+
return None
|
|
4054
|
+
if target_layer and str(e.get("layer") or "") \
|
|
4055
|
+
and str(e.get("layer")) != str(target_layer):
|
|
4056
|
+
try:
|
|
4057
|
+
protect.guard_move(self, node_id, target_layer, override=False,
|
|
4058
|
+
actor=actor)
|
|
4059
|
+
except protect.ProtectionError:
|
|
4060
|
+
if not override:
|
|
4061
|
+
raise
|
|
4062
|
+
try:
|
|
4063
|
+
# 层闸按**既有节点真层**解析(layer=None ⇒ protect._resolve_target
|
|
4064
|
+
# 取索引/ fm 的真层)——与 add 的 N221 跨层覆写同口径(源层在先)。
|
|
4065
|
+
protect.guard_overwrite(self, node_id, override=False, actor=actor)
|
|
4066
|
+
except protect.ProtectionError:
|
|
4067
|
+
if not override:
|
|
4068
|
+
raise
|
|
4069
|
+
return None
|
|
4070
|
+
|
|
4071
|
+
# 生效条件:kw 中 gated 为假值时旁路直接 ACCEPT 写入并返回 bypass;否则 writelimit.check 非 None 时按 CONVERGE→MERGE 并经 converge_into(带 session 归属)并入 target、DROP/DEFER 只记 forgetting 日志,无限流拦截时按 forgetting.assess 的四态处理(ACCEPT **先过档位闸 `_autonomy_gate_rewrite`**——既有 node_id ⇒ C 改写(confirm/plan 出单且不落盘、full 放行)、不存在 ⇒ A 新增(confirm/full 放行、plan fail-closed `autonomy_forbidden`),放行才走 add——issue50-d:落盘 importance 恒等于裁决 imp["score"](hint 或启发式)并落 importance_source("hint"|"heuristic"),add 的自动保护位只认显式声明;MERGE 走 reinforce(带 content 与 session,返回体带 content_sink 去向);DROP 经 forgetting.record_drop 落去向留痕并带 dropped 去向单;DEFER 不落盘、经既有入队单点 self.propose 把**正文 + 声明 meta(**kw)+「为何待定」**送进审核队列(返回体带 proposed=pid;node_id 非字符串时兜底成内容派生 id 并写 proposed_id_fallback),并照旧只记 forgetting 留痕)。
|
|
3416
4072
|
def remember_gated(self, node_id, content, layer="contextual", **kw):
|
|
3417
4073
|
"""写入情景层记忆前的**主动遗忘闸门**:三问 → 四态。
|
|
3418
4074
|
|
|
@@ -3424,6 +4080,34 @@ class MdCGOS(MdCG):
|
|
|
3424
4080
|
DROP 丢弃(低熵噪音)/ DEFER 待定(不写)。
|
|
3425
4081
|
四种结果都写进 `_forgetting.jsonl`,可审计。
|
|
3426
4082
|
|
|
4083
|
+
issue50-b(2026-10-01):DEFER 此前**只留一行痕**——不落盘、无收件箱。
|
|
4084
|
+
issue50-a 把半重复判 DEFER 后,这条出口的语义(「待定**复核**」)与
|
|
4085
|
+
实际(静默不记)分叉:噪声缺陷会换成「不落盘且无收件箱」缺陷。故
|
|
4086
|
+
DEFER 出口接线**既有入队单点** `self.propose`(与审计/策略闸
|
|
4087
|
+
`writepipe._gate_audit`、一致性闸 `_gate_consistency` 同一条链、同一
|
|
4088
|
+
payload_hash 幂等对账):正文随提案进审核队列,裁决 accept 即落盘;
|
|
4089
|
+
「为何待定」写进提案记录的 `extra.defer_reason`(propose 的既有任意槽,
|
|
4090
|
+
不新增队列字段/队列类型)。
|
|
4091
|
+
|
|
4092
|
+
issue50-c F1(2026-10-01,扩面修):入队**只带 content/layer/sensitivity**,
|
|
4093
|
+
调用方声明的其余 meta 全丢——accept 只能从队列记录取值,故「同一声明」下
|
|
4094
|
+
DEFER→accept 落盘的节点比直接 ACCEPT 少 tags/condition_space/
|
|
4095
|
+
verification_basis/non_applicable_conditions/derived_from/relation/role
|
|
4096
|
+
(实测 tags=[] 而对面有值、derived_from 缺失、bucket_zh 因 tags 空而不生成)。
|
|
4097
|
+
处置:入队时把本闸收到的 meta 一并落进队列记录(`**kw` → `self.propose`,
|
|
4098
|
+
口径与直接 ACCEPT 的 `add(**kw)` 同源),accept 分支既有的
|
|
4099
|
+
`tags=/condition_space=/**extra` 映射即把它写回节点 fm——**两条路径元数据
|
|
4100
|
+
等价**,且 review_decide 无需改动。
|
|
4101
|
+
|
|
4102
|
+
issue50-c F2(2026-10-01):`node_id=None` 时 propose 造 pid 的
|
|
4103
|
+
`node_id + str(…)` 抛 TypeError;按仓内既有形态(`forgetting._prefeed_id`)
|
|
4104
|
+
兜底成内容派生 id,不崩且非静默(返回体 `proposed_id_fallback`)。
|
|
4105
|
+
|
|
4106
|
+
接线只覆盖**本闸自己**的 DEFER——
|
|
4107
|
+
writelimit 限流 DEFER 在上方已 return(其语义是「先别写」,原文在
|
|
4108
|
+
recent 时间线,不是内容待定),gated=False 旁路与 ACCEPT 后因
|
|
4109
|
+
on_conflict=defer 降级的 DEFER(`out["conflict"]`)都不在此列。
|
|
4110
|
+
|
|
3427
4111
|
B3(2026-09-30):MERGE 分支此前**不把 content 往下传**(`reinforce`
|
|
3428
4112
|
也没有该槽)——近重复的第二次写入,其新正文 100% 丢弃,只有既有节点
|
|
3429
4113
|
被强化;现传 content + session,新正文按目标缺的行追加为聚合行落盘,
|
|
@@ -3431,6 +4115,26 @@ class MdCGOS(MdCG):
|
|
|
3431
4115
|
来源,`assess` :214 先于 MERGE)把新正文全文与去向记进 `_forgetting.jsonl`
|
|
3432
4116
|
并返回 `dropped` 去向单(trace_id/sha1/检索入口)。两条分支都有处置。
|
|
3433
4117
|
H3:合并时把写入方会话归属并列落目标 fm(`fm.merge_sources`)。
|
|
4118
|
+
|
|
4119
|
+
issue50-d(2026-10-02):ACCEPT 分支**落盘=裁决值**——此前「hint 非
|
|
4120
|
+
None 才透传」的口径把启发式裁决值(0.84/0.71 量级,隔离沙箱实证)
|
|
4121
|
+
全部丢在闸门上,落盘一律默认 0.5;现无条件落 `imp["score"]` 并按
|
|
4122
|
+
`imp["from"]` 落 `importance_source`("hint"|"heuristic",进 fm 与
|
|
4123
|
+
索引条目)。`add` 的自动保护位只认显式声明:heuristic 过线只落分、
|
|
4124
|
+
不打保护位——由此审计面文案(assess 保护分支按 from 分叉)、落盘分、
|
|
4125
|
+
保护位三者首次同源一致。外部基准(hive-memory-bench v1.0)测得 L3
|
|
4126
|
+
使用端损耗 3-4/19:本批让重要度轴带电(排序次级键从此拿到真分)。
|
|
4127
|
+
|
|
4128
|
+
补强批次(2026-10-02,复核发现 1/2 的收口):ACCEPT 分支此前**无任何
|
|
4129
|
+
档位判据**、直调 `self.add`——`gated=true`(插件自动记忆主写入通道)
|
|
4130
|
+
在缺省 confirm 档对既有 node_id 直接覆写(不出变更单)、在计划档对新
|
|
4131
|
+
node_id 也照样写入。现该分支落盘前先经 `_autonomy_gate_rewrite`
|
|
4132
|
+
(gated 面档位判定单点,与写链 `writepipe._gate_autonomy` 同口径、
|
|
4133
|
+
同一存在性单点 `forgetting.prior_node`):C 改写 confirm/plan 出单、
|
|
4134
|
+
full 放行;A 新增 confirm/full 放行(行为逐位不变)、plan fail-closed。
|
|
4135
|
+
出单前先跑 `write_qualify` 资格探针——受保护/越权覆写照旧当场拒,
|
|
4136
|
+
不因「改为入队」而放宽既有判据(纯加严)。出单/拒绝都写一行
|
|
4137
|
+
`_forgetting.jsonl`(verdict=CONFIRM/FORBID + hint),可审计。
|
|
3434
4138
|
"""
|
|
3435
4139
|
role = kw.get("role")
|
|
3436
4140
|
vb = kw.get("verification_basis")
|
|
@@ -3462,6 +4166,18 @@ class MdCGOS(MdCG):
|
|
|
3462
4166
|
if lim is not None:
|
|
3463
4167
|
if lim["verdict"] == "CONVERGE":
|
|
3464
4168
|
tgt = lim["target"]
|
|
4169
|
+
# 三档自治批次②:档位判定在**限流闸裁决之后、合并原语之前**。
|
|
4170
|
+
_ag = self._autonomy_gate_merge(node_id, tgt, content, kw,
|
|
4171
|
+
primitive="converge_into",
|
|
4172
|
+
gate=lim)
|
|
4173
|
+
if _ag is not None:
|
|
4174
|
+
forgetting.log(self, {"t": time.time(), "node_id": node_id,
|
|
4175
|
+
"layer": layer,
|
|
4176
|
+
"verdict": _ag["verdict"],
|
|
4177
|
+
"reason": _ag["hint"],
|
|
4178
|
+
"limiter": lim.get("limiter"),
|
|
4179
|
+
"actor": self.actor})
|
|
4180
|
+
return _ag
|
|
3465
4181
|
out = {"verdict": "MERGE", "node_id": node_id,
|
|
3466
4182
|
"merged_into": tgt, "gate": lim,
|
|
3467
4183
|
"converged": writelimit.converge_into(
|
|
@@ -3489,8 +4205,39 @@ class MdCGOS(MdCG):
|
|
|
3489
4205
|
v = verdict["verdict"]
|
|
3490
4206
|
out = {"verdict": v, "node_id": node_id, "gate": verdict}
|
|
3491
4207
|
if v == "ACCEPT":
|
|
3492
|
-
|
|
3493
|
-
|
|
4208
|
+
# issue50-d(2026-10-02):**落盘=裁决值**。此前「hint 非 None 才
|
|
4209
|
+
# 透传」的口径把启发式裁决值全部丢在闸门上——隔离沙箱实证:
|
|
4210
|
+
# u_heur_new 裁决 0.837/落盘 0.5、t_heur 裁决 0.711/落盘 0.5,落盘
|
|
4211
|
+
# 重要度不携带任何区分信息(启发式全 0.5、插件 hint 通道全 0.6),
|
|
4212
|
+
# 排序/使用端拿不到真分。现无条件落 `imp["score"]`(hint 或启发式,
|
|
4213
|
+
# 均为 assess 实际使用的裁决值,含 0~1 裁剪),并按裁决来源落
|
|
4214
|
+
# `importance_source`("hint"|"heuristic")进 fm 与索引条目——
|
|
4215
|
+
# add 的自动保护位据此只认显式声明(heuristic 过线只落分、不打
|
|
4216
|
+
# 保护位,与裁决②一致;writelimit 的保护豁免仍读原始 hint,不受
|
|
4217
|
+
# 本批影响)。
|
|
4218
|
+
kw["importance"] = verdict["importance"]["score"]
|
|
4219
|
+
kw["importance_source"] = verdict["importance"]["from"]
|
|
4220
|
+
# 三档自治批次②补强(2026-10-02):**gated 面的 C 改写 / A 新增档位
|
|
4221
|
+
# 判定**。此前本分支直调 `self.add`——`gated=true` 的覆写在确认档
|
|
4222
|
+
# 不落任何档位判定就覆写既有节点(复核发现 1:插件自动记忆的主写入
|
|
4223
|
+
# 通道可旁路确认档),计划档的新增也不被拦(发现 2)。判定与写链
|
|
4224
|
+
# `writepipe._gate_autonomy` **同口径**、同一存在性单点
|
|
4225
|
+
# (`forgetting.prior_node`):既有 node_id ⇒ C 改写(confirm/plan
|
|
4226
|
+
# 出单、full 放行)、不存在 ⇒ A 新增(confirm/full 放行、plan
|
|
4227
|
+
# fail-closed);出单前先跑 write_qualify 资格探针(受保护/越权覆写
|
|
4228
|
+
# 照旧当场拒,不静默入队)。位置在 importance 落盘值写入之后,故
|
|
4229
|
+
# 变更单载荷带的是**裁决值**(issue50-d 同源口径)。
|
|
4230
|
+
_ag = self._autonomy_gate_rewrite(node_id, content, layer, kw,
|
|
4231
|
+
override=override, gate=verdict)
|
|
4232
|
+
if _ag is not None:
|
|
4233
|
+
forgetting.log(self, {"t": time.time(), "node_id": node_id,
|
|
4234
|
+
"layer": layer,
|
|
4235
|
+
"verdict": _ag["verdict"],
|
|
4236
|
+
"reason": _ag["hint"],
|
|
4237
|
+
"importance": verdict["importance"],
|
|
4238
|
+
"entropy": verdict["entropy"],
|
|
4239
|
+
"actor": self.actor})
|
|
4240
|
+
return _ag
|
|
3494
4241
|
try:
|
|
3495
4242
|
out["written"] = self.add(node_id, content, layer=layer,
|
|
3496
4243
|
override=override,
|
|
@@ -3508,6 +4255,24 @@ class MdCGOS(MdCG):
|
|
|
3508
4255
|
writelimit.record_accepted(self, node_id, content)
|
|
3509
4256
|
elif v == "MERGE":
|
|
3510
4257
|
tgt = verdict["redundancy"]["with"]
|
|
4258
|
+
# 三档自治批次②:档位判定在**遗忘闸四态裁决之后、合并原语之前**
|
|
4259
|
+
# (纯加严——裁决仍归 forgetting.assess,档位只决定「立即合并」还是
|
|
4260
|
+
# 「出变更单」)。合并对象为空(tgt 假值)时不上报变更单:原路径
|
|
4261
|
+
# 的 reinforce 也只在 tgt 为真时执行,档位不得把「无事发生」变成
|
|
4262
|
+
# 「一张凭空指向空目标的单」。
|
|
4263
|
+
if tgt:
|
|
4264
|
+
_ag = self._autonomy_gate_merge(node_id, tgt, content, kw,
|
|
4265
|
+
primitive="reinforce",
|
|
4266
|
+
gate=verdict)
|
|
4267
|
+
if _ag is not None:
|
|
4268
|
+
forgetting.log(self, {"t": time.time(), "node_id": node_id,
|
|
4269
|
+
"layer": layer,
|
|
4270
|
+
"verdict": _ag["verdict"],
|
|
4271
|
+
"reason": _ag["hint"],
|
|
4272
|
+
"importance": verdict["importance"],
|
|
4273
|
+
"entropy": verdict["entropy"],
|
|
4274
|
+
"actor": self.actor})
|
|
4275
|
+
return _ag
|
|
3511
4276
|
out["merged_into"] = tgt
|
|
3512
4277
|
# B3 ②(2026-09-30):把**新正文**与写入方归属一并传下去。此前只传
|
|
3513
4278
|
# tgt——调用方手上正握着 content 与 node_id 却不往下传,reinforce 写回
|
|
@@ -3531,6 +4296,56 @@ class MdCGOS(MdCG):
|
|
|
3531
4296
|
self, content, node_id=node_id, target=tgt, verdict=verdict,
|
|
3532
4297
|
layer=layer, sensitivity=kw.get("sensitivity"),
|
|
3533
4298
|
session=_writer_session(self, kw), actor=self.actor)
|
|
4299
|
+
elif v == "DEFER":
|
|
4300
|
+
# issue50-b(2026-10-01):遗忘闸门的 DEFER 此前只写一行
|
|
4301
|
+
# `_forgetting.jsonl` 留痕——**不落盘且无收件箱**(对比 DROP 分支
|
|
4302
|
+
# 有 `forgetting.record_drop` 留全文 + trace_id)。第①条(issue50-a)
|
|
4303
|
+
# 把半重复从「落成近似重复节点」改成 DEFER 之后,这条出口就会把
|
|
4304
|
+
# 噪声缺陷换成「静默不记」缺陷。处置:接线**既有入队单点**
|
|
4305
|
+
# `self.propose`——与审计/策略闸(`writepipe._gate_audit`)、一致性闸
|
|
4306
|
+
# (`_gate_consistency`)**同一条入队链**(不新增第二套入队实现),
|
|
4307
|
+
# 复用其 payload_hash 幂等对账(同内容重复写入不长第二条)。
|
|
4308
|
+
# 只覆盖**本闸自己的** DEFER——writelimit 限流 DEFER 在上方已 return
|
|
4309
|
+
# (语义是「先别写」,原文在 recent 时间线,不是内容待定);
|
|
4310
|
+
# gated=False 旁路与 ACCEPT 后 on_conflict=defer 的降级 DEFER 都不走
|
|
4311
|
+
# 本分支(后者发生在 `if v == "ACCEPT"` 内,v 就地改写)。
|
|
4312
|
+
# 「为何待定」写进提案的既有任意槽 extra(不新增队列字段/队列类型,
|
|
4313
|
+
# 见 propose 的 `**kw → extra`);密级随 sensitivity 一并透传,避免
|
|
4314
|
+
# 声明 private 在此静默降级 internal(B2 同一漏传族)。
|
|
4315
|
+
#
|
|
4316
|
+
# issue50-c F1(2026-10-01,扩面修):此前入队只带 content/layer/
|
|
4317
|
+
# sensitivity,**调用方声明的其余 meta 全丢**(tags/condition_space/
|
|
4318
|
+
# verification_basis/non_applicable_conditions/derived_from/relation/
|
|
4319
|
+
# role…)。accept 只能从队列记录取值,于是「同一声明」下
|
|
4320
|
+
# DEFER→accept 落盘的节点比「直接 ACCEPT」少掉这些键(实测 tags=[]
|
|
4321
|
+
# 而对面有值、derived_from 缺失、role 缺失、bucket_zh 因 tags 空而
|
|
4322
|
+
# 不生成)——同一份声明的两条落地路径元数据不等价。处置:把本闸收到
|
|
4323
|
+
# 的那组 meta **随入队一并落进队列记录**(`**kw` 交给 `self.propose`:
|
|
4324
|
+
# 其形参 `tags`/`condition_space`/`sensitivity`/`verify` 落 rec 专属槽,
|
|
4325
|
+
# 其余键落 `extra`——与直接 ACCEPT 走 `add(**kw)` 的口径同源,不新增
|
|
4326
|
+
# 队列字段/队列类型);accept 分支早已把 `rec.tags`/`rec.condition_space`
|
|
4327
|
+
# /`**extra` 映射回 `add`(mdcos.py:2573),故无需改 review_decide。
|
|
4328
|
+
#
|
|
4329
|
+
# issue50-c F2(2026-10-01):`node_id=None` 时上文 `**kw` 透传依旧,
|
|
4330
|
+
# 但 propose 造 pid 用 `node_id + str(time.time()) + …`——None + str
|
|
4331
|
+
# 抛 TypeError。本闸的 ACCEPT/MERGE/DROP 三个出口都会经 `add` 对
|
|
4332
|
+
# 非字符串 node_id 抛 ValueError(fail-closed 既有行为),唯独 DEFER
|
|
4333
|
+
# 出口的入队单点会以 TypeError 崩出。处置:按仓内既有形态兜底成
|
|
4334
|
+
# **内容派生 id**(`forgetting._prefeed_id` 同款——forgetting.py:879
|
|
4335
|
+
# 对「无 id 候选」即用 `pre_<sha1[:12]>`,同一语义的单点复用),
|
|
4336
|
+
# 使「DEFER 有收件箱」在缺 id 时同样成立、不崩;兜底事实写进返回体
|
|
4337
|
+
# `proposed_id_fallback`(非静默——调用方可判、可检索),**不进
|
|
4338
|
+
# forgetting 留痕行**(该行形状由 issue50-b 冻结)。
|
|
4339
|
+
_why = "遗忘闸门:" + str(verdict.get("reason") or "待定复核")
|
|
4340
|
+
_dup_with = (verdict.get("redundancy") or {}).get("with")
|
|
4341
|
+
if _dup_with:
|
|
4342
|
+
_why += "(重复对象 %s)" % _dup_with
|
|
4343
|
+
_nid = node_id if isinstance(node_id, str) and node_id else \
|
|
4344
|
+
forgetting._prefeed_id(content)
|
|
4345
|
+
out["proposed"] = self.propose(
|
|
4346
|
+
_nid, content, layer=layer, **kw, defer_reason=_why)
|
|
4347
|
+
if _nid != node_id:
|
|
4348
|
+
out["proposed_id_fallback"] = _nid
|
|
3534
4349
|
# DROP 的去向留痕已由 record_drop 单点落盘(含全文/trace_id),此处只补
|
|
3535
4350
|
# ACCEPT/MERGE/DEFER 的裁决留痕,避免同一次裁决出现两行。
|
|
3536
4351
|
if v != "DROP":
|
|
@@ -4373,11 +5188,27 @@ class MdCGSecure(MdCGOS):
|
|
|
4373
5188
|
|
|
4374
5189
|
# ---------- 管理:需 can_admin ----------
|
|
4375
5190
|
|
|
4376
|
-
#
|
|
4377
|
-
def
|
|
5191
|
+
# 生效条件:principal 在位时调用 principal.require_admin("forget")(越权抛 AccessDenied),返回 None;
|
|
5192
|
+
def _forget_admin(self):
|
|
5193
|
+
"""删除面的**管理权限闸单点**(三档自治批次②:两个入口共用一份口径)。
|
|
5194
|
+
|
|
5195
|
+
为什么抽出来:`forget`(既有)与 `forget_gated`(档位路径)都必须先过
|
|
5196
|
+
这道闸——`MdCGSecure` 的 can_admin 要求原先只写在 `forget` 里,档位路径
|
|
5197
|
+
若直调 `_forget_apply` 就把它整个跳过了(实测:`MDCG_CAN_ADMIN=0` 的身份
|
|
5198
|
+
经 `mdcg_forget` 能把节点删掉)。判据只此一份,两个入口都走它。
|
|
5199
|
+
"""
|
|
4378
5200
|
self.principal.require_admin("forget")
|
|
5201
|
+
|
|
5202
|
+
# 生效条件:先 _forget_admin(),再转 super().forget(node_id, reason, override=override);
|
|
5203
|
+
def forget(self, node_id: str, reason: str = "", override: bool = False):
|
|
5204
|
+
self._forget_admin()
|
|
4379
5205
|
return super().forget(node_id, reason, override=override)
|
|
4380
5206
|
|
|
5207
|
+
# 生效条件:先 _forget_admin()(与 forget 同序同错型——管理闸在资格闸与档位判定**之前**),再转 super().forget_gated(node_id, reason, override=override);三档自治批次② 的 D 删除工具面路径由此把 can_admin 要求带进档位路径;
|
|
5208
|
+
def forget_gated(self, node_id: str, reason: str = "", override: bool = False):
|
|
5209
|
+
self._forget_admin()
|
|
5210
|
+
return super().forget_gated(node_id, reason, override=override)
|
|
5211
|
+
|
|
4381
5212
|
# 生效条件:先 principal.require_admin("restore"),再转 super().restore(node_id, force=force)。
|
|
4382
5213
|
def restore(self, node_id: str, force: bool = False):
|
|
4383
5214
|
self.principal.require_admin("restore")
|