@neat.is/core 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -817,8 +817,8 @@ init_cjs_shims();
817
817
 
818
818
  // src/ingest.ts
819
819
  init_cjs_shims();
820
- var import_node_fs6 = require("fs");
821
- var import_node_path7 = __toESM(require("path"), 1);
820
+ var import_node_fs7 = require("fs");
821
+ var import_node_path8 = __toESM(require("path"), 1);
822
822
  var sourceMapJs = __toESM(require("source-map-js"), 1);
823
823
 
824
824
  // src/policy.ts
@@ -2167,16 +2167,16 @@ var PolicyViolationsLog = class {
2167
2167
  };
2168
2168
 
2169
2169
  // src/ingest.ts
2170
- var import_types7 = require("@neat.is/types");
2170
+ var import_types8 = require("@neat.is/types");
2171
2171
 
2172
2172
  // src/extract/routes.ts
2173
2173
  init_cjs_shims();
2174
- var import_node_path6 = __toESM(require("path"), 1);
2175
- var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2176
- var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2177
- var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2178
- var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1);
2179
- var import_types5 = require("@neat.is/types");
2174
+ var import_node_path7 = __toESM(require("path"), 1);
2175
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
2176
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
2177
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
2178
+ var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
2179
+ var import_types6 = require("@neat.is/types");
2180
2180
 
2181
2181
  // src/extract/shared.ts
2182
2182
  init_cjs_shims();
@@ -2547,7 +2547,15 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
2547
2547
  return { fileNodeId, nodesAdded, edgesAdded };
2548
2548
  }
2549
2549
 
2550
- // src/extract/routes.ts
2550
+ // src/extract/imports.ts
2551
+ init_cjs_shims();
2552
+ var import_node_path6 = __toESM(require("path"), 1);
2553
+ var import_node_fs6 = require("fs");
2554
+ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2555
+ var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2556
+ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2557
+ var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1);
2558
+ var import_types5 = require("@neat.is/types");
2551
2559
  var PARSE_CHUNK = 16384;
