enigma-cli 1.43.0 → 1.45.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/assets/registry/primitives/registry.json +498 -8
- package/assets/registry/utils/registry.json +4 -2
- package/assets/skills/anti-overengineering-policy/skill.json +1 -1
- package/assets/skills/anti-overengineering-review/skill.json +1 -1
- package/assets/skills/backend-policy/skill.json +1 -1
- package/assets/skills/ciphera-style-policy/skill.json +1 -1
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/skill.json +1 -1
- package/assets/skills/database-expert/skill.json +1 -1
- package/assets/skills/debugging-policy/skill.json +1 -1
- package/assets/skills/dependency-policy/skill.json +1 -1
- package/assets/skills/email-policy/skill.json +1 -1
- package/assets/skills/frontend-design/skill.json +1 -1
- package/assets/skills/frontend-policy/SKILL.md +21 -1
- package/assets/skills/frontend-policy/skill.json +2 -2
- package/assets/skills/git-policy/skill.json +1 -1
- package/assets/skills/logo-sourcing-policy/skill.json +1 -1
- package/assets/skills/security-policy/skill.json +1 -1
- package/assets/skills/skill-creator/skill.json +1 -1
- package/assets/skills/task-completion-policy/skill.json +1 -1
- package/assets/skills/technical-writing-policy/skill.json +1 -1
- package/assets/skills/testing-policy/skill.json +1 -1
- package/assets/skills/validation-policy/skill.json +1 -1
- package/bin/checksums.json +4 -4
- package/bin/enigma.mjs +34 -0
- package/dist/codegraph-hook.js +2483 -0
- package/dist/guardrails.js +130 -0
- package/dist/post-edit.js +131 -1
- package/package.json +1 -1
|
@@ -0,0 +1,2483 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/util.ts
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
import { dirname as dirname2, join, relative, resolve, sep } from "path";
|
|
16
|
+
import { existsSync, statSync, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
17
|
+
function isDir(pth) {
|
|
18
|
+
try {
|
|
19
|
+
return statSync(pth).isDirectory();
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function enigmaHome() {
|
|
25
|
+
return process.env.ENIGMA_CONFIG_HOME || homedir();
|
|
26
|
+
}
|
|
27
|
+
function readJson(file) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(readFileSync2(file, "utf8").replace(/^\uFEFF/, ""));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
var init_util = __esm({
|
|
35
|
+
"src/util.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/secret-box.ts
|
|
41
|
+
import { join as join2 } from "path";
|
|
42
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
43
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
|
|
44
|
+
function keyPath() {
|
|
45
|
+
return join2(enigmaHome(), ".enigma.key");
|
|
46
|
+
}
|
|
47
|
+
function loadKey() {
|
|
48
|
+
const path = keyPath();
|
|
49
|
+
if (existsSync2(path)) {
|
|
50
|
+
const hex = readFileSync3(path, "utf8").trim();
|
|
51
|
+
const key2 = Buffer.from(hex, "hex");
|
|
52
|
+
if (key2.length === KEY_BYTES) return key2;
|
|
53
|
+
}
|
|
54
|
+
const key = randomBytes(KEY_BYTES);
|
|
55
|
+
writeFileSync2(path, key.toString("hex"), { mode: 384 });
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
function isEncryptedSecret(value) {
|
|
59
|
+
return typeof value === "string" && value.startsWith(PREFIX);
|
|
60
|
+
}
|
|
61
|
+
function encryptSecret(plain) {
|
|
62
|
+
if (!plain) return "";
|
|
63
|
+
if (isEncryptedSecret(plain)) return plain;
|
|
64
|
+
const iv = randomBytes(IV_BYTES);
|
|
65
|
+
const cipher = createCipheriv("aes-256-gcm", loadKey(), iv);
|
|
66
|
+
const ct = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
67
|
+
const tag = cipher.getAuthTag();
|
|
68
|
+
return `${PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
|
|
69
|
+
}
|
|
70
|
+
function decryptSecret(stored) {
|
|
71
|
+
if (!stored) return "";
|
|
72
|
+
if (!isEncryptedSecret(stored)) return stored;
|
|
73
|
+
try {
|
|
74
|
+
const [ivB64, tagB64, ctB64] = stored.slice(PREFIX.length).split(":");
|
|
75
|
+
if (!ivB64 || !tagB64 || !ctB64) return "";
|
|
76
|
+
const decipher = createDecipheriv("aes-256-gcm", loadKey(), Buffer.from(ivB64, "base64"));
|
|
77
|
+
decipher.setAuthTag(Buffer.from(tagB64, "base64"));
|
|
78
|
+
return Buffer.concat([decipher.update(Buffer.from(ctB64, "base64")), decipher.final()]).toString("utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
var PREFIX, KEY_BYTES, IV_BYTES;
|
|
84
|
+
var init_secret_box = __esm({
|
|
85
|
+
"src/secret-box.ts"() {
|
|
86
|
+
"use strict";
|
|
87
|
+
init_util();
|
|
88
|
+
PREFIX = "enc:v1:";
|
|
89
|
+
KEY_BYTES = 32;
|
|
90
|
+
IV_BYTES = 12;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// src/config.ts
|
|
95
|
+
var config_exports = {};
|
|
96
|
+
__export(config_exports, {
|
|
97
|
+
CONFIG_CHOICES: () => CONFIG_CHOICES,
|
|
98
|
+
CONFIG_DEFAULTS: () => CONFIG_DEFAULTS,
|
|
99
|
+
CONFIG_FILE: () => CONFIG_FILE,
|
|
100
|
+
DASHBOARD_BINDS: () => DASHBOARD_BINDS,
|
|
101
|
+
DASHBOARD_MODES: () => DASHBOARD_MODES,
|
|
102
|
+
GATE_SEVERITIES: () => GATE_SEVERITIES,
|
|
103
|
+
LOGO_COLOR_POLICIES: () => LOGO_COLOR_POLICIES,
|
|
104
|
+
MINIMAL_CODE_LEVELS: () => MINIMAL_CODE_LEVELS,
|
|
105
|
+
OUTPUT_STYLES: () => OUTPUT_STYLES,
|
|
106
|
+
PROMPT_SECRET_MODES: () => PROMPT_SECRET_MODES,
|
|
107
|
+
RECALL_PROVIDERS: () => RECALL_PROVIDERS,
|
|
108
|
+
SKILL_UPDATE_POLICIES: () => SKILL_UPDATE_POLICIES,
|
|
109
|
+
getRecallApiKey: () => getRecallApiKey,
|
|
110
|
+
readConfig: () => readConfig,
|
|
111
|
+
readConfigAt: () => readConfigAt,
|
|
112
|
+
readEnigmaList: () => readEnigmaList,
|
|
113
|
+
readGlobalConfig: () => readGlobalConfig,
|
|
114
|
+
readProjectConfig: () => readProjectConfig,
|
|
115
|
+
setBypassDisabled: () => setBypassDisabled,
|
|
116
|
+
setEnigmaToggle: () => setEnigmaToggle,
|
|
117
|
+
setEnigmaValue: () => setEnigmaValue,
|
|
118
|
+
setEnigmaValueAt: () => setEnigmaValueAt,
|
|
119
|
+
setPackAccount: () => setPackAccount,
|
|
120
|
+
setRecallApiKey: () => setRecallApiKey,
|
|
121
|
+
setSkillAgentOff: () => setSkillAgentOff,
|
|
122
|
+
setSkillDiscarded: () => setSkillDiscarded,
|
|
123
|
+
setToolPath: () => setToolPath,
|
|
124
|
+
unsetEnigmaValueAt: () => unsetEnigmaValueAt,
|
|
125
|
+
updateEnigmaList: () => updateEnigmaList
|
|
126
|
+
});
|
|
127
|
+
import { join as join3 } from "path";
|
|
128
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
129
|
+
function configPath(scope) {
|
|
130
|
+
if (scope === "local") return join3(process.cwd(), CONFIG_FILE);
|
|
131
|
+
return join3(enigmaHome(), CONFIG_FILE);
|
|
132
|
+
}
|
|
133
|
+
function mergeConfigFiles(paths) {
|
|
134
|
+
const sources = [];
|
|
135
|
+
let config = { ...CONFIG_DEFAULTS };
|
|
136
|
+
for (const path of paths) {
|
|
137
|
+
const raw = existsSync3(path) ? readJson(path) : null;
|
|
138
|
+
if (raw) {
|
|
139
|
+
config = { ...config, ...raw };
|
|
140
|
+
sources.push(path);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { config, sources };
|
|
144
|
+
}
|
|
145
|
+
function readConfig() {
|
|
146
|
+
return mergeConfigFiles([configPath("global"), configPath("local")]);
|
|
147
|
+
}
|
|
148
|
+
function readGlobalConfig() {
|
|
149
|
+
return mergeConfigFiles([configPath("global")]).config;
|
|
150
|
+
}
|
|
151
|
+
function readConfigAt(projectDir2) {
|
|
152
|
+
return mergeConfigFiles([configPath("global"), join3(projectDir2, CONFIG_FILE)]).config;
|
|
153
|
+
}
|
|
154
|
+
function readProjectConfig(projectDir2) {
|
|
155
|
+
const path = join3(projectDir2, CONFIG_FILE);
|
|
156
|
+
return (existsSync3(path) ? readJson(path) : null) || {};
|
|
157
|
+
}
|
|
158
|
+
function writeConfigFile(path, next) {
|
|
159
|
+
const dir = join3(path, "..");
|
|
160
|
+
if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
|
|
161
|
+
writeFileSync3(path, `${JSON.stringify(next, null, 2)}
|
|
162
|
+
`);
|
|
163
|
+
return path;
|
|
164
|
+
}
|
|
165
|
+
function setEnigmaValue(key, value, scope) {
|
|
166
|
+
const path = configPath(scope);
|
|
167
|
+
return writeConfigFile(path, { ...readJson(path) || {}, [key]: value });
|
|
168
|
+
}
|
|
169
|
+
function setEnigmaValueAt(projectDir2, key, value) {
|
|
170
|
+
const path = join3(projectDir2, CONFIG_FILE);
|
|
171
|
+
return writeConfigFile(path, { ...readJson(path) || {}, [key]: value });
|
|
172
|
+
}
|
|
173
|
+
function unsetEnigmaValueAt(projectDir2, key) {
|
|
174
|
+
const path = join3(projectDir2, CONFIG_FILE);
|
|
175
|
+
const current = readJson(path) || {};
|
|
176
|
+
delete current[key];
|
|
177
|
+
return writeConfigFile(path, current);
|
|
178
|
+
}
|
|
179
|
+
function setEnigmaToggle(key, value, scope) {
|
|
180
|
+
return setEnigmaValue(key, value, scope);
|
|
181
|
+
}
|
|
182
|
+
function getRecallApiKey(config) {
|
|
183
|
+
return decryptSecret(config.recallApiKey || "");
|
|
184
|
+
}
|
|
185
|
+
function setRecallApiKey(value, scope) {
|
|
186
|
+
return setEnigmaValue("recallApiKey", value ? encryptSecret(value) : "", scope);
|
|
187
|
+
}
|
|
188
|
+
function setToolPath(tool, binPath, scope = "global") {
|
|
189
|
+
const path = configPath(scope);
|
|
190
|
+
const current = readJson(path) || {};
|
|
191
|
+
const existing = current.toolPaths && typeof current.toolPaths === "object" ? current.toolPaths : {};
|
|
192
|
+
const next = { ...current, toolPaths: { ...existing, [tool]: binPath } };
|
|
193
|
+
const dir = join3(path, "..");
|
|
194
|
+
if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
|
|
195
|
+
writeFileSync3(path, JSON.stringify(next, null, 2) + "\n");
|
|
196
|
+
return path;
|
|
197
|
+
}
|
|
198
|
+
function setPackAccount(key, account, scope = "global") {
|
|
199
|
+
const path = configPath(scope);
|
|
200
|
+
const current = readJson(path) || {};
|
|
201
|
+
const existing = current.packAccounts && typeof current.packAccounts === "object" ? { ...current.packAccounts } : {};
|
|
202
|
+
if (account === null) delete existing[key];
|
|
203
|
+
else existing[key] = account;
|
|
204
|
+
const dir = join3(path, "..");
|
|
205
|
+
if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
|
|
206
|
+
writeFileSync3(path, `${JSON.stringify({ ...current, packAccounts: existing }, null, 2)}
|
|
207
|
+
`);
|
|
208
|
+
return path;
|
|
209
|
+
}
|
|
210
|
+
function updateEnigmaList(key, name, include, scope) {
|
|
211
|
+
const set = new Set((readJson(configPath(scope)) || {})[key] || []);
|
|
212
|
+
if (include) set.add(name);
|
|
213
|
+
else set.delete(name);
|
|
214
|
+
return setEnigmaValue(key, [...set], scope);
|
|
215
|
+
}
|
|
216
|
+
function readEnigmaList(key, scope) {
|
|
217
|
+
return (readJson(configPath(scope)) || {})[key] || [];
|
|
218
|
+
}
|
|
219
|
+
function updateGlobalList(key, name, include) {
|
|
220
|
+
updateEnigmaList(key, name, include, "global");
|
|
221
|
+
}
|
|
222
|
+
function setBypassDisabled(name, disabled) {
|
|
223
|
+
updateGlobalList("bypassDisabled", name, disabled);
|
|
224
|
+
}
|
|
225
|
+
function setSkillDiscarded(name, discarded) {
|
|
226
|
+
updateGlobalList("discardedSkills", name, discarded);
|
|
227
|
+
}
|
|
228
|
+
function setSkillAgentOff(skill, agent, off) {
|
|
229
|
+
const path = configPath("global");
|
|
230
|
+
const current = readJson(path) || {};
|
|
231
|
+
const existing = current.skillAgentsOff && typeof current.skillAgentsOff === "object" ? current.skillAgentsOff : {};
|
|
232
|
+
const list = new Set(Array.isArray(existing[skill]) ? existing[skill] : []);
|
|
233
|
+
if (off) list.add(agent);
|
|
234
|
+
else list.delete(agent);
|
|
235
|
+
const nextMap = { ...existing };
|
|
236
|
+
if (list.size) nextMap[skill] = [...list];
|
|
237
|
+
else delete nextMap[skill];
|
|
238
|
+
const next = { ...current, skillAgentsOff: nextMap };
|
|
239
|
+
const dir = join3(path, "..");
|
|
240
|
+
if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
|
|
241
|
+
writeFileSync3(path, JSON.stringify(next, null, 2) + "\n");
|
|
242
|
+
}
|
|
243
|
+
var CONFIG_FILE, OUTPUT_STYLES, MINIMAL_CODE_LEVELS, DASHBOARD_MODES, DASHBOARD_BINDS, PROMPT_SECRET_MODES, SKILL_UPDATE_POLICIES, LOGO_COLOR_POLICIES, RECALL_PROVIDERS, GATE_SEVERITIES, CONFIG_DEFAULTS, CONFIG_CHOICES;
|
|
244
|
+
var init_config = __esm({
|
|
245
|
+
"src/config.ts"() {
|
|
246
|
+
"use strict";
|
|
247
|
+
init_util();
|
|
248
|
+
init_secret_box();
|
|
249
|
+
CONFIG_FILE = ".enigma.json";
|
|
250
|
+
OUTPUT_STYLES = ["off", "lite", "full", "ultra"];
|
|
251
|
+
MINIMAL_CODE_LEVELS = ["off", "lite", "full", "ultra"];
|
|
252
|
+
DASHBOARD_MODES = ["off", "on-demand", "always"];
|
|
253
|
+
DASHBOARD_BINDS = ["loopback", "lan", "custom"];
|
|
254
|
+
PROMPT_SECRET_MODES = ["redact", "reject"];
|
|
255
|
+
SKILL_UPDATE_POLICIES = ["overwrite", "keep"];
|
|
256
|
+
LOGO_COLOR_POLICIES = ["ask", "adapt-background", "adapt-logo"];
|
|
257
|
+
RECALL_PROVIDERS = ["claude-local", "anthropic", "openai"];
|
|
258
|
+
GATE_SEVERITIES = ["error", "warning", "info"];
|
|
259
|
+
CONFIG_DEFAULTS = {
|
|
260
|
+
commitEmoji: true,
|
|
261
|
+
updateNotifier: true,
|
|
262
|
+
fullscreen: true,
|
|
263
|
+
parallelSubagents: false,
|
|
264
|
+
outputStyle: "off",
|
|
265
|
+
minimalCode: "full",
|
|
266
|
+
logoColorPolicy: "ask",
|
|
267
|
+
autoSync: true,
|
|
268
|
+
statusline: true,
|
|
269
|
+
statuslineRefresh: 10,
|
|
270
|
+
claudeTrust: true,
|
|
271
|
+
kimiTrust: true,
|
|
272
|
+
remoteSkills: true,
|
|
273
|
+
skillUpdatePolicy: "overwrite",
|
|
274
|
+
permissionBypass: true,
|
|
275
|
+
autoLint: false,
|
|
276
|
+
guardrails: true,
|
|
277
|
+
trim: true,
|
|
278
|
+
verify: true,
|
|
279
|
+
verifyCommand: "",
|
|
280
|
+
compress: false,
|
|
281
|
+
codeGraph: true,
|
|
282
|
+
gate: true,
|
|
283
|
+
dashboard: "off",
|
|
284
|
+
tokenPrice: 0,
|
|
285
|
+
tokenSpeed: 0,
|
|
286
|
+
usageStats: false,
|
|
287
|
+
recall: false,
|
|
288
|
+
recallLlm: true,
|
|
289
|
+
recallProvider: "claude-local",
|
|
290
|
+
recallModel: "",
|
|
291
|
+
recallApiBase: "",
|
|
292
|
+
recallApiKey: "",
|
|
293
|
+
proxy: false,
|
|
294
|
+
usageApi: false,
|
|
295
|
+
promptSecretGuard: false,
|
|
296
|
+
promptSecretMode: "redact",
|
|
297
|
+
resourceCap: 60,
|
|
298
|
+
lowMemoryCap: 80,
|
|
299
|
+
planSessionLimit: 0,
|
|
300
|
+
planWeeklyLimit: 0,
|
|
301
|
+
planWeeklySonnetLimit: 0,
|
|
302
|
+
planWeeklyOpusLimit: 0,
|
|
303
|
+
planWeeklyReset: "mon 00:00",
|
|
304
|
+
dashboardLive: true,
|
|
305
|
+
dashboardPort: 0,
|
|
306
|
+
dashboardBind: "loopback",
|
|
307
|
+
dashboardBindAddress: "",
|
|
308
|
+
apiPort: 8e3,
|
|
309
|
+
apiAccount: "",
|
|
310
|
+
apiProfile: "",
|
|
311
|
+
apiPack: "",
|
|
312
|
+
toolPaths: {},
|
|
313
|
+
bypassDisabled: [],
|
|
314
|
+
discardedSkills: [],
|
|
315
|
+
skillAgentsOff: {},
|
|
316
|
+
packs: [],
|
|
317
|
+
packAccounts: {},
|
|
318
|
+
gateProtectedBranches: [],
|
|
319
|
+
gateSeverity: "warning",
|
|
320
|
+
gateTidyBranches: true,
|
|
321
|
+
ciWatch: true
|
|
322
|
+
};
|
|
323
|
+
CONFIG_CHOICES = {
|
|
324
|
+
outputStyle: OUTPUT_STYLES,
|
|
325
|
+
minimalCode: MINIMAL_CODE_LEVELS,
|
|
326
|
+
dashboard: DASHBOARD_MODES,
|
|
327
|
+
dashboardBind: DASHBOARD_BINDS,
|
|
328
|
+
promptSecretMode: PROMPT_SECRET_MODES,
|
|
329
|
+
skillUpdatePolicy: SKILL_UPDATE_POLICIES,
|
|
330
|
+
logoColorPolicy: LOGO_COLOR_POLICIES,
|
|
331
|
+
recallProvider: RECALL_PROVIDERS,
|
|
332
|
+
gateSeverity: GATE_SEVERITIES
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// src/codegraph-types.ts
|
|
338
|
+
var WALK_RELATIONS;
|
|
339
|
+
var init_codegraph_types = __esm({
|
|
340
|
+
"src/codegraph-types.ts"() {
|
|
341
|
+
"use strict";
|
|
342
|
+
WALK_RELATIONS = /* @__PURE__ */ new Set(["imports", "calls", "references", "extends", "implements"]);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// src/codegraph-rank.ts
|
|
347
|
+
function tokenize(text) {
|
|
348
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP.has(t));
|
|
349
|
+
}
|
|
350
|
+
function counts(tokens) {
|
|
351
|
+
const m = /* @__PURE__ */ new Map();
|
|
352
|
+
for (const t of tokens) m.set(t, (m.get(t) ?? 0) + 1);
|
|
353
|
+
return m;
|
|
354
|
+
}
|
|
355
|
+
function computeIdf(bags) {
|
|
356
|
+
const df = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const bag of bags) for (const t of bag) df.set(t, (df.get(t) ?? 0) + 1);
|
|
358
|
+
const idf = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const [t, d] of df) idf.set(t, Math.log(1 + bags.length / (1 + d)));
|
|
360
|
+
return idf;
|
|
361
|
+
}
|
|
362
|
+
function overlapScore(query, doc, idf) {
|
|
363
|
+
let s = 0;
|
|
364
|
+
for (const [t, qn] of query) {
|
|
365
|
+
const dn = doc.get(t);
|
|
366
|
+
if (dn) s += qn * dn * (idf.get(t) ?? 1);
|
|
367
|
+
}
|
|
368
|
+
return s;
|
|
369
|
+
}
|
|
370
|
+
function bm25(query, doc, idf, dl, avgdl) {
|
|
371
|
+
const k1 = 1.2;
|
|
372
|
+
const b = 0.75;
|
|
373
|
+
const norm = k1 * (1 - b + b * dl / (avgdl || 1));
|
|
374
|
+
let s = 0;
|
|
375
|
+
for (const t of query.keys()) {
|
|
376
|
+
const tf = doc.get(t);
|
|
377
|
+
if (tf) s += (idf.get(t) ?? 1) * (tf * (k1 + 1) / (tf + norm));
|
|
378
|
+
}
|
|
379
|
+
return s;
|
|
380
|
+
}
|
|
381
|
+
function bagLength(m) {
|
|
382
|
+
let s = 0;
|
|
383
|
+
for (const v of m.values()) s += v;
|
|
384
|
+
return s;
|
|
385
|
+
}
|
|
386
|
+
function hasTerm(f, t) {
|
|
387
|
+
return f.has(t) || f.has(`${t}s`) || t.endsWith("s") && f.has(t.slice(0, -1));
|
|
388
|
+
}
|
|
389
|
+
function matchedShare(query, fields, idf, defaultIdf) {
|
|
390
|
+
let matched = 0;
|
|
391
|
+
let total = 0;
|
|
392
|
+
for (const t of query.keys()) {
|
|
393
|
+
const w = idf.get(t) ?? defaultIdf;
|
|
394
|
+
total += w;
|
|
395
|
+
if (fields.some((f) => hasTerm(f, t))) matched += w;
|
|
396
|
+
}
|
|
397
|
+
return total > 0 ? matched / total : 0;
|
|
398
|
+
}
|
|
399
|
+
function personalizedPageRank(nodeIds, edges, seeds, opts = {}) {
|
|
400
|
+
const alpha = opts.alpha ?? 0.25;
|
|
401
|
+
const iters = opts.iters ?? 25;
|
|
402
|
+
const ids = /* @__PURE__ */ new Set();
|
|
403
|
+
for (const id of nodeIds) if (!opts.nodeFilter || opts.nodeFilter(id)) ids.add(id);
|
|
404
|
+
const adj = /* @__PURE__ */ new Map();
|
|
405
|
+
const link = (a, b) => {
|
|
406
|
+
const arr = adj.get(a);
|
|
407
|
+
if (arr) arr.push(b);
|
|
408
|
+
else adj.set(a, [b]);
|
|
409
|
+
};
|
|
410
|
+
for (const [source, target, rel] of edges) {
|
|
411
|
+
if (!WALK_RELATIONS.has(rel)) continue;
|
|
412
|
+
if (!ids.has(source) || !ids.has(target)) continue;
|
|
413
|
+
link(source, target);
|
|
414
|
+
link(target, source);
|
|
415
|
+
}
|
|
416
|
+
let seedTotal = 0;
|
|
417
|
+
for (const [id, w] of seeds) if (ids.has(id) && w > 0) seedTotal += w;
|
|
418
|
+
if (seedTotal <= 0) return /* @__PURE__ */ new Map();
|
|
419
|
+
const restart = /* @__PURE__ */ new Map();
|
|
420
|
+
for (const [id, w] of seeds) if (ids.has(id) && w > 0) restart.set(id, w / seedTotal);
|
|
421
|
+
let rank = new Map(restart);
|
|
422
|
+
for (let i = 0; i < iters; i++) {
|
|
423
|
+
const next = /* @__PURE__ */ new Map();
|
|
424
|
+
for (const [id, r] of restart) next.set(id, alpha * r);
|
|
425
|
+
let dangling = 0;
|
|
426
|
+
for (const [id, mass] of rank) {
|
|
427
|
+
const nbrs = adj.get(id);
|
|
428
|
+
if (!nbrs || nbrs.length === 0) {
|
|
429
|
+
dangling += mass;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const share = (1 - alpha) * mass / nbrs.length;
|
|
433
|
+
for (const nb of nbrs) next.set(nb, (next.get(nb) ?? 0) + share);
|
|
434
|
+
}
|
|
435
|
+
if (dangling > 0) {
|
|
436
|
+
const dm = (1 - alpha) * dangling;
|
|
437
|
+
for (const [sid, r] of restart) next.set(sid, (next.get(sid) ?? 0) + dm * r);
|
|
438
|
+
}
|
|
439
|
+
rank = next;
|
|
440
|
+
}
|
|
441
|
+
let max = 0;
|
|
442
|
+
for (const v of rank.values()) if (v > max) max = v;
|
|
443
|
+
if (max <= 0) return /* @__PURE__ */ new Map();
|
|
444
|
+
const out = /* @__PURE__ */ new Map();
|
|
445
|
+
for (const [id, v] of rank) out.set(id, v / max);
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
function pathOfId(id) {
|
|
449
|
+
const hash = id.indexOf("#");
|
|
450
|
+
return hash === -1 ? id : id.slice(0, hash);
|
|
451
|
+
}
|
|
452
|
+
function inDegree(edges, crossFileOnly = false) {
|
|
453
|
+
const deg = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const [source, target, rel] of edges) {
|
|
455
|
+
if (!WALK_RELATIONS.has(rel)) continue;
|
|
456
|
+
if (crossFileOnly && pathOfId(source) === pathOfId(target)) continue;
|
|
457
|
+
deg.set(target, (deg.get(target) ?? 0) + 1);
|
|
458
|
+
}
|
|
459
|
+
return deg;
|
|
460
|
+
}
|
|
461
|
+
function isTestPath(path) {
|
|
462
|
+
return /(^|\/)(tests?|__tests__|spec)\/|(_test|\.test|\.spec)\.[a-z]+$|(^|\/)(test_[^/]+|conftest)\.py$/i.test(path || "");
|
|
463
|
+
}
|
|
464
|
+
var STOP;
|
|
465
|
+
var init_codegraph_rank = __esm({
|
|
466
|
+
"src/codegraph-rank.ts"() {
|
|
467
|
+
"use strict";
|
|
468
|
+
init_codegraph_types();
|
|
469
|
+
STOP = /* @__PURE__ */ new Set([
|
|
470
|
+
"the",
|
|
471
|
+
"a",
|
|
472
|
+
"an",
|
|
473
|
+
"and",
|
|
474
|
+
"or",
|
|
475
|
+
"but",
|
|
476
|
+
"if",
|
|
477
|
+
"then",
|
|
478
|
+
"else",
|
|
479
|
+
"for",
|
|
480
|
+
"of",
|
|
481
|
+
"to",
|
|
482
|
+
"in",
|
|
483
|
+
"on",
|
|
484
|
+
"at",
|
|
485
|
+
"by",
|
|
486
|
+
"with",
|
|
487
|
+
"from",
|
|
488
|
+
"as",
|
|
489
|
+
"is",
|
|
490
|
+
"are",
|
|
491
|
+
"was",
|
|
492
|
+
"were",
|
|
493
|
+
"be",
|
|
494
|
+
"been",
|
|
495
|
+
"it",
|
|
496
|
+
"its",
|
|
497
|
+
"this",
|
|
498
|
+
"that",
|
|
499
|
+
"these",
|
|
500
|
+
"those",
|
|
501
|
+
"there",
|
|
502
|
+
"here",
|
|
503
|
+
"how",
|
|
504
|
+
"what",
|
|
505
|
+
"when",
|
|
506
|
+
"where",
|
|
507
|
+
"which",
|
|
508
|
+
"who",
|
|
509
|
+
"why",
|
|
510
|
+
"do",
|
|
511
|
+
"does",
|
|
512
|
+
"did",
|
|
513
|
+
"can",
|
|
514
|
+
"could",
|
|
515
|
+
"should",
|
|
516
|
+
"would",
|
|
517
|
+
"will",
|
|
518
|
+
"not",
|
|
519
|
+
"no",
|
|
520
|
+
"we",
|
|
521
|
+
"you",
|
|
522
|
+
"i",
|
|
523
|
+
"my",
|
|
524
|
+
"our",
|
|
525
|
+
"your",
|
|
526
|
+
"me",
|
|
527
|
+
"us",
|
|
528
|
+
"get",
|
|
529
|
+
"set",
|
|
530
|
+
"new",
|
|
531
|
+
"all",
|
|
532
|
+
"any",
|
|
533
|
+
"some",
|
|
534
|
+
"into",
|
|
535
|
+
"out"
|
|
536
|
+
]);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// src/codegraph-extract.ts
|
|
541
|
+
import { execFileSync } from "child_process";
|
|
542
|
+
import { extname, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
543
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
544
|
+
function symRules(lang) {
|
|
545
|
+
switch (lang) {
|
|
546
|
+
case "ts":
|
|
547
|
+
case "js":
|
|
548
|
+
return [
|
|
549
|
+
{ kind: "function", re: /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\*?\s+([A-Za-z_$][\w$]*)/gm },
|
|
550
|
+
{ kind: "class", re: /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/gm },
|
|
551
|
+
{ kind: "interface", re: /^\s*(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/gm },
|
|
552
|
+
{ kind: "type", re: /^\s*(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\s*[=<]/gm },
|
|
553
|
+
{ kind: "enum", re: /^\s*(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)/gm },
|
|
554
|
+
{ kind: "function", re: /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*(?::[^=]+)?=>|[A-Za-z_$][\w$]*\s*=>)/gm }
|
|
555
|
+
];
|
|
556
|
+
case "python":
|
|
557
|
+
return [
|
|
558
|
+
{ kind: "function", re: /^\s*(?:async\s+)?def\s+([A-Za-z_]\w*)/gm },
|
|
559
|
+
{ kind: "class", re: /^\s*class\s+([A-Za-z_]\w*)/gm }
|
|
560
|
+
];
|
|
561
|
+
case "go":
|
|
562
|
+
return [
|
|
563
|
+
{ kind: "function", re: /^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm },
|
|
564
|
+
{ kind: "struct", re: /^\s*type\s+([A-Za-z_]\w*)\s+struct\b/gm },
|
|
565
|
+
{ kind: "interface", re: /^\s*type\s+([A-Za-z_]\w*)\s+interface\b/gm }
|
|
566
|
+
];
|
|
567
|
+
case "rust":
|
|
568
|
+
return [
|
|
569
|
+
{ kind: "function", re: /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_]\w*)/gm },
|
|
570
|
+
{ kind: "struct", re: /^\s*(?:pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_]\w*)/gm },
|
|
571
|
+
{ kind: "enum", re: /^\s*(?:pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_]\w*)/gm },
|
|
572
|
+
{ kind: "trait", re: /^\s*(?:pub(?:\([^)]*\))?\s+)?trait\s+([A-Za-z_]\w*)/gm }
|
|
573
|
+
];
|
|
574
|
+
case "java":
|
|
575
|
+
case "csharp":
|
|
576
|
+
case "kotlin":
|
|
577
|
+
return [
|
|
578
|
+
{ kind: "class", re: /^\s*(?:[\w@]+\s+)*(?:class|record)\s+([A-Za-z_]\w*)/gm },
|
|
579
|
+
{ kind: "interface", re: /^\s*(?:[\w@]+\s+)*interface\s+([A-Za-z_]\w*)/gm },
|
|
580
|
+
{ kind: "enum", re: /^\s*(?:[\w@]+\s+)*enum\s+([A-Za-z_]\w*)/gm },
|
|
581
|
+
{ kind: "struct", re: /^\s*(?:[\w@]+\s+)*struct\s+([A-Za-z_]\w*)/gm },
|
|
582
|
+
{ kind: "function", re: /^\s*(?:(?:public|private|protected|internal|static|final|abstract|virtual|override|suspend|open|fun)\s+)+[A-Za-z_][\w<>\[\],. ]*?\s+([A-Za-z_]\w*)\s*\(/gm }
|
|
583
|
+
];
|
|
584
|
+
case "ruby":
|
|
585
|
+
return [
|
|
586
|
+
{ kind: "function", re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?=]?)/gm },
|
|
587
|
+
{ kind: "class", re: /^\s*class\s+([A-Za-z_]\w*)/gm },
|
|
588
|
+
{ kind: "module", re: /^\s*module\s+([A-Za-z_]\w*)/gm }
|
|
589
|
+
];
|
|
590
|
+
case "php":
|
|
591
|
+
return [
|
|
592
|
+
{ kind: "function", re: /^\s*(?:(?:public|private|protected|static|abstract|final)\s+)*function\s+([A-Za-z_]\w*)/gm },
|
|
593
|
+
{ kind: "class", re: /^\s*(?:(?:abstract|final)\s+)*class\s+([A-Za-z_]\w*)/gm },
|
|
594
|
+
{ kind: "interface", re: /^\s*interface\s+([A-Za-z_]\w*)/gm },
|
|
595
|
+
{ kind: "trait", re: /^\s*trait\s+([A-Za-z_]\w*)/gm }
|
|
596
|
+
];
|
|
597
|
+
case "c":
|
|
598
|
+
case "cpp":
|
|
599
|
+
return [
|
|
600
|
+
{ kind: "struct", re: /^\s*(?:typedef\s+)?struct\s+([A-Za-z_]\w*)/gm },
|
|
601
|
+
{ kind: "class", re: /^\s*class\s+([A-Za-z_]\w*)/gm },
|
|
602
|
+
{ kind: "enum", re: /^\s*enum\s+(?:class\s+)?([A-Za-z_]\w*)/gm },
|
|
603
|
+
{ kind: "function", re: /^[A-Za-z_][\w\s*&:<>]*?\b([A-Za-z_]\w*)\s*\([^;{)]*\)\s*\{/gm }
|
|
604
|
+
];
|
|
605
|
+
default:
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function importSpecs(lang, content) {
|
|
610
|
+
const out = /* @__PURE__ */ new Set();
|
|
611
|
+
const push = (re) => {
|
|
612
|
+
let m;
|
|
613
|
+
while (m = re.exec(content)) if (m[1]) out.add(m[1]);
|
|
614
|
+
};
|
|
615
|
+
switch (lang) {
|
|
616
|
+
case "ts":
|
|
617
|
+
case "js":
|
|
618
|
+
push(/^\s*import\s+[\s\S]*?\s+from\s+["']([^"']+)["']/gm);
|
|
619
|
+
push(/^\s*import\s+["']([^"']+)["']/gm);
|
|
620
|
+
push(/^\s*export\s+[\s\S]*?\s+from\s+["']([^"']+)["']/gm);
|
|
621
|
+
push(/\brequire\(\s*["']([^"']+)["']\s*\)/g);
|
|
622
|
+
break;
|
|
623
|
+
case "python":
|
|
624
|
+
push(/^\s*import\s+([A-Za-z_][\w.]*)/gm);
|
|
625
|
+
push(/^\s*from\s+([A-Za-z_.][\w.]*)\s+import\b/gm);
|
|
626
|
+
break;
|
|
627
|
+
case "go": {
|
|
628
|
+
push(/^\s*import\s+"([^"]+)"/gm);
|
|
629
|
+
const block = /import\s*\(([\s\S]*?)\)/g;
|
|
630
|
+
let b;
|
|
631
|
+
while (b = block.exec(content)) {
|
|
632
|
+
const q = /"([^"]+)"/g;
|
|
633
|
+
let m;
|
|
634
|
+
while (m = q.exec(b[1])) out.add(m[1]);
|
|
635
|
+
}
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case "rust":
|
|
639
|
+
push(/^\s*use\s+([A-Za-z_][\w:]*)/gm);
|
|
640
|
+
break;
|
|
641
|
+
case "java":
|
|
642
|
+
case "kotlin":
|
|
643
|
+
push(/^\s*import\s+(?:static\s+)?([A-Za-z_][\w.]*)/gm);
|
|
644
|
+
break;
|
|
645
|
+
case "csharp":
|
|
646
|
+
push(/^\s*using\s+(?:static\s+)?([A-Za-z_][\w.]*)/gm);
|
|
647
|
+
break;
|
|
648
|
+
case "ruby":
|
|
649
|
+
push(/^\s*require(?:_relative)?\s+["']([^"']+)["']/gm);
|
|
650
|
+
break;
|
|
651
|
+
case "php":
|
|
652
|
+
push(/^\s*use\s+([A-Za-z_\\][\w\\]*)/gm);
|
|
653
|
+
break;
|
|
654
|
+
case "c":
|
|
655
|
+
case "cpp":
|
|
656
|
+
push(/^\s*#\s*include\s*[<"]([^>"]+)[>"]/gm);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
return [...out];
|
|
660
|
+
}
|
|
661
|
+
function importBindings(lang, content) {
|
|
662
|
+
if (lang !== "ts" && lang !== "js" && lang !== "python") return null;
|
|
663
|
+
const names = /* @__PURE__ */ new Set();
|
|
664
|
+
const namespaces = [];
|
|
665
|
+
const addClause = (clause) => {
|
|
666
|
+
for (const part of clause.split(",")) {
|
|
667
|
+
const alias = part.trim().split(/\s+as\s+/).pop();
|
|
668
|
+
if (alias && /^[A-Za-z_$][\w$]*$/.test(alias)) names.add(alias);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
if (lang === "python") {
|
|
672
|
+
let m2;
|
|
673
|
+
const from = /^\s*from\s+[\w.]+\s+import\s+\(?([^)\n]+)\)?/gm;
|
|
674
|
+
while (m2 = from.exec(content)) addClause(m2[1].replace(/\*/g, ""));
|
|
675
|
+
return { names: [...names], namespaces };
|
|
676
|
+
}
|
|
677
|
+
let m;
|
|
678
|
+
const star = /^\s*import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']([^"']+)["']/gm;
|
|
679
|
+
while (m = star.exec(content)) namespaces.push([m[1], m[2]]);
|
|
680
|
+
const requireStar = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*["']([^"']+)["']\s*\)/g;
|
|
681
|
+
while (m = requireStar.exec(content)) namespaces.push([m[1], m[2]]);
|
|
682
|
+
const named = /^\s*(?:import|export)\s+(?:type\s+)?([\s\S]*?)\s+from\s+["'][^"']+["']/gm;
|
|
683
|
+
while (m = named.exec(content)) {
|
|
684
|
+
const clause = m[1];
|
|
685
|
+
if (clause.includes("*")) continue;
|
|
686
|
+
const braced = /\{([\s\S]*?)\}/.exec(clause);
|
|
687
|
+
if (braced) addClause(braced[1]);
|
|
688
|
+
const bare = clause.replace(/\{[\s\S]*?\}/g, "").replace(/,/g, " ").trim();
|
|
689
|
+
if (/^[A-Za-z_$][\w$]*$/.test(bare)) names.add(bare);
|
|
690
|
+
}
|
|
691
|
+
const requireNamed = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\(/g;
|
|
692
|
+
while (m = requireNamed.exec(content)) addClause(m[1].replace(/:/g, " as "));
|
|
693
|
+
return { names: [...names], namespaces };
|
|
694
|
+
}
|
|
695
|
+
function lineResolver(content) {
|
|
696
|
+
const nl = [];
|
|
697
|
+
for (let i = 0; i < content.length; i++) if (content.charCodeAt(i) === 10) nl.push(i);
|
|
698
|
+
return (idx) => {
|
|
699
|
+
let lo = 0, hi = nl.length;
|
|
700
|
+
while (lo < hi) {
|
|
701
|
+
const mid = lo + hi >> 1;
|
|
702
|
+
if (nl[mid] < idx) lo = mid + 1;
|
|
703
|
+
else hi = mid;
|
|
704
|
+
}
|
|
705
|
+
return lo + 1;
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function indentOf(line) {
|
|
709
|
+
let n = 0;
|
|
710
|
+
while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
|
|
711
|
+
return n;
|
|
712
|
+
}
|
|
713
|
+
function withoutTrailingComment(line) {
|
|
714
|
+
const slash = line.indexOf("//");
|
|
715
|
+
return (slash === -1 ? line : line.slice(0, slash)).trimEnd();
|
|
716
|
+
}
|
|
717
|
+
function braceEnd(lines, startIdx, lang) {
|
|
718
|
+
const LOOKAHEAD_LINES = 3;
|
|
719
|
+
let openLine = -1;
|
|
720
|
+
for (let i = startIdx; i <= Math.min(startIdx + LOOKAHEAD_LINES, lines.length - 1); i++) {
|
|
721
|
+
if (withoutTrailingComment(lines[i]).endsWith("{")) {
|
|
722
|
+
openLine = i;
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (openLine === -1) return startIdx + 1;
|
|
727
|
+
const hashComment = HASH_COMMENT_LANGS.has(lang);
|
|
728
|
+
const preprocessor = PREPROCESSOR_LANGS.has(lang);
|
|
729
|
+
let depth = 0;
|
|
730
|
+
let inBlockComment = false;
|
|
731
|
+
for (let i = openLine; i < lines.length; i++) {
|
|
732
|
+
const line = lines[i];
|
|
733
|
+
const hashEndsLine = hashComment || preprocessor && line.trimStart().startsWith("#");
|
|
734
|
+
const from = i === openLine ? line.lastIndexOf("{") : 0;
|
|
735
|
+
let inString = null;
|
|
736
|
+
for (let c = from; c < line.length; c++) {
|
|
737
|
+
const ch = line[c];
|
|
738
|
+
if (inBlockComment) {
|
|
739
|
+
if (ch === "*" && line[c + 1] === "/") {
|
|
740
|
+
inBlockComment = false;
|
|
741
|
+
c++;
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (inString) {
|
|
746
|
+
if (ch === "\\") {
|
|
747
|
+
c++;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (ch === inString) inString = null;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (ch === "/" && line[c + 1] === "/") break;
|
|
754
|
+
if (ch === "#" && hashEndsLine && line[c + 1] !== "{") break;
|
|
755
|
+
if (ch === "/" && line[c + 1] === "*") {
|
|
756
|
+
inBlockComment = true;
|
|
757
|
+
c++;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
761
|
+
inString = ch;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (ch === "{") {
|
|
765
|
+
depth++;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (ch === "}" && --depth <= 0) return i + 1;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return lines.length;
|
|
772
|
+
}
|
|
773
|
+
function indentEnd(lines, startIdx) {
|
|
774
|
+
const base = indentOf(lines[startIdx]);
|
|
775
|
+
let end = startIdx + 1;
|
|
776
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
777
|
+
const line = lines[i];
|
|
778
|
+
if (!line.trim()) continue;
|
|
779
|
+
if (indentOf(line) <= base) break;
|
|
780
|
+
end = i + 1;
|
|
781
|
+
}
|
|
782
|
+
return end;
|
|
783
|
+
}
|
|
784
|
+
function clampToSiblings(symbols, lines) {
|
|
785
|
+
const indentAt = (line) => indentOf(lines[line - 1] ?? "");
|
|
786
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
787
|
+
const s = symbols[i];
|
|
788
|
+
for (let j = i + 1; j < symbols.length; j++) {
|
|
789
|
+
const next = symbols[j];
|
|
790
|
+
if (next.line <= s.line) continue;
|
|
791
|
+
if (indentAt(next.line) > indentAt(s.line)) continue;
|
|
792
|
+
s.endLine = Math.max(s.line, Math.min(s.endLine, next.line - 1));
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function signatureOf(line) {
|
|
798
|
+
const MAX_SIGNATURE = 200;
|
|
799
|
+
let sig = line.trim();
|
|
800
|
+
const brace = sig.indexOf("{");
|
|
801
|
+
if (brace > 0) sig = sig.slice(0, brace).trim();
|
|
802
|
+
if (sig.endsWith(":")) sig = sig.slice(0, -1).trim();
|
|
803
|
+
return sig.length > MAX_SIGNATURE ? `${sig.slice(0, MAX_SIGNATURE)}...` : sig;
|
|
804
|
+
}
|
|
805
|
+
function extractFile(lang, content) {
|
|
806
|
+
const lineAt = lineResolver(content);
|
|
807
|
+
const lines = content.split("\n");
|
|
808
|
+
const braced = BRACE_LANGS.has(lang);
|
|
809
|
+
const symbols = [];
|
|
810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
811
|
+
for (const { kind, re } of symRules(lang)) {
|
|
812
|
+
let m;
|
|
813
|
+
re.lastIndex = 0;
|
|
814
|
+
while (m = re.exec(content)) {
|
|
815
|
+
const name = m[1];
|
|
816
|
+
if (!name) continue;
|
|
817
|
+
const nameIdx = m.index + m[0].lastIndexOf(name);
|
|
818
|
+
const key = `${name}@${nameIdx}`;
|
|
819
|
+
if (seen.has(key)) continue;
|
|
820
|
+
seen.add(key);
|
|
821
|
+
const line = lineAt(nameIdx);
|
|
822
|
+
const startIdx = line - 1;
|
|
823
|
+
const endLine = braced ? braceEnd(lines, startIdx, lang) : indentEnd(lines, startIdx);
|
|
824
|
+
symbols.push({ name, kind, line, endLine: Math.max(line, endLine), signature: signatureOf(lines[startIdx] ?? "") });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
symbols.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
828
|
+
if (braced) clampToSiblings(symbols, lines);
|
|
829
|
+
return { symbols, imports: importSpecs(lang, content) };
|
|
830
|
+
}
|
|
831
|
+
function gitList(root, args) {
|
|
832
|
+
try {
|
|
833
|
+
return execFileSync("git", ["-C", root, "ls-files", ...args, "-z"], {
|
|
834
|
+
encoding: "utf8",
|
|
835
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
836
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
837
|
+
windowsHide: true
|
|
838
|
+
});
|
|
839
|
+
} catch {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function gitSourceFiles(root) {
|
|
844
|
+
const out = gitList(root, ["--cached", "--others", "--exclude-standard"]);
|
|
845
|
+
if (out === null) return null;
|
|
846
|
+
const nested = existsSync4(join4(root, ".gitmodules")) ? gitList(root, ["--recurse-submodules"]) : null;
|
|
847
|
+
const files = [];
|
|
848
|
+
const seen = /* @__PURE__ */ new Set();
|
|
849
|
+
for (const rel of [...out.split("\0"), ...(nested ?? "").split("\0")]) {
|
|
850
|
+
if (files.length >= MAX_FILES) break;
|
|
851
|
+
if (!rel || seen.has(rel)) continue;
|
|
852
|
+
seen.add(rel);
|
|
853
|
+
if (!LANG_BY_EXT[extname(rel).toLowerCase()]) continue;
|
|
854
|
+
if (rel.split("/").some((seg) => seg.startsWith(".") || IGNORE_DIRS.has(seg))) continue;
|
|
855
|
+
files.push(join4(root, rel));
|
|
856
|
+
}
|
|
857
|
+
return files;
|
|
858
|
+
}
|
|
859
|
+
function walk(root) {
|
|
860
|
+
const tracked = gitSourceFiles(root);
|
|
861
|
+
if (tracked && tracked.length) return { files: tracked, truncated: tracked.length >= MAX_FILES };
|
|
862
|
+
const files = [];
|
|
863
|
+
const queue = [root];
|
|
864
|
+
let visited = 0;
|
|
865
|
+
for (let head = 0; head < queue.length && files.length < MAX_FILES && visited < MAX_ENTRIES; head++) {
|
|
866
|
+
let entries;
|
|
867
|
+
try {
|
|
868
|
+
entries = readdirSync2(queue[head], { withFileTypes: true });
|
|
869
|
+
} catch {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
for (const e of entries) {
|
|
873
|
+
if (files.length >= MAX_FILES || ++visited >= MAX_ENTRIES) break;
|
|
874
|
+
if (e.isDirectory()) {
|
|
875
|
+
if (!e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) queue.push(join4(queue[head], e.name));
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (!e.isFile()) continue;
|
|
879
|
+
if (LANG_BY_EXT[extname(e.name).toLowerCase()]) files.push(join4(queue[head], e.name));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return { files, truncated: files.length >= MAX_FILES || visited >= MAX_ENTRIES };
|
|
883
|
+
}
|
|
884
|
+
function relPath(root, abs) {
|
|
885
|
+
return relative2(root, abs).split("\\").join("/");
|
|
886
|
+
}
|
|
887
|
+
function scanFiles(rootDir) {
|
|
888
|
+
const root = resolve2(rootDir || process.cwd());
|
|
889
|
+
const files = [];
|
|
890
|
+
const walked = walk(root);
|
|
891
|
+
let truncated = walked.truncated;
|
|
892
|
+
for (const abs of walked.files) {
|
|
893
|
+
let size = 0;
|
|
894
|
+
let mtimeMs = 0;
|
|
895
|
+
try {
|
|
896
|
+
const st = statSync2(abs);
|
|
897
|
+
size = st.size;
|
|
898
|
+
mtimeMs = st.mtimeMs;
|
|
899
|
+
} catch {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (size > MAX_FILE_BYTES) {
|
|
903
|
+
truncated = true;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
let content;
|
|
907
|
+
try {
|
|
908
|
+
content = readFileSync4(abs, "utf8");
|
|
909
|
+
} catch {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const lang = LANG_BY_EXT[extname(abs).toLowerCase()];
|
|
913
|
+
const { symbols, imports } = extractFile(lang, content);
|
|
914
|
+
files.push({
|
|
915
|
+
path: relPath(root, abs),
|
|
916
|
+
lang,
|
|
917
|
+
loc: content ? content.split("\n").length : 0,
|
|
918
|
+
symbols,
|
|
919
|
+
imports,
|
|
920
|
+
bindings: importBindings(lang, content) ?? void 0,
|
|
921
|
+
chars: content.length,
|
|
922
|
+
size,
|
|
923
|
+
mtimeMs
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return { root, files, truncated };
|
|
927
|
+
}
|
|
928
|
+
function statSourceFiles(root) {
|
|
929
|
+
const out = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const abs of walk(root).files) {
|
|
931
|
+
try {
|
|
932
|
+
const st = statSync2(abs);
|
|
933
|
+
if (st.size > MAX_FILE_BYTES) continue;
|
|
934
|
+
out.set(relPath(root, abs), { size: st.size, mtimeMs: st.mtimeMs });
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return out;
|
|
939
|
+
}
|
|
940
|
+
var IGNORE_DIRS, LANG_BY_EXT, MAX_FILES, MAX_FILE_BYTES, BRACE_LANGS, HASH_COMMENT_LANGS, PREPROCESSOR_LANGS, MAX_ENTRIES;
|
|
941
|
+
var init_codegraph_extract = __esm({
|
|
942
|
+
"src/codegraph-extract.ts"() {
|
|
943
|
+
"use strict";
|
|
944
|
+
IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
945
|
+
"node_modules",
|
|
946
|
+
".git",
|
|
947
|
+
"dist",
|
|
948
|
+
"build",
|
|
949
|
+
"out",
|
|
950
|
+
".next",
|
|
951
|
+
".nuxt",
|
|
952
|
+
"target",
|
|
953
|
+
"vendor",
|
|
954
|
+
"__pycache__",
|
|
955
|
+
".venv",
|
|
956
|
+
"venv",
|
|
957
|
+
"coverage",
|
|
958
|
+
".cache",
|
|
959
|
+
".idea",
|
|
960
|
+
".vscode",
|
|
961
|
+
".turbo",
|
|
962
|
+
".parcel-cache",
|
|
963
|
+
".svelte-kit",
|
|
964
|
+
"Pods",
|
|
965
|
+
".gradle",
|
|
966
|
+
".dart_tool"
|
|
967
|
+
]);
|
|
968
|
+
LANG_BY_EXT = {
|
|
969
|
+
".ts": "ts",
|
|
970
|
+
".tsx": "ts",
|
|
971
|
+
".mts": "ts",
|
|
972
|
+
".cts": "ts",
|
|
973
|
+
".js": "js",
|
|
974
|
+
".jsx": "js",
|
|
975
|
+
".mjs": "js",
|
|
976
|
+
".cjs": "js",
|
|
977
|
+
".py": "python",
|
|
978
|
+
".go": "go",
|
|
979
|
+
".rs": "rust",
|
|
980
|
+
".java": "java",
|
|
981
|
+
".rb": "ruby",
|
|
982
|
+
".php": "php",
|
|
983
|
+
".cs": "csharp",
|
|
984
|
+
".kt": "kotlin",
|
|
985
|
+
".kts": "kotlin",
|
|
986
|
+
".c": "c",
|
|
987
|
+
".h": "c",
|
|
988
|
+
".cpp": "cpp",
|
|
989
|
+
".cc": "cpp",
|
|
990
|
+
".cxx": "cpp",
|
|
991
|
+
".hpp": "cpp",
|
|
992
|
+
".hh": "cpp"
|
|
993
|
+
};
|
|
994
|
+
MAX_FILES = 8e3;
|
|
995
|
+
MAX_FILE_BYTES = 512 * 1024;
|
|
996
|
+
BRACE_LANGS = /* @__PURE__ */ new Set(["ts", "js", "go", "rust", "java", "csharp", "kotlin", "php", "c", "cpp"]);
|
|
997
|
+
HASH_COMMENT_LANGS = /* @__PURE__ */ new Set(["php"]);
|
|
998
|
+
PREPROCESSOR_LANGS = /* @__PURE__ */ new Set(["c", "cpp"]);
|
|
999
|
+
MAX_ENTRIES = 6e4;
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
// src/codegraph.ts
|
|
1004
|
+
var codegraph_exports = {};
|
|
1005
|
+
__export(codegraph_exports, {
|
|
1006
|
+
GRAPH_VERSION: () => GRAPH_VERSION,
|
|
1007
|
+
WALK_RELATIONS: () => WALK_RELATIONS,
|
|
1008
|
+
codeGraphArchitecture: () => codeGraphArchitecture,
|
|
1009
|
+
codeGraphAvailable: () => codeGraphAvailable,
|
|
1010
|
+
codeGraphProjects: () => codeGraphProjects,
|
|
1011
|
+
codeGraphSchema: () => codeGraphSchema,
|
|
1012
|
+
codeGraphStatus: () => codeGraphStatus,
|
|
1013
|
+
driftCount: () => driftCount,
|
|
1014
|
+
ensureProjectForCwd: () => ensureProjectForCwd,
|
|
1015
|
+
findProjectForCwd: () => findProjectForCwd,
|
|
1016
|
+
graphNodes: () => graphNodes,
|
|
1017
|
+
indexProject: () => indexProject,
|
|
1018
|
+
isCleanDrift: () => isCleanDrift,
|
|
1019
|
+
isManagedPath: () => isManagedPath,
|
|
1020
|
+
listProjects: () => listProjects,
|
|
1021
|
+
loadBodyIndex: () => loadBodyIndex,
|
|
1022
|
+
loadCompatibleGraph: () => loadCompatibleGraph,
|
|
1023
|
+
loadFreshGraph: () => loadFreshGraph,
|
|
1024
|
+
loadGraph: () => loadGraph,
|
|
1025
|
+
probeDrift: () => probeDrift,
|
|
1026
|
+
resetCodeGraph: () => resetCodeGraph,
|
|
1027
|
+
scanFiles: () => scanFiles,
|
|
1028
|
+
searchGraph: () => searchGraph
|
|
1029
|
+
});
|
|
1030
|
+
import { homedir as homedir2 } from "os";
|
|
1031
|
+
import { createHash as createHash2 } from "crypto";
|
|
1032
|
+
import { basename, join as join5, resolve as resolve3 } from "path";
|
|
1033
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1034
|
+
function storeDir() {
|
|
1035
|
+
return process.env.ENIGMA_CODEGRAPH_DIR || join5(homedir2(), ".enigma", "codegraph");
|
|
1036
|
+
}
|
|
1037
|
+
function projectsFile() {
|
|
1038
|
+
return join5(storeDir(), "projects.json");
|
|
1039
|
+
}
|
|
1040
|
+
function graphFile(id) {
|
|
1041
|
+
return join5(storeDir(), `${id}.json`);
|
|
1042
|
+
}
|
|
1043
|
+
function bodyIndexFile(id) {
|
|
1044
|
+
return join5(storeDir(), `${id}.bodies.json`);
|
|
1045
|
+
}
|
|
1046
|
+
function readJsonSafe(file, fallback) {
|
|
1047
|
+
try {
|
|
1048
|
+
return existsSync5(file) ? JSON.parse(readFileSync5(file, "utf8")) : fallback;
|
|
1049
|
+
} catch {
|
|
1050
|
+
return fallback;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function writeJson(file, value) {
|
|
1054
|
+
mkdirSync3(storeDir(), { recursive: true });
|
|
1055
|
+
writeFileSync4(file, JSON.stringify(value));
|
|
1056
|
+
}
|
|
1057
|
+
function langFamily(lang) {
|
|
1058
|
+
return LANG_FAMILY[lang] ?? lang;
|
|
1059
|
+
}
|
|
1060
|
+
function verifyModule(base, known, lang) {
|
|
1061
|
+
if (!base || base.startsWith("..")) return null;
|
|
1062
|
+
if (known.has(base)) return base;
|
|
1063
|
+
const exts = EXT_ORDER[lang] ?? RESOLVE_EXTS;
|
|
1064
|
+
for (const ext of exts) if (known.has(base + ext)) return base + ext;
|
|
1065
|
+
for (const name of INDEX_ORDER[lang] ?? INDEX_NAMES) {
|
|
1066
|
+
for (const ext of exts) if (known.has(`${base}/${name}${ext}`)) return `${base}/${name}${ext}`;
|
|
1067
|
+
}
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
function orders(all, byLang) {
|
|
1071
|
+
const out = {};
|
|
1072
|
+
for (const [lang, preferred] of Object.entries(byLang)) out[lang] = [...preferred, ...all.filter((v) => preferred.indexOf(v) === -1)];
|
|
1073
|
+
return out;
|
|
1074
|
+
}
|
|
1075
|
+
function goModulePath(root) {
|
|
1076
|
+
try {
|
|
1077
|
+
return /^\s*module\s+(\S+)/m.exec(readFileSync5(join5(root, "go.mod"), "utf8"))?.[1] ?? null;
|
|
1078
|
+
} catch {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function createImportResolver(root, known) {
|
|
1083
|
+
const goModule = goModulePath(root);
|
|
1084
|
+
let goByDir = null;
|
|
1085
|
+
const dirOf = (rel) => rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "";
|
|
1086
|
+
const joinRel = (dir, rest) => [...dir ? [dir] : [], ...rest].join("/");
|
|
1087
|
+
const underRoots = (segments, lang) => {
|
|
1088
|
+
for (const base of MODULE_ROOTS) {
|
|
1089
|
+
const hit = verifyModule(joinRel(base, segments), known, lang);
|
|
1090
|
+
if (hit) return hit;
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1093
|
+
};
|
|
1094
|
+
const relative4 = (fromRel, spec, lang) => {
|
|
1095
|
+
const abs = resolve3(join5(root, fromRel), "..", spec);
|
|
1096
|
+
return verifyModule(relPath(root, abs), known, lang);
|
|
1097
|
+
};
|
|
1098
|
+
return (fromRel, lang, spec) => {
|
|
1099
|
+
if (lang === "go") {
|
|
1100
|
+
if (!goModule || spec !== goModule && !spec.startsWith(`${goModule}/`)) return [];
|
|
1101
|
+
const dir = spec === goModule ? "" : spec.slice(goModule.length + 1);
|
|
1102
|
+
if (!goByDir) {
|
|
1103
|
+
goByDir = /* @__PURE__ */ new Map();
|
|
1104
|
+
for (const p of known) {
|
|
1105
|
+
if (!p.endsWith(".go")) continue;
|
|
1106
|
+
const d = dirOf(p);
|
|
1107
|
+
const list = goByDir.get(d);
|
|
1108
|
+
if (list) list.push(p);
|
|
1109
|
+
else goByDir.set(d, [p]);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return goByDir.get(dir) ?? [];
|
|
1113
|
+
}
|
|
1114
|
+
if (lang === "python") {
|
|
1115
|
+
const dots = /^\.+/.exec(spec)?.[0].length ?? 0;
|
|
1116
|
+
const segments = spec.slice(dots).split(".").filter(Boolean);
|
|
1117
|
+
if (dots > 0) {
|
|
1118
|
+
let dir = dirOf(fromRel);
|
|
1119
|
+
for (let up = 1; up < dots; up++) dir = dirOf(dir);
|
|
1120
|
+
return [verifyModule(joinRel(dir, segments), known, lang)].filter((p) => !!p);
|
|
1121
|
+
}
|
|
1122
|
+
return [underRoots(segments, lang)].filter((p) => !!p);
|
|
1123
|
+
}
|
|
1124
|
+
if (spec.startsWith(".")) return [relative4(fromRel, spec, lang)].filter((p) => !!p);
|
|
1125
|
+
const separator = MODULE_SEPARATOR[lang];
|
|
1126
|
+
if (separator) {
|
|
1127
|
+
const segments = spec.split(separator).filter((s) => s && s !== "crate" && s !== "self");
|
|
1128
|
+
return [underRoots(segments, lang) ?? (segments.length > 1 ? underRoots(segments.slice(0, -1), lang) : null)].filter((p) => !!p);
|
|
1129
|
+
}
|
|
1130
|
+
const alias = /^[@~#]\//.test(spec);
|
|
1131
|
+
if (BARE_IS_PACKAGE.has(lang) && !alias && (!spec.includes("/") || spec.startsWith("@"))) return [];
|
|
1132
|
+
const path = spec.replace(/^[@~#]\//, "").replace(/^\/+/, "");
|
|
1133
|
+
return [relative4(fromRel, `./${path}`, lang) ?? underRoots(path.split("/"), lang)].filter((p) => !!p);
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function symbolId(path, name, taken) {
|
|
1137
|
+
const base = `${path}#${name}`;
|
|
1138
|
+
if (!taken.has(base)) {
|
|
1139
|
+
taken.add(base);
|
|
1140
|
+
return base;
|
|
1141
|
+
}
|
|
1142
|
+
for (let n = 2; ; n++) {
|
|
1143
|
+
const candidate = `${base}~${n}`;
|
|
1144
|
+
if (!taken.has(candidate)) {
|
|
1145
|
+
taken.add(candidate);
|
|
1146
|
+
return candidate;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function mintNodes(files) {
|
|
1151
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1152
|
+
const nodes = [];
|
|
1153
|
+
for (const f of files) {
|
|
1154
|
+
nodes.push({ id: f.path, name: basename(f.path), kind: "file", path: f.path, line: 1, endLine: f.loc, signature: "", chars: f.chars });
|
|
1155
|
+
for (const s of f.symbols) {
|
|
1156
|
+
nodes.push({ id: symbolId(f.path, s.name, taken), name: s.name, kind: s.kind, path: f.path, line: s.line, endLine: s.endLine, signature: s.signature });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return nodes;
|
|
1160
|
+
}
|
|
1161
|
+
function enclosingSymbol(spans, line) {
|
|
1162
|
+
let found = null;
|
|
1163
|
+
for (const s of spans) {
|
|
1164
|
+
if (s.line > line) break;
|
|
1165
|
+
if (line <= s.endLine) found = s.id;
|
|
1166
|
+
}
|
|
1167
|
+
return found;
|
|
1168
|
+
}
|
|
1169
|
+
function inheritanceTargets(signature) {
|
|
1170
|
+
const out = [];
|
|
1171
|
+
const ext = /\bextends\s+([\w$.<>,\s]+?)(?:\s+implements\b|$)/.exec(signature);
|
|
1172
|
+
const impl = /\bimplements\s+([\w$.<>,\s]+)$/.exec(signature);
|
|
1173
|
+
const names = (clause) => clause.split(",").map((s) => s.trim().replace(/<.*$/, "").split(".").pop() || "").filter((s) => s.length >= MIN_REF_NAME);
|
|
1174
|
+
if (ext) for (const n of names(ext[1])) out.push(["extends", n]);
|
|
1175
|
+
if (impl) for (const n of names(impl[1])) out.push(["implements", n]);
|
|
1176
|
+
return out;
|
|
1177
|
+
}
|
|
1178
|
+
function buildEdges(root, files, importEdges, resolveImport) {
|
|
1179
|
+
const nodes = mintNodes(files);
|
|
1180
|
+
const spansByFile = /* @__PURE__ */ new Map();
|
|
1181
|
+
const defsByName = /* @__PURE__ */ new Map();
|
|
1182
|
+
for (const n of nodes) {
|
|
1183
|
+
if (n.kind === "file") {
|
|
1184
|
+
spansByFile.set(n.path, []);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
spansByFile.get(n.path).push({ id: n.id, line: n.line, endLine: n.endLine });
|
|
1188
|
+
if (n.name.length >= MIN_REF_NAME) {
|
|
1189
|
+
let list = defsByName.get(n.name);
|
|
1190
|
+
if (!list) {
|
|
1191
|
+
list = [];
|
|
1192
|
+
defsByName.set(n.name, list);
|
|
1193
|
+
}
|
|
1194
|
+
list.push({ id: n.id, path: n.path });
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
for (const spans of spansByFile.values()) spans.sort((a, b) => a.line - b.line);
|
|
1198
|
+
const importsOf = /* @__PURE__ */ new Map();
|
|
1199
|
+
for (const [from, to] of importEdges) {
|
|
1200
|
+
let set = importsOf.get(from);
|
|
1201
|
+
if (!set) {
|
|
1202
|
+
set = /* @__PURE__ */ new Set();
|
|
1203
|
+
importsOf.set(from, set);
|
|
1204
|
+
}
|
|
1205
|
+
set.add(to);
|
|
1206
|
+
}
|
|
1207
|
+
const edges = [];
|
|
1208
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1209
|
+
const push = (from, to, rel) => {
|
|
1210
|
+
if (edges.length >= MAX_EDGES || from === to) return;
|
|
1211
|
+
const key = `${from}|${to}|${rel}`;
|
|
1212
|
+
if (seen.has(key)) return;
|
|
1213
|
+
seen.add(key);
|
|
1214
|
+
edges.push([from, to, rel]);
|
|
1215
|
+
};
|
|
1216
|
+
for (const f of files) {
|
|
1217
|
+
for (const span of spansByFile.get(f.path) ?? []) push(f.path, span.id, "contains");
|
|
1218
|
+
for (const to of importsOf.get(f.path) ?? []) push(f.path, to, "imports");
|
|
1219
|
+
}
|
|
1220
|
+
const known = new Set(files.map((f) => f.path));
|
|
1221
|
+
const langByPath = new Map(files.map((f) => [f.path, f.lang]));
|
|
1222
|
+
const idByPathName = /* @__PURE__ */ new Map();
|
|
1223
|
+
const localDefs = /* @__PURE__ */ new Map();
|
|
1224
|
+
for (const n of nodes) {
|
|
1225
|
+
if (n.kind === "file") {
|
|
1226
|
+
localDefs.set(n.path, /* @__PURE__ */ new Map());
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const key = `${n.path}#${n.name}`;
|
|
1230
|
+
if (!idByPathName.has(key)) idByPathName.set(key, n.id);
|
|
1231
|
+
const local = localDefs.get(n.path);
|
|
1232
|
+
local.set(n.name, local.has(n.name) ? null : n.id);
|
|
1233
|
+
}
|
|
1234
|
+
function binderFor(f) {
|
|
1235
|
+
const local = localDefs.get(f.path) ?? /* @__PURE__ */ new Map();
|
|
1236
|
+
const imported = importsOf.get(f.path);
|
|
1237
|
+
const family = langFamily(f.lang);
|
|
1238
|
+
const reachable = (path) => langFamily(langByPath.get(path) ?? "") === family;
|
|
1239
|
+
const unique = (name) => {
|
|
1240
|
+
const candidates = defsByName.get(name)?.filter((c) => imported?.has(c.path) && reachable(c.path));
|
|
1241
|
+
return candidates?.length === 1 ? candidates[0].id : null;
|
|
1242
|
+
};
|
|
1243
|
+
if (!f.bindings) {
|
|
1244
|
+
return (name) => {
|
|
1245
|
+
if (local.has(name)) return local.get(name) ?? null;
|
|
1246
|
+
if (imported?.size) return unique(name);
|
|
1247
|
+
const all = defsByName.get(name)?.filter((c) => reachable(c.path));
|
|
1248
|
+
return all?.length === 1 ? all[0].id : null;
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const bound = new Set(f.bindings.names);
|
|
1252
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
1253
|
+
for (const [alias, spec] of f.bindings.namespaces) {
|
|
1254
|
+
const [target] = resolveImport(f.path, f.lang, spec);
|
|
1255
|
+
if (target) namespaces.set(alias, target);
|
|
1256
|
+
}
|
|
1257
|
+
return (name, memberOf) => {
|
|
1258
|
+
if (memberOf !== void 0) {
|
|
1259
|
+
const path = namespaces.get(memberOf);
|
|
1260
|
+
return path ? idByPathName.get(`${path}#${name}`) ?? null : null;
|
|
1261
|
+
}
|
|
1262
|
+
if (local.has(name)) return local.get(name) ?? null;
|
|
1263
|
+
return bound.has(name) ? unique(name) : null;
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
const bodies = [];
|
|
1267
|
+
for (const f of files) {
|
|
1268
|
+
let content = "";
|
|
1269
|
+
try {
|
|
1270
|
+
content = readFileSync5(join5(root, f.path), "utf8");
|
|
1271
|
+
} catch {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
const spans = spansByFile.get(f.path) ?? [];
|
|
1275
|
+
const declLines = new Set(f.symbols.map((s) => s.line));
|
|
1276
|
+
const lines = content.split("\n");
|
|
1277
|
+
indexBodies(bodies, f.path, spans, lines);
|
|
1278
|
+
const bind = binderFor(f);
|
|
1279
|
+
const strict = !!f.bindings;
|
|
1280
|
+
for (const s of f.symbols) {
|
|
1281
|
+
if (s.kind !== "class" && s.kind !== "interface" && s.kind !== "struct") continue;
|
|
1282
|
+
const self = spans.find((sp) => sp.line === s.line);
|
|
1283
|
+
if (!self) continue;
|
|
1284
|
+
for (const [rel, name] of inheritanceTargets(s.signature)) {
|
|
1285
|
+
const target = bind(name);
|
|
1286
|
+
if (target) push(self.id, target, rel);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1290
|
+
const lineNo = i + 1;
|
|
1291
|
+
if (declLines.has(lineNo)) continue;
|
|
1292
|
+
const line = lines[i];
|
|
1293
|
+
const from = enclosingSymbol(spans, lineNo) ?? f.path;
|
|
1294
|
+
const ident = /[A-Za-z_$][\w$]*/g;
|
|
1295
|
+
let m;
|
|
1296
|
+
while (m = ident.exec(line)) {
|
|
1297
|
+
const name = m[0];
|
|
1298
|
+
if (name.length < MIN_REF_NAME || !defsByName.has(name)) continue;
|
|
1299
|
+
const memberOf = strict ? /([A-Za-z_$][\w$]*)\s*\.\s*$/.exec(line.slice(0, m.index))?.[1] : void 0;
|
|
1300
|
+
const target = bind(name, memberOf);
|
|
1301
|
+
if (!target || target === from) continue;
|
|
1302
|
+
const after = line.slice(m.index + name.length);
|
|
1303
|
+
push(from, target, /^\s*\(/.test(after) ? "calls" : "references");
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return { edges, bodies };
|
|
1308
|
+
}
|
|
1309
|
+
function indexBodies(out, path, spans, lines) {
|
|
1310
|
+
const covered = new Uint8Array(lines.length + 1);
|
|
1311
|
+
for (const s of spans) {
|
|
1312
|
+
const text = lines.slice(s.line - 1, s.endLine).join("\n");
|
|
1313
|
+
for (let l = s.line; l <= Math.min(s.endLine, lines.length); l++) covered[l] = 1;
|
|
1314
|
+
const bag = topTokens(text);
|
|
1315
|
+
if (bag.length) out.push([s.id, bag]);
|
|
1316
|
+
}
|
|
1317
|
+
const residual = [];
|
|
1318
|
+
for (let l = 1; l <= lines.length; l++) if (!covered[l]) residual.push(lines[l - 1]);
|
|
1319
|
+
const fileBag = topTokens(residual.join("\n"));
|
|
1320
|
+
if (fileBag.length) out.push([path, fileBag]);
|
|
1321
|
+
}
|
|
1322
|
+
function topTokens(text) {
|
|
1323
|
+
const m = /* @__PURE__ */ new Map();
|
|
1324
|
+
for (const t of tokenize(text)) m.set(t, (m.get(t) ?? 0) + 1);
|
|
1325
|
+
return [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_BODY_TOKENS);
|
|
1326
|
+
}
|
|
1327
|
+
function isManagedPath(dir) {
|
|
1328
|
+
const managed = resolve3(join5(enigmaHome(), ".enigma"));
|
|
1329
|
+
return dir === managed || dir.startsWith(managed + (dir.includes("\\") ? "\\" : "/"));
|
|
1330
|
+
}
|
|
1331
|
+
function indexProject(rootDir) {
|
|
1332
|
+
const requested = resolve3(rootDir || process.cwd());
|
|
1333
|
+
if (isManagedPath(requested)) {
|
|
1334
|
+
throw new Error(`${requested} is inside enigma's own managed directory - a temporary checkout, not a project to index.`);
|
|
1335
|
+
}
|
|
1336
|
+
const { root, files, truncated } = scanFiles(rootDir);
|
|
1337
|
+
const id = createHash2("sha1").update(root).digest("hex").slice(0, 12);
|
|
1338
|
+
const name = basename(root) || root;
|
|
1339
|
+
const known = new Set(files.map((f) => f.path));
|
|
1340
|
+
const resolveImport = createImportResolver(root, known);
|
|
1341
|
+
const importEdges = [];
|
|
1342
|
+
const externalModules = {};
|
|
1343
|
+
for (const f of files) {
|
|
1344
|
+
for (const spec of f.imports) {
|
|
1345
|
+
const targets = resolveImport(f.path, f.lang, spec);
|
|
1346
|
+
if (!targets.length) {
|
|
1347
|
+
externalModules[spec] = (externalModules[spec] || 0) + 1;
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
for (const target of targets) if (target !== f.path) importEdges.push([f.path, target]);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const { edges, bodies } = buildEdges(root, files, importEdges, resolveImport);
|
|
1354
|
+
const graph = { version: GRAPH_VERSION, id, name, root, indexedAt: Date.now(), files, importEdges, edges, externalModules, truncated };
|
|
1355
|
+
writeJson(graphFile(id), graph);
|
|
1356
|
+
writeJson(bodyIndexFile(id), bodies);
|
|
1357
|
+
const symbols = files.reduce((n, f) => n + f.symbols.length, 0);
|
|
1358
|
+
const entry = { id, name, root, indexedAt: graph.indexedAt, files: files.length, symbols, truncated };
|
|
1359
|
+
writeJson(projectsFile(), [...storedProjects().filter((p) => p.id !== id), entry]);
|
|
1360
|
+
return entry;
|
|
1361
|
+
}
|
|
1362
|
+
function storedProjects() {
|
|
1363
|
+
const stored = readJsonSafe(projectsFile(), []).map((p) => ({ ...p, truncated: p.truncated === true }));
|
|
1364
|
+
const kept = stored.filter((p) => p.root && !isManagedPath(p.root));
|
|
1365
|
+
if (kept.length !== stored.length) {
|
|
1366
|
+
for (const dead of stored.filter((p) => !kept.includes(p))) {
|
|
1367
|
+
rmSync2(graphFile(dead.id), { force: true });
|
|
1368
|
+
rmSync2(bodyIndexFile(dead.id), { force: true });
|
|
1369
|
+
}
|
|
1370
|
+
writeJson(projectsFile(), kept);
|
|
1371
|
+
}
|
|
1372
|
+
return kept;
|
|
1373
|
+
}
|
|
1374
|
+
function listProjects() {
|
|
1375
|
+
return storedProjects().filter((p) => existsSync5(p.root)).sort((a, b) => b.indexedAt - a.indexedAt);
|
|
1376
|
+
}
|
|
1377
|
+
function codeGraphProjects() {
|
|
1378
|
+
return listProjects();
|
|
1379
|
+
}
|
|
1380
|
+
function resolveProject(project) {
|
|
1381
|
+
const all = listProjects();
|
|
1382
|
+
if (!all.length) return null;
|
|
1383
|
+
if (project) {
|
|
1384
|
+
const named = all.find((p) => p.id === project || p.name === project || p.root === project);
|
|
1385
|
+
return named ?? (looksLikePath(project) ? covering(all, resolve3(project)) : null);
|
|
1386
|
+
}
|
|
1387
|
+
return covering(all, resolve3(process.cwd())) ?? all[0];
|
|
1388
|
+
}
|
|
1389
|
+
function looksLikePath(value) {
|
|
1390
|
+
return value.startsWith(".") || value.includes("/") || value.includes("\\");
|
|
1391
|
+
}
|
|
1392
|
+
function covering(all, dir) {
|
|
1393
|
+
return all.filter((p) => dir === p.root || dir.startsWith(`${p.root}${dir.includes("\\") ? "\\" : "/"}`)).sort((a, b) => b.root.length - a.root.length)[0] ?? null;
|
|
1394
|
+
}
|
|
1395
|
+
function projectRootOf(dir) {
|
|
1396
|
+
const start = resolve3(dir);
|
|
1397
|
+
if (ROOT_MARKERS.some((marker) => existsSync5(join5(start, marker)))) return start;
|
|
1398
|
+
const home = resolve3(homedir2());
|
|
1399
|
+
let current = start;
|
|
1400
|
+
for (let up = 0; up < MAX_ROOT_WALK; up++) {
|
|
1401
|
+
const parent = resolve3(current, "..");
|
|
1402
|
+
if (parent === current || parent === home) break;
|
|
1403
|
+
current = parent;
|
|
1404
|
+
if (existsSync5(join5(current, ".git"))) return current;
|
|
1405
|
+
}
|
|
1406
|
+
return start;
|
|
1407
|
+
}
|
|
1408
|
+
function ensureProjectForCwd(dir) {
|
|
1409
|
+
const covering2 = findProjectForCwd(dir);
|
|
1410
|
+
if (covering2) return covering2;
|
|
1411
|
+
return indexProject(projectRootOf(resolve3(dir || process.cwd()))).id;
|
|
1412
|
+
}
|
|
1413
|
+
function findProjectForCwd(dir) {
|
|
1414
|
+
return covering(listProjects(), resolve3(dir || process.cwd()))?.id ?? null;
|
|
1415
|
+
}
|
|
1416
|
+
function loadGraph(project) {
|
|
1417
|
+
const entry = resolveProject(project);
|
|
1418
|
+
if (!entry) return null;
|
|
1419
|
+
return readJsonSafe(graphFile(entry.id), null);
|
|
1420
|
+
}
|
|
1421
|
+
function loadCompatibleGraph(project) {
|
|
1422
|
+
const graph = loadGraph(project);
|
|
1423
|
+
if (!graph || graph.version === GRAPH_VERSION) return graph;
|
|
1424
|
+
if (!graph.root || !existsSync5(graph.root)) return null;
|
|
1425
|
+
indexProject(graph.root);
|
|
1426
|
+
return loadGraph(graph.id);
|
|
1427
|
+
}
|
|
1428
|
+
function loadBodyIndex(graphId) {
|
|
1429
|
+
const raw = readJsonSafe(bodyIndexFile(graphId), []);
|
|
1430
|
+
const out = /* @__PURE__ */ new Map();
|
|
1431
|
+
for (const [id, bag] of raw) out.set(id, new Map(bag));
|
|
1432
|
+
return out;
|
|
1433
|
+
}
|
|
1434
|
+
function isCleanDrift(d) {
|
|
1435
|
+
return d.changed.length === 0 && d.added.length === 0 && d.removed.length === 0;
|
|
1436
|
+
}
|
|
1437
|
+
function driftCount(d) {
|
|
1438
|
+
return d.changed.length + d.added.length + d.removed.length;
|
|
1439
|
+
}
|
|
1440
|
+
function probeDrift(graph) {
|
|
1441
|
+
const drift = { changed: [], added: [], removed: [] };
|
|
1442
|
+
const onDisk = statSourceFiles(graph.root);
|
|
1443
|
+
const indexed = new Map(graph.files.map((f) => [f.path, f]));
|
|
1444
|
+
for (const [path, st] of onDisk) {
|
|
1445
|
+
const rec = indexed.get(path);
|
|
1446
|
+
if (!rec) {
|
|
1447
|
+
drift.added.push(path);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (rec.size !== st.size || rec.mtimeMs !== st.mtimeMs) drift.changed.push(path);
|
|
1451
|
+
}
|
|
1452
|
+
for (const path of indexed.keys()) if (!onDisk.has(path)) drift.removed.push(path);
|
|
1453
|
+
drift.changed.sort();
|
|
1454
|
+
drift.added.sort();
|
|
1455
|
+
drift.removed.sort();
|
|
1456
|
+
return drift;
|
|
1457
|
+
}
|
|
1458
|
+
function loadFreshGraph(project, refresh = true) {
|
|
1459
|
+
const graph = loadGraph(project);
|
|
1460
|
+
if (!graph) return null;
|
|
1461
|
+
if (!refresh) return graph;
|
|
1462
|
+
if (graph.version !== GRAPH_VERSION || !isCleanDrift(probeDrift(graph))) {
|
|
1463
|
+
indexProject(graph.root);
|
|
1464
|
+
return loadGraph(graph.id);
|
|
1465
|
+
}
|
|
1466
|
+
return graph;
|
|
1467
|
+
}
|
|
1468
|
+
function graphNodes(graph) {
|
|
1469
|
+
return mintNodes(graph.files);
|
|
1470
|
+
}
|
|
1471
|
+
function topByCrossFileDegree(g, limit) {
|
|
1472
|
+
const degree = inDegree(g.edges ?? [], true);
|
|
1473
|
+
if (!degree.size) return [];
|
|
1474
|
+
const named = new Map(mintNodes(g.files).map((n) => [n.id, n.name]));
|
|
1475
|
+
return [...degree.entries()].filter(([id]) => id.includes("#")).map(([id, refs]) => ({ name: named.get(id) ?? id, refs })).sort((a, b) => b.refs - a.refs || a.name.localeCompare(b.name)).slice(0, limit);
|
|
1476
|
+
}
|
|
1477
|
+
function codeGraphArchitecture(project) {
|
|
1478
|
+
const g = loadCompatibleGraph(project);
|
|
1479
|
+
if (!g) return null;
|
|
1480
|
+
const languages = {};
|
|
1481
|
+
const packages = {};
|
|
1482
|
+
for (const f of g.files) {
|
|
1483
|
+
languages[f.lang] = (languages[f.lang] || 0) + 1;
|
|
1484
|
+
const top = f.path.includes("/") ? f.path.slice(0, f.path.indexOf("/")) : ".";
|
|
1485
|
+
packages[top] = (packages[top] || 0) + 1;
|
|
1486
|
+
}
|
|
1487
|
+
const imported = new Set(g.importEdges.map(([, to]) => to));
|
|
1488
|
+
const reach = /* @__PURE__ */ new Map();
|
|
1489
|
+
for (const [from] of g.importEdges) reach.set(from, (reach.get(from) ?? 0) + 1);
|
|
1490
|
+
const named = /(^|\/)(main|index|cli|app|server|bin)\.\w+$/;
|
|
1491
|
+
const entryPoints = g.files.filter((f) => !imported.has(f.path) || named.test(f.path)).sort((a, b) => (reach.get(b.path) ?? 0) - (reach.get(a.path) ?? 0) || Number(named.test(b.path)) - Number(named.test(a.path)) || a.path.localeCompare(b.path)).map((f) => f.path).filter((p, i, a) => a.indexOf(p) === i).slice(0, 15);
|
|
1492
|
+
const hotspots = topByCrossFileDegree(g, 15);
|
|
1493
|
+
const externalModules = Object.entries(g.externalModules).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count).slice(0, 20);
|
|
1494
|
+
const symbols = g.files.reduce((n, f) => n + f.symbols.length, 0);
|
|
1495
|
+
return { project: g.name, languages, files: g.files.length, symbols, importEdges: g.importEdges.length, entryPoints, hotspots, packages, externalModules };
|
|
1496
|
+
}
|
|
1497
|
+
function codeGraphSchema(project) {
|
|
1498
|
+
const g = loadCompatibleGraph(project);
|
|
1499
|
+
if (!g) return null;
|
|
1500
|
+
const nodes = { File: g.files.length };
|
|
1501
|
+
for (const f of g.files) for (const s of f.symbols) {
|
|
1502
|
+
const label = s.kind.charAt(0).toUpperCase() + s.kind.slice(1);
|
|
1503
|
+
nodes[label] = (nodes[label] || 0) + 1;
|
|
1504
|
+
}
|
|
1505
|
+
const edges = {};
|
|
1506
|
+
for (const [, , rel] of g.edges ?? []) edges[rel.toUpperCase()] = (edges[rel.toUpperCase()] || 0) + 1;
|
|
1507
|
+
if (!Object.keys(edges).length) edges.IMPORTS = g.importEdges.length;
|
|
1508
|
+
return { nodes, edges };
|
|
1509
|
+
}
|
|
1510
|
+
function searchGraph(project, opts = {}) {
|
|
1511
|
+
const g = loadCompatibleGraph(project);
|
|
1512
|
+
if (!g) return [];
|
|
1513
|
+
let re = null;
|
|
1514
|
+
if (opts.name) {
|
|
1515
|
+
try {
|
|
1516
|
+
re = new RegExp(opts.name, "i");
|
|
1517
|
+
} catch {
|
|
1518
|
+
re = new RegExp(opts.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
const out = [];
|
|
1522
|
+
const limit = opts.limit ?? 50;
|
|
1523
|
+
for (const f of g.files) for (const s of f.symbols) {
|
|
1524
|
+
if (opts.kind && s.kind !== opts.kind) continue;
|
|
1525
|
+
if (re && !re.test(s.name)) continue;
|
|
1526
|
+
out.push({ name: s.name, kind: s.kind, file: f.path, line: s.line });
|
|
1527
|
+
if (out.length >= limit) return out;
|
|
1528
|
+
}
|
|
1529
|
+
return out;
|
|
1530
|
+
}
|
|
1531
|
+
function resetCodeGraph() {
|
|
1532
|
+
try {
|
|
1533
|
+
rmSync2(storeDir(), { recursive: true, force: true });
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function codeGraphAvailable() {
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
function codeGraphStatus() {
|
|
1541
|
+
return { enabled: readConfig().config.codeGraph, available: true, projects: listProjects().length };
|
|
1542
|
+
}
|
|
1543
|
+
var GRAPH_VERSION, RESOLVE_EXTS, LANG_EXTS, LANG_INDEX, MIN_REF_NAME, MAX_EDGES, LANG_FAMILY, INDEX_NAMES, MODULE_ROOTS, MODULE_SEPARATOR, BARE_IS_PACKAGE, EXT_ORDER, INDEX_ORDER, MAX_BODY_TOKENS, ROOT_MARKERS, MAX_ROOT_WALK;
|
|
1544
|
+
var init_codegraph = __esm({
|
|
1545
|
+
"src/codegraph.ts"() {
|
|
1546
|
+
"use strict";
|
|
1547
|
+
init_util();
|
|
1548
|
+
init_config();
|
|
1549
|
+
init_codegraph_rank();
|
|
1550
|
+
init_codegraph_extract();
|
|
1551
|
+
init_codegraph_extract();
|
|
1552
|
+
init_codegraph_types();
|
|
1553
|
+
GRAPH_VERSION = 4;
|
|
1554
|
+
RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".rb", ".php", ".cs", ".kt"];
|
|
1555
|
+
LANG_EXTS = {
|
|
1556
|
+
ts: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
|
|
1557
|
+
js: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
|
|
1558
|
+
python: [".py"],
|
|
1559
|
+
go: [".go"],
|
|
1560
|
+
rust: [".rs"],
|
|
1561
|
+
java: [".java"],
|
|
1562
|
+
kotlin: [".kt"],
|
|
1563
|
+
ruby: [".rb"],
|
|
1564
|
+
php: [".php"],
|
|
1565
|
+
csharp: [".cs"]
|
|
1566
|
+
};
|
|
1567
|
+
LANG_INDEX = { python: ["__init__"], go: ["mod"], rust: ["mod"] };
|
|
1568
|
+
MIN_REF_NAME = 3;
|
|
1569
|
+
MAX_EDGES = 4e5;
|
|
1570
|
+
LANG_FAMILY = {
|
|
1571
|
+
ts: "js",
|
|
1572
|
+
js: "js",
|
|
1573
|
+
c: "c",
|
|
1574
|
+
cpp: "c",
|
|
1575
|
+
java: "jvm",
|
|
1576
|
+
kotlin: "jvm"
|
|
1577
|
+
};
|
|
1578
|
+
INDEX_NAMES = ["index", "__init__", "mod"];
|
|
1579
|
+
MODULE_ROOTS = ["", "src", "lib", "app", "source", "src/main/java", "src/main/kotlin", "src/test/java"];
|
|
1580
|
+
MODULE_SEPARATOR = { python: ".", java: ".", kotlin: ".", csharp: ".", php: "\\", rust: "::" };
|
|
1581
|
+
BARE_IS_PACKAGE = /* @__PURE__ */ new Set(["ts", "js"]);
|
|
1582
|
+
EXT_ORDER = orders(RESOLVE_EXTS, LANG_EXTS);
|
|
1583
|
+
INDEX_ORDER = orders(INDEX_NAMES, LANG_INDEX);
|
|
1584
|
+
MAX_BODY_TOKENS = 80;
|
|
1585
|
+
ROOT_MARKERS = [".git", "package.json", "go.mod", "pyproject.toml", "Cargo.toml", "pom.xml", "composer.json"];
|
|
1586
|
+
MAX_ROOT_WALK = 6;
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
// src/codegraph-query.ts
|
|
1591
|
+
var codegraph_query_exports = {};
|
|
1592
|
+
__export(codegraph_query_exports, {
|
|
1593
|
+
NOT_INDEXED: () => NOT_INDEXED,
|
|
1594
|
+
codeGraphAsk: () => codeGraphAsk,
|
|
1595
|
+
codeGraphCheck: () => codeGraphCheck,
|
|
1596
|
+
codeGraphGrep: () => codeGraphGrep,
|
|
1597
|
+
codeGraphMap: () => codeGraphMap,
|
|
1598
|
+
codeGraphSkeleton: () => codeGraphSkeleton,
|
|
1599
|
+
codeGraphTrace: () => codeGraphTrace,
|
|
1600
|
+
isCleanDrift: () => isCleanDrift,
|
|
1601
|
+
resolveSymbolNodes: () => resolveSymbolNodes
|
|
1602
|
+
});
|
|
1603
|
+
import { join as join6 } from "path";
|
|
1604
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1605
|
+
function load(opts) {
|
|
1606
|
+
const graph = loadFreshGraph(opts.project, opts.refresh ?? true);
|
|
1607
|
+
if (!graph) return null;
|
|
1608
|
+
const nodes = graphNodes(graph);
|
|
1609
|
+
return { graph, nodes, byId: new Map(nodes.map((n) => [n.id, n])) };
|
|
1610
|
+
}
|
|
1611
|
+
function normalizePrefix(prefix) {
|
|
1612
|
+
return prefix.split("\\").join("/").replace(/\/+$/, "");
|
|
1613
|
+
}
|
|
1614
|
+
function underPrefix(path, prefix) {
|
|
1615
|
+
return prefix === "" || path === prefix || path.startsWith(`${prefix}/`);
|
|
1616
|
+
}
|
|
1617
|
+
function savingsFor(nodes, paths) {
|
|
1618
|
+
const sizes = /* @__PURE__ */ new Map();
|
|
1619
|
+
for (const n of nodes) if (n.kind === "file" && typeof n.chars === "number") sizes.set(n.path, n.chars);
|
|
1620
|
+
let baselineChars = 0;
|
|
1621
|
+
let files = 0;
|
|
1622
|
+
for (const p of new Set(paths)) {
|
|
1623
|
+
const c = sizes.get(p);
|
|
1624
|
+
if (c === void 0) continue;
|
|
1625
|
+
baselineChars += c;
|
|
1626
|
+
files++;
|
|
1627
|
+
}
|
|
1628
|
+
return files > 0 ? { files, baselineChars } : void 0;
|
|
1629
|
+
}
|
|
1630
|
+
function sliceSpan(root, path, from, to) {
|
|
1631
|
+
try {
|
|
1632
|
+
const lines = readFileSync6(join6(root, path), "utf8").split("\n");
|
|
1633
|
+
const start = Math.max(1, from);
|
|
1634
|
+
const end = Math.min(lines.length, to);
|
|
1635
|
+
const slice = lines.slice(start - 1, end);
|
|
1636
|
+
if (slice.length > MAX_SPAN_LINES) {
|
|
1637
|
+
const head = slice.slice(0, MAX_SPAN_LINES);
|
|
1638
|
+
head.push(`... (+${slice.length - MAX_SPAN_LINES} more lines; open ${path}:${start}-${end})`);
|
|
1639
|
+
return head.join("\n");
|
|
1640
|
+
}
|
|
1641
|
+
return slice.join("\n");
|
|
1642
|
+
} catch {
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function resolveSymbolNodes(nodes, query, inPrefix) {
|
|
1647
|
+
const lower = query.toLowerCase();
|
|
1648
|
+
const suffixHash = `#${lower}`;
|
|
1649
|
+
const suffixDot = `.${lower}`;
|
|
1650
|
+
const stripOrdinals = (s) => s.replace(/~\d+(?=\.|$)/g, "");
|
|
1651
|
+
let matches = nodes.filter((n) => {
|
|
1652
|
+
if (n.kind === "file") return false;
|
|
1653
|
+
if (n.name.toLowerCase() === lower) return true;
|
|
1654
|
+
const id = n.id.toLowerCase();
|
|
1655
|
+
const hash = id.indexOf("#");
|
|
1656
|
+
const candidate = hash === -1 ? id : id.slice(0, hash + 1) + stripOrdinals(id.slice(hash + 1));
|
|
1657
|
+
return candidate.endsWith(suffixHash) || candidate.endsWith(suffixDot);
|
|
1658
|
+
});
|
|
1659
|
+
if (!matches.length && query.includes(".")) {
|
|
1660
|
+
const last = query.slice(query.lastIndexOf(".") + 1).toLowerCase();
|
|
1661
|
+
if (last) matches = nodes.filter((n) => n.kind !== "file" && n.name.toLowerCase() === last);
|
|
1662
|
+
}
|
|
1663
|
+
if (!matches.length && query.includes(".") && !query.includes("#")) {
|
|
1664
|
+
matches = nodes.filter((n) => n.kind === "file" && (n.path.toLowerCase() === lower || n.path.toLowerCase().endsWith(`/${lower}`)));
|
|
1665
|
+
}
|
|
1666
|
+
if (inPrefix !== void 0) matches = matches.filter((n) => underPrefix(n.path, inPrefix));
|
|
1667
|
+
return matches;
|
|
1668
|
+
}
|
|
1669
|
+
function toHit(n, score, relation) {
|
|
1670
|
+
return {
|
|
1671
|
+
kind: relation ? relation === "imports" ? "callee" : "symbol" : "symbol",
|
|
1672
|
+
name: n.name,
|
|
1673
|
+
symbolKind: n.kind,
|
|
1674
|
+
path: n.path,
|
|
1675
|
+
line: n.line,
|
|
1676
|
+
endLine: n.endLine,
|
|
1677
|
+
signature: n.signature,
|
|
1678
|
+
score,
|
|
1679
|
+
relation
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
function findSubject(query, nodes, inPrefix) {
|
|
1683
|
+
const words = [...new Set(query.split(/[^A-Za-z0-9_.]+/).filter(Boolean))].sort((a, b) => b.length - a.length);
|
|
1684
|
+
for (const w of words) {
|
|
1685
|
+
const hits = resolveSymbolNodes(nodes, w, inPrefix);
|
|
1686
|
+
if (hits.length) return { subject: hits, tried: w };
|
|
1687
|
+
}
|
|
1688
|
+
return { subject: [], tried: words[0] ?? query };
|
|
1689
|
+
}
|
|
1690
|
+
function structuralAsk(query, loaded, limit, inPrefix) {
|
|
1691
|
+
const wantsIn = INCOMING.test(query);
|
|
1692
|
+
const wantsOut = OUTGOING.test(query);
|
|
1693
|
+
if (!wantsIn && !wantsOut) return null;
|
|
1694
|
+
const { subject, tried } = findSubject(query, loaded.nodes, inPrefix);
|
|
1695
|
+
const nudge = (name) => `No indexed edges for '${name}'. Showing word matches instead; for exact edges try: enigma codegraph callers ${name}`;
|
|
1696
|
+
if (!subject.length) return { fallthrough: nudge(tried) };
|
|
1697
|
+
const ids = new Set(subject.map((n) => n.id));
|
|
1698
|
+
const outgoing = wantsOut && !wantsIn;
|
|
1699
|
+
const rels = new Set(outgoing ? OUTGOING_RELS : INCOMING_RELS);
|
|
1700
|
+
const hits = [];
|
|
1701
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1702
|
+
for (const [source, target, rel] of loaded.graph.edges) {
|
|
1703
|
+
if (!rels.has(rel)) continue;
|
|
1704
|
+
const anchor = outgoing ? source : target;
|
|
1705
|
+
const other = outgoing ? target : source;
|
|
1706
|
+
if (!ids.has(anchor) || seen.has(other + rel)) continue;
|
|
1707
|
+
seen.add(other + rel);
|
|
1708
|
+
const node = loaded.byId.get(other);
|
|
1709
|
+
if (!node) continue;
|
|
1710
|
+
if (inPrefix !== void 0 && !underPrefix(node.path, inPrefix)) continue;
|
|
1711
|
+
hits.push({ ...toHit(node, 1, rel), kind: outgoing ? "callee" : "caller" });
|
|
1712
|
+
}
|
|
1713
|
+
if (!hits.length) return { fallthrough: nudge(subject[0].name) };
|
|
1714
|
+
hits.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line);
|
|
1715
|
+
return {
|
|
1716
|
+
query,
|
|
1717
|
+
mode: "structural",
|
|
1718
|
+
subject: subject[0].name,
|
|
1719
|
+
hits: hits.slice(0, limit),
|
|
1720
|
+
note: outgoing ? `outgoing edges from ${subject[0].name}` : `callers and references of ${subject[0].name}`
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
function lexicalAsk(query, loaded, limit, inPrefix) {
|
|
1724
|
+
const q = new Map([...counts(tokenize(query)).keys()].map((t) => [t, 1]));
|
|
1725
|
+
const bodies = loadBodyIndex(loaded.graph.id);
|
|
1726
|
+
const wantsTests = /\b(tests?|specs?|coverage|assert(?:ion)?s?|fixtures?|mocks?)\b/i.test(query);
|
|
1727
|
+
const testFactor = (path) => !wantsTests && isTestPath(path) ? TEST_PENALTY : 1;
|
|
1728
|
+
const nodes = inPrefix === void 0 ? loaded.nodes : loaded.nodes.filter((n) => underPrefix(n.path, inPrefix));
|
|
1729
|
+
const docs = nodes.map((n) => ({
|
|
1730
|
+
n,
|
|
1731
|
+
name: counts(tokenize(n.name)),
|
|
1732
|
+
path: counts(tokenize(n.path)),
|
|
1733
|
+
body: mergeBags(counts(tokenize(n.signature)), bodies.get(n.id))
|
|
1734
|
+
}));
|
|
1735
|
+
if (!docs.length) {
|
|
1736
|
+
return {
|
|
1737
|
+
query,
|
|
1738
|
+
mode: "empty",
|
|
1739
|
+
hits: [],
|
|
1740
|
+
note: loaded.nodes.length ? `No indexed files under '${inPrefix}'. Check the path, or widen it by dropping --in.` : NOT_INDEXED
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
const idf = computeIdf(docs.map((d) => /* @__PURE__ */ new Set([...d.name.keys(), ...d.path.keys(), ...d.body.keys()])));
|
|
1744
|
+
const defaultIdf = Math.log(1 + docs.length);
|
|
1745
|
+
const avgBody = docs.reduce((a, d) => a + bagLength(d.body), 0) / docs.length;
|
|
1746
|
+
const lex = /* @__PURE__ */ new Map();
|
|
1747
|
+
let maxLex = 0;
|
|
1748
|
+
for (const { n, name, path, body } of docs) {
|
|
1749
|
+
const total = (overlapScore(q, name, idf) * 3 + overlapScore(q, path, idf) * 2 + bm25(q, body, idf, bagLength(body), avgBody)) * testFactor(n.path);
|
|
1750
|
+
if (total > 0) {
|
|
1751
|
+
lex.set(n.id, total);
|
|
1752
|
+
maxLex = Math.max(maxLex, total);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
const allowed = inPrefix === void 0 ? void 0 : (id) => underPrefix(loaded.byId.get(id)?.path ?? "", inPrefix);
|
|
1756
|
+
const pr = lex.size ? personalizedPageRank(nodes.map((n) => n.id), loaded.graph.edges, lex, { nodeFilter: allowed }) : /* @__PURE__ */ new Map();
|
|
1757
|
+
const candidates = new Set(lex.keys());
|
|
1758
|
+
for (const [id, p] of pr) if (p >= RESCUE_FLOOR) candidates.add(id);
|
|
1759
|
+
const docById = new Map(docs.map((d) => [d.n.id, d]));
|
|
1760
|
+
const hits = [];
|
|
1761
|
+
const coverageOf = /* @__PURE__ */ new Map();
|
|
1762
|
+
const strengthOf = /* @__PURE__ */ new Map();
|
|
1763
|
+
for (const id of candidates) {
|
|
1764
|
+
const n = loaded.byId.get(id);
|
|
1765
|
+
if (!n) continue;
|
|
1766
|
+
const lexN = maxLex > 0 ? (lex.get(id) ?? 0) / maxLex : 0;
|
|
1767
|
+
const blended = (lexN + GRAPH_WEIGHT * (pr.get(id) ?? 0)) * testFactor(n.path);
|
|
1768
|
+
if (blended <= 0) continue;
|
|
1769
|
+
const hit = toHit(n, blended);
|
|
1770
|
+
const d = docById.get(id);
|
|
1771
|
+
coverageOf.set(hit, d ? matchedShare(q, [d.name, d.path, d.body], idf, defaultIdf) : 0);
|
|
1772
|
+
strengthOf.set(hit, d && n.kind !== "file" ? matchedShare(q, [d.name], idf, defaultIdf) : 0);
|
|
1773
|
+
hits.push(hit);
|
|
1774
|
+
}
|
|
1775
|
+
hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path) || a.line - b.line);
|
|
1776
|
+
const top = hits.slice(0, limit);
|
|
1777
|
+
return {
|
|
1778
|
+
query,
|
|
1779
|
+
mode: hits.length ? "lexical" : "empty",
|
|
1780
|
+
hits: top,
|
|
1781
|
+
coverage: hits.length && q.size ? coverageOf.get(hits[0]) : void 0,
|
|
1782
|
+
coverageStrong: hits.length && q.size ? strengthOf.get(hits[0]) : void 0,
|
|
1783
|
+
note: hits.length ? void 0 : "No matching nodes. Try different words, or re-index: enigma codegraph index"
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function mergeBags(a, b) {
|
|
1787
|
+
if (!b) return a;
|
|
1788
|
+
const out = new Map(a);
|
|
1789
|
+
for (const [t, c] of b) out.set(t, (out.get(t) ?? 0) + c);
|
|
1790
|
+
return out;
|
|
1791
|
+
}
|
|
1792
|
+
function codeGraphAsk(query, opts = {}) {
|
|
1793
|
+
const loaded = load(opts);
|
|
1794
|
+
if (!loaded) return null;
|
|
1795
|
+
const limit = opts.limit ?? 8;
|
|
1796
|
+
const inPrefix = opts.in === void 0 ? void 0 : normalizePrefix(opts.in);
|
|
1797
|
+
const structural = structuralAsk(query, loaded, limit, inPrefix);
|
|
1798
|
+
let result;
|
|
1799
|
+
if (structural && "hits" in structural) {
|
|
1800
|
+
result = structural;
|
|
1801
|
+
} else {
|
|
1802
|
+
result = lexicalAsk(query, loaded, limit, inPrefix);
|
|
1803
|
+
if (structural) result.note = result.note ? `${structural.fallthrough}
|
|
1804
|
+
${result.note}` : structural.fallthrough;
|
|
1805
|
+
}
|
|
1806
|
+
if (opts.source) {
|
|
1807
|
+
for (const h of result.hits) {
|
|
1808
|
+
const code = sliceSpan(loaded.graph.root, h.path, h.line, h.endLine);
|
|
1809
|
+
if (code) h.code = code;
|
|
1810
|
+
}
|
|
1811
|
+
result.saved = savingsFor(loaded.nodes, result.hits.map((h) => h.path));
|
|
1812
|
+
}
|
|
1813
|
+
return result;
|
|
1814
|
+
}
|
|
1815
|
+
function adjacency(edges, direction) {
|
|
1816
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1817
|
+
for (const [source, target, rel] of edges) {
|
|
1818
|
+
if (!WALK_RELATIONS.has(rel)) continue;
|
|
1819
|
+
const key = direction === "in" ? target : source;
|
|
1820
|
+
const other = direction === "in" ? source : target;
|
|
1821
|
+
const entry = { other, relation: rel };
|
|
1822
|
+
const arr = adj.get(key);
|
|
1823
|
+
if (arr) arr.push(entry);
|
|
1824
|
+
else adj.set(key, [entry]);
|
|
1825
|
+
}
|
|
1826
|
+
return adj;
|
|
1827
|
+
}
|
|
1828
|
+
function codeGraphTrace(symbol, opts = {}) {
|
|
1829
|
+
const loaded = load(opts);
|
|
1830
|
+
if (!loaded) return null;
|
|
1831
|
+
const direction = opts.direction ?? "in";
|
|
1832
|
+
const depth = Math.max(1, opts.depth ?? 1);
|
|
1833
|
+
const inPrefix = opts.in === void 0 ? void 0 : normalizePrefix(opts.in);
|
|
1834
|
+
const matches = resolveSymbolNodes(loaded.nodes, symbol, inPrefix);
|
|
1835
|
+
if (!matches.length) {
|
|
1836
|
+
return { symbol, direction, depth, matched: [], hits: [], note: `No symbol named '${symbol}' in the graph. Try: enigma codegraph search ${symbol}` };
|
|
1837
|
+
}
|
|
1838
|
+
const seeds = [...matches];
|
|
1839
|
+
for (const m of matches) {
|
|
1840
|
+
if (m.kind !== "file") continue;
|
|
1841
|
+
for (const n of loaded.nodes) if (n.kind !== "file" && n.path === m.path) seeds.push(n);
|
|
1842
|
+
}
|
|
1843
|
+
const adj = adjacency(loaded.graph.edges, direction);
|
|
1844
|
+
const visited = new Set(seeds.map((s) => s.id));
|
|
1845
|
+
const hits = [];
|
|
1846
|
+
let frontier = [...visited];
|
|
1847
|
+
for (let d = 1; d <= depth && frontier.length; d++) {
|
|
1848
|
+
const next = [];
|
|
1849
|
+
for (const current of frontier) {
|
|
1850
|
+
for (const { other, relation } of adj.get(current) ?? []) {
|
|
1851
|
+
if (visited.has(other)) continue;
|
|
1852
|
+
visited.add(other);
|
|
1853
|
+
const node = loaded.byId.get(other);
|
|
1854
|
+
if (!node) continue;
|
|
1855
|
+
if (inPrefix !== void 0 && !underPrefix(node.path, inPrefix)) continue;
|
|
1856
|
+
hits.push({ id: other, name: node.name, kind: node.kind, path: node.path, line: node.line, relation, depth: d });
|
|
1857
|
+
next.push(other);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
frontier = next;
|
|
1861
|
+
}
|
|
1862
|
+
hits.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path) || a.line - b.line);
|
|
1863
|
+
return {
|
|
1864
|
+
symbol,
|
|
1865
|
+
direction,
|
|
1866
|
+
depth,
|
|
1867
|
+
matched: matches.map((m) => ({ name: m.name, path: m.path, line: m.line })),
|
|
1868
|
+
hits,
|
|
1869
|
+
note: hits.length ? void 0 : `'${symbol}' resolved, but the graph has no ${direction === "in" ? "inbound" : "outbound"} edges for it.`
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function codeGraphSkeleton(file, opts = {}) {
|
|
1873
|
+
const loaded = load(opts);
|
|
1874
|
+
if (!loaded) return null;
|
|
1875
|
+
const want = normalizePrefix(file);
|
|
1876
|
+
let defs = loaded.nodes.filter((n) => n.kind !== "file" && n.path === want);
|
|
1877
|
+
if (!defs.length) {
|
|
1878
|
+
const paths = new Set(loaded.nodes.filter((n) => n.path === want || n.path.endsWith(`/${want}`)).map((n) => n.path));
|
|
1879
|
+
if (paths.size > 1) return { file: want, entries: [], note: `Ambiguous. Matches: ${[...paths].sort().join(", ")}` };
|
|
1880
|
+
const [path] = paths;
|
|
1881
|
+
if (path) defs = loaded.nodes.filter((n) => n.kind !== "file" && n.path === path);
|
|
1882
|
+
}
|
|
1883
|
+
if (!defs.length) return { file: want, entries: [], note: "No definitions indexed for this file." };
|
|
1884
|
+
defs.sort((a, b) => a.line - b.line);
|
|
1885
|
+
return {
|
|
1886
|
+
file: defs[0].path,
|
|
1887
|
+
entries: defs.map((n) => ({ name: n.name, kind: n.kind, line: n.line, endLine: n.endLine, signature: n.signature })),
|
|
1888
|
+
saved: savingsFor(loaded.nodes, [defs[0].path])
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
function dirKey(path, depth) {
|
|
1892
|
+
const segments = path.split("/");
|
|
1893
|
+
return segments.slice(0, Math.min(depth, segments.length)).join("/");
|
|
1894
|
+
}
|
|
1895
|
+
function topHubs(nodes, deg, cap) {
|
|
1896
|
+
return nodes.map((n) => ({ name: n.name, kind: n.kind, path: n.path, line: n.line, inDegree: deg.get(n.id) ?? 0 })).filter((h) => h.inDegree > 0).sort((a, b) => b.inDegree - a.inDegree || a.name.localeCompare(b.name) || a.path.localeCompare(b.path)).slice(0, cap);
|
|
1897
|
+
}
|
|
1898
|
+
function codeGraphMap(opts = {}) {
|
|
1899
|
+
const loaded = load(opts);
|
|
1900
|
+
if (!loaded) return null;
|
|
1901
|
+
const maxDirs = opts.maxDirs ?? DEFAULT_MAX_DIRS;
|
|
1902
|
+
const inPrefix = opts.in === void 0 ? void 0 : normalizePrefix(opts.in);
|
|
1903
|
+
const nodes = inPrefix === void 0 ? loaded.nodes : loaded.nodes.filter((n) => underPrefix(n.path, inPrefix));
|
|
1904
|
+
const deg = inDegree(loaded.graph.edges, true);
|
|
1905
|
+
const fileNodes = nodes.filter((n) => n.kind === "file");
|
|
1906
|
+
const langByPath = new Map(loaded.graph.files.map((f) => [f.path, f.lang]));
|
|
1907
|
+
const depth1 = /* @__PURE__ */ new Map();
|
|
1908
|
+
for (const n of fileNodes) depth1.set(dirKey(n.path, 1), (depth1.get(dirKey(n.path, 1)) ?? 0) + 1);
|
|
1909
|
+
let splitSegment = null;
|
|
1910
|
+
for (const [seg, count] of depth1) {
|
|
1911
|
+
if (!fileNodes.length || count / fileNodes.length <= SPLIT_THRESHOLD) continue;
|
|
1912
|
+
const subdirs = new Set(fileNodes.filter((n) => dirKey(n.path, 1) === seg && n.path.split("/").length > 2).map((n) => dirKey(n.path, 2)));
|
|
1913
|
+
if (subdirs.size >= 2) splitSegment = seg;
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1917
|
+
for (const n of nodes) {
|
|
1918
|
+
const key = dirKey(n.path, splitSegment !== null && dirKey(n.path, 1) === splitSegment ? 2 : 1);
|
|
1919
|
+
let g = groups.get(key);
|
|
1920
|
+
if (!g) {
|
|
1921
|
+
g = { files: [], symbols: [] };
|
|
1922
|
+
groups.set(key, g);
|
|
1923
|
+
}
|
|
1924
|
+
if (n.kind === "file") g.files.push(n);
|
|
1925
|
+
else g.symbols.push(n);
|
|
1926
|
+
}
|
|
1927
|
+
const filePaths = new Set(fileNodes.map((f) => f.path));
|
|
1928
|
+
const dirs = [...groups.entries()].map(([path, g]) => ({
|
|
1929
|
+
path,
|
|
1930
|
+
files: g.files.length,
|
|
1931
|
+
symbols: g.symbols.length,
|
|
1932
|
+
languages: [...new Set(g.files.map((f) => langByPath.get(f.path)).filter((l) => !!l))].sort(),
|
|
1933
|
+
hubs: topHubs(g.symbols, deg, DEFAULT_HUBS_PER_DIR),
|
|
1934
|
+
isFile: filePaths.has(path)
|
|
1935
|
+
})).sort((a, b) => b.symbols - a.symbols || a.path.localeCompare(b.path));
|
|
1936
|
+
const symbols = nodes.filter((n) => n.kind !== "file");
|
|
1937
|
+
const shown = dirs.slice(0, maxDirs);
|
|
1938
|
+
const hotspots = topHubs(symbols, deg, DEFAULT_HOTSPOTS);
|
|
1939
|
+
const surfaced = [...shown.flatMap((d) => d.hubs.map((h) => h.path)), ...hotspots.map((h) => h.path)];
|
|
1940
|
+
return {
|
|
1941
|
+
totals: {
|
|
1942
|
+
files: fileNodes.length,
|
|
1943
|
+
symbols: symbols.length,
|
|
1944
|
+
edges: loaded.graph.edges.length,
|
|
1945
|
+
languages: [...new Set(fileNodes.map((f) => langByPath.get(f.path)).filter((l) => !!l))].sort()
|
|
1946
|
+
},
|
|
1947
|
+
dirs: shown,
|
|
1948
|
+
hotspots,
|
|
1949
|
+
dropped: Math.max(0, dirs.length - maxDirs),
|
|
1950
|
+
truncated: loaded.graph.truncated === true,
|
|
1951
|
+
saved: savingsFor(loaded.nodes, surfaced)
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
function compileGrep(pattern, opts) {
|
|
1955
|
+
const flags = opts.ignoreCase ? "i" : "";
|
|
1956
|
+
if (opts.fixed) return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
|
|
1957
|
+
try {
|
|
1958
|
+
return new RegExp(pattern, flags);
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
throw new Error(`invalid regex '${pattern}': ${e.message} - pass --fixed to search for it literally.`);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function codeGraphGrep(pattern, opts = {}) {
|
|
1964
|
+
const loaded = load(opts);
|
|
1965
|
+
if (!loaded) return null;
|
|
1966
|
+
const maxHits = opts.maxHits ?? DEFAULT_MAX_HITS;
|
|
1967
|
+
const regex = compileGrep(pattern, opts);
|
|
1968
|
+
const inPrefix = opts.in === void 0 ? void 0 : normalizePrefix(opts.in);
|
|
1969
|
+
const deg = inDegree(loaded.graph.edges, true);
|
|
1970
|
+
const spansByFile = /* @__PURE__ */ new Map();
|
|
1971
|
+
for (const n of loaded.nodes) {
|
|
1972
|
+
if (n.kind === "file") continue;
|
|
1973
|
+
const arr = spansByFile.get(n.path);
|
|
1974
|
+
if (arr) arr.push(n);
|
|
1975
|
+
else spansByFile.set(n.path, [n]);
|
|
1976
|
+
}
|
|
1977
|
+
for (const arr of spansByFile.values()) arr.sort((a, b) => a.line - b.line);
|
|
1978
|
+
const files = loaded.nodes.filter((n) => n.kind === "file" && (inPrefix === void 0 || underPrefix(n.path, inPrefix)));
|
|
1979
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1980
|
+
let collected = 0;
|
|
1981
|
+
let truncatedHits = 0;
|
|
1982
|
+
let truncatedFiles = 0;
|
|
1983
|
+
for (const file of files) {
|
|
1984
|
+
let text;
|
|
1985
|
+
try {
|
|
1986
|
+
text = readFileSync6(join6(loaded.graph.root, file.path), "utf8");
|
|
1987
|
+
} catch {
|
|
1988
|
+
truncatedFiles++;
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
const spans = spansByFile.get(file.path) ?? [];
|
|
1992
|
+
const lines = text.split("\n");
|
|
1993
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1994
|
+
if (!regex.test(lines[i])) continue;
|
|
1995
|
+
if (collected >= maxHits) {
|
|
1996
|
+
truncatedHits++;
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
collected++;
|
|
2000
|
+
const lineNo = i + 1;
|
|
2001
|
+
let sym = null;
|
|
2002
|
+
for (const s of spans) {
|
|
2003
|
+
if (s.line > lineNo) break;
|
|
2004
|
+
if (lineNo <= s.endLine) sym = s;
|
|
2005
|
+
}
|
|
2006
|
+
const key = sym ? sym.id : `file:${file.path}`;
|
|
2007
|
+
let group = groups.get(key);
|
|
2008
|
+
if (!group) {
|
|
2009
|
+
group = {
|
|
2010
|
+
symbol: sym ? { name: sym.name, kind: sym.kind, line: sym.line, endLine: sym.endLine } : null,
|
|
2011
|
+
path: file.path,
|
|
2012
|
+
inDegree: sym ? deg.get(sym.id) ?? 0 : 0,
|
|
2013
|
+
hits: []
|
|
2014
|
+
};
|
|
2015
|
+
groups.set(key, group);
|
|
2016
|
+
}
|
|
2017
|
+
const trimmed = lines[i].trim();
|
|
2018
|
+
group.hits.push({ line: lineNo, text: trimmed.length > MAX_HIT_TEXT ? trimmed.slice(0, MAX_HIT_TEXT) : trimmed });
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const sorted = [...groups.values()].sort((a, b) => b.inDegree - a.inDegree || a.path.localeCompare(b.path) || (a.symbol?.line ?? 0) - (b.symbol?.line ?? 0));
|
|
2022
|
+
return {
|
|
2023
|
+
pattern,
|
|
2024
|
+
filesSearched: files.length,
|
|
2025
|
+
totalHits: collected,
|
|
2026
|
+
groups: sorted,
|
|
2027
|
+
truncated: { files: truncatedFiles, hits: truncatedHits, unindexedFiles: loaded.graph.truncated === true },
|
|
2028
|
+
saved: savingsFor(loaded.nodes, sorted.map((g) => g.path))
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
function codeGraphCheck(project) {
|
|
2032
|
+
const graph = loadFreshGraph(project, false);
|
|
2033
|
+
if (!graph) return null;
|
|
2034
|
+
const drift = probeDrift(graph);
|
|
2035
|
+
return {
|
|
2036
|
+
project: graph.name,
|
|
2037
|
+
root: graph.root,
|
|
2038
|
+
indexedAt: graph.indexedAt,
|
|
2039
|
+
drift,
|
|
2040
|
+
stale: driftCount(drift),
|
|
2041
|
+
truncated: graph.truncated === true
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
var NOT_INDEXED, MAX_SPAN_LINES, INCOMING, OUTGOING, INCOMING_RELS, OUTGOING_RELS, GRAPH_WEIGHT, RESCUE_FLOOR, TEST_PENALTY, DEFAULT_MAX_DIRS, DEFAULT_HUBS_PER_DIR, DEFAULT_HOTSPOTS, SPLIT_THRESHOLD, DEFAULT_MAX_HITS, MAX_HIT_TEXT;
|
|
2045
|
+
var init_codegraph_query = __esm({
|
|
2046
|
+
"src/codegraph-query.ts"() {
|
|
2047
|
+
"use strict";
|
|
2048
|
+
init_codegraph();
|
|
2049
|
+
init_codegraph_rank();
|
|
2050
|
+
init_codegraph();
|
|
2051
|
+
NOT_INDEXED = "No project indexed yet. Run: enigma codegraph index";
|
|
2052
|
+
MAX_SPAN_LINES = 80;
|
|
2053
|
+
INCOMING = /\b(caller|callers|calls?\s+into|who\s+calls|what\s+calls|called\s+by|used\s+by|uses|who\s+imports?|what\s+imports?|imported\s+by|importers?)\b/i;
|
|
2054
|
+
OUTGOING = /\b(callee|callees|calls\s+what|imports?|depends\s+on)\b/i;
|
|
2055
|
+
INCOMING_RELS = ["calls", "references", "implements", "extends"];
|
|
2056
|
+
OUTGOING_RELS = ["calls", "references", "imports", "implements", "extends"];
|
|
2057
|
+
GRAPH_WEIGHT = 0.5;
|
|
2058
|
+
RESCUE_FLOOR = 0.15;
|
|
2059
|
+
TEST_PENALTY = 0.35;
|
|
2060
|
+
DEFAULT_MAX_DIRS = 16;
|
|
2061
|
+
DEFAULT_HUBS_PER_DIR = 3;
|
|
2062
|
+
DEFAULT_HOTSPOTS = 12;
|
|
2063
|
+
SPLIT_THRESHOLD = 0.6;
|
|
2064
|
+
DEFAULT_MAX_HITS = 300;
|
|
2065
|
+
MAX_HIT_TEXT = 160;
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
// src/codegraph-format.ts
|
|
2070
|
+
var codegraph_format_exports = {};
|
|
2071
|
+
__export(codegraph_format_exports, {
|
|
2072
|
+
formatAsk: () => formatAsk,
|
|
2073
|
+
formatFreshness: () => formatFreshness,
|
|
2074
|
+
formatGrep: () => formatGrep,
|
|
2075
|
+
formatMap: () => formatMap,
|
|
2076
|
+
formatSkeleton: () => formatSkeleton,
|
|
2077
|
+
formatSubgraph: () => formatSubgraph,
|
|
2078
|
+
formatTrace: () => formatTrace
|
|
2079
|
+
});
|
|
2080
|
+
function toTokens(chars) {
|
|
2081
|
+
return Math.round(chars / 4);
|
|
2082
|
+
}
|
|
2083
|
+
function savingsLine(saved, outputChars) {
|
|
2084
|
+
if (!saved) return "";
|
|
2085
|
+
const saved_ = Math.max(0, saved.baselineChars - outputChars);
|
|
2086
|
+
return `
|
|
2087
|
+
|
|
2088
|
+
~${toTokens(saved_).toLocaleString("en-US")} tokens saved vs reading ${saved.files} file${saved.files === 1 ? "" : "s"} whole.`;
|
|
2089
|
+
}
|
|
2090
|
+
function withSavings(body, saved) {
|
|
2091
|
+
return body + savingsLine(saved, body.length);
|
|
2092
|
+
}
|
|
2093
|
+
function formatAsk(r) {
|
|
2094
|
+
const head = `code graph - "${r.query}" (${r.mode})`;
|
|
2095
|
+
if (!r.hits.length) return `${head}
|
|
2096
|
+
|
|
2097
|
+
${r.note ?? "No matches."}
|
|
2098
|
+
`;
|
|
2099
|
+
const lines = [head];
|
|
2100
|
+
if (r.note) lines.push("", r.note);
|
|
2101
|
+
lines.push("");
|
|
2102
|
+
for (const h of r.hits) {
|
|
2103
|
+
const where = h.symbolKind === "file" ? h.path : `${h.path}:${h.line}-${h.endLine}`;
|
|
2104
|
+
lines.push(`- ${h.name} - ${h.symbolKind} - ${where}${h.relation ? ` (${h.relation})` : ""}`);
|
|
2105
|
+
if (h.signature) lines.push(` ${h.signature}`);
|
|
2106
|
+
if (h.code) lines.push("", "```", h.code, "```", "");
|
|
2107
|
+
}
|
|
2108
|
+
if (r.coverage !== void 0 && r.coverage < 0.5) {
|
|
2109
|
+
lines.push("", `Only ${Math.round(r.coverage * 100)}% of the query matched anything indexed - try naming a symbol or file directly.`);
|
|
2110
|
+
}
|
|
2111
|
+
return `${withSavings(lines.join("\n"), r.saved)}
|
|
2112
|
+
`;
|
|
2113
|
+
}
|
|
2114
|
+
function formatTrace(r) {
|
|
2115
|
+
const empty = r.direction === "in" ? "Nothing depends on it." : "It depends on nothing indexed.";
|
|
2116
|
+
const head = `${r.symbol} - ${r.direction === "in" ? "callers and references" : "outgoing dependencies"} (depth ${r.depth})`;
|
|
2117
|
+
if (!r.matched.length || !r.hits.length) return `${head}
|
|
2118
|
+
|
|
2119
|
+
${r.note ?? empty}
|
|
2120
|
+
`;
|
|
2121
|
+
const lines = [head, ""];
|
|
2122
|
+
lines.push(`defined at: ${r.matched.map((m) => `${m.path}:${m.line}`).join(", ")}`, "");
|
|
2123
|
+
for (const h of r.hits) {
|
|
2124
|
+
lines.push(`- ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.relation}${r.depth > 1 ? ` - depth ${h.depth}` : ""}`);
|
|
2125
|
+
}
|
|
2126
|
+
lines.push("", `${r.hits.length} node${r.hits.length === 1 ? "" : "s"}.`);
|
|
2127
|
+
return `${lines.join("\n")}
|
|
2128
|
+
`;
|
|
2129
|
+
}
|
|
2130
|
+
function formatSkeleton(r) {
|
|
2131
|
+
const head = `code graph skeleton - ${r.file}`;
|
|
2132
|
+
if (!r.entries.length) return `${head}
|
|
2133
|
+
|
|
2134
|
+
${r.note ?? "No definitions."}
|
|
2135
|
+
`;
|
|
2136
|
+
const lines = r.entries.map((e) => `- ${e.line}-${e.endLine} ${e.kind} ${e.name}${e.signature ? ` ${e.signature}` : ""}`);
|
|
2137
|
+
return `${withSavings([head, "", ...lines].join("\n"), r.saved)}
|
|
2138
|
+
`;
|
|
2139
|
+
}
|
|
2140
|
+
function formatMap(r) {
|
|
2141
|
+
const { totals } = r;
|
|
2142
|
+
const lines = [`repo map - ${totals.files} files - ${totals.symbols} symbols - ${totals.edges} edges - ${totals.languages.join(", ")}`, ""];
|
|
2143
|
+
for (const d of r.dirs) {
|
|
2144
|
+
const label = (d.isFile ? d.path : `${d.path}/`).padEnd(DIR_COL_WIDTH);
|
|
2145
|
+
const hubs = d.hubs.length ? ` hubs: ${d.hubs.map((h) => `${h.name} (${h.path.split("/").pop()}, ${h.inDegree} in)`).join(", ")}` : "";
|
|
2146
|
+
lines.push(`${label}${d.files} files - ${d.symbols} symbols${hubs}`);
|
|
2147
|
+
}
|
|
2148
|
+
if (r.dropped > 0) lines.push(`... +${r.dropped} more director${r.dropped === 1 ? "y" : "ies"} not shown (raise --max-dirs to see them)`);
|
|
2149
|
+
if (r.hotspots.length) {
|
|
2150
|
+
lines.push("", "hotspots:");
|
|
2151
|
+
for (const h of r.hotspots) lines.push(` ${h.name} - ${h.kind} - ${h.path}:${h.line} - ${h.inDegree} in`);
|
|
2152
|
+
}
|
|
2153
|
+
if (r.truncated) lines.push("", INCOMPLETE_INDEX);
|
|
2154
|
+
return `${withSavings(lines.join("\n"), r.saved)}
|
|
2155
|
+
`;
|
|
2156
|
+
}
|
|
2157
|
+
function formatGrep(r) {
|
|
2158
|
+
const head = `"${r.pattern}" - ${r.totalHits} hit${r.totalHits === 1 ? "" : "s"} in ${r.groups.length} symbol${r.groups.length === 1 ? "" : "s"} (searched ${r.filesSearched} indexed files)`;
|
|
2159
|
+
const notes = [];
|
|
2160
|
+
if (r.truncated.hits > 0) notes.push(`+${r.truncated.hits} more hits past the cap (raise --max-hits).`);
|
|
2161
|
+
if (r.truncated.files > 0) notes.push(`${r.truncated.files} indexed file${r.truncated.files === 1 ? "" : "s"} could not be read.`);
|
|
2162
|
+
if (r.truncated.unindexedFiles) notes.push(INCOMPLETE_INDEX);
|
|
2163
|
+
if (!r.groups.length) return `${head}${notes.length ? `
|
|
2164
|
+
|
|
2165
|
+
${notes.join("\n")}` : ""}
|
|
2166
|
+
`;
|
|
2167
|
+
const lines = [head, ""];
|
|
2168
|
+
for (const g of r.groups) {
|
|
2169
|
+
const where = g.symbol ? `${g.symbol.name} - ${g.symbol.kind} - ${g.path}:${g.symbol.line}-${g.symbol.endLine} - ${g.inDegree} in` : `${g.path} - module level`;
|
|
2170
|
+
lines.push(where);
|
|
2171
|
+
for (const h of g.hits) lines.push(` ${h.line}: ${h.text}`);
|
|
2172
|
+
lines.push("");
|
|
2173
|
+
}
|
|
2174
|
+
lines.push(...notes);
|
|
2175
|
+
return `${withSavings(lines.join("\n").trimEnd(), r.saved)}
|
|
2176
|
+
`;
|
|
2177
|
+
}
|
|
2178
|
+
function sliceShare(shown, total) {
|
|
2179
|
+
if (!total || !shown) return "0%";
|
|
2180
|
+
const pct = shown / total * 100;
|
|
2181
|
+
return pct < 1 ? "<1%" : `${Math.round(pct)}%`;
|
|
2182
|
+
}
|
|
2183
|
+
function formatSubgraph(r) {
|
|
2184
|
+
const what = r.focus ? `around ${r.focus.map((f) => `${f.name} (${f.path}:${f.line})`).join(", ")}` : `${r.scope} overview`;
|
|
2185
|
+
const head = `${r.project} - graph slice: ${what}`;
|
|
2186
|
+
if (!r.nodes.length) return `${head}
|
|
2187
|
+
|
|
2188
|
+
${r.note ?? "Nothing to draw."}
|
|
2189
|
+
`;
|
|
2190
|
+
const lines = [
|
|
2191
|
+
head,
|
|
2192
|
+
"",
|
|
2193
|
+
// Against the population this scope draws from: a file slice carries import edges only,
|
|
2194
|
+
// so measuring it against the symbol graph's total understates it several times over.
|
|
2195
|
+
`${r.nodes.length} nodes - ${r.edges.length} edges - depth ${r.depth} - ${sliceShare(r.edges.length, r.scope === "files" ? r.totals.importEdges : r.totals.edges)} of the graph's wiring`,
|
|
2196
|
+
""
|
|
2197
|
+
];
|
|
2198
|
+
const out = /* @__PURE__ */ new Map();
|
|
2199
|
+
for (const e of r.edges) {
|
|
2200
|
+
const arr = out.get(e.source);
|
|
2201
|
+
if (arr) arr.push(`${e.relation} ${e.target}`);
|
|
2202
|
+
else out.set(e.source, [`${e.relation} ${e.target}`]);
|
|
2203
|
+
}
|
|
2204
|
+
for (const n of r.nodes.slice(0, 40)) {
|
|
2205
|
+
const where = n.kind === "file" ? n.path : `${n.path}:${n.line}`;
|
|
2206
|
+
const links = out.get(n.id) ?? [];
|
|
2207
|
+
lines.push(`- ${n.name} - ${n.kind} - ${where} (${n.inDegree} in${n.hidden ? `, +${n.hidden} hidden` : ""})`);
|
|
2208
|
+
for (const l of links.slice(0, 6)) lines.push(` -> ${l}`);
|
|
2209
|
+
if (links.length > 6) lines.push(` -> (+${links.length - 6} more)`);
|
|
2210
|
+
}
|
|
2211
|
+
if (r.nodes.length > 40) lines.push(` (+${r.nodes.length - 40} more nodes; raise --limit or narrow with --in)`);
|
|
2212
|
+
if (r.truncated) lines.push("", "TRUNCATED: the node cap was reached, so reachable nodes were left out.");
|
|
2213
|
+
if (r.incomplete) lines.push("", INCOMPLETE_INDEX);
|
|
2214
|
+
return `${lines.join("\n")}
|
|
2215
|
+
`;
|
|
2216
|
+
}
|
|
2217
|
+
function formatFreshness(r) {
|
|
2218
|
+
const head = `${r.project} - indexed ${new Date(r.indexedAt).toISOString().replace("T", " ").slice(0, 16)}`;
|
|
2219
|
+
const incomplete = r.truncated ? `
|
|
2220
|
+
|
|
2221
|
+
${INCOMPLETE_INDEX}` : "";
|
|
2222
|
+
if (r.stale === 0) return `${head}
|
|
2223
|
+
|
|
2224
|
+
The graph matches the working tree.${incomplete}
|
|
2225
|
+
`;
|
|
2226
|
+
const lines = [head, "", `${r.stale} file${r.stale === 1 ? "" : "s"} drifted since the last index:`];
|
|
2227
|
+
const show = (label, paths) => {
|
|
2228
|
+
if (!paths.length) return;
|
|
2229
|
+
lines.push(` ${label}: ${paths.slice(0, 10).join(", ")}${paths.length > 10 ? ` (+${paths.length - 10} more)` : ""}`);
|
|
2230
|
+
};
|
|
2231
|
+
show("changed", r.drift.changed);
|
|
2232
|
+
show("added", r.drift.added);
|
|
2233
|
+
show("removed", r.drift.removed);
|
|
2234
|
+
lines.push("", "Re-index with: enigma codegraph index");
|
|
2235
|
+
return `${lines.join("\n")}${incomplete}
|
|
2236
|
+
`;
|
|
2237
|
+
}
|
|
2238
|
+
var INCOMPLETE_INDEX, DIR_COL_WIDTH;
|
|
2239
|
+
var init_codegraph_format = __esm({
|
|
2240
|
+
"src/codegraph-format.ts"() {
|
|
2241
|
+
"use strict";
|
|
2242
|
+
INCOMPLETE_INDEX = "INCOMPLETE: the last scan hit a limit (too many files, or files too large), so some files were never indexed.";
|
|
2243
|
+
DIR_COL_WIDTH = 22;
|
|
2244
|
+
}
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
// src/codegraph-hook.ts
|
|
2248
|
+
import { homedir as homedir3 } from "os";
|
|
2249
|
+
import { spawn } from "child_process";
|
|
2250
|
+
import { basename as basename2, join as join7, relative as relative3, resolve as resolve4 } from "path";
|
|
2251
|
+
|
|
2252
|
+
// src/repo-keyed-file.ts
|
|
2253
|
+
import { dirname } from "path";
|
|
2254
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
2255
|
+
var MAX_REPOS = 100;
|
|
2256
|
+
function readRepoKeyedFile(path, version) {
|
|
2257
|
+
try {
|
|
2258
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
2259
|
+
if (!parsed || parsed.version !== version || typeof parsed.repos !== "object" || parsed.repos === null) return {};
|
|
2260
|
+
return parsed.repos;
|
|
2261
|
+
} catch {
|
|
2262
|
+
return {};
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
function writeRepoKeyedFile(path, version, repos, recencyOf) {
|
|
2266
|
+
let kept = repos;
|
|
2267
|
+
const entries = Object.entries(repos);
|
|
2268
|
+
if (entries.length > MAX_REPOS) {
|
|
2269
|
+
entries.sort((a, b) => recencyOf(b[1]) - recencyOf(a[1]));
|
|
2270
|
+
kept = Object.fromEntries(entries.slice(0, MAX_REPOS));
|
|
2271
|
+
}
|
|
2272
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2273
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2274
|
+
try {
|
|
2275
|
+
writeFileSync(tmp, `${JSON.stringify({ version, repos: kept })}
|
|
2276
|
+
`);
|
|
2277
|
+
renameSync(tmp, path);
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
try {
|
|
2280
|
+
rmSync(tmp, { force: true });
|
|
2281
|
+
} catch {
|
|
2282
|
+
}
|
|
2283
|
+
throw err;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/codegraph-hook.ts
|
|
2288
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2289
|
+
var MIN_PROMPT_CHARS = 12;
|
|
2290
|
+
var PROMPT_HITS = 3;
|
|
2291
|
+
var STRONG_FLOOR = 0.25;
|
|
2292
|
+
var BROAD_FLOOR = 0.4;
|
|
2293
|
+
var INJECTED_CAP = 60;
|
|
2294
|
+
var BLAST_CAP = 12;
|
|
2295
|
+
function parsePayload(raw) {
|
|
2296
|
+
try {
|
|
2297
|
+
return JSON.parse(raw);
|
|
2298
|
+
} catch {
|
|
2299
|
+
return {};
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function projectDir(input) {
|
|
2303
|
+
return resolve4(process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd());
|
|
2304
|
+
}
|
|
2305
|
+
function emit(event, additionalContext) {
|
|
2306
|
+
process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext } }));
|
|
2307
|
+
}
|
|
2308
|
+
function stateDir() {
|
|
2309
|
+
return join7(process.env.ENIGMA_CODEGRAPH_DIR || join7(homedir3(), ".enigma", "codegraph"), "sessions");
|
|
2310
|
+
}
|
|
2311
|
+
function stateFile(dir, sessionId) {
|
|
2312
|
+
const key = `${dir}|${sessionId}`.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "default";
|
|
2313
|
+
return join7(stateDir(), `${key}.json`);
|
|
2314
|
+
}
|
|
2315
|
+
function readState(dir, sessionId) {
|
|
2316
|
+
try {
|
|
2317
|
+
const parsed = JSON.parse(readFileSync7(stateFile(dir, sessionId), "utf8"));
|
|
2318
|
+
return { injected: Array.isArray(parsed.injected) ? parsed.injected : [], dirty: parsed.dirty === true };
|
|
2319
|
+
} catch {
|
|
2320
|
+
return { injected: [], dirty: false };
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
function writeState(dir, sessionId, state) {
|
|
2324
|
+
try {
|
|
2325
|
+
mkdirSync4(stateDir(), { recursive: true });
|
|
2326
|
+
writeFileSync5(stateFile(dir, sessionId), JSON.stringify({ injected: state.injected.slice(-INJECTED_CAP), dirty: state.dirty }));
|
|
2327
|
+
} catch {
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
var STATUSLINE_SNAPSHOT_VERSION = 2;
|
|
2331
|
+
function projectRoot(cg, project, fallback) {
|
|
2332
|
+
try {
|
|
2333
|
+
return cg.listProjects().find((p) => p.id === project)?.root ?? fallback;
|
|
2334
|
+
} catch {
|
|
2335
|
+
return fallback;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function writeStatuslineSnapshot(root, symbols, stale) {
|
|
2339
|
+
try {
|
|
2340
|
+
const dir = process.env.ENIGMA_CODEGRAPH_DIR || join7(homedir3(), ".enigma", "codegraph");
|
|
2341
|
+
const target = join7(dir, "statusline.json");
|
|
2342
|
+
const repos = readRepoKeyedFile(target, STATUSLINE_SNAPSHOT_VERSION);
|
|
2343
|
+
repos[root] = { root, symbols, stale, at: Date.now() };
|
|
2344
|
+
writeRepoKeyedFile(target, STATUSLINE_SNAPSHOT_VERSION, repos, (s) => s.at);
|
|
2345
|
+
} catch {
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
async function sessionStart(dir) {
|
|
2349
|
+
const q = await Promise.resolve().then(() => (init_codegraph_query(), codegraph_query_exports));
|
|
2350
|
+
const fmt = await Promise.resolve().then(() => (init_codegraph_format(), codegraph_format_exports));
|
|
2351
|
+
const cg = await Promise.resolve().then(() => (init_codegraph(), codegraph_exports));
|
|
2352
|
+
let project;
|
|
2353
|
+
try {
|
|
2354
|
+
project = cg.ensureProjectForCwd(dir);
|
|
2355
|
+
} catch {
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
const directive = [
|
|
2359
|
+
"[enigma] This repo is indexed as a code graph. To find, understand or change code, ask the graph before grepping - it answers from a prebuilt index with exact file:line. Pick the ONE tool that fits and act on its answer; most tasks need a single call.",
|
|
2360
|
+
` - enigma_codegraph_ask: where a task's code lives, ranked, with the source inlined. The default for "how does X work" / "where is Y".`,
|
|
2361
|
+
" - enigma_codegraph_trace: exact edges. Who calls a symbol, or what it calls with direction out. Run it BEFORE changing anything shared.",
|
|
2362
|
+
" - enigma_codegraph_skeleton: a file's whole API surface, no bodies.",
|
|
2363
|
+
" - enigma_codegraph_grep: every occurrence, grouped by enclosing symbol - when you need them ALL, not the top matches."
|
|
2364
|
+
].join("\n");
|
|
2365
|
+
const map = q.codeGraphMap({ project, refresh: false, maxDirs: 8 });
|
|
2366
|
+
const fresh = q.codeGraphCheck(project);
|
|
2367
|
+
const stale = fresh && fresh.stale > 0 ? `
|
|
2368
|
+
${fresh.stale} file(s) changed since the last index; the next query refreshes them.` : "";
|
|
2369
|
+
writeStatuslineSnapshot(projectRoot(cg, project, dir), map?.totals.symbols ?? 0, fresh?.stale ?? 0);
|
|
2370
|
+
emit("SessionStart", map ? `${directive}
|
|
2371
|
+
|
|
2372
|
+
${fmt.formatMap(map).trimEnd()}${stale}` : directive);
|
|
2373
|
+
}
|
|
2374
|
+
function coveringProject(cg, dir) {
|
|
2375
|
+
const project = cg.findProjectForCwd(dir);
|
|
2376
|
+
if (project) return project;
|
|
2377
|
+
backgroundIndex(cg, dir);
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
var INDEX_COOLDOWN_MS = 6e4;
|
|
2381
|
+
function indexMarkerFile(dir) {
|
|
2382
|
+
const key = dir.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "default";
|
|
2383
|
+
return join7(stateDir(), `indexing-${key}`);
|
|
2384
|
+
}
|
|
2385
|
+
function claimIndex(dir) {
|
|
2386
|
+
const file = indexMarkerFile(dir);
|
|
2387
|
+
try {
|
|
2388
|
+
const age = Date.now() - statSync3(file).mtimeMs;
|
|
2389
|
+
if (age >= 0 && age < INDEX_COOLDOWN_MS) return false;
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
try {
|
|
2393
|
+
mkdirSync4(stateDir(), { recursive: true });
|
|
2394
|
+
writeFileSync5(file, String(Date.now()));
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
return true;
|
|
2398
|
+
}
|
|
2399
|
+
function backgroundIndex(cg, dir) {
|
|
2400
|
+
if (cg.isManagedPath(dir) || !claimIndex(dir)) return;
|
|
2401
|
+
try {
|
|
2402
|
+
const child = spawn(process.execPath, [process.argv[1], "codegraph", "index", dir], { detached: true, stdio: "ignore", windowsHide: true });
|
|
2403
|
+
child.unref();
|
|
2404
|
+
} catch {
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
async function prompt(input, dir) {
|
|
2408
|
+
const text = String(input.prompt ?? "").trim();
|
|
2409
|
+
if (text.length < MIN_PROMPT_CHARS) return;
|
|
2410
|
+
const q = await Promise.resolve().then(() => (init_codegraph_query(), codegraph_query_exports));
|
|
2411
|
+
const cg = await Promise.resolve().then(() => (init_codegraph(), codegraph_exports));
|
|
2412
|
+
const project = coveringProject(cg, dir);
|
|
2413
|
+
if (!project) return;
|
|
2414
|
+
const answer = q.codeGraphAsk(text, { project, refresh: false, limit: PROMPT_HITS * 2 });
|
|
2415
|
+
if (!answer || !answer.hits.length) return;
|
|
2416
|
+
if (answer.mode === "lexical" && (answer.coverageStrong ?? 0) < STRONG_FLOOR && (answer.coverage ?? 0) < BROAD_FLOOR) return;
|
|
2417
|
+
const state = readState(dir, input.session_id ?? "default");
|
|
2418
|
+
const seen = new Set(state.injected);
|
|
2419
|
+
const pointer = (h) => `${h.path}:${h.line}`;
|
|
2420
|
+
const fresh = answer.hits.filter((h) => !seen.has(pointer(h))).slice(0, PROMPT_HITS);
|
|
2421
|
+
if (!fresh.length) return;
|
|
2422
|
+
const lines = fresh.map((h) => ` - ${h.name} (${h.symbolKind}) ${h.path}:${h.line}-${h.endLine}`);
|
|
2423
|
+
emit("UserPromptSubmit", `[enigma] Code graph - nodes this task touches (locators; read or ask for the source as needed):
|
|
2424
|
+
${lines.join("\n")}`);
|
|
2425
|
+
state.injected = [...state.injected, ...fresh.map(pointer)];
|
|
2426
|
+
writeState(dir, input.session_id ?? "default", state);
|
|
2427
|
+
}
|
|
2428
|
+
async function postEdit(input, dir) {
|
|
2429
|
+
const file = input.tool_input?.file_path;
|
|
2430
|
+
if (!file) return;
|
|
2431
|
+
const q = await Promise.resolve().then(() => (init_codegraph_query(), codegraph_query_exports));
|
|
2432
|
+
const cg = await Promise.resolve().then(() => (init_codegraph(), codegraph_exports));
|
|
2433
|
+
const project = coveringProject(cg, dir);
|
|
2434
|
+
if (!project) return;
|
|
2435
|
+
const state = readState(dir, input.session_id ?? "default");
|
|
2436
|
+
state.dirty = true;
|
|
2437
|
+
writeState(dir, input.session_id ?? "default", state);
|
|
2438
|
+
const rel = relative3(dir, resolve4(file)).split("\\").join("/");
|
|
2439
|
+
if (!rel || rel.startsWith("..")) return;
|
|
2440
|
+
const trace = q.codeGraphTrace(rel, { project, refresh: false, depth: 1 });
|
|
2441
|
+
if (!trace || !trace.hits.length) return;
|
|
2442
|
+
const shown = trace.hits.slice(0, BLAST_CAP);
|
|
2443
|
+
const more = trace.hits.length - shown.length;
|
|
2444
|
+
const lines = shown.map((h) => ` - ${h.name} (${h.kind}) ${h.path}:${h.line}`);
|
|
2445
|
+
emit("PostToolUse", `[enigma] Blast radius of ${basename2(rel)} - ${trace.hits.length} dependent(s):
|
|
2446
|
+
${lines.join("\n")}${more > 0 ? `
|
|
2447
|
+
... +${more} more` : ""}`);
|
|
2448
|
+
}
|
|
2449
|
+
async function stop(input, dir) {
|
|
2450
|
+
const sessionId = input.session_id ?? "default";
|
|
2451
|
+
const state = readState(dir, sessionId);
|
|
2452
|
+
if (!state.dirty) return;
|
|
2453
|
+
writeState(dir, sessionId, { ...state, dirty: false });
|
|
2454
|
+
const cg = await Promise.resolve().then(() => (init_codegraph(), codegraph_exports));
|
|
2455
|
+
try {
|
|
2456
|
+
const q = await Promise.resolve().then(() => (init_codegraph_query(), codegraph_query_exports));
|
|
2457
|
+
const project = cg.findProjectForCwd(dir);
|
|
2458
|
+
if (!project) throw new Error("not indexed yet");
|
|
2459
|
+
const fresh = q.codeGraphCheck(project);
|
|
2460
|
+
const map = q.codeGraphMap({ project, refresh: false });
|
|
2461
|
+
writeStatuslineSnapshot(projectRoot(cg, project, dir), map?.totals.symbols ?? 0, fresh?.stale ?? 0);
|
|
2462
|
+
} catch {
|
|
2463
|
+
}
|
|
2464
|
+
backgroundIndex(cg, dir);
|
|
2465
|
+
}
|
|
2466
|
+
async function runCodeGraphHook(event, payload) {
|
|
2467
|
+
try {
|
|
2468
|
+
const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2469
|
+
if (!readConfig2().config.codeGraph) return 0;
|
|
2470
|
+
const input = parsePayload(payload);
|
|
2471
|
+
const dir = projectDir(input);
|
|
2472
|
+
if (!existsSync6(dir)) return 0;
|
|
2473
|
+
if (event === "session-start") await sessionStart(dir);
|
|
2474
|
+
else if (event === "prompt") await prompt(input, dir);
|
|
2475
|
+
else if (event === "post-edit") await postEdit(input, dir);
|
|
2476
|
+
else if (event === "stop") await stop(input, dir);
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
return 0;
|
|
2480
|
+
}
|
|
2481
|
+
export {
|
|
2482
|
+
runCodeGraphHook
|
|
2483
|
+
};
|