lattice-graph 0.3.0 → 0.5.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/package.json +1 -3
- package/src/commands/build.ts +7 -1
- package/src/commands/init.ts +148 -25
- package/src/commands/update.ts +8 -7
- package/src/config.ts +28 -2
- package/src/extract/tag-scanner.ts +1 -0
- package/src/lsp/builder.ts +162 -28
- package/src/lsp/calls.ts +44 -5
- package/src/lsp/client.ts +30 -8
- package/src/lsp/symbols.ts +34 -7
- package/src/lsp/types.ts +1 -0
- package/src/main.ts +40 -11
- package/src/types/config.ts +8 -1
- package/scripts/postinstall.ts +0 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lattice-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Knowledge graph CLI for coding agents — navigate code through flows, not grep.",
|
|
5
5
|
"module": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -9,12 +9,10 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src",
|
|
12
|
-
"scripts",
|
|
13
12
|
"README.md",
|
|
14
13
|
"LICENSE"
|
|
15
14
|
],
|
|
16
15
|
"scripts": {
|
|
17
|
-
"postinstall": "bun scripts/postinstall.ts",
|
|
18
16
|
"dev": "bun src/main.ts",
|
|
19
17
|
"test": "bun test",
|
|
20
18
|
"lint": "bunx biome check src/ tests/",
|
package/src/commands/build.ts
CHANGED
|
@@ -73,7 +73,13 @@ function buildLanguageConfigs(config: LatticeConfig): readonly LanguageConfig[]
|
|
|
73
73
|
configs.push(buildLanguageConfig("python", [config.root], ["tests"]));
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
if (config.languages.includes("go") && config.go) {
|
|
77
|
+
configs.push(buildLanguageConfig("go", config.go.sourceRoots, config.go.testPaths));
|
|
78
|
+
} else if (config.languages.includes("go")) {
|
|
79
|
+
configs.push(buildLanguageConfig("go", [config.root], []));
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
return configs;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
export { executeBuild };
|
|
85
|
+
export { buildLanguageConfigs, executeBuild };
|
package/src/commands/init.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { err, ok, type Result } from "../types/result.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -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");
|
|
@@ -102,32 +113,144 @@ function generateToml(languages: readonly string[], root: string): string {
|
|
|
102
113
|
return lines.join("\n");
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
/** Checks
|
|
116
|
+
/** Checks prerequisites for language support. */
|
|
106
117
|
function checkLspAvailability(languages: readonly string[]): readonly string[] {
|
|
107
118
|
const warnings: string[] = [];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const checks: Record<string, { bundled: string; system: string; install: string }> = {
|
|
111
|
-
typescript: {
|
|
112
|
-
bundled: join(latticeRoot, "node_modules", ".bin", "typescript-language-server"),
|
|
113
|
-
system: "typescript-language-server",
|
|
114
|
-
install: "reinstall lattice-graph",
|
|
115
|
-
},
|
|
116
|
-
python: {
|
|
117
|
-
bundled: join(latticeRoot, "vendor", "venv", "bin", "zubanls"),
|
|
118
|
-
system: "zubanls",
|
|
119
|
-
install: "reinstall lattice-graph (or pip install zuban)",
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
for (const lang of languages) {
|
|
124
|
-
const check = checks[lang];
|
|
125
|
-
if (!check) continue;
|
|
126
|
-
if (!existsSync(check.bundled) && !Bun.which(check.system)) {
|
|
127
|
-
warnings.push(`Warning: ${check.system} not found. ${check.install}`);
|
|
128
|
-
}
|
|
119
|
+
if (languages.includes("python") && !Bun.which("python3") && !Bun.which("python")) {
|
|
120
|
+
warnings.push("Warning: Python 3 not found. Install Python 3 to enable Python support.");
|
|
129
121
|
}
|
|
130
122
|
return warnings;
|
|
131
123
|
}
|
|
132
124
|
|
|
133
|
-
|
|
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/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
|
package/src/config.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { parse as parseToml } from "smol-toml";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
GoConfig,
|
|
4
|
+
LatticeConfig,
|
|
5
|
+
LintConfig,
|
|
6
|
+
PythonConfig,
|
|
7
|
+
TypeScriptConfig,
|
|
8
|
+
} from "./types/config.ts";
|
|
3
9
|
import { err, ok, type Result } from "./types/result.ts";
|
|
4
10
|
|
|
5
|
-
const DEFAULT_EXCLUDE = [
|
|
11
|
+
const DEFAULT_EXCLUDE = [
|
|
12
|
+
"node_modules",
|
|
13
|
+
"venv",
|
|
14
|
+
"vendor",
|
|
15
|
+
".git",
|
|
16
|
+
"dist",
|
|
17
|
+
"__pycache__",
|
|
18
|
+
".lattice",
|
|
19
|
+
];
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* Parses a TOML string into a validated LatticeConfig.
|
|
@@ -38,6 +52,8 @@ function parseConfig(tomlString: string): Result<LatticeConfig, string> {
|
|
|
38
52
|
? parseTypeScriptSection(raw.typescript)
|
|
39
53
|
: undefined;
|
|
40
54
|
|
|
55
|
+
const goConfig = languages.includes("go") ? parseGoSection(raw.go) : undefined;
|
|
56
|
+
|
|
41
57
|
const lintConfig = parseLintSection(raw.lint);
|
|
42
58
|
|
|
43
59
|
return ok({
|
|
@@ -46,6 +62,7 @@ function parseConfig(tomlString: string): Result<LatticeConfig, string> {
|
|
|
46
62
|
exclude,
|
|
47
63
|
python: pythonConfig,
|
|
48
64
|
typescript: typescriptConfig,
|
|
65
|
+
go: goConfig,
|
|
49
66
|
lint: lintConfig,
|
|
50
67
|
});
|
|
51
68
|
}
|
|
@@ -69,6 +86,15 @@ function parseTypeScriptSection(raw: unknown): TypeScriptConfig {
|
|
|
69
86
|
};
|
|
70
87
|
}
|
|
71
88
|
|
|
89
|
+
/** Parses the [go] section with defaults for missing fields. */
|
|
90
|
+
function parseGoSection(raw: unknown): GoConfig {
|
|
91
|
+
const section = isRecord(raw) ? raw : {};
|
|
92
|
+
return {
|
|
93
|
+
sourceRoots: isStringArray(section.source_roots) ? section.source_roots : ["."],
|
|
94
|
+
testPaths: isStringArray(section.test_paths) ? section.test_paths : [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
/** Parses the [lint] section with defaults for missing fields. */
|
|
73
99
|
function parseLintSection(raw: unknown): LintConfig {
|
|
74
100
|
const section = isRecord(raw) ? raw : {};
|
|
@@ -7,6 +7,7 @@ const NAME_PATTERN = /^[a-z][a-z0-9._-]*$/;
|
|
|
7
7
|
const COMMENT_PREFIXES: Record<string, RegExp> = {
|
|
8
8
|
typescript: /^\s*(?:\/\/|\/\*\*?|\*)\s*/,
|
|
9
9
|
python: /^\s*(?:#|""")\s*/,
|
|
10
|
+
go: /^\s*(?:\/\/)\s*/,
|
|
10
11
|
};
|
|
11
12
|
const DEFAULT_COMMENT_PREFIX = /^\s*(?:\/\/|#|--|\/\*\*?|\*)\s*/;
|
|
12
13
|
|
package/src/lsp/builder.ts
CHANGED
|
@@ -40,12 +40,19 @@ type BuildStats = {
|
|
|
40
40
|
readonly durationMs: number;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const VENDOR_VENV = join(import.meta.dir, "..", "..", "vendor", "venv");
|
|
44
|
+
|
|
45
|
+
const LANGUAGE_EXTENSIONS: Record<string, readonly string[]> = {
|
|
46
|
+
typescript: [".ts", ".tsx"],
|
|
47
|
+
python: [".py"],
|
|
48
|
+
go: [".go"],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Resolves the LSP server binary for a language. Auto-installs zuban on first use. */
|
|
44
52
|
function resolveLspServer(
|
|
45
53
|
language: string,
|
|
46
54
|
): { command: string; args: readonly string[]; languageId: string } | undefined {
|
|
47
55
|
if (language === "typescript") {
|
|
48
|
-
// Check node_modules/.bin/ first (bundled with lattice-graph)
|
|
49
56
|
const bundled = join(
|
|
50
57
|
import.meta.dir,
|
|
51
58
|
"..",
|
|
@@ -58,13 +65,131 @@ function resolveLspServer(
|
|
|
58
65
|
return { command, args: ["--stdio"], languageId: "typescript" };
|
|
59
66
|
}
|
|
60
67
|
if (language === "python") {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
return { command, args: [], languageId: "python" };
|
|
68
|
+
const zubanls = resolveZuban();
|
|
69
|
+
if (!zubanls) return undefined;
|
|
70
|
+
return { command: zubanls, args: [], languageId: "python" };
|
|
71
|
+
}
|
|
72
|
+
if (language === "go") {
|
|
73
|
+
const goplsBin = resolveGopls();
|
|
74
|
+
if (!goplsBin) return undefined;
|
|
75
|
+
return { command: goplsBin, args: ["serve"], languageId: "go" };
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Finds or installs zubanls. Returns the binary path, or undefined if installation fails. */
|
|
81
|
+
function resolveZuban(): string | undefined {
|
|
82
|
+
const isWindows = process.platform === "win32";
|
|
83
|
+
const binDir = isWindows ? "Scripts" : "bin";
|
|
84
|
+
const binName = isWindows ? "zubanls.exe" : "zubanls";
|
|
85
|
+
|
|
86
|
+
// 1. Check vendored venv (installed by a previous build)
|
|
87
|
+
const vendored = join(VENDOR_VENV, binDir, binName);
|
|
88
|
+
if (existsSync(vendored)) return vendored;
|
|
89
|
+
|
|
90
|
+
// 2. Check system PATH
|
|
91
|
+
const system = Bun.which("zubanls");
|
|
92
|
+
if (system) return system;
|
|
93
|
+
|
|
94
|
+
// 3. Auto-install into vendored venv
|
|
95
|
+
return installZuban();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Creates a venv and pip-installs zuban. Returns zubanls path or undefined on failure. */
|
|
99
|
+
function installZuban(): string | undefined {
|
|
100
|
+
const isWindows = process.platform === "win32";
|
|
101
|
+
const binDir = isWindows ? "Scripts" : "bin";
|
|
102
|
+
const binName = isWindows ? "zubanls.exe" : "zubanls";
|
|
103
|
+
|
|
104
|
+
const python = Bun.which("python3") ?? Bun.which("python");
|
|
105
|
+
if (!python) {
|
|
106
|
+
console.error("Python not found. Install Python 3 to enable Python support.");
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log("Installing Python language server (zuban)...");
|
|
111
|
+
|
|
112
|
+
const venvResult = Bun.spawnSync([python, "-m", "venv", VENDOR_VENV], {
|
|
113
|
+
stdout: "ignore",
|
|
114
|
+
stderr: "pipe",
|
|
115
|
+
});
|
|
116
|
+
if (venvResult.exitCode !== 0) {
|
|
117
|
+
console.error(`Failed to create venv: ${venvResult.stderr.toString()}`);
|
|
118
|
+
return undefined;
|
|
64
119
|
}
|
|
120
|
+
|
|
121
|
+
const pip = join(VENDOR_VENV, binDir, isWindows ? "pip.exe" : "pip");
|
|
122
|
+
const pipResult = Bun.spawnSync([pip, "install", "zuban", "--quiet"], {
|
|
123
|
+
stdout: "ignore",
|
|
124
|
+
stderr: "pipe",
|
|
125
|
+
});
|
|
126
|
+
if (pipResult.exitCode !== 0) {
|
|
127
|
+
console.error(`Failed to install zuban: ${pipResult.stderr.toString()}`);
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const zubanls = join(VENDOR_VENV, binDir, binName);
|
|
132
|
+
if (!existsSync(zubanls)) {
|
|
133
|
+
console.error("zubanls not found after installation");
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log("done");
|
|
138
|
+
return zubanls;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Resolves gopls binary path from GOBIN or GOPATH/bin. */
|
|
142
|
+
function findGoplsBinary(): string | undefined {
|
|
143
|
+
const gobin = process.env.GOBIN;
|
|
144
|
+
if (gobin) {
|
|
145
|
+
const goplsPath = join(gobin, "gopls");
|
|
146
|
+
if (existsSync(goplsPath)) return goplsPath;
|
|
147
|
+
}
|
|
148
|
+
const gopath = process.env.GOPATH ?? join(process.env.HOME ?? "", "go");
|
|
149
|
+
const gopathBin = join(gopath, "bin", "gopls");
|
|
150
|
+
if (existsSync(gopathBin)) return gopathBin;
|
|
65
151
|
return undefined;
|
|
66
152
|
}
|
|
67
153
|
|
|
154
|
+
/** Finds or installs gopls. Returns the binary path, or undefined if unavailable. */
|
|
155
|
+
function resolveGopls(): string | undefined {
|
|
156
|
+
const system = Bun.which("gopls");
|
|
157
|
+
if (system) return system;
|
|
158
|
+
|
|
159
|
+
const found = findGoplsBinary();
|
|
160
|
+
if (found) return found;
|
|
161
|
+
|
|
162
|
+
return installGopls();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Installs gopls via go install. Returns binary path or undefined on failure. */
|
|
166
|
+
function installGopls(): string | undefined {
|
|
167
|
+
const goBin = Bun.which("go");
|
|
168
|
+
if (!goBin) {
|
|
169
|
+
console.error("Go not found. Install Go to enable Go support.");
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log("Installing Go language server (gopls)...");
|
|
174
|
+
|
|
175
|
+
const result = Bun.spawnSync([goBin, "install", "golang.org/x/tools/gopls@latest"], {
|
|
176
|
+
stdout: "ignore",
|
|
177
|
+
stderr: "pipe",
|
|
178
|
+
});
|
|
179
|
+
if (result.exitCode !== 0) {
|
|
180
|
+
console.error(`Failed to install gopls: ${result.stderr.toString()}`);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const installed = findGoplsBinary();
|
|
185
|
+
if (!installed) {
|
|
186
|
+
console.error("gopls not found after installation");
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
console.log("done");
|
|
190
|
+
return installed;
|
|
191
|
+
}
|
|
192
|
+
|
|
68
193
|
/**
|
|
69
194
|
* Builds the knowledge graph by querying LSP servers for symbols and call hierarchy,
|
|
70
195
|
* scanning for @lattice: tags, and writing everything to SQLite.
|
|
@@ -117,7 +242,10 @@ async function buildGraph(opts: BuildGraphOptions): Promise<BuildStats> {
|
|
|
117
242
|
|
|
118
243
|
for (const filePath of files) {
|
|
119
244
|
const relativePath = relative(projectRoot, filePath);
|
|
120
|
-
const isTest =
|
|
245
|
+
const isTest =
|
|
246
|
+
langConfig.language === "go"
|
|
247
|
+
? relativePath.endsWith("_test.go")
|
|
248
|
+
: langConfig.testPaths.some((tp) => relativePath.startsWith(tp));
|
|
121
249
|
const source = await Bun.file(filePath).text();
|
|
122
250
|
|
|
123
251
|
const symbols = await client.documentSymbol(filePath);
|
|
@@ -136,30 +264,36 @@ async function buildGraph(opts: BuildGraphOptions): Promise<BuildStats> {
|
|
|
136
264
|
fileDataList.push({ filePath, relativePath, nodesWithPos });
|
|
137
265
|
}
|
|
138
266
|
|
|
139
|
-
// Phase 2a: outgoingCalls —
|
|
140
|
-
|
|
141
|
-
for (const
|
|
142
|
-
|
|
267
|
+
// Phase 2a: outgoingCalls — gopls doesn't support this over stdio, so skip for Go
|
|
268
|
+
if (langConfig.language !== "go")
|
|
269
|
+
for (const fd of fileDataList) {
|
|
270
|
+
for (const nwp of fd.nodesWithPos) {
|
|
271
|
+
if (nwp.node.kind !== "function" && nwp.node.kind !== "method") continue;
|
|
143
272
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
273
|
+
try {
|
|
274
|
+
const items = await client.prepareCallHierarchy(
|
|
275
|
+
fd.filePath,
|
|
276
|
+
nwp.selectionLine,
|
|
277
|
+
nwp.selectionCharacter,
|
|
278
|
+
);
|
|
279
|
+
if (items.length === 0) continue;
|
|
280
|
+
const item = items[0];
|
|
281
|
+
if (!item) continue;
|
|
282
|
+
|
|
283
|
+
const calls = await client.outgoingCalls(item);
|
|
284
|
+
const { edges, externalCalls } = outgoingCallsToEdges(
|
|
285
|
+
nwp.node.id,
|
|
286
|
+
calls,
|
|
287
|
+
projectRoot,
|
|
288
|
+
langConfig.language,
|
|
289
|
+
);
|
|
290
|
+
allEdges.push(...edges);
|
|
291
|
+
allExternalCalls.push(...externalCalls);
|
|
292
|
+
} catch {
|
|
293
|
+
// outgoingCalls not supported by this server — skip silently
|
|
294
|
+
}
|
|
160
295
|
}
|
|
161
296
|
}
|
|
162
|
-
}
|
|
163
297
|
|
|
164
298
|
// Phase 2b: references — "who references each function?"
|
|
165
299
|
const nodesByFile = new Map<string, readonly Node[]>();
|
|
@@ -235,7 +369,7 @@ function buildLanguageConfig(
|
|
|
235
369
|
sourceRoots: readonly string[],
|
|
236
370
|
testPaths: readonly string[],
|
|
237
371
|
): LanguageConfig {
|
|
238
|
-
const extensions = language
|
|
372
|
+
const extensions = LANGUAGE_EXTENSIONS[language] ?? [];
|
|
239
373
|
return { language, extensions, sourceRoots, testPaths };
|
|
240
374
|
}
|
|
241
375
|
|
package/src/lsp/calls.ts
CHANGED
|
@@ -14,12 +14,14 @@ type CallConversionResult = {
|
|
|
14
14
|
* @param sourceId - The Lattice node ID of the calling function
|
|
15
15
|
* @param calls - Outgoing calls from LSP
|
|
16
16
|
* @param projectRoot - Absolute path to the project root
|
|
17
|
+
* @param language - Language identifier for external call detection
|
|
17
18
|
* @returns Internal edges and external call records
|
|
18
19
|
*/
|
|
19
20
|
function outgoingCallsToEdges(
|
|
20
21
|
sourceId: string,
|
|
21
22
|
calls: readonly CallHierarchyOutgoingCall[],
|
|
22
23
|
projectRoot: string,
|
|
24
|
+
language: string,
|
|
23
25
|
): CallConversionResult {
|
|
24
26
|
const edges: Edge[] = [];
|
|
25
27
|
const externalCalls: ExternalCall[] = [];
|
|
@@ -28,11 +30,15 @@ function outgoingCallsToEdges(
|
|
|
28
30
|
for (const call of calls) {
|
|
29
31
|
const uri = call.to.uri;
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const isExternal =
|
|
34
|
+
!uri.startsWith(projectFilePrefix) ||
|
|
35
|
+
uri.includes("/node_modules/") ||
|
|
36
|
+
uri.includes("/pkg/mod/");
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
if (isExternal) {
|
|
39
|
+
if (language !== "go" && isTypeDeclaration(uri)) continue;
|
|
40
|
+
|
|
41
|
+
const pkg = language === "go" ? extractGoModuleName(uri) : extractPackageName(uri);
|
|
36
42
|
if (pkg) {
|
|
37
43
|
externalCalls.push({ nodeId: sourceId, package: pkg, symbol: call.to.name });
|
|
38
44
|
}
|
|
@@ -63,6 +69,33 @@ function extractPackageName(uri: string): string | undefined {
|
|
|
63
69
|
return afterNm.split("/")[0];
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Extracts the Go module name from a module cache or stdlib URI.
|
|
74
|
+
* Module cache paths contain /pkg/mod/ with @version.
|
|
75
|
+
* Stdlib paths contain /go/src/ without @version.
|
|
76
|
+
*/
|
|
77
|
+
function extractGoModuleName(uri: string): string | undefined {
|
|
78
|
+
const decoded = decodeURIComponent(uri);
|
|
79
|
+
|
|
80
|
+
// Go module cache: contains /pkg/mod/ with @version
|
|
81
|
+
const pkgModIdx = decoded.indexOf("/pkg/mod/");
|
|
82
|
+
if (pkgModIdx !== -1) {
|
|
83
|
+
const afterMod = decoded.slice(pkgModIdx + "/pkg/mod/".length);
|
|
84
|
+
const atIdx = afterMod.indexOf("@");
|
|
85
|
+
if (atIdx !== -1) {
|
|
86
|
+
return afterMod.slice(0, atIdx);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Go stdlib: contains /go/src/ without @version
|
|
91
|
+
const goSrcMatch = decoded.match(/\/go\/src\/(.+?)\/[^/]+$/);
|
|
92
|
+
if (goSrcMatch && !decoded.includes("@")) {
|
|
93
|
+
return goSrcMatch[1];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
/**
|
|
67
100
|
* Checks if a URI points to a type-only package (not a runtime dependency).
|
|
68
101
|
* Type definition packages (@types/*, typescript, bun-types) are filtered out.
|
|
@@ -81,4 +114,10 @@ function isTypeDeclaration(uri: string): boolean {
|
|
|
81
114
|
return false;
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
export {
|
|
117
|
+
export {
|
|
118
|
+
type CallConversionResult,
|
|
119
|
+
extractGoModuleName,
|
|
120
|
+
extractPackageName,
|
|
121
|
+
isTypeDeclaration,
|
|
122
|
+
outgoingCallsToEdges,
|
|
123
|
+
};
|
package/src/lsp/client.ts
CHANGED
|
@@ -80,7 +80,12 @@ async function createLspClient(opts: LspClientOptions): Promise<LspClient> {
|
|
|
80
80
|
|
|
81
81
|
try {
|
|
82
82
|
const msg = JSON.parse(body) as JsonRpcMessage;
|
|
83
|
-
if (msg.
|
|
83
|
+
if (msg.method && msg.id !== undefined) {
|
|
84
|
+
// Server-initiated request — reply with null to unblock the server
|
|
85
|
+
const reply = JSON.stringify({ jsonrpc: "2.0", id: msg.id, result: null });
|
|
86
|
+
const replyHeader = `Content-Length: ${Buffer.byteLength(reply)}\r\n\r\n`;
|
|
87
|
+
proc.stdin?.write(replyHeader + reply);
|
|
88
|
+
} else if (msg.id !== undefined && pending.has(msg.id)) {
|
|
84
89
|
const handler = pending.get(msg.id);
|
|
85
90
|
pending.delete(msg.id);
|
|
86
91
|
if (msg.error) {
|
|
@@ -95,7 +100,7 @@ async function createLspClient(opts: LspClientOptions): Promise<LspClient> {
|
|
|
95
100
|
}
|
|
96
101
|
});
|
|
97
102
|
|
|
98
|
-
function sendRequest(method: string, params: unknown): Promise<unknown> {
|
|
103
|
+
function sendRequest(method: string, params: unknown, timeoutMs = 30000): Promise<unknown> {
|
|
99
104
|
const id = nextId++;
|
|
100
105
|
const msg: JsonRpcMessage = { jsonrpc: "2.0", id, method, params };
|
|
101
106
|
const body = JSON.stringify(msg);
|
|
@@ -103,7 +108,20 @@ async function createLspClient(opts: LspClientOptions): Promise<LspClient> {
|
|
|
103
108
|
proc.stdin?.write(header + body);
|
|
104
109
|
|
|
105
110
|
return new Promise((resolve, reject) => {
|
|
106
|
-
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
pending.delete(id);
|
|
113
|
+
reject(new Error(`LSP request ${method} timed out after ${timeoutMs}ms`));
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
pending.set(id, {
|
|
116
|
+
resolve: (v: unknown) => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
resolve(v);
|
|
119
|
+
},
|
|
120
|
+
reject: (e: Error) => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
reject(e);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
107
125
|
});
|
|
108
126
|
}
|
|
109
127
|
|
|
@@ -144,7 +162,7 @@ async function createLspClient(opts: LspClientOptions): Promise<LspClient> {
|
|
|
144
162
|
const client: LspClient = {
|
|
145
163
|
async waitForReady(probePath: string): Promise<void> {
|
|
146
164
|
openFile(probePath);
|
|
147
|
-
const delays = [100, 200, 400, 800, 1600, 3200];
|
|
165
|
+
const delays = [100, 200, 400, 800, 1600, 3200, 5000, 5000];
|
|
148
166
|
for (const delay of delays) {
|
|
149
167
|
const result = (await sendRequest("textDocument/documentSymbol", {
|
|
150
168
|
textDocument: { uri: `file://${probePath}` },
|
|
@@ -168,10 +186,14 @@ async function createLspClient(opts: LspClientOptions): Promise<LspClient> {
|
|
|
168
186
|
character: number,
|
|
169
187
|
): Promise<readonly CallHierarchyItem[]> {
|
|
170
188
|
openFile(filePath);
|
|
171
|
-
const result = await sendRequest(
|
|
172
|
-
textDocument
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
const result = await sendRequest(
|
|
190
|
+
"textDocument/prepareCallHierarchy",
|
|
191
|
+
{
|
|
192
|
+
textDocument: { uri: `file://${filePath}` },
|
|
193
|
+
position: { line, character },
|
|
194
|
+
},
|
|
195
|
+
5000,
|
|
196
|
+
);
|
|
175
197
|
return (result as readonly CallHierarchyItem[]) ?? [];
|
|
176
198
|
},
|
|
177
199
|
|
package/src/lsp/symbols.ts
CHANGED
|
@@ -18,7 +18,7 @@ function documentSymbolsToNodes(
|
|
|
18
18
|
isTest: boolean,
|
|
19
19
|
): readonly Node[] {
|
|
20
20
|
const nodes: Node[] = [];
|
|
21
|
-
flattenSymbols(symbols, filePath, language, isTest, [], nodes);
|
|
21
|
+
flattenSymbols(symbols, filePath, language, isTest, [], nodes, new Map());
|
|
22
22
|
return nodes;
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -29,19 +29,20 @@ function flattenSymbols(
|
|
|
29
29
|
isTest: boolean,
|
|
30
30
|
parentNames: readonly string[],
|
|
31
31
|
results: Node[],
|
|
32
|
+
seenIds: Map<string, number>,
|
|
32
33
|
): void {
|
|
33
34
|
for (const sym of symbols) {
|
|
34
35
|
const kind = symbolKindToNodeKind(sym.kind);
|
|
35
36
|
if (!kind) {
|
|
36
37
|
// Still recurse into children for non-matching kinds (e.g., modules)
|
|
37
38
|
if (sym.children) {
|
|
38
|
-
flattenSymbols(sym.children, filePath, language, isTest, parentNames, results);
|
|
39
|
+
flattenSymbols(sym.children, filePath, language, isTest, parentNames, results, seenIds);
|
|
39
40
|
}
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const qualifiedName = [...parentNames, sym.name].join(".");
|
|
44
|
-
const id = `${filePath}::${qualifiedName}
|
|
45
|
+
const id = deduplicateId(`${filePath}::${qualifiedName}`, seenIds);
|
|
45
46
|
|
|
46
47
|
results.push({
|
|
47
48
|
id,
|
|
@@ -57,17 +58,33 @@ function flattenSymbols(
|
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
if (sym.children) {
|
|
60
|
-
flattenSymbols(
|
|
61
|
+
flattenSymbols(
|
|
62
|
+
sym.children,
|
|
63
|
+
filePath,
|
|
64
|
+
language,
|
|
65
|
+
isTest,
|
|
66
|
+
[...parentNames, sym.name],
|
|
67
|
+
results,
|
|
68
|
+
seenIds,
|
|
69
|
+
);
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
/** Returns a unique ID, appending $N for duplicates (e.g., Go's multiple init() per file). */
|
|
75
|
+
function deduplicateId(baseId: string, seenIds: Map<string, number>): string {
|
|
76
|
+
const count = seenIds.get(baseId) ?? 0;
|
|
77
|
+
seenIds.set(baseId, count + 1);
|
|
78
|
+
return count === 0 ? baseId : `${baseId}$${count + 1}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
function symbolKindToNodeKind(kind: number): NodeKind | undefined {
|
|
66
82
|
if (kind === SymbolKind.Function) return "function";
|
|
67
83
|
if (kind === SymbolKind.Method) return "method";
|
|
68
84
|
if (kind === SymbolKind.Constructor) return "method";
|
|
69
85
|
if (kind === SymbolKind.Class) return "class";
|
|
70
86
|
if (kind === SymbolKind.Interface) return "type";
|
|
87
|
+
if (kind === SymbolKind.Struct) return "class";
|
|
71
88
|
return undefined;
|
|
72
89
|
}
|
|
73
90
|
|
|
@@ -89,7 +106,7 @@ function documentSymbolsToNodesWithPositions(
|
|
|
89
106
|
isTest: boolean,
|
|
90
107
|
): readonly NodeWithPosition[] {
|
|
91
108
|
const results: NodeWithPosition[] = [];
|
|
92
|
-
flattenSymbolsWithPositions(symbols, filePath, language, isTest, [], results);
|
|
109
|
+
flattenSymbolsWithPositions(symbols, filePath, language, isTest, [], results, new Map());
|
|
93
110
|
return results;
|
|
94
111
|
}
|
|
95
112
|
|
|
@@ -100,18 +117,27 @@ function flattenSymbolsWithPositions(
|
|
|
100
117
|
isTest: boolean,
|
|
101
118
|
parentNames: readonly string[],
|
|
102
119
|
results: NodeWithPosition[],
|
|
120
|
+
seenIds: Map<string, number>,
|
|
103
121
|
): void {
|
|
104
122
|
for (const sym of symbols) {
|
|
105
123
|
const kind = symbolKindToNodeKind(sym.kind);
|
|
106
124
|
if (!kind) {
|
|
107
125
|
if (sym.children) {
|
|
108
|
-
flattenSymbolsWithPositions(
|
|
126
|
+
flattenSymbolsWithPositions(
|
|
127
|
+
sym.children,
|
|
128
|
+
filePath,
|
|
129
|
+
language,
|
|
130
|
+
isTest,
|
|
131
|
+
parentNames,
|
|
132
|
+
results,
|
|
133
|
+
seenIds,
|
|
134
|
+
);
|
|
109
135
|
}
|
|
110
136
|
continue;
|
|
111
137
|
}
|
|
112
138
|
|
|
113
139
|
const qualifiedName = [...parentNames, sym.name].join(".");
|
|
114
|
-
const id = `${filePath}::${qualifiedName}
|
|
140
|
+
const id = deduplicateId(`${filePath}::${qualifiedName}`, seenIds);
|
|
115
141
|
|
|
116
142
|
results.push({
|
|
117
143
|
node: {
|
|
@@ -138,6 +164,7 @@ function flattenSymbolsWithPositions(
|
|
|
138
164
|
isTest,
|
|
139
165
|
[...parentNames, sym.name],
|
|
140
166
|
results,
|
|
167
|
+
seenIds,
|
|
141
168
|
);
|
|
142
169
|
}
|
|
143
170
|
}
|
package/src/lsp/types.ts
CHANGED
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/config.ts
CHANGED
|
@@ -11,6 +11,12 @@ type TypeScriptConfig = {
|
|
|
11
11
|
readonly tsconfig: string | undefined;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
/** Configuration for a Go language section. */
|
|
15
|
+
type GoConfig = {
|
|
16
|
+
readonly sourceRoots: readonly string[];
|
|
17
|
+
readonly testPaths: readonly string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
/** Lint-specific configuration. */
|
|
15
21
|
type LintConfig = {
|
|
16
22
|
readonly strict: boolean;
|
|
@@ -24,7 +30,8 @@ type LatticeConfig = {
|
|
|
24
30
|
readonly exclude: readonly string[];
|
|
25
31
|
readonly python: PythonConfig | undefined;
|
|
26
32
|
readonly typescript: TypeScriptConfig | undefined;
|
|
33
|
+
readonly go: GoConfig | undefined;
|
|
27
34
|
readonly lint: LintConfig;
|
|
28
35
|
};
|
|
29
36
|
|
|
30
|
-
export type { LatticeConfig, LintConfig, PythonConfig, TypeScriptConfig };
|
|
37
|
+
export type { GoConfig, LatticeConfig, LintConfig, PythonConfig, TypeScriptConfig };
|
package/scripts/postinstall.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Postinstall script — creates a minimal Python venv and installs zuban into it.
|
|
4
|
-
* This gives zuban a proper Python environment with typeshed stubs.
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
const VENDOR_DIR = join(import.meta.dir, "..", "vendor");
|
|
10
|
-
const VENV_DIR = join(VENDOR_DIR, "venv");
|
|
11
|
-
|
|
12
|
-
function findPython(): string | undefined {
|
|
13
|
-
for (const cmd of ["python3", "python"]) {
|
|
14
|
-
if (Bun.which(cmd)) return cmd;
|
|
15
|
-
}
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function main() {
|
|
20
|
-
const isWindows = process.platform === "win32";
|
|
21
|
-
const zubanBin = join(VENV_DIR, isWindows ? "Scripts" : "bin", isWindows ? "zubanls.exe" : "zubanls");
|
|
22
|
-
|
|
23
|
-
if (existsSync(zubanBin)) {
|
|
24
|
-
console.log("zuban already installed");
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const python = findPython();
|
|
29
|
-
if (!python) {
|
|
30
|
-
console.warn("Warning: Python not found. Python support requires python3 in PATH.");
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
console.log("Installing zuban for Python support...");
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
// Create venv
|
|
38
|
-
const venvResult = Bun.spawnSync([python, "-m", "venv", VENV_DIR], {
|
|
39
|
-
stdout: "ignore",
|
|
40
|
-
stderr: "pipe",
|
|
41
|
-
});
|
|
42
|
-
if (venvResult.exitCode !== 0) {
|
|
43
|
-
throw new Error(`Failed to create venv: ${venvResult.stderr.toString()}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Install zuban via pip
|
|
47
|
-
const pip = join(VENV_DIR, isWindows ? "Scripts" : "bin", "pip");
|
|
48
|
-
const pipResult = Bun.spawnSync([pip, "install", "zuban", "--quiet"], {
|
|
49
|
-
stdout: "ignore",
|
|
50
|
-
stderr: "pipe",
|
|
51
|
-
});
|
|
52
|
-
if (pipResult.exitCode !== 0) {
|
|
53
|
-
throw new Error(`Failed to install zuban: ${pipResult.stderr.toString()}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!existsSync(zubanBin)) {
|
|
57
|
-
throw new Error("zubanls binary not found after installation");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
console.log("zuban installed successfully");
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.warn(
|
|
63
|
-
`Warning: Failed to install zuban: ${error instanceof Error ? error.message : error}. ` +
|
|
64
|
-
"Python support requires: pip install zuban",
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
main();
|