lattice-graph 0.5.0 → 0.5.2

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
@@ -19,16 +19,16 @@
19
19
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
20
20
  </p>
21
21
 
22
- 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.
22
+ 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.
23
23
 
24
24
  ## The Problem
25
25
 
26
26
  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:
27
27
 
28
- - **Context rot** stale file contents from earlier exploration steps degrade attention
29
- - **Token waste** reading 500-line files to understand 20-line functions
30
- - **Terminology mismatch** searching "checkout timeout" fails when the code calls it `chargeCard`
31
- - **Cold start** every conversation starts from zero with no understanding of the codebase
28
+ - **Context rot** - stale file contents from earlier exploration steps degrade attention
29
+ - **Token waste** - reading 500-line files to understand 20-line functions
30
+ - **Terminology mismatch** - searching "checkout timeout" fails when the code calls it `chargeCard`
31
+ - **Cold start** - every conversation starts from zero with no understanding of the codebase
32
32
 
33
33
  ## The Solution
34
34
 
@@ -103,7 +103,7 @@ lattice code charge # read just the function source to edit
103
103
 
104
104
  ## Tags
105
105
 
106
- 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.
106
+ 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.
107
107
 
108
108
  | Tag | Purpose | Example |
109
109
  |-----|---------|---------|
@@ -114,7 +114,7 @@ Lattice uses four tags placed in comments directly above function definitions. T
114
114
 
115
115
  **What you tag:** Route handlers, CLI commands, cron jobs, external API calls, database operations, event publishers, event consumers.
116
116
 
117
- **What you don't tag:** Everything else. Intermediate functions, callers, callees, types all derived automatically from the call graph.
117
+ **What you don't tag:** Everything else. Intermediate functions, callers, callees, types - all derived automatically from the call graph.
118
118
 
119
119
  ### Syntax rules
120
120
 
@@ -137,7 +137,7 @@ FLOW: checkout
137
137
  send_confirmation
138
138
  ```
139
139
 
140
- `create_order`, `charge`, `save_order` none of these need tags. Their flow membership is derived.
140
+ `create_order`, `charge`, `save_order` - none of these need tags. Their flow membership is derived.
141
141
 
142
142
  ### Async job dispatch patterns
143
143
 
@@ -162,11 +162,11 @@ def process_ingest_vacancies(data, services):
162
162
 
163
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
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`.
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
166
 
167
167
  ### Tags and protocols/interfaces
168
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:
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
170
 
171
171
  ```python
172
172
  # Good: tag is on the function the flow reaches
@@ -271,11 +271,11 @@ Affected flows: checkout
271
271
 
272
272
  Instead of grepping and reading files, an agent using Lattice follows this flow:
273
273
 
274
- 1. **Orient** `lattice overview` to understand the project landscape
275
- 2. **Locate** `lattice flow <name>` to see the relevant call tree
276
- 3. **Understand** `lattice context <symbol>` for a specific function's neighborhood
277
- 4. **Scope** `lattice impact <symbol>` to know what's affected by a change
278
- 5. **Edit** `lattice code <symbol>` to read only the function being modified
274
+ 1. **Orient** - `lattice overview` to understand the project landscape
275
+ 2. **Locate** - `lattice flow <name>` to see the relevant call tree
276
+ 3. **Understand** - `lattice context <symbol>` for a specific function's neighborhood
277
+ 4. **Scope** - `lattice impact <symbol>` to know what's affected by a change
278
+ 5. **Edit** - `lattice code <symbol>` to read only the function being modified
279
279
 
280
280
  Total context consumed: ~200-500 tokens instead of 5,000-50,000 from reading files.
281
281
 
@@ -303,22 +303,22 @@ lattice lint --unresolved # show unresolved reference details
303
303
  ```
304
304
 
305
305
  The linter detects:
306
- - **Missing tags** route handlers without `@lattice:flow`, external calls without `@lattice:boundary`
307
- - **Invalid tags** tags on classes instead of functions, missing values
308
- - **Typos** `@lattice:flow chekout` when `checkout` exists elsewhere
309
- - **Orphaned events** emits without handlers, handlers without emitters
310
- - **Stale tags** boundary tags on functions that no longer call the package
306
+ - **Missing tags** - route handlers without `@lattice:flow`, external calls without `@lattice:boundary`
307
+ - **Invalid tags** - tags on classes instead of functions, missing values
308
+ - **Typos** - `@lattice:flow chekout` when `checkout` exists elsewhere
309
+ - **Orphaned events** - emits without handlers, handlers without emitters
310
+ - **Stale tags** - boundary tags on functions that no longer call the package
311
311
 
