@neat.is/core 0.4.25 → 0.4.26-dev.20260703

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 path45 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path43 === suffix || path43.endsWith(suffix)) {
62
+ if (path45 === suffix || path45.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_path41.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
193
+ return import_node_path41.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_path41, 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_path41 = __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,13 @@ 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
+ }
338
347
  function parseOtlpRequest(body) {
339
348
  const out = [];
340
349
  for (const rs of body.resourceSpans ?? []) {
@@ -361,6 +370,10 @@ function parseOtlpRequest(body) {
361
370
  attributes: attrs,
362
371
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
363
372
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
373
+ messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
374
+ messagingDestination: messagingDestinationOf(attrs),
375
+ graphqlOperationName: typeof attrs["graphql.operation.name"] === "string" && attrs["graphql.operation.name"].length > 0 ? attrs["graphql.operation.name"] : void 0,
376
+ graphqlOperationType: typeof attrs["graphql.operation.type"] === "string" && attrs["graphql.operation.type"].length > 0 ? attrs["graphql.operation.type"] : void 0,
364
377
  statusCode: span.status?.code,
365
378
  errorMessage: span.status?.message,
366
379
  exception: extractExceptionFromEvents(span.events)
@@ -372,10 +385,10 @@ function parseOtlpRequest(body) {
372
385
  return out;
373
386
  }
374
387
  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");
388
+ const here = import_node_path42.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
389
+ const protoRoot = import_node_path42.default.resolve(here, "..", "proto");
377
390
  const root = new import_protobufjs.default.Root();
378
- root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
391
+ root.resolvePath = (_origin, target) => import_node_path42.default.resolve(protoRoot, target);
379
392
  root.loadSync(
380
393
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
381
394
  { keepCase: true }
@@ -420,7 +433,21 @@ async function buildOtelReceiver(opts) {
420
433
  logger: false,
421
434
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
422
435
  });
423
- mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
436
+ const REJECT_WARN_INTERVAL_MS = 6e4;
437
+ let lastRejectWarnAt = 0;
438
+ const warnRejectedOtlp = () => {
439
+ const now = Date.now();
440
+ if (now - lastRejectWarnAt < REJECT_WARN_INTERVAL_MS) return;
441
+ lastRejectWarnAt = now;
442
+ console.warn(
443
+ "[neatd] rejecting OTLP spans on /v1/traces \u2014 missing or invalid bearer token (set NEAT_OTEL_TOKEN on the instrumented app)"
444
+ );
445
+ };
446
+ mountBearerAuth(app, {
447
+ token: opts.authToken,
448
+ trustProxy: opts.trustProxy,
449
+ onReject: warnRejectedOtlp
450
+ });
424
451
  const queue = [];
425
452
  let draining = false;
426
453
  let drainPromise = Promise.resolve();
@@ -597,12 +624,12 @@ function logSpanHandler(span) {
597
624
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
598
625
  );
599
626
  }
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;
627
+ var import_node_path42, 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
628
  var init_otel = __esm({
602
629
  "src/otel.ts"() {
603
630
  "use strict";
604
631
  init_cjs_shims();
605
- import_node_path40 = __toESM(require("path"), 1);
632
+ import_node_path42 = __toESM(require("path"), 1);
606
633
  import_node_url2 = require("url");
607
634
  import_fastify2 = __toESM(require("fastify"), 1);
608
635
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -1192,19 +1219,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1192
1219
  function longestIncomingWalk(graph, start, maxDepth) {
1193
1220
  let best = { path: [start], edges: [] };
1194
1221
  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] };
1222
+ function step(node, path45, edges) {
1223
+ if (path45.length > best.path.length) {
1224
+ best = { path: [...path45], edges: [...edges] };
1198
1225
  }
1199
- if (path43.length - 1 >= maxDepth) return;
1226
+ if (path45.length - 1 >= maxDepth) return;
1200
1227
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1201
1228
  for (const [srcId, edge] of incoming) {
1202
1229
  if (visited.has(srcId)) continue;
1203
1230
  visited.add(srcId);
1204
- path43.push(srcId);
1231
+ path45.push(srcId);
1205
1232
  edges.push(edge);
1206
- step(srcId, path43, edges);
1207
- path43.pop();
1233
+ step(srcId, path45, edges);
1234
+ path45.pop();
1208
1235
  edges.pop();
1209
1236
  visited.delete(srcId);
1210
1237
  }
@@ -1212,14 +1239,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
1212
1239
  step(start, [start], []);
1213
1240
  return best;
1214
1241
  }
1215
- function databaseRootCauseShape(graph, origin, walk) {
1242
+ function databaseRootCauseShape(graph, origin, walk3) {
1216
1243
  const targetDb = origin;
1217
1244
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1218
1245
  if (candidatePairs.length === 0) return null;
1219
- for (const id of walk.path) {
1246
+ for (const id of walk3.path) {
1220
1247
  const owner = resolveOwningService(graph, id);
1221
1248
  if (!owner) continue;
1222
- const { id: serviceId3, svc } = owner;
1249
+ const { id: serviceId4, svc } = owner;
1223
1250
  const deps = svc.dependencies ?? {};
1224
1251
  for (const pair of candidatePairs) {
1225
1252
  const declared = deps[pair.driver];
@@ -1232,7 +1259,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1232
1259
  );
1233
1260
  if (!result.compatible) {
1234
1261
  return {
1235
- rootCauseNode: serviceId3,
1262
+ rootCauseNode: serviceId4,
1236
1263
  rootCauseReason: result.reason ?? "incompatible driver",
1237
1264
  ...result.minDriverVersion ? {
1238
1265
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1243,11 +1270,11 @@ function databaseRootCauseShape(graph, origin, walk) {
1243
1270
  }
1244
1271
  return null;
1245
1272
  }
1246
- function serviceRootCauseShape(graph, _origin, walk) {
1247
- for (const id of walk.path) {
1273
+ function serviceRootCauseShape(graph, _origin, walk3) {
1274
+ for (const id of walk3.path) {
1248
1275
  const owner = resolveOwningService(graph, id);
1249
1276
  if (!owner) continue;
1250
- const { id: serviceId3, svc } = owner;
1277
+ const { id: serviceId4, svc } = owner;
1251
1278
  const deps = svc.dependencies ?? {};
1252
1279
  const serviceNodeEngine = svc.nodeEngine;
1253
1280
  for (const constraint of nodeEngineConstraints()) {
@@ -1256,7 +1283,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1256
1283
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1257
1284
  if (!result.compatible && result.reason) {
1258
1285
  return {
1259
- rootCauseNode: serviceId3,
1286
+ rootCauseNode: serviceId4,
1260
1287
  rootCauseReason: result.reason,
1261
1288
  ...result.requiredNodeVersion ? {
1262
1289
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1271,7 +1298,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1271
1298
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1272
1299
  if (!result.compatible && result.reason) {
1273
1300
  return {
1274
- rootCauseNode: serviceId3,
1301
+ rootCauseNode: serviceId4,
1275
1302
  rootCauseReason: result.reason,
1276
1303
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1277
1304
  };
@@ -1280,10 +1307,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
1280
1307
  }
1281
1308
  return null;
1282
1309
  }
1283
- function fileRootCauseShape(graph, origin, walk) {
1310
+ function fileRootCauseShape(graph, origin, walk3) {
1284
1311
  const owner = resolveOwningService(graph, origin.id);
1285
1312
  if (!owner) return null;
1286
- return serviceRootCauseShape(graph, owner.svc, walk);
1313
+ return serviceRootCauseShape(graph, owner.svc, walk3);
1287
1314
  }
1288
1315
  var rootCauseShapes = {
1289
1316
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
@@ -1295,16 +1322,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1295
1322
  const origin = graph.getNodeAttributes(errorNodeId);
1296
1323
  const shape = rootCauseShapes[origin.type];
1297
1324
  if (shape) {
1298
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1299
- const match = shape(graph, origin, walk);
1325
+ const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1326
+ const match = shape(graph, origin, walk3);
1300
1327
  if (match) {
1301
1328
  const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1302
1329
  return import_types.RootCauseResultSchema.parse({
1303
1330
  rootCauseNode: match.rootCauseNode,
1304
1331
  rootCauseReason: reason,
1305
- traversalPath: walk.path,
1306
- edgeProvenances: walk.edges.map((e) => e.provenance),
1307
- confidence: confidenceFromMix(walk.edges),
1332
+ traversalPath: walk3.path,
1333
+ edgeProvenances: walk3.edges.map((e) => e.provenance),
1334
+ confidence: confidenceFromMix(walk3.edges),
1308
1335
  fixRecommendation: match.fixRecommendation
1309
1336
  });
1310
1337
  }
@@ -1362,9 +1389,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1362
1389
  function isFailingCallEdge(e) {
1363
1390
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1364
1391
  }
1365
- function callSourcesForService(graph, serviceId3) {
1366
- const ids = [serviceId3];
1367
- for (const edgeId of graph.outboundEdges(serviceId3)) {
1392
+ function callSourcesForService(graph, serviceId4) {
1393
+ const ids = [serviceId4];
1394
+ for (const edgeId of graph.outboundEdges(serviceId4)) {
1368
1395
  const e = graph.getEdgeAttributes(edgeId);
1369
1396
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1370
1397
  const tgt = graph.getNodeAttributes(e.target);
@@ -1381,9 +1408,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1381
1408
  }
1382
1409
  return id < curId;
1383
1410
  }
1384
- function dominantFailingCall(graph, serviceId3, visited) {
1411
+ function dominantFailingCall(graph, serviceId4, visited) {
1385
1412
  let best = null;
1386
- for (const src of callSourcesForService(graph, serviceId3)) {
1413
+ for (const src of callSourcesForService(graph, serviceId4)) {
1387
1414
  for (const edgeId of graph.outboundEdges(src)) {
1388
1415
  const e = graph.getEdgeAttributes(edgeId);
1389
1416
  if (!isFailingCallEdge(e)) continue;
@@ -1398,26 +1425,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
1398
1425
  return best;
1399
1426
  }
1400
1427
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1401
- const path43 = [originServiceId];
1428
+ const path45 = [originServiceId];
1402
1429
  const edges = [];
1403
1430
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1404
1431
  let current = originServiceId;
1405
1432
  for (let depth = 0; depth < maxDepth; depth++) {
1406
1433
  const hop = dominantFailingCall(graph, current, visited);
1407
1434
  if (!hop) break;
1408
- path43.push(hop.nextService);
1435
+ path45.push(hop.nextService);
1409
1436
  edges.push(hop.edge);
1410
1437
  visited.add(hop.nextService);
1411
1438
  current = hop.nextService;
1412
1439
  }
1413
1440
  if (edges.length === 0) return null;
1414
- return { path: path43, edges, culprit: current };
1441
+ return { path: path45, edges, culprit: current };
1415
1442
  }
1416
1443
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1417
1444
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1418
1445
  if (!chain) return null;
1419
1446
  const culprit = chain.culprit;
1420
- const path43 = [...chain.path];
1447
+ const path45 = [...chain.path];
1421
1448
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1422
1449
  const baseConfidence = confidenceFromMix(chain.edges);
1423
1450
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1425,14 +1452,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1425
1452
  if (loc) {
1426
1453
  let rootCauseNode = culprit;
1427
1454
  if (loc.fileNode) {
1428
- path43.push(loc.fileNode);
1455
+ path45.push(loc.fileNode);
1429
1456
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1430
1457
  rootCauseNode = loc.fileNode;
1431
1458
  }
1432
1459
  return import_types.RootCauseResultSchema.parse({
1433
1460
  rootCauseNode,
1434
1461
  rootCauseReason: loc.rootCauseReason,
1435
- traversalPath: path43,
1462
+ traversalPath: path45,
1436
1463
  edgeProvenances,
1437
1464
  confidence,
1438
1465
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1444,7 +1471,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1444
1471
  return import_types.RootCauseResultSchema.parse({
1445
1472
  rootCauseNode: culprit,
1446
1473
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1447
- traversalPath: path43,
1474
+ traversalPath: path45,
1448
1475
  edgeProvenances,
1449
1476
  confidence,
1450
1477
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2368,10 +2395,48 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2368
2395
  ]);
2369
2396
  var WIRE_SPAN_KIND_CLIENT = 3;
2370
2397
  var WIRE_SPAN_KIND_PRODUCER = 4;
2398
+ var WIRE_SPAN_KIND_CONSUMER = 5;
2371
2399
  function spanMintsObservedEdge(kind) {
2372
2400
  if (kind === void 0 || kind === 0) return true;
2373
2401
  return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
2374
2402
  }
2403
+ function spanMintsMessagingEdge(kind) {
2404
+ return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
2405
+ }
2406
+ function spanServesGraphqlOperation(kind) {
2407
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2408
+ }
2409
+ function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
2410
+ const id = (0, import_types3.graphqlOperationId)(serviceName, operationType, operationName);
2411
+ if (graph.hasNode(id)) return id;
2412
+ const node = {
2413
+ id,
2414
+ type: import_types3.NodeType.GraphQLOperationNode,
2415
+ name: operationName,
2416
+ service: serviceName,
2417
+ operationType: operationType.toLowerCase(),
2418
+ operationName,
2419
+ discoveredVia: "otel"
2420
+ };
2421
+ graph.addNode(id, node);
2422
+ return id;
2423
+ }
2424
+ function messagingDestinationKind(system) {
2425
+ return `${system}-topic`;
2426
+ }
2427
+ function ensureMessagingDestinationNode(graph, system, destination) {
2428
+ const id = (0, import_types3.infraId)(messagingDestinationKind(system), destination);
2429
+ if (graph.hasNode(id)) return id;
2430
+ const node = {
2431
+ id,
2432
+ type: import_types3.NodeType.InfraNode,
2433
+ name: destination,
2434
+ provider: "self",
2435
+ kind: messagingDestinationKind(system)
2436
+ };
2437
+ graph.addNode(id, node);
2438
+ return id;
2439
+ }
2375
2440
  var PARENT_SPAN_CACHE_SIZE = 1e4;
2376
2441
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
2377
2442
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -2465,6 +2530,21 @@ function ensureDatabaseNode(graph, host, engine) {
2465
2530
  graph.addNode(id, node);
2466
2531
  return id;
2467
2532
  }
2533
+ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
2534
+ const id = (0, import_types3.localDatabaseId)(serviceName, name);
2535
+ if (graph.hasNode(id)) return id;
2536
+ const node = {
2537
+ id,
2538
+ type: import_types3.NodeType.DatabaseNode,
2539
+ name,
2540
+ engine,
2541
+ engineVersion: "unknown",
2542
+ compatibleDrivers: [],
2543
+ discoveredVia: "otel"
2544
+ };
2545
+ graph.addNode(id, node);
2546
+ return id;
2547
+ }
2468
2548
  function ensureFrontierNode(graph, host, ts) {
2469
2549
  const id = frontierIdFor(host);
2470
2550
  if (graph.hasNode(id)) {
@@ -2716,10 +2796,21 @@ async function handleSpan(ctx, span) {
2716
2796
  let affectedNode = sourceId;
2717
2797
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2718
2798
  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);
2799
+ if (mintsFromCallerSide) {
2800
+ const host = pickAddress(span);
2801
+ let targetId;
2802
+ if (host) {
2803
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2804
+ targetId = (0, import_types3.databaseId)(host);
2805
+ } else {
2806
+ const localName = span.dbName ?? span.dbSystem;
2807
+ targetId = ensureLocalDatabaseNode(
2808
+ ctx.graph,
2809
+ span.service,
2810
+ localName,
2811
+ span.dbSystem
2812
+ );
2813
+ }
2723
2814
  const result = upsertObservedEdge(
2724
2815
  ctx.graph,
2725
2816
  import_types3.EdgeType.CONNECTS_TO,
@@ -2731,6 +2822,40 @@ async function handleSpan(ctx, span) {
2731
2822
  );
2732
2823
  if (result) affectedNode = targetId;
2733
2824
  }
2825
+ } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
2826
+ const targetId = ensureMessagingDestinationNode(
2827
+ ctx.graph,
2828
+ span.messagingSystem,
2829
+ span.messagingDestination
2830
+ );
2831
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types3.EdgeType.CONSUMES_FROM : import_types3.EdgeType.PUBLISHES_TO;
2832
+ const result = upsertObservedEdge(
2833
+ ctx.graph,
2834
+ edgeType,
2835
+ observedSource(),
2836
+ targetId,
2837
+ ts,
2838
+ isError,
2839
+ callSiteEvidence
2840
+ );
2841
+ if (result) affectedNode = targetId;
2842
+ } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
2843
+ const targetId = ensureGraphqlOperationNode(
2844
+ ctx.graph,
2845
+ span.service,
2846
+ span.graphqlOperationType,
2847
+ span.graphqlOperationName
2848
+ );
2849
+ const result = upsertObservedEdge(
2850
+ ctx.graph,
2851
+ import_types3.EdgeType.CONTAINS,
2852
+ observedSource(),
2853
+ targetId,
2854
+ ts,
2855
+ isError,
2856
+ callSiteEvidence
2857
+ );
2858
+ if (result) affectedNode = targetId;
2734
2859
  } else {
2735
2860
  const host = pickAddress(span);
2736
2861
  let resolvedViaAddress = false;
@@ -2840,29 +2965,29 @@ function promoteFrontierNodes(graph, opts = {}) {
2840
2965
  toPromote.push({ frontierId: id, serviceId: target });
2841
2966
  });
2842
2967
  let promoted = 0;
2843
- for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
2968
+ for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
2844
2969
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
2845
2970
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
2846
2971
  if (!gate.allowed) {
2847
2972
  continue;
2848
2973
  }
2849
2974
  }
2850
- rewireFrontierEdges(graph, frontierId2, serviceId3);
2975
+ rewireFrontierEdges(graph, frontierId2, serviceId4);
2851
2976
  graph.dropNode(frontierId2);
2852
2977
  promoted++;
2853
2978
  }
2854
2979
  return promoted;
2855
2980
  }
2856
- function rewireFrontierEdges(graph, frontierId2, serviceId3) {
2981
+ function rewireFrontierEdges(graph, frontierId2, serviceId4) {
2857
2982
  const inbound = [...graph.inboundEdges(frontierId2)];
2858
2983
  const outbound = [...graph.outboundEdges(frontierId2)];
2859
2984
  for (const edgeId of inbound) {
2860
2985
  const edge = graph.getEdgeAttributes(edgeId);
2861
- rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
2986
+ rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
2862
2987
  }
2863
2988
  for (const edgeId of outbound) {
2864
2989
  const edge = graph.getEdgeAttributes(edgeId);
2865
- rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
2990
+ rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
2866
2991
  }
2867
2992
  }
2868
2993
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -3624,9 +3749,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
3624
3749
  "StatefulSet",
3625
3750
  "DaemonSet"
3626
3751
  ]);
3627
- function addAliases(graph, serviceId3, candidates) {
3628
- if (!graph.hasNode(serviceId3)) return;
3629
- const node = graph.getNodeAttributes(serviceId3);
3752
+ function addAliases(graph, serviceId4, candidates) {
3753
+ if (!graph.hasNode(serviceId4)) return;
3754
+ const node = graph.getNodeAttributes(serviceId4);
3630
3755
  if (node.type !== import_types6.NodeType.ServiceNode) return;
3631
3756
  const set = new Set(node.aliases ?? []);
3632
3757
  for (const c of candidates) {
@@ -3636,7 +3761,7 @@ function addAliases(graph, serviceId3, candidates) {
3636
3761
  }
3637
3762
  if (set.size === 0) return;
3638
3763
  const updated = { ...node, aliases: [...set].sort() };
3639
- graph.replaceNodeAttributes(serviceId3, updated);
3764
+ graph.replaceNodeAttributes(serviceId4, updated);
3640
3765
  }
3641
3766
  function indexServicesByName(services) {
3642
3767
  const map = /* @__PURE__ */ new Map();
@@ -3669,12 +3794,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3669
3794
  }
3670
3795
  if (!compose?.services) return;
3671
3796
  for (const [composeName, svc] of Object.entries(compose.services)) {
3672
- const serviceId3 = serviceIndex.get(composeName);
3673
- if (!serviceId3) continue;
3797
+ const serviceId4 = serviceIndex.get(composeName);
3798
+ if (!serviceId4) continue;
3674
3799
  const aliases = /* @__PURE__ */ new Set([composeName]);
3675
3800
  if (svc.container_name) aliases.add(svc.container_name);
3676
3801
  if (svc.hostname) aliases.add(svc.hostname);
3677
- addAliases(graph, serviceId3, aliases);
3802
+ addAliases(graph, serviceId4, aliases);
3678
3803
  }
3679
3804
  }
3680
3805
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -3787,16 +3912,28 @@ init_cjs_shims();
3787
3912
  var import_node_fs10 = require("fs");
3788
3913
  var import_node_path10 = __toESM(require("path"), 1);
3789
3914
  var import_types7 = require("@neat.is/types");
3915
+ function buildServiceHostIndex(services) {
3916
+ const knownHosts = /* @__PURE__ */ new Set();
3917
+ const hostToNodeId = /* @__PURE__ */ new Map();
3918
+ for (const service of services) {
3919
+ const base = import_node_path10.default.basename(service.dir);
3920
+ knownHosts.add(base);
3921
+ knownHosts.add(service.pkg.name);
3922
+ hostToNodeId.set(base, service.node.id);
3923
+ hostToNodeId.set(service.pkg.name, service.node.id);
3924
+ }
3925
+ return { knownHosts, hostToNodeId };
3926
+ }
3790
3927
  async function walkSourceFiles(dir) {
3791
3928
  const out = [];
3792
- async function walk(current) {
3929
+ async function walk3(current) {
3793
3930
  const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3794
3931
  for (const entry of entries) {
3795
3932
  const full = import_node_path10.default.join(current, entry.name);
3796
3933
  if (entry.isDirectory()) {
3797
3934
  if (IGNORED_DIRS.has(entry.name)) continue;
3798
3935
  if (await isPythonVenvDir(full)) continue;
3799
- await walk(full);
3936
+ await walk3(full);
3800
3937
  } 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
3938
  // would attribute our instrumentation imports to the user's service.
3802
3939
  !isNeatAuthoredSourceFile(entry.name)) {
@@ -3804,7 +3941,7 @@ async function walkSourceFiles(dir) {
3804
3941
  }
3805
3942
  }
3806
3943
  }
3807
- await walk(dir);
3944
+ await walk3(dir);
3808
3945
  return out;
3809
3946
  }
3810
3947
  async function loadSourceFiles(dir) {
@@ -4898,20 +5035,20 @@ var import_node_path21 = __toESM(require("path"), 1);
4898
5035
  var import_types10 = require("@neat.is/types");
4899
5036
  async function walkConfigFiles(dir) {
4900
5037
  const out = [];
4901
- async function walk(current) {
5038
+ async function walk3(current) {
4902
5039
  const entries = await import_node_fs14.promises.readdir(current, { withFileTypes: true });
4903
5040
  for (const entry of entries) {
4904
5041
  const full = import_node_path21.default.join(current, entry.name);
4905
5042
  if (entry.isDirectory()) {
4906
5043
  if (IGNORED_DIRS.has(entry.name)) continue;
4907
5044
  if (await isPythonVenvDir(full)) continue;
4908
- await walk(full);
5045
+ await walk3(full);
4909
5046
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
4910
5047
  out.push(full);
4911
5048
  }
4912
5049
  }
4913
5050
  }
4914
- await walk(dir);
5051
+ await walk3(dir);
4915
5052
  return out;
4916
5053
  }
4917
5054
  async function addConfigNodes(graph, services, scanPath) {
@@ -4959,17 +5096,347 @@ async function addConfigNodes(graph, services, scanPath) {
4959
5096
  return { nodesAdded, edgesAdded };
4960
5097
  }
4961
5098
 
5099
+ // src/extract/routes.ts
5100
+ init_cjs_shims();
5101
+ var import_node_path22 = __toESM(require("path"), 1);
5102
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5103
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5104
+ var import_types11 = require("@neat.is/types");
5105
+ var PARSE_CHUNK2 = 16384;
5106
+ function parseSource2(parser, source) {
5107
+ return parser.parse(
5108
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5109
+ );
5110
+ }
5111
+ function makeJsParser2() {
5112
+ const p = new import_tree_sitter2.default();
5113
+ p.setLanguage(import_tree_sitter_javascript2.default);
5114
+ return p;
5115
+ }
5116
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5117
+ "get",
5118
+ "post",
5119
+ "put",
5120
+ "patch",
5121
+ "delete",
5122
+ "options",
5123
+ "head",
5124
+ "all"
5125
+ ]);
5126
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5127
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5128
+ function canonicalizeTemplate(raw) {
5129
+ let p = raw.split("?")[0].split("#")[0];
5130
+ if (!p.startsWith("/")) p = "/" + p;
5131
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
5132
+ return p;
5133
+ }
5134
+ function isDynamicSegment(seg) {
5135
+ if (seg.length === 0) return false;
5136
+ if (seg.includes(":")) return true;
5137
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
5138
+ if (/^\d+$/.test(seg)) return true;
5139
+ 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;
5140
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
5141
+ return false;
5142
+ }
5143
+ function normalizePathTemplate(raw) {
5144
+ const canonical = canonicalizeTemplate(raw);
5145
+ const segments = canonical.split("/").filter((s) => s.length > 0);
5146
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
5147
+ return "/" + normalised.join("/");
5148
+ }
5149
+ function walk(node, visit) {
5150
+ visit(node);
5151
+ for (let i = 0; i < node.namedChildCount; i++) {
5152
+ const child = node.namedChild(i);
5153
+ if (child) walk(child, visit);
5154
+ }
5155
+ }
5156
+ function staticStringText(node) {
5157
+ if (node.type === "string") {
5158
+ for (let i = 0; i < node.namedChildCount; i++) {
5159
+ const child = node.namedChild(i);
5160
+ if (child?.type === "string_fragment") return child.text;
5161
+ }
5162
+ return "";
5163
+ }
5164
+ if (node.type === "template_string") {
5165
+ for (let i = 0; i < node.namedChildCount; i++) {
5166
+ if (node.namedChild(i)?.type === "template_substitution") return null;
5167
+ }
5168
+ const raw = node.text;
5169
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5170
+ }
5171
+ return null;
5172
+ }
5173
+ function objectStringProp(objNode, key) {
5174
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5175
+ const pair = objNode.namedChild(i);
5176
+ if (!pair || pair.type !== "pair") continue;
5177
+ const k = pair.childForFieldName("key");
5178
+ if (!k) continue;
5179
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
5180
+ if (kText !== key) continue;
5181
+ const v = pair.childForFieldName("value");
5182
+ if (v) return staticStringText(v);
5183
+ }
5184
+ return null;
5185
+ }
5186
+ function fastifyRouteMethods(objNode) {
5187
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5188
+ const pair = objNode.namedChild(i);
5189
+ if (!pair || pair.type !== "pair") continue;
5190
+ const k = pair.childForFieldName("key");
5191
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
5192
+ if (kText !== "method") continue;
5193
+ const v = pair.childForFieldName("value");
5194
+ if (!v) return [];
5195
+ if (v.type === "string" || v.type === "template_string") {
5196
+ const s = staticStringText(v);
5197
+ return s ? [s.toUpperCase()] : [];
5198
+ }
5199
+ if (v.type === "array") {
5200
+ const out = [];
5201
+ for (let j = 0; j < v.namedChildCount; j++) {
5202
+ const el = v.namedChild(j);
5203
+ if (el && (el.type === "string" || el.type === "template_string")) {
5204
+ const s = staticStringText(el);
5205
+ if (s) out.push(s.toUpperCase());
5206
+ }
5207
+ }
5208
+ return out;
5209
+ }
5210
+ }
5211
+ return [];
5212
+ }
5213
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
5214
+ const tree = parseSource2(parser, source);
5215
+ const out = [];
5216
+ const framework = hasExpress ? "express" : "fastify";
5217
+ walk(tree.rootNode, (node) => {
5218
+ if (node.type !== "call_expression") return;
5219
+ const fn = node.childForFieldName("function");
5220
+ if (!fn || fn.type !== "member_expression") return;
5221
+ const prop = fn.childForFieldName("property");
5222
+ if (!prop) return;
5223
+ const method = prop.text.toLowerCase();
5224
+ const args = node.childForFieldName("arguments");
5225
+ const first = args?.namedChild(0);
5226
+ if (!first) return;
5227
+ const line = node.startPosition.row + 1;
5228
+ if (ROUTER_METHODS.has(method)) {
5229
+ const p = staticStringText(first);
5230
+ if (p && p.startsWith("/")) {
5231
+ out.push({
5232
+ method: method === "all" ? "ALL" : method.toUpperCase(),
5233
+ pathTemplate: canonicalizeTemplate(p),
5234
+ line,
5235
+ framework
5236
+ });
5237
+ }
5238
+ return;
5239
+ }
5240
+ if (method === "route" && hasFastify && first.type === "object") {
5241
+ const url = objectStringProp(first, "url");
5242
+ if (!url || !url.startsWith("/")) return;
5243
+ const methods = fastifyRouteMethods(first);
5244
+ const list = methods.length > 0 ? methods : ["ALL"];
5245
+ for (const m of list) {
5246
+ out.push({
5247
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5248
+ pathTemplate: canonicalizeTemplate(url),
5249
+ line,
5250
+ framework: "fastify"
5251
+ });
5252
+ }
5253
+ }
5254
+ });
5255
+ return out;
5256
+ }
5257
+ function segmentsOf(relFile) {
5258
+ return toPosix2(relFile).split("/").filter((s) => s.length > 0);
5259
+ }
5260
+ function isNextAppRouteFile(relFile) {
5261
+ const segs = segmentsOf(relFile);
5262
+ if (!segs.includes("app")) return false;
5263
+ const base = segs[segs.length - 1] ?? "";
5264
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
5265
+ }
5266
+ function isNextPagesApiFile(relFile) {
5267
+ const segs = segmentsOf(relFile);
5268
+ const pagesIdx = segs.indexOf("pages");
5269
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
5270
+ const base = segs[segs.length - 1] ?? "";
5271
+ if (/^_(app|document|middleware)\./.test(base)) return false;
5272
+ return JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(base));
5273
+ }
5274
+ function nextSegment(seg) {
5275
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
5276
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
5277
+ if (catchAll) return ":" + catchAll[1];
5278
+ const dynamic = seg.match(/^\[(.+?)\]$/);
5279
+ if (dynamic) return ":" + dynamic[1];
5280
+ return seg;
5281
+ }
5282
+ function nextAppPathTemplate(relFile) {
5283
+ const segs = segmentsOf(relFile);
5284
+ const appIdx = segs.lastIndexOf("app");
5285
+ const between = segs.slice(appIdx + 1, segs.length - 1);
5286
+ const parts = [];
5287
+ for (const seg of between) {
5288
+ const mapped = nextSegment(seg);
5289
+ if (mapped !== null) parts.push(mapped);
5290
+ }
5291
+ return "/" + parts.join("/");
5292
+ }
5293
+ function nextPagesApiPathTemplate(relFile) {
5294
+ const segs = segmentsOf(relFile);
5295
+ const pagesIdx = segs.indexOf("pages");
5296
+ const rest = segs.slice(pagesIdx + 1);
5297
+ const parts = [];
5298
+ for (let i = 0; i < rest.length; i++) {
5299
+ let seg = rest[i];
5300
+ if (i === rest.length - 1) {
5301
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
5302
+ if (seg === "index") continue;
5303
+ }
5304
+ const mapped = nextSegment(seg);
5305
+ if (mapped !== null) parts.push(mapped);
5306
+ }
5307
+ return "/" + parts.join("/");
5308
+ }
5309
+ function nextAppMethods(root) {
5310
+ const out = [];
5311
+ walk(root, (node) => {
5312
+ if (node.type !== "export_statement") return;
5313
+ const decl = node.childForFieldName("declaration");
5314
+ if (!decl) return;
5315
+ const line = node.startPosition.row + 1;
5316
+ if (decl.type === "function_declaration") {
5317
+ const name = decl.childForFieldName("name")?.text;
5318
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5319
+ return;
5320
+ }
5321
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
5322
+ for (let i = 0; i < decl.namedChildCount; i++) {
5323
+ const d = decl.namedChild(i);
5324
+ if (d?.type !== "variable_declarator") continue;
5325
+ const name = d.childForFieldName("name")?.text;
5326
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5327
+ }
5328
+ }
5329
+ });
5330
+ return out;
5331
+ }
5332
+ function nextRoutesFromFile(source, relFile, parser) {
5333
+ if (isNextAppRouteFile(relFile)) {
5334
+ const tree = parseSource2(parser, source);
5335
+ const template = nextAppPathTemplate(relFile);
5336
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
5337
+ method,
5338
+ pathTemplate: canonicalizeTemplate(template),
5339
+ line,
5340
+ framework: "next"
5341
+ }));
5342
+ }
5343
+ if (isNextPagesApiFile(relFile)) {
5344
+ return [
5345
+ {
5346
+ method: "ALL",
5347
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
5348
+ line: 1,
5349
+ framework: "next"
5350
+ }
5351
+ ];
5352
+ }
5353
+ return [];
5354
+ }
5355
+ async function addRoutes(graph, services) {
5356
+ const jsParser = makeJsParser2();
5357
+ let nodesAdded = 0;
5358
+ let edgesAdded = 0;
5359
+ for (const service of services) {
5360
+ const deps = {
5361
+ ...service.pkg.dependencies ?? {},
5362
+ ...service.pkg.devDependencies ?? {}
5363
+ };
5364
+ const hasExpress = deps["express"] !== void 0;
5365
+ const hasFastify = deps["fastify"] !== void 0;
5366
+ const hasNext = deps["next"] !== void 0;
5367
+ if (!hasExpress && !hasFastify && !hasNext) continue;
5368
+ const files = await loadSourceFiles(service.dir);
5369
+ for (const file of files) {
5370
+ if (isTestPath(file.path)) continue;
5371
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(file.path))) continue;
5372
+ const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
5373
+ let routes;
5374
+ try {
5375
+ if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5376
+ routes = nextRoutesFromFile(file.content, relFile, jsParser);
5377
+ } else if (hasExpress || hasFastify) {
5378
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
5379
+ } else {
5380
+ routes = [];
5381
+ }
5382
+ } catch (err) {
5383
+ recordExtractionError("route extraction", file.path, err);
5384
+ continue;
5385
+ }
5386
+ if (routes.length === 0) continue;
5387
+ for (const route of routes) {
5388
+ const rid = (0, import_types11.routeId)(service.pkg.name, route.method, route.pathTemplate);
5389
+ if (!graph.hasNode(rid)) {
5390
+ const node = {
5391
+ id: rid,
5392
+ type: import_types11.NodeType.RouteNode,
5393
+ name: `${route.method} ${route.pathTemplate}`,
5394
+ service: service.pkg.name,
5395
+ method: route.method,
5396
+ pathTemplate: route.pathTemplate,
5397
+ path: relFile,
5398
+ line: route.line,
5399
+ framework: route.framework,
5400
+ discoveredVia: "static"
5401
+ };
5402
+ graph.addNode(rid, node);
5403
+ nodesAdded++;
5404
+ }
5405
+ const containsId = (0, import_types11.extractedEdgeId)(service.node.id, rid, import_types11.EdgeType.CONTAINS);
5406
+ if (!graph.hasEdge(containsId)) {
5407
+ const edge = {
5408
+ id: containsId,
5409
+ source: service.node.id,
5410
+ target: rid,
5411
+ type: import_types11.EdgeType.CONTAINS,
5412
+ provenance: import_types11.Provenance.EXTRACTED,
5413
+ confidence: (0, import_types11.confidenceForExtracted)("structural"),
5414
+ evidence: {
5415
+ file: relFile,
5416
+ line: route.line,
5417
+ snippet: snippet(file.content, route.line)
5418
+ }
5419
+ };
5420
+ graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
5421
+ edgesAdded++;
5422
+ }
5423
+ }
5424
+ }
5425
+ }
5426
+ return { nodesAdded, edgesAdded };
5427
+ }
5428
+
4962
5429
  // src/extract/calls/index.ts
4963
5430
  init_cjs_shims();
4964
- var import_types17 = require("@neat.is/types");
5431
+ var import_types19 = require("@neat.is/types");
4965
5432
 
4966
5433
  // src/extract/calls/http.ts
4967
5434
  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);
5435
+ var import_node_path23 = __toESM(require("path"), 1);
5436
+ var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5437
+ var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
4971
5438
  var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
4972
- var import_types11 = require("@neat.is/types");
5439
+ var import_types12 = require("@neat.is/types");
4973
5440
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
4974
5441
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
4975
5442
  function isInsideJsxExternalLink(node) {
@@ -4997,14 +5464,14 @@ function collectStringLiterals(node, out) {
4997
5464
  if (child) collectStringLiterals(child, out);
4998
5465
  }
4999
5466
  }
5000
- var PARSE_CHUNK2 = 16384;
5001
- function parseSource2(parser, source) {
5467
+ var PARSE_CHUNK3 = 16384;
5468
+ function parseSource3(parser, source) {
5002
5469
  return parser.parse(
5003
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5470
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5004
5471
  );
5005
5472
  }
5006
5473
  function callsFromSource(source, parser, knownHosts) {
5007
- const tree = parseSource2(parser, source);
5474
+ const tree = parseSource3(parser, source);
5008
5475
  const literals = [];
5009
5476
  collectStringLiterals(tree.rootNode, literals);
5010
5477
  const out = [];
@@ -5018,27 +5485,20 @@ function callsFromSource(source, parser, knownHosts) {
5018
5485
  }
5019
5486
  return out;
5020
5487
  }
5021
- function makeJsParser2() {
5022
- const p = new import_tree_sitter2.default();
5023
- p.setLanguage(import_tree_sitter_javascript2.default);
5488
+ function makeJsParser3() {
5489
+ const p = new import_tree_sitter3.default();
5490
+ p.setLanguage(import_tree_sitter_javascript3.default);
5024
5491
  return p;
5025
5492
  }
5026
5493
  function makePyParser2() {
5027
- const p = new import_tree_sitter2.default();
5494
+ const p = new import_tree_sitter3.default();
5028
5495
  p.setLanguage(import_tree_sitter_python2.default);
5029
5496
  return p;
5030
5497
  }
5031
5498
  async function addHttpCallEdges(graph, services) {
5032
- const jsParser = makeJsParser2();
5499
+ const jsParser = makeJsParser3();
5033
5500
  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
- }
5501
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5042
5502
  let nodesAdded = 0;
5043
5503
  let edgesAdded = 0;
5044
5504
  for (const service of services) {
@@ -5046,7 +5506,7 @@ async function addHttpCallEdges(graph, services) {
5046
5506
  const seen = /* @__PURE__ */ new Set();
5047
5507
  for (const file of files) {
5048
5508
  if (isTestPath(file.path)) continue;
5049
- const parser = import_node_path22.default.extname(file.path) === ".py" ? pyParser : jsParser;
5509
+ const parser = import_node_path23.default.extname(file.path) === ".py" ? pyParser : jsParser;
5050
5510
  let sites;
5051
5511
  try {
5052
5512
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -5055,14 +5515,14 @@ async function addHttpCallEdges(graph, services) {
5055
5515
  continue;
5056
5516
  }
5057
5517
  if (sites.length === 0) continue;
5058
- const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
5518
+ const relFile = toPosix2(import_node_path23.default.relative(service.dir, file.path));
5059
5519
  for (const site of sites) {
5060
5520
  const targetId = hostToNodeId.get(site.host);
5061
5521
  if (!targetId || targetId === service.node.id) continue;
5062
5522
  const dedupKey = `${relFile}|${targetId}`;
5063
5523
  if (seen.has(dedupKey)) continue;
5064
5524
  seen.add(dedupKey);
5065
- const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
5525
+ const confidence = (0, import_types12.confidenceForExtracted)("url-literal-service-target");
5066
5526
  const ev = {
5067
5527
  file: relFile,
5068
5528
  line: site.line,
@@ -5076,25 +5536,25 @@ async function addHttpCallEdges(graph, services) {
5076
5536
  );
5077
5537
  nodesAdded += n;
5078
5538
  edgesAdded += e;
5079
- if (!(0, import_types11.passesExtractedFloor)(confidence)) {
5539
+ if (!(0, import_types12.passesExtractedFloor)(confidence)) {
5080
5540
  noteExtractedDropped({
5081
5541
  source: fileNodeId,
5082
5542
  target: targetId,
5083
- type: import_types11.EdgeType.CALLS,
5543
+ type: import_types12.EdgeType.CALLS,
5084
5544
  confidence,
5085
5545
  confidenceKind: "url-literal-service-target",
5086
5546
  evidence: ev
5087
5547
  });
5088
5548
  continue;
5089
5549
  }
5090
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types11.EdgeType.CALLS);
5550
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types12.EdgeType.CALLS);
5091
5551
  if (!graph.hasEdge(edgeId)) {
5092
5552
  const edge = {
5093
5553
  id: edgeId,
5094
5554
  source: fileNodeId,
5095
5555
  target: targetId,
5096
- type: import_types11.EdgeType.CALLS,
5097
- provenance: import_types11.Provenance.EXTRACTED,
5556
+ type: import_types12.EdgeType.CALLS,
5557
+ provenance: import_types12.Provenance.EXTRACTED,
5098
5558
  confidence,
5099
5559
  evidence: ev
5100
5560
  };
@@ -5107,10 +5567,279 @@ async function addHttpCallEdges(graph, services) {
5107
5567
  return { nodesAdded, edgesAdded };
5108
5568
  }
5109
5569
 
5570
+ // src/extract/calls/route-match.ts
5571
+ init_cjs_shims();
5572
+ var import_node_path24 = __toESM(require("path"), 1);
5573
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
5574
+ var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
5575
+ var import_types13 = require("@neat.is/types");
5576
+ var PARSE_CHUNK4 = 16384;
5577
+ function parseSource4(parser, source) {
5578
+ return parser.parse(
5579
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
5580
+ );
5581
+ }
5582
+ function makeJsParser4() {
5583
+ const p = new import_tree_sitter4.default();
5584
+ p.setLanguage(import_tree_sitter_javascript4.default);
5585
+ return p;
5586
+ }
5587
+ var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5588
+ var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
5589
+ function walk2(node, visit) {
5590
+ visit(node);
5591
+ for (let i = 0; i < node.namedChildCount; i++) {
5592
+ const child = node.namedChild(i);
5593
+ if (child) walk2(child, visit);
5594
+ }
5595
+ }
5596
+ function reconstructUrl(node) {
5597
+ if (node.type === "string") {
5598
+ for (let i = 0; i < node.namedChildCount; i++) {
5599
+ const child = node.namedChild(i);
5600
+ if (child?.type === "string_fragment") return child.text;
5601
+ }
5602
+ return "";
5603
+ }
5604
+ if (node.type === "template_string") {
5605
+ let out = "";
5606
+ for (let i = 0; i < node.namedChildCount; i++) {
5607
+ const child = node.namedChild(i);
5608
+ if (!child) continue;
5609
+ if (child.type === "string_fragment") out += child.text;
5610
+ else if (child.type === "template_substitution") out += ":param";
5611
+ }
5612
+ if (out.length === 0) {
5613
+ const raw = node.text;
5614
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5615
+ }
5616
+ return out;
5617
+ }
5618
+ return null;
5619
+ }
5620
+ function methodFromOptions(objNode) {
5621
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5622
+ const pair = objNode.namedChild(i);
5623
+ if (!pair || pair.type !== "pair") continue;
5624
+ const k = pair.childForFieldName("key");
5625
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5626
+ if (kText !== "method") continue;
5627
+ const v = pair.childForFieldName("value");
5628
+ if (v && (v.type === "string" || v.type === "template_string")) {
5629
+ const s = stringText(v);
5630
+ return s ? s.toUpperCase() : void 0;
5631
+ }
5632
+ }
5633
+ return void 0;
5634
+ }
5635
+ function stringText(node) {
5636
+ if (node.type === "string") {
5637
+ for (let i = 0; i < node.namedChildCount; i++) {
5638
+ const child = node.namedChild(i);
5639
+ if (child?.type === "string_fragment") return child.text;
5640
+ }
5641
+ return "";
5642
+ }
5643
+ return null;
5644
+ }
5645
+ function urlNodeFromConfig(objNode) {
5646
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5647
+ const pair = objNode.namedChild(i);
5648
+ if (!pair || pair.type !== "pair") continue;
5649
+ const k = pair.childForFieldName("key");
5650
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5651
+ if (kText === "url") return pair.childForFieldName("value");
5652
+ }
5653
+ return null;
5654
+ }
5655
+ function pathOf(urlStr) {
5656
+ try {
5657
+ const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
5658
+ const parsed = new URL(candidate);
5659
+ return parsed.pathname || "/";
5660
+ } catch {
5661
+ return null;
5662
+ }
5663
+ }
5664
+ function matchHost(urlStr, knownHosts) {
5665
+ for (const host of knownHosts) {
5666
+ if (urlMatchesHost(urlStr, host)) return host;
5667
+ }
5668
+ return null;
5669
+ }
5670
+ function clientCallSitesFromSource(source, parser, knownHosts) {
5671
+ const tree = parseSource4(parser, source);
5672
+ const out = [];
5673
+ const push = (urlNode, method, callNode) => {
5674
+ const urlStr = reconstructUrl(urlNode);
5675
+ if (!urlStr) return;
5676
+ const host = matchHost(urlStr, knownHosts);
5677
+ if (!host) return;
5678
+ const p = pathOf(urlStr);
5679
+ if (p === null) return;
5680
+ const line = callNode.startPosition.row + 1;
5681
+ out.push({
5682
+ host,
5683
+ method,
5684
+ pathTemplate: p,
5685
+ line,
5686
+ snippet: snippet(source, line)
5687
+ });
5688
+ };
5689
+ walk2(tree.rootNode, (node) => {
5690
+ if (node.type !== "call_expression") return;
5691
+ const fn = node.childForFieldName("function");
5692
+ if (!fn) return;
5693
+ const args = node.childForFieldName("arguments");
5694
+ const first = args?.namedChild(0);
5695
+ if (!first) return;
5696
+ const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
5697
+ if (fn.type === "identifier" && fnName === "fetch") {
5698
+ const opts = args?.namedChild(1);
5699
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5700
+ push(first, method, node);
5701
+ return;
5702
+ }
5703
+ if (fn.type === "identifier" && fnName === "axios") {
5704
+ if (first.type === "object") {
5705
+ const urlNode = urlNodeFromConfig(first);
5706
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5707
+ } else {
5708
+ const opts = args?.namedChild(1);
5709
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5710
+ push(first, method, node);
5711
+ }
5712
+ return;
5713
+ }
5714
+ if (fn.type === "member_expression") {
5715
+ const obj = fn.childForFieldName("object");
5716
+ const objName = obj?.text ?? "";
5717
+ if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
5718
+ if (fnName === "request" && first.type === "object") {
5719
+ const urlNode = urlNodeFromConfig(first);
5720
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5721
+ } else {
5722
+ push(first, fnName.toUpperCase(), node);
5723
+ }
5724
+ return;
5725
+ }
5726
+ if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
5727
+ const opts = args?.namedChild(1);
5728
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
5729
+ push(first, method, node);
5730
+ return;
5731
+ }
5732
+ }
5733
+ });
5734
+ return out;
5735
+ }
5736
+ function buildRouteIndex(graph) {
5737
+ const index = /* @__PURE__ */ new Map();
5738
+ graph.forEachNode((_id, attrs) => {
5739
+ const node = attrs;
5740
+ if (node.type !== import_types13.NodeType.RouteNode) return;
5741
+ const route = attrs;
5742
+ const owner = (0, import_types13.serviceId)(route.service);
5743
+ const entry = {
5744
+ method: route.method.toUpperCase(),
5745
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
5746
+ routeNodeId: route.id
5747
+ };
5748
+ const list = index.get(owner);
5749
+ if (list) list.push(entry);
5750
+ else index.set(owner, [entry]);
5751
+ });
5752
+ return index;
5753
+ }
5754
+ function findRoute(entries, method, normalizedPath) {
5755
+ return entries.find(
5756
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
5757
+ );
5758
+ }
5759
+ async function addRouteCallEdges(graph, services) {
5760
+ const jsParser = makeJsParser4();
5761
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5762
+ const routeIndex = buildRouteIndex(graph);
5763
+ if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
5764
+ let nodesAdded = 0;
5765
+ let edgesAdded = 0;
5766
+ for (const service of services) {
5767
+ const files = await loadSourceFiles(service.dir);
5768
+ const seen = /* @__PURE__ */ new Set();
5769
+ for (const file of files) {
5770
+ if (isTestPath(file.path)) continue;
5771
+ if (!JS_CLIENT_EXTENSIONS.has(import_node_path24.default.extname(file.path))) continue;
5772
+ let sites;
5773
+ try {
5774
+ sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
5775
+ } catch (err) {
5776
+ recordExtractionError("route-match call extraction", file.path, err);
5777
+ continue;
5778
+ }
5779
+ if (sites.length === 0) continue;
5780
+ const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
5781
+ for (const site of sites) {
5782
+ const serverServiceId = hostToNodeId.get(site.host);
5783
+ if (!serverServiceId || serverServiceId === service.node.id) continue;
5784
+ const entries = routeIndex.get(serverServiceId);
5785
+ if (!entries) continue;
5786
+ const normalizedPath = normalizePathTemplate(site.pathTemplate);
5787
+ const match = findRoute(entries, site.method, normalizedPath);
5788
+ if (!match) continue;
5789
+ const dedupKey = `${relFile}|${match.routeNodeId}`;
5790
+ if (seen.has(dedupKey)) continue;
5791
+ seen.add(dedupKey);
5792
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5793
+ graph,
5794
+ service.pkg.name,
5795
+ service.node.id,
5796
+ relFile
5797
+ );
5798
+ nodesAdded += n;
5799
+ edgesAdded += e;
5800
+ const confidence = (0, import_types13.confidenceForExtracted)("verified-call-site");
5801
+ const ev = {
5802
+ file: relFile,
5803
+ line: site.line,
5804
+ snippet: site.snippet,
5805
+ method: site.method ?? match.method,
5806
+ pathTemplate: site.pathTemplate
5807
+ };
5808
+ if (!(0, import_types13.passesExtractedFloor)(confidence)) {
5809
+ noteExtractedDropped({
5810
+ source: fileNodeId,
5811
+ target: match.routeNodeId,
5812
+ type: import_types13.EdgeType.CALLS,
5813
+ confidence,
5814
+ confidenceKind: "verified-call-site",
5815
+ evidence: ev
5816
+ });
5817
+ continue;
5818
+ }
5819
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, match.routeNodeId, import_types13.EdgeType.CALLS);
5820
+ if (!graph.hasEdge(edgeId)) {
5821
+ const edge = {
5822
+ id: edgeId,
5823
+ source: fileNodeId,
5824
+ target: match.routeNodeId,
5825
+ type: import_types13.EdgeType.CALLS,
5826
+ provenance: import_types13.Provenance.EXTRACTED,
5827
+ confidence,
5828
+ evidence: ev
5829
+ };
5830
+ graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
5831
+ edgesAdded++;
5832
+ }
5833
+ }
5834
+ }
5835
+ }
5836
+ return { nodesAdded, edgesAdded };
5837
+ }
5838
+
5110
5839
  // src/extract/calls/kafka.ts
5111
5840
  init_cjs_shims();
5112
- var import_node_path23 = __toESM(require("path"), 1);
5113
- var import_types12 = require("@neat.is/types");
5841
+ var import_node_path25 = __toESM(require("path"), 1);
5842
+ var import_types14 = require("@neat.is/types");
5114
5843
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
5115
5844
  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
5845
  function findAll(re, text) {
@@ -5131,7 +5860,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5131
5860
  seen.add(key);
5132
5861
  const line = lineOf(file.content, topic);
5133
5862
  out.push({
5134
- infraId: (0, import_types12.infraId)("kafka-topic", topic),
5863
+ infraId: (0, import_types14.infraId)("kafka-topic", topic),
5135
5864
  name: topic,
5136
5865
  kind: "kafka-topic",
5137
5866
  edgeType,
@@ -5140,7 +5869,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5140
5869
  // tier (ADR-066).
5141
5870
  confidenceKind: "verified-call-site",
5142
5871
  evidence: {
5143
- file: import_node_path23.default.relative(serviceDir, file.path),
5872
+ file: import_node_path25.default.relative(serviceDir, file.path),
5144
5873
  line,
5145
5874
  snippet: snippet(file.content, line)
5146
5875
  }
@@ -5153,8 +5882,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5153
5882
 
5154
5883
  // src/extract/calls/redis.ts
5155
5884
  init_cjs_shims();
5156
- var import_node_path24 = __toESM(require("path"), 1);
5157
- var import_types13 = require("@neat.is/types");
5885
+ var import_node_path26 = __toESM(require("path"), 1);
5886
+ var import_types15 = require("@neat.is/types");
5158
5887
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
5159
5888
  function redisEndpointsFromFile(file, serviceDir) {
5160
5889
  const out = [];
@@ -5167,7 +5896,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5167
5896
  seen.add(host);
5168
5897
  const line = lineOf(file.content, host);
5169
5898
  out.push({
5170
- infraId: (0, import_types13.infraId)("redis", host),
5899
+ infraId: (0, import_types15.infraId)("redis", host),
5171
5900
  name: host,
5172
5901
  kind: "redis",
5173
5902
  edgeType: "CALLS",
@@ -5176,7 +5905,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5176
5905
  // support tier (ADR-066).
5177
5906
  confidenceKind: "url-with-structural-support",
5178
5907
  evidence: {
5179
- file: import_node_path24.default.relative(serviceDir, file.path),
5908
+ file: import_node_path26.default.relative(serviceDir, file.path),
5180
5909
  line,
5181
5910
  snippet: snippet(file.content, line)
5182
5911
  }
@@ -5187,8 +5916,8 @@ function redisEndpointsFromFile(file, serviceDir) {
5187
5916
 
5188
5917
  // src/extract/calls/aws.ts
5189
5918
  init_cjs_shims();
5190
- var import_node_path25 = __toESM(require("path"), 1);
5191
- var import_types14 = require("@neat.is/types");
5919
+ var import_node_path27 = __toESM(require("path"), 1);
5920
+ var import_types16 = require("@neat.is/types");
5192
5921
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
5193
5922
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
5194
5923
  function hasMarker(text, markers) {
@@ -5212,7 +5941,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5212
5941
  seen.add(key);
5213
5942
  const line = lineOf(file.content, name);
5214
5943
  out.push({
5215
- infraId: (0, import_types14.infraId)(kind, name),
5944
+ infraId: (0, import_types16.infraId)(kind, name),
5216
5945
  name,
5217
5946
  kind,
5218
5947
  edgeType: "CALLS",
@@ -5221,7 +5950,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5221
5950
  // (ADR-066).
5222
5951
  confidenceKind: "verified-call-site",
5223
5952
  evidence: {
5224
- file: import_node_path25.default.relative(serviceDir, file.path),
5953
+ file: import_node_path27.default.relative(serviceDir, file.path),
5225
5954
  line,
5226
5955
  snippet: snippet(file.content, line)
5227
5956
  }
@@ -5246,8 +5975,8 @@ function awsEndpointsFromFile(file, serviceDir) {
5246
5975
 
5247
5976
  // src/extract/calls/grpc.ts
5248
5977
  init_cjs_shims();
5249
- var import_node_path26 = __toESM(require("path"), 1);
5250
- var import_types15 = require("@neat.is/types");
5978
+ var import_node_path28 = __toESM(require("path"), 1);
5979
+ var import_types17 = require("@neat.is/types");
5251
5980
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
5252
5981
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
5253
5982
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -5296,7 +6025,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5296
6025
  const { kind } = classified;
5297
6026
  const line = lineOf(file.content, m[0]);
5298
6027
  out.push({
5299
- infraId: (0, import_types15.infraId)(kind, name),
6028
+ infraId: (0, import_types17.infraId)(kind, name),
5300
6029
  name,
5301
6030
  kind,
5302
6031
  edgeType: "CALLS",
@@ -5305,7 +6034,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5305
6034
  // tier (ADR-066).
5306
6035
  confidenceKind: "verified-call-site",
5307
6036
  evidence: {
5308
- file: import_node_path26.default.relative(serviceDir, file.path),
6037
+ file: import_node_path28.default.relative(serviceDir, file.path),
5309
6038
  line,
5310
6039
  snippet: snippet(file.content, line)
5311
6040
  }
@@ -5316,8 +6045,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
5316
6045
 
5317
6046
  // src/extract/calls/supabase.ts
5318
6047
  init_cjs_shims();
5319
- var import_node_path27 = __toESM(require("path"), 1);
5320
- var import_types16 = require("@neat.is/types");
6048
+ var import_node_path29 = __toESM(require("path"), 1);
6049
+ var import_types18 = require("@neat.is/types");
5321
6050
  var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
5322
6051
  var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
5323
6052
  var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
@@ -5355,7 +6084,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5355
6084
  seen.add(name);
5356
6085
  const line = lineOf(file.content, m[0]);
5357
6086
  out.push({
5358
- infraId: (0, import_types16.infraId)("supabase", name),
6087
+ infraId: (0, import_types18.infraId)("supabase", name),
5359
6088
  name,
5360
6089
  kind: "supabase",
5361
6090
  edgeType: "CALLS",
@@ -5365,7 +6094,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5365
6094
  // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
5366
6095
  confidenceKind: "verified-call-site",
5367
6096
  evidence: {
5368
- file: import_node_path27.default.relative(serviceDir, file.path),
6097
+ file: import_node_path29.default.relative(serviceDir, file.path),
5369
6098
  line,
5370
6099
  snippet: snippet(file.content, line)
5371
6100
  }
@@ -5378,11 +6107,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5378
6107
  function edgeTypeFromEndpoint(ep) {
5379
6108
  switch (ep.edgeType) {
5380
6109
  case "PUBLISHES_TO":
5381
- return import_types17.EdgeType.PUBLISHES_TO;
6110
+ return import_types19.EdgeType.PUBLISHES_TO;
5382
6111
  case "CONSUMES_FROM":
5383
- return import_types17.EdgeType.CONSUMES_FROM;
6112
+ return import_types19.EdgeType.CONSUMES_FROM;
5384
6113
  default:
5385
- return import_types17.EdgeType.CALLS;
6114
+ return import_types19.EdgeType.CALLS;
5386
6115
  }
5387
6116
  }
5388
6117
  function isAwsKind(kind) {
@@ -5410,7 +6139,7 @@ async function addExternalEndpointEdges(graph, services) {
5410
6139
  if (!graph.hasNode(ep.infraId)) {
5411
6140
  const node = {
5412
6141
  id: ep.infraId,
5413
- type: import_types17.NodeType.InfraNode,
6142
+ type: import_types19.NodeType.InfraNode,
5414
6143
  name: ep.name,
5415
6144
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
5416
6145
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -5422,7 +6151,7 @@ async function addExternalEndpointEdges(graph, services) {
5422
6151
  nodesAdded++;
5423
6152
  }
5424
6153
  const edgeType = edgeTypeFromEndpoint(ep);
5425
- const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
6154
+ const confidence = (0, import_types19.confidenceForExtracted)(ep.confidenceKind);
5426
6155
  const relFile = toPosix2(ep.evidence.file);
5427
6156
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5428
6157
  graph,
@@ -5432,7 +6161,7 @@ async function addExternalEndpointEdges(graph, services) {
5432
6161
  );
5433
6162
  nodesAdded += n;
5434
6163
  edgesAdded += e;
5435
- if (!(0, import_types17.passesExtractedFloor)(confidence)) {
6164
+ if (!(0, import_types19.passesExtractedFloor)(confidence)) {
5436
6165
  noteExtractedDropped({
5437
6166
  source: fileNodeId,
5438
6167
  target: ep.infraId,
@@ -5452,7 +6181,7 @@ async function addExternalEndpointEdges(graph, services) {
5452
6181
  source: fileNodeId,
5453
6182
  target: ep.infraId,
5454
6183
  type: edgeType,
5455
- provenance: import_types17.Provenance.EXTRACTED,
6184
+ provenance: import_types19.Provenance.EXTRACTED,
5456
6185
  confidence,
5457
6186
  evidence: ep.evidence
5458
6187
  };
@@ -5466,9 +6195,10 @@ async function addExternalEndpointEdges(graph, services) {
5466
6195
  async function addCallEdges(graph, services) {
5467
6196
  const http = await addHttpCallEdges(graph, services);
5468
6197
  const ext = await addExternalEndpointEdges(graph, services);
6198
+ const routes = await addRouteCallEdges(graph, services);
5469
6199
  return {
5470
- nodesAdded: http.nodesAdded + ext.nodesAdded,
5471
- edgesAdded: http.edgesAdded + ext.edgesAdded
6200
+ nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
6201
+ edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
5472
6202
  };
5473
6203
  }
5474
6204
 
@@ -5477,16 +6207,16 @@ init_cjs_shims();
5477
6207
 
5478
6208
  // src/extract/infra/docker-compose.ts
5479
6209
  init_cjs_shims();
5480
- var import_node_path28 = __toESM(require("path"), 1);
5481
- var import_types19 = require("@neat.is/types");
6210
+ var import_node_path30 = __toESM(require("path"), 1);
6211
+ var import_types21 = require("@neat.is/types");
5482
6212
 
5483
6213
  // src/extract/infra/shared.ts
5484
6214
  init_cjs_shims();
5485
- var import_types18 = require("@neat.is/types");
6215
+ var import_types20 = require("@neat.is/types");
5486
6216
  function makeInfraNode(kind, name, provider = "self", extras) {
5487
6217
  return {
5488
- id: (0, import_types18.infraId)(kind, name),
5489
- type: import_types18.NodeType.InfraNode,
6218
+ id: (0, import_types20.infraId)(kind, name),
6219
+ type: import_types20.NodeType.InfraNode,
5490
6220
  name,
5491
6221
  provider,
5492
6222
  kind,
@@ -5515,7 +6245,7 @@ function dependsOnList(value) {
5515
6245
  }
5516
6246
  function serviceNameToServiceNode(name, services) {
5517
6247
  for (const s of services) {
5518
- if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
6248
+ if (s.node.name === name || import_node_path30.default.basename(s.dir) === name) return s.node.id;
5519
6249
  }
5520
6250
  return null;
5521
6251
  }
@@ -5524,7 +6254,7 @@ async function addComposeInfra(graph, scanPath, services) {
5524
6254
  let edgesAdded = 0;
5525
6255
  let composePath = null;
5526
6256
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5527
- const abs = import_node_path28.default.join(scanPath, name);
6257
+ const abs = import_node_path30.default.join(scanPath, name);
5528
6258
  if (await exists(abs)) {
5529
6259
  composePath = abs;
5530
6260
  break;
@@ -5537,13 +6267,13 @@ async function addComposeInfra(graph, scanPath, services) {
5537
6267
  } catch (err) {
5538
6268
  recordExtractionError(
5539
6269
  "infra docker-compose",
5540
- import_node_path28.default.relative(scanPath, composePath),
6270
+ import_node_path30.default.relative(scanPath, composePath),
5541
6271
  err
5542
6272
  );
5543
6273
  return { nodesAdded, edgesAdded };
5544
6274
  }
5545
6275
  if (!compose?.services) return { nodesAdded, edgesAdded };
5546
- const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
6276
+ const evidenceFile = import_node_path30.default.relative(scanPath, composePath).split(import_node_path30.default.sep).join("/");
5547
6277
  const composeNameToNodeId = /* @__PURE__ */ new Map();
5548
6278
  for (const [composeName, svc] of Object.entries(compose.services)) {
5549
6279
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -5565,15 +6295,15 @@ async function addComposeInfra(graph, scanPath, services) {
5565
6295
  for (const dep of dependsOnList(svc.depends_on)) {
5566
6296
  const targetId = composeNameToNodeId.get(dep);
5567
6297
  if (!targetId) continue;
5568
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
6298
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types21.EdgeType.DEPENDS_ON);
5569
6299
  if (graph.hasEdge(edgeId)) continue;
5570
6300
  const edge = {
5571
6301
  id: edgeId,
5572
6302
  source: sourceId,
5573
6303
  target: targetId,
5574
- type: import_types19.EdgeType.DEPENDS_ON,
5575
- provenance: import_types19.Provenance.EXTRACTED,
5576
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
6304
+ type: import_types21.EdgeType.DEPENDS_ON,
6305
+ provenance: import_types21.Provenance.EXTRACTED,
6306
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
5577
6307
  evidence: { file: evidenceFile }
5578
6308
  };
5579
6309
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5585,9 +6315,9 @@ async function addComposeInfra(graph, scanPath, services) {
5585
6315
 
5586
6316
  // src/extract/infra/dockerfile.ts
5587
6317
  init_cjs_shims();
5588
- var import_node_path29 = __toESM(require("path"), 1);
6318
+ var import_node_path31 = __toESM(require("path"), 1);
5589
6319
  var import_node_fs15 = require("fs");
5590
- var import_types20 = require("@neat.is/types");
6320
+ var import_types22 = require("@neat.is/types");
5591
6321
  function readDockerfile(content) {
5592
6322
  let image = null;
5593
6323
  const ports = [];
@@ -5616,7 +6346,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5616
6346
  let nodesAdded = 0;
5617
6347
  let edgesAdded = 0;
5618
6348
  for (const service of services) {
5619
- const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
6349
+ const dockerfilePath = import_node_path31.default.join(service.dir, "Dockerfile");
5620
6350
  if (!await exists(dockerfilePath)) continue;
5621
6351
  let content;
5622
6352
  try {
@@ -5624,7 +6354,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5624
6354
  } catch (err) {
5625
6355
  recordExtractionError(
5626
6356
  "infra dockerfile",
5627
- import_node_path29.default.relative(scanPath, dockerfilePath),
6357
+ import_node_path31.default.relative(scanPath, dockerfilePath),
5628
6358
  err
5629
6359
  );
5630
6360
  continue;
@@ -5636,8 +6366,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5636
6366
  graph.addNode(node.id, node);
5637
6367
  nodesAdded++;
5638
6368
  }
5639
- const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5640
- const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
6369
+ const relDockerfile = toPosix2(import_node_path31.default.relative(service.dir, dockerfilePath));
6370
+ const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, dockerfilePath));
5641
6371
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5642
6372
  graph,
5643
6373
  service.pkg.name,
@@ -5646,15 +6376,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5646
6376
  );
5647
6377
  nodesAdded += fn;
5648
6378
  edgesAdded += fe;
5649
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
6379
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types22.EdgeType.RUNS_ON);
5650
6380
  if (!graph.hasEdge(edgeId)) {
5651
6381
  const edge = {
5652
6382
  id: edgeId,
5653
6383
  source: fileNodeId,
5654
6384
  target: node.id,
5655
- type: import_types20.EdgeType.RUNS_ON,
5656
- provenance: import_types20.Provenance.EXTRACTED,
5657
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6385
+ type: import_types22.EdgeType.RUNS_ON,
6386
+ provenance: import_types22.Provenance.EXTRACTED,
6387
+ confidence: (0, import_types22.confidenceForExtracted)("structural"),
5658
6388
  evidence: {
5659
6389
  file: evidenceFile,
5660
6390
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -5669,15 +6399,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5669
6399
  graph.addNode(portNode.id, portNode);
5670
6400
  nodesAdded++;
5671
6401
  }
5672
- const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
6402
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types22.EdgeType.CONNECTS_TO);
5673
6403
  if (graph.hasEdge(portEdgeId)) continue;
5674
6404
  const portEdge = {
5675
6405
  id: portEdgeId,
5676
6406
  source: fileNodeId,
5677
6407
  target: portNode.id,
5678
- type: import_types20.EdgeType.CONNECTS_TO,
5679
- provenance: import_types20.Provenance.EXTRACTED,
5680
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6408
+ type: import_types22.EdgeType.CONNECTS_TO,
6409
+ provenance: import_types22.Provenance.EXTRACTED,
6410
+ confidence: (0, import_types22.confidenceForExtracted)("structural"),
5681
6411
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5682
6412
  };
5683
6413
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -5690,8 +6420,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5690
6420
  // src/extract/infra/terraform.ts
5691
6421
  init_cjs_shims();
5692
6422
  var import_node_fs16 = require("fs");
5693
- var import_node_path30 = __toESM(require("path"), 1);
5694
- var import_types21 = require("@neat.is/types");
6423
+ var import_node_path32 = __toESM(require("path"), 1);
6424
+ var import_types23 = require("@neat.is/types");
5695
6425
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5696
6426
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5697
6427
  async function walkTfFiles(start, depth = 0, max = 5) {
@@ -5701,11 +6431,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
5701
6431
  for (const entry of entries) {
5702
6432
  if (entry.isDirectory()) {
5703
6433
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
5704
- const child = import_node_path30.default.join(start, entry.name);
6434
+ const child = import_node_path32.default.join(start, entry.name);
5705
6435
  if (await isPythonVenvDir(child)) continue;
5706
6436
  out.push(...await walkTfFiles(child, depth + 1, max));
5707
6437
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
5708
- out.push(import_node_path30.default.join(start, entry.name));
6438
+ out.push(import_node_path32.default.join(start, entry.name));
5709
6439
  }
5710
6440
  }
5711
6441
  return out;
@@ -5737,7 +6467,7 @@ async function addTerraformResources(graph, scanPath) {
5737
6467
  const files = await walkTfFiles(scanPath);
5738
6468
  for (const file of files) {
5739
6469
  const content = await import_node_fs16.promises.readFile(file, "utf8");
5740
- const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
6470
+ const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, file));
5741
6471
  const resources = [];
5742
6472
  const byKey = /* @__PURE__ */ new Map();
5743
6473
  RESOURCE_RE.lastIndex = 0;
@@ -5772,16 +6502,16 @@ async function addTerraformResources(graph, scanPath) {
5772
6502
  if (!target) continue;
5773
6503
  if (seen.has(target.nodeId)) continue;
5774
6504
  seen.add(target.nodeId);
5775
- const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
6505
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types23.EdgeType.DEPENDS_ON);
5776
6506
  if (graph.hasEdge(edgeId)) continue;
5777
6507
  const line = lineAt(content, resource.bodyOffset + ref.index);
5778
6508
  const edge = {
5779
6509
  id: edgeId,
5780
6510
  source: resource.nodeId,
5781
6511
  target: target.nodeId,
5782
- type: import_types21.EdgeType.DEPENDS_ON,
5783
- provenance: import_types21.Provenance.EXTRACTED,
5784
- confidence: (0, import_types21.confidenceForExtracted)("structural"),
6512
+ type: import_types23.EdgeType.DEPENDS_ON,
6513
+ provenance: import_types23.Provenance.EXTRACTED,
6514
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
5785
6515
  evidence: { file: evidenceFile, line, snippet: key }
5786
6516
  };
5787
6517
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5795,7 +6525,7 @@ async function addTerraformResources(graph, scanPath) {
5795
6525
  // src/extract/infra/k8s.ts
5796
6526
  init_cjs_shims();
5797
6527
  var import_node_fs17 = require("fs");
5798
- var import_node_path31 = __toESM(require("path"), 1);
6528
+ var import_node_path33 = __toESM(require("path"), 1);
5799
6529
  var import_yaml3 = require("yaml");
5800
6530
  var K8S_KIND_TO_INFRA_KIND = {
5801
6531
  Service: "k8s-service",
@@ -5813,11 +6543,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
5813
6543
  for (const entry of entries) {
5814
6544
  if (entry.isDirectory()) {
5815
6545
  if (IGNORED_DIRS.has(entry.name)) continue;
5816
- const child = import_node_path31.default.join(start, entry.name);
6546
+ const child = import_node_path33.default.join(start, entry.name);
5817
6547
  if (await isPythonVenvDir(child)) continue;
5818
6548
  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));
6549
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path33.default.extname(entry.name))) {
6550
+ out.push(import_node_path33.default.join(start, entry.name));
5821
6551
  }
5822
6552
  }
5823
6553
  return out;
@@ -5861,17 +6591,17 @@ async function addInfra(graph, scanPath, services) {
5861
6591
  }
5862
6592
 
5863
6593
  // src/extract/index.ts
5864
- var import_node_path33 = __toESM(require("path"), 1);
6594
+ var import_node_path35 = __toESM(require("path"), 1);
5865
6595
 
5866
6596
  // src/extract/retire.ts
5867
6597
  init_cjs_shims();
5868
6598
  var import_node_fs18 = require("fs");
5869
- var import_node_path32 = __toESM(require("path"), 1);
5870
- var import_types22 = require("@neat.is/types");
6599
+ var import_node_path34 = __toESM(require("path"), 1);
6600
+ var import_types24 = require("@neat.is/types");
5871
6601
  function dropOrphanedFileNodes(graph) {
5872
6602
  const orphans = [];
5873
6603
  graph.forEachNode((id, attrs) => {
5874
- if (attrs.type !== import_types22.NodeType.FileNode) return;
6604
+ if (attrs.type !== import_types24.NodeType.FileNode) return;
5875
6605
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5876
6606
  orphans.push(id);
5877
6607
  }
@@ -5884,14 +6614,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5884
6614
  const bases = [scanPath, ...serviceDirs];
5885
6615
  graph.forEachEdge((id, attrs) => {
5886
6616
  const edge = attrs;
5887
- if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
6617
+ if (edge.provenance !== import_types24.Provenance.EXTRACTED) return;
5888
6618
  const evidenceFile = edge.evidence?.file;
5889
6619
  if (!evidenceFile) return;
5890
- if (import_node_path32.default.isAbsolute(evidenceFile)) {
6620
+ if (import_node_path34.default.isAbsolute(evidenceFile)) {
5891
6621
  if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
5892
6622
  return;
5893
6623
  }
5894
- const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
6624
+ const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path34.default.join(base, evidenceFile)));
5895
6625
  if (!found) toDrop.push(id);
5896
6626
  });
5897
6627
  for (const id of toDrop) graph.dropEdge(id);
@@ -5910,6 +6640,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5910
6640
  const importGraph = await addImports(graph, services);
5911
6641
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
5912
6642
  const phase3 = await addConfigNodes(graph, services, scanPath);
6643
+ const routePhase = await addRoutes(graph, services);
5913
6644
  const phase4 = await addCallEdges(graph, services);
5914
6645
  const phase5 = await addInfra(graph, scanPath, services);
5915
6646
  const ghostsRetired = retireExtractedEdgesByMissingFile(
@@ -5931,7 +6662,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5931
6662
  }
5932
6663
  const droppedEntries = drainDroppedExtracted();
5933
6664
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5934
- const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
6665
+ const rejectedPath = import_node_path35.default.join(import_node_path35.default.dirname(opts.errorsPath), "rejected.ndjson");
5935
6666
  try {
5936
6667
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5937
6668
  } catch (err) {
@@ -5941,8 +6672,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5941
6672
  }
5942
6673
  }
5943
6674
  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,
6675
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
6676
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
5946
6677
  frontiersPromoted,
5947
6678
  extractionErrors: errorEntries.length,
5948
6679
  errorEntries,
@@ -5966,8 +6697,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5966
6697
  // src/persist.ts
5967
6698
  init_cjs_shims();
5968
6699
  var import_node_fs19 = require("fs");
5969
- var import_node_path34 = __toESM(require("path"), 1);
5970
- var import_types23 = require("@neat.is/types");
6700
+ var import_node_path36 = __toESM(require("path"), 1);
6701
+ var import_types25 = require("@neat.is/types");
5971
6702
  var SCHEMA_VERSION = 4;
5972
6703
  function migrateV1ToV2(payload) {
5973
6704
  const nodes = payload.graph.nodes;
@@ -5989,12 +6720,12 @@ function migrateV2ToV3(payload) {
5989
6720
  for (const edge of edges) {
5990
6721
  const attrs = edge.attributes;
5991
6722
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5992
- attrs.provenance = import_types23.Provenance.OBSERVED;
6723
+ attrs.provenance = import_types25.Provenance.OBSERVED;
5993
6724
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5994
6725
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5995
6726
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5996
6727
  if (type && source && target) {
5997
- const newId = (0, import_types23.observedEdgeId)(source, target, type);
6728
+ const newId = (0, import_types25.observedEdgeId)(source, target, type);
5998
6729
  attrs.id = newId;
5999
6730
  if (edge.key) edge.key = newId;
6000
6731
  }
@@ -6003,7 +6734,7 @@ function migrateV2ToV3(payload) {
6003
6734
  return { ...payload, schemaVersion: 3 };
6004
6735
  }
6005
6736
  async function ensureDir(filePath) {
6006
- await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
6737
+ await import_node_fs19.promises.mkdir(import_node_path36.default.dirname(filePath), { recursive: true });
6007
6738
  }
6008
6739
  async function saveGraphToDisk(graph, outPath) {
6009
6740
  await ensureDir(outPath);
@@ -6086,19 +6817,19 @@ function startPersistLoop(graph, outPath, opts = {}) {
6086
6817
  init_cjs_shims();
6087
6818
  var import_fastify = __toESM(require("fastify"), 1);
6088
6819
  var import_cors = __toESM(require("@fastify/cors"), 1);
6089
- var import_types26 = require("@neat.is/types");
6820
+ var import_types28 = require("@neat.is/types");
6090
6821
 
6091
6822
  // src/extend/index.ts
6092
6823
  init_cjs_shims();
6093
6824
  var import_node_fs21 = require("fs");
6094
- var import_node_path36 = __toESM(require("path"), 1);
6825
+ var import_node_path38 = __toESM(require("path"), 1);
6095
6826
  var import_node_os2 = __toESM(require("os"), 1);
6096
6827
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
6097
6828
 
6098
6829
  // src/installers/package-manager.ts
6099
6830
  init_cjs_shims();
6100
6831
  var import_node_fs20 = require("fs");
6101
- var import_node_path35 = __toESM(require("path"), 1);
6832
+ var import_node_path37 = __toESM(require("path"), 1);
6102
6833
  var import_node_child_process = require("child_process");
6103
6834
  var LOCKFILE_PRIORITY = [
6104
6835
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -6120,22 +6851,22 @@ async function exists2(p) {
6120
6851
  }
6121
6852
  }
6122
6853
  async function detectPackageManager(serviceDir) {
6123
- let dir = import_node_path35.default.resolve(serviceDir);
6854
+ let dir = import_node_path37.default.resolve(serviceDir);
6124
6855
  const stops = /* @__PURE__ */ new Set();
6125
6856
  for (let i = 0; i < 64; i++) {
6126
6857
  if (stops.has(dir)) break;
6127
6858
  stops.add(dir);
6128
6859
  for (const candidate of LOCKFILE_PRIORITY) {
6129
- const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
6860
+ const lockPath = import_node_path37.default.join(dir, candidate.lockfile);
6130
6861
  if (await exists2(lockPath)) {
6131
6862
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
6132
6863
  }
6133
6864
  }
6134
- const parent = import_node_path35.default.dirname(dir);
6865
+ const parent = import_node_path37.default.dirname(dir);
6135
6866
  if (parent === dir) break;
6136
6867
  dir = parent;
6137
6868
  }
6138
- return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
6869
+ return { pm: "npm", cwd: import_node_path37.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
6139
6870
  }
6140
6871
  async function runPackageManagerInstall(cmd) {
6141
6872
  return new Promise((resolve) => {
@@ -6184,7 +6915,7 @@ async function fileExists2(p) {
6184
6915
  }
6185
6916
  }
6186
6917
  async function readPackageJson(scanPath) {
6187
- const pkgPath = import_node_path36.default.join(scanPath, "package.json");
6918
+ const pkgPath = import_node_path38.default.join(scanPath, "package.json");
6188
6919
  const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
6189
6920
  return JSON.parse(raw);
6190
6921
  }
@@ -6195,11 +6926,11 @@ async function findHookFiles(scanPath) {
6195
6926
  ).sort();
6196
6927
  }
6197
6928
  function extendLogPath() {
6198
- return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
6929
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path38.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
6199
6930
  }
6200
6931
  async function appendExtendLog(entry) {
6201
6932
  const logPath = extendLogPath();
6202
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
6933
+ await import_node_fs21.promises.mkdir(import_node_path38.default.dirname(logPath), { recursive: true });
6203
6934
  await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
6204
6935
  }
6205
6936
  function splicedContent(fileContent, snippet2) {
@@ -6258,7 +6989,7 @@ function lookupInstrumentation(library, installedVersion) {
6258
6989
  }
6259
6990
  async function describeProjectInstrumentation(ctx) {
6260
6991
  const hookFiles = await findHookFiles(ctx.scanPath);
6261
- const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
6992
+ const envNeat = await fileExists2(import_node_path38.default.join(ctx.scanPath, ".env.neat"));
6262
6993
  const registryInstrPackages = new Set(
6263
6994
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
6264
6995
  );
@@ -6280,16 +7011,16 @@ async function applyExtension(ctx, args, options) {
6280
7011
  );
6281
7012
  }
6282
7013
  for (const file of hookFiles) {
6283
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
7014
+ const content = await import_node_fs21.promises.readFile(import_node_path38.default.join(ctx.scanPath, file), "utf8");
6284
7015
  if (content.includes(args.registration_snippet)) {
6285
7016
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
6286
7017
  }
6287
7018
  }
6288
7019
  const primaryFile = hookFiles[0];
6289
- const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
7020
+ const primaryPath = import_node_path38.default.join(ctx.scanPath, primaryFile);
6290
7021
  const filesTouched = [];
6291
7022
  const depsAdded = [];
6292
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
7023
+ const pkgPath = import_node_path38.default.join(ctx.scanPath, "package.json");
6293
7024
  const pkg = await readPackageJson(ctx.scanPath);
6294
7025
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
6295
7026
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -6335,7 +7066,7 @@ async function dryRunExtension(ctx, args) {
6335
7066
  };
6336
7067
  }
6337
7068
  for (const file of hookFiles) {
6338
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
7069
+ const content = await import_node_fs21.promises.readFile(import_node_path38.default.join(ctx.scanPath, file), "utf8");
6339
7070
  if (content.includes(args.registration_snippet)) {
6340
7071
  return {
6341
7072
  library: args.library,
@@ -6357,7 +7088,7 @@ async function dryRunExtension(ctx, args) {
6357
7088
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
6358
7089
  filesTouched.push("package.json");
6359
7090
  }
6360
- const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
7091
+ const hookContent = await import_node_fs21.promises.readFile(import_node_path38.default.join(ctx.scanPath, primaryFile), "utf8");
6361
7092
  const patched = splicedContent(hookContent, args.registration_snippet);
6362
7093
  if (patched) {
6363
7094
  filesTouched.push(primaryFile);
@@ -6378,7 +7109,7 @@ async function rollbackExtension(ctx, args) {
6378
7109
  if (!match) {
6379
7110
  return { undone: false, message: "no apply found for library" };
6380
7111
  }
6381
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
7112
+ const pkgPath = import_node_path38.default.join(ctx.scanPath, "package.json");
6382
7113
  if (await fileExists2(pkgPath)) {
6383
7114
  const pkg = await readPackageJson(ctx.scanPath);
6384
7115
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -6389,7 +7120,7 @@ async function rollbackExtension(ctx, args) {
6389
7120
  }
6390
7121
  const hookFiles = await findHookFiles(ctx.scanPath);
6391
7122
  for (const file of hookFiles) {
6392
- const filePath = import_node_path36.default.join(ctx.scanPath, file);
7123
+ const filePath = import_node_path38.default.join(ctx.scanPath, file);
6393
7124
  const content = await import_node_fs21.promises.readFile(filePath, "utf8");
6394
7125
  if (content.includes(match.registration_snippet)) {
6395
7126
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -6405,7 +7136,7 @@ async function rollbackExtension(ctx, args) {
6405
7136
 
6406
7137
  // src/divergences.ts
6407
7138
  init_cjs_shims();
6408
- var import_types24 = require("@neat.is/types");
7139
+ var import_types26 = require("@neat.is/types");
6409
7140
  function bucketKey(source, target, type) {
6410
7141
  return `${type}|${source}|${target}`;
6411
7142
  }
@@ -6413,22 +7144,22 @@ function bucketEdges(graph) {
6413
7144
  const buckets = /* @__PURE__ */ new Map();
6414
7145
  graph.forEachEdge((id, attrs) => {
6415
7146
  const e = attrs;
6416
- const parsed = (0, import_types24.parseEdgeId)(id);
7147
+ const parsed = (0, import_types26.parseEdgeId)(id);
6417
7148
  const provenance = parsed?.provenance ?? e.provenance;
6418
7149
  const key = bucketKey(e.source, e.target, e.type);
6419
7150
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
6420
7151
  switch (provenance) {
6421
- case import_types24.Provenance.EXTRACTED:
7152
+ case import_types26.Provenance.EXTRACTED:
6422
7153
  cur.extracted = e;
6423
7154
  break;
6424
- case import_types24.Provenance.OBSERVED:
7155
+ case import_types26.Provenance.OBSERVED:
6425
7156
  cur.observed = e;
6426
7157
  break;
6427
- case import_types24.Provenance.INFERRED:
7158
+ case import_types26.Provenance.INFERRED:
6428
7159
  cur.inferred = e;
6429
7160
  break;
6430
7161
  default:
6431
- if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
7162
+ if (e.provenance === import_types26.Provenance.STALE) cur.stale = e;
6432
7163
  }
6433
7164
  buckets.set(key, cur);
6434
7165
  });
@@ -6437,7 +7168,7 @@ function bucketEdges(graph) {
6437
7168
  function nodeIsFrontier(graph, nodeId) {
6438
7169
  if (!graph.hasNode(nodeId)) return false;
6439
7170
  const attrs = graph.getNodeAttributes(nodeId);
6440
- return attrs.type === import_types24.NodeType.FrontierNode;
7171
+ return attrs.type === import_types26.NodeType.FrontierNode;
6441
7172
  }
6442
7173
  function clampConfidence(n) {
6443
7174
  if (!Number.isFinite(n)) return 0;
@@ -6457,14 +7188,14 @@ function gradedConfidence(edge) {
6457
7188
  return clampConfidence(confidenceForEdge(edge));
6458
7189
  }
6459
7190
  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
7191
+ import_types26.EdgeType.CALLS,
7192
+ import_types26.EdgeType.CONNECTS_TO,
7193
+ import_types26.EdgeType.PUBLISHES_TO,
7194
+ import_types26.EdgeType.CONSUMES_FROM
6464
7195
  ]);
6465
7196
  function detectMissingDivergences(graph, bucket) {
6466
7197
  const out = [];
6467
- if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
7198
+ if (bucket.type === import_types26.EdgeType.CONTAINS) return out;
6468
7199
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
6469
7200
  if (!nodeIsFrontier(graph, bucket.target)) {
6470
7201
  out.push({
@@ -6505,7 +7236,7 @@ function declaredHostFor(svc) {
6505
7236
  function hasExtractedConfiguredBy(graph, svcId) {
6506
7237
  for (const edgeId of graph.outboundEdges(svcId)) {
6507
7238
  const e = graph.getEdgeAttributes(edgeId);
6508
- if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
7239
+ if (e.type === import_types26.EdgeType.CONFIGURED_BY && e.provenance === import_types26.Provenance.EXTRACTED) {
6509
7240
  return true;
6510
7241
  }
6511
7242
  }
@@ -6518,10 +7249,10 @@ function detectHostMismatch(graph, svcId, svc) {
6518
7249
  const out = [];
6519
7250
  for (const edgeId of graph.outboundEdges(svcId)) {
6520
7251
  const edge = graph.getEdgeAttributes(edgeId);
6521
- if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6522
- if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
7252
+ if (edge.type !== import_types26.EdgeType.CONNECTS_TO) continue;
7253
+ if (edge.provenance !== import_types26.Provenance.OBSERVED) continue;
6523
7254
  const target = graph.getNodeAttributes(edge.target);
6524
- if (target.type !== import_types24.NodeType.DatabaseNode) continue;
7255
+ if (target.type !== import_types26.NodeType.DatabaseNode) continue;
6525
7256
  const observedHost = target.host?.trim();
6526
7257
  if (!observedHost) continue;
6527
7258
  if (observedHost === declaredHost) continue;
@@ -6543,10 +7274,10 @@ function detectCompatDivergences(graph, svcId, svc) {
6543
7274
  const deps = svc.dependencies ?? {};
6544
7275
  for (const edgeId of graph.outboundEdges(svcId)) {
6545
7276
  const edge = graph.getEdgeAttributes(edgeId);
6546
- if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6547
- if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
7277
+ if (edge.type !== import_types26.EdgeType.CONNECTS_TO) continue;
7278
+ if (edge.provenance !== import_types26.Provenance.OBSERVED) continue;
6548
7279
  const target = graph.getNodeAttributes(edge.target);
6549
- if (target.type !== import_types24.NodeType.DatabaseNode) continue;
7280
+ if (target.type !== import_types26.NodeType.DatabaseNode) continue;
6550
7281
  for (const pair of compatPairs()) {
6551
7282
  if (pair.engine !== target.engine) continue;
6552
7283
  const declared = deps[pair.driver];
@@ -6605,7 +7336,7 @@ function suppressHostMismatchHalves(all) {
6605
7336
  for (const d of all) {
6606
7337
  if (d.type !== "host-mismatch") continue;
6607
7338
  observedHalf.add(`${d.source}->${d.target}`);
6608
- declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
7339
+ declaredHalf.add((0, import_types26.databaseId)(d.extractedHost));
6609
7340
  }
6610
7341
  if (observedHalf.size === 0) return all;
6611
7342
  return all.filter((d) => {
@@ -6624,7 +7355,7 @@ function computeDivergences(graph, opts = {}) {
6624
7355
  }
6625
7356
  graph.forEachNode((nodeId, attrs) => {
6626
7357
  const n = attrs;
6627
- if (n.type !== import_types24.NodeType.ServiceNode) return;
7358
+ if (n.type !== import_types26.NodeType.ServiceNode) return;
6628
7359
  const svc = n;
6629
7360
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
6630
7361
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -6658,7 +7389,7 @@ function computeDivergences(graph, opts = {}) {
6658
7389
  if (a.source !== b.source) return a.source.localeCompare(b.source);
6659
7390
  return a.target.localeCompare(b.target);
6660
7391
  });
6661
- return import_types24.DivergenceResultSchema.parse({
7392
+ return import_types26.DivergenceResultSchema.parse({
6662
7393
  divergences: filtered,
6663
7394
  totalAffected: filtered.length,
6664
7395
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6744,23 +7475,23 @@ function canonicalJson(value) {
6744
7475
 
6745
7476
  // src/projects.ts
6746
7477
  init_cjs_shims();
6747
- var import_node_path37 = __toESM(require("path"), 1);
7478
+ var import_node_path39 = __toESM(require("path"), 1);
6748
7479
  function pathsForProject(project, baseDir) {
6749
7480
  if (project === DEFAULT_PROJECT) {
6750
7481
  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")
7482
+ snapshotPath: import_node_path39.default.join(baseDir, "graph.json"),
7483
+ errorsPath: import_node_path39.default.join(baseDir, "errors.ndjson"),
7484
+ staleEventsPath: import_node_path39.default.join(baseDir, "stale-events.ndjson"),
7485
+ embeddingsCachePath: import_node_path39.default.join(baseDir, "embeddings.json"),
7486
+ policyViolationsPath: import_node_path39.default.join(baseDir, "policy-violations.ndjson")
6756
7487
  };
6757
7488
  }
6758
7489
  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`)
7490
+ snapshotPath: import_node_path39.default.join(baseDir, `${project}.json`),
7491
+ errorsPath: import_node_path39.default.join(baseDir, `errors.${project}.ndjson`),
7492
+ staleEventsPath: import_node_path39.default.join(baseDir, `stale-events.${project}.ndjson`),
7493
+ embeddingsCachePath: import_node_path39.default.join(baseDir, `embeddings.${project}.json`),
7494
+ policyViolationsPath: import_node_path39.default.join(baseDir, `policy-violations.${project}.ndjson`)
6764
7495
  };
6765
7496
  }
6766
7497
  var Projects = class {
@@ -6798,23 +7529,23 @@ var Projects = class {
6798
7529
  init_cjs_shims();
6799
7530
  var import_node_fs23 = require("fs");
6800
7531
  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");
7532
+ var import_node_path40 = __toESM(require("path"), 1);
7533
+ var import_types27 = require("@neat.is/types");
6803
7534
  var LOCK_TIMEOUT_MS = 5e3;
6804
7535
  var LOCK_RETRY_MS = 50;
6805
7536
  function neatHome() {
6806
7537
  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");
7538
+ if (override && override.length > 0) return import_node_path40.default.resolve(override);
7539
+ return import_node_path40.default.join(import_node_os3.default.homedir(), ".neat");
6809
7540
  }
6810
7541
  function registryPath() {
6811
- return import_node_path38.default.join(neatHome(), "projects.json");
7542
+ return import_node_path40.default.join(neatHome(), "projects.json");
6812
7543
  }
6813
7544
  function registryLockPath() {
6814
- return import_node_path38.default.join(neatHome(), "projects.json.lock");
7545
+ return import_node_path40.default.join(neatHome(), "projects.json.lock");
6815
7546
  }
6816
7547
  function daemonPidPath() {
6817
- return import_node_path38.default.join(neatHome(), "neatd.pid");
7548
+ return import_node_path40.default.join(neatHome(), "neatd.pid");
6818
7549
  }
6819
7550
  function isPidAliveDefault(pid) {
6820
7551
  try {
@@ -6874,7 +7605,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
6874
7605
  }
6875
7606
  }
6876
7607
  async function normalizeProjectPath(input) {
6877
- const resolved = import_node_path38.default.resolve(input);
7608
+ const resolved = import_node_path40.default.resolve(input);
6878
7609
  try {
6879
7610
  return await import_node_fs23.promises.realpath(resolved);
6880
7611
  } catch {
@@ -6882,7 +7613,7 @@ async function normalizeProjectPath(input) {
6882
7613
  }
6883
7614
  }
6884
7615
  async function writeAtomically(target, contents) {
6885
- await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
7616
+ await import_node_fs23.promises.mkdir(import_node_path40.default.dirname(target), { recursive: true });
6886
7617
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
6887
7618
  const fd = await import_node_fs23.promises.open(tmp, "w");
6888
7619
  try {
@@ -6895,7 +7626,7 @@ async function writeAtomically(target, contents) {
6895
7626
  }
6896
7627
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6897
7628
  const deadline = Date.now() + timeoutMs;
6898
- await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
7629
+ await import_node_fs23.promises.mkdir(import_node_path40.default.dirname(lockPath), { recursive: true });
6899
7630
  let probedHolder = false;
6900
7631
  while (true) {
6901
7632
  try {
@@ -6948,10 +7679,10 @@ async function readRegistry() {
6948
7679
  throw err;
6949
7680
  }
6950
7681
  const parsed = JSON.parse(raw);
6951
- return import_types25.RegistryFileSchema.parse(parsed);
7682
+ return import_types27.RegistryFileSchema.parse(parsed);
6952
7683
  }
6953
7684
  async function writeRegistry(reg) {
6954
- const validated = import_types25.RegistryFileSchema.parse(reg);
7685
+ const validated = import_types27.RegistryFileSchema.parse(reg);
6955
7686
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6956
7687
  }
6957
7688
  var ProjectNameCollisionError = class extends Error {
@@ -7292,11 +8023,11 @@ function registerRoutes(scope, ctx) {
7292
8023
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7293
8024
  const parsed = [];
7294
8025
  for (const c of candidates) {
7295
- const r = import_types26.DivergenceTypeSchema.safeParse(c);
8026
+ const r = import_types28.DivergenceTypeSchema.safeParse(c);
7296
8027
  if (!r.success) {
7297
8028
  return reply.code(400).send({
7298
8029
  error: `unknown divergence type "${c}"`,
7299
- allowed: import_types26.DivergenceTypeSchema.options
8030
+ allowed: import_types28.DivergenceTypeSchema.options
7300
8031
  });
7301
8032
  }
7302
8033
  parsed.push(r.data);
@@ -7515,7 +8246,7 @@ function registerRoutes(scope, ctx) {
7515
8246
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
7516
8247
  let violations = await log.readAll();
7517
8248
  if (req.query.severity) {
7518
- const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
8249
+ const sev = import_types28.PolicySeveritySchema.safeParse(req.query.severity);
7519
8250
  if (!sev.success) {
7520
8251
  return reply.code(400).send({
7521
8252
  error: "invalid severity",
@@ -7554,7 +8285,7 @@ function registerRoutes(scope, ctx) {
7554
8285
  scope.post("/policies/check", async (req, reply) => {
7555
8286
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7556
8287
  if (!proj) return;
7557
- const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
8288
+ const parsed = import_types28.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7558
8289
  if (!parsed.success) {
7559
8290
  return reply.code(400).send({
7560
8291
  error: "invalid /policies/check body",
@@ -7817,7 +8548,7 @@ init_otel_grpc();
7817
8548
  // src/daemon.ts
7818
8549
  init_cjs_shims();
7819
8550
  var import_node_fs25 = require("fs");
7820
- var import_node_path42 = __toESM(require("path"), 1);
8551
+ var import_node_path44 = __toESM(require("path"), 1);
7821
8552
  var import_node_module = require("module");
7822
8553
  init_otel();
7823
8554
  init_auth();
@@ -7825,7 +8556,7 @@ init_auth();
7825
8556
  // src/unrouted.ts
7826
8557
  init_cjs_shims();
7827
8558
  var import_node_fs24 = require("fs");
7828
- var import_node_path41 = __toESM(require("path"), 1);
8559
+ var import_node_path43 = __toESM(require("path"), 1);
7829
8560
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
7830
8561
  return {
7831
8562
  timestamp: now.toISOString(),
@@ -7835,34 +8566,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
7835
8566
  };
7836
8567
  }
7837
8568
  async function appendUnroutedSpan(neatHome2, record) {
7838
- const target = import_node_path41.default.join(neatHome2, "errors.ndjson");
8569
+ const target = import_node_path43.default.join(neatHome2, "errors.ndjson");
7839
8570
  await import_node_fs24.promises.mkdir(neatHome2, { recursive: true });
7840
8571
  await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
7841
8572
  }
7842
8573
  function unroutedErrorsPath(neatHome2) {
7843
- return import_node_path41.default.join(neatHome2, "errors.ndjson");
8574
+ return import_node_path43.default.join(neatHome2, "errors.ndjson");
7844
8575
  }
7845
8576
 
7846
8577
  // src/daemon.ts
7847
- var import_types27 = require("@neat.is/types");
8578
+ var import_types29 = require("@neat.is/types");
7848
8579
  function daemonJsonPath(scanPath) {
7849
- return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
8580
+ return import_node_path44.default.join(scanPath, "neat-out", "daemon.json");
7850
8581
  }
7851
8582
  function daemonsDiscoveryDir(home) {
7852
8583
  const base = home && home.length > 0 ? home : neatHomeFromEnv();
7853
- return import_node_path42.default.join(base, "daemons");
8584
+ return import_node_path44.default.join(base, "daemons");
7854
8585
  }
7855
8586
  function daemonDiscoveryPath(project, home) {
7856
- return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
8587
+ return import_node_path44.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
7857
8588
  }
7858
8589
  function sanitizeDiscoveryName(project) {
7859
8590
  return project.replace(/[^A-Za-z0-9._-]/g, "_");
7860
8591
  }
7861
8592
  function neatHomeFromEnv() {
7862
8593
  const env = process.env.NEAT_HOME;
7863
- if (env && env.length > 0) return import_node_path42.default.resolve(env);
8594
+ if (env && env.length > 0) return import_node_path44.default.resolve(env);
7864
8595
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7865
- return import_node_path42.default.join(home, ".neat");
8596
+ return import_node_path44.default.join(home, ".neat");
7866
8597
  }
7867
8598
  function resolveNeatVersion() {
7868
8599
  if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
@@ -7903,17 +8634,21 @@ function teardownSlot(slot) {
7903
8634
  slot.stopPersist();
7904
8635
  } catch {
7905
8636
  }
8637
+ try {
8638
+ slot.stopStaleness();
8639
+ } catch {
8640
+ }
7906
8641
  try {
7907
8642
  slot.detachEvents();
7908
8643
  } catch {
7909
8644
  }
7910
8645
  }
7911
8646
  function neatHomeFor(opts) {
7912
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
8647
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path44.default.resolve(opts.neatHome);
7913
8648
  const env = process.env.NEAT_HOME;
7914
- if (env && env.length > 0) return import_node_path42.default.resolve(env);
8649
+ if (env && env.length > 0) return import_node_path44.default.resolve(env);
7915
8650
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7916
- return import_node_path42.default.join(home, ".neat");
8651
+ return import_node_path44.default.join(home, ".neat");
7917
8652
  }
7918
8653
  function routeSpanToProject(serviceName, projects) {
7919
8654
  if (!serviceName) return DEFAULT_PROJECT;
@@ -7957,11 +8692,11 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
7957
8692
  if (!serviceName) return true;
7958
8693
  if (serviceNameMatchesProject(serviceName, project)) return true;
7959
8694
  return graph.someNode(
7960
- (_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
8695
+ (_id, attrs) => attrs.type === import_types29.NodeType.ServiceNode && attrs.name === serviceName
7961
8696
  );
7962
8697
  }
7963
8698
  async function bootstrapProject(entry) {
7964
- const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
8699
+ const paths = pathsForProject(entry.name, import_node_path44.default.join(entry.path, "neat-out"));
7965
8700
  try {
7966
8701
  const stat = await import_node_fs25.promises.stat(entry.path);
7967
8702
  if (!stat.isDirectory()) {
@@ -7979,6 +8714,8 @@ async function bootstrapProject(entry) {
7979
8714
  paths,
7980
8715
  stopPersist: () => {
7981
8716
  },
8717
+ stopStaleness: () => {
8718
+ },
7982
8719
  detachEvents: () => {
7983
8720
  },
7984
8721
  status: "broken",
@@ -7993,6 +8730,10 @@ async function bootstrapProject(entry) {
7993
8730
  try {
7994
8731
  await extractFromDirectory(graph, entry.path);
7995
8732
  const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
8733
+ const stopStaleness = startStalenessLoop(graph, {
8734
+ staleEventsPath: paths.staleEventsPath,
8735
+ project: entry.name
8736
+ });
7996
8737
  await touchLastSeen(entry.name).catch(() => {
7997
8738
  });
7998
8739
  return {
@@ -8001,6 +8742,7 @@ async function bootstrapProject(entry) {
8001
8742
  outPath,
8002
8743
  paths,
8003
8744
  stopPersist,
8745
+ stopStaleness,
8004
8746
  detachEvents,
8005
8747
  status: "active"
8006
8748
  };
@@ -8057,7 +8799,7 @@ async function startDaemon(opts = {}) {
8057
8799
  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
8800
  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
8801
  const singleProject = projectArg;
8060
- const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
8802
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path44.default.resolve(projectPathArg) : null;
8061
8803
  if (singleProject && !singleProjectPath) {
8062
8804
  throw new Error(
8063
8805
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -8072,7 +8814,7 @@ async function startDaemon(opts = {}) {
8072
8814
  );
8073
8815
  }
8074
8816
  }
8075
- const pidPath = import_node_path42.default.join(home, "neatd.pid");
8817
+ const pidPath = import_node_path44.default.join(home, "neatd.pid");
8076
8818
  await writeAtomically(pidPath, `${process.pid}
8077
8819
  `);
8078
8820
  const slots = /* @__PURE__ */ new Map();
@@ -8474,8 +9216,8 @@ async function startDaemon(opts = {}) {
8474
9216
  let registryWatcher = null;
8475
9217
  let reloadTimer = null;
8476
9218
  if (!singleProject) try {
8477
- const regDir = import_node_path42.default.dirname(regPath);
8478
- const regBase = import_node_path42.default.basename(regPath);
9219
+ const regDir = import_node_path44.default.dirname(regPath);
9220
+ const regBase = import_node_path44.default.basename(regPath);
8479
9221
  registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
8480
9222
  if (filename !== null && filename !== regBase) return;
8481
9223
  if (reloadTimer) clearTimeout(reloadTimer);