@maind-dev/cli 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/build/index.js +3930 -58
- package/build/meta.json +1 -0
- package/package.json +20 -11
- package/build/auth.js +0 -76
- package/build/auth.js.map +0 -1
- package/build/ci-report.js +0 -146
- package/build/ci-report.js.map +0 -1
- package/build/ci-result.js +0 -145
- package/build/ci-result.js.map +0 -1
- package/build/context.js +0 -157
- package/build/context.js.map +0 -1
- package/build/git-context.js +0 -212
- package/build/git-context.js.map +0 -1
- package/build/git.js +0 -162
- package/build/git.js.map +0 -1
- package/build/index.js.map +0 -1
- package/build/mcp-client.js +0 -221
- package/build/mcp-client.js.map +0 -1
- package/build/render.js +0 -81
- package/build/render.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,52 +1,3893 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
|
|
3
|
+
// src/context.ts
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/auth.ts
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { homedir, platform } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
function configDir() {
|
|
12
|
+
const home = homedir();
|
|
13
|
+
const env = process.env;
|
|
14
|
+
if (platform() === "darwin") {
|
|
15
|
+
return join(home, "Library", "Application Support", "maind");
|
|
16
|
+
}
|
|
17
|
+
if (platform() === "win32") {
|
|
18
|
+
const appdata = env.APPDATA && env.APPDATA.length > 0 ? env.APPDATA : join(home, "AppData", "Roaming");
|
|
19
|
+
return join(appdata, "maind");
|
|
20
|
+
}
|
|
21
|
+
const xdg = env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0 ? env.XDG_CONFIG_HOME : join(home, ".config");
|
|
22
|
+
return join(xdg, "maind");
|
|
23
|
+
}
|
|
24
|
+
function readFileTrimmed(path) {
|
|
25
|
+
if (!existsSync(path)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(path, "utf8").trim();
|
|
28
|
+
return raw.length > 0 ? raw : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var MissingApiKeyError = class extends Error {
|
|
34
|
+
constructor() {
|
|
35
|
+
super(
|
|
36
|
+
"No API key found. Set MAIND_API_KEY (a read-scoped CI key from https://maind.dev/keys) or pair the maind bridge on this machine first."
|
|
37
|
+
);
|
|
38
|
+
this.name = "MissingApiKeyError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function resolveAuth() {
|
|
42
|
+
const fromEnv = process.env.MAIND_API_KEY?.trim();
|
|
43
|
+
const apiKey = fromEnv && fromEnv.length > 0 ? fromEnv : readFileTrimmed(join(configDir(), "api-key"));
|
|
44
|
+
if (!apiKey) throw new MissingApiKeyError();
|
|
45
|
+
const deviceId = readFileTrimmed(join(configDir(), "device-id"));
|
|
46
|
+
return { apiKey, deviceId, keyPrefix: keyPrefixOf(apiKey) };
|
|
47
|
+
}
|
|
48
|
+
function keyPrefixOf(apiKey) {
|
|
49
|
+
const m = /^(mnd_[0-9a-f]{8})_/i.exec(apiKey);
|
|
50
|
+
if (m) return m[1];
|
|
51
|
+
return apiKey.slice(0, 12);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/mcp-client.ts
|
|
55
|
+
var DEFAULT_MCP_URL = "https://mcp.maind.dev/mcp";
|
|
56
|
+
var CLIENT_FAMILY = "maind-cli";
|
|
57
|
+
var McpError = class extends Error {
|
|
58
|
+
constructor(message, code) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.name = "McpError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var McpClient = class {
|
|
65
|
+
mcpUrl;
|
|
66
|
+
apiKey;
|
|
67
|
+
deviceId;
|
|
68
|
+
clientVersion;
|
|
69
|
+
projectKey;
|
|
70
|
+
sessionId = null;
|
|
71
|
+
nextId = 1;
|
|
72
|
+
initialized = false;
|
|
73
|
+
constructor(cfg) {
|
|
74
|
+
this.apiKey = cfg.apiKey;
|
|
75
|
+
this.deviceId = cfg.deviceId;
|
|
76
|
+
this.clientVersion = cfg.clientVersion;
|
|
77
|
+
this.projectKey = cfg.projectKey ?? null;
|
|
78
|
+
this.mcpUrl = cfg.mcpUrl ?? process.env.MAIND_MCP_URL ?? DEFAULT_MCP_URL;
|
|
79
|
+
}
|
|
80
|
+
headers() {
|
|
81
|
+
const h = {
|
|
82
|
+
"content-type": "application/json",
|
|
83
|
+
accept: "application/json, text/event-stream",
|
|
84
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
85
|
+
"x-maind-client-family": CLIENT_FAMILY,
|
|
86
|
+
"x-maind-client-version": this.clientVersion
|
|
87
|
+
};
|
|
88
|
+
if (this.deviceId) h["x-maind-device-id"] = this.deviceId;
|
|
89
|
+
if (this.projectKey) h["x-maind-project-key"] = this.projectKey;
|
|
90
|
+
if (this.sessionId) h["mcp-session-id"] = this.sessionId;
|
|
91
|
+
return h;
|
|
92
|
+
}
|
|
93
|
+
/** Performs the MCP handshake: initialize, capture session-id, send initialized. */
|
|
94
|
+
async connect() {
|
|
95
|
+
if (this.initialized) return;
|
|
96
|
+
const res = await this.post({
|
|
97
|
+
jsonrpc: "2.0",
|
|
98
|
+
id: this.nextId++,
|
|
99
|
+
method: "initialize",
|
|
100
|
+
params: {
|
|
101
|
+
protocolVersion: "2025-06-18",
|
|
102
|
+
capabilities: {},
|
|
103
|
+
clientInfo: { name: CLIENT_FAMILY, version: this.clientVersion }
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (res.message.error) {
|
|
107
|
+
throw new McpError(`initialize failed: ${res.message.error.message}`, res.message.error.code);
|
|
108
|
+
}
|
|
109
|
+
if (res.sessionId) this.sessionId = res.sessionId;
|
|
110
|
+
await this.post({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
111
|
+
this.initialized = true;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calls a maind MCP tool and returns its decoded JSON payload. maind tools
|
|
115
|
+
* return their result as a JSON string inside content[0].text, so we parse it.
|
|
116
|
+
*/
|
|
117
|
+
async callTool(name, args) {
|
|
118
|
+
if (!this.initialized) await this.connect();
|
|
119
|
+
const res = await this.post({
|
|
120
|
+
jsonrpc: "2.0",
|
|
121
|
+
id: this.nextId++,
|
|
122
|
+
method: "tools/call",
|
|
123
|
+
params: { name, arguments: args }
|
|
124
|
+
});
|
|
125
|
+
if (res.message.error) {
|
|
126
|
+
throw new McpError(`tool ${name} failed: ${res.message.error.message}`, res.message.error.code);
|
|
127
|
+
}
|
|
128
|
+
const result = res.message.result;
|
|
129
|
+
const text = result?.content?.find((c) => c.type === "text" && typeof c.text === "string")?.text;
|
|
130
|
+
if (typeof text !== "string") {
|
|
131
|
+
throw new McpError(`tool ${name} returned no text content`);
|
|
132
|
+
}
|
|
133
|
+
const parsed = parseJsonLoose(text);
|
|
134
|
+
if (parsed === void 0) {
|
|
135
|
+
throw new McpError(`tool ${name} returned non-JSON text`);
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
/** POSTs a JSON-RPC frame and unpacks the JSON-or-SSE response. */
|
|
140
|
+
async post(frame) {
|
|
141
|
+
let response;
|
|
142
|
+
try {
|
|
143
|
+
response = await fetch(this.mcpUrl, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: this.headers(),
|
|
146
|
+
body: JSON.stringify(frame)
|
|
147
|
+
});
|
|
148
|
+
} catch (err2) {
|
|
149
|
+
throw new McpError(`network error contacting ${this.mcpUrl}: ${stringifyErr(err2)}`);
|
|
150
|
+
}
|
|
151
|
+
const sid = response.headers.get("mcp-session-id");
|
|
152
|
+
if (response.status === 401) {
|
|
153
|
+
const body = await response.text().catch(() => "");
|
|
154
|
+
throw new McpError(`auth rejected (401): ${body.slice(0, 200)}`, 401);
|
|
155
|
+
}
|
|
156
|
+
if (response.status === 202) {
|
|
157
|
+
return { message: { jsonrpc: "2.0", result: null }, sessionId: sid };
|
|
158
|
+
}
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const body = await response.text().catch(() => "");
|
|
161
|
+
throw new McpError(`upstream HTTP ${response.status}: ${body.slice(0, 300)}`, response.status);
|
|
162
|
+
}
|
|
163
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
164
|
+
const raw = contentType.startsWith("text/event-stream") ? await readSseMessage(response) : await response.text();
|
|
165
|
+
if (raw.trim().length === 0) {
|
|
166
|
+
return { message: { jsonrpc: "2.0", result: null }, sessionId: sid };
|
|
167
|
+
}
|
|
168
|
+
let message;
|
|
169
|
+
try {
|
|
170
|
+
message = JSON.parse(raw);
|
|
171
|
+
} catch {
|
|
172
|
+
throw new McpError(`could not parse JSON-RPC response: ${raw.slice(0, 200)}`);
|
|
173
|
+
}
|
|
174
|
+
return { message, sessionId: sid };
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
async function readSseMessage(response) {
|
|
178
|
+
if (!response.body) return "";
|
|
179
|
+
const reader = response.body.getReader();
|
|
180
|
+
const decoder = new TextDecoder();
|
|
181
|
+
let buffer = "";
|
|
182
|
+
let dataLines = [];
|
|
183
|
+
let lastPayload = "";
|
|
184
|
+
const flush = () => {
|
|
185
|
+
if (dataLines.length === 0) return;
|
|
186
|
+
const payload = dataLines.join("\n").trim();
|
|
187
|
+
dataLines = [];
|
|
188
|
+
if (payload.length > 0) lastPayload = payload;
|
|
189
|
+
};
|
|
190
|
+
while (true) {
|
|
191
|
+
const { value: value2, done } = await reader.read();
|
|
192
|
+
if (done) break;
|
|
193
|
+
buffer += decoder.decode(value2, { stream: true });
|
|
194
|
+
let nl;
|
|
195
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
196
|
+
const rawLine = buffer.slice(0, nl);
|
|
197
|
+
buffer = buffer.slice(nl + 1);
|
|
198
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
199
|
+
if (line === "") {
|
|
200
|
+
flush();
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (line.startsWith(":")) continue;
|
|
204
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^\s/, ""));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
flush();
|
|
208
|
+
return lastPayload;
|
|
209
|
+
}
|
|
210
|
+
function stringifyErr(err2) {
|
|
211
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
212
|
+
}
|
|
213
|
+
function parseJsonLoose(text) {
|
|
214
|
+
const trimmed = text.trim();
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(trimmed);
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
const firstObj = trimmed.indexOf("{");
|
|
220
|
+
const firstArr = trimmed.indexOf("[");
|
|
221
|
+
const candidates = [firstObj, firstArr].filter((i) => i >= 0);
|
|
222
|
+
if (candidates.length === 0) return void 0;
|
|
223
|
+
const start = Math.min(...candidates);
|
|
224
|
+
const open = trimmed[start];
|
|
225
|
+
const close = open === "{" ? "}" : "]";
|
|
226
|
+
const end = trimmed.lastIndexOf(close);
|
|
227
|
+
if (end <= start) return void 0;
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
230
|
+
} catch {
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/render.ts
|
|
236
|
+
function buildManifest(lessons, conventions, meta) {
|
|
237
|
+
return {
|
|
238
|
+
schema: "maind.context.manifest/v1",
|
|
239
|
+
generated_at: meta.generatedAt,
|
|
240
|
+
key_prefix: meta.keyPrefix,
|
|
241
|
+
run_ref: meta.runRef,
|
|
242
|
+
stack: meta.stack,
|
|
243
|
+
query: meta.query,
|
|
244
|
+
served: lessons.map((l) => ({
|
|
245
|
+
id: l.id,
|
|
246
|
+
version: typeof l.version === "number" ? l.version : null,
|
|
247
|
+
tier: l.tier ?? null,
|
|
248
|
+
last_validated_at: l.last_validated_at ?? null
|
|
249
|
+
})),
|
|
250
|
+
conventions: conventions.map((c) => c.id)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function renderContextMarkdown(lessons, conventions, meta) {
|
|
254
|
+
const lines = [];
|
|
255
|
+
lines.push("<!-- Generated by `maind context` \u2014 do not edit by hand. -->");
|
|
256
|
+
lines.push(`<!-- generated_at: ${meta.generatedAt} | key: ${meta.keyPrefix}${meta.runRef ? ` | run: ${meta.runRef}` : ""} -->`);
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("# maind context");
|
|
259
|
+
lines.push("");
|
|
260
|
+
lines.push(
|
|
261
|
+
"Vetted lessons and conventions for this stack, pulled from maind for the agent running in this CI job. Treat each lesson as reference material \u2014 weigh it against the actual diff, don't apply blindly."
|
|
262
|
+
);
|
|
263
|
+
lines.push("");
|
|
264
|
+
const stackLabel = meta.stack.length > 0 ? meta.stack.join(", ") : "(none)";
|
|
265
|
+
lines.push(`**Stack:** ${stackLabel} \xB7 **Lessons:** ${lessons.length} \xB7 **Conventions:** ${conventions.length}`);
|
|
266
|
+
lines.push("");
|
|
267
|
+
if (conventions.length > 0) {
|
|
268
|
+
lines.push("## Always-on conventions");
|
|
269
|
+
lines.push("");
|
|
270
|
+
for (const c of conventions) {
|
|
271
|
+
lines.push(`- **${c.title}** \`${c.id}\``);
|
|
272
|
+
}
|
|
273
|
+
lines.push("");
|
|
274
|
+
}
|
|
275
|
+
if (lessons.length === 0) {
|
|
276
|
+
lines.push("## Lessons");
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push("_No stack-relevant lessons were returned for this query._");
|
|
279
|
+
lines.push("");
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
lines.push("## Lessons");
|
|
283
|
+
lines.push("");
|
|
284
|
+
for (const l of lessons) {
|
|
285
|
+
lines.push(`### ${l.title}`);
|
|
286
|
+
const meta2 = [`\`${l.id}\``];
|
|
287
|
+
if (l.tier) meta2.push(l.tier);
|
|
288
|
+
if (typeof l.version === "number") meta2.push(`v${l.version}`);
|
|
289
|
+
if (l.last_validated_at) meta2.push(`validated ${l.last_validated_at}`);
|
|
290
|
+
lines.push(meta2.join(" \xB7 "));
|
|
291
|
+
lines.push("");
|
|
292
|
+
if (l.summary) {
|
|
293
|
+
lines.push(`> ${l.summary}`);
|
|
294
|
+
lines.push("");
|
|
295
|
+
}
|
|
296
|
+
if (l.body && l.body.trim().length > 0) {
|
|
297
|
+
lines.push(l.body.trim());
|
|
298
|
+
lines.push("");
|
|
299
|
+
}
|
|
300
|
+
lines.push("---");
|
|
301
|
+
lines.push("");
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/context.ts
|
|
307
|
+
function csvSplit(s) {
|
|
308
|
+
return s.split(",").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
309
|
+
}
|
|
310
|
+
function parseContextArgs(argv) {
|
|
311
|
+
const get = (name) => {
|
|
312
|
+
const i = argv.indexOf(name);
|
|
313
|
+
if (i === -1 || i + 1 >= argv.length) return void 0;
|
|
314
|
+
return argv[i + 1];
|
|
315
|
+
};
|
|
316
|
+
const stack = csvSplit(get("--stack") ?? "");
|
|
317
|
+
const platforms = csvSplit(get("--platforms") ?? "");
|
|
318
|
+
const tools = csvSplit(get("--tools") ?? "");
|
|
319
|
+
const languages = csvSplit(get("--languages") ?? "");
|
|
320
|
+
const budgetRaw = get("--budget");
|
|
321
|
+
const budgetParsed = budgetRaw ? Number.parseInt(budgetRaw, 10) : NaN;
|
|
322
|
+
const budget = Number.isFinite(budgetParsed) && budgetParsed > 0 ? Math.min(budgetParsed, 50) : 15;
|
|
323
|
+
return {
|
|
324
|
+
stack,
|
|
325
|
+
languages,
|
|
326
|
+
// --stack is a convenience that seeds the platforms filter; explicit --platforms wins/merges.
|
|
327
|
+
platforms: platforms.length > 0 ? platforms : stack,
|
|
328
|
+
tools,
|
|
329
|
+
query: get("--query") ?? null,
|
|
330
|
+
out: get("--out") ?? ".maind/context.md",
|
|
331
|
+
manifest: get("--manifest") ?? ".maind/manifest.json",
|
|
332
|
+
budget,
|
|
333
|
+
runRef: get("--run-ref") ?? process.env.GITHUB_RUN_ID ?? null,
|
|
334
|
+
mcpUrl: get("--mcp-url") ?? null,
|
|
335
|
+
clientVersion: "0.1.0"
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function nowIso() {
|
|
339
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
340
|
+
}
|
|
341
|
+
function writeArtifact(path, content) {
|
|
342
|
+
const dir = dirname(path);
|
|
343
|
+
if (dir && dir !== ".") mkdirSync(dir, { recursive: true });
|
|
344
|
+
writeFileSync(path, content, "utf8");
|
|
345
|
+
}
|
|
346
|
+
async function runContext(opts) {
|
|
347
|
+
const query = opts.query ?? deriveQuery(opts);
|
|
348
|
+
const meta = {
|
|
349
|
+
stack: opts.stack,
|
|
350
|
+
languages: opts.languages,
|
|
351
|
+
platforms: opts.platforms,
|
|
352
|
+
tools: opts.tools,
|
|
353
|
+
query,
|
|
354
|
+
budget: opts.budget,
|
|
355
|
+
generatedAt: nowIso(),
|
|
356
|
+
keyPrefix: "unknown",
|
|
357
|
+
runRef: opts.runRef,
|
|
358
|
+
mcpUrl: opts.mcpUrl ?? process.env.MAIND_MCP_URL ?? "https://mcp.maind.dev/mcp"
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
const auth = resolveAuth();
|
|
362
|
+
meta.keyPrefix = auth.keyPrefix;
|
|
363
|
+
const client = new McpClient({
|
|
364
|
+
apiKey: auth.apiKey,
|
|
365
|
+
deviceId: auth.deviceId,
|
|
366
|
+
clientVersion: opts.clientVersion,
|
|
367
|
+
mcpUrl: opts.mcpUrl ?? void 0
|
|
368
|
+
});
|
|
369
|
+
const search = await client.callTool("search_lessons", {
|
|
370
|
+
query,
|
|
371
|
+
platforms: opts.platforms,
|
|
372
|
+
languages: opts.languages,
|
|
373
|
+
tools: opts.tools,
|
|
374
|
+
limit: Math.min(opts.budget, 10)
|
|
375
|
+
// search_lessons caps limit at 10 server-side
|
|
376
|
+
});
|
|
377
|
+
const lessons = Array.isArray(search.lessons) ? search.lessons : [];
|
|
378
|
+
let conventions = [];
|
|
379
|
+
try {
|
|
380
|
+
const briefing = await client.callTool("get_session_briefing", {
|
|
381
|
+
tools: opts.tools.length > 0 ? opts.tools : ["claude-code"],
|
|
382
|
+
languages: opts.languages,
|
|
383
|
+
platforms: opts.platforms
|
|
384
|
+
});
|
|
385
|
+
if (Array.isArray(briefing.convention_index)) conventions = briefing.convention_index;
|
|
386
|
+
} catch (err2) {
|
|
387
|
+
warn(`conventions unavailable (continuing with lessons only): ${msg(err2)}`);
|
|
388
|
+
}
|
|
389
|
+
writeArtifact(opts.out, renderContextMarkdown(lessons, conventions, meta));
|
|
390
|
+
writeArtifact(opts.manifest, JSON.stringify(buildManifest(lessons, conventions, meta), null, 2) + "\n");
|
|
391
|
+
process.stderr.write(
|
|
392
|
+
`maind context: wrote ${lessons.length} lessons + ${conventions.length} conventions \u2192 ${opts.out} (manifest: ${opts.manifest})
|
|
393
|
+
`
|
|
394
|
+
);
|
|
395
|
+
return 0;
|
|
396
|
+
} catch (err2) {
|
|
397
|
+
const reason = err2 instanceof MissingApiKeyError ? err2.message : msg(err2);
|
|
398
|
+
warn(`context pull failed, continuing without maind context: ${reason}`);
|
|
399
|
+
const emptyManifest = buildManifest([], [], meta);
|
|
400
|
+
emptyManifest.error = reason;
|
|
401
|
+
try {
|
|
402
|
+
writeArtifact(opts.out, failOpenMarkdown(meta, reason));
|
|
403
|
+
writeArtifact(opts.manifest, JSON.stringify(emptyManifest, null, 2) + "\n");
|
|
404
|
+
} catch (writeErr) {
|
|
405
|
+
warn(`could not write fail-open artifacts: ${msg(writeErr)}`);
|
|
406
|
+
return 1;
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function deriveQuery(opts) {
|
|
412
|
+
const parts = [...opts.stack, ...opts.tools, ...opts.languages];
|
|
413
|
+
return parts.length > 0 ? parts.join(" ") : "common build and CI pitfalls";
|
|
414
|
+
}
|
|
415
|
+
function failOpenMarkdown(meta, reason) {
|
|
416
|
+
return [
|
|
417
|
+
"<!-- Generated by `maind context` (fail-open path). -->",
|
|
418
|
+
`<!-- generated_at: ${meta.generatedAt} -->`,
|
|
419
|
+
"",
|
|
420
|
+
"# maind context",
|
|
421
|
+
"",
|
|
422
|
+
`_maind context was unavailable for this run: ${reason}_`,
|
|
423
|
+
"",
|
|
424
|
+
"The build can proceed normally \u2014 this file is informational only.",
|
|
425
|
+
""
|
|
426
|
+
].join("\n");
|
|
427
|
+
}
|
|
428
|
+
function msg(err2) {
|
|
429
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
430
|
+
}
|
|
431
|
+
function warn(line) {
|
|
432
|
+
process.stderr.write(`maind context: ${line}
|
|
433
|
+
`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/ci-report.ts
|
|
437
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
438
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
439
|
+
|
|
440
|
+
// src/git.ts
|
|
441
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
442
|
+
|
|
443
|
+
// ../../packages/project-key/build/index.js
|
|
444
|
+
import { execFileSync } from "node:child_process";
|
|
445
|
+
import { createHash } from "node:crypto";
|
|
446
|
+
function sha256Hex(input) {
|
|
447
|
+
return createHash("sha256").update(input).digest("hex");
|
|
448
|
+
}
|
|
449
|
+
function normalizeRemoteUrl(raw) {
|
|
450
|
+
let s = raw.trim();
|
|
451
|
+
if (!s)
|
|
452
|
+
return null;
|
|
453
|
+
const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(s);
|
|
454
|
+
if (scheme) {
|
|
455
|
+
s = s.slice(scheme[0].length);
|
|
456
|
+
s = s.replace(/^[^@/]+@/, "");
|
|
457
|
+
s = s.replace(/^([^/:]+):\d+(?=\/)/, "$1");
|
|
458
|
+
} else {
|
|
459
|
+
s = s.replace(/^[^@/]+@/, "");
|
|
460
|
+
const i = s.indexOf(":");
|
|
461
|
+
if (i >= 0)
|
|
462
|
+
s = `${s.slice(0, i)}/${s.slice(i + 1)}`;
|
|
463
|
+
}
|
|
464
|
+
s = s.replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
|
|
465
|
+
return s.length > 0 ? s : null;
|
|
466
|
+
}
|
|
467
|
+
function projectKeyFromRemote(raw) {
|
|
468
|
+
const norm = normalizeRemoteUrl(raw);
|
|
469
|
+
return norm ? sha256Hex(norm) : null;
|
|
470
|
+
}
|
|
471
|
+
var PROJECT_KEY_RE = /^[a-f0-9]{64}$/;
|
|
472
|
+
function isProjectKey(value2) {
|
|
473
|
+
return typeof value2 === "string" && PROJECT_KEY_RE.test(value2);
|
|
474
|
+
}
|
|
475
|
+
function computeProjectKey(cwd = process.cwd()) {
|
|
476
|
+
const override = process.env.MAIND_PROJECT_KEY?.trim().toLowerCase();
|
|
477
|
+
if (isProjectKey(override))
|
|
478
|
+
return override;
|
|
479
|
+
let remote;
|
|
480
|
+
try {
|
|
481
|
+
remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
482
|
+
cwd,
|
|
483
|
+
encoding: "utf8",
|
|
484
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
485
|
+
timeout: 5e3
|
|
486
|
+
}).trim();
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
if (!remote)
|
|
491
|
+
return null;
|
|
492
|
+
return projectKeyFromRemote(remote);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/git.ts
|
|
496
|
+
function run(bin, args) {
|
|
497
|
+
try {
|
|
498
|
+
return execFileSync2(bin, args, {
|
|
499
|
+
encoding: "utf8",
|
|
500
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
501
|
+
timeout: 5e3
|
|
502
|
+
}).trim();
|
|
503
|
+
} catch {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
var git = (args) => run("git", args);
|
|
508
|
+
var gh = (args) => run("gh", args);
|
|
509
|
+
function resolveDefaultBranch() {
|
|
510
|
+
const sym = git(["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
|
|
511
|
+
if (sym) {
|
|
512
|
+
const m = /refs\/remotes\/origin\/(.+)$/.exec(sym);
|
|
513
|
+
if (m) return m[1];
|
|
514
|
+
}
|
|
515
|
+
for (const cand of ["main", "master"]) {
|
|
516
|
+
if (git(["rev-parse", "--verify", "--quiet", `origin/${cand}`]) !== null) return cand;
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
function gatherGitState() {
|
|
521
|
+
const inside = git(["rev-parse", "--is-inside-work-tree"]);
|
|
522
|
+
if (inside !== "true") {
|
|
523
|
+
return {
|
|
524
|
+
isRepo: false,
|
|
525
|
+
projectKey: null,
|
|
526
|
+
branch: null,
|
|
527
|
+
defaultBranch: null,
|
|
528
|
+
ahead: null,
|
|
529
|
+
behind: null,
|
|
530
|
+
uncommitted: 0,
|
|
531
|
+
workingPaths: []
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const remote = git(["config", "--get", "remote.origin.url"]);
|
|
535
|
+
const projectKey = remote ? projectKeyFromRemote(remote) : null;
|
|
536
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
537
|
+
const defaultBranch = resolveDefaultBranch();
|
|
538
|
+
let ahead = null;
|
|
539
|
+
let behind = null;
|
|
540
|
+
if (defaultBranch) {
|
|
541
|
+
const counts = git(["rev-list", "--left-right", "--count", `origin/${defaultBranch}...HEAD`]);
|
|
542
|
+
if (counts) {
|
|
543
|
+
const [b, a] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10));
|
|
544
|
+
if (Number.isFinite(b)) behind = b;
|
|
545
|
+
if (Number.isFinite(a)) ahead = a;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const porcelain = git(["status", "--porcelain"]) ?? "";
|
|
549
|
+
const changedLines = porcelain.split("\n").filter((l) => l.trim().length > 0);
|
|
550
|
+
const workingPaths = changedLines.map((l) => l.slice(3).split(" -> ").pop().trim().replace(/^"|"$/g, "")).filter((p) => p.length > 0).slice(0, 32);
|
|
551
|
+
return {
|
|
552
|
+
isRepo: true,
|
|
553
|
+
projectKey,
|
|
554
|
+
branch,
|
|
555
|
+
defaultBranch,
|
|
556
|
+
ahead,
|
|
557
|
+
behind,
|
|
558
|
+
uncommitted: changedLines.length,
|
|
559
|
+
workingPaths
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function ciFromChecks(prNumber) {
|
|
563
|
+
const out = gh(["pr", "checks", String(prNumber), "--json", "bucket"]);
|
|
564
|
+
if (!out) return "unknown";
|
|
565
|
+
try {
|
|
566
|
+
const rows = JSON.parse(out);
|
|
567
|
+
if (rows.length === 0) return "unknown";
|
|
568
|
+
if (rows.some((r) => r.bucket === "fail")) return "failure";
|
|
569
|
+
if (rows.some((r) => r.bucket === "pending")) return "pending";
|
|
570
|
+
if (rows.every((r) => r.bucket === "pass" || r.bucket === "skipping")) return "success";
|
|
571
|
+
return "unknown";
|
|
572
|
+
} catch {
|
|
573
|
+
return "unknown";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function gatherOpenPrs(currentBranch, max = 5) {
|
|
577
|
+
const out = gh(["pr", "list", "--state", "open", "--limit", String(max), "--json", "number,title,headRefName"]);
|
|
578
|
+
if (out === null) return { available: false, prs: [] };
|
|
579
|
+
try {
|
|
580
|
+
const rows = JSON.parse(out);
|
|
581
|
+
const prs = rows.map((r) => ({
|
|
582
|
+
number: r.number,
|
|
583
|
+
title: r.title,
|
|
584
|
+
isCurrentBranch: currentBranch != null && r.headRefName === currentBranch,
|
|
585
|
+
// Only resolve CI for the current branch's PR (cheap) — others stay unknown to avoid N gh calls.
|
|
586
|
+
ci: currentBranch != null && r.headRefName === currentBranch ? ciFromChecks(r.number) : "unknown"
|
|
587
|
+
}));
|
|
588
|
+
return { available: true, prs };
|
|
589
|
+
} catch {
|
|
590
|
+
return { available: false, prs: [] };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/ci-report.ts
|
|
595
|
+
var DEFAULT_REPORT_URL = "https://app.maind.dev/api/ci/report";
|
|
596
|
+
var PROJECT_KEY_RE2 = /^[a-f0-9]{64}$/;
|
|
597
|
+
function parseCiReportArgs(argv) {
|
|
598
|
+
const get = (name) => {
|
|
599
|
+
const i = argv.indexOf(name);
|
|
600
|
+
if (i === -1 || i + 1 >= argv.length) return void 0;
|
|
601
|
+
return argv[i + 1];
|
|
602
|
+
};
|
|
603
|
+
return {
|
|
604
|
+
lessonPath: get("--lesson") ?? null,
|
|
605
|
+
rationale: get("--rationale") ?? null,
|
|
606
|
+
failureType: get("--failure-type") ?? null,
|
|
607
|
+
signature: get("--signature") ?? null,
|
|
608
|
+
summary: get("--summary") ?? null,
|
|
609
|
+
provider: get("--provider") ?? "github-actions",
|
|
610
|
+
logsPath: get("--logs") ?? null,
|
|
611
|
+
remediation: get("--remediation") ?? null,
|
|
612
|
+
reportUrl: get("--report-url") ?? process.env.MAIND_REPORT_URL ?? DEFAULT_REPORT_URL,
|
|
613
|
+
projectKey: get("--project-key") ?? null
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
function ciEnv() {
|
|
617
|
+
const env = process.env;
|
|
618
|
+
const runId = env.GITHUB_RUN_ID;
|
|
619
|
+
const repo = env.GITHUB_REPOSITORY;
|
|
620
|
+
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
621
|
+
return {
|
|
622
|
+
workflow_name: env.GITHUB_WORKFLOW || void 0,
|
|
623
|
+
run_id: runId || void 0,
|
|
624
|
+
run_url: runId && repo ? `${server}/${repo}/actions/runs/${runId}` : void 0
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function sha256(s) {
|
|
628
|
+
return createHash2("sha256").update(s, "utf8").digest("hex");
|
|
629
|
+
}
|
|
630
|
+
async function runCiReport(opts) {
|
|
631
|
+
try {
|
|
632
|
+
if (!opts.lessonPath) {
|
|
633
|
+
warn2("missing --lesson <path-to-authored-lesson.md>; nothing to report.");
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
if (!opts.rationale) {
|
|
637
|
+
warn2("missing --rationale; the agent should explain why this is a lesson.");
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
if (!opts.failureType) {
|
|
641
|
+
warn2("missing --failure-type (e.g. tsc-error, test-failure).");
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
644
|
+
const lesson = readFileSync2(opts.lessonPath, "utf8");
|
|
645
|
+
if (lesson.trim().length < 50) {
|
|
646
|
+
warn2("authored lesson is too short \u2014 skipping report.");
|
|
647
|
+
return 0;
|
|
648
|
+
}
|
|
649
|
+
const env = ciEnv();
|
|
650
|
+
const runId = env.run_id ?? "local";
|
|
651
|
+
const signature = opts.signature ?? `${opts.failureType}:${sha256(opts.failureType + lesson).slice(0, 16)}`;
|
|
652
|
+
const provenance = {
|
|
653
|
+
ci_provider: opts.provider,
|
|
654
|
+
run_id: runId,
|
|
655
|
+
failure_type: opts.failureType,
|
|
656
|
+
failure_signature: signature,
|
|
657
|
+
draft_rationale: opts.rationale
|
|
658
|
+
};
|
|
659
|
+
if (env.workflow_name) provenance.workflow_name = env.workflow_name;
|
|
660
|
+
if (env.run_url) provenance.run_url = env.run_url;
|
|
661
|
+
if (opts.summary) provenance.failure_summary = opts.summary;
|
|
662
|
+
if (opts.remediation) provenance.remediation_hint = opts.remediation;
|
|
663
|
+
if (opts.logsPath) {
|
|
664
|
+
try {
|
|
665
|
+
provenance.failure_logs = readFileSync2(opts.logsPath, "utf8").slice(0, 8e3);
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const overrideKey = opts.projectKey?.trim().toLowerCase();
|
|
670
|
+
const projectKey = overrideKey && PROJECT_KEY_RE2.test(overrideKey) ? overrideKey : gatherGitState().projectKey;
|
|
671
|
+
const auth = resolveAuth();
|
|
672
|
+
const body = JSON.stringify({
|
|
673
|
+
lesson,
|
|
674
|
+
content_hash: sha256(lesson),
|
|
675
|
+
ci_provenance: provenance,
|
|
676
|
+
...projectKey ? { project_key: projectKey } : {}
|
|
677
|
+
});
|
|
678
|
+
let res;
|
|
679
|
+
try {
|
|
680
|
+
res = await fetch(opts.reportUrl, {
|
|
681
|
+
method: "POST",
|
|
682
|
+
headers: {
|
|
683
|
+
"content-type": "application/json",
|
|
684
|
+
authorization: `Bearer ${auth.apiKey}`
|
|
685
|
+
},
|
|
686
|
+
body
|
|
687
|
+
});
|
|
688
|
+
} catch (err2) {
|
|
689
|
+
warn2(`network error posting to ${opts.reportUrl}: ${msg2(err2)}`);
|
|
690
|
+
return 0;
|
|
691
|
+
}
|
|
692
|
+
const text = await res.text().catch(() => "");
|
|
693
|
+
if (!res.ok) {
|
|
694
|
+
warn2(`ingest rejected (HTTP ${res.status}): ${text.slice(0, 300)}`);
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
697
|
+
let parsed = {};
|
|
698
|
+
try {
|
|
699
|
+
parsed = JSON.parse(text);
|
|
700
|
+
} catch {
|
|
701
|
+
}
|
|
702
|
+
const scope = parsed.routing_decision === "org_private" ? parsed.repo_scoped ? " [org-private, repo-scoped]" : " [org-private]" : "";
|
|
703
|
+
process.stderr.write(
|
|
704
|
+
`maind ci-report: ${parsed.deduplicated ? "deduplicated" : "filed"} draft ${parsed.draft_id ?? "?"}${scope}` + (parsed.review_url ? ` \u2192 ${parsed.review_url}` : "") + "\n"
|
|
705
|
+
);
|
|
706
|
+
return 0;
|
|
707
|
+
} catch (err2) {
|
|
708
|
+
const reason = err2 instanceof MissingApiKeyError ? err2.message : msg2(err2);
|
|
709
|
+
warn2(`report failed, continuing: ${reason}`);
|
|
710
|
+
return 0;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function msg2(err2) {
|
|
714
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
715
|
+
}
|
|
716
|
+
function warn2(line) {
|
|
717
|
+
process.stderr.write(`maind ci-report: ${line}
|
|
718
|
+
`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/ci-result.ts
|
|
722
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
723
|
+
var DEFAULT_RESULT_URL = "https://app.maind.dev/api/ci/result";
|
|
724
|
+
function parseCiResultArgs(argv) {
|
|
725
|
+
const get = (name) => {
|
|
726
|
+
const i = argv.indexOf(name);
|
|
727
|
+
if (i === -1 || i + 1 >= argv.length) return void 0;
|
|
728
|
+
return argv[i + 1];
|
|
729
|
+
};
|
|
730
|
+
return {
|
|
731
|
+
outcome: get("--outcome") ?? null,
|
|
732
|
+
manifestPath: get("--manifest") ?? ".maind/manifest.json",
|
|
733
|
+
runRef: get("--run-ref") ?? null,
|
|
734
|
+
runUrl: get("--run-url") ?? null,
|
|
735
|
+
resultUrl: get("--result-url") ?? process.env.MAIND_RESULT_URL ?? DEFAULT_RESULT_URL
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function normalizeOutcome(raw) {
|
|
739
|
+
const s = raw.trim().toLowerCase();
|
|
740
|
+
if (["success", "passed", "pass", "passing", "ok", "green", "0"].includes(s)) return "success";
|
|
741
|
+
if (["failure", "failed", "fail", "failing", "error", "red", "cancelled", "canceled"].includes(s)) {
|
|
742
|
+
return "failure";
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
function ciEnv2() {
|
|
747
|
+
const env = process.env;
|
|
748
|
+
const runId = env.GITHUB_RUN_ID;
|
|
749
|
+
const repo = env.GITHUB_REPOSITORY;
|
|
750
|
+
const server = env.GITHUB_SERVER_URL ?? "https://github.com";
|
|
751
|
+
return {
|
|
752
|
+
run_id: runId || void 0,
|
|
753
|
+
run_url: runId && repo ? `${server}/${repo}/actions/runs/${runId}` : void 0
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
async function runCiResult(opts) {
|
|
757
|
+
try {
|
|
758
|
+
if (!opts.outcome) {
|
|
759
|
+
warn3("missing --outcome <success|failure> (e.g. --outcome ${{ job.status }}).");
|
|
760
|
+
return 0;
|
|
761
|
+
}
|
|
762
|
+
const outcome = normalizeOutcome(opts.outcome);
|
|
763
|
+
if (!outcome) {
|
|
764
|
+
warn3(`unrecognized --outcome '${opts.outcome}'; expected success|failure.`);
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
767
|
+
let manifestRaw;
|
|
768
|
+
try {
|
|
769
|
+
manifestRaw = readFileSync3(opts.manifestPath, "utf8");
|
|
770
|
+
} catch {
|
|
771
|
+
warn3(`no manifest at ${opts.manifestPath} (did 'maind context' run first?); nothing to attribute.`);
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
774
|
+
let manifest;
|
|
775
|
+
try {
|
|
776
|
+
manifest = JSON.parse(manifestRaw);
|
|
777
|
+
} catch {
|
|
778
|
+
warn3(`manifest at ${opts.manifestPath} is not valid JSON; skipping.`);
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
const lessonIds = Array.isArray(manifest.served) ? manifest.served.map((s) => s.id).filter((id) => typeof id === "string" && id.length > 0) : [];
|
|
782
|
+
if (lessonIds.length === 0) {
|
|
783
|
+
warn3("manifest lists no served lessons; nothing to report.");
|
|
784
|
+
return 0;
|
|
785
|
+
}
|
|
786
|
+
const env = ciEnv2();
|
|
787
|
+
const runRef = opts.runRef ?? manifest.run_ref ?? env.run_id ?? null;
|
|
788
|
+
if (!runRef) {
|
|
789
|
+
warn3("no run ref (--run-ref / manifest.run_ref / $GITHUB_RUN_ID); cannot attribute an outcome.");
|
|
790
|
+
return 0;
|
|
791
|
+
}
|
|
792
|
+
const runUrl = opts.runUrl ?? env.run_url ?? null;
|
|
793
|
+
const auth = resolveAuth();
|
|
794
|
+
const body = JSON.stringify({
|
|
795
|
+
outcome,
|
|
796
|
+
run_ref: runRef,
|
|
797
|
+
run_url: runUrl,
|
|
798
|
+
lesson_ids: lessonIds
|
|
799
|
+
});
|
|
800
|
+
let res;
|
|
801
|
+
try {
|
|
802
|
+
res = await fetch(opts.resultUrl, {
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: {
|
|
805
|
+
"content-type": "application/json",
|
|
806
|
+
authorization: `Bearer ${auth.apiKey}`
|
|
807
|
+
},
|
|
808
|
+
body
|
|
809
|
+
});
|
|
810
|
+
} catch (err2) {
|
|
811
|
+
warn3(`network error posting to ${opts.resultUrl}: ${msg3(err2)}`);
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
const text = await res.text().catch(() => "");
|
|
815
|
+
if (!res.ok) {
|
|
816
|
+
warn3(`ingest rejected (HTTP ${res.status}): ${text.slice(0, 300)}`);
|
|
817
|
+
return 0;
|
|
818
|
+
}
|
|
819
|
+
let parsed = {};
|
|
820
|
+
try {
|
|
821
|
+
parsed = JSON.parse(text);
|
|
822
|
+
} catch {
|
|
823
|
+
}
|
|
824
|
+
process.stderr.write(
|
|
825
|
+
`maind ci-result: ${outcome} \xB7 recorded ${parsed.recorded ?? "?"} \xB7 skipped ${parsed.skipped ?? 0} (run ${runRef})
|
|
826
|
+
`
|
|
827
|
+
);
|
|
828
|
+
return 0;
|
|
829
|
+
} catch (err2) {
|
|
830
|
+
const reason = err2 instanceof MissingApiKeyError ? err2.message : msg3(err2);
|
|
831
|
+
warn3(`result failed, continuing: ${reason}`);
|
|
832
|
+
return 0;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function msg3(err2) {
|
|
836
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
837
|
+
}
|
|
838
|
+
function warn3(line) {
|
|
839
|
+
process.stderr.write(`maind ci-result: ${line}
|
|
840
|
+
`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// src/git-context.ts
|
|
844
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
845
|
+
import { dirname as dirname2 } from "node:path";
|
|
846
|
+
function parseGitContextArgs(argv) {
|
|
847
|
+
const get = (name) => {
|
|
848
|
+
const i = argv.indexOf(name);
|
|
849
|
+
if (i === -1 || i + 1 >= argv.length) return void 0;
|
|
850
|
+
return argv[i + 1];
|
|
851
|
+
};
|
|
852
|
+
return {
|
|
853
|
+
out: get("--out") ?? ".maind/git-context.md",
|
|
854
|
+
manifest: get("--manifest") ?? null,
|
|
855
|
+
mcpUrl: get("--mcp-url") ?? null,
|
|
856
|
+
noMcp: argv.includes("--no-mcp"),
|
|
857
|
+
clientVersion: "0.4.0"
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function nowIso2() {
|
|
861
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
862
|
+
}
|
|
863
|
+
function writeArtifact2(path, content) {
|
|
864
|
+
const dir = dirname2(path);
|
|
865
|
+
if (dir && dir !== ".") mkdirSync2(dir, { recursive: true });
|
|
866
|
+
writeFileSync2(path, content, "utf8");
|
|
867
|
+
}
|
|
868
|
+
function stateQuery(git3, prs) {
|
|
869
|
+
if (prs.some((p) => p.isCurrentBranch && p.ci === "failure")) return "ci failure red build fix";
|
|
870
|
+
if ((git3.behind ?? 0) > 0 && (git3.ahead ?? 0) > 0) return "rebase merge integration diverged branch";
|
|
871
|
+
if ((git3.behind ?? 0) > 0) return "branch behind main pull rebase update";
|
|
872
|
+
if (prs.some((p) => p.isCurrentBranch)) return "pull request review merge workflow";
|
|
873
|
+
if ((git3.ahead ?? 0) > 0) return "open pull request commit message branch";
|
|
874
|
+
return "git branch commit pull request workflow";
|
|
875
|
+
}
|
|
876
|
+
async function runGitContext(opts) {
|
|
877
|
+
const git3 = gatherGitState();
|
|
878
|
+
const prResult = git3.isRepo ? gatherOpenPrs(git3.branch) : { available: false, prs: [] };
|
|
879
|
+
let peer = null;
|
|
880
|
+
let lessons = [];
|
|
881
|
+
let lessonsAvailable = false;
|
|
882
|
+
if (!opts.noMcp && git3.isRepo) {
|
|
883
|
+
try {
|
|
884
|
+
const auth = resolveAuth();
|
|
885
|
+
const client = new McpClient({
|
|
886
|
+
apiKey: auth.apiKey,
|
|
887
|
+
deviceId: auth.deviceId,
|
|
888
|
+
clientVersion: opts.clientVersion,
|
|
889
|
+
mcpUrl: opts.mcpUrl ?? void 0,
|
|
890
|
+
// ADR-212 B2: scope repo-specific content to this repo (sha256 only).
|
|
891
|
+
projectKey: git3.projectKey
|
|
892
|
+
});
|
|
893
|
+
if (git3.projectKey && git3.workingPaths.length > 0) {
|
|
894
|
+
try {
|
|
895
|
+
const c = await client.callTool("check_path_conflicts", {
|
|
896
|
+
project_key: git3.projectKey,
|
|
897
|
+
paths: git3.workingPaths
|
|
898
|
+
});
|
|
899
|
+
peer = {
|
|
900
|
+
peersActive: c.peers_active ?? 0,
|
|
901
|
+
conflicts: (c.conflicts ?? []).filter((x) => x.path).map((x) => ({ path: x.path, focus: x.focus })),
|
|
902
|
+
recommendation: c.recommendation ?? null,
|
|
903
|
+
checked: true
|
|
904
|
+
};
|
|
905
|
+
} catch (err2) {
|
|
906
|
+
warn4(`peer check unavailable: ${msg4(err2)}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
try {
|
|
910
|
+
const res = await client.callTool("search_lessons", {
|
|
911
|
+
query: stateQuery(git3, prResult.prs),
|
|
912
|
+
platforms: ["git", "github"],
|
|
913
|
+
limit: 3
|
|
914
|
+
});
|
|
915
|
+
lessons = Array.isArray(res.lessons) ? res.lessons : [];
|
|
916
|
+
lessonsAvailable = true;
|
|
917
|
+
} catch (err2) {
|
|
918
|
+
warn4(`git lessons unavailable: ${msg4(err2)}`);
|
|
919
|
+
}
|
|
920
|
+
} catch (err2) {
|
|
921
|
+
warn4(`maind enrichment skipped (${msg4(err2)}); rendering local git state only.`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
writeArtifact2(opts.out, renderMarkdown(git3, prResult, peer, lessons, lessonsAvailable));
|
|
925
|
+
if (opts.manifest) {
|
|
926
|
+
writeArtifact2(opts.manifest, JSON.stringify(buildManifest2(git3, prResult.prs, peer), null, 2) + "\n");
|
|
927
|
+
}
|
|
928
|
+
process.stderr.write(
|
|
929
|
+
`maind git-context: ${git3.isRepo ? `${git3.branch ?? "?"} (+${git3.ahead ?? "?"}/-${git3.behind ?? "?"})` : "not a git repo"}` + (peer?.conflicts.length ? `, ${peer.conflicts.length} peer conflict(s)` : "") + ` \u2192 ${opts.out}
|
|
930
|
+
`
|
|
931
|
+
);
|
|
932
|
+
return 0;
|
|
933
|
+
}
|
|
934
|
+
function ciIcon(ci) {
|
|
935
|
+
return ci === "success" ? "\u{1F7E2}" : ci === "failure" ? "\u{1F534}" : ci === "pending" ? "\u{1F7E1}" : "\u26AA";
|
|
936
|
+
}
|
|
937
|
+
function renderMarkdown(git3, prResult, peer, lessons, lessonsAvailable) {
|
|
938
|
+
const L = [];
|
|
939
|
+
L.push("<!-- Generated by `maind git-context` \u2014 local git state for the agent. -->");
|
|
940
|
+
L.push(`<!-- generated_at: ${nowIso2()} -->`);
|
|
941
|
+
L.push("");
|
|
942
|
+
L.push("# maind git-context");
|
|
943
|
+
L.push("");
|
|
944
|
+
if (!git3.isRepo) {
|
|
945
|
+
L.push("_Not inside a git work tree \u2014 no git context to report._");
|
|
946
|
+
L.push("");
|
|
947
|
+
return L.join("\n");
|
|
948
|
+
}
|
|
949
|
+
const aheadBehind = git3.defaultBranch != null ? `${git3.ahead ?? "?"} ahead, ${git3.behind ?? "?"} behind \`${git3.defaultBranch}\`` : "no upstream default branch resolved";
|
|
950
|
+
L.push(`**Branch:** \`${git3.branch ?? "?"}\` \u2014 ${aheadBehind}`);
|
|
951
|
+
L.push(`**Working tree:** ${git3.uncommitted} uncommitted change${git3.uncommitted === 1 ? "" : "s"}`);
|
|
952
|
+
if (git3.projectKey) L.push(`**Project:** \`${git3.projectKey.slice(0, 12)}\u2026\` (sha256 of remote \u2014 URL never sent)`);
|
|
953
|
+
L.push("");
|
|
954
|
+
if (prResult.available) {
|
|
955
|
+
if (prResult.prs.length === 0) {
|
|
956
|
+
L.push("**Open PRs:** none");
|
|
957
|
+
} else {
|
|
958
|
+
L.push("**Open PRs:**");
|
|
959
|
+
for (const p of prResult.prs) {
|
|
960
|
+
const tag = p.isCurrentBranch ? ` ${ciIcon(p.ci)} CI:${p.ci} \xB7 this branch` : "";
|
|
961
|
+
L.push(`- #${p.number} ${p.title}${tag}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
} else {
|
|
965
|
+
L.push("**Open PRs:** _unavailable (`gh` not installed / not authenticated)_");
|
|
966
|
+
}
|
|
967
|
+
L.push("");
|
|
968
|
+
if (peer?.checked) {
|
|
969
|
+
if (peer.conflicts.length > 0) {
|
|
970
|
+
L.push(`**\u26A0 Parallel sessions on this repo:** ${peer.peersActive} active \u2014 path conflicts:`);
|
|
971
|
+
for (const c of peer.conflicts) {
|
|
972
|
+
L.push(`- \`${c.path}\`${c.focus ? ` \u2014 peer focus: ${c.focus}` : ""}`);
|
|
973
|
+
}
|
|
974
|
+
if (peer.recommendation) L.push(`- _${peer.recommendation}_`);
|
|
975
|
+
} else if (peer.peersActive > 0) {
|
|
976
|
+
L.push(`**Parallel sessions on this repo:** ${peer.peersActive} active \u2014 no conflict on your changed paths.`);
|
|
977
|
+
} else {
|
|
978
|
+
L.push("**Parallel sessions on this repo:** none.");
|
|
979
|
+
}
|
|
980
|
+
} else {
|
|
981
|
+
L.push("**Parallel sessions:** _not checked (no key / no changed paths / offline)._");
|
|
982
|
+
}
|
|
983
|
+
L.push("");
|
|
984
|
+
if (lessonsAvailable && lessons.length > 0) {
|
|
985
|
+
L.push("## Relevant git lessons (maind)");
|
|
986
|
+
for (const l of lessons) {
|
|
987
|
+
L.push(`- **${l.title ?? l.id}**${l.id ? ` \`${l.id}\`` : ""}`);
|
|
988
|
+
}
|
|
989
|
+
L.push("");
|
|
990
|
+
}
|
|
991
|
+
L.push('_Full PR workflow: `activate_skill({id:"skill_pr_lifecycle"})`._');
|
|
992
|
+
L.push("");
|
|
993
|
+
return L.join("\n");
|
|
994
|
+
}
|
|
995
|
+
function buildManifest2(git3, prs, peer) {
|
|
996
|
+
return {
|
|
997
|
+
schema: "maind.git-context/v1",
|
|
998
|
+
generated_at: nowIso2(),
|
|
999
|
+
project_key: git3.projectKey,
|
|
1000
|
+
branch: git3.branch,
|
|
1001
|
+
default_branch: git3.defaultBranch,
|
|
1002
|
+
ahead: git3.ahead,
|
|
1003
|
+
behind: git3.behind,
|
|
1004
|
+
uncommitted: git3.uncommitted,
|
|
1005
|
+
open_prs: prs.map((p) => ({ number: p.number, ci: p.ci, current_branch: p.isCurrentBranch })),
|
|
1006
|
+
peers_active: peer?.checked ? peer.peersActive : null,
|
|
1007
|
+
path_conflicts: peer?.conflicts.map((c) => c.path) ?? []
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function msg4(err2) {
|
|
1011
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
1012
|
+
}
|
|
1013
|
+
function warn4(line) {
|
|
1014
|
+
process.stderr.write(`maind git-context: ${line}
|
|
1015
|
+
`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/index-cmd.ts
|
|
1019
|
+
import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
|
|
1020
|
+
import { existsSync as existsSync2, mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1021
|
+
import { tmpdir } from "node:os";
|
|
1022
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1023
|
+
import { fileURLToPath } from "node:url";
|
|
1024
|
+
|
|
1025
|
+
// ../../packages/code-index/build/node.js
|
|
1026
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
1027
|
+
import { posix as posix4 } from "node:path";
|
|
1028
|
+
|
|
1029
|
+
// ../../packages/code-index/build/parse/tree-sitter/parser.js
|
|
1030
|
+
import { join as join3 } from "node:path";
|
|
1031
|
+
import Parser from "web-tree-sitter";
|
|
1032
|
+
|
|
1033
|
+
// ../../packages/code-index/build/parse/keys.js
|
|
1034
|
+
function moduleKey(filePath) {
|
|
1035
|
+
return `${filePath}#<module>`;
|
|
1036
|
+
}
|
|
1037
|
+
function symbolKey(filePath, qualifiedName, kind) {
|
|
1038
|
+
return `${filePath}#${qualifiedName}:${kind}`;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// ../../packages/code-index/build/parse/types.js
|
|
1042
|
+
function languageForPath(filePath) {
|
|
1043
|
+
const lower = filePath.toLowerCase();
|
|
1044
|
+
if (lower.endsWith(".tsx"))
|
|
1045
|
+
return "tsx";
|
|
1046
|
+
if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts"))
|
|
1047
|
+
return "typescript";
|
|
1048
|
+
if (lower.endsWith(".js") || lower.endsWith(".jsx") || lower.endsWith(".mjs") || lower.endsWith(".cjs"))
|
|
1049
|
+
return "javascript";
|
|
1050
|
+
if (lower.endsWith(".py") || lower.endsWith(".pyi"))
|
|
1051
|
+
return "python";
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
function isSkippablePath(filePath) {
|
|
1055
|
+
return filePath.toLowerCase().endsWith(".d.ts");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ../../packages/code-index/build/parse/lang/shared.js
|
|
1059
|
+
function newResult() {
|
|
1060
|
+
return { nodes: [], edges: [], refs: [], imports: [] };
|
|
1061
|
+
}
|
|
1062
|
+
function line1(row) {
|
|
1063
|
+
return row + 1;
|
|
1064
|
+
}
|
|
1065
|
+
function signatureOf(node) {
|
|
1066
|
+
const firstLine = node.text.split("\n", 1)[0] ?? "";
|
|
1067
|
+
return firstLine.trim().slice(0, 200);
|
|
1068
|
+
}
|
|
1069
|
+
function calleeName(callee) {
|
|
1070
|
+
if (!callee)
|
|
1071
|
+
return null;
|
|
1072
|
+
switch (callee.type) {
|
|
1073
|
+
case "identifier":
|
|
1074
|
+
case "property_identifier":
|
|
1075
|
+
case "type_identifier":
|
|
1076
|
+
return callee.text;
|
|
1077
|
+
case "member_expression":
|
|
1078
|
+
return callee.childForFieldName("property")?.text ?? null;
|
|
1079
|
+
case "attribute":
|
|
1080
|
+
return callee.childForFieldName("attribute")?.text ?? null;
|
|
1081
|
+
default:
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function collectCallTargets(body, callType) {
|
|
1086
|
+
const names = /* @__PURE__ */ new Set();
|
|
1087
|
+
for (const call of body.descendantsOfType(callType)) {
|
|
1088
|
+
const name = calleeName(call.childForFieldName("function"));
|
|
1089
|
+
if (name)
|
|
1090
|
+
names.add(name);
|
|
1091
|
+
}
|
|
1092
|
+
return [...names];
|
|
1093
|
+
}
|
|
1094
|
+
function addCallRefs(res, fromKey, body, callType) {
|
|
1095
|
+
for (const target of collectCallTargets(body, callType)) {
|
|
1096
|
+
res.refs.push({ fromKey, kind: "calls", targetName: target });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ../../packages/code-index/build/parse/lang/python.js
|
|
1101
|
+
var CALL_TYPE = "call";
|
|
1102
|
+
function extractPython(root, ctx) {
|
|
1103
|
+
const res = newResult();
|
|
1104
|
+
for (const stmt of root.namedChildren) {
|
|
1105
|
+
handleTopLevel(stmt, ctx, res);
|
|
1106
|
+
}
|
|
1107
|
+
return res;
|
|
1108
|
+
}
|
|
1109
|
+
function unwrapDecorated(node) {
|
|
1110
|
+
if (node.type === "decorated_definition") {
|
|
1111
|
+
return node.childForFieldName("definition") ?? node;
|
|
1112
|
+
}
|
|
1113
|
+
return node;
|
|
1114
|
+
}
|
|
1115
|
+
function handleTopLevel(rawStmt, ctx, res) {
|
|
1116
|
+
const stmt = unwrapDecorated(rawStmt);
|
|
1117
|
+
switch (stmt.type) {
|
|
1118
|
+
case "import_statement":
|
|
1119
|
+
case "import_from_statement": {
|
|
1120
|
+
const mod = stmt.childForFieldName("module_name") ?? stmt.childForFieldName("name");
|
|
1121
|
+
const spec = mod?.text ?? firstDottedName(stmt);
|
|
1122
|
+
if (spec)
|
|
1123
|
+
res.imports.push({ fromModuleKey: ctx.moduleKey, specifier: spec });
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
case "function_definition": {
|
|
1127
|
+
const name = stmt.childForFieldName("name")?.text;
|
|
1128
|
+
if (!name)
|
|
1129
|
+
return;
|
|
1130
|
+
const node = makeNode(stmt, ctx, name, name, "function");
|
|
1131
|
+
res.nodes.push(node);
|
|
1132
|
+
res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
|
|
1133
|
+
const body = stmt.childForFieldName("body");
|
|
1134
|
+
if (body)
|
|
1135
|
+
addCallRefs(res, node.nodeKey, body, CALL_TYPE);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
case "class_definition":
|
|
1139
|
+
addClass(stmt, ctx, res);
|
|
1140
|
+
return;
|
|
1141
|
+
case "expression_statement": {
|
|
1142
|
+
const assign = stmt.namedChildren.find((c) => c.type === "assignment");
|
|
1143
|
+
const left = assign?.childForFieldName("left");
|
|
1144
|
+
if (left && left.type === "identifier") {
|
|
1145
|
+
const node = makeNode(stmt, ctx, left.text, left.text, "variable");
|
|
1146
|
+
res.nodes.push(node);
|
|
1147
|
+
res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
|
|
1148
|
+
}
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
default:
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function addClass(stmt, ctx, res) {
|
|
1156
|
+
const name = stmt.childForFieldName("name")?.text;
|
|
1157
|
+
if (!name)
|
|
1158
|
+
return;
|
|
1159
|
+
const classNode = makeNode(stmt, ctx, name, name, "class");
|
|
1160
|
+
res.nodes.push(classNode);
|
|
1161
|
+
res.edges.push({ srcKey: ctx.moduleKey, dstKey: classNode.nodeKey, kind: "contains" });
|
|
1162
|
+
const supers = stmt.childForFieldName("superclasses");
|
|
1163
|
+
if (supers) {
|
|
1164
|
+
for (const arg of supers.namedChildren) {
|
|
1165
|
+
if (arg.type === "identifier") {
|
|
1166
|
+
res.refs.push({ fromKey: classNode.nodeKey, kind: "extends", targetName: arg.text });
|
|
1167
|
+
} else if (arg.type === "attribute") {
|
|
1168
|
+
const attr = arg.childForFieldName("attribute")?.text;
|
|
1169
|
+
if (attr)
|
|
1170
|
+
res.refs.push({ fromKey: classNode.nodeKey, kind: "extends", targetName: attr });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const body = stmt.childForFieldName("body");
|
|
1175
|
+
if (!body)
|
|
1176
|
+
return;
|
|
1177
|
+
for (const member of body.namedChildren) {
|
|
1178
|
+
const def = unwrapDecorated(member);
|
|
1179
|
+
if (def.type !== "function_definition")
|
|
1180
|
+
continue;
|
|
1181
|
+
const mName = def.childForFieldName("name")?.text;
|
|
1182
|
+
if (!mName)
|
|
1183
|
+
continue;
|
|
1184
|
+
const qn = `${name}.${mName}`;
|
|
1185
|
+
const methodNode = makeNode(def, ctx, mName, qn, "method");
|
|
1186
|
+
res.nodes.push(methodNode);
|
|
1187
|
+
res.edges.push({ srcKey: classNode.nodeKey, dstKey: methodNode.nodeKey, kind: "contains" });
|
|
1188
|
+
const mBody = def.childForFieldName("body");
|
|
1189
|
+
if (mBody)
|
|
1190
|
+
addCallRefs(res, methodNode.nodeKey, mBody, CALL_TYPE);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function makeNode(node, ctx, name, qualifiedName, kind) {
|
|
1194
|
+
return {
|
|
1195
|
+
nodeKey: symbolKey(ctx.filePath, qualifiedName, kind),
|
|
1196
|
+
kind,
|
|
1197
|
+
name,
|
|
1198
|
+
qualifiedName,
|
|
1199
|
+
filePath: ctx.filePath,
|
|
1200
|
+
language: ctx.language,
|
|
1201
|
+
startLine: line1(node.startPosition.row),
|
|
1202
|
+
endLine: line1(node.endPosition.row),
|
|
1203
|
+
startByte: node.startIndex,
|
|
1204
|
+
endByte: node.endIndex,
|
|
1205
|
+
signature: signatureOf(node),
|
|
1206
|
+
doc: null
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function firstDottedName(stmt) {
|
|
1210
|
+
return stmt.descendantsOfType("dotted_name")[0]?.text ?? null;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// ../../packages/code-index/build/parse/lang/typescript.js
|
|
1214
|
+
var CALL_TYPE2 = "call_expression";
|
|
1215
|
+
function extractTypescript(root, ctx) {
|
|
1216
|
+
const res = newResult();
|
|
1217
|
+
for (const stmt of root.namedChildren) {
|
|
1218
|
+
handleTopLevel2(stmt, ctx, res);
|
|
1219
|
+
}
|
|
1220
|
+
return res;
|
|
1221
|
+
}
|
|
1222
|
+
function unwrapExport(node) {
|
|
1223
|
+
if (node.type === "export_statement") {
|
|
1224
|
+
return node.childForFieldName("declaration") ?? node;
|
|
1225
|
+
}
|
|
1226
|
+
return node;
|
|
1227
|
+
}
|
|
1228
|
+
function handleTopLevel2(rawStmt, ctx, res) {
|
|
1229
|
+
const stmt = unwrapExport(rawStmt);
|
|
1230
|
+
switch (stmt.type) {
|
|
1231
|
+
case "import_statement": {
|
|
1232
|
+
const spec = stmt.childForFieldName("source")?.text;
|
|
1233
|
+
if (spec)
|
|
1234
|
+
res.imports.push({ fromModuleKey: ctx.moduleKey, specifier: stripQuotes(spec) });
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
case "function_declaration":
|
|
1238
|
+
case "generator_function_declaration": {
|
|
1239
|
+
addFunctionLike(stmt, ctx, res, "function");
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
case "class_declaration":
|
|
1243
|
+
case "abstract_class_declaration": {
|
|
1244
|
+
addClass2(stmt, ctx, res);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
case "interface_declaration":
|
|
1248
|
+
addSimpleNamed(stmt, ctx, res, "interface");
|
|
1249
|
+
return;
|
|
1250
|
+
case "type_alias_declaration":
|
|
1251
|
+
addSimpleNamed(stmt, ctx, res, "type_alias");
|
|
1252
|
+
return;
|
|
1253
|
+
case "enum_declaration":
|
|
1254
|
+
addSimpleNamed(stmt, ctx, res, "enum");
|
|
1255
|
+
return;
|
|
1256
|
+
case "lexical_declaration":
|
|
1257
|
+
case "variable_declaration": {
|
|
1258
|
+
for (const decl of stmt.namedChildren) {
|
|
1259
|
+
if (decl.type !== "variable_declarator")
|
|
1260
|
+
continue;
|
|
1261
|
+
const nameNode = decl.childForFieldName("name");
|
|
1262
|
+
if (!nameNode || nameNode.type !== "identifier")
|
|
1263
|
+
continue;
|
|
1264
|
+
const value2 = decl.childForFieldName("value");
|
|
1265
|
+
const isFn = value2?.type === "arrow_function" || value2?.type === "function_expression" || value2?.type === "function";
|
|
1266
|
+
addFunctionLike(decl, ctx, res, isFn ? "function" : "variable", nameNode, value2);
|
|
1267
|
+
}
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
default:
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function addFunctionLike(declNode, ctx, res, kind, nameOverride, bodyOverride) {
|
|
1275
|
+
const nameNode = nameOverride ?? declNode.childForFieldName("name");
|
|
1276
|
+
const name = nameNode?.text;
|
|
1277
|
+
if (!name)
|
|
1278
|
+
return;
|
|
1279
|
+
const node = makeNode2(declNode, ctx, name, name, kind);
|
|
1280
|
+
res.nodes.push(node);
|
|
1281
|
+
res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
|
|
1282
|
+
const body = bodyOverride ?? declNode.childForFieldName("body") ?? declNode;
|
|
1283
|
+
if (kind === "function" && body)
|
|
1284
|
+
addCallRefs(res, node.nodeKey, body, CALL_TYPE2);
|
|
1285
|
+
}
|
|
1286
|
+
function addSimpleNamed(declNode, ctx, res, kind) {
|
|
1287
|
+
const name = declNode.childForFieldName("name")?.text;
|
|
1288
|
+
if (!name)
|
|
1289
|
+
return;
|
|
1290
|
+
const node = makeNode2(declNode, ctx, name, name, kind);
|
|
1291
|
+
res.nodes.push(node);
|
|
1292
|
+
res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
|
|
1293
|
+
}
|
|
1294
|
+
function addClass2(declNode, ctx, res) {
|
|
1295
|
+
const name = declNode.childForFieldName("name")?.text;
|
|
1296
|
+
if (!name)
|
|
1297
|
+
return;
|
|
1298
|
+
const classNode = makeNode2(declNode, ctx, name, name, "class");
|
|
1299
|
+
res.nodes.push(classNode);
|
|
1300
|
+
res.edges.push({ srcKey: ctx.moduleKey, dstKey: classNode.nodeKey, kind: "contains" });
|
|
1301
|
+
for (const heritage of declNode.descendantsOfType("class_heritage")) {
|
|
1302
|
+
for (const clause of heritage.namedChildren) {
|
|
1303
|
+
const kind = clause.type === "implements_clause" ? "implements" : "extends";
|
|
1304
|
+
for (const id of clause.descendantsOfType("type_identifier")) {
|
|
1305
|
+
res.refs.push({ fromKey: classNode.nodeKey, kind, targetName: id.text });
|
|
1306
|
+
}
|
|
1307
|
+
for (const id of clause.descendantsOfType("identifier")) {
|
|
1308
|
+
res.refs.push({ fromKey: classNode.nodeKey, kind, targetName: id.text });
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
const body = declNode.childForFieldName("body");
|
|
1313
|
+
if (!body)
|
|
1314
|
+
return;
|
|
1315
|
+
for (const member of body.namedChildren) {
|
|
1316
|
+
if (member.type !== "method_definition")
|
|
1317
|
+
continue;
|
|
1318
|
+
const mName = member.childForFieldName("name")?.text;
|
|
1319
|
+
if (!mName)
|
|
1320
|
+
continue;
|
|
1321
|
+
const qn = `${name}.${mName}`;
|
|
1322
|
+
const methodNode = makeNode2(member, ctx, mName, qn, "method");
|
|
1323
|
+
res.nodes.push(methodNode);
|
|
1324
|
+
res.edges.push({ srcKey: classNode.nodeKey, dstKey: methodNode.nodeKey, kind: "contains" });
|
|
1325
|
+
const mBody = member.childForFieldName("body");
|
|
1326
|
+
if (mBody)
|
|
1327
|
+
addCallRefs(res, methodNode.nodeKey, mBody, CALL_TYPE2);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function makeNode2(node, ctx, name, qualifiedName, kind) {
|
|
1331
|
+
return {
|
|
1332
|
+
nodeKey: symbolKey(ctx.filePath, qualifiedName, kind),
|
|
1333
|
+
kind,
|
|
1334
|
+
name,
|
|
1335
|
+
qualifiedName,
|
|
1336
|
+
filePath: ctx.filePath,
|
|
1337
|
+
language: ctx.language,
|
|
1338
|
+
startLine: line1(node.startPosition.row),
|
|
1339
|
+
endLine: line1(node.endPosition.row),
|
|
1340
|
+
startByte: node.startIndex,
|
|
1341
|
+
endByte: node.endIndex,
|
|
1342
|
+
signature: signatureOf(node),
|
|
1343
|
+
doc: null
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
function stripQuotes(s) {
|
|
1347
|
+
return s.replace(/^["'`]|["'`]$/g, "");
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// ../../packages/code-index/build/parse/tree-sitter/grammars.js
|
|
1351
|
+
import { createRequire } from "node:module";
|
|
1352
|
+
import { join as join2 } from "node:path";
|
|
1353
|
+
var GRAMMAR_FILE = {
|
|
1354
|
+
typescript: "tree-sitter-typescript.wasm",
|
|
1355
|
+
tsx: "tree-sitter-tsx.wasm",
|
|
1356
|
+
javascript: "tree-sitter-javascript.wasm",
|
|
1357
|
+
python: "tree-sitter-python.wasm"
|
|
1358
|
+
};
|
|
1359
|
+
var CORE_WASM_FILE = "tree-sitter.wasm";
|
|
1360
|
+
function grammarPath(language, assetDir) {
|
|
1361
|
+
if (assetDir)
|
|
1362
|
+
return join2(assetDir, GRAMMAR_FILE[language]);
|
|
1363
|
+
return createRequire(import.meta.url).resolve(`tree-sitter-wasms/out/${GRAMMAR_FILE[language]}`);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// ../../packages/code-index/build/parse/tree-sitter/parser.js
|
|
1367
|
+
var SUPPORTED = ["typescript", "tsx", "javascript", "python"];
|
|
1368
|
+
var TreeSitterParser = class {
|
|
1369
|
+
initialized = false;
|
|
1370
|
+
parser = null;
|
|
1371
|
+
grammars = /* @__PURE__ */ new Map();
|
|
1372
|
+
assetDir;
|
|
1373
|
+
constructor(opts = {}) {
|
|
1374
|
+
this.assetDir = opts.assetDir;
|
|
1375
|
+
}
|
|
1376
|
+
async init() {
|
|
1377
|
+
if (this.initialized)
|
|
1378
|
+
return;
|
|
1379
|
+
await Parser.init(this.assetDir ? { locateFile: (file) => join3(this.assetDir, file || CORE_WASM_FILE) } : void 0);
|
|
1380
|
+
this.parser = new Parser();
|
|
1381
|
+
this.initialized = true;
|
|
1382
|
+
}
|
|
1383
|
+
supports(language) {
|
|
1384
|
+
return SUPPORTED.includes(language);
|
|
1385
|
+
}
|
|
1386
|
+
async parse(filePath, source, language) {
|
|
1387
|
+
await this.init();
|
|
1388
|
+
const parser = this.parser;
|
|
1389
|
+
parser.setLanguage(await this.grammar(language));
|
|
1390
|
+
const tree = parser.parse(source);
|
|
1391
|
+
if (!tree) {
|
|
1392
|
+
return { filePath, language, nodes: [], edges: [], refs: [], imports: [] };
|
|
1393
|
+
}
|
|
1394
|
+
const root = tree.rootNode;
|
|
1395
|
+
const mKey = moduleKey(filePath);
|
|
1396
|
+
const moduleNode = {
|
|
1397
|
+
nodeKey: mKey,
|
|
1398
|
+
kind: "module",
|
|
1399
|
+
name: filePath.split("/").pop() ?? filePath,
|
|
1400
|
+
qualifiedName: "<module>",
|
|
1401
|
+
filePath,
|
|
1402
|
+
language,
|
|
1403
|
+
startLine: 1,
|
|
1404
|
+
endLine: line1(root.endPosition.row),
|
|
1405
|
+
startByte: 0,
|
|
1406
|
+
// UTF-16 code-unit length (matches tree-sitter's startIndex/endIndex units).
|
|
1407
|
+
endByte: source.length,
|
|
1408
|
+
signature: null,
|
|
1409
|
+
doc: null
|
|
1410
|
+
};
|
|
1411
|
+
const ctx = { filePath, language, source, moduleKey: mKey };
|
|
1412
|
+
const extracted = language === "python" ? extractPython(root, ctx) : extractTypescript(root, ctx);
|
|
1413
|
+
tree.delete();
|
|
1414
|
+
return {
|
|
1415
|
+
filePath,
|
|
1416
|
+
language,
|
|
1417
|
+
nodes: [moduleNode, ...extracted.nodes],
|
|
1418
|
+
edges: extracted.edges,
|
|
1419
|
+
refs: extracted.refs,
|
|
1420
|
+
imports: extracted.imports
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
async grammar(language) {
|
|
1424
|
+
let g = this.grammars.get(language);
|
|
1425
|
+
if (!g) {
|
|
1426
|
+
g = await Parser.Language.load(grammarPath(language, this.assetDir));
|
|
1427
|
+
this.grammars.set(language, g);
|
|
1428
|
+
}
|
|
1429
|
+
return g;
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
// ../../packages/code-index/build/query/engine.js
|
|
1434
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1435
|
+
import { posix as posix3 } from "node:path";
|
|
1436
|
+
|
|
1437
|
+
// ../../packages/code-index/build/embed/recipe.js
|
|
1438
|
+
var EMBED_RECIPE_VERSION = "1";
|
|
1439
|
+
var MAX_CHARS = 1e3;
|
|
1440
|
+
function nodeEmbedText(n) {
|
|
1441
|
+
return [`${n.kind} ${n.qualifiedName}`, n.signature ?? "", n.doc ?? "", n.filePath].filter((s) => s.length > 0).join("\n").slice(0, MAX_CHARS);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ../../packages/code-index/build/embed/types.js
|
|
1445
|
+
var EMBEDDABLE_KINDS = [
|
|
1446
|
+
"function",
|
|
1447
|
+
"method",
|
|
1448
|
+
"class",
|
|
1449
|
+
"interface",
|
|
1450
|
+
"enum",
|
|
1451
|
+
"type_alias"
|
|
1452
|
+
];
|
|
1453
|
+
|
|
1454
|
+
// ../../packages/code-index/build/util/vector.js
|
|
1455
|
+
function cosineSimilarity(a, b) {
|
|
1456
|
+
const n = Math.min(a.length, b.length);
|
|
1457
|
+
let dot = 0;
|
|
1458
|
+
let na = 0;
|
|
1459
|
+
let nb = 0;
|
|
1460
|
+
for (let i = 0; i < n; i++) {
|
|
1461
|
+
const x = a[i];
|
|
1462
|
+
const y = b[i];
|
|
1463
|
+
dot += x * y;
|
|
1464
|
+
na += x * x;
|
|
1465
|
+
nb += y * y;
|
|
1466
|
+
}
|
|
1467
|
+
if (na === 0 || nb === 0)
|
|
1468
|
+
return 0;
|
|
1469
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ../../packages/code-index/build/graph/resolve.js
|
|
1473
|
+
import { posix } from "node:path";
|
|
1474
|
+
var RESOLVABLE_KINDS = /* @__PURE__ */ new Set([
|
|
1475
|
+
"function",
|
|
1476
|
+
"method",
|
|
1477
|
+
"class",
|
|
1478
|
+
"interface",
|
|
1479
|
+
"enum",
|
|
1480
|
+
"type_alias"
|
|
1481
|
+
]);
|
|
1482
|
+
var SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py"];
|
|
1483
|
+
function buildResolutionIndex(entries) {
|
|
1484
|
+
const nameToKeys = /* @__PURE__ */ new Map();
|
|
1485
|
+
const files = /* @__PURE__ */ new Set();
|
|
1486
|
+
for (const e of entries) {
|
|
1487
|
+
files.add(e.filePath);
|
|
1488
|
+
if (!RESOLVABLE_KINDS.has(e.kind))
|
|
1489
|
+
continue;
|
|
1490
|
+
const list = nameToKeys.get(e.name);
|
|
1491
|
+
if (list)
|
|
1492
|
+
list.push(e.nodeKey);
|
|
1493
|
+
else
|
|
1494
|
+
nameToKeys.set(e.name, [e.nodeKey]);
|
|
1495
|
+
}
|
|
1496
|
+
return { nameToKeys, files };
|
|
1497
|
+
}
|
|
1498
|
+
function resolveFileEdges(parsed, index) {
|
|
1499
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1500
|
+
const edges = [];
|
|
1501
|
+
const srcKeys = /* @__PURE__ */ new Set();
|
|
1502
|
+
const push2 = (srcKey, dstKey, kind) => {
|
|
1503
|
+
if (srcKey === dstKey)
|
|
1504
|
+
return;
|
|
1505
|
+
const dedup = `${srcKey}\0${dstKey}\0${kind}`;
|
|
1506
|
+
if (seen.has(dedup))
|
|
1507
|
+
return;
|
|
1508
|
+
seen.add(dedup);
|
|
1509
|
+
edges.push({ srcKey, dstKey, kind });
|
|
1510
|
+
srcKeys.add(srcKey);
|
|
1511
|
+
};
|
|
1512
|
+
for (const e of parsed.edges)
|
|
1513
|
+
push2(e.srcKey, e.dstKey, e.kind);
|
|
1514
|
+
for (const n of parsed.nodes)
|
|
1515
|
+
srcKeys.add(n.nodeKey);
|
|
1516
|
+
for (const ref of parsed.refs) {
|
|
1517
|
+
const candidates = index.nameToKeys.get(ref.targetName);
|
|
1518
|
+
if (!candidates || candidates.length !== 1)
|
|
1519
|
+
continue;
|
|
1520
|
+
push2(ref.fromKey, candidates[0], ref.kind);
|
|
1521
|
+
}
|
|
1522
|
+
for (const imp of parsed.imports) {
|
|
1523
|
+
const target = resolveImportTarget(parsed.filePath, imp.specifier, index.files);
|
|
1524
|
+
if (target)
|
|
1525
|
+
push2(imp.fromModuleKey, moduleKey(target), "imports");
|
|
1526
|
+
}
|
|
1527
|
+
return { filePath: parsed.filePath, srcKeys: [...srcKeys], edges };
|
|
1528
|
+
}
|
|
1529
|
+
function resolveImportTarget(fromFile, specifier, files) {
|
|
1530
|
+
if (!specifier.startsWith("."))
|
|
1531
|
+
return null;
|
|
1532
|
+
const base = posix.normalize(posix.join(posix.dirname(fromFile), specifier));
|
|
1533
|
+
const candidates = [];
|
|
1534
|
+
const hasSourceExt = SOURCE_EXTS.some((e) => base.endsWith(e));
|
|
1535
|
+
if (hasSourceExt)
|
|
1536
|
+
candidates.push(base);
|
|
1537
|
+
const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
|
|
1538
|
+
if (jsLike) {
|
|
1539
|
+
const stem = base.slice(0, -jsLike[0].length);
|
|
1540
|
+
for (const e of SOURCE_EXTS)
|
|
1541
|
+
candidates.push(stem + e);
|
|
1542
|
+
}
|
|
1543
|
+
if (!hasSourceExt) {
|
|
1544
|
+
for (const e of SOURCE_EXTS)
|
|
1545
|
+
candidates.push(base + e);
|
|
1546
|
+
for (const e of SOURCE_EXTS)
|
|
1547
|
+
candidates.push(`${base}/index${e}`);
|
|
1548
|
+
}
|
|
1549
|
+
for (const c of candidates) {
|
|
1550
|
+
if (files.has(c))
|
|
1551
|
+
return c;
|
|
1552
|
+
}
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// ../../packages/code-index/build/incremental/discover.js
|
|
1557
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1558
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1559
|
+
import { readdirSync, statSync } from "node:fs";
|
|
1560
|
+
import { posix as posix2, sep } from "node:path";
|
|
1561
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
1562
|
+
".git",
|
|
1563
|
+
"node_modules",
|
|
1564
|
+
"build",
|
|
1565
|
+
"dist",
|
|
1566
|
+
".next",
|
|
1567
|
+
"out",
|
|
1568
|
+
"coverage",
|
|
1569
|
+
".turbo",
|
|
1570
|
+
".cache",
|
|
1571
|
+
"__pycache__",
|
|
1572
|
+
".venv",
|
|
1573
|
+
"venv"
|
|
1574
|
+
]);
|
|
1575
|
+
function runGit(repoRoot2, args) {
|
|
1576
|
+
try {
|
|
1577
|
+
return execFileSync3("git", args, {
|
|
1578
|
+
cwd: repoRoot2,
|
|
1579
|
+
encoding: "utf8",
|
|
1580
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1581
|
+
timeout: 15e3,
|
|
1582
|
+
maxBuffer: 64 * 1024 * 1024
|
|
1583
|
+
});
|
|
1584
|
+
} catch {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
var git2 = runGit;
|
|
1589
|
+
function toPosix(p) {
|
|
1590
|
+
return sep === "/" ? p : p.split(sep).join("/");
|
|
1591
|
+
}
|
|
1592
|
+
function classify(paths) {
|
|
1593
|
+
const out = [];
|
|
1594
|
+
for (const raw of paths) {
|
|
1595
|
+
const path = toPosix(raw).trim();
|
|
1596
|
+
if (!path || isSkippablePath(path))
|
|
1597
|
+
continue;
|
|
1598
|
+
const language = languageForPath(path);
|
|
1599
|
+
if (language)
|
|
1600
|
+
out.push({ path, language });
|
|
1601
|
+
}
|
|
1602
|
+
return out;
|
|
1603
|
+
}
|
|
1604
|
+
function classifyPaths(paths) {
|
|
1605
|
+
return classify(paths);
|
|
1606
|
+
}
|
|
1607
|
+
function discoverSourceFiles(repoRoot2) {
|
|
1608
|
+
const tracked = git2(repoRoot2, ["ls-files", "--cached", "--others", "--exclude-standard"]);
|
|
1609
|
+
if (tracked !== null) {
|
|
1610
|
+
return classify(tracked.split("\n"));
|
|
1611
|
+
}
|
|
1612
|
+
const acc = [];
|
|
1613
|
+
walk(repoRoot2, "", acc);
|
|
1614
|
+
return classify(acc);
|
|
1615
|
+
}
|
|
1616
|
+
function walk(root, rel, acc) {
|
|
1617
|
+
let entries;
|
|
1618
|
+
try {
|
|
1619
|
+
entries = readdirSync(posix2.join(root, rel), { withFileTypes: true });
|
|
1620
|
+
} catch {
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
for (const entry of entries) {
|
|
1624
|
+
if (entry.name.startsWith(".") && entry.name !== ".") {
|
|
1625
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (IGNORE_DIRS.has(entry.name))
|
|
1629
|
+
continue;
|
|
1630
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
1631
|
+
if (entry.isDirectory())
|
|
1632
|
+
walk(root, childRel, acc);
|
|
1633
|
+
else if (entry.isFile())
|
|
1634
|
+
acc.push(childRel);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function enumerateChangedPaths(repoRoot2, opts = {}) {
|
|
1638
|
+
if (headCommit(repoRoot2) === null)
|
|
1639
|
+
return null;
|
|
1640
|
+
const includeUncommitted = opts.includeUncommitted !== false;
|
|
1641
|
+
const ref = opts.since ?? "HEAD";
|
|
1642
|
+
const diffArgs = ["diff", "--name-status", "--diff-filter=d", ref];
|
|
1643
|
+
if (!includeUncommitted) {
|
|
1644
|
+
if (!opts.since) {
|
|
1645
|
+
throw new Error("nothing to enumerate: include_uncommitted=false requires a `since` ref");
|
|
1646
|
+
}
|
|
1647
|
+
diffArgs.push("HEAD");
|
|
1648
|
+
}
|
|
1649
|
+
const diff = git2(repoRoot2, diffArgs);
|
|
1650
|
+
if (diff === null) {
|
|
1651
|
+
throw new Error(`git diff failed \u2014 is '${ref}' a valid ref?`);
|
|
1652
|
+
}
|
|
1653
|
+
const paths = /* @__PURE__ */ new Map();
|
|
1654
|
+
for (const line of diff.split("\n")) {
|
|
1655
|
+
if (!line.trim())
|
|
1656
|
+
continue;
|
|
1657
|
+
const cols = line.split(" ");
|
|
1658
|
+
if (cols.length < 2)
|
|
1659
|
+
continue;
|
|
1660
|
+
const code = cols[0][0];
|
|
1661
|
+
const p = unquote((cols.length >= 3 ? cols[cols.length - 1] : cols[1]).trim());
|
|
1662
|
+
if (!p)
|
|
1663
|
+
continue;
|
|
1664
|
+
if (code === "A" || code === "C")
|
|
1665
|
+
paths.set(p, "added");
|
|
1666
|
+
else if (code === "R")
|
|
1667
|
+
paths.set(p, "renamed");
|
|
1668
|
+
else
|
|
1669
|
+
paths.set(p, "modified");
|
|
1670
|
+
}
|
|
1671
|
+
if (includeUncommitted) {
|
|
1672
|
+
const status2 = git2(repoRoot2, ["status", "--porcelain", "-uall"]) ?? "";
|
|
1673
|
+
for (const line of status2.split("\n")) {
|
|
1674
|
+
if (line.length < 4)
|
|
1675
|
+
continue;
|
|
1676
|
+
const x = line[0];
|
|
1677
|
+
const y = line[1];
|
|
1678
|
+
if (x === "D" || y === "D")
|
|
1679
|
+
continue;
|
|
1680
|
+
let p = line.slice(3);
|
|
1681
|
+
const arrow = p.indexOf(" -> ");
|
|
1682
|
+
if (arrow >= 0)
|
|
1683
|
+
p = p.slice(arrow + 4);
|
|
1684
|
+
p = unquote(p);
|
|
1685
|
+
if (!p || paths.has(p))
|
|
1686
|
+
continue;
|
|
1687
|
+
if (x === "?" || x === "A" || y === "A")
|
|
1688
|
+
paths.set(p, "added");
|
|
1689
|
+
else if (x === "R")
|
|
1690
|
+
paths.set(p, "renamed");
|
|
1691
|
+
else
|
|
1692
|
+
paths.set(p, "modified");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return paths;
|
|
1696
|
+
}
|
|
1697
|
+
function unquote(p) {
|
|
1698
|
+
return p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
|
|
1699
|
+
}
|
|
1700
|
+
function changedSourceFiles(repoRoot2, opts = {}) {
|
|
1701
|
+
const paths = enumerateChangedPaths(repoRoot2, opts);
|
|
1702
|
+
if (paths === null)
|
|
1703
|
+
return null;
|
|
1704
|
+
return classify(paths.keys());
|
|
1705
|
+
}
|
|
1706
|
+
function statFile(repoRoot2, relPath) {
|
|
1707
|
+
try {
|
|
1708
|
+
const s = statSync(posix2.join(repoRoot2, relPath));
|
|
1709
|
+
return { mtimeMs: Math.floor(s.mtimeMs), size: s.size };
|
|
1710
|
+
} catch {
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
function hashContent(content) {
|
|
1715
|
+
return createHash3("sha256").update(content).digest("hex");
|
|
1716
|
+
}
|
|
1717
|
+
function headCommit(repoRoot2) {
|
|
1718
|
+
return git2(repoRoot2, ["rev-parse", "HEAD"])?.trim() ?? null;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// ../../packages/code-index/build/incremental/hunks.js
|
|
1722
|
+
var MAX_DIFF_BYTES = 16 * 1024 * 1024;
|
|
1723
|
+
var MAX_HUNKS_PER_FILE = 500;
|
|
1724
|
+
var MAX_ADDED_LINES_PER_FILE = 4e3;
|
|
1725
|
+
var MAX_UNTRACKED_HEAD_BYTES = 256 * 1024;
|
|
1726
|
+
var HUNK_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
|
|
1727
|
+
function changedHunks(repoRoot2, opts = {}) {
|
|
1728
|
+
if (runGit(repoRoot2, ["rev-parse", "HEAD"]) === null)
|
|
1729
|
+
return null;
|
|
1730
|
+
const includeUncommitted = opts.includeUncommitted !== false;
|
|
1731
|
+
const ref = opts.since ?? "HEAD";
|
|
1732
|
+
const diffArgs = [
|
|
1733
|
+
"diff",
|
|
1734
|
+
"--unified=0",
|
|
1735
|
+
"--no-color",
|
|
1736
|
+
"--no-ext-diff",
|
|
1737
|
+
"--find-renames",
|
|
1738
|
+
"--diff-filter=d",
|
|
1739
|
+
ref
|
|
1740
|
+
];
|
|
1741
|
+
if (!includeUncommitted) {
|
|
1742
|
+
if (!opts.since) {
|
|
1743
|
+
throw new Error("nothing to enumerate: include_uncommitted=false requires a `since` ref");
|
|
1744
|
+
}
|
|
1745
|
+
diffArgs.push("HEAD");
|
|
1746
|
+
}
|
|
1747
|
+
const diff = runGit(repoRoot2, diffArgs);
|
|
1748
|
+
if (diff === null) {
|
|
1749
|
+
throw new Error(`git diff failed \u2014 is '${ref}' a valid ref?`);
|
|
1750
|
+
}
|
|
1751
|
+
const files = /* @__PURE__ */ new Map();
|
|
1752
|
+
let coverage = "full";
|
|
1753
|
+
if (Buffer.byteLength(diff, "utf8") > MAX_DIFF_BYTES) {
|
|
1754
|
+
coverage = "none";
|
|
1755
|
+
} else {
|
|
1756
|
+
parseUnifiedDiff(diff, files, () => {
|
|
1757
|
+
coverage = "partial";
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (includeUncommitted) {
|
|
1761
|
+
const status2 = runGit(repoRoot2, ["status", "--porcelain", "-uall"]) ?? "";
|
|
1762
|
+
for (const line of status2.split("\n")) {
|
|
1763
|
+
if (line.length < 4 || line[0] !== "?")
|
|
1764
|
+
continue;
|
|
1765
|
+
let p = line.slice(3);
|
|
1766
|
+
if (p.startsWith('"') && p.endsWith('"'))
|
|
1767
|
+
p = p.slice(1, -1);
|
|
1768
|
+
if (!p || files.has(p))
|
|
1769
|
+
continue;
|
|
1770
|
+
files.set(p, {
|
|
1771
|
+
path: p,
|
|
1772
|
+
status: "added",
|
|
1773
|
+
binary: false,
|
|
1774
|
+
wholeFile: true,
|
|
1775
|
+
degraded: false,
|
|
1776
|
+
addedRanges: [],
|
|
1777
|
+
deletionPoints: [],
|
|
1778
|
+
addedLines: []
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return { files, coverage };
|
|
1783
|
+
}
|
|
1784
|
+
function parseUnifiedDiff(diff, files, onDegrade) {
|
|
1785
|
+
let current = null;
|
|
1786
|
+
let hunkCount = 0;
|
|
1787
|
+
let pendingRename = false;
|
|
1788
|
+
let pendingNewFile = false;
|
|
1789
|
+
const commit = () => {
|
|
1790
|
+
if (current)
|
|
1791
|
+
files.set(current.path, current);
|
|
1792
|
+
current = null;
|
|
1793
|
+
hunkCount = 0;
|
|
1794
|
+
};
|
|
1795
|
+
for (const line of diff.split("\n")) {
|
|
1796
|
+
if (line.startsWith("diff --git ")) {
|
|
1797
|
+
commit();
|
|
1798
|
+
pendingRename = false;
|
|
1799
|
+
pendingNewFile = false;
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
if (line.startsWith("new file mode")) {
|
|
1803
|
+
pendingNewFile = true;
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
if (line.startsWith("rename to ")) {
|
|
1807
|
+
pendingRename = true;
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
|
|
1811
|
+
if (current)
|
|
1812
|
+
current.binary = true;
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
if (line.startsWith("+++ ")) {
|
|
1816
|
+
let p = line.slice(4).trim();
|
|
1817
|
+
if (p.startsWith('"') && p.endsWith('"'))
|
|
1818
|
+
p = p.slice(1, -1);
|
|
1819
|
+
if (p.startsWith("b/"))
|
|
1820
|
+
p = p.slice(2);
|
|
1821
|
+
if (p === "/dev/null")
|
|
1822
|
+
continue;
|
|
1823
|
+
current = {
|
|
1824
|
+
path: p,
|
|
1825
|
+
status: pendingNewFile ? "added" : pendingRename ? "renamed" : "modified",
|
|
1826
|
+
binary: false,
|
|
1827
|
+
wholeFile: false,
|
|
1828
|
+
degraded: false,
|
|
1829
|
+
addedRanges: [],
|
|
1830
|
+
deletionPoints: [],
|
|
1831
|
+
addedLines: []
|
|
1832
|
+
};
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
if (!current)
|
|
1836
|
+
continue;
|
|
1837
|
+
const m = HUNK_RE.exec(line);
|
|
1838
|
+
if (m) {
|
|
1839
|
+
hunkCount++;
|
|
1840
|
+
if (hunkCount > MAX_HUNKS_PER_FILE) {
|
|
1841
|
+
if (!current.degraded) {
|
|
1842
|
+
current.degraded = true;
|
|
1843
|
+
current.addedRanges = [];
|
|
1844
|
+
current.deletionPoints = [];
|
|
1845
|
+
current.addedLines = [];
|
|
1846
|
+
onDegrade();
|
|
1847
|
+
}
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1850
|
+
const start = Number(m[1]);
|
|
1851
|
+
const count = m[2] === void 0 ? 1 : Number(m[2]);
|
|
1852
|
+
if (count > 0)
|
|
1853
|
+
current.addedRanges.push({ start, end: start + count - 1 });
|
|
1854
|
+
else
|
|
1855
|
+
current.deletionPoints.push(start);
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
if (line.startsWith("+") && !line.startsWith("+++") && !current.degraded && current.addedLines.length < MAX_ADDED_LINES_PER_FILE) {
|
|
1859
|
+
current.addedLines.push(line.slice(1));
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
commit();
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// ../../packages/code-index/build/review/risk-rules.js
|
|
1866
|
+
var RISK_RULES = [
|
|
1867
|
+
{
|
|
1868
|
+
flag: "sql_migration_added",
|
|
1869
|
+
path: /(^|\/)migrations\/[^/]+\.sql$/i,
|
|
1870
|
+
onlyWhenAdded: true
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
flag: "security_definer_touched",
|
|
1874
|
+
path: /\.sql$/i,
|
|
1875
|
+
content: /\bSECURITY\s+DEFINER\b/i
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
flag: "rls_policy_changed",
|
|
1879
|
+
path: /\.sql$/i,
|
|
1880
|
+
content: /\b(CREATE|ALTER|DROP)\s+POLICY\b|\bROW\s+LEVEL\s+SECURITY\b/i
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
flag: "auth_config_changed",
|
|
1884
|
+
path: /(^|\/)supabase\/config\.toml$|(^|\/)(middleware|proxy)\.(ts|js)$/
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
flag: "package_manifest_changed",
|
|
1888
|
+
path: /(^|\/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|requirements\.txt|pyproject\.toml|Cargo\.toml|go\.mod)$/
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
flag: "ci_workflow_changed",
|
|
1892
|
+
path: /^\.github\/workflows\/[^/]+\.ya?ml$|^\.gitlab-ci\.yml$/
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
flag: "env_or_secrets_file",
|
|
1896
|
+
path: /(^|\/)\.env(\.[^/]+)?$|\.(pem|key)$/
|
|
1897
|
+
}
|
|
1898
|
+
];
|
|
1899
|
+
|
|
1900
|
+
// ../../packages/code-index/build/review/risk-scan.js
|
|
1901
|
+
function scanRiskFlags(input) {
|
|
1902
|
+
const flags = /* @__PURE__ */ new Set();
|
|
1903
|
+
for (const [path, status2] of input.paths) {
|
|
1904
|
+
for (const rule of RISK_RULES) {
|
|
1905
|
+
if (flags.has(rule.flag))
|
|
1906
|
+
continue;
|
|
1907
|
+
if (!rule.path.test(path))
|
|
1908
|
+
continue;
|
|
1909
|
+
if (rule.onlyWhenAdded && status2 !== "added")
|
|
1910
|
+
continue;
|
|
1911
|
+
if (!rule.content) {
|
|
1912
|
+
flags.add(rule.flag);
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
const text = contentForPath(path, input);
|
|
1916
|
+
if (text !== null && rule.content.test(text))
|
|
1917
|
+
flags.add(rule.flag);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return [...flags].sort();
|
|
1921
|
+
}
|
|
1922
|
+
function contentForPath(path, input) {
|
|
1923
|
+
const hunks = input.hunks.get(path);
|
|
1924
|
+
if (hunks && !hunks.wholeFile && !hunks.degraded) {
|
|
1925
|
+
return hunks.addedLines.join("\n");
|
|
1926
|
+
}
|
|
1927
|
+
return input.readUntracked(path);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// ../../packages/code-index/build/store/schema.js
|
|
1931
|
+
var SCHEMA_VERSION = "1";
|
|
1932
|
+
var SCHEMA_SQL = (
|
|
1933
|
+
/* sql */
|
|
1934
|
+
`
|
|
1935
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
1936
|
+
key TEXT PRIMARY KEY,
|
|
1937
|
+
value TEXT NOT NULL
|
|
1938
|
+
);
|
|
1939
|
+
|
|
1940
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
1941
|
+
path TEXT PRIMARY KEY,
|
|
1942
|
+
language TEXT NOT NULL,
|
|
1943
|
+
content_hash TEXT NOT NULL,
|
|
1944
|
+
mtime_ms INTEGER NOT NULL,
|
|
1945
|
+
size INTEGER NOT NULL,
|
|
1946
|
+
indexed_at INTEGER NOT NULL
|
|
1947
|
+
);
|
|
1948
|
+
|
|
1949
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
1950
|
+
node_key TEXT PRIMARY KEY,
|
|
1951
|
+
kind TEXT NOT NULL,
|
|
1952
|
+
name TEXT NOT NULL,
|
|
1953
|
+
qualified_name TEXT NOT NULL,
|
|
1954
|
+
file_path TEXT NOT NULL,
|
|
1955
|
+
language TEXT NOT NULL,
|
|
1956
|
+
start_line INTEGER NOT NULL,
|
|
1957
|
+
end_line INTEGER NOT NULL,
|
|
1958
|
+
start_byte INTEGER NOT NULL,
|
|
1959
|
+
end_byte INTEGER NOT NULL,
|
|
1960
|
+
signature TEXT,
|
|
1961
|
+
doc TEXT
|
|
1962
|
+
);
|
|
1963
|
+
CREATE INDEX IF NOT EXISTS nodes_file_idx ON nodes(file_path);
|
|
1964
|
+
CREATE INDEX IF NOT EXISTS nodes_kind_idx ON nodes(kind);
|
|
1965
|
+
CREATE INDEX IF NOT EXISTS nodes_name_idx ON nodes(name);
|
|
1966
|
+
|
|
1967
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
1968
|
+
src_key TEXT NOT NULL,
|
|
1969
|
+
dst_key TEXT NOT NULL,
|
|
1970
|
+
kind TEXT NOT NULL,
|
|
1971
|
+
PRIMARY KEY (src_key, dst_key, kind)
|
|
1972
|
+
);
|
|
1973
|
+
CREATE INDEX IF NOT EXISTS edges_dst_idx ON edges(dst_key, kind);
|
|
1974
|
+
CREATE INDEX IF NOT EXISTS edges_src_idx ON edges(src_key, kind);
|
|
1975
|
+
|
|
1976
|
+
-- Standalone (self-managed) FTS5 table \u2014 we populate/delete it in the same
|
|
1977
|
+
-- transaction as nodes, avoiding external-content trigger bookkeeping. The
|
|
1978
|
+
-- search_text column carries name + camel/snake-split tokens + doc + signature
|
|
1979
|
+
-- so unicode61 can match identifier fragments.
|
|
1980
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
|
1981
|
+
node_key UNINDEXED,
|
|
1982
|
+
search_text,
|
|
1983
|
+
tokenize = 'unicode61'
|
|
1984
|
+
);
|
|
1985
|
+
|
|
1986
|
+
-- Reserved for Phase 2 (semantic). Unused in Phase 1.
|
|
1987
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
1988
|
+
node_key TEXT PRIMARY KEY,
|
|
1989
|
+
dim INTEGER NOT NULL,
|
|
1990
|
+
vec BLOB NOT NULL,
|
|
1991
|
+
content_hash TEXT NOT NULL
|
|
1992
|
+
);
|
|
1993
|
+
`
|
|
1994
|
+
);
|
|
1995
|
+
var META_SCHEMA_VERSION = "schema_version";
|
|
1996
|
+
var META_LAST_INDEX_COMMIT = "last_index_commit";
|
|
1997
|
+
var META_LAST_INDEXED_AT = "last_indexed_at";
|
|
1998
|
+
var META_EMBED_RECIPE_VERSION = "embed_recipe_version";
|
|
1999
|
+
var META_EMBED_MODEL = "embed_model";
|
|
2000
|
+
var META_EMBED_DIM = "embed_dim";
|
|
2001
|
+
var META_LAST_SYNCED_AT = "last_synced_at";
|
|
2002
|
+
var META_LAST_SYNCED_COMMIT = "last_synced_commit";
|
|
2003
|
+
|
|
2004
|
+
// ../../packages/code-index/build/util/identifiers.js
|
|
2005
|
+
function splitIdentifier(id) {
|
|
2006
|
+
return id.replace(/[_\-./]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/\s+/).filter((t) => t.length > 0);
|
|
2007
|
+
}
|
|
2008
|
+
function buildSearchText(parts) {
|
|
2009
|
+
const frags = /* @__PURE__ */ new Set();
|
|
2010
|
+
frags.add(parts.name.toLowerCase());
|
|
2011
|
+
frags.add(parts.qualifiedName.toLowerCase());
|
|
2012
|
+
for (const f of splitIdentifier(parts.name))
|
|
2013
|
+
frags.add(f);
|
|
2014
|
+
for (const f of splitIdentifier(parts.qualifiedName))
|
|
2015
|
+
frags.add(f);
|
|
2016
|
+
const extra = [parts.doc ?? "", parts.signature ?? ""].join(" ").trim();
|
|
2017
|
+
const base = [...frags].join(" ");
|
|
2018
|
+
return extra ? `${base} ${extra}` : base;
|
|
2019
|
+
}
|
|
2020
|
+
function toFtsMatchQuery(query) {
|
|
2021
|
+
const frags = /* @__PURE__ */ new Set();
|
|
2022
|
+
for (const raw of query.split(/\s+/)) {
|
|
2023
|
+
const cleaned = raw.replace(/[^A-Za-z0-9]+/g, "");
|
|
2024
|
+
if (cleaned.length === 0)
|
|
2025
|
+
continue;
|
|
2026
|
+
frags.add(cleaned.toLowerCase());
|
|
2027
|
+
for (const f of splitIdentifier(cleaned))
|
|
2028
|
+
frags.add(f);
|
|
2029
|
+
}
|
|
2030
|
+
if (frags.size === 0)
|
|
2031
|
+
return null;
|
|
2032
|
+
return [...frags].map((f) => `${f}*`).join(" OR ");
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// ../../packages/code-index/build/query/engine.js
|
|
2036
|
+
var CodeIndexEngine = class {
|
|
2037
|
+
store;
|
|
2038
|
+
parser;
|
|
2039
|
+
repoRoot;
|
|
2040
|
+
embedder;
|
|
2041
|
+
constructor(deps) {
|
|
2042
|
+
this.store = deps.store;
|
|
2043
|
+
this.parser = deps.parser;
|
|
2044
|
+
this.repoRoot = deps.repoRoot;
|
|
2045
|
+
this.embedder = deps.embedder ?? null;
|
|
2046
|
+
}
|
|
2047
|
+
async indexRepository(opts = {}) {
|
|
2048
|
+
const started = opts.now ?? Date.now();
|
|
2049
|
+
await this.store.init();
|
|
2050
|
+
await this.parser.init();
|
|
2051
|
+
const storedVersion = await this.store.getMeta(META_SCHEMA_VERSION);
|
|
2052
|
+
const hadIndex = await this.store.countFiles() > 0;
|
|
2053
|
+
if (opts.full || hadIndex && storedVersion !== SCHEMA_VERSION) {
|
|
2054
|
+
await this.wipe();
|
|
2055
|
+
}
|
|
2056
|
+
const discovered = discoverSourceFiles(this.repoRoot);
|
|
2057
|
+
const discoveredPaths = new Set(discovered.map((d) => d.path));
|
|
2058
|
+
const storedFiles = await this.store.getAllFiles();
|
|
2059
|
+
const storedByPath = new Map(storedFiles.map((f) => [f.path, f]));
|
|
2060
|
+
const incremental = storedFiles.length > 0 && !opts.full;
|
|
2061
|
+
const stats = {
|
|
2062
|
+
filesScanned: discovered.length,
|
|
2063
|
+
filesParsed: 0,
|
|
2064
|
+
filesSkipped: 0,
|
|
2065
|
+
filesDeleted: 0,
|
|
2066
|
+
nodeCount: 0,
|
|
2067
|
+
edgeCount: 0,
|
|
2068
|
+
durationMs: 0,
|
|
2069
|
+
incremental
|
|
2070
|
+
};
|
|
2071
|
+
for (const f of storedFiles) {
|
|
2072
|
+
if (!discoveredPaths.has(f.path)) {
|
|
2073
|
+
await this.store.deleteFile(f.path);
|
|
2074
|
+
stats.filesDeleted++;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
const parsedFiles = [];
|
|
2078
|
+
let done = 0;
|
|
2079
|
+
for (const d of discovered) {
|
|
2080
|
+
done++;
|
|
2081
|
+
const st = statFile(this.repoRoot, d.path);
|
|
2082
|
+
if (!st) {
|
|
2083
|
+
stats.filesSkipped++;
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
const prior = storedByPath.get(d.path);
|
|
2087
|
+
if (prior && prior.mtimeMs === st.mtimeMs && prior.size === st.size) {
|
|
2088
|
+
stats.filesSkipped++;
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
const content = this.readRepoFile(d.path);
|
|
2092
|
+
if (content === null) {
|
|
2093
|
+
stats.filesSkipped++;
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const contentHash = hashContent(content);
|
|
2097
|
+
if (prior && prior.contentHash === contentHash) {
|
|
2098
|
+
await this.store.upsertFile({ ...prior, mtimeMs: st.mtimeMs, indexedAt: started });
|
|
2099
|
+
stats.filesSkipped++;
|
|
2100
|
+
continue;
|
|
2101
|
+
}
|
|
2102
|
+
const parsed = await this.parser.parse(d.path, content, d.language);
|
|
2103
|
+
const ftsText = /* @__PURE__ */ new Map();
|
|
2104
|
+
for (const n of parsed.nodes)
|
|
2105
|
+
ftsText.set(n.nodeKey, buildSearchText(n));
|
|
2106
|
+
const fileRow = {
|
|
2107
|
+
path: d.path,
|
|
2108
|
+
language: d.language,
|
|
2109
|
+
contentHash,
|
|
2110
|
+
mtimeMs: st.mtimeMs,
|
|
2111
|
+
size: st.size,
|
|
2112
|
+
indexedAt: started
|
|
2113
|
+
};
|
|
2114
|
+
await this.store.replaceFileNodes(fileRow, parsed.nodes, ftsText);
|
|
2115
|
+
parsedFiles.push(parsed);
|
|
2116
|
+
stats.filesParsed++;
|
|
2117
|
+
opts.onProgress?.({ path: d.path, done, total: discovered.length });
|
|
2118
|
+
}
|
|
2119
|
+
if (parsedFiles.length > 0) {
|
|
2120
|
+
const nameIndex = await this.store.getNameIndex();
|
|
2121
|
+
const resIndex = buildResolutionIndex(nameIndex);
|
|
2122
|
+
for (const parsed of parsedFiles) {
|
|
2123
|
+
const resolved = resolveFileEdges(parsed, resIndex);
|
|
2124
|
+
await this.store.replaceOutgoingEdges(resolved.srcKeys, resolved.edges);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (this.embedder && opts.embed) {
|
|
2128
|
+
const embeddable = await this.store.getNodesByKinds([...EMBEDDABLE_KINDS]);
|
|
2129
|
+
const existing = await this.store.getEmbeddingContentHashes();
|
|
2130
|
+
const pending = [];
|
|
2131
|
+
for (const n of embeddable) {
|
|
2132
|
+
const text = nodeEmbedText(n);
|
|
2133
|
+
const hash = hashContent(`${this.embedder.recipeVersion}\0${text}`);
|
|
2134
|
+
if (existing.get(n.nodeKey) !== hash)
|
|
2135
|
+
pending.push({ nodeKey: n.nodeKey, text, hash });
|
|
2136
|
+
}
|
|
2137
|
+
const BATCH = 64;
|
|
2138
|
+
for (let i = 0; i < pending.length; i += BATCH) {
|
|
2139
|
+
const batch = pending.slice(i, i + BATCH);
|
|
2140
|
+
const vecs = await this.embedder.embedDocuments(batch.map((b) => b.text));
|
|
2141
|
+
await this.store.upsertEmbeddings(batch.map((b, j) => ({
|
|
2142
|
+
nodeKey: b.nodeKey,
|
|
2143
|
+
dim: this.embedder.dim,
|
|
2144
|
+
vec: vecs[j],
|
|
2145
|
+
contentHash: b.hash
|
|
2146
|
+
})));
|
|
2147
|
+
const done2 = Math.min(i + BATCH, pending.length);
|
|
2148
|
+
opts.onProgress?.({ path: `embedding symbols`, done: done2, total: pending.length });
|
|
2149
|
+
}
|
|
2150
|
+
await this.store.pruneOrphanEmbeddings();
|
|
2151
|
+
await this.store.setMeta(META_EMBED_RECIPE_VERSION, this.embedder.recipeVersion);
|
|
2152
|
+
await this.store.setMeta(META_EMBED_MODEL, this.embedder.model);
|
|
2153
|
+
await this.store.setMeta(META_EMBED_DIM, String(this.embedder.dim));
|
|
2154
|
+
}
|
|
2155
|
+
await this.store.setMeta(META_SCHEMA_VERSION, SCHEMA_VERSION);
|
|
2156
|
+
await this.store.setMeta(META_LAST_INDEXED_AT, String(started));
|
|
2157
|
+
const head = headCommit(this.repoRoot);
|
|
2158
|
+
if (head)
|
|
2159
|
+
await this.store.setMeta(META_LAST_INDEX_COMMIT, head);
|
|
2160
|
+
stats.nodeCount = await this.store.countNodes();
|
|
2161
|
+
stats.edgeCount = await this.store.countEdges();
|
|
2162
|
+
stats.durationMs = (opts.now ?? Date.now()) - started;
|
|
2163
|
+
return stats;
|
|
2164
|
+
}
|
|
2165
|
+
async status() {
|
|
2166
|
+
await this.store.init();
|
|
2167
|
+
const fileCount = await this.store.countFiles();
|
|
2168
|
+
const langs = await this.store.languageDistribution();
|
|
2169
|
+
const languages = {};
|
|
2170
|
+
for (const l of langs)
|
|
2171
|
+
languages[l.language] = l.count;
|
|
2172
|
+
const lastAt = await this.store.getMeta(META_LAST_INDEXED_AT);
|
|
2173
|
+
return {
|
|
2174
|
+
indexed: fileCount > 0,
|
|
2175
|
+
fileCount,
|
|
2176
|
+
nodeCount: await this.store.countNodes(),
|
|
2177
|
+
edgeCount: await this.store.countEdges(),
|
|
2178
|
+
embeddingCount: await this.store.countEmbeddings(),
|
|
2179
|
+
lastIndexedAt: lastAt ? Number(lastAt) : null,
|
|
2180
|
+
lastIndexCommit: await this.store.getMeta(META_LAST_INDEX_COMMIT),
|
|
2181
|
+
schemaVersion: await this.store.getMeta(META_SCHEMA_VERSION),
|
|
2182
|
+
languages
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
async searchGraph(query, opts = {}) {
|
|
2186
|
+
await this.store.init();
|
|
2187
|
+
const limit = clampLimit(opts.limit, 20, 100);
|
|
2188
|
+
const hits = await this.store.searchFts(query, opts.kind ?? null, limit);
|
|
2189
|
+
const out = [];
|
|
2190
|
+
for (const h of hits) {
|
|
2191
|
+
const n = await this.store.getNode(h.nodeKey);
|
|
2192
|
+
if (n)
|
|
2193
|
+
out.push({ ...toHit(n), score: h.score });
|
|
2194
|
+
}
|
|
2195
|
+
return out;
|
|
2196
|
+
}
|
|
2197
|
+
async semanticQuery(query, opts = {}) {
|
|
2198
|
+
await this.store.init();
|
|
2199
|
+
if (!this.embedder)
|
|
2200
|
+
return [];
|
|
2201
|
+
const limit = clampLimit(opts.limit, 20, 100);
|
|
2202
|
+
const qvec = await this.embedder.embedQuery(query);
|
|
2203
|
+
const all = await this.store.allEmbeddings();
|
|
2204
|
+
const scored = all.map((e) => ({ nodeKey: e.nodeKey, score: cosineSimilarity(qvec, e.vec) })).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
2205
|
+
const out = [];
|
|
2206
|
+
for (const s of scored) {
|
|
2207
|
+
const n = await this.store.getNode(s.nodeKey);
|
|
2208
|
+
if (n)
|
|
2209
|
+
out.push({ ...toHit(n), score: s.score });
|
|
2210
|
+
}
|
|
2211
|
+
return out;
|
|
2212
|
+
}
|
|
2213
|
+
async tracePath(nodeKey, opts = {}) {
|
|
2214
|
+
await this.store.init();
|
|
2215
|
+
const direction = opts.direction ?? "out";
|
|
2216
|
+
const maxDepth = clampLimit(opts.maxDepth, 3, 5);
|
|
2217
|
+
const limit = clampLimit(opts.limit, 100, 1e3);
|
|
2218
|
+
const kinds = opts.kinds ?? null;
|
|
2219
|
+
const steps = [];
|
|
2220
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
2221
|
+
const visited = /* @__PURE__ */ new Set([nodeKey]);
|
|
2222
|
+
let frontier = [nodeKey];
|
|
2223
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
2224
|
+
const next = [];
|
|
2225
|
+
for (const key of frontier) {
|
|
2226
|
+
const edges = await this.store.neighbors(key, direction, kinds, limit);
|
|
2227
|
+
for (const e of edges) {
|
|
2228
|
+
const id = `${e.srcKey} ${e.dstKey} ${e.kind}`;
|
|
2229
|
+
if (seenEdge.has(id))
|
|
2230
|
+
continue;
|
|
2231
|
+
seenEdge.add(id);
|
|
2232
|
+
steps.push({ srcKey: e.srcKey, dstKey: e.dstKey, kind: e.kind });
|
|
2233
|
+
if (steps.length >= limit)
|
|
2234
|
+
return steps;
|
|
2235
|
+
const other = direction === "out" ? e.dstKey : e.srcKey;
|
|
2236
|
+
if (!visited.has(other)) {
|
|
2237
|
+
visited.add(other);
|
|
2238
|
+
next.push(other);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
frontier = next;
|
|
2243
|
+
}
|
|
2244
|
+
return steps;
|
|
2245
|
+
}
|
|
2246
|
+
async getCodeSnippet(nodeKey) {
|
|
2247
|
+
await this.store.init();
|
|
2248
|
+
const n = await this.store.getNode(nodeKey);
|
|
2249
|
+
if (!n)
|
|
2250
|
+
return null;
|
|
2251
|
+
let source = "";
|
|
2252
|
+
try {
|
|
2253
|
+
const full = readFileSync4(posix3.join(this.repoRoot, n.filePath), "utf8");
|
|
2254
|
+
source = full.slice(n.startByte, n.endByte);
|
|
2255
|
+
} catch {
|
|
2256
|
+
return null;
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
nodeKey: n.nodeKey,
|
|
2260
|
+
filePath: n.filePath,
|
|
2261
|
+
startLine: n.startLine,
|
|
2262
|
+
endLine: n.endLine,
|
|
2263
|
+
source
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
async getArchitecture(opts = {}) {
|
|
2267
|
+
await this.store.init();
|
|
2268
|
+
const kinds = await this.store.kindDistribution();
|
|
2269
|
+
const langs = await this.store.languageDistribution();
|
|
2270
|
+
const kindDistribution = {};
|
|
2271
|
+
for (const k of kinds)
|
|
2272
|
+
kindDistribution[k.kind] = k.count;
|
|
2273
|
+
const languageDistribution = {};
|
|
2274
|
+
for (const l of langs)
|
|
2275
|
+
languageDistribution[l.language] = l.count;
|
|
2276
|
+
return {
|
|
2277
|
+
fileCount: await this.store.countFiles(),
|
|
2278
|
+
nodeCount: await this.store.countNodes(),
|
|
2279
|
+
edgeCount: await this.store.countEdges(),
|
|
2280
|
+
kindDistribution,
|
|
2281
|
+
languageDistribution,
|
|
2282
|
+
topModules: await this.store.topModulesByFanIn(clampLimit(opts.topN, 10, 50))
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
async changedSymbols(opts = {}) {
|
|
2286
|
+
await this.store.init();
|
|
2287
|
+
const files = changedSourceFiles(this.repoRoot, opts);
|
|
2288
|
+
if (files === null) {
|
|
2289
|
+
throw new Error("changedSymbols requires a git repository (change detection is git-based)");
|
|
2290
|
+
}
|
|
2291
|
+
const limit = clampLimit(opts.limit, 64, 200);
|
|
2292
|
+
const symbols = [];
|
|
2293
|
+
let indexedFiles = 0;
|
|
2294
|
+
for (const f of files) {
|
|
2295
|
+
const rows = await this.store.getNodesByFile(f.path);
|
|
2296
|
+
if (rows.length > 0)
|
|
2297
|
+
indexedFiles++;
|
|
2298
|
+
symbols.push(...rows);
|
|
2299
|
+
}
|
|
2300
|
+
symbols.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
|
|
2301
|
+
return {
|
|
2302
|
+
changedFiles: files.length,
|
|
2303
|
+
indexedFiles,
|
|
2304
|
+
symbols: symbols.slice(0, limit),
|
|
2305
|
+
truncated: symbols.length > limit
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
async reviewChanges(opts = {}) {
|
|
2309
|
+
await this.store.init();
|
|
2310
|
+
const rawPaths = enumerateChangedPaths(this.repoRoot, opts);
|
|
2311
|
+
if (rawPaths === null) {
|
|
2312
|
+
throw new Error("reviewChanges requires a git repository (change detection is git-based)");
|
|
2313
|
+
}
|
|
2314
|
+
const hunksRes = changedHunks(this.repoRoot, opts);
|
|
2315
|
+
const hunks = hunksRes?.files ?? /* @__PURE__ */ new Map();
|
|
2316
|
+
let hunkCoverage = hunksRes?.coverage ?? "none";
|
|
2317
|
+
const riskFlags = scanRiskFlags({
|
|
2318
|
+
paths: rawPaths,
|
|
2319
|
+
hunks,
|
|
2320
|
+
readUntracked: (p) => this.readRepoFileHead(p, MAX_UNTRACKED_HEAD_BYTES)
|
|
2321
|
+
});
|
|
2322
|
+
const files = classifyPaths(rawPaths.keys());
|
|
2323
|
+
const limit = clampLimit(opts.limit, 64, 200);
|
|
2324
|
+
const symbols = [];
|
|
2325
|
+
let indexedFiles = 0;
|
|
2326
|
+
let staleFiles = 0;
|
|
2327
|
+
for (const f of files) {
|
|
2328
|
+
const rows = await this.store.getNodesByFile(f.path);
|
|
2329
|
+
if (rows.length === 0)
|
|
2330
|
+
continue;
|
|
2331
|
+
indexedFiles++;
|
|
2332
|
+
const fileRow = await this.store.getFile(f.path);
|
|
2333
|
+
const content = this.readRepoFile(f.path);
|
|
2334
|
+
const stale = fileRow !== null && content !== null && hashContent(content) !== fileRow.contentHash;
|
|
2335
|
+
if (stale) {
|
|
2336
|
+
staleFiles++;
|
|
2337
|
+
if (hunkCoverage === "full")
|
|
2338
|
+
hunkCoverage = "partial";
|
|
2339
|
+
}
|
|
2340
|
+
const fileStatus = rawPaths.get(f.path) ?? "modified";
|
|
2341
|
+
const fh = hunks.get(f.path);
|
|
2342
|
+
for (const row of rows) {
|
|
2343
|
+
symbols.push(classifyReviewSymbol(row, fileStatus, fh, stale));
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
symbols.sort((a, b) => changeRank(a.change) - changeRank(b.change) || a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
|
|
2347
|
+
return {
|
|
2348
|
+
changedFiles: files.length,
|
|
2349
|
+
indexedFiles,
|
|
2350
|
+
staleFiles,
|
|
2351
|
+
symbols: symbols.slice(0, limit),
|
|
2352
|
+
truncated: symbols.length > limit,
|
|
2353
|
+
riskFlags,
|
|
2354
|
+
hunkCoverage,
|
|
2355
|
+
baseKind: opts.since ? "ref" : "uncommitted"
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
async close() {
|
|
2359
|
+
await this.store.close();
|
|
2360
|
+
}
|
|
2361
|
+
// ── internal ──
|
|
2362
|
+
readRepoFileHead(relPath, maxBytes) {
|
|
2363
|
+
const content = this.readRepoFile(relPath);
|
|
2364
|
+
return content === null ? null : content.slice(0, maxBytes);
|
|
2365
|
+
}
|
|
2366
|
+
readRepoFile(relPath) {
|
|
2367
|
+
try {
|
|
2368
|
+
return readFileSync4(posix3.join(this.repoRoot, relPath), "utf8");
|
|
2369
|
+
} catch {
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
async wipe() {
|
|
2374
|
+
for (const f of await this.store.getAllFiles()) {
|
|
2375
|
+
await this.store.deleteFile(f.path);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
function toHit(n) {
|
|
2380
|
+
return {
|
|
2381
|
+
nodeKey: n.nodeKey,
|
|
2382
|
+
kind: n.kind,
|
|
2383
|
+
name: n.name,
|
|
2384
|
+
qualifiedName: n.qualifiedName,
|
|
2385
|
+
filePath: n.filePath,
|
|
2386
|
+
language: n.language,
|
|
2387
|
+
startLine: n.startLine,
|
|
2388
|
+
endLine: n.endLine
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
function clampLimit(v, dflt, max) {
|
|
2392
|
+
if (v === void 0 || !Number.isFinite(v))
|
|
2393
|
+
return dflt;
|
|
2394
|
+
return Math.max(1, Math.min(max, Math.floor(v)));
|
|
2395
|
+
}
|
|
2396
|
+
function changeRank(c) {
|
|
2397
|
+
return c === "added" ? 0 : c === "modified" ? 1 : 2;
|
|
2398
|
+
}
|
|
2399
|
+
function classifyReviewSymbol(row, fileStatus, fh, stale) {
|
|
2400
|
+
if (stale)
|
|
2401
|
+
return { ...row, change: "co_located", changedLineCount: 0, staleIndex: true };
|
|
2402
|
+
if (fileStatus === "added" || fh?.wholeFile) {
|
|
2403
|
+
return {
|
|
2404
|
+
...row,
|
|
2405
|
+
change: "added",
|
|
2406
|
+
changedLineCount: row.endLine - row.startLine + 1,
|
|
2407
|
+
staleIndex: false
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
if (!fh || fh.degraded || fh.binary) {
|
|
2411
|
+
return { ...row, change: "co_located", changedLineCount: 0, staleIndex: false };
|
|
2412
|
+
}
|
|
2413
|
+
let overlap = 0;
|
|
2414
|
+
for (const r of fh.addedRanges) {
|
|
2415
|
+
const lo = Math.max(r.start, row.startLine);
|
|
2416
|
+
const hi = Math.min(r.end, row.endLine);
|
|
2417
|
+
if (hi >= lo)
|
|
2418
|
+
overlap += hi - lo + 1;
|
|
2419
|
+
}
|
|
2420
|
+
const deletionTouch = fh.deletionPoints.some((c) => c >= row.startLine - 1 && c <= row.endLine);
|
|
2421
|
+
if (overlap > 0 || deletionTouch) {
|
|
2422
|
+
return { ...row, change: "modified", changedLineCount: overlap, staleIndex: false };
|
|
2423
|
+
}
|
|
2424
|
+
return { ...row, change: "co_located", changedLineCount: 0, staleIndex: false };
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// ../../packages/code-index/build/store/node-sqlite.js
|
|
2428
|
+
import Database from "better-sqlite3";
|
|
2429
|
+
function toNodeRow(r) {
|
|
2430
|
+
return {
|
|
2431
|
+
nodeKey: r.node_key,
|
|
2432
|
+
kind: r.kind,
|
|
2433
|
+
name: r.name,
|
|
2434
|
+
qualifiedName: r.qualified_name,
|
|
2435
|
+
filePath: r.file_path,
|
|
2436
|
+
language: r.language,
|
|
2437
|
+
startLine: r.start_line,
|
|
2438
|
+
endLine: r.end_line,
|
|
2439
|
+
startByte: r.start_byte,
|
|
2440
|
+
endByte: r.end_byte,
|
|
2441
|
+
signature: r.signature,
|
|
2442
|
+
doc: r.doc
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
var CHUNK = 400;
|
|
2446
|
+
var NodeSqliteStore = class {
|
|
2447
|
+
db;
|
|
2448
|
+
constructor(dbPath) {
|
|
2449
|
+
this.db = new Database(dbPath);
|
|
2450
|
+
this.db.pragma("journal_mode = WAL");
|
|
2451
|
+
this.db.pragma("foreign_keys = ON");
|
|
2452
|
+
}
|
|
2453
|
+
async init() {
|
|
2454
|
+
this.db.exec(SCHEMA_SQL);
|
|
2455
|
+
}
|
|
2456
|
+
async close() {
|
|
2457
|
+
this.db.close();
|
|
2458
|
+
}
|
|
2459
|
+
async getMeta(key) {
|
|
2460
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
2461
|
+
return row?.value ?? null;
|
|
2462
|
+
}
|
|
2463
|
+
async setMeta(key, value2) {
|
|
2464
|
+
this.db.prepare("INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value2);
|
|
2465
|
+
}
|
|
2466
|
+
async getAllFiles() {
|
|
2467
|
+
const rows = this.db.prepare("SELECT * FROM files").all();
|
|
2468
|
+
return rows.map((r) => ({
|
|
2469
|
+
path: r.path,
|
|
2470
|
+
language: r.language,
|
|
2471
|
+
contentHash: r.content_hash,
|
|
2472
|
+
mtimeMs: r.mtime_ms,
|
|
2473
|
+
size: r.size,
|
|
2474
|
+
indexedAt: r.indexed_at
|
|
2475
|
+
}));
|
|
2476
|
+
}
|
|
2477
|
+
async getFile(path) {
|
|
2478
|
+
const r = this.db.prepare("SELECT * FROM files WHERE path = ?").get(path);
|
|
2479
|
+
if (!r)
|
|
2480
|
+
return null;
|
|
2481
|
+
return {
|
|
2482
|
+
path: r.path,
|
|
2483
|
+
language: r.language,
|
|
2484
|
+
contentHash: r.content_hash,
|
|
2485
|
+
mtimeMs: r.mtime_ms,
|
|
2486
|
+
size: r.size,
|
|
2487
|
+
indexedAt: r.indexed_at
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
async upsertFile(row) {
|
|
2491
|
+
this.upsertFileStmt().run(row.path, row.language, row.contentHash, row.mtimeMs, row.size, row.indexedAt);
|
|
2492
|
+
}
|
|
2493
|
+
async replaceFileNodes(file, nodes, ftsText) {
|
|
2494
|
+
const tx = this.db.transaction(() => {
|
|
2495
|
+
this.deleteFileGraph(file.path);
|
|
2496
|
+
const insNode = this.insertNodeStmt();
|
|
2497
|
+
const insFts = this.insertFtsStmt();
|
|
2498
|
+
for (const n of nodes) {
|
|
2499
|
+
insNode.run(n.nodeKey, n.kind, n.name, n.qualifiedName, n.filePath, n.language, n.startLine, n.endLine, n.startByte, n.endByte, n.signature, n.doc);
|
|
2500
|
+
insFts.run(n.nodeKey, ftsText.get(n.nodeKey) ?? n.name);
|
|
2501
|
+
}
|
|
2502
|
+
this.upsertFileStmt().run(file.path, file.language, file.contentHash, file.mtimeMs, file.size, file.indexedAt);
|
|
2503
|
+
});
|
|
2504
|
+
tx();
|
|
2505
|
+
}
|
|
2506
|
+
async deleteFile(path) {
|
|
2507
|
+
const tx = this.db.transaction(() => {
|
|
2508
|
+
this.deleteFileGraph(path);
|
|
2509
|
+
this.db.prepare("DELETE FROM files WHERE path = ?").run(path);
|
|
2510
|
+
});
|
|
2511
|
+
tx();
|
|
2512
|
+
}
|
|
2513
|
+
async replaceOutgoingEdges(srcKeys, edges) {
|
|
2514
|
+
const tx = this.db.transaction(() => {
|
|
2515
|
+
const del = this.db.prepare("DELETE FROM edges WHERE src_key = ?");
|
|
2516
|
+
for (const k of srcKeys)
|
|
2517
|
+
del.run(k);
|
|
2518
|
+
const ins = this.db.prepare("INSERT OR IGNORE INTO edges(src_key, dst_key, kind) VALUES(?, ?, ?)");
|
|
2519
|
+
for (const e of edges)
|
|
2520
|
+
ins.run(e.srcKey, e.dstKey, e.kind);
|
|
2521
|
+
});
|
|
2522
|
+
tx();
|
|
2523
|
+
}
|
|
2524
|
+
async getNameIndex() {
|
|
2525
|
+
const rows = this.db.prepare("SELECT node_key, name, kind, file_path FROM nodes").all();
|
|
2526
|
+
return rows.map((r) => ({
|
|
2527
|
+
nodeKey: r.node_key,
|
|
2528
|
+
name: r.name,
|
|
2529
|
+
kind: r.kind,
|
|
2530
|
+
filePath: r.file_path
|
|
2531
|
+
}));
|
|
2532
|
+
}
|
|
2533
|
+
async getNode(nodeKey) {
|
|
2534
|
+
const r = this.db.prepare("SELECT * FROM nodes WHERE node_key = ?").get(nodeKey);
|
|
2535
|
+
return r ? toNodeRow(r) : null;
|
|
2536
|
+
}
|
|
2537
|
+
async getNodesByFile(path) {
|
|
2538
|
+
const rows = this.db.prepare("SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line").all(path);
|
|
2539
|
+
return rows.map(toNodeRow);
|
|
2540
|
+
}
|
|
2541
|
+
async getNodesByKinds(kinds) {
|
|
2542
|
+
if (kinds.length === 0)
|
|
2543
|
+
return [];
|
|
2544
|
+
const rows = this.db.prepare(`SELECT * FROM nodes WHERE kind IN (${kinds.map(() => "?").join(",")})`).all(...kinds);
|
|
2545
|
+
return rows.map(toNodeRow);
|
|
2546
|
+
}
|
|
2547
|
+
async getAllNodes() {
|
|
2548
|
+
const rows = this.db.prepare("SELECT * FROM nodes").all();
|
|
2549
|
+
return rows.map(toNodeRow);
|
|
2550
|
+
}
|
|
2551
|
+
async getAllEdges() {
|
|
2552
|
+
const rows = this.db.prepare("SELECT src_key, dst_key, kind FROM edges").all();
|
|
2553
|
+
return rows.map((r) => ({
|
|
2554
|
+
srcKey: r.src_key,
|
|
2555
|
+
dstKey: r.dst_key,
|
|
2556
|
+
kind: r.kind
|
|
2557
|
+
}));
|
|
2558
|
+
}
|
|
2559
|
+
async upsertEmbeddings(rows) {
|
|
2560
|
+
const stmt = this.db.prepare("INSERT OR REPLACE INTO embeddings(node_key, dim, vec, content_hash) VALUES(?, ?, ?, ?)");
|
|
2561
|
+
const tx = this.db.transaction(() => {
|
|
2562
|
+
for (const r of rows) {
|
|
2563
|
+
const buf = Buffer.from(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength);
|
|
2564
|
+
stmt.run(r.nodeKey, r.dim, buf, r.contentHash);
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
tx();
|
|
2568
|
+
}
|
|
2569
|
+
async getEmbeddingContentHashes() {
|
|
2570
|
+
const rows = this.db.prepare("SELECT node_key, content_hash FROM embeddings").all();
|
|
2571
|
+
return new Map(rows.map((r) => [r.node_key, r.content_hash]));
|
|
2572
|
+
}
|
|
2573
|
+
async allEmbeddings() {
|
|
2574
|
+
const rows = this.db.prepare("SELECT node_key, vec FROM embeddings").all();
|
|
2575
|
+
return rows.map((r) => ({
|
|
2576
|
+
nodeKey: r.node_key,
|
|
2577
|
+
vec: new Float32Array(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength / 4)
|
|
2578
|
+
}));
|
|
2579
|
+
}
|
|
2580
|
+
async pruneOrphanEmbeddings() {
|
|
2581
|
+
const info = this.db.prepare("DELETE FROM embeddings WHERE node_key NOT IN (SELECT node_key FROM nodes)").run();
|
|
2582
|
+
return info.changes;
|
|
2583
|
+
}
|
|
2584
|
+
async countEmbeddings() {
|
|
2585
|
+
return this.db.prepare("SELECT COUNT(*) AS c FROM embeddings").get().c;
|
|
2586
|
+
}
|
|
2587
|
+
async searchFts(query, kind, limit) {
|
|
2588
|
+
const match = toFtsMatchQuery(query);
|
|
2589
|
+
if (!match)
|
|
2590
|
+
return [];
|
|
2591
|
+
const sql = kind ? `SELECT f.node_key AS node_key, bm25(nodes_fts) AS score
|
|
2592
|
+
FROM nodes_fts f JOIN nodes n ON n.node_key = f.node_key
|
|
2593
|
+
WHERE nodes_fts MATCH ? AND n.kind = ?
|
|
2594
|
+
ORDER BY score LIMIT ?` : `SELECT f.node_key AS node_key, bm25(nodes_fts) AS score
|
|
2595
|
+
FROM nodes_fts f JOIN nodes n ON n.node_key = f.node_key
|
|
2596
|
+
WHERE nodes_fts MATCH ?
|
|
2597
|
+
ORDER BY score LIMIT ?`;
|
|
2598
|
+
const stmt = this.db.prepare(sql);
|
|
2599
|
+
const rows = kind ? stmt.all(match, kind, limit) : stmt.all(match, limit);
|
|
2600
|
+
return rows.map((r) => ({ nodeKey: r.node_key, score: -r.score }));
|
|
2601
|
+
}
|
|
2602
|
+
async neighbors(nodeKey, direction, kinds, limit) {
|
|
2603
|
+
const col = direction === "out" ? "src_key" : "dst_key";
|
|
2604
|
+
let sql = `SELECT src_key, dst_key, kind FROM edges WHERE ${col} = ?`;
|
|
2605
|
+
const params = [nodeKey];
|
|
2606
|
+
if (kinds && kinds.length > 0) {
|
|
2607
|
+
sql += ` AND kind IN (${kinds.map(() => "?").join(",")})`;
|
|
2608
|
+
params.push(...kinds);
|
|
2609
|
+
}
|
|
2610
|
+
sql += " LIMIT ?";
|
|
2611
|
+
params.push(limit);
|
|
2612
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
2613
|
+
return rows.map((r) => ({
|
|
2614
|
+
srcKey: r.src_key,
|
|
2615
|
+
dstKey: r.dst_key,
|
|
2616
|
+
kind: r.kind
|
|
2617
|
+
}));
|
|
2618
|
+
}
|
|
2619
|
+
async countFiles() {
|
|
2620
|
+
return this.db.prepare("SELECT COUNT(*) AS c FROM files").get().c;
|
|
2621
|
+
}
|
|
2622
|
+
async countNodes() {
|
|
2623
|
+
return this.db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
|
|
2624
|
+
}
|
|
2625
|
+
async countEdges() {
|
|
2626
|
+
return this.db.prepare("SELECT COUNT(*) AS c FROM edges").get().c;
|
|
2627
|
+
}
|
|
2628
|
+
async kindDistribution() {
|
|
2629
|
+
const rows = this.db.prepare("SELECT kind, COUNT(*) AS c FROM nodes GROUP BY kind ORDER BY c DESC").all();
|
|
2630
|
+
return rows.map((r) => ({ kind: r.kind, count: r.c }));
|
|
2631
|
+
}
|
|
2632
|
+
async languageDistribution() {
|
|
2633
|
+
const rows = this.db.prepare("SELECT language, COUNT(*) AS c FROM files GROUP BY language ORDER BY c DESC").all();
|
|
2634
|
+
return rows.map((r) => ({ language: r.language, count: r.c }));
|
|
2635
|
+
}
|
|
2636
|
+
async topModulesByFanIn(limit) {
|
|
2637
|
+
const rows = this.db.prepare(`SELECT n.file_path AS file_path, n.language AS language,
|
|
2638
|
+
(SELECT COUNT(*) FROM nodes n2 WHERE n2.file_path = n.file_path) AS symbol_count,
|
|
2639
|
+
(SELECT COUNT(*) FROM edges e WHERE e.src_key = n.node_key AND e.kind = 'imports') AS imports_out,
|
|
2640
|
+
(SELECT COUNT(*) FROM edges e WHERE e.dst_key = n.node_key AND e.kind = 'imports') AS imports_in
|
|
2641
|
+
FROM nodes n
|
|
2642
|
+
WHERE n.kind = 'module'
|
|
2643
|
+
ORDER BY imports_in DESC, symbol_count DESC
|
|
2644
|
+
LIMIT ?`).all(limit);
|
|
2645
|
+
return rows.map((r) => ({
|
|
2646
|
+
filePath: r.file_path,
|
|
2647
|
+
language: r.language,
|
|
2648
|
+
symbolCount: r.symbol_count,
|
|
2649
|
+
importsOut: r.imports_out,
|
|
2650
|
+
importsIn: r.imports_in
|
|
2651
|
+
}));
|
|
2652
|
+
}
|
|
2653
|
+
// ── internal helpers ──
|
|
2654
|
+
deleteFileGraph(path) {
|
|
2655
|
+
const keys = this.db.prepare("SELECT node_key FROM nodes WHERE file_path = ?").all(path).map((r) => r.node_key);
|
|
2656
|
+
const delEdge = this.db.prepare("DELETE FROM edges WHERE src_key = ?");
|
|
2657
|
+
const delFts = this.db.prepare("DELETE FROM nodes_fts WHERE node_key = ?");
|
|
2658
|
+
for (let i = 0; i < keys.length; i += CHUNK) {
|
|
2659
|
+
for (const k of keys.slice(i, i + CHUNK)) {
|
|
2660
|
+
delEdge.run(k);
|
|
2661
|
+
delFts.run(k);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
this.db.prepare("DELETE FROM nodes WHERE file_path = ?").run(path);
|
|
2665
|
+
}
|
|
2666
|
+
_insNode = null;
|
|
2667
|
+
insertNodeStmt() {
|
|
2668
|
+
if (!this._insNode) {
|
|
2669
|
+
this._insNode = this.db.prepare(`INSERT OR REPLACE INTO nodes(
|
|
2670
|
+
node_key, kind, name, qualified_name, file_path, language,
|
|
2671
|
+
start_line, end_line, start_byte, end_byte, signature, doc
|
|
2672
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
2673
|
+
}
|
|
2674
|
+
return this._insNode;
|
|
2675
|
+
}
|
|
2676
|
+
_insFts = null;
|
|
2677
|
+
insertFtsStmt() {
|
|
2678
|
+
if (!this._insFts) {
|
|
2679
|
+
this._insFts = this.db.prepare("INSERT INTO nodes_fts(node_key, search_text) VALUES(?, ?)");
|
|
2680
|
+
}
|
|
2681
|
+
return this._insFts;
|
|
2682
|
+
}
|
|
2683
|
+
_upsertFile = null;
|
|
2684
|
+
upsertFileStmt() {
|
|
2685
|
+
if (!this._upsertFile) {
|
|
2686
|
+
this._upsertFile = this.db.prepare(`INSERT INTO files(path, language, content_hash, mtime_ms, size, indexed_at)
|
|
2687
|
+
VALUES(?, ?, ?, ?, ?, ?)
|
|
2688
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
2689
|
+
language = excluded.language,
|
|
2690
|
+
content_hash = excluded.content_hash,
|
|
2691
|
+
mtime_ms = excluded.mtime_ms,
|
|
2692
|
+
size = excluded.size,
|
|
2693
|
+
indexed_at = excluded.indexed_at`);
|
|
2694
|
+
}
|
|
2695
|
+
return this._upsertFile;
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
|
|
2699
|
+
// ../../packages/code-index/build/sync/types.js
|
|
2700
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2701
|
+
function sha2562(input) {
|
|
2702
|
+
return createHash4("sha256").update(input, "utf8").digest("hex");
|
|
2703
|
+
}
|
|
2704
|
+
function toCloudNode(n) {
|
|
2705
|
+
return {
|
|
2706
|
+
node_key_hash: sha2562(n.nodeKey),
|
|
2707
|
+
name: n.name,
|
|
2708
|
+
qualified_name: n.qualifiedName,
|
|
2709
|
+
path_hash: sha2562(n.filePath),
|
|
2710
|
+
kind: n.kind,
|
|
2711
|
+
language: n.language,
|
|
2712
|
+
start_line: n.startLine,
|
|
2713
|
+
end_line: n.endLine,
|
|
2714
|
+
signature_hash: n.signature ? sha2562(n.signature) : null
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function toCloudEdge(e) {
|
|
2718
|
+
return {
|
|
2719
|
+
src_key_hash: sha2562(e.srcKey),
|
|
2720
|
+
dst_key_hash: sha2562(e.dstKey),
|
|
2721
|
+
kind: e.kind
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
function toCloudEmbedding(v, contentHash, meta) {
|
|
2725
|
+
return {
|
|
2726
|
+
node_key_hash: sha2562(v.nodeKey),
|
|
2727
|
+
embedding: Array.from(v.vec),
|
|
2728
|
+
recipe_version: meta.recipeVersion,
|
|
2729
|
+
dim: meta.dim,
|
|
2730
|
+
model: meta.model,
|
|
2731
|
+
content_hash: contentHash
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
// ../../packages/code-index/build/sync/client.js
|
|
2736
|
+
var SyncError = class extends Error {
|
|
2737
|
+
status;
|
|
2738
|
+
code;
|
|
2739
|
+
constructor(message, status2, code) {
|
|
2740
|
+
super(message);
|
|
2741
|
+
this.status = status2;
|
|
2742
|
+
this.code = code;
|
|
2743
|
+
this.name = "SyncError";
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
var CodeIndexSync = class {
|
|
2747
|
+
store;
|
|
2748
|
+
cfg;
|
|
2749
|
+
base;
|
|
2750
|
+
chunk;
|
|
2751
|
+
fetchImpl;
|
|
2752
|
+
constructor(store, cfg) {
|
|
2753
|
+
this.store = store;
|
|
2754
|
+
this.cfg = cfg;
|
|
2755
|
+
this.base = cfg.endpoint.replace(/\/+$/, "");
|
|
2756
|
+
this.chunk = cfg.chunkSize && cfg.chunkSize > 0 ? cfg.chunkSize : 1e3;
|
|
2757
|
+
this.fetchImpl = cfg.fetchImpl ?? fetch;
|
|
2758
|
+
}
|
|
2759
|
+
/** Close the underlying store handle. */
|
|
2760
|
+
async close() {
|
|
2761
|
+
await this.store.close();
|
|
2762
|
+
}
|
|
2763
|
+
/** Enable/disable the opt-in consent (CLI `maind sync enable`). */
|
|
2764
|
+
async setConsent(enabled, policyVersion) {
|
|
2765
|
+
await this.post("/api/codebase/consent", {
|
|
2766
|
+
enabled,
|
|
2767
|
+
policy_version: policyVersion ?? null
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
/** Cloud sync status for this repo (`maind sync status`). */
|
|
2771
|
+
async remoteStatus() {
|
|
2772
|
+
const qs = new URLSearchParams({ project_key: this.cfg.projectKey });
|
|
2773
|
+
const json = await this.get(`/api/codebase/status?${qs.toString()}`);
|
|
2774
|
+
return json;
|
|
2775
|
+
}
|
|
2776
|
+
/** Local index status (for the CLI to decide whether there's anything to push). */
|
|
2777
|
+
async localStatus() {
|
|
2778
|
+
await this.store.init();
|
|
2779
|
+
return {
|
|
2780
|
+
indexed: await this.store.countFiles() > 0,
|
|
2781
|
+
nodeCount: await this.store.countNodes(),
|
|
2782
|
+
edgeCount: await this.store.countEdges(),
|
|
2783
|
+
embeddingCount: await this.store.countEmbeddings(),
|
|
2784
|
+
fileCount: await this.store.countFiles(),
|
|
2785
|
+
headCommit: await this.store.getMeta(META_LAST_INDEX_COMMIT),
|
|
2786
|
+
lastSyncedAt: numOrNull(await this.store.getMeta(META_LAST_SYNCED_AT))
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Full push: upsert every local node + edge, then prune cloud rows that no
|
|
2791
|
+
* longer exist locally (deletions reconcile). Incremental-by-commit is a later
|
|
2792
|
+
* optimization; a full upsert is idempotent and fine at repo scale.
|
|
2793
|
+
*/
|
|
2794
|
+
async push() {
|
|
2795
|
+
await this.store.init();
|
|
2796
|
+
const nodes = (await this.store.getAllNodes()).map(toCloudNode);
|
|
2797
|
+
const edges = (await this.store.getAllEdges()).map(toCloudEdge);
|
|
2798
|
+
const schemaVersion = await this.store.getMeta(META_SCHEMA_VERSION) ?? SCHEMA_VERSION;
|
|
2799
|
+
const headCommit2 = await this.store.getMeta(META_LAST_INDEX_COMMIT);
|
|
2800
|
+
const fileCount = await this.store.countFiles();
|
|
2801
|
+
const langDist = Object.fromEntries((await this.store.languageDistribution()).map((l) => [l.language, l.count]));
|
|
2802
|
+
const manifest = {
|
|
2803
|
+
project_key: this.cfg.projectKey,
|
|
2804
|
+
head_commit: headCommit2,
|
|
2805
|
+
schema_version: schemaVersion,
|
|
2806
|
+
file_count: fileCount,
|
|
2807
|
+
lang_dist: langDist
|
|
2808
|
+
};
|
|
2809
|
+
let phaseDone = 0;
|
|
2810
|
+
const nodeChunks = chunkArray(nodes, this.chunk);
|
|
2811
|
+
const edgeChunks = chunkArray(edges, this.chunk * 2);
|
|
2812
|
+
const totalChunks = Math.max(1, nodeChunks.length + edgeChunks.length);
|
|
2813
|
+
for (const chunk of nodeChunks) {
|
|
2814
|
+
await this.post("/api/codebase/push", { ...manifest, nodes: chunk, edges: [] });
|
|
2815
|
+
this.cfg.onProgress?.({ phase: "nodes", done: ++phaseDone, total: totalChunks });
|
|
2816
|
+
}
|
|
2817
|
+
for (const chunk of edgeChunks) {
|
|
2818
|
+
await this.post("/api/codebase/push", { ...manifest, nodes: [], edges: chunk });
|
|
2819
|
+
this.cfg.onProgress?.({ phase: "edges", done: ++phaseDone, total: totalChunks });
|
|
2820
|
+
}
|
|
2821
|
+
if (nodeChunks.length === 0 && edgeChunks.length === 0) {
|
|
2822
|
+
await this.post("/api/codebase/push", { ...manifest, nodes: [], edges: [] });
|
|
2823
|
+
}
|
|
2824
|
+
const keep = nodes.map((n) => n.node_key_hash);
|
|
2825
|
+
const pruneRes = await this.post("/api/codebase/prune", {
|
|
2826
|
+
project_key: this.cfg.projectKey,
|
|
2827
|
+
keep_node_key_hashes: keep
|
|
2828
|
+
});
|
|
2829
|
+
this.cfg.onProgress?.({ phase: "prune", done: totalChunks, total: totalChunks });
|
|
2830
|
+
const embeddings = await this.store.countEmbeddings() > 0 ? await this.pushEmbeddings() : 0;
|
|
2831
|
+
await this.store.setMeta(META_LAST_SYNCED_AT, String(Date.now()));
|
|
2832
|
+
if (headCommit2)
|
|
2833
|
+
await this.store.setMeta(META_LAST_SYNCED_COMMIT, headCommit2);
|
|
2834
|
+
return {
|
|
2835
|
+
nodes: nodes.length,
|
|
2836
|
+
edges: edges.length,
|
|
2837
|
+
pruned: numOr0(pruneRes.nodes_deleted),
|
|
2838
|
+
embeddings,
|
|
2839
|
+
headCommit: headCommit2
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Push symbol vectors (ADR-220). Diffs local content-hashes against the
|
|
2844
|
+
* remote map (get_codebase_embedding_hashes) and uploads only new/changed
|
|
2845
|
+
* vectors — a full re-upload would be MBs on every sync. Returns the number
|
|
2846
|
+
* of vectors uploaded. Requires the embedder identity recorded by
|
|
2847
|
+
* `index --embed` (META_EMBED_*); older indexes must re-run it once.
|
|
2848
|
+
*/
|
|
2849
|
+
async pushEmbeddings() {
|
|
2850
|
+
await this.store.init();
|
|
2851
|
+
const localHashes = await this.store.getEmbeddingContentHashes();
|
|
2852
|
+
if (localHashes.size === 0)
|
|
2853
|
+
return 0;
|
|
2854
|
+
const meta = await this.embedMeta();
|
|
2855
|
+
const qs = new URLSearchParams({ project_key: this.cfg.projectKey });
|
|
2856
|
+
const remote = await this.get(`/api/codebase/embedding-hashes?${qs.toString()}`);
|
|
2857
|
+
const remoteHashes = remote.hashes ?? {};
|
|
2858
|
+
const pending = (await this.store.allEmbeddings()).map((v) => toCloudEmbedding(v, localHashes.get(v.nodeKey) ?? "", meta)).filter((c) => c.content_hash !== "" && remoteHashes[c.node_key_hash] !== c.content_hash);
|
|
2859
|
+
if (pending.length === 0)
|
|
2860
|
+
return 0;
|
|
2861
|
+
const chunks = chunkArray(pending, EMBED_CHUNK);
|
|
2862
|
+
let done = 0;
|
|
2863
|
+
for (const chunk of chunks) {
|
|
2864
|
+
await this.post("/api/codebase/push-embeddings", {
|
|
2865
|
+
project_key: this.cfg.projectKey,
|
|
2866
|
+
embeddings: chunk
|
|
2867
|
+
});
|
|
2868
|
+
this.cfg.onProgress?.({ phase: "embeddings", done: ++done, total: chunks.length });
|
|
2869
|
+
}
|
|
2870
|
+
return pending.length;
|
|
2871
|
+
}
|
|
2872
|
+
/** The embedder identity recorded at `index --embed` time. */
|
|
2873
|
+
async embedMeta() {
|
|
2874
|
+
const recipeVersion = await this.store.getMeta(META_EMBED_RECIPE_VERSION);
|
|
2875
|
+
const model = await this.store.getMeta(META_EMBED_MODEL);
|
|
2876
|
+
const dim = numOrNull(await this.store.getMeta(META_EMBED_DIM));
|
|
2877
|
+
if (!recipeVersion || !model || !dim) {
|
|
2878
|
+
throw new SyncError("local embeddings lack embedder metadata \u2014 re-run `maind index --embed` once to record it", 0, "missing_embed_meta");
|
|
2879
|
+
}
|
|
2880
|
+
return { recipeVersion, model, dim };
|
|
2881
|
+
}
|
|
2882
|
+
// ── HTTP ──
|
|
2883
|
+
headers() {
|
|
2884
|
+
const h = {
|
|
2885
|
+
"content-type": "application/json",
|
|
2886
|
+
authorization: `Bearer ${this.cfg.apiKey}`
|
|
2887
|
+
};
|
|
2888
|
+
if (this.cfg.deviceId)
|
|
2889
|
+
h["x-maind-device-id"] = this.cfg.deviceId;
|
|
2890
|
+
return h;
|
|
2891
|
+
}
|
|
2892
|
+
async post(path, body) {
|
|
2893
|
+
const res = await this.fetchImpl(`${this.base}${path}`, {
|
|
2894
|
+
method: "POST",
|
|
2895
|
+
headers: this.headers(),
|
|
2896
|
+
body: JSON.stringify(body)
|
|
2897
|
+
});
|
|
2898
|
+
return this.unwrap(res, path);
|
|
2899
|
+
}
|
|
2900
|
+
async get(path) {
|
|
2901
|
+
const res = await this.fetchImpl(`${this.base}${path}`, {
|
|
2902
|
+
method: "GET",
|
|
2903
|
+
headers: this.headers()
|
|
2904
|
+
});
|
|
2905
|
+
return this.unwrap(res, path);
|
|
2906
|
+
}
|
|
2907
|
+
async unwrap(res, path) {
|
|
2908
|
+
const text = await res.text().catch(() => "");
|
|
2909
|
+
let json = null;
|
|
2910
|
+
try {
|
|
2911
|
+
json = text ? JSON.parse(text) : null;
|
|
2912
|
+
} catch {
|
|
2913
|
+
json = null;
|
|
2914
|
+
}
|
|
2915
|
+
if (!res.ok || !json || json.ok !== true) {
|
|
2916
|
+
const code = json?.error;
|
|
2917
|
+
throw new SyncError(`${path} failed (HTTP ${res.status})${code ? `: ${code}` : ""}`, res.status, code);
|
|
2918
|
+
}
|
|
2919
|
+
return json;
|
|
2920
|
+
}
|
|
2921
|
+
};
|
|
2922
|
+
var EMBED_CHUNK = 250;
|
|
2923
|
+
function chunkArray(items, size) {
|
|
2924
|
+
const out = [];
|
|
2925
|
+
for (let i = 0; i < items.length; i += size)
|
|
2926
|
+
out.push(items.slice(i, i + size));
|
|
2927
|
+
return out;
|
|
2928
|
+
}
|
|
2929
|
+
function numOrNull(v) {
|
|
2930
|
+
if (v === null)
|
|
2931
|
+
return null;
|
|
2932
|
+
const n = Number(v);
|
|
2933
|
+
return Number.isFinite(n) ? n : null;
|
|
2934
|
+
}
|
|
2935
|
+
function numOr0(v) {
|
|
2936
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// ../../packages/code-index/build/node.js
|
|
2940
|
+
var DEFAULT_DB_RELPATH = ".maind/code-index.sqlite";
|
|
2941
|
+
function createNodeCodeIndex(opts) {
|
|
2942
|
+
const dbPath = opts.dbPath ?? posix4.join(opts.repoRoot, DEFAULT_DB_RELPATH);
|
|
2943
|
+
mkdirSync3(posix4.dirname(dbPath), { recursive: true });
|
|
2944
|
+
const store = new NodeSqliteStore(dbPath);
|
|
2945
|
+
const parser = new TreeSitterParser();
|
|
2946
|
+
return new CodeIndexEngine({
|
|
2947
|
+
store,
|
|
2948
|
+
parser,
|
|
2949
|
+
repoRoot: opts.repoRoot,
|
|
2950
|
+
embedder: opts.embedder
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
function createCodeIndexSync(opts) {
|
|
2954
|
+
const dbPath = opts.dbPath ?? posix4.join(opts.repoRoot, DEFAULT_DB_RELPATH);
|
|
2955
|
+
const projectKey = opts.projectKey ?? computeProjectKey(opts.repoRoot) ?? void 0;
|
|
2956
|
+
if (!projectKey) {
|
|
2957
|
+
throw new Error("No project_key \u2014 the repo has no git remote. Cloud sync scopes by sha256(remote); commit a remote or pass an explicit key.");
|
|
2958
|
+
}
|
|
2959
|
+
const store = new NodeSqliteStore(dbPath);
|
|
2960
|
+
return new CodeIndexSync(store, {
|
|
2961
|
+
endpoint: opts.endpoint,
|
|
2962
|
+
apiKey: opts.apiKey,
|
|
2963
|
+
projectKey,
|
|
2964
|
+
deviceId: opts.deviceId,
|
|
2965
|
+
chunkSize: opts.chunkSize,
|
|
2966
|
+
onProgress: opts.onProgress
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
// ../../packages/embedding/build/index.js
|
|
2971
|
+
var EMBEDDING_MODEL = process.env.MAIND_EMBEDDING_MODEL ?? "Xenova/bge-small-en-v1.5";
|
|
2972
|
+
var EMBEDDING_DIM = Number(process.env.MAIND_EMBEDDING_DIM ?? 384);
|
|
2973
|
+
var EMBEDDING_DTYPE = process.env.MAIND_EMBEDDING_DTYPE ?? "q8";
|
|
2974
|
+
var EMBEDDING_QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
|
|
2975
|
+
var extractorPromise = null;
|
|
2976
|
+
async function getExtractor() {
|
|
2977
|
+
if (!extractorPromise) {
|
|
2978
|
+
extractorPromise = (async () => {
|
|
2979
|
+
const mod = await import("@huggingface/transformers");
|
|
2980
|
+
const cacheDir = process.env.MAIND_EMBEDDING_CACHE_DIR;
|
|
2981
|
+
if (cacheDir)
|
|
2982
|
+
mod.env.cacheDir = cacheDir;
|
|
2983
|
+
return mod.pipeline("feature-extraction", EMBEDDING_MODEL, {
|
|
2984
|
+
dtype: EMBEDDING_DTYPE,
|
|
2985
|
+
device: "cpu"
|
|
2986
|
+
});
|
|
2987
|
+
})();
|
|
2988
|
+
}
|
|
2989
|
+
return extractorPromise;
|
|
2990
|
+
}
|
|
2991
|
+
function createEmbedder() {
|
|
2992
|
+
const embedder = {
|
|
2993
|
+
async embedDocuments(texts) {
|
|
2994
|
+
if (texts.length === 0)
|
|
2995
|
+
return [];
|
|
2996
|
+
const extractor = await getExtractor();
|
|
2997
|
+
const out = await extractor(texts, { pooling: "cls", normalize: true });
|
|
2998
|
+
return out.tolist().map((v) => Float32Array.from(v));
|
|
2999
|
+
},
|
|
3000
|
+
async embedQuery(text) {
|
|
3001
|
+
const [vec] = await embedder.embedDocuments([
|
|
3002
|
+
EMBEDDING_QUERY_PREFIX + text
|
|
3003
|
+
]);
|
|
3004
|
+
return vec;
|
|
3005
|
+
}
|
|
3006
|
+
};
|
|
3007
|
+
return embedder;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// ../../packages/code-index/build/embed/node.js
|
|
3011
|
+
var NodeEmbedder = class {
|
|
3012
|
+
dim = EMBEDDING_DIM;
|
|
3013
|
+
model = EMBEDDING_MODEL;
|
|
3014
|
+
recipeVersion = EMBED_RECIPE_VERSION;
|
|
3015
|
+
inner = createEmbedder();
|
|
3016
|
+
embedDocuments(texts) {
|
|
3017
|
+
return this.inner.embedDocuments(texts);
|
|
3018
|
+
}
|
|
3019
|
+
embedQuery(text) {
|
|
3020
|
+
return this.inner.embedQuery(text);
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
|
|
3024
|
+
// ../../packages/code-index/build/node-embed.js
|
|
3025
|
+
function createNodeEmbedder() {
|
|
3026
|
+
return new NodeEmbedder();
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/index-cmd.ts
|
|
3030
|
+
var OK_FILE_ENV = "MAIND_ORT_OK_FILE";
|
|
3031
|
+
var WORKER_FLAG = "--_ort-worker";
|
|
3032
|
+
var CLI_ENTRY = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
3033
|
+
function isOrtWorker(argv) {
|
|
3034
|
+
return argv.includes(WORKER_FLAG);
|
|
3035
|
+
}
|
|
3036
|
+
function runOrtIsolated(argv) {
|
|
3037
|
+
const dir = mkdtempSync(join4(tmpdir(), "maind-ort-"));
|
|
3038
|
+
const okFile = join4(dir, "ok");
|
|
3039
|
+
const res = spawnSync(process.execPath, [CLI_ENTRY, ...argv, WORKER_FLAG], {
|
|
3040
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
3041
|
+
env: { ...process.env, [OK_FILE_ENV]: okFile }
|
|
3042
|
+
});
|
|
3043
|
+
const ok = existsSync2(okFile);
|
|
3044
|
+
try {
|
|
3045
|
+
rmSync(dirname3(okFile), { recursive: true, force: true });
|
|
3046
|
+
} catch {
|
|
3047
|
+
}
|
|
3048
|
+
if (ok) return 0;
|
|
3049
|
+
return res.status && res.status !== 0 ? res.status : 1;
|
|
3050
|
+
}
|
|
3051
|
+
function markOrtOk() {
|
|
3052
|
+
const f = process.env[OK_FILE_ENV];
|
|
3053
|
+
if (f) {
|
|
3054
|
+
try {
|
|
3055
|
+
writeFileSync3(f, "ok");
|
|
3056
|
+
} catch {
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
function flag(argv, name) {
|
|
3061
|
+
return argv.includes(name);
|
|
3062
|
+
}
|
|
3063
|
+
function value(argv, name) {
|
|
3064
|
+
const i = argv.indexOf(name);
|
|
3065
|
+
if (i === -1 || i + 1 >= argv.length) return void 0;
|
|
3066
|
+
return argv[i + 1];
|
|
3067
|
+
}
|
|
3068
|
+
function resolveRoot(explicit) {
|
|
3069
|
+
if (explicit) return explicit;
|
|
3070
|
+
try {
|
|
3071
|
+
return execFileSync4("git", ["rev-parse", "--show-toplevel"], {
|
|
3072
|
+
encoding: "utf8",
|
|
3073
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3074
|
+
}).trim();
|
|
3075
|
+
} catch {
|
|
3076
|
+
return process.cwd();
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
function openIndex(opts) {
|
|
3080
|
+
return createNodeCodeIndex({
|
|
3081
|
+
repoRoot: opts.root,
|
|
3082
|
+
dbPath: opts.db ?? void 0,
|
|
3083
|
+
embedder: createNodeEmbedder()
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
function baseOpts(argv) {
|
|
3087
|
+
return {
|
|
3088
|
+
root: resolveRoot(value(argv, "--root")),
|
|
3089
|
+
db: value(argv, "--db") ?? null,
|
|
3090
|
+
full: flag(argv, "--full"),
|
|
3091
|
+
embed: flag(argv, "--embed"),
|
|
3092
|
+
json: flag(argv, "--json")
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
function parseIndexArgs(argv) {
|
|
3096
|
+
return baseOpts(argv);
|
|
3097
|
+
}
|
|
3098
|
+
function parseIndexSearchArgs(argv) {
|
|
3099
|
+
const positional = argv.find((a) => !a.startsWith("--"));
|
|
3100
|
+
const limitRaw = value(argv, "--limit");
|
|
3101
|
+
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 15;
|
|
3102
|
+
const kind = value(argv, "--kind");
|
|
3103
|
+
return {
|
|
3104
|
+
...baseOpts(argv),
|
|
3105
|
+
query: positional ?? "",
|
|
3106
|
+
kind: kind ?? null,
|
|
3107
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 15
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
async function runIndex(opts) {
|
|
3111
|
+
const idx = openIndex(opts);
|
|
3112
|
+
try {
|
|
3113
|
+
const stats = await idx.indexRepository({
|
|
3114
|
+
full: opts.full,
|
|
3115
|
+
embed: opts.embed,
|
|
3116
|
+
onProgress: opts.json ? void 0 : (p) => {
|
|
3117
|
+
process.stderr.write(
|
|
3118
|
+
`\r ${p.done}/${p.total} ${p.path.slice(0, 60)}`.padEnd(84)
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
});
|
|
3122
|
+
if (opts.json) {
|
|
3123
|
+
process.stdout.write(JSON.stringify(stats) + "\n");
|
|
3124
|
+
} else {
|
|
3125
|
+
process.stderr.write("\r".padEnd(84) + "\r");
|
|
3126
|
+
const extra = opts.embed ? `, ${(await idx.status()).embeddingCount} embeddings` : "";
|
|
3127
|
+
process.stdout.write(
|
|
3128
|
+
` \u2713 ${stats.filesParsed} parsed \xB7 ${stats.filesSkipped} unchanged \xB7 ${stats.filesDeleted} removed \u2192 ${stats.nodeCount} symbols, ${stats.edgeCount} edges (${stats.durationMs}ms${stats.incremental ? ", incremental" : ""}${extra})
|
|
3129
|
+
`
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
markOrtOk();
|
|
3133
|
+
return 0;
|
|
3134
|
+
} finally {
|
|
3135
|
+
await idx.close();
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
async function runIndexStatus(opts) {
|
|
3139
|
+
const idx = openIndex(opts);
|
|
3140
|
+
try {
|
|
3141
|
+
const status2 = await idx.status();
|
|
3142
|
+
if (opts.json) {
|
|
3143
|
+
process.stdout.write(JSON.stringify(status2) + "\n");
|
|
3144
|
+
return 0;
|
|
3145
|
+
}
|
|
3146
|
+
if (!status2.indexed) {
|
|
3147
|
+
process.stdout.write(" not indexed \u2014 run `maind index` first.\n");
|
|
3148
|
+
return 0;
|
|
3149
|
+
}
|
|
3150
|
+
const langs = Object.entries(status2.languages).map(([l, c]) => `${l}:${c}`).join(" ");
|
|
3151
|
+
const when = status2.lastIndexedAt ? new Date(status2.lastIndexedAt).toISOString() : "\u2014";
|
|
3152
|
+
process.stdout.write(
|
|
3153
|
+
` ${status2.fileCount} files \xB7 ${status2.nodeCount} symbols \xB7 ${status2.edgeCount} edges \xB7 ${status2.embeddingCount} embeddings
|
|
3154
|
+
languages: ${langs || "\u2014"}
|
|
3155
|
+
last indexed: ${when} (commit ${status2.lastIndexCommit?.slice(0, 8) ?? "\u2014"}, schema v${status2.schemaVersion ?? "\u2014"})
|
|
3156
|
+
`
|
|
3157
|
+
);
|
|
3158
|
+
return 0;
|
|
3159
|
+
} finally {
|
|
3160
|
+
await idx.close();
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
async function runIndexSearch(opts) {
|
|
3164
|
+
if (!opts.query) {
|
|
3165
|
+
process.stderr.write("maind: index-search needs a query. Usage: maind index-search <query> [--kind <k>] [--limit <n>]\n");
|
|
3166
|
+
return 2;
|
|
3167
|
+
}
|
|
3168
|
+
const idx = openIndex(opts);
|
|
3169
|
+
try {
|
|
3170
|
+
const hits = await idx.searchGraph(opts.query, { kind: opts.kind ?? void 0, limit: opts.limit });
|
|
3171
|
+
if (opts.json) {
|
|
3172
|
+
process.stdout.write(JSON.stringify(hits) + "\n");
|
|
3173
|
+
return 0;
|
|
3174
|
+
}
|
|
3175
|
+
if (hits.length === 0) {
|
|
3176
|
+
process.stdout.write(" no matches.\n");
|
|
3177
|
+
return 0;
|
|
3178
|
+
}
|
|
3179
|
+
for (const h of hits) {
|
|
3180
|
+
process.stdout.write(
|
|
3181
|
+
` ${h.kind.padEnd(10)} ${h.qualifiedName} ${h.filePath}:${h.startLine}
|
|
3182
|
+
`
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3185
|
+
return 0;
|
|
3186
|
+
} finally {
|
|
3187
|
+
await idx.close();
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
async function runIndexSemantic(opts) {
|
|
3191
|
+
if (!opts.query) {
|
|
3192
|
+
process.stderr.write("maind: index-semantic needs a query. Usage: maind index-semantic <query> [--limit <n>]\n");
|
|
3193
|
+
return 2;
|
|
3194
|
+
}
|
|
3195
|
+
const idx = openIndex(opts);
|
|
3196
|
+
try {
|
|
3197
|
+
const hits = await idx.semanticQuery(opts.query, { limit: opts.limit });
|
|
3198
|
+
if (opts.json) {
|
|
3199
|
+
process.stdout.write(JSON.stringify(hits) + "\n");
|
|
3200
|
+
} else if (hits.length === 0) {
|
|
3201
|
+
process.stdout.write(
|
|
3202
|
+
" no matches \u2014 did you build embeddings? Run 'maind index --embed' first.\n"
|
|
3203
|
+
);
|
|
3204
|
+
} else {
|
|
3205
|
+
for (const h of hits) {
|
|
3206
|
+
process.stdout.write(
|
|
3207
|
+
` ${(h.score ?? 0).toFixed(3)} ${h.kind.padEnd(10)} ${h.qualifiedName} ${h.filePath}:${h.startLine}
|
|
3208
|
+
`
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
markOrtOk();
|
|
3213
|
+
return 0;
|
|
3214
|
+
} finally {
|
|
3215
|
+
await idx.close();
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async function runIndexArch(opts) {
|
|
3219
|
+
const idx = openIndex(opts);
|
|
3220
|
+
try {
|
|
3221
|
+
const arch = await idx.getArchitecture({ topN: 10 });
|
|
3222
|
+
if (opts.json) {
|
|
3223
|
+
process.stdout.write(JSON.stringify(arch) + "\n");
|
|
3224
|
+
return 0;
|
|
3225
|
+
}
|
|
3226
|
+
const kinds = Object.entries(arch.kindDistribution).map(([k, c]) => `${k}:${c}`).join(" ");
|
|
3227
|
+
process.stdout.write(
|
|
3228
|
+
` ${arch.fileCount} files \xB7 ${arch.nodeCount} symbols \xB7 ${arch.edgeCount} edges
|
|
3229
|
+
kinds: ${kinds}
|
|
3230
|
+
most-imported modules:
|
|
3231
|
+
`
|
|
3232
|
+
);
|
|
3233
|
+
for (const m of arch.topModules) {
|
|
3234
|
+
process.stdout.write(
|
|
3235
|
+
` ${String(m.importsIn).padStart(4)} \u2190 ${m.filePath} (${m.symbolCount} symbols)
|
|
3236
|
+
`
|
|
3237
|
+
);
|
|
3238
|
+
}
|
|
3239
|
+
return 0;
|
|
3240
|
+
} finally {
|
|
3241
|
+
await idx.close();
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/review.ts
|
|
3246
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
3247
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3248
|
+
import { dirname as dirname4 } from "node:path";
|
|
3249
|
+
|
|
3250
|
+
// src/review-render.ts
|
|
3251
|
+
var COMMENT_MARKER = "<!-- maind-review -->";
|
|
3252
|
+
var COMMENT_FINDINGS_CAP = 30;
|
|
3253
|
+
var FOOTER = "maind review \xB7 deterministic, provenance-backed";
|
|
3254
|
+
function buildReviewManifest(data, verdict, meta) {
|
|
3255
|
+
const review = data.review;
|
|
3256
|
+
return {
|
|
3257
|
+
schema: "maind.review.manifest/v1",
|
|
3258
|
+
generated_at: meta.generatedAt,
|
|
3259
|
+
key_prefix: meta.keyPrefix,
|
|
3260
|
+
run_ref: meta.runRef,
|
|
3261
|
+
base: meta.base,
|
|
3262
|
+
diff_stats: meta.diffStats,
|
|
3263
|
+
symbol_count: meta.symbolCount,
|
|
3264
|
+
risk_flags: review?.risk_flags ?? [],
|
|
3265
|
+
findings: (review?.convention_findings ?? []).map((f) => ({
|
|
3266
|
+
convention_id: f.convention_id,
|
|
3267
|
+
version: f.convention_version,
|
|
3268
|
+
severity: f.severity,
|
|
3269
|
+
grade: f.grade,
|
|
3270
|
+
matched_symbol_count: f.matched_symbols.length,
|
|
3271
|
+
matched_changed_symbol_count: f.matched_changed_symbols?.length ?? 0
|
|
3272
|
+
})),
|
|
3273
|
+
served: (review?.lesson_hints ?? []).map((l) => ({
|
|
3274
|
+
id: l.id,
|
|
3275
|
+
version: typeof l.version === "number" ? l.version : null,
|
|
3276
|
+
tier: l.tier ?? null
|
|
3277
|
+
})),
|
|
3278
|
+
verdict: verdict?.ok === true && typeof verdict.verdict === "string" ? {
|
|
3279
|
+
verdict: verdict.verdict,
|
|
3280
|
+
counts: {
|
|
3281
|
+
block: verdict.counts?.block ?? 0,
|
|
3282
|
+
warn: verdict.counts?.warn ?? 0,
|
|
3283
|
+
note: verdict.counts?.note ?? 0
|
|
3284
|
+
},
|
|
3285
|
+
fail_on: meta.failOn
|
|
3286
|
+
} : null,
|
|
3287
|
+
coverage_note: review?.coverage_note ?? null
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
function severityBadge(f) {
|
|
3291
|
+
return `**[${f.severity}]**`;
|
|
3292
|
+
}
|
|
3293
|
+
function findingLine(f) {
|
|
3294
|
+
const cite = `\`${f.convention_id}@v${f.convention_version}\``;
|
|
3295
|
+
const grade = f.grade === "verdict" ? " \xB7 verdict-grade" : "";
|
|
3296
|
+
const changed = f.matched_changed_symbols?.length ?? 0;
|
|
3297
|
+
const matchInfo = changed > 0 ? `matched on changed symbols: ${formatSymbols(f.matched_changed_symbols ?? [])}` : `matched (co-located): ${formatSymbols(f.matched_symbols)}`;
|
|
3298
|
+
const summary = f.summary?.trim() ? ` \u2014 ${f.summary.trim()}` : "";
|
|
3299
|
+
return `- ${severityBadge(f)} ${cite}${summary}${grade}
|
|
3300
|
+
${matchInfo}`;
|
|
3301
|
+
}
|
|
3302
|
+
function formatSymbols(symbols, max = 4) {
|
|
3303
|
+
const shown = symbols.slice(0, max).map((s) => `\`${s}\``);
|
|
3304
|
+
const rest = symbols.length - shown.length;
|
|
3305
|
+
return shown.join(", ") + (rest > 0 ? ` (+${rest} more)` : "");
|
|
3306
|
+
}
|
|
3307
|
+
function verdictLabel(verdict) {
|
|
3308
|
+
const counts = verdict.counts ?? {};
|
|
3309
|
+
return `**Verdict: ${(verdict.verdict ?? "?").toUpperCase()}** (block: ${counts.block ?? 0} \xB7 warn: ${counts.warn ?? 0} \xB7 note: ${counts.note ?? 0})`;
|
|
3310
|
+
}
|
|
3311
|
+
function baseLabel(meta) {
|
|
3312
|
+
return meta.base ? `against \`${meta.base}\`` : "uncommitted work";
|
|
3313
|
+
}
|
|
3314
|
+
function renderReviewMarkdown(data, verdict, meta) {
|
|
3315
|
+
const review = data.review;
|
|
3316
|
+
const findings = review?.convention_findings ?? [];
|
|
3317
|
+
const hints = review?.lesson_hints ?? [];
|
|
3318
|
+
const riskFlags = review?.risk_flags ?? [];
|
|
3319
|
+
const lines = [];
|
|
3320
|
+
lines.push("<!-- Generated by `maind review` \u2014 do not edit by hand. -->");
|
|
3321
|
+
lines.push(
|
|
3322
|
+
`<!-- generated_at: ${meta.generatedAt} | key: ${meta.keyPrefix}${meta.runRef ? ` | run: ${meta.runRef}` : ""} -->`
|
|
3323
|
+
);
|
|
3324
|
+
lines.push("");
|
|
3325
|
+
lines.push("# maind review");
|
|
3326
|
+
lines.push("");
|
|
3327
|
+
lines.push(
|
|
3328
|
+
`Reviewed ${baseLabel(meta)}: ${meta.symbolCount} changed symbol(s) across ${meta.diffStats?.changed_file_count ?? 0} file(s) \xB7 findings: ${findings.length} \xB7 risk flags: ${riskFlags.length} \xB7 lesson hints: ${hints.length}`
|
|
3329
|
+
);
|
|
3330
|
+
lines.push("");
|
|
3331
|
+
if (verdict?.ok === true && verdict.verdict) {
|
|
3332
|
+
lines.push(verdictLabel(verdict));
|
|
3333
|
+
if (verdict.trust_note) {
|
|
3334
|
+
lines.push("");
|
|
3335
|
+
lines.push(`_${verdict.trust_note}_`);
|
|
3336
|
+
}
|
|
3337
|
+
lines.push("");
|
|
3338
|
+
}
|
|
3339
|
+
if (findings.length > 0) {
|
|
3340
|
+
lines.push("## Convention findings");
|
|
3341
|
+
lines.push("");
|
|
3342
|
+
for (const f of findings) lines.push(findingLine(f));
|
|
3343
|
+
lines.push("");
|
|
3344
|
+
} else {
|
|
3345
|
+
lines.push("## Convention findings");
|
|
3346
|
+
lines.push("");
|
|
3347
|
+
lines.push("_No convention findings for this change set._");
|
|
3348
|
+
lines.push("");
|
|
3349
|
+
}
|
|
3350
|
+
if (riskFlags.length > 0) {
|
|
3351
|
+
lines.push("## Risk flags");
|
|
3352
|
+
lines.push("");
|
|
3353
|
+
lines.push(
|
|
3354
|
+
"_Path heuristics from the local diff \u2014 informational, they never block._"
|
|
3355
|
+
);
|
|
3356
|
+
lines.push("");
|
|
3357
|
+
for (const flag2 of riskFlags) lines.push(`- \`${flag2}\``);
|
|
3358
|
+
lines.push("");
|
|
3359
|
+
}
|
|
3360
|
+
if (hints.length > 0) {
|
|
3361
|
+
lines.push("## Lesson hints");
|
|
3362
|
+
lines.push("");
|
|
3363
|
+
lines.push("_Semantic suggestions \u2014 advisory, never part of a verdict._");
|
|
3364
|
+
lines.push("");
|
|
3365
|
+
for (const h of hints) {
|
|
3366
|
+
const parts = [`\`${h.id}\``];
|
|
3367
|
+
if (h.tier) parts.push(h.tier);
|
|
3368
|
+
if (h.match_reason) parts.push(h.match_reason);
|
|
3369
|
+
lines.push(`- **${h.title}** (${parts.join(" \xB7 ")})`);
|
|
3370
|
+
}
|
|
3371
|
+
lines.push("");
|
|
3372
|
+
}
|
|
3373
|
+
if (review?.coverage_note) {
|
|
3374
|
+
lines.push("## Coverage");
|
|
3375
|
+
lines.push("");
|
|
3376
|
+
lines.push(`> ${review.coverage_note}`);
|
|
3377
|
+
lines.push("");
|
|
3378
|
+
}
|
|
3379
|
+
lines.push("---");
|
|
3380
|
+
lines.push(FOOTER);
|
|
3381
|
+
lines.push("");
|
|
3382
|
+
return lines.join("\n");
|
|
3383
|
+
}
|
|
3384
|
+
function renderReviewComment(data, verdict, meta, reportPath) {
|
|
3385
|
+
const review = data.review;
|
|
3386
|
+
const findings = review?.convention_findings ?? [];
|
|
3387
|
+
const riskFlags = review?.risk_flags ?? [];
|
|
3388
|
+
const hints = review?.lesson_hints ?? [];
|
|
3389
|
+
const lines = [];
|
|
3390
|
+
lines.push(COMMENT_MARKER);
|
|
3391
|
+
lines.push("## maind review");
|
|
3392
|
+
lines.push("");
|
|
3393
|
+
if (verdict?.ok === true && verdict.verdict) {
|
|
3394
|
+
lines.push(verdictLabel(verdict));
|
|
3395
|
+
lines.push("");
|
|
3396
|
+
}
|
|
3397
|
+
if (findings.length === 0) {
|
|
3398
|
+
lines.push(
|
|
3399
|
+
`No convention findings for this change set (${meta.symbolCount} changed symbol(s) reviewed ${baseLabel(meta)}).`
|
|
3400
|
+
);
|
|
3401
|
+
lines.push("");
|
|
3402
|
+
} else {
|
|
3403
|
+
const shown = findings.slice(0, COMMENT_FINDINGS_CAP);
|
|
3404
|
+
for (const f of shown) lines.push(findingLine(f));
|
|
3405
|
+
if (findings.length > shown.length) {
|
|
3406
|
+
lines.push("");
|
|
3407
|
+
lines.push(
|
|
3408
|
+
`\u2026and ${findings.length - shown.length} more finding(s) \u2014 see the \`${reportPath}\` build artifact.`
|
|
3409
|
+
);
|
|
3410
|
+
}
|
|
3411
|
+
lines.push("");
|
|
3412
|
+
}
|
|
3413
|
+
if (riskFlags.length > 0) {
|
|
3414
|
+
lines.push(
|
|
3415
|
+
`**Risk flags:** ${riskFlags.map((f) => `\`${f}\``).join(" \xB7 ")} _(heuristics, never blocking)_`
|
|
3416
|
+
);
|
|
3417
|
+
lines.push("");
|
|
3418
|
+
}
|
|
3419
|
+
if (hints.length > 0) {
|
|
3420
|
+
lines.push("**Related lessons:** " + hints.map((h) => `\`${h.id}\``).join(" \xB7 "));
|
|
3421
|
+
lines.push("");
|
|
3422
|
+
}
|
|
3423
|
+
if (review?.coverage_note) {
|
|
3424
|
+
lines.push(`> ${review.coverage_note}`);
|
|
3425
|
+
lines.push("");
|
|
3426
|
+
}
|
|
3427
|
+
lines.push("---");
|
|
3428
|
+
lines.push(`_${FOOTER}_`);
|
|
3429
|
+
lines.push("");
|
|
3430
|
+
return lines.join("\n");
|
|
3431
|
+
}
|
|
3432
|
+
function renderFailOpenReview(meta, reason) {
|
|
3433
|
+
return [
|
|
3434
|
+
"<!-- Generated by `maind review` (fail-open path). -->",
|
|
3435
|
+
`<!-- generated_at: ${meta.generatedAt} -->`,
|
|
3436
|
+
"",
|
|
3437
|
+
"# maind review",
|
|
3438
|
+
"",
|
|
3439
|
+
`_maind review was unavailable for this run: ${reason}_`,
|
|
3440
|
+
"",
|
|
3441
|
+
"The build can proceed normally \u2014 this file is informational only.",
|
|
3442
|
+
""
|
|
3443
|
+
].join("\n");
|
|
3444
|
+
}
|
|
3445
|
+
function renderFailOpenComment(meta, reason) {
|
|
3446
|
+
return [
|
|
3447
|
+
COMMENT_MARKER,
|
|
3448
|
+
"## maind review",
|
|
3449
|
+
"",
|
|
3450
|
+
`_maind review was unavailable for this run: ${reason}_`,
|
|
3451
|
+
"",
|
|
3452
|
+
"---",
|
|
3453
|
+
`_${FOOTER}_`,
|
|
3454
|
+
""
|
|
3455
|
+
].join("\n");
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
// src/review.ts
|
|
3459
|
+
var PROJECT_KEY_RE3 = /^[a-f0-9]{64}$/;
|
|
3460
|
+
var CLI_CLIENT_VERSION = "0.6.1";
|
|
3461
|
+
var DEFAULT_COMMENT_PATH = ".maind/review-comment.md";
|
|
3462
|
+
var SYMBOL_CAP = 64;
|
|
3463
|
+
function parseReviewArgs(argv) {
|
|
3464
|
+
const get = (name) => {
|
|
3465
|
+
const i = argv.indexOf(name);
|
|
3466
|
+
if (i === -1 || i + 1 >= argv.length) return void 0;
|
|
3467
|
+
return argv[i + 1];
|
|
3468
|
+
};
|
|
3469
|
+
const has = (name) => argv.includes(name);
|
|
3470
|
+
let comment = null;
|
|
3471
|
+
const ci = argv.indexOf("--comment");
|
|
3472
|
+
if (ci !== -1) {
|
|
3473
|
+
const next = argv[ci + 1];
|
|
3474
|
+
comment = next && !next.startsWith("--") ? next : DEFAULT_COMMENT_PATH;
|
|
3475
|
+
}
|
|
3476
|
+
const failOnRaw = get("--fail-on");
|
|
3477
|
+
const failOn = failOnRaw === "block" || failOnRaw === "warn" ? failOnRaw : null;
|
|
3478
|
+
return {
|
|
3479
|
+
base: get("--base") ?? null,
|
|
3480
|
+
uncommitted: has("--uncommitted"),
|
|
3481
|
+
summary: get("--summary") ?? null,
|
|
3482
|
+
json: has("--json"),
|
|
3483
|
+
out: get("--out") ?? ".maind/review.md",
|
|
3484
|
+
comment,
|
|
3485
|
+
gate: has("--gate"),
|
|
3486
|
+
failOn,
|
|
3487
|
+
strict: has("--strict"),
|
|
3488
|
+
noReport: has("--no-report"),
|
|
3489
|
+
projectKey: get("--project-key") ?? null,
|
|
3490
|
+
mcpUrl: get("--mcp-url") ?? null,
|
|
3491
|
+
runRef: get("--run-ref") ?? process.env.GITHUB_RUN_ID ?? null,
|
|
3492
|
+
clientVersion: CLI_CLIENT_VERSION
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
function nowIso3() {
|
|
3496
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3497
|
+
}
|
|
3498
|
+
function writeArtifact3(path, content) {
|
|
3499
|
+
const dir = dirname4(path);
|
|
3500
|
+
if (dir && dir !== ".") mkdirSync4(dir, { recursive: true });
|
|
3501
|
+
writeFileSync4(path, content, "utf8");
|
|
3502
|
+
}
|
|
3503
|
+
function manifestPathFor(out) {
|
|
3504
|
+
return out.endsWith(".md") ? out.slice(0, -3) + ".json" : out + ".json";
|
|
3505
|
+
}
|
|
3506
|
+
function verdictCrossesFailOn(verdict, failOn) {
|
|
3507
|
+
return failOn === "block" ? verdict === "block" : verdict === "block" || verdict === "warn";
|
|
3508
|
+
}
|
|
3509
|
+
function resolveRepoRoot() {
|
|
3510
|
+
try {
|
|
3511
|
+
return execFileSync5("git", ["rev-parse", "--show-toplevel"], {
|
|
3512
|
+
encoding: "utf8",
|
|
3513
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3514
|
+
}).trim();
|
|
3515
|
+
} catch {
|
|
3516
|
+
return null;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
async function runReview(opts) {
|
|
3520
|
+
const meta = {
|
|
3521
|
+
generatedAt: nowIso3(),
|
|
3522
|
+
keyPrefix: "unknown",
|
|
3523
|
+
runRef: opts.runRef,
|
|
3524
|
+
base: opts.base,
|
|
3525
|
+
mcpUrl: opts.mcpUrl ?? process.env.MAIND_MCP_URL ?? "https://mcp.maind.dev/mcp",
|
|
3526
|
+
symbolCount: 0,
|
|
3527
|
+
diffStats: null,
|
|
3528
|
+
gate: opts.gate,
|
|
3529
|
+
failOn: opts.gate ? opts.failOn : null
|
|
3530
|
+
};
|
|
3531
|
+
if (opts.failOn && !opts.gate) {
|
|
3532
|
+
warn5("--fail-on has no effect without --gate (advisory runs always exit 0).");
|
|
3533
|
+
}
|
|
3534
|
+
const failOpen = (reason) => {
|
|
3535
|
+
warn5(`review unavailable, ${opts.strict ? "failing (--strict)" : "continuing"}: ${reason}`);
|
|
3536
|
+
try {
|
|
3537
|
+
writeArtifact3(opts.out, renderFailOpenReview(meta, reason));
|
|
3538
|
+
const manifest = buildReviewManifest({}, null, meta);
|
|
3539
|
+
manifest.error = reason;
|
|
3540
|
+
writeArtifact3(manifestPathFor(opts.out), JSON.stringify(manifest, null, 2) + "\n");
|
|
3541
|
+
if (opts.comment) writeArtifact3(opts.comment, renderFailOpenComment(meta, reason));
|
|
3542
|
+
if (opts.json) process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
|
|
3543
|
+
} catch (writeErr) {
|
|
3544
|
+
warn5(`could not write fail-open artifacts: ${msg5(writeErr)}`);
|
|
3545
|
+
return opts.strict ? 2 : 1;
|
|
3546
|
+
}
|
|
3547
|
+
return opts.strict ? 2 : 0;
|
|
3548
|
+
};
|
|
3549
|
+
try {
|
|
3550
|
+
const root = resolveRepoRoot();
|
|
3551
|
+
if (!root) {
|
|
3552
|
+
return failOpen("not a git repository \u2014 review is diff-based.");
|
|
3553
|
+
}
|
|
3554
|
+
const engine = createNodeCodeIndex({ repoRoot: root });
|
|
3555
|
+
let result;
|
|
3556
|
+
try {
|
|
3557
|
+
const status2 = await engine.status();
|
|
3558
|
+
if (!status2.indexed || status2.fileCount === 0) {
|
|
3559
|
+
warn5("no local index found \u2014 building it now (first run in this checkout)\u2026");
|
|
3560
|
+
const stats = await engine.indexRepository();
|
|
3561
|
+
warn5(`indexed ${stats.filesParsed} file(s) in ${Math.round(stats.durationMs / 1e3)}s.`);
|
|
3562
|
+
}
|
|
3563
|
+
result = await engine.reviewChanges({
|
|
3564
|
+
since: opts.base ?? void 0,
|
|
3565
|
+
includeUncommitted: opts.base ? opts.uncommitted : true,
|
|
3566
|
+
limit: SYMBOL_CAP
|
|
3567
|
+
});
|
|
3568
|
+
} finally {
|
|
3569
|
+
await engine.close().catch(() => void 0);
|
|
3570
|
+
}
|
|
3571
|
+
const diffStats = {
|
|
3572
|
+
changed_file_count: result.changedFiles,
|
|
3573
|
+
unindexed_file_count: result.changedFiles - result.indexedFiles,
|
|
3574
|
+
stale_file_count: result.staleFiles,
|
|
3575
|
+
truncated: result.truncated,
|
|
3576
|
+
hunk_coverage: result.hunkCoverage,
|
|
3577
|
+
base_kind: result.baseKind
|
|
3578
|
+
};
|
|
3579
|
+
meta.diffStats = diffStats;
|
|
3580
|
+
meta.symbolCount = result.symbols.length;
|
|
3581
|
+
const summary = opts.summary?.trim() || void 0;
|
|
3582
|
+
if (result.symbols.length === 0 && result.riskFlags.length === 0 && !summary) {
|
|
3583
|
+
const empty = {
|
|
3584
|
+
ok: true,
|
|
3585
|
+
review: {
|
|
3586
|
+
convention_findings: [],
|
|
3587
|
+
lesson_hints: [],
|
|
3588
|
+
risk_flags: [],
|
|
3589
|
+
coverage_note: result.changedFiles === 0 ? "No changed files detected." : "Changed files found but no indexed symbols \u2014 nothing reviewable at symbol level."
|
|
3590
|
+
}
|
|
3591
|
+
};
|
|
3592
|
+
writeAll(empty, null, meta, opts);
|
|
3593
|
+
process.stderr.write(
|
|
3594
|
+
`maind review: no reviewable changes (${result.changedFiles} changed file(s)) \u2192 ${opts.out}
|
|
3595
|
+
`
|
|
3596
|
+
);
|
|
3597
|
+
return 0;
|
|
3598
|
+
}
|
|
3599
|
+
const auth = resolveAuth();
|
|
3600
|
+
meta.keyPrefix = auth.keyPrefix;
|
|
3601
|
+
const overrideKey = opts.projectKey?.trim().toLowerCase();
|
|
3602
|
+
const projectKey = overrideKey && PROJECT_KEY_RE3.test(overrideKey) ? overrideKey : gatherGitState().projectKey;
|
|
3603
|
+
const client = new McpClient({
|
|
3604
|
+
apiKey: auth.apiKey,
|
|
3605
|
+
deviceId: auth.deviceId,
|
|
3606
|
+
clientVersion: opts.clientVersion,
|
|
3607
|
+
mcpUrl: opts.mcpUrl ?? void 0,
|
|
3608
|
+
projectKey
|
|
3609
|
+
});
|
|
3610
|
+
const reviewArgs = {
|
|
3611
|
+
changed_symbols: result.symbols.map((s) => ({
|
|
3612
|
+
name: s.name,
|
|
3613
|
+
qualified_name: s.qualifiedName,
|
|
3614
|
+
kind: s.kind,
|
|
3615
|
+
language: s.language,
|
|
3616
|
+
change: s.change,
|
|
3617
|
+
changed_line_count: s.changedLineCount
|
|
3618
|
+
})),
|
|
3619
|
+
risk_flags: result.riskFlags,
|
|
3620
|
+
diff_stats: diffStats,
|
|
3621
|
+
...summary ? { change_summary: summary } : {},
|
|
3622
|
+
...projectKey ? { project_key: projectKey } : {}
|
|
3623
|
+
};
|
|
3624
|
+
const review = await client.callTool("review_changes", reviewArgs);
|
|
3625
|
+
if (review.ok !== true || !review.review) {
|
|
3626
|
+
return failOpen(
|
|
3627
|
+
`review_changes rejected: ${review.error ?? "unknown_error"}${review.message ? ` \u2014 ${review.message}` : ""}`
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
let verdict = null;
|
|
3631
|
+
if (opts.gate) {
|
|
3632
|
+
const findings = review.review.convention_findings ?? [];
|
|
3633
|
+
try {
|
|
3634
|
+
verdict = await client.callTool("review_verdict", {
|
|
3635
|
+
findings: findings.map((f) => ({
|
|
3636
|
+
convention_id: f.convention_id,
|
|
3637
|
+
convention_version: f.convention_version,
|
|
3638
|
+
matched_symbols: f.matched_symbols,
|
|
3639
|
+
matched_changed_symbols: f.matched_changed_symbols ?? []
|
|
3640
|
+
})),
|
|
3641
|
+
risk_flags: review.review.risk_flags ?? [],
|
|
3642
|
+
...projectKey ? { project_key: projectKey } : {}
|
|
3643
|
+
});
|
|
3644
|
+
if (verdict.ok !== true) {
|
|
3645
|
+
if (verdict.error === "requires_team" || verdict.error === "requires_enterprise") {
|
|
3646
|
+
warn5(
|
|
3647
|
+
"gate skipped: review_verdict is a Team-plan feature \u2014 findings below are advisory. " + (verdict.message ?? "Upgrade at https://maind.dev/pricing.")
|
|
3648
|
+
);
|
|
3649
|
+
verdict = null;
|
|
3650
|
+
} else {
|
|
3651
|
+
if (opts.strict) {
|
|
3652
|
+
return failOpen(
|
|
3653
|
+
`review_verdict failed: ${verdict.error ?? "unknown_error"}${verdict.message ? ` \u2014 ${verdict.message}` : ""}`
|
|
3654
|
+
);
|
|
3655
|
+
}
|
|
3656
|
+
warn5(
|
|
3657
|
+
`gate unavailable (continuing advisory): ${verdict.error ?? "unknown_error"}`
|
|
3658
|
+
);
|
|
3659
|
+
verdict = null;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
} catch (err2) {
|
|
3663
|
+
if (opts.strict) return failOpen(`review_verdict failed: ${msg5(err2)}`);
|
|
3664
|
+
warn5(`gate unavailable (continuing advisory): ${msg5(err2)}`);
|
|
3665
|
+
verdict = null;
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
writeAll(review, verdict, meta, opts);
|
|
3669
|
+
const findingCount = review.review.convention_findings?.length ?? 0;
|
|
3670
|
+
const verdictNote = verdict?.verdict ? ` \xB7 verdict: ${verdict.verdict}` : "";
|
|
3671
|
+
process.stderr.write(
|
|
3672
|
+
`maind review: ${findingCount} finding(s), ${review.review.risk_flags?.length ?? 0} risk flag(s)${verdictNote} \u2192 ${opts.out}
|
|
3673
|
+
`
|
|
3674
|
+
);
|
|
3675
|
+
if (opts.gate && opts.failOn && verdict?.verdict) {
|
|
3676
|
+
if (verdictCrossesFailOn(verdict.verdict, opts.failOn)) {
|
|
3677
|
+
warn5(
|
|
3678
|
+
`verdict '${verdict.verdict}' crossed --fail-on ${opts.failOn} \u2014 failing the check.`
|
|
3679
|
+
);
|
|
3680
|
+
return 1;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
return 0;
|
|
3684
|
+
} catch (err2) {
|
|
3685
|
+
const reason = err2 instanceof MissingApiKeyError ? err2.message : msg5(err2);
|
|
3686
|
+
return failOpen(reason);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
function writeAll(review, verdict, meta, opts) {
|
|
3690
|
+
writeArtifact3(opts.out, renderReviewMarkdown(review, verdict, meta));
|
|
3691
|
+
const manifest = buildReviewManifest(review, verdict, meta);
|
|
3692
|
+
writeArtifact3(manifestPathFor(opts.out), JSON.stringify(manifest, null, 2) + "\n");
|
|
3693
|
+
if (opts.comment) {
|
|
3694
|
+
writeArtifact3(opts.comment, renderReviewComment(review, verdict, meta, opts.out));
|
|
3695
|
+
}
|
|
3696
|
+
if (opts.json) process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
|
|
3697
|
+
}
|
|
3698
|
+
function msg5(err2) {
|
|
3699
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
3700
|
+
}
|
|
3701
|
+
function warn5(line) {
|
|
3702
|
+
process.stderr.write(`maind review: ${line}
|
|
3703
|
+
`);
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
// src/sync-cmd.ts
|
|
3707
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
3708
|
+
var CONSENT_POLICY_VERSION = "2026-07-05";
|
|
3709
|
+
var DEFAULT_DASHBOARD_URL = "https://app.maind.dev";
|
|
3710
|
+
function err(line) {
|
|
3711
|
+
process.stderr.write(`maind: ${line}
|
|
3712
|
+
`);
|
|
3713
|
+
}
|
|
3714
|
+
function repoRoot() {
|
|
3715
|
+
try {
|
|
3716
|
+
return execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
3717
|
+
} catch {
|
|
3718
|
+
return process.cwd();
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
function dashboardUrl() {
|
|
3722
|
+
const raw = process.env.MAIND_DASHBOARD_URL?.trim();
|
|
3723
|
+
return (raw && raw.length > 0 ? raw : DEFAULT_DASHBOARD_URL).replace(/\/+$/, "");
|
|
3724
|
+
}
|
|
3725
|
+
function makeSync(json) {
|
|
3726
|
+
let auth;
|
|
3727
|
+
try {
|
|
3728
|
+
auth = resolveAuth();
|
|
3729
|
+
} catch (e) {
|
|
3730
|
+
if (e instanceof MissingApiKeyError) {
|
|
3731
|
+
err(e.message);
|
|
3732
|
+
return null;
|
|
3733
|
+
}
|
|
3734
|
+
throw e;
|
|
3735
|
+
}
|
|
3736
|
+
try {
|
|
3737
|
+
return createCodeIndexSync({
|
|
3738
|
+
repoRoot: repoRoot(),
|
|
3739
|
+
endpoint: dashboardUrl(),
|
|
3740
|
+
apiKey: auth.apiKey,
|
|
3741
|
+
deviceId: auth.deviceId,
|
|
3742
|
+
onProgress: json ? void 0 : (p) => process.stderr.write(` ${p.phase}: ${p.done}/${p.total}\r`)
|
|
3743
|
+
});
|
|
3744
|
+
} catch (e) {
|
|
3745
|
+
err(e instanceof Error ? e.message : String(e));
|
|
3746
|
+
return null;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
function msg6(e) {
|
|
3750
|
+
return e instanceof Error ? e.message : String(e);
|
|
3751
|
+
}
|
|
3752
|
+
function parseSyncArgs(argv) {
|
|
3753
|
+
const positional = argv.find((a) => !a.startsWith("-"));
|
|
3754
|
+
const sub = positional === "status" || positional === "enable" || positional === "disable" ? positional : "push";
|
|
3755
|
+
return { sub, json: argv.includes("--json") };
|
|
3756
|
+
}
|
|
3757
|
+
async function runSync(opts) {
|
|
3758
|
+
const sync = makeSync(opts.json);
|
|
3759
|
+
if (!sync) return 1;
|
|
3760
|
+
try {
|
|
3761
|
+
switch (opts.sub) {
|
|
3762
|
+
case "enable":
|
|
3763
|
+
return await enable(sync);
|
|
3764
|
+
case "disable":
|
|
3765
|
+
return await disable(sync);
|
|
3766
|
+
case "status":
|
|
3767
|
+
return await status(sync, opts.json);
|
|
3768
|
+
default:
|
|
3769
|
+
return await push(sync, opts.json);
|
|
3770
|
+
}
|
|
3771
|
+
} catch (e) {
|
|
3772
|
+
if (e instanceof SyncError && e.code === "consent_required") {
|
|
3773
|
+
err("codebase cloud-sync is not enabled \u2014 run `maind sync enable` first.");
|
|
3774
|
+
return 1;
|
|
3775
|
+
}
|
|
3776
|
+
err(`sync failed: ${msg6(e)}`);
|
|
3777
|
+
return 1;
|
|
3778
|
+
} finally {
|
|
3779
|
+
await sync.close().catch(() => {
|
|
3780
|
+
});
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
async function enable(sync) {
|
|
3784
|
+
await sync.setConsent(true, CONSENT_POLICY_VERSION);
|
|
3785
|
+
process.stdout.write(
|
|
3786
|
+
"codebase cloud-sync enabled. Structured rows (symbol names, topology, hashes) will be pushed on `maind sync` \u2014 never source, snippets, or plaintext paths.\n"
|
|
3787
|
+
);
|
|
3788
|
+
return 0;
|
|
3789
|
+
}
|
|
3790
|
+
async function disable(sync) {
|
|
3791
|
+
await sync.setConsent(false);
|
|
3792
|
+
process.stdout.write("codebase cloud-sync disabled.\n");
|
|
3793
|
+
return 0;
|
|
3794
|
+
}
|
|
3795
|
+
async function status(sync, json) {
|
|
3796
|
+
const local = await sync.localStatus();
|
|
3797
|
+
const remote = await sync.remoteStatus();
|
|
3798
|
+
if (json) {
|
|
3799
|
+
process.stdout.write(JSON.stringify({ ok: true, local, remote }, null, 2) + "\n");
|
|
3800
|
+
return 0;
|
|
3801
|
+
}
|
|
3802
|
+
process.stdout.write(
|
|
3803
|
+
`local: ${local.indexed ? `${local.nodeCount} symbols \xB7 ${local.edgeCount} edges \xB7 ${local.fileCount} files` : "not indexed"}${local.lastSyncedAt ? ` \xB7 last synced ${new Date(local.lastSyncedAt).toISOString()}` : ""}
|
|
3804
|
+
cloud: ${remote.indexed ? `${remote.node_count ?? "?"} symbols \xB7 ${remote.edge_count ?? "?"} edges` : "not synced"}
|
|
3805
|
+
`
|
|
3806
|
+
);
|
|
3807
|
+
return 0;
|
|
3808
|
+
}
|
|
3809
|
+
async function push(sync, json) {
|
|
3810
|
+
const local = await sync.localStatus();
|
|
3811
|
+
if (!local.indexed || local.nodeCount === 0) {
|
|
3812
|
+
err("no local index found \u2014 run `maind index` first.");
|
|
3813
|
+
return 1;
|
|
3814
|
+
}
|
|
3815
|
+
const res = await sync.push();
|
|
3816
|
+
process.stderr.write("\n");
|
|
3817
|
+
if (json) {
|
|
3818
|
+
process.stdout.write(JSON.stringify({ ok: true, ...res }, null, 2) + "\n");
|
|
3819
|
+
return 0;
|
|
3820
|
+
}
|
|
3821
|
+
process.stdout.write(
|
|
3822
|
+
`synced ${res.nodes} symbols + ${res.edges} edges to the cloud${res.embeddings ? ` + ${res.embeddings} vectors` : ""}${res.pruned ? ` (pruned ${res.pruned} stale)` : ""}${res.headCommit ? ` @ ${res.headCommit.slice(0, 8)}` : ""}.
|
|
3823
|
+
`
|
|
3824
|
+
);
|
|
3825
|
+
return 0;
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
// src/index.ts
|
|
3829
|
+
var VERSION = "0.6.1";
|
|
3830
|
+
var HELP = `maind ${VERSION} \u2014 headless maind CLI for CI + git context
|
|
18
3831
|
|
|
19
3832
|
Usage:
|
|
20
3833
|
maind context [options] Pull stack-relevant lessons + conventions into the runner
|
|
21
3834
|
maind git-context [options] Briefing of local git state + peers + relevant lessons (session-start)
|
|
3835
|
+
maind project-key Print this repo's project_key (sha256) for repo-scoped content
|
|
3836
|
+
maind index [--embed] [..] Build/update the local codebase index (--embed builds symbol vectors)
|
|
3837
|
+
maind index-status [--json] Show the local index status (files, symbols, edges, embeddings)
|
|
3838
|
+
maind index-search <q> [..] Lexical symbol search (--kind <k> --limit <n> --json)
|
|
3839
|
+
maind index-semantic <q> [.] Semantic symbol search (needs 'index --embed'; --limit <n> --json)
|
|
3840
|
+
maind index-arch [--json] Architecture overview (symbol kinds + most-imported modules)
|
|
3841
|
+
maind sync Push the local index to the cloud (opt-in; hashed rows only)
|
|
3842
|
+
maind sync status [--json] Show local vs cloud sync state
|
|
3843
|
+
maind sync enable|disable Opt in/out of codebase cloud-sync (consent)
|
|
3844
|
+
maind review [options] Provenance-backed review findings for a change set (advisory by default)
|
|
22
3845
|
maind ci-report [options] Report a CI failure as a draft-lesson candidate (on failure)
|
|
23
3846
|
maind ci-result [options] Report a build outcome + served manifest (end of job)
|
|
24
3847
|
maind --help Show this help
|
|
25
3848
|
maind --version Print version
|
|
26
3849
|
|
|
3850
|
+
review options (advisory by default \u2014 exits 0 unless --gate + --fail-on):
|
|
3851
|
+
--base <ref> Review the diff against this git ref (e.g. origin/main)
|
|
3852
|
+
--uncommitted With --base: also include uncommitted changes (default without --base)
|
|
3853
|
+
--summary <text> Prose change summary \u2014 enables index-less review, sharpens hints
|
|
3854
|
+
--out <path> Findings report markdown (default .maind/review.md; always written,
|
|
3855
|
+
a manifest JSON lands next to it as review.json)
|
|
3856
|
+
--comment [<path>] Also write PR-comment markdown (default .maind/review-comment.md);
|
|
3857
|
+
line 1 carries the idempotency marker for the workflow to post via gh
|
|
3858
|
+
--json Print the review manifest JSON to stdout (pipeable)
|
|
3859
|
+
--gate Aggregate findings into a server-side pass|warn|block verdict (Team plan)
|
|
3860
|
+
--fail-on block|warn With --gate: exit 1 when the verdict crosses this threshold
|
|
3861
|
+
--strict Infra errors exit 2 instead of failing open with exit 0
|
|
3862
|
+
--no-report Do not report run counts to maind (reserved; run reporting
|
|
3863
|
+
lands with the review_runs ingest in a later release)
|
|
3864
|
+
--project-key <hex> Repo project_key override (default: auto-detect from git)
|
|
3865
|
+
--mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
|
|
3866
|
+
|
|
27
3867
|
git-context options (run at session-start; reads local git + gh, all fail-open):
|
|
28
3868
|
--out <path> Briefing markdown output (default .maind/git-context.md)
|
|
29
3869
|
--manifest <path> Optional structured JSON output (git_context manifest)
|
|
30
|
-
--no-mcp Local git state only
|
|
31
|
-
--mcp-url <url> Override MCP endpoint (default
|
|
3870
|
+
--no-mcp Local git state only \u2014 skip peer-conflict + lesson enrichment
|
|
3871
|
+
--mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
|
|
32
3872
|
|
|
33
3873
|
ci-report options (run in an on-failure step; the agent authors the lesson first):
|
|
34
|
-
--lesson <path> Agent-authored lesson markdown (frontmatter + body)
|
|
35
|
-
--rationale <text> Why this is a reusable lesson
|
|
36
|
-
--failure-type <t> e.g. tsc-error, test-failure, lint-error
|
|
3874
|
+
--lesson <path> Agent-authored lesson markdown (frontmatter + body) \u2014 required
|
|
3875
|
+
--rationale <text> Why this is a reusable lesson \u2014 required
|
|
3876
|
+
--failure-type <t> e.g. tsc-error, test-failure, lint-error \u2014 required
|
|
37
3877
|
--signature <s> Stable dedup key (default: failure-type + lesson hash)
|
|
38
3878
|
--summary <text> Short failure description
|
|
39
3879
|
--remediation <text> Suggested fix
|
|
40
3880
|
--logs <path> Optional failure-log file (truncated to 8k)
|
|
41
3881
|
--provider <p> CI provider (default github-actions)
|
|
42
|
-
--
|
|
3882
|
+
--project-key <hex> Repo project_key for org-private repo-scoping (default: auto-detect from git)
|
|
3883
|
+
--report-url <url> Override ingest endpoint (default $MAIND_REPORT_URL or prod)
|
|
43
3884
|
|
|
44
3885
|
ci-result options (run at the end of a job, success OR failure):
|
|
45
|
-
--outcome <s> Build outcome, e.g. \${{ job.status }} (success|failure)
|
|
3886
|
+
--outcome <s> Build outcome, e.g. \${{ job.status }} (success|failure) \u2014 required
|
|
46
3887
|
--manifest <path> Served-lesson manifest from 'maind context' (default .maind/manifest.json)
|
|
47
|
-
--run-ref <id> Build identifier (default manifest.run_ref or
|
|
3888
|
+
--run-ref <id> Build identifier (default manifest.run_ref or $GITHUB_RUN_ID)
|
|
48
3889
|
--run-url <url> Link to the CI run (default derived from GitHub env)
|
|
49
|
-
--result-url <url> Override ingest endpoint (default
|
|
3890
|
+
--result-url <url> Override ingest endpoint (default $MAIND_RESULT_URL or prod)
|
|
50
3891
|
|
|
51
3892
|
context options:
|
|
52
3893
|
--stack <a,b> Stack hint (seeds the platforms filter), e.g. nextjs,supabase
|
|
@@ -57,8 +3898,8 @@ context options:
|
|
|
57
3898
|
--budget <n> Max lessons to pull (1-50, default 15; server caps search at 10)
|
|
58
3899
|
--out <path> Context markdown output (default .maind/context.md)
|
|
59
3900
|
--manifest <path> Manifest JSON output (default .maind/manifest.json)
|
|
60
|
-
--run-ref <id> Build identifier for the manifest (default
|
|
61
|
-
--mcp-url <url> Override MCP endpoint (default
|
|
3901
|
+
--run-ref <id> Build identifier for the manifest (default $GITHUB_RUN_ID)
|
|
3902
|
+
--mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
|
|
62
3903
|
|
|
63
3904
|
Auth:
|
|
64
3905
|
Reads MAIND_API_KEY (a read-scoped CI key from https://maind.dev/keys), or falls
|
|
@@ -66,37 +3907,68 @@ Auth:
|
|
|
66
3907
|
error it still writes explanatory artifacts and exits 0 so the build proceeds.
|
|
67
3908
|
`;
|
|
68
3909
|
async function main() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
3910
|
+
const argv = process.argv.slice(2);
|
|
3911
|
+
const cmd = argv[0];
|
|
3912
|
+
if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
3913
|
+
process.stdout.write(HELP);
|
|
3914
|
+
return 0;
|
|
3915
|
+
}
|
|
3916
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
3917
|
+
process.stdout.write(VERSION + "\n");
|
|
3918
|
+
return 0;
|
|
3919
|
+
}
|
|
3920
|
+
if (cmd === "context") {
|
|
3921
|
+
return runContext(parseContextArgs(argv.slice(1)));
|
|
3922
|
+
}
|
|
3923
|
+
if (cmd === "git-context") {
|
|
3924
|
+
return runGitContext(parseGitContextArgs(argv.slice(1)));
|
|
3925
|
+
}
|
|
3926
|
+
if (cmd === "project-key") {
|
|
3927
|
+
const key = gatherGitState().projectKey;
|
|
3928
|
+
if (!key) {
|
|
3929
|
+
process.stderr.write("maind: not a git repo with a remote \u2014 no project_key.\n");
|
|
3930
|
+
return 1;
|
|
78
3931
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
3932
|
+
process.stdout.write(key + "\n");
|
|
3933
|
+
return 0;
|
|
3934
|
+
}
|
|
3935
|
+
const usesOrt = cmd === "index" && argv.includes("--embed") || cmd === "index-semantic";
|
|
3936
|
+
if (usesOrt && !isOrtWorker(argv)) {
|
|
3937
|
+
return runOrtIsolated(argv);
|
|
3938
|
+
}
|
|
3939
|
+
if (cmd === "index") {
|
|
3940
|
+
return runIndex(parseIndexArgs(argv.slice(1)));
|
|
3941
|
+
}
|
|
3942
|
+
if (cmd === "index-status") {
|
|
3943
|
+
return runIndexStatus(parseIndexArgs(argv.slice(1)));
|
|
3944
|
+
}
|
|
3945
|
+
if (cmd === "index-search") {
|
|
3946
|
+
return runIndexSearch(parseIndexSearchArgs(argv.slice(1)));
|
|
3947
|
+
}
|
|
3948
|
+
if (cmd === "index-semantic") {
|
|
3949
|
+
return runIndexSemantic(parseIndexSearchArgs(argv.slice(1)));
|
|
3950
|
+
}
|
|
3951
|
+
if (cmd === "index-arch") {
|
|
3952
|
+
return runIndexArch(parseIndexArgs(argv.slice(1)));
|
|
3953
|
+
}
|
|
3954
|
+
if (cmd === "sync") {
|
|
3955
|
+
return runSync(parseSyncArgs(argv.slice(1)));
|
|
3956
|
+
}
|
|
3957
|
+
if (cmd === "review") {
|
|
3958
|
+
return runReview(parseReviewArgs(argv.slice(1)));
|
|
3959
|
+
}
|
|
3960
|
+
if (cmd === "ci-report") {
|
|
3961
|
+
return runCiReport(parseCiReportArgs(argv.slice(1)));
|
|
3962
|
+
}
|
|
3963
|
+
if (cmd === "ci-result") {
|
|
3964
|
+
return runCiResult(parseCiResultArgs(argv.slice(1)));
|
|
3965
|
+
}
|
|
3966
|
+
process.stderr.write(`maind: unknown command '${cmd}'. Run 'maind --help'.
|
|
3967
|
+
`);
|
|
3968
|
+
return 2;
|
|
93
3969
|
}
|
|
94
|
-
main()
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// an unexpected programming error. Surface it on stderr and exit non-zero.
|
|
99
|
-
process.stderr.write(`maind: fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
100
|
-
process.exit(1);
|
|
3970
|
+
main().then((code) => process.exit(code)).catch((err2) => {
|
|
3971
|
+
process.stderr.write(`maind: fatal: ${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}
|
|
3972
|
+
`);
|
|
3973
|
+
process.exit(1);
|
|
101
3974
|
});
|
|
102
|
-
//# sourceMappingURL=index.js.map
|