dsh-config-manager 0.1.73 → 0.1.77
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.js +92 -12
- package/lib/client.d.ts +112 -13
- package/lib/client.js +2102 -888
- package/lib/core/messages.d.ts +1 -0
- package/lib/core/messages.js +2 -0
- package/lib/core/rollback-attribution.d.ts +81 -0
- package/lib/core/rollback-attribution.js +236 -0
- package/lib/core/rollback-ledger.d.ts +29 -3
- package/lib/core/rollback-ledger.js +125 -7
- package/lib/core/rollback-merge.d.ts +22 -0
- package/lib/core/rollback-merge.js +203 -0
- package/lib/core/rollback-point.d.ts +335 -3
- package/lib/core/rollback-point.js +1323 -169
- package/lib/core/types.d.ts +18 -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/sync/messages.d.ts +1 -0
- package/lib/sync/messages.js +3 -0
- package/lib/sync/sync-engine.js +56 -7
- package/lib/ui/badge-tone.d.ts +16 -0
- package/lib/ui/badge-tone.js +20 -0
- package/lib/ui/i18n.d.ts +5 -5
- package/lib/ui/i18n.js +10 -10
- 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/lib/utils/bundle-scan.d.ts +5 -3
- package/lib/utils/bundle-scan.js +5 -3
- package/lib/utils/line-diff.d.ts +7 -1
- package/lib/utils/line-diff.js +8 -2
- 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.ts +98 -13
- package/src/adapters/units.test.ts +26 -0
- package/src/client/chat/ChatEntryView.tsx +179 -49
- package/src/client/chat/RollbackFileIcon.tsx +7 -45
- package/src/client/chat/chat-entry-view.test.ts +440 -1
- package/src/client/chat/chat-entry-view.ts +222 -11
- package/src/client/chat/chat-node-registration.ts +111 -6
- package/src/client/chat/chat-slots.ts +30 -12
- package/src/client/common/ContentPicker.tsx +581 -175
- package/src/client/common/DiffTable.tsx +12 -23
- package/src/client/common/host-primitives.ts +141 -0
- package/src/client/common/ui.tsx +17 -3
- package/src/client/config-manager.module.css +658 -404
- package/src/client/css-scale-guard.test.ts +41 -3
- package/src/client/css-token-guard.test.ts +10 -4
- package/src/client/environment/maintenance/DiskUsageCard.tsx +20 -3
- package/src/client/export/ExportView.tsx +2 -0
- package/src/client/index.ts +22 -4
- package/src/client/library/RestorePlanView.tsx +0 -1
- package/src/client/locales.ts +54 -12
- package/src/client/recovery/RollbackCard.tsx +56 -2
- package/src/client/recovery/RollbackDiffPane.tsx +18 -6
- package/src/client/recovery/RollbackFileChanges.tsx +46 -2
- package/src/client/recovery/recovery-locales.ts +24 -4
- package/src/client/recovery/rollback-file-changes.test.ts +37 -1
- package/src/client/sync/PushPreviewCard.tsx +57 -0
- package/src/client/sync/SyncPanel.tsx +2 -37
- package/src/client/sync/SyncSettingsView.test.ts +3 -0
- package/src/client/sync/sync-push-preview.test.ts +14 -0
- package/src/client/sync/sync-view.ts +9 -1
- package/src/core/messages.ts +2 -0
- package/src/core/rollback-attribution.test.ts +120 -0
- package/src/core/rollback-attribution.ts +267 -0
- package/src/core/rollback-ledger.ts +123 -7
- package/src/core/rollback-merge.test.ts +133 -0
- package/src/core/rollback-merge.ts +213 -0
- package/src/core/rollback-point.test.ts +1004 -17
- package/src/core/rollback-point.ts +1421 -175
- package/src/core/types.ts +19 -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/sync/messages.ts +3 -0
- package/src/sync/sync-engine.ts +55 -7
- package/src/sync/sync-preview-push.test.ts +158 -0
- package/src/ui/badge-tone.test.ts +23 -0
- package/src/ui/badge-tone.ts +27 -0
- package/src/ui/i18n.ts +10 -10
- 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
- package/src/utils/bundle-scan.ts +5 -3
- package/src/utils/line-diff.test.ts +12 -0
- package/src/utils/line-diff.ts +9 -3
|
@@ -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
|