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.
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +11 -9
- package/README.md +447 -401
- package/bench/ab/onair-concept.json +22 -0
- package/bench/ab/onair-deadcode.json +7 -0
- package/bench/ab/onair-hard.json +32 -0
- package/bench/ab/onair-impact.json +17 -0
- package/bench/ab/onair-refactors.json +18 -0
- package/bench/ab/onair-survey.json +17 -0
- package/bench/ab/onair-tasks.json +20 -0
- package/bench/ab/onair.json +52 -0
- package/bench/ab/vscode-concept.json +22 -0
- package/bench/ab/vscode-hard.json +32 -0
- package/bench/ab/vscode-impact.json +17 -0
- package/bench/ab/vscode-refactors.json +18 -0
- package/bench/ab/vscode-tasks.json +20 -0
- package/bench/ab/vscode.json +52 -0
- package/bench/ab.mjs +213 -0
- package/bench/bench.mjs +98 -0
- package/bench/resolution.mjs +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +257 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +12 -2
- package/dist/config/defaults.js +38 -31
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +26 -14
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +73 -13
- package/dist/db/index.js +481 -39
- package/dist/db/index.js.map +1 -1
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +28 -0
- package/dist/engine.js.map +1 -0
- package/dist/indexer/imports.d.ts +6 -0
- package/dist/indexer/imports.js +254 -0
- package/dist/indexer/imports.js.map +1 -0
- package/dist/indexer/index.d.ts +63 -0
- package/dist/indexer/index.js +360 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/process.d.ts +22 -0
- package/dist/indexer/process.js +42 -0
- package/dist/indexer/process.js.map +1 -0
- package/dist/indexer/scan.d.ts +43 -0
- package/dist/indexer/scan.js +223 -0
- package/dist/indexer/scan.js.map +1 -0
- package/dist/indexer/worker.d.ts +1 -0
- package/dist/indexer/worker.js +23 -0
- package/dist/indexer/worker.js.map +1 -0
- package/dist/llm/comment-generator.d.ts +17 -19
- package/dist/llm/comment-generator.js +46 -80
- package/dist/llm/comment-generator.js.map +1 -1
- package/dist/llm/index.d.ts +0 -1
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/prompts.d.ts +0 -1
- package/dist/llm/prompts.js +16 -16
- package/dist/parser/extractor.d.ts +21 -16
- package/dist/parser/extractor.js +700 -228
- package/dist/parser/extractor.js.map +1 -1
- package/dist/parser/index.d.ts +17 -21
- package/dist/parser/index.js +115 -171
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/langs/c.d.ts +3 -0
- package/dist/parser/langs/c.js +879 -0
- package/dist/parser/langs/c.js.map +1 -0
- package/dist/parser/langs/csharp.d.ts +2 -0
- package/dist/parser/langs/csharp.js +548 -0
- package/dist/parser/langs/csharp.js.map +1 -0
- package/dist/parser/langs/dart.d.ts +2 -0
- package/dist/parser/langs/dart.js +893 -0
- package/dist/parser/langs/dart.js.map +1 -0
- package/dist/parser/langs/go.d.ts +2 -0
- package/dist/parser/langs/go.js +455 -0
- package/dist/parser/langs/go.js.map +1 -0
- package/dist/parser/langs/java.d.ts +2 -0
- package/dist/parser/langs/java.js +390 -0
- package/dist/parser/langs/java.js.map +1 -0
- package/dist/parser/langs/php.d.ts +2 -0
- package/dist/parser/langs/php.js +486 -0
- package/dist/parser/langs/php.js.map +1 -0
- package/dist/parser/langs/python.d.ts +2 -0
- package/dist/parser/langs/python.js +216 -0
- package/dist/parser/langs/python.js.map +1 -0
- package/dist/parser/langs/ruby.d.ts +2 -0
- package/dist/parser/langs/ruby.js +602 -0
- package/dist/parser/langs/ruby.js.map +1 -0
- package/dist/parser/langs/rust.d.ts +2 -0
- package/dist/parser/langs/rust.js +931 -0
- package/dist/parser/langs/rust.js.map +1 -0
- package/dist/parser/langs/swift.d.ts +2 -0
- package/dist/parser/langs/swift.js +616 -0
- package/dist/parser/langs/swift.js.map +1 -0
- package/dist/parser/langs/typescript.d.ts +4 -0
- package/dist/parser/langs/typescript.js +535 -0
- package/dist/parser/langs/typescript.js.map +1 -0
- package/dist/parser/registry.d.ts +16 -0
- package/dist/parser/registry.js +87 -0
- package/dist/parser/registry.js.map +1 -0
- package/dist/parser/returns.d.ts +9 -0
- package/dist/parser/returns.js +152 -0
- package/dist/parser/returns.js.map +1 -0
- package/dist/parser/spec.d.ts +165 -0
- package/dist/parser/spec.js +188 -0
- package/dist/parser/spec.js.map +1 -0
- package/dist/query/arity.d.ts +27 -0
- package/dist/query/arity.js +187 -0
- package/dist/query/arity.js.map +1 -0
- package/dist/query/callgraph.d.ts +113 -0
- package/dist/query/callgraph.js +332 -0
- package/dist/query/callgraph.js.map +1 -0
- package/dist/query/format.d.ts +36 -0
- package/dist/query/format.js +136 -0
- package/dist/query/format.js.map +1 -0
- package/dist/query/hierarchy.d.ts +44 -0
- package/dist/query/hierarchy.js +441 -0
- package/dist/query/hierarchy.js.map +1 -0
- package/dist/query/resolve.d.ts +265 -0
- package/dist/query/resolve.js +1957 -0
- package/dist/query/resolve.js.map +1 -0
- package/dist/query/search.d.ts +35 -0
- package/dist/query/search.js +379 -0
- package/dist/query/search.js.map +1 -0
- package/dist/query/words.d.ts +30 -0
- package/dist/query/words.js +201 -0
- package/dist/query/words.js.map +1 -0
- package/dist/server/index.d.ts +1 -3
- package/dist/server/index.js +313 -73
- package/dist/server/index.js.map +1 -1
- package/dist/tools/context.d.ts +35 -0
- package/dist/tools/context.js +79 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/explore.d.ts +51 -0
- package/dist/tools/explore.js +779 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/impact.d.ts +21 -0
- package/dist/tools/impact.js +743 -0
- package/dist/tools/impact.js.map +1 -0
- package/dist/tools/search.d.ts +59 -0
- package/dist/tools/search.js +843 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/stats.d.ts +26 -0
- package/dist/tools/stats.js +604 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +95 -40
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher/index.d.ts +18 -16
- package/dist/watcher/index.js +94 -77
- package/dist/watcher/index.js.map +1 -1
- package/package.json +21 -24
- package/plugin/server.mjs +50 -0
- package/skills/audit/SKILL.md +37 -0
- package/skills/deadcode/SKILL.md +33 -0
- package/skills/explore/SKILL.md +54 -0
- package/skills/impact/SKILL.md +24 -0
- package/skills/status/SKILL.md +11 -0
- package/src/cli.ts +256 -0
- package/src/config/defaults.ts +50 -32
- package/src/config/index.ts +23 -14
- package/src/db/index.ts +540 -43
- package/src/engine.ts +30 -0
- package/src/indexer/imports.ts +241 -0
- package/src/indexer/index.ts +369 -0
- package/src/indexer/process.ts +63 -0
- package/src/indexer/scan.ts +226 -0
- package/src/indexer/worker.ts +24 -0
- package/src/llm/comment-generator.ts +52 -103
- package/src/llm/index.ts +80 -80
- package/src/llm/prompts.ts +95 -95
- package/src/parser/extractor.ts +643 -256
- package/src/parser/index.ts +105 -205
- package/src/parser/langs/c.ts +860 -0
- package/src/parser/langs/csharp.ts +519 -0
- package/src/parser/langs/dart.ts +820 -0
- package/src/parser/langs/go.ts +437 -0
- package/src/parser/langs/java.ts +361 -0
- package/src/parser/langs/php.ts +450 -0
- package/src/parser/langs/python.ts +206 -0
- package/src/parser/langs/ruby.ts +554 -0
- package/src/parser/langs/rust.ts +904 -0
- package/src/parser/langs/swift.ts +590 -0
- package/src/parser/langs/typescript.ts +499 -0
- package/src/parser/registry.ts +88 -0
- package/src/parser/returns.ts +137 -0
- package/src/parser/spec.ts +309 -0
- package/src/query/arity.ts +173 -0
- package/src/query/callgraph.ts +473 -0
- package/src/query/format.ts +136 -0
- package/src/query/hierarchy.ts +447 -0
- package/src/query/resolve.ts +1972 -0
- package/src/query/search.ts +430 -0
- package/src/query/words.ts +183 -0
- package/src/server/index.ts +390 -79
- package/src/tools/context.ts +82 -0
- package/src/tools/explore.ts +860 -0
- package/src/tools/impact.ts +748 -0
- package/src/tools/search.ts +886 -0
- package/src/tools/stats.ts +633 -0
- package/src/types.ts +113 -45
- package/src/version.ts +2 -0
- package/src/watcher/index.ts +82 -88
- package/tsconfig.json +7 -4
- package/.mcp.json +0 -8
- package/commands/clast-status.md +0 -9
- package/dist/config/defaults.d.ts.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/db/index.d.ts.map +0 -1
- package/dist/db/queries.d.ts +0 -44
- package/dist/db/queries.d.ts.map +0 -1
- package/dist/db/queries.js +0 -251
- package/dist/db/queries.js.map +0 -1
- package/dist/db/schema.d.ts +0 -3
- package/dist/db/schema.d.ts.map +0 -1
- package/dist/db/schema.js +0 -94
- package/dist/db/schema.js.map +0 -1
- package/dist/db/types.d.ts +0 -44
- package/dist/db/types.d.ts.map +0 -1
- package/dist/db/types.js +0 -2
- package/dist/db/types.js.map +0 -1
- package/dist/llm/comment-generator.d.ts.map +0 -1
- package/dist/llm/index.d.ts.map +0 -1
- package/dist/llm/prompts.d.ts.map +0 -1
- package/dist/parser/call-resolver.d.ts +0 -15
- package/dist/parser/call-resolver.d.ts.map +0 -1
- package/dist/parser/call-resolver.js +0 -16
- package/dist/parser/call-resolver.js.map +0 -1
- package/dist/parser/extractor.d.ts.map +0 -1
- package/dist/parser/index.d.ts.map +0 -1
- package/dist/parser/languages.d.ts +0 -11
- package/dist/parser/languages.d.ts.map +0 -1
- package/dist/parser/languages.js +0 -74
- package/dist/parser/languages.js.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/register-tools.d.ts +0 -7
- package/dist/server/register-tools.d.ts.map +0 -1
- package/dist/server/register-tools.js +0 -17
- package/dist/server/register-tools.js.map +0 -1
- package/dist/server/tools/clast-call-graph.d.ts +0 -4
- package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
- package/dist/server/tools/clast-call-graph.js +0 -70
- package/dist/server/tools/clast-call-graph.js.map +0 -1
- package/dist/server/tools/clast-comment.d.ts +0 -5
- package/dist/server/tools/clast-comment.d.ts.map +0 -1
- package/dist/server/tools/clast-comment.js +0 -64
- package/dist/server/tools/clast-comment.js.map +0 -1
- package/dist/server/tools/clast-file-summary.d.ts +0 -4
- package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
- package/dist/server/tools/clast-file-summary.js +0 -42
- package/dist/server/tools/clast-file-summary.js.map +0 -1
- package/dist/server/tools/clast-get-context.d.ts +0 -4
- package/dist/server/tools/clast-get-context.d.ts.map +0 -1
- package/dist/server/tools/clast-get-context.js +0 -80
- package/dist/server/tools/clast-get-context.js.map +0 -1
- package/dist/server/tools/clast-reindex.d.ts +0 -5
- package/dist/server/tools/clast-reindex.d.ts.map +0 -1
- package/dist/server/tools/clast-reindex.js +0 -36
- package/dist/server/tools/clast-reindex.js.map +0 -1
- package/dist/server/tools/clast-search.d.ts +0 -4
- package/dist/server/tools/clast-search.d.ts.map +0 -1
- package/dist/server/tools/clast-search.js +0 -29
- package/dist/server/tools/clast-search.js.map +0 -1
- package/dist/server/tools/clast-status.d.ts +0 -5
- package/dist/server/tools/clast-status.d.ts.map +0 -1
- package/dist/server/tools/clast-status.js +0 -23
- package/dist/server/tools/clast-status.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/watcher/gitignore.d.ts +0 -9
- package/dist/watcher/gitignore.d.ts.map +0 -1
- package/dist/watcher/gitignore.js +0 -38
- package/dist/watcher/gitignore.js.map +0 -1
- package/dist/watcher/index.d.ts.map +0 -1
- package/skills/clast-explore/SKILL.md +0 -41
- package/src/db/queries.ts +0 -332
- package/src/db/schema.ts +0 -106
- package/src/db/types.ts +0 -47
- package/src/parser/call-resolver.ts +0 -17
- package/src/parser/languages.ts +0 -86
- package/src/server/register-tools.ts +0 -30
- package/src/server/tools/clast-call-graph.ts +0 -82
- package/src/server/tools/clast-comment.ts +0 -76
- package/src/server/tools/clast-file-summary.ts +0 -59
- package/src/server/tools/clast-get-context.ts +0 -97
- package/src/server/tools/clast-reindex.ts +0 -52
- package/src/server/tools/clast-search.ts +0 -38
- package/src/server/tools/clast-status.ts +0 -37
- package/src/watcher/gitignore.ts +0 -42
package/src/db/index.ts
CHANGED
|
@@ -1,63 +1,560 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { mkdirSync,
|
|
3
|
-
import { dirname } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
db =
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
+
transaction<T>(fn: () => T): T {
|
|
356
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
40
357
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
358
|
+
const result = fn();
|
|
359
|
+
this.db.exec('COMMIT');
|
|
360
|
+
this.generation++;
|
|
361
|
+
return result;
|
|
43
362
|
} catch (err) {
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
54
|
-
this.
|
|
388
|
+
if (this.nameIds.size > 400_000) this.nameIds.clear();
|
|
389
|
+
this.nameIds.set(name, id);
|
|
390
|
+
return id;
|
|
55
391
|
}
|
|
56
392
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
+
}
|