@dreamtree-org/graphify 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,23 +31,44 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  EXTRACTOR_REGISTRY: () => EXTRACTOR_REGISTRY,
34
+ ExtractionCache: () => ExtractionCache,
34
35
  ExtractionResultSchema: () => ExtractionResultSchema,
35
36
  ExtractionValidationError: () => ExtractionValidationError,
37
+ affectedBy: () => affectedBy,
36
38
  analyze: () => analyze,
39
+ buildContextPack: () => buildContextPack,
37
40
  buildGraph: () => buildGraph,
41
+ checkRules: () => checkRules,
38
42
  cluster: () => cluster,
39
43
  collectFiles: () => collectFiles,
44
+ diffGraphs: () => diffGraphs,
40
45
  explainNode: () => explainNode,
41
46
  exportGraph: () => exportGraph,
42
47
  extract: () => extract,
48
+ extractMysql: () => extractMysql,
49
+ globToRegExp: () => globToRegExp,
50
+ graphForDirectory: () => graphForDirectory,
51
+ isTestFile: () => isTestFile,
43
52
  loadGraph: () => loadGraph,
53
+ loadResults: () => loadResults,
54
+ mergeGraphs: () => mergeGraphs,
44
55
  queryGraph: () => queryGraph,
56
+ rankForTask: () => rankForTask,
57
+ renderContextPack: () => renderContextPack,
58
+ renderLessons: () => renderLessons,
45
59
  renderReport: () => renderReport,
60
+ renderReview: () => renderReview,
61
+ resolveCrossFileReferences: () => resolveCrossFileReferences,
46
62
  resolveNode: () => resolveNode,
63
+ reviewRevisions: () => reviewRevisions,
47
64
  runPipeline: () => runPipeline,
65
+ saveResult: () => saveResult,
48
66
  scoreNodes: () => scoreNodes,
49
67
  shortestPath: () => shortestPath,
50
- validateExtraction: () => validateExtraction
68
+ testsForChangedFiles: () => testsForChangedFiles,
69
+ testsForNode: () => testsForNode,
70
+ validateExtraction: () => validateExtraction,
71
+ validateRules: () => validateRules
51
72
  });
52
73
  module.exports = __toCommonJS(index_exports);
53
74
 
@@ -293,7 +314,7 @@ async function createParser(grammarName) {
293
314
  }
294
315
 
295
316
  // src/extractors/csharp.ts
