bluera-knowledge 0.9.32 → 0.9.36

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 (198) hide show
  1. package/.claude/hooks/post-edit-check.sh +5 -3
  2. package/.claude/skills/atomic-commits/SKILL.md +3 -1
  3. package/.husky/pre-commit +3 -2
  4. package/.prettierrc +9 -0
  5. package/.versionrc.json +1 -1
  6. package/CHANGELOG.md +70 -0
  7. package/CLAUDE.md +6 -0
  8. package/README.md +25 -13
  9. package/bun.lock +277 -33
  10. package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
  11. package/dist/chunk-6FHWC36B.js.map +1 -0
  12. package/dist/{chunk-RST4XGRL.js → chunk-DC7CGSGT.js} +288 -241
  13. package/dist/chunk-DC7CGSGT.js.map +1 -0
  14. package/dist/{chunk-6PBP5DVD.js → chunk-WFNPNAAP.js} +3212 -3054
  15. package/dist/chunk-WFNPNAAP.js.map +1 -0
  16. package/dist/{chunk-WT2DAEO7.js → chunk-Z2KKVH45.js} +548 -482
  17. package/dist/chunk-Z2KKVH45.js.map +1 -0
  18. package/dist/index.js +871 -758
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/server.js +3 -3
  21. package/dist/watch.service-BJV3TI3F.js +7 -0
  22. package/dist/workers/background-worker-cli.js +97 -71
  23. package/dist/workers/background-worker-cli.js.map +1 -1
  24. package/eslint.config.js +43 -1
  25. package/package.json +18 -11
  26. package/plugin.json +8 -0
  27. package/python/requirements.txt +1 -1
  28. package/src/analysis/ast-parser.test.ts +12 -11
  29. package/src/analysis/ast-parser.ts +28 -22
  30. package/src/analysis/code-graph.test.ts +52 -62
  31. package/src/analysis/code-graph.ts +9 -13
  32. package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
  33. package/src/analysis/dependency-usage-analyzer.ts +52 -24
  34. package/src/analysis/go-ast-parser.test.ts +22 -22
  35. package/src/analysis/go-ast-parser.ts +18 -25
  36. package/src/analysis/parser-factory.test.ts +9 -9
  37. package/src/analysis/parser-factory.ts +3 -3
  38. package/src/analysis/python-ast-parser.test.ts +27 -27
  39. package/src/analysis/python-ast-parser.ts +2 -2
  40. package/src/analysis/repo-url-resolver.test.ts +82 -82
  41. package/src/analysis/rust-ast-parser.test.ts +19 -19
  42. package/src/analysis/rust-ast-parser.ts +17 -27
  43. package/src/analysis/tree-sitter-parser.test.ts +3 -3
  44. package/src/analysis/tree-sitter-parser.ts +10 -16
  45. package/src/cli/commands/crawl.test.ts +40 -24
  46. package/src/cli/commands/crawl.ts +186 -166
  47. package/src/cli/commands/index-cmd.test.ts +90 -90
  48. package/src/cli/commands/index-cmd.ts +52 -36
  49. package/src/cli/commands/mcp.test.ts +6 -6
  50. package/src/cli/commands/mcp.ts +2 -2
  51. package/src/cli/commands/plugin-api.test.ts +16 -18
  52. package/src/cli/commands/plugin-api.ts +9 -6
  53. package/src/cli/commands/search.test.ts +16 -7
  54. package/src/cli/commands/search.ts +124 -87
  55. package/src/cli/commands/serve.test.ts +67 -25
  56. package/src/cli/commands/serve.ts +18 -3
  57. package/src/cli/commands/setup.test.ts +176 -101
  58. package/src/cli/commands/setup.ts +140 -117
  59. package/src/cli/commands/store.test.ts +82 -53
  60. package/src/cli/commands/store.ts +56 -37
  61. package/src/cli/program.ts +2 -2
  62. package/src/crawl/article-converter.test.ts +4 -1
  63. package/src/crawl/article-converter.ts +46 -31
  64. package/src/crawl/bridge.test.ts +240 -132
  65. package/src/crawl/bridge.ts +87 -30
  66. package/src/crawl/claude-client.test.ts +124 -56
  67. package/src/crawl/claude-client.ts +7 -15
  68. package/src/crawl/intelligent-crawler.test.ts +65 -22
  69. package/src/crawl/intelligent-crawler.ts +86 -53
  70. package/src/crawl/markdown-utils.ts +1 -4
  71. package/src/db/embeddings.ts +4 -6
  72. package/src/db/lance.test.ts +4 -4
  73. package/src/db/lance.ts +16 -12
  74. package/src/index.ts +26 -17
  75. package/src/logging/index.ts +1 -5
  76. package/src/logging/logger.ts +3 -5
  77. package/src/logging/payload.test.ts +1 -1
  78. package/src/logging/payload.ts +3 -5
  79. package/src/mcp/commands/index.ts +2 -2
  80. package/src/mcp/commands/job.commands.ts +12 -18
  81. package/src/mcp/commands/meta.commands.ts +13 -13
  82. package/src/mcp/commands/registry.ts +5 -8
  83. package/src/mcp/commands/store.commands.ts +19 -19
  84. package/src/mcp/handlers/execute.handler.test.ts +10 -10
  85. package/src/mcp/handlers/execute.handler.ts +4 -5
  86. package/src/mcp/handlers/index.ts +10 -14
  87. package/src/mcp/handlers/job.handler.test.ts +10 -10
  88. package/src/mcp/handlers/job.handler.ts +22 -25
  89. package/src/mcp/handlers/search.handler.test.ts +36 -65
  90. package/src/mcp/handlers/search.handler.ts +135 -104
  91. package/src/mcp/handlers/store.handler.test.ts +41 -52
  92. package/src/mcp/handlers/store.handler.ts +108 -88
  93. package/src/mcp/schemas/index.test.ts +73 -68
  94. package/src/mcp/schemas/index.ts +18 -12
  95. package/src/mcp/server.test.ts +1 -1
  96. package/src/mcp/server.ts +59 -46
  97. package/src/plugin/commands.test.ts +230 -95
  98. package/src/plugin/commands.ts +24 -25
  99. package/src/plugin/dependency-analyzer.test.ts +52 -52
  100. package/src/plugin/dependency-analyzer.ts +85 -22
  101. package/src/plugin/git-clone.test.ts +24 -13
  102. package/src/plugin/git-clone.ts +3 -7
  103. package/src/server/app.test.ts +109 -109
  104. package/src/server/app.ts +32 -23
  105. package/src/server/index.test.ts +64 -66
  106. package/src/services/chunking.service.test.ts +32 -32
  107. package/src/services/chunking.service.ts +16 -9
  108. package/src/services/code-graph.service.test.ts +30 -36
  109. package/src/services/code-graph.service.ts +24 -10
  110. package/src/services/code-unit.service.test.ts +55 -11
  111. package/src/services/code-unit.service.ts +85 -11
  112. package/src/services/config.service.test.ts +37 -18
  113. package/src/services/config.service.ts +30 -7
  114. package/src/services/index.service.test.ts +49 -18
  115. package/src/services/index.service.ts +98 -48
  116. package/src/services/index.ts +6 -9
  117. package/src/services/job.service.test.ts +22 -22
  118. package/src/services/job.service.ts +18 -18
  119. package/src/services/project-root.service.test.ts +1 -3
  120. package/src/services/search.service.test.ts +248 -120
  121. package/src/services/search.service.ts +286 -156
  122. package/src/services/services.test.ts +1 -1
  123. package/src/services/snippet.service.test.ts +14 -6
  124. package/src/services/snippet.service.ts +7 -5
  125. package/src/services/store.service.test.ts +68 -29
  126. package/src/services/store.service.ts +41 -12
  127. package/src/services/watch.service.test.ts +34 -14
  128. package/src/services/watch.service.ts +11 -1
  129. package/src/types/brands.test.ts +3 -1
  130. package/src/types/index.ts +2 -13
  131. package/src/types/search.ts +10 -8
  132. package/src/utils/type-guards.test.ts +20 -15
  133. package/src/utils/type-guards.ts +1 -1
  134. package/src/workers/background-worker-cli.ts +28 -30
  135. package/src/workers/background-worker.test.ts +54 -40
  136. package/src/workers/background-worker.ts +76 -60
  137. package/src/workers/pid-file.test.ts +167 -0
  138. package/src/workers/pid-file.ts +82 -0
  139. package/src/workers/spawn-worker.test.ts +22 -10
  140. package/src/workers/spawn-worker.ts +6 -6
  141. package/tests/analysis/ast-parser.test.ts +3 -3
  142. package/tests/analysis/code-graph.test.ts +5 -5
  143. package/tests/fixtures/code-snippets/api/error-handling.ts +4 -15
  144. package/tests/fixtures/code-snippets/api/rest-controller.ts +3 -9
  145. package/tests/fixtures/code-snippets/auth/jwt-auth.ts +5 -21
  146. package/tests/fixtures/code-snippets/auth/oauth-flow.ts +4 -4
  147. package/tests/fixtures/code-snippets/database/repository-pattern.ts +11 -3
  148. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +2 -2
  149. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +1 -1
  150. package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +2 -2
  151. package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +2 -2
  152. package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +22 -20
  153. package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +13 -10
  154. package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +10 -7
  155. package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +2 -2
  156. package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +1 -1
  157. package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +16 -16
  158. package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +2 -2
  159. package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +3 -3
  160. package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +1 -1
  161. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +2 -2
  162. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +1 -1
  163. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +7 -7
  164. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +3 -3
  165. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +1 -1
  166. package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +6 -6
  167. package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +3 -3
  168. package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +1 -1
  169. package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +1 -1
  170. package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +1 -1
  171. package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +2 -2
  172. package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +4 -4
  173. package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +1 -1
  174. package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +1 -1
  175. package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +166 -169
  176. package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +8 -8
  177. package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +3 -3
  178. package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +2 -2
  179. package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +2 -2
  180. package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +30 -33
  181. package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +2 -2
  182. package/tests/fixtures/test-server.ts +3 -2
  183. package/tests/helpers/performance-metrics.ts +8 -25
  184. package/tests/helpers/search-relevance.ts +14 -69
  185. package/tests/integration/cli-consistency.test.ts +6 -5
  186. package/tests/integration/python-bridge.test.ts +13 -3
  187. package/tests/mcp/server.test.ts +1 -1
  188. package/tests/services/code-unit.service.test.ts +48 -0
  189. package/tests/services/job.service.test.ts +124 -0
  190. package/tests/services/search.progressive-context.test.ts +2 -2
  191. package/.claude-plugin/plugin.json +0 -13
  192. package/dist/chunk-6PBP5DVD.js.map +0 -1
  193. package/dist/chunk-L2YVNC63.js.map +0 -1
  194. package/dist/chunk-RST4XGRL.js.map +0 -1
  195. package/dist/chunk-WT2DAEO7.js.map +0 -1
  196. package/dist/watch.service-YAIKKDCF.js +0 -7
  197. package/skills/atomic-commits/SKILL.md +0 -77
  198. /package/dist/{watch.service-YAIKKDCF.js.map → watch.service-BJV3TI3F.js.map} +0 -0