312
312
  ## How It Works
313
313
 
314
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
315
+ 2. **Combined edge strategies** - both `outgoingCalls` and `references` are used to maximize edge coverage across files
316
316
  3. **Tag scanner** reads `@lattice:` comments and associates them with the function below
317
317
  4. **Graph builder** inserts everything into a SQLite database with nodes, edges, and tags
318
318
  5. **Event synthesis** creates invisible edges from `@lattice:emits` to `@lattice:handles` nodes, connecting async flows
319
319
  6. **CLI queries** traverse the graph and return compact, scoped results
320
320
 
321
- The graph is stored at `.lattice/graph.db` a single SQLite file.
321
+ The graph is stored at `.lattice/graph.db` - a single SQLite file.
322
322
 
323
323
  ## Configuration
324
324
 
@@ -408,7 +408,7 @@ lattice build && lattice lint # after adding/changing tags
408
408
  }
409
409
  ```
410
410
 
411
- The hooks remind the agent to try Lattice before falling back to traditional tools. They don't block they guide.
411
+ The hooks remind the agent to try Lattice before falling back to traditional tools. They don't block - they guide.
412
412
 
413
413
  ## Requirements
414
414
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lattice-graph",
3
- "version": "0.5.0",
4
- "description": "Knowledge graph CLI for coding agents navigate code through flows, not grep.",
3
+ "version": "0.5.2",
4
+ "description": "Knowledge graph CLI for coding agents - navigate code through flows, not grep.",
5
5
  "module": "src/main.ts",
6
6
  "type": "module",
7
7
  "bin": {
@@ -77,7 +77,7 @@ function detectLanguages(projectRoot: string): readonly string[] {
77
77
  return languages;
78
78
  }
79
79
 
80
- /** Detects the source root uses "src" if it exists, otherwise ".". */
80
+ /** Detects the source root - uses "src" if it exists, otherwise ".". */
81
81
  function detectRoot(projectRoot: string): string {
82
82
  const srcPath = `${projectRoot}/src`;
83
83
  try {
@@ -4,7 +4,7 @@ import type { LintIssue, LintResult } from "../types/lint.ts";
4
4
 
5
5
  /**
6
6
  * Runs all lint checks against the built knowledge graph.
7
- * Does not modify the database reports only.
7
+ * Does not modify the database - reports only.
8
8
  *
9
9
  * @param db - An open Database handle (readonly)
10
10
  * @param config - Lattice configuration
@@ -51,7 +51,7 @@ function checkInvalidTags(db: Database, issues: LintIssue[]): void {
51
51
  file: row.file,
52
52
  line: row.line_start,
53
53
  symbol: row.name,
54
- message: `@lattice:${row.tag_kind} tag on a ${row.node_kind} tags should only be on functions or methods`,
54
+ message: `@lattice:${row.tag_kind} tag on a ${row.node_kind} - tags should only be on functions or methods`,
55
55
  });
56
56
  }
57
57
  }
@@ -66,7 +66,7 @@ function checkTypos(db: Database, issues: LintIssue[]): void {
66
66
  const commons = tagCounts.filter((t) => t.cnt > 1);
67
67
 
68
68
  for (const single of singletons) {
69
- // Require longer names for typo detection short names like "s3" and "sqs" are distinct
69
+ // Require longer names for typo detection - short names like "s3" and "sqs" are distinct
70
70
  const minLength = 4;
71
71
  if (single.value.length < minLength) continue;
72
72
 
@@ -95,7 +95,7 @@ function checkTypos(db: Database, issues: LintIssue[]): void {
95
95
  file: tagNode.file,
96
96
  line: tagNode.line_start,
97
97
  symbol: tagNode.name,
98
- message: `@lattice:${single.kind} "${single.value}" did you mean "${bestMatch.value}"? (used ${bestMatch.cnt} times elsewhere)`,
98
+ message: `@lattice:${single.kind} "${single.value}" - did you mean "${bestMatch.value}"? (used ${bestMatch.cnt} times elsewhere)`,
99
99
  });
100
100
  }
101
101
  }
@@ -119,7 +119,7 @@ function checkOrphanedEvents(db: Database, issues: LintIssue[]): void {
119
119
  file: row.file,
120
120
  line: row.line_start,
121
121
  symbol: row.name,
122
- message: `@lattice:emits "${row.value}" has no handler no @lattice:handles "${row.value}" found`,
122
+ message: `@lattice:emits "${row.value}" has no handler - no @lattice:handles "${row.value}" found`,
123
123
  });
124
124
  }
125
125
 
@@ -138,7 +138,7 @@ function checkOrphanedEvents(db: Database, issues: LintIssue[]): void {
138
138
  file: row.file,
139
139
  line: row.line_start,
140
140
  symbol: row.name,
141
- message: `@lattice:handles "${row.value}" has no emitter no @lattice:emits "${row.value}" found`,
141
+ message: `@lattice:handles "${row.value}" has no emitter - no @lattice:emits "${row.value}" found`,
142
142
  });
143
143
  }
144
144
  }
@@ -178,7 +178,7 @@ function checkStaleBoundaryTags(db: Database, issues: LintIssue[]): void {
178
178
  file: tag.file,
179
179
  line: tag.line_start,
180
180
  symbol: tag.name,
181
- message: `@lattice:boundary "${tag.value}" may be stale no external calls found in this function`,
181
+ message: `@lattice:boundary "${tag.value}" may be stale - no external calls found in this function`,
182
182
  });
183
183
  }
184
184
  }
@@ -217,7 +217,7 @@ function checkMissingBoundaryTags(db: Database, issues: LintIssue[]): void {
217
217
  }
218
218
 
219
219
  /**
220
- * Checks for flow entry points with zero callees the flow tree is just the root node.
220
+ * Checks for flow entry points with zero callees - the flow tree is just the root node.
221
221
  * This typically indicates dynamic dispatch, decorated functions, or missing event connections.
222
222
  */
223
223
  function checkDeadEndFlows(db: Database, issues: LintIssue[]): void {
@@ -246,7 +246,7 @@ function checkDeadEndFlows(db: Database, issues: LintIssue[]): void {
246
246
  file: entry.file,
247
247
  line: entry.line_start,
248
248
  symbol: entry.name,
249
- message: `Flow "${entry.flow_name}" entry point has no callees the call tree may be incomplete. If this function dispatches through a queue or dynamic dispatch, add @lattice:emits/@lattice:handles tags.`,
249
+ message: `Flow "${entry.flow_name}" entry point has no callees - the call tree may be incomplete. If this function dispatches through a queue or dynamic dispatch, add @lattice:emits/@lattice:handles tags.`,
250
250
  });
251
251
  }
252
252
  }
@@ -22,38 +22,38 @@ function executePopulate(db: Database, _config: LatticeConfig): string {
22
22
  return sections.join("\n\n");
23
23
  }
24
24
 
25
- /** Outputs the tag spec what each tag means and the syntax rules. */
25
+ /** Outputs the tag spec - what each tag means and the syntax rules. */
26
26
  function tagSpecSection(): string {
27
27
  return `## Lattice Tag Specification
