@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/cli.cjs CHANGED
@@ -825,8 +825,8 @@ init_cjs_shims();
825
825
 
826
826
  // src/ingest.ts
827
827
  init_cjs_shims();
828
- var import_node_fs7 = require("fs");
829
- var import_node_path8 = __toESM(require("path"), 1);
828
+ var import_node_fs8 = require("fs");
829
+ var import_node_path9 = __toESM(require("path"), 1);
830
830
  var sourceMapJs = __toESM(require("source-map-js"), 1);
831
831
 
832
832
  // src/policy.ts
@@ -2175,16 +2175,16 @@ var PolicyViolationsLog = class {
2175
2175
  };
2176
2176
 
2177
2177
  // src/ingest.ts
2178
- var import_types7 = require("@neat.is/types");
2178
+ var import_types8 = require("@neat.is/types");
2179
2179
 
2180
2180
  // src/extract/routes.ts
2181
2181
  init_cjs_shims();
2182
- var import_node_path7 = __toESM(require("path"), 1);
2183
- var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2184
- var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2185
- var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2186
- var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1);
2187
- var import_types5 = require("@neat.is/types");
2182
+ var import_node_path8 = __toESM(require("path"), 1);
2183
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
2184
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
2185
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
2186
+ var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
2187
+ var import_types6 = require("@neat.is/types");
2188
2188
 
2189
2189
  // src/extract/shared.ts
2190
2190
  init_cjs_shims();
@@ -2567,7 +2567,15 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
2567
2567
  return { fileNodeId, nodesAdded, edgesAdded };
2568
2568
  }
2569
2569
 
2570
- // src/extract/routes.ts
2570
+ // src/extract/imports.ts
2571
+ init_cjs_shims();
2572
+ var import_node_path7 = __toESM(require("path"), 1);
2573
+ var import_node_fs7 = require("fs");
2574
+ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2575
+ var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2576
+ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2577
+ var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1);
2578
+ var import_types5 = require("@neat.is/types");
2571
2579
  var PARSE_CHUNK = 16384;
