opencode-rag-plugin 1.12.17 → 1.14.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.
Files changed (336) hide show
  1. package/ReadMe.md +15 -13
  2. package/dist/api.d.ts +54 -2
  3. package/dist/api.js +41 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/chunker/base.js +2 -0
  6. package/dist/chunker/base.js.map +1 -1
  7. package/dist/chunker/bash.d.ts +5 -0
  8. package/dist/chunker/bash.js +5 -0
  9. package/dist/chunker/bash.js.map +1 -1
  10. package/dist/chunker/c.d.ts +6 -0
  11. package/dist/chunker/c.js +6 -0
  12. package/dist/chunker/c.js.map +1 -1
  13. package/dist/chunker/cpp.d.ts +6 -0
  14. package/dist/chunker/cpp.js +6 -0
  15. package/dist/chunker/cpp.js.map +1 -1
  16. package/dist/chunker/csharp.d.ts +6 -0
  17. package/dist/chunker/csharp.js +6 -0
  18. package/dist/chunker/csharp.js.map +1 -1
  19. package/dist/chunker/css.d.ts +6 -0
  20. package/dist/chunker/css.js +6 -0
  21. package/dist/chunker/css.js.map +1 -1
  22. package/dist/chunker/doc.d.ts +18 -0
  23. package/dist/chunker/doc.js +18 -0
  24. package/dist/chunker/doc.js.map +1 -1
  25. package/dist/chunker/dockerfile.d.ts +6 -0
  26. package/dist/chunker/dockerfile.js +6 -0
  27. package/dist/chunker/dockerfile.js.map +1 -1
  28. package/dist/chunker/docx.d.ts +18 -0
  29. package/dist/chunker/docx.js +18 -0
  30. package/dist/chunker/docx.js.map +1 -1
  31. package/dist/chunker/excel.d.ts +19 -0
  32. package/dist/chunker/excel.js +19 -0
  33. package/dist/chunker/excel.js.map +1 -1
  34. package/dist/chunker/go.d.ts +5 -0
  35. package/dist/chunker/go.js +5 -0
  36. package/dist/chunker/go.js.map +1 -1
  37. package/dist/chunker/grammar.d.ts +62 -0
  38. package/dist/chunker/grammar.js +133 -0
  39. package/dist/chunker/grammar.js.map +1 -1
  40. package/dist/chunker/html.d.ts +5 -0
  41. package/dist/chunker/html.js +5 -0
  42. package/dist/chunker/html.js.map +1 -1
  43. package/dist/chunker/image.d.ts +31 -0
  44. package/dist/chunker/image.js +50 -4
  45. package/dist/chunker/image.js.map +1 -1
  46. package/dist/chunker/ini.d.ts +5 -0
  47. package/dist/chunker/ini.js +5 -0
  48. package/dist/chunker/ini.js.map +1 -1
  49. package/dist/chunker/java.d.ts +5 -0
  50. package/dist/chunker/java.js +5 -0
  51. package/dist/chunker/java.js.map +1 -1
  52. package/dist/chunker/javascript.d.ts +6 -0
  53. package/dist/chunker/javascript.js +6 -0
  54. package/dist/chunker/javascript.js.map +1 -1
  55. package/dist/chunker/json.d.ts +5 -0
  56. package/dist/chunker/json.js +5 -0
  57. package/dist/chunker/json.js.map +1 -1
  58. package/dist/chunker/kotlin.d.ts +6 -0
  59. package/dist/chunker/kotlin.js +6 -0
  60. package/dist/chunker/kotlin.js.map +1 -1
  61. package/dist/chunker/loader.d.ts +11 -0
  62. package/dist/chunker/loader.js +20 -0
  63. package/dist/chunker/loader.js.map +1 -1
  64. package/dist/chunker/markdown.d.ts +5 -0
  65. package/dist/chunker/markdown.js +5 -0
  66. package/dist/chunker/markdown.js.map +1 -1
  67. package/dist/chunker/pdf.d.ts +19 -0
  68. package/dist/chunker/pdf.js +30 -0
  69. package/dist/chunker/pdf.js.map +1 -1
  70. package/dist/chunker/php.d.ts +5 -0
  71. package/dist/chunker/php.js +5 -0
  72. package/dist/chunker/php.js.map +1 -1
  73. package/dist/chunker/powershell.d.ts +5 -0
  74. package/dist/chunker/powershell.js +5 -0
  75. package/dist/chunker/powershell.js.map +1 -1
  76. package/dist/chunker/python.d.ts +5 -0
  77. package/dist/chunker/python.js +5 -0
  78. package/dist/chunker/python.js.map +1 -1
  79. package/dist/chunker/razor.d.ts +12 -0
  80. package/dist/chunker/razor.js +25 -0
  81. package/dist/chunker/razor.js.map +1 -1
  82. package/dist/chunker/ruby.d.ts +5 -0
  83. package/dist/chunker/ruby.js +5 -0
  84. package/dist/chunker/ruby.js.map +1 -1
  85. package/dist/chunker/rust.d.ts +6 -0
  86. package/dist/chunker/rust.js +6 -0
  87. package/dist/chunker/rust.js.map +1 -1
  88. package/dist/chunker/sln.d.ts +11 -0
  89. package/dist/chunker/sln.js +11 -0
  90. package/dist/chunker/sln.js.map +1 -1
  91. package/dist/chunker/sql.d.ts +5 -0
  92. package/dist/chunker/sql.js +5 -0
  93. package/dist/chunker/sql.js.map +1 -1
  94. package/dist/chunker/ssl.d.ts +12 -0
  95. package/dist/chunker/ssl.js +21 -2
  96. package/dist/chunker/ssl.js.map +1 -1
  97. package/dist/chunker/swift.d.ts +5 -0
  98. package/dist/chunker/swift.js +5 -0
  99. package/dist/chunker/swift.js.map +1 -1
  100. package/dist/chunker/tex.d.ts +12 -0
  101. package/dist/chunker/tex.js +12 -0
  102. package/dist/chunker/tex.js.map +1 -1
  103. package/dist/chunker/toml.d.ts +5 -0
  104. package/dist/chunker/toml.js +5 -0
  105. package/dist/chunker/toml.js.map +1 -1
  106. package/dist/chunker/typescript.d.ts +6 -0
  107. package/dist/chunker/typescript.js +6 -0
  108. package/dist/chunker/typescript.js.map +1 -1
  109. package/dist/chunker/xml.d.ts +5 -0
  110. package/dist/chunker/xml.js +5 -0
  111. package/dist/chunker/xml.js.map +1 -1
  112. package/dist/chunker/yaml.d.ts +5 -0
  113. package/dist/chunker/yaml.js +5 -0
  114. package/dist/chunker/yaml.js.map +1 -1
  115. package/dist/cli/commands/clear.d.ts +12 -0
  116. package/dist/cli/commands/clear.js +81 -0
  117. package/dist/cli/commands/clear.js.map +1 -0
  118. package/dist/cli/commands/describe-image.d.ts +14 -0
  119. package/dist/cli/commands/describe-image.js +68 -0
  120. package/dist/cli/commands/describe-image.js.map +1 -0
  121. package/dist/cli/commands/dump.d.ts +13 -0
  122. package/dist/cli/commands/dump.js +55 -0
  123. package/dist/cli/commands/dump.js.map +1 -0
  124. package/dist/cli/commands/eval.d.ts +32 -0
  125. package/dist/cli/commands/eval.js +158 -0
  126. package/dist/cli/commands/eval.js.map +1 -0
  127. package/dist/cli/commands/index-command.d.ts +17 -0
  128. package/dist/cli/commands/index-command.js +167 -0
  129. package/dist/cli/commands/index-command.js.map +1 -0
  130. package/dist/cli/commands/index.d.ts +16 -0
  131. package/dist/cli/commands/index.js +17 -0
  132. package/dist/cli/commands/index.js.map +1 -0
  133. package/dist/cli/commands/init-helpers.d.ts +101 -0
  134. package/dist/cli/commands/init-helpers.js +398 -0
  135. package/dist/cli/commands/init-helpers.js.map +1 -0
  136. package/dist/cli/commands/init.d.ts +19 -0
  137. package/dist/cli/commands/init.js +251 -0
  138. package/dist/cli/commands/init.js.map +1 -0
  139. package/dist/cli/commands/list.d.ts +13 -0
  140. package/dist/cli/commands/list.js +46 -0
  141. package/dist/cli/commands/list.js.map +1 -0
  142. package/dist/cli/commands/mcp.d.ts +14 -0
  143. package/dist/cli/commands/mcp.js +35 -0
  144. package/dist/cli/commands/mcp.js.map +1 -0
  145. package/dist/cli/commands/query.d.ts +14 -0
  146. package/dist/cli/commands/query.js +86 -0
  147. package/dist/cli/commands/query.js.map +1 -0
  148. package/dist/cli/commands/show.d.ts +13 -0
  149. package/dist/cli/commands/show.js +51 -0
  150. package/dist/cli/commands/show.js.map +1 -0
  151. package/dist/cli/commands/status.d.ts +14 -0
  152. package/dist/cli/commands/status.js +104 -0
  153. package/dist/cli/commands/status.js.map +1 -0
  154. package/dist/cli/commands/ui.d.ts +14 -0
  155. package/dist/cli/commands/ui.js +69 -0
  156. package/dist/cli/commands/ui.js.map +1 -0
  157. package/dist/cli/commands/update.d.ts +14 -0
  158. package/dist/cli/commands/update.js +76 -0
  159. package/dist/cli/commands/update.js.map +1 -0
  160. package/dist/cli/format.d.ts +112 -0
  161. package/dist/cli/format.js +184 -0
  162. package/dist/cli/format.js.map +1 -0
  163. package/dist/cli/helpers.d.ts +51 -0
  164. package/dist/cli/helpers.js +86 -0
  165. package/dist/cli/helpers.js.map +1 -0
  166. package/dist/cli/index.d.ts +28 -0
  167. package/dist/cli/index.js +107 -0
  168. package/dist/cli/index.js.map +1 -0
  169. package/dist/cli/progress.d.ts +39 -0
  170. package/dist/cli/progress.js +137 -0
  171. package/dist/cli/progress.js.map +1 -0
  172. package/dist/cli/types.d.ts +40 -0
  173. package/dist/cli/types.js +3 -0
  174. package/dist/cli/types.js.map +1 -0
  175. package/dist/cli.d.ts +11 -4
  176. package/dist/cli.js +9 -1253
  177. package/dist/cli.js.map +1 -1
  178. package/dist/content/doc.d.ts +1 -1
  179. package/dist/content/doc.js +1 -1
  180. package/dist/content/doc.js.map +1 -1
  181. package/dist/content/docx.d.ts +1 -1
  182. package/dist/content/docx.js +1 -1
  183. package/dist/content/docx.js.map +1 -1
  184. package/dist/content/excel.d.ts +1 -1
  185. package/dist/content/excel.js +1 -1
  186. package/dist/content/excel.js.map +1 -1
  187. package/dist/content/pdf.d.ts +1 -1
  188. package/dist/content/pdf.js +1 -1
  189. package/dist/content/pdf.js.map +1 -1
  190. package/dist/content/reader.d.ts +3 -1
  191. package/dist/content/reader.js +42 -10
  192. package/dist/content/reader.js.map +1 -1
  193. package/dist/core/bootstrap.d.ts +18 -0
  194. package/dist/core/bootstrap.js +9 -15
  195. package/dist/core/bootstrap.js.map +1 -1
  196. package/dist/core/config.d.ts +146 -0
  197. package/dist/core/config.js +66 -14
  198. package/dist/core/config.js.map +1 -1
  199. package/dist/core/doc-progress.d.ts +15 -0
  200. package/dist/core/doc-progress.js +61 -0
  201. package/dist/core/doc-progress.js.map +1 -0
  202. package/dist/core/fileLogger.d.ts +7 -0
  203. package/dist/core/fileLogger.js +2 -0
  204. package/dist/core/fileLogger.js.map +1 -1
  205. package/dist/core/interfaces.d.ts +52 -1
  206. package/dist/core/manifest.d.ts +25 -0
  207. package/dist/core/manifest.js +15 -1
  208. package/dist/core/manifest.js.map +1 -1
  209. package/dist/core/provider-defaults.d.ts +8 -0
  210. package/dist/core/provider-defaults.js +3 -0
  211. package/dist/core/provider-defaults.js.map +1 -1
  212. package/dist/core/rag-injection-flag.d.ts +3 -0
  213. package/dist/core/rag-injection-flag.js +2 -0
  214. package/dist/core/rag-injection-flag.js.map +1 -1
  215. package/dist/core/resolve-api-key.d.ts +1 -0
  216. package/dist/core/resolve-api-key.js +1 -0
  217. package/dist/core/resolve-api-key.js.map +1 -1
  218. package/dist/core/runtime-overrides.d.ts +4 -0
  219. package/dist/core/runtime-overrides.js +4 -1
  220. package/dist/core/runtime-overrides.js.map +1 -1
  221. package/dist/describer/anthropic.d.ts +22 -2
  222. package/dist/describer/anthropic.js +43 -111
  223. package/dist/describer/anthropic.js.map +1 -1
  224. package/dist/describer/describer.d.ts +21 -4
  225. package/dist/describer/describer.js +53 -112
  226. package/dist/describer/describer.js.map +1 -1
  227. package/dist/describer/gemini.d.ts +1 -2
  228. package/dist/describer/gemini.js +21 -114
  229. package/dist/describer/gemini.js.map +1 -1
  230. package/dist/describer/shared.d.ts +3 -0
  231. package/dist/describer/shared.js +23 -0
  232. package/dist/describer/shared.js.map +1 -0
  233. package/dist/embedder/factory.d.ts +25 -0
  234. package/dist/embedder/factory.js +25 -0
  235. package/dist/embedder/factory.js.map +1 -1
  236. package/dist/embedder/health.d.ts +10 -0
  237. package/dist/embedder/health.js +26 -14
  238. package/dist/embedder/health.js.map +1 -1
  239. package/dist/embedder/http.d.ts +47 -0
  240. package/dist/embedder/http.js +61 -3
  241. package/dist/embedder/http.js.map +1 -1
  242. package/dist/eval/index.d.ts +6 -0
  243. package/dist/eval/index.js.map +1 -1
  244. package/dist/eval/run-token-test.js +1 -5
  245. package/dist/eval/run-token-test.js.map +1 -1
  246. package/dist/eval/session-logger.d.ts +5 -0
  247. package/dist/eval/session-logger.js +4 -0
  248. package/dist/eval/session-logger.js.map +1 -1
  249. package/dist/eval/storage.d.ts +9 -0
  250. package/dist/eval/storage.js +9 -5
  251. package/dist/eval/storage.js.map +1 -1
  252. package/dist/eval/token-analysis.d.ts +8 -0
  253. package/dist/eval/token-analysis.js +6 -2
  254. package/dist/eval/token-analysis.js.map +1 -1
  255. package/dist/eval/token-counter.d.ts +1 -3
  256. package/dist/eval/token-counter.js +4 -5
  257. package/dist/eval/token-counter.js.map +1 -1
  258. package/dist/eval/types.d.ts +6 -0
  259. package/dist/eval/types.js +2 -0
  260. package/dist/eval/types.js.map +1 -1
  261. package/dist/index.d.ts +11 -0
  262. package/dist/index.js +12 -3
  263. package/dist/index.js.map +1 -1
  264. package/dist/indexer/description-stage.d.ts +1 -1
  265. package/dist/indexer/description-stage.js +25 -5
  266. package/dist/indexer/description-stage.js.map +1 -1
  267. package/dist/indexer/embed-stage.js +0 -8
  268. package/dist/indexer/embed-stage.js.map +1 -1
  269. package/dist/indexer/pipeline.d.ts +27 -3
  270. package/dist/indexer/pipeline.js +325 -143
  271. package/dist/indexer/pipeline.js.map +1 -1
  272. package/dist/indexer/stats.d.ts +35 -0
  273. package/dist/indexer/stats.js +9 -0
  274. package/dist/indexer/stats.js.map +1 -1
  275. package/dist/indexer/watch.d.ts +25 -0
  276. package/dist/indexer/watch.js +21 -0
  277. package/dist/indexer/watch.js.map +1 -1
  278. package/dist/indexer/worker.d.ts +96 -2
  279. package/dist/indexer/worker.js +109 -24
  280. package/dist/indexer/worker.js.map +1 -1
  281. package/dist/indexer.d.ts +11 -1
  282. package/dist/indexer.js +9 -0
  283. package/dist/indexer.js.map +1 -1
  284. package/dist/mcp/cli.d.ts +3 -0
  285. package/dist/mcp/cli.js +1 -0
  286. package/dist/mcp/cli.js.map +1 -1
  287. package/dist/mcp/handlers.d.ts +41 -0
  288. package/dist/mcp/handlers.js +40 -1
  289. package/dist/mcp/handlers.js.map +1 -1
  290. package/dist/mcp/server.d.ts +8 -0
  291. package/dist/mcp/server.js +18 -1
  292. package/dist/mcp/server.js.map +1 -1
  293. package/dist/opencode/create-read-tool.js +1 -0
  294. package/dist/opencode/create-read-tool.js.map +1 -1
  295. package/dist/opencode/tools.d.ts +8 -17
  296. package/dist/opencode/tools.js +74 -109
  297. package/dist/opencode/tools.js.map +1 -1
  298. package/dist/plugin-entry.d.ts +6 -0
  299. package/dist/plugin-entry.js +6 -0
  300. package/dist/plugin-entry.js.map +1 -1
  301. package/dist/plugin.d.ts +37 -0
  302. package/dist/plugin.js +278 -41
  303. package/dist/plugin.js.map +1 -1
  304. package/dist/retriever/keyword-index.js +9 -9
  305. package/dist/retriever/keyword-index.js.map +1 -1
  306. package/dist/retriever/retriever.js +14 -4
  307. package/dist/retriever/retriever.js.map +1 -1
  308. package/dist/tui.d.ts +5 -0
  309. package/dist/tui.js +101 -13
  310. package/dist/tui.js.map +1 -1
  311. package/dist/types/opencode-plugin.d.ts +4 -1
  312. package/dist/updater.d.ts +23 -1
  313. package/dist/updater.js +55 -16
  314. package/dist/updater.js.map +1 -1
  315. package/dist/vectorstore/factory.d.ts +13 -0
  316. package/dist/vectorstore/factory.js +13 -0
  317. package/dist/vectorstore/factory.js.map +1 -1
  318. package/dist/vectorstore/lancedb.d.ts +78 -1
  319. package/dist/vectorstore/lancedb.js +195 -21
  320. package/dist/vectorstore/lancedb.js.map +1 -1
  321. package/dist/vectorstore/memory.d.ts +5 -0
  322. package/dist/vectorstore/memory.js +11 -0
  323. package/dist/vectorstore/memory.js.map +1 -1
  324. package/dist/watcher.d.ts +24 -0
  325. package/dist/watcher.js +17 -35
  326. package/dist/watcher.js.map +1 -1
  327. package/dist/web/api.d.ts +37 -0
  328. package/dist/web/api.js +54 -0
  329. package/dist/web/api.js.map +1 -1
  330. package/dist/web/server.d.ts +15 -0
  331. package/dist/web/server.js +14 -0
  332. package/dist/web/server.js.map +1 -1
  333. package/dist/web/static.d.ts +8 -0
  334. package/dist/web/static.js +8 -0
  335. package/dist/web/static.js.map +1 -1
  336. package/package.json +11 -15
