clast-ai 0.1.2 → 0.2.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 (290) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +11 -9
  3. package/README.md +447 -401
  4. package/bench/ab/onair-concept.json +22 -0
  5. package/bench/ab/onair-deadcode.json +7 -0
  6. package/bench/ab/onair-hard.json +32 -0
  7. package/bench/ab/onair-impact.json +17 -0
  8. package/bench/ab/onair-refactors.json +18 -0
  9. package/bench/ab/onair-survey.json +17 -0
  10. package/bench/ab/onair-tasks.json +20 -0
  11. package/bench/ab/onair.json +52 -0
  12. package/bench/ab/vscode-concept.json +22 -0
  13. package/bench/ab/vscode-hard.json +32 -0
  14. package/bench/ab/vscode-impact.json +17 -0
  15. package/bench/ab/vscode-refactors.json +18 -0
  16. package/bench/ab/vscode-tasks.json +20 -0
  17. package/bench/ab/vscode.json +52 -0
  18. package/bench/ab.mjs +213 -0
  19. package/bench/bench.mjs +98 -0
  20. package/bench/resolution.mjs +75 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +257 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config/defaults.d.ts +12 -2
  25. package/dist/config/defaults.js +38 -31
  26. package/dist/config/defaults.js.map +1 -1
  27. package/dist/config/index.d.ts +1 -1
  28. package/dist/config/index.js +26 -14
  29. package/dist/config/index.js.map +1 -1
  30. package/dist/db/index.d.ts +73 -13
  31. package/dist/db/index.js +481 -39
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/engine.d.ts +15 -0
  34. package/dist/engine.js +28 -0
  35. package/dist/engine.js.map +1 -0
  36. package/dist/indexer/imports.d.ts +6 -0
  37. package/dist/indexer/imports.js +254 -0
  38. package/dist/indexer/imports.js.map +1 -0
  39. package/dist/indexer/index.d.ts +63 -0
  40. package/dist/indexer/index.js +360 -0
  41. package/dist/indexer/index.js.map +1 -0
  42. package/dist/indexer/process.d.ts +22 -0
  43. package/dist/indexer/process.js +42 -0
  44. package/dist/indexer/process.js.map +1 -0
  45. package/dist/indexer/scan.d.ts +43 -0
  46. package/dist/indexer/scan.js +223 -0
  47. package/dist/indexer/scan.js.map +1 -0
  48. package/dist/indexer/worker.d.ts +1 -0
  49. package/dist/indexer/worker.js +23 -0
  50. package/dist/indexer/worker.js.map +1 -0
  51. package/dist/llm/comment-generator.d.ts +17 -19
  52. package/dist/llm/comment-generator.js +46 -80
  53. package/dist/llm/comment-generator.js.map +1 -1
  54. package/dist/llm/index.d.ts +0 -1
  55. package/dist/llm/index.js.map +1 -1
  56. package/dist/llm/prompts.d.ts +0 -1
  57. package/dist/llm/prompts.js +16 -16
  58. package/dist/parser/extractor.d.ts +21 -16
  59. package/dist/parser/extractor.js +700 -228
  60. package/dist/parser/extractor.js.map +1 -1
  61. package/dist/parser/index.d.ts +17 -21
  62. package/dist/parser/index.js +115 -171
  63. package/dist/parser/index.js.map +1 -1
  64. package/dist/parser/langs/c.d.ts +3 -0
  65. package/dist/parser/langs/c.js +879 -0
  66. package/dist/parser/langs/c.js.map +1 -0
  67. package/dist/parser/langs/csharp.d.ts +2 -0
  68. package/dist/parser/langs/csharp.js +548 -0
  69. package/dist/parser/langs/csharp.js.map +1 -0
  70. package/dist/parser/langs/dart.d.ts +2 -0
  71. package/dist/parser/langs/dart.js +893 -0
  72. package/dist/parser/langs/dart.js.map +1 -0
  73. package/dist/parser/langs/go.d.ts +2 -0
  74. package/dist/parser/langs/go.js +455 -0
  75. package/dist/parser/langs/go.js.map +1 -0
  76. package/dist/parser/langs/java.d.ts +2 -0
  77. package/dist/parser/langs/java.js +390 -0
  78. package/dist/parser/langs/java.js.map +1 -0
  79. package/dist/parser/langs/php.d.ts +2 -0
  80. package/dist/parser/langs/php.js +486 -0
  81. package/dist/parser/langs/php.js.map +1 -0
  82. package/dist/parser/langs/python.d.ts +2 -0
  83. package/dist/parser/langs/python.js +216 -0
  84. package/dist/parser/langs/python.js.map +1 -0
  85. package/dist/parser/langs/ruby.d.ts +2 -0
  86. package/dist/parser/langs/ruby.js +602 -0
  87. package/dist/parser/langs/ruby.js.map +1 -0
  88. package/dist/parser/langs/rust.d.ts +2 -0
  89. package/dist/parser/langs/rust.js +931 -0
  90. package/dist/parser/langs/rust.js.map +1 -0
  91. package/dist/parser/langs/swift.d.ts +2 -0
  92. package/dist/parser/langs/swift.js +616 -0
  93. package/dist/parser/langs/swift.js.map +1 -0
  94. package/dist/parser/langs/typescript.d.ts +4 -0
  95. package/dist/parser/langs/typescript.js +535 -0
  96. package/dist/parser/langs/typescript.js.map +1 -0
  97. package/dist/parser/registry.d.ts +16 -0
  98. package/dist/parser/registry.js +87 -0
  99. package/dist/parser/registry.js.map +1 -0
  100. package/dist/parser/returns.d.ts +9 -0
  101. package/dist/parser/returns.js +152 -0
  102. package/dist/parser/returns.js.map +1 -0
  103. package/dist/parser/spec.d.ts +165 -0
  104. package/dist/parser/spec.js +188 -0
  105. package/dist/parser/spec.js.map +1 -0
  106. package/dist/query/arity.d.ts +27 -0
  107. package/dist/query/arity.js +187 -0
  108. package/dist/query/arity.js.map +1 -0
  109. package/dist/query/callgraph.d.ts +113 -0
  110. package/dist/query/callgraph.js +332 -0
  111. package/dist/query/callgraph.js.map +1 -0
  112. package/dist/query/format.d.ts +36 -0
  113. package/dist/query/format.js +136 -0
  114. package/dist/query/format.js.map +1 -0
  115. package/dist/query/hierarchy.d.ts +44 -0
  116. package/dist/query/hierarchy.js +441 -0
  117. package/dist/query/hierarchy.js.map +1 -0
  118. package/dist/query/resolve.d.ts +265 -0
  119. package/dist/query/resolve.js +1957 -0
  120. package/dist/query/resolve.js.map +1 -0
  121. package/dist/query/search.d.ts +35 -0
  122. package/dist/query/search.js +379 -0
  123. package/dist/query/search.js.map +1 -0
  124. package/dist/query/words.d.ts +30 -0
  125. package/dist/query/words.js +201 -0
  126. package/dist/query/words.js.map +1 -0
  127. package/dist/server/index.d.ts +1 -3
  128. package/dist/server/index.js +313 -73
  129. package/dist/server/index.js.map +1 -1
  130. package/dist/tools/context.d.ts +35 -0
  131. package/dist/tools/context.js +79 -0
  132. package/dist/tools/context.js.map +1 -0
  133. package/dist/tools/explore.d.ts +51 -0
  134. package/dist/tools/explore.js +779 -0
  135. package/dist/tools/explore.js.map +1 -0
  136. package/dist/tools/impact.d.ts +21 -0
  137. package/dist/tools/impact.js +743 -0
  138. package/dist/tools/impact.js.map +1 -0
  139. package/dist/tools/search.d.ts +59 -0
  140. package/dist/tools/search.js +843 -0
  141. package/dist/tools/search.js.map +1 -0
  142. package/dist/tools/stats.d.ts +26 -0
  143. package/dist/tools/stats.js +604 -0
  144. package/dist/tools/stats.js.map +1 -0
  145. package/dist/types.d.ts +95 -40
  146. package/dist/types.js +9 -1
  147. package/dist/types.js.map +1 -1
  148. package/dist/version.d.ts +2 -0
  149. package/dist/version.js +3 -0
  150. package/dist/version.js.map +1 -0
  151. package/dist/watcher/index.d.ts +18 -16
  152. package/dist/watcher/index.js +94 -77
  153. package/dist/watcher/index.js.map +1 -1
  154. package/package.json +21 -24
  155. package/plugin/server.mjs +50 -0
  156. package/skills/audit/SKILL.md +37 -0
  157. package/skills/deadcode/SKILL.md +33 -0
  158. package/skills/explore/SKILL.md +54 -0
  159. package/skills/impact/SKILL.md +24 -0
  160. package/skills/status/SKILL.md +11 -0
  161. package/src/cli.ts +256 -0
  162. package/src/config/defaults.ts +50 -32
  163. package/src/config/index.ts +23 -14
  164. package/src/db/index.ts +540 -43
  165. package/src/engine.ts +30 -0
  166. package/src/indexer/imports.ts +241 -0
  167. package/src/indexer/index.ts +369 -0
  168. package/src/indexer/process.ts +63 -0
  169. package/src/indexer/scan.ts +226 -0
  170. package/src/indexer/worker.ts +24 -0
  171. package/src/llm/comment-generator.ts +52 -103
  172. package/src/llm/index.ts +80 -80
  173. package/src/llm/prompts.ts +95 -95
  174. package/src/parser/extractor.ts +643 -256
  175. package/src/parser/index.ts +105 -205
  176. package/src/parser/langs/c.ts +860 -0
  177. package/src/parser/langs/csharp.ts +519 -0
  178. package/src/parser/langs/dart.ts +820 -0
  179. package/src/parser/langs/go.ts +437 -0
  180. package/src/parser/langs/java.ts +361 -0
  181. package/src/parser/langs/php.ts +450 -0
  182. package/src/parser/langs/python.ts +206 -0
  183. package/src/parser/langs/ruby.ts +554 -0
  184. package/src/parser/langs/rust.ts +904 -0
  185. package/src/parser/langs/swift.ts +590 -0
  186. package/src/parser/langs/typescript.ts +499 -0
  187. package/src/parser/registry.ts +88 -0
  188. package/src/parser/returns.ts +137 -0
  189. package/src/parser/spec.ts +309 -0
  190. package/src/query/arity.ts +173 -0
  191. package/src/query/callgraph.ts +473 -0
  192. package/src/query/format.ts +136 -0
  193. package/src/query/hierarchy.ts +447 -0
  194. package/src/query/resolve.ts +1972 -0
  195. package/src/query/search.ts +430 -0
  196. package/src/query/words.ts +183 -0
  197. package/src/server/index.ts +390 -79
  198. package/src/tools/context.ts +82 -0
  199. package/src/tools/explore.ts +860 -0
  200. package/src/tools/impact.ts +748 -0
  201. package/src/tools/search.ts +886 -0
  202. package/src/tools/stats.ts +633 -0
  203. package/src/types.ts +113 -45
  204. package/src/version.ts +2 -0
  205. package/src/watcher/index.ts +82 -88
  206. package/tsconfig.json +7 -4
  207. package/.mcp.json +0 -8
  208. package/commands/clast-status.md +0 -9
  209. package/dist/config/defaults.d.ts.map +0 -1
  210. package/dist/config/index.d.ts.map +0 -1
  211. package/dist/db/index.d.ts.map +0 -1
  212. package/dist/db/queries.d.ts +0 -44
  213. package/dist/db/queries.d.ts.map +0 -1
  214. package/dist/db/queries.js +0 -251
  215. package/dist/db/queries.js.map +0 -1
  216. package/dist/db/schema.d.ts +0 -3
  217. package/dist/db/schema.d.ts.map +0 -1
  218. package/dist/db/schema.js +0 -94
  219. package/dist/db/schema.js.map +0 -1
  220. package/dist/db/types.d.ts +0 -44
  221. package/dist/db/types.d.ts.map +0 -1
  222. package/dist/db/types.js +0 -2
  223. package/dist/db/types.js.map +0 -1
  224. package/dist/llm/comment-generator.d.ts.map +0 -1
  225. package/dist/llm/index.d.ts.map +0 -1
  226. package/dist/llm/prompts.d.ts.map +0 -1
  227. package/dist/parser/call-resolver.d.ts +0 -15
  228. package/dist/parser/call-resolver.d.ts.map +0 -1
  229. package/dist/parser/call-resolver.js +0 -16
  230. package/dist/parser/call-resolver.js.map +0 -1
  231. package/dist/parser/extractor.d.ts.map +0 -1
  232. package/dist/parser/index.d.ts.map +0 -1
  233. package/dist/parser/languages.d.ts +0 -11
  234. package/dist/parser/languages.d.ts.map +0 -1
  235. package/dist/parser/languages.js +0 -74
  236. package/dist/parser/languages.js.map +0 -1
  237. package/dist/server/index.d.ts.map +0 -1
  238. package/dist/server/register-tools.d.ts +0 -7
  239. package/dist/server/register-tools.d.ts.map +0 -1
  240. package/dist/server/register-tools.js +0 -17
  241. package/dist/server/register-tools.js.map +0 -1
  242. package/dist/server/tools/clast-call-graph.d.ts +0 -4
  243. package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
  244. package/dist/server/tools/clast-call-graph.js +0 -70
  245. package/dist/server/tools/clast-call-graph.js.map +0 -1
  246. package/dist/server/tools/clast-comment.d.ts +0 -5
  247. package/dist/server/tools/clast-comment.d.ts.map +0 -1
  248. package/dist/server/tools/clast-comment.js +0 -64
  249. package/dist/server/tools/clast-comment.js.map +0 -1
  250. package/dist/server/tools/clast-file-summary.d.ts +0 -4
  251. package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
  252. package/dist/server/tools/clast-file-summary.js +0 -42
  253. package/dist/server/tools/clast-file-summary.js.map +0 -1
  254. package/dist/server/tools/clast-get-context.d.ts +0 -4
  255. package/dist/server/tools/clast-get-context.d.ts.map +0 -1
  256. package/dist/server/tools/clast-get-context.js +0 -80
  257. package/dist/server/tools/clast-get-context.js.map +0 -1
  258. package/dist/server/tools/clast-reindex.d.ts +0 -5
  259. package/dist/server/tools/clast-reindex.d.ts.map +0 -1
  260. package/dist/server/tools/clast-reindex.js +0 -36
  261. package/dist/server/tools/clast-reindex.js.map +0 -1
  262. package/dist/server/tools/clast-search.d.ts +0 -4
  263. package/dist/server/tools/clast-search.d.ts.map +0 -1
  264. package/dist/server/tools/clast-search.js +0 -29
  265. package/dist/server/tools/clast-search.js.map +0 -1
  266. package/dist/server/tools/clast-status.d.ts +0 -5
  267. package/dist/server/tools/clast-status.d.ts.map +0 -1
  268. package/dist/server/tools/clast-status.js +0 -23
  269. package/dist/server/tools/clast-status.js.map +0 -1
  270. package/dist/types.d.ts.map +0 -1
  271. package/dist/watcher/gitignore.d.ts +0 -9
  272. package/dist/watcher/gitignore.d.ts.map +0 -1
  273. package/dist/watcher/gitignore.js +0 -38
  274. package/dist/watcher/gitignore.js.map +0 -1
  275. package/dist/watcher/index.d.ts.map +0 -1
  276. package/skills/clast-explore/SKILL.md +0 -41
  277. package/src/db/queries.ts +0 -332
  278. package/src/db/schema.ts +0 -106
  279. package/src/db/types.ts +0 -47
  280. package/src/parser/call-resolver.ts +0 -17
  281. package/src/parser/languages.ts +0 -86
  282. package/src/server/register-tools.ts +0 -30
  283. package/src/server/tools/clast-call-graph.ts +0 -82
  284. package/src/server/tools/clast-comment.ts +0 -76
  285. package/src/server/tools/clast-file-summary.ts +0 -59
  286. package/src/server/tools/clast-get-context.ts +0 -97
  287. package/src/server/tools/clast-reindex.ts +0 -52
  288. package/src/server/tools/clast-search.ts +0 -38
  289. package/src/server/tools/clast-status.ts +0 -37
  290. package/src/watcher/gitignore.ts +0 -42