2572
2580
  function parseSource(parser, source) {
2573
2581
  return parser.parse(
@@ -2589,533 +2597,910 @@ function makeGoParser() {
2589
2597
  p.setLanguage(import_tree_sitter_go.default);
2590
2598
  return p;
2591
2599
  }
2592
- var ROUTER_METHODS = /* @__PURE__ */ new Set([
2593
- "get",
2594
- "post",
2595
- "put",
2596
- "patch",
2597
- "delete",
2598
- "options",
2599
- "head",
2600
- "all"
2601
- ]);
2602
- var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2603
- var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
2604
- function ginRoutesFromSource(source, parser) {
2605
- const tree = parseSource(parser, source);
2606
- const prefixes = /* @__PURE__ */ new Map();
2607
- const out = [];
2608
- walk(tree.rootNode, (node) => {
2609
- if (node.type === "short_var_declaration" || node.type === "var_spec") {
2610
- const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
2611
- const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
2612
- if (name && value?.type === "call_expression") {
2613
- const fn2 = value.childForFieldName("function");
2614
- const field = fn2?.childForFieldName("field")?.text;
2615
- const first2 = value.childForFieldName("arguments")?.namedChild(0);
2616
- if (field === "Group" && first2?.type === "interpreted_string_literal") {
2617
- prefixes.set(name, first2.text.slice(1, -1));
2618
- }
2619
- }
2620
- return;
2621
- }
2622
- if (node.type !== "call_expression") return;
2623
- const fn = node.childForFieldName("function");
2624
- if (fn?.type !== "selector_expression") return;
2625
- const method = fn.childForFieldName("field")?.text?.toUpperCase();
2626
- if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
2627
- const receiver = fn.childForFieldName("operand")?.text ?? "";
2628
- const first = node.childForFieldName("arguments")?.namedChild(0);
2629
- if (first?.type !== "interpreted_string_literal") return;
2630
- const leaf = first.text.slice(1, -1);
2631
- out.push({
2632
- method: method === "ALL" ? "ALL" : method,
2633
- pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
2634
- line: node.startPosition.row + 1,
2635
- framework: "gin"
2636
- });
2637
- });
2638
- return out;
2639
- }
2640
- var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
2641
- var NESTJS_METHODS = /* @__PURE__ */ new Map([
2642
- ["Get", "GET"],
2643
- ["Post", "POST"],
2644
- ["Put", "PUT"],
2645
- ["Patch", "PATCH"],
2646
- ["Delete", "DELETE"],
2647
- ["Options", "OPTIONS"],
2648
- ["Head", "HEAD"],
2649
- ["All", "ALL"]
2650
- ]);
2651
- function canonicalizeTemplate(raw) {
2652
- let p = raw.split("?")[0].split("#")[0];
2653
- if (!p.startsWith("/")) p = "/" + p;
2654
- if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
2655
- return p;
2656
- }
2657
- function isDynamicSegment(seg) {
2658
- if (seg.length === 0) return false;
2659
- if (seg.includes(":")) return true;
2660
- if (seg.startsWith("{") || seg.startsWith("[")) return true;
2661
- if (/^\d+$/.test(seg)) return true;
2662
- 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;
2663
- if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
2664
- return false;
2600
+ function stringLiteralText(node) {
2601
+ for (let i = 0; i < node.childCount; i++) {
2602
+ const child = node.child(i);
2603
+ if (child?.type === "string_fragment") return child.text;
2604
+ }
2605
+ const raw = node.text;
2606
+ if (raw.length >= 2) return raw.slice(1, -1);
2607
+ return raw.length === 0 ? null : "";
2665
2608
  }
2666
- function normalizePathTemplate(raw) {
2667
- const canonical = canonicalizeTemplate(raw);
2668
- const segments = canonical.split("/").filter((s) => s.length > 0);
2669
- const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
2670
- return "/" + normalised.join("/");
2609
+ function clipSnippet(text) {
2610
+ const oneLine = text.split("\n")[0] ?? text;
2611
+ return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
2671
2612
  }
2672
- function walk(node, visit) {
2673
- visit(node);
2613
+ function collectGoImports(node, out) {
2614
+ if (node.type === "import_spec") {
2615
+ const pathNode = node.childForFieldName("path");
2616
+ if (pathNode) {
2617
+ const specifier = pathNode.text.replace(/^`|`$/g, "").replace(/^"|"$/g, "");
2618
+ if (specifier) out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2619
+ }
2620
+ return;
2621
+ }
2674
2622
  for (let i = 0; i < node.namedChildCount; i++) {
2675
2623
  const child = node.namedChild(i);
2676
- if (child) walk(child, visit);
2624
+ if (child) collectGoImports(child, out);
2677
2625
  }
2678
2626
  }
2679
- function staticStringText(node) {
2680
- if (node.type === "string") {
2681
- for (let i = 0; i < node.namedChildCount; i++) {
2682
- const child = node.namedChild(i);
2683
- if (child?.type === "string_fragment") return child.text;
2627
+ function collectJsImports(node, out) {
2628
+ if (node.type === "import_statement") {
2629
+ const source = node.childForFieldName("source");
2630
+ if (source) {
2631
+ const specifier = stringLiteralText(source);
2632
+ if (specifier) {
2633
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2634
+ }
2684
2635
  }
2685
- return "";
2636
+ return;
2686
2637
  }
2687
- if (node.type === "template_string") {
2688
- for (let i = 0; i < node.namedChildCount; i++) {
2689
- if (node.namedChild(i)?.type === "template_substitution") return null;
2638
+ if (node.type === "call_expression") {
2639
+ const fn = node.childForFieldName("function");
2640
+ if (fn?.type === "identifier" && fn.text === "require") {
2641
+ const args = node.childForFieldName("arguments");
2642
+ const firstArg = args?.namedChild(0);
2643
+ if (firstArg?.type === "string") {
2644
+ const specifier = stringLiteralText(firstArg);
2645
+ if (specifier) {
2646
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2647
+ }
2648
+ }
2690
2649
  }
2691
- const raw = node.text;
2692
- return raw.length >= 2 ? raw.slice(1, -1) : "";
2693
2650
  }
2694
- return null;
2651
+ for (let i = 0; i < node.namedChildCount; i++) {
2652
+ const child = node.namedChild(i);
2653
+ if (child) collectJsImports(child, out);
2654
+ }
2695
2655
  }
2696
- function objectStringProp(objNode, key) {
2697
- for (let i = 0; i < objNode.namedChildCount; i++) {
2698
- const pair = objNode.namedChild(i);
2699
- if (!pair || pair.type !== "pair") continue;
2700
- const k = pair.childForFieldName("key");
2701
- if (!k) continue;
2702
- const kText = k.type === "string" ? staticStringText(k) : k.text;
2703
- if (kText !== key) continue;
2704
- const v = pair.childForFieldName("value");
2705
- if (v) return staticStringText(v);
2656
+ function collectImportedNames(node, out) {
2657
+ if (node.type === "aliased_import") {
2658
+ const nameNode = node.childForFieldName("name");
2659
+ if (nameNode) out.push(nameNode.text);
2660
+ return;
2661
+ }
2662
+ if (node.type === "dotted_name") {
2663
+ out.push(node.text);
2664
+ return;
2665
+ }
2666
+ for (let i = 0; i < node.namedChildCount; i++) {
2667
+ const child = node.namedChild(i);
2668
+ if (child) collectImportedNames(child, out);
2706
2669
  }
2707
- return null;
2708
2670
  }
2709
- function fastifyRouteMethods(objNode) {
2710
- for (let i = 0; i < objNode.namedChildCount; i++) {
2711
- const pair = objNode.namedChild(i);
2712
- if (!pair || pair.type !== "pair") continue;
2713
- const k = pair.childForFieldName("key");
2714
- const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
2715
- if (kText !== "method") continue;
2716
- const v = pair.childForFieldName("value");
2717
- if (!v) return [];
2718
- if (v.type === "string" || v.type === "template_string") {
2719
- const s = staticStringText(v);
2720
- return s ? [s.toUpperCase()] : [];
2721
- }
2722
- if (v.type === "array") {
2723
- const out = [];
2724
- for (let j = 0; j < v.namedChildCount; j++) {
2725
- const el = v.namedChild(j);
2726
- if (el && (el.type === "string" || el.type === "template_string")) {
2727
- const s = staticStringText(el);
2728
- if (s) out.push(s.toUpperCase());
2671
+ function collectPyImports(node, out) {
2672
+ if (node.type === "import_from_statement") {
2673
+ let level = 0;
2674
+ let modulePath = "";
2675
+ const names = [];
2676
+ let pastFrom = false;
2677
+ let pastImport = false;
2678
+ for (let i = 0; i < node.childCount; i++) {
2679
+ const child = node.child(i);
2680
+ if (!child) continue;
2681
+ if (!pastFrom) {
2682
+ if (child.type === "from") pastFrom = true;
2683
+ continue;
2684
+ }
2685
+ if (!pastImport) {
2686
+ if (child.type === "import") {
2687
+ pastImport = true;
2688
+ continue;
2689
+ }
2690
+ if (child.type === "relative_import") {
2691
+ for (let j = 0; j < child.childCount; j++) {
2692
+ const rc = child.child(j);
2693
+ if (!rc) continue;
2694
+ if (rc.type === "import_prefix") {
2695
+ for (let k = 0; k < rc.childCount; k++) {
2696
+ if (rc.child(k)?.type === ".") level++;
2697
+ }
2698
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
2699
+ }
2700
+ } else if (child.type === "dotted_name") {
2701
+ modulePath = child.text;
2729
2702
  }
2703
+ continue;
2730
2704
  }
2731
- return out;
2705
+ collectImportedNames(child, names);
2706
+ }
2707
+ if (level > 0 || modulePath) {
2708
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2732
2709
  }
2733
2710
  }
2734
- return [];
2711
+ for (let i = 0; i < node.namedChildCount; i++) {
2712
+ const child = node.namedChild(i);
2713
+ if (child) collectPyImports(child, out);
2714
+ }
2735
2715
  }
2736
- function nestDecoratorImports(root) {
2737
- const imports = /* @__PURE__ */ new Map();
2738
- walk(root, (node) => {
2739
- if (node.type !== "import_statement") return;
2740
- const source = node.childForFieldName("source");
2741
- if (!source || staticStringText(source) !== "@nestjs/common") return;
2742
- walk(node, (child) => {
2743
- if (child.type !== "import_specifier") return;
2744
- const imported = child.childForFieldName("name")?.text;
2745
- const local = child.childForFieldName("alias")?.text ?? imported;
2746
- if (!imported || !local) return;
2747
- if (imported === "Controller" || NESTJS_METHODS.has(imported)) {
2748
- imports.set(local, imported);
2749
- }
2750
- });
2751
- });
2752
- return imports;
2753
- }
2754
- function decoratorCall(node) {
2755
- if (node.type !== "decorator") return null;
2756
- const expression = node.namedChild(0);
2757
- return expression?.type === "call_expression" ? expression : null;
2716
+ async function fileExists(p) {
2717
+ try {
2718
+ await import_node_fs7.promises.access(p);
2719
+ return true;
2720
+ } catch {
2721
+ return false;
2722
+ }
2758
2723
  }
2759
- function decoratorCanonicalName(decorator, imports) {
2760
- const call = decoratorCall(decorator);
2761
- const fn = call?.childForFieldName("function");
2762
- if (!fn || fn.type !== "identifier") return null;
2763
- return imports.get(fn.text) ?? null;
2724
+ function isWithinServiceDir(candidate, serviceDir) {
2725
+ const rel = import_node_path7.default.relative(serviceDir, candidate);
2726
+ return rel !== "" && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
2764
2727
  }
2765
- function nestStaticPaths(call) {
2766
- const args = call.childForFieldName("arguments");
2767
- const first = args?.namedChild(0);
2768
- if (!first) return [""];
2769
- const single = staticStringText(first);
2770
- if (single !== null) return [single];
2771
- if (first.type !== "array") return [];
2772
- const paths = [];
2773
- for (let i = 0; i < first.namedChildCount; i++) {
2774
- const item = first.namedChild(i);
2775
- if (!item) continue;
2776
- const value = staticStringText(item);
2777
- if (value !== null) paths.push(value);
2728
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2729
+ var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
2730
+ async function firstExistingCandidate(base, serviceDir) {
2731
+ for (const ext of JS_EXTENSIONS) {
2732
+ const candidate = base + ext;
2733
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2734
+ return toPosix(import_node_path7.default.relative(serviceDir, candidate));
2735
+ }
2778
2736
  }
2779
- return paths;
2737
+ for (const indexFile of JS_INDEX_FILES) {
2738
+ const candidate = import_node_path7.default.join(base, indexFile);
2739
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2740
+ return toPosix(import_node_path7.default.relative(serviceDir, candidate));
2741
+ }
2742
+ }
2743
+ return null;
2780
2744
  }
2781
- function nestJoinedPath(prefix, leaf) {
2782
- const segments = [prefix, leaf].map((part) => part.replace(/^\/+|\/+$/g, "")).filter((part) => part.length > 0);
2783
- return canonicalizeTemplate(segments.join("/"));
2745
+ async function loadTsPathConfig(serviceDir) {
2746
+ const tsconfigPath = import_node_path7.default.join(serviceDir, "tsconfig.json");
2747
+ let raw;
2748
+ try {
2749
+ raw = await import_node_fs7.promises.readFile(tsconfigPath, "utf8");
2750
+ } catch {
2751
+ return null;
2752
+ }
2753
+ try {
2754
+ const parsed = JSON.parse(raw);
2755
+ const paths = parsed.compilerOptions?.paths;
2756
+ if (!paths || Object.keys(paths).length === 0) return null;
2757
+ const baseUrl = parsed.compilerOptions?.baseUrl;
2758
+ return { paths, baseDir: baseUrl ? import_node_path7.default.resolve(serviceDir, baseUrl) : serviceDir };
2759
+ } catch (err) {
2760
+ recordExtractionError("import alias resolution", tsconfigPath, err);
2761
+ return null;
2762
+ }
2784
2763
  }
2785
- function nestjsRoutesFromSource(source, parser) {
2786
- const tree = parseSource(parser, source);
2787
- const imports = nestDecoratorImports(tree.rootNode);
2788
- if (![...imports.values()].includes("Controller")) return [];
2789
- const out = [];
2790
- walk(tree.rootNode, (node) => {
2791
- if (node.type !== "class_declaration") return;
2792
- const decoratorOwner = node.parent?.type === "export_statement" ? node.parent : node;
2793
- const classDecorators = [];
2794
- for (let i = 0; i < decoratorOwner.namedChildCount; i++) {
2795
- const child = decoratorOwner.namedChild(i);
2796
- if (child?.type === "decorator") classDecorators.push(child);
2764
+ async function resolveTsAlias(specifier, config, serviceDir) {
2765
+ for (const [pattern, targets] of Object.entries(config.paths)) {
2766
+ let suffix = null;
2767
+ if (pattern === specifier) {
2768
+ suffix = "";
2769
+ } else if (pattern.endsWith("/*")) {
2770
+ const prefix = pattern.slice(0, -1);
2771
+ if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
2797
2772
  }
2798
- const controller = classDecorators.find(
2799
- (decorator) => decoratorCanonicalName(decorator, imports) === "Controller"
2800
- );
2801
- const controllerCall = controller ? decoratorCall(controller) : null;
2802
- if (!controllerCall) return;
2803
- const prefixes = nestStaticPaths(controllerCall);
2804
- if (prefixes.length === 0) return;
2805
- const body = node.childForFieldName("body");
2806
- if (!body) return;
2807
- for (let i = 0; i < body.namedChildCount; i++) {
2808
- const methodNode = body.namedChild(i);
2809
- if (methodNode?.type !== "method_definition") continue;
2810
- for (let j = 0; j < methodNode.namedChildCount; j++) {
2811
- const decorator = methodNode.namedChild(j);
2812
- if (decorator?.type !== "decorator") continue;
2813
- const canonical = decoratorCanonicalName(decorator, imports);
2814
- const method = canonical ? NESTJS_METHODS.get(canonical) : void 0;
2815
- const call = decoratorCall(decorator);
2816
- if (!method || !call) continue;
2817
- const leaves = nestStaticPaths(call);
2818
- for (const prefix of prefixes) {
2819
- for (const leaf of leaves) {
2820
- out.push({
2821
- method,
2822
- pathTemplate: nestJoinedPath(prefix, leaf),
2823
- line: decorator.startPosition.row + 1,
2824
- framework: "nestjs"
2825
- });
2826
- }
2827
- }
2773
+ if (suffix === null) continue;
2774
+ for (const target of targets) {
2775
+ const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
2776
+ const resolvedBase = import_node_path7.default.resolve(config.baseDir, targetBase, suffix);
2777
+ const hit = await firstExistingCandidate(resolvedBase, serviceDir);
2778
+ if (hit) return hit;
2779
+ if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
2780
+ return toPosix(import_node_path7.default.relative(serviceDir, resolvedBase));
2828
2781
  }
2829
2782
  }
2830
- });
2831
- return out;
2783
+ }
2784
+ return null;
2832
2785
  }
2833
- function serverRoutesFromSource(source, parser, hasExpress, hasFastify, hasHono = false) {
2834
- const tree = parseSource(parser, source);
2835
- const out = [];
2836
- const framework = hasExpress ? "express" : hasFastify ? "fastify" : hasHono ? "hono" : "unknown";
2837
- walk(tree.rootNode, (node) => {
2838
- if (node.type !== "call_expression") return;
2839
- const fn = node.childForFieldName("function");
2840
- if (!fn || fn.type !== "member_expression") return;
2841
- const prop = fn.childForFieldName("property");
2842
- if (!prop) return;
2843
- const method = prop.text.toLowerCase();
2844
- const args = node.childForFieldName("arguments");
2845
- const first = args?.namedChild(0);
2846
- if (!first) return;
2847
- const line = node.startPosition.row + 1;
2848
- if (ROUTER_METHODS.has(method)) {
2849
- const p = staticStringText(first);
2850
- if (p && p.startsWith("/")) {
2851
- out.push({
2852
- method: method === "all" ? "ALL" : method.toUpperCase(),
2853
- pathTemplate: canonicalizeTemplate(p),
2854
- line,
2855
- framework
2856
- });
2786
+ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
2787
+ if (!specifier) return null;
2788
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
2789
+ const base = import_node_path7.default.resolve(importerDir, specifier);
2790
+ const ext = import_node_path7.default.extname(specifier);
2791
+ if (ext) {
2792
+ if (ext === ".js" || ext === ".jsx") {
2793
+ const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
2794
+ const tsSibling = base.slice(0, -ext.length) + tsExt;
2795
+ if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
2796
+ return toPosix(import_node_path7.default.relative(serviceDir, tsSibling));
2797
+ }
2857
2798
  }
2858
- return;
2859
- }
2860
- if (method === "route" && hasFastify && first.type === "object") {
2861
- const url = objectStringProp(first, "url");
2862
- if (!url || !url.startsWith("/")) return;
2863
- const methods = fastifyRouteMethods(first);
2864
- const list = methods.length > 0 ? methods : ["ALL"];
2865
- for (const m of list) {
2866
- out.push({
2867
- method: m === "ALL" ? "ALL" : m.toUpperCase(),
2868
- pathTemplate: canonicalizeTemplate(url),
2869
- line,
2870
- framework: "fastify"
2871
- });
2799
+ if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
2800
+ return toPosix(import_node_path7.default.relative(serviceDir, base));
2801
+ }
2802
+ if (!JS_EXTENSIONS.includes(ext)) {
2803
+ return firstExistingCandidate(base, serviceDir);
2872
2804
  }
2805
+ return null;
2873
2806
  }
2874
- });
2875
- return out;
2876
- }
2877
- function segmentsOf(relFile) {
2878
- return toPosix(relFile).split("/").filter((s) => s.length > 0);
2879
- }
2880
- function isNextAppRouteFile(relFile) {
2881
- const segs = segmentsOf(relFile);
2882
- if (!segs.includes("app")) return false;
2883
- const base = segs[segs.length - 1] ?? "";
2884
- return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
2885
- }
2886
- function isNextPagesApiFile(relFile) {
2887
- const segs = segmentsOf(relFile);
2888
- const pagesIdx = segs.indexOf("pages");
2889
- if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
2890
- const base = segs[segs.length - 1] ?? "";
2891
- if (/^_(app|document|middleware)\./.test(base)) return false;
2892
- return JS_ROUTE_EXTENSIONS.has(import_node_path7.default.extname(base));
2893
- }
2894
- function nextSegment(seg) {
2895
- if (seg.startsWith("(") && seg.endsWith(")")) return null;
2896
- const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
2897
- if (catchAll) return ":" + catchAll[1];
2898
- const dynamic = seg.match(/^\[(.+?)\]$/);
2899
- if (dynamic) return ":" + dynamic[1];
2900
- return seg;
2901
- }
2902
- function nextAppPathTemplate(relFile) {
2903
- const segs = segmentsOf(relFile);
2904
- const appIdx = segs.lastIndexOf("app");
2905
- const between = segs.slice(appIdx + 1, segs.length - 1);
2906
- const parts = [];
2907
- for (const seg of between) {
2908
- const mapped = nextSegment(seg);
2909
- if (mapped !== null) parts.push(mapped);
2807
+ return firstExistingCandidate(base, serviceDir);
2910
2808
  }
2911
- return "/" + parts.join("/");
2809
+ if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
2810
+ return null;
2912
2811
  }
2913
- function nextPagesApiPathTemplate(relFile) {
2914
- const segs = segmentsOf(relFile);
2915
- const pagesIdx = segs.indexOf("pages");
2916
- const rest = segs.slice(pagesIdx + 1);
2917
- const parts = [];
2918
- for (let i = 0; i < rest.length; i++) {
2919
- let seg = rest[i];
2920
- if (i === rest.length - 1) {
2921
- seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
2922
- if (seg === "index") continue;
2812
+ async function resolvePyImport(imp, importerPath, serviceDir) {
2813
+ let baseDir;
2814
+ if (imp.level > 0) {
2815
+ baseDir = import_node_path7.default.dirname(importerPath);
2816
+ for (let i = 1; i < imp.level; i++) baseDir = import_node_path7.default.dirname(baseDir);
2817
+ } else {
2818
+ baseDir = serviceDir;
2819
+ }
2820
+ const moduleBase = imp.modulePath ? import_node_path7.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
2821
+ const resolved = /* @__PURE__ */ new Set();
2822
+ let needModuleFile = imp.names.length === 0;
2823
+ for (const name of imp.names) {
2824
+ const submoduleFile = import_node_path7.default.join(moduleBase, `${name}.py`);
2825
+ const subpackageInit = import_node_path7.default.join(moduleBase, name, "__init__.py");
2826
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
2827
+ resolved.add(toPosix(import_node_path7.default.relative(serviceDir, submoduleFile)));
2828
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
2829
+ resolved.add(toPosix(import_node_path7.default.relative(serviceDir, subpackageInit)));
2830
+ } else {
2831
+ needModuleFile = true;
2923
2832
  }
2924
- const mapped = nextSegment(seg);
2925
- if (mapped !== null) parts.push(mapped);
2926
2833
  }
2927
- return "/" + parts.join("/");
2834
+ if (needModuleFile) {
2835
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path7.default.join(moduleBase, "__init__.py")] : [import_node_path7.default.join(moduleBase, "__init__.py")];
2836
+ for (const candidate of moduleFileCandidates) {
2837
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2838
+ resolved.add(toPosix(import_node_path7.default.relative(serviceDir, candidate)));
2839
+ break;
2840
+ }
2841
+ }
2842
+ }
2843
+ return [...resolved];
2928
2844
  }
2929
- function nextAppMethods(root) {
2930
- const out = [];
2931
- walk(root, (node) => {
2932
- if (node.type !== "export_statement") return;
2933
- const decl = node.childForFieldName("declaration");
2934
- if (!decl) return;
2935
- const line = node.startPosition.row + 1;
2936
- if (decl.type === "function_declaration") {
2937
- const name = decl.childForFieldName("name")?.text;
2938
- if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
2939
- return;
2845
+ async function resolveGoImport(specifier, modulePath, serviceDir) {
2846
+ if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
2847
+ const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
2848
+ const dir = import_node_path7.default.join(serviceDir, suffix);
2849
+ const entries = await import_node_fs7.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
2850
+ const candidates = entries.filter((entry2) => entry2.isFile() && entry2.name.endsWith(".go") && !entry2.name.endsWith("_test.go")).map((entry2) => import_node_path7.default.join(dir, entry2.name));
2851
+ if (candidates.length !== 1) return null;
2852
+ return toPosix(import_node_path7.default.relative(serviceDir, candidates[0]));
2853
+ }
2854
+ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
2855
+ const importeeFileId = (0, import_types5.fileId)(serviceName, importeeRelPath);
2856
+ if (!graph.hasNode(importeeFileId)) return 0;
2857
+ const edgeId = (0, import_types5.extractedEdgeId)(importerFileId, importeeFileId, import_types5.EdgeType.IMPORTS);
2858
+ if (graph.hasEdge(edgeId)) return 0;
2859
+ const edge = {
2860
+ id: edgeId,
2861
+ source: importerFileId,
2862
+ target: importeeFileId,
2863
+ type: import_types5.EdgeType.IMPORTS,
2864
+ provenance: import_types5.Provenance.EXTRACTED,
2865
+ confidence: (0, import_types5.confidenceForExtracted)("structural"),
2866
+ evidence: { file: importerRelPath, line, snippet: snippet2 }
2867
+ };
2868
+ graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
2869
+ return 1;
2870
+ }
2871
+ async function addImports(graph, services) {
2872
+ const jsParser = makeJsParser();
2873
+ const pyParser = makePyParser();
2874
+ const goParser = makeGoParser();
2875
+ let edgesAdded = 0;
2876
+ for (const service of services) {
2877
+ const tsPaths = await loadTsPathConfig(service.dir);
2878
+ const files = await loadSourceFiles(service.dir);
2879
+ for (const file of files) {
2880
+ if (isTestPath(file.path)) continue;
2881
+ const relFile = toPosix(import_node_path7.default.relative(service.dir, file.path));
2882
+ const importerFileId = (0, import_types5.fileId)(service.pkg.name, relFile);
2883
+ const isPython = import_node_path7.default.extname(file.path) === ".py";
2884
+ const isGo = import_node_path7.default.extname(file.path) === ".go";
2885
+ if (isGo) {
2886
+ let goImports = [];
2887
+ try {
2888
+ const tree = parseSource(goParser, file.content);
2889
+ collectGoImports(tree.rootNode, goImports);
2890
+ } catch (err) {
2891
+ recordExtractionError("import extraction", file.path, err);
2892
+ continue;
2893
+ }
2894
+ const goMod = await import_node_fs7.promises.readFile(import_node_path7.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
2895
+ const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
2896
+ if (!modulePath) continue;
2897
+ for (const imp of goImports) {
2898
+ const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
2899
+ if (!resolved) continue;
2900
+ edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
2901
+ }
2902
+ continue;
2903
+ }
2904
+ if (isPython) {
2905
+ let pyImports = [];
2906
+ try {
2907
+ const tree = parseSource(pyParser, file.content);
2908
+ collectPyImports(tree.rootNode, pyImports);
2909
+ } catch (err) {
2910
+ recordExtractionError("import extraction", file.path, err);
2911
+ continue;
2912
+ }
2913
+ for (const imp of pyImports) {
2914
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
2915
+ for (const resolved of resolvedPaths) {
2916
+ edgesAdded += emitImportEdge(
2917
+ graph,
2918
+ service.pkg.name,
2919
+ importerFileId,
2920
+ relFile,
2921
+ resolved,
2922
+ imp.line,
2923
+ imp.snippet
2924
+ );
2925
+ }
2926
+ }
2927
+ continue;
2928
+ }
2929
+ let jsImports = [];
2930
+ try {
2931
+ const tree = parseSource(jsParser, file.content);
2932
+ collectJsImports(tree.rootNode, jsImports);
2933
+ } catch (err) {
2934
+ recordExtractionError("import extraction", file.path, err);
2935
+ continue;
2936
+ }
2937
+ for (const imp of jsImports) {
2938
+ const resolved = await resolveJsImport(imp.specifier, import_node_path7.default.dirname(file.path), service.dir, tsPaths);
2939
+ if (!resolved) continue;
2940
+ edgesAdded += emitImportEdge(
2941
+ graph,
2942
+ service.pkg.name,
2943
+ importerFileId,
2944
+ relFile,
2945
+ resolved,
2946
+ imp.line,
2947
+ imp.snippet
2948
+ );
2949
+ }
2940
2950
  }
2941
- if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
2942
- for (let i = 0; i < decl.namedChildCount; i++) {
2943
- const d = decl.namedChild(i);
2944
- if (d?.type !== "variable_declarator") continue;
2945
- const name = d.childForFieldName("name")?.text;
2946
- if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
2951
+ }
2952
+ return { nodesAdded: 0, edgesAdded };
2953
+ }
2954
+
2955
+ // src/extract/routes.ts
2956
+ var PARSE_CHUNK2 = 16384;
2957
+ function parseSource2(parser, source) {
2958
+ return parser.parse(
2959
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
2960
+ );
2961
+ }
2962
+ function makeJsParser2() {
2963
+ const p = new import_tree_sitter2.default();
2964
+ p.setLanguage(import_tree_sitter_javascript2.default);
2965
+ return p;
2966
+ }
2967
+ function makePyParser2() {
2968
+ const p = new import_tree_sitter2.default();
2969
+ p.setLanguage(import_tree_sitter_python2.default);
2970
+ return p;
2971
+ }
2972
+ function makeGoParser2() {
2973
+ const p = new import_tree_sitter2.default();
2974
+ p.setLanguage(import_tree_sitter_go2.default);
2975
+ return p;
2976
+ }
2977
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
2978
+ "get",
2979
+ "post",
2980
+ "put",
2981
+ "patch",
2982
+ "delete",
2983
+ "options",
2984
+ "head",
2985
+ "all"
2986
+ ]);
2987
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2988
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
2989
+ function ginRoutesFromSource(source, parser) {
2990
+ const tree = parseSource2(parser, source);
2991
+ const prefixes = /* @__PURE__ */ new Map();
2992
+ const out = [];
2993
+ walk(tree.rootNode, (node) => {
2994
+ if (node.type === "short_var_declaration" || node.type === "var_spec") {
2995
+ const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
2996
+ const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
2997
+ if (name && value?.type === "call_expression") {
2998
+ const fn2 = value.childForFieldName("function");
2999
+ const field = fn2?.childForFieldName("field")?.text;
3000
+ const first2 = value.childForFieldName("arguments")?.namedChild(0);
3001
+ if (field === "Group" && first2?.type === "interpreted_string_literal") {
3002
+ prefixes.set(name, first2.text.slice(1, -1));
3003
+ }
2947
3004
  }
3005
+ return;
2948
3006
  }
3007
+ if (node.type !== "call_expression") return;
3008
+ const fn = node.childForFieldName("function");
3009
+ if (fn?.type !== "selector_expression") return;
3010
+ const method = fn.childForFieldName("field")?.text?.toUpperCase();
3011
+ if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
3012
+ const receiver = fn.childForFieldName("operand")?.text ?? "";
3013
+ const first = node.childForFieldName("arguments")?.namedChild(0);
3014
+ if (first?.type !== "interpreted_string_literal") return;
3015
+ const leaf = first.text.slice(1, -1);
3016
+ out.push({
3017
+ method: method === "ALL" ? "ALL" : method,
3018
+ pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
3019
+ line: node.startPosition.row + 1,
3020
+ framework: "gin"
3021
+ });
2949
3022
  });
2950
3023
  return out;
2951
3024
  }
2952
- function nextRoutesFromFile(source, relFile, parser) {
2953
- if (isNextAppRouteFile(relFile)) {
2954
- const tree = parseSource(parser, source);
2955
- const template = nextAppPathTemplate(relFile);
2956
- return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
2957
- method,
2958
- pathTemplate: canonicalizeTemplate(template),
2959
- line,
2960
- framework: "next"
2961
- }));
2962
- }
2963
- if (isNextPagesApiFile(relFile)) {
2964
- return [
2965
- {
2966
- method: "ALL",
2967
- pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
2968
- line: 1,
2969
- framework: "next"
2970
- }
2971
- ];
2972
- }
2973
- return [];
3025
+ var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
3026
+ var NESTJS_METHODS = /* @__PURE__ */ new Map([
3027
+ ["Get", "GET"],
3028
+ ["Post", "POST"],
3029
+ ["Put", "PUT"],
3030
+ ["Patch", "PATCH"],
3031
+ ["Delete", "DELETE"],
3032
+ ["Options", "OPTIONS"],
3033
+ ["Head", "HEAD"],
3034
+ ["All", "ALL"]
3035
+ ]);
3036
+ function canonicalizeTemplate(raw) {
3037
+ let p = raw.split("?")[0].split("#")[0];
3038
+ if (!p.startsWith("/")) p = "/" + p;
3039
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
3040
+ return p;
2974
3041
  }
2975
- function pyStaticStringText(node) {
2976
- if (node.type !== "string") return null;
3042
+ function isDynamicSegment(seg) {
3043
+ if (seg.length === 0) return false;
3044
+ if (seg.includes(":")) return true;
3045
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
3046
+ if (/^\d+$/.test(seg)) return true;
3047
+ 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;
3048
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
3049
+ return false;
3050
+ }
3051
+ function normalizePathTemplate(raw) {
3052
+ const canonical = canonicalizeTemplate(raw);
3053
+ const segments = canonical.split("/").filter((s) => s.length > 0);
3054
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
3055
+ return "/" + normalised.join("/");
3056
+ }
3057
+ function walk(node, visit) {
3058
+ visit(node);
2977
3059
  for (let i = 0; i < node.namedChildCount; i++) {
2978
3060
  const child = node.namedChild(i);
2979
- if (child?.type === "interpolation") return null;
2980
- if (child?.type === "string_content") return child.text;
3061
+ if (child) walk(child, visit);
2981
3062
  }
2982
- return "";
2983
3063
  }
2984
- function keywordArrayStrings(argsNode, key) {
2985
- for (let i = 0; i < argsNode.namedChildCount; i++) {
2986
- const arg = argsNode.namedChild(i);
2987
- if (arg?.type !== "keyword_argument") continue;
2988
- if (arg.childForFieldName("name")?.text !== key) continue;
2989
- const val = arg.childForFieldName("value");
2990
- if (!val || val.type !== "list") return [];
2991
- const out = [];
2992
- for (let j = 0; j < val.namedChildCount; j++) {
2993
- const el = val.namedChild(j);
2994
- if (el?.type === "string") {
2995
- const s = pyStaticStringText(el);
2996
- if (s) out.push(s);
2997
- }
3064
+ function staticStringText(node) {
3065
+ if (node.type === "string") {
3066
+ for (let i = 0; i < node.namedChildCount; i++) {
3067
+ const child = node.namedChild(i);
3068
+ if (child?.type === "string_fragment") return child.text;
2998
3069
  }
2999
- return out;
3070
+ return "";
3000
3071
  }
3001
- return [];
3002
- }
3003
- function collectPythonRouterPrefixes(root) {
3004
- const prefixes = /* @__PURE__ */ new Map();
3005
- walk(root, (node) => {
3006
- if (node.type !== "assignment") return;
3007
- const right = node.childForFieldName("right");
3008
- if (!right || right.type !== "call") return;
3009
- const fn = right.childForFieldName("function");
3010
- if (!fn) return;
3011
- const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
3012
- const prefixKey = ctor === "APIRouter" ? "prefix" : ctor === "Blueprint" ? "url_prefix" : null;
3013
- if (!prefixKey) return;
3014
- const left = node.childForFieldName("left");
3015
- if (!left || left.type !== "identifier") return;
3016
- const args = right.childForFieldName("arguments");
3017
- if (!args) return;
3018
- for (let i = 0; i < args.namedChildCount; i++) {
3019
- const arg = args.namedChild(i);
3020
- if (arg?.type !== "keyword_argument") continue;
3021
- if (arg.childForFieldName("name")?.text !== prefixKey) continue;
3022
- const val = arg.childForFieldName("value");
3023
- const p = val ? pyStaticStringText(val) : null;
3024
- if (p !== null) prefixes.set(left.text, p);
3072
+ if (node.type === "template_string") {
3073
+ for (let i = 0; i < node.namedChildCount; i++) {
3074
+ if (node.namedChild(i)?.type === "template_substitution") return null;
3025
3075
  }
3026
- });
3027
- return prefixes;
3076
+ const raw = node.text;
3077
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
3078
+ }
3079
+ return null;
3028
3080
  }
3029
- function collectStringConstants(root) {
3030
- const consts = /* @__PURE__ */ new Map();
3081
+ function objectStringProp(objNode, key) {
3082
+ for (let i = 0; i < objNode.namedChildCount; i++) {
3083
+ const pair = objNode.namedChild(i);
3084
+ if (!pair || pair.type !== "pair") continue;
3085
+ const k = pair.childForFieldName("key");
3086
+ if (!k) continue;
3087
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
3088
+ if (kText !== key) continue;
3089
+ const v = pair.childForFieldName("value");
3090
+ if (v) return staticStringText(v);
3091
+ }
3092
+ return null;
3093
+ }
3094
+ function fastifyRouteMethods(objNode) {
3095
+ for (let i = 0; i < objNode.namedChildCount; i++) {
3096
+ const pair = objNode.namedChild(i);
3097
+ if (!pair || pair.type !== "pair") continue;
3098
+ const k = pair.childForFieldName("key");
3099
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
3100
+ if (kText !== "method") continue;
3101
+ const v = pair.childForFieldName("value");
3102
+ if (!v) return [];
3103
+ if (v.type === "string" || v.type === "template_string") {
3104
+ const s = staticStringText(v);
3105
+ return s ? [s.toUpperCase()] : [];
3106
+ }
3107
+ if (v.type === "array") {
3108
+ const out = [];
3109
+ for (let j = 0; j < v.namedChildCount; j++) {
3110
+ const el = v.namedChild(j);
3111
+ if (el && (el.type === "string" || el.type === "template_string")) {
3112
+ const s = staticStringText(el);
3113
+ if (s) out.push(s.toUpperCase());
3114
+ }
3115
+ }
3116
+ return out;
3117
+ }
3118
+ }
3119
+ return [];
3120
+ }
3121
+ function nestDecoratorImports(root) {
3122
+ const imports = /* @__PURE__ */ new Map();
3031
3123
  walk(root, (node) => {
3032
- if (node.type !== "assignment") return;
3033
- const left = node.childForFieldName("left");
3034
- const right = node.childForFieldName("right");
3035
- if (left?.type !== "identifier" || right?.type !== "string") return;
3036
- const v = pyStaticStringText(right);
3037
- if (v !== null) consts.set(left.text, v);
3124
+ if (node.type !== "import_statement") return;
3125
+ const source = node.childForFieldName("source");
3126
+ if (!source || staticStringText(source) !== "@nestjs/common") return;
3127
+ walk(node, (child) => {
3128
+ if (child.type !== "import_specifier") return;
3129
+ const imported = child.childForFieldName("name")?.text;
3130
+ const local = child.childForFieldName("alias")?.text ?? imported;
3131
+ if (!imported || !local) return;
3132
+ if (imported === "Controller" || NESTJS_METHODS.has(imported)) {
3133
+ imports.set(local, imported);
3134
+ }
3135
+ });
3038
3136
  });
3039
- return consts;
3137
+ return imports;
3040
3138
  }
3041
- function resolvePrefixArg(node, consts) {
3042
- if (!node) return null;
3043
- if (node.type === "string") return pyStaticStringText(node);
3044
- if (node.type === "identifier") return consts.get(node.text) ?? null;
3045
- if (node.type === "attribute") {
3046
- const attr = node.childForFieldName("attribute")?.text;
3047
- return attr ? consts.get(attr) ?? null : null;
3139
+ function decoratorCall(node) {
3140
+ if (node.type !== "decorator") return null;
3141
+ const expression = node.namedChild(0);
3142
+ return expression?.type === "call_expression" ? expression : null;
3143
+ }
3144
+ function decoratorCanonicalName(decorator, imports) {
3145
+ const call = decoratorCall(decorator);
3146
+ const fn = call?.childForFieldName("function");
3147
+ if (!fn || fn.type !== "identifier") return null;
3148
+ return imports.get(fn.text) ?? null;
3149
+ }
3150
+ function nestStaticPaths(call) {
3151
+ const args = call.childForFieldName("arguments");
3152
+ const first = args?.namedChild(0);
3153
+ if (!first) return [""];
3154
+ const single = staticStringText(first);
3155
+ if (single !== null) return [single];
3156
+ if (first.type !== "array") return [];
3157
+ const paths = [];
3158
+ for (let i = 0; i < first.namedChildCount; i++) {
3159
+ const item = first.namedChild(i);
3160
+ if (!item) continue;
3161
+ const value = staticStringText(item);
3162
+ if (value !== null) paths.push(value);
3048
3163
  }
3049
- return null;
3164
+ return paths;
3050
3165
  }
3051
- function collectMountPrefixes(root, consts) {
3052
- const mounts = /* @__PURE__ */ new Map();
3053
- walk(root, (node) => {
3054
- if (node.type !== "call") return;
3055
- const fn = node.childForFieldName("function");
3056
- if (fn?.type !== "attribute") return;
3057
- const method = fn.childForFieldName("attribute")?.text;
3058
- const prefixKey = method === "include_router" ? "prefix" : method === "register_blueprint" ? "url_prefix" : null;
3059
- if (!prefixKey) return;
3060
- const args = node.childForFieldName("arguments");
3061
- const first = args?.namedChild(0);
3062
- if (first?.type !== "identifier") return;
3063
- let prefix = null;
3064
- for (let i = 0; i < (args?.namedChildCount ?? 0); i++) {
3065
- const a = args.namedChild(i);
3066
- if (a?.type !== "keyword_argument") continue;
3067
- if (a.childForFieldName("name")?.text !== prefixKey) continue;
3068
- prefix = resolvePrefixArg(a.childForFieldName("value"), consts);
3166
+ function nestJoinedPath(prefix, leaf) {
3167
+ const segments = [prefix, leaf].map((part) => part.replace(/^\/+|\/+$/g, "")).filter((part) => part.length > 0);
3168
+ return canonicalizeTemplate(segments.join("/"));
3169
+ }
3170
+ function nestjsRoutesFromSource(source, parser) {
3171
+ const tree = parseSource2(parser, source);
3172
+ const imports = nestDecoratorImports(tree.rootNode);
3173
+ if (![...imports.values()].includes("Controller")) return [];
3174
+ const out = [];
3175
+ walk(tree.rootNode, (node) => {
3176
+ if (node.type !== "class_declaration") return;
3177
+ const decoratorOwner = node.parent?.type === "export_statement" ? node.parent : node;
3178
+ const classDecorators = [];
3179
+ for (let i = 0; i < decoratorOwner.namedChildCount; i++) {
3180
+ const child = decoratorOwner.namedChild(i);
3181
+ if (child?.type === "decorator") classDecorators.push(child);
3182
+ }
3183
+ const controller = classDecorators.find(
3184
+ (decorator) => decoratorCanonicalName(decorator, imports) === "Controller"
3185
+ );
3186
+ const controllerCall = controller ? decoratorCall(controller) : null;
3187
+ if (!controllerCall) return;
3188
+ const prefixes = nestStaticPaths(controllerCall);
3189
+ if (prefixes.length === 0) return;
3190
+ const body = node.childForFieldName("body");
3191
+ if (!body) return;
3192
+ for (let i = 0; i < body.namedChildCount; i++) {
3193
+ const methodNode = body.namedChild(i);
3194
+ if (methodNode?.type !== "method_definition") continue;
3195
+ for (let j = 0; j < methodNode.namedChildCount; j++) {
3196
+ const decorator = methodNode.namedChild(j);
3197
+ if (decorator?.type !== "decorator") continue;
3198
+ const canonical = decoratorCanonicalName(decorator, imports);
3199
+ const method = canonical ? NESTJS_METHODS.get(canonical) : void 0;
3200
+ const call = decoratorCall(decorator);
3201
+ if (!method || !call) continue;
3202
+ const leaves = nestStaticPaths(call);
3203
+ for (const prefix of prefixes) {
3204
+ for (const leaf of leaves) {
3205
+ out.push({
3206
+ method,
3207
+ pathTemplate: nestJoinedPath(prefix, leaf),
3208
+ line: decorator.startPosition.row + 1,
3209
+ framework: "nestjs"
3210
+ });
3211
+ }
3212
+ }
3213
+ }
3069
3214
  }
3070
- if (prefix !== null && prefix.length > 0) mounts.set(first.text, prefix);
3071
3215
  });
3072
- return mounts;
3216
+ return out;
3073
3217
  }
3074
- function pythonRoutesFromSource(source, parser, framework) {
3075
- const tree = parseSource(parser, source);
3076
- const prefixes = collectPythonRouterPrefixes(tree.rootNode);
3077
- const consts = collectStringConstants(tree.rootNode);
3078
- const mounts = collectMountPrefixes(tree.rootNode, consts);
3218
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify, hasHono = false) {
3219
+ const tree = parseSource2(parser, source);
3079
3220
  const out = [];
3221
+ const framework = hasExpress ? "express" : hasFastify ? "fastify" : hasHono ? "hono" : "unknown";
3080
3222
  walk(tree.rootNode, (node) => {
3081
- if (node.type !== "decorator") return;
3082
- const call = node.namedChild(0);
3083
- if (!call || call.type !== "call") return;
3084
- const fn = call.childForFieldName("function");
3085
- if (!fn || fn.type !== "attribute") return;
3086
- const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
3087
- if (!method) return;
3088
- const isVerb = FASTAPI_METHODS.has(method);
3089
- const isFlaskRoute = method === "route";
3090
- const isApiRoute = method === "api_route";
3091
- if (!isVerb && !isFlaskRoute && !isApiRoute) return;
3092
- const args = call.childForFieldName("arguments");
3223
+ if (node.type !== "call_expression") return;
3224
+ const fn = node.childForFieldName("function");
3225
+ if (!fn || fn.type !== "member_expression") return;
3226
+ const prop = fn.childForFieldName("property");
3227
+ if (!prop) return;
3228
+ const method = prop.text.toLowerCase();
3229
+ const args = node.childForFieldName("arguments");
3093
3230
  const first = args?.namedChild(0);
3094
- if (!first || first.type !== "string") return;
3095
- const rawPath = pyStaticStringText(first);
3096
- if (rawPath === null || !rawPath.startsWith("/")) return;
3097
- const obj = fn.childForFieldName("object")?.text;
3098
- const routerPrefix = obj ? prefixes.get(obj) ?? "" : "";
3099
- const mountPrefix = obj ? mounts.get(obj) ?? "" : "";
3100
- const pathTemplate = canonicalizeTemplate(mountPrefix + routerPrefix + rawPath);
3231
+ if (!first) return;
3101
3232
  const line = node.startPosition.row + 1;
3102
- if (isVerb) {
3103
- out.push({ method: method.toUpperCase(), pathTemplate, line, framework });
3233
+ if (ROUTER_METHODS.has(method)) {
3234
+ const p = staticStringText(first);
3235
+ if (p && p.startsWith("/")) {
3236
+ out.push({
3237
+ method: method === "all" ? "ALL" : method.toUpperCase(),
3238
+ pathTemplate: canonicalizeTemplate(p),
3239
+ line,
3240
+ framework
3241
+ });
3242
+ }
3104
3243
  return;
3105
3244
  }
3106
- const methods = keywordArrayStrings(args, "methods");
3107
- const list = methods.length > 0 ? methods : isFlaskRoute ? ["GET"] : ["ALL"];
3108
- for (const m of list) {
3109
- out.push({ method: m === "ALL" ? "ALL" : m.toUpperCase(), pathTemplate, line, framework });
3245
+ if (method === "route" && hasFastify && first.type === "object") {
3246
+ const url = objectStringProp(first, "url");
3247
+ if (!url || !url.startsWith("/")) return;
3248
+ const methods = fastifyRouteMethods(first);
3249
+ const list = methods.length > 0 ? methods : ["ALL"];
3250
+ for (const m of list) {
3251
+ out.push({
3252
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
3253
+ pathTemplate: canonicalizeTemplate(url),
3254
+ line,
3255
+ framework: "fastify"
3256
+ });
3257
+ }
3110
3258
  }
3111
3259
  });
3112
3260
  return out;
3113
3261
  }
3114
- function djangoRoutesFromSource(source, parser) {
3115
- const tree = parseSource(parser, source);
3116
- const out = [];
3117
- walk(tree.rootNode, (node) => {
3118
- if (node.type !== "assignment") return;
3262
+ function segmentsOf(relFile) {
3263
+ return toPosix(relFile).split("/").filter((s) => s.length > 0);
3264
+ }
3265
+ function isNextAppRouteFile(relFile) {
3266
+ const segs = segmentsOf(relFile);
3267
+ if (!segs.includes("app")) return false;
3268
+ const base = segs[segs.length - 1] ?? "";
3269
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
3270
+ }
3271
+ function isNextPagesApiFile(relFile) {
3272
+ const segs = segmentsOf(relFile);
3273
+ const pagesIdx = segs.indexOf("pages");
3274
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
3275
+ const base = segs[segs.length - 1] ?? "";
3276
+ if (/^_(app|document|middleware)\./.test(base)) return false;
3277
+ return JS_ROUTE_EXTENSIONS.has(import_node_path8.default.extname(base));
3278
+ }
3279
+ function nextSegment(seg) {
3280
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
3281
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
3282
+ if (catchAll) return ":" + catchAll[1];
3283
+ const dynamic = seg.match(/^\[(.+?)\]$/);
3284
+ if (dynamic) return ":" + dynamic[1];
3285
+ return seg;
3286
+ }
3287
+ function nextAppPathTemplate(relFile) {
3288
+ const segs = segmentsOf(relFile);
3289
+ const appIdx = segs.lastIndexOf("app");
3290
+ const between = segs.slice(appIdx + 1, segs.length - 1);
3291
+ const parts = [];
3292
+ for (const seg of between) {
3293
+ const mapped = nextSegment(seg);
3294
+ if (mapped !== null) parts.push(mapped);
3295
+ }
3296
+ return "/" + parts.join("/");
3297
+ }
3298
+ function nextPagesApiPathTemplate(relFile) {
3299
+ const segs = segmentsOf(relFile);
3300
+ const pagesIdx = segs.indexOf("pages");
3301
+ const rest = segs.slice(pagesIdx + 1);
3302
+ const parts = [];
3303
+ for (let i = 0; i < rest.length; i++) {
3304
+ let seg = rest[i];
3305
+ if (i === rest.length - 1) {
3306
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
3307
+ if (seg === "index") continue;
3308
+ }
3309
+ const mapped = nextSegment(seg);
3310
+ if (mapped !== null) parts.push(mapped);
3311
+ }
3312
+ return "/" + parts.join("/");
3313
+ }
3314
+ function nextAppMethods(root) {
3315
+ const out = [];
3316
+ walk(root, (node) => {
3317
+ if (node.type !== "export_statement") return;
3318
+ const decl = node.childForFieldName("declaration");
3319
+ if (!decl) return;
3320
+ const line = node.startPosition.row + 1;
3321
+ if (decl.type === "function_declaration") {
3322
+ const name = decl.childForFieldName("name")?.text;
3323
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
3324
+ return;
3325
+ }
3326
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
3327
+ for (let i = 0; i < decl.namedChildCount; i++) {
3328
+ const d = decl.namedChild(i);
3329
+ if (d?.type !== "variable_declarator") continue;
3330
+ const name = d.childForFieldName("name")?.text;
3331
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
3332
+ }
3333
+ }
3334
+ });
3335
+ return out;
3336
+ }
3337
+ function nextRoutesFromFile(source, relFile, parser) {
3338
+ if (isNextAppRouteFile(relFile)) {
3339
+ const tree = parseSource2(parser, source);
3340
+ const template = nextAppPathTemplate(relFile);
3341
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
3342
+ method,
3343
+ pathTemplate: canonicalizeTemplate(template),
3344
+ line,
3345
+ framework: "next"
3346
+ }));
3347
+ }
3348
+ if (isNextPagesApiFile(relFile)) {
3349
+ return [
3350
+ {
3351
+ method: "ALL",
3352
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
3353
+ line: 1,
3354
+ framework: "next"
3355
+ }
3356
+ ];
3357
+ }
3358
+ return [];
3359
+ }
3360
+ function pyStaticStringText(node) {
3361
+ if (node.type !== "string") return null;
3362
+ for (let i = 0; i < node.namedChildCount; i++) {
3363
+ const child = node.namedChild(i);
3364
+ if (child?.type === "interpolation") return null;
3365
+ if (child?.type === "string_content") return child.text;
3366
+ }
3367
+ return "";
3368
+ }
3369
+ function keywordArrayStrings(argsNode, key) {
3370
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
3371
+ const arg = argsNode.namedChild(i);
3372
+ if (arg?.type !== "keyword_argument") continue;
3373
+ if (arg.childForFieldName("name")?.text !== key) continue;
3374
+ const val = arg.childForFieldName("value");
3375
+ if (!val || val.type !== "list") return [];
3376
+ const out = [];
3377
+ for (let j = 0; j < val.namedChildCount; j++) {
3378
+ const el = val.namedChild(j);
3379
+ if (el?.type === "string") {
3380
+ const s = pyStaticStringText(el);
3381
+ if (s) out.push(s);
3382
+ }
3383
+ }
3384
+ return out;
3385
+ }
3386
+ return [];
3387
+ }
3388
+ function collectPythonRouterPrefixes(root) {
3389
+ const prefixes = /* @__PURE__ */ new Map();
3390
+ walk(root, (node) => {
3391
+ if (node.type !== "assignment") return;
3392
+ const right = node.childForFieldName("right");
3393
+ if (!right || right.type !== "call") return;
3394
+ const fn = right.childForFieldName("function");
3395
+ if (!fn) return;
3396
+ const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
3397
+ const prefixKey = ctor === "APIRouter" ? "prefix" : ctor === "Blueprint" ? "url_prefix" : null;
3398
+ if (!prefixKey) return;
3399
+ const left = node.childForFieldName("left");
3400
+ if (!left || left.type !== "identifier") return;
3401
+ const args = right.childForFieldName("arguments");
3402
+ if (!args) return;
3403
+ for (let i = 0; i < args.namedChildCount; i++) {
3404
+ const arg = args.namedChild(i);
3405
+ if (arg?.type !== "keyword_argument") continue;
3406
+ if (arg.childForFieldName("name")?.text !== prefixKey) continue;
3407
+ const val = arg.childForFieldName("value");
3408
+ const p = val ? pyStaticStringText(val) : null;
3409
+ if (p !== null) prefixes.set(left.text, p);
3410
+ }
3411
+ });
3412
+ return prefixes;
3413
+ }
3414
+ function collectStringConstants(root) {
3415
+ const consts = /* @__PURE__ */ new Map();
3416
+ walk(root, (node) => {
3417
+ if (node.type !== "assignment") return;
3418
+ const left = node.childForFieldName("left");
3419
+ const right = node.childForFieldName("right");
3420
+ if (left?.type !== "identifier" || right?.type !== "string") return;
3421
+ const v = pyStaticStringText(right);
3422
+ if (v !== null) consts.set(left.text, v);
3423
+ });
3424
+ return consts;
3425
+ }
3426
+ function resolvePrefixArg(node, consts) {
3427
+ if (!node) return null;
3428
+ if (node.type === "string") return pyStaticStringText(node);
3429
+ if (node.type === "identifier") return consts.get(node.text) ?? null;
3430
+ if (node.type === "attribute") {
3431
+ const attr = node.childForFieldName("attribute")?.text;
3432
+ return attr ? consts.get(attr) ?? null : null;
3433
+ }
3434
+ return null;
3435
+ }
3436
+ function collectMountPrefixes(root, consts) {
3437
+ const mounts = /* @__PURE__ */ new Map();
3438
+ walk(root, (node) => {
3439
+ if (node.type !== "call") return;
3440
+ const fn = node.childForFieldName("function");
3441
+ if (fn?.type !== "attribute") return;
3442
+ const method = fn.childForFieldName("attribute")?.text;
3443
+ const prefixKey = method === "include_router" ? "prefix" : method === "register_blueprint" ? "url_prefix" : null;
3444
+ if (!prefixKey) return;
3445
+ const args = node.childForFieldName("arguments");
3446
+ const first = args?.namedChild(0);
3447
+ if (first?.type !== "identifier") return;
3448
+ let prefix = null;
3449
+ for (let i = 0; i < (args?.namedChildCount ?? 0); i++) {
3450
+ const a = args.namedChild(i);
3451
+ if (a?.type !== "keyword_argument") continue;
3452
+ if (a.childForFieldName("name")?.text !== prefixKey) continue;
3453
+ prefix = resolvePrefixArg(a.childForFieldName("value"), consts);
3454
+ }
3455
+ if (prefix !== null && prefix.length > 0) mounts.set(first.text, prefix);
3456
+ });
3457
+ return mounts;
3458
+ }
3459
+ function pythonRoutesFromSource(source, parser, framework) {
3460
+ const tree = parseSource2(parser, source);
3461
+ const prefixes = collectPythonRouterPrefixes(tree.rootNode);
3462
+ const consts = collectStringConstants(tree.rootNode);
3463
+ const mounts = collectMountPrefixes(tree.rootNode, consts);
3464
+ const out = [];
3465
+ walk(tree.rootNode, (node) => {
3466
+ if (node.type !== "decorator") return;
3467
+ const call = node.namedChild(0);
3468
+ if (!call || call.type !== "call") return;
3469
+ const fn = call.childForFieldName("function");
3470
+ if (!fn || fn.type !== "attribute") return;
3471
+ const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
3472
+ if (!method) return;
3473
+ const isVerb = FASTAPI_METHODS.has(method);
3474
+ const isFlaskRoute = method === "route";
3475
+ const isApiRoute = method === "api_route";
3476
+ if (!isVerb && !isFlaskRoute && !isApiRoute) return;
3477
+ const args = call.childForFieldName("arguments");
3478
+ const first = args?.namedChild(0);
3479
+ if (!first || first.type !== "string") return;
3480
+ const rawPath = pyStaticStringText(first);
3481
+ if (rawPath === null || !rawPath.startsWith("/")) return;
3482
+ const obj = fn.childForFieldName("object")?.text;
3483
+ const routerPrefix = obj ? prefixes.get(obj) ?? "" : "";
3484
+ const mountPrefix = obj ? mounts.get(obj) ?? "" : "";
3485
+ const pathTemplate = canonicalizeTemplate(mountPrefix + routerPrefix + rawPath);
3486
+ const line = node.startPosition.row + 1;
3487
+ if (isVerb) {
3488
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework });
3489
+ return;
3490
+ }
3491
+ const methods = keywordArrayStrings(args, "methods");
3492
+ const list = methods.length > 0 ? methods : isFlaskRoute ? ["GET"] : ["ALL"];
3493
+ for (const m of list) {
3494
+ out.push({ method: m === "ALL" ? "ALL" : m.toUpperCase(), pathTemplate, line, framework });
3495
+ }
3496
+ });
3497
+ return out;
3498
+ }
3499
+ function djangoRoutesFromSource(source, parser) {
3500
+ const tree = parseSource2(parser, source);
3501
+ const out = [];
3502
+ walk(tree.rootNode, (node) => {
3503
+ if (node.type !== "assignment") return;
3119
3504
  if (node.childForFieldName("left")?.text !== "urlpatterns") return;
3120
3505
  const list = node.childForFieldName("right");
3121
3506
  if (!list || list.type !== "list") return;
@@ -3140,36 +3525,347 @@ function djangoRoutesFromSource(source, parser) {
3140
3525
  });
3141
3526
  return out;
3142
3527
  }
3143
- async function addRoutes(graph, services) {
3144
- const jsParser = makeJsParser();
3145
- const pyParser = makePyParser();
3146
- const goParser = makeGoParser();
3147
- let nodesAdded = 0;
3148
- let edgesAdded = 0;
3149
- for (const service of services) {
3150
- const deps = {
3151
- ...service.pkg.dependencies ?? {},
3152
- ...service.pkg.devDependencies ?? {}
3153
- };
3154
- const hasExpress = deps["express"] !== void 0;
3155
- const hasFastify = deps["fastify"] !== void 0;
3156
- const hasHono = deps["hono"] !== void 0;
3157
- const hasNext = deps["next"] !== void 0;
3158
- const hasNestjs = deps["@nestjs/core"] !== void 0;
3159
- const hasFastapi = deps["fastapi"] !== void 0;
3160
- const hasFlask = deps["flask"] !== void 0;
3528
+ function namedArgs(argsNode) {
3529
+ const out = [];
3530
+ if (!argsNode) return out;
3531
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
3532
+ const c = argsNode.namedChild(i);
3533
+ if (c && c.type !== "comment") out.push(c);
3534
+ }
3535
+ return out;
3536
+ }
3537
+ function parseUseMount(callNode) {
3538
+ const args = namedArgs(callNode.childForFieldName("arguments"));
3539
+ if (args.length === 0) return null;
3540
+ const first = args[0];
3541
+ const firstStr = first.type === "string" || first.type === "template_string" ? staticStringText(first) : null;
3542
+ if (firstStr !== null && firstStr.startsWith("/")) {
3543
+ const prefix = canonicalizeTemplate(firstStr);
3544
+ const second = args[1];
3545
+ const target = second && second.type === "identifier" ? second.text : null;
3546
+ return { prefix: prefix === "/" ? "" : prefix, target };
3547
+ }
3548
+ if (args.length === 1 && first.type === "identifier") return { prefix: "", target: first.text };
3549
+ return null;
3550
+ }
3551
+ function unwrapRouterExpr(node, expressLocals, routerCtors) {
3552
+ if (node.type === "identifier") return { base: { alias: node.text }, mounts: [] };
3553
+ if (node.type !== "call_expression") return null;
3554
+ const fn = node.childForFieldName("function");
3555
+ if (!fn) return null;
3556
+ if (fn.type === "member_expression") {
3557
+ const prop = fn.childForFieldName("property")?.text;
3558
+ const obj = fn.childForFieldName("object");
3559
+ if (!prop || !obj) return null;
3560
+ if (prop === "use") {
3561
+ const inner = unwrapRouterExpr(obj, expressLocals, routerCtors);
3562
+ if (!inner) return null;
3563
+ const mount = parseUseMount(node);
3564
+ return { base: inner.base, mounts: mount ? [...inner.mounts, mount] : inner.mounts };
3565
+ }
3566
+ if (prop === "Router" && obj.type === "identifier" && expressLocals.has(obj.text)) {
3567
+ return { base: "newRouter", mounts: [] };
3568
+ }
3569
+ return null;
3570
+ }
3571
+ if (fn.type === "identifier") {
3572
+ if (expressLocals.has(fn.text)) return { base: "app", mounts: [] };
3573
+ if (routerCtors.has(fn.text)) return { base: "newRouter", mounts: [] };
3574
+ }
3575
+ return null;
3576
+ }
3577
+ function collectExpressImports(root) {
3578
+ const expressLocals = /* @__PURE__ */ new Set();
3579
+ const routerCtors = /* @__PURE__ */ new Set();
3580
+ const bindings = [];
3581
+ const addFromExpress = (local, sel, exported) => {
3582
+ if (sel === "default" || sel === "namespace") expressLocals.add(local);
3583
+ else if (exported === "Router") routerCtors.add(local);
3584
+ };
3585
+ walk(root, (node) => {
3586
+ if (node.type === "import_statement") {
3587
+ const source = node.childForFieldName("source");
3588
+ const spec = source ? staticStringText(source) : null;
3589
+ if (!spec) return;
3590
+ let clause = null;
3591
+ for (let i = 0; i < node.namedChildCount; i++) {
3592
+ const c = node.namedChild(i);
3593
+ if (c?.type === "import_clause") clause = c;
3594
+ }
3595
+ if (!clause) return;
3596
+ for (let i = 0; i < clause.namedChildCount; i++) {
3597
+ const c = clause.namedChild(i);
3598
+ if (!c) continue;
3599
+ if (c.type === "identifier") {
3600
+ if (spec === "express") addFromExpress(c.text, "default", "default");
3601
+ else bindings.push({ local: c.text, specifier: spec, sel: "default" });
3602
+ } else if (c.type === "namespace_import") {
3603
+ const id = c.namedChild(0);
3604
+ if (id?.type === "identifier") {
3605
+ if (spec === "express") addFromExpress(id.text, "namespace", "namespace");
3606
+ else bindings.push({ local: id.text, specifier: spec, sel: "namespace" });
3607
+ }
3608
+ } else if (c.type === "named_imports") {
3609
+ for (let j = 0; j < c.namedChildCount; j++) {
3610
+ const s = c.namedChild(j);
3611
+ if (s?.type !== "import_specifier") continue;
3612
+ const name = s.childForFieldName("name")?.text;
3613
+ if (!name) continue;
3614
+ const local = s.childForFieldName("alias")?.text ?? name;
3615
+ if (spec === "express") addFromExpress(local, name, name);
3616
+ else bindings.push({ local, specifier: spec, sel: name });
3617
+ }
3618
+ }
3619
+ }
3620
+ return;
3621
+ }
3622
+ if (node.type === "variable_declarator") {
3623
+ const value = node.childForFieldName("value");
3624
+ if (value?.type !== "call_expression") return;
3625
+ const fn = value.childForFieldName("function");
3626
+ if (fn?.type !== "identifier" || fn.text !== "require") return;
3627
+ const arg = namedArgs(value.childForFieldName("arguments"))[0];
3628
+ const spec = arg ? staticStringText(arg) : null;
3629
+ if (!spec) return;
3630
+ const name = node.childForFieldName("name");
3631
+ if (name?.type === "identifier") {
3632
+ if (spec === "express") expressLocals.add(name.text);
3633
+ else bindings.push({ local: name.text, specifier: spec, sel: "default" });
3634
+ } else if (name?.type === "object_pattern") {
3635
+ for (let i = 0; i < name.namedChildCount; i++) {
3636
+ const el = name.namedChild(i);
3637
+ if (!el) continue;
3638
+ let local;
3639
+ let exported;
3640
+ if (el.type === "shorthand_property_identifier_pattern") {
3641
+ local = el.text;
3642
+ exported = el.text;
3643
+ } else if (el.type === "pair_pattern") {
3644
+ exported = el.childForFieldName("key")?.text;
3645
+ local = el.childForFieldName("value")?.text ?? exported;
3646
+ }
3647
+ if (!local || !exported) continue;
3648
+ if (spec === "express") addFromExpress(local, exported, exported);
3649
+ else bindings.push({ local, specifier: spec, sel: exported });
3650
+ }
3651
+ }
3652
+ }
3653
+ });
3654
+ return { expressLocals, routerCtors, bindings };
3655
+ }
3656
+ function analyzeExpressFile(root, dir) {
3657
+ const { expressLocals, routerCtors, bindings } = collectExpressImports(root);
3658
+ const routerVars = /* @__PURE__ */ new Map();
3659
+ const appVars = /* @__PURE__ */ new Set();
3660
+ const exportNamed = /* @__PURE__ */ new Map();
3661
+ let exportDefaultName = null;
3662
+ const getVar = (name) => {
3663
+ let rv = routerVars.get(name);
3664
+ if (!rv) {
3665
+ rv = { declares: false, mounts: [] };
3666
+ routerVars.set(name, rv);
3667
+ }
3668
+ return rv;
3669
+ };
3670
+ const refFromExpr = (expr, key) => {
3671
+ if (expr.type === "identifier") return expr.text;
3672
+ const u = unwrapRouterExpr(expr, expressLocals, routerCtors);
3673
+ if (!u) return null;
3674
+ const rv = getVar(key);
3675
+ for (const m of u.mounts) rv.mounts.push(m);
3676
+ if (typeof u.base === "object") rv.aliasOf = u.base.alias;
3677
+ return key;
3678
+ };
3679
+ const isExported = (declarator) => {
3680
+ const decl = declarator.parent;
3681
+ return decl?.parent?.type === "export_statement";
3682
+ };
3683
+ walk(root, (node) => {
3684
+ if (node.type === "variable_declarator") {
3685
+ const value = node.childForFieldName("value");
3686
+ const name = node.childForFieldName("name");
3687
+ if (name?.type !== "identifier" || !value) return;
3688
+ if (value.type === "call_expression") {
3689
+ const fn = value.childForFieldName("function");
3690
+ if (fn?.type === "identifier" && fn.text === "require") return;
3691
+ }
3692
+ const u = unwrapRouterExpr(value, expressLocals, routerCtors);
3693
+ if (!u) return;
3694
+ const rv = getVar(name.text);
3695
+ for (const m of u.mounts) rv.mounts.push(m);
3696
+ if (u.base === "app") appVars.add(name.text);
3697
+ else if (typeof u.base === "object") rv.aliasOf = u.base.alias;
3698
+ if (isExported(node)) exportNamed.set(name.text, name.text);
3699
+ return;
3700
+ }
3701
+ if (node.type === "call_expression") {
3702
+ const fn = node.childForFieldName("function");
3703
+ if (fn?.type !== "member_expression") return;
3704
+ const obj = fn.childForFieldName("object");
3705
+ const prop = fn.childForFieldName("property")?.text;
3706
+ if (obj?.type !== "identifier" || !prop) return;
3707
+ if (prop === "use") {
3708
+ const m = parseUseMount(node);
3709
+ if (m) getVar(obj.text).mounts.push(m);
3710
+ } else if (ROUTER_METHODS.has(prop.toLowerCase())) {
3711
+ const first = namedArgs(node.childForFieldName("arguments"))[0];
3712
+ const p = first ? staticStringText(first) : null;
3713
+ if (p !== null && p.startsWith("/")) getVar(obj.text).declares = true;
3714
+ }
3715
+ return;
3716
+ }
3717
+ if (node.type === "export_statement") {
3718
+ let clause = null;
3719
+ for (let i = 0; i < node.namedChildCount; i++) {
3720
+ const c = node.namedChild(i);
3721
+ if (c?.type === "export_clause") clause = c;
3722
+ }
3723
+ if (clause) {
3724
+ for (let i = 0; i < clause.namedChildCount; i++) {
3725
+ const spec = clause.namedChild(i);
3726
+ if (spec?.type !== "export_specifier") continue;
3727
+ const local = spec.childForFieldName("name")?.text;
3728
+ if (!local) continue;
3729
+ const exportedAs = spec.childForFieldName("alias")?.text ?? local;
3730
+ if (exportedAs === "default") exportDefaultName = local;
3731
+ else exportNamed.set(exportedAs, local);
3732
+ }
3733
+ return;
3734
+ }
3735
+ if (node.childForFieldName("declaration")) return;
3736
+ for (let i = 0; i < node.namedChildCount; i++) {
3737
+ const c = node.namedChild(i);
3738
+ if (c && c.type !== "export_clause") {
3739
+ exportDefaultName = refFromExpr(c, "#default");
3740
+ break;
3741
+ }
3742
+ }
3743
+ return;
3744
+ }
3745
+ if (node.type === "assignment_expression") {
3746
+ const left = node.childForFieldName("left");
3747
+ const right = node.childForFieldName("right");
3748
+ if (left?.type !== "member_expression" || !right) return;
3749
+ const lobj = left.childForFieldName("object")?.text;
3750
+ const lprop = left.childForFieldName("property")?.text;
3751
+ if (lobj === "module" && lprop === "exports") exportDefaultName = refFromExpr(right, "#default");
3752
+ else if (lobj === "exports" && lprop) {
3753
+ const key = refFromExpr(right, `#exp:${lprop}`);
3754
+ if (key) exportNamed.set(lprop, key);
3755
+ }
3756
+ }
3757
+ });
3758
+ return {
3759
+ dir,
3760
+ routerVars,
3761
+ appVars,
3762
+ exportDefaultName,
3763
+ exportNamed,
3764
+ rawBindings: bindings,
3765
+ importedRouters: /* @__PURE__ */ new Map()
3766
+ };
3767
+ }
3768
+ async function expressMountPrefixes(files, serviceDir, tsPaths) {
3769
+ const jsParser = makeJsParser2();
3770
+ const fileInfo = /* @__PURE__ */ new Map();
3771
+ for (const f of files) {
3772
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path8.default.extname(f.path))) continue;
3773
+ if (isTestPath(f.path)) continue;
3774
+ const rel = toPosix(import_node_path8.default.relative(serviceDir, f.path));
3775
+ try {
3776
+ const tree = parseSource2(jsParser, f.content);
3777
+ fileInfo.set(rel, analyzeExpressFile(tree.rootNode, import_node_path8.default.dirname(f.path)));
3778
+ } catch {
3779
+ }
3780
+ }
3781
+ if (fileInfo.size === 0) return /* @__PURE__ */ new Map();
3782
+ for (const info of fileInfo.values()) {
3783
+ for (const b of info.rawBindings) {
3784
+ const resolved = await resolveJsImport(b.specifier, info.dir, serviceDir, tsPaths);
3785
+ if (!resolved || !fileInfo.has(resolved)) continue;
3786
+ info.importedRouters.set(b.local, { file: resolved, sel: b.sel === "namespace" ? "default" : b.sel });
3787
+ }
3788
+ }
3789
+ const resolveTarget = (name, file) => {
3790
+ const info = fileInfo.get(file);
3791
+ if (!info) return null;
3792
+ if (info.routerVars.has(name)) return { file, name };
3793
+ const imp = info.importedRouters.get(name);
3794
+ if (!imp) return null;
3795
+ const target = fileInfo.get(imp.file);
3796
+ if (!target) return null;
3797
+ const key = imp.sel === "default" ? target.exportDefaultName : target.exportNamed.get(imp.sel);
3798
+ if (!key) return null;
3799
+ return { file: imp.file, name: key };
3800
+ };
3801
+ const filePrefix = /* @__PURE__ */ new Map();
3802
+ const conflicted = /* @__PURE__ */ new Set();
3803
+ const apply4 = (file, prefix) => {
3804
+ if (conflicted.has(file)) return;
3805
+ const existing = filePrefix.get(file);
3806
+ if (existing === void 0) filePrefix.set(file, prefix);
3807
+ else if (existing !== prefix) {
3808
+ filePrefix.delete(file);
3809
+ conflicted.add(file);
3810
+ }
3811
+ };
3812
+ const visited = /* @__PURE__ */ new Set();
3813
+ const collect = (file, name, accPrefix) => {
3814
+ const key = `${file}|${name}|${accPrefix}`;
3815
+ if (visited.has(key)) return;
3816
+ visited.add(key);
3817
+ const info = fileInfo.get(file);
3818
+ const rv = info?.routerVars.get(name);
3819
+ if (!info || !rv) return;
3820
+ if (rv.declares && info.appVars.size === 0) apply4(file, accPrefix);
3821
+ for (const m of rv.mounts) {
3822
+ if (!m.target) continue;
3823
+ const t = resolveTarget(m.target, file);
3824
+ if (t) collect(t.file, t.name, accPrefix + m.prefix);
3825
+ }
3826
+ if (rv.aliasOf) {
3827
+ const t = resolveTarget(rv.aliasOf, file);
3828
+ if (t) collect(t.file, t.name, accPrefix);
3829
+ }
3830
+ };
3831
+ for (const [rel, info] of fileInfo) {
3832
+ for (const appVar of info.appVars) collect(rel, appVar, "");
3833
+ }
3834
+ const out = /* @__PURE__ */ new Map();
3835
+ for (const [file, prefix] of filePrefix) if (prefix && prefix !== "/") out.set(file, prefix);
3836
+ return out;
3837
+ }
3838
+ async function addRoutes(graph, services) {
3839
+ const jsParser = makeJsParser2();
3840
+ const pyParser = makePyParser2();
3841
+ const goParser = makeGoParser2();
3842
+ let nodesAdded = 0;
3843
+ let edgesAdded = 0;
3844
+ for (const service of services) {
3845
+ const deps = {
3846
+ ...service.pkg.dependencies ?? {},
3847
+ ...service.pkg.devDependencies ?? {}
3848
+ };
3849
+ const hasExpress = deps["express"] !== void 0;
3850
+ const hasFastify = deps["fastify"] !== void 0;
3851
+ const hasHono = deps["hono"] !== void 0;
3852
+ const hasNext = deps["next"] !== void 0;
3853
+ const hasNestjs = deps["@nestjs/core"] !== void 0;
3854
+ const hasFastapi = deps["fastapi"] !== void 0;
3855
+ const hasFlask = deps["flask"] !== void 0;
3161
3856
  const hasDjango = deps["django"] !== void 0;
3162
3857
  const hasGin = deps["github.com/gin-gonic/gin"] !== void 0;
3163
3858
  if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasNestjs && !hasFastapi && !hasFlask && !hasDjango && !hasGin)
3164
3859
  continue;
3165
3860
  const files = await loadSourceFiles(service.dir);
3861
+ const mountPrefixes = hasExpress ? await expressMountPrefixes(files, service.dir, await loadTsPathConfig(service.dir)) : /* @__PURE__ */ new Map();
3166
3862
  for (const file of files) {
3167
3863
  if (isTestPath(file.path)) continue;
3168
- const ext = import_node_path7.default.extname(file.path);
3864
+ const ext = import_node_path8.default.extname(file.path);
3169
3865
  const isPy = ext === ".py";
3170
3866
  const isGo = ext === ".go";
3171
3867
  if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy && !isGo) continue;
3172
- const relFile = toPosix(import_node_path7.default.relative(service.dir, file.path));
3868
+ const relFile = toPosix(import_node_path8.default.relative(service.dir, file.path));
3173
3869
  let routes;
3174
3870
  try {
3175
3871
  if (isGo) {
@@ -3191,16 +3887,18 @@ async function addRoutes(graph, services) {
3191
3887
  continue;
3192
3888
  }
3193
3889
  if (routes.length === 0) continue;
3890
+ const mountPrefix = mountPrefixes.get(relFile);
3194
3891
  for (const route of routes) {
3195
- const rid = (0, import_types5.routeId)(service.pkg.name, route.method, route.pathTemplate);
3892
+ const pathTemplate = mountPrefix ? canonicalizeTemplate(mountPrefix + route.pathTemplate) : route.pathTemplate;
3893
+ const rid = (0, import_types6.routeId)(service.pkg.name, route.method, pathTemplate);
3196
3894
  if (!graph.hasNode(rid)) {
3197
3895
  const node = {
3198
3896
  id: rid,
3199
- type: import_types5.NodeType.RouteNode,
3200
- name: `${route.method} ${route.pathTemplate}`,
3897
+ type: import_types6.NodeType.RouteNode,
3898
+ name: `${route.method} ${pathTemplate}`,
3201
3899
  service: service.pkg.name,
3202
3900
  method: route.method,
3203
- pathTemplate: route.pathTemplate,
3901
+ pathTemplate,
3204
3902
  path: relFile,
3205
3903
  line: route.line,
3206
3904
  framework: route.framework,
@@ -3209,15 +3907,15 @@ async function addRoutes(graph, services) {
3209
3907
  graph.addNode(rid, node);
3210
3908
  nodesAdded++;
3211
3909
  }
3212
- const containsId = (0, import_types5.extractedEdgeId)(service.node.id, rid, import_types5.EdgeType.CONTAINS);
3910
+ const containsId = (0, import_types6.extractedEdgeId)(service.node.id, rid, import_types6.EdgeType.CONTAINS);
3213
3911
  if (!graph.hasEdge(containsId)) {
3214
3912
  const edge = {
3215
3913
  id: containsId,
3216
3914
  source: service.node.id,
3217
3915
  target: rid,
3218
- type: import_types5.EdgeType.CONTAINS,
3219
- provenance: import_types5.Provenance.EXTRACTED,
3220
- confidence: (0, import_types5.confidenceForExtracted)("structural"),
3916
+ type: import_types6.EdgeType.CONTAINS,
3917
+ provenance: import_types6.Provenance.EXTRACTED,
3918
+ confidence: (0, import_types6.confidenceForExtracted)("structural"),
3221
3919
  evidence: {
3222
3920
  file: relFile,
3223
3921
  line: route.line,
@@ -3235,7 +3933,7 @@ async function addRoutes(graph, services) {
3235
3933
 
3236
3934
  // src/columns.ts
3237
3935
  init_cjs_shims();
3238
- var import_types6 = require("@neat.is/types");
3936
+ var import_types7 = require("@neat.is/types");
3239
3937
  var OBSERVED_COLUMN_CONFIDENCE = 0.9;
3240
3938
  function normalizeProvenances(provenances) {
3241
3939
  return [...new Set(provenances)].sort();
@@ -3263,10 +3961,10 @@ function foldColumns(existing, names, provenance, confidence) {
3263
3961
  return out;
3264
3962
  }
3265
3963
  function columnIsDeclared(col) {
3266
- return col.provenances.includes(import_types6.Provenance.EXTRACTED);
3964
+ return col.provenances.includes(import_types7.Provenance.EXTRACTED);
3267
3965
  }
3268
3966
  function columnIsObserved(col) {
3269
- return col.provenances.includes(import_types6.Provenance.OBSERVED);
3967
+ return col.provenances.includes(import_types7.Provenance.OBSERVED);
3270
3968
  }
3271
3969
 
3272
3970
  // src/ingest.ts
@@ -3489,7 +4187,7 @@ function languageForExt(relPath) {
3489
4187
  function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
3490
4188
  let p = toPosix2(filepath).replace(/^file:\/\//, "");
3491
4189
  if (scanPath && scanPath.length > 0) {
3492
- const absRoot = toPosix2(import_node_path8.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
4190
+ const absRoot = toPosix2(import_node_path9.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
3493
4191
  const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
3494
4192
  if (p.startsWith(anchor)) return p.slice(anchor.length);
3495
4193
  }
@@ -3517,10 +4215,10 @@ function resolveDistToSrc(absFilepath, line) {
3517
4215
  entry2 = null;
3518
4216
  const mapPath = `${absFilepath}.map`;
3519
4217
  try {
3520
- if ((0, import_node_fs7.existsSync)(mapPath)) {
3521
- const raw = JSON.parse((0, import_node_fs7.readFileSync)(mapPath, "utf8"));
4218
+ if ((0, import_node_fs8.existsSync)(mapPath)) {
4219
+ const raw = JSON.parse((0, import_node_fs8.readFileSync)(mapPath, "utf8"));
3522
4220
  const consumer = new sourceMapJs.SourceMapConsumer(raw);
3523
- entry2 = { consumer, dir: import_node_path8.default.dirname(mapPath) };
4221
+ entry2 = { consumer, dir: import_node_path9.default.dirname(mapPath) };
3524
4222
  }
3525
4223
  } catch {
3526
4224
  entry2 = null;
@@ -3535,7 +4233,7 @@ function resolveDistToSrc(absFilepath, line) {
3535
4233
  });
3536
4234
  if (!pos || !pos.source) return null;
3537
4235
  const root = entry2.consumer.sourceRoot ?? "";
3538
- const resolved = import_node_path8.default.resolve(entry2.dir, root, pos.source);
4236
+ const resolved = import_node_path9.default.resolve(entry2.dir, root, pos.source);
3539
4237
  return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
3540
4238
  } catch {
3541
4239
  return null;
@@ -3568,11 +4266,11 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
3568
4266
  };
3569
4267
  }
3570
4268
  function reconcileObservedRelPath(graph, serviceName, relPath) {
3571
- if (graph.hasNode((0, import_types7.fileId)(serviceName, relPath))) return relPath;
4269
+ if (graph.hasNode((0, import_types8.fileId)(serviceName, relPath))) return relPath;
3572
4270
  let best = null;
3573
4271
  graph.forEachNode((_id, attrs) => {
3574
4272
  const a = attrs;
3575
- if (a.type !== import_types7.NodeType.FileNode || a.service !== serviceName) return;
4273
+ if (a.type !== import_types8.NodeType.FileNode || a.service !== serviceName) return;
3576
4274
  if (a.discoveredVia === "otel") return;
3577
4275
  const p = a.path;
3578
4276
  if (!p) return;
@@ -3584,14 +4282,14 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
3584
4282
  }
3585
4283
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3586
4284
  const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
3587
- const canonicalService = svcAttrs && svcAttrs.type === import_types7.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
4285
+ const canonicalService = svcAttrs && svcAttrs.type === import_types8.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
3588
4286
  const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
3589
- const fileNodeId = (0, import_types7.fileId)(canonicalService, relPath);
4287
+ const fileNodeId = (0, import_types8.fileId)(canonicalService, relPath);
3590
4288
  if (!graph.hasNode(fileNodeId)) {
3591
4289
  const language = languageForExt(relPath);
3592
4290
  const node = {
3593
4291
  id: fileNodeId,
3594
- type: import_types7.NodeType.FileNode,
4292
+ type: import_types8.NodeType.FileNode,
3595
4293
  service: canonicalService,
3596
4294
  path: relPath,
3597
4295
  ...language ? { language } : {},
@@ -3600,14 +4298,14 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3600
4298
  };
3601
4299
  graph.addNode(fileNodeId, node);
3602
4300
  }
3603
- const containsId = makeObservedEdgeId(import_types7.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
4301
+ const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
3604
4302
  if (!graph.hasEdge(containsId)) {
3605
4303
  const edge = {
3606
4304
  id: containsId,
3607
4305
  source: serviceNodeId,
3608
4306
  target: fileNodeId,
3609
- type: import_types7.EdgeType.CONTAINS,
3610
- provenance: import_types7.Provenance.OBSERVED
4307
+ type: import_types8.EdgeType.CONTAINS,
4308
+ provenance: import_types8.Provenance.OBSERVED
3611
4309
  };
3612
4310
  graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3613
4311
  }
@@ -3631,11 +4329,11 @@ function pickContainingSymbol(candidates, fn) {
3631
4329
  return [...candidates].sort(bySpan)[0].id;
3632
4330
  }
3633
4331
  function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line) {
3634
- const sid = (0, import_types7.symbolId)(service, relPath, fn);
4332
+ const sid = (0, import_types8.symbolId)(service, relPath, fn);
3635
4333
  if (!graph.hasNode(sid)) {
3636
4334
  const node = {
3637
4335
  id: sid,
3638
- type: import_types7.NodeType.SymbolNode,
4336
+ type: import_types8.NodeType.SymbolNode,
3639
4337
  kind: "function",
3640
4338
  qualname: fn,
3641
4339
  span: { startLine: line, endLine: line },
@@ -3645,14 +4343,14 @@ function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line)
3645
4343
  };
3646
4344
  graph.addNode(sid, node);
3647
4345
  }
3648
- const containsId = makeObservedEdgeId(import_types7.EdgeType.CONTAINS, fileNodeId, sid);
4346
+ const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, fileNodeId, sid);
3649
4347
  if (!graph.hasEdge(containsId)) {
3650
4348
  const edge = {
3651
4349
  id: containsId,
3652
4350
  source: fileNodeId,
3653
4351
  target: sid,
3654
- type: import_types7.EdgeType.CONTAINS,
3655
- provenance: import_types7.Provenance.OBSERVED
4352
+ type: import_types8.EdgeType.CONTAINS,
4353
+ provenance: import_types8.Provenance.OBSERVED
3656
4354
  };
3657
4355
  graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
3658
4356
  }
@@ -3664,9 +4362,9 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
3664
4362
  let sawSymbol = false;
3665
4363
  const candidates = [];
3666
4364
  graph.forEachOutboundEdge(fileNodeId, (_edge, edgeAttrs, _source, target) => {
3667
- if (edgeAttrs.type !== import_types7.EdgeType.CONTAINS) return;
4365
+ if (edgeAttrs.type !== import_types8.EdgeType.CONTAINS) return;
3668
4366
  const t = graph.getNodeAttributes(target);
3669
- if (t.type !== import_types7.NodeType.SymbolNode) return;
4367
+ if (t.type !== import_types8.NodeType.SymbolNode) return;
3670
4368
  sawSymbol = true;
3671
4369
  if (line >= t.span.startLine && line <= t.span.endLine) {
3672
4370
  candidates.push({ id: target, symbol: t });
@@ -3679,17 +4377,17 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
3679
4377
  return fileNodeId;
3680
4378
  }
3681
4379
  function makeObservedEdgeId(type, source, target) {
3682
- return (0, import_types7.observedEdgeId)(source, target, type);
4380
+ return (0, import_types8.observedEdgeId)(source, target, type);
3683
4381
  }
3684
4382
  function makeInferredEdgeId(type, source, target) {
3685
- return (0, import_types7.inferredEdgeId)(source, target, type);
4383
+ return (0, import_types8.inferredEdgeId)(source, target, type);
3686
4384
  }
3687
4385
  var INFERRED_CONFIDENCE = 0.6;
3688
4386
  var STITCH_MAX_DEPTH = 2;
3689
4387
  var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
3690
- import_types7.EdgeType.CALLS,
3691
- import_types7.EdgeType.CONNECTS_TO,
3692
- import_types7.EdgeType.DEPENDS_ON
4388
+ import_types8.EdgeType.CALLS,
4389
+ import_types8.EdgeType.CONNECTS_TO,
4390
+ import_types8.EdgeType.DEPENDS_ON
3693
4391
  ]);
3694
4392
  var WIRE_SPAN_KIND_CLIENT = 3;
3695
4393
  var WIRE_SPAN_KIND_PRODUCER = 4;
@@ -3705,11 +4403,11 @@ function spanServesGraphqlOperation(kind) {
3705
4403
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3706
4404
  }
3707
4405
  function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
3708
- const id = (0, import_types7.graphqlOperationId)(serviceName, operationType, operationName);
4406
+ const id = (0, import_types8.graphqlOperationId)(serviceName, operationType, operationName);
3709
4407
  if (graph.hasNode(id)) return id;
3710
4408
  const node = {
3711
4409
  id,
3712
- type: import_types7.NodeType.GraphQLOperationNode,
4410
+ type: import_types8.NodeType.GraphQLOperationNode,
3713
4411
  name: operationName,
3714
4412
  service: serviceName,
3715
4413
  operationType: operationType.toLowerCase(),
@@ -3723,11 +4421,11 @@ function spanServesGrpcMethod(kind) {
3723
4421
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3724
4422
  }
3725
4423
  function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
3726
- const id = (0, import_types7.grpcMethodId)(rpcService, rpcMethod);
4424
+ const id = (0, import_types8.grpcMethodId)(rpcService, rpcMethod);
3727
4425
  if (graph.hasNode(id)) return id;
3728
4426
  const node = {
3729
4427
  id,
3730
- type: import_types7.NodeType.GrpcMethodNode,
4428
+ type: import_types8.NodeType.GrpcMethodNode,
3731
4429
  name: `${rpcService}/${rpcMethod}`,
3732
4430
  rpcService,
3733
4431
  rpcMethod,
@@ -3740,11 +4438,11 @@ function spanServesWebsocketChannel(kind) {
3740
4438
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3741
4439
  }
3742
4440
  function ensureWebsocketChannelNode(graph, serviceName, channel) {
3743
- const id = (0, import_types7.websocketChannelId)(serviceName, channel);
4441
+ const id = (0, import_types8.websocketChannelId)(serviceName, channel);
3744
4442
  if (graph.hasNode(id)) return id;
3745
4443
  const node = {
3746
4444
  id,
3747
- type: import_types7.NodeType.WebSocketChannelNode,
4445
+ type: import_types8.NodeType.WebSocketChannelNode,
3748
4446
  name: channel,
3749
4447
  service: serviceName,
3750
4448
  channel,
@@ -3757,11 +4455,11 @@ function messagingDestinationKind(system) {
3757
4455
  return `${system}-topic`;
3758
4456
  }
3759
4457
  function ensureMessagingDestinationNode(graph, system, destination) {
3760
- const id = (0, import_types7.infraId)(messagingDestinationKind(system), destination);
4458
+ const id = (0, import_types8.infraId)(messagingDestinationKind(system), destination);
3761
4459
  if (graph.hasNode(id)) return id;
3762
4460
  const node = {
3763
4461
  id,
3764
- type: import_types7.NodeType.InfraNode,
4462
+ type: import_types8.NodeType.InfraNode,
3765
4463
  name: destination,
3766
4464
  provider: "self",
3767
4465
  kind: messagingDestinationKind(system)
@@ -3805,9 +4503,9 @@ function lookupParentSpan(traceId, parentSpanId, now) {
3805
4503
  };
3806
4504
  }
3807
4505
  function resolveServiceId(graph, host, env) {
3808
- const envTagged = (0, import_types7.serviceId)(host, env);
4506
+ const envTagged = (0, import_types8.serviceId)(host, env);
3809
4507
  if (graph.hasNode(envTagged)) return envTagged;
3810
- const envLess = (0, import_types7.serviceId)(host);
4508
+ const envLess = (0, import_types8.serviceId)(host);
3811
4509
  if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
3812
4510
  let sameEnv = null;
3813
4511
  let envLessMatch = null;
@@ -3815,7 +4513,7 @@ function resolveServiceId(graph, host, env) {
3815
4513
  graph.forEachNode((id, attrs) => {
3816
4514
  if (sameEnv) return;
3817
4515
  const a = attrs;
3818
- if (a.type !== import_types7.NodeType.ServiceNode) return;
4516
+ if (a.type !== import_types8.NodeType.ServiceNode) return;
3819
4517
  const matchesByName = a.name === host;
3820
4518
  const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
3821
4519
  if (!matchesByName && !matchesByAlias) return;
@@ -3830,14 +4528,14 @@ function resolveServiceId(graph, host, env) {
3830
4528
  return sameEnv ?? envLessMatch ?? anyMatch;
3831
4529
  }
3832
4530
  function frontierIdFor(host) {
3833
- return (0, import_types7.frontierId)(host);
4531
+ return (0, import_types8.frontierId)(host);
3834
4532
  }
3835
4533
  function ensureServiceNode(graph, serviceName, env) {
3836
- const id = (0, import_types7.serviceId)(serviceName, env);
4534
+ const id = (0, import_types8.serviceId)(serviceName, env);
3837
4535
  if (graph.hasNode(id)) return id;
3838
4536
  const wanted = serviceName.toLowerCase();
3839
4537
  const extractedId = graph.findNode((_nid, attrs) => {
3840
- if (attrs.type !== import_types7.NodeType.ServiceNode) return false;
4538
+ if (attrs.type !== import_types8.NodeType.ServiceNode) return false;
3841
4539
  const svc = attrs;
3842
4540
  if (svc.discoveredVia === "otel") return false;
3843
4541
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
@@ -3845,7 +4543,7 @@ function ensureServiceNode(graph, serviceName, env) {
3845
4543
  if (extractedId) return extractedId;
3846
4544
  const node = {
3847
4545
  id,
3848
- type: import_types7.NodeType.ServiceNode,
4546
+ type: import_types8.NodeType.ServiceNode,
3849
4547
  name: serviceName,
3850
4548
  language: "unknown",
3851
4549
  discoveredVia: "otel",
@@ -3855,11 +4553,11 @@ function ensureServiceNode(graph, serviceName, env) {
3855
4553
  return id;
3856
4554
  }
3857
4555
  function ensureInfraNode(graph, kind, name, provider) {
3858
- const id = (0, import_types7.infraId)(kind, name);
4556
+ const id = (0, import_types8.infraId)(kind, name);
3859
4557
  if (graph.hasNode(id)) return id;
3860
4558
  const node = {
3861
4559
  id,
3862
- type: import_types7.NodeType.InfraNode,
4560
+ type: import_types8.NodeType.InfraNode,
3863
4561
  name,
3864
4562
  provider,
3865
4563
  kind
@@ -3871,7 +4569,7 @@ var COLUMN_BEARING_INFRA_KINDS = /* @__PURE__ */ new Set(["sql-table", "supabase
3871
4569
  function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
3872
4570
  if (!columns || columns.length === 0 || !graph.hasNode(tableNodeId)) return;
3873
4571
  const node = graph.getNodeAttributes(tableNodeId);
3874
- if (node.type !== import_types7.NodeType.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
4572
+ if (node.type !== import_types8.NodeType.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
3875
4573
  return;
3876
4574
  }
3877
4575
  graph.replaceNodeAttributes(tableNodeId, {
@@ -3880,14 +4578,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
3880
4578
  });
3881
4579
  }
3882
4580
  function mergeObservedColumns(graph, tableNodeId, columns) {
3883
- mergeColumnsAt(graph, tableNodeId, columns, import_types7.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
4581
+ mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
3884
4582
  }
3885
4583
  function ensureDatabaseNode(graph, host, engine) {
3886
- const id = (0, import_types7.databaseId)(host);
4584
+ const id = (0, import_types8.databaseId)(host);
3887
4585
  if (graph.hasNode(id)) return id;
3888
4586
  const node = {
3889
4587
  id,
3890
- type: import_types7.NodeType.DatabaseNode,
4588
+ type: import_types8.NodeType.DatabaseNode,
3891
4589
  name: host,
3892
4590
  engine,
3893
4591
  engineVersion: "unknown",
@@ -3899,11 +4597,11 @@ function ensureDatabaseNode(graph, host, engine) {
3899
4597
  return id;
3900
4598
  }
3901
4599
  function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
3902
- const id = (0, import_types7.localDatabaseId)(serviceName, name);
4600
+ const id = (0, import_types8.localDatabaseId)(serviceName, name);
3903
4601
  if (graph.hasNode(id)) return id;
3904
4602
  const node = {
3905
4603
  id,
3906
- type: import_types7.NodeType.DatabaseNode,
4604
+ type: import_types8.NodeType.DatabaseNode,
3907
4605
  name,
3908
4606
  engine,
3909
4607
  engineVersion: "unknown",
@@ -3918,17 +4616,17 @@ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
3918
4616
  const sources = [serviceNodeId];
3919
4617
  for (const edgeId of graph.outboundEdges(serviceNodeId)) {
3920
4618
  const e = graph.getEdgeAttributes(edgeId);
3921
- if (e.type === import_types7.EdgeType.CONTAINS) sources.push(e.target);
4619
+ if (e.type === import_types8.EdgeType.CONTAINS) sources.push(e.target);
3922
4620
  }
3923
4621
  const matches = /* @__PURE__ */ new Set();
3924
4622
  for (const src of sources) {
3925
4623
  if (!graph.hasNode(src)) continue;
3926
4624
  for (const edgeId of graph.outboundEdges(src)) {
3927
4625
  const edge = graph.getEdgeAttributes(edgeId);
3928
- if (edge.type !== import_types7.EdgeType.CONNECTS_TO || edge.provenance !== import_types7.Provenance.EXTRACTED) continue;
4626
+ if (edge.type !== import_types8.EdgeType.CONNECTS_TO || edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
3929
4627
  if (!graph.hasNode(edge.target)) continue;
3930
4628
  const target = graph.getNodeAttributes(edge.target);
3931
- if (target.type !== import_types7.NodeType.DatabaseNode || target.engine !== engine) continue;
4629
+ if (target.type !== import_types8.NodeType.DatabaseNode || target.engine !== engine) continue;
3932
4630
  matches.add(edge.target);
3933
4631
  }
3934
4632
  }
@@ -3943,7 +4641,7 @@ function ensureFrontierNode(graph, host, ts) {
3943
4641
  }
3944
4642
  const node = {
3945
4643
  id,
3946
- type: import_types7.NodeType.FrontierNode,
4644
+ type: import_types8.NodeType.FrontierNode,
3947
4645
  name: host,
3948
4646
  host,
3949
4647
  firstObserved: ts,
@@ -3967,11 +4665,11 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
3967
4665
  };
3968
4666
  const updated = {
3969
4667
  ...existing,
3970
- provenance: import_types7.Provenance.OBSERVED,
4668
+ provenance: import_types8.Provenance.OBSERVED,
3971
4669
  lastObserved: ts,
3972
4670
  callCount: newSpanCount,
3973
4671
  signal: newSignal,
3974
- confidence: (0, import_types7.confidenceForObservedSignal)(newSignal),
4672
+ confidence: (0, import_types8.confidenceForObservedSignal)(newSignal),
3975
4673
  grain
3976
4674
  // backfills legacy edges that predate ADR-142
3977
4675
  };
@@ -3988,8 +4686,8 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
3988
4686
  source,
3989
4687
  target,
3990
4688
  type,
3991
- provenance: import_types7.Provenance.OBSERVED,
3992
- confidence: (0, import_types7.confidenceForObservedSignal)(signal),
4689
+ provenance: import_types8.Provenance.OBSERVED,
4690
+ confidence: (0, import_types8.confidenceForObservedSignal)(signal),
3993
4691
  lastObserved: ts,
3994
4692
  callCount: 1,
3995
4693
  signal,
@@ -4011,9 +4709,9 @@ function stitchTrace(graph, sourceServiceId, ts) {
4011
4709
  const outbound = graph.outboundEdges(nodeId);
4012
4710
  for (const edgeId of outbound) {
4013
4711
  const edge = graph.getEdgeAttributes(edgeId);
4014
- if (edge.provenance !== import_types7.Provenance.EXTRACTED) continue;
4712
+ if (edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
4015
4713
  if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
4016
- if (graph.hasEdge((0, import_types7.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
4714
+ if (graph.hasEdge((0, import_types8.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
4017
4715
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
4018
4716
  if (!visited.has(edge.target)) {
4019
4717
  visited.add(edge.target);
@@ -4035,23 +4733,23 @@ function upsertInferredEdge(graph, type, source, target, ts) {
4035
4733
  source,
4036
4734
  target,
4037
4735
  type,
4038
- provenance: import_types7.Provenance.INFERRED,
4736
+ provenance: import_types8.Provenance.INFERRED,
4039
4737
  confidence: INFERRED_CONFIDENCE,
4040
4738
  lastObserved: ts
4041
4739
  };
4042
4740
  graph.addEdgeWithKey(id, source, target, edge);
4043
4741
  }
4044
4742
  async function appendErrorEvent(ctx, ev) {
4045
- await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
4046
- await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
4743
+ await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(ctx.errorsPath), { recursive: true });
4744
+ await import_node_fs8.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
4047
4745
  }
4048
4746
  function incidentAffectedNode(span, graph, scanPath) {
4049
- const sid = (0, import_types7.serviceId)(span.service, span.env);
4747
+ const sid = (0, import_types8.serviceId)(span.service, span.env);
4050
4748
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
4051
4749
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
4052
4750
  if (callSite) {
4053
4751
  const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
4054
- return (0, import_types7.fileId)(span.service, relPath);
4752
+ return (0, import_types8.fileId)(span.service, relPath);
4055
4753
  }
4056
4754
  return sid;
4057
4755
  }
@@ -4084,8 +4782,8 @@ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
4084
4782
  return async (span) => {
4085
4783
  const ev = buildErrorEventForReceiver(span, graph, scanPath);
4086
4784
  if (!ev) return;
4087
- await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
4088
- await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4785
+ await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
4786
+ await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4089
4787
  };
4090
4788
  }
4091
4789
  async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
@@ -4176,7 +4874,7 @@ function findRouteNodeByHttpRoute(graph, serviceName, method, httpRoute) {
4176
4874
  graph.forEachNode((id, attrs) => {
4177
4875
  if (found) return;
4178
4876
  const a = attrs;
4179
- if (a.type !== import_types7.NodeType.RouteNode || a.service !== serviceName) return;
4877
+ if (a.type !== import_types8.NodeType.RouteNode || a.service !== serviceName) return;
4180
4878
  if (m && a.method !== "ALL" && a.method !== m) return;
4181
4879
  if (normalizePathTemplate(a.pathTemplate) === target) found = id;
4182
4880
  });
@@ -4207,7 +4905,7 @@ async function handleSpan(ctx, span) {
4207
4905
  let targetId;
4208
4906
  if (host) {
4209
4907
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
4210
- targetId = (0, import_types7.databaseId)(host);
4908
+ targetId = (0, import_types8.databaseId)(host);
4211
4909
  } else {
4212
4910
  const declared = findDeclaredDatabaseForService(ctx.graph, sourceId, span.dbSystem);
4213
4911
  if (declared) {
@@ -4224,7 +4922,7 @@ async function handleSpan(ctx, span) {
4224
4922
  }
4225
4923
  const result = upsertObservedEdge(
4226
4924
  ctx.graph,
4227
- import_types7.EdgeType.CONNECTS_TO,
4925
+ import_types8.EdgeType.CONNECTS_TO,
4228
4926
  observedSource(),
4229
4927
  targetId,
4230
4928
  ts,
@@ -4236,7 +4934,7 @@ async function handleSpan(ctx, span) {
4236
4934
  const collectionId = ensureInfraNode(ctx.graph, "mongodb-collection", span.dbCollection, "self");
4237
4935
  upsertObservedEdge(
4238
4936
  ctx.graph,
4239
- import_types7.EdgeType.CALLS,
4937
+ import_types8.EdgeType.CALLS,
4240
4938
  observedSource(),
4241
4939
  collectionId,
4242
4940
  ts,
@@ -4248,7 +4946,7 @@ async function handleSpan(ctx, span) {
4248
4946
  const tableId = ensureInfraNode(ctx.graph, "sql-table", span.dbTable, "self");
4249
4947
  upsertObservedEdge(
4250
4948
  ctx.graph,
4251
- import_types7.EdgeType.CALLS,
4949
+ import_types8.EdgeType.CALLS,
4252
4950
  observedSource(),
4253
4951
  tableId,
4254
4952
  ts,
@@ -4264,7 +4962,7 @@ async function handleSpan(ctx, span) {
4264
4962
  span.messagingSystem,
4265
4963
  span.messagingDestination
4266
4964
  );
4267
- const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types7.EdgeType.CONSUMES_FROM : import_types7.EdgeType.PUBLISHES_TO;
4965
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types8.EdgeType.CONSUMES_FROM : import_types8.EdgeType.PUBLISHES_TO;
4268
4966
  const result = upsertObservedEdge(
4269
4967
  ctx.graph,
4270
4968
  edgeType,
@@ -4284,7 +4982,7 @@ async function handleSpan(ctx, span) {
4284
4982
  );
4285
4983
  const result = upsertObservedEdge(
4286
4984
  ctx.graph,
4287
- import_types7.EdgeType.CONTAINS,
4985
+ import_types8.EdgeType.CONTAINS,
4288
4986
  observedSource(),
4289
4987
  targetId,
4290
4988
  ts,
@@ -4296,7 +4994,7 @@ async function handleSpan(ctx, span) {
4296
4994
  const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
4297
4995
  const result = upsertObservedEdge(
4298
4996
  ctx.graph,
4299
- import_types7.EdgeType.CONTAINS,
4997
+ import_types8.EdgeType.CONTAINS,
4300
4998
  observedSource(),
4301
4999
  targetId,
4302
5000
  ts,
@@ -4312,7 +5010,7 @@ async function handleSpan(ctx, span) {
4312
5010
  );
4313
5011
  const result = upsertObservedEdge(
4314
5012
  ctx.graph,
4315
- import_types7.EdgeType.CONNECTS_TO,
5013
+ import_types8.EdgeType.CONNECTS_TO,
4316
5014
  observedSource(),
4317
5015
  targetId,
4318
5016
  ts,
@@ -4328,7 +5026,7 @@ async function handleSpan(ctx, span) {
4328
5026
  if (targetId && targetId !== sourceId) {
4329
5027
  upsertObservedEdge(
4330
5028
  ctx.graph,
4331
- import_types7.EdgeType.CALLS,
5029
+ import_types8.EdgeType.CALLS,
4332
5030
  observedSource(),
4333
5031
  targetId,
4334
5032
  ts,
@@ -4341,7 +5039,7 @@ async function handleSpan(ctx, span) {
4341
5039
  const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
4342
5040
  upsertObservedEdge(
4343
5041
  ctx.graph,
4344
- import_types7.EdgeType.CALLS,
5042
+ import_types8.EdgeType.CALLS,
4345
5043
  observedSource(),
4346
5044
  frontierNodeId,
4347
5045
  ts,
@@ -4367,7 +5065,7 @@ async function handleSpan(ctx, span) {
4367
5065
  } : void 0;
4368
5066
  upsertObservedEdge(
4369
5067
  ctx.graph,
4370
- import_types7.EdgeType.CALLS,
5068
+ import_types8.EdgeType.CALLS,
4371
5069
  fallbackSource,
4372
5070
  sourceId,
4373
5071
  ts,
@@ -4386,7 +5084,7 @@ async function handleSpan(ctx, span) {
4386
5084
  );
4387
5085
  if (routeNodeId) {
4388
5086
  const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
4389
- upsertObservedEdge(ctx.graph, import_types7.EdgeType.CONTAINS, (0, import_types7.serviceId)(routeSvc), routeNodeId, ts, isError);
5087
+ upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
4390
5088
  }
4391
5089
  }
4392
5090
  if (span.statusCode === 2) {
@@ -4425,7 +5123,7 @@ function promoteFrontierNodes(graph, opts = {}) {
4425
5123
  const aliasIndex = /* @__PURE__ */ new Map();
4426
5124
  graph.forEachNode((id, attrs) => {
4427
5125
  const a = attrs;
4428
- if (a.type !== import_types7.NodeType.ServiceNode) return;
5126
+ if (a.type !== import_types8.NodeType.ServiceNode) return;
4429
5127
  aliasIndex.set(a.name, id);
4430
5128
  if (a.aliases) {
4431
5129
  for (const alias of a.aliases) aliasIndex.set(alias, id);
@@ -4434,7 +5132,7 @@ function promoteFrontierNodes(graph, opts = {}) {
4434
5132
  const toPromote = [];
4435
5133
  graph.forEachNode((id, attrs) => {
4436
5134
  const a = attrs;
4437
- if (a.type !== import_types7.NodeType.FrontierNode) return;
5135
+ if (a.type !== import_types8.NodeType.FrontierNode) return;
4438
5136
  const target = aliasIndex.get(a.host);
4439
5137
  if (!target) return;
4440
5138
  if (target === id) return;
@@ -4468,7 +5166,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId7) {
4468
5166
  }
4469
5167
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
4470
5168
  graph.dropEdge(oldEdgeId);
4471
- 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);
5169
+ 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);
4472
5170
  if (graph.hasEdge(newId)) {
4473
5171
  const existing = graph.getEdgeAttributes(newId);
4474
5172
  const merged = {
@@ -4502,12 +5200,12 @@ async function markStaleEdges(graph, options = {}) {
4502
5200
  const project = options.project ?? DEFAULT_PROJECT;
4503
5201
  graph.forEachEdge((id, attrs) => {
4504
5202
  const e = attrs;
4505
- if (e.provenance !== import_types7.Provenance.OBSERVED) return;
5203
+ if (e.provenance !== import_types8.Provenance.OBSERVED) return;
4506
5204
  if (!e.lastObserved) return;
4507
5205
  const threshold = thresholdForEdgeType(e.type, thresholds);
4508
5206
  const age = now - new Date(e.lastObserved).getTime();
4509
5207
  if (age > threshold) {
4510
- const updated = { ...e, provenance: import_types7.Provenance.STALE, confidence: 0.3 };
5208
+ const updated = { ...e, provenance: import_types8.Provenance.STALE, confidence: 0.3 };
4511
5209
  graph.replaceEdgeAttributes(id, updated);
4512
5210
  events.push({
4513
5211
  edgeId: id,
@@ -4524,8 +5222,8 @@ async function markStaleEdges(graph, options = {}) {
4524
5222
  project,
4525
5223
  payload: {
4526
5224
  edgeId: id,
4527
- from: import_types7.Provenance.OBSERVED,
4528
- to: import_types7.Provenance.STALE
5225
+ from: import_types8.Provenance.OBSERVED,
5226
+ to: import_types8.Provenance.STALE
4529
5227
  }
4530
5228
  });
4531
5229
  }
@@ -4536,13 +5234,13 @@ async function markStaleEdges(graph, options = {}) {
4536
5234
  return { count: events.length, events };
4537
5235
  }
4538
5236
  async function appendStaleEvents(staleEventsPath, events) {
4539
- await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(staleEventsPath), { recursive: true });
5237
+ await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(staleEventsPath), { recursive: true });
4540
5238
  const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
4541
- await import_node_fs7.promises.appendFile(staleEventsPath, lines, "utf8");
5239
+ await import_node_fs8.promises.appendFile(staleEventsPath, lines, "utf8");
4542
5240
  }
4543
5241
  async function readStaleEvents(staleEventsPath) {
4544
5242
  try {
4545
- const raw = await import_node_fs7.promises.readFile(staleEventsPath, "utf8");
5243
+ const raw = await import_node_fs8.promises.readFile(staleEventsPath, "utf8");
4546
5244
  return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
4547
5245
  } catch (err) {
4548
5246
  if (err.code === "ENOENT") return [];
@@ -4576,7 +5274,7 @@ function startStalenessLoop(graph, options = {}) {
4576
5274
  }
4577
5275
  async function readErrorEvents(errorsPath) {
4578
5276
  try {
4579
- const raw = await import_node_fs7.promises.readFile(errorsPath, "utf8");
5277
+ const raw = await import_node_fs8.promises.readFile(errorsPath, "utf8");
4580
5278
  const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
4581
5279
  return dedupeIncidents(events);
4582
5280
  } catch (err) {
@@ -4634,7 +5332,7 @@ function mergeSnapshot(graph, snapshot) {
4634
5332
  const validEdges = [];
4635
5333
  for (const node of incomingNodes) {
4636
5334
  if (node.attributes === void 0) continue;
4637
- const parsed = import_types7.GraphNodeSchema.safeParse(node.attributes);
5335
+ const parsed = import_types8.GraphNodeSchema.safeParse(node.attributes);
4638
5336
  if (!parsed.success) {
4639
5337
  issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
4640
5338
  continue;
@@ -4643,7 +5341,7 @@ function mergeSnapshot(graph, snapshot) {
4643
5341
  }
4644
5342
  for (const edge of incomingEdges) {
4645
5343
  if (edge.attributes === void 0) continue;
4646
- const parsed = import_types7.GraphEdgeSchema.safeParse(edge.attributes);
5344
+ const parsed = import_types8.GraphEdgeSchema.safeParse(edge.attributes);
4647
5345
  if (!parsed.success) {
4648
5346
  const label = edge.key ?? `${edge.source}->${edge.target}`;
4649
5347
  issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
@@ -4673,16 +5371,16 @@ function mergeSnapshot(graph, snapshot) {
4673
5371
 
4674
5372
  // src/extract/services.ts
4675
5373
  init_cjs_shims();
4676
- var import_node_fs11 = require("fs");
4677
- var import_node_path12 = __toESM(require("path"), 1);
5374
+ var import_node_fs12 = require("fs");
5375
+ var import_node_path13 = __toESM(require("path"), 1);
4678
5376
  var import_ignore = __toESM(require("ignore"), 1);
4679
5377
  var import_minimatch2 = require("minimatch");
4680
- var import_types9 = require("@neat.is/types");
5378
+ var import_types10 = require("@neat.is/types");
4681
5379
 
4682
5380
  // src/extract/python.ts
4683
5381
  init_cjs_shims();
4684
- var import_node_fs8 = require("fs");
4685
- var import_node_path9 = __toESM(require("path"), 1);
5382
+ var import_node_fs9 = require("fs");
5383
+ var import_node_path10 = __toESM(require("path"), 1);
4686
5384
  var import_smol_toml = require("smol-toml");
4687
5385
  var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
4688
5386
  function parseRequirementsTxt(content) {
@@ -4715,25 +5413,25 @@ function depsFromPyProject(pyproject) {
4715
5413
  return out;
4716
5414
  }
4717
5415
  async function discoverPythonService(serviceDir) {
4718
- const pyprojectPath = import_node_path9.default.join(serviceDir, "pyproject.toml");
4719
- const requirementsPath = import_node_path9.default.join(serviceDir, "requirements.txt");
4720
- const setupPath = import_node_path9.default.join(serviceDir, "setup.py");
5416
+ const pyprojectPath = import_node_path10.default.join(serviceDir, "pyproject.toml");
5417
+ const requirementsPath = import_node_path10.default.join(serviceDir, "requirements.txt");
5418
+ const setupPath = import_node_path10.default.join(serviceDir, "setup.py");
4721
5419
  const hasPyproject = await exists(pyprojectPath);
4722
5420
  const hasRequirements = await exists(requirementsPath);
4723
5421
  const hasSetup = await exists(setupPath);
4724
5422
  if (!hasPyproject && !hasRequirements && !hasSetup) return null;
4725
- let name = import_node_path9.default.basename(serviceDir);
5423
+ let name = import_node_path10.default.basename(serviceDir);
4726
5424
  let version;
4727
5425
  const dependencies = {};
4728
5426
  if (hasPyproject) {
4729
- const raw = await import_node_fs8.promises.readFile(pyprojectPath, "utf8");
5427
+ const raw = await import_node_fs9.promises.readFile(pyprojectPath, "utf8");
4730
5428
  const pyproject = (0, import_smol_toml.parse)(raw);
4731
5429
  name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
4732
5430
  version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
4733
5431
  Object.assign(dependencies, depsFromPyProject(pyproject));
4734
5432
  }
4735
5433
  if (hasRequirements) {
4736
- const raw = await import_node_fs8.promises.readFile(requirementsPath, "utf8");
5434
+ const raw = await import_node_fs9.promises.readFile(requirementsPath, "utf8");
4737
5435
  Object.assign(dependencies, parseRequirementsTxt(raw));
4738
5436
  }
4739
5437
  return { name, version, dependencies };
@@ -4748,9 +5446,9 @@ function pythonToPackage(service) {
4748
5446
 
4749
5447
  // src/extract/go.ts
4750
5448
  init_cjs_shims();
4751
- var import_node_fs9 = require("fs");
4752
- var import_node_path10 = __toESM(require("path"), 1);
4753
- var import_types8 = require("@neat.is/types");
5449
+ var import_node_fs10 = require("fs");
5450
+ var import_node_path11 = __toESM(require("path"), 1);
5451
+ var import_types9 = require("@neat.is/types");
4754
5452
  function parseGoMod(source) {
4755
5453
  const module2 = source.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
4756
5454
  if (!module2) return null;
@@ -4769,7 +5467,7 @@ function parseGoMod(source) {
4769
5467
  async function discoverGoService(scanPath, dir) {
4770
5468
  let raw;
4771
5469
  try {
4772
- raw = await import_node_fs9.promises.readFile(import_node_path10.default.join(dir, "go.mod"), "utf8");
5470
+ raw = await import_node_fs10.promises.readFile(import_node_path11.default.join(dir, "go.mod"), "utf8");
4773
5471
  } catch {
4774
5472
  return null;
4775
5473
  }
@@ -4778,12 +5476,12 @@ async function discoverGoService(scanPath, dir) {
4778
5476
  const name = mod.module.split("/").filter(Boolean).pop() ?? mod.module;
4779
5477
  const pkg = { name, dependencies: mod.dependencies };
4780
5478
  const node = {
4781
- id: (0, import_types8.serviceId)(name),
4782
- type: import_types8.NodeType.ServiceNode,
5479
+ id: (0, import_types9.serviceId)(name),
5480
+ type: import_types9.NodeType.ServiceNode,
4783
5481
  name,
4784
5482
  language: "go",
4785
5483
  dependencies: mod.dependencies,
4786
- repoPath: import_node_path10.default.relative(scanPath, dir),
5484
+ repoPath: import_node_path11.default.relative(scanPath, dir),
4787
5485
  ...mod.dependencies["github.com/gin-gonic/gin"] ? { framework: "gin" } : {}
4788
5486
  };
4789
5487
  return { pkg, dir, node };
@@ -4791,17 +5489,17 @@ async function discoverGoService(scanPath, dir) {
4791
5489
 
4792
5490
  // src/extract/owners.ts
4793
5491
  init_cjs_shims();
4794
- var import_node_fs10 = require("fs");
4795
- var import_node_path11 = __toESM(require("path"), 1);
5492
+ var import_node_fs11 = require("fs");
5493
+ var import_node_path12 = __toESM(require("path"), 1);
4796
5494
  var import_minimatch = require("minimatch");
4797
5495
  async function loadCodeowners(scanPath) {
4798
5496
  const candidates = [
4799
- import_node_path11.default.join(scanPath, "CODEOWNERS"),
4800
- import_node_path11.default.join(scanPath, ".github", "CODEOWNERS")
5497
+ import_node_path12.default.join(scanPath, "CODEOWNERS"),
5498
+ import_node_path12.default.join(scanPath, ".github", "CODEOWNERS")
4801
5499
  ];
4802
5500
  for (const file of candidates) {
4803
5501
  if (await exists(file)) {
4804
- const raw = await import_node_fs10.promises.readFile(file, "utf8");
5502
+ const raw = await import_node_fs11.promises.readFile(file, "utf8");
4805
5503
  return parseCodeowners(raw);
4806
5504
  }
4807
5505
  }
@@ -4819,7 +5517,7 @@ function parseCodeowners(raw) {
4819
5517
  return { rules };
4820
5518
  }
4821
5519
  function matchOwner(file, repoPath) {
4822
- const normalized = repoPath.split(import_node_path11.default.sep).join("/");
5520
+ const normalized = repoPath.split(import_node_path12.default.sep).join("/");
4823
5521
  for (const rule of file.rules) {
4824
5522
  if (matchesPattern(rule.pattern, normalized)) return rule.owners;
4825
5523
  }
@@ -4835,7 +5533,7 @@ function matchesPattern(rawPattern, repoPath) {
4835
5533
  return false;
4836
5534
  }
4837
5535
  async function readPackageJsonAuthor(serviceDir) {
4838
- const pkgPath = import_node_path11.default.join(serviceDir, "package.json");
5536
+ const pkgPath = import_node_path12.default.join(serviceDir, "package.json");
4839
5537
  if (!await exists(pkgPath)) return null;
4840
5538
  try {
4841
5539
  const pkg = await readJson(pkgPath);
@@ -4847,985 +5545,601 @@ async function readPackageJsonAuthor(serviceDir) {
4847
5545
  return null;
4848
5546
  }
4849
5547
  }
4850
- async function computeServiceOwner(codeowners, repoPath, serviceDir) {
4851
- if (codeowners && repoPath !== void 0) {
4852
- const owner = matchOwner(codeowners, repoPath);
4853
- if (owner) return owner;
4854
- }
4855
- const author = await readPackageJsonAuthor(serviceDir);
4856
- return author ?? void 0;
4857
- }
4858
-
4859
- // src/extract/services.ts
4860
- var DEFAULT_SCAN_DEPTH = 5;
4861
- function parseScanDepth() {
4862
- const raw = process.env.NEAT_SCAN_DEPTH;
4863
- if (!raw) return DEFAULT_SCAN_DEPTH;
4864
- const n = Number.parseInt(raw, 10);
4865
- return Number.isFinite(n) && n >= 0 ? n : DEFAULT_SCAN_DEPTH;
4866
- }
4867
- function workspaceGlobs(pkg) {
4868
- const ws = pkg.workspaces;
4869
- if (!ws) return null;
4870
- if (Array.isArray(ws)) return ws.length > 0 ? ws : null;
4871
- if (Array.isArray(ws.packages)) return ws.packages.length > 0 ? ws.packages : null;
4872
- return null;
4873
- }
4874
- async function hasPythonManifest(dir) {
4875
- 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"));
4876
- }
4877
- async function hasGoManifest(dir) {
4878
- return exists(import_node_path12.default.join(dir, "go.mod"));
4879
- }
4880
- async function loadGitignore(scanPath) {
4881
- const gitignorePath = import_node_path12.default.join(scanPath, ".gitignore");
4882
- if (!await exists(gitignorePath)) return null;
4883
- const raw = await import_node_fs11.promises.readFile(gitignorePath, "utf8");
4884
- return (0, import_ignore.default)().add(raw);
4885
- }
4886
- async function walkDirs(start, scanPath, options, visit) {
4887
- async function recurse(current, depth) {
4888
- if (depth > options.maxDepth) return;
4889
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
4890
- for (const entry2 of entries) {
4891
- if (!entry2.isDirectory()) continue;
4892
- if (IGNORED_DIRS.has(entry2.name)) continue;
4893
- const child = import_node_path12.default.join(current, entry2.name);
4894
- if (options.ig) {
4895
- const rel = import_node_path12.default.relative(scanPath, child).split(import_node_path12.default.sep).join("/");
4896
- if (rel && options.ig.ignores(rel + "/")) continue;
4897
- }
4898
- if (await isPythonVenvDir(child)) continue;
4899
- await visit(child);
4900
- await recurse(child, depth + 1);
4901
- }
4902
- }
4903
- await recurse(start, 0);
4904
- }
4905
- async function expandWorkspaceGlobs(scanPath, globs) {
4906
- const found = /* @__PURE__ */ new Set();
4907
- const scanDepth = parseScanDepth();
4908
- for (const raw of globs) {
4909
- const pattern = raw.replace(/^\.\//, "");
4910
- if (!pattern.includes("*")) {
4911
- const candidate = import_node_path12.default.join(scanPath, pattern);
4912
- if (await exists(import_node_path12.default.join(candidate, "package.json"))) found.add(candidate);
4913
- continue;
4914
- }
4915
- const segments = pattern.split("/");
4916
- const staticSegments = [];
4917
- for (const seg of segments) {
4918
- if (seg.includes("*")) break;
4919
- staticSegments.push(seg);
4920
- }
4921
- const start = import_node_path12.default.join(scanPath, ...staticSegments);
4922
- if (!await exists(start)) continue;
4923
- const hasDoubleStar = pattern.includes("**");
4924
- const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
4925
- await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
4926
- const rel = import_node_path12.default.relative(scanPath, dir).split(import_node_path12.default.sep).join("/");
4927
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path12.default.join(dir, "package.json"))) {
4928
- found.add(dir);
4929
- }
4930
- });
4931
- }
4932
- return [...found];
4933
- }
4934
- function detectJsFramework(pkg) {
4935
- const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4936
- if (deps["next"] !== void 0) return "next";
4937
- if (deps["remix"] !== void 0) return "remix";
4938
- for (const k of Object.keys(deps)) {
4939
- if (k.startsWith("@remix-run/")) return "remix";
4940
- }
4941
- if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
4942
- if (deps["nuxt"] !== void 0) return "nuxt";
4943
- if (deps["astro"] !== void 0) return "astro";
4944
- if (deps["@nestjs/core"] !== void 0) return "nestjs";
4945
- return void 0;
4946
- }
4947
- async function detectJsServiceLanguage(dir, pkg) {
4948
- const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4949
- if (deps["typescript"] !== void 0) return "typescript";
4950
- const entries = await import_node_fs11.promises.readdir(dir).catch(() => []);
4951
- if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
4952
- return "javascript";
4953
- }
4954
- async function discoverNodeService(scanPath, dir) {
4955
- const pkgPath = import_node_path12.default.join(dir, "package.json");
4956
- if (!await exists(pkgPath)) return null;
4957
- let pkg;
4958
- try {
4959
- pkg = await readJson(pkgPath);
4960
- } catch (err) {
4961
- recordExtractionError("services", import_node_path12.default.relative(scanPath, pkgPath), err);
4962
- return null;
4963
- }
4964
- if (!pkg.name) return null;
4965
- const framework = detectJsFramework(pkg);
4966
- const language = await detectJsServiceLanguage(dir, pkg);
4967
- const node = {
4968
- id: (0, import_types9.serviceId)(pkg.name),
4969
- type: import_types9.NodeType.ServiceNode,
4970
- name: pkg.name,
4971
- language,
4972
- version: pkg.version,
4973
- dependencies: pkg.dependencies ?? {},
4974
- repoPath: import_node_path12.default.relative(scanPath, dir),
4975
- ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
4976
- ...framework ? { framework } : {}
4977
- };
4978
- return { pkg, dir, node };
4979
- }
4980
- async function discoverPyService(scanPath, dir) {
4981
- const py = await discoverPythonService(dir);
4982
- if (!py) return null;
4983
- const pkg = pythonToPackage(py);
4984
- const node = {
4985
- id: (0, import_types9.serviceId)(py.name),
4986
- type: import_types9.NodeType.ServiceNode,
4987
- name: py.name,
4988
- language: "python",
4989
- version: py.version,
4990
- dependencies: py.dependencies,
4991
- repoPath: import_node_path12.default.relative(scanPath, dir)
4992
- };
4993
- return { pkg, dir, node };
4994
- }
4995
- async function discoverServices(scanPath) {
4996
- const rootPkgPath = import_node_path12.default.join(scanPath, "package.json");
4997
- let rootPkg = null;
4998
- if (await exists(rootPkgPath)) {
4999
- try {
5000
- rootPkg = await readJson(rootPkgPath);
5001
- } catch (err) {
5002
- recordExtractionError(
5003
- "services workspaces",
5004
- import_node_path12.default.relative(scanPath, rootPkgPath),
5005
- err
5006
- );
5007
- }
5008
- }
5009
- const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
5010
- const candidateDirs = [];
5011
- if (wsGlobs) {
5012
- candidateDirs.push(...await expandWorkspaceGlobs(scanPath, wsGlobs));
5013
- } else {
5014
- if (rootPkg && rootPkg.name) {
5015
- candidateDirs.push(scanPath);
5016
- } else if (await hasPythonManifest(scanPath) || await hasGoManifest(scanPath)) {
5017
- candidateDirs.push(scanPath);
5018
- }
5019
- const ig = await loadGitignore(scanPath);
5020
- await walkDirs(
5021
- scanPath,
5022
- scanPath,
5023
- { maxDepth: parseScanDepth(), ig },
5024
- async (dir) => {
5025
- if (await exists(import_node_path12.default.join(dir, "package.json"))) {
5026
- candidateDirs.push(dir);
5027
- } else if (await hasPythonManifest(dir) || await hasGoManifest(dir)) {
5028
- candidateDirs.push(dir);
5029
- }
5030
- }
5031
- );
5032
- }
5033
- candidateDirs.sort();
5034
- const seen = /* @__PURE__ */ new Map();
5035
- const out = [];
5036
- for (const dir of candidateDirs) {
5037
- const service = await discoverNodeService(scanPath, dir) ?? await discoverPyService(scanPath, dir) ?? await discoverGoService(scanPath, dir);
5038
- if (!service) continue;
5039
- const existingDir = seen.get(service.node.name);
5040
- if (existingDir !== void 0) {
5041
- const a = import_node_path12.default.relative(scanPath, existingDir) || ".";
5042
- const b = import_node_path12.default.relative(scanPath, dir) || ".";
5043
- console.warn(
5044
- `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
5045
- );
5046
- continue;
5047
- }
5048
- seen.set(service.node.name, dir);
5049
- out.push(service);
5050
- }
5051
- const codeowners = await loadCodeowners(scanPath);
5052
- for (const service of out) {
5053
- const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
5054
- if (owner !== void 0) service.node.owner = owner;
5055
- }
5056
- return out;
5057
- }
5058
- function addServiceNodes(graph, services) {
5059
- let nodesAdded = 0;
5060
- for (const service of services) {
5061
- if (!graph.hasNode(service.node.id)) {
5062
- graph.addNode(service.node.id, { ...service.node, discoveredVia: "static" });
5063
- nodesAdded++;
5064
- continue;
5065
- }
5066
- const existing = graph.getNodeAttributes(service.node.id);
5067
- const mergedDiscoveredVia = existing.discoveredVia === "otel" ? "merged" : "static";
5068
- graph.replaceNodeAttributes(service.node.id, {
5069
- ...existing,
5070
- ...service.node,
5071
- discoveredVia: mergedDiscoveredVia
5072
- });
5073
- }
5074
- return nodesAdded;
5075
- }
5076
-
5077
- // src/extract/aliases.ts
5078
- init_cjs_shims();
5079
- var import_node_path13 = __toESM(require("path"), 1);
5080
- var import_node_fs12 = require("fs");
5081
- var import_yaml2 = require("yaml");
5082
- var import_types10 = require("@neat.is/types");
5083
- var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
5084
- "Service",
5085
- "Deployment",
5086
- "StatefulSet",
5087
- "DaemonSet"
5088
- ]);
5089
- function addAliases(graph, serviceId7, candidates) {
5090
- if (!graph.hasNode(serviceId7)) return;
5091
- const node = graph.getNodeAttributes(serviceId7);
5092
- if (node.type !== import_types10.NodeType.ServiceNode) return;
5093
- const set = new Set(node.aliases ?? []);
5094
- for (const c of candidates) {
5095
- if (!c) continue;
5096
- if (c === node.name) continue;
5097
- set.add(c);
5098
- }
5099
- if (set.size === 0) return;
5100
- const updated = { ...node, aliases: [...set].sort() };
5101
- graph.replaceNodeAttributes(serviceId7, updated);
5102
- }
5103
- function indexServicesByName(services) {
5104
- const map = /* @__PURE__ */ new Map();
5105
- for (const s of services) {
5106
- map.set(s.node.name, s.node.id);
5107
- map.set(import_node_path13.default.basename(s.dir), s.node.id);
5108
- }
5109
- return map;
5110
- }
5111
- async function collectComposeAliases(graph, scanPath, serviceIndex) {
5112
- let composePath = null;
5113
- for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5114
- const abs = import_node_path13.default.join(scanPath, name);
5115
- if (await exists(abs)) {
5116
- composePath = abs;
5117
- break;
5118
- }
5119
- }
5120
- if (!composePath) return;
5121
- let compose;
5122
- try {
5123
- compose = await readYaml(composePath);
5124
- } catch (err) {
5125
- recordExtractionError(
5126
- "aliases compose",
5127
- import_node_path13.default.relative(scanPath, composePath),
5128
- err
5129
- );
5130
- return;
5131
- }
5132
- if (!compose?.services) return;
5133
- for (const [composeName, svc] of Object.entries(compose.services)) {
5134
- const serviceId7 = serviceIndex.get(composeName);
5135
- if (!serviceId7) continue;
5136
- const aliases = /* @__PURE__ */ new Set([composeName]);
5137
- if (svc.container_name) aliases.add(svc.container_name);
5138
- if (svc.hostname) aliases.add(svc.hostname);
5139
- addAliases(graph, serviceId7, aliases);
5140
- }
5141
- }
5142
- var LABEL_KEYS = /* @__PURE__ */ new Set([
5143
- "service",
5144
- "service.name",
5145
- "app",
5146
- "app.name",
5147
- "com.docker.compose.service",
5148
- "org.opencontainers.image.title"
5149
- ]);
5150
- function parseDockerfileLabels(content) {
5151
- const out = [];
5152
- const lineRegex = /^\s*label\s+(.+)$/i;
5153
- for (const raw of content.split("\n")) {
5154
- const m = lineRegex.exec(raw);
5155
- if (!m) continue;
5156
- const rest = m[1];
5157
- const pairRegex = /([\w.-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g;
5158
- let pair;
5159
- while ((pair = pairRegex.exec(rest)) !== null) {
5160
- const key = pair[1].toLowerCase();
5161
- if (!LABEL_KEYS.has(key)) continue;
5162
- const value = pair[3] ?? pair[4] ?? pair[5] ?? "";
5163
- if (value) out.push(value);
5164
- }
5165
- }
5166
- return out;
5167
- }
5168
- async function collectDockerfileAliases(graph, services) {
5169
- for (const service of services) {
5170
- const dockerfilePath = import_node_path13.default.join(service.dir, "Dockerfile");
5171
- if (!await exists(dockerfilePath)) continue;
5172
- let content;
5173
- try {
5174
- content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
5175
- } catch (err) {
5176
- recordExtractionError("aliases dockerfile", dockerfilePath, err);
5177
- continue;
5178
- }
5179
- const aliases = parseDockerfileLabels(content);
5180
- if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
5181
- }
5182
- }
5183
- async function walkYamlFiles(start, depth = 0, max = 5) {
5184
- if (depth > max) return [];
5185
- const out = [];
5186
- const entries = await import_node_fs12.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5187
- for (const entry2 of entries) {
5188
- if (entry2.isDirectory()) {
5189
- if (IGNORED_DIRS.has(entry2.name)) continue;
5190
- const child = import_node_path13.default.join(start, entry2.name);
5191
- if (await isPythonVenvDir(child)) continue;
5192
- out.push(...await walkYamlFiles(child, depth + 1, max));
5193
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path13.default.extname(entry2.name))) {
5194
- out.push(import_node_path13.default.join(start, entry2.name));
5195
- }
5196
- }
5197
- return out;
5198
- }
5199
- function k8sHostnames(name, namespace) {
5200
- const ns = namespace ?? "default";
5201
- return [
5202
- name,
5203
- `${name}.${ns}`,
5204
- `${name}.${ns}.svc`,
5205
- `${name}.${ns}.svc.cluster.local`
5206
- ];
5207
- }
5208
- function k8sServiceTarget(doc, byName) {
5209
- const selector = doc.spec?.selector;
5210
- const selectorApp = selector?.app ?? selector?.matchLabels?.app;
5211
- if (selectorApp && byName.has(selectorApp)) return byName.get(selectorApp);
5212
- const labelApp = doc.metadata?.labels?.app;
5213
- if (labelApp && byName.has(labelApp)) return byName.get(labelApp);
5214
- const metaName = doc.metadata?.name;
5215
- if (metaName && byName.has(metaName)) return byName.get(metaName);
5216
- return null;
5217
- }
5218
- async function collectK8sAliases(graph, scanPath, serviceIndex) {
5219
- const files = await walkYamlFiles(scanPath);
5220
- for (const file of files) {
5221
- const content = await import_node_fs12.promises.readFile(file, "utf8");
5222
- let docs;
5223
- try {
5224
- docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
5225
- } catch {
5226
- continue;
5227
- }
5228
- for (const doc of docs) {
5229
- if (!doc?.kind || !doc.metadata?.name) continue;
5230
- if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
5231
- const target = k8sServiceTarget(doc, serviceIndex);
5232
- if (!target) continue;
5233
- addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
5234
- }
5235
- }
5236
- }
5237
- async function addServiceAliases(graph, scanPath, services) {
5238
- const byName = indexServicesByName(services);
5239
- await collectComposeAliases(graph, scanPath, byName);
5240
- await collectDockerfileAliases(graph, services);
5241
- await collectK8sAliases(graph, scanPath, byName);
5242
- }
5243
-
5244
- // src/extract/files.ts
5245
- init_cjs_shims();
5246
- var import_node_path14 = __toESM(require("path"), 1);
5247
- async function addFiles(graph, services) {
5248
- let nodesAdded = 0;
5249
- let edgesAdded = 0;
5250
- for (const service of services) {
5251
- const filePaths = await walkSourceFiles(service.dir);
5252
- for (const filePath of filePaths) {
5253
- const relPath = toPosix(import_node_path14.default.relative(service.dir, filePath));
5254
- const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
5255
- graph,
5256
- service.pkg.name,
5257
- service.node.id,
5258
- relPath
5259
- );
5260
- nodesAdded += n;
5261
- edgesAdded += e;
5262
- }
5263
- }
5264
- return { nodesAdded, edgesAdded };
5265
- }
5266
-
5267
- // src/extract/symbols.ts
5268
- init_cjs_shims();
5269
- var import_node_path15 = __toESM(require("path"), 1);
5270
- var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5271
- var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5272
- var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
5273
- var import_types11 = require("@neat.is/types");
5274
- var PARSE_CHUNK2 = 16384;
5275
- var GRAMMAR_BY_EXT = {
5276
- ".ts": import_tree_sitter_typescript.default.typescript,
5277
- ".tsx": import_tree_sitter_typescript.default.tsx,
5278
- ".js": import_tree_sitter_javascript2.default,
5279
- ".jsx": import_tree_sitter_javascript2.default,
5280
- ".mjs": import_tree_sitter_javascript2.default,
5281
- ".cjs": import_tree_sitter_javascript2.default
5282
- };
5283
- function parseSource2(parser, source) {
5284
- return parser.parse(
5285
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5286
- );
5287
- }
5288
- function methodName(node) {
5289
- const name = node.childForFieldName("name");
5290
- return name ? name.text : null;
5291
- }
5292
- function collectSymbolDefs(root) {
5293
- const out = [];
5294
- const push = (kind, qualname, node) => {
5295
- out.push({
5296
- kind,
5297
- qualname,
5298
- startLine: node.startPosition.row + 1,
5299
- endLine: node.endPosition.row + 1
5300
- });
5301
- };
5302
- const visit = (node, classCtx) => {
5303
- switch (node.type) {
5304
- case "function_declaration":
5305
- case "generator_function_declaration": {
5306
- const name = node.childForFieldName("name")?.text;
5307
- if (name) push("function", name, node);
5308
- break;
5309
- }
5310
- case "class_declaration":
5311
- case "abstract_class_declaration":
5312
- case "class": {
5313
- const name = node.childForFieldName("name")?.text;
5314
- if (name) push("class", name, node);
5315
- const body = node.childForFieldName("body");
5316
- if (body) {
5317
- for (let i = 0; i < body.namedChildCount; i++) {
5318
- const child = body.namedChild(i);
5319
- if (child) visit(child, name ?? classCtx);
5320
- }
5321
- }
5322
- return;
5323
- }
5324
- case "method_definition": {
5325
- const name = methodName(node);
5326
- if (name) {
5327
- const kind = name === "constructor" ? "constructor" : "method";
5328
- push(kind, classCtx ? `${classCtx}.${name}` : name, node);
5329
- }
5330
- break;
5331
- }
5332
- case "variable_declarator": {
5333
- const value = node.childForFieldName("value");
5334
- if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
5335
- const nameNode = node.childForFieldName("name");
5336
- if (nameNode && nameNode.type === "identifier") {
5337
- push("function", nameNode.text, node);
5338
- }
5339
- }
5340
- break;
5341
- }
5342
- }
5343
- for (let i = 0; i < node.namedChildCount; i++) {
5344
- const child = node.namedChild(i);
5345
- if (child) visit(child, classCtx);
5346
- }
5347
- };
5348
- visit(root, void 0);
5349
- return out;
5350
- }
5351
- function disambiguate(defs) {
5352
- const counts = /* @__PURE__ */ new Map();
5353
- for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
5354
- const seen = /* @__PURE__ */ new Map();
5355
- return defs.map((def) => {
5356
- if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
5357
- const ordinal = seen.get(def.qualname) ?? 0;
5358
- seen.set(def.qualname, ordinal + 1);
5359
- return { def, disambiguator: ordinal };
5360
- });
5361
- }
5362
- async function addSymbols(graph, services) {
5363
- const parsers = /* @__PURE__ */ new Map();
5364
- const parserForExt2 = (ext) => {
5365
- const grammar = GRAMMAR_BY_EXT[ext];
5366
- if (!grammar) return null;
5367
- let parser = parsers.get(ext);
5368
- if (!parser) {
5369
- parser = new import_tree_sitter2.default();
5370
- parser.setLanguage(grammar);
5371
- parsers.set(ext, parser);
5372
- }
5373
- return parser;
5374
- };
5375
- let nodesAdded = 0;
5376
- let edgesAdded = 0;
5377
- for (const service of services) {
5378
- const files = await loadSourceFiles(service.dir);
5379
- for (const file of files) {
5380
- const parser = parserForExt2(import_node_path15.default.extname(file.path));
5381
- if (!parser) continue;
5382
- const relPath = toPosix(import_node_path15.default.relative(service.dir, file.path));
5383
- let defs;
5384
- try {
5385
- const tree = parseSource2(parser, file.content);
5386
- defs = collectSymbolDefs(tree.rootNode);
5387
- } catch (err) {
5388
- recordExtractionError("symbol extraction", file.path, err);
5389
- continue;
5390
- }
5391
- if (defs.length === 0) continue;
5392
- const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5393
- graph,
5394
- service.pkg.name,
5395
- service.node.id,
5396
- relPath
5397
- );
5398
- nodesAdded += fn;
5399
- edgesAdded += fe;
5400
- for (const { def, disambiguator } of disambiguate(defs)) {
5401
- const sid = (0, import_types11.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
5402
- if (!graph.hasNode(sid)) {
5403
- const node = {
5404
- id: sid,
5405
- type: import_types11.NodeType.SymbolNode,
5406
- kind: def.kind,
5407
- qualname: def.qualname,
5408
- span: { startLine: def.startLine, endLine: def.endLine },
5409
- service: service.pkg.name,
5410
- relPath,
5411
- discoveredVia: "static"
5412
- };
5413
- graph.addNode(sid, node);
5414
- nodesAdded++;
5415
- }
5416
- const containsId = (0, import_types11.extractedEdgeId)(fileNodeId, sid, import_types11.EdgeType.CONTAINS);
5417
- if (!graph.hasEdge(containsId)) {
5418
- const edge = {
5419
- id: containsId,
5420
- source: fileNodeId,
5421
- target: sid,
5422
- type: import_types11.EdgeType.CONTAINS,
5423
- provenance: import_types11.Provenance.EXTRACTED,
5424
- confidence: (0, import_types11.confidenceForExtracted)("structural"),
5425
- evidence: {
5426
- file: relPath,
5427
- line: def.startLine,
5428
- snippet: snippet(file.content, def.startLine)
5429
- }
5430
- };
5431
- graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
5432
- edgesAdded++;
5433
- }
5434
- }
5435
- }
5548
+ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
5549
+ if (codeowners && repoPath !== void 0) {
5550
+ const owner = matchOwner(codeowners, repoPath);
5551
+ if (owner) return owner;
5436
5552
  }
5437
- return { nodesAdded, edgesAdded };
5553
+ const author = await readPackageJsonAuthor(serviceDir);
5554
+ return author ?? void 0;
5438
5555
  }
5439
5556
 
5440
- // src/extract/symbol-edges.ts
5441
- init_cjs_shims();
5442
- var import_node_path17 = __toESM(require("path"), 1);
5443
- var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
5444
- var import_types13 = require("@neat.is/types");
5445
-
5446
- // src/extract/imports.ts
5447
- init_cjs_shims();
5448
- var import_node_path16 = __toESM(require("path"), 1);
5449
- var import_node_fs13 = require("fs");
5450
- var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5451
- var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5452
- var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5453
- var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
5454
- var import_types12 = require("@neat.is/types");
5455
- var PARSE_CHUNK3 = 16384;
5456
- function parseSource3(parser, source) {
5457
- return parser.parse(
5458
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5459
- );
5557
+ // src/extract/services.ts
5558
+ var DEFAULT_SCAN_DEPTH = 5;
5559
+ function parseScanDepth() {
5560
+ const raw = process.env.NEAT_SCAN_DEPTH;
5561
+ if (!raw) return DEFAULT_SCAN_DEPTH;
5562
+ const n = Number.parseInt(raw, 10);
5563
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_SCAN_DEPTH;
5460
5564
  }
5461
- function makeJsParser2() {
5462
- const p = new import_tree_sitter3.default();
5463
- p.setLanguage(import_tree_sitter_javascript3.default);
5464
- return p;
5565
+ function workspaceGlobs(pkg) {
5566
+ const ws = pkg.workspaces;
5567
+ if (!ws) return null;
5568
+ if (Array.isArray(ws)) return ws.length > 0 ? ws : null;
5569
+ if (Array.isArray(ws.packages)) return ws.packages.length > 0 ? ws.packages : null;
5570
+ return null;
5465
5571
  }
5466
- function makePyParser2() {
5467
- const p = new import_tree_sitter3.default();
5468
- p.setLanguage(import_tree_sitter_python2.default);
5469
- return p;
5572
+ async function hasPythonManifest(dir) {
5573
+ return await exists(import_node_path13.default.join(dir, "pyproject.toml")) || await exists(import_node_path13.default.join(dir, "requirements.txt")) || await exists(import_node_path13.default.join(dir, "setup.py"));
5470
5574
  }
5471
- function makeGoParser2() {
5472
- const p = new import_tree_sitter3.default();
5473
- p.setLanguage(import_tree_sitter_go2.default);
5474
- return p;
5575
+ async function hasGoManifest(dir) {
5576
+ return exists(import_node_path13.default.join(dir, "go.mod"));
5475
5577
  }
5476
- function stringLiteralText(node) {
5477
- for (let i = 0; i < node.childCount; i++) {
5478
- const child = node.child(i);
5479
- if (child?.type === "string_fragment") return child.text;
5480
- }
5481
- const raw = node.text;
5482
- if (raw.length >= 2) return raw.slice(1, -1);
5483
- return raw.length === 0 ? null : "";
5578
+ async function loadGitignore(scanPath) {
5579
+ const gitignorePath = import_node_path13.default.join(scanPath, ".gitignore");
5580
+ if (!await exists(gitignorePath)) return null;
5581
+ const raw = await import_node_fs12.promises.readFile(gitignorePath, "utf8");
5582
+ return (0, import_ignore.default)().add(raw);
5484
5583
  }
5485
- function clipSnippet(text) {
5486
- const oneLine = text.split("\n")[0] ?? text;
5487
- return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
5584
+ async function walkDirs(start, scanPath, options, visit) {
5585
+ async function recurse(current, depth) {
5586
+ if (depth > options.maxDepth) return;
5587
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
5588
+ for (const entry2 of entries) {
5589
+ if (!entry2.isDirectory()) continue;
5590
+ if (IGNORED_DIRS.has(entry2.name)) continue;
5591
+ const child = import_node_path13.default.join(current, entry2.name);
5592
+ if (options.ig) {
5593
+ const rel = import_node_path13.default.relative(scanPath, child).split(import_node_path13.default.sep).join("/");
5594
+ if (rel && options.ig.ignores(rel + "/")) continue;
5595
+ }
5596
+ if (await isPythonVenvDir(child)) continue;
5597
+ await visit(child);
5598
+ await recurse(child, depth + 1);
5599
+ }
5600
+ }
5601
+ await recurse(start, 0);
5488
5602
  }
5489
- function collectGoImports(node, out) {
5490
- if (node.type === "import_spec") {
5491
- const pathNode = node.childForFieldName("path");
5492
- if (pathNode) {
5493
- const specifier = pathNode.text.replace(/^`|`$/g, "").replace(/^"|"$/g, "");
5494
- if (specifier) out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5603
+ async function expandWorkspaceGlobs(scanPath, globs) {
5604
+ const found = /* @__PURE__ */ new Set();
5605
+ const scanDepth = parseScanDepth();
5606
+ for (const raw of globs) {
5607
+ const pattern = raw.replace(/^\.\//, "");
5608
+ if (!pattern.includes("*")) {
5609
+ const candidate = import_node_path13.default.join(scanPath, pattern);
5610
+ if (await exists(import_node_path13.default.join(candidate, "package.json"))) found.add(candidate);
5611
+ continue;
5495
5612
  }
5496
- return;
5613
+ const segments = pattern.split("/");
5614
+ const staticSegments = [];
5615
+ for (const seg of segments) {
5616
+ if (seg.includes("*")) break;
5617
+ staticSegments.push(seg);
5618
+ }
5619
+ const start = import_node_path13.default.join(scanPath, ...staticSegments);
5620
+ if (!await exists(start)) continue;
5621
+ const hasDoubleStar = pattern.includes("**");
5622
+ const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
5623
+ await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
5624
+ const rel = import_node_path13.default.relative(scanPath, dir).split(import_node_path13.default.sep).join("/");
5625
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path13.default.join(dir, "package.json"))) {
5626
+ found.add(dir);
5627
+ }
5628
+ });
5497
5629
  }
5498
- for (let i = 0; i < node.namedChildCount; i++) {
5499
- const child = node.namedChild(i);
5500
- if (child) collectGoImports(child, out);
5630
+ return [...found];
5631
+ }
5632
+ function detectJsFramework(pkg) {
5633
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5634
+ if (deps["next"] !== void 0) return "next";
5635
+ if (deps["remix"] !== void 0) return "remix";
5636
+ for (const k of Object.keys(deps)) {
5637
+ if (k.startsWith("@remix-run/")) return "remix";
5501
5638
  }
5639
+ if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
5640
+ if (deps["nuxt"] !== void 0) return "nuxt";
5641
+ if (deps["astro"] !== void 0) return "astro";
5642
+ if (deps["@nestjs/core"] !== void 0) return "nestjs";
5643
+ return void 0;
5502
5644
  }
5503
- function collectJsImports(node, out) {
5504
- if (node.type === "import_statement") {
5505
- const source = node.childForFieldName("source");
5506
- if (source) {
5507
- const specifier = stringLiteralText(source);
5508
- if (specifier) {
5509
- out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5510
- }
5645
+ async function detectJsServiceLanguage(dir, pkg) {
5646
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5647
+ if (deps["typescript"] !== void 0) return "typescript";
5648
+ const entries = await import_node_fs12.promises.readdir(dir).catch(() => []);
5649
+ if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
5650
+ return "javascript";
5651
+ }
5652
+ async function discoverNodeService(scanPath, dir) {
5653
+ const pkgPath = import_node_path13.default.join(dir, "package.json");
5654
+ if (!await exists(pkgPath)) return null;
5655
+ let pkg;
5656
+ try {
5657
+ pkg = await readJson(pkgPath);
5658
+ } catch (err) {
5659
+ recordExtractionError("services", import_node_path13.default.relative(scanPath, pkgPath), err);
5660
+ return null;
5661
+ }
5662
+ if (!pkg.name) return null;
5663
+ const framework = detectJsFramework(pkg);
5664
+ const language = await detectJsServiceLanguage(dir, pkg);
5665
+ const node = {
5666
+ id: (0, import_types10.serviceId)(pkg.name),
5667
+ type: import_types10.NodeType.ServiceNode,
5668
+ name: pkg.name,
5669
+ language,
5670
+ version: pkg.version,
5671
+ dependencies: pkg.dependencies ?? {},
5672
+ repoPath: import_node_path13.default.relative(scanPath, dir),
5673
+ ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
5674
+ ...framework ? { framework } : {}
5675
+ };
5676
+ return { pkg, dir, node };
5677
+ }
5678
+ async function discoverPyService(scanPath, dir) {
5679
+ const py = await discoverPythonService(dir);
5680
+ if (!py) return null;
5681
+ const pkg = pythonToPackage(py);
5682
+ const node = {
5683
+ id: (0, import_types10.serviceId)(py.name),
5684
+ type: import_types10.NodeType.ServiceNode,
5685
+ name: py.name,
5686
+ language: "python",
5687
+ version: py.version,
5688
+ dependencies: py.dependencies,
5689
+ repoPath: import_node_path13.default.relative(scanPath, dir)
5690
+ };
5691
+ return { pkg, dir, node };
5692
+ }
5693
+ async function discoverServices(scanPath) {
5694
+ const rootPkgPath = import_node_path13.default.join(scanPath, "package.json");
5695
+ let rootPkg = null;
5696
+ if (await exists(rootPkgPath)) {
5697
+ try {
5698
+ rootPkg = await readJson(rootPkgPath);
5699
+ } catch (err) {
5700
+ recordExtractionError(
5701
+ "services workspaces",
5702
+ import_node_path13.default.relative(scanPath, rootPkgPath),
5703
+ err
5704
+ );
5511
5705
  }
5512
- return;
5513
5706
  }
5514
- if (node.type === "call_expression") {
5515
- const fn = node.childForFieldName("function");
5516
- if (fn?.type === "identifier" && fn.text === "require") {
5517
- const args = node.childForFieldName("arguments");
5518
- const firstArg = args?.namedChild(0);
5519
- if (firstArg?.type === "string") {
5520
- const specifier = stringLiteralText(firstArg);
5521
- if (specifier) {
5522
- out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5707
+ const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
5708
+ const candidateDirs = [];
5709
+ if (wsGlobs) {
5710
+ candidateDirs.push(...await expandWorkspaceGlobs(scanPath, wsGlobs));
5711
+ } else {
5712
+ if (rootPkg && rootPkg.name) {
5713
+ candidateDirs.push(scanPath);
5714
+ } else if (await hasPythonManifest(scanPath) || await hasGoManifest(scanPath)) {
5715
+ candidateDirs.push(scanPath);
5716
+ }
5717
+ const ig = await loadGitignore(scanPath);
5718
+ await walkDirs(
5719
+ scanPath,
5720
+ scanPath,
5721
+ { maxDepth: parseScanDepth(), ig },
5722
+ async (dir) => {
5723
+ if (await exists(import_node_path13.default.join(dir, "package.json"))) {
5724
+ candidateDirs.push(dir);
5725
+ } else if (await hasPythonManifest(dir) || await hasGoManifest(dir)) {
5726
+ candidateDirs.push(dir);
5523
5727
  }
5524
5728
  }
5729
+ );
5730
+ }
5731
+ candidateDirs.sort();
5732
+ const seen = /* @__PURE__ */ new Map();
5733
+ const out = [];
5734
+ for (const dir of candidateDirs) {
5735
+ const service = await discoverNodeService(scanPath, dir) ?? await discoverPyService(scanPath, dir) ?? await discoverGoService(scanPath, dir);
5736
+ if (!service) continue;
5737
+ const existingDir = seen.get(service.node.name);
5738
+ if (existingDir !== void 0) {
5739
+ const a = import_node_path13.default.relative(scanPath, existingDir) || ".";
5740
+ const b = import_node_path13.default.relative(scanPath, dir) || ".";
5741
+ console.warn(
5742
+ `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
5743
+ );
5744
+ continue;
5745
+ }
5746
+ seen.set(service.node.name, dir);
5747
+ out.push(service);
5748
+ }
5749
+ const codeowners = await loadCodeowners(scanPath);
5750
+ for (const service of out) {
5751
+ const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
5752
+ if (owner !== void 0) service.node.owner = owner;
5753
+ }
5754
+ return out;
5755
+ }
5756
+ function addServiceNodes(graph, services) {
5757
+ let nodesAdded = 0;
5758
+ for (const service of services) {
5759
+ if (!graph.hasNode(service.node.id)) {
5760
+ graph.addNode(service.node.id, { ...service.node, discoveredVia: "static" });
5761
+ nodesAdded++;
5762
+ continue;
5525
5763
  }
5764
+ const existing = graph.getNodeAttributes(service.node.id);
5765
+ const mergedDiscoveredVia = existing.discoveredVia === "otel" ? "merged" : "static";
5766
+ graph.replaceNodeAttributes(service.node.id, {
5767
+ ...existing,
5768
+ ...service.node,
5769
+ discoveredVia: mergedDiscoveredVia
5770
+ });
5771
+ }
5772
+ return nodesAdded;
5773
+ }
5774
+
5775
+ // src/extract/aliases.ts
5776
+ init_cjs_shims();
5777
+ var import_node_path14 = __toESM(require("path"), 1);
5778
+ var import_node_fs13 = require("fs");
5779
+ var import_yaml2 = require("yaml");
5780
+ var import_types11 = require("@neat.is/types");
5781
+ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
5782
+ "Service",
5783
+ "Deployment",
5784
+ "StatefulSet",
5785
+ "DaemonSet"
5786
+ ]);
5787
+ function addAliases(graph, serviceId7, candidates) {
5788
+ if (!graph.hasNode(serviceId7)) return;
5789
+ const node = graph.getNodeAttributes(serviceId7);
5790
+ if (node.type !== import_types11.NodeType.ServiceNode) return;
5791
+ const set = new Set(node.aliases ?? []);
5792
+ for (const c of candidates) {
5793
+ if (!c) continue;
5794
+ if (c === node.name) continue;
5795
+ set.add(c);
5526
5796
  }
5527
- for (let i = 0; i < node.namedChildCount; i++) {
5528
- const child = node.namedChild(i);
5529
- if (child) collectJsImports(child, out);
5797
+ if (set.size === 0) return;
5798
+ const updated = { ...node, aliases: [...set].sort() };
5799
+ graph.replaceNodeAttributes(serviceId7, updated);
5800
+ }
5801
+ function indexServicesByName(services) {
5802
+ const map = /* @__PURE__ */ new Map();
5803
+ for (const s of services) {
5804
+ map.set(s.node.name, s.node.id);
5805
+ map.set(import_node_path14.default.basename(s.dir), s.node.id);
5530
5806
  }
5807
+ return map;
5531
5808
  }
5532
- function collectImportedNames(node, out) {
5533
- if (node.type === "aliased_import") {
5534
- const nameNode = node.childForFieldName("name");
5535
- if (nameNode) out.push(nameNode.text);
5536
- return;
5809
+ async function collectComposeAliases(graph, scanPath, serviceIndex) {
5810
+ let composePath = null;
5811
+ for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5812
+ const abs = import_node_path14.default.join(scanPath, name);
5813
+ if (await exists(abs)) {
5814
+ composePath = abs;
5815
+ break;
5816
+ }
5537
5817
  }
5538
- if (node.type === "dotted_name") {
5539
- out.push(node.text);
5818
+ if (!composePath) return;
5819
+ let compose;
5820
+ try {
5821
+ compose = await readYaml(composePath);
5822
+ } catch (err) {
5823
+ recordExtractionError(
5824
+ "aliases compose",
5825
+ import_node_path14.default.relative(scanPath, composePath),
5826
+ err
5827
+ );
5540
5828
  return;
5541
5829
  }
5542
- for (let i = 0; i < node.namedChildCount; i++) {
5543
- const child = node.namedChild(i);
5544
- if (child) collectImportedNames(child, out);
5830
+ if (!compose?.services) return;
5831
+ for (const [composeName, svc] of Object.entries(compose.services)) {
5832
+ const serviceId7 = serviceIndex.get(composeName);
5833
+ if (!serviceId7) continue;
5834
+ const aliases = /* @__PURE__ */ new Set([composeName]);
5835
+ if (svc.container_name) aliases.add(svc.container_name);
5836
+ if (svc.hostname) aliases.add(svc.hostname);
5837
+ addAliases(graph, serviceId7, aliases);
5545
5838
  }
5546
5839
  }
5547
- function collectPyImports(node, out) {
5548
- if (node.type === "import_from_statement") {
5549
- let level = 0;
5550
- let modulePath = "";
5551
- const names = [];
5552
- let pastFrom = false;
5553
- let pastImport = false;
5554
- for (let i = 0; i < node.childCount; i++) {
5555
- const child = node.child(i);
5556
- if (!child) continue;
5557
- if (!pastFrom) {
5558
- if (child.type === "from") pastFrom = true;
5559
- continue;
5560
- }
5561
- if (!pastImport) {
5562
- if (child.type === "import") {
5563
- pastImport = true;
5564
- continue;
5565
- }
5566
- if (child.type === "relative_import") {
5567
- for (let j = 0; j < child.childCount; j++) {
5568
- const rc = child.child(j);
5569
- if (!rc) continue;
5570
- if (rc.type === "import_prefix") {
5571
- for (let k = 0; k < rc.childCount; k++) {
5572
- if (rc.child(k)?.type === ".") level++;
5573
- }
5574
- } else if (rc.type === "dotted_name") modulePath = rc.text;
5575
- }
5576
- } else if (child.type === "dotted_name") {
5577
- modulePath = child.text;
5578
- }
5579
- continue;
5580
- }
5581
- collectImportedNames(child, names);
5582
- }
5583
- if (level > 0 || modulePath) {
5584
- out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5840
+ var LABEL_KEYS = /* @__PURE__ */ new Set([
5841
+ "service",
5842
+ "service.name",
5843
+ "app",
5844
+ "app.name",
5845
+ "com.docker.compose.service",
5846
+ "org.opencontainers.image.title"
5847
+ ]);
5848
+ function parseDockerfileLabels(content) {
5849
+ const out = [];
5850
+ const lineRegex = /^\s*label\s+(.+)$/i;
5851
+ for (const raw of content.split("\n")) {
5852
+ const m = lineRegex.exec(raw);
5853
+ if (!m) continue;
5854
+ const rest = m[1];
5855
+ const pairRegex = /([\w.-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g;
5856
+ let pair;
5857
+ while ((pair = pairRegex.exec(rest)) !== null) {
5858
+ const key = pair[1].toLowerCase();
5859
+ if (!LABEL_KEYS.has(key)) continue;
5860
+ const value = pair[3] ?? pair[4] ?? pair[5] ?? "";
5861
+ if (value) out.push(value);
5585
5862
  }
5586
5863
  }
5587
- for (let i = 0; i < node.namedChildCount; i++) {
5588
- const child = node.namedChild(i);
5589
- if (child) collectPyImports(child, out);
5864
+ return out;
5865
+ }
5866
+ async function collectDockerfileAliases(graph, services) {
5867
+ for (const service of services) {
5868
+ const dockerfilePath = import_node_path14.default.join(service.dir, "Dockerfile");
5869
+ if (!await exists(dockerfilePath)) continue;
5870
+ let content;
5871
+ try {
5872
+ content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
5873
+ } catch (err) {
5874
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
5875
+ continue;
5876
+ }
5877
+ const aliases = parseDockerfileLabels(content);
5878
+ if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
5590
5879
  }
5591
5880
  }
5592
- async function fileExists(p) {
5593
- try {
5594
- await import_node_fs13.promises.access(p);
5595
- return true;
5596
- } catch {
5597
- return false;
5881
+ async function walkYamlFiles(start, depth = 0, max = 5) {
5882
+ if (depth > max) return [];
5883
+ const out = [];
5884
+ const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5885
+ for (const entry2 of entries) {
5886
+ if (entry2.isDirectory()) {
5887
+ if (IGNORED_DIRS.has(entry2.name)) continue;
5888
+ const child = import_node_path14.default.join(start, entry2.name);
5889
+ if (await isPythonVenvDir(child)) continue;
5890
+ out.push(...await walkYamlFiles(child, depth + 1, max));
5891
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path14.default.extname(entry2.name))) {
5892
+ out.push(import_node_path14.default.join(start, entry2.name));
5893
+ }
5598
5894
  }
5895
+ return out;
5599
5896
  }
5600
- function isWithinServiceDir(candidate, serviceDir) {
5601
- const rel = import_node_path16.default.relative(serviceDir, candidate);
5602
- return rel !== "" && !rel.startsWith("..") && !import_node_path16.default.isAbsolute(rel);
5897
+ function k8sHostnames(name, namespace) {
5898
+ const ns = namespace ?? "default";
5899
+ return [
5900
+ name,
5901
+ `${name}.${ns}`,
5902
+ `${name}.${ns}.svc`,
5903
+ `${name}.${ns}.svc.cluster.local`
5904
+ ];
5603
5905
  }
5604
- var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
5605
- var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
5606
- async function firstExistingCandidate(base, serviceDir) {
5607
- for (const ext of JS_EXTENSIONS) {
5608
- const candidate = base + ext;
5609
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5610
- return toPosix(import_node_path16.default.relative(serviceDir, candidate));
5906
+ function k8sServiceTarget(doc, byName) {
5907
+ const selector = doc.spec?.selector;
5908
+ const selectorApp = selector?.app ?? selector?.matchLabels?.app;
5909
+ if (selectorApp && byName.has(selectorApp)) return byName.get(selectorApp);
5910
+ const labelApp = doc.metadata?.labels?.app;
5911
+ if (labelApp && byName.has(labelApp)) return byName.get(labelApp);
5912
+ const metaName = doc.metadata?.name;
5913
+ if (metaName && byName.has(metaName)) return byName.get(metaName);
5914
+ return null;
5915
+ }
5916
+ async function collectK8sAliases(graph, scanPath, serviceIndex) {
5917
+ const files = await walkYamlFiles(scanPath);
5918
+ for (const file of files) {
5919
+ const content = await import_node_fs13.promises.readFile(file, "utf8");
5920
+ let docs;
5921
+ try {
5922
+ docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
5923
+ } catch {
5924
+ continue;
5611
5925
  }
5612
- }
5613
- for (const indexFile of JS_INDEX_FILES) {
5614
- const candidate = import_node_path16.default.join(base, indexFile);
5615
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5616
- return toPosix(import_node_path16.default.relative(serviceDir, candidate));
5926
+ for (const doc of docs) {
5927
+ if (!doc?.kind || !doc.metadata?.name) continue;
5928
+ if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
5929
+ const target = k8sServiceTarget(doc, serviceIndex);
5930
+ if (!target) continue;
5931
+ addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
5617
5932
  }
5618
5933
  }
5619
- return null;
5620
5934
  }
5621
- async function loadTsPathConfig(serviceDir) {
5622
- const tsconfigPath = import_node_path16.default.join(serviceDir, "tsconfig.json");
5623
- let raw;
5624
- try {
5625
- raw = await import_node_fs13.promises.readFile(tsconfigPath, "utf8");
5626
- } catch {
5627
- return null;
5628
- }
5629
- try {
5630
- const parsed = JSON.parse(raw);
5631
- const paths = parsed.compilerOptions?.paths;
5632
- if (!paths || Object.keys(paths).length === 0) return null;
5633
- const baseUrl = parsed.compilerOptions?.baseUrl;
5634
- return { paths, baseDir: baseUrl ? import_node_path16.default.resolve(serviceDir, baseUrl) : serviceDir };
5635
- } catch (err) {
5636
- recordExtractionError("import alias resolution", tsconfigPath, err);
5637
- return null;
5638
- }
5935
+ async function addServiceAliases(graph, scanPath, services) {
5936
+ const byName = indexServicesByName(services);
5937
+ await collectComposeAliases(graph, scanPath, byName);
5938
+ await collectDockerfileAliases(graph, services);
5939
+ await collectK8sAliases(graph, scanPath, byName);
5639
5940
  }
5640
- async function resolveTsAlias(specifier, config, serviceDir) {
5641
- for (const [pattern, targets] of Object.entries(config.paths)) {
5642
- let suffix = null;
5643
- if (pattern === specifier) {
5644
- suffix = "";
5645
- } else if (pattern.endsWith("/*")) {
5646
- const prefix = pattern.slice(0, -1);
5647
- if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
5648
- }
5649
- if (suffix === null) continue;
5650
- for (const target of targets) {
5651
- const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
5652
- const resolvedBase = import_node_path16.default.resolve(config.baseDir, targetBase, suffix);
5653
- const hit = await firstExistingCandidate(resolvedBase, serviceDir);
5654
- if (hit) return hit;
5655
- if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
5656
- return toPosix(import_node_path16.default.relative(serviceDir, resolvedBase));
5657
- }
5941
+
5942
+ // src/extract/files.ts
5943
+ init_cjs_shims();
5944
+ var import_node_path15 = __toESM(require("path"), 1);
5945
+ async function addFiles(graph, services) {
5946
+ let nodesAdded = 0;
5947
+ let edgesAdded = 0;
5948
+ for (const service of services) {
5949
+ const filePaths = await walkSourceFiles(service.dir);
5950
+ for (const filePath of filePaths) {
5951
+ const relPath = toPosix(import_node_path15.default.relative(service.dir, filePath));
5952
+ const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
5953
+ graph,
5954
+ service.pkg.name,
5955
+ service.node.id,
5956
+ relPath
5957
+ );
5958
+ nodesAdded += n;
5959
+ edgesAdded += e;
5658
5960
  }
5659
5961
  }
5660
- return null;
5962
+ return { nodesAdded, edgesAdded };
5963
+ }
5964
+
5965
+ // src/extract/symbols.ts
5966
+ init_cjs_shims();
5967
+ var import_node_path16 = __toESM(require("path"), 1);
5968
+ var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5969
+ var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5970
+ var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
5971
+ var import_types12 = require("@neat.is/types");
5972
+ var PARSE_CHUNK3 = 16384;
5973
+ var GRAMMAR_BY_EXT = {
5974
+ ".ts": import_tree_sitter_typescript.default.typescript,
5975
+ ".tsx": import_tree_sitter_typescript.default.tsx,
5976
+ ".js": import_tree_sitter_javascript3.default,
5977
+ ".jsx": import_tree_sitter_javascript3.default,
5978
+ ".mjs": import_tree_sitter_javascript3.default,
5979
+ ".cjs": import_tree_sitter_javascript3.default
5980
+ };
5981
+ function parseSource3(parser, source) {
5982
+ return parser.parse(
5983
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5984
+ );
5661
5985
  }
5662
- async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
5663
- if (!specifier) return null;
5664
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
5665
- const base = import_node_path16.default.resolve(importerDir, specifier);
5666
- const ext = import_node_path16.default.extname(specifier);
5667
- if (ext) {
5668
- if (ext === ".js" || ext === ".jsx") {
5669
- const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
5670
- const tsSibling = base.slice(0, -ext.length) + tsExt;
5671
- if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
5672
- return toPosix(import_node_path16.default.relative(serviceDir, tsSibling));
5986
+ function methodName(node) {
5987
+ const name = node.childForFieldName("name");
5988
+ return name ? name.text : null;
5989
+ }
5990
+ function collectSymbolDefs(root) {
5991
+ const out = [];
5992
+ const push = (kind, qualname, node) => {
5993
+ out.push({
5994
+ kind,
5995
+ qualname,
5996
+ startLine: node.startPosition.row + 1,
5997
+ endLine: node.endPosition.row + 1
5998
+ });
5999
+ };
6000
+ const visit = (node, classCtx) => {
6001
+ switch (node.type) {
6002
+ case "function_declaration":
6003
+ case "generator_function_declaration": {
6004
+ const name = node.childForFieldName("name")?.text;
6005
+ if (name) push("function", name, node);
6006
+ break;
6007
+ }
6008
+ case "class_declaration":
6009
+ case "abstract_class_declaration":
6010
+ case "class": {
6011
+ const name = node.childForFieldName("name")?.text;
6012
+ if (name) push("class", name, node);
6013
+ const body = node.childForFieldName("body");
6014
+ if (body) {
6015
+ for (let i = 0; i < body.namedChildCount; i++) {
6016
+ const child = body.namedChild(i);
6017
+ if (child) visit(child, name ?? classCtx);
6018
+ }
5673
6019
  }
6020
+ return;
5674
6021
  }
5675
- if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
5676
- return toPosix(import_node_path16.default.relative(serviceDir, base));
6022
+ case "method_definition": {
6023
+ const name = methodName(node);
6024
+ if (name) {
6025
+ const kind = name === "constructor" ? "constructor" : "method";
6026
+ push(kind, classCtx ? `${classCtx}.${name}` : name, node);
6027
+ }
6028
+ break;
5677
6029
  }
5678
- return null;
5679
- }
5680
- return firstExistingCandidate(base, serviceDir);
5681
- }
5682
- if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
5683
- return null;
5684
- }
5685
- async function resolvePyImport(imp, importerPath, serviceDir) {
5686
- let baseDir;
5687
- if (imp.level > 0) {
5688
- baseDir = import_node_path16.default.dirname(importerPath);
5689
- for (let i = 1; i < imp.level; i++) baseDir = import_node_path16.default.dirname(baseDir);
5690
- } else {
5691
- baseDir = serviceDir;
5692
- }
5693
- const moduleBase = imp.modulePath ? import_node_path16.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
5694
- const resolved = /* @__PURE__ */ new Set();
5695
- let needModuleFile = imp.names.length === 0;
5696
- for (const name of imp.names) {
5697
- const submoduleFile = import_node_path16.default.join(moduleBase, `${name}.py`);
5698
- const subpackageInit = import_node_path16.default.join(moduleBase, name, "__init__.py");
5699
- if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
5700
- resolved.add(toPosix(import_node_path16.default.relative(serviceDir, submoduleFile)));
5701
- } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
5702
- resolved.add(toPosix(import_node_path16.default.relative(serviceDir, subpackageInit)));
5703
- } else {
5704
- needModuleFile = true;
5705
- }
5706
- }
5707
- if (needModuleFile) {
5708
- const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path16.default.join(moduleBase, "__init__.py")] : [import_node_path16.default.join(moduleBase, "__init__.py")];
5709
- for (const candidate of moduleFileCandidates) {
5710
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5711
- resolved.add(toPosix(import_node_path16.default.relative(serviceDir, candidate)));
6030
+ case "variable_declarator": {
6031
+ const value = node.childForFieldName("value");
6032
+ if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
6033
+ const nameNode = node.childForFieldName("name");
6034
+ if (nameNode && nameNode.type === "identifier") {
6035
+ push("function", nameNode.text, node);
6036
+ }
6037
+ }
5712
6038
  break;
5713
6039
  }
5714
6040
  }
5715
- }
5716
- return [...resolved];
6041
+ for (let i = 0; i < node.namedChildCount; i++) {
6042
+ const child = node.namedChild(i);
6043
+ if (child) visit(child, classCtx);
6044
+ }
6045
+ };
6046
+ visit(root, void 0);
6047
+ return out;
5717
6048
  }
5718
- async function resolveGoImport(specifier, modulePath, serviceDir) {
5719
- if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
5720
- const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
5721
- const dir = import_node_path16.default.join(serviceDir, suffix);
5722
- const entries = await import_node_fs13.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
5723
- const candidates = entries.filter((entry2) => entry2.isFile() && entry2.name.endsWith(".go") && !entry2.name.endsWith("_test.go")).map((entry2) => import_node_path16.default.join(dir, entry2.name));
5724
- if (candidates.length !== 1) return null;
5725
- return toPosix(import_node_path16.default.relative(serviceDir, candidates[0]));
6049
+ function disambiguate(defs) {
6050
+ const counts = /* @__PURE__ */ new Map();
6051
+ for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
6052
+ const seen = /* @__PURE__ */ new Map();
6053
+ return defs.map((def) => {
6054
+ if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
6055
+ const ordinal = seen.get(def.qualname) ?? 0;
6056
+ seen.set(def.qualname, ordinal + 1);
6057
+ return { def, disambiguator: ordinal };
6058
+ });
5726
6059
  }
5727
- function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
5728
- const importeeFileId = (0, import_types12.fileId)(serviceName, importeeRelPath);
5729
- if (!graph.hasNode(importeeFileId)) return 0;
5730
- const edgeId = (0, import_types12.extractedEdgeId)(importerFileId, importeeFileId, import_types12.EdgeType.IMPORTS);
5731
- if (graph.hasEdge(edgeId)) return 0;
5732
- const edge = {
5733
- id: edgeId,
5734
- source: importerFileId,
5735
- target: importeeFileId,
5736
- type: import_types12.EdgeType.IMPORTS,
5737
- provenance: import_types12.Provenance.EXTRACTED,
5738
- confidence: (0, import_types12.confidenceForExtracted)("structural"),
5739
- evidence: { file: importerRelPath, line, snippet: snippet2 }
6060
+ async function addSymbols(graph, services) {
6061
+ const parsers = /* @__PURE__ */ new Map();
6062
+ const parserForExt2 = (ext) => {
6063
+ const grammar = GRAMMAR_BY_EXT[ext];
6064
+ if (!grammar) return null;
6065
+ let parser = parsers.get(ext);
6066
+ if (!parser) {
6067
+ parser = new import_tree_sitter3.default();
6068
+ parser.setLanguage(grammar);
6069
+ parsers.set(ext, parser);
6070
+ }
6071
+ return parser;
5740
6072
  };
5741
- graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
5742
- return 1;
5743
- }
5744
- async function addImports(graph, services) {
5745
- const jsParser = makeJsParser2();
5746
- const pyParser = makePyParser2();
5747
- const goParser = makeGoParser2();
6073
+ let nodesAdded = 0;
5748
6074
  let edgesAdded = 0;
5749
6075
  for (const service of services) {
5750
- const tsPaths = await loadTsPathConfig(service.dir);
5751
6076
  const files = await loadSourceFiles(service.dir);
5752
6077
  for (const file of files) {
5753
- if (isTestPath(file.path)) continue;
5754
- const relFile = toPosix(import_node_path16.default.relative(service.dir, file.path));
5755
- const importerFileId = (0, import_types12.fileId)(service.pkg.name, relFile);
5756
- const isPython = import_node_path16.default.extname(file.path) === ".py";
5757
- const isGo = import_node_path16.default.extname(file.path) === ".go";
5758
- if (isGo) {
5759
- let goImports = [];
5760
- try {
5761
- const tree = parseSource3(goParser, file.content);
5762
- collectGoImports(tree.rootNode, goImports);
5763
- } catch (err) {
5764
- recordExtractionError("import extraction", file.path, err);
5765
- continue;
5766
- }
5767
- const goMod = await import_node_fs13.promises.readFile(import_node_path16.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
5768
- const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
5769
- if (!modulePath) continue;
5770
- for (const imp of goImports) {
5771
- const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
5772
- if (!resolved) continue;
5773
- edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
5774
- }
5775
- continue;
5776
- }
5777
- if (isPython) {
5778
- let pyImports = [];
5779
- try {
5780
- const tree = parseSource3(pyParser, file.content);
5781
- collectPyImports(tree.rootNode, pyImports);
5782
- } catch (err) {
5783
- recordExtractionError("import extraction", file.path, err);
5784
- continue;
5785
- }
5786
- for (const imp of pyImports) {
5787
- const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
5788
- for (const resolved of resolvedPaths) {
5789
- edgesAdded += emitImportEdge(
5790
- graph,
5791
- service.pkg.name,
5792
- importerFileId,
5793
- relFile,
5794
- resolved,
5795
- imp.line,
5796
- imp.snippet
5797
- );
5798
- }
5799
- }
5800
- continue;
5801
- }
5802
- let jsImports = [];
6078
+ const parser = parserForExt2(import_node_path16.default.extname(file.path));
6079
+ if (!parser) continue;
6080
+ const relPath = toPosix(import_node_path16.default.relative(service.dir, file.path));
6081
+ let defs;
5803
6082
  try {
5804
- const tree = parseSource3(jsParser, file.content);
5805
- collectJsImports(tree.rootNode, jsImports);
6083
+ const tree = parseSource3(parser, file.content);
6084
+ defs = collectSymbolDefs(tree.rootNode);
5806
6085
  } catch (err) {
5807
- recordExtractionError("import extraction", file.path, err);
6086
+ recordExtractionError("symbol extraction", file.path, err);
5808
6087
  continue;
5809
6088
  }
5810
- for (const imp of jsImports) {
5811
- const resolved = await resolveJsImport(imp.specifier, import_node_path16.default.dirname(file.path), service.dir, tsPaths);
5812
- if (!resolved) continue;
5813
- edgesAdded += emitImportEdge(
5814
- graph,
5815
- service.pkg.name,
5816
- importerFileId,
5817
- relFile,
5818
- resolved,
5819
- imp.line,
5820
- imp.snippet
5821
- );
6089
+ if (defs.length === 0) continue;
6090
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6091
+ graph,
6092
+ service.pkg.name,
6093
+ service.node.id,
6094
+ relPath
6095
+ );
6096
+ nodesAdded += fn;
6097
+ edgesAdded += fe;
6098
+ for (const { def, disambiguator } of disambiguate(defs)) {
6099
+ const sid = (0, import_types12.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
6100
+ if (!graph.hasNode(sid)) {
6101
+ const node = {
6102
+ id: sid,
6103
+ type: import_types12.NodeType.SymbolNode,
6104
+ kind: def.kind,
6105
+ qualname: def.qualname,
6106
+ span: { startLine: def.startLine, endLine: def.endLine },
6107
+ service: service.pkg.name,
6108
+ relPath,
6109
+ discoveredVia: "static"
6110
+ };
6111
+ graph.addNode(sid, node);
6112
+ nodesAdded++;
6113
+ }
6114
+ const containsId = (0, import_types12.extractedEdgeId)(fileNodeId, sid, import_types12.EdgeType.CONTAINS);
6115
+ if (!graph.hasEdge(containsId)) {
6116
+ const edge = {
6117
+ id: containsId,
6118
+ source: fileNodeId,
6119
+ target: sid,
6120
+ type: import_types12.EdgeType.CONTAINS,
6121
+ provenance: import_types12.Provenance.EXTRACTED,
6122
+ confidence: (0, import_types12.confidenceForExtracted)("structural"),
6123
+ evidence: {
6124
+ file: relPath,
6125
+ line: def.startLine,
6126
+ snippet: snippet(file.content, def.startLine)
6127
+ }
6128
+ };
6129
+ graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
6130
+ edgesAdded++;
6131
+ }
5822
6132
  }
5823
6133
  }
5824
6134
  }
5825
- return { nodesAdded: 0, edgesAdded };
6135
+ return { nodesAdded, edgesAdded };
5826
6136
  }
5827
6137
 
5828
6138
  // src/extract/symbol-edges.ts
6139
+ init_cjs_shims();
6140
+ var import_node_path17 = __toESM(require("path"), 1);
6141
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
6142
+ var import_types13 = require("@neat.is/types");
5829
6143
  function extendsInfo(classHeritage) {
5830
6144
  for (let i = 0; i < classHeritage.namedChildCount; i++) {
5831
6145
  const child = classHeritage.namedChild(i);
@@ -5936,7 +6250,7 @@ async function addSymbolEdges(graph, services) {
5936
6250
  const fileDir = import_node_path17.default.dirname(file.path);
5937
6251
  let root;
5938
6252
  try {
5939
- root = parseSource2(parser, file.content).rootNode;
6253
+ root = parseSource3(parser, file.content).rootNode;
5940
6254
  } catch (err) {
5941
6255
  recordExtractionError("symbol edge extraction", file.path, err);
5942
6256
  continue;
@@ -8388,7 +8702,7 @@ function columnsFromObject(obj) {
8388
8702
  }
8389
8703
  function drizzleEndpointsFromFile(file, serviceDir) {
8390
8704
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
8391
- const tree = parseSource2(parserForExt(import_node_path38.default.extname(file.path)), file.content);
8705
+ const tree = parseSource3(parserForExt(import_node_path38.default.extname(file.path)), file.content);
8392
8706
  const out = [];
8393
8707
  const seen = /* @__PURE__ */ new Set();
8394
8708
  const walk6 = (node) => {
@@ -15004,9 +15318,11 @@ var ALL_PHASES = [
15004
15318
  "services",
15005
15319
  "aliases",
15006
15320
  "files",
15321
+ "symbols",
15007
15322
  "imports",
15008
15323
  "databases",
15009
15324
  "configs",
15325
+ "routes",
15010
15326
  "calls",
15011
15327
  "infra"
15012
15328
  ];
@@ -15029,7 +15345,9 @@ function classifyChange(relPath) {
15029
15345
  }
15030
15346
  if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
15031
15347
  phases.add("files");
15348
+ phases.add("symbols");
15032
15349
  phases.add("imports");
15350
+ phases.add("routes");
15033
15351
  phases.add("calls");
15034
15352
  }
15035
15353
  if (/\.ya?ml$/.test(base) && !/^docker-compose.*\.ya?ml$/.test(base)) {
@@ -15056,6 +15374,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
15056
15374
  nodesAdded += r.nodesAdded;
15057
15375
  edgesAdded += r.edgesAdded;
15058
15376
  }
15377
+ if (phases.has("symbols")) {
15378
+ const r = await addSymbols(graph, services);
15379
+ nodesAdded += r.nodesAdded;
15380
+ edgesAdded += r.edgesAdded;
15381
+ }
15059
15382
  if (phases.has("imports")) {
15060
15383
  const r = await addImports(graph, services);
15061
15384
  nodesAdded += r.nodesAdded;
@@ -15071,6 +15394,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
15071
15394
  nodesAdded += r.nodesAdded;
15072
15395
  edgesAdded += r.edgesAdded;
15073
15396
  }
15397
+ if (phases.has("routes")) {
15398
+ const r = await addRoutes(graph, services);
15399
+ nodesAdded += r.nodesAdded;
15400
+ edgesAdded += r.edgesAdded;
15401
+ }
15074
15402
  if (phases.has("calls")) {
15075
15403
  const r = await addCallEdges(graph, services);
15076
15404
  nodesAdded += r.nodesAdded;