2552
2560
  function parseSource(parser, source) {
2553
2561
  return parser.parse(
@@ -2569,155 +2577,532 @@ function makeGoParser() {
2569
2577
  p.setLanguage(import_tree_sitter_go.default);
2570
2578
  return p;
2571
2579
  }
2572
- var ROUTER_METHODS = /* @__PURE__ */ new Set([
2573
- "get",
2574
- "post",
2575
- "put",
2576
- "patch",
2577
- "delete",
2578
- "options",
2579
- "head",
2580
- "all"
2581
- ]);
2582
- var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2583
- var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
2584
- function ginRoutesFromSource(source, parser) {
2585
- const tree = parseSource(parser, source);
2586
- const prefixes = /* @__PURE__ */ new Map();
2587
- const out = [];
2588
- walk(tree.rootNode, (node) => {
2589
- if (node.type === "short_var_declaration" || node.type === "var_spec") {
2590
- const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
2591
- const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
2592
- if (name && value?.type === "call_expression") {
2593
- const fn2 = value.childForFieldName("function");
2594
- const field = fn2?.childForFieldName("field")?.text;
2595
- const first2 = value.childForFieldName("arguments")?.namedChild(0);
2596
- if (field === "Group" && first2?.type === "interpreted_string_literal") {
2597
- prefixes.set(name, first2.text.slice(1, -1));
2598
- }
2599
- }
2600
- return;
2601
- }
2602
- if (node.type !== "call_expression") return;
2603
- const fn = node.childForFieldName("function");
2604
- if (fn?.type !== "selector_expression") return;
2605
- const method = fn.childForFieldName("field")?.text?.toUpperCase();
2606
- if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
2607
- const receiver = fn.childForFieldName("operand")?.text ?? "";
2608
- const first = node.childForFieldName("arguments")?.namedChild(0);
2609
- if (first?.type !== "interpreted_string_literal") return;
2610
- const leaf = first.text.slice(1, -1);
2611
- out.push({
2612
- method: method === "ALL" ? "ALL" : method,
2613
- pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
2614
- line: node.startPosition.row + 1,
2615
- framework: "gin"
2616
- });
2617
- });
2618
- return out;
2619
- }
2620
- var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
2621
- var NESTJS_METHODS = /* @__PURE__ */ new Map([
2622
- ["Get", "GET"],
2623
- ["Post", "POST"],
2624
- ["Put", "PUT"],
2625
- ["Patch", "PATCH"],
2626
- ["Delete", "DELETE"],
2627
- ["Options", "OPTIONS"],
2628
- ["Head", "HEAD"],
2629
- ["All", "ALL"]
2630
- ]);
2631
- function canonicalizeTemplate(raw) {
2632
- let p = raw.split("?")[0].split("#")[0];
2633
- if (!p.startsWith("/")) p = "/" + p;
2634
- if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
2635
- return p;
2636
- }
2637
- function isDynamicSegment(seg) {
2638
- if (seg.length === 0) return false;
2639
- if (seg.includes(":")) return true;
2640
- if (seg.startsWith("{") || seg.startsWith("[")) return true;
2641
- if (/^\d+$/.test(seg)) return true;
2642
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return true;
2643
- if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
2644
- return false;
2580
+ function stringLiteralText(node) {
2581
+ for (let i = 0; i < node.childCount; i++) {
2582
+ const child = node.child(i);
2583
+ if (child?.type === "string_fragment") return child.text;
2584
+ }
2585
+ const raw = node.text;
2586
+ if (raw.length >= 2) return raw.slice(1, -1);
2587
+ return raw.length === 0 ? null : "";
2645
2588
  }
2646
- function normalizePathTemplate(raw) {
2647
- const canonical = canonicalizeTemplate(raw);
2648
- const segments = canonical.split("/").filter((s) => s.length > 0);
2649
- const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
2650
- return "/" + normalised.join("/");
2589
+ function clipSnippet(text) {
2590
+ const oneLine = text.split("\n")[0] ?? text;
2591
+ return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
2651
2592
  }
2652
- function walk(node, visit) {
2653
- visit(node);
2593
+ function collectGoImports(node, out) {
2594
+ if (node.type === "import_spec") {
2595
+ const pathNode = node.childForFieldName("path");
2596
+ if (pathNode) {
2597
+ const specifier = pathNode.text.replace(/^`|`$/g, "").replace(/^"|"$/g, "");
2598
+ if (specifier) out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2599
+ }
2600
+ return;
2601
+ }
2654
2602
  for (let i = 0; i < node.namedChildCount; i++) {
2655
2603
  const child = node.namedChild(i);
2656
- if (child) walk(child, visit);
2604
+ if (child) collectGoImports(child, out);
2657
2605
  }
2658
2606
  }
2659
- function staticStringText(node) {
2660
- if (node.type === "string") {
2661
- for (let i = 0; i < node.namedChildCount; i++) {
2662
- const child = node.namedChild(i);
2663
- if (child?.type === "string_fragment") return child.text;
2607
+ function collectJsImports(node, out) {
2608
+ if (node.type === "import_statement") {
2609
+ const source = node.childForFieldName("source");
2610
+ if (source) {
2611
+ const specifier = stringLiteralText(source);
2612
+ if (specifier) {
2613
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2614
+ }
2664
2615
  }
2665
- return "";
2616
+ return;
2666
2617
  }
2667
- if (node.type === "template_string") {
2668
- for (let i = 0; i < node.namedChildCount; i++) {
2669
- if (node.namedChild(i)?.type === "template_substitution") return null;
2618
+ if (node.type === "call_expression") {
2619
+ const fn = node.childForFieldName("function");
2620
+ if (fn?.type === "identifier" && fn.text === "require") {
2621
+ const args = node.childForFieldName("arguments");
2622
+ const firstArg = args?.namedChild(0);
2623
+ if (firstArg?.type === "string") {
2624
+ const specifier = stringLiteralText(firstArg);
2625
+ if (specifier) {
2626
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2627
+ }
2628
+ }
2670
2629
  }
2671
- const raw = node.text;
2672
- return raw.length >= 2 ? raw.slice(1, -1) : "";
2673
2630
  }
2674
- return null;
2631
+ for (let i = 0; i < node.namedChildCount; i++) {
2632
+ const child = node.namedChild(i);
2633
+ if (child) collectJsImports(child, out);
2634
+ }
2675
2635
  }
2676
- function objectStringProp(objNode, key) {
2677
- for (let i = 0; i < objNode.namedChildCount; i++) {
2678
- const pair = objNode.namedChild(i);
2679
- if (!pair || pair.type !== "pair") continue;
2680
- const k = pair.childForFieldName("key");
2681
- if (!k) continue;
2682
- const kText = k.type === "string" ? staticStringText(k) : k.text;
2683
- if (kText !== key) continue;
2684
- const v = pair.childForFieldName("value");
2685
- if (v) return staticStringText(v);
2636
+ function collectImportedNames(node, out) {
2637
+ if (node.type === "aliased_import") {
2638
+ const nameNode = node.childForFieldName("name");
2639
+ if (nameNode) out.push(nameNode.text);
2640
+ return;
2641
+ }
2642
+ if (node.type === "dotted_name") {
2643
+ out.push(node.text);
2644
+ return;
2645
+ }
2646
+ for (let i = 0; i < node.namedChildCount; i++) {
2647
+ const child = node.namedChild(i);
2648
+ if (child) collectImportedNames(child, out);
2686
2649
  }
2687
- return null;
2688
2650
  }
2689
- function fastifyRouteMethods(objNode) {
2690
- for (let i = 0; i < objNode.namedChildCount; i++) {
2691
- const pair = objNode.namedChild(i);
2692
- if (!pair || pair.type !== "pair") continue;
2693
- const k = pair.childForFieldName("key");
2694
- const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
2695
- if (kText !== "method") continue;
2696
- const v = pair.childForFieldName("value");
2697
- if (!v) return [];
2698
- if (v.type === "string" || v.type === "template_string") {
2699
- const s = staticStringText(v);
2700
- return s ? [s.toUpperCase()] : [];
2701
- }
2702
- if (v.type === "array") {
2703
- const out = [];
2704
- for (let j = 0; j < v.namedChildCount; j++) {
2705
- const el = v.namedChild(j);
2706
- if (el && (el.type === "string" || el.type === "template_string")) {
2707
- const s = staticStringText(el);
2708
- if (s) out.push(s.toUpperCase());
2651
+ function collectPyImports(node, out) {
2652
+ if (node.type === "import_from_statement") {
2653
+ let level = 0;
2654
+ let modulePath = "";
2655
+ const names = [];
2656
+ let pastFrom = false;
2657
+ let pastImport = false;
2658
+ for (let i = 0; i < node.childCount; i++) {
2659
+ const child = node.child(i);
2660
+ if (!child) continue;
2661
+ if (!pastFrom) {
2662
+ if (child.type === "from") pastFrom = true;
2663
+ continue;
2664
+ }
2665
+ if (!pastImport) {
2666
+ if (child.type === "import") {
2667
+ pastImport = true;
2668
+ continue;
2669
+ }
2670
+ if (child.type === "relative_import") {
2671
+ for (let j = 0; j < child.childCount; j++) {
2672
+ const rc = child.child(j);
2673
+ if (!rc) continue;
2674
+ if (rc.type === "import_prefix") {
2675
+ for (let k = 0; k < rc.childCount; k++) {
2676
+ if (rc.child(k)?.type === ".") level++;
2677
+ }
2678
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
2679
+ }
2680
+ } else if (child.type === "dotted_name") {
2681
+ modulePath = child.text;
2709
2682
  }
2683
+ continue;
2710
2684
  }
2711
- return out;
2685
+ collectImportedNames(child, names);
2686
+ }
2687
+ if (level > 0 || modulePath) {
2688
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2712
2689
  }
2713
2690
  }
2714
- return [];
2691
+ for (let i = 0; i < node.namedChildCount; i++) {
2692
+ const child = node.namedChild(i);
2693
+ if (child) collectPyImports(child, out);
2694
+ }
2715
2695
  }
2716
- function nestDecoratorImports(root) {
2717
- const imports = /* @__PURE__ */ new Map();
2718
- walk(root, (node) => {
2719
- if (node.type !== "import_statement") return;
2720
- const source = node.childForFieldName("source");
2696
+ async function fileExists(p) {
2697
+ try {
2698
+ await import_node_fs6.promises.access(p);
2699
+ return true;
2700
+ } catch {
2701
+ return false;
2702
+ }
2703
+ }
2704
+ function isWithinServiceDir(candidate, serviceDir) {
2705
+ const rel = import_node_path6.default.relative(serviceDir, candidate);
2706
+ return rel !== "" && !rel.startsWith("..") && !import_node_path6.default.isAbsolute(rel);
2707
+ }
2708
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2709
+ var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
2710
+ async function firstExistingCandidate(base, serviceDir) {
2711
+ for (const ext of JS_EXTENSIONS) {
2712
+ const candidate = base + ext;
2713
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2714
+ return toPosix(import_node_path6.default.relative(serviceDir, candidate));
2715
+ }
2716
+ }
2717
+ for (const indexFile of JS_INDEX_FILES) {
2718
+ const candidate = import_node_path6.default.join(base, indexFile);
2719
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2720
+ return toPosix(import_node_path6.default.relative(serviceDir, candidate));
2721
+ }
2722
+ }
2723
+ return null;
2724
+ }
2725
+ async function loadTsPathConfig(serviceDir) {
2726
+ const tsconfigPath = import_node_path6.default.join(serviceDir, "tsconfig.json");
2727
+ let raw;
2728
+ try {
2729
+ raw = await import_node_fs6.promises.readFile(tsconfigPath, "utf8");
2730
+ } catch {
2731
+ return null;
2732
+ }
2733
+ try {
2734
+ const parsed = JSON.parse(raw);
2735
+ const paths = parsed.compilerOptions?.paths;
2736
+ if (!paths || Object.keys(paths).length === 0) return null;
2737
+ const baseUrl = parsed.compilerOptions?.baseUrl;
2738
+ return { paths, baseDir: baseUrl ? import_node_path6.default.resolve(serviceDir, baseUrl) : serviceDir };
2739
+ } catch (err) {
2740
+ recordExtractionError("import alias resolution", tsconfigPath, err);
2741
+ return null;
2742
+ }
2743
+ }
2744
+ async function resolveTsAlias(specifier, config, serviceDir) {
2745
+ for (const [pattern, targets] of Object.entries(config.paths)) {
2746
+ let suffix = null;
2747
+ if (pattern === specifier) {
2748
+ suffix = "";
2749
+ } else if (pattern.endsWith("/*")) {
2750
+ const prefix = pattern.slice(0, -1);
2751
+ if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
2752
+ }
2753
+ if (suffix === null) continue;
2754
+ for (const target of targets) {
2755
+ const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
2756
+ const resolvedBase = import_node_path6.default.resolve(config.baseDir, targetBase, suffix);
2757
+ const hit = await firstExistingCandidate(resolvedBase, serviceDir);
2758
+ if (hit) return hit;
2759
+ if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
2760
+ return toPosix(import_node_path6.default.relative(serviceDir, resolvedBase));
2761
+ }
2762
+ }
2763
+ }
2764
+ return null;
2765
+ }
2766
+ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
2767
+ if (!specifier) return null;
2768
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
2769
+ const base = import_node_path6.default.resolve(importerDir, specifier);
2770
+ const ext = import_node_path6.default.extname(specifier);
2771
+ if (ext) {
2772
+ if (ext === ".js" || ext === ".jsx") {
2773
+ const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
2774
+ const tsSibling = base.slice(0, -ext.length) + tsExt;
2775
+ if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
2776
+ return toPosix(import_node_path6.default.relative(serviceDir, tsSibling));
2777
+ }
2778
+ }
2779
+ if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
2780
+ return toPosix(import_node_path6.default.relative(serviceDir, base));
2781
+ }
2782
+ if (!JS_EXTENSIONS.includes(ext)) {
2783
+ return firstExistingCandidate(base, serviceDir);
2784
+ }
2785
+ return null;
2786
+ }
2787
+ return firstExistingCandidate(base, serviceDir);
2788
+ }
2789
+ if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
2790
+ return null;
2791
+ }
2792
+ async function resolvePyImport(imp, importerPath, serviceDir) {
2793
+ let baseDir;
2794
+ if (imp.level > 0) {
2795
+ baseDir = import_node_path6.default.dirname(importerPath);
2796
+ for (let i = 1; i < imp.level; i++) baseDir = import_node_path6.default.dirname(baseDir);
2797
+ } else {
2798
+ baseDir = serviceDir;
2799
+ }
2800
+ const moduleBase = imp.modulePath ? import_node_path6.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
2801
+ const resolved = /* @__PURE__ */ new Set();
2802
+ let needModuleFile = imp.names.length === 0;
2803
+ for (const name of imp.names) {
2804
+ const submoduleFile = import_node_path6.default.join(moduleBase, `${name}.py`);
2805
+ const subpackageInit = import_node_path6.default.join(moduleBase, name, "__init__.py");
2806
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
2807
+ resolved.add(toPosix(import_node_path6.default.relative(serviceDir, submoduleFile)));
2808
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
2809
+ resolved.add(toPosix(import_node_path6.default.relative(serviceDir, subpackageInit)));
2810
+ } else {
2811
+ needModuleFile = true;
2812
+ }
2813
+ }
2814
+ if (needModuleFile) {
2815
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path6.default.join(moduleBase, "__init__.py")] : [import_node_path6.default.join(moduleBase, "__init__.py")];
2816
+ for (const candidate of moduleFileCandidates) {
2817
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2818
+ resolved.add(toPosix(import_node_path6.default.relative(serviceDir, candidate)));
2819
+ break;
2820
+ }
2821
+ }
2822
+ }
2823
+ return [...resolved];
2824
+ }
2825
+ async function resolveGoImport(specifier, modulePath, serviceDir) {
2826
+ if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
2827
+ const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
2828
+ const dir = import_node_path6.default.join(serviceDir, suffix);
2829
+ const entries = await import_node_fs6.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
2830
+ const candidates = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".go") && !entry.name.endsWith("_test.go")).map((entry) => import_node_path6.default.join(dir, entry.name));
2831
+ if (candidates.length !== 1) return null;
2832
+ return toPosix(import_node_path6.default.relative(serviceDir, candidates[0]));
2833
+ }
2834
+ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
2835
+ const importeeFileId = (0, import_types5.fileId)(serviceName, importeeRelPath);
2836
+ if (!graph.hasNode(importeeFileId)) return 0;
2837
+ const edgeId = (0, import_types5.extractedEdgeId)(importerFileId, importeeFileId, import_types5.EdgeType.IMPORTS);
2838
+ if (graph.hasEdge(edgeId)) return 0;
2839
+ const edge = {
2840
+ id: edgeId,
2841
+ source: importerFileId,
2842
+ target: importeeFileId,
2843
+ type: import_types5.EdgeType.IMPORTS,
2844
+ provenance: import_types5.Provenance.EXTRACTED,
2845
+ confidence: (0, import_types5.confidenceForExtracted)("structural"),
2846
+ evidence: { file: importerRelPath, line, snippet: snippet2 }
2847
+ };
2848
+ graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
2849
+ return 1;
2850
+ }
2851
+ async function addImports(graph, services) {
2852
+ const jsParser = makeJsParser();
2853
+ const pyParser = makePyParser();
2854
+ const goParser = makeGoParser();
2855
+ let edgesAdded = 0;
2856
+ for (const service of services) {
2857
+ const tsPaths = await loadTsPathConfig(service.dir);
2858
+ const files = await loadSourceFiles(service.dir);
2859
+ for (const file of files) {
2860
+ if (isTestPath(file.path)) continue;
2861
+ const relFile = toPosix(import_node_path6.default.relative(service.dir, file.path));
2862
+ const importerFileId = (0, import_types5.fileId)(service.pkg.name, relFile);
2863
+ const isPython = import_node_path6.default.extname(file.path) === ".py";
2864
+ const isGo = import_node_path6.default.extname(file.path) === ".go";
2865
+ if (isGo) {
2866
+ let goImports = [];
2867
+ try {
2868
+ const tree = parseSource(goParser, file.content);
2869
+ collectGoImports(tree.rootNode, goImports);
2870
+ } catch (err) {
2871
+ recordExtractionError("import extraction", file.path, err);
2872
+ continue;
2873
+ }
2874
+ const goMod = await import_node_fs6.promises.readFile(import_node_path6.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
2875
+ const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
2876
+ if (!modulePath) continue;
2877
+ for (const imp of goImports) {
2878
+ const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
2879
+ if (!resolved) continue;
2880
+ edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
2881
+ }
2882
+ continue;
2883
+ }
2884
+ if (isPython) {
2885
+ let pyImports = [];
2886
+ try {
2887
+ const tree = parseSource(pyParser, file.content);
2888
+ collectPyImports(tree.rootNode, pyImports);
2889
+ } catch (err) {
2890
+ recordExtractionError("import extraction", file.path, err);
2891
+ continue;
2892
+ }
2893
+ for (const imp of pyImports) {
2894
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
2895
+ for (const resolved of resolvedPaths) {
2896
+ edgesAdded += emitImportEdge(
2897
+ graph,
2898
+ service.pkg.name,
2899
+ importerFileId,
2900
+ relFile,
2901
+ resolved,
2902
+ imp.line,
2903
+ imp.snippet
2904
+ );
2905
+ }
2906
+ }
2907
+ continue;
2908
+ }
2909
+ let jsImports = [];
2910
+ try {
2911
+ const tree = parseSource(jsParser, file.content);
2912
+ collectJsImports(tree.rootNode, jsImports);
2913
+ } catch (err) {
2914
+ recordExtractionError("import extraction", file.path, err);
2915
+ continue;
2916
+ }
2917
+ for (const imp of jsImports) {
2918
+ const resolved = await resolveJsImport(imp.specifier, import_node_path6.default.dirname(file.path), service.dir, tsPaths);
2919
+ if (!resolved) continue;
2920
+ edgesAdded += emitImportEdge(
2921
+ graph,
2922
+ service.pkg.name,
2923
+ importerFileId,
2924
+ relFile,
2925
+ resolved,
2926
+ imp.line,
2927
+ imp.snippet
2928
+ );
2929
+ }
2930
+ }
2931
+ }
2932
+ return { nodesAdded: 0, edgesAdded };
2933
+ }
2934
+
2935
+ // src/extract/routes.ts
2936
+ var PARSE_CHUNK2 = 16384;
2937
+ function parseSource2(parser, source) {
2938
+ return parser.parse(
2939
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
2940
+ );
2941
+ }
2942
+ function makeJsParser2() {
2943
+ const p = new import_tree_sitter2.default();
2944
+ p.setLanguage(import_tree_sitter_javascript2.default);
2945
+ return p;
2946
+ }
2947
+ function makePyParser2() {
2948
+ const p = new import_tree_sitter2.default();
2949
+ p.setLanguage(import_tree_sitter_python2.default);
2950
+ return p;
2951
+ }
2952
+ function makeGoParser2() {
2953
+ const p = new import_tree_sitter2.default();
2954
+ p.setLanguage(import_tree_sitter_go2.default);
2955
+ return p;
2956
+ }
2957
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
2958
+ "get",
2959
+ "post",
2960
+ "put",
2961
+ "patch",
2962
+ "delete",
2963
+ "options",
2964
+ "head",
2965
+ "all"
2966
+ ]);
2967
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2968
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
2969
+ function ginRoutesFromSource(source, parser) {
2970
+ const tree = parseSource2(parser, source);
2971
+ const prefixes = /* @__PURE__ */ new Map();
2972
+ const out = [];
2973
+ walk(tree.rootNode, (node) => {
2974
+ if (node.type === "short_var_declaration" || node.type === "var_spec") {
2975
+ const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
2976
+ const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
2977
+ if (name && value?.type === "call_expression") {
2978
+ const fn2 = value.childForFieldName("function");
2979
+ const field = fn2?.childForFieldName("field")?.text;
2980
+ const first2 = value.childForFieldName("arguments")?.namedChild(0);
2981
+ if (field === "Group" && first2?.type === "interpreted_string_literal") {
2982
+ prefixes.set(name, first2.text.slice(1, -1));
2983
+ }
2984
+ }
2985
+ return;
2986
+ }
2987
+ if (node.type !== "call_expression") return;
2988
+ const fn = node.childForFieldName("function");
2989
+ if (fn?.type !== "selector_expression") return;
2990
+ const method = fn.childForFieldName("field")?.text?.toUpperCase();
2991
+ if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
2992
+ const receiver = fn.childForFieldName("operand")?.text ?? "";
2993
+ const first = node.childForFieldName("arguments")?.namedChild(0);
2994
+ if (first?.type !== "interpreted_string_literal") return;
2995
+ const leaf = first.text.slice(1, -1);
2996
+ out.push({
2997
+ method: method === "ALL" ? "ALL" : method,
2998
+ pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
2999
+ line: node.startPosition.row + 1,
3000
+ framework: "gin"
3001
+ });
3002
+ });
3003
+ return out;
3004
+ }
3005
+ var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
3006
+ var NESTJS_METHODS = /* @__PURE__ */ new Map([
3007
+ ["Get", "GET"],
3008
+ ["Post", "POST"],
3009
+ ["Put", "PUT"],
3010
+ ["Patch", "PATCH"],
3011
+ ["Delete", "DELETE"],
3012
+ ["Options", "OPTIONS"],
3013
+ ["Head", "HEAD"],
3014
+ ["All", "ALL"]
3015
+ ]);
3016
+ function canonicalizeTemplate(raw) {
3017
+ let p = raw.split("?")[0].split("#")[0];
3018
+ if (!p.startsWith("/")) p = "/" + p;
3019
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
3020
+ return p;
3021
+ }
3022
+ function isDynamicSegment(seg) {
3023
+ if (seg.length === 0) return false;
3024
+ if (seg.includes(":")) return true;
3025
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
3026
+ if (/^\d+$/.test(seg)) return true;
3027
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return true;
3028
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
3029
+ return false;
3030
+ }
3031
+ function normalizePathTemplate(raw) {
3032
+ const canonical = canonicalizeTemplate(raw);
3033
+ const segments = canonical.split("/").filter((s) => s.length > 0);
3034
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
3035
+ return "/" + normalised.join("/");
3036
+ }
3037
+ function walk(node, visit) {
3038
+ visit(node);
3039
+ for (let i = 0; i < node.namedChildCount; i++) {
3040
+ const child = node.namedChild(i);
3041
+ if (child) walk(child, visit);
3042
+ }
3043
+ }
3044
+ function staticStringText(node) {
3045
+ if (node.type === "string") {
3046
+ for (let i = 0; i < node.namedChildCount; i++) {
3047
+ const child = node.namedChild(i);
3048
+ if (child?.type === "string_fragment") return child.text;
3049
+ }
3050
+ return "";
3051
+ }
3052
+ if (node.type === "template_string") {
3053
+ for (let i = 0; i < node.namedChildCount; i++) {
3054
+ if (node.namedChild(i)?.type === "template_substitution") return null;
3055
+ }
3056
+ const raw = node.text;
3057
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
3058
+ }
3059
+ return null;
3060
+ }
3061
+ function objectStringProp(objNode, key) {
3062
+ for (let i = 0; i < objNode.namedChildCount; i++) {
3063
+ const pair = objNode.namedChild(i);
3064
+ if (!pair || pair.type !== "pair") continue;
3065
+ const k = pair.childForFieldName("key");
3066
+ if (!k) continue;
3067
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
3068
+ if (kText !== key) continue;
3069
+ const v = pair.childForFieldName("value");
3070
+ if (v) return staticStringText(v);
3071
+ }
3072
+ return null;
3073
+ }
3074
+ function fastifyRouteMethods(objNode) {
3075
+ for (let i = 0; i < objNode.namedChildCount; i++) {
3076
+ const pair = objNode.namedChild(i);
3077
+ if (!pair || pair.type !== "pair") continue;
3078
+ const k = pair.childForFieldName("key");
3079
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
3080
+ if (kText !== "method") continue;
3081
+ const v = pair.childForFieldName("value");
3082
+ if (!v) return [];
3083
+ if (v.type === "string" || v.type === "template_string") {
3084
+ const s = staticStringText(v);
3085
+ return s ? [s.toUpperCase()] : [];
3086
+ }
3087
+ if (v.type === "array") {
3088
+ const out = [];
3089
+ for (let j = 0; j < v.namedChildCount; j++) {
3090
+ const el = v.namedChild(j);
3091
+ if (el && (el.type === "string" || el.type === "template_string")) {
3092
+ const s = staticStringText(el);
3093
+ if (s) out.push(s.toUpperCase());
3094
+ }
3095
+ }
3096
+ return out;
3097
+ }
3098
+ }
3099
+ return [];
3100
+ }
3101
+ function nestDecoratorImports(root) {
3102
+ const imports = /* @__PURE__ */ new Map();
3103
+ walk(root, (node) => {
3104
+ if (node.type !== "import_statement") return;
3105
+ const source = node.childForFieldName("source");
2721
3106
  if (!source || staticStringText(source) !== "@nestjs/common") return;
2722
3107
  walk(node, (child) => {
2723
3108
  if (child.type !== "import_specifier") return;
@@ -2763,7 +3148,7 @@ function nestJoinedPath(prefix, leaf) {
2763
3148
  return canonicalizeTemplate(segments.join("/"));
2764
3149
  }
2765
3150
  function nestjsRoutesFromSource(source, parser) {
2766
- const tree = parseSource(parser, source);
3151
+ const tree = parseSource2(parser, source);
2767
3152
  const imports = nestDecoratorImports(tree.rootNode);
2768
3153
  if (![...imports.values()].includes("Controller")) return [];
2769
3154
  const out = [];
@@ -2811,7 +3196,7 @@ function nestjsRoutesFromSource(source, parser) {
2811
3196
  return out;
2812
3197
  }
2813
3198
  function serverRoutesFromSource(source, parser, hasExpress, hasFastify, hasHono = false) {
2814
- const tree = parseSource(parser, source);
3199
+ const tree = parseSource2(parser, source);
2815
3200
  const out = [];
2816
3201
  const framework = hasExpress ? "express" : hasFastify ? "fastify" : hasHono ? "hono" : "unknown";
2817
3202
  walk(tree.rootNode, (node) => {
@@ -2869,7 +3254,7 @@ function isNextPagesApiFile(relFile) {
2869
3254
  if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
2870
3255
  const base = segs[segs.length - 1] ?? "";
2871
3256
  if (/^_(app|document|middleware)\./.test(base)) return false;
2872
- return JS_ROUTE_EXTENSIONS.has(import_node_path6.default.extname(base));
3257
+ return JS_ROUTE_EXTENSIONS.has(import_node_path7.default.extname(base));
2873
3258
  }
2874
3259
  function nextSegment(seg) {
2875
3260
  if (seg.startsWith("(") && seg.endsWith(")")) return null;
@@ -2931,7 +3316,7 @@ function nextAppMethods(root) {
2931
3316
  }
2932
3317
  function nextRoutesFromFile(source, relFile, parser) {
2933
3318
  if (isNextAppRouteFile(relFile)) {
2934
- const tree = parseSource(parser, source);
3319
+ const tree = parseSource2(parser, source);
2935
3320
  const template = nextAppPathTemplate(relFile);
2936
3321
  return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
2937
3322
  method,
@@ -3052,7 +3437,7 @@ function collectMountPrefixes(root, consts) {
3052
3437
  return mounts;
3053
3438
  }
3054
3439
  function pythonRoutesFromSource(source, parser, framework) {
3055
- const tree = parseSource(parser, source);
3440
+ const tree = parseSource2(parser, source);
3056
3441
  const prefixes = collectPythonRouterPrefixes(tree.rootNode);
3057
3442
  const consts = collectStringConstants(tree.rootNode);
3058
3443
  const mounts = collectMountPrefixes(tree.rootNode, consts);
@@ -3092,7 +3477,7 @@ function pythonRoutesFromSource(source, parser, framework) {
3092
3477
  return out;
3093
3478
  }
3094
3479
  function djangoRoutesFromSource(source, parser) {
3095
- const tree = parseSource(parser, source);
3480
+ const tree = parseSource2(parser, source);
3096
3481
  const out = [];
3097
3482
  walk(tree.rootNode, (node) => {
3098
3483
  if (node.type !== "assignment") return;
@@ -3120,10 +3505,320 @@ function djangoRoutesFromSource(source, parser) {
3120
3505
  });
3121
3506
  return out;
3122
3507
  }
3508
+ function namedArgs(argsNode) {
3509
+ const out = [];
3510
+ if (!argsNode) return out;
3511
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
3512
+ const c = argsNode.namedChild(i);
3513
+ if (c && c.type !== "comment") out.push(c);
3514
+ }
3515
+ return out;
3516
+ }
3517
+ function parseUseMount(callNode) {
3518
+ const args = namedArgs(callNode.childForFieldName("arguments"));
3519
+ if (args.length === 0) return null;
3520
+ const first = args[0];
3521
+ const firstStr = first.type === "string" || first.type === "template_string" ? staticStringText(first) : null;
3522
+ if (firstStr !== null && firstStr.startsWith("/")) {
3523
+ const prefix = canonicalizeTemplate(firstStr);
3524
+ const second = args[1];
3525
+ const target = second && second.type === "identifier" ? second.text : null;
3526
+ return { prefix: prefix === "/" ? "" : prefix, target };
3527
+ }
3528
+ if (args.length === 1 && first.type === "identifier") return { prefix: "", target: first.text };
3529
+ return null;
3530
+ }
3531
+ function unwrapRouterExpr(node, expressLocals, routerCtors) {
3532
+ if (node.type === "identifier") return { base: { alias: node.text }, mounts: [] };
3533
+ if (node.type !== "call_expression") return null;
3534
+ const fn = node.childForFieldName("function");
3535
+ if (!fn) return null;
3536
+ if (fn.type === "member_expression") {
3537
+ const prop = fn.childForFieldName("property")?.text;
3538
+ const obj = fn.childForFieldName("object");
3539
+ if (!prop || !obj) return null;
3540
+ if (prop === "use") {
3541
+ const inner = unwrapRouterExpr(obj, expressLocals, routerCtors);
3542
+ if (!inner) return null;
3543
+ const mount = parseUseMount(node);
3544
+ return { base: inner.base, mounts: mount ? [...inner.mounts, mount] : inner.mounts };
3545
+ }
3546
+ if (prop === "Router" && obj.type === "identifier" && expressLocals.has(obj.text)) {
3547
+ return { base: "newRouter", mounts: [] };
3548
+ }
3549
+ return null;
3550
+ }
3551
+ if (fn.type === "identifier") {
3552
+ if (expressLocals.has(fn.text)) return { base: "app", mounts: [] };
3553
+ if (routerCtors.has(fn.text)) return { base: "newRouter", mounts: [] };
3554
+ }
3555
+ return null;
3556
+ }
3557
+ function collectExpressImports(root) {
3558
+ const expressLocals = /* @__PURE__ */ new Set();
3559
+ const routerCtors = /* @__PURE__ */ new Set();
3560
+ const bindings = [];
3561
+ const addFromExpress = (local, sel, exported) => {
3562
+ if (sel === "default" || sel === "namespace") expressLocals.add(local);
3563
+ else if (exported === "Router") routerCtors.add(local);
3564
+ };
3565
+ walk(root, (node) => {
3566
+ if (node.type === "import_statement") {
3567
+ const source = node.childForFieldName("source");
3568
+ const spec = source ? staticStringText(source) : null;
3569
+ if (!spec) return;
3570
+ let clause = null;
3571
+ for (let i = 0; i < node.namedChildCount; i++) {
3572
+ const c = node.namedChild(i);
3573
+ if (c?.type === "import_clause") clause = c;
3574
+ }
3575
+ if (!clause) return;
3576
+ for (let i = 0; i < clause.namedChildCount; i++) {
3577
+ const c = clause.namedChild(i);
3578
+ if (!c) continue;
3579
+ if (c.type === "identifier") {
3580
+ if (spec === "express") addFromExpress(c.text, "default", "default");
3581
+ else bindings.push({ local: c.text, specifier: spec, sel: "default" });
3582
+ } else if (c.type === "namespace_import") {
3583
+ const id = c.namedChild(0);
3584
+ if (id?.type === "identifier") {
3585
+ if (spec === "express") addFromExpress(id.text, "namespace", "namespace");
3586
+ else bindings.push({ local: id.text, specifier: spec, sel: "namespace" });
3587
+ }
3588
+ } else if (c.type === "named_imports") {
3589
+ for (let j = 0; j < c.namedChildCount; j++) {
3590
+ const s = c.namedChild(j);
3591
+ if (s?.type !== "import_specifier") continue;
3592
+ const name = s.childForFieldName("name")?.text;
3593
+ if (!name) continue;
3594
+ const local = s.childForFieldName("alias")?.text ?? name;
3595
+ if (spec === "express") addFromExpress(local, name, name);
3596
+ else bindings.push({ local, specifier: spec, sel: name });
3597
+ }
3598
+ }
3599
+ }
3600
+ return;
3601
+ }
3602
+ if (node.type === "variable_declarator") {
3603
+ const value = node.childForFieldName("value");
3604
+ if (value?.type !== "call_expression") return;
3605
+ const fn = value.childForFieldName("function");
3606
+ if (fn?.type !== "identifier" || fn.text !== "require") return;
3607
+ const arg = namedArgs(value.childForFieldName("arguments"))[0];
3608
+ const spec = arg ? staticStringText(arg) : null;
3609
+ if (!spec) return;
3610
+ const name = node.childForFieldName("name");
3611
+ if (name?.type === "identifier") {
3612
+ if (spec === "express") expressLocals.add(name.text);
3613
+ else bindings.push({ local: name.text, specifier: spec, sel: "default" });
3614
+ } else if (name?.type === "object_pattern") {
3615
+ for (let i = 0; i < name.namedChildCount; i++) {
3616
+ const el = name.namedChild(i);
3617
+ if (!el) continue;
3618
+ let local;
3619
+ let exported;
3620
+ if (el.type === "shorthand_property_identifier_pattern") {
3621
+ local = el.text;
3622
+ exported = el.text;
3623
+ } else if (el.type === "pair_pattern") {
3624
+ exported = el.childForFieldName("key")?.text;
3625
+ local = el.childForFieldName("value")?.text ?? exported;
3626
+ }
3627
+ if (!local || !exported) continue;
3628
+ if (spec === "express") addFromExpress(local, exported, exported);
3629
+ else bindings.push({ local, specifier: spec, sel: exported });
3630
+ }
3631
+ }
3632
+ }
3633
+ });
3634
+ return { expressLocals, routerCtors, bindings };
3635
+ }
3636
+ function analyzeExpressFile(root, dir) {
3637
+ const { expressLocals, routerCtors, bindings } = collectExpressImports(root);
3638
+ const routerVars = /* @__PURE__ */ new Map();
3639
+ const appVars = /* @__PURE__ */ new Set();
3640
+ const exportNamed = /* @__PURE__ */ new Map();
3641
+ let exportDefaultName = null;
3642
+ const getVar = (name) => {
3643
+ let rv = routerVars.get(name);
3644
+ if (!rv) {
3645
+ rv = { declares: false, mounts: [] };
3646
+ routerVars.set(name, rv);
3647
+ }
3648
+ return rv;
3649
+ };
3650
+ const refFromExpr = (expr, key) => {
3651
+ if (expr.type === "identifier") return expr.text;
3652
+ const u = unwrapRouterExpr(expr, expressLocals, routerCtors);
3653
+ if (!u) return null;
3654
+ const rv = getVar(key);
3655
+ for (const m of u.mounts) rv.mounts.push(m);
3656
+ if (typeof u.base === "object") rv.aliasOf = u.base.alias;
3657
+ return key;
3658
+ };
3659
+ const isExported = (declarator) => {
3660
+ const decl = declarator.parent;
3661
+ return decl?.parent?.type === "export_statement";
3662
+ };
3663
+ walk(root, (node) => {
3664
+ if (node.type === "variable_declarator") {
3665
+ const value = node.childForFieldName("value");
3666
+ const name = node.childForFieldName("name");
3667
+ if (name?.type !== "identifier" || !value) return;
3668
+ if (value.type === "call_expression") {
3669
+ const fn = value.childForFieldName("function");
3670
+ if (fn?.type === "identifier" && fn.text === "require") return;
3671
+ }
3672
+ const u = unwrapRouterExpr(value, expressLocals, routerCtors);
3673
+ if (!u) return;
3674
+ const rv = getVar(name.text);
3675
+ for (const m of u.mounts) rv.mounts.push(m);
3676
+ if (u.base === "app") appVars.add(name.text);
3677
+ else if (typeof u.base === "object") rv.aliasOf = u.base.alias;
3678
+ if (isExported(node)) exportNamed.set(name.text, name.text);
3679
+ return;
3680
+ }
3681
+ if (node.type === "call_expression") {
3682
+ const fn = node.childForFieldName("function");
3683
+ if (fn?.type !== "member_expression") return;
3684
+ const obj = fn.childForFieldName("object");
3685
+ const prop = fn.childForFieldName("property")?.text;
3686
+ if (obj?.type !== "identifier" || !prop) return;
3687
+ if (prop === "use") {
3688
+ const m = parseUseMount(node);
3689
+ if (m) getVar(obj.text).mounts.push(m);
3690
+ } else if (ROUTER_METHODS.has(prop.toLowerCase())) {
3691
+ const first = namedArgs(node.childForFieldName("arguments"))[0];
3692
+ const p = first ? staticStringText(first) : null;
3693
+ if (p !== null && p.startsWith("/")) getVar(obj.text).declares = true;
3694
+ }
3695
+ return;
3696
+ }
3697
+ if (node.type === "export_statement") {
3698
+ let clause = null;
3699
+ for (let i = 0; i < node.namedChildCount; i++) {
3700
+ const c = node.namedChild(i);
3701
+ if (c?.type === "export_clause") clause = c;
3702
+ }
3703
+ if (clause) {
3704
+ for (let i = 0; i < clause.namedChildCount; i++) {
3705
+ const spec = clause.namedChild(i);
3706
+ if (spec?.type !== "export_specifier") continue;
3707
+ const local = spec.childForFieldName("name")?.text;
3708
+ if (!local) continue;
3709
+ const exportedAs = spec.childForFieldName("alias")?.text ?? local;
3710
+ if (exportedAs === "default") exportDefaultName = local;
3711
+ else exportNamed.set(exportedAs, local);
3712
+ }
3713
+ return;
3714
+ }
3715
+ if (node.childForFieldName("declaration")) return;
3716
+ for (let i = 0; i < node.namedChildCount; i++) {
3717
+ const c = node.namedChild(i);
3718
+ if (c && c.type !== "export_clause") {
3719
+ exportDefaultName = refFromExpr(c, "#default");
3720
+ break;
3721
+ }
3722
+ }
3723
+ return;
3724
+ }
3725
+ if (node.type === "assignment_expression") {
3726
+ const left = node.childForFieldName("left");
3727
+ const right = node.childForFieldName("right");
3728
+ if (left?.type !== "member_expression" || !right) return;
3729
+ const lobj = left.childForFieldName("object")?.text;
3730
+ const lprop = left.childForFieldName("property")?.text;
3731
+ if (lobj === "module" && lprop === "exports") exportDefaultName = refFromExpr(right, "#default");
3732
+ else if (lobj === "exports" && lprop) {
3733
+ const key = refFromExpr(right, `#exp:${lprop}`);
3734
+ if (key) exportNamed.set(lprop, key);
3735
+ }
3736
+ }
3737
+ });
3738
+ return {
3739
+ dir,
3740
+ routerVars,
3741
+ appVars,
3742
+ exportDefaultName,
3743
+ exportNamed,
3744
+ rawBindings: bindings,
3745
+ importedRouters: /* @__PURE__ */ new Map()
3746
+ };
3747
+ }
3748
+ async function expressMountPrefixes(files, serviceDir, tsPaths) {
3749
+ const jsParser = makeJsParser2();
3750
+ const fileInfo = /* @__PURE__ */ new Map();
3751
+ for (const f of files) {
3752
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path7.default.extname(f.path))) continue;
3753
+ if (isTestPath(f.path)) continue;
3754
+ const rel = toPosix(import_node_path7.default.relative(serviceDir, f.path));
3755
+ try {
3756
+ const tree = parseSource2(jsParser, f.content);
3757
+ fileInfo.set(rel, analyzeExpressFile(tree.rootNode, import_node_path7.default.dirname(f.path)));
3758
+ } catch {
3759
+ }
3760
+ }
3761
+ if (fileInfo.size === 0) return /* @__PURE__ */ new Map();
3762
+ for (const info of fileInfo.values()) {
3763
+ for (const b of info.rawBindings) {
3764
+ const resolved = await resolveJsImport(b.specifier, info.dir, serviceDir, tsPaths);
3765
+ if (!resolved || !fileInfo.has(resolved)) continue;
3766
+ info.importedRouters.set(b.local, { file: resolved, sel: b.sel === "namespace" ? "default" : b.sel });
3767
+ }
3768
+ }
3769
+ const resolveTarget = (name, file) => {
3770
+ const info = fileInfo.get(file);
3771
+ if (!info) return null;
3772
+ if (info.routerVars.has(name)) return { file, name };
3773
+ const imp = info.importedRouters.get(name);
3774
+ if (!imp) return null;
3775
+ const target = fileInfo.get(imp.file);
3776
+ if (!target) return null;
3777
+ const key = imp.sel === "default" ? target.exportDefaultName : target.exportNamed.get(imp.sel);
3778
+ if (!key) return null;
3779
+ return { file: imp.file, name: key };
3780
+ };
3781
+ const filePrefix = /* @__PURE__ */ new Map();
3782
+ const conflicted = /* @__PURE__ */ new Set();
3783
+ const apply = (file, prefix) => {
3784
+ if (conflicted.has(file)) return;
3785
+ const existing = filePrefix.get(file);
3786
+ if (existing === void 0) filePrefix.set(file, prefix);
3787
+ else if (existing !== prefix) {
3788
+ filePrefix.delete(file);
3789
+ conflicted.add(file);
3790
+ }
3791
+ };
3792
+ const visited = /* @__PURE__ */ new Set();
3793
+ const collect = (file, name, accPrefix) => {
3794
+ const key = `${file}|${name}|${accPrefix}`;
3795
+ if (visited.has(key)) return;
3796
+ visited.add(key);
3797
+ const info = fileInfo.get(file);
3798
+ const rv = info?.routerVars.get(name);
3799
+ if (!info || !rv) return;
3800
+ if (rv.declares && info.appVars.size === 0) apply(file, accPrefix);
3801
+ for (const m of rv.mounts) {
3802
+ if (!m.target) continue;
3803
+ const t = resolveTarget(m.target, file);
3804
+ if (t) collect(t.file, t.name, accPrefix + m.prefix);
3805
+ }
3806
+ if (rv.aliasOf) {
3807
+ const t = resolveTarget(rv.aliasOf, file);
3808
+ if (t) collect(t.file, t.name, accPrefix);
3809
+ }
3810
+ };
3811
+ for (const [rel, info] of fileInfo) {
3812
+ for (const appVar of info.appVars) collect(rel, appVar, "");
3813
+ }
3814
+ const out = /* @__PURE__ */ new Map();
3815
+ for (const [file, prefix] of filePrefix) if (prefix && prefix !== "/") out.set(file, prefix);
3816
+ return out;
3817
+ }
3123
3818
  async function addRoutes(graph, services) {
3124
- const jsParser = makeJsParser();
3125
- const pyParser = makePyParser();
3126
- const goParser = makeGoParser();
3819
+ const jsParser = makeJsParser2();
3820
+ const pyParser = makePyParser2();
3821
+ const goParser = makeGoParser2();
3127
3822
  let nodesAdded = 0;
3128
3823
  let edgesAdded = 0;
3129
3824
  for (const service of services) {
@@ -3143,13 +3838,14 @@ async function addRoutes(graph, services) {
3143
3838
  if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasNestjs && !hasFastapi && !hasFlask && !hasDjango && !hasGin)
3144
3839
  continue;
3145
3840
  const files = await loadSourceFiles(service.dir);
3841
+ const mountPrefixes = hasExpress ? await expressMountPrefixes(files, service.dir, await loadTsPathConfig(service.dir)) : /* @__PURE__ */ new Map();
3146
3842
  for (const file of files) {
3147
3843
  if (isTestPath(file.path)) continue;
3148
- const ext = import_node_path6.default.extname(file.path);
3844
+ const ext = import_node_path7.default.extname(file.path);
3149
3845
  const isPy = ext === ".py";
3150
3846
  const isGo = ext === ".go";
3151
3847
  if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy && !isGo) continue;
3152
- const relFile = toPosix(import_node_path6.default.relative(service.dir, file.path));
3848
+ const relFile = toPosix(import_node_path7.default.relative(service.dir, file.path));
3153
3849
  let routes;
3154
3850
  try {
3155
3851
  if (isGo) {
@@ -3171,16 +3867,18 @@ async function addRoutes(graph, services) {
3171
3867
  continue;
3172
3868
  }
3173
3869
  if (routes.length === 0) continue;
3870
+ const mountPrefix = mountPrefixes.get(relFile);
3174
3871
  for (const route of routes) {
3175
- const rid = (0, import_types5.routeId)(service.pkg.name, route.method, route.pathTemplate);
3872
+ const pathTemplate = mountPrefix ? canonicalizeTemplate(mountPrefix + route.pathTemplate) : route.pathTemplate;
3873
+ const rid = (0, import_types6.routeId)(service.pkg.name, route.method, pathTemplate);
3176
3874
  if (!graph.hasNode(rid)) {
3177
3875
  const node = {
3178
3876
  id: rid,
3179
- type: import_types5.NodeType.RouteNode,
3180
- name: `${route.method} ${route.pathTemplate}`,
3877
+ type: import_types6.NodeType.RouteNode,
3878
+ name: `${route.method} ${pathTemplate}`,
3181
3879
  service: service.pkg.name,
3182
3880
  method: route.method,
3183
- pathTemplate: route.pathTemplate,
3881
+ pathTemplate,
3184
3882
  path: relFile,
3185
3883
  line: route.line,
3186
3884
  framework: route.framework,
@@ -3189,15 +3887,15 @@ async function addRoutes(graph, services) {
3189
3887
  graph.addNode(rid, node);
3190
3888
  nodesAdded++;
3191
3889
  }
3192
- const containsId = (0, import_types5.extractedEdgeId)(service.node.id, rid, import_types5.EdgeType.CONTAINS);
3890
+ const containsId = (0, import_types6.extractedEdgeId)(service.node.id, rid, import_types6.EdgeType.CONTAINS);
3193
3891
  if (!graph.hasEdge(containsId)) {
3194
3892
  const edge = {
3195
3893
  id: containsId,
3196
3894
  source: service.node.id,
3197
3895
  target: rid,
3198
- type: import_types5.EdgeType.CONTAINS,
3199
- provenance: import_types5.Provenance.EXTRACTED,
3200
- confidence: (0, import_types5.confidenceForExtracted)("structural"),
3896
+ type: import_types6.EdgeType.CONTAINS,
3897
+ provenance: import_types6.Provenance.EXTRACTED,
3898
+ confidence: (0, import_types6.confidenceForExtracted)("structural"),
3201
3899
  evidence: {
3202
3900
  file: relFile,
3203
3901
  line: route.line,
@@ -3215,7 +3913,7 @@ async function addRoutes(graph, services) {
3215
3913
 
3216
3914
  // src/columns.ts
3217
3915
  init_cjs_shims();
3218
- var import_types6 = require("@neat.is/types");
3916
+ var import_types7 = require("@neat.is/types");
3219
3917
  var OBSERVED_COLUMN_CONFIDENCE = 0.9;
3220
3918
  function normalizeProvenances(provenances) {
3221
3919
  return [...new Set(provenances)].sort();
@@ -3243,10 +3941,10 @@ function foldColumns(existing, names, provenance, confidence) {
3243
3941
  return out;
3244
3942
  }
3245
3943
  function columnIsDeclared(col) {
3246
- return col.provenances.includes(import_types6.Provenance.EXTRACTED);
3944
+ return col.provenances.includes(import_types7.Provenance.EXTRACTED);
3247
3945
  }
3248
3946
  function columnIsObserved(col) {
3249
- return col.provenances.includes(import_types6.Provenance.OBSERVED);
3947
+ return col.provenances.includes(import_types7.Provenance.OBSERVED);
3250
3948
  }
3251
3949
 
3252
3950
  // src/ingest.ts
@@ -3469,7 +4167,7 @@ function languageForExt(relPath) {
3469
4167
  function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
3470
4168
  let p = toPosix2(filepath).replace(/^file:\/\//, "");
3471
4169
  if (scanPath && scanPath.length > 0) {
3472
- const absRoot = toPosix2(import_node_path7.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
4170
+ const absRoot = toPosix2(import_node_path8.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
3473
4171
  const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
3474
4172
  if (p.startsWith(anchor)) return p.slice(anchor.length);
3475
4173
  }
@@ -3497,10 +4195,10 @@ function resolveDistToSrc(absFilepath, line) {
3497
4195
  entry = null;
3498
4196
  const mapPath = `${absFilepath}.map`;
3499
4197
  try {
3500
- if ((0, import_node_fs6.existsSync)(mapPath)) {
3501
- const raw = JSON.parse((0, import_node_fs6.readFileSync)(mapPath, "utf8"));
4198
+ if ((0, import_node_fs7.existsSync)(mapPath)) {
4199
+ const raw = JSON.parse((0, import_node_fs7.readFileSync)(mapPath, "utf8"));
3502
4200
  const consumer = new sourceMapJs.SourceMapConsumer(raw);
3503
- entry = { consumer, dir: import_node_path7.default.dirname(mapPath) };
4201
+ entry = { consumer, dir: import_node_path8.default.dirname(mapPath) };
3504
4202
  }
3505
4203
  } catch {
3506
4204
  entry = null;
@@ -3515,7 +4213,7 @@ function resolveDistToSrc(absFilepath, line) {
3515
4213
  });
3516
4214
  if (!pos || !pos.source) return null;
3517
4215
  const root = entry.consumer.sourceRoot ?? "";
3518
- const resolved = import_node_path7.default.resolve(entry.dir, root, pos.source);
4216
+ const resolved = import_node_path8.default.resolve(entry.dir, root, pos.source);
3519
4217
  return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
3520
4218
  } catch {
3521
4219
  return null;
@@ -3548,11 +4246,11 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
3548
4246
  };
3549
4247
  }
3550
4248
  function reconcileObservedRelPath(graph, serviceName, relPath) {
3551
- if (graph.hasNode((0, import_types7.fileId)(serviceName, relPath))) return relPath;
4249
+ if (graph.hasNode((0, import_types8.fileId)(serviceName, relPath))) return relPath;
3552
4250
  let best = null;
3553
4251
  graph.forEachNode((_id, attrs) => {
3554
4252
  const a = attrs;
3555
- if (a.type !== import_types7.NodeType.FileNode || a.service !== serviceName) return;
4253
+ if (a.type !== import_types8.NodeType.FileNode || a.service !== serviceName) return;
3556
4254
  if (a.discoveredVia === "otel") return;
3557
4255
  const p = a.path;
3558
4256
  if (!p) return;
@@ -3564,14 +4262,14 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
3564
4262
  }
3565
4263
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3566
4264
  const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
3567
- const canonicalService = svcAttrs && svcAttrs.type === import_types7.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
4265
+ const canonicalService = svcAttrs && svcAttrs.type === import_types8.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
3568
4266
  const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
3569
- const fileNodeId = (0, import_types7.fileId)(canonicalService, relPath);
4267
+ const fileNodeId = (0, import_types8.fileId)(canonicalService, relPath);
3570
4268
  if (!graph.hasNode(fileNodeId)) {
3571
4269
  const language = languageForExt(relPath);
3572
4270
  const node = {
3573
4271
  id: fileNodeId,
3574
- type: import_types7.NodeType.FileNode,
4272
+ type: import_types8.NodeType.FileNode,
3575
4273
  service: canonicalService,
3576
4274
  path: relPath,
3577
4275
  ...language ? { language } : {},
@@ -3580,14 +4278,14 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3580
4278
  };
3581
4279
  graph.addNode(fileNodeId, node);
3582
4280
  }
3583
- const containsId = makeObservedEdgeId(import_types7.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
4281
+ const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
3584
4282
  if (!graph.hasEdge(containsId)) {
3585
4283
  const edge = {
3586
4284
  id: containsId,
3587
4285
  source: serviceNodeId,
3588
4286
  target: fileNodeId,
3589
- type: import_types7.EdgeType.CONTAINS,
3590
- provenance: import_types7.Provenance.OBSERVED
4287
+ type: import_types8.EdgeType.CONTAINS,
4288
+ provenance: import_types8.Provenance.OBSERVED
3591
4289
  };
3592
4290
  graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3593
4291
  }
@@ -3611,11 +4309,11 @@ function pickContainingSymbol(candidates, fn) {
3611
4309
  return [...candidates].sort(bySpan)[0].id;
3612
4310
  }
3613
4311
  function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line) {
3614
- const sid = (0, import_types7.symbolId)(service, relPath, fn);
4312
+ const sid = (0, import_types8.symbolId)(service, relPath, fn);
3615
4313
  if (!graph.hasNode(sid)) {
3616
4314
  const node = {
3617
4315
  id: sid,
3618
- type: import_types7.NodeType.SymbolNode,
4316
+ type: import_types8.NodeType.SymbolNode,
3619
4317
  kind: "function",
3620
4318
  qualname: fn,
3621
4319
  span: { startLine: line, endLine: line },
@@ -3625,14 +4323,14 @@ function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line)
3625
4323
  };
3626
4324
  graph.addNode(sid, node);
3627
4325
  }
3628
- const containsId = makeObservedEdgeId(import_types7.EdgeType.CONTAINS, fileNodeId, sid);
4326
+ const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, fileNodeId, sid);
3629
4327
  if (!graph.hasEdge(containsId)) {
3630
4328
  const edge = {
3631
4329
  id: containsId,
3632
4330
  source: fileNodeId,
3633
4331
  target: sid,
3634
- type: import_types7.EdgeType.CONTAINS,
3635
- provenance: import_types7.Provenance.OBSERVED
4332
+ type: import_types8.EdgeType.CONTAINS,
4333
+ provenance: import_types8.Provenance.OBSERVED
3636
4334
  };
3637
4335
  graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
3638
4336
  }
@@ -3644,9 +4342,9 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
3644
4342
  let sawSymbol = false;
3645
4343
  const candidates = [];
3646
4344
  graph.forEachOutboundEdge(fileNodeId, (_edge, edgeAttrs, _source, target) => {
3647
- if (edgeAttrs.type !== import_types7.EdgeType.CONTAINS) return;
4345
+ if (edgeAttrs.type !== import_types8.EdgeType.CONTAINS) return;
3648
4346
  const t = graph.getNodeAttributes(target);
3649
- if (t.type !== import_types7.NodeType.SymbolNode) return;
4347
+ if (t.type !== import_types8.NodeType.SymbolNode) return;
3650
4348
  sawSymbol = true;
3651
4349
  if (line >= t.span.startLine && line <= t.span.endLine) {
3652
4350
  candidates.push({ id: target, symbol: t });
@@ -3659,17 +4357,17 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
3659
4357
  return fileNodeId;
3660
4358
  }
3661
4359
  function makeObservedEdgeId(type, source, target) {
3662
- return (0, import_types7.observedEdgeId)(source, target, type);
4360
+ return (0, import_types8.observedEdgeId)(source, target, type);
3663
4361
  }
3664
4362
  function makeInferredEdgeId(type, source, target) {
3665
- return (0, import_types7.inferredEdgeId)(source, target, type);
4363
+ return (0, import_types8.inferredEdgeId)(source, target, type);
3666
4364
  }
3667
4365
  var INFERRED_CONFIDENCE = 0.6;
3668
4366
  var STITCH_MAX_DEPTH = 2;
3669
4367
  var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
3670
- import_types7.EdgeType.CALLS,
3671
- import_types7.EdgeType.CONNECTS_TO,
3672
- import_types7.EdgeType.DEPENDS_ON
4368
+ import_types8.EdgeType.CALLS,
4369
+ import_types8.EdgeType.CONNECTS_TO,
4370
+ import_types8.EdgeType.DEPENDS_ON
3673
4371
  ]);
3674
4372
  var WIRE_SPAN_KIND_CLIENT = 3;
3675
4373
  var WIRE_SPAN_KIND_PRODUCER = 4;
@@ -3685,11 +4383,11 @@ function spanServesGraphqlOperation(kind) {
3685
4383
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3686
4384
  }
3687
4385
  function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
3688
- const id = (0, import_types7.graphqlOperationId)(serviceName, operationType, operationName);
4386
+ const id = (0, import_types8.graphqlOperationId)(serviceName, operationType, operationName);
3689
4387
  if (graph.hasNode(id)) return id;
3690
4388
  const node = {
3691
4389
  id,
3692
- type: import_types7.NodeType.GraphQLOperationNode,
4390
+ type: import_types8.NodeType.GraphQLOperationNode,
3693
4391
  name: operationName,
3694
4392
  service: serviceName,
3695
4393
  operationType: operationType.toLowerCase(),
@@ -3703,11 +4401,11 @@ function spanServesGrpcMethod(kind) {
3703
4401
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3704
4402
  }
3705
4403
  function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
3706
- const id = (0, import_types7.grpcMethodId)(rpcService, rpcMethod);
4404
+ const id = (0, import_types8.grpcMethodId)(rpcService, rpcMethod);
3707
4405
  if (graph.hasNode(id)) return id;
3708
4406
  const node = {
3709
4407
  id,
3710
- type: import_types7.NodeType.GrpcMethodNode,
4408
+ type: import_types8.NodeType.GrpcMethodNode,
3711
4409
  name: `${rpcService}/${rpcMethod}`,
3712
4410
  rpcService,
3713
4411
  rpcMethod,
@@ -3720,11 +4418,11 @@ function spanServesWebsocketChannel(kind) {
3720
4418
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3721
4419
  }
3722
4420
  function ensureWebsocketChannelNode(graph, serviceName, channel) {
3723
- const id = (0, import_types7.websocketChannelId)(serviceName, channel);
4421
+ const id = (0, import_types8.websocketChannelId)(serviceName, channel);
3724
4422
  if (graph.hasNode(id)) return id;
3725
4423
  const node = {
3726
4424
  id,
3727
- type: import_types7.NodeType.WebSocketChannelNode,
4425
+ type: import_types8.NodeType.WebSocketChannelNode,
3728
4426
  name: channel,
3729
4427
  service: serviceName,
3730
4428
  channel,
@@ -3737,11 +4435,11 @@ function messagingDestinationKind(system) {
3737
4435
  return `${system}-topic`;
3738
4436
  }
3739
4437
  function ensureMessagingDestinationNode(graph, system, destination) {
3740
- const id = (0, import_types7.infraId)(messagingDestinationKind(system), destination);
4438
+ const id = (0, import_types8.infraId)(messagingDestinationKind(system), destination);
3741
4439
  if (graph.hasNode(id)) return id;
3742
4440
  const node = {
3743
4441
  id,
3744
- type: import_types7.NodeType.InfraNode,
4442
+ type: import_types8.NodeType.InfraNode,
3745
4443
  name: destination,
3746
4444
  provider: "self",
3747
4445
  kind: messagingDestinationKind(system)
@@ -3785,9 +4483,9 @@ function lookupParentSpan(traceId, parentSpanId, now) {
3785
4483
  };
3786
4484
  }
3787
4485
  function resolveServiceId(graph, host, env) {
3788
- const envTagged = (0, import_types7.serviceId)(host, env);
4486
+ const envTagged = (0, import_types8.serviceId)(host, env);
3789
4487
  if (graph.hasNode(envTagged)) return envTagged;
3790
- const envLess = (0, import_types7.serviceId)(host);
4488
+ const envLess = (0, import_types8.serviceId)(host);
3791
4489
  if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
3792
4490
  let sameEnv = null;
3793
4491
  let envLessMatch = null;
@@ -3795,7 +4493,7 @@ function resolveServiceId(graph, host, env) {
3795
4493
  graph.forEachNode((id, attrs) => {
3796
4494
  if (sameEnv) return;
3797
4495
  const a = attrs;
3798
- if (a.type !== import_types7.NodeType.ServiceNode) return;
4496
+ if (a.type !== import_types8.NodeType.ServiceNode) return;
3799
4497
  const matchesByName = a.name === host;
3800
4498
  const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
3801
4499
  if (!matchesByName && !matchesByAlias) return;
@@ -3810,14 +4508,14 @@ function resolveServiceId(graph, host, env) {
3810
4508
  return sameEnv ?? envLessMatch ?? anyMatch;
3811
4509
  }
3812
4510
  function frontierIdFor(host) {
3813
- return (0, import_types7.frontierId)(host);
4511
+ return (0, import_types8.frontierId)(host);
3814
4512
  }
3815
4513
  function ensureServiceNode(graph, serviceName, env) {
3816
- const id = (0, import_types7.serviceId)(serviceName, env);
4514
+ const id = (0, import_types8.serviceId)(serviceName, env);
3817
4515
  if (graph.hasNode(id)) return id;
3818
4516
  const wanted = serviceName.toLowerCase();
3819
4517
  const extractedId = graph.findNode((_nid, attrs) => {
3820
- if (attrs.type !== import_types7.NodeType.ServiceNode) return false;
4518
+ if (attrs.type !== import_types8.NodeType.ServiceNode) return false;
3821
4519
  const svc = attrs;
3822
4520
  if (svc.discoveredVia === "otel") return false;
3823
4521
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
@@ -3825,7 +4523,7 @@ function ensureServiceNode(graph, serviceName, env) {
3825
4523
  if (extractedId) return extractedId;
3826
4524
  const node = {
3827
4525
  id,
3828
- type: import_types7.NodeType.ServiceNode,
4526
+ type: import_types8.NodeType.ServiceNode,
3829
4527
  name: serviceName,
3830
4528
  language: "unknown",
3831
4529
  discoveredVia: "otel",
@@ -3835,11 +4533,11 @@ function ensureServiceNode(graph, serviceName, env) {
3835
4533
  return id;
3836
4534
  }
3837
4535
  function ensureInfraNode(graph, kind, name, provider) {
3838
- const id = (0, import_types7.infraId)(kind, name);
4536
+ const id = (0, import_types8.infraId)(kind, name);
3839
4537
  if (graph.hasNode(id)) return id;
3840
4538
  const node = {
3841
4539
  id,
3842
- type: import_types7.NodeType.InfraNode,
4540
+ type: import_types8.NodeType.InfraNode,
3843
4541
  name,
3844
4542
  provider,
3845
4543
  kind
@@ -3851,7 +4549,7 @@ var COLUMN_BEARING_INFRA_KINDS = /* @__PURE__ */ new Set(["sql-table", "supabase
3851
4549
  function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
3852
4550
  if (!columns || columns.length === 0 || !graph.hasNode(tableNodeId)) return;
3853
4551
  const node = graph.getNodeAttributes(tableNodeId);
3854
- if (node.type !== import_types7.NodeType.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
4552
+ if (node.type !== import_types8.NodeType.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
3855
4553
  return;
3856
4554
  }
3857
4555
  graph.replaceNodeAttributes(tableNodeId, {
@@ -3860,14 +4558,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
3860
4558
  });
3861
4559
  }
3862
4560
  function mergeObservedColumns(graph, tableNodeId, columns) {
3863
- mergeColumnsAt(graph, tableNodeId, columns, import_types7.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
4561
+ mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
3864
4562
  }
3865
4563
  function ensureDatabaseNode(graph, host, engine) {
3866
- const id = (0, import_types7.databaseId)(host);
4564
+ const id = (0, import_types8.databaseId)(host);
3867
4565
  if (graph.hasNode(id)) return id;
3868
4566
  const node = {
3869
4567
  id,
3870
- type: import_types7.NodeType.DatabaseNode,
4568
+ type: import_types8.NodeType.DatabaseNode,
3871
4569
  name: host,
3872
4570
  engine,
3873
4571
  engineVersion: "unknown",
@@ -3879,11 +4577,11 @@ function ensureDatabaseNode(graph, host, engine) {
3879
4577
  return id;
3880
4578
  }
3881
4579
  function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
3882
- const id = (0, import_types7.localDatabaseId)(serviceName, name);
4580
+ const id = (0, import_types8.localDatabaseId)(serviceName, name);
3883
4581
  if (graph.hasNode(id)) return id;
3884
4582
  const node = {
3885
4583
  id,
3886
- type: import_types7.NodeType.DatabaseNode,
4584
+ type: import_types8.NodeType.DatabaseNode,
3887
4585
  name,
3888
4586
  engine,
3889
4587
  engineVersion: "unknown",
@@ -3898,17 +4596,17 @@ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
3898
4596
  const sources = [serviceNodeId];
3899
4597
  for (const edgeId of graph.outboundEdges(serviceNodeId)) {
3900
4598
  const e = graph.getEdgeAttributes(edgeId);
3901
- if (e.type === import_types7.EdgeType.CONTAINS) sources.push(e.target);
4599
+ if (e.type === import_types8.EdgeType.CONTAINS) sources.push(e.target);
3902
4600
  }
3903
4601
  const matches = /* @__PURE__ */ new Set();
3904
4602
  for (const src of sources) {
3905
4603
  if (!graph.hasNode(src)) continue;
3906
4604
  for (const edgeId of graph.outboundEdges(src)) {
3907
4605
  const edge = graph.getEdgeAttributes(edgeId);
3908
- if (edge.type !== import_types7.EdgeType.CONNECTS_TO || edge.provenance !== import_types7.Provenance.EXTRACTED) continue;
4606
+ if (edge.type !== import_types8.EdgeType.CONNECTS_TO || edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
3909
4607
  if (!graph.hasNode(edge.target)) continue;
3910
4608
  const target = graph.getNodeAttributes(edge.target);
3911
- if (target.type !== import_types7.NodeType.DatabaseNode || target.engine !== engine) continue;
4609
+ if (target.type !== import_types8.NodeType.DatabaseNode || target.engine !== engine) continue;
3912
4610
  matches.add(edge.target);
3913
4611
  }
3914
4612
  }
@@ -3923,7 +4621,7 @@ function ensureFrontierNode(graph, host, ts) {
3923
4621
  }
3924
4622
  const node = {
3925
4623
  id,
3926
- type: import_types7.NodeType.FrontierNode,
4624
+ type: import_types8.NodeType.FrontierNode,
3927
4625
  name: host,
3928
4626
  host,
3929
4627
  firstObserved: ts,
@@ -3947,11 +4645,11 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
3947
4645
  };
3948
4646
  const updated = {
3949
4647
  ...existing,
3950
- provenance: import_types7.Provenance.OBSERVED,
4648
+ provenance: import_types8.Provenance.OBSERVED,
3951
4649
  lastObserved: ts,
3952
4650
  callCount: newSpanCount,
3953
4651
  signal: newSignal,
3954
- confidence: (0, import_types7.confidenceForObservedSignal)(newSignal),
4652
+ confidence: (0, import_types8.confidenceForObservedSignal)(newSignal),
3955
4653
  grain
3956
4654
  // backfills legacy edges that predate ADR-142
3957
4655
  };
@@ -3968,8 +4666,8 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
3968
4666
  source,
3969
4667
  target,
3970
4668
  type,
3971
- provenance: import_types7.Provenance.OBSERVED,
3972
- confidence: (0, import_types7.confidenceForObservedSignal)(signal),
4669
+ provenance: import_types8.Provenance.OBSERVED,
4670
+ confidence: (0, import_types8.confidenceForObservedSignal)(signal),
3973
4671
  lastObserved: ts,
3974
4672
  callCount: 1,
3975
4673
  signal,
@@ -3991,9 +4689,9 @@ function stitchTrace(graph, sourceServiceId, ts) {
3991
4689
  const outbound = graph.outboundEdges(nodeId);
3992
4690
  for (const edgeId of outbound) {
3993
4691
  const edge = graph.getEdgeAttributes(edgeId);
3994
- if (edge.provenance !== import_types7.Provenance.EXTRACTED) continue;
4692
+ if (edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
3995
4693
  if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
3996
- if (graph.hasEdge((0, import_types7.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
4694
+ if (graph.hasEdge((0, import_types8.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
3997
4695
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
3998
4696
  if (!visited.has(edge.target)) {
3999
4697
  visited.add(edge.target);
@@ -4015,23 +4713,23 @@ function upsertInferredEdge(graph, type, source, target, ts) {
4015
4713
  source,
4016
4714
  target,
4017
4715
  type,
4018
- provenance: import_types7.Provenance.INFERRED,
4716
+ provenance: import_types8.Provenance.INFERRED,
4019
4717
  confidence: INFERRED_CONFIDENCE,
4020
4718
  lastObserved: ts
4021
4719
  };
4022
4720
  graph.addEdgeWithKey(id, source, target, edge);
4023
4721
  }
4024
4722
  async function appendErrorEvent(ctx, ev) {
4025
- await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(ctx.errorsPath), { recursive: true });
4026
- await import_node_fs6.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
4723
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
4724
+ await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
4027
4725
  }
4028
4726
  function incidentAffectedNode(span, graph, scanPath) {
4029
- const sid = (0, import_types7.serviceId)(span.service, span.env);
4727
+ const sid = (0, import_types8.serviceId)(span.service, span.env);
4030
4728
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
4031
4729
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
4032
4730
  if (callSite) {
4033
4731
  const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
4034
- return (0, import_types7.fileId)(span.service, relPath);
4732
+ return (0, import_types8.fileId)(span.service, relPath);
4035
4733
  }
4036
4734
  return sid;
4037
4735
  }
@@ -4064,8 +4762,8 @@ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
4064
4762
  return async (span) => {
4065
4763
  const ev = buildErrorEventForReceiver(span, graph, scanPath);
4066
4764
  if (!ev) return;
4067
- await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
4068
- await import_node_fs6.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4765
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
4766
+ await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4069
4767
  };
4070
4768
  }
4071
4769
  async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
@@ -4156,7 +4854,7 @@ function findRouteNodeByHttpRoute(graph, serviceName, method, httpRoute) {
4156
4854
  graph.forEachNode((id, attrs) => {
4157
4855
  if (found) return;
4158
4856
  const a = attrs;
4159
- if (a.type !== import_types7.NodeType.RouteNode || a.service !== serviceName) return;
4857
+ if (a.type !== import_types8.NodeType.RouteNode || a.service !== serviceName) return;
4160
4858
  if (m && a.method !== "ALL" && a.method !== m) return;
4161
4859
  if (normalizePathTemplate(a.pathTemplate) === target) found = id;
4162
4860
  });
@@ -4187,7 +4885,7 @@ async function handleSpan(ctx, span) {
4187
4885
  let targetId;
4188
4886
  if (host) {
4189
4887
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
4190
- targetId = (0, import_types7.databaseId)(host);
4888
+ targetId = (0, import_types8.databaseId)(host);
4191
4889
  } else {
4192
4890
  const declared = findDeclaredDatabaseForService(ctx.graph, sourceId, span.dbSystem);
4193
4891
  if (declared) {
@@ -4204,7 +4902,7 @@ async function handleSpan(ctx, span) {
4204
4902
  }
4205
4903
  const result = upsertObservedEdge(
4206
4904
  ctx.graph,
4207
- import_types7.EdgeType.CONNECTS_TO,
4905
+ import_types8.EdgeType.CONNECTS_TO,
4208
4906
  observedSource(),
4209
4907
  targetId,
4210
4908
  ts,
@@ -4216,7 +4914,7 @@ async function handleSpan(ctx, span) {
4216
4914
  const collectionId = ensureInfraNode(ctx.graph, "mongodb-collection", span.dbCollection, "self");
4217
4915
  upsertObservedEdge(
4218
4916
  ctx.graph,
4219
- import_types7.EdgeType.CALLS,
4917
+ import_types8.EdgeType.CALLS,
4220
4918
  observedSource(),
4221
4919
  collectionId,
4222
4920
  ts,
@@ -4228,7 +4926,7 @@ async function handleSpan(ctx, span) {
4228
4926
  const tableId = ensureInfraNode(ctx.graph, "sql-table", span.dbTable, "self");
4229
4927
  upsertObservedEdge(
4230
4928
  ctx.graph,
4231
- import_types7.EdgeType.CALLS,
4929
+ import_types8.EdgeType.CALLS,
4232
4930
  observedSource(),
4233
4931
  tableId,
4234
4932
  ts,
@@ -4244,7 +4942,7 @@ async function handleSpan(ctx, span) {
4244
4942
  span.messagingSystem,
4245
4943
  span.messagingDestination
4246
4944
  );
4247
- const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types7.EdgeType.CONSUMES_FROM : import_types7.EdgeType.PUBLISHES_TO;
4945
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types8.EdgeType.CONSUMES_FROM : import_types8.EdgeType.PUBLISHES_TO;
4248
4946
  const result = upsertObservedEdge(
4249
4947
  ctx.graph,
4250
4948
  edgeType,
@@ -4264,7 +4962,7 @@ async function handleSpan(ctx, span) {
4264
4962
  );
4265
4963
  const result = upsertObservedEdge(
4266
4964
  ctx.graph,
4267
- import_types7.EdgeType.CONTAINS,
4965
+ import_types8.EdgeType.CONTAINS,
4268
4966
  observedSource(),
4269
4967
  targetId,
4270
4968
  ts,
@@ -4276,7 +4974,7 @@ async function handleSpan(ctx, span) {
4276
4974
  const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
4277
4975
  const result = upsertObservedEdge(
4278
4976
  ctx.graph,
4279
- import_types7.EdgeType.CONTAINS,
4977
+ import_types8.EdgeType.CONTAINS,
4280
4978
  observedSource(),
4281
4979
  targetId,
4282
4980
  ts,
@@ -4292,7 +4990,7 @@ async function handleSpan(ctx, span) {
4292
4990
  );
4293
4991
  const result = upsertObservedEdge(
4294
4992
  ctx.graph,
4295
- import_types7.EdgeType.CONNECTS_TO,
4993
+ import_types8.EdgeType.CONNECTS_TO,
4296
4994
  observedSource(),
4297
4995
  targetId,
4298
4996
  ts,
@@ -4308,7 +5006,7 @@ async function handleSpan(ctx, span) {
4308
5006
  if (targetId && targetId !== sourceId) {
4309
5007
  upsertObservedEdge(
4310
5008
  ctx.graph,
4311
- import_types7.EdgeType.CALLS,
5009
+ import_types8.EdgeType.CALLS,
4312
5010
  observedSource(),
4313
5011
  targetId,
4314
5012
  ts,
@@ -4321,7 +5019,7 @@ async function handleSpan(ctx, span) {
4321
5019
  const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
4322
5020
  upsertObservedEdge(
4323
5021
  ctx.graph,
4324
- import_types7.EdgeType.CALLS,
5022
+ import_types8.EdgeType.CALLS,
4325
5023
  observedSource(),
4326
5024
  frontierNodeId,
4327
5025
  ts,
@@ -4347,7 +5045,7 @@ async function handleSpan(ctx, span) {
4347
5045
  } : void 0;
4348
5046
  upsertObservedEdge(
4349
5047
  ctx.graph,
4350
- import_types7.EdgeType.CALLS,
5048
+ import_types8.EdgeType.CALLS,
4351
5049
  fallbackSource,
4352
5050
  sourceId,
4353
5051
  ts,
@@ -4366,7 +5064,7 @@ async function handleSpan(ctx, span) {
4366
5064
  );
4367
5065
  if (routeNodeId) {
4368
5066
  const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
4369
- upsertObservedEdge(ctx.graph, import_types7.EdgeType.CONTAINS, (0, import_types7.serviceId)(routeSvc), routeNodeId, ts, isError);
5067
+ upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
4370
5068
  }
4371
5069
  }
4372
5070
  if (span.statusCode === 2) {
@@ -4405,7 +5103,7 @@ function promoteFrontierNodes(graph, opts = {}) {
4405
5103
  const aliasIndex = /* @__PURE__ */ new Map();
4406
5104
  graph.forEachNode((id, attrs) => {
4407
5105
  const a = attrs;
4408
- if (a.type !== import_types7.NodeType.ServiceNode) return;
5106
+ if (a.type !== import_types8.NodeType.ServiceNode) return;
4409
5107
  aliasIndex.set(a.name, id);
4410
5108
  if (a.aliases) {
4411
5109
  for (const alias of a.aliases) aliasIndex.set(alias, id);
@@ -4414,7 +5112,7 @@ function promoteFrontierNodes(graph, opts = {}) {
4414
5112
  const toPromote = [];
4415
5113
  graph.forEachNode((id, attrs) => {
4416
5114
  const a = attrs;
4417
- if (a.type !== import_types7.NodeType.FrontierNode) return;
5115
+ if (a.type !== import_types8.NodeType.FrontierNode) return;
4418
5116
  const target = aliasIndex.get(a.host);
4419
5117
  if (!target) return;
4420
5118
  if (target === id) return;
@@ -4448,7 +5146,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId7) {
4448
5146
  }
4449
5147
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
4450
5148
  graph.dropEdge(oldEdgeId);
4451
- const newId = edge.provenance === import_types7.Provenance.OBSERVED ? (0, import_types7.observedEdgeId)(newSource, newTarget, edge.type) : edge.provenance === import_types7.Provenance.INFERRED ? (0, import_types7.inferredEdgeId)(newSource, newTarget, edge.type) : (0, import_types7.extractedEdgeId)(newSource, newTarget, edge.type);
5149
+ const newId = edge.provenance === import_types8.Provenance.OBSERVED ? (0, import_types8.observedEdgeId)(newSource, newTarget, edge.type) : edge.provenance === import_types8.Provenance.INFERRED ? (0, import_types8.inferredEdgeId)(newSource, newTarget, edge.type) : (0, import_types8.extractedEdgeId)(newSource, newTarget, edge.type);
4452
5150
  if (graph.hasEdge(newId)) {
4453
5151
  const existing = graph.getEdgeAttributes(newId);
4454
5152
  const merged = {
@@ -4482,12 +5180,12 @@ async function markStaleEdges(graph, options = {}) {
4482
5180
  const project = options.project ?? DEFAULT_PROJECT;
4483
5181
  graph.forEachEdge((id, attrs) => {
4484
5182
  const e = attrs;
4485
- if (e.provenance !== import_types7.Provenance.OBSERVED) return;
5183
+ if (e.provenance !== import_types8.Provenance.OBSERVED) return;
4486
5184
  if (!e.lastObserved) return;
4487
5185
  const threshold = thresholdForEdgeType(e.type, thresholds);
4488
5186
  const age = now - new Date(e.lastObserved).getTime();
4489
5187
  if (age > threshold) {
4490
- const updated = { ...e, provenance: import_types7.Provenance.STALE, confidence: 0.3 };
5188
+ const updated = { ...e, provenance: import_types8.Provenance.STALE, confidence: 0.3 };
4491
5189
  graph.replaceEdgeAttributes(id, updated);
4492
5190
  events.push({
4493
5191
  edgeId: id,
@@ -4504,8 +5202,8 @@ async function markStaleEdges(graph, options = {}) {
4504
5202
  project,
4505
5203
  payload: {
4506
5204
  edgeId: id,
4507
- from: import_types7.Provenance.OBSERVED,
4508
- to: import_types7.Provenance.STALE
5205
+ from: import_types8.Provenance.OBSERVED,
5206
+ to: import_types8.Provenance.STALE
4509
5207
  }
4510
5208
  });
4511
5209
  }
@@ -4516,13 +5214,13 @@ async function markStaleEdges(graph, options = {}) {
4516
5214
  return { count: events.length, events };
4517
5215
  }
4518
5216
  async function appendStaleEvents(staleEventsPath, events) {
4519
- await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(staleEventsPath), { recursive: true });
5217
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(staleEventsPath), { recursive: true });
4520
5218
  const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
4521
- await import_node_fs6.promises.appendFile(staleEventsPath, lines, "utf8");
5219
+ await import_node_fs7.promises.appendFile(staleEventsPath, lines, "utf8");
4522
5220
  }
4523
5221
  async function readStaleEvents(staleEventsPath) {
4524
5222
  try {
4525
- const raw = await import_node_fs6.promises.readFile(staleEventsPath, "utf8");
5223
+ const raw = await import_node_fs7.promises.readFile(staleEventsPath, "utf8");
4526
5224
  return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
4527
5225
  } catch (err) {
4528
5226
  if (err.code === "ENOENT") return [];
@@ -4556,7 +5254,7 @@ function startStalenessLoop(graph, options = {}) {
4556
5254
  }
4557
5255
  async function readErrorEvents(errorsPath) {
4558
5256
  try {
4559
- const raw = await import_node_fs6.promises.readFile(errorsPath, "utf8");
5257
+ const raw = await import_node_fs7.promises.readFile(errorsPath, "utf8");
4560
5258
  const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
4561
5259
  return dedupeIncidents(events);
4562
5260
  } catch (err) {
@@ -4614,7 +5312,7 @@ function mergeSnapshot(graph, snapshot) {
4614
5312
  const validEdges = [];
4615
5313
  for (const node of incomingNodes) {
4616
5314
  if (node.attributes === void 0) continue;
4617
- const parsed = import_types7.GraphNodeSchema.safeParse(node.attributes);
5315
+ const parsed = import_types8.GraphNodeSchema.safeParse(node.attributes);
4618
5316
  if (!parsed.success) {
4619
5317
  issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
4620
5318
  continue;
@@ -4623,7 +5321,7 @@ function mergeSnapshot(graph, snapshot) {
4623
5321
  }
4624
5322
  for (const edge of incomingEdges) {
4625
5323
  if (edge.attributes === void 0) continue;
4626
- const parsed = import_types7.GraphEdgeSchema.safeParse(edge.attributes);
5324
+ const parsed = import_types8.GraphEdgeSchema.safeParse(edge.attributes);
4627
5325
  if (!parsed.success) {
4628
5326
  const label = edge.key ?? `${edge.source}->${edge.target}`;
4629
5327
  issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
@@ -4653,16 +5351,16 @@ function mergeSnapshot(graph, snapshot) {
4653
5351
 
4654
5352
  // src/extract/services.ts
4655
5353
  init_cjs_shims();
4656
- var import_node_fs10 = require("fs");
4657
- var import_node_path11 = __toESM(require("path"), 1);
5354
+ var import_node_fs11 = require("fs");
5355
+ var import_node_path12 = __toESM(require("path"), 1);
4658
5356
  var import_ignore = __toESM(require("ignore"), 1);
4659
5357
  var import_minimatch2 = require("minimatch");
4660
- var import_types9 = require("@neat.is/types");
5358
+ var import_types10 = require("@neat.is/types");
4661
5359
 
4662
5360
  // src/extract/python.ts
4663
5361
  init_cjs_shims();
4664
- var import_node_fs7 = require("fs");
4665
- var import_node_path8 = __toESM(require("path"), 1);
5362
+ var import_node_fs8 = require("fs");
5363
+ var import_node_path9 = __toESM(require("path"), 1);
4666
5364
  var import_smol_toml = require("smol-toml");
4667
5365
  var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
4668
5366
  function parseRequirementsTxt(content) {
@@ -4695,25 +5393,25 @@ function depsFromPyProject(pyproject) {
4695
5393
  return out;
4696
5394
  }
4697
5395
  async function discoverPythonService(serviceDir) {
4698
- const pyprojectPath = import_node_path8.default.join(serviceDir, "pyproject.toml");
4699
- const requirementsPath = import_node_path8.default.join(serviceDir, "requirements.txt");
4700
- const setupPath = import_node_path8.default.join(serviceDir, "setup.py");
5396
+ const pyprojectPath = import_node_path9.default.join(serviceDir, "pyproject.toml");
5397
+ const requirementsPath = import_node_path9.default.join(serviceDir, "requirements.txt");
5398
+ const setupPath = import_node_path9.default.join(serviceDir, "setup.py");
4701
5399
  const hasPyproject = await exists(pyprojectPath);
4702
5400
  const hasRequirements = await exists(requirementsPath);
4703
5401
  const hasSetup = await exists(setupPath);
4704
5402
  if (!hasPyproject && !hasRequirements && !hasSetup) return null;
4705
- let name = import_node_path8.default.basename(serviceDir);
5403
+ let name = import_node_path9.default.basename(serviceDir);
4706
5404
  let version;
4707
5405
  const dependencies = {};
4708
5406
  if (hasPyproject) {
4709
- const raw = await import_node_fs7.promises.readFile(pyprojectPath, "utf8");
5407
+ const raw = await import_node_fs8.promises.readFile(pyprojectPath, "utf8");
4710
5408
  const pyproject = (0, import_smol_toml.parse)(raw);
4711
5409
  name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
4712
5410
  version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
4713
5411
  Object.assign(dependencies, depsFromPyProject(pyproject));
4714
5412
  }
4715
5413
  if (hasRequirements) {
4716
- const raw = await import_node_fs7.promises.readFile(requirementsPath, "utf8");
5414
+ const raw = await import_node_fs8.promises.readFile(requirementsPath, "utf8");
4717
5415
  Object.assign(dependencies, parseRequirementsTxt(raw));
4718
5416
  }
4719
5417
  return { name, version, dependencies };
@@ -4728,9 +5426,9 @@ function pythonToPackage(service) {
4728
5426
 
4729
5427
  // src/extract/go.ts
4730
5428
  init_cjs_shims();
4731
- var import_node_fs8 = require("fs");
4732
- var import_node_path9 = __toESM(require("path"), 1);
4733
- var import_types8 = require("@neat.is/types");
5429
+ var import_node_fs9 = require("fs");
5430
+ var import_node_path10 = __toESM(require("path"), 1);
5431
+ var import_types9 = require("@neat.is/types");
4734
5432
  function parseGoMod(source) {
4735
5433
  const module2 = source.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
4736
5434
  if (!module2) return null;
@@ -4749,7 +5447,7 @@ function parseGoMod(source) {
4749
5447
  async function discoverGoService(scanPath, dir) {
4750
5448
  let raw;
4751
5449
  try {
4752
- raw = await import_node_fs8.promises.readFile(import_node_path9.default.join(dir, "go.mod"), "utf8");
5450
+ raw = await import_node_fs9.promises.readFile(import_node_path10.default.join(dir, "go.mod"), "utf8");
4753
5451
  } catch {
4754
5452
  return null;
4755
5453
  }
@@ -4758,12 +5456,12 @@ async function discoverGoService(scanPath, dir) {
4758
5456
  const name = mod.module.split("/").filter(Boolean).pop() ?? mod.module;
4759
5457
  const pkg = { name, dependencies: mod.dependencies };
4760
5458
  const node = {
4761
- id: (0, import_types8.serviceId)(name),
4762
- type: import_types8.NodeType.ServiceNode,
5459
+ id: (0, import_types9.serviceId)(name),
5460
+ type: import_types9.NodeType.ServiceNode,
4763
5461
  name,
4764
5462
  language: "go",
4765
5463
  dependencies: mod.dependencies,
4766
- repoPath: import_node_path9.default.relative(scanPath, dir),
5464
+ repoPath: import_node_path10.default.relative(scanPath, dir),
4767
5465
  ...mod.dependencies["github.com/gin-gonic/gin"] ? { framework: "gin" } : {}
4768
5466
  };
4769
5467
  return { pkg, dir, node };
@@ -4771,17 +5469,17 @@ async function discoverGoService(scanPath, dir) {
4771
5469
 
4772
5470
  // src/extract/owners.ts
4773
5471
  init_cjs_shims();
4774
- var import_node_fs9 = require("fs");
4775
- var import_node_path10 = __toESM(require("path"), 1);
5472
+ var import_node_fs10 = require("fs");
5473
+ var import_node_path11 = __toESM(require("path"), 1);
4776
5474
  var import_minimatch = require("minimatch");
4777
5475
  async function loadCodeowners(scanPath) {
4778
5476
  const candidates = [
4779
- import_node_path10.default.join(scanPath, "CODEOWNERS"),
4780
- import_node_path10.default.join(scanPath, ".github", "CODEOWNERS")
5477
+ import_node_path11.default.join(scanPath, "CODEOWNERS"),
5478
+ import_node_path11.default.join(scanPath, ".github", "CODEOWNERS")
4781
5479
  ];
4782
5480
  for (const file of candidates) {
4783
5481
  if (await exists(file)) {
4784
- const raw = await import_node_fs9.promises.readFile(file, "utf8");
5482
+ const raw = await import_node_fs10.promises.readFile(file, "utf8");
4785
5483
  return parseCodeowners(raw);
4786
5484
  }
4787
5485
  }
@@ -4799,7 +5497,7 @@ function parseCodeowners(raw) {
4799
5497
  return { rules };
4800
5498
  }
4801
5499
  function matchOwner(file, repoPath) {
4802
- const normalized = repoPath.split(import_node_path10.default.sep).join("/");
5500
+ const normalized = repoPath.split(import_node_path11.default.sep).join("/");
4803
5501
  for (const rule of file.rules) {
4804
5502
  if (matchesPattern(rule.pattern, normalized)) return rule.owners;
4805
5503
  }
@@ -4815,7 +5513,7 @@ function matchesPattern(rawPattern, repoPath) {
4815
5513
  return false;
4816
5514
  }
4817
5515
  async function readPackageJsonAuthor(serviceDir) {
4818
- const pkgPath = import_node_path10.default.join(serviceDir, "package.json");
5516
+ const pkgPath = import_node_path11.default.join(serviceDir, "package.json");
4819
5517
  if (!await exists(pkgPath)) return null;
4820
5518
  try {
4821
5519
  const pkg = await readJson(pkgPath);
@@ -4852,27 +5550,27 @@ function workspaceGlobs(pkg) {
4852
5550
  return null;
4853
5551
  }
4854
5552
  async function hasPythonManifest(dir) {
4855
- return await exists(import_node_path11.default.join(dir, "pyproject.toml")) || await exists(import_node_path11.default.join(dir, "requirements.txt")) || await exists(import_node_path11.default.join(dir, "setup.py"));
5553
+ return await exists(import_node_path12.default.join(dir, "pyproject.toml")) || await exists(import_node_path12.default.join(dir, "requirements.txt")) || await exists(import_node_path12.default.join(dir, "setup.py"));
4856
5554
  }
4857
5555
  async function hasGoManifest(dir) {
4858
- return exists(import_node_path11.default.join(dir, "go.mod"));
5556
+ return exists(import_node_path12.default.join(dir, "go.mod"));
4859
5557
  }
4860
5558
  async function loadGitignore(scanPath) {
4861
- const gitignorePath = import_node_path11.default.join(scanPath, ".gitignore");
5559
+ const gitignorePath = import_node_path12.default.join(scanPath, ".gitignore");
4862
5560
  if (!await exists(gitignorePath)) return null;
4863
- const raw = await import_node_fs10.promises.readFile(gitignorePath, "utf8");
5561
+ const raw = await import_node_fs11.promises.readFile(gitignorePath, "utf8");
4864
5562
  return (0, import_ignore.default)().add(raw);
4865
5563
  }
4866
5564
  async function walkDirs(start, scanPath, options, visit) {
4867
5565
  async function recurse(current, depth) {
4868
5566
  if (depth > options.maxDepth) return;
4869
- const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
5567
+ const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
4870
5568
  for (const entry of entries) {
4871
5569
  if (!entry.isDirectory()) continue;
4872
5570
  if (IGNORED_DIRS.has(entry.name)) continue;
4873
- const child = import_node_path11.default.join(current, entry.name);
5571
+ const child = import_node_path12.default.join(current, entry.name);
4874
5572
  if (options.ig) {
4875
- const rel = import_node_path11.default.relative(scanPath, child).split(import_node_path11.default.sep).join("/");
5573
+ const rel = import_node_path12.default.relative(scanPath, child).split(import_node_path12.default.sep).join("/");
4876
5574
  if (rel && options.ig.ignores(rel + "/")) continue;
4877
5575
  }
4878
5576
  if (await isPythonVenvDir(child)) continue;
@@ -4888,8 +5586,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
4888
5586
  for (const raw of globs) {
4889
5587
  const pattern = raw.replace(/^\.\//, "");
4890
5588
  if (!pattern.includes("*")) {
4891
- const candidate = import_node_path11.default.join(scanPath, pattern);
4892
- if (await exists(import_node_path11.default.join(candidate, "package.json"))) found.add(candidate);
5589
+ const candidate = import_node_path12.default.join(scanPath, pattern);
5590
+ if (await exists(import_node_path12.default.join(candidate, "package.json"))) found.add(candidate);
4893
5591
  continue;
4894
5592
  }
4895
5593
  const segments = pattern.split("/");
@@ -4898,13 +5596,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
4898
5596
  if (seg.includes("*")) break;
4899
5597
  staticSegments.push(seg);
4900
5598
  }
4901
- const start = import_node_path11.default.join(scanPath, ...staticSegments);
5599
+ const start = import_node_path12.default.join(scanPath, ...staticSegments);
4902
5600
  if (!await exists(start)) continue;
4903
5601
  const hasDoubleStar = pattern.includes("**");
4904
5602
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
4905
5603
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
4906
- const rel = import_node_path11.default.relative(scanPath, dir).split(import_node_path11.default.sep).join("/");
4907
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path11.default.join(dir, "package.json"))) {
5604
+ const rel = import_node_path12.default.relative(scanPath, dir).split(import_node_path12.default.sep).join("/");
5605
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path12.default.join(dir, "package.json"))) {
4908
5606
  found.add(dir);
4909
5607
  }
4910
5608
  });
@@ -4927,31 +5625,31 @@ function detectJsFramework(pkg) {
4927
5625
  async function detectJsServiceLanguage(dir, pkg) {
4928
5626
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4929
5627
  if (deps["typescript"] !== void 0) return "typescript";
4930
- const entries = await import_node_fs10.promises.readdir(dir).catch(() => []);
5628
+ const entries = await import_node_fs11.promises.readdir(dir).catch(() => []);
4931
5629
  if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
4932
5630
  return "javascript";
4933
5631
  }
4934
5632
  async function discoverNodeService(scanPath, dir) {
4935
- const pkgPath = import_node_path11.default.join(dir, "package.json");
5633
+ const pkgPath = import_node_path12.default.join(dir, "package.json");
4936
5634
  if (!await exists(pkgPath)) return null;
4937
5635
  let pkg;
4938
5636
  try {
4939
5637
  pkg = await readJson(pkgPath);
4940
5638
  } catch (err) {
4941
- recordExtractionError("services", import_node_path11.default.relative(scanPath, pkgPath), err);
5639
+ recordExtractionError("services", import_node_path12.default.relative(scanPath, pkgPath), err);
4942
5640
  return null;
4943
5641
  }
4944
5642
  if (!pkg.name) return null;
4945
5643
  const framework = detectJsFramework(pkg);
4946
5644
  const language = await detectJsServiceLanguage(dir, pkg);
4947
5645
  const node = {
4948
- id: (0, import_types9.serviceId)(pkg.name),
4949
- type: import_types9.NodeType.ServiceNode,
5646
+ id: (0, import_types10.serviceId)(pkg.name),
5647
+ type: import_types10.NodeType.ServiceNode,
4950
5648
  name: pkg.name,
4951
5649
  language,
4952
5650
  version: pkg.version,
4953
5651
  dependencies: pkg.dependencies ?? {},
4954
- repoPath: import_node_path11.default.relative(scanPath, dir),
5652
+ repoPath: import_node_path12.default.relative(scanPath, dir),
4955
5653
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
4956
5654
  ...framework ? { framework } : {}
4957
5655
  };
@@ -4962,18 +5660,18 @@ async function discoverPyService(scanPath, dir) {
4962
5660
  if (!py) return null;
4963
5661
  const pkg = pythonToPackage(py);
4964
5662
  const node = {
4965
- id: (0, import_types9.serviceId)(py.name),
4966
- type: import_types9.NodeType.ServiceNode,
5663
+ id: (0, import_types10.serviceId)(py.name),
5664
+ type: import_types10.NodeType.ServiceNode,
4967
5665
  name: py.name,
4968
5666
  language: "python",
4969
5667
  version: py.version,
4970
5668
  dependencies: py.dependencies,
4971
- repoPath: import_node_path11.default.relative(scanPath, dir)
5669
+ repoPath: import_node_path12.default.relative(scanPath, dir)
4972
5670
  };
4973
5671
  return { pkg, dir, node };
4974
5672
  }
4975
5673
  async function discoverServices(scanPath) {
4976
- const rootPkgPath = import_node_path11.default.join(scanPath, "package.json");
5674
+ const rootPkgPath = import_node_path12.default.join(scanPath, "package.json");
4977
5675
  let rootPkg = null;
4978
5676
  if (await exists(rootPkgPath)) {
4979
5677
  try {
@@ -4981,7 +5679,7 @@ async function discoverServices(scanPath) {
4981
5679
  } catch (err) {
4982
5680
  recordExtractionError(
4983
5681
  "services workspaces",
4984
- import_node_path11.default.relative(scanPath, rootPkgPath),
5682
+ import_node_path12.default.relative(scanPath, rootPkgPath),
4985
5683
  err
4986
5684
  );
4987
5685
  }
@@ -5002,7 +5700,7 @@ async function discoverServices(scanPath) {
5002
5700
  scanPath,
5003
5701
  { maxDepth: parseScanDepth(), ig },
5004
5702
  async (dir) => {
5005
- if (await exists(import_node_path11.default.join(dir, "package.json"))) {
5703
+ if (await exists(import_node_path12.default.join(dir, "package.json"))) {
5006
5704
  candidateDirs.push(dir);
5007
5705
  } else if (await hasPythonManifest(dir) || await hasGoManifest(dir)) {
5008
5706
  candidateDirs.push(dir);
@@ -5018,8 +5716,8 @@ async function discoverServices(scanPath) {
5018
5716
  if (!service) continue;
5019
5717
  const existingDir = seen.get(service.node.name);
5020
5718
  if (existingDir !== void 0) {
5021
- const a = import_node_path11.default.relative(scanPath, existingDir) || ".";
5022
- const b = import_node_path11.default.relative(scanPath, dir) || ".";
5719
+ const a = import_node_path12.default.relative(scanPath, existingDir) || ".";
5720
+ const b = import_node_path12.default.relative(scanPath, dir) || ".";
5023
5721
  console.warn(
5024
5722
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
5025
5723
  );
@@ -5056,10 +5754,10 @@ function addServiceNodes(graph, services) {
5056
5754
 
5057
5755
  // src/extract/aliases.ts
5058
5756
  init_cjs_shims();
5059
- var import_node_path12 = __toESM(require("path"), 1);
5060
- var import_node_fs11 = require("fs");
5757
+ var import_node_path13 = __toESM(require("path"), 1);
5758
+ var import_node_fs12 = require("fs");
5061
5759
  var import_yaml2 = require("yaml");
5062
- var import_types10 = require("@neat.is/types");
5760
+ var import_types11 = require("@neat.is/types");
5063
5761
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
5064
5762
  "Service",
5065
5763
  "Deployment",
@@ -5069,7 +5767,7 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
5069
5767
  function addAliases(graph, serviceId7, candidates) {
5070
5768
  if (!graph.hasNode(serviceId7)) return;
5071
5769
  const node = graph.getNodeAttributes(serviceId7);
5072
- if (node.type !== import_types10.NodeType.ServiceNode) return;
5770
+ if (node.type !== import_types11.NodeType.ServiceNode) return;
5073
5771
  const set = new Set(node.aliases ?? []);
5074
5772
  for (const c of candidates) {
5075
5773
  if (!c) continue;
@@ -5084,14 +5782,14 @@ function indexServicesByName(services) {
5084
5782
  const map = /* @__PURE__ */ new Map();
5085
5783
  for (const s of services) {
5086
5784
  map.set(s.node.name, s.node.id);
5087
- map.set(import_node_path12.default.basename(s.dir), s.node.id);
5785
+ map.set(import_node_path13.default.basename(s.dir), s.node.id);
5088
5786
  }
5089
5787
  return map;
5090
5788
  }
5091
5789
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
5092
5790
  let composePath = null;
5093
5791
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5094
- const abs = import_node_path12.default.join(scanPath, name);
5792
+ const abs = import_node_path13.default.join(scanPath, name);
5095
5793
  if (await exists(abs)) {
5096
5794
  composePath = abs;
5097
5795
  break;
@@ -5104,7 +5802,7 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
5104
5802
  } catch (err) {
5105
5803
  recordExtractionError(
5106
5804
  "aliases compose",
5107
- import_node_path12.default.relative(scanPath, composePath),
5805
+ import_node_path13.default.relative(scanPath, composePath),
5108
5806
  err
5109
5807
  );
5110
5808
  return;
@@ -5147,11 +5845,11 @@ function parseDockerfileLabels(content) {
5147
5845
  }
5148
5846
  async function collectDockerfileAliases(graph, services) {
5149
5847
  for (const service of services) {
5150
- const dockerfilePath = import_node_path12.default.join(service.dir, "Dockerfile");
5848
+ const dockerfilePath = import_node_path13.default.join(service.dir, "Dockerfile");
5151
5849
  if (!await exists(dockerfilePath)) continue;
5152
5850
  let content;
5153
5851
  try {
5154
- content = await import_node_fs11.promises.readFile(dockerfilePath, "utf8");
5852
+ content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
5155
5853
  } catch (err) {
5156
5854
  recordExtractionError("aliases dockerfile", dockerfilePath, err);
5157
5855
  continue;
@@ -5163,15 +5861,15 @@ async function collectDockerfileAliases(graph, services) {
5163
5861
  async function walkYamlFiles(start, depth = 0, max = 5) {
5164
5862
  if (depth > max) return [];
5165
5863
  const out = [];
5166
- const entries = await import_node_fs11.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5864
+ const entries = await import_node_fs12.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5167
5865
  for (const entry of entries) {
5168
5866
  if (entry.isDirectory()) {
5169
5867
  if (IGNORED_DIRS.has(entry.name)) continue;
5170
- const child = import_node_path12.default.join(start, entry.name);
5868
+ const child = import_node_path13.default.join(start, entry.name);
5171
5869
  if (await isPythonVenvDir(child)) continue;
5172
5870
  out.push(...await walkYamlFiles(child, depth + 1, max));
5173
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name))) {
5174
- out.push(import_node_path12.default.join(start, entry.name));
5871
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path13.default.extname(entry.name))) {
5872
+ out.push(import_node_path13.default.join(start, entry.name));
5175
5873
  }
5176
5874
  }
5177
5875
  return out;
@@ -5198,614 +5896,230 @@ function k8sServiceTarget(doc, byName) {
5198
5896
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
5199
5897
  const files = await walkYamlFiles(scanPath);
5200
5898
  for (const file of files) {
5201
- const content = await import_node_fs11.promises.readFile(file, "utf8");
5899
+ const content = await import_node_fs12.promises.readFile(file, "utf8");
5202
5900
  let docs;
5203
5901
  try {
5204
- docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
5205
- } catch {
5206
- continue;
5207
- }
5208
- for (const doc of docs) {
5209
- if (!doc?.kind || !doc.metadata?.name) continue;
5210
- if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
5211
- const target = k8sServiceTarget(doc, serviceIndex);
5212
- if (!target) continue;
5213
- addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
5214
- }
5215
- }
5216
- }
5217
- async function addServiceAliases(graph, scanPath, services) {
5218
- const byName = indexServicesByName(services);
5219
- await collectComposeAliases(graph, scanPath, byName);
5220
- await collectDockerfileAliases(graph, services);
5221
- await collectK8sAliases(graph, scanPath, byName);
5222
- }
5223
-
5224
- // src/extract/files.ts
5225
- init_cjs_shims();
5226
- var import_node_path13 = __toESM(require("path"), 1);
5227
- async function addFiles(graph, services) {
5228
- let nodesAdded = 0;
5229
- let edgesAdded = 0;
5230
- for (const service of services) {
5231
- const filePaths = await walkSourceFiles(service.dir);
5232
- for (const filePath of filePaths) {
5233
- const relPath = toPosix(import_node_path13.default.relative(service.dir, filePath));
5234
- const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
5235
- graph,
5236
- service.pkg.name,
5237
- service.node.id,
5238
- relPath
5239
- );
5240
- nodesAdded += n;
5241
- edgesAdded += e;
5242
- }
5243
- }
5244
- return { nodesAdded, edgesAdded };
5245
- }
5246
-
5247
- // src/extract/symbols.ts
5248
- init_cjs_shims();
5249
- var import_node_path14 = __toESM(require("path"), 1);
5250
- var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5251
- var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5252
- var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
5253
- var import_types11 = require("@neat.is/types");
5254
- var PARSE_CHUNK2 = 16384;
5255
- var GRAMMAR_BY_EXT = {
5256
- ".ts": import_tree_sitter_typescript.default.typescript,
5257
- ".tsx": import_tree_sitter_typescript.default.tsx,
5258
- ".js": import_tree_sitter_javascript2.default,
5259
- ".jsx": import_tree_sitter_javascript2.default,
5260
- ".mjs": import_tree_sitter_javascript2.default,
5261
- ".cjs": import_tree_sitter_javascript2.default
5262
- };
5263
- function parseSource2(parser, source) {
5264
- return parser.parse(
5265
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5266
- );
5267
- }
5268
- function methodName(node) {
5269
- const name = node.childForFieldName("name");
5270
- return name ? name.text : null;
5271
- }
5272
- function collectSymbolDefs(root) {
5273
- const out = [];
5274
- const push = (kind, qualname, node) => {
5275
- out.push({
5276
- kind,
5277
- qualname,
5278
- startLine: node.startPosition.row + 1,
5279
- endLine: node.endPosition.row + 1
5280
- });
5281
- };
5282
- const visit = (node, classCtx) => {
5283
- switch (node.type) {
5284
- case "function_declaration":
5285
- case "generator_function_declaration": {
5286
- const name = node.childForFieldName("name")?.text;
5287
- if (name) push("function", name, node);
5288
- break;
5289
- }
5290
- case "class_declaration":
5291
- case "abstract_class_declaration":
5292
- case "class": {
5293
- const name = node.childForFieldName("name")?.text;
5294
- if (name) push("class", name, node);
5295
- const body = node.childForFieldName("body");
5296
- if (body) {
5297
- for (let i = 0; i < body.namedChildCount; i++) {
5298
- const child = body.namedChild(i);
5299
- if (child) visit(child, name ?? classCtx);
5300
- }
5301
- }
5302
- return;
5303
- }
5304
- case "method_definition": {
5305
- const name = methodName(node);
5306
- if (name) {
5307
- const kind = name === "constructor" ? "constructor" : "method";
5308
- push(kind, classCtx ? `${classCtx}.${name}` : name, node);
5309
- }
5310
- break;
5311
- }
5312
- case "variable_declarator": {
5313
- const value = node.childForFieldName("value");
5314
- if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
5315
- const nameNode = node.childForFieldName("name");
5316
- if (nameNode && nameNode.type === "identifier") {
5317
- push("function", nameNode.text, node);
5318
- }
5319
- }
5320
- break;
5321
- }
5322
- }
5323
- for (let i = 0; i < node.namedChildCount; i++) {
5324
- const child = node.namedChild(i);
5325
- if (child) visit(child, classCtx);
5326
- }
5327
- };
5328
- visit(root, void 0);
5329
- return out;
5330
- }
5331
- function disambiguate(defs) {
5332
- const counts = /* @__PURE__ */ new Map();
5333
- for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
5334
- const seen = /* @__PURE__ */ new Map();
5335
- return defs.map((def) => {
5336
- if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
5337
- const ordinal = seen.get(def.qualname) ?? 0;
5338
- seen.set(def.qualname, ordinal + 1);
5339
- return { def, disambiguator: ordinal };
5340
- });
5341
- }
5342
- async function addSymbols(graph, services) {
5343
- const parsers = /* @__PURE__ */ new Map();
5344
- const parserForExt2 = (ext) => {
5345
- const grammar = GRAMMAR_BY_EXT[ext];
5346
- if (!grammar) return null;
5347
- let parser = parsers.get(ext);
5348
- if (!parser) {
5349
- parser = new import_tree_sitter2.default();
5350
- parser.setLanguage(grammar);
5351
- parsers.set(ext, parser);
5902
+ docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
5903
+ } catch {
5904
+ continue;
5352
5905
  }
5353
- return parser;
5354
- };
5906
+ for (const doc of docs) {
5907
+ if (!doc?.kind || !doc.metadata?.name) continue;
5908
+ if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
5909
+ const target = k8sServiceTarget(doc, serviceIndex);
5910
+ if (!target) continue;
5911
+ addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
5912
+ }
5913
+ }
5914
+ }
5915
+ async function addServiceAliases(graph, scanPath, services) {
5916
+ const byName = indexServicesByName(services);
5917
+ await collectComposeAliases(graph, scanPath, byName);
5918
+ await collectDockerfileAliases(graph, services);
5919
+ await collectK8sAliases(graph, scanPath, byName);
5920
+ }
5921
+
5922
+ // src/extract/files.ts
5923
+ init_cjs_shims();
5924
+ var import_node_path14 = __toESM(require("path"), 1);
5925
+ async function addFiles(graph, services) {
5355
5926
  let nodesAdded = 0;
5356
5927
  let edgesAdded = 0;
5357
5928
  for (const service of services) {
5358
- const files = await loadSourceFiles(service.dir);
5359
- for (const file of files) {
5360
- const parser = parserForExt2(import_node_path14.default.extname(file.path));
5361
- if (!parser) continue;
5362
- const relPath = toPosix(import_node_path14.default.relative(service.dir, file.path));
5363
- let defs;
5364
- try {
5365
- const tree = parseSource2(parser, file.content);
5366
- defs = collectSymbolDefs(tree.rootNode);
5367
- } catch (err) {
5368
- recordExtractionError("symbol extraction", file.path, err);
5369
- continue;
5370
- }
5371
- if (defs.length === 0) continue;
5372
- const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5929
+ const filePaths = await walkSourceFiles(service.dir);
5930
+ for (const filePath of filePaths) {
5931
+ const relPath = toPosix(import_node_path14.default.relative(service.dir, filePath));
5932
+ const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
5373
5933
  graph,
5374
5934
  service.pkg.name,
5375
5935
  service.node.id,
5376
5936
  relPath
5377
5937
  );
5378
- nodesAdded += fn;
5379
- edgesAdded += fe;
5380
- for (const { def, disambiguator } of disambiguate(defs)) {
5381
- const sid = (0, import_types11.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
5382
- if (!graph.hasNode(sid)) {
5383
- const node = {
5384
- id: sid,
5385
- type: import_types11.NodeType.SymbolNode,
5386
- kind: def.kind,
5387
- qualname: def.qualname,
5388
- span: { startLine: def.startLine, endLine: def.endLine },
5389
- service: service.pkg.name,
5390
- relPath,
5391
- discoveredVia: "static"
5392
- };
5393
- graph.addNode(sid, node);
5394
- nodesAdded++;
5395
- }
5396
- const containsId = (0, import_types11.extractedEdgeId)(fileNodeId, sid, import_types11.EdgeType.CONTAINS);
5397
- if (!graph.hasEdge(containsId)) {
5398
- const edge = {
5399
- id: containsId,
5400
- source: fileNodeId,
5401
- target: sid,
5402
- type: import_types11.EdgeType.CONTAINS,
5403
- provenance: import_types11.Provenance.EXTRACTED,
5404
- confidence: (0, import_types11.confidenceForExtracted)("structural"),
5405
- evidence: {
5406
- file: relPath,
5407
- line: def.startLine,
5408
- snippet: snippet(file.content, def.startLine)
5409
- }
5410
- };
5411
- graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
5412
- edgesAdded++;
5413
- }
5414
- }
5938
+ nodesAdded += n;
5939
+ edgesAdded += e;
5415
5940
  }
5416
5941
  }
5417
5942
  return { nodesAdded, edgesAdded };
5418
5943
  }
5419
5944
 
5420
- // src/extract/symbol-edges.ts
5421
- init_cjs_shims();
5422
- var import_node_path16 = __toESM(require("path"), 1);
5423
- var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
5424
- var import_types13 = require("@neat.is/types");
5425
-
5426
- // src/extract/imports.ts
5945
+ // src/extract/symbols.ts
5427
5946
  init_cjs_shims();
5428
5947
  var import_node_path15 = __toESM(require("path"), 1);
5429
- var import_node_fs12 = require("fs");
5430
5948
  var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5431
5949
  var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5432
- var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5433
- var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
5950
+ var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
5434
5951
  var import_types12 = require("@neat.is/types");
5435
5952
  var PARSE_CHUNK3 = 16384;
5953
+ var GRAMMAR_BY_EXT = {
5954
+ ".ts": import_tree_sitter_typescript.default.typescript,
5955
+ ".tsx": import_tree_sitter_typescript.default.tsx,
5956
+ ".js": import_tree_sitter_javascript3.default,
5957
+ ".jsx": import_tree_sitter_javascript3.default,
5958
+ ".mjs": import_tree_sitter_javascript3.default,
5959
+ ".cjs": import_tree_sitter_javascript3.default
5960
+ };
5436
5961
  function parseSource3(parser, source) {
5437
5962
  return parser.parse(
5438
5963
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5439
- );
5440
- }
5441
- function makeJsParser2() {
5442
- const p = new import_tree_sitter3.default();
5443
- p.setLanguage(import_tree_sitter_javascript3.default);
5444
- return p;
5445
- }
5446
- function makePyParser2() {
5447
- const p = new import_tree_sitter3.default();
5448
- p.setLanguage(import_tree_sitter_python2.default);
5449
- return p;
5450
- }
5451
- function makeGoParser2() {
5452
- const p = new import_tree_sitter3.default();
5453
- p.setLanguage(import_tree_sitter_go2.default);
5454
- return p;
5455
- }
5456
- function stringLiteralText(node) {
5457
- for (let i = 0; i < node.childCount; i++) {
5458
- const child = node.child(i);
5459
- if (child?.type === "string_fragment") return child.text;
5460
- }
5461
- const raw = node.text;
5462
- if (raw.length >= 2) return raw.slice(1, -1);
5463
- return raw.length === 0 ? null : "";
5464
- }
5465
- function clipSnippet(text) {
5466
- const oneLine = text.split("\n")[0] ?? text;
5467
- return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
5468
- }
5469
- function collectGoImports(node, out) {
5470
- if (node.type === "import_spec") {
5471
- const pathNode = node.childForFieldName("path");
5472
- if (pathNode) {
5473
- const specifier = pathNode.text.replace(/^`|`$/g, "").replace(/^"|"$/g, "");
5474
- if (specifier) out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5475
- }
5476
- return;
5477
- }
5478
- for (let i = 0; i < node.namedChildCount; i++) {
5479
- const child = node.namedChild(i);
5480
- if (child) collectGoImports(child, out);
5481
- }
5482
- }
5483
- function collectJsImports(node, out) {
5484
- if (node.type === "import_statement") {
5485
- const source = node.childForFieldName("source");
5486
- if (source) {
5487
- const specifier = stringLiteralText(source);
5488
- if (specifier) {
5489
- out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5490
- }
5491
- }
5492
- return;
5493
- }
5494
- if (node.type === "call_expression") {
5495
- const fn = node.childForFieldName("function");
5496
- if (fn?.type === "identifier" && fn.text === "require") {
5497
- const args = node.childForFieldName("arguments");
5498
- const firstArg = args?.namedChild(0);
5499
- if (firstArg?.type === "string") {
5500
- const specifier = stringLiteralText(firstArg);
5501
- if (specifier) {
5502
- out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5503
- }
5504
- }
5505
- }
5506
- }
5507
- for (let i = 0; i < node.namedChildCount; i++) {
5508
- const child = node.namedChild(i);
5509
- if (child) collectJsImports(child, out);
5510
- }
5511
- }
5512
- function collectImportedNames(node, out) {
5513
- if (node.type === "aliased_import") {
5514
- const nameNode = node.childForFieldName("name");
5515
- if (nameNode) out.push(nameNode.text);
5516
- return;
5517
- }
5518
- if (node.type === "dotted_name") {
5519
- out.push(node.text);
5520
- return;
5521
- }
5522
- for (let i = 0; i < node.namedChildCount; i++) {
5523
- const child = node.namedChild(i);
5524
- if (child) collectImportedNames(child, out);
5525
- }
5526
- }
5527
- function collectPyImports(node, out) {
5528
- if (node.type === "import_from_statement") {
5529
- let level = 0;
5530
- let modulePath = "";
5531
- const names = [];
5532
- let pastFrom = false;
5533
- let pastImport = false;
5534
- for (let i = 0; i < node.childCount; i++) {
5535
- const child = node.child(i);
5536
- if (!child) continue;
5537
- if (!pastFrom) {
5538
- if (child.type === "from") pastFrom = true;
5539
- continue;
5540
- }
5541
- if (!pastImport) {
5542
- if (child.type === "import") {
5543
- pastImport = true;
5544
- continue;
5545
- }
5546
- if (child.type === "relative_import") {
5547
- for (let j = 0; j < child.childCount; j++) {
5548
- const rc = child.child(j);
5549
- if (!rc) continue;
5550
- if (rc.type === "import_prefix") {
5551
- for (let k = 0; k < rc.childCount; k++) {
5552
- if (rc.child(k)?.type === ".") level++;
5553
- }
5554
- } else if (rc.type === "dotted_name") modulePath = rc.text;
5555
- }
5556
- } else if (child.type === "dotted_name") {
5557
- modulePath = child.text;
5558
- }
5559
- continue;
5560
- }
5561
- collectImportedNames(child, names);
5562
- }
5563
- if (level > 0 || modulePath) {
5564
- out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5565
- }
5566
- }
5567
- for (let i = 0; i < node.namedChildCount; i++) {
5568
- const child = node.namedChild(i);
5569
- if (child) collectPyImports(child, out);
5570
- }
5571
- }
5572
- async function fileExists(p) {
5573
- try {
5574
- await import_node_fs12.promises.access(p);
5575
- return true;
5576
- } catch {
5577
- return false;
5578
- }
5579
- }
5580
- function isWithinServiceDir(candidate, serviceDir) {
5581
- const rel = import_node_path15.default.relative(serviceDir, candidate);
5582
- return rel !== "" && !rel.startsWith("..") && !import_node_path15.default.isAbsolute(rel);
5583
- }
5584
- var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
5585
- var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
5586
- async function firstExistingCandidate(base, serviceDir) {
5587
- for (const ext of JS_EXTENSIONS) {
5588
- const candidate = base + ext;
5589
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5590
- return toPosix(import_node_path15.default.relative(serviceDir, candidate));
5591
- }
5592
- }
5593
- for (const indexFile of JS_INDEX_FILES) {
5594
- const candidate = import_node_path15.default.join(base, indexFile);
5595
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5596
- return toPosix(import_node_path15.default.relative(serviceDir, candidate));
5597
- }
5598
- }
5599
- return null;
5600
- }
5601
- async function loadTsPathConfig(serviceDir) {
5602
- const tsconfigPath = import_node_path15.default.join(serviceDir, "tsconfig.json");
5603
- let raw;
5604
- try {
5605
- raw = await import_node_fs12.promises.readFile(tsconfigPath, "utf8");
5606
- } catch {
5607
- return null;
5608
- }
5609
- try {
5610
- const parsed = JSON.parse(raw);
5611
- const paths = parsed.compilerOptions?.paths;
5612
- if (!paths || Object.keys(paths).length === 0) return null;
5613
- const baseUrl = parsed.compilerOptions?.baseUrl;
5614
- return { paths, baseDir: baseUrl ? import_node_path15.default.resolve(serviceDir, baseUrl) : serviceDir };
5615
- } catch (err) {
5616
- recordExtractionError("import alias resolution", tsconfigPath, err);
5617
- return null;
5618
- }
5964
+ );
5619
5965
  }
5620
- async function resolveTsAlias(specifier, config, serviceDir) {
5621
- for (const [pattern, targets] of Object.entries(config.paths)) {
5622
- let suffix = null;
5623
- if (pattern === specifier) {
5624
- suffix = "";
5625
- } else if (pattern.endsWith("/*")) {
5626
- const prefix = pattern.slice(0, -1);
5627
- if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
5628
- }
5629
- if (suffix === null) continue;
5630
- for (const target of targets) {
5631
- const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
5632
- const resolvedBase = import_node_path15.default.resolve(config.baseDir, targetBase, suffix);
5633
- const hit = await firstExistingCandidate(resolvedBase, serviceDir);
5634
- if (hit) return hit;
5635
- if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
5636
- return toPosix(import_node_path15.default.relative(serviceDir, resolvedBase));
5637
- }
5638
- }
5639
- }
5640
- return null;
5966
+ function methodName(node) {
5967
+ const name = node.childForFieldName("name");
5968
+ return name ? name.text : null;
5641
5969
  }
5642
- async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
5643
- if (!specifier) return null;
5644
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
5645
- const base = import_node_path15.default.resolve(importerDir, specifier);
5646
- const ext = import_node_path15.default.extname(specifier);
5647
- if (ext) {
5648
- if (ext === ".js" || ext === ".jsx") {
5649
- const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
5650
- const tsSibling = base.slice(0, -ext.length) + tsExt;
5651
- if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
5652
- return toPosix(import_node_path15.default.relative(serviceDir, tsSibling));
5970
+ function collectSymbolDefs(root) {
5971
+ const out = [];
5972
+ const push = (kind, qualname, node) => {
5973
+ out.push({
5974
+ kind,
5975
+ qualname,
5976
+ startLine: node.startPosition.row + 1,
5977
+ endLine: node.endPosition.row + 1
5978
+ });
5979
+ };
5980
+ const visit = (node, classCtx) => {
5981
+ switch (node.type) {
5982
+ case "function_declaration":
5983
+ case "generator_function_declaration": {
5984
+ const name = node.childForFieldName("name")?.text;
5985
+ if (name) push("function", name, node);
5986
+ break;
5987
+ }
5988
+ case "class_declaration":
5989
+ case "abstract_class_declaration":
5990
+ case "class": {
5991
+ const name = node.childForFieldName("name")?.text;
5992
+ if (name) push("class", name, node);
5993
+ const body = node.childForFieldName("body");
5994
+ if (body) {
5995
+ for (let i = 0; i < body.namedChildCount; i++) {
5996
+ const child = body.namedChild(i);
5997
+ if (child) visit(child, name ?? classCtx);
5998
+ }
5653
5999
  }
6000
+ return;
5654
6001
  }
5655
- if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
5656
- return toPosix(import_node_path15.default.relative(serviceDir, base));
6002
+ case "method_definition": {
6003
+ const name = methodName(node);
6004
+ if (name) {
6005
+ const kind = name === "constructor" ? "constructor" : "method";
6006
+ push(kind, classCtx ? `${classCtx}.${name}` : name, node);
6007
+ }
6008
+ break;
5657
6009
  }
5658
- return null;
5659
- }
5660
- return firstExistingCandidate(base, serviceDir);
5661
- }
5662
- if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
5663
- return null;
5664
- }
5665
- async function resolvePyImport(imp, importerPath, serviceDir) {
5666
- let baseDir;
5667
- if (imp.level > 0) {
5668
- baseDir = import_node_path15.default.dirname(importerPath);
5669
- for (let i = 1; i < imp.level; i++) baseDir = import_node_path15.default.dirname(baseDir);
5670
- } else {
5671
- baseDir = serviceDir;
5672
- }
5673
- const moduleBase = imp.modulePath ? import_node_path15.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
5674
- const resolved = /* @__PURE__ */ new Set();
5675
- let needModuleFile = imp.names.length === 0;
5676
- for (const name of imp.names) {
5677
- const submoduleFile = import_node_path15.default.join(moduleBase, `${name}.py`);
5678
- const subpackageInit = import_node_path15.default.join(moduleBase, name, "__init__.py");
5679
- if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
5680
- resolved.add(toPosix(import_node_path15.default.relative(serviceDir, submoduleFile)));
5681
- } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
5682
- resolved.add(toPosix(import_node_path15.default.relative(serviceDir, subpackageInit)));
5683
- } else {
5684
- needModuleFile = true;
5685
- }
5686
- }
5687
- if (needModuleFile) {
5688
- const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path15.default.join(moduleBase, "__init__.py")] : [import_node_path15.default.join(moduleBase, "__init__.py")];
5689
- for (const candidate of moduleFileCandidates) {
5690
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5691
- resolved.add(toPosix(import_node_path15.default.relative(serviceDir, candidate)));
6010
+ case "variable_declarator": {
6011
+ const value = node.childForFieldName("value");
6012
+ if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
6013
+ const nameNode = node.childForFieldName("name");
6014
+ if (nameNode && nameNode.type === "identifier") {
6015
+ push("function", nameNode.text, node);
6016
+ }
6017
+ }
5692
6018
  break;
5693
6019
  }
5694
6020
  }
5695
- }
5696
- return [...resolved];
6021
+ for (let i = 0; i < node.namedChildCount; i++) {
6022
+ const child = node.namedChild(i);
6023
+ if (child) visit(child, classCtx);
6024
+ }
6025
+ };
6026
+ visit(root, void 0);
6027
+ return out;
5697
6028
  }
5698
- async function resolveGoImport(specifier, modulePath, serviceDir) {
5699
- if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
5700
- const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
5701
- const dir = import_node_path15.default.join(serviceDir, suffix);
5702
- const entries = await import_node_fs12.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
5703
- const candidates = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".go") && !entry.name.endsWith("_test.go")).map((entry) => import_node_path15.default.join(dir, entry.name));
5704
- if (candidates.length !== 1) return null;
5705
- return toPosix(import_node_path15.default.relative(serviceDir, candidates[0]));
6029
+ function disambiguate(defs) {
6030
+ const counts = /* @__PURE__ */ new Map();
6031
+ for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
6032
+ const seen = /* @__PURE__ */ new Map();
6033
+ return defs.map((def) => {
6034
+ if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
6035
+ const ordinal = seen.get(def.qualname) ?? 0;
6036
+ seen.set(def.qualname, ordinal + 1);
6037
+ return { def, disambiguator: ordinal };
6038
+ });
5706
6039
  }
5707
- function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
5708
- const importeeFileId = (0, import_types12.fileId)(serviceName, importeeRelPath);
5709
- if (!graph.hasNode(importeeFileId)) return 0;
5710
- const edgeId = (0, import_types12.extractedEdgeId)(importerFileId, importeeFileId, import_types12.EdgeType.IMPORTS);
5711
- if (graph.hasEdge(edgeId)) return 0;
5712
- const edge = {
5713
- id: edgeId,
5714
- source: importerFileId,
5715
- target: importeeFileId,
5716
- type: import_types12.EdgeType.IMPORTS,
5717
- provenance: import_types12.Provenance.EXTRACTED,
5718
- confidence: (0, import_types12.confidenceForExtracted)("structural"),
5719
- evidence: { file: importerRelPath, line, snippet: snippet2 }
6040
+ async function addSymbols(graph, services) {
6041
+ const parsers = /* @__PURE__ */ new Map();
6042
+ const parserForExt2 = (ext) => {
6043
+ const grammar = GRAMMAR_BY_EXT[ext];
6044
+ if (!grammar) return null;
6045
+ let parser = parsers.get(ext);
6046
+ if (!parser) {
6047
+ parser = new import_tree_sitter3.default();
6048
+ parser.setLanguage(grammar);
6049
+ parsers.set(ext, parser);
6050
+ }
6051
+ return parser;
5720
6052
  };
5721
- graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
5722
- return 1;
5723
- }
5724
- async function addImports(graph, services) {
5725
- const jsParser = makeJsParser2();
5726
- const pyParser = makePyParser2();
5727
- const goParser = makeGoParser2();
6053
+ let nodesAdded = 0;
5728
6054
  let edgesAdded = 0;
5729
6055
  for (const service of services) {
5730
- const tsPaths = await loadTsPathConfig(service.dir);
5731
6056
  const files = await loadSourceFiles(service.dir);
5732
6057
  for (const file of files) {
5733
- if (isTestPath(file.path)) continue;
5734
- const relFile = toPosix(import_node_path15.default.relative(service.dir, file.path));
5735
- const importerFileId = (0, import_types12.fileId)(service.pkg.name, relFile);
5736
- const isPython = import_node_path15.default.extname(file.path) === ".py";
5737
- const isGo = import_node_path15.default.extname(file.path) === ".go";
5738
- if (isGo) {
5739
- let goImports = [];
5740
- try {
5741
- const tree = parseSource3(goParser, file.content);
5742
- collectGoImports(tree.rootNode, goImports);
5743
- } catch (err) {
5744
- recordExtractionError("import extraction", file.path, err);
5745
- continue;
5746
- }
5747
- const goMod = await import_node_fs12.promises.readFile(import_node_path15.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
5748
- const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
5749
- if (!modulePath) continue;
5750
- for (const imp of goImports) {
5751
- const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
5752
- if (!resolved) continue;
5753
- edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
5754
- }
5755
- continue;
5756
- }
5757
- if (isPython) {
5758
- let pyImports = [];
5759
- try {
5760
- const tree = parseSource3(pyParser, file.content);
5761
- collectPyImports(tree.rootNode, pyImports);
5762
- } catch (err) {
5763
- recordExtractionError("import extraction", file.path, err);
5764
- continue;
5765
- }
5766
- for (const imp of pyImports) {
5767
- const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
5768
- for (const resolved of resolvedPaths) {
5769
- edgesAdded += emitImportEdge(
5770
- graph,
5771
- service.pkg.name,
5772
- importerFileId,
5773
- relFile,
5774
- resolved,
5775
- imp.line,
5776
- imp.snippet
5777
- );
5778
- }
5779
- }
5780
- continue;
5781
- }
5782
- let jsImports = [];
6058
+ const parser = parserForExt2(import_node_path15.default.extname(file.path));
6059
+ if (!parser) continue;
6060
+ const relPath = toPosix(import_node_path15.default.relative(service.dir, file.path));
6061
+ let defs;
5783
6062
  try {
5784
- const tree = parseSource3(jsParser, file.content);
5785
- collectJsImports(tree.rootNode, jsImports);
6063
+ const tree = parseSource3(parser, file.content);
6064
+ defs = collectSymbolDefs(tree.rootNode);
5786
6065
  } catch (err) {
5787
- recordExtractionError("import extraction", file.path, err);
6066
+ recordExtractionError("symbol extraction", file.path, err);
5788
6067
  continue;
5789
6068
  }
5790
- for (const imp of jsImports) {
5791
- const resolved = await resolveJsImport(imp.specifier, import_node_path15.default.dirname(file.path), service.dir, tsPaths);
5792
- if (!resolved) continue;
5793
- edgesAdded += emitImportEdge(
5794
- graph,
5795
- service.pkg.name,
5796
- importerFileId,
5797
- relFile,
5798
- resolved,
5799
- imp.line,
5800
- imp.snippet
5801
- );
6069
+ if (defs.length === 0) continue;
6070
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6071
+ graph,
6072
+ service.pkg.name,
6073
+ service.node.id,
6074
+ relPath
6075
+ );
6076
+ nodesAdded += fn;
6077
+ edgesAdded += fe;
6078
+ for (const { def, disambiguator } of disambiguate(defs)) {
6079
+ const sid = (0, import_types12.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
6080
+ if (!graph.hasNode(sid)) {
6081
+ const node = {
6082
+ id: sid,
6083
+ type: import_types12.NodeType.SymbolNode,
6084
+ kind: def.kind,
6085
+ qualname: def.qualname,
6086
+ span: { startLine: def.startLine, endLine: def.endLine },
6087
+ service: service.pkg.name,
6088
+ relPath,
6089
+ discoveredVia: "static"
6090
+ };
6091
+ graph.addNode(sid, node);
6092
+ nodesAdded++;
6093
+ }
6094
+ const containsId = (0, import_types12.extractedEdgeId)(fileNodeId, sid, import_types12.EdgeType.CONTAINS);
6095
+ if (!graph.hasEdge(containsId)) {
6096
+ const edge = {
6097
+ id: containsId,
6098
+ source: fileNodeId,
6099
+ target: sid,
6100
+ type: import_types12.EdgeType.CONTAINS,
6101
+ provenance: import_types12.Provenance.EXTRACTED,
6102
+ confidence: (0, import_types12.confidenceForExtracted)("structural"),
6103
+ evidence: {
6104
+ file: relPath,
6105
+ line: def.startLine,
6106
+ snippet: snippet(file.content, def.startLine)
6107
+ }
6108
+ };
6109
+ graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
6110
+ edgesAdded++;
6111
+ }
5802
6112
  }
5803
6113
  }
5804
6114
  }
5805
- return { nodesAdded: 0, edgesAdded };
6115
+ return { nodesAdded, edgesAdded };
5806
6116
  }
5807
6117
 
5808
6118
  // src/extract/symbol-edges.ts
6119
+ init_cjs_shims();
6120
+ var import_node_path16 = __toESM(require("path"), 1);
6121
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
6122
+ var import_types13 = require("@neat.is/types");
5809
6123
  function extendsInfo(classHeritage) {
5810
6124
  for (let i = 0; i < classHeritage.namedChildCount; i++) {
5811
6125
  const child = classHeritage.namedChild(i);
@@ -5916,7 +6230,7 @@ async function addSymbolEdges(graph, services) {
5916
6230
  const fileDir = import_node_path16.default.dirname(file.path);
5917
6231
  let root;
5918
6232
  try {
5919
- root = parseSource2(parser, file.content).rootNode;
6233
+ root = parseSource3(parser, file.content).rootNode;
5920
6234
  } catch (err) {
5921
6235
  recordExtractionError("symbol edge extraction", file.path, err);
5922
6236
  continue;
@@ -8368,7 +8682,7 @@ function columnsFromObject(obj) {
8368
8682
  }
8369
8683
  function drizzleEndpointsFromFile(file, serviceDir) {
8370
8684
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
8371
- const tree = parseSource2(parserForExt(import_node_path37.default.extname(file.path)), file.content);
8685
+ const tree = parseSource3(parserForExt(import_node_path37.default.extname(file.path)), file.content);
8372
8686
  const out = [];
8373
8687
  const seen = /* @__PURE__ */ new Set();
8374
8688
  const walk6 = (node) => {