@equinor/fusion-framework-cli-plugin-ai-index 3.0.6 → 3.0.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 (195) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/esm/bin/apply-metadata.js +13 -5
  3. package/dist/esm/bin/apply-metadata.js.map +1 -1
  4. package/dist/esm/bin/apply-schema.js +7 -1
  5. package/dist/esm/bin/apply-schema.js.map +1 -1
  6. package/dist/esm/bin/apply-schema.test.js +2 -1
  7. package/dist/esm/bin/apply-schema.test.js.map +1 -1
  8. package/dist/esm/bin/{delete-removed-files.js → create-delete-removed-files-stream.js} +12 -1
  9. package/dist/esm/bin/create-delete-removed-files-stream.js.map +1 -0
  10. package/dist/esm/bin/embed.js +260 -97
  11. package/dist/esm/bin/embed.js.map +1 -1
  12. package/dist/esm/bin/execute-pipeline.js +5 -0
  13. package/dist/esm/bin/execute-pipeline.js.map +1 -1
  14. package/dist/esm/bin/get-diff.js +6 -0
  15. package/dist/esm/bin/get-diff.js.map +1 -1
  16. package/dist/esm/{embeddings-command.options.js → command-options-schema.js} +1 -1
  17. package/dist/esm/command-options-schema.js.map +1 -0
  18. package/dist/esm/{embeddings-command.js → command.js} +4 -2
  19. package/dist/esm/command.js.map +1 -0
  20. package/dist/esm/{create-command.js → create-index-command.js} +13 -5
  21. package/dist/esm/create-index-command.js.map +1 -0
  22. package/dist/esm/{schema.js → define-index-schema.js} +1 -1
  23. package/dist/esm/define-index-schema.js.map +1 -0
  24. package/dist/esm/delete-command.js +11 -2
  25. package/dist/esm/delete-command.js.map +1 -1
  26. package/dist/esm/delete-index-command.js +4 -0
  27. package/dist/esm/delete-index-command.js.map +1 -1
  28. package/dist/esm/{delete-command.options.js → delete-options-schema.js} +1 -1
  29. package/dist/esm/delete-options-schema.js.map +1 -0
  30. package/dist/esm/embed-command.js +3 -4
  31. package/dist/esm/embed-command.js.map +1 -1
  32. package/dist/esm/index.js +3 -3
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/search-command.js +20 -0
  35. package/dist/esm/search-command.js.map +1 -1
  36. package/dist/esm/utils/git/{metadata.js → extract-git-metadata.js} +4 -2
  37. package/dist/esm/utils/git/extract-git-metadata.js.map +1 -0
  38. package/dist/esm/utils/git/get-changed-files.js +87 -0
  39. package/dist/esm/utils/git/get-changed-files.js.map +1 -0
  40. package/dist/esm/utils/git/{file-changes.js → get-file-status.js} +9 -95
  41. package/dist/esm/utils/git/get-file-status.js.map +1 -0
  42. package/dist/esm/utils/git/{status.js → get-git-status.js} +5 -2
  43. package/dist/esm/utils/git/get-git-status.js.map +1 -0
  44. package/dist/esm/utils/git/get-git.js +30 -0
  45. package/dist/esm/utils/git/get-git.js.map +1 -0
  46. package/dist/esm/utils/git/index.js +7 -4
  47. package/dist/esm/utils/git/index.js.map +1 -1
  48. package/dist/esm/utils/git/is-file-changed.js +19 -0
  49. package/dist/esm/utils/git/is-file-changed.js.map +1 -0
  50. package/dist/esm/utils/git/resolve-project-root.js +22 -0
  51. package/dist/esm/utils/git/resolve-project-root.js.map +1 -0
  52. package/dist/esm/utils/markdown/index.js +3 -1
  53. package/dist/esm/utils/markdown/index.js.map +1 -1
  54. package/dist/esm/utils/markdown/is-markdown-file.js +10 -0
  55. package/dist/esm/utils/markdown/is-markdown-file.js.map +1 -0
  56. package/dist/esm/utils/markdown/parse-markdown-file.js +29 -0
  57. package/dist/esm/utils/markdown/parse-markdown-file.js.map +1 -0
  58. package/dist/esm/utils/markdown/{parser.js → parse-markdown.js} +8 -37
  59. package/dist/esm/utils/markdown/parse-markdown.js.map +1 -0
  60. package/dist/esm/utils/{embedding-dimensions.js → resolve-embedding-dimensions.js} +3 -1
  61. package/dist/esm/utils/resolve-embedding-dimensions.js.map +1 -0
  62. package/dist/esm/utils/{package-resolver.js → resolve-package.js} +4 -1
  63. package/dist/esm/utils/resolve-package.js.map +1 -0
  64. package/dist/esm/utils/ts-doc/create-typescript-document.js +27 -0
  65. package/dist/esm/utils/ts-doc/create-typescript-document.js.map +1 -0
  66. package/dist/esm/utils/ts-doc/extract-document-from-class-node.js +86 -0
  67. package/dist/esm/utils/ts-doc/extract-document-from-class-node.js.map +1 -0
  68. package/dist/esm/utils/ts-doc/extract-document-from-node.js +82 -0
  69. package/dist/esm/utils/ts-doc/extract-document-from-node.js.map +1 -0
  70. package/dist/esm/utils/ts-doc/index.js +3 -1
  71. package/dist/esm/utils/ts-doc/index.js.map +1 -1
  72. package/dist/esm/utils/ts-doc/is-typescript-file.js +10 -0
  73. package/dist/esm/utils/ts-doc/is-typescript-file.js.map +1 -0
  74. package/dist/esm/utils/ts-doc/{constants.js → node-kinds.js} +1 -1
  75. package/dist/esm/utils/ts-doc/node-kinds.js.map +1 -0
  76. package/dist/esm/utils/ts-doc/parse-ts-doc-from-file-sync.js +23 -0
  77. package/dist/esm/utils/ts-doc/parse-ts-doc-from-file-sync.js.map +1 -0
  78. package/dist/esm/utils/ts-doc/parse-ts-doc-sync.js +18 -0
  79. package/dist/esm/utils/ts-doc/parse-ts-doc-sync.js.map +1 -0
  80. package/dist/esm/utils/ts-doc/process-source-file.js +22 -0
  81. package/dist/esm/utils/ts-doc/process-source-file.js.map +1 -0
  82. package/dist/esm/utils/zod-to-azure-fields.js +7 -0
  83. package/dist/esm/utils/zod-to-azure-fields.js.map +1 -1
  84. package/dist/esm/utils/zod-to-azure-fields.test.js +8 -7
  85. package/dist/esm/utils/zod-to-azure-fields.test.js.map +1 -1
  86. package/dist/esm/version.js +1 -1
  87. package/dist/tsconfig.tsbuildinfo +1 -1
  88. package/dist/types/bin/apply-schema.d.ts +1 -1
  89. package/dist/types/config.d.ts +1 -1
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/utils/git/get-changed-files.d.ts +13 -0
  92. package/dist/types/utils/git/get-file-status.d.ts +13 -0
  93. package/dist/types/utils/git/{git-client.d.ts → get-git.d.ts} +0 -10
  94. package/dist/types/utils/git/index.d.ts +7 -4
  95. package/dist/types/utils/git/is-file-changed.d.ts +12 -0
  96. package/dist/types/utils/git/resolve-project-root.d.ts +10 -0
  97. package/dist/types/utils/markdown/index.d.ts +3 -1
  98. package/dist/types/utils/markdown/is-markdown-file.d.ts +7 -0
  99. package/dist/types/utils/markdown/parse-markdown-file.d.ts +14 -0
  100. package/dist/types/utils/markdown/parse-markdown.d.ts +14 -0
  101. package/dist/types/utils/ts-doc/create-typescript-document.d.ts +12 -0
  102. package/dist/types/utils/ts-doc/extract-document-from-class-node.d.ts +15 -0
  103. package/dist/types/utils/ts-doc/extract-document-from-node.d.ts +20 -0
  104. package/dist/types/utils/ts-doc/index.d.ts +3 -1
  105. package/dist/types/utils/ts-doc/is-typescript-file.d.ts +7 -0
  106. package/dist/types/utils/ts-doc/parse-ts-doc-from-file-sync.d.ts +14 -0
  107. package/dist/types/utils/ts-doc/parse-ts-doc-sync.d.ts +12 -0
  108. package/dist/types/utils/ts-doc/process-source-file.d.ts +11 -0
  109. package/dist/types/version.d.ts +1 -1
  110. package/package.json +8 -8
  111. package/src/bin/apply-metadata.ts +69 -57
  112. package/src/bin/apply-schema.test.ts +2 -1
  113. package/src/bin/apply-schema.ts +6 -1
  114. package/src/bin/{delete-removed-files.ts → create-delete-removed-files-stream.ts} +12 -1
  115. package/src/bin/embed.ts +389 -145
  116. package/src/bin/execute-pipeline.ts +5 -0
  117. package/src/bin/get-diff.ts +7 -1
  118. package/src/bin/types.ts +1 -1
  119. package/src/{embeddings-command.ts → command.ts} +3 -1
  120. package/src/config.ts +1 -1
  121. package/src/{create-command.ts → create-index-command.ts} +15 -5
  122. package/src/delete-command.ts +11 -2
  123. package/src/delete-index-command.ts +4 -0
  124. package/src/embed-command.ts +3 -6
  125. package/src/index.ts +3 -3
  126. package/src/search-command.ts +20 -0
  127. package/src/utils/git/{metadata.ts → extract-git-metadata.ts} +3 -1
  128. package/src/utils/git/get-changed-files.ts +93 -0
  129. package/src/utils/git/{file-changes.ts → get-file-status.ts} +9 -103
  130. package/src/utils/git/{status.ts → get-git-status.ts} +4 -1
  131. package/src/utils/git/{git-client.ts → get-git.ts} +4 -22
  132. package/src/utils/git/index.ts +7 -4
  133. package/src/utils/git/is-file-changed.ts +21 -0
  134. package/src/utils/git/resolve-project-root.ts +22 -0
  135. package/src/utils/markdown/index.ts +3 -1
  136. package/src/utils/markdown/is-markdown-file.ts +9 -0
  137. package/src/utils/markdown/parse-markdown-file.ts +36 -0
  138. package/src/utils/markdown/{parser.ts → parse-markdown.ts} +16 -51
  139. package/src/utils/{embedding-dimensions.ts → resolve-embedding-dimensions.ts} +2 -0
  140. package/src/utils/{package-resolver.ts → resolve-package.ts} +3 -0
  141. package/src/utils/ts-doc/create-typescript-document.ts +36 -0
  142. package/src/utils/ts-doc/extract-document-from-class-node.ts +106 -0
  143. package/src/utils/ts-doc/extract-document-from-node.ts +120 -0
  144. package/src/utils/ts-doc/index.ts +3 -1
  145. package/src/utils/ts-doc/is-typescript-file.ts +9 -0
  146. package/src/utils/ts-doc/parse-ts-doc-from-file-sync.ts +32 -0
  147. package/src/utils/ts-doc/parse-ts-doc-sync.ts +23 -0
  148. package/src/utils/ts-doc/process-source-file.ts +30 -0
  149. package/src/utils/zod-to-azure-fields.test.ts +10 -7
  150. package/src/utils/zod-to-azure-fields.ts +7 -0
  151. package/src/version.ts +1 -1
  152. package/dist/esm/bin/delete-removed-files.js.map +0 -1
  153. package/dist/esm/bin/file-stream.js +0 -22
  154. package/dist/esm/bin/file-stream.js.map +0 -1
  155. package/dist/esm/create-command.js.map +0 -1
  156. package/dist/esm/delete-command.options.js.map +0 -1
  157. package/dist/esm/embeddings-command.js.map +0 -1
  158. package/dist/esm/embeddings-command.options.js.map +0 -1
  159. package/dist/esm/schema.js.map +0 -1
  160. package/dist/esm/utils/embedding-dimensions.js.map +0 -1
  161. package/dist/esm/utils/git/file-changes.js.map +0 -1
  162. package/dist/esm/utils/git/git-client.js +0 -48
  163. package/dist/esm/utils/git/git-client.js.map +0 -1
  164. package/dist/esm/utils/git/metadata.js.map +0 -1
  165. package/dist/esm/utils/git/status.js.map +0 -1
  166. package/dist/esm/utils/markdown/parser.js.map +0 -1
  167. package/dist/esm/utils/package-resolver.js.map +0 -1
  168. package/dist/esm/utils/ts-doc/constants.js.map +0 -1
  169. package/dist/esm/utils/ts-doc/extractors.js +0 -189
  170. package/dist/esm/utils/ts-doc/extractors.js.map +0 -1
  171. package/dist/esm/utils/ts-doc/parser.js +0 -46
  172. package/dist/esm/utils/ts-doc/parser.js.map +0 -1
  173. package/dist/types/bin/file-stream.d.ts +0 -1
  174. package/dist/types/utils/git/file-changes.d.ts +0 -36
  175. package/dist/types/utils/markdown/parser.d.ts +0 -34
  176. package/dist/types/utils/ts-doc/extractors.d.ts +0 -42
  177. package/dist/types/utils/ts-doc/parser.d.ts +0 -32
  178. package/src/bin/file-stream.ts +0 -34
  179. package/src/utils/ts-doc/extractors.ts +0 -260
  180. package/src/utils/ts-doc/parser.ts +0 -60
  181. /package/dist/types/bin/{delete-removed-files.d.ts → create-delete-removed-files-stream.d.ts} +0 -0
  182. /package/dist/types/{embeddings-command.options.d.ts → command-options-schema.d.ts} +0 -0
  183. /package/dist/types/{embeddings-command.d.ts → command.d.ts} +0 -0
  184. /package/dist/types/{create-command.d.ts → create-index-command.d.ts} +0 -0
  185. /package/dist/types/{schema.d.ts → define-index-schema.d.ts} +0 -0
  186. /package/dist/types/{delete-command.options.d.ts → delete-options-schema.d.ts} +0 -0
  187. /package/dist/types/utils/git/{metadata.d.ts → extract-git-metadata.d.ts} +0 -0
  188. /package/dist/types/utils/git/{status.d.ts → get-git-status.d.ts} +0 -0
  189. /package/dist/types/utils/{embedding-dimensions.d.ts → resolve-embedding-dimensions.d.ts} +0 -0
  190. /package/dist/types/utils/{package-resolver.d.ts → resolve-package.d.ts} +0 -0
  191. /package/dist/types/utils/ts-doc/{constants.d.ts → node-kinds.d.ts} +0 -0
  192. /package/src/{embeddings-command.options.ts → command-options-schema.ts} +0 -0
  193. /package/src/{schema.ts → define-index-schema.ts} +0 -0
  194. /package/src/{delete-command.options.ts → delete-options-schema.ts} +0 -0
  195. /package/src/utils/ts-doc/{constants.ts → node-kinds.ts} +0 -0
