@furongjun1999/dsh-memory 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -18
- package/docs/README.md +1 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
- package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
- package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
- package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
- package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
- package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
- package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
- package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
- package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +527 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +191 -26
- package/md_cg/mdcg.py +594 -39
- package/md_cg/mdcos.py +869 -38
- package/md_cg/nodefile.py +74 -1
- package/md_cg/protect.py +79 -4
- package/md_cg/provenance.py +1 -1
- package/md_cg/review_cli.py +31 -2
- package/md_cg/rollback.py +411 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_autonomy_admission.py +1098 -0
- package/md_cg/test_autonomy_modes.py +1972 -0
- package/md_cg/test_b1_auto_id_multiproc.py +7 -0
- package/md_cg/test_b1b2_write_face.py +7 -0
- package/md_cg/test_b3_merge_keeps_content.py +7 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_i50a_half_dup_defer.py +408 -0
- package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
- package/md_cg/test_i50c_meta_passthrough.py +608 -0
- package/md_cg/test_i50d_importance_source.py +531 -0
- package/md_cg/test_i50e_readside_protection.py +656 -0
- package/md_cg/test_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -0
- package/md_cg/test_linkref.py +7 -0
- package/md_cg/test_mode_parity.py +1393 -0
- package/md_cg/test_mutation_rollback.py +805 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9_forget_protect.py +7 -0
- package/md_cg/test_p9c_dedup_hints.py +7 -0
- package/md_cg/test_policy_required_ccg.py +5 -1
- package/md_cg/test_protocol.py +7 -0
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/writepipe.py +101 -2
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""三档自治 · 回滚原语通用化(设计 v0.2 §四/§十一 · 批次③)。
|
|
3
|
+
|
|
4
|
+
设计 §四「回滚原语(已有,需通用化)」四条在本文档与实现中的落点:
|
|
5
|
+
|
|
6
|
+
1. **受保护快照面推广为一切 C/D/B 动作**:`_protected_history/<id>/<时间戳>.md`
|
|
7
|
+
+ `_protected_audit.jsonl`(protect.py 面)**面复用、语义泛化**——执行桥
|
|
8
|
+
经 `preimage()` 在**执行时点**调 `protect.snapshot_preimage()` 拍前像;
|
|
9
|
+
`protect.py` 的既有 `snapshot()` / `guard_*` 行为**一字不改**(守卫钉死)。
|
|
10
|
+
2. **软删(D)**:`trash/` + 删除清单的既有 `restore` 接进统一口径——
|
|
11
|
+
`rollback_mutation` 的 D 分支调 `cg.restore(force=True)`;restore 走 add
|
|
12
|
+
全量重建,探针实测与原件有字节差(多 `sensitivity: null` 键),故其后按
|
|
13
|
+
前像**校准**(`calibrated` 读数如实返回,不假装 restore 本身逐字节)。
|
|
14
|
+
3. **合并(B)**:聚合行 `- 【聚合 …】`(`forgetting.AGG_MARK`)**定向剥离**
|
|
15
|
+
(`strip_aggregate_lines`)回退到合并前正文;fm 全量取前像——
|
|
16
|
+
merge_count/last_merge_at/importance/merge_sources/protected 等**不可逆**
|
|
17
|
+
(`-0.05` 会被 `min(1.0,…)` 截断、last_merge_at 旧值无从得知),前像快照
|
|
18
|
+
是唯一诚实来源。
|
|
19
|
+
4. **分数类(E)**:不属本批(设计 §四 明文「分数类:各模块既有 rollback」)。
|
|
20
|
+
|
|
21
|
+
统一回滚入口 `rollback_mutation(cg, mutation, ...)`:输入=变更单的**执行时点
|
|
22
|
+
载荷**(执行桥补全后的 `mutation_executed` 形态;CLI 从 decisions.jsonl 的
|
|
23
|
+
`rec["mutation"]` 读回),按动作类分派 C/B/D 三条原语,回滚后做**残留核对**
|
|
24
|
+
(边/索引条目/聚合行三面与前像时点影响面逐位对拍)与**逐字节比对前像**读数。
|
|
25
|
+
|
|
26
|
+
边界(如实登记):
|
|
27
|
+
· 前像=执行时点快照——提议时点**不拍**(设计 §四:从提议到 accept 之间目标
|
|
28
|
+
可能被第三方改动,回滚必须撤销本变更本身);执行记录里的 `before` 引用由
|
|
29
|
+
执行桥补全,故对**未执行**(reject/fail-closed/pending)的变更单,本模块
|
|
30
|
+
fail-closed 拒绝回滚(`rollback_gap`,不允许凭空猜一个前像)。
|
|
31
|
+
· 加密库(覆写 `_seal_content` 的实例)密文层不保证逐字节可复现(每次封装
|
|
32
|
+
熵不同)——`bytes_equal_preimage` 在非加密库上成立(探针实测);
|
|
33
|
+
加密库语义为「明文内容 + 结构可恢复」,读数如实返回。
|
|
34
|
+
· `restore` 的 tombstone 检查保留语义:回滚是「确认该删除属于本变更」的
|
|
35
|
+
处置,故显式 `force=True` 且 `reason` 写入返回体,不静默绕过。
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
|
|
41
|
+
from . import chain
|
|
42
|
+
from . import forgetting as _forgetting
|
|
43
|
+
from . import nodefile
|
|
44
|
+
from . import protect
|
|
45
|
+
from . import subgraph as _subgraph
|
|
46
|
+
from . import trust
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"RollbackError", "PreimageError",
|
|
50
|
+
"command_for", "preimage", "collect_impact", "read_preimage",
|
|
51
|
+
"strip_aggregate_lines", "rollback_mutation",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
#: 影响面「索引条目」对拍键(`collect_impact` 采集、残留核对比对,同一份口径)。
|
|
55
|
+
_INDEX_KEYS = ("present", "path", "layer", "importance", "merge_count",
|
|
56
|
+
"protected", "tags")
|
|
57
|
+
#: 需要前像/可回滚的动作类(设计 §一:B 合并 / C 改写 / D 删除)。
|
|
58
|
+
_ROLLBACK_ACTIONS = ("B", "C", "D")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RollbackError(RuntimeError):
|
|
62
|
+
"""回滚无法执行(前像缺失/不可读、动作类未知等)——fail-closed。"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, code, message, **extra):
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
self.code = code
|
|
67
|
+
self.extra = extra
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PreimageError(RuntimeError):
|
|
71
|
+
"""执行时点前像拍摄失败——执行桥据此 fail-closed(一切破坏性动作先留前像)。"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# 生效条件:pid 非空时返回 `python -X utf8 -m md_cg.rollback_cli --root "<root>" --pid <pid>` 形态的命令串(唯一构造点);pid 为空时抛 ValueError;
|
|
75
|
+
def command_for(root, pid) -> str:
|
|
76
|
+
"""回滚命令串(**唯一构造点**,可执行——守卫实测演练同一条串)。
|
|
77
|
+
|
|
78
|
+
形态自定为 `md_cg.rollback_cli`:CLI 从 decisions.jsonl 读回该 pid 的
|
|
79
|
+
执行记录(`rec["mutation"]`,含执行时点前像/影响面/回滚命令),按动作类
|
|
80
|
+
分派统一回滚入口。root 用双引号包裹(路径可含空格)。
|
|
81
|
+
"""
|
|
82
|
+
if not pid:
|
|
83
|
+
raise ValueError("回滚命令缺 pid:无 pid 即无法定位执行记录——"
|
|
84
|
+
"fail-closed 不构造命令串。")
|
|
85
|
+
return ('python -X utf8 -m md_cg.rollback_cli --root "%s" --pid %s'
|
|
86
|
+
% (root, pid))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# 生效条件:action 归一后属 _ROLLBACK_ACTIONS 时——forgetting.prior_node 判目标不存在则返回 None(动作不会发生,由调用方走既有 target_missing/not_found 路径);目标在位于执行时点拍前像(protect.snapshot_preimage)并采集影响面与回滚命令,返回 {"before","impact","rollback"};拍摄失败(目标在位而快照未落)抛 PreimageError(fail-closed);action 不属 _ROLLBACK_ACTIONS 时返回 None;
|
|
90
|
+
def preimage(cg, action, target, pid=None, reason=""):
|
|
91
|
+
"""执行时点前像(快照引用 + 影响面 + 回滚命令)——**唯一采集点**。
|
|
92
|
+
|
|
93
|
+
由执行桥(`mdcos._mutation_execute`)在动作原语落盘**之前**调用:
|
|
94
|
+
拍到的快照即「执行前一刻」的盘面(含提议之后、执行之前的第三方改动),
|
|
95
|
+
回滚据此撤销本变更本身(设计 §四:前像=执行时点,不是提议时点)。
|
|
96
|
+
|
|
97
|
+
返回 None 的两条路径(都=动作不会发生,无需留前像):
|
|
98
|
+
· 动作类不属 B/C/D(未知动作类由执行桥的既有分支 fail-closed);
|
|
99
|
+
· 目标节点不存在(既有原语会走 target_missing/not_found,执行桥的
|
|
100
|
+
C 分支另有显式 target_missing 检查——本函数不抢它的错误形态)。
|
|
101
|
+
"""
|
|
102
|
+
act = str(action or "").strip().upper()
|
|
103
|
+
if act not in _ROLLBACK_ACTIONS:
|
|
104
|
+
return None
|
|
105
|
+
if _forgetting.prior_node(cg, target) is None:
|
|
106
|
+
return None
|
|
107
|
+
rel = protect.snapshot_preimage(cg, target, action=act, pid=pid,
|
|
108
|
+
reason=reason)
|
|
109
|
+
if rel is None:
|
|
110
|
+
raise PreimageError(
|
|
111
|
+
"执行时点前像拍摄失败(目标 %s 在位但快照未落盘)——"
|
|
112
|
+
"一切破坏性动作先留前像(设计 §四/§六),本次 fail-closed 未执行。"
|
|
113
|
+
% target)
|
|
114
|
+
return {"before": rel,
|
|
115
|
+
"impact": collect_impact(cg, target),
|
|
116
|
+
"rollback": command_for(cg.root, pid)}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# 生效条件:对任意 cg、node_id 均返回影响面读数 dict——{"edges": {"declared","children","parents"}, "index": {"present","path","layer","importance","merge_count","protected","tags"}, "agg_lines": [...]}(节点不可读时各面取空值/None,不抛);本函数只读,不产生副作用;
|
|
120
|
+
def collect_impact(cg, node_id):
|
|
121
|
+
"""执行时点影响面读数(设计 §四:边 / 索引条目 / 聚合行)。
|
|
122
|
+
|
|
123
|
+
三个面都是**执行时点**(动作落盘前)的读数——回滚后由
|
|
124
|
+
`rollback_mutation` 重采一次逐位对拍(残留核对:回滚后不得残留本变更
|
|
125
|
+
在任一面的痕迹)。采集走既有单点:`subgraph.children/parents_index`
|
|
126
|
+
(声明式 ∪ 边式)、索引条目直读、聚合行按 `forgetting.AGG_MARK` 字面判。
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
node = cg.get(node_id)
|
|
130
|
+
except Exception: # noqa: BLE001 —— 读面失败按空读数采
|
|
131
|
+
node = None
|
|
132
|
+
fm = (node or {}).get("frontmatter") or {}
|
|
133
|
+
content = (node or {}).get("content") or ""
|
|
134
|
+
entry = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id) \
|
|
135
|
+
or {}
|
|
136
|
+
return {
|
|
137
|
+
"edges": {
|
|
138
|
+
"declared": list(fm.get("edges") or []),
|
|
139
|
+
"children": _subgraph.children(cg, node_id),
|
|
140
|
+
"parents": list(_subgraph.parents_index(cg).get(node_id) or []),
|
|
141
|
+
},
|
|
142
|
+
"index": {
|
|
143
|
+
"present": bool(entry),
|
|
144
|
+
"path": entry.get("path"),
|
|
145
|
+
"layer": entry.get("layer"),
|
|
146
|
+
"importance": entry.get("importance"),
|
|
147
|
+
"merge_count": entry.get("merge_count"),
|
|
148
|
+
"protected": bool(entry.get("protected")),
|
|
149
|
+
"tags": list(entry.get("tags") or []),
|
|
150
|
+
},
|
|
151
|
+
# 聚合行行号(1 起):正文里字面含 AGG_MARK 的行——forgetting.aggregate_line
|
|
152
|
+
# 生成的形态是 `- 【聚合 月-日 时:分】…`;宽判据(含标记即计)与回滚核对同源。
|
|
153
|
+
"agg_lines": [i + 1 for i, l in enumerate(content.splitlines())
|
|
154
|
+
if _forgetting.AGG_MARK in l],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# 生效条件:rel 指向的文件不存在时抛 RollbackError(code="preimage_missing");存在则读取并以 cg._open_content(node_id, fm, raw) 对称解封,返回 (fm, content)——content 为 None(无密钥/身份不符)时抛 RollbackError(code="preimage_unreadable");
|
|
159
|
+
def read_preimage(cg, node_id, rel):
|
|
160
|
+
"""读执行时点前像文件(**与 snapshot_preimage 对称**:写走 _write_node 封装、
|
|
161
|
+
读走 _open_content 解封)——返回 (fm, content)。
|
|
162
|
+
|
|
163
|
+
注意:快照文件以节点文件同款 nodefile 序列化落盘;加密库中其内容为密封
|
|
164
|
+
形态,必须经解封钩子读,直接 nodefile.loads 会把密文当明文再次封装
|
|
165
|
+
(双重封装)。非加密库两钩子恒等(探针实测逐字节一致)。
|
|
166
|
+
"""
|
|
167
|
+
p = os.path.join(cg.root, str(rel).replace("/", os.sep))
|
|
168
|
+
if not os.path.isfile(p):
|
|
169
|
+
raise RollbackError("preimage_missing",
|
|
170
|
+
"前像文件不存在:%s——变更单可能未执行(无前像)"
|
|
171
|
+
"或快照被移走;fail-closed 不猜一个前像。" % rel)
|
|
172
|
+
with open(p, encoding="utf-8") as f:
|
|
173
|
+
fm, raw = nodefile.loads(f.read())
|
|
174
|
+
content = cg._open_content(node_id, fm, raw)
|
|
175
|
+
if content is None:
|
|
176
|
+
raise RollbackError("preimage_unreadable",
|
|
177
|
+
"前像不可解封(无密钥/身份不符):%s。" % rel)
|
|
178
|
+
return fm, content
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# 生效条件:content 以 reference 为前缀且其后追加段逐段为「\n + 含 AGG_MARK 的非空行」时,逐段剥掉该追加段返回 (reference + 剩余段, 被剥行列表);不以 reference 为前缀、追加段形态不符、或 reference 为空时原样返回 (content, [])——不猜、不做字符串手术;
|
|
182
|
+
def strip_aggregate_lines(content, reference):
|
|
183
|
+
"""定向剥离(设计 §四:合并回滚)——把**合并追加的聚合行**从正文里剥掉。
|
|
184
|
+
|
|
185
|
+
为什么是「前缀 + 追加段」判据而不是全局扫行:合并原语(`forgetting.
|
|
186
|
+
reinforce` / `writelimit.converge_into`)对正文是**追加式**的
|
|
187
|
+
(`aggregate_line`:`new_body = body + "\\n" + line`),回滚的目标是回到
|
|
188
|
+
前像正文——精确、可判、不误伤正文里历史遗留的聚合行(它们在前像里就已
|
|
189
|
+
存在,属「合并前正文」的一部分,不该动)。非追加式形态(第三方改写等)
|
|
190
|
+
一律不手术:返回原 content 与空剥离表,由调用方按读数决策。
|
|
191
|
+
|
|
192
|
+
返回 `(new_content, removed)`;追加段完全剥净时 `new_content == reference`
|
|
193
|
+
(正常回滚场景,逐字节)。判据与 `forgetting.aggregate_line` 同款字面
|
|
194
|
+
(`AGG_MARK` 常量单点在 forgetting.py,不另写第二份)。
|
|
195
|
+
"""
|
|
196
|
+
text = content or ""
|
|
197
|
+
ref = reference or ""
|
|
198
|
+
if not ref or not text.startswith(ref):
|
|
199
|
+
return text, []
|
|
200
|
+
tail = text[len(ref):]
|
|
201
|
+
removed = []
|
|
202
|
+
while tail.startswith("\n"):
|
|
203
|
+
nl = tail.find("\n", 1)
|
|
204
|
+
line = tail[1:] if nl < 0 else tail[1:nl]
|
|
205
|
+
if not line.strip() or _forgetting.AGG_MARK not in line:
|
|
206
|
+
break # 空行/非聚合行:停止(不越界手术)
|
|
207
|
+
removed.append(line)
|
|
208
|
+
tail = "" if nl < 0 else tail[nl:]
|
|
209
|
+
# 收尾:追加段的**纯换行尾**(`nodefile.dumps:313` 给不以换行结尾的正文
|
|
210
|
+
# 补过一个尾换行,读回时即多出——探针实测:合并后读回形态为
|
|
211
|
+
# `前像 + "\n" + 聚合行 + "\n"`)在已剥过行时一并剥净,使正常场景逐字节
|
|
212
|
+
# 回到前像;未剥过行(removed 为空)不动分毫。
|
|
213
|
+
if removed and not tail.strip("\n"):
|
|
214
|
+
tail = ""
|
|
215
|
+
return ref + tail, removed
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# 生效条件:把 fm/content 以 _write_node 落盘到既有 path,随后按 _node_entry(与 _scan_nodes 同源)重算索引条目写入 index["nodes"] 与 _dirty(索引持久化靠 _dirty→flush→_index_log 重放),并失效 subgraph/chain/trust 三个反查缓存;本函数只被本模块的回滚分支调用;
|
|
219
|
+
def _land(cg, node_id, path, fm, content):
|
|
220
|
+
"""回滚的**唯一落盘口**:写盘 + 索引重算 + 缓存失效(三步同口径)。
|
|
221
|
+
|
|
222
|
+
与 `mdcos.review_decide` merge 分支的定向 upsert 同款(`_node_entry`
|
|
223
|
+
与 `_scan_nodes` 同源,字段集零漂移);走 `_dirty`(**不走 `_stage`**
|
|
224
|
+
——它会虚增 bucket 计数,目标已在索引)。
|
|
225
|
+
"""
|
|
226
|
+
cg._write_node(node_id, path, fm, content)
|
|
227
|
+
layer = str(fm.get("layer") or "knowledge")
|
|
228
|
+
try:
|
|
229
|
+
e = cg._node_entry(path, layer, fm, content)
|
|
230
|
+
except Exception: # noqa: BLE001 —— 重算失败不阻断回滚落盘
|
|
231
|
+
e = None
|
|
232
|
+
if e is not None:
|
|
233
|
+
cg.index["nodes"][node_id] = e
|
|
234
|
+
cg._dirty[node_id] = e
|
|
235
|
+
_subgraph.invalidate_cache(cg)
|
|
236
|
+
chain.invalidate_cache(cg)
|
|
237
|
+
trust.invalidate_cache(cg)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# 生效条件:node_id 的索引条目存在时返回其盘面绝对路径;不存在或条目无 path 时返回 None;
|
|
241
|
+
def _disk_path(cg, node_id):
|
|
242
|
+
e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(node_id) or {}
|
|
243
|
+
p = e.get("path")
|
|
244
|
+
return os.path.join(cg.root, p) if p else None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# 生效条件:对任意路径返回 bytes(不存在/不可读时返回 None——比对判据按 None 处理,不抛);
|
|
248
|
+
def _bytes_of(path):
|
|
249
|
+
if not path:
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
with open(path, "rb") as f:
|
|
253
|
+
return f.read()
|
|
254
|
+
except OSError:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# 生效条件:path 与 rel 都取到字节时返回两者逐字节相等的布尔;任一侧字节取不到时返回 False(不可证即不认——比对判据 fail-closed);
|
|
259
|
+
def _bytes_equal(path, rel_path):
|
|
260
|
+
a, b = _bytes_of(path), _bytes_of(rel_path)
|
|
261
|
+
return a is not None and b is not None and a == b
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# 生效条件:C 改写回滚——索引中节点在位(不在位返回 ok False error target_missing)时把前像 (fm0,c0) 落到节点盘面 path(_land)+ flush,返回读数 dict;
|
|
265
|
+
def _rollback_rewrite(cg, tgt, fm0, c0):
|
|
266
|
+
path = _disk_path(cg, tgt)
|
|
267
|
+
if path is None or _forgetting.prior_node(cg, tgt) is None:
|
|
268
|
+
return {"ok": False, "error": "target_missing",
|
|
269
|
+
"detail": "C 回滚:目标 %s 不在索引中——本变更之外还有变化,"
|
|
270
|
+
"fail-closed 不猜落点(如属误删可先 restore)。" % tgt}
|
|
271
|
+
_land(cg, tgt, path, fm0, c0)
|
|
272
|
+
cg.flush()
|
|
273
|
+
return {"ok": True, "restored_path": os.path.relpath(path, cg.root)
|
|
274
|
+
.replace("\\", "/"), "method": "preimage"}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# 生效条件:B 合并回滚——目标在位(不在位返回 ok False error target_missing)时先按 strip_aggregate_lines 剥离追加的聚合行,剥离结果与前像正文逐字节相等则正文取剥离结果、否则取前像正文(stripped_ok 如实记录),fm 全量取前像 (fm0),经 _land 落盘 + flush;返回读数 dict(removed_lines/stripped_ok/method);
|
|
278
|
+
def _rollback_merge(cg, tgt, fm0, c0):
|
|
279
|
+
cur = None
|
|
280
|
+
try:
|
|
281
|
+
cur = cg.get(tgt)
|
|
282
|
+
except Exception: # noqa: BLE001
|
|
283
|
+
cur = None
|
|
284
|
+
if not cur:
|
|
285
|
+
return {"ok": False, "error": "target_missing",
|
|
286
|
+
"detail": "B 回滚:目标 %s 不可读——本变更之外还有变化,"
|
|
287
|
+
"fail-closed 不猜落点。" % tgt}
|
|
288
|
+
path = _disk_path(cg, tgt)
|
|
289
|
+
if path is None:
|
|
290
|
+
return {"ok": False, "error": "target_missing",
|
|
291
|
+
"detail": "B 回滚:目标 %s 索引条目缺 path。" % tgt}
|
|
292
|
+
c1 = cur.get("content") or ""
|
|
293
|
+
stripped, removed = strip_aggregate_lines(c1, c0)
|
|
294
|
+
stripped_ok = stripped == c0
|
|
295
|
+
# 正文取剥离结果(正常场景逐字节==前像);剥离不精确(追加段含非聚合行等)
|
|
296
|
+
# 时以快照正文为准——回滚的判据是「回到前像」,stripped_ok 如实记录。
|
|
297
|
+
# fm 全量取前像:merge_count/last_merge_at/importance(+0.05 可被 1.0 截断)/
|
|
298
|
+
# merge_sources/protected/lifecycle_state 等均不可逆,前像是唯一诚实来源。
|
|
299
|
+
_land(cg, tgt, path, fm0, stripped if stripped_ok else c0)
|
|
300
|
+
cg.flush()
|
|
301
|
+
return {"ok": True, "restored_path": os.path.relpath(path, cg.root)
|
|
302
|
+
.replace("\\", "/"),
|
|
303
|
+
"method": "strip_aggregate_lines" if stripped_ok else "preimage",
|
|
304
|
+
"stripped_ok": stripped_ok, "removed_lines": removed}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# 生效条件:D 删除回滚——先调既有 cg.restore(tgt, force=True)(trash + 删除清单口径);restore 失败且(错误非 not_in_trash 或节点仍不在库)时返回 ok False(detail 带 restore 原返回体);restore 成功、或 not_in_trash 而节点已在库(此前已回滚过=幂等重放)时继续:按前像做逐字节校准(盘面与快照字节不等时经 _land 以 (fm0,c0) 重写,calibrated=True 如实记录——探针实测 restore 重建会多 sensitivity:null 键),返回读数 dict(replayed 标注重放);
|
|
308
|
+
def _rollback_delete(cg, tgt, fm0, c0, rel):
|
|
309
|
+
res = cg.restore(tgt, force=True)
|
|
310
|
+
replayed = (not res.get("ok")) and res.get("error") == "not_in_trash" \
|
|
311
|
+
and _forgetting.prior_node(cg, tgt) is not None
|
|
312
|
+
if not res.get("ok") and not replayed:
|
|
313
|
+
return {"ok": False, "error": res.get("error") or "restore_failed",
|
|
314
|
+
"detail": {"restore": res},
|
|
315
|
+
"hint": "D 回滚走既有 restore(trash + 删除清单)——失败即"
|
|
316
|
+
"未恢复;如 trash 缺失可核对前像目录。"}
|
|
317
|
+
path = _disk_path(cg, tgt)
|
|
318
|
+
if path is None:
|
|
319
|
+
return {"ok": False, "error": "target_missing",
|
|
320
|
+
"detail": "D 回滚:restore 返回 ok 但索引无条目。"}
|
|
321
|
+
pre_p = os.path.join(cg.root, str(rel).replace("/", os.sep))
|
|
322
|
+
restore_bytes_equal = _bytes_equal(path, pre_p)
|
|
323
|
+
calibrated = False
|
|
324
|
+
if not restore_bytes_equal:
|
|
325
|
+
# 校准(如实):restore 的 add 全量重建与快照存在字节差(实测 =
|
|
326
|
+
# 多 `sensitivity: null` 键)——回滚的判据是「回到前像」,故以快照
|
|
327
|
+
# 重写一次;`restore_bytes_equal=False` 读数保留,不假装 restore
|
|
328
|
+
# 本身逐字节。
|
|
329
|
+
_land(cg, tgt, path, fm0, c0)
|
|
330
|
+
calibrated = True
|
|
331
|
+
cg.flush()
|
|
332
|
+
return {"ok": True, "restored_path": os.path.relpath(path, cg.root)
|
|
333
|
+
.replace("\\", "/"),
|
|
334
|
+
"method": "restore+preimage_calibration" if calibrated
|
|
335
|
+
else "restore",
|
|
336
|
+
"replayed": replayed,
|
|
337
|
+
"calibrated": calibrated,
|
|
338
|
+
"restore_bytes_equal": restore_bytes_equal}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# 生效条件:before_impact 与 after 两个影响面 dict(缺面按空 dict)逐面返回对拍布尔——{"agg_lines_restored","edges_restored","index_restored"};
|
|
342
|
+
def _residual(before_impact, after):
|
|
343
|
+
bi = before_impact if isinstance(before_impact, dict) else {}
|
|
344
|
+
return {
|
|
345
|
+
"agg_lines_restored": (after.get("agg_lines") or [])
|
|
346
|
+
== (bi.get("agg_lines") or []),
|
|
347
|
+
"edges_restored": (after.get("edges") or {}) == (bi.get("edges") or {}),
|
|
348
|
+
"index_restored": {k: (after.get("index") or {}).get(k)
|
|
349
|
+
for k in _INDEX_KEYS}
|
|
350
|
+
== {k: (bi.get("index") or {}).get(k)
|
|
351
|
+
for k in _INDEX_KEYS},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# 生效条件:mutation 为 dict 且含非空 target 与 before(否则返回 ok False error mutation_missing/rollback_gap 带 hint);before 文件不可读按 RollbackError(code) 返回 ok False;动作类属 C/B/D 时按对应分支回滚,成功再重采影响面做残留对拍并做逐字节比对,返回 {"ok": True, "action","target","preimage","restored_path","bytes_equal_preimage","residual","method",...};动作类不属 C/B/D 时返回 ok False error rollback_action_unknown;本函数不抛(可预期错误一律返回体);
|
|
356
|
+
def rollback_mutation(cg, mutation, reason="", actor=None):
|
|
357
|
+
"""统一回滚入口(设计 §四 四条:C/B/D 分派 + 残留核对 + 逐字节比对)。
|
|
358
|
+
|
|
359
|
+
输入=**执行时点载荷**(`autonomy_modes.mutation_executed` 形态,含
|
|
360
|
+
action/target/before/impact/rollback);CLI 从 decisions.jsonl 的
|
|
361
|
+
`rec["mutation"]` 读回。`reason`/`actor` 进返回体(审计由各分支的
|
|
362
|
+
落盘/restore 留痕承担,本模块不另写第二套台账)。
|
|
363
|
+
"""
|
|
364
|
+
if not isinstance(mutation, dict):
|
|
365
|
+
return {"ok": False, "error": "mutation_missing",
|
|
366
|
+
"hint": "回滚入口需要变更单的执行时点载荷(dict)——"
|
|
367
|
+
"decisions.jsonl 的 rec[\"mutation\"]。"}
|
|
368
|
+
act = str(mutation.get("action") or "").strip().upper()
|
|
369
|
+
tgt = str(mutation.get("target") or "").strip()
|
|
370
|
+
rel = mutation.get("before")
|
|
371
|
+
if not tgt:
|
|
372
|
+
return {"ok": False, "error": "mutation_missing",
|
|
373
|
+
"hint": "载荷缺 target——无法定位回滚对象。"}
|
|
374
|
+
if not rel:
|
|
375
|
+
return {"ok": False, "error": "rollback_gap",
|
|
376
|
+
"hint": "载荷缺 before(执行时点前像引用):该变更单**未执行**"
|
|
377
|
+
"(reject / fail-closed / pending)或执行记录不完整——"
|
|
378
|
+
"fail-closed 不猜一个前像(前像必须来自执行时点)。"}
|
|
379
|
+
if act not in _ROLLBACK_ACTIONS:
|
|
380
|
+
return {"ok": False, "error": "rollback_action_unknown",
|
|
381
|
+
"action": act or None,
|
|
382
|
+
"hint": "回滚只覆盖 C 改写 / B 合并 / D 删除(设计 §四);"
|
|
383
|
+
"动作类 %r 未定义回滚——fail-closed。" % (act or None)}
|
|
384
|
+
try:
|
|
385
|
+
fm0, c0 = read_preimage(cg, tgt, rel)
|
|
386
|
+
except RollbackError as exc:
|
|
387
|
+
return {"ok": False, "error": exc.code, "detail": str(exc),
|
|
388
|
+
"action": act, "target": tgt, "preimage": rel}
|
|
389
|
+
if act == "C":
|
|
390
|
+
res = _rollback_rewrite(cg, tgt, fm0, c0)
|
|
391
|
+
elif act == "B":
|
|
392
|
+
res = _rollback_merge(cg, tgt, fm0, c0)
|
|
393
|
+
else: # "D"
|
|
394
|
+
res = _rollback_delete(cg, tgt, fm0, c0, rel)
|
|
395
|
+
if not res.get("ok"):
|
|
396
|
+
out = dict(res)
|
|
397
|
+
out.update({"action": act, "target": tgt, "preimage": rel})
|
|
398
|
+
return out
|
|
399
|
+
# 残留核对(设计 §十一 红项面):重采影响面,与前像时点记录逐面对拍。
|
|
400
|
+
after = collect_impact(cg, tgt)
|
|
401
|
+
residual = _residual(mutation.get("impact"), after)
|
|
402
|
+
# 逐字节比对前像(设计 §十一):盘面 vs 快照文件。
|
|
403
|
+
pre_p = os.path.join(cg.root, str(rel).replace("/", os.sep))
|
|
404
|
+
disk_p = _disk_path(cg, tgt)
|
|
405
|
+
out = {"ok": True, "action": act, "target": tgt, "preimage": rel,
|
|
406
|
+
"rollback_command": mutation.get("rollback") or "",
|
|
407
|
+
"reason": str(reason or ""), "actor": actor,
|
|
408
|
+
"bytes_equal_preimage": _bytes_equal(disk_p, pre_p),
|
|
409
|
+
"residual": residual, "impact_after": after}
|
|
410
|
+
out.update(res)
|
|
411
|
+
return out
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""rollback_cli · 变更单回滚命令行(三档自治 · 设计 v0.2 §四「回滚原语四条」)。
|
|
3
|
+
|
|
4
|
+
用法(root 须与执行该变更单的部署一致:--root 或环境变量 MDCG_ROOT):
|
|
5
|
+
python -X utf8 -m md_cg.rollback_cli --root <root> --pid <pid>
|
|
6
|
+
python -X utf8 -m md_cg.rollback_cli --root <root> --pid <pid> --reason "..."
|
|
7
|
+
|
|
8
|
+
本模块是**回滚命令串的载体**:变更单执行时点的 `rollback` 字段即指向本 CLI
|
|
9
|
+
(`md_cg.rollback.command_for` 构造,随执行记录落 decisions.jsonl 的
|
|
10
|
+
`rec["mutation"]["rollback"]`)。CLI 做的事:
|
|
11
|
+
|
|
12
|
+
1. 从 `<root>/hippocampus/decisions.jsonl` 读回该 pid 最后一条
|
|
13
|
+
`decision="accept"` 的裁决记录,取其 `rec["mutation"]`(**执行时点载荷**
|
|
14
|
+
——动作类/目标/前像/影响面/回滚命令由执行桥补全);
|
|
15
|
+
2. 交统一回滚入口 `rollback.rollback_mutation` 按动作类分派:
|
|
16
|
+
· C 改写 → 从执行时点前像恢复(含第三方改动的那个版本);
|
|
17
|
+
· B 合并 → 聚合行定向剥离 + 前像 fm 恢复;
|
|
18
|
+
· D 删除 → 既有 restore(trash + 删除清单口径)+ 前像校准;
|
|
19
|
+
3. 输出 JSON(stdout):`ok` + `bytes_equal_preimage`(逐字节比对前像读数)
|
|
20
|
+
+ `residual`(残留核对:边/索引条目/聚合行三面)。
|
|
21
|
+
|
|
22
|
+
退出码:0 = 回滚完成(读数照实打印,含校准等如实字段);1 = 未回滚
|
|
23
|
+
(错误体照实打印——绝不假装成功)。
|
|
24
|
+
|
|
25
|
+
边界(如实):本 CLI 不做裁决(回滚是执行记录的既有授权动作——它的执行时点
|
|
26
|
+
前像就是执行桥拍的);对**未执行**(reject / fail-closed / pending)的变更单,
|
|
27
|
+
统一入口以 `rollback_gap` fail-closed 拒绝(无前像即不猜)。
|
|
28
|
+
"""
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
36
|
+
except AttributeError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
_HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
40
|
+
if _HERE not in sys.path:
|
|
41
|
+
sys.path.insert(0, _HERE)
|
|
42
|
+
|
|
43
|
+
from md_cg import rollback as _rb # noqa: E402
|
|
44
|
+
from md_cg.mdcos import MdCGOS # noqa: E402
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# 生效条件:args 的 root 属性为真值(含 getattr 缺省 None 时回落)否则回落 os.environ.get("MDCG_ROOT", ""),两者皆空串或该值经 os.path.isdir 判定不是目录时 sys.exit 退出,否则返回该 root;
|
|
48
|
+
def _root(args):
|
|
49
|
+
root = getattr(args, "root", None) or os.environ.get("MDCG_ROOT", "")
|
|
50
|
+
if not root:
|
|
51
|
+
sys.exit("错误:未指定存储根(--root 或环境变量 MDCG_ROOT)。\n"
|
|
52
|
+
"root 必须与执行该变更单的部署一致——猜错会回滚到另一个库。")
|
|
53
|
+
if not os.path.isdir(root):
|
|
54
|
+
sys.exit("错误:root 不存在:%s" % root)
|
|
55
|
+
return root
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# 生效条件:cg 与 pid 就绪时返回 decisions 记录中 pid 匹配、decision=="accept" 且 rec["mutation"] 为 dict 的最后一条记录;无匹配返回 None(调用方 fail-closed);
|
|
59
|
+
def _mutation_record(cg, pid):
|
|
60
|
+
"""从裁决记录读回变更单的执行时点载荷(rec["mutation"])。"""
|
|
61
|
+
picked = None
|
|
62
|
+
for r in cg.decisions():
|
|
63
|
+
if r.get("pid") != pid:
|
|
64
|
+
continue
|
|
65
|
+
if r.get("decision") != "accept":
|
|
66
|
+
continue
|
|
67
|
+
if isinstance(r.get("mutation"), dict):
|
|
68
|
+
picked = r
|
|
69
|
+
return picked
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# 生效条件:argv 为 None(默认)时由 argparse 解析 sys.argv、否则解析传入的 argv(--root/--pid 必填、--reason 可选),解析成功后取 root 构造 MdCGOS(root, autoflush=1),无匹配执行记录时打印 ok False error decision_not_found 并返回 1,否则调 rollback.rollback_mutation 打印其结果 JSON,按其 ok 真值返回 0、假值返回 1;finally 中执行 cg.close();
|
|
73
|
+
def main(argv=None):
|
|
74
|
+
ap = argparse.ArgumentParser(
|
|
75
|
+
prog="python -m md_cg.rollback_cli",
|
|
76
|
+
description="变更单回滚(设计 §四:从执行时点前像恢复)")
|
|
77
|
+
ap.add_argument("--root", help="存储根目录(默认环境变量 MDCG_ROOT)")
|
|
78
|
+
ap.add_argument("--pid", required=True, help="变更单的裁决 pid(accept 记录)")
|
|
79
|
+
ap.add_argument("--reason", default="", help="回滚理由(进返回体)")
|
|
80
|
+
args = ap.parse_args(argv)
|
|
81
|
+
|
|
82
|
+
cg = MdCGOS(_root(args), autoflush=1)
|
|
83
|
+
try:
|
|
84
|
+
rec = _mutation_record(cg, args.pid)
|
|
85
|
+
if rec is None:
|
|
86
|
+
out = {"ok": False, "error": "decision_not_found", "pid": args.pid,
|
|
87
|
+
"hint": "decisions.jsonl 中无该 pid 的 accept 执行记录"
|
|
88
|
+
"(或记录缺 mutation 载荷)——变更单未执行即无可"
|
|
89
|
+
"回滚的前像(fail-closed,不猜)。"}
|
|
90
|
+
print(json.dumps(out, ensure_ascii=False))
|
|
91
|
+
return 1
|
|
92
|
+
out = _rb.rollback_mutation(cg, rec["mutation"], reason=args.reason,
|
|
93
|
+
actor="rollback-cli")
|
|
94
|
+
print(json.dumps(out, ensure_ascii=False, default=str))
|
|
95
|
+
return 0 if out.get("ok") else 1
|
|
96
|
+
finally:
|
|
97
|
+
# 收尾(与 review_cli 同款):回滚落盘经 _dirty→flush→_index_log,
|
|
98
|
+
# 不 close 则「节点在盘上但索引无条目」;库层另有 atexit 兜底,显式
|
|
99
|
+
# 收尾才是正路。
|
|
100
|
+
cg.close()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
sys.exit(main())
|
|
@@ -31,6 +31,28 @@ from . import zh_en_atoms
|
|
|
31
31
|
WIN = 6 # 组合共现窗口(token 距离,探针实证口径)
|
|
32
32
|
_ATOMS_ZH = None
|
|
33
33
|
|
|
34
|
+
# 中文段判据(单点,2026-09-30 检索归一层作用域收窄时立):
|
|
35
|
+
# 中文 = CJK 统一表意文字基本区 U+4E00–U+9FFF。与 en_normalizer 分词字符类
|
|
36
|
+
# `[A-Za-z\u4e00-\u9fff]` 的中文区间同区间;rust/src/text.rs::is_zh 是同判据的
|
|
37
|
+
# Rust 侧单点(两侧归一层共用同一判据;改动须两侧同步,否则 rank_parity 漂移)。
|
|
38
|
+
# 消费方:unify.py::unify_query(中文段逐字保留)、rust/src/atoms.rs::Atoms::unify。
|
|
39
|
+
ZH_LO, ZH_HI = "\u4e00", "\u9fff"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 生效条件:单参 ch 为长度恰为 1 的 str 时返回 ZH_LO <= ch <= ZH_HI(CJK 基本区);长度 != 1(空串/多字符串)一律返回 False(2026-09-30 清理批次把边界收紧为「长度恰为 1」——此前只在 ch 为空串时为假,多字符串按字典序比较可能被误判为真)。
|
|
43
|
+
def is_zh_char(ch):
|
|
44
|
+
"""中文段判据(单点):CJK 统一表意文字基本区 U+4E00–U+9FFF。
|
|
45
|
+
|
|
46
|
+
边界约定(2026-09-30 清理批次乙2):**仅长度恰为 1 的串可为真**——
|
|
47
|
+
空串与长度 >1 的串一律 False。此前只有空串为假(链式比较
|
|
48
|
+
`ZH_LO <= ch` 对空串即假),而多字符串按字典序比较:`is_zh_char("中文")`
|
|
49
|
+
判真(首字符在区间即真)=判据外溢。两侧调用点清点结果:本函数只有
|
|
50
|
+
`unify.py::_runs` 与 `test_unify_scope.py::zh_runs` 两处消费,均逐字符
|
|
51
|
+
传入 ⇒ 本收紧对存量行为零变更;Rust 侧 `text.rs::is_zh` 收 `char`,
|
|
52
|
+
天然无空/多字符二态,边界语义两侧一致(docstring 已互指)。
|
|
53
|
+
"""
|
|
54
|
+
return len(ch) == 1 and ZH_LO <= ch <= ZH_HI
|
|
55
|
+
|
|
34
56
|
|
|
35
57
|
# 生效条件:模块级常量 `_ATOMS_ZH` 为 None 时按 `__file__` 所在目录的 atoms.json 取 `data.get("atoms", [])` 各项 `zh` 建集合并缓存,非 None 时直接返回 `_ATOMS_ZH`。
|
|
36
58
|
def atoms_zh():
|