@yolk_vat-y/dsh-project-memory 0.5.5 → 0.5.7

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/watch.js CHANGED
@@ -1,10 +1,7 @@
1
1
  import { existsSync, statSync } from 'node:fs'
2
2
  import path from 'node:path'
3
- import { isSupportedCode, isSupportedDoc, isUnwatchableRoot, memoryRootFor, readFileForIndex, relativePath, storeKey, walkDir } from './util/fs.js'
4
- import { buildDocEntries } from './doc-pipeline.js'
5
- import { docEntriesNeedBackfill } from './doc-index.js'
6
- import { scanSymbols } from './symbols.js'
7
- import { linkEntries } from './link.js'
3
+ import { isUnwatchableRoot, memoryRootFor, relativePath, storeKey, walkDir } from './util/fs.js'
4
+ import { OVERSIZE, UNCHANGED, commitFileUpdates, fileKind, planFileIndex, toFileUpdate } from './index-pipeline.js'
8
5
  import { ProjectMemoryStore } from './store.js'
9
6
  import { onFileChanged, isTypeScriptFile } from './enhancer.js'
10
7
 
@@ -94,18 +91,16 @@ export class WatchManager {
94
91
  }
95
92
 
96
93
  async pollRoot(root, state) {
97
- const files = walkDir(root)
98
94
  const seen = new Set()
99
- let changed = 0
95
+ const updates = []
96
+ // rel → 本轮采集的 mtime:size。提交成功后才落进快照;CAS 失败的条目留在原地等下一轮。
97
+ const signatures = new Map()
100
98
 
101
- // First pass: collect all file info and compute hashes/entries (async work outside commit)
102
- const fileUpdates = []
103
-
104
- for (const filePath of files) {
99
+ for (const filePath of walkDir(root)) {
105
100
  const rel = storeKey(relativePath(root, filePath))
106
101
  seen.add(rel)
107
- const ext = path.extname(filePath).toLowerCase()
108
- if (!isSupportedDoc(ext) && !isSupportedCode(ext)) continue
102
+ const kind = fileKind(path.extname(filePath).toLowerCase())
103
+ if (!kind) continue
109
104
 
110
105
  let stats
111
106
  try {
@@ -116,86 +111,49 @@ export class WatchManager {
116
111
  const sig = `${stats.mtimeMs}:${stats.size}`
117
112
  if (state.snapshot[rel] === sig) continue
118
113
 
119
- if (isSupportedCode(ext) && this.config.maxFileSizeMb && stats.size > this.config.maxFileSizeMb * 1024 * 1024) {
120
- continue
121
- }
122
-
123
- // 单次读盘:同一 buffer 供哈希与正文使用
124
- const { hash, buffer } = readFileForIndex(filePath)
125
- const existing = state.store.fileRecord(rel)
126
- // 旧 store 的 doc 条目缺 terms → 一次性回填(即使哈希未变)
127
- const needsBackfill = isSupportedDoc(ext) && docEntriesNeedBackfill(state.store.entries[rel])
128
- if (existing && existing.sha256 === hash && !needsBackfill) continue
129
-
114
+ const record = state.store.fileRecord(rel)
115
+ let plan
130
116
  try {
131
- let entries
132
- if (isSupportedCode(ext)) {
133
- entries = scanSymbols(rel, filePath, buffer.toString('utf8'))
134
- } else {
135
- entries = await buildDocEntries(rel, filePath, {
136
- chunkChars: this.config.chunkChars,
137
- maxChunks: this.config.maxChunksPerFile,
138
- maxFileSizeMb: this.config.maxFileSizeMb,
139
- maxPdfPages: this.config.maxPdfPages,
140
- })
141
- if (entries === null) {
142
- // Dump file - update snapshot so we don't re-hash next poll, but don't index
143
- fileUpdates.push({ rel, expectedHash: state.store.fileRecord(rel)?.sha256, deleted: true, _sig: sig })
144
- changed++
145
- state.failures?.delete(rel)
146
- continue
147
- }
148
- }
149
- fileUpdates.push({ rel, expectedHash: state.store.fileRecord(rel)?.sha256, hash, size: stats.size, entries, type: isSupportedCode(ext) ? 'code' : 'doc', _sig: sig })
150
- changed++
151
- state.failures?.delete(rel)
117
+ plan = await planFileIndex({
118
+ rel,
119
+ filePath,
120
+ kind,
121
+ config: this.config,
122
+ record,
123
+ existingEntries: state.store.entries[rel],
124
+ size: stats.size,
125
+ })
152
126
  } catch (err) {
153
- // Index failed - rollback snapshot so next poll retries
154
- delete state.snapshot[rel]
155
- // 同一个文件的同一个错误只上报一次:坏文件每轮都会重试,逐轮 console.error 会刷屏
156
- if (!state.failures) state.failures = new Map()
127
+ // 坏文件每轮都会重试:同一个文件的同一个错误只上报一次,否则逐轮 console.error 刷屏。
157
128
  if (state.failures.get(rel) !== err.message) {
158
129
  state.failures.set(rel, err.message)
159
130
  console.error(`[dsh-project-memory] re-index failed for ${rel}: ${err.message}`)
160
131
  }
161
132
  continue
162
133
  }
163
- }
164
-
165
- // Single commit with all updates
166
- state.store.commit((s) => {
167
- for (const update of fileUpdates) {
168
- const result = s.applyFileUpdate(update.rel, update)
169
- if (result.skipped) {
170
- // CAS failed - file was modified concurrently, rollback snapshot to retry next poll
171
- delete state.snapshot[update.rel]
172
- // Mark this update to skip snapshot update after commit
173
- update._skipSnapshot = true
174
- }
175
- }
176
-
177
- // Remove deleted files
178
- for (const rel of Object.keys(s.files)) {
179
- if (!seen.has(rel)) {
180
- s.removeFile(rel)
181
- }
134
+ state.failures.delete(rel)
135
+ // 内容未变 / 体积超限也要落快照:否则 mtime:size 快路径永远命中不了,
136
+ // 每次 poll 都要把整个仓库重读一遍再哈希(watch 默认开、15s 一轮)。
137
+ if (plan.key === UNCHANGED || plan.key === OVERSIZE) {
138
+ state.snapshot[rel] = sig
139
+ continue
182
140
  }
183
141
 
184
- if (changed) {
185
- linkEntries(s)
186
- }
187
- })
142
+ signatures.set(rel, sig)
143
+ updates.push(toFileUpdate(rel, plan, record))
144
+ }
188
145
 
189
- // Trigger TS enhancement for code files (TS/JS only)
190
- for (const update of fileUpdates) {
191
- if (update._skipSnapshot) continue
192
- if (update.type === 'code') {
193
- const filePath = path.join(root, update.rel)
194
- if (isTypeScriptFile(filePath)) {
195
- onFileChanged(state.store, update.rel, filePath, this.config, root)
196
- }
146
+ // 单事务写盘;CAS 失败的条目本轮不落快照,下一轮自然重试。
147
+ const { stale } = commitFileUpdates(state.store, { updates, unseen: seen, link: updates.length > 0 })
148
+ const failed = new Set(stale)
149
+ for (const update of updates) {
150
+ if (failed.has(update.rel)) continue
151
+ state.snapshot[update.rel] = signatures.get(update.rel)
152
+ if (update.type !== 'code') continue
153
+ const filePath = path.join(root, update.rel)
154
+ if (isTypeScriptFile(filePath)) {
155
+ onFileChanged(state.store, update.rel, filePath, this.config, root)
197
156
  }
198
- state.snapshot[update.rel] = update._sig
199
157
  }
200
158
  }
201
159
  }