package/src/bin/embed.ts CHANGED
@@ -20,16 +20,22 @@ import { getFileStatus, resolveProjectRoot } from '../utils/git/index.js';
20
20
  import { isTypescriptFile, parseTsDocFromFileSync } from '../utils/ts-doc/index.js';
21
21
 
22
22
  import { getDiff } from './get-diff.js';
23
- import { createDeleteRemovedFilesStream } from './delete-removed-files.js';
23
+ import { createDeleteRemovedFilesStream } from './create-delete-removed-files-stream.js';
24
24
  import { applyMetadata } from './apply-metadata.js';
25
25
  import { applySchema } from './apply-schema.js';
26
26
  import type {
27
27
  DocumentEntry,
28
28
  EmbeddingsBinOptions,
29
+ DeleteRemovedFilesResult,
29
30
  ProcessedFile,
30
31
  UpdateVectorStoreResult,
31
32
  } from './types.js';
33
+ import type { ChangedFile } from '../utils/git/index.js';
34
+ import type { CommandOptions } from '../command-options-schema.js';
35
+ import type { FusionAIConfigWithIndex } from '../config.js';
36
+ import type { FrameworkInstance } from '@equinor/fusion-framework-cli-plugin-ai-base';
32
37
  import type { VectorStoreDocument } from '@equinor/fusion-framework-module-ai/lib';
