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.
Files changed (200) hide show
  1. package/.claude/commands/code-review.md +15 -0
  2. package/.claude/hooks/post-edit-check.sh +5 -3
  3. package/.claude/skills/atomic-commits/SKILL.md +3 -1
  4. package/.claude/skills/code-review-repo/skill.md +62 -0
  5. package/.husky/pre-commit +3 -2
  6. package/.prettierrc +9 -0
  7. package/.versionrc.json +1 -1
  8. package/CHANGELOG.md +35 -0
  9. package/CLAUDE.md +6 -0
  10. package/README.md +25 -13
  11. package/bun.lock +277 -33
  12. package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
  13. package/dist/chunk-6FHWC36B.js.map +1 -0
  14. package/dist/{chunk-2SJHNRXD.js → chunk-DC7CGSGT.js} +288 -241
  15. package/dist/chunk-DC7CGSGT.js.map +1 -0
  16. package/dist/{chunk-RWSXP3PQ.js → chunk-WFNPNAAP.js} +3194 -3024
  17. package/dist/chunk-WFNPNAAP.js.map +1 -0
  18. package/dist/{chunk-OGEY66FZ.js → chunk-Z2KKVH45.js} +548 -482
  19. package/dist/chunk-Z2KKVH45.js.map +1 -0
  20. package/dist/index.js +871 -754
  21. package/dist/index.js.map +1 -1
  22. package/dist/mcp/server.js +3 -3
  23. package/dist/watch.service-BJV3TI3F.js +7 -0
  24. package/dist/workers/background-worker-cli.js +46 -45
  25. package/dist/workers/background-worker-cli.js.map +1 -1
  26. package/eslint.config.js +43 -1
  27. package/package.json +18 -11
  28. package/plugin.json +8 -0
  29. package/python/requirements.txt +1 -1
  30. package/src/analysis/ast-parser.test.ts +12 -11
  31. package/src/analysis/ast-parser.ts +28 -22
  32. package/src/analysis/code-graph.test.ts +52 -62
  33. package/src/analysis/code-graph.ts +9 -13
  34. package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
  35. package/src/analysis/dependency-usage-analyzer.ts +52 -24
  36. package/src/analysis/go-ast-parser.test.ts +22 -22
  37. package/src/analysis/go-ast-parser.ts +18 -25
  38. package/src/analysis/parser-factory.test.ts +9 -9
  39. package/src/analysis/parser-factory.ts +3 -3
  40. package/src/analysis/python-ast-parser.test.ts +27 -27
  41. package/src/analysis/python-ast-parser.ts +2 -2
  42. package/src/analysis/repo-url-resolver.test.ts +82 -82
  43. package/src/analysis/rust-ast-parser.test.ts +19 -19
  44. package/src/analysis/rust-ast-parser.ts +17 -27
  45. package/src/analysis/tree-sitter-parser.test.ts +3 -3
  46. package/src/analysis/tree-sitter-parser.ts +10 -16
  47. package/src/cli/commands/crawl.test.ts +40 -24
  48. package/src/cli/commands/crawl.ts +186 -161
  49. package/src/cli/commands/index-cmd.test.ts +90 -90
  50. package/src/cli/commands/index-cmd.ts +52 -36
  51. package/src/cli/commands/mcp.test.ts +6 -6
  52. package/src/cli/commands/mcp.ts +2 -2
  53. package/src/cli/commands/plugin-api.test.ts +16 -18
  54. package/src/cli/commands/plugin-api.ts +9 -6
  55. package/src/cli/commands/search.test.ts +16 -7
  56. package/src/cli/commands/search.ts +124 -87
  57. package/src/cli/commands/serve.test.ts +67 -25
  58. package/src/cli/commands/serve.ts +18 -3
  59. package/src/cli/commands/setup.test.ts +176 -101
  60. package/src/cli/commands/setup.ts +140 -117
  61. package/src/cli/commands/store.test.ts +82 -53
  62. package/src/cli/commands/store.ts +56 -37
  63. package/src/cli/program.ts +2 -2
  64. package/src/crawl/article-converter.test.ts +4 -1
  65. package/src/crawl/article-converter.ts +46 -31
  66. package/src/crawl/bridge.test.ts +240 -132
  67. package/src/crawl/bridge.ts +87 -30
  68. package/src/crawl/claude-client.test.ts +124 -56
  69. package/src/crawl/claude-client.ts +7 -15
  70. package/src/crawl/intelligent-crawler.test.ts +65 -22
  71. package/src/crawl/intelligent-crawler.ts +86 -53
  72. package/src/crawl/markdown-utils.ts +1 -4
  73. package/src/db/embeddings.ts +4 -6
  74. package/src/db/lance.test.ts +63 -4
  75. package/src/db/lance.ts +31 -12
  76. package/src/index.ts +26 -17
  77. package/src/logging/index.ts +1 -5
  78. package/src/logging/logger.ts +3 -5
  79. package/src/logging/payload.test.ts +1 -1
  80. package/src/logging/payload.ts +3 -5
  81. package/src/mcp/commands/index.ts +2 -2
  82. package/src/mcp/commands/job.commands.ts +12 -18
  83. package/src/mcp/commands/meta.commands.ts +13 -13
  84. package/src/mcp/commands/registry.ts +5 -8
  85. package/src/mcp/commands/store.commands.ts +19 -19
  86. package/src/mcp/handlers/execute.handler.test.ts +10 -10
  87. package/src/mcp/handlers/execute.handler.ts +4 -5
  88. package/src/mcp/handlers/index.ts +10 -14
  89. package/src/mcp/handlers/job.handler.test.ts +10 -10
  90. package/src/mcp/handlers/job.handler.ts +22 -25
  91. package/src/mcp/handlers/search.handler.test.ts +36 -65
  92. package/src/mcp/handlers/search.handler.ts +135 -104
  93. package/src/mcp/handlers/store.handler.test.ts +41 -52
  94. package/src/mcp/handlers/store.handler.ts +108 -88
  95. package/src/mcp/schemas/index.test.ts +73 -68
  96. package/src/mcp/schemas/index.ts +18 -12
  97. package/src/mcp/server.test.ts +1 -1
  98. package/src/mcp/server.ts +59 -46
  99. package/src/plugin/commands.test.ts +230 -95
  100. package/src/plugin/commands.ts +24 -25
  101. package/src/plugin/dependency-analyzer.test.ts +52 -52
  102. package/src/plugin/dependency-analyzer.ts +85 -22
  103. package/src/plugin/git-clone.test.ts +24 -13
  104. package/src/plugin/git-clone.ts +3 -7
  105. package/src/server/app.test.ts +109 -109
  106. package/src/server/app.ts +32 -23
  107. package/src/server/index.test.ts +64 -66
  108. package/src/services/chunking.service.test.ts +32 -32
  109. package/src/services/chunking.service.ts +16 -9
  110. package/src/services/code-graph.service.test.ts +30 -36
  111. package/src/services/code-graph.service.ts +24 -10
  112. package/src/services/code-unit.service.test.ts +55 -11
  113. package/src/services/code-unit.service.ts +85 -11
  114. package/src/services/config.service.test.ts +37 -18
  115. package/src/services/config.service.ts +30 -7
  116. package/src/services/index.service.test.ts +49 -18
  117. package/src/services/index.service.ts +98 -48
  118. package/src/services/index.ts +8 -10
  119. package/src/services/job.service.test.ts +22 -22
  120. package/src/services/job.service.ts +18 -18
  121. package/src/services/project-root.service.test.ts +1 -3
  122. package/src/services/search.service.test.ts +248 -120
  123. package/src/services/search.service.ts +286 -156
  124. package/src/services/services.test.ts +36 -0
  125. package/src/services/snippet.service.test.ts +14 -6
  126. package/src/services/snippet.service.ts +7 -5
  127. package/src/services/store.service.test.ts +68 -29
  128. package/src/services/store.service.ts +41 -12
  129. package/src/services/watch.service.test.ts +34 -14
  130. package/src/services/watch.service.ts +11 -1
  131. package/src/types/brands.test.ts +3 -1
  132. package/src/types/index.ts +2 -13
  133. package/src/types/search.ts +10 -8
  134. package/src/utils/type-guards.test.ts +20 -15
  135. package/src/utils/type-guards.ts +1 -1
  136. package/src/workers/background-worker-cli.ts +2 -2
  137. package/src/workers/background-worker.test.ts +54 -40
  138. package/src/workers/background-worker.ts +76 -60
  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 +5 -4
  186. package/tests/integration/e2e-workflow.test.ts +2 -0
  187. package/tests/integration/python-bridge.test.ts +13 -3
  188. package/tests/mcp/server.test.ts +1 -1
  189. package/tests/services/code-unit.service.test.ts +48 -0
  190. package/tests/services/job.service.test.ts +124 -0
  191. package/tests/services/search.progressive-context.test.ts +2 -2
  192. package/.claude-plugin/plugin.json +0 -13
  193. package/BUGS-FOUND.md +0 -71
  194. package/dist/chunk-2SJHNRXD.js.map +0 -1
  195. package/dist/chunk-L2YVNC63.js.map +0 -1
  196. package/dist/chunk-OGEY66FZ.js.map +0 -1
  197. package/dist/chunk-RWSXP3PQ.js.map +0 -1
  198. package/dist/watch.service-YAIKKDCF.js +0 -7
  199. package/skills/atomic-commits/SKILL.md +0 -77
  200. /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 { RustASTParser } from '../analysis/rust-ast-parser.js';
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(graph: CodeGraph, filePath: string, symbolName: string): { calledBy: number; calls: number } {
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(graph: CodeGraph, filePath: string, symbolName: string): Array<{ id: string; relationship: string }> {
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(type: string): type is 'function' | 'class' | 'interface' | 'type' | 'const' | 'method' {
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(type: string): 'function' | 'class' | 'interface' | 'type' | 'const' | 'method' | undefined {
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(type: string): 'calls' | 'imports' | 'extends' | 'implements' | undefined {
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 function with braces in string literals', () => {
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
- // The brace counting may be incorrect due to string literal braces
114
- // but it should still complete the extraction
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 brace in comments (limitation - not supported)', () => {
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 template literals with braces (limitation)', () => {
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 \${var} end\`;
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?.fullContent).toContain('template');
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 (naive: next empty line or next top-level declaration)
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
- // NOTE: This brace counting does not handle braces inside strings or comments.
52
- // It may incorrectly determine boundaries if code contains braces in string literals.
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
- for (const char of line) {
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.replace(/^\s*export\s+/, '').replace(/^\s*async\s+/, '').trim();
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
- // TODO: This regex is limited and may not handle complex return types, generics, or multiline signatures
94
- const match = sig.match(/function\s+(\w+\([^)]*\):\s*\w+)/);
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(new RegExp(`((?:const|let|var)\\s+${name}\\s*=\\s*(?:async\\s+)?\\([^)]*\\)(?::\\s*[^=]+)?)`));
107
- if (arrowMatch?.[1] != null && arrowMatch[1] !== '') return arrowMatch[1].trim();
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 type { AppConfig } from '../types/config.js';
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
- this.config = { ...DEFAULT_CONFIG };
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;