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 +23 -23
- package/package.json +2 -2
- package/src/commands/init.ts +1 -1
- package/src/commands/lint.ts +9 -9
- package/src/commands/populate.ts +17 -17
- package/src/extract/tag-scanner.ts +2 -2
- package/src/graph/queries.ts +1 -1
- package/src/lsp/builder.ts +64 -43
- package/src/lsp/calls.ts +1 -1
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +1 -1
- package/src/types/result.ts +1 -1
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
|
|
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**
|
|
29
|
-
- **Token waste**
|
|
30
|
-
- **Terminology mismatch**
|
|
31
|
-
- **Cold start**
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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**
|
|
275
|
-
2. **Locate**
|
|
276
|
-
3. **Understand**
|
|
277
|
-
4. **Scope**
|
|
278
|
-
5. **Edit**
|
|
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**
|
|
307
|
-
- **Invalid tags**
|
|
308
|
-
- **Typos**
|
|
309
|
-
- **Orphaned events**
|
|
310
|
-
- **Stale tags**
|
|
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**
|
|
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`
|
|
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
|
|
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.
|
|
4
|
-
"description": "Knowledge graph CLI for coding agents
|
|
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": {
|
package/src/commands/init.ts
CHANGED
|
@@ -77,7 +77,7 @@ function detectLanguages(projectRoot: string): readonly string[] {
|
|
|
77
77
|
return languages;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/** Detects the source root
|
|
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 {
|
package/src/commands/lint.ts
CHANGED
|
@@ -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
|
|
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}
|
|
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
|
|
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}"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/commands/populate.ts
CHANGED
|
@@ -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
|
|
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>\`
|
|
32
|
-
- \`@lattice:boundary <system>\`
|
|
33
|
-
- \`@lattice:emits <event>\`
|
|
34
|
-
- \`@lattice:handles <event>\`
|
|
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
|
|
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
|
|
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
|
|
53
|
+
order = create_order(req) # no tag - derived from call graph
|
|
54
54
|
return order
|
|
55
55
|
|
|
56
|
-
def create_order(req): # no tag
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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}'
|
|
78
|
+
errors.push(`Line ${tagLine}: invalid tag name '${value}' - must be lowercase kebab-case`);
|
|
79
79
|
continue;
|
|
80
80
|
}
|
|
81
81
|
|
package/src/graph/queries.ts
CHANGED
|
@@ -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
|
|
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
|
package/src/lsp/builder.ts
CHANGED
|
@@ -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
|
|
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
|
|
69
|
-
if (!
|
|
70
|
-
return { command:
|
|
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
|
|
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 ? "
|
|
84
|
+
const binName = isWindows ? "zuban.exe" : "zuban";
|
|
85
|
+
const venvPath = join(LATTICE_HOME, "venv");
|
|
85
86
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 ? "
|
|
100
|
+
const binName = isWindows ? "zuban.exe" : "zuban";
|
|
101
|
+
const venvPath = join(LATTICE_HOME, "venv");
|
|
103
102
|
|
|
104
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
132
|
-
if (!existsSync(
|
|
133
|
-
console.error("
|
|
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
|
|
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
|
|
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
|
|
314
|
+
// outgoingCalls not supported by this server - skip silently
|
|
294
315
|
}
|
|
295
316
|
}
|
|
296
317
|
}
|
|
297
318
|
|
|
298
|
-
// Phase 2b: references
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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);
|
package/src/types/result.ts
CHANGED
|
@@ -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
|
|
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
|