@furongjun1999/dsh-memory 0.7.0 → 0.7.2
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 +8 -6
- package/codebuddy/CODEBUDDY.md +5 -5
- 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/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/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -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/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 +25 -25
- 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//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/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +4 -4
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench6_arms.py +10 -6
- package/md_cg/census.py +2 -1
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +17 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +63 -7
- package/md_cg/mdcg.py +157 -31
- package/md_cg/mdcos.py +675 -19
- package/md_cg/mreview/locate.py +1 -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 +420 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/routing.py +10 -1
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -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_cons200_scan_selection.py +791 -0
- package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
- package/md_cg/test_generation_guard.py +15 -9
- package/md_cg/test_health_corrupt_utf8.py +37 -0
- 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_index_crossprocess_reload.py +39 -24
- 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_issue53_cipher_health_hash.py +385 -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_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_p2_mcp.py +8 -0
- 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_stgidx_index_parity.py +1020 -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/writepipe.py +101 -2
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/zcode/AGENTS.md +5 -5
package/md_cg/mdcos.py
CHANGED
|
@@ -37,7 +37,7 @@ from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT
|
|
|
37
37
|
from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
38
38
|
identity, consistency, metacognition, crypto, sustain,
|
|
39
39
|
self_state, predict, evolution, weights, pooling,
|
|
40
|
-
writelimit, reach, trust, roleviews)
|
|
40
|
+
writelimit, reach, trust, roleviews, autonomy_modes)
|
|
41
41
|
from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
|
|
42
42
|
read_jsonl_tail, count_jsonl, publish)
|
|
43
43
|
from .security import (Principal, TenantRegistry, AccessDenied,
|
|
@@ -2012,10 +2012,11 @@ class MdCGOS(MdCG):
|
|
|
2012
2012
|
|
|
2013
2013
|
# ================= 4. 审核队列(inbox → decisions) =================
|
|
2014
2014
|
|
|
2015
|
-
# 生效条件:当 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 键(缺省不落键 = 存量条目形状逐位不变);
|
|
2016
2016
|
def propose(self, node_id: str, content: str, layer: str = "knowledge",
|
|
2017
2017
|
tags=None, condition_space=None, verify=None,
|
|
2018
|
-
info: bool = False,
|
|
2018
|
+
info: bool = False, kind: str = None, dedup_key: str = None,
|
|
2019
|
+
**kw):
|
|
2019
2020
|
"""把一个候选记忆放入海马体 inbox,等待审核(不直接持久化)。
|
|
2020
2021
|
|
|
2021
2022
|
verify —— 验收判据(内联声明,裁决阶段只读),形如:
|
|
@@ -2028,10 +2029,22 @@ class MdCGOS(MdCG):
|
|
|
2028
2029
|
不再入队。语义:重试与崩溃恢复无害——入队落盘成功而响应丢失时
|
|
2029
2030
|
(MCP 客户端超时重试的真正机制),重试对账命中既有记录并返回原 pid。
|
|
2030
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
|
+
|
|
2031
2044
|
返回值:info=False(默认)返回 pid 字符串(保形,存量调用零变化);
|
|
2032
2045
|
info=True 返回 {"pid", "dedup", "dup_of", "dup_status"}。
|
|
2033
2046
|
"""
|
|
2034
|
-
phash = _sig(content)
|
|
2047
|
+
phash = dedup_key or _sig(content)
|
|
2035
2048
|
with FileLock(self.inbox_log, strict=True):
|
|
2036
2049
|
# 增量对账(issue #32):原实现每条全量 read_jsonl(inbox) +
|
|
2037
2050
|
# read_jsonl(decisions)(锁内 O(M+D)/条、批量 O(M²),旧格式行
|
|
@@ -2074,6 +2087,11 @@ class MdCGOS(MdCG):
|
|
|
2074
2087
|
"verify": verify or {}, "verify_hash": vhash,
|
|
2075
2088
|
"extra": kw, "actor": self.actor,
|
|
2076
2089
|
"session": getattr(self, "session", None)}
|
|
2090
|
+
# 三档自治批次②(设计 §四):类型字段**只在显式给定时落键**——
|
|
2091
|
+
# 缺省不落,存量与新普通提案的 rec 形状逐位不变(零回归),
|
|
2092
|
+
# 读取方按「缺键 = proposal」判(零迁移)。
|
|
2093
|
+
if kind:
|
|
2094
|
+
rec["kind"] = str(kind)
|
|
2077
2095
|
append_jsonl(self.inbox_log, rec)
|
|
2078
2096
|
self._audit("propose", node_id, pid=pid, layer=layer,
|
|
2079
2097
|
payload_hash=phash, verify_hash=vhash)
|
|
@@ -2342,7 +2360,7 @@ class MdCGOS(MdCG):
|
|
|
2342
2360
|
audit_source="hippocampus/decisions.jsonl", **extra)
|
|
2343
2361
|
return nid
|
|
2344
2362
|
|
|
2345
|
-
# 生效条件:当 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;
|
|
2346
2364
|
def _record_decision(self, pid, item, decision, status, reason, round_no,
|
|
2347
2365
|
rt_verdict, issues, vhash, result):
|
|
2348
2366
|
"""落盘一轮裁决:jsonl(权威)+ md 审计节点(可复核、供外部审计)。"""
|
|
@@ -2355,6 +2373,13 @@ class MdCGOS(MdCG):
|
|
|
2355
2373
|
rec["record_hash"] = self._record_hash(rec)
|
|
2356
2374
|
rec["result"] = {k: v for k, v in result.items()
|
|
2357
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
|
|
2358
2383
|
# 裁决记录不能丢:strict 锁内追加(并发裁决不交错;
|
|
2359
2384
|
# 丢一条裁决会让提案回 pending → 重复落盘,比让裁决者等一下代价大)
|
|
2360
2385
|
with FileLock(self.decisions_log, strict=True):
|
|
@@ -2460,7 +2485,158 @@ class MdCGOS(MdCG):
|
|
|
2460
2485
|
"verify_hash": src.get("verify_hash"),
|
|
2461
2486
|
"source": "hippocampus/decisions.jsonl"}
|
|
2462
2487
|
|
|
2463
|
-
# 生效条件:
|
|
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。
|
|
2464
2640
|
def review_decide(self, pid: str, decision: str, edits: dict = None,
|
|
2465
2641
|
merge_into: str = None, reason: str = "",
|
|
2466
2642
|
redteam: dict = None, issues=None,
|
|
@@ -2479,6 +2655,14 @@ class MdCGOS(MdCG):
|
|
|
2479
2655
|
的写入门控(它根本不写)。此处 noop 是**裁决动作**,与 lifecycle/trust 中
|
|
2480
2656
|
同名的**状态迁移结果码**分属两层(见模块常量区注释)。
|
|
2481
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
|
+
|
|
2482
2666
|
两条验证纪律(借自任务分级协议的验证端):
|
|
2483
2667
|
1. 判据只读:verify 由 propose 声明,裁决阶段传入不同判据 → verify_readonly。
|
|
2484
2668
|
2. 红队门控 + 再审批:redteam.verdict=reject(或带 issues 的 reject)不落库,
|
|
@@ -2534,6 +2718,14 @@ class MdCGOS(MdCG):
|
|
|
2534
2718
|
return {"ok": False, "error": "redteam_required",
|
|
2535
2719
|
"detail": "MDCG_REDTEAM_REQUIRED=1:accept 必须带红队 pass"}
|
|
2536
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
|
+
|
|
2537
2729
|
result = {"pid": pid, "decision": decision, "round": round_no,
|
|
2538
2730
|
"redteam": rt_v or "absent"}
|
|
2539
2731
|
if decision == DECISION_NOOP:
|
|
@@ -2544,6 +2736,11 @@ class MdCGOS(MdCG):
|
|
|
2544
2736
|
elif decision == "reject":
|
|
2545
2737
|
result["ok"] = True
|
|
2546
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 即等价——映射不需要新增第二套实现,本分支职责与判据不变。
|
|
2547
2744
|
content = item["content"]
|
|
2548
2745
|
tags = list(item.get("tags") or [])
|
|
2549
2746
|
layer = item.get("layer") or "knowledge"
|
|
@@ -2627,6 +2824,18 @@ class MdCGOS(MdCG):
|
|
|
2627
2824
|
self.flush()
|
|
2628
2825
|
result.update(ok=True, node_id=target)
|
|
2629
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
|
+
"""
|
|
2630
2839
|
# status 映射统一出口:reject→"rejected"、noop→"noop"(同为终态,见
|
|
2631
2840
|
# TERMINAL_DECISION_STATUS)、其余→"accepted"。
|
|
2632
2841
|
result = self._record_decision(
|
|
@@ -2648,6 +2857,40 @@ class MdCGOS(MdCG):
|
|
|
2648
2857
|
self.flush()
|
|
2649
2858
|
return result
|
|
2650
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
|
+
|
|
2651
2894
|
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.decisions_log)),记录内容取决于 decisions_log 可读结果;
|
|
2652
2895
|
def decisions(self):
|
|
2653
2896
|
return list(read_jsonl(self.decisions_log))
|
|
@@ -2655,11 +2898,17 @@ class MdCGOS(MdCG):
|
|
|
2655
2898
|
# ================= 5. tombstone + 恢复时删除检查 =================
|
|
2656
2899
|
|
|
2657
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;
|
|
2658
|
-
def
|
|
2659
|
-
"""
|
|
2901
|
+
def _forget_qualify(self, node_id: str, override: bool = False):
|
|
2902
|
+
"""删除的**既有资格前置闸**(单点):索引代际感知 → not_found → 写保护。
|
|
2660
2903
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2904
|
+
为什么抽出来:三档自治(设计 §三 硬约束②「判定必须在既有资格闸通过
|
|
2905
|
+
之后」)需要在**不重复、不重排、不放宽**这三道既有判据的前提下,先让
|
|
2906
|
+
资格闸跑完再问档位。判据只此一份——`forget`(库层原路径)与
|
|
2907
|
+
`forget_gated`(工具面档位路径)共用,不各写一份必然漂移。
|
|
2908
|
+
|
|
2909
|
+
返回 (entry, None) 表示资格通过(entry = 索引条目);(None, err) 表示
|
|
2910
|
+
资格未过(err 即既有 forget 的返回体;保护闸抛出的异常照旧外抛,不在
|
|
2911
|
+
此吞掉——调用方看到的语义与改动前逐位一致)。
|
|
2663
2912
|
"""
|
|
2664
2913
|
# N195(2026-09-28):删除面与写面同源,先做索引代际感知。此前直读
|
|
2665
2914
|
# 本进程内存索引——他进程刚写入的 anchor/self 节点在本进程索引中
|
|
@@ -2668,8 +2917,28 @@ class MdCGOS(MdCG):
|
|
|
2668
2917
|
self._maybe_reload_index()
|
|
2669
2918
|
e = self.index["nodes"].get(node_id)
|
|
2670
2919
|
if not e:
|
|
2671
|
-
return {"ok": False, "error": "not_found"}
|
|
2920
|
+
return None, {"ok": False, "error": "not_found"}
|
|
2672
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` 体逐位一致。"""
|
|
2673
2942
|
src = os.path.join(self.root, e["path"])
|
|
2674
2943
|
fm, content = self._read(e)
|
|
2675
2944
|
h = _sig(content or "", 16)
|
|
@@ -2691,6 +2960,43 @@ class MdCGOS(MdCG):
|
|
|
2691
2960
|
self._audit("forget", node_id, reason=reason, payload_hash=h)
|
|
2692
2961
|
return {"ok": True, "id": node_id, "tombstone": h}
|
|
2693
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
|
+
|
|
2694
3000
|
# 生效条件:无输入形参;调用即返回 list(read_jsonl(self.deletions_log)),记录内容取决于 deletions_log 可读结果;
|
|
2695
3001
|
def deletions(self):
|
|
2696
3002
|
return list(read_jsonl(self.deletions_log))
|
|
@@ -2699,7 +3005,7 @@ class MdCGOS(MdCG):
|
|
|
2699
3005
|
def is_tombstoned(self, node_id: str):
|
|
2700
3006
|
return any(r.get("id") == node_id for r in read_jsonl(self.deletions_log))
|
|
2701
3007
|
|
|
2702
|
-
# 生效条件:当 node_id 传入时,若 deletions_log 中存在该 id 且 force=False 返回 tombstoned 拒绝;否则检查 trash_dir/{node_id}.md,os.path.exists 为 False 返回 not_in_trash
|
|
3008
|
+
# 生效条件:当 node_id 传入时,若 deletions_log 中存在该 id 且 force=False 返回 tombstoned 拒绝;否则检查 trash_dir/{node_id}.md,os.path.exists 为 False 返回 not_in_trash;文件含坏字节(非 UTF-8)无法解析时返回 error=corrupt 且 trash 源原样保留(不删不移);可打开则读取并以 add(override=True) + trash frontmatter 元数据全量透传(显式形参逐项传,其余键经 **extra 回写;lifecycle_state 仅在 active→该态合法迁移时透传)恢复、移除 trash 源、audit,返回 ok True/id/forced=bool(force);
|
|
2703
3009
|
def restore(self, node_id: str, force: bool = False):
|
|
2704
3010
|
"""恢复:若在删除清单中且未 force → 拒绝(恢复时删除检查)。"""
|
|
2705
3011
|
tomb = [r for r in read_jsonl(self.deletions_log) if r.get("id") == node_id]
|
|
@@ -2709,8 +3015,14 @@ class MdCGOS(MdCG):
|
|
|
2709
3015
|
src = os.path.join(self.trash_dir, f"{node_id}.md")
|
|
2710
3016
|
if not os.path.exists(src):
|
|
2711
3017
|
return {"ok": False, "error": "not_in_trash"}
|
|
2712
|
-
|
|
2713
|
-
|
|
3018
|
+
try:
|
|
3019
|
+
with open(src, encoding="utf-8") as f:
|
|
3020
|
+
fm, content = nodefile.loads(f.read())
|
|
3021
|
+
except UnicodeDecodeError:
|
|
3022
|
+
# trash 源损坏:返回结构化失败,源文件原样保留(不删不移)
|
|
3023
|
+
return {"ok": False, "error": "corrupt",
|
|
3024
|
+
"reason": "trash 文件非 UTF-8(损坏),恢复中止;"
|
|
3025
|
+
"源文件原样保留"}
|
|
2714
3026
|
layer = fm.get("layer", "knowledge")
|
|
2715
3027
|
# 恢复 = 原样放回:add 是**全量重建 fm**,只传个位数字段会把 edges/
|
|
2716
3028
|
# depends_on/验证态/生命周期/created_at/语义摘要/证据计数等元数据永久
|
|
@@ -3593,7 +3905,176 @@ class MdCGOS(MdCG):
|
|
|
3593
3905
|
|
|
3594
3906
|
# ---- 主动遗忘(写入侧三问闸门)+ 写保护盘点 ----
|
|
3595
3907
|
|
|
3596
|
-
#
|
|
3908
|
+
# 生效条件:以 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" 的终态(**不调用任何合并原语、不落盘**);
|
|
3909
|
+
def _autonomy_gate_merge(self, node_id, tgt, content, kw, primitive,
|
|
3910
|
+
reason="", gate=None):
|
|
3911
|
+
"""三档自治档位在 **B 合并**出口的判定单点(设计 §三:变更确认档需确认)。
|
|
3912
|
+
|
|
3913
|
+
为什么是单点:合并的**两个落点**(`writelimit.check` 的 CONVERGE →
|
|
3914
|
+
`converge_into`、`forgetting.assess` 的 MERGE → `reinforce`)都必须
|
|
3915
|
+
过同一判据——两处各写一份必然漂移;且判定必须晚于**各自的裁决**
|
|
3916
|
+
(限流闸 / 遗忘三问),档位不参与裁决(纯加严)。
|
|
3917
|
+
|
|
3918
|
+
变更单载荷:动作类 B · 目标 tgt · 后像 = 本次新内容 · 理由 · 复现参数
|
|
3919
|
+
(override/session——merge 原语的两个非默认槽,裁决 accept 时按此重放)。
|
|
3920
|
+
提议者/会话由 propose 的库端快照自动归属(`actor`/`session` 字段)。
|
|
3921
|
+
"""
|
|
3922
|
+
from . import autonomy_modes as _am
|
|
3923
|
+
# 资格在先(纯加严):合并落盘时 reinforce/converge_into 内部的
|
|
3924
|
+
# guard_overwrite 若会拒(受保护/越权),必须在**出单之前**照样拒——
|
|
3925
|
+
# 否则一次本该报错的合并会变成「静默入队」。
|
|
3926
|
+
self.write_qualify(tgt, override=bool(kw.get("override")),
|
|
3927
|
+
actor=self.actor)
|
|
3928
|
+
dec = _am.decide(_am.B_MERGE)
|
|
3929
|
+
if dec["decision"] == _am.ALLOW:
|
|
3930
|
+
return None
|
|
3931
|
+
aut = {"mode": dec["mode"], "action": dec["action"],
|
|
3932
|
+
"action_name": dec["action_name"], "decision": dec["decision"]}
|
|
3933
|
+
if dec["decision"] == _am.FORBID:
|
|
3934
|
+
# 形态对齐原 MERGE 出口(gate / merged_into 两键照带):调用方按
|
|
3935
|
+
# 「gate.reason」取值读判断依据的既有读法不因本闸断裂。
|
|
3936
|
+
return {"verdict": "FORBID", "node_id": node_id, "gate": gate,
|
|
3937
|
+
"merged_into": tgt, "moved_to": "autonomy_forbidden",
|
|
3938
|
+
"autonomy": aut, "hint": dec["hint"]}
|
|
3939
|
+
# 队列的层闸(MdCGSecure.propose)按**目标节点的真层**校验:合并落点
|
|
3940
|
+
# 本来就是目标层,用调用方声明的层会让闸与落点错位(issue50 前车:
|
|
3941
|
+
# 校验层与落盘层错位会造死提案)。
|
|
3942
|
+
tlayer = ((self.index.get("nodes") or {}).get(tgt) or {}).get("layer") \
|
|
3943
|
+
or ((self.get(tgt) or {}).get("frontmatter") or {}).get("layer") \
|
|
3944
|
+
or "contextual"
|
|
3945
|
+
pay = _am.mutation_payload(
|
|
3946
|
+
_am.B_MERGE, tgt, after=content,
|
|
3947
|
+
reason=reason or ("档位确认(变更确认档):新写入内容并入既有节点 %s"
|
|
3948
|
+
"(原语 %s)" % (tgt, primitive)),
|
|
3949
|
+
primitive=primitive,
|
|
3950
|
+
meta={"override": bool(kw.get("override")),
|
|
3951
|
+
"session": _writer_session(self, kw)})
|
|
3952
|
+
pr = _am.propose_mutation(self, _am.B_MERGE, tgt, payload=pay,
|
|
3953
|
+
layer=tlayer,
|
|
3954
|
+
sensitivity=kw.get("sensitivity"), info=True)
|
|
3955
|
+
return {"verdict": "CONFIRM", "node_id": node_id, "gate": gate,
|
|
3956
|
+
"merged_into": tgt, "moved_to": "review_queue",
|
|
3957
|
+
"pid": pr["pid"], "mutation": pay, "autonomy": aut,
|
|
3958
|
+
"hint": "%s 本次未合并:变更单已入审核队列(pid=%s,目标 %s,"
|
|
3959
|
+
"新内容随单)。" % (dec["hint"], pr["pid"], tgt)}
|
|
3960
|
+
|
|
3961
|
+
# 生效条件:以 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(计划外零变更);
|
|
3962
|
+
def _autonomy_gate_rewrite(self, node_id, content, layer, kw,
|
|
3963
|
+
override=False, reason="", gate=None):
|
|
3964
|
+
"""三档自治档位在 **gated 面「C 改写 / A 新增」**出口的判定单点。
|
|
3965
|
+
|
|
3966
|
+
为什么要它(补强批次 2026-10-02,复核发现 1/2):`gated=true` 的写入
|
|
3967
|
+
在上游 `writepipe._gate_gated` 是**替代执行路径**(`remember_gated`
|
|
3968
|
+
自行落盘后直接返回终态,链尾 `_gate_autonomy` 根本不执行)——而
|
|
3969
|
+
`remember_gated` 的 ACCEPT 分支此前**无任何档位判据**、直调 `self.add`:
|
|
3970
|
+
缺省(confirm)档对**既有 node_id 的覆写**直接落盘(不出变更单)、
|
|
3971
|
+
计划档对**新 node_id** 也照样写入。二者都与设计 §三(C=需确认、
|
|
3972
|
+
计划外零变更)相抵。本方法是该面**唯一**的档位判定点(与写链
|
|
3973
|
+
`writepipe._gate_autonomy`、合并面 `_autonomy_gate_merge` 同口径)。
|
|
3974
|
+
|
|
3975
|
+
位置(纯加严,设计 §三 硬约束②):遗忘闸四态裁决(`forgetting.assess`)
|
|
3976
|
+
之后、`self.add` 落盘之前;档位**不参与**裁决、也不放宽任何既有判据。
|
|
3977
|
+
|
|
3978
|
+
动作类判定:`forgetting.prior_node(self, node_id)` 同 id 存在 ⇒
|
|
3979
|
+
**C 改写**;不存在 ⇒ **A 新增**——判据复用同一单点,不另写一份存在性判据。
|
|
3980
|
+
|
|
3981
|
+
资格在先:既有节点出单**之前**先 `self.write_qualify`(与 add 内部同一批
|
|
3982
|
+
protect 单点,只判不写)——受保护/越权覆写照旧当场 `ProtectionError`/
|
|
3983
|
+
`AccessDenied`,不会变成「静默入队」(放宽既有判据)。
|
|
3984
|
+
|
|
3985
|
+
载荷 meta(复现原动作):取**写链同一份白名单** `writepipe.
|
|
3986
|
+
_AUTONOMY_META_KEYS`(延迟导入,单一真源),并补 gated 面独有的 fm 扩展键
|
|
3987
|
+
`role`——直写路径 `self.add(..., **kw)` 会把 role 经 add 的 `**extra`
|
|
3988
|
+
落 fm,重放不带就会「出单→确认→写」与直写两条路径 fm 不等价。
|
|
3989
|
+
边界(如实):`kw` 里的 `session` 不在本白名单内、且执行桥会把它
|
|
3990
|
+
收进 B 原语的 session 槽(C 面不复现)——当前两个 gated 调用点
|
|
3991
|
+
(`writepipe._gate_gated` / `mcp_server.mdcg_remember`)都不传 session。
|
|
3992
|
+
|
|
3993
|
+
返回形态:出单 = `ok=True/committed=False/moved_to="review_queue"`
|
|
3994
|
+
且带 pid(口径同写链 `_gate_autonomy`);FORBID = `ok=False/
|
|
3995
|
+
moved_to="autonomy_forbidden"`(口径同写链 A 面);ALLOW = `None`
|
|
3996
|
+
(调用方原路落盘,行为逐位不变)。
|
|
3997
|
+
"""
|
|
3998
|
+
from . import autonomy_modes as _am
|
|
3999
|
+
# 载荷 meta 白名单的单一真源在写链(写面唯一定义处);延迟导入防循环。
|
|
4000
|
+
from . import writepipe as _wp
|
|
4001
|
+
prior = forgetting.prior_node(self, node_id)
|
|
4002
|
+
action = _am.C_REWRITE if prior is not None else _am.A_ADD
|
|
4003
|
+
if prior is not None:
|
|
4004
|
+
# 资格在先(纯加严):同 `_autonomy_gate_merge` / `writepipe.
|
|
4005
|
+
# _gate_autonomy`——覆写的层闸/写保护闸住在 `self.add` 内部,档位
|
|
4006
|
+
# 先出单会把一次本该报错的覆写变成「静默入队」(放宽既有判据)。
|
|
4007
|
+
self.write_qualify(node_id, target_layer=layer,
|
|
4008
|
+
override=bool(override), actor=self.actor)
|
|
4009
|
+
dec = _am.decide(action)
|
|
4010
|
+
if dec["decision"] == _am.ALLOW:
|
|
4011
|
+
return None
|
|
4012
|
+
aut = {"mode": dec["mode"], "action": dec["action"],
|
|
4013
|
+
"action_name": dec["action_name"], "decision": dec["decision"]}
|
|
4014
|
+
if dec["decision"] == _am.FORBID:
|
|
4015
|
+
return {"ok": False, "verdict": "FORBID", "node_id": node_id,
|
|
4016
|
+
"committed": False, "gate": gate,
|
|
4017
|
+
"moved_to": "autonomy_forbidden", "error": "autonomy_forbid",
|
|
4018
|
+
"autonomy": aut, "hint": dec["hint"]}
|
|
4019
|
+
meta = {k: kw[k] for k in _wp._AUTONOMY_META_KEYS
|
|
4020
|
+
if kw.get(k) is not None}
|
|
4021
|
+
if kw.get("role") is not None:
|
|
4022
|
+
meta["role"] = kw["role"]
|
|
4023
|
+
if override:
|
|
4024
|
+
meta["override"] = True
|
|
4025
|
+
pay = _am.mutation_payload(
|
|
4026
|
+
action, node_id, after=content,
|
|
4027
|
+
reason=reason or ("gated 面覆写(gated=true / remember_gated 对既有"
|
|
4028
|
+
"节点 %s 的 %s)" % (node_id, _am.ACTION_NAMES[action])),
|
|
4029
|
+
primitive="add", meta=meta)
|
|
4030
|
+
pr = _am.propose_mutation(self, action, node_id, payload=pay,
|
|
4031
|
+
layer=layer,
|
|
4032
|
+
sensitivity=kw.get("sensitivity"), info=True)
|
|
4033
|
+
return {"ok": True, "verdict": "CONFIRM", "node_id": node_id,
|
|
4034
|
+
"committed": False, "gate": gate, "moved_to": "review_queue",
|
|
4035
|
+
"pid": pr["pid"], "mutation": pay, "autonomy": aut,
|
|
4036
|
+
"hint": "%s 本次未落盘:变更单已入审核队列(pid=%s,目标 %s)。"
|
|
4037
|
+
% (dec["hint"], pr["pid"], node_id)}
|
|
4038
|
+
|
|
4039
|
+
# 生效条件:node_id 不在索引中(新节点)时返回 None(新写入面没有「既有资格闸」,其层闸由落盘面把守);在索引中时以 override=False 跑 protect.guard_overwrite(既有节点覆写前的统一双闸:principal 层闸按**既有节点真层** + 引擎级写保护闸)与(target_layer 非空且与真层不同时)protect.guard_move(受保护节点移出保护层的降级闸),任一拒绝抛出的 AccessDenied 一律照抛、ProtectionError 仅在 override 为假时照抛(override 为真时放行——真实写入那一次会带授权并写快照,本探针**不写快照**);全部通过返回 None;本函数不落盘、不改索引、不写任何快照;
|
|
4040
|
+
def write_qualify(self, node_id, target_layer=None, override=False,
|
|
4041
|
+
actor=None):
|
|
4042
|
+
"""既有节点写面的**资格探针**(三档自治:档位判定必须晚于既有资格闸)。
|
|
4043
|
+
|
|
4044
|
+
为什么要它(纯加严,设计 §三 硬约束②):写面落盘时的两道资格闸
|
|
4045
|
+
——`protect.guard_overwrite`(层闸 + 写保护闸)与 `protect.guard_move`
|
|
4046
|
+
(降级闸)——住在 `MdCG.add` **内部**。若档位闸在 add 之前就判「出变更
|
|
4047
|
+
单」,一次本该被拒的越权/受保护覆写就从 `AccessDenied`/`ProtectionError`
|
|
4048
|
+
变成「静默入队」——那是**放宽既有判据**(未授权者凭空获得一个排单出口)。
|
|
4049
|
+
故档位判定前先用**同一批单点**跑一遍资格:判据只有一份(protect 的那些
|
|
4050
|
+
单点),这里只负责按 add 的次序与范围调用它们。
|
|
4051
|
+
|
|
4052
|
+
无副作用边界(如实):override=False 时 guard_write/guard_move 只判不写;
|
|
4053
|
+
override=True 的情形**不由本探针**产生快照——快照归真实写入那一次
|
|
4054
|
+
(`_allow` → `_protected_history`)。故本探针不得被当作「授权检查」以外的
|
|
4055
|
+
用途(它不建目录、不落文件、不动索引)。
|
|
4056
|
+
"""
|
|
4057
|
+
e = (self.index.get("nodes") or {}).get(node_id)
|
|
4058
|
+
if not e:
|
|
4059
|
+
return None
|
|
4060
|
+
if target_layer and str(e.get("layer") or "") \
|
|
4061
|
+
and str(e.get("layer")) != str(target_layer):
|
|
4062
|
+
try:
|
|
4063
|
+
protect.guard_move(self, node_id, target_layer, override=False,
|
|
4064
|
+
actor=actor)
|
|
4065
|
+
except protect.ProtectionError:
|
|
4066
|
+
if not override:
|
|
4067
|
+
raise
|
|
4068
|
+
try:
|
|
4069
|
+
# 层闸按**既有节点真层**解析(layer=None ⇒ protect._resolve_target
|
|
4070
|
+
# 取索引/ fm 的真层)——与 add 的 N221 跨层覆写同口径(源层在先)。
|
|
4071
|
+
protect.guard_overwrite(self, node_id, override=False, actor=actor)
|
|
4072
|
+
except protect.ProtectionError:
|
|
4073
|
+
if not override:
|
|
4074
|
+
raise
|
|
4075
|
+
return None
|
|
4076
|
+
|
|
4077
|
+
# 生效条件: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 留痕)。
|
|
3597
4078
|
def remember_gated(self, node_id, content, layer="contextual", **kw):
|
|
3598
4079
|
"""写入情景层记忆前的**主动遗忘闸门**:三问 → 四态。
|
|
3599
4080
|
|
|
@@ -3605,6 +4086,34 @@ class MdCGOS(MdCG):
|
|
|
3605
4086
|
DROP 丢弃(低熵噪音)/ DEFER 待定(不写)。
|
|
3606
4087
|
四种结果都写进 `_forgetting.jsonl`,可审计。
|
|
3607
4088
|
|
|
4089
|
+
issue50-b(2026-10-01):DEFER 此前**只留一行痕**——不落盘、无收件箱。
|
|
4090
|
+
issue50-a 把半重复判 DEFER 后,这条出口的语义(「待定**复核**」)与
|
|
4091
|
+
实际(静默不记)分叉:噪声缺陷会换成「不落盘且无收件箱」缺陷。故
|
|
4092
|
+
DEFER 出口接线**既有入队单点** `self.propose`(与审计/策略闸
|
|
4093
|
+
`writepipe._gate_audit`、一致性闸 `_gate_consistency` 同一条链、同一
|
|
4094
|
+
payload_hash 幂等对账):正文随提案进审核队列,裁决 accept 即落盘;
|
|
4095
|
+
「为何待定」写进提案记录的 `extra.defer_reason`(propose 的既有任意槽,
|
|
4096
|
+
不新增队列字段/队列类型)。
|
|
4097
|
+
|
|
4098
|
+
issue50-c F1(2026-10-01,扩面修):入队**只带 content/layer/sensitivity**,
|
|
4099
|
+
调用方声明的其余 meta 全丢——accept 只能从队列记录取值,故「同一声明」下
|
|
4100
|
+
DEFER→accept 落盘的节点比直接 ACCEPT 少 tags/condition_space/
|
|
4101
|
+
verification_basis/non_applicable_conditions/derived_from/relation/role
|
|
4102
|
+
(实测 tags=[] 而对面有值、derived_from 缺失、bucket_zh 因 tags 空而不生成)。
|
|
4103
|
+
处置:入队时把本闸收到的 meta 一并落进队列记录(`**kw` → `self.propose`,
|
|
4104
|
+
口径与直接 ACCEPT 的 `add(**kw)` 同源),accept 分支既有的
|
|
4105
|
+
`tags=/condition_space=/**extra` 映射即把它写回节点 fm——**两条路径元数据
|
|
4106
|
+
等价**,且 review_decide 无需改动。
|
|
4107
|
+
|
|
4108
|
+
issue50-c F2(2026-10-01):`node_id=None` 时 propose 造 pid 的
|
|
4109
|
+
`node_id + str(…)` 抛 TypeError;按仓内既有形态(`forgetting._prefeed_id`)
|
|
4110
|
+
兜底成内容派生 id,不崩且非静默(返回体 `proposed_id_fallback`)。
|
|
4111
|
+
|
|
4112
|
+
接线只覆盖**本闸自己**的 DEFER——
|
|
4113
|
+
writelimit 限流 DEFER 在上方已 return(其语义是「先别写」,原文在
|
|
4114
|
+
recent 时间线,不是内容待定),gated=False 旁路与 ACCEPT 后因
|
|
4115
|
+
on_conflict=defer 降级的 DEFER(`out["conflict"]`)都不在此列。
|
|
4116
|
+
|
|
3608
4117
|
B3(2026-09-30):MERGE 分支此前**不把 content 往下传**(`reinforce`
|
|
3609
4118
|
也没有该槽)——近重复的第二次写入,其新正文 100% 丢弃,只有既有节点
|
|
3610
4119
|
被强化;现传 content + session,新正文按目标缺的行追加为聚合行落盘,
|
|
@@ -3612,6 +4121,26 @@ class MdCGOS(MdCG):
|
|
|
3612
4121
|
来源,`assess` :214 先于 MERGE)把新正文全文与去向记进 `_forgetting.jsonl`
|
|
3613
4122
|
并返回 `dropped` 去向单(trace_id/sha1/检索入口)。两条分支都有处置。
|
|
3614
4123
|
H3:合并时把写入方会话归属并列落目标 fm(`fm.merge_sources`)。
|
|
4124
|
+
|
|
4125
|
+
issue50-d(2026-10-02):ACCEPT 分支**落盘=裁决值**——此前「hint 非
|
|
4126
|
+
None 才透传」的口径把启发式裁决值(0.84/0.71 量级,隔离沙箱实证)
|
|
4127
|
+
全部丢在闸门上,落盘一律默认 0.5;现无条件落 `imp["score"]` 并按
|
|
4128
|
+
`imp["from"]` 落 `importance_source`("hint"|"heuristic",进 fm 与
|
|
4129
|
+
索引条目)。`add` 的自动保护位只认显式声明:heuristic 过线只落分、
|
|
4130
|
+
不打保护位——由此审计面文案(assess 保护分支按 from 分叉)、落盘分、
|
|
4131
|
+
保护位三者首次同源一致。外部基准(hive-memory-bench v1.0)测得 L3
|
|
4132
|
+
使用端损耗 3-4/19:本批让重要度轴带电(排序次级键从此拿到真分)。
|
|
4133
|
+
|
|
4134
|
+
补强批次(2026-10-02,复核发现 1/2 的收口):ACCEPT 分支此前**无任何
|
|
4135
|
+
档位判据**、直调 `self.add`——`gated=true`(插件自动记忆主写入通道)
|
|
4136
|
+
在缺省 confirm 档对既有 node_id 直接覆写(不出变更单)、在计划档对新
|
|
4137
|
+
node_id 也照样写入。现该分支落盘前先经 `_autonomy_gate_rewrite`
|
|
4138
|
+
(gated 面档位判定单点,与写链 `writepipe._gate_autonomy` 同口径、
|
|
4139
|
+
同一存在性单点 `forgetting.prior_node`):C 改写 confirm/plan 出单、
|
|
4140
|
+
full 放行;A 新增 confirm/full 放行(行为逐位不变)、plan fail-closed。
|
|
4141
|
+
出单前先跑 `write_qualify` 资格探针——受保护/越权覆写照旧当场拒,
|
|
4142
|
+
不因「改为入队」而放宽既有判据(纯加严)。出单/拒绝都写一行
|
|
4143
|
+
`_forgetting.jsonl`(verdict=CONFIRM/FORBID + hint),可审计。
|
|
3615
4144
|
"""
|
|
3616
4145
|
role = kw.get("role")
|
|
3617
4146
|
vb = kw.get("verification_basis")
|
|
@@ -3643,6 +4172,18 @@ class MdCGOS(MdCG):
|
|
|
3643
4172
|
if lim is not None:
|
|
3644
4173
|
if lim["verdict"] == "CONVERGE":
|
|
3645
4174
|
tgt = lim["target"]
|
|
4175
|
+
# 三档自治批次②:档位判定在**限流闸裁决之后、合并原语之前**。
|
|
4176
|
+
_ag = self._autonomy_gate_merge(node_id, tgt, content, kw,
|
|
4177
|
+
primitive="converge_into",
|
|
4178
|
+
gate=lim)
|
|
4179
|
+
if _ag is not None:
|
|
4180
|
+
forgetting.log(self, {"t": time.time(), "node_id": node_id,
|
|
4181
|
+
"layer": layer,
|
|
4182
|
+
"verdict": _ag["verdict"],
|
|
4183
|
+
"reason": _ag["hint"],
|
|
4184
|
+
"limiter": lim.get("limiter"),
|
|
4185
|
+
"actor": self.actor})
|
|
4186
|
+
return _ag
|
|
3646
4187
|
out = {"verdict": "MERGE", "node_id": node_id,
|
|
3647
4188
|
"merged_into": tgt, "gate": lim,
|
|
3648
4189
|
"converged": writelimit.converge_into(
|
|
@@ -3670,8 +4211,39 @@ class MdCGOS(MdCG):
|
|
|
3670
4211
|
v = verdict["verdict"]
|
|
3671
4212
|
out = {"verdict": v, "node_id": node_id, "gate": verdict}
|
|
3672
4213
|
if v == "ACCEPT":
|
|
3673
|
-
|
|
3674
|
-
|
|
4214
|
+
# issue50-d(2026-10-02):**落盘=裁决值**。此前「hint 非 None 才
|
|
4215
|
+
# 透传」的口径把启发式裁决值全部丢在闸门上——隔离沙箱实证:
|
|
4216
|
+
# u_heur_new 裁决 0.837/落盘 0.5、t_heur 裁决 0.711/落盘 0.5,落盘
|
|
4217
|
+
# 重要度不携带任何区分信息(启发式全 0.5、插件 hint 通道全 0.6),
|
|
4218
|
+
# 排序/使用端拿不到真分。现无条件落 `imp["score"]`(hint 或启发式,
|
|
4219
|
+
# 均为 assess 实际使用的裁决值,含 0~1 裁剪),并按裁决来源落
|
|
4220
|
+
# `importance_source`("hint"|"heuristic")进 fm 与索引条目——
|
|
4221
|
+
# add 的自动保护位据此只认显式声明(heuristic 过线只落分、不打
|
|
4222
|
+
# 保护位,与裁决②一致;writelimit 的保护豁免仍读原始 hint,不受
|
|
4223
|
+
# 本批影响)。
|
|
4224
|
+
kw["importance"] = verdict["importance"]["score"]
|
|
4225
|
+
kw["importance_source"] = verdict["importance"]["from"]
|
|
4226
|
+
# 三档自治批次②补强(2026-10-02):**gated 面的 C 改写 / A 新增档位
|
|
4227
|
+
# 判定**。此前本分支直调 `self.add`——`gated=true` 的覆写在确认档
|
|
4228
|
+
# 不落任何档位判定就覆写既有节点(复核发现 1:插件自动记忆的主写入
|
|
4229
|
+
# 通道可旁路确认档),计划档的新增也不被拦(发现 2)。判定与写链
|
|
4230
|
+
# `writepipe._gate_autonomy` **同口径**、同一存在性单点
|
|
4231
|
+
# (`forgetting.prior_node`):既有 node_id ⇒ C 改写(confirm/plan
|
|
4232
|
+
# 出单、full 放行)、不存在 ⇒ A 新增(confirm/full 放行、plan
|
|
4233
|
+
# fail-closed);出单前先跑 write_qualify 资格探针(受保护/越权覆写
|
|
4234
|
+
# 照旧当场拒,不静默入队)。位置在 importance 落盘值写入之后,故
|
|
4235
|
+
# 变更单载荷带的是**裁决值**(issue50-d 同源口径)。
|
|
4236
|
+
_ag = self._autonomy_gate_rewrite(node_id, content, layer, kw,
|
|
4237
|
+
override=override, gate=verdict)
|
|
4238
|
+
if _ag is not None:
|
|
4239
|
+
forgetting.log(self, {"t": time.time(), "node_id": node_id,
|
|
4240
|
+
"layer": layer,
|
|
4241
|
+
"verdict": _ag["verdict"],
|
|
4242
|
+
"reason": _ag["hint"],
|
|
4243
|
+
"importance": verdict["importance"],
|
|
4244
|
+
"entropy": verdict["entropy"],
|
|
4245
|
+
"actor": self.actor})
|
|
4246
|
+
return _ag
|
|
3675
4247
|
try:
|
|
3676
4248
|
out["written"] = self.add(node_id, content, layer=layer,
|
|
3677
4249
|
override=override,
|
|
@@ -3689,6 +4261,24 @@ class MdCGOS(MdCG):
|
|
|
3689
4261
|
writelimit.record_accepted(self, node_id, content)
|
|
3690
4262
|
elif v == "MERGE":
|
|
3691
4263
|
tgt = verdict["redundancy"]["with"]
|
|
4264
|
+
# 三档自治批次②:档位判定在**遗忘闸四态裁决之后、合并原语之前**
|
|
4265
|
+
# (纯加严——裁决仍归 forgetting.assess,档位只决定「立即合并」还是
|
|
4266
|
+
# 「出变更单」)。合并对象为空(tgt 假值)时不上报变更单:原路径
|
|
4267
|
+
# 的 reinforce 也只在 tgt 为真时执行,档位不得把「无事发生」变成
|
|
4268
|
+
# 「一张凭空指向空目标的单」。
|
|
4269
|
+
if tgt:
|
|
4270
|
+
_ag = self._autonomy_gate_merge(node_id, tgt, content, kw,
|
|
4271
|
+
primitive="reinforce",
|
|
4272
|
+
gate=verdict)
|
|
4273
|
+
if _ag is not None:
|
|
4274
|
+
forgetting.log(self, {"t": time.time(), "node_id": node_id,
|
|
4275
|
+
"layer": layer,
|
|
4276
|
+
"verdict": _ag["verdict"],
|
|
4277
|
+
"reason": _ag["hint"],
|
|
4278
|
+
"importance": verdict["importance"],
|
|
4279
|
+
"entropy": verdict["entropy"],
|
|
4280
|
+
"actor": self.actor})
|
|
4281
|
+
return _ag
|
|
3692
4282
|
out["merged_into"] = tgt
|
|
3693
4283
|
# B3 ②(2026-09-30):把**新正文**与写入方归属一并传下去。此前只传
|
|
3694
4284
|
# tgt——调用方手上正握着 content 与 node_id 却不往下传,reinforce 写回
|
|
@@ -3712,6 +4302,56 @@ class MdCGOS(MdCG):
|
|
|
3712
4302
|
self, content, node_id=node_id, target=tgt, verdict=verdict,
|
|
3713
4303
|
layer=layer, sensitivity=kw.get("sensitivity"),
|
|
3714
4304
|
session=_writer_session(self, kw), actor=self.actor)
|
|
4305
|
+
elif v == "DEFER":
|
|
4306
|
+
# issue50-b(2026-10-01):遗忘闸门的 DEFER 此前只写一行
|
|
4307
|
+
# `_forgetting.jsonl` 留痕——**不落盘且无收件箱**(对比 DROP 分支
|
|
4308
|
+
# 有 `forgetting.record_drop` 留全文 + trace_id)。第①条(issue50-a)
|
|
4309
|
+
# 把半重复从「落成近似重复节点」改成 DEFER 之后,这条出口就会把
|
|
4310
|
+
# 噪声缺陷换成「静默不记」缺陷。处置:接线**既有入队单点**
|
|
4311
|
+
# `self.propose`——与审计/策略闸(`writepipe._gate_audit`)、一致性闸
|
|
4312
|
+
# (`_gate_consistency`)**同一条入队链**(不新增第二套入队实现),
|
|
4313
|
+
# 复用其 payload_hash 幂等对账(同内容重复写入不长第二条)。
|
|
4314
|
+
# 只覆盖**本闸自己的** DEFER——writelimit 限流 DEFER 在上方已 return
|
|
4315
|
+
# (语义是「先别写」,原文在 recent 时间线,不是内容待定);
|
|
4316
|
+
# gated=False 旁路与 ACCEPT 后 on_conflict=defer 的降级 DEFER 都不走
|
|
4317
|
+
# 本分支(后者发生在 `if v == "ACCEPT"` 内,v 就地改写)。
|
|
4318
|
+
# 「为何待定」写进提案的既有任意槽 extra(不新增队列字段/队列类型,
|
|
4319
|
+
# 见 propose 的 `**kw → extra`);密级随 sensitivity 一并透传,避免
|
|
4320
|
+
# 声明 private 在此静默降级 internal(B2 同一漏传族)。
|
|
4321
|
+
#
|
|
4322
|
+
# issue50-c F1(2026-10-01,扩面修):此前入队只带 content/layer/
|
|
4323
|
+
# sensitivity,**调用方声明的其余 meta 全丢**(tags/condition_space/
|
|
4324
|
+
# verification_basis/non_applicable_conditions/derived_from/relation/
|
|
4325
|
+
# role…)。accept 只能从队列记录取值,于是「同一声明」下
|
|
4326
|
+
# DEFER→accept 落盘的节点比「直接 ACCEPT」少掉这些键(实测 tags=[]
|
|
4327
|
+
# 而对面有值、derived_from 缺失、role 缺失、bucket_zh 因 tags 空而
|
|
4328
|
+
# 不生成)——同一份声明的两条落地路径元数据不等价。处置:把本闸收到
|
|
4329
|
+
# 的那组 meta **随入队一并落进队列记录**(`**kw` 交给 `self.propose`:
|
|
4330
|
+
# 其形参 `tags`/`condition_space`/`sensitivity`/`verify` 落 rec 专属槽,
|
|
4331
|
+
# 其余键落 `extra`——与直接 ACCEPT 走 `add(**kw)` 的口径同源,不新增
|
|
4332
|
+
# 队列字段/队列类型);accept 分支早已把 `rec.tags`/`rec.condition_space`
|
|
4333
|
+
# /`**extra` 映射回 `add`(mdcos.py:2573),故无需改 review_decide。
|
|
4334
|
+
#
|
|
4335
|
+
# issue50-c F2(2026-10-01):`node_id=None` 时上文 `**kw` 透传依旧,
|
|
4336
|
+
# 但 propose 造 pid 用 `node_id + str(time.time()) + …`——None + str
|
|
4337
|
+
# 抛 TypeError。本闸的 ACCEPT/MERGE/DROP 三个出口都会经 `add` 对
|
|
4338
|
+
# 非字符串 node_id 抛 ValueError(fail-closed 既有行为),唯独 DEFER
|
|
4339
|
+
# 出口的入队单点会以 TypeError 崩出。处置:按仓内既有形态兜底成
|
|
4340
|
+
# **内容派生 id**(`forgetting._prefeed_id` 同款——forgetting.py:879
|
|
4341
|
+
# 对「无 id 候选」即用 `pre_<sha1[:12]>`,同一语义的单点复用),
|
|
4342
|
+
# 使「DEFER 有收件箱」在缺 id 时同样成立、不崩;兜底事实写进返回体
|
|
4343
|
+
# `proposed_id_fallback`(非静默——调用方可判、可检索),**不进
|
|
4344
|
+
# forgetting 留痕行**(该行形状由 issue50-b 冻结)。
|
|
4345
|
+
_why = "遗忘闸门:" + str(verdict.get("reason") or "待定复核")
|
|
4346
|
+
_dup_with = (verdict.get("redundancy") or {}).get("with")
|
|
4347
|
+
if _dup_with:
|
|
4348
|
+
_why += "(重复对象 %s)" % _dup_with
|
|
4349
|
+
_nid = node_id if isinstance(node_id, str) and node_id else \
|
|
4350
|
+
forgetting._prefeed_id(content)
|
|
4351
|
+
out["proposed"] = self.propose(
|
|
4352
|
+
_nid, content, layer=layer, **kw, defer_reason=_why)
|
|
4353
|
+
if _nid != node_id:
|
|
4354
|
+
out["proposed_id_fallback"] = _nid
|
|
3715
4355
|
# DROP 的去向留痕已由 record_drop 单点落盘(含全文/trace_id),此处只补
|
|
3716
4356
|
# ACCEPT/MERGE/DEFER 的裁决留痕,避免同一次裁决出现两行。
|
|
3717
4357
|
if v != "DROP":
|
|
@@ -4554,11 +5194,27 @@ class MdCGSecure(MdCGOS):
|
|
|
4554
5194
|
|
|
4555
5195
|
# ---------- 管理:需 can_admin ----------
|
|
4556
5196
|
|
|
4557
|
-
#
|
|
4558
|
-
def
|
|
5197
|
+
# 生效条件:principal 在位时调用 principal.require_admin("forget")(越权抛 AccessDenied),返回 None;
|
|
5198
|
+
def _forget_admin(self):
|
|
5199
|
+
"""删除面的**管理权限闸单点**(三档自治批次②:两个入口共用一份口径)。
|
|
5200
|
+
|
|
5201
|
+
为什么抽出来:`forget`(既有)与 `forget_gated`(档位路径)都必须先过
|
|
5202
|
+
这道闸——`MdCGSecure` 的 can_admin 要求原先只写在 `forget` 里,档位路径
|
|
5203
|
+
若直调 `_forget_apply` 就把它整个跳过了(实测:`MDCG_CAN_ADMIN=0` 的身份
|
|
5204
|
+
经 `mdcg_forget` 能把节点删掉)。判据只此一份,两个入口都走它。
|
|
5205
|
+
"""
|
|
4559
5206
|
self.principal.require_admin("forget")
|
|
5207
|
+
|
|
5208
|
+
# 生效条件:先 _forget_admin(),再转 super().forget(node_id, reason, override=override);
|
|
5209
|
+
def forget(self, node_id: str, reason: str = "", override: bool = False):
|
|
5210
|
+
self._forget_admin()
|
|
4560
5211
|
return super().forget(node_id, reason, override=override)
|
|
4561
5212
|
|
|
5213
|
+
# 生效条件:先 _forget_admin()(与 forget 同序同错型——管理闸在资格闸与档位判定**之前**),再转 super().forget_gated(node_id, reason, override=override);三档自治批次② 的 D 删除工具面路径由此把 can_admin 要求带进档位路径;
|
|
5214
|
+
def forget_gated(self, node_id: str, reason: str = "", override: bool = False):
|
|
5215
|
+
self._forget_admin()
|
|
5216
|
+
return super().forget_gated(node_id, reason, override=override)
|
|
5217
|
+
|
|
4562
5218
|
# 生效条件:先 principal.require_admin("restore"),再转 super().restore(node_id, force=force)。
|
|
4563
5219
|
def restore(self, node_id: str, force: bool = False):
|
|
4564
5220
|
self.principal.require_admin("restore")
|