dsh-vscode-mode 0.21.4 → 0.22.0
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 +50 -4
- package/lib/client.js +72 -98
- package/lib/client.js.map +2 -2
- package/lib/index.js +2419 -32
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/state/diffSummary.ts +43 -4
- package/src/client/ui/DiffLauncher.ts +1 -1
- package/src/client/ui/EditorView.ts +10 -2
- package/src/client/ui/OfficialDiffBar.tsx +1 -12
- package/src/client/ui/OfficialReview.ts +13 -3
- package/src/client/ui/SidecarDiffBar.tsx +0 -22
- package/src/index.ts +9 -0
- package/src/missingReview.ts +36 -14
- package/src/pending/capture.ts +136 -0
- package/src/pending/decide.ts +213 -0
- package/src/pending/hunks.ts +241 -0
- package/src/pending/kernel.ts +233 -0
- package/src/pending/persist.ts +324 -0
- package/src/pending/store.ts +467 -0
- package/src/pending/vcs.ts +444 -0
- package/src/pendingCompat.ts +189 -0
- package/src/pendingRpc.ts +128 -0
- package/src/reviewDb.ts +3 -0
- package/src/reviewRpc.ts +52 -1
- package/src/rpc.ts +29 -2
- package/src/shared/pendingReview.ts +105 -0
- package/src/shared/rpc.ts +18 -0
- package/src/store.ts +19 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 待审投影的工作区存储:把采集到的写入折进单条累计条目,并承接 Keep/Revert 与外部改动的吸收。
|
|
3
|
+
* @author ddj 2026年10月29号
|
|
4
|
+
*/
|
|
5
|
+
import type { PendingDecide, PendingDetail, PendingHistoryMove, PendingItem, PendingPage } from '../shared/pendingReview.js'
|
|
6
|
+
import type { ReviewBlockState } from '../shared/reviewRegions.js'
|
|
7
|
+
import { reviewBlocks } from '../shared/reviewRegions.js'
|
|
8
|
+
import { unlink } from 'node:fs/promises'
|
|
9
|
+
import { resolve, sep } from 'node:path'
|
|
10
|
+
import type { PendingWrite } from './capture.js'
|
|
11
|
+
import { readText } from './capture.js'
|
|
12
|
+
import type { ShellLike, VcsImportOptions } from './vcs.js'
|
|
13
|
+
import { importVcsChanges } from './vcs.js'
|
|
14
|
+
import type { DecideOutcome, WriteResult } from './decide.js'
|
|
15
|
+
import { capabilityOf, decide, diffOf } from './decide.js'
|
|
16
|
+
import { entryFromNew, isSettled, mergeEntries } from './kernel.js'
|
|
17
|
+
import type { PendingEntry } from './kernel.js'
|
|
18
|
+
import type { PendingFlags, StoredEntry } from './persist.js'
|
|
19
|
+
import {
|
|
20
|
+
dropEntry, earliestUndone, latestApplied, loadBlocks, loadEntries, pushHistory,
|
|
21
|
+
readOp, saveBlocks, saveEntry, setHistoryState, writeOp,
|
|
22
|
+
} from './persist.js'
|
|
23
|
+
|
|
24
|
+
/** 统计一次差异的增删行数。 */
|
|
25
|
+
function countsOf(entry: PendingEntry): { added: number; deleted: number } {
|
|
26
|
+
const diff = diffOf(entry)
|
|
27
|
+
if (diff.kind !== 'text') return { added: 0, deleted: 0 }
|
|
28
|
+
let added = 0
|
|
29
|
+
let deleted = 0
|
|
30
|
+
for (const hunk of diff.hunks) {
|
|
31
|
+
for (const line of hunk.lines) {
|
|
32
|
+
if (line.startsWith('+')) added++
|
|
33
|
+
else if (line.startsWith('-')) deleted++
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { added, deleted }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @author ddj 2026年10月30号
|
|
41
|
+
* @description: 由当前累计条目现场推导块身份;块是差异的纯函数,落库的只是决定状态。
|
|
42
|
+
* @param entry 累计条目
|
|
43
|
+
* @returns 块状态列表(决定一律 pending,判定推进后由下一次推导自然收窄)
|
|
44
|
+
*/
|
|
45
|
+
function blockStatesOf(entry: PendingEntry): ReviewBlockState[] {
|
|
46
|
+
const diff = diffOf(entry)
|
|
47
|
+
if (diff.kind !== 'text') return []
|
|
48
|
+
return reviewBlocks(diff).map((block) => ({ id: block.id, decision: 'pending' as const }))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @author ddj 2026年10月29号
|
|
53
|
+
* @description: 把一次已确认写入折进累计条目:同一身份只保留一条,基线留最早、内容取最新。
|
|
54
|
+
* @param stored 已有条目(可为空)
|
|
55
|
+
* @param write 采集到的写入
|
|
56
|
+
* @returns 折叠后的条目与标记
|
|
57
|
+
*/
|
|
58
|
+
export function foldWrite(stored: StoredEntry | undefined, write: PendingWrite): StoredEntry {
|
|
59
|
+
const fresh: PendingEntry = entryFromNew({
|
|
60
|
+
id: write.identity,
|
|
61
|
+
path: write.path,
|
|
62
|
+
sessionId: write.sessionId,
|
|
63
|
+
earlierVersion: write.before === null ? 'none' : 'file',
|
|
64
|
+
oldText: write.before ?? '',
|
|
65
|
+
newText: write.after ?? '',
|
|
66
|
+
updatedAt: write.now,
|
|
67
|
+
})
|
|
68
|
+
if (!stored) {
|
|
69
|
+
return {
|
|
70
|
+
entry: fresh,
|
|
71
|
+
flags: { missing: write.after === null, diverged: false, oversized: false, revision: 0 },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const merged = mergeEntries(stored.entry, fresh)
|
|
75
|
+
merged.path = write.path
|
|
76
|
+
return {
|
|
77
|
+
entry: merged,
|
|
78
|
+
flags: {
|
|
79
|
+
missing: write.after === null,
|
|
80
|
+
diverged: false,
|
|
81
|
+
oversized: stored.flags.oversized,
|
|
82
|
+
revision: stored.flags.revision + 1,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @author ddj 2026年10月29号
|
|
89
|
+
* @description: 条目转列表投影,已解决条目仍保留但不可再决定。
|
|
90
|
+
* @param stored 条目与标记
|
|
91
|
+
* @returns 列表项
|
|
92
|
+
*/
|
|
93
|
+
export function itemOf(stored: StoredEntry): PendingItem {
|
|
94
|
+
const { entry, flags } = stored
|
|
95
|
+
const counts = countsOf(entry)
|
|
96
|
+
const item: PendingItem = {
|
|
97
|
+
id: entry.id,
|
|
98
|
+
path: entry.path,
|
|
99
|
+
earlierVersion: entry.earlierVersion,
|
|
100
|
+
added: counts.added,
|
|
101
|
+
deleted: counts.deleted,
|
|
102
|
+
settled: isSettled(entry),
|
|
103
|
+
unseen: entry.unseen === true,
|
|
104
|
+
missing: flags.missing,
|
|
105
|
+
diverged: flags.diverged,
|
|
106
|
+
oversized: flags.oversized,
|
|
107
|
+
revision: flags.revision,
|
|
108
|
+
updatedAt: entry.updatedAt,
|
|
109
|
+
sessionId: entry.sessionId,
|
|
110
|
+
sessionIds: entry.sessionIds,
|
|
111
|
+
}
|
|
112
|
+
if (entry.lineage !== undefined) item.lineage = entry.lineage
|
|
113
|
+
return item
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @author ddj 2026年10月29号
|
|
118
|
+
* @description: 读一页待审条目,按更新时间升序;会话过滤走血缘集合。
|
|
119
|
+
* @param cwd 工作区
|
|
120
|
+
* @param sessionId 当前会话
|
|
121
|
+
* @returns 列表
|
|
122
|
+
*/
|
|
123
|
+
export function loadPage(cwd: string, sessionId?: string): PendingPage {
|
|
124
|
+
const all = loadEntries(cwd)
|
|
125
|
+
const visible = sessionId ? all.filter((stored) => stored.entry.sessionIds.includes(sessionId)) : all
|
|
126
|
+
const items = visible.map(itemOf)
|
|
127
|
+
const gaps: string[] = []
|
|
128
|
+
for (const stored of visible) {
|
|
129
|
+
if (stored.flags.oversized) gaps.push(`${stored.entry.path}: 文本超出上限,仅保留标记`)
|
|
130
|
+
}
|
|
131
|
+
return { items, pending: items.filter((item) => !item.settled).length, nextCursor: null, gaps }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @author ddj 2026年10月29号
|
|
136
|
+
* @description: 读单条详情,包含可交给现有面板渲染的差异与块决定。
|
|
137
|
+
* @param cwd 工作区
|
|
138
|
+
* @param id 条目身份
|
|
139
|
+
* @returns 详情,条目不存在时 null
|
|
140
|
+
*/
|
|
141
|
+
export function loadDetail(cwd: string, id: string): PendingDetail | null {
|
|
142
|
+
const stored = loadEntries(cwd).find((item) => item.entry.id === id)
|
|
143
|
+
if (!stored) return null
|
|
144
|
+
const diff = diffOf(stored.entry)
|
|
145
|
+
return {
|
|
146
|
+
item: itemOf(stored),
|
|
147
|
+
diff,
|
|
148
|
+
blocks: blockStatesOf(stored.entry),
|
|
149
|
+
capability: capabilityOf(stored.entry, stored.flags),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @author ddj 2026年10月29号
|
|
155
|
+
* @description: 采集入口:折叠写入并落库。
|
|
156
|
+
* @param write 已确认写入
|
|
157
|
+
* @returns 无
|
|
158
|
+
*/
|
|
159
|
+
export function recordWrite(write: PendingWrite): void {
|
|
160
|
+
const stored = loadEntries(write.cwd).find((item) => item.entry.id === write.identity)
|
|
161
|
+
if (stored && stored.entry.oldText === stored.entry.newText && stored.entry.newText === (write.after ?? '')) return
|
|
162
|
+
saveEntry(write.cwd, foldWrite(stored, write))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @author ddj 2026年10月29号
|
|
167
|
+
* @description: 外部改动吸收:当前内容与条目最新内容不同时采纳并标分歧,基线不动。
|
|
168
|
+
* @param stored 条目
|
|
169
|
+
* @param content 磁盘当前内容
|
|
170
|
+
* @param now 时间
|
|
171
|
+
* @returns 新条目与标记
|
|
172
|
+
*/
|
|
173
|
+
export function absorbed(stored: StoredEntry, content: string, now: number): StoredEntry {
|
|
174
|
+
return {
|
|
175
|
+
entry: { ...stored.entry, newText: content, updatedAt: now, unseen: true },
|
|
176
|
+
flags: { ...stored.flags, diverged: true, missing: false, revision: stored.flags.revision + 1 },
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @author ddj 2026年10月29号
|
|
182
|
+
* @description: 读回磁盘并把可读的外部改动吸收进条目;缺失只标标记不删条目。
|
|
183
|
+
* @param stored 条目
|
|
184
|
+
* @param content 磁盘当前内容,null 表示缺失或不可读
|
|
185
|
+
* @param now 时间
|
|
186
|
+
* @returns 需要落库的新状态,无需改动时为 null
|
|
187
|
+
*/
|
|
188
|
+
export function refreshed(stored: StoredEntry, content: string | null, now: number): StoredEntry | null {
|
|
189
|
+
if (content === null) {
|
|
190
|
+
if (stored.flags.missing) return null
|
|
191
|
+
return { entry: stored.entry, flags: { ...stored.flags, missing: true, revision: stored.flags.revision + 1 } }
|
|
192
|
+
}
|
|
193
|
+
if (content === stored.entry.newText) {
|
|
194
|
+
if (!stored.flags.missing) return null
|
|
195
|
+
return { entry: stored.entry, flags: { ...stored.flags, missing: false, revision: stored.flags.revision + 1 } }
|
|
196
|
+
}
|
|
197
|
+
return absorbed(stored, content, now)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @author ddj 2026年10月29号
|
|
202
|
+
* @description: 构造带版本守卫与读回校验的写盘函数;内容已被外部改动时直接拒绝。
|
|
203
|
+
* @param fs 文件系统
|
|
204
|
+
* @param cwd 工作区
|
|
205
|
+
* @param identity 目标身份
|
|
206
|
+
* @param expected 写前应当具备的内容
|
|
207
|
+
* @param signal 取消信号
|
|
208
|
+
* @param policy 沙箱策略
|
|
209
|
+
* @param allowCreate 文件已被删除时是否允许按快照重建(仅撤销/重做路径使用)
|
|
210
|
+
* @returns 写盘函数
|
|
211
|
+
*/
|
|
212
|
+
export function writerFor(
|
|
213
|
+
fs: any, cwd: string, identity: string, expected: string, signal?: AbortSignal, policy?: unknown,
|
|
214
|
+
allowCreate = false,
|
|
215
|
+
): (content: string | null) => Promise<WriteResult> {
|
|
216
|
+
return async (content) => {
|
|
217
|
+
let target: any
|
|
218
|
+
try {
|
|
219
|
+
target = await fs.resolve(identity, { cwd, signal })
|
|
220
|
+
} catch {
|
|
221
|
+
return { ok: false, reason: 'resolve-failed' }
|
|
222
|
+
}
|
|
223
|
+
const current = await readText(fs, target).catch(() => undefined)
|
|
224
|
+
if (current === undefined) return { ok: false, reason: 'read-failed' }
|
|
225
|
+
// 撤销「Revert 删除新建文件」时文件已不存在,只有显式允许重建才放行
|
|
226
|
+
const recreate = allowCreate && current === null
|
|
227
|
+
if (!recreate && current !== expected) return { ok: false, reason: 'diverged' }
|
|
228
|
+
const info = await fs.stat(target, signal).catch(() => undefined)
|
|
229
|
+
try {
|
|
230
|
+
// 第五个实参是沙箱策略;缺它时 provider 会以 FS_SANDBOX_DENIED 拒绝写入
|
|
231
|
+
const written = await fs.writeText(
|
|
232
|
+
target, content ?? '', info ? { kind: 'replaceIfVersion', version: info.version } : undefined, signal, policy,
|
|
233
|
+
)
|
|
234
|
+
const readBack = await readText(fs, target)
|
|
235
|
+
if ((content ?? '') !== (readBack ?? '')) return { ok: false, reason: 'read-back-mismatch' }
|
|
236
|
+
if (content === null && readBack !== null) return { ok: false, reason: 'delete-failed' }
|
|
237
|
+
void written
|
|
238
|
+
return { ok: true, content }
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return { ok: false, reason: reasonOf(error) }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @author ddj 2026年10月29号
|
|
247
|
+
* @description: 执行一次 Keep/Revert 并把结果落库;失败只返回原因,不写任何状态。
|
|
248
|
+
* @param fs 文件系统
|
|
249
|
+
* @param cwd 工作区
|
|
250
|
+
* @param request 判定请求
|
|
251
|
+
* @param signal 取消信号
|
|
252
|
+
* @returns 判定结果
|
|
253
|
+
*/
|
|
254
|
+
export async function applyDecide(
|
|
255
|
+
fs: any, cwd: string, request: PendingDecide, signal?: AbortSignal, policy?: unknown,
|
|
256
|
+
): Promise<DecideOutcome | { ok: false; reason: string }> {
|
|
257
|
+
const stored = loadEntries(cwd).find((item) => item.entry.id === request.id)
|
|
258
|
+
if (!stored) return { ok: false, reason: 'unknown-item' }
|
|
259
|
+
const beforeBlocks = blockStatesOf(stored.entry)
|
|
260
|
+
const outcome = await decide({
|
|
261
|
+
request,
|
|
262
|
+
entry: stored.entry,
|
|
263
|
+
flags: stored.flags,
|
|
264
|
+
blocks: beforeBlocks,
|
|
265
|
+
now: Date.now(),
|
|
266
|
+
write: (content) => {
|
|
267
|
+
if (content === null) return deleteTarget(fs, cwd, stored.entry.id, signal)
|
|
268
|
+
return writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy)(content)
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason }
|
|
272
|
+
saveEntry(cwd, { entry: outcome.entry, flags: outcome.flags })
|
|
273
|
+
saveBlocks(cwd, request.id, outcome.blocks)
|
|
274
|
+
// 记录撤销快照(含是否真的写过盘),撤销/重做都从这条记录反推
|
|
275
|
+
pushHistory(cwd, 'decide', request.id, JSON.stringify({
|
|
276
|
+
before: { entry: stored.entry, flags: stored.flags, blocks: beforeBlocks },
|
|
277
|
+
after: { entry: outcome.entry, flags: outcome.flags, blocks: outcome.blocks as ReviewBlockState[] },
|
|
278
|
+
wrote: outcome.wrote,
|
|
279
|
+
}), Date.now())
|
|
280
|
+
return outcome
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* @author ddj 2026年10月30号
|
|
285
|
+
* @description: 版本库导入:把工作副本相对基线的改动折进待审条目,已存在条目按折叠规则并入。
|
|
286
|
+
* @param shell shell 能力
|
|
287
|
+
* @param cwd 工作区
|
|
288
|
+
* @param sessionId 触发导入的会话
|
|
289
|
+
* @param options 导入选项
|
|
290
|
+
* @returns 导入条数与探测到的改动数
|
|
291
|
+
*/
|
|
292
|
+
export async function importFromVcs(
|
|
293
|
+
shell: ShellLike, cwd: string, sessionId: string, options: VcsImportOptions = {},
|
|
294
|
+
): Promise<{ imported: number; changes: number }> {
|
|
295
|
+
const changes = await importVcsChanges(shell, cwd, options)
|
|
296
|
+
const now = Date.now()
|
|
297
|
+
for (const change of changes) {
|
|
298
|
+
recordWrite({
|
|
299
|
+
cwd,
|
|
300
|
+
sessionId,
|
|
301
|
+
path: change.path,
|
|
302
|
+
identity: change.path,
|
|
303
|
+
before: change.earlierVersion === 'none' ? null : change.oldText,
|
|
304
|
+
after: change.newText,
|
|
305
|
+
now,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
return { imported: changes.length, changes: changes.length }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @author ddj 2026年10月30号
|
|
313
|
+
* @description: 撤销或重做最近一次判定。先按 operationId 判幂等,再据栈顶记录判定方向是否合法,
|
|
314
|
+
* 只有写盘过的判定才反向写盘,写盘失败时条目状态保持原样。
|
|
315
|
+
* @param fs 文件系统
|
|
316
|
+
* @param cwd 工作区
|
|
317
|
+
* @param move 撤销/重做请求
|
|
318
|
+
* @param signal 取消信号
|
|
319
|
+
* @returns 结果条目与是否写盘
|
|
320
|
+
*/
|
|
321
|
+
export async function undoDecide(
|
|
322
|
+
fs: any, cwd: string, move: PendingHistoryMove, signal?: AbortSignal, policy?: unknown,
|
|
323
|
+
): Promise<{ ok: true; item: PendingItem; wrote: boolean } | { ok: false; reason: string }> {
|
|
324
|
+
const replay = readOp(cwd, move.operationId)
|
|
325
|
+
if (replay && replay.state === 'committed' && replay.result) return JSON.parse(replay.result) as { ok: true; item: PendingItem; wrote: boolean }
|
|
326
|
+
|
|
327
|
+
const record = move.direction === 'undo' ? latestApplied(cwd) : earliestUndone(cwd)
|
|
328
|
+
if (!record) return { ok: false, reason: move.direction === 'undo' ? 'no-undo' : 'no-redo' }
|
|
329
|
+
const snapshot = parseSnapshot(record.snapshot)
|
|
330
|
+
if (!snapshot) return { ok: false, reason: 'bad-history' }
|
|
331
|
+
const stored = loadEntries(cwd).find((item) => item.entry.id === record.entryId)
|
|
332
|
+
if (!stored) return { ok: false, reason: 'unknown-item' }
|
|
333
|
+
const target = move.direction === 'undo' ? snapshot.before : snapshot.after
|
|
334
|
+
|
|
335
|
+
writeOp(cwd, move.operationId, JSON.stringify(move), 'prepared')
|
|
336
|
+
let wrote = false
|
|
337
|
+
if (snapshot.wrote) {
|
|
338
|
+
const result = await writeBack(fs, cwd, stored, target, signal, policy)
|
|
339
|
+
if (!result.ok) return { ok: false, reason: result.reason }
|
|
340
|
+
wrote = true
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
saveEntry(cwd, { entry: target.entry, flags: target.flags })
|
|
344
|
+
saveBlocks(cwd, record.entryId, target.blocks)
|
|
345
|
+
setHistoryState(cwd, record.id, move.direction === 'undo' ? 'undone' : 'applied')
|
|
346
|
+
const outcome = { ok: true as const, item: itemOf({ entry: target.entry, flags: target.flags }), wrote }
|
|
347
|
+
writeOp(cwd, move.operationId, JSON.stringify(move), 'committed', { result: JSON.stringify(outcome) })
|
|
348
|
+
return outcome
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @author ddj 2026年10月30号
|
|
353
|
+
* @description: 把文件内容恢复到目标快照:目标为缺失则删除,否则按版本守卫写回并读回校验。
|
|
354
|
+
* @param fs 文件系统
|
|
355
|
+
* @param cwd 工作区
|
|
356
|
+
* @param stored 当前条目
|
|
357
|
+
* @param target 目标快照
|
|
358
|
+
* @param signal 取消信号
|
|
359
|
+
* @returns 写盘结果
|
|
360
|
+
*/
|
|
361
|
+
async function writeBack(
|
|
362
|
+
fs: any, cwd: string, stored: StoredEntry, target: HistoryState, signal?: AbortSignal, policy?: unknown,
|
|
363
|
+
): Promise<WriteResult> {
|
|
364
|
+
if (target.flags.missing) return deleteTarget(fs, cwd, stored.entry.id, signal)
|
|
365
|
+
const write = writerFor(fs, cwd, stored.entry.id, stored.entry.newText, signal, policy, true)
|
|
366
|
+
return write(target.entry.newText)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** 历史快照中单侧状态。 */
|
|
370
|
+
interface HistoryState {
|
|
371
|
+
entry: PendingEntry
|
|
372
|
+
flags: PendingFlags
|
|
373
|
+
blocks: ReviewBlockState[]
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** 一次判定的双向快照。 */
|
|
377
|
+
interface HistorySnapshot {
|
|
378
|
+
before: HistoryState
|
|
379
|
+
after: HistoryState
|
|
380
|
+
wrote: boolean
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @author ddj 2026年10月30号
|
|
385
|
+
* @description: 解析历史快照;损坏时返回 null,不猜测内容。
|
|
386
|
+
* @param text 快照文本
|
|
387
|
+
* @returns 快照或 null
|
|
388
|
+
*/
|
|
389
|
+
function parseSnapshot(text: string): HistorySnapshot | null {
|
|
390
|
+
try {
|
|
391
|
+
const parsed = JSON.parse(text) as HistorySnapshot
|
|
392
|
+
if (!parsed?.before?.entry || !parsed?.after?.entry) return null
|
|
393
|
+
return parsed
|
|
394
|
+
} catch {
|
|
395
|
+
return null
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @author ddj 2026年10月29号
|
|
401
|
+
* @description: Revert 的删除分支:FileSystem 抽象没有删除能力,只有确认仍在工作区内才用宿主 unlink。
|
|
402
|
+
* @param fs 文件系统
|
|
403
|
+
* @param cwd 工作区
|
|
404
|
+
* @param identity 目标身份
|
|
405
|
+
* @param signal 取消信号
|
|
406
|
+
* @returns 删除结果
|
|
407
|
+
*/
|
|
408
|
+
async function deleteTarget(fs: any, cwd: string, identity: string, signal?: AbortSignal): Promise<WriteResult> {
|
|
409
|
+
let target: any
|
|
410
|
+
try {
|
|
411
|
+
target = await fs.resolve(identity, { cwd, signal })
|
|
412
|
+
} catch {
|
|
413
|
+
return { ok: false, reason: 'resolve-failed' }
|
|
414
|
+
}
|
|
415
|
+
const info = await fs.stat(target, signal).catch(() => undefined)
|
|
416
|
+
if (!info) return { ok: true, content: null }
|
|
417
|
+
const absolute = String(fs.processPath(target))
|
|
418
|
+
if (!insideWorkspace(cwd, absolute)) return { ok: false, reason: 'outside-workspace' }
|
|
419
|
+
try {
|
|
420
|
+
await unlink(absolute)
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return { ok: false, reason: reasonOf(error) }
|
|
423
|
+
}
|
|
424
|
+
const after = await fs.stat(target, signal).catch(() => undefined)
|
|
425
|
+
return after ? { ok: false, reason: 'delete-failed' } : { ok: true, content: null }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* @author ddj 2026年10月29号
|
|
430
|
+
* @description: 判断宿主路径是否位于工作区内,Windows 下按大小写不敏感比较。
|
|
431
|
+
* @param cwd 工作区
|
|
432
|
+
* @param absolute 目标绝对路径
|
|
433
|
+
* @returns 是否在工作区内
|
|
434
|
+
*/
|
|
435
|
+
function insideWorkspace(cwd: string, absolute: string): boolean {
|
|
436
|
+
const root = resolve(cwd)
|
|
437
|
+
const candidate = resolve(absolute)
|
|
438
|
+
const normalize = (value: string) => (process.platform === 'win32' ? value.toLowerCase() : value)
|
|
439
|
+
const rootKey = normalize(root).endsWith(sep) ? normalize(root) : normalize(root) + sep
|
|
440
|
+
return normalize(candidate).startsWith(rootKey)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @author ddj 2026年10月29号
|
|
445
|
+
* @description: 从列表移除已解决条目;未解决条目拒绝移除,避免表里看不到却被删掉。
|
|
446
|
+
* @param cwd 工作区
|
|
447
|
+
* @param id 条目身份
|
|
448
|
+
* @param revision 并发校验版本
|
|
449
|
+
* @returns 是否移除
|
|
450
|
+
*/
|
|
451
|
+
export function removeEntry(cwd: string, id: string, revision: number): boolean {
|
|
452
|
+
const stored = loadEntries(cwd).find((item) => item.entry.id === id)
|
|
453
|
+
if (!stored || stored.flags.revision !== revision || !isSettled(stored.entry)) return false
|
|
454
|
+
dropEntry(cwd, id)
|
|
455
|
+
return true
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* @author ddj 2026年10月29号
|
|
460
|
+
* @description: 取错误码用于拒绝原因,没有则回落到消息文本。
|
|
461
|
+
* @param error 抛出的错误
|
|
462
|
+
* @returns 原因字符串
|
|
463
|
+
*/
|
|
464
|
+
function reasonOf(error: unknown): string {
|
|
465
|
+
const code = (error as { code?: unknown }).code
|
|
466
|
+
return typeof code === 'string' ? code : String(error)
|
|
467
|
+
}
|