dsh-vscode-mode 0.25.0 → 0.26.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 +11 -5
- package/lib/client.js +1613 -1317
- package/lib/client.js.map +2 -2
- package/lib/index.js +7028 -6516
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/downloadFile.ts +36 -6
- package/src/client/downloadSink.ts +162 -0
- package/src/client/reviewBar.ts +3 -0
- package/src/client/reviewBatch.ts +9 -1
- package/src/client/reviewKeepAll.ts +72 -0
- package/src/client/state/diffSummary.ts +34 -7
- package/src/client/ui/DownloadPicker.ts +2 -1
- package/src/client/ui/EditorView.ts +117 -29
- package/src/client/ui/ReviewAllConfirm.ts +1 -1
- package/src/index.ts +1 -1
- package/src/pending/persist.ts +94 -5
- package/src/pending/store.ts +76 -1
- package/src/pendingCompat.ts +80 -9
- package/src/pendingRpc.ts +7 -6
- package/src/reviewDb.ts +119 -8
- package/src/reviewRpc.ts +110 -3
- package/src/rpc.ts +14 -10
- package/src/shared/review.ts +21 -0
- package/src/shared/rpc.ts +3 -1
- package/src/store.ts +44 -0
package/src/reviewDb.ts
CHANGED
|
@@ -285,6 +285,38 @@ export function reviewDetail(cwd: string, id: string): ReviewDetail | null {
|
|
|
285
285
|
: '该记录缺少可验证的原始字节或受控写回能力;仍可显示差异和 Keep,不能猜测历史全文执行 Undo。' } }
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* @private
|
|
290
|
+
* @author ddj 2026年11月03号
|
|
291
|
+
* @description: 把一个文件的待处理差异块整体置为目标决策,块级判定仍走 changeBlocks 的 hunk 分支。
|
|
292
|
+
* @param db 判定事务连接
|
|
293
|
+
* @param id 文件身份
|
|
294
|
+
* @param decision 目标决策
|
|
295
|
+
* @returns 无
|
|
296
|
+
*/
|
|
297
|
+
function markBlocks(db: DatabaseSync, id: string, decision: DecideReview['decision']): void {
|
|
298
|
+
db.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND decision='pending'").run(decision, id)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @private
|
|
303
|
+
* @author ddj 2026年11月03号
|
|
304
|
+
* @description: 按块决定聚合回写文件决定:仍含 pending 即 pending,多值即 mixed,单一值取该值。
|
|
305
|
+
* @param db 判定事务连接
|
|
306
|
+
* @param id 文件身份
|
|
307
|
+
* @param fallback 没有任何块时的兜底决策
|
|
308
|
+
* @returns 无
|
|
309
|
+
*/
|
|
310
|
+
function aggregateDecision(db: DatabaseSync, id: string, fallback: string): void {
|
|
311
|
+
const states = db.prepare('SELECT DISTINCT decision FROM review_blocks WHERE file_id=?').all(id) as Row[]
|
|
312
|
+
const decisions = states.map((row) => String(row.decision))
|
|
313
|
+
let aggregate = fallback
|
|
314
|
+
if (decisions.includes('pending')) aggregate = 'pending'
|
|
315
|
+
else if (decisions.length > 1) aggregate = 'mixed'
|
|
316
|
+
else if (decisions.length === 1) aggregate = decisions[0]
|
|
317
|
+
db.prepare('UPDATE review_files SET decision=? WHERE id=?').run(aggregate, id)
|
|
318
|
+
}
|
|
319
|
+
|
|
288
320
|
/** @public @author ddj 2026年10月27号 @param db Active decision transaction. @param request File or block action. Updates aggregate without losing mixed decisions. @throws Invalid or already-decided block. */
|
|
289
321
|
export function changeBlocks(db: DatabaseSync, request: DecideReview): void {
|
|
290
322
|
if (request.scope === 'hunk') {
|
|
@@ -292,15 +324,9 @@ export function changeBlocks(db: DatabaseSync, request: DecideReview): void {
|
|
|
292
324
|
.run(request.decision, request.id, request.blockId ?? '')
|
|
293
325
|
if (changed.changes !== 1) throw new Error('差异块不存在或已处理')
|
|
294
326
|
} else {
|
|
295
|
-
db
|
|
327
|
+
markBlocks(db, request.id, request.decision)
|
|
296
328
|
}
|
|
297
|
-
|
|
298
|
-
const decisions = states.map((row) => String(row.decision))
|
|
299
|
-
let aggregate = request.decision as string
|
|
300
|
-
if (decisions.includes('pending')) aggregate = 'pending'
|
|
301
|
-
else if (decisions.length > 1) aggregate = 'mixed'
|
|
302
|
-
else if (decisions.length === 1) aggregate = decisions[0]
|
|
303
|
-
db.prepare('UPDATE review_files SET decision=? WHERE id=?').run(aggregate, request.id)
|
|
329
|
+
aggregateDecision(db, request.id, request.decision)
|
|
304
330
|
}
|
|
305
331
|
|
|
306
332
|
/** @public @author ddj 2026年10月26号 @param cwd Workspace. @param request Frozen identity/revision. @returns Idempotent decision; rejection never guesses a target. */
|
|
@@ -334,3 +360,88 @@ export function decideReview(cwd: string, request: DecideReview): { revision: nu
|
|
|
334
360
|
throw error
|
|
335
361
|
}
|
|
336
362
|
}
|
|
363
|
+
|
|
364
|
+
/** 官方批量 Keep 的一条目标:id/seq 参与幂等编码,revision 只用于审计。 */
|
|
365
|
+
export interface OfficialKeepItem {
|
|
366
|
+
id: string
|
|
367
|
+
seq: number
|
|
368
|
+
revision: number
|
|
369
|
+
operationId: string
|
|
370
|
+
}
|
|
371
|
+
/** 官方批量 Keep 的结果;skipped 只记录条目级原因,段级异常由调用方捕获。 */
|
|
372
|
+
export interface OfficialKeepOutcome {
|
|
373
|
+
paths: string[]
|
|
374
|
+
skipped: { id: string; reason: string }[]
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @private
|
|
379
|
+
* @author ddj 2026年11月03号
|
|
380
|
+
* @description: 取当前仍待审的官方记录摘要,作为批量写入的权威依据(不用客户端快照值)。
|
|
381
|
+
* @param db 判定事务连接
|
|
382
|
+
* @returns id → 库内 seq/revision/identity
|
|
383
|
+
*/
|
|
384
|
+
function pendingRows(db: DatabaseSync): Map<string, { seq: number; revision: number; identity: string }> {
|
|
385
|
+
const rows = db.prepare("SELECT id,seq,revision,identity FROM review_files WHERE current=1 AND decision='pending'").all() as Row[]
|
|
386
|
+
const map = new Map<string, { seq: number; revision: number; identity: string }>()
|
|
387
|
+
for (const row of rows) map.set(String(row.id), { seq: Number(row.seq), revision: Number(row.revision), identity: String(row.identity) })
|
|
388
|
+
return map
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @public
|
|
393
|
+
* @author ddj 2026年11月03号
|
|
394
|
+
* @description: 一次事务内批量保留官方待审记录。写入条件取库内当前 revision,客户端的 seq/revision 只用于派生操作号,
|
|
395
|
+
* 因此快照漂移不会让整批中止;已结案、有尚待核对 Undo、操作号冲突的条目只跳过自己。
|
|
396
|
+
* @param cwd 工作区
|
|
397
|
+
* @param items 待保留条目(含派生好的 operationId)
|
|
398
|
+
* @returns 成功保留的 identity 列表与跳过明细
|
|
399
|
+
* @throws 事务级异常(SQL/程序错误)整体回滚后抛出
|
|
400
|
+
*/
|
|
401
|
+
export function keepOfficialAll(cwd: string, items: OfficialKeepItem[]): OfficialKeepOutcome {
|
|
402
|
+
const db = reviewDb(cwd)
|
|
403
|
+
const rows = pendingRows(db)
|
|
404
|
+
const outcome: OfficialKeepOutcome = { paths: [], skipped: [] }
|
|
405
|
+
const keep = db.prepare(`UPDATE review_files SET decision='accepted',revision=revision+1
|
|
406
|
+
WHERE id=? AND seq=? AND revision=? AND current=1 AND decision='pending'`)
|
|
407
|
+
const prepared = db.prepare("SELECT 1 FROM review_ops WHERE state='prepared' AND json_extract(request,'$.id')=?")
|
|
408
|
+
|
|
409
|
+
db.exec('BEGIN IMMEDIATE')
|
|
410
|
+
try {
|
|
411
|
+
for (const item of items) {
|
|
412
|
+
const encoded = JSON.stringify({ id: item.id, seq: item.seq })
|
|
413
|
+
const previous = db.prepare('SELECT request,result FROM review_ops WHERE operation_id=?').get(item.operationId) as Row | undefined
|
|
414
|
+
|
|
415
|
+
// 同一批次重放(客户端重试)按操作号幂等返回,不重复推进 revision
|
|
416
|
+
if (previous) {
|
|
417
|
+
if (previous.request !== encoded || !previous.result) outcome.skipped.push({ id: item.id, reason: '该条目已由其他操作处理' })
|
|
418
|
+
else outcome.paths.push(String((JSON.parse(String(previous.result)) as { identity: string }).identity))
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const row = rows.get(item.id)
|
|
423
|
+
if (!row || prepared.get(item.id)) {
|
|
424
|
+
outcome.skipped.push({ id: item.id, reason: row ? '该文件有尚待核对的 Undo,暂不能 Keep' : '已结案或不再是待审记录' })
|
|
425
|
+
continue
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 权威条件用库内 revision:客户端没看见的中间版本也能安全保留,落笔后仍由 CAS 兜住并发
|
|
429
|
+
if (keep.run(item.id, row.seq, row.revision).changes !== 1) {
|
|
430
|
+
outcome.skipped.push({ id: item.id, reason: '审查版本冲突,请刷新后重试' })
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
markBlocks(db, item.id, 'accepted')
|
|
435
|
+
aggregateDecision(db, item.id, 'accepted')
|
|
436
|
+
db.prepare("INSERT INTO review_ops VALUES(?,?,'committed',?,NULL)")
|
|
437
|
+
.run(item.operationId, encoded, JSON.stringify({ identity: row.identity }))
|
|
438
|
+
outcome.paths.push(row.identity)
|
|
439
|
+
rows.delete(item.id)
|
|
440
|
+
}
|
|
441
|
+
db.exec('COMMIT')
|
|
442
|
+
return outcome
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (db.isTransaction) db.exec('ROLLBACK')
|
|
445
|
+
throw error
|
|
446
|
+
}
|
|
447
|
+
}
|
package/src/reviewRpc.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** Session-scoped official review RPC. @author ddj 2026年10月26号 */
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
2
3
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
3
|
-
import type { ReviewDetail, ReviewItem, ReviewPage } from './shared/review.js'
|
|
4
|
+
import type { KeepAllItem, KeepAllResult, ReviewDetail, ReviewItem, ReviewPage } from './shared/review.js'
|
|
4
5
|
import type { PendingPage } from './shared/pendingReview.js'
|
|
5
|
-
import { addedFiles, decideReview, reviewDetail, reviewPage } from './reviewDb.js'
|
|
6
|
+
import { addedFiles, decideReview, keepOfficialAll, reviewDetail, reviewPage } from './reviewDb.js'
|
|
6
7
|
import { undoReview } from './reviewUndo.js'
|
|
7
|
-
import { applyDecide, loadDetail, loadPage } from './pending/store.js'
|
|
8
|
+
import { applyDecide, keepAll, loadDetail, loadPage } from './pending/store.js'
|
|
8
9
|
import { PENDING_PREFIX, mergedPage, pendingDetailOf, pendingItemOf, pendingSamePath } from './pendingCompat.js'
|
|
9
10
|
import { bucketOf, resolveScope, sessionOf } from './registry.js'
|
|
10
11
|
import type { Registry } from './registry.js'
|
|
@@ -164,6 +165,88 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
164
165
|
} catch { return { items: [], pending: 0, nextCursor: null, gaps: [] } }
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* @private
|
|
170
|
+
* @author ddj 2026年11月03号
|
|
171
|
+
* @description: 按批次与条目派生幂等操作号:同一批次重放复用同一结果,不重复推进 revision。
|
|
172
|
+
* @param batchId 客户端批次身份
|
|
173
|
+
* @param id 条目身份
|
|
174
|
+
* @param seq 事件序号
|
|
175
|
+
* @returns 64 位 hex 操作号
|
|
176
|
+
*/
|
|
177
|
+
function operationOf(batchId: string, id: string, seq: number): string {
|
|
178
|
+
return createHash('sha256').update(`${batchId}|${id}|${seq}`).digest('hex')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @private
|
|
183
|
+
* @author ddj 2026年11月03号
|
|
184
|
+
* @description: 取条目展示名,缺失时退回身份本身。
|
|
185
|
+
* @param items 本批条目
|
|
186
|
+
* @param id 条目身份
|
|
187
|
+
* @returns 展示名
|
|
188
|
+
*/
|
|
189
|
+
function displayOf(items: KeepAllItem[], id: string): string {
|
|
190
|
+
return items.find((item) => item.id === id)?.display ?? id
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @private
|
|
195
|
+
* @author ddj 2026年11月03号
|
|
196
|
+
* @description: 官方段的批量保留。段级异常只记进 result.error,不取消待审段。
|
|
197
|
+
* @param cwd 工作区
|
|
198
|
+
* @param batchId 批次身份
|
|
199
|
+
* @param items 官方条目
|
|
200
|
+
* @param result 汇总结果
|
|
201
|
+
* @returns 无
|
|
202
|
+
*/
|
|
203
|
+
function keepOfficialStage(cwd: string, batchId: string, items: KeepAllItem[], result: KeepAllResult): void {
|
|
204
|
+
try {
|
|
205
|
+
const outcome = keepOfficialAll(cwd, items.map((item) => ({
|
|
206
|
+
id: item.id, seq: item.seq, revision: item.revision, operationId: operationOf(batchId, item.id, item.seq),
|
|
207
|
+
})))
|
|
208
|
+
result.completed += outcome.paths.length
|
|
209
|
+
result.paths.push(...outcome.paths)
|
|
210
|
+
for (const skip of outcome.skipped) result.skipped.push({ id: skip.id, display: displayOf(items, skip.id), reason: skip.reason })
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
213
|
+
log.warn(`[review] keepAll 官方段失败: ${message}`)
|
|
214
|
+
if (!result.error) result.error = message
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @private
|
|
220
|
+
* @author ddj 2026年11月03号
|
|
221
|
+
* @description: 待审段的批量保留。冷端只跳过待审条目(判定需要活跃会话),官方条目不受影响。
|
|
222
|
+
* @param scope 已解析作用域
|
|
223
|
+
* @param batchId 批次身份
|
|
224
|
+
* @param items 待审条目(带 pending 前缀)
|
|
225
|
+
* @param result 汇总结果
|
|
226
|
+
* @returns 无
|
|
227
|
+
*/
|
|
228
|
+
async function keepPendingStage(
|
|
229
|
+
scope: { cwd: string; live: boolean }, batchId: string, items: KeepAllItem[], result: KeepAllResult,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
if (!scope.live) {
|
|
232
|
+
for (const item of items) result.skipped.push({ id: item.id, display: item.display, reason: '待审条目需要在会话活跃的一端判定' })
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
const targets = items.map((item) => ({
|
|
236
|
+
id: item.id.slice(PENDING_PREFIX.length), display: item.display, operationId: operationOf(batchId, item.id, item.seq),
|
|
237
|
+
}))
|
|
238
|
+
try {
|
|
239
|
+
const outcome = await keepAll(scope.cwd, targets)
|
|
240
|
+
result.completed += outcome.kept.length
|
|
241
|
+
for (const entry of outcome.kept) result.paths.push(entry.path)
|
|
242
|
+
for (const skip of outcome.skipped) result.skipped.push({ id: PENDING_PREFIX + skip.id, display: skip.display, reason: skip.reason })
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
245
|
+
log.warn(`[review] keepAll 待审段失败: ${message}`)
|
|
246
|
+
if (!result.error) result.error = message
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
167
250
|
return {
|
|
168
251
|
'edrv.review.list': async (args) => {
|
|
169
252
|
const { cwd } = await scopeOf(args.sessionId)
|
|
@@ -252,5 +335,29 @@ export function reviewHandlers(ctx: Ctx, registry: Registry): Partial<RpcHandler
|
|
|
252
335
|
return { ok: false, error: message }
|
|
253
336
|
}
|
|
254
337
|
},
|
|
338
|
+
'edrv.review.keepAll': async (args) => {
|
|
339
|
+
// 批量 Keep 只写共享审查库与待审库,不写源文件;冷端只影响待审条目,官方条目照常保留。
|
|
340
|
+
try {
|
|
341
|
+
const scope = await scopeOf(args.sessionId)
|
|
342
|
+
const items = Array.isArray(args.items) ? args.items : []
|
|
343
|
+
if (!items.length) return { ok: false, error: '没有可保留的待审记录' }
|
|
344
|
+
if (items.length > BATCH_LIMIT) {
|
|
345
|
+
return { ok: false, error: `待审记录过多(${items.length} 条,上限 ${BATCH_LIMIT}),请先分批处理后重试` }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result: KeepAllResult = { completed: 0, total: items.length, paths: [], skipped: [], error: '' }
|
|
349
|
+
const official = items.filter((item) => !item.id.startsWith(PENDING_PREFIX))
|
|
350
|
+
const pending = items.filter((item) => item.id.startsWith(PENDING_PREFIX))
|
|
351
|
+
if (official.length) keepOfficialStage(scope.cwd, args.batchId, official, result)
|
|
352
|
+
if (pending.length) await keepPendingStage(scope, args.batchId, pending, result)
|
|
353
|
+
log.info(`[review] keepAll live=${scope.live ? 1 : 0} total=${result.total}`
|
|
354
|
+
+ ` kept=${result.completed} skipped=${result.skipped.length}`)
|
|
355
|
+
return { ok: true, result }
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
358
|
+
log.warn(`[review] keepAll 失败: ${message}`)
|
|
359
|
+
return { ok: false, error: message }
|
|
360
|
+
}
|
|
361
|
+
},
|
|
255
362
|
}
|
|
256
363
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
archiveRecords,
|
|
16
16
|
archiveCanceled,
|
|
17
17
|
autoArchiveStale,
|
|
18
|
+
retireCovered,
|
|
18
19
|
policyOf,
|
|
19
20
|
resolveTarget,
|
|
20
21
|
resolveTargetIn,
|
|
@@ -108,15 +109,16 @@ function recView(record: DiffRecord, ws: RemoteWs | null = null): RecordView {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* @
|
|
112
|
+
* 旧写入口守卫:本会话可见的待审累计基线接管了该路径时一律拒绝,避免两套权威同时写同一文件。
|
|
113
|
+
* 只按会话可见范围判定:别的会话在管、本会话看不见的条目不能拦人,否则用户无从处理。
|
|
114
|
+
* @author ddj 2026年11月3号
|
|
115
|
+
* @param sc 会话与工作区
|
|
114
116
|
* @param path 请求涉及的路径(可为空表示无法定位)
|
|
115
117
|
* @returns 拒绝原因,未接管时为 null
|
|
116
118
|
*/
|
|
117
|
-
function legacyBlocked(cwd: string, path: string | undefined): string | null {
|
|
119
|
+
function legacyBlocked(sc: { session: Session; cwd: string }, path: string | undefined): string | null {
|
|
118
120
|
if (!path) return null
|
|
119
|
-
if (!pendingCovers(cwd, path)) return null
|
|
121
|
+
if (!pendingCovers(sc.cwd, path, sc.session.id)) return null
|
|
120
122
|
return '该文件已由待审累计基线接管,请使用面板 Keep/Revert(edrv.review.decide)'
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -645,6 +647,8 @@ export function buildHandlers(
|
|
|
645
647
|
// #endregion
|
|
646
648
|
await autoArchiveStale(ctx, sc.session, sc.cwd, bucket)
|
|
647
649
|
await archiveCanceled(ctx, sc.session, sc.cwd, bucket)
|
|
650
|
+
// 已被待审累计基线接管的路径:旧记录直接结案归档,旧通道的 Keep/Undo 随之从界面消失
|
|
651
|
+
await retireCovered(ctx, sc.session, sc.cwd, bucket)
|
|
648
652
|
// #region debug log
|
|
649
653
|
staleMs = performance.now() - staleStart
|
|
650
654
|
// #endregion
|
|
@@ -706,7 +710,7 @@ export function buildHandlers(
|
|
|
706
710
|
const sc = await requireSession(ctx, args.sessionId)
|
|
707
711
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
708
712
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
709
|
-
const blocked = legacyBlocked(sc
|
|
713
|
+
const blocked = legacyBlocked(sc, bucket.get(args.callId)?.path)
|
|
710
714
|
if (blocked) return { ok: false, error: blocked }
|
|
711
715
|
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'accepted' }])
|
|
712
716
|
const item = results[0]
|
|
@@ -717,7 +721,7 @@ export function buildHandlers(
|
|
|
717
721
|
const sc = await requireSession(ctx, args.sessionId)
|
|
718
722
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
719
723
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
720
|
-
const blocked = legacyBlocked(sc
|
|
724
|
+
const blocked = legacyBlocked(sc, bucket.get(args.callId)?.path)
|
|
721
725
|
if (blocked) return { ok: false, error: blocked }
|
|
722
726
|
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, [{ callId: args.callId, scope: args.scope, hunkIndex: args.hunkIndex, decision: 'rejected' }])
|
|
723
727
|
const item = results[0]
|
|
@@ -727,7 +731,7 @@ export function buildHandlers(
|
|
|
727
731
|
'edrv.missingReview': async (args) => {
|
|
728
732
|
const sc = await requireSession(ctx, args.sessionId)
|
|
729
733
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
730
|
-
const blocked = legacyBlocked(sc
|
|
734
|
+
const blocked = legacyBlocked(sc, args.path)
|
|
731
735
|
if (blocked) return { ok: false, error: blocked }
|
|
732
736
|
try {
|
|
733
737
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
@@ -742,7 +746,7 @@ export function buildHandlers(
|
|
|
742
746
|
'edrv.missingDecide': async (args) => {
|
|
743
747
|
const sc = await requireSession(ctx, args.sessionId)
|
|
744
748
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
745
|
-
const blocked = legacyBlocked(sc
|
|
749
|
+
const blocked = legacyBlocked(sc, missingPath(args.token))
|
|
746
750
|
if (blocked) return { ok: false, error: blocked }
|
|
747
751
|
try {
|
|
748
752
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
@@ -754,7 +758,7 @@ export function buildHandlers(
|
|
|
754
758
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
755
759
|
const bucket = await bucketOf(registry, ctx, sc.cwd)
|
|
756
760
|
const items = Array.isArray(args.items) ? args.items : []
|
|
757
|
-
const blocked = items.map((item) => legacyBlocked(sc
|
|
761
|
+
const blocked = items.map((item) => legacyBlocked(sc, bucket.get(item.callId)?.path)).find((reason) => reason !== null)
|
|
758
762
|
if (blocked) return { ok: false, error: blocked }
|
|
759
763
|
const results = await applyDecisions(ctx, sc.session, sc.cwd, bucket, items)
|
|
760
764
|
return { ok: true, results }
|
package/src/shared/review.ts
CHANGED
|
@@ -94,3 +94,24 @@ export interface DecideReview extends ReviewRequest {
|
|
|
94
94
|
decision: 'accepted' | 'rejected'
|
|
95
95
|
}
|
|
96
96
|
export type ReviewResult<T> = ({ ok: true } & T) | { ok: false; error: string }
|
|
97
|
+
/** 批量 Keep 的一条目标:seq/revision 只参与操作幂等与审计,写入条件以库内此刻状态为准。 */
|
|
98
|
+
export interface KeepAllItem {
|
|
99
|
+
id: string
|
|
100
|
+
display: string
|
|
101
|
+
seq: number
|
|
102
|
+
revision: number
|
|
103
|
+
}
|
|
104
|
+
/** 批量 Keep 中被跳过的条目:跳过不是失败,条目仍留在待审列表里。 */
|
|
105
|
+
export interface KeepAllSkip {
|
|
106
|
+
id: string
|
|
107
|
+
display: string
|
|
108
|
+
reason: string
|
|
109
|
+
}
|
|
110
|
+
/** 批量 Keep 的结果:completed/paths 只计真正落库的条目,error 只记录段级异常。 */
|
|
111
|
+
export interface KeepAllResult {
|
|
112
|
+
completed: number
|
|
113
|
+
total: number
|
|
114
|
+
paths: string[]
|
|
115
|
+
skipped: KeepAllSkip[]
|
|
116
|
+
error: string
|
|
117
|
+
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { DAP_ACTIONS } from './dap.js'
|
|
|
27
27
|
import { transportUrl } from './remoteChannel.js'
|
|
28
28
|
import type { LoggerLevel } from './logger.js'
|
|
29
29
|
import type { SvnViewRequest, SvnViewPayload } from './svnExplorer.js'
|
|
30
|
-
import type { DecideReview, ReviewDetail, ReviewPage, ReviewRequest } from './review.js'
|
|
30
|
+
import type { DecideReview, KeepAllItem, KeepAllResult, ReviewDetail, ReviewPage, ReviewRequest } from './review.js'
|
|
31
31
|
import type {
|
|
32
32
|
PendingDecide, PendingDetail, PendingHistoryMove, PendingImport,
|
|
33
33
|
PendingItem, PendingPage, PendingRemove, PendingRequest,
|
|
@@ -378,6 +378,7 @@ export interface RpcRequestMap {
|
|
|
378
378
|
'edrv.review.list': ReviewRequest & { cursor?: number; history?: boolean; path?: string; all?: boolean }
|
|
379
379
|
'edrv.review.detail': ReviewRequest & { id: string }
|
|
380
380
|
'edrv.review.decide': DecideReview
|
|
381
|
+
'edrv.review.keepAll': ReviewRequest & { batchId: string; items: KeepAllItem[] }
|
|
381
382
|
'edrv.pending.list': PendingRequest
|
|
382
383
|
'edrv.pending.detail': PendingRequest & { id: string }
|
|
383
384
|
'edrv.pending.decide': PendingDecide
|
|
@@ -563,6 +564,7 @@ export interface RpcOkMap {
|
|
|
563
564
|
'edrv.review.list': { page: ReviewPage }
|
|
564
565
|
'edrv.review.detail': { detail: ReviewDetail }
|
|
565
566
|
'edrv.review.decide': { revision: number }
|
|
567
|
+
'edrv.review.keepAll': { result: KeepAllResult }
|
|
566
568
|
'edrv.pending.list': { page: PendingPage }
|
|
567
569
|
'edrv.pending.detail': { detail: PendingDetail }
|
|
568
570
|
'edrv.pending.decide': { item: PendingItem; wrote: boolean }
|
package/src/store.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { SIDECAR, SIDECAR_ARCHIVE } from './paths.js'
|
|
|
11
11
|
import { log } from './log.js'
|
|
12
12
|
import { debugRecord } from './debugLog.js'
|
|
13
13
|
import { activeReady, appendArchive, checkActiveSource, commitActive, committedRecord, migrateActive, restoreReverts, type ActiveChange } from './archiveDb.js'
|
|
14
|
+
import { pendingSamePath } from './pendingCompat.js'
|
|
15
|
+
import { loadEntries } from './pending/persist.js'
|
|
14
16
|
|
|
15
17
|
export { SIDECAR, SIDECAR_ARCHIVE }
|
|
16
18
|
export const READ_CAP = 8 * 1024 * 1024
|
|
@@ -303,6 +305,48 @@ export async function autoArchiveStale(ctx: Ctx, session: Session, cwd: string,
|
|
|
303
305
|
return stale.length
|
|
304
306
|
}
|
|
305
307
|
|
|
308
|
+
/**
|
|
309
|
+
* @public
|
|
310
|
+
* @author ddj 2026年11月3号
|
|
311
|
+
* 待审累计基线接管某路径后,旧 sidecar 记录不再有独立决策价值:标记 superseded 并归档,
|
|
312
|
+
* 连编辑器的行内 Keep/Undo 一起消失,彻底去掉"旧通道还能点、点了又被守卫拒绝"的双权威残留。
|
|
313
|
+
* 接管是文件级事实,按工作区全部待审条目判定,不跟随"本会话能否看见"漂移。
|
|
314
|
+
* @param ctx DSH 上下文
|
|
315
|
+
* @param session 当前会话
|
|
316
|
+
* @param cwd 工作区
|
|
317
|
+
* @param bucket 活跃记录桶
|
|
318
|
+
* @returns 本次结案记录数
|
|
319
|
+
* @throws 归档失败时恢复内存标志并透传错误
|
|
320
|
+
*/
|
|
321
|
+
export async function retireCovered(
|
|
322
|
+
ctx: Ctx, session: Session, cwd: string, bucket: Map<string, DiffRecord>,
|
|
323
|
+
): Promise<number> {
|
|
324
|
+
const covers = loadEntries(cwd).flatMap((stored) => [stored.entry.path, stored.entry.id]).filter((path) => !!path)
|
|
325
|
+
if (!covers.length) return 0
|
|
326
|
+
|
|
327
|
+
const taken: DiffRecord[] = []
|
|
328
|
+
for (const rec of bucket.values()) {
|
|
329
|
+
if (rec.archived || rec.superseded === true) continue
|
|
330
|
+
if (covers.some((cover) => pendingSamePath(cover, rec.path))) taken.push(rec)
|
|
331
|
+
}
|
|
332
|
+
if (!taken.length) return 0
|
|
333
|
+
|
|
334
|
+
const original = taken.map((rec) => ({ rec, at: rec.at, superseded: rec.superseded }))
|
|
335
|
+
const at = new Date().toISOString()
|
|
336
|
+
for (const rec of taken) { rec.superseded = true; rec.at = at }
|
|
337
|
+
try {
|
|
338
|
+
await archiveRecords(ctx, session, cwd, bucket, taken, '已由待审累计基线接管,旧工具记录不再单独决策')
|
|
339
|
+
} catch (error) {
|
|
340
|
+
for (const item of original) {
|
|
341
|
+
if (item.rec.archived) continue
|
|
342
|
+
item.rec.superseded = item.superseded
|
|
343
|
+
item.rec.at = item.at
|
|
344
|
+
}
|
|
345
|
+
throw error
|
|
346
|
+
}
|
|
347
|
+
return taken.length
|
|
348
|
+
}
|
|
349
|
+
|
|
306
350
|
/**
|
|
307
351
|
* @public
|
|
308
352
|
* @author ddj 2026年10月10号
|