@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/index.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", (req, reply, done) => {
60
- const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path42 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path38 === suffix || path38.endsWith(suffix)) {
62
+ if (path42 === suffix || path42.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -185,8 +185,8 @@ function reshapeGrpcRequest(req) {
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 }
@@ -574,12 +574,12 @@ function logSpanHandler(span) {
574
574
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
575
575
  );
576
576
  }
577
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
577
+ var import_node_path39, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
578
578
  var init_otel = __esm({
579
579
  "src/otel.ts"() {
580
580
  "use strict";
581
581
  init_cjs_shims();
582
- import_node_path35 = __toESM(require("path"), 1);
582
+ import_node_path39 = __toESM(require("path"), 1);
583
583
  import_node_url2 = require("url");
584
584
  import_fastify2 = __toESM(require("fastify"), 1);
585
585
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -1117,19 +1117,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1117
1117
  function longestIncomingWalk(graph, start, maxDepth) {
1118
1118
  let best = { path: [start], edges: [] };
1119
1119
  const visited = /* @__PURE__ */ new Set([start]);
1120
- function step(node, path38, edges) {
1121
- if (path38.length > best.path.length) {
1122
- best = { path: [...path38], edges: [...edges] };
1120
+ function step(node, path42, edges) {
1121
+ if (path42.length > best.path.length) {
1122
+ best = { path: [...path42], edges: [...edges] };
1123
1123
  }
1124
- if (path38.length - 1 >= maxDepth) return;
1124
+ if (path42.length - 1 >= maxDepth) return;
1125
1125
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1126
1126
  for (const [srcId, edge] of incoming) {
1127
1127
  if (visited.has(srcId)) continue;
1128
1128
  visited.add(srcId);
1129
- path38.push(srcId);
1129
+ path42.push(srcId);
1130
1130
  edges.push(edge);
1131
- step(srcId, path38, edges);
1132
- path38.pop();
1131
+ step(srcId, path42, edges);
1132
+ path42.pop();
1133
1133
  edges.pop();
1134
1134
  visited.delete(srcId);
1135
1135
  }
@@ -1961,7 +1961,7 @@ function ensureFrontierNode(graph, host, ts) {
1961
1961
  graph.addNode(id, node);
1962
1962
  return id;
1963
1963
  }
1964
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1964
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1965
1965
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1966
1966
  const id = makeObservedEdgeId(type, source, target);
1967
1967
  if (graph.hasEdge(id)) {
@@ -1998,7 +1998,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1998
1998
  confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1999
1999
  lastObserved: ts,
2000
2000
  callCount: 1,
2001
- signal
2001
+ signal,
2002
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
2003
+ // Only set when code.filepath was present on the span — never fabricated.
2004
+ ...evidence ? { evidence } : {}
2002
2005
  };
2003
2006
  graph.addEdgeWithKey(id, source, target, edge);
2004
2007
  return { edge, created: true };
@@ -2092,6 +2095,7 @@ async function handleSpan(ctx, span) {
2092
2095
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2093
2096
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2094
2097
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2098
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2095
2099
  let affectedNode = sourceId;
2096
2100
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2097
2101
  if (span.dbSystem) {
@@ -2105,7 +2109,8 @@ async function handleSpan(ctx, span) {
2105
2109
  observedSource(),
2106
2110
  targetId,
2107
2111
  ts,
2108
- isError
2112
+ isError,
2113
+ callSiteEvidence
2109
2114
  );
2110
2115
  if (result) affectedNode = targetId;
2111
2116
  }
@@ -2121,7 +2126,8 @@ async function handleSpan(ctx, span) {
2121
2126
  observedSource(),
2122
2127
  targetId,
2123
2128
  ts,
2124
- isError
2129
+ isError,
2130
+ callSiteEvidence
2125
2131
  );
2126
2132
  affectedNode = targetId;
2127
2133
  resolvedViaAddress = true;
@@ -2133,7 +2139,8 @@ async function handleSpan(ctx, span) {
2133
2139
  observedSource(),
2134
2140
  frontierNodeId,
2135
2141
  ts,
2136
- isError
2142
+ isError,
2143
+ callSiteEvidence
2137
2144
  );
2138
2145
  affectedNode = frontierNodeId;
2139
2146
  resolvedViaAddress = true;
@@ -3082,16 +3089,433 @@ async function addServiceAliases(graph, scanPath, services) {
3082
3089
  await collectK8sAliases(graph, scanPath, byName);
3083
3090
  }
3084
3091
 
3085
- // src/extract/databases/index.ts
3092
+ // src/extract/files.ts
3086
3093
  init_cjs_shims();
3087
- var import_node_path17 = __toESM(require("path"), 1);
3094
+
3095
+ // src/extract/calls/shared.ts
3096
+ init_cjs_shims();
3097
+ var import_node_fs10 = require("fs");
3098
+ var import_node_path10 = __toESM(require("path"), 1);
3088
3099
  var import_types7 = require("@neat.is/types");
3100
+ async function walkSourceFiles(dir) {
3101
+ const out = [];
3102
+ async function walk(current) {
3103
+ const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3104
+ for (const entry of entries) {
3105
+ const full = import_node_path10.default.join(current, entry.name);
3106
+ if (entry.isDirectory()) {
3107
+ if (IGNORED_DIRS.has(entry.name)) continue;
3108
+ if (await isPythonVenvDir(full)) continue;
3109
+ await walk(full);
3110
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name))) {
3111
+ out.push(full);
3112
+ }
3113
+ }
3114
+ }
3115
+ await walk(dir);
3116
+ return out;
3117
+ }
3118
+ async function loadSourceFiles(dir) {
3119
+ const paths = await walkSourceFiles(dir);
3120
+ const out = [];
3121
+ for (const p of paths) {
3122
+ try {
3123
+ const content = await import_node_fs10.promises.readFile(p, "utf8");
3124
+ out.push({ path: p, content });
3125
+ } catch {
3126
+ }
3127
+ }
3128
+ return out;
3129
+ }
3130
+ function lineOf(text, needle) {
3131
+ const idx = text.indexOf(needle);
3132
+ if (idx < 0) return 1;
3133
+ return text.slice(0, idx).split("\n").length;
3134
+ }
3135
+ function snippet(text, line) {
3136
+ const lines = text.split("\n");
3137
+ return (lines[line - 1] ?? "").trim();
3138
+ }
3139
+ function toPosix2(p) {
3140
+ return p.split("\\").join("/");
3141
+ }
3142
+ function languageForPath(relPath) {
3143
+ switch (import_node_path10.default.extname(relPath).toLowerCase()) {
3144
+ case ".py":
3145
+ return "python";
3146
+ case ".ts":
3147
+ case ".tsx":
3148
+ return "typescript";
3149
+ case ".js":
3150
+ case ".jsx":
3151
+ case ".mjs":
3152
+ case ".cjs":
3153
+ return "javascript";
3154
+ default:
3155
+ return void 0;
3156
+ }
3157
+ }
3158
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3159
+ let nodesAdded = 0;
3160
+ let edgesAdded = 0;
3161
+ const fileNodeId = (0, import_types7.fileId)(serviceName, relPath);
3162
+ if (!graph.hasNode(fileNodeId)) {
3163
+ const language = languageForPath(relPath);
3164
+ const node = {
3165
+ id: fileNodeId,
3166
+ type: import_types7.NodeType.FileNode,
3167
+ service: serviceName,
3168
+ path: relPath,
3169
+ ...language ? { language } : {},
3170
+ discoveredVia: "static"
3171
+ };
3172
+ graph.addNode(fileNodeId, node);
3173
+ nodesAdded++;
3174
+ }
3175
+ const containsId = (0, import_types7.extractedEdgeId)(serviceNodeId, fileNodeId, import_types7.EdgeType.CONTAINS);
3176
+ if (!graph.hasEdge(containsId)) {
3177
+ const edge = {
3178
+ id: containsId,
3179
+ source: serviceNodeId,
3180
+ target: fileNodeId,
3181
+ type: import_types7.EdgeType.CONTAINS,
3182
+ provenance: import_types7.Provenance.EXTRACTED,
3183
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3184
+ evidence: { file: relPath }
3185
+ };
3186
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3187
+ edgesAdded++;
3188
+ }
3189
+ return { fileNodeId, nodesAdded, edgesAdded };
3190
+ }
3191
+
3192
+ // src/extract/files.ts
3193
+ var import_node_path11 = __toESM(require("path"), 1);
3194
+ async function addFiles(graph, services) {
3195
+ let nodesAdded = 0;
3196
+ let edgesAdded = 0;
3197
+ for (const service of services) {
3198
+ const filePaths = await walkSourceFiles(service.dir);
3199
+ for (const filePath of filePaths) {
3200
+ const relPath = toPosix2(import_node_path11.default.relative(service.dir, filePath));
3201
+ const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
3202
+ graph,
3203
+ service.pkg.name,
3204
+ service.node.id,
3205
+ relPath
3206
+ );
3207
+ nodesAdded += n;
3208
+ edgesAdded += e;
3209
+ }
3210
+ }
3211
+ return { nodesAdded, edgesAdded };
3212
+ }
3213
+
3214
+ // src/extract/imports.ts
3215
+ init_cjs_shims();
3216
+ var import_node_path12 = __toESM(require("path"), 1);
3217
+ var import_node_fs11 = require("fs");
3218
+ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3219
+ var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3220
+ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3221
+ var import_types8 = require("@neat.is/types");
3222
+ var PARSE_CHUNK = 16384;
3223
+ function parseSource(parser, source) {
3224
+ return parser.parse(
3225
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3226
+ );
3227
+ }
3228
+ function makeJsParser() {
3229
+ const p = new import_tree_sitter.default();
3230
+ p.setLanguage(import_tree_sitter_javascript.default);
3231
+ return p;
3232
+ }
3233
+ function makePyParser() {
3234
+ const p = new import_tree_sitter.default();
3235
+ p.setLanguage(import_tree_sitter_python.default);
3236
+ return p;
3237
+ }
3238
+ function stringLiteralText(node) {
3239
+ for (let i = 0; i < node.childCount; i++) {
3240
+ const child = node.child(i);
3241
+ if (child?.type === "string_fragment") return child.text;
3242
+ }
3243
+ const raw = node.text;
3244
+ if (raw.length >= 2) return raw.slice(1, -1);
3245
+ return raw.length === 0 ? null : "";
3246
+ }
3247
+ function clipSnippet(text) {
3248
+ const oneLine = text.split("\n")[0] ?? text;
3249
+ return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
3250
+ }
3251
+ function collectJsImports(node, out) {
3252
+ if (node.type === "import_statement") {
3253
+ const source = node.childForFieldName("source");
3254
+ if (source) {
3255
+ const specifier = stringLiteralText(source);
3256
+ if (specifier) {
3257
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3258
+ }
3259
+ }
3260
+ return;
3261
+ }
3262
+ if (node.type === "call_expression") {
3263
+ const fn = node.childForFieldName("function");
3264
+ if (fn?.type === "identifier" && fn.text === "require") {
3265
+ const args = node.childForFieldName("arguments");
3266
+ const firstArg = args?.namedChild(0);
3267
+ if (firstArg?.type === "string") {
3268
+ const specifier = stringLiteralText(firstArg);
3269
+ if (specifier) {
3270
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3271
+ }
3272
+ }
3273
+ }
3274
+ }
3275
+ for (let i = 0; i < node.namedChildCount; i++) {
3276
+ const child = node.namedChild(i);
3277
+ if (child) collectJsImports(child, out);
3278
+ }
3279
+ }
3280
+ function collectPyImports(node, out) {
3281
+ if (node.type === "import_from_statement") {
3282
+ let level = 0;
3283
+ let modulePath = "";
3284
+ let pastFrom = false;
3285
+ for (let i = 0; i < node.childCount; i++) {
3286
+ const child = node.child(i);
3287
+ if (!child) continue;
3288
+ if (!pastFrom) {
3289
+ if (child.type === "from") pastFrom = true;
3290
+ continue;
3291
+ }
3292
+ if (child.type === "import") break;
3293
+ if (child.type === "relative_import") {
3294
+ for (let j = 0; j < child.childCount; j++) {
3295
+ const rc = child.child(j);
3296
+ if (!rc) continue;
3297
+ if (rc.type === "import_prefix") {
3298
+ for (let k = 0; k < rc.childCount; k++) {
3299
+ if (rc.child(k)?.type === ".") level++;
3300
+ }
3301
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3302
+ }
3303
+ break;
3304
+ }
3305
+ if (child.type === "dotted_name") {
3306
+ modulePath = child.text;
3307
+ break;
3308
+ }
3309
+ }
3310
+ if (level > 0 || modulePath) {
3311
+ out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3312
+ }
3313
+ }
3314
+ for (let i = 0; i < node.namedChildCount; i++) {
3315
+ const child = node.namedChild(i);
3316
+ if (child) collectPyImports(child, out);
3317
+ }
3318
+ }
3319
+ async function fileExists(p) {
3320
+ try {
3321
+ await import_node_fs11.promises.access(p);
3322
+ return true;
3323
+ } catch {
3324
+ return false;
3325
+ }
3326
+ }
3327
+ function isWithinServiceDir(candidate, serviceDir) {
3328
+ const rel = import_node_path12.default.relative(serviceDir, candidate);
3329
+ return rel !== "" && !rel.startsWith("..") && !import_node_path12.default.isAbsolute(rel);
3330
+ }
3331
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
3332
+ var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
3333
+ async function firstExistingCandidate(base, serviceDir) {
3334
+ for (const ext of JS_EXTENSIONS) {
3335
+ const candidate = base + ext;
3336
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3337
+ return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
3338
+ }
3339
+ }
3340
+ for (const indexFile of JS_INDEX_FILES) {
3341
+ const candidate = import_node_path12.default.join(base, indexFile);
3342
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3343
+ return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
3344
+ }
3345
+ }
3346
+ return null;
3347
+ }
3348
+ async function loadTsPathConfig(serviceDir) {
3349
+ const tsconfigPath = import_node_path12.default.join(serviceDir, "tsconfig.json");
3350
+ let raw;
3351
+ try {
3352
+ raw = await import_node_fs11.promises.readFile(tsconfigPath, "utf8");
3353
+ } catch {
3354
+ return null;
3355
+ }
3356
+ try {
3357
+ const parsed = JSON.parse(raw);
3358
+ const paths = parsed.compilerOptions?.paths;
3359
+ if (!paths || Object.keys(paths).length === 0) return null;
3360
+ const baseUrl = parsed.compilerOptions?.baseUrl;
3361
+ return { paths, baseDir: baseUrl ? import_node_path12.default.resolve(serviceDir, baseUrl) : serviceDir };
3362
+ } catch (err) {
3363
+ recordExtractionError("import alias resolution", tsconfigPath, err);
3364
+ return null;
3365
+ }
3366
+ }
3367
+ async function resolveTsAlias(specifier, config, serviceDir) {
3368
+ for (const [pattern, targets] of Object.entries(config.paths)) {
3369
+ let suffix = null;
3370
+ if (pattern === specifier) {
3371
+ suffix = "";
3372
+ } else if (pattern.endsWith("/*")) {
3373
+ const prefix = pattern.slice(0, -1);
3374
+ if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
3375
+ }
3376
+ if (suffix === null) continue;
3377
+ for (const target of targets) {
3378
+ const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
3379
+ const resolvedBase = import_node_path12.default.resolve(config.baseDir, targetBase, suffix);
3380
+ const hit = await firstExistingCandidate(resolvedBase, serviceDir);
3381
+ if (hit) return hit;
3382
+ if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
3383
+ return toPosix2(import_node_path12.default.relative(serviceDir, resolvedBase));
3384
+ }
3385
+ }
3386
+ }
3387
+ return null;
3388
+ }
3389
+ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3390
+ if (!specifier) return null;
3391
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
3392
+ const base = import_node_path12.default.resolve(importerDir, specifier);
3393
+ const ext = import_node_path12.default.extname(specifier);
3394
+ if (ext) {
3395
+ if (ext === ".js" || ext === ".jsx") {
3396
+ const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
3397
+ const tsSibling = base.slice(0, -ext.length) + tsExt;
3398
+ if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
3399
+ return toPosix2(import_node_path12.default.relative(serviceDir, tsSibling));
3400
+ }
3401
+ }
3402
+ if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
3403
+ return toPosix2(import_node_path12.default.relative(serviceDir, base));
3404
+ }
3405
+ return null;
3406
+ }
3407
+ return firstExistingCandidate(base, serviceDir);
3408
+ }
3409
+ if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
3410
+ return null;
3411
+ }
3412
+ async function resolvePyImport(imp, importerPath, serviceDir) {
3413
+ if (!imp.modulePath) return null;
3414
+ const relPath = imp.modulePath.split(".").join("/");
3415
+ let baseDir;
3416
+ if (imp.level > 0) {
3417
+ baseDir = import_node_path12.default.dirname(importerPath);
3418
+ for (let i = 1; i < imp.level; i++) baseDir = import_node_path12.default.dirname(baseDir);
3419
+ } else {
3420
+ baseDir = serviceDir;
3421
+ }
3422
+ const candidates = [import_node_path12.default.join(baseDir, `${relPath}.py`), import_node_path12.default.join(baseDir, relPath, "__init__.py")];
3423
+ for (const candidate of candidates) {
3424
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3425
+ return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
3426
+ }
3427
+ }
3428
+ return null;
3429
+ }
3430
+ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3431
+ const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
3432
+ if (!graph.hasNode(importeeFileId)) return 0;
3433
+ const edgeId = (0, import_types8.extractedEdgeId)(importerFileId, importeeFileId, import_types8.EdgeType.IMPORTS);
3434
+ if (graph.hasEdge(edgeId)) return 0;
3435
+ const edge = {
3436
+ id: edgeId,
3437
+ source: importerFileId,
3438
+ target: importeeFileId,
3439
+ type: import_types8.EdgeType.IMPORTS,
3440
+ provenance: import_types8.Provenance.EXTRACTED,
3441
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3442
+ evidence: { file: importerRelPath, line, snippet: snippet2 }
3443
+ };
3444
+ graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
3445
+ return 1;
3446
+ }
3447
+ async function addImports(graph, services) {
3448
+ const jsParser = makeJsParser();
3449
+ const pyParser = makePyParser();
3450
+ let edgesAdded = 0;
3451
+ for (const service of services) {
3452
+ const tsPaths = await loadTsPathConfig(service.dir);
3453
+ const files = await loadSourceFiles(service.dir);
3454
+ for (const file of files) {
3455
+ if (isTestPath(file.path)) continue;
3456
+ const relFile = toPosix2(import_node_path12.default.relative(service.dir, file.path));
3457
+ const importerFileId = (0, import_types8.fileId)(service.pkg.name, relFile);
3458
+ const isPython = import_node_path12.default.extname(file.path) === ".py";
3459
+ if (isPython) {
3460
+ let pyImports = [];
3461
+ try {
3462
+ const tree = parseSource(pyParser, file.content);
3463
+ collectPyImports(tree.rootNode, pyImports);
3464
+ } catch (err) {
3465
+ recordExtractionError("import extraction", file.path, err);
3466
+ continue;
3467
+ }
3468
+ for (const imp of pyImports) {
3469
+ const resolved = await resolvePyImport(imp, file.path, service.dir);
3470
+ if (!resolved) continue;
3471
+ edgesAdded += emitImportEdge(
3472
+ graph,
3473
+ service.pkg.name,
3474
+ importerFileId,
3475
+ relFile,
3476
+ resolved,
3477
+ imp.line,
3478
+ imp.snippet
3479
+ );
3480
+ }
3481
+ continue;
3482
+ }
3483
+ let jsImports = [];
3484
+ try {
3485
+ const tree = parseSource(jsParser, file.content);
3486
+ collectJsImports(tree.rootNode, jsImports);
3487
+ } catch (err) {
3488
+ recordExtractionError("import extraction", file.path, err);
3489
+ continue;
3490
+ }
3491
+ for (const imp of jsImports) {
3492
+ const resolved = await resolveJsImport(imp.specifier, import_node_path12.default.dirname(file.path), service.dir, tsPaths);
3493
+ if (!resolved) continue;
3494
+ edgesAdded += emitImportEdge(
3495
+ graph,
3496
+ service.pkg.name,
3497
+ importerFileId,
3498
+ relFile,
3499
+ resolved,
3500
+ imp.line,
3501
+ imp.snippet
3502
+ );
3503
+ }
3504
+ }
3505
+ }
3506
+ return { nodesAdded: 0, edgesAdded };
3507
+ }
3508
+
3509
+ // src/extract/databases/index.ts
3510
+ init_cjs_shims();
3511
+ var import_node_path20 = __toESM(require("path"), 1);
3512
+ var import_types9 = require("@neat.is/types");
3089
3513
 
3090
3514
  // src/extract/databases/db-config-yaml.ts
3091
3515
  init_cjs_shims();
3092
- var import_node_path10 = __toESM(require("path"), 1);
3516
+ var import_node_path13 = __toESM(require("path"), 1);
3093
3517
  async function parse(serviceDir) {
3094
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3518
+ const yamlPath = import_node_path13.default.join(serviceDir, "db-config.yaml");
3095
3519
  if (!await exists(yamlPath)) return [];
3096
3520
  const raw = await readYaml(yamlPath);
3097
3521
  return [
@@ -3109,13 +3533,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
3109
3533
 
3110
3534
  // src/extract/databases/dotenv.ts
3111
3535
  init_cjs_shims();
3112
- var import_node_fs11 = require("fs");
3113
- var import_node_path12 = __toESM(require("path"), 1);
3536
+ var import_node_fs13 = require("fs");
3537
+ var import_node_path15 = __toESM(require("path"), 1);
3114
3538
 
3115
3539
  // src/extract/databases/shared.ts
3116
3540
  init_cjs_shims();
3117
- var import_node_fs10 = require("fs");
3118
- var import_node_path11 = __toESM(require("path"), 1);
3541
+ var import_node_fs12 = require("fs");
3542
+ var import_node_path14 = __toESM(require("path"), 1);
3119
3543
  function schemeToEngine(scheme) {
3120
3544
  const s = scheme.toLowerCase().split("+")[0];
3121
3545
  switch (s) {
@@ -3154,14 +3578,14 @@ function parseConnectionString(url) {
3154
3578
  }
3155
3579
  async function readIfExists(filePath) {
3156
3580
  try {
3157
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3581
+ return await import_node_fs12.promises.readFile(filePath, "utf8");
3158
3582
  } catch {
3159
3583
  return null;
3160
3584
  }
3161
3585
  }
3162
3586
  async function findFirst(serviceDir, candidates) {
3163
3587
  for (const rel of candidates) {
3164
- const abs = import_node_path11.default.join(serviceDir, rel);
3588
+ const abs = import_node_path14.default.join(serviceDir, rel);
3165
3589
  const content = await readIfExists(abs);
3166
3590
  if (content !== null) return abs;
3167
3591
  }
@@ -3212,15 +3636,15 @@ function parseDotenvLine(line) {
3212
3636
  return { key, value };
3213
3637
  }
3214
3638
  async function parse2(serviceDir) {
3215
- const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3639
+ const entries = await import_node_fs13.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3216
3640
  const configs = [];
3217
3641
  const seen = /* @__PURE__ */ new Set();
3218
3642
  for (const entry of entries) {
3219
3643
  if (!entry.isFile()) continue;
3220
3644
  const match = isConfigFile(entry.name);
3221
3645
  if (!match.match || match.fileType !== "env") continue;
3222
- const filePath = import_node_path12.default.join(serviceDir, entry.name);
3223
- const content = await import_node_fs11.promises.readFile(filePath, "utf8");
3646
+ const filePath = import_node_path15.default.join(serviceDir, entry.name);
3647
+ const content = await import_node_fs13.promises.readFile(filePath, "utf8");
3224
3648
  for (const line of content.split("\n")) {
3225
3649
  const parsed = parseDotenvLine(line);
3226
3650
  if (!parsed) continue;
@@ -3239,9 +3663,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3239
3663
 
3240
3664
  // src/extract/databases/prisma.ts
3241
3665
  init_cjs_shims();
3242
- var import_node_path13 = __toESM(require("path"), 1);
3666
+ var import_node_path16 = __toESM(require("path"), 1);
3243
3667
  async function parse3(serviceDir) {
3244
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
3668
+ const schemaPath = import_node_path16.default.join(serviceDir, "prisma", "schema.prisma");
3245
3669
  const content = await readIfExists(schemaPath);
3246
3670
  if (!content) return [];
3247
3671
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3373,10 +3797,10 @@ var knexParser = { name: "knex", parse: parse5 };
3373
3797
 
3374
3798
  // src/extract/databases/ormconfig.ts
3375
3799
  init_cjs_shims();
3376
- var import_node_path14 = __toESM(require("path"), 1);
3800
+ var import_node_path17 = __toESM(require("path"), 1);
3377
3801
  async function parse6(serviceDir) {
3378
3802
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
3379
- const abs = import_node_path14.default.join(serviceDir, candidate);
3803
+ const abs = import_node_path17.default.join(serviceDir, candidate);
3380
3804
  if (!await exists(abs)) continue;
3381
3805
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3382
3806
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -3436,9 +3860,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3436
3860
 
3437
3861
  // src/extract/databases/sequelize.ts
3438
3862
  init_cjs_shims();
3439
- var import_node_path15 = __toESM(require("path"), 1);
3863
+ var import_node_path18 = __toESM(require("path"), 1);
3440
3864
  async function parse8(serviceDir) {
3441
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3865
+ const configPath = import_node_path18.default.join(serviceDir, "config", "config.json");
3442
3866
  if (!await exists(configPath)) return [];
3443
3867
  const raw = await readJson(configPath);
3444
3868
  const out = [];
@@ -3465,7 +3889,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3465
3889
 
3466
3890
  // src/extract/databases/docker-compose.ts
3467
3891
  init_cjs_shims();
3468
- var import_node_path16 = __toESM(require("path"), 1);
3892
+ var import_node_path19 = __toESM(require("path"), 1);
3469
3893
  function portFromService(svc) {
3470
3894
  for (const raw of svc.ports ?? []) {
3471
3895
  const str = String(raw);
@@ -3492,7 +3916,7 @@ function databaseFromEnv(svc) {
3492
3916
  }
3493
3917
  async function parse9(serviceDir) {
3494
3918
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3495
- const abs = import_node_path16.default.join(serviceDir, name);
3919
+ const abs = import_node_path19.default.join(serviceDir, name);
3496
3920
  if (!await exists(abs)) continue;
3497
3921
  const raw = await readYaml(abs);
3498
3922
  if (!raw?.services) return [];
@@ -3533,8 +3957,8 @@ function compatibleDriversFor(engine) {
3533
3957
  }
3534
3958
  function toDatabaseNode(config) {
3535
3959
  return {
3536
- id: (0, import_types7.databaseId)(config.host),
3537
- type: import_types7.NodeType.DatabaseNode,
3960
+ id: (0, import_types9.databaseId)(config.host),
3961
+ type: import_types9.NodeType.DatabaseNode,
3538
3962
  name: config.database || config.host,
3539
3963
  engine: config.engine,
3540
3964
  engineVersion: config.engineVersion,
@@ -3664,19 +4088,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3664
4088
  discoveredVia: mergedDiscoveredVia
3665
4089
  });
3666
4090
  }
4091
+ const relConfigFile = toPosix2(import_node_path20.default.relative(service.dir, config.sourceFile));
4092
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4093
+ graph,
4094
+ service.pkg.name,
4095
+ service.node.id,
4096
+ relConfigFile
4097
+ );
4098
+ nodesAdded += fn;
4099
+ edgesAdded += fe;
4100
+ const evidenceFile = toPosix2(import_node_path20.default.relative(scanPath, config.sourceFile));
3667
4101
  const edge = {
3668
- id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
3669
- source: service.node.id,
4102
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, dbNode.id, import_types9.EdgeType.CONNECTS_TO),
4103
+ source: fileNodeId,
3670
4104
  target: dbNode.id,
3671
- type: import_types7.EdgeType.CONNECTS_TO,
3672
- provenance: import_types7.Provenance.EXTRACTED,
3673
- confidence: (0, import_types7.confidenceForExtracted)("structural"),
3674
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3675
- // Ghost-edge cleanup keys retirement on this; the conditional
3676
- // sourceFile spread that used to live here was a v0.1.x leftover.
3677
- evidence: {
3678
- file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3679
- }
4105
+ type: import_types9.EdgeType.CONNECTS_TO,
4106
+ provenance: import_types9.Provenance.EXTRACTED,
4107
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
4108
+ evidence: { file: evidenceFile }
3680
4109
  };
3681
4110
  if (!graph.hasEdge(edge.id)) {
3682
4111
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3702,15 +4131,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3702
4131
 
3703
4132
  // src/extract/configs.ts
3704
4133
  init_cjs_shims();
3705
- var import_node_fs12 = require("fs");
3706
- var import_node_path18 = __toESM(require("path"), 1);
3707
- var import_types8 = require("@neat.is/types");
4134
+ var import_node_fs14 = require("fs");
4135
+ var import_node_path21 = __toESM(require("path"), 1);
4136
+ var import_types10 = require("@neat.is/types");
3708
4137
  async function walkConfigFiles(dir) {
3709
4138
  const out = [];
3710
4139
  async function walk(current) {
3711
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
4140
+ const entries = await import_node_fs14.promises.readdir(current, { withFileTypes: true });
3712
4141
  for (const entry of entries) {
3713
- const full = import_node_path18.default.join(current, entry.name);
4142
+ const full = import_node_path21.default.join(current, entry.name);
3714
4143
  if (entry.isDirectory()) {
3715
4144
  if (IGNORED_DIRS.has(entry.name)) continue;
3716
4145
  if (await isPythonVenvDir(full)) continue;
@@ -3728,147 +4157,57 @@ async function addConfigNodes(graph, services, scanPath) {
3728
4157
  let edgesAdded = 0;
3729
4158
  for (const service of services) {
3730
4159
  const configFiles = await walkConfigFiles(service.dir);
3731
- for (const file of configFiles) {
3732
- const relPath = import_node_path18.default.relative(scanPath, file);
3733
- const node = {
3734
- id: (0, import_types8.configId)(relPath),
3735
- type: import_types8.NodeType.ConfigNode,
3736
- name: import_node_path18.default.basename(file),
3737
- path: relPath,
3738
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3739
- };
3740
- if (!graph.hasNode(node.id)) {
3741
- graph.addNode(node.id, node);
3742
- nodesAdded++;
3743
- }
3744
- const edge = {
3745
- id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
3746
- source: service.node.id,
3747
- target: node.id,
3748
- type: import_types8.EdgeType.CONFIGURED_BY,
3749
- provenance: import_types8.Provenance.EXTRACTED,
3750
- confidence: (0, import_types8.confidenceForExtracted)("structural"),
3751
- evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3752
- };
3753
- if (!graph.hasEdge(edge.id)) {
3754
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3755
- edgesAdded++;
3756
- }
3757
- }
3758
- }
3759
- return { nodesAdded, edgesAdded };
3760
- }
3761
-
3762
- // src/extract/calls/index.ts
3763
- init_cjs_shims();
3764
- var import_types15 = require("@neat.is/types");
3765
-
3766
- // src/extract/calls/http.ts
3767
- init_cjs_shims();
3768
- var import_node_path20 = __toESM(require("path"), 1);
3769
- var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3770
- var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3771
- var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3772
- var import_types10 = require("@neat.is/types");
3773
-
3774
- // src/extract/calls/shared.ts
3775
- init_cjs_shims();
3776
- var import_node_fs13 = require("fs");
3777
- var import_node_path19 = __toESM(require("path"), 1);
3778
- var import_types9 = require("@neat.is/types");
3779
- async function walkSourceFiles(dir) {
3780
- const out = [];
3781
- async function walk(current) {
3782
- const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3783
- for (const entry of entries) {
3784
- const full = import_node_path19.default.join(current, entry.name);
3785
- if (entry.isDirectory()) {
3786
- if (IGNORED_DIRS.has(entry.name)) continue;
3787
- if (await isPythonVenvDir(full)) continue;
3788
- await walk(full);
3789
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3790
- out.push(full);
3791
- }
3792
- }
3793
- }
3794
- await walk(dir);
3795
- return out;
3796
- }
3797
- async function loadSourceFiles(dir) {
3798
- const paths = await walkSourceFiles(dir);
3799
- const out = [];
3800
- for (const p of paths) {
3801
- try {
3802
- const content = await import_node_fs13.promises.readFile(p, "utf8");
3803
- out.push({ path: p, content });
3804
- } catch {
3805
- }
3806
- }
3807
- return out;
3808
- }
3809
- function lineOf(text, needle) {
3810
- const idx = text.indexOf(needle);
3811
- if (idx < 0) return 1;
3812
- return text.slice(0, idx).split("\n").length;
3813
- }
3814
- function snippet(text, line) {
3815
- const lines = text.split("\n");
3816
- return (lines[line - 1] ?? "").trim();
3817
- }
3818
- function toPosix2(p) {
3819
- return p.split("\\").join("/");
3820
- }
3821
- function languageForPath(relPath) {
3822
- switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3823
- case ".py":
3824
- return "python";
3825
- case ".ts":
3826
- case ".tsx":
3827
- return "typescript";
3828
- case ".js":
3829
- case ".jsx":
3830
- case ".mjs":
3831
- case ".cjs":
3832
- return "javascript";
3833
- default:
3834
- return void 0;
3835
- }
3836
- }
3837
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3838
- let nodesAdded = 0;
3839
- let edgesAdded = 0;
3840
- const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3841
- if (!graph.hasNode(fileNodeId)) {
3842
- const language = languageForPath(relPath);
3843
- const node = {
3844
- id: fileNodeId,
3845
- type: import_types9.NodeType.FileNode,
3846
- service: serviceName,
3847
- path: relPath,
3848
- ...language ? { language } : {},
3849
- discoveredVia: "static"
3850
- };
3851
- graph.addNode(fileNodeId, node);
3852
- nodesAdded++;
3853
- }
3854
- const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3855
- if (!graph.hasEdge(containsId)) {
3856
- const edge = {
3857
- id: containsId,
3858
- source: serviceNodeId,
3859
- target: fileNodeId,
3860
- type: import_types9.EdgeType.CONTAINS,
3861
- provenance: import_types9.Provenance.EXTRACTED,
3862
- confidence: (0, import_types9.confidenceForExtracted)("structural"),
3863
- evidence: { file: relPath }
3864
- };
3865
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3866
- edgesAdded++;
4160
+ for (const file of configFiles) {
4161
+ const relPath = import_node_path21.default.relative(scanPath, file);
4162
+ const node = {
4163
+ id: (0, import_types10.configId)(relPath),
4164
+ type: import_types10.NodeType.ConfigNode,
4165
+ name: import_node_path21.default.basename(file),
4166
+ path: relPath,
4167
+ fileType: isConfigFile(import_node_path21.default.basename(file)).fileType
4168
+ };
4169
+ if (!graph.hasNode(node.id)) {
4170
+ graph.addNode(node.id, node);
4171
+ nodesAdded++;
4172
+ }
4173
+ const relToService = toPosix2(import_node_path21.default.relative(service.dir, file));
4174
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4175
+ graph,
4176
+ service.pkg.name,
4177
+ service.node.id,
4178
+ relToService
4179
+ );
4180
+ nodesAdded += fn;
4181
+ edgesAdded += fe;
4182
+ const edge = {
4183
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types10.EdgeType.CONFIGURED_BY),
4184
+ source: fileNodeId,
4185
+ target: node.id,
4186
+ type: import_types10.EdgeType.CONFIGURED_BY,
4187
+ provenance: import_types10.Provenance.EXTRACTED,
4188
+ confidence: (0, import_types10.confidenceForExtracted)("structural"),
4189
+ evidence: { file: relPath.split(import_node_path21.default.sep).join("/") }
4190
+ };
4191
+ if (!graph.hasEdge(edge.id)) {
4192
+ graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
4193
+ edgesAdded++;
4194
+ }
4195
+ }
3867
4196
  }
3868
- return { fileNodeId, nodesAdded, edgesAdded };
4197
+ return { nodesAdded, edgesAdded };
3869
4198
  }
3870
4199
 
4200
+ // src/extract/calls/index.ts
4201
+ init_cjs_shims();
4202
+ var import_types16 = require("@neat.is/types");
4203
+
3871
4204
  // src/extract/calls/http.ts
4205
+ init_cjs_shims();
4206
+ var import_node_path22 = __toESM(require("path"), 1);
4207
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
4208
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
4209
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
4210
+ var import_types11 = require("@neat.is/types");
3872
4211
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3873
4212
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3874
4213
  function isInsideJsxExternalLink(node) {
@@ -3896,14 +4235,14 @@ function collectStringLiterals(node, out) {
3896
4235
  if (child) collectStringLiterals(child, out);
3897
4236
  }
3898
4237
  }
3899
- var PARSE_CHUNK = 16384;
3900
- function parseSource(parser, source) {
4238
+ var PARSE_CHUNK2 = 16384;
4239
+ function parseSource2(parser, source) {
3901
4240
  return parser.parse(
3902
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
4241
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
3903
4242
  );
3904
4243
  }
3905
4244
  function callsFromSource(source, parser, knownHosts) {
3906
- const tree = parseSource(parser, source);
4245
+ const tree = parseSource2(parser, source);
3907
4246
  const literals = [];
3908
4247
  collectStringLiterals(tree.rootNode, literals);
3909
4248
  const out = [];
@@ -3917,25 +4256,25 @@ function callsFromSource(source, parser, knownHosts) {
3917
4256
  }
3918
4257
  return out;
3919
4258
  }
3920
- function makeJsParser() {
3921
- const p = new import_tree_sitter.default();
3922
- p.setLanguage(import_tree_sitter_javascript.default);
4259
+ function makeJsParser2() {
4260
+ const p = new import_tree_sitter2.default();
4261
+ p.setLanguage(import_tree_sitter_javascript2.default);
3923
4262
  return p;
3924
4263
  }
3925
- function makePyParser() {
3926
- const p = new import_tree_sitter.default();
3927
- p.setLanguage(import_tree_sitter_python.default);
4264
+ function makePyParser2() {
4265
+ const p = new import_tree_sitter2.default();
4266
+ p.setLanguage(import_tree_sitter_python2.default);
3928
4267
  return p;
3929
4268
  }
3930
4269
  async function addHttpCallEdges(graph, services) {
3931
- const jsParser = makeJsParser();
3932
- const pyParser = makePyParser();
4270
+ const jsParser = makeJsParser2();
4271
+ const pyParser = makePyParser2();
3933
4272
  const knownHosts = /* @__PURE__ */ new Set();
3934
4273
  const hostToNodeId = /* @__PURE__ */ new Map();
3935
4274
  for (const service of services) {
3936
- knownHosts.add(import_node_path20.default.basename(service.dir));
4275
+ knownHosts.add(import_node_path22.default.basename(service.dir));
3937
4276
  knownHosts.add(service.pkg.name);
3938
- hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
4277
+ hostToNodeId.set(import_node_path22.default.basename(service.dir), service.node.id);
3939
4278
  hostToNodeId.set(service.pkg.name, service.node.id);
3940
4279
  }
3941
4280
  let nodesAdded = 0;
@@ -3945,7 +4284,7 @@ async function addHttpCallEdges(graph, services) {
3945
4284
  const seen = /* @__PURE__ */ new Set();
3946
4285
  for (const file of files) {
3947
4286
  if (isTestPath(file.path)) continue;
3948
- const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
4287
+ const parser = import_node_path22.default.extname(file.path) === ".py" ? pyParser : jsParser;
3949
4288
  let sites;
3950
4289
  try {
3951
4290
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -3954,14 +4293,14 @@ async function addHttpCallEdges(graph, services) {
3954
4293
  continue;
3955
4294
  }
3956
4295
  if (sites.length === 0) continue;
3957
- const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
4296
+ const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
3958
4297
  for (const site of sites) {
3959
4298
  const targetId = hostToNodeId.get(site.host);
3960
4299
  if (!targetId || targetId === service.node.id) continue;
3961
4300
  const dedupKey = `${relFile}|${targetId}`;
3962
4301
  if (seen.has(dedupKey)) continue;
3963
4302
  seen.add(dedupKey);
3964
- const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
4303
+ const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
3965
4304
  const ev = {
3966
4305
  file: relFile,
3967
4306
  line: site.line,
@@ -3975,25 +4314,25 @@ async function addHttpCallEdges(graph, services) {
3975
4314
  );
3976
4315
  nodesAdded += n;
3977
4316
  edgesAdded += e;
3978
- if (!(0, import_types10.passesExtractedFloor)(confidence)) {
4317
+ if (!(0, import_types11.passesExtractedFloor)(confidence)) {
3979
4318
  noteExtractedDropped({
3980
4319
  source: fileNodeId,
3981
4320
  target: targetId,
3982
- type: import_types10.EdgeType.CALLS,
4321
+ type: import_types11.EdgeType.CALLS,
3983
4322
  confidence,
3984
4323
  confidenceKind: "hostname-shape-match",
3985
4324
  evidence: ev
3986
4325
  });
3987
4326
  continue;
3988
4327
  }
3989
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
4328
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types11.EdgeType.CALLS);
3990
4329
  if (!graph.hasEdge(edgeId)) {
3991
4330
  const edge = {
3992
4331
  id: edgeId,
3993
4332
  source: fileNodeId,
3994
4333
  target: targetId,
3995
- type: import_types10.EdgeType.CALLS,
3996
- provenance: import_types10.Provenance.EXTRACTED,
4334
+ type: import_types11.EdgeType.CALLS,
4335
+ provenance: import_types11.Provenance.EXTRACTED,
3997
4336
  confidence,
3998
4337
  evidence: ev
3999
4338
  };
@@ -4008,8 +4347,8 @@ async function addHttpCallEdges(graph, services) {
4008
4347
 
4009
4348
  // src/extract/calls/kafka.ts
4010
4349
  init_cjs_shims();
4011
- var import_node_path21 = __toESM(require("path"), 1);
4012
- var import_types11 = require("@neat.is/types");
4350
+ var import_node_path23 = __toESM(require("path"), 1);
4351
+ var import_types12 = require("@neat.is/types");
4013
4352
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
4014
4353
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
4015
4354
  function findAll(re, text) {
@@ -4030,7 +4369,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4030
4369
  seen.add(key);
4031
4370
  const line = lineOf(file.content, topic);
4032
4371
  out.push({
4033
- infraId: (0, import_types11.infraId)("kafka-topic", topic),
4372
+ infraId: (0, import_types12.infraId)("kafka-topic", topic),
4034
4373
  name: topic,
4035
4374
  kind: "kafka-topic",
4036
4375
  edgeType,
@@ -4039,7 +4378,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4039
4378
  // tier (ADR-066).
4040
4379
  confidenceKind: "verified-call-site",
4041
4380
  evidence: {
4042
- file: import_node_path21.default.relative(serviceDir, file.path),
4381
+ file: import_node_path23.default.relative(serviceDir, file.path),
4043
4382
  line,
4044
4383
  snippet: snippet(file.content, line)
4045
4384
  }
@@ -4052,8 +4391,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4052
4391
 
4053
4392
  // src/extract/calls/redis.ts
4054
4393
  init_cjs_shims();
4055
- var import_node_path22 = __toESM(require("path"), 1);
4056
- var import_types12 = require("@neat.is/types");
4394
+ var import_node_path24 = __toESM(require("path"), 1);
4395
+ var import_types13 = require("@neat.is/types");
4057
4396
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
4058
4397
  function redisEndpointsFromFile(file, serviceDir) {
4059
4398
  const out = [];
@@ -4066,7 +4405,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4066
4405
  seen.add(host);
4067
4406
  const line = lineOf(file.content, host);
4068
4407
  out.push({
4069
- infraId: (0, import_types12.infraId)("redis", host),
4408
+ infraId: (0, import_types13.infraId)("redis", host),
4070
4409
  name: host,
4071
4410
  kind: "redis",
4072
4411
  edgeType: "CALLS",
@@ -4075,7 +4414,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4075
4414
  // support tier (ADR-066).
4076
4415
  confidenceKind: "url-with-structural-support",
4077
4416
  evidence: {
4078
- file: import_node_path22.default.relative(serviceDir, file.path),
4417
+ file: import_node_path24.default.relative(serviceDir, file.path),
4079
4418
  line,
4080
4419
  snippet: snippet(file.content, line)
4081
4420
  }
@@ -4086,8 +4425,8 @@ function redisEndpointsFromFile(file, serviceDir) {
4086
4425
 
4087
4426
  // src/extract/calls/aws.ts
4088
4427
  init_cjs_shims();
4089
- var import_node_path23 = __toESM(require("path"), 1);
4090
- var import_types13 = require("@neat.is/types");
4428
+ var import_node_path25 = __toESM(require("path"), 1);
4429
+ var import_types14 = require("@neat.is/types");
4091
4430
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
4092
4431
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
4093
4432
  function hasMarker(text, markers) {
@@ -4111,7 +4450,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4111
4450
  seen.add(key);
4112
4451
  const line = lineOf(file.content, name);
4113
4452
  out.push({
4114
- infraId: (0, import_types13.infraId)(kind, name),
4453
+ infraId: (0, import_types14.infraId)(kind, name),
4115
4454
  name,
4116
4455
  kind,
4117
4456
  edgeType: "CALLS",
@@ -4120,7 +4459,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4120
4459
  // (ADR-066).
4121
4460
  confidenceKind: "verified-call-site",
4122
4461
  evidence: {
4123
- file: import_node_path23.default.relative(serviceDir, file.path),
4462
+ file: import_node_path25.default.relative(serviceDir, file.path),
4124
4463
  line,
4125
4464
  snippet: snippet(file.content, line)
4126
4465
  }
@@ -4145,13 +4484,14 @@ function awsEndpointsFromFile(file, serviceDir) {
4145
4484
 
4146
4485
  // src/extract/calls/grpc.ts
4147
4486
  init_cjs_shims();
4148
- var import_node_path24 = __toESM(require("path"), 1);
4149
- var import_types14 = require("@neat.is/types");
4487
+ var import_node_path26 = __toESM(require("path"), 1);
4488
+ var import_types15 = require("@neat.is/types");
4150
4489
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
4151
4490
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
4152
4491
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
4153
4492
  function isLikelyAddress(value) {
4154
4493
  if (!value) return false;
4494
+ if (/\s/.test(value) || value.startsWith("{")) return false;
4155
4495
  return /:\d{2,5}$/.test(value) || value.includes(".");
4156
4496
  }
4157
4497
  function normaliseForMatch(s) {
@@ -4194,7 +4534,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4194
4534
  const { kind } = classified;
4195
4535
  const line = lineOf(file.content, m[0]);
4196
4536
  out.push({
4197
- infraId: (0, import_types14.infraId)(kind, name),
4537
+ infraId: (0, import_types15.infraId)(kind, name),
4198
4538
  name,
4199
4539
  kind,
4200
4540
  edgeType: "CALLS",
@@ -4203,7 +4543,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4203
4543
  // tier (ADR-066).
4204
4544
  confidenceKind: "verified-call-site",
4205
4545
  evidence: {
4206
- file: import_node_path24.default.relative(serviceDir, file.path),
4546
+ file: import_node_path26.default.relative(serviceDir, file.path),
4207
4547
  line,
4208
4548
  snippet: snippet(file.content, line)
4209
4549
  }
@@ -4216,11 +4556,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
4216
4556
  function edgeTypeFromEndpoint(ep) {
4217
4557
  switch (ep.edgeType) {
4218
4558
  case "PUBLISHES_TO":
4219
- return import_types15.EdgeType.PUBLISHES_TO;
4559
+ return import_types16.EdgeType.PUBLISHES_TO;
4220
4560
  case "CONSUMES_FROM":
4221
- return import_types15.EdgeType.CONSUMES_FROM;
4561
+ return import_types16.EdgeType.CONSUMES_FROM;
4222
4562
  default:
4223
- return import_types15.EdgeType.CALLS;
4563
+ return import_types16.EdgeType.CALLS;
4224
4564
  }
4225
4565
  }
4226
4566
  function isAwsKind(kind) {
@@ -4247,7 +4587,7 @@ async function addExternalEndpointEdges(graph, services) {
4247
4587
  if (!graph.hasNode(ep.infraId)) {
4248
4588
  const node = {
4249
4589
  id: ep.infraId,
4250
- type: import_types15.NodeType.InfraNode,
4590
+ type: import_types16.NodeType.InfraNode,
4251
4591
  name: ep.name,
4252
4592
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4253
4593
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4259,7 +4599,7 @@ async function addExternalEndpointEdges(graph, services) {
4259
4599
  nodesAdded++;
4260
4600
  }
4261
4601
  const edgeType = edgeTypeFromEndpoint(ep);
4262
- const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4602
+ const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4263
4603
  const relFile = toPosix2(ep.evidence.file);
4264
4604
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4265
4605
  graph,
@@ -4269,7 +4609,7 @@ async function addExternalEndpointEdges(graph, services) {
4269
4609
  );
4270
4610
  nodesAdded += n;
4271
4611
  edgesAdded += e;
4272
- if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4612
+ if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4273
4613
  noteExtractedDropped({
4274
4614
  source: fileNodeId,
4275
4615
  target: ep.infraId,
@@ -4289,7 +4629,7 @@ async function addExternalEndpointEdges(graph, services) {
4289
4629
  source: fileNodeId,
4290
4630
  target: ep.infraId,
4291
4631
  type: edgeType,
4292
- provenance: import_types15.Provenance.EXTRACTED,
4632
+ provenance: import_types16.Provenance.EXTRACTED,
4293
4633
  confidence,
4294
4634
  evidence: ep.evidence
4295
4635
  };
@@ -4314,16 +4654,16 @@ init_cjs_shims();
4314
4654
 
4315
4655
  // src/extract/infra/docker-compose.ts
4316
4656
  init_cjs_shims();
4317
- var import_node_path25 = __toESM(require("path"), 1);
4318
- var import_types17 = require("@neat.is/types");
4657
+ var import_node_path27 = __toESM(require("path"), 1);
4658
+ var import_types18 = require("@neat.is/types");
4319
4659
 
4320
4660
  // src/extract/infra/shared.ts
4321
4661
  init_cjs_shims();
4322
- var import_types16 = require("@neat.is/types");
4662
+ var import_types17 = require("@neat.is/types");
4323
4663
  function makeInfraNode(kind, name, provider = "self", extras) {
4324
4664
  return {
4325
- id: (0, import_types16.infraId)(kind, name),
4326
- type: import_types16.NodeType.InfraNode,
4665
+ id: (0, import_types17.infraId)(kind, name),
4666
+ type: import_types17.NodeType.InfraNode,
4327
4667
  name,
4328
4668
  provider,
4329
4669
  kind,
@@ -4352,7 +4692,7 @@ function dependsOnList(value) {
4352
4692
  }
4353
4693
  function serviceNameToServiceNode(name, services) {
4354
4694
  for (const s of services) {
4355
- if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
4695
+ if (s.node.name === name || import_node_path27.default.basename(s.dir) === name) return s.node.id;
4356
4696
  }
4357
4697
  return null;
4358
4698
  }
@@ -4361,7 +4701,7 @@ async function addComposeInfra(graph, scanPath, services) {
4361
4701
  let edgesAdded = 0;
4362
4702
  let composePath = null;
4363
4703
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4364
- const abs = import_node_path25.default.join(scanPath, name);
4704
+ const abs = import_node_path27.default.join(scanPath, name);
4365
4705
  if (await exists(abs)) {
4366
4706
  composePath = abs;
4367
4707
  break;
@@ -4374,13 +4714,13 @@ async function addComposeInfra(graph, scanPath, services) {
4374
4714
  } catch (err) {
4375
4715
  recordExtractionError(
4376
4716
  "infra docker-compose",
4377
- import_node_path25.default.relative(scanPath, composePath),
4717
+ import_node_path27.default.relative(scanPath, composePath),
4378
4718
  err
4379
4719
  );
4380
4720
  return { nodesAdded, edgesAdded };
4381
4721
  }
4382
4722
  if (!compose?.services) return { nodesAdded, edgesAdded };
4383
- const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
4723
+ const evidenceFile = import_node_path27.default.relative(scanPath, composePath).split(import_node_path27.default.sep).join("/");
4384
4724
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4385
4725
  for (const [composeName, svc] of Object.entries(compose.services)) {
4386
4726
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4402,15 +4742,15 @@ async function addComposeInfra(graph, scanPath, services) {
4402
4742
  for (const dep of dependsOnList(svc.depends_on)) {
4403
4743
  const targetId = composeNameToNodeId.get(dep);
4404
4744
  if (!targetId) continue;
4405
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4745
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4406
4746
  if (graph.hasEdge(edgeId)) continue;
4407
4747
  const edge = {
4408
4748
  id: edgeId,
4409
4749
  source: sourceId,
4410
4750
  target: targetId,
4411
- type: import_types17.EdgeType.DEPENDS_ON,
4412
- provenance: import_types17.Provenance.EXTRACTED,
4413
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4751
+ type: import_types18.EdgeType.DEPENDS_ON,
4752
+ provenance: import_types18.Provenance.EXTRACTED,
4753
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4414
4754
  evidence: { file: evidenceFile }
4415
4755
  };
4416
4756
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4422,9 +4762,9 @@ async function addComposeInfra(graph, scanPath, services) {
4422
4762
 
4423
4763
  // src/extract/infra/dockerfile.ts
4424
4764
  init_cjs_shims();
4425
- var import_node_path26 = __toESM(require("path"), 1);
4426
- var import_node_fs14 = require("fs");
4427
- var import_types18 = require("@neat.is/types");
4765
+ var import_node_path28 = __toESM(require("path"), 1);
4766
+ var import_node_fs15 = require("fs");
4767
+ var import_types19 = require("@neat.is/types");
4428
4768
  function runtimeImage(content) {
4429
4769
  const lines = content.split("\n");
4430
4770
  let last = null;
@@ -4443,15 +4783,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4443
4783
  let nodesAdded = 0;
4444
4784
  let edgesAdded = 0;
4445
4785
  for (const service of services) {
4446
- const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
4786
+ const dockerfilePath = import_node_path28.default.join(service.dir, "Dockerfile");
4447
4787
  if (!await exists(dockerfilePath)) continue;
4448
4788
  let content;
4449
4789
  try {
4450
- content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
4790
+ content = await import_node_fs15.promises.readFile(dockerfilePath, "utf8");
4451
4791
  } catch (err) {
4452
4792
  recordExtractionError(
4453
4793
  "infra dockerfile",
4454
- import_node_path26.default.relative(scanPath, dockerfilePath),
4794
+ import_node_path28.default.relative(scanPath, dockerfilePath),
4455
4795
  err
4456
4796
  );
4457
4797
  continue;
@@ -4463,17 +4803,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4463
4803
  graph.addNode(node.id, node);
4464
4804
  nodesAdded++;
4465
4805
  }
4466
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4806
+ const relDockerfile = toPosix2(import_node_path28.default.relative(service.dir, dockerfilePath));
4807
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4808
+ graph,
4809
+ service.pkg.name,
4810
+ service.node.id,
4811
+ relDockerfile
4812
+ );
4813
+ nodesAdded += fn;
4814
+ edgesAdded += fe;
4815
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
4467
4816
  if (!graph.hasEdge(edgeId)) {
4468
4817
  const edge = {
4469
4818
  id: edgeId,
4470
- source: service.node.id,
4819
+ source: fileNodeId,
4471
4820
  target: node.id,
4472
- type: import_types18.EdgeType.RUNS_ON,
4473
- provenance: import_types18.Provenance.EXTRACTED,
4474
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4821
+ type: import_types19.EdgeType.RUNS_ON,
4822
+ provenance: import_types19.Provenance.EXTRACTED,
4823
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4475
4824
  evidence: {
4476
- file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4825
+ file: toPosix2(import_node_path28.default.relative(scanPath, dockerfilePath))
4477
4826
  }
4478
4827
  };
4479
4828
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4485,21 +4834,21 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4485
4834
 
4486
4835
  // src/extract/infra/terraform.ts
4487
4836
  init_cjs_shims();
4488
- var import_node_fs15 = require("fs");
4489
- var import_node_path27 = __toESM(require("path"), 1);
4837
+ var import_node_fs16 = require("fs");
4838
+ var import_node_path29 = __toESM(require("path"), 1);
4490
4839
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4491
4840
  async function walkTfFiles(start, depth = 0, max = 5) {
4492
4841
  if (depth > max) return [];
4493
4842
  const out = [];
4494
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4843
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4495
4844
  for (const entry of entries) {
4496
4845
  if (entry.isDirectory()) {
4497
4846
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4498
- const child = import_node_path27.default.join(start, entry.name);
4847
+ const child = import_node_path29.default.join(start, entry.name);
4499
4848
  if (await isPythonVenvDir(child)) continue;
4500
4849
  out.push(...await walkTfFiles(child, depth + 1, max));
4501
4850
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4502
- out.push(import_node_path27.default.join(start, entry.name));
4851
+ out.push(import_node_path29.default.join(start, entry.name));
4503
4852
  }
4504
4853
  }
4505
4854
  return out;
@@ -4508,7 +4857,7 @@ async function addTerraformResources(graph, scanPath) {
4508
4857
  let nodesAdded = 0;
4509
4858
  const files = await walkTfFiles(scanPath);
4510
4859
  for (const file of files) {
4511
- const content = await import_node_fs15.promises.readFile(file, "utf8");
4860
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
4512
4861
  RESOURCE_RE.lastIndex = 0;
4513
4862
  let m;
4514
4863
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -4526,8 +4875,8 @@ async function addTerraformResources(graph, scanPath) {
4526
4875
 
4527
4876
  // src/extract/infra/k8s.ts
4528
4877
  init_cjs_shims();
4529
- var import_node_fs16 = require("fs");
4530
- var import_node_path28 = __toESM(require("path"), 1);
4878
+ var import_node_fs17 = require("fs");
4879
+ var import_node_path30 = __toESM(require("path"), 1);
4531
4880
  var import_yaml3 = require("yaml");
4532
4881
  var K8S_KIND_TO_INFRA_KIND = {
4533
4882
  Service: "k8s-service",
@@ -4541,15 +4890,15 @@ var K8S_KIND_TO_INFRA_KIND = {
4541
4890
  async function walkYamlFiles2(start, depth = 0, max = 5) {
4542
4891
  if (depth > max) return [];
4543
4892
  const out = [];
4544
- const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4893
+ const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4545
4894
  for (const entry of entries) {
4546
4895
  if (entry.isDirectory()) {
4547
4896
  if (IGNORED_DIRS.has(entry.name)) continue;
4548
- const child = import_node_path28.default.join(start, entry.name);
4897
+ const child = import_node_path30.default.join(start, entry.name);
4549
4898
  if (await isPythonVenvDir(child)) continue;
4550
4899
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4551
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
4552
- out.push(import_node_path28.default.join(start, entry.name));
4900
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path30.default.extname(entry.name))) {
4901
+ out.push(import_node_path30.default.join(start, entry.name));
4553
4902
  }
4554
4903
  }
4555
4904
  return out;
@@ -4558,7 +4907,7 @@ async function addK8sResources(graph, scanPath) {
4558
4907
  let nodesAdded = 0;
4559
4908
  const files = await walkYamlFiles2(scanPath);
4560
4909
  for (const file of files) {
4561
- const content = await import_node_fs16.promises.readFile(file, "utf8");
4910
+ const content = await import_node_fs17.promises.readFile(file, "utf8");
4562
4911
  let docs;
4563
4912
  try {
4564
4913
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -4593,17 +4942,17 @@ async function addInfra(graph, scanPath, services) {
4593
4942
  }
4594
4943
 
4595
4944
  // src/extract/index.ts
4596
- var import_node_path30 = __toESM(require("path"), 1);
4945
+ var import_node_path32 = __toESM(require("path"), 1);
4597
4946
 
4598
4947
  // src/extract/retire.ts
4599
4948
  init_cjs_shims();
4600
- var import_node_fs17 = require("fs");
4601
- var import_node_path29 = __toESM(require("path"), 1);
4602
- var import_types19 = require("@neat.is/types");
4949
+ var import_node_fs18 = require("fs");
4950
+ var import_node_path31 = __toESM(require("path"), 1);
4951
+ var import_types20 = require("@neat.is/types");
4603
4952
  function dropOrphanedFileNodes(graph) {
4604
4953
  const orphans = [];
4605
4954
  graph.forEachNode((id, attrs) => {
4606
- if (attrs.type !== import_types19.NodeType.FileNode) return;
4955
+ if (attrs.type !== import_types20.NodeType.FileNode) return;
4607
4956
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4608
4957
  orphans.push(id);
4609
4958
  }
@@ -4616,14 +4965,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4616
4965
  const bases = [scanPath, ...serviceDirs];
4617
4966
  graph.forEachEdge((id, attrs) => {
4618
4967
  const edge = attrs;
4619
- if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4968
+ if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
4620
4969
  const evidenceFile = edge.evidence?.file;
4621
4970
  if (!evidenceFile) return;
4622
- if (import_node_path29.default.isAbsolute(evidenceFile)) {
4623
- if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
4971
+ if (import_node_path31.default.isAbsolute(evidenceFile)) {
4972
+ if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
4624
4973
  return;
4625
4974
  }
4626
- const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
4975
+ const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path31.default.join(base, evidenceFile)));
4627
4976
  if (!found) toDrop.push(id);
4628
4977
  });
4629
4978
  for (const id of toDrop) graph.dropEdge(id);
@@ -4638,6 +4987,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4638
4987
  const services = await discoverServices(scanPath);
4639
4988
  const phase1Nodes = addServiceNodes(graph, services);
4640
4989
  await addServiceAliases(graph, scanPath, services);
4990
+ const fileEnum = await addFiles(graph, services);
4991
+ const importGraph = await addImports(graph, services);
4641
4992
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
4642
4993
  const phase3 = await addConfigNodes(graph, services, scanPath);
4643
4994
  const phase4 = await addCallEdges(graph, services);
@@ -4661,7 +5012,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4661
5012
  }
4662
5013
  const droppedEntries = drainDroppedExtracted();
4663
5014
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4664
- const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
5015
+ const rejectedPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.errorsPath), "rejected.ndjson");
4665
5016
  try {
4666
5017
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4667
5018
  } catch (err) {
@@ -4671,8 +5022,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4671
5022
  }
4672
5023
  }
4673
5024
  const result = {
4674
- nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
4675
- edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
5025
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
5026
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
4676
5027
  frontiersPromoted,
4677
5028
  extractionErrors: errorEntries.length,
4678
5029
  errorEntries,
@@ -4695,9 +5046,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4695
5046
 
4696
5047
  // src/persist.ts
4697
5048
  init_cjs_shims();
4698
- var import_node_fs18 = require("fs");
4699
- var import_node_path31 = __toESM(require("path"), 1);
4700
- var import_types20 = require("@neat.is/types");
5049
+ var import_node_fs19 = require("fs");
5050
+ var import_node_path33 = __toESM(require("path"), 1);
5051
+ var import_types21 = require("@neat.is/types");
4701
5052
  var SCHEMA_VERSION = 4;
4702
5053
  function migrateV1ToV2(payload) {
4703
5054
  const nodes = payload.graph.nodes;
@@ -4719,12 +5070,12 @@ function migrateV2ToV3(payload) {
4719
5070
  for (const edge of edges) {
4720
5071
  const attrs = edge.attributes;
4721
5072
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4722
- attrs.provenance = import_types20.Provenance.OBSERVED;
5073
+ attrs.provenance = import_types21.Provenance.OBSERVED;
4723
5074
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4724
5075
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4725
5076
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4726
5077
  if (type && source && target) {
4727
- const newId = (0, import_types20.observedEdgeId)(source, target, type);
5078
+ const newId = (0, import_types21.observedEdgeId)(source, target, type);
4728
5079
  attrs.id = newId;
4729
5080
  if (edge.key) edge.key = newId;
4730
5081
  }
@@ -4733,7 +5084,7 @@ function migrateV2ToV3(payload) {
4733
5084
  return { ...payload, schemaVersion: 3 };
4734
5085
  }
4735
5086
  async function ensureDir(filePath) {
4736
- await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
5087
+ await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
4737
5088
  }
4738
5089
  async function saveGraphToDisk(graph, outPath) {
4739
5090
  await ensureDir(outPath);
@@ -4743,13 +5094,13 @@ async function saveGraphToDisk(graph, outPath) {
4743
5094
  graph: graph.export()
4744
5095
  };
4745
5096
  const tmp = `${outPath}.tmp`;
4746
- await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4747
- await import_node_fs18.promises.rename(tmp, outPath);
5097
+ await import_node_fs19.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
5098
+ await import_node_fs19.promises.rename(tmp, outPath);
4748
5099
  }
4749
5100
  async function loadGraphFromDisk(graph, outPath) {
4750
5101
  let raw;
4751
5102
  try {
4752
- raw = await import_node_fs18.promises.readFile(outPath, "utf8");
5103
+ raw = await import_node_fs19.promises.readFile(outPath, "utf8");
4753
5104
  } catch (err) {
4754
5105
  if (err.code === "ENOENT") return;
4755
5106
  throw err;
@@ -4810,11 +5161,326 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4810
5161
  init_cjs_shims();
4811
5162
  var import_fastify = __toESM(require("fastify"), 1);
4812
5163
  var import_cors = __toESM(require("@fastify/cors"), 1);
4813
- var import_types23 = require("@neat.is/types");
5164
+ var import_types24 = require("@neat.is/types");
5165
+
5166
+ // src/extend/index.ts
5167
+ init_cjs_shims();
5168
+ var import_node_fs21 = require("fs");
5169
+ var import_node_path35 = __toESM(require("path"), 1);
5170
+ var import_node_os2 = __toESM(require("os"), 1);
5171
+ var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
5172
+
5173
+ // src/installers/package-manager.ts
5174
+ init_cjs_shims();
5175
+ var import_node_fs20 = require("fs");
5176
+ var import_node_path34 = __toESM(require("path"), 1);
5177
+ var import_node_child_process = require("child_process");
5178
+ var LOCKFILE_PRIORITY = [
5179
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
5180
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
5181
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
5182
+ {
5183
+ lockfile: "package-lock.json",
5184
+ pm: "npm",
5185
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
5186
+ }
5187
+ ];
5188
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
5189
+ async function exists2(p) {
5190
+ try {
5191
+ await import_node_fs20.promises.access(p);
5192
+ return true;
5193
+ } catch {
5194
+ return false;
5195
+ }
5196
+ }
5197
+ async function detectPackageManager(serviceDir) {
5198
+ let dir = import_node_path34.default.resolve(serviceDir);
5199
+ const stops = /* @__PURE__ */ new Set();
5200
+ for (let i = 0; i < 64; i++) {
5201
+ if (stops.has(dir)) break;
5202
+ stops.add(dir);
5203
+ for (const candidate of LOCKFILE_PRIORITY) {
5204
+ const lockPath = import_node_path34.default.join(dir, candidate.lockfile);
5205
+ if (await exists2(lockPath)) {
5206
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5207
+ }
5208
+ }
5209
+ const parent = import_node_path34.default.dirname(dir);
5210
+ if (parent === dir) break;
5211
+ dir = parent;
5212
+ }
5213
+ return { pm: "npm", cwd: import_node_path34.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5214
+ }
5215
+ async function runPackageManagerInstall(cmd) {
5216
+ return new Promise((resolve) => {
5217
+ const child = (0, import_node_child_process.spawn)(cmd.pm, cmd.args, {
5218
+ cwd: cmd.cwd,
5219
+ // Inherit PATH + HOME so the user's installed managers resolve.
5220
+ env: process.env,
5221
+ // `false` keeps the parent in control of cleanup if the orchestrator
5222
+ // exits before install finishes. Cross-platform-safe.
5223
+ shell: false,
5224
+ stdio: ["ignore", "ignore", "pipe"]
5225
+ });
5226
+ let stderr = "";
5227
+ child.stderr?.on("data", (chunk) => {
5228
+ stderr += chunk.toString("utf8");
5229
+ });
5230
+ child.on("error", (err) => {
5231
+ resolve({
5232
+ pm: cmd.pm,
5233
+ cwd: cmd.cwd,
5234
+ args: cmd.args,
5235
+ exitCode: 127,
5236
+ stderr: stderr + `
5237
+ ${err.message}`
5238
+ });
5239
+ });
5240
+ child.on("close", (code) => {
5241
+ resolve({
5242
+ pm: cmd.pm,
5243
+ cwd: cmd.cwd,
5244
+ args: cmd.args,
5245
+ exitCode: code ?? 1,
5246
+ stderr: stderr.trim()
5247
+ });
5248
+ });
5249
+ });
5250
+ }
5251
+
5252
+ // src/extend/index.ts
5253
+ async function fileExists2(p) {
5254
+ try {
5255
+ await import_node_fs21.promises.access(p);
5256
+ return true;
5257
+ } catch {
5258
+ return false;
5259
+ }
5260
+ }
5261
+ async function readPackageJson(scanPath) {
5262
+ const pkgPath = import_node_path35.default.join(scanPath, "package.json");
5263
+ const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
5264
+ return JSON.parse(raw);
5265
+ }
5266
+ async function findHookFiles(scanPath) {
5267
+ const entries = await import_node_fs21.promises.readdir(scanPath);
5268
+ return entries.filter(
5269
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
5270
+ ).sort();
5271
+ }
5272
+ function extendLogPath() {
5273
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path35.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5274
+ }
5275
+ async function appendExtendLog(entry) {
5276
+ const logPath = extendLogPath();
5277
+ await import_node_fs21.promises.mkdir(import_node_path35.default.dirname(logPath), { recursive: true });
5278
+ await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
5279
+ }
5280
+ function splicedContent(fileContent, snippet2) {
5281
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
5282
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
5283
+ __INSTRUMENTATION_BLOCK__`);
5284
+ }
5285
+ const lines = fileContent.split("\n");
5286
+ let lastPushIdx = -1;
5287
+ for (let i = 0; i < lines.length; i++) {
5288
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
5289
+ }
5290
+ if (lastPushIdx >= 0) {
5291
+ lines.splice(lastPushIdx + 1, 0, snippet2);
5292
+ return lines.join("\n");
5293
+ }
5294
+ for (let i = 0; i < lines.length; i++) {
5295
+ if (lines[i].includes("new NodeSDK(")) {
5296
+ lines.splice(i, 0, snippet2);
5297
+ return lines.join("\n");
5298
+ }
5299
+ }
5300
+ return null;
5301
+ }
5302
+ async function listUninstrumented(ctx) {
5303
+ const pkg = await readPackageJson(ctx.scanPath);
5304
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5305
+ const results = [];
5306
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
5307
+ const entry = (0, import_instrumentation_registry.resolve)(library, installedVersion);
5308
+ if (!entry) continue;
5309
+ if (entry.coverage === "bundled" || entry.coverage === "http-only") continue;
5310
+ results.push({
5311
+ library,
5312
+ coverage: entry.coverage,
5313
+ installedVersion,
5314
+ instrumentation_package: entry.instrumentation_package,
5315
+ package_version: entry.package_version,
5316
+ registration: entry.registration,
5317
+ notes: entry.notes
5318
+ });
5319
+ }
5320
+ return results;
5321
+ }
5322
+ function lookupInstrumentation(library, installedVersion) {
5323
+ const entry = (0, import_instrumentation_registry.resolve)(library, installedVersion);
5324
+ if (!entry) return null;
5325
+ return {
5326
+ library: entry.library,
5327
+ coverage: entry.coverage,
5328
+ instrumentation_package: entry.instrumentation_package,
5329
+ package_version: entry.package_version,
5330
+ registration: entry.registration,
5331
+ notes: entry.notes
5332
+ };
5333
+ }
5334
+ async function describeProjectInstrumentation(ctx) {
5335
+ const hookFiles = await findHookFiles(ctx.scanPath);
5336
+ const envNeat = await fileExists2(import_node_path35.default.join(ctx.scanPath, ".env.neat"));
5337
+ const registryInstrPackages = new Set(
5338
+ (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5339
+ );
5340
+ const pkg = await readPackageJson(ctx.scanPath);
5341
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5342
+ const installedDeps = {};
5343
+ for (const [key, version] of Object.entries(allDeps)) {
5344
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
5345
+ installedDeps[key] = version;
5346
+ }
5347
+ }
5348
+ return { hookFiles, envNeat, installedDeps };
5349
+ }
5350
+ async function applyExtension(ctx, args, options) {
5351
+ const hookFiles = await findHookFiles(ctx.scanPath);
5352
+ if (hookFiles.length === 0) {
5353
+ throw new Error(
5354
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
5355
+ );
5356
+ }
5357
+ for (const file of hookFiles) {
5358
+ const content = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, file), "utf8");
5359
+ if (content.includes(args.registration_snippet)) {
5360
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5361
+ }
5362
+ }
5363
+ const primaryFile = hookFiles[0];
5364
+ const primaryPath = import_node_path35.default.join(ctx.scanPath, primaryFile);
5365
+ const filesTouched = [];
5366
+ const depsAdded = [];
5367
+ const pkgPath = import_node_path35.default.join(ctx.scanPath, "package.json");
5368
+ const pkg = await readPackageJson(ctx.scanPath);
5369
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5370
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
5371
+ await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5372
+ filesTouched.push("package.json");
5373
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
5374
+ }
5375
+ const hookContent = await import_node_fs21.promises.readFile(primaryPath, "utf8");
5376
+ const patched = splicedContent(hookContent, args.registration_snippet);
5377
+ if (!patched) {
5378
+ throw new Error(
5379
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
5380
+ );
5381
+ }
5382
+ await import_node_fs21.promises.writeFile(primaryPath, patched, "utf8");
5383
+ filesTouched.push(primaryFile);
5384
+ const cmd = await detectPackageManager(ctx.scanPath);
5385
+ const installer = options?.runInstall ?? runPackageManagerInstall;
5386
+ const install = await installer(cmd);
5387
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
5388
+ await appendExtendLog({
5389
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5390
+ project: ctx.project,
5391
+ library: args.library,
5392
+ instrumentation_package: args.instrumentation_package,
5393
+ version: args.version,
5394
+ registration_snippet: args.registration_snippet,
5395
+ filesTouched,
5396
+ depsAdded,
5397
+ installOutput
5398
+ });
5399
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
5400
+ }
5401
+ async function dryRunExtension(ctx, args) {
5402
+ const hookFiles = await findHookFiles(ctx.scanPath);
5403
+ if (hookFiles.length === 0) {
5404
+ return {
5405
+ library: args.library,
5406
+ filesTouched: [],
5407
+ depsToAdd: [],
5408
+ packageJsonPatch: {},
5409
+ templatePatch: "No hook files found. Run 'neat init' first."
5410
+ };
5411
+ }
5412
+ for (const file of hookFiles) {
5413
+ const content = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, file), "utf8");
5414
+ if (content.includes(args.registration_snippet)) {
5415
+ return {
5416
+ library: args.library,
5417
+ filesTouched: [],
5418
+ depsToAdd: [],
5419
+ packageJsonPatch: {},
5420
+ templatePatch: "Already applied \u2014 no changes would be made."
5421
+ };
5422
+ }
5423
+ }
5424
+ const primaryFile = hookFiles[0];
5425
+ const filesTouched = [];
5426
+ const depsToAdd = [];
5427
+ let packageJsonPatch = {};
5428
+ let templatePatch = "";
5429
+ const pkg = await readPackageJson(ctx.scanPath);
5430
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5431
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
5432
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5433
+ filesTouched.push("package.json");
5434
+ }
5435
+ const hookContent = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, primaryFile), "utf8");
5436
+ const patched = splicedContent(hookContent, args.registration_snippet);
5437
+ if (patched) {
5438
+ filesTouched.push(primaryFile);
5439
+ templatePatch = `+ ${args.registration_snippet}`;
5440
+ } else {
5441
+ templatePatch = "Could not find insertion point in hook file.";
5442
+ }
5443
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
5444
+ }
5445
+ async function rollbackExtension(ctx, args) {
5446
+ const logPath = extendLogPath();
5447
+ if (!await fileExists2(logPath)) {
5448
+ return { undone: false, message: "no apply found for library" };
5449
+ }
5450
+ const raw = await import_node_fs21.promises.readFile(logPath, "utf8");
5451
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5452
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
5453
+ if (!match) {
5454
+ return { undone: false, message: "no apply found for library" };
5455
+ }
5456
+ const pkgPath = import_node_path35.default.join(ctx.scanPath, "package.json");
5457
+ if (await fileExists2(pkgPath)) {
5458
+ const pkg = await readPackageJson(ctx.scanPath);
5459
+ if (pkg.dependencies?.[match.instrumentation_package]) {
5460
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
5461
+ pkg.dependencies = rest;
5462
+ await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5463
+ }
5464
+ }
5465
+ const hookFiles = await findHookFiles(ctx.scanPath);
5466
+ for (const file of hookFiles) {
5467
+ const filePath = import_node_path35.default.join(ctx.scanPath, file);
5468
+ const content = await import_node_fs21.promises.readFile(filePath, "utf8");
5469
+ if (content.includes(match.registration_snippet)) {
5470
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
5471
+ await import_node_fs21.promises.writeFile(filePath, filtered, "utf8");
5472
+ break;
5473
+ }
5474
+ }
5475
+ return {
5476
+ undone: true,
5477
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
5478
+ };
5479
+ }
4814
5480
 
4815
5481
  // src/divergences.ts
4816
5482
  init_cjs_shims();
4817
- var import_types21 = require("@neat.is/types");
5483
+ var import_types22 = require("@neat.is/types");
4818
5484
  function bucketKey(source, target, type) {
4819
5485
  return `${type}|${source}|${target}`;
4820
5486
  }
@@ -4822,22 +5488,22 @@ function bucketEdges(graph) {
4822
5488
  const buckets = /* @__PURE__ */ new Map();
4823
5489
  graph.forEachEdge((id, attrs) => {
4824
5490
  const e = attrs;
4825
- const parsed = (0, import_types21.parseEdgeId)(id);
5491
+ const parsed = (0, import_types22.parseEdgeId)(id);
4826
5492
  const provenance = parsed?.provenance ?? e.provenance;
4827
5493
  const key = bucketKey(e.source, e.target, e.type);
4828
5494
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4829
5495
  switch (provenance) {
4830
- case import_types21.Provenance.EXTRACTED:
5496
+ case import_types22.Provenance.EXTRACTED:
4831
5497
  cur.extracted = e;
4832
5498
  break;
4833
- case import_types21.Provenance.OBSERVED:
5499
+ case import_types22.Provenance.OBSERVED:
4834
5500
  cur.observed = e;
4835
5501
  break;
4836
- case import_types21.Provenance.INFERRED:
5502
+ case import_types22.Provenance.INFERRED:
4837
5503
  cur.inferred = e;
4838
5504
  break;
4839
5505
  default:
4840
- if (e.provenance === import_types21.Provenance.STALE) cur.stale = e;
5506
+ if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
4841
5507
  }
4842
5508
  buckets.set(key, cur);
4843
5509
  });
@@ -4846,7 +5512,7 @@ function bucketEdges(graph) {
4846
5512
  function nodeIsFrontier(graph, nodeId) {
4847
5513
  if (!graph.hasNode(nodeId)) return false;
4848
5514
  const attrs = graph.getNodeAttributes(nodeId);
4849
- return attrs.type === import_types21.NodeType.FrontierNode;
5515
+ return attrs.type === import_types22.NodeType.FrontierNode;
4850
5516
  }
4851
5517
  function clampConfidence(n) {
4852
5518
  if (!Number.isFinite(n)) return 0;
@@ -4867,7 +5533,7 @@ function gradedConfidence(edge) {
4867
5533
  }
4868
5534
  function detectMissingDivergences(graph, bucket) {
4869
5535
  const out = [];
4870
- if (bucket.type === import_types21.EdgeType.CONTAINS) return out;
5536
+ if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
4871
5537
  if (bucket.extracted && !bucket.observed) {
4872
5538
  if (!nodeIsFrontier(graph, bucket.target)) {
4873
5539
  out.push({
@@ -4908,7 +5574,7 @@ function declaredHostFor(svc) {
4908
5574
  function hasExtractedConfiguredBy(graph, svcId) {
4909
5575
  for (const edgeId of graph.outboundEdges(svcId)) {
4910
5576
  const e = graph.getEdgeAttributes(edgeId);
4911
- if (e.type === import_types21.EdgeType.CONFIGURED_BY && e.provenance === import_types21.Provenance.EXTRACTED) {
5577
+ if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
4912
5578
  return true;
4913
5579
  }
4914
5580
  }
@@ -4921,10 +5587,10 @@ function detectHostMismatch(graph, svcId, svc) {
4921
5587
  const out = [];
4922
5588
  for (const edgeId of graph.outboundEdges(svcId)) {
4923
5589
  const edge = graph.getEdgeAttributes(edgeId);
4924
- if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4925
- if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
5590
+ if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5591
+ if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
4926
5592
  const target = graph.getNodeAttributes(edge.target);
4927
- if (target.type !== import_types21.NodeType.DatabaseNode) continue;
5593
+ if (target.type !== import_types22.NodeType.DatabaseNode) continue;
4928
5594
  const observedHost = target.host?.trim();
4929
5595
  if (!observedHost) continue;
4930
5596
  if (observedHost === declaredHost) continue;
@@ -4946,10 +5612,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4946
5612
  const deps = svc.dependencies ?? {};
4947
5613
  for (const edgeId of graph.outboundEdges(svcId)) {
4948
5614
  const edge = graph.getEdgeAttributes(edgeId);
4949
- if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4950
- if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
5615
+ if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5616
+ if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
4951
5617
  const target = graph.getNodeAttributes(edge.target);
4952
- if (target.type !== import_types21.NodeType.DatabaseNode) continue;
5618
+ if (target.type !== import_types22.NodeType.DatabaseNode) continue;
4953
5619
  for (const pair of compatPairs()) {
4954
5620
  if (pair.engine !== target.engine) continue;
4955
5621
  const declared = deps[pair.driver];
@@ -5010,7 +5676,7 @@ function computeDivergences(graph, opts = {}) {
5010
5676
  }
5011
5677
  graph.forEachNode((nodeId, attrs) => {
5012
5678
  const n = attrs;
5013
- if (n.type !== import_types21.NodeType.ServiceNode) return;
5679
+ if (n.type !== import_types22.NodeType.ServiceNode) return;
5014
5680
  const svc = n;
5015
5681
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5016
5682
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5043,7 +5709,7 @@ function computeDivergences(graph, opts = {}) {
5043
5709
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5044
5710
  return a.target.localeCompare(b.target);
5045
5711
  });
5046
- return import_types21.DivergenceResultSchema.parse({
5712
+ return import_types22.DivergenceResultSchema.parse({
5047
5713
  divergences: filtered,
5048
5714
  totalAffected: filtered.length,
5049
5715
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5052,7 +5718,7 @@ function computeDivergences(graph, opts = {}) {
5052
5718
 
5053
5719
  // src/diff.ts
5054
5720
  init_cjs_shims();
5055
- var import_node_fs19 = require("fs");
5721
+ var import_node_fs22 = require("fs");
5056
5722
  async function loadSnapshotForDiff(target) {
5057
5723
  if (/^https?:\/\//i.test(target)) {
5058
5724
  const res = await fetch(target);
@@ -5061,7 +5727,7 @@ async function loadSnapshotForDiff(target) {
5061
5727
  }
5062
5728
  return await res.json();
5063
5729
  }
5064
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
5730
+ const raw = await import_node_fs22.promises.readFile(target, "utf8");
5065
5731
  return JSON.parse(raw);
5066
5732
  }
5067
5733
  function indexEntries(entries) {
@@ -5129,23 +5795,23 @@ function canonicalJson(value) {
5129
5795
 
5130
5796
  // src/projects.ts
5131
5797
  init_cjs_shims();
5132
- var import_node_path32 = __toESM(require("path"), 1);
5798
+ var import_node_path36 = __toESM(require("path"), 1);
5133
5799
  function pathsForProject(project, baseDir) {
5134
5800
  if (project === DEFAULT_PROJECT) {
5135
5801
  return {
5136
- snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
5137
- errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
5138
- staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
5139
- embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
5140
- policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
5802
+ snapshotPath: import_node_path36.default.join(baseDir, "graph.json"),
5803
+ errorsPath: import_node_path36.default.join(baseDir, "errors.ndjson"),
5804
+ staleEventsPath: import_node_path36.default.join(baseDir, "stale-events.ndjson"),
5805
+ embeddingsCachePath: import_node_path36.default.join(baseDir, "embeddings.json"),
5806
+ policyViolationsPath: import_node_path36.default.join(baseDir, "policy-violations.ndjson")
5141
5807
  };
5142
5808
  }
5143
5809
  return {
5144
- snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
5145
- errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
5146
- staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
5147
- embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
5148
- policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
5810
+ snapshotPath: import_node_path36.default.join(baseDir, `${project}.json`),
5811
+ errorsPath: import_node_path36.default.join(baseDir, `errors.${project}.ndjson`),
5812
+ staleEventsPath: import_node_path36.default.join(baseDir, `stale-events.${project}.ndjson`),
5813
+ embeddingsCachePath: import_node_path36.default.join(baseDir, `embeddings.${project}.json`),
5814
+ policyViolationsPath: import_node_path36.default.join(baseDir, `policy-violations.${project}.ndjson`)
5149
5815
  };
5150
5816
  }
5151
5817
  var Projects = class {
@@ -5181,25 +5847,25 @@ var Projects = class {
5181
5847
 
5182
5848
  // src/registry.ts
5183
5849
  init_cjs_shims();
5184
- var import_node_fs20 = require("fs");
5185
- var import_node_os2 = __toESM(require("os"), 1);
5186
- var import_node_path33 = __toESM(require("path"), 1);
5187
- var import_types22 = require("@neat.is/types");
5850
+ var import_node_fs23 = require("fs");
5851
+ var import_node_os3 = __toESM(require("os"), 1);
5852
+ var import_node_path37 = __toESM(require("path"), 1);
5853
+ var import_types23 = require("@neat.is/types");
5188
5854
  var LOCK_TIMEOUT_MS = 5e3;
5189
5855
  var LOCK_RETRY_MS = 50;
5190
5856
  function neatHome() {
5191
5857
  const override = process.env.NEAT_HOME;
5192
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
5193
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
5858
+ if (override && override.length > 0) return import_node_path37.default.resolve(override);
5859
+ return import_node_path37.default.join(import_node_os3.default.homedir(), ".neat");
5194
5860
  }
5195
5861
  function registryPath() {
5196
- return import_node_path33.default.join(neatHome(), "projects.json");
5862
+ return import_node_path37.default.join(neatHome(), "projects.json");
5197
5863
  }
5198
5864
  function registryLockPath() {
5199
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
5865
+ return import_node_path37.default.join(neatHome(), "projects.json.lock");
5200
5866
  }
5201
5867
  function daemonPidPath() {
5202
- return import_node_path33.default.join(neatHome(), "neatd.pid");
5868
+ return import_node_path37.default.join(neatHome(), "neatd.pid");
5203
5869
  }
5204
5870
  function isPidAliveDefault(pid) {
5205
5871
  try {
@@ -5211,7 +5877,7 @@ function isPidAliveDefault(pid) {
5211
5877
  }
5212
5878
  async function readPidFile(file) {
5213
5879
  try {
5214
- const raw = await import_node_fs20.promises.readFile(file, "utf8");
5880
+ const raw = await import_node_fs23.promises.readFile(file, "utf8");
5215
5881
  const pid = Number.parseInt(raw.trim(), 10);
5216
5882
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5217
5883
  } catch {
@@ -5259,32 +5925,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5259
5925
  }
5260
5926
  }
5261
5927
  async function normalizeProjectPath(input) {
5262
- const resolved = import_node_path33.default.resolve(input);
5928
+ const resolved = import_node_path37.default.resolve(input);
5263
5929
  try {
5264
- return await import_node_fs20.promises.realpath(resolved);
5930
+ return await import_node_fs23.promises.realpath(resolved);
5265
5931
  } catch {
5266
5932
  return resolved;
5267
5933
  }
5268
5934
  }
5269
5935
  async function writeAtomically(target, contents) {
5270
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
5936
+ await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
5271
5937
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5272
- const fd = await import_node_fs20.promises.open(tmp, "w");
5938
+ const fd = await import_node_fs23.promises.open(tmp, "w");
5273
5939
  try {
5274
5940
  await fd.writeFile(contents, "utf8");
5275
5941
  await fd.sync();
5276
5942
  } finally {
5277
5943
  await fd.close();
5278
5944
  }
5279
- await import_node_fs20.promises.rename(tmp, target);
5945
+ await import_node_fs23.promises.rename(tmp, target);
5280
5946
  }
5281
5947
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5282
5948
  const deadline = Date.now() + timeoutMs;
5283
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
5949
+ await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(lockPath), { recursive: true });
5284
5950
  let probedHolder = false;
5285
5951
  while (true) {
5286
5952
  try {
5287
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
5953
+ const fd = await import_node_fs23.promises.open(lockPath, "wx");
5288
5954
  try {
5289
5955
  await fd.writeFile(`${process.pid}
5290
5956
  `, "utf8");
@@ -5309,7 +5975,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
5309
5975
  }
5310
5976
  }
5311
5977
  async function releaseLock(lockPath) {
5312
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
5978
+ await import_node_fs23.promises.unlink(lockPath).catch(() => {
5313
5979
  });
5314
5980
  }
5315
5981
  async function withLock(fn) {
@@ -5325,7 +5991,7 @@ async function readRegistry() {
5325
5991
  const file = registryPath();
5326
5992
  let raw;
5327
5993
  try {
5328
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5994
+ raw = await import_node_fs23.promises.readFile(file, "utf8");
5329
5995
  } catch (err) {
5330
5996
  if (err.code === "ENOENT") {
5331
5997
  return { version: 1, projects: [] };
@@ -5333,10 +5999,10 @@ async function readRegistry() {
5333
5999
  throw err;
5334
6000
  }
5335
6001
  const parsed = JSON.parse(raw);
5336
- return import_types22.RegistryFileSchema.parse(parsed);
6002
+ return import_types23.RegistryFileSchema.parse(parsed);
5337
6003
  }
5338
6004
  async function writeRegistry(reg) {
5339
- const validated = import_types22.RegistryFileSchema.parse(reg);
6005
+ const validated = import_types23.RegistryFileSchema.parse(reg);
5340
6006
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5341
6007
  }
5342
6008
  var ProjectNameCollisionError = class extends Error {
@@ -5608,11 +6274,11 @@ function registerRoutes(scope, ctx) {
5608
6274
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5609
6275
  const parsed = [];
5610
6276
  for (const c of candidates) {
5611
- const r = import_types23.DivergenceTypeSchema.safeParse(c);
6277
+ const r = import_types24.DivergenceTypeSchema.safeParse(c);
5612
6278
  if (!r.success) {
5613
6279
  return reply.code(400).send({
5614
6280
  error: `unknown divergence type "${c}"`,
5615
- allowed: import_types23.DivergenceTypeSchema.options
6281
+ allowed: import_types24.DivergenceTypeSchema.options
5616
6282
  });
5617
6283
  }
5618
6284
  parsed.push(r.data);
@@ -5825,7 +6491,7 @@ function registerRoutes(scope, ctx) {
5825
6491
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5826
6492
  let violations = await log.readAll();
5827
6493
  if (req.query.severity) {
5828
- const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
6494
+ const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
5829
6495
  if (!sev.success) {
5830
6496
  return reply.code(400).send({
5831
6497
  error: "invalid severity",
@@ -5842,7 +6508,7 @@ function registerRoutes(scope, ctx) {
5842
6508
  scope.post("/policies/check", async (req, reply) => {
5843
6509
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5844
6510
  if (!proj) return;
5845
- const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6511
+ const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5846
6512
  if (!parsed.success) {
5847
6513
  return reply.code(400).send({
5848
6514
  error: "invalid /policies/check body",
@@ -5875,6 +6541,105 @@ function registerRoutes(scope, ctx) {
5875
6541
  violations
5876
6542
  };
5877
6543
  });
6544
+ scope.get("/extend/list-uninstrumented", async (req, reply) => {
6545
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6546
+ if (!proj) return;
6547
+ if (!proj.scanPath) {
6548
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6549
+ }
6550
+ try {
6551
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
6552
+ return { libraries: results };
6553
+ } catch (err) {
6554
+ return reply.code(500).send({ error: err.message });
6555
+ }
6556
+ });
6557
+ scope.get("/extend/lookup", async (req, reply) => {
6558
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6559
+ if (!proj) return;
6560
+ const { library, version } = req.query;
6561
+ if (!library) {
6562
+ return reply.code(400).send({ error: "query parameter `library` is required" });
6563
+ }
6564
+ const result = lookupInstrumentation(library, version);
6565
+ if (!result) {
6566
+ return reply.code(404).send({ error: "library not found in registry", library });
6567
+ }
6568
+ return result;
6569
+ });
6570
+ scope.get("/extend/describe", async (req, reply) => {
6571
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6572
+ if (!proj) return;
6573
+ if (!proj.scanPath) {
6574
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6575
+ }
6576
+ try {
6577
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
6578
+ return state;
6579
+ } catch (err) {
6580
+ return reply.code(500).send({ error: err.message });
6581
+ }
6582
+ });
6583
+ scope.post("/extend/apply", async (req, reply) => {
6584
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6585
+ if (!proj) return;
6586
+ if (!proj.scanPath) {
6587
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6588
+ }
6589
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6590
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6591
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6592
+ }
6593
+ try {
6594
+ const result = await applyExtension(
6595
+ { project: proj.name, scanPath: proj.scanPath },
6596
+ { library, instrumentation_package, version, registration_snippet }
6597
+ );
6598
+ return result;
6599
+ } catch (err) {
6600
+ return reply.code(500).send({ error: err.message });
6601
+ }
6602
+ });
6603
+ scope.post("/extend/dry-run", async (req, reply) => {
6604
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6605
+ if (!proj) return;
6606
+ if (!proj.scanPath) {
6607
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6608
+ }
6609
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6610
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6611
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6612
+ }
6613
+ try {
6614
+ const result = await dryRunExtension(
6615
+ { project: proj.name, scanPath: proj.scanPath },
6616
+ { library, instrumentation_package, version, registration_snippet }
6617
+ );
6618
+ return result;
6619
+ } catch (err) {
6620
+ return reply.code(500).send({ error: err.message });
6621
+ }
6622
+ });
6623
+ scope.post("/extend/rollback", async (req, reply) => {
6624
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6625
+ if (!proj) return;
6626
+ if (!proj.scanPath) {
6627
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6628
+ }
6629
+ const { library } = req.body ?? {};
6630
+ if (!library) {
6631
+ return reply.code(400).send({ error: "body must include library" });
6632
+ }
6633
+ try {
6634
+ const result = await rollbackExtension(
6635
+ { project: proj.name, scanPath: proj.scanPath },
6636
+ { library }
6637
+ );
6638
+ return result;
6639
+ } catch (err) {
6640
+ return reply.code(500).send({ error: err.message });
6641
+ }
6642
+ });
5878
6643
  }
5879
6644
  async function buildApi(opts) {
5880
6645
  const app = (0, import_fastify.default)({ logger: false });
@@ -5985,15 +6750,15 @@ init_otel_grpc();
5985
6750
 
5986
6751
  // src/daemon.ts
5987
6752
  init_cjs_shims();
5988
- var import_node_fs22 = require("fs");
5989
- var import_node_path37 = __toESM(require("path"), 1);
6753
+ var import_node_fs25 = require("fs");
6754
+ var import_node_path41 = __toESM(require("path"), 1);
5990
6755
  init_otel();
5991
6756
  init_auth();
5992
6757
 
5993
6758
  // src/unrouted.ts
5994
6759
  init_cjs_shims();
5995
- var import_node_fs21 = require("fs");
5996
- var import_node_path36 = __toESM(require("path"), 1);
6760
+ var import_node_fs24 = require("fs");
6761
+ var import_node_path40 = __toESM(require("path"), 1);
5997
6762
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
5998
6763
  return {
5999
6764
  timestamp: now.toISOString(),
@@ -6003,21 +6768,21 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
6003
6768
  };
6004
6769
  }
6005
6770
  async function appendUnroutedSpan(neatHome2, record) {
6006
- const target = import_node_path36.default.join(neatHome2, "errors.ndjson");
6007
- await import_node_fs21.promises.mkdir(neatHome2, { recursive: true });
6008
- await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6771
+ const target = import_node_path40.default.join(neatHome2, "errors.ndjson");
6772
+ await import_node_fs24.promises.mkdir(neatHome2, { recursive: true });
6773
+ await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6009
6774
  }
6010
6775
  function unroutedErrorsPath(neatHome2) {
6011
- return import_node_path36.default.join(neatHome2, "errors.ndjson");
6776
+ return import_node_path40.default.join(neatHome2, "errors.ndjson");
6012
6777
  }
6013
6778
 
6014
6779
  // src/daemon.ts
6015
6780
  function neatHomeFor(opts) {
6016
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
6781
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path41.default.resolve(opts.neatHome);
6017
6782
  const env = process.env.NEAT_HOME;
6018
- if (env && env.length > 0) return import_node_path37.default.resolve(env);
6783
+ if (env && env.length > 0) return import_node_path41.default.resolve(env);
6019
6784
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6020
- return import_node_path37.default.join(home, ".neat");
6785
+ return import_node_path41.default.join(home, ".neat");
6021
6786
  }
6022
6787
  function routeSpanToProject(serviceName, projects) {
6023
6788
  if (!serviceName) return DEFAULT_PROJECT;
@@ -6052,9 +6817,9 @@ function isTokenContained(needle, haystack) {
6052
6817
  return tokens.includes(needle);
6053
6818
  }
6054
6819
  async function bootstrapProject(entry) {
6055
- const paths = pathsForProject(entry.name, import_node_path37.default.join(entry.path, "neat-out"));
6820
+ const paths = pathsForProject(entry.name, import_node_path41.default.join(entry.path, "neat-out"));
6056
6821
  try {
6057
- const stat = await import_node_fs22.promises.stat(entry.path);
6822
+ const stat = await import_node_fs25.promises.stat(entry.path);
6058
6823
  if (!stat.isDirectory()) {
6059
6824
  throw new Error(`registered path ${entry.path} is not a directory`);
6060
6825
  }
@@ -6120,13 +6885,13 @@ async function startDaemon(opts = {}) {
6120
6885
  const home = neatHomeFor(opts);
6121
6886
  const regPath = registryPath();
6122
6887
  try {
6123
- await import_node_fs22.promises.access(regPath);
6888
+ await import_node_fs25.promises.access(regPath);
6124
6889
  } catch {
6125
6890
  throw new Error(
6126
6891
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
6127
6892
  );
6128
6893
  }
6129
- const pidPath = import_node_path37.default.join(home, "neatd.pid");
6894
+ const pidPath = import_node_path41.default.join(home, "neatd.pid");
6130
6895
  await writeAtomically(pidPath, `${process.pid}
6131
6896
  `);
6132
6897
  const slots = /* @__PURE__ */ new Map();
@@ -6284,7 +7049,7 @@ async function startDaemon(opts = {}) {
6284
7049
  }
6285
7050
  if (restApp) await restApp.close().catch(() => {
6286
7051
  });
6287
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
7052
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
6288
7053
  });
6289
7054
  throw new Error(
6290
7055
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -6390,7 +7155,7 @@ async function startDaemon(opts = {}) {
6390
7155
  });
6391
7156
  if (otlpApp) await otlpApp.close().catch(() => {
6392
7157
  });
6393
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
7158
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
6394
7159
  });
6395
7160
  throw new Error(
6396
7161
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -6436,9 +7201,9 @@ async function startDaemon(opts = {}) {
6436
7201
  let registryWatcher = null;
6437
7202
  let reloadTimer = null;
6438
7203
  try {
6439
- const regDir = import_node_path37.default.dirname(regPath);
6440
- const regBase = import_node_path37.default.basename(regPath);
6441
- registryWatcher = (0, import_node_fs22.watch)(regDir, (_eventType, filename) => {
7204
+ const regDir = import_node_path41.default.dirname(regPath);
7205
+ const regBase = import_node_path41.default.basename(regPath);
7206
+ registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
6442
7207
  if (filename !== null && filename !== regBase) return;
6443
7208
  if (reloadTimer) clearTimeout(reloadTimer);
6444
7209
  reloadTimer = setTimeout(() => {
@@ -6481,7 +7246,7 @@ async function startDaemon(opts = {}) {
6481
7246
  } catch {
6482
7247
  }
6483
7248
  }
6484
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
7249
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
6485
7250
  });
6486
7251
  };
6487
7252
  return {