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/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.prepare("UPDATE review_blocks SET decision=? WHERE file_id=? AND decision='pending'").run(request.decision, request.id)
327
+ markBlocks(db, request.id, request.decision)
296
328
  }
297
- const states = db.prepare('SELECT DISTINCT decision FROM review_blocks WHERE file_id=?').all(request.id) as Row[]
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
- * @author ddj 2026年10月29号
113
- * @param cwd 工作区
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.cwd, bucket.get(args.callId)?.path)
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.cwd, bucket.get(args.callId)?.path)
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.cwd, args.path)
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.cwd, missingPath(args.token))
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.cwd, bucket.get(item.callId)?.path)).find((reason) => reason !== null)
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 }
@@ -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号