@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/neatd.cjs CHANGED
@@ -780,8 +780,8 @@ init_cjs_shims();
780
780
 
781
781
  // src/ingest.ts
782
782
  init_cjs_shims();
783
- var import_node_fs6 = require("fs");
784
- var import_node_path7 = __toESM(require("path"), 1);
783
+ var import_node_fs7 = require("fs");
784
+ var import_node_path8 = __toESM(require("path"), 1);
785
785
  var sourceMapJs = __toESM(require("source-map-js"), 1);
786
786
 
787
787
  // src/policy.ts
@@ -2130,16 +2130,16 @@ var PolicyViolationsLog = class {
2130
2130
  };
2131
2131
 
2132
2132
  // src/ingest.ts
2133
- var import_types7 = require("@neat.is/types");
2133
+ var import_types8 = require("@neat.is/types");
2134
2134
 
2135
2135
  // src/extract/routes.ts
2136
2136
  init_cjs_shims();
2137
- var import_node_path6 = __toESM(require("path"), 1);
2138
- var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2139
- var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2140
- var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2141
- var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1);
2142
- var import_types5 = require("@neat.is/types");
2137
+ var import_node_path7 = __toESM(require("path"), 1);
2138
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
2139
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
2140
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
2141
+ var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
2142
+ var import_types6 = require("@neat.is/types");
2143
2143
 
2144
2144
  // src/extract/shared.ts
2145
2145
  init_cjs_shims();
@@ -2510,7 +2510,15 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
2510
2510
  return { fileNodeId, nodesAdded, edgesAdded };
2511
2511
  }
2512
2512
 
2513
- // src/extract/routes.ts
2513
+ // src/extract/imports.ts
2514
+ init_cjs_shims();
2515
+ var import_node_path6 = __toESM(require("path"), 1);
2516
+ var import_node_fs6 = require("fs");
2517
+ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2518
+ var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2519
+ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2520
+ var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1);
2521
+ var import_types5 = require("@neat.is/types");
2514
2522
  var PARSE_CHUNK = 16384;
