@timmeck/brain 1.8.5 → 2.0.0

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 (221) hide show
  1. package/.dockerignore +11 -0
  2. package/Dockerfile +21 -0
  3. package/README.md +19 -0
  4. package/assets/brain-avatar-256.png +0 -0
  5. package/assets/brain-avatar-512.png +0 -0
  6. package/assets/brain-avatar.png +0 -0
  7. package/brain.log +1164 -0
  8. package/{src/cli/commands/dashboard.ts → dashboard.html} +688 -807
  9. package/dist/api/server.d.ts +4 -18
  10. package/dist/api/server.js +4 -173
  11. package/dist/api/server.js.map +1 -1
  12. package/dist/brain.d.ts +6 -0
  13. package/dist/brain.js +56 -4
  14. package/dist/brain.js.map +1 -1
  15. package/dist/cli/colors.d.ts +4 -25
  16. package/dist/cli/colors.js +3 -89
  17. package/dist/cli/colors.js.map +1 -1
  18. package/dist/cli/commands/peers.d.ts +2 -0
  19. package/dist/cli/commands/peers.js +38 -0
  20. package/dist/cli/commands/peers.js.map +1 -0
  21. package/dist/cli/commands/start.js +54 -17
  22. package/dist/cli/commands/start.js.map +1 -1
  23. package/dist/db/connection.d.ts +1 -2
  24. package/dist/db/connection.js +1 -18
  25. package/dist/db/connection.js.map +1 -1
  26. package/dist/index.js +3 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/ipc/__tests__/protocol.test.d.ts +1 -0
  29. package/dist/ipc/__tests__/protocol.test.js +117 -0
  30. package/dist/ipc/__tests__/protocol.test.js.map +1 -0
  31. package/dist/ipc/client.d.ts +1 -16
  32. package/dist/ipc/client.js +1 -100
  33. package/dist/ipc/client.js.map +1 -1
  34. package/dist/ipc/protocol.d.ts +1 -8
  35. package/dist/ipc/protocol.js +1 -28
  36. package/dist/ipc/protocol.js.map +1 -1
  37. package/dist/ipc/router.js +8 -0
  38. package/dist/ipc/router.js.map +1 -1
  39. package/dist/ipc/server.d.ts +1 -22
  40. package/dist/ipc/server.js +1 -163
  41. package/dist/ipc/server.js.map +1 -1
  42. package/dist/mcp/http-server.d.ts +1 -7
  43. package/dist/mcp/http-server.js +6 -117
  44. package/dist/mcp/http-server.js.map +1 -1
  45. package/dist/mcp/server.js +5 -61
  46. package/dist/mcp/server.js.map +1 -1
  47. package/dist/signals/__tests__/fingerprint.test.d.ts +1 -0
  48. package/dist/signals/__tests__/fingerprint.test.js +118 -0
  49. package/dist/signals/__tests__/fingerprint.test.js.map +1 -0
  50. package/dist/types/ipc.types.d.ts +1 -11
  51. package/dist/utils/__tests__/hash.test.d.ts +1 -0
  52. package/dist/utils/__tests__/hash.test.js +32 -0
  53. package/dist/utils/__tests__/hash.test.js.map +1 -0
  54. package/dist/utils/__tests__/paths.test.d.ts +1 -0
  55. package/dist/utils/__tests__/paths.test.js +75 -0
  56. package/dist/utils/__tests__/paths.test.js.map +1 -0
  57. package/dist/utils/events.d.ts +4 -8
  58. package/dist/utils/events.js +2 -14
  59. package/dist/utils/events.js.map +1 -1
  60. package/dist/utils/hash.d.ts +1 -1
  61. package/dist/utils/hash.js +1 -4
  62. package/dist/utils/hash.js.map +1 -1
  63. package/dist/utils/logger.d.ts +3 -2
  64. package/dist/utils/logger.js +8 -35
  65. package/dist/utils/logger.js.map +1 -1
  66. package/dist/utils/paths.d.ts +2 -1
  67. package/dist/utils/paths.js +4 -13
  68. package/dist/utils/paths.js.map +1 -1
  69. package/gen_avatar.py +142 -0
  70. package/package.json +2 -1
  71. package/BRAIN_PLAN.md +0 -3324
  72. package/src/api/server.ts +0 -395
  73. package/src/brain.ts +0 -266
  74. package/src/cli/colors.ts +0 -116
  75. package/src/cli/commands/config.ts +0 -169
  76. package/src/cli/commands/doctor.ts +0 -124
  77. package/src/cli/commands/explain.ts +0 -83
  78. package/src/cli/commands/export.ts +0 -31
  79. package/src/cli/commands/import.ts +0 -199
  80. package/src/cli/commands/insights.ts +0 -65
  81. package/src/cli/commands/learn.ts +0 -24
  82. package/src/cli/commands/modules.ts +0 -53
  83. package/src/cli/commands/network.ts +0 -67
  84. package/src/cli/commands/projects.ts +0 -42
  85. package/src/cli/commands/query.ts +0 -120
  86. package/src/cli/commands/start.ts +0 -62
  87. package/src/cli/commands/status.ts +0 -75
  88. package/src/cli/commands/stop.ts +0 -34
  89. package/src/cli/ipc-helper.ts +0 -22
  90. package/src/cli/update-check.ts +0 -63
  91. package/src/code/analyzer.ts +0 -117
  92. package/src/code/fingerprint.ts +0 -87
  93. package/src/code/matcher.ts +0 -129
  94. package/src/code/parsers/generic.ts +0 -29
  95. package/src/code/parsers/python.ts +0 -54
  96. package/src/code/parsers/typescript.ts +0 -65
  97. package/src/code/registry.ts +0 -60
  98. package/src/code/scorer.ts +0 -120
  99. package/src/config.ts +0 -135
  100. package/src/dashboard/server.ts +0 -142
  101. package/src/db/connection.ts +0 -22
  102. package/src/db/migrations/001_core_schema.ts +0 -120
  103. package/src/db/migrations/002_learning_schema.ts +0 -38
  104. package/src/db/migrations/003_code_schema.ts +0 -53
  105. package/src/db/migrations/004_synapses_schema.ts +0 -57
  106. package/src/db/migrations/005_fts_indexes.ts +0 -78
  107. package/src/db/migrations/006_synapses_phase3.ts +0 -17
  108. package/src/db/migrations/007_feedback.ts +0 -13
  109. package/src/db/migrations/008_git_integration.ts +0 -38
  110. package/src/db/migrations/009_embeddings.ts +0 -8
  111. package/src/db/migrations/index.ts +0 -70
  112. package/src/db/repositories/antipattern.repository.ts +0 -66
  113. package/src/db/repositories/code-module.repository.ts +0 -142
  114. package/src/db/repositories/error.repository.ts +0 -189
  115. package/src/db/repositories/insight.repository.ts +0 -99
  116. package/src/db/repositories/notification.repository.ts +0 -66
  117. package/src/db/repositories/project.repository.ts +0 -93
  118. package/src/db/repositories/rule.repository.ts +0 -108
  119. package/src/db/repositories/solution.repository.ts +0 -154
  120. package/src/db/repositories/synapse.repository.ts +0 -163
  121. package/src/db/repositories/terminal.repository.ts +0 -101
  122. package/src/embeddings/engine.ts +0 -238
  123. package/src/hooks/post-tool-use.ts +0 -92
  124. package/src/hooks/post-write.ts +0 -129
  125. package/src/index.ts +0 -63
  126. package/src/ipc/client.ts +0 -118
  127. package/src/ipc/protocol.ts +0 -35
  128. package/src/ipc/router.ts +0 -133
  129. package/src/ipc/server.ts +0 -176
  130. package/src/learning/confidence-scorer.ts +0 -80
  131. package/src/learning/decay.ts +0 -46
  132. package/src/learning/learning-engine.ts +0 -170
  133. package/src/learning/pattern-extractor.ts +0 -90
  134. package/src/learning/rule-generator.ts +0 -74
  135. package/src/main.rs:10:5 +0 -0
  136. package/src/matching/error-matcher.ts +0 -166
  137. package/src/matching/fingerprint.ts +0 -34
  138. package/src/matching/similarity.ts +0 -61
  139. package/src/matching/tfidf.ts +0 -74
  140. package/src/matching/tokenizer.ts +0 -41
  141. package/src/mcp/auto-detect.ts +0 -93
  142. package/src/mcp/http-server.ts +0 -140
  143. package/src/mcp/server.ts +0 -73
  144. package/src/mcp/tools.ts +0 -328
  145. package/src/parsing/error-parser.ts +0 -28
  146. package/src/parsing/parsers/compiler.ts +0 -93
  147. package/src/parsing/parsers/generic.ts +0 -28
  148. package/src/parsing/parsers/go.ts +0 -97
  149. package/src/parsing/parsers/node.ts +0 -69
  150. package/src/parsing/parsers/python.ts +0 -62
  151. package/src/parsing/parsers/rust.ts +0 -50
  152. package/src/parsing/parsers/shell.ts +0 -42
  153. package/src/parsing/types.ts +0 -47
  154. package/src/research/gap-analyzer.ts +0 -135
  155. package/src/research/insight-generator.ts +0 -123
  156. package/src/research/research-engine.ts +0 -116
  157. package/src/research/synergy-detector.ts +0 -126
  158. package/src/research/template-extractor.ts +0 -130
  159. package/src/research/trend-analyzer.ts +0 -127
  160. package/src/services/analytics.service.ts +0 -226
  161. package/src/services/code.service.ts +0 -271
  162. package/src/services/error.service.ts +0 -266
  163. package/src/services/git.service.ts +0 -132
  164. package/src/services/notification.service.ts +0 -41
  165. package/src/services/prevention.service.ts +0 -159
  166. package/src/services/research.service.ts +0 -98
  167. package/src/services/solution.service.ts +0 -174
  168. package/src/services/synapse.service.ts +0 -59
  169. package/src/services/terminal.service.ts +0 -81
  170. package/src/synapses/activation.ts +0 -80
  171. package/src/synapses/decay.ts +0 -38
  172. package/src/synapses/hebbian.ts +0 -69
  173. package/src/synapses/pathfinder.ts +0 -81
  174. package/src/synapses/synapse-manager.ts +0 -113
  175. package/src/types/code.types.ts +0 -52
  176. package/src/types/config.types.ts +0 -103
  177. package/src/types/error.types.ts +0 -67
  178. package/src/types/ipc.types.ts +0 -8
  179. package/src/types/mcp.types.ts +0 -53
  180. package/src/types/research.types.ts +0 -28
  181. package/src/types/solution.types.ts +0 -30
  182. package/src/types/synapse.types.ts +0 -50
  183. package/src/utils/events.ts +0 -45
  184. package/src/utils/hash.ts +0 -5
  185. package/src/utils/logger.ts +0 -48
  186. package/src/utils/paths.ts +0 -19
  187. package/tests/e2e/test_code_intelligence.py +0 -1015
  188. package/tests/e2e/test_error_memory.py +0 -451
  189. package/tests/e2e/test_full_integration.py +0 -534
  190. package/tests/fixtures/code-modules/modules.ts +0 -83
  191. package/tests/fixtures/errors/go.ts +0 -9
  192. package/tests/fixtures/errors/node.ts +0 -24
  193. package/tests/fixtures/errors/python.ts +0 -21
  194. package/tests/fixtures/errors/rust.ts +0 -25
  195. package/tests/fixtures/errors/shell.ts +0 -15
  196. package/tests/fixtures/solutions/solutions.ts +0 -27
  197. package/tests/helpers/setup-db.ts +0 -52
  198. package/tests/integration/code-flow.test.ts +0 -86
  199. package/tests/integration/error-flow.test.ts +0 -83
  200. package/tests/integration/ipc-flow.test.ts +0 -166
  201. package/tests/integration/learning-cycle.test.ts +0 -82
  202. package/tests/integration/synapse-flow.test.ts +0 -117
  203. package/tests/unit/code/analyzer.test.ts +0 -58
  204. package/tests/unit/code/fingerprint.test.ts +0 -51
  205. package/tests/unit/code/scorer.test.ts +0 -55
  206. package/tests/unit/learning/confidence-scorer.test.ts +0 -60
  207. package/tests/unit/learning/decay.test.ts +0 -45
  208. package/tests/unit/learning/pattern-extractor.test.ts +0 -50
  209. package/tests/unit/matching/error-matcher.test.ts +0 -69
  210. package/tests/unit/matching/fingerprint.test.ts +0 -47
  211. package/tests/unit/matching/similarity.test.ts +0 -65
  212. package/tests/unit/matching/tfidf.test.ts +0 -71
  213. package/tests/unit/matching/tokenizer.test.ts +0 -83
  214. package/tests/unit/parsing/parsers.test.ts +0 -113
  215. package/tests/unit/research/gap-analyzer.test.ts +0 -45
  216. package/tests/unit/research/trend-analyzer.test.ts +0 -45
  217. package/tests/unit/synapses/activation.test.ts +0 -80
  218. package/tests/unit/synapses/decay.test.ts +0 -27
  219. package/tests/unit/synapses/hebbian.test.ts +0 -96
  220. package/tests/unit/synapses/pathfinder.test.ts +0 -72
  221. package/tsconfig.json +0 -18