@@ -1,10 +1,14 @@
1
+ import fs from "node:fs/promises";
1
2
  import path from "node:path";
2
3
  import pLimit from "p-limit";
3
4
  import { scanWorkspaceFiles } from "../content/reader.js";
4
5
  import { loadManifest, saveManifest, normalizeFilePath } from "../core/manifest.js";
5
6
  import { embedBatch } from "../embedder/factory.js";
7
+ import { createVectorStore } from "../vectorstore/factory.js";
8
+ import { swapStoreDirectories } from "../vectorstore/lancedb.js";
6
9
  import { createIndexStats } from "./stats.js";
7
- import { prepareFile } from "./worker.js";
10
+ import { prepareFile, buildTextsToEmbed, storeFileChunks } from "./worker.js";
11
+ import { buildFallbackDescription } from "./description-stage.js";
8
12
  import { getCurrentCommit, getChangedFilesSince, getUntrackedFiles, getRepoRoot } from "./git-diff.js";
9
13
  export { createWatchPassScheduler, createWatchIgnore } from "./watch.js";
10
14
  function createLogger(logger) {
@@ -29,17 +33,77 @@ function tryUpdateLastGitCommit(cwd, manifest) {
29
33
  return false;
30
34
  }
31
35
  }
36
+ const LOCK_FILE = "index.lock";
37
+ const LOCK_MAX_AGE_MS = 5 * 60 * 1000;
38
+ function isPidAlive(pid) {
39
+ try {
40
+ process.kill(pid, 0);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ /**
48
+ * Execute a full index pass: scan workspace, prepare files, generate embeddings
49
+ * (and optionally AI descriptions), and store results into the vector store.
50
+ * Uses a lock file to prevent concurrent passes.
51
+ *
52
+ * @param options - Configuration for the index pass.
53
+ * @returns Aggregate statistics for the pass.
54
+ */
32
55
  export async function runIndexPass(options) {
33
56
  const logger = createLogger(options.logger);
57
+ const lockPath = path.join(options.storePath, LOCK_FILE);
58
+ try {
59
+ const raw = await fs.readFile(lockPath, "utf-8");
60
+ const lock = JSON.parse(raw);
61
+ const age = lock.startedAt ? Date.now() - lock.startedAt : Infinity;
62
+ if (lock.pid && !isPidAlive(lock.pid)) {
63
+ logger.debug(`Stale lock from dead process ${lock.pid} — continuing`);
64
+ }
65
+ else if (lock.startedAt && age < LOCK_MAX_AGE_MS) {
66
+ logger.warn(`Another index pass is running (PID ${lock.pid ?? "unknown"}). Skipping.`);
67
+ return createIndexStats(0, "missing");
68
+ }
69
+ }
70
+ catch {
71
+ // No lock file — proceed
72
+ }
73
+ try {
74
+ await fs.writeFile(lockPath, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), "utf-8");
75
+ }
76
+ catch {
77
+ // Best-effort lock
78
+ }
79
+ try {
80
+ return await runIndexPassInner(options, logger);
81
+ }
82
+ finally {
83
+ try {
84
+ await fs.unlink(lockPath);
85
+ }
86
+ catch { }
87
+ }
88
+ }
89
+ async function runIndexPassInner(options, logger) {
34
90
  const loadResult = await loadManifest(options.storePath);
35
91
  const manifest = loadResult.manifest;
36
92
  let manifestStatus = loadResult.status;
37
93
  let rebuildPerformed = false;
38
- logger.debug(`Manifest loaded: ${manifestStatus}, ${Object.keys(manifest.files).length} entries`);
94
+ logger.info(`Manifest loaded: ${manifestStatus}, ${Object.keys(manifest.files).length} entries`);
39
95
  if (options.force) {
40
96
  manifestStatus = "missing";
41
97
  logger.debug("Force mode: ignoring manifest");
42
98
  }
99
+ // Clear files that had description failures so they are fully re-indexed
100
+ const descFailedPaths = Object.keys(manifest.files).filter((p) => manifest.files[p]?.descriptionFailed);
101
+ if (descFailedPaths.length > 0) {
102
+ logger.info(` ${descFailedPaths.length} file(s) marked as description-failed — re-indexing`);
103
+ for (const p of descFailedPaths) {
104
+ delete manifest.files[p];
105
+ }
106
+ }
43
107
  let filterPaths;
44
108
  let gitDeletedPaths = [];
45
109
  if (!options.force && manifestStatus === "ok" && manifest.lastGitCommit) {
@@ -59,8 +123,10 @@ export async function runIndexPass(options) {
59
123
  }
60
124
  }
61
125
  }
126
+ const scanStart = Date.now();
62
127
  const workspaceFiles = await scanWorkspaceFiles(options.cwd, options.config, logger, options.force ? undefined : manifest, filterPaths);
63
- logger.debug(`Workspace scan complete: ${workspaceFiles.length} files`);
128
+ const scanSec = ((Date.now() - scanStart) / 1000).toFixed(1);
129
+ logger.info(`Workspace scan complete: ${workspaceFiles.length} files in ${scanSec}s`);
64
130
  const existingCount = await options.store.count();
65
131
  // Detect data loss: if the store has far fewer chunks than the manifest expects,
66
132
  // treat it as a corrupt store (e.g. schema migration dropped the old table).
@@ -76,8 +142,10 @@ export async function runIndexPass(options) {
76
142
  manifestStatus = "missing";
77
143
  }
78
144
  }
