@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
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""三档自治**档位单一入口**(设计 v0.2 §三/§四/§五 落码 · 批次②)。
|
|
3
|
+
|
|
4
|
+
为什么另立本模块:设计 §三 的硬约束①「**单一入口**」——档位是**一个** env 键
|
|
5
|
+
(`MDCG_AUTONOMY=plan|confirm|full`,缺省 `confirm`),由**一处**判据函数读取
|
|
6
|
+
与解释;不得各模块各读一份。形态照抄 `md_cg/sleep.py:84-170` 的 §4.7「env 表
|
|
7
|
+
单一真源」纪律:**改缺省只改本表**,所有读取点只经 `autonomy_env()` /
|
|
8
|
+
`mode()`,不得再写第二处字面量(守卫 `test_autonomy_modes.py` 把这条钉死:
|
|
9
|
+
全仓 `.py` 里带引号的 `MDCG_AUTONOMY` 只许出现在本模块的 `AUTONOMY_ENV_KEYS`)。
|
|
10
|
+
|
|
11
|
+
命名避坑:`md_cg/autonomy.py` **已被占用**(信息差驱动探索闭环,
|
|
12
|
+
`mdcos.py:3415` 的 `insight(act=explore)` 调用它)——本模块是另一个东西,
|
|
13
|
+
故用 `autonomy_modes.py`,且**不**复用 `autonomy` 的任何符号。
|
|
14
|
+
|
|
15
|
+
档位语义(设计 §三 裁决矩阵,逐格照抄):
|
|
16
|
+
|
|
17
|
+
动作 计划模式 变更确认(缺省) 完全访问
|
|
18
|
+
A 新增 允许(须计划步骤) 允许 允许
|
|
19
|
+
B 合并 允许(须计划步骤) **需确认** 允许
|
|
20
|
+
C 改写 **需确认** **需确认** 允许
|
|
21
|
+
D 删除 **需确认** **需确认** 允许
|
|
22
|
+
E 权重/生命周期 允许(须计划步骤) 允许 允许
|
|
23
|
+
|
|
24
|
+
判定函数 `decide(action_class)` 返回 `allow|confirm|forbid`(+可读 hint):
|
|
25
|
+
「允许=自动执行」「确认=先出变更单、经裁定后才落」「禁止=fail-closed 拒并给 hint」。
|
|
26
|
+
|
|
27
|
+
三条硬约束的落法(设计 §三):
|
|
28
|
+
① 单一入口——本模块是**全仓唯一**读 `MDCG_AUTONOMY` 的地方;
|
|
29
|
+
② 纯加严——接线点一律在**既有资格闸通过之后**(写链在 audit/consistency/gated
|
|
30
|
+
之后、落盘之前;遗忘合并闸在 writelimit/forgetting 裁决之后;删除在
|
|
31
|
+
`protect.guard_forget` 之后),档位**不参与资格判定**,也不放宽任何既有判据;
|
|
32
|
+
③ 计划外零变更——计划模式下未命中计划步骤的动作 = `forbid`(fail-closed 带
|
|
33
|
+
hint),不静默跳过。**本批计划输入面未落**:无计划(`plan=None`)时
|
|
34
|
+
「须命中计划步骤」的 A/B/E 一律 forbid;C/D 在计划档走「需确认」,与
|
|
35
|
+
变更确认档同格(矩阵如此,无需计划)。
|
|
36
|
+
|
|
37
|
+
非法 env 值:`mode()` **fail-closed 抛 `AutonomyModeError`**(hint 列合法值),
|
|
38
|
+
**不静默降级**到缺省档——静默降级会把「配置打错」变成「放权范围与预期不符」,
|
|
39
|
+
正是设计要消灭的那类不实。
|
|
40
|
+
|
|
41
|
+
**批次③(2026-10-02)**:变更单载荷在(动作类/目标/后像/理由/primitive/meta)
|
|
42
|
+
之上补三字段——`before`(前像,**执行时点**快照引用)· `impact`(执行时点影响面
|
|
43
|
+
读数)· `rollback`(可执行的回滚命令串)。三键在**提议时点**即占位(恒在键面,
|
|
44
|
+
取值 None/""——字段完备),由执行桥(`mdcos._mutation_execute`)在动作落盘前
|
|
45
|
+
经 `mutation_executed()` 补全为执行时点实值;回滚原语通用化与回滚实测在
|
|
46
|
+
`md_cg/rollback.py` + `md_cg/rollback_cli.py`(守卫 `test_mutation_rollback.py`)。
|
|
47
|
+
|
|
48
|
+
**批次④(2026-10-02,设计 §六/§七)**:**准入闸**——`settle()` 是生效面
|
|
49
|
+
(结算「实际生效档位」并冻结进进程级结算态):`env MDCG_AUTONOMY=full` 而
|
|
50
|
+
准入读数(`md_cg/admission.py` 的 R1–R4)不满足 ⇒ `effective` **回落 confirm**
|
|
51
|
+
并在结算体 `alerts` 里逐条报「缺哪条读数」(另向 stderr 打一行启动面告警)
|
|
52
|
+
——**不静默降级**;`plan`/`confirm` 不受读数影响(零 IO、零告警);非法 env
|
|
53
|
+
仍 fail-closed 抛 `AutonomyModeError`。
|
|
54
|
+
|
|
55
|
+
「不可选中」的落法(设计 §六 逐字):
|
|
56
|
+
· 准入闸作用于**经 env 选中 full** 的路径(`mode()` 读 env 后折算);
|
|
57
|
+
· `decide(mode_explicit=…)` 是**显式强制指定**(守卫/工具用;不经 env 键
|
|
58
|
+
= 不属「选中」行为),不参与准入折算——生产热路径一律走 env 路径;
|
|
59
|
+
· **未 settle(生效面未启用)时 `mode()` 与改动前逐位一致**(对拍);
|
|
60
|
+
结算只对「配置档位 = full」的进程态生效,且以 `settle` 的调用为界
|
|
61
|
+
(触发频度由调用方控制:MCP 启动面/常驻巡检按需调用;`admission.gate`
|
|
62
|
+
的读数有 TTL 缓存,见 `md_cg/admission.py`——热路径 `mode()`/`decide()`
|
|
63
|
+
零 IO,不因准入闸增加任何每次调用的扫描)。
|
|
64
|
+
|
|
65
|
+
**补强批次(v1.1,2026-10-02)**:**结算异常语义 fail-closed**——`settle()`
|
|
66
|
+
对读数结算过程的一切异常**捕获并写「结算失败/不可判」态**(键 `error` 非空,
|
|
67
|
+
与「未调用过 settle」可区分;`alerts` 一并报「读数不可得」),向 stderr 打
|
|
68
|
+
一行告警——**不静默**;失败态的**有效档位**两格:
|
|
69
|
+
· 此前**从未成功结算** ⇒ 回落 `confirm`——收口「数据面损坏(如溢出崩溃)
|
|
70
|
+
+ 未结算 ⇒ `mode()` 照常返回 full」的 fail-open(设计 §六「不满足不可
|
|
71
|
+
选中/不静默降级」);
|
|
72
|
+
· 此前**已有成功结算** ⇒ 保持上一次成功结算的 effective(**不升不降**,
|
|
73
|
+
既有对拍锚不得破;异常只是把结算态标记为失败/不可判,可随时重结算)。
|
|
74
|
+
`plan`/`confirm` 不受影响;**未调用过 settle 的进程 `mode()` 与改动前逐位
|
|
75
|
+
一致**;合法数据面下的结算与读数逐位不变。
|
|
76
|
+
|
|
77
|
+
**补强批次(v1.2,2026-10-03 · 步骤⑤补强)**:**E 面写盘前闸接线**——设计 §三
|
|
78
|
+
矩阵的 E 列在 plan 档是「允许(须命中计划步骤)」,而接入前**生产码**的
|
|
79
|
+
`decide()` 调用点只覆盖 A/B/C/D(plan 档 E 类动作照跑并写盘,实测 importance
|
|
80
|
+
0.5→0.57 落盘)。本批把 `decide(E_WEIGHT)` 接到 E 面的**写盘入口**——
|
|
81
|
+
`weights.recalc` / `freshness.recalc` / `lifecycle.set_state` /
|
|
82
|
+
`lifecycle.backfill` 的 apply 路径(位置=既有资格面之后、任何盘面写入之前),
|
|
83
|
+
并立 `e_gate()` 为 E 面**唯一判定入口**、`forbidden_result()` 为统一的
|
|
84
|
+
fail-closed 返回体(带档位读数与可执行 hint、零写盘、不出单)。语义两格
|
|
85
|
+
(设计 §三 逐格):plan 档无计划 ⇒ fail-closed;confirm/full ⇒ ALLOW、原链
|
|
86
|
+
**原样**(「confirm/full 全线逐位不变」是硬约束,非建议)。未接的 E 面入口
|
|
87
|
+
与理由(`lifecycle.stamp` 属 B 链 / 库层 `require_transition` 禁接线 /
|
|
88
|
+
分数类回滚面待裁)逐条登记在 `e_gate()` docstring——留池项据此从「E 面整体
|
|
89
|
+
未接线」**收窄**为「上述三处(各有面归属或待裁)」。
|
|
90
|
+
|
|
91
|
+
本批**不做**(登记留池):计划输入面(`plan=None` ⇒ plan 档 E 一律 fail-closed,
|
|
92
|
+
故 E 面的「命中计划步骤」分支仍待计划输入面接线);准入闸的生产调用点接线
|
|
93
|
+
(启动面/常驻巡检调 `settle`——本批落生效面单点与守卫,接线由宿主按需调用)。
|
|
94
|
+
"""
|
|
95
|
+
from __future__ import annotations
|
|
96
|
+
|
|
97
|
+
import hashlib
|
|
98
|
+
import os
|
|
99
|
+
import sys
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
"AutonomyModeError", "AUTONOMY_ENV_DEFAULTS", "AUTONOMY_ENV_KEYS",
|
|
104
|
+
"AUTONOMY_MODES", "DEFAULT_MODE", "ACTION_CLASSES", "ACTION_NAMES",
|
|
105
|
+
"ALLOW", "CONFIRM", "FORBID", "PLAN_STEP", "MATRIX",
|
|
106
|
+
"A_ADD", "B_MERGE", "C_REWRITE", "D_DELETE", "E_WEIGHT",
|
|
107
|
+
"KIND_PROPOSAL", "KIND_MUTATION", "KIND_FIELD",
|
|
108
|
+
"autonomy_env", "mode", "decide",
|
|
109
|
+
# 补强批次 v1.2:E 面写盘前闸(判定唯一入口 + 统一 fail-closed 返回体)
|
|
110
|
+
"e_gate", "forbidden_result",
|
|
111
|
+
"order_kind", "mutation_view",
|
|
112
|
+
"mutation_payload", "mutation_executed", "mutation_dedup_key",
|
|
113
|
+
"propose_mutation",
|
|
114
|
+
# 批次④:准入闸生效面(设计 §六/§七)
|
|
115
|
+
"MODE_CONFIRM", "MODE_FULL",
|
|
116
|
+
"settle", "admission_state", "reset_settlement",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
# ---- §三「单一入口」:env 表的**单一真源** --------------------------------
|
|
120
|
+
# 纪律:**改缺省只改这里**;读取点只经 `autonomy_env()` / `mode()`。
|
|
121
|
+
AUTONOMY_ENV_DEFAULTS = {
|
|
122
|
+
"mode": "confirm", # 缺省档 = 变更确认(设计 §〇.2 裁定①)
|
|
123
|
+
}
|
|
124
|
+
#: 各键的 env 名(**全仓唯一的字面量落点**——守卫按此 grep 钉死)。
|
|
125
|
+
AUTONOMY_ENV_KEYS = {
|
|
126
|
+
"mode": "MDCG_AUTONOMY",
|
|
127
|
+
}
|
|
128
|
+
#: 合法档位(顺序即「计划 → 变更确认 → 完全访问」的放权阶梯,设计 §七)。
|
|
129
|
+
AUTONOMY_MODES = ("plan", "confirm", "full")
|
|
130
|
+
#: 缺省档位的具名常量(= 真源表取值,不另写第二处字面量)。
|
|
131
|
+
DEFAULT_MODE = AUTONOMY_ENV_DEFAULTS["mode"]
|
|
132
|
+
#: 阶梯两端的具名常量(从 AUTONOMY_MODES 派生——不另写第二处字面量)。
|
|
133
|
+
#: 批次④ 准入闸只作用于 MODE_FULL(设计 §六「该档不可选中」);回落目标 = MODE_CONFIRM。
|
|
134
|
+
MODE_CONFIRM = AUTONOMY_MODES[1]
|
|
135
|
+
MODE_FULL = AUTONOMY_MODES[2]
|
|
136
|
+
|
|
137
|
+
# ---- 动作类(设计 §一 五类,一手普查口径) --------------------------------
|
|
138
|
+
A_ADD = "A" # 新增节点
|
|
139
|
+
B_MERGE = "B" # 合并(新内容并入既有节点)
|
|
140
|
+
C_REWRITE = "C" # 改写(既有节点就地改)
|
|
141
|
+
D_DELETE = "D" # 删除(软删)
|
|
142
|
+
E_WEIGHT = "E" # 权重与生命周期
|
|
143
|
+
ACTION_CLASSES = (A_ADD, B_MERGE, C_REWRITE, D_DELETE, E_WEIGHT)
|
|
144
|
+
ACTION_NAMES = {A_ADD: "新增", B_MERGE: "合并", C_REWRITE: "改写",
|
|
145
|
+
D_DELETE: "删除", E_WEIGHT: "权重/生命周期"}
|
|
146
|
+
|
|
147
|
+
# ---- 判定值 ---------------------------------------------------------------
|
|
148
|
+
ALLOW = "allow" # 自动执行(与改动前逐位一致)
|
|
149
|
+
CONFIRM = "confirm" # 先出变更单,经裁决 accept 后才落
|
|
150
|
+
FORBID = "forbid" # fail-closed 拒,带可执行 hint
|
|
151
|
+
#: 矩阵**内部**格标记:该格语义是「允许(须命中计划中的步骤)」——不是判定值,
|
|
152
|
+
#: `decide` 会把它按计划命中情况折算成 ALLOW/FORBID(见 decide)。
|
|
153
|
+
PLAN_STEP = "plan_step"
|
|
154
|
+
|
|
155
|
+
#: §三 裁决矩阵(**唯一真源**):mode → 动作类 → 判定/格标记。
|
|
156
|
+
MATRIX = {
|
|
157
|
+
"plan": {A_ADD: PLAN_STEP, B_MERGE: PLAN_STEP, C_REWRITE: CONFIRM,
|
|
158
|
+
D_DELETE: CONFIRM, E_WEIGHT: PLAN_STEP},
|
|
159
|
+
"confirm": {A_ADD: ALLOW, B_MERGE: CONFIRM, C_REWRITE: CONFIRM,
|
|
160
|
+
D_DELETE: CONFIRM, E_WEIGHT: ALLOW},
|
|
161
|
+
"full": {A_ADD: ALLOW, B_MERGE: ALLOW, C_REWRITE: ALLOW,
|
|
162
|
+
D_DELETE: ALLOW, E_WEIGHT: ALLOW},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ---- 变更单(设计 §四:复用既有审核队列,靠类型字段区分) ------------------
|
|
166
|
+
#: 队列条目类型字段(**落 rec 顶层**)。取值见下;**缺键 = proposal**(存量
|
|
167
|
+
#: 条目零迁移:老记录没有这个键,一律按原有提案语义读)。
|
|
168
|
+
KIND_FIELD = "kind"
|
|
169
|
+
KIND_PROPOSAL = "proposal" # 原有:新写入未获 ACCEPT 的提案
|
|
170
|
+
KIND_MUTATION = "mutation" # 本批新增:对既有记忆的 B/C/D 变更单
|
|
171
|
+
#: 变更单载荷的落点(propose 的既有任意槽 `extra`,沿用 issue50-b 的
|
|
172
|
+
#: `extra.defer_reason` 先例——不新增第二套队列字段族)。**为什么不放 rec
|
|
173
|
+
#: 顶层**:顶层 `kind` 是设计 §四 明文的「类型字段」(读取面/统计面共用),
|
|
174
|
+
#: 而动作类/目标/后像/理由是**单条变更单的业务载荷**,塞顶层会与既有读取方
|
|
175
|
+
#: (`_brief` 取 content、accept 分支取 tags/condition_space)抢键名。
|
|
176
|
+
MUTATION_SLOT = "mutation"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AutonomyModeError(ValueError):
|
|
180
|
+
"""档位 env 非法(fail-closed:报错 + hint 列合法值,不静默降级)。"""
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# 生效条件:name 为 AUTONOMY_ENV_DEFAULTS 的键时按 AUTONOMY_ENV_KEYS[name] 从 environ(缺省 os.environ)取名取值,缺键(None)时回落真源缺省;返回**原始字符串**(不做解释——解释归 mode());name 非表内键时抛 KeyError(拼错键名即编程错误,不静默回落);
|
|
184
|
+
def autonomy_env(name: str, environ=None) -> str:
|
|
185
|
+
"""§三 env 表的唯一读取出口(原始字面量)。"""
|
|
186
|
+
env = os.environ if environ is None else environ
|
|
187
|
+
v = env.get(AUTONOMY_ENV_KEYS[name])
|
|
188
|
+
return AUTONOMY_ENV_DEFAULTS[name] if v is None else str(v)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# 生效条件:autonomy_env("mode") 去空白转小写后属 AUTONOMY_MODES 时返回该档位名;不属(含空串)时抛 AutonomyModeError(hint 列合法值)——不静默降级;本函数是「配置档位」的唯一解释点(env 原文),settle() 与 mode() 共用;
|
|
192
|
+
def _configured_mode(environ=None) -> str:
|
|
193
|
+
"""配置档位(env 原文解释;非法值 fail-closed,文案与旧 mode() 逐字一致)。"""
|
|
194
|
+
raw = autonomy_env("mode", environ)
|
|
195
|
+
m = (raw or "").strip().lower()
|
|
196
|
+
if m not in AUTONOMY_MODES:
|
|
197
|
+
raise AutonomyModeError(
|
|
198
|
+
"非法档位 %s=%r:允许值 %s(缺省 %s)。"
|
|
199
|
+
"本次动作 fail-closed 未执行——不静默降级到缺省档"
|
|
200
|
+
"(静默降级会让「配置打错」伪装成「放权范围与预期不符」)。"
|
|
201
|
+
"改正环境变量后重试即可。"
|
|
202
|
+
% (AUTONOMY_ENV_KEYS["mode"], raw, "|".join(AUTONOMY_MODES),
|
|
203
|
+
DEFAULT_MODE))
|
|
204
|
+
return m
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# 生效条件:恒返回「实际生效档位」——配置档位非 full(plan/confirm)时即配置档位(不受准入读数影响、零 IO);配置档位为 full 且进程已由 settle() 结算(结算态 configured=full 且 effective 非空——含**结算失败态**,其 effective 按「未结算⇒confirm/已结算⇒保持上次」两格取值,见 settle)时返回结算的 effective(读数不满足/不可得 ⇒ MODE_CONFIRM=设计 §六「该档不可选中」);配置档位为 full 而未结算时返回 full(生效面未启用=与改动前逐位一致);environ 显式传参(非进程级 os.environ)时不参与准入折算;
|
|
208
|
+
def mode(environ=None) -> str:
|
|
209
|
+
"""当前**实际生效**档位(缺省 confirm;full 经准入闸,见 settle())。
|
|
210
|
+
|
|
211
|
+
非法值 fail-closed 抛 AutonomyModeError(不静默降级)。
|
|
212
|
+
"""
|
|
213
|
+
m = _configured_mode(environ)
|
|
214
|
+
if (m == MODE_FULL and environ is None
|
|
215
|
+
and _SETTLEMENT["configured"] == MODE_FULL
|
|
216
|
+
and _SETTLEMENT["effective"]):
|
|
217
|
+
return _SETTLEMENT["effective"]
|
|
218
|
+
return m
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---- 批次④:准入闸生效面(设计 §六/§七) --------------------------------
|
|
222
|
+
#: 进程级结算态(生效面**唯一落点**):settle() 写入;mode() / admission_state()
|
|
223
|
+
#: 只读。键集恒定(形态固定,便于守卫与宿主逐键读)。`error`(补强批次):
|
|
224
|
+
#: None = 最近一次结算正常;非空 = 最近一次结算**失败/不可判**(读数不可得)
|
|
225
|
+
#: ——这是「结算失败态」与「未调用过 settle」(configured=None)的可区分标记。
|
|
226
|
+
_SETTLEMENT = {"configured": None, "effective": None, "settled": False,
|
|
227
|
+
"admission": None, "alerts": [], "root": None, "t": 0.0,
|
|
228
|
+
"error": None}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# 生效条件:text 为字符串时向 stderr 写一行;写失败静默吞掉(告警已在结算体 alerts/error 里,stderr 只是启动面可观测性增强;写失败不得影响结算本身);
|
|
232
|
+
def _warn(text: str) -> None:
|
|
233
|
+
"""启动面告警单点(设计 §七「自动回落并在启动面告警」)。"""
|
|
234
|
+
try:
|
|
235
|
+
sys.stderr.write(text)
|
|
236
|
+
except Exception: # noqa: BLE001
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# 生效条件:configured 档位经 _configured_mode(environ) 取 env 值(非法即 fail-closed 抛 AutonomyModeError);configured 非 full 时零 IO 返回 {configured, effective=configured, settled=False, admission=None, alerts=[], error=None};configured=full 时调 admission.gate(root 或 admission.default_root(), ttl, force) 结算——读数满足 ⇒ effective=full、alerts=[];不满足(含不可判)⇒ effective=MODE_CONFIRM 且 alerts 逐条列出未过读数 {reading, ok, detail} 并向 stderr 打一行启动面告警;结算过程抛异常(读数不可得)⇒ 捕获不抛出、写失败态(error 非空 + alerts 一条「读数不可得」)——此前从未成功结算 ⇒ effective=MODE_CONFIRM(fail-closed,收口 fail-open)、已有成功结算 ⇒ 保持上次成功结算的 effective(不升不降);所有分支的结算体(含 root/t/error)写入进程级 _SETTLEMENT 后原样返回;
|
|
241
|
+
def settle(environ=None, root=None, force=False, ttl=None) -> dict:
|
|
242
|
+
"""结算「实际生效档位」(准入闸生效面,设计 §六/§七)。
|
|
243
|
+
|
|
244
|
+
env `MDCG_AUTONOMY=full` 而准入读数(`md_cg/admission.py` 的 R1–R4)
|
|
245
|
+
不满足 ⇒ `effective` 回落 `confirm`,`alerts` 报「缺哪条读数」
|
|
246
|
+
——**不静默降级**;`plan`/`confirm` 不受读数影响(零 IO、零告警);
|
|
247
|
+
非法 env 仍 fail-closed 抛 `AutonomyModeError`。
|
|
248
|
+
|
|
249
|
+
**结算失败/不可判(补强批次)**:读数结算过程抛异常(读数不可得)⇒
|
|
250
|
+
**不抛出、不静默**——写失败态(`error` 非空 + `alerts` 一条「读数不可得」),
|
|
251
|
+
有效档位两格:此前从未成功结算 ⇒ 回落 `confirm`(收口「未结算 + 崩溃 ⇒
|
|
252
|
+
full 照常生效」的 fail-open);此前已有成功结算 ⇒ 保持上一次成功结算的
|
|
253
|
+
effective(不升不降,既有对拍锚)。告警一行打 stderr。
|
|
254
|
+
|
|
255
|
+
调用面(触发频度):由调用方按需调用——MCP 启动面一次、常驻巡检按
|
|
256
|
+
TTL 周期可重调(读数缓存见 `admission.gate`);热路径 `mode()` /
|
|
257
|
+
`decide()` 只读结算态(零 IO),不因准入闸增加任何每次调用的扫描。
|
|
258
|
+
"""
|
|
259
|
+
cfg = _configured_mode(environ)
|
|
260
|
+
now = time.time()
|
|
261
|
+
if cfg != MODE_FULL:
|
|
262
|
+
out = {"configured": cfg, "effective": cfg, "settled": False,
|
|
263
|
+
"admission": None, "alerts": [], "root": None, "t": now,
|
|
264
|
+
"error": None}
|
|
265
|
+
_SETTLEMENT.update(out)
|
|
266
|
+
return out
|
|
267
|
+
from . import admission as _adm # 惰性 import:热路径 import 面不变
|
|
268
|
+
try:
|
|
269
|
+
r = _adm.gate(root if root is not None else _adm.default_root(),
|
|
270
|
+
ttl=ttl, force=force)
|
|
271
|
+
except Exception as exc: # noqa: BLE001
|
|
272
|
+
# 结算失败/不可判(补强批次):读数不可得 ⇒ fail-closed、不静默。
|
|
273
|
+
# 有效档位两格:从未成功结算 ⇒ confirm(收口「数据面损坏 + 未结算 ⇒
|
|
274
|
+
# full」的 fail-open);已有成功结算 ⇒ 保持上次 effective(不升不降)。
|
|
275
|
+
prev = _SETTLEMENT["effective"] if _SETTLEMENT["settled"] else None
|
|
276
|
+
eff = prev or MODE_CONFIRM
|
|
277
|
+
if prev:
|
|
278
|
+
tail = "实际生效档位保持上次成功结算的 %s(不升不降)" % eff
|
|
279
|
+
else:
|
|
280
|
+
tail = "已回落 %s" % eff
|
|
281
|
+
err = "%s: %s" % (type(exc).__name__, exc)
|
|
282
|
+
detail = ("读数不可得(结算过程异常 %s)——%s(不静默降级;"
|
|
283
|
+
"修好读数面后重新 settle 即可)。" % (err, tail))
|
|
284
|
+
out = {"configured": cfg, "effective": eff, "settled": True,
|
|
285
|
+
"admission": None,
|
|
286
|
+
"alerts": [{"reading": "settle_error", "ok": None,
|
|
287
|
+
"detail": detail}],
|
|
288
|
+
"root": root, "t": now, "error": err}
|
|
289
|
+
_warn("[MdCG 三档自治] %s=%s 但结算失败/不可判:%s ——%s"
|
|
290
|
+
"(不静默降级;读数详情:python -X utf8 -m md_cg.admission "
|
|
291
|
+
"--root \"%s\")。\n"
|
|
292
|
+
% (AUTONOMY_ENV_KEYS["mode"], cfg, err, tail,
|
|
293
|
+
root if root is not None else "<default>"))
|
|
294
|
+
_SETTLEMENT.update(out)
|
|
295
|
+
return out
|
|
296
|
+
if r["ok"]:
|
|
297
|
+
out = {"configured": cfg, "effective": cfg, "settled": True,
|
|
298
|
+
"admission": r, "alerts": [], "root": r["root"], "t": now,
|
|
299
|
+
"error": None}
|
|
300
|
+
else:
|
|
301
|
+
alerts = [{"reading": rd["id"], "ok": rd["ok"], "detail": rd["detail"]}
|
|
302
|
+
for rd in r["readings"] if rd["ok"] is not True]
|
|
303
|
+
out = {"configured": cfg, "effective": MODE_CONFIRM, "settled": True,
|
|
304
|
+
"admission": r, "alerts": alerts, "root": r["root"], "t": now,
|
|
305
|
+
"error": None}
|
|
306
|
+
# 启动面告警(设计 §七「自动回落并在启动面告警」)——一行,可观测;
|
|
307
|
+
# stderr 写失败不得影响结算本身(告警已在返回体 alerts 里)。
|
|
308
|
+
_warn("[MdCG 三档自治] %s=%s 但准入读数不满足:缺 %s ——实际生效"
|
|
309
|
+
"档位回落 %s(不静默降级;读数详情:python -X utf8 -m "
|
|
310
|
+
"md_cg.admission --root \"%s\")。\n"
|
|
311
|
+
% (AUTONOMY_ENV_KEYS["mode"], cfg,
|
|
312
|
+
"、".join(a["reading"] for a in alerts),
|
|
313
|
+
MODE_CONFIRM, r["root"]))
|
|
314
|
+
_SETTLEMENT.update(out)
|
|
315
|
+
return out
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# 生效条件:无入参;返回进程级结算态的快照副本(configured/effective/settled/admission/alerts/root/t/error,alerts 为列表副本)——只读零 IO,供宿主与守卫答「它今天够格吗」(error 非空 ⇒ 最近一次结算失败/不可判);
|
|
319
|
+
def admission_state() -> dict:
|
|
320
|
+
"""最近一次 settle() 的结算体快照(含 alerts/error)——只读、零副作用。"""
|
|
321
|
+
st = dict(_SETTLEMENT)
|
|
322
|
+
st["alerts"] = list(st.get("alerts") or [])
|
|
323
|
+
return st
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# 生效条件:无入参;把进程级结算态复位为未结算(configured=None、effective=None、settled=False、admission=None、alerts=[]、root=None、t=0.0、error=None),无返回值(供测试/宿主重结算);
|
|
327
|
+
def reset_settlement() -> None:
|
|
328
|
+
"""清结算态(mode() 回到「配置档位」旧行为)。"""
|
|
329
|
+
_SETTLEMENT.update({"configured": None, "effective": None,
|
|
330
|
+
"settled": False, "admission": None, "alerts": [],
|
|
331
|
+
"root": None, "t": 0.0, "error": None})
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# 生效条件:action_class 归一后属 ACTION_CLASSES(否则抛 ValueError);plan 命中集 _plan_actions(plan) 为 None(无计划输入)时 A/B/E 判 forbid(带「计划输入面待后续批次」hint),非 None 时命中该动作类判 allow、否则 forbid(计划外零变更);其余格按 MATRIX 原样返回 allow/confirm;返回 {"action","action_name","mode","decision","hint"},decision ∈ ALLOW/CONFIRM/FORBID;
|
|
335
|
+
def decide(action_class, mode_explicit=None, plan=None, environ=None) -> dict:
|
|
336
|
+
"""§三 裁决矩阵判定:返回 allow | confirm | forbid(+ 可读 hint)。
|
|
337
|
+
|
|
338
|
+
action_class —— A/B/C/D/E(大小写不敏感;非法值抛 ValueError)。
|
|
339
|
+
mode_explicit —— 显式档位;None(缺省)时**经唯一入口 `mode()` 读 env**
|
|
340
|
+
(非法 env 即 fail-closed 抛错;`mode()` 返回**实际生效
|
|
341
|
+
档位**——full 经准入闸折算,见 `settle()`,回落时本判定
|
|
342
|
+
按 confirm 档矩阵走)。形参刻意不叫 `mode`:
|
|
343
|
+
同名会遮蔽模块级唯一入口,`mode(environ)` 就成了对局部名的
|
|
344
|
+
调用——「单一入口」必须能被静态钉死(见守卫的定点变异)。
|
|
345
|
+
显式传值(含 mode_explicit="full")是**强制指定**,不参与
|
|
346
|
+
准入折算(不经 env 键=不属设计 §六 的「选中」行为)。
|
|
347
|
+
plan —— 计划(**本批只落最小判定语义**):None = 无计划输入;
|
|
348
|
+
可给动作类集合(可迭代,元素为 "A"/"B"… 或含 "action" 键的
|
|
349
|
+
映射)。计划模式下 A/B/E 只有在命中计划步骤时才 allow。
|
|
350
|
+
|
|
351
|
+
调用方**必须**只看 `decision`,不得自行比较档位名(否则就是各读一份)。
|
|
352
|
+
"""
|
|
353
|
+
act = str(action_class or "").strip().upper()
|
|
354
|
+
if act not in ACTION_CLASSES:
|
|
355
|
+
raise ValueError("未知动作类:%r(允许:%s)"
|
|
356
|
+
% (action_class, "/".join(ACTION_CLASSES)))
|
|
357
|
+
m = mode(environ) if mode_explicit is None else _norm_mode(mode_explicit)
|
|
358
|
+
cell = MATRIX[m][act]
|
|
359
|
+
out = {"action": act, "action_name": ACTION_NAMES[act], "mode": m,
|
|
360
|
+
"decision": cell, "hint": ""}
|
|
361
|
+
if cell == PLAN_STEP:
|
|
362
|
+
steps = _plan_actions(plan)
|
|
363
|
+
if steps is None:
|
|
364
|
+
return _with(out, FORBID,
|
|
365
|
+
"计划模式(%s=plan):动作类 %s(%s)须命中计划中的步骤,"
|
|
366
|
+
"但本次没有计划可依——fail-closed 未执行、未出变更单。"
|
|
367
|
+
"计划模式需先提供计划(计划输入面待后续批次)。"
|
|
368
|
+
% (AUTONOMY_ENV_KEYS["mode"], act, ACTION_NAMES[act]))
|
|
369
|
+
if act in steps:
|
|
370
|
+
return _with(out, ALLOW, "")
|
|
371
|
+
return _with(out, FORBID,
|
|
372
|
+
"计划外零变更(设计 §三 硬约束③):动作类 %s(%s)不在"
|
|
373
|
+
"本次计划里——fail-closed 未执行。要执行请把该动作类写进"
|
|
374
|
+
"计划,或改用 %s=%s/%s。"
|
|
375
|
+
% (act, ACTION_NAMES[act], AUTONOMY_ENV_KEYS["mode"],
|
|
376
|
+
"confirm", "full"))
|
|
377
|
+
if cell == CONFIRM:
|
|
378
|
+
return _with(out, CONFIRM,
|
|
379
|
+
"变更确认档(%s=%s):动作类 %s(%s)需先出变更单,"
|
|
380
|
+
"经裁决 accept 后才落盘——本次未落盘。"
|
|
381
|
+
"裁决入口:python -m md_cg.review_cli list 后 accept/reject,"
|
|
382
|
+
"或 cg(op=review, pid=<pid>, decision=accept|reject, "
|
|
383
|
+
"reason=<理由>)。"
|
|
384
|
+
% (AUTONOMY_ENV_KEYS["mode"], m, act, ACTION_NAMES[act]))
|
|
385
|
+
return _with(out, ALLOW, "")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# 生效条件:m 去空白转小写后属 AUTONOMY_MODES 时原样返回该名,否则抛 AutonomyModeError(显式传参也要过同一合法值闸——档位名的合法值只有一处定义);
|
|
389
|
+
def _norm_mode(m) -> str:
|
|
390
|
+
s = (m or "").strip().lower()
|
|
391
|
+
if s not in AUTONOMY_MODES:
|
|
392
|
+
raise AutonomyModeError(
|
|
393
|
+
"非法档位 %r:允许值 %s(缺省 %s)。"
|
|
394
|
+
% (m, "|".join(AUTONOMY_MODES), DEFAULT_MODE))
|
|
395
|
+
return s
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# 生效条件:out 为 dict 时把 decision 与 hint 写入(hint 为假值也照写,保证键恒在)后返回同一 dict;
|
|
399
|
+
def _with(out: dict, decision: str, hint: str) -> dict:
|
|
400
|
+
out["decision"] = decision
|
|
401
|
+
out["hint"] = hint
|
|
402
|
+
return out
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# 生效条件:plan 为假值(None/空)时返回 None(=无计划输入);plan 为映射时取其 "actions" 键(缺则取 "steps" 里各步的 "action" 或 "actions" 键的并集);plan 为可迭代时逐个取元素本身或其 "action" 键;全部归一为去空白大写后的动作类集合(不含 ACTION_CLASSES 的元素被丢弃);
|
|
406
|
+
def _plan_actions(plan):
|
|
407
|
+
"""计划 → 允许的动作类集合;None/空 = 无计划输入(与「空计划」同判)。"""
|
|
408
|
+
if not plan:
|
|
409
|
+
return None
|
|
410
|
+
items = None
|
|
411
|
+
if isinstance(plan, dict):
|
|
412
|
+
if plan.get("actions"):
|
|
413
|
+
items = list(plan.get("actions"))
|
|
414
|
+
elif plan.get("steps"):
|
|
415
|
+
items = []
|
|
416
|
+
for st in plan.get("steps") or []:
|
|
417
|
+
if isinstance(st, dict):
|
|
418
|
+
items.extend(st.get("actions") or ([st["action"]]
|
|
419
|
+
if st.get("action")
|
|
420
|
+
else []))
|
|
421
|
+
elif st:
|
|
422
|
+
items.append(st)
|
|
423
|
+
elif isinstance(plan, str):
|
|
424
|
+
items = [plan]
|
|
425
|
+
else:
|
|
426
|
+
try:
|
|
427
|
+
items = list(plan)
|
|
428
|
+
except TypeError:
|
|
429
|
+
return None
|
|
430
|
+
out = set()
|
|
431
|
+
for it in items or []:
|
|
432
|
+
if isinstance(it, dict):
|
|
433
|
+
it = it.get("action") or it.get("actions") or ""
|
|
434
|
+
if isinstance(it, (list, tuple, set)):
|
|
435
|
+
out.update(str(x).strip().upper() for x in it)
|
|
436
|
+
continue
|
|
437
|
+
s = str(it or "").strip().upper()
|
|
438
|
+
if s in ACTION_CLASSES:
|
|
439
|
+
out.add(s)
|
|
440
|
+
return out
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ---- 补强批次(v1.2):E 面**写盘前**闸(设计 §三 plan 档 E「须命中计划步骤」) ----
|
|
444
|
+
# 为什么需要单点:E 类动作(权重与生命周期)的写盘入口散在三处模块
|
|
445
|
+
# (`weights.recalc` / `freshness.recalc` / `lifecycle.set_state|backfill`),
|
|
446
|
+
# 若各写一份档位判据就是「各模块各读一份」——违背设计 §三 硬约束①「单一入口」。
|
|
447
|
+
# 故判定经本函数(对 `decide` 的具名再出口),返回体与返回语义见下。
|
|
448
|
+
|
|
449
|
+
# 生效条件:恒返回 decide(E_WEIGHT, plan=plan, environ=environ) 的**原样体**(含 mode/action/action_name/decision/hint 五键,decision ∈ ALLOW/CONFIRM/FORBID);plan 为 None(本批现状:计划输入面无接线)时 plan 档 E 判 FORBID(fail-closed 带 hint);confirm/full 两档恒 ALLOW(与 E 列矩阵逐格一致 ⇒ 调用方原链原样);environ 显式传参(非进程级 os.environ)时不参与准入折算(同 decide);非法 env 值仍由 mode() fail-closed 抛 AutonomyModeError;本函数零 IO、零副作用;
|
|
450
|
+
def e_gate(plan=None, environ=None) -> dict:
|
|
451
|
+
"""E 类动作**写盘前**的档位判定(**唯一入口**:E 面各写盘入口共用)。
|
|
452
|
+
|
|
453
|
+
调用方**只看 `decision`**:
|
|
454
|
+
|
|
455
|
+
· `ALLOW`(confirm/full 两档;将来计划命中时亦同)⇒ **原链原样**——
|
|
456
|
+
「confirm/full 全线逐位不变」是硬约束:本闸在放行分支不产生任何行为
|
|
457
|
+
差异(不写盘、不出单、不加返回键)。
|
|
458
|
+
· 其余(现状只有 plan 档无计划 ⇒ `FORBID`)⇒ 调 `forbidden_result()`
|
|
459
|
+
早退:**不落盘、不出单、不静默**,带档位读数与可执行 hint。
|
|
460
|
+
|
|
461
|
+
位置纪律(设计 §三 硬约束②「纯加严」):调用点一律在**既有资格面之后、
|
|
462
|
+
盘面写入之前**——档位**不参与**资格判定(非法生命周期迁移/受保护拒绝/
|
|
463
|
+
节点不存在一律照旧先判、各回各的 error),也**不放宽**任何既有判据
|
|
464
|
+
(放行分支=返回 None 级别的「什么都不做」)。
|
|
465
|
+
|
|
466
|
+
边界(如实登记:**未接闸**的 E 面入口与理由——留池项据此收窄):
|
|
467
|
+
· `lifecycle.stamp` —— **无 IO 纯函数**(就地改 fm 副本,落盘由调用方
|
|
468
|
+
做),其两个调用方 `forgetting.reinforce` / `writelimit.converge_into`
|
|
469
|
+
都属 **B 合并/趋同链**(B 已有自己的闸,plan 档先于此处 fail-closed);
|
|
470
|
+
在 E 闸里再拦一次=用 E 判定去挡 B 动作,语义交叉且与 G 组「合并原语
|
|
471
|
+
不含档位判据」的机制前提相抵。故不接。
|
|
472
|
+
· `MdCG.add` 的 `lifecycle.require_transition` 面 —— **库层禁接线**
|
|
473
|
+
(守卫 G 组结构判据钉死):「accept 执行桥直调库层越过确认判定」正是
|
|
474
|
+
建立在此前提上。
|
|
475
|
+
· 分数类**回滚**面(`freshness.rollback` / `weights.rollback`)——回滚是
|
|
476
|
+
「破坏可逆」准入读数(R3)与设计 §四 第 4 条的兑现面:在 plan 档拦
|
|
477
|
+
回滚与设计语义相抵,属**设计面待裁**(不在本批自行扩面)。
|
|
478
|
+
"""
|
|
479
|
+
return decide(E_WEIGHT, plan=plan, environ=environ)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# 生效条件:dec 支持 .get 时返回新 dict {"ok":False,"error":"autonomy_forbidden","autonomy":{mode,action,action_name,decision}(四键取自 dec,缺键回落 None),"hint":dec["hint"] 或 ""},并把 **extra 的键值原样并入(各写盘面补自己的上下文键);不落盘、不出单、不改任何状态;
|
|
483
|
+
def forbidden_result(dec, **extra) -> dict:
|
|
484
|
+
"""E 面写盘前 fail-closed 的**统一返回体**(不静默:带档位读数与可执行 hint)。
|
|
485
|
+
|
|
486
|
+
键面固定:`ok=False` + `error="autonomy_forbidden"`(与写链/合并/删除三处
|
|
487
|
+
fail-closed 出口同值——读面可按同一错误码归并)+ `autonomy`(档位/动作类/
|
|
488
|
+
判定读数,形态同 `writepipe._gate_autonomy` 的 `_aut`)+ `hint`(来自
|
|
489
|
+
`decide`,含「怎么改才能执行」的可执行指引)。
|
|
490
|
+
"""
|
|
491
|
+
out = {"ok": False, "error": "autonomy_forbidden",
|
|
492
|
+
"autonomy": {"mode": dec.get("mode"), "action": dec.get("action"),
|
|
493
|
+
"action_name": dec.get("action_name"),
|
|
494
|
+
"decision": dec.get("decision")},
|
|
495
|
+
"hint": dec.get("hint") or ""}
|
|
496
|
+
out.update(extra)
|
|
497
|
+
return out
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ---- 变更单:构造/读取/幂等(设计 §四) ------------------------------------
|
|
501
|
+
|
|
502
|
+
# 生效条件:rec 支持 .get 且 rec.get("kind") 为真值时返回其去空白值,否则返回 KIND_PROPOSAL(**存量条目无 kind 一律视为 proposal**,零迁移);
|
|
503
|
+
def order_kind(rec) -> str:
|
|
504
|
+
"""队列条目类型(缺键 = proposal——存量零迁移)。"""
|
|
505
|
+
try:
|
|
506
|
+
k = str(rec.get(KIND_FIELD) or "").strip()
|
|
507
|
+
except AttributeError:
|
|
508
|
+
k = ""
|
|
509
|
+
return k or KIND_PROPOSAL
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# 生效条件:rec 支持 .get 时返回该变更单的载荷视图 {"kind","action","action_name","target","after","reason","primitive","before","impact","rollback","meta"}(非变更单返回 None;载荷缺失的键回落 None/空串);本函数只读,不产生任何副作用;
|
|
513
|
+
def mutation_view(rec):
|
|
514
|
+
"""变更单的只读视图(非变更单返回 None)——显示面/执行桥共用同一取值口径。
|
|
515
|
+
|
|
516
|
+
`before`/`impact`/`rollback`(批次③):三字段在提议时点即占键(取值
|
|
517
|
+
None/""=尚未执行),执行后由执行桥补全(见 `mutation_executed`)。
|
|
518
|
+
"""
|
|
519
|
+
if order_kind(rec) != KIND_MUTATION:
|
|
520
|
+
return None
|
|
521
|
+
try:
|
|
522
|
+
slot = (rec.get("extra") or {}).get(MUTATION_SLOT) or {}
|
|
523
|
+
except AttributeError:
|
|
524
|
+
slot = {}
|
|
525
|
+
act = str(slot.get("action") or "").strip().upper()
|
|
526
|
+
return {"kind": KIND_MUTATION,
|
|
527
|
+
"action": act or None,
|
|
528
|
+
"action_name": ACTION_NAMES.get(act) or "",
|
|
529
|
+
"target": slot.get("target") or rec.get("id"),
|
|
530
|
+
"after": slot.get("after") if slot.get("after") is not None
|
|
531
|
+
else rec.get("content"),
|
|
532
|
+
"reason": slot.get("reason") or "",
|
|
533
|
+
"primitive": slot.get("primitive") or "",
|
|
534
|
+
# 执行时点三字段(设计 §四,批次③):提议时点为 None/"",执行后补全。
|
|
535
|
+
"before": slot.get("before"),
|
|
536
|
+
"impact": slot.get("impact"),
|
|
537
|
+
"rollback": slot.get("rollback") or "",
|
|
538
|
+
"meta": dict(slot.get("meta") or {}),
|
|
539
|
+
"raw": slot}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
# 生效条件:action_class 归一后属 ACTION_CLASSES(否则抛 ValueError),target 非空(否则抛 ValueError 且带 hint);返回变更单载荷 dict——键:action/action_name/target/after/reason/primitive/before/impact/rollback/meta(meta 为可复现原动作所需的落盘参数副本,缺省空 dict;后三者为执行时点字段,提议时点占位 None/"");
|
|
543
|
+
def mutation_payload(action_class, target, after="", reason="",
|
|
544
|
+
primitive="", meta=None, before=None, impact=None,
|
|
545
|
+
rollback=None) -> dict:
|
|
546
|
+
"""变更单载荷(**唯一构造点**:动作类+目标+后像+理由+复现参数+执行时点三字段)。
|
|
547
|
+
|
|
548
|
+
执行时点三字段(设计 §四,批次③)——**键恒在**(提议时点即占位,
|
|
549
|
+
取值 None/""),由执行桥在动作落盘前经 `mutation_executed()` 补全:
|
|
550
|
+
|
|
551
|
+
· `before` —— 前像(可回滚句柄):**执行时点**快照的引用。
|
|
552
|
+
提议时点**不拍**:从提议到 accept 之间目标可能被第三方改动,回滚必须
|
|
553
|
+
撤销**本变更本身**、不抹第三方改动(设计 §四 明文)——前像必须反映
|
|
554
|
+
「执行前一刻」的盘面,故由 `mdcos._mutation_execute` 在动作原语落盘
|
|
555
|
+
之前调 `rollback.preimage()` 拍摄。
|
|
556
|
+
· `impact` —— 影响面:执行时点采集的读数(边/索引条目/聚合行)。
|
|
557
|
+
· `rollback` —— 回滚命令:可执行的回滚命令串(`rollback.command_for`
|
|
558
|
+
构造,形态 `python -X utf8 -m md_cg.rollback_cli --root <root> --pid <pid>`,
|
|
559
|
+
守卫实测演练)。
|
|
560
|
+
"""
|
|
561
|
+
act = str(action_class or "").strip().upper()
|
|
562
|
+
if act not in ACTION_CLASSES:
|
|
563
|
+
raise ValueError("未知动作类:%r(允许:%s)"
|
|
564
|
+
% (action_class, "/".join(ACTION_CLASSES)))
|
|
565
|
+
tgt = str(target or "").strip()
|
|
566
|
+
if not tgt:
|
|
567
|
+
raise ValueError("变更单缺目标节点 id:动作类 %s 的变更单必须指名目标"
|
|
568
|
+
"(无目标即无法复核、无法回滚)——fail-closed 拒绝出单。" % act)
|
|
569
|
+
return {"action": act, "action_name": ACTION_NAMES[act], "target": tgt,
|
|
570
|
+
"after": "" if after is None else after, "reason": str(reason or ""),
|
|
571
|
+
"primitive": str(primitive or ""),
|
|
572
|
+
# 执行时点三字段(键恒在;提议时点 = 未执行,占位 None/"")
|
|
573
|
+
"before": before, "impact": impact,
|
|
574
|
+
"rollback": str(rollback or ""),
|
|
575
|
+
"meta": dict(meta or {})}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# 生效条件:payload 支持 dict() 复制(None/假值按空 dict 起底)时返回**新 dict**——原载荷逐键复制后补 kind=KIND_MUTATION 与 executed=True,并仅在实参非 None/非空时覆写 before/impact/rollback(缺省保留原载荷取值,不把 None 伪装成实值);
|
|
579
|
+
def mutation_executed(payload, before=None, impact=None, rollback=None) -> dict:
|
|
580
|
+
"""执行时点补全(**唯一构造点**):把前像/影响面/回滚命令并进载荷副本。
|
|
581
|
+
|
|
582
|
+
为什么不原地改载荷:出单时载荷已随队列条目落盘(append-only)——执行记录
|
|
583
|
+
里的补全形态是**新对象**,「提议时点载荷」与「执行时点载荷」两个事实都保留
|
|
584
|
+
(外部对照时能看见「执行时点才知道的东西在执行时才出现」)。执行桥把本函数
|
|
585
|
+
的返回值放进裁决返回体,`mdcos._record_decision` 再把它落进 decisions.jsonl
|
|
586
|
+
的 rec["mutation"]——回滚命令(CLI)据此读回前像与动作类。
|
|
587
|
+
"""
|
|
588
|
+
out = dict(payload or {})
|
|
589
|
+
out["kind"] = KIND_MUTATION
|
|
590
|
+
out["executed"] = True
|
|
591
|
+
if before is not None:
|
|
592
|
+
out["before"] = before
|
|
593
|
+
if impact is not None:
|
|
594
|
+
out["impact"] = impact
|
|
595
|
+
if rollback:
|
|
596
|
+
out["rollback"] = rollback
|
|
597
|
+
return out
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# 生效条件:target 非空(否则抛 ValueError)时,对「动作类 + 目标 + 后像」三者的规范化拼接取 sha1 前 32 位十六进制返回;同(动作类+目标+后像)恒得同键、任一不同即得不同键(幂等对账键,形状沿用 propose 的 payload_hash);
|
|
601
|
+
def mutation_dedup_key(action_class, target, after) -> str:
|
|
602
|
+
"""变更单幂等键:**同(动作类+目标+后像)重复提议不长第二条**。
|
|
603
|
+
|
|
604
|
+
为什么不能沿用 propose 的 `_sig(content)`:它是**内容**签名,不带目标——
|
|
605
|
+
「同一份新正文并入节点 X」与「并入节点 Y」会撞成同一条(第二条被幂等
|
|
606
|
+
对账吃掉、或裁决到错的目标)。故键面显式含动作类与目标;形状仍是 sha1
|
|
607
|
+
十六进制(与 payload_hash 同形,`_inbox_phash_index`/`_cascade_dedup`
|
|
608
|
+
照旧按字符串比对,无需认识本键的构造)。
|
|
609
|
+
"""
|
|
610
|
+
tgt = str(target or "").strip()
|
|
611
|
+
if not tgt:
|
|
612
|
+
raise ValueError("变更单缺目标节点 id:幂等键无法构造——fail-closed。")
|
|
613
|
+
act = str(action_class or "").strip().upper()
|
|
614
|
+
text = "%s\x1f%s\x1f%s" % (act, tgt, after if after is not None else "")
|
|
615
|
+
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:32]
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
# 生效条件:cg 具可调用的 propose、target 非空时,按 mutation_payload 组载荷、按 mutation_dedup_key 组幂等键,调**既有入队单点** cg.propose(kind=KIND_MUTATION、内容=后像、携带载荷进 extra[MUTATION_SLOT]),返回 propose 的结果(info 为真时为字典,否则为 pid 字符串);
|
|
619
|
+
def propose_mutation(cg, action_class, target, after="", reason="",
|
|
620
|
+
layer="knowledge", sensitivity=None, primitive="",
|
|
621
|
+
meta=None, info=False, extra=None, payload=None, **kw):
|
|
622
|
+
"""把一张变更单交给**既有入队单点** `cg.propose`(设计 §四:不新建第二套队列)。
|
|
623
|
+
|
|
624
|
+
载荷落 `extra[MUTATION_SLOT]`(沿用 `extra.defer_reason` 先例);类型字段
|
|
625
|
+
`kind` 落 rec 顶层(设计 §四 明文的类型字段)。目标 id 同时占 propose 的
|
|
626
|
+
既有 `node_id` 槽、后像占 `content` 槽,故 `review_list` 等既有读取面
|
|
627
|
+
不用认识本模块也能看到「改哪个节点、改成什么」。
|
|
628
|
+
|
|
629
|
+
payload —— 已构造好的载荷(调用方若是先建载荷再出单,可传入以免构造两遍;
|
|
630
|
+
缺省 None 时按上面的具名参数现构造)。
|
|
631
|
+
"""
|
|
632
|
+
pay = payload or mutation_payload(action_class, target, after=after,
|
|
633
|
+
reason=reason, primitive=primitive,
|
|
634
|
+
meta=meta)
|
|
635
|
+
ex = dict(extra or {})
|
|
636
|
+
ex.update(kw)
|
|
637
|
+
ex[MUTATION_SLOT] = pay
|
|
638
|
+
return cg.propose(pay["target"], pay["after"], layer=layer,
|
|
639
|
+
sensitivity=sensitivity, kind=KIND_MUTATION,
|
|
640
|
+
dedup_key=mutation_dedup_key(pay["action"], pay["target"],
|
|
641
|
+
pay["after"]),
|
|
642
|
+
info=info, **ex)
|