@neat.is/core 0.4.11 → 0.4.13

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.
@@ -454,19 +454,19 @@ function confidenceFromMix(edges, now = Date.now()) {
454
454
  function longestIncomingWalk(graph, start, maxDepth) {
455
455
  let best = { path: [start], edges: [] };
456
456
  const visited = /* @__PURE__ */ new Set([start]);
457
- function step(node, path34, edges) {
458
- if (path34.length > best.path.length) {
459
- best = { path: [...path34], edges: [...edges] };
457
+ function step(node, path38, edges) {
458
+ if (path38.length > best.path.length) {
459
+ best = { path: [...path38], edges: [...edges] };
460
460
  }
461
- if (path34.length - 1 >= maxDepth) return;
461
+ if (path38.length - 1 >= maxDepth) return;
462
462
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
463
463
  for (const [srcId, edge] of incoming) {
464
464
  if (visited.has(srcId)) continue;
465
465
  visited.add(srcId);
466
- path34.push(srcId);
466
+ path38.push(srcId);
467
467
  edges.push(edge);
468
- step(srcId, path34, edges);
469
- path34.pop();
468
+ step(srcId, path38, edges);
469
+ path38.pop();
470
470
  edges.pop();
471
471
  visited.delete(srcId);
472
472
  }
@@ -1385,7 +1385,7 @@ function ensureFrontierNode(graph, host, ts) {
1385
1385
  graph.addNode(id, node);
1386
1386
  return id;
1387
1387
  }
1388
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1388
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1389
1389
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1390
1390
  const id = makeObservedEdgeId(type, source, target);
1391
1391
  if (graph.hasEdge(id)) {
@@ -1422,7 +1422,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1422
1422
  confidence: confidenceForObservedSignal(signal),
1423
1423
  lastObserved: ts,
1424
1424
  callCount: 1,
1425
- signal
1425
+ signal,
1426
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
1427
+ // Only set when code.filepath was present on the span — never fabricated.
1428
+ ...evidence ? { evidence } : {}
1426
1429
  };
1427
1430
  graph.addEdgeWithKey(id, source, target, edge);
1428
1431
  return { edge, created: true };
@@ -1516,6 +1519,7 @@ async function handleSpan(ctx, span) {
1516
1519
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1517
1520
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
1518
1521
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1522
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
1519
1523
  let affectedNode = sourceId;
1520
1524
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
1521
1525
  if (span.dbSystem) {
@@ -1529,7 +1533,8 @@ async function handleSpan(ctx, span) {
1529
1533
  observedSource(),
1530
1534
  targetId,
1531
1535
  ts,
1532
- isError
1536
+ isError,
1537
+ callSiteEvidence
1533
1538
  );
1534
1539
  if (result) affectedNode = targetId;
1535
1540
  }
@@ -1545,7 +1550,8 @@ async function handleSpan(ctx, span) {
1545
1550
  observedSource(),
1546
1551
  targetId,
1547
1552
  ts,
1548
- isError
1553
+ isError,
1554
+ callSiteEvidence
1549
1555
  );
1550
1556
  affectedNode = targetId;
1551
1557
  resolvedViaAddress = true;
@@ -1557,7 +1563,8 @@ async function handleSpan(ctx, span) {
1557
1563
  observedSource(),
1558
1564
  frontierNodeId,
1559
1565
  ts,
1560
- isError
1566
+ isError,
1567
+ callSiteEvidence
1561
1568
  );
1562
1569
  affectedNode = frontierNodeId;
1563
1570
  resolvedViaAddress = true;
@@ -2512,20 +2519,445 @@ async function addServiceAliases(graph, scanPath, services) {
2512
2519
  await collectK8sAliases(graph, scanPath, byName);
2513
2520
  }
2514
2521
 
2515
- // src/extract/databases/index.ts
2516
- import path17 from "path";
2522
+ // src/extract/calls/shared.ts
2523
+ import { promises as fs10 } from "fs";
2524
+ import path10 from "path";
2517
2525
  import {
2518
2526
  EdgeType as EdgeType4,
2519
2527
  NodeType as NodeType6,
2520
2528
  Provenance as Provenance2,
2529
+ confidenceForExtracted,
2530
+ extractedEdgeId as extractedEdgeId3,
2531
+ fileId as fileId2
2532
+ } from "@neat.is/types";
2533
+ async function walkSourceFiles(dir) {
2534
+ const out = [];
2535
+ async function walk(current) {
2536
+ const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
2537
+ for (const entry of entries) {
2538
+ const full = path10.join(current, entry.name);
2539
+ if (entry.isDirectory()) {
2540
+ if (IGNORED_DIRS.has(entry.name)) continue;
2541
+ if (await isPythonVenvDir(full)) continue;
2542
+ await walk(full);
2543
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))) {
2544
+ out.push(full);
2545
+ }
2546
+ }
2547
+ }
2548
+ await walk(dir);
2549
+ return out;
2550
+ }
2551
+ async function loadSourceFiles(dir) {
2552
+ const paths = await walkSourceFiles(dir);
2553
+ const out = [];
2554
+ for (const p of paths) {
2555
+ try {
2556
+ const content = await fs10.readFile(p, "utf8");
2557
+ out.push({ path: p, content });
2558
+ } catch {
2559
+ }
2560
+ }
2561
+ return out;
2562
+ }
2563
+ function lineOf(text, needle) {
2564
+ const idx = text.indexOf(needle);
2565
+ if (idx < 0) return 1;
2566
+ return text.slice(0, idx).split("\n").length;
2567
+ }
2568
+ function snippet(text, line) {
2569
+ const lines = text.split("\n");
2570
+ return (lines[line - 1] ?? "").trim();
2571
+ }
2572
+ function toPosix2(p) {
2573
+ return p.split("\\").join("/");
2574
+ }
2575
+ function languageForPath(relPath) {
2576
+ switch (path10.extname(relPath).toLowerCase()) {
2577
+ case ".py":
2578
+ return "python";
2579
+ case ".ts":
2580
+ case ".tsx":
2581
+ return "typescript";
2582
+ case ".js":
2583
+ case ".jsx":
2584
+ case ".mjs":
2585
+ case ".cjs":
2586
+ return "javascript";
2587
+ default:
2588
+ return void 0;
2589
+ }
2590
+ }
2591
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
2592
+ let nodesAdded = 0;
2593
+ let edgesAdded = 0;
2594
+ const fileNodeId = fileId2(serviceName, relPath);
2595
+ if (!graph.hasNode(fileNodeId)) {
2596
+ const language = languageForPath(relPath);
2597
+ const node = {
2598
+ id: fileNodeId,
2599
+ type: NodeType6.FileNode,
2600
+ service: serviceName,
2601
+ path: relPath,
2602
+ ...language ? { language } : {},
2603
+ discoveredVia: "static"
2604
+ };
2605
+ graph.addNode(fileNodeId, node);
2606
+ nodesAdded++;
2607
+ }
2608
+ const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType4.CONTAINS);
2609
+ if (!graph.hasEdge(containsId)) {
2610
+ const edge = {
2611
+ id: containsId,
2612
+ source: serviceNodeId,
2613
+ target: fileNodeId,
2614
+ type: EdgeType4.CONTAINS,
2615
+ provenance: Provenance2.EXTRACTED,
2616
+ confidence: confidenceForExtracted("structural"),
2617
+ evidence: { file: relPath }
2618
+ };
2619
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
2620
+ edgesAdded++;
2621
+ }
2622
+ return { fileNodeId, nodesAdded, edgesAdded };
2623
+ }
2624
+
2625
+ // src/extract/files.ts
2626
+ import path11 from "path";
2627
+ async function addFiles(graph, services) {
2628
+ let nodesAdded = 0;
2629
+ let edgesAdded = 0;
2630
+ for (const service of services) {
2631
+ const filePaths = await walkSourceFiles(service.dir);
2632
+ for (const filePath of filePaths) {
2633
+ const relPath = toPosix2(path11.relative(service.dir, filePath));
2634
+ const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
2635
+ graph,
2636
+ service.pkg.name,
2637
+ service.node.id,
2638
+ relPath
2639
+ );
2640
+ nodesAdded += n;
2641
+ edgesAdded += e;
2642
+ }
2643
+ }
2644
+ return { nodesAdded, edgesAdded };
2645
+ }
2646
+
2647
+ // src/extract/imports.ts
2648
+ import path12 from "path";
2649
+ import { promises as fs11 } from "fs";
2650
+ import Parser from "tree-sitter";
2651
+ import JavaScript from "tree-sitter-javascript";
2652
+ import Python from "tree-sitter-python";
2653
+ import {
2654
+ EdgeType as EdgeType5,
2655
+ Provenance as Provenance3,
2656
+ confidenceForExtracted as confidenceForExtracted2,
2657
+ extractedEdgeId as extractedEdgeId4,
2658
+ fileId as fileId3
2659
+ } from "@neat.is/types";
2660
+ var PARSE_CHUNK = 16384;
2661
+ function parseSource(parser, source) {
2662
+ return parser.parse(
2663
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
2664
+ );
2665
+ }
2666
+ function makeJsParser() {
2667
+ const p = new Parser();
2668
+ p.setLanguage(JavaScript);
2669
+ return p;
2670
+ }
2671
+ function makePyParser() {
2672
+ const p = new Parser();
2673
+ p.setLanguage(Python);
2674
+ return p;
2675
+ }
2676
+ function stringLiteralText(node) {
2677
+ for (let i = 0; i < node.childCount; i++) {
2678
+ const child = node.child(i);
2679
+ if (child?.type === "string_fragment") return child.text;
2680
+ }
2681
+ const raw = node.text;
2682
+ if (raw.length >= 2) return raw.slice(1, -1);
2683
+ return raw.length === 0 ? null : "";
2684
+ }
2685
+ function clipSnippet(text) {
2686
+ const oneLine = text.split("\n")[0] ?? text;
2687
+ return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
2688
+ }
2689
+ function collectJsImports(node, out) {
2690
+ if (node.type === "import_statement") {
2691
+ const source = node.childForFieldName("source");
2692
+ if (source) {
2693
+ const specifier = stringLiteralText(source);
2694
+ if (specifier) {
2695
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2696
+ }
2697
+ }
2698
+ return;
2699
+ }
2700
+ if (node.type === "call_expression") {
2701
+ const fn = node.childForFieldName("function");
2702
+ if (fn?.type === "identifier" && fn.text === "require") {
2703
+ const args = node.childForFieldName("arguments");
2704
+ const firstArg = args?.namedChild(0);
2705
+ if (firstArg?.type === "string") {
2706
+ const specifier = stringLiteralText(firstArg);
2707
+ if (specifier) {
2708
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2709
+ }
2710
+ }
2711
+ }
2712
+ }
2713
+ for (let i = 0; i < node.namedChildCount; i++) {
2714
+ const child = node.namedChild(i);
2715
+ if (child) collectJsImports(child, out);
2716
+ }
2717
+ }
2718
+ function collectPyImports(node, out) {
2719
+ if (node.type === "import_from_statement") {
2720
+ let level = 0;
2721
+ let modulePath = "";
2722
+ let pastFrom = false;
2723
+ for (let i = 0; i < node.childCount; i++) {
2724
+ const child = node.child(i);
2725
+ if (!child) continue;
2726
+ if (!pastFrom) {
2727
+ if (child.type === "from") pastFrom = true;
2728
+ continue;
2729
+ }
2730
+ if (child.type === "import") break;
2731
+ if (child.type === "relative_import") {
2732
+ for (let j = 0; j < child.childCount; j++) {
2733
+ const rc = child.child(j);
2734
+ if (!rc) continue;
2735
+ if (rc.type === "import_prefix") {
2736
+ for (let k = 0; k < rc.childCount; k++) {
2737
+ if (rc.child(k)?.type === ".") level++;
2738
+ }
2739
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
2740
+ }
2741
+ break;
2742
+ }
2743
+ if (child.type === "dotted_name") {
2744
+ modulePath = child.text;
2745
+ break;
2746
+ }
2747
+ }
2748
+ if (level > 0 || modulePath) {
2749
+ out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2750
+ }
2751
+ }
2752
+ for (let i = 0; i < node.namedChildCount; i++) {
2753
+ const child = node.namedChild(i);
2754
+ if (child) collectPyImports(child, out);
2755
+ }
2756
+ }
2757
+ async function fileExists(p) {
2758
+ try {
2759
+ await fs11.access(p);
2760
+ return true;
2761
+ } catch {
2762
+ return false;
2763
+ }
2764
+ }
2765
+ function isWithinServiceDir(candidate, serviceDir) {
2766
+ const rel = path12.relative(serviceDir, candidate);
2767
+ return rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
2768
+ }
2769
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
2770
+ var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
2771
+ async function firstExistingCandidate(base, serviceDir) {
2772
+ for (const ext of JS_EXTENSIONS) {
2773
+ const candidate = base + ext;
2774
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2775
+ return toPosix2(path12.relative(serviceDir, candidate));
2776
+ }
2777
+ }
2778
+ for (const indexFile of JS_INDEX_FILES) {
2779
+ const candidate = path12.join(base, indexFile);
2780
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2781
+ return toPosix2(path12.relative(serviceDir, candidate));
2782
+ }
2783
+ }
2784
+ return null;
2785
+ }
2786
+ async function loadTsPathConfig(serviceDir) {
2787
+ const tsconfigPath = path12.join(serviceDir, "tsconfig.json");
2788
+ let raw;
2789
+ try {
2790
+ raw = await fs11.readFile(tsconfigPath, "utf8");
2791
+ } catch {
2792
+ return null;
2793
+ }
2794
+ try {
2795
+ const parsed = JSON.parse(raw);
2796
+ const paths = parsed.compilerOptions?.paths;
2797
+ if (!paths || Object.keys(paths).length === 0) return null;
2798
+ const baseUrl = parsed.compilerOptions?.baseUrl;
2799
+ return { paths, baseDir: baseUrl ? path12.resolve(serviceDir, baseUrl) : serviceDir };
2800
+ } catch (err) {
2801
+ recordExtractionError("import alias resolution", tsconfigPath, err);
2802
+ return null;
2803
+ }
2804
+ }
2805
+ async function resolveTsAlias(specifier, config, serviceDir) {
2806
+ for (const [pattern, targets] of Object.entries(config.paths)) {
2807
+ let suffix = null;
2808
+ if (pattern === specifier) {
2809
+ suffix = "";
2810
+ } else if (pattern.endsWith("/*")) {
2811
+ const prefix = pattern.slice(0, -1);
2812
+ if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
2813
+ }
2814
+ if (suffix === null) continue;
2815
+ for (const target of targets) {
2816
+ const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
2817
+ const resolvedBase = path12.resolve(config.baseDir, targetBase, suffix);
2818
+ const hit = await firstExistingCandidate(resolvedBase, serviceDir);
2819
+ if (hit) return hit;
2820
+ if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
2821
+ return toPosix2(path12.relative(serviceDir, resolvedBase));
2822
+ }
2823
+ }
2824
+ }
2825
+ return null;
2826
+ }
2827
+ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
2828
+ if (!specifier) return null;
2829
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
2830
+ const base = path12.resolve(importerDir, specifier);
2831
+ const ext = path12.extname(specifier);
2832
+ if (ext) {
2833
+ if (ext === ".js" || ext === ".jsx") {
2834
+ const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
2835
+ const tsSibling = base.slice(0, -ext.length) + tsExt;
2836
+ if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
2837
+ return toPosix2(path12.relative(serviceDir, tsSibling));
2838
+ }
2839
+ }
2840
+ if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
2841
+ return toPosix2(path12.relative(serviceDir, base));
2842
+ }
2843
+ return null;
2844
+ }
2845
+ return firstExistingCandidate(base, serviceDir);
2846
+ }
2847
+ if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
2848
+ return null;
2849
+ }
2850
+ async function resolvePyImport(imp, importerPath, serviceDir) {
2851
+ if (!imp.modulePath) return null;
2852
+ const relPath = imp.modulePath.split(".").join("/");
2853
+ let baseDir;
2854
+ if (imp.level > 0) {
2855
+ baseDir = path12.dirname(importerPath);
2856
+ for (let i = 1; i < imp.level; i++) baseDir = path12.dirname(baseDir);
2857
+ } else {
2858
+ baseDir = serviceDir;
2859
+ }
2860
+ const candidates = [path12.join(baseDir, `${relPath}.py`), path12.join(baseDir, relPath, "__init__.py")];
2861
+ for (const candidate of candidates) {
2862
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2863
+ return toPosix2(path12.relative(serviceDir, candidate));
2864
+ }
2865
+ }
2866
+ return null;
2867
+ }
2868
+ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
2869
+ const importeeFileId = fileId3(serviceName, importeeRelPath);
2870
+ if (!graph.hasNode(importeeFileId)) return 0;
2871
+ const edgeId = extractedEdgeId4(importerFileId, importeeFileId, EdgeType5.IMPORTS);
2872
+ if (graph.hasEdge(edgeId)) return 0;
2873
+ const edge = {
2874
+ id: edgeId,
2875
+ source: importerFileId,
2876
+ target: importeeFileId,
2877
+ type: EdgeType5.IMPORTS,
2878
+ provenance: Provenance3.EXTRACTED,
2879
+ confidence: confidenceForExtracted2("structural"),
2880
+ evidence: { file: importerRelPath, line, snippet: snippet2 }
2881
+ };
2882
+ graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
2883
+ return 1;
2884
+ }
2885
+ async function addImports(graph, services) {
2886
+ const jsParser = makeJsParser();
2887
+ const pyParser = makePyParser();
2888
+ let edgesAdded = 0;
2889
+ for (const service of services) {
2890
+ const tsPaths = await loadTsPathConfig(service.dir);
2891
+ const files = await loadSourceFiles(service.dir);
2892
+ for (const file of files) {
2893
+ if (isTestPath(file.path)) continue;
2894
+ const relFile = toPosix2(path12.relative(service.dir, file.path));
2895
+ const importerFileId = fileId3(service.pkg.name, relFile);
2896
+ const isPython = path12.extname(file.path) === ".py";
2897
+ if (isPython) {
2898
+ let pyImports = [];
2899
+ try {
2900
+ const tree = parseSource(pyParser, file.content);
2901
+ collectPyImports(tree.rootNode, pyImports);
2902
+ } catch (err) {
2903
+ recordExtractionError("import extraction", file.path, err);
2904
+ continue;
2905
+ }
2906
+ for (const imp of pyImports) {
2907
+ const resolved = await resolvePyImport(imp, file.path, service.dir);
2908
+ if (!resolved) continue;
2909
+ edgesAdded += emitImportEdge(
2910
+ graph,
2911
+ service.pkg.name,
2912
+ importerFileId,
2913
+ relFile,
2914
+ resolved,
2915
+ imp.line,
2916
+ imp.snippet
2917
+ );
2918
+ }
2919
+ continue;
2920
+ }
2921
+ let jsImports = [];
2922
+ try {
2923
+ const tree = parseSource(jsParser, file.content);
2924
+ collectJsImports(tree.rootNode, jsImports);
2925
+ } catch (err) {
2926
+ recordExtractionError("import extraction", file.path, err);
2927
+ continue;
2928
+ }
2929
+ for (const imp of jsImports) {
2930
+ const resolved = await resolveJsImport(imp.specifier, path12.dirname(file.path), service.dir, tsPaths);
2931
+ if (!resolved) continue;
2932
+ edgesAdded += emitImportEdge(
2933
+ graph,
2934
+ service.pkg.name,
2935
+ importerFileId,
2936
+ relFile,
2937
+ resolved,
2938
+ imp.line,
2939
+ imp.snippet
2940
+ );
2941
+ }
2942
+ }
2943
+ }
2944
+ return { nodesAdded: 0, edgesAdded };
2945
+ }
2946
+
2947
+ // src/extract/databases/index.ts
2948
+ import path20 from "path";
2949
+ import {
2950
+ EdgeType as EdgeType6,
2951
+ NodeType as NodeType7,
2952
+ Provenance as Provenance4,
2521
2953
  databaseId as databaseId2,
2522
- confidenceForExtracted
2954
+ confidenceForExtracted as confidenceForExtracted3
2523
2955
  } from "@neat.is/types";
