lattice-graph 0.4.0 → 0.5.1
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/build.ts +1 -1
- package/src/commands/init.ts +145 -3
- package/src/commands/lint.ts +9 -9
- package/src/commands/populate.ts +17 -17
- package/src/commands/update.ts +8 -7
- 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 +40 -11
- 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.
|
|
4
|
-
"description": "Knowledge graph CLI for coding agents
|
|
3
|
+
"version": "0.5.1",
|
|
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/build.ts
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { err, ok, type Result } from "../types/result.ts";
|
|
4
4
|
|
|
@@ -27,6 +27,17 @@ function executeInit(projectRoot: string): Result<string, string> {
|
|
|
27
27
|
writeFileSync(tomlPath, toml);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Append Lattice section to CLAUDE.md (skip if already present)
|
|
31
|
+
const claudeDir = join(projectRoot, ".claude");
|
|
32
|
+
const claudeMdPath = join(claudeDir, "CLAUDE.md");
|
|
33
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
34
|
+
const existing = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, "utf-8") : "";
|
|
35
|
+
if (!existing.includes("## Code Navigation")) {
|
|
36
|
+
const snippet = generateClaudeSnippet(languages);
|
|
37
|
+
const separator = existing.length > 0 && !existing.endsWith("\n\n") ? "\n\n" : "";
|
|
38
|
+
appendFileSync(claudeMdPath, `${separator}${snippet}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
// Check LSP server availability
|
|
31
42
|
const warnings = checkLspAvailability(languages);
|
|
32
43
|
const message = ["Initialized Lattice project", ...warnings].join("\n");
|
|
@@ -66,7 +77,7 @@ function detectLanguages(projectRoot: string): readonly string[] {
|
|
|
66
77
|
return languages;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
/** Detects the source root
|
|
80
|
+
/** Detects the source root - uses "src" if it exists, otherwise ".". */
|
|
70
81
|
function detectRoot(projectRoot: string): string {
|
|
71
82
|
const srcPath = `${projectRoot}/src`;
|
|
72
83
|
try {
|
|
@@ -111,4 +122,135 @@ function checkLspAvailability(languages: readonly string[]): readonly string[] {
|
|
|
111
122
|
return warnings;
|
|
112
123
|
}
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
const PYTHON_EXAMPLE = `\`\`\`bash
|
|
126
|
+
# 1. Orient: what flows exist?
|
|
127
|
+
lattice overview
|
|
128
|
+
|
|
129
|
+
# 2. Locate: find the relevant flow
|
|
130
|
+
lattice flow user-registration
|
|
131
|
+
|
|
132
|
+
# Output:
|
|
133
|
+
# register (app/auth/routes.py:45)
|
|
134
|
+
# → validate_input (app/auth/validation.py:12)
|
|
135
|
+
# → create_user (app/auth/service.py:30)
|
|
136
|
+
# → hash_password (app/auth/crypto.py:8)
|
|
137
|
+
# → insert_user (app/storage/postgres.py:55) [postgres]
|
|
138
|
+
# → send_welcome_email (app/notifications/email.py:20) [sendgrid]
|
|
139
|
+
|
|
140
|
+
# 3. Understand: zoom into the function you suspect
|
|
141
|
+
lattice context create_user
|
|
142
|
+
|
|
143
|
+
# 4. Scope: check what breaks if you change it
|
|
144
|
+
lattice impact create_user
|
|
145
|
+
|
|
146
|
+
# 5. Read: get just that function's source
|
|
147
|
+
lattice code create_user
|
|
148
|
+
|
|
149
|
+
# 6. Edit: use Read/Edit tools on app/auth/service.py:30
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
### Symbol format
|
|
153
|
+
|
|
154
|
+
Unique names work directly. Ambiguous names need file qualification.
|
|
155
|
+
|
|
156
|
+
\`\`\`bash
|
|
157
|
+
lattice context create_user # unique name
|
|
158
|
+
lattice context app/auth/service.py::create_user # file::function
|
|
159
|
+
lattice context app/models.py::User.save # file::Class.method
|
|
160
|
+
\`\`\``;
|
|
161
|
+
|
|
162
|
+
const TYPESCRIPT_EXAMPLE = `\`\`\`bash
|
|
163
|
+
# 1. Orient: what flows exist?
|
|
164
|
+
lattice overview
|
|
165
|
+
|
|
166
|
+
# 2. Locate: find the relevant flow
|
|
167
|
+
lattice flow checkout
|
|
168
|
+
|
|
169
|
+
# Output:
|
|
170
|
+
# handleCheckout (src/api/checkout.ts:25)
|
|
171
|
+
# → validateCart (src/cart/validation.ts:12)
|
|
172
|
+
# → createOrder (src/orders/service.ts:40)
|
|
173
|
+
# → insertOrder (src/db/orders.ts:18) [postgres]
|
|
174
|
+
# → chargePayment (src/payments/stripe.ts:30) [stripe]
|
|
175
|
+
# → sendConfirmation (src/notifications/email.ts:55) [sendgrid]
|
|
176
|
+
|
|
177
|
+
# 3. Understand: zoom into the function you suspect
|
|
178
|
+
lattice context createOrder
|
|
179
|
+
|
|
180
|
+
# 4. Scope: check what breaks if you change it
|
|
181
|
+
lattice impact createOrder
|
|
182
|
+
|
|
183
|
+
# 5. Read: get just that function's source
|
|
184
|
+
lattice code createOrder
|
|
185
|
+
|
|
186
|
+
# 6. Edit: use Read/Edit tools on src/orders/service.ts:40
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
### Symbol format
|
|
190
|
+
|
|
191
|
+
Unique names work directly. Ambiguous names need file qualification.
|
|
192
|
+
|
|
193
|
+
\`\`\`bash
|
|
194
|
+
lattice context createOrder # unique name
|
|
195
|
+
lattice context src/orders/service.ts::createOrder # file::function
|
|
196
|
+
\`\`\``;
|
|
197
|
+
|
|
198
|
+
const GO_EXAMPLE = `\`\`\`bash
|
|
199
|
+
# 1. Orient: what flows exist?
|
|
200
|
+
lattice overview
|
|
201
|
+
|
|
202
|
+
# 2. Locate: find the relevant flow
|
|
203
|
+
lattice flow create-order
|
|
204
|
+
|
|
205
|
+
# Output:
|
|
206
|
+
# HandleCreateOrder (internal/api/orders.go:35)
|
|
207
|
+
# → ValidateRequest (internal/api/validation.go:20)
|
|
208
|
+
# → CreateOrder (internal/service/orders.go:45)
|
|
209
|
+
# → InsertOrder (internal/repo/orders.go:28) [postgres]
|
|
210
|
+
# → PublishEvent (internal/events/publisher.go:15) [nats]
|
|
211
|
+
|
|
212
|
+
# 3. Understand: zoom into the function you suspect
|
|
213
|
+
lattice context CreateOrder
|
|
214
|
+
|
|
215
|
+
# 4. Scope: check what breaks if you change it
|
|
216
|
+
lattice impact CreateOrder
|
|
217
|
+
|
|
218
|
+
# 5. Read: get just that function's source
|
|
219
|
+
lattice code CreateOrder
|
|
220
|
+
|
|
221
|
+
# 6. Edit: use Read/Edit tools on internal/service/orders.go:45
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
### Symbol format
|
|
225
|
+
|
|
226
|
+
Unique names work directly. Ambiguous names need file qualification.
|
|
227
|
+
|
|
228
|
+
\`\`\`bash
|
|
229
|
+
lattice context CreateOrder # unique name
|
|
230
|
+
lattice context internal/service/orders.go::CreateOrder # file::function
|
|
231
|
+
lattice context internal/api/server.go::Server.Handle # file::Struct.Method
|
|
232
|
+
\`\`\``;
|
|
233
|
+
|
|
234
|
+
/** Generates a CLAUDE.md snippet with language-appropriate few-shot examples. */
|
|
235
|
+
function generateClaudeSnippet(languages: readonly string[]): string {
|
|
236
|
+
const primary = languages[0] ?? "typescript";
|
|
237
|
+
const example =
|
|
238
|
+
primary === "python" ? PYTHON_EXAMPLE : primary === "go" ? GO_EXAMPLE : TYPESCRIPT_EXAMPLE;
|
|
239
|
+
|
|
240
|
+
return `## Code Navigation
|
|
241
|
+
|
|
242
|
+
This project uses **Lattice** for codebase navigation. Use Lattice before reading files or Grep.
|
|
243
|
+
|
|
244
|
+
### Example: full workflow
|
|
245
|
+
|
|
246
|
+
${example}
|
|
247
|
+
|
|
248
|
+
### After code changes
|
|
249
|
+
|
|
250
|
+
\`\`\`bash
|
|
251
|
+
lattice update
|
|
252
|
+
\`\`\`
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export { executeInit, generateClaudeSnippet };
|
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
|
|
package/src/commands/update.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { discoverFiles } from "../files.ts";
|
|
|
5
5
|
import { checkSchemaVersion } from "../graph/database.ts";
|
|
6
6
|
import type { LatticeConfig } from "../types/config.ts";
|
|
7
7
|
import { isOk, ok, type Result } from "../types/result.ts";
|
|
8
|
-
import { executeBuild } from "./build.ts";
|
|
8
|
+
import { buildLanguageConfigs, executeBuild } from "./build.ts";
|
|
9
9
|
|
|
10
10
|
/** Statistics from an incremental update. */
|
|
11
11
|
type UpdateStats = {
|
|
@@ -58,13 +58,14 @@ async function executeUpdate(
|
|
|
58
58
|
}
|
|
59
59
|
const lastBuild = Number.parseInt(metaRow.value, 10);
|
|
60
60
|
|
|
61
|
-
// Discover files
|
|
62
|
-
const
|
|
63
|
-
const sourceRoots = config.typescript?.sourceRoots ?? [config.root];
|
|
61
|
+
// Discover files across all configured languages
|
|
62
|
+
const languageConfigs = buildLanguageConfigs(config);
|
|
64
63
|
const allFiles: string[] = [];
|
|
65
|
-
for (const
|
|
66
|
-
const
|
|
67
|
-
|
|
64
|
+
for (const lang of languageConfigs) {
|
|
65
|
+
for (const srcRoot of lang.sourceRoots) {
|
|
66
|
+
const absRoot = resolve(projectRoot, srcRoot);
|
|
67
|
+
allFiles.push(...discoverFiles(absRoot, lang.extensions, config.exclude));
|
|
68
|
+
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// Find changed files
|
|
@@ -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.
|
|
40
|
+
const VERSION = "0.5.0";
|
|
41
41
|
|
|
42
42
|
const program = new Command();
|
|
43
43
|
program.name("lattice").description("Knowledge graph CLI for coding agents").version(VERSION);
|
|
@@ -269,17 +269,14 @@ program
|
|
|
269
269
|
const nodes = resolveSymbol(db, symbol);
|
|
270
270
|
|
|
271
271
|
if (nodes.length === 0) {
|
|
272
|
+
printUnknownSymbolError(db, symbol);
|
|
272
273
|
db.close();
|
|
273
|
-
console.log(`Unknown symbol: ${symbol}`);
|
|
274
274
|
process.exit(1);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
if (nodes.length > 1) {
|
|
278
|
+
printAmbiguousSymbolError(nodes);
|
|
278
279
|
db.close();
|
|
279
|
-
console.log("Ambiguous symbol. Matches:");
|
|
280
|
-
for (const n of nodes) {
|
|
281
|
-
console.log(` ${n.id}`);
|
|
282
|
-
}
|
|
283
280
|
process.exit(1);
|
|
284
281
|
}
|
|
285
282
|
|
|
@@ -459,15 +456,12 @@ function resolveOne(db: Database, symbol: string): Node {
|
|
|
459
456
|
const nodes = resolveSymbol(db, symbol);
|
|
460
457
|
if (nodes.length === 0) {
|
|
461
458
|
db.close();
|
|
462
|
-
|
|
459
|
+
printUnknownSymbolError(db, symbol);
|
|
463
460
|
process.exit(1);
|
|
464
461
|
}
|
|
465
462
|
if (nodes.length > 1) {
|
|
466
463
|
db.close();
|
|
467
|
-
|
|
468
|
-
for (const n of nodes) {
|
|
469
|
-
console.error(` ${n.id}`);
|
|
470
|
-
}
|
|
464
|
+
printAmbiguousSymbolError(nodes);
|
|
471
465
|
process.exit(1);
|
|
472
466
|
}
|
|
473
467
|
const node = nodes[0];
|
|
@@ -479,6 +473,41 @@ function resolveOne(db: Database, symbol: string): Node {
|
|
|
479
473
|
return node;
|
|
480
474
|
}
|
|
481
475
|
|
|
476
|
+
function printUnknownSymbolError(db: Database, symbol: string): void {
|
|
477
|
+
console.error(`Unknown symbol: ${symbol}\n`);
|
|
478
|
+
console.error("Symbol format:");
|
|
479
|
+
console.error(" function_name unique name (if unambiguous)");
|
|
480
|
+
console.error(" src/file.ts::functionName file::function");
|
|
481
|
+
console.error(" src/file.py::Class.method file::Class.method\n");
|
|
482
|
+
|
|
483
|
+
const similar = findSimilarSymbols(db, symbol);
|
|
484
|
+
if (similar.length > 0) {
|
|
485
|
+
console.error("Did you mean:");
|
|
486
|
+
for (const s of similar) {
|
|
487
|
+
console.error(` ${s}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function printAmbiguousSymbolError(nodes: readonly Node[]): void {
|
|
493
|
+
console.error("Ambiguous symbol. Qualify with file path:\n");
|
|
494
|
+
for (const n of nodes) {
|
|
495
|
+
console.error(` lattice context ${n.id}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function findSimilarSymbols(db: Database, symbol: string): readonly string[] {
|
|
500
|
+
const lower = symbol.toLowerCase();
|
|
501
|
+
const parts = lower.split(".");
|
|
502
|
+
const name = parts[parts.length - 1] ?? lower;
|
|
503
|
+
|
|
504
|
+
const rows = db
|
|
505
|
+
.query("SELECT id, name FROM nodes WHERE LOWER(name) LIKE ? LIMIT 5")
|
|
506
|
+
.all(`%${name}%`) as { id: string; name: string }[];
|
|
507
|
+
|
|
508
|
+
return rows.map((r) => r.id);
|
|
509
|
+
}
|
|
510
|
+
|
|
482
511
|
/** Builds a flow tree by recursively following call edges from a root node. */
|
|
483
512
|
function buildFlowTree(
|
|
484
513
|
db: Database,
|
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
|