2515
2523
  function parseSource(parser, source) {
2516
2524
  return parser.parse(
@@ -2532,155 +2540,532 @@ function makeGoParser() {
2532
2540
  p.setLanguage(import_tree_sitter_go.default);
2533
2541
  return p;
2534
2542
  }
2535
- var ROUTER_METHODS = /* @__PURE__ */ new Set([
2536
- "get",
2537
- "post",
2538
- "put",
2539
- "patch",
2540
- "delete",
2541
- "options",
2542
- "head",
2543
- "all"
2544
- ]);
2545
- var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2546
- var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
2547
- function ginRoutesFromSource(source, parser) {
2548
- const tree = parseSource(parser, source);
2549
- const prefixes = /* @__PURE__ */ new Map();
2550
- const out = [];
2551
- walk(tree.rootNode, (node) => {
2552
- if (node.type === "short_var_declaration" || node.type === "var_spec") {
2553
- const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
2554
- const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
2555
- if (name && value?.type === "call_expression") {
2556
- const fn2 = value.childForFieldName("function");
2557
- const field = fn2?.childForFieldName("field")?.text;
2558
- const first2 = value.childForFieldName("arguments")?.namedChild(0);
2559
- if (field === "Group" && first2?.type === "interpreted_string_literal") {
2560
- prefixes.set(name, first2.text.slice(1, -1));
2561
- }
2562
- }
2563
- return;
2564
- }
2565
- if (node.type !== "call_expression") return;
2566
- const fn = node.childForFieldName("function");
2567
- if (fn?.type !== "selector_expression") return;
2568
- const method = fn.childForFieldName("field")?.text?.toUpperCase();
2569
- if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
2570
- const receiver = fn.childForFieldName("operand")?.text ?? "";
2571
- const first = node.childForFieldName("arguments")?.namedChild(0);
2572
- if (first?.type !== "interpreted_string_literal") return;
2573
- const leaf = first.text.slice(1, -1);
2574
- out.push({
2575
- method: method === "ALL" ? "ALL" : method,
2576
- pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
2577
- line: node.startPosition.row + 1,
2578
- framework: "gin"
2579
- });
2580
- });
2581
- return out;
2582
- }
2583
- var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
2584
- var NESTJS_METHODS = /* @__PURE__ */ new Map([
2585
- ["Get", "GET"],
2586
- ["Post", "POST"],
2587
- ["Put", "PUT"],
2588
- ["Patch", "PATCH"],
2589
- ["Delete", "DELETE"],
2590
- ["Options", "OPTIONS"],
2591
- ["Head", "HEAD"],
2592
- ["All", "ALL"]
2593
- ]);
2594
- function canonicalizeTemplate(raw) {
2595
- let p = raw.split("?")[0].split("#")[0];
2596
- if (!p.startsWith("/")) p = "/" + p;
2597
- if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
2598
- return p;
2599
- }
2600
- function isDynamicSegment(seg) {
2601
- if (seg.length === 0) return false;
2602
- if (seg.includes(":")) return true;
2603
- if (seg.startsWith("{") || seg.startsWith("[")) return true;
2604
- if (/^\d+$/.test(seg)) return true;
2605
- 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;
2606
- if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
2607
- return false;
2543
+ function stringLiteralText(node) {
2544
+ for (let i = 0; i < node.childCount; i++) {
2545
+ const child = node.child(i);
2546
+ if (child?.type === "string_fragment") return child.text;
2547
+ }
2548
+ const raw = node.text;
2549
+ if (raw.length >= 2) return raw.slice(1, -1);
2550
+ return raw.length === 0 ? null : "";
2608
2551
  }
2609
- function normalizePathTemplate(raw) {
2610
- const canonical = canonicalizeTemplate(raw);
2611
- const segments = canonical.split("/").filter((s) => s.length > 0);
2612
- const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
2613
- return "/" + normalised.join("/");
2552
+ function clipSnippet(text) {
2553
+ const oneLine = text.split("\n")[0] ?? text;
2554
+ return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
2614
2555
  }
2615
- function walk(node, visit) {
2616
- visit(node);
2556
+ function collectGoImports(node, out) {
2557
+ if (node.type === "import_spec") {
2558
+ const pathNode = node.childForFieldName("path");
2559
+ if (pathNode) {
2560
+ const specifier = pathNode.text.replace(/^`|`$/g, "").replace(/^"|"$/g, "");
2561
+ if (specifier) out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2562
+ }
2563
+ return;
2564
+ }
2617
2565
  for (let i = 0; i < node.namedChildCount; i++) {
2618
2566
  const child = node.namedChild(i);
2619
- if (child) walk(child, visit);
2567
+ if (child) collectGoImports(child, out);
2620
2568
  }
2621
2569
  }
2622
- function staticStringText(node) {
2623
- if (node.type === "string") {
2624
- for (let i = 0; i < node.namedChildCount; i++) {
2625
- const child = node.namedChild(i);
2626
- if (child?.type === "string_fragment") return child.text;
2570
+ function collectJsImports(node, out) {
2571
+ if (node.type === "import_statement") {
2572
+ const source = node.childForFieldName("source");
2573
+ if (source) {
2574
+ const specifier = stringLiteralText(source);
2575
+ if (specifier) {
2576
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2577
+ }
2627
2578
  }
2628
- return "";
2579
+ return;
2629
2580
  }
2630
- if (node.type === "template_string") {
2631
- for (let i = 0; i < node.namedChildCount; i++) {
2632
- if (node.namedChild(i)?.type === "template_substitution") return null;
2581
+ if (node.type === "call_expression") {
2582
+ const fn = node.childForFieldName("function");
2583
+ if (fn?.type === "identifier" && fn.text === "require") {
2584
+ const args = node.childForFieldName("arguments");
2585
+ const firstArg = args?.namedChild(0);
2586
+ if (firstArg?.type === "string") {
2587
+ const specifier = stringLiteralText(firstArg);
2588
+ if (specifier) {
2589
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2590
+ }
2591
+ }
2633
2592
  }
2634
- const raw = node.text;
2635
- return raw.length >= 2 ? raw.slice(1, -1) : "";
2636
2593
  }
2637
- return null;
2594
+ for (let i = 0; i < node.namedChildCount; i++) {
2595
+ const child = node.namedChild(i);
2596
+ if (child) collectJsImports(child, out);
2597
+ }
2638
2598
  }
2639
- function objectStringProp(objNode, key) {
2640
- for (let i = 0; i < objNode.namedChildCount; i++) {
2641
- const pair = objNode.namedChild(i);
2642
- if (!pair || pair.type !== "pair") continue;
2643
- const k = pair.childForFieldName("key");
2644
- if (!k) continue;
2645
- const kText = k.type === "string" ? staticStringText(k) : k.text;
2646
- if (kText !== key) continue;
2647
- const v = pair.childForFieldName("value");
2648
- if (v) return staticStringText(v);
2599
+ function collectImportedNames(node, out) {
2600
+ if (node.type === "aliased_import") {
2601
+ const nameNode = node.childForFieldName("name");
2602
+ if (nameNode) out.push(nameNode.text);
2603
+ return;
2604
+ }
2605
+ if (node.type === "dotted_name") {
2606
+ out.push(node.text);
2607
+ return;
2608
+ }
2609
+ for (let i = 0; i < node.namedChildCount; i++) {
2610
+ const child = node.namedChild(i);
2611
+ if (child) collectImportedNames(child, out);
2649
2612
  }
2650
- return null;
2651
2613
  }
2652
- function fastifyRouteMethods(objNode) {
2653
- for (let i = 0; i < objNode.namedChildCount; i++) {
2654
- const pair = objNode.namedChild(i);
2655
- if (!pair || pair.type !== "pair") continue;
2656
- const k = pair.childForFieldName("key");
2657
- const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
2658
- if (kText !== "method") continue;
2659
- const v = pair.childForFieldName("value");
2660
- if (!v) return [];
2661
- if (v.type === "string" || v.type === "template_string") {
2662
- const s = staticStringText(v);
2663
- return s ? [s.toUpperCase()] : [];
2664
- }
2665
- if (v.type === "array") {
2666
- const out = [];
2667
- for (let j = 0; j < v.namedChildCount; j++) {
2668
- const el = v.namedChild(j);
2669
- if (el && (el.type === "string" || el.type === "template_string")) {
2670
- const s = staticStringText(el);
2671
- if (s) out.push(s.toUpperCase());
2614
+ function collectPyImports(node, out) {
2615
+ if (node.type === "import_from_statement") {
2616
+ let level = 0;
2617
+ let modulePath = "";
2618
+ const names = [];
2619
+ let pastFrom = false;
2620
+ let pastImport = false;
2621
+ for (let i = 0; i < node.childCount; i++) {
2622
+ const child = node.child(i);
2623
+ if (!child) continue;
2624
+ if (!pastFrom) {
2625
+ if (child.type === "from") pastFrom = true;
2626
+ continue;
2627
+ }
2628
+ if (!pastImport) {
2629
+ if (child.type === "import") {
2630
+ pastImport = true;
2631
+ continue;
2632
+ }
2633
+ if (child.type === "relative_import") {
2634
+ for (let j = 0; j < child.childCount; j++) {
2635
+ const rc = child.child(j);
2636
+ if (!rc) continue;
2637
+ if (rc.type === "import_prefix") {
2638
+ for (let k = 0; k < rc.childCount; k++) {
2639
+ if (rc.child(k)?.type === ".") level++;
2640
+ }
2641
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
2642
+ }
2643
+ } else if (child.type === "dotted_name") {
2644
+ modulePath = child.text;
2672
2645
  }
2646
+ continue;
2673
2647
  }
2674
- return out;
2648
+ collectImportedNames(child, names);
2649
+ }
2650
+ if (level > 0 || modulePath) {
2651
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2675
2652
  }
2676
2653
  }
2677
- return [];
2654
+ for (let i = 0; i < node.namedChildCount; i++) {
2655
+ const child = node.namedChild(i);
2656
+ if (child) collectPyImports(child, out);
2657
+ }
2678
2658
  }
2679
- function nestDecoratorImports(root) {
2680
- const imports = /* @__PURE__ */ new Map();
2681
- walk(root, (node) => {
2682
- if (node.type !== "import_statement") return;
2683
- const source = node.childForFieldName("source");
2659
+ async function fileExists(p) {
2660
+ try {
2661
+ await import_node_fs6.promises.access(p);
2662
+ return true;
2663
+ } catch {
2664
+ return false;
2665
+ }
2666
+ }
2667
+ function isWithinServiceDir(candidate, serviceDir) {
2668
+ const rel = import_node_path6.default.relative(serviceDir, candidate);
2669
+ return rel !== "" && !rel.startsWith("..") && !import_node_path6.default.isAbsolute(rel);
2670
+ }
2671
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2672
+ var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
2673
+ async function firstExistingCandidate(base, serviceDir) {
2674
+ for (const ext of JS_EXTENSIONS) {
2675
+ const candidate = base + ext;
2676
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2677
+ return toPosix(import_node_path6.default.relative(serviceDir, candidate));
2678
+ }
2679
+ }
2680
+ for (const indexFile of JS_INDEX_FILES) {
2681
+ const candidate = import_node_path6.default.join(base, indexFile);
2682
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2683
+ return toPosix(import_node_path6.default.relative(serviceDir, candidate));
2684
+ }
2685
+ }
2686
+ return null;
2687
+ }
2688
+ async function loadTsPathConfig(serviceDir) {
2689
+ const tsconfigPath = import_node_path6.default.join(serviceDir, "tsconfig.json");
2690
+ let raw;
2691
+ try {
2692
+ raw = await import_node_fs6.promises.readFile(tsconfigPath, "utf8");
2693
+ } catch {
2694
+ return null;
2695
+ }
2696
+ try {
2697
+ const parsed = JSON.parse(raw);
2698
+ const paths = parsed.compilerOptions?.paths;
2699
+ if (!paths || Object.keys(paths).length === 0) return null;
2700
+ const baseUrl = parsed.compilerOptions?.baseUrl;
2701
+ return { paths, baseDir: baseUrl ? import_node_path6.default.resolve(serviceDir, baseUrl) : serviceDir };
2702
+ } catch (err) {
2703
+ recordExtractionError("import alias resolution", tsconfigPath, err);
2704
+ return null;
2705
+ }
2706
+ }
2707
+ async function resolveTsAlias(specifier, config, serviceDir) {
2708
+ for (const [pattern, targets] of Object.entries(config.paths)) {
2709
+ let suffix = null;
2710
+ if (pattern === specifier) {
2711
+ suffix = "";
2712
+ } else if (pattern.endsWith("/*")) {
2713
+ const prefix = pattern.slice(0, -1);
2714
+ if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
2715
+ }
2716
+ if (suffix === null) continue;
2717
+ for (const target of targets) {
2718
+ const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
2719
+ const resolvedBase = import_node_path6.default.resolve(config.baseDir, targetBase, suffix);
2720
+ const hit = await firstExistingCandidate(resolvedBase, serviceDir);
2721
+ if (hit) return hit;
2722
+ if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
2723
+ return toPosix(import_node_path6.default.relative(serviceDir, resolvedBase));
2724
+ }
2725
+ }
2726
+ }
2727
+ return null;
2728
+ }
2729
+ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
2730
+ if (!specifier) return null;
2731
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
2732
+ const base = import_node_path6.default.resolve(importerDir, specifier);
2733
+ const ext = import_node_path6.default.extname(specifier);
2734
+ if (ext) {
2735
+ if (ext === ".js" || ext === ".jsx") {
2736
+ const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
2737
+ const tsSibling = base.slice(0, -ext.length) + tsExt;
2738
+ if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
2739
+ return toPosix(import_node_path6.default.relative(serviceDir, tsSibling));
2740
+ }
2741
+ }
2742
+ if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
2743
+ return toPosix(import_node_path6.default.relative(serviceDir, base));
2744
+ }
2745
+ if (!JS_EXTENSIONS.includes(ext)) {
2746
+ return firstExistingCandidate(base, serviceDir);
2747
+ }
2748
+ return null;
2749
+ }
2750
+ return firstExistingCandidate(base, serviceDir);
2751
+ }
2752
+ if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
2753
+ return null;
2754
+ }
2755
+ async function resolvePyImport(imp, importerPath, serviceDir) {
2756
+ let baseDir;
2757
+ if (imp.level > 0) {
2758
+ baseDir = import_node_path6.default.dirname(importerPath);
2759
+ for (let i = 1; i < imp.level; i++) baseDir = import_node_path6.default.dirname(baseDir);
2760
+ } else {
2761
+ baseDir = serviceDir;
2762
+ }
2763
+ const moduleBase = imp.modulePath ? import_node_path6.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
2764
+ const resolved = /* @__PURE__ */ new Set();
2765
+ let needModuleFile = imp.names.length === 0;
2766
+ for (const name of imp.names) {
2767
+ const submoduleFile = import_node_path6.default.join(moduleBase, `${name}.py`);
2768
+ const subpackageInit = import_node_path6.default.join(moduleBase, name, "__init__.py");
2769
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
2770
+ resolved.add(toPosix(import_node_path6.default.relative(serviceDir, submoduleFile)));
2771
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
2772
+ resolved.add(toPosix(import_node_path6.default.relative(serviceDir, subpackageInit)));
2773
+ } else {
2774
+ needModuleFile = true;
2775
+ }
2776
+ }
2777
+ if (needModuleFile) {
2778
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path6.default.join(moduleBase, "__init__.py")] : [import_node_path6.default.join(moduleBase, "__init__.py")];
2779
+ for (const candidate of moduleFileCandidates) {
2780
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2781
+ resolved.add(toPosix(import_node_path6.default.relative(serviceDir, candidate)));
2782
+ break;
2783
+ }
2784
+ }
2785
+ }
2786
+ return [...resolved];
2787
+ }
2788
+ async function resolveGoImport(specifier, modulePath, serviceDir) {
2789
+ if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
2790
+ const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
2791
+ const dir = import_node_path6.default.join(serviceDir, suffix);
2792
+ const entries = await import_node_fs6.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
2793
+ const candidates = entries.filter((entry2) => entry2.isFile() && entry2.name.endsWith(".go") && !entry2.name.endsWith("_test.go")).map((entry2) => import_node_path6.default.join(dir, entry2.name));
2794
+ if (candidates.length !== 1) return null;
2795
+ return toPosix(import_node_path6.default.relative(serviceDir, candidates[0]));
2796
+ }
2797
+ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
2798
+ const importeeFileId = (0, import_types5.fileId)(serviceName, importeeRelPath);
2799
+ if (!graph.hasNode(importeeFileId)) return 0;
2800
+ const edgeId = (0, import_types5.extractedEdgeId)(importerFileId, importeeFileId, import_types5.EdgeType.IMPORTS);
2801
+ if (graph.hasEdge(edgeId)) return 0;
2802
+ const edge = {
2803
+ id: edgeId,
2804
+ source: importerFileId,
2805
+ target: importeeFileId,
2806
+ type: import_types5.EdgeType.IMPORTS,
2807
+ provenance: import_types5.Provenance.EXTRACTED,
2808
+ confidence: (0, import_types5.confidenceForExtracted)("structural"),
2809
+ evidence: { file: importerRelPath, line, snippet: snippet2 }
2810
+ };
2811
+ graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
2812
+ return 1;
2813
+ }
2814
+ async function addImports(graph, services) {
2815
+ const jsParser = makeJsParser();
2816
+ const pyParser = makePyParser();
2817
+ const goParser = makeGoParser();
2818
+ let edgesAdded = 0;
2819
+ for (const service of services) {
2820
+ const tsPaths = await loadTsPathConfig(service.dir);
2821
+ const files = await loadSourceFiles(service.dir);
2822
+ for (const file of files) {
2823
+ if (isTestPath(file.path)) continue;
2824
+ const relFile = toPosix(import_node_path6.default.relative(service.dir, file.path));
2825
+ const importerFileId = (0, import_types5.fileId)(service.pkg.name, relFile);
2826
+ const isPython = import_node_path6.default.extname(file.path) === ".py";
2827
+ const isGo = import_node_path6.default.extname(file.path) === ".go";
2828
+ if (isGo) {
2829
+ let goImports = [];
2830
+ try {
2831
+ const tree = parseSource(goParser, file.content);
2832
+ collectGoImports(tree.rootNode, goImports);
2833
+ } catch (err) {
2834
+ recordExtractionError("import extraction", file.path, err);
2835
+ continue;
2836
+ }
2837
+ const goMod = await import_node_fs6.promises.readFile(import_node_path6.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
2838
+ const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
2839
+ if (!modulePath) continue;
2840
+ for (const imp of goImports) {
2841
+ const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
2842
+ if (!resolved) continue;
2843
+ edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
2844
+ }
2845
+ continue;
2846
+ }
2847
+ if (isPython) {
2848
+ let pyImports = [];
2849
+ try {
2850
+ const tree = parseSource(pyParser, file.content);
2851
+ collectPyImports(tree.rootNode, pyImports);
2852
+ } catch (err) {
2853
+ recordExtractionError("import extraction", file.path, err);
2854
+ continue;
2855
+ }
2856
+ for (const imp of pyImports) {
2857
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
2858
+ for (const resolved of resolvedPaths) {
2859
+ edgesAdded += emitImportEdge(
2860
+ graph,
2861
+ service.pkg.name,
2862
+ importerFileId,
2863
+ relFile,
2864
+ resolved,
2865
+ imp.line,
2866
+ imp.snippet
2867
+ );
2868
+ }
2869
+ }
2870
+ continue;
2871
+ }
2872
+ let jsImports = [];
2873
+ try {
2874
+ const tree = parseSource(jsParser, file.content);
2875
+ collectJsImports(tree.rootNode, jsImports);
2876
+ } catch (err) {
2877
+ recordExtractionError("import extraction", file.path, err);
2878
+ continue;
2879
+ }
2880
+ for (const imp of jsImports) {
2881
+ const resolved = await resolveJsImport(imp.specifier, import_node_path6.default.dirname(file.path), service.dir, tsPaths);
2882
+ if (!resolved) continue;
2883
+ edgesAdded += emitImportEdge(
2884
+ graph,
2885
+ service.pkg.name,
2886
+ importerFileId,
2887
+ relFile,
2888
+ resolved,
2889
+ imp.line,
2890
+ imp.snippet
2891
+ );
2892
+ }
2893
+ }
2894
+ }
2895
+ return { nodesAdded: 0, edgesAdded };
2896
+ }
2897
+
2898
+ // src/extract/routes.ts
2899
+ var PARSE_CHUNK2 = 16384;
2900
+ function parseSource2(parser, source) {
2901
+ return parser.parse(
2902
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
2903
+ );
2904
+ }
2905
+ function makeJsParser2() {
2906
+ const p = new import_tree_sitter2.default();
2907
+ p.setLanguage(import_tree_sitter_javascript2.default);
2908
+ return p;
2909
+ }
2910
+ function makePyParser2() {
2911
+ const p = new import_tree_sitter2.default();
2912
+ p.setLanguage(import_tree_sitter_python2.default);
2913
+ return p;
2914
+ }
2915
+ function makeGoParser2() {
2916
+ const p = new import_tree_sitter2.default();
2917
+ p.setLanguage(import_tree_sitter_go2.default);
2918
+ return p;
2919
+ }
2920
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
2921
+ "get",
2922
+ "post",
2923
+ "put",
2924
+ "patch",
2925
+ "delete",
2926
+ "options",
2927
+ "head",
2928
+ "all"
2929
+ ]);
2930
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2931
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
2932
+ function ginRoutesFromSource(source, parser) {
2933
+ const tree = parseSource2(parser, source);
2934
+ const prefixes = /* @__PURE__ */ new Map();
2935
+ const out = [];
2936
+ walk(tree.rootNode, (node) => {
2937
+ if (node.type === "short_var_declaration" || node.type === "var_spec") {
2938
+ const name = node.childForFieldName("left")?.namedChild(0)?.text ?? node.childForFieldName("name")?.text;
2939
+ const value = node.childForFieldName("right")?.namedChild(0) ?? node.childForFieldName("value");
2940
+ if (name && value?.type === "call_expression") {
2941
+ const fn2 = value.childForFieldName("function");
2942
+ const field = fn2?.childForFieldName("field")?.text;
2943
+ const first2 = value.childForFieldName("arguments")?.namedChild(0);
2944
+ if (field === "Group" && first2?.type === "interpreted_string_literal") {
2945
+ prefixes.set(name, first2.text.slice(1, -1));
2946
+ }
2947
+ }
2948
+ return;
2949
+ }
2950
+ if (node.type !== "call_expression") return;
2951
+ const fn = node.childForFieldName("function");
2952
+ if (fn?.type !== "selector_expression") return;
2953
+ const method = fn.childForFieldName("field")?.text?.toUpperCase();
2954
+ if (!method || !ROUTER_METHODS.has(method.toLowerCase())) return;
2955
+ const receiver = fn.childForFieldName("operand")?.text ?? "";
2956
+ const first = node.childForFieldName("arguments")?.namedChild(0);
2957
+ if (first?.type !== "interpreted_string_literal") return;
2958
+ const leaf = first.text.slice(1, -1);
2959
+ out.push({
2960
+ method: method === "ALL" ? "ALL" : method,
2961
+ pathTemplate: canonicalizeTemplate((prefixes.get(receiver) ?? "") + leaf),
2962
+ line: node.startPosition.row + 1,
2963
+ framework: "gin"
2964
+ });
2965
+ });
2966
+ return out;
2967
+ }
2968
+ var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
2969
+ var NESTJS_METHODS = /* @__PURE__ */ new Map([
2970
+ ["Get", "GET"],
2971
+ ["Post", "POST"],
2972
+ ["Put", "PUT"],
2973
+ ["Patch", "PATCH"],
2974
+ ["Delete", "DELETE"],
2975
+ ["Options", "OPTIONS"],
2976
+ ["Head", "HEAD"],
2977
+ ["All", "ALL"]
2978
+ ]);
2979
+ function canonicalizeTemplate(raw) {
2980
+ let p = raw.split("?")[0].split("#")[0];
2981
+ if (!p.startsWith("/")) p = "/" + p;
2982
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
2983
+ return p;
2984
+ }
2985
+ function isDynamicSegment(seg) {
2986
+ if (seg.length === 0) return false;
2987
+ if (seg.includes(":")) return true;
2988
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
2989
+ if (/^\d+$/.test(seg)) return true;
2990
+ 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;
2991
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
2992
+ return false;
2993
+ }
2994
+ function normalizePathTemplate(raw) {
2995
+ const canonical = canonicalizeTemplate(raw);
2996
+ const segments = canonical.split("/").filter((s) => s.length > 0);
2997
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
2998
+ return "/" + normalised.join("/");
2999
+ }
3000
+ function walk(node, visit) {
3001
+ visit(node);
3002
+ for (let i = 0; i < node.namedChildCount; i++) {
3003
+ const child = node.namedChild(i);
3004
+ if (child) walk(child, visit);
3005
+ }
3006
+ }
3007
+ function staticStringText(node) {
3008
+ if (node.type === "string") {
3009
+ for (let i = 0; i < node.namedChildCount; i++) {
3010
+ const child = node.namedChild(i);
3011
+ if (child?.type === "string_fragment") return child.text;
3012
+ }
3013
+ return "";
3014
+ }
3015
+ if (node.type === "template_string") {
3016
+ for (let i = 0; i < node.namedChildCount; i++) {
3017
+ if (node.namedChild(i)?.type === "template_substitution") return null;
3018
+ }
3019
+ const raw = node.text;
3020
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
3021
+ }
3022
+ return null;
3023
+ }
3024
+ function objectStringProp(objNode, key) {
3025
+ for (let i = 0; i < objNode.namedChildCount; i++) {
3026
+ const pair = objNode.namedChild(i);
3027
+ if (!pair || pair.type !== "pair") continue;
3028
+ const k = pair.childForFieldName("key");
3029
+ if (!k) continue;
3030
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
3031
+ if (kText !== key) continue;
3032
+ const v = pair.childForFieldName("value");
3033
+ if (v) return staticStringText(v);
3034
+ }
3035
+ return null;
3036
+ }
3037
+ function fastifyRouteMethods(objNode) {
3038
+ for (let i = 0; i < objNode.namedChildCount; i++) {
3039
+ const pair = objNode.namedChild(i);
3040
+ if (!pair || pair.type !== "pair") continue;
3041
+ const k = pair.childForFieldName("key");
3042
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
3043
+ if (kText !== "method") continue;
3044
+ const v = pair.childForFieldName("value");
3045
+ if (!v) return [];
3046
+ if (v.type === "string" || v.type === "template_string") {
3047
+ const s = staticStringText(v);
3048
+ return s ? [s.toUpperCase()] : [];
3049
+ }
3050
+ if (v.type === "array") {
3051
+ const out = [];
3052
+ for (let j = 0; j < v.namedChildCount; j++) {
3053
+ const el = v.namedChild(j);
3054
+ if (el && (el.type === "string" || el.type === "template_string")) {
3055
+ const s = staticStringText(el);
3056
+ if (s) out.push(s.toUpperCase());
3057
+ }
3058
+ }
3059
+ return out;
3060
+ }
3061
+ }
3062
+ return [];
3063
+ }
3064
+ function nestDecoratorImports(root) {
3065
+ const imports = /* @__PURE__ */ new Map();
3066
+ walk(root, (node) => {
3067
+ if (node.type !== "import_statement") return;
3068
+ const source = node.childForFieldName("source");
2684
3069
  if (!source || staticStringText(source) !== "@nestjs/common") return;
2685
3070
  walk(node, (child) => {
2686
3071
  if (child.type !== "import_specifier") return;
@@ -2726,7 +3111,7 @@ function nestJoinedPath(prefix, leaf) {
2726
3111
  return canonicalizeTemplate(segments.join("/"));
2727
3112
  }
2728
3113
  function nestjsRoutesFromSource(source, parser) {
2729
- const tree = parseSource(parser, source);
3114
+ const tree = parseSource2(parser, source);
2730
3115
  const imports = nestDecoratorImports(tree.rootNode);
2731
3116
  if (![...imports.values()].includes("Controller")) return [];
2732
3117
  const out = [];
@@ -2774,7 +3159,7 @@ function nestjsRoutesFromSource(source, parser) {
2774
3159
  return out;
2775
3160
  }
2776
3161
  function serverRoutesFromSource(source, parser, hasExpress, hasFastify, hasHono = false) {
2777
- const tree = parseSource(parser, source);
3162
+ const tree = parseSource2(parser, source);
2778
3163
  const out = [];
2779
3164
  const framework = hasExpress ? "express" : hasFastify ? "fastify" : hasHono ? "hono" : "unknown";
2780
3165
  walk(tree.rootNode, (node) => {
@@ -2832,7 +3217,7 @@ function isNextPagesApiFile(relFile) {
2832
3217
  if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
2833
3218
  const base = segs[segs.length - 1] ?? "";
2834
3219
  if (/^_(app|document|middleware)\./.test(base)) return false;
2835
- return JS_ROUTE_EXTENSIONS.has(import_node_path6.default.extname(base));
3220
+ return JS_ROUTE_EXTENSIONS.has(import_node_path7.default.extname(base));
2836
3221
  }
2837
3222
  function nextSegment(seg) {
2838
3223
  if (seg.startsWith("(") && seg.endsWith(")")) return null;
@@ -2894,7 +3279,7 @@ function nextAppMethods(root) {
2894
3279
  }
2895
3280
  function nextRoutesFromFile(source, relFile, parser) {
2896
3281
  if (isNextAppRouteFile(relFile)) {
2897
- const tree = parseSource(parser, source);
3282
+ const tree = parseSource2(parser, source);
2898
3283
  const template = nextAppPathTemplate(relFile);
2899
3284
  return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
2900
3285
  method,
@@ -3015,7 +3400,7 @@ function collectMountPrefixes(root, consts) {
3015
3400
  return mounts;
3016
3401
  }
3017
3402
  function pythonRoutesFromSource(source, parser, framework) {
3018
- const tree = parseSource(parser, source);
3403
+ const tree = parseSource2(parser, source);
3019
3404
  const prefixes = collectPythonRouterPrefixes(tree.rootNode);
3020
3405
  const consts = collectStringConstants(tree.rootNode);
3021
3406
  const mounts = collectMountPrefixes(tree.rootNode, consts);
@@ -3055,7 +3440,7 @@ function pythonRoutesFromSource(source, parser, framework) {
3055
3440
  return out;
3056
3441
  }
3057
3442
  function djangoRoutesFromSource(source, parser) {
3058
- const tree = parseSource(parser, source);
3443
+ const tree = parseSource2(parser, source);
3059
3444
  const out = [];
3060
3445
  walk(tree.rootNode, (node) => {
3061
3446
  if (node.type !== "assignment") return;
@@ -3083,10 +3468,320 @@ function djangoRoutesFromSource(source, parser) {
3083
3468
  });
3084
3469
  return out;
3085
3470
  }
3471
+ function namedArgs(argsNode) {
3472
+ const out = [];
3473
+ if (!argsNode) return out;
3474
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
3475
+ const c = argsNode.namedChild(i);
3476
+ if (c && c.type !== "comment") out.push(c);
3477
+ }
3478
+ return out;
3479
+ }
3480
+ function parseUseMount(callNode) {
3481
+ const args = namedArgs(callNode.childForFieldName("arguments"));
3482
+ if (args.length === 0) return null;
3483
+ const first = args[0];
3484
+ const firstStr = first.type === "string" || first.type === "template_string" ? staticStringText(first) : null;
3485
+ if (firstStr !== null && firstStr.startsWith("/")) {
3486
+ const prefix = canonicalizeTemplate(firstStr);
3487
+ const second = args[1];
3488
+ const target = second && second.type === "identifier" ? second.text : null;
3489
+ return { prefix: prefix === "/" ? "" : prefix, target };
3490
+ }
3491
+ if (args.length === 1 && first.type === "identifier") return { prefix: "", target: first.text };
3492
+ return null;
3493
+ }
3494
+ function unwrapRouterExpr(node, expressLocals, routerCtors) {
3495
+ if (node.type === "identifier") return { base: { alias: node.text }, mounts: [] };
3496
+ if (node.type !== "call_expression") return null;
3497
+ const fn = node.childForFieldName("function");
3498
+ if (!fn) return null;
3499
+ if (fn.type === "member_expression") {
3500
+ const prop = fn.childForFieldName("property")?.text;
3501
+ const obj = fn.childForFieldName("object");
3502
+ if (!prop || !obj) return null;
3503
+ if (prop === "use") {
3504
+ const inner = unwrapRouterExpr(obj, expressLocals, routerCtors);
3505
+ if (!inner) return null;
3506
+ const mount = parseUseMount(node);
3507
+ return { base: inner.base, mounts: mount ? [...inner.mounts, mount] : inner.mounts };
3508
+ }
3509
+ if (prop === "Router" && obj.type === "identifier" && expressLocals.has(obj.text)) {
3510
+ return { base: "newRouter", mounts: [] };
3511
+ }
3512
+ return null;
3513
+ }
3514
+ if (fn.type === "identifier") {
3515
+ if (expressLocals.has(fn.text)) return { base: "app", mounts: [] };
3516
+ if (routerCtors.has(fn.text)) return { base: "newRouter", mounts: [] };
3517
+ }
3518
+ return null;
3519
+ }
3520
+ function collectExpressImports(root) {
3521
+ const expressLocals = /* @__PURE__ */ new Set();
3522
+ const routerCtors = /* @__PURE__ */ new Set();
3523
+ const bindings = [];
3524
+ const addFromExpress = (local, sel, exported) => {
3525
+ if (sel === "default" || sel === "namespace") expressLocals.add(local);
3526
+ else if (exported === "Router") routerCtors.add(local);
3527
+ };
3528
+ walk(root, (node) => {
3529
+ if (node.type === "import_statement") {
3530
+ const source = node.childForFieldName("source");
3531
+ const spec = source ? staticStringText(source) : null;
3532
+ if (!spec) return;
3533
+ let clause = null;
3534
+ for (let i = 0; i < node.namedChildCount; i++) {
3535
+ const c = node.namedChild(i);
3536
+ if (c?.type === "import_clause") clause = c;
3537
+ }
3538
+ if (!clause) return;
3539
+ for (let i = 0; i < clause.namedChildCount; i++) {
3540
+ const c = clause.namedChild(i);
3541
+ if (!c) continue;
3542
+ if (c.type === "identifier") {
3543
+ if (spec === "express") addFromExpress(c.text, "default", "default");
3544
+ else bindings.push({ local: c.text, specifier: spec, sel: "default" });
3545
+ } else if (c.type === "namespace_import") {
3546
+ const id = c.namedChild(0);
3547
+ if (id?.type === "identifier") {
3548
+ if (spec === "express") addFromExpress(id.text, "namespace", "namespace");
3549
+ else bindings.push({ local: id.text, specifier: spec, sel: "namespace" });
3550
+ }
3551
+ } else if (c.type === "named_imports") {
3552
+ for (let j = 0; j < c.namedChildCount; j++) {
3553
+ const s = c.namedChild(j);
3554
+ if (s?.type !== "import_specifier") continue;
3555
+ const name = s.childForFieldName("name")?.text;
3556
+ if (!name) continue;
3557
+ const local = s.childForFieldName("alias")?.text ?? name;
3558
+ if (spec === "express") addFromExpress(local, name, name);
3559
+ else bindings.push({ local, specifier: spec, sel: name });
3560
+ }
3561
+ }
3562
+ }
3563
+ return;
3564
+ }
3565
+ if (node.type === "variable_declarator") {
3566
+ const value = node.childForFieldName("value");
3567
+ if (value?.type !== "call_expression") return;
3568
+ const fn = value.childForFieldName("function");
3569
+ if (fn?.type !== "identifier" || fn.text !== "require") return;
3570
+ const arg = namedArgs(value.childForFieldName("arguments"))[0];
3571
+ const spec = arg ? staticStringText(arg) : null;
3572
+ if (!spec) return;
3573
+ const name = node.childForFieldName("name");
3574
+ if (name?.type === "identifier") {
3575
+ if (spec === "express") expressLocals.add(name.text);
3576
+ else bindings.push({ local: name.text, specifier: spec, sel: "default" });
3577
+ } else if (name?.type === "object_pattern") {
3578
+ for (let i = 0; i < name.namedChildCount; i++) {
3579
+ const el = name.namedChild(i);
3580
+ if (!el) continue;
3581
+ let local;
3582
+ let exported;
3583
+ if (el.type === "shorthand_property_identifier_pattern") {
3584
+ local = el.text;
3585
+ exported = el.text;
3586
+ } else if (el.type === "pair_pattern") {
3587
+ exported = el.childForFieldName("key")?.text;
3588
+ local = el.childForFieldName("value")?.text ?? exported;
3589
+ }
3590
+ if (!local || !exported) continue;
3591
+ if (spec === "express") addFromExpress(local, exported, exported);
3592
+ else bindings.push({ local, specifier: spec, sel: exported });
3593
+ }
3594
+ }
3595
+ }
3596
+ });
3597
+ return { expressLocals, routerCtors, bindings };
3598
+ }
3599
+ function analyzeExpressFile(root, dir) {
3600
+ const { expressLocals, routerCtors, bindings } = collectExpressImports(root);
3601
+ const routerVars = /* @__PURE__ */ new Map();
3602
+ const appVars = /* @__PURE__ */ new Set();
3603
+ const exportNamed = /* @__PURE__ */ new Map();
3604
+ let exportDefaultName = null;
3605
+ const getVar = (name) => {
3606
+ let rv = routerVars.get(name);
3607
+ if (!rv) {
3608
+ rv = { declares: false, mounts: [] };
3609
+ routerVars.set(name, rv);
3610
+ }
3611
+ return rv;
3612
+ };
3613
+ const refFromExpr = (expr, key) => {
3614
+ if (expr.type === "identifier") return expr.text;
3615
+ const u = unwrapRouterExpr(expr, expressLocals, routerCtors);
3616
+ if (!u) return null;
3617
+ const rv = getVar(key);
3618
+ for (const m of u.mounts) rv.mounts.push(m);
3619
+ if (typeof u.base === "object") rv.aliasOf = u.base.alias;
3620
+ return key;
3621
+ };
3622
+ const isExported = (declarator) => {
3623
+ const decl = declarator.parent;
3624
+ return decl?.parent?.type === "export_statement";
3625
+ };
3626
+ walk(root, (node) => {
3627
+ if (node.type === "variable_declarator") {
3628
+ const value = node.childForFieldName("value");
3629
+ const name = node.childForFieldName("name");
3630
+ if (name?.type !== "identifier" || !value) return;
3631
+ if (value.type === "call_expression") {
3632
+ const fn = value.childForFieldName("function");
3633
+ if (fn?.type === "identifier" && fn.text === "require") return;
3634
+ }
3635
+ const u = unwrapRouterExpr(value, expressLocals, routerCtors);
3636
+ if (!u) return;
3637
+ const rv = getVar(name.text);
3638
+ for (const m of u.mounts) rv.mounts.push(m);
3639
+ if (u.base === "app") appVars.add(name.text);
3640
+ else if (typeof u.base === "object") rv.aliasOf = u.base.alias;
3641
+ if (isExported(node)) exportNamed.set(name.text, name.text);
3642
+ return;
3643
+ }
3644
+ if (node.type === "call_expression") {
3645
+ const fn = node.childForFieldName("function");
3646
+ if (fn?.type !== "member_expression") return;
3647
+ const obj = fn.childForFieldName("object");
3648
+ const prop = fn.childForFieldName("property")?.text;
3649
+ if (obj?.type !== "identifier" || !prop) return;
3650
+ if (prop === "use") {
3651
+ const m = parseUseMount(node);
3652
+ if (m) getVar(obj.text).mounts.push(m);
3653
+ } else if (ROUTER_METHODS.has(prop.toLowerCase())) {
3654
+ const first = namedArgs(node.childForFieldName("arguments"))[0];
3655
+ const p = first ? staticStringText(first) : null;
3656
+ if (p !== null && p.startsWith("/")) getVar(obj.text).declares = true;
3657
+ }
3658
+ return;
3659
+ }
3660
+ if (node.type === "export_statement") {
3661
+ let clause = null;
3662
+ for (let i = 0; i < node.namedChildCount; i++) {
3663
+ const c = node.namedChild(i);
3664
+ if (c?.type === "export_clause") clause = c;
3665
+ }
3666
+ if (clause) {
3667
+ for (let i = 0; i < clause.namedChildCount; i++) {
3668
+ const spec = clause.namedChild(i);
3669
+ if (spec?.type !== "export_specifier") continue;
3670
+ const local = spec.childForFieldName("name")?.text;
3671
+ if (!local) continue;
3672
+ const exportedAs = spec.childForFieldName("alias")?.text ?? local;
3673
+ if (exportedAs === "default") exportDefaultName = local;
3674
+ else exportNamed.set(exportedAs, local);
3675
+ }
3676
+ return;
3677
+ }
3678
+ if (node.childForFieldName("declaration")) return;
3679
+ for (let i = 0; i < node.namedChildCount; i++) {
3680
+ const c = node.namedChild(i);
3681
+ if (c && c.type !== "export_clause") {
3682
+ exportDefaultName = refFromExpr(c, "#default");
3683
+ break;
3684
+ }
3685
+ }
3686
+ return;
3687
+ }
3688
+ if (node.type === "assignment_expression") {
3689
+ const left = node.childForFieldName("left");
3690
+ const right = node.childForFieldName("right");
3691
+ if (left?.type !== "member_expression" || !right) return;
3692
+ const lobj = left.childForFieldName("object")?.text;
3693
+ const lprop = left.childForFieldName("property")?.text;
3694
+ if (lobj === "module" && lprop === "exports") exportDefaultName = refFromExpr(right, "#default");
3695
+ else if (lobj === "exports" && lprop) {
3696
+ const key = refFromExpr(right, `#exp:${lprop}`);
3697
+ if (key) exportNamed.set(lprop, key);
3698
+ }
3699
+ }
3700
+ });
3701
+ return {
3702
+ dir,
3703
+ routerVars,
3704
+ appVars,
3705
+ exportDefaultName,
3706
+ exportNamed,
3707
+ rawBindings: bindings,
3708
+ importedRouters: /* @__PURE__ */ new Map()
3709
+ };
3710
+ }
3711
+ async function expressMountPrefixes(files, serviceDir, tsPaths) {
3712
+ const jsParser = makeJsParser2();
3713
+ const fileInfo = /* @__PURE__ */ new Map();
3714
+ for (const f of files) {
3715
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path7.default.extname(f.path))) continue;
3716
+ if (isTestPath(f.path)) continue;
3717
+ const rel = toPosix(import_node_path7.default.relative(serviceDir, f.path));
3718
+ try {
3719
+ const tree = parseSource2(jsParser, f.content);
3720
+ fileInfo.set(rel, analyzeExpressFile(tree.rootNode, import_node_path7.default.dirname(f.path)));
3721
+ } catch {
3722
+ }
3723
+ }
3724
+ if (fileInfo.size === 0) return /* @__PURE__ */ new Map();
3725
+ for (const info of fileInfo.values()) {
3726
+ for (const b of info.rawBindings) {
3727
+ const resolved = await resolveJsImport(b.specifier, info.dir, serviceDir, tsPaths);
3728
+ if (!resolved || !fileInfo.has(resolved)) continue;
3729
+ info.importedRouters.set(b.local, { file: resolved, sel: b.sel === "namespace" ? "default" : b.sel });
3730
+ }
3731
+ }
3732
+ const resolveTarget = (name, file) => {
3733
+ const info = fileInfo.get(file);
3734
+ if (!info) return null;
3735
+ if (info.routerVars.has(name)) return { file, name };
3736
+ const imp = info.importedRouters.get(name);
3737
+ if (!imp) return null;
3738
+ const target = fileInfo.get(imp.file);
3739
+ if (!target) return null;
3740
+ const key = imp.sel === "default" ? target.exportDefaultName : target.exportNamed.get(imp.sel);
3741
+ if (!key) return null;
3742
+ return { file: imp.file, name: key };
3743
+ };
3744
+ const filePrefix = /* @__PURE__ */ new Map();
3745
+ const conflicted = /* @__PURE__ */ new Set();
3746
+ const apply = (file, prefix) => {
3747
+ if (conflicted.has(file)) return;
3748
+ const existing = filePrefix.get(file);
3749
+ if (existing === void 0) filePrefix.set(file, prefix);
3750
+ else if (existing !== prefix) {
3751
+ filePrefix.delete(file);
3752
+ conflicted.add(file);
3753
+ }
3754
+ };
3755
+ const visited = /* @__PURE__ */ new Set();
3756
+ const collect = (file, name, accPrefix) => {
3757
+ const key = `${file}|${name}|${accPrefix}`;
3758
+ if (visited.has(key)) return;
3759
+ visited.add(key);
3760
+ const info = fileInfo.get(file);
3761
+ const rv = info?.routerVars.get(name);
3762
+ if (!info || !rv) return;
3763
+ if (rv.declares && info.appVars.size === 0) apply(file, accPrefix);
3764
+ for (const m of rv.mounts) {
3765
+ if (!m.target) continue;
3766
+ const t = resolveTarget(m.target, file);
3767
+ if (t) collect(t.file, t.name, accPrefix + m.prefix);
3768
+ }
3769
+ if (rv.aliasOf) {
3770
+ const t = resolveTarget(rv.aliasOf, file);
3771
+ if (t) collect(t.file, t.name, accPrefix);
3772
+ }
3773
+ };
3774
+ for (const [rel, info] of fileInfo) {
3775
+ for (const appVar of info.appVars) collect(rel, appVar, "");
3776
+ }
3777
+ const out = /* @__PURE__ */ new Map();
3778
+ for (const [file, prefix] of filePrefix) if (prefix && prefix !== "/") out.set(file, prefix);
3779
+ return out;
3780
+ }
3086
3781
  async function addRoutes(graph, services) {
3087
- const jsParser = makeJsParser();
3088
- const pyParser = makePyParser();
3089
- const goParser = makeGoParser();
3782
+ const jsParser = makeJsParser2();
3783
+ const pyParser = makePyParser2();
3784
+ const goParser = makeGoParser2();
3090
3785
  let nodesAdded = 0;
3091
3786
  let edgesAdded = 0;
3092
3787
  for (const service of services) {
@@ -3106,13 +3801,14 @@ async function addRoutes(graph, services) {
3106
3801
  if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasNestjs && !hasFastapi && !hasFlask && !hasDjango && !hasGin)
3107
3802
  continue;
3108
3803
  const files = await loadSourceFiles(service.dir);
3804
+ const mountPrefixes = hasExpress ? await expressMountPrefixes(files, service.dir, await loadTsPathConfig(service.dir)) : /* @__PURE__ */ new Map();
3109
3805
  for (const file of files) {
3110
3806
  if (isTestPath(file.path)) continue;
3111
- const ext = import_node_path6.default.extname(file.path);
3807
+ const ext = import_node_path7.default.extname(file.path);
3112
3808
  const isPy = ext === ".py";
3113
3809
  const isGo = ext === ".go";
3114
3810
  if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy && !isGo) continue;
3115
- const relFile = toPosix(import_node_path6.default.relative(service.dir, file.path));
3811
+ const relFile = toPosix(import_node_path7.default.relative(service.dir, file.path));
3116
3812
  let routes;
3117
3813
  try {
3118
3814
  if (isGo) {
@@ -3134,16 +3830,18 @@ async function addRoutes(graph, services) {
3134
3830
  continue;
3135
3831
  }
3136
3832
  if (routes.length === 0) continue;
3833
+ const mountPrefix = mountPrefixes.get(relFile);
3137
3834
  for (const route of routes) {
3138
- const rid = (0, import_types5.routeId)(service.pkg.name, route.method, route.pathTemplate);
3835
+ const pathTemplate = mountPrefix ? canonicalizeTemplate(mountPrefix + route.pathTemplate) : route.pathTemplate;
3836
+ const rid = (0, import_types6.routeId)(service.pkg.name, route.method, pathTemplate);
3139
3837
  if (!graph.hasNode(rid)) {
3140
3838
  const node = {
3141
3839
  id: rid,
3142
- type: import_types5.NodeType.RouteNode,
3143
- name: `${route.method} ${route.pathTemplate}`,
3840
+ type: import_types6.NodeType.RouteNode,
3841
+ name: `${route.method} ${pathTemplate}`,
3144
3842
  service: service.pkg.name,
3145
3843
  method: route.method,
3146
- pathTemplate: route.pathTemplate,
3844
+ pathTemplate,
3147
3845
  path: relFile,
3148
3846
  line: route.line,
3149
3847
  framework: route.framework,
@@ -3152,15 +3850,15 @@ async function addRoutes(graph, services) {
3152
3850
  graph.addNode(rid, node);
3153
3851
  nodesAdded++;
3154
3852
  }
3155
- const containsId = (0, import_types5.extractedEdgeId)(service.node.id, rid, import_types5.EdgeType.CONTAINS);
3853
+ const containsId = (0, import_types6.extractedEdgeId)(service.node.id, rid, import_types6.EdgeType.CONTAINS);
3156
3854
  if (!graph.hasEdge(containsId)) {
3157
3855
  const edge = {
3158
3856
  id: containsId,
3159
3857
  source: service.node.id,
3160
3858
  target: rid,
3161
- type: import_types5.EdgeType.CONTAINS,
3162
- provenance: import_types5.Provenance.EXTRACTED,
3163
- confidence: (0, import_types5.confidenceForExtracted)("structural"),
3859
+ type: import_types6.EdgeType.CONTAINS,
3860
+ provenance: import_types6.Provenance.EXTRACTED,
3861
+ confidence: (0, import_types6.confidenceForExtracted)("structural"),
3164
3862
  evidence: {
3165
3863
  file: relFile,
3166
3864
  line: route.line,
@@ -3178,7 +3876,7 @@ async function addRoutes(graph, services) {
3178
3876
 
3179
3877
  // src/columns.ts
3180
3878
  init_cjs_shims();
3181
- var import_types6 = require("@neat.is/types");
3879
+ var import_types7 = require("@neat.is/types");
3182
3880
  var OBSERVED_COLUMN_CONFIDENCE = 0.9;
3183
3881
  function normalizeProvenances(provenances) {
3184
3882
  return [...new Set(provenances)].sort();
@@ -3206,10 +3904,10 @@ function foldColumns(existing, names, provenance, confidence) {
3206
3904
  return out;
3207
3905
  }
3208
3906
  function columnIsDeclared(col) {
3209
- return col.provenances.includes(import_types6.Provenance.EXTRACTED);
3907
+ return col.provenances.includes(import_types7.Provenance.EXTRACTED);
3210
3908
  }
3211
3909
  function columnIsObserved(col) {
3212
- return col.provenances.includes(import_types6.Provenance.OBSERVED);
3910
+ return col.provenances.includes(import_types7.Provenance.OBSERVED);
3213
3911
  }
3214
3912
 
3215
3913
  // src/ingest.ts
@@ -3432,7 +4130,7 @@ function languageForExt(relPath) {
3432
4130
  function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
3433
4131
  let p = toPosix2(filepath).replace(/^file:\/\//, "");
3434
4132
  if (scanPath && scanPath.length > 0) {
3435
- const absRoot = toPosix2(import_node_path7.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
4133
+ const absRoot = toPosix2(import_node_path8.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
3436
4134
  const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
3437
4135
  if (p.startsWith(anchor)) return p.slice(anchor.length);
3438
4136
  }
@@ -3460,10 +4158,10 @@ function resolveDistToSrc(absFilepath, line) {
3460
4158
  entry2 = null;
3461
4159
  const mapPath = `${absFilepath}.map`;
3462
4160
  try {
3463
- if ((0, import_node_fs6.existsSync)(mapPath)) {
3464
- const raw = JSON.parse((0, import_node_fs6.readFileSync)(mapPath, "utf8"));
4161
+ if ((0, import_node_fs7.existsSync)(mapPath)) {
4162
+ const raw = JSON.parse((0, import_node_fs7.readFileSync)(mapPath, "utf8"));
3465
4163
  const consumer = new sourceMapJs.SourceMapConsumer(raw);
3466
- entry2 = { consumer, dir: import_node_path7.default.dirname(mapPath) };
4164
+ entry2 = { consumer, dir: import_node_path8.default.dirname(mapPath) };
3467
4165
  }
3468
4166
  } catch {
3469
4167
  entry2 = null;
@@ -3478,7 +4176,7 @@ function resolveDistToSrc(absFilepath, line) {
3478
4176
  });
3479
4177
  if (!pos || !pos.source) return null;
3480
4178
  const root = entry2.consumer.sourceRoot ?? "";
3481
- const resolved = import_node_path7.default.resolve(entry2.dir, root, pos.source);
4179
+ const resolved = import_node_path8.default.resolve(entry2.dir, root, pos.source);
3482
4180
  return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
3483
4181
  } catch {
3484
4182
  return null;
@@ -3511,11 +4209,11 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
3511
4209
  };
3512
4210
  }
3513
4211
  function reconcileObservedRelPath(graph, serviceName, relPath) {
3514
- if (graph.hasNode((0, import_types7.fileId)(serviceName, relPath))) return relPath;
4212
+ if (graph.hasNode((0, import_types8.fileId)(serviceName, relPath))) return relPath;
3515
4213
  let best = null;
3516
4214
  graph.forEachNode((_id, attrs) => {
3517
4215
  const a = attrs;
3518
- if (a.type !== import_types7.NodeType.FileNode || a.service !== serviceName) return;
4216
+ if (a.type !== import_types8.NodeType.FileNode || a.service !== serviceName) return;
3519
4217
  if (a.discoveredVia === "otel") return;
3520
4218
  const p = a.path;
3521
4219
  if (!p) return;
@@ -3527,14 +4225,14 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
3527
4225
  }
3528
4226
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3529
4227
  const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
3530
- const canonicalService = svcAttrs && svcAttrs.type === import_types7.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
4228
+ const canonicalService = svcAttrs && svcAttrs.type === import_types8.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
3531
4229
  const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
3532
- const fileNodeId = (0, import_types7.fileId)(canonicalService, relPath);
4230
+ const fileNodeId = (0, import_types8.fileId)(canonicalService, relPath);
3533
4231
  if (!graph.hasNode(fileNodeId)) {
3534
4232
  const language = languageForExt(relPath);
3535
4233
  const node = {
3536
4234
  id: fileNodeId,
3537
- type: import_types7.NodeType.FileNode,
4235
+ type: import_types8.NodeType.FileNode,
3538
4236
  service: canonicalService,
3539
4237
  path: relPath,
3540
4238
  ...language ? { language } : {},
@@ -3543,14 +4241,14 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3543
4241
  };
3544
4242
  graph.addNode(fileNodeId, node);
3545
4243
  }
3546
- const containsId = makeObservedEdgeId(import_types7.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
4244
+ const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
3547
4245
  if (!graph.hasEdge(containsId)) {
3548
4246
  const edge = {
3549
4247
  id: containsId,
3550
4248
  source: serviceNodeId,
3551
4249
  target: fileNodeId,
3552
- type: import_types7.EdgeType.CONTAINS,
3553
- provenance: import_types7.Provenance.OBSERVED
4250
+ type: import_types8.EdgeType.CONTAINS,
4251
+ provenance: import_types8.Provenance.OBSERVED
3554
4252
  };
3555
4253
  graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3556
4254
  }
@@ -3574,11 +4272,11 @@ function pickContainingSymbol(candidates, fn) {
3574
4272
  return [...candidates].sort(bySpan)[0].id;
3575
4273
  }
3576
4274
  function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line) {
3577
- const sid = (0, import_types7.symbolId)(service, relPath, fn);
4275
+ const sid = (0, import_types8.symbolId)(service, relPath, fn);
3578
4276
  if (!graph.hasNode(sid)) {
3579
4277
  const node = {
3580
4278
  id: sid,
3581
- type: import_types7.NodeType.SymbolNode,
4279
+ type: import_types8.NodeType.SymbolNode,
3582
4280
  kind: "function",
3583
4281
  qualname: fn,
3584
4282
  span: { startLine: line, endLine: line },
@@ -3588,14 +4286,14 @@ function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line)
3588
4286
  };
3589
4287
  graph.addNode(sid, node);
3590
4288
  }
3591
- const containsId = makeObservedEdgeId(import_types7.EdgeType.CONTAINS, fileNodeId, sid);
4289
+ const containsId = makeObservedEdgeId(import_types8.EdgeType.CONTAINS, fileNodeId, sid);
3592
4290
  if (!graph.hasEdge(containsId)) {
3593
4291
  const edge = {
3594
4292
  id: containsId,
3595
4293
  source: fileNodeId,
3596
4294
  target: sid,
3597
- type: import_types7.EdgeType.CONTAINS,
3598
- provenance: import_types7.Provenance.OBSERVED
4295
+ type: import_types8.EdgeType.CONTAINS,
4296
+ provenance: import_types8.Provenance.OBSERVED
3599
4297
  };
3600
4298
  graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
3601
4299
  }
@@ -3607,9 +4305,9 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
3607
4305
  let sawSymbol = false;
3608
4306
  const candidates = [];
3609
4307
  graph.forEachOutboundEdge(fileNodeId, (_edge, edgeAttrs, _source, target) => {
3610
- if (edgeAttrs.type !== import_types7.EdgeType.CONTAINS) return;
4308
+ if (edgeAttrs.type !== import_types8.EdgeType.CONTAINS) return;
3611
4309
  const t = graph.getNodeAttributes(target);
3612
- if (t.type !== import_types7.NodeType.SymbolNode) return;
4310
+ if (t.type !== import_types8.NodeType.SymbolNode) return;
3613
4311
  sawSymbol = true;
3614
4312
  if (line >= t.span.startLine && line <= t.span.endLine) {
3615
4313
  candidates.push({ id: target, symbol: t });
@@ -3622,17 +4320,17 @@ function landObservedSymbol(graph, fileNodeId, service, relPath, callSite) {
3622
4320
  return fileNodeId;
3623
4321
  }
3624
4322
  function makeObservedEdgeId(type, source, target) {
3625
- return (0, import_types7.observedEdgeId)(source, target, type);
4323
+ return (0, import_types8.observedEdgeId)(source, target, type);
3626
4324
  }
3627
4325
  function makeInferredEdgeId(type, source, target) {
3628
- return (0, import_types7.inferredEdgeId)(source, target, type);
4326
+ return (0, import_types8.inferredEdgeId)(source, target, type);
3629
4327
  }
3630
4328
  var INFERRED_CONFIDENCE = 0.6;
3631
4329
  var STITCH_MAX_DEPTH = 2;
3632
4330
  var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
3633
- import_types7.EdgeType.CALLS,
3634
- import_types7.EdgeType.CONNECTS_TO,
3635
- import_types7.EdgeType.DEPENDS_ON
4331
+ import_types8.EdgeType.CALLS,
4332
+ import_types8.EdgeType.CONNECTS_TO,
4333
+ import_types8.EdgeType.DEPENDS_ON
3636
4334
  ]);
3637
4335
  var WIRE_SPAN_KIND_CLIENT = 3;
3638
4336
  var WIRE_SPAN_KIND_PRODUCER = 4;
@@ -3648,11 +4346,11 @@ function spanServesGraphqlOperation(kind) {
3648
4346
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3649
4347
  }
3650
4348
  function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
3651
- const id = (0, import_types7.graphqlOperationId)(serviceName, operationType, operationName);
4349
+ const id = (0, import_types8.graphqlOperationId)(serviceName, operationType, operationName);
3652
4350
  if (graph.hasNode(id)) return id;
3653
4351
  const node = {
3654
4352
  id,
3655
- type: import_types7.NodeType.GraphQLOperationNode,
4353
+ type: import_types8.NodeType.GraphQLOperationNode,
3656
4354
  name: operationName,
3657
4355
  service: serviceName,
3658
4356
  operationType: operationType.toLowerCase(),
@@ -3666,11 +4364,11 @@ function spanServesGrpcMethod(kind) {
3666
4364
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3667
4365
  }
3668
4366
  function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
3669
- const id = (0, import_types7.grpcMethodId)(rpcService, rpcMethod);
4367
+ const id = (0, import_types8.grpcMethodId)(rpcService, rpcMethod);
3670
4368
  if (graph.hasNode(id)) return id;
3671
4369
  const node = {
3672
4370
  id,
3673
- type: import_types7.NodeType.GrpcMethodNode,
4371
+ type: import_types8.NodeType.GrpcMethodNode,
3674
4372
  name: `${rpcService}/${rpcMethod}`,
3675
4373
  rpcService,
3676
4374
  rpcMethod,
@@ -3683,11 +4381,11 @@ function spanServesWebsocketChannel(kind) {
3683
4381
  return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
3684
4382
  }
3685
4383
  function ensureWebsocketChannelNode(graph, serviceName, channel) {
3686
- const id = (0, import_types7.websocketChannelId)(serviceName, channel);
4384
+ const id = (0, import_types8.websocketChannelId)(serviceName, channel);
3687
4385
  if (graph.hasNode(id)) return id;
3688
4386
  const node = {
3689
4387
  id,
3690
- type: import_types7.NodeType.WebSocketChannelNode,
4388
+ type: import_types8.NodeType.WebSocketChannelNode,
3691
4389
  name: channel,
3692
4390
  service: serviceName,
3693
4391
  channel,
@@ -3700,11 +4398,11 @@ function messagingDestinationKind(system) {
3700
4398
  return `${system}-topic`;
3701
4399
  }
3702
4400
  function ensureMessagingDestinationNode(graph, system, destination) {
3703
- const id = (0, import_types7.infraId)(messagingDestinationKind(system), destination);
4401
+ const id = (0, import_types8.infraId)(messagingDestinationKind(system), destination);
3704
4402
  if (graph.hasNode(id)) return id;
3705
4403
  const node = {
3706
4404
  id,
3707
- type: import_types7.NodeType.InfraNode,
4405
+ type: import_types8.NodeType.InfraNode,
3708
4406
  name: destination,
3709
4407
  provider: "self",
3710
4408
  kind: messagingDestinationKind(system)
@@ -3748,9 +4446,9 @@ function lookupParentSpan(traceId, parentSpanId, now) {
3748
4446
  };
3749
4447
  }
3750
4448
  function resolveServiceId(graph, host, env) {
3751
- const envTagged = (0, import_types7.serviceId)(host, env);
4449
+ const envTagged = (0, import_types8.serviceId)(host, env);
3752
4450
  if (graph.hasNode(envTagged)) return envTagged;
3753
- const envLess = (0, import_types7.serviceId)(host);
4451
+ const envLess = (0, import_types8.serviceId)(host);
3754
4452
  if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
3755
4453
  let sameEnv = null;
3756
4454
  let envLessMatch = null;
@@ -3758,7 +4456,7 @@ function resolveServiceId(graph, host, env) {
3758
4456
  graph.forEachNode((id, attrs) => {
3759
4457
  if (sameEnv) return;
3760
4458
  const a = attrs;
3761
- if (a.type !== import_types7.NodeType.ServiceNode) return;
4459
+ if (a.type !== import_types8.NodeType.ServiceNode) return;
3762
4460
  const matchesByName = a.name === host;
3763
4461
  const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
3764
4462
  if (!matchesByName && !matchesByAlias) return;
@@ -3773,14 +4471,14 @@ function resolveServiceId(graph, host, env) {
3773
4471
  return sameEnv ?? envLessMatch ?? anyMatch;
3774
4472
  }
3775
4473
  function frontierIdFor(host) {
3776
- return (0, import_types7.frontierId)(host);
4474
+ return (0, import_types8.frontierId)(host);
3777
4475
  }
3778
4476
  function ensureServiceNode(graph, serviceName, env) {
3779
- const id = (0, import_types7.serviceId)(serviceName, env);
4477
+ const id = (0, import_types8.serviceId)(serviceName, env);
3780
4478
  if (graph.hasNode(id)) return id;
3781
4479
  const wanted = serviceName.toLowerCase();
3782
4480
  const extractedId = graph.findNode((_nid, attrs) => {
3783
- if (attrs.type !== import_types7.NodeType.ServiceNode) return false;
4481
+ if (attrs.type !== import_types8.NodeType.ServiceNode) return false;
3784
4482
  const svc = attrs;
3785
4483
  if (svc.discoveredVia === "otel") return false;
3786
4484
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
@@ -3788,7 +4486,7 @@ function ensureServiceNode(graph, serviceName, env) {
3788
4486
  if (extractedId) return extractedId;
3789
4487
  const node = {
3790
4488
  id,
3791
- type: import_types7.NodeType.ServiceNode,
4489
+ type: import_types8.NodeType.ServiceNode,
3792
4490
  name: serviceName,
3793
4491
  language: "unknown",
3794
4492
  discoveredVia: "otel",
@@ -3798,11 +4496,11 @@ function ensureServiceNode(graph, serviceName, env) {
3798
4496
  return id;
3799
4497
  }
3800
4498
  function ensureInfraNode(graph, kind, name, provider) {
3801
- const id = (0, import_types7.infraId)(kind, name);
4499
+ const id = (0, import_types8.infraId)(kind, name);
3802
4500
  if (graph.hasNode(id)) return id;
3803
4501
  const node = {
3804
4502
  id,
3805
- type: import_types7.NodeType.InfraNode,
4503
+ type: import_types8.NodeType.InfraNode,
3806
4504
  name,
3807
4505
  provider,
3808
4506
  kind
@@ -3814,7 +4512,7 @@ var COLUMN_BEARING_INFRA_KINDS = /* @__PURE__ */ new Set(["sql-table", "supabase
3814
4512
  function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
3815
4513
  if (!columns || columns.length === 0 || !graph.hasNode(tableNodeId)) return;
3816
4514
  const node = graph.getNodeAttributes(tableNodeId);
3817
- if (node.type !== import_types7.NodeType.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
4515
+ if (node.type !== import_types8.NodeType.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
3818
4516
  return;
3819
4517
  }
3820
4518
  graph.replaceNodeAttributes(tableNodeId, {
@@ -3823,14 +4521,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
3823
4521
  });
3824
4522
  }
3825
4523
  function mergeObservedColumns(graph, tableNodeId, columns) {
3826
- mergeColumnsAt(graph, tableNodeId, columns, import_types7.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
4524
+ mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
3827
4525
  }
3828
4526
  function ensureDatabaseNode(graph, host, engine) {
3829
- const id = (0, import_types7.databaseId)(host);
4527
+ const id = (0, import_types8.databaseId)(host);
3830
4528
  if (graph.hasNode(id)) return id;
3831
4529
  const node = {
3832
4530
  id,
3833
- type: import_types7.NodeType.DatabaseNode,
4531
+ type: import_types8.NodeType.DatabaseNode,
3834
4532
  name: host,
3835
4533
  engine,
3836
4534
  engineVersion: "unknown",
@@ -3842,11 +4540,11 @@ function ensureDatabaseNode(graph, host, engine) {
3842
4540
  return id;
3843
4541
  }
3844
4542
  function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
3845
- const id = (0, import_types7.localDatabaseId)(serviceName, name);
4543
+ const id = (0, import_types8.localDatabaseId)(serviceName, name);
3846
4544
  if (graph.hasNode(id)) return id;
3847
4545
  const node = {
3848
4546
  id,
3849
- type: import_types7.NodeType.DatabaseNode,
4547
+ type: import_types8.NodeType.DatabaseNode,
3850
4548
  name,
3851
4549
  engine,
3852
4550
  engineVersion: "unknown",
@@ -3861,17 +4559,17 @@ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
3861
4559
  const sources = [serviceNodeId];
3862
4560
  for (const edgeId of graph.outboundEdges(serviceNodeId)) {
3863
4561
  const e = graph.getEdgeAttributes(edgeId);
3864
- if (e.type === import_types7.EdgeType.CONTAINS) sources.push(e.target);
4562
+ if (e.type === import_types8.EdgeType.CONTAINS) sources.push(e.target);
3865
4563
  }
3866
4564
  const matches = /* @__PURE__ */ new Set();
3867
4565
  for (const src of sources) {
3868
4566
  if (!graph.hasNode(src)) continue;
3869
4567
  for (const edgeId of graph.outboundEdges(src)) {
3870
4568
  const edge = graph.getEdgeAttributes(edgeId);
3871
- if (edge.type !== import_types7.EdgeType.CONNECTS_TO || edge.provenance !== import_types7.Provenance.EXTRACTED) continue;
4569
+ if (edge.type !== import_types8.EdgeType.CONNECTS_TO || edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
3872
4570
  if (!graph.hasNode(edge.target)) continue;
3873
4571
  const target = graph.getNodeAttributes(edge.target);
3874
- if (target.type !== import_types7.NodeType.DatabaseNode || target.engine !== engine) continue;
4572
+ if (target.type !== import_types8.NodeType.DatabaseNode || target.engine !== engine) continue;
3875
4573
  matches.add(edge.target);
3876
4574
  }
3877
4575
  }
@@ -3886,7 +4584,7 @@ function ensureFrontierNode(graph, host, ts) {
3886
4584
  }
3887
4585
  const node = {
3888
4586
  id,
3889
- type: import_types7.NodeType.FrontierNode,
4587
+ type: import_types8.NodeType.FrontierNode,
3890
4588
  name: host,
3891
4589
  host,
3892
4590
  firstObserved: ts,
@@ -3910,11 +4608,11 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
3910
4608
  };
3911
4609
  const updated = {
3912
4610
  ...existing,
3913
- provenance: import_types7.Provenance.OBSERVED,
4611
+ provenance: import_types8.Provenance.OBSERVED,
3914
4612
  lastObserved: ts,
3915
4613
  callCount: newSpanCount,
3916
4614
  signal: newSignal,
3917
- confidence: (0, import_types7.confidenceForObservedSignal)(newSignal),
4615
+ confidence: (0, import_types8.confidenceForObservedSignal)(newSignal),
3918
4616
  grain
3919
4617
  // backfills legacy edges that predate ADR-142
3920
4618
  };
@@ -3931,8 +4629,8 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
3931
4629
  source,
3932
4630
  target,
3933
4631
  type,
3934
- provenance: import_types7.Provenance.OBSERVED,
3935
- confidence: (0, import_types7.confidenceForObservedSignal)(signal),
4632
+ provenance: import_types8.Provenance.OBSERVED,
4633
+ confidence: (0, import_types8.confidenceForObservedSignal)(signal),
3936
4634
  lastObserved: ts,
3937
4635
  callCount: 1,
3938
4636
  signal,
@@ -3954,9 +4652,9 @@ function stitchTrace(graph, sourceServiceId, ts) {
3954
4652
  const outbound = graph.outboundEdges(nodeId);
3955
4653
  for (const edgeId of outbound) {
3956
4654
  const edge = graph.getEdgeAttributes(edgeId);
3957
- if (edge.provenance !== import_types7.Provenance.EXTRACTED) continue;
4655
+ if (edge.provenance !== import_types8.Provenance.EXTRACTED) continue;
3958
4656
  if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
3959
- if (graph.hasEdge((0, import_types7.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
4657
+ if (graph.hasEdge((0, import_types8.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
3960
4658
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
3961
4659
  if (!visited.has(edge.target)) {
3962
4660
  visited.add(edge.target);
@@ -3978,23 +4676,23 @@ function upsertInferredEdge(graph, type, source, target, ts) {
3978
4676
  source,
3979
4677
  target,
3980
4678
  type,
3981
- provenance: import_types7.Provenance.INFERRED,
4679
+ provenance: import_types8.Provenance.INFERRED,
3982
4680
  confidence: INFERRED_CONFIDENCE,
3983
4681
  lastObserved: ts
3984
4682
  };
3985
4683
  graph.addEdgeWithKey(id, source, target, edge);
3986
4684
  }
3987
4685
  async function appendErrorEvent(ctx, ev) {
3988
- await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(ctx.errorsPath), { recursive: true });
3989
- await import_node_fs6.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
4686
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
4687
+ await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
3990
4688
  }
3991
4689
  function incidentAffectedNode(span, graph, scanPath) {
3992
- const sid = (0, import_types7.serviceId)(span.service, span.env);
4690
+ const sid = (0, import_types8.serviceId)(span.service, span.env);
3993
4691
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
3994
4692
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
3995
4693
  if (callSite) {
3996
4694
  const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
3997
- return (0, import_types7.fileId)(span.service, relPath);
4695
+ return (0, import_types8.fileId)(span.service, relPath);
3998
4696
  }
3999
4697
  return sid;
4000
4698
  }
@@ -4027,8 +4725,8 @@ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
4027
4725
  return async (span) => {
4028
4726
  const ev = buildErrorEventForReceiver(span, graph, scanPath);
4029
4727
  if (!ev) return;
4030
- await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
4031
- await import_node_fs6.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4728
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
4729
+ await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4032
4730
  };
4033
4731
  }
4034
4732
  async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
@@ -4119,7 +4817,7 @@ function findRouteNodeByHttpRoute(graph, serviceName, method, httpRoute) {
4119
4817
  graph.forEachNode((id, attrs) => {
4120
4818
  if (found) return;
4121
4819
  const a = attrs;
4122
- if (a.type !== import_types7.NodeType.RouteNode || a.service !== serviceName) return;
4820
+ if (a.type !== import_types8.NodeType.RouteNode || a.service !== serviceName) return;
4123
4821
  if (m && a.method !== "ALL" && a.method !== m) return;
4124
4822
  if (normalizePathTemplate(a.pathTemplate) === target) found = id;
4125
4823
  });
@@ -4150,7 +4848,7 @@ async function handleSpan(ctx, span) {
4150
4848
  let targetId;
4151
4849
  if (host) {
4152
4850
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
4153
- targetId = (0, import_types7.databaseId)(host);
4851
+ targetId = (0, import_types8.databaseId)(host);
4154
4852
  } else {
4155
4853
  const declared = findDeclaredDatabaseForService(ctx.graph, sourceId, span.dbSystem);
4156
4854
  if (declared) {
@@ -4167,7 +4865,7 @@ async function handleSpan(ctx, span) {
4167
4865
  }
4168
4866
  const result = upsertObservedEdge(
4169
4867
  ctx.graph,
4170
- import_types7.EdgeType.CONNECTS_TO,
4868
+ import_types8.EdgeType.CONNECTS_TO,
4171
4869
  observedSource(),
4172
4870
  targetId,
4173
4871
  ts,
@@ -4179,7 +4877,7 @@ async function handleSpan(ctx, span) {
4179
4877
  const collectionId = ensureInfraNode(ctx.graph, "mongodb-collection", span.dbCollection, "self");
4180
4878
  upsertObservedEdge(
4181
4879
  ctx.graph,
4182
- import_types7.EdgeType.CALLS,
4880
+ import_types8.EdgeType.CALLS,
4183
4881
  observedSource(),
4184
4882
  collectionId,
4185
4883
  ts,
@@ -4191,7 +4889,7 @@ async function handleSpan(ctx, span) {
4191
4889
  const tableId = ensureInfraNode(ctx.graph, "sql-table", span.dbTable, "self");
4192
4890
  upsertObservedEdge(
4193
4891
  ctx.graph,
4194
- import_types7.EdgeType.CALLS,
4892
+ import_types8.EdgeType.CALLS,
4195
4893
  observedSource(),
4196
4894
  tableId,
4197
4895
  ts,
@@ -4207,7 +4905,7 @@ async function handleSpan(ctx, span) {
4207
4905
  span.messagingSystem,
4208
4906
  span.messagingDestination
4209
4907
  );
4210
- const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types7.EdgeType.CONSUMES_FROM : import_types7.EdgeType.PUBLISHES_TO;
4908
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types8.EdgeType.CONSUMES_FROM : import_types8.EdgeType.PUBLISHES_TO;
4211
4909
  const result = upsertObservedEdge(
4212
4910
  ctx.graph,
4213
4911
  edgeType,
@@ -4227,7 +4925,7 @@ async function handleSpan(ctx, span) {
4227
4925
  );
4228
4926
  const result = upsertObservedEdge(
4229
4927
  ctx.graph,
4230
- import_types7.EdgeType.CONTAINS,
4928
+ import_types8.EdgeType.CONTAINS,
4231
4929
  observedSource(),
4232
4930
  targetId,
4233
4931
  ts,
@@ -4239,7 +4937,7 @@ async function handleSpan(ctx, span) {
4239
4937
  const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
4240
4938
  const result = upsertObservedEdge(
4241
4939
  ctx.graph,
4242
- import_types7.EdgeType.CONTAINS,
4940
+ import_types8.EdgeType.CONTAINS,
4243
4941
  observedSource(),
4244
4942
  targetId,
4245
4943
  ts,
@@ -4255,7 +4953,7 @@ async function handleSpan(ctx, span) {
4255
4953
  );
4256
4954
  const result = upsertObservedEdge(
4257
4955
  ctx.graph,
4258
- import_types7.EdgeType.CONNECTS_TO,
4956
+ import_types8.EdgeType.CONNECTS_TO,
4259
4957
  observedSource(),
4260
4958
  targetId,
4261
4959
  ts,
@@ -4271,7 +4969,7 @@ async function handleSpan(ctx, span) {
4271
4969
  if (targetId && targetId !== sourceId) {
4272
4970
  upsertObservedEdge(
4273
4971
  ctx.graph,
4274
- import_types7.EdgeType.CALLS,
4972
+ import_types8.EdgeType.CALLS,
4275
4973
  observedSource(),
4276
4974
  targetId,
4277
4975
  ts,
@@ -4284,7 +4982,7 @@ async function handleSpan(ctx, span) {
4284
4982
  const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
4285
4983
  upsertObservedEdge(
4286
4984
  ctx.graph,
4287
- import_types7.EdgeType.CALLS,
4985
+ import_types8.EdgeType.CALLS,
4288
4986
  observedSource(),
4289
4987
  frontierNodeId,
4290
4988
  ts,
@@ -4310,7 +5008,7 @@ async function handleSpan(ctx, span) {
4310
5008
  } : void 0;
4311
5009
  upsertObservedEdge(
4312
5010
  ctx.graph,
4313
- import_types7.EdgeType.CALLS,
5011
+ import_types8.EdgeType.CALLS,
4314
5012
  fallbackSource,
4315
5013
  sourceId,
4316
5014
  ts,
@@ -4329,7 +5027,7 @@ async function handleSpan(ctx, span) {
4329
5027
  );
4330
5028
  if (routeNodeId) {
4331
5029
  const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
4332
- upsertObservedEdge(ctx.graph, import_types7.EdgeType.CONTAINS, (0, import_types7.serviceId)(routeSvc), routeNodeId, ts, isError);
5030
+ upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
4333
5031
  }
4334
5032
  }
4335
5033
  if (span.statusCode === 2) {
@@ -4368,7 +5066,7 @@ function promoteFrontierNodes(graph, opts = {}) {
4368
5066
  const aliasIndex = /* @__PURE__ */ new Map();
4369
5067
  graph.forEachNode((id, attrs) => {
4370
5068
  const a = attrs;
4371
- if (a.type !== import_types7.NodeType.ServiceNode) return;
5069
+ if (a.type !== import_types8.NodeType.ServiceNode) return;
4372
5070
  aliasIndex.set(a.name, id);
4373
5071
  if (a.aliases) {
4374
5072
  for (const alias of a.aliases) aliasIndex.set(alias, id);
@@ -4377,7 +5075,7 @@ function promoteFrontierNodes(graph, opts = {}) {
4377
5075
  const toPromote = [];
4378
5076
  graph.forEachNode((id, attrs) => {
4379
5077
  const a = attrs;
4380
- if (a.type !== import_types7.NodeType.FrontierNode) return;
5078
+ if (a.type !== import_types8.NodeType.FrontierNode) return;
4381
5079
  const target = aliasIndex.get(a.host);
4382
5080
  if (!target) return;
4383
5081
  if (target === id) return;
@@ -4411,7 +5109,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId7) {
4411
5109
  }
4412
5110
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
4413
5111
  graph.dropEdge(oldEdgeId);
4414
- 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);
5112
+ 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);
4415
5113
  if (graph.hasEdge(newId)) {
4416
5114
  const existing = graph.getEdgeAttributes(newId);
4417
5115
  const merged = {
@@ -4442,12 +5140,12 @@ async function markStaleEdges(graph, options = {}) {
4442
5140
  const project = options.project ?? DEFAULT_PROJECT;
4443
5141
  graph.forEachEdge((id, attrs) => {
4444
5142
  const e = attrs;
4445
- if (e.provenance !== import_types7.Provenance.OBSERVED) return;
5143
+ if (e.provenance !== import_types8.Provenance.OBSERVED) return;
4446
5144
  if (!e.lastObserved) return;
4447
5145
  const threshold = thresholdForEdgeType(e.type, thresholds);
4448
5146
  const age = now - new Date(e.lastObserved).getTime();
4449
5147
  if (age > threshold) {
4450
- const updated = { ...e, provenance: import_types7.Provenance.STALE, confidence: 0.3 };
5148
+ const updated = { ...e, provenance: import_types8.Provenance.STALE, confidence: 0.3 };
4451
5149
  graph.replaceEdgeAttributes(id, updated);
4452
5150
  events.push({
4453
5151
  edgeId: id,
@@ -4464,8 +5162,8 @@ async function markStaleEdges(graph, options = {}) {
4464
5162
  project,
4465
5163
  payload: {
4466
5164
  edgeId: id,
4467
- from: import_types7.Provenance.OBSERVED,
4468
- to: import_types7.Provenance.STALE
5165
+ from: import_types8.Provenance.OBSERVED,
5166
+ to: import_types8.Provenance.STALE
4469
5167
  }
4470
5168
  });
4471
5169
  }
@@ -4476,13 +5174,13 @@ async function markStaleEdges(graph, options = {}) {
4476
5174
  return { count: events.length, events };
4477
5175
  }
4478
5176
  async function appendStaleEvents(staleEventsPath, events) {
4479
- await import_node_fs6.promises.mkdir(import_node_path7.default.dirname(staleEventsPath), { recursive: true });
5177
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(staleEventsPath), { recursive: true });
4480
5178
  const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
4481
- await import_node_fs6.promises.appendFile(staleEventsPath, lines, "utf8");
5179
+ await import_node_fs7.promises.appendFile(staleEventsPath, lines, "utf8");
4482
5180
  }
4483
5181
  async function readStaleEvents(staleEventsPath) {
4484
5182
  try {
4485
- const raw = await import_node_fs6.promises.readFile(staleEventsPath, "utf8");
5183
+ const raw = await import_node_fs7.promises.readFile(staleEventsPath, "utf8");
4486
5184
  return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
4487
5185
  } catch (err) {
4488
5186
  if (err.code === "ENOENT") return [];
@@ -4516,7 +5214,7 @@ function startStalenessLoop(graph, options = {}) {
4516
5214
  }
4517
5215
  async function readErrorEvents(errorsPath) {
4518
5216
  try {
4519
- const raw = await import_node_fs6.promises.readFile(errorsPath, "utf8");
5217
+ const raw = await import_node_fs7.promises.readFile(errorsPath, "utf8");
4520
5218
  const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
4521
5219
  return dedupeIncidents(events);
4522
5220
  } catch (err) {
@@ -4574,7 +5272,7 @@ function mergeSnapshot(graph, snapshot) {
4574
5272
  const validEdges = [];
4575
5273
  for (const node of incomingNodes) {
4576
5274
  if (node.attributes === void 0) continue;
4577
- const parsed = import_types7.GraphNodeSchema.safeParse(node.attributes);
5275
+ const parsed = import_types8.GraphNodeSchema.safeParse(node.attributes);
4578
5276
  if (!parsed.success) {
4579
5277
  issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
4580
5278
  continue;
@@ -4583,7 +5281,7 @@ function mergeSnapshot(graph, snapshot) {
4583
5281
  }
4584
5282
  for (const edge of incomingEdges) {
4585
5283
  if (edge.attributes === void 0) continue;
4586
- const parsed = import_types7.GraphEdgeSchema.safeParse(edge.attributes);
5284
+ const parsed = import_types8.GraphEdgeSchema.safeParse(edge.attributes);
4587
5285
  if (!parsed.success) {
4588
5286
  const label = edge.key ?? `${edge.source}->${edge.target}`;
4589
5287
  issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
@@ -4613,16 +5311,16 @@ function mergeSnapshot(graph, snapshot) {
4613
5311
 
4614
5312
  // src/extract/services.ts
4615
5313
  init_cjs_shims();
4616
- var import_node_fs10 = require("fs");
4617
- var import_node_path11 = __toESM(require("path"), 1);
5314
+ var import_node_fs11 = require("fs");
5315
+ var import_node_path12 = __toESM(require("path"), 1);
4618
5316
  var import_ignore = __toESM(require("ignore"), 1);
4619
5317
  var import_minimatch2 = require("minimatch");
4620
- var import_types9 = require("@neat.is/types");
5318
+ var import_types10 = require("@neat.is/types");
4621
5319
 
4622
5320
  // src/extract/python.ts
4623
5321
  init_cjs_shims();
4624
- var import_node_fs7 = require("fs");
4625
- var import_node_path8 = __toESM(require("path"), 1);
5322
+ var import_node_fs8 = require("fs");
5323
+ var import_node_path9 = __toESM(require("path"), 1);
4626
5324
  var import_smol_toml = require("smol-toml");
4627
5325
  var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
4628
5326
  function parseRequirementsTxt(content) {
@@ -4655,25 +5353,25 @@ function depsFromPyProject(pyproject) {
4655
5353
  return out;
4656
5354
  }
4657
5355
  async function discoverPythonService(serviceDir) {
4658
- const pyprojectPath = import_node_path8.default.join(serviceDir, "pyproject.toml");
4659
- const requirementsPath = import_node_path8.default.join(serviceDir, "requirements.txt");
4660
- const setupPath = import_node_path8.default.join(serviceDir, "setup.py");
5356
+ const pyprojectPath = import_node_path9.default.join(serviceDir, "pyproject.toml");
5357
+ const requirementsPath = import_node_path9.default.join(serviceDir, "requirements.txt");
5358
+ const setupPath = import_node_path9.default.join(serviceDir, "setup.py");
4661
5359
  const hasPyproject = await exists(pyprojectPath);
4662
5360
  const hasRequirements = await exists(requirementsPath);
4663
5361
  const hasSetup = await exists(setupPath);
4664
5362
  if (!hasPyproject && !hasRequirements && !hasSetup) return null;
4665
- let name = import_node_path8.default.basename(serviceDir);
5363
+ let name = import_node_path9.default.basename(serviceDir);
4666
5364
  let version;
4667
5365
  const dependencies = {};
4668
5366
  if (hasPyproject) {
4669
- const raw = await import_node_fs7.promises.readFile(pyprojectPath, "utf8");
5367
+ const raw = await import_node_fs8.promises.readFile(pyprojectPath, "utf8");
4670
5368
  const pyproject = (0, import_smol_toml.parse)(raw);
4671
5369
  name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
4672
5370
  version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
4673
5371
  Object.assign(dependencies, depsFromPyProject(pyproject));
4674
5372
  }
4675
5373
  if (hasRequirements) {
4676
- const raw = await import_node_fs7.promises.readFile(requirementsPath, "utf8");
5374
+ const raw = await import_node_fs8.promises.readFile(requirementsPath, "utf8");
4677
5375
  Object.assign(dependencies, parseRequirementsTxt(raw));
4678
5376
  }
4679
5377
  return { name, version, dependencies };
@@ -4688,9 +5386,9 @@ function pythonToPackage(service) {
4688
5386
 
4689
5387
  // src/extract/go.ts
4690
5388
  init_cjs_shims();
4691
- var import_node_fs8 = require("fs");
4692
- var import_node_path9 = __toESM(require("path"), 1);
4693
- var import_types8 = require("@neat.is/types");
5389
+ var import_node_fs9 = require("fs");
5390
+ var import_node_path10 = __toESM(require("path"), 1);
5391
+ var import_types9 = require("@neat.is/types");
4694
5392
  function parseGoMod(source) {
4695
5393
  const module2 = source.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
4696
5394
  if (!module2) return null;
@@ -4709,7 +5407,7 @@ function parseGoMod(source) {
4709
5407
  async function discoverGoService(scanPath, dir) {
4710
5408
  let raw;
4711
5409
  try {
4712
- raw = await import_node_fs8.promises.readFile(import_node_path9.default.join(dir, "go.mod"), "utf8");
5410
+ raw = await import_node_fs9.promises.readFile(import_node_path10.default.join(dir, "go.mod"), "utf8");
4713
5411
  } catch {
4714
5412
  return null;
4715
5413
  }
@@ -4718,12 +5416,12 @@ async function discoverGoService(scanPath, dir) {
4718
5416
  const name = mod.module.split("/").filter(Boolean).pop() ?? mod.module;
4719
5417
  const pkg = { name, dependencies: mod.dependencies };
4720
5418
  const node = {
4721
- id: (0, import_types8.serviceId)(name),
4722
- type: import_types8.NodeType.ServiceNode,
5419
+ id: (0, import_types9.serviceId)(name),
5420
+ type: import_types9.NodeType.ServiceNode,
4723
5421
  name,
4724
5422
  language: "go",
4725
5423
  dependencies: mod.dependencies,
4726
- repoPath: import_node_path9.default.relative(scanPath, dir),
5424
+ repoPath: import_node_path10.default.relative(scanPath, dir),
4727
5425
  ...mod.dependencies["github.com/gin-gonic/gin"] ? { framework: "gin" } : {}
4728
5426
  };
4729
5427
  return { pkg, dir, node };
@@ -4731,17 +5429,17 @@ async function discoverGoService(scanPath, dir) {
4731
5429
 
4732
5430
  // src/extract/owners.ts
4733
5431
  init_cjs_shims();
4734
- var import_node_fs9 = require("fs");
4735
- var import_node_path10 = __toESM(require("path"), 1);
5432
+ var import_node_fs10 = require("fs");
5433
+ var import_node_path11 = __toESM(require("path"), 1);
4736
5434
  var import_minimatch = require("minimatch");
4737
5435
  async function loadCodeowners(scanPath) {
4738
5436
  const candidates = [
4739
- import_node_path10.default.join(scanPath, "CODEOWNERS"),
4740
- import_node_path10.default.join(scanPath, ".github", "CODEOWNERS")
5437
+ import_node_path11.default.join(scanPath, "CODEOWNERS"),
5438
+ import_node_path11.default.join(scanPath, ".github", "CODEOWNERS")
4741
5439
  ];
4742
5440
  for (const file of candidates) {
4743
5441
  if (await exists(file)) {
4744
- const raw = await import_node_fs9.promises.readFile(file, "utf8");
5442
+ const raw = await import_node_fs10.promises.readFile(file, "utf8");
4745
5443
  return parseCodeowners(raw);
4746
5444
  }
4747
5445
  }
@@ -4759,7 +5457,7 @@ function parseCodeowners(raw) {
4759
5457
  return { rules };
4760
5458
  }
4761
5459
  function matchOwner(file, repoPath) {
4762
- const normalized = repoPath.split(import_node_path10.default.sep).join("/");
5460
+ const normalized = repoPath.split(import_node_path11.default.sep).join("/");
4763
5461
  for (const rule of file.rules) {
4764
5462
  if (matchesPattern(rule.pattern, normalized)) return rule.owners;
4765
5463
  }
@@ -4775,7 +5473,7 @@ function matchesPattern(rawPattern, repoPath) {
4775
5473
  return false;
4776
5474
  }
4777
5475
  async function readPackageJsonAuthor(serviceDir) {
4778
- const pkgPath = import_node_path10.default.join(serviceDir, "package.json");
5476
+ const pkgPath = import_node_path11.default.join(serviceDir, "package.json");
4779
5477
  if (!await exists(pkgPath)) return null;
4780
5478
  try {
4781
5479
  const pkg = await readJson(pkgPath);
@@ -4812,27 +5510,27 @@ function workspaceGlobs(pkg) {
4812
5510
  return null;
4813
5511
  }
4814
5512
  async function hasPythonManifest(dir) {
4815
- return await exists(import_node_path11.default.join(dir, "pyproject.toml")) || await exists(import_node_path11.default.join(dir, "requirements.txt")) || await exists(import_node_path11.default.join(dir, "setup.py"));
5513
+ 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"));
4816
5514
  }
4817
5515
  async function hasGoManifest(dir) {
4818
- return exists(import_node_path11.default.join(dir, "go.mod"));
5516
+ return exists(import_node_path12.default.join(dir, "go.mod"));
4819
5517
  }
4820
5518
  async function loadGitignore(scanPath) {
4821
- const gitignorePath = import_node_path11.default.join(scanPath, ".gitignore");
5519
+ const gitignorePath = import_node_path12.default.join(scanPath, ".gitignore");
4822
5520
  if (!await exists(gitignorePath)) return null;
4823
- const raw = await import_node_fs10.promises.readFile(gitignorePath, "utf8");
5521
+ const raw = await import_node_fs11.promises.readFile(gitignorePath, "utf8");
4824
5522
  return (0, import_ignore.default)().add(raw);
4825
5523
  }
4826
5524
  async function walkDirs(start, scanPath, options, visit) {
4827
5525
  async function recurse(current, depth) {
4828
5526
  if (depth > options.maxDepth) return;
4829
- const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
5527
+ const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
4830
5528
  for (const entry2 of entries) {
4831
5529
  if (!entry2.isDirectory()) continue;
4832
5530
  if (IGNORED_DIRS.has(entry2.name)) continue;
4833
- const child = import_node_path11.default.join(current, entry2.name);
5531
+ const child = import_node_path12.default.join(current, entry2.name);
4834
5532
  if (options.ig) {
4835
- const rel = import_node_path11.default.relative(scanPath, child).split(import_node_path11.default.sep).join("/");
5533
+ const rel = import_node_path12.default.relative(scanPath, child).split(import_node_path12.default.sep).join("/");
4836
5534
  if (rel && options.ig.ignores(rel + "/")) continue;
4837
5535
  }
4838
5536
  if (await isPythonVenvDir(child)) continue;
@@ -4848,8 +5546,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
4848
5546
  for (const raw of globs) {
4849
5547
  const pattern = raw.replace(/^\.\//, "");
4850
5548
  if (!pattern.includes("*")) {
4851
- const candidate = import_node_path11.default.join(scanPath, pattern);
4852
- if (await exists(import_node_path11.default.join(candidate, "package.json"))) found.add(candidate);
5549
+ const candidate = import_node_path12.default.join(scanPath, pattern);
5550
+ if (await exists(import_node_path12.default.join(candidate, "package.json"))) found.add(candidate);
4853
5551
  continue;
4854
5552
  }
4855
5553
  const segments = pattern.split("/");
@@ -4858,13 +5556,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
4858
5556
  if (seg.includes("*")) break;
4859
5557
  staticSegments.push(seg);
4860
5558
  }
4861
- const start = import_node_path11.default.join(scanPath, ...staticSegments);
5559
+ const start = import_node_path12.default.join(scanPath, ...staticSegments);
4862
5560
  if (!await exists(start)) continue;
4863
5561
  const hasDoubleStar = pattern.includes("**");
4864
5562
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
4865
5563
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
4866
- const rel = import_node_path11.default.relative(scanPath, dir).split(import_node_path11.default.sep).join("/");
4867
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path11.default.join(dir, "package.json"))) {
5564
+ const rel = import_node_path12.default.relative(scanPath, dir).split(import_node_path12.default.sep).join("/");
5565
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path12.default.join(dir, "package.json"))) {
4868
5566
  found.add(dir);
4869
5567
  }
4870
5568
  });
@@ -4887,31 +5585,31 @@ function detectJsFramework(pkg) {
4887
5585
  async function detectJsServiceLanguage(dir, pkg) {
4888
5586
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4889
5587
  if (deps["typescript"] !== void 0) return "typescript";
4890
- const entries = await import_node_fs10.promises.readdir(dir).catch(() => []);
5588
+ const entries = await import_node_fs11.promises.readdir(dir).catch(() => []);
4891
5589
  if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
4892
5590
  return "javascript";
4893
5591
  }
4894
5592
  async function discoverNodeService(scanPath, dir) {
4895
- const pkgPath = import_node_path11.default.join(dir, "package.json");
5593
+ const pkgPath = import_node_path12.default.join(dir, "package.json");
4896
5594
  if (!await exists(pkgPath)) return null;
4897
5595
  let pkg;
4898
5596
  try {
4899
5597
  pkg = await readJson(pkgPath);
4900
5598
  } catch (err) {
4901
- recordExtractionError("services", import_node_path11.default.relative(scanPath, pkgPath), err);
5599
+ recordExtractionError("services", import_node_path12.default.relative(scanPath, pkgPath), err);
4902
5600
  return null;
4903
5601
  }
4904
5602
  if (!pkg.name) return null;
4905
5603
  const framework = detectJsFramework(pkg);
4906
5604
  const language = await detectJsServiceLanguage(dir, pkg);
4907
5605
  const node = {
4908
- id: (0, import_types9.serviceId)(pkg.name),
4909
- type: import_types9.NodeType.ServiceNode,
5606
+ id: (0, import_types10.serviceId)(pkg.name),
5607
+ type: import_types10.NodeType.ServiceNode,
4910
5608
  name: pkg.name,
4911
5609
  language,
4912
5610
  version: pkg.version,
4913
5611
  dependencies: pkg.dependencies ?? {},
4914
- repoPath: import_node_path11.default.relative(scanPath, dir),
5612
+ repoPath: import_node_path12.default.relative(scanPath, dir),
4915
5613
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
4916
5614
  ...framework ? { framework } : {}
4917
5615
  };
@@ -4922,18 +5620,18 @@ async function discoverPyService(scanPath, dir) {
4922
5620
  if (!py) return null;
4923
5621
  const pkg = pythonToPackage(py);
4924
5622
  const node = {
4925
- id: (0, import_types9.serviceId)(py.name),
4926
- type: import_types9.NodeType.ServiceNode,
5623
+ id: (0, import_types10.serviceId)(py.name),
5624
+ type: import_types10.NodeType.ServiceNode,
4927
5625
  name: py.name,
4928
5626
  language: "python",
4929
5627
  version: py.version,
4930
5628
  dependencies: py.dependencies,
4931
- repoPath: import_node_path11.default.relative(scanPath, dir)
5629
+ repoPath: import_node_path12.default.relative(scanPath, dir)
4932
5630
  };
4933
5631
  return { pkg, dir, node };
4934
5632
  }
4935
5633
  async function discoverServices(scanPath) {
4936
- const rootPkgPath = import_node_path11.default.join(scanPath, "package.json");
5634
+ const rootPkgPath = import_node_path12.default.join(scanPath, "package.json");
4937
5635
  let rootPkg = null;
4938
5636
  if (await exists(rootPkgPath)) {
4939
5637
  try {
@@ -4941,7 +5639,7 @@ async function discoverServices(scanPath) {
4941
5639
  } catch (err) {
4942
5640
  recordExtractionError(
4943
5641
  "services workspaces",
4944
- import_node_path11.default.relative(scanPath, rootPkgPath),
5642
+ import_node_path12.default.relative(scanPath, rootPkgPath),
4945
5643
  err
4946
5644
  );
4947
5645
  }
@@ -4962,7 +5660,7 @@ async function discoverServices(scanPath) {
4962
5660
  scanPath,
4963
5661
  { maxDepth: parseScanDepth(), ig },
4964
5662
  async (dir) => {
4965
- if (await exists(import_node_path11.default.join(dir, "package.json"))) {
5663
+ if (await exists(import_node_path12.default.join(dir, "package.json"))) {
4966
5664
  candidateDirs.push(dir);
4967
5665
  } else if (await hasPythonManifest(dir) || await hasGoManifest(dir)) {
4968
5666
  candidateDirs.push(dir);
@@ -4978,8 +5676,8 @@ async function discoverServices(scanPath) {
4978
5676
  if (!service) continue;
4979
5677
  const existingDir = seen.get(service.node.name);
4980
5678
  if (existingDir !== void 0) {
4981
- const a = import_node_path11.default.relative(scanPath, existingDir) || ".";
4982
- const b = import_node_path11.default.relative(scanPath, dir) || ".";
5679
+ const a = import_node_path12.default.relative(scanPath, existingDir) || ".";
5680
+ const b = import_node_path12.default.relative(scanPath, dir) || ".";
4983
5681
  console.warn(
4984
5682
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
4985
5683
  );
@@ -5016,10 +5714,10 @@ function addServiceNodes(graph, services) {
5016
5714
 
5017
5715
  // src/extract/aliases.ts
5018
5716
  init_cjs_shims();
5019
- var import_node_path12 = __toESM(require("path"), 1);
5020
- var import_node_fs11 = require("fs");
5717
+ var import_node_path13 = __toESM(require("path"), 1);
5718
+ var import_node_fs12 = require("fs");
5021
5719
  var import_yaml2 = require("yaml");
5022
- var import_types10 = require("@neat.is/types");
5720
+ var import_types11 = require("@neat.is/types");
5023
5721
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
5024
5722
  "Service",
5025
5723
  "Deployment",
@@ -5029,7 +5727,7 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
5029
5727
  function addAliases(graph, serviceId7, candidates) {
5030
5728
  if (!graph.hasNode(serviceId7)) return;
5031
5729
  const node = graph.getNodeAttributes(serviceId7);
5032
- if (node.type !== import_types10.NodeType.ServiceNode) return;
5730
+ if (node.type !== import_types11.NodeType.ServiceNode) return;
5033
5731
  const set = new Set(node.aliases ?? []);
5034
5732
  for (const c of candidates) {
5035
5733
  if (!c) continue;
@@ -5044,14 +5742,14 @@ function indexServicesByName(services) {
5044
5742
  const map = /* @__PURE__ */ new Map();
5045
5743
  for (const s of services) {
5046
5744
  map.set(s.node.name, s.node.id);
5047
- map.set(import_node_path12.default.basename(s.dir), s.node.id);
5745
+ map.set(import_node_path13.default.basename(s.dir), s.node.id);
5048
5746
  }
5049
5747
  return map;
5050
5748
  }
5051
5749
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
5052
5750
  let composePath = null;
5053
5751
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5054
- const abs = import_node_path12.default.join(scanPath, name);
5752
+ const abs = import_node_path13.default.join(scanPath, name);
5055
5753
  if (await exists(abs)) {
5056
5754
  composePath = abs;
5057
5755
  break;
@@ -5064,7 +5762,7 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
5064
5762
  } catch (err) {
5065
5763
  recordExtractionError(
5066
5764
  "aliases compose",
5067
- import_node_path12.default.relative(scanPath, composePath),
5765
+ import_node_path13.default.relative(scanPath, composePath),
5068
5766
  err
5069
5767
  );
5070
5768
  return;
@@ -5107,11 +5805,11 @@ function parseDockerfileLabels(content) {
5107
5805
  }
5108
5806
  async function collectDockerfileAliases(graph, services) {
5109
5807
  for (const service of services) {
5110
- const dockerfilePath = import_node_path12.default.join(service.dir, "Dockerfile");
5808
+ const dockerfilePath = import_node_path13.default.join(service.dir, "Dockerfile");
5111
5809
  if (!await exists(dockerfilePath)) continue;
5112
5810
  let content;
5113
5811
  try {
5114
- content = await import_node_fs11.promises.readFile(dockerfilePath, "utf8");
5812
+ content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
5115
5813
  } catch (err) {
5116
5814
  recordExtractionError("aliases dockerfile", dockerfilePath, err);
5117
5815
  continue;
@@ -5123,15 +5821,15 @@ async function collectDockerfileAliases(graph, services) {
5123
5821
  async function walkYamlFiles(start, depth = 0, max = 5) {
5124
5822
  if (depth > max) return [];
5125
5823
  const out = [];
5126
- const entries = await import_node_fs11.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5824
+ const entries = await import_node_fs12.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5127
5825
  for (const entry2 of entries) {
5128
5826
  if (entry2.isDirectory()) {
5129
5827
  if (IGNORED_DIRS.has(entry2.name)) continue;
5130
- const child = import_node_path12.default.join(start, entry2.name);
5828
+ const child = import_node_path13.default.join(start, entry2.name);
5131
5829
  if (await isPythonVenvDir(child)) continue;
5132
5830
  out.push(...await walkYamlFiles(child, depth + 1, max));
5133
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry2.name))) {
5134
- out.push(import_node_path12.default.join(start, entry2.name));
5831
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path13.default.extname(entry2.name))) {
5832
+ out.push(import_node_path13.default.join(start, entry2.name));
5135
5833
  }
5136
5834
  }
5137
5835
  return out;
@@ -5158,614 +5856,230 @@ function k8sServiceTarget(doc, byName) {
5158
5856
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
5159
5857
  const files = await walkYamlFiles(scanPath);
5160
5858
  for (const file of files) {
5161
- const content = await import_node_fs11.promises.readFile(file, "utf8");
5859
+ const content = await import_node_fs12.promises.readFile(file, "utf8");
5162
5860
  let docs;
5163
5861
  try {
5164
- docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
5165
- } catch {
5166
- continue;
5167
- }
5168
- for (const doc of docs) {
5169
- if (!doc?.kind || !doc.metadata?.name) continue;
5170
- if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
5171
- const target = k8sServiceTarget(doc, serviceIndex);
5172
- if (!target) continue;
5173
- addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
5174
- }
5175
- }
5176
- }
5177
- async function addServiceAliases(graph, scanPath, services) {
5178
- const byName = indexServicesByName(services);
5179
- await collectComposeAliases(graph, scanPath, byName);
5180
- await collectDockerfileAliases(graph, services);
5181
- await collectK8sAliases(graph, scanPath, byName);
5182
- }
5183
-
5184
- // src/extract/files.ts
5185
- init_cjs_shims();
5186
- var import_node_path13 = __toESM(require("path"), 1);
5187
- async function addFiles(graph, services) {
5188
- let nodesAdded = 0;
5189
- let edgesAdded = 0;
5190
- for (const service of services) {
5191
- const filePaths = await walkSourceFiles(service.dir);
5192
- for (const filePath of filePaths) {
5193
- const relPath = toPosix(import_node_path13.default.relative(service.dir, filePath));
5194
- const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
5195
- graph,
5196
- service.pkg.name,
5197
- service.node.id,
5198
- relPath
5199
- );
5200
- nodesAdded += n;
5201
- edgesAdded += e;
5202
- }
5203
- }
5204
- return { nodesAdded, edgesAdded };
5205
- }
5206
-
5207
- // src/extract/symbols.ts
5208
- init_cjs_shims();
5209
- var import_node_path14 = __toESM(require("path"), 1);
5210
- var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5211
- var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5212
- var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
5213
- var import_types11 = require("@neat.is/types");
5214
- var PARSE_CHUNK2 = 16384;
5215
- var GRAMMAR_BY_EXT = {
5216
- ".ts": import_tree_sitter_typescript.default.typescript,
5217
- ".tsx": import_tree_sitter_typescript.default.tsx,
5218
- ".js": import_tree_sitter_javascript2.default,
5219
- ".jsx": import_tree_sitter_javascript2.default,
5220
- ".mjs": import_tree_sitter_javascript2.default,
5221
- ".cjs": import_tree_sitter_javascript2.default
5222
- };
5223
- function parseSource2(parser, source) {
5224
- return parser.parse(
5225
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5226
- );
5227
- }
5228
- function methodName(node) {
5229
- const name = node.childForFieldName("name");
5230
- return name ? name.text : null;
5231
- }
5232
- function collectSymbolDefs(root) {
5233
- const out = [];
5234
- const push = (kind, qualname, node) => {
5235
- out.push({
5236
- kind,
5237
- qualname,
5238
- startLine: node.startPosition.row + 1,
5239
- endLine: node.endPosition.row + 1
5240
- });
5241
- };
5242
- const visit = (node, classCtx) => {
5243
- switch (node.type) {
5244
- case "function_declaration":
5245
- case "generator_function_declaration": {
5246
- const name = node.childForFieldName("name")?.text;
5247
- if (name) push("function", name, node);
5248
- break;
5249
- }
5250
- case "class_declaration":
5251
- case "abstract_class_declaration":
5252
- case "class": {
5253
- const name = node.childForFieldName("name")?.text;
5254
- if (name) push("class", name, node);
5255
- const body = node.childForFieldName("body");
5256
- if (body) {
5257
- for (let i = 0; i < body.namedChildCount; i++) {
5258
- const child = body.namedChild(i);
5259
- if (child) visit(child, name ?? classCtx);
5260
- }
5261
- }
5262
- return;
5263
- }
5264
- case "method_definition": {
5265
- const name = methodName(node);
5266
- if (name) {
5267
- const kind = name === "constructor" ? "constructor" : "method";
5268
- push(kind, classCtx ? `${classCtx}.${name}` : name, node);
5269
- }
5270
- break;
5271
- }
5272
- case "variable_declarator": {
5273
- const value = node.childForFieldName("value");
5274
- if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
5275
- const nameNode = node.childForFieldName("name");
5276
- if (nameNode && nameNode.type === "identifier") {
5277
- push("function", nameNode.text, node);
5278
- }
5279
- }
5280
- break;
5281
- }
5282
- }
5283
- for (let i = 0; i < node.namedChildCount; i++) {
5284
- const child = node.namedChild(i);
5285
- if (child) visit(child, classCtx);
5286
- }
5287
- };
5288
- visit(root, void 0);
5289
- return out;
5290
- }
5291
- function disambiguate(defs) {
5292
- const counts = /* @__PURE__ */ new Map();
5293
- for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
5294
- const seen = /* @__PURE__ */ new Map();
5295
- return defs.map((def) => {
5296
- if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
5297
- const ordinal = seen.get(def.qualname) ?? 0;
5298
- seen.set(def.qualname, ordinal + 1);
5299
- return { def, disambiguator: ordinal };
5300
- });
5301
- }
5302
- async function addSymbols(graph, services) {
5303
- const parsers = /* @__PURE__ */ new Map();
5304
- const parserForExt2 = (ext) => {
5305
- const grammar = GRAMMAR_BY_EXT[ext];
5306
- if (!grammar) return null;
5307
- let parser = parsers.get(ext);
5308
- if (!parser) {
5309
- parser = new import_tree_sitter2.default();
5310
- parser.setLanguage(grammar);
5311
- parsers.set(ext, parser);
5862
+ docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
5863
+ } catch {
5864
+ continue;
5312
5865
  }
5313
- return parser;
5314
- };
5866
+ for (const doc of docs) {
5867
+ if (!doc?.kind || !doc.metadata?.name) continue;
5868
+ if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
5869
+ const target = k8sServiceTarget(doc, serviceIndex);
5870
+ if (!target) continue;
5871
+ addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
5872
+ }
5873
+ }
5874
+ }
5875
+ async function addServiceAliases(graph, scanPath, services) {
5876
+ const byName = indexServicesByName(services);
5877
+ await collectComposeAliases(graph, scanPath, byName);
5878
+ await collectDockerfileAliases(graph, services);
5879
+ await collectK8sAliases(graph, scanPath, byName);
5880
+ }
5881
+
5882
+ // src/extract/files.ts
5883
+ init_cjs_shims();
5884
+ var import_node_path14 = __toESM(require("path"), 1);
5885
+ async function addFiles(graph, services) {
5315
5886
  let nodesAdded = 0;
5316
5887
  let edgesAdded = 0;
5317
5888
  for (const service of services) {
5318
- const files = await loadSourceFiles(service.dir);
5319
- for (const file of files) {
5320
- const parser = parserForExt2(import_node_path14.default.extname(file.path));
5321
- if (!parser) continue;
5322
- const relPath = toPosix(import_node_path14.default.relative(service.dir, file.path));
5323
- let defs;
5324
- try {
5325
- const tree = parseSource2(parser, file.content);
5326
- defs = collectSymbolDefs(tree.rootNode);
5327
- } catch (err) {
5328
- recordExtractionError("symbol extraction", file.path, err);
5329
- continue;
5330
- }
5331
- if (defs.length === 0) continue;
5332
- const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5889
+ const filePaths = await walkSourceFiles(service.dir);
5890
+ for (const filePath of filePaths) {
5891
+ const relPath = toPosix(import_node_path14.default.relative(service.dir, filePath));
5892
+ const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
5333
5893
  graph,
5334
5894
  service.pkg.name,
5335
5895
  service.node.id,
5336
5896
  relPath
5337
5897
  );
5338
- nodesAdded += fn;
5339
- edgesAdded += fe;
5340
- for (const { def, disambiguator } of disambiguate(defs)) {
5341
- const sid = (0, import_types11.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
5342
- if (!graph.hasNode(sid)) {
5343
- const node = {
5344
- id: sid,
5345
- type: import_types11.NodeType.SymbolNode,
5346
- kind: def.kind,
5347
- qualname: def.qualname,
5348
- span: { startLine: def.startLine, endLine: def.endLine },
5349
- service: service.pkg.name,
5350
- relPath,
5351
- discoveredVia: "static"
5352
- };
5353
- graph.addNode(sid, node);
5354
- nodesAdded++;
5355
- }
5356
- const containsId = (0, import_types11.extractedEdgeId)(fileNodeId, sid, import_types11.EdgeType.CONTAINS);
5357
- if (!graph.hasEdge(containsId)) {
5358
- const edge = {
5359
- id: containsId,
5360
- source: fileNodeId,
5361
- target: sid,
5362
- type: import_types11.EdgeType.CONTAINS,
5363
- provenance: import_types11.Provenance.EXTRACTED,
5364
- confidence: (0, import_types11.confidenceForExtracted)("structural"),
5365
- evidence: {
5366
- file: relPath,
5367
- line: def.startLine,
5368
- snippet: snippet(file.content, def.startLine)
5369
- }
5370
- };
5371
- graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
5372
- edgesAdded++;
5373
- }
5374
- }
5898
+ nodesAdded += n;
5899
+ edgesAdded += e;
5375
5900
  }
5376
5901
  }
5377
5902
  return { nodesAdded, edgesAdded };
5378
5903
  }
5379
5904
 
5380
- // src/extract/symbol-edges.ts
5381
- init_cjs_shims();
5382
- var import_node_path16 = __toESM(require("path"), 1);
5383
- var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
5384
- var import_types13 = require("@neat.is/types");
5385
-
5386
- // src/extract/imports.ts
5905
+ // src/extract/symbols.ts
5387
5906
  init_cjs_shims();
5388
5907
  var import_node_path15 = __toESM(require("path"), 1);
5389
- var import_node_fs12 = require("fs");
5390
5908
  var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5391
5909
  var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5392
- var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5393
- var import_tree_sitter_go2 = __toESM(require("tree-sitter-go"), 1);
5910
+ var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1);
5394
5911
  var import_types12 = require("@neat.is/types");
5395
5912
  var PARSE_CHUNK3 = 16384;
5913
+ var GRAMMAR_BY_EXT = {
5914
+ ".ts": import_tree_sitter_typescript.default.typescript,
5915
+ ".tsx": import_tree_sitter_typescript.default.tsx,
5916
+ ".js": import_tree_sitter_javascript3.default,
5917
+ ".jsx": import_tree_sitter_javascript3.default,
5918
+ ".mjs": import_tree_sitter_javascript3.default,
5919
+ ".cjs": import_tree_sitter_javascript3.default
5920
+ };
5396
5921
  function parseSource3(parser, source) {
5397
5922
  return parser.parse(
5398
5923
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5399
- );
5400
- }
5401
- function makeJsParser2() {
5402
- const p = new import_tree_sitter3.default();
5403
- p.setLanguage(import_tree_sitter_javascript3.default);
5404
- return p;
5405
- }
5406
- function makePyParser2() {
5407
- const p = new import_tree_sitter3.default();
5408
- p.setLanguage(import_tree_sitter_python2.default);
5409
- return p;
5410
- }
5411
- function makeGoParser2() {
5412
- const p = new import_tree_sitter3.default();
5413
- p.setLanguage(import_tree_sitter_go2.default);
5414
- return p;
5415
- }
5416
- function stringLiteralText(node) {
5417
- for (let i = 0; i < node.childCount; i++) {
5418
- const child = node.child(i);
5419
- if (child?.type === "string_fragment") return child.text;
5420
- }
5421
- const raw = node.text;
5422
- if (raw.length >= 2) return raw.slice(1, -1);
5423
- return raw.length === 0 ? null : "";
5424
- }
5425
- function clipSnippet(text) {
5426
- const oneLine = text.split("\n")[0] ?? text;
5427
- return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
5428
- }
5429
- function collectGoImports(node, out) {
5430
- if (node.type === "import_spec") {
5431
- const pathNode = node.childForFieldName("path");
5432
- if (pathNode) {
5433
- const specifier = pathNode.text.replace(/^`|`$/g, "").replace(/^"|"$/g, "");
5434
- if (specifier) out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5435
- }
5436
- return;
5437
- }
5438
- for (let i = 0; i < node.namedChildCount; i++) {
5439
- const child = node.namedChild(i);
5440
- if (child) collectGoImports(child, out);
5441
- }
5442
- }
5443
- function collectJsImports(node, out) {
5444
- if (node.type === "import_statement") {
5445
- const source = node.childForFieldName("source");
5446
- if (source) {
5447
- const specifier = stringLiteralText(source);
5448
- if (specifier) {
5449
- out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5450
- }
5451
- }
5452
- return;
5453
- }
5454
- if (node.type === "call_expression") {
5455
- const fn = node.childForFieldName("function");
5456
- if (fn?.type === "identifier" && fn.text === "require") {
5457
- const args = node.childForFieldName("arguments");
5458
- const firstArg = args?.namedChild(0);
5459
- if (firstArg?.type === "string") {
5460
- const specifier = stringLiteralText(firstArg);
5461
- if (specifier) {
5462
- out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5463
- }
5464
- }
5465
- }
5466
- }
5467
- for (let i = 0; i < node.namedChildCount; i++) {
5468
- const child = node.namedChild(i);
5469
- if (child) collectJsImports(child, out);
5470
- }
5471
- }
5472
- function collectImportedNames(node, out) {
5473
- if (node.type === "aliased_import") {
5474
- const nameNode = node.childForFieldName("name");
5475
- if (nameNode) out.push(nameNode.text);
5476
- return;
5477
- }
5478
- if (node.type === "dotted_name") {
5479
- out.push(node.text);
5480
- return;
5481
- }
5482
- for (let i = 0; i < node.namedChildCount; i++) {
5483
- const child = node.namedChild(i);
5484
- if (child) collectImportedNames(child, out);
5485
- }
5486
- }
5487
- function collectPyImports(node, out) {
5488
- if (node.type === "import_from_statement") {
5489
- let level = 0;
5490
- let modulePath = "";
5491
- const names = [];
5492
- let pastFrom = false;
5493
- let pastImport = false;
5494
- for (let i = 0; i < node.childCount; i++) {
5495
- const child = node.child(i);
5496
- if (!child) continue;
5497
- if (!pastFrom) {
5498
- if (child.type === "from") pastFrom = true;
5499
- continue;
5500
- }
5501
- if (!pastImport) {
5502
- if (child.type === "import") {
5503
- pastImport = true;
5504
- continue;
5505
- }
5506
- if (child.type === "relative_import") {
5507
- for (let j = 0; j < child.childCount; j++) {
5508
- const rc = child.child(j);
5509
- if (!rc) continue;
5510
- if (rc.type === "import_prefix") {
5511
- for (let k = 0; k < rc.childCount; k++) {
5512
- if (rc.child(k)?.type === ".") level++;
5513
- }
5514
- } else if (rc.type === "dotted_name") modulePath = rc.text;
5515
- }
5516
- } else if (child.type === "dotted_name") {
5517
- modulePath = child.text;
5518
- }
5519
- continue;
5520
- }
5521
- collectImportedNames(child, names);
5522
- }
5523
- if (level > 0 || modulePath) {
5524
- out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
5525
- }
5526
- }
5527
- for (let i = 0; i < node.namedChildCount; i++) {
5528
- const child = node.namedChild(i);
5529
- if (child) collectPyImports(child, out);
5530
- }
5531
- }
5532
- async function fileExists(p) {
5533
- try {
5534
- await import_node_fs12.promises.access(p);
5535
- return true;
5536
- } catch {
5537
- return false;
5538
- }
5539
- }
5540
- function isWithinServiceDir(candidate, serviceDir) {
5541
- const rel = import_node_path15.default.relative(serviceDir, candidate);
5542
- return rel !== "" && !rel.startsWith("..") && !import_node_path15.default.isAbsolute(rel);
5543
- }
5544
- var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
5545
- var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
5546
- async function firstExistingCandidate(base, serviceDir) {
5547
- for (const ext of JS_EXTENSIONS) {
5548
- const candidate = base + ext;
5549
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5550
- return toPosix(import_node_path15.default.relative(serviceDir, candidate));
5551
- }
5552
- }
5553
- for (const indexFile of JS_INDEX_FILES) {
5554
- const candidate = import_node_path15.default.join(base, indexFile);
5555
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5556
- return toPosix(import_node_path15.default.relative(serviceDir, candidate));
5557
- }
5558
- }
5559
- return null;
5560
- }
5561
- async function loadTsPathConfig(serviceDir) {
5562
- const tsconfigPath = import_node_path15.default.join(serviceDir, "tsconfig.json");
5563
- let raw;
5564
- try {
5565
- raw = await import_node_fs12.promises.readFile(tsconfigPath, "utf8");
5566
- } catch {
5567
- return null;
5568
- }
5569
- try {
5570
- const parsed = JSON.parse(raw);
5571
- const paths = parsed.compilerOptions?.paths;
5572
- if (!paths || Object.keys(paths).length === 0) return null;
5573
- const baseUrl = parsed.compilerOptions?.baseUrl;
5574
- return { paths, baseDir: baseUrl ? import_node_path15.default.resolve(serviceDir, baseUrl) : serviceDir };
5575
- } catch (err) {
5576
- recordExtractionError("import alias resolution", tsconfigPath, err);
5577
- return null;
5578
- }
5924
+ );
5579
5925
  }
5580
- async function resolveTsAlias(specifier, config, serviceDir) {
5581
- for (const [pattern, targets] of Object.entries(config.paths)) {
5582
- let suffix = null;
5583
- if (pattern === specifier) {
5584
- suffix = "";
5585
- } else if (pattern.endsWith("/*")) {
5586
- const prefix = pattern.slice(0, -1);
5587
- if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
5588
- }
5589
- if (suffix === null) continue;
5590
- for (const target of targets) {
5591
- const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
5592
- const resolvedBase = import_node_path15.default.resolve(config.baseDir, targetBase, suffix);
5593
- const hit = await firstExistingCandidate(resolvedBase, serviceDir);
5594
- if (hit) return hit;
5595
- if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
5596
- return toPosix(import_node_path15.default.relative(serviceDir, resolvedBase));
5597
- }
5598
- }
5599
- }
5600
- return null;
5926
+ function methodName(node) {
5927
+ const name = node.childForFieldName("name");
5928
+ return name ? name.text : null;
5601
5929
  }
5602
- async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
5603
- if (!specifier) return null;
5604
- if (specifier.startsWith("./") || specifier.startsWith("../")) {
5605
- const base = import_node_path15.default.resolve(importerDir, specifier);
5606
- const ext = import_node_path15.default.extname(specifier);
5607
- if (ext) {
5608
- if (ext === ".js" || ext === ".jsx") {
5609
- const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
5610
- const tsSibling = base.slice(0, -ext.length) + tsExt;
5611
- if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
5612
- return toPosix(import_node_path15.default.relative(serviceDir, tsSibling));
5930
+ function collectSymbolDefs(root) {
5931
+ const out = [];
5932
+ const push = (kind, qualname, node) => {
5933
+ out.push({
5934
+ kind,
5935
+ qualname,
5936
+ startLine: node.startPosition.row + 1,
5937
+ endLine: node.endPosition.row + 1
5938
+ });
5939
+ };
5940
+ const visit = (node, classCtx) => {
5941
+ switch (node.type) {
5942
+ case "function_declaration":
5943
+ case "generator_function_declaration": {
5944
+ const name = node.childForFieldName("name")?.text;
5945
+ if (name) push("function", name, node);
5946
+ break;
5947
+ }
5948
+ case "class_declaration":
5949
+ case "abstract_class_declaration":
5950
+ case "class": {
5951
+ const name = node.childForFieldName("name")?.text;
5952
+ if (name) push("class", name, node);
5953
+ const body = node.childForFieldName("body");
5954
+ if (body) {
5955
+ for (let i = 0; i < body.namedChildCount; i++) {
5956
+ const child = body.namedChild(i);
5957
+ if (child) visit(child, name ?? classCtx);
5958
+ }
5613
5959
  }
5960
+ return;
5614
5961
  }
5615
- if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
5616
- return toPosix(import_node_path15.default.relative(serviceDir, base));
5962
+ case "method_definition": {
5963
+ const name = methodName(node);
5964
+ if (name) {
5965
+ const kind = name === "constructor" ? "constructor" : "method";
5966
+ push(kind, classCtx ? `${classCtx}.${name}` : name, node);
5967
+ }
5968
+ break;
5617
5969
  }
5618
- return null;
5619
- }
5620
- return firstExistingCandidate(base, serviceDir);
5621
- }
5622
- if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
5623
- return null;
5624
- }
5625
- async function resolvePyImport(imp, importerPath, serviceDir) {
5626
- let baseDir;
5627
- if (imp.level > 0) {
5628
- baseDir = import_node_path15.default.dirname(importerPath);
5629
- for (let i = 1; i < imp.level; i++) baseDir = import_node_path15.default.dirname(baseDir);
5630
- } else {
5631
- baseDir = serviceDir;
5632
- }
5633
- const moduleBase = imp.modulePath ? import_node_path15.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
5634
- const resolved = /* @__PURE__ */ new Set();
5635
- let needModuleFile = imp.names.length === 0;
5636
- for (const name of imp.names) {
5637
- const submoduleFile = import_node_path15.default.join(moduleBase, `${name}.py`);
5638
- const subpackageInit = import_node_path15.default.join(moduleBase, name, "__init__.py");
5639
- if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
5640
- resolved.add(toPosix(import_node_path15.default.relative(serviceDir, submoduleFile)));
5641
- } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
5642
- resolved.add(toPosix(import_node_path15.default.relative(serviceDir, subpackageInit)));
5643
- } else {
5644
- needModuleFile = true;
5645
- }
5646
- }
5647
- if (needModuleFile) {
5648
- const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path15.default.join(moduleBase, "__init__.py")] : [import_node_path15.default.join(moduleBase, "__init__.py")];
5649
- for (const candidate of moduleFileCandidates) {
5650
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
5651
- resolved.add(toPosix(import_node_path15.default.relative(serviceDir, candidate)));
5970
+ case "variable_declarator": {
5971
+ const value = node.childForFieldName("value");
5972
+ if (value && (value.type === "arrow_function" || value.type === "function" || value.type === "function_expression" || value.type === "generator_function")) {
5973
+ const nameNode = node.childForFieldName("name");
5974
+ if (nameNode && nameNode.type === "identifier") {
5975
+ push("function", nameNode.text, node);
5976
+ }
5977
+ }
5652
5978
  break;
5653
5979
  }
5654
5980
  }
5655
- }
5656
- return [...resolved];
5981
+ for (let i = 0; i < node.namedChildCount; i++) {
5982
+ const child = node.namedChild(i);
5983
+ if (child) visit(child, classCtx);
5984
+ }
5985
+ };
5986
+ visit(root, void 0);
5987
+ return out;
5657
5988
  }
5658
- async function resolveGoImport(specifier, modulePath, serviceDir) {
5659
- if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
5660
- const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
5661
- const dir = import_node_path15.default.join(serviceDir, suffix);
5662
- const entries = await import_node_fs12.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
5663
- const candidates = entries.filter((entry2) => entry2.isFile() && entry2.name.endsWith(".go") && !entry2.name.endsWith("_test.go")).map((entry2) => import_node_path15.default.join(dir, entry2.name));
5664
- if (candidates.length !== 1) return null;
5665
- return toPosix(import_node_path15.default.relative(serviceDir, candidates[0]));
5989
+ function disambiguate(defs) {
5990
+ const counts = /* @__PURE__ */ new Map();
5991
+ for (const def of defs) counts.set(def.qualname, (counts.get(def.qualname) ?? 0) + 1);
5992
+ const seen = /* @__PURE__ */ new Map();
5993
+ return defs.map((def) => {
5994
+ if ((counts.get(def.qualname) ?? 0) <= 1) return { def };
5995
+ const ordinal = seen.get(def.qualname) ?? 0;
5996
+ seen.set(def.qualname, ordinal + 1);
5997
+ return { def, disambiguator: ordinal };
5998
+ });
5666
5999
  }
5667
- function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
5668
- const importeeFileId = (0, import_types12.fileId)(serviceName, importeeRelPath);
5669
- if (!graph.hasNode(importeeFileId)) return 0;
5670
- const edgeId = (0, import_types12.extractedEdgeId)(importerFileId, importeeFileId, import_types12.EdgeType.IMPORTS);
5671
- if (graph.hasEdge(edgeId)) return 0;
5672
- const edge = {
5673
- id: edgeId,
5674
- source: importerFileId,
5675
- target: importeeFileId,
5676
- type: import_types12.EdgeType.IMPORTS,
5677
- provenance: import_types12.Provenance.EXTRACTED,
5678
- confidence: (0, import_types12.confidenceForExtracted)("structural"),
5679
- evidence: { file: importerRelPath, line, snippet: snippet2 }
6000
+ async function addSymbols(graph, services) {
6001
+ const parsers = /* @__PURE__ */ new Map();
6002
+ const parserForExt2 = (ext) => {
6003
+ const grammar = GRAMMAR_BY_EXT[ext];
6004
+ if (!grammar) return null;
6005
+ let parser = parsers.get(ext);
6006
+ if (!parser) {
6007
+ parser = new import_tree_sitter3.default();
6008
+ parser.setLanguage(grammar);
6009
+ parsers.set(ext, parser);
6010
+ }
6011
+ return parser;
5680
6012
  };
5681
- graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
5682
- return 1;
5683
- }
5684
- async function addImports(graph, services) {
5685
- const jsParser = makeJsParser2();
5686
- const pyParser = makePyParser2();
5687
- const goParser = makeGoParser2();
6013
+ let nodesAdded = 0;
5688
6014
  let edgesAdded = 0;
5689
6015
  for (const service of services) {
5690
- const tsPaths = await loadTsPathConfig(service.dir);
5691
6016
  const files = await loadSourceFiles(service.dir);
5692
6017
  for (const file of files) {
5693
- if (isTestPath(file.path)) continue;
5694
- const relFile = toPosix(import_node_path15.default.relative(service.dir, file.path));
5695
- const importerFileId = (0, import_types12.fileId)(service.pkg.name, relFile);
5696
- const isPython = import_node_path15.default.extname(file.path) === ".py";
5697
- const isGo = import_node_path15.default.extname(file.path) === ".go";
5698
- if (isGo) {
5699
- let goImports = [];
5700
- try {
5701
- const tree = parseSource3(goParser, file.content);
5702
- collectGoImports(tree.rootNode, goImports);
5703
- } catch (err) {
5704
- recordExtractionError("import extraction", file.path, err);
5705
- continue;
5706
- }
5707
- const goMod = await import_node_fs12.promises.readFile(import_node_path15.default.join(service.dir, "go.mod"), "utf8").catch(() => "");
5708
- const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
5709
- if (!modulePath) continue;
5710
- for (const imp of goImports) {
5711
- const resolved = await resolveGoImport(imp.specifier, modulePath, service.dir);
5712
- if (!resolved) continue;
5713
- edgesAdded += emitImportEdge(graph, service.pkg.name, importerFileId, relFile, resolved, imp.line, imp.snippet);
5714
- }
5715
- continue;
5716
- }
5717
- if (isPython) {
5718
- let pyImports = [];
5719
- try {
5720
- const tree = parseSource3(pyParser, file.content);
5721
- collectPyImports(tree.rootNode, pyImports);
5722
- } catch (err) {
5723
- recordExtractionError("import extraction", file.path, err);
5724
- continue;
5725
- }
5726
- for (const imp of pyImports) {
5727
- const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
5728
- for (const resolved of resolvedPaths) {
5729
- edgesAdded += emitImportEdge(
5730
- graph,
5731
- service.pkg.name,
5732
- importerFileId,
5733
- relFile,
5734
- resolved,
5735
- imp.line,
5736
- imp.snippet
5737
- );
5738
- }
5739
- }
5740
- continue;
5741
- }
5742
- let jsImports = [];
6018
+ const parser = parserForExt2(import_node_path15.default.extname(file.path));
6019
+ if (!parser) continue;
6020
+ const relPath = toPosix(import_node_path15.default.relative(service.dir, file.path));
6021
+ let defs;
5743
6022
  try {
5744
- const tree = parseSource3(jsParser, file.content);
5745
- collectJsImports(tree.rootNode, jsImports);
6023
+ const tree = parseSource3(parser, file.content);
6024
+ defs = collectSymbolDefs(tree.rootNode);
5746
6025
  } catch (err) {
5747
- recordExtractionError("import extraction", file.path, err);
6026
+ recordExtractionError("symbol extraction", file.path, err);
5748
6027
  continue;
5749
6028
  }
5750
- for (const imp of jsImports) {
5751
- const resolved = await resolveJsImport(imp.specifier, import_node_path15.default.dirname(file.path), service.dir, tsPaths);
5752
- if (!resolved) continue;
5753
- edgesAdded += emitImportEdge(
5754
- graph,
5755
- service.pkg.name,
5756
- importerFileId,
5757
- relFile,
5758
- resolved,
5759
- imp.line,
5760
- imp.snippet
5761
- );
6029
+ if (defs.length === 0) continue;
6030
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6031
+ graph,
6032
+ service.pkg.name,
6033
+ service.node.id,
6034
+ relPath
6035
+ );
6036
+ nodesAdded += fn;
6037
+ edgesAdded += fe;
6038
+ for (const { def, disambiguator } of disambiguate(defs)) {
6039
+ const sid = (0, import_types12.symbolId)(service.pkg.name, relPath, def.qualname, disambiguator);
6040
+ if (!graph.hasNode(sid)) {
6041
+ const node = {
6042
+ id: sid,
6043
+ type: import_types12.NodeType.SymbolNode,
6044
+ kind: def.kind,
6045
+ qualname: def.qualname,
6046
+ span: { startLine: def.startLine, endLine: def.endLine },
6047
+ service: service.pkg.name,
6048
+ relPath,
6049
+ discoveredVia: "static"
6050
+ };
6051
+ graph.addNode(sid, node);
6052
+ nodesAdded++;
6053
+ }
6054
+ const containsId = (0, import_types12.extractedEdgeId)(fileNodeId, sid, import_types12.EdgeType.CONTAINS);
6055
+ if (!graph.hasEdge(containsId)) {
6056
+ const edge = {
6057
+ id: containsId,
6058
+ source: fileNodeId,
6059
+ target: sid,
6060
+ type: import_types12.EdgeType.CONTAINS,
6061
+ provenance: import_types12.Provenance.EXTRACTED,
6062
+ confidence: (0, import_types12.confidenceForExtracted)("structural"),
6063
+ evidence: {
6064
+ file: relPath,
6065
+ line: def.startLine,
6066
+ snippet: snippet(file.content, def.startLine)
6067
+ }
6068
+ };
6069
+ graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
6070
+ edgesAdded++;
6071
+ }
5762
6072
  }
5763
6073
  }
5764
6074
  }
5765
- return { nodesAdded: 0, edgesAdded };
6075
+ return { nodesAdded, edgesAdded };
5766
6076
  }
5767
6077
 
5768
6078
  // src/extract/symbol-edges.ts
6079
+ init_cjs_shims();
6080
+ var import_node_path16 = __toESM(require("path"), 1);
6081
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
6082
+ var import_types13 = require("@neat.is/types");
5769
6083
  function extendsInfo(classHeritage) {
5770
6084
  for (let i = 0; i < classHeritage.namedChildCount; i++) {
5771
6085
  const child = classHeritage.namedChild(i);
@@ -5876,7 +6190,7 @@ async function addSymbolEdges(graph, services) {
5876
6190
  const fileDir = import_node_path16.default.dirname(file.path);
5877
6191
  let root;
5878
6192
  try {
5879
- root = parseSource2(parser, file.content).rootNode;
6193
+ root = parseSource3(parser, file.content).rootNode;
5880
6194
  } catch (err) {
5881
6195
  recordExtractionError("symbol edge extraction", file.path, err);
5882
6196
  continue;
@@ -8328,7 +8642,7 @@ function columnsFromObject(obj) {
8328
8642
  }
8329
8643
  function drizzleEndpointsFromFile(file, serviceDir) {
8330
8644
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
8331
- const tree = parseSource2(parserForExt(import_node_path37.default.extname(file.path)), file.content);
8645
+ const tree = parseSource3(parserForExt(import_node_path37.default.extname(file.path)), file.content);
8332
8646
  const out = [];
8333
8647
  const seen = /* @__PURE__ */ new Set();
8334
8648
  const walk6 = (node) => {