296
- async function extractCSharp(filePath) {
317
+ async function extractCSharp(filePath, displayPath) {
297
318
  const parser = await createParser("c_sharp");
298
319
  const raw = await fs2.readFile(filePath);
299
320
  const content = raw.toString("utf-8");
@@ -302,7 +323,7 @@ async function extractCSharp(filePath) {
302
323
  throw new Error(`extractCSharp: parser produced no tree for ${filePath}`);
303
324
  }
304
325
  const root = tree.rootNode;
305
- const sourceFile = toPosix(filePath);
326
+ const sourceFile = toPosix(displayPath ?? filePath);
306
327
  const builder = new ExtractionBuilder();
307
328
  const fileId = sourceFile;
308
329
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -506,7 +527,7 @@ function defaultPackageQualifier(importPath) {
506
527
  const segments = importPath.split("/");
507
528
  return segments[segments.length - 1] ?? importPath;
508
529
  }
509
- async function extractGo(filePath) {
530
+ async function extractGo(filePath, displayPath) {
510
531
  const parser = await createParser("go");
511
532
  const raw = await fs3.readFile(filePath);
512
533
  const content = raw.toString("utf-8");
@@ -515,7 +536,7 @@ async function extractGo(filePath) {
515
536
  throw new Error(`extractGo: parser produced no tree for ${filePath}`);
516
537
  }
517
538
  const root = tree.rootNode;
518
- const sourceFile = toPosix(filePath);
539
+ const sourceFile = toPosix(displayPath ?? filePath);
519
540
  const builder = new ExtractionBuilder();
520
541
  const fileId = sourceFile;
521
542
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -713,7 +734,7 @@ async function extractGo(filePath) {
713
734
 
714
735
  // src/extractors/java.ts
715
736
  var fs4 = __toESM(require("fs/promises"), 1);
716
- async function extractJava(filePath) {
737
+ async function extractJava(filePath, displayPath) {
717
738
  const parser = await createParser("java");
718
739
  const raw = await fs4.readFile(filePath);
719
740
  const content = raw.toString("utf-8");
@@ -722,7 +743,7 @@ async function extractJava(filePath) {
722
743
  throw new Error(`extractJava: parser produced no tree for ${filePath}`);
723
744
  }
724
745
  const root = tree.rootNode;
725
- const sourceFile = toPosix(filePath);
746
+ const sourceFile = toPosix(displayPath ?? filePath);
726
747
  const builder = new ExtractionBuilder();
727
748
  const fileId = sourceFile;
728
749
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -914,7 +935,7 @@ async function extractJava(filePath) {
914
935
 
915
936
  // src/extractors/python.ts
916
937
  var fs5 = __toESM(require("fs/promises"), 1);
917
- async function extractPython(filePath) {
938
+ async function extractPython(filePath, displayPath) {
918
939
  const parser = await createParser("python");
919
940
  const raw = await fs5.readFile(filePath);
920
941
  const content = raw.toString("utf-8");
@@ -923,7 +944,7 @@ async function extractPython(filePath) {
923
944
  throw new Error(`extractPython: parser produced no tree for ${filePath}`);
924
945
  }
925
946
  const root = tree.rootNode;
926
- const sourceFile = toPosix(filePath);
947
+ const sourceFile = toPosix(displayPath ?? filePath);
927
948
  const builder = new ExtractionBuilder();
928
949
  const fileId = sourceFile;
929
950
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1158,7 +1179,7 @@ function stringLiteralContent(node) {
1158
1179
  const content = namedChildren(node).find((c) => c.type === "string_content");
1159
1180
  return content ? content.text : "";
1160
1181
  }
1161
- async function extractRuby(filePath) {
1182
+ async function extractRuby(filePath, displayPath) {
1162
1183
  const parser = await createParser("ruby");
1163
1184
  const raw = await fs6.readFile(filePath);
1164
1185
  const content = raw.toString("utf-8");
@@ -1167,7 +1188,7 @@ async function extractRuby(filePath) {
1167
1188
  throw new Error(`extractRuby: parser produced no tree for ${filePath}`);
1168
1189
  }
1169
1190
  const root = tree.rootNode;
1170
- const sourceFile = toPosix(filePath);
1191
+ const sourceFile = toPosix(displayPath ?? filePath);
1171
1192
  const builder = new ExtractionBuilder();
1172
1193
  const fileId = sourceFile;
1173
1194
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1360,7 +1381,7 @@ function convertUsePath(segments) {
1360
1381
  }
1361
1382
  return segments.join("::");
1362
1383
  }
1363
- async function extractRust(filePath) {
1384
+ async function extractRust(filePath, displayPath) {
1364
1385
  const parser = await createParser("rust");
1365
1386
  const raw = await fs7.readFile(filePath);
1366
1387
  const content = raw.toString("utf-8");
@@ -1369,7 +1390,7 @@ async function extractRust(filePath) {
1369
1390
  throw new Error(`extractRust: parser produced no tree for ${filePath}`);
1370
1391
  }
1371
1392
  const root = tree.rootNode;
1372
- const sourceFile = toPosix(filePath);
1393
+ const sourceFile = toPosix(displayPath ?? filePath);
1373
1394
  const builder = new ExtractionBuilder();
1374
1395
  const fileId = sourceFile;
1375
1396
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1582,10 +1603,10 @@ async function extractRust(filePath) {
1582
1603
  }
1583
1604
  }
1584
1605
  } else if (fn.type === "scoped_identifier") {
1585
- const path8 = fn.childForFieldName("path")?.text;
1606
+ const path11 = fn.childForFieldName("path")?.text;
1586
1607
  const name = fn.childForFieldName("name")?.text;
1587
- if (path8 && name) {
1588
- const viaImport = importIndex.get(path8);
1608
+ if (path11 && name) {
1609
+ const viaImport = importIndex.get(path11);
1589
1610
  if (viaImport) {
1590
1611
  targetId = viaImport.id;
1591
1612
  confidence = "INFERRED";
@@ -1626,7 +1647,7 @@ function firstStringArgument(call) {
1626
1647
  if (!first || first.type !== "string") return null;
1627
1648
  return stripQuotes2(first.text);
1628
1649
  }
1629
- async function extractTypeScript(filePath) {
1650
+ async function extractTypeScript(filePath, displayPath) {
1630
1651
  const ext = path3.extname(filePath);
1631
1652
  const grammar = grammarForExtension(ext);
1632
1653
  const parser = await createParser(grammar);
@@ -1637,7 +1658,7 @@ async function extractTypeScript(filePath) {
1637
1658
  throw new Error(`extractTypeScript: parser produced no tree for ${filePath}`);
1638
1659
  }
1639
1660
  const root = tree.rootNode;
1640
- const sourceFile = toPosix(filePath);
1661
+ const sourceFile = toPosix(displayPath ?? filePath);
1641
1662
  const builder = new ExtractionBuilder();
1642
1663
  const fileId = sourceFile;
1643
1664
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1954,13 +1975,13 @@ var EXTRACTOR_REGISTRY = {
1954
1975
  ".cs": extractCSharp,
1955
1976
  ".rb": extractRuby
1956
1977
  };
1957
- async function extract(filePath) {
1978
+ async function extract(filePath, displayPath) {
1958
1979
  const ext = path4.extname(filePath);
1959
1980
  const extractor = EXTRACTOR_REGISTRY[ext];
1960
1981
  if (!extractor) {
1961
1982
  return { nodes: [], edges: [] };
1962
1983
  }
1963
- return extractor(filePath);
1984
+ return extractor(filePath, displayPath);
1964
1985
  }
1965
1986
 
1966
1987
  // src/build.ts
@@ -2065,6 +2086,107 @@ function buildGraph(extractions) {
2065
2086
  return graph;
2066
2087
  }
2067
2088
 
2089
+ // src/resolve.ts
2090
+ var CODE_EXTENSIONS2 = [
2091
+ ".ts",
2092
+ ".tsx",
2093
+ ".mts",
2094
+ ".cts",
2095
+ ".js",
2096
+ ".jsx",
2097
+ ".mjs",
2098
+ ".cjs",
2099
+ ".py",
2100
+ ".go",
2101
+ ".rs",
2102
+ ".java",
2103
+ ".cs",
2104
+ ".rb"
2105
+ ];
2106
+ function isOpaque(id) {
2107
+ return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
2108
+ }
2109
+ function stripKnownExtension(p) {
2110
+ for (const ext of CODE_EXTENSIONS2) {
2111
+ if (p.endsWith(ext)) return p.slice(0, -ext.length);
2112
+ }
2113
+ return null;
2114
+ }
2115
+ function candidatePaths(guessed) {
2116
+ const stem = stripKnownExtension(guessed) ?? guessed;
2117
+ const candidates = [];
2118
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}${ext}`);
2119
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${guessed}/index${ext}`);
2120
+ if (stem !== guessed) {
2121
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}/index${ext}`);
2122
+ }
2123
+ return candidates.filter((c) => c !== guessed);
2124
+ }
2125
+ function uniqueMatch(guessed, candidates, realIds) {
2126
+ const matches = /* @__PURE__ */ new Set();
2127
+ for (const candidate of candidates) {
2128
+ if (realIds.has(candidate)) matches.add(candidate);
2129
+ }
2130
+ if (matches.size !== 1) return null;
2131
+ return [...matches][0];
2132
+ }
2133
+ function resolveCrossFileReferences(extractions) {
2134
+ const realIds = /* @__PURE__ */ new Set();
2135
+ const realFiles = /* @__PURE__ */ new Set();
2136
+ for (const extraction of extractions) {
2137
+ for (const node of extraction.nodes) realIds.add(node.id);
2138
+ for (const node of extraction.nodes) {
2139
+ const sep3 = node.id.indexOf("::");
2140
+ if (sep3 > 0) realFiles.add(node.id.slice(0, sep3));
2141
+ }
2142
+ for (const edge of extraction.edges) {
2143
+ if (edge.relation === "contains") realFiles.add(edge.source);
2144
+ }
2145
+ }
2146
+ const remap = /* @__PURE__ */ new Map();
2147
+ const resolveId = (id) => {
2148
+ if (isOpaque(id)) return null;
2149
+ const cached = remap.get(id);
2150
+ if (cached !== void 0) return cached;
2151
+ const sep3 = id.indexOf("::");
2152
+ let resolved;
2153
+ if (sep3 > 0) {
2154
+ if (realIds.has(id)) return null;
2155
+ const guessedPath = id.slice(0, sep3);
2156
+ const name = id.slice(sep3);
2157
+ const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
2158
+ resolved = uniqueMatch(id, candidates, realIds);
2159
+ } else {
2160
+ if (realFiles.has(id)) return null;
2161
+ resolved = uniqueMatch(id, candidatePaths(id), realFiles);
2162
+ }
2163
+ if (resolved !== null) remap.set(id, resolved);
2164
+ return resolved;
2165
+ };
2166
+ let resolvedEndpoints = 0;
2167
+ for (const extraction of extractions) {
2168
+ for (const edge of extraction.edges) {
2169
+ const source = resolveId(edge.source);
2170
+ if (source !== null) {
2171
+ edge.source = source;
2172
+ resolvedEndpoints++;
2173
+ }
2174
+ const target = resolveId(edge.target);
2175
+ if (target !== null) {
2176
+ edge.target = target;
2177
+ resolvedEndpoints++;
2178
+ }
2179
+ }
2180
+ }
2181
+ let droppedPlaceholders = 0;
2182
+ for (const extraction of extractions) {
2183
+ const before = extraction.nodes.length;
2184
+ extraction.nodes = extraction.nodes.filter((node) => !remap.has(node.id));
2185
+ droppedPlaceholders += before - extraction.nodes.length;
2186
+ }
2187
+ return { resolvedEndpoints, droppedPlaceholders };
2188
+ }
2189
+
2068
2190
  // src/cluster.ts
2069
2191
  var import_node_crypto = require("crypto");
2070
2192
  var import_graphology2 = __toESM(require("graphology"), 1);
@@ -2345,7 +2467,37 @@ var nodePath = __toESM(require("path"), 1);
2345
2467
  var MAX_FETCH_BYTES = 50 * 1024 * 1024;
2346
2468
  var MAX_TEXT_BYTES = 10 * 1024 * 1024;
2347
2469
  var MAX_LABEL_LEN = 256;
2348
- function validateGraphPath(path8, base) {
2470
+ var DEFAULT_MYSQL_PORT = 3306;
2471
+ function validateDsn(dsn) {
2472
+ let parsed;
2473
+ try {
2474
+ parsed = new URL(dsn);
2475
+ } catch {
2476
+ throw new Error("Invalid DSN \u2014 expected mysql://user:pass@host:port/database");
2477
+ }
2478
+ if (parsed.protocol !== "mysql:") {
2479
+ throw new Error(`DSN scheme not supported: "${parsed.protocol}" (only mysql: is supported)`);
2480
+ }
2481
+ const database = decodeURIComponent(parsed.pathname.replace(/^\//, ""));
2482
+ if (!database || database.includes("/")) {
2483
+ throw new Error("DSN must name exactly one database, e.g. mysql://localhost:3306/mydb");
2484
+ }
2485
+ if (!parsed.hostname) {
2486
+ throw new Error("DSN must include a host, e.g. mysql://localhost:3306/mydb");
2487
+ }
2488
+ const port = parsed.port ? Number(parsed.port) : DEFAULT_MYSQL_PORT;
2489
+ return {
2490
+ safeDisplay: `mysql://${parsed.hostname}:${port}/${database}`,
2491
+ connection: {
2492
+ host: parsed.hostname,
2493
+ port,
2494
+ user: decodeURIComponent(parsed.username) || "root",
2495
+ password: decodeURIComponent(parsed.password),
2496
+ database
2497
+ }
2498
+ };
2499
+ }
2500
+ function validateGraphPath(path11, base) {
2349
2501
  const baseDir = base ?? nodePath.join(process.cwd(), "graphify-out");
2350
2502
  let resolvedBase;
2351
2503
  try {
@@ -2353,7 +2505,7 @@ function validateGraphPath(path8, base) {
2353
2505
  } catch {
2354
2506
  throw new Error(`Base directory does not exist: ${baseDir}`);
2355
2507
  }
2356
- const candidate = nodePath.isAbsolute(path8) ? path8 : nodePath.join(resolvedBase, path8);
2508
+ const candidate = nodePath.isAbsolute(path11) ? path11 : nodePath.join(resolvedBase, path11);
2357
2509
  const resolvedCandidate = nodePath.resolve(candidate);
2358
2510
  let realCandidate = resolvedCandidate;
2359
2511
  try {
@@ -2363,7 +2515,7 @@ function validateGraphPath(path8, base) {
2363
2515
  const relative4 = nodePath.relative(resolvedBase, realCandidate);
2364
2516
  const escapes = relative4 === ".." || relative4.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative4);
2365
2517
  if (escapes) {
2366
- throw new Error(`Path escapes graphify-out/: ${path8}`);
2518
+ throw new Error(`Path escapes graphify-out/: ${path11}`);
2367
2519
  }
2368
2520
  return realCandidate;
2369
2521
  }
@@ -2506,28 +2658,82 @@ async function exportGraph(graph, options, report) {
2506
2658
  }
2507
2659
 
2508
2660
  // src/pipeline.ts
2661
+ var fs12 = __toESM(require("fs/promises"), 1);
2662
+ var path7 = __toESM(require("path"), 1);
2663
+
2664
+ // src/extractionCache.ts
2665
+ var import_node_crypto3 = require("crypto");
2666
+ var fs11 = __toESM(require("fs/promises"), 1);
2509
2667
  var path6 = __toESM(require("path"), 1);
2668
+ var ExtractionCache = class {
2669
+ dir;
2670
+ constructor(outDir) {
2671
+ this.dir = path6.join(outDir, "cache");
2672
+ }
2673
+ key(relFile, content) {
2674
+ return (0, import_node_crypto3.createHash)("sha256").update(relFile).update("\0").update(content).digest("hex");
2675
+ }
2676
+ async get(key) {
2677
+ try {
2678
+ const raw = await fs11.readFile(path6.join(this.dir, `${key}.json`), "utf-8");
2679
+ const parsed = JSON.parse(raw);
2680
+ if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
2681
+ return parsed;
2682
+ } catch {
2683
+ return null;
2684
+ }
2685
+ }
2686
+ async put(key, extraction) {
2687
+ await fs11.mkdir(this.dir, { recursive: true });
2688
+ await fs11.writeFile(path6.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
2689
+ }
2690
+ };
2691
+
2692
+ // src/pipeline.ts
2510
2693
  async function runPipeline(root, options = {}) {
2511
2694
  const progress = options.onProgress ?? (() => {
2512
2695
  });
2513
- const resolvedRoot = path6.resolve(root);
2696
+ const resolvedRoot = path7.resolve(root);
2514
2697
  progress(`Scanning ${resolvedRoot} ...`);
2515
2698
  const manifest = collectFiles(resolvedRoot);
2516
2699
  progress(
2517
2700
  `Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
2518
2701
  );
2702
+ const outDir = options.outDir ?? path7.join(resolvedRoot, "graphify-out");
2703
+ const cache = new ExtractionCache(outDir);
2519
2704
  progress("Extracting...");
2520
2705
  const extractions = [];
2706
+ let cacheHits = 0;
2521
2707
  for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
2522
- const filePath = path6.relative(process.cwd(), path6.join(resolvedRoot, relFile)) || relFile;
2708
+ const filePath = path7.relative(process.cwd(), path7.join(resolvedRoot, relFile)) || relFile;
2523
2709
  try {
2524
- const extraction = await extract(filePath);
2710
+ const key = cache.key(relFile, await fs12.readFile(path7.join(resolvedRoot, relFile)));
2711
+ let extraction = options.update ? await cache.get(key) : null;
2712
+ if (extraction) {
2713
+ cacheHits++;
2714
+ } else {
2715
+ extraction = await extract(filePath, relFile);
2716
+ await cache.put(key, extraction);
2717
+ }
2525
2718
  extractions.push(extraction);
2526
2719
  } catch (error) {
2527
2720
  progress(` extraction failed for ${relFile}: ${error.message}`);
2528
2721
  throw error;
2529
2722
  }
2530
2723
  }
2724
+ if (options.update) {
2725
+ progress(` cache: ${cacheHits} hit(s), ${extractions.length - cacheHits} re-extracted.`);
2726
+ }
2727
+ if (options.extraExtractions && options.extraExtractions.length > 0) {
2728
+ extractions.push(...options.extraExtractions);
2729
+ }
2730
+ progress("Resolving cross-file references...");
2731
+ const resolveStats = resolveCrossFileReferences(extractions);
2732
+ if (resolveStats.resolvedEndpoints > 0) {
2733
+ progress(
2734
+ ` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
2735
+ );
2736
+ }
2531
2737
  progress("Building graph...");
2532
2738
  const graph = buildGraph(extractions);
2533
2739
  progress(`Clustering (${options.algorithm ?? "louvain"})...`);
@@ -2536,7 +2742,6 @@ async function runPipeline(root, options = {}) {
2536
2742
  const analysis = analyze(graph);
2537
2743
  progress("Rendering report...");
2538
2744
  const report = renderReport(graph, analysis);
2539
- const outDir = options.outDir ?? path6.join(resolvedRoot, "graphify-out");
2540
2745
  progress(`Exporting to ${outDir} ...`);
2541
2746
  await exportGraph(
2542
2747
  graph,
@@ -2555,12 +2760,12 @@ async function runPipeline(root, options = {}) {
2555
2760
  }
2556
2761
 
2557
2762
  // src/graphStore.ts
2558
- var fs11 = __toESM(require("fs/promises"), 1);
2559
- var path7 = __toESM(require("path"), 1);
2763
+ var fs13 = __toESM(require("fs/promises"), 1);
2764
+ var path8 = __toESM(require("path"), 1);
2560
2765
  var import_graphology3 = __toESM(require("graphology"), 1);
2561
- async function loadGraph(outDir = path7.join(process.cwd(), "graphify-out")) {
2766
+ async function loadGraph(outDir = path8.join(process.cwd(), "graphify-out")) {
2562
2767
  const jsonPath = validateGraphPath("graph.json", outDir);
2563
- const raw = await fs11.readFile(jsonPath, "utf-8");
2768
+ const raw = await fs13.readFile(jsonPath, "utf-8");
2564
2769
  const data = JSON.parse(raw);
2565
2770
  return import_graphology3.default.from(data);
2566
2771
  }
@@ -2597,8 +2802,48 @@ var STOPWORDS = /* @__PURE__ */ new Set([
2597
2802
  "does",
2598
2803
  "that's"
2599
2804
  ]);
2805
+ function splitCamelCase(text) {
2806
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
2807
+ }
2600
2808
  function tokenize(text) {
2601
- return text.toLowerCase().split(/[^a-z0-9_.]+/).filter((token) => token.length > 1 && !STOPWORDS.has(token));
2809
+ return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1 && !STOPWORDS.has(token));
2810
+ }
2811
+ function subtokenize(text) {
2812
+ return splitCamelCase(text).toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 1);
2813
+ }
2814
+ var FUZZY_MIN_TOKEN_LEN = 3;
2815
+ var FUZZY_THRESHOLD = 0.7;
2816
+ var SUBTOKEN_MATCH_SCORE = 1;
2817
+ var SUBSTRING_MATCH_SCORE = 0.6;
2818
+ var FUZZY_MATCH_WEIGHT = 0.5;
2819
+ function osaDistance(a, b) {
2820
+ let prev2 = [];
2821
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
2822
+ let curr = new Array(b.length + 1).fill(0);
2823
+ for (let i = 1; i <= a.length; i++) {
2824
+ curr[0] = i;
2825
+ for (let j = 1; j <= b.length; j++) {
2826
+ const substitution = a[i - 1] === b[j - 1] ? 0 : 1;
2827
+ let best = Math.min(
2828
+ prev[j] + 1,
2829
+ curr[j - 1] + 1,
2830
+ prev[j - 1] + substitution
2831
+ );
2832
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
2833
+ best = Math.min(best, prev2[j - 2] + 1);
2834
+ }
2835
+ curr[j] = best;
2836
+ }
2837
+ [prev2, prev, curr] = [prev, curr, new Array(b.length + 1).fill(0)];
2838
+ }
2839
+ return prev[b.length];
2840
+ }
2841
+ function fuzzySimilarity(a, b) {
2842
+ if (a === b) return 1;
2843
+ const maxLen = Math.max(a.length, b.length);
2844
+ if (maxLen === 0) return 1;
2845
+ if (Math.abs(a.length - b.length) / maxLen > 1 - FUZZY_THRESHOLD) return 0;
2846
+ return 1 - osaDistance(a, b) / maxLen;
2602
2847
  }
2603
2848
  function scoreNodes(graph, query) {
2604
2849
  const tokens = tokenize(query);
@@ -2606,9 +2851,25 @@ function scoreNodes(graph, query) {
2606
2851
  graph.forEachNode((id, attributes) => {
2607
2852
  const label = attributes.label ?? id;
2608
2853
  const haystack = `${label} ${id}`.toLowerCase();
2854
+ const subtokens = new Set(subtokenize(`${label} ${id}`));
2609
2855
  let score = 0;
2610
2856
  for (const token of tokens) {
2611
- if (haystack.includes(token)) score += 1;
2857
+ if (subtokens.has(token)) {
2858
+ score += SUBTOKEN_MATCH_SCORE;
2859
+ continue;
2860
+ }
2861
+ if (haystack.includes(token)) {
2862
+ score += SUBSTRING_MATCH_SCORE;
2863
+ continue;
2864
+ }
2865
+ if (token.length >= FUZZY_MIN_TOKEN_LEN) {
2866
+ let best = 0;
2867
+ for (const subtoken of subtokens) {
2868
+ const similarity = fuzzySimilarity(token, subtoken);
2869
+ if (similarity > best) best = similarity;
2870
+ }
2871
+ if (best >= FUZZY_THRESHOLD) score += best * FUZZY_MATCH_WEIGHT;
2872
+ }
2612
2873
  }
2613
2874
  if (score > 0) matches.push({ id, label, score });
2614
2875
  });
@@ -2622,22 +2883,36 @@ function resolveNode(graph, query) {
2622
2883
  var DEFAULT_MAX_DEPTH = 2;
2623
2884
  var DEFAULT_MAX_SEEDS = 5;
2624
2885
  var DEFAULT_BUDGET = 40;
2886
+ var DEFAULT_TOKEN_BUDGET = 2e3;
2887
+ var NODES_PER_TOKEN = 1 / 10;
2888
+ function nodeTokenCost(graph, id) {
2889
+ const attrs = graph.getNodeAttributes(id);
2890
+ const label = attrs.label ?? id;
2891
+ const sourceFile = attrs.sourceFile ?? "";
2892
+ return Math.ceil((id.length * 2 + label.length + sourceFile.length + 24) / 4);
2893
+ }
2625
2894
  function queryGraph(graph, question, options = {}) {
2626
2895
  const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
2627
2896
  const maxSeeds = options.maxSeeds ?? DEFAULT_MAX_SEEDS;
2628
- const budget = options.budget ?? DEFAULT_BUDGET;
2897
+ const tokenBudget = options.tokenBudget ?? DEFAULT_TOKEN_BUDGET;
2898
+ const budget = options.budget ?? Math.max(DEFAULT_BUDGET, Math.ceil(tokenBudget * NODES_PER_TOKEN));
2629
2899
  const allMatches = scoreNodes(graph, question);
2630
2900
  const seeds = allMatches.length > 0 ? allMatches.slice(0, maxSeeds) : [];
2631
2901
  const visited = /* @__PURE__ */ new Map();
2632
2902
  const frontier = seeds.map((s) => ({ id: s.id, depth: 0 }));
2633
- for (const seed of seeds) visited.set(seed.id, 0);
2634
- while (frontier.length > 0 && visited.size < budget) {
2903
+ let spentTokens = 0;
2904
+ for (const seed of seeds) {
2905
+ visited.set(seed.id, 0);
2906
+ spentTokens += nodeTokenCost(graph, seed.id);
2907
+ }
2908
+ while (frontier.length > 0 && visited.size < budget && spentTokens < tokenBudget) {
2635
2909
  const current = options.dfs ? frontier.pop() : frontier.shift();
2636
2910
  if (current.depth >= maxDepth) continue;
2637
2911
  const neighbors = [...graph.neighbors(current.id)].sort((a, b) => a.localeCompare(b));
2638
2912
  for (const neighbor of neighbors) {
2639
- if (visited.has(neighbor) || visited.size >= budget) continue;
2913
+ if (visited.has(neighbor) || visited.size >= budget || spentTokens >= tokenBudget) continue;
2640
2914
  visited.set(neighbor, current.depth + 1);
2915
+ spentTokens += nodeTokenCost(graph, neighbor);
2641
2916
  frontier.push({ id: neighbor, depth: current.depth + 1 });
2642
2917
  }
2643
2918
  }
@@ -2665,7 +2940,30 @@ function queryGraph(graph, question, options = {}) {
2665
2940
  edges.sort(
2666
2941
  (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.relation.localeCompare(b.relation)
2667
2942
  );
2668
- return { seeds, visited: visitedNodes, edges };
2943
+ return enforceTokenBudget({ seeds, visited: visitedNodes, edges }, tokenBudget);
2944
+ }
2945
+ function enforceTokenBudget(result, tokenBudget) {
2946
+ const cost = (value) => Math.ceil(JSON.stringify(value).length / 4);
2947
+ const visited = [...result.visited];
2948
+ let edges = [...result.edges];
2949
+ let total = cost(result.seeds) + visited.reduce((s, n) => s + cost(n), 0) + edges.reduce((s, e) => s + cost(e), 0);
2950
+ const seedIds = new Set(result.seeds.map((s) => s.id));
2951
+ while (total > tokenBudget && visited.length > 0) {
2952
+ const last = visited[visited.length - 1];
2953
+ if (seedIds.has(last.id)) break;
2954
+ visited.pop();
2955
+ total -= cost(last);
2956
+ const remaining = [];
2957
+ for (const edge of edges) {
2958
+ if (edge.source === last.id || edge.target === last.id) {
2959
+ total -= cost(edge);
2960
+ } else {
2961
+ remaining.push(edge);
2962
+ }
2963
+ }
2964
+ edges = remaining;
2965
+ }
2966
+ return { seeds: result.seeds, visited, edges };
2669
2967
  }
2670
2968
  function shortestPath(graph, fromQuery, toQuery) {
2671
2969
  const from = resolveNode(graph, fromQuery);
@@ -2691,25 +2989,25 @@ function shortestPath(graph, fromQuery, toQuery) {
2691
2989
  if (!visited.has(to.id)) {
2692
2990
  return { from, to, found: false, path: [], edges: [] };
2693
2991
  }
2694
- const path8 = [to.id];
2992
+ const path11 = [to.id];
2695
2993
  let cursor = to.id;
2696
2994
  while (cursor !== from.id) {
2697
2995
  const prev = predecessor.get(cursor);
2698
2996
  if (!prev) break;
2699
- path8.unshift(prev);
2997
+ path11.unshift(prev);
2700
2998
  cursor = prev;
2701
2999
  }
2702
3000
  const edges = [];
2703
- for (let i = 0; i < path8.length - 1; i++) {
2704
- const a = path8[i];
2705
- const b = path8[i + 1];
3001
+ for (let i = 0; i < path11.length - 1; i++) {
3002
+ const a = path11[i];
3003
+ const b = path11[i + 1];
2706
3004
  const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];
2707
3005
  if (key) {
2708
3006
  const attrs = graph.getEdgeAttributes(key);
2709
3007
  edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });
2710
3008
  }
2711
3009
  }
2712
- return { from, to, found: true, path: path8, edges };
3010
+ return { from, to, found: true, path: path11, edges };
2713
3011
  }
2714
3012
  function explainNode(graph, query) {
2715
3013
  const match = resolveNode(graph, query);
@@ -2735,25 +3033,813 @@ function explainNode(graph, query) {
2735
3033
  incoming
2736
3034
  };
2737
3035
  }
3036
+
3037
+ // src/impact.ts
3038
+ var DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
3039
+ "calls",
3040
+ "imports",
3041
+ "imports_from",
3042
+ "inherits",
3043
+ "implements",
3044
+ "mixes_in",
3045
+ "embeds",
3046
+ "references",
3047
+ "re_exports",
3048
+ "method",
3049
+ "contains"
3050
+ ]);
3051
+ var DEFAULT_IMPACT_DEPTH = 3;
3052
+ var DEFAULT_IMPACT_LIMIT = 200;
3053
+ var CONFIDENCE_RANK2 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
3054
+ function affectedBy(graph, nodeQuery, options = {}) {
3055
+ const target = resolveNode(graph, nodeQuery);
3056
+ if (!target) return null;
3057
+ const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
3058
+ const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
3059
+ const affected = [];
3060
+ const visited = /* @__PURE__ */ new Set([target.id]);
3061
+ let frontier = [target.id];
3062
+ let truncated = false;
3063
+ for (let depth = 1; depth <= maxDepth && frontier.length > 0 && !truncated; depth++) {
3064
+ const nextFrontier = /* @__PURE__ */ new Map();
3065
+ for (const current of [...frontier].sort((a, b) => a.localeCompare(b))) {
3066
+ graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
3067
+ if (visited.has(source)) return;
3068
+ const relation = edgeAttrs.relation;
3069
+ if (!DEPENDENCY_RELATIONS.has(relation)) return;
3070
+ const confidence = edgeAttrs.confidence;
3071
+ const existing = nextFrontier.get(source);
3072
+ if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
3073
+ const attrs = graph.getNodeAttributes(source);
3074
+ nextFrontier.set(source, {
3075
+ id: source,
3076
+ label: attrs.label ?? source,
3077
+ sourceFile: attrs.sourceFile ?? "",
3078
+ sourceLocation: attrs.sourceLocation ?? "",
3079
+ depth,
3080
+ via: { relation, confidence, dependsOn: current }
3081
+ });
3082
+ });
3083
+ }
3084
+ const roundNodes = [...nextFrontier.values()].sort((a, b) => a.id.localeCompare(b.id));
3085
+ for (const node of roundNodes) {
3086
+ if (affected.length >= limit) {
3087
+ truncated = true;
3088
+ break;
3089
+ }
3090
+ visited.add(node.id);
3091
+ affected.push(node);
3092
+ }
3093
+ frontier = roundNodes.filter((n) => visited.has(n.id)).map((n) => n.id);
3094
+ }
3095
+ const byConfidence = { EXTRACTED: 0, INFERRED: 0, AMBIGUOUS: 0 };
3096
+ for (const node of affected) byConfidence[node.via.confidence] += 1;
3097
+ return { target, affected, byConfidence, maxDepth, truncated };
3098
+ }
3099
+
3100
+ // src/merge.ts
3101
+ var import_graphology4 = __toESM(require("graphology"), 1);
3102
+ var CONFIDENCE_RANK3 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
3103
+ function strongerConfidence2(a, b) {
3104
+ return CONFIDENCE_RANK3[a] >= CONFIDENCE_RANK3[b] ? a : b;
3105
+ }
3106
+ function isShared(id) {
3107
+ return id.startsWith("external:") || id.startsWith("module:");
3108
+ }
3109
+ function namespaced(project, id) {
3110
+ return isShared(id) ? id : `${project}/${id}`;
3111
+ }
3112
+ function mergeGraphs(entries) {
3113
+ const names = entries.map((e) => e.name);
3114
+ const duplicate = names.find((name, i) => names.indexOf(name) !== i);
3115
+ if (duplicate !== void 0) {
3116
+ throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
3117
+ }
3118
+ const merged = new import_graphology4.default({ type: "directed", multi: true, allowSelfLoops: true });
3119
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
3120
+ for (const { name, graph } of sorted) {
3121
+ const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
3122
+ for (const id of nodeIds) {
3123
+ const attrs = graph.getNodeAttributes(id);
3124
+ const newId = namespaced(name, id);
3125
+ if (merged.hasNode(newId)) {
3126
+ merged.setNodeAttribute(newId, "project", "(shared)");
3127
+ continue;
3128
+ }
3129
+ const sourceFile = attrs.sourceFile ?? "";
3130
+ merged.addNode(newId, {
3131
+ label: attrs.label ?? id,
3132
+ sourceFile: isShared(id) || sourceFile === "" ? sourceFile : `${name}/${sourceFile}`,
3133
+ sourceLocation: attrs.sourceLocation ?? "",
3134
+ project: name
3135
+ });
3136
+ }
3137
+ const edgeKeys = [...graph.edges()].sort((a, b) => a.localeCompare(b));
3138
+ for (const edgeKey of edgeKeys) {
3139
+ const attrs = graph.getEdgeAttributes(edgeKey);
3140
+ const source = namespaced(name, graph.source(edgeKey));
3141
+ const target = namespaced(name, graph.target(edgeKey));
3142
+ const relation = String(attrs.relation);
3143
+ const confidence = attrs.confidence;
3144
+ const key = `${source}|${relation}|${target}`;
3145
+ if (merged.hasEdge(key)) {
3146
+ const existing = merged.getEdgeAttribute(key, "confidence");
3147
+ merged.setEdgeAttribute(key, "confidence", strongerConfidence2(existing, confidence));
3148
+ continue;
3149
+ }
3150
+ merged.addEdgeWithKey(key, source, target, { relation, confidence });
3151
+ }
3152
+ }
3153
+ return merged;
3154
+ }
3155
+
3156
+ // src/extractors/mysql.ts
3157
+ var schemaId = (schema) => `db:${schema}`;
3158
+ var tableId = (schema, table) => `db:${schema}::${table}`;
3159
+ var columnId = (schema, table, column) => `db:${schema}::${table}.${column}`;
3160
+ async function defaultQueryFn(dsn) {
3161
+ const { connection } = validateDsn(dsn);
3162
+ const mysql = await import("mysql2/promise");
3163
+ const conn = await mysql.createConnection({
3164
+ host: connection.host,
3165
+ port: connection.port,
3166
+ user: connection.user,
3167
+ password: connection.password,
3168
+ database: connection.database
3169
+ });
3170
+ return {
3171
+ query: async (sql, params) => {
3172
+ const [rows] = await conn.execute(sql, [...params]);
3173
+ return rows;
3174
+ },
3175
+ close: () => conn.end()
3176
+ };
3177
+ }
3178
+ async function extractMysql(dsn, queryFn) {
3179
+ const { safeDisplay, connection } = validateDsn(dsn);
3180
+ const schema = connection.database;
3181
+ let query = queryFn;
3182
+ let close;
3183
+ if (!query) {
3184
+ const live = await defaultQueryFn(dsn);
3185
+ query = live.query;
3186
+ close = live.close;
3187
+ }
3188
+ try {
3189
+ const nodes = [];
3190
+ const edges = [];
3191
+ nodes.push({ id: schemaId(schema), label: schema, sourceFile: safeDisplay, sourceLocation: schema });
3192
+ const tableRows = await query(
3193
+ "SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
3194
+ [schema]
3195
+ );
3196
+ const tables = tableRows.map((row) => ({
3197
+ name: String(row.TABLE_NAME),
3198
+ isView: String(row.TABLE_TYPE).toUpperCase() === "VIEW"
3199
+ }));
3200
+ const tableNames = new Set(tables.map((t) => t.name));
3201
+ for (const table of tables) {
3202
+ nodes.push({
3203
+ id: tableId(schema, table.name),
3204
+ label: `${table.isView ? "view" : "table"} ${table.name}`,
3205
+ sourceFile: safeDisplay,
3206
+ sourceLocation: table.name
3207
+ });
3208
+ edges.push({
3209
+ source: schemaId(schema),
3210
+ target: tableId(schema, table.name),
3211
+ relation: "contains",
3212
+ confidence: "EXTRACTED"
3213
+ });
3214
+ }
3215
+ const columnRows = await query(
3216
+ "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, ORDINAL_POSITION",
3217
+ [schema]
3218
+ );
3219
+ for (const row of columnRows) {
3220
+ const table = String(row.TABLE_NAME);
3221
+ const column = String(row.COLUMN_NAME);
3222
+ if (!tableNames.has(table)) continue;
3223
+ nodes.push({
3224
+ id: columnId(schema, table, column),
3225
+ label: `${table}.${column}: ${String(row.COLUMN_TYPE)}`,
3226
+ sourceFile: safeDisplay,
3227
+ sourceLocation: `${table}.${column}`
3228
+ });
3229
+ edges.push({
3230
+ source: tableId(schema, table),
3231
+ target: columnId(schema, table, column),
3232
+ relation: "contains",
3233
+ confidence: "EXTRACTED"
3234
+ });
3235
+ }
3236
+ const fkRows = await query(
3237
+ "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",
3238
+ [schema]
3239
+ );
3240
+ for (const row of fkRows) {
3241
+ const table = String(row.TABLE_NAME);
3242
+ const column = String(row.COLUMN_NAME);
3243
+ const referenced = String(row.REFERENCED_TABLE_NAME);
3244
+ if (!tableNames.has(table) || !tableNames.has(referenced)) continue;
3245
+ edges.push({
3246
+ source: columnId(schema, table, column),
3247
+ target: tableId(schema, referenced),
3248
+ relation: "references",
3249
+ confidence: "EXTRACTED"
3250
+ });
3251
+ }
3252
+ const viewNames = tables.filter((t) => t.isView).map((t) => t.name);
3253
+ if (viewNames.length > 0) {
3254
+ await addViewEdges(query, schema, viewNames, tableNames, edges);
3255
+ }
3256
+ return { nodes, edges };
3257
+ } finally {
3258
+ await close?.();
3259
+ }
3260
+ }
3261
+ async function addViewEdges(query, schema, viewNames, tableNames, edges) {
3262
+ try {
3263
+ const usageRows = await query(
3264
+ "SELECT VIEW_NAME, TABLE_NAME FROM information_schema.VIEW_TABLE_USAGE WHERE VIEW_SCHEMA = ? AND TABLE_SCHEMA = ? ORDER BY VIEW_NAME, TABLE_NAME",
3265
+ [schema, schema]
3266
+ );
3267
+ for (const row of usageRows) {
3268
+ const view = String(row.VIEW_NAME);
3269
+ const table = String(row.TABLE_NAME);
3270
+ if (!tableNames.has(view) || !tableNames.has(table) || view === table) continue;
3271
+ edges.push({
3272
+ source: tableId(schema, view),
3273
+ target: tableId(schema, table),
3274
+ relation: "references",
3275
+ confidence: "EXTRACTED"
3276
+ });
3277
+ }
3278
+ return;
3279
+ } catch {
3280
+ }
3281
+ const definitionRows = await query(
3282
+ "SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
3283
+ [schema]
3284
+ );
3285
+ for (const row of definitionRows) {
3286
+ const view = String(row.TABLE_NAME);
3287
+ if (!viewNames.includes(view)) continue;
3288
+ const definition = String(row.VIEW_DEFINITION ?? "").toLowerCase();
3289
+ for (const table of [...tableNames].sort((a, b) => a.localeCompare(b))) {
3290
+ if (table === view) continue;
3291
+ if (new RegExp(`\\b${table.toLowerCase()}\\b`).test(definition)) {
3292
+ edges.push({
3293
+ source: tableId(schema, view),
3294
+ target: tableId(schema, table),
3295
+ relation: "references",
3296
+ confidence: "INFERRED"
3297
+ });
3298
+ }
3299
+ }
3300
+ }
3301
+ }
3302
+
3303
+ // src/memory.ts
3304
+ var import_node_crypto4 = require("crypto");
3305
+ var fs14 = __toESM(require("fs/promises"), 1);
3306
+ var path9 = __toESM(require("path"), 1);
3307
+ var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
3308
+ var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
3309
+ function validateResult(value) {
3310
+ if (!value.question) throw new Error("save-result requires --question");
3311
+ if (!value.answer) throw new Error("save-result requires --answer");
3312
+ if (!OUTCOMES.has(value.outcome)) {
3313
+ throw new Error(`--outcome must be one of: useful, dead_end, corrected (got "${value.outcome}")`);
3314
+ }
3315
+ if (!TYPES.has(value.type)) {
3316
+ throw new Error(`--type must be one of: query, path, explain, affected (got "${value.type}")`);
3317
+ }
3318
+ if (value.outcome === "corrected" && !value.correction) {
3319
+ throw new Error("--outcome corrected requires --correction with what the right answer was");
3320
+ }
3321
+ }
3322
+ async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
3323
+ validateResult(entry);
3324
+ await fs14.mkdir(memoryDir, { recursive: true });
3325
+ const saved = { ...entry, savedAt: now.toISOString() };
3326
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
3327
+ const stamp = saved.savedAt.replace(/[:.]/g, "-");
3328
+ const filePath = path9.join(memoryDir, `result-${stamp}-${hash}.json`);
3329
+ await fs14.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
3330
+ return filePath;
3331
+ }
3332
+ async function loadResults(memoryDir) {
3333
+ let files;
3334
+ try {
3335
+ files = (await fs14.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
3336
+ } catch {
3337
+ return [];
3338
+ }
3339
+ const results = [];
3340
+ for (const file of files.sort((a, b) => a.localeCompare(b))) {
3341
+ try {
3342
+ const parsed = JSON.parse(await fs14.readFile(path9.join(memoryDir, file), "utf-8"));
3343
+ if (parsed.question && parsed.savedAt) results.push(parsed);
3344
+ } catch {
3345
+ }
3346
+ }
3347
+ return results;
3348
+ }
3349
+ function renderLessons(results, options = {}) {
3350
+ const halfLife = options.halfLifeDays ?? 30;
3351
+ const now = options.now ?? /* @__PURE__ */ new Date();
3352
+ const minCorroboration = options.minCorroboration ?? 2;
3353
+ const weightOf = (savedAt) => {
3354
+ const ageDays = Math.max(0, (now.getTime() - new Date(savedAt).getTime()) / 864e5);
3355
+ return Math.pow(0.5, ageDays / halfLife);
3356
+ };
3357
+ const signals = /* @__PURE__ */ new Map();
3358
+ const corrections = [];
3359
+ for (const result of results) {
3360
+ const weight = weightOf(result.savedAt);
3361
+ for (const node of result.nodes ?? []) {
3362
+ const signal = signals.get(node) ?? { useful: 0, deadEnd: 0, usefulCount: 0, deadEndCount: 0 };
3363
+ if (result.outcome === "useful") {
3364
+ signal.useful += weight;
3365
+ signal.usefulCount++;
3366
+ } else if (result.outcome === "dead_end") {
3367
+ signal.deadEnd += weight;
3368
+ signal.deadEndCount++;
3369
+ }
3370
+ signals.set(node, signal);
3371
+ }
3372
+ if (result.outcome === "corrected" && result.correction) {
3373
+ corrections.push({ question: result.question, correction: result.correction, savedAt: result.savedAt });
3374
+ }
3375
+ }
3376
+ 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);
3377
+ const lines = [
3378
+ "# Graph lessons",
3379
+ "",
3380
+ `_Aggregated from ${results.length} saved result(s); signal half-life ${halfLife} day(s)._`,
3381
+ "",
3382
+ "## Nodes that keep answering",
3383
+ ""
3384
+ ];
3385
+ const useful = byScore("useful");
3386
+ if (useful.length === 0) {
3387
+ lines.push(`(none yet with >= ${minCorroboration} corroborating results \u2014 keep saving results)`);
3388
+ } else {
3389
+ for (const [node, s] of useful) {
3390
+ lines.push(`- **${node}** \u2014 score ${s.useful.toFixed(2)} across ${s.usefulCount} useful result(s)`);
3391
+ }
3392
+ }
3393
+ lines.push("", "## Dead ends", "");
3394
+ const deadEnds = byScore("deadEnd");
3395
+ if (deadEnds.length === 0) {
3396
+ lines.push("(none recorded)");
3397
+ } else {
3398
+ for (const [node, s] of deadEnds) {
3399
+ lines.push(`- **${node}** \u2014 score ${s.deadEnd.toFixed(2)} across ${s.deadEndCount} dead-end result(s)`);
3400
+ }
3401
+ }
3402
+ lines.push("", "## Corrections", "");
3403
+ if (corrections.length === 0) {
3404
+ lines.push("(none recorded)");
3405
+ } else {
3406
+ corrections.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
3407
+ for (const c of corrections.slice(0, 20)) {
3408
+ lines.push(`- (${c.savedAt.slice(0, 10)}) Q: ${c.question}`);
3409
+ lines.push(` Right answer: ${c.correction}`);
3410
+ }
3411
+ }
3412
+ lines.push("");
3413
+ return lines.join("\n");
3414
+ }
3415
+
3416
+ // src/context.ts
3417
+ var DEFAULT_CONTEXT_BUDGET = 4e3;
3418
+ var DEFAULT_MAX_SEEDS2 = 8;
3419
+ var DEFAULT_MAX_SNIPPET_LINES = 60;
3420
+ var NEIGHBOR_DECAY = 0.4;
3421
+ var tokensOf = (text) => Math.ceil(text.length / 4);
3422
+ function lineNumberOf(sourceLocation) {
3423
+ const match = /^L(\d+)$/.exec(sourceLocation);
3424
+ return match ? Number(match[1]) : null;
3425
+ }
3426
+ function isReadableFile(sourceFile) {
3427
+ return sourceFile !== "" && !sourceFile.startsWith("<") && !sourceFile.includes("://");
3428
+ }
3429
+ function rankForTask(graph, task, maxSeeds = DEFAULT_MAX_SEEDS2) {
3430
+ const seeds = scoreNodes(graph, task).slice(0, maxSeeds);
3431
+ const ranked = /* @__PURE__ */ new Map();
3432
+ for (const seed of seeds) {
3433
+ ranked.set(seed.id, { id: seed.id, score: seed.score, reason: "matches the task" });
3434
+ }
3435
+ for (const seed of seeds) {
3436
+ graph.forEachEdge(seed.id, (_edge, attrs, source, target) => {
3437
+ const neighbor = source === seed.id ? target : source;
3438
+ if (ranked.has(neighbor)) return;
3439
+ const relation = String(attrs.relation);
3440
+ const direction = source === seed.id ? `${relation} ->` : `<- ${relation}`;
3441
+ ranked.set(neighbor, {
3442
+ id: neighbor,
3443
+ score: seed.score * NEIGHBOR_DECAY,
3444
+ reason: `${direction} ${seed.label}`
3445
+ });
3446
+ });
3447
+ }
3448
+ return [...ranked.values()].sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
3449
+ }
3450
+ function snippetRange(startLine, fileLineCount, entityStartsInFile, maxLines) {
3451
+ const nextStart = entityStartsInFile.find((l) => l > startLine);
3452
+ const hardEnd = nextStart !== void 0 ? nextStart - 1 : fileLineCount;
3453
+ return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };
3454
+ }
3455
+ async function buildContextPack(graph, task, readFile13, options = {}) {
3456
+ const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;
3457
+ const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;
3458
+ const ranked = rankForTask(graph, task, options.maxSeeds);
3459
+ const entityStarts = /* @__PURE__ */ new Map();
3460
+ graph.forEachNode((_id, attrs) => {
3461
+ const file = attrs.sourceFile;
3462
+ const line = lineNumberOf(attrs.sourceLocation ?? "");
3463
+ if (file && line !== null) {
3464
+ const starts = entityStarts.get(file) ?? [];
3465
+ starts.push(line);
3466
+ entityStarts.set(file, starts);
3467
+ }
3468
+ });
3469
+ for (const starts of entityStarts.values()) starts.sort((a, b) => a - b);
3470
+ const fileCache = /* @__PURE__ */ new Map();
3471
+ const readLines = async (file) => {
3472
+ const cached = fileCache.get(file);
3473
+ if (cached !== void 0) return cached;
3474
+ let lines;
3475
+ try {
3476
+ lines = (await readFile13(file)).split("\n");
3477
+ } catch {
3478
+ lines = null;
3479
+ }
3480
+ fileCache.set(file, lines);
3481
+ return lines;
3482
+ };
3483
+ const snippets = [];
3484
+ const overflow = [];
3485
+ const coveredRanges = /* @__PURE__ */ new Map();
3486
+ let tokens = 0;
3487
+ for (const node of ranked) {
3488
+ const attrs = graph.getNodeAttributes(node.id);
3489
+ const file = attrs.sourceFile ?? "";
3490
+ const startLine = lineNumberOf(attrs.sourceLocation ?? "");
3491
+ if (!isReadableFile(file) || startLine === null) continue;
3492
+ const lines = await readLines(file);
3493
+ if (lines === null) continue;
3494
+ const { start, end } = snippetRange(startLine, lines.length, entityStarts.get(file) ?? [], maxSnippetLines);
3495
+ const covered = (coveredRanges.get(file) ?? []).some((r) => start >= r.start && end <= r.end);
3496
+ if (covered) continue;
3497
+ const code = lines.slice(start - 1, end).join("\n");
3498
+ const cost = tokensOf(code) + 15;
3499
+ if (tokens + cost > tokenBudget) {
3500
+ overflow.push({ nodeId: node.id, file: `${file}:L${startLine}` });
3501
+ continue;
3502
+ }
3503
+ tokens += cost;
3504
+ snippets.push({
3505
+ nodeId: node.id,
3506
+ label: attrs.label ?? node.id,
3507
+ file,
3508
+ startLine: start,
3509
+ endLine: end,
3510
+ code,
3511
+ reason: node.reason,
3512
+ score: node.score
3513
+ });
3514
+ const ranges = coveredRanges.get(file) ?? [];
3515
+ ranges.push({ start, end });
3516
+ coveredRanges.set(file, ranges);
3517
+ }
3518
+ return { task, snippets, tokens, tokenBudget, overflow };
3519
+ }
3520
+ function renderContextPack(pack) {
3521
+ const lines = [
3522
+ `# Context pack: ${pack.task}`,
3523
+ "",
3524
+ `_~${pack.tokens} of ${pack.tokenBudget} token budget, ${pack.snippets.length} snippet(s)._`,
3525
+ ""
3526
+ ];
3527
+ if (pack.snippets.length === 0) {
3528
+ lines.push("No matching code found \u2014 try different keywords.");
3529
+ return lines.join("\n");
3530
+ }
3531
+ for (const snippet of pack.snippets) {
3532
+ lines.push(`## ${snippet.label} \u2014 \`${snippet.file}:L${snippet.startLine}-L${snippet.endLine}\``);
3533
+ lines.push(`_${snippet.reason}_`);
3534
+ lines.push("```");
3535
+ lines.push(snippet.code);
3536
+ lines.push("```");
3537
+ lines.push("");
3538
+ }
3539
+ if (pack.overflow.length > 0) {
3540
+ lines.push(`## Didn't fit the budget (${pack.overflow.length})`);
3541
+ for (const item of pack.overflow.slice(0, 15)) {
3542
+ lines.push(`- ${item.nodeId} (${item.file})`);
3543
+ }
3544
+ lines.push("");
3545
+ }
3546
+ return lines.join("\n");
3547
+ }
3548
+
3549
+ // src/testmap.ts
3550
+ var TEST_FILE_PATTERNS = [
3551
+ /(^|\/)tests?\//,
3552
+ // tests/ or test/ directory
3553
+ /(^|\/)__tests__\//,
3554
+ /(^|\/)spec\//,
3555
+ /\.test\.[cm]?[jt]sx?$/,
3556
+ /\.spec\.[cm]?[jt]sx?$/,
3557
+ /(^|\/)test_[^/]+\.py$/,
3558
+ /_test\.py$/,
3559
+ /_test\.go$/,
3560
+ /Tests?\.(java|cs)$/,
3561
+ /_spec\.rb$/,
3562
+ /_test\.rb$/
3563
+ ];
3564
+ function isTestFile(path11) {
3565
+ return TEST_FILE_PATTERNS.some((p) => p.test(path11));
3566
+ }
3567
+ var TEST_REACH_DEPTH = 4;
3568
+ var TEST_REACH_LIMIT = 2e3;
3569
+ function selectionFromImpact(graph, targetIds, targetLabel) {
3570
+ const best = /* @__PURE__ */ new Map();
3571
+ let affectedNodeCount = 0;
3572
+ for (const id of targetIds) {
3573
+ const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
3574
+ if (!impact) continue;
3575
+ affectedNodeCount += impact.affected.length;
3576
+ for (const node of impact.affected) {
3577
+ if (!isTestFile(node.sourceFile)) continue;
3578
+ const existing = best.get(node.sourceFile);
3579
+ if (!existing || node.depth < existing.depth) {
3580
+ best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });
3581
+ }
3582
+ }
3583
+ }
3584
+ 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));
3585
+ return { target: targetLabel, testFiles, affectedNodeCount };
3586
+ }
3587
+ function testsForNode(graph, nodeQuery) {
3588
+ const match = resolveNode(graph, nodeQuery);
3589
+ if (!match) return null;
3590
+ return selectionFromImpact(graph, [match.id], match.id);
3591
+ }
3592
+ function testsForChangedFiles(graph, changedFiles) {
3593
+ const fileIds = [];
3594
+ const selfSelected = /* @__PURE__ */ new Map();
3595
+ for (const file of changedFiles) {
3596
+ if (isTestFile(file)) {
3597
+ selfSelected.set(file, { depth: 0, via: "(changed directly)" });
3598
+ continue;
3599
+ }
3600
+ if (graph.hasNode(file)) fileIds.push(file);
3601
+ }
3602
+ const selection = selectionFromImpact(graph, fileIds, changedFiles.join(", "));
3603
+ for (const [file, info] of selfSelected) {
3604
+ if (!selection.testFiles.some((t) => t.file === file)) {
3605
+ selection.testFiles.push({ file, depth: info.depth, via: info.via });
3606
+ }
3607
+ }
3608
+ selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
3609
+ return selection;
3610
+ }
3611
+
3612
+ // src/diff.ts
3613
+ var import_node_child_process = require("child_process");
3614
+ var fs15 = __toESM(require("fs/promises"), 1);
3615
+ var os = __toESM(require("os"), 1);
3616
+ var path10 = __toESM(require("path"), 1);
3617
+ var import_node_util = require("util");
3618
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
3619
+ async function graphForDirectory(root) {
3620
+ const manifest = collectFiles(root);
3621
+ const extractions = [];
3622
+ for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
3623
+ extractions.push(await extract(path10.join(root, relFile), relFile));
3624
+ }
3625
+ resolveCrossFileReferences(extractions);
3626
+ return buildGraph(extractions);
3627
+ }
3628
+ async function checkoutRev(repoRoot, rev) {
3629
+ const dest = await fs15.mkdtemp(path10.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
3630
+ const { stdout } = await execFileAsync(
3631
+ "git",
3632
+ ["-C", repoRoot, "archive", "--format=tar", rev],
3633
+ { encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }
3634
+ );
3635
+ await new Promise((resolve5, reject) => {
3636
+ const tar = (0, import_node_child_process.execFile)("tar", ["-x", "-C", dest], (error) => error ? reject(error) : resolve5());
3637
+ tar.stdin?.end(stdout);
3638
+ });
3639
+ return dest;
3640
+ }
3641
+ function edgeSignature(graph, id) {
3642
+ const signature = /* @__PURE__ */ new Set();
3643
+ graph.forEachOutEdge(id, (_e, attrs, _s, target) => signature.add(`-> ${String(attrs.relation)} ${target}`));
3644
+ graph.forEachInEdge(id, (_e, attrs, source) => signature.add(`<- ${String(attrs.relation)} ${source}`));
3645
+ return signature;
3646
+ }
3647
+ function describeSymbol(graph, id) {
3648
+ const attrs = graph.getNodeAttributes(id);
3649
+ const impact = affectedBy(graph, id, { maxDepth: 3 });
3650
+ const tests = testsForNode(graph, id);
3651
+ return {
3652
+ id,
3653
+ label: attrs.label ?? id,
3654
+ sourceFile: attrs.sourceFile ?? "",
3655
+ blastRadius: impact?.affected.length ?? 0,
3656
+ tests: tests?.testFiles.map((t) => t.file) ?? []
3657
+ };
3658
+ }
3659
+ function entityIds(graph) {
3660
+ const ids = /* @__PURE__ */ new Set();
3661
+ graph.forEachNode((id) => {
3662
+ if (id.includes("::")) ids.add(id);
3663
+ });
3664
+ return ids;
3665
+ }
3666
+ function diffGraphs(baseGraph, headGraph, base, head) {
3667
+ const baseIds = entityIds(baseGraph);
3668
+ const headIds = entityIds(headGraph);
3669
+ const added = [];
3670
+ const removed = [];
3671
+ const rewired = [];
3672
+ for (const id of [...headIds].sort((a, b) => a.localeCompare(b))) {
3673
+ if (!baseIds.has(id)) {
3674
+ added.push(describeSymbol(headGraph, id));
3675
+ continue;
3676
+ }
3677
+ const before = edgeSignature(baseGraph, id);
3678
+ const after = edgeSignature(headGraph, id);
3679
+ const gained = [...after].filter((e) => !before.has(e)).sort((a, b) => a.localeCompare(b));
3680
+ const lost = [...before].filter((e) => !after.has(e)).sort((a, b) => a.localeCompare(b));
3681
+ if (gained.length > 0 || lost.length > 0) {
3682
+ rewired.push({ ...describeSymbol(headGraph, id), gainedEdges: gained, lostEdges: lost });
3683
+ }
3684
+ }
3685
+ for (const id of [...baseIds].sort((a, b) => a.localeCompare(b))) {
3686
+ if (!headIds.has(id)) removed.push(describeSymbol(baseGraph, id));
3687
+ }
3688
+ return { base, head, added, removed, rewired };
3689
+ }
3690
+ async function reviewRevisions(repoRoot, base, head = "HEAD") {
3691
+ const [baseDir, headDir] = await Promise.all([checkoutRev(repoRoot, base), checkoutRev(repoRoot, head)]);
3692
+ try {
3693
+ const [baseGraph, headGraph] = [await graphForDirectory(baseDir), await graphForDirectory(headDir)];
3694
+ return diffGraphs(baseGraph, headGraph, base, head);
3695
+ } finally {
3696
+ await Promise.all([
3697
+ fs15.rm(baseDir, { recursive: true, force: true }),
3698
+ fs15.rm(headDir, { recursive: true, force: true })
3699
+ ]);
3700
+ }
3701
+ }
3702
+ function renderReview(diff) {
3703
+ const lines = [
3704
+ `# Structural review: ${diff.base}..${diff.head}`,
3705
+ "",
3706
+ `${diff.added.length} symbol(s) added, ${diff.removed.length} removed, ${diff.rewired.length} rewired.`,
3707
+ ""
3708
+ ];
3709
+ const section = (title, symbols) => {
3710
+ lines.push(`## ${title} (${symbols.length})`, "");
3711
+ if (symbols.length === 0) {
3712
+ lines.push("(none)", "");
3713
+ return;
3714
+ }
3715
+ for (const s of symbols) {
3716
+ const tests = s.tests.length > 0 ? ` | tests: ${s.tests.join(", ")}` : " | tests: none found";
3717
+ lines.push(`- **${s.label}** (${s.sourceFile}) \u2014 blast radius ${s.blastRadius}${tests}`);
3718
+ const r = s;
3719
+ if (r.gainedEdges) {
3720
+ for (const e of r.gainedEdges.slice(0, 5)) lines.push(` - gained: ${e}`);
3721
+ for (const e of r.lostEdges.slice(0, 5)) lines.push(` - lost: ${e}`);
3722
+ }
3723
+ }
3724
+ lines.push("");
3725
+ };
3726
+ section("Removed \u2014 check every caller", diff.removed);
3727
+ section("Rewired \u2014 dependencies changed", diff.rewired);
3728
+ section("Added", diff.added);
3729
+ const allTests = /* @__PURE__ */ new Set();
3730
+ for (const s of [...diff.removed, ...diff.rewired, ...diff.added]) {
3731
+ for (const t of s.tests) allTests.add(t);
3732
+ }
3733
+ lines.push(`## Suggested test run (${allTests.size} file(s))`, "");
3734
+ for (const t of [...allTests].sort((a, b) => a.localeCompare(b))) lines.push(`- ${t}`);
3735
+ lines.push("");
3736
+ return lines.join("\n");
3737
+ }
3738
+
3739
+ // src/check.ts
3740
+ var DEPENDENCY_RELATIONS2 = /* @__PURE__ */ new Set([
3741
+ "calls",
3742
+ "imports",
3743
+ "imports_from",
3744
+ "inherits",
3745
+ "implements",
3746
+ "mixes_in",
3747
+ "embeds",
3748
+ "re_exports"
3749
+ ]);
3750
+ function globToRegExp(glob) {
3751
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
3752
+ const pattern = escaped.replace(
3753
+ /\*\*|\*|\?/g,
3754
+ (token) => token === "**" ? ".*" : token === "*" ? "[^/]*" : "[^/]"
3755
+ );
3756
+ return new RegExp(`^${pattern}$`);
3757
+ }
3758
+ function validateRules(value) {
3759
+ const config = value;
3760
+ if (!config || !Array.isArray(config.rules)) {
3761
+ throw new Error('Rules file must be JSON of shape { "rules": [{ "name", "from", "disallow": [...] }] }');
3762
+ }
3763
+ for (const rule of config.rules) {
3764
+ if (!rule.name || typeof rule.from !== "string" || !Array.isArray(rule.disallow)) {
3765
+ throw new Error(`Malformed rule "${rule.name ?? "(unnamed)"}" \u2014 need name, from, and disallow[].`);
3766
+ }
3767
+ }
3768
+ return config;
3769
+ }
3770
+ function checkRules(graph, config) {
3771
+ const compiled = config.rules.map((rule) => ({
3772
+ rule,
3773
+ from: globToRegExp(rule.from),
3774
+ disallow: rule.disallow.map(globToRegExp)
3775
+ }));
3776
+ const violations = [];
3777
+ graph.forEachEdge((_edge, attrs, source, target) => {
3778
+ const relation = String(attrs.relation);
3779
+ if (!DEPENDENCY_RELATIONS2.has(relation)) return;
3780
+ const fromFile = graph.getNodeAttribute(source, "sourceFile") ?? "";
3781
+ const toFile = graph.getNodeAttribute(target, "sourceFile") ?? "";
3782
+ if (!fromFile || !toFile || fromFile === toFile) return;
3783
+ for (const { rule, from, disallow } of compiled) {
3784
+ if (!from.test(fromFile)) continue;
3785
+ if (disallow.some((d) => d.test(toFile))) {
3786
+ violations.push({
3787
+ rule: rule.name,
3788
+ reason: rule.reason,
3789
+ fromFile,
3790
+ fromNode: source,
3791
+ toFile,
3792
+ toNode: target,
3793
+ relation
3794
+ });
3795
+ }
3796
+ }
3797
+ });
3798
+ violations.sort(
3799
+ (a, b) => a.rule.localeCompare(b.rule) || a.fromFile.localeCompare(b.fromFile) || a.toFile.localeCompare(b.toFile) || a.fromNode.localeCompare(b.fromNode)
3800
+ );
3801
+ return violations;
3802
+ }
2738
3803
  // Annotate the CommonJS export names for ESM import in node:
2739
3804
  0 && (module.exports = {
2740
3805
  EXTRACTOR_REGISTRY,
3806
+ ExtractionCache,
2741
3807
  ExtractionResultSchema,
2742
3808
  ExtractionValidationError,
3809
+ affectedBy,
2743
3810
  analyze,
3811
+ buildContextPack,
2744
3812
  buildGraph,
3813
+ checkRules,
2745
3814
  cluster,
2746
3815
  collectFiles,
3816
+ diffGraphs,
2747
3817
  explainNode,
2748
3818
  exportGraph,
2749
3819
  extract,
3820
+ extractMysql,
3821
+ globToRegExp,
3822
+ graphForDirectory,
3823
+ isTestFile,
2750
3824
  loadGraph,
3825
+ loadResults,
3826
+ mergeGraphs,
2751
3827
  queryGraph,
3828
+ rankForTask,
3829
+ renderContextPack,
3830
+ renderLessons,
2752
3831
  renderReport,
3832
+ renderReview,
3833
+ resolveCrossFileReferences,
2753
3834
  resolveNode,
3835
+ reviewRevisions,
2754
3836
  runPipeline,
3837
+ saveResult,
2755
3838
  scoreNodes,
2756
3839
  shortestPath,
2757
- validateExtraction
3840
+ testsForChangedFiles,
3841
+ testsForNode,
3842
+ validateExtraction,
3843
+ validateRules
2758
3844
  });
2759
3845
  //# sourceMappingURL=index.cjs.map