@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.
package/dist/neatd.cjs CHANGED
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
57
57
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
58
58
  const publicRead = opts.publicRead === true;
59
59
  app.addHook("preHandler", (req2, reply, done) => {
60
- const path40 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path44 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path40 === suffix || path40.endsWith(suffix)) {
62
+ if (path44 === suffix || path44.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -185,8 +185,8 @@ function reshapeGrpcRequest(req2) {
185
185
  };
186
186
  }
187
187
  function resolveProtoRoot() {
188
- const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
189
- return import_node_path34.default.resolve(here, "..", "proto");
188
+ const here = import_node_path38.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
189
+ return import_node_path38.default.resolve(here, "..", "proto");
190
190
  }
191
191
  function loadTraceService() {
192
192
  const protoRoot = resolveProtoRoot();
@@ -254,13 +254,13 @@ async function startOtelGrpcReceiver(opts) {
254
254
  })
255
255
  };
256
256
  }
257
- var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
257
+ var import_node_url, import_node_path38, import_node_crypto2, grpc, protoLoader;
258
258
  var init_otel_grpc = __esm({
259
259
  "src/otel-grpc.ts"() {
260
260
  "use strict";
261
261
  init_cjs_shims();
262
262
  import_node_url = require("url");
263
- import_node_path34 = __toESM(require("path"), 1);
263
+ import_node_path38 = __toESM(require("path"), 1);
264
264
  import_node_crypto2 = require("crypto");
265
265
  grpc = __toESM(require("@grpc/grpc-js"), 1);
266
266
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -370,10 +370,10 @@ function parseOtlpRequest(body) {
370
370
  return out;
371
371
  }
372
372
  function loadProtoRoot() {
373
- const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
374
- const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
373
+ const here = import_node_path39.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
374
+ const protoRoot = import_node_path39.default.resolve(here, "..", "proto");
375
375
  const root = new import_protobufjs.default.Root();
376
- root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
376
+ root.resolvePath = (_origin, target) => import_node_path39.default.resolve(protoRoot, target);
377
377
  root.loadSync(
378
378
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
379
379
  { keepCase: true }
@@ -566,12 +566,12 @@ async function buildOtelReceiver(opts) {
566
566
  };
567
567
  return decorated;
568
568
  }
569
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
569
+ var import_node_path39, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
570
570
  var init_otel = __esm({
571
571
  "src/otel.ts"() {
572
572
  "use strict";
573
573
  init_cjs_shims();
574
- import_node_path35 = __toESM(require("path"), 1);
574
+ import_node_path39 = __toESM(require("path"), 1);
575
575
  import_node_url2 = require("url");
576
576
  import_fastify2 = __toESM(require("fastify"), 1);
577
577
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -587,14 +587,14 @@ var init_otel = __esm({
587
587
 
588
588
  // src/neatd.ts
589
589
  init_cjs_shims();
590
- var import_node_fs23 = require("fs");
591
- var import_node_path39 = __toESM(require("path"), 1);
590
+ var import_node_fs26 = require("fs");
591
+ var import_node_path43 = __toESM(require("path"), 1);
592
592
  var import_node_module = require("module");
593
593
 
594
594
  // src/daemon.ts
595
595
  init_cjs_shims();
596
- var import_node_fs22 = require("fs");
597
- var import_node_path37 = __toESM(require("path"), 1);
596
+ var import_node_fs25 = require("fs");
597
+ var import_node_path41 = __toESM(require("path"), 1);
598
598
 
599
599
  // src/graph.ts
600
600
  init_cjs_shims();
@@ -1073,19 +1073,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1073
1073
  function longestIncomingWalk(graph, start, maxDepth) {
1074
1074
  let best = { path: [start], edges: [] };
1075
1075
  const visited = /* @__PURE__ */ new Set([start]);
1076
- function step(node, path40, edges) {
1077
- if (path40.length > best.path.length) {
1078
- best = { path: [...path40], edges: [...edges] };
1076
+ function step(node, path44, edges) {
1077
+ if (path44.length > best.path.length) {
1078
+ best = { path: [...path44], edges: [...edges] };
1079
1079
  }
1080
- if (path40.length - 1 >= maxDepth) return;
1080
+ if (path44.length - 1 >= maxDepth) return;
1081
1081
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1082
1082
  for (const [srcId, edge] of incoming) {
1083
1083
  if (visited.has(srcId)) continue;
1084
1084
  visited.add(srcId);
1085
- path40.push(srcId);
1085
+ path44.push(srcId);
1086
1086
  edges.push(edge);
1087
- step(srcId, path40, edges);
1088
- path40.pop();
1087
+ step(srcId, path44, edges);
1088
+ path44.pop();
1089
1089
  edges.pop();
1090
1090
  visited.delete(srcId);
1091
1091
  }
@@ -1895,7 +1895,7 @@ function ensureFrontierNode(graph, host, ts) {
1895
1895
  graph.addNode(id, node);
1896
1896
  return id;
1897
1897
  }
1898
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1898
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1899
1899
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1900
1900
  const id = makeObservedEdgeId(type, source, target);
1901
1901
  if (graph.hasEdge(id)) {
@@ -1932,7 +1932,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1932
1932
  confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1933
1933
  lastObserved: ts,
1934
1934
  callCount: 1,
1935
- signal
1935
+ signal,
1936
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
1937
+ // Only set when code.filepath was present on the span — never fabricated.
1938
+ ...evidence ? { evidence } : {}
1936
1939
  };
1937
1940
  graph.addEdgeWithKey(id, source, target, edge);
1938
1941
  return { edge, created: true };
@@ -2026,6 +2029,7 @@ async function handleSpan(ctx, span) {
2026
2029
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2027
2030
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2028
2031
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2032
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2029
2033
  let affectedNode = sourceId;
2030
2034
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2031
2035
  if (span.dbSystem) {
@@ -2039,7 +2043,8 @@ async function handleSpan(ctx, span) {
2039
2043
  observedSource(),
2040
2044
  targetId,
2041
2045
  ts,
2042
- isError
2046
+ isError,
2047
+ callSiteEvidence
2043
2048
  );
2044
2049
  if (result) affectedNode = targetId;
2045
2050
  }
@@ -2055,7 +2060,8 @@ async function handleSpan(ctx, span) {
2055
2060
  observedSource(),
2056
2061
  targetId,
2057
2062
  ts,
2058
- isError
2063
+ isError,
2064
+ callSiteEvidence
2059
2065
  );
2060
2066
  affectedNode = targetId;
2061
2067
  resolvedViaAddress = true;
@@ -2067,7 +2073,8 @@ async function handleSpan(ctx, span) {
2067
2073
  observedSource(),
2068
2074
  frontierNodeId,
2069
2075
  ts,
2070
- isError
2076
+ isError,
2077
+ callSiteEvidence
2071
2078
  );
2072
2079
  affectedNode = frontierNodeId;
2073
2080
  resolvedViaAddress = true;
@@ -2943,16 +2950,433 @@ async function addServiceAliases(graph, scanPath, services) {
2943
2950
  await collectK8sAliases(graph, scanPath, byName);
2944
2951
  }
2945
2952
 
2946
- // src/extract/databases/index.ts
2953
+ // src/extract/files.ts
2947
2954
  init_cjs_shims();
2948
- var import_node_path17 = __toESM(require("path"), 1);
2955
+
2956
+ // src/extract/calls/shared.ts
2957
+ init_cjs_shims();
2958
+ var import_node_fs10 = require("fs");
2959
+ var import_node_path10 = __toESM(require("path"), 1);
2949
2960
  var import_types7 = require("@neat.is/types");
2961
+ async function walkSourceFiles(dir) {
2962
+ const out = [];
2963
+ async function walk(current) {
2964
+ const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2965
+ for (const entry2 of entries) {
2966
+ const full = import_node_path10.default.join(current, entry2.name);
2967
+ if (entry2.isDirectory()) {
2968
+ if (IGNORED_DIRS.has(entry2.name)) continue;
2969
+ if (await isPythonVenvDir(full)) continue;
2970
+ await walk(full);
2971
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name))) {
2972
+ out.push(full);
2973
+ }
2974
+ }
2975
+ }
2976
+ await walk(dir);
2977
+ return out;
2978
+ }
2979
+ async function loadSourceFiles(dir) {
2980
+ const paths = await walkSourceFiles(dir);
2981
+ const out = [];
2982
+ for (const p of paths) {
2983
+ try {
2984
+ const content = await import_node_fs10.promises.readFile(p, "utf8");
2985
+ out.push({ path: p, content });
2986
+ } catch {
2987
+ }
2988
+ }
2989
+ return out;
2990
+ }
2991
+ function lineOf(text, needle) {
2992
+ const idx = text.indexOf(needle);
2993
+ if (idx < 0) return 1;
2994
+ return text.slice(0, idx).split("\n").length;
2995
+ }
2996
+ function snippet(text, line) {
2997
+ const lines = text.split("\n");
2998
+ return (lines[line - 1] ?? "").trim();
2999
+ }
3000
+ function toPosix2(p) {
3001
+ return p.split("\\").join("/");
3002
+ }
3003
+ function languageForPath(relPath) {
3004
+ switch (import_node_path10.default.extname(relPath).toLowerCase()) {
3005
+ case ".py":
3006
+ return "python";
3007
+ case ".ts":
3008
+ case ".tsx":
3009
+ return "typescript";
3010
+ case ".js":
3011
+ case ".jsx":
3012
+ case ".mjs":
3013
+ case ".cjs":
3014
+ return "javascript";
3015
+ default:
3016
+ return void 0;
3017
+ }
3018
+ }
3019
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3020
+ let nodesAdded = 0;
3021
+ let edgesAdded = 0;
3022
+ const fileNodeId = (0, import_types7.fileId)(serviceName, relPath);
3023
+ if (!graph.hasNode(fileNodeId)) {
3024
+ const language = languageForPath(relPath);
3025
+ const node = {
3026
+ id: fileNodeId,
3027
+ type: import_types7.NodeType.FileNode,
3028
+ service: serviceName,
3029
+ path: relPath,
3030
+ ...language ? { language } : {},
3031
+ discoveredVia: "static"
3032
+ };
3033
+ graph.addNode(fileNodeId, node);
3034
+ nodesAdded++;
3035
+ }
3036
+ const containsId = (0, import_types7.extractedEdgeId)(serviceNodeId, fileNodeId, import_types7.EdgeType.CONTAINS);
3037
+ if (!graph.hasEdge(containsId)) {
3038
+ const edge = {
3039
+ id: containsId,
3040
+ source: serviceNodeId,
3041
+ target: fileNodeId,
3042
+ type: import_types7.EdgeType.CONTAINS,
3043
+ provenance: import_types7.Provenance.EXTRACTED,
3044
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3045
+ evidence: { file: relPath }
3046
+ };
3047
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3048
+ edgesAdded++;
3049
+ }
3050
+ return { fileNodeId, nodesAdded, edgesAdded };
3051
+ }
3052
+
3053
+ // src/extract/files.ts
3054
+ var import_node_path11 = __toESM(require("path"), 1);
3055
+ async function addFiles(graph, services) {
3056
+ let nodesAdded = 0;
3057
+ let edgesAdded = 0;
3058
+ for (const service of services) {
3059
+ const filePaths = await walkSourceFiles(service.dir);
3060
+ for (const filePath of filePaths) {
3061
+ const relPath = toPosix2(import_node_path11.default.relative(service.dir, filePath));
3062
+ const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
3063
+ graph,
3064
+ service.pkg.name,
3065
+ service.node.id,
3066
+ relPath
3067
+ );
3068
+ nodesAdded += n;
3069
+ edgesAdded += e;
3070
+ }
3071
+ }
3072
+ return { nodesAdded, edgesAdded };
3073
+ }
3074
+
3075
+ // src/extract/imports.ts
3076
+ init_cjs_shims();
3077
+ var import_node_path12 = __toESM(require("path"), 1);
3078
+ var import_node_fs11 = require("fs");
3079
+ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3080
+ var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3081
+ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3082
+ var import_types8 = require("@neat.is/types");
3083
+ var PARSE_CHUNK = 16384;
3084
+ function parseSource(parser, source) {
3085
+ return parser.parse(
3086
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3087
+ );
3088
+ }
3089
+ function makeJsParser() {
3090
+ const p = new import_tree_sitter.default();
3091
+ p.setLanguage(import_tree_sitter_javascript.default);
3092
+ return p;
3093
+ }
3094
+ function makePyParser() {
3095
+ const p = new import_tree_sitter.default();
3096
+ p.setLanguage(import_tree_sitter_python.default);
3097
+ return p;
3098
+ }
3099
+ function stringLiteralText(node) {
3100
+ for (let i = 0; i < node.childCount; i++) {
3101
+ const child = node.child(i);
3102
+ if (child?.type === "string_fragment") return child.text;
3103
+ }
3104
+ const raw = node.text;
3105
+ if (raw.length >= 2) return raw.slice(1, -1);
3106
+ return raw.length === 0 ? null : "";
3107
+ }
3108
+ function clipSnippet(text) {
3109
+ const oneLine = text.split("\n")[0] ?? text;
3110
+ return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
3111
+ }
3112
+ function collectJsImports(node, out) {
3113
+ if (node.type === "import_statement") {
3114
+ const source = node.childForFieldName("source");
3115
+ if (source) {
3116
+ const specifier = stringLiteralText(source);
3117
+ if (specifier) {
3118
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3119
+ }
3120
+ }
3121
+ return;
3122
+ }
3123
+ if (node.type === "call_expression") {
3124
+ const fn = node.childForFieldName("function");
3125
+ if (fn?.type === "identifier" && fn.text === "require") {
3126
+ const args = node.childForFieldName("arguments");
3127
+ const firstArg = args?.namedChild(0);
3128
+ if (firstArg?.type === "string") {
3129
+ const specifier = stringLiteralText(firstArg);
3130
+ if (specifier) {
3131
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3132
+ }
3133
+ }
3134
+ }
3135
+ }
3136
+ for (let i = 0; i < node.namedChildCount; i++) {
3137
+ const child = node.namedChild(i);
3138
+ if (child) collectJsImports(child, out);
3139
+ }
3140
+ }
3141
+ function collectPyImports(node, out) {
3142
+ if (node.type === "import_from_statement") {
3143
+ let level = 0;
3144
+ let modulePath = "";
3145
+ let pastFrom = false;
3146
+ for (let i = 0; i < node.childCount; i++) {
3147
+ const child = node.child(i);
3148
+ if (!child) continue;
3149
+ if (!pastFrom) {
3150
+ if (child.type === "from") pastFrom = true;
3151
+ continue;
3152
+ }
3153
+ if (child.type === "import") break;
3154
+ if (child.type === "relative_import") {
3155
+ for (let j = 0; j < child.childCount; j++) {
3156
+ const rc = child.child(j);
3157
+ if (!rc) continue;
3158
+ if (rc.type === "import_prefix") {
3159
+ for (let k = 0; k < rc.childCount; k++) {
3160
+ if (rc.child(k)?.type === ".") level++;
3161
+ }
3162
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3163
+ }
3164
+ break;
3165
+ }
3166
+ if (child.type === "dotted_name") {
3167
+ modulePath = child.text;
3168
+ break;
3169
+ }
3170
+ }
3171
+ if (level > 0 || modulePath) {
3172
+ out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3173
+ }
3174
+ }
3175
+ for (let i = 0; i < node.namedChildCount; i++) {
3176
+ const child = node.namedChild(i);
3177
+ if (child) collectPyImports(child, out);
3178
+ }
3179
+ }
3180
+ async function fileExists(p) {
3181
+ try {
3182
+ await import_node_fs11.promises.access(p);
3183
+ return true;
3184
+ } catch {
3185
+ return false;
3186
+ }
3187
+ }
3188
+ function isWithinServiceDir(candidate, serviceDir) {
3189
+ const rel = import_node_path12.default.relative(serviceDir, candidate);
3190
+ return rel !== "" && !rel.startsWith("..") && !import_node_path12.default.isAbsolute(rel);
3191
+ }
3192
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
3193
+ var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
3194
+ async function firstExistingCandidate(base, serviceDir) {
3195
+ for (const ext of JS_EXTENSIONS) {
3196
+ const candidate = base + ext;
3197
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3198
+ return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
3199
+ }
3200
+ }
3201
+ for (const indexFile of JS_INDEX_FILES) {
3202
+ const candidate = import_node_path12.default.join(base, indexFile);
3203
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3204
+ return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
3205
+ }
3206
+ }
3207
+ return null;
3208
+ }
3209
+ async function loadTsPathConfig(serviceDir) {
3210
+ const tsconfigPath = import_node_path12.default.join(serviceDir, "tsconfig.json");
3211
+ let raw;
3212
+ try {
3213
+ raw = await import_node_fs11.promises.readFile(tsconfigPath, "utf8");
3214
+ } catch {
3215
+ return null;
3216
+ }
3217
+ try {
3218
+ const parsed = JSON.parse(raw);
3219
+ const paths = parsed.compilerOptions?.paths;
3220
+ if (!paths || Object.keys(paths).length === 0) return null;
3221
+ const baseUrl = parsed.compilerOptions?.baseUrl;
3222
+ return { paths, baseDir: baseUrl ? import_node_path12.default.resolve(serviceDir, baseUrl) : serviceDir };
3223
+ } catch (err) {
3224
+ recordExtractionError("import alias resolution", tsconfigPath, err);
3225
+ return null;
3226
+ }
3227
+ }
3228
+ async function resolveTsAlias(specifier, config, serviceDir) {
3229
+ for (const [pattern, targets] of Object.entries(config.paths)) {
3230
+ let suffix = null;
3231
+ if (pattern === specifier) {
3232
+ suffix = "";
3233
+ } else if (pattern.endsWith("/*")) {
3234
+ const prefix = pattern.slice(0, -1);
3235
+ if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
3236
+ }
3237
+ if (suffix === null) continue;
3238
+ for (const target of targets) {
3239
+ const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
3240
+ const resolvedBase = import_node_path12.default.resolve(config.baseDir, targetBase, suffix);
3241
+ const hit = await firstExistingCandidate(resolvedBase, serviceDir);
3242
+ if (hit) return hit;
3243
+ if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
3244
+ return toPosix2(import_node_path12.default.relative(serviceDir, resolvedBase));
3245
+ }
3246
+ }
3247
+ }
3248
+ return null;
3249
+ }
3250
+ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3251
+ if (!specifier) return null;
3252
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
3253
+ const base = import_node_path12.default.resolve(importerDir, specifier);
3254
+ const ext = import_node_path12.default.extname(specifier);
3255
+ if (ext) {
3256
+ if (ext === ".js" || ext === ".jsx") {
3257
+ const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
3258
+ const tsSibling = base.slice(0, -ext.length) + tsExt;
3259
+ if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
3260
+ return toPosix2(import_node_path12.default.relative(serviceDir, tsSibling));
3261
+ }
3262
+ }
3263
+ if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
3264
+ return toPosix2(import_node_path12.default.relative(serviceDir, base));
3265
+ }
3266
+ return null;
3267
+ }
3268
+ return firstExistingCandidate(base, serviceDir);
3269
+ }
3270
+ if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
3271
+ return null;
3272
+ }
3273
+ async function resolvePyImport(imp, importerPath, serviceDir) {
3274
+ if (!imp.modulePath) return null;
3275
+ const relPath = imp.modulePath.split(".").join("/");
3276
+ let baseDir;
3277
+ if (imp.level > 0) {
3278
+ baseDir = import_node_path12.default.dirname(importerPath);
3279
+ for (let i = 1; i < imp.level; i++) baseDir = import_node_path12.default.dirname(baseDir);
3280
+ } else {
3281
+ baseDir = serviceDir;
3282
+ }
3283
+ const candidates = [import_node_path12.default.join(baseDir, `${relPath}.py`), import_node_path12.default.join(baseDir, relPath, "__init__.py")];
3284
+ for (const candidate of candidates) {
3285
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3286
+ return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
3287
+ }
3288
+ }
3289
+ return null;
3290
+ }
3291
+ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3292
+ const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
3293
+ if (!graph.hasNode(importeeFileId)) return 0;
3294
+ const edgeId = (0, import_types8.extractedEdgeId)(importerFileId, importeeFileId, import_types8.EdgeType.IMPORTS);
3295
+ if (graph.hasEdge(edgeId)) return 0;
3296
+ const edge = {
3297
+ id: edgeId,
3298
+ source: importerFileId,
3299
+ target: importeeFileId,
3300
+ type: import_types8.EdgeType.IMPORTS,
3301
+ provenance: import_types8.Provenance.EXTRACTED,
3302
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3303
+ evidence: { file: importerRelPath, line, snippet: snippet2 }
3304
+ };
3305
+ graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
3306
+ return 1;
3307
+ }
3308
+ async function addImports(graph, services) {
3309
+ const jsParser = makeJsParser();
3310
+ const pyParser = makePyParser();
3311
+ let edgesAdded = 0;
3312
+ for (const service of services) {
3313
+ const tsPaths = await loadTsPathConfig(service.dir);
3314
+ const files = await loadSourceFiles(service.dir);
3315
+ for (const file of files) {
3316
+ if (isTestPath(file.path)) continue;
3317
+ const relFile = toPosix2(import_node_path12.default.relative(service.dir, file.path));
3318
+ const importerFileId = (0, import_types8.fileId)(service.pkg.name, relFile);
3319
+ const isPython = import_node_path12.default.extname(file.path) === ".py";
3320
+ if (isPython) {
3321
+ let pyImports = [];
3322
+ try {
3323
+ const tree = parseSource(pyParser, file.content);
3324
+ collectPyImports(tree.rootNode, pyImports);
3325
+ } catch (err) {
3326
+ recordExtractionError("import extraction", file.path, err);
3327
+ continue;
3328
+ }
3329
+ for (const imp of pyImports) {
3330
+ const resolved = await resolvePyImport(imp, file.path, service.dir);
3331
+ if (!resolved) continue;
3332
+ edgesAdded += emitImportEdge(
3333
+ graph,
3334
+ service.pkg.name,
3335
+ importerFileId,
3336
+ relFile,
3337
+ resolved,
3338
+ imp.line,
3339
+ imp.snippet
3340
+ );
3341
+ }
3342
+ continue;
3343
+ }
3344
+ let jsImports = [];
3345
+ try {
3346
+ const tree = parseSource(jsParser, file.content);
3347
+ collectJsImports(tree.rootNode, jsImports);
3348
+ } catch (err) {
3349
+ recordExtractionError("import extraction", file.path, err);
3350
+ continue;
3351
+ }
3352
+ for (const imp of jsImports) {
3353
+ const resolved = await resolveJsImport(imp.specifier, import_node_path12.default.dirname(file.path), service.dir, tsPaths);
3354
+ if (!resolved) continue;
3355
+ edgesAdded += emitImportEdge(
3356
+ graph,
3357
+ service.pkg.name,
3358
+ importerFileId,
3359
+ relFile,
3360
+ resolved,
3361
+ imp.line,
3362
+ imp.snippet
3363
+ );
3364
+ }
3365
+ }
3366
+ }
3367
+ return { nodesAdded: 0, edgesAdded };
3368
+ }
3369
+
3370
+ // src/extract/databases/index.ts
3371
+ init_cjs_shims();
3372
+ var import_node_path20 = __toESM(require("path"), 1);
3373
+ var import_types9 = require("@neat.is/types");
2950
3374
 
2951
3375
  // src/extract/databases/db-config-yaml.ts
2952
3376
  init_cjs_shims();
2953
- var import_node_path10 = __toESM(require("path"), 1);
3377
+ var import_node_path13 = __toESM(require("path"), 1);
2954
3378
  async function parse(serviceDir) {
2955
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3379
+ const yamlPath = import_node_path13.default.join(serviceDir, "db-config.yaml");
2956
3380
  if (!await exists(yamlPath)) return [];
2957
3381
  const raw = await readYaml(yamlPath);
2958
3382
  return [
@@ -2970,13 +3394,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2970
3394
 
2971
3395
  // src/extract/databases/dotenv.ts
2972
3396
  init_cjs_shims();
2973
- var import_node_fs11 = require("fs");
2974
- var import_node_path12 = __toESM(require("path"), 1);
3397
+ var import_node_fs13 = require("fs");
3398
+ var import_node_path15 = __toESM(require("path"), 1);
2975
3399
 
2976
3400
  // src/extract/databases/shared.ts
2977
3401
  init_cjs_shims();
2978
- var import_node_fs10 = require("fs");
2979
- var import_node_path11 = __toESM(require("path"), 1);
3402
+ var import_node_fs12 = require("fs");
3403
+ var import_node_path14 = __toESM(require("path"), 1);
2980
3404
  function schemeToEngine(scheme) {
2981
3405
  const s = scheme.toLowerCase().split("+")[0];
2982
3406
  switch (s) {
@@ -3015,14 +3439,14 @@ function parseConnectionString(url) {
3015
3439
  }
3016
3440
  async function readIfExists(filePath) {
3017
3441
  try {
3018
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3442
+ return await import_node_fs12.promises.readFile(filePath, "utf8");
3019
3443
  } catch {
3020
3444
  return null;
3021
3445
  }
3022
3446
  }
3023
3447
  async function findFirst(serviceDir, candidates) {
3024
3448
  for (const rel of candidates) {
3025
- const abs = import_node_path11.default.join(serviceDir, rel);
3449
+ const abs = import_node_path14.default.join(serviceDir, rel);
3026
3450
  const content = await readIfExists(abs);
3027
3451
  if (content !== null) return abs;
3028
3452
  }
@@ -3073,15 +3497,15 @@ function parseDotenvLine(line) {
3073
3497
  return { key, value };
3074
3498
  }
3075
3499
  async function parse2(serviceDir) {
3076
- const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3500
+ const entries = await import_node_fs13.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3077
3501
  const configs = [];
3078
3502
  const seen = /* @__PURE__ */ new Set();
3079
3503
  for (const entry2 of entries) {
3080
3504
  if (!entry2.isFile()) continue;
3081
3505
  const match = isConfigFile(entry2.name);
3082
3506
  if (!match.match || match.fileType !== "env") continue;
3083
- const filePath = import_node_path12.default.join(serviceDir, entry2.name);
3084
- const content = await import_node_fs11.promises.readFile(filePath, "utf8");
3507
+ const filePath = import_node_path15.default.join(serviceDir, entry2.name);
3508
+ const content = await import_node_fs13.promises.readFile(filePath, "utf8");
3085
3509
  for (const line of content.split("\n")) {
3086
3510
  const parsed = parseDotenvLine(line);
3087
3511
  if (!parsed) continue;
@@ -3100,9 +3524,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3100
3524
 
3101
3525
  // src/extract/databases/prisma.ts
3102
3526
  init_cjs_shims();
3103
- var import_node_path13 = __toESM(require("path"), 1);
3527
+ var import_node_path16 = __toESM(require("path"), 1);
3104
3528
  async function parse3(serviceDir) {
3105
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
3529
+ const schemaPath = import_node_path16.default.join(serviceDir, "prisma", "schema.prisma");
3106
3530
  const content = await readIfExists(schemaPath);
3107
3531
  if (!content) return [];
3108
3532
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3234,10 +3658,10 @@ var knexParser = { name: "knex", parse: parse5 };
3234
3658
 
3235
3659
  // src/extract/databases/ormconfig.ts
3236
3660
  init_cjs_shims();
3237
- var import_node_path14 = __toESM(require("path"), 1);
3661
+ var import_node_path17 = __toESM(require("path"), 1);
3238
3662
  async function parse6(serviceDir) {
3239
3663
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
3240
- const abs = import_node_path14.default.join(serviceDir, candidate);
3664
+ const abs = import_node_path17.default.join(serviceDir, candidate);
3241
3665
  if (!await exists(abs)) continue;
3242
3666
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3243
3667
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -3297,9 +3721,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3297
3721
 
3298
3722
  // src/extract/databases/sequelize.ts
3299
3723
  init_cjs_shims();
3300
- var import_node_path15 = __toESM(require("path"), 1);
3724
+ var import_node_path18 = __toESM(require("path"), 1);
3301
3725
  async function parse8(serviceDir) {
3302
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3726
+ const configPath = import_node_path18.default.join(serviceDir, "config", "config.json");
3303
3727
  if (!await exists(configPath)) return [];
3304
3728
  const raw = await readJson(configPath);
3305
3729
  const out = [];
@@ -3326,7 +3750,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3326
3750
 
3327
3751
  // src/extract/databases/docker-compose.ts
3328
3752
  init_cjs_shims();
3329
- var import_node_path16 = __toESM(require("path"), 1);
3753
+ var import_node_path19 = __toESM(require("path"), 1);
3330
3754
  function portFromService(svc) {
3331
3755
  for (const raw of svc.ports ?? []) {
3332
3756
  const str = String(raw);
@@ -3353,7 +3777,7 @@ function databaseFromEnv(svc) {
3353
3777
  }
3354
3778
  async function parse9(serviceDir) {
3355
3779
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3356
- const abs = import_node_path16.default.join(serviceDir, name);
3780
+ const abs = import_node_path19.default.join(serviceDir, name);
3357
3781
  if (!await exists(abs)) continue;
3358
3782
  const raw = await readYaml(abs);
3359
3783
  if (!raw?.services) return [];
@@ -3394,8 +3818,8 @@ function compatibleDriversFor(engine) {
3394
3818
  }
3395
3819
  function toDatabaseNode(config) {
3396
3820
  return {
3397
- id: (0, import_types7.databaseId)(config.host),
3398
- type: import_types7.NodeType.DatabaseNode,
3821
+ id: (0, import_types9.databaseId)(config.host),
3822
+ type: import_types9.NodeType.DatabaseNode,
3399
3823
  name: config.database || config.host,
3400
3824
  engine: config.engine,
3401
3825
  engineVersion: config.engineVersion,
@@ -3525,19 +3949,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3525
3949
  discoveredVia: mergedDiscoveredVia
3526
3950
  });
3527
3951
  }
3952
+ const relConfigFile = toPosix2(import_node_path20.default.relative(service.dir, config.sourceFile));
3953
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3954
+ graph,
3955
+ service.pkg.name,
3956
+ service.node.id,
3957
+ relConfigFile
3958
+ );
3959
+ nodesAdded += fn;
3960
+ edgesAdded += fe;
3961
+ const evidenceFile = toPosix2(import_node_path20.default.relative(scanPath, config.sourceFile));
3528
3962
  const edge = {
3529
- id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
3530
- source: service.node.id,
3963
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, dbNode.id, import_types9.EdgeType.CONNECTS_TO),
3964
+ source: fileNodeId,
3531
3965
  target: dbNode.id,
3532
- type: import_types7.EdgeType.CONNECTS_TO,
3533
- provenance: import_types7.Provenance.EXTRACTED,
3534
- confidence: (0, import_types7.confidenceForExtracted)("structural"),
3535
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3536
- // Ghost-edge cleanup keys retirement on this; the conditional
3537
- // sourceFile spread that used to live here was a v0.1.x leftover.
3538
- evidence: {
3539
- file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3540
- }
3966
+ type: import_types9.EdgeType.CONNECTS_TO,
3967
+ provenance: import_types9.Provenance.EXTRACTED,
3968
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3969
+ evidence: { file: evidenceFile }
3541
3970
  };
3542
3971
  if (!graph.hasEdge(edge.id)) {
3543
3972
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3563,15 +3992,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3563
3992
 
3564
3993
  // src/extract/configs.ts
3565
3994
  init_cjs_shims();
3566
- var import_node_fs12 = require("fs");
3567
- var import_node_path18 = __toESM(require("path"), 1);
3568
- var import_types8 = require("@neat.is/types");
3995
+ var import_node_fs14 = require("fs");
3996
+ var import_node_path21 = __toESM(require("path"), 1);
3997
+ var import_types10 = require("@neat.is/types");
3569
3998
  async function walkConfigFiles(dir) {
3570
3999
  const out = [];
3571
4000
  async function walk(current) {
3572
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
4001
+ const entries = await import_node_fs14.promises.readdir(current, { withFileTypes: true });
3573
4002
  for (const entry2 of entries) {
3574
- const full = import_node_path18.default.join(current, entry2.name);
4003
+ const full = import_node_path21.default.join(current, entry2.name);
3575
4004
  if (entry2.isDirectory()) {
3576
4005
  if (IGNORED_DIRS.has(entry2.name)) continue;
3577
4006
  if (await isPythonVenvDir(full)) continue;
@@ -3590,26 +4019,35 @@ async function addConfigNodes(graph, services, scanPath) {
3590
4019
  for (const service of services) {
3591
4020
  const configFiles = await walkConfigFiles(service.dir);
3592
4021
  for (const file of configFiles) {
3593
- const relPath = import_node_path18.default.relative(scanPath, file);
4022
+ const relPath = import_node_path21.default.relative(scanPath, file);
3594
4023
  const node = {
3595
- id: (0, import_types8.configId)(relPath),
3596
- type: import_types8.NodeType.ConfigNode,
3597
- name: import_node_path18.default.basename(file),
4024
+ id: (0, import_types10.configId)(relPath),
4025
+ type: import_types10.NodeType.ConfigNode,
4026
+ name: import_node_path21.default.basename(file),
3598
4027
  path: relPath,
3599
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
4028
+ fileType: isConfigFile(import_node_path21.default.basename(file)).fileType
3600
4029
  };
3601
4030
  if (!graph.hasNode(node.id)) {
3602
4031
  graph.addNode(node.id, node);
3603
4032
  nodesAdded++;
3604
4033
  }
4034
+ const relToService = toPosix2(import_node_path21.default.relative(service.dir, file));
4035
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4036
+ graph,
4037
+ service.pkg.name,
4038
+ service.node.id,
4039
+ relToService
4040
+ );
4041
+ nodesAdded += fn;
4042
+ edgesAdded += fe;
3605
4043
  const edge = {
3606
- id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
3607
- source: service.node.id,
4044
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types10.EdgeType.CONFIGURED_BY),
4045
+ source: fileNodeId,
3608
4046
  target: node.id,
3609
- type: import_types8.EdgeType.CONFIGURED_BY,
3610
- provenance: import_types8.Provenance.EXTRACTED,
3611
- confidence: (0, import_types8.confidenceForExtracted)("structural"),
3612
- evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
4047
+ type: import_types10.EdgeType.CONFIGURED_BY,
4048
+ provenance: import_types10.Provenance.EXTRACTED,
4049
+ confidence: (0, import_types10.confidenceForExtracted)("structural"),
4050
+ evidence: { file: relPath.split(import_node_path21.default.sep).join("/") }
3613
4051
  };
3614
4052
  if (!graph.hasEdge(edge.id)) {
3615
4053
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3622,114 +4060,15 @@ async function addConfigNodes(graph, services, scanPath) {
3622
4060
 
3623
4061
  // src/extract/calls/index.ts
3624
4062
  init_cjs_shims();
3625
- var import_types15 = require("@neat.is/types");
4063
+ var import_types16 = require("@neat.is/types");
3626
4064
 
3627
4065
  // src/extract/calls/http.ts
3628
4066
  init_cjs_shims();
3629
- var import_node_path20 = __toESM(require("path"), 1);
3630
- var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3631
- var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3632
- var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3633
- var import_types10 = require("@neat.is/types");
3634
-
3635
- // src/extract/calls/shared.ts
3636
- init_cjs_shims();
3637
- var import_node_fs13 = require("fs");
3638
- var import_node_path19 = __toESM(require("path"), 1);
3639
- var import_types9 = require("@neat.is/types");
3640
- async function walkSourceFiles(dir) {
3641
- const out = [];
3642
- async function walk(current) {
3643
- const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3644
- for (const entry2 of entries) {
3645
- const full = import_node_path19.default.join(current, entry2.name);
3646
- if (entry2.isDirectory()) {
3647
- if (IGNORED_DIRS.has(entry2.name)) continue;
3648
- if (await isPythonVenvDir(full)) continue;
3649
- await walk(full);
3650
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3651
- out.push(full);
3652
- }
3653
- }
3654
- }
3655
- await walk(dir);
3656
- return out;
3657
- }
3658
- async function loadSourceFiles(dir) {
3659
- const paths = await walkSourceFiles(dir);
3660
- const out = [];
3661
- for (const p of paths) {
3662
- try {
3663
- const content = await import_node_fs13.promises.readFile(p, "utf8");
3664
- out.push({ path: p, content });
3665
- } catch {
3666
- }
3667
- }
3668
- return out;
3669
- }
3670
- function lineOf(text, needle) {
3671
- const idx = text.indexOf(needle);
3672
- if (idx < 0) return 1;
3673
- return text.slice(0, idx).split("\n").length;
3674
- }
3675
- function snippet(text, line) {
3676
- const lines = text.split("\n");
3677
- return (lines[line - 1] ?? "").trim();
3678
- }
3679
- function toPosix2(p) {
3680
- return p.split("\\").join("/");
3681
- }
3682
- function languageForPath(relPath) {
3683
- switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3684
- case ".py":
3685
- return "python";
3686
- case ".ts":
3687
- case ".tsx":
3688
- return "typescript";
3689
- case ".js":
3690
- case ".jsx":
3691
- case ".mjs":
3692
- case ".cjs":
3693
- return "javascript";
3694
- default:
3695
- return void 0;
3696
- }
3697
- }
3698
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3699
- let nodesAdded = 0;
3700
- let edgesAdded = 0;
3701
- const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3702
- if (!graph.hasNode(fileNodeId)) {
3703
- const language = languageForPath(relPath);
3704
- const node = {
3705
- id: fileNodeId,
3706
- type: import_types9.NodeType.FileNode,
3707
- service: serviceName,
3708
- path: relPath,
3709
- ...language ? { language } : {},
3710
- discoveredVia: "static"
3711
- };
3712
- graph.addNode(fileNodeId, node);
3713
- nodesAdded++;
3714
- }
3715
- const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3716
- if (!graph.hasEdge(containsId)) {
3717
- const edge = {
3718
- id: containsId,
3719
- source: serviceNodeId,
3720
- target: fileNodeId,
3721
- type: import_types9.EdgeType.CONTAINS,
3722
- provenance: import_types9.Provenance.EXTRACTED,
3723
- confidence: (0, import_types9.confidenceForExtracted)("structural"),
3724
- evidence: { file: relPath }
3725
- };
3726
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3727
- edgesAdded++;
3728
- }
3729
- return { fileNodeId, nodesAdded, edgesAdded };
3730
- }
3731
-
3732
- // src/extract/calls/http.ts
4067
+ var import_node_path22 = __toESM(require("path"), 1);
4068
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
4069
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
4070
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
4071
+ var import_types11 = require("@neat.is/types");
3733
4072
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3734
4073
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3735
4074
  function isInsideJsxExternalLink(node) {
@@ -3757,14 +4096,14 @@ function collectStringLiterals(node, out) {
3757
4096
  if (child) collectStringLiterals(child, out);
3758
4097
  }
3759
4098
  }
3760
- var PARSE_CHUNK = 16384;
3761
- function parseSource(parser, source) {
4099
+ var PARSE_CHUNK2 = 16384;
4100
+ function parseSource2(parser, source) {
3762
4101
  return parser.parse(
3763
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
4102
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
3764
4103
  );
3765
4104
  }
3766
4105
  function callsFromSource(source, parser, knownHosts) {
3767
- const tree = parseSource(parser, source);
4106
+ const tree = parseSource2(parser, source);
3768
4107
  const literals = [];
3769
4108
  collectStringLiterals(tree.rootNode, literals);
3770
4109
  const out = [];
@@ -3778,25 +4117,25 @@ function callsFromSource(source, parser, knownHosts) {
3778
4117
  }
3779
4118
  return out;
3780
4119
  }
3781
- function makeJsParser() {
3782
- const p = new import_tree_sitter.default();
3783
- p.setLanguage(import_tree_sitter_javascript.default);
4120
+ function makeJsParser2() {
4121
+ const p = new import_tree_sitter2.default();
4122
+ p.setLanguage(import_tree_sitter_javascript2.default);
3784
4123
  return p;
3785
4124
  }
3786
- function makePyParser() {
3787
- const p = new import_tree_sitter.default();
3788
- p.setLanguage(import_tree_sitter_python.default);
4125
+ function makePyParser2() {
4126
+ const p = new import_tree_sitter2.default();
4127
+ p.setLanguage(import_tree_sitter_python2.default);
3789
4128
  return p;
3790
4129
  }
3791
4130
  async function addHttpCallEdges(graph, services) {
3792
- const jsParser = makeJsParser();
3793
- const pyParser = makePyParser();
4131
+ const jsParser = makeJsParser2();
4132
+ const pyParser = makePyParser2();
3794
4133
  const knownHosts = /* @__PURE__ */ new Set();
3795
4134
  const hostToNodeId = /* @__PURE__ */ new Map();
3796
4135
  for (const service of services) {
3797
- knownHosts.add(import_node_path20.default.basename(service.dir));
4136
+ knownHosts.add(import_node_path22.default.basename(service.dir));
3798
4137
  knownHosts.add(service.pkg.name);
3799
- hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
4138
+ hostToNodeId.set(import_node_path22.default.basename(service.dir), service.node.id);
3800
4139
  hostToNodeId.set(service.pkg.name, service.node.id);
3801
4140
  }
3802
4141
  let nodesAdded = 0;
@@ -3806,7 +4145,7 @@ async function addHttpCallEdges(graph, services) {
3806
4145
  const seen = /* @__PURE__ */ new Set();
3807
4146
  for (const file of files) {
3808
4147
  if (isTestPath(file.path)) continue;
3809
- const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
4148
+ const parser = import_node_path22.default.extname(file.path) === ".py" ? pyParser : jsParser;
3810
4149
  let sites;
3811
4150
  try {
3812
4151
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -3815,14 +4154,14 @@ async function addHttpCallEdges(graph, services) {
3815
4154
  continue;
3816
4155
  }
3817
4156
  if (sites.length === 0) continue;
3818
- const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
4157
+ const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
3819
4158
  for (const site of sites) {
3820
4159
  const targetId = hostToNodeId.get(site.host);
3821
4160
  if (!targetId || targetId === service.node.id) continue;
3822
4161
  const dedupKey = `${relFile}|${targetId}`;
3823
4162
  if (seen.has(dedupKey)) continue;
3824
4163
  seen.add(dedupKey);
3825
- const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
4164
+ const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
3826
4165
  const ev = {
3827
4166
  file: relFile,
3828
4167
  line: site.line,
@@ -3836,25 +4175,25 @@ async function addHttpCallEdges(graph, services) {
3836
4175
  );
3837
4176
  nodesAdded += n;
3838
4177
  edgesAdded += e;
3839
- if (!(0, import_types10.passesExtractedFloor)(confidence)) {
4178
+ if (!(0, import_types11.passesExtractedFloor)(confidence)) {
3840
4179
  noteExtractedDropped({
3841
4180
  source: fileNodeId,
3842
4181
  target: targetId,
3843
- type: import_types10.EdgeType.CALLS,
4182
+ type: import_types11.EdgeType.CALLS,
3844
4183
  confidence,
3845
4184
  confidenceKind: "hostname-shape-match",
3846
4185
  evidence: ev
3847
4186
  });
3848
4187
  continue;
3849
4188
  }
3850
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
4189
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types11.EdgeType.CALLS);
3851
4190
  if (!graph.hasEdge(edgeId)) {
3852
4191
  const edge = {
3853
4192
  id: edgeId,
3854
4193
  source: fileNodeId,
3855
4194
  target: targetId,
3856
- type: import_types10.EdgeType.CALLS,
3857
- provenance: import_types10.Provenance.EXTRACTED,
4195
+ type: import_types11.EdgeType.CALLS,
4196
+ provenance: import_types11.Provenance.EXTRACTED,
3858
4197
  confidence,
3859
4198
  evidence: ev
3860
4199
  };
@@ -3869,8 +4208,8 @@ async function addHttpCallEdges(graph, services) {
3869
4208
 
3870
4209
  // src/extract/calls/kafka.ts
3871
4210
  init_cjs_shims();
3872
- var import_node_path21 = __toESM(require("path"), 1);
3873
- var import_types11 = require("@neat.is/types");
4211
+ var import_node_path23 = __toESM(require("path"), 1);
4212
+ var import_types12 = require("@neat.is/types");
3874
4213
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3875
4214
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3876
4215
  function findAll(re, text) {
@@ -3891,7 +4230,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3891
4230
  seen.add(key);
3892
4231
  const line = lineOf(file.content, topic);
3893
4232
  out.push({
3894
- infraId: (0, import_types11.infraId)("kafka-topic", topic),
4233
+ infraId: (0, import_types12.infraId)("kafka-topic", topic),
3895
4234
  name: topic,
3896
4235
  kind: "kafka-topic",
3897
4236
  edgeType,
@@ -3900,7 +4239,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3900
4239
  // tier (ADR-066).
3901
4240
  confidenceKind: "verified-call-site",
3902
4241
  evidence: {
3903
- file: import_node_path21.default.relative(serviceDir, file.path),
4242
+ file: import_node_path23.default.relative(serviceDir, file.path),
3904
4243
  line,
3905
4244
  snippet: snippet(file.content, line)
3906
4245
  }
@@ -3913,8 +4252,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3913
4252
 
3914
4253
  // src/extract/calls/redis.ts
3915
4254
  init_cjs_shims();
3916
- var import_node_path22 = __toESM(require("path"), 1);
3917
- var import_types12 = require("@neat.is/types");
4255
+ var import_node_path24 = __toESM(require("path"), 1);
4256
+ var import_types13 = require("@neat.is/types");
3918
4257
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3919
4258
  function redisEndpointsFromFile(file, serviceDir) {
3920
4259
  const out = [];
@@ -3927,7 +4266,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3927
4266
  seen.add(host);
3928
4267
  const line = lineOf(file.content, host);
3929
4268
  out.push({
3930
- infraId: (0, import_types12.infraId)("redis", host),
4269
+ infraId: (0, import_types13.infraId)("redis", host),
3931
4270
  name: host,
3932
4271
  kind: "redis",
3933
4272
  edgeType: "CALLS",
@@ -3936,7 +4275,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3936
4275
  // support tier (ADR-066).
3937
4276
  confidenceKind: "url-with-structural-support",
3938
4277
  evidence: {
3939
- file: import_node_path22.default.relative(serviceDir, file.path),
4278
+ file: import_node_path24.default.relative(serviceDir, file.path),
3940
4279
  line,
3941
4280
  snippet: snippet(file.content, line)
3942
4281
  }
@@ -3947,8 +4286,8 @@ function redisEndpointsFromFile(file, serviceDir) {
3947
4286
 
3948
4287
  // src/extract/calls/aws.ts
3949
4288
  init_cjs_shims();
3950
- var import_node_path23 = __toESM(require("path"), 1);
3951
- var import_types13 = require("@neat.is/types");
4289
+ var import_node_path25 = __toESM(require("path"), 1);
4290
+ var import_types14 = require("@neat.is/types");
3952
4291
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3953
4292
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3954
4293
  function hasMarker(text, markers) {
@@ -3972,7 +4311,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3972
4311
  seen.add(key);
3973
4312
  const line = lineOf(file.content, name);
3974
4313
  out.push({
3975
- infraId: (0, import_types13.infraId)(kind, name),
4314
+ infraId: (0, import_types14.infraId)(kind, name),
3976
4315
  name,
3977
4316
  kind,
3978
4317
  edgeType: "CALLS",
@@ -3981,7 +4320,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3981
4320
  // (ADR-066).
3982
4321
  confidenceKind: "verified-call-site",
3983
4322
  evidence: {
3984
- file: import_node_path23.default.relative(serviceDir, file.path),
4323
+ file: import_node_path25.default.relative(serviceDir, file.path),
3985
4324
  line,
3986
4325
  snippet: snippet(file.content, line)
3987
4326
  }
@@ -4006,13 +4345,14 @@ function awsEndpointsFromFile(file, serviceDir) {
4006
4345
 
4007
4346
  // src/extract/calls/grpc.ts
4008
4347
  init_cjs_shims();
4009
- var import_node_path24 = __toESM(require("path"), 1);
4010
- var import_types14 = require("@neat.is/types");
4348
+ var import_node_path26 = __toESM(require("path"), 1);
4349
+ var import_types15 = require("@neat.is/types");
4011
4350
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
4012
4351
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
4013
4352
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
4014
4353
  function isLikelyAddress(value) {
4015
4354
  if (!value) return false;
4355
+ if (/\s/.test(value) || value.startsWith("{")) return false;
4016
4356
  return /:\d{2,5}$/.test(value) || value.includes(".");
4017
4357
  }
4018
4358
  function normaliseForMatch(s) {
@@ -4055,7 +4395,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4055
4395
  const { kind } = classified;
4056
4396
  const line = lineOf(file.content, m[0]);
4057
4397
  out.push({
4058
- infraId: (0, import_types14.infraId)(kind, name),
4398
+ infraId: (0, import_types15.infraId)(kind, name),
4059
4399
  name,
4060
4400
  kind,
4061
4401
  edgeType: "CALLS",
@@ -4064,7 +4404,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4064
4404
  // tier (ADR-066).
4065
4405
  confidenceKind: "verified-call-site",
4066
4406
  evidence: {
4067
- file: import_node_path24.default.relative(serviceDir, file.path),
4407
+ file: import_node_path26.default.relative(serviceDir, file.path),
4068
4408
  line,
4069
4409
  snippet: snippet(file.content, line)
4070
4410
  }
@@ -4077,11 +4417,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
4077
4417
  function edgeTypeFromEndpoint(ep) {
4078
4418
  switch (ep.edgeType) {
4079
4419
  case "PUBLISHES_TO":
4080
- return import_types15.EdgeType.PUBLISHES_TO;
4420
+ return import_types16.EdgeType.PUBLISHES_TO;
4081
4421
  case "CONSUMES_FROM":
4082
- return import_types15.EdgeType.CONSUMES_FROM;
4422
+ return import_types16.EdgeType.CONSUMES_FROM;
4083
4423
  default:
4084
- return import_types15.EdgeType.CALLS;
4424
+ return import_types16.EdgeType.CALLS;
4085
4425
  }
4086
4426
  }
4087
4427
  function isAwsKind(kind) {
@@ -4108,7 +4448,7 @@ async function addExternalEndpointEdges(graph, services) {
4108
4448
  if (!graph.hasNode(ep.infraId)) {
4109
4449
  const node = {
4110
4450
  id: ep.infraId,
4111
- type: import_types15.NodeType.InfraNode,
4451
+ type: import_types16.NodeType.InfraNode,
4112
4452
  name: ep.name,
4113
4453
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4114
4454
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4120,7 +4460,7 @@ async function addExternalEndpointEdges(graph, services) {
4120
4460
  nodesAdded++;
4121
4461
  }
4122
4462
  const edgeType = edgeTypeFromEndpoint(ep);
4123
- const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4463
+ const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4124
4464
  const relFile = toPosix2(ep.evidence.file);
4125
4465
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4126
4466
  graph,
@@ -4130,7 +4470,7 @@ async function addExternalEndpointEdges(graph, services) {
4130
4470
  );
4131
4471
  nodesAdded += n;
4132
4472
  edgesAdded += e;
4133
- if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4473
+ if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4134
4474
  noteExtractedDropped({
4135
4475
  source: fileNodeId,
4136
4476
  target: ep.infraId,
@@ -4150,7 +4490,7 @@ async function addExternalEndpointEdges(graph, services) {
4150
4490
  source: fileNodeId,
4151
4491
  target: ep.infraId,
4152
4492
  type: edgeType,
4153
- provenance: import_types15.Provenance.EXTRACTED,
4493
+ provenance: import_types16.Provenance.EXTRACTED,
4154
4494
  confidence,
4155
4495
  evidence: ep.evidence
4156
4496
  };
@@ -4175,16 +4515,16 @@ init_cjs_shims();
4175
4515
 
4176
4516
  // src/extract/infra/docker-compose.ts
4177
4517
  init_cjs_shims();
4178
- var import_node_path25 = __toESM(require("path"), 1);
4179
- var import_types17 = require("@neat.is/types");
4518
+ var import_node_path27 = __toESM(require("path"), 1);
4519
+ var import_types18 = require("@neat.is/types");
4180
4520
 
4181
4521
  // src/extract/infra/shared.ts
4182
4522
  init_cjs_shims();
4183
- var import_types16 = require("@neat.is/types");
4523
+ var import_types17 = require("@neat.is/types");
4184
4524
  function makeInfraNode(kind, name, provider = "self", extras) {
4185
4525
  return {
4186
- id: (0, import_types16.infraId)(kind, name),
4187
- type: import_types16.NodeType.InfraNode,
4526
+ id: (0, import_types17.infraId)(kind, name),
4527
+ type: import_types17.NodeType.InfraNode,
4188
4528
  name,
4189
4529
  provider,
4190
4530
  kind,
@@ -4213,7 +4553,7 @@ function dependsOnList(value) {
4213
4553
  }
4214
4554
  function serviceNameToServiceNode(name, services) {
4215
4555
  for (const s of services) {
4216
- if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
4556
+ if (s.node.name === name || import_node_path27.default.basename(s.dir) === name) return s.node.id;
4217
4557
  }
4218
4558
  return null;
4219
4559
  }
@@ -4222,7 +4562,7 @@ async function addComposeInfra(graph, scanPath, services) {
4222
4562
  let edgesAdded = 0;
4223
4563
  let composePath = null;
4224
4564
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4225
- const abs = import_node_path25.default.join(scanPath, name);
4565
+ const abs = import_node_path27.default.join(scanPath, name);
4226
4566
  if (await exists(abs)) {
4227
4567
  composePath = abs;
4228
4568
  break;
@@ -4235,13 +4575,13 @@ async function addComposeInfra(graph, scanPath, services) {
4235
4575
  } catch (err) {
4236
4576
  recordExtractionError(
4237
4577
  "infra docker-compose",
4238
- import_node_path25.default.relative(scanPath, composePath),
4578
+ import_node_path27.default.relative(scanPath, composePath),
4239
4579
  err
4240
4580
  );
4241
4581
  return { nodesAdded, edgesAdded };
4242
4582
  }
4243
4583
  if (!compose?.services) return { nodesAdded, edgesAdded };
4244
- const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
4584
+ const evidenceFile = import_node_path27.default.relative(scanPath, composePath).split(import_node_path27.default.sep).join("/");
4245
4585
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4246
4586
  for (const [composeName, svc] of Object.entries(compose.services)) {
4247
4587
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4263,15 +4603,15 @@ async function addComposeInfra(graph, scanPath, services) {
4263
4603
  for (const dep of dependsOnList(svc.depends_on)) {
4264
4604
  const targetId = composeNameToNodeId.get(dep);
4265
4605
  if (!targetId) continue;
4266
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4606
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4267
4607
  if (graph.hasEdge(edgeId)) continue;
4268
4608
  const edge = {
4269
4609
  id: edgeId,
4270
4610
  source: sourceId,
4271
4611
  target: targetId,
4272
- type: import_types17.EdgeType.DEPENDS_ON,
4273
- provenance: import_types17.Provenance.EXTRACTED,
4274
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4612
+ type: import_types18.EdgeType.DEPENDS_ON,
4613
+ provenance: import_types18.Provenance.EXTRACTED,
4614
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4275
4615
  evidence: { file: evidenceFile }
4276
4616
  };
4277
4617
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4283,9 +4623,9 @@ async function addComposeInfra(graph, scanPath, services) {
4283
4623
 
4284
4624
  // src/extract/infra/dockerfile.ts
4285
4625
  init_cjs_shims();
4286
- var import_node_path26 = __toESM(require("path"), 1);
4287
- var import_node_fs14 = require("fs");
4288
- var import_types18 = require("@neat.is/types");
4626
+ var import_node_path28 = __toESM(require("path"), 1);
4627
+ var import_node_fs15 = require("fs");
4628
+ var import_types19 = require("@neat.is/types");
4289
4629
  function runtimeImage(content) {
4290
4630
  const lines = content.split("\n");
4291
4631
  let last = null;
@@ -4304,15 +4644,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4304
4644
  let nodesAdded = 0;
4305
4645
  let edgesAdded = 0;
4306
4646
  for (const service of services) {
4307
- const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
4647
+ const dockerfilePath = import_node_path28.default.join(service.dir, "Dockerfile");
4308
4648
  if (!await exists(dockerfilePath)) continue;
4309
4649
  let content;
4310
4650
  try {
4311
- content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
4651
+ content = await import_node_fs15.promises.readFile(dockerfilePath, "utf8");
4312
4652
  } catch (err) {
4313
4653
  recordExtractionError(
4314
4654
  "infra dockerfile",
4315
- import_node_path26.default.relative(scanPath, dockerfilePath),
4655
+ import_node_path28.default.relative(scanPath, dockerfilePath),
4316
4656
  err
4317
4657
  );
4318
4658
  continue;
@@ -4324,17 +4664,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4324
4664
  graph.addNode(node.id, node);
4325
4665
  nodesAdded++;
4326
4666
  }
4327
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4667
+ const relDockerfile = toPosix2(import_node_path28.default.relative(service.dir, dockerfilePath));
4668
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4669
+ graph,
4670
+ service.pkg.name,
4671
+ service.node.id,
4672
+ relDockerfile
4673
+ );
4674
+ nodesAdded += fn;
4675
+ edgesAdded += fe;
4676
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
4328
4677
  if (!graph.hasEdge(edgeId)) {
4329
4678
  const edge = {
4330
4679
  id: edgeId,
4331
- source: service.node.id,
4680
+ source: fileNodeId,
4332
4681
  target: node.id,
4333
- type: import_types18.EdgeType.RUNS_ON,
4334
- provenance: import_types18.Provenance.EXTRACTED,
4335
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4682
+ type: import_types19.EdgeType.RUNS_ON,
4683
+ provenance: import_types19.Provenance.EXTRACTED,
4684
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4336
4685
  evidence: {
4337
- file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4686
+ file: toPosix2(import_node_path28.default.relative(scanPath, dockerfilePath))
4338
4687
  }
4339
4688
  };
4340
4689
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4346,21 +4695,21 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4346
4695
 
4347
4696
  // src/extract/infra/terraform.ts
4348
4697
  init_cjs_shims();
4349
- var import_node_fs15 = require("fs");
4350
- var import_node_path27 = __toESM(require("path"), 1);
4698
+ var import_node_fs16 = require("fs");
4699
+ var import_node_path29 = __toESM(require("path"), 1);
4351
4700
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4352
4701
  async function walkTfFiles(start, depth = 0, max = 5) {
4353
4702
  if (depth > max) return [];
4354
4703
  const out = [];
4355
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4704
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4356
4705
  for (const entry2 of entries) {
4357
4706
  if (entry2.isDirectory()) {
4358
4707
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
4359
- const child = import_node_path27.default.join(start, entry2.name);
4708
+ const child = import_node_path29.default.join(start, entry2.name);
4360
4709
  if (await isPythonVenvDir(child)) continue;
4361
4710
  out.push(...await walkTfFiles(child, depth + 1, max));
4362
4711
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
4363
- out.push(import_node_path27.default.join(start, entry2.name));
4712
+ out.push(import_node_path29.default.join(start, entry2.name));
4364
4713
  }
4365
4714
  }
4366
4715
  return out;
@@ -4369,7 +4718,7 @@ async function addTerraformResources(graph, scanPath) {
4369
4718
  let nodesAdded = 0;
4370
4719
  const files = await walkTfFiles(scanPath);
4371
4720
  for (const file of files) {
4372
- const content = await import_node_fs15.promises.readFile(file, "utf8");
4721
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
4373
4722
  RESOURCE_RE.lastIndex = 0;
4374
4723
  let m;
4375
4724
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -4387,8 +4736,8 @@ async function addTerraformResources(graph, scanPath) {
4387
4736
 
4388
4737
  // src/extract/infra/k8s.ts
4389
4738
  init_cjs_shims();
4390
- var import_node_fs16 = require("fs");
4391
- var import_node_path28 = __toESM(require("path"), 1);
4739
+ var import_node_fs17 = require("fs");
4740
+ var import_node_path30 = __toESM(require("path"), 1);
4392
4741
  var import_yaml3 = require("yaml");
4393
4742
  var K8S_KIND_TO_INFRA_KIND = {
4394
4743
  Service: "k8s-service",
@@ -4402,15 +4751,15 @@ var K8S_KIND_TO_INFRA_KIND = {
4402
4751
  async function walkYamlFiles2(start, depth = 0, max = 5) {
4403
4752
  if (depth > max) return [];
4404
4753
  const out = [];
4405
- const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4754
+ const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4406
4755
  for (const entry2 of entries) {
4407
4756
  if (entry2.isDirectory()) {
4408
4757
  if (IGNORED_DIRS.has(entry2.name)) continue;
4409
- const child = import_node_path28.default.join(start, entry2.name);
4758
+ const child = import_node_path30.default.join(start, entry2.name);
4410
4759
  if (await isPythonVenvDir(child)) continue;
4411
4760
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4412
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
4413
- out.push(import_node_path28.default.join(start, entry2.name));
4761
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path30.default.extname(entry2.name))) {
4762
+ out.push(import_node_path30.default.join(start, entry2.name));
4414
4763
  }
4415
4764
  }
4416
4765
  return out;
@@ -4419,7 +4768,7 @@ async function addK8sResources(graph, scanPath) {
4419
4768
  let nodesAdded = 0;
4420
4769
  const files = await walkYamlFiles2(scanPath);
4421
4770
  for (const file of files) {
4422
- const content = await import_node_fs16.promises.readFile(file, "utf8");
4771
+ const content = await import_node_fs17.promises.readFile(file, "utf8");
4423
4772
  let docs;
4424
4773
  try {
4425
4774
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -4454,17 +4803,17 @@ async function addInfra(graph, scanPath, services) {
4454
4803
  }
4455
4804
 
4456
4805
  // src/extract/index.ts
4457
- var import_node_path30 = __toESM(require("path"), 1);
4806
+ var import_node_path32 = __toESM(require("path"), 1);
4458
4807
 
4459
4808
  // src/extract/retire.ts
4460
4809
  init_cjs_shims();
4461
- var import_node_fs17 = require("fs");
4462
- var import_node_path29 = __toESM(require("path"), 1);
4463
- var import_types19 = require("@neat.is/types");
4810
+ var import_node_fs18 = require("fs");
4811
+ var import_node_path31 = __toESM(require("path"), 1);
4812
+ var import_types20 = require("@neat.is/types");
4464
4813
  function dropOrphanedFileNodes(graph) {
4465
4814
  const orphans = [];
4466
4815
  graph.forEachNode((id, attrs) => {
4467
- if (attrs.type !== import_types19.NodeType.FileNode) return;
4816
+ if (attrs.type !== import_types20.NodeType.FileNode) return;
4468
4817
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4469
4818
  orphans.push(id);
4470
4819
  }
@@ -4477,14 +4826,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4477
4826
  const bases = [scanPath, ...serviceDirs];
4478
4827
  graph.forEachEdge((id, attrs) => {
4479
4828
  const edge = attrs;
4480
- if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4829
+ if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
4481
4830
  const evidenceFile = edge.evidence?.file;
4482
4831
  if (!evidenceFile) return;
4483
- if (import_node_path29.default.isAbsolute(evidenceFile)) {
4484
- if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
4832
+ if (import_node_path31.default.isAbsolute(evidenceFile)) {
4833
+ if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
4485
4834
  return;
4486
4835
  }
4487
- const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
4836
+ const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path31.default.join(base, evidenceFile)));
4488
4837
  if (!found) toDrop.push(id);
4489
4838
  });
4490
4839
  for (const id of toDrop) graph.dropEdge(id);
@@ -4499,6 +4848,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4499
4848
  const services = await discoverServices(scanPath);
4500
4849
  const phase1Nodes = addServiceNodes(graph, services);
4501
4850
  await addServiceAliases(graph, scanPath, services);
4851
+ const fileEnum = await addFiles(graph, services);
4852
+ const importGraph = await addImports(graph, services);
4502
4853
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
4503
4854
  const phase3 = await addConfigNodes(graph, services, scanPath);
4504
4855
  const phase4 = await addCallEdges(graph, services);
@@ -4522,7 +4873,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4522
4873
  }
4523
4874
  const droppedEntries = drainDroppedExtracted();
4524
4875
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4525
- const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
4876
+ const rejectedPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.errorsPath), "rejected.ndjson");
4526
4877
  try {
4527
4878
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4528
4879
  } catch (err) {
@@ -4532,8 +4883,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4532
4883
  }
4533
4884
  }
4534
4885
  const result = {
4535
- nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
4536
- edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
4886
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
4887
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
4537
4888
  frontiersPromoted,
4538
4889
  extractionErrors: errorEntries.length,
4539
4890
  errorEntries,
@@ -4556,9 +4907,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4556
4907
 
4557
4908
  // src/persist.ts
4558
4909
  init_cjs_shims();
4559
- var import_node_fs18 = require("fs");
4560
- var import_node_path31 = __toESM(require("path"), 1);
4561
- var import_types20 = require("@neat.is/types");
4910
+ var import_node_fs19 = require("fs");
4911
+ var import_node_path33 = __toESM(require("path"), 1);
4912
+ var import_types21 = require("@neat.is/types");
4562
4913
  var SCHEMA_VERSION = 4;
4563
4914
  function migrateV1ToV2(payload) {
4564
4915
  const nodes = payload.graph.nodes;
@@ -4580,12 +4931,12 @@ function migrateV2ToV3(payload) {
4580
4931
  for (const edge of edges) {
4581
4932
  const attrs = edge.attributes;
4582
4933
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4583
- attrs.provenance = import_types20.Provenance.OBSERVED;
4934
+ attrs.provenance = import_types21.Provenance.OBSERVED;
4584
4935
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4585
4936
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4586
4937
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4587
4938
  if (type && source && target) {
4588
- const newId = (0, import_types20.observedEdgeId)(source, target, type);
4939
+ const newId = (0, import_types21.observedEdgeId)(source, target, type);
4589
4940
  attrs.id = newId;
4590
4941
  if (edge.key) edge.key = newId;
4591
4942
  }
@@ -4594,7 +4945,7 @@ function migrateV2ToV3(payload) {
4594
4945
  return { ...payload, schemaVersion: 3 };
4595
4946
  }
4596
4947
  async function ensureDir(filePath) {
4597
- await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4948
+ await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
4598
4949
  }
4599
4950
  async function saveGraphToDisk(graph, outPath) {
4600
4951
  await ensureDir(outPath);
@@ -4604,13 +4955,13 @@ async function saveGraphToDisk(graph, outPath) {
4604
4955
  graph: graph.export()
4605
4956
  };
4606
4957
  const tmp = `${outPath}.tmp`;
4607
- await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4608
- await import_node_fs18.promises.rename(tmp, outPath);
4958
+ await import_node_fs19.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4959
+ await import_node_fs19.promises.rename(tmp, outPath);
4609
4960
  }
4610
4961
  async function loadGraphFromDisk(graph, outPath) {
4611
4962
  let raw;
4612
4963
  try {
4613
- raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4964
+ raw = await import_node_fs19.promises.readFile(outPath, "utf8");
4614
4965
  } catch (err) {
4615
4966
  if (err.code === "ENOENT") return;
4616
4967
  throw err;
@@ -4669,23 +5020,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4669
5020
 
4670
5021
  // src/projects.ts
4671
5022
  init_cjs_shims();
4672
- var import_node_path32 = __toESM(require("path"), 1);
5023
+ var import_node_path34 = __toESM(require("path"), 1);
4673
5024
  function pathsForProject(project, baseDir) {
4674
5025
  if (project === DEFAULT_PROJECT) {
4675
5026
  return {
4676
- snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
4677
- errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
4678
- staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
4679
- embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
4680
- policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
5027
+ snapshotPath: import_node_path34.default.join(baseDir, "graph.json"),
5028
+ errorsPath: import_node_path34.default.join(baseDir, "errors.ndjson"),
5029
+ staleEventsPath: import_node_path34.default.join(baseDir, "stale-events.ndjson"),
5030
+ embeddingsCachePath: import_node_path34.default.join(baseDir, "embeddings.json"),
5031
+ policyViolationsPath: import_node_path34.default.join(baseDir, "policy-violations.ndjson")
4681
5032
  };
4682
5033
  }
4683
5034
  return {
4684
- snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
4685
- errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
4686
- staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
4687
- embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
4688
- policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
5035
+ snapshotPath: import_node_path34.default.join(baseDir, `${project}.json`),
5036
+ errorsPath: import_node_path34.default.join(baseDir, `errors.${project}.ndjson`),
5037
+ staleEventsPath: import_node_path34.default.join(baseDir, `stale-events.${project}.ndjson`),
5038
+ embeddingsCachePath: import_node_path34.default.join(baseDir, `embeddings.${project}.json`),
5039
+ policyViolationsPath: import_node_path34.default.join(baseDir, `policy-violations.${project}.ndjson`)
4689
5040
  };
4690
5041
  }
4691
5042
  var Projects = class {
@@ -4723,11 +5074,326 @@ var Projects = class {
4723
5074
  init_cjs_shims();
4724
5075
  var import_fastify = __toESM(require("fastify"), 1);
4725
5076
  var import_cors = __toESM(require("@fastify/cors"), 1);
4726
- var import_types23 = require("@neat.is/types");
5077
+ var import_types24 = require("@neat.is/types");
5078
+
5079
+ // src/extend/index.ts
5080
+ init_cjs_shims();
5081
+ var import_node_fs21 = require("fs");
5082
+ var import_node_path36 = __toESM(require("path"), 1);
5083
+ var import_node_os2 = __toESM(require("os"), 1);
5084
+ var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
5085
+
5086
+ // src/installers/package-manager.ts
5087
+ init_cjs_shims();
5088
+ var import_node_fs20 = require("fs");
5089
+ var import_node_path35 = __toESM(require("path"), 1);
5090
+ var import_node_child_process = require("child_process");
5091
+ var LOCKFILE_PRIORITY = [
5092
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
5093
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
5094
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
5095
+ {
5096
+ lockfile: "package-lock.json",
5097
+ pm: "npm",
5098
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
5099
+ }
5100
+ ];
5101
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
5102
+ async function exists2(p) {
5103
+ try {
5104
+ await import_node_fs20.promises.access(p);
5105
+ return true;
5106
+ } catch {
5107
+ return false;
5108
+ }
5109
+ }
5110
+ async function detectPackageManager(serviceDir) {
5111
+ let dir = import_node_path35.default.resolve(serviceDir);
5112
+ const stops = /* @__PURE__ */ new Set();
5113
+ for (let i = 0; i < 64; i++) {
5114
+ if (stops.has(dir)) break;
5115
+ stops.add(dir);
5116
+ for (const candidate of LOCKFILE_PRIORITY) {
5117
+ const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
5118
+ if (await exists2(lockPath)) {
5119
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5120
+ }
5121
+ }
5122
+ const parent = import_node_path35.default.dirname(dir);
5123
+ if (parent === dir) break;
5124
+ dir = parent;
5125
+ }
5126
+ return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5127
+ }
5128
+ async function runPackageManagerInstall(cmd) {
5129
+ return new Promise((resolve) => {
5130
+ const child = (0, import_node_child_process.spawn)(cmd.pm, cmd.args, {
5131
+ cwd: cmd.cwd,
5132
+ // Inherit PATH + HOME so the user's installed managers resolve.
5133
+ env: process.env,
5134
+ // `false` keeps the parent in control of cleanup if the orchestrator
5135
+ // exits before install finishes. Cross-platform-safe.
5136
+ shell: false,
5137
+ stdio: ["ignore", "ignore", "pipe"]
5138
+ });
5139
+ let stderr = "";
5140
+ child.stderr?.on("data", (chunk) => {
5141
+ stderr += chunk.toString("utf8");
5142
+ });
5143
+ child.on("error", (err) => {
5144
+ resolve({
5145
+ pm: cmd.pm,
5146
+ cwd: cmd.cwd,
5147
+ args: cmd.args,
5148
+ exitCode: 127,
5149
+ stderr: stderr + `
5150
+ ${err.message}`
5151
+ });
5152
+ });
5153
+ child.on("close", (code) => {
5154
+ resolve({
5155
+ pm: cmd.pm,
5156
+ cwd: cmd.cwd,
5157
+ args: cmd.args,
5158
+ exitCode: code ?? 1,
5159
+ stderr: stderr.trim()
5160
+ });
5161
+ });
5162
+ });
5163
+ }
5164
+
5165
+ // src/extend/index.ts
5166
+ async function fileExists2(p) {
5167
+ try {
5168
+ await import_node_fs21.promises.access(p);
5169
+ return true;
5170
+ } catch {
5171
+ return false;
5172
+ }
5173
+ }
5174
+ async function readPackageJson(scanPath) {
5175
+ const pkgPath = import_node_path36.default.join(scanPath, "package.json");
5176
+ const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
5177
+ return JSON.parse(raw);
5178
+ }
5179
+ async function findHookFiles(scanPath) {
5180
+ const entries = await import_node_fs21.promises.readdir(scanPath);
5181
+ return entries.filter(
5182
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
5183
+ ).sort();
5184
+ }
5185
+ function extendLogPath() {
5186
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5187
+ }
5188
+ async function appendExtendLog(entry2) {
5189
+ const logPath = extendLogPath();
5190
+ await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
5191
+ await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
5192
+ }
5193
+ function splicedContent(fileContent, snippet2) {
5194
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
5195
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
5196
+ __INSTRUMENTATION_BLOCK__`);
5197
+ }
5198
+ const lines = fileContent.split("\n");
5199
+ let lastPushIdx = -1;
5200
+ for (let i = 0; i < lines.length; i++) {
5201
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
5202
+ }
5203
+ if (lastPushIdx >= 0) {
5204
+ lines.splice(lastPushIdx + 1, 0, snippet2);
5205
+ return lines.join("\n");
5206
+ }
5207
+ for (let i = 0; i < lines.length; i++) {
5208
+ if (lines[i].includes("new NodeSDK(")) {
5209
+ lines.splice(i, 0, snippet2);
5210
+ return lines.join("\n");
5211
+ }
5212
+ }
5213
+ return null;
5214
+ }
5215
+ async function listUninstrumented(ctx) {
5216
+ const pkg = await readPackageJson(ctx.scanPath);
5217
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5218
+ const results = [];
5219
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
5220
+ const entry2 = (0, import_instrumentation_registry.resolve)(library, installedVersion);
5221
+ if (!entry2) continue;
5222
+ if (entry2.coverage === "bundled" || entry2.coverage === "http-only") continue;
5223
+ results.push({
5224
+ library,
5225
+ coverage: entry2.coverage,
5226
+ installedVersion,
5227
+ instrumentation_package: entry2.instrumentation_package,
5228
+ package_version: entry2.package_version,
5229
+ registration: entry2.registration,
5230
+ notes: entry2.notes
5231
+ });
5232
+ }
5233
+ return results;
5234
+ }
5235
+ function lookupInstrumentation(library, installedVersion) {
5236
+ const entry2 = (0, import_instrumentation_registry.resolve)(library, installedVersion);
5237
+ if (!entry2) return null;
5238
+ return {
5239
+ library: entry2.library,
5240
+ coverage: entry2.coverage,
5241
+ instrumentation_package: entry2.instrumentation_package,
5242
+ package_version: entry2.package_version,
5243
+ registration: entry2.registration,
5244
+ notes: entry2.notes
5245
+ };
5246
+ }
5247
+ async function describeProjectInstrumentation(ctx) {
5248
+ const hookFiles = await findHookFiles(ctx.scanPath);
5249
+ const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
5250
+ const registryInstrPackages = new Set(
5251
+ (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5252
+ );
5253
+ const pkg = await readPackageJson(ctx.scanPath);
5254
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5255
+ const installedDeps = {};
5256
+ for (const [key, version] of Object.entries(allDeps)) {
5257
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
5258
+ installedDeps[key] = version;
5259
+ }
5260
+ }
5261
+ return { hookFiles, envNeat, installedDeps };
5262
+ }
5263
+ async function applyExtension(ctx, args, options) {
5264
+ const hookFiles = await findHookFiles(ctx.scanPath);
5265
+ if (hookFiles.length === 0) {
5266
+ throw new Error(
5267
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
5268
+ );
5269
+ }
5270
+ for (const file of hookFiles) {
5271
+ const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5272
+ if (content.includes(args.registration_snippet)) {
5273
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5274
+ }
5275
+ }
5276
+ const primaryFile = hookFiles[0];
5277
+ const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
5278
+ const filesTouched = [];
5279
+ const depsAdded = [];
5280
+ const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5281
+ const pkg = await readPackageJson(ctx.scanPath);
5282
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5283
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
5284
+ await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5285
+ filesTouched.push("package.json");
5286
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
5287
+ }
5288
+ const hookContent = await import_node_fs21.promises.readFile(primaryPath, "utf8");
5289
+ const patched = splicedContent(hookContent, args.registration_snippet);
5290
+ if (!patched) {
5291
+ throw new Error(
5292
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
5293
+ );
5294
+ }
5295
+ await import_node_fs21.promises.writeFile(primaryPath, patched, "utf8");
5296
+ filesTouched.push(primaryFile);
5297
+ const cmd = await detectPackageManager(ctx.scanPath);
5298
+ const installer = options?.runInstall ?? runPackageManagerInstall;
5299
+ const install = await installer(cmd);
5300
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
5301
+ await appendExtendLog({
5302
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5303
+ project: ctx.project,
5304
+ library: args.library,
5305
+ instrumentation_package: args.instrumentation_package,
5306
+ version: args.version,
5307
+ registration_snippet: args.registration_snippet,
5308
+ filesTouched,
5309
+ depsAdded,
5310
+ installOutput
5311
+ });
5312
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
5313
+ }
5314
+ async function dryRunExtension(ctx, args) {
5315
+ const hookFiles = await findHookFiles(ctx.scanPath);
5316
+ if (hookFiles.length === 0) {
5317
+ return {
5318
+ library: args.library,
5319
+ filesTouched: [],
5320
+ depsToAdd: [],
5321
+ packageJsonPatch: {},
5322
+ templatePatch: "No hook files found. Run 'neat init' first."
5323
+ };
5324
+ }
5325
+ for (const file of hookFiles) {
5326
+ const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5327
+ if (content.includes(args.registration_snippet)) {
5328
+ return {
5329
+ library: args.library,
5330
+ filesTouched: [],
5331
+ depsToAdd: [],
5332
+ packageJsonPatch: {},
5333
+ templatePatch: "Already applied \u2014 no changes would be made."
5334
+ };
5335
+ }
5336
+ }
5337
+ const primaryFile = hookFiles[0];
5338
+ const filesTouched = [];
5339
+ const depsToAdd = [];
5340
+ let packageJsonPatch = {};
5341
+ let templatePatch = "";
5342
+ const pkg = await readPackageJson(ctx.scanPath);
5343
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5344
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
5345
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5346
+ filesTouched.push("package.json");
5347
+ }
5348
+ const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
5349
+ const patched = splicedContent(hookContent, args.registration_snippet);
5350
+ if (patched) {
5351
+ filesTouched.push(primaryFile);
5352
+ templatePatch = `+ ${args.registration_snippet}`;
5353
+ } else {
5354
+ templatePatch = "Could not find insertion point in hook file.";
5355
+ }
5356
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
5357
+ }
5358
+ async function rollbackExtension(ctx, args) {
5359
+ const logPath = extendLogPath();
5360
+ if (!await fileExists2(logPath)) {
5361
+ return { undone: false, message: "no apply found for library" };
5362
+ }
5363
+ const raw = await import_node_fs21.promises.readFile(logPath, "utf8");
5364
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5365
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
5366
+ if (!match) {
5367
+ return { undone: false, message: "no apply found for library" };
5368
+ }
5369
+ const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5370
+ if (await fileExists2(pkgPath)) {
5371
+ const pkg = await readPackageJson(ctx.scanPath);
5372
+ if (pkg.dependencies?.[match.instrumentation_package]) {
5373
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
5374
+ pkg.dependencies = rest;
5375
+ await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5376
+ }
5377
+ }
5378
+ const hookFiles = await findHookFiles(ctx.scanPath);
5379
+ for (const file of hookFiles) {
5380
+ const filePath = import_node_path36.default.join(ctx.scanPath, file);
5381
+ const content = await import_node_fs21.promises.readFile(filePath, "utf8");
5382
+ if (content.includes(match.registration_snippet)) {
5383
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
5384
+ await import_node_fs21.promises.writeFile(filePath, filtered, "utf8");
5385
+ break;
5386
+ }
5387
+ }
5388
+ return {
5389
+ undone: true,
5390
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
5391
+ };
5392
+ }
4727
5393
 
4728
5394
  // src/divergences.ts
4729
5395
  init_cjs_shims();
4730
- var import_types21 = require("@neat.is/types");
5396
+ var import_types22 = require("@neat.is/types");
4731
5397
  function bucketKey(source, target, type) {
4732
5398
  return `${type}|${source}|${target}`;
4733
5399
  }
@@ -4735,22 +5401,22 @@ function bucketEdges(graph) {
4735
5401
  const buckets = /* @__PURE__ */ new Map();
4736
5402
  graph.forEachEdge((id, attrs) => {
4737
5403
  const e = attrs;
4738
- const parsed = (0, import_types21.parseEdgeId)(id);
5404
+ const parsed = (0, import_types22.parseEdgeId)(id);
4739
5405
  const provenance = parsed?.provenance ?? e.provenance;
4740
5406
  const key = bucketKey(e.source, e.target, e.type);
4741
5407
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4742
5408
  switch (provenance) {
4743
- case import_types21.Provenance.EXTRACTED:
5409
+ case import_types22.Provenance.EXTRACTED:
4744
5410
  cur.extracted = e;
4745
5411
  break;
4746
- case import_types21.Provenance.OBSERVED:
5412
+ case import_types22.Provenance.OBSERVED:
4747
5413
  cur.observed = e;
4748
5414
  break;
4749
- case import_types21.Provenance.INFERRED:
5415
+ case import_types22.Provenance.INFERRED:
4750
5416
  cur.inferred = e;
4751
5417
  break;
4752
5418
  default:
4753
- if (e.provenance === import_types21.Provenance.STALE) cur.stale = e;
5419
+ if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
4754
5420
  }
4755
5421
  buckets.set(key, cur);
4756
5422
  });
@@ -4759,7 +5425,7 @@ function bucketEdges(graph) {
4759
5425
  function nodeIsFrontier(graph, nodeId) {
4760
5426
  if (!graph.hasNode(nodeId)) return false;
4761
5427
  const attrs = graph.getNodeAttributes(nodeId);
4762
- return attrs.type === import_types21.NodeType.FrontierNode;
5428
+ return attrs.type === import_types22.NodeType.FrontierNode;
4763
5429
  }
4764
5430
  function clampConfidence(n) {
4765
5431
  if (!Number.isFinite(n)) return 0;
@@ -4780,7 +5446,7 @@ function gradedConfidence(edge) {
4780
5446
  }
4781
5447
  function detectMissingDivergences(graph, bucket) {
4782
5448
  const out = [];
4783
- if (bucket.type === import_types21.EdgeType.CONTAINS) return out;
5449
+ if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
4784
5450
  if (bucket.extracted && !bucket.observed) {
4785
5451
  if (!nodeIsFrontier(graph, bucket.target)) {
4786
5452
  out.push({
@@ -4821,7 +5487,7 @@ function declaredHostFor(svc) {
4821
5487
  function hasExtractedConfiguredBy(graph, svcId) {
4822
5488
  for (const edgeId of graph.outboundEdges(svcId)) {
4823
5489
  const e = graph.getEdgeAttributes(edgeId);
4824
- if (e.type === import_types21.EdgeType.CONFIGURED_BY && e.provenance === import_types21.Provenance.EXTRACTED) {
5490
+ if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
4825
5491
  return true;
4826
5492
  }
4827
5493
  }
@@ -4834,10 +5500,10 @@ function detectHostMismatch(graph, svcId, svc) {
4834
5500
  const out = [];
4835
5501
  for (const edgeId of graph.outboundEdges(svcId)) {
4836
5502
  const edge = graph.getEdgeAttributes(edgeId);
4837
- if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4838
- if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
5503
+ if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5504
+ if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
4839
5505
  const target = graph.getNodeAttributes(edge.target);
4840
- if (target.type !== import_types21.NodeType.DatabaseNode) continue;
5506
+ if (target.type !== import_types22.NodeType.DatabaseNode) continue;
4841
5507
  const observedHost = target.host?.trim();
4842
5508
  if (!observedHost) continue;
4843
5509
  if (observedHost === declaredHost) continue;
@@ -4859,10 +5525,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4859
5525
  const deps = svc.dependencies ?? {};
4860
5526
  for (const edgeId of graph.outboundEdges(svcId)) {
4861
5527
  const edge = graph.getEdgeAttributes(edgeId);
4862
- if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4863
- if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
5528
+ if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5529
+ if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
4864
5530
  const target = graph.getNodeAttributes(edge.target);
4865
- if (target.type !== import_types21.NodeType.DatabaseNode) continue;
5531
+ if (target.type !== import_types22.NodeType.DatabaseNode) continue;
4866
5532
  for (const pair of compatPairs()) {
4867
5533
  if (pair.engine !== target.engine) continue;
4868
5534
  const declared = deps[pair.driver];
@@ -4923,7 +5589,7 @@ function computeDivergences(graph, opts = {}) {
4923
5589
  }
4924
5590
  graph.forEachNode((nodeId, attrs) => {
4925
5591
  const n = attrs;
4926
- if (n.type !== import_types21.NodeType.ServiceNode) return;
5592
+ if (n.type !== import_types22.NodeType.ServiceNode) return;
4927
5593
  const svc = n;
4928
5594
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4929
5595
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4956,7 +5622,7 @@ function computeDivergences(graph, opts = {}) {
4956
5622
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4957
5623
  return a.target.localeCompare(b.target);
4958
5624
  });
4959
- return import_types21.DivergenceResultSchema.parse({
5625
+ return import_types22.DivergenceResultSchema.parse({
4960
5626
  divergences: filtered,
4961
5627
  totalAffected: filtered.length,
4962
5628
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4965,7 +5631,7 @@ function computeDivergences(graph, opts = {}) {
4965
5631
 
4966
5632
  // src/diff.ts
4967
5633
  init_cjs_shims();
4968
- var import_node_fs19 = require("fs");
5634
+ var import_node_fs22 = require("fs");
4969
5635
  async function loadSnapshotForDiff(target) {
4970
5636
  if (/^https?:\/\//i.test(target)) {
4971
5637
  const res = await fetch(target);
@@ -4974,7 +5640,7 @@ async function loadSnapshotForDiff(target) {
4974
5640
  }
4975
5641
  return await res.json();
4976
5642
  }
4977
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
5643
+ const raw = await import_node_fs22.promises.readFile(target, "utf8");
4978
5644
  return JSON.parse(raw);
4979
5645
  }
4980
5646
  function indexEntries(entries) {
@@ -5042,25 +5708,25 @@ function canonicalJson(value) {
5042
5708
 
5043
5709
  // src/registry.ts
5044
5710
  init_cjs_shims();
5045
- var import_node_fs20 = require("fs");
5046
- var import_node_os2 = __toESM(require("os"), 1);
5047
- var import_node_path33 = __toESM(require("path"), 1);
5048
- var import_types22 = require("@neat.is/types");
5711
+ var import_node_fs23 = require("fs");
5712
+ var import_node_os3 = __toESM(require("os"), 1);
5713
+ var import_node_path37 = __toESM(require("path"), 1);
5714
+ var import_types23 = require("@neat.is/types");
5049
5715
  var LOCK_TIMEOUT_MS = 5e3;
5050
5716
  var LOCK_RETRY_MS = 50;
5051
5717
  function neatHome() {
5052
5718
  const override = process.env.NEAT_HOME;
5053
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
5054
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
5719
+ if (override && override.length > 0) return import_node_path37.default.resolve(override);
5720
+ return import_node_path37.default.join(import_node_os3.default.homedir(), ".neat");
5055
5721
  }
5056
5722
  function registryPath() {
5057
- return import_node_path33.default.join(neatHome(), "projects.json");
5723
+ return import_node_path37.default.join(neatHome(), "projects.json");
5058
5724
  }
5059
5725
  function registryLockPath() {
5060
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
5726
+ return import_node_path37.default.join(neatHome(), "projects.json.lock");
5061
5727
  }
5062
5728
  function daemonPidPath() {
5063
- return import_node_path33.default.join(neatHome(), "neatd.pid");
5729
+ return import_node_path37.default.join(neatHome(), "neatd.pid");
5064
5730
  }
5065
5731
  function isPidAliveDefault(pid) {
5066
5732
  try {
@@ -5072,7 +5738,7 @@ function isPidAliveDefault(pid) {
5072
5738
  }
5073
5739
  async function readPidFile(file) {
5074
5740
  try {
5075
- const raw = await import_node_fs20.promises.readFile(file, "utf8");
5741
+ const raw = await import_node_fs23.promises.readFile(file, "utf8");
5076
5742
  const pid = Number.parseInt(raw.trim(), 10);
5077
5743
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5078
5744
  } catch {
@@ -5120,24 +5786,24 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5120
5786
  }
5121
5787
  }
5122
5788
  async function writeAtomically(target, contents) {
5123
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
5789
+ await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
5124
5790
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5125
- const fd = await import_node_fs20.promises.open(tmp, "w");
5791
+ const fd = await import_node_fs23.promises.open(tmp, "w");
5126
5792
  try {
5127
5793
  await fd.writeFile(contents, "utf8");
5128
5794
  await fd.sync();
5129
5795
  } finally {
5130
5796
  await fd.close();
5131
5797
  }
5132
- await import_node_fs20.promises.rename(tmp, target);
5798
+ await import_node_fs23.promises.rename(tmp, target);
5133
5799
  }
5134
5800
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5135
5801
  const deadline = Date.now() + timeoutMs;
5136
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
5802
+ await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(lockPath), { recursive: true });
5137
5803
  let probedHolder = false;
5138
5804
  while (true) {
5139
5805
  try {
5140
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
5806
+ const fd = await import_node_fs23.promises.open(lockPath, "wx");
5141
5807
  try {
5142
5808
  await fd.writeFile(`${process.pid}
5143
5809
  `, "utf8");
@@ -5162,7 +5828,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
5162
5828
  }
5163
5829
  }
5164
5830
  async function releaseLock(lockPath) {
5165
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
5831
+ await import_node_fs23.promises.unlink(lockPath).catch(() => {
5166
5832
  });
5167
5833
  }
5168
5834
  async function withLock(fn) {
@@ -5178,7 +5844,7 @@ async function readRegistry() {
5178
5844
  const file = registryPath();
5179
5845
  let raw;
5180
5846
  try {
5181
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5847
+ raw = await import_node_fs23.promises.readFile(file, "utf8");
5182
5848
  } catch (err) {
5183
5849
  if (err.code === "ENOENT") {
5184
5850
  return { version: 1, projects: [] };
@@ -5186,10 +5852,10 @@ async function readRegistry() {
5186
5852
  throw err;
5187
5853
  }
5188
5854
  const parsed = JSON.parse(raw);
5189
- return import_types22.RegistryFileSchema.parse(parsed);
5855
+ return import_types23.RegistryFileSchema.parse(parsed);
5190
5856
  }
5191
5857
  async function writeRegistry(reg) {
5192
- const validated = import_types22.RegistryFileSchema.parse(reg);
5858
+ const validated = import_types23.RegistryFileSchema.parse(reg);
5193
5859
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5194
5860
  }
5195
5861
  async function getProject(name) {
@@ -5411,11 +6077,11 @@ function registerRoutes(scope, ctx) {
5411
6077
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5412
6078
  const parsed = [];
5413
6079
  for (const c of candidates) {
5414
- const r = import_types23.DivergenceTypeSchema.safeParse(c);
6080
+ const r = import_types24.DivergenceTypeSchema.safeParse(c);
5415
6081
  if (!r.success) {
5416
6082
  return reply.code(400).send({
5417
6083
  error: `unknown divergence type "${c}"`,
5418
- allowed: import_types23.DivergenceTypeSchema.options
6084
+ allowed: import_types24.DivergenceTypeSchema.options
5419
6085
  });
5420
6086
  }
5421
6087
  parsed.push(r.data);
@@ -5628,7 +6294,7 @@ function registerRoutes(scope, ctx) {
5628
6294
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5629
6295
  let violations = await log.readAll();
5630
6296
  if (req2.query.severity) {
5631
- const sev = import_types23.PolicySeveritySchema.safeParse(req2.query.severity);
6297
+ const sev = import_types24.PolicySeveritySchema.safeParse(req2.query.severity);
5632
6298
  if (!sev.success) {
5633
6299
  return reply.code(400).send({
5634
6300
  error: "invalid severity",
@@ -5645,7 +6311,7 @@ function registerRoutes(scope, ctx) {
5645
6311
  scope.post("/policies/check", async (req2, reply) => {
5646
6312
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
5647
6313
  if (!proj) return;
5648
- const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
6314
+ const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
5649
6315
  if (!parsed.success) {
5650
6316
  return reply.code(400).send({
5651
6317
  error: "invalid /policies/check body",
@@ -5678,6 +6344,105 @@ function registerRoutes(scope, ctx) {
5678
6344
  violations
5679
6345
  };
5680
6346
  });
6347
+ scope.get("/extend/list-uninstrumented", async (req2, reply) => {
6348
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6349
+ if (!proj) return;
6350
+ if (!proj.scanPath) {
6351
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6352
+ }
6353
+ try {
6354
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
6355
+ return { libraries: results };
6356
+ } catch (err) {
6357
+ return reply.code(500).send({ error: err.message });
6358
+ }
6359
+ });
6360
+ scope.get("/extend/lookup", async (req2, reply) => {
6361
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6362
+ if (!proj) return;
6363
+ const { library, version } = req2.query;
6364
+ if (!library) {
6365
+ return reply.code(400).send({ error: "query parameter `library` is required" });
6366
+ }
6367
+ const result = lookupInstrumentation(library, version);
6368
+ if (!result) {
6369
+ return reply.code(404).send({ error: "library not found in registry", library });
6370
+ }
6371
+ return result;
6372
+ });
6373
+ scope.get("/extend/describe", async (req2, reply) => {
6374
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6375
+ if (!proj) return;
6376
+ if (!proj.scanPath) {
6377
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6378
+ }
6379
+ try {
6380
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
6381
+ return state;
6382
+ } catch (err) {
6383
+ return reply.code(500).send({ error: err.message });
6384
+ }
6385
+ });
6386
+ scope.post("/extend/apply", async (req2, reply) => {
6387
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6388
+ if (!proj) return;
6389
+ if (!proj.scanPath) {
6390
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6391
+ }
6392
+ const { library, instrumentation_package, version, registration_snippet } = req2.body ?? {};
6393
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6394
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6395
+ }
6396
+ try {
6397
+ const result = await applyExtension(
6398
+ { project: proj.name, scanPath: proj.scanPath },
6399
+ { library, instrumentation_package, version, registration_snippet }
6400
+ );
6401
+ return result;
6402
+ } catch (err) {
6403
+ return reply.code(500).send({ error: err.message });
6404
+ }
6405
+ });
6406
+ scope.post("/extend/dry-run", async (req2, reply) => {
6407
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6408
+ if (!proj) return;
6409
+ if (!proj.scanPath) {
6410
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6411
+ }
6412
+ const { library, instrumentation_package, version, registration_snippet } = req2.body ?? {};
6413
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6414
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6415
+ }
6416
+ try {
6417
+ const result = await dryRunExtension(
6418
+ { project: proj.name, scanPath: proj.scanPath },
6419
+ { library, instrumentation_package, version, registration_snippet }
6420
+ );
6421
+ return result;
6422
+ } catch (err) {
6423
+ return reply.code(500).send({ error: err.message });
6424
+ }
6425
+ });
6426
+ scope.post("/extend/rollback", async (req2, reply) => {
6427
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6428
+ if (!proj) return;
6429
+ if (!proj.scanPath) {
6430
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6431
+ }
6432
+ const { library } = req2.body ?? {};
6433
+ if (!library) {
6434
+ return reply.code(400).send({ error: "body must include library" });
6435
+ }
6436
+ try {
6437
+ const result = await rollbackExtension(
6438
+ { project: proj.name, scanPath: proj.scanPath },
6439
+ { library }
6440
+ );
6441
+ return result;
6442
+ } catch (err) {
6443
+ return reply.code(500).send({ error: err.message });
6444
+ }
6445
+ });
5681
6446
  }
5682
6447
  async function buildApi(opts) {
5683
6448
  const app = (0, import_fastify.default)({ logger: false });
@@ -5788,8 +6553,8 @@ init_auth();
5788
6553
 
5789
6554
  // src/unrouted.ts
5790
6555
  init_cjs_shims();
5791
- var import_node_fs21 = require("fs");
5792
- var import_node_path36 = __toESM(require("path"), 1);
6556
+ var import_node_fs24 = require("fs");
6557
+ var import_node_path40 = __toESM(require("path"), 1);
5793
6558
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
5794
6559
  return {
5795
6560
  timestamp: now.toISOString(),
@@ -5799,21 +6564,21 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
5799
6564
  };
5800
6565
  }
5801
6566
  async function appendUnroutedSpan(neatHome3, record) {
5802
- const target = import_node_path36.default.join(neatHome3, "errors.ndjson");
5803
- await import_node_fs21.promises.mkdir(neatHome3, { recursive: true });
5804
- await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6567
+ const target = import_node_path40.default.join(neatHome3, "errors.ndjson");
6568
+ await import_node_fs24.promises.mkdir(neatHome3, { recursive: true });
6569
+ await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
5805
6570
  }
5806
6571
  function unroutedErrorsPath(neatHome3) {
5807
- return import_node_path36.default.join(neatHome3, "errors.ndjson");
6572
+ return import_node_path40.default.join(neatHome3, "errors.ndjson");
5808
6573
  }
5809
6574
 
5810
6575
  // src/daemon.ts
5811
6576
  function neatHomeFor(opts) {
5812
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
6577
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path41.default.resolve(opts.neatHome);
5813
6578
  const env = process.env.NEAT_HOME;
5814
- if (env && env.length > 0) return import_node_path37.default.resolve(env);
6579
+ if (env && env.length > 0) return import_node_path41.default.resolve(env);
5815
6580
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5816
- return import_node_path37.default.join(home, ".neat");
6581
+ return import_node_path41.default.join(home, ".neat");
5817
6582
  }
5818
6583
  function routeSpanToProject(serviceName, projects) {
5819
6584
  if (!serviceName) return DEFAULT_PROJECT;
@@ -5848,9 +6613,9 @@ function isTokenContained(needle, haystack) {
5848
6613
  return tokens.includes(needle);
5849
6614
  }
5850
6615
  async function bootstrapProject(entry2) {
5851
- const paths = pathsForProject(entry2.name, import_node_path37.default.join(entry2.path, "neat-out"));
6616
+ const paths = pathsForProject(entry2.name, import_node_path41.default.join(entry2.path, "neat-out"));
5852
6617
  try {
5853
- const stat = await import_node_fs22.promises.stat(entry2.path);
6618
+ const stat = await import_node_fs25.promises.stat(entry2.path);
5854
6619
  if (!stat.isDirectory()) {
5855
6620
  throw new Error(`registered path ${entry2.path} is not a directory`);
5856
6621
  }
@@ -5916,13 +6681,13 @@ async function startDaemon(opts = {}) {
5916
6681
  const home = neatHomeFor(opts);
5917
6682
  const regPath = registryPath();
5918
6683
  try {
5919
- await import_node_fs22.promises.access(regPath);
6684
+ await import_node_fs25.promises.access(regPath);
5920
6685
  } catch {
5921
6686
  throw new Error(
5922
6687
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
5923
6688
  );
5924
6689
  }
5925
- const pidPath = import_node_path37.default.join(home, "neatd.pid");
6690
+ const pidPath = import_node_path41.default.join(home, "neatd.pid");
5926
6691
  await writeAtomically(pidPath, `${process.pid}
5927
6692
  `);
5928
6693
  const slots = /* @__PURE__ */ new Map();
@@ -6080,7 +6845,7 @@ async function startDaemon(opts = {}) {
6080
6845
  }
6081
6846
  if (restApp) await restApp.close().catch(() => {
6082
6847
  });
6083
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6848
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
6084
6849
  });
6085
6850
  throw new Error(
6086
6851
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -6186,7 +6951,7 @@ async function startDaemon(opts = {}) {
6186
6951
  });
6187
6952
  if (otlpApp) await otlpApp.close().catch(() => {
6188
6953
  });
6189
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6954
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
6190
6955
  });
6191
6956
  throw new Error(
6192
6957
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -6232,9 +6997,9 @@ async function startDaemon(opts = {}) {
6232
6997
  let registryWatcher = null;
6233
6998
  let reloadTimer = null;
6234
6999
  try {
6235
- const regDir = import_node_path37.default.dirname(regPath);
6236
- const regBase = import_node_path37.default.basename(regPath);
6237
- registryWatcher = (0, import_node_fs22.watch)(regDir, (_eventType, filename) => {
7000
+ const regDir = import_node_path41.default.dirname(regPath);
7001
+ const regBase = import_node_path41.default.basename(regPath);
7002
+ registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
6238
7003
  if (filename !== null && filename !== regBase) return;
6239
7004
  if (reloadTimer) clearTimeout(reloadTimer);
6240
7005
  reloadTimer = setTimeout(() => {
@@ -6277,7 +7042,7 @@ async function startDaemon(opts = {}) {
6277
7042
  } catch {
6278
7043
  }
6279
7044
  }
6280
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
7045
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
6281
7046
  });
6282
7047
  };
6283
7048
  return {
@@ -6297,9 +7062,9 @@ init_auth();
6297
7062
 
6298
7063
  // src/web-spawn.ts
6299
7064
  init_cjs_shims();
6300
- var import_node_child_process = require("child_process");
7065
+ var import_node_child_process2 = require("child_process");
6301
7066
  var import_node_net = __toESM(require("net"), 1);
6302
- var import_node_path38 = __toESM(require("path"), 1);
7067
+ var import_node_path42 = __toESM(require("path"), 1);
6303
7068
  var DEFAULT_WEB_PORT = 6328;
6304
7069
  async function assertPortFree(port) {
6305
7070
  await new Promise((resolve, reject) => {
@@ -6327,10 +7092,10 @@ function resolveWebPackageDir() {
6327
7092
  eval("require")
6328
7093
  );
6329
7094
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
6330
- return import_node_path38.default.dirname(pkgJsonPath);
7095
+ return import_node_path42.default.dirname(pkgJsonPath);
6331
7096
  }
6332
7097
  function resolveStandaloneServerEntry(webDir) {
6333
- return import_node_path38.default.join(webDir, ".next/standalone/packages/web/server.js");
7098
+ return import_node_path42.default.join(webDir, ".next/standalone/packages/web/server.js");
6334
7099
  }
6335
7100
  async function spawnWebUI(restPort) {
6336
7101
  const portRaw = process.env.NEAT_WEB_PORT;
@@ -6354,8 +7119,8 @@ async function spawnWebUI(restPort) {
6354
7119
  HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0",
6355
7120
  NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
6356
7121
  };
6357
- const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
6358
- cwd: import_node_path38.default.dirname(serverEntry),
7122
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
7123
+ cwd: import_node_path42.default.dirname(serverEntry),
6359
7124
  env,
6360
7125
  stdio: ["ignore", "inherit", "inherit"],
6361
7126
  detached: false
@@ -6468,14 +7233,14 @@ function localVersion() {
6468
7233
  }
6469
7234
  function neatHome2() {
6470
7235
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
6471
- return import_node_path39.default.resolve(process.env.NEAT_HOME);
7236
+ return import_node_path43.default.resolve(process.env.NEAT_HOME);
6472
7237
  }
6473
7238
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6474
- return import_node_path39.default.join(home, ".neat");
7239
+ return import_node_path43.default.join(home, ".neat");
6475
7240
  }
6476
7241
  async function readPid() {
6477
7242
  try {
6478
- const raw = await import_node_fs23.promises.readFile(import_node_path39.default.join(neatHome2(), "neatd.pid"), "utf8");
7243
+ const raw = await import_node_fs26.promises.readFile(import_node_path43.default.join(neatHome2(), "neatd.pid"), "utf8");
6479
7244
  const n = Number.parseInt(raw.trim(), 10);
6480
7245
  return Number.isFinite(n) ? n : null;
6481
7246
  } catch {