lattice-graph 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +391 -0
- package/package.json +56 -0
- package/src/commands/build.ts +208 -0
- package/src/commands/init.ts +111 -0
- package/src/commands/lint.ts +245 -0
- package/src/commands/populate.ts +224 -0
- package/src/commands/update.ts +175 -0
- package/src/config.ts +93 -0
- package/src/extract/extractor.ts +13 -0
- package/src/extract/parser.ts +117 -0
- package/src/extract/python/calls.ts +121 -0
- package/src/extract/python/extractor.ts +171 -0
- package/src/extract/python/frameworks.ts +142 -0
- package/src/extract/python/imports.ts +115 -0
- package/src/extract/python/symbols.ts +121 -0
- package/src/extract/tags.ts +77 -0
- package/src/extract/typescript/calls.ts +110 -0
- package/src/extract/typescript/extractor.ts +130 -0
- package/src/extract/typescript/imports.ts +71 -0
- package/src/extract/typescript/symbols.ts +252 -0
- package/src/graph/database.ts +95 -0
- package/src/graph/queries.ts +336 -0
- package/src/graph/writer.ts +147 -0
- package/src/main.ts +525 -0
- package/src/output/json.ts +79 -0
- package/src/output/text.ts +265 -0
- package/src/types/config.ts +32 -0
- package/src/types/graph.ts +87 -0
- package/src/types/lint.ts +21 -0
- package/src/types/result.ts +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<table align="center"><tr><td>
|
|
2
|
+
<pre>
|
|
3
|
+
|
|
4
|
+
██╗ █████╗ ████████╗████████╗██╗ ██████╗███████╗
|
|
5
|
+
██║ ██╔══██╗╚══██╔══╝╚══██╔══╝██║██╔════╝██╔════╝
|
|
6
|
+
██║ ███████║ ██║ ██║ ██║██║ █████╗
|
|
7
|
+
██║ ██╔══██║ ██║ ██║ ██║██║ ██╔══╝
|
|
8
|
+
███████╗██║ ██║ ██║ ██║ ██║╚██████╗███████╗
|
|
9
|
+
╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝
|
|
10
|
+
|
|
11
|
+
</pre>
|
|
12
|
+
</td></tr></table>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<strong>Knowledge graph CLI for coding agents</strong>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://bun.sh"><img src="https://img.shields.io/badge/runtime-Bun-f472b6" alt="Bun"></a>
|
|
20
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License"></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
## The Problem
|
|
26
|
+
|
|
27
|
+
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:
|
|
28
|
+
|
|
29
|
+
- **Context rot** — stale file contents from earlier exploration steps degrade attention
|
|
30
|
+
- **Token waste** — reading 500-line files to understand 20-line functions
|
|
31
|
+
- **Terminology mismatch** — searching "checkout timeout" fails when the code calls it `chargeCard`
|
|
32
|
+
- **Cold start** — every conversation starts from zero with no understanding of the codebase
|
|
33
|
+
|
|
34
|
+
## The Solution
|
|
35
|
+
|
|
36
|
+
Embed lightweight annotations (`@lattice:` tags) in your source code, then let Lattice build a graph that agents traverse instead of searching.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# @lattice:flow checkout
|
|
40
|
+
@app.post("/api/checkout")
|
|
41
|
+
def handle_checkout(req):
|
|
42
|
+
order = create_order(req) # ← no tag needed, derived from call graph
|
|
43
|
+
return order
|
|
44
|
+
|
|
45
|
+
# @lattice:boundary stripe
|
|
46
|
+
def charge(amount, token):
|
|
47
|
+
return stripe.charges.create(amount=amount, source=token)
|
|
48
|
+
|
|
49
|
+
# @lattice:emits order.created
|
|
50
|
+
def emit_order_created(order_id):
|
|
51
|
+
queue.publish("order.created", {"order_id": order_id})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Four tags, placed only at entry points and boundaries. Everything in between is derived from the AST.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
### bun (recommended)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bun add -g lattice-graph
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### npx (no install)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bunx lattice-graph init
|
|
68
|
+
bunx lattice-graph build
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### From source
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/your-org/lattice.git
|
|
75
|
+
cd lattice
|
|
76
|
+
bun install
|
|
77
|
+
bun run build # compiles to ./lattice binary
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Initialize Lattice in your project
|
|
84
|
+
cd your-project
|
|
85
|
+
lattice init # creates .lattice/ and lattice.toml
|
|
86
|
+
|
|
87
|
+
# Build the knowledge graph
|
|
88
|
+
lattice build # parses all files, builds SQLite graph
|
|
89
|
+
|
|
90
|
+
# Query the graph
|
|
91
|
+
lattice overview # project landscape: flows, boundaries, events
|
|
92
|
+
lattice flow checkout # full call tree from entry point to boundaries
|
|
93
|
+
lattice context charge # callers, callees, flows, boundary info
|
|
94
|
+
lattice impact charge # what breaks if you change this function
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Tags
|
|
98
|
+
|
|
99
|
+
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.
|
|
100
|
+
|
|
101
|
+
| Tag | Purpose | Example |
|
|
102
|
+
|-----|---------|---------|
|
|
103
|
+
| `@lattice:flow <name>` | Marks a flow entry point | `# @lattice:flow checkout` |
|
|
104
|
+
| `@lattice:boundary <system>` | Marks where code exits the codebase | `# @lattice:boundary stripe` |
|
|
105
|
+
| `@lattice:emits <event>` | Marks event/message emission | `# @lattice:emits order.created` |
|
|
106
|
+
| `@lattice:handles <event>` | Marks event/message consumption | `# @lattice:handles order.created` |
|
|
107
|
+
|
|
108
|
+
**What you tag:** Route handlers, CLI commands, cron jobs, external API calls, database operations, event publishers, event consumers.
|
|
109
|
+
|
|
110
|
+
**What you don't tag:** Everything else. Intermediate functions, callers, callees, types — all derived automatically from the call graph.
|
|
111
|
+
|
|
112
|
+
### Syntax rules
|
|
113
|
+
|
|
114
|
+
- Tags go in the comment block directly above a function definition
|
|
115
|
+
- No blank lines between the tag comment and the function
|
|
116
|
+
- Names are kebab-case: `user-registration`, `order.created`, `aws-s3`
|
|
117
|
+
- Multiple values: `# @lattice:flow checkout, payment`
|
|
118
|
+
- Works with any comment style: `#`, `//`, `/* */`, `--`
|
|
119
|
+
|
|
120
|
+
### How flow propagation works
|
|
121
|
+
|
|
122
|
+
Only flow entry points are tagged. Lattice traces the call graph from the entry point and automatically includes every function in the chain:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
FLOW: checkout
|
|
126
|
+
handle_checkout → create_order → charge [BOUNDARY: stripe]
|
|
127
|
+
→ save_order [BOUNDARY: postgres]
|
|
128
|
+
→ emits order.created
|
|
129
|
+
↓ (event edge)
|
|
130
|
+
send_confirmation
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`create_order`, `charge`, `save_order` — none of these need tags. Their flow membership is derived.
|
|
134
|
+
|
|
135
|
+
## Commands
|
|
136
|
+
|
|
137
|
+
### Build Commands
|
|
138
|
+
|
|
139
|
+
| Command | Description |
|
|
140
|
+
|---------|-------------|
|
|
141
|
+
| `lattice init` | Initialize `.lattice/` in a project, detect languages |
|
|
142
|
+
| `lattice build` | Full index: parse all files, build knowledge graph |
|
|
143
|
+
| `lattice update` | Incremental: re-index only files changed since last build |
|
|
144
|
+
| `lattice lint` | Validate tags: syntax, typos, orphans, missing tags |
|
|
145
|
+
| `lattice populate` | Output agent instructions for tagging the codebase |
|
|
146
|
+
|
|
147
|
+
### Query Commands
|
|
148
|
+
|
|
149
|
+
| Command | Description |
|
|
150
|
+
|---------|-------------|
|
|
151
|
+
| `lattice overview` | Project landscape: flows, boundaries, event connections |
|
|
152
|
+
| `lattice flows` | List all flows with their entry points |
|
|
153
|
+
| `lattice flow <name>` | Full call tree from a flow's entry point to its boundaries |
|
|
154
|
+
| `lattice context <symbol>` | Symbol neighborhood: callers, callees, flows, boundary |
|
|
155
|
+
| `lattice callers <symbol>` | What calls this symbol |
|
|
156
|
+
| `lattice callees <symbol>` | What this symbol calls |
|
|
157
|
+
| `lattice trace <flow> --to <boundary>` | Call chain from flow entry to a specific boundary |
|
|
158
|
+
| `lattice impact <symbol>` | Everything affected if this symbol changes |
|
|
159
|
+
| `lattice boundaries` | All external system boundaries |
|
|
160
|
+
| `lattice events` | All event connections (emits → handles) |
|
|
161
|
+
| `lattice code <symbol>` | Source code of a specific function |
|
|
162
|
+
|
|
163
|
+
All query commands support `--json` for programmatic consumption.
|
|
164
|
+
|
|
165
|
+
## Example Output
|
|
166
|
+
|
|
167
|
+
### `lattice overview`
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
Flows:
|
|
171
|
+
checkout → POST /api/checkout (src/routes/checkout.py:4)
|
|
172
|
+
|
|
173
|
+
Boundaries:
|
|
174
|
+
stripe → 1 function across 1 file
|
|
175
|
+
postgres → 1 function across 1 file
|
|
176
|
+
|
|
177
|
+
Events:
|
|
178
|
+
order.created → emitted by emit_order_created, handled by send_confirmation
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `lattice flow checkout`
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
handle_checkout (src/routes/checkout.py:4)
|
|
185
|
+
→ create_order (src/services/order.py:4)
|
|
186
|
+
→ save_order (src/db/orders.py:4) [postgres]
|
|
187
|
+
→ charge (src/gateways/payment.py:4) [stripe]
|
|
188
|
+
→ build_stripe_payload (src/gateways/payment.py:8)
|
|
189
|
+
→ emit_order_created (src/services/order.py:10) emits order.created
|
|
190
|
+
→ send_confirmation (src/workers/email.py:2)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### `lattice context charge`
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
charge (src/gateways/payment.py:4)
|
|
197
|
+
signature: charge(amount: float, token: str) -> dict
|
|
198
|
+
flows: checkout (derived)
|
|
199
|
+
|
|
200
|
+
callers:
|
|
201
|
+
← create_order (src/services/order.py:4)
|
|
202
|
+
|
|
203
|
+
callees:
|
|
204
|
+
→ build_stripe_payload (src/gateways/payment.py:8)
|
|
205
|
+
|
|
206
|
+
boundary: stripe
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `lattice impact charge`
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
Direct callers:
|
|
213
|
+
← create_order (src/services/order.py:4)
|
|
214
|
+
|
|
215
|
+
Transitive callers:
|
|
216
|
+
← handle_checkout (src/routes/checkout.py:4)
|
|
217
|
+
|
|
218
|
+
Affected flows: checkout
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Agent Workflow
|
|
222
|
+
|
|
223
|
+
Instead of grepping and reading files, an agent using Lattice follows this flow:
|
|
224
|
+
|
|
225
|
+
1. **Orient** — `lattice overview` to understand the project landscape
|
|
226
|
+
2. **Locate** — `lattice flow <name>` to see the relevant call tree
|
|
227
|
+
3. **Understand** — `lattice context <symbol>` for a specific function's neighborhood
|
|
228
|
+
4. **Scope** — `lattice impact <symbol>` to know what's affected by a change
|
|
229
|
+
5. **Edit** — `lattice code <symbol>` to read only the function being modified
|
|
230
|
+
|
|
231
|
+
Total context consumed: ~200-500 tokens instead of 5,000-50,000 from reading files.
|
|
232
|
+
|
|
233
|
+
## Tagging Your Codebase
|
|
234
|
+
|
|
235
|
+
### Automated
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
lattice build # build graph without tags first
|
|
239
|
+
lattice populate # outputs instructions for a coding agent
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`lattice populate` generates a structured prompt that tells a coding agent exactly what to tag, where, and with what values. The agent reads the instructions, adds the tags, then validates:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
lattice build && lattice lint # rebuild graph and validate tags
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Linting
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
lattice lint # check for missing, invalid, stale tags
|
|
252
|
+
lattice lint --strict # treat warnings as errors (for CI)
|
|
253
|
+
lattice lint --unresolved # show unresolved reference details
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The linter detects:
|
|
257
|
+
- **Missing tags** — route handlers without `@lattice:flow`, external calls without `@lattice:boundary`
|
|
258
|
+
- **Invalid tags** — tags on classes instead of functions, missing values
|
|
259
|
+
- **Typos** — `@lattice:flow chekout` when `checkout` exists elsewhere
|
|
260
|
+
- **Orphaned events** — emits without handlers, handlers without emitters
|
|
261
|
+
- **Stale tags** — boundary tags on functions that no longer call the package
|
|
262
|
+
|
|
263
|
+
## How It Works
|
|
264
|
+
|
|
265
|
+
1. **Tree-sitter** parses source files into ASTs (Python and TypeScript supported)
|
|
266
|
+
2. **Extractors** walk the AST to extract symbols (functions, classes, methods), call edges, imports, and framework patterns
|
|
267
|
+
3. **Tag parser** reads `@lattice:` comments and associates them with the function below
|
|
268
|
+
4. **Graph builder** inserts everything into a SQLite database with nodes, edges, and tags
|
|
269
|
+
5. **Event synthesis** creates invisible edges from `@lattice:emits` to `@lattice:handles` nodes
|
|
270
|
+
6. **Cross-file resolution** matches callee names to known symbols across the codebase
|
|
271
|
+
7. **CLI queries** traverse the graph and return compact, scoped results
|
|
272
|
+
|
|
273
|
+
The graph is stored at `.lattice/graph.db` — a single SQLite file.
|
|
274
|
+
|
|
275
|
+
## Configuration
|
|
276
|
+
|
|
277
|
+
`lattice.toml` in your project root:
|
|
278
|
+
|
|
279
|
+
```toml
|
|
280
|
+
[project]
|
|
281
|
+
languages = ["python", "typescript"]
|
|
282
|
+
root = "src"
|
|
283
|
+
exclude = ["node_modules", "venv", ".git", "dist", "__pycache__"]
|
|
284
|
+
|
|
285
|
+
[python]
|
|
286
|
+
source_roots = ["src"]
|
|
287
|
+
test_paths = ["tests"]
|
|
288
|
+
frameworks = ["fastapi"]
|
|
289
|
+
|
|
290
|
+
[typescript]
|
|
291
|
+
source_roots = ["src"]
|
|
292
|
+
test_paths = ["__tests__"]
|
|
293
|
+
frameworks = ["express"]
|
|
294
|
+
|
|
295
|
+
[lint]
|
|
296
|
+
strict = false
|
|
297
|
+
ignore = ["scripts/**"]
|
|
298
|
+
|
|
299
|
+
[lint.boundaries]
|
|
300
|
+
packages = ["stripe", "boto3", "psycopg2", "requests", "sendgrid"]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Supported Languages
|
|
304
|
+
|
|
305
|
+
| Language | Status | Frameworks |
|
|
306
|
+
|----------|--------|------------|
|
|
307
|
+
| Python | Supported | FastAPI, Flask, Django, Celery |
|
|
308
|
+
| TypeScript | Supported | Express, NestJS, Next.js |
|
|
309
|
+
|
|
310
|
+
Adding a new language requires implementing one extractor. The graph schema, CLI commands, linter, and output formatting are all language-agnostic.
|
|
311
|
+
|
|
312
|
+
## Agent Integration (Claude Code)
|
|
313
|
+
|
|
314
|
+
To make coding agents use Lattice instead of grep/read for codebase navigation, add these files to your project.
|
|
315
|
+
|
|
316
|
+
### `.claude/CLAUDE.md`
|
|
317
|
+
|
|
318
|
+
```markdown
|
|
319
|
+
# Codebase Navigation
|
|
320
|
+
|
|
321
|
+
This project uses Lattice for codebase navigation. Before using Grep, Glob, or
|
|
322
|
+
reading files to understand code, use Lattice commands via Bash.
|
|
323
|
+
|
|
324
|
+
## Workflow for any task
|
|
325
|
+
|
|
326
|
+
1. Orient: lattice overview
|
|
327
|
+
2. Locate: lattice flow <name>
|
|
328
|
+
3. Understand: lattice context <symbol>
|
|
329
|
+
4. Scope: lattice impact <symbol>
|
|
330
|
+
5. Read: lattice code <symbol>
|
|
331
|
+
6. Edit: Read/Edit tools on the specific file and line range
|
|
332
|
+
|
|
333
|
+
## When to use Lattice vs traditional tools
|
|
334
|
+
|
|
335
|
+
- Understand a function -> lattice context <symbol> (not Grep)
|
|
336
|
+
- Find callers -> lattice callers <symbol> (not Grep)
|
|
337
|
+
- See a business flow -> lattice flow <name> (not reading files)
|
|
338
|
+
- Check change impact -> lattice impact <symbol> (not Grep for usages)
|
|
339
|
+
- Read code to edit -> lattice code <symbol> (not Read on whole file)
|
|
340
|
+
- Search for string literal -> Grep (this is fine)
|
|
341
|
+
- Find config files -> Glob (this is fine)
|
|
342
|
+
|
|
343
|
+
## After code changes
|
|
344
|
+
|
|
345
|
+
lattice update # incremental re-index
|
|
346
|
+
lattice build && lattice lint # after adding/changing tags
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### `.claude/settings.json`
|
|
350
|
+
|
|
351
|
+
```json
|
|
352
|
+
{
|
|
353
|
+
"hooks": {
|
|
354
|
+
"PreToolUse": [
|
|
355
|
+
{
|
|
356
|
+
"matcher": "Grep",
|
|
357
|
+
"command": "echo \"STOP: Before using Grep, check if Lattice can answer your question faster:\n lattice context <symbol> - function neighborhood\n lattice callers <symbol> - what calls this\n lattice flow <name> - full call tree\n lattice impact <symbol> - change impact\nGrep is appropriate for: string literals, config values, error messages.\nGrep is NOT appropriate for: finding function definitions, understanding call chains, tracing flows.\""
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"matcher": "Glob",
|
|
361
|
+
"command": "echo \"STOP: Before using Glob, check if Lattice can answer your question faster:\n lattice overview - all flows, boundaries, events\n lattice flows - all entry points\n lattice boundaries - all external systems\n lattice context <symbol> - where a symbol lives\nGlob is appropriate for: config files, assets, non-code files.\nGlob is NOT appropriate for: finding source files or function locations.\""
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The hooks remind the agent to try Lattice before falling back to traditional tools. They don't block — they guide.
|
|
369
|
+
|
|
370
|
+
## Requirements
|
|
371
|
+
|
|
372
|
+
- [Bun](https://bun.sh) >= 1.0 (for development and compilation)
|
|
373
|
+
- No runtime dependencies for the compiled binary (except WASM grammars in `node_modules/`)
|
|
374
|
+
|
|
375
|
+
## Development
|
|
376
|
+
|
|
377
|
+
```bash
|
|
378
|
+
bun install # install dependencies + git hooks
|
|
379
|
+
bun run dev # run CLI in development mode
|
|
380
|
+
bun run test # run 182 tests
|
|
381
|
+
bun run lint # biome check
|
|
382
|
+
bun run typecheck # tsc --noEmit
|
|
383
|
+
bun run check # lint + typecheck + tests
|
|
384
|
+
bun run build # compile to single binary
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Pre-commit hooks enforce Biome formatting and TypeScript type checking. Pre-push hooks run the full test suite.
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lattice-graph",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Knowledge graph CLI for coding agents — navigate code through flows, not grep.",
|
|
5
|
+
"module": "src/main.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"lattice": "src/main.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "bun src/main.ts",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"lint": "bunx biome check src/ tests/",
|
|
19
|
+
"format": "bunx biome check --write src/ tests/",
|
|
20
|
+
"typecheck": "bun tsc --noEmit",
|
|
21
|
+
"check": "bun run lint && bun run typecheck && bun run test",
|
|
22
|
+
"build": "bun build src/main.ts --compile --outfile lattice",
|
|
23
|
+
"prepare": "bunx lefthook install"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"cli",
|
|
27
|
+
"knowledge-graph",
|
|
28
|
+
"coding-agent",
|
|
29
|
+
"code-navigation",
|
|
30
|
+
"tree-sitter",
|
|
31
|
+
"ast",
|
|
32
|
+
"static-analysis"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/your-org/lattice"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"bun": ">=1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@biomejs/biome": "^2.4.8",
|
|
44
|
+
"@types/bun": "latest",
|
|
45
|
+
"lefthook": "^2.1.4"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"typescript": "^5"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"commander": "^14.0.3",
|
|
52
|
+
"smol-toml": "^1.6.0",
|
|
53
|
+
"tree-sitter-wasms": "0.1.11",
|
|
54
|
+
"web-tree-sitter": "0.24.7"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import type { Extractor } from "../extract/extractor.ts";
|
|
5
|
+
import { initTreeSitter } from "../extract/parser.ts";
|
|
6
|
+
import { createPythonExtractor } from "../extract/python/extractor.ts";
|
|
7
|
+
import { createTypeScriptExtractor } from "../extract/typescript/extractor.ts";
|
|
8
|
+
import { createDatabase } from "../graph/database.ts";
|
|
9
|
+
import {
|
|
10
|
+
insertEdges,
|
|
11
|
+
insertNodes,
|
|
12
|
+
insertTags,
|
|
13
|
+
insertUnresolved,
|
|
14
|
+
synthesizeEventEdges,
|
|
15
|
+
} from "../graph/writer.ts";
|
|
16
|
+
import type { LatticeConfig } from "../types/config.ts";
|
|
17
|
+
import type { ExtractionResult } from "../types/graph.ts";
|
|
18
|
+
import { err, ok, type Result } from "../types/result.ts";
|
|
19
|
+
|
|
20
|
+
/** Statistics from a completed build. */
|
|
21
|
+
type BuildStats = {
|
|
22
|
+
readonly fileCount: number;
|
|
23
|
+
readonly nodeCount: number;
|
|
24
|
+
readonly edgeCount: number;
|
|
25
|
+
readonly tagCount: number;
|
|
26
|
+
readonly eventEdgeCount: number;
|
|
27
|
+
readonly durationMs: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Executes a full build of the knowledge graph.
|
|
32
|
+
* Walks the source tree, runs extractors on all matching files,
|
|
33
|
+
* inserts results into SQLite, and synthesizes event edges.
|
|
34
|
+
*
|
|
35
|
+
* @param projectRoot - Absolute or relative path to the project root
|
|
36
|
+
* @param config - Parsed Lattice configuration
|
|
37
|
+
* @returns Build statistics or an error message
|
|
38
|
+
*/
|
|
39
|
+
// @lattice:flow build
|
|
40
|
+
async function executeBuild(
|
|
41
|
+
projectRoot: string,
|
|
42
|
+
config: LatticeConfig,
|
|
43
|
+
): Promise<Result<BuildStats, string>> {
|
|
44
|
+
const startTime = Date.now();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Initialize tree-sitter and create extractors
|
|
48
|
+
await initTreeSitter();
|
|
49
|
+
const extractors = await createExtractors(config);
|
|
50
|
+
if (extractors.length === 0) {
|
|
51
|
+
return err("No extractors available for configured languages");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build extension → extractor mapping
|
|
55
|
+
const extByExt = new Map<string, Extractor>();
|
|
56
|
+
for (const ext of extractors) {
|
|
57
|
+
for (const fileExt of ext.fileExtensions) {
|
|
58
|
+
extByExt.set(fileExt, ext);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create .lattice directory and database
|
|
63
|
+
const latticeDir = join(projectRoot, ".lattice");
|
|
64
|
+
mkdirSync(latticeDir, { recursive: true });
|
|
65
|
+
const dbPath = join(latticeDir, "graph.db");
|
|
66
|
+
const db = createDatabase(dbPath);
|
|
67
|
+
|
|
68
|
+
// Walk the source tree
|
|
69
|
+
const sourceRoot = join(projectRoot, config.root);
|
|
70
|
+
const glob = new Bun.Glob("**/*");
|
|
71
|
+
const files: string[] = [];
|
|
72
|
+
|
|
73
|
+
for await (const path of glob.scan({ cwd: sourceRoot, dot: false })) {
|
|
74
|
+
const ext = `.${path.split(".").pop()}`;
|
|
75
|
+
if (!extByExt.has(ext)) continue;
|
|
76
|
+
if (isExcluded(path, config.exclude)) continue;
|
|
77
|
+
files.push(path);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract all files
|
|
81
|
+
let totalNodes = 0;
|
|
82
|
+
let totalEdges = 0;
|
|
83
|
+
let totalTags = 0;
|
|
84
|
+
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const ext = `.${file.split(".").pop()}`;
|
|
87
|
+
const extractor = extByExt.get(ext);
|
|
88
|
+
if (!extractor) continue;
|
|
89
|
+
|
|
90
|
+
const fullPath = join(sourceRoot, file);
|
|
91
|
+
const source = await Bun.file(fullPath).text();
|
|
92
|
+
const relativePath = relative(projectRoot, fullPath);
|
|
93
|
+
|
|
94
|
+
const result: ExtractionResult = await extractor.extract(relativePath, source);
|
|
95
|
+
|
|
96
|
+
insertNodes(db, result.nodes);
|
|
97
|
+
insertEdges(db, result.edges);
|
|
98
|
+
insertTags(db, result.tags);
|
|
99
|
+
insertUnresolved(db, result.unresolved);
|
|
100
|
+
|
|
101
|
+
totalNodes += result.nodes.length;
|
|
102
|
+
totalEdges += result.edges.length;
|
|
103
|
+
totalTags += result.tags.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Cross-file resolution: resolve uncertain edges by matching callee names to known nodes
|
|
107
|
+
resolveCrossFileEdges(db);
|
|
108
|
+
|
|
109
|
+
// Synthesize event edges
|
|
110
|
+
synthesizeEventEdges(db);
|
|
111
|
+
const eventEdgeRow = db.query("SELECT COUNT(*) as c FROM edges WHERE kind = 'event'").get() as {
|
|
112
|
+
c: number;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Write build metadata
|
|
116
|
+
const now = new Date().toISOString();
|
|
117
|
+
db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_build', ?)", [now]);
|
|
118
|
+
db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('lattice_version', '0.1.0')");
|
|
119
|
+
|
|
120
|
+
db.close();
|
|
121
|
+
|
|
122
|
+
return ok({
|
|
123
|
+
fileCount: files.length,
|
|
124
|
+
nodeCount: totalNodes,
|
|
125
|
+
edgeCount: totalEdges + eventEdgeRow.c,
|
|
126
|
+
tagCount: totalTags,
|
|
127
|
+
eventEdgeCount: eventEdgeRow.c,
|
|
128
|
+
durationMs: Date.now() - startTime,
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return err(`Build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Creates extractors for all configured languages. */
|
|
136
|
+
async function createExtractors(config: LatticeConfig): Promise<readonly Extractor[]> {
|
|
137
|
+
const extractors: Extractor[] = [];
|
|
138
|
+
for (const lang of config.languages) {
|
|
139
|
+
if (lang === "python") {
|
|
140
|
+
extractors.push(await createPythonExtractor());
|
|
141
|
+
}
|
|
142
|
+
if (lang === "typescript") {
|
|
143
|
+
extractors.push(await createTypeScriptExtractor());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return extractors;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves uncertain cross-file edges by matching callee names to known nodes.
|
|
151
|
+
* If a callee name (e.g., "create_order") matches exactly one node name in the graph,
|
|
152
|
+
* the edge target is updated to the full node ID.
|
|
153
|
+
*/
|
|
154
|
+
function resolveCrossFileEdges(db: Database): void {
|
|
155
|
+
// Find all uncertain edges where target_id is not a known node
|
|
156
|
+
const uncertainEdges = db
|
|
157
|
+
.query(
|
|
158
|
+
`SELECT e.rowid, e.source_id, e.target_id FROM edges e
|
|
159
|
+
WHERE e.certainty = 'uncertain'
|
|
160
|
+
AND NOT EXISTS (SELECT 1 FROM nodes WHERE id = e.target_id)`,
|
|
161
|
+
)
|
|
162
|
+
.all() as { rowid: number; source_id: string; target_id: string }[];
|
|
163
|
+
|
|
164
|
+
// Build a name→id map for all nodes (only keep unambiguous names)
|
|
165
|
+
const allNodes = db.query("SELECT id, name FROM nodes").all() as { id: string; name: string }[];
|
|
166
|
+
const nameToIds = new Map<string, string[]>();
|
|
167
|
+
for (const node of allNodes) {
|
|
168
|
+
const existing = nameToIds.get(node.name);
|
|
169
|
+
if (existing) {
|
|
170
|
+
existing.push(node.id);
|
|
171
|
+
} else {
|
|
172
|
+
nameToIds.set(node.name, [node.id]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const deleteStmt = db.prepare("DELETE FROM edges WHERE rowid = ?");
|
|
177
|
+
const insertStmt = db.prepare(
|
|
178
|
+
"INSERT OR IGNORE INTO edges (source_id, target_id, kind, certainty) VALUES (?, ?, 'calls', 'certain')",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const tx = db.transaction(() => {
|
|
182
|
+
for (const edge of uncertainEdges) {
|
|
183
|
+
// Try to resolve the callee name
|
|
184
|
+
const calleeName = edge.target_id.split(".").pop() ?? edge.target_id;
|
|
185
|
+
const candidates = nameToIds.get(calleeName);
|
|
186
|
+
|
|
187
|
+
if (candidates && candidates.length === 1 && candidates[0]) {
|
|
188
|
+
// Unambiguous match — replace the uncertain edge
|
|
189
|
+
deleteStmt.run(edge.rowid);
|
|
190
|
+
insertStmt.run(edge.source_id, candidates[0]);
|
|
191
|
+
}
|
|
192
|
+
// If ambiguous or not found, leave the uncertain edge as-is
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
tx();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Checks if a file path matches any exclude pattern. */
|
|
199
|
+
function isExcluded(filePath: string, excludePatterns: readonly string[]): boolean {
|
|
200
|
+
for (const pattern of excludePatterns) {
|
|
201
|
+
if (filePath.includes(pattern.replace("**", "").replace("*", ""))) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { type BuildStats, executeBuild };
|