28
28
 
29
29
  Four tags, placed in comments directly above function definitions:
30
30
 
31
- - \`@lattice:flow <name>\` Flow entry point. Where execution begins for a business operation. Route handlers, CLI commands, cron jobs, queue consumers.
32
- - \`@lattice:boundary <system>\` External boundary. Where code leaves the codebase. API calls, database queries, cache operations, third-party SDKs.
33
- - \`@lattice:emits <event>\` Event emission. Publishes to a queue, event bus, or notification system.
34
- - \`@lattice:handles <event>\` Event consumption. Subscribes to or processes events. Must match a corresponding emits tag.
31
+ - \`@lattice:flow <name>\` - Flow entry point. Where execution begins for a business operation. Route handlers, CLI commands, cron jobs, queue consumers.
32
+ - \`@lattice:boundary <system>\` - External boundary. Where code leaves the codebase. API calls, database queries, cache operations, third-party SDKs.
33
+ - \`@lattice:emits <event>\` - Event emission. Publishes to a queue, event bus, or notification system.
34
+ - \`@lattice:handles <event>\` - Event consumption. Subscribes to or processes events. Must match a corresponding emits tag.
35
35
 
36
36
  Rules:
37
37
  - Place the tag comment directly above the function definition, no blank lines between
38
38
  - Names are kebab-case: \`checkout\`, \`user-registration\`, \`order.created\`, \`aws-s3\`
39
39
  - Multiple values: \`# @lattice:flow checkout, payment\`
40
- - Do NOT tag intermediate functions only entry points and boundaries. Everything in between is derived from the call graph automatically.`;
40
+ - Do NOT tag intermediate functions - only entry points and boundaries. Everything in between is derived from the call graph automatically.`;
41
41
  }
42
42
 
43
43
  /** Few-shot examples showing correct tagging across different scenarios. */
44
44
  function examplesSection(): string {
45
45
  return `## Examples
