lattice-graph 0.1.0 → 0.3.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/README.md +69 -25
- package/package.json +8 -9
- package/scripts/postinstall.ts +69 -0
- package/src/commands/build.ts +44 -173
- package/src/commands/init.ts +39 -17
- package/src/commands/lint.ts +145 -53
- package/src/commands/populate.ts +14 -3
- package/src/commands/update.ts +75 -137
- package/src/config.ts +0 -2
- package/src/extract/tag-scanner.ts +94 -0
- package/src/files.ts +56 -0
- package/src/graph/database.ts +6 -8
- package/src/graph/writer.ts +17 -21
- package/src/lsp/builder.ts +248 -0
- package/src/lsp/calls.ts +84 -0
- package/src/lsp/client.ts +211 -0
- package/src/lsp/symbols.ts +146 -0
- package/src/lsp/types.ts +73 -0
- package/src/main.ts +2 -18
- package/src/types/config.ts +0 -2
- package/src/types/graph.ts +6 -34
- package/src/types/lint.ts +0 -1
- package/src/extract/extractor.ts +0 -13
- package/src/extract/parser.ts +0 -117
- package/src/extract/python/calls.ts +0 -121
- package/src/extract/python/extractor.ts +0 -171
- package/src/extract/python/frameworks.ts +0 -142
- package/src/extract/python/imports.ts +0 -115
- package/src/extract/python/symbols.ts +0 -121
- package/src/extract/tags.ts +0 -77
- package/src/extract/typescript/calls.ts +0 -110
- package/src/extract/typescript/extractor.ts +0 -130
- package/src/extract/typescript/imports.ts +0 -71
- package/src/extract/typescript/symbols.ts +0 -252
package/README.md
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
<table align="center"><tr><td>
|
|
2
2
|
<pre>
|
|
3
|
-
|
|
4
3
|
██╗ █████╗ ████████╗████████╗██╗ ██████╗███████╗
|
|
5
4
|
██║ ██╔══██╗╚══██╔══╝╚══██╔══╝██║██╔════╝██╔════╝
|
|
6
5
|
██║ ███████║ ██║ ██║ ██║██║ █████╗
|
|
7
6
|
██║ ██╔══██║ ██║ ██║ ██║██║ ██╔══╝
|
|
8
7
|
███████╗██║ ██║ ██║ ██║ ██║╚██████╗███████╗
|
|
9
8
|
╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝
|
|
10
|
-
|
|
11
9
|
</pre>
|
|
12
10
|
</td></tr></table>
|
|
13
11
|
|
|
@@ -16,6 +14,7 @@
|
|
|
16
14
|
</p>
|
|
17
15
|
|
|
18
16
|
<p align="center">
|
|
17
|
+
<a href="https://www.npmjs.com/package/lattice-graph"><img src="https://img.shields.io/npm/v/lattice-graph" alt="npm"></a>
|
|
19
18
|
<a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-f472b6" alt="Bun"></a>
|
|
20
19
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
|
|
21
20
|
</p>
|
|
@@ -71,7 +70,7 @@ bunx lattice-graph build
|
|
|
71
70
|
### From source
|
|
72
71
|
|
|
73
72
|
```bash
|
|
74
|
-
git clone https://github.com/
|
|
73
|
+
git clone https://github.com/kilupskalvis/lattice.git
|
|
75
74
|
cd lattice
|
|
76
75
|
bun install
|
|
77
76
|
bun run build # compiles to ./lattice binary
|
|
@@ -80,18 +79,26 @@ bun run build # compiles to ./lattice binary
|
|
|
80
79
|
## Quick Start
|
|
81
80
|
|
|
82
81
|
```bash
|
|
83
|
-
# Initialize Lattice in your project
|
|
82
|
+
# 1. Initialize Lattice in your project
|
|
84
83
|
cd your-project
|
|
85
84
|
lattice init # creates .lattice/ and lattice.toml
|
|
85
|
+
# Edit lattice.toml to set the correct root, frameworks, etc.
|
|
86
86
|
|
|
87
|
-
# Build the
|
|
88
|
-
lattice build # parses all files
|
|
87
|
+
# 2. Build the structural graph
|
|
88
|
+
lattice build # parses all files into a knowledge graph
|
|
89
89
|
|
|
90
|
-
#
|
|
90
|
+
# 3. Tag your codebase (give this output to your coding agent)
|
|
91
|
+
lattice populate # outputs tag spec, examples, and workflow
|
|
92
|
+
# The agent reads the instructions, adds @lattice: tags to your source code,
|
|
93
|
+
# then validates with:
|
|
94
|
+
lattice build && lattice lint # rebuild and check for errors
|
|
95
|
+
|
|
96
|
+
# 4. Now the agent can navigate the codebase through the graph
|
|
91
97
|
lattice overview # project landscape: flows, boundaries, events
|
|
92
98
|
lattice flow checkout # full call tree from entry point to boundaries
|
|
93
99
|
lattice context charge # callers, callees, flows, boundary info
|
|
94
100
|
lattice impact charge # what breaks if you change this function
|
|
101
|
+
lattice code charge # read just the function source to edit
|
|
95
102
|
```
|
|
96
103
|
|
|
97
104
|
## Tags
|
|
@@ -132,6 +139,48 @@ FLOW: checkout
|
|
|
132
139
|
|
|
133
140
|
`create_order`, `charge`, `save_order` — none of these need tags. Their flow membership is derived.
|
|
134
141
|
|
|
142
|
+
### Async job dispatch patterns
|
|
143
|
+
|
|
144
|
+
For systems that use message queues (SQS, RabbitMQ), Lambda invocations, or Celery tasks, use `emits`/`handles` to connect the submission side to the processing side:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# @lattice:flow ingest-vacancies
|
|
148
|
+
@router.post("/vacancies")
|
|
149
|
+
def create_vacancies(request, service):
|
|
150
|
+
job = service.submit_ingest(request.data)
|
|
151
|
+
return {"job_id": job.id}
|
|
152
|
+
|
|
153
|
+
# @lattice:emits job.ingest-vacancies
|
|
154
|
+
def submit_ingest(self, data):
|
|
155
|
+
self.queue.submit(data) # submits to SQS
|
|
156
|
+
|
|
157
|
+
# @lattice:handles job.ingest-vacancies
|
|
158
|
+
def process_ingest_vacancies(data, services):
|
|
159
|
+
normalized = services.normalize(data) # calls OpenAI
|
|
160
|
+
services.writer.index(normalized) # writes to Weaviate
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
With these tags, `lattice flow ingest-vacancies` shows the complete chain from the API route through the queue to the worker, including normalization and indexing. Without the `emits`/`handles` tags, the flow tree stops at the queue submission.
|
|
164
|
+
|
|
165
|
+
**Key rule:** Worker handlers, Lambda consumers, and background job processors are NOT separate flows — they are the receiving side of an async dispatch. Tag them with `@lattice:handles`, not `@lattice:flow`.
|
|
166
|
+
|
|
167
|
+
### Tags and protocols/interfaces
|
|
168
|
+
|
|
169
|
+
When using dependency injection or protocol-based dispatch, place `emits` tags on the function that the flow actually passes through — not on a concrete implementation behind an interface:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
# Good: tag is on the function the flow reaches
|
|
173
|
+
# @lattice:emits job.sync
|
|
174
|
+
def submit_sync(self):
|
|
175
|
+
self._invoker.invoke_sync()
|
|
176
|
+
|
|
177
|
+
# Bad: tag on concrete class that flow doesn't reach directly
|
|
178
|
+
class LambdaInvoker:
|
|
179
|
+
# @lattice:emits job.sync ← flow resolves to protocol, not this
|
|
180
|
+
def invoke_sync(self):
|
|
181
|
+
lambda_client.invoke(...)
|
|
182
|
+
```
|
|
183
|
+
|
|
135
184
|
## Commands
|
|
136
185
|
|
|
137
186
|
### Build Commands
|
|
@@ -262,13 +311,12 @@ The linter detects:
|
|
|
262
311
|
|
|
263
312
|
## How It Works
|
|
264
313
|
|
|
265
|
-
1. **
|
|
266
|
-
2. **
|
|
267
|
-
3. **Tag
|
|
314
|
+
1. **LSP servers** (`typescript-language-server` for TypeScript, `zubanls` for Python) provide type-checked symbol and call information
|
|
315
|
+
2. **Combined edge strategies** — both `outgoingCalls` and `references` are used to maximize edge coverage across files
|
|
316
|
+
3. **Tag scanner** reads `@lattice:` comments and associates them with the function below
|
|
268
317
|
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. **
|
|
271
|
-
7. **CLI queries** traverse the graph and return compact, scoped results
|
|
318
|
+
5. **Event synthesis** creates invisible edges from `@lattice:emits` to `@lattice:handles` nodes, connecting async flows
|
|
319
|
+
6. **CLI queries** traverse the graph and return compact, scoped results
|
|
272
320
|
|
|
273
321
|
The graph is stored at `.lattice/graph.db` — a single SQLite file.
|
|
274
322
|
|
|
@@ -285,29 +333,24 @@ exclude = ["node_modules", "venv", ".git", "dist", "__pycache__"]
|
|
|
285
333
|
[python]
|
|
286
334
|
source_roots = ["src"]
|
|
287
335
|
test_paths = ["tests"]
|
|
288
|
-
frameworks = ["fastapi"]
|
|
289
336
|
|
|
290
337
|
[typescript]
|
|
291
338
|
source_roots = ["src"]
|
|
292
339
|
test_paths = ["__tests__"]
|
|
293
|
-
frameworks = ["express"]
|
|
294
340
|
|
|
295
341
|
[lint]
|
|
296
342
|
strict = false
|
|
297
343
|
ignore = ["scripts/**"]
|
|
298
|
-
|
|
299
|
-
[lint.boundaries]
|
|
300
|
-
packages = ["stripe", "boto3", "psycopg2", "requests", "sendgrid"]
|
|
301
344
|
```
|
|
302
345
|
|
|
303
346
|
## Supported Languages
|
|
304
347
|
|
|
305
|
-
| Language |
|
|
306
|
-
|
|
307
|
-
| Python |
|
|
308
|
-
| TypeScript |
|
|
348
|
+
| Language | LSP Server | Status |
|
|
349
|
+
|----------|-----------|--------|
|
|
350
|
+
| Python | [zuban](https://github.com/zubanls/zuban) | Supported |
|
|
351
|
+
| TypeScript | typescript-language-server | Supported |
|
|
309
352
|
|
|
310
|
-
Adding a new language requires
|
|
353
|
+
Adding a new language requires adding its LSP server to the defaults. The graph schema, CLI commands, linter, and output formatting are all language-agnostic.
|
|
311
354
|
|
|
312
355
|
## Agent Integration (Claude Code)
|
|
313
356
|
|
|
@@ -370,14 +413,15 @@ The hooks remind the agent to try Lattice before falling back to traditional too
|
|
|
370
413
|
## Requirements
|
|
371
414
|
|
|
372
415
|
- [Bun](https://bun.sh) >= 1.0 (for development and compilation)
|
|
373
|
-
-
|
|
416
|
+
- For TypeScript: `npm install -g typescript-language-server typescript`
|
|
417
|
+
- For Python: `pip install zuban`
|
|
374
418
|
|
|
375
419
|
## Development
|
|
376
420
|
|
|
377
421
|
```bash
|
|
378
422
|
bun install # install dependencies + git hooks
|
|
379
423
|
bun run dev # run CLI in development mode
|
|
380
|
-
bun run test # run
|
|
424
|
+
bun run test # run 187 tests
|
|
381
425
|
bun run lint # biome check
|
|
382
426
|
bun run typecheck # tsc --noEmit
|
|
383
427
|
bun run check # lint + typecheck + tests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lattice-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Knowledge graph CLI for coding agents — navigate code through flows, not grep.",
|
|
5
5
|
"module": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src",
|
|
12
|
+
"scripts",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
17
|
+
"postinstall": "bun scripts/postinstall.ts",
|
|
16
18
|
"dev": "bun src/main.ts",
|
|
17
19
|
"test": "bun test",
|
|
18
20
|
"lint": "bunx biome check src/ tests/",
|
|
@@ -27,14 +29,14 @@
|
|
|
27
29
|
"knowledge-graph",
|
|
28
30
|
"coding-agent",
|
|
29
31
|
"code-navigation",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
+
"lsp",
|
|
33
|
+
"language-server",
|
|
32
34
|
"static-analysis"
|
|
33
35
|
],
|
|
34
36
|
"license": "MIT",
|
|
35
37
|
"repository": {
|
|
36
38
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/
|
|
39
|
+
"url": "https://github.com/kilupskalvis/lattice"
|
|
38
40
|
},
|
|
39
41
|
"engines": {
|
|
40
42
|
"bun": ">=1.0.0"
|
|
@@ -44,13 +46,10 @@
|
|
|
44
46
|
"@types/bun": "latest",
|
|
45
47
|
"lefthook": "^2.1.4"
|
|
46
48
|
},
|
|
47
|
-
"peerDependencies": {
|
|
48
|
-
"typescript": "^5"
|
|
49
|
-
},
|
|
50
49
|
"dependencies": {
|
|
51
50
|
"commander": "^14.0.3",
|
|
52
51
|
"smol-toml": "^1.6.0",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
52
|
+
"typescript": "^5",
|
|
53
|
+
"typescript-language-server": "^4"
|
|
55
54
|
}
|
|
56
55
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script — creates a minimal Python venv and installs zuban into it.
|
|
4
|
+
* This gives zuban a proper Python environment with typeshed stubs.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const VENDOR_DIR = join(import.meta.dir, "..", "vendor");
|
|
10
|
+
const VENV_DIR = join(VENDOR_DIR, "venv");
|
|
11
|
+
|
|
12
|
+
function findPython(): string | undefined {
|
|
13
|
+
for (const cmd of ["python3", "python"]) {
|
|
14
|
+
if (Bun.which(cmd)) return cmd;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const isWindows = process.platform === "win32";
|
|
21
|
+
const zubanBin = join(VENV_DIR, isWindows ? "Scripts" : "bin", isWindows ? "zubanls.exe" : "zubanls");
|
|
22
|
+
|
|
23
|
+
if (existsSync(zubanBin)) {
|
|
24
|
+
console.log("zuban already installed");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const python = findPython();
|
|
29
|
+
if (!python) {
|
|
30
|
+
console.warn("Warning: Python not found. Python support requires python3 in PATH.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log("Installing zuban for Python support...");
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Create venv
|
|
38
|
+
const venvResult = Bun.spawnSync([python, "-m", "venv", VENV_DIR], {
|
|
39
|
+
stdout: "ignore",
|
|
40
|
+
stderr: "pipe",
|
|
41
|
+
});
|
|
42
|
+
if (venvResult.exitCode !== 0) {
|
|
43
|
+
throw new Error(`Failed to create venv: ${venvResult.stderr.toString()}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Install zuban via pip
|
|
47
|
+
const pip = join(VENV_DIR, isWindows ? "Scripts" : "bin", "pip");
|
|
48
|
+
const pipResult = Bun.spawnSync([pip, "install", "zuban", "--quiet"], {
|
|
49
|
+
stdout: "ignore",
|
|
50
|
+
stderr: "pipe",
|
|
51
|
+
});
|
|
52
|
+
if (pipResult.exitCode !== 0) {
|
|
53
|
+
throw new Error(`Failed to install zuban: ${pipResult.stderr.toString()}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!existsSync(zubanBin)) {
|
|
57
|
+
throw new Error("zubanls binary not found after installation");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log("zuban installed successfully");
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.warn(
|
|
63
|
+
`Warning: Failed to install zuban: ${error instanceof Error ? error.message : error}. ` +
|
|
64
|
+
"Python support requires: pip install zuban",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
main();
|
package/src/commands/build.ts
CHANGED
|
@@ -1,39 +1,22 @@
|
|
|
1
|
-
import type { Database } from "bun:sqlite";
|
|
2
1
|
import { mkdirSync } from "node:fs";
|
|
3
|
-
import { join
|
|
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";
|
|
2
|
+
import { join } from "node:path";
|
|
8
3
|
import { createDatabase } from "../graph/database.ts";
|
|
9
4
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "../graph/writer.ts";
|
|
5
|
+
type BuildStats,
|
|
6
|
+
buildGraph,
|
|
7
|
+
buildLanguageConfig,
|
|
8
|
+
type LanguageConfig,
|
|
9
|
+
} from "../lsp/builder.ts";
|
|
16
10
|
import type { LatticeConfig } from "../types/config.ts";
|
|
17
|
-
import type { ExtractionResult } from "../types/graph.ts";
|
|
18
11
|
import { err, ok, type Result } from "../types/result.ts";
|
|
19
12
|
|
|
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
13
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
14
|
+
* Performs a full graph build using LSP extraction.
|
|
15
|
+
* Clears existing data, spawns a language server per configured language,
|
|
16
|
+
* extracts symbols and call hierarchy, scans for tags, and writes everything to SQLite.
|
|
34
17
|
*
|
|
35
|
-
* @param projectRoot - Absolute
|
|
36
|
-
* @param config - Parsed
|
|
18
|
+
* @param projectRoot - Absolute path to the project root
|
|
19
|
+
* @param config - Parsed lattice.toml configuration
|
|
37
20
|
* @returns Build statistics or an error message
|
|
38
21
|
*/
|
|
39
22
|
// @lattice:flow build
|
|
@@ -41,168 +24,56 @@ async function executeBuild(
|
|
|
41
24
|
projectRoot: string,
|
|
42
25
|
config: LatticeConfig,
|
|
43
26
|
): Promise<Result<BuildStats, string>> {
|
|
44
|
-
const startTime = Date.now();
|
|
45
|
-
|
|
46
27
|
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
28
|
const latticeDir = join(projectRoot, ".lattice");
|
|
64
29
|
mkdirSync(latticeDir, { recursive: true });
|
|
65
30
|
const dbPath = join(latticeDir, "graph.db");
|
|
66
31
|
const db = createDatabase(dbPath);
|
|
67
32
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|
|
33
|
+
db.run("DELETE FROM external_calls");
|
|
34
|
+
db.run("DELETE FROM tags");
|
|
35
|
+
db.run("DELETE FROM edges");
|
|
36
|
+
db.run("DELETE FROM nodes");
|
|
84
37
|
|
|
85
|
-
|
|
86
|
-
const ext = `.${file.split(".").pop()}`;
|
|
87
|
-
const extractor = extByExt.get(ext);
|
|
88
|
-
if (!extractor) continue;
|
|
38
|
+
const languageConfigs = buildLanguageConfigs(config);
|
|
89
39
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
};
|
|
40
|
+
const stats = await buildGraph({
|
|
41
|
+
projectRoot,
|
|
42
|
+
db,
|
|
43
|
+
languageConfigs,
|
|
44
|
+
exclude: config.exclude,
|
|
45
|
+
});
|
|
114
46
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('lattice_version', '0.1.0')");
|
|
47
|
+
db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_build', ?)", [
|
|
48
|
+
String(Date.now()),
|
|
49
|
+
]);
|
|
119
50
|
|
|
120
51
|
db.close();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
}
|
|
52
|
+
return ok(stats);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
145
55
|
}
|
|
146
|
-
return extractors;
|
|
147
56
|
}
|
|
148
57
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
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 }[];
|
|
58
|
+
/** Builds LanguageConfig entries from LatticeConfig for each configured language. */
|
|
59
|
+
function buildLanguageConfigs(config: LatticeConfig): readonly LanguageConfig[] {
|
|
60
|
+
const configs: LanguageConfig[] = [];
|
|
163
61
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
existing.push(node.id);
|
|
171
|
-
} else {
|
|
172
|
-
nameToIds.set(node.name, [node.id]);
|
|
173
|
-
}
|
|
62
|
+
if (config.languages.includes("typescript") && config.typescript) {
|
|
63
|
+
configs.push(
|
|
64
|
+
buildLanguageConfig("typescript", config.typescript.sourceRoots, config.typescript.testPaths),
|
|
65
|
+
);
|
|
66
|
+
} else if (config.languages.includes("typescript")) {
|
|
67
|
+
configs.push(buildLanguageConfig("typescript", [config.root], []));
|
|
174
68
|
}
|
|
175
69
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
}
|
|
70
|
+
if (config.languages.includes("python") && config.python) {
|
|
71
|
+
configs.push(buildLanguageConfig("python", config.python.sourceRoots, config.python.testPaths));
|
|
72
|
+
} else if (config.languages.includes("python")) {
|
|
73
|
+
configs.push(buildLanguageConfig("python", [config.root], ["tests"]));
|
|
204
74
|
}
|
|
205
|
-
|
|
75
|
+
|
|
76
|
+
return configs;
|
|
206
77
|
}
|
|
207
78
|
|
|
208
|
-
export {
|
|
79
|
+
export { executeBuild };
|
package/src/commands/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { err, ok, type Result } from "../types/result.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -16,16 +16,22 @@ function executeInit(projectRoot: string): Result<string, string> {
|
|
|
16
16
|
const latticeDir = join(projectRoot, ".lattice");
|
|
17
17
|
mkdirSync(latticeDir, { recursive: true });
|
|
18
18
|
|
|
19
|
+
// Detect languages
|
|
20
|
+
const languages = detectLanguages(projectRoot);
|
|
21
|
+
|
|
19
22
|
// Generate lattice.toml if it doesn't exist
|
|
20
23
|
const tomlPath = join(projectRoot, "lattice.toml");
|
|
21
24
|
if (!existsSync(tomlPath)) {
|
|
22
|
-
const languages = detectLanguages(projectRoot);
|
|
23
25
|
const root = detectRoot(projectRoot);
|
|
24
26
|
const toml = generateToml(languages, root);
|
|
25
27
|
writeFileSync(tomlPath, toml);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
// Check LSP server availability
|
|
31
|
+
const warnings = checkLspAvailability(languages);
|
|
32
|
+
const message = ["Initialized Lattice project", ...warnings].join("\n");
|
|
33
|
+
|
|
34
|
+
return ok(message);
|
|
29
35
|
} catch (error) {
|
|
30
36
|
return err(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
37
|
}
|
|
@@ -84,23 +90,11 @@ function generateToml(languages: readonly string[], root: string): string {
|
|
|
84
90
|
];
|
|
85
91
|
|
|
86
92
|
if (languages.includes("python")) {
|
|
87
|
-
lines.push(
|
|
88
|
-
"[python]",
|
|
89
|
-
`source_roots = ["${root}"]`,
|
|
90
|
-
'test_paths = ["tests"]',
|
|
91
|
-
"frameworks = []",
|
|
92
|
-
"",
|
|
93
|
-
);
|
|
93
|
+
lines.push("[python]", `source_roots = ["${root}"]`, 'test_paths = ["tests"]', "");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (languages.includes("typescript")) {
|
|
97
|
-
lines.push(
|
|
98
|
-
"[typescript]",
|
|
99
|
-
`source_roots = ["${root}"]`,
|
|
100
|
-
'test_paths = ["tests"]',
|
|
101
|
-
"frameworks = []",
|
|
102
|
-
"",
|
|
103
|
-
);
|
|
97
|
+
lines.push("[typescript]", `source_roots = ["${root}"]`, 'test_paths = ["tests"]', "");
|
|
104
98
|
}
|
|
105
99
|
|
|
106
100
|
lines.push("[lint]", "strict = false", "ignore = []", "");
|
|
@@ -108,4 +102,32 @@ function generateToml(languages: readonly string[], root: string): string {
|
|
|
108
102
|
return lines.join("\n");
|
|
109
103
|
}
|
|
110
104
|
|
|
105
|
+
/** Checks if language server binaries are available (bundled or in PATH). */
|
|
106
|
+
function checkLspAvailability(languages: readonly string[]): readonly string[] {
|
|
107
|
+
const warnings: string[] = [];
|
|
108
|
+
const latticeRoot = resolve(import.meta.dir, "..", "..");
|
|
109
|
+
|
|
110
|
+
const checks: Record<string, { bundled: string; system: string; install: string }> = {
|
|
111
|
+
typescript: {
|
|
112
|
+
bundled: join(latticeRoot, "node_modules", ".bin", "typescript-language-server"),
|
|
113
|
+
system: "typescript-language-server",
|
|
114
|
+
install: "reinstall lattice-graph",
|
|
115
|
+
},
|
|
116
|
+
python: {
|
|
117
|
+
bundled: join(latticeRoot, "vendor", "venv", "bin", "zubanls"),
|
|
118
|
+
system: "zubanls",
|
|
119
|
+
install: "reinstall lattice-graph (or pip install zuban)",
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const lang of languages) {
|
|
124
|
+
const check = checks[lang];
|
|
125
|
+
if (!check) continue;
|
|
126
|
+
if (!existsSync(check.bundled) && !Bun.which(check.system)) {
|
|
127
|
+
warnings.push(`Warning: ${check.system} not found. ${check.install}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return warnings;
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
export { executeInit };
|