lattice-graph 0.1.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.
@@ -0,0 +1,71 @@
1
+ import type { TreeSitterNode, TreeSitterTree } from "../parser.ts";
2
+
3
+ /** A parsed import statement from TypeScript source. */
4
+ type TypeScriptImport = {
5
+ readonly module: string;
6
+ readonly names: readonly string[];
7
+ readonly defaultImport: string | undefined;
8
+ readonly isRelative: boolean;
9
+ readonly line: number;
10
+ };
11
+
12
+ /**
13
+ * Extracts import statements from a TypeScript AST.
14
+ *
15
+ * @param tree - Parsed tree-sitter tree
16
+ * @param _filePath - Relative file path (unused, kept for interface consistency)
17
+ * @returns Parsed imports with module paths and imported names
18
+ */
19
+ function extractTypeScriptImports(
20
+ tree: TreeSitterTree,
21
+ _filePath: string,
22
+ ): readonly TypeScriptImport[] {
23
+ const imports: TypeScriptImport[] = [];
24
+
25
+ for (const child of tree.rootNode.children) {
26
+ if (child.type === "import_statement") {
27
+ parseImportStatement(child, imports);
28
+ }
29
+ }
30
+
31
+ return imports;
32
+ }
33
+
34
+ /** Parses an import statement into module path, named imports, and default import. */
35
+ function parseImportStatement(node: TreeSitterNode, results: TypeScriptImport[]): void {
36
+ const fromClause = node.children.find((c) => c.type === "string");
37
+ if (!fromClause) return;
38
+
39
+ const contentNode = fromClause.children.find((c) => c.type === "string_fragment");
40
+ const module = contentNode?.text ?? "";
41
+ const isRelative = module.startsWith(".");
42
+
43
+ const importClause = node.children.find((c) => c.type === "import_clause");
44
+ if (!importClause) return;
45
+
46
+ let defaultImport: string | undefined;
47
+ const names: string[] = [];
48
+
49
+ for (const child of importClause.children) {
50
+ if (child.type === "identifier") {
51
+ defaultImport = child.text;
52
+ } else if (child.type === "named_imports") {
53
+ for (const spec of child.children) {
54
+ if (spec.type === "import_specifier") {
55
+ const nameNode = spec.childForFieldName("name");
56
+ if (nameNode) names.push(nameNode.text);
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ results.push({
63
+ module,
64
+ names,
65
+ defaultImport,
66
+ isRelative,
67
+ line: node.startPosition.row + 1,
68
+ });
69
+ }
70
+
71
+ export { extractTypeScriptImports, type TypeScriptImport };
@@ -0,0 +1,252 @@
1
+ import type { Node, NodeKind } from "../../types/graph.ts";
2
+ import type { TreeSitterNode, TreeSitterTree } from "../parser.ts";
3
+
4
+ /**
5
+ * Extracts symbols (functions, classes, methods, interfaces, type aliases)
6
+ * from a TypeScript AST.
7
+ *
8
+ * @param tree - Parsed tree-sitter tree
9
+ * @param filePath - Relative file path for node ID construction
10
+ * @param _source - Original source text (unused, kept for interface consistency)
11
+ * @returns Extracted nodes with deterministic IDs
12
+ */
13
+ function extractTypeScriptSymbols(
14
+ tree: TreeSitterTree,
15
+ filePath: string,
16
+ _source: string,
17
+ ): readonly Node[] {
18
+ const nodes: Node[] = [];
19
+ visitNode(tree.rootNode, filePath, [], nodes);
20
+ return nodes;
21
+ }
22
+
23
+ /** Scope entry for tracking parent context. */
24
+ type ScopeEntry = { readonly name: string; readonly isClass: boolean };
25
+
26
+ /** Recursively visits AST nodes to extract symbols. */
27
+ function visitNode(
28
+ node: TreeSitterNode,
29
+ filePath: string,
30
+ scopeStack: readonly ScopeEntry[],
31
+ results: Node[],
32
+ ): void {
33
+ switch (node.type) {
34
+ case "function_declaration":
35
+ extractFunction(node, filePath, scopeStack, results);
36
+ return;
37
+ case "class_declaration":
38
+ extractClass(node, filePath, scopeStack, results);
39
+ return;
40
+ case "interface_declaration":
41
+ extractTypeDecl(node, filePath, scopeStack, results, "type");
42
+ return;
43
+ case "type_alias_declaration":
44
+ extractTypeDecl(node, filePath, scopeStack, results, "type");
45
+ return;
46
+ case "export_statement": {
47
+ // Unwrap: export function X, export class X, export const X = ...
48
+ for (const child of node.children) {
49
+ visitNode(child, filePath, scopeStack, results);
50
+ }
51
+ return;
52
+ }
53
+ case "lexical_declaration": {
54
+ // const handler = async () => { ... }
55
+ extractArrowFunctions(node, filePath, scopeStack, results);
56
+ return;
57
+ }
58
+ case "method_definition": {
59
+ extractMethod(node, filePath, scopeStack, results);
60
+ return;
61
+ }
62
+ default:
63
+ for (const child of node.children) {
64
+ visitNode(child, filePath, scopeStack, results);
65
+ }
66
+ }
67
+ }
68
+
69
+ /** Extracts a function declaration. */
70
+ function extractFunction(
71
+ node: TreeSitterNode,
72
+ filePath: string,
73
+ scopeStack: readonly ScopeEntry[],
74
+ results: Node[],
75
+ ): void {
76
+ const nameNode = node.childForFieldName("name");
77
+ if (!nameNode) return;
78
+
79
+ const name = nameNode.text;
80
+ const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
81
+ const id = `${filePath}::${qualifiedName}`;
82
+
83
+ results.push({
84
+ id,
85
+ kind: "function",
86
+ name,
87
+ file: filePath,
88
+ lineStart: node.startPosition.row + 1,
89
+ lineEnd: node.endPosition.row + 1,
90
+ language: "typescript",
91
+ signature: buildSignature(node, name),
92
+ isTest: false,
93
+ metadata: undefined,
94
+ });
95
+ }
96
+
97
+ /** Extracts a class declaration and recurses into its body for methods. */
98
+ function extractClass(
99
+ node: TreeSitterNode,
100
+ filePath: string,
101
+ scopeStack: readonly ScopeEntry[],
102
+ results: Node[],
103
+ ): void {
104
+ const nameNode = node.childForFieldName("name");
105
+ if (!nameNode) return;
106
+
107
+ const name = nameNode.text;
108
+ const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
109
+ const id = `${filePath}::${qualifiedName}`;
110
+
111
+ results.push({
112
+ id,
113
+ kind: "class",
114
+ name,
115
+ file: filePath,
116
+ lineStart: node.startPosition.row + 1,
117
+ lineEnd: node.endPosition.row + 1,
118
+ language: "typescript",
119
+ signature: undefined,
120
+ isTest: false,
121
+ metadata: undefined,
122
+ });
123
+
124
+ // Recurse into class body for methods
125
+ const body = node.childForFieldName("body");
126
+ if (body) {
127
+ const newScope = [...scopeStack, { name, isClass: true }];
128
+ for (const child of body.children) {
129
+ visitNode(child, filePath, newScope, results);
130
+ }
131
+ }
132
+ }
133
+
134
+ /** Extracts a method definition inside a class. */
135
+ function extractMethod(
136
+ node: TreeSitterNode,
137
+ filePath: string,
138
+ scopeStack: readonly ScopeEntry[],
139
+ results: Node[],
140
+ ): void {
141
+ const nameNode = node.childForFieldName("name");
142
+ if (!nameNode) return;
143
+
144
+ const name = nameNode.text;
145
+ const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
146
+ const id = `${filePath}::${qualifiedName}`;
147
+
148
+ results.push({
149
+ id,
150
+ kind: "method",
151
+ name,
152
+ file: filePath,
153
+ lineStart: node.startPosition.row + 1,
154
+ lineEnd: node.endPosition.row + 1,
155
+ language: "typescript",
156
+ signature: buildSignature(node, name),
157
+ isTest: false,
158
+ metadata: undefined,
159
+ });
160
+ }
161
+
162
+ /** Extracts an interface or type alias declaration. */
163
+ function extractTypeDecl(
164
+ node: TreeSitterNode,
165
+ filePath: string,
166
+ scopeStack: readonly ScopeEntry[],
167
+ results: Node[],
168
+ kind: NodeKind,
169
+ ): void {
170
+ const nameNode = node.childForFieldName("name");
171
+ if (!nameNode) return;
172
+
173
+ const name = nameNode.text;
174
+ const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
175
+ const id = `${filePath}::${qualifiedName}`;
176
+
177
+ results.push({
178
+ id,
179
+ kind,
180
+ name,
181
+ file: filePath,
182
+ lineStart: node.startPosition.row + 1,
183
+ lineEnd: node.endPosition.row + 1,
184
+ language: "typescript",
185
+ signature: undefined,
186
+ isTest: false,
187
+ metadata: undefined,
188
+ });
189
+ }
190
+
191
+ /** Extracts arrow functions assigned to const/let variables. */
192
+ function extractArrowFunctions(
193
+ node: TreeSitterNode,
194
+ filePath: string,
195
+ scopeStack: readonly ScopeEntry[],
196
+ results: Node[],
197
+ ): void {
198
+ for (const child of node.children) {
199
+ if (child.type === "variable_declarator") {
200
+ const nameNode = child.childForFieldName("name");
201
+ const valueNode = child.childForFieldName("value");
202
+ if (nameNode && valueNode?.type === "arrow_function") {
203
+ const name = nameNode.text;
204
+ const qualifiedName = [...scopeStack.map((s) => s.name), name].join(".");
205
+ const id = `${filePath}::${qualifiedName}`;
206
+
207
+ results.push({
208
+ id,
209
+ kind: "function",
210
+ name,
211
+ file: filePath,
212
+ lineStart: node.startPosition.row + 1,
213
+ lineEnd: node.endPosition.row + 1,
214
+ language: "typescript",
215
+ signature: buildArrowSignature(valueNode, name),
216
+ isTest: false,
217
+ metadata: undefined,
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ /** Builds a signature string from a function/method declaration. */
225
+ function buildSignature(node: TreeSitterNode, name: string): string {
226
+ const params = node.childForFieldName("parameters");
227
+ const returnType =
228
+ node.childForFieldName("return_type") ??
229
+ node.children.find((c) => c.type === "type_annotation");
230
+
231
+ const paramsStr = params?.text ?? "()";
232
+ const returnStr = returnType?.text ?? "";
233
+
234
+ return `${name}${paramsStr}${returnStr ? ` ${returnStr}` : ""}`;
235
+ }
236
+
237
+ /** Builds a signature string from an arrow function. */
238
+ function buildArrowSignature(node: TreeSitterNode, name: string): string {
239
+ const params =
240
+ node.childForFieldName("parameters") ??
241
+ node.children.find((c) => c.type === "formal_parameters");
242
+ const returnType =
243
+ node.childForFieldName("return_type") ??
244
+ node.children.find((c) => c.type === "type_annotation");
245
+
246
+ const paramsStr = params?.text ?? "()";
247
+ const returnStr = returnType?.text ?? "";
248
+
249
+ return `${name}${paramsStr}${returnStr ? ` ${returnStr}` : ""}`;
250
+ }
251
+
252
+ export { extractTypeScriptSymbols };
@@ -0,0 +1,95 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { err, ok, type Result } from "../types/result.ts";
3
+
4
+ const SCHEMA_VERSION = "1";
5
+
6
+ const SCHEMA_SQL = `
7
+ CREATE TABLE IF NOT EXISTS nodes (
8
+ id TEXT PRIMARY KEY,
9
+ kind TEXT NOT NULL,
10
+ name TEXT NOT NULL,
11
+ file TEXT NOT NULL,
12
+ line_start INTEGER NOT NULL,
13
+ line_end INTEGER NOT NULL,
14
+ language TEXT NOT NULL,
15
+ signature TEXT,
16
+ is_test INTEGER DEFAULT 0,
17
+ metadata TEXT
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS edges (
21
+ source_id TEXT NOT NULL,
22
+ target_id TEXT NOT NULL,
23
+ kind TEXT NOT NULL,
24
+ certainty TEXT DEFAULT 'certain',
25
+ PRIMARY KEY (source_id, target_id, kind)
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS tags (
29
+ node_id TEXT NOT NULL REFERENCES nodes(id),
30
+ kind TEXT NOT NULL,
31
+ value TEXT NOT NULL,
32
+ PRIMARY KEY (node_id, kind, value)
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS unresolved (
36
+ file TEXT NOT NULL,
37
+ line INTEGER NOT NULL,
38
+ expression TEXT NOT NULL,
39
+ reason TEXT NOT NULL,
40
+ PRIMARY KEY (file, line, expression)
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS meta (
44
+ key TEXT PRIMARY KEY,
45
+ value TEXT NOT NULL
46
+ );
47
+
48
+ CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id, kind);
49
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id, kind);
50
+ CREATE INDEX IF NOT EXISTS idx_tags_kind_value ON tags(kind, value);
51
+ CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file);
52
+ CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
53
+ `;
54
+
55
+ /**
56
+ * Creates or opens a SQLite database with the Lattice schema.
57
+ * Uses WAL mode for concurrent read performance.
58
+ *
59
+ * @param path - File path for the database, or ":memory:" for in-memory
60
+ * @returns An open Database handle with the schema applied
61
+ */
62
+ // @lattice:boundary sqlite
63
+ function createDatabase(path: string): Database {
64
+ const db = new Database(path);
65
+ db.run("PRAGMA journal_mode = WAL");
66
+ db.run("PRAGMA foreign_keys = ON");
67
+ for (const statement of SCHEMA_SQL.split(";").filter((s) => s.trim())) {
68
+ db.run(`${statement};`);
69
+ }
70
+ db.run("INSERT OR IGNORE INTO meta (key, value) VALUES ('schema_version', ?)", [SCHEMA_VERSION]);
71
+ return db;
72
+ }
73
+
74
+ /**
75
+ * Validates that the database schema version matches the expected version.
76
+ *
77
+ * @param db - An open Database handle
78
+ * @returns Ok if versions match, Err with a message if they don't
79
+ */
80
+ function checkSchemaVersion(db: Database): Result<undefined, string> {
81
+ const row = db.query("SELECT value FROM meta WHERE key = 'schema_version'").get() as {
82
+ value: string;
83
+ } | null;
84
+ if (!row) {
85
+ return err("No schema_version found in database");
86
+ }
87
+ if (row.value !== SCHEMA_VERSION) {
88
+ return err(
89
+ `Schema version mismatch: expected ${SCHEMA_VERSION}, found ${row.value}. Run 'lattice build' to rebuild.`,
90
+ );
91
+ }
92
+ return ok(undefined);
93
+ }
94
+
95
+ export { checkSchemaVersion, createDatabase, SCHEMA_VERSION };