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
|
@@ -32,16 +32,16 @@ export function doWork() {
|
|
|
32
32
|
helper();
|
|
33
33
|
return 'done';
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
}
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
37
|
];
|
|
38
38
|
|
|
39
39
|
const graph = await service.buildGraph(files);
|
|
40
40
|
const nodes = graph.getAllNodes();
|
|
41
41
|
|
|
42
42
|
expect(nodes.length).toBe(2);
|
|
43
|
-
expect(nodes.some(n => n.name === 'helper')).toBe(true);
|
|
44
|
-
expect(nodes.some(n => n.name === 'doWork')).toBe(true);
|
|
43
|
+
expect(nodes.some((n) => n.name === 'helper')).toBe(true);
|
|
44
|
+
expect(nodes.some((n) => n.name === 'doWork')).toBe(true);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it('should track function calls', async () => {
|
|
@@ -56,15 +56,15 @@ function caller() {
|
|
|
56
56
|
function callee() {
|
|
57
57
|
return 42;
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
}
|
|
59
|
+
`,
|
|
60
|
+
},
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
const graph = await service.buildGraph(files);
|
|
64
64
|
|
|
65
65
|
// Check that caller calls callee
|
|
66
66
|
const callerEdges = graph.getEdges('/src/main.ts:caller');
|
|
67
|
-
const callsCallee = callerEdges.some(e => e.to.includes('callee') && e.type === 'calls');
|
|
67
|
+
const callsCallee = callerEdges.some((e) => e.to.includes('callee') && e.type === 'calls');
|
|
68
68
|
expect(callsCallee).toBe(true);
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -78,13 +78,13 @@ import { helper } from './utils.js';
|
|
|
78
78
|
function useHelper() {
|
|
79
79
|
helper();
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
}
|
|
81
|
+
`,
|
|
82
|
+
},
|
|
83
83
|
];
|
|
84
84
|
|
|
85
85
|
const graph = await service.buildGraph(files);
|
|
86
86
|
const consumerEdges = graph.getEdges('/src/consumer.ts');
|
|
87
|
-
const hasImport = consumerEdges.some(e => e.type === 'imports');
|
|
87
|
+
const hasImport = consumerEdges.some((e) => e.type === 'imports');
|
|
88
88
|
expect(hasImport).toBe(true);
|
|
89
89
|
});
|
|
90
90
|
|
|
@@ -92,7 +92,7 @@ function useHelper() {
|
|
|
92
92
|
const files = [
|
|
93
93
|
{ path: '/src/readme.md', content: '# README\n\nThis is docs.' },
|
|
94
94
|
{ path: '/src/config.json', content: '{"key": "value"}' },
|
|
95
|
-
{ path: '/src/main.ts', content: 'export function main() {}' }
|
|
95
|
+
{ path: '/src/main.ts', content: 'export function main() {}' },
|
|
96
96
|
];
|
|
97
97
|
|
|
98
98
|
const graph = await service.buildGraph(files);
|
|
@@ -114,8 +114,8 @@ function useHelper() {
|
|
|
114
114
|
export function helper() {
|
|
115
115
|
return 'help';
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
}
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
119
|
];
|
|
120
120
|
|
|
121
121
|
const graph = await service.buildGraph(files);
|
|
@@ -140,9 +140,7 @@ export function helper() {
|
|
|
140
140
|
|
|
141
141
|
it('should cache loaded graphs', async () => {
|
|
142
142
|
const storeId = createStoreId('cached-store');
|
|
143
|
-
const files = [
|
|
144
|
-
{ path: '/src/main.ts', content: 'export function main() {}' }
|
|
145
|
-
];
|
|
143
|
+
const files = [{ path: '/src/main.ts', content: 'export function main() {}' }];
|
|
146
144
|
|
|
147
145
|
const graph = await service.buildGraph(files);
|
|
148
146
|
await service.saveGraph(storeId, graph);
|
|
@@ -157,9 +155,7 @@ export function helper() {
|
|
|
157
155
|
|
|
158
156
|
it('should persist graph to JSON file', async () => {
|
|
159
157
|
const storeId = createStoreId('persisted-store');
|
|
160
|
-
const files = [
|
|
161
|
-
{ path: '/src/main.ts', content: 'export function main() {}' }
|
|
162
|
-
];
|
|
158
|
+
const files = [{ path: '/src/main.ts', content: 'export function main() {}' }];
|
|
163
159
|
|
|
164
160
|
const graph = await service.buildGraph(files);
|
|
165
161
|
await service.saveGraph(storeId, graph);
|
|
@@ -189,8 +185,8 @@ export class Calculator {
|
|
|
189
185
|
return a - b;
|
|
190
186
|
}
|
|
191
187
|
}
|
|
192
|
-
|
|
193
|
-
}
|
|
188
|
+
`,
|
|
189
|
+
},
|
|
194
190
|
];
|
|
195
191
|
|
|
196
192
|
const graph = await service.buildGraph(files);
|
|
@@ -204,13 +200,13 @@ export class Calculator {
|
|
|
204
200
|
|
|
205
201
|
const nodes = loadedGraph!.getAllNodes();
|
|
206
202
|
// Should have class + 2 methods
|
|
207
|
-
const classNode = nodes.find(n => n.name === 'Calculator' && n.type === 'class');
|
|
208
|
-
const methodNodes = nodes.filter(n => n.type === 'method');
|
|
203
|
+
const classNode = nodes.find((n) => n.name === 'Calculator' && n.type === 'class');
|
|
204
|
+
const methodNodes = nodes.filter((n) => n.type === 'method');
|
|
209
205
|
|
|
210
206
|
expect(classNode).toBeDefined();
|
|
211
207
|
expect(methodNodes.length).toBe(2);
|
|
212
|
-
expect(methodNodes.some(m => m.name === 'add')).toBe(true);
|
|
213
|
-
expect(methodNodes.some(m => m.name === 'subtract')).toBe(true);
|
|
208
|
+
expect(methodNodes.some((m) => m.name === 'add')).toBe(true);
|
|
209
|
+
expect(methodNodes.some((m) => m.name === 'subtract')).toBe(true);
|
|
214
210
|
});
|
|
215
211
|
});
|
|
216
212
|
|
|
@@ -227,8 +223,8 @@ function caller() {
|
|
|
227
223
|
function target() {
|
|
228
224
|
return 1;
|
|
229
225
|
}
|
|
230
|
-
|
|
231
|
-
}
|
|
226
|
+
`,
|
|
227
|
+
},
|
|
232
228
|
];
|
|
233
229
|
|
|
234
230
|
const graph = await service.buildGraph(files);
|
|
@@ -260,31 +256,29 @@ function target() {
|
|
|
260
256
|
function callee() {
|
|
261
257
|
return 1;
|
|
262
258
|
}
|
|
263
|
-
|
|
264
|
-
}
|
|
259
|
+
`,
|
|
260
|
+
},
|
|
265
261
|
];
|
|
266
262
|
|
|
267
263
|
const graph = await service.buildGraph(files);
|
|
268
264
|
const related = service.getRelatedCode(graph, '/src/main.ts', 'target');
|
|
269
265
|
|
|
270
266
|
// target is called by caller (and possibly itself due to regex-based detection)
|
|
271
|
-
const callers = related.filter(r => r.relationship === 'calls this');
|
|
267
|
+
const callers = related.filter((r) => r.relationship === 'calls this');
|
|
272
268
|
expect(callers.length).toBeGreaterThanOrEqual(1);
|
|
273
|
-
expect(callers.some(c => c.id.includes('caller'))).toBe(true);
|
|
269
|
+
expect(callers.some((c) => c.id.includes('caller'))).toBe(true);
|
|
274
270
|
|
|
275
271
|
// target calls callee
|
|
276
|
-
const callees = related.filter(r => r.relationship === 'called by this');
|
|
272
|
+
const callees = related.filter((r) => r.relationship === 'called by this');
|
|
277
273
|
expect(callees.length).toBeGreaterThanOrEqual(1);
|
|
278
|
-
expect(callees.some(c => c.id.includes('callee'))).toBe(true);
|
|
274
|
+
expect(callees.some((c) => c.id.includes('callee'))).toBe(true);
|
|
279
275
|
});
|
|
280
276
|
});
|
|
281
277
|
|
|
282
278
|
describe('clearCache', () => {
|
|
283
279
|
it('should clear cached graphs', async () => {
|
|
284
280
|
const storeId = createStoreId('cache-test');
|
|
285
|
-
const files = [
|
|
286
|
-
{ path: '/src/main.ts', content: 'export function main() {}' }
|
|
287
|
-
];
|
|
281
|
+
const files = [{ path: '/src/main.ts', content: 'export function main() {}' }];
|
|
288
282
|
|
|
289
283
|
const graph = await service.buildGraph(files);
|
|
290
284
|
await service.saveGraph(storeId, graph);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
|
-
import { CodeGraph, type GraphNode } from '../analysis/code-graph.js';
|
|
4
3
|
import { ASTParser } from '../analysis/ast-parser.js';
|
|
5
|
-
import {
|
|
4
|
+
import { CodeGraph, type GraphNode } from '../analysis/code-graph.js';
|
|
6
5
|
import { GoASTParser } from '../analysis/go-ast-parser.js';
|
|
7
6
|
import { ParserFactory } from '../analysis/parser-factory.js';
|
|
7
|
+
import { RustASTParser } from '../analysis/rust-ast-parser.js';
|
|
8
8
|
import type { PythonBridge } from '../crawl/bridge.js';
|
|
9
9
|
import type { StoreId } from '../types/brands.js';
|
|
10
10
|
|
|
@@ -157,7 +157,7 @@ export class CodeGraphService {
|
|
|
157
157
|
name: node.name,
|
|
158
158
|
exported: node.exported,
|
|
159
159
|
startLine: node.startLine,
|
|
160
|
-
endLine: node.endLine
|
|
160
|
+
endLine: node.endLine,
|
|
161
161
|
};
|
|
162
162
|
if (node.signature !== undefined) {
|
|
163
163
|
graphNode.signature = node.signature;
|
|
@@ -195,7 +195,7 @@ export class CodeGraphService {
|
|
|
195
195
|
from: edge.from,
|
|
196
196
|
to: edge.to,
|
|
197
197
|
type: edgeType,
|
|
198
|
-
confidence: edge.confidence
|
|
198
|
+
confidence: edge.confidence,
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -209,18 +209,26 @@ export class CodeGraphService {
|
|
|
209
209
|
/**
|
|
210
210
|
* Get usage stats for a code element.
|
|
211
211
|
*/
|
|
212
|
-
getUsageStats(
|
|
212
|
+
getUsageStats(
|
|
213
|
+
graph: CodeGraph,
|
|
214
|
+
filePath: string,
|
|
215
|
+
symbolName: string
|
|
216
|
+
): { calledBy: number; calls: number } {
|
|
213
217
|
const nodeId = `${filePath}:${symbolName}`;
|
|
214
218
|
return {
|
|
215
219
|
calledBy: graph.getCalledByCount(nodeId),
|
|
216
|
-
calls: graph.getCallsCount(nodeId)
|
|
220
|
+
calls: graph.getCallsCount(nodeId),
|
|
217
221
|
};
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
/**
|
|
221
225
|
* Get related code (callers and callees) for a code element.
|
|
222
226
|
*/
|
|
223
|
-
getRelatedCode(
|
|
227
|
+
getRelatedCode(
|
|
228
|
+
graph: CodeGraph,
|
|
229
|
+
filePath: string,
|
|
230
|
+
symbolName: string
|
|
231
|
+
): Array<{ id: string; relationship: string }> {
|
|
224
232
|
const nodeId = `${filePath}:${symbolName}`;
|
|
225
233
|
const related: Array<{ id: string; relationship: string }> = [];
|
|
226
234
|
|
|
@@ -269,14 +277,18 @@ export class CodeGraphService {
|
|
|
269
277
|
/**
|
|
270
278
|
* Type guard for valid node types.
|
|
271
279
|
*/
|
|
272
|
-
private isValidNodeType(
|
|
280
|
+
private isValidNodeType(
|
|
281
|
+
type: string
|
|
282
|
+
): type is 'function' | 'class' | 'interface' | 'type' | 'const' | 'method' {
|
|
273
283
|
return ['function', 'class', 'interface', 'type', 'const', 'method'].includes(type);
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
/**
|
|
277
287
|
* Validate and return a node type, or undefined if invalid.
|
|
278
288
|
*/
|
|
279
|
-
private validateNodeType(
|
|
289
|
+
private validateNodeType(
|
|
290
|
+
type: string
|
|
291
|
+
): 'function' | 'class' | 'interface' | 'type' | 'const' | 'method' | undefined {
|
|
280
292
|
if (this.isValidNodeType(type)) {
|
|
281
293
|
return type;
|
|
282
294
|
}
|
|
@@ -293,7 +305,9 @@ export class CodeGraphService {
|
|
|
293
305
|
/**
|
|
294
306
|
* Validate and return an edge type, or undefined if invalid.
|
|
295
307
|
*/
|
|
296
|
-
private validateEdgeType(
|
|
308
|
+
private validateEdgeType(
|
|
309
|
+
type: string
|
|
310
|
+
): 'calls' | 'imports' | 'extends' | 'implements' | undefined {
|
|
297
311
|
if (this.isValidEdgeType(type)) {
|
|
298
312
|
return type;
|
|
299
313
|
}
|
|
@@ -97,7 +97,7 @@ function nested() {
|
|
|
97
97
|
expect(result?.fullContent).toContain('if (true)');
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
it('handles
|
|
100
|
+
it('correctly handles braces inside double-quoted string literals', () => {
|
|
101
101
|
const code = `
|
|
102
102
|
function withString() {
|
|
103
103
|
const template = "{ this is a brace }";
|
|
@@ -107,11 +107,24 @@ function withString() {
|
|
|
107
107
|
|
|
108
108
|
const result = service.extractCodeUnit(code, 'withString', 'typescript');
|
|
109
109
|
|
|
110
|
-
// NOTE: Current implementation has a bug - it doesn't handle braces in strings
|
|
111
|
-
// This test documents the current behavior
|
|
112
110
|
expect(result).toBeDefined();
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
expect(result?.endLine).toBe(4);
|
|
112
|
+
expect(result?.fullContent).toContain('return template');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('correctly handles braces inside single-quoted string literals', () => {
|
|
116
|
+
const code = `
|
|
117
|
+
function withSingleQuote() {
|
|
118
|
+
const json = '{"key": "value"}';
|
|
119
|
+
return json;
|
|
120
|
+
}
|
|
121
|
+
`.trim();
|
|
122
|
+
|
|
123
|
+
const result = service.extractCodeUnit(code, 'withSingleQuote', 'typescript');
|
|
124
|
+
|
|
125
|
+
expect(result).toBeDefined();
|
|
126
|
+
expect(result?.endLine).toBe(4);
|
|
127
|
+
expect(result?.fullContent).toContain('return json');
|
|
115
128
|
});
|
|
116
129
|
|
|
117
130
|
it('returns undefined for non-existent function', () => {
|
|
@@ -524,7 +537,7 @@ const unrelated = { key: "value" };
|
|
|
524
537
|
expect(result?.fullContent).not.toContain('unrelated');
|
|
525
538
|
});
|
|
526
539
|
|
|
527
|
-
it('handles
|
|
540
|
+
it('correctly handles braces in single-line comments', () => {
|
|
528
541
|
const code = `
|
|
529
542
|
function withComment() {
|
|
530
543
|
// This comment has a brace: {
|
|
@@ -534,15 +547,30 @@ function withComment() {
|
|
|
534
547
|
|
|
535
548
|
const result = service.extractCodeUnit(code, 'withComment', 'typescript');
|
|
536
549
|
|
|
537
|
-
// NOTE: Current implementation doesn't handle braces in comments
|
|
538
|
-
// This may cause incorrect boundary detection
|
|
539
550
|
expect(result).toBeDefined();
|
|
551
|
+
expect(result?.endLine).toBe(4);
|
|
552
|
+
expect(result?.fullContent).toContain('return true');
|
|
540
553
|
});
|
|
541
554
|
|
|
542
|
-
it('handles
|
|
555
|
+
it('correctly handles braces in multi-line comments', () => {
|
|
556
|
+
const code = `
|
|
557
|
+
function withMultiComment() {
|
|
558
|
+
/* { braces } in comment */
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
`.trim();
|
|
562
|
+
|
|
563
|
+
const result = service.extractCodeUnit(code, 'withMultiComment', 'typescript');
|
|
564
|
+
|
|
565
|
+
expect(result).toBeDefined();
|
|
566
|
+
expect(result?.endLine).toBe(4);
|
|
567
|
+
expect(result?.fullContent).toContain('return false');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('correctly handles template literals with embedded expressions', () => {
|
|
543
571
|
const code = `
|
|
544
572
|
function withTemplate() {
|
|
545
|
-
const str = \`template \${
|
|
573
|
+
const str = \`template \${value} end\`;
|
|
546
574
|
return str;
|
|
547
575
|
}
|
|
548
576
|
`.trim();
|
|
@@ -550,7 +578,23 @@ function withTemplate() {
|
|
|
550
578
|
const result = service.extractCodeUnit(code, 'withTemplate', 'typescript');
|
|
551
579
|
|
|
552
580
|
expect(result).toBeDefined();
|
|
553
|
-
expect(result?.
|
|
581
|
+
expect(result?.endLine).toBe(4);
|
|
582
|
+
expect(result?.fullContent).toContain('return str');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('correctly handles template literals with braces in text', () => {
|
|
586
|
+
const code = `
|
|
587
|
+
function withTemplateBraces() {
|
|
588
|
+
const json = \`{"key": "value"}\`;
|
|
589
|
+
return json;
|
|
590
|
+
}
|
|
591
|
+
`.trim();
|
|
592
|
+
|
|
593
|
+
const result = service.extractCodeUnit(code, 'withTemplateBraces', 'typescript');
|
|
594
|
+
|
|
595
|
+
expect(result).toBeDefined();
|
|
596
|
+
expect(result?.endLine).toBe(4);
|
|
597
|
+
expect(result?.fullContent).toContain('return json');
|
|
554
598
|
});
|
|
555
599
|
});
|
|
556
600
|
|
|
@@ -43,17 +43,83 @@ export class CodeUnitService {
|
|
|
43
43
|
|
|
44
44
|
if (startLine === -1) return undefined;
|
|
45
45
|
|
|
46
|
-
// Find end line
|
|
46
|
+
// Find end line using state machine that tracks strings and comments
|
|
47
47
|
let endLine = startLine;
|
|
48
48
|
let braceCount = 0;
|
|
49
49
|
let foundFirstBrace = false;
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
|
|
51
|
+
// State machine for tracking context
|
|
52
|
+
let inSingleQuote = false;
|
|
53
|
+
let inDoubleQuote = false;
|
|
54
|
+
let inTemplateLiteral = false;
|
|
55
|
+
let inMultiLineComment = false;
|
|
56
|
+
|
|
53
57
|
for (let i = startLine - 1; i < lines.length; i++) {
|
|
54
58
|
const line = lines[i] ?? '';
|
|
59
|
+
let inSingleLineComment = false;
|
|
60
|
+
|
|
61
|
+
for (let j = 0; j < line.length; j++) {
|
|
62
|
+
const char = line[j];
|
|
63
|
+
const prevChar = j > 0 ? line[j - 1] : '';
|
|
64
|
+
const nextChar = j < line.length - 1 ? line[j + 1] : '';
|
|
65
|
+
|
|
66
|
+
// Skip escaped characters within strings
|
|
67
|
+
if (prevChar === '\\' && (inSingleQuote || inDoubleQuote || inTemplateLiteral)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Inside multi-line comment - only look for end marker
|
|
72
|
+
if (inMultiLineComment) {
|
|
73
|
+
if (char === '*' && nextChar === '/') {
|
|
74
|
+
inMultiLineComment = false;
|
|
75
|
+
j++; // Skip the /
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Inside single-line comment - skip rest of line
|
|
81
|
+
if (inSingleLineComment) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Inside a string - only look for closing delimiter
|
|
86
|
+
if (inSingleQuote) {
|
|
87
|
+
if (char === "'") inSingleQuote = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (inDoubleQuote) {
|
|
91
|
+
if (char === '"') inDoubleQuote = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (inTemplateLiteral) {
|
|
95
|
+
if (char === '`') inTemplateLiteral = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Not inside any special context - check for context starters
|
|
100
|
+
if (char === '/' && nextChar === '*') {
|
|
101
|
+
inMultiLineComment = true;
|
|
102
|
+
j++; // Skip the *
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (char === '/' && nextChar === '/') {
|
|
106
|
+
inSingleLineComment = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === "'") {
|
|
110
|
+
inSingleQuote = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (char === '"') {
|
|
114
|
+
inDoubleQuote = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === '`') {
|
|
118
|
+
inTemplateLiteral = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
55
121
|
|
|
56
|
-
|
|
122
|
+
// Count braces (we're not inside any string or comment)
|
|
57
123
|
if (char === '{') {
|
|
58
124
|
braceCount++;
|
|
59
125
|
foundFirstBrace = true;
|
|
@@ -80,19 +146,22 @@ export class CodeUnitService {
|
|
|
80
146
|
fullContent,
|
|
81
147
|
startLine,
|
|
82
148
|
endLine,
|
|
83
|
-
language
|
|
149
|
+
language,
|
|
84
150
|
};
|
|
85
151
|
}
|
|
86
152
|
|
|
87
153
|
private extractSignature(line: string, name: string, type: string): string {
|
|
88
154
|
// Remove 'export', 'async', trim whitespace
|
|
89
|
-
const sig = line
|
|
155
|
+
const sig = line
|
|
156
|
+
.replace(/^\s*export\s+/, '')
|
|
157
|
+
.replace(/^\s*async\s+/, '')
|
|
158
|
+
.trim();
|
|
90
159
|
|
|
91
160
|
if (type === 'function') {
|
|
92
161
|
// Extract just "functionName(params): returnType"
|
|
93
|
-
//
|
|
94
|
-
const match = sig.match(/function\s+(\w+\([^)]*\):\s
|
|
95
|
-
if (match?.[1] !== undefined && match[1].length > 0) return match[1];
|
|
162
|
+
// Supports: simple types, generics (Promise<T>), arrays (T[]), unions (T | null)
|
|
163
|
+
const match = sig.match(/function\s+(\w+\([^)]*\):\s*[\w<>[\],\s|]+)/);
|
|
164
|
+
if (match?.[1] !== undefined && match[1].length > 0) return match[1].trim();
|
|
96
165
|
}
|
|
97
166
|
|
|
98
167
|
if (type === 'class') {
|
|
@@ -103,8 +172,13 @@ export class CodeUnitService {
|
|
|
103
172
|
// For arrow functions, extract the variable declaration part
|
|
104
173
|
// Example: const myFunc = (param: string): void => ...
|
|
105
174
|
// Returns: const myFunc = (param: string): void
|
|
106
|
-
const arrowMatch = sig.match(
|
|
107
|
-
|
|
175
|
+
const arrowMatch = sig.match(
|
|
176
|
+
new RegExp(
|
|
177
|
+
`((?:const|let|var)\\s+${name}\\s*=\\s*(?:async\\s+)?\\([^)]*\\)(?::\\s*[^=]+)?)`
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
const matchedSig = arrowMatch?.[1];
|
|
181
|
+
if (matchedSig !== undefined && matchedSig !== '') return matchedSig.trim();
|
|
108
182
|
|
|
109
183
|
// Fallback for simple arrow functions without params
|
|
110
184
|
return `const ${name}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { ConfigService } from './config.service.js';
|
|
3
|
-
import { rm, mkdtemp } from 'node:fs/promises';
|
|
3
|
+
import { rm, mkdtemp, writeFile, access } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
|
|
@@ -42,10 +42,7 @@ describe('ConfigService', () => {
|
|
|
42
42
|
|
|
43
43
|
describe('path expansion', () => {
|
|
44
44
|
it('expands tilde to home directory', () => {
|
|
45
|
-
const service = new ConfigService(
|
|
46
|
-
configPath,
|
|
47
|
-
'~/.bluera/data'
|
|
48
|
-
);
|
|
45
|
+
const service = new ConfigService(configPath, '~/.bluera/data');
|
|
49
46
|
const dataDir = service.resolveDataDir();
|
|
50
47
|
// Note: expandPath is called in constructor, so ~ will be kept
|
|
51
48
|
// since dataDir parameter is explicitly provided
|
|
@@ -53,11 +50,7 @@ describe('ConfigService', () => {
|
|
|
53
50
|
});
|
|
54
51
|
|
|
55
52
|
it('resolves relative paths against project root', () => {
|
|
56
|
-
const service = new ConfigService(
|
|
57
|
-
configPath,
|
|
58
|
-
undefined,
|
|
59
|
-
tempDir
|
|
60
|
-
);
|
|
53
|
+
const service = new ConfigService(configPath, undefined, tempDir);
|
|
61
54
|
const dataDir = service.resolveDataDir();
|
|
62
55
|
// When dataDir is undefined, it uses DEFAULT_CONFIG.dataDir which is relative
|
|
63
56
|
expect(dataDir).toContain(tempDir);
|
|
@@ -65,20 +58,14 @@ describe('ConfigService', () => {
|
|
|
65
58
|
|
|
66
59
|
it('keeps absolute paths as-is', () => {
|
|
67
60
|
const absolutePath = '/absolute/path/to/data';
|
|
68
|
-
const service = new ConfigService(
|
|
69
|
-
configPath,
|
|
70
|
-
absolutePath
|
|
71
|
-
);
|
|
61
|
+
const service = new ConfigService(configPath, absolutePath);
|
|
72
62
|
const dataDir = service.resolveDataDir();
|
|
73
63
|
expect(dataDir).toBe(absolutePath);
|
|
74
64
|
});
|
|
75
65
|
|
|
76
66
|
it('uses explicit dataDir when provided', () => {
|
|
77
67
|
const explicitDir = '/explicit/data/dir';
|
|
78
|
-
const service = new ConfigService(
|
|
79
|
-
configPath,
|
|
80
|
-
explicitDir
|
|
81
|
-
);
|
|
68
|
+
const service = new ConfigService(configPath, explicitDir);
|
|
82
69
|
const dataDir = service.resolveDataDir();
|
|
83
70
|
expect(dataDir).toBe(explicitDir);
|
|
84
71
|
});
|
|
@@ -124,4 +111,36 @@ describe('ConfigService', () => {
|
|
|
124
111
|
expect(loaded).toBeDefined();
|
|
125
112
|
});
|
|
126
113
|
});
|
|
114
|
+
|
|
115
|
+
describe('first-run vs corruption handling (CLAUDE.md compliance)', () => {
|
|
116
|
+
it('creates config file on first run when missing', async () => {
|
|
117
|
+
// Config file does not exist
|
|
118
|
+
const config = await configService.load();
|
|
119
|
+
|
|
120
|
+
// Should return default config
|
|
121
|
+
expect(config.version).toBe(1);
|
|
122
|
+
|
|
123
|
+
// File should now exist (created automatically)
|
|
124
|
+
await expect(access(configPath)).resolves.toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('throws on corrupted config file', async () => {
|
|
128
|
+
// Create corrupted config file
|
|
129
|
+
await writeFile(configPath, '{invalid json syntax');
|
|
130
|
+
|
|
131
|
+
// Create fresh service (no cache)
|
|
132
|
+
const freshService = new ConfigService(configPath, tempDir);
|
|
133
|
+
|
|
134
|
+
// Should throw per CLAUDE.md "fail early and fast"
|
|
135
|
+
await expect(freshService.load()).rejects.toThrow();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('throws with descriptive message on JSON parse error', async () => {
|
|
139
|
+
await writeFile(configPath, '{"incomplete":');
|
|
140
|
+
|
|
141
|
+
const freshService = new ConfigService(configPath, tempDir);
|
|
142
|
+
|
|
143
|
+
await expect(freshService.load()).rejects.toThrow(/JSON|parse|config/i);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
127
146
|
});
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
1
|
+
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
3
2
|
import { homedir } from 'node:os';
|
|
4
|
-
import
|
|
5
|
-
import { DEFAULT_CONFIG } from '../types/config.js';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
6
4
|
import { ProjectRootService } from './project-root.service.js';
|
|
5
|
+
import { DEFAULT_CONFIG } from '../types/config.js';
|
|
6
|
+
import type { AppConfig } from '../types/config.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a file exists
|
|
10
|
+
*/
|
|
11
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
await access(path);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
7
19
|
|
|
8
20
|
export class ConfigService {
|
|
9
21
|
private readonly configPath: string;
|
|
@@ -33,12 +45,23 @@ export class ConfigService {
|
|
|
33
45
|
return this.config;
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
const exists = await fileExists(this.configPath);
|
|
49
|
+
if (!exists) {
|
|
50
|
+
// First run - create config file with defaults
|
|
51
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
52
|
+
await this.save(this.config);
|
|
53
|
+
return this.config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// File exists - load it (throws on corruption per CLAUDE.md "fail early")
|
|
57
|
+
const content = await readFile(this.configPath, 'utf-8');
|
|
36
58
|
try {
|
|
37
|
-
const content = await readFile(this.configPath, 'utf-8');
|
|
38
59
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
39
60
|
this.config = { ...DEFAULT_CONFIG, ...JSON.parse(content) } as AppConfig;
|
|
40
|
-
} catch {
|
|
41
|
-
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Failed to parse config file at ${this.configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
64
|
+
);
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
return this.config;
|