@dreamtree-org/graphify 1.0.0 → 1.1.2
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 +110 -35
- package/dist/chunk-6JLEILYF.js +207 -0
- package/dist/chunk-6JLEILYF.js.map +1 -0
- package/dist/{chunk-DG5FECXV.js → chunk-PXVI2L45.js} +614 -39
- package/dist/chunk-PXVI2L45.js.map +1 -0
- package/dist/chunk-YT7B6DOD.js +554 -0
- package/dist/chunk-YT7B6DOD.js.map +1 -0
- package/dist/chunk-ZPB37LLQ.js +155 -0
- package/dist/chunk-ZPB37LLQ.js.map +1 -0
- package/dist/cli/index.cjs +1996 -147
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +693 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +1192 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +343 -5
- package/dist/index.d.ts +343 -5
- package/dist/index.js +50 -5
- package/dist/mcp/server.cjs +444 -56
- package/dist/mcp/server.cjs.map +1 -1
- package/dist/mcp/server.js +65 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mysql-EJ6XOWR4.js +8 -0
- package/dist/mysql-EJ6XOWR4.js.map +1 -0
- package/package.json +2 -1
- package/src/skill/SKILL.md +45 -10
- package/dist/chunk-5ANIDX3G.js +0 -364
- package/dist/chunk-5ANIDX3G.js.map +0 -1
- package/dist/chunk-DG5FECXV.js.map +0 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -127,7 +127,36 @@ function validateUrl(url) {
|
|
|
127
127
|
}
|
|
128
128
|
return parsed.toString();
|
|
129
129
|
}
|
|
130
|
-
function
|
|
130
|
+
function validateDsn(dsn) {
|
|
131
|
+
let parsed;
|
|
132
|
+
try {
|
|
133
|
+
parsed = new URL(dsn);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error("Invalid DSN \u2014 expected mysql://user:pass@host:port/database");
|
|
136
|
+
}
|
|
137
|
+
if (parsed.protocol !== "mysql:") {
|
|
138
|
+
throw new Error(`DSN scheme not supported: "${parsed.protocol}" (only mysql: is supported)`);
|
|
139
|
+
}
|
|
140
|
+
const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
|
|
141
|
+
if (!database || database.includes("/")) {
|
|
142
|
+
throw new Error("DSN must name exactly one database, e.g. mysql://localhost:3306/mydb");
|
|
143
|
+
}
|
|
144
|
+
if (!parsed.hostname) {
|
|
145
|
+
throw new Error("DSN must include a host, e.g. mysql://localhost:3306/mydb");
|
|
146
|
+
}
|
|
147
|
+
const port = parsed.port ? Number(parsed.port) : DEFAULT_MYSQL_PORT;
|
|
148
|
+
return {
|
|
149
|
+
safeDisplay: `mysql://${parsed.hostname}:${port}/${database}`,
|
|
150
|
+
connection: {
|
|
151
|
+
host: parsed.hostname,
|
|
152
|
+
port,
|
|
153
|
+
user: decodeURIComponent(parsed.username) || "root",
|
|
154
|
+
password: decodeURIComponent(parsed.password),
|
|
155
|
+
database
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function validateGraphPath(path18, base) {
|
|
131
160
|
const baseDir = base ?? nodePath.join(process.cwd(), "graphify-out");
|
|
132
161
|
let resolvedBase;
|
|
133
162
|
try {
|
|
@@ -135,7 +164,7 @@ function validateGraphPath(path11, base) {
|
|
|
135
164
|
} catch {
|
|
136
165
|
throw new Error(`Base directory does not exist: ${baseDir}`);
|
|
137
166
|
}
|
|
138
|
-
const candidate = nodePath.isAbsolute(
|
|
167
|
+
const candidate = nodePath.isAbsolute(path18) ? path18 : nodePath.join(resolvedBase, path18);
|
|
139
168
|
const resolvedCandidate = nodePath.resolve(candidate);
|
|
140
169
|
let realCandidate = resolvedCandidate;
|
|
141
170
|
try {
|
|
@@ -145,7 +174,7 @@ function validateGraphPath(path11, base) {
|
|
|
145
174
|
const relative4 = nodePath.relative(resolvedBase, realCandidate);
|
|
146
175
|
const escapes = relative4 === ".." || relative4.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative4);
|
|
147
176
|
if (escapes) {
|
|
148
|
-
throw new Error(`Path escapes graphify-out/: ${
|
|
177
|
+
throw new Error(`Path escapes graphify-out/: ${path18}`);
|
|
149
178
|
}
|
|
150
179
|
return realCandidate;
|
|
151
180
|
}
|
|
@@ -157,7 +186,7 @@ function sanitizeLabel(text) {
|
|
|
157
186
|
function escapeHtml(text) {
|
|
158
187
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
159
188
|
}
|
|
160
|
-
var import_node_crypto2, dns, http, https, fs, net, nodePath, ALLOWED_SCHEMES, MAX_FETCH_BYTES, MAX_TEXT_BYTES, MAX_LABEL_LEN, BLOCKED_HOSTNAMES, BLOCKED_IPV4_CIDRS;
|
|
189
|
+
var import_node_crypto2, dns, http, https, fs, net, nodePath, ALLOWED_SCHEMES, MAX_FETCH_BYTES, MAX_TEXT_BYTES, MAX_LABEL_LEN, BLOCKED_HOSTNAMES, BLOCKED_IPV4_CIDRS, DEFAULT_MYSQL_PORT;
|
|
161
190
|
var init_security = __esm({
|
|
162
191
|
"src/security.ts"() {
|
|
163
192
|
"use strict";
|
|
@@ -213,6 +242,7 @@ var init_security = __esm({
|
|
|
213
242
|
"255.255.255.255/32"
|
|
214
243
|
// broadcast
|
|
215
244
|
];
|
|
245
|
+
DEFAULT_MYSQL_PORT = 3306;
|
|
216
246
|
}
|
|
217
247
|
});
|
|
218
248
|
|
|
@@ -236,8 +266,43 @@ var init_graphStore = __esm({
|
|
|
236
266
|
});
|
|
237
267
|
|
|
238
268
|
// src/query.ts
|
|
269
|
+
function splitCamelCase(text) {
|
|
270
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
271
|
+
}
|
|
239
272
|
function tokenize(text) {
|
|
240
|
-
return text.toLowerCase().split(/[^a-z0-
|
|
273
|
+
return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1 && !STOPWORDS.has(token));
|
|
274
|
+
}
|
|
275
|
+
function subtokenize(text) {
|
|
276
|
+
return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1);
|
|
277
|
+
}
|
|
278
|
+
function osaDistance(a, b) {
|
|
279
|
+
let prev2 = [];
|
|
280
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
281
|
+
let curr = new Array(b.length + 1).fill(0);
|
|
282
|
+
for (let i = 1; i <= a.length; i++) {
|
|
283
|
+
curr[0] = i;
|
|
284
|
+
for (let j = 1; j <= b.length; j++) {
|
|
285
|
+
const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
286
|
+
let best = Math.min(
|
|
287
|
+
prev[j] + 1,
|
|
288
|
+
curr[j - 1] + 1,
|
|
289
|
+
prev[j - 1] + substitution
|
|
290
|
+
);
|
|
291
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
292
|
+
best = Math.min(best, prev2[j - 2] + 1);
|
|
293
|
+
}
|
|
294
|
+
curr[j] = best;
|
|
295
|
+
}
|
|
296
|
+
[prev2, prev, curr] = [prev, curr, new Array(b.length + 1).fill(0)];
|
|
297
|
+
}
|
|
298
|
+
return prev[b.length];
|
|
299
|
+
}
|
|
300
|
+
function fuzzySimilarity(a, b) {
|
|
301
|
+
if (a === b) return 1;
|
|
302
|
+
const maxLen = Math.max(a.length, b.length);
|
|
303
|
+
if (maxLen === 0) return 1;
|
|
304
|
+
if (Math.abs(a.length - b.length) / maxLen > 1 - FUZZY_THRESHOLD) return 0;
|
|
305
|
+
return 1 - osaDistance(a, b) / maxLen;
|
|
241
306
|
}
|
|
242
307
|
function scoreNodes(graph, query) {
|
|
243
308
|
const tokens = tokenize(query);
|
|
@@ -245,9 +310,25 @@ function scoreNodes(graph, query) {
|
|
|
245
310
|
graph.forEachNode((id, attributes) => {
|
|
246
311
|
const label = attributes.label ?? id;
|
|
247
312
|
const haystack = `${label} ${id}`.toLowerCase();
|
|
313
|
+
const subtokens = new Set(subtokenize(`${label} ${id}`));
|
|
248
314
|
let score = 0;
|
|
249
315
|
for (const token of tokens) {
|
|
250
|
-
if (
|
|
316
|
+
if (subtokens.has(token)) {
|
|
317
|
+
score += SUBTOKEN_MATCH_SCORE;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (haystack.includes(token)) {
|
|
321
|
+
score += SUBSTRING_MATCH_SCORE;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (token.length >= FUZZY_MIN_TOKEN_LEN) {
|
|
325
|
+
let best = 0;
|
|
326
|
+
for (const subtoken of subtokens) {
|
|
327
|
+
const similarity = fuzzySimilarity(token, subtoken);
|
|
328
|
+
if (similarity > best) best = similarity;
|
|
329
|
+
}
|
|
330
|
+
if (best >= FUZZY_THRESHOLD) score += best * FUZZY_MATCH_WEIGHT;
|
|
331
|
+
}
|
|
251
332
|
}
|
|
252
333
|
if (score > 0) matches.push({ id, label, score });
|
|
253
334
|
});
|
|
@@ -258,22 +339,34 @@ function resolveNode(graph, query) {
|
|
|
258
339
|
const matches = scoreNodes(graph, query);
|
|
259
340
|
return matches[0] ?? null;
|
|
260
341
|
}
|
|
342
|
+
function nodeTokenCost(graph, id) {
|
|
343
|
+
const attrs = graph.getNodeAttributes(id);
|
|
344
|
+
const label = attrs.label ?? id;
|
|
345
|
+
const sourceFile = attrs.sourceFile ?? "";
|
|
346
|
+
return Math.ceil((id.length * 2 + label.length + sourceFile.length + 24) / 4);
|
|
347
|
+
}
|
|
261
348
|
function queryGraph(graph, question, options = {}) {
|
|
262
349
|
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
263
350
|
const maxSeeds = options.maxSeeds ?? DEFAULT_MAX_SEEDS;
|
|
264
|
-
const
|
|
351
|
+
const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
|
|
352
|
+
const budget = options.budget ?? Math.max(DEFAULT_BUDGET, Math.ceil(tokenBudget * NODES_PER_TOKEN));
|
|
265
353
|
const allMatches = scoreNodes(graph, question);
|
|
266
354
|
const seeds = allMatches.length > 0 ? allMatches.slice(0, maxSeeds) : [];
|
|
267
355
|
const visited = /* @__PURE__ */ new Map();
|
|
268
356
|
const frontier = seeds.map((s) => ({ id: s.id, depth: 0 }));
|
|
269
|
-
|
|
270
|
-
|
|
357
|
+
let spentTokens = 0;
|
|
358
|
+
for (const seed of seeds) {
|
|
359
|
+
visited.set(seed.id, 0);
|
|
360
|
+
spentTokens += nodeTokenCost(graph, seed.id);
|
|
361
|
+
}
|
|
362
|
+
while (frontier.length > 0 && visited.size < budget && spentTokens < tokenBudget) {
|
|
271
363
|
const current = options.dfs ? frontier.pop() : frontier.shift();
|
|
272
364
|
if (current.depth >= maxDepth) continue;
|
|
273
365
|
const neighbors = [...graph.neighbors(current.id)].sort((a, b) => a.localeCompare(b));
|
|
274
366
|
for (const neighbor of neighbors) {
|
|
275
|
-
if (visited.has(neighbor) || visited.size >= budget) continue;
|
|
367
|
+
if (visited.has(neighbor) || visited.size >= budget || spentTokens >= tokenBudget) continue;
|
|
276
368
|
visited.set(neighbor, current.depth + 1);
|
|
369
|
+
spentTokens += nodeTokenCost(graph, neighbor);
|
|
277
370
|
frontier.push({ id: neighbor, depth: current.depth + 1 });
|
|
278
371
|
}
|
|
279
372
|
}
|
|
@@ -301,7 +394,30 @@ function queryGraph(graph, question, options = {}) {
|
|
|
301
394
|
edges.sort(
|
|
302
395
|
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
|
|
303
396
|
);
|
|
304
|
-
return { seeds, visited: visitedNodes, edges };
|
|
397
|
+
return enforceTokenBudget({ seeds, visited: visitedNodes, edges }, tokenBudget);
|
|
398
|
+
}
|
|
399
|
+
function enforceTokenBudget(result, tokenBudget) {
|
|
400
|
+
const cost = (value) => Math.ceil(JSON.stringify(value).length / 4);
|
|
401
|
+
const visited = [...result.visited];
|
|
402
|
+
let edges = [...result.edges];
|
|
403
|
+
let total = cost(result.seeds) + visited.reduce((s, n) => s + cost(n), 0) + edges.reduce((s, e) => s + cost(e), 0);
|
|
404
|
+
const seedIds = new Set(result.seeds.map((s) => s.id));
|
|
405
|
+
while (total > tokenBudget && visited.length > 0) {
|
|
406
|
+
const last = visited[visited.length - 1];
|
|
407
|
+
if (seedIds.has(last.id)) break;
|
|
408
|
+
visited.pop();
|
|
409
|
+
total -= cost(last);
|
|
410
|
+
const remaining = [];
|
|
411
|
+
for (const edge of edges) {
|
|
412
|
+
if (edge.source === last.id || edge.target === last.id) {
|
|
413
|
+
total -= cost(edge);
|
|
414
|
+
} else {
|
|
415
|
+
remaining.push(edge);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
edges = remaining;
|
|
419
|
+
}
|
|
420
|
+
return { seeds: result.seeds, visited, edges };
|
|
305
421
|
}
|
|
306
422
|
function shortestPath(graph, fromQuery, toQuery) {
|
|
307
423
|
const from = resolveNode(graph, fromQuery);
|
|
@@ -327,25 +443,25 @@ function shortestPath(graph, fromQuery, toQuery) {
|
|
|
327
443
|
if (!visited.has(to.id)) {
|
|
328
444
|
return { from, to, found: false, path: [], edges: [] };
|
|
329
445
|
}
|
|
330
|
-
const
|
|
446
|
+
const path18 = [to.id];
|
|
331
447
|
let cursor = to.id;
|
|
332
448
|
while (cursor !== from.id) {
|
|
333
449
|
const prev = predecessor.get(cursor);
|
|
334
450
|
if (!prev) break;
|
|
335
|
-
|
|
451
|
+
path18.unshift(prev);
|
|
336
452
|
cursor = prev;
|
|
337
453
|
}
|
|
338
454
|
const edges = [];
|
|
339
|
-
for (let i = 0; i <
|
|
340
|
-
const a =
|
|
341
|
-
const b =
|
|
455
|
+
for (let i = 0; i < path18.length - 1; i++) {
|
|
456
|
+
const a = path18[i];
|
|
457
|
+
const b = path18[i + 1];
|
|
342
458
|
const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];
|
|
343
459
|
if (key) {
|
|
344
460
|
const attrs = graph.getEdgeAttributes(key);
|
|
345
461
|
edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });
|
|
346
462
|
}
|
|
347
463
|
}
|
|
348
|
-
return { from, to, found: true, path:
|
|
464
|
+
return { from, to, found: true, path: path18, edges };
|
|
349
465
|
}
|
|
350
466
|
function explainNode(graph, query) {
|
|
351
467
|
const match = resolveNode(graph, query);
|
|
@@ -371,7 +487,7 @@ function explainNode(graph, query) {
|
|
|
371
487
|
incoming
|
|
372
488
|
};
|
|
373
489
|
}
|
|
374
|
-
var STOPWORDS, DEFAULT_MAX_DEPTH, DEFAULT_MAX_SEEDS, DEFAULT_BUDGET;
|
|
490
|
+
var STOPWORDS, FUZZY_MIN_TOKEN_LEN, FUZZY_THRESHOLD, SUBTOKEN_MATCH_SCORE, SUBSTRING_MATCH_SCORE, FUZZY_MATCH_WEIGHT, DEFAULT_MAX_DEPTH, DEFAULT_MAX_SEEDS, DEFAULT_BUDGET, DEFAULT_TOKEN_BUDGET, NODES_PER_TOKEN;
|
|
375
491
|
var init_query = __esm({
|
|
376
492
|
"src/query.ts"() {
|
|
377
493
|
"use strict";
|
|
@@ -407,9 +523,300 @@ var init_query = __esm({
|
|
|
407
523
|
"does",
|
|
408
524
|
"that's"
|
|
409
525
|
]);
|
|
526
|
+
FUZZY_MIN_TOKEN_LEN = 3;
|
|
527
|
+
FUZZY_THRESHOLD = 0.7;
|
|
528
|
+
SUBTOKEN_MATCH_SCORE = 1;
|
|
529
|
+
SUBSTRING_MATCH_SCORE = 0.6;
|
|
530
|
+
FUZZY_MATCH_WEIGHT = 0.5;
|
|
410
531
|
DEFAULT_MAX_DEPTH = 2;
|
|
411
532
|
DEFAULT_MAX_SEEDS = 5;
|
|
412
533
|
DEFAULT_BUDGET = 40;
|
|
534
|
+
DEFAULT_TOKEN_BUDGET = 2e3;
|
|
535
|
+
NODES_PER_TOKEN = 1 / 10;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// src/impact.ts
|
|
540
|
+
function affectedBy(graph, nodeQuery, options = {}) {
|
|
541
|
+
const target = resolveNode(graph, nodeQuery);
|
|
542
|
+
if (!target) return null;
|
|
543
|
+
const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
|
|
544
|
+
const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
|
|
545
|
+
const affected = [];
|
|
546
|
+
const visited = /* @__PURE__ */ new Set([target.id]);
|
|
547
|
+
let frontier = [target.id];
|
|
548
|
+
let truncated = false;
|
|
549
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {
|
|
550
|
+
const nextFrontier = /* @__PURE__ */ new Map();
|
|
551
|
+
for (const current of [...frontier].sort((a, b) => a.localeCompare(b))) {
|
|
552
|
+
graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
|
|
553
|
+
if (visited.has(source)) return;
|
|
554
|
+
const relation = edgeAttrs.relation;
|
|
555
|
+
if (!DEPENDENCY_RELATIONS.has(relation)) return;
|
|
556
|
+
const confidence = edgeAttrs.confidence;
|
|
557
|
+
const existing = nextFrontier.get(source);
|
|
558
|
+
if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
|
|
559
|
+
const attrs = graph.getNodeAttributes(source);
|
|
560
|
+
nextFrontier.set(source, {
|
|
561
|
+
id: source,
|
|
562
|
+
label: attrs.label ?? source,
|
|
563
|
+
sourceFile: attrs.sourceFile ?? "",
|
|
564
|
+
sourceLocation: attrs.sourceLocation ?? "",
|
|
565
|
+
depth,
|
|
566
|
+
via: { relation, confidence, dependsOn: current }
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const roundNodes = [...nextFrontier.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
571
|
+
for (const node of roundNodes) {
|
|
572
|
+
if (affected.length >= limit) {
|
|
573
|
+
truncated = true;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
visited.add(node.id);
|
|
577
|
+
affected.push(node);
|
|
578
|
+
}
|
|
579
|
+
frontier = roundNodes.filter((n) => visited.has(n.id)).map((n) => n.id);
|
|
580
|
+
}
|
|
581
|
+
const byConfidence = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
|
|
582
|
+
for (const node of affected) byConfidence[node.via.confidence] += 1;
|
|
583
|
+
return { target, affected, byConfidence, maxDepth, truncated };
|
|
584
|
+
}
|
|
585
|
+
var DEPENDENCY_RELATIONS, DEFAULT_IMPACT_DEPTH, DEFAULT_IMPACT_LIMIT, CONFIDENCE_RANK2;
|
|
586
|
+
var init_impact = __esm({
|
|
587
|
+
"src/impact.ts"() {
|
|
588
|
+
"use strict";
|
|
589
|
+
init_cjs_shims();
|
|
590
|
+
init_query();
|
|
591
|
+
DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
|
|
592
|
+
"calls",
|
|
593
|
+
"imports",
|
|
594
|
+
"imports_from",
|
|
595
|
+
"inherits",
|
|
596
|
+
"implements",
|
|
597
|
+
"mixes_in",
|
|
598
|
+
"embeds",
|
|
599
|
+
"references",
|
|
600
|
+
"re_exports",
|
|
601
|
+
"method",
|
|
602
|
+
"contains"
|
|
603
|
+
]);
|
|
604
|
+
DEFAULT_IMPACT_DEPTH = 3;
|
|
605
|
+
DEFAULT_IMPACT_LIMIT = 200;
|
|
606
|
+
CONFIDENCE_RANK2 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// src/testmap.ts
|
|
611
|
+
function isTestFile(path18) {
|
|
612
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(path18));
|
|
613
|
+
}
|
|
614
|
+
function selectionFromImpact(graph, targetIds, targetLabel) {
|
|
615
|
+
const best = /* @__PURE__ */ new Map();
|
|
616
|
+
let affectedNodeCount = 0;
|
|
617
|
+
for (const id of targetIds) {
|
|
618
|
+
const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
|
|
619
|
+
if (!impact) continue;
|
|
620
|
+
affectedNodeCount += impact.affected.length;
|
|
621
|
+
for (const node of impact.affected) {
|
|
622
|
+
if (!isTestFile(node.sourceFile)) continue;
|
|
623
|
+
const existing = best.get(node.sourceFile);
|
|
624
|
+
if (!existing || node.depth < existing.depth) {
|
|
625
|
+
best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const testFiles = [...best.entries()].map(([file, info]) => ({ file, depth: info.depth, via: info.via })).sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
630
|
+
return { target: targetLabel, testFiles, affectedNodeCount };
|
|
631
|
+
}
|
|
632
|
+
function testsForNode(graph, nodeQuery) {
|
|
633
|
+
const match = resolveNode(graph, nodeQuery);
|
|
634
|
+
if (!match) return null;
|
|
635
|
+
return selectionFromImpact(graph, [match.id], match.id);
|
|
636
|
+
}
|
|
637
|
+
function testsForChangedFiles(graph, changedFiles) {
|
|
638
|
+
const fileIds = [];
|
|
639
|
+
const selfSelected = /* @__PURE__ */ new Map();
|
|
640
|
+
for (const file of changedFiles) {
|
|
641
|
+
if (isTestFile(file)) {
|
|
642
|
+
selfSelected.set(file, { depth: 0, via: "(changed directly)" });
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (graph.hasNode(file)) fileIds.push(file);
|
|
646
|
+
}
|
|
647
|
+
const selection = selectionFromImpact(graph, fileIds, changedFiles.join(", "));
|
|
648
|
+
for (const [file, info] of selfSelected) {
|
|
649
|
+
if (!selection.testFiles.some((t) => t.file === file)) {
|
|
650
|
+
selection.testFiles.push({ file, depth: info.depth, via: info.via });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
|
|
654
|
+
return selection;
|
|
655
|
+
}
|
|
656
|
+
var TEST_FILE_PATTERNS, TEST_REACH_DEPTH, TEST_REACH_LIMIT;
|
|
657
|
+
var init_testmap = __esm({
|
|
658
|
+
"src/testmap.ts"() {
|
|
659
|
+
"use strict";
|
|
660
|
+
init_cjs_shims();
|
|
661
|
+
init_impact();
|
|
662
|
+
init_query();
|
|
663
|
+
TEST_FILE_PATTERNS = [
|
|
664
|
+
/(^|\/)tests?\//,
|
|
665
|
+
// tests/ or test/ directory
|
|
666
|
+
/(^|\/)__tests__\//,
|
|
667
|
+
/(^|\/)spec\//,
|
|
668
|
+
/\.test\.[cm]?[jt]sx?$/,
|
|
669
|
+
/\.spec\.[cm]?[jt]sx?$/,
|
|
670
|
+
/(^|\/)test_[^/]+\.py$/,
|
|
671
|
+
/_test\.py$/,
|
|
672
|
+
/_test\.go$/,
|
|
673
|
+
/Tests?\.(java|cs)$/,
|
|
674
|
+
/_spec\.rb$/,
|
|
675
|
+
/_test\.rb$/
|
|
676
|
+
];
|
|
677
|
+
TEST_REACH_DEPTH = 4;
|
|
678
|
+
TEST_REACH_LIMIT = 2e3;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// src/context.ts
|
|
683
|
+
function lineNumberOf(sourceLocation) {
|
|
684
|
+
const match = /^L(\d+)$/.exec(sourceLocation);
|
|
685
|
+
return match ? Number(match[1]) : null;
|
|
686
|
+
}
|
|
687
|
+
function isReadableFile(sourceFile) {
|
|
688
|
+
return sourceFile !== "" && !sourceFile.startsWith("<") && !sourceFile.includes("://");
|
|
689
|
+
}
|
|
690
|
+
function rankForTask(graph, task, maxSeeds = DEFAULT_MAX_SEEDS2) {
|
|
691
|
+
const seeds = scoreNodes(graph, task).slice(0, maxSeeds);
|
|
692
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
693
|
+
for (const seed of seeds) {
|
|
694
|
+
ranked.set(seed.id, { id: seed.id, score: seed.score, reason: "matches the task" });
|
|
695
|
+
}
|
|
696
|
+
for (const seed of seeds) {
|
|
697
|
+
graph.forEachEdge(seed.id, (_edge, attrs, source, target) => {
|
|
698
|
+
const neighbor = source === seed.id ? target : source;
|
|
699
|
+
if (ranked.has(neighbor)) return;
|
|
700
|
+
const relation = String(attrs.relation);
|
|
701
|
+
const direction = source === seed.id ? `${relation} ->` : `<- ${relation}`;
|
|
702
|
+
ranked.set(neighbor, {
|
|
703
|
+
id: neighbor,
|
|
704
|
+
score: seed.score * NEIGHBOR_DECAY,
|
|
705
|
+
reason: `${direction} ${seed.label}`
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return [...ranked.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
|
|
710
|
+
}
|
|
711
|
+
function snippetRange(startLine, fileLineCount, entityStartsInFile, maxLines) {
|
|
712
|
+
const nextStart = entityStartsInFile.find((l) => l > startLine);
|
|
713
|
+
const hardEnd = nextStart !== void 0 ? nextStart - 1 : fileLineCount;
|
|
714
|
+
return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };
|
|
715
|
+
}
|
|
716
|
+
async function buildContextPack(graph, task, readFile21, options = {}) {
|
|
717
|
+
const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;
|
|
718
|
+
const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;
|
|
719
|
+
const ranked = rankForTask(graph, task, options.maxSeeds);
|
|
720
|
+
const entityStarts = /* @__PURE__ */ new Map();
|
|
721
|
+
graph.forEachNode((_id, attrs) => {
|
|
722
|
+
const file = attrs.sourceFile;
|
|
723
|
+
const line = lineNumberOf(attrs.sourceLocation ?? "");
|
|
724
|
+
if (file && line !== null) {
|
|
725
|
+
const starts = entityStarts.get(file) ?? [];
|
|
726
|
+
starts.push(line);
|
|
727
|
+
entityStarts.set(file, starts);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
for (const starts of entityStarts.values()) starts.sort((a, b) => a - b);
|
|
731
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
732
|
+
const readLines = async (file) => {
|
|
733
|
+
const cached = fileCache.get(file);
|
|
734
|
+
if (cached !== void 0) return cached;
|
|
735
|
+
let lines;
|
|
736
|
+
try {
|
|
737
|
+
lines = (await readFile21(file)).split("\n");
|
|
738
|
+
} catch {
|
|
739
|
+
lines = null;
|
|
740
|
+
}
|
|
741
|
+
fileCache.set(file, lines);
|
|
742
|
+
return lines;
|
|
743
|
+
};
|
|
744
|
+
const snippets = [];
|
|
745
|
+
const overflow = [];
|
|
746
|
+
const coveredRanges = /* @__PURE__ */ new Map();
|
|
747
|
+
let tokens = 0;
|
|
748
|
+
for (const node of ranked) {
|
|
749
|
+
const attrs = graph.getNodeAttributes(node.id);
|
|
750
|
+
const file = attrs.sourceFile ?? "";
|
|
751
|
+
const startLine = lineNumberOf(attrs.sourceLocation ?? "");
|
|
752
|
+
if (!isReadableFile(file) || startLine === null) continue;
|
|
753
|
+
const lines = await readLines(file);
|
|
754
|
+
if (lines === null) continue;
|
|
755
|
+
const { start, end } = snippetRange(startLine, lines.length, entityStarts.get(file) ?? [], maxSnippetLines);
|
|
756
|
+
const covered = (coveredRanges.get(file) ?? []).some((r) => start >= r.start && end <= r.end);
|
|
757
|
+
if (covered) continue;
|
|
758
|
+
const code = lines.slice(start - 1, end).join("\n");
|
|
759
|
+
const cost = tokensOf2(code) + 15;
|
|
760
|
+
if (tokens + cost > tokenBudget) {
|
|
761
|
+
overflow.push({ nodeId: node.id, file: `${file}:L${startLine}` });
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
tokens += cost;
|
|
765
|
+
snippets.push({
|
|
766
|
+
nodeId: node.id,
|
|
767
|
+
label: attrs.label ?? node.id,
|
|
768
|
+
file,
|
|
769
|
+
startLine: start,
|
|
770
|
+
endLine: end,
|
|
771
|
+
code,
|
|
772
|
+
reason: node.reason,
|
|
773
|
+
score: node.score
|
|
774
|
+
});
|
|
775
|
+
const ranges = coveredRanges.get(file) ?? [];
|
|
776
|
+
ranges.push({ start, end });
|
|
777
|
+
coveredRanges.set(file, ranges);
|
|
778
|
+
}
|
|
779
|
+
return { task, snippets, tokens, tokenBudget, overflow };
|
|
780
|
+
}
|
|
781
|
+
function renderContextPack(pack) {
|
|
782
|
+
const lines = [
|
|
783
|
+
`# Context pack: ${pack.task}`,
|
|
784
|
+
"",
|
|
785
|
+
`_~${pack.tokens} of ${pack.tokenBudget} token budget, ${pack.snippets.length} snippet(s)._`,
|
|
786
|
+
""
|
|
787
|
+
];
|
|
788
|
+
if (pack.snippets.length === 0) {
|
|
789
|
+
lines.push("No matching code found \u2014 try different keywords.");
|
|
790
|
+
return lines.join("\n");
|
|
791
|
+
}
|
|
792
|
+
for (const snippet of pack.snippets) {
|
|
793
|
+
lines.push(`## ${snippet.label} \u2014 \`${snippet.file}:L${snippet.startLine}-L${snippet.endLine}\``);
|
|
794
|
+
lines.push(`_${snippet.reason}_`);
|
|
795
|
+
lines.push("```");
|
|
796
|
+
lines.push(snippet.code);
|
|
797
|
+
lines.push("```");
|
|
798
|
+
lines.push("");
|
|
799
|
+
}
|
|
800
|
+
if (pack.overflow.length > 0) {
|
|
801
|
+
lines.push(`## Didn't fit the budget (${pack.overflow.length})`);
|
|
802
|
+
for (const item of pack.overflow.slice(0, 15)) {
|
|
803
|
+
lines.push(`- ${item.nodeId} (${item.file})`);
|
|
804
|
+
}
|
|
805
|
+
lines.push("");
|
|
806
|
+
}
|
|
807
|
+
return lines.join("\n");
|
|
808
|
+
}
|
|
809
|
+
var DEFAULT_CONTEXT_BUDGET, DEFAULT_MAX_SEEDS2, DEFAULT_MAX_SNIPPET_LINES, NEIGHBOR_DECAY, tokensOf2;
|
|
810
|
+
var init_context = __esm({
|
|
811
|
+
"src/context.ts"() {
|
|
812
|
+
"use strict";
|
|
813
|
+
init_cjs_shims();
|
|
814
|
+
init_query();
|
|
815
|
+
DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
816
|
+
DEFAULT_MAX_SEEDS2 = 8;
|
|
817
|
+
DEFAULT_MAX_SNIPPET_LINES = 60;
|
|
818
|
+
NEIGHBOR_DECAY = 0.4;
|
|
819
|
+
tokensOf2 = (text) => Math.ceil(text.length / 4);
|
|
413
820
|
}
|
|
414
821
|
});
|
|
415
822
|
|
|
@@ -432,12 +839,13 @@ function createGraphifyMcpServer(outDir) {
|
|
|
432
839
|
inputSchema: {
|
|
433
840
|
question: import_zod2.z.string().describe("Natural-language question or keywords to search the graph for."),
|
|
434
841
|
dfs: import_zod2.z.boolean().optional().describe("Use depth-first traversal instead of the default breadth-first."),
|
|
435
|
-
budget: import_zod2.z.number().int().positive().optional().describe("
|
|
842
|
+
budget: import_zod2.z.number().int().positive().optional().describe("Hard cap on how many nodes to include."),
|
|
843
|
+
tokenBudget: import_zod2.z.number().int().positive().optional().describe("Approximate cap, in tokens, on the serialized size of the result (default 2000).")
|
|
436
844
|
}
|
|
437
845
|
},
|
|
438
|
-
async ({ question, dfs, budget }) => {
|
|
846
|
+
async ({ question, dfs, budget, tokenBudget }) => {
|
|
439
847
|
const graph = await loadGraph(outDir);
|
|
440
|
-
const result = queryGraph(graph, question, { dfs, budget });
|
|
848
|
+
const result = queryGraph(graph, question, { dfs, budget, tokenBudget });
|
|
441
849
|
return textResult(JSON.stringify(result, null, 2));
|
|
442
850
|
}
|
|
443
851
|
);
|
|
@@ -476,41 +884,257 @@ function createGraphifyMcpServer(outDir) {
|
|
|
476
884
|
return textResult(JSON.stringify(result, null, 2));
|
|
477
885
|
}
|
|
478
886
|
);
|
|
887
|
+
server.registerTool(
|
|
888
|
+
"affected",
|
|
889
|
+
{
|
|
890
|
+
description: 'Reverse impact analysis \u2014 "what breaks if I change X". Walks incoming dependency edges (calls/imports/inherits/...) transitively and returns everything that depends on the node, grouped by depth, with per-confidence blast-radius counts.',
|
|
891
|
+
inputSchema: {
|
|
892
|
+
node: import_zod2.z.string().describe("Name/label (or id) of the node being changed."),
|
|
893
|
+
depth: import_zod2.z.number().int().positive().optional().describe("How many reverse hops to follow (default 3)."),
|
|
894
|
+
limit: import_zod2.z.number().int().positive().optional().describe("Cap on affected nodes reported (default 200).")
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
async ({ node, depth, limit }) => {
|
|
898
|
+
const graph = await loadGraph(outDir);
|
|
899
|
+
const result = affectedBy(graph, node, { maxDepth: depth, limit });
|
|
900
|
+
if (!result) {
|
|
901
|
+
return textResult(`No node matched "${node}".`);
|
|
902
|
+
}
|
|
903
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
server.registerTool(
|
|
907
|
+
"context",
|
|
908
|
+
{
|
|
909
|
+
description: "Token-budgeted working-set pack: the actual code snippets relevant to a task, graph-ranked and packed to a budget. Call this FIRST when starting a task \u2014 it replaces query-then-read-whole-files.",
|
|
910
|
+
inputSchema: {
|
|
911
|
+
task: import_zod2.z.string().describe("What you are about to do, in natural language."),
|
|
912
|
+
budget: import_zod2.z.number().int().positive().optional().describe("Approximate token cap (default 4000).")
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
async ({ task, budget }) => {
|
|
916
|
+
const graph = await loadGraph(outDir);
|
|
917
|
+
const pack = await buildContextPack(graph, task, (p) => fs23.readFile(p, "utf-8"), {
|
|
918
|
+
tokenBudget: budget
|
|
919
|
+
});
|
|
920
|
+
return textResult(renderContextPack(pack));
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
server.registerTool(
|
|
924
|
+
"tests",
|
|
925
|
+
{
|
|
926
|
+
description: "Structural test selection: the minimal test files worth running to verify a change to the given node, via reverse dependency reachability \u2014 no coverage instrumentation.",
|
|
927
|
+
inputSchema: {
|
|
928
|
+
node: import_zod2.z.string().describe("Name/label (or id) of the changed node.")
|
|
929
|
+
}
|
|
930
|
+
},
|
|
931
|
+
async ({ node }) => {
|
|
932
|
+
const graph = await loadGraph(outDir);
|
|
933
|
+
const result = testsForNode(graph, node);
|
|
934
|
+
if (!result) {
|
|
935
|
+
return textResult(`No node matched "${node}".`);
|
|
936
|
+
}
|
|
937
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
938
|
+
}
|
|
939
|
+
);
|
|
479
940
|
return server;
|
|
480
941
|
}
|
|
481
942
|
async function startServer(graphPath, transport = new import_stdio.StdioServerTransport()) {
|
|
482
|
-
const outDir =
|
|
483
|
-
const fileName =
|
|
943
|
+
const outDir = path16.dirname(path16.resolve(graphPath));
|
|
944
|
+
const fileName = path16.basename(graphPath);
|
|
484
945
|
validateGraphPath(fileName, outDir);
|
|
485
946
|
const server = createGraphifyMcpServer(outDir);
|
|
486
947
|
await server.connect(transport);
|
|
487
948
|
}
|
|
488
949
|
async function main(argv = process.argv) {
|
|
489
|
-
const graphPath = argv[2] ??
|
|
950
|
+
const graphPath = argv[2] ?? path16.join(process.cwd(), "graphify-out", "graph.json");
|
|
490
951
|
await startServer(graphPath);
|
|
491
952
|
}
|
|
492
|
-
var
|
|
953
|
+
var fs23, path16, import_mcp, import_stdio, import_zod2;
|
|
493
954
|
var init_server = __esm({
|
|
494
955
|
"src/mcp/server.ts"() {
|
|
495
956
|
"use strict";
|
|
496
957
|
init_cjs_shims();
|
|
497
|
-
|
|
958
|
+
fs23 = __toESM(require("fs/promises"), 1);
|
|
959
|
+
path16 = __toESM(require("path"), 1);
|
|
498
960
|
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
499
961
|
import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
500
962
|
import_zod2 = require("zod");
|
|
963
|
+
init_context();
|
|
501
964
|
init_graphStore();
|
|
965
|
+
init_impact();
|
|
502
966
|
init_query();
|
|
967
|
+
init_testmap();
|
|
968
|
+
init_security();
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// src/extractors/mysql.ts
|
|
973
|
+
var mysql_exports = {};
|
|
974
|
+
__export(mysql_exports, {
|
|
975
|
+
extractMysql: () => extractMysql
|
|
976
|
+
});
|
|
977
|
+
async function defaultQueryFn(dsn) {
|
|
978
|
+
const { connection } = validateDsn(dsn);
|
|
979
|
+
const mysql = await import("mysql2/promise");
|
|
980
|
+
const conn = await mysql.createConnection({
|
|
981
|
+
host: connection.host,
|
|
982
|
+
port: connection.port,
|
|
983
|
+
user: connection.user,
|
|
984
|
+
password: connection.password,
|
|
985
|
+
database: connection.database
|
|
986
|
+
});
|
|
987
|
+
return {
|
|
988
|
+
query: async (sql, params) => {
|
|
989
|
+
const [rows] = await conn.execute(sql, [...params]);
|
|
990
|
+
return rows;
|
|
991
|
+
},
|
|
992
|
+
close: () => conn.end()
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
async function extractMysql(dsn, queryFn) {
|
|
996
|
+
const { safeDisplay, connection } = validateDsn(dsn);
|
|
997
|
+
const schema = connection.database;
|
|
998
|
+
let query = queryFn;
|
|
999
|
+
let close;
|
|
1000
|
+
if (!query) {
|
|
1001
|
+
const live = await defaultQueryFn(dsn);
|
|
1002
|
+
query = live.query;
|
|
1003
|
+
close = live.close;
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
const nodes = [];
|
|
1007
|
+
const edges = [];
|
|
1008
|
+
nodes.push({ id: schemaId(schema), label: schema, sourceFile: safeDisplay, sourceLocation: schema });
|
|
1009
|
+
const tableRows = await query(
|
|
1010
|
+
"SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
|
|
1011
|
+
[schema]
|
|
1012
|
+
);
|
|
1013
|
+
const tables = tableRows.map((row) => ({
|
|
1014
|
+
name: String(row.TABLE_NAME),
|
|
1015
|
+
isView: String(row.TABLE_TYPE).toUpperCase() === "VIEW"
|
|
1016
|
+
}));
|
|
1017
|
+
const tableNames = new Set(tables.map((t) => t.name));
|
|
1018
|
+
for (const table of tables) {
|
|
1019
|
+
nodes.push({
|
|
1020
|
+
id: tableId(schema, table.name),
|
|
1021
|
+
label: `${table.isView ? "view" : "table"} ${table.name}`,
|
|
1022
|
+
sourceFile: safeDisplay,
|
|
1023
|
+
sourceLocation: table.name
|
|
1024
|
+
});
|
|
1025
|
+
edges.push({
|
|
1026
|
+
source: schemaId(schema),
|
|
1027
|
+
target: tableId(schema, table.name),
|
|
1028
|
+
relation: "contains",
|
|
1029
|
+
confidence: "EXTRACTED"
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
const columnRows = await query(
|
|
1033
|
+
"SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, ORDINAL_POSITION",
|
|
1034
|
+
[schema]
|
|
1035
|
+
);
|
|
1036
|
+
for (const row of columnRows) {
|
|
1037
|
+
const table = String(row.TABLE_NAME);
|
|
1038
|
+
const column = String(row.COLUMN_NAME);
|
|
1039
|
+
if (!tableNames.has(table)) continue;
|
|
1040
|
+
nodes.push({
|
|
1041
|
+
id: columnId(schema, table, column),
|
|
1042
|
+
label: `${table}.${column}: ${String(row.COLUMN_TYPE)}`,
|
|
1043
|
+
sourceFile: safeDisplay,
|
|
1044
|
+
sourceLocation: `${table}.${column}`
|
|
1045
|
+
});
|
|
1046
|
+
edges.push({
|
|
1047
|
+
source: tableId(schema, table),
|
|
1048
|
+
target: columnId(schema, table, column),
|
|
1049
|
+
relation: "contains",
|
|
1050
|
+
confidence: "EXTRACTED"
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
const fkRows = await query(
|
|
1054
|
+
"SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL ORDER BY TABLE_NAME, COLUMN_NAME",
|
|
1055
|
+
[schema]
|
|
1056
|
+
);
|
|
1057
|
+
for (const row of fkRows) {
|
|
1058
|
+
const table = String(row.TABLE_NAME);
|
|
1059
|
+
const column = String(row.COLUMN_NAME);
|
|
1060
|
+
const referenced = String(row.REFERENCED_TABLE_NAME);
|
|
1061
|
+
if (!tableNames.has(table) || !tableNames.has(referenced)) continue;
|
|
1062
|
+
edges.push({
|
|
1063
|
+
source: columnId(schema, table, column),
|
|
1064
|
+
target: tableId(schema, referenced),
|
|
1065
|
+
relation: "references",
|
|
1066
|
+
confidence: "EXTRACTED"
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const viewNames = tables.filter((t) => t.isView).map((t) => t.name);
|
|
1070
|
+
if (viewNames.length > 0) {
|
|
1071
|
+
await addViewEdges(query, schema, viewNames, tableNames, edges);
|
|
1072
|
+
}
|
|
1073
|
+
return { nodes, edges };
|
|
1074
|
+
} finally {
|
|
1075
|
+
await close?.();
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
async function addViewEdges(query, schema, viewNames, tableNames, edges) {
|
|
1079
|
+
try {
|
|
1080
|
+
const usageRows = await query(
|
|
1081
|
+
"SELECT VIEW_NAME, TABLE_NAME FROM information_schema.VIEW_TABLE_USAGE WHERE VIEW_SCHEMA = ? AND TABLE_SCHEMA = ? ORDER BY VIEW_NAME, TABLE_NAME",
|
|
1082
|
+
[schema, schema]
|
|
1083
|
+
);
|
|
1084
|
+
for (const row of usageRows) {
|
|
1085
|
+
const view = String(row.VIEW_NAME);
|
|
1086
|
+
const table = String(row.TABLE_NAME);
|
|
1087
|
+
if (!tableNames.has(view) || !tableNames.has(table) || view === table) continue;
|
|
1088
|
+
edges.push({
|
|
1089
|
+
source: tableId(schema, view),
|
|
1090
|
+
target: tableId(schema, table),
|
|
1091
|
+
relation: "references",
|
|
1092
|
+
confidence: "EXTRACTED"
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
return;
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
const definitionRows = await query(
|
|
1099
|
+
"SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
|
|
1100
|
+
[schema]
|
|
1101
|
+
);
|
|
1102
|
+
for (const row of definitionRows) {
|
|
1103
|
+
const view = String(row.TABLE_NAME);
|
|
1104
|
+
if (!viewNames.includes(view)) continue;
|
|
1105
|
+
const definition = String(row.VIEW_DEFINITION ?? "").toLowerCase();
|
|
1106
|
+
for (const table of [...tableNames].sort((a, b) => a.localeCompare(b))) {
|
|
1107
|
+
if (table === view) continue;
|
|
1108
|
+
if (new RegExp(`\\b${table.toLowerCase()}\\b`).test(definition)) {
|
|
1109
|
+
edges.push({
|
|
1110
|
+
source: tableId(schema, view),
|
|
1111
|
+
target: tableId(schema, table),
|
|
1112
|
+
relation: "references",
|
|
1113
|
+
confidence: "INFERRED"
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
var schemaId, tableId, columnId;
|
|
1120
|
+
var init_mysql = __esm({
|
|
1121
|
+
"src/extractors/mysql.ts"() {
|
|
1122
|
+
"use strict";
|
|
1123
|
+
init_cjs_shims();
|
|
503
1124
|
init_security();
|
|
1125
|
+
schemaId = (schema) => `db:${schema}`;
|
|
1126
|
+
tableId = (schema, table) => `db:${schema}::${table}`;
|
|
1127
|
+
columnId = (schema, table, column) => `db:${schema}::${table}.${column}`;
|
|
504
1128
|
}
|
|
505
1129
|
});
|
|
506
1130
|
|
|
507
1131
|
// src/cli/index.ts
|
|
508
1132
|
init_cjs_shims();
|
|
509
|
-
var
|
|
510
|
-
var
|
|
511
|
-
var
|
|
512
|
-
var
|
|
513
|
-
var
|
|
1133
|
+
var import_node_child_process3 = require("child_process");
|
|
1134
|
+
var fs24 = __toESM(require("fs/promises"), 1);
|
|
1135
|
+
var os3 = __toESM(require("os"), 1);
|
|
1136
|
+
var path17 = __toESM(require("path"), 1);
|
|
1137
|
+
var import_node_util3 = require("util");
|
|
514
1138
|
var import_chokidar = require("chokidar");
|
|
515
1139
|
var import_commander = require("commander");
|
|
516
1140
|
|
|
@@ -841,7 +1465,8 @@ init_graphStore();
|
|
|
841
1465
|
|
|
842
1466
|
// src/pipeline.ts
|
|
843
1467
|
init_cjs_shims();
|
|
844
|
-
var
|
|
1468
|
+
var fs14 = __toESM(require("fs/promises"), 1);
|
|
1469
|
+
var path9 = __toESM(require("path"), 1);
|
|
845
1470
|
|
|
846
1471
|
// src/build.ts
|
|
847
1472
|
init_cjs_shims();
|
|
@@ -947,10 +1572,39 @@ function buildGraph(extractions) {
|
|
|
947
1572
|
return graph;
|
|
948
1573
|
}
|
|
949
1574
|
|
|
950
|
-
// src/
|
|
1575
|
+
// src/extractionCache.ts
|
|
951
1576
|
init_cjs_shims();
|
|
952
|
-
var
|
|
1577
|
+
var import_node_crypto3 = require("crypto");
|
|
1578
|
+
var fs4 = __toESM(require("fs/promises"), 1);
|
|
953
1579
|
var path3 = __toESM(require("path"), 1);
|
|
1580
|
+
var ExtractionCache = class {
|
|
1581
|
+
dir;
|
|
1582
|
+
constructor(outDir) {
|
|
1583
|
+
this.dir = path3.join(outDir, "cache");
|
|
1584
|
+
}
|
|
1585
|
+
key(relFile, content) {
|
|
1586
|
+
return (0, import_node_crypto3.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
|
|
1587
|
+
}
|
|
1588
|
+
async get(key) {
|
|
1589
|
+
try {
|
|
1590
|
+
const raw = await fs4.readFile(path3.join(this.dir, `${key}.json`), "utf-8");
|
|
1591
|
+
const parsed = JSON.parse(raw);
|
|
1592
|
+
if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
|
|
1593
|
+
return parsed;
|
|
1594
|
+
} catch {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
async put(key, extraction) {
|
|
1599
|
+
await fs4.mkdir(this.dir, { recursive: true });
|
|
1600
|
+
await fs4.writeFile(path3.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
// src/detect.ts
|
|
1605
|
+
init_cjs_shims();
|
|
1606
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
1607
|
+
var path4 = __toESM(require("path"), 1);
|
|
954
1608
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
955
1609
|
"node_modules",
|
|
956
1610
|
".git",
|
|
@@ -1031,13 +1685,13 @@ function countWords(content) {
|
|
|
1031
1685
|
return trimmed.split(/\s+/).length;
|
|
1032
1686
|
}
|
|
1033
1687
|
function readTextSafely(filePath) {
|
|
1034
|
-
const buf =
|
|
1688
|
+
const buf = fs5.readFileSync(filePath);
|
|
1035
1689
|
return buf.toString("utf-8");
|
|
1036
1690
|
}
|
|
1037
1691
|
function collectFiles(root) {
|
|
1038
|
-
const scanRoot =
|
|
1039
|
-
const
|
|
1040
|
-
if (!
|
|
1692
|
+
const scanRoot = path4.resolve(root);
|
|
1693
|
+
const stat2 = fs5.statSync(scanRoot);
|
|
1694
|
+
if (!stat2.isDirectory()) {
|
|
1041
1695
|
throw new Error(`collectFiles: not a directory: ${scanRoot}`);
|
|
1042
1696
|
}
|
|
1043
1697
|
const files = {
|
|
@@ -1055,15 +1709,15 @@ function collectFiles(root) {
|
|
|
1055
1709
|
const dir = stack.pop();
|
|
1056
1710
|
let entries;
|
|
1057
1711
|
try {
|
|
1058
|
-
entries =
|
|
1712
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1059
1713
|
} catch {
|
|
1060
1714
|
continue;
|
|
1061
1715
|
}
|
|
1062
1716
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1063
1717
|
for (const entry of entries) {
|
|
1064
1718
|
if (entry.isSymbolicLink()) continue;
|
|
1065
|
-
const fullPath =
|
|
1066
|
-
const relPath =
|
|
1719
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1720
|
+
const relPath = path4.relative(scanRoot, fullPath);
|
|
1067
1721
|
if (entry.isDirectory()) {
|
|
1068
1722
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
1069
1723
|
stack.push(fullPath);
|
|
@@ -1074,7 +1728,7 @@ function collectFiles(root) {
|
|
|
1074
1728
|
skippedSensitive.push(relPath);
|
|
1075
1729
|
continue;
|
|
1076
1730
|
}
|
|
1077
|
-
const ext =
|
|
1731
|
+
const ext = path4.extname(entry.name);
|
|
1078
1732
|
const category = categorize(ext);
|
|
1079
1733
|
if (category === null) continue;
|
|
1080
1734
|
files[category].push(relPath);
|
|
@@ -1103,15 +1757,15 @@ function collectFiles(root) {
|
|
|
1103
1757
|
|
|
1104
1758
|
// src/extract.ts
|
|
1105
1759
|
init_cjs_shims();
|
|
1106
|
-
var
|
|
1760
|
+
var path7 = __toESM(require("path"), 1);
|
|
1107
1761
|
|
|
1108
1762
|
// src/extractors/csharp.ts
|
|
1109
1763
|
init_cjs_shims();
|
|
1110
|
-
var
|
|
1764
|
+
var fs6 = __toESM(require("fs/promises"), 1);
|
|
1111
1765
|
|
|
1112
1766
|
// src/extractors/common.ts
|
|
1113
1767
|
init_cjs_shims();
|
|
1114
|
-
var
|
|
1768
|
+
var path5 = __toESM(require("path"), 1);
|
|
1115
1769
|
function namedChildren(node) {
|
|
1116
1770
|
return node.namedChildren.filter((c) => c !== null);
|
|
1117
1771
|
}
|
|
@@ -1119,7 +1773,7 @@ function descendantsOfType(node, types) {
|
|
|
1119
1773
|
return node.descendantsOfType(types).filter((c) => c !== null);
|
|
1120
1774
|
}
|
|
1121
1775
|
function toPosix(p) {
|
|
1122
|
-
return p.split(
|
|
1776
|
+
return p.split(path5.sep).join("/");
|
|
1123
1777
|
}
|
|
1124
1778
|
function lineOf(node) {
|
|
1125
1779
|
return `L${node.startPosition.row + 1}`;
|
|
@@ -1132,9 +1786,9 @@ function externalId(name) {
|
|
|
1132
1786
|
}
|
|
1133
1787
|
function resolveModuleRef(sourceFile, specifier) {
|
|
1134
1788
|
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
1135
|
-
const dir =
|
|
1136
|
-
const joined = specifier.startsWith("/") ? specifier :
|
|
1137
|
-
const normalized =
|
|
1789
|
+
const dir = path5.posix.dirname(toPosix(sourceFile));
|
|
1790
|
+
const joined = specifier.startsWith("/") ? specifier : path5.posix.join(dir, specifier);
|
|
1791
|
+
const normalized = path5.posix.normalize(joined);
|
|
1138
1792
|
return { id: normalized, label: specifier, external: false };
|
|
1139
1793
|
}
|
|
1140
1794
|
return { id: `module:${specifier}`, label: specifier, external: true };
|
|
@@ -1190,16 +1844,16 @@ async function createParser(grammarName) {
|
|
|
1190
1844
|
}
|
|
1191
1845
|
|
|
1192
1846
|
// src/extractors/csharp.ts
|
|
1193
|
-
async function extractCSharp(filePath) {
|
|
1847
|
+
async function extractCSharp(filePath, displayPath) {
|
|
1194
1848
|
const parser = await createParser("c_sharp");
|
|
1195
|
-
const raw = await
|
|
1849
|
+
const raw = await fs6.readFile(filePath);
|
|
1196
1850
|
const content = raw.toString("utf-8");
|
|
1197
1851
|
const tree = parser.parse(content);
|
|
1198
1852
|
if (!tree) {
|
|
1199
1853
|
throw new Error(`extractCSharp: parser produced no tree for ${filePath}`);
|
|
1200
1854
|
}
|
|
1201
1855
|
const root = tree.rootNode;
|
|
1202
|
-
const sourceFile = toPosix(filePath);
|
|
1856
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1203
1857
|
const builder = new ExtractionBuilder();
|
|
1204
1858
|
const fileId = sourceFile;
|
|
1205
1859
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1389,7 +2043,7 @@ async function extractCSharp(filePath) {
|
|
|
1389
2043
|
|
|
1390
2044
|
// src/extractors/go.ts
|
|
1391
2045
|
init_cjs_shims();
|
|
1392
|
-
var
|
|
2046
|
+
var fs7 = __toESM(require("fs/promises"), 1);
|
|
1393
2047
|
function stripQuotes(text) {
|
|
1394
2048
|
if (text.length >= 2) {
|
|
1395
2049
|
const first = text[0];
|
|
@@ -1404,16 +2058,16 @@ function defaultPackageQualifier(importPath) {
|
|
|
1404
2058
|
const segments = importPath.split("/");
|
|
1405
2059
|
return segments[segments.length - 1] ?? importPath;
|
|
1406
2060
|
}
|
|
1407
|
-
async function extractGo(filePath) {
|
|
2061
|
+
async function extractGo(filePath, displayPath) {
|
|
1408
2062
|
const parser = await createParser("go");
|
|
1409
|
-
const raw = await
|
|
2063
|
+
const raw = await fs7.readFile(filePath);
|
|
1410
2064
|
const content = raw.toString("utf-8");
|
|
1411
2065
|
const tree = parser.parse(content);
|
|
1412
2066
|
if (!tree) {
|
|
1413
2067
|
throw new Error(`extractGo: parser produced no tree for ${filePath}`);
|
|
1414
2068
|
}
|
|
1415
2069
|
const root = tree.rootNode;
|
|
1416
|
-
const sourceFile = toPosix(filePath);
|
|
2070
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1417
2071
|
const builder = new ExtractionBuilder();
|
|
1418
2072
|
const fileId = sourceFile;
|
|
1419
2073
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1611,17 +2265,17 @@ async function extractGo(filePath) {
|
|
|
1611
2265
|
|
|
1612
2266
|
// src/extractors/java.ts
|
|
1613
2267
|
init_cjs_shims();
|
|
1614
|
-
var
|
|
1615
|
-
async function extractJava(filePath) {
|
|
2268
|
+
var fs8 = __toESM(require("fs/promises"), 1);
|
|
2269
|
+
async function extractJava(filePath, displayPath) {
|
|
1616
2270
|
const parser = await createParser("java");
|
|
1617
|
-
const raw = await
|
|
2271
|
+
const raw = await fs8.readFile(filePath);
|
|
1618
2272
|
const content = raw.toString("utf-8");
|
|
1619
2273
|
const tree = parser.parse(content);
|
|
1620
2274
|
if (!tree) {
|
|
1621
2275
|
throw new Error(`extractJava: parser produced no tree for ${filePath}`);
|
|
1622
2276
|
}
|
|
1623
2277
|
const root = tree.rootNode;
|
|
1624
|
-
const sourceFile = toPosix(filePath);
|
|
2278
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1625
2279
|
const builder = new ExtractionBuilder();
|
|
1626
2280
|
const fileId = sourceFile;
|
|
1627
2281
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -1813,17 +2467,17 @@ async function extractJava(filePath) {
|
|
|
1813
2467
|
|
|
1814
2468
|
// src/extractors/python.ts
|
|
1815
2469
|
init_cjs_shims();
|
|
1816
|
-
var
|
|
1817
|
-
async function extractPython(filePath) {
|
|
2470
|
+
var fs9 = __toESM(require("fs/promises"), 1);
|
|
2471
|
+
async function extractPython(filePath, displayPath) {
|
|
1818
2472
|
const parser = await createParser("python");
|
|
1819
|
-
const raw = await
|
|
2473
|
+
const raw = await fs9.readFile(filePath);
|
|
1820
2474
|
const content = raw.toString("utf-8");
|
|
1821
2475
|
const tree = parser.parse(content);
|
|
1822
2476
|
if (!tree) {
|
|
1823
2477
|
throw new Error(`extractPython: parser produced no tree for ${filePath}`);
|
|
1824
2478
|
}
|
|
1825
2479
|
const root = tree.rootNode;
|
|
1826
|
-
const sourceFile = toPosix(filePath);
|
|
2480
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
1827
2481
|
const builder = new ExtractionBuilder();
|
|
1828
2482
|
const fileId = sourceFile;
|
|
1829
2483
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -2049,7 +2703,7 @@ async function extractPython(filePath) {
|
|
|
2049
2703
|
|
|
2050
2704
|
// src/extractors/ruby.ts
|
|
2051
2705
|
init_cjs_shims();
|
|
2052
|
-
var
|
|
2706
|
+
var fs10 = __toESM(require("fs/promises"), 1);
|
|
2053
2707
|
function conventionalConstantName(specifier) {
|
|
2054
2708
|
const lastSegment = specifier.split("/").filter(Boolean).pop() ?? specifier;
|
|
2055
2709
|
return lastSegment.split("_").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
@@ -2059,16 +2713,16 @@ function stringLiteralContent(node) {
|
|
|
2059
2713
|
const content = namedChildren(node).find((c) => c.type === "string_content");
|
|
2060
2714
|
return content ? content.text : "";
|
|
2061
2715
|
}
|
|
2062
|
-
async function extractRuby(filePath) {
|
|
2716
|
+
async function extractRuby(filePath, displayPath) {
|
|
2063
2717
|
const parser = await createParser("ruby");
|
|
2064
|
-
const raw = await
|
|
2718
|
+
const raw = await fs10.readFile(filePath);
|
|
2065
2719
|
const content = raw.toString("utf-8");
|
|
2066
2720
|
const tree = parser.parse(content);
|
|
2067
2721
|
if (!tree) {
|
|
2068
2722
|
throw new Error(`extractRuby: parser produced no tree for ${filePath}`);
|
|
2069
2723
|
}
|
|
2070
2724
|
const root = tree.rootNode;
|
|
2071
|
-
const sourceFile = toPosix(filePath);
|
|
2725
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
2072
2726
|
const builder = new ExtractionBuilder();
|
|
2073
2727
|
const fileId = sourceFile;
|
|
2074
2728
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -2246,7 +2900,7 @@ async function extractRuby(filePath) {
|
|
|
2246
2900
|
|
|
2247
2901
|
// src/extractors/rust.ts
|
|
2248
2902
|
init_cjs_shims();
|
|
2249
|
-
var
|
|
2903
|
+
var fs11 = __toESM(require("fs/promises"), 1);
|
|
2250
2904
|
function convertUsePath(segments) {
|
|
2251
2905
|
const first = segments[0];
|
|
2252
2906
|
if (first === "crate" || first === "self") {
|
|
@@ -2262,16 +2916,16 @@ function convertUsePath(segments) {
|
|
|
2262
2916
|
}
|
|
2263
2917
|
return segments.join("::");
|
|
2264
2918
|
}
|
|
2265
|
-
async function extractRust(filePath) {
|
|
2919
|
+
async function extractRust(filePath, displayPath) {
|
|
2266
2920
|
const parser = await createParser("rust");
|
|
2267
|
-
const raw = await
|
|
2921
|
+
const raw = await fs11.readFile(filePath);
|
|
2268
2922
|
const content = raw.toString("utf-8");
|
|
2269
2923
|
const tree = parser.parse(content);
|
|
2270
2924
|
if (!tree) {
|
|
2271
2925
|
throw new Error(`extractRust: parser produced no tree for ${filePath}`);
|
|
2272
2926
|
}
|
|
2273
2927
|
const root = tree.rootNode;
|
|
2274
|
-
const sourceFile = toPosix(filePath);
|
|
2928
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
2275
2929
|
const builder = new ExtractionBuilder();
|
|
2276
2930
|
const fileId = sourceFile;
|
|
2277
2931
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -2484,10 +3138,10 @@ async function extractRust(filePath) {
|
|
|
2484
3138
|
}
|
|
2485
3139
|
}
|
|
2486
3140
|
} else if (fn.type === "scoped_identifier") {
|
|
2487
|
-
const
|
|
3141
|
+
const path18 = fn.childForFieldName("path")?.text;
|
|
2488
3142
|
const name = fn.childForFieldName("name")?.text;
|
|
2489
|
-
if (
|
|
2490
|
-
const viaImport = importIndex.get(
|
|
3143
|
+
if (path18 && name) {
|
|
3144
|
+
const viaImport = importIndex.get(path18);
|
|
2491
3145
|
if (viaImport) {
|
|
2492
3146
|
targetId = viaImport.id;
|
|
2493
3147
|
confidence = "INFERRED";
|
|
@@ -2503,8 +3157,8 @@ async function extractRust(filePath) {
|
|
|
2503
3157
|
|
|
2504
3158
|
// src/extractors/typescript.ts
|
|
2505
3159
|
init_cjs_shims();
|
|
2506
|
-
var
|
|
2507
|
-
var
|
|
3160
|
+
var fs12 = __toESM(require("fs/promises"), 1);
|
|
3161
|
+
var path6 = __toESM(require("path"), 1);
|
|
2508
3162
|
function grammarForExtension(ext) {
|
|
2509
3163
|
const lower = ext.toLowerCase();
|
|
2510
3164
|
if (lower === ".tsx") return "tsx";
|
|
@@ -2529,18 +3183,18 @@ function firstStringArgument(call) {
|
|
|
2529
3183
|
if (!first || first.type !== "string") return null;
|
|
2530
3184
|
return stripQuotes2(first.text);
|
|
2531
3185
|
}
|
|
2532
|
-
async function extractTypeScript(filePath) {
|
|
2533
|
-
const ext =
|
|
3186
|
+
async function extractTypeScript(filePath, displayPath) {
|
|
3187
|
+
const ext = path6.extname(filePath);
|
|
2534
3188
|
const grammar = grammarForExtension(ext);
|
|
2535
3189
|
const parser = await createParser(grammar);
|
|
2536
|
-
const raw = await
|
|
3190
|
+
const raw = await fs12.readFile(filePath);
|
|
2537
3191
|
const content = raw.toString("utf-8");
|
|
2538
3192
|
const tree = parser.parse(content);
|
|
2539
3193
|
if (!tree) {
|
|
2540
3194
|
throw new Error(`extractTypeScript: parser produced no tree for ${filePath}`);
|
|
2541
3195
|
}
|
|
2542
3196
|
const root = tree.rootNode;
|
|
2543
|
-
const sourceFile = toPosix(filePath);
|
|
3197
|
+
const sourceFile = toPosix(displayPath ?? filePath);
|
|
2544
3198
|
const builder = new ExtractionBuilder();
|
|
2545
3199
|
const fileId = sourceFile;
|
|
2546
3200
|
builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
|
|
@@ -2857,13 +3511,13 @@ var EXTRACTOR_REGISTRY = {
|
|
|
2857
3511
|
".cs": extractCSharp,
|
|
2858
3512
|
".rb": extractRuby
|
|
2859
3513
|
};
|
|
2860
|
-
async function extract(filePath) {
|
|
2861
|
-
const ext =
|
|
3514
|
+
async function extract(filePath, displayPath) {
|
|
3515
|
+
const ext = path7.extname(filePath);
|
|
2862
3516
|
const extractor = EXTRACTOR_REGISTRY[ext];
|
|
2863
3517
|
if (!extractor) {
|
|
2864
3518
|
return { nodes: [], edges: [] };
|
|
2865
3519
|
}
|
|
2866
|
-
return extractor(filePath);
|
|
3520
|
+
return extractor(filePath, displayPath);
|
|
2867
3521
|
}
|
|
2868
3522
|
|
|
2869
3523
|
// src/report.ts
|
|
@@ -2945,37 +3599,206 @@ function renderReport(graph, analysis, now = /* @__PURE__ */ new Date()) {
|
|
|
2945
3599
|
return lines.join("\n");
|
|
2946
3600
|
}
|
|
2947
3601
|
|
|
2948
|
-
// src/
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
3602
|
+
// src/resolve.ts
|
|
3603
|
+
init_cjs_shims();
|
|
3604
|
+
var fs13 = __toESM(require("fs/promises"), 1);
|
|
3605
|
+
var path8 = __toESM(require("path"), 1);
|
|
3606
|
+
var CODE_EXTENSIONS2 = [
|
|
3607
|
+
".ts",
|
|
3608
|
+
".tsx",
|
|
3609
|
+
".mts",
|
|
3610
|
+
".cts",
|
|
3611
|
+
".js",
|
|
3612
|
+
".jsx",
|
|
3613
|
+
".mjs",
|
|
3614
|
+
".cjs",
|
|
3615
|
+
".py",
|
|
3616
|
+
".go",
|
|
3617
|
+
".rs",
|
|
3618
|
+
".java",
|
|
3619
|
+
".cs",
|
|
3620
|
+
".rb"
|
|
3621
|
+
];
|
|
3622
|
+
function isOpaque(id) {
|
|
3623
|
+
return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
|
|
3624
|
+
}
|
|
3625
|
+
var SELF_IMPORT_STEMS = (subpath) => subpath === "" ? ["index", "src/index", "lib/index", "source/index", "src/main"] : [
|
|
3626
|
+
subpath,
|
|
3627
|
+
`src/${subpath}`,
|
|
3628
|
+
`lib/${subpath}`,
|
|
3629
|
+
`source/${subpath}`,
|
|
3630
|
+
`${subpath}/index`,
|
|
3631
|
+
`src/${subpath}/index`,
|
|
3632
|
+
`lib/${subpath}/index`
|
|
3633
|
+
];
|
|
3634
|
+
function selfImportCandidates(spec, selfNames) {
|
|
3635
|
+
const selfName = selfNames.find((n) => spec === n || spec.startsWith(`${n}/`));
|
|
3636
|
+
if (selfName === void 0) return null;
|
|
3637
|
+
const subpath = spec === selfName ? "" : spec.slice(selfName.length + 1);
|
|
3638
|
+
return SELF_IMPORT_STEMS(subpath).flatMap((stem) => CODE_EXTENSIONS2.map((ext) => `${stem}${ext}`));
|
|
3639
|
+
}
|
|
3640
|
+
async function readSelfNames(root) {
|
|
3641
|
+
try {
|
|
3642
|
+
const pkg = JSON.parse(await fs13.readFile(path8.join(root, "package.json"), "utf-8"));
|
|
3643
|
+
return typeof pkg.name === "string" && pkg.name.length > 0 ? [pkg.name] : [];
|
|
3644
|
+
} catch {
|
|
3645
|
+
return [];
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
function stripKnownExtension(p) {
|
|
3649
|
+
for (const ext of CODE_EXTENSIONS2) {
|
|
3650
|
+
if (p.endsWith(ext)) return p.slice(0, -ext.length);
|
|
3651
|
+
}
|
|
3652
|
+
return null;
|
|
3653
|
+
}
|
|
3654
|
+
function candidatePaths(guessed) {
|
|
3655
|
+
const stem = stripKnownExtension(guessed) ?? guessed;
|
|
3656
|
+
const candidates = [];
|
|
3657
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}${ext}`);
|
|
3658
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${guessed}/index${ext}`);
|
|
3659
|
+
if (stem !== guessed) {
|
|
3660
|
+
for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}/index${ext}`);
|
|
3661
|
+
}
|
|
3662
|
+
return candidates.filter((c) => c !== guessed);
|
|
3663
|
+
}
|
|
3664
|
+
function uniqueMatch(guessed, candidates, realIds) {
|
|
3665
|
+
const matches = /* @__PURE__ */ new Set();
|
|
3666
|
+
for (const candidate of candidates) {
|
|
3667
|
+
if (realIds.has(candidate)) matches.add(candidate);
|
|
3668
|
+
}
|
|
3669
|
+
if (matches.size !== 1) return null;
|
|
3670
|
+
return [...matches][0];
|
|
3671
|
+
}
|
|
3672
|
+
function resolveCrossFileReferences(extractions, options = {}) {
|
|
3673
|
+
const selfNames = options.selfNames ?? [];
|
|
3674
|
+
const realIds = /* @__PURE__ */ new Set();
|
|
3675
|
+
const realFiles = /* @__PURE__ */ new Set();
|
|
3676
|
+
for (const extraction of extractions) {
|
|
3677
|
+
for (const node of extraction.nodes) realIds.add(node.id);
|
|
3678
|
+
const first = extraction.nodes[0];
|
|
3679
|
+
if (first && first.id === first.sourceFile && !isOpaque(first.id)) {
|
|
3680
|
+
realFiles.add(first.id);
|
|
3681
|
+
}
|
|
3682
|
+
for (const node of extraction.nodes) {
|
|
3683
|
+
const sep3 = node.id.indexOf("::");
|
|
3684
|
+
if (sep3 > 0) realFiles.add(node.id.slice(0, sep3));
|
|
3685
|
+
}
|
|
3686
|
+
for (const edge of extraction.edges) {
|
|
3687
|
+
if (edge.relation === "contains") realFiles.add(edge.source);
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
const remap = /* @__PURE__ */ new Map();
|
|
3691
|
+
const resolveId = (id) => {
|
|
3692
|
+
if (id.startsWith("module:") && selfNames.length > 0) {
|
|
3693
|
+
const cachedSelf = remap.get(id);
|
|
3694
|
+
if (cachedSelf !== void 0) return cachedSelf;
|
|
3695
|
+
const candidates = selfImportCandidates(id.slice("module:".length), selfNames);
|
|
3696
|
+
if (candidates !== null) {
|
|
3697
|
+
const resolved2 = uniqueMatch(id, candidates, realFiles);
|
|
3698
|
+
if (resolved2 !== null) {
|
|
3699
|
+
remap.set(id, resolved2);
|
|
3700
|
+
return resolved2;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
return null;
|
|
3704
|
+
}
|
|
3705
|
+
if (isOpaque(id)) return null;
|
|
3706
|
+
const cached = remap.get(id);
|
|
3707
|
+
if (cached !== void 0) return cached;
|
|
3708
|
+
const sep3 = id.indexOf("::");
|
|
3709
|
+
let resolved;
|
|
3710
|
+
if (sep3 > 0) {
|
|
3711
|
+
if (realIds.has(id)) return null;
|
|
3712
|
+
const guessedPath = id.slice(0, sep3);
|
|
3713
|
+
const name = id.slice(sep3);
|
|
3714
|
+
const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
|
|
3715
|
+
resolved = uniqueMatch(id, candidates, realIds);
|
|
3716
|
+
} else {
|
|
3717
|
+
if (realFiles.has(id)) return null;
|
|
3718
|
+
resolved = uniqueMatch(id, candidatePaths(id), realFiles);
|
|
3719
|
+
}
|
|
3720
|
+
if (resolved !== null) remap.set(id, resolved);
|
|
3721
|
+
return resolved;
|
|
3722
|
+
};
|
|
3723
|
+
let resolvedEndpoints = 0;
|
|
3724
|
+
for (const extraction of extractions) {
|
|
3725
|
+
for (const edge of extraction.edges) {
|
|
3726
|
+
const source = resolveId(edge.source);
|
|
3727
|
+
if (source !== null) {
|
|
3728
|
+
edge.source = source;
|
|
3729
|
+
resolvedEndpoints++;
|
|
3730
|
+
}
|
|
3731
|
+
const target = resolveId(edge.target);
|
|
3732
|
+
if (target !== null) {
|
|
3733
|
+
edge.target = target;
|
|
3734
|
+
resolvedEndpoints++;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
let droppedPlaceholders = 0;
|
|
3739
|
+
for (const extraction of extractions) {
|
|
3740
|
+
const before = extraction.nodes.length;
|
|
3741
|
+
extraction.nodes = extraction.nodes.filter((node) => !remap.has(node.id));
|
|
3742
|
+
droppedPlaceholders += before - extraction.nodes.length;
|
|
3743
|
+
}
|
|
3744
|
+
return { resolvedEndpoints, droppedPlaceholders };
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
// src/pipeline.ts
|
|
3748
|
+
async function runPipeline(root, options = {}) {
|
|
3749
|
+
const progress = options.onProgress ?? (() => {
|
|
3750
|
+
});
|
|
3751
|
+
const resolvedRoot = path9.resolve(root);
|
|
3752
|
+
progress(`Scanning ${resolvedRoot} ...`);
|
|
3753
|
+
const manifest = collectFiles(resolvedRoot);
|
|
3754
|
+
progress(
|
|
3755
|
+
`Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
|
|
3756
|
+
);
|
|
3757
|
+
const outDir = options.outDir ?? path9.join(resolvedRoot, "graphify-out");
|
|
3758
|
+
const cache = new ExtractionCache(outDir);
|
|
3759
|
+
progress("Extracting...");
|
|
3760
|
+
const extractions = [];
|
|
3761
|
+
let cacheHits = 0;
|
|
3762
|
+
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
3763
|
+
const filePath = path9.relative(process.cwd(), path9.join(resolvedRoot, relFile)) || relFile;
|
|
3764
|
+
try {
|
|
3765
|
+
const key = cache.key(relFile, await fs14.readFile(path9.join(resolvedRoot, relFile)));
|
|
3766
|
+
let extraction = options.update ? await cache.get(key) : null;
|
|
3767
|
+
if (extraction) {
|
|
3768
|
+
cacheHits++;
|
|
3769
|
+
} else {
|
|
3770
|
+
extraction = await extract(filePath, relFile);
|
|
3771
|
+
await cache.put(key, extraction);
|
|
3772
|
+
}
|
|
3773
|
+
extractions.push(extraction);
|
|
3774
|
+
} catch (error) {
|
|
3775
|
+
progress(` extraction failed for ${relFile}: ${error.message}`);
|
|
3776
|
+
throw error;
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
if (options.update) {
|
|
3780
|
+
progress(` cache: ${cacheHits} hit(s), ${extractions.length - cacheHits} re-extracted.`);
|
|
3781
|
+
}
|
|
3782
|
+
if (options.extraExtractions && options.extraExtractions.length > 0) {
|
|
3783
|
+
extractions.push(...options.extraExtractions);
|
|
3784
|
+
}
|
|
3785
|
+
progress("Resolving cross-file references...");
|
|
3786
|
+
const resolveStats = resolveCrossFileReferences(extractions, {
|
|
3787
|
+
selfNames: await readSelfNames(resolvedRoot)
|
|
3788
|
+
});
|
|
3789
|
+
if (resolveStats.resolvedEndpoints > 0) {
|
|
3790
|
+
progress(
|
|
3791
|
+
` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
|
|
3792
|
+
);
|
|
3793
|
+
}
|
|
3794
|
+
progress("Building graph...");
|
|
3795
|
+
const graph = buildGraph(extractions);
|
|
3796
|
+
progress(`Clustering (${options.algorithm ?? "louvain"})...`);
|
|
3797
|
+
cluster(graph, { algorithm: options.algorithm, maxIterations: options.maxIterations });
|
|
3798
|
+
progress("Analyzing...");
|
|
3799
|
+
const analysis = analyze(graph);
|
|
2976
3800
|
progress("Rendering report...");
|
|
2977
3801
|
const report = renderReport(graph, analysis);
|
|
2978
|
-
const outDir = options.outDir ?? path7.join(resolvedRoot, "graphify-out");
|
|
2979
3802
|
progress(`Exporting to ${outDir} ...`);
|
|
2980
3803
|
await exportGraph(
|
|
2981
3804
|
graph,
|
|
@@ -2996,6 +3819,232 @@ async function runPipeline(root, options = {}) {
|
|
|
2996
3819
|
// src/cli/index.ts
|
|
2997
3820
|
init_security();
|
|
2998
3821
|
|
|
3822
|
+
// src/cli/commands/affected.ts
|
|
3823
|
+
init_cjs_shims();
|
|
3824
|
+
init_graphStore();
|
|
3825
|
+
init_impact();
|
|
3826
|
+
async function runAffected(nodeName, options = {}) {
|
|
3827
|
+
const graph = await loadGraph();
|
|
3828
|
+
const result = affectedBy(graph, nodeName, { maxDepth: options.depth, limit: options.limit });
|
|
3829
|
+
if (!result) {
|
|
3830
|
+
console.log(`No node matched "${nodeName}".`);
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
console.log(`Impact of changing ${result.target.label} (${result.target.id}):`);
|
|
3834
|
+
if (result.affected.length === 0) {
|
|
3835
|
+
console.log(" Nothing in the graph depends on it \u2014 no incoming dependency edges.");
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
let currentDepth = 0;
|
|
3839
|
+
for (const node of result.affected) {
|
|
3840
|
+
if (node.depth !== currentDepth) {
|
|
3841
|
+
currentDepth = node.depth;
|
|
3842
|
+
console.log("");
|
|
3843
|
+
console.log(currentDepth === 1 ? "Directly affected:" : `Affected at depth ${currentDepth}:`);
|
|
3844
|
+
}
|
|
3845
|
+
const isPlaceholder = node.sourceFile === "<unknown>" || node.sourceFile === "<external>" || node.sourceFile === "";
|
|
3846
|
+
const where = isPlaceholder ? "(reference only)" : `${node.sourceFile}${node.sourceLocation ? `:${node.sourceLocation}` : ""}`;
|
|
3847
|
+
console.log(
|
|
3848
|
+
` ${node.label} \u2014 ${where} (--${node.via.relation}--> ${node.via.dependsOn}, ${node.via.confidence})`
|
|
3849
|
+
);
|
|
3850
|
+
}
|
|
3851
|
+
console.log("");
|
|
3852
|
+
const { EXTRACTED, INFERRED, AMBIGUOUS } = result.byConfidence;
|
|
3853
|
+
console.log(
|
|
3854
|
+
`Blast radius: ${result.affected.length} node(s) within ${result.maxDepth} hop(s) \u2014 ${EXTRACTED} certain (EXTRACTED), ${INFERRED} likely (INFERRED), ${AMBIGUOUS} uncertain (AMBIGUOUS).`
|
|
3855
|
+
);
|
|
3856
|
+
if (result.truncated) {
|
|
3857
|
+
console.log("(List truncated \u2014 raise --limit to see more.)");
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
// src/cli/commands/benchmark.ts
|
|
3862
|
+
init_cjs_shims();
|
|
3863
|
+
var fs15 = __toESM(require("fs/promises"), 1);
|
|
3864
|
+
init_graphStore();
|
|
3865
|
+
init_query();
|
|
3866
|
+
init_testmap();
|
|
3867
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
3868
|
+
async function benchmarkQuestion(graph, question, readFile21) {
|
|
3869
|
+
const result = queryGraph(graph, question);
|
|
3870
|
+
const graphTokens = tokensOf(JSON.stringify(result));
|
|
3871
|
+
const sourceFiles = /* @__PURE__ */ new Set();
|
|
3872
|
+
for (const node of result.visited) {
|
|
3873
|
+
if (node.sourceFile && !node.sourceFile.startsWith("<") && !node.sourceFile.includes("://")) {
|
|
3874
|
+
sourceFiles.add(node.sourceFile);
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
let naiveTokens = 0;
|
|
3878
|
+
let filesRead = 0;
|
|
3879
|
+
let filesMissing = 0;
|
|
3880
|
+
for (const file of [...sourceFiles].sort((a, b) => a.localeCompare(b))) {
|
|
3881
|
+
try {
|
|
3882
|
+
naiveTokens += tokensOf(await readFile21(file));
|
|
3883
|
+
filesRead++;
|
|
3884
|
+
} catch {
|
|
3885
|
+
filesMissing++;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
return {
|
|
3889
|
+
question,
|
|
3890
|
+
graphTokens,
|
|
3891
|
+
naiveTokens,
|
|
3892
|
+
reduction: graphTokens > 0 && naiveTokens > 0 ? naiveTokens / graphTokens : 0,
|
|
3893
|
+
filesRead,
|
|
3894
|
+
filesMissing
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
var NON_QUESTION_FILE = /(^|\/)(examples?|scripts?|bench(?:mark)?s?|fixtures?|docs?|\.github)\/|\.(config|conf)\.[^/]+$|(^|\/)(rollup|webpack|vite|vitest|jest|babel|tsup|eslint|prettier)\.[^/]*$/i;
|
|
3898
|
+
function defaultQuestions(graph, count = 5) {
|
|
3899
|
+
const entries = [...graph.nodes()].map((id) => ({
|
|
3900
|
+
id,
|
|
3901
|
+
degree: graph.degree(id),
|
|
3902
|
+
label: graph.getNodeAttribute(id, "label") ?? id,
|
|
3903
|
+
file: graph.getNodeAttribute(id, "sourceFile") ?? ""
|
|
3904
|
+
})).filter((e) => e.id.includes("::")).filter((e) => !isTestFile(e.file) && !NON_QUESTION_FILE.test(e.file)).sort((a, b) => b.degree - a.degree || a.id.localeCompare(b.id));
|
|
3905
|
+
return entries.slice(0, count).map((e) => `how does ${e.label} work`);
|
|
3906
|
+
}
|
|
3907
|
+
async function runBenchmark(questions) {
|
|
3908
|
+
const graph = await loadGraph();
|
|
3909
|
+
const effective = questions.length > 0 ? questions : defaultQuestions(graph);
|
|
3910
|
+
if (effective.length === 0) {
|
|
3911
|
+
console.log("The graph has no entity nodes to benchmark against \u2014 build it first.");
|
|
3912
|
+
return [];
|
|
3913
|
+
}
|
|
3914
|
+
const rows = [];
|
|
3915
|
+
for (const question of effective) {
|
|
3916
|
+
rows.push(await benchmarkQuestion(graph, question, (p) => fs15.readFile(p, "utf-8")));
|
|
3917
|
+
}
|
|
3918
|
+
console.log("Token cost per question \u2014 graph answer vs reading the files it touches:\n");
|
|
3919
|
+
for (const row of rows) {
|
|
3920
|
+
const reduction = row.reduction > 0 ? `${row.reduction.toFixed(1)}x` : "n/a";
|
|
3921
|
+
console.log(` "${row.question}"`);
|
|
3922
|
+
console.log(
|
|
3923
|
+
` graph: ~${row.graphTokens} tokens | files: ~${row.naiveTokens} tokens (${row.filesRead} file(s)${row.filesMissing ? `, ${row.filesMissing} unreadable` : ""}) | reduction: ${reduction}`
|
|
3924
|
+
);
|
|
3925
|
+
}
|
|
3926
|
+
const scored = rows.filter((r) => r.reduction > 0);
|
|
3927
|
+
if (scored.length > 0) {
|
|
3928
|
+
const avg = scored.reduce((s, r) => s + r.reduction, 0) / scored.length;
|
|
3929
|
+
console.log(`
|
|
3930
|
+
Average reduction: ${avg.toFixed(1)}x across ${scored.length} question(s).`);
|
|
3931
|
+
} else {
|
|
3932
|
+
console.log("\nNo reduction could be measured \u2014 run from the project root so source files are readable.");
|
|
3933
|
+
}
|
|
3934
|
+
return rows;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
// src/cli/commands/check.ts
|
|
3938
|
+
init_cjs_shims();
|
|
3939
|
+
var fs16 = __toESM(require("fs/promises"), 1);
|
|
3940
|
+
var path10 = __toESM(require("path"), 1);
|
|
3941
|
+
|
|
3942
|
+
// src/check.ts
|
|
3943
|
+
init_cjs_shims();
|
|
3944
|
+
var DEPENDENCY_RELATIONS2 = /* @__PURE__ */ new Set([
|
|
3945
|
+
"calls",
|
|
3946
|
+
"imports",
|
|
3947
|
+
"imports_from",
|
|
3948
|
+
"inherits",
|
|
3949
|
+
"implements",
|
|
3950
|
+
"mixes_in",
|
|
3951
|
+
"embeds",
|
|
3952
|
+
"re_exports"
|
|
3953
|
+
]);
|
|
3954
|
+
function globToRegExp(glob) {
|
|
3955
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3956
|
+
const pattern = escaped.replace(
|
|
3957
|
+
/\*\*|\*|\?/g,
|
|
3958
|
+
(token) => token === "**" ? ".*" : token === "*" ? "[^/]*" : "[^/]"
|
|
3959
|
+
);
|
|
3960
|
+
return new RegExp(`^${pattern}$`);
|
|
3961
|
+
}
|
|
3962
|
+
function validateRules(value) {
|
|
3963
|
+
const config = value;
|
|
3964
|
+
if (!config || !Array.isArray(config.rules)) {
|
|
3965
|
+
throw new Error('Rules file must be JSON of shape { "rules": [{ "name", "from", "disallow": [...] }] }');
|
|
3966
|
+
}
|
|
3967
|
+
for (const rule of config.rules) {
|
|
3968
|
+
if (!rule.name || typeof rule.from !== "string" || !Array.isArray(rule.disallow)) {
|
|
3969
|
+
throw new Error(`Malformed rule "${rule.name ?? "(unnamed)"}" \u2014 need name, from, and disallow[].`);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return config;
|
|
3973
|
+
}
|
|
3974
|
+
function checkRules(graph, config) {
|
|
3975
|
+
const compiled = config.rules.map((rule) => ({
|
|
3976
|
+
rule,
|
|
3977
|
+
from: globToRegExp(rule.from),
|
|
3978
|
+
disallow: rule.disallow.map(globToRegExp)
|
|
3979
|
+
}));
|
|
3980
|
+
const violations = [];
|
|
3981
|
+
graph.forEachEdge((_edge, attrs, source, target) => {
|
|
3982
|
+
const relation = String(attrs.relation);
|
|
3983
|
+
if (!DEPENDENCY_RELATIONS2.has(relation)) return;
|
|
3984
|
+
const fromFile = graph.getNodeAttribute(source, "sourceFile") ?? "";
|
|
3985
|
+
const toFile = graph.getNodeAttribute(target, "sourceFile") ?? "";
|
|
3986
|
+
if (!fromFile || !toFile || fromFile === toFile) return;
|
|
3987
|
+
for (const { rule, from, disallow } of compiled) {
|
|
3988
|
+
if (!from.test(fromFile)) continue;
|
|
3989
|
+
if (disallow.some((d) => d.test(toFile))) {
|
|
3990
|
+
violations.push({
|
|
3991
|
+
rule: rule.name,
|
|
3992
|
+
reason: rule.reason,
|
|
3993
|
+
fromFile,
|
|
3994
|
+
fromNode: source,
|
|
3995
|
+
toFile,
|
|
3996
|
+
toNode: target,
|
|
3997
|
+
relation
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
});
|
|
4002
|
+
violations.sort(
|
|
4003
|
+
(a, b) => a.rule.localeCompare(b.rule) || a.fromFile.localeCompare(b.fromFile) || a.toFile.localeCompare(b.toFile) || a.fromNode.localeCompare(b.fromNode)
|
|
4004
|
+
);
|
|
4005
|
+
return violations;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
// src/cli/commands/check.ts
|
|
4009
|
+
init_graphStore();
|
|
4010
|
+
async function runCheck(options = {}) {
|
|
4011
|
+
const rulesPath = path10.resolve(options.rules ?? "graphify.rules.json");
|
|
4012
|
+
let raw;
|
|
4013
|
+
try {
|
|
4014
|
+
raw = await fs16.readFile(rulesPath, "utf-8");
|
|
4015
|
+
} catch {
|
|
4016
|
+
throw new Error(
|
|
4017
|
+
`No rules file at ${rulesPath} \u2014 create graphify.rules.json with { "rules": [{ "name": "...", "from": "src/a/**", "disallow": ["src/b/**"] }] }.`
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
4020
|
+
const config = validateRules(JSON.parse(raw));
|
|
4021
|
+
const graph = await loadGraph();
|
|
4022
|
+
const violations = checkRules(graph, config);
|
|
4023
|
+
if (violations.length === 0) {
|
|
4024
|
+
console.log(`OK \u2014 ${config.rules.length} rule(s), no violations.`);
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
console.log(`${violations.length} violation(s):`);
|
|
4028
|
+
for (const v of violations) {
|
|
4029
|
+
console.log(` [${v.rule}] ${v.fromNode} --${v.relation}--> ${v.toNode}`);
|
|
4030
|
+
console.log(` ${v.fromFile} must not depend on ${v.toFile}${v.reason ? ` \u2014 ${v.reason}` : ""}`);
|
|
4031
|
+
}
|
|
4032
|
+
process.exitCode = 1;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
// src/cli/commands/context.ts
|
|
4036
|
+
init_cjs_shims();
|
|
4037
|
+
var fs17 = __toESM(require("fs/promises"), 1);
|
|
4038
|
+
init_context();
|
|
4039
|
+
init_graphStore();
|
|
4040
|
+
async function runContext(task, options = {}) {
|
|
4041
|
+
const graph = await loadGraph();
|
|
4042
|
+
const pack = await buildContextPack(graph, task, (p) => fs17.readFile(p, "utf-8"), {
|
|
4043
|
+
tokenBudget: options.budget
|
|
4044
|
+
});
|
|
4045
|
+
console.log(renderContextPack(pack));
|
|
4046
|
+
}
|
|
4047
|
+
|
|
2999
4048
|
// src/cli/commands/explain.ts
|
|
3000
4049
|
init_cjs_shims();
|
|
3001
4050
|
init_graphStore();
|
|
@@ -3007,9 +4056,13 @@ async function runExplain(nodeName) {
|
|
|
3007
4056
|
console.log(`No node matched "${nodeName}".`);
|
|
3008
4057
|
return;
|
|
3009
4058
|
}
|
|
3010
|
-
const location = result.sourceLocation ? `:${result.sourceLocation}` : "";
|
|
3011
4059
|
console.log(result.label);
|
|
3012
|
-
|
|
4060
|
+
if (result.sourceFile === "<unknown>" || result.sourceFile === "<external>" || result.sourceFile === "") {
|
|
4061
|
+
console.log(" location: (reference only \u2014 no declaration seen; check its outgoing edges for the real definition)");
|
|
4062
|
+
} else {
|
|
4063
|
+
const location = result.sourceLocation ? `:${result.sourceLocation}` : "";
|
|
4064
|
+
console.log(` location: ${result.sourceFile}${location}`);
|
|
4065
|
+
}
|
|
3013
4066
|
if (result.communityLabel) {
|
|
3014
4067
|
console.log(` community: ${result.communityLabel}`);
|
|
3015
4068
|
}
|
|
@@ -3033,32 +4086,685 @@ async function runExplain(nodeName) {
|
|
|
3033
4086
|
}
|
|
3034
4087
|
}
|
|
3035
4088
|
|
|
4089
|
+
// src/cli/commands/review.ts
|
|
4090
|
+
init_cjs_shims();
|
|
4091
|
+
|
|
4092
|
+
// src/diff.ts
|
|
4093
|
+
init_cjs_shims();
|
|
4094
|
+
var import_node_child_process = require("child_process");
|
|
4095
|
+
var fs18 = __toESM(require("fs/promises"), 1);
|
|
4096
|
+
var os = __toESM(require("os"), 1);
|
|
4097
|
+
var path11 = __toESM(require("path"), 1);
|
|
4098
|
+
var import_node_util = require("util");
|
|
4099
|
+
init_impact();
|
|
4100
|
+
init_testmap();
|
|
4101
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
4102
|
+
async function graphForDirectory(root) {
|
|
4103
|
+
const manifest = collectFiles(root);
|
|
4104
|
+
const extractions = [];
|
|
4105
|
+
for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
|
|
4106
|
+
extractions.push(await extract(path11.join(root, relFile), relFile));
|
|
4107
|
+
}
|
|
4108
|
+
resolveCrossFileReferences(extractions, { selfNames: await readSelfNames(root) });
|
|
4109
|
+
return buildGraph(extractions);
|
|
4110
|
+
}
|
|
4111
|
+
async function checkoutRev(repoRoot, rev) {
|
|
4112
|
+
const dest = await fs18.mkdtemp(path11.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
|
|
4113
|
+
const { stdout } = await execFileAsync(
|
|
4114
|
+
"git",
|
|
4115
|
+
["-C", repoRoot, "archive", "--format=tar", rev],
|
|
4116
|
+
{ encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }
|
|
4117
|
+
);
|
|
4118
|
+
await new Promise((resolve10, reject) => {
|
|
4119
|
+
const tar = (0, import_node_child_process.execFile)("tar", ["-x", "-C", dest], (error) => error ? reject(error) : resolve10());
|
|
4120
|
+
tar.stdin?.end(stdout);
|
|
4121
|
+
});
|
|
4122
|
+
return dest;
|
|
4123
|
+
}
|
|
4124
|
+
function edgeSignature(graph, id) {
|
|
4125
|
+
const signature = /* @__PURE__ */ new Set();
|
|
4126
|
+
graph.forEachOutEdge(id, (_e, attrs, _s, target) => signature.add(`-> ${String(attrs.relation)} ${target}`));
|
|
4127
|
+
graph.forEachInEdge(id, (_e, attrs, source) => signature.add(`<- ${String(attrs.relation)} ${source}`));
|
|
4128
|
+
return signature;
|
|
4129
|
+
}
|
|
4130
|
+
function describeSymbol(graph, id) {
|
|
4131
|
+
const attrs = graph.getNodeAttributes(id);
|
|
4132
|
+
const impact = affectedBy(graph, id, { maxDepth: 3 });
|
|
4133
|
+
const tests = testsForNode(graph, id);
|
|
4134
|
+
return {
|
|
4135
|
+
id,
|
|
4136
|
+
label: attrs.label ?? id,
|
|
4137
|
+
sourceFile: attrs.sourceFile ?? "",
|
|
4138
|
+
blastRadius: impact?.affected.length ?? 0,
|
|
4139
|
+
tests: tests?.testFiles.map((t) => t.file) ?? []
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
function entityIds(graph) {
|
|
4143
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4144
|
+
graph.forEachNode((id) => {
|
|
4145
|
+
if (id.includes("::")) ids.add(id);
|
|
4146
|
+
});
|
|
4147
|
+
return ids;
|
|
4148
|
+
}
|
|
4149
|
+
function diffGraphs(baseGraph, headGraph, base, head) {
|
|
4150
|
+
const baseIds = entityIds(baseGraph);
|
|
4151
|
+
const headIds = entityIds(headGraph);
|
|
4152
|
+
const added = [];
|
|
4153
|
+
const removed = [];
|
|
4154
|
+
const rewired = [];
|
|
4155
|
+
for (const id of [...headIds].sort((a, b) => a.localeCompare(b))) {
|
|
4156
|
+
if (!baseIds.has(id)) {
|
|
4157
|
+
added.push(describeSymbol(headGraph, id));
|
|
4158
|
+
continue;
|
|
4159
|
+
}
|
|
4160
|
+
const before = edgeSignature(baseGraph, id);
|
|
4161
|
+
const after = edgeSignature(headGraph, id);
|
|
4162
|
+
const gained = [...after].filter((e) => !before.has(e)).sort((a, b) => a.localeCompare(b));
|
|
4163
|
+
const lost = [...before].filter((e) => !after.has(e)).sort((a, b) => a.localeCompare(b));
|
|
4164
|
+
if (gained.length > 0 || lost.length > 0) {
|
|
4165
|
+
rewired.push({ ...describeSymbol(headGraph, id), gainedEdges: gained, lostEdges: lost });
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
for (const id of [...baseIds].sort((a, b) => a.localeCompare(b))) {
|
|
4169
|
+
if (!headIds.has(id)) removed.push(describeSymbol(baseGraph, id));
|
|
4170
|
+
}
|
|
4171
|
+
return { base, head, added, removed, rewired };
|
|
4172
|
+
}
|
|
4173
|
+
async function reviewRevisions(repoRoot, base, head = "HEAD") {
|
|
4174
|
+
const [baseDir, headDir] = await Promise.all([checkoutRev(repoRoot, base), checkoutRev(repoRoot, head)]);
|
|
4175
|
+
try {
|
|
4176
|
+
const [baseGraph, headGraph] = [await graphForDirectory(baseDir), await graphForDirectory(headDir)];
|
|
4177
|
+
return diffGraphs(baseGraph, headGraph, base, head);
|
|
4178
|
+
} finally {
|
|
4179
|
+
await Promise.all([
|
|
4180
|
+
fs18.rm(baseDir, { recursive: true, force: true }),
|
|
4181
|
+
fs18.rm(headDir, { recursive: true, force: true })
|
|
4182
|
+
]);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
function renderReview(diff) {
|
|
4186
|
+
const lines = [
|
|
4187
|
+
`# Structural review: ${diff.base}..${diff.head}`,
|
|
4188
|
+
"",
|
|
4189
|
+
`${diff.added.length} symbol(s) added, ${diff.removed.length} removed, ${diff.rewired.length} rewired.`,
|
|
4190
|
+
""
|
|
4191
|
+
];
|
|
4192
|
+
const section = (title, symbols) => {
|
|
4193
|
+
lines.push(`## ${title} (${symbols.length})`, "");
|
|
4194
|
+
if (symbols.length === 0) {
|
|
4195
|
+
lines.push("(none)", "");
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
for (const s of symbols) {
|
|
4199
|
+
const tests = s.tests.length > 0 ? ` | tests: ${s.tests.join(", ")}` : " | tests: none found";
|
|
4200
|
+
lines.push(`- **${s.label}** (${s.sourceFile}) \u2014 blast radius ${s.blastRadius}${tests}`);
|
|
4201
|
+
const r = s;
|
|
4202
|
+
if (r.gainedEdges) {
|
|
4203
|
+
for (const e of r.gainedEdges.slice(0, 5)) lines.push(` - gained: ${e}`);
|
|
4204
|
+
for (const e of r.lostEdges.slice(0, 5)) lines.push(` - lost: ${e}`);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
lines.push("");
|
|
4208
|
+
};
|
|
4209
|
+
section("Removed \u2014 check every caller", diff.removed);
|
|
4210
|
+
section("Rewired \u2014 dependencies changed", diff.rewired);
|
|
4211
|
+
section("Added", diff.added);
|
|
4212
|
+
const allTests = /* @__PURE__ */ new Set();
|
|
4213
|
+
for (const s of [...diff.removed, ...diff.rewired, ...diff.added]) {
|
|
4214
|
+
for (const t of s.tests) allTests.add(t);
|
|
4215
|
+
}
|
|
4216
|
+
lines.push(`## Suggested test run (${allTests.size} file(s))`, "");
|
|
4217
|
+
for (const t of [...allTests].sort((a, b) => a.localeCompare(b))) lines.push(`- ${t}`);
|
|
4218
|
+
lines.push("");
|
|
4219
|
+
return lines.join("\n");
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
// src/cli/commands/review.ts
|
|
4223
|
+
async function runReview(base, head) {
|
|
4224
|
+
const diff = await reviewRevisions(process.cwd(), base, head ?? "HEAD");
|
|
4225
|
+
console.log(renderReview(diff));
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
// src/cli/commands/tests.ts
|
|
4229
|
+
init_cjs_shims();
|
|
4230
|
+
var import_node_child_process2 = require("child_process");
|
|
4231
|
+
var import_node_util2 = require("util");
|
|
4232
|
+
init_graphStore();
|
|
4233
|
+
init_testmap();
|
|
4234
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
4235
|
+
async function runTests(nodeQuery, options = {}) {
|
|
4236
|
+
const graph = await loadGraph();
|
|
4237
|
+
let selection;
|
|
4238
|
+
if (options.changed !== void 0 && options.changed !== false) {
|
|
4239
|
+
const args = ["diff", "--name-only"];
|
|
4240
|
+
if (typeof options.changed === "string") args.push(options.changed);
|
|
4241
|
+
const { stdout } = await execFileAsync2("git", args);
|
|
4242
|
+
const changedFiles = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4243
|
+
if (changedFiles.length === 0) {
|
|
4244
|
+
console.log("No changed files in the diff.");
|
|
4245
|
+
return;
|
|
4246
|
+
}
|
|
4247
|
+
selection = testsForChangedFiles(graph, changedFiles);
|
|
4248
|
+
} else if (nodeQuery) {
|
|
4249
|
+
selection = testsForNode(graph, nodeQuery);
|
|
4250
|
+
if (!selection) {
|
|
4251
|
+
console.log(`No node matched "${nodeQuery}".`);
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
} else {
|
|
4255
|
+
console.log("Provide a node to select tests for, or --changed for the working-tree diff.");
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
if (selection.testFiles.length === 0) {
|
|
4259
|
+
console.log(
|
|
4260
|
+
`No test files found in the blast radius of ${selection.target} (${selection.affectedNodeCount} affected node(s) checked) \u2014 either it's untested or the tests reach it through a path the graph does not capture.`
|
|
4261
|
+
);
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
console.log(`Test files for ${selection.target} (most direct first):`);
|
|
4265
|
+
for (const test of selection.testFiles) {
|
|
4266
|
+
const via = test.depth === 0 ? test.via : `depth ${test.depth}, via ${test.via}`;
|
|
4267
|
+
console.log(` ${test.file} (${via})`);
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
// src/cli/commands/global.ts
|
|
4272
|
+
init_cjs_shims();
|
|
4273
|
+
var fs19 = __toESM(require("fs/promises"), 1);
|
|
4274
|
+
var os2 = __toESM(require("os"), 1);
|
|
4275
|
+
var path12 = __toESM(require("path"), 1);
|
|
4276
|
+
init_graphStore();
|
|
4277
|
+
|
|
4278
|
+
// src/merge.ts
|
|
4279
|
+
init_cjs_shims();
|
|
4280
|
+
var import_graphology4 = __toESM(require("graphology"), 1);
|
|
4281
|
+
var CONFIDENCE_RANK3 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
|
|
4282
|
+
function strongerConfidence2(a, b) {
|
|
4283
|
+
return CONFIDENCE_RANK3[a] >= CONFIDENCE_RANK3[b] ? a : b;
|
|
4284
|
+
}
|
|
4285
|
+
function isShared(id) {
|
|
4286
|
+
return id.startsWith("external:") || id.startsWith("module:");
|
|
4287
|
+
}
|
|
4288
|
+
function namespaced(project, id) {
|
|
4289
|
+
return isShared(id) ? id : `${project}/${id}`;
|
|
4290
|
+
}
|
|
4291
|
+
function mergeGraphs(entries) {
|
|
4292
|
+
const names = entries.map((e) => e.name);
|
|
4293
|
+
const duplicate = names.find((name, i) => names.indexOf(name) !== i);
|
|
4294
|
+
if (duplicate !== void 0) {
|
|
4295
|
+
throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
|
|
4296
|
+
}
|
|
4297
|
+
const merged = new import_graphology4.default({ type: "directed", multi: true, allowSelfLoops: true });
|
|
4298
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
4299
|
+
for (const { name, graph } of sorted) {
|
|
4300
|
+
const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
|
|
4301
|
+
for (const id of nodeIds) {
|
|
4302
|
+
const attrs = graph.getNodeAttributes(id);
|
|
4303
|
+
const newId = namespaced(name, id);
|
|
4304
|
+
if (merged.hasNode(newId)) {
|
|
4305
|
+
merged.setNodeAttribute(newId, "project", "(shared)");
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
const sourceFile = attrs.sourceFile ?? "";
|
|
4309
|
+
merged.addNode(newId, {
|
|
4310
|
+
label: attrs.label ?? id,
|
|
4311
|
+
sourceFile: isShared(id) || sourceFile === "" ? sourceFile : `${name}/${sourceFile}`,
|
|
4312
|
+
sourceLocation: attrs.sourceLocation ?? "",
|
|
4313
|
+
project: name
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
const edgeKeys = [...graph.edges()].sort((a, b) => a.localeCompare(b));
|
|
4317
|
+
for (const edgeKey of edgeKeys) {
|
|
4318
|
+
const attrs = graph.getEdgeAttributes(edgeKey);
|
|
4319
|
+
const source = namespaced(name, graph.source(edgeKey));
|
|
4320
|
+
const target = namespaced(name, graph.target(edgeKey));
|
|
4321
|
+
const relation = String(attrs.relation);
|
|
4322
|
+
const confidence = attrs.confidence;
|
|
4323
|
+
const key = `${source}|${relation}|${target}`;
|
|
4324
|
+
if (merged.hasEdge(key)) {
|
|
4325
|
+
const existing = merged.getEdgeAttribute(key, "confidence");
|
|
4326
|
+
merged.setEdgeAttribute(key, "confidence", strongerConfidence2(existing, confidence));
|
|
4327
|
+
continue;
|
|
4328
|
+
}
|
|
4329
|
+
merged.addEdgeWithKey(key, source, target, { relation, confidence });
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
return merged;
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
// src/cli/commands/global.ts
|
|
4336
|
+
function registryDir() {
|
|
4337
|
+
return path12.join(os2.homedir(), ".graphify");
|
|
4338
|
+
}
|
|
4339
|
+
function registryPath() {
|
|
4340
|
+
return path12.join(registryDir(), "global.json");
|
|
4341
|
+
}
|
|
4342
|
+
async function readRegistry() {
|
|
4343
|
+
try {
|
|
4344
|
+
const raw = await fs19.readFile(registryPath(), "utf-8");
|
|
4345
|
+
const parsed = JSON.parse(raw);
|
|
4346
|
+
const projects = parsed.projects;
|
|
4347
|
+
if (!Array.isArray(projects)) return { projects: [] };
|
|
4348
|
+
return { projects: projects.filter((p) => typeof p?.name === "string" && typeof p?.root === "string") };
|
|
4349
|
+
} catch {
|
|
4350
|
+
return { projects: [] };
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
async function writeRegistry(registry) {
|
|
4354
|
+
await fs19.mkdir(registryDir(), { recursive: true });
|
|
4355
|
+
await fs19.writeFile(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
|
|
4356
|
+
}
|
|
4357
|
+
async function projectNameFor(root) {
|
|
4358
|
+
try {
|
|
4359
|
+
const raw = await fs19.readFile(path12.join(root, "package.json"), "utf-8");
|
|
4360
|
+
const name = JSON.parse(raw).name;
|
|
4361
|
+
if (typeof name === "string" && name.length > 0) return name;
|
|
4362
|
+
} catch {
|
|
4363
|
+
}
|
|
4364
|
+
return path12.basename(root);
|
|
4365
|
+
}
|
|
4366
|
+
async function runGlobalAdd(target = ".") {
|
|
4367
|
+
const root = path12.resolve(target);
|
|
4368
|
+
const graphJson = path12.join(root, "graphify-out", "graph.json");
|
|
4369
|
+
try {
|
|
4370
|
+
await fs19.access(graphJson);
|
|
4371
|
+
} catch {
|
|
4372
|
+
throw new Error(`${root} has no graphify-out/graph.json \u2014 run \`graphify ${target}\` first, then add it.`);
|
|
4373
|
+
}
|
|
4374
|
+
const name = await projectNameFor(root);
|
|
4375
|
+
const registry = await readRegistry();
|
|
4376
|
+
const existing = registry.projects.find((p) => p.name === name);
|
|
4377
|
+
if (existing) {
|
|
4378
|
+
if (existing.root === root) {
|
|
4379
|
+
console.log(`${name} is already registered (${root}).`);
|
|
4380
|
+
return;
|
|
4381
|
+
}
|
|
4382
|
+
throw new Error(
|
|
4383
|
+
`A different project is already registered as "${name}" (${existing.root}). Remove it first with \`graphify global remove\`.`
|
|
4384
|
+
);
|
|
4385
|
+
}
|
|
4386
|
+
registry.projects.push({ name, root });
|
|
4387
|
+
registry.projects.sort((a, b) => a.name.localeCompare(b.name));
|
|
4388
|
+
await writeRegistry(registry);
|
|
4389
|
+
console.log(`Registered ${name} (${root}). ${registry.projects.length} project(s) in the global graph.`);
|
|
4390
|
+
}
|
|
4391
|
+
async function runGlobalRemove(name) {
|
|
4392
|
+
const registry = await readRegistry();
|
|
4393
|
+
const before = registry.projects.length;
|
|
4394
|
+
registry.projects = registry.projects.filter((p) => p.name !== name);
|
|
4395
|
+
if (registry.projects.length === before) {
|
|
4396
|
+
throw new Error(`No project named "${name}" is registered. See \`graphify global list\`.`);
|
|
4397
|
+
}
|
|
4398
|
+
await writeRegistry(registry);
|
|
4399
|
+
console.log(`Removed ${name}. ${registry.projects.length} project(s) remain.`);
|
|
4400
|
+
}
|
|
4401
|
+
async function runGlobalList() {
|
|
4402
|
+
const registry = await readRegistry();
|
|
4403
|
+
if (registry.projects.length === 0) {
|
|
4404
|
+
console.log("No projects registered. Add one with `graphify global add <path>`.");
|
|
4405
|
+
return;
|
|
4406
|
+
}
|
|
4407
|
+
for (const project of registry.projects) {
|
|
4408
|
+
console.log(`${project.name} ${project.root}`);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
async function loadEntries(sources) {
|
|
4412
|
+
const entries = [];
|
|
4413
|
+
for (const source of sources) {
|
|
4414
|
+
try {
|
|
4415
|
+
entries.push({ name: source.name, graph: await loadGraph(source.outDir) });
|
|
4416
|
+
} catch (error) {
|
|
4417
|
+
console.warn(`Skipping ${source.name}: ${error.message}`);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
return entries;
|
|
4421
|
+
}
|
|
4422
|
+
async function mergeAndExport(entries, outDir) {
|
|
4423
|
+
if (entries.length === 0) {
|
|
4424
|
+
throw new Error("Nothing to merge \u2014 no project graphs could be loaded.");
|
|
4425
|
+
}
|
|
4426
|
+
const merged = mergeGraphs(entries);
|
|
4427
|
+
cluster(merged);
|
|
4428
|
+
const analysis = analyze(merged);
|
|
4429
|
+
const report = renderReport(merged, analysis);
|
|
4430
|
+
await exportGraph(merged, { outDir }, report);
|
|
4431
|
+
console.log(
|
|
4432
|
+
`Merged ${entries.length} project(s) into ${outDir} \u2014 ${merged.order} node(s), ${merged.size} edge(s).`
|
|
4433
|
+
);
|
|
4434
|
+
}
|
|
4435
|
+
async function runGlobalBuild(options = {}) {
|
|
4436
|
+
const registry = await readRegistry();
|
|
4437
|
+
if (registry.projects.length === 0) {
|
|
4438
|
+
throw new Error("No projects registered. Add some with `graphify global add <path>` first.");
|
|
4439
|
+
}
|
|
4440
|
+
const entries = await loadEntries(
|
|
4441
|
+
registry.projects.map((p) => ({ name: p.name, outDir: path12.join(p.root, "graphify-out") }))
|
|
4442
|
+
);
|
|
4443
|
+
await mergeAndExport(entries, path12.resolve(options.out ?? path12.join(registryDir(), "global-out")));
|
|
4444
|
+
}
|
|
4445
|
+
async function runMerge(targets, options = {}) {
|
|
4446
|
+
if (targets.length < 2) {
|
|
4447
|
+
throw new Error("Provide at least two project directories to merge.");
|
|
4448
|
+
}
|
|
4449
|
+
const sources = [];
|
|
4450
|
+
for (const target of targets) {
|
|
4451
|
+
const root = path12.resolve(target);
|
|
4452
|
+
sources.push({ name: await projectNameFor(root), outDir: path12.join(root, "graphify-out") });
|
|
4453
|
+
}
|
|
4454
|
+
const names = sources.map((s) => s.name);
|
|
4455
|
+
const duplicate = names.find((name, i) => names.indexOf(name) !== i);
|
|
4456
|
+
if (duplicate !== void 0) {
|
|
4457
|
+
throw new Error(`Two of the given projects resolve to the same name ("${duplicate}") \u2014 rename one.`);
|
|
4458
|
+
}
|
|
4459
|
+
const entries = await loadEntries(sources);
|
|
4460
|
+
await mergeAndExport(entries, path12.resolve(options.out ?? "graphify-merged"));
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
// src/cli/commands/hook.ts
|
|
4464
|
+
init_cjs_shims();
|
|
4465
|
+
var fs20 = __toESM(require("fs/promises"), 1);
|
|
4466
|
+
var path13 = __toESM(require("path"), 1);
|
|
4467
|
+
var MARKER_BEGIN = "# >>> graphify >>>";
|
|
4468
|
+
var MARKER_END = "# <<< graphify <<<";
|
|
4469
|
+
var HOOK_NAMES = ["post-commit", "post-merge"];
|
|
4470
|
+
var HOOK_BLOCK = `${MARKER_BEGIN}
|
|
4471
|
+
# Rebuild the graphify knowledge graph in the background so the hook never
|
|
4472
|
+
# slows down the commit. Errors are silenced \u2014 a failed rebuild should never
|
|
4473
|
+
# break a git operation. Installed by \`graphify hook install\`.
|
|
4474
|
+
(graphify . --update --no-viz >/dev/null 2>&1 &)
|
|
4475
|
+
${MARKER_END}`;
|
|
4476
|
+
async function hooksDir(cwd) {
|
|
4477
|
+
const gitDir = path13.join(cwd, ".git");
|
|
4478
|
+
let stats;
|
|
4479
|
+
try {
|
|
4480
|
+
stats = await fs20.stat(gitDir);
|
|
4481
|
+
} catch {
|
|
4482
|
+
throw new Error(`${cwd} is not a git repository (no .git directory) \u2014 run this from the repo root.`);
|
|
4483
|
+
}
|
|
4484
|
+
if (!stats.isDirectory()) {
|
|
4485
|
+
const content = (await fs20.readFile(gitDir, "utf-8")).trim();
|
|
4486
|
+
const match = /^gitdir:\s*(.+)$/.exec(content);
|
|
4487
|
+
if (!match) throw new Error(`${gitDir} exists but is neither a directory nor a gitdir pointer.`);
|
|
4488
|
+
return path13.resolve(cwd, match[1], "hooks");
|
|
4489
|
+
}
|
|
4490
|
+
return path13.join(gitDir, "hooks");
|
|
4491
|
+
}
|
|
4492
|
+
async function runHookInstall(cwd = process.cwd()) {
|
|
4493
|
+
const dir = await hooksDir(cwd);
|
|
4494
|
+
await fs20.mkdir(dir, { recursive: true });
|
|
4495
|
+
const results = [];
|
|
4496
|
+
for (const hook of HOOK_NAMES) {
|
|
4497
|
+
const hookPath = path13.join(dir, hook);
|
|
4498
|
+
let existing = null;
|
|
4499
|
+
try {
|
|
4500
|
+
existing = await fs20.readFile(hookPath, "utf-8");
|
|
4501
|
+
} catch {
|
|
4502
|
+
}
|
|
4503
|
+
if (existing === null) {
|
|
4504
|
+
await fs20.writeFile(hookPath, `#!/bin/sh
|
|
4505
|
+
${HOOK_BLOCK}
|
|
4506
|
+
`, { mode: 493 });
|
|
4507
|
+
results.push({ hook, path: hookPath, action: "created" });
|
|
4508
|
+
} else if (existing.includes(MARKER_BEGIN)) {
|
|
4509
|
+
results.push({ hook, path: hookPath, action: "already-installed" });
|
|
4510
|
+
} else {
|
|
4511
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
4512
|
+
await fs20.writeFile(hookPath, `${existing}${separator}${HOOK_BLOCK}
|
|
4513
|
+
`, "utf-8");
|
|
4514
|
+
await fs20.chmod(hookPath, 493);
|
|
4515
|
+
results.push({ hook, path: hookPath, action: "appended" });
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
for (const result of results) {
|
|
4519
|
+
console.log(`${result.hook}: ${result.action} (${result.path})`);
|
|
4520
|
+
}
|
|
4521
|
+
console.log("");
|
|
4522
|
+
console.log("The graph will rebuild in the background after each commit and pull.");
|
|
4523
|
+
console.log("Remove with `graphify hook uninstall`.");
|
|
4524
|
+
return results;
|
|
4525
|
+
}
|
|
4526
|
+
async function runHookUninstall(cwd = process.cwd()) {
|
|
4527
|
+
const dir = await hooksDir(cwd);
|
|
4528
|
+
const results = [];
|
|
4529
|
+
for (const hook of HOOK_NAMES) {
|
|
4530
|
+
const hookPath = path13.join(dir, hook);
|
|
4531
|
+
let existing = null;
|
|
4532
|
+
try {
|
|
4533
|
+
existing = await fs20.readFile(hookPath, "utf-8");
|
|
4534
|
+
} catch {
|
|
4535
|
+
}
|
|
4536
|
+
if (existing === null || !existing.includes(MARKER_BEGIN)) {
|
|
4537
|
+
results.push({ hook, path: hookPath, action: "not-installed" });
|
|
4538
|
+
continue;
|
|
4539
|
+
}
|
|
4540
|
+
const blockPattern = new RegExp(`\\n?${MARKER_BEGIN}[\\s\\S]*?${MARKER_END}\\n?`);
|
|
4541
|
+
const remaining = existing.replace(blockPattern, "\n").trim();
|
|
4542
|
+
if (remaining === "" || remaining === "#!/bin/sh") {
|
|
4543
|
+
await fs20.unlink(hookPath);
|
|
4544
|
+
} else {
|
|
4545
|
+
await fs20.writeFile(hookPath, `${remaining}
|
|
4546
|
+
`, "utf-8");
|
|
4547
|
+
}
|
|
4548
|
+
results.push({ hook, path: hookPath, action: "removed" });
|
|
4549
|
+
}
|
|
4550
|
+
for (const result of results) {
|
|
4551
|
+
console.log(`${result.hook}: ${result.action}`);
|
|
4552
|
+
}
|
|
4553
|
+
return results;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
// src/memory.ts
|
|
4557
|
+
init_cjs_shims();
|
|
4558
|
+
var import_node_crypto4 = require("crypto");
|
|
4559
|
+
var fs21 = __toESM(require("fs/promises"), 1);
|
|
4560
|
+
var path14 = __toESM(require("path"), 1);
|
|
4561
|
+
var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
|
|
4562
|
+
var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
|
|
4563
|
+
function validateResult(value) {
|
|
4564
|
+
if (!value.question) throw new Error("save-result requires --question");
|
|
4565
|
+
if (!value.answer) throw new Error("save-result requires --answer");
|
|
4566
|
+
if (!OUTCOMES.has(value.outcome)) {
|
|
4567
|
+
throw new Error(`--outcome must be one of: useful, dead_end, corrected (got "${value.outcome}")`);
|
|
4568
|
+
}
|
|
4569
|
+
if (!TYPES.has(value.type)) {
|
|
4570
|
+
throw new Error(`--type must be one of: query, path, explain, affected (got "${value.type}")`);
|
|
4571
|
+
}
|
|
4572
|
+
if (value.outcome === "corrected" && !value.correction) {
|
|
4573
|
+
throw new Error("--outcome corrected requires --correction with what the right answer was");
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
|
|
4577
|
+
validateResult(entry);
|
|
4578
|
+
await fs21.mkdir(memoryDir, { recursive: true });
|
|
4579
|
+
const saved = { ...entry, savedAt: now.toISOString() };
|
|
4580
|
+
const hash = (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
|
|
4581
|
+
const stamp = saved.savedAt.replace(/[:.]/g, "-");
|
|
4582
|
+
const filePath = path14.join(memoryDir, `result-${stamp}-${hash}.json`);
|
|
4583
|
+
await fs21.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
|
|
4584
|
+
return filePath;
|
|
4585
|
+
}
|
|
4586
|
+
async function loadResults(memoryDir) {
|
|
4587
|
+
let files;
|
|
4588
|
+
try {
|
|
4589
|
+
files = (await fs21.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
|
|
4590
|
+
} catch {
|
|
4591
|
+
return [];
|
|
4592
|
+
}
|
|
4593
|
+
const results = [];
|
|
4594
|
+
for (const file of files.sort((a, b) => a.localeCompare(b))) {
|
|
4595
|
+
try {
|
|
4596
|
+
const parsed = JSON.parse(await fs21.readFile(path14.join(memoryDir, file), "utf-8"));
|
|
4597
|
+
if (parsed.question && parsed.savedAt) results.push(parsed);
|
|
4598
|
+
} catch {
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
return results;
|
|
4602
|
+
}
|
|
4603
|
+
function renderLessons(results, options = {}) {
|
|
4604
|
+
const halfLife = options.halfLifeDays ?? 30;
|
|
4605
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
4606
|
+
const minCorroboration = options.minCorroboration ?? 2;
|
|
4607
|
+
const weightOf = (savedAt) => {
|
|
4608
|
+
const ageDays = Math.max(0, (now.getTime() - new Date(savedAt).getTime()) / 864e5);
|
|
4609
|
+
return Math.pow(0.5, ageDays / halfLife);
|
|
4610
|
+
};
|
|
4611
|
+
const signals = /* @__PURE__ */ new Map();
|
|
4612
|
+
const corrections = [];
|
|
4613
|
+
for (const result of results) {
|
|
4614
|
+
const weight = weightOf(result.savedAt);
|
|
4615
|
+
for (const node of result.nodes ?? []) {
|
|
4616
|
+
const signal = signals.get(node) ?? { useful: 0, deadEnd: 0, usefulCount: 0, deadEndCount: 0 };
|
|
4617
|
+
if (result.outcome === "useful") {
|
|
4618
|
+
signal.useful += weight;
|
|
4619
|
+
signal.usefulCount++;
|
|
4620
|
+
} else if (result.outcome === "dead_end") {
|
|
4621
|
+
signal.deadEnd += weight;
|
|
4622
|
+
signal.deadEndCount++;
|
|
4623
|
+
}
|
|
4624
|
+
signals.set(node, signal);
|
|
4625
|
+
}
|
|
4626
|
+
if (result.outcome === "corrected" && result.correction) {
|
|
4627
|
+
corrections.push({ question: result.question, correction: result.correction, savedAt: result.savedAt });
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
const byScore = (kind) => [...signals.entries()].filter(([, s]) => kind === "useful" ? s.usefulCount >= minCorroboration : s.deadEndCount > 0).sort((a, b) => b[1][kind] - a[1][kind] || a[0].localeCompare(b[0])).slice(0, 10);
|
|
4631
|
+
const lines = [
|
|
4632
|
+
"# Graph lessons",
|
|
4633
|
+
"",
|
|
4634
|
+
`_Aggregated from ${results.length} saved result(s); signal half-life ${halfLife} day(s)._`,
|
|
4635
|
+
"",
|
|
4636
|
+
"## Nodes that keep answering",
|
|
4637
|
+
""
|
|
4638
|
+
];
|
|
4639
|
+
const useful = byScore("useful");
|
|
4640
|
+
if (useful.length === 0) {
|
|
4641
|
+
lines.push(`(none yet with >= ${minCorroboration} corroborating results \u2014 keep saving results)`);
|
|
4642
|
+
} else {
|
|
4643
|
+
for (const [node, s] of useful) {
|
|
4644
|
+
lines.push(`- **${node}** \u2014 score ${s.useful.toFixed(2)} across ${s.usefulCount} useful result(s)`);
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
lines.push("", "## Dead ends", "");
|
|
4648
|
+
const deadEnds = byScore("deadEnd");
|
|
4649
|
+
if (deadEnds.length === 0) {
|
|
4650
|
+
lines.push("(none recorded)");
|
|
4651
|
+
} else {
|
|
4652
|
+
for (const [node, s] of deadEnds) {
|
|
4653
|
+
lines.push(`- **${node}** \u2014 score ${s.deadEnd.toFixed(2)} across ${s.deadEndCount} dead-end result(s)`);
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
lines.push("", "## Corrections", "");
|
|
4657
|
+
if (corrections.length === 0) {
|
|
4658
|
+
lines.push("(none recorded)");
|
|
4659
|
+
} else {
|
|
4660
|
+
corrections.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
4661
|
+
for (const c of corrections.slice(0, 20)) {
|
|
4662
|
+
lines.push(`- (${c.savedAt.slice(0, 10)}) Q: ${c.question}`);
|
|
4663
|
+
lines.push(` Right answer: ${c.correction}`);
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
lines.push("");
|
|
4667
|
+
return lines.join("\n");
|
|
4668
|
+
}
|
|
4669
|
+
|
|
3036
4670
|
// src/cli/commands/install.ts
|
|
3037
4671
|
init_cjs_shims();
|
|
3038
4672
|
var import_node_module3 = require("module");
|
|
3039
|
-
var
|
|
3040
|
-
var
|
|
4673
|
+
var fs22 = __toESM(require("fs/promises"), 1);
|
|
4674
|
+
var path15 = __toESM(require("path"), 1);
|
|
3041
4675
|
var require4 = (0, import_node_module3.createRequire)(importMetaUrl);
|
|
3042
4676
|
function packageRoot() {
|
|
3043
4677
|
const packageJsonPath = require4.resolve("@dreamtree-org/graphify/package.json");
|
|
3044
|
-
return
|
|
4678
|
+
return path15.dirname(packageJsonPath);
|
|
4679
|
+
}
|
|
4680
|
+
var MD_MARKER_BEGIN = "<!-- >>> graphify >>> -->";
|
|
4681
|
+
var MD_MARKER_END = "<!-- <<< graphify <<< -->";
|
|
4682
|
+
async function writeOwnedFile(filePath, content) {
|
|
4683
|
+
await fs22.mkdir(path15.dirname(filePath), { recursive: true });
|
|
4684
|
+
await fs22.writeFile(filePath, content, "utf-8");
|
|
3045
4685
|
}
|
|
3046
|
-
async function
|
|
3047
|
-
const
|
|
3048
|
-
|
|
4686
|
+
async function upsertMarkedBlock(filePath, content) {
|
|
4687
|
+
const block = `${MD_MARKER_BEGIN}
|
|
4688
|
+
${content.trim()}
|
|
4689
|
+
${MD_MARKER_END}`;
|
|
4690
|
+
let existing = null;
|
|
4691
|
+
try {
|
|
4692
|
+
existing = await fs22.readFile(filePath, "utf-8");
|
|
4693
|
+
} catch {
|
|
4694
|
+
}
|
|
4695
|
+
if (existing === null) {
|
|
4696
|
+
await fs22.mkdir(path15.dirname(filePath), { recursive: true });
|
|
4697
|
+
await fs22.writeFile(filePath, `${block}
|
|
4698
|
+
`, "utf-8");
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
if (existing.includes(MD_MARKER_BEGIN)) {
|
|
4702
|
+
const pattern = new RegExp(`${MD_MARKER_BEGIN}[\\s\\S]*?${MD_MARKER_END}`);
|
|
4703
|
+
await fs22.writeFile(filePath, existing.replace(pattern, block), "utf-8");
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4707
|
+
await fs22.writeFile(filePath, `${existing}${separator}${block}
|
|
4708
|
+
`, "utf-8");
|
|
4709
|
+
}
|
|
4710
|
+
var PLATFORMS = {
|
|
4711
|
+
claude: async (cwd, content) => {
|
|
4712
|
+
const target = path15.join(cwd, ".claude", "skills", "graphify", "SKILL.md");
|
|
4713
|
+
await writeOwnedFile(target, content);
|
|
4714
|
+
return { host: "Claude Code", path: target };
|
|
4715
|
+
},
|
|
4716
|
+
cursor: async (cwd, content) => {
|
|
4717
|
+
const target = path15.join(cwd, ".cursor", "rules", "graphify.mdc");
|
|
4718
|
+
const body = content.replace(/^---[\s\S]*?---\n/, "");
|
|
4719
|
+
await writeOwnedFile(
|
|
4720
|
+
target,
|
|
4721
|
+
`---
|
|
4722
|
+
description: Query the graphify knowledge graph for codebase structure questions
|
|
4723
|
+
alwaysApply: false
|
|
4724
|
+
---
|
|
4725
|
+
${body}`
|
|
4726
|
+
);
|
|
4727
|
+
return { host: "Cursor", path: target };
|
|
4728
|
+
},
|
|
4729
|
+
windsurf: async (cwd, content) => {
|
|
4730
|
+
const target = path15.join(cwd, ".windsurf", "rules", "graphify.md");
|
|
4731
|
+
await writeOwnedFile(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
4732
|
+
return { host: "Windsurf", path: target };
|
|
4733
|
+
},
|
|
4734
|
+
cline: async (cwd, content) => {
|
|
4735
|
+
const target = path15.join(cwd, ".clinerules", "graphify.md");
|
|
4736
|
+
await writeOwnedFile(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
4737
|
+
return { host: "Cline", path: target };
|
|
4738
|
+
},
|
|
4739
|
+
agents: async (cwd, content) => {
|
|
4740
|
+
const target = path15.join(cwd, "AGENTS.md");
|
|
4741
|
+
await upsertMarkedBlock(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
4742
|
+
return { host: "AGENTS.md agents (Codex, opencode, Amp, ...)", path: target };
|
|
4743
|
+
},
|
|
4744
|
+
gemini: async (cwd, content) => {
|
|
4745
|
+
const target = path15.join(cwd, "GEMINI.md");
|
|
4746
|
+
await upsertMarkedBlock(target, content.replace(/^---[\s\S]*?---\n/, ""));
|
|
4747
|
+
return { host: "Gemini CLI", path: target };
|
|
4748
|
+
}
|
|
4749
|
+
};
|
|
4750
|
+
var SUPPORTED_PLATFORMS = Object.keys(PLATFORMS).sort((a, b) => a.localeCompare(b));
|
|
4751
|
+
async function runInstall(platforms = ["claude"], cwd = process.cwd()) {
|
|
4752
|
+
const sourcePath = path15.join(packageRoot(), "src", "skill", "SKILL.md");
|
|
4753
|
+
const content = await fs22.readFile(sourcePath, "utf-8");
|
|
4754
|
+
const requested = platforms.includes("all") ? SUPPORTED_PLATFORMS : platforms;
|
|
3049
4755
|
const results = [];
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
4756
|
+
for (const platform of requested) {
|
|
4757
|
+
const installer = PLATFORMS[platform];
|
|
4758
|
+
if (!installer) {
|
|
4759
|
+
throw new Error(
|
|
4760
|
+
`Unknown platform "${platform}" \u2014 supported: ${SUPPORTED_PLATFORMS.join(", ")}, or "all".`
|
|
4761
|
+
);
|
|
4762
|
+
}
|
|
4763
|
+
results.push(await installer(cwd, content));
|
|
4764
|
+
}
|
|
3055
4765
|
for (const result of results) {
|
|
3056
|
-
console.log(`Installed graphify
|
|
4766
|
+
console.log(`Installed graphify for ${result.host} -> ${result.path}`);
|
|
3057
4767
|
}
|
|
3058
|
-
console.log("");
|
|
3059
|
-
console.log(
|
|
3060
|
-
"Other hosts (Cursor, etc.) are not implemented yet by `graphify install` \u2014 see ARCHITECTURE.md."
|
|
3061
|
-
);
|
|
3062
4768
|
return results;
|
|
3063
4769
|
}
|
|
3064
4770
|
|
|
@@ -3096,7 +4802,7 @@ init_graphStore();
|
|
|
3096
4802
|
init_query();
|
|
3097
4803
|
async function runQuery(question, options = {}) {
|
|
3098
4804
|
const graph = await loadGraph();
|
|
3099
|
-
const result = queryGraph(graph, question, { dfs: options.dfs,
|
|
4805
|
+
const result = queryGraph(graph, question, { dfs: options.dfs, tokenBudget: options.budget });
|
|
3100
4806
|
if (result.seeds.length === 0) {
|
|
3101
4807
|
console.log(
|
|
3102
4808
|
`No nodes matched "${question}". Try different keywords, or run \`graphify explain <node>\` if you already know the name.`
|
|
@@ -3121,15 +4827,15 @@ async function runQuery(question, options = {}) {
|
|
|
3121
4827
|
}
|
|
3122
4828
|
|
|
3123
4829
|
// src/cli/index.ts
|
|
3124
|
-
var
|
|
4830
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
3125
4831
|
function looksLikeGitUrl(value) {
|
|
3126
4832
|
if (!/^https?:\/\//i.test(value)) return false;
|
|
3127
4833
|
return /github\.com|gitlab\.com|bitbucket\.org/i.test(value) || value.endsWith(".git");
|
|
3128
4834
|
}
|
|
3129
4835
|
async function cloneRepo(url) {
|
|
3130
4836
|
const validated = validateUrl(url);
|
|
3131
|
-
const dest = await
|
|
3132
|
-
await
|
|
4837
|
+
const dest = await fs24.mkdtemp(path17.join(os3.tmpdir(), "graphify-clone-"));
|
|
4838
|
+
await execFileAsync3("git", ["clone", "--depth", "1", validated, dest]);
|
|
3133
4839
|
return dest;
|
|
3134
4840
|
}
|
|
3135
4841
|
function exportOptionsFrom(options) {
|
|
@@ -3142,7 +4848,7 @@ function clusterOptionsFrom(options) {
|
|
|
3142
4848
|
};
|
|
3143
4849
|
}
|
|
3144
4850
|
async function runClusterOnly(root, options) {
|
|
3145
|
-
const outDir =
|
|
4851
|
+
const outDir = path17.join(root, "graphify-out");
|
|
3146
4852
|
const graph = await loadGraph(outDir);
|
|
3147
4853
|
cluster(graph, clusterOptionsFrom(options));
|
|
3148
4854
|
const analysis = analyze(graph);
|
|
@@ -3173,12 +4879,12 @@ async function watchAndRebuild(root, runOnce) {
|
|
|
3173
4879
|
});
|
|
3174
4880
|
}
|
|
3175
4881
|
var program = new import_commander.Command();
|
|
3176
|
-
program.name("graphify").description("Turn a folder of code/docs/papers into a queryable knowledge graph.").argument("[path]", "path to scan, or a GitHub URL", ".").option("--mode <mode>", "extraction mode (deep for richer INFERRED edges \u2014 reserved, no-op in v1)").option("--update", "incremental
|
|
4882
|
+
program.name("graphify").description("Turn a folder of code/docs/papers into a queryable knowledge graph.").argument("[path]", "path to scan, or a GitHub URL", ".").option("--mode <mode>", "extraction mode (deep for richer INFERRED edges \u2014 reserved, no-op in v1)").option("--update", "incremental rebuild \u2014 reuse cached extractions for unchanged files").option("--watch", "rebuild on file change").option("--cluster-only", "rerun clustering on an existing graph").option("--leiden", "use Leiden instead of Louvain for community detection (v2 \u2014 better community quality)").option("--max-iterations <n>", "Leiden only: cap on outer iterations for very large graphs", Number).option("--no-viz", "skip graph.html").option("--svg", "also export graph.svg (not implemented in v1)").option("--graphml", "also export graph.graphml (not implemented in v1)").option("--neo4j", "generate graphify-out/cypher.txt for Neo4j (not implemented in v1)").option("--mcp", "start MCP stdio server instead of running the pipeline").option("--mysql <dsn>", "also extract a MySQL schema (mysql://user:pass@host:port/db) into the graph").action(async (targetArg, options) => {
|
|
3177
4883
|
try {
|
|
3178
4884
|
if (options.mcp) {
|
|
3179
|
-
const outDir =
|
|
4885
|
+
const outDir = path17.resolve(targetArg === "." ? process.cwd() : targetArg, "graphify-out");
|
|
3180
4886
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
3181
|
-
await startServer2(
|
|
4887
|
+
await startServer2(path17.join(outDir, "graph.json"));
|
|
3182
4888
|
return;
|
|
3183
4889
|
}
|
|
3184
4890
|
let root = targetArg;
|
|
@@ -3186,12 +4892,7 @@ program.name("graphify").description("Turn a folder of code/docs/papers into a q
|
|
|
3186
4892
|
console.error(`Cloning ${targetArg} ...`);
|
|
3187
4893
|
root = await cloneRepo(targetArg);
|
|
3188
4894
|
}
|
|
3189
|
-
root =
|
|
3190
|
-
if (options.update) {
|
|
3191
|
-
console.error(
|
|
3192
|
-
"--update (incremental re-extract) is not implemented yet in v1 \u2014 running the full pipeline instead."
|
|
3193
|
-
);
|
|
3194
|
-
}
|
|
4895
|
+
root = path17.resolve(root);
|
|
3195
4896
|
if (options.mode) {
|
|
3196
4897
|
console.error(`--mode ${options.mode} is reserved for a future richer-INFERRED-edges pass \u2014 currently a no-op.`);
|
|
3197
4898
|
}
|
|
@@ -3199,9 +4900,17 @@ program.name("graphify").description("Turn a folder of code/docs/papers into a q
|
|
|
3199
4900
|
await runClusterOnly(root, options);
|
|
3200
4901
|
return;
|
|
3201
4902
|
}
|
|
4903
|
+
let extraExtractions;
|
|
4904
|
+
if (options.mysql) {
|
|
4905
|
+
const { extractMysql: extractMysql2 } = await Promise.resolve().then(() => (init_mysql(), mysql_exports));
|
|
4906
|
+
console.error(`Extracting MySQL schema from ${validateDsn(options.mysql).safeDisplay} ...`);
|
|
4907
|
+
extraExtractions = [await extractMysql2(options.mysql)];
|
|
4908
|
+
}
|
|
3202
4909
|
const runOnce = () => runPipeline(root, {
|
|
3203
4910
|
...exportOptionsFrom(options),
|
|
3204
4911
|
...clusterOptionsFrom(options),
|
|
4912
|
+
extraExtractions,
|
|
4913
|
+
update: options.update,
|
|
3205
4914
|
onProgress: (m) => console.error(m)
|
|
3206
4915
|
});
|
|
3207
4916
|
await runOnce();
|
|
@@ -3238,13 +4947,153 @@ program.command("explain <node>").description("plain-language explanation of a n
|
|
|
3238
4947
|
process.exitCode = 1;
|
|
3239
4948
|
}
|
|
3240
4949
|
});
|
|
3241
|
-
program.command("
|
|
4950
|
+
program.command("affected <node>").description("reverse impact analysis \u2014 everything that (transitively) depends on a node").option("--depth <n>", "how many reverse hops to follow (default 3)", Number).option("--limit <n>", "cap on affected nodes reported (default 200)", Number).action(async (node, options) => {
|
|
4951
|
+
try {
|
|
4952
|
+
await runAffected(node, options);
|
|
4953
|
+
} catch (error) {
|
|
4954
|
+
console.error(`graphify affected: ${error.message}`);
|
|
4955
|
+
process.exitCode = 1;
|
|
4956
|
+
}
|
|
4957
|
+
});
|
|
4958
|
+
program.command("context <task>").description("token-budgeted working-set pack \u2014 the actual code for a task, in one call").option("--budget <tokens>", "approximate token cap for the pack (default 4000)", Number).action(async (task, options) => {
|
|
4959
|
+
try {
|
|
4960
|
+
await runContext(task, options);
|
|
4961
|
+
} catch (error) {
|
|
4962
|
+
console.error(`graphify context: ${error.message}`);
|
|
4963
|
+
process.exitCode = 1;
|
|
4964
|
+
}
|
|
4965
|
+
});
|
|
4966
|
+
program.command("tests [node]").description("structural test selection \u2014 the minimal test files worth running for a change").option("--changed [rev]", "select for the working-tree diff (optionally vs a revision) instead of a node").action(async (node, options) => {
|
|
4967
|
+
try {
|
|
4968
|
+
await runTests(node, options);
|
|
4969
|
+
} catch (error) {
|
|
4970
|
+
console.error(`graphify tests: ${error.message}`);
|
|
4971
|
+
process.exitCode = 1;
|
|
4972
|
+
}
|
|
4973
|
+
});
|
|
4974
|
+
program.command("review <base> [head]").description("structural review between two git revisions: added/removed/rewired symbols with blast radius + tests").action(async (base, head) => {
|
|
3242
4975
|
try {
|
|
3243
|
-
await
|
|
4976
|
+
await runReview(base, head);
|
|
4977
|
+
} catch (error) {
|
|
4978
|
+
console.error(`graphify review: ${error.message}`);
|
|
4979
|
+
process.exitCode = 1;
|
|
4980
|
+
}
|
|
4981
|
+
});
|
|
4982
|
+
program.command("check").description("check the graph against dependency rules (graphify.rules.json) \u2014 exits 1 on violations").option("--rules <file>", "rules file (default ./graphify.rules.json)").action(async (options) => {
|
|
4983
|
+
try {
|
|
4984
|
+
await runCheck(options);
|
|
4985
|
+
} catch (error) {
|
|
4986
|
+
console.error(`graphify check: ${error.message}`);
|
|
4987
|
+
process.exitCode = 1;
|
|
4988
|
+
}
|
|
4989
|
+
});
|
|
4990
|
+
program.command("benchmark [questions...]").description("measure token savings of graph answers vs reading the files they touch").action(async (questions) => {
|
|
4991
|
+
try {
|
|
4992
|
+
await runBenchmark(questions);
|
|
4993
|
+
} catch (error) {
|
|
4994
|
+
console.error(`graphify benchmark: ${error.message}`);
|
|
4995
|
+
process.exitCode = 1;
|
|
4996
|
+
}
|
|
4997
|
+
});
|
|
4998
|
+
program.command("install").description("install the skill/rules files into local agents (claude, cursor, windsurf, cline, agents, gemini, all)").option("--platform <names...>", "platform(s) to install for", ["claude"]).action(async (options) => {
|
|
4999
|
+
try {
|
|
5000
|
+
await runInstall(options.platform);
|
|
3244
5001
|
} catch (error) {
|
|
3245
5002
|
console.error(`graphify install: ${error.message}`);
|
|
3246
5003
|
process.exitCode = 1;
|
|
3247
5004
|
}
|
|
3248
5005
|
});
|
|
5006
|
+
var globalCommand = program.command("global").description("manage the cross-project global graph (registry at ~/.graphify/global.json)");
|
|
5007
|
+
globalCommand.command("add [path]").description("register a project (must already have graphify-out/graph.json)").action(async (target) => {
|
|
5008
|
+
try {
|
|
5009
|
+
await runGlobalAdd(target);
|
|
5010
|
+
} catch (error) {
|
|
5011
|
+
console.error(`graphify global add: ${error.message}`);
|
|
5012
|
+
process.exitCode = 1;
|
|
5013
|
+
}
|
|
5014
|
+
});
|
|
5015
|
+
globalCommand.command("remove <name>").description("unregister a project by name").action(async (name) => {
|
|
5016
|
+
try {
|
|
5017
|
+
await runGlobalRemove(name);
|
|
5018
|
+
} catch (error) {
|
|
5019
|
+
console.error(`graphify global remove: ${error.message}`);
|
|
5020
|
+
process.exitCode = 1;
|
|
5021
|
+
}
|
|
5022
|
+
});
|
|
5023
|
+
globalCommand.command("list").description("list registered projects").action(async () => {
|
|
5024
|
+
try {
|
|
5025
|
+
await runGlobalList();
|
|
5026
|
+
} catch (error) {
|
|
5027
|
+
console.error(`graphify global list: ${error.message}`);
|
|
5028
|
+
process.exitCode = 1;
|
|
5029
|
+
}
|
|
5030
|
+
});
|
|
5031
|
+
globalCommand.command("build").description("merge every registered project into one global graph").option("--out <dir>", "output directory (default ~/.graphify/global-out)").action(async (options) => {
|
|
5032
|
+
try {
|
|
5033
|
+
await runGlobalBuild(options);
|
|
5034
|
+
} catch (error) {
|
|
5035
|
+
console.error(`graphify global build: ${error.message}`);
|
|
5036
|
+
process.exitCode = 1;
|
|
5037
|
+
}
|
|
5038
|
+
});
|
|
5039
|
+
program.command("merge <dirs...>").description("one-shot merge of two or more built project graphs into one").option("--out <dir>", "output directory (default ./graphify-merged)").action(async (dirs, options) => {
|
|
5040
|
+
try {
|
|
5041
|
+
await runMerge(dirs, options);
|
|
5042
|
+
} catch (error) {
|
|
5043
|
+
console.error(`graphify merge: ${error.message}`);
|
|
5044
|
+
process.exitCode = 1;
|
|
5045
|
+
}
|
|
5046
|
+
});
|
|
5047
|
+
program.command("save-result").description("save a Q&A result to graphify-out/memory/ for the graph feedback loop").requiredOption("--question <q>", "the question that was asked").requiredOption("--answer <a>", "the answer that was given").option("--nodes <ids...>", "node ids/labels cited in the answer", []).option("--type <t>", "query|path|explain|affected", "query").option("--outcome <o>", "useful|dead_end|corrected", "useful").option("--correction <text>", "what the right answer was (pairs with --outcome corrected)").action(async (options) => {
|
|
5048
|
+
try {
|
|
5049
|
+
const memoryDir = path17.join(process.cwd(), "graphify-out", "memory");
|
|
5050
|
+
const file = await saveResult(
|
|
5051
|
+
{
|
|
5052
|
+
question: options.question,
|
|
5053
|
+
answer: options.answer,
|
|
5054
|
+
nodes: options.nodes,
|
|
5055
|
+
type: options.type,
|
|
5056
|
+
outcome: options.outcome,
|
|
5057
|
+
correction: options.correction
|
|
5058
|
+
},
|
|
5059
|
+
memoryDir
|
|
5060
|
+
);
|
|
5061
|
+
console.log(`Saved -> ${file}`);
|
|
5062
|
+
} catch (error) {
|
|
5063
|
+
console.error(`graphify save-result: ${error.message}`);
|
|
5064
|
+
process.exitCode = 1;
|
|
5065
|
+
}
|
|
5066
|
+
});
|
|
5067
|
+
program.command("reflect").description("aggregate graphify-out/memory/ into a recency-weighted lessons doc").option("--half-life-days <n>", "a result loses half its weight every N days (default 30)", Number).option("--out <file>", "output path (default graphify-out/reflections/LESSONS.md)").action(async (options) => {
|
|
5068
|
+
try {
|
|
5069
|
+
const memoryDir = path17.join(process.cwd(), "graphify-out", "memory");
|
|
5070
|
+
const results = await loadResults(memoryDir);
|
|
5071
|
+
const lessons = renderLessons(results, { halfLifeDays: options.halfLifeDays });
|
|
5072
|
+
const outPath = path17.resolve(options.out ?? path17.join(process.cwd(), "graphify-out", "reflections", "LESSONS.md"));
|
|
5073
|
+
await fs24.mkdir(path17.dirname(outPath), { recursive: true });
|
|
5074
|
+
await fs24.writeFile(outPath, lessons, "utf-8");
|
|
5075
|
+
console.log(`Reflected ${results.length} result(s) -> ${outPath}`);
|
|
5076
|
+
} catch (error) {
|
|
5077
|
+
console.error(`graphify reflect: ${error.message}`);
|
|
5078
|
+
process.exitCode = 1;
|
|
5079
|
+
}
|
|
5080
|
+
});
|
|
5081
|
+
var hookCommand = program.command("hook").description("manage the git hooks that auto-rebuild the graph on commit/pull");
|
|
5082
|
+
hookCommand.command("install").description("install post-commit and post-merge hooks (preserves existing hook content)").action(async () => {
|
|
5083
|
+
try {
|
|
5084
|
+
await runHookInstall();
|
|
5085
|
+
} catch (error) {
|
|
5086
|
+
console.error(`graphify hook install: ${error.message}`);
|
|
5087
|
+
process.exitCode = 1;
|
|
5088
|
+
}
|
|
5089
|
+
});
|
|
5090
|
+
hookCommand.command("uninstall").description("remove the graphify block from the git hooks").action(async () => {
|
|
5091
|
+
try {
|
|
5092
|
+
await runHookUninstall();
|
|
5093
|
+
} catch (error) {
|
|
5094
|
+
console.error(`graphify hook uninstall: ${error.message}`);
|
|
5095
|
+
process.exitCode = 1;
|
|
5096
|
+
}
|
|
5097
|
+
});
|
|
3249
5098
|
program.parseAsync(process.argv);
|
|
3250
5099
|
//# sourceMappingURL=index.cjs.map
|