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 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/your-org/lattice.git
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 knowledge graph
88
- lattice build # parses all files, builds SQLite graph
87
+ # 2. Build the structural graph
88
+ lattice build # parses all files into a knowledge graph
89
89
 
90
- # Query the graph
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. **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
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. **Cross-file resolution** matches callee names to known symbols across the codebase
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 | Status | Frameworks |
306
- |----------|--------|------------|
307
- | Python | Supported | FastAPI, Flask, Django, Celery |
308
- | TypeScript | Supported | Express, NestJS, Next.js |
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 implementing one extractor. The graph schema, CLI commands, linter, and output formatting are all language-agnostic.
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
- - No runtime dependencies for the compiled binary (except WASM grammars in `node_modules/`)
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 182 tests
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.1.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
- "tree-sitter",
31
- "ast",
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/your-org/lattice"
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
- "tree-sitter-wasms": "0.1.11",
54
- "web-tree-sitter": "0.24.7"
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();
@@ -1,39 +1,22 @@
1
- import type { Database } from "bun:sqlite";
2
1
  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";
2
+ import { join } from "node:path";
8
3
  import { createDatabase } from "../graph/database.ts";
9
4
  import {
10
- insertEdges,
11
- insertNodes,
12
- insertTags,
13
- insertUnresolved,
14
- synthesizeEventEdges,
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
- * 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.
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 or relative path to the project root
36
- * @param config - Parsed Lattice configuration
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
- // 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;
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
- for (const file of files) {
86
- const ext = `.${file.split(".").pop()}`;
87
- const extractor = extByExt.get(ext);
88
- if (!extractor) continue;
38
+ const languageConfigs = buildLanguageConfigs(config);
89
39
 
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
- };
40
+ const stats = await buildGraph({
41
+ projectRoot,
42
+ db,
43
+ languageConfigs,
44
+ exclude: config.exclude,
45
+ });
114
46
 
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')");
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
- 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
- }
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
- * 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 }[];
58
+ /** Builds LanguageConfig entries from LatticeConfig for each configured language. */
59
+ function buildLanguageConfigs(config: LatticeConfig): readonly LanguageConfig[] {
60
+ const configs: LanguageConfig[] = [];
163
61
 
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
- }
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
- 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
- }
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
- return false;
75
+
76
+ return configs;
206
77
  }
207
78
 
208
- export { type BuildStats, executeBuild };
79
+ export { executeBuild };
@@ -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
- return ok("Initialized Lattice project");
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 };