46
46
 
47
- ### Python FastAPI route with boundary and events
47
+ ### Python - FastAPI route with boundary and events
48
48
 
49
49
  \`\`\`python
50
50
  # @lattice:flow checkout
51
51
  @app.post("/api/checkout")
52
52
  def handle_checkout(req):
53
- order = create_order(req) # no tag derived from call graph
53
+ order = create_order(req) # no tag - derived from call graph
54
54
  return order
55
55
 
56
- def create_order(req): # no tag derived from call graph
56
+ def create_order(req): # no tag - derived from call graph
57
57
  charge(req.amount, req.token)
58
58
  save_order(req)
59
59
  emit_order_created(req.order_id)
@@ -75,7 +75,7 @@ def send_confirmation(event):
75
75
  sendgrid.send(event.order_id)
76
76
  \`\`\`
77
77
 
78
- ### TypeScript Express route with database boundary
78
+ ### TypeScript - Express route with database boundary
79
79
 
80
80
  \`\`\`typescript
81
81
  // @lattice:flow user-registration
@@ -90,7 +90,7 @@ async function createUser(data: CreateUserInput): Promise<User> {
90
90
  }
91
91
  \`\`\`
92
92
 
93
- ### Python Celery task as entry point
93
+ ### Python - Celery task as entry point
94
94
 
95
95
  \`\`\`python
96
96
  # @lattice:flow invoice-generation
@@ -108,7 +108,7 @@ def render_invoice(order):
108
108
  \`\`\``;
109
109
  }
110
110
 
111
- /** Brief project summary from the graph just enough context, not a file listing. */
111
+ /** Brief project summary from the graph - just enough context, not a file listing. */
112
112
  function projectSummarySection(db: Database): string {
113
113
  const lines: string[] = ["## This Project", ""];
114
114
 
@@ -148,15 +148,15 @@ Follow these steps in order. Do not skip validation steps.
148
148
 
149
149
  ### Step 1: Tag entry points
150
150
 
151
- Read the source files and identify all entry points route handlers, CLI commands, cron jobs, queue consumers, event listeners. Add \`@lattice:flow <name>\` above each one.
151
+ Read the source files and identify all entry points - route handlers, CLI commands, cron jobs, queue consumers, event listeners. Add \`@lattice:flow <name>\` above each one.
152
152
 
153
- Use domain names for flows: "checkout", "user-registration", "invoice-generation" not function names.
153
+ Use domain names for flows: "checkout", "user-registration", "invoice-generation" - not function names.
154
154
 
155
155
  ### Step 2: Tag boundaries
156
156
 
157
- Identify all functions that call external systems APIs, databases, caches, file storage, third-party SDKs. Add \`@lattice:boundary <system>\` above each one.
157
+ Identify all functions that call external systems - APIs, databases, caches, file storage, third-party SDKs. Add \`@lattice:boundary <system>\` above each one.
158
158
 
159
- Use the external system name: "stripe", "postgres", "redis", "s3" not the function or library name.
159
+ Use the external system name: "stripe", "postgres", "redis", "s3" - not the function or library name.
160
160
 
161
161
  ### Step 3: Tag async dispatch (queues, Lambda, Celery)
162
162
 
@@ -165,7 +165,7 @@ If the codebase submits work to a queue (SQS, RabbitMQ), invokes Lambda function
165
165
  - Add \`@lattice:emits job.<name>\` on the function that submits/invokes the async work
166
166
  - Add \`@lattice:handles job.<name>\` on the function that processes the work on the other side
167
167
 
168
- Important: Worker handlers and Lambda consumers are NOT separate flows they are the receiving side of an async dispatch. Tag them with \`@lattice:handles\`, not \`@lattice:flow\`.
168
+ Important: Worker handlers and Lambda consumers are NOT separate flows - they are the receiving side of an async dispatch. Tag them with \`@lattice:handles\`, not \`@lattice:flow\`.
169
169
 
170
170
  Place \`emits\` tags on the function the flow actually passes through, not on a concrete implementation behind a protocol or interface.
171
171
 
@@ -48,7 +48,7 @@ function scanTags(source: string, nodes: readonly Node[], language?: string): Ta
48
48
  // Skip tags that are inside a function body and point to the SAME function
49
49
  // (these are @lattice: mentions in string literals, not real tags).
50
50
  // Tags between functions (e.g., above a decorated function whose predecessor's
51
- // range overlaps) are fine the target will be a different, later function.
51
+ // range overlaps) are fine - the target will be a different, later function.
52
52
  const containingNode = candidateNodes.find(
53
53
  (n) => tagLine > n.lineStart && tagLine <= n.lineEnd,
54
54
  );
@@ -75,7 +75,7 @@ function scanTags(source: string, nodes: readonly Node[], language?: string): Ta
75
75
 
76
76
  for (const value of values) {
77
77
  if (!NAME_PATTERN.test(value)) {
78
- errors.push(`Line ${tagLine}: invalid tag name '${value}' must be lowercase kebab-case`);
78
+ errors.push(`Line ${tagLine}: invalid tag name '${value}' - must be lowercase kebab-case`);
79
79
  continue;
80
80
  }
81
81
 
@@ -132,7 +132,7 @@ function getCallees(db: Database, nodeId: string): readonly Node[] {
132
132
 
133
133
  /**
134
134
  * Returns all transitive callers of a node (upstream traversal).
135
- * Used for impact analysis "what is affected if I change this?"
135
+ * Used for impact analysis - "what is affected if I change this?"
136
136
  *
137
137
  * @param db - An open Database handle
138
138
  * @param nodeId - The full node ID to analyze
@@ -1,5 +1,5 @@
1
1
  import type { Database } from "bun:sqlite";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
3
  import { join, relative, resolve } from "node:path";
4
4
  import { scanTags } from "../extract/tag-scanner.ts";
5
5
  import { discoverFiles } from "../files.ts";
@@ -40,7 +40,7 @@ type BuildStats = {
40
40
  readonly durationMs: number;
41
41
  };
42
42
 
43
- const VENDOR_VENV = join(import.meta.dir, "..", "..", "vendor", "venv");
43
+ const LATTICE_HOME = join(process.env.HOME ?? process.env.USERPROFILE ?? ".", ".lattice");
44
44
 
45
45
  const LANGUAGE_EXTENSIONS: Record<string, readonly string[]> = {
46
46
  typescript: [".ts", ".tsx"],
@@ -65,9 +65,9 @@ function resolveLspServer(
65
65
  return { command, args: ["--stdio"], languageId: "typescript" };
66
66
  }
67
67
  if (language === "python") {
68
- const zubanls = resolveZuban();
69
- if (!zubanls) return undefined;
70
- return { command: zubanls, args: [], languageId: "python" };
68
+ const zuban = resolveZuban();
69
+ if (!zuban) return undefined;
70
+ return { command: zuban, args: ["server"], languageId: "python" };
71
71
  }
72
72
  if (language === "go") {
73
73
  const goplsBin = resolveGopls();
@@ -77,65 +77,86 @@ function resolveLspServer(
77
77
  return undefined;
78
78
  }
79
79
 
80
- /** Finds or installs zubanls. Returns the binary path, or undefined if installation fails. */
80
+ /** Finds or installs zuban. Returns the binary path, or undefined if installation fails. */
81
81
  function resolveZuban(): string | undefined {
82
82
  const isWindows = process.platform === "win32";
83
83
  const binDir = isWindows ? "Scripts" : "bin";
84
- const binName = isWindows ? "zubanls.exe" : "zubanls";
84
+ const binName = isWindows ? "zuban.exe" : "zuban";
85
+ const venvPath = join(LATTICE_HOME, "venv");
85
86
 
86
- // 1. Check vendored venv (installed by a previous build)
87
- const vendored = join(VENDOR_VENV, binDir, binName);
87
+ const vendored = join(venvPath, binDir, binName);
88
88
  if (existsSync(vendored)) return vendored;
89
89
 
90
- // 2. Check system PATH
91
- const system = Bun.which("zubanls");
90
+ const system = Bun.which("zuban");
92
91
  if (system) return system;
93
92
 
94
- // 3. Auto-install into vendored venv
95
93
  return installZuban();
96
94
  }
97
95
 
98
- /** Creates a venv and pip-installs zuban. Returns zubanls path or undefined on failure. */
96
+ /** Installs zuban into ~/.lattice/venv. Tries uv first, falls back to pip. */
99
97
  function installZuban(): string | undefined {
100
98
  const isWindows = process.platform === "win32";
101
99
  const binDir = isWindows ? "Scripts" : "bin";
102
- const binName = isWindows ? "zubanls.exe" : "zubanls";
100
+ const binName = isWindows ? "zuban.exe" : "zuban";
101
+ const venvPath = join(LATTICE_HOME, "venv");
103
102
 
104
- const python = Bun.which("python3") ?? Bun.which("python");
105
- if (!python) {
106
- console.error("Python not found. Install Python 3 to enable Python support.");
107
- return undefined;
108
- }
103
+ mkdirSync(LATTICE_HOME, { recursive: true });
109
104
 
110
105
  console.log("Installing Python language server (zuban)...");
111
106
 
112
- const venvResult = Bun.spawnSync([python, "-m", "venv", VENDOR_VENV], {
113
- stdout: "ignore",
114
- stderr: "pipe",
115
- });
116
- if (venvResult.exitCode !== 0) {
117
- console.error(`Failed to create venv: ${venvResult.stderr.toString()}`);
118
- return undefined;
119
- }
107
+ const uv = Bun.which("uv");
108
+ if (uv) {
109
+ const result = Bun.spawnSync([uv, "venv", venvPath], {
110
+ stdout: "ignore",
111
+ stderr: "pipe",
112
+ });
113
+ if (result.exitCode !== 0) {
114
+ console.error(`Failed to create venv: ${result.stderr.toString()}`);
115
+ return undefined;
116
+ }
117
+ const uvPip = Bun.spawnSync(
118
+ [uv, "pip", "install", "zuban", "--python", join(venvPath, binDir, "python3")],
119
+ { stdout: "ignore", stderr: "pipe" },
120
+ );
121
+ if (uvPip.exitCode !== 0) {
122
+ console.error(`Failed to install zuban: ${uvPip.stderr.toString()}`);
123
+ return undefined;
124
+ }
125
+ } else {
126
+ const python = Bun.which("python3") ?? Bun.which("python");
127
+ if (!python) {
128
+ console.error("Python not found. Install Python 3 or uv to enable Python support.");
129
+ return undefined;
130
+ }
120
131
 
121
- const pip = join(VENDOR_VENV, binDir, isWindows ? "pip.exe" : "pip");
122
- const pipResult = Bun.spawnSync([pip, "install", "zuban", "--quiet"], {
123
- stdout: "ignore",
124
- stderr: "pipe",
125
- });
126
- if (pipResult.exitCode !== 0) {
127
- console.error(`Failed to install zuban: ${pipResult.stderr.toString()}`);
128
- return undefined;
132
+ const venvResult = Bun.spawnSync([python, "-m", "venv", venvPath], {
133
+ stdout: "ignore",
134
+ stderr: "pipe",
135
+ });
136
+ if (venvResult.exitCode !== 0) {
137
+ console.error(`Failed to create venv: ${venvResult.stderr.toString()}`);
138
+ return undefined;
139
+ }
140
+
141
+ const pip = join(venvPath, binDir, isWindows ? "pip.exe" : "pip");
142
+ const pipResult = Bun.spawnSync([pip, "install", "zuban", "--quiet"], {
143
+ stdout: "ignore",
144
+ stderr: "pipe",
145
+ });
146
+ if (pipResult.exitCode !== 0) {
147
+ console.error(`Failed to install zuban: ${pipResult.stderr.toString()}`);
148
+ return undefined;
149
+ }
129
150
  }
130
151
 
131
- const zubanls = join(VENDOR_VENV, binDir, binName);
132
- if (!existsSync(zubanls)) {
133
- console.error("zubanls not found after installation");
152
+ const zuban = join(venvPath, binDir, binName);
153
+ if (!existsSync(zuban)) {
154
+ console.error("zuban not found after installation");
134
155
  return undefined;
135
156
  }
136
157
 
137
158
  console.log("done");
138
- return zubanls;
159
+ return zuban;
139
160
  }
140
161
 
141
162
  /** Resolves gopls binary path from GOBIN or GOPATH/bin. */
@@ -264,7 +285,7 @@ async function buildGraph(opts: BuildGraphOptions): Promise<BuildStats> {
264
285
  fileDataList.push({ filePath, relativePath, nodesWithPos });
265
286
  }
266
287
 
267
- // Phase 2a: outgoingCalls gopls doesn't support this over stdio, so skip for Go
288
+ // Phase 2a: outgoingCalls - gopls doesn't support this over stdio, so skip for Go
268
289
  if (langConfig.language !== "go")
269
290
  for (const fd of fileDataList) {
270
291
  for (const nwp of fd.nodesWithPos) {
@@ -290,12 +311,12 @@ async function buildGraph(opts: BuildGraphOptions): Promise<BuildStats> {
290
311
  allEdges.push(...edges);
291
312
  allExternalCalls.push(...externalCalls);
292
313
  } catch {
293
- // outgoingCalls not supported by this server skip silently
314
+ // outgoingCalls not supported by this server - skip silently
294
315
  }
295
316
  }
296
317
  }
297
318
 
298
- // Phase 2b: references "who references each function?"
319
+ // Phase 2b: references - "who references each function?"
299
320
  const nodesByFile = new Map<string, readonly Node[]>();
300
321
  for (const fd of fileDataList) {
301
322
  nodesByFile.set(
@@ -335,7 +356,7 @@ async function buildGraph(opts: BuildGraphOptions): Promise<BuildStats> {
335
356
  allEdges.push({ sourceId: caller.id, targetId: nwp.node.id, kind: "calls" });
336
357
  }
337
358
  } catch {
338
- // references not supported by this server skip silently
359
+ // references not supported by this server - skip silently
339
360
  }
340
361
  }
341
362
  }
package/src/lsp/calls.ts CHANGED
@@ -99,7 +99,7 @@ function extractGoModuleName(uri: string): string | undefined {
99
99
  /**
100
100
  * Checks if a URI points to a type-only package (not a runtime dependency).
101
101
  * Type definition packages (@types/*, typescript, bun-types) are filtered out.
102
- * Actual library .d.ts stubs (e.g., stripe/index.d.ts) are kept they represent runtime deps.
102
+ * Actual library .d.ts stubs (e.g., stripe/index.d.ts) are kept - they represent runtime deps.
103
103
  */
104
104
  function isTypeDeclaration(uri: string): boolean {
105
105
  // TypeScript type-only packages
package/src/lsp/client.ts CHANGED
@@ -81,7 +81,7 @@ async function createLspClient(opts: LspClientOptions): Promise<LspClient> {
81
81
  try {
82
82
  const msg = JSON.parse(body) as JsonRpcMessage;
83
83
  if (msg.method && msg.id !== undefined) {
84
- // Server-initiated request reply with null to unblock the server
84
+ // Server-initiated request - reply with null to unblock the server
85
85
  const reply = JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: null });
86
86
  const replyHeader = `Content-Length: ${Buffer.byteLength(reply)}\r\n\r\n`;
87
87
  proc.stdin?.write(replyHeader + reply);
package/src/main.ts CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  import type { Node } from "./types/graph.ts";
38
38
  import { isOk, unwrap } from "./types/result.ts";
39
39
 
40
- const VERSION = "0.5.0";
40
+ const VERSION = "0.5.2";
41
41
 
42
42
  const program = new Command();
43
43
  program.name("lattice").description("Knowledge graph CLI for coding agents").version(VERSION);
@@ -32,7 +32,7 @@ function isErr<T, E>(result: Result<T, E>): result is Err<E> {
32
32
 
33
33
  /**
34
34
  * Extracts the data from an Ok result or throws on Err.
35
- * Only use at program boundaries prefer pattern matching elsewhere.
35
+ * Only use at program boundaries - prefer pattern matching elsewhere.
36
36
  *
37
37
  * @param result - The result to unwrap
38
38
  * @returns The data inside the Ok variant