package/src/db/index.ts CHANGED
@@ -1,63 +1,560 @@
1
- import initSqlJs, { type Database } from 'sql.js';
2
- import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
3
- import { dirname } from 'node:path';
4
- import { initializeSchema } from './schema.js';
5
- import { Queries } from './queries.js';
6
-
7
- export class ClastDatabase {
8
- private db: Database;
9
- private dbPath: string;
10
- private saveTimer: ReturnType<typeof setInterval> | null = null;
11
- public queries: Queries;
12
-
13
- private constructor(db: Database, dbPath: string) {
1
+ import { createRequire } from 'node:module';
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import type { DatabaseSync, StatementSync } from 'node:sqlite';
5
+ import type { Extraction, FileRow, SymbolRow } from '../types.js';
6
+ import { ftsWords, initials, splitIdentifier } from '../query/words.js';
7
+
8
+ /**
9
+ * Bump whenever the schema or the extraction output changes shape. The index
10
+ * is a cache: a version mismatch simply drops and rebuilds it.
11
+ */
12
+ export const SCHEMA_VERSION = 26;
13
+
14
+ /** Kinds that get a full-text row (concept search). Others are found by name. */
15
+ const FTS_KINDS = new Set(['function', 'method', 'class', 'interface', 'struct', 'trait', 'enum', 'type', 'namespace', 'macro']);
16
+
17
+ type SqlValue = string | number | bigint | null | Uint8Array;
18
+
19
+ function loadSqlite(): typeof import('node:sqlite') {
20
+ // node:sqlite prints an ExperimentalWarning on some Node versions; an MCP
21
+ // server should not spam stderr with it.
22
+ const original = process.emitWarning;
23
+ process.emitWarning = ((warning: string | Error, ...rest: unknown[]) => {
24
+ const msg = typeof warning === 'string' ? warning : warning?.message;
25
+ if (msg && msg.includes('SQLite')) return;
26
+ return (original as (...a: unknown[]) => void).call(process, warning, ...rest);
27
+ }) as typeof process.emitWarning;
28
+ try {
29
+ const require = createRequire(import.meta.url);
30
+ return require('node:sqlite') as typeof import('node:sqlite');
31
+ } catch (err) {
32
+ throw new Error(
33
+ `clast requires Node.js >= 22.13 (built-in node:sqlite). Current: ${process.version}. ${String(err)}`
34
+ );
35
+ } finally {
36
+ process.emitWarning = original;
37
+ }
38
+ }
39
+
40
+ function processAlive(pid: number): boolean {
41
+ try {
42
+ process.kill(pid, 0);
43
+ return true;
44
+ } catch (err) {
45
+ // EPERM: the process exists but belongs to someone else.
46
+ return (err as NodeJS.ErrnoException).code === 'EPERM';
47
+ }
48
+ }
49
+
50
+ const SCHEMA = `
51
+ CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL) WITHOUT ROWID;
52
+
53
+ CREATE TABLE files (
54
+ id INTEGER PRIMARY KEY,
55
+ path TEXT NOT NULL UNIQUE,
56
+ hash TEXT NOT NULL,
57
+ mtime REAL NOT NULL,
58
+ size INTEGER NOT NULL,
59
+ lang TEXT NOT NULL,
60
+ line_count INTEGER NOT NULL DEFAULT 0,
61
+ symbol_count INTEGER NOT NULL DEFAULT 0,
62
+ indexed_at INTEGER NOT NULL,
63
+ default_export TEXT
64
+ );
65
+
66
+ CREATE TABLE symbols (
67
+ id INTEGER PRIMARY KEY,
68
+ file_id INTEGER NOT NULL,
69
+ name TEXT NOT NULL,
70
+ name_lc TEXT NOT NULL,
71
+ abbr TEXT NOT NULL DEFAULT '',
72
+ qname TEXT NOT NULL,
73
+ kind TEXT NOT NULL,
74
+ signature TEXT NOT NULL DEFAULT '',
75
+ doc TEXT NOT NULL DEFAULT '',
76
+ start_line INTEGER NOT NULL,
77
+ end_line INTEGER NOT NULL,
78
+ start_col INTEGER NOT NULL DEFAULT 0,
79
+ doc_line INTEGER NOT NULL DEFAULT 0,
80
+ parent_id INTEGER,
81
+ exported INTEGER NOT NULL DEFAULT 0,
82
+ ret TEXT
83
+ );
84
+
85
+ CREATE VIRTUAL TABLE symbols_fts USING fts5(
86
+ words, sig, doc, path,
87
+ content='', contentless_delete=1,
88
+ tokenize='porter unicode61'
89
+ );
90
+
91
+ CREATE TABLE names (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE);
92
+
93
+ CREATE TABLE occ (
94
+ name_id INTEGER NOT NULL,
95
+ file_id INTEGER NOT NULL,
96
+ lines TEXT NOT NULL,
97
+ PRIMARY KEY (name_id, file_id)
98
+ ) WITHOUT ROWID;
99
+
100
+ CREATE TABLE calls (
101
+ file_id INTEGER NOT NULL,
102
+ caller_id INTEGER,
103
+ name_id INTEGER NOT NULL,
104
+ line INTEGER NOT NULL,
105
+ receiver TEXT,
106
+ rtype TEXT,
107
+ count INTEGER NOT NULL DEFAULT 1,
108
+ via INTEGER,
109
+ argc INTEGER NOT NULL DEFAULT -1
110
+ );
111
+
112
+ CREATE TABLE imports (
113
+ file_id INTEGER NOT NULL,
114
+ source TEXT NOT NULL,
115
+ names TEXT NOT NULL DEFAULT '',
116
+ line INTEGER NOT NULL,
117
+ resolved_id INTEGER
118
+ );
119
+
120
+ CREATE TABLE heritage (
121
+ symbol_id INTEGER NOT NULL,
122
+ file_id INTEGER NOT NULL,
123
+ name TEXT NOT NULL,
124
+ kind TEXT NOT NULL,
125
+ qualifier TEXT,
126
+ arity INTEGER NOT NULL DEFAULT 0
127
+ );
128
+
129
+ CREATE TABLE field_types (
130
+ container TEXT NOT NULL,
131
+ field TEXT NOT NULL,
132
+ type TEXT NOT NULL,
133
+ file_id INTEGER NOT NULL
134
+ );
135
+
136
+ CREATE TABLE gen_docs (
137
+ path TEXT NOT NULL,
138
+ qname TEXT NOT NULL,
139
+ text TEXT NOT NULL,
140
+ created_at INTEGER NOT NULL,
141
+ PRIMARY KEY (path, qname)
142
+ ) WITHOUT ROWID;
143
+ `;
144
+
145
+ /** Secondary indexes. Kept separate so a cold bulk load can build them at the end. */
146
+ const INDEXES = `
147
+ CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
148
+ CREATE INDEX IF NOT EXISTS idx_symbols_name_lc ON symbols(name_lc);
149
+ CREATE INDEX IF NOT EXISTS idx_symbols_abbr ON symbols(abbr) WHERE abbr <> '';
150
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id, start_line);
151
+ CREATE INDEX IF NOT EXISTS idx_symbols_parent ON symbols(parent_id) WHERE parent_id IS NOT NULL;
152
+ CREATE INDEX IF NOT EXISTS idx_occ_file ON occ(file_id);
153
+ CREATE INDEX IF NOT EXISTS idx_calls_name ON calls(name_id);
154
+ CREATE INDEX IF NOT EXISTS idx_calls_caller ON calls(caller_id);
155
+ CREATE INDEX IF NOT EXISTS idx_calls_file ON calls(file_id);
156
+ CREATE INDEX IF NOT EXISTS idx_imports_file ON imports(file_id, resolved_id);
157
+ CREATE INDEX IF NOT EXISTS idx_imports_resolved ON imports(resolved_id, file_id);
158
+ CREATE INDEX IF NOT EXISTS idx_heritage_name ON heritage(name);
159
+ CREATE INDEX IF NOT EXISTS idx_heritage_symbol ON heritage(symbol_id);
160
+ CREATE INDEX IF NOT EXISTS idx_heritage_file ON heritage(file_id);
161
+ CREATE INDEX IF NOT EXISTS idx_field_types ON field_types(container, field);
162
+ CREATE INDEX IF NOT EXISTS idx_field_types_file ON field_types(file_id);
163
+ `;
164
+
165
+ const INDEX_NAMES = ['idx_symbols_name', 'idx_symbols_name_lc', 'idx_symbols_abbr', 'idx_symbols_file', 'idx_symbols_parent', 'idx_occ_file', 'idx_calls_name', 'idx_calls_caller', 'idx_calls_file', 'idx_imports_file', 'idx_imports_resolved', 'idx_heritage_name', 'idx_heritage_symbol', 'idx_heritage_file', 'idx_field_types', 'idx_field_types_file'];
166
+
167
+ export const SYMBOL_COLUMNS =
168
+ 's.id, s.file_id, s.name, s.name_lc, s.qname, s.kind, s.signature, s.doc, s.start_line, s.end_line, ' +
169
+ 's.start_col, s.doc_line, s.parent_id, s.exported, s.ret, f.path, f.lang';
170
+ export const SYMBOL_FROM = 'symbols s JOIN files f ON f.id = s.file_id';
171
+
172
+ export interface FileMeta {
173
+ path: string;
174
+ hash: string;
175
+ mtime: number;
176
+ size: number;
177
+ }
178
+
179
+ export class ClastDb {
180
+ readonly db: DatabaseSync;
181
+ readonly path: string;
182
+ private stmts = new Map<string, StatementSync>();
183
+ private nameIds = new Map<string, number>();
184
+ private journalOff = false;
185
+ /** Incremented on every write; lets query caches invalidate cheaply. */
186
+ generation = 0;
187
+
188
+ private constructor(db: DatabaseSync, path: string) {
14
189
  this.db = db;
15
- this.dbPath = dbPath;
16
- initializeSchema(this.db);
17
- this.queries = new Queries(this.db);
190
+ this.path = path;
191
+ }
192
+
193
+ static open(dbPath: string): ClastDb {
194
+ const sqlite = loadSqlite();
195
+ const memory = dbPath === ':memory:';
196
+ if (!memory) {
197
+ const dir = dirname(dbPath);
198
+ mkdirSync(dir, { recursive: true });
199
+ // Keep the cache directory out of version control without touching the
200
+ // user's .gitignore.
201
+ const ignoreFile = join(dir, '.gitignore');
202
+ if (!existsSync(ignoreFile)) {
203
+ try { writeFileSync(ignoreFile, '*\n'); } catch { /* read-only dir */ }
204
+ }
205
+ }
206
+
207
+ let raw = new sqlite.DatabaseSync(dbPath);
208
+ let version = 0;
209
+ try {
210
+ const row = raw.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get() as
211
+ | { value: string }
212
+ | undefined;
213
+ version = row ? Number(row.value) : 0;
214
+ } catch {
215
+ version = 0;
216
+ }
217
+
218
+ // A bulk load that never finished (crash, kill) leaves an index that cannot be trusted.
219
+ let interrupted = false;
220
+ if (version === SCHEMA_VERSION) {
221
+ try {
222
+ const row = raw.prepare(`SELECT value FROM meta WHERE key = 'bulk_in_progress'`).get() as
223
+ | { value: string }
224
+ | undefined;
225
+ // The marker holds the pid of the loading process. If that process is still alive
226
+ // (another session indexing the same project) the index is merely busy, not broken.
227
+ const pid = row ? Number(row.value) : 0;
228
+ interrupted = pid > 0 && pid !== process.pid && !processAlive(pid);
229
+ if (pid === process.pid) interrupted = true; // our own earlier load in this process failed
230
+ } catch {
231
+ interrupted = true;
232
+ }
233
+ }
234
+
235
+ if (version !== SCHEMA_VERSION || interrupted) {
236
+ const hasTables =
237
+ (raw.prepare(`SELECT COUNT(*) AS n FROM sqlite_master`).get() as { n: number }).n > 0;
238
+ if (hasTables && !memory) {
239
+ // Old or foreign schema, or an interrupted load: the index is a cache, start over.
240
+ raw.close();
241
+ try {
242
+ for (const suffix of ['', '-wal', '-shm']) rmSync(dbPath + suffix, { force: true });
243
+ } catch (err) {
244
+ throw new Error(
245
+ `clast: cannot replace the outdated index at ${dbPath} (is another clast process using it?): ${String(err)}`
246
+ );
247
+ }
248
+ raw = new sqlite.DatabaseSync(dbPath);
249
+ }
250
+ raw.exec('PRAGMA journal_mode = WAL');
251
+ raw.exec(SCHEMA);
252
+ raw
253
+ .prepare(`INSERT INTO meta (key, value) VALUES ('schema_version', ?)`)
254
+ .run(String(SCHEMA_VERSION));
255
+ }
256
+
257
+ raw.exec('PRAGMA journal_mode = WAL');
258
+ raw.exec('PRAGMA synchronous = NORMAL');
259
+ raw.exec('PRAGMA busy_timeout = 5000');
260
+ raw.exec('PRAGMA temp_store = MEMORY');
261
+ // Modest private cache; the memory map below serves hot pages straight from the OS file cache.
262
+ raw.exec('PRAGMA cache_size = -32768');
263
+ raw.exec(INDEXES);
264
+ raw.exec('PRAGMA mmap_size = 268435456');
265
+ return new ClastDb(raw, dbPath);
266
+ }
18
267
 
19
- // Auto-save every 10 seconds
20
- this.saveTimer = setInterval(() => this.save(), 10000);
268
+ close(): void {
269
+ try {
270
+ this.db.exec('PRAGMA optimize');
271
+ } catch { /* ignore */ }
272
+ this.stmts.clear();
273
+ this.db.close();
21
274
  }
22
275
 
23
- static async open(dbPath: string): Promise<ClastDatabase> {
24
- mkdirSync(dirname(dbPath), { recursive: true });
276
+ /**
277
+ * Bulk-load mode for large index runs: no fsync, no auto-checkpoint, and
278
+ * (for a cold index) secondary indexes are built once at the end. The index
279
+ * is a rebuildable cache, so durability is traded for speed here.
280
+ */
281
+ beginBulk(dropIndexes: boolean): void {
282
+ // If the process dies mid-load the index is rebuilt from scratch on the next start.
283
+ this.setMeta('bulk_in_progress', String(process.pid));
284
+ this.db.exec('PRAGMA synchronous = OFF');
285
+ this.db.exec('PRAGMA wal_autocheckpoint = 0');
286
+ this.db.exec('PRAGMA cache_size = -262144');
287
+ if (dropIndexes) {
288
+ // Cold load of an empty index: no journal at all (nothing to roll back to).
289
+ this.stmts.clear();
290
+ try {
291
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
292
+ this.db.exec('PRAGMA journal_mode = OFF');
293
+ this.journalOff = true;
294
+ } catch {
295
+ this.journalOff = false; // another connection holds the database: stay in WAL
296
+ }
297
+ for (const name of INDEX_NAMES) this.db.exec(`DROP INDEX IF EXISTS ${name}`);
298
+ }
299
+ }
25
300
 
26
- const SQL = await initSqlJs();
301
+ /** Drop all indexed data (generated docs are kept). Used by forced rebuilds. */
302
+ clearIndex(): void {
303
+ this.transaction(() => {
304
+ for (const table of ['symbols', 'occ', 'calls', 'imports', 'heritage', 'field_types', 'names', 'files']) {
305
+ this.db.exec(`DELETE FROM ${table}`);
306
+ }
307
+ this.db.exec(`INSERT INTO symbols_fts(symbols_fts) VALUES('delete-all')`);
308
+ });
309
+ this.nameIds.clear();
310
+ }
27
311
 
28
- let db: Database;
29
- if (existsSync(dbPath)) {
30
- const buffer = readFileSync(dbPath);
31
- db = new SQL.Database(buffer);
32
- } else {
33
- db = new SQL.Database();
312
+ endBulk(): void {
313
+ this.stmts.clear();
314
+ if (this.journalOff) {
315
+ this.db.exec('PRAGMA journal_mode = WAL');
316
+ this.journalOff = false;
34
317
  }
318
+ this.db.exec(INDEXES);
319
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
320
+ this.db.exec('PRAGMA wal_autocheckpoint = 1000');
321
+ this.db.exec('PRAGMA synchronous = NORMAL');
322
+ this.db.exec('PRAGMA cache_size = -32768');
323
+ try { this.db.exec('PRAGMA shrink_memory'); } catch { /* optional */ }
324
+ // Sampled statistics are enough for the planner and far cheaper than a full ANALYZE.
325
+ this.db.exec('PRAGMA analysis_limit = 400');
326
+ this.db.exec('ANALYZE');
327
+ this.stmts.clear();
328
+ this.setMeta('bulk_in_progress', '0');
329
+ }
330
+
331
+ // --- generic helpers -----------------------------------------------------
332
+
333
+ private stmt(sql: string): StatementSync {
334
+ let s = this.stmts.get(sql);
335
+ if (!s) {
336
+ s = this.db.prepare(sql);
337
+ this.stmts.set(sql, s);
338
+ }
339
+ return s;
340
+ }
341
+
342
+ all<T>(sql: string, ...params: SqlValue[]): T[] {
343
+ return this.stmt(sql).all(...params) as T[];
344
+ }
345
+
346
+ get<T>(sql: string, ...params: SqlValue[]): T | undefined {
347
+ return this.stmt(sql).get(...params) as T | undefined;
348
+ }
35
349
 
36
- return new ClastDatabase(db, dbPath);
350
+ run(sql: string, ...params: SqlValue[]): { changes: number; lastInsertRowid: number } {
351
+ const r = this.stmt(sql).run(...params);
352
+ return { changes: Number(r.changes), lastInsertRowid: Number(r.lastInsertRowid) };
37
353
  }
38
354
 
39
- save(): void {
355
+ transaction<T>(fn: () => T): T {
356
+ this.db.exec('BEGIN IMMEDIATE');
40
357
  try {
41
- const data = this.db.export();
42
- writeFileSync(this.dbPath, Buffer.from(data));
358
+ const result = fn();
359
+ this.db.exec('COMMIT');
360
+ this.generation++;
361
+ return result;
43
362
  } catch (err) {
44
- console.error('[clast] Failed to save database:', err);
363
+ // Without a journal (cold bulk load) there is nothing to roll back to; the
364
+ // bulk_in_progress marker makes the next start rebuild the index.
365
+ try { this.db.exec(this.journalOff ? 'COMMIT' : 'ROLLBACK'); } catch { /* already closed */ }
366
+ throw err;
45
367
  }
46
368
  }
47
369
 
48
- close(): void {
49
- if (this.saveTimer) {
50
- clearInterval(this.saveTimer);
51
- this.saveTimer = null;
370
+ getMeta(key: string): string | undefined {
371
+ return this.get<{ value: string }>(`SELECT value FROM meta WHERE key = ?`, key)?.value;
372
+ }
373
+
374
+ setMeta(key: string, value: string): void {
375
+ this.run(`INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, key, value);
376
+ }
377
+
378
+ // --- names ---------------------------------------------------------------
379
+
380
+ nameId(name: string, create: boolean): number | undefined {
381
+ const cached = this.nameIds.get(name);
382
+ if (cached !== undefined) return cached;
383
+ let id = this.get<{ id: number }>(`SELECT id FROM names WHERE name = ?`, name)?.id;
384
+ if (id === undefined) {
385
+ if (!create) return undefined;
386
+ id = this.run(`INSERT INTO names (name) VALUES (?)`, name).lastInsertRowid;
52
387
  }
53
- this.save();
54
- this.db.close();
388
+ if (this.nameIds.size > 400_000) this.nameIds.clear();
389
+ this.nameIds.set(name, id);
390
+ return id;
55
391
  }
56
392
 
57
- get raw(): Database {
58
- return this.db;
393
+ // --- files ---------------------------------------------------------------
394
+
395
+ fileByPath(path: string): FileRow | undefined {
396
+ return this.get<FileRow>(`SELECT * FROM files WHERE path = ?`, path);
397
+ }
398
+
399
+ fileById(id: number): FileRow | undefined {
400
+ return this.get<FileRow>(`SELECT * FROM files WHERE id = ?`, id);
401
+ }
402
+
403
+ allFiles(): FileRow[] {
404
+ return this.all<FileRow>(`SELECT * FROM files ORDER BY path`);
405
+ }
406
+
407
+ touchFile(id: number, mtime: number, size: number): void {
408
+ this.run(`UPDATE files SET mtime = ?, size = ? WHERE id = ?`, mtime, size, id);
409
+ }
410
+
411
+ private clearFileData(fileId: number): void {
412
+ this.run(`DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_id = ?)`, fileId);
413
+ this.run(`DELETE FROM symbols WHERE file_id = ?`, fileId);
414
+ this.run(`DELETE FROM occ WHERE file_id = ?`, fileId);
415
+ this.run(`DELETE FROM calls WHERE file_id = ?`, fileId);
416
+ this.run(`DELETE FROM imports WHERE file_id = ?`, fileId);
417
+ this.run(`DELETE FROM heritage WHERE file_id = ?`, fileId);
418
+ this.run(`DELETE FROM field_types WHERE file_id = ?`, fileId);
419
+ }
420
+
421
+ /** Remove a file and everything derived from it. Call inside a transaction. */
422
+ deleteFile(path: string): boolean {
423
+ const row = this.fileByPath(path);
424
+ if (!row) return false;
425
+ this.clearFileData(row.id);
426
+ this.run(`UPDATE imports SET resolved_id = NULL WHERE resolved_id = ?`, row.id);
427
+ this.run(`DELETE FROM files WHERE id = ?`, row.id);
428
+ return true;
429
+ }
430
+
431
+ /**
432
+ * Replace all indexed data for a file. Call inside a transaction.
433
+ * Returns the file id.
434
+ */
435
+ writeFile(meta: FileMeta, ex: Extraction): number {
436
+ const existing = this.fileByPath(meta.path);
437
+ let fileId: number;
438
+ const now = Date.now();
439
+ if (existing) {
440
+ fileId = existing.id;
441
+ this.clearFileData(fileId);
442
+ this.run(
443
+ `UPDATE files SET hash = ?, mtime = ?, size = ?, lang = ?, line_count = ?, symbol_count = ?, indexed_at = ?, default_export = ? WHERE id = ?`,
444
+ meta.hash, meta.mtime, meta.size, ex.lang, ex.lineCount, ex.symbols.length, now, ex.defaultExport, fileId
445
+ );
446
+ } else {
447
+ fileId = this.run(
448
+ `INSERT INTO files (path, hash, mtime, size, lang, line_count, symbol_count, indexed_at, default_export) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
449
+ meta.path, meta.hash, meta.mtime, meta.size, ex.lang, ex.lineCount, ex.symbols.length, now, ex.defaultExport
450
+ ).lastInsertRowid;
451
+ }
452
+
453
+ const insertSymbol = this.stmt(
454
+ `INSERT INTO symbols (file_id, name, name_lc, abbr, qname, kind, signature, doc, start_line, end_line, start_col, doc_line, parent_id, exported, ret)
455
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
456
+ );
457
+ const insertFts = this.stmt(`INSERT INTO symbols_fts (rowid, words, sig, doc, path) VALUES (?, ?, ?, ?, ?)`);
458
+ const pathWords = splitIdentifier(meta.path).join(' ');
459
+ const ids: number[] = new Array(ex.symbols.length);
460
+ for (let i = 0; i < ex.symbols.length; i++) {
461
+ const s = ex.symbols[i]!;
462
+ const parentId = s.parent >= 0 ? ids[s.parent] ?? null : null;
463
+ const r = insertSymbol.run(
464
+ fileId, s.name, s.name.toLowerCase(), initials(s.name), s.qname, s.kind, s.signature, s.doc,
465
+ s.startLine, s.endLine, s.startCol, s.docLine, parentId, s.exported ? 1 : 0, s.ret
466
+ );
467
+ const id = Number(r.lastInsertRowid);
468
+ ids[i] = id;
469
+ if (FTS_KINDS.has(s.kind)) {
470
+ insertFts.run(id, ftsWords(s.qname), ftsWords(s.signature), s.doc, pathWords);
471
+ }
472
+ }
473
+
474
+ const insertOcc = this.stmt(`INSERT OR REPLACE INTO occ (name_id, file_id, lines) VALUES (?, ?, ?)`);
475
+ for (const [name, lines] of ex.occ) {
476
+ insertOcc.run(this.nameId(name, true)!, fileId, lines);
477
+ }
478
+
479
+ const insertCall = this.stmt(
480
+ `INSERT INTO calls (file_id, caller_id, name_id, line, receiver, rtype, count, argc) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
481
+ );
482
+ const callIds: number[] = new Array(ex.calls.length);
483
+ for (let i = 0; i < ex.calls.length; i++) {
484
+ const c = ex.calls[i]!;
485
+ const r = insertCall.run(
486
+ fileId, c.caller >= 0 ? ids[c.caller] ?? null : null, this.nameId(c.name, true)!, c.line, c.receiver, c.rtype, c.count,
487
+ c.argc ?? -1
488
+ );
489
+ callIds[i] = Number(r.lastInsertRowid);
490
+ }
491
+ // Chain links point at other calls of the same file: set once all rows have ids.
492
+ const linkCall = this.stmt(`UPDATE calls SET via = ? WHERE rowid = ?`);
493
+ for (let i = 0; i < ex.calls.length; i++) {
494
+ const via = ex.calls[i]!.via;
495
+ if (via >= 0 && callIds[via] !== undefined) linkCall.run(callIds[via]!, callIds[i]!);
496
+ }
497
+
498
+ const insertImport = this.stmt(`INSERT INTO imports (file_id, source, names, line) VALUES (?, ?, ?, ?)`);
499
+ for (const im of ex.imports) {
500
+ insertImport.run(fileId, im.source, im.names.join(','), im.line);
501
+ }
502
+
503
+ const insertHeritage = this.stmt(`INSERT INTO heritage (symbol_id, file_id, name, kind, qualifier, arity) VALUES (?, ?, ?, ?, ?, ?)`);
504
+ for (const h of ex.heritage) {
505
+ const sid = ids[h.symbol];
506
+ if (sid !== undefined) insertHeritage.run(sid, fileId, h.name, h.kind, h.qualifier, h.arity ?? 0);
507
+ }
508
+
509
+ const insertFieldType = this.stmt(`INSERT INTO field_types (container, field, type, file_id) VALUES (?, ?, ?, ?)`);
510
+ for (const [container, field, type] of ex.fieldTypes) insertFieldType.run(container, field, type, fileId);
511
+
512
+ return fileId;
59
513
  }
60
- }
61
514
 
62
- export { Queries } from './queries.js';
63
- export type { FileRow, AstNodeRow, CallEdgeRow, CommentRow, StatusRow } from './types.js';
515
+ // --- symbols -------------------------------------------------------------
516
+
517
+ symbolById(id: number): SymbolRow | undefined {
518
+ return this.get<SymbolRow>(`SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM} WHERE s.id = ?`, id);
519
+ }
520
+
521
+ symbolsByName(name: string): SymbolRow[] {
522
+ return this.all<SymbolRow>(`SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM} WHERE s.name = ?`, name);
523
+ }
524
+
525
+ symbolsInFile(fileId: number): SymbolRow[] {
526
+ return this.all<SymbolRow>(
527
+ `SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM} WHERE s.file_id = ? ORDER BY s.start_line, s.end_line DESC`,
528
+ fileId
529
+ );
530
+ }
531
+
532
+ /** Innermost symbol containing a line in a file. */
533
+ enclosingSymbol(fileId: number, line: number): SymbolRow | undefined {
534
+ return this.get<SymbolRow>(
535
+ `SELECT ${SYMBOL_COLUMNS} FROM ${SYMBOL_FROM}
536
+ WHERE s.file_id = ? AND s.start_line <= ? AND s.end_line >= ?
537
+ ORDER BY s.start_line DESC, s.end_line ASC LIMIT 1`,
538
+ fileId, line, line
539
+ );
540
+ }
541
+
542
+ /** Number of files that mention a name (cheap popularity signal). */
543
+ refFileCount(name: string): number {
544
+ const id = this.nameId(name, false);
545
+ if (id === undefined) return 0;
546
+ return this.get<{ n: number }>(`SELECT COUNT(*) AS n FROM occ WHERE name_id = ?`, id)?.n ?? 0;
547
+ }
548
+
549
+ stats(): { files: number; symbols: number; calls: number; names: number; imports: number; lastIndexed: number | null } {
550
+ const row = this.get<{ files: number; symbols: number; calls: number; names: number; imports: number; last: number | null }>(
551
+ `SELECT (SELECT COUNT(*) FROM files) AS files,
552
+ (SELECT COUNT(*) FROM symbols) AS symbols,
553
+ (SELECT COUNT(*) FROM calls) AS calls,
554
+ (SELECT COUNT(*) FROM names) AS names,
555
+ (SELECT COUNT(*) FROM imports) AS imports,
556
+ (SELECT MAX(indexed_at) FROM files) AS last`
557
+ )!;
558
+ return { files: row.files, symbols: row.symbols, calls: row.calls, names: row.names, imports: row.imports, lastIndexed: row.last };
559
+ }
560
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { loadConfig, resolveDbPath, type ClastConfig } from './config/index.js';
4
+ import { ClastDb } from './db/index.js';
5
+ import { Indexer } from './indexer/index.js';
6
+
7
+ /**
8
+ * One indexed project: configuration, database and indexer. Shared by the MCP
9
+ * server, the CLI and tests.
10
+ */
11
+ export class Clast {
12
+ readonly root: string;
13
+ readonly config: ClastConfig;
14
+ readonly db: ClastDb;
15
+ readonly indexer: Indexer;
16
+
17
+ constructor(root: string, config?: ClastConfig) {
18
+ this.root = resolve(root);
19
+ if (!existsSync(this.root) || !statSync(this.root).isDirectory()) {
20
+ throw new Error(`project root does not exist or is not a directory: ${this.root}`);
21
+ }
22
+ this.config = config ?? loadConfig(this.root);
23
+ this.db = ClastDb.open(resolveDbPath(this.root, this.config));
24
+ this.indexer = new Indexer(this.root, this.config, this.db);
25
+ }
26
+
27
+ close(): void {
28
+ this.db.close();
29
+ }
30
+ }