dsh-vscode-mode 0.1.10 → 0.1.12

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/store.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * 作者 ddj 2026-08-20
5
5
  */
6
6
  import type { ArchiveBatch, ArchiveData, DiffRecord, SidecarData } from './shared/types.js'
7
+ import type { ReadState } from './shared/diff.js'
8
+ import { fingerprint, isNoopHunk, locateHunks, preciseHunk } from './shared/diff.js'
7
9
  import { archiveEntryFor, groupByBatch, normalizeRecord } from './model.js'
8
10
 
9
11
  export const SIDECAR = '.dsh-edit-review.json'
@@ -105,21 +107,23 @@ export async function saveBucket(ctx: Ctx, cwd: string, recsMap: Map<string, Dif
105
107
  * @author ddj 2026年08月20号
106
108
  * @param cache path → 内容(null=缺失/读取失败,''=过大跳过)
107
109
  */
108
- export async function readForCache(ctx: Ctx, session: Session, cache: Map<string, string | null>, path: string): Promise<string | null> {
109
- if (cache.has(path)) return cache.get(path) ?? null
110
+ export async function readForCache(ctx: Ctx, session: Session, cache: Map<string, ReadState>, path: string): Promise<ReadState> {
111
+ const cached = cache.get(path)
112
+ if (cached) return cached
110
113
  const fs = ctx.get('fs')
111
- if (!fs) { cache.set(path, null); return null }
114
+ if (!fs) { const state: ReadState = { kind: 'unavailable' }; cache.set(path, state); return state }
112
115
  try {
113
116
  const target = await resolveTarget(ctx, session, path)
114
117
  const info = await fs.stat(target)
115
- if (!info || info.type !== 'file') { cache.set(path, null); return null }
116
- if ((info.size ?? 0) > READ_CAP) { cache.set(path, ''); return '' }
117
- const content = await fs.readText(target)
118
- cache.set(path, content)
119
- return content
118
+ if (!info || info.type !== 'file') { const state: ReadState = { kind: 'missing' }; cache.set(path, state); return state }
119
+ if ((info.size ?? 0) > READ_CAP) { const state: ReadState = { kind: 'unavailable' }; cache.set(path, state); return state }
120
+ const state: ReadState = { kind: 'content', content: await fs.readText(target) }
121
+ cache.set(path, state)
122
+ return state
120
123
  } catch (error) {
121
- cache.set(path, null)
122
- return null
124
+ const state: ReadState = { kind: 'unavailable' }
125
+ cache.set(path, state)
126
+ return state
123
127
  }
124
128
  }
125
129
 
@@ -128,28 +132,22 @@ export async function readForCache(ctx: Ctx, session: Session, cache: Map<string
128
132
  * 或新建文件已不存在,或全为空差异。满足则应由 host 自动归档。
129
133
  * @author ddj 2026年08月20号
130
134
  */
131
- export async function recordIsStale(ctx: Ctx, session: Session, cache: Map<string, string | null>, rec: DiffRecord): Promise<boolean> {
135
+ export async function recordIsStale(ctx: Ctx, session: Session, cache: Map<string, ReadState>, rec: DiffRecord): Promise<boolean> {
132
136
  if (rec.superseded === true) return true
133
137
  if (!Array.isArray(rec.hunks) || !rec.hunks.length) return true
134
- const perHunk = Array.isArray(rec.decisions.perHunk) ? rec.decisions.perHunk : []
135
- let hasLocatablePending = false
136
- for (let i = 0; i < rec.hunks.length; i++) {
137
- const st = perHunk.length ? perHunk[i] : rec.decisions.call
138
- if (st === 'accepted' || st === 'rejected') continue
139
- if (rec.create === true) {
140
- const content = await readForCache(ctx, session, cache, rec.path)
141
- if (content !== null) { hasLocatablePending = true; break }
142
- continue
143
- }
144
- const precise = rec.toolName === 'edit' && rec.callHunk ? rec.callHunk : rec.hunks[i]
145
- const oldText = precise?.oldText ?? null
146
- const newText = precise?.newText ?? null
147
- if (oldText !== null && oldText === newText) continue
148
- if (newText === null || newText === undefined) continue
149
- const content = await readForCache(ctx, session, cache, rec.path)
150
- if (content !== null && content.indexOf(newText) >= 0) { hasLocatablePending = true; break }
151
- }
152
- return !hasLocatablePending
138
+ const state = await readForCache(ctx, session, cache, rec.path)
139
+ if (state.kind === 'unavailable') return false
140
+ if (state.kind === 'missing') return true
141
+ if (rec.create === true) return false
142
+ const pending = rec.hunks.map((_, idx) => ({ idx, hunk: preciseHunk(rec, idx), status: rec.decisions.perHunk[idx] ?? rec.decisions.call }))
143
+ .filter((item) => item.status === 'pending' && item.hunk && !isNoopHunk(item.hunk))
144
+ if (!pending.length) return true
145
+ const currentFingerprint = fingerprint(state.content)
146
+ if (rec.afterFingerprint && currentFingerprint === rec.afterFingerprint) { rec.conflict = false; return false }
147
+ const locations = locateHunks(state.content, pending.map((item) => item.hunk!))
148
+ if (locations.some((location) => location.matched)) { rec.conflict = false; return false }
149
+ rec.conflict = true
150
+ return false
153
151
  }
154
152
 
155
153
  /**
@@ -161,7 +159,7 @@ export async function recordIsStale(ctx: Ctx, session: Session, cache: Map<strin
161
159
  export async function autoArchiveStale(ctx: Ctx, session: Session, cwd: string, bucket: Map<string, DiffRecord>): Promise<number> {
162
160
  const fs = ctx.get('fs')
163
161
  if (!fs) return 0
164
- const cache = new Map<string, string | null>()
162
+ const cache = new Map<string, ReadState>()
165
163
  const stale: DiffRecord[] = []
166
164
  for (const rec of bucket.values()) {
167
165
  if (rec.archived || rec.superseded === true) continue