145
+ // Effective store used throughout the pass — may be a temp store for atomic rebuild.
146
+ let effectiveStore = options.store;
147
+ let tempStorePath;
79
148
  if (options.force || (manifestStatus !== "ok" && existingCount > 0)) {
80
- await options.store.clear();
81
149
  options.keywordIndex?.clear();
82
150
  for (const key of Object.keys(manifest.files)) {
83
151
  delete manifest.files[key];
@@ -88,6 +156,23 @@ export async function runIndexPass(options) {
88
156
  logger.warn("Manifest missing or corrupt; rebuilding full index.");
89
157
  }
90
158
  manifestStatus = options.force ? "missing" : manifestStatus;
159
+ // Build the new index into a temporary store first, then atomically
160
+ // swap on completion. Original data stays untouched if the process
161
+ // is aborted (Ctrl+C, crash) before the swap completes.
162
+ if (options.dimension) {
163
+ tempStorePath = options.storePath + "_tmp";
164
+ try {
165
+ await fs.rm(tempStorePath, { recursive: true, force: true });
166
+ }
167
+ catch { /* may not exist */ }
168
+ effectiveStore = createVectorStore(options.config, tempStorePath, options.dimension);
169
+ logger.debug(`Rebuilding index in temporary store at ${tempStorePath}`);
170
+ }
171
+ else {
172
+ // Fallback — in-memory store or no dimension: clear in place.
173
+ logger.warn("No embedding dimension available; falling back to in-place clear.");
174
+ await options.store.clear();
175
+ }
91
176
  }
92
177
  const stats = createIndexStats(workspaceFiles.length, manifestStatus);
93
178
  stats.rebuildPerformed = rebuildPerformed;
@@ -103,7 +188,8 @@ export async function runIndexPass(options) {
103
188
  const currentPaths = new Set(workspaceFiles.map((file) => file.normalizedPath));
104
189
  let stalePaths;
105
190
  if (filterPaths) {
106
- stalePaths = gitDeletedPaths.map((p) => normalizeFilePath(path.resolve(options.cwd, p)));
191
+ const repoRoot = getRepoRoot(options.cwd) ?? options.cwd;
192
+ stalePaths = gitDeletedPaths.map((p) => normalizeFilePath(path.resolve(repoRoot, p)));
107
193
  }
108
194
  else {
109
195
  stalePaths = Object.keys(manifest.files).filter((p) => !currentPaths.has(p));
@@ -112,7 +198,7 @@ export async function runIndexPass(options) {
112
198
  logger.debug(`Removing ${stalePaths.length} stale files from index...`);
113
199
  const deleteLimit = pLimit(options.config.indexing.concurrency);
114
200
  await Promise.all(stalePaths.map((p) => deleteLimit(async () => {
115
- await options.store.deleteByFilePath(p);
201
+ await effectiveStore.deleteByFilePath(p);
116
202
  options.keywordIndex?.removeByFilePath(p);
117
203
  delete manifest.files[p];
118
204
  stats.deletedFiles++;
@@ -136,143 +222,246 @@ export async function runIndexPass(options) {
136
222
  }
137
223
  }
138
224
  if (stats.deletedFiles > 0) {
139
- logger.info(`Removed ${stats.deletedFiles} deleted files from index.`);
225
+ logger.debug(`Removed ${stats.deletedFiles} deleted files from index.`);
140
226
  }
141
- logger.info(`Processing ${workspaceFiles.length} files (${newCount} new, ${modifiedCount} modified, ${unchangedCount} unchanged)...`);
142
- // ── Phase 1: prepare all files (chunk + description + keyword + textToEmbed) ──
227
+ logger.debug(`Processing ${workspaceFiles.length} files (${newCount} new, ${modifiedCount} modified, ${unchangedCount} unchanged)...`);
228
+ // ── Phase 1: prepare all files (chunk + keyword, defer descriptions if provider present) ──
143
229
  const limit = pLimit(options.config.indexing.concurrency);
144
- let completed = 0;
145
- let started = 0;
146
- const total = workspaceFiles.length;
230
+ const deferDescriptions = !!options.descriptionProvider;
147
231
  const prepared = await Promise.all(workspaceFiles.map((file) => limit(async () => {
148
- const fileLabel = file.normalizedPath;
149
- started++;
150
- logger.info(` Preparing file: ${started} - ${fileLabel}`);
151
- const prep = await prepareFile(file, options.cwd, manifest.files[file.normalizedPath], options.config, options.keywordIndex, options.descriptionProvider, logger);
152
- completed++;
153
- if (total > 20 && completed % 50 === 0) {
154
- logger.info(` Preparing file ${completed}/${total}...`);
232
+ const fileLabel = path.relative(options.cwd, file.normalizedPath).replace(/\\/g, "/");
233
+ const isActive = !file.isEmpty && !file.isTooSmall &&
234
+ (!manifest.files[file.normalizedPath] || manifest.files[file.normalizedPath].hash !== file.hash);
235
+ if (isActive) {
236
+ options.progress?.startFile(fileLabel);
155
237
  }
156
- return prep;
157
- })));
158
- // ── Handle deletions from store for files being re-processed ──
159
- const deletionLimit = pLimit(options.config.indexing.concurrency);
160
- await Promise.all(prepared.map((prep) => deletionLimit(async () => {
161
- if (prep.earlyResult?.isRemoved) {
162
- await options.store.deleteByFilePath(prep.normalizedPath);
163
- options.keywordIndex?.removeByFilePath(prep.normalizedPath);
164
- return;
165
- }
166
- if (prep.isModified && prep.chunks && prep.chunks.length > 0) {
167
- await options.store.deleteByFilePath(prep.normalizedPath);
168
- options.keywordIndex?.removeByFilePath(prep.normalizedPath);
238
+ const prep = await prepareFile(file, options.cwd, manifest.files[file.normalizedPath], options.config, options.keywordIndex, options.descriptionProvider, logger, deferDescriptions);
239
+ if (prep.earlyResult && isActive) {
240
+ options.progress?.finishFile(fileLabel);
169
241
  }
242
+ return prep;
170
243
  })));
171
- // ── Phase 2: global embedding batch ──
172
- const allTextToEmbed = [];
173
- const textPrepIndex = [];
174
- for (let i = 0; i < prepared.length; i++) {
175
- const prep = prepared[i];
176
- if (prep.chunks && prep.textToEmbed && prep.textToEmbed.length > 0) {
177
- for (const text of prep.textToEmbed) {
178
- allTextToEmbed.push(text);
179
- textPrepIndex.push(i);
244
+ // ── Phase 1.5: global description generation (single pool, no nested concurrency) ──
245
+ if (deferDescriptions) {
246
+ const deferredPreps = prepared.filter((p) => p.chunks && p.chunks.length > 0 && p.relPath !== undefined);
247
+ if (deferredPreps.length > 0) {
248
+ const allChunks = [];
249
+ const oversizedChunks = [];
250
+ const maxContentChars = options.config.description?.maxContentChars;
251
+ for (const prep of deferredPreps) {
252
+ for (const chunk of prep.chunks) {
253
+ if (chunk.metadata.contentType !== "image") {
254
+ if (maxContentChars && chunk.content.length > maxContentChars) {
255
+ oversizedChunks.push(chunk);
256
+ }
257
+ else {
258
+ allChunks.push(chunk);
259
+ }
260
+ }
261
+ }
262
+ }
263
+ for (const chunk of oversizedChunks) {
264
+ chunk.description = buildFallbackDescription(chunk);
265
+ }
266
+ // Advance progress to Description stage before descriptions start
267
+ for (const prep of deferredPreps) {
268
+ options.progress?.finishStage(prep.fileLabel);
269
+ }
270
+ if (allChunks.length > 0) {
271
+ try {
272
+ const batchResult = await options.descriptionProvider.generateBatchDescriptions(allChunks, (msg) => logger.debug(msg));
273
+ for (const chunk of allChunks) {
274
+ const desc = batchResult.get(chunk.id);
275
+ if (desc && desc.trim().length > 0) {
276
+ chunk.description = desc;
277
+ }
278
+ }
279
+ }
280
+ catch (err) {
281
+ logger.warn(` Global description generation failed: ${err.message}`);
282
+ for (const prep of deferredPreps) {
283
+ if (prep.chunks.some((c) => c.metadata.contentType !== "image")) {
284
+ prep.descriptionFailed = true;
285
+ }
286
+ }
287
+ }
288
+ }
289
+ // Phase 1.6: build textToEmbed with descriptions injected
290
+ for (const prep of deferredPreps) {
291
+ prep.textToEmbed = buildTextsToEmbed(prep.chunks, prep.relPath, prep.metaHeader ?? "", prep.docPrefix ?? "", prep.isImageFile ?? false);
180
292
  }
181
293
  }
182
294
  }
183
- let allEmbeddings = [];
184
- if (allTextToEmbed.length > 0) {
185
- const embedConcurrency = options.config.indexing.embedConcurrency ?? 1;
186
- logger.info(` Embedding ${allTextToEmbed.length} chunks in global batch (concurrency: ${embedConcurrency})...`);
187
- allEmbeddings = await embedBatch(options.embedder, allTextToEmbed, options.config.indexing.embedBatchSize, "document", embedConcurrency);
188
- logger.debug(` Embedding batch complete: ${allEmbeddings.length} vectors`);
295
+ // ── Phase 2+3: per-file delete, embed + store (in parallel) ──
296
+ // Deletions for modified/re-removed files happen per-worker, right before
297
+ // embedding, so that an abort mid-embed doesn't orphan old entries.
298
+ const isOllama = options.embedder.name === "ollama";
299
+ const ollamaMaxBatch = options.config.indexing.ollamaMaxBatchSize ?? 4000;
300
+ const defaultBatchSize = options.config.indexing.embedBatchSize;
301
+ const defaultConcurrency = options.config.indexing.embedConcurrency ?? 1;
302
+ // File metadata look-up for manifest entries
303
+ const fileMeta = new Map(workspaceFiles.map((f) => [f.normalizedPath, { mtime: f.mtime, size: f.size }]));
304
+ // Serialised manifest-save queue — prevents concurrent write races and acts
305
+ // as a checkpoint for Ctrl+C resilience. Each worker appends to this chain
306
+ // after a successful store, so previously completed files are never lost.
307
+ let manifestSaveChain = Promise.resolve(undefined);
308
+ function enqueueManifestSave() {
309
+ manifestSaveChain = manifestSaveChain.then(() => saveManifest(options.storePath, manifest));
189
310
  }
190
- // ── Distribute embeddings back to prepared files ──
191
- const embedByPrep = new Map();
192
- for (let i = 0; i < textPrepIndex.length; i++) {
193
- const prepIdx = textPrepIndex[i];
194
- const emb = allEmbeddings[i];
195
- if (!embedByPrep.has(prepIdx)) {
196
- embedByPrep.set(prepIdx, []);
311
+ const aborted = () => options.abortSignal?.aborted ?? false;
312
+ const embedStoreLimit = pLimit(options.config.indexing.concurrency);
313
+ const workerResults = await Promise.all(prepared.map((prep) => embedStoreLimit(async () => {
314
+ // ── Check abort signal before any work ──
315
+ if (aborted()) {
316
+ return { normalizedPath: prep.normalizedPath, skipped: true };
197
317
  }
198
- if (emb) {
199
- embedByPrep.get(prepIdx).push(emb);
200
- }
201
- }
202
- // ── Phase 3: store all files with their embeddings (batched) ──
203
- logger.debug(`Phase 3: storing chunks for ${prepared.length} files...`);
204
- const allValidChunks = [];
205
- const workerResults = prepared.map((prep, idx) => {
318
+ // ── Early results (empty / too-small / unchanged / chunking failure) ──
206
319
  if (prep.earlyResult) {
320
+ // Remove stale manifest/store entries for files that no longer produce chunks
321
+ if (prep.earlyResult.isRemoved) {
322
+ await effectiveStore.deleteByFilePath(prep.normalizedPath);
323
+ options.keywordIndex?.removeByFilePath(prep.normalizedPath);
324
+ delete manifest.files[prep.normalizedPath];
325
+ enqueueManifestSave();
326
+ }
207
327
  return prep.earlyResult;
208
328
  }
209
- if (!prep.chunks || !prep.textToEmbed) {
329
+ if (!prep.chunks || !prep.textToEmbed || prep.textToEmbed.length === 0) {
330
+ options.progress?.finishFile(prep.fileLabel);
331
+ return {
332
+ normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
333
+ fileLabel: prep.fileLabel,
334
+ isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
335
+ isTooSmall: false, isRemoved: true, hadChunks: false,
336
+ descriptionFailed: prep.descriptionFailed,
337
+ };
338
+ }
339
+ // ── Embed ──
340
+ const batchSize = isOllama
341
+ ? Math.min(prep.textToEmbed.length, ollamaMaxBatch)
342
+ : defaultBatchSize;
343
+ const concurrency = isOllama ? 1 : defaultConcurrency;
344
+ options.progress?.finishStage(prep.fileLabel);
345
+ let embeddings;
346
+ try {
347
+ embeddings = await embedBatch(options.embedder, prep.textToEmbed, batchSize, "document", concurrency);
348
+ }
349
+ catch (err) {
350
+ logger.warn(` ${prep.fileLabel} (embedding failed: ${err.message})`);
351
+ options.progress?.failFile(prep.fileLabel);
210
352
  return {
211
- normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0, fileLabel: prep.fileLabel,
212
- isNew: false, isModified: false, isUnchanged: false, isEmpty: false, isTooSmall: false, isRemoved: true, hadChunks: false,
353
+ normalizedPath: prep.normalizedPath, hash: prep.hash, chunkCount: 0,
354
+ fileLabel: prep.fileLabel,
355
+ isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
356
+ isTooSmall: false, isRemoved: true, hadChunks: false,
357
+ descriptionFailed: prep.descriptionFailed,
358
+ };
359
+ }
360
+ // ── Store (new data first; orphan cleanup is handled inside the store) ──
361
+ const result = await storeFileChunks(prep, embeddings, effectiveStore, logger);
362
+ // ── Update manifest in-memory and enqueue an atomic save ──
363
+ if (result.chunkCount > 0 && !result.isRemoved) {
364
+ const meta = fileMeta.get(result.normalizedPath);
365
+ manifest.files[result.normalizedPath] = {
366
+ hash: result.hash,
367
+ chunkCount: result.chunkCount,
368
+ indexedAt: Date.now(),
369
+ mtime: meta?.mtime,
370
+ size: meta?.size,
371
+ descriptionFailed: result.descriptionFailed,
213
372
  };
373
+ enqueueManifestSave();
214
374
  }
215
- const embeddings = embedByPrep.get(idx) ?? [];
216
- for (let i = 0; i < prep.chunks.length; i++) {
217
- const emb = embeddings[i];
218
- if (Array.isArray(emb) && emb.length > 0 && typeof emb[0] === "number") {
219
- prep.chunks[i].embedding = emb;
375
+ else if (result.isRemoved) {
376
+ delete manifest.files[result.normalizedPath];
377
+ enqueueManifestSave();
378
+ }
379
+ options.progress?.finishFile(prep.fileLabel);
380
+ return result;
381
+ })));
382
+ // Strip skipped sentinels
383
+ const finalResults = [];
384
+ for (const r of workerResults) {
385
+ if (r.skipped)
386
+ break;
387
+ finalResults.push(r);
388
+ }
389
+ // Drain any in-flight manifest saves so all file entries are durable
390
+ await manifestSaveChain;
391
+ // Update mtime/size for unchanged files (speeds up the next scan)
392
+ for (const { normalizedPath, mtime, size } of workspaceFiles) {
393
+ const entry = manifest.files[normalizedPath];
394
+ if (entry && (mtime !== undefined || size !== undefined)) {
395
+ entry.mtime = mtime;
396
+ entry.size = size;
397
+ }
398
+ }
399
+ // ── Aggregate stats ──
400
+ aggregateStats(stats, finalResults);
401
+ // Update timestamps; advance lastGitCommit ONLY on a complete pass
402
+ manifest.lastIndexedAt = Date.now();
403
+ if (!aborted()) {
404
+ tryUpdateLastGitCommit(options.cwd, manifest);
405
+ }
406
+ // ── Atomically promote temp store if a full rebuild was performed ──
407
+ if (tempStorePath) {
408
+ if (!aborted()) {
409
+ try {
410
+ await effectiveStore.close();
411
+ await options.store.close();
412
+ // Swap the newly-built temp directory into the real path
413
+ await swapStoreDirectories(tempStorePath, options.storePath);
414
+ // Re-open the original store handle so callers can search the new data
415
+ if (typeof options.store.reopen === "function") {
416
+ await options.store.reopen(options.storePath);
417
+ }
418
+ logger.debug(`Promoted temporary store ${tempStorePath} → ${options.storePath}`);
220
419
  }
221
- else {
222
- prep.chunks[i].embedding = undefined;
420
+ catch (err) {
421
+ logger.warn(`Could not promote temporary store: ${err.message}. ` +
422
+ `Original data preserved at ${options.storePath}`);
423
+ try {
424
+ await fs.rm(tempStorePath, { recursive: true, force: true });
425
+ }
426
+ catch { }
223
427
  }
224
428
  }
225
- const validChunks = prep.chunks.filter((c) => c.embedding && c.embedding.length > 0);
226
- allValidChunks.push(...validChunks);
227
- logger.info(` ${prep.fileLabel} (${prep.chunks.length} chunks${prep.isModified ? ", modified" : ", new"})`);
228
- return {
229
- normalizedPath: prep.normalizedPath,
230
- hash: prep.hash,
231
- chunkCount: prep.chunks.length,
232
- fileLabel: prep.fileLabel,
233
- isNew: !prep.isModified,
234
- isModified: prep.isModified,
235
- isUnchanged: false,
236
- isEmpty: false,
237
- isTooSmall: false,
238
- isRemoved: false,
239
- hadChunks: prep.chunks.length > 0,
240
- };
241
- });
242
- if (allValidChunks.length > 0) {
243
- const STORE_BATCH = 1000;
244
- for (let i = 0; i < allValidChunks.length; i += STORE_BATCH) {
245
- const batch = allValidChunks.slice(i, i + STORE_BATCH);
246
- await options.store.addChunks(batch);
429
+ else {
430
+ // Aborted — discard temp, keep original data intact
431
+ effectiveStore.close().catch(() => { });
432
+ try {
433
+ await fs.rm(tempStorePath, { recursive: true, force: true });
434
+ }
435
+ catch { }
436
+ logger.debug("Index pass cancelled; discarded temporary store.");
247
437
  }
248
- logger.debug(` Stored ${allValidChunks.length} chunks in batched writes`);
249
438
  }
250
- // ── Aggregate stats and update manifest ──
251
- aggregateStats(stats, workerResults, manifest, workspaceFiles);
252
- manifest.lastIndexedAt = Date.now();
253
- tryUpdateLastGitCommit(options.cwd, manifest);
439
+ // Save manifest and keyword index (always to the real store path — after
440
+ // a successful swap this points to the new data; after an abort it's the old).
254
441
  await saveManifest(options.storePath, manifest);
255
442
  await options.keywordIndex?.save(options.storePath);
256
- stats.finalCount = await options.store.count();
443
+ // Count from the store — after a successful swap, the original handle has
444
+ // been reopened pointing to the new directory.
445
+ try {
446
+ stats.finalCount = await options.store.count();
447
+ }
448
+ catch {
449
+ stats.finalCount = (tempStorePath ? 0 : stats.totalChunks);
450
+ }
257
451
  return stats;
258
452
  }
259
- function aggregateStats(stats, results, manifest, workspaceFiles) {
260
- const fileMeta = new Map(workspaceFiles.map((f) => [f.normalizedPath, { mtime: f.mtime, size: f.size }]));
453
+ function aggregateStats(stats, results) {
261
454
  for (const result of results) {
262
455
  if (result.isEmpty) {
263
456
  stats.skippedEmptyFiles++;
264
- if (result.isRemoved) {
265
- delete manifest.files[result.normalizedPath];
457
+ if (result.isRemoved)
266
458
  stats.removedFiles++;
267
- }
268
459
  continue;
269
460
  }
270
461
  if (result.isTooSmall) {
271
462
  stats.skippedSmallFiles++;
272
- if (result.isRemoved) {
273
- delete manifest.files[result.normalizedPath];
463
+ if (result.isRemoved)
274
464
  stats.removedFiles++;
275
- }
276
465
  continue;
277
466
  }
278
467
  if (result.isUnchanged) {
@@ -280,7 +469,6 @@ function aggregateStats(stats, results, manifest, workspaceFiles) {
280
469
  continue;
281
470
  }
282
471
  if (result.isRemoved) {
283
- delete manifest.files[result.normalizedPath];
284
472
  stats.removedFiles++;
285
473
  continue;
286
474
  }
@@ -291,65 +479,59 @@ function aggregateStats(stats, results, manifest, workspaceFiles) {
291
479
  stats.newFiles++;
292
480
  }
293
481
  if (result.chunkCount > 0) {
294
- const meta = fileMeta.get(result.normalizedPath);
295
- manifest.files[result.normalizedPath] = {
296
- hash: result.hash,
297
- chunkCount: result.chunkCount,
298
- indexedAt: Date.now(),
299
- mtime: meta?.mtime,
300
- size: meta?.size,
301
- };
482
+ if (result.descriptionFailed) {
483
+ stats.descriptionFailedFiles++;
484
+ }
302
485
  stats.totalChunks += result.chunkCount;
303
486
  stats.batchesFlushed++;
304
487
  }
305
488
  }
306
- for (const { normalizedPath, mtime, size } of workspaceFiles) {
307
- const entry = manifest.files[normalizedPath];
308
- if (entry && (mtime !== undefined || size !== undefined)) {
309
- entry.mtime = mtime;
310
- entry.size = size;
311
- }
312
- }
313
489
  }
314
- export async function getIndexStatusSummary(cwd, storePath, config, store) {
490
+ export async function getIndexStatusSummary(cwd, storePath, config, store, skipScan) {
315
491
  const loadResult = await loadManifest(storePath);
316
492
  const manifest = loadResult.manifest;
317
- const workspaceFiles = await scanWorkspaceFiles(cwd, config, undefined, manifest);
318
493
  const storeCount = await store.count();
319
494
  if (loadResult.status !== "ok") {
320
495
  return {
321
496
  manifestStatus: loadResult.status,
322
497
  manifestEntries: 0,
323
498
  upToDateFiles: 0,
324
- pendingFiles: workspaceFiles.length,
499
+ pendingFiles: 0,
325
500
  rebuildRequired: storeCount > 0,
326
501
  storeChunkCount: storeCount,
327
502
  manifestExpectedChunks: 0,
328
503
  };
329
504
  }
330
- const currentPaths = new Set(workspaceFiles.map((file) => file.normalizedPath));
331
505
  let upToDateFiles = 0;
332
506
  let pendingFiles = 0;
333
- for (const file of workspaceFiles) {
334
- const previous = manifest.files[file.normalizedPath];
335
- if (file.isEmpty || file.isTooSmall) {
336
- if (previous)
507
+ let manifestExpectedChunks = 0;
508
+ if (!skipScan) {
509
+ const workspaceFiles = await scanWorkspaceFiles(cwd, config, undefined, manifest);
510
+ const currentPaths = new Set(workspaceFiles.map((file) => file.normalizedPath));
511
+ for (const file of workspaceFiles) {
512
+ const previous = manifest.files[file.normalizedPath];
513
+ if (file.isEmpty || file.isTooSmall) {
514
+ if (previous)
515
+ pendingFiles++;
516
+ continue;
517
+ }
518
+ if (previous && previous.hash === file.hash) {
519
+ upToDateFiles++;
520
+ }
521
+ else {
337
522
  pendingFiles++;
338
- continue;
339
- }
340
- if (previous && previous.hash === file.hash) {
341
- upToDateFiles++;
523
+ }
342
524
  }
343
- else {
344
- pendingFiles++;
525
+ for (const indexedPath of Object.keys(manifest.files)) {
526
+ if (!currentPaths.has(indexedPath)) {
527
+ pendingFiles++;
528
+ }
345
529
  }
530
+ manifestExpectedChunks = Object.values(manifest.files).reduce((sum, entry) => sum + entry.chunkCount, 0);
346
531
  }
347
- for (const indexedPath of Object.keys(manifest.files)) {
348
- if (!currentPaths.has(indexedPath)) {
349
- pendingFiles++;
350
- }
532
+ else {
533
+ manifestExpectedChunks = Object.values(manifest.files).reduce((sum, entry) => sum + entry.chunkCount, 0);
351
534
  }
352
- const manifestExpectedChunks = Object.values(manifest.files).reduce((sum, entry) => sum + entry.chunkCount, 0);
353
535
  return {
354
536
  manifestStatus: loadResult.status,
355
537
  manifestEntries: Object.keys(manifest.files).length,