@@ -1,75 +0,0 @@
1
- import { Command } from 'commander';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { getDataDir } from '../../utils/paths.js';
5
- import { withIpc } from '../ipc-helper.js';
6
- import { c, icons, header, keyValue, divider } from '../colors.js';
7
- import { checkForUpdate, getCurrentVersion } from '../update-check.js';
8
-
9
- export function statusCommand(): Command {
10
- return new Command('status')
11
- .description('Show Brain daemon status')
12
- .action(async () => {
13
- const pidPath = path.join(getDataDir(), 'brain.pid');
14
-
15
- if (!fs.existsSync(pidPath)) {
16
- console.log(`${icons.brain} Brain Daemon: ${c.red.bold('NOT RUNNING')}`);
17
- return;
18
- }
19
-
20
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
21
- let running = false;
22
- try {
23
- process.kill(pid, 0);
24
- running = true;
25
- } catch { /* not running */ }
26
-
27
- if (!running) {
28
- console.log(`${icons.brain} Brain Daemon: ${c.red.bold('NOT RUNNING')} ${c.dim('(stale PID file)')}`);
29
- return;
30
- }
31
-
32
- console.log(header(`Brain Status v${getCurrentVersion()}`, icons.brain));
33
- console.log(` ${c.green(`${icons.dot} RUNNING`)} ${c.dim(`(PID ${pid})`)}`);
34
-
35
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
- await withIpc(async (client) => {
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- const summary: any = await client.request('analytics.summary', {});
39
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
- const network: any = await client.request('synapse.stats', {});
41
-
42
- const dbPath = path.join(getDataDir(), 'brain.db');
43
- let dbSize = '?';
44
- try {
45
- const stat = fs.statSync(dbPath);
46
- dbSize = `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
47
- } catch { /* ignore */ }
48
-
49
- console.log(keyValue('Database', `${dbPath} (${dbSize})`));
50
- console.log();
51
-
52
- console.log(` ${icons.error} ${c.purple.bold('Error Brain')}`);
53
- console.log(` ${c.label('Errors:')} ${c.value(summary.errors?.total ?? 0)} total, ${c.red(summary.errors?.unresolved ?? 0)} unresolved, ${c.dim(`${summary.errors?.last7d ?? 0} last 7d`)}`);
54
- console.log(` ${c.label('Solutions:')} ${c.value(summary.solutions?.total ?? 0)}`);
55
- console.log(` ${c.label('Rules:')} ${c.green(summary.rules?.active ?? 0)} active`);
56
- console.log(` ${c.label('Anti-Pat.:')} ${c.value(summary.antipatterns?.total ?? 0)}`);
57
- console.log();
58
-
59
- console.log(` ${icons.module} ${c.blue.bold('Code Brain')}`);
60
- console.log(` ${c.label('Modules:')} ${c.value(summary.modules?.total ?? 0)} registered`);
61
- console.log();
62
-
63
- console.log(` ${icons.synapse} ${c.cyan.bold('Synapse Network')}`);
64
- console.log(` ${c.label('Synapses:')} ${c.value(network.totalSynapses ?? 0)}`);
65
- console.log(` ${c.label('Avg weight:')} ${c.value((network.avgWeight ?? 0).toFixed(2))}`);
66
- console.log();
67
-
68
- console.log(` ${icons.insight} ${c.orange.bold('Research Brain')}`);
69
- console.log(` ${c.label('Insights:')} ${c.value(summary.insights?.active ?? 0)} active`);
70
-
71
- await checkForUpdate();
72
- console.log(`\n${divider()}`);
73
- });
74
- });
75
- }
@@ -1,34 +0,0 @@
1
- import { Command } from 'commander';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import { getDataDir } from '../../utils/paths.js';
5
- import { c, icons } from '../colors.js';
6
-
7
- export function stopCommand(): Command {
8
- return new Command('stop')
9
- .description('Stop the Brain daemon')
10
- .action(() => {
11
- const pidPath = path.join(getDataDir(), 'brain.pid');
12
-
13
- if (!fs.existsSync(pidPath)) {
14
- console.log(`${icons.brain} ${c.dim('Brain daemon is not running (no PID file found).')}`);
15
- return;
16
- }
17
-
18
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
19
-
20
- try {
21
- process.kill(pid, 'SIGTERM');
22
- console.log(`${icons.brain} ${c.success('Brain daemon stopped')} ${c.dim(`(PID: ${pid})`)}`);
23
- } catch (err) {
24
- if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
25
- console.log(`${icons.brain} ${c.dim('Brain daemon was not running (stale PID file removed).')}`);
26
- } else {
27
- console.error(`${icons.error} ${c.error(`Failed to stop daemon: ${err}`)}`);
28
- }
29
- }
30
-
31
- // Clean up PID file
32
- try { fs.unlinkSync(pidPath); } catch { /* ignore */ }
33
- });
34
- }
@@ -1,22 +0,0 @@
1
- import { IpcClient } from '../ipc/client.js';
2
- import { getPipeName } from '../utils/paths.js';
3
- import { c, icons } from './colors.js';
4
-
5
- export async function withIpc<T>(fn: (client: IpcClient) => Promise<T>): Promise<T> {
6
- const client = new IpcClient(getPipeName(), 5000);
7
- try {
8
- await client.connect();
9
- return await fn(client);
10
- } catch (err) {
11
- if (err instanceof Error && err.message.includes('ENOENT')) {
12
- console.error(`${icons.error} ${c.error('Brain daemon is not running.')} Start it with: ${c.cyan('brain start')}`);
13
- } else if (err instanceof Error && err.message.includes('ECONNREFUSED')) {
14
- console.error(`${icons.error} ${c.error('Brain daemon is not responding.')} Try: ${c.cyan('brain stop && brain start')}`);
15
- } else {
16
- console.error(`${icons.error} ${c.error(err instanceof Error ? err.message : String(err))}`);
17
- }
18
- process.exit(1);
19
- } finally {
20
- client.disconnect();
21
- }
22
- }
@@ -1,63 +0,0 @@
1
- import https from 'node:https';
2
- import { c, icons } from './colors.js';
3
-
4
- // Read current version from package.json at build time
5
- import { createRequire } from 'node:module';
6
- const require = createRequire(import.meta.url);
7
- const pkg = require('../../package.json');
8
- const CURRENT_VERSION: string = pkg.version;
9
-
10
- export function getCurrentVersion(): string {
11
- return CURRENT_VERSION;
12
- }
13
-
14
- function fetchLatestVersion(): Promise<string | null> {
15
- return new Promise((resolve) => {
16
- const timeout = setTimeout(() => resolve(null), 3000);
17
-
18
- const req = https.get(
19
- 'https://registry.npmjs.org/@timmeck/brain/latest',
20
- { headers: { Accept: 'application/json' } },
21
- (res) => {
22
- let data = '';
23
- res.on('data', (chunk) => { data += chunk; });
24
- res.on('end', () => {
25
- clearTimeout(timeout);
26
- try {
27
- const json = JSON.parse(data);
28
- resolve(json.version ?? null);
29
- } catch {
30
- resolve(null);
31
- }
32
- });
33
- },
34
- );
35
- req.on('error', () => {
36
- clearTimeout(timeout);
37
- resolve(null);
38
- });
39
- });
40
- }
41
-
42
- function isNewer(latest: string, current: string): boolean {
43
- const l = latest.split('.').map(Number);
44
- const c = current.split('.').map(Number);
45
- for (let i = 0; i < 3; i++) {
46
- if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
47
- if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
48
- }
49
- return false;
50
- }
51
-
52
- export async function checkForUpdate(): Promise<void> {
53
- try {
54
- const latest = await fetchLatestVersion();
55
- if (latest && isNewer(latest, CURRENT_VERSION)) {
56
- console.log();
57
- console.log(` ${icons.star} ${c.orange.bold(`Update available: v${CURRENT_VERSION} → v${latest}`)}`);
58
- console.log(` Run: ${c.cyan('npm update -g @timmeck/brain')}`);
59
- }
60
- } catch {
61
- // silently ignore — update check is best-effort
62
- }
63
- }
@@ -1,117 +0,0 @@
1
- import type { ExportInfo } from '../types/code.types.js';
2
- import * as tsParser from './parsers/typescript.js';
3
- import * as pyParser from './parsers/python.js';
4
- import * as genericParser from './parsers/generic.js';
5
-
6
- export interface AnalysisResult {
7
- exports: ExportInfo[];
8
- externalDeps: string[];
9
- internalDeps: string[];
10
- isPure: boolean;
11
- hasTypeAnnotations: boolean;
12
- linesOfCode: number;
13
- complexity: number;
14
- }
15
-
16
- const SIDE_EFFECT_PATTERNS = [
17
- 'fs.', 'process.exit', 'process.env', 'console.', 'fetch(',
18
- 'XMLHttpRequest', 'document.', 'window.',
19
- 'global.', 'require(',
20
- ];
21
-
22
- function getParser(language: string) {
23
- switch (language) {
24
- case 'typescript':
25
- case 'javascript':
26
- return tsParser;
27
- case 'python':
28
- return pyParser;
29
- default:
30
- return genericParser;
31
- }
32
- }
33
-
34
- export function analyzeCode(source: string, language: string): AnalysisResult {
35
- const parser = getParser(language);
36
- const exports = parser.extractExports(source);
37
- const { external, internal } = parser.extractImports(source);
38
- const isPure = checkPurity(source);
39
- const typed = parser.hasTypeAnnotations(source);
40
- const linesOfCode = source.split('\n').filter(l => l.trim().length > 0).length;
41
- const complexity = computeCyclomaticComplexity(source, language);
42
-
43
- return {
44
- exports,
45
- externalDeps: external,
46
- internalDeps: internal,
47
- isPure,
48
- hasTypeAnnotations: typed,
49
- linesOfCode,
50
- complexity,
51
- };
52
- }
53
-
54
- /**
55
- * Computes cyclomatic complexity: counts decision points in the code.
56
- * CC = 1 + number of decision points (if, else if, for, while, case, catch, &&, ||, ?:)
57
- */
58
- export function computeCyclomaticComplexity(source: string, language: string): number {
59
- // Remove comments and strings to avoid false positives
60
- const cleaned = source
61
- .replace(/\/\/.*$/gm, '') // single-line comments
62
- .replace(/\/\*[\s\S]*?\*\//g, '') // multi-line comments
63
- .replace(/#.*$/gm, '') // Python comments
64
- .replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, '""'); // strings
65
-
66
- let complexity = 1; // Base complexity
67
-
68
- // Language-agnostic decision point patterns
69
- const patterns = [
70
- /\bif\b/g,
71
- /\belse\s+if\b/g,
72
- /\belif\b/g,
73
- /\bfor\b/g,
74
- /\bwhile\b/g,
75
- /\bcase\b/g,
76
- /\bcatch\b/g,
77
- /\bexcept\b/g,
78
- /&&/g,
79
- /\|\|/g,
80
- /\?\s*[^:]/g, // ternary operator (not type annotations)
81
- ];
82
-
83
- for (const pattern of patterns) {
84
- const matches = cleaned.match(pattern);
85
- if (matches) complexity += matches.length;
86
- }
87
-
88
- return complexity;
89
- }
90
-
91
- export function checkPurity(source: string): boolean {
92
- return !SIDE_EFFECT_PATTERNS.some(p => source.includes(p));
93
- }
94
-
95
- export function measureCohesion(exports: ExportInfo[]): number {
96
- if (exports.length <= 1) return 1.0;
97
-
98
- const names = exports.map(e =>
99
- e.name
100
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
101
- .replace(/([a-z\d])([A-Z])/g, '$1 $2')
102
- .toLowerCase()
103
- .split(/\s+/)
104
- );
105
-
106
- const vocab = new Set<string>();
107
- names.forEach(tokens => tokens.forEach(t => vocab.add(t)));
108
-
109
- let sharedTokens = 0;
110
- for (const token of vocab) {
111
- const count = names.filter(n => n.includes(token)).length;
112
- if (count > 1) sharedTokens += count;
113
- }
114
-
115
- const maxPossible = names.length * vocab.size;
116
- return maxPossible === 0 ? 0 : sharedTokens / maxPossible;
117
- }
@@ -1,87 +0,0 @@
1
- import { sha256 } from '../utils/hash.js';
2
-
3
- export function fingerprintCode(source: string, language: string): string {
4
- let normalized = stripComments(source, language);
5
- normalized = normalized.replace(/\s+/g, ' ').trim();
6
- normalized = normalizeIdentifiers(normalized, language);
7
- normalized = normalized.replace(/'[^']*'/g, "'<STR>'");
8
- normalized = normalized.replace(/"[^"]*"/g, '"<STR>"');
9
- normalized = normalized.replace(/`[^`]*`/g, '`<STR>`');
10
- normalized = normalized.replace(/\b\d+\b/g, '<NUM>');
11
- return sha256(normalized);
12
- }
13
-
14
- export function stripComments(source: string, language: string): string {
15
- switch (language) {
16
- case 'typescript':
17
- case 'javascript':
18
- case 'java':
19
- case 'go':
20
- case 'rust':
21
- case 'c':
22
- case 'cpp':
23
- return source
24
- .replace(/\/\/.*$/gm, '')
25
- .replace(/\/\*[\s\S]*?\*\//g, '');
26
- case 'python':
27
- return source
28
- .replace(/#.*$/gm, '')
29
- .replace(/"""[\s\S]*?"""/g, '')
30
- .replace(/'''[\s\S]*?'''/g, '');
31
- default:
32
- return source
33
- .replace(/\/\/.*$/gm, '')
34
- .replace(/#.*$/gm, '');
35
- }
36
- }
37
-
38
- function normalizeIdentifiers(source: string, language: string): string {
39
- const importNames = extractImportNames(source, language);
40
- const keywords = getLanguageKeywords(language);
41
- const preserve = new Set([...importNames, ...keywords]);
42
-
43
- return source.replace(/\b[a-zA-Z_]\w*\b/g, (match) => {
44
- if (preserve.has(match)) return match;
45
- if (match[0] === match[0]!.toUpperCase() && match[0] !== match[0]!.toLowerCase()) return '<CLASS>';
46
- return '<VAR>';
47
- });
48
- }
49
-
50
- function extractImportNames(source: string, language: string): string[] {
51
- const names: string[] = [];
52
-
53
- if (language === 'typescript' || language === 'javascript') {
54
- const re = /import\s+(?:\{([^}]+)\}|\*\s+as\s+(\w+)|(\w+))/g;
55
- let m: RegExpExecArray | null;
56
- while ((m = re.exec(source)) !== null) {
57
- if (m[1]) names.push(...m[1].split(',').map(s => s.trim().split(/\s+as\s+/).pop()!));
58
- if (m[2]) names.push(m[2]);
59
- if (m[3]) names.push(m[3]);
60
- }
61
- } else if (language === 'python') {
62
- const re = /(?:from\s+\S+\s+)?import\s+(.+)/g;
63
- let m: RegExpExecArray | null;
64
- while ((m = re.exec(source)) !== null) {
65
- names.push(...m[1]!.split(',').map(s => {
66
- const parts = s.trim().split(/\s+as\s+/);
67
- return parts[parts.length - 1]!;
68
- }));
69
- }
70
- }
71
-
72
- return names.filter(Boolean);
73
- }
74
-
75
- function getLanguageKeywords(language: string): string[] {
76
- const common = ['if', 'else', 'for', 'while', 'return', 'break', 'continue', 'switch', 'case', 'default', 'try', 'catch', 'throw', 'new', 'delete', 'true', 'false', 'null', 'undefined', 'void'];
77
-
78
- const langKeywords: Record<string, string[]> = {
79
- typescript: [...common, 'const', 'let', 'var', 'function', 'class', 'interface', 'type', 'enum', 'import', 'export', 'from', 'async', 'await', 'extends', 'implements', 'readonly', 'private', 'public', 'protected', 'static', 'abstract', 'as', 'is', 'in', 'of', 'typeof', 'keyof', 'infer', 'never', 'unknown', 'any', 'string', 'number', 'boolean', 'symbol', 'object'],
80
- javascript: [...common, 'const', 'let', 'var', 'function', 'class', 'import', 'export', 'from', 'async', 'await', 'extends', 'typeof', 'instanceof', 'in', 'of', 'this', 'super', 'yield'],
81
- python: ['def', 'class', 'import', 'from', 'if', 'elif', 'else', 'for', 'while', 'return', 'yield', 'break', 'continue', 'try', 'except', 'finally', 'raise', 'with', 'as', 'pass', 'lambda', 'True', 'False', 'None', 'and', 'or', 'not', 'in', 'is', 'global', 'nonlocal', 'assert', 'async', 'await', 'self'],
82
- rust: [...common, 'fn', 'let', 'mut', 'pub', 'struct', 'enum', 'impl', 'trait', 'use', 'mod', 'crate', 'self', 'super', 'match', 'loop', 'move', 'ref', 'where', 'async', 'await', 'dyn', 'Box', 'Vec', 'String', 'Option', 'Result', 'Some', 'None', 'Ok', 'Err'],
83
- go: [...common, 'func', 'package', 'import', 'type', 'struct', 'interface', 'map', 'chan', 'go', 'defer', 'select', 'range', 'var', 'const', 'nil', 'make', 'len', 'append', 'cap', 'copy', 'close'],
84
- };
85
-
86
- return langKeywords[language] ?? common;
87
- }
@@ -1,129 +0,0 @@
1
- import type { CodeModuleRecord } from '../types/code.types.js';
2
- import { fingerprintCode } from './fingerprint.js';
3
- import { tokenize } from '../matching/tokenizer.js';
4
- import { cosineSimilarity } from '../matching/similarity.js';
5
-
6
- export interface CodeMatchResult {
7
- moduleId: number;
8
- score: number;
9
- matchType: 'exact' | 'structural' | 'semantic' | 'vector';
10
- }
11
-
12
- export function findExactMatches(
13
- fingerprint: string,
14
- candidates: CodeModuleRecord[],
15
- ): CodeMatchResult[] {
16
- return candidates
17
- .filter(c => c.fingerprint === fingerprint)
18
- .map(c => ({ moduleId: c.id, score: 1.0, matchType: 'exact' as const }));
19
- }
20
-
21
- export function findStructuralMatches(
22
- source: string,
23
- language: string,
24
- candidates: CodeModuleRecord[],
25
- threshold: number = 0.75,
26
- ): CodeMatchResult[] {
27
- const fp = fingerprintCode(source, language);
28
- const results: CodeMatchResult[] = [];
29
-
30
- for (const candidate of candidates) {
31
- if (candidate.fingerprint === fp) {
32
- results.push({ moduleId: candidate.id, score: 1.0, matchType: 'structural' });
33
- continue;
34
- }
35
-
36
- const tokensA = tokenize(source);
37
- const tokensB = tokenize(candidate.name + ' ' + (candidate.description ?? ''));
38
- const sim = cosineSimilarity(tokensA, tokensB);
39
- if (sim >= threshold) {
40
- results.push({ moduleId: candidate.id, score: sim, matchType: 'structural' });
41
- }
42
- }
43
-
44
- return results.sort((a, b) => b.score - a.score);
45
- }
46
-
47
- export function findSemanticMatches(
48
- description: string,
49
- candidates: CodeModuleRecord[],
50
- threshold: number = 0.5,
51
- ): CodeMatchResult[] {
52
- const queryTokens = tokenize(description);
53
-
54
- return candidates
55
- .map(c => {
56
- const candidateTokens = tokenize(
57
- [c.name, c.description ?? '', c.file_path].join(' ')
58
- );
59
- const score = cosineSimilarity(queryTokens, candidateTokens);
60
- return { moduleId: c.id, score, matchType: 'semantic' as const };
61
- })
62
- .filter(r => r.score >= threshold)
63
- .sort((a, b) => b.score - a.score);
64
- }
65
-
66
- /**
67
- * Find matches using pre-computed vector embeddings.
68
- * Vector scores are computed externally (by the EmbeddingEngine) and passed in.
69
- */
70
- export function findVectorMatches(
71
- vectorScores: Map<number, number>,
72
- threshold: number = 0.5,
73
- ): CodeMatchResult[] {
74
- const results: CodeMatchResult[] = [];
75
-
76
- for (const [moduleId, score] of vectorScores) {
77
- if (score >= threshold) {
78
- results.push({ moduleId, score, matchType: 'vector' });
79
- }
80
- }
81
-
82
- return results.sort((a, b) => b.score - a.score);
83
- }
84
-
85
- /**
86
- * Hybrid search: combine structural + semantic + vector matches,
87
- * deduplicating and taking the highest score per module.
88
- */
89
- export function findHybridMatches(
90
- source: string,
91
- language: string,
92
- description: string,
93
- candidates: CodeModuleRecord[],
94
- vectorScores?: Map<number, number>,
95
- ): CodeMatchResult[] {
96
- const scoreMap = new Map<number, CodeMatchResult>();
97
-
98
- // Structural matches (highest priority)
99
- for (const match of findStructuralMatches(source, language, candidates, 0.5)) {
100
- const existing = scoreMap.get(match.moduleId);
101
- if (!existing || match.score > existing.score) {
102
- scoreMap.set(match.moduleId, match);
103
- }
104
- }
105
-
106
- // Semantic matches
107
- for (const match of findSemanticMatches(description, candidates, 0.3)) {
108
- const existing = scoreMap.get(match.moduleId);
109
- if (!existing || match.score > existing.score) {
110
- scoreMap.set(match.moduleId, match);
111
- }
112
- }
113
-
114
- // Vector matches (if available)
115
- if (vectorScores && vectorScores.size > 0) {
116
- for (const match of findVectorMatches(vectorScores, 0.4)) {
117
- const existing = scoreMap.get(match.moduleId);
118
- if (!existing) {
119
- scoreMap.set(match.moduleId, match);
120
- } else {
121
- // Boost existing matches that also have high vector similarity
122
- const vectorBoost = match.score * 0.15;
123
- existing.score = Math.min(1.0, existing.score + vectorBoost);
124
- }
125
- }
126
- }
127
-
128
- return [...scoreMap.values()].sort((a, b) => b.score - a.score);
129
- }
@@ -1,29 +0,0 @@
1
- import type { ExportInfo } from '../../types/code.types.js';
2
-
3
- const FUNC_RE = /(?:function|func|fn|def|sub)\s+(\w+)/g;
4
- const CLASS_RE = /(?:class|struct|type)\s+(\w+)/g;
5
-
6
- export function extractExports(source: string): ExportInfo[] {
7
- const exports: ExportInfo[] = [];
8
- let match: RegExpExecArray | null;
9
-
10
- const funcRe = new RegExp(FUNC_RE.source, 'g');
11
- while ((match = funcRe.exec(source)) !== null) {
12
- exports.push({ name: match[1]!, type: 'function' });
13
- }
14
-
15
- const classRe = new RegExp(CLASS_RE.source, 'g');
16
- while ((match = classRe.exec(source)) !== null) {
17
- exports.push({ name: match[1]!, type: 'class' });
18
- }
19
-
20
- return exports;
21
- }
22
-
23
- export function extractImports(_source: string): { external: string[]; internal: string[] } {
24
- return { external: [], internal: [] };
25
- }
26
-
27
- export function hasTypeAnnotations(_source: string): boolean {
28
- return false;
29
- }
@@ -1,54 +0,0 @@
1
- import type { ExportInfo } from '../../types/code.types.js';
2
-
3
- const FUNC_DEF_RE = /^def\s+(\w+)\s*\(/gm;
4
- const CLASS_DEF_RE = /^class\s+(\w+)/gm;
5
- const IMPORT_RE = /^(?:from\s+(\S+)\s+)?import\s+(.+)/gm;
6
- const TOP_LEVEL_ASSIGN_RE = /^([A-Z_][A-Z_\d]*)\s*=/gm;
7
-
8
- export function extractExports(source: string): ExportInfo[] {
9
- const exports: ExportInfo[] = [];
10
- let match: RegExpExecArray | null;
11
-
12
- const funcRe = new RegExp(FUNC_DEF_RE.source, 'gm');
13
- while ((match = funcRe.exec(source)) !== null) {
14
- if (!match[1]!.startsWith('_')) {
15
- exports.push({ name: match[1]!, type: 'function' });
16
- }
17
- }
18
-
19
- const classRe = new RegExp(CLASS_DEF_RE.source, 'gm');
20
- while ((match = classRe.exec(source)) !== null) {
21
- if (!match[1]!.startsWith('_')) {
22
- exports.push({ name: match[1]!, type: 'class' });
23
- }
24
- }
25
-
26
- const constRe = new RegExp(TOP_LEVEL_ASSIGN_RE.source, 'gm');
27
- while ((match = constRe.exec(source)) !== null) {
28
- exports.push({ name: match[1]!, type: 'constant' });
29
- }
30
-
31
- return exports;
32
- }
33
-
34
- export function extractImports(source: string): { external: string[]; internal: string[] } {
35
- const external: string[] = [];
36
- const internal: string[] = [];
37
- let match: RegExpExecArray | null;
38
-
39
- const importRe = new RegExp(IMPORT_RE.source, 'gm');
40
- while ((match = importRe.exec(source)) !== null) {
41
- const module = match[1] ?? match[2]!.trim().split(/\s*,\s*/)[0]!;
42
- if (module.startsWith('.')) {
43
- internal.push(module);
44
- } else {
45
- external.push(module);
46
- }
47
- }
48
-
49
- return { external, internal };
50
- }
51
-
52
- export function hasTypeAnnotations(source: string): boolean {
53
- return /:\s*\w+/.test(source) && /->/.test(source);
54
- }
@@ -1,65 +0,0 @@
1
- import type { ExportInfo } from '../../types/code.types.js';
2
-
3
- const NAMED_EXPORT_RE = /export\s+(?:async\s+)?(?:function|const|let|var|class|interface|type|enum)\s+(\w+)/g;
4
- const DEFAULT_EXPORT_RE = /export\s+default\s+(?:(?:async\s+)?(?:function|class)\s+)?(\w+)?/g;
5
- const IMPORT_RE = /import\s+(?:(?:type\s+)?(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*\s+from\s+)?['"]([^'"]+)['"]/g;
6
-
7
- export function extractExports(source: string): ExportInfo[] {
8
- const exports: ExportInfo[] = [];
9
- let match: RegExpExecArray | null;
10
-
11
- const namedRe = new RegExp(NAMED_EXPORT_RE.source, 'g');
12
- while ((match = namedRe.exec(source)) !== null) {
13
- const line = source.substring(
14
- source.lastIndexOf('\n', match.index) + 1,
15
- source.indexOf('\n', match.index),
16
- );
17
- exports.push({
18
- name: match[1]!,
19
- type: detectExportType(line),
20
- });
21
- }
22
-
23
- const defaultRe = new RegExp(DEFAULT_EXPORT_RE.source, 'g');
24
- while ((match = defaultRe.exec(source)) !== null) {
25
- exports.push({
26
- name: match[1] ?? 'default',
27
- type: 'function',
28
- });
29
- }
30
-
31
- return exports;
32
- }
33
-
34
- export function extractImports(source: string): { external: string[]; internal: string[] } {
35
- const external: string[] = [];
36
- const internal: string[] = [];
37
- let match: RegExpExecArray | null;
38
-
39
- const importRe = new RegExp(IMPORT_RE.source, 'g');
40
- while ((match = importRe.exec(source)) !== null) {
41
- const specifier = match[1]!;
42
- if (specifier.startsWith('.') || specifier.startsWith('/')) {
43
- internal.push(specifier);
44
- } else {
45
- external.push(specifier);
46
- }
47
- }
48
-
49
- return { external, internal };
50
- }
51
-
52
- export function hasTypeAnnotations(source: string): boolean {
53
- return /:\s*\w+[\[\]<>|&]*\s*[=;,)\n{]/.test(source) ||
54
- /interface\s+\w+/.test(source) ||
55
- /type\s+\w+\s*=/.test(source);
56
- }
57
-
58
- function detectExportType(line: string): ExportInfo['type'] {
59
- if (/\bfunction\b/.test(line)) return 'function';
60
- if (/\bclass\b/.test(line)) return 'class';
61
- if (/\binterface\b/.test(line)) return 'interface';
62
- if (/\btype\b/.test(line)) return 'type';
63
- if (/\bconst\b/.test(line)) return 'constant';
64
- return 'variable';
65
- }