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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,391 @@
1
+ <table align="center"><tr><td>
2
+ <pre>
3
+
4
+ ██╗ █████╗ ████████╗████████╗██╗ ██████╗███████╗
5
+ ██║ ██╔══██╗╚══██╔══╝╚══██╔══╝██║██╔════╝██╔════╝
6
+ ██║ ███████║ ██║ ██║ ██║██║ █████╗
7
+ ██║ ██╔══██║ ██║ ██║ ██║██║ ██╔══╝
8
+ ███████╗██║ ██║ ██║ ██║ ██║╚██████╗███████╗
9
+ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝
10
+
11
+ </pre>
12
+ </td></tr></table>
13
+
14
+ <p align="center">
15
+ <strong>Knowledge graph CLI for coding agents</strong>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-f472b6" alt="Bun"></a>
20
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
21
+ </p>
22
+
23
+ Lattice builds a knowledge graph of your codebase that coding agents query instead of grep and file reading. Agents get precise, scoped context — a flow's call tree, a function's callers, the impact of a change — in minimal tokens. No more reading entire files to understand three functions.
24
+
25
+ ## The Problem
26
+
27
+ Coding agents today dump raw source code into their context windows. They grep for keywords, read whole files, and hope the relevant code is somewhere in the noise. This causes:
28
+
29
+ - **Context rot** — stale file contents from earlier exploration steps degrade attention
30
+ - **Token waste** — reading 500-line files to understand 20-line functions
31
+ - **Terminology mismatch** — searching "checkout timeout" fails when the code calls it `chargeCard`
32
+ - **Cold start** — every conversation starts from zero with no understanding of the codebase
33
+
34
+ ## The Solution
35
+
36
+ Embed lightweight annotations (`@lattice:` tags) in your source code, then let Lattice build a graph that agents traverse instead of searching.
37
+
38
+ ```python
39
+ # @lattice:flow checkout
40
+ @app.post("/api/checkout")
41
+ def handle_checkout(req):
42
+ order = create_order(req) # ← no tag needed, derived from call graph
43
+ return order
44
+
45
+ # @lattice:boundary stripe
46
+ def charge(amount, token):
47
+ return stripe.charges.create(amount=amount, source=token)
48
+
49
+ # @lattice:emits order.created
50
+ def emit_order_created(order_id):
51
+ queue.publish("order.created", {"order_id": order_id})
52
+ ```
53
+
54
+ Four tags, placed only at entry points and boundaries. Everything in between is derived from the AST.
55
+
56
+ ## Installation
57
+
58
+ ### bun (recommended)
59
+
60
+ ```bash
61
+ bun add -g lattice-graph
62
+ ```
63
+
64
+ ### npx (no install)
65
+
66
+ ```bash
67
+ bunx lattice-graph init
68
+ bunx lattice-graph build
69
+ ```
70
+
71
+ ### From source
72
+
73
+ ```bash
74
+ git clone https://github.com/your-org/lattice.git
75
+ cd lattice
76
+ bun install
77
+ bun run build # compiles to ./lattice binary
78
+ ```
79
+
80
+ ## Quick Start
81
+
82
+ ```bash
83
+ # Initialize Lattice in your project
84
+ cd your-project
85
+ lattice init # creates .lattice/ and lattice.toml
86
+
87
+ # Build the knowledge graph
88
+ lattice build # parses all files, builds SQLite graph
89
+
90
+ # Query the graph
91
+ lattice overview # project landscape: flows, boundaries, events
92
+ lattice flow checkout # full call tree from entry point to boundaries
93
+ lattice context charge # callers, callees, flows, boundary info
94
+ lattice impact charge # what breaks if you change this function
95
+ ```
96
+
97
+ ## Tags
98
+
99
+ Lattice uses four tags placed in comments directly above function definitions. Tags capture what the AST cannot — business flow entry points, external system boundaries, and invisible runtime connections.
100
+
101
+ | Tag | Purpose | Example |
102
+ |-----|---------|---------|
103
+ | `@lattice:flow <name>` | Marks a flow entry point | `# @lattice:flow checkout` |
104
+ | `@lattice:boundary <system>` | Marks where code exits the codebase | `# @lattice:boundary stripe` |
105
+ | `@lattice:emits <event>` | Marks event/message emission | `# @lattice:emits order.created` |
106
+ | `@lattice:handles <event>` | Marks event/message consumption | `# @lattice:handles order.created` |
107
+
108
+ **What you tag:** Route handlers, CLI commands, cron jobs, external API calls, database operations, event publishers, event consumers.
109
+
110
+ **What you don't tag:** Everything else. Intermediate functions, callers, callees, types — all derived automatically from the call graph.
111
+
112
+ ### Syntax rules
113
+
114
+ - Tags go in the comment block directly above a function definition
115
+ - No blank lines between the tag comment and the function
116
+ - Names are kebab-case: `user-registration`, `order.created`, `aws-s3`
117
+ - Multiple values: `# @lattice:flow checkout, payment`
118
+ - Works with any comment style: `#`, `//`, `/* */`, `--`
119
+
120
+ ### How flow propagation works
121
+
122
+ Only flow entry points are tagged. Lattice traces the call graph from the entry point and automatically includes every function in the chain:
123
+
124
+ ```
125
+ FLOW: checkout
126
+ handle_checkout → create_order → charge [BOUNDARY: stripe]
127
+ → save_order [BOUNDARY: postgres]
128
+ → emits order.created
129
+ ↓ (event edge)
130
+ send_confirmation
131
+ ```
132
+
133
+ `create_order`, `charge`, `save_order` — none of these need tags. Their flow membership is derived.
134
+
135
+ ## Commands
136
+
137
+ ### Build Commands
138
+
139
+ | Command | Description |
140
+ |---------|-------------|
141
+ | `lattice init` | Initialize `.lattice/` in a project, detect languages |
142
+ | `lattice build` | Full index: parse all files, build knowledge graph |
143
+ | `lattice update` | Incremental: re-index only files changed since last build |
144
+ | `lattice lint` | Validate tags: syntax, typos, orphans, missing tags |
145
+ | `lattice populate` | Output agent instructions for tagging the codebase |
146
+
147
+ ### Query Commands
148
+
149
+ | Command | Description |
150
+ |---------|-------------|
151
+ | `lattice overview` | Project landscape: flows, boundaries, event connections |
152
+ | `lattice flows` | List all flows with their entry points |
153
+ | `lattice flow <name>` | Full call tree from a flow's entry point to its boundaries |
154
+ | `lattice context <symbol>` | Symbol neighborhood: callers, callees, flows, boundary |
155
+ | `lattice callers <symbol>` | What calls this symbol |
156
+ | `lattice callees <symbol>` | What this symbol calls |
157
+ | `lattice trace <flow> --to <boundary>` | Call chain from flow entry to a specific boundary |
158
+ | `lattice impact <symbol>` | Everything affected if this symbol changes |
159
+ | `lattice boundaries` | All external system boundaries |
160
+ | `lattice events` | All event connections (emits → handles) |
161
+ | `lattice code <symbol>` | Source code of a specific function |
162
+
163
+ All query commands support `--json` for programmatic consumption.
164
+
165
+ ## Example Output
166
+
167
+ ### `lattice overview`
168
+
169
+ ```
170
+ Flows:
171
+ checkout → POST /api/checkout (src/routes/checkout.py:4)
172
+
173
+ Boundaries:
174
+ stripe → 1 function across 1 file
175
+ postgres → 1 function across 1 file
176
+
177
+ Events:
178
+ order.created → emitted by emit_order_created, handled by send_confirmation
179
+ ```
180
+
181
+ ### `lattice flow checkout`
182
+
183
+ ```
184
+ handle_checkout (src/routes/checkout.py:4)
185
+ → create_order (src/services/order.py:4)
186
+ → save_order (src/db/orders.py:4) [postgres]
187
+ → charge (src/gateways/payment.py:4) [stripe]
188
+ → build_stripe_payload (src/gateways/payment.py:8)
189
+ → emit_order_created (src/services/order.py:10) emits order.created
190
+ → send_confirmation (src/workers/email.py:2)
191
+ ```
192
+
193
+ ### `lattice context charge`
194
+
195
+ ```
196
+ charge (src/gateways/payment.py:4)
197
+ signature: charge(amount: float, token: str) -> dict
198
+ flows: checkout (derived)
199
+
200
+ callers:
201
+ ← create_order (src/services/order.py:4)
202
+
203
+ callees:
204
+ → build_stripe_payload (src/gateways/payment.py:8)
205
+
206
+ boundary: stripe
207
+ ```
208
+
209
+ ### `lattice impact charge`
210
+
211
+ ```
212
+ Direct callers:
213
+ ← create_order (src/services/order.py:4)
214
+
215
+ Transitive callers:
216
+ ← handle_checkout (src/routes/checkout.py:4)
217
+
218
+ Affected flows: checkout
219
+ ```
220
+
221
+ ## Agent Workflow
222
+
223
+ Instead of grepping and reading files, an agent using Lattice follows this flow:
224
+
225
+ 1. **Orient** — `lattice overview` to understand the project landscape
226
+ 2. **Locate** — `lattice flow <name>` to see the relevant call tree
227
+ 3. **Understand** — `lattice context <symbol>` for a specific function's neighborhood
228
+ 4. **Scope** — `lattice impact <symbol>` to know what's affected by a change
229
+ 5. **Edit** — `lattice code <symbol>` to read only the function being modified
230
+
231
+ Total context consumed: ~200-500 tokens instead of 5,000-50,000 from reading files.
232
+
233
+ ## Tagging Your Codebase
234
+
235
+ ### Automated
236
+
237
+ ```bash
238
+ lattice build # build graph without tags first
239
+ lattice populate # outputs instructions for a coding agent
240
+ ```
241
+
242
+ `lattice populate` generates a structured prompt that tells a coding agent exactly what to tag, where, and with what values. The agent reads the instructions, adds the tags, then validates:
243
+
244
+ ```bash
245
+ lattice build && lattice lint # rebuild graph and validate tags
246
+ ```
247
+
248
+ ### Linting
249
+
250
+ ```bash
251
+ lattice lint # check for missing, invalid, stale tags
252
+ lattice lint --strict # treat warnings as errors (for CI)
253
+ lattice lint --unresolved # show unresolved reference details
254
+ ```
255
+
256
+ The linter detects:
257
+ - **Missing tags** — route handlers without `@lattice:flow`, external calls without `@lattice:boundary`
258
+ - **Invalid tags** — tags on classes instead of functions, missing values
259
+ - **Typos** — `@lattice:flow chekout` when `checkout` exists elsewhere
260
+ - **Orphaned events** — emits without handlers, handlers without emitters
261
+ - **Stale tags** — boundary tags on functions that no longer call the package
262
+
263
+ ## How It Works
264
+
265
+ 1. **Tree-sitter** parses source files into ASTs (Python and TypeScript supported)
266
+ 2. **Extractors** walk the AST to extract symbols (functions, classes, methods), call edges, imports, and framework patterns
267
+ 3. **Tag parser** reads `@lattice:` comments and associates them with the function below
268
+ 4. **Graph builder** inserts everything into a SQLite database with nodes, edges, and tags
269
+ 5. **Event synthesis** creates invisible edges from `@lattice:emits` to `@lattice:handles` nodes
270
+ 6. **Cross-file resolution** matches callee names to known symbols across the codebase
271
+ 7. **CLI queries** traverse the graph and return compact, scoped results
272
+
273
+ The graph is stored at `.lattice/graph.db` — a single SQLite file.
274
+
275
+ ## Configuration
276
+
277
+ `lattice.toml` in your project root:
278
+
279
+ ```toml
280
+ [project]
281
+ languages = ["python", "typescript"]
282
+ root = "src"
283
+ exclude = ["node_modules", "venv", ".git", "dist", "__pycache__"]
284
+
285
+ [python]
286
+ source_roots = ["src"]
287
+ test_paths = ["tests"]
288
+ frameworks = ["fastapi"]
289
+
290
+ [typescript]
291
+ source_roots = ["src"]
292
+ test_paths = ["__tests__"]
293
+ frameworks = ["express"]
294
+
295
+ [lint]
296
+ strict = false
297
+ ignore = ["scripts/**"]
298
+
299
+ [lint.boundaries]
300
+ packages = ["stripe", "boto3", "psycopg2", "requests", "sendgrid"]
301
+ ```
302
+
303
+ ## Supported Languages
304
+
305
+ | Language | Status | Frameworks |
306
+ |----------|--------|------------|
307
+ | Python | Supported | FastAPI, Flask, Django, Celery |
308
+ | TypeScript | Supported | Express, NestJS, Next.js |
309
+
310
+ Adding a new language requires implementing one extractor. The graph schema, CLI commands, linter, and output formatting are all language-agnostic.
311
+
312
+ ## Agent Integration (Claude Code)
313
+
314
+ To make coding agents use Lattice instead of grep/read for codebase navigation, add these files to your project.
315
+
316
+ ### `.claude/CLAUDE.md`
317
+
318
+ ```markdown
319
+ # Codebase Navigation
320
+
321
+ This project uses Lattice for codebase navigation. Before using Grep, Glob, or
322
+ reading files to understand code, use Lattice commands via Bash.
323
+
324
+ ## Workflow for any task
325
+
326
+ 1. Orient: lattice overview
327
+ 2. Locate: lattice flow <name>
328
+ 3. Understand: lattice context <symbol>
329
+ 4. Scope: lattice impact <symbol>
330
+ 5. Read: lattice code <symbol>
331
+ 6. Edit: Read/Edit tools on the specific file and line range
332
+
333
+ ## When to use Lattice vs traditional tools
334
+
335
+ - Understand a function -> lattice context <symbol> (not Grep)
336
+ - Find callers -> lattice callers <symbol> (not Grep)
337
+ - See a business flow -> lattice flow <name> (not reading files)
338
+ - Check change impact -> lattice impact <symbol> (not Grep for usages)
339
+ - Read code to edit -> lattice code <symbol> (not Read on whole file)
340
+ - Search for string literal -> Grep (this is fine)
341
+ - Find config files -> Glob (this is fine)
342
+
343
+ ## After code changes
344
+
345
+ lattice update # incremental re-index
346
+ lattice build && lattice lint # after adding/changing tags
347
+ ```
348
+
349
+ ### `.claude/settings.json`
350
+
351
+ ```json
352
+ {
353
+ "hooks": {
354
+ "PreToolUse": [
355
+ {
356
+ "matcher": "Grep",
357
+ "command": "echo \"STOP: Before using Grep, check if Lattice can answer your question faster:\n lattice context <symbol> - function neighborhood\n lattice callers <symbol> - what calls this\n lattice flow <name> - full call tree\n lattice impact <symbol> - change impact\nGrep is appropriate for: string literals, config values, error messages.\nGrep is NOT appropriate for: finding function definitions, understanding call chains, tracing flows.\""
358
+ },
359
+ {
360
+ "matcher": "Glob",
361
+ "command": "echo \"STOP: Before using Glob, check if Lattice can answer your question faster:\n lattice overview - all flows, boundaries, events\n lattice flows - all entry points\n lattice boundaries - all external systems\n lattice context <symbol> - where a symbol lives\nGlob is appropriate for: config files, assets, non-code files.\nGlob is NOT appropriate for: finding source files or function locations.\""
362
+ }
363
+ ]
364
+ }
365
+ }
366
+ ```
367
+
368
+ The hooks remind the agent to try Lattice before falling back to traditional tools. They don't block — they guide.
369
+
370
+ ## Requirements
371
+
372
+ - [Bun](https://bun.sh) >= 1.0 (for development and compilation)
373
+ - No runtime dependencies for the compiled binary (except WASM grammars in `node_modules/`)
374
+
375
+ ## Development
376
+
377
+ ```bash
378
+ bun install # install dependencies + git hooks
379
+ bun run dev # run CLI in development mode
380
+ bun run test # run 182 tests
381
+ bun run lint # biome check
382
+ bun run typecheck # tsc --noEmit
383
+ bun run check # lint + typecheck + tests
384
+ bun run build # compile to single binary
385
+ ```
386
+
387
+ Pre-commit hooks enforce Biome formatting and TypeScript type checking. Pre-push hooks run the full test suite.
388
+
389
+ ## License
390
+
391
+ MIT
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "lattice-graph",
3
+ "version": "0.1.0",
4
+ "description": "Knowledge graph CLI for coding agents — navigate code through flows, not grep.",
5
+ "module": "src/main.ts",
6
+ "type": "module",
7
+ "bin": {
8
+ "lattice": "src/main.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "dev": "bun src/main.ts",
17
+ "test": "bun test",
18
+ "lint": "bunx biome check src/ tests/",
19
+ "format": "bunx biome check --write src/ tests/",
20
+ "typecheck": "bun tsc --noEmit",
21
+ "check": "bun run lint && bun run typecheck && bun run test",
22
+ "build": "bun build src/main.ts --compile --outfile lattice",
23
+ "prepare": "bunx lefthook install"
24
+ },
25
+ "keywords": [
26
+ "cli",
27
+ "knowledge-graph",
28
+ "coding-agent",
29
+ "code-navigation",
30
+ "tree-sitter",
31
+ "ast",
32
+ "static-analysis"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/your-org/lattice"
38
+ },
39
+ "engines": {
40
+ "bun": ">=1.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@biomejs/biome": "^2.4.8",
44
+ "@types/bun": "latest",
45
+ "lefthook": "^2.1.4"
46
+ },
47
+ "peerDependencies": {
48
+ "typescript": "^5"
49
+ },
50
+ "dependencies": {
51
+ "commander": "^14.0.3",
52
+ "smol-toml": "^1.6.0",
53
+ "tree-sitter-wasms": "0.1.11",
54
+ "web-tree-sitter": "0.24.7"
55
+ }
56
+ }
@@ -0,0 +1,208 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { mkdirSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import type { Extractor } from "../extract/extractor.ts";
5
+ import { initTreeSitter } from "../extract/parser.ts";
6
+ import { createPythonExtractor } from "../extract/python/extractor.ts";
7
+ import { createTypeScriptExtractor } from "../extract/typescript/extractor.ts";
8
+ import { createDatabase } from "../graph/database.ts";
9
+ import {
10
+ insertEdges,
11
+ insertNodes,
12
+ insertTags,
13
+ insertUnresolved,
14
+ synthesizeEventEdges,
15
+ } from "../graph/writer.ts";
16
+ import type { LatticeConfig } from "../types/config.ts";
17
+ import type { ExtractionResult } from "../types/graph.ts";
18
+ import { err, ok, type Result } from "../types/result.ts";
19
+
20
+ /** Statistics from a completed build. */
21
+ type BuildStats = {
22
+ readonly fileCount: number;
23
+ readonly nodeCount: number;
24
+ readonly edgeCount: number;
25
+ readonly tagCount: number;
26
+ readonly eventEdgeCount: number;
27
+ readonly durationMs: number;
28
+ };
29
+
30
+ /**
31
+ * Executes a full build of the knowledge graph.
32
+ * Walks the source tree, runs extractors on all matching files,
33
+ * inserts results into SQLite, and synthesizes event edges.
34
+ *
35
+ * @param projectRoot - Absolute or relative path to the project root
36
+ * @param config - Parsed Lattice configuration
37
+ * @returns Build statistics or an error message
38
+ */
39
+ // @lattice:flow build
40
+ async function executeBuild(
41
+ projectRoot: string,
42
+ config: LatticeConfig,
43
+ ): Promise<Result<BuildStats, string>> {
44
+ const startTime = Date.now();
45
+
46
+ try {
47
+ // Initialize tree-sitter and create extractors
48
+ await initTreeSitter();
49
+ const extractors = await createExtractors(config);
50
+ if (extractors.length === 0) {
51
+ return err("No extractors available for configured languages");
52
+ }
53
+
54
+ // Build extension → extractor mapping
55
+ const extByExt = new Map<string, Extractor>();
56
+ for (const ext of extractors) {
57
+ for (const fileExt of ext.fileExtensions) {
58
+ extByExt.set(fileExt, ext);
59
+ }
60
+ }
61
+
62
+ // Create .lattice directory and database
63
+ const latticeDir = join(projectRoot, ".lattice");
64
+ mkdirSync(latticeDir, { recursive: true });
65
+ const dbPath = join(latticeDir, "graph.db");
66
+ const db = createDatabase(dbPath);
67
+
68
+ // Walk the source tree
69
+ const sourceRoot = join(projectRoot, config.root);
70
+ const glob = new Bun.Glob("**/*");
71
+ const files: string[] = [];
72
+
73
+ for await (const path of glob.scan({ cwd: sourceRoot, dot: false })) {
74
+ const ext = `.${path.split(".").pop()}`;
75
+ if (!extByExt.has(ext)) continue;
76
+ if (isExcluded(path, config.exclude)) continue;
77
+ files.push(path);
78
+ }
79
+
80
+ // Extract all files
81
+ let totalNodes = 0;
82
+ let totalEdges = 0;
83
+ let totalTags = 0;
84
+
85
+ for (const file of files) {
86
+ const ext = `.${file.split(".").pop()}`;
87
+ const extractor = extByExt.get(ext);
88
+ if (!extractor) continue;
89
+
90
+ const fullPath = join(sourceRoot, file);
91
+ const source = await Bun.file(fullPath).text();
92
+ const relativePath = relative(projectRoot, fullPath);
93
+
94
+ const result: ExtractionResult = await extractor.extract(relativePath, source);
95
+
96
+ insertNodes(db, result.nodes);
97
+ insertEdges(db, result.edges);
98
+ insertTags(db, result.tags);
99
+ insertUnresolved(db, result.unresolved);
100
+
101
+ totalNodes += result.nodes.length;
102
+ totalEdges += result.edges.length;
103
+ totalTags += result.tags.length;
104
+ }
105
+
106
+ // Cross-file resolution: resolve uncertain edges by matching callee names to known nodes
107
+ resolveCrossFileEdges(db);
108
+
109
+ // Synthesize event edges
110
+ synthesizeEventEdges(db);
111
+ const eventEdgeRow = db.query("SELECT COUNT(*) as c FROM edges WHERE kind = 'event'").get() as {
112
+ c: number;
113
+ };
114
+
115
+ // Write build metadata
116
+ const now = new Date().toISOString();
117
+ db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_build', ?)", [now]);
118
+ db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('lattice_version', '0.1.0')");
119
+
120
+ db.close();
121
+
122
+ return ok({
123
+ fileCount: files.length,
124
+ nodeCount: totalNodes,
125
+ edgeCount: totalEdges + eventEdgeRow.c,
126
+ tagCount: totalTags,
127
+ eventEdgeCount: eventEdgeRow.c,
128
+ durationMs: Date.now() - startTime,
129
+ });
130
+ } catch (error) {
131
+ return err(`Build failed: ${error instanceof Error ? error.message : String(error)}`);
132
+ }
133
+ }
134
+
135
+ /** Creates extractors for all configured languages. */
136
+ async function createExtractors(config: LatticeConfig): Promise<readonly Extractor[]> {
137
+ const extractors: Extractor[] = [];
138
+ for (const lang of config.languages) {
139
+ if (lang === "python") {
140
+ extractors.push(await createPythonExtractor());
141
+ }
142
+ if (lang === "typescript") {
143
+ extractors.push(await createTypeScriptExtractor());
144
+ }
145
+ }
146
+ return extractors;
147
+ }
148
+
149
+ /**
150
+ * Resolves uncertain cross-file edges by matching callee names to known nodes.
151
+ * If a callee name (e.g., "create_order") matches exactly one node name in the graph,
152
+ * the edge target is updated to the full node ID.
153
+ */
154
+ function resolveCrossFileEdges(db: Database): void {
155
+ // Find all uncertain edges where target_id is not a known node
156
+ const uncertainEdges = db
157
+ .query(
158
+ `SELECT e.rowid, e.source_id, e.target_id FROM edges e
159
+ WHERE e.certainty = 'uncertain'
160
+ AND NOT EXISTS (SELECT 1 FROM nodes WHERE id = e.target_id)`,
161
+ )
162
+ .all() as { rowid: number; source_id: string; target_id: string }[];
163
+
164
+ // Build a name→id map for all nodes (only keep unambiguous names)
165
+ const allNodes = db.query("SELECT id, name FROM nodes").all() as { id: string; name: string }[];
166
+ const nameToIds = new Map<string, string[]>();
167
+ for (const node of allNodes) {
168
+ const existing = nameToIds.get(node.name);
169
+ if (existing) {
170
+ existing.push(node.id);
171
+ } else {
172
+ nameToIds.set(node.name, [node.id]);
173
+ }
174
+ }
175
+
176
+ const deleteStmt = db.prepare("DELETE FROM edges WHERE rowid = ?");
177
+ const insertStmt = db.prepare(
178
+ "INSERT OR IGNORE INTO edges (source_id, target_id, kind, certainty) VALUES (?, ?, 'calls', 'certain')",
179
+ );
180
+
181
+ const tx = db.transaction(() => {
182
+ for (const edge of uncertainEdges) {
183
+ // Try to resolve the callee name
184
+ const calleeName = edge.target_id.split(".").pop() ?? edge.target_id;
185
+ const candidates = nameToIds.get(calleeName);
186
+
187
+ if (candidates && candidates.length === 1 && candidates[0]) {
188
+ // Unambiguous match — replace the uncertain edge
189
+ deleteStmt.run(edge.rowid);
190
+ insertStmt.run(edge.source_id, candidates[0]);
191
+ }
192
+ // If ambiguous or not found, leave the uncertain edge as-is
193
+ }
194
+ });
195
+ tx();
196
+ }
197
+
198
+ /** Checks if a file path matches any exclude pattern. */
199
+ function isExcluded(filePath: string, excludePatterns: readonly string[]): boolean {
200
+ for (const pattern of excludePatterns) {
201
+ if (filePath.includes(pattern.replace("**", "").replace("*", ""))) {
202
+ return true;
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+
208
+ export { type BuildStats, executeBuild };