@dreamtree-org/graphify 1.1.1 → 1.1.3

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.
@@ -156,7 +156,7 @@ function validateDsn(dsn) {
156
156
  }
157
157
  };
158
158
  }
159
- function validateGraphPath(path17, base) {
159
+ function validateGraphPath(path18, base) {
160
160
  const baseDir = base ?? nodePath.join(process.cwd(), "graphify-out");
161
161
  let resolvedBase;
162
162
  try {
@@ -164,7 +164,7 @@ function validateGraphPath(path17, base) {
164
164
  } catch {
165
165
  throw new Error(`Base directory does not exist: ${baseDir}`);
166
166
  }
167
- const candidate = nodePath.isAbsolute(path17) ? path17 : nodePath.join(resolvedBase, path17);
167
+ const candidate = nodePath.isAbsolute(path18) ? path18 : nodePath.join(resolvedBase, path18);
168
168
  const resolvedCandidate = nodePath.resolve(candidate);
169
169
  let realCandidate = resolvedCandidate;
170
170
  try {
@@ -174,7 +174,7 @@ function validateGraphPath(path17, base) {
174
174
  const relative4 = nodePath.relative(resolvedBase, realCandidate);
175
175
  const escapes = relative4 === ".." || relative4.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(relative4);
176
176
  if (escapes) {
177
- throw new Error(`Path escapes graphify-out/: ${path17}`);
177
+ throw new Error(`Path escapes graphify-out/: ${path18}`);
178
178
  }
179
179
  return realCandidate;
180
180
  }
@@ -332,7 +332,10 @@ function scoreNodes(graph, query) {
332
332
  }
333
333
  if (score > 0) matches.push({ id, label, score });
334
334
  });
335
- matches.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
335
+ const placeholderRank = (id) => id.startsWith("module:") || id.startsWith("external:") ? 1 : 0;
336
+ matches.sort(
337
+ (a, b) => b.score - a.score || placeholderRank(a.id) - placeholderRank(b.id) || a.id.localeCompare(b.id)
338
+ );
336
339
  return matches;
337
340
  }
338
341
  function resolveNode(graph, query) {
@@ -443,25 +446,25 @@ function shortestPath(graph, fromQuery, toQuery) {
443
446
  if (!visited.has(to.id)) {
444
447
  return { from, to, found: false, path: [], edges: [] };
445
448
  }
446
- const path17 = [to.id];
449
+ const path18 = [to.id];
447
450
  let cursor = to.id;
448
451
  while (cursor !== from.id) {
449
452
  const prev = predecessor.get(cursor);
450
453
  if (!prev) break;
451
- path17.unshift(prev);
454
+ path18.unshift(prev);
452
455
  cursor = prev;
453
456
  }
454
457
  const edges = [];
455
- for (let i = 0; i < path17.length - 1; i++) {
456
- const a = path17[i];
457
- const b = path17[i + 1];
458
+ for (let i = 0; i < path18.length - 1; i++) {
459
+ const a = path18[i];
460
+ const b = path18[i + 1];
458
461
  const key = graph.edges(a, b)[0] ?? graph.edges(b, a)[0];
459
462
  if (key) {
460
463
  const attrs = graph.getEdgeAttributes(key);
461
464
  edges.push({ source: a, target: b, relation: String(attrs.relation), confidence: String(attrs.confidence) });
462
465
  }
463
466
  }
464
- return { from, to, found: true, path: path17, edges };
467
+ return { from, to, found: true, path: path18, edges };
465
468
  }
466
469
  function explainNode(graph, query) {
467
470
  const match = resolveNode(graph, query);
@@ -542,6 +545,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
542
545
  if (!target) return null;
543
546
  const maxDepth = options.maxDepth ?? DEFAULT_IMPACT_DEPTH;
544
547
  const limit = options.limit ?? DEFAULT_IMPACT_LIMIT;
548
+ const followed = options.relations ? new Set(options.relations) : DEPENDENCY_RELATIONS;
545
549
  const affected = [];
546
550
  const visited = /* @__PURE__ */ new Set([target.id]);
547
551
  let frontier = [target.id];
@@ -552,7 +556,7 @@ function affectedBy(graph, nodeQuery, options = {}) {
552
556
  graph.forEachInEdge(current, (_edge, edgeAttrs, source) => {
553
557
  if (visited.has(source)) return;
554
558
  const relation = edgeAttrs.relation;
555
- if (!DEPENDENCY_RELATIONS.has(relation)) return;
559
+ if (!followed.has(relation)) return;
556
560
  const confidence = edgeAttrs.confidence;
557
561
  const existing = nextFrontier.get(source);
558
562
  if (existing && CONFIDENCE_RANK2[existing.via.confidence] >= CONFIDENCE_RANK2[confidence]) return;
@@ -607,6 +611,100 @@ var init_impact = __esm({
607
611
  }
608
612
  });
609
613
 
614
+ // src/testmap.ts
615
+ function isTestFile(path18) {
616
+ return TEST_FILE_PATTERNS.some((p) => p.test(path18));
617
+ }
618
+ function selectionFromImpact(graph, targetIds, targetLabel, relations) {
619
+ const best = /* @__PURE__ */ new Map();
620
+ let affectedNodeCount = 0;
621
+ for (const id of targetIds) {
622
+ const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT, relations });
623
+ if (!impact) continue;
624
+ affectedNodeCount += impact.affected.length;
625
+ for (const node of impact.affected) {
626
+ if (!isTestFile(node.sourceFile)) continue;
627
+ const existing = best.get(node.sourceFile);
628
+ if (!existing || node.depth < existing.depth) {
629
+ best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });
630
+ }
631
+ }
632
+ }
633
+ 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));
634
+ return {
635
+ target: targetLabel,
636
+ testFiles,
637
+ affectedNodeCount,
638
+ precision: relations ? "usage" : "import-reachability"
639
+ };
640
+ }
641
+ function entityRootsOf(graph, id) {
642
+ if (id.includes("::")) return [id];
643
+ const roots = [];
644
+ graph.forEachOutEdge(id, (_edge, attrs, _source, target) => {
645
+ if (String(attrs.relation) === "contains") roots.push(target);
646
+ });
647
+ roots.sort((a, b) => a.localeCompare(b));
648
+ return roots.length > 0 ? roots : [id];
649
+ }
650
+ function testsForNode(graph, nodeQuery) {
651
+ const match = resolveNode(graph, nodeQuery);
652
+ if (!match) return null;
653
+ const precise = selectionFromImpact(graph, entityRootsOf(graph, match.id), match.id, USAGE_RELATIONS);
654
+ if (precise.testFiles.length > 0) return precise;
655
+ return selectionFromImpact(graph, [match.id], match.id);
656
+ }
657
+ function testsForChangedFiles(graph, changedFiles) {
658
+ const fileIds = [];
659
+ const selfSelected = /* @__PURE__ */ new Map();
660
+ for (const file of changedFiles) {
661
+ if (isTestFile(file)) {
662
+ selfSelected.set(file, { depth: 0, via: "(changed directly)" });
663
+ continue;
664
+ }
665
+ if (graph.hasNode(file)) fileIds.push(file);
666
+ }
667
+ const label = changedFiles.join(", ");
668
+ const preciseRoots = fileIds.flatMap((f) => entityRootsOf(graph, f));
669
+ let selection = selectionFromImpact(graph, preciseRoots, label, USAGE_RELATIONS);
670
+ if (selection.testFiles.length === 0) {
671
+ selection = selectionFromImpact(graph, fileIds, label);
672
+ }
673
+ for (const [file, info] of selfSelected) {
674
+ if (!selection.testFiles.some((t) => t.file === file)) {
675
+ selection.testFiles.push({ file, depth: info.depth, via: info.via });
676
+ }
677
+ }
678
+ selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
679
+ return selection;
680
+ }
681
+ var TEST_FILE_PATTERNS, TEST_REACH_DEPTH, TEST_REACH_LIMIT, USAGE_RELATIONS;
682
+ var init_testmap = __esm({
683
+ "src/testmap.ts"() {
684
+ "use strict";
685
+ init_cjs_shims();
686
+ init_impact();
687
+ init_query();
688
+ TEST_FILE_PATTERNS = [
689
+ /(^|\/)tests?\//,
690
+ // tests/ or test/ directory
691
+ /(^|\/)__tests__\//,
692
+ /(^|\/)spec\//,
693
+ /\.test\.[cm]?[jt]sx?$/,
694
+ /\.spec\.[cm]?[jt]sx?$/,
695
+ /(^|\/)test_[^/]+\.py$/,
696
+ /_test\.py$/,
697
+ /_test\.go$/,
698
+ /Tests?\.(java|cs)$/,
699
+ /_spec\.rb$/,
700
+ /_test\.rb$/
701
+ ];
702
+ TEST_REACH_DEPTH = 4;
703
+ TEST_REACH_LIMIT = 2e3;
704
+ USAGE_RELATIONS = ["calls", "inherits", "implements", "mixes_in", "embeds", "references"];
705
+ }
706
+ });
707
+
610
708
  // src/context.ts
