bluera-knowledge 0.9.31 → 0.9.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/code-review.md +15 -0
- package/.claude/hooks/post-edit-check.sh +5 -3
- package/.claude/skills/atomic-commits/SKILL.md +3 -1
- package/.claude/skills/code-review-repo/skill.md +62 -0
- package/.husky/pre-commit +3 -2
- package/.prettierrc +9 -0
- package/.versionrc.json +1 -1
- package/CHANGELOG.md +35 -0
- package/CLAUDE.md +6 -0
- package/README.md +25 -13
- package/bun.lock +277 -33
- package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
- package/dist/chunk-6FHWC36B.js.map +1 -0
- package/dist/{chunk-2SJHNRXD.js → chunk-DC7CGSGT.js} +288 -241
- package/dist/chunk-DC7CGSGT.js.map +1 -0
- package/dist/{chunk-RWSXP3PQ.js → chunk-WFNPNAAP.js} +3194 -3024
- package/dist/chunk-WFNPNAAP.js.map +1 -0
- package/dist/{chunk-OGEY66FZ.js → chunk-Z2KKVH45.js} +548 -482
- package/dist/chunk-Z2KKVH45.js.map +1 -0
- package/dist/index.js +871 -754
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/watch.service-BJV3TI3F.js +7 -0
- package/dist/workers/background-worker-cli.js +46 -45
- package/dist/workers/background-worker-cli.js.map +1 -1
- package/eslint.config.js +43 -1
- package/package.json +18 -11
- package/plugin.json +8 -0
- package/python/requirements.txt +1 -1
- package/src/analysis/ast-parser.test.ts +12 -11
- package/src/analysis/ast-parser.ts +28 -22
- package/src/analysis/code-graph.test.ts +52 -62
- package/src/analysis/code-graph.ts +9 -13
- package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
- package/src/analysis/dependency-usage-analyzer.ts +52 -24
- package/src/analysis/go-ast-parser.test.ts +22 -22
- package/src/analysis/go-ast-parser.ts +18 -25
- package/src/analysis/parser-factory.test.ts +9 -9
- package/src/analysis/parser-factory.ts +3 -3
- package/src/analysis/python-ast-parser.test.ts +27 -27
- package/src/analysis/python-ast-parser.ts +2 -2
- package/src/analysis/repo-url-resolver.test.ts +82 -82
- package/src/analysis/rust-ast-parser.test.ts +19 -19
- package/src/analysis/rust-ast-parser.ts +17 -27
- package/src/analysis/tree-sitter-parser.test.ts +3 -3
- package/src/analysis/tree-sitter-parser.ts +10 -16
- package/src/cli/commands/crawl.test.ts +40 -24
- package/src/cli/commands/crawl.ts +186 -161
- package/src/cli/commands/index-cmd.test.ts +90 -90
- package/src/cli/commands/index-cmd.ts +52 -36
- package/src/cli/commands/mcp.test.ts +6 -6
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/plugin-api.test.ts +16 -18
- package/src/cli/commands/plugin-api.ts +9 -6
- package/src/cli/commands/search.test.ts +16 -7
- package/src/cli/commands/search.ts +124 -87
- package/src/cli/commands/serve.test.ts +67 -25
- package/src/cli/commands/serve.ts +18 -3
- package/src/cli/commands/setup.test.ts +176 -101
- package/src/cli/commands/setup.ts +140 -117
- package/src/cli/commands/store.test.ts +82 -53
- package/src/cli/commands/store.ts +56 -37
- package/src/cli/program.ts +2 -2
- package/src/crawl/article-converter.test.ts +4 -1
- package/src/crawl/article-converter.ts +46 -31
- package/src/crawl/bridge.test.ts +240 -132
- package/src/crawl/bridge.ts +87 -30
- package/src/crawl/claude-client.test.ts +124 -56
- package/src/crawl/claude-client.ts +7 -15
- package/src/crawl/intelligent-crawler.test.ts +65 -22
- package/src/crawl/intelligent-crawler.ts +86 -53
- package/src/crawl/markdown-utils.ts +1 -4
- package/src/db/embeddings.ts +4 -6
- package/src/db/lance.test.ts +63 -4
- package/src/db/lance.ts +31 -12
- package/src/index.ts +26 -17
- package/src/logging/index.ts +1 -5
- package/src/logging/logger.ts +3 -5
- package/src/logging/payload.test.ts +1 -1
- package/src/logging/payload.ts +3 -5
- package/src/mcp/commands/index.ts +2 -2
- package/src/mcp/commands/job.commands.ts +12 -18
- package/src/mcp/commands/meta.commands.ts +13 -13
- package/src/mcp/commands/registry.ts +5 -8
- package/src/mcp/commands/store.commands.ts +19 -19
- package/src/mcp/handlers/execute.handler.test.ts +10 -10
- package/src/mcp/handlers/execute.handler.ts +4 -5
- package/src/mcp/handlers/index.ts +10 -14
- package/src/mcp/handlers/job.handler.test.ts +10 -10
- package/src/mcp/handlers/job.handler.ts +22 -25
- package/src/mcp/handlers/search.handler.test.ts +36 -65
- package/src/mcp/handlers/search.handler.ts +135 -104
- package/src/mcp/handlers/store.handler.test.ts +41 -52
- package/src/mcp/handlers/store.handler.ts +108 -88
- package/src/mcp/schemas/index.test.ts +73 -68
- package/src/mcp/schemas/index.ts +18 -12
- package/src/mcp/server.test.ts +1 -1
- package/src/mcp/server.ts +59 -46
- package/src/plugin/commands.test.ts +230 -95
- package/src/plugin/commands.ts +24 -25
- package/src/plugin/dependency-analyzer.test.ts +52 -52
- package/src/plugin/dependency-analyzer.ts +85 -22
- package/src/plugin/git-clone.test.ts +24 -13
- package/src/plugin/git-clone.ts +3 -7
- package/src/server/app.test.ts +109 -109
- package/src/server/app.ts +32 -23
- package/src/server/index.test.ts +64 -66
- package/src/services/chunking.service.test.ts +32 -32
- package/src/services/chunking.service.ts +16 -9
- package/src/services/code-graph.service.test.ts +30 -36
- package/src/services/code-graph.service.ts +24 -10
- package/src/services/code-unit.service.test.ts +55 -11
- package/src/services/code-unit.service.ts +85 -11
- package/src/services/config.service.test.ts +37 -18
- package/src/services/config.service.ts +30 -7
- package/src/services/index.service.test.ts +49 -18
- package/src/services/index.service.ts +98 -48
- package/src/services/index.ts +8 -10
- package/src/services/job.service.test.ts +22 -22
- package/src/services/job.service.ts +18 -18
- package/src/services/project-root.service.test.ts +1 -3
- package/src/services/search.service.test.ts +248 -120
- package/src/services/search.service.ts +286 -156
- package/src/services/services.test.ts +36 -0
- package/src/services/snippet.service.test.ts +14 -6
- package/src/services/snippet.service.ts +7 -5
- package/src/services/store.service.test.ts +68 -29
- package/src/services/store.service.ts +41 -12
- package/src/services/watch.service.test.ts +34 -14
- package/src/services/watch.service.ts +11 -1
- package/src/types/brands.test.ts +3 -1
- package/src/types/index.ts +2 -13
- package/src/types/search.ts +10 -8
- package/src/utils/type-guards.test.ts +20 -15
- package/src/utils/type-guards.ts +1 -1
- package/src/workers/background-worker-cli.ts +2 -2
- package/src/workers/background-worker.test.ts +54 -40
- package/src/workers/background-worker.ts +76 -60
- package/src/workers/spawn-worker.test.ts +22 -10
- package/src/workers/spawn-worker.ts +6 -6
- package/tests/analysis/ast-parser.test.ts +3 -3
- package/tests/analysis/code-graph.test.ts +5 -5
- package/tests/fixtures/code-snippets/api/error-handling.ts +4 -15
- package/tests/fixtures/code-snippets/api/rest-controller.ts +3 -9
- package/tests/fixtures/code-snippets/auth/jwt-auth.ts +5 -21
- package/tests/fixtures/code-snippets/auth/oauth-flow.ts +4 -4
- package/tests/fixtures/code-snippets/database/repository-pattern.ts +11 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +22 -20
- package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +13 -10
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +10 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +16 -16
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +7 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +6 -6
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +4 -4
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +166 -169
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +8 -8
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +30 -33
- package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +2 -2
- package/tests/fixtures/test-server.ts +3 -2
- package/tests/helpers/performance-metrics.ts +8 -25
- package/tests/helpers/search-relevance.ts +14 -69
- package/tests/integration/cli-consistency.test.ts +5 -4
- package/tests/integration/e2e-workflow.test.ts +2 -0
- package/tests/integration/python-bridge.test.ts +13 -3
- package/tests/mcp/server.test.ts +1 -1
- package/tests/services/code-unit.service.test.ts +48 -0
- package/tests/services/job.service.test.ts +124 -0
- package/tests/services/search.progressive-context.test.ts +2 -2
- package/.claude-plugin/plugin.json +0 -13
- package/BUGS-FOUND.md +0 -71
- package/dist/chunk-2SJHNRXD.js.map +0 -1
- package/dist/chunk-L2YVNC63.js.map +0 -1
- package/dist/chunk-OGEY66FZ.js.map +0 -1
- package/dist/chunk-RWSXP3PQ.js.map +0 -1
- package/dist/watch.service-YAIKKDCF.js +0 -7
- package/skills/atomic-commits/SKILL.md +0 -77
- /package/dist/{watch.service-YAIKKDCF.js.map → watch.service-BJV3TI3F.js.map} +0 -0
package/src/types/index.ts
CHANGED
|
@@ -9,15 +9,7 @@ export {
|
|
|
9
9
|
} from './brands.js';
|
|
10
10
|
|
|
11
11
|
// Result type
|
|
12
|
-
export {
|
|
13
|
-
type Result,
|
|
14
|
-
ok,
|
|
15
|
-
err,
|
|
16
|
-
isOk,
|
|
17
|
-
isErr,
|
|
18
|
-
unwrap,
|
|
19
|
-
unwrapOr,
|
|
20
|
-
} from './result.js';
|
|
12
|
+
export { type Result, ok, err, isOk, isErr, unwrap, unwrapOr } from './result.js';
|
|
21
13
|
|
|
22
14
|
// Store types
|
|
23
15
|
export {
|
|
@@ -60,7 +52,4 @@ export {
|
|
|
60
52
|
} from './config.js';
|
|
61
53
|
|
|
62
54
|
// Progress types
|
|
63
|
-
export {
|
|
64
|
-
type ProgressEvent,
|
|
65
|
-
type ProgressCallback,
|
|
66
|
-
} from './progress.js';
|
|
55
|
+
export { type ProgressEvent, type ProgressCallback } from './progress.js';
|
package/src/types/search.ts
CHANGED
|
@@ -75,14 +75,16 @@ export interface SearchResult {
|
|
|
75
75
|
readonly full?: ResultFull | undefined;
|
|
76
76
|
|
|
77
77
|
// Ranking attribution metadata for transparency
|
|
78
|
-
readonly rankingMetadata?:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
readonly rankingMetadata?:
|
|
79
|
+
| {
|
|
80
|
+
readonly vectorRank?: number; // Position in vector results (1-based)
|
|
81
|
+
readonly ftsRank?: number; // Position in FTS results (1-based)
|
|
82
|
+
readonly vectorRRF: number; // Vector contribution to RRF score
|
|
83
|
+
readonly ftsRRF: number; // FTS contribution to RRF score
|
|
84
|
+
readonly fileTypeBoost: number; // File type multiplier applied
|
|
85
|
+
readonly frameworkBoost: number; // Framework context multiplier
|
|
86
|
+
}
|
|
87
|
+
| undefined;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
export interface SearchResponse {
|
|
@@ -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
|
|
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)) {
|
package/src/utils/type-guards.ts
CHANGED
|
@@ -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,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { JobService } from '../services/job.service.js';
|
|
5
4
|
import { BackgroundWorker } from './background-worker.js';
|
|
6
5
|
import { createServices } from '../services/index.js';
|
|
6
|
+
import { JobService } from '../services/job.service.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Background worker CLI entry point
|
|
@@ -44,7 +44,7 @@ async function main(): Promise<void> {
|
|
|
44
44
|
console.log(`[${jobId}] Received SIGTERM, cancelling job...`);
|
|
45
45
|
jobService.updateJob(jobId, {
|
|
46
46
|
status: 'cancelled',
|
|
47
|
-
message: 'Job cancelled by user'
|
|
47
|
+
message: 'Job cancelled by user',
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
// Clean up PID file
|
|
@@ -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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
179
|
+
const progress = calculateIndexProgress(event.current, event.total);
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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();
|