38
+ import type { Observable } from 'rxjs';
33
39
  import { readFileSync } from 'node:fs';
34
40
  import { generateChunkId } from '../utils/generate-chunk-id.js';
35
41
 
@@ -61,12 +67,16 @@ class ProgressDisplay {
61
67
  /** Minimum interval (ms) between CI progress lines for the same line slot. */
62
68
  private static CI_LOG_INTERVAL_MS = 15_000;
63
69
 
64
- /** Register the line labels up front and print empty placeholders. */
70
+ /**
71
+ * Register the line labels up front and print empty placeholders.
72
+ * @param count - Number of progress lines to reserve.
73
+ */
65
74
  start(count: number): void {
66
75
  this.lines = new Array<string>(count).fill('');
67
76
  this.spinning = new Array<boolean>(count).fill(false);
68
77
  this.lastCiLog = new Array<number>(count).fill(0);
69
78
 
79
+ // Skip the ANSI cursor block in CI — plain console.log lines are used instead
70
80
  if (!IS_CI) {
71
81
  // Print placeholder lines so the cursor block exists
72
82
  for (let i = 0; i < count; i++) {
@@ -78,13 +88,20 @@ class ProgressDisplay {
78
88
  this.started = true;
79
89
  }
80
90
 
81
- /** Update a specific line (0-indexed) without touching the others. */
91
+ /**
92
+ * Update a specific line (0-indexed) without touching the others.
93
+ * @param line - Index of the progress line to update.
94
+ * @param message - Message to display on that line.
95
+ */
82
96
  update(line: number, message: string): void {
97
+ // Nothing to update before the display has been started
83
98
  if (!this.started) return;
84
99
  this.lines[line] = message;
85
100
  this.spinning[line] = true;
101
+ // In CI, throttle to plain log lines instead of redrawing the cursor block
86
102
  if (IS_CI) {
87
103
  const now = Date.now();
104
+ // Only log again once the throttle interval has elapsed for this line
88
105
  if (now - this.lastCiLog[line] >= ProgressDisplay.CI_LOG_INTERVAL_MS) {
89
106
  this.lastCiLog[line] = now;
90
107
  console.log(`⏳ ${message}`);
@@ -94,12 +111,18 @@ class ProgressDisplay {
94
111
  this.render(line);
95
112
  }
96
113
 
97
- /** Mark a line as completed — stops its spinner and shows a checkmark. */
114
+ /**
115
+ * Mark a line as completed — stops its spinner and shows a checkmark.
116
+ * @param line - Index of the progress line to complete.
117
+ * @param message - Final message to display on that line.
118
+ */
98
119
  succeed(line: number, message: string): void {
120
+ // Nothing to update before the display has been started
99
121
  if (!this.started) return;
100
122
  const text = `✅ ${message}`;
101
123
  this.lines[line] = text;
102
124
  this.spinning[line] = false;
125
+ // In CI, print the final line directly instead of redrawing the cursor block
103
126
  if (IS_CI) {
104
127
  console.log(text);
105
128
  return;
@@ -109,8 +132,11 @@ class ProgressDisplay {
109
132
 
110
133
  /** Clear all progress lines and leave the cursor on a clean line. */
111
134
  clear(): void {
135
+ // Nothing to clear before the display has been started
112
136
  if (!this.started) return;
137
+ // Stop the spinner tick before tearing down the display
113
138
  if (this.timer) clearInterval(this.timer);
139
+ // Skip cursor cleanup entirely in CI, where no cursor block was drawn
114
140
  if (!IS_CI) {
115
141
  // Move up to the first progress line and clear each one
116
142
  for (let i = 0; i < this.lines.length; i++) {
@@ -126,14 +152,19 @@ class ProgressDisplay {
126
152
  /** Advance the spinner frame and re-render all spinning lines. */
127
153
  private tick(): void {
128
154
  this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
155
+ // Re-render every line, skipping ones that aren't currently spinning
129
156
  for (let i = 0; i < this.lines.length; i++) {
157
+ // Only re-render lines that are still actively spinning
130
158
  if (this.spinning[i] && this.lines[i]) {
131
159
  this.render(i);
132
160
  }
133
161
  }
134
162
  }
135
163
 
136
- /** Render a single line at its position. */
164
+ /**
165
+ * Render a single line at its position.
166
+ * @param line - Index of the progress line to render.
167
+ */
137
168
  private render(line: number): void {
138
169
  const linesUp = this.lines.length - line;
139
170
  const prefix = this.spinning[line] ? SPINNER_FRAMES[this.frame] : '';
@@ -198,78 +229,120 @@ const EMBED_BUFFER_FLUSH_MS = 10_000;
198
229
  const MAX_RETRIES = 4;
199
230
 
200
231
  /**
201
- * Main entry point for the embeddings bin.
202
- * Orchestrates the entire embeddings generation pipeline.
203
- * @internal
232
+ * Prints resolved run configuration for `--debug` troubleshooting.
233
+ *
234
+ * @param options - Resolved command options for this run.
235
+ * @param filePatterns - Glob patterns used to discover files to index.
236
+ * @param config - Fusion AI index configuration for this run.
204
237
  */
205
- export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
206
- const { framework, options, config, filePatterns } = binOptions;
207
- const debug = options.debug ?? false;
208
-
209
- console.log(`📇 Index: ${options.indexName}`);
238
+ function logDebugConfig(
239
+ options: CommandOptions,
240
+ filePatterns: string[],
241
+ config: FusionAIConfigWithIndex,
242
+ ): void {
243
+ console.debug('[debug] Embed model:', options.embedModel);
244
+ console.debug('[debug] File patterns:', filePatterns);
245
+ console.debug(
246
+ '[debug] Allowed patterns:',
247
+ config.index?.patterns ?? ['**/*.ts', '**/*.tsx', '**/*.md', '**/*.mdx'],
248
+ );
249
+ console.debug('[debug] Raw patterns:', config.index?.rawPatterns ?? []);
250
+ console.debug('[debug] Ignore patterns:', config.index?.ignore ?? defaultIgnore);
251
+ console.debug('[debug] Diff mode:', options.diff);
252
+ console.debug('[debug] Dry run:', options.dryRun);
253
+ console.debug('[debug] Clean:', options.clean);
254
+ }
210
255
 
211
- if (debug) {
212
- console.debug('[debug] Embed model:', options.embedModel);
213
- console.debug('[debug] File patterns:', filePatterns);
214
- console.debug(
215
- '[debug] Allowed patterns:',
216
- config.index?.patterns ?? ['**/*.ts', '**/*.tsx', '**/*.md', '**/*.mdx'],
217
- );
218
- console.debug('[debug] Raw patterns:', config.index?.rawPatterns ?? []);
219
- console.debug('[debug] Ignore patterns:', config.index?.ignore ?? defaultIgnore);
220
- console.debug('[debug] Diff mode:', options.diff);
221
- console.debug('[debug] Dry run:', options.dryRun);
222
- console.debug('[debug] Clean:', options.clean);
256
+ /**
257
+ * Deletes all existing documents from the vector store when `--clean` is set.
258
+ * No-op in dry-run mode or when `--clean` was not requested.
259
+ *
260
+ * @param framework - Framework instance used to access the configured vector store.
261
+ * @param options - Resolved command options for this run.
262
+ */
263
+ async function cleanVectorStoreIfRequested(
264
+ framework: FrameworkInstance,
265
+ options: CommandOptions,
266
+ ): Promise<void> {
267
+ // Only wipe the index when explicitly requested and not in dry-run mode
268
+ if (!options.clean || options.dryRun) {
269
+ return;
223
270
  }
224
-
225
- const progress = new ProgressDisplay();
226
-
227
- // Handle clean operation (destructive - deletes all existing documents)
271
+ console.log('🧹 Cleaning vector store: deleting all existing documents...');
228
272
  const vectorStoreService = framework.ai.useIndex(options.indexName);
229
- if (options.clean && !options.dryRun) {
230
- console.log('🧹 Cleaning vector store: deleting all existing documents...');
231
- // OData filter: delete all documents with non-empty source (all indexed docs)
232
- await vectorStoreService.deleteDocuments({
233
- filter: { filterExpression: "metadata/source ne ''" },
234
- });
235
- console.log('✅ Vector store cleaned successfully');
236
- }
237
-
238
- // Handle diff-based processing (workflow mode)
239
- const changedFiles = options.diff ? await getDiff(options) : [];
273
+ // OData filter: delete all documents with non-empty source (all indexed docs)
274
+ await vectorStoreService.deleteDocuments({
275
+ filter: { filterExpression: "metadata/source ne ''" },
276
+ });
277
+ console.log('✅ Vector store cleaned successfully');
278
+ }
240
279
 
241
- // Create file stream: diff mode uses git changes, normal mode uses globby
242
- const files$ = (() => {
243
- if (options.diff) {
244
- return from(changedFiles);
245
- }
280
+ /**
281
+ * Creates the raw file stream: diff mode reuses already-resolved changed
282
+ * files, normal mode globs the filesystem and resolves git status per match.
283
+ *
284
+ * @param options - Resolved command options for this run.
285
+ * @param config - Fusion AI index configuration for this run.
286
+ * @param filePatterns - Glob patterns used to discover files to index.
287
+ * @param changedFiles - Pre-resolved changed files, used when `options.diff` is set.
288
+ * @returns A stream emitting each matched {@link ChangedFile}.
289
+ */
290
+ function createFileStream$(
291
+ options: CommandOptions,
292
+ config: FusionAIConfigWithIndex,
293
+ filePatterns: string[],
294
+ changedFiles: ChangedFile[],
295
+ ): Observable<ChangedFile> {
296
+ // Diff mode reuses the already-resolved changed files instead of globbing.
297
+ if (options.diff) {
298
+ return from(changedFiles);
299
+ }
246
300
 
247
- // Directories to skip before expensive git operations.
248
- // Note: Even with gitignore: true, globby still traverses ignored directories when .gitignore
249
- // contains negation patterns (like !.yarn/releases), so we add explicit ignore patterns
250
- // to prevent traversing these directories entirely.
251
- const ignore = config.index?.ignore ?? defaultIgnore;
301
+ // Directories to skip before expensive git operations.
302
+ // Note: Even with gitignore: true, globby still traverses ignored directories when .gitignore
303
+ // contains negation patterns (like !.yarn/releases), so we add explicit ignore patterns
304
+ // to prevent traversing these directories entirely.
305
+ const ignore = config.index?.ignore ?? defaultIgnore;
252
306
 
253
- // Respect .gitignore by default; configs targeting build artifacts can opt out.
254
- const gitignore = config.index?.gitignore ?? true;
307
+ // Respect .gitignore by default; configs targeting build artifacts can opt out.
308
+ const gitignore = config.index?.gitignore ?? true;
255
309
 
256
- return from(
310
+ return (
311
+ from(
257
312
  globbyStream(filePatterns, {
258
313
  ignore,
259
314
  onlyFiles: true,
260
315
  gitignore,
261
316
  absolute: true,
262
317
  }),
263
- ).pipe(
264
- // Get git status concurrently (capped to avoid spawning too many git processes)
265
- mergeMap((path) => getFileStatus(path), GIT_CONCURRENCY),
266
- concatMap((files) => from(files)),
267
- // Share stream for multiple subscribers (removedFiles$ and indexFiles$)
268
- shareReplay({ refCount: true }),
269
- );
270
- })();
271
-
272
- // Process files: enrich with metadata and filter by allowed patterns
318
+ )
319
+ // Resolve git status for each matched file, then flatten to individual files
320
+ .pipe(
321
+ // Get git status concurrently (capped to avoid spawning too many git processes)
322
+ mergeMap((path) => getFileStatus(path), GIT_CONCURRENCY),
323
+ concatMap((files) => from(files)),
324
+ // Share stream for multiple subscribers (removedFiles$ and indexFiles$)
325
+ shareReplay({ refCount: true }),
326
+ )
327
+ );
328
+ }
329
+
330
+ /**
331
+ * Enriches each file with its project-relative path and filters out files
332
+ * that don't match the configured index patterns.
333
+ *
334
+ * @param files$ - Raw file stream from {@link createFileStream$}.
335
+ * @param config - Fusion AI index configuration for this run.
336
+ * @param debug - Whether to log skipped files for troubleshooting.
337
+ * @param progress - Progress display used to report parsing progress.
338
+ * @returns A stream of {@link ProcessedFile} entries matching the allowed patterns.
339
+ */
340
+ function createProcessedFilesStream$(
341
+ files$: Observable<ChangedFile>,
342
+ config: FusionAIConfigWithIndex,
343
+ debug: boolean,
344
+ progress: ProgressDisplay,
345
+ ): Observable<ProcessedFile> {
273
346
  const allowedFilePatterns = config.index?.patterns ?? [
274
347
  '**/*.ts',
275
348
  '**/*.tsx',
@@ -277,9 +350,9 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
277
350
  '**/*.mdx',
278
351
  ];
279
352
 
280
- // Process files: enrich with metadata and filter by allowed patterns
281
353
  let fileCount = 0;
282
- const processedFiles$ = files$.pipe(
354
+ // Resolve each file's project-relative path and filter by allowed patterns
355
+ return files$.pipe(
283
356
  map((file) => {
284
357
  const { filepath, status } = file;
285
358
  const projectRoot = resolveProjectRoot(filepath);
@@ -294,6 +367,7 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
294
367
  }),
295
368
  filter((file) => {
296
369
  const matches = multimatch(file.relativePath, allowedFilePatterns);
370
+ // Surface skipped files in debug output for troubleshooting pattern config
297
371
  if (debug && matches.length === 0) {
298
372
  console.debug('[debug] Skipped (no pattern match):', file.relativePath);
299
373
  }
@@ -307,22 +381,27 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
307
381
  // Share for multiple subscribers (removedFiles$, markdown$, typescript$)
308
382
  shareReplay({ refCount: true }),
309
383
  );
384
+ }
310
385
 
311
- // Split stream: removed files for deletion, new/modified for indexing
312
- const removedFiles$ = processedFiles$.pipe(filter((file) => file.status === 'removed'));
313
-
314
- // Create processing streams
315
- const delete$ = createDeleteRemovedFilesStream(removedFiles$, framework, options);
316
-
317
- // New/modified files for indexing
318
- const indexFiles$ = processedFiles$.pipe(
319
- filter((file) => file.status === 'new' || file.status === 'modified'),
320
- // Share for markdown$ and typescript$ pipelines
321
- shareReplay({ refCount: true }),
322
- );
323
-
386
+ /**
387
+ * Parses new/modified files (raw, markdown, or TypeScript) into document
388
+ * entries ready for metadata enrichment.
389
+ *
390
+ * @param indexFiles$ - Stream of new/modified files to parse.
391
+ * @param config - Fusion AI index configuration for this run.
392
+ * @param debug - Whether to log per-file chunk counts for troubleshooting.
393
+ * @param progress - Progress display used to report parsing progress.
394
+ * @returns A stream of {@link DocumentEntry} objects, one per parsed file.
395
+ */
396
+ function createParsedDocumentsStream$(
397
+ indexFiles$: Observable<ProcessedFile>,
398
+ config: FusionAIConfigWithIndex,
399
+ debug: boolean,
400
+ progress: ProgressDisplay,
401
+ ): Observable<DocumentEntry> {
324
402
  const isRawFile = (file: ProcessedFile): boolean => {
325
403
  const matches = multimatch(file.relativePath, config.index?.rawPatterns ?? []);
404
+ // A file is "raw" when it matches any configured raw pattern
326
405
  if (matches.length > 0) {
327
406
  return true;
328
407
  }
@@ -330,6 +409,8 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
330
409
  };
331
410
 
332
411
  let docCount = 0;
412
+
413
+ // Read raw-pattern files verbatim as a single document, skipping markdown/TS parsing
333
414
  const rawFiles$ = indexFiles$.pipe(
334
415
  filter(isRawFile),
335
416
  map((file): DocumentEntry => {
@@ -347,12 +428,14 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
347
428
  }),
348
429
  );
349
430
 
431
+ // Parse markdown files not already handled as raw content
350
432
  const markdown$ = indexFiles$.pipe(
351
433
  filter((x) => !isRawFile(x)),
352
434
  filter((file) => isMarkdownFile(file.path)),
353
435
  mergeMap(async (file) => {
354
436
  const documents = await parseMarkdownFile(file);
355
437
  docCount++;
438
+ // Surface per-file chunk counts in debug output
356
439
  if (debug) {
357
440
  console.debug(`[debug] Markdown ${file.relativePath} → ${documents.length} chunk(s)`);
358
441
  }
@@ -361,12 +444,14 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
361
444
  }),
362
445
  );
363
446
 
447
+ // Parse TypeScript files not already handled as raw content
364
448
  const typescript$ = indexFiles$.pipe(
365
449
  filter((x) => !isRawFile(x)),
366
450
  filter((file) => isTypescriptFile(file.path)),
367
451
  map((file) => {
368
452
  const documents = parseTsDocFromFileSync(file);
369
453
  docCount++;
454
+ // Surface per-file chunk counts in debug output
370
455
  if (debug) {
371
456
  console.debug(`[debug] TypeScript ${file.relativePath} → ${documents.length} chunk(s)`);
372
457
  }
@@ -376,36 +461,66 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
376
461
  );
377
462
 
378
463
  // Merge parsed streams and signal when all parsing is done
379
- const parsed$ = merge(rawFiles$, markdown$, typescript$).pipe(
464
+ return merge(rawFiles$, markdown$, typescript$).pipe(
380
465
  finalize(() => {
381
466
  progress.succeed(LINE_PARSE, `📄 Parsed ${docCount} files`);
382
467
  }),
383
468
  );
469
+ }
384
470
 
385
- // Apply metadata to documents
386
- let metadataCount = 0;
387
- let metadataDone = false;
388
- const applyMetadata$ = applyMetadata(parsed$, config.index, (source) => {
389
- metadataCount++;
390
- progress.update(LINE_META, `🏷️ Metadata [${metadataCount}] ${source}`);
471
+ /** Mutable progress counters shared between a pipeline stage and the final reporter. */
472
+ interface StageProgressState {
473
+ count: number;
474
+ done: boolean;
475
+ }
476
+
477
+ /**
478
+ * Applies metadata enrichment to parsed documents and tracks progress.
479
+ *
480
+ * @param parsed$ - Stream of parsed document entries.
481
+ * @param config - Fusion AI index configuration for this run.
482
+ * @param progress - Progress display used to report metadata progress.
483
+ * @returns The enriched document stream along with its progress state.
484
+ */
485
+ function createMetadataStream$(
486
+ parsed$: Observable<DocumentEntry>,
487
+ config: FusionAIConfigWithIndex,
488
+ progress: ProgressDisplay,
489
+ ): { stream$: Observable<VectorStoreDocument[]>; state: StageProgressState } {
490
+ const state: StageProgressState = { count: 0, done: false };
491
+ // Track metadata progress via the callback, then mark the stage done on completion
492
+ const stream$ = applyMetadata(parsed$, config.index, (source) => {
493
+ state.count++;
494
+ progress.update(LINE_META, `🏷️ Metadata [${state.count}] ${source}`);
391
495
  }).pipe(
392
496
  finalize(() => {
393
- metadataDone = true;
394
- progress.succeed(LINE_META, `🏷️ Metadata ${metadataCount} documents`);
497
+ state.done = true;
498
+ progress.succeed(LINE_META, `🏷️ Metadata ${state.count} documents`);
395
499
  }),
396
500
  );
501
+ return { stream$, state };
502
+ }
397
503
 
398
- // Resolve promoted schema fields (if schema is configured) — runs after
399
- // metadata enrichment so the resolver has access to git, package, and
400
- // custom attributes from attributeProcessor
401
- const applySchema$ = applySchema(applyMetadata$, config.index?.schema);
402
-
403
- // Generate embeddings in batches with retry on rate-limit (429) errors
404
- const embeddingService = framework.ai.useEmbed(options.embedModel);
405
-
406
- let embeddedCount = 0;
407
- let embeddingDone = false;
408
- const applyEmbedding$ = applySchema$.pipe(
504
+ /**
505
+ * Generates embeddings in batches with retry on rate-limit (429) errors.
506
+ *
507
+ * @param documents$ - Stream of schema-resolved document batches to embed.
508
+ * @param embeddingService - Embedding client used to compute document vectors.
509
+ * @param debug - Whether to log batch sizes for troubleshooting.
510
+ * @param progress - Progress display used to report embedding progress.
511
+ * @param metadataState - Progress state from {@link createMetadataStream$}, used as the denominator.
512
+ * @returns The embedded document stream along with its progress state.
513
+ */
514
+ function createEmbeddingStream$(
515
+ documents$: Observable<VectorStoreDocument[]>,
516
+ embeddingService: ReturnType<FrameworkInstance['ai']['useEmbed']>,
517
+ debug: boolean,
518
+ progress: ProgressDisplay,
519
+ metadataState: StageProgressState,
520
+ ): { stream$: Observable<VectorStoreDocument[]>; state: StageProgressState } {
521
+ const state: StageProgressState = { count: 0, done: false };
522
+ // Buffer, embed with retry, then re-attach embeddings onto each document
523
+ const stream$ = documents$.pipe(
409
524
  // Flatten all documents from file-level batches, then re-batch for the API
410
525
  concatMap((documents) => from(documents)),
411
526
  // Flush when EMBED_BATCH_SIZE docs accumulate OR after EMBED_BUFFER_FLUSH_MS,
@@ -413,72 +528,106 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
413
528
  bufferTime(EMBED_BUFFER_FLUSH_MS, null, EMBED_BATCH_SIZE),
414
529
  filter((batch) => batch.length > 0),
415
530
  mergeMap((batch) => {
531
+ // Surface batch sizes in debug output for troubleshooting throughput
416
532
  if (debug) {
417
533
  console.debug(`[debug] Embedding batch of ${batch.length} documents`);
418
534
  }
419
- return from(embeddingService.embedDocuments(batch.map((d) => d.pageContent))).pipe(
420
- retry({
421
- count: MAX_RETRIES,
422
- delay: (error, retryIndex) => {
423
- // Auth errors are terminal abort immediately with actionable message
424
- if (error?.name === 'NoAccountsError') {
425
- console.error(
426
- '\n🔒 Authentication expired. Run `ffc auth login` then retry with `--diff`.',
427
- );
428
- throw error;
429
- }
430
-
431
- const retryAfterSec =
432
- error?.response?.headers?.get?.('retry-after') ??
433
- error?.responseHeaders?.['retry-after'];
434
- const retryAfterMs = retryAfterSec ? Number(retryAfterSec) * 1000 : 0;
435
-
436
- const backoffMs = 2 ** retryIndex * 1000;
437
- const delayMs = Math.max(backoffMs, retryAfterMs);
438
-
439
- console.warn(
440
- `\n⏳ Retry ${retryIndex}/${MAX_RETRIES} for batch of ${batch.length} in ${delayMs}ms`,
441
- );
442
- return timer(delayMs);
443
- },
444
- }),
445
- map((allEmbeddings) => {
446
- return batch.map((document, i) => {
447
- embeddedCount++;
448
- const total = metadataDone ? metadataCount : 0;
449
- const pct = total > 0 ? ` ${Math.round((embeddedCount / total) * 100)}%` : '';
450
- const denominator = total > 0 ? `/${total}` : '';
451
- progress.update(
452
- LINE_EMBED,
453
- `🧠 Embedding [${embeddedCount}${denominator}]${pct} — ${document.metadata.source}`,
454
- );
455
- const metadata = { ...document.metadata, embedding: allEmbeddings[i] };
456
- return { ...document, metadata };
457
- });
458
- }),
535
+ return (
536
+ from(
537
+ embeddingService.embedDocuments(
538
+ batch
539
+ // Extract each document's page content for the embedding API call
540
+ .map((d) => d.pageContent),
541
+ ),
542
+ )
543
+ // Retry transient/rate-limit errors, then attach embeddings back onto each document
544
+ .pipe(
545
+ retry({
546
+ count: MAX_RETRIES,
547
+ delay: (error, retryIndex) => {
548
+ // Auth errors are terminal — abort immediately with actionable message
549
+ if (error?.name === 'NoAccountsError') {
550
+ console.error(
551
+ '\n🔒 Authentication expired. Run `ffc auth login` then retry with `--diff`.',
552
+ );
553
+ throw error;
554
+ }
555
+
556
+ const retryAfterSec =
557
+ error?.response?.headers?.get?.('retry-after') ??
558
+ error?.responseHeaders?.['retry-after'];
559
+ const retryAfterMs = retryAfterSec ? Number(retryAfterSec) * 1000 : 0;
560
+
561
+ const backoffMs = 2 ** retryIndex * 1000;
562
+ const delayMs = Math.max(backoffMs, retryAfterMs);
563
+
564
+ console.warn(
565
+ `\n⏳ Retry ${retryIndex}/${MAX_RETRIES} for batch of ${batch.length} in ${delayMs}ms`,
566
+ );
567
+ return timer(delayMs);
568
+ },
569
+ }),
570
+ map((allEmbeddings) => {
571
+ // Re-attach the corresponding embedding onto each document in the batch
572
+ return batch.map((document, i) => {
573
+ state.count++;
574
+ const total = metadataState.done ? metadataState.count : 0;
575
+ const pct = total > 0 ? ` ${Math.round((state.count / total) * 100)}%` : '';
576
+ const denominator = total > 0 ? `/${total}` : '';
577
+ progress.update(
578
+ LINE_EMBED,
579
+ `🧠 Embedding [${state.count}${denominator}]${pct} — ${document.metadata.source}`,
580
+ );
581
+ const metadata = { ...document.metadata, embedding: allEmbeddings[i] };
582
+ return { ...document, metadata };
583
+ });
584
+ }),
585
+ )
459
586
  );
460
587
  }, EMBED_BATCH_CONCURRENCY),
461
588
  finalize(() => {
462
- embeddingDone = true;
463
- progress.succeed(LINE_EMBED, `🧠 Embedded ${embeddedCount} documents`);
589
+ state.done = true;
590
+ progress.succeed(LINE_EMBED, `🧠 Embedded ${state.count} documents`);
464
591
  }),
465
592
  );
593
+ return { stream$, state };
594
+ }
466
595
 
467
- // Update vector store — batch documents and upsert concurrently
468
- const upsert$ = applyEmbedding$.pipe(
596
+ /**
597
+ * Batches embedded documents and upserts them into the vector store.
598
+ *
599
+ * @param embedding$ - Stream of embedded document batches.
600
+ * @param framework - Framework instance used to access the configured vector store.
601
+ * @param options - Resolved command options for this run.
602
+ * @param debug - Whether to log upserted document IDs for troubleshooting.
603
+ * @returns A stream emitting the result of each batch upsert.
604
+ */
605
+ function createUpsertStream$(
606
+ embedding$: Observable<VectorStoreDocument[]>,
607
+ framework: FrameworkInstance,
608
+ options: CommandOptions,
609
+ debug: boolean,
610
+ ): Observable<UpdateVectorStoreResult> {
611
+ // Re-batch embedded documents into fixed-size groups for bulk upsert
612
+ return embedding$.pipe(
469
613
  // Flatten file-level batches, then re-batch into groups of 20 for bulk upsert
470
614
  concatMap((documents) => from(documents)),
471
615
  bufferCount(20),
472
616
  mergeMap(async (documents) => {
473
617
  const vectorStoreService = framework.ai.useIndex(options.indexName);
618
+ // Nothing to upsert when the batch is empty
474
619
  if (documents.length === 0) {
475
620
  return undefined;
476
621
  }
622
+ // Skip the actual vector store mutation when running in dry-run mode
477
623
  if (!options.dryRun) {
624
+ // Surface upserted document IDs in debug output for troubleshooting
478
625
  if (debug) {
479
626
  console.debug(
480
627
  `[debug] Upserting batch of ${documents.length} documents:`,
481
- documents.map((d) => d.id),
628
+ documents
629
+ // Log just the IDs to keep debug output concise
630
+ .map((d) => d.id),
482
631
  );
483
632
  }
484
633
  await vectorStoreService.addDocuments(documents);
@@ -490,8 +639,23 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
490
639
  }, UPSERT_CONCURRENCY),
491
640
  filter((result): result is UpdateVectorStoreResult => Boolean(result)),
492
641
  );
642
+ }
493
643
 
494
- // Execute pipeline
644
+ /**
645
+ * Runs the deletion/upsert pipeline to completion, reporting progress and
646
+ * a final summary, then exits the process.
647
+ *
648
+ * @param delete$ - Stream of removed-file deletion results.
649
+ * @param upsert$ - Stream of document upsert results.
650
+ * @param progress - Progress display used to report indexing progress.
651
+ * @param embeddingState - Progress state from {@link createEmbeddingStream$}, used as the denominator.
652
+ */
653
+ function runIndexingPipeline(
654
+ delete$: Observable<DeleteRemovedFilesResult>,
655
+ upsert$: Observable<UpdateVectorStoreResult>,
656
+ progress: ProgressDisplay,
657
+ embeddingState: StageProgressState,
658
+ ): void {
495
659
  // Track indexing results for reporting: deleted file paths and added document IDs
496
660
  let indexedCount = 0;
497
661
  const indexingResults: { deleted: string[]; added: { source: string; id: string }[] } = {
@@ -507,10 +671,12 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
507
671
  next: (result) => {
508
672
  // Track deleted files by relative path
509
673
  if (result.status === 'deleted') {
674
+ // Reduce each deleted file entry down to its relative path for reporting.
510
675
  indexingResults.deleted.push(...result.files.map((file) => file.relativePath));
511
676
  }
512
677
  // Track added documents with source and ID (one file can produce multiple IDs)
513
678
  else if (result.status === 'added') {
679
+ // Reduce each added document down to its source path and id for reporting.
514
680
  indexingResults.added.push(
515
681
  ...result.documents.map((document) => ({
516
682
  source: document.metadata.source,
@@ -519,7 +685,7 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
519
685
  );
520
686
  indexedCount += result.documents.length;
521
687
  // Use embeddedCount as denominator — only show % once embedding is done
522
- const total = embeddingDone ? embeddedCount : 0;
688
+ const total = embeddingState.done ? embeddingState.count : 0;
523
689
  const pct = total > 0 ? ` ${Math.round((indexedCount / total) * 100)}%` : '';
524
690
  const denominator = total > 0 ? `/${total}` : '';
525
691
  progress.update(LINE_INDEX, `📤 Indexed [${indexedCount}${denominator}]${pct}`);
@@ -536,6 +702,7 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
536
702
  if (indexingResults.deleted.length > 0) {
537
703
  console.log(`🗑️ Deleted: ${indexingResults.deleted.length} files`);
538
704
  }
705
+ // Only print an "indexed" summary line when documents were actually added
539
706
  if (indexingResults.added.length > 0) {
540
707
  console.log(`📥 Indexed: ${indexingResults.added.length} documents`);
541
708
  }
@@ -544,3 +711,80 @@ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
544
711
  },
545
712
  });
546
713
  }
714
+
715
+ /**
716
+ * Main entry point for the embeddings bin.
717
+ * Orchestrates the entire embeddings generation pipeline.
718
+ *
719
+ * @param binOptions - Framework, resolved options, config, and file patterns for this run.
720
+ * @throws {Error} Propagates unrecoverable embedding/vector-store errors from the pipeline.
721
+ * @internal
722
+ */
723
+ export async function embed(binOptions: EmbeddingsBinOptions): Promise<void> {
724
+ const { framework, options, config, filePatterns } = binOptions;
725
+ const debug = options.debug ?? false;
726
+
727
+ console.log(`📇 Index: ${options.indexName}`);
728
+
729
+ // Print resolved run configuration when --debug is set
730
+ if (debug) {
731
+ logDebugConfig(options, filePatterns, config);
732
+ }
733
+
734
+ const progress = new ProgressDisplay();
735
+
736
+ await cleanVectorStoreIfRequested(framework, options);
737
+
738
+ // Handle diff-based processing (workflow mode)
739
+ const changedFiles = options.diff ? await getDiff(options) : [];
740
+
741
+ // Create file stream: diff mode uses git changes, normal mode uses globby
742
+ const files$ = createFileStream$(options, config, filePatterns, changedFiles);
743
+
744
+ // Enrich files with metadata and filter by allowed patterns
745
+ const processedFiles$ = createProcessedFilesStream$(files$, config, debug, progress);
746
+
747
+ // Split stream: removed files for deletion, new/modified for indexing
748
+ const removedFiles$ = processedFiles$.pipe(filter((file) => file.status === 'removed'));
749
+
750
+ // Create processing streams
751
+ const delete$ = createDeleteRemovedFilesStream(removedFiles$, framework, options);
752
+
753
+ // New/modified files for indexing
754
+ const indexFiles$ = processedFiles$.pipe(
755
+ filter((file) => file.status === 'new' || file.status === 'modified'),
756
+ // Share for markdown$ and typescript$ pipelines
757
+ shareReplay({ refCount: true }),
758
+ );
759
+
760
+ // Parse raw/markdown/TypeScript files into document entries
761
+ const parsed$ = createParsedDocumentsStream$(indexFiles$, config, debug, progress);
762
+
763
+ // Apply metadata to documents
764
+ const { stream$: applyMetadata$, state: metadataState } = createMetadataStream$(
765
+ parsed$,
766
+ config,
767
+ progress,
768
+ );
769
+
770
+ // Resolve promoted schema fields (if schema is configured) — runs after
771
+ // metadata enrichment so the resolver has access to git, package, and
772
+ // custom attributes from attributeProcessor
773
+ const applySchema$ = applySchema(applyMetadata$, config.index?.schema);
774
+
775
+ // Generate embeddings in batches with retry on rate-limit (429) errors
776
+ const embeddingService = framework.ai.useEmbed(options.embedModel);
777
+ const { stream$: applyEmbedding$, state: embeddingState } = createEmbeddingStream$(
778
+ applySchema$,
779
+ embeddingService,
780
+ debug,
781
+ progress,
782
+ metadataState,
783
+ );
784
+
785
+ // Update vector store — batch documents and upsert concurrently
786
+ const upsert$ = createUpsertStream$(applyEmbedding$, framework, options, debug);
787
+
788
+ // Execute pipeline and report final results
789
+ runIndexingPipeline(delete$, upsert$, progress, embeddingState);
790
+ }