dsh-config-manager 0.1.76 → 0.1.78
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/lib/adapters/plugin-data-declared.d.ts +12 -0
- package/lib/adapters/plugin-data-declared.js +57 -0
- package/lib/adapters/plugin-data.d.ts +4 -6
- package/lib/adapters/plugin-data.js +8 -36
- package/lib/adapters/plugins.d.ts +16 -0
- package/lib/adapters/plugins.js +223 -16
- package/lib/client.d.ts +104 -9
- package/lib/client.js +1800 -718
- package/lib/core/analyzer.js +3 -1
- package/lib/core/backup.d.ts +9 -0
- package/lib/core/backup.js +16 -1
- package/lib/core/messages.d.ts +11 -0
- package/lib/core/messages.js +22 -0
- package/lib/core/plugin-cli.d.ts +26 -0
- package/lib/core/plugin-cli.js +62 -2
- package/lib/core/rollback-attribution.d.ts +81 -0
- package/lib/core/rollback-attribution.js +236 -0
- package/lib/core/rollback-merge.d.ts +22 -0
- package/lib/core/rollback-merge.js +203 -0
- package/lib/core/rollback-point.d.ts +114 -0
- package/lib/core/rollback-point.js +534 -17
- package/lib/core/types.d.ts +28 -0
- package/lib/index.d.ts +26 -3
- package/lib/index.js +129 -16
- package/lib/routes/recovery.js +11 -0
- package/lib/schema/section-registry.js +5 -5
- package/lib/schema/types.d.ts +14 -0
- package/lib/ui/badge-tone.d.ts +16 -0
- package/lib/ui/badge-tone.js +20 -0
- package/lib/ui/i18n.d.ts +4 -4
- package/lib/ui/i18n.js +8 -8
- package/lib/ui/rollback-view.d.ts +95 -0
- package/lib/ui/rollback-view.js +55 -0
- package/lib/ui/selection-model.d.ts +140 -1
- package/lib/ui/selection-model.js +241 -9
- package/package.json +1 -1
- package/src/adapters/plugin-data-declared.ts +60 -0
- package/src/adapters/plugin-data.ts +7 -33
- package/src/adapters/plugins.test.ts +162 -0
- package/src/adapters/plugins.ts +236 -17
- package/src/adapters/units.test.ts +26 -0
- package/src/client/chat/ChatEntryView.tsx +147 -46
- package/src/client/chat/chat-entry-view.test.ts +277 -1
- package/src/client/chat/chat-entry-view.ts +98 -4
- package/src/client/chat/chat-node-registration.ts +71 -0
- package/src/client/chat/chat-slots.ts +30 -12
- package/src/client/common/ContentPicker.tsx +581 -175
- package/src/client/common/host-primitives.ts +64 -1
- package/src/client/common/ui.tsx +17 -3
- package/src/client/config-manager.module.css +630 -397
- package/src/client/css-scale-guard.test.ts +41 -3
- package/src/client/css-token-guard.test.ts +10 -4
- package/src/client/export/ExportView.tsx +2 -0
- package/src/client/index.ts +15 -3
- package/src/client/locales.ts +54 -10
- package/src/client/recovery/RollbackCard.tsx +56 -2
- package/src/client/recovery/RollbackDiffPane.tsx +5 -0
- package/src/client/recovery/RollbackFileChanges.tsx +46 -2
- package/src/client/recovery/recovery-locales.ts +22 -0
- package/src/client/recovery/rollback-file-changes.test.ts +1 -1
- package/src/core/analyzer.ts +3 -1
- package/src/core/backup.ts +16 -1
- package/src/core/messages.ts +22 -0
- package/src/core/plugin-cli.fs.test.ts +65 -3
- package/src/core/plugin-cli.ts +55 -2
- package/src/core/rollback-attribution.test.ts +120 -0
- package/src/core/rollback-attribution.ts +267 -0
- package/src/core/rollback-merge.test.ts +133 -0
- package/src/core/rollback-merge.ts +213 -0
- package/src/core/rollback-point.test.ts +412 -0
- package/src/core/rollback-point.ts +560 -20
- package/src/core/types.ts +29 -0
- package/src/index.facade.test.ts +60 -0
- package/src/index.ts +133 -19
- package/src/routes/recovery.ts +11 -0
- package/src/schema/section-registry.ts +5 -5
- package/src/schema/types.ts +14 -0
- package/src/ui/badge-tone.test.ts +23 -0
- package/src/ui/badge-tone.ts +27 -0
- package/src/ui/i18n.ts +8 -8
- package/src/ui/rollback-view.test.ts +27 -0
- package/src/ui/rollback-view.ts +139 -0
- package/src/ui/selection-model.test.ts +240 -2
- package/src/ui/selection-model.ts +349 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行级**三方合并**(base / ours / theirs)—— 回退时「撤销本会话的改动、保留别人的改动」的判定内核。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它(2026-10-17 跨对话安全回退):
|
|
5
|
+
* 回滚点的「工作区态」是整棵树的快照,回退若直接整文件覆盖,会把别的对话在留点之后
|
|
6
|
+
* 产生的修改一起抹掉。正确语义是:把 **base→ours**(本会话那次改动的反向)与
|
|
7
|
+
* **base→theirs**(别人后来的改动)同时应用到文件上 —— 即标准三方合并。
|
|
8
|
+
*
|
|
9
|
+
* 三方是谁(由调用方给定,本模块只管算):
|
|
10
|
+
* · `base` = 共同祖先:本会话改动之后、别人的改动之前的那个版本;
|
|
11
|
+
* · `ours` = 回退目标:回滚点里记录的内容(= 本会话改之前的版本);
|
|
12
|
+
* · `theirs` = 当前磁盘上的内容。
|
|
13
|
+
* 结果为 `ours + (theirs - base)`,即撤销本会话、保留别人。
|
|
14
|
+
*
|
|
15
|
+
* 四条硬规矩:
|
|
16
|
+
* ① **不确定就不合并**:任一方向的 diff 被规模预算降级(`degraded`)⇒ 直接拒绝(reason=`degraded`),
|
|
17
|
+
* 绝不拿一个「整块替换」的近似编辑脚本去算合并。
|
|
18
|
+
* ② **冲突绝不自动选边**:两侧改到同一处(含同一插入点)就报冲突,返回 theirs 原文 + `ok:false`,
|
|
19
|
+
* 调用方必须**不写文件**并把冲突交给用户。
|
|
20
|
+
* ③ **二进制不合并**(含 NUL 即二进制,与 git 同口径)。
|
|
21
|
+
* ④ **依赖风险只提示、不假装证明**:合并要删掉的行里的标识符若仍出现在结果中,
|
|
22
|
+
* 给出**启发式**风险提示(`risks`)—— 它不构成「删除不安全」的证明,也绝不因此声称合并正确。
|
|
23
|
+
*/
|
|
24
|
+
import { diffLines, looksBinary } from '../utils/line-diff.js';
|
|
25
|
+
/** 标识符取样用的最小长度(太短的词(i、id)同名词满天飞,只会制造噪声) */
|
|
26
|
+
const RISK_TOKEN_MIN = 4;
|
|
27
|
+
/** 不参与依赖提示的通用词(语言关键字 / 极常见标识符) */
|
|
28
|
+
const RISK_STOPWORDS = new Set([
|
|
29
|
+
'const', 'let', 'var', 'function', 'return', 'import', 'export', 'from', 'class', 'interface', 'type',
|
|
30
|
+
'public', 'private', 'readonly', 'async', 'await', 'true', 'false', 'null', 'undefined', 'this', 'void',
|
|
31
|
+
'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'new', 'throw', 'try', 'catch',
|
|
32
|
+
'string', 'number', 'boolean', 'object', 'Array', 'Promise', 'Record', 'Partial', 'default',
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* 一段文本相对 base 的**变更块**(按 base 的行区间表示)。
|
|
36
|
+
* 返回 null = 该方向的 diff 被降级(规模预算),调用方必须放弃合并。
|
|
37
|
+
*/
|
|
38
|
+
function changeBlocks(base, target) {
|
|
39
|
+
// context=0:只要变更行,块边界自己算(省得剔上下文)
|
|
40
|
+
const diff = diffLines(base, target, { context: 0 });
|
|
41
|
+
if (diff.degraded)
|
|
42
|
+
return null;
|
|
43
|
+
const blocks = [];
|
|
44
|
+
for (const hunk of diff.hunks) {
|
|
45
|
+
// 当前块与「已消耗到 base 的哪一行」
|
|
46
|
+
let block = null;
|
|
47
|
+
let cursor = Math.max(0, hunk.oldStart - 1);
|
|
48
|
+
const flush = () => {
|
|
49
|
+
if (block !== null)
|
|
50
|
+
blocks.push(block);
|
|
51
|
+
block = null;
|
|
52
|
+
};
|
|
53
|
+
for (const line of hunk.lines) {
|
|
54
|
+
if (line.kind === 'context') {
|
|
55
|
+
flush();
|
|
56
|
+
if (line.oldNo !== null)
|
|
57
|
+
cursor = line.oldNo;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (line.kind === 'del') {
|
|
61
|
+
const at = line.oldNo === null ? cursor : line.oldNo - 1;
|
|
62
|
+
if (block === null)
|
|
63
|
+
block = { start: at, end: at, lines: [] };
|
|
64
|
+
block.end = at + 1;
|
|
65
|
+
cursor = at + 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// add:没有 del 时是纯插入(位置 = 当前游标)
|
|
69
|
+
if (block === null)
|
|
70
|
+
block = { start: cursor, end: cursor, lines: [] };
|
|
71
|
+
block.lines.push(line.text);
|
|
72
|
+
}
|
|
73
|
+
flush();
|
|
74
|
+
}
|
|
75
|
+
return blocks;
|
|
76
|
+
}
|
|
77
|
+
/** 两个块在 base 上是否**互相触碰**(保守:含边界与纯插入点) */
|
|
78
|
+
function touches(a, b) {
|
|
79
|
+
const aPoint = a.start === a.end;
|
|
80
|
+
const bPoint = b.start === b.end;
|
|
81
|
+
if (aPoint && bPoint)
|
|
82
|
+
return a.start === b.start;
|
|
83
|
+
// **纯插入点落在区间端点不算冲突**:「删掉 `[s,e)`」与「在 e 处插入」可以共存(先删、再在删除后
|
|
84
|
+
// 的位置插入)。真机 2026-10-17 的「撤销 A 的描述 + 保留 B 的描述」正是这种形状:A 删掉了尾部
|
|
85
|
+
// 一行,B 又在其后追加了一行 —— 旧判据把它们当冲突,于是回退变成「什么都不做」。
|
|
86
|
+
// 只有落在区间**内部**(严格介于 s 与 e 之间)的插入才是真正说不清的冲突。
|
|
87
|
+
if (aPoint)
|
|
88
|
+
return a.start > b.start && a.start < b.end;
|
|
89
|
+
if (bPoint)
|
|
90
|
+
return b.start > a.start && b.start < a.end;
|
|
91
|
+
return a.start < b.end && b.start < a.end;
|
|
92
|
+
}
|
|
93
|
+
/** 两侧的块是否「改成了同一件事」(内容逐行相同即可取其一,不算冲突) */
|
|
94
|
+
function sameChange(a, b) {
|
|
95
|
+
if (a.lines.length !== b.lines.length)
|
|
96
|
+
return false;
|
|
97
|
+
for (let i = 0; i < a.lines.length; i += 1)
|
|
98
|
+
if (a.lines[i] !== b.lines[i])
|
|
99
|
+
return false;
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
/** 从被撤销的行里取「可能仍被引用」的标识符(启发式,见模块头 ④) */
|
|
103
|
+
function riskTokens(lines) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
for (const token of line.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? []) {
|
|
107
|
+
if (token.length < RISK_TOKEN_MIN)
|
|
108
|
+
continue;
|
|
109
|
+
if (RISK_STOPWORDS.has(token))
|
|
110
|
+
continue;
|
|
111
|
+
seen.add(token);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return [...seen];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 三方合并(纯函数、无 I/O)。
|
|
118
|
+
*
|
|
119
|
+
* 语义:结果是 `ours` 上再应用 `(theirs - base)` —— 撤销「我们从 base 改到 ours 的那部分」,
|
|
120
|
+
* 同时保留「别人从 base 改到 theirs 的那部分」。
|
|
121
|
+
*/
|
|
122
|
+
export function mergeThreeWay(ours, base, theirs) {
|
|
123
|
+
const empty = (reason, conflicts = 0, risks = []) => ({
|
|
124
|
+
ok: false, text: theirs, reason, conflicts, revertedLines: 0, risks,
|
|
125
|
+
});
|
|
126
|
+
// 没有基线就退化成「谁的改动都不认识」—— 调用方负责在无 base 时根本不调用本函数
|
|
127
|
+
if (looksBinary(base) || looksBinary(ours) || looksBinary(theirs))
|
|
128
|
+
return empty('binary');
|
|
129
|
+
if (ours === base) {
|
|
130
|
+
// 本会话没改过 ⇒ 结论就是「保持别人现在的内容」(不是失败)
|
|
131
|
+
return { ok: true, text: theirs, reason: 'clean', conflicts: 0, revertedLines: 0, risks: [] };
|
|
132
|
+
}
|
|
133
|
+
if (theirs === base) {
|
|
134
|
+
// 别人没改过 ⇒ 直接回到 ours
|
|
135
|
+
return { ok: true, text: ours, reason: 'clean', conflicts: 0, revertedLines: 0, risks: [] };
|
|
136
|
+
}
|
|
137
|
+
if (ours === theirs) {
|
|
138
|
+
// 双方最终内容一致 ⇒ 谁都不用动
|
|
139
|
+
return { ok: true, text: theirs, reason: 'clean', conflicts: 0, revertedLines: 0, risks: [] };
|
|
140
|
+
}
|
|
141
|
+
const oursBlocks = changeBlocks(base, ours);
|
|
142
|
+
const theirsBlocks = changeBlocks(base, theirs);
|
|
143
|
+
if (oursBlocks === null || theirsBlocks === null)
|
|
144
|
+
return empty('degraded');
|
|
145
|
+
// 冲突扫描:任一 ours 块与任一 theirs 块触碰且内容不同 ⇒ 冲突
|
|
146
|
+
let conflicts = 0;
|
|
147
|
+
for (const a of oursBlocks) {
|
|
148
|
+
for (const b of theirsBlocks) {
|
|
149
|
+
if (!touches(a, b))
|
|
150
|
+
continue;
|
|
151
|
+
if (sameChange(a, b))
|
|
152
|
+
continue;
|
|
153
|
+
conflicts += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (conflicts > 0)
|
|
157
|
+
return empty('conflict', conflicts);
|
|
158
|
+
const baseLines = splitLinesOf(base);
|
|
159
|
+
const merged = [];
|
|
160
|
+
const reverted = [];
|
|
161
|
+
let cursor = 0;
|
|
162
|
+
// 把两侧的块按 base 位置归并(位置相同的「同一改动」只应用一次)
|
|
163
|
+
const all = [
|
|
164
|
+
...oursBlocks.map((block) => ({ block, owner: 'ours' })),
|
|
165
|
+
...theirsBlocks.map((block) => ({ block, owner: 'theirs' })),
|
|
166
|
+
].sort((left, right) => left.block.start - right.block.start);
|
|
167
|
+
for (const item of all) {
|
|
168
|
+
const { block, owner } = item;
|
|
169
|
+
if (block.start < cursor) {
|
|
170
|
+
// 已被同一位置的另一个块消费(只可能是「同一改动」)—— 跳过重复应用
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
for (let i = cursor; i < block.start && i < baseLines.length; i += 1)
|
|
174
|
+
merged.push(baseLines[i]);
|
|
175
|
+
if (owner === 'ours') {
|
|
176
|
+
// 这一段就是「被撤销的本会话改动」:记下它删掉了哪些行,供依赖提示用
|
|
177
|
+
for (let i = block.start; i < block.end && i < baseLines.length; i += 1)
|
|
178
|
+
reverted.push(baseLines[i]);
|
|
179
|
+
}
|
|
180
|
+
for (const line of block.lines)
|
|
181
|
+
merged.push(line);
|
|
182
|
+
cursor = Math.max(cursor, block.end);
|
|
183
|
+
}
|
|
184
|
+
for (let i = cursor; i < baseLines.length; i += 1)
|
|
185
|
+
merged.push(baseLines[i]);
|
|
186
|
+
// 尾换行以**当前磁盘**(theirs)为准:我们是在它的基础上撤销本会话的改动,不擅改它原本的收尾形态
|
|
187
|
+
const text = merged.join('\n') + (merged.length > 0 && theirs.endsWith('\n') ? '\n' : '');
|
|
188
|
+
// 依赖风险(启发式):被撤销的行里出现过、且合并结果里仍能读到的标识符
|
|
189
|
+
const tokens = riskTokens(reverted);
|
|
190
|
+
const risks = tokens.filter((token) => new RegExp('\\b' + token + '\\b').test(text)).slice(0, 8);
|
|
191
|
+
return { ok: true, text, reason: 'clean', conflicts: 0, revertedLines: reverted.length, risks };
|
|
192
|
+
}
|
|
193
|
+
/** 与 `splitLines` 同口径的本地折行(避免为了一个函数再引一层依赖) */
|
|
194
|
+
function splitLinesOf(text) {
|
|
195
|
+
if (text === '')
|
|
196
|
+
return [];
|
|
197
|
+
const normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
198
|
+
const lines = normalized.split('\n');
|
|
199
|
+
if (lines.length > 0 && lines[lines.length - 1] === '')
|
|
200
|
+
lines.pop();
|
|
201
|
+
return lines;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=rollback-merge.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DiffHunk } from '../utils/line-diff.ts';
|
|
2
|
+
import type { RollbackWriteAttributionPort, RollbackWriteRecorder } from './rollback-attribution.ts';
|
|
2
3
|
import { ROLLBACK_DEFAULT_LEDGER_BYTES } from './rollback-ledger.ts';
|
|
3
4
|
export declare const ROLLBACK_SCHEMA_VERSION = 2;
|
|
4
5
|
/** 回滚点库目录名(dataDir/rollback-points)——路由族 /recovery/rollback-points/* 同源 */
|
|
@@ -575,6 +576,23 @@ export interface RollbackPreviewFile {
|
|
|
575
576
|
* **宁可说「不知道」,也绝不谎报行数。**
|
|
576
577
|
*/
|
|
577
578
|
truncated: boolean;
|
|
579
|
+
/**
|
|
580
|
+
* true = 该文件在留点**之后**是**别的会话**写的(写入归属台账证明;见 core/rollback-attribution.ts)。
|
|
581
|
+
* 界面据此默认不勾选它(用户仍可勾上)。缺字段 / false = **没有归属证据**,
|
|
582
|
+
* 而不是「本会话写的」——「未判定」与「确知是本会话」必须分开。
|
|
583
|
+
*/
|
|
584
|
+
foreign?: boolean;
|
|
585
|
+
/** 那些「别的会话」的会话 id(仅当 foreign 为 true) */
|
|
586
|
+
foreignSessionId?: string;
|
|
587
|
+
/** 那次写入的时间(毫秒;仅当 foreign 为 true) */
|
|
588
|
+
foreignAt?: number;
|
|
589
|
+
/**
|
|
590
|
+
* true = 这次回退**不会改动它**:要么与其它对话的改动冲突、要么无法安全合并(缺共同祖先 / 二进制 /
|
|
591
|
+
* 差异被降级 / 对方删了它)。行级统计一律为 null —— 「不改动」不是「0 行变化」。
|
|
592
|
+
*/
|
|
593
|
+
skipped?: boolean;
|
|
594
|
+
/** 不改动的机器原因(界面映射成字典键,绝不裸渲染) */
|
|
595
|
+
mergeReason?: string;
|
|
578
596
|
}
|
|
579
597
|
/** 单文件差异里一侧(当前磁盘 / 回退点快照)的展示信息。 */
|
|
580
598
|
export interface RollbackFileDiffSide {
|
|
@@ -616,6 +634,15 @@ export interface RollbackFileDiffResult {
|
|
|
616
634
|
degraded: boolean;
|
|
617
635
|
/** 无法逐行对比的原因(有值时 hunks 为空) */
|
|
618
636
|
reason?: RollbackFileDiffReason;
|
|
637
|
+
/** true = 「回退后」一侧显示的是**三方合并**的结果,而不是记录里的原文 */
|
|
638
|
+
merged?: boolean;
|
|
639
|
+
/**
|
|
640
|
+
* true = 这次回退**不会改动**它(与其它对话的改动冲突 / 无法安全合并)—— 此时 after 与 before 相同。
|
|
641
|
+
* 界面必须把「不会改动」与「0 行变化」分开说。
|
|
642
|
+
*/
|
|
643
|
+
mergeSkipped?: boolean;
|
|
644
|
+
/** 不改动的机器原因(界面映射成字典键,绝不裸渲染) */
|
|
645
|
+
mergeReason?: string;
|
|
619
646
|
limits: typeof ROLLBACK_FILE_DIFF_LIMITS;
|
|
620
647
|
}
|
|
621
648
|
export interface RollbackPreviewWorkspace {
|
|
@@ -784,6 +811,28 @@ export interface RollbackSegmentResult {
|
|
|
784
811
|
*/
|
|
785
812
|
createdRefusedReason?: 'capture-incomplete';
|
|
786
813
|
writtenBytes?: number;
|
|
814
|
+
/**
|
|
815
|
+
* 因路径选择(onlyPaths)而未恢复 / 未删除的条目数。
|
|
816
|
+
* 与 createdSkipped(安全拒删)**分开**:这里的原因是用户自己没勾它,不是安全判据挡下。
|
|
817
|
+
*/
|
|
818
|
+
selectionSkipped?: number;
|
|
819
|
+
/**
|
|
820
|
+
* 因**归属隔离**(2026-10-17 定案 B)而跳过的记录条目数:该文件在留点之后是**别的会话**写的。
|
|
821
|
+
* 只在**没有显式点名**(`onlyPaths` 缺省)时生效;显式点名 = 用户明确要它,归属不再拦。
|
|
822
|
+
*/
|
|
823
|
+
foreignSkipped?: number;
|
|
824
|
+
/** 三方合并成功、写回「撤销本会话 + 保留对方改动」的条目数(有界清单同给) */
|
|
825
|
+
mergedCount?: number;
|
|
826
|
+
mergedPaths?: string[];
|
|
827
|
+
/** 冲突:两侧改到同一处 ⇒ **未写**,交给用户处理 */
|
|
828
|
+
conflictCount?: number;
|
|
829
|
+
conflictPaths?: string[];
|
|
830
|
+
/** 无法安全撤销(无中间版本 / 二进制 / 差异被降级 / 对方改成了不存在)⇒ **未写** */
|
|
831
|
+
unavailableCount?: number;
|
|
832
|
+
unavailablePaths?: string[];
|
|
833
|
+
/** 写入前发现又被改过(并发)⇒ **未写** */
|
|
834
|
+
concurrentCount?: number;
|
|
835
|
+
concurrentPaths?: string[];
|
|
787
836
|
/** 会话段:fork 出来的子会话 id */
|
|
788
837
|
childSessionId?: string;
|
|
789
838
|
guidance?: RollbackGuidance[];
|
|
@@ -803,6 +852,16 @@ export interface RollbackRewindInput {
|
|
|
803
852
|
* (把已写回的文件还原、把已删的文件写回)。拒删判据见 createdRemovalRefusal。
|
|
804
853
|
*/
|
|
805
854
|
removeCreated?: boolean;
|
|
855
|
+
/**
|
|
856
|
+
* **路径级选择性恢复**(2026-10-17 用户定案 A):只恢复 / 只删除这些路径。
|
|
857
|
+
*
|
|
858
|
+
* 缺省(undefined)= 既有全量口径,行为一字不变。给了值就:
|
|
859
|
+
* · 记录在案的文件里**不在**清单中的:不写(计入 selectionSkipped);
|
|
860
|
+
* · 「留点后新建」的候选里**不在**清单中的:不删(同样计入)。
|
|
861
|
+
* 清单里出现**既未记录、也不在删除候选**里的路径 ⇒ **fail-closed 拒绝、零写入**(invalid-input):
|
|
862
|
+
* 绝不把「用户打错一个路径」变成「静默少恢复几个文件」。
|
|
863
|
+
*/
|
|
864
|
+
onlyPaths?: readonly string[];
|
|
806
865
|
}
|
|
807
866
|
export interface RollbackRewindResult {
|
|
808
867
|
ok: boolean;
|
|
@@ -829,6 +888,11 @@ export type RollbackStorageProbe = () => Promise<{
|
|
|
829
888
|
export interface RollbackEngineOptions {
|
|
830
889
|
/** 插件数据目录(= $DSH_HOME/dsh-config-manager);回滚点库在 dataDir/rollback-points */
|
|
831
890
|
dataDir: string;
|
|
891
|
+
/**
|
|
892
|
+
* 写入归属台账(2026-10-17 用户定案 B)。缺省 = 没有归属信息 ⇒ 预览不标 foreign,
|
|
893
|
+
* 回退行为与旧版**逐字一致**(未判定绝不当作「别的会话写的」)。
|
|
894
|
+
*/
|
|
895
|
+
attribution?: RollbackWriteAttributionPort;
|
|
832
896
|
/** DSH home(会话根 = homeDir/sessions) */
|
|
833
897
|
homeDir: string;
|
|
834
898
|
now?: () => number;
|
|
@@ -1020,6 +1084,8 @@ export declare class RollbackPointEngine {
|
|
|
1020
1084
|
private ledgerSnapshotCache;
|
|
1021
1085
|
/** 台账存储层(v3:清单 + 单条记录 + 内容寻址载荷;issue #89) */
|
|
1022
1086
|
private readonly ledger;
|
|
1087
|
+
/** 写入归属(2026-10-17 定案 B)。undefined = 没有归属信息 ⇒ 预览不标 foreign */
|
|
1088
|
+
private readonly attribution;
|
|
1023
1089
|
private readonly ledgerBytesBudget;
|
|
1024
1090
|
/**
|
|
1025
1091
|
* 工作区捕获的挂钟时间预算(毫秒;见 ROLLBACK_DEFAULT_CAPTURE_BUDGET_MS)。**可变**:
|
|
@@ -1377,6 +1443,13 @@ export declare class RollbackPointEngine {
|
|
|
1377
1443
|
* `budget` 是**单张预览**共享的累计预算(字节 + 步数),跨条目生效。
|
|
1378
1444
|
*/
|
|
1379
1445
|
private previewFileCounts;
|
|
1446
|
+
/**
|
|
1447
|
+
* **合并结果**的逐文件行级统计(与 `previewFileCounts` 同一套预算与降级判据)。
|
|
1448
|
+
*
|
|
1449
|
+
* 为什么单独一份:合并后要写进去的内容既不是记录里的原文(ours),也不是磁盘现状(theirs)——
|
|
1450
|
+
* 预览若照旧拿 ours 算差异,就会把「其实会被保留的对方改动」显示成「会被删掉」。
|
|
1451
|
+
*/
|
|
1452
|
+
private previewMergeCounts;
|
|
1380
1453
|
/** 只读预览:零写入(不过确认门、不拍 guard、不写任何字节)。 */
|
|
1381
1454
|
preview(id: string): Promise<RollbackPreviewResult>;
|
|
1382
1455
|
/**
|
|
@@ -1406,6 +1479,41 @@ export declare class RollbackPointEngine {
|
|
|
1406
1479
|
* · 不是绝对路径。
|
|
1407
1480
|
*/
|
|
1408
1481
|
private createdRemovalRefusal;
|
|
1482
|
+
/**
|
|
1483
|
+
* 把一条「留点之后被**别的会话**写过」的记录做成三方合并结果(2026-10-17 跨对话安全回退)。
|
|
1484
|
+
*
|
|
1485
|
+
* 三方:`ours` = 记录里的内容(回退目标)、`base` = 目标点之后**最早**那份中间版本、
|
|
1486
|
+
* `theirs` = 当前磁盘。结果是「撤销本会话那次改动 + 保留对方后来的改动」。
|
|
1487
|
+
*
|
|
1488
|
+
* 返回:`write`(合并干净且确实要写,带目标哈希 + 期望的当前哈希)、
|
|
1489
|
+
* `unchanged`(合并结果就是当前内容 —— 对方没动这一处,本来就不需要写)、
|
|
1490
|
+
* `blocked`(不能安全处理:冲突 / 没有中间版本 / 二进制 / 差异被降级 / 对方改成了不存在 / 读不出)。
|
|
1491
|
+
*/
|
|
1492
|
+
private mergeForeignRecord;
|
|
1493
|
+
/**
|
|
1494
|
+
* 三方合并的 **base**:目标点之后**最早**那份仍含该路径的中间版本(共同祖先)。
|
|
1495
|
+
*
|
|
1496
|
+
* 为什么这么取:回滚点的「工作区态」是回合开始前拍的 ⇒ 别的会话那个回合的点,
|
|
1497
|
+
* 正好是「本会话改动之后、对方改动之前」的版本 —— 标准三方合并的共同祖先。
|
|
1498
|
+
* 取**最早**而不是最近是刻意选安全方向:宁可少撤销,也绝不把对方更晚的改动算进 ours 侧。
|
|
1499
|
+
*
|
|
1500
|
+
* 排除四类:目标点自己(`createdAt > afterAt`)、**救援点 guard**(它在执行回退那一刻拍,
|
|
1501
|
+
* 内容 ≈ 当前磁盘,拿它当 base 会让合并结果直接等于 ours = 整文件覆盖,正是本机制要消灭的形态)、
|
|
1502
|
+
* **本会话的点**(见上)、以及内容读不出来的点(继续往后找 —— 找不到 ⇒ 调用方按「无法安全撤销」
|
|
1503
|
+
* 处理,绝不用近似值代替)。
|
|
1504
|
+
*/
|
|
1505
|
+
private baseVersionFor;
|
|
1506
|
+
/**
|
|
1507
|
+
* **别的会话的回合点里记录过的路径集合** —— 删除集保护的第二种证据(不依赖归属台账)。
|
|
1508
|
+
*
|
|
1509
|
+
* 为什么需要第二证据:归属可能整条缺失(工具不在白名单 / 另一个 DSH 进程把 attribution.json
|
|
1510
|
+
* 整体覆盖写 / 采集时机错过),而「别人的点里出现过这个路径」是**点台账本身**记录的事实 ——
|
|
1511
|
+
* 它足以证明「这条路径在留点之后被别的会话所在的回合捕获过」,因此删它就是销毁别人的工作。
|
|
1512
|
+
*
|
|
1513
|
+
* 有界:只扫目标点之后**最早**的 `limit` 个别的会话的点(按时间升序),且只读路径不读内容。
|
|
1514
|
+
* 扫不完不影响安全方向(少保护也会被归属那条兜一部分),但绝不因此声称「已扫全」。
|
|
1515
|
+
*/
|
|
1516
|
+
private foreignRecordedPaths;
|
|
1409
1517
|
private restoreWorkspace;
|
|
1410
1518
|
/**
|
|
1411
1519
|
* S3:会话回退 = 在目标回合前的稳定边界 **fork 子会话**。
|
|
@@ -1493,6 +1601,11 @@ export interface RollbackTrackerOptions {
|
|
|
1493
1601
|
size: number;
|
|
1494
1602
|
mtimeMs: number;
|
|
1495
1603
|
} | undefined>;
|
|
1604
|
+
/**
|
|
1605
|
+
* 写入归属采集器(2026-10-17 定案 B)。缺省 = 不采集 ⇒ 预览永远没有 foreign 标记,
|
|
1606
|
+
* 回退行为与旧版**逐字一致**。
|
|
1607
|
+
*/
|
|
1608
|
+
attribution?: RollbackWriteRecorder;
|
|
1496
1609
|
}
|
|
1497
1610
|
export interface RollbackTrackerResult {
|
|
1498
1611
|
captured: boolean;
|
|
@@ -1554,6 +1667,7 @@ export declare class RollbackAutoTracker {
|
|
|
1554
1667
|
private readonly pollIntervalMs;
|
|
1555
1668
|
private readonly log;
|
|
1556
1669
|
private readonly statSessionLog;
|
|
1670
|
+
private readonly attribution;
|
|
1557
1671
|
private readonly bindings;
|
|
1558
1672
|
private readonly open;
|
|
1559
1673
|
/**
|