@@ -4,7 +4,7 @@ import {
4
4
  isDocumentMetadata,
5
5
  isPartialAppConfig,
6
6
  isFloat32ArrayData,
7
- hasDefaultExport
7
+ hasDefaultExport,
8
8
  } from './type-guards.js';
9
9
  import type { DocumentMetadata } from '../types/document.js';
10
10
 
@@ -21,7 +21,12 @@ describe('TypeGuards - parseJSON', () => {
21
21
 
22
22
  it('throws on invalid JSON structure', () => {
23
23
  const validator = (value: unknown): value is { name: string } => {
24
- return typeof value === 'object' && value !== null && 'name' in value && typeof (value as any).name === 'string';
24
+ return (
25
+ typeof value === 'object' &&
26
+ value !== null &&
27
+ 'name' in value &&
28
+ typeof (value as any).name === 'string'
29
+ );
25
30
  };
26
31
 
27
32
  expect(() => {
@@ -39,7 +44,7 @@ describe('TypeGuards - parseJSON', () => {
39
44
 
40
45
  it('handles arrays with validator', () => {
41
46
  const validator = (value: unknown): value is string[] => {
42
- return Array.isArray(value) && value.every(v => typeof v === 'string');
47
+ return Array.isArray(value) && value.every((v) => typeof v === 'string');
43
48
  };
44
49
 
45
50
  const result = parseJSON('["a","b","c"]', validator);
@@ -79,7 +84,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
79
84
  const metadata: DocumentMetadata = {
80
85
  storeId: 'store-123',
81
86
  path: '/path/to/file.ts',
82
- docType: 'code'
87
+ docType: 'code',
83
88
  };
84
89
 
85
90
  expect(isDocumentMetadata(metadata)).toBe(true);
@@ -102,7 +107,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
102
107
  it('returns false when missing required storeId', () => {
103
108
  const invalid = {
104
109
  path: '/path/to/file.ts',
105
- docType: 'code'
110
+ docType: 'code',
106
111
  };
107
112
 
108
113
  expect(isDocumentMetadata(invalid)).toBe(false);
@@ -111,7 +116,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
111
116
  it('returns false when missing required path', () => {
112
117
  const invalid = {
113
118
  storeId: 'store-123',
114
- docType: 'code'
119
+ docType: 'code',
115
120
  };
116
121
 
117
122
  expect(isDocumentMetadata(invalid)).toBe(false);
@@ -120,7 +125,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
120
125
  it('returns false when missing required docType', () => {
121
126
  const invalid = {
122
127
  storeId: 'store-123',
123
- path: '/path/to/file.ts'
128
+ path: '/path/to/file.ts',
124
129
  };
125
130
 
126
131
  expect(isDocumentMetadata(invalid)).toBe(false);
@@ -130,7 +135,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
130
135
  const invalid = {
131
136
  storeId: 123,
132
137
  path: '/path/to/file.ts',
133
- docType: 'code'
138
+ docType: 'code',
134
139
  };
135
140
 
136
141
  expect(isDocumentMetadata(invalid)).toBe(false);
@@ -140,7 +145,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
140
145
  const invalid = {
141
146
  storeId: 'store-123',
142
147
  path: 123,
143
- docType: 'code'
148
+ docType: 'code',
144
149
  };
145
150
 
146
151
  expect(isDocumentMetadata(invalid)).toBe(false);
@@ -150,7 +155,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
150
155
  const invalid = {
151
156
  storeId: 'store-123',
152
157
  path: '/path/to/file.ts',
153
- docType: 123
158
+ docType: 123,
154
159
  };
155
160
 
156
161
  expect(isDocumentMetadata(invalid)).toBe(false);
@@ -161,7 +166,7 @@ describe('TypeGuards - isDocumentMetadata', () => {
161
166
  storeId: 'store-123',
162
167
  path: '/path/to/file.ts',
163
168
  docType: 'code',
164
- extraProp: 'allowed'
169
+ extraProp: 'allowed',
165
170
  };
166
171
 
167
172
  expect(isDocumentMetadata(metadata)).toBe(true);
@@ -172,7 +177,7 @@ describe('TypeGuards - isPartialAppConfig', () => {
172
177
  it('returns true for valid partial config', () => {
173
178
  const config = {
174
179
  port: 3000,
175
- host: 'localhost'
180
+ host: 'localhost',
176
181
  };
177
182
 
178
183
  expect(isPartialAppConfig(config)).toBe(true);
@@ -199,7 +204,7 @@ describe('TypeGuards - isPartialAppConfig', () => {
199
204
  it('returns true for object with any properties', () => {
200
205
  const config = {
201
206
  anyProp: 'value',
202
- anotherProp: 123
207
+ anotherProp: 123,
203
208
  };
204
209
 
205
210
  expect(isPartialAppConfig(config)).toBe(true);
@@ -296,7 +301,7 @@ describe('TypeGuards - hasDefaultExport', () => {
296
301
  it('returns true for object with default and other properties', () => {
297
302
  const module = {
298
303
  default: 'value',
299
- named: 'export'
304
+ named: 'export',
300
305
  };
301
306
 
302
307
  expect(hasDefaultExport(module)).toBe(true);
@@ -308,7 +313,7 @@ describe('TypeGuards - Type Narrowing', () => {
308
313
  const data: unknown = {
309
314
  storeId: 'store-123',
310
315
  path: '/file.ts',
311
- docType: 'code'
316
+ docType: 'code',
312
317
  };
313
318
 
314
319
  if (isDocumentMetadata(data)) {
@@ -2,8 +2,8 @@
2
2
  * Type guard utilities to replace type assertions
3
3
  */
4
4
 
5
- import type { DocumentMetadata } from '../types/document.js';
6
5
  import type { AppConfig } from '../types/config.js';
6
+ import type { DocumentMetadata } from '../types/document.js';
7
7
 
8
8
  /**
9
9
  * Safely parse JSON with validation
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { JobService } from '../services/job.service.js';
5
2
  import { BackgroundWorker } from './background-worker.js';
3
+ import { writePidFile, deletePidFile, buildPidFilePath } from './pid-file.js';
6
4
  import { createServices } from '../services/index.js';
5
+ import { JobService } from '../services/job.service.js';
7
6
 
8
7
  /**
9
8
  * Background worker CLI entry point
@@ -27,16 +26,18 @@ async function main(): Promise<void> {
27
26
  const jobService = new JobService(dataDir);
28
27
  const services = await createServices(undefined, dataDir);
29
28
 
30
- // Write PID file for job cancellation
31
- const pidFile = path.join(
29
+ // Write PID file for job cancellation - CRITICAL: must succeed or job cannot be cancelled
30
+ const pidFile = buildPidFilePath(
32
31
  jobService['jobsDir'], // Access private field for PID path
33
- `${jobId}.pid`
32
+ jobId
34
33
  );
35
34
 
36
35
  try {
37
- fs.writeFileSync(pidFile, process.pid.toString(), 'utf-8');
36
+ writePidFile(pidFile, process.pid);
38
37
  } catch (error) {
39
- console.error('Warning: Could not write PID file:', error);
38
+ // CRITICAL: Cannot proceed without PID file - job would be uncancellable
39
+ console.error(error instanceof Error ? error.message : String(error));
40
+ process.exit(1);
40
41
  }
41
42
 
42
43
  // Handle SIGTERM for graceful shutdown
@@ -44,16 +45,15 @@ async function main(): Promise<void> {
44
45
  console.log(`[${jobId}] Received SIGTERM, cancelling job...`);
45
46
  jobService.updateJob(jobId, {
46
47
  status: 'cancelled',
47
- message: 'Job cancelled by user'
48
+ message: 'Job cancelled by user',
48
49
  });
49
50
 
50
- // Clean up PID file
51
- try {
52
- if (fs.existsSync(pidFile)) {
53
- fs.unlinkSync(pidFile);
54
- }
55
- } catch (error) {
56
- console.error('Warning: Could not remove PID file:', error);
51
+ // Clean up PID file (best-effort - don't block shutdown)
52
+ const deleteResult = deletePidFile(pidFile, 'sigterm');
53
+ if (!deleteResult.success && deleteResult.error !== undefined) {
54
+ console.error(
55
+ `Warning: Could not remove PID file during SIGTERM: ${deleteResult.error.message}`
56
+ );
57
57
  }
58
58
 
59
59
  process.exit(0);
@@ -71,13 +71,12 @@ async function main(): Promise<void> {
71
71
  try {
72
72
  await worker.executeJob(jobId);
73
73
 
74
- // Clean up PID file on success
75
- try {
76
- if (fs.existsSync(pidFile)) {
77
- fs.unlinkSync(pidFile);
78
- }
79
- } catch (error) {
80
- console.error('Warning: Could not remove PID file:', error);
74
+ // Clean up PID file on success (best-effort - don't change exit code)
75
+ const successCleanup = deletePidFile(pidFile, 'success');
76
+ if (!successCleanup.success && successCleanup.error !== undefined) {
77
+ console.error(
78
+ `Warning: Could not remove PID file after success: ${successCleanup.error.message}`
79
+ );
81
80
  }
82
81
 
83
82
  console.log(`[${jobId}] Job completed successfully`);
@@ -86,13 +85,12 @@ async function main(): Promise<void> {
86
85
  // Job service already updated with failure status in BackgroundWorker
87
86
  console.error(`[${jobId}] Job failed:`, error);
88
87
 
89
- // Clean up PID file on failure
90
- try {
91
- if (fs.existsSync(pidFile)) {
92
- fs.unlinkSync(pidFile);
93
- }
94
- } catch (cleanupError) {
95
- console.error('Warning: Could not remove PID file:', cleanupError);
88
+ // Clean up PID file on failure (best-effort - exit code reflects job failure)
89
+ const failureCleanup = deletePidFile(pidFile, 'failure');
90
+ if (!failureCleanup.success && failureCleanup.error !== undefined) {
91
+ console.error(
92
+ `Warning: Could not remove PID file after failure: ${failureCleanup.error.message}`
93
+ );
96
94
  }
97
95
 
98
96
  process.exit(1);
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { BackgroundWorker } from './background-worker.js';
2
+ import { BackgroundWorker, calculateIndexProgress } from './background-worker.js';
3
3
  import { JobService } from '../services/job.service.js';
4
4
  import { StoreService } from '../services/store.service.js';
5
5
  import { IndexService } from '../services/index.service.js';
@@ -31,7 +31,13 @@ describe('BackgroundWorker', () => {
31
31
  embeddingEngine = {
32
32
  embed: vi.fn().mockResolvedValue(new Array(384).fill(0)),
33
33
  } as unknown as EmbeddingEngine;
34
- worker = new BackgroundWorker(jobService, storeService, indexService, lanceStore, embeddingEngine);
34
+ worker = new BackgroundWorker(
35
+ jobService,
36
+ storeService,
37
+ indexService,
38
+ lanceStore,
39
+ embeddingEngine
40
+ );
35
41
  });
36
42
 
37
43
  afterEach(() => {
@@ -42,27 +48,23 @@ describe('BackgroundWorker', () => {
42
48
 
43
49
  describe('executeJob', () => {
44
50
  it('should throw error for non-existent job', async () => {
45
- await expect(worker.executeJob('non-existent')).rejects.toThrow(
46
- 'Job non-existent not found'
47
- );
51
+ await expect(worker.executeJob('non-existent')).rejects.toThrow('Job non-existent not found');
48
52
  });
49
53
 
50
54
  it('should throw error for unknown job type', async () => {
51
55
  const job = jobService.createJob({
52
56
  // @ts-expect-error testing invalid job type
53
57
  type: 'unknown',
54
- details: { storeId: 'test' }
58
+ details: { storeId: 'test' },
55
59
  });
56
60
 
57
- await expect(worker.executeJob(job.id)).rejects.toThrow(
58
- 'Unknown job type: unknown'
59
- );
61
+ await expect(worker.executeJob(job.id)).rejects.toThrow('Unknown job type: unknown');
60
62
  });
61
63
 
62
64
  it('should set job to running status before execution', async () => {
63
65
  const job = jobService.createJob({
64
66
  type: 'crawl',
65
- details: { storeId: 'test' }
67
+ details: { storeId: 'test' },
66
68
  });
67
69
 
68
70
  try {
@@ -79,12 +81,10 @@ describe('BackgroundWorker', () => {
79
81
  it('should update job to failed status on error', async () => {
80
82
  const job = jobService.createJob({
81
83
  type: 'crawl',
82
- details: { storeId: 'test', url: 'https://example.com' }
84
+ details: { storeId: 'test', url: 'https://example.com' },
83
85
  });
84
86
 
85
- await expect(worker.executeJob(job.id)).rejects.toThrow(
86
- 'Web store test not found'
87
- );
87
+ await expect(worker.executeJob(job.id)).rejects.toThrow('Web store test not found');
88
88
 
89
89
  const updated = jobService.getJob(job.id);
90
90
  expect(updated?.status).toBe('failed');
@@ -96,48 +96,39 @@ describe('BackgroundWorker', () => {
96
96
  it('should throw error for job without storeId', async () => {
97
97
  const job = jobService.createJob({
98
98
  type: 'index',
99
- details: {}
99
+ details: {},
100
100
  });
101
101
 
102
- await expect(worker.executeJob(job.id)).rejects.toThrow(
103
- 'Store ID required for index job'
104
- );
102
+ await expect(worker.executeJob(job.id)).rejects.toThrow('Store ID required for index job');
105
103
  });
106
104
 
107
105
  it('should throw error for non-existent store', async () => {
108
106
  const job = jobService.createJob({
109
107
  type: 'index',
110
- details: { storeId: 'non-existent-store' }
108
+ details: { storeId: 'non-existent-store' },
111
109
  });
112
110
 
113
- await expect(worker.executeJob(job.id)).rejects.toThrow(
114
- 'Store non-existent-store not found'
115
- );
111
+ await expect(worker.executeJob(job.id)).rejects.toThrow('Store non-existent-store not found');
116
112
  });
117
-
118
113
  });
119
114
 
120
115
  describe('executeCloneJob', () => {
121
116
  it('should throw error for job without storeId', async () => {
122
117
  const job = jobService.createJob({
123
118
  type: 'clone',
124
- details: {}
119
+ details: {},
125
120
  });
126
121
 
127
- await expect(worker.executeJob(job.id)).rejects.toThrow(
128
- 'Store ID required for clone job'
129
- );
122
+ await expect(worker.executeJob(job.id)).rejects.toThrow('Store ID required for clone job');
130
123
  });
131
124
 
132
125
  it('should throw error for non-existent store', async () => {
133
126
  const job = jobService.createJob({
134
127
  type: 'clone',
135
- details: { storeId: 'non-existent-store' }
128
+ details: { storeId: 'non-existent-store' },
136
129
  });
137
130
 
138
- await expect(worker.executeJob(job.id)).rejects.toThrow(
139
- 'Store non-existent-store not found'
140
- );
131
+ await expect(worker.executeJob(job.id)).rejects.toThrow('Store non-existent-store not found');
141
132
  });
142
133
  });
143
134
 
@@ -145,29 +136,25 @@ describe('BackgroundWorker', () => {
145
136
  it('should throw error for job without storeId', async () => {
146
137
  const job = jobService.createJob({
147
138
  type: 'crawl',
148
- details: { url: 'https://example.com' }
139
+ details: { url: 'https://example.com' },
149
140
  });
150
141
 
151
- await expect(worker.executeJob(job.id)).rejects.toThrow(
152
- 'Store ID required for crawl job'
153
- );
142
+ await expect(worker.executeJob(job.id)).rejects.toThrow('Store ID required for crawl job');
154
143
  });
155
144
 
156
145
  it('should throw error for job without url', async () => {
157
146
  const job = jobService.createJob({
158
147
  type: 'crawl',
159
- details: { storeId: 'test-store' }
148
+ details: { storeId: 'test-store' },
160
149
  });
161
150
 
162
- await expect(worker.executeJob(job.id)).rejects.toThrow(
163
- 'URL required for crawl job'
164
- );
151
+ await expect(worker.executeJob(job.id)).rejects.toThrow('URL required for crawl job');
165
152
  });
166
153
 
167
154
  it('should throw error for non-existent store', async () => {
168
155
  const job = jobService.createJob({
169
156
  type: 'crawl',
170
- details: { storeId: 'non-existent-store', url: 'https://example.com' }
157
+ details: { storeId: 'non-existent-store', url: 'https://example.com' },
171
158
  });
172
159
 
173
160
  await expect(worker.executeJob(job.id)).rejects.toThrow(
@@ -175,4 +162,31 @@ describe('BackgroundWorker', () => {
175
162
  );
176
163
  });
177
164
  });
165
+
166
+ describe('calculateIndexProgress', () => {
167
+ it('handles event.total === 0 without division by zero (NaN)', () => {
168
+ const result = calculateIndexProgress(0, 0, 70);
169
+ expect(Number.isNaN(result)).toBe(false);
170
+ expect(result).toBe(0);
171
+ });
172
+
173
+ it('handles event.total === 0 with scale 100', () => {
174
+ const result = calculateIndexProgress(0, 0, 100);
175
+ expect(Number.isNaN(result)).toBe(false);
176
+ expect(result).toBe(0);
177
+ });
178
+
179
+ it('calculates progress correctly for non-zero total', () => {
180
+ // 5/10 * 70 = 35
181
+ expect(calculateIndexProgress(5, 10, 70)).toBe(35);
182
+ // 5/10 * 100 = 50
183
+ expect(calculateIndexProgress(5, 10, 100)).toBe(50);
184
+ // 10/10 * 100 = 100
185
+ expect(calculateIndexProgress(10, 10, 100)).toBe(100);
186
+ });
187
+
188
+ it('uses default scale of 100', () => {
189
+ expect(calculateIndexProgress(5, 10)).toBe(50);
190
+ });
191
+ });
178
192
  });
@@ -1,13 +1,29 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { IntelligentCrawler, type CrawlProgress } from '../crawl/intelligent-crawler.js';
3
+ import { IndexService } from '../services/index.service.js';
2
4
  import { JobService } from '../services/job.service.js';
3
5
  import { StoreService } from '../services/store.service.js';
4
- import { IndexService } from '../services/index.service.js';
5
- import type { LanceStore } from '../db/lance.js';
6
+ import { createStoreId, createDocumentId } from '../types/brands.js';
6
7
  import type { EmbeddingEngine } from '../db/embeddings.js';
7
- import { IntelligentCrawler, type CrawlProgress } from '../crawl/intelligent-crawler.js';
8
- import type { Job } from '../types/job.js';
8
+ import type { LanceStore } from '../db/lance.js';
9
9
  import type { Document } from '../types/document.js';
10
- import { createStoreId, createDocumentId } from '../types/brands.js';
10
+ import type { Job } from '../types/job.js';
11
+
12
+ /**
13
+ * Calculate index progress as a percentage, handling division by zero.
14
+ * @param current - Current number of items processed
15
+ * @param total - Total number of items (may be 0)
16
+ * @param scale - Scale factor for progress (default 100 for 0-100%)
17
+ * @returns Progress value, or 0 if total is 0
18
+ */
19
+ export function calculateIndexProgress(
20
+ current: number,
21
+ total: number,
22
+ scale: number = 100
23
+ ): number {
24
+ if (total === 0) return 0;
25
+ return (current / total) * scale;
26
+ }
11
27
 
12
28
  export class BackgroundWorker {
13
29
  constructor(
@@ -34,7 +50,7 @@ export class BackgroundWorker {
34
50
  status: 'running',
35
51
  message: `Starting ${job.type} operation...`,
36
52
  progress: 0,
37
- details: { startedAt: new Date().toISOString() }
53
+ details: { startedAt: new Date().toISOString() },
38
54
  });
39
55
 
40
56
  // Execute based on job type
@@ -57,12 +73,12 @@ export class BackgroundWorker {
57
73
  status: 'completed',
58
74
  progress: 100,
59
75
  message: `${job.type} operation completed successfully`,
60
- details: { completedAt: new Date().toISOString() }
76
+ details: { completedAt: new Date().toISOString() },
61
77
  });
62
78
  } catch (error) {
63
79
  // Mark as failed
64
80
  const errorDetails: Record<string, unknown> = {
65
- completedAt: new Date().toISOString()
81
+ completedAt: new Date().toISOString(),
66
82
  };
67
83
  if (error instanceof Error && error.stack !== undefined) {
68
84
  errorDetails['error'] = error.stack;
@@ -72,7 +88,7 @@ export class BackgroundWorker {
72
88
  this.jobService.updateJob(jobId, {
73
89
  status: 'failed',
74
90
  message: error instanceof Error ? error.message : 'Unknown error',
75
- details: errorDetails
91
+ details: errorDetails,
76
92
  });
77
93
  throw error;
78
94
  }
@@ -101,30 +117,33 @@ export class BackgroundWorker {
101
117
  this.jobService.updateJob(job.id, {
102
118
  status: 'running',
103
119
  message: 'Repository cloned, starting indexing...',
104
- progress: 30
120
+ progress: 30,
105
121
  });
106
122
 
107
123
  // Index the repository with progress updates
108
- const result = await this.indexService.indexStore(store, (event: { type: string; current: number; total: number; message: string }) => {
109
- // Check if job was cancelled
110
- const currentJob = this.jobService.getJob(job.id);
111
- if (currentJob?.status === 'cancelled') {
112
- throw new Error('Job cancelled by user');
113
- }
124
+ const result = await this.indexService.indexStore(
125
+ store,
126
+ (event: { type: string; current: number; total: number; message: string }) => {
127
+ // Check if job was cancelled
128
+ const currentJob = this.jobService.getJob(job.id);
129
+ if (currentJob?.status === 'cancelled') {
130
+ throw new Error('Job cancelled by user');
131
+ }
114
132
 
115
- // Indexing is 70% of total progress (30-100%)
116
- const indexProgress = (event.current / event.total) * 70;
117
- const totalProgress = 30 + indexProgress;
133
+ // Indexing is 70% of total progress (30-100%)
134
+ const indexProgress = calculateIndexProgress(event.current, event.total, 70);
135
+ const totalProgress = 30 + indexProgress;
118
136
 
119
- this.jobService.updateJob(job.id, {
120
- message: `Indexed ${String(event.current)}/${String(event.total)} files`,
121
- progress: Math.min(99, totalProgress), // Cap at 99 until fully complete
122
- details: {
123
- filesProcessed: event.current,
124
- totalFiles: event.total
125
- }
126
- });
127
- });
137
+ this.jobService.updateJob(job.id, {
138
+ message: `Indexed ${String(event.current)}/${String(event.total)} files`,
139
+ progress: Math.min(99, totalProgress), // Cap at 99 until fully complete
140
+ details: {
141
+ filesProcessed: event.current,
142
+ totalFiles: event.total,
143
+ },
144
+ });
145
+ }
146
+ );
128
147
 
129
148
  if (!result.success) {
130
149
  throw result.error;
@@ -148,24 +167,27 @@ export class BackgroundWorker {
148
167
  }
149
168
 
150
169
  // Index with progress updates
151
- const result = await this.indexService.indexStore(store, (event: { type: string; current: number; total: number; message: string }) => {
152
- // Check if job was cancelled
153
- const currentJob = this.jobService.getJob(job.id);
154
- if (currentJob?.status === 'cancelled') {
155
- throw new Error('Job cancelled by user');
156
- }
170
+ const result = await this.indexService.indexStore(
171
+ store,
172
+ (event: { type: string; current: number; total: number; message: string }) => {
173
+ // Check if job was cancelled
174
+ const currentJob = this.jobService.getJob(job.id);
175
+ if (currentJob?.status === 'cancelled') {
176
+ throw new Error('Job cancelled by user');
177
+ }
157
178
 
158
- const progress = (event.current / event.total) * 100;
179
+ const progress = calculateIndexProgress(event.current, event.total);
159
180
 
160
- this.jobService.updateJob(job.id, {
161
- message: `Indexed ${String(event.current)}/${String(event.total)} files`,
162
- progress: Math.min(99, progress), // Cap at 99 until fully complete
163
- details: {
164
- filesProcessed: event.current,
165
- totalFiles: event.total
166
- }
167
- });
168
- });
181
+ this.jobService.updateJob(job.id, {
182
+ message: `Indexed ${String(event.current)}/${String(event.total)} files`,
183
+ progress: Math.min(99, progress), // Cap at 99 until fully complete
184
+ details: {
185
+ filesProcessed: event.current,
186
+ totalFiles: event.total,
187
+ },
188
+ });
189
+ }
190
+ );
169
191
 
170
192
  if (!result.success) {
171
193
  throw result.error;
@@ -176,15 +198,8 @@ export class BackgroundWorker {
176
198
  * Execute a crawl job (web crawling + indexing)
177
199
  */
178
200
  private async executeCrawlJob(job: Job): Promise<void> {
179
- const {
180
- storeId,
181
- url,
182
- crawlInstruction,
183
- extractInstruction,
184
- maxPages,
185
- simple,
186
- useHeadless,
187
- } = job.details;
201
+ const { storeId, url, crawlInstruction, extractInstruction, maxPages, simple, useHeadless } =
202
+ job.details;
188
203
 
189
204
  if (storeId === undefined || typeof storeId !== 'string') {
190
205
  throw new Error('Store ID required for crawl job');
@@ -195,7 +210,7 @@ export class BackgroundWorker {
195
210
 
196
211
  // Get the store
197
212
  const store = await this.storeService.get(createStoreId(storeId));
198
- if (!store || store.type !== 'web') {
213
+ if (store?.type !== 'web') {
199
214
  throw new Error(`Web store ${storeId} not found`);
200
215
  }
201
216
 
@@ -204,10 +219,9 @@ export class BackgroundWorker {
204
219
 
205
220
  // Listen for progress events
206
221
  crawler.on('progress', (progress: CrawlProgress) => {
207
- // Check if job was cancelled
222
+ // Check if job was cancelled - just return early, for-await loop will throw and finally will cleanup
208
223
  const currentJob = this.jobService.getJob(job.id);
209
224
  if (currentJob?.status === 'cancelled') {
210
- void crawler.stop();
211
225
  return;
212
226
  }
213
227
 
@@ -215,9 +229,11 @@ export class BackgroundWorker {
215
229
  const crawlProgress = (progress.pagesVisited / resolvedMaxPages) * 80;
216
230
 
217
231
  this.jobService.updateJob(job.id, {
218
- message: progress.message ?? `Crawling page ${String(progress.pagesVisited)}/${String(resolvedMaxPages)}`,
232
+ message:
233
+ progress.message ??
234
+ `Crawling page ${String(progress.pagesVisited)}/${String(resolvedMaxPages)}`,
219
235
  progress: Math.min(80, crawlProgress),
220
- details: { pagesCrawled: progress.pagesVisited }
236
+ details: { pagesCrawled: progress.pagesVisited },
221
237
  });
222
238
  });
223
239
 
@@ -276,7 +292,7 @@ export class BackgroundWorker {
276
292
  if (docs.length > 0) {
277
293
  this.jobService.updateJob(job.id, {
278
294
  message: 'Indexing crawled documents...',
279
- progress: 85
295
+ progress: 85,
280
296
  });
281
297
 
282
298
  await this.lanceStore.addDocuments(store.id, docs);
@@ -285,7 +301,7 @@ export class BackgroundWorker {
285
301
  this.jobService.updateJob(job.id, {
286
302
  message: `Crawled and indexed ${String(docs.length)} pages`,
287
303
  progress: 100,
288
- details: { pagesCrawled: docs.length }
304
+ details: { pagesCrawled: docs.length },
289
305
  });
290
306
  } finally {
291
307
  await crawler.stop();