2524
2956
 
2525
2957
  // src/extract/databases/db-config-yaml.ts
2526
- import path10 from "path";
2958
+ import path13 from "path";
2527
2959
  async function parse(serviceDir) {
2528
- const yamlPath = path10.join(serviceDir, "db-config.yaml");
2960
+ const yamlPath = path13.join(serviceDir, "db-config.yaml");
2529
2961
  if (!await exists(yamlPath)) return [];
2530
2962
  const raw = await readYaml(yamlPath);
2531
2963
  return [
@@ -2542,12 +2974,12 @@ async function parse(serviceDir) {
2542
2974
  var dbConfigYamlParser = { name: "db-config.yaml", parse };
2543
2975
 
2544
2976
  // src/extract/databases/dotenv.ts
2545
- import { promises as fs11 } from "fs";
2546
- import path12 from "path";
2977
+ import { promises as fs13 } from "fs";
2978
+ import path15 from "path";
2547
2979
 
2548
2980
  // src/extract/databases/shared.ts
2549
- import { promises as fs10 } from "fs";
2550
- import path11 from "path";
2981
+ import { promises as fs12 } from "fs";
2982
+ import path14 from "path";
2551
2983
  function schemeToEngine(scheme) {
2552
2984
  const s = scheme.toLowerCase().split("+")[0];
2553
2985
  switch (s) {
@@ -2586,14 +3018,14 @@ function parseConnectionString(url) {
2586
3018
  }
2587
3019
  async function readIfExists(filePath) {
2588
3020
  try {
2589
- return await fs10.readFile(filePath, "utf8");
3021
+ return await fs12.readFile(filePath, "utf8");
2590
3022
  } catch {
2591
3023
  return null;
2592
3024
  }
2593
3025
  }
2594
3026
  async function findFirst(serviceDir, candidates) {
2595
3027
  for (const rel of candidates) {
2596
- const abs = path11.join(serviceDir, rel);
3028
+ const abs = path14.join(serviceDir, rel);
2597
3029
  const content = await readIfExists(abs);
2598
3030
  if (content !== null) return abs;
2599
3031
  }
@@ -2644,15 +3076,15 @@ function parseDotenvLine(line) {
2644
3076
  return { key, value };
2645
3077
  }
2646
3078
  async function parse2(serviceDir) {
2647
- const entries = await fs11.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3079
+ const entries = await fs13.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2648
3080
  const configs = [];
2649
3081
  const seen = /* @__PURE__ */ new Set();
2650
3082
  for (const entry of entries) {
2651
3083
  if (!entry.isFile()) continue;
2652
3084
  const match = isConfigFile(entry.name);
2653
3085
  if (!match.match || match.fileType !== "env") continue;
2654
- const filePath = path12.join(serviceDir, entry.name);
2655
- const content = await fs11.readFile(filePath, "utf8");
3086
+ const filePath = path15.join(serviceDir, entry.name);
3087
+ const content = await fs13.readFile(filePath, "utf8");
2656
3088
  for (const line of content.split("\n")) {
2657
3089
  const parsed = parseDotenvLine(line);
2658
3090
  if (!parsed) continue;
@@ -2670,9 +3102,9 @@ async function parse2(serviceDir) {
2670
3102
  var dotenvParser = { name: ".env", parse: parse2 };
2671
3103
 
2672
3104
  // src/extract/databases/prisma.ts
2673
- import path13 from "path";
3105
+ import path16 from "path";
2674
3106
  async function parse3(serviceDir) {
2675
- const schemaPath = path13.join(serviceDir, "prisma", "schema.prisma");
3107
+ const schemaPath = path16.join(serviceDir, "prisma", "schema.prisma");
2676
3108
  const content = await readIfExists(schemaPath);
2677
3109
  if (!content) return [];
2678
3110
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2801,10 +3233,10 @@ async function parse5(serviceDir) {
2801
3233
  var knexParser = { name: "knex", parse: parse5 };
2802
3234
 
2803
3235
  // src/extract/databases/ormconfig.ts
2804
- import path14 from "path";
3236
+ import path17 from "path";
2805
3237
  async function parse6(serviceDir) {
2806
3238
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2807
- const abs = path14.join(serviceDir, candidate);
3239
+ const abs = path17.join(serviceDir, candidate);
2808
3240
  if (!await exists(abs)) continue;
2809
3241
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2810
3242
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2862,9 +3294,9 @@ async function parse7(serviceDir) {
2862
3294
  var typeormParser = { name: "typeorm", parse: parse7 };
2863
3295
 
2864
3296
  // src/extract/databases/sequelize.ts
2865
- import path15 from "path";
3297
+ import path18 from "path";
2866
3298
  async function parse8(serviceDir) {
2867
- const configPath = path15.join(serviceDir, "config", "config.json");
3299
+ const configPath = path18.join(serviceDir, "config", "config.json");
2868
3300
  if (!await exists(configPath)) return [];
2869
3301
  const raw = await readJson(configPath);
2870
3302
  const out = [];
@@ -2890,7 +3322,7 @@ async function parse8(serviceDir) {
2890
3322
  var sequelizeParser = { name: "sequelize", parse: parse8 };
2891
3323
 
2892
3324
  // src/extract/databases/docker-compose.ts
2893
- import path16 from "path";
3325
+ import path19 from "path";
2894
3326
  function portFromService(svc) {
2895
3327
  for (const raw of svc.ports ?? []) {
2896
3328
  const str = String(raw);
@@ -2917,7 +3349,7 @@ function databaseFromEnv(svc) {
2917
3349
  }
2918
3350
  async function parse9(serviceDir) {
2919
3351
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2920
- const abs = path16.join(serviceDir, name);
3352
+ const abs = path19.join(serviceDir, name);
2921
3353
  if (!await exists(abs)) continue;
2922
3354
  const raw = await readYaml(abs);
2923
3355
  if (!raw?.services) return [];
@@ -2959,7 +3391,7 @@ function compatibleDriversFor(engine) {
2959
3391
  function toDatabaseNode(config) {
2960
3392
  return {
2961
3393
  id: databaseId2(config.host),
2962
- type: NodeType6.DatabaseNode,
3394
+ type: NodeType7.DatabaseNode,
2963
3395
  name: config.database || config.host,
2964
3396
  engine: config.engine,
2965
3397
  engineVersion: config.engineVersion,
@@ -3089,19 +3521,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3089
3521
  discoveredVia: mergedDiscoveredVia
3090
3522
  });
3091
3523
  }
3524
+ const relConfigFile = toPosix2(path20.relative(service.dir, config.sourceFile));
3525
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3526
+ graph,
3527
+ service.pkg.name,
3528
+ service.node.id,
3529
+ relConfigFile
3530
+ );
3531
+ nodesAdded += fn;
3532
+ edgesAdded += fe;
3533
+ const evidenceFile = toPosix2(path20.relative(scanPath, config.sourceFile));
3092
3534
  const edge = {
3093
- id: extractedEdgeId2(service.node.id, dbNode.id, EdgeType4.CONNECTS_TO),
3094
- source: service.node.id,
3535
+ id: extractedEdgeId2(fileNodeId, dbNode.id, EdgeType6.CONNECTS_TO),
3536
+ source: fileNodeId,
3095
3537
  target: dbNode.id,
3096
- type: EdgeType4.CONNECTS_TO,
3097
- provenance: Provenance2.EXTRACTED,
3098
- confidence: confidenceForExtracted("structural"),
3099
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3100
- // Ghost-edge cleanup keys retirement on this; the conditional
3101
- // sourceFile spread that used to live here was a v0.1.x leftover.
3102
- evidence: {
3103
- file: path17.relative(scanPath, config.sourceFile).split(path17.sep).join("/")
3104
- }
3538
+ type: EdgeType6.CONNECTS_TO,
3539
+ provenance: Provenance4.EXTRACTED,
3540
+ confidence: confidenceForExtracted3("structural"),
3541
+ evidence: { file: evidenceFile }
3105
3542
  };
3106
3543
  if (!graph.hasEdge(edge.id)) {
3107
3544
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3126,194 +3563,98 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3126
3563
  }
3127
3564
 
3128
3565
  // src/extract/configs.ts
3129
- import { promises as fs12 } from "fs";
3130
- import path18 from "path";
3566
+ import { promises as fs14 } from "fs";
3567
+ import path21 from "path";
3131
3568
  import {
3132
- EdgeType as EdgeType5,
3133
- NodeType as NodeType7,
3134
- Provenance as Provenance3,
3569
+ EdgeType as EdgeType7,
3570
+ NodeType as NodeType8,
3571
+ Provenance as Provenance5,
3135
3572
  configId,
3136
- confidenceForExtracted as confidenceForExtracted2
3573
+ confidenceForExtracted as confidenceForExtracted4
3137
3574
  } from "@neat.is/types";
3138
3575
  async function walkConfigFiles(dir) {
3139
3576
  const out = [];
3140
3577
  async function walk(current) {
3141
- const entries = await fs12.readdir(current, { withFileTypes: true });
3578
+ const entries = await fs14.readdir(current, { withFileTypes: true });
3142
3579
  for (const entry of entries) {
3143
- const full = path18.join(current, entry.name);
3580
+ const full = path21.join(current, entry.name);
3144
3581
  if (entry.isDirectory()) {
3145
3582
  if (IGNORED_DIRS.has(entry.name)) continue;
3146
3583
  if (await isPythonVenvDir(full)) continue;
3147
3584
  await walk(full);
3148
3585
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
3149
3586
  out.push(full);
3150
- }
3151
- }
3152
- }
3153
- await walk(dir);
3154
- return out;
3155
- }
3156
- async function addConfigNodes(graph, services, scanPath) {
3157
- let nodesAdded = 0;
3158
- let edgesAdded = 0;
3159
- for (const service of services) {
3160
- const configFiles = await walkConfigFiles(service.dir);
3161
- for (const file of configFiles) {
3162
- const relPath = path18.relative(scanPath, file);
3163
- const node = {
3164
- id: configId(relPath),
3165
- type: NodeType7.ConfigNode,
3166
- name: path18.basename(file),
3167
- path: relPath,
3168
- fileType: isConfigFile(path18.basename(file)).fileType
3169
- };
3170
- if (!graph.hasNode(node.id)) {
3171
- graph.addNode(node.id, node);
3172
- nodesAdded++;
3173
- }
3174
- const edge = {
3175
- id: extractedEdgeId2(service.node.id, node.id, EdgeType5.CONFIGURED_BY),
3176
- source: service.node.id,
3177
- target: node.id,
3178
- type: EdgeType5.CONFIGURED_BY,
3179
- provenance: Provenance3.EXTRACTED,
3180
- confidence: confidenceForExtracted2("structural"),
3181
- evidence: { file: relPath.split(path18.sep).join("/") }
3182
- };
3183
- if (!graph.hasEdge(edge.id)) {
3184
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3185
- edgesAdded++;
3186
- }
3187
- }
3188
- }
3189
- return { nodesAdded, edgesAdded };
3190
- }
3191
-
3192
- // src/extract/calls/index.ts
3193
- import {
3194
- EdgeType as EdgeType8,
3195
- NodeType as NodeType9,
3196
- Provenance as Provenance6,
3197
- confidenceForExtracted as confidenceForExtracted5,
3198
- passesExtractedFloor as passesExtractedFloor2
3199
- } from "@neat.is/types";
3200
-
3201
- // src/extract/calls/http.ts
3202
- import path20 from "path";
3203
- import Parser from "tree-sitter";
3204
- import JavaScript from "tree-sitter-javascript";
3205
- import Python from "tree-sitter-python";
3206
- import {
3207
- EdgeType as EdgeType7,
3208
- Provenance as Provenance5,
3209
- confidenceForExtracted as confidenceForExtracted4,
3210
- passesExtractedFloor
3211
- } from "@neat.is/types";
3212
-
3213
- // src/extract/calls/shared.ts
3214
- import { promises as fs13 } from "fs";
3215
- import path19 from "path";
3216
- import {
3217
- EdgeType as EdgeType6,
3218
- NodeType as NodeType8,
3219
- Provenance as Provenance4,
3220
- confidenceForExtracted as confidenceForExtracted3,
3221
- extractedEdgeId as extractedEdgeId3,
3222
- fileId as fileId2
3223
- } from "@neat.is/types";
3224
- async function walkSourceFiles(dir) {
3225
- const out = [];
3226
- async function walk(current) {
3227
- const entries = await fs13.readdir(current, { withFileTypes: true }).catch(() => []);
3228
- for (const entry of entries) {
3229
- const full = path19.join(current, entry.name);
3230
- if (entry.isDirectory()) {
3231
- if (IGNORED_DIRS.has(entry.name)) continue;
3232
- if (await isPythonVenvDir(full)) continue;
3233
- await walk(full);
3234
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
3235
- out.push(full);
3236
- }
3237
- }
3238
- }
3239
- await walk(dir);
3240
- return out;
3241
- }
3242
- async function loadSourceFiles(dir) {
3243
- const paths = await walkSourceFiles(dir);
3244
- const out = [];
3245
- for (const p of paths) {
3246
- try {
3247
- const content = await fs13.readFile(p, "utf8");
3248
- out.push({ path: p, content });
3249
- } catch {
3250
- }
3251
- }
3252
- return out;
3253
- }
3254
- function lineOf(text, needle) {
3255
- const idx = text.indexOf(needle);
3256
- if (idx < 0) return 1;
3257
- return text.slice(0, idx).split("\n").length;
3258
- }
3259
- function snippet(text, line) {
3260
- const lines = text.split("\n");
3261
- return (lines[line - 1] ?? "").trim();
3262
- }
3263
- function toPosix2(p) {
3264
- return p.split("\\").join("/");
3265
- }
3266
- function languageForPath(relPath) {
3267
- switch (path19.extname(relPath).toLowerCase()) {
3268
- case ".py":
3269
- return "python";
3270
- case ".ts":
3271
- case ".tsx":
3272
- return "typescript";
3273
- case ".js":
3274
- case ".jsx":
3275
- case ".mjs":
3276
- case ".cjs":
3277
- return "javascript";
3278
- default:
3279
- return void 0;
3587
+ }
3588
+ }
3280
3589
  }
3590
+ await walk(dir);
3591
+ return out;
3281
3592
  }
3282
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3593
+ async function addConfigNodes(graph, services, scanPath) {
3283
3594
  let nodesAdded = 0;
3284
3595
  let edgesAdded = 0;
3285
- const fileNodeId = fileId2(serviceName, relPath);
3286
- if (!graph.hasNode(fileNodeId)) {
3287
- const language = languageForPath(relPath);
3288
- const node = {
3289
- id: fileNodeId,
3290
- type: NodeType8.FileNode,
3291
- service: serviceName,
3292
- path: relPath,
3293
- ...language ? { language } : {},
3294
- discoveredVia: "static"
3295
- };
3296
- graph.addNode(fileNodeId, node);
3297
- nodesAdded++;
3298
- }
3299
- const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType6.CONTAINS);
3300
- if (!graph.hasEdge(containsId)) {
3301
- const edge = {
3302
- id: containsId,
3303
- source: serviceNodeId,
3304
- target: fileNodeId,
3305
- type: EdgeType6.CONTAINS,
3306
- provenance: Provenance4.EXTRACTED,
3307
- confidence: confidenceForExtracted3("structural"),
3308
- evidence: { file: relPath }
3309
- };
3310
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3311
- edgesAdded++;
3596
+ for (const service of services) {
3597
+ const configFiles = await walkConfigFiles(service.dir);
3598
+ for (const file of configFiles) {
3599
+ const relPath = path21.relative(scanPath, file);
3600
+ const node = {
3601
+ id: configId(relPath),
3602
+ type: NodeType8.ConfigNode,
3603
+ name: path21.basename(file),
3604
+ path: relPath,
3605
+ fileType: isConfigFile(path21.basename(file)).fileType
3606
+ };
3607
+ if (!graph.hasNode(node.id)) {
3608
+ graph.addNode(node.id, node);
3609
+ nodesAdded++;
3610
+ }
3611
+ const relToService = toPosix2(path21.relative(service.dir, file));
3612
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3613
+ graph,
3614
+ service.pkg.name,
3615
+ service.node.id,
3616
+ relToService
3617
+ );
3618
+ nodesAdded += fn;
3619
+ edgesAdded += fe;
3620
+ const edge = {
3621
+ id: extractedEdgeId2(fileNodeId, node.id, EdgeType7.CONFIGURED_BY),
3622
+ source: fileNodeId,
3623
+ target: node.id,
3624
+ type: EdgeType7.CONFIGURED_BY,
3625
+ provenance: Provenance5.EXTRACTED,
3626
+ confidence: confidenceForExtracted4("structural"),
3627
+ evidence: { file: relPath.split(path21.sep).join("/") }
3628
+ };
3629
+ if (!graph.hasEdge(edge.id)) {
3630
+ graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3631
+ edgesAdded++;
3632
+ }
3633
+ }
3312
3634
  }
3313
- return { fileNodeId, nodesAdded, edgesAdded };
3635
+ return { nodesAdded, edgesAdded };
3314
3636
  }
3315
3637
 
3638
+ // src/extract/calls/index.ts
3639
+ import {
3640
+ EdgeType as EdgeType9,
3641
+ NodeType as NodeType9,
3642
+ Provenance as Provenance7,
3643
+ confidenceForExtracted as confidenceForExtracted6,
3644
+ passesExtractedFloor as passesExtractedFloor2
3645
+ } from "@neat.is/types";
3646
+
3316
3647
  // src/extract/calls/http.ts
3648
+ import path22 from "path";
3649
+ import Parser2 from "tree-sitter";
3650
+ import JavaScript2 from "tree-sitter-javascript";
3651
+ import Python2 from "tree-sitter-python";
3652
+ import {
3653
+ EdgeType as EdgeType8,
3654
+ Provenance as Provenance6,
3655
+ confidenceForExtracted as confidenceForExtracted5,
3656
+ passesExtractedFloor
3657
+ } from "@neat.is/types";
3317
3658
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3318
3659
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3319
3660
  function isInsideJsxExternalLink(node) {
@@ -3341,14 +3682,14 @@ function collectStringLiterals(node, out) {
3341
3682
  if (child) collectStringLiterals(child, out);
3342
3683
  }
3343
3684
  }
3344
- var PARSE_CHUNK = 16384;
3345
- function parseSource(parser, source) {
3685
+ var PARSE_CHUNK2 = 16384;
3686
+ function parseSource2(parser, source) {
3346
3687
  return parser.parse(
3347
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3688
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
3348
3689
  );
3349
3690
  }
3350
3691
  function callsFromSource(source, parser, knownHosts) {
3351
- const tree = parseSource(parser, source);
3692
+ const tree = parseSource2(parser, source);
3352
3693
  const literals = [];
3353
3694
  collectStringLiterals(tree.rootNode, literals);
3354
3695
  const out = [];
@@ -3362,25 +3703,25 @@ function callsFromSource(source, parser, knownHosts) {
3362
3703
  }
3363
3704
  return out;
3364
3705
  }
3365
- function makeJsParser() {
3366
- const p = new Parser();
3367
- p.setLanguage(JavaScript);
3706
+ function makeJsParser2() {
3707
+ const p = new Parser2();
3708
+ p.setLanguage(JavaScript2);
3368
3709
  return p;
3369
3710
  }
3370
- function makePyParser() {
3371
- const p = new Parser();
3372
- p.setLanguage(Python);
3711
+ function makePyParser2() {
3712
+ const p = new Parser2();
3713
+ p.setLanguage(Python2);
3373
3714
  return p;
3374
3715
  }
3375
3716
  async function addHttpCallEdges(graph, services) {
3376
- const jsParser = makeJsParser();
3377
- const pyParser = makePyParser();
3717
+ const jsParser = makeJsParser2();
3718
+ const pyParser = makePyParser2();
3378
3719
  const knownHosts = /* @__PURE__ */ new Set();
3379
3720
  const hostToNodeId = /* @__PURE__ */ new Map();
3380
3721
  for (const service of services) {
3381
- knownHosts.add(path20.basename(service.dir));
3722
+ knownHosts.add(path22.basename(service.dir));
3382
3723
  knownHosts.add(service.pkg.name);
3383
- hostToNodeId.set(path20.basename(service.dir), service.node.id);
3724
+ hostToNodeId.set(path22.basename(service.dir), service.node.id);
3384
3725
  hostToNodeId.set(service.pkg.name, service.node.id);
3385
3726
  }
3386
3727
  let nodesAdded = 0;
@@ -3390,7 +3731,7 @@ async function addHttpCallEdges(graph, services) {
3390
3731
  const seen = /* @__PURE__ */ new Set();
3391
3732
  for (const file of files) {
3392
3733
  if (isTestPath(file.path)) continue;
3393
- const parser = path20.extname(file.path) === ".py" ? pyParser : jsParser;
3734
+ const parser = path22.extname(file.path) === ".py" ? pyParser : jsParser;
3394
3735
  let sites;
3395
3736
  try {
3396
3737
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -3399,14 +3740,14 @@ async function addHttpCallEdges(graph, services) {
3399
3740
  continue;
3400
3741
  }
3401
3742
  if (sites.length === 0) continue;
3402
- const relFile = toPosix2(path20.relative(service.dir, file.path));
3743
+ const relFile = toPosix2(path22.relative(service.dir, file.path));
3403
3744
  for (const site of sites) {
3404
3745
  const targetId = hostToNodeId.get(site.host);
3405
3746
  if (!targetId || targetId === service.node.id) continue;
3406
3747
  const dedupKey = `${relFile}|${targetId}`;
3407
3748
  if (seen.has(dedupKey)) continue;
3408
3749
  seen.add(dedupKey);
3409
- const confidence = confidenceForExtracted4("hostname-shape-match");
3750
+ const confidence = confidenceForExtracted5("hostname-shape-match");
3410
3751
  const ev = {
3411
3752
  file: relFile,
3412
3753
  line: site.line,
@@ -3424,21 +3765,21 @@ async function addHttpCallEdges(graph, services) {
3424
3765
  noteExtractedDropped({
3425
3766
  source: fileNodeId,
3426
3767
  target: targetId,
3427
- type: EdgeType7.CALLS,
3768
+ type: EdgeType8.CALLS,
3428
3769
  confidence,
3429
3770
  confidenceKind: "hostname-shape-match",
3430
3771
  evidence: ev
3431
3772
  });
3432
3773
  continue;
3433
3774
  }
3434
- const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType7.CALLS);
3775
+ const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType8.CALLS);
3435
3776
  if (!graph.hasEdge(edgeId)) {
3436
3777
  const edge = {
3437
3778
  id: edgeId,
3438
3779
  source: fileNodeId,
3439
3780
  target: targetId,
3440
- type: EdgeType7.CALLS,
3441
- provenance: Provenance5.EXTRACTED,
3781
+ type: EdgeType8.CALLS,
3782
+ provenance: Provenance6.EXTRACTED,
3442
3783
  confidence,
3443
3784
  evidence: ev
3444
3785
  };
@@ -3452,7 +3793,7 @@ async function addHttpCallEdges(graph, services) {
3452
3793
  }
3453
3794
 
3454
3795
  // src/extract/calls/kafka.ts
3455
- import path21 from "path";
3796
+ import path23 from "path";
3456
3797
  import { infraId } from "@neat.is/types";
3457
3798
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3458
3799
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3483,7 +3824,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3483
3824
  // tier (ADR-066).
3484
3825
  confidenceKind: "verified-call-site",
3485
3826
  evidence: {
3486
- file: path21.relative(serviceDir, file.path),
3827
+ file: path23.relative(serviceDir, file.path),
3487
3828
  line,
3488
3829
  snippet: snippet(file.content, line)
3489
3830
  }
@@ -3495,7 +3836,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3495
3836
  }
3496
3837
 
3497
3838
  // src/extract/calls/redis.ts
3498
- import path22 from "path";
3839
+ import path24 from "path";
3499
3840
  import { infraId as infraId2 } from "@neat.is/types";
3500
3841
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3501
3842
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3518,7 +3859,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3518
3859
  // support tier (ADR-066).
3519
3860
  confidenceKind: "url-with-structural-support",
3520
3861
  evidence: {
3521
- file: path22.relative(serviceDir, file.path),
3862
+ file: path24.relative(serviceDir, file.path),
3522
3863
  line,
3523
3864
  snippet: snippet(file.content, line)
3524
3865
  }
@@ -3528,7 +3869,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3528
3869
  }
3529
3870
 
3530
3871
  // src/extract/calls/aws.ts
3531
- import path23 from "path";
3872
+ import path25 from "path";
3532
3873
  import { infraId as infraId3 } from "@neat.is/types";
3533
3874
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3534
3875
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3562,7 +3903,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3562
3903
  // (ADR-066).
3563
3904
  confidenceKind: "verified-call-site",
3564
3905
  evidence: {
3565
- file: path23.relative(serviceDir, file.path),
3906
+ file: path25.relative(serviceDir, file.path),
3566
3907
  line,
3567
3908
  snippet: snippet(file.content, line)
3568
3909
  }
@@ -3586,13 +3927,14 @@ function awsEndpointsFromFile(file, serviceDir) {
3586
3927
  }
3587
3928
 
3588
3929
  // src/extract/calls/grpc.ts
3589
- import path24 from "path";
3930
+ import path26 from "path";
3590
3931
  import { infraId as infraId4 } from "@neat.is/types";
3591
3932
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3592
3933
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3593
3934
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3594
3935
  function isLikelyAddress(value) {
3595
3936
  if (!value) return false;
3937
+ if (/\s/.test(value) || value.startsWith("{")) return false;
3596
3938
  return /:\d{2,5}$/.test(value) || value.includes(".");
3597
3939
  }
3598
3940
  function normaliseForMatch(s) {
@@ -3644,7 +3986,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3644
3986
  // tier (ADR-066).
3645
3987
  confidenceKind: "verified-call-site",
3646
3988
  evidence: {
3647
- file: path24.relative(serviceDir, file.path),
3989
+ file: path26.relative(serviceDir, file.path),
3648
3990
  line,
3649
3991
  snippet: snippet(file.content, line)
3650
3992
  }
@@ -3657,11 +3999,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3657
3999
  function edgeTypeFromEndpoint(ep) {
3658
4000
  switch (ep.edgeType) {
3659
4001
  case "PUBLISHES_TO":
3660
- return EdgeType8.PUBLISHES_TO;
4002
+ return EdgeType9.PUBLISHES_TO;
3661
4003
  case "CONSUMES_FROM":
3662
- return EdgeType8.CONSUMES_FROM;
4004
+ return EdgeType9.CONSUMES_FROM;
3663
4005
  default:
3664
- return EdgeType8.CALLS;
4006
+ return EdgeType9.CALLS;
3665
4007
  }
3666
4008
  }
3667
4009
  function isAwsKind(kind) {
@@ -3700,7 +4042,7 @@ async function addExternalEndpointEdges(graph, services) {
3700
4042
  nodesAdded++;
3701
4043
  }
3702
4044
  const edgeType = edgeTypeFromEndpoint(ep);
3703
- const confidence = confidenceForExtracted5(ep.confidenceKind);
4045
+ const confidence = confidenceForExtracted6(ep.confidenceKind);
3704
4046
  const relFile = toPosix2(ep.evidence.file);
3705
4047
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3706
4048
  graph,
@@ -3730,7 +4072,7 @@ async function addExternalEndpointEdges(graph, services) {
3730
4072
  source: fileNodeId,
3731
4073
  target: ep.infraId,
3732
4074
  type: edgeType,
3733
- provenance: Provenance6.EXTRACTED,
4075
+ provenance: Provenance7.EXTRACTED,
3734
4076
  confidence,
3735
4077
  evidence: ep.evidence
3736
4078
  };
@@ -3751,8 +4093,8 @@ async function addCallEdges(graph, services) {
3751
4093
  }
3752
4094
 
3753
4095
  // src/extract/infra/docker-compose.ts
3754
- import path25 from "path";
3755
- import { EdgeType as EdgeType9, Provenance as Provenance7, confidenceForExtracted as confidenceForExtracted6 } from "@neat.is/types";
4096
+ import path27 from "path";
4097
+ import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
3756
4098
 
3757
4099
  // src/extract/infra/shared.ts
3758
4100
  import { NodeType as NodeType10, infraId as infraId5 } from "@neat.is/types";
@@ -3788,7 +4130,7 @@ function dependsOnList(value) {
3788
4130
  }
3789
4131
  function serviceNameToServiceNode(name, services) {
3790
4132
  for (const s of services) {
3791
- if (s.node.name === name || path25.basename(s.dir) === name) return s.node.id;
4133
+ if (s.node.name === name || path27.basename(s.dir) === name) return s.node.id;
3792
4134
  }
3793
4135
  return null;
3794
4136
  }
@@ -3797,7 +4139,7 @@ async function addComposeInfra(graph, scanPath, services) {
3797
4139
  let edgesAdded = 0;
3798
4140
  let composePath = null;
3799
4141
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3800
- const abs = path25.join(scanPath, name);
4142
+ const abs = path27.join(scanPath, name);
3801
4143
  if (await exists(abs)) {
3802
4144
  composePath = abs;
3803
4145
  break;
@@ -3810,13 +4152,13 @@ async function addComposeInfra(graph, scanPath, services) {
3810
4152
  } catch (err) {
3811
4153
  recordExtractionError(
3812
4154
  "infra docker-compose",
3813
- path25.relative(scanPath, composePath),
4155
+ path27.relative(scanPath, composePath),
3814
4156
  err
3815
4157
  );
3816
4158
  return { nodesAdded, edgesAdded };
3817
4159
  }
3818
4160
  if (!compose?.services) return { nodesAdded, edgesAdded };
3819
- const evidenceFile = path25.relative(scanPath, composePath).split(path25.sep).join("/");
4161
+ const evidenceFile = path27.relative(scanPath, composePath).split(path27.sep).join("/");
3820
4162
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3821
4163
  for (const [composeName, svc] of Object.entries(compose.services)) {
3822
4164
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3838,15 +4180,15 @@ async function addComposeInfra(graph, scanPath, services) {
3838
4180
  for (const dep of dependsOnList(svc.depends_on)) {
3839
4181
  const targetId = composeNameToNodeId.get(dep);
3840
4182
  if (!targetId) continue;
3841
- const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType9.DEPENDS_ON);
4183
+ const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType10.DEPENDS_ON);
3842
4184
  if (graph.hasEdge(edgeId)) continue;
3843
4185
  const edge = {
3844
4186
  id: edgeId,
3845
4187
  source: sourceId,
3846
4188
  target: targetId,
3847
- type: EdgeType9.DEPENDS_ON,
3848
- provenance: Provenance7.EXTRACTED,
3849
- confidence: confidenceForExtracted6("structural"),
4189
+ type: EdgeType10.DEPENDS_ON,
4190
+ provenance: Provenance8.EXTRACTED,
4191
+ confidence: confidenceForExtracted7("structural"),
3850
4192
  evidence: { file: evidenceFile }
3851
4193
  };
3852
4194
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3857,9 +4199,9 @@ async function addComposeInfra(graph, scanPath, services) {
3857
4199
  }
3858
4200
 
3859
4201
  // src/extract/infra/dockerfile.ts
3860
- import path26 from "path";
3861
- import { promises as fs14 } from "fs";
3862
- import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
4202
+ import path28 from "path";
4203
+ import { promises as fs15 } from "fs";
4204
+ import { EdgeType as EdgeType11, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
3863
4205
  function runtimeImage(content) {
3864
4206
  const lines = content.split("\n");
3865
4207
  let last = null;
@@ -3878,15 +4220,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3878
4220
  let nodesAdded = 0;
3879
4221
  let edgesAdded = 0;
3880
4222
  for (const service of services) {
3881
- const dockerfilePath = path26.join(service.dir, "Dockerfile");
4223
+ const dockerfilePath = path28.join(service.dir, "Dockerfile");
3882
4224
  if (!await exists(dockerfilePath)) continue;
3883
4225
  let content;
3884
4226
  try {
3885
- content = await fs14.readFile(dockerfilePath, "utf8");
4227
+ content = await fs15.readFile(dockerfilePath, "utf8");
3886
4228
  } catch (err) {
3887
4229
  recordExtractionError(
3888
4230
  "infra dockerfile",
3889
- path26.relative(scanPath, dockerfilePath),
4231
+ path28.relative(scanPath, dockerfilePath),
3890
4232
  err
3891
4233
  );
3892
4234
  continue;
@@ -3898,17 +4240,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3898
4240
  graph.addNode(node.id, node);
3899
4241
  nodesAdded++;
3900
4242
  }
3901
- const edgeId = extractedEdgeId2(service.node.id, node.id, EdgeType10.RUNS_ON);
4243
+ const relDockerfile = toPosix2(path28.relative(service.dir, dockerfilePath));
4244
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4245
+ graph,
4246
+ service.pkg.name,
4247
+ service.node.id,
4248
+ relDockerfile
4249
+ );
4250
+ nodesAdded += fn;
4251
+ edgesAdded += fe;
4252
+ const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType11.RUNS_ON);
3902
4253
  if (!graph.hasEdge(edgeId)) {
3903
4254
  const edge = {
3904
4255
  id: edgeId,
3905
- source: service.node.id,
4256
+ source: fileNodeId,
3906
4257
  target: node.id,
3907
- type: EdgeType10.RUNS_ON,
3908
- provenance: Provenance8.EXTRACTED,
3909
- confidence: confidenceForExtracted7("structural"),
4258
+ type: EdgeType11.RUNS_ON,
4259
+ provenance: Provenance9.EXTRACTED,
4260
+ confidence: confidenceForExtracted8("structural"),
3910
4261
  evidence: {
3911
- file: path26.relative(scanPath, dockerfilePath).split(path26.sep).join("/")
4262
+ file: toPosix2(path28.relative(scanPath, dockerfilePath))
3912
4263
  }
3913
4264
  };
3914
4265
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3919,21 +4270,21 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3919
4270
  }
3920
4271
 
3921
4272
  // src/extract/infra/terraform.ts
3922
- import { promises as fs15 } from "fs";
3923
- import path27 from "path";
4273
+ import { promises as fs16 } from "fs";
4274
+ import path29 from "path";
3924
4275
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3925
4276
  async function walkTfFiles(start, depth = 0, max = 5) {
3926
4277
  if (depth > max) return [];
3927
4278
  const out = [];
3928
- const entries = await fs15.readdir(start, { withFileTypes: true }).catch(() => []);
4279
+ const entries = await fs16.readdir(start, { withFileTypes: true }).catch(() => []);
3929
4280
  for (const entry of entries) {
3930
4281
  if (entry.isDirectory()) {
3931
4282
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3932
- const child = path27.join(start, entry.name);
4283
+ const child = path29.join(start, entry.name);
3933
4284
  if (await isPythonVenvDir(child)) continue;
3934
4285
  out.push(...await walkTfFiles(child, depth + 1, max));
3935
4286
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3936
- out.push(path27.join(start, entry.name));
4287
+ out.push(path29.join(start, entry.name));
3937
4288
  }
3938
4289
  }
3939
4290
  return out;
@@ -3942,7 +4293,7 @@ async function addTerraformResources(graph, scanPath) {
3942
4293
  let nodesAdded = 0;
3943
4294
  const files = await walkTfFiles(scanPath);
3944
4295
  for (const file of files) {
3945
- const content = await fs15.readFile(file, "utf8");
4296
+ const content = await fs16.readFile(file, "utf8");
3946
4297
  RESOURCE_RE.lastIndex = 0;
3947
4298
  let m;
3948
4299
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3959,8 +4310,8 @@ async function addTerraformResources(graph, scanPath) {
3959
4310
  }
3960
4311
 
3961
4312
  // src/extract/infra/k8s.ts
3962
- import { promises as fs16 } from "fs";
3963
- import path28 from "path";
4313
+ import { promises as fs17 } from "fs";
4314
+ import path30 from "path";
3964
4315
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
3965
4316
  var K8S_KIND_TO_INFRA_KIND = {
3966
4317
  Service: "k8s-service",
@@ -3974,15 +4325,15 @@ var K8S_KIND_TO_INFRA_KIND = {
3974
4325
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3975
4326
  if (depth > max) return [];
3976
4327
  const out = [];
3977
- const entries = await fs16.readdir(start, { withFileTypes: true }).catch(() => []);
4328
+ const entries = await fs17.readdir(start, { withFileTypes: true }).catch(() => []);
3978
4329
  for (const entry of entries) {
3979
4330
  if (entry.isDirectory()) {
3980
4331
  if (IGNORED_DIRS.has(entry.name)) continue;
3981
- const child = path28.join(start, entry.name);
4332
+ const child = path30.join(start, entry.name);
3982
4333
  if (await isPythonVenvDir(child)) continue;
3983
4334
  out.push(...await walkYamlFiles2(child, depth + 1, max));
3984
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path28.extname(entry.name))) {
3985
- out.push(path28.join(start, entry.name));
4335
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path30.extname(entry.name))) {
4336
+ out.push(path30.join(start, entry.name));
3986
4337
  }
3987
4338
  }
3988
4339
  return out;
@@ -3991,7 +4342,7 @@ async function addK8sResources(graph, scanPath) {
3991
4342
  let nodesAdded = 0;
3992
4343
  const files = await walkYamlFiles2(scanPath);
3993
4344
  for (const file of files) {
3994
- const content = await fs16.readFile(file, "utf8");
4345
+ const content = await fs17.readFile(file, "utf8");
3995
4346
  let docs;
3996
4347
  try {
3997
4348
  docs = parseAllDocuments2(content).map((d) => d.toJSON());
@@ -4026,12 +4377,12 @@ async function addInfra(graph, scanPath, services) {
4026
4377
  }
4027
4378
 
4028
4379
  // src/extract/index.ts
4029
- import path30 from "path";
4380
+ import path32 from "path";
4030
4381
 
4031
4382
  // src/extract/retire.ts
4032
4383
  import { existsSync as existsSync2 } from "fs";
4033
- import path29 from "path";
4034
- import { NodeType as NodeType11, Provenance as Provenance9 } from "@neat.is/types";
4384
+ import path31 from "path";
4385
+ import { NodeType as NodeType11, Provenance as Provenance10 } from "@neat.is/types";
4035
4386
  function dropOrphanedFileNodes(graph) {
4036
4387
  const orphans = [];
4037
4388
  graph.forEachNode((id, attrs) => {
@@ -4048,7 +4399,7 @@ function retireEdgesByFile(graph, file) {
4048
4399
  const toDrop = [];
4049
4400
  graph.forEachEdge((id, attrs) => {
4050
4401
  const edge = attrs;
4051
- if (edge.provenance !== Provenance9.EXTRACTED) return;
4402
+ if (edge.provenance !== Provenance10.EXTRACTED) return;
4052
4403
  if (!edge.evidence?.file) return;
4053
4404
  if (edge.evidence.file === normalized) toDrop.push(id);
4054
4405
  });
@@ -4061,14 +4412,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4061
4412
  const bases = [scanPath, ...serviceDirs];
4062
4413
  graph.forEachEdge((id, attrs) => {
4063
4414
  const edge = attrs;
4064
- if (edge.provenance !== Provenance9.EXTRACTED) return;
4415
+ if (edge.provenance !== Provenance10.EXTRACTED) return;
4065
4416
  const evidenceFile = edge.evidence?.file;
4066
4417
  if (!evidenceFile) return;
4067
- if (path29.isAbsolute(evidenceFile)) {
4418
+ if (path31.isAbsolute(evidenceFile)) {
4068
4419
  if (!existsSync2(evidenceFile)) toDrop.push(id);
4069
4420
  return;
4070
4421
  }
4071
- const found = bases.some((base) => existsSync2(path29.join(base, evidenceFile)));
4422
+ const found = bases.some((base) => existsSync2(path31.join(base, evidenceFile)));
4072
4423
  if (!found) toDrop.push(id);
4073
4424
  });
4074
4425
  for (const id of toDrop) graph.dropEdge(id);
@@ -4083,6 +4434,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4083
4434
  const services = await discoverServices(scanPath);
4084
4435
  const phase1Nodes = addServiceNodes(graph, services);
4085
4436
  await addServiceAliases(graph, scanPath, services);
4437
+ const fileEnum = await addFiles(graph, services);
4438
+ const importGraph = await addImports(graph, services);
4086
4439
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
4087
4440
  const phase3 = await addConfigNodes(graph, services, scanPath);
4088
4441
  const phase4 = await addCallEdges(graph, services);
@@ -4106,7 +4459,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4106
4459
  }
4107
4460
  const droppedEntries = drainDroppedExtracted();
4108
4461
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4109
- const rejectedPath = path30.join(path30.dirname(opts.errorsPath), "rejected.ndjson");
4462
+ const rejectedPath = path32.join(path32.dirname(opts.errorsPath), "rejected.ndjson");
4110
4463
  try {
4111
4464
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4112
4465
  } catch (err) {
@@ -4116,8 +4469,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4116
4469
  }
4117
4470
  }
4118
4471
  const result = {
4119
- nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
4120
- edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
4472
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
4473
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
4121
4474
  frontiersPromoted,
4122
4475
  extractionErrors: errorEntries.length,
4123
4476
  errorEntries,
@@ -4141,10 +4494,10 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4141
4494
  // src/divergences.ts
4142
4495
  import {
4143
4496
  DivergenceResultSchema,
4144
- EdgeType as EdgeType11,
4497
+ EdgeType as EdgeType12,
4145
4498
  NodeType as NodeType12,
4146
4499
  parseEdgeId,
4147
- Provenance as Provenance10
4500
+ Provenance as Provenance11
4148
4501
  } from "@neat.is/types";
4149
4502
  function bucketKey(source, target, type) {
4150
4503
  return `${type}|${source}|${target}`;
@@ -4158,17 +4511,17 @@ function bucketEdges(graph) {
4158
4511
  const key = bucketKey(e.source, e.target, e.type);
4159
4512
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4160
4513
  switch (provenance) {
4161
- case Provenance10.EXTRACTED:
4514
+ case Provenance11.EXTRACTED:
4162
4515
  cur.extracted = e;
4163
4516
  break;
4164
- case Provenance10.OBSERVED:
4517
+ case Provenance11.OBSERVED:
4165
4518
  cur.observed = e;
4166
4519
  break;
4167
- case Provenance10.INFERRED:
4520
+ case Provenance11.INFERRED:
4168
4521
  cur.inferred = e;
4169
4522
  break;
4170
4523
  default:
4171
- if (e.provenance === Provenance10.STALE) cur.stale = e;
4524
+ if (e.provenance === Provenance11.STALE) cur.stale = e;
4172
4525
  }
4173
4526
  buckets.set(key, cur);
4174
4527
  });
@@ -4198,7 +4551,7 @@ function gradedConfidence(edge) {
4198
4551
  }
4199
4552
  function detectMissingDivergences(graph, bucket) {
4200
4553
  const out = [];
4201
- if (bucket.type === EdgeType11.CONTAINS) return out;
4554
+ if (bucket.type === EdgeType12.CONTAINS) return out;
4202
4555
  if (bucket.extracted && !bucket.observed) {
4203
4556
  if (!nodeIsFrontier(graph, bucket.target)) {
4204
4557
  out.push({
@@ -4239,7 +4592,7 @@ function declaredHostFor(svc) {
4239
4592
  function hasExtractedConfiguredBy(graph, svcId) {
4240
4593
  for (const edgeId of graph.outboundEdges(svcId)) {
4241
4594
  const e = graph.getEdgeAttributes(edgeId);
4242
- if (e.type === EdgeType11.CONFIGURED_BY && e.provenance === Provenance10.EXTRACTED) {
4595
+ if (e.type === EdgeType12.CONFIGURED_BY && e.provenance === Provenance11.EXTRACTED) {
4243
4596
  return true;
4244
4597
  }
4245
4598
  }
@@ -4252,8 +4605,8 @@ function detectHostMismatch(graph, svcId, svc) {
4252
4605
  const out = [];
4253
4606
  for (const edgeId of graph.outboundEdges(svcId)) {
4254
4607
  const edge = graph.getEdgeAttributes(edgeId);
4255
- if (edge.type !== EdgeType11.CONNECTS_TO) continue;
4256
- if (edge.provenance !== Provenance10.OBSERVED) continue;
4608
+ if (edge.type !== EdgeType12.CONNECTS_TO) continue;
4609
+ if (edge.provenance !== Provenance11.OBSERVED) continue;
4257
4610
  const target = graph.getNodeAttributes(edge.target);
4258
4611
  if (target.type !== NodeType12.DatabaseNode) continue;
4259
4612
  const observedHost = target.host?.trim();
@@ -4277,8 +4630,8 @@ function detectCompatDivergences(graph, svcId, svc) {
4277
4630
  const deps = svc.dependencies ?? {};
4278
4631
  for (const edgeId of graph.outboundEdges(svcId)) {
4279
4632
  const edge = graph.getEdgeAttributes(edgeId);
4280
- if (edge.type !== EdgeType11.CONNECTS_TO) continue;
4281
- if (edge.provenance !== Provenance10.OBSERVED) continue;
4633
+ if (edge.type !== EdgeType12.CONNECTS_TO) continue;
4634
+ if (edge.provenance !== Provenance11.OBSERVED) continue;
4282
4635
  const target = graph.getNodeAttributes(edge.target);
4283
4636
  if (target.type !== NodeType12.DatabaseNode) continue;
4284
4637
  for (const pair of compatPairs()) {
@@ -4382,9 +4735,9 @@ function computeDivergences(graph, opts = {}) {
4382
4735
  }
4383
4736
 
4384
4737
  // src/persist.ts
4385
- import { promises as fs17 } from "fs";
4386
- import path31 from "path";
4387
- import { Provenance as Provenance11, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4738
+ import { promises as fs18 } from "fs";
4739
+ import path33 from "path";
4740
+ import { Provenance as Provenance12, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4388
4741
  var SCHEMA_VERSION = 4;
4389
4742
  function migrateV1ToV2(payload) {
4390
4743
  const nodes = payload.graph.nodes;
@@ -4406,7 +4759,7 @@ function migrateV2ToV3(payload) {
4406
4759
  for (const edge of edges) {
4407
4760
  const attrs = edge.attributes;
4408
4761
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4409
- attrs.provenance = Provenance11.OBSERVED;
4762
+ attrs.provenance = Provenance12.OBSERVED;
4410
4763
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4411
4764
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4412
4765
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -4420,7 +4773,7 @@ function migrateV2ToV3(payload) {
4420
4773
  return { ...payload, schemaVersion: 3 };
4421
4774
  }
4422
4775
  async function ensureDir(filePath) {
4423
- await fs17.mkdir(path31.dirname(filePath), { recursive: true });
4776
+ await fs18.mkdir(path33.dirname(filePath), { recursive: true });
4424
4777
  }
4425
4778
  async function saveGraphToDisk(graph, outPath) {
4426
4779
  await ensureDir(outPath);
@@ -4430,13 +4783,13 @@ async function saveGraphToDisk(graph, outPath) {
4430
4783
  graph: graph.export()
4431
4784
  };
4432
4785
  const tmp = `${outPath}.tmp`;
4433
- await fs17.writeFile(tmp, JSON.stringify(payload), "utf8");
4434
- await fs17.rename(tmp, outPath);
4786
+ await fs18.writeFile(tmp, JSON.stringify(payload), "utf8");
4787
+ await fs18.rename(tmp, outPath);
4435
4788
  }
4436
4789
  async function loadGraphFromDisk(graph, outPath) {
4437
4790
  let raw;
4438
4791
  try {
4439
- raw = await fs17.readFile(outPath, "utf8");
4792
+ raw = await fs18.readFile(outPath, "utf8");
4440
4793
  } catch (err) {
4441
4794
  if (err.code === "ENOENT") return;
4442
4795
  throw err;
@@ -4494,7 +4847,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4494
4847
  }
4495
4848
 
4496
4849
  // src/diff.ts
4497
- import { promises as fs18 } from "fs";
4850
+ import { promises as fs19 } from "fs";
4498
4851
  async function loadSnapshotForDiff(target) {
4499
4852
  if (/^https?:\/\//i.test(target)) {
4500
4853
  const res = await fetch(target);
@@ -4503,7 +4856,7 @@ async function loadSnapshotForDiff(target) {
4503
4856
  }
4504
4857
  return await res.json();
4505
4858
  }
4506
- const raw = await fs18.readFile(target, "utf8");
4859
+ const raw = await fs19.readFile(target, "utf8");
4507
4860
  return JSON.parse(raw);
4508
4861
  }
4509
4862
  function indexEntries(entries) {
@@ -4570,23 +4923,23 @@ function canonicalJson(value) {
4570
4923
  }
4571
4924
 
4572
4925
  // src/projects.ts
4573
- import path32 from "path";
4926
+ import path34 from "path";
4574
4927
  function pathsForProject(project, baseDir) {
4575
4928
  if (project === DEFAULT_PROJECT) {
4576
4929
  return {
4577
- snapshotPath: path32.join(baseDir, "graph.json"),
4578
- errorsPath: path32.join(baseDir, "errors.ndjson"),
4579
- staleEventsPath: path32.join(baseDir, "stale-events.ndjson"),
4580
- embeddingsCachePath: path32.join(baseDir, "embeddings.json"),
4581
- policyViolationsPath: path32.join(baseDir, "policy-violations.ndjson")
4930
+ snapshotPath: path34.join(baseDir, "graph.json"),
4931
+ errorsPath: path34.join(baseDir, "errors.ndjson"),
4932
+ staleEventsPath: path34.join(baseDir, "stale-events.ndjson"),
4933
+ embeddingsCachePath: path34.join(baseDir, "embeddings.json"),
4934
+ policyViolationsPath: path34.join(baseDir, "policy-violations.ndjson")
4582
4935
  };
4583
4936
  }
4584
4937
  return {
4585
- snapshotPath: path32.join(baseDir, `${project}.json`),
4586
- errorsPath: path32.join(baseDir, `errors.${project}.ndjson`),
4587
- staleEventsPath: path32.join(baseDir, `stale-events.${project}.ndjson`),
4588
- embeddingsCachePath: path32.join(baseDir, `embeddings.${project}.json`),
4589
- policyViolationsPath: path32.join(baseDir, `policy-violations.${project}.ndjson`)
4938
+ snapshotPath: path34.join(baseDir, `${project}.json`),
4939
+ errorsPath: path34.join(baseDir, `errors.${project}.ndjson`),
4940
+ staleEventsPath: path34.join(baseDir, `stale-events.${project}.ndjson`),
4941
+ embeddingsCachePath: path34.join(baseDir, `embeddings.${project}.json`),
4942
+ policyViolationsPath: path34.join(baseDir, `policy-violations.${project}.ndjson`)
4590
4943
  };
4591
4944
  }
4592
4945
  var Projects = class {
@@ -4625,9 +4978,9 @@ function parseExtraProjects(raw) {
4625
4978
  }
4626
4979
 
4627
4980
  // src/registry.ts
4628
- import { promises as fs19 } from "fs";
4981
+ import { promises as fs20 } from "fs";
4629
4982
  import os2 from "os";
4630
- import path33 from "path";
4983
+ import path35 from "path";
4631
4984
  import {
4632
4985
  RegistryFileSchema
4633
4986
  } from "@neat.is/types";
@@ -4635,17 +4988,17 @@ var LOCK_TIMEOUT_MS = 5e3;
4635
4988
  var LOCK_RETRY_MS = 50;
4636
4989
  function neatHome() {
4637
4990
  const override = process.env.NEAT_HOME;
4638
- if (override && override.length > 0) return path33.resolve(override);
4639
- return path33.join(os2.homedir(), ".neat");
4991
+ if (override && override.length > 0) return path35.resolve(override);
4992
+ return path35.join(os2.homedir(), ".neat");
4640
4993
  }
4641
4994
  function registryPath() {
4642
- return path33.join(neatHome(), "projects.json");
4995
+ return path35.join(neatHome(), "projects.json");
4643
4996
  }
4644
4997
  function registryLockPath() {
4645
- return path33.join(neatHome(), "projects.json.lock");
4998
+ return path35.join(neatHome(), "projects.json.lock");
4646
4999
  }
4647
5000
  function daemonPidPath() {
4648
- return path33.join(neatHome(), "neatd.pid");
5001
+ return path35.join(neatHome(), "neatd.pid");
4649
5002
  }
4650
5003
  function isPidAliveDefault(pid) {
4651
5004
  try {
@@ -4657,7 +5010,7 @@ function isPidAliveDefault(pid) {
4657
5010
  }
4658
5011
  async function readPidFile(file) {
4659
5012
  try {
4660
- const raw = await fs19.readFile(file, "utf8");
5013
+ const raw = await fs20.readFile(file, "utf8");
4661
5014
  const pid = Number.parseInt(raw.trim(), 10);
4662
5015
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
4663
5016
  } catch {
@@ -4705,32 +5058,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
4705
5058
  }
4706
5059
  }
4707
5060
  async function normalizeProjectPath(input) {
4708
- const resolved = path33.resolve(input);
5061
+ const resolved = path35.resolve(input);
4709
5062
  try {
4710
- return await fs19.realpath(resolved);
5063
+ return await fs20.realpath(resolved);
4711
5064
  } catch {
4712
5065
  return resolved;
4713
5066
  }
4714
5067
  }
4715
5068
  async function writeAtomically(target, contents) {
4716
- await fs19.mkdir(path33.dirname(target), { recursive: true });
5069
+ await fs20.mkdir(path35.dirname(target), { recursive: true });
4717
5070
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4718
- const fd = await fs19.open(tmp, "w");
5071
+ const fd = await fs20.open(tmp, "w");
4719
5072
  try {
4720
5073
  await fd.writeFile(contents, "utf8");
4721
5074
  await fd.sync();
4722
5075
  } finally {
4723
5076
  await fd.close();
4724
5077
  }
4725
- await fs19.rename(tmp, target);
5078
+ await fs20.rename(tmp, target);
4726
5079
  }
4727
5080
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
4728
5081
  const deadline = Date.now() + timeoutMs;
4729
- await fs19.mkdir(path33.dirname(lockPath), { recursive: true });
5082
+ await fs20.mkdir(path35.dirname(lockPath), { recursive: true });
4730
5083
  let probedHolder = false;
4731
5084
  while (true) {
4732
5085
  try {
4733
- const fd = await fs19.open(lockPath, "wx");
5086
+ const fd = await fs20.open(lockPath, "wx");
4734
5087
  try {
4735
5088
  await fd.writeFile(`${process.pid}
4736
5089
  `, "utf8");
@@ -4755,7 +5108,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
4755
5108
  }
4756
5109
  }
4757
5110
  async function releaseLock(lockPath) {
4758
- await fs19.unlink(lockPath).catch(() => {
5111
+ await fs20.unlink(lockPath).catch(() => {
4759
5112
  });
4760
5113
  }
4761
5114
  async function withLock(fn) {
@@ -4771,7 +5124,7 @@ async function readRegistry() {
4771
5124
  const file = registryPath();
4772
5125
  let raw;
4773
5126
  try {
4774
- raw = await fs19.readFile(file, "utf8");
5127
+ raw = await fs20.readFile(file, "utf8");
4775
5128
  } catch (err) {
4776
5129
  if (err.code === "ENOENT") {
4777
5130
  return { version: 1, projects: [] };
@@ -4868,6 +5221,319 @@ import Fastify from "fastify";
4868
5221
  import cors from "@fastify/cors";
4869
5222
  import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
4870
5223
 
5224
+ // src/extend/index.ts
5225
+ import { promises as fs22 } from "fs";
5226
+ import path37 from "path";
5227
+ import os3 from "os";
5228
+ import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
5229
+
5230
+ // src/installers/package-manager.ts
5231
+ import { promises as fs21 } from "fs";
5232
+ import path36 from "path";
5233
+ import { spawn } from "child_process";
5234
+ var LOCKFILE_PRIORITY = [
5235
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
5236
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
5237
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
5238
+ {
5239
+ lockfile: "package-lock.json",
5240
+ pm: "npm",
5241
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
5242
+ }
5243
+ ];
5244
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
5245
+ async function exists2(p) {
5246
+ try {
5247
+ await fs21.access(p);
5248
+ return true;
5249
+ } catch {
5250
+ return false;
5251
+ }
5252
+ }
5253
+ async function detectPackageManager(serviceDir) {
5254
+ let dir = path36.resolve(serviceDir);
5255
+ const stops = /* @__PURE__ */ new Set();
5256
+ for (let i = 0; i < 64; i++) {
5257
+ if (stops.has(dir)) break;
5258
+ stops.add(dir);
5259
+ for (const candidate of LOCKFILE_PRIORITY) {
5260
+ const lockPath = path36.join(dir, candidate.lockfile);
5261
+ if (await exists2(lockPath)) {
5262
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5263
+ }
5264
+ }
5265
+ const parent = path36.dirname(dir);
5266
+ if (parent === dir) break;
5267
+ dir = parent;
5268
+ }
5269
+ return { pm: "npm", cwd: path36.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5270
+ }
5271
+ async function runPackageManagerInstall(cmd) {
5272
+ return new Promise((resolve) => {
5273
+ const child = spawn(cmd.pm, cmd.args, {
5274
+ cwd: cmd.cwd,
5275
+ // Inherit PATH + HOME so the user's installed managers resolve.
5276
+ env: process.env,
5277
+ // `false` keeps the parent in control of cleanup if the orchestrator
5278
+ // exits before install finishes. Cross-platform-safe.
5279
+ shell: false,
5280
+ stdio: ["ignore", "ignore", "pipe"]
5281
+ });
5282
+ let stderr = "";
5283
+ child.stderr?.on("data", (chunk) => {
5284
+ stderr += chunk.toString("utf8");
5285
+ });
5286
+ child.on("error", (err) => {
5287
+ resolve({
5288
+ pm: cmd.pm,
5289
+ cwd: cmd.cwd,
5290
+ args: cmd.args,
5291
+ exitCode: 127,
5292
+ stderr: stderr + `
5293
+ ${err.message}`
5294
+ });
5295
+ });
5296
+ child.on("close", (code) => {
5297
+ resolve({
5298
+ pm: cmd.pm,
5299
+ cwd: cmd.cwd,
5300
+ args: cmd.args,
5301
+ exitCode: code ?? 1,
5302
+ stderr: stderr.trim()
5303
+ });
5304
+ });
5305
+ });
5306
+ }
5307
+
5308
+ // src/extend/index.ts
5309
+ async function fileExists2(p) {
5310
+ try {
5311
+ await fs22.access(p);
5312
+ return true;
5313
+ } catch {
5314
+ return false;
5315
+ }
5316
+ }
5317
+ async function readPackageJson(scanPath) {
5318
+ const pkgPath = path37.join(scanPath, "package.json");
5319
+ const raw = await fs22.readFile(pkgPath, "utf8");
5320
+ return JSON.parse(raw);
5321
+ }
5322
+ async function findHookFiles(scanPath) {
5323
+ const entries = await fs22.readdir(scanPath);
5324
+ return entries.filter(
5325
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
5326
+ ).sort();
5327
+ }
5328
+ function extendLogPath() {
5329
+ return process.env.NEAT_EXTEND_LOG ?? path37.join(os3.homedir(), ".neat", "extend-log.ndjson");
5330
+ }
5331
+ async function appendExtendLog(entry) {
5332
+ const logPath = extendLogPath();
5333
+ await fs22.mkdir(path37.dirname(logPath), { recursive: true });
5334
+ await fs22.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
5335
+ }
5336
+ function splicedContent(fileContent, snippet2) {
5337
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
5338
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
5339
+ __INSTRUMENTATION_BLOCK__`);
5340
+ }
5341
+ const lines = fileContent.split("\n");
5342
+ let lastPushIdx = -1;
5343
+ for (let i = 0; i < lines.length; i++) {
5344
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
5345
+ }
5346
+ if (lastPushIdx >= 0) {
5347
+ lines.splice(lastPushIdx + 1, 0, snippet2);
5348
+ return lines.join("\n");
5349
+ }
5350
+ for (let i = 0; i < lines.length; i++) {
5351
+ if (lines[i].includes("new NodeSDK(")) {
5352
+ lines.splice(i, 0, snippet2);
5353
+ return lines.join("\n");
5354
+ }
5355
+ }
5356
+ return null;
5357
+ }
5358
+ async function listUninstrumented(ctx) {
5359
+ const pkg = await readPackageJson(ctx.scanPath);
5360
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5361
+ const results = [];
5362
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
5363
+ const entry = registryResolve(library, installedVersion);
5364
+ if (!entry) continue;
5365
+ if (entry.coverage === "bundled" || entry.coverage === "http-only") continue;
5366
+ results.push({
5367
+ library,
5368
+ coverage: entry.coverage,
5369
+ installedVersion,
5370
+ instrumentation_package: entry.instrumentation_package,
5371
+ package_version: entry.package_version,
5372
+ registration: entry.registration,
5373
+ notes: entry.notes
5374
+ });
5375
+ }
5376
+ return results;
5377
+ }
5378
+ function lookupInstrumentation(library, installedVersion) {
5379
+ const entry = registryResolve(library, installedVersion);
5380
+ if (!entry) return null;
5381
+ return {
5382
+ library: entry.library,
5383
+ coverage: entry.coverage,
5384
+ instrumentation_package: entry.instrumentation_package,
5385
+ package_version: entry.package_version,
5386
+ registration: entry.registration,
5387
+ notes: entry.notes
5388
+ };
5389
+ }
5390
+ async function describeProjectInstrumentation(ctx) {
5391
+ const hookFiles = await findHookFiles(ctx.scanPath);
5392
+ const envNeat = await fileExists2(path37.join(ctx.scanPath, ".env.neat"));
5393
+ const registryInstrPackages = new Set(
5394
+ registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
5395
+ );
5396
+ const pkg = await readPackageJson(ctx.scanPath);
5397
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5398
+ const installedDeps = {};
5399
+ for (const [key, version] of Object.entries(allDeps)) {
5400
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
5401
+ installedDeps[key] = version;
5402
+ }
5403
+ }
5404
+ return { hookFiles, envNeat, installedDeps };
5405
+ }
5406
+ async function applyExtension(ctx, args, options) {
5407
+ const hookFiles = await findHookFiles(ctx.scanPath);
5408
+ if (hookFiles.length === 0) {
5409
+ throw new Error(
5410
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
5411
+ );
5412
+ }
5413
+ for (const file of hookFiles) {
5414
+ const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
5415
+ if (content.includes(args.registration_snippet)) {
5416
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5417
+ }
5418
+ }
5419
+ const primaryFile = hookFiles[0];
5420
+ const primaryPath = path37.join(ctx.scanPath, primaryFile);
5421
+ const filesTouched = [];
5422
+ const depsAdded = [];
5423
+ const pkgPath = path37.join(ctx.scanPath, "package.json");
5424
+ const pkg = await readPackageJson(ctx.scanPath);
5425
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5426
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
5427
+ await fs22.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5428
+ filesTouched.push("package.json");
5429
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
5430
+ }
5431
+ const hookContent = await fs22.readFile(primaryPath, "utf8");
5432
+ const patched = splicedContent(hookContent, args.registration_snippet);
5433
+ if (!patched) {
5434
+ throw new Error(
5435
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
5436
+ );
5437
+ }
5438
+ await fs22.writeFile(primaryPath, patched, "utf8");
5439
+ filesTouched.push(primaryFile);
5440
+ const cmd = await detectPackageManager(ctx.scanPath);
5441
+ const installer = options?.runInstall ?? runPackageManagerInstall;
5442
+ const install = await installer(cmd);
5443
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
5444
+ await appendExtendLog({
5445
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5446
+ project: ctx.project,
5447
+ library: args.library,
5448
+ instrumentation_package: args.instrumentation_package,
5449
+ version: args.version,
5450
+ registration_snippet: args.registration_snippet,
5451
+ filesTouched,
5452
+ depsAdded,
5453
+ installOutput
5454
+ });
5455
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
5456
+ }
5457
+ async function dryRunExtension(ctx, args) {
5458
+ const hookFiles = await findHookFiles(ctx.scanPath);
5459
+ if (hookFiles.length === 0) {
5460
+ return {
5461
+ library: args.library,
5462
+ filesTouched: [],
5463
+ depsToAdd: [],
5464
+ packageJsonPatch: {},
5465
+ templatePatch: "No hook files found. Run 'neat init' first."
5466
+ };
5467
+ }
5468
+ for (const file of hookFiles) {
5469
+ const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
5470
+ if (content.includes(args.registration_snippet)) {
5471
+ return {
5472
+ library: args.library,
5473
+ filesTouched: [],
5474
+ depsToAdd: [],
5475
+ packageJsonPatch: {},
5476
+ templatePatch: "Already applied \u2014 no changes would be made."
5477
+ };
5478
+ }
5479
+ }
5480
+ const primaryFile = hookFiles[0];
5481
+ const filesTouched = [];
5482
+ const depsToAdd = [];
5483
+ let packageJsonPatch = {};
5484
+ let templatePatch = "";
5485
+ const pkg = await readPackageJson(ctx.scanPath);
5486
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5487
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
5488
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5489
+ filesTouched.push("package.json");
5490
+ }
5491
+ const hookContent = await fs22.readFile(path37.join(ctx.scanPath, primaryFile), "utf8");
5492
+ const patched = splicedContent(hookContent, args.registration_snippet);
5493
+ if (patched) {
5494
+ filesTouched.push(primaryFile);
5495
+ templatePatch = `+ ${args.registration_snippet}`;
5496
+ } else {
5497
+ templatePatch = "Could not find insertion point in hook file.";
5498
+ }
5499
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
5500
+ }
5501
+ async function rollbackExtension(ctx, args) {
5502
+ const logPath = extendLogPath();
5503
+ if (!await fileExists2(logPath)) {
5504
+ return { undone: false, message: "no apply found for library" };
5505
+ }
5506
+ const raw = await fs22.readFile(logPath, "utf8");
5507
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5508
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
5509
+ if (!match) {
5510
+ return { undone: false, message: "no apply found for library" };
5511
+ }
5512
+ const pkgPath = path37.join(ctx.scanPath, "package.json");
5513
+ if (await fileExists2(pkgPath)) {
5514
+ const pkg = await readPackageJson(ctx.scanPath);
5515
+ if (pkg.dependencies?.[match.instrumentation_package]) {
5516
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
5517
+ pkg.dependencies = rest;
5518
+ await fs22.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5519
+ }
5520
+ }
5521
+ const hookFiles = await findHookFiles(ctx.scanPath);
5522
+ for (const file of hookFiles) {
5523
+ const filePath = path37.join(ctx.scanPath, file);
5524
+ const content = await fs22.readFile(filePath, "utf8");
5525
+ if (content.includes(match.registration_snippet)) {
5526
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
5527
+ await fs22.writeFile(filePath, filtered, "utf8");
5528
+ break;
5529
+ }
5530
+ }
5531
+ return {
5532
+ undone: true,
5533
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
5534
+ };
5535
+ }
5536
+
4871
5537
  // src/streaming.ts
4872
5538
  var SSE_HEARTBEAT_MS = 3e4;
4873
5539
  var SSE_BACKPRESSURE_CAP = 1e3;
@@ -5324,6 +5990,105 @@ function registerRoutes(scope, ctx) {
5324
5990
  violations
5325
5991
  };
5326
5992
  });
5993
+ scope.get("/extend/list-uninstrumented", async (req, reply) => {
5994
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5995
+ if (!proj) return;
5996
+ if (!proj.scanPath) {
5997
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5998
+ }
5999
+ try {
6000
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
6001
+ return { libraries: results };
6002
+ } catch (err) {
6003
+ return reply.code(500).send({ error: err.message });
6004
+ }
6005
+ });
6006
+ scope.get("/extend/lookup", async (req, reply) => {
6007
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6008
+ if (!proj) return;
6009
+ const { library, version } = req.query;
6010
+ if (!library) {
6011
+ return reply.code(400).send({ error: "query parameter `library` is required" });
6012
+ }
6013
+ const result = lookupInstrumentation(library, version);
6014
+ if (!result) {
6015
+ return reply.code(404).send({ error: "library not found in registry", library });
6016
+ }
6017
+ return result;
6018
+ });
6019
+ scope.get("/extend/describe", async (req, reply) => {
6020
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6021
+ if (!proj) return;
6022
+ if (!proj.scanPath) {
6023
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6024
+ }
6025
+ try {
6026
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
6027
+ return state;
6028
+ } catch (err) {
6029
+ return reply.code(500).send({ error: err.message });
6030
+ }
6031
+ });
6032
+ scope.post("/extend/apply", async (req, reply) => {
6033
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6034
+ if (!proj) return;
6035
+ if (!proj.scanPath) {
6036
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6037
+ }
6038
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6039
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6040
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6041
+ }
6042
+ try {
6043
+ const result = await applyExtension(
6044
+ { project: proj.name, scanPath: proj.scanPath },
6045
+ { library, instrumentation_package, version, registration_snippet }
6046
+ );
6047
+ return result;
6048
+ } catch (err) {
6049
+ return reply.code(500).send({ error: err.message });
6050
+ }
6051
+ });
6052
+ scope.post("/extend/dry-run", async (req, reply) => {
6053
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6054
+ if (!proj) return;
6055
+ if (!proj.scanPath) {
6056
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6057
+ }
6058
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6059
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6060
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6061
+ }
6062
+ try {
6063
+ const result = await dryRunExtension(
6064
+ { project: proj.name, scanPath: proj.scanPath },
6065
+ { library, instrumentation_package, version, registration_snippet }
6066
+ );
6067
+ return result;
6068
+ } catch (err) {
6069
+ return reply.code(500).send({ error: err.message });
6070
+ }
6071
+ });
6072
+ scope.post("/extend/rollback", async (req, reply) => {
6073
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6074
+ if (!proj) return;
6075
+ if (!proj.scanPath) {
6076
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6077
+ }
6078
+ const { library } = req.body ?? {};
6079
+ if (!library) {
6080
+ return reply.code(400).send({ error: "body must include library" });
6081
+ }
6082
+ try {
6083
+ const result = await rollbackExtension(
6084
+ { project: proj.name, scanPath: proj.scanPath },
6085
+ { library }
6086
+ );
6087
+ return result;
6088
+ } catch (err) {
6089
+ return reply.code(500).send({ error: err.message });
6090
+ }
6091
+ });
5327
6092
  }
5328
6093
  async function buildApi(opts) {
5329
6094
  const app = Fastify({ logger: false });
@@ -5459,6 +6224,7 @@ export {
5459
6224
  discoverServices,
5460
6225
  addServiceNodes,
5461
6226
  addServiceAliases,
6227
+ addImports,
5462
6228
  addDatabasesAndCompat,
5463
6229
  addConfigNodes,
5464
6230
  addCallEdges,
@@ -5469,6 +6235,8 @@ export {
5469
6235
  saveGraphToDisk,
5470
6236
  loadGraphFromDisk,
5471
6237
  startPersistLoop,
6238
+ detectPackageManager,
6239
+ runPackageManagerInstall,
5472
6240
  loadSnapshotForDiff,
5473
6241
  computeGraphDiff,
5474
6242
  pathsForProject,
@@ -5488,4 +6256,4 @@ export {
5488
6256
  removeProject,
5489
6257
  buildApi
5490
6258
  };
5491
- //# sourceMappingURL=chunk-OJHI33LD.js.map
6259
+ //# sourceMappingURL=chunk-5W7H35JJ.js.map