611
709
  function lineNumberOf(sourceLocation) {
612
710
  const match = /^L(\d+)$/.exec(sourceLocation);
@@ -641,7 +739,7 @@ function snippetRange(startLine, fileLineCount, entityStartsInFile, maxLines) {
641
739
  const hardEnd = nextStart !== void 0 ? nextStart - 1 : fileLineCount;
642
740
  return { start: startLine, end: Math.min(hardEnd, startLine + maxLines - 1) };
643
741
  }
644
- async function buildContextPack(graph, task, readFile20, options = {}) {
742
+ async function buildContextPack(graph, task, readFile21, options = {}) {
645
743
  const tokenBudget = options.tokenBudget ?? DEFAULT_CONTEXT_BUDGET;
646
744
  const maxSnippetLines = options.maxSnippetLines ?? DEFAULT_MAX_SNIPPET_LINES;
647
745
  const ranked = rankForTask(graph, task, options.maxSeeds);
@@ -662,7 +760,7 @@ async function buildContextPack(graph, task, readFile20, options = {}) {
662
760
  if (cached !== void 0) return cached;
663
761
  let lines;
664
762
  try {
665
- lines = (await readFile20(file)).split("\n");
763
+ lines = (await readFile21(file)).split("\n");
666
764
  } catch {
667
765
  lines = null;
668
766
  }
@@ -748,78 +846,6 @@ var init_context = __esm({
748
846
  }
749
847
  });
750
848
 
751
- // src/testmap.ts
752
- function isTestFile(path17) {
753
- return TEST_FILE_PATTERNS.some((p) => p.test(path17));
754
- }
755
- function selectionFromImpact(graph, targetIds, targetLabel) {
756
- const best = /* @__PURE__ */ new Map();
757
- let affectedNodeCount = 0;
758
- for (const id of targetIds) {
759
- const impact = affectedBy(graph, id, { maxDepth: TEST_REACH_DEPTH, limit: TEST_REACH_LIMIT });
760
- if (!impact) continue;
761
- affectedNodeCount += impact.affected.length;
762
- for (const node of impact.affected) {
763
- if (!isTestFile(node.sourceFile)) continue;
764
- const existing = best.get(node.sourceFile);
765
- if (!existing || node.depth < existing.depth) {
766
- best.set(node.sourceFile, { depth: node.depth, via: node.via.dependsOn });
767
- }
768
- }
769
- }
770
- 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));
771
- return { target: targetLabel, testFiles, affectedNodeCount };
772
- }
773
- function testsForNode(graph, nodeQuery) {
774
- const match = resolveNode(graph, nodeQuery);
775
- if (!match) return null;
776
- return selectionFromImpact(graph, [match.id], match.id);
777
- }
778
- function testsForChangedFiles(graph, changedFiles) {
779
- const fileIds = [];
780
- const selfSelected = /* @__PURE__ */ new Map();
781
- for (const file of changedFiles) {
782
- if (isTestFile(file)) {
783
- selfSelected.set(file, { depth: 0, via: "(changed directly)" });
784
- continue;
785
- }
786
- if (graph.hasNode(file)) fileIds.push(file);
787
- }
788
- const selection = selectionFromImpact(graph, fileIds, changedFiles.join(", "));
789
- for (const [file, info] of selfSelected) {
790
- if (!selection.testFiles.some((t) => t.file === file)) {
791
- selection.testFiles.push({ file, depth: info.depth, via: info.via });
792
- }
793
- }
794
- selection.testFiles.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
795
- return selection;
796
- }
797
- var TEST_FILE_PATTERNS, TEST_REACH_DEPTH, TEST_REACH_LIMIT;
798
- var init_testmap = __esm({
799
- "src/testmap.ts"() {
800
- "use strict";
801
- init_cjs_shims();
802
- init_impact();
803
- init_query();
804
- TEST_FILE_PATTERNS = [
805
- /(^|\/)tests?\//,
806
- // tests/ or test/ directory
807
- /(^|\/)__tests__\//,
808
- /(^|\/)spec\//,
809
- /\.test\.[cm]?[jt]sx?$/,
810
- /\.spec\.[cm]?[jt]sx?$/,
811
- /(^|\/)test_[^/]+\.py$/,
812
- /_test\.py$/,
813
- /_test\.go$/,
814
- /Tests?\.(java|cs)$/,
815
- /_spec\.rb$/,
816
- /_test\.rb$/
817
- ];
818
- TEST_REACH_DEPTH = 4;
819
- TEST_REACH_LIMIT = 2e3;
820
- }
821
- });
822
-
823
849
  // src/mcp/server.ts
824
850
  var server_exports = {};
825
851
  __export(server_exports, {
@@ -914,7 +940,7 @@ function createGraphifyMcpServer(outDir) {
914
940
  },
915
941
  async ({ task, budget }) => {
916
942
  const graph = await loadGraph(outDir);
917
- const pack = await buildContextPack(graph, task, (p) => fs22.readFile(p, "utf-8"), {
943
+ const pack = await buildContextPack(graph, task, (p) => fs23.readFile(p, "utf-8"), {
918
944
  tokenBudget: budget
919
945
  });
920
946
  return textResult(renderContextPack(pack));
@@ -940,23 +966,23 @@ function createGraphifyMcpServer(outDir) {
940
966
  return server;
941
967
  }
942
968
  async function startServer(graphPath, transport = new import_stdio.StdioServerTransport()) {
943
- const outDir = path15.dirname(path15.resolve(graphPath));
944
- const fileName = path15.basename(graphPath);
969
+ const outDir = path16.dirname(path16.resolve(graphPath));
970
+ const fileName = path16.basename(graphPath);
945
971
  validateGraphPath(fileName, outDir);
946
972
  const server = createGraphifyMcpServer(outDir);
947
973
  await server.connect(transport);
948
974
  }
949
975
  async function main(argv = process.argv) {
950
- const graphPath = argv[2] ?? path15.join(process.cwd(), "graphify-out", "graph.json");
976
+ const graphPath = argv[2] ?? path16.join(process.cwd(), "graphify-out", "graph.json");
951
977
  await startServer(graphPath);
952
978
  }
953
- var fs22, path15, import_mcp, import_stdio, import_zod2;
979
+ var fs23, path16, import_mcp, import_stdio, import_zod2;
954
980
  var init_server = __esm({
955
981
  "src/mcp/server.ts"() {
956
982
  "use strict";
957
983
  init_cjs_shims();
958
- fs22 = __toESM(require("fs/promises"), 1);
959
- path15 = __toESM(require("path"), 1);
984
+ fs23 = __toESM(require("fs/promises"), 1);
985
+ path16 = __toESM(require("path"), 1);
960
986
  import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
961
987
  import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
962
988
  import_zod2 = require("zod");
@@ -1131,9 +1157,9 @@ var init_mysql = __esm({
1131
1157
  // src/cli/index.ts
1132
1158
  init_cjs_shims();
1133
1159
  var import_node_child_process3 = require("child_process");
1134
- var fs23 = __toESM(require("fs/promises"), 1);
1160
+ var fs24 = __toESM(require("fs/promises"), 1);
1135
1161
  var os3 = __toESM(require("os"), 1);
1136
- var path16 = __toESM(require("path"), 1);
1162
+ var path17 = __toESM(require("path"), 1);
1137
1163
  var import_node_util3 = require("util");
1138
1164
  var import_chokidar = require("chokidar");
1139
1165
  var import_commander = require("commander");
@@ -1465,8 +1491,8 @@ init_graphStore();
1465
1491
 
1466
1492
  // src/pipeline.ts
1467
1493
  init_cjs_shims();
1468
- var fs13 = __toESM(require("fs/promises"), 1);
1469
- var path8 = __toESM(require("path"), 1);
1494
+ var fs14 = __toESM(require("fs/promises"), 1);
1495
+ var path9 = __toESM(require("path"), 1);
1470
1496
 
1471
1497
  // src/build.ts
1472
1498
  init_cjs_shims();
@@ -3138,10 +3164,10 @@ async function extractRust(filePath, displayPath) {
3138
3164
  }
3139
3165
  }
3140
3166
  } else if (fn.type === "scoped_identifier") {
3141
- const path17 = fn.childForFieldName("path")?.text;
3167
+ const path18 = fn.childForFieldName("path")?.text;
3142
3168
  const name = fn.childForFieldName("name")?.text;
3143
- if (path17 && name) {
3144
- const viaImport = importIndex.get(path17);
3169
+ if (path18 && name) {
3170
+ const viaImport = importIndex.get(path18);
3145
3171
  if (viaImport) {
3146
3172
  targetId = viaImport.id;
3147
3173
  confidence = "INFERRED";
@@ -3213,13 +3239,11 @@ async function extractTypeScript(filePath, displayPath) {
3213
3239
  });
3214
3240
  }
3215
3241
  function importTargetId(binding, property) {
3216
- if (!binding.ref.external) {
3217
- if (binding.kind === "named" && binding.importedName) {
3218
- return entityId(binding.ref.id, binding.importedName);
3219
- }
3220
- if (binding.kind === "namespace" && property) {
3221
- return entityId(binding.ref.id, property);
3222
- }
3242
+ if (binding.kind === "named" && binding.importedName) {
3243
+ return entityId(binding.ref.id, binding.importedName);
3244
+ }
3245
+ if (binding.kind === "namespace" && property) {
3246
+ return entityId(binding.ref.id, property);
3223
3247
  }
3224
3248
  return binding.ref.id;
3225
3249
  }
@@ -3257,24 +3281,35 @@ async function extractTypeScript(filePath, displayPath) {
3257
3281
  }
3258
3282
  }
3259
3283
  }
3284
+ function unwrapExpression(node) {
3285
+ let current = node;
3286
+ while (current.type === "as_expression" || current.type === "satisfies_expression" || current.type === "non_null_expression" || current.type === "parenthesized_expression") {
3287
+ const inner = namedChildren(current)[0];
3288
+ if (!inner) break;
3289
+ current = inner;
3290
+ }
3291
+ return current;
3292
+ }
3260
3293
  function handleTopLevelVariableFunctions(node) {
3294
+ const isExported = node.parent?.type === "export_statement";
3261
3295
  for (const declarator of namedChildren(node)) {
3262
3296
  if (declarator.type !== "variable_declarator") continue;
3263
3297
  const value = declarator.childForFieldName("value");
3264
3298
  if (!value) continue;
3265
- if (value.type === "arrow_function" || value.type === "function_expression") {
3299
+ const core = unwrapExpression(value);
3300
+ if (core.type === "arrow_function" || core.type === "function_expression") {
3266
3301
  const name = declarator.childForFieldName("name")?.text;
3267
3302
  if (!name) continue;
3268
3303
  const id = entityId(sourceFile, name);
3269
3304
  builder.addNode({ id, label: `function ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
3270
3305
  builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
3271
3306
  nameIndex.set(name, id);
3272
- functionNodeMap.set(value.id, id);
3307
+ functionNodeMap.set(core.id, id);
3273
3308
  continue;
3274
3309
  }
3275
- if (value.type === "call_expression") {
3276
- const callee = value.childForFieldName("function");
3277
- const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(value) : null;
3310
+ if (core.type === "call_expression") {
3311
+ const callee = core.childForFieldName("function");
3312
+ const specifier = callee?.type === "identifier" && callee.text === "require" ? firstStringArgument(core) : null;
3278
3313
  if (specifier) {
3279
3314
  const nameNode = declarator.childForFieldName("name");
3280
3315
  if (nameNode) {
@@ -3282,6 +3317,21 @@ async function extractTypeScript(filePath, displayPath) {
3282
3317
  addModuleNode(ref);
3283
3318
  registerRequireBinding(nameNode, ref);
3284
3319
  }
3320
+ continue;
3321
+ }
3322
+ }
3323
+ if (isExported) {
3324
+ const name = declarator.childForFieldName("name")?.text;
3325
+ if (!name) continue;
3326
+ const id = entityId(sourceFile, name);
3327
+ builder.addNode({ id, label: `const ${name}`, sourceFile, sourceLocation: lineOf(declarator) });
3328
+ builder.addEdge({ source: fileId, target: id, relation: "contains", confidence: "EXTRACTED" });
3329
+ nameIndex.set(name, id);
3330
+ if (core.type === "identifier") {
3331
+ const aliased = nameIndex.get(core.text);
3332
+ if (aliased !== void 0 && aliased !== id) {
3333
+ builder.addEdge({ source: id, target: aliased, relation: "references", confidence: "EXTRACTED" });
3334
+ }
3285
3335
  }
3286
3336
  }
3287
3337
  }
@@ -3601,6 +3651,8 @@ function renderReport(graph, analysis, now = /* @__PURE__ */ new Date()) {
3601
3651
 
3602
3652
  // src/resolve.ts
3603
3653
  init_cjs_shims();
3654
+ var fs13 = __toESM(require("fs/promises"), 1);
3655
+ var path8 = __toESM(require("path"), 1);
3604
3656
  var CODE_EXTENSIONS2 = [
3605
3657
  ".ts",
3606
3658
  ".tsx",
@@ -3620,6 +3672,29 @@ var CODE_EXTENSIONS2 = [
3620
3672
  function isOpaque(id) {
3621
3673
  return id.startsWith("module:") || id.startsWith("external:") || id.startsWith("db:");
3622
3674
  }
3675
+ var SELF_IMPORT_STEMS = (subpath) => subpath === "" ? ["index", "src/index", "lib/index", "source/index", "src/main"] : [
3676
+ subpath,
3677
+ `src/${subpath}`,
3678
+ `lib/${subpath}`,
3679
+ `source/${subpath}`,
3680
+ `${subpath}/index`,
3681
+ `src/${subpath}/index`,
3682
+ `lib/${subpath}/index`
3683
+ ];
3684
+ function selfImportCandidates(spec, selfNames) {
3685
+ const selfName = selfNames.find((n) => spec === n || spec.startsWith(`${n}/`));
3686
+ if (selfName === void 0) return null;
3687
+ const subpath = spec === selfName ? "" : spec.slice(selfName.length + 1);
3688
+ return SELF_IMPORT_STEMS(subpath).flatMap((stem) => CODE_EXTENSIONS2.map((ext) => `${stem}${ext}`));
3689
+ }
3690
+ async function readSelfNames(root) {
3691
+ try {
3692
+ const pkg = JSON.parse(await fs13.readFile(path8.join(root, "package.json"), "utf-8"));
3693
+ return typeof pkg.name === "string" && pkg.name.length > 0 ? [pkg.name] : [];
3694
+ } catch {
3695
+ return [];
3696
+ }
3697
+ }
3623
3698
  function stripKnownExtension(p) {
3624
3699
  for (const ext of CODE_EXTENSIONS2) {
3625
3700
  if (p.endsWith(ext)) return p.slice(0, -ext.length);
@@ -3636,19 +3711,23 @@ function candidatePaths(guessed) {
3636
3711
  }
3637
3712
  return candidates.filter((c) => c !== guessed);
3638
3713
  }
3639
- function uniqueMatch(guessed, candidates, realIds) {
3714
+ function uniqueMatch(candidates, real) {
3640
3715
  const matches = /* @__PURE__ */ new Set();
3641
3716
  for (const candidate of candidates) {
3642
- if (realIds.has(candidate)) matches.add(candidate);
3717
+ if (real.has(candidate)) matches.add(candidate);
3643
3718
  }
3644
- if (matches.size !== 1) return null;
3645
- return [...matches][0];
3719
+ return matches.size === 1 ? [...matches][0] : null;
3646
3720
  }
3647
- function resolveCrossFileReferences(extractions) {
3721
+ function resolveCrossFileReferences(extractions, options = {}) {
3722
+ const selfNames = options.selfNames ?? [];
3648
3723
  const realIds = /* @__PURE__ */ new Set();
3649
3724
  const realFiles = /* @__PURE__ */ new Set();
3650
3725
  for (const extraction of extractions) {
3651
3726
  for (const node of extraction.nodes) realIds.add(node.id);
3727
+ const first = extraction.nodes[0];
3728
+ if (first && first.id === first.sourceFile && !isOpaque(first.id)) {
3729
+ realFiles.add(first.id);
3730
+ }
3652
3731
  for (const node of extraction.nodes) {
3653
3732
  const sep3 = node.id.indexOf("::");
3654
3733
  if (sep3 > 0) realFiles.add(node.id.slice(0, sep3));
@@ -3658,7 +3737,24 @@ function resolveCrossFileReferences(extractions) {
3658
3737
  }
3659
3738
  }
3660
3739
  const remap = /* @__PURE__ */ new Map();
3661
- const resolveId = (id) => {
3740
+ let resolvedEndpoints = 0;
3741
+ const applyRemap = (resolveId) => {
3742
+ for (const extraction of extractions) {
3743
+ for (const edge of extraction.edges) {
3744
+ const source = resolveId(edge.source);
3745
+ if (source !== null) {
3746
+ edge.source = source;
3747
+ resolvedEndpoints++;
3748
+ }
3749
+ const target = resolveId(edge.target);
3750
+ if (target !== null) {
3751
+ edge.target = target;
3752
+ resolvedEndpoints++;
3753
+ }
3754
+ }
3755
+ }
3756
+ };
3757
+ const resolvePathId = (id) => {
3662
3758
  if (isOpaque(id)) return null;
3663
3759
  const cached = remap.get(id);
3664
3760
  if (cached !== void 0) return cached;
@@ -3668,30 +3764,77 @@ function resolveCrossFileReferences(extractions) {
3668
3764
  if (realIds.has(id)) return null;
3669
3765
  const guessedPath = id.slice(0, sep3);
3670
3766
  const name = id.slice(sep3);
3671
- const candidates = candidatePaths(guessedPath).map((p) => `${p}${name}`);
3672
- resolved = uniqueMatch(id, candidates, realIds);
3767
+ resolved = uniqueMatch(candidatePaths(guessedPath).map((p) => `${p}${name}`), realIds);
3673
3768
  } else {
3674
3769
  if (realFiles.has(id)) return null;
3675
- resolved = uniqueMatch(id, candidatePaths(id), realFiles);
3770
+ resolved = uniqueMatch(candidatePaths(id), realFiles);
3676
3771
  }
3677
3772
  if (resolved !== null) remap.set(id, resolved);
3678
3773
  return resolved;
3679
3774
  };
3680
- let resolvedEndpoints = 0;
3775
+ applyRemap(resolvePathId);
3776
+ const namedAlias = /* @__PURE__ */ new Map();
3777
+ const starTargets = /* @__PURE__ */ new Map();
3681
3778
  for (const extraction of extractions) {
3682
3779
  for (const edge of extraction.edges) {
3683
- const source = resolveId(edge.source);
3684
- if (source !== null) {
3685
- edge.source = source;
3686
- resolvedEndpoints++;
3687
- }
3688
- const target = resolveId(edge.target);
3689
- if (target !== null) {
3690
- edge.target = target;
3691
- resolvedEndpoints++;
3780
+ if (edge.relation !== "re_exports" || !realFiles.has(edge.source)) continue;
3781
+ const sep3 = edge.target.lastIndexOf("::");
3782
+ if (sep3 > 0 && realIds.has(edge.target)) {
3783
+ namedAlias.set(`${edge.source}|${edge.target.slice(sep3 + 2)}`, edge.target);
3784
+ } else if (sep3 < 0 && realFiles.has(edge.target)) {
3785
+ const targets = starTargets.get(edge.source) ?? [];
3786
+ targets.push(edge.target);
3787
+ starTargets.set(edge.source, targets);
3692
3788
  }
3693
3789
  }
3694
3790
  }
3791
+ const throughBarrel = (file, name) => {
3792
+ const direct = `${file}::${name}`;
3793
+ if (realIds.has(direct)) return direct;
3794
+ const aliased = namedAlias.get(`${file}|${name}`);
3795
+ if (aliased !== void 0) return aliased;
3796
+ const viaStar = (starTargets.get(file) ?? []).map((sf) => `${sf}::${name}`).filter((id) => realIds.has(id));
3797
+ return viaStar.length === 1 ? viaStar[0] : null;
3798
+ };
3799
+ const resolveBarrelId = (id) => {
3800
+ const cached = remap.get(id);
3801
+ if (cached !== void 0) return cached;
3802
+ if (id.startsWith("module:")) {
3803
+ const spec = id.slice("module:".length);
3804
+ const sep4 = spec.indexOf("::");
3805
+ const moduleSpec = sep4 > 0 ? spec.slice(0, sep4) : spec;
3806
+ const name = sep4 > 0 ? spec.slice(sep4 + 2) : null;
3807
+ const candidates = selfImportCandidates(moduleSpec, selfNames);
3808
+ if (candidates === null) {
3809
+ if (name === null) return null;
3810
+ remap.set(id, `module:${moduleSpec}`);
3811
+ return `module:${moduleSpec}`;
3812
+ }
3813
+ const file = uniqueMatch(candidates, realFiles);
3814
+ let resolved = null;
3815
+ if (file !== null) {
3816
+ resolved = name !== null ? throughBarrel(file, name) ?? file : file;
3817
+ } else if (name !== null) {
3818
+ resolved = `module:${moduleSpec}`;
3819
+ }
3820
+ if (resolved !== null) remap.set(id, resolved);
3821
+ return resolved;
3822
+ }
3823
+ const sep3 = id.indexOf("::");
3824
+ if (sep3 > 0 && !realIds.has(id)) {
3825
+ const guessedPath = id.slice(0, sep3);
3826
+ const name = id.slice(sep3 + 2);
3827
+ const files = [guessedPath, ...candidatePaths(guessedPath)].filter((f) => realFiles.has(f));
3828
+ const resolvedSet = new Set(files.map((f) => throughBarrel(f, name)).filter((r) => r !== null));
3829
+ if (resolvedSet.size === 1) {
3830
+ const resolved = [...resolvedSet][0];
3831
+ remap.set(id, resolved);
3832
+ return resolved;
3833
+ }
3834
+ }
3835
+ return null;
3836
+ };
3837
+ applyRemap(resolveBarrelId);
3695
3838
  let droppedPlaceholders = 0;
3696
3839
  for (const extraction of extractions) {
3697
3840
  const before = extraction.nodes.length;
@@ -3705,21 +3848,21 @@ function resolveCrossFileReferences(extractions) {
3705
3848
  async function runPipeline(root, options = {}) {
3706
3849
  const progress = options.onProgress ?? (() => {
3707
3850
  });
3708
- const resolvedRoot = path8.resolve(root);
3851
+ const resolvedRoot = path9.resolve(root);
3709
3852
  progress(`Scanning ${resolvedRoot} ...`);
3710
3853
  const manifest = collectFiles(resolvedRoot);
3711
3854
  progress(
3712
3855
  `Found ${manifest.totalFiles} file(s) (${manifest.files.code.length} code, ${manifest.skippedSensitive.length} skipped as sensitive).`
3713
3856
  );
3714
- const outDir = options.outDir ?? path8.join(resolvedRoot, "graphify-out");
3857
+ const outDir = options.outDir ?? path9.join(resolvedRoot, "graphify-out");
3715
3858
  const cache = new ExtractionCache(outDir);
3716
3859
  progress("Extracting...");
3717
3860
  const extractions = [];
3718
3861
  let cacheHits = 0;
3719
3862
  for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
3720
- const filePath = path8.relative(process.cwd(), path8.join(resolvedRoot, relFile)) || relFile;
3863
+ const filePath = path9.relative(process.cwd(), path9.join(resolvedRoot, relFile)) || relFile;
3721
3864
  try {
3722
- const key = cache.key(relFile, await fs13.readFile(path8.join(resolvedRoot, relFile)));
3865
+ const key = cache.key(relFile, await fs14.readFile(path9.join(resolvedRoot, relFile)));
3723
3866
  let extraction = options.update ? await cache.get(key) : null;
3724
3867
  if (extraction) {
3725
3868
  cacheHits++;
@@ -3740,7 +3883,9 @@ async function runPipeline(root, options = {}) {
3740
3883
  extractions.push(...options.extraExtractions);
3741
3884
  }
3742
3885
  progress("Resolving cross-file references...");
3743
- const resolveStats = resolveCrossFileReferences(extractions);
3886
+ const resolveStats = resolveCrossFileReferences(extractions, {
3887
+ selfNames: await readSelfNames(resolvedRoot)
3888
+ });
3744
3889
  if (resolveStats.resolvedEndpoints > 0) {
3745
3890
  progress(
3746
3891
  ` re-pointed ${resolveStats.resolvedEndpoints} guessed reference(s) at real nodes (${resolveStats.droppedPlaceholders} placeholder node(s) dropped).`
@@ -3797,9 +3942,10 @@ async function runAffected(nodeName, options = {}) {
3797
3942
  console.log("");
3798
3943
  console.log(currentDepth === 1 ? "Directly affected:" : `Affected at depth ${currentDepth}:`);
3799
3944
  }
3800
- const location = node.sourceLocation ? `:${node.sourceLocation}` : "";
3945
+ const isPlaceholder = node.sourceFile === "<unknown>" || node.sourceFile === "<external>" || node.sourceFile === "";
3946
+ const where = isPlaceholder ? "(reference only)" : `${node.sourceFile}${node.sourceLocation ? `:${node.sourceLocation}` : ""}`;
3801
3947
  console.log(
3802
- ` ${node.label} \u2014 ${node.sourceFile}${location} (--${node.via.relation}--> ${node.via.dependsOn}, ${node.via.confidence})`
3948
+ ` ${node.label} \u2014 ${where} (--${node.via.relation}--> ${node.via.dependsOn}, ${node.via.confidence})`
3803
3949
  );
3804
3950
  }
3805
3951
  console.log("");
@@ -3814,11 +3960,12 @@ async function runAffected(nodeName, options = {}) {
3814
3960
 
3815
3961
  // src/cli/commands/benchmark.ts
3816
3962
  init_cjs_shims();
3817
- var fs14 = __toESM(require("fs/promises"), 1);
3963
+ var fs15 = __toESM(require("fs/promises"), 1);
3818
3964
  init_graphStore();
3819
3965
  init_query();
3966
+ init_testmap();
3820
3967
  var tokensOf = (text) => Math.ceil(text.length / 4);
3821
- async function benchmarkQuestion(graph, question, readFile20) {
3968
+ async function benchmarkQuestion(graph, question, readFile21) {
3822
3969
  const result = queryGraph(graph, question);
3823
3970
  const graphTokens = tokensOf(JSON.stringify(result));
3824
3971
  const sourceFiles = /* @__PURE__ */ new Set();
@@ -3832,7 +3979,7 @@ async function benchmarkQuestion(graph, question, readFile20) {
3832
3979
  let filesMissing = 0;
3833
3980
  for (const file of [...sourceFiles].sort((a, b) => a.localeCompare(b))) {
3834
3981
  try {
3835
- naiveTokens += tokensOf(await readFile20(file));
3982
+ naiveTokens += tokensOf(await readFile21(file));
3836
3983
  filesRead++;
3837
3984
  } catch {
3838
3985
  filesMissing++;
@@ -3847,12 +3994,14 @@ async function benchmarkQuestion(graph, question, readFile20) {
3847
3994
  filesMissing
3848
3995
  };
3849
3996
  }
3997
+ var NON_QUESTION_FILE = /(^|\/)(examples?|scripts?|bench(?:mark)?s?|fixtures?|docs?|\.github)\/|\.(config|conf)\.[^/]+$|(^|\/)(rollup|webpack|vite|vitest|jest|babel|tsup|eslint|prettier)\.[^/]*$/i;
3850
3998
  function defaultQuestions(graph, count = 5) {
3851
3999
  const entries = [...graph.nodes()].map((id) => ({
3852
4000
  id,
3853
4001
  degree: graph.degree(id),
3854
- label: graph.getNodeAttribute(id, "label") ?? id
3855
- })).filter((e) => e.id.includes("::")).sort((a, b) => b.degree - a.degree || a.id.localeCompare(b.id));
4002
+ label: graph.getNodeAttribute(id, "label") ?? id,
4003
+ file: graph.getNodeAttribute(id, "sourceFile") ?? ""
4004
+ })).filter((e) => e.id.includes("::")).filter((e) => !isTestFile(e.file) && !NON_QUESTION_FILE.test(e.file)).sort((a, b) => b.degree - a.degree || a.id.localeCompare(b.id));
3856
4005
  return entries.slice(0, count).map((e) => `how does ${e.label} work`);
3857
4006
  }
3858
4007
  async function runBenchmark(questions) {
@@ -3864,7 +4013,7 @@ async function runBenchmark(questions) {
3864
4013
  }
3865
4014
  const rows = [];
3866
4015
  for (const question of effective) {
3867
- rows.push(await benchmarkQuestion(graph, question, (p) => fs14.readFile(p, "utf-8")));
4016
+ rows.push(await benchmarkQuestion(graph, question, (p) => fs15.readFile(p, "utf-8")));
3868
4017
  }
3869
4018
  console.log("Token cost per question \u2014 graph answer vs reading the files it touches:\n");
3870
4019
  for (const row of rows) {
@@ -3887,8 +4036,8 @@ Average reduction: ${avg.toFixed(1)}x across ${scored.length} question(s).`);
3887
4036
 
3888
4037
  // src/cli/commands/check.ts
3889
4038
  init_cjs_shims();
3890
- var fs15 = __toESM(require("fs/promises"), 1);
3891
- var path9 = __toESM(require("path"), 1);
4039
+ var fs16 = __toESM(require("fs/promises"), 1);
4040
+ var path10 = __toESM(require("path"), 1);
3892
4041
 
3893
4042
  // src/check.ts
3894
4043
  init_cjs_shims();
@@ -3959,10 +4108,10 @@ function checkRules(graph, config) {
3959
4108
  // src/cli/commands/check.ts
3960
4109
  init_graphStore();
3961
4110
  async function runCheck(options = {}) {
3962
- const rulesPath = path9.resolve(options.rules ?? "graphify.rules.json");
4111
+ const rulesPath = path10.resolve(options.rules ?? "graphify.rules.json");
3963
4112
  let raw;
3964
4113
  try {
3965
- raw = await fs15.readFile(rulesPath, "utf-8");
4114
+ raw = await fs16.readFile(rulesPath, "utf-8");
3966
4115
  } catch {
3967
4116
  throw new Error(
3968
4117
  `No rules file at ${rulesPath} \u2014 create graphify.rules.json with { "rules": [{ "name": "...", "from": "src/a/**", "disallow": ["src/b/**"] }] }.`
@@ -3985,12 +4134,12 @@ async function runCheck(options = {}) {
3985
4134
 
3986
4135
  // src/cli/commands/context.ts
3987
4136
  init_cjs_shims();
3988
- var fs16 = __toESM(require("fs/promises"), 1);
4137
+ var fs17 = __toESM(require("fs/promises"), 1);
3989
4138
  init_context();
3990
4139
  init_graphStore();
3991
4140
  async function runContext(task, options = {}) {
3992
4141
  const graph = await loadGraph();
3993
- const pack = await buildContextPack(graph, task, (p) => fs16.readFile(p, "utf-8"), {
4142
+ const pack = await buildContextPack(graph, task, (p) => fs17.readFile(p, "utf-8"), {
3994
4143
  tokenBudget: options.budget
3995
4144
  });
3996
4145
  console.log(renderContextPack(pack));
@@ -4007,9 +4156,13 @@ async function runExplain(nodeName) {
4007
4156
  console.log(`No node matched "${nodeName}".`);
4008
4157
  return;
4009
4158
  }
4010
- const location = result.sourceLocation ? `:${result.sourceLocation}` : "";
4011
4159
  console.log(result.label);
4012
- console.log(` location: ${result.sourceFile}${location}`);
4160
+ if (result.sourceFile === "<unknown>" || result.sourceFile === "<external>" || result.sourceFile === "") {
4161
+ console.log(" location: (reference only \u2014 no declaration seen; check its outgoing edges for the real definition)");
4162
+ } else {
4163
+ const location = result.sourceLocation ? `:${result.sourceLocation}` : "";
4164
+ console.log(` location: ${result.sourceFile}${location}`);
4165
+ }
4013
4166
  if (result.communityLabel) {
4014
4167
  console.log(` community: ${result.communityLabel}`);
4015
4168
  }
@@ -4039,9 +4192,9 @@ init_cjs_shims();
4039
4192
  // src/diff.ts
4040
4193
  init_cjs_shims();
4041
4194
  var import_node_child_process = require("child_process");
4042
- var fs17 = __toESM(require("fs/promises"), 1);
4195
+ var fs18 = __toESM(require("fs/promises"), 1);
4043
4196
  var os = __toESM(require("os"), 1);
4044
- var path10 = __toESM(require("path"), 1);
4197
+ var path11 = __toESM(require("path"), 1);
4045
4198
  var import_node_util = require("util");
4046
4199
  init_impact();
4047
4200
  init_testmap();
@@ -4050,13 +4203,13 @@ async function graphForDirectory(root) {
4050
4203
  const manifest = collectFiles(root);
4051
4204
  const extractions = [];
4052
4205
  for (const relFile of manifest.files.code.sort((a, b) => a.localeCompare(b))) {
4053
- extractions.push(await extract(path10.join(root, relFile), relFile));
4206
+ extractions.push(await extract(path11.join(root, relFile), relFile));
4054
4207
  }
4055
- resolveCrossFileReferences(extractions);
4208
+ resolveCrossFileReferences(extractions, { selfNames: await readSelfNames(root) });
4056
4209
  return buildGraph(extractions);
4057
4210
  }
4058
4211
  async function checkoutRev(repoRoot, rev) {
4059
- const dest = await fs17.mkdtemp(path10.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
4212
+ const dest = await fs18.mkdtemp(path11.join(os.tmpdir(), `graphify-review-${rev.replace(/[^a-zA-Z0-9]/g, "_")}-`));
4060
4213
  const { stdout } = await execFileAsync(
4061
4214
  "git",
4062
4215
  ["-C", repoRoot, "archive", "--format=tar", rev],
@@ -4124,8 +4277,8 @@ async function reviewRevisions(repoRoot, base, head = "HEAD") {
4124
4277
  return diffGraphs(baseGraph, headGraph, base, head);
4125
4278
  } finally {
4126
4279
  await Promise.all([
4127
- fs17.rm(baseDir, { recursive: true, force: true }),
4128
- fs17.rm(headDir, { recursive: true, force: true })
4280
+ fs18.rm(baseDir, { recursive: true, force: true }),
4281
+ fs18.rm(headDir, { recursive: true, force: true })
4129
4282
  ]);
4130
4283
  }
4131
4284
  }
@@ -4208,7 +4361,8 @@ async function runTests(nodeQuery, options = {}) {
4208
4361
  );
4209
4362
  return;
4210
4363
  }
4211
- console.log(`Test files for ${selection.target} (most direct first):`);
4364
+ const how = selection.precision === "usage" ? "selected by actual usage (calls/inheritance)" : "selected by import reachability \u2014 may over-include files sharing an entry point";
4365
+ console.log(`Test files for ${selection.target} (${how}; most direct first):`);
4212
4366
  for (const test of selection.testFiles) {
4213
4367
  const via = test.depth === 0 ? test.via : `depth ${test.depth}, via ${test.via}`;
4214
4368
  console.log(` ${test.file} (${via})`);
@@ -4217,9 +4371,9 @@ async function runTests(nodeQuery, options = {}) {
4217
4371
 
4218
4372
  // src/cli/commands/global.ts
4219
4373
  init_cjs_shims();
4220
- var fs18 = __toESM(require("fs/promises"), 1);
4374
+ var fs19 = __toESM(require("fs/promises"), 1);
4221
4375
  var os2 = __toESM(require("os"), 1);
4222
- var path11 = __toESM(require("path"), 1);
4376
+ var path12 = __toESM(require("path"), 1);
4223
4377
  init_graphStore();
4224
4378
 
4225
4379
  // src/merge.ts
@@ -4281,14 +4435,14 @@ function mergeGraphs(entries) {
4281
4435
 
4282
4436
  // src/cli/commands/global.ts
4283
4437
  function registryDir() {
4284
- return path11.join(os2.homedir(), ".graphify");
4438
+ return path12.join(os2.homedir(), ".graphify");
4285
4439
  }
4286
4440
  function registryPath() {
4287
- return path11.join(registryDir(), "global.json");
4441
+ return path12.join(registryDir(), "global.json");
4288
4442
  }
4289
4443
  async function readRegistry() {
4290
4444
  try {
4291
- const raw = await fs18.readFile(registryPath(), "utf-8");
4445
+ const raw = await fs19.readFile(registryPath(), "utf-8");
4292
4446
  const parsed = JSON.parse(raw);
4293
4447
  const projects = parsed.projects;
4294
4448
  if (!Array.isArray(projects)) return { projects: [] };
@@ -4298,23 +4452,23 @@ async function readRegistry() {
4298
4452
  }
4299
4453
  }
4300
4454
  async function writeRegistry(registry) {
4301
- await fs18.mkdir(registryDir(), { recursive: true });
4302
- await fs18.writeFile(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
4455
+ await fs19.mkdir(registryDir(), { recursive: true });
4456
+ await fs19.writeFile(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
4303
4457
  }
4304
4458
  async function projectNameFor(root) {
4305
4459
  try {
4306
- const raw = await fs18.readFile(path11.join(root, "package.json"), "utf-8");
4460
+ const raw = await fs19.readFile(path12.join(root, "package.json"), "utf-8");
4307
4461
  const name = JSON.parse(raw).name;
4308
4462
  if (typeof name === "string" && name.length > 0) return name;
4309
4463
  } catch {
4310
4464
  }
4311
- return path11.basename(root);
4465
+ return path12.basename(root);
4312
4466
  }
4313
4467
  async function runGlobalAdd(target = ".") {
4314
- const root = path11.resolve(target);
4315
- const graphJson = path11.join(root, "graphify-out", "graph.json");
4468
+ const root = path12.resolve(target);
4469
+ const graphJson = path12.join(root, "graphify-out", "graph.json");
4316
4470
  try {
4317
- await fs18.access(graphJson);
4471
+ await fs19.access(graphJson);
4318
4472
  } catch {
4319
4473
  throw new Error(`${root} has no graphify-out/graph.json \u2014 run \`graphify ${target}\` first, then add it.`);
4320
4474
  }
@@ -4385,9 +4539,9 @@ async function runGlobalBuild(options = {}) {
4385
4539
  throw new Error("No projects registered. Add some with `graphify global add <path>` first.");
4386
4540
  }
4387
4541
  const entries = await loadEntries(
4388
- registry.projects.map((p) => ({ name: p.name, outDir: path11.join(p.root, "graphify-out") }))
4542
+ registry.projects.map((p) => ({ name: p.name, outDir: path12.join(p.root, "graphify-out") }))
4389
4543
  );
4390
- await mergeAndExport(entries, path11.resolve(options.out ?? path11.join(registryDir(), "global-out")));
4544
+ await mergeAndExport(entries, path12.resolve(options.out ?? path12.join(registryDir(), "global-out")));
4391
4545
  }
4392
4546
  async function runMerge(targets, options = {}) {
4393
4547
  if (targets.length < 2) {
@@ -4395,8 +4549,8 @@ async function runMerge(targets, options = {}) {
4395
4549
  }
4396
4550
  const sources = [];
4397
4551
  for (const target of targets) {
4398
- const root = path11.resolve(target);
4399
- sources.push({ name: await projectNameFor(root), outDir: path11.join(root, "graphify-out") });
4552
+ const root = path12.resolve(target);
4553
+ sources.push({ name: await projectNameFor(root), outDir: path12.join(root, "graphify-out") });
4400
4554
  }
4401
4555
  const names = sources.map((s) => s.name);
4402
4556
  const duplicate = names.find((name, i) => names.indexOf(name) !== i);
@@ -4404,13 +4558,13 @@ async function runMerge(targets, options = {}) {
4404
4558
  throw new Error(`Two of the given projects resolve to the same name ("${duplicate}") \u2014 rename one.`);
4405
4559
  }
4406
4560
  const entries = await loadEntries(sources);
4407
- await mergeAndExport(entries, path11.resolve(options.out ?? "graphify-merged"));
4561
+ await mergeAndExport(entries, path12.resolve(options.out ?? "graphify-merged"));
4408
4562
  }
4409
4563
 
4410
4564
  // src/cli/commands/hook.ts
4411
4565
  init_cjs_shims();
4412
- var fs19 = __toESM(require("fs/promises"), 1);
4413
- var path12 = __toESM(require("path"), 1);
4566
+ var fs20 = __toESM(require("fs/promises"), 1);
4567
+ var path13 = __toESM(require("path"), 1);
4414
4568
  var MARKER_BEGIN = "# >>> graphify >>>";
4415
4569
  var MARKER_END = "# <<< graphify <<<";
4416
4570
  var HOOK_NAMES = ["post-commit", "post-merge"];
@@ -4421,34 +4575,34 @@ var HOOK_BLOCK = `${MARKER_BEGIN}
4421
4575
  (graphify . --update --no-viz >/dev/null 2>&1 &)
4422
4576
  ${MARKER_END}`;
4423
4577
  async function hooksDir(cwd) {
4424
- const gitDir = path12.join(cwd, ".git");
4578
+ const gitDir = path13.join(cwd, ".git");
4425
4579
  let stats;
4426
4580
  try {
4427
- stats = await fs19.stat(gitDir);
4581
+ stats = await fs20.stat(gitDir);
4428
4582
  } catch {
4429
4583
  throw new Error(`${cwd} is not a git repository (no .git directory) \u2014 run this from the repo root.`);
4430
4584
  }
4431
4585
  if (!stats.isDirectory()) {
4432
- const content = (await fs19.readFile(gitDir, "utf-8")).trim();
4586
+ const content = (await fs20.readFile(gitDir, "utf-8")).trim();
4433
4587
  const match = /^gitdir:\s*(.+)$/.exec(content);
4434
4588
  if (!match) throw new Error(`${gitDir} exists but is neither a directory nor a gitdir pointer.`);
4435
- return path12.resolve(cwd, match[1], "hooks");
4589
+ return path13.resolve(cwd, match[1], "hooks");
4436
4590
  }
4437
- return path12.join(gitDir, "hooks");
4591
+ return path13.join(gitDir, "hooks");
4438
4592
  }
4439
4593
  async function runHookInstall(cwd = process.cwd()) {
4440
4594
  const dir = await hooksDir(cwd);
4441
- await fs19.mkdir(dir, { recursive: true });
4595
+ await fs20.mkdir(dir, { recursive: true });
4442
4596
  const results = [];
4443
4597
  for (const hook of HOOK_NAMES) {
4444
- const hookPath = path12.join(dir, hook);
4598
+ const hookPath = path13.join(dir, hook);
4445
4599
  let existing = null;
4446
4600
  try {
4447
- existing = await fs19.readFile(hookPath, "utf-8");
4601
+ existing = await fs20.readFile(hookPath, "utf-8");
4448
4602
  } catch {
4449
4603
  }
4450
4604
  if (existing === null) {
4451
- await fs19.writeFile(hookPath, `#!/bin/sh
4605
+ await fs20.writeFile(hookPath, `#!/bin/sh
4452
4606
  ${HOOK_BLOCK}
4453
4607
  `, { mode: 493 });
4454
4608
  results.push({ hook, path: hookPath, action: "created" });
@@ -4456,9 +4610,9 @@ ${HOOK_BLOCK}
4456
4610
  results.push({ hook, path: hookPath, action: "already-installed" });
4457
4611
  } else {
4458
4612
  const separator = existing.endsWith("\n") ? "" : "\n";
4459
- await fs19.writeFile(hookPath, `${existing}${separator}${HOOK_BLOCK}
4613
+ await fs20.writeFile(hookPath, `${existing}${separator}${HOOK_BLOCK}
4460
4614
  `, "utf-8");
4461
- await fs19.chmod(hookPath, 493);
4615
+ await fs20.chmod(hookPath, 493);
4462
4616
  results.push({ hook, path: hookPath, action: "appended" });
4463
4617
  }
4464
4618
  }
@@ -4474,10 +4628,10 @@ async function runHookUninstall(cwd = process.cwd()) {
4474
4628
  const dir = await hooksDir(cwd);
4475
4629
  const results = [];
4476
4630
  for (const hook of HOOK_NAMES) {
4477
- const hookPath = path12.join(dir, hook);
4631
+ const hookPath = path13.join(dir, hook);
4478
4632
  let existing = null;
4479
4633
  try {
4480
- existing = await fs19.readFile(hookPath, "utf-8");
4634
+ existing = await fs20.readFile(hookPath, "utf-8");
4481
4635
  } catch {
4482
4636
  }
4483
4637
  if (existing === null || !existing.includes(MARKER_BEGIN)) {
@@ -4487,9 +4641,9 @@ async function runHookUninstall(cwd = process.cwd()) {
4487
4641
  const blockPattern = new RegExp(`\\n?${MARKER_BEGIN}[\\s\\S]*?${MARKER_END}\\n?`);
4488
4642
  const remaining = existing.replace(blockPattern, "\n").trim();
4489
4643
  if (remaining === "" || remaining === "#!/bin/sh") {
4490
- await fs19.unlink(hookPath);
4644
+ await fs20.unlink(hookPath);
4491
4645
  } else {
4492
- await fs19.writeFile(hookPath, `${remaining}
4646
+ await fs20.writeFile(hookPath, `${remaining}
4493
4647
  `, "utf-8");
4494
4648
  }
4495
4649
  results.push({ hook, path: hookPath, action: "removed" });
@@ -4503,8 +4657,8 @@ async function runHookUninstall(cwd = process.cwd()) {
4503
4657
  // src/memory.ts
4504
4658
  init_cjs_shims();
4505
4659
  var import_node_crypto4 = require("crypto");
4506
- var fs20 = __toESM(require("fs/promises"), 1);
4507
- var path13 = __toESM(require("path"), 1);
4660
+ var fs21 = __toESM(require("fs/promises"), 1);
4661
+ var path14 = __toESM(require("path"), 1);
4508
4662
  var OUTCOMES = /* @__PURE__ */ new Set(["useful", "dead_end", "corrected"]);
4509
4663
  var TYPES = /* @__PURE__ */ new Set(["query", "path", "explain", "affected"]);
4510
4664
  function validateResult(value) {
@@ -4522,25 +4676,25 @@ function validateResult(value) {
4522
4676
  }
4523
4677
  async function saveResult(entry, memoryDir, now = /* @__PURE__ */ new Date()) {
4524
4678
  validateResult(entry);
4525
- await fs20.mkdir(memoryDir, { recursive: true });
4679
+ await fs21.mkdir(memoryDir, { recursive: true });
4526
4680
  const saved = { ...entry, savedAt: now.toISOString() };
4527
4681
  const hash = (0, import_node_crypto4.createHash)("sha256").update(JSON.stringify(saved)).digest("hex").slice(0, 8);
4528
4682
  const stamp = saved.savedAt.replace(/[:.]/g, "-");
4529
- const filePath = path13.join(memoryDir, `result-${stamp}-${hash}.json`);
4530
- await fs20.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
4683
+ const filePath = path14.join(memoryDir, `result-${stamp}-${hash}.json`);
4684
+ await fs21.writeFile(filePath, JSON.stringify(saved, null, 2), "utf-8");
4531
4685
  return filePath;
4532
4686
  }
4533
4687
  async function loadResults(memoryDir) {
4534
4688
  let files;
4535
4689
  try {
4536
- files = (await fs20.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
4690
+ files = (await fs21.readdir(memoryDir)).filter((f) => f.startsWith("result-") && f.endsWith(".json"));
4537
4691
  } catch {
4538
4692
  return [];
4539
4693
  }
4540
4694
  const results = [];
4541
4695
  for (const file of files.sort((a, b) => a.localeCompare(b))) {
4542
4696
  try {
4543
- const parsed = JSON.parse(await fs20.readFile(path13.join(memoryDir, file), "utf-8"));
4697
+ const parsed = JSON.parse(await fs21.readFile(path14.join(memoryDir, file), "utf-8"));
4544
4698
  if (parsed.question && parsed.savedAt) results.push(parsed);
4545
4699
  } catch {
4546
4700
  }
@@ -4617,18 +4771,18 @@ function renderLessons(results, options = {}) {
4617
4771
  // src/cli/commands/install.ts
4618
4772
  init_cjs_shims();
4619
4773
  var import_node_module3 = require("module");
4620
- var fs21 = __toESM(require("fs/promises"), 1);
4621
- var path14 = __toESM(require("path"), 1);
4774
+ var fs22 = __toESM(require("fs/promises"), 1);
4775
+ var path15 = __toESM(require("path"), 1);
4622
4776
  var require4 = (0, import_node_module3.createRequire)(importMetaUrl);
4623
4777
  function packageRoot() {
4624
4778
  const packageJsonPath = require4.resolve("@dreamtree-org/graphify/package.json");
4625
- return path14.dirname(packageJsonPath);
4779
+ return path15.dirname(packageJsonPath);
4626
4780
  }
4627
4781
  var MD_MARKER_BEGIN = "<!-- >>> graphify >>> -->";
4628
4782
  var MD_MARKER_END = "<!-- <<< graphify <<< -->";
4629
4783
  async function writeOwnedFile(filePath, content) {
4630
- await fs21.mkdir(path14.dirname(filePath), { recursive: true });
4631
- await fs21.writeFile(filePath, content, "utf-8");
4784
+ await fs22.mkdir(path15.dirname(filePath), { recursive: true });
4785
+ await fs22.writeFile(filePath, content, "utf-8");
4632
4786
  }
4633
4787
  async function upsertMarkedBlock(filePath, content) {
4634
4788
  const block = `${MD_MARKER_BEGIN}
@@ -4636,32 +4790,32 @@ ${content.trim()}
4636
4790
  ${MD_MARKER_END}`;
4637
4791
  let existing = null;
4638
4792
  try {
4639
- existing = await fs21.readFile(filePath, "utf-8");
4793
+ existing = await fs22.readFile(filePath, "utf-8");
4640
4794
  } catch {
4641
4795
  }
4642
4796
  if (existing === null) {
4643
- await fs21.mkdir(path14.dirname(filePath), { recursive: true });
4644
- await fs21.writeFile(filePath, `${block}
4797
+ await fs22.mkdir(path15.dirname(filePath), { recursive: true });
4798
+ await fs22.writeFile(filePath, `${block}
4645
4799
  `, "utf-8");
4646
4800
  return;
4647
4801
  }
4648
4802
  if (existing.includes(MD_MARKER_BEGIN)) {
4649
4803
  const pattern = new RegExp(`${MD_MARKER_BEGIN}[\\s\\S]*?${MD_MARKER_END}`);
4650
- await fs21.writeFile(filePath, existing.replace(pattern, block), "utf-8");
4804
+ await fs22.writeFile(filePath, existing.replace(pattern, block), "utf-8");
4651
4805
  return;
4652
4806
  }
4653
4807
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4654
- await fs21.writeFile(filePath, `${existing}${separator}${block}
4808
+ await fs22.writeFile(filePath, `${existing}${separator}${block}
4655
4809
  `, "utf-8");
4656
4810
  }
4657
4811
  var PLATFORMS = {
4658
4812
  claude: async (cwd, content) => {
4659
- const target = path14.join(cwd, ".claude", "skills", "graphify", "SKILL.md");
4813
+ const target = path15.join(cwd, ".claude", "skills", "graphify", "SKILL.md");
4660
4814
  await writeOwnedFile(target, content);
4661
4815
  return { host: "Claude Code", path: target };
4662
4816
  },
4663
4817
  cursor: async (cwd, content) => {
4664
- const target = path14.join(cwd, ".cursor", "rules", "graphify.mdc");
4818
+ const target = path15.join(cwd, ".cursor", "rules", "graphify.mdc");
4665
4819
  const body = content.replace(/^---[\s\S]*?---\n/, "");
4666
4820
  await writeOwnedFile(
4667
4821
  target,
@@ -4674,30 +4828,30 @@ ${body}`
4674
4828
  return { host: "Cursor", path: target };
4675
4829
  },
4676
4830
  windsurf: async (cwd, content) => {
4677
- const target = path14.join(cwd, ".windsurf", "rules", "graphify.md");
4831
+ const target = path15.join(cwd, ".windsurf", "rules", "graphify.md");
4678
4832
  await writeOwnedFile(target, content.replace(/^---[\s\S]*?---\n/, ""));
4679
4833
  return { host: "Windsurf", path: target };
4680
4834
  },
4681
4835
  cline: async (cwd, content) => {
4682
- const target = path14.join(cwd, ".clinerules", "graphify.md");
4836
+ const target = path15.join(cwd, ".clinerules", "graphify.md");
4683
4837
  await writeOwnedFile(target, content.replace(/^---[\s\S]*?---\n/, ""));
4684
4838
  return { host: "Cline", path: target };
4685
4839
  },
4686
4840
  agents: async (cwd, content) => {
4687
- const target = path14.join(cwd, "AGENTS.md");
4841
+ const target = path15.join(cwd, "AGENTS.md");
4688
4842
  await upsertMarkedBlock(target, content.replace(/^---[\s\S]*?---\n/, ""));
4689
4843
  return { host: "AGENTS.md agents (Codex, opencode, Amp, ...)", path: target };
4690
4844
  },
4691
4845
  gemini: async (cwd, content) => {
4692
- const target = path14.join(cwd, "GEMINI.md");
4846
+ const target = path15.join(cwd, "GEMINI.md");
4693
4847
  await upsertMarkedBlock(target, content.replace(/^---[\s\S]*?---\n/, ""));
4694
4848
  return { host: "Gemini CLI", path: target };
4695
4849
  }
4696
4850
  };
4697
4851
  var SUPPORTED_PLATFORMS = Object.keys(PLATFORMS).sort((a, b) => a.localeCompare(b));
4698
4852
  async function runInstall(platforms = ["claude"], cwd = process.cwd()) {
4699
- const sourcePath = path14.join(packageRoot(), "src", "skill", "SKILL.md");
4700
- const content = await fs21.readFile(sourcePath, "utf-8");
4853
+ const sourcePath = path15.join(packageRoot(), "src", "skill", "SKILL.md");
4854
+ const content = await fs22.readFile(sourcePath, "utf-8");
4701
4855
  const requested = platforms.includes("all") ? SUPPORTED_PLATFORMS : platforms;
4702
4856
  const results = [];
4703
4857
  for (const platform of requested) {
@@ -4781,7 +4935,7 @@ function looksLikeGitUrl(value) {
4781
4935
  }
4782
4936
  async function cloneRepo(url) {
4783
4937
  const validated = validateUrl(url);
4784
- const dest = await fs23.mkdtemp(path16.join(os3.tmpdir(), "graphify-clone-"));
4938
+ const dest = await fs24.mkdtemp(path17.join(os3.tmpdir(), "graphify-clone-"));
4785
4939
  await execFileAsync3("git", ["clone", "--depth", "1", validated, dest]);
4786
4940
  return dest;
4787
4941
  }
@@ -4795,7 +4949,7 @@ function clusterOptionsFrom(options) {
4795
4949
  };
4796
4950
  }
4797
4951
  async function runClusterOnly(root, options) {
4798
- const outDir = path16.join(root, "graphify-out");
4952
+ const outDir = path17.join(root, "graphify-out");
4799
4953
  const graph = await loadGraph(outDir);
4800
4954
  cluster(graph, clusterOptionsFrom(options));
4801
4955
  const analysis = analyze(graph);
@@ -4829,9 +4983,9 @@ var program = new import_commander.Command();
4829
4983
  program.name("graphify").description("Turn a folder of code/docs/papers into a queryable knowledge graph.").argument("[path]", "path to scan, or a GitHub URL", ".").option("--mode <mode>", "extraction mode (deep for richer INFERRED edges \u2014 reserved, no-op in v1)").option("--update", "incremental rebuild \u2014 reuse cached extractions for unchanged files").option("--watch", "rebuild on file change").option("--cluster-only", "rerun clustering on an existing graph").option("--leiden", "use Leiden instead of Louvain for community detection (v2 \u2014 better community quality)").option("--max-iterations <n>", "Leiden only: cap on outer iterations for very large graphs", Number).option("--no-viz", "skip graph.html").option("--svg", "also export graph.svg (not implemented in v1)").option("--graphml", "also export graph.graphml (not implemented in v1)").option("--neo4j", "generate graphify-out/cypher.txt for Neo4j (not implemented in v1)").option("--mcp", "start MCP stdio server instead of running the pipeline").option("--mysql <dsn>", "also extract a MySQL schema (mysql://user:pass@host:port/db) into the graph").action(async (targetArg, options) => {
4830
4984
  try {
4831
4985
  if (options.mcp) {
4832
- const outDir = path16.resolve(targetArg === "." ? process.cwd() : targetArg, "graphify-out");
4986
+ const outDir = path17.resolve(targetArg === "." ? process.cwd() : targetArg, "graphify-out");
4833
4987
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4834
- await startServer2(path16.join(outDir, "graph.json"));
4988
+ await startServer2(path17.join(outDir, "graph.json"));
4835
4989
  return;
4836
4990
  }
4837
4991
  let root = targetArg;
@@ -4839,7 +4993,7 @@ program.name("graphify").description("Turn a folder of code/docs/papers into a q
4839
4993
  console.error(`Cloning ${targetArg} ...`);
4840
4994
  root = await cloneRepo(targetArg);
4841
4995
  }
4842
- root = path16.resolve(root);
4996
+ root = path17.resolve(root);
4843
4997
  if (options.mode) {
4844
4998
  console.error(`--mode ${options.mode} is reserved for a future richer-INFERRED-edges pass \u2014 currently a no-op.`);
4845
4999
  }
@@ -4993,7 +5147,7 @@ program.command("merge <dirs...>").description("one-shot merge of two or more bu
4993
5147
  });
4994
5148
  program.command("save-result").description("save a Q&A result to graphify-out/memory/ for the graph feedback loop").requiredOption("--question <q>", "the question that was asked").requiredOption("--answer <a>", "the answer that was given").option("--nodes <ids...>", "node ids/labels cited in the answer", []).option("--type <t>", "query|path|explain|affected", "query").option("--outcome <o>", "useful|dead_end|corrected", "useful").option("--correction <text>", "what the right answer was (pairs with --outcome corrected)").action(async (options) => {
4995
5149
  try {
4996
- const memoryDir = path16.join(process.cwd(), "graphify-out", "memory");
5150
+ const memoryDir = path17.join(process.cwd(), "graphify-out", "memory");
4997
5151
  const file = await saveResult(
4998
5152
  {
4999
5153
  question: options.question,
@@ -5013,12 +5167,12 @@ program.command("save-result").description("save a Q&A result to graphify-out/me
5013
5167
  });
5014
5168
  program.command("reflect").description("aggregate graphify-out/memory/ into a recency-weighted lessons doc").option("--half-life-days <n>", "a result loses half its weight every N days (default 30)", Number).option("--out <file>", "output path (default graphify-out/reflections/LESSONS.md)").action(async (options) => {
5015
5169
  try {
5016
- const memoryDir = path16.join(process.cwd(), "graphify-out", "memory");
5170
+ const memoryDir = path17.join(process.cwd(), "graphify-out", "memory");
5017
5171
  const results = await loadResults(memoryDir);
5018
5172
  const lessons = renderLessons(results, { halfLifeDays: options.halfLifeDays });
5019
- const outPath = path16.resolve(options.out ?? path16.join(process.cwd(), "graphify-out", "reflections", "LESSONS.md"));
5020
- await fs23.mkdir(path16.dirname(outPath), { recursive: true });
5021
- await fs23.writeFile(outPath, lessons, "utf-8");
5173
+ const outPath = path17.resolve(options.out ?? path17.join(process.cwd(), "graphify-out", "reflections", "LESSONS.md"));
5174
+ await fs24.mkdir(path17.dirname(outPath), { recursive: true });
5175
+ await fs24.writeFile(outPath, lessons, "utf-8");
5022
5176
  console.log(`Reflected ${results.length} result(s) -> ${outPath}`);
5023
5177
  } catch (error) {
5024
5178
  console.error(`graphify reflect: ${error.message}`);