@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.
@@ -1,8 +1,12 @@
1
+ import {
2
+ affectedBy,
3
+ testsForNode
4
+ } from "./chunk-YT7B6DOD.js";
1
5
  import {
2
6
  escapeHtml,
3
7
  sanitizeLabel,
4
8
  validateGraphPath
5
- } from "./chunk-5ANIDX3G.js";
9
+ } from "./chunk-6JLEILYF.js";
6
10
 
7
11
  // src/detect.ts
8
12
  import * as fs from "fs";
@@ -242,7 +246,7 @@ async function createParser(grammarName) {
242
246
  }
243
247
 
244
248
  // src/extractors/csharp.ts
245
- async function extractCSharp(filePath) {
249
+ async function extractCSharp(filePath, displayPath) {
246
250
  const parser = await createParser("c_sharp");
247
251
  const raw = await fs2.readFile(filePath);
248
252
  const content = raw.toString("utf-8");
@@ -251,7 +255,7 @@ async function extractCSharp(filePath) {
251
255
  throw new Error(`extractCSharp: parser produced no tree for ${filePath}`);
252
256
  }
253
257
  const root = tree.rootNode;
254
- const sourceFile = toPosix(filePath);
258
+ const sourceFile = toPosix(displayPath ?? filePath);
255
259
  const builder = new ExtractionBuilder();
256
260
  const fileId = sourceFile;
257
261
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -455,7 +459,7 @@ function defaultPackageQualifier(importPath) {
455
459
  const segments = importPath.split("/");
456
460
  return segments[segments.length - 1] ?? importPath;
457
461
  }
458
- async function extractGo(filePath) {
462
+ async function extractGo(filePath, displayPath) {
459
463
  const parser = await createParser("go");
460
464
  const raw = await fs3.readFile(filePath);
461
465
  const content = raw.toString("utf-8");
@@ -464,7 +468,7 @@ async function extractGo(filePath) {
464
468
  throw new Error(`extractGo: parser produced no tree for ${filePath}`);
465
469
  }
466
470
  const root = tree.rootNode;
467
- const sourceFile = toPosix(filePath);
471
+ const sourceFile = toPosix(displayPath ?? filePath);
468
472
  const builder = new ExtractionBuilder();
469
473
  const fileId = sourceFile;
470
474
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -662,7 +666,7 @@ async function extractGo(filePath) {
662
666
 
663
667
  // src/extractors/java.ts
664
668
  import * as fs4 from "fs/promises";
665
- async function extractJava(filePath) {
669
+ async function extractJava(filePath, displayPath) {
666
670
  const parser = await createParser("java");
667
671
  const raw = await fs4.readFile(filePath);
668
672
  const content = raw.toString("utf-8");
@@ -671,7 +675,7 @@ async function extractJava(filePath) {
671
675
  throw new Error(`extractJava: parser produced no tree for ${filePath}`);
672
676
  }
673
677
  const root = tree.rootNode;
674
- const sourceFile = toPosix(filePath);
678
+ const sourceFile = toPosix(displayPath ?? filePath);
675
679
  const builder = new ExtractionBuilder();
676
680
  const fileId = sourceFile;
677
681
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -863,7 +867,7 @@ async function extractJava(filePath) {
863
867
 
864
868
  // src/extractors/python.ts
865
869
  import * as fs5 from "fs/promises";
866
- async function extractPython(filePath) {
870
+ async function extractPython(filePath, displayPath) {
867
871
  const parser = await createParser("python");
868
872
  const raw = await fs5.readFile(filePath);
869
873
  const content = raw.toString("utf-8");
@@ -872,7 +876,7 @@ async function extractPython(filePath) {
872
876
  throw new Error(`extractPython: parser produced no tree for ${filePath}`);
873
877
  }
874
878
  const root = tree.rootNode;
875
- const sourceFile = toPosix(filePath);
879
+ const sourceFile = toPosix(displayPath ?? filePath);
876
880
  const builder = new ExtractionBuilder();
877
881
  const fileId = sourceFile;
878
882
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1107,7 +1111,7 @@ function stringLiteralContent(node) {
1107
1111
  const content = namedChildren(node).find((c) => c.type === "string_content");
1108
1112
  return content ? content.text : "";
1109
1113
  }
1110
- async function extractRuby(filePath) {
1114
+ async function extractRuby(filePath, displayPath) {
1111
1115
  const parser = await createParser("ruby");
1112
1116
  const raw = await fs6.readFile(filePath);
1113
1117
  const content = raw.toString("utf-8");
@@ -1116,7 +1120,7 @@ async function extractRuby(filePath) {
1116
1120
  throw new Error(`extractRuby: parser produced no tree for ${filePath}`);
1117
1121
  }
1118
1122
  const root = tree.rootNode;
1119
- const sourceFile = toPosix(filePath);
1123
+ const sourceFile = toPosix(displayPath ?? filePath);
1120
1124
  const builder = new ExtractionBuilder();
1121
1125
  const fileId = sourceFile;
1122
1126
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1309,7 +1313,7 @@ function convertUsePath(segments) {
1309
1313
  }
1310
1314
  return segments.join("::");
1311
1315
  }
1312
- async function extractRust(filePath) {
1316
+ async function extractRust(filePath, displayPath) {
1313
1317
  const parser = await createParser("rust");
1314
1318
  const raw = await fs7.readFile(filePath);
1315
1319
  const content = raw.toString("utf-8");
@@ -1318,7 +1322,7 @@ async function extractRust(filePath) {
1318
1322
  throw new Error(`extractRust: parser produced no tree for ${filePath}`);
1319
1323
  }
1320
1324
  const root = tree.rootNode;
1321
- const sourceFile = toPosix(filePath);
1325
+ const sourceFile = toPosix(displayPath ?? filePath);
1322
1326
  const builder = new ExtractionBuilder();
1323
1327
  const fileId = sourceFile;
1324
1328
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1531,10 +1535,10 @@ async function extractRust(filePath) {
1531
1535
  }
1532
1536
  }
1533
1537
  } else if (fn.type === "scoped_identifier") {
1534
- const path7 = fn.childForFieldName("path")?.text;
1538
+ const path11 = fn.childForFieldName("path")?.text;
1535
1539
  const name = fn.childForFieldName("name")?.text;
1536
- if (path7 && name) {
1537
- const viaImport = importIndex.get(path7);
1540
+ if (path11 && name) {
1541
+ const viaImport = importIndex.get(path11);
1538
1542
  if (viaImport) {
1539
1543
  targetId = viaImport.id;
1540
1544
  confidence = "INFERRED";
@@ -1575,7 +1579,7 @@ function firstStringArgument(call) {
1575
1579
  if (!first || first.type !== "string") return null;
1576
1580
  return stripQuotes2(first.text);
1577
1581
  }
1578
- async function extractTypeScript(filePath) {
1582
+ async function extractTypeScript(filePath, displayPath) {
1579
1583
  const ext = path3.extname(filePath);
1580
1584
  const grammar = grammarForExtension(ext);
1581
1585
  const parser = await createParser(grammar);
@@ -1586,7 +1590,7 @@ async function extractTypeScript(filePath) {
1586
1590
  throw new Error(`extractTypeScript: parser produced no tree for ${filePath}`);
1587
1591
  }
1588
1592
  const root = tree.rootNode;
1589
- const sourceFile = toPosix(filePath);
1593
+ const sourceFile = toPosix(displayPath ?? filePath);
1590
1594
  const builder = new ExtractionBuilder();
1591
1595
  const fileId = sourceFile;
1592
1596
  builder.addNode({ id: fileId, label: sourceFile, sourceFile, sourceLocation: "L1" });
@@ -1903,13 +1907,13 @@ var EXTRACTOR_REGISTRY = {
1903
1907
  ".cs": extractCSharp,
1904
1908
  ".rb": extractRuby
1905
1909
  };
1906
- async function extract(filePath) {
1910
+ async function extract(filePath, displayPath) {
1907
1911
  const ext = path4.extname(filePath);
1908
1912
  const extractor = EXTRACTOR_REGISTRY[ext];
1909
1913
  if (!extractor) {
1910
1914
  return { nodes: [], edges: [] };
1911
1915
  }
1912
- return extractor(filePath);
1916
+ return extractor(filePath, displayPath);
1913
1917
  }
1914
1918
 
1915
1919
  // src/schema.ts
@@ -2012,6 +2016,150 @@ function buildGraph(extractions) {
2012
2016
  return graph;
2013
2017
  }
2014
2018
 
2019
+ // src/resolve.ts
2020
+ import * as fs9 from "fs/promises";
2021
+ import * as path5 from "path";
2022
+ var CODE_EXTENSIONS2 = [
2023
+ ".ts",
2024
+ ".tsx",
2025
+ ".mts",
2026
+ ".cts",
2027
+ ".js",
2028
+ ".jsx",
2029
+ ".mjs",
2030
+ ".cjs",
2031
+ ".py",
2032
+ ".go",
2033
+ ".rs",
2034
+ ".java",
2035
+ ".cs",
2036
+ ".rb"
2037
+ ];
2038
+ function isOpaque(id) {
2039
+ return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
2040
+ }
2041
+ var SELF_IMPORT_STEMS = (subpath) => subpath === "" ? ["index", "src/index", "lib/index", "source/index", "src/main"] : [
2042
+ subpath,
2043
+ `src/${subpath}`,
2044
+ `lib/${subpath}`,
2045
+ `source/${subpath}`,
2046
+ `${subpath}/index`,
2047
+ `src/${subpath}/index`,
2048
+ `lib/${subpath}/index`
2049
+ ];
2050
+ function selfImportCandidates(spec, selfNames) {
2051
+ const selfName = selfNames.find((n) => spec === n || spec.startsWith(`${n}/`));
2052
+ if (selfName === void 0) return null;
2053
+ const subpath = spec === selfName ? "" : spec.slice(selfName.length + 1);
2054
+ return SELF_IMPORT_STEMS(subpath).flatMap((stem) => CODE_EXTENSIONS2.map((ext) => `${stem}${ext}`));
2055
+ }
2056
+ async function readSelfNames(root) {
2057
+ try {
2058
+ const pkg = JSON.parse(await fs9.readFile(path5.join(root, "package.json"), "utf-8"));
2059
+ return typeof pkg.name === "string" && pkg.name.length > 0 ? [pkg.name] : [];
2060
+ } catch {
2061
+ return [];
2062
+ }
2063
+ }
2064
+ function stripKnownExtension(p) {
2065
+ for (const ext of CODE_EXTENSIONS2) {
2066
+ if (p.endsWith(ext)) return p.slice(0, -ext.length);
2067
+ }
2068
+ return null;
2069
+ }
2070
+ function candidatePaths(guessed) {
2071
+ const stem = stripKnownExtension(guessed) ?? guessed;
2072
+ const candidates = [];
2073
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}${ext}`);
2074
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${guessed}/index${ext}`);
2075
+ if (stem !== guessed) {
2076
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}/index${ext}`);
2077
+ }
2078
+ return candidates.filter((c) => c !== guessed);
2079
+ }
2080
+ function uniqueMatch(guessed, candidates, realIds) {
2081
+ const matches = /* @__PURE__ */ new Set();
2082
+ for (const candidate of candidates) {
2083
+ if (realIds.has(candidate)) matches.add(candidate);
2084
+ }
2085
+ if (matches.size !== 1) return null;
2086
+ return [...matches][0];
2087
+ }
2088
+ function resolveCrossFileReferences(extractions, options = {}) {
2089
+ const selfNames = options.selfNames ?? [];
2090
+ const realIds = /* @__PURE__ */ new Set();
2091
+ const realFiles = /* @__PURE__ */ new Set();
2092
+ for (const extraction of extractions) {
2093
+ for (const node of extraction.nodes) realIds.add(node.id);
2094
+ const first = extraction.nodes[0];
2095
+ if (first && first.id === first.sourceFile && !isOpaque(first.id)) {
2096
+ realFiles.add(first.id);
2097
+ }
2098
+ for (const node of extraction.nodes) {
2099
+ const sep2 = node.id.indexOf("::");
2100
+ if (sep2 > 0) realFiles.add(node.id.slice(0, sep2));
2101
+ }
2102
+ for (const edge of extraction.edges) {
2103
+ if (edge.relation === "contains") realFiles.add(edge.source);
2104
+ }
2105
+ }
2106
+ const remap = /* @__PURE__ */ new Map();
2107
+ const resolveId = (id) => {
2108
+ if (id.startsWith("module:") && selfNames.length > 0) {
2109
+ const cachedSelf = remap.get(id);
2110
+ if (cachedSelf !== void 0) return cachedSelf;
2111
+ const candidates = selfImportCandidates(id.slice("module:".length), selfNames);
2112
+ if (candidates !== null) {
2113
+ const resolved2 = uniqueMatch(id, candidates, realFiles);
2114
+ if (resolved2 !== null) {
2115
+ remap.set(id, resolved2);
2116
+ return resolved2;
2117
+ }
2118
+ }
2119
+ return null;
2120
+ }
2121
+ if (isOpaque(id)) return null;
2122
+ const cached = remap.get(id);
2123
+ if (cached !== void 0) return cached;
2124
+ const sep2 = id.indexOf("::");
2125
+ let resolved;
2126
+ if (sep2 > 0) {
2127
+ if (realIds.has(id)) return null;
2128
+ const guessedPath = id.slice(0, sep2);
2129
+ const name = id.slice(sep2);
2130
+ const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
2131
+ resolved = uniqueMatch(id, candidates, realIds);
2132
+ } else {
2133
+ if (realFiles.has(id)) return null;
2134
+ resolved = uniqueMatch(id, candidatePaths(id), realFiles);
2135
+ }
2136
+ if (resolved !== null) remap.set(id, resolved);
2137
+ return resolved;
2138
+ };
2139
+ let resolvedEndpoints = 0;
2140
+ for (const extraction of extractions) {
2141
+ for (const edge of extraction.edges) {
2142
+ const source = resolveId(edge.source);
2143
+ if (source !== null) {
2144
+ edge.source = source;
2145
+ resolvedEndpoints++;
2146
+ }
2147
+ const target = resolveId(edge.target);
2148
+ if (target !== null) {
2149
+ edge.target = target;
2150
+ resolvedEndpoints++;
2151
+ }
2152
+ }
2153
+ }
2154
+ let droppedPlaceholders = 0;
2155
+ for (const extraction of extractions) {
2156
+ const before = extraction.nodes.length;
2157
+ extraction.nodes = extraction.nodes.filter((node) => !remap.has(node.id));
2158
+ droppedPlaceholders += before - extraction.nodes.length;
2159
+ }
2160
+ return { resolvedEndpoints, droppedPlaceholders };
2161
+ }
2162
+
2015
2163
  // src/cluster.ts
2016
2164
  import { createHash } from "crypto";
2017
2165
  import Graph2 from "graphology";
@@ -2278,15 +2426,15 @@ function renderReport(graph, analysis, now = /* @__PURE__ */ new Date()) {
2278
2426
 
2279
2427
  // src/export.ts
2280
2428
  import { createRequire as createRequire2 } from "module";
2281
- import * as fs9 from "fs/promises";
2282
- import * as path5 from "path";
2429
+ import * as fs10 from "fs/promises";
2430
+ import * as path6 from "path";
2283
2431
  var require3 = createRequire2(import.meta.url);
2284
2432
  function resolveVisNetworkAssets() {
2285
2433
  const packageJsonPath = require3.resolve("vis-network/package.json");
2286
- const root = path5.dirname(packageJsonPath);
2434
+ const root = path6.dirname(packageJsonPath);
2287
2435
  return {
2288
- jsPath: path5.join(root, "standalone", "umd", "vis-network.min.js"),
2289
- cssPath: path5.join(root, "styles", "vis-network.min.css")
2436
+ jsPath: path6.join(root, "standalone", "umd", "vis-network.min.js"),
2437
+ cssPath: path6.join(root, "styles", "vis-network.min.css")
2290
2438
  };
2291
2439
  }
2292
2440
  var COMMUNITY_PALETTE = [
@@ -2345,8 +2493,8 @@ community: ${communityLabel}` : ""}`,
2345
2493
  async function renderHtml(graph) {
2346
2494
  const { jsPath, cssPath } = resolveVisNetworkAssets();
2347
2495
  const [visJs, visCss] = await Promise.all([
2348
- fs9.readFile(jsPath, "utf-8"),
2349
- fs9.readFile(cssPath, "utf-8")
2496
+ fs10.readFile(jsPath, "utf-8"),
2497
+ fs10.readFile(cssPath, "utf-8")
2350
2498
  ]);
2351
2499
  const { nodes, edges } = buildVisNodesAndEdges(graph);
2352
2500
  const dataJson = safeInlineJson({ nodes, edges });
@@ -2386,17 +2534,17 @@ ${visJs}
2386
2534
  `;
2387
2535
  }
2388
2536
  async function exportGraph(graph, options, report) {
2389
- const outDir = path5.resolve(options.outDir);
2390
- await fs9.mkdir(outDir, { recursive: true });
2537
+ const outDir = path6.resolve(options.outDir);
2538
+ await fs10.mkdir(outDir, { recursive: true });
2391
2539
  const jsonPath = validateGraphPath("graph.json", outDir);
2392
- await fs9.writeFile(jsonPath, JSON.stringify(graph.toJSON(), null, 2), "utf-8");
2540
+ await fs10.writeFile(jsonPath, JSON.stringify(graph.toJSON(), null, 2), "utf-8");
2393
2541
  if (options.html !== false) {
2394
2542
  const htmlPath = validateGraphPath("graph.html", outDir);
2395
- await fs9.writeFile(htmlPath, await renderHtml(graph), "utf-8");
2543
+ await fs10.writeFile(htmlPath, await renderHtml(graph), "utf-8");
2396
2544
  }
2397
2545
  if (report !== void 0) {
2398
2546
  const reportPath = validateGraphPath("GRAPH_REPORT.md", outDir);
2399
- await fs9.writeFile(reportPath, report, "utf-8");
2547
+ await fs10.writeFile(reportPath, report, "utf-8");
2400
2548
  }
2401
2549
  const notImplemented = [];
2402
2550
  if (options.svg) notImplemented.push("--svg");
@@ -2408,29 +2556,83 @@ async function exportGraph(graph, options, report) {
2408
2556
  }
2409
2557
  }
2410
2558
 
2559
+ // src/extractionCache.ts
2560
+ import { createHash as createHash2 } from "crypto";
2561
+ import * as fs11 from "fs/promises";
2562
+ import * as path7 from "path";
2563
+ var ExtractionCache = class {
2564
+ dir;
2565
+ constructor(outDir) {
2566
+ this.dir = path7.join(outDir, "cache");
2567
+ }
2568
+ key(relFile, content) {
2569
+ return createHash2("sha256").update(relFile).update("\0").update(content).digest("hex");
2570
+ }
2571
+ async get(key) {
2572
+ try {
2573
+ const raw = await fs11.readFile(path7.join(this.dir, `${key}.json`), "utf-8");
2574
+ const parsed = JSON.parse(raw);
2575
+ if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
2576
+ return parsed;
2577
+ } catch {
2578
+ return null;
2579
+ }
2580
+ }
2581
+ async put(key, extraction) {
2582
+ await fs11.mkdir(this.dir, { recursive: true });
2583
+ await fs11.writeFile(path7.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
2584
+ }
2585
+ };
2586
+
2411
2587
  // src/pipeline.ts
2412
- import * as path6 from "path";
2588
+ import * as fs12 from "fs/promises";
2589
+ import * as path8 from "path";
2413
2590
  async function runPipeline(root, options = {}) {
2414
2591
  const progress = options.onProgress ?? (() => {
2415
2592
  });
2416
- const resolvedRoot = path6.resolve(root);
2593
+ const resolvedRoot = path8.resolve(root);
2417
2594
  progress(`Scanning ${resolvedRoot} ...`);
2418
2595
  const manifest = collectFiles(resolvedRoot);
2419
2596
  progress(
2420
2597
  `Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
2421
2598
  );
2599
+ const outDir = options.outDir ?? path8.join(resolvedRoot, "graphify-out");
2600
+ const cache = new ExtractionCache(outDir);
2422
2601
  progress("Extracting...");
2423
2602
  const extractions = [];
2603
+ let cacheHits = 0;
2424
2604
  for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
2425
- const filePath = path6.relative(process.cwd(), path6.join(resolvedRoot, relFile)) || relFile;
2605
+ const filePath = path8.relative(process.cwd(), path8.join(resolvedRoot, relFile)) || relFile;
2426
2606
  try {
2427
- const extraction = await extract(filePath);
2607
+ const key = cache.key(relFile, await fs12.readFile(path8.join(resolvedRoot, relFile)));
2608
+ let extraction = options.update ? await cache.get(key) : null;
2609
+ if (extraction) {
2610
+ cacheHits++;
2611
+ } else {
2612
+ extraction = await extract(filePath, relFile);
2613
+ await cache.put(key, extraction);
2614
+ }
2428
2615
  extractions.push(extraction);
2429
2616
  } catch (error) {
2430
2617
  progress(` extraction failed for ${relFile}: ${error.message}`);
2431
2618
  throw error;
2432
2619
  }
2433
2620
  }
2621
+ if (options.update) {
2622
+ progress(` cache: ${cacheHits} hit(s), ${extractions.length - cacheHits} re-extracted.`);
2623
+ }
2624
+ if (options.extraExtractions && options.extraExtractions.length > 0) {
2625
+ extractions.push(...options.extraExtractions);
2626
+ }
2627
+ progress("Resolving cross-file references...");
2628
+ const resolveStats = resolveCrossFileReferences(extractions, {
2629
+ selfNames: await readSelfNames(resolvedRoot)
2630
+ });
2631
+ if (resolveStats.resolvedEndpoints > 0) {
2632
+ progress(
2633
+ ` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
2634
+ );
2635
+ }
2434
2636
  progress("Building graph...");
2435
2637
  const graph = buildGraph(extractions);
2436
2638
  progress(`Clustering (${options.algorithm ?? "louvain"})...`);
@@ -2439,7 +2641,6 @@ async function runPipeline(root, options = {}) {
2439
2641
  const analysis = analyze(graph);
2440
2642
  progress("Rendering report...");
2441
2643
  const report = renderReport(graph, analysis);
2442
- const outDir = options.outDir ?? path6.join(resolvedRoot, "graphify-out");
2443
2644
  progress(`Exporting to ${outDir} ...`);
2444
2645
  await exportGraph(
2445
2646
  graph,
@@ -2457,6 +2658,367 @@ async function runPipeline(root, options = {}) {
2457
2658
  return { manifest, graph, analysis, report };
2458
2659
  }
2459
2660
 
2661
+ // src/merge.ts
2662
+ import Graph3 from "graphology";
2663
+ var CONFIDENCE_RANK2 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
2664
+ function strongerConfidence2(a, b) {
2665
+ return CONFIDENCE_RANK2[a] >= CONFIDENCE_RANK2[b] ? a : b;
2666
+ }
2667
+ function isShared(id) {
2668
+ return id.startsWith("external:") || id.startsWith("module:");
2669
+ }
2670
+ function namespaced(project, id) {
2671
+ return isShared(id) ? id : `${project}/${id}`;
2672
+ }
2673
+ function mergeGraphs(entries) {
2674
+ const names = entries.map((e) => e.name);
2675
+ const duplicate = names.find((name, i) => names.indexOf(name) !== i);
2676
+ if (duplicate !== void 0) {
2677
+ throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
2678
+ }
2679
+ const merged = new Graph3({ type: "directed", multi: true, allowSelfLoops: true });
2680
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
2681
+ for (const { name, graph } of sorted) {
2682
+ const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
2683
+ for (const id of nodeIds) {
2684
+ const attrs = graph.getNodeAttributes(id);
2685
+ const newId = namespaced(name, id);
2686
+ if (merged.hasNode(newId)) {
2687
+ merged.setNodeAttribute(newId, "project", "(shared)");
2688
+ continue;
2689
+ }
2690
+ const sourceFile = attrs.sourceFile ?? "";
2691
+ merged.addNode(newId, {
2692
+ label: attrs.label ?? id,
2693
+ sourceFile: isShared(id) || sourceFile === "" ? sourceFile : `${name}/${sourceFile}`,
2694
+ sourceLocation: attrs.sourceLocation ?? "",
2695
+ project: name
2696
+ });
2697
+ }
2698
+ const edgeKeys = [...graph.edges()].sort((a, b) => a.localeCompare(b));
2699
+ for (const edgeKey of edgeKeys) {
2700
+ const attrs = graph.getEdgeAttributes(edgeKey);
2701
+ const source = namespaced(name, graph.source(edgeKey));
2702
+ const target = namespaced(name, graph.target(edgeKey));
2703
+ const relation = String(attrs.relation);
2704
+ const confidence = attrs.confidence;
2705
+ const key = `${source}|${relation}|${target}`;
2706
+ if (merged.hasEdge(key)) {
2707
+ const existing = merged.getEdgeAttribute(key, "confidence");
2708
+ merged.setEdgeAttribute(key, "confidence", strongerConfidence2(existing, confidence));
2709
+ continue;
2710
+ }
2711
+ merged.addEdgeWithKey(key, source, target, { relation, confidence });
2712
+ }
2713
+ }
2714
+ return merged;
2715
+ }
2716
+
2717
+ // src/memory.ts
2718
+ import { createHash as createHash3 } from "crypto";
2719
+ import * as fs13 from "fs/promises";
2720
+ import * as path9 from "path";
2721
+ var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
2722
+ var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
2723
+ function validateResult(value) {
2724
+ if (!value.question) throw new Error("save-result requires --question");
2725
+ if (!value.answer) throw new Error("save-result requires --answer");
2726
+ if (!OUTCOMES.has(value.outcome)) {
2727
+ throw new Error(`--outcome must be one of: useful, dead_end, corrected (got "${value.outcome}")`);
2728
+ }
2729
+ if (!TYPES.has(value.type)) {
2730
+ throw new Error(`--type must be one of: query, path, explain, affected (got "${value.type}")`);
2731
+ }
2732
+ if (value.outcome === "corrected" && !value.correction) {
2733
+ throw new Error("--outcome corrected requires --correction with what the right answer was");
2734
+ }
2735
+ }
2736
+ async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
2737
+ validateResult(entry);
2738
+ await fs13.mkdir(memoryDir, { recursive: true });
2739
+ const saved = { ...entry, savedAt: now.toISOString() };
2740
+ const hash = createHash3("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
2741
+ const stamp = saved.savedAt.replace(/[:.]/g, "-");
2742
+ const filePath = path9.join(memoryDir, `result-${stamp}-${hash}.json`);
2743
+ await fs13.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
2744
+ return filePath;
2745
+ }
2746
+ async function loadResults(memoryDir) {
2747
+ let files;
2748
+ try {
2749
+ files = (await fs13.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
2750
+ } catch {
2751
+ return [];
2752
+ }
2753
+ const results = [];
2754
+ for (const file of files.sort((a, b) => a.localeCompare(b))) {
2755
+ try {
2756
+ const parsed = JSON.parse(await fs13.readFile(path9.join(memoryDir, file), "utf-8"));
2757
+ if (parsed.question && parsed.savedAt) results.push(parsed);
2758
+ } catch {
2759
+ }
2760
+ }
2761
+ return results;
2762
+ }
2763
+ function renderLessons(results, options = {}) {
2764
+ const halfLife = options.halfLifeDays ?? 30;
2765
+ const now = options.now ?? /* @__PURE__ */ new Date();
2766
+ const minCorroboration = options.minCorroboration ?? 2;
2767
+ const weightOf = (savedAt) => {
2768
+ const ageDays = Math.max(0, (now.getTime() - new Date(savedAt).getTime()) / 864e5);
2769
+ return Math.pow(0.5, ageDays / halfLife);
2770
+ };
2771
+ const signals = /* @__PURE__ */ new Map();
2772
+ const corrections = [];
2773
+ for (const result of results) {
2774
+ const weight = weightOf(result.savedAt);
2775
+ for (const node of result.nodes ?? []) {
2776
+ const signal = signals.get(node) ?? { useful: 0, deadEnd: 0, usefulCount: 0, deadEndCount: 0 };
2777
+ if (result.outcome === "useful") {
2778
+ signal.useful += weight;
2779
+ signal.usefulCount++;
2780
+ } else if (result.outcome === "dead_end") {
2781
+ signal.deadEnd += weight;
2782
+ signal.deadEndCount++;
2783
+ }
2784
+ signals.set(node, signal);
2785
+ }
2786
+ if (result.outcome === "corrected" && result.correction) {
2787
+ corrections.push({ question: result.question, correction: result.correction, savedAt: result.savedAt });
2788
+ }
2789
+ }
2790
+ 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);
2791
+ const lines = [
2792
+ "# Graph lessons",
2793
+ "",
2794
+ `_Aggregated from ${results.length} saved result(s); signal half-life ${halfLife} day(s)._`,
2795
+ "",
2796
+ "## Nodes that keep answering",
2797
+ ""
2798
+ ];
2799
+ const useful = byScore("useful");
2800
+ if (useful.length === 0) {
2801
+ lines.push(`(none yet with >= ${minCorroboration} corroborating results \u2014 keep saving results)`);
2802
+ } else {
2803
+ for (const [node, s] of useful) {
2804
+ lines.push(`- **${node}** \u2014 score ${s.useful.toFixed(2)} across ${s.usefulCount} useful result(s)`);
2805
+ }
2806
+ }
2807
+ lines.push("", "## Dead ends", "");
2808
+ const deadEnds = byScore("deadEnd");
2809
+ if (deadEnds.length === 0) {
2810
+ lines.push("(none recorded)");
2811
+ } else {
2812
+ for (const [node, s] of deadEnds) {
2813
+ lines.push(`- **${node}** \u2014 score ${s.deadEnd.toFixed(2)} across ${s.deadEndCount} dead-end result(s)`);
2814
+ }
2815
+ }
2816
+ lines.push("", "## Corrections", "");
2817
+ if (corrections.length === 0) {
2818
+ lines.push("(none recorded)");
2819
+ } else {
2820
+ corrections.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
2821
+ for (const c of corrections.slice(0, 20)) {
2822
+ lines.push(`- (${c.savedAt.slice(0, 10)}) Q: ${c.question}`);
2823
+ lines.push(` Right answer: ${c.correction}`);
2824
+ }
2825
+ }
2826
+ lines.push("");
2827
+ return lines.join("\n");
2828
+ }
2829
+
2830
+ // src/diff.ts
2831
+ import { execFile } from "child_process";
2832
+ import * as fs14 from "fs/promises";
2833
+ import * as os from "os";
2834
+ import * as path10 from "path";
2835
+ import { promisify } from "util";
2836
+ var execFileAsync = promisify(execFile);
2837
+ async function graphForDirectory(root) {
2838
+ const manifest = collectFiles(root);
2839
+ const extractions = [];
2840
+ for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
2841
+ extractions.push(await extract(path10.join(root, relFile), relFile));
2842
+ }
2843
+ resolveCrossFileReferences(extractions, { selfNames: await readSelfNames(root) });
2844
+ return buildGraph(extractions);
2845
+ }
2846
+ async function checkoutRev(repoRoot, rev) {
2847
+ const dest = await fs14.mkdtemp(path10.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
2848
+ const { stdout } = await execFileAsync(
2849
+ "git",
2850
+ ["-C", repoRoot, "archive", "--format=tar", rev],
2851
+ { encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }
2852
+ );
2853
+ await new Promise((resolve4, reject) => {
2854
+ const tar = execFile("tar", ["-x", "-C", dest], (error) => error ? reject(error) : resolve4());
2855
+ tar.stdin?.end(stdout);
2856
+ });
2857
+ return dest;
2858
+ }
2859
+ function edgeSignature(graph, id) {
2860
+ const signature = /* @__PURE__ */ new Set();
2861
+ graph.forEachOutEdge(id, (_e, attrs, _s, target) => signature.add(`-> ${String(attrs.relation)} ${target}`));
2862
+ graph.forEachInEdge(id, (_e, attrs, source) => signature.add(`<- ${String(attrs.relation)} ${source}`));
2863
+ return signature;
2864
+ }
2865
+ function describeSymbol(graph, id) {
2866
+ const attrs = graph.getNodeAttributes(id);
2867
+ const impact = affectedBy(graph, id, { maxDepth: 3 });
2868
+ const tests = testsForNode(graph, id);
2869
+ return {
2870
+ id,
2871
+ label: attrs.label ?? id,
2872
+ sourceFile: attrs.sourceFile ?? "",
2873
+ blastRadius: impact?.affected.length ?? 0,
2874
+ tests: tests?.testFiles.map((t) => t.file) ?? []
2875
+ };
2876
+ }
2877
+ function entityIds(graph) {
2878
+ const ids = /* @__PURE__ */ new Set();
2879
+ graph.forEachNode((id) => {
2880
+ if (id.includes("::")) ids.add(id);
2881
+ });
2882
+ return ids;
2883
+ }
2884
+ function diffGraphs(baseGraph, headGraph, base, head) {
2885
+ const baseIds = entityIds(baseGraph);
2886
+ const headIds = entityIds(headGraph);
2887
+ const added = [];
2888
+ const removed = [];
2889
+ const rewired = [];
2890
+ for (const id of [...headIds].sort((a, b) => a.localeCompare(b))) {
2891
+ if (!baseIds.has(id)) {
2892
+ added.push(describeSymbol(headGraph, id));
2893
+ continue;
2894
+ }
2895
+ const before = edgeSignature(baseGraph, id);
2896
+ const after = edgeSignature(headGraph, id);
2897
+ const gained = [...after].filter((e) => !before.has(e)).sort((a, b) => a.localeCompare(b));
2898
+ const lost = [...before].filter((e) => !after.has(e)).sort((a, b) => a.localeCompare(b));
2899
+ if (gained.length > 0 || lost.length > 0) {
2900
+ rewired.push({ ...describeSymbol(headGraph, id), gainedEdges: gained, lostEdges: lost });
2901
+ }
2902
+ }
2903
+ for (const id of [...baseIds].sort((a, b) => a.localeCompare(b))) {
2904
+ if (!headIds.has(id)) removed.push(describeSymbol(baseGraph, id));
2905
+ }
2906
+ return { base, head, added, removed, rewired };
2907
+ }
2908
+ async function reviewRevisions(repoRoot, base, head = "HEAD") {
2909
+ const [baseDir, headDir] = await Promise.all([checkoutRev(repoRoot, base), checkoutRev(repoRoot, head)]);
2910
+ try {
2911
+ const [baseGraph, headGraph] = [await graphForDirectory(baseDir), await graphForDirectory(headDir)];
2912
+ return diffGraphs(baseGraph, headGraph, base, head);
2913
+ } finally {
2914
+ await Promise.all([
2915
+ fs14.rm(baseDir, { recursive: true, force: true }),
2916
+ fs14.rm(headDir, { recursive: true, force: true })
2917
+ ]);
2918
+ }
2919
+ }
2920
+ function renderReview(diff) {
2921
+ const lines = [
2922
+ `# Structural review: ${diff.base}..${diff.head}`,
2923
+ "",
2924
+ `${diff.added.length} symbol(s) added, ${diff.removed.length} removed, ${diff.rewired.length} rewired.`,
2925
+ ""
2926
+ ];
2927
+ const section = (title, symbols) => {
2928
+ lines.push(`## ${title} (${symbols.length})`, "");
2929
+ if (symbols.length === 0) {
2930
+ lines.push("(none)", "");
2931
+ return;
2932
+ }
2933
+ for (const s of symbols) {
2934
+ const tests = s.tests.length > 0 ? ` | tests: ${s.tests.join(", ")}` : " | tests: none found";
2935
+ lines.push(`- **${s.label}** (${s.sourceFile}) \u2014 blast radius ${s.blastRadius}${tests}`);
2936
+ const r = s;
2937
+ if (r.gainedEdges) {
2938
+ for (const e of r.gainedEdges.slice(0, 5)) lines.push(` - gained: ${e}`);
2939
+ for (const e of r.lostEdges.slice(0, 5)) lines.push(` - lost: ${e}`);
2940
+ }
2941
+ }
2942
+ lines.push("");
2943
+ };
2944
+ section("Removed \u2014 check every caller", diff.removed);
2945
+ section("Rewired \u2014 dependencies changed", diff.rewired);
2946
+ section("Added", diff.added);
2947
+ const allTests = /* @__PURE__ */ new Set();
2948
+ for (const s of [...diff.removed, ...diff.rewired, ...diff.added]) {
2949
+ for (const t of s.tests) allTests.add(t);
2950
+ }
2951
+ lines.push(`## Suggested test run (${allTests.size} file(s))`, "");
2952
+ for (const t of [...allTests].sort((a, b) => a.localeCompare(b))) lines.push(`- ${t}`);
2953
+ lines.push("");
2954
+ return lines.join("\n");
2955
+ }
2956
+
2957
+ // src/check.ts
2958
+ var DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
2959
+ "calls",
2960
+ "imports",
2961
+ "imports_from",
2962
+ "inherits",
2963
+ "implements",
2964
+ "mixes_in",
2965
+ "embeds",
2966
+ "re_exports"
2967
+ ]);
2968
+ function globToRegExp(glob) {
2969
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2970
+ const pattern = escaped.replace(
2971
+ /\*\*|\*|\?/g,
2972
+ (token) => token === "**" ? ".*" : token === "*" ? "[^/]*" : "[^/]"
2973
+ );
2974
+ return new RegExp(`^${pattern}$`);
2975
+ }
2976
+ function validateRules(value) {
2977
+ const config = value;
2978
+ if (!config || !Array.isArray(config.rules)) {
2979
+ throw new Error('Rules file must be JSON of shape { "rules": [{ "name", "from", "disallow": [...] }] }');
2980
+ }
2981
+ for (const rule of config.rules) {
2982
+ if (!rule.name || typeof rule.from !== "string" || !Array.isArray(rule.disallow)) {
2983
+ throw new Error(`Malformed rule "${rule.name ?? "(unnamed)"}" \u2014 need name, from, and disallow[].`);
2984
+ }
2985
+ }
2986
+ return config;
2987
+ }
2988
+ function checkRules(graph, config) {
2989
+ const compiled = config.rules.map((rule) => ({
2990
+ rule,
2991
+ from: globToRegExp(rule.from),
2992
+ disallow: rule.disallow.map(globToRegExp)
2993
+ }));
2994
+ const violations = [];
2995
+ graph.forEachEdge((_edge, attrs, source, target) => {
2996
+ const relation = String(attrs.relation);
2997
+ if (!DEPENDENCY_RELATIONS.has(relation)) return;
2998
+ const fromFile = graph.getNodeAttribute(source, "sourceFile") ?? "";
2999
+ const toFile = graph.getNodeAttribute(target, "sourceFile") ?? "";
3000
+ if (!fromFile || !toFile || fromFile === toFile) return;
3001
+ for (const { rule, from, disallow } of compiled) {
3002
+ if (!from.test(fromFile)) continue;
3003
+ if (disallow.some((d) => d.test(toFile))) {
3004
+ violations.push({
3005
+ rule: rule.name,
3006
+ reason: rule.reason,
3007
+ fromFile,
3008
+ fromNode: source,
3009
+ toFile,
3010
+ toNode: target,
3011
+ relation
3012
+ });
3013
+ }
3014
+ }
3015
+ });
3016
+ violations.sort(
3017
+ (a, b) => a.rule.localeCompare(b.rule) || a.fromFile.localeCompare(b.fromFile) || a.toFile.localeCompare(b.toFile) || a.fromNode.localeCompare(b.fromNode)
3018
+ );
3019
+ return violations;
3020
+ }
3021
+
2460
3022
  export {
2461
3023
  collectFiles,
2462
3024
  EXTRACTOR_REGISTRY,
@@ -2465,10 +3027,23 @@ export {
2465
3027
  ExtractionValidationError,
2466
3028
  validateExtraction,
2467
3029
  buildGraph,
3030
+ resolveCrossFileReferences,
2468
3031
  cluster,
2469
3032
  analyze,
2470
3033
  renderReport,
2471
3034
  exportGraph,
2472
- runPipeline
3035
+ ExtractionCache,
3036
+ runPipeline,
3037
+ mergeGraphs,
3038
+ saveResult,
3039
+ loadResults,
3040
+ renderLessons,
3041
+ graphForDirectory,
3042
+ diffGraphs,
3043
+ reviewRevisions,
3044
+ renderReview,
3045
+ globToRegExp,
3046
+ validateRules,
3047
+ checkRules
2473
3048
  };
2474
- //# sourceMappingURL=chunk-DG5FECXV.js.map
3049
+ //# sourceMappingURL=chunk-PXVI2L45.js.map