@neat.is/core 0.4.26-dev.20260702 → 0.4.26

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 path43 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path46 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path43 === suffix || path43.endsWith(suffix)) {
62
+ if (path46 === suffix || path46.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -70,11 +70,13 @@ function mountBearerAuth(app, opts) {
70
70
  }
71
71
  const header = req.headers.authorization;
72
72
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
73
+ opts.onReject?.();
73
74
  void reply.code(401).send({ error: "unauthorized" });
74
75
  return;
75
76
  }
76
77
  const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
77
78
  if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
79
+ opts.onReject?.();
78
80
  void reply.code(401).send({ error: "unauthorized" });
79
81
  return;
80
82
  }
@@ -187,8 +189,8 @@ function reshapeGrpcRequest(req) {
187
189
  };
188
190
  }
189
191
  function resolveProtoRoot() {
190
- const here = import_node_path39.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
191
- return import_node_path39.default.resolve(here, "..", "proto");
192
+ const here = import_node_path42.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
193
+ return import_node_path42.default.resolve(here, "..", "proto");
192
194
  }
193
195
  function loadTraceService() {
194
196
  const protoRoot = resolveProtoRoot();
@@ -256,13 +258,13 @@ async function startOtelGrpcReceiver(opts) {
256
258
  })
257
259
  };
258
260
  }
259
- var import_node_url, import_node_path39, import_node_crypto2, grpc, protoLoader;
261
+ var import_node_url, import_node_path42, import_node_crypto2, grpc, protoLoader;
260
262
  var init_otel_grpc = __esm({
261
263
  "src/otel-grpc.ts"() {
262
264
  "use strict";
263
265
  init_cjs_shims();
264
266
  import_node_url = require("url");
265
- import_node_path39 = __toESM(require("path"), 1);
267
+ import_node_path42 = __toESM(require("path"), 1);
266
268
  import_node_crypto2 = require("crypto");
267
269
  grpc = __toESM(require("@grpc/grpc-js"), 1);
268
270
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -335,6 +337,36 @@ function pickEnv(spanAttrs, resourceAttrs) {
335
337
  }
336
338
  return ENV_FALLBACK;
337
339
  }
340
+ function messagingDestinationOf(attrs) {
341
+ for (const key of ["messaging.destination.name", "messaging.destination"]) {
342
+ const v = attrs[key];
343
+ if (typeof v === "string" && v.length > 0) return v;
344
+ }
345
+ return void 0;
346
+ }
347
+ function hasWebsocketUpgradeHeader(attrs) {
348
+ const v = attrs["http.request.header.upgrade"];
349
+ const matches = (s) => typeof s === "string" && s.trim().toLowerCase() === "websocket";
350
+ if (Array.isArray(v)) return v.some(matches);
351
+ return matches(v);
352
+ }
353
+ function websocketChannelPathOf(attrs) {
354
+ const route = attrs["http.route"];
355
+ if (typeof route === "string" && route.length > 0) return route;
356
+ for (const key of ["url.path", "http.target"]) {
357
+ const v = attrs[key];
358
+ if (typeof v === "string" && v.length > 0) {
359
+ const q = v.indexOf("?");
360
+ const path46 = q === -1 ? v : v.slice(0, q);
361
+ if (path46.length > 0) return path46;
362
+ }
363
+ }
364
+ return void 0;
365
+ }
366
+ function websocketChannelOf(attrs) {
367
+ if (!hasWebsocketUpgradeHeader(attrs)) return void 0;
368
+ return websocketChannelPathOf(attrs);
369
+ }
338
370
  function parseOtlpRequest(body) {
339
371
  const out = [];
340
372
  for (const rs of body.resourceSpans ?? []) {
@@ -361,6 +393,14 @@ function parseOtlpRequest(body) {
361
393
  attributes: attrs,
362
394
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
363
395
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
396
+ messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
397
+ messagingDestination: messagingDestinationOf(attrs),
398
+ graphqlOperationName: typeof attrs["graphql.operation.name"] === "string" && attrs["graphql.operation.name"].length > 0 ? attrs["graphql.operation.name"] : void 0,
399
+ graphqlOperationType: typeof attrs["graphql.operation.type"] === "string" && attrs["graphql.operation.type"].length > 0 ? attrs["graphql.operation.type"] : void 0,
400
+ rpcSystem: typeof attrs["rpc.system"] === "string" && attrs["rpc.system"].length > 0 ? attrs["rpc.system"] : void 0,
401
+ rpcService: typeof attrs["rpc.service"] === "string" && attrs["rpc.service"].length > 0 ? attrs["rpc.service"] : void 0,
402
+ rpcMethod: typeof attrs["rpc.method"] === "string" && attrs["rpc.method"].length > 0 ? attrs["rpc.method"] : void 0,
403
+ websocketChannel: websocketChannelOf(attrs),
364
404
  statusCode: span.status?.code,
365
405
  errorMessage: span.status?.message,
366
406
  exception: extractExceptionFromEvents(span.events)
@@ -372,10 +412,10 @@ function parseOtlpRequest(body) {
372
412
  return out;
373
413
  }
374
414
  function loadProtoRoot() {
375
- const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
376
- const protoRoot = import_node_path40.default.resolve(here, "..", "proto");
415
+ const here = import_node_path43.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
416
+ const protoRoot = import_node_path43.default.resolve(here, "..", "proto");
377
417
  const root = new import_protobufjs.default.Root();
378
- root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
418
+ root.resolvePath = (_origin, target) => import_node_path43.default.resolve(protoRoot, target);
379
419
  root.loadSync(
380
420
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
381
421
  { keepCase: true }
@@ -420,7 +460,21 @@ async function buildOtelReceiver(opts) {
420
460
  logger: false,
421
461
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
422
462
  });
423
- mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
463
+ const REJECT_WARN_INTERVAL_MS = 6e4;
464
+ let lastRejectWarnAt = 0;
465
+ const warnRejectedOtlp = () => {
466
+ const now = Date.now();
467
+ if (now - lastRejectWarnAt < REJECT_WARN_INTERVAL_MS) return;
468
+ lastRejectWarnAt = now;
469
+ console.warn(
470
+ "[neatd] rejecting OTLP spans on /v1/traces \u2014 missing or invalid bearer token (set NEAT_OTEL_TOKEN on the instrumented app)"
471
+ );
472
+ };
473
+ mountBearerAuth(app, {
474
+ token: opts.authToken,
475
+ trustProxy: opts.trustProxy,
476
+ onReject: warnRejectedOtlp
477
+ });
424
478
  const queue = [];
425
479
  let draining = false;
426
480
  let drainPromise = Promise.resolve();
@@ -597,12 +651,12 @@ function logSpanHandler(span) {
597
651
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
598
652
  );
599
653
  }
600
- var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
654
+ var import_node_path43, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
601
655
  var init_otel = __esm({
602
656
  "src/otel.ts"() {
603
657
  "use strict";
604
658
  init_cjs_shims();
605
- import_node_path40 = __toESM(require("path"), 1);
659
+ import_node_path43 = __toESM(require("path"), 1);
606
660
  import_node_url2 = require("url");
607
661
  import_fastify2 = __toESM(require("fastify"), 1);
608
662
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -1192,19 +1246,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1192
1246
  function longestIncomingWalk(graph, start, maxDepth) {
1193
1247
  let best = { path: [start], edges: [] };
1194
1248
  const visited = /* @__PURE__ */ new Set([start]);
1195
- function step(node, path43, edges) {
1196
- if (path43.length > best.path.length) {
1197
- best = { path: [...path43], edges: [...edges] };
1249
+ function step(node, path46, edges) {
1250
+ if (path46.length > best.path.length) {
1251
+ best = { path: [...path46], edges: [...edges] };
1198
1252
  }
1199
- if (path43.length - 1 >= maxDepth) return;
1253
+ if (path46.length - 1 >= maxDepth) return;
1200
1254
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1201
1255
  for (const [srcId, edge] of incoming) {
1202
1256
  if (visited.has(srcId)) continue;
1203
1257
  visited.add(srcId);
1204
- path43.push(srcId);
1258
+ path46.push(srcId);
1205
1259
  edges.push(edge);
1206
- step(srcId, path43, edges);
1207
- path43.pop();
1260
+ step(srcId, path46, edges);
1261
+ path46.pop();
1208
1262
  edges.pop();
1209
1263
  visited.delete(srcId);
1210
1264
  }
@@ -1212,14 +1266,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
1212
1266
  step(start, [start], []);
1213
1267
  return best;
1214
1268
  }
1215
- function databaseRootCauseShape(graph, origin, walk) {
1269
+ function databaseRootCauseShape(graph, origin, walk3) {
1216
1270
  const targetDb = origin;
1217
1271
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1218
1272
  if (candidatePairs.length === 0) return null;
1219
- for (const id of walk.path) {
1273
+ for (const id of walk3.path) {
1220
1274
  const owner = resolveOwningService(graph, id);
1221
1275
  if (!owner) continue;
1222
- const { id: serviceId3, svc } = owner;
1276
+ const { id: serviceId4, svc } = owner;
1223
1277
  const deps = svc.dependencies ?? {};
1224
1278
  for (const pair of candidatePairs) {
1225
1279
  const declared = deps[pair.driver];
@@ -1232,7 +1286,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1232
1286
  );
1233
1287
  if (!result.compatible) {
1234
1288
  return {
1235
- rootCauseNode: serviceId3,
1289
+ rootCauseNode: serviceId4,
1236
1290
  rootCauseReason: result.reason ?? "incompatible driver",
1237
1291
  ...result.minDriverVersion ? {
1238
1292
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1243,11 +1297,11 @@ function databaseRootCauseShape(graph, origin, walk) {
1243
1297
  }
1244
1298
  return null;
1245
1299
  }
1246
- function serviceRootCauseShape(graph, _origin, walk) {
1247
- for (const id of walk.path) {
1300
+ function serviceRootCauseShape(graph, _origin, walk3) {
1301
+ for (const id of walk3.path) {
1248
1302
  const owner = resolveOwningService(graph, id);
1249
1303
  if (!owner) continue;
1250
- const { id: serviceId3, svc } = owner;
1304
+ const { id: serviceId4, svc } = owner;
1251
1305
  const deps = svc.dependencies ?? {};
1252
1306
  const serviceNodeEngine = svc.nodeEngine;
1253
1307
  for (const constraint of nodeEngineConstraints()) {
@@ -1256,7 +1310,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1256
1310
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1257
1311
  if (!result.compatible && result.reason) {
1258
1312
  return {
1259
- rootCauseNode: serviceId3,
1313
+ rootCauseNode: serviceId4,
1260
1314
  rootCauseReason: result.reason,
1261
1315
  ...result.requiredNodeVersion ? {
1262
1316
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1271,7 +1325,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1271
1325
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1272
1326
  if (!result.compatible && result.reason) {
1273
1327
  return {
1274
- rootCauseNode: serviceId3,
1328
+ rootCauseNode: serviceId4,
1275
1329
  rootCauseReason: result.reason,
1276
1330
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1277
1331
  };
@@ -1280,10 +1334,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
1280
1334
  }
1281
1335
  return null;
1282
1336
  }
1283
- function fileRootCauseShape(graph, origin, walk) {
1337
+ function fileRootCauseShape(graph, origin, walk3) {
1284
1338
  const owner = resolveOwningService(graph, origin.id);
1285
1339
  if (!owner) return null;
1286
- return serviceRootCauseShape(graph, owner.svc, walk);
1340
+ return serviceRootCauseShape(graph, owner.svc, walk3);
1287
1341
  }
1288
1342
  var rootCauseShapes = {
1289
1343
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
@@ -1295,16 +1349,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1295
1349
  const origin = graph.getNodeAttributes(errorNodeId);
1296
1350
  const shape = rootCauseShapes[origin.type];
1297
1351
  if (shape) {
1298
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1299
- const match = shape(graph, origin, walk);
1352
+ const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1353
+ const match = shape(graph, origin, walk3);
1300
1354
  if (match) {
1301
1355
  const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1302
1356
  return import_types.RootCauseResultSchema.parse({
1303
1357
  rootCauseNode: match.rootCauseNode,
1304
1358
  rootCauseReason: reason,
1305
- traversalPath: walk.path,
1306
- edgeProvenances: walk.edges.map((e) => e.provenance),
1307
- confidence: confidenceFromMix(walk.edges),
1359
+ traversalPath: walk3.path,
1360
+ edgeProvenances: walk3.edges.map((e) => e.provenance),
1361
+ confidence: confidenceFromMix(walk3.edges),
1308
1362
  fixRecommendation: match.fixRecommendation
1309
1363
  });
1310
1364
  }
@@ -1362,9 +1416,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1362
1416
  function isFailingCallEdge(e) {
1363
1417
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1364
1418
  }
1365
- function callSourcesForService(graph, serviceId3) {
1366
- const ids = [serviceId3];
1367
- for (const edgeId of graph.outboundEdges(serviceId3)) {
1419
+ function callSourcesForService(graph, serviceId4) {
1420
+ const ids = [serviceId4];
1421
+ for (const edgeId of graph.outboundEdges(serviceId4)) {
1368
1422
  const e = graph.getEdgeAttributes(edgeId);
1369
1423
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1370
1424
  const tgt = graph.getNodeAttributes(e.target);
@@ -1381,9 +1435,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1381
1435
  }
1382
1436
  return id < curId;
1383
1437
  }
1384
- function dominantFailingCall(graph, serviceId3, visited) {
1438
+ function dominantFailingCall(graph, serviceId4, visited) {
1385
1439
  let best = null;
1386
- for (const src of callSourcesForService(graph, serviceId3)) {
1440
+ for (const src of callSourcesForService(graph, serviceId4)) {
1387
1441
  for (const edgeId of graph.outboundEdges(src)) {
1388
1442
  const e = graph.getEdgeAttributes(edgeId);
1389
1443
  if (!isFailingCallEdge(e)) continue;
@@ -1398,26 +1452,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
1398
1452
  return best;
1399
1453
  }
1400
1454
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1401
- const path43 = [originServiceId];
1455
+ const path46 = [originServiceId];
1402
1456
  const edges = [];
1403
1457
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1404
1458
  let current = originServiceId;
1405
1459
  for (let depth = 0; depth < maxDepth; depth++) {
1406
1460
  const hop = dominantFailingCall(graph, current, visited);
1407
1461
  if (!hop) break;
1408
- path43.push(hop.nextService);
1462
+ path46.push(hop.nextService);
1409
1463
  edges.push(hop.edge);
1410
1464
  visited.add(hop.nextService);
1411
1465
  current = hop.nextService;
1412
1466
  }
1413
1467
  if (edges.length === 0) return null;
1414
- return { path: path43, edges, culprit: current };
1468
+ return { path: path46, edges, culprit: current };
1415
1469
  }
1416
1470
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1417
1471
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1418
1472
  if (!chain) return null;
1419
1473
  const culprit = chain.culprit;
1420
- const path43 = [...chain.path];
1474
+ const path46 = [...chain.path];
1421
1475
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1422
1476
  const baseConfidence = confidenceFromMix(chain.edges);
1423
1477
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1425,14 +1479,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1425
1479
  if (loc) {
1426
1480
  let rootCauseNode = culprit;
1427
1481
  if (loc.fileNode) {
1428
- path43.push(loc.fileNode);
1482
+ path46.push(loc.fileNode);
1429
1483
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1430
1484
  rootCauseNode = loc.fileNode;
1431
1485
  }
1432
1486
  return import_types.RootCauseResultSchema.parse({
1433
1487
  rootCauseNode,
1434
1488
  rootCauseReason: loc.rootCauseReason,
1435
- traversalPath: path43,
1489
+ traversalPath: path46,
1436
1490
  edgeProvenances,
1437
1491
  confidence,
1438
1492
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1444,7 +1498,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1444
1498
  return import_types.RootCauseResultSchema.parse({
1445
1499
  rootCauseNode: culprit,
1446
1500
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1447
- traversalPath: path43,
1501
+ traversalPath: path46,
1448
1502
  edgeProvenances,
1449
1503
  confidence,
1450
1504
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2368,10 +2422,82 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2368
2422
  ]);
2369
2423
  var WIRE_SPAN_KIND_CLIENT = 3;
2370
2424
  var WIRE_SPAN_KIND_PRODUCER = 4;
2425
+ var WIRE_SPAN_KIND_CONSUMER = 5;
2371
2426
  function spanMintsObservedEdge(kind) {
2372
2427
  if (kind === void 0 || kind === 0) return true;
2373
2428
  return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
2374
2429
  }
2430
+ function spanMintsMessagingEdge(kind) {
2431
+ return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
2432
+ }
2433
+ function spanServesGraphqlOperation(kind) {
2434
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2435
+ }
2436
+ function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
2437
+ const id = (0, import_types3.graphqlOperationId)(serviceName, operationType, operationName);
2438
+ if (graph.hasNode(id)) return id;
2439
+ const node = {
2440
+ id,
2441
+ type: import_types3.NodeType.GraphQLOperationNode,
2442
+ name: operationName,
2443
+ service: serviceName,
2444
+ operationType: operationType.toLowerCase(),
2445
+ operationName,
2446
+ discoveredVia: "otel"
2447
+ };
2448
+ graph.addNode(id, node);
2449
+ return id;
2450
+ }
2451
+ function spanServesGrpcMethod(kind) {
2452
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2453
+ }
2454
+ function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
2455
+ const id = (0, import_types3.grpcMethodId)(rpcService, rpcMethod);
2456
+ if (graph.hasNode(id)) return id;
2457
+ const node = {
2458
+ id,
2459
+ type: import_types3.NodeType.GrpcMethodNode,
2460
+ name: `${rpcService}/${rpcMethod}`,
2461
+ rpcService,
2462
+ rpcMethod,
2463
+ discoveredVia: "otel"
2464
+ };
2465
+ graph.addNode(id, node);
2466
+ return id;
2467
+ }
2468
+ function spanServesWebsocketChannel(kind) {
2469
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2470
+ }
2471
+ function ensureWebsocketChannelNode(graph, serviceName, channel) {
2472
+ const id = (0, import_types3.websocketChannelId)(serviceName, channel);
2473
+ if (graph.hasNode(id)) return id;
2474
+ const node = {
2475
+ id,
2476
+ type: import_types3.NodeType.WebSocketChannelNode,
2477
+ name: channel,
2478
+ service: serviceName,
2479
+ channel,
2480
+ discoveredVia: "otel"
2481
+ };
2482
+ graph.addNode(id, node);
2483
+ return id;
2484
+ }
2485
+ function messagingDestinationKind(system) {
2486
+ return `${system}-topic`;
2487
+ }
2488
+ function ensureMessagingDestinationNode(graph, system, destination) {
2489
+ const id = (0, import_types3.infraId)(messagingDestinationKind(system), destination);
2490
+ if (graph.hasNode(id)) return id;
2491
+ const node = {
2492
+ id,
2493
+ type: import_types3.NodeType.InfraNode,
2494
+ name: destination,
2495
+ provider: "self",
2496
+ kind: messagingDestinationKind(system)
2497
+ };
2498
+ graph.addNode(id, node);
2499
+ return id;
2500
+ }
2375
2501
  var PARENT_SPAN_CACHE_SIZE = 1e4;
2376
2502
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
2377
2503
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -2465,6 +2591,21 @@ function ensureDatabaseNode(graph, host, engine) {
2465
2591
  graph.addNode(id, node);
2466
2592
  return id;
2467
2593
  }
2594
+ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
2595
+ const id = (0, import_types3.localDatabaseId)(serviceName, name);
2596
+ if (graph.hasNode(id)) return id;
2597
+ const node = {
2598
+ id,
2599
+ type: import_types3.NodeType.DatabaseNode,
2600
+ name,
2601
+ engine,
2602
+ engineVersion: "unknown",
2603
+ compatibleDrivers: [],
2604
+ discoveredVia: "otel"
2605
+ };
2606
+ graph.addNode(id, node);
2607
+ return id;
2608
+ }
2468
2609
  function ensureFrontierNode(graph, host, ts) {
2469
2610
  const id = frontierIdFor(host);
2470
2611
  if (graph.hasNode(id)) {
@@ -2716,10 +2857,21 @@ async function handleSpan(ctx, span) {
2716
2857
  let affectedNode = sourceId;
2717
2858
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2718
2859
  if (span.dbSystem) {
2719
- const host = pickAddress(span);
2720
- if (mintsFromCallerSide && host) {
2721
- ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2722
- const targetId = (0, import_types3.databaseId)(host);
2860
+ if (mintsFromCallerSide) {
2861
+ const host = pickAddress(span);
2862
+ let targetId;
2863
+ if (host) {
2864
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2865
+ targetId = (0, import_types3.databaseId)(host);
2866
+ } else {
2867
+ const localName = span.dbName ?? span.dbSystem;
2868
+ targetId = ensureLocalDatabaseNode(
2869
+ ctx.graph,
2870
+ span.service,
2871
+ localName,
2872
+ span.dbSystem
2873
+ );
2874
+ }
2723
2875
  const result = upsertObservedEdge(
2724
2876
  ctx.graph,
2725
2877
  import_types3.EdgeType.CONNECTS_TO,
@@ -2731,6 +2883,68 @@ async function handleSpan(ctx, span) {
2731
2883
  );
2732
2884
  if (result) affectedNode = targetId;
2733
2885
  }
2886
+ } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
2887
+ const targetId = ensureMessagingDestinationNode(
2888
+ ctx.graph,
2889
+ span.messagingSystem,
2890
+ span.messagingDestination
2891
+ );
2892
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types3.EdgeType.CONSUMES_FROM : import_types3.EdgeType.PUBLISHES_TO;
2893
+ const result = upsertObservedEdge(
2894
+ ctx.graph,
2895
+ edgeType,
2896
+ observedSource(),
2897
+ targetId,
2898
+ ts,
2899
+ isError,
2900
+ callSiteEvidence
2901
+ );
2902
+ if (result) affectedNode = targetId;
2903
+ } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
2904
+ const targetId = ensureGraphqlOperationNode(
2905
+ ctx.graph,
2906
+ span.service,
2907
+ span.graphqlOperationType,
2908
+ span.graphqlOperationName
2909
+ );
2910
+ const result = upsertObservedEdge(
2911
+ ctx.graph,
2912
+ import_types3.EdgeType.CONTAINS,
2913
+ observedSource(),
2914
+ targetId,
2915
+ ts,
2916
+ isError,
2917
+ callSiteEvidence
2918
+ );
2919
+ if (result) affectedNode = targetId;
2920
+ } else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
2921
+ const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
2922
+ const result = upsertObservedEdge(
2923
+ ctx.graph,
2924
+ import_types3.EdgeType.CONTAINS,
2925
+ observedSource(),
2926
+ targetId,
2927
+ ts,
2928
+ isError,
2929
+ callSiteEvidence
2930
+ );
2931
+ if (result) affectedNode = targetId;
2932
+ } else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
2933
+ const targetId = ensureWebsocketChannelNode(
2934
+ ctx.graph,
2935
+ span.service,
2936
+ span.websocketChannel
2937
+ );
2938
+ const result = upsertObservedEdge(
2939
+ ctx.graph,
2940
+ import_types3.EdgeType.CONNECTS_TO,
2941
+ observedSource(),
2942
+ targetId,
2943
+ ts,
2944
+ isError,
2945
+ callSiteEvidence
2946
+ );
2947
+ if (result) affectedNode = targetId;
2734
2948
  } else {
2735
2949
  const host = pickAddress(span);
2736
2950
  let resolvedViaAddress = false;
@@ -2840,29 +3054,29 @@ function promoteFrontierNodes(graph, opts = {}) {
2840
3054
  toPromote.push({ frontierId: id, serviceId: target });
2841
3055
  });
2842
3056
  let promoted = 0;
2843
- for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
3057
+ for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
2844
3058
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
2845
3059
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
2846
3060
  if (!gate.allowed) {
2847
3061
  continue;
2848
3062
  }
2849
3063
  }
2850
- rewireFrontierEdges(graph, frontierId2, serviceId3);
3064
+ rewireFrontierEdges(graph, frontierId2, serviceId4);
2851
3065
  graph.dropNode(frontierId2);
2852
3066
  promoted++;
2853
3067
  }
2854
3068
  return promoted;
2855
3069
  }
2856
- function rewireFrontierEdges(graph, frontierId2, serviceId3) {
3070
+ function rewireFrontierEdges(graph, frontierId2, serviceId4) {
2857
3071
  const inbound = [...graph.inboundEdges(frontierId2)];
2858
3072
  const outbound = [...graph.outboundEdges(frontierId2)];
2859
3073
  for (const edgeId of inbound) {
2860
3074
  const edge = graph.getEdgeAttributes(edgeId);
2861
- rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
3075
+ rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
2862
3076
  }
2863
3077
  for (const edgeId of outbound) {
2864
3078
  const edge = graph.getEdgeAttributes(edgeId);
2865
- rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
3079
+ rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
2866
3080
  }
2867
3081
  }
2868
3082
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -3624,9 +3838,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
3624
3838
  "StatefulSet",
3625
3839
  "DaemonSet"
3626
3840
  ]);
3627
- function addAliases(graph, serviceId3, candidates) {
3628
- if (!graph.hasNode(serviceId3)) return;
3629
- const node = graph.getNodeAttributes(serviceId3);
3841
+ function addAliases(graph, serviceId4, candidates) {
3842
+ if (!graph.hasNode(serviceId4)) return;
3843
+ const node = graph.getNodeAttributes(serviceId4);
3630
3844
  if (node.type !== import_types6.NodeType.ServiceNode) return;
3631
3845
  const set = new Set(node.aliases ?? []);
3632
3846
  for (const c of candidates) {
@@ -3636,7 +3850,7 @@ function addAliases(graph, serviceId3, candidates) {
3636
3850
  }
3637
3851
  if (set.size === 0) return;
3638
3852
  const updated = { ...node, aliases: [...set].sort() };
3639
- graph.replaceNodeAttributes(serviceId3, updated);
3853
+ graph.replaceNodeAttributes(serviceId4, updated);
3640
3854
  }
3641
3855
  function indexServicesByName(services) {
3642
3856
  const map = /* @__PURE__ */ new Map();
@@ -3669,12 +3883,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3669
3883
  }
3670
3884
  if (!compose?.services) return;
3671
3885
  for (const [composeName, svc] of Object.entries(compose.services)) {
3672
- const serviceId3 = serviceIndex.get(composeName);
3673
- if (!serviceId3) continue;
3886
+ const serviceId4 = serviceIndex.get(composeName);
3887
+ if (!serviceId4) continue;
3674
3888
  const aliases = /* @__PURE__ */ new Set([composeName]);
3675
3889
  if (svc.container_name) aliases.add(svc.container_name);
3676
3890
  if (svc.hostname) aliases.add(svc.hostname);
3677
- addAliases(graph, serviceId3, aliases);
3891
+ addAliases(graph, serviceId4, aliases);
3678
3892
  }
3679
3893
  }
3680
3894
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -3787,16 +4001,28 @@ init_cjs_shims();
3787
4001
  var import_node_fs10 = require("fs");
3788
4002
  var import_node_path10 = __toESM(require("path"), 1);
3789
4003
  var import_types7 = require("@neat.is/types");
4004
+ function buildServiceHostIndex(services) {
4005
+ const knownHosts = /* @__PURE__ */ new Set();
4006
+ const hostToNodeId = /* @__PURE__ */ new Map();
4007
+ for (const service of services) {
4008
+ const base = import_node_path10.default.basename(service.dir);
4009
+ knownHosts.add(base);
4010
+ knownHosts.add(service.pkg.name);
4011
+ hostToNodeId.set(base, service.node.id);
4012
+ hostToNodeId.set(service.pkg.name, service.node.id);
4013
+ }
4014
+ return { knownHosts, hostToNodeId };
4015
+ }
3790
4016
  async function walkSourceFiles(dir) {
3791
4017
  const out = [];
3792
- async function walk(current) {
4018
+ async function walk3(current) {
3793
4019
  const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3794
4020
  for (const entry of entries) {
3795
4021
  const full = import_node_path10.default.join(current, entry.name);
3796
4022
  if (entry.isDirectory()) {
3797
4023
  if (IGNORED_DIRS.has(entry.name)) continue;
3798
4024
  if (await isPythonVenvDir(full)) continue;
3799
- await walk(full);
4025
+ await walk3(full);
3800
4026
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3801
4027
  // would attribute our instrumentation imports to the user's service.
3802
4028
  !isNeatAuthoredSourceFile(entry.name)) {
@@ -3804,7 +4030,7 @@ async function walkSourceFiles(dir) {
3804
4030
  }
3805
4031
  }
3806
4032
  }
3807
- await walk(dir);
4033
+ await walk3(dir);
3808
4034
  return out;
3809
4035
  }
3810
4036
  async function loadSourceFiles(dir) {
@@ -4898,20 +5124,20 @@ var import_node_path21 = __toESM(require("path"), 1);
4898
5124
  var import_types10 = require("@neat.is/types");
4899
5125
  async function walkConfigFiles(dir) {
4900
5126
  const out = [];
4901
- async function walk(current) {
5127
+ async function walk3(current) {
4902
5128
  const entries = await import_node_fs14.promises.readdir(current, { withFileTypes: true });
4903
5129
  for (const entry of entries) {
4904
5130
  const full = import_node_path21.default.join(current, entry.name);
4905
5131
  if (entry.isDirectory()) {
4906
5132
  if (IGNORED_DIRS.has(entry.name)) continue;
4907
5133
  if (await isPythonVenvDir(full)) continue;
4908
- await walk(full);
5134
+ await walk3(full);
4909
5135
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
4910
5136
  out.push(full);
4911
5137
  }
4912
5138
  }
4913
5139
  }
4914
- await walk(dir);
5140
+ await walk3(dir);
4915
5141
  return out;
4916
5142
  }
4917
5143
  async function addConfigNodes(graph, services, scanPath) {
@@ -4959,17 +5185,468 @@ async function addConfigNodes(graph, services, scanPath) {
4959
5185
  return { nodesAdded, edgesAdded };
4960
5186
  }
4961
5187
 
5188
+ // src/extract/routes.ts
5189
+ init_cjs_shims();
5190
+ var import_node_path22 = __toESM(require("path"), 1);
5191
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5192
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5193
+ var import_types11 = require("@neat.is/types");
5194
+ var PARSE_CHUNK2 = 16384;
5195
+ function parseSource2(parser, source) {
5196
+ return parser.parse(
5197
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5198
+ );
5199
+ }
5200
+ function makeJsParser2() {
5201
+ const p = new import_tree_sitter2.default();
5202
+ p.setLanguage(import_tree_sitter_javascript2.default);
5203
+ return p;
5204
+ }
5205
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5206
+ "get",
5207
+ "post",
5208
+ "put",
5209
+ "patch",
5210
+ "delete",
5211
+ "options",
5212
+ "head",
5213
+ "all"
5214
+ ]);
5215
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5216
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5217
+ function canonicalizeTemplate(raw) {
5218
+ let p = raw.split("?")[0].split("#")[0];
5219
+ if (!p.startsWith("/")) p = "/" + p;
5220
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
5221
+ return p;
5222
+ }
5223
+ function isDynamicSegment(seg) {
5224
+ if (seg.length === 0) return false;
5225
+ if (seg.includes(":")) return true;
5226
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
5227
+ if (/^\d+$/.test(seg)) return true;
5228
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return true;
5229
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
5230
+ return false;
5231
+ }
5232
+ function normalizePathTemplate(raw) {
5233
+ const canonical = canonicalizeTemplate(raw);
5234
+ const segments = canonical.split("/").filter((s) => s.length > 0);
5235
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
5236
+ return "/" + normalised.join("/");
5237
+ }
5238
+ function walk(node, visit) {
5239
+ visit(node);
5240
+ for (let i = 0; i < node.namedChildCount; i++) {
5241
+ const child = node.namedChild(i);
5242
+ if (child) walk(child, visit);
5243
+ }
5244
+ }
5245
+ function staticStringText(node) {
5246
+ if (node.type === "string") {
5247
+ for (let i = 0; i < node.namedChildCount; i++) {
5248
+ const child = node.namedChild(i);
5249
+ if (child?.type === "string_fragment") return child.text;
5250
+ }
5251
+ return "";
5252
+ }
5253
+ if (node.type === "template_string") {
5254
+ for (let i = 0; i < node.namedChildCount; i++) {
5255
+ if (node.namedChild(i)?.type === "template_substitution") return null;
5256
+ }
5257
+ const raw = node.text;
5258
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5259
+ }
5260
+ return null;
5261
+ }
5262
+ function objectStringProp(objNode, key) {
5263
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5264
+ const pair = objNode.namedChild(i);
5265
+ if (!pair || pair.type !== "pair") continue;
5266
+ const k = pair.childForFieldName("key");
5267
+ if (!k) continue;
5268
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
5269
+ if (kText !== key) continue;
5270
+ const v = pair.childForFieldName("value");
5271
+ if (v) return staticStringText(v);
5272
+ }
5273
+ return null;
5274
+ }
5275
+ function fastifyRouteMethods(objNode) {
5276
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5277
+ const pair = objNode.namedChild(i);
5278
+ if (!pair || pair.type !== "pair") continue;
5279
+ const k = pair.childForFieldName("key");
5280
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
5281
+ if (kText !== "method") continue;
5282
+ const v = pair.childForFieldName("value");
5283
+ if (!v) return [];
5284
+ if (v.type === "string" || v.type === "template_string") {
5285
+ const s = staticStringText(v);
5286
+ return s ? [s.toUpperCase()] : [];
5287
+ }
5288
+ if (v.type === "array") {
5289
+ const out = [];
5290
+ for (let j = 0; j < v.namedChildCount; j++) {
5291
+ const el = v.namedChild(j);
5292
+ if (el && (el.type === "string" || el.type === "template_string")) {
5293
+ const s = staticStringText(el);
5294
+ if (s) out.push(s.toUpperCase());
5295
+ }
5296
+ }
5297
+ return out;
5298
+ }
5299
+ }
5300
+ return [];
5301
+ }
5302
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
5303
+ const tree = parseSource2(parser, source);
5304
+ const out = [];
5305
+ const framework = hasExpress ? "express" : "fastify";
5306
+ walk(tree.rootNode, (node) => {
5307
+ if (node.type !== "call_expression") return;
5308
+ const fn = node.childForFieldName("function");
5309
+ if (!fn || fn.type !== "member_expression") return;
5310
+ const prop = fn.childForFieldName("property");
5311
+ if (!prop) return;
5312
+ const method = prop.text.toLowerCase();
5313
+ const args = node.childForFieldName("arguments");
5314
+ const first = args?.namedChild(0);
5315
+ if (!first) return;
5316
+ const line = node.startPosition.row + 1;
5317
+ if (ROUTER_METHODS.has(method)) {
5318
+ const p = staticStringText(first);
5319
+ if (p && p.startsWith("/")) {
5320
+ out.push({
5321
+ method: method === "all" ? "ALL" : method.toUpperCase(),
5322
+ pathTemplate: canonicalizeTemplate(p),
5323
+ line,
5324
+ framework
5325
+ });
5326
+ }
5327
+ return;
5328
+ }
5329
+ if (method === "route" && hasFastify && first.type === "object") {
5330
+ const url = objectStringProp(first, "url");
5331
+ if (!url || !url.startsWith("/")) return;
5332
+ const methods = fastifyRouteMethods(first);
5333
+ const list = methods.length > 0 ? methods : ["ALL"];
5334
+ for (const m of list) {
5335
+ out.push({
5336
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5337
+ pathTemplate: canonicalizeTemplate(url),
5338
+ line,
5339
+ framework: "fastify"
5340
+ });
5341
+ }
5342
+ }
5343
+ });
5344
+ return out;
5345
+ }
5346
+ function segmentsOf(relFile) {
5347
+ return toPosix2(relFile).split("/").filter((s) => s.length > 0);
5348
+ }
5349
+ function isNextAppRouteFile(relFile) {
5350
+ const segs = segmentsOf(relFile);
5351
+ if (!segs.includes("app")) return false;
5352
+ const base = segs[segs.length - 1] ?? "";
5353
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
5354
+ }
5355
+ function isNextPagesApiFile(relFile) {
5356
+ const segs = segmentsOf(relFile);
5357
+ const pagesIdx = segs.indexOf("pages");
5358
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
5359
+ const base = segs[segs.length - 1] ?? "";
5360
+ if (/^_(app|document|middleware)\./.test(base)) return false;
5361
+ return JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(base));
5362
+ }
5363
+ function nextSegment(seg) {
5364
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
5365
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
5366
+ if (catchAll) return ":" + catchAll[1];
5367
+ const dynamic = seg.match(/^\[(.+?)\]$/);
5368
+ if (dynamic) return ":" + dynamic[1];
5369
+ return seg;
5370
+ }
5371
+ function nextAppPathTemplate(relFile) {
5372
+ const segs = segmentsOf(relFile);
5373
+ const appIdx = segs.lastIndexOf("app");
5374
+ const between = segs.slice(appIdx + 1, segs.length - 1);
5375
+ const parts = [];
5376
+ for (const seg of between) {
5377
+ const mapped = nextSegment(seg);
5378
+ if (mapped !== null) parts.push(mapped);
5379
+ }
5380
+ return "/" + parts.join("/");
5381
+ }
5382
+ function nextPagesApiPathTemplate(relFile) {
5383
+ const segs = segmentsOf(relFile);
5384
+ const pagesIdx = segs.indexOf("pages");
5385
+ const rest = segs.slice(pagesIdx + 1);
5386
+ const parts = [];
5387
+ for (let i = 0; i < rest.length; i++) {
5388
+ let seg = rest[i];
5389
+ if (i === rest.length - 1) {
5390
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
5391
+ if (seg === "index") continue;
5392
+ }
5393
+ const mapped = nextSegment(seg);
5394
+ if (mapped !== null) parts.push(mapped);
5395
+ }
5396
+ return "/" + parts.join("/");
5397
+ }
5398
+ function nextAppMethods(root) {
5399
+ const out = [];
5400
+ walk(root, (node) => {
5401
+ if (node.type !== "export_statement") return;
5402
+ const decl = node.childForFieldName("declaration");
5403
+ if (!decl) return;
5404
+ const line = node.startPosition.row + 1;
5405
+ if (decl.type === "function_declaration") {
5406
+ const name = decl.childForFieldName("name")?.text;
5407
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5408
+ return;
5409
+ }
5410
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
5411
+ for (let i = 0; i < decl.namedChildCount; i++) {
5412
+ const d = decl.namedChild(i);
5413
+ if (d?.type !== "variable_declarator") continue;
5414
+ const name = d.childForFieldName("name")?.text;
5415
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5416
+ }
5417
+ }
5418
+ });
5419
+ return out;
5420
+ }
5421
+ function nextRoutesFromFile(source, relFile, parser) {
5422
+ if (isNextAppRouteFile(relFile)) {
5423
+ const tree = parseSource2(parser, source);
5424
+ const template = nextAppPathTemplate(relFile);
5425
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
5426
+ method,
5427
+ pathTemplate: canonicalizeTemplate(template),
5428
+ line,
5429
+ framework: "next"
5430
+ }));
5431
+ }
5432
+ if (isNextPagesApiFile(relFile)) {
5433
+ return [
5434
+ {
5435
+ method: "ALL",
5436
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
5437
+ line: 1,
5438
+ framework: "next"
5439
+ }
5440
+ ];
5441
+ }
5442
+ return [];
5443
+ }
5444
+ async function addRoutes(graph, services) {
5445
+ const jsParser = makeJsParser2();
5446
+ let nodesAdded = 0;
5447
+ let edgesAdded = 0;
5448
+ for (const service of services) {
5449
+ const deps = {
5450
+ ...service.pkg.dependencies ?? {},
5451
+ ...service.pkg.devDependencies ?? {}
5452
+ };
5453
+ const hasExpress = deps["express"] !== void 0;
5454
+ const hasFastify = deps["fastify"] !== void 0;
5455
+ const hasNext = deps["next"] !== void 0;
5456
+ if (!hasExpress && !hasFastify && !hasNext) continue;
5457
+ const files = await loadSourceFiles(service.dir);
5458
+ for (const file of files) {
5459
+ if (isTestPath(file.path)) continue;
5460
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(file.path))) continue;
5461
+ const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
5462
+ let routes;
5463
+ try {
5464
+ if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5465
+ routes = nextRoutesFromFile(file.content, relFile, jsParser);
5466
+ } else if (hasExpress || hasFastify) {
5467
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
5468
+ } else {
5469
+ routes = [];
5470
+ }
5471
+ } catch (err) {
5472
+ recordExtractionError("route extraction", file.path, err);
5473
+ continue;
5474
+ }
5475
+ if (routes.length === 0) continue;
5476
+ for (const route of routes) {
5477
+ const rid = (0, import_types11.routeId)(service.pkg.name, route.method, route.pathTemplate);
5478
+ if (!graph.hasNode(rid)) {
5479
+ const node = {
5480
+ id: rid,
5481
+ type: import_types11.NodeType.RouteNode,
5482
+ name: `${route.method} ${route.pathTemplate}`,
5483
+ service: service.pkg.name,
5484
+ method: route.method,
5485
+ pathTemplate: route.pathTemplate,
5486
+ path: relFile,
5487
+ line: route.line,
5488
+ framework: route.framework,
5489
+ discoveredVia: "static"
5490
+ };
5491
+ graph.addNode(rid, node);
5492
+ nodesAdded++;
5493
+ }
5494
+ const containsId = (0, import_types11.extractedEdgeId)(service.node.id, rid, import_types11.EdgeType.CONTAINS);
5495
+ if (!graph.hasEdge(containsId)) {
5496
+ const edge = {
5497
+ id: containsId,
5498
+ source: service.node.id,
5499
+ target: rid,
5500
+ type: import_types11.EdgeType.CONTAINS,
5501
+ provenance: import_types11.Provenance.EXTRACTED,
5502
+ confidence: (0, import_types11.confidenceForExtracted)("structural"),
5503
+ evidence: {
5504
+ file: relFile,
5505
+ line: route.line,
5506
+ snippet: snippet(file.content, route.line)
5507
+ }
5508
+ };
5509
+ graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
5510
+ edgesAdded++;
5511
+ }
5512
+ }
5513
+ }
5514
+ }
5515
+ return { nodesAdded, edgesAdded };
5516
+ }
5517
+
5518
+ // src/extract/proto.ts
5519
+ init_cjs_shims();
5520
+ var import_node_fs15 = require("fs");
5521
+ var import_node_path23 = __toESM(require("path"), 1);
5522
+ var import_types12 = require("@neat.is/types");
5523
+ var PROTO_EXTENSION = ".proto";
5524
+ function packageOf(content) {
5525
+ const m = content.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/m);
5526
+ return m ? m[1] : null;
5527
+ }
5528
+ function lineAt(content, offset) {
5529
+ return content.slice(0, offset).split("\n").length;
5530
+ }
5531
+ function grpcMethodsFromProto(content, fqPackage) {
5532
+ const out = [];
5533
+ const serviceRe = /\bservice\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g;
5534
+ let sm;
5535
+ while ((sm = serviceRe.exec(content)) !== null) {
5536
+ const serviceName = sm[1];
5537
+ const rpcService = fqPackage ? `${fqPackage}.${serviceName}` : serviceName;
5538
+ const bodyStart = serviceRe.lastIndex;
5539
+ let depth = 1;
5540
+ let i = bodyStart;
5541
+ for (; i < content.length && depth > 0; i++) {
5542
+ const ch = content[i];
5543
+ if (ch === "{") depth++;
5544
+ else if (ch === "}") depth--;
5545
+ }
5546
+ const body = content.slice(bodyStart, i - 1);
5547
+ const rpcRe = /\brpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
5548
+ let rm;
5549
+ while ((rm = rpcRe.exec(body)) !== null) {
5550
+ const rpcMethod = rm[1];
5551
+ const line = lineAt(content, bodyStart + rm.index);
5552
+ out.push({ rpcService, rpcMethod, line });
5553
+ }
5554
+ serviceRe.lastIndex = i;
5555
+ }
5556
+ return out;
5557
+ }
5558
+ async function walkProtoFiles(dir) {
5559
+ const out = [];
5560
+ async function walk3(current) {
5561
+ const entries = await import_node_fs15.promises.readdir(current, { withFileTypes: true }).catch(() => []);
5562
+ for (const entry of entries) {
5563
+ const full = import_node_path23.default.join(current, entry.name);
5564
+ if (entry.isDirectory()) {
5565
+ if (IGNORED_DIRS.has(entry.name)) continue;
5566
+ if (await isPythonVenvDir(full)) continue;
5567
+ await walk3(full);
5568
+ } else if (entry.isFile() && import_node_path23.default.extname(entry.name) === PROTO_EXTENSION) {
5569
+ out.push(full);
5570
+ }
5571
+ }
5572
+ }
5573
+ await walk3(dir);
5574
+ return out;
5575
+ }
5576
+ async function addGrpcMethods(graph, services) {
5577
+ let nodesAdded = 0;
5578
+ let edgesAdded = 0;
5579
+ for (const service of services) {
5580
+ const protoPaths = await walkProtoFiles(service.dir);
5581
+ for (const protoPath of protoPaths) {
5582
+ if (isTestPath(protoPath)) continue;
5583
+ const relFile = toPosix2(import_node_path23.default.relative(service.dir, protoPath));
5584
+ let content;
5585
+ try {
5586
+ content = await import_node_fs15.promises.readFile(protoPath, "utf8");
5587
+ } catch (err) {
5588
+ recordExtractionError("proto extraction", protoPath, err);
5589
+ continue;
5590
+ }
5591
+ let methods;
5592
+ try {
5593
+ methods = grpcMethodsFromProto(content, packageOf(content));
5594
+ } catch (err) {
5595
+ recordExtractionError("proto extraction", protoPath, err);
5596
+ continue;
5597
+ }
5598
+ if (methods.length === 0) continue;
5599
+ for (const method of methods) {
5600
+ const mid = (0, import_types12.grpcMethodId)(method.rpcService, method.rpcMethod);
5601
+ if (!graph.hasNode(mid)) {
5602
+ const node = {
5603
+ id: mid,
5604
+ type: import_types12.NodeType.GrpcMethodNode,
5605
+ name: `${method.rpcService}/${method.rpcMethod}`,
5606
+ rpcService: method.rpcService,
5607
+ rpcMethod: method.rpcMethod,
5608
+ path: relFile,
5609
+ line: method.line,
5610
+ discoveredVia: "static"
5611
+ };
5612
+ graph.addNode(mid, node);
5613
+ nodesAdded++;
5614
+ }
5615
+ const containsId = (0, import_types12.extractedEdgeId)(service.node.id, mid, import_types12.EdgeType.CONTAINS);
5616
+ if (!graph.hasEdge(containsId)) {
5617
+ const edge = {
5618
+ id: containsId,
5619
+ source: service.node.id,
5620
+ target: mid,
5621
+ type: import_types12.EdgeType.CONTAINS,
5622
+ provenance: import_types12.Provenance.EXTRACTED,
5623
+ confidence: (0, import_types12.confidenceForExtracted)("structural"),
5624
+ evidence: {
5625
+ file: relFile,
5626
+ line: method.line,
5627
+ snippet: snippet(content, method.line)
5628
+ }
5629
+ };
5630
+ graph.addEdgeWithKey(containsId, service.node.id, mid, edge);
5631
+ edgesAdded++;
5632
+ }
5633
+ }
5634
+ }
5635
+ }
5636
+ return { nodesAdded, edgesAdded };
5637
+ }
5638
+
4962
5639
  // src/extract/calls/index.ts
4963
5640
  init_cjs_shims();
4964
- var import_types17 = require("@neat.is/types");
5641
+ var import_types20 = require("@neat.is/types");
4965
5642
 
4966
5643
  // src/extract/calls/http.ts
4967
5644
  init_cjs_shims();
4968
- var import_node_path22 = __toESM(require("path"), 1);
4969
- var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
4970
- var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5645
+ var import_node_path24 = __toESM(require("path"), 1);
5646
+ var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5647
+ var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
4971
5648
  var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
4972
- var import_types11 = require("@neat.is/types");
5649
+ var import_types13 = require("@neat.is/types");
4973
5650
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
4974
5651
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
4975
5652
  function isInsideJsxExternalLink(node) {
@@ -4997,14 +5674,14 @@ function collectStringLiterals(node, out) {
4997
5674
  if (child) collectStringLiterals(child, out);
4998
5675
  }
4999
5676
  }
5000
- var PARSE_CHUNK2 = 16384;
5001
- function parseSource2(parser, source) {
5677
+ var PARSE_CHUNK3 = 16384;
5678
+ function parseSource3(parser, source) {
5002
5679
  return parser.parse(
5003
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5680
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5004
5681
  );
5005
5682
  }
5006
5683
  function callsFromSource(source, parser, knownHosts) {
5007
- const tree = parseSource2(parser, source);
5684
+ const tree = parseSource3(parser, source);
5008
5685
  const literals = [];
5009
5686
  collectStringLiterals(tree.rootNode, literals);
5010
5687
  const out = [];
@@ -5018,27 +5695,20 @@ function callsFromSource(source, parser, knownHosts) {
5018
5695
  }
5019
5696
  return out;
5020
5697
  }
5021
- function makeJsParser2() {
5022
- const p = new import_tree_sitter2.default();
5023
- p.setLanguage(import_tree_sitter_javascript2.default);
5698
+ function makeJsParser3() {
5699
+ const p = new import_tree_sitter3.default();
5700
+ p.setLanguage(import_tree_sitter_javascript3.default);
5024
5701
  return p;
5025
5702
  }
5026
5703
  function makePyParser2() {
5027
- const p = new import_tree_sitter2.default();
5704
+ const p = new import_tree_sitter3.default();
5028
5705
  p.setLanguage(import_tree_sitter_python2.default);
5029
5706
  return p;
5030
5707
  }
5031
5708
  async function addHttpCallEdges(graph, services) {
5032
- const jsParser = makeJsParser2();
5709
+ const jsParser = makeJsParser3();
5033
5710
  const pyParser = makePyParser2();
5034
- const knownHosts = /* @__PURE__ */ new Set();
5035
- const hostToNodeId = /* @__PURE__ */ new Map();
5036
- for (const service of services) {
5037
- knownHosts.add(import_node_path22.default.basename(service.dir));
5038
- knownHosts.add(service.pkg.name);
5039
- hostToNodeId.set(import_node_path22.default.basename(service.dir), service.node.id);
5040
- hostToNodeId.set(service.pkg.name, service.node.id);
5041
- }
5711
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5042
5712
  let nodesAdded = 0;
5043
5713
  let edgesAdded = 0;
5044
5714
  for (const service of services) {
@@ -5046,7 +5716,7 @@ async function addHttpCallEdges(graph, services) {
5046
5716
  const seen = /* @__PURE__ */ new Set();
5047
5717
  for (const file of files) {
5048
5718
  if (isTestPath(file.path)) continue;
5049
- const parser = import_node_path22.default.extname(file.path) === ".py" ? pyParser : jsParser;
5719
+ const parser = import_node_path24.default.extname(file.path) === ".py" ? pyParser : jsParser;
5050
5720
  let sites;
5051
5721
  try {
5052
5722
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -5055,14 +5725,14 @@ async function addHttpCallEdges(graph, services) {
5055
5725
  continue;
5056
5726
  }
5057
5727
  if (sites.length === 0) continue;
5058
- const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
5728
+ const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
5059
5729
  for (const site of sites) {
5060
5730
  const targetId = hostToNodeId.get(site.host);
5061
5731
  if (!targetId || targetId === service.node.id) continue;
5062
5732
  const dedupKey = `${relFile}|${targetId}`;
5063
5733
  if (seen.has(dedupKey)) continue;
5064
5734
  seen.add(dedupKey);
5065
- const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
5735
+ const confidence = (0, import_types13.confidenceForExtracted)("url-literal-service-target");
5066
5736
  const ev = {
5067
5737
  file: relFile,
5068
5738
  line: site.line,
@@ -5076,25 +5746,25 @@ async function addHttpCallEdges(graph, services) {
5076
5746
  );
5077
5747
  nodesAdded += n;
5078
5748
  edgesAdded += e;
5079
- if (!(0, import_types11.passesExtractedFloor)(confidence)) {
5749
+ if (!(0, import_types13.passesExtractedFloor)(confidence)) {
5080
5750
  noteExtractedDropped({
5081
5751
  source: fileNodeId,
5082
5752
  target: targetId,
5083
- type: import_types11.EdgeType.CALLS,
5753
+ type: import_types13.EdgeType.CALLS,
5084
5754
  confidence,
5085
5755
  confidenceKind: "url-literal-service-target",
5086
5756
  evidence: ev
5087
5757
  });
5088
5758
  continue;
5089
5759
  }
5090
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types11.EdgeType.CALLS);
5760
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types13.EdgeType.CALLS);
5091
5761
  if (!graph.hasEdge(edgeId)) {
5092
5762
  const edge = {
5093
5763
  id: edgeId,
5094
5764
  source: fileNodeId,
5095
5765
  target: targetId,
5096
- type: import_types11.EdgeType.CALLS,
5097
- provenance: import_types11.Provenance.EXTRACTED,
5766
+ type: import_types13.EdgeType.CALLS,
5767
+ provenance: import_types13.Provenance.EXTRACTED,
5098
5768
  confidence,
5099
5769
  evidence: ev
5100
5770
  };
@@ -5107,10 +5777,279 @@ async function addHttpCallEdges(graph, services) {
5107
5777
  return { nodesAdded, edgesAdded };
5108
5778
  }
5109
5779
 
5780
+ // src/extract/calls/route-match.ts
5781
+ init_cjs_shims();
5782
+ var import_node_path25 = __toESM(require("path"), 1);
5783
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
5784
+ var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
5785
+ var import_types14 = require("@neat.is/types");
5786
+ var PARSE_CHUNK4 = 16384;
5787
+ function parseSource4(parser, source) {
5788
+ return parser.parse(
5789
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
5790
+ );
5791
+ }
5792
+ function makeJsParser4() {
5793
+ const p = new import_tree_sitter4.default();
5794
+ p.setLanguage(import_tree_sitter_javascript4.default);
5795
+ return p;
5796
+ }
5797
+ var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5798
+ var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
5799
+ function walk2(node, visit) {
5800
+ visit(node);
5801
+ for (let i = 0; i < node.namedChildCount; i++) {
5802
+ const child = node.namedChild(i);
5803
+ if (child) walk2(child, visit);
5804
+ }
5805
+ }
5806
+ function reconstructUrl(node) {
5807
+ if (node.type === "string") {
5808
+ for (let i = 0; i < node.namedChildCount; i++) {
5809
+ const child = node.namedChild(i);
5810
+ if (child?.type === "string_fragment") return child.text;
5811
+ }
5812
+ return "";
5813
+ }
5814
+ if (node.type === "template_string") {
5815
+ let out = "";
5816
+ for (let i = 0; i < node.namedChildCount; i++) {
5817
+ const child = node.namedChild(i);
5818
+ if (!child) continue;
5819
+ if (child.type === "string_fragment") out += child.text;
5820
+ else if (child.type === "template_substitution") out += ":param";
5821
+ }
5822
+ if (out.length === 0) {
5823
+ const raw = node.text;
5824
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5825
+ }
5826
+ return out;
5827
+ }
5828
+ return null;
5829
+ }
5830
+ function methodFromOptions(objNode) {
5831
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5832
+ const pair = objNode.namedChild(i);
5833
+ if (!pair || pair.type !== "pair") continue;
5834
+ const k = pair.childForFieldName("key");
5835
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5836
+ if (kText !== "method") continue;
5837
+ const v = pair.childForFieldName("value");
5838
+ if (v && (v.type === "string" || v.type === "template_string")) {
5839
+ const s = stringText(v);
5840
+ return s ? s.toUpperCase() : void 0;
5841
+ }
5842
+ }
5843
+ return void 0;
5844
+ }
5845
+ function stringText(node) {
5846
+ if (node.type === "string") {
5847
+ for (let i = 0; i < node.namedChildCount; i++) {
5848
+ const child = node.namedChild(i);
5849
+ if (child?.type === "string_fragment") return child.text;
5850
+ }
5851
+ return "";
5852
+ }
5853
+ return null;
5854
+ }
5855
+ function urlNodeFromConfig(objNode) {
5856
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5857
+ const pair = objNode.namedChild(i);
5858
+ if (!pair || pair.type !== "pair") continue;
5859
+ const k = pair.childForFieldName("key");
5860
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5861
+ if (kText === "url") return pair.childForFieldName("value");
5862
+ }
5863
+ return null;
5864
+ }
5865
+ function pathOf(urlStr) {
5866
+ try {
5867
+ const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
5868
+ const parsed = new URL(candidate);
5869
+ return parsed.pathname || "/";
5870
+ } catch {
5871
+ return null;
5872
+ }
5873
+ }
5874
+ function matchHost(urlStr, knownHosts) {
5875
+ for (const host of knownHosts) {
5876
+ if (urlMatchesHost(urlStr, host)) return host;
5877
+ }
5878
+ return null;
5879
+ }
5880
+ function clientCallSitesFromSource(source, parser, knownHosts) {
5881
+ const tree = parseSource4(parser, source);
5882
+ const out = [];
5883
+ const push = (urlNode, method, callNode) => {
5884
+ const urlStr = reconstructUrl(urlNode);
5885
+ if (!urlStr) return;
5886
+ const host = matchHost(urlStr, knownHosts);
5887
+ if (!host) return;
5888
+ const p = pathOf(urlStr);
5889
+ if (p === null) return;
5890
+ const line = callNode.startPosition.row + 1;
5891
+ out.push({
5892
+ host,
5893
+ method,
5894
+ pathTemplate: p,
5895
+ line,
5896
+ snippet: snippet(source, line)
5897
+ });
5898
+ };
5899
+ walk2(tree.rootNode, (node) => {
5900
+ if (node.type !== "call_expression") return;
5901
+ const fn = node.childForFieldName("function");
5902
+ if (!fn) return;
5903
+ const args = node.childForFieldName("arguments");
5904
+ const first = args?.namedChild(0);
5905
+ if (!first) return;
5906
+ const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
5907
+ if (fn.type === "identifier" && fnName === "fetch") {
5908
+ const opts = args?.namedChild(1);
5909
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5910
+ push(first, method, node);
5911
+ return;
5912
+ }
5913
+ if (fn.type === "identifier" && fnName === "axios") {
5914
+ if (first.type === "object") {
5915
+ const urlNode = urlNodeFromConfig(first);
5916
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5917
+ } else {
5918
+ const opts = args?.namedChild(1);
5919
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5920
+ push(first, method, node);
5921
+ }
5922
+ return;
5923
+ }
5924
+ if (fn.type === "member_expression") {
5925
+ const obj = fn.childForFieldName("object");
5926
+ const objName = obj?.text ?? "";
5927
+ if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
5928
+ if (fnName === "request" && first.type === "object") {
5929
+ const urlNode = urlNodeFromConfig(first);
5930
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5931
+ } else {
5932
+ push(first, fnName.toUpperCase(), node);
5933
+ }
5934
+ return;
5935
+ }
5936
+ if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
5937
+ const opts = args?.namedChild(1);
5938
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
5939
+ push(first, method, node);
5940
+ return;
5941
+ }
5942
+ }
5943
+ });
5944
+ return out;
5945
+ }
5946
+ function buildRouteIndex(graph) {
5947
+ const index = /* @__PURE__ */ new Map();
5948
+ graph.forEachNode((_id, attrs) => {
5949
+ const node = attrs;
5950
+ if (node.type !== import_types14.NodeType.RouteNode) return;
5951
+ const route = attrs;
5952
+ const owner = (0, import_types14.serviceId)(route.service);
5953
+ const entry = {
5954
+ method: route.method.toUpperCase(),
5955
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
5956
+ routeNodeId: route.id
5957
+ };
5958
+ const list = index.get(owner);
5959
+ if (list) list.push(entry);
5960
+ else index.set(owner, [entry]);
5961
+ });
5962
+ return index;
5963
+ }
5964
+ function findRoute(entries, method, normalizedPath) {
5965
+ return entries.find(
5966
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
5967
+ );
5968
+ }
5969
+ async function addRouteCallEdges(graph, services) {
5970
+ const jsParser = makeJsParser4();
5971
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5972
+ const routeIndex = buildRouteIndex(graph);
5973
+ if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
5974
+ let nodesAdded = 0;
5975
+ let edgesAdded = 0;
5976
+ for (const service of services) {
5977
+ const files = await loadSourceFiles(service.dir);
5978
+ const seen = /* @__PURE__ */ new Set();
5979
+ for (const file of files) {
5980
+ if (isTestPath(file.path)) continue;
5981
+ if (!JS_CLIENT_EXTENSIONS.has(import_node_path25.default.extname(file.path))) continue;
5982
+ let sites;
5983
+ try {
5984
+ sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
5985
+ } catch (err) {
5986
+ recordExtractionError("route-match call extraction", file.path, err);
5987
+ continue;
5988
+ }
5989
+ if (sites.length === 0) continue;
5990
+ const relFile = toPosix2(import_node_path25.default.relative(service.dir, file.path));
5991
+ for (const site of sites) {
5992
+ const serverServiceId = hostToNodeId.get(site.host);
5993
+ if (!serverServiceId || serverServiceId === service.node.id) continue;
5994
+ const entries = routeIndex.get(serverServiceId);
5995
+ if (!entries) continue;
5996
+ const normalizedPath = normalizePathTemplate(site.pathTemplate);
5997
+ const match = findRoute(entries, site.method, normalizedPath);
5998
+ if (!match) continue;
5999
+ const dedupKey = `${relFile}|${match.routeNodeId}`;
6000
+ if (seen.has(dedupKey)) continue;
6001
+ seen.add(dedupKey);
6002
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
6003
+ graph,
6004
+ service.pkg.name,
6005
+ service.node.id,
6006
+ relFile
6007
+ );
6008
+ nodesAdded += n;
6009
+ edgesAdded += e;
6010
+ const confidence = (0, import_types14.confidenceForExtracted)("verified-call-site");
6011
+ const ev = {
6012
+ file: relFile,
6013
+ line: site.line,
6014
+ snippet: site.snippet,
6015
+ method: site.method ?? match.method,
6016
+ pathTemplate: site.pathTemplate
6017
+ };
6018
+ if (!(0, import_types14.passesExtractedFloor)(confidence)) {
6019
+ noteExtractedDropped({
6020
+ source: fileNodeId,
6021
+ target: match.routeNodeId,
6022
+ type: import_types14.EdgeType.CALLS,
6023
+ confidence,
6024
+ confidenceKind: "verified-call-site",
6025
+ evidence: ev
6026
+ });
6027
+ continue;
6028
+ }
6029
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, match.routeNodeId, import_types14.EdgeType.CALLS);
6030
+ if (!graph.hasEdge(edgeId)) {
6031
+ const edge = {
6032
+ id: edgeId,
6033
+ source: fileNodeId,
6034
+ target: match.routeNodeId,
6035
+ type: import_types14.EdgeType.CALLS,
6036
+ provenance: import_types14.Provenance.EXTRACTED,
6037
+ confidence,
6038
+ evidence: ev
6039
+ };
6040
+ graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
6041
+ edgesAdded++;
6042
+ }
6043
+ }
6044
+ }
6045
+ }
6046
+ return { nodesAdded, edgesAdded };
6047
+ }
6048
+
5110
6049
  // src/extract/calls/kafka.ts
5111
6050
  init_cjs_shims();
5112
- var import_node_path23 = __toESM(require("path"), 1);
5113
- var import_types12 = require("@neat.is/types");
6051
+ var import_node_path26 = __toESM(require("path"), 1);
6052
+ var import_types15 = require("@neat.is/types");
5114
6053
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
5115
6054
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
5116
6055
  function findAll(re, text) {
@@ -5131,7 +6070,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5131
6070
  seen.add(key);
5132
6071
  const line = lineOf(file.content, topic);
5133
6072
  out.push({
5134
- infraId: (0, import_types12.infraId)("kafka-topic", topic),
6073
+ infraId: (0, import_types15.infraId)("kafka-topic", topic),
5135
6074
  name: topic,
5136
6075
  kind: "kafka-topic",
5137
6076
  edgeType,
@@ -5140,7 +6079,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5140
6079
  // tier (ADR-066).
5141
6080
  confidenceKind: "verified-call-site",
5142
6081
  evidence: {
5143
- file: import_node_path23.default.relative(serviceDir, file.path),
6082
+ file: import_node_path26.default.relative(serviceDir, file.path),
5144
6083
  line,
5145
6084
  snippet: snippet(file.content, line)
5146
6085
  }
@@ -5153,8 +6092,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5153
6092
 
5154
6093
  // src/extract/calls/redis.ts
5155
6094
  init_cjs_shims();
5156
- var import_node_path24 = __toESM(require("path"), 1);
5157
- var import_types13 = require("@neat.is/types");
6095
+ var import_node_path27 = __toESM(require("path"), 1);
6096
+ var import_types16 = require("@neat.is/types");
5158
6097
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
5159
6098
  function redisEndpointsFromFile(file, serviceDir) {
5160
6099
  const out = [];
@@ -5167,7 +6106,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5167
6106
  seen.add(host);
5168
6107
  const line = lineOf(file.content, host);
5169
6108
  out.push({
5170
- infraId: (0, import_types13.infraId)("redis", host),
6109
+ infraId: (0, import_types16.infraId)("redis", host),
5171
6110
  name: host,
5172
6111
  kind: "redis",
5173
6112
  edgeType: "CALLS",
@@ -5176,7 +6115,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5176
6115
  // support tier (ADR-066).
5177
6116
  confidenceKind: "url-with-structural-support",
5178
6117
  evidence: {
5179
- file: import_node_path24.default.relative(serviceDir, file.path),
6118
+ file: import_node_path27.default.relative(serviceDir, file.path),
5180
6119
  line,
5181
6120
  snippet: snippet(file.content, line)
5182
6121
  }
@@ -5187,8 +6126,8 @@ function redisEndpointsFromFile(file, serviceDir) {
5187
6126
 
5188
6127
  // src/extract/calls/aws.ts
5189
6128
  init_cjs_shims();
5190
- var import_node_path25 = __toESM(require("path"), 1);
5191
- var import_types14 = require("@neat.is/types");
6129
+ var import_node_path28 = __toESM(require("path"), 1);
6130
+ var import_types17 = require("@neat.is/types");
5192
6131
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
5193
6132
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
5194
6133
  function hasMarker(text, markers) {
@@ -5212,7 +6151,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5212
6151
  seen.add(key);
5213
6152
  const line = lineOf(file.content, name);
5214
6153
  out.push({
5215
- infraId: (0, import_types14.infraId)(kind, name),
6154
+ infraId: (0, import_types17.infraId)(kind, name),
5216
6155
  name,
5217
6156
  kind,
5218
6157
  edgeType: "CALLS",
@@ -5221,7 +6160,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5221
6160
  // (ADR-066).
5222
6161
  confidenceKind: "verified-call-site",
5223
6162
  evidence: {
5224
- file: import_node_path25.default.relative(serviceDir, file.path),
6163
+ file: import_node_path28.default.relative(serviceDir, file.path),
5225
6164
  line,
5226
6165
  snippet: snippet(file.content, line)
5227
6166
  }
@@ -5246,8 +6185,8 @@ function awsEndpointsFromFile(file, serviceDir) {
5246
6185
 
5247
6186
  // src/extract/calls/grpc.ts
5248
6187
  init_cjs_shims();
5249
- var import_node_path26 = __toESM(require("path"), 1);
5250
- var import_types15 = require("@neat.is/types");
6188
+ var import_node_path29 = __toESM(require("path"), 1);
6189
+ var import_types18 = require("@neat.is/types");
5251
6190
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
5252
6191
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
5253
6192
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -5296,7 +6235,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5296
6235
  const { kind } = classified;
5297
6236
  const line = lineOf(file.content, m[0]);
5298
6237
  out.push({
5299
- infraId: (0, import_types15.infraId)(kind, name),
6238
+ infraId: (0, import_types18.infraId)(kind, name),
5300
6239
  name,
5301
6240
  kind,
5302
6241
  edgeType: "CALLS",
@@ -5305,7 +6244,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5305
6244
  // tier (ADR-066).
5306
6245
  confidenceKind: "verified-call-site",
5307
6246
  evidence: {
5308
- file: import_node_path26.default.relative(serviceDir, file.path),
6247
+ file: import_node_path29.default.relative(serviceDir, file.path),
5309
6248
  line,
5310
6249
  snippet: snippet(file.content, line)
5311
6250
  }
@@ -5316,8 +6255,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
5316
6255
 
5317
6256
  // src/extract/calls/supabase.ts
5318
6257
  init_cjs_shims();
5319
- var import_node_path27 = __toESM(require("path"), 1);
5320
- var import_types16 = require("@neat.is/types");
6258
+ var import_node_path30 = __toESM(require("path"), 1);
6259
+ var import_types19 = require("@neat.is/types");
5321
6260
  var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
5322
6261
  var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
5323
6262
  var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
@@ -5355,7 +6294,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5355
6294
  seen.add(name);
5356
6295
  const line = lineOf(file.content, m[0]);
5357
6296
  out.push({
5358
- infraId: (0, import_types16.infraId)("supabase", name),
6297
+ infraId: (0, import_types19.infraId)("supabase", name),
5359
6298
  name,
5360
6299
  kind: "supabase",
5361
6300
  edgeType: "CALLS",
@@ -5365,7 +6304,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5365
6304
  // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
5366
6305
  confidenceKind: "verified-call-site",
5367
6306
  evidence: {
5368
- file: import_node_path27.default.relative(serviceDir, file.path),
6307
+ file: import_node_path30.default.relative(serviceDir, file.path),
5369
6308
  line,
5370
6309
  snippet: snippet(file.content, line)
5371
6310
  }
@@ -5378,11 +6317,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5378
6317
  function edgeTypeFromEndpoint(ep) {
5379
6318
  switch (ep.edgeType) {
5380
6319
  case "PUBLISHES_TO":
5381
- return import_types17.EdgeType.PUBLISHES_TO;
6320
+ return import_types20.EdgeType.PUBLISHES_TO;
5382
6321
  case "CONSUMES_FROM":
5383
- return import_types17.EdgeType.CONSUMES_FROM;
6322
+ return import_types20.EdgeType.CONSUMES_FROM;
5384
6323
  default:
5385
- return import_types17.EdgeType.CALLS;
6324
+ return import_types20.EdgeType.CALLS;
5386
6325
  }
5387
6326
  }
5388
6327
  function isAwsKind(kind) {
@@ -5410,7 +6349,7 @@ async function addExternalEndpointEdges(graph, services) {
5410
6349
  if (!graph.hasNode(ep.infraId)) {
5411
6350
  const node = {
5412
6351
  id: ep.infraId,
5413
- type: import_types17.NodeType.InfraNode,
6352
+ type: import_types20.NodeType.InfraNode,
5414
6353
  name: ep.name,
5415
6354
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
5416
6355
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -5422,7 +6361,7 @@ async function addExternalEndpointEdges(graph, services) {
5422
6361
  nodesAdded++;
5423
6362
  }
5424
6363
  const edgeType = edgeTypeFromEndpoint(ep);
5425
- const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
6364
+ const confidence = (0, import_types20.confidenceForExtracted)(ep.confidenceKind);
5426
6365
  const relFile = toPosix2(ep.evidence.file);
5427
6366
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5428
6367
  graph,
@@ -5432,7 +6371,7 @@ async function addExternalEndpointEdges(graph, services) {
5432
6371
  );
5433
6372
  nodesAdded += n;
5434
6373
  edgesAdded += e;
5435
- if (!(0, import_types17.passesExtractedFloor)(confidence)) {
6374
+ if (!(0, import_types20.passesExtractedFloor)(confidence)) {
5436
6375
  noteExtractedDropped({
5437
6376
  source: fileNodeId,
5438
6377
  target: ep.infraId,
@@ -5452,7 +6391,7 @@ async function addExternalEndpointEdges(graph, services) {
5452
6391
  source: fileNodeId,
5453
6392
  target: ep.infraId,
5454
6393
  type: edgeType,
5455
- provenance: import_types17.Provenance.EXTRACTED,
6394
+ provenance: import_types20.Provenance.EXTRACTED,
5456
6395
  confidence,
5457
6396
  evidence: ep.evidence
5458
6397
  };
@@ -5466,9 +6405,10 @@ async function addExternalEndpointEdges(graph, services) {
5466
6405
  async function addCallEdges(graph, services) {
5467
6406
  const http = await addHttpCallEdges(graph, services);
5468
6407
  const ext = await addExternalEndpointEdges(graph, services);
6408
+ const routes = await addRouteCallEdges(graph, services);
5469
6409
  return {
5470
- nodesAdded: http.nodesAdded + ext.nodesAdded,
5471
- edgesAdded: http.edgesAdded + ext.edgesAdded
6410
+ nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
6411
+ edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
5472
6412
  };
5473
6413
  }
5474
6414
 
@@ -5477,16 +6417,16 @@ init_cjs_shims();
5477
6417
 
5478
6418
  // src/extract/infra/docker-compose.ts
5479
6419
  init_cjs_shims();
5480
- var import_node_path28 = __toESM(require("path"), 1);
5481
- var import_types19 = require("@neat.is/types");
6420
+ var import_node_path31 = __toESM(require("path"), 1);
6421
+ var import_types22 = require("@neat.is/types");
5482
6422
 
5483
6423
  // src/extract/infra/shared.ts
5484
6424
  init_cjs_shims();
5485
- var import_types18 = require("@neat.is/types");
6425
+ var import_types21 = require("@neat.is/types");
5486
6426
  function makeInfraNode(kind, name, provider = "self", extras) {
5487
6427
  return {
5488
- id: (0, import_types18.infraId)(kind, name),
5489
- type: import_types18.NodeType.InfraNode,
6428
+ id: (0, import_types21.infraId)(kind, name),
6429
+ type: import_types21.NodeType.InfraNode,
5490
6430
  name,
5491
6431
  provider,
5492
6432
  kind,
@@ -5515,7 +6455,7 @@ function dependsOnList(value) {
5515
6455
  }
5516
6456
  function serviceNameToServiceNode(name, services) {
5517
6457
  for (const s of services) {
5518
- if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
6458
+ if (s.node.name === name || import_node_path31.default.basename(s.dir) === name) return s.node.id;
5519
6459
  }
5520
6460
  return null;
5521
6461
  }
@@ -5524,7 +6464,7 @@ async function addComposeInfra(graph, scanPath, services) {
5524
6464
  let edgesAdded = 0;
5525
6465
  let composePath = null;
5526
6466
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5527
- const abs = import_node_path28.default.join(scanPath, name);
6467
+ const abs = import_node_path31.default.join(scanPath, name);
5528
6468
  if (await exists(abs)) {
5529
6469
  composePath = abs;
5530
6470
  break;
@@ -5537,13 +6477,13 @@ async function addComposeInfra(graph, scanPath, services) {
5537
6477
  } catch (err) {
5538
6478
  recordExtractionError(
5539
6479
  "infra docker-compose",
5540
- import_node_path28.default.relative(scanPath, composePath),
6480
+ import_node_path31.default.relative(scanPath, composePath),
5541
6481
  err
5542
6482
  );
5543
6483
  return { nodesAdded, edgesAdded };
5544
6484
  }
5545
6485
  if (!compose?.services) return { nodesAdded, edgesAdded };
5546
- const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
6486
+ const evidenceFile = import_node_path31.default.relative(scanPath, composePath).split(import_node_path31.default.sep).join("/");
5547
6487
  const composeNameToNodeId = /* @__PURE__ */ new Map();
5548
6488
  for (const [composeName, svc] of Object.entries(compose.services)) {
5549
6489
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -5565,15 +6505,15 @@ async function addComposeInfra(graph, scanPath, services) {
5565
6505
  for (const dep of dependsOnList(svc.depends_on)) {
5566
6506
  const targetId = composeNameToNodeId.get(dep);
5567
6507
  if (!targetId) continue;
5568
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
6508
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types22.EdgeType.DEPENDS_ON);
5569
6509
  if (graph.hasEdge(edgeId)) continue;
5570
6510
  const edge = {
5571
6511
  id: edgeId,
5572
6512
  source: sourceId,
5573
6513
  target: targetId,
5574
- type: import_types19.EdgeType.DEPENDS_ON,
5575
- provenance: import_types19.Provenance.EXTRACTED,
5576
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
6514
+ type: import_types22.EdgeType.DEPENDS_ON,
6515
+ provenance: import_types22.Provenance.EXTRACTED,
6516
+ confidence: (0, import_types22.confidenceForExtracted)("structural"),
5577
6517
  evidence: { file: evidenceFile }
5578
6518
  };
5579
6519
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5585,9 +6525,9 @@ async function addComposeInfra(graph, scanPath, services) {
5585
6525
 
5586
6526
  // src/extract/infra/dockerfile.ts
5587
6527
  init_cjs_shims();
5588
- var import_node_path29 = __toESM(require("path"), 1);
5589
- var import_node_fs15 = require("fs");
5590
- var import_types20 = require("@neat.is/types");
6528
+ var import_node_path32 = __toESM(require("path"), 1);
6529
+ var import_node_fs16 = require("fs");
6530
+ var import_types23 = require("@neat.is/types");
5591
6531
  function readDockerfile(content) {
5592
6532
  let image = null;
5593
6533
  const ports = [];
@@ -5616,15 +6556,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5616
6556
  let nodesAdded = 0;
5617
6557
  let edgesAdded = 0;
5618
6558
  for (const service of services) {
5619
- const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
6559
+ const dockerfilePath = import_node_path32.default.join(service.dir, "Dockerfile");
5620
6560
  if (!await exists(dockerfilePath)) continue;
5621
6561
  let content;
5622
6562
  try {
5623
- content = await import_node_fs15.promises.readFile(dockerfilePath, "utf8");
6563
+ content = await import_node_fs16.promises.readFile(dockerfilePath, "utf8");
5624
6564
  } catch (err) {
5625
6565
  recordExtractionError(
5626
6566
  "infra dockerfile",
5627
- import_node_path29.default.relative(scanPath, dockerfilePath),
6567
+ import_node_path32.default.relative(scanPath, dockerfilePath),
5628
6568
  err
5629
6569
  );
5630
6570
  continue;
@@ -5636,8 +6576,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5636
6576
  graph.addNode(node.id, node);
5637
6577
  nodesAdded++;
5638
6578
  }
5639
- const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5640
- const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
6579
+ const relDockerfile = toPosix2(import_node_path32.default.relative(service.dir, dockerfilePath));
6580
+ const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, dockerfilePath));
5641
6581
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5642
6582
  graph,
5643
6583
  service.pkg.name,
@@ -5646,15 +6586,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5646
6586
  );
5647
6587
  nodesAdded += fn;
5648
6588
  edgesAdded += fe;
5649
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
6589
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types23.EdgeType.RUNS_ON);
5650
6590
  if (!graph.hasEdge(edgeId)) {
5651
6591
  const edge = {
5652
6592
  id: edgeId,
5653
6593
  source: fileNodeId,
5654
6594
  target: node.id,
5655
- type: import_types20.EdgeType.RUNS_ON,
5656
- provenance: import_types20.Provenance.EXTRACTED,
5657
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6595
+ type: import_types23.EdgeType.RUNS_ON,
6596
+ provenance: import_types23.Provenance.EXTRACTED,
6597
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
5658
6598
  evidence: {
5659
6599
  file: evidenceFile,
5660
6600
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -5669,15 +6609,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5669
6609
  graph.addNode(portNode.id, portNode);
5670
6610
  nodesAdded++;
5671
6611
  }
5672
- const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
6612
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types23.EdgeType.CONNECTS_TO);
5673
6613
  if (graph.hasEdge(portEdgeId)) continue;
5674
6614
  const portEdge = {
5675
6615
  id: portEdgeId,
5676
6616
  source: fileNodeId,
5677
6617
  target: portNode.id,
5678
- type: import_types20.EdgeType.CONNECTS_TO,
5679
- provenance: import_types20.Provenance.EXTRACTED,
5680
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6618
+ type: import_types23.EdgeType.CONNECTS_TO,
6619
+ provenance: import_types23.Provenance.EXTRACTED,
6620
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
5681
6621
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5682
6622
  };
5683
6623
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -5689,23 +6629,23 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5689
6629
 
5690
6630
  // src/extract/infra/terraform.ts
5691
6631
  init_cjs_shims();
5692
- var import_node_fs16 = require("fs");
5693
- var import_node_path30 = __toESM(require("path"), 1);
5694
- var import_types21 = require("@neat.is/types");
6632
+ var import_node_fs17 = require("fs");
6633
+ var import_node_path33 = __toESM(require("path"), 1);
6634
+ var import_types24 = require("@neat.is/types");
5695
6635
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5696
6636
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5697
6637
  async function walkTfFiles(start, depth = 0, max = 5) {
5698
6638
  if (depth > max) return [];
5699
6639
  const out = [];
5700
- const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
6640
+ const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5701
6641
  for (const entry of entries) {
5702
6642
  if (entry.isDirectory()) {
5703
6643
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
5704
- const child = import_node_path30.default.join(start, entry.name);
6644
+ const child = import_node_path33.default.join(start, entry.name);
5705
6645
  if (await isPythonVenvDir(child)) continue;
5706
6646
  out.push(...await walkTfFiles(child, depth + 1, max));
5707
6647
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
5708
- out.push(import_node_path30.default.join(start, entry.name));
6648
+ out.push(import_node_path33.default.join(start, entry.name));
5709
6649
  }
5710
6650
  }
5711
6651
  return out;
@@ -5724,7 +6664,7 @@ function blockBody(content, from) {
5724
6664
  }
5725
6665
  return null;
5726
6666
  }
5727
- function lineAt(content, index) {
6667
+ function lineAt2(content, index) {
5728
6668
  let line = 1;
5729
6669
  for (let i = 0; i < index && i < content.length; i++) {
5730
6670
  if (content[i] === "\n") line++;
@@ -5736,8 +6676,8 @@ async function addTerraformResources(graph, scanPath) {
5736
6676
  let edgesAdded = 0;
5737
6677
  const files = await walkTfFiles(scanPath);
5738
6678
  for (const file of files) {
5739
- const content = await import_node_fs16.promises.readFile(file, "utf8");
5740
- const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
6679
+ const content = await import_node_fs17.promises.readFile(file, "utf8");
6680
+ const evidenceFile = toPosix2(import_node_path33.default.relative(scanPath, file));
5741
6681
  const resources = [];
5742
6682
  const byKey = /* @__PURE__ */ new Map();
5743
6683
  RESOURCE_RE.lastIndex = 0;
@@ -5772,16 +6712,16 @@ async function addTerraformResources(graph, scanPath) {
5772
6712
  if (!target) continue;
5773
6713
  if (seen.has(target.nodeId)) continue;
5774
6714
  seen.add(target.nodeId);
5775
- const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
6715
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types24.EdgeType.DEPENDS_ON);
5776
6716
  if (graph.hasEdge(edgeId)) continue;
5777
- const line = lineAt(content, resource.bodyOffset + ref.index);
6717
+ const line = lineAt2(content, resource.bodyOffset + ref.index);
5778
6718
  const edge = {
5779
6719
  id: edgeId,
5780
6720
  source: resource.nodeId,
5781
6721
  target: target.nodeId,
5782
- type: import_types21.EdgeType.DEPENDS_ON,
5783
- provenance: import_types21.Provenance.EXTRACTED,
5784
- confidence: (0, import_types21.confidenceForExtracted)("structural"),
6722
+ type: import_types24.EdgeType.DEPENDS_ON,
6723
+ provenance: import_types24.Provenance.EXTRACTED,
6724
+ confidence: (0, import_types24.confidenceForExtracted)("structural"),
5785
6725
  evidence: { file: evidenceFile, line, snippet: key }
5786
6726
  };
5787
6727
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5794,8 +6734,8 @@ async function addTerraformResources(graph, scanPath) {
5794
6734
 
5795
6735
  // src/extract/infra/k8s.ts
5796
6736
  init_cjs_shims();
5797
- var import_node_fs17 = require("fs");
5798
- var import_node_path31 = __toESM(require("path"), 1);
6737
+ var import_node_fs18 = require("fs");
6738
+ var import_node_path34 = __toESM(require("path"), 1);
5799
6739
  var import_yaml3 = require("yaml");
5800
6740
  var K8S_KIND_TO_INFRA_KIND = {
5801
6741
  Service: "k8s-service",
@@ -5809,15 +6749,15 @@ var K8S_KIND_TO_INFRA_KIND = {
5809
6749
  async function walkYamlFiles2(start, depth = 0, max = 5) {
5810
6750
  if (depth > max) return [];
5811
6751
  const out = [];
5812
- const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
6752
+ const entries = await import_node_fs18.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5813
6753
  for (const entry of entries) {
5814
6754
  if (entry.isDirectory()) {
5815
6755
  if (IGNORED_DIRS.has(entry.name)) continue;
5816
- const child = import_node_path31.default.join(start, entry.name);
6756
+ const child = import_node_path34.default.join(start, entry.name);
5817
6757
  if (await isPythonVenvDir(child)) continue;
5818
6758
  out.push(...await walkYamlFiles2(child, depth + 1, max));
5819
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry.name))) {
5820
- out.push(import_node_path31.default.join(start, entry.name));
6759
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path34.default.extname(entry.name))) {
6760
+ out.push(import_node_path34.default.join(start, entry.name));
5821
6761
  }
5822
6762
  }
5823
6763
  return out;
@@ -5826,7 +6766,7 @@ async function addK8sResources(graph, scanPath) {
5826
6766
  let nodesAdded = 0;
5827
6767
  const files = await walkYamlFiles2(scanPath);
5828
6768
  for (const file of files) {
5829
- const content = await import_node_fs17.promises.readFile(file, "utf8");
6769
+ const content = await import_node_fs18.promises.readFile(file, "utf8");
5830
6770
  let docs;
5831
6771
  try {
5832
6772
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -5861,17 +6801,17 @@ async function addInfra(graph, scanPath, services) {
5861
6801
  }
5862
6802
 
5863
6803
  // src/extract/index.ts
5864
- var import_node_path33 = __toESM(require("path"), 1);
6804
+ var import_node_path36 = __toESM(require("path"), 1);
5865
6805
 
5866
6806
  // src/extract/retire.ts
5867
6807
  init_cjs_shims();
5868
- var import_node_fs18 = require("fs");
5869
- var import_node_path32 = __toESM(require("path"), 1);
5870
- var import_types22 = require("@neat.is/types");
6808
+ var import_node_fs19 = require("fs");
6809
+ var import_node_path35 = __toESM(require("path"), 1);
6810
+ var import_types25 = require("@neat.is/types");
5871
6811
  function dropOrphanedFileNodes(graph) {
5872
6812
  const orphans = [];
5873
6813
  graph.forEachNode((id, attrs) => {
5874
- if (attrs.type !== import_types22.NodeType.FileNode) return;
6814
+ if (attrs.type !== import_types25.NodeType.FileNode) return;
5875
6815
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5876
6816
  orphans.push(id);
5877
6817
  }
@@ -5884,14 +6824,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5884
6824
  const bases = [scanPath, ...serviceDirs];
5885
6825
  graph.forEachEdge((id, attrs) => {
5886
6826
  const edge = attrs;
5887
- if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
6827
+ if (edge.provenance !== import_types25.Provenance.EXTRACTED) return;
5888
6828
  const evidenceFile = edge.evidence?.file;
5889
6829
  if (!evidenceFile) return;
5890
- if (import_node_path32.default.isAbsolute(evidenceFile)) {
5891
- if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
6830
+ if (import_node_path35.default.isAbsolute(evidenceFile)) {
6831
+ if (!(0, import_node_fs19.existsSync)(evidenceFile)) toDrop.push(id);
5892
6832
  return;
5893
6833
  }
5894
- const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
6834
+ const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path35.default.join(base, evidenceFile)));
5895
6835
  if (!found) toDrop.push(id);
5896
6836
  });
5897
6837
  for (const id of toDrop) graph.dropEdge(id);
@@ -5910,6 +6850,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5910
6850
  const importGraph = await addImports(graph, services);
5911
6851
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
5912
6852
  const phase3 = await addConfigNodes(graph, services, scanPath);
6853
+ const routePhase = await addRoutes(graph, services);
6854
+ const grpcPhase = await addGrpcMethods(graph, services);
5913
6855
  const phase4 = await addCallEdges(graph, services);
5914
6856
  const phase5 = await addInfra(graph, scanPath, services);
5915
6857
  const ghostsRetired = retireExtractedEdgesByMissingFile(
@@ -5931,7 +6873,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5931
6873
  }
5932
6874
  const droppedEntries = drainDroppedExtracted();
5933
6875
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5934
- const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
6876
+ const rejectedPath = import_node_path36.default.join(import_node_path36.default.dirname(opts.errorsPath), "rejected.ndjson");
5935
6877
  try {
5936
6878
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5937
6879
  } catch (err) {
@@ -5941,8 +6883,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5941
6883
  }
5942
6884
  }
5943
6885
  const result = {
5944
- nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
5945
- edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
6886
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
6887
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
5946
6888
  frontiersPromoted,
5947
6889
  extractionErrors: errorEntries.length,
5948
6890
  errorEntries,
@@ -5965,9 +6907,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5965
6907
 
5966
6908
  // src/persist.ts
5967
6909
  init_cjs_shims();
5968
- var import_node_fs19 = require("fs");
5969
- var import_node_path34 = __toESM(require("path"), 1);
5970
- var import_types23 = require("@neat.is/types");
6910
+ var import_node_fs20 = require("fs");
6911
+ var import_node_path37 = __toESM(require("path"), 1);
6912
+ var import_types26 = require("@neat.is/types");
5971
6913
  var SCHEMA_VERSION = 4;
5972
6914
  function migrateV1ToV2(payload) {
5973
6915
  const nodes = payload.graph.nodes;
@@ -5989,12 +6931,12 @@ function migrateV2ToV3(payload) {
5989
6931
  for (const edge of edges) {
5990
6932
  const attrs = edge.attributes;
5991
6933
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5992
- attrs.provenance = import_types23.Provenance.OBSERVED;
6934
+ attrs.provenance = import_types26.Provenance.OBSERVED;
5993
6935
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5994
6936
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5995
6937
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5996
6938
  if (type && source && target) {
5997
- const newId = (0, import_types23.observedEdgeId)(source, target, type);
6939
+ const newId = (0, import_types26.observedEdgeId)(source, target, type);
5998
6940
  attrs.id = newId;
5999
6941
  if (edge.key) edge.key = newId;
6000
6942
  }
@@ -6003,7 +6945,7 @@ function migrateV2ToV3(payload) {
6003
6945
  return { ...payload, schemaVersion: 3 };
6004
6946
  }
6005
6947
  async function ensureDir(filePath) {
6006
- await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
6948
+ await import_node_fs20.promises.mkdir(import_node_path37.default.dirname(filePath), { recursive: true });
6007
6949
  }
6008
6950
  async function saveGraphToDisk(graph, outPath) {
6009
6951
  await ensureDir(outPath);
@@ -6013,13 +6955,13 @@ async function saveGraphToDisk(graph, outPath) {
6013
6955
  graph: graph.export()
6014
6956
  };
6015
6957
  const tmp = `${outPath}.tmp`;
6016
- await import_node_fs19.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
6017
- await import_node_fs19.promises.rename(tmp, outPath);
6958
+ await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
6959
+ await import_node_fs20.promises.rename(tmp, outPath);
6018
6960
  }
6019
6961
  async function loadGraphFromDisk(graph, outPath) {
6020
6962
  let raw;
6021
6963
  try {
6022
- raw = await import_node_fs19.promises.readFile(outPath, "utf8");
6964
+ raw = await import_node_fs20.promises.readFile(outPath, "utf8");
6023
6965
  } catch (err) {
6024
6966
  if (err.code === "ENOENT") return;
6025
6967
  throw err;
@@ -6086,19 +7028,19 @@ function startPersistLoop(graph, outPath, opts = {}) {
6086
7028
  init_cjs_shims();
6087
7029
  var import_fastify = __toESM(require("fastify"), 1);
6088
7030
  var import_cors = __toESM(require("@fastify/cors"), 1);
6089
- var import_types26 = require("@neat.is/types");
7031
+ var import_types29 = require("@neat.is/types");
6090
7032
 
6091
7033
  // src/extend/index.ts
6092
7034
  init_cjs_shims();
6093
- var import_node_fs21 = require("fs");
6094
- var import_node_path36 = __toESM(require("path"), 1);
7035
+ var import_node_fs22 = require("fs");
7036
+ var import_node_path39 = __toESM(require("path"), 1);
6095
7037
  var import_node_os2 = __toESM(require("os"), 1);
6096
7038
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
6097
7039
 
6098
7040
  // src/installers/package-manager.ts
6099
7041
  init_cjs_shims();
6100
- var import_node_fs20 = require("fs");
6101
- var import_node_path35 = __toESM(require("path"), 1);
7042
+ var import_node_fs21 = require("fs");
7043
+ var import_node_path38 = __toESM(require("path"), 1);
6102
7044
  var import_node_child_process = require("child_process");
6103
7045
  var LOCKFILE_PRIORITY = [
6104
7046
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -6113,29 +7055,29 @@ var LOCKFILE_PRIORITY = [
6113
7055
  var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
6114
7056
  async function exists2(p) {
6115
7057
  try {
6116
- await import_node_fs20.promises.access(p);
7058
+ await import_node_fs21.promises.access(p);
6117
7059
  return true;
6118
7060
  } catch {
6119
7061
  return false;
6120
7062
  }
6121
7063
  }
6122
7064
  async function detectPackageManager(serviceDir) {
6123
- let dir = import_node_path35.default.resolve(serviceDir);
7065
+ let dir = import_node_path38.default.resolve(serviceDir);
6124
7066
  const stops = /* @__PURE__ */ new Set();
6125
7067
  for (let i = 0; i < 64; i++) {
6126
7068
  if (stops.has(dir)) break;
6127
7069
  stops.add(dir);
6128
7070
  for (const candidate of LOCKFILE_PRIORITY) {
6129
- const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
7071
+ const lockPath = import_node_path38.default.join(dir, candidate.lockfile);
6130
7072
  if (await exists2(lockPath)) {
6131
7073
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
6132
7074
  }
6133
7075
  }
6134
- const parent = import_node_path35.default.dirname(dir);
7076
+ const parent = import_node_path38.default.dirname(dir);
6135
7077
  if (parent === dir) break;
6136
7078
  dir = parent;
6137
7079
  }
6138
- return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7080
+ return { pm: "npm", cwd: import_node_path38.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
6139
7081
  }
6140
7082
  async function runPackageManagerInstall(cmd) {
6141
7083
  return new Promise((resolve) => {
@@ -6177,30 +7119,30 @@ ${err.message}`
6177
7119
  // src/extend/index.ts
6178
7120
  async function fileExists2(p) {
6179
7121
  try {
6180
- await import_node_fs21.promises.access(p);
7122
+ await import_node_fs22.promises.access(p);
6181
7123
  return true;
6182
7124
  } catch {
6183
7125
  return false;
6184
7126
  }
6185
7127
  }
6186
7128
  async function readPackageJson(scanPath) {
6187
- const pkgPath = import_node_path36.default.join(scanPath, "package.json");
6188
- const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
7129
+ const pkgPath = import_node_path39.default.join(scanPath, "package.json");
7130
+ const raw = await import_node_fs22.promises.readFile(pkgPath, "utf8");
6189
7131
  return JSON.parse(raw);
6190
7132
  }
6191
7133
  async function findHookFiles(scanPath) {
6192
- const entries = await import_node_fs21.promises.readdir(scanPath);
7134
+ const entries = await import_node_fs22.promises.readdir(scanPath);
6193
7135
  return entries.filter(
6194
7136
  (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
6195
7137
  ).sort();
6196
7138
  }
6197
7139
  function extendLogPath() {
6198
- return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7140
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path39.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
6199
7141
  }
6200
7142
  async function appendExtendLog(entry) {
6201
7143
  const logPath = extendLogPath();
6202
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
6203
- await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
7144
+ await import_node_fs22.promises.mkdir(import_node_path39.default.dirname(logPath), { recursive: true });
7145
+ await import_node_fs22.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
6204
7146
  }
6205
7147
  function splicedContent(fileContent, snippet2) {
6206
7148
  if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
@@ -6258,7 +7200,7 @@ function lookupInstrumentation(library, installedVersion) {
6258
7200
  }
6259
7201
  async function describeProjectInstrumentation(ctx) {
6260
7202
  const hookFiles = await findHookFiles(ctx.scanPath);
6261
- const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
7203
+ const envNeat = await fileExists2(import_node_path39.default.join(ctx.scanPath, ".env.neat"));
6262
7204
  const registryInstrPackages = new Set(
6263
7205
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
6264
7206
  );
@@ -6280,31 +7222,31 @@ async function applyExtension(ctx, args, options) {
6280
7222
  );
6281
7223
  }
6282
7224
  for (const file of hookFiles) {
6283
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
7225
+ const content = await import_node_fs22.promises.readFile(import_node_path39.default.join(ctx.scanPath, file), "utf8");
6284
7226
  if (content.includes(args.registration_snippet)) {
6285
7227
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
6286
7228
  }
6287
7229
  }
6288
7230
  const primaryFile = hookFiles[0];
6289
- const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
7231
+ const primaryPath = import_node_path39.default.join(ctx.scanPath, primaryFile);
6290
7232
  const filesTouched = [];
6291
7233
  const depsAdded = [];
6292
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
7234
+ const pkgPath = import_node_path39.default.join(ctx.scanPath, "package.json");
6293
7235
  const pkg = await readPackageJson(ctx.scanPath);
6294
7236
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
6295
7237
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
6296
- await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7238
+ await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
6297
7239
  filesTouched.push("package.json");
6298
7240
  depsAdded.push(`${args.instrumentation_package}@${args.version}`);
6299
7241
  }
6300
- const hookContent = await import_node_fs21.promises.readFile(primaryPath, "utf8");
7242
+ const hookContent = await import_node_fs22.promises.readFile(primaryPath, "utf8");
6301
7243
  const patched = splicedContent(hookContent, args.registration_snippet);
6302
7244
  if (!patched) {
6303
7245
  throw new Error(
6304
7246
  `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
6305
7247
  );
6306
7248
  }
6307
- await import_node_fs21.promises.writeFile(primaryPath, patched, "utf8");
7249
+ await import_node_fs22.promises.writeFile(primaryPath, patched, "utf8");
6308
7250
  filesTouched.push(primaryFile);
6309
7251
  const cmd = await detectPackageManager(ctx.scanPath);
6310
7252
  const installer = options?.runInstall ?? runPackageManagerInstall;
@@ -6335,7 +7277,7 @@ async function dryRunExtension(ctx, args) {
6335
7277
  };
6336
7278
  }
6337
7279
  for (const file of hookFiles) {
6338
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
7280
+ const content = await import_node_fs22.promises.readFile(import_node_path39.default.join(ctx.scanPath, file), "utf8");
6339
7281
  if (content.includes(args.registration_snippet)) {
6340
7282
  return {
6341
7283
  library: args.library,
@@ -6357,7 +7299,7 @@ async function dryRunExtension(ctx, args) {
6357
7299
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
6358
7300
  filesTouched.push("package.json");
6359
7301
  }
6360
- const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
7302
+ const hookContent = await import_node_fs22.promises.readFile(import_node_path39.default.join(ctx.scanPath, primaryFile), "utf8");
6361
7303
  const patched = splicedContent(hookContent, args.registration_snippet);
6362
7304
  if (patched) {
6363
7305
  filesTouched.push(primaryFile);
@@ -6372,28 +7314,28 @@ async function rollbackExtension(ctx, args) {
6372
7314
  if (!await fileExists2(logPath)) {
6373
7315
  return { undone: false, message: "no apply found for library" };
6374
7316
  }
6375
- const raw = await import_node_fs21.promises.readFile(logPath, "utf8");
7317
+ const raw = await import_node_fs22.promises.readFile(logPath, "utf8");
6376
7318
  const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
6377
7319
  const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
6378
7320
  if (!match) {
6379
7321
  return { undone: false, message: "no apply found for library" };
6380
7322
  }
6381
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
7323
+ const pkgPath = import_node_path39.default.join(ctx.scanPath, "package.json");
6382
7324
  if (await fileExists2(pkgPath)) {
6383
7325
  const pkg = await readPackageJson(ctx.scanPath);
6384
7326
  if (pkg.dependencies?.[match.instrumentation_package]) {
6385
7327
  const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
6386
7328
  pkg.dependencies = rest;
6387
- await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7329
+ await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
6388
7330
  }
6389
7331
  }
6390
7332
  const hookFiles = await findHookFiles(ctx.scanPath);
6391
7333
  for (const file of hookFiles) {
6392
- const filePath = import_node_path36.default.join(ctx.scanPath, file);
6393
- const content = await import_node_fs21.promises.readFile(filePath, "utf8");
7334
+ const filePath = import_node_path39.default.join(ctx.scanPath, file);
7335
+ const content = await import_node_fs22.promises.readFile(filePath, "utf8");
6394
7336
  if (content.includes(match.registration_snippet)) {
6395
7337
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
6396
- await import_node_fs21.promises.writeFile(filePath, filtered, "utf8");
7338
+ await import_node_fs22.promises.writeFile(filePath, filtered, "utf8");
6397
7339
  break;
6398
7340
  }
6399
7341
  }
@@ -6405,7 +7347,7 @@ async function rollbackExtension(ctx, args) {
6405
7347
 
6406
7348
  // src/divergences.ts
6407
7349
  init_cjs_shims();
6408
- var import_types24 = require("@neat.is/types");
7350
+ var import_types27 = require("@neat.is/types");
6409
7351
  function bucketKey(source, target, type) {
6410
7352
  return `${type}|${source}|${target}`;
6411
7353
  }
@@ -6413,22 +7355,22 @@ function bucketEdges(graph) {
6413
7355
  const buckets = /* @__PURE__ */ new Map();
6414
7356
  graph.forEachEdge((id, attrs) => {
6415
7357
  const e = attrs;
6416
- const parsed = (0, import_types24.parseEdgeId)(id);
7358
+ const parsed = (0, import_types27.parseEdgeId)(id);
6417
7359
  const provenance = parsed?.provenance ?? e.provenance;
6418
7360
  const key = bucketKey(e.source, e.target, e.type);
6419
7361
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
6420
7362
  switch (provenance) {
6421
- case import_types24.Provenance.EXTRACTED:
7363
+ case import_types27.Provenance.EXTRACTED:
6422
7364
  cur.extracted = e;
6423
7365
  break;
6424
- case import_types24.Provenance.OBSERVED:
7366
+ case import_types27.Provenance.OBSERVED:
6425
7367
  cur.observed = e;
6426
7368
  break;
6427
- case import_types24.Provenance.INFERRED:
7369
+ case import_types27.Provenance.INFERRED:
6428
7370
  cur.inferred = e;
6429
7371
  break;
6430
7372
  default:
6431
- if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
7373
+ if (e.provenance === import_types27.Provenance.STALE) cur.stale = e;
6432
7374
  }
6433
7375
  buckets.set(key, cur);
6434
7376
  });
@@ -6437,7 +7379,12 @@ function bucketEdges(graph) {
6437
7379
  function nodeIsFrontier(graph, nodeId) {
6438
7380
  if (!graph.hasNode(nodeId)) return false;
6439
7381
  const attrs = graph.getNodeAttributes(nodeId);
6440
- return attrs.type === import_types24.NodeType.FrontierNode;
7382
+ return attrs.type === import_types27.NodeType.FrontierNode;
7383
+ }
7384
+ function nodeIsWebsocketChannel(graph, nodeId) {
7385
+ if (!graph.hasNode(nodeId)) return false;
7386
+ const attrs = graph.getNodeAttributes(nodeId);
7387
+ return attrs.type === import_types27.NodeType.WebSocketChannelNode;
6441
7388
  }
6442
7389
  function clampConfidence(n) {
6443
7390
  if (!Number.isFinite(n)) return 0;
@@ -6457,14 +7404,14 @@ function gradedConfidence(edge) {
6457
7404
  return clampConfidence(confidenceForEdge(edge));
6458
7405
  }
6459
7406
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6460
- import_types24.EdgeType.CALLS,
6461
- import_types24.EdgeType.CONNECTS_TO,
6462
- import_types24.EdgeType.PUBLISHES_TO,
6463
- import_types24.EdgeType.CONSUMES_FROM
7407
+ import_types27.EdgeType.CALLS,
7408
+ import_types27.EdgeType.CONNECTS_TO,
7409
+ import_types27.EdgeType.PUBLISHES_TO,
7410
+ import_types27.EdgeType.CONSUMES_FROM
6464
7411
  ]);
6465
7412
  function detectMissingDivergences(graph, bucket) {
6466
7413
  const out = [];
6467
- if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
7414
+ if (bucket.type === import_types27.EdgeType.CONTAINS) return out;
6468
7415
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
6469
7416
  if (!nodeIsFrontier(graph, bucket.target)) {
6470
7417
  out.push({
@@ -6479,7 +7426,7 @@ function detectMissingDivergences(graph, bucket) {
6479
7426
  });
6480
7427
  }
6481
7428
  }
6482
- if (bucket.observed && !bucket.extracted) {
7429
+ if (bucket.observed && !bucket.extracted && !nodeIsWebsocketChannel(graph, bucket.target)) {
6483
7430
  out.push({
6484
7431
  type: "missing-extracted",
6485
7432
  source: bucket.source,
@@ -6505,7 +7452,7 @@ function declaredHostFor(svc) {
6505
7452
  function hasExtractedConfiguredBy(graph, svcId) {
6506
7453
  for (const edgeId of graph.outboundEdges(svcId)) {
6507
7454
  const e = graph.getEdgeAttributes(edgeId);
6508
- if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
7455
+ if (e.type === import_types27.EdgeType.CONFIGURED_BY && e.provenance === import_types27.Provenance.EXTRACTED) {
6509
7456
  return true;
6510
7457
  }
6511
7458
  }
@@ -6518,10 +7465,10 @@ function detectHostMismatch(graph, svcId, svc) {
6518
7465
  const out = [];
6519
7466
  for (const edgeId of graph.outboundEdges(svcId)) {
6520
7467
  const edge = graph.getEdgeAttributes(edgeId);
6521
- if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6522
- if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
7468
+ if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7469
+ if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
6523
7470
  const target = graph.getNodeAttributes(edge.target);
6524
- if (target.type !== import_types24.NodeType.DatabaseNode) continue;
7471
+ if (target.type !== import_types27.NodeType.DatabaseNode) continue;
6525
7472
  const observedHost = target.host?.trim();
6526
7473
  if (!observedHost) continue;
6527
7474
  if (observedHost === declaredHost) continue;
@@ -6543,10 +7490,10 @@ function detectCompatDivergences(graph, svcId, svc) {
6543
7490
  const deps = svc.dependencies ?? {};
6544
7491
  for (const edgeId of graph.outboundEdges(svcId)) {
6545
7492
  const edge = graph.getEdgeAttributes(edgeId);
6546
- if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6547
- if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
7493
+ if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7494
+ if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
6548
7495
  const target = graph.getNodeAttributes(edge.target);
6549
- if (target.type !== import_types24.NodeType.DatabaseNode) continue;
7496
+ if (target.type !== import_types27.NodeType.DatabaseNode) continue;
6550
7497
  for (const pair of compatPairs()) {
6551
7498
  if (pair.engine !== target.engine) continue;
6552
7499
  const declared = deps[pair.driver];
@@ -6605,7 +7552,7 @@ function suppressHostMismatchHalves(all) {
6605
7552
  for (const d of all) {
6606
7553
  if (d.type !== "host-mismatch") continue;
6607
7554
  observedHalf.add(`${d.source}->${d.target}`);
6608
- declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
7555
+ declaredHalf.add((0, import_types27.databaseId)(d.extractedHost));
6609
7556
  }
6610
7557
  if (observedHalf.size === 0) return all;
6611
7558
  return all.filter((d) => {
@@ -6624,7 +7571,7 @@ function computeDivergences(graph, opts = {}) {
6624
7571
  }
6625
7572
  graph.forEachNode((nodeId, attrs) => {
6626
7573
  const n = attrs;
6627
- if (n.type !== import_types24.NodeType.ServiceNode) return;
7574
+ if (n.type !== import_types27.NodeType.ServiceNode) return;
6628
7575
  const svc = n;
6629
7576
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
6630
7577
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -6658,7 +7605,7 @@ function computeDivergences(graph, opts = {}) {
6658
7605
  if (a.source !== b.source) return a.source.localeCompare(b.source);
6659
7606
  return a.target.localeCompare(b.target);
6660
7607
  });
6661
- return import_types24.DivergenceResultSchema.parse({
7608
+ return import_types27.DivergenceResultSchema.parse({
6662
7609
  divergences: filtered,
6663
7610
  totalAffected: filtered.length,
6664
7611
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6667,7 +7614,7 @@ function computeDivergences(graph, opts = {}) {
6667
7614
 
6668
7615
  // src/diff.ts
6669
7616
  init_cjs_shims();
6670
- var import_node_fs22 = require("fs");
7617
+ var import_node_fs23 = require("fs");
6671
7618
  async function loadSnapshotForDiff(target) {
6672
7619
  if (/^https?:\/\//i.test(target)) {
6673
7620
  const res = await fetch(target);
@@ -6676,7 +7623,7 @@ async function loadSnapshotForDiff(target) {
6676
7623
  }
6677
7624
  return await res.json();
6678
7625
  }
6679
- const raw = await import_node_fs22.promises.readFile(target, "utf8");
7626
+ const raw = await import_node_fs23.promises.readFile(target, "utf8");
6680
7627
  return JSON.parse(raw);
6681
7628
  }
6682
7629
  function indexEntries(entries) {
@@ -6744,23 +7691,23 @@ function canonicalJson(value) {
6744
7691
 
6745
7692
  // src/projects.ts
6746
7693
  init_cjs_shims();
6747
- var import_node_path37 = __toESM(require("path"), 1);
7694
+ var import_node_path40 = __toESM(require("path"), 1);
6748
7695
  function pathsForProject(project, baseDir) {
6749
7696
  if (project === DEFAULT_PROJECT) {
6750
7697
  return {
6751
- snapshotPath: import_node_path37.default.join(baseDir, "graph.json"),
6752
- errorsPath: import_node_path37.default.join(baseDir, "errors.ndjson"),
6753
- staleEventsPath: import_node_path37.default.join(baseDir, "stale-events.ndjson"),
6754
- embeddingsCachePath: import_node_path37.default.join(baseDir, "embeddings.json"),
6755
- policyViolationsPath: import_node_path37.default.join(baseDir, "policy-violations.ndjson")
7698
+ snapshotPath: import_node_path40.default.join(baseDir, "graph.json"),
7699
+ errorsPath: import_node_path40.default.join(baseDir, "errors.ndjson"),
7700
+ staleEventsPath: import_node_path40.default.join(baseDir, "stale-events.ndjson"),
7701
+ embeddingsCachePath: import_node_path40.default.join(baseDir, "embeddings.json"),
7702
+ policyViolationsPath: import_node_path40.default.join(baseDir, "policy-violations.ndjson")
6756
7703
  };
6757
7704
  }
6758
7705
  return {
6759
- snapshotPath: import_node_path37.default.join(baseDir, `${project}.json`),
6760
- errorsPath: import_node_path37.default.join(baseDir, `errors.${project}.ndjson`),
6761
- staleEventsPath: import_node_path37.default.join(baseDir, `stale-events.${project}.ndjson`),
6762
- embeddingsCachePath: import_node_path37.default.join(baseDir, `embeddings.${project}.json`),
6763
- policyViolationsPath: import_node_path37.default.join(baseDir, `policy-violations.${project}.ndjson`)
7706
+ snapshotPath: import_node_path40.default.join(baseDir, `${project}.json`),
7707
+ errorsPath: import_node_path40.default.join(baseDir, `errors.${project}.ndjson`),
7708
+ staleEventsPath: import_node_path40.default.join(baseDir, `stale-events.${project}.ndjson`),
7709
+ embeddingsCachePath: import_node_path40.default.join(baseDir, `embeddings.${project}.json`),
7710
+ policyViolationsPath: import_node_path40.default.join(baseDir, `policy-violations.${project}.ndjson`)
6764
7711
  };
6765
7712
  }
6766
7713
  var Projects = class {
@@ -6796,25 +7743,25 @@ var Projects = class {
6796
7743
 
6797
7744
  // src/registry.ts
6798
7745
  init_cjs_shims();
6799
- var import_node_fs23 = require("fs");
7746
+ var import_node_fs24 = require("fs");
6800
7747
  var import_node_os3 = __toESM(require("os"), 1);
6801
- var import_node_path38 = __toESM(require("path"), 1);
6802
- var import_types25 = require("@neat.is/types");
7748
+ var import_node_path41 = __toESM(require("path"), 1);
7749
+ var import_types28 = require("@neat.is/types");
6803
7750
  var LOCK_TIMEOUT_MS = 5e3;
6804
7751
  var LOCK_RETRY_MS = 50;
6805
7752
  function neatHome() {
6806
7753
  const override = process.env.NEAT_HOME;
6807
- if (override && override.length > 0) return import_node_path38.default.resolve(override);
6808
- return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
7754
+ if (override && override.length > 0) return import_node_path41.default.resolve(override);
7755
+ return import_node_path41.default.join(import_node_os3.default.homedir(), ".neat");
6809
7756
  }
6810
7757
  function registryPath() {
6811
- return import_node_path38.default.join(neatHome(), "projects.json");
7758
+ return import_node_path41.default.join(neatHome(), "projects.json");
6812
7759
  }
6813
7760
  function registryLockPath() {
6814
- return import_node_path38.default.join(neatHome(), "projects.json.lock");
7761
+ return import_node_path41.default.join(neatHome(), "projects.json.lock");
6815
7762
  }
6816
7763
  function daemonPidPath() {
6817
- return import_node_path38.default.join(neatHome(), "neatd.pid");
7764
+ return import_node_path41.default.join(neatHome(), "neatd.pid");
6818
7765
  }
6819
7766
  function isPidAliveDefault(pid) {
6820
7767
  try {
@@ -6826,7 +7773,7 @@ function isPidAliveDefault(pid) {
6826
7773
  }
6827
7774
  async function readPidFile(file) {
6828
7775
  try {
6829
- const raw = await import_node_fs23.promises.readFile(file, "utf8");
7776
+ const raw = await import_node_fs24.promises.readFile(file, "utf8");
6830
7777
  const pid = Number.parseInt(raw.trim(), 10);
6831
7778
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
6832
7779
  } catch {
@@ -6874,32 +7821,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
6874
7821
  }
6875
7822
  }
6876
7823
  async function normalizeProjectPath(input) {
6877
- const resolved = import_node_path38.default.resolve(input);
7824
+ const resolved = import_node_path41.default.resolve(input);
6878
7825
  try {
6879
- return await import_node_fs23.promises.realpath(resolved);
7826
+ return await import_node_fs24.promises.realpath(resolved);
6880
7827
  } catch {
6881
7828
  return resolved;
6882
7829
  }
6883
7830
  }
6884
7831
  async function writeAtomically(target, contents) {
6885
- await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
7832
+ await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(target), { recursive: true });
6886
7833
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
6887
- const fd = await import_node_fs23.promises.open(tmp, "w");
7834
+ const fd = await import_node_fs24.promises.open(tmp, "w");
6888
7835
  try {
6889
7836
  await fd.writeFile(contents, "utf8");
6890
7837
  await fd.sync();
6891
7838
  } finally {
6892
7839
  await fd.close();
6893
7840
  }
6894
- await import_node_fs23.promises.rename(tmp, target);
7841
+ await import_node_fs24.promises.rename(tmp, target);
6895
7842
  }
6896
7843
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6897
7844
  const deadline = Date.now() + timeoutMs;
6898
- await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
7845
+ await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(lockPath), { recursive: true });
6899
7846
  let probedHolder = false;
6900
7847
  while (true) {
6901
7848
  try {
6902
- const fd = await import_node_fs23.promises.open(lockPath, "wx");
7849
+ const fd = await import_node_fs24.promises.open(lockPath, "wx");
6903
7850
  try {
6904
7851
  await fd.writeFile(`${process.pid}
6905
7852
  `, "utf8");
@@ -6924,7 +7871,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
6924
7871
  }
6925
7872
  }
6926
7873
  async function releaseLock(lockPath) {
6927
- await import_node_fs23.promises.unlink(lockPath).catch(() => {
7874
+ await import_node_fs24.promises.unlink(lockPath).catch(() => {
6928
7875
  });
6929
7876
  }
6930
7877
  async function withLock(fn) {
@@ -6940,7 +7887,7 @@ async function readRegistry() {
6940
7887
  const file = registryPath();
6941
7888
  let raw;
6942
7889
  try {
6943
- raw = await import_node_fs23.promises.readFile(file, "utf8");
7890
+ raw = await import_node_fs24.promises.readFile(file, "utf8");
6944
7891
  } catch (err) {
6945
7892
  if (err.code === "ENOENT") {
6946
7893
  return { version: 1, projects: [] };
@@ -6948,10 +7895,10 @@ async function readRegistry() {
6948
7895
  throw err;
6949
7896
  }
6950
7897
  const parsed = JSON.parse(raw);
6951
- return import_types25.RegistryFileSchema.parse(parsed);
7898
+ return import_types28.RegistryFileSchema.parse(parsed);
6952
7899
  }
6953
7900
  async function writeRegistry(reg) {
6954
- const validated = import_types25.RegistryFileSchema.parse(reg);
7901
+ const validated = import_types28.RegistryFileSchema.parse(reg);
6955
7902
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6956
7903
  }
6957
7904
  var ProjectNameCollisionError = class extends Error {
@@ -7045,7 +7992,7 @@ function pruneTtlMs() {
7045
7992
  }
7046
7993
  async function statPathStatus(p) {
7047
7994
  try {
7048
- const stat = await import_node_fs23.promises.stat(p);
7995
+ const stat = await import_node_fs24.promises.stat(p);
7049
7996
  return stat.isDirectory() ? "present" : "unknown";
7050
7997
  } catch (err) {
7051
7998
  return err.code === "ENOENT" ? "gone" : "unknown";
@@ -7292,11 +8239,11 @@ function registerRoutes(scope, ctx) {
7292
8239
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7293
8240
  const parsed = [];
7294
8241
  for (const c of candidates) {
7295
- const r = import_types26.DivergenceTypeSchema.safeParse(c);
8242
+ const r = import_types29.DivergenceTypeSchema.safeParse(c);
7296
8243
  if (!r.success) {
7297
8244
  return reply.code(400).send({
7298
8245
  error: `unknown divergence type "${c}"`,
7299
- allowed: import_types26.DivergenceTypeSchema.options
8246
+ allowed: import_types29.DivergenceTypeSchema.options
7300
8247
  });
7301
8248
  }
7302
8249
  parsed.push(r.data);
@@ -7515,7 +8462,7 @@ function registerRoutes(scope, ctx) {
7515
8462
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
7516
8463
  let violations = await log.readAll();
7517
8464
  if (req.query.severity) {
7518
- const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
8465
+ const sev = import_types29.PolicySeveritySchema.safeParse(req.query.severity);
7519
8466
  if (!sev.success) {
7520
8467
  return reply.code(400).send({
7521
8468
  error: "invalid severity",
@@ -7554,7 +8501,7 @@ function registerRoutes(scope, ctx) {
7554
8501
  scope.post("/policies/check", async (req, reply) => {
7555
8502
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7556
8503
  if (!proj) return;
7557
- const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
8504
+ const parsed = import_types29.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7558
8505
  if (!parsed.success) {
7559
8506
  return reply.code(400).send({
7560
8507
  error: "invalid /policies/check body",
@@ -7816,16 +8763,95 @@ init_otel_grpc();
7816
8763
 
7817
8764
  // src/daemon.ts
7818
8765
  init_cjs_shims();
7819
- var import_node_fs25 = require("fs");
7820
- var import_node_path42 = __toESM(require("path"), 1);
8766
+ var import_node_fs26 = require("fs");
8767
+ var import_node_path45 = __toESM(require("path"), 1);
7821
8768
  var import_node_module = require("module");
7822
8769
  init_otel();
8770
+
8771
+ // src/connectors/index.ts
8772
+ init_cjs_shims();
8773
+ var NO_ENV = "unknown";
8774
+ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
8775
+ const signals = await connector.poll(ctx);
8776
+ let edgesCreated = 0;
8777
+ let edgesUpdated = 0;
8778
+ let unresolved = 0;
8779
+ for (const signal of signals) {
8780
+ const resolved = resolveTarget(signal, ctx);
8781
+ if (!resolved) {
8782
+ unresolved++;
8783
+ continue;
8784
+ }
8785
+ const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
8786
+ const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : void 0;
8787
+ const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
8788
+ const evidence = callSite ? {
8789
+ file: reconcileObservedRelPath(graph, resolved.serviceName, callSite.relPath),
8790
+ line: callSite.line
8791
+ } : void 0;
8792
+ const calls = Math.trunc(signal.callCount);
8793
+ if (calls < 1) continue;
8794
+ const errors = Math.min(Math.max(Math.trunc(signal.errorCount), 0), calls);
8795
+ let created = false;
8796
+ let ok = true;
8797
+ for (let i = 0; i < calls; i++) {
8798
+ const result = upsertObservedEdge(
8799
+ graph,
8800
+ resolved.edgeType,
8801
+ sourceId,
8802
+ resolved.targetNodeId,
8803
+ signal.lastObservedIso,
8804
+ i < errors,
8805
+ evidence
8806
+ );
8807
+ if (!result) {
8808
+ ok = false;
8809
+ break;
8810
+ }
8811
+ if (i === 0) created = result.created;
8812
+ }
8813
+ if (!ok) {
8814
+ unresolved++;
8815
+ continue;
8816
+ }
8817
+ if (created) edgesCreated++;
8818
+ else edgesUpdated++;
8819
+ }
8820
+ return { signalCount: signals.length, edgesCreated, edgesUpdated, unresolved };
8821
+ }
8822
+ var DEFAULT_POLL_INTERVAL_MS = 6e4;
8823
+ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options = {}) {
8824
+ let stopped = false;
8825
+ let since = ctx.since;
8826
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
8827
+ const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
8828
+ const tick = () => {
8829
+ if (stopped) return;
8830
+ void (async () => {
8831
+ const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
8832
+ try {
8833
+ await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
8834
+ since = tickStartedAt;
8835
+ } catch (err) {
8836
+ onError(err);
8837
+ }
8838
+ })();
8839
+ };
8840
+ const interval = setInterval(tick, intervalMs);
8841
+ if (typeof interval.unref === "function") interval.unref();
8842
+ return () => {
8843
+ stopped = true;
8844
+ clearInterval(interval);
8845
+ };
8846
+ }
8847
+
8848
+ // src/daemon.ts
7823
8849
  init_auth();
7824
8850
 
7825
8851
  // src/unrouted.ts
7826
8852
  init_cjs_shims();
7827
- var import_node_fs24 = require("fs");
7828
- var import_node_path41 = __toESM(require("path"), 1);
8853
+ var import_node_fs25 = require("fs");
8854
+ var import_node_path44 = __toESM(require("path"), 1);
7829
8855
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
7830
8856
  return {
7831
8857
  timestamp: now.toISOString(),
@@ -7835,34 +8861,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
7835
8861
  };
7836
8862
  }
7837
8863
  async function appendUnroutedSpan(neatHome2, record) {
7838
- const target = import_node_path41.default.join(neatHome2, "errors.ndjson");
7839
- await import_node_fs24.promises.mkdir(neatHome2, { recursive: true });
7840
- await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
8864
+ const target = import_node_path44.default.join(neatHome2, "errors.ndjson");
8865
+ await import_node_fs25.promises.mkdir(neatHome2, { recursive: true });
8866
+ await import_node_fs25.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
7841
8867
  }
7842
8868
  function unroutedErrorsPath(neatHome2) {
7843
- return import_node_path41.default.join(neatHome2, "errors.ndjson");
8869
+ return import_node_path44.default.join(neatHome2, "errors.ndjson");
7844
8870
  }
7845
8871
 
7846
8872
  // src/daemon.ts
7847
- var import_types27 = require("@neat.is/types");
8873
+ var import_types30 = require("@neat.is/types");
7848
8874
  function daemonJsonPath(scanPath) {
7849
- return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
8875
+ return import_node_path45.default.join(scanPath, "neat-out", "daemon.json");
7850
8876
  }
7851
8877
  function daemonsDiscoveryDir(home) {
7852
8878
  const base = home && home.length > 0 ? home : neatHomeFromEnv();
7853
- return import_node_path42.default.join(base, "daemons");
8879
+ return import_node_path45.default.join(base, "daemons");
7854
8880
  }
7855
8881
  function daemonDiscoveryPath(project, home) {
7856
- return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
8882
+ return import_node_path45.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
7857
8883
  }
7858
8884
  function sanitizeDiscoveryName(project) {
7859
8885
  return project.replace(/[^A-Za-z0-9._-]/g, "_");
7860
8886
  }
7861
8887
  function neatHomeFromEnv() {
7862
8888
  const env = process.env.NEAT_HOME;
7863
- if (env && env.length > 0) return import_node_path42.default.resolve(env);
8889
+ if (env && env.length > 0) return import_node_path45.default.resolve(env);
7864
8890
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7865
- return import_node_path42.default.join(home, ".neat");
8891
+ return import_node_path45.default.join(home, ".neat");
7866
8892
  }
7867
8893
  function resolveNeatVersion() {
7868
8894
  if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
@@ -7894,7 +8920,7 @@ async function clearDaemonRecord(record, home) {
7894
8920
  } catch {
7895
8921
  }
7896
8922
  try {
7897
- await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
8923
+ await import_node_fs26.promises.unlink(daemonDiscoveryPath(record.project, home));
7898
8924
  } catch {
7899
8925
  }
7900
8926
  }
@@ -7903,17 +8929,25 @@ function teardownSlot(slot) {
7903
8929
  slot.stopPersist();
7904
8930
  } catch {
7905
8931
  }
8932
+ try {
8933
+ slot.stopStaleness();
8934
+ } catch {
8935
+ }
8936
+ try {
8937
+ slot.stopConnectors();
8938
+ } catch {
8939
+ }
7906
8940
  try {
7907
8941
  slot.detachEvents();
7908
8942
  } catch {
7909
8943
  }
7910
8944
  }
7911
8945
  function neatHomeFor(opts) {
7912
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
8946
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path45.default.resolve(opts.neatHome);
7913
8947
  const env = process.env.NEAT_HOME;
7914
- if (env && env.length > 0) return import_node_path42.default.resolve(env);
8948
+ if (env && env.length > 0) return import_node_path45.default.resolve(env);
7915
8949
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7916
- return import_node_path42.default.join(home, ".neat");
8950
+ return import_node_path45.default.join(home, ".neat");
7917
8951
  }
7918
8952
  function routeSpanToProject(serviceName, projects) {
7919
8953
  if (!serviceName) return DEFAULT_PROJECT;
@@ -7957,13 +8991,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
7957
8991
  if (!serviceName) return true;
7958
8992
  if (serviceNameMatchesProject(serviceName, project)) return true;
7959
8993
  return graph.someNode(
7960
- (_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
8994
+ (_id, attrs) => attrs.type === import_types30.NodeType.ServiceNode && attrs.name === serviceName
7961
8995
  );
7962
8996
  }
7963
- async function bootstrapProject(entry) {
7964
- const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
8997
+ async function bootstrapProject(entry, connectors = []) {
8998
+ const paths = pathsForProject(entry.name, import_node_path45.default.join(entry.path, "neat-out"));
7965
8999
  try {
7966
- const stat = await import_node_fs25.promises.stat(entry.path);
9000
+ const stat = await import_node_fs26.promises.stat(entry.path);
7967
9001
  if (!stat.isDirectory()) {
7968
9002
  throw new Error(`registered path ${entry.path} is not a directory`);
7969
9003
  }
@@ -7979,6 +9013,10 @@ async function bootstrapProject(entry) {
7979
9013
  paths,
7980
9014
  stopPersist: () => {
7981
9015
  },
9016
+ stopStaleness: () => {
9017
+ },
9018
+ stopConnectors: () => {
9019
+ },
7982
9020
  detachEvents: () => {
7983
9021
  },
7984
9022
  status: "broken",
@@ -7993,6 +9031,22 @@ async function bootstrapProject(entry) {
7993
9031
  try {
7994
9032
  await extractFromDirectory(graph, entry.path);
7995
9033
  const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
9034
+ const stopStaleness = startStalenessLoop(graph, {
9035
+ staleEventsPath: paths.staleEventsPath,
9036
+ project: entry.name
9037
+ });
9038
+ const stopFns = connectors.map(
9039
+ (registration) => startConnectorPollLoop(
9040
+ registration.connector,
9041
+ { projectDir: entry.path, credentials: registration.credentials },
9042
+ graph,
9043
+ registration.resolveTarget,
9044
+ { intervalMs: registration.intervalMs }
9045
+ )
9046
+ );
9047
+ const stopConnectors = () => {
9048
+ for (const stop of stopFns) stop();
9049
+ };
7996
9050
  await touchLastSeen(entry.name).catch(() => {
7997
9051
  });
7998
9052
  return {
@@ -8001,6 +9055,8 @@ async function bootstrapProject(entry) {
8001
9055
  outPath,
8002
9056
  paths,
8003
9057
  stopPersist,
9058
+ stopStaleness,
9059
+ stopConnectors,
8004
9060
  detachEvents,
8005
9061
  status: "active"
8006
9062
  };
@@ -8057,7 +9113,7 @@ async function startDaemon(opts = {}) {
8057
9113
  const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
8058
9114
  const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
8059
9115
  const singleProject = projectArg;
8060
- const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
9116
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path45.default.resolve(projectPathArg) : null;
8061
9117
  if (singleProject && !singleProjectPath) {
8062
9118
  throw new Error(
8063
9119
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -8065,14 +9121,14 @@ async function startDaemon(opts = {}) {
8065
9121
  }
8066
9122
  if (!singleProject) {
8067
9123
  try {
8068
- await import_node_fs25.promises.access(regPath);
9124
+ await import_node_fs26.promises.access(regPath);
8069
9125
  } catch {
8070
9126
  throw new Error(
8071
9127
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
8072
9128
  );
8073
9129
  }
8074
9130
  }
8075
- const pidPath = import_node_path42.default.join(home, "neatd.pid");
9131
+ const pidPath = import_node_path45.default.join(home, "neatd.pid");
8076
9132
  await writeAtomically(pidPath, `${process.pid}
8077
9133
  `);
8078
9134
  const slots = /* @__PURE__ */ new Map();
@@ -8116,7 +9172,7 @@ async function startDaemon(opts = {}) {
8116
9172
  }
8117
9173
  async function tryRecoverSlot(entry) {
8118
9174
  try {
8119
- const fresh = await bootstrapProject(entry);
9175
+ const fresh = await bootstrapProject(entry, opts.connectors ?? []);
8120
9176
  const prior = slots.get(entry.name);
8121
9177
  if (prior) teardownSlot(prior);
8122
9178
  slots.set(entry.name, fresh);
@@ -8140,7 +9196,7 @@ async function startDaemon(opts = {}) {
8140
9196
  bootstrapStatus.set(entry.name, "bootstrapping");
8141
9197
  bootstrapStartedAt.set(entry.name, Date.now());
8142
9198
  try {
8143
- const slot = await bootstrapProject(entry);
9199
+ const slot = await bootstrapProject(entry, opts.connectors ?? []);
8144
9200
  const prior = slots.get(entry.name);
8145
9201
  if (prior) teardownSlot(prior);
8146
9202
  slots.set(entry.name, slot);
@@ -8266,7 +9322,7 @@ async function startDaemon(opts = {}) {
8266
9322
  }
8267
9323
  if (restApp) await restApp.close().catch(() => {
8268
9324
  });
8269
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9325
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8270
9326
  });
8271
9327
  throw new Error(
8272
9328
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -8392,7 +9448,7 @@ async function startDaemon(opts = {}) {
8392
9448
  });
8393
9449
  if (otlpApp) await otlpApp.close().catch(() => {
8394
9450
  });
8395
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9451
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8396
9452
  });
8397
9453
  throw new Error(
8398
9454
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -8427,7 +9483,7 @@ async function startDaemon(opts = {}) {
8427
9483
  });
8428
9484
  if (otlpApp) await otlpApp.close().catch(() => {
8429
9485
  });
8430
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9486
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8431
9487
  });
8432
9488
  throw new Error(
8433
9489
  `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
@@ -8474,9 +9530,9 @@ async function startDaemon(opts = {}) {
8474
9530
  let registryWatcher = null;
8475
9531
  let reloadTimer = null;
8476
9532
  if (!singleProject) try {
8477
- const regDir = import_node_path42.default.dirname(regPath);
8478
- const regBase = import_node_path42.default.basename(regPath);
8479
- registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
9533
+ const regDir = import_node_path45.default.dirname(regPath);
9534
+ const regBase = import_node_path45.default.basename(regPath);
9535
+ registryWatcher = (0, import_node_fs26.watch)(regDir, (_eventType, filename) => {
8480
9536
  if (filename !== null && filename !== regBase) return;
8481
9537
  if (reloadTimer) clearTimeout(reloadTimer);
8482
9538
  reloadTimer = setTimeout(() => {
@@ -8525,7 +9581,7 @@ async function startDaemon(opts = {}) {
8525
9581
  if (daemonRecord) {
8526
9582
  await clearDaemonRecord(daemonRecord, home);
8527
9583
  }
8528
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9584
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8529
9585
  });
8530
9586
  };
8531
9587
  return {