@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.
@@ -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 path10 = fn.childForFieldName("path")?.text;
1535
1539
  const name = fn.childForFieldName("name")?.text;
1536
- if (path7 && name) {
1537
- const viaImport = importIndex.get(path7);
1540
+ if (path10 && name) {
1541
+ const viaImport = importIndex.get(path10);
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,107 @@ function buildGraph(extractions) {
2012
2016
  return graph;
2013
2017
  }
2014
2018
 
2019
+ // src/resolve.ts
2020
+ var CODE_EXTENSIONS2 = [
2021
+ ".ts",
2022
+ ".tsx",
2023
+ ".mts",
2024
+ ".cts",
2025
+ ".js",
2026
+ ".jsx",
2027
+ ".mjs",
2028
+ ".cjs",
2029
+ ".py",
2030
+ ".go",
2031
+ ".rs",
2032
+ ".java",
2033
+ ".cs",
2034
+ ".rb"
2035
+ ];
2036
+ function isOpaque(id) {
2037
+ return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
2038
+ }
2039
+ function stripKnownExtension(p) {
2040
+ for (const ext of CODE_EXTENSIONS2) {
2041
+ if (p.endsWith(ext)) return p.slice(0, -ext.length);
2042
+ }
2043
+ return null;
2044
+ }
2045
+ function candidatePaths(guessed) {
2046
+ const stem = stripKnownExtension(guessed) ?? guessed;
2047
+ const candidates = [];
2048
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}${ext}`);
2049
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${guessed}/index${ext}`);
2050
+ if (stem !== guessed) {
2051
+ for (const ext of CODE_EXTENSIONS2) candidates.push(`${stem}/index${ext}`);
2052
+ }
2053
+ return candidates.filter((c) => c !== guessed);
2054
+ }
2055
+ function uniqueMatch(guessed, candidates, realIds) {
2056
+ const matches = /* @__PURE__ */ new Set();
2057
+ for (const candidate of candidates) {
2058
+ if (realIds.has(candidate)) matches.add(candidate);
2059
+ }
2060
+ if (matches.size !== 1) return null;
2061
+ return [...matches][0];
2062
+ }
2063
+ function resolveCrossFileReferences(extractions) {
2064
+ const realIds = /* @__PURE__ */ new Set();
2065
+ const realFiles = /* @__PURE__ */ new Set();
2066
+ for (const extraction of extractions) {
2067
+ for (const node of extraction.nodes) realIds.add(node.id);
2068
+ for (const node of extraction.nodes) {
2069
+ const sep2 = node.id.indexOf("::");
2070
+ if (sep2 > 0) realFiles.add(node.id.slice(0, sep2));
2071
+ }
2072
+ for (const edge of extraction.edges) {
2073
+ if (edge.relation === "contains") realFiles.add(edge.source);
2074
+ }
2075
+ }
2076
+ const remap = /* @__PURE__ */ new Map();
2077
+ const resolveId = (id) => {
2078
+ if (isOpaque(id)) return null;
2079
+ const cached = remap.get(id);
2080
+ if (cached !== void 0) return cached;
2081
+ const sep2 = id.indexOf("::");
2082
+ let resolved;
2083
+ if (sep2 > 0) {
2084
+ if (realIds.has(id)) return null;
2085
+ const guessedPath = id.slice(0, sep2);
2086
+ const name = id.slice(sep2);
2087
+ const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
2088
+ resolved = uniqueMatch(id, candidates, realIds);
2089
+ } else {
2090
+ if (realFiles.has(id)) return null;
2091
+ resolved = uniqueMatch(id, candidatePaths(id), realFiles);
2092
+ }
2093
+ if (resolved !== null) remap.set(id, resolved);
2094
+ return resolved;
2095
+ };
2096
+ let resolvedEndpoints = 0;
2097
+ for (const extraction of extractions) {
2098
+ for (const edge of extraction.edges) {
2099
+ const source = resolveId(edge.source);
2100
+ if (source !== null) {
2101
+ edge.source = source;
2102
+ resolvedEndpoints++;
2103
+ }
2104
+ const target = resolveId(edge.target);
2105
+ if (target !== null) {
2106
+ edge.target = target;
2107
+ resolvedEndpoints++;
2108
+ }
2109
+ }
2110
+ }
2111
+ let droppedPlaceholders = 0;
2112
+ for (const extraction of extractions) {
2113
+ const before = extraction.nodes.length;
2114
+ extraction.nodes = extraction.nodes.filter((node) => !remap.has(node.id));
2115
+ droppedPlaceholders += before - extraction.nodes.length;
2116
+ }
2117
+ return { resolvedEndpoints, droppedPlaceholders };
2118
+ }
2119
+
2015
2120
  // src/cluster.ts
2016
2121
  import { createHash } from "crypto";
2017
2122
  import Graph2 from "graphology";
@@ -2408,29 +2513,81 @@ async function exportGraph(graph, options, report) {
2408
2513
  }
2409
2514
  }
2410
2515
 
2411
- // src/pipeline.ts
2516
+ // src/extractionCache.ts
2517
+ import { createHash as createHash2 } from "crypto";
2518
+ import * as fs10 from "fs/promises";
2412
2519
  import * as path6 from "path";
2520
+ var ExtractionCache = class {
2521
+ dir;
2522
+ constructor(outDir) {
2523
+ this.dir = path6.join(outDir, "cache");
2524
+ }
2525
+ key(relFile, content) {
2526
+ return createHash2("sha256").update(relFile).update("\0").update(content).digest("hex");
2527
+ }
2528
+ async get(key) {
2529
+ try {
2530
+ const raw = await fs10.readFile(path6.join(this.dir, `${key}.json`), "utf-8");
2531
+ const parsed = JSON.parse(raw);
2532
+ if (!Array.isArray(parsed.nodes) || !Array.isArray(parsed.edges)) return null;
2533
+ return parsed;
2534
+ } catch {
2535
+ return null;
2536
+ }
2537
+ }
2538
+ async put(key, extraction) {
2539
+ await fs10.mkdir(this.dir, { recursive: true });
2540
+ await fs10.writeFile(path6.join(this.dir, `${key}.json`), JSON.stringify(extraction), "utf-8");
2541
+ }
2542
+ };
2543
+
2544
+ // src/pipeline.ts
2545
+ import * as fs11 from "fs/promises";
2546
+ import * as path7 from "path";
2413
2547
  async function runPipeline(root, options = {}) {
2414
2548
  const progress = options.onProgress ?? (() => {
2415
2549
  });
2416
- const resolvedRoot = path6.resolve(root);
2550
+ const resolvedRoot = path7.resolve(root);
2417
2551
  progress(`Scanning ${resolvedRoot} ...`);
2418
2552
  const manifest = collectFiles(resolvedRoot);
2419
2553
  progress(
2420
2554
  `Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
2421
2555
  );
2556
+ const outDir = options.outDir ?? path7.join(resolvedRoot, "graphify-out");
2557
+ const cache = new ExtractionCache(outDir);
2422
2558
  progress("Extracting...");
2423
2559
  const extractions = [];
2560
+ let cacheHits = 0;
2424
2561
  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;
2562
+ const filePath = path7.relative(process.cwd(), path7.join(resolvedRoot, relFile)) || relFile;
2426
2563
  try {
2427
- const extraction = await extract(filePath);
2564
+ const key = cache.key(relFile, await fs11.readFile(path7.join(resolvedRoot, relFile)));
2565
+ let extraction = options.update ? await cache.get(key) : null;
2566
+ if (extraction) {
2567
+ cacheHits++;
2568
+ } else {
2569
+ extraction = await extract(filePath, relFile);
2570
+ await cache.put(key, extraction);
2571
+ }
2428
2572
  extractions.push(extraction);
2429
2573
  } catch (error) {
2430
2574
  progress(` extraction failed for ${relFile}: ${error.message}`);
2431
2575
  throw error;
2432
2576
  }
2433
2577
  }
2578
+ if (options.update) {
2579
+ progress(` cache: ${cacheHits} hit(s), ${extractions.length - cacheHits} re-extracted.`);
2580
+ }
2581
+ if (options.extraExtractions && options.extraExtractions.length > 0) {
2582
+ extractions.push(...options.extraExtractions);
2583
+ }
2584
+ progress("Resolving cross-file references...");
2585
+ const resolveStats = resolveCrossFileReferences(extractions);
2586
+ if (resolveStats.resolvedEndpoints > 0) {
2587
+ progress(
2588
+ ` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
2589
+ );
2590
+ }
2434
2591
  progress("Building graph...");
2435
2592
  const graph = buildGraph(extractions);
2436
2593
  progress(`Clustering (${options.algorithm ?? "louvain"})...`);
@@ -2439,7 +2596,6 @@ async function runPipeline(root, options = {}) {
2439
2596
  const analysis = analyze(graph);
2440
2597
  progress("Rendering report...");
2441
2598
  const report = renderReport(graph, analysis);
2442
- const outDir = options.outDir ?? path6.join(resolvedRoot, "graphify-out");
2443
2599
  progress(`Exporting to ${outDir} ...`);
2444
2600
  await exportGraph(
2445
2601
  graph,
@@ -2457,6 +2613,367 @@ async function runPipeline(root, options = {}) {
2457
2613
  return { manifest, graph, analysis, report };
2458
2614
  }
2459
2615
 
2616
+ // src/merge.ts
2617
+ import Graph3 from "graphology";
2618
+ var CONFIDENCE_RANK2 = { EXTRACTED: 3, INFERRED: 2, AMBIGUOUS: 1 };
2619
+ function strongerConfidence2(a, b) {
2620
+ return CONFIDENCE_RANK2[a] >= CONFIDENCE_RANK2[b] ? a : b;
2621
+ }
2622
+ function isShared(id) {
2623
+ return id.startsWith("external:") || id.startsWith("module:");
2624
+ }
2625
+ function namespaced(project, id) {
2626
+ return isShared(id) ? id : `${project}/${id}`;
2627
+ }
2628
+ function mergeGraphs(entries) {
2629
+ const names = entries.map((e) => e.name);
2630
+ const duplicate = names.find((name, i) => names.indexOf(name) !== i);
2631
+ if (duplicate !== void 0) {
2632
+ throw new Error(`Duplicate project name in merge: "${duplicate}" \u2014 every entry needs a unique name.`);
2633
+ }
2634
+ const merged = new Graph3({ type: "directed", multi: true, allowSelfLoops: true });
2635
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
2636
+ for (const { name, graph } of sorted) {
2637
+ const nodeIds = [...graph.nodes()].sort((a, b) => a.localeCompare(b));
2638
+ for (const id of nodeIds) {
2639
+ const attrs = graph.getNodeAttributes(id);
2640
+ const newId = namespaced(name, id);
2641
+ if (merged.hasNode(newId)) {
2642
+ merged.setNodeAttribute(newId, "project", "(shared)");
2643
+ continue;
2644
+ }
2645
+ const sourceFile = attrs.sourceFile ?? "";
2646
+ merged.addNode(newId, {
2647
+ label: attrs.label ?? id,
2648
+ sourceFile: isShared(id) || sourceFile === "" ? sourceFile : `${name}/${sourceFile}`,
2649
+ sourceLocation: attrs.sourceLocation ?? "",
2650
+ project: name
2651
+ });
2652
+ }
2653
+ const edgeKeys = [...graph.edges()].sort((a, b) => a.localeCompare(b));
2654
+ for (const edgeKey of edgeKeys) {
2655
+ const attrs = graph.getEdgeAttributes(edgeKey);
2656
+ const source = namespaced(name, graph.source(edgeKey));
2657
+ const target = namespaced(name, graph.target(edgeKey));
2658
+ const relation = String(attrs.relation);
2659
+ const confidence = attrs.confidence;
2660
+ const key = `${source}|${relation}|${target}`;
2661
+ if (merged.hasEdge(key)) {
2662
+ const existing = merged.getEdgeAttribute(key, "confidence");
2663
+ merged.setEdgeAttribute(key, "confidence", strongerConfidence2(existing, confidence));
2664
+ continue;
2665
+ }
2666
+ merged.addEdgeWithKey(key, source, target, { relation, confidence });
2667
+ }
2668
+ }
2669
+ return merged;
2670
+ }
2671
+
2672
+ // src/memory.ts
2673
+ import { createHash as createHash3 } from "crypto";
2674
+ import * as fs12 from "fs/promises";
2675
+ import * as path8 from "path";
2676
+ var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
2677
+ var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
2678
+ function validateResult(value) {
2679
+ if (!value.question) throw new Error("save-result requires --question");
2680
+ if (!value.answer) throw new Error("save-result requires --answer");
2681
+ if (!OUTCOMES.has(value.outcome)) {
2682
+ throw new Error(`--outcome must be one of: useful, dead_end, corrected (got "${value.outcome}")`);
2683
+ }
2684
+ if (!TYPES.has(value.type)) {
2685
+ throw new Error(`--type must be one of: query, path, explain, affected (got "${value.type}")`);
2686
+ }
2687
+ if (value.outcome === "corrected" && !value.correction) {
2688
+ throw new Error("--outcome corrected requires --correction with what the right answer was");
2689
+ }
2690
+ }
2691
+ async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
2692
+ validateResult(entry);
2693
+ await fs12.mkdir(memoryDir, { recursive: true });
2694
+ const saved = { ...entry, savedAt: now.toISOString() };
2695
+ const hash = createHash3("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
2696
+ const stamp = saved.savedAt.replace(/[:.]/g, "-");
2697
+ const filePath = path8.join(memoryDir, `result-${stamp}-${hash}.json`);
2698
+ await fs12.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
2699
+ return filePath;
2700
+ }
2701
+ async function loadResults(memoryDir) {
2702
+ let files;
2703
+ try {
2704
+ files = (await fs12.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
2705
+ } catch {
2706
+ return [];
2707
+ }
2708
+ const results = [];
2709
+ for (const file of files.sort((a, b) => a.localeCompare(b))) {
2710
+ try {
2711
+ const parsed = JSON.parse(await fs12.readFile(path8.join(memoryDir, file), "utf-8"));
2712
+ if (parsed.question && parsed.savedAt) results.push(parsed);
2713
+ } catch {
2714
+ }
2715
+ }
2716
+ return results;
2717
+ }
2718
+ function renderLessons(results, options = {}) {
2719
+ const halfLife = options.halfLifeDays ?? 30;
2720
+ const now = options.now ?? /* @__PURE__ */ new Date();
2721
+ const minCorroboration = options.minCorroboration ?? 2;
2722
+ const weightOf = (savedAt) => {
2723
+ const ageDays = Math.max(0, (now.getTime() - new Date(savedAt).getTime()) / 864e5);
2724
+ return Math.pow(0.5, ageDays / halfLife);
2725
+ };
2726
+ const signals = /* @__PURE__ */ new Map();
2727
+ const corrections = [];
2728
+ for (const result of results) {
2729
+ const weight = weightOf(result.savedAt);
2730
+ for (const node of result.nodes ?? []) {
2731
+ const signal = signals.get(node) ?? { useful: 0, deadEnd: 0, usefulCount: 0, deadEndCount: 0 };
2732
+ if (result.outcome === "useful") {
2733
+ signal.useful += weight;
2734
+ signal.usefulCount++;
2735
+ } else if (result.outcome === "dead_end") {
2736
+ signal.deadEnd += weight;
2737
+ signal.deadEndCount++;
2738
+ }
2739
+ signals.set(node, signal);
2740
+ }
2741
+ if (result.outcome === "corrected" && result.correction) {
2742
+ corrections.push({ question: result.question, correction: result.correction, savedAt: result.savedAt });
2743
+ }
2744
+ }
2745
+ 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);
2746
+ const lines = [
2747
+ "# Graph lessons",
2748
+ "",
2749
+ `_Aggregated from ${results.length} saved result(s); signal half-life ${halfLife} day(s)._`,
2750
+ "",
2751
+ "## Nodes that keep answering",
2752
+ ""
2753
+ ];
2754
+ const useful = byScore("useful");
2755
+ if (useful.length === 0) {
2756
+ lines.push(`(none yet with >= ${minCorroboration} corroborating results \u2014 keep saving results)`);
2757
+ } else {
2758
+ for (const [node, s] of useful) {
2759
+ lines.push(`- **${node}** \u2014 score ${s.useful.toFixed(2)} across ${s.usefulCount} useful result(s)`);
2760
+ }
2761
+ }
2762
+ lines.push("", "## Dead ends", "");
2763
+ const deadEnds = byScore("deadEnd");
2764
+ if (deadEnds.length === 0) {
2765
+ lines.push("(none recorded)");
2766
+ } else {
2767
+ for (const [node, s] of deadEnds) {
2768
+ lines.push(`- **${node}** \u2014 score ${s.deadEnd.toFixed(2)} across ${s.deadEndCount} dead-end result(s)`);
2769
+ }
2770
+ }
2771
+ lines.push("", "## Corrections", "");
2772
+ if (corrections.length === 0) {
2773
+ lines.push("(none recorded)");
2774
+ } else {
2775
+ corrections.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
2776
+ for (const c of corrections.slice(0, 20)) {
2777
+ lines.push(`- (${c.savedAt.slice(0, 10)}) Q: ${c.question}`);
2778
+ lines.push(` Right answer: ${c.correction}`);
2779
+ }
2780
+ }
2781
+ lines.push("");
2782
+ return lines.join("\n");
2783
+ }
2784
+
2785
+ // src/diff.ts
2786
+ import { execFile } from "child_process";
2787
+ import * as fs13 from "fs/promises";
2788
+ import * as os from "os";
2789
+ import * as path9 from "path";
2790
+ import { promisify } from "util";
2791
+ var execFileAsync = promisify(execFile);
2792
+ async function graphForDirectory(root) {
2793
+ const manifest = collectFiles(root);
2794
+ const extractions = [];
2795
+ for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
2796
+ extractions.push(await extract(path9.join(root, relFile), relFile));
2797
+ }
2798
+ resolveCrossFileReferences(extractions);
2799
+ return buildGraph(extractions);
2800
+ }
2801
+ async function checkoutRev(repoRoot, rev) {
2802
+ const dest = await fs13.mkdtemp(path9.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
2803
+ const { stdout } = await execFileAsync(
2804
+ "git",
2805
+ ["-C", repoRoot, "archive", "--format=tar", rev],
2806
+ { encoding: "buffer", maxBuffer: 512 * 1024 * 1024 }
2807
+ );
2808
+ await new Promise((resolve4, reject) => {
2809
+ const tar = execFile("tar", ["-x", "-C", dest], (error) => error ? reject(error) : resolve4());
2810
+ tar.stdin?.end(stdout);
2811
+ });
2812
+ return dest;
2813
+ }
2814
+ function edgeSignature(graph, id) {
2815
+ const signature = /* @__PURE__ */ new Set();
2816
+ graph.forEachOutEdge(id, (_e, attrs, _s, target) => signature.add(`-> ${String(attrs.relation)} ${target}`));
2817
+ graph.forEachInEdge(id, (_e, attrs, source) => signature.add(`<- ${String(attrs.relation)} ${source}`));
2818
+ return signature;
2819
+ }
2820
+ function describeSymbol(graph, id) {
2821
+ const attrs = graph.getNodeAttributes(id);
2822
+ const impact = affectedBy(graph, id, { maxDepth: 3 });
2823
+ const tests = testsForNode(graph, id);
2824
+ return {
2825
+ id,
2826
+ label: attrs.label ?? id,
2827
+ sourceFile: attrs.sourceFile ?? "",
2828
+ blastRadius: impact?.affected.length ?? 0,
2829
+ tests: tests?.testFiles.map((t) => t.file) ?? []
2830
+ };
2831
+ }
2832
+ function entityIds(graph) {
2833
+ const ids = /* @__PURE__ */ new Set();
2834
+ graph.forEachNode((id) => {
2835
+ if (id.includes("::")) ids.add(id);
2836
+ });
2837
+ return ids;
2838
+ }
2839
+ function diffGraphs(baseGraph, headGraph, base, head) {
2840
+ const baseIds = entityIds(baseGraph);
2841
+ const headIds = entityIds(headGraph);
2842
+ const added = [];
2843
+ const removed = [];
2844
+ const rewired = [];
2845
+ for (const id of [...headIds].sort((a, b) => a.localeCompare(b))) {
2846
+ if (!baseIds.has(id)) {
2847
+ added.push(describeSymbol(headGraph, id));
2848
+ continue;
2849
+ }
2850
+ const before = edgeSignature(baseGraph, id);
2851
+ const after = edgeSignature(headGraph, id);
2852
+ const gained = [...after].filter((e) => !before.has(e)).sort((a, b) => a.localeCompare(b));
2853
+ const lost = [...before].filter((e) => !after.has(e)).sort((a, b) => a.localeCompare(b));
2854
+ if (gained.length > 0 || lost.length > 0) {
2855
+ rewired.push({ ...describeSymbol(headGraph, id), gainedEdges: gained, lostEdges: lost });
2856
+ }
2857
+ }
2858
+ for (const id of [...baseIds].sort((a, b) => a.localeCompare(b))) {
2859
+ if (!headIds.has(id)) removed.push(describeSymbol(baseGraph, id));
2860
+ }
2861
+ return { base, head, added, removed, rewired };
2862
+ }
2863
+ async function reviewRevisions(repoRoot, base, head = "HEAD") {
2864
+ const [baseDir, headDir] = await Promise.all([checkoutRev(repoRoot, base), checkoutRev(repoRoot, head)]);
2865
+ try {
2866
+ const [baseGraph, headGraph] = [await graphForDirectory(baseDir), await graphForDirectory(headDir)];
2867
+ return diffGraphs(baseGraph, headGraph, base, head);
2868
+ } finally {
2869
+ await Promise.all([
2870
+ fs13.rm(baseDir, { recursive: true, force: true }),
2871
+ fs13.rm(headDir, { recursive: true, force: true })
2872
+ ]);
2873
+ }
2874
+ }
2875
+ function renderReview(diff) {
2876
+ const lines = [
2877
+ `# Structural review: ${diff.base}..${diff.head}`,
2878
+ "",
2879
+ `${diff.added.length} symbol(s) added, ${diff.removed.length} removed, ${diff.rewired.length} rewired.`,
2880
+ ""
2881
+ ];
2882
+ const section = (title, symbols) => {
2883
+ lines.push(`## ${title} (${symbols.length})`, "");
2884
+ if (symbols.length === 0) {
2885
+ lines.push("(none)", "");
2886
+ return;
2887
+ }
2888
+ for (const s of symbols) {
2889
+ const tests = s.tests.length > 0 ? ` | tests: ${s.tests.join(", ")}` : " | tests: none found";
2890
+ lines.push(`- **${s.label}** (${s.sourceFile}) \u2014 blast radius ${s.blastRadius}${tests}`);
2891
+ const r = s;
2892
+ if (r.gainedEdges) {
2893
+ for (const e of r.gainedEdges.slice(0, 5)) lines.push(` - gained: ${e}`);
2894
+ for (const e of r.lostEdges.slice(0, 5)) lines.push(` - lost: ${e}`);
2895
+ }
2896
+ }
2897
+ lines.push("");
2898
+ };
2899
+ section("Removed \u2014 check every caller", diff.removed);
2900
+ section("Rewired \u2014 dependencies changed", diff.rewired);
2901
+ section("Added", diff.added);
2902
+ const allTests = /* @__PURE__ */ new Set();
2903
+ for (const s of [...diff.removed, ...diff.rewired, ...diff.added]) {
2904
+ for (const t of s.tests) allTests.add(t);
2905
+ }
2906
+ lines.push(`## Suggested test run (${allTests.size} file(s))`, "");
2907
+ for (const t of [...allTests].sort((a, b) => a.localeCompare(b))) lines.push(`- ${t}`);
2908
+ lines.push("");
2909
+ return lines.join("\n");
2910
+ }
2911
+
2912
+ // src/check.ts
2913
+ var DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
2914
+ "calls",
2915
+ "imports",
2916
+ "imports_from",
2917
+ "inherits",
2918
+ "implements",
2919
+ "mixes_in",
2920
+ "embeds",
2921
+ "re_exports"
2922
+ ]);
2923
+ function globToRegExp(glob) {
2924
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2925
+ const pattern = escaped.replace(
2926
+ /\*\*|\*|\?/g,
2927
+ (token) => token === "**" ? ".*" : token === "*" ? "[^/]*" : "[^/]"
2928
+ );
2929
+ return new RegExp(`^${pattern}$`);
2930
+ }
2931
+ function validateRules(value) {
2932
+ const config = value;
2933
+ if (!config || !Array.isArray(config.rules)) {
2934
+ throw new Error('Rules file must be JSON of shape { "rules": [{ "name", "from", "disallow": [...] }] }');
2935
+ }
2936
+ for (const rule of config.rules) {
2937
+ if (!rule.name || typeof rule.from !== "string" || !Array.isArray(rule.disallow)) {
2938
+ throw new Error(`Malformed rule "${rule.name ?? "(unnamed)"}" \u2014 need name, from, and disallow[].`);
2939
+ }
2940
+ }
2941
+ return config;
2942
+ }
2943
+ function checkRules(graph, config) {
2944
+ const compiled = config.rules.map((rule) => ({
2945
+ rule,
2946
+ from: globToRegExp(rule.from),
2947
+ disallow: rule.disallow.map(globToRegExp)
2948
+ }));
2949
+ const violations = [];
2950
+ graph.forEachEdge((_edge, attrs, source, target) => {
2951
+ const relation = String(attrs.relation);
2952
+ if (!DEPENDENCY_RELATIONS.has(relation)) return;
2953
+ const fromFile = graph.getNodeAttribute(source, "sourceFile") ?? "";
2954
+ const toFile = graph.getNodeAttribute(target, "sourceFile") ?? "";
2955
+ if (!fromFile || !toFile || fromFile === toFile) return;
2956
+ for (const { rule, from, disallow } of compiled) {
2957
+ if (!from.test(fromFile)) continue;
2958
+ if (disallow.some((d) => d.test(toFile))) {
2959
+ violations.push({
2960
+ rule: rule.name,
2961
+ reason: rule.reason,
2962
+ fromFile,
2963
+ fromNode: source,
2964
+ toFile,
2965
+ toNode: target,
2966
+ relation
2967
+ });
2968
+ }
2969
+ }
2970
+ });
2971
+ violations.sort(
2972
+ (a, b) => a.rule.localeCompare(b.rule) || a.fromFile.localeCompare(b.fromFile) || a.toFile.localeCompare(b.toFile) || a.fromNode.localeCompare(b.fromNode)
2973
+ );
2974
+ return violations;
2975
+ }
2976
+
2460
2977
  export {
2461
2978
  collectFiles,
2462
2979
  EXTRACTOR_REGISTRY,
@@ -2465,10 +2982,23 @@ export {
2465
2982
  ExtractionValidationError,
2466
2983
  validateExtraction,
2467
2984
  buildGraph,
2985
+ resolveCrossFileReferences,
2468
2986
  cluster,
2469
2987
  analyze,
2470
2988
  renderReport,
2471
2989
  exportGraph,
2472
- runPipeline
2990
+ ExtractionCache,
2991
+ runPipeline,
2992
+ mergeGraphs,
2993
+ saveResult,
2994
+ loadResults,
2995
+ renderLessons,
2996
+ graphForDirectory,
2997
+ diffGraphs,
2998
+ reviewRevisions,
2999
+ renderReview,
3000
+ globToRegExp,
3001
+ validateRules,
3002
+ checkRules
2473
3003
  };
2474
- //# sourceMappingURL=chunk-DG5FECXV.js.map
3004
+ //# sourceMappingURL=chunk-EW23BLJC.js.map