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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/neatd.cjs CHANGED
@@ -58,9 +58,9 @@ function mountBearerAuth(app, opts) {
58
58
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
59
59
  const publicRead = opts.publicRead === true;
60
60
  app.addHook("preHandler", (req2, reply, done) => {
61
- const path45 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
+ const path48 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
62
62
  for (const suffix of suffixes) {
63
- if (path45 === suffix || path45.endsWith(suffix)) {
63
+ if (path48 === suffix || path48.endsWith(suffix)) {
64
64
  done();
65
65
  return;
66
66
  }
@@ -71,11 +71,13 @@ function mountBearerAuth(app, opts) {
71
71
  }
72
72
  const header = req2.headers.authorization;
73
73
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
74
+ opts.onReject?.();
74
75
  void reply.code(401).send({ error: "unauthorized" });
75
76
  return;
76
77
  }
77
78
  const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
78
79
  if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
80
+ opts.onReject?.();
79
81
  void reply.code(401).send({ error: "unauthorized" });
80
82
  return;
81
83
  }
@@ -188,8 +190,8 @@ function reshapeGrpcRequest(req2) {
188
190
  };
189
191
  }
190
192
  function resolveProtoRoot() {
191
- const here = import_node_path39.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
192
- return import_node_path39.default.resolve(here, "..", "proto");
193
+ const here = import_node_path42.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
194
+ return import_node_path42.default.resolve(here, "..", "proto");
193
195
  }
194
196
  function loadTraceService() {
195
197
  const protoRoot = resolveProtoRoot();
@@ -257,13 +259,13 @@ async function startOtelGrpcReceiver(opts) {
257
259
  })
258
260
  };
259
261
  }
260
- var import_node_url, import_node_path39, import_node_crypto2, grpc, protoLoader;
262
+ var import_node_url, import_node_path42, import_node_crypto2, grpc, protoLoader;
261
263
  var init_otel_grpc = __esm({
262
264
  "src/otel-grpc.ts"() {
263
265
  "use strict";
264
266
  init_cjs_shims();
265
267
  import_node_url = require("url");
266
- import_node_path39 = __toESM(require("path"), 1);
268
+ import_node_path42 = __toESM(require("path"), 1);
267
269
  import_node_crypto2 = require("crypto");
268
270
  grpc = __toESM(require("@grpc/grpc-js"), 1);
269
271
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -336,6 +338,36 @@ function pickEnv(spanAttrs, resourceAttrs) {
336
338
  }
337
339
  return ENV_FALLBACK;
338
340
  }
341
+ function messagingDestinationOf(attrs) {
342
+ for (const key of ["messaging.destination.name", "messaging.destination"]) {
343
+ const v = attrs[key];
344
+ if (typeof v === "string" && v.length > 0) return v;
345
+ }
346
+ return void 0;
347
+ }
348
+ function hasWebsocketUpgradeHeader(attrs) {
349
+ const v = attrs["http.request.header.upgrade"];
350
+ const matches = (s) => typeof s === "string" && s.trim().toLowerCase() === "websocket";
351
+ if (Array.isArray(v)) return v.some(matches);
352
+ return matches(v);
353
+ }
354
+ function websocketChannelPathOf(attrs) {
355
+ const route = attrs["http.route"];
356
+ if (typeof route === "string" && route.length > 0) return route;
357
+ for (const key of ["url.path", "http.target"]) {
358
+ const v = attrs[key];
359
+ if (typeof v === "string" && v.length > 0) {
360
+ const q = v.indexOf("?");
361
+ const path48 = q === -1 ? v : v.slice(0, q);
362
+ if (path48.length > 0) return path48;
363
+ }
364
+ }
365
+ return void 0;
366
+ }
367
+ function websocketChannelOf(attrs) {
368
+ if (!hasWebsocketUpgradeHeader(attrs)) return void 0;
369
+ return websocketChannelPathOf(attrs);
370
+ }
339
371
  function parseOtlpRequest(body) {
340
372
  const out = [];
341
373
  for (const rs of body.resourceSpans ?? []) {
@@ -362,6 +394,14 @@ function parseOtlpRequest(body) {
362
394
  attributes: attrs,
363
395
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
364
396
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
397
+ messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
398
+ messagingDestination: messagingDestinationOf(attrs),
399
+ graphqlOperationName: typeof attrs["graphql.operation.name"] === "string" && attrs["graphql.operation.name"].length > 0 ? attrs["graphql.operation.name"] : void 0,
400
+ graphqlOperationType: typeof attrs["graphql.operation.type"] === "string" && attrs["graphql.operation.type"].length > 0 ? attrs["graphql.operation.type"] : void 0,
401
+ rpcSystem: typeof attrs["rpc.system"] === "string" && attrs["rpc.system"].length > 0 ? attrs["rpc.system"] : void 0,
402
+ rpcService: typeof attrs["rpc.service"] === "string" && attrs["rpc.service"].length > 0 ? attrs["rpc.service"] : void 0,
403
+ rpcMethod: typeof attrs["rpc.method"] === "string" && attrs["rpc.method"].length > 0 ? attrs["rpc.method"] : void 0,
404
+ websocketChannel: websocketChannelOf(attrs),
365
405
  statusCode: span.status?.code,
366
406
  errorMessage: span.status?.message,
367
407
  exception: extractExceptionFromEvents(span.events)
@@ -373,10 +413,10 @@ function parseOtlpRequest(body) {
373
413
  return out;
374
414
  }
375
415
  function loadProtoRoot() {
376
- const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
377
- const protoRoot = import_node_path40.default.resolve(here, "..", "proto");
416
+ const here = import_node_path43.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
417
+ const protoRoot = import_node_path43.default.resolve(here, "..", "proto");
378
418
  const root = new import_protobufjs.default.Root();
379
- root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
419
+ root.resolvePath = (_origin, target) => import_node_path43.default.resolve(protoRoot, target);
380
420
  root.loadSync(
381
421
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
382
422
  { keepCase: true }
@@ -421,7 +461,21 @@ async function buildOtelReceiver(opts) {
421
461
  logger: false,
422
462
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
423
463
  });
424
- mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
464
+ const REJECT_WARN_INTERVAL_MS = 6e4;
465
+ let lastRejectWarnAt = 0;
466
+ const warnRejectedOtlp = () => {
467
+ const now = Date.now();
468
+ if (now - lastRejectWarnAt < REJECT_WARN_INTERVAL_MS) return;
469
+ lastRejectWarnAt = now;
470
+ console.warn(
471
+ "[neatd] rejecting OTLP spans on /v1/traces \u2014 missing or invalid bearer token (set NEAT_OTEL_TOKEN on the instrumented app)"
472
+ );
473
+ };
474
+ mountBearerAuth(app, {
475
+ token: opts.authToken,
476
+ trustProxy: opts.trustProxy,
477
+ onReject: warnRejectedOtlp
478
+ });
425
479
  const queue = [];
426
480
  let draining = false;
427
481
  let drainPromise = Promise.resolve();
@@ -590,12 +644,12 @@ async function listenSteppingOtlp(app, requestedPort, host) {
590
644
  }
591
645
  }
592
646
  }
593
- 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;
647
+ var import_node_path43, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
594
648
  var init_otel = __esm({
595
649
  "src/otel.ts"() {
596
650
  "use strict";
597
651
  init_cjs_shims();
598
- import_node_path40 = __toESM(require("path"), 1);
652
+ import_node_path43 = __toESM(require("path"), 1);
599
653
  import_node_url2 = require("url");
600
654
  import_fastify2 = __toESM(require("fastify"), 1);
601
655
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -618,14 +672,14 @@ __export(neatd_exports, {
618
672
  });
619
673
  module.exports = __toCommonJS(neatd_exports);
620
674
  init_cjs_shims();
621
- var import_node_fs27 = require("fs");
622
- var import_node_path44 = __toESM(require("path"), 1);
675
+ var import_node_fs28 = require("fs");
676
+ var import_node_path47 = __toESM(require("path"), 1);
623
677
  var import_node_module2 = require("module");
624
678
 
625
679
  // src/daemon.ts
626
680
  init_cjs_shims();
627
- var import_node_fs25 = require("fs");
628
- var import_node_path42 = __toESM(require("path"), 1);
681
+ var import_node_fs26 = require("fs");
682
+ var import_node_path45 = __toESM(require("path"), 1);
629
683
  var import_node_module = require("module");
630
684
 
631
685
  // src/graph.ts
@@ -1155,19 +1209,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1155
1209
  function longestIncomingWalk(graph, start, maxDepth) {
1156
1210
  let best = { path: [start], edges: [] };
1157
1211
  const visited = /* @__PURE__ */ new Set([start]);
1158
- function step(node, path45, edges) {
1159
- if (path45.length > best.path.length) {
1160
- best = { path: [...path45], edges: [...edges] };
1212
+ function step(node, path48, edges) {
1213
+ if (path48.length > best.path.length) {
1214
+ best = { path: [...path48], edges: [...edges] };
1161
1215
  }
1162
- if (path45.length - 1 >= maxDepth) return;
1216
+ if (path48.length - 1 >= maxDepth) return;
1163
1217
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1164
1218
  for (const [srcId, edge] of incoming) {
1165
1219
  if (visited.has(srcId)) continue;
1166
1220
  visited.add(srcId);
1167
- path45.push(srcId);
1221
+ path48.push(srcId);
1168
1222
  edges.push(edge);
1169
- step(srcId, path45, edges);
1170
- path45.pop();
1223
+ step(srcId, path48, edges);
1224
+ path48.pop();
1171
1225
  edges.pop();
1172
1226
  visited.delete(srcId);
1173
1227
  }
@@ -1175,14 +1229,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
1175
1229
  step(start, [start], []);
1176
1230
  return best;
1177
1231
  }
1178
- function databaseRootCauseShape(graph, origin, walk) {
1232
+ function databaseRootCauseShape(graph, origin, walk3) {
1179
1233
  const targetDb = origin;
1180
1234
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1181
1235
  if (candidatePairs.length === 0) return null;
1182
- for (const id of walk.path) {
1236
+ for (const id of walk3.path) {
1183
1237
  const owner = resolveOwningService(graph, id);
1184
1238
  if (!owner) continue;
1185
- const { id: serviceId3, svc } = owner;
1239
+ const { id: serviceId4, svc } = owner;
1186
1240
  const deps = svc.dependencies ?? {};
1187
1241
  for (const pair of candidatePairs) {
1188
1242
  const declared = deps[pair.driver];
@@ -1195,7 +1249,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1195
1249
  );
1196
1250
  if (!result.compatible) {
1197
1251
  return {
1198
- rootCauseNode: serviceId3,
1252
+ rootCauseNode: serviceId4,
1199
1253
  rootCauseReason: result.reason ?? "incompatible driver",
1200
1254
  ...result.minDriverVersion ? {
1201
1255
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1206,11 +1260,11 @@ function databaseRootCauseShape(graph, origin, walk) {
1206
1260
  }
1207
1261
  return null;
1208
1262
  }
1209
- function serviceRootCauseShape(graph, _origin, walk) {
1210
- for (const id of walk.path) {
1263
+ function serviceRootCauseShape(graph, _origin, walk3) {
1264
+ for (const id of walk3.path) {
1211
1265
  const owner = resolveOwningService(graph, id);
1212
1266
  if (!owner) continue;
1213
- const { id: serviceId3, svc } = owner;
1267
+ const { id: serviceId4, svc } = owner;
1214
1268
  const deps = svc.dependencies ?? {};
1215
1269
  const serviceNodeEngine = svc.nodeEngine;
1216
1270
  for (const constraint of nodeEngineConstraints()) {
@@ -1219,7 +1273,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1219
1273
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1220
1274
  if (!result.compatible && result.reason) {
1221
1275
  return {
1222
- rootCauseNode: serviceId3,
1276
+ rootCauseNode: serviceId4,
1223
1277
  rootCauseReason: result.reason,
1224
1278
  ...result.requiredNodeVersion ? {
1225
1279
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1234,7 +1288,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1234
1288
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1235
1289
  if (!result.compatible && result.reason) {
1236
1290
  return {
1237
- rootCauseNode: serviceId3,
1291
+ rootCauseNode: serviceId4,
1238
1292
  rootCauseReason: result.reason,
1239
1293
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1240
1294
  };
@@ -1243,10 +1297,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
1243
1297
  }
1244
1298
  return null;
1245
1299
  }
1246
- function fileRootCauseShape(graph, origin, walk) {
1300
+ function fileRootCauseShape(graph, origin, walk3) {
1247
1301
  const owner = resolveOwningService(graph, origin.id);
1248
1302
  if (!owner) return null;
1249
- return serviceRootCauseShape(graph, owner.svc, walk);
1303
+ return serviceRootCauseShape(graph, owner.svc, walk3);
1250
1304
  }
1251
1305
  var rootCauseShapes = {
1252
1306
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
@@ -1258,16 +1312,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1258
1312
  const origin = graph.getNodeAttributes(errorNodeId);
1259
1313
  const shape = rootCauseShapes[origin.type];
1260
1314
  if (shape) {
1261
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1262
- const match = shape(graph, origin, walk);
1315
+ const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1316
+ const match = shape(graph, origin, walk3);
1263
1317
  if (match) {
1264
1318
  const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1265
1319
  return import_types.RootCauseResultSchema.parse({
1266
1320
  rootCauseNode: match.rootCauseNode,
1267
1321
  rootCauseReason: reason,
1268
- traversalPath: walk.path,
1269
- edgeProvenances: walk.edges.map((e) => e.provenance),
1270
- confidence: confidenceFromMix(walk.edges),
1322
+ traversalPath: walk3.path,
1323
+ edgeProvenances: walk3.edges.map((e) => e.provenance),
1324
+ confidence: confidenceFromMix(walk3.edges),
1271
1325
  fixRecommendation: match.fixRecommendation
1272
1326
  });
1273
1327
  }
@@ -1325,9 +1379,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1325
1379
  function isFailingCallEdge(e) {
1326
1380
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1327
1381
  }
1328
- function callSourcesForService(graph, serviceId3) {
1329
- const ids = [serviceId3];
1330
- for (const edgeId of graph.outboundEdges(serviceId3)) {
1382
+ function callSourcesForService(graph, serviceId4) {
1383
+ const ids = [serviceId4];
1384
+ for (const edgeId of graph.outboundEdges(serviceId4)) {
1331
1385
  const e = graph.getEdgeAttributes(edgeId);
1332
1386
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1333
1387
  const tgt = graph.getNodeAttributes(e.target);
@@ -1344,9 +1398,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1344
1398
  }
1345
1399
  return id < curId;
1346
1400
  }
1347
- function dominantFailingCall(graph, serviceId3, visited) {
1401
+ function dominantFailingCall(graph, serviceId4, visited) {
1348
1402
  let best = null;
1349
- for (const src of callSourcesForService(graph, serviceId3)) {
1403
+ for (const src of callSourcesForService(graph, serviceId4)) {
1350
1404
  for (const edgeId of graph.outboundEdges(src)) {
1351
1405
  const e = graph.getEdgeAttributes(edgeId);
1352
1406
  if (!isFailingCallEdge(e)) continue;
@@ -1361,26 +1415,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
1361
1415
  return best;
1362
1416
  }
1363
1417
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1364
- const path45 = [originServiceId];
1418
+ const path48 = [originServiceId];
1365
1419
  const edges = [];
1366
1420
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1367
1421
  let current = originServiceId;
1368
1422
  for (let depth = 0; depth < maxDepth; depth++) {
1369
1423
  const hop = dominantFailingCall(graph, current, visited);
1370
1424
  if (!hop) break;
1371
- path45.push(hop.nextService);
1425
+ path48.push(hop.nextService);
1372
1426
  edges.push(hop.edge);
1373
1427
  visited.add(hop.nextService);
1374
1428
  current = hop.nextService;
1375
1429
  }
1376
1430
  if (edges.length === 0) return null;
1377
- return { path: path45, edges, culprit: current };
1431
+ return { path: path48, edges, culprit: current };
1378
1432
  }
1379
1433
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1380
1434
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1381
1435
  if (!chain) return null;
1382
1436
  const culprit = chain.culprit;
1383
- const path45 = [...chain.path];
1437
+ const path48 = [...chain.path];
1384
1438
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1385
1439
  const baseConfidence = confidenceFromMix(chain.edges);
1386
1440
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1388,14 +1442,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1388
1442
  if (loc) {
1389
1443
  let rootCauseNode = culprit;
1390
1444
  if (loc.fileNode) {
1391
- path45.push(loc.fileNode);
1445
+ path48.push(loc.fileNode);
1392
1446
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1393
1447
  rootCauseNode = loc.fileNode;
1394
1448
  }
1395
1449
  return import_types.RootCauseResultSchema.parse({
1396
1450
  rootCauseNode,
1397
1451
  rootCauseReason: loc.rootCauseReason,
1398
- traversalPath: path45,
1452
+ traversalPath: path48,
1399
1453
  edgeProvenances,
1400
1454
  confidence,
1401
1455
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1407,7 +1461,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1407
1461
  return import_types.RootCauseResultSchema.parse({
1408
1462
  rootCauseNode: culprit,
1409
1463
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1410
- traversalPath: path45,
1464
+ traversalPath: path48,
1411
1465
  edgeProvenances,
1412
1466
  confidence,
1413
1467
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2007,6 +2061,28 @@ var DEFAULT_STALE_THRESHOLDS = {
2007
2061
  CONFIGURED_BY: DAY_MS,
2008
2062
  RUNS_ON: DAY_MS
2009
2063
  };
2064
+ var FALLBACK_STALE_THRESHOLD_MS = DAY_MS;
2065
+ function loadStaleThresholdsFromEnv() {
2066
+ const raw = process.env.NEAT_STALE_THRESHOLDS;
2067
+ if (!raw) return DEFAULT_STALE_THRESHOLDS;
2068
+ try {
2069
+ const overrides = JSON.parse(raw);
2070
+ const merged = { ...DEFAULT_STALE_THRESHOLDS };
2071
+ for (const [k, v] of Object.entries(overrides)) {
2072
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) merged[k] = v;
2073
+ }
2074
+ return merged;
2075
+ } catch (err) {
2076
+ console.warn(
2077
+ `[neat] NEAT_STALE_THRESHOLDS could not be parsed (${err.message}); using defaults`
2078
+ );
2079
+ return DEFAULT_STALE_THRESHOLDS;
2080
+ }
2081
+ }
2082
+ function thresholdForEdgeType(edgeType, overrides) {
2083
+ const map = overrides ?? loadStaleThresholdsFromEnv();
2084
+ return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
2085
+ }
2010
2086
  var DEFAULT_INCIDENT_THRESHOLDS = {
2011
2087
  threshold: 5,
2012
2088
  windowMs: 6e4
@@ -2309,10 +2385,82 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2309
2385
  ]);
2310
2386
  var WIRE_SPAN_KIND_CLIENT = 3;
2311
2387
  var WIRE_SPAN_KIND_PRODUCER = 4;
2388
+ var WIRE_SPAN_KIND_CONSUMER = 5;
2312
2389
  function spanMintsObservedEdge(kind) {
2313
2390
  if (kind === void 0 || kind === 0) return true;
2314
2391
  return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
2315
2392
  }
2393
+ function spanMintsMessagingEdge(kind) {
2394
+ return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
2395
+ }
2396
+ function spanServesGraphqlOperation(kind) {
2397
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2398
+ }
2399
+ function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
2400
+ const id = (0, import_types3.graphqlOperationId)(serviceName, operationType, operationName);
2401
+ if (graph.hasNode(id)) return id;
2402
+ const node = {
2403
+ id,
2404
+ type: import_types3.NodeType.GraphQLOperationNode,
2405
+ name: operationName,
2406
+ service: serviceName,
2407
+ operationType: operationType.toLowerCase(),
2408
+ operationName,
2409
+ discoveredVia: "otel"
2410
+ };
2411
+ graph.addNode(id, node);
2412
+ return id;
2413
+ }
2414
+ function spanServesGrpcMethod(kind) {
2415
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2416
+ }
2417
+ function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
2418
+ const id = (0, import_types3.grpcMethodId)(rpcService, rpcMethod);
2419
+ if (graph.hasNode(id)) return id;
2420
+ const node = {
2421
+ id,
2422
+ type: import_types3.NodeType.GrpcMethodNode,
2423
+ name: `${rpcService}/${rpcMethod}`,
2424
+ rpcService,
2425
+ rpcMethod,
2426
+ discoveredVia: "otel"
2427
+ };
2428
+ graph.addNode(id, node);
2429
+ return id;
2430
+ }
2431
+ function spanServesWebsocketChannel(kind) {
2432
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2433
+ }
2434
+ function ensureWebsocketChannelNode(graph, serviceName, channel) {
2435
+ const id = (0, import_types3.websocketChannelId)(serviceName, channel);
2436
+ if (graph.hasNode(id)) return id;
2437
+ const node = {
2438
+ id,
2439
+ type: import_types3.NodeType.WebSocketChannelNode,
2440
+ name: channel,
2441
+ service: serviceName,
2442
+ channel,
2443
+ discoveredVia: "otel"
2444
+ };
2445
+ graph.addNode(id, node);
2446
+ return id;
2447
+ }
2448
+ function messagingDestinationKind(system) {
2449
+ return `${system}-topic`;
2450
+ }
2451
+ function ensureMessagingDestinationNode(graph, system, destination) {
2452
+ const id = (0, import_types3.infraId)(messagingDestinationKind(system), destination);
2453
+ if (graph.hasNode(id)) return id;
2454
+ const node = {
2455
+ id,
2456
+ type: import_types3.NodeType.InfraNode,
2457
+ name: destination,
2458
+ provider: "self",
2459
+ kind: messagingDestinationKind(system)
2460
+ };
2461
+ graph.addNode(id, node);
2462
+ return id;
2463
+ }
2316
2464
  var PARENT_SPAN_CACHE_SIZE = 1e4;
2317
2465
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
2318
2466
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -2406,6 +2554,21 @@ function ensureDatabaseNode(graph, host, engine) {
2406
2554
  graph.addNode(id, node);
2407
2555
  return id;
2408
2556
  }
2557
+ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
2558
+ const id = (0, import_types3.localDatabaseId)(serviceName, name);
2559
+ if (graph.hasNode(id)) return id;
2560
+ const node = {
2561
+ id,
2562
+ type: import_types3.NodeType.DatabaseNode,
2563
+ name,
2564
+ engine,
2565
+ engineVersion: "unknown",
2566
+ compatibleDrivers: [],
2567
+ discoveredVia: "otel"
2568
+ };
2569
+ graph.addNode(id, node);
2570
+ return id;
2571
+ }
2409
2572
  function ensureFrontierNode(graph, host, ts) {
2410
2573
  const id = frontierIdFor(host);
2411
2574
  if (graph.hasNode(id)) {
@@ -2657,10 +2820,21 @@ async function handleSpan(ctx, span) {
2657
2820
  let affectedNode = sourceId;
2658
2821
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2659
2822
  if (span.dbSystem) {
2660
- const host = pickAddress(span);
2661
- if (mintsFromCallerSide && host) {
2662
- ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2663
- const targetId = (0, import_types3.databaseId)(host);
2823
+ if (mintsFromCallerSide) {
2824
+ const host = pickAddress(span);
2825
+ let targetId;
2826
+ if (host) {
2827
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2828
+ targetId = (0, import_types3.databaseId)(host);
2829
+ } else {
2830
+ const localName = span.dbName ?? span.dbSystem;
2831
+ targetId = ensureLocalDatabaseNode(
2832
+ ctx.graph,
2833
+ span.service,
2834
+ localName,
2835
+ span.dbSystem
2836
+ );
2837
+ }
2664
2838
  const result = upsertObservedEdge(
2665
2839
  ctx.graph,
2666
2840
  import_types3.EdgeType.CONNECTS_TO,
@@ -2672,6 +2846,68 @@ async function handleSpan(ctx, span) {
2672
2846
  );
2673
2847
  if (result) affectedNode = targetId;
2674
2848
  }
2849
+ } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
2850
+ const targetId = ensureMessagingDestinationNode(
2851
+ ctx.graph,
2852
+ span.messagingSystem,
2853
+ span.messagingDestination
2854
+ );
2855
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types3.EdgeType.CONSUMES_FROM : import_types3.EdgeType.PUBLISHES_TO;
2856
+ const result = upsertObservedEdge(
2857
+ ctx.graph,
2858
+ edgeType,
2859
+ observedSource(),
2860
+ targetId,
2861
+ ts,
2862
+ isError,
2863
+ callSiteEvidence
2864
+ );
2865
+ if (result) affectedNode = targetId;
2866
+ } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
2867
+ const targetId = ensureGraphqlOperationNode(
2868
+ ctx.graph,
2869
+ span.service,
2870
+ span.graphqlOperationType,
2871
+ span.graphqlOperationName
2872
+ );
2873
+ const result = upsertObservedEdge(
2874
+ ctx.graph,
2875
+ import_types3.EdgeType.CONTAINS,
2876
+ observedSource(),
2877
+ targetId,
2878
+ ts,
2879
+ isError,
2880
+ callSiteEvidence
2881
+ );
2882
+ if (result) affectedNode = targetId;
2883
+ } else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
2884
+ const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
2885
+ const result = upsertObservedEdge(
2886
+ ctx.graph,
2887
+ import_types3.EdgeType.CONTAINS,
2888
+ observedSource(),
2889
+ targetId,
2890
+ ts,
2891
+ isError,
2892
+ callSiteEvidence
2893
+ );
2894
+ if (result) affectedNode = targetId;
2895
+ } else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
2896
+ const targetId = ensureWebsocketChannelNode(
2897
+ ctx.graph,
2898
+ span.service,
2899
+ span.websocketChannel
2900
+ );
2901
+ const result = upsertObservedEdge(
2902
+ ctx.graph,
2903
+ import_types3.EdgeType.CONNECTS_TO,
2904
+ observedSource(),
2905
+ targetId,
2906
+ ts,
2907
+ isError,
2908
+ callSiteEvidence
2909
+ );
2910
+ if (result) affectedNode = targetId;
2675
2911
  } else {
2676
2912
  const host = pickAddress(span);
2677
2913
  let resolvedViaAddress = false;
@@ -2781,29 +3017,29 @@ function promoteFrontierNodes(graph, opts = {}) {
2781
3017
  toPromote.push({ frontierId: id, serviceId: target });
2782
3018
  });
2783
3019
  let promoted = 0;
2784
- for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
3020
+ for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
2785
3021
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
2786
3022
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
2787
3023
  if (!gate.allowed) {
2788
3024
  continue;
2789
3025
  }
2790
3026
  }
2791
- rewireFrontierEdges(graph, frontierId2, serviceId3);
3027
+ rewireFrontierEdges(graph, frontierId2, serviceId4);
2792
3028
  graph.dropNode(frontierId2);
2793
3029
  promoted++;
2794
3030
  }
2795
3031
  return promoted;
2796
3032
  }
2797
- function rewireFrontierEdges(graph, frontierId2, serviceId3) {
3033
+ function rewireFrontierEdges(graph, frontierId2, serviceId4) {
2798
3034
  const inbound = [...graph.inboundEdges(frontierId2)];
2799
3035
  const outbound = [...graph.outboundEdges(frontierId2)];
2800
3036
  for (const edgeId of inbound) {
2801
3037
  const edge = graph.getEdgeAttributes(edgeId);
2802
- rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
3038
+ rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
2803
3039
  }
2804
3040
  for (const edgeId of outbound) {
2805
3041
  const edge = graph.getEdgeAttributes(edgeId);
2806
- rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
3042
+ rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
2807
3043
  }
2808
3044
  }
2809
3045
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -2832,6 +3068,51 @@ function pickLater(a, b) {
2832
3068
  if (!b) return a;
2833
3069
  return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
2834
3070
  }
3071
+ async function markStaleEdges(graph, options = {}) {
3072
+ const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
3073
+ const now = options.now ?? Date.now();
3074
+ const events = [];
3075
+ const project = options.project ?? DEFAULT_PROJECT;
3076
+ graph.forEachEdge((id, attrs) => {
3077
+ const e = attrs;
3078
+ if (e.provenance !== import_types3.Provenance.OBSERVED) return;
3079
+ if (!e.lastObserved) return;
3080
+ const threshold = thresholdForEdgeType(e.type, thresholds);
3081
+ const age = now - new Date(e.lastObserved).getTime();
3082
+ if (age > threshold) {
3083
+ const updated = { ...e, provenance: import_types3.Provenance.STALE, confidence: 0.3 };
3084
+ graph.replaceEdgeAttributes(id, updated);
3085
+ events.push({
3086
+ edgeId: id,
3087
+ source: e.source,
3088
+ target: e.target,
3089
+ edgeType: e.type,
3090
+ thresholdMs: threshold,
3091
+ ageMs: age,
3092
+ lastObserved: e.lastObserved,
3093
+ transitionedAt: new Date(now).toISOString()
3094
+ });
3095
+ emitNeatEvent({
3096
+ type: "stale-transition",
3097
+ project,
3098
+ payload: {
3099
+ edgeId: id,
3100
+ from: import_types3.Provenance.OBSERVED,
3101
+ to: import_types3.Provenance.STALE
3102
+ }
3103
+ });
3104
+ }
3105
+ });
3106
+ if (options.staleEventsPath && events.length > 0) {
3107
+ await appendStaleEvents(options.staleEventsPath, events);
3108
+ }
3109
+ return { count: events.length, events };
3110
+ }
3111
+ async function appendStaleEvents(staleEventsPath, events) {
3112
+ await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(staleEventsPath), { recursive: true });
3113
+ const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
3114
+ await import_node_fs3.promises.appendFile(staleEventsPath, lines, "utf8");
3115
+ }
2835
3116
  async function readStaleEvents(staleEventsPath) {
2836
3117
  try {
2837
3118
  const raw = await import_node_fs3.promises.readFile(staleEventsPath, "utf8");
@@ -2841,6 +3122,31 @@ async function readStaleEvents(staleEventsPath) {
2841
3122
  throw err;
2842
3123
  }
2843
3124
  }
3125
+ function startStalenessLoop(graph, options = {}) {
3126
+ let stopped = false;
3127
+ const intervalMs = options.intervalMs ?? 6e4;
3128
+ const tick = () => {
3129
+ if (stopped) return;
3130
+ void (async () => {
3131
+ try {
3132
+ await markStaleEdges(graph, {
3133
+ thresholds: options.thresholds,
3134
+ staleEventsPath: options.staleEventsPath,
3135
+ project: options.project
3136
+ });
3137
+ if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
3138
+ } catch (err) {
3139
+ console.error("staleness tick failed", err);
3140
+ }
3141
+ })();
3142
+ };
3143
+ const interval = setInterval(tick, intervalMs);
3144
+ if (typeof interval.unref === "function") interval.unref();
3145
+ return () => {
3146
+ stopped = true;
3147
+ clearInterval(interval);
3148
+ };
3149
+ }
2844
3150
  async function readErrorEvents(errorsPath) {
2845
3151
  try {
2846
3152
  const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
@@ -3492,9 +3798,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
3492
3798
  "StatefulSet",
3493
3799
  "DaemonSet"
3494
3800
  ]);
3495
- function addAliases(graph, serviceId3, candidates) {
3496
- if (!graph.hasNode(serviceId3)) return;
3497
- const node = graph.getNodeAttributes(serviceId3);
3801
+ function addAliases(graph, serviceId4, candidates) {
3802
+ if (!graph.hasNode(serviceId4)) return;
3803
+ const node = graph.getNodeAttributes(serviceId4);
3498
3804
  if (node.type !== import_types6.NodeType.ServiceNode) return;
3499
3805
  const set = new Set(node.aliases ?? []);
3500
3806
  for (const c of candidates) {
@@ -3504,7 +3810,7 @@ function addAliases(graph, serviceId3, candidates) {
3504
3810
  }
3505
3811
  if (set.size === 0) return;
3506
3812
  const updated = { ...node, aliases: [...set].sort() };
3507
- graph.replaceNodeAttributes(serviceId3, updated);
3813
+ graph.replaceNodeAttributes(serviceId4, updated);
3508
3814
  }
3509
3815
  function indexServicesByName(services) {
3510
3816
  const map = /* @__PURE__ */ new Map();
@@ -3537,12 +3843,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3537
3843
  }
3538
3844
  if (!compose?.services) return;
3539
3845
  for (const [composeName, svc] of Object.entries(compose.services)) {
3540
- const serviceId3 = serviceIndex.get(composeName);
3541
- if (!serviceId3) continue;
3846
+ const serviceId4 = serviceIndex.get(composeName);
3847
+ if (!serviceId4) continue;
3542
3848
  const aliases = /* @__PURE__ */ new Set([composeName]);
3543
3849
  if (svc.container_name) aliases.add(svc.container_name);
3544
3850
  if (svc.hostname) aliases.add(svc.hostname);
3545
- addAliases(graph, serviceId3, aliases);
3851
+ addAliases(graph, serviceId4, aliases);
3546
3852
  }
3547
3853
  }
3548
3854
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -3655,16 +3961,28 @@ init_cjs_shims();
3655
3961
  var import_node_fs10 = require("fs");
3656
3962
  var import_node_path10 = __toESM(require("path"), 1);
3657
3963
  var import_types7 = require("@neat.is/types");
3964
+ function buildServiceHostIndex(services) {
3965
+ const knownHosts = /* @__PURE__ */ new Set();
3966
+ const hostToNodeId = /* @__PURE__ */ new Map();
3967
+ for (const service of services) {
3968
+ const base = import_node_path10.default.basename(service.dir);
3969
+ knownHosts.add(base);
3970
+ knownHosts.add(service.pkg.name);
3971
+ hostToNodeId.set(base, service.node.id);
3972
+ hostToNodeId.set(service.pkg.name, service.node.id);
3973
+ }
3974
+ return { knownHosts, hostToNodeId };
3975
+ }
3658
3976
  async function walkSourceFiles(dir) {
3659
3977
  const out = [];
3660
- async function walk(current) {
3978
+ async function walk3(current) {
3661
3979
  const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3662
3980
  for (const entry2 of entries) {
3663
3981
  const full = import_node_path10.default.join(current, entry2.name);
3664
3982
  if (entry2.isDirectory()) {
3665
3983
  if (IGNORED_DIRS.has(entry2.name)) continue;
3666
3984
  if (await isPythonVenvDir(full)) continue;
3667
- await walk(full);
3985
+ await walk3(full);
3668
3986
  } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3669
3987
  // would attribute our instrumentation imports to the user's service.
3670
3988
  !isNeatAuthoredSourceFile(entry2.name)) {
@@ -3672,7 +3990,7 @@ async function walkSourceFiles(dir) {
3672
3990
  }
3673
3991
  }
3674
3992
  }
3675
- await walk(dir);
3993
+ await walk3(dir);
3676
3994
  return out;
3677
3995
  }
3678
3996
  async function loadSourceFiles(dir) {
@@ -4766,20 +5084,20 @@ var import_node_path21 = __toESM(require("path"), 1);
4766
5084
  var import_types10 = require("@neat.is/types");
4767
5085
  async function walkConfigFiles(dir) {
4768
5086
  const out = [];
4769
- async function walk(current) {
5087
+ async function walk3(current) {
4770
5088
  const entries = await import_node_fs14.promises.readdir(current, { withFileTypes: true });
4771
5089
  for (const entry2 of entries) {
4772
5090
  const full = import_node_path21.default.join(current, entry2.name);
4773
5091
  if (entry2.isDirectory()) {
4774
5092
  if (IGNORED_DIRS.has(entry2.name)) continue;
4775
5093
  if (await isPythonVenvDir(full)) continue;
4776
- await walk(full);
5094
+ await walk3(full);
4777
5095
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
4778
5096
  out.push(full);
4779
5097
  }
4780
5098
  }
4781
5099
  }
4782
- await walk(dir);
5100
+ await walk3(dir);
4783
5101
  return out;
4784
5102
  }
4785
5103
  async function addConfigNodes(graph, services, scanPath) {
@@ -4827,142 +5145,586 @@ async function addConfigNodes(graph, services, scanPath) {
4827
5145
  return { nodesAdded, edgesAdded };
4828
5146
  }
4829
5147
 
4830
- // src/extract/calls/index.ts
4831
- init_cjs_shims();
4832
- var import_types17 = require("@neat.is/types");
4833
-
4834
- // src/extract/calls/http.ts
5148
+ // src/extract/routes.ts
4835
5149
  init_cjs_shims();
4836
5150
  var import_node_path22 = __toESM(require("path"), 1);
4837
5151
  var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
4838
5152
  var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
4839
- var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
4840
5153
  var import_types11 = require("@neat.is/types");
4841
- var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
4842
- var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
4843
- function isInsideJsxExternalLink(node) {
4844
- let cursor = node.parent;
4845
- while (cursor) {
4846
- if (cursor.type === "jsx_attribute") {
4847
- let owner = cursor.parent;
4848
- while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
4849
- owner = owner.parent;
4850
- }
4851
- if (!owner) return false;
4852
- const tagNode = owner.namedChild(0);
4853
- const tagName = tagNode?.text ?? "";
4854
- const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
4855
- return JSX_EXTERNAL_LINK_TAGS.has(right);
4856
- }
4857
- cursor = cursor.parent;
4858
- }
5154
+ var PARSE_CHUNK2 = 16384;
5155
+ function parseSource2(parser, source) {
5156
+ return parser.parse(
5157
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5158
+ );
5159
+ }
5160
+ function makeJsParser2() {
5161
+ const p = new import_tree_sitter2.default();
5162
+ p.setLanguage(import_tree_sitter_javascript2.default);
5163
+ return p;
5164
+ }
5165
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5166
+ "get",
5167
+ "post",
5168
+ "put",
5169
+ "patch",
5170
+ "delete",
5171
+ "options",
5172
+ "head",
5173
+ "all"
5174
+ ]);
5175
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5176
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5177
+ function canonicalizeTemplate(raw) {
5178
+ let p = raw.split("?")[0].split("#")[0];
5179
+ if (!p.startsWith("/")) p = "/" + p;
5180
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
5181
+ return p;
5182
+ }
5183
+ function isDynamicSegment(seg) {
5184
+ if (seg.length === 0) return false;
5185
+ if (seg.includes(":")) return true;
5186
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
5187
+ if (/^\d+$/.test(seg)) return true;
5188
+ 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;
5189
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
4859
5190
  return false;
4860
5191
  }
4861
- function collectStringLiterals(node, out) {
4862
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
5192
+ function normalizePathTemplate(raw) {
5193
+ const canonical = canonicalizeTemplate(raw);
5194
+ const segments = canonical.split("/").filter((s) => s.length > 0);
5195
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
5196
+ return "/" + normalised.join("/");
5197
+ }
5198
+ function walk(node, visit) {
5199
+ visit(node);
4863
5200
  for (let i = 0; i < node.namedChildCount; i++) {
4864
5201
  const child = node.namedChild(i);
4865
- if (child) collectStringLiterals(child, out);
5202
+ if (child) walk(child, visit);
4866
5203
  }
4867
5204
  }
4868
- var PARSE_CHUNK2 = 16384;
4869
- function parseSource2(parser, source) {
4870
- return parser.parse(
4871
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
4872
- );
5205
+ function staticStringText(node) {
5206
+ if (node.type === "string") {
5207
+ for (let i = 0; i < node.namedChildCount; i++) {
5208
+ const child = node.namedChild(i);
5209
+ if (child?.type === "string_fragment") return child.text;
5210
+ }
5211
+ return "";
5212
+ }
5213
+ if (node.type === "template_string") {
5214
+ for (let i = 0; i < node.namedChildCount; i++) {
5215
+ if (node.namedChild(i)?.type === "template_substitution") return null;
5216
+ }
5217
+ const raw = node.text;
5218
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5219
+ }
5220
+ return null;
4873
5221
  }
4874
- function callsFromSource(source, parser, knownHosts) {
5222
+ function objectStringProp(objNode, key) {
5223
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5224
+ const pair = objNode.namedChild(i);
5225
+ if (!pair || pair.type !== "pair") continue;
5226
+ const k = pair.childForFieldName("key");
5227
+ if (!k) continue;
5228
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
5229
+ if (kText !== key) continue;
5230
+ const v = pair.childForFieldName("value");
5231
+ if (v) return staticStringText(v);
5232
+ }
5233
+ return null;
5234
+ }
5235
+ function fastifyRouteMethods(objNode) {
5236
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5237
+ const pair = objNode.namedChild(i);
5238
+ if (!pair || pair.type !== "pair") continue;
5239
+ const k = pair.childForFieldName("key");
5240
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
5241
+ if (kText !== "method") continue;
5242
+ const v = pair.childForFieldName("value");
5243
+ if (!v) return [];
5244
+ if (v.type === "string" || v.type === "template_string") {
5245
+ const s = staticStringText(v);
5246
+ return s ? [s.toUpperCase()] : [];
5247
+ }
5248
+ if (v.type === "array") {
5249
+ const out = [];
5250
+ for (let j = 0; j < v.namedChildCount; j++) {
5251
+ const el = v.namedChild(j);
5252
+ if (el && (el.type === "string" || el.type === "template_string")) {
5253
+ const s = staticStringText(el);
5254
+ if (s) out.push(s.toUpperCase());
5255
+ }
5256
+ }
5257
+ return out;
5258
+ }
5259
+ }
5260
+ return [];
5261
+ }
5262
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
4875
5263
  const tree = parseSource2(parser, source);
4876
- const literals = [];
4877
- collectStringLiterals(tree.rootNode, literals);
4878
5264
  const out = [];
4879
- for (const lit of literals) {
4880
- if (isInsideJsxExternalLink(lit.node)) continue;
4881
- for (const host of knownHosts) {
4882
- if (urlMatchesHost(lit.text, host)) {
4883
- out.push({ host, line: lit.node.startPosition.row + 1 });
5265
+ const framework = hasExpress ? "express" : "fastify";
5266
+ walk(tree.rootNode, (node) => {
5267
+ if (node.type !== "call_expression") return;
5268
+ const fn = node.childForFieldName("function");
5269
+ if (!fn || fn.type !== "member_expression") return;
5270
+ const prop = fn.childForFieldName("property");
5271
+ if (!prop) return;
5272
+ const method = prop.text.toLowerCase();
5273
+ const args = node.childForFieldName("arguments");
5274
+ const first = args?.namedChild(0);
5275
+ if (!first) return;
5276
+ const line = node.startPosition.row + 1;
5277
+ if (ROUTER_METHODS.has(method)) {
5278
+ const p = staticStringText(first);
5279
+ if (p && p.startsWith("/")) {
5280
+ out.push({
5281
+ method: method === "all" ? "ALL" : method.toUpperCase(),
5282
+ pathTemplate: canonicalizeTemplate(p),
5283
+ line,
5284
+ framework
5285
+ });
4884
5286
  }
5287
+ return;
4885
5288
  }
4886
- }
5289
+ if (method === "route" && hasFastify && first.type === "object") {
5290
+ const url = objectStringProp(first, "url");
5291
+ if (!url || !url.startsWith("/")) return;
5292
+ const methods = fastifyRouteMethods(first);
5293
+ const list = methods.length > 0 ? methods : ["ALL"];
5294
+ for (const m of list) {
5295
+ out.push({
5296
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5297
+ pathTemplate: canonicalizeTemplate(url),
5298
+ line,
5299
+ framework: "fastify"
5300
+ });
5301
+ }
5302
+ }
5303
+ });
4887
5304
  return out;
4888
5305
  }
4889
- function makeJsParser2() {
4890
- const p = new import_tree_sitter2.default();
4891
- p.setLanguage(import_tree_sitter_javascript2.default);
4892
- return p;
5306
+ function segmentsOf(relFile) {
5307
+ return toPosix2(relFile).split("/").filter((s) => s.length > 0);
5308
+ }
5309
+ function isNextAppRouteFile(relFile) {
5310
+ const segs = segmentsOf(relFile);
5311
+ if (!segs.includes("app")) return false;
5312
+ const base = segs[segs.length - 1] ?? "";
5313
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
5314
+ }
5315
+ function isNextPagesApiFile(relFile) {
5316
+ const segs = segmentsOf(relFile);
5317
+ const pagesIdx = segs.indexOf("pages");
5318
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
5319
+ const base = segs[segs.length - 1] ?? "";
5320
+ if (/^_(app|document|middleware)\./.test(base)) return false;
5321
+ return JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(base));
5322
+ }
5323
+ function nextSegment(seg) {
5324
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
5325
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
5326
+ if (catchAll) return ":" + catchAll[1];
5327
+ const dynamic = seg.match(/^\[(.+?)\]$/);
5328
+ if (dynamic) return ":" + dynamic[1];
5329
+ return seg;
5330
+ }
5331
+ function nextAppPathTemplate(relFile) {
5332
+ const segs = segmentsOf(relFile);
5333
+ const appIdx = segs.lastIndexOf("app");
5334
+ const between = segs.slice(appIdx + 1, segs.length - 1);
5335
+ const parts = [];
5336
+ for (const seg of between) {
5337
+ const mapped = nextSegment(seg);
5338
+ if (mapped !== null) parts.push(mapped);
5339
+ }
5340
+ return "/" + parts.join("/");
5341
+ }
5342
+ function nextPagesApiPathTemplate(relFile) {
5343
+ const segs = segmentsOf(relFile);
5344
+ const pagesIdx = segs.indexOf("pages");
5345
+ const rest = segs.slice(pagesIdx + 1);
5346
+ const parts = [];
5347
+ for (let i = 0; i < rest.length; i++) {
5348
+ let seg = rest[i];
5349
+ if (i === rest.length - 1) {
5350
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
5351
+ if (seg === "index") continue;
5352
+ }
5353
+ const mapped = nextSegment(seg);
5354
+ if (mapped !== null) parts.push(mapped);
5355
+ }
5356
+ return "/" + parts.join("/");
5357
+ }
5358
+ function nextAppMethods(root) {
5359
+ const out = [];
5360
+ walk(root, (node) => {
5361
+ if (node.type !== "export_statement") return;
5362
+ const decl = node.childForFieldName("declaration");
5363
+ if (!decl) return;
5364
+ const line = node.startPosition.row + 1;
5365
+ if (decl.type === "function_declaration") {
5366
+ const name = decl.childForFieldName("name")?.text;
5367
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5368
+ return;
5369
+ }
5370
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
5371
+ for (let i = 0; i < decl.namedChildCount; i++) {
5372
+ const d = decl.namedChild(i);
5373
+ if (d?.type !== "variable_declarator") continue;
5374
+ const name = d.childForFieldName("name")?.text;
5375
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5376
+ }
5377
+ }
5378
+ });
5379
+ return out;
4893
5380
  }
4894
- function makePyParser2() {
4895
- const p = new import_tree_sitter2.default();
4896
- p.setLanguage(import_tree_sitter_python2.default);
4897
- return p;
5381
+ function nextRoutesFromFile(source, relFile, parser) {
5382
+ if (isNextAppRouteFile(relFile)) {
5383
+ const tree = parseSource2(parser, source);
5384
+ const template = nextAppPathTemplate(relFile);
5385
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
5386
+ method,
5387
+ pathTemplate: canonicalizeTemplate(template),
5388
+ line,
5389
+ framework: "next"
5390
+ }));
5391
+ }
5392
+ if (isNextPagesApiFile(relFile)) {
5393
+ return [
5394
+ {
5395
+ method: "ALL",
5396
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
5397
+ line: 1,
5398
+ framework: "next"
5399
+ }
5400
+ ];
5401
+ }
5402
+ return [];
4898
5403
  }
4899
- async function addHttpCallEdges(graph, services) {
5404
+ async function addRoutes(graph, services) {
4900
5405
  const jsParser = makeJsParser2();
4901
- const pyParser = makePyParser2();
4902
- const knownHosts = /* @__PURE__ */ new Set();
4903
- const hostToNodeId = /* @__PURE__ */ new Map();
4904
- for (const service of services) {
4905
- knownHosts.add(import_node_path22.default.basename(service.dir));
4906
- knownHosts.add(service.pkg.name);
4907
- hostToNodeId.set(import_node_path22.default.basename(service.dir), service.node.id);
4908
- hostToNodeId.set(service.pkg.name, service.node.id);
4909
- }
4910
5406
  let nodesAdded = 0;
4911
5407
  let edgesAdded = 0;
4912
5408
  for (const service of services) {
5409
+ const deps = {
5410
+ ...service.pkg.dependencies ?? {},
5411
+ ...service.pkg.devDependencies ?? {}
5412
+ };
5413
+ const hasExpress = deps["express"] !== void 0;
5414
+ const hasFastify = deps["fastify"] !== void 0;
5415
+ const hasNext = deps["next"] !== void 0;
5416
+ if (!hasExpress && !hasFastify && !hasNext) continue;
4913
5417
  const files = await loadSourceFiles(service.dir);
4914
- const seen = /* @__PURE__ */ new Set();
4915
5418
  for (const file of files) {
4916
5419
  if (isTestPath(file.path)) continue;
4917
- const parser = import_node_path22.default.extname(file.path) === ".py" ? pyParser : jsParser;
4918
- let sites;
5420
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(file.path))) continue;
5421
+ const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
5422
+ let routes;
4919
5423
  try {
4920
- sites = callsFromSource(file.content, parser, knownHosts);
5424
+ if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5425
+ routes = nextRoutesFromFile(file.content, relFile, jsParser);
5426
+ } else if (hasExpress || hasFastify) {
5427
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
5428
+ } else {
5429
+ routes = [];
5430
+ }
4921
5431
  } catch (err) {
4922
- recordExtractionError("http call extraction", file.path, err);
5432
+ recordExtractionError("route extraction", file.path, err);
4923
5433
  continue;
4924
5434
  }
4925
- if (sites.length === 0) continue;
4926
- const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
4927
- for (const site of sites) {
4928
- const targetId = hostToNodeId.get(site.host);
4929
- if (!targetId || targetId === service.node.id) continue;
4930
- const dedupKey = `${relFile}|${targetId}`;
4931
- if (seen.has(dedupKey)) continue;
4932
- seen.add(dedupKey);
4933
- const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
4934
- const ev = {
4935
- file: relFile,
4936
- line: site.line,
4937
- snippet: snippet(file.content, site.line)
4938
- };
4939
- const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4940
- graph,
4941
- service.pkg.name,
5435
+ if (routes.length === 0) continue;
5436
+ for (const route of routes) {
5437
+ const rid = (0, import_types11.routeId)(service.pkg.name, route.method, route.pathTemplate);
5438
+ if (!graph.hasNode(rid)) {
5439
+ const node = {
5440
+ id: rid,
5441
+ type: import_types11.NodeType.RouteNode,
5442
+ name: `${route.method} ${route.pathTemplate}`,
5443
+ service: service.pkg.name,
5444
+ method: route.method,
5445
+ pathTemplate: route.pathTemplate,
5446
+ path: relFile,
5447
+ line: route.line,
5448
+ framework: route.framework,
5449
+ discoveredVia: "static"
5450
+ };
5451
+ graph.addNode(rid, node);
5452
+ nodesAdded++;
5453
+ }
5454
+ const containsId = (0, import_types11.extractedEdgeId)(service.node.id, rid, import_types11.EdgeType.CONTAINS);
5455
+ if (!graph.hasEdge(containsId)) {
5456
+ const edge = {
5457
+ id: containsId,
5458
+ source: service.node.id,
5459
+ target: rid,
5460
+ type: import_types11.EdgeType.CONTAINS,
5461
+ provenance: import_types11.Provenance.EXTRACTED,
5462
+ confidence: (0, import_types11.confidenceForExtracted)("structural"),
5463
+ evidence: {
5464
+ file: relFile,
5465
+ line: route.line,
5466
+ snippet: snippet(file.content, route.line)
5467
+ }
5468
+ };
5469
+ graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
5470
+ edgesAdded++;
5471
+ }
5472
+ }
5473
+ }
5474
+ }
5475
+ return { nodesAdded, edgesAdded };
5476
+ }
5477
+
5478
+ // src/extract/proto.ts
5479
+ init_cjs_shims();
5480
+ var import_node_fs15 = require("fs");
5481
+ var import_node_path23 = __toESM(require("path"), 1);
5482
+ var import_types12 = require("@neat.is/types");
5483
+ var PROTO_EXTENSION = ".proto";
5484
+ function packageOf(content) {
5485
+ const m = content.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/m);
5486
+ return m ? m[1] : null;
5487
+ }
5488
+ function lineAt(content, offset) {
5489
+ return content.slice(0, offset).split("\n").length;
5490
+ }
5491
+ function grpcMethodsFromProto(content, fqPackage) {
5492
+ const out = [];
5493
+ const serviceRe = /\bservice\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g;
5494
+ let sm;
5495
+ while ((sm = serviceRe.exec(content)) !== null) {
5496
+ const serviceName = sm[1];
5497
+ const rpcService = fqPackage ? `${fqPackage}.${serviceName}` : serviceName;
5498
+ const bodyStart = serviceRe.lastIndex;
5499
+ let depth = 1;
5500
+ let i = bodyStart;
5501
+ for (; i < content.length && depth > 0; i++) {
5502
+ const ch = content[i];
5503
+ if (ch === "{") depth++;
5504
+ else if (ch === "}") depth--;
5505
+ }
5506
+ const body = content.slice(bodyStart, i - 1);
5507
+ const rpcRe = /\brpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
5508
+ let rm;
5509
+ while ((rm = rpcRe.exec(body)) !== null) {
5510
+ const rpcMethod = rm[1];
5511
+ const line = lineAt(content, bodyStart + rm.index);
5512
+ out.push({ rpcService, rpcMethod, line });
5513
+ }
5514
+ serviceRe.lastIndex = i;
5515
+ }
5516
+ return out;
5517
+ }
5518
+ async function walkProtoFiles(dir) {
5519
+ const out = [];
5520
+ async function walk3(current) {
5521
+ const entries = await import_node_fs15.promises.readdir(current, { withFileTypes: true }).catch(() => []);
5522
+ for (const entry2 of entries) {
5523
+ const full = import_node_path23.default.join(current, entry2.name);
5524
+ if (entry2.isDirectory()) {
5525
+ if (IGNORED_DIRS.has(entry2.name)) continue;
5526
+ if (await isPythonVenvDir(full)) continue;
5527
+ await walk3(full);
5528
+ } else if (entry2.isFile() && import_node_path23.default.extname(entry2.name) === PROTO_EXTENSION) {
5529
+ out.push(full);
5530
+ }
5531
+ }
5532
+ }
5533
+ await walk3(dir);
5534
+ return out;
5535
+ }
5536
+ async function addGrpcMethods(graph, services) {
5537
+ let nodesAdded = 0;
5538
+ let edgesAdded = 0;
5539
+ for (const service of services) {
5540
+ const protoPaths = await walkProtoFiles(service.dir);
5541
+ for (const protoPath of protoPaths) {
5542
+ if (isTestPath(protoPath)) continue;
5543
+ const relFile = toPosix2(import_node_path23.default.relative(service.dir, protoPath));
5544
+ let content;
5545
+ try {
5546
+ content = await import_node_fs15.promises.readFile(protoPath, "utf8");
5547
+ } catch (err) {
5548
+ recordExtractionError("proto extraction", protoPath, err);
5549
+ continue;
5550
+ }
5551
+ let methods;
5552
+ try {
5553
+ methods = grpcMethodsFromProto(content, packageOf(content));
5554
+ } catch (err) {
5555
+ recordExtractionError("proto extraction", protoPath, err);
5556
+ continue;
5557
+ }
5558
+ if (methods.length === 0) continue;
5559
+ for (const method of methods) {
5560
+ const mid = (0, import_types12.grpcMethodId)(method.rpcService, method.rpcMethod);
5561
+ if (!graph.hasNode(mid)) {
5562
+ const node = {
5563
+ id: mid,
5564
+ type: import_types12.NodeType.GrpcMethodNode,
5565
+ name: `${method.rpcService}/${method.rpcMethod}`,
5566
+ rpcService: method.rpcService,
5567
+ rpcMethod: method.rpcMethod,
5568
+ path: relFile,
5569
+ line: method.line,
5570
+ discoveredVia: "static"
5571
+ };
5572
+ graph.addNode(mid, node);
5573
+ nodesAdded++;
5574
+ }
5575
+ const containsId = (0, import_types12.extractedEdgeId)(service.node.id, mid, import_types12.EdgeType.CONTAINS);
5576
+ if (!graph.hasEdge(containsId)) {
5577
+ const edge = {
5578
+ id: containsId,
5579
+ source: service.node.id,
5580
+ target: mid,
5581
+ type: import_types12.EdgeType.CONTAINS,
5582
+ provenance: import_types12.Provenance.EXTRACTED,
5583
+ confidence: (0, import_types12.confidenceForExtracted)("structural"),
5584
+ evidence: {
5585
+ file: relFile,
5586
+ line: method.line,
5587
+ snippet: snippet(content, method.line)
5588
+ }
5589
+ };
5590
+ graph.addEdgeWithKey(containsId, service.node.id, mid, edge);
5591
+ edgesAdded++;
5592
+ }
5593
+ }
5594
+ }
5595
+ }
5596
+ return { nodesAdded, edgesAdded };
5597
+ }
5598
+
5599
+ // src/extract/calls/index.ts
5600
+ init_cjs_shims();
5601
+ var import_types20 = require("@neat.is/types");
5602
+
5603
+ // src/extract/calls/http.ts
5604
+ init_cjs_shims();
5605
+ var import_node_path24 = __toESM(require("path"), 1);
5606
+ var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5607
+ var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5608
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5609
+ var import_types13 = require("@neat.is/types");
5610
+ var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
5611
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
5612
+ function isInsideJsxExternalLink(node) {
5613
+ let cursor = node.parent;
5614
+ while (cursor) {
5615
+ if (cursor.type === "jsx_attribute") {
5616
+ let owner = cursor.parent;
5617
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
5618
+ owner = owner.parent;
5619
+ }
5620
+ if (!owner) return false;
5621
+ const tagNode = owner.namedChild(0);
5622
+ const tagName = tagNode?.text ?? "";
5623
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
5624
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
5625
+ }
5626
+ cursor = cursor.parent;
5627
+ }
5628
+ return false;
5629
+ }
5630
+ function collectStringLiterals(node, out) {
5631
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
5632
+ for (let i = 0; i < node.namedChildCount; i++) {
5633
+ const child = node.namedChild(i);
5634
+ if (child) collectStringLiterals(child, out);
5635
+ }
5636
+ }
5637
+ var PARSE_CHUNK3 = 16384;
5638
+ function parseSource3(parser, source) {
5639
+ return parser.parse(
5640
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5641
+ );
5642
+ }
5643
+ function callsFromSource(source, parser, knownHosts) {
5644
+ const tree = parseSource3(parser, source);
5645
+ const literals = [];
5646
+ collectStringLiterals(tree.rootNode, literals);
5647
+ const out = [];
5648
+ for (const lit of literals) {
5649
+ if (isInsideJsxExternalLink(lit.node)) continue;
5650
+ for (const host of knownHosts) {
5651
+ if (urlMatchesHost(lit.text, host)) {
5652
+ out.push({ host, line: lit.node.startPosition.row + 1 });
5653
+ }
5654
+ }
5655
+ }
5656
+ return out;
5657
+ }
5658
+ function makeJsParser3() {
5659
+ const p = new import_tree_sitter3.default();
5660
+ p.setLanguage(import_tree_sitter_javascript3.default);
5661
+ return p;
5662
+ }
5663
+ function makePyParser2() {
5664
+ const p = new import_tree_sitter3.default();
5665
+ p.setLanguage(import_tree_sitter_python2.default);
5666
+ return p;
5667
+ }
5668
+ async function addHttpCallEdges(graph, services) {
5669
+ const jsParser = makeJsParser3();
5670
+ const pyParser = makePyParser2();
5671
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5672
+ let nodesAdded = 0;
5673
+ let edgesAdded = 0;
5674
+ for (const service of services) {
5675
+ const files = await loadSourceFiles(service.dir);
5676
+ const seen = /* @__PURE__ */ new Set();
5677
+ for (const file of files) {
5678
+ if (isTestPath(file.path)) continue;
5679
+ const parser = import_node_path24.default.extname(file.path) === ".py" ? pyParser : jsParser;
5680
+ let sites;
5681
+ try {
5682
+ sites = callsFromSource(file.content, parser, knownHosts);
5683
+ } catch (err) {
5684
+ recordExtractionError("http call extraction", file.path, err);
5685
+ continue;
5686
+ }
5687
+ if (sites.length === 0) continue;
5688
+ const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
5689
+ for (const site of sites) {
5690
+ const targetId = hostToNodeId.get(site.host);
5691
+ if (!targetId || targetId === service.node.id) continue;
5692
+ const dedupKey = `${relFile}|${targetId}`;
5693
+ if (seen.has(dedupKey)) continue;
5694
+ seen.add(dedupKey);
5695
+ const confidence = (0, import_types13.confidenceForExtracted)("url-literal-service-target");
5696
+ const ev = {
5697
+ file: relFile,
5698
+ line: site.line,
5699
+ snippet: snippet(file.content, site.line)
5700
+ };
5701
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5702
+ graph,
5703
+ service.pkg.name,
4942
5704
  service.node.id,
4943
5705
  relFile
4944
5706
  );
4945
5707
  nodesAdded += n;
4946
5708
  edgesAdded += e;
4947
- if (!(0, import_types11.passesExtractedFloor)(confidence)) {
5709
+ if (!(0, import_types13.passesExtractedFloor)(confidence)) {
4948
5710
  noteExtractedDropped({
4949
5711
  source: fileNodeId,
4950
5712
  target: targetId,
4951
- type: import_types11.EdgeType.CALLS,
5713
+ type: import_types13.EdgeType.CALLS,
4952
5714
  confidence,
4953
5715
  confidenceKind: "url-literal-service-target",
4954
5716
  evidence: ev
4955
5717
  });
4956
5718
  continue;
4957
5719
  }
4958
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types11.EdgeType.CALLS);
5720
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types13.EdgeType.CALLS);
4959
5721
  if (!graph.hasEdge(edgeId)) {
4960
5722
  const edge = {
4961
5723
  id: edgeId,
4962
5724
  source: fileNodeId,
4963
5725
  target: targetId,
4964
- type: import_types11.EdgeType.CALLS,
4965
- provenance: import_types11.Provenance.EXTRACTED,
5726
+ type: import_types13.EdgeType.CALLS,
5727
+ provenance: import_types13.Provenance.EXTRACTED,
4966
5728
  confidence,
4967
5729
  evidence: ev
4968
5730
  };
@@ -4975,10 +5737,279 @@ async function addHttpCallEdges(graph, services) {
4975
5737
  return { nodesAdded, edgesAdded };
4976
5738
  }
4977
5739
 
5740
+ // src/extract/calls/route-match.ts
5741
+ init_cjs_shims();
5742
+ var import_node_path25 = __toESM(require("path"), 1);
5743
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
5744
+ var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
5745
+ var import_types14 = require("@neat.is/types");
5746
+ var PARSE_CHUNK4 = 16384;
5747
+ function parseSource4(parser, source) {
5748
+ return parser.parse(
5749
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
5750
+ );
5751
+ }
5752
+ function makeJsParser4() {
5753
+ const p = new import_tree_sitter4.default();
5754
+ p.setLanguage(import_tree_sitter_javascript4.default);
5755
+ return p;
5756
+ }
5757
+ var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5758
+ var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
5759
+ function walk2(node, visit) {
5760
+ visit(node);
5761
+ for (let i = 0; i < node.namedChildCount; i++) {
5762
+ const child = node.namedChild(i);
5763
+ if (child) walk2(child, visit);
5764
+ }
5765
+ }
5766
+ function reconstructUrl(node) {
5767
+ if (node.type === "string") {
5768
+ for (let i = 0; i < node.namedChildCount; i++) {
5769
+ const child = node.namedChild(i);
5770
+ if (child?.type === "string_fragment") return child.text;
5771
+ }
5772
+ return "";
5773
+ }
5774
+ if (node.type === "template_string") {
5775
+ let out = "";
5776
+ for (let i = 0; i < node.namedChildCount; i++) {
5777
+ const child = node.namedChild(i);
5778
+ if (!child) continue;
5779
+ if (child.type === "string_fragment") out += child.text;
5780
+ else if (child.type === "template_substitution") out += ":param";
5781
+ }
5782
+ if (out.length === 0) {
5783
+ const raw = node.text;
5784
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5785
+ }
5786
+ return out;
5787
+ }
5788
+ return null;
5789
+ }
5790
+ function methodFromOptions(objNode) {
5791
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5792
+ const pair = objNode.namedChild(i);
5793
+ if (!pair || pair.type !== "pair") continue;
5794
+ const k = pair.childForFieldName("key");
5795
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5796
+ if (kText !== "method") continue;
5797
+ const v = pair.childForFieldName("value");
5798
+ if (v && (v.type === "string" || v.type === "template_string")) {
5799
+ const s = stringText(v);
5800
+ return s ? s.toUpperCase() : void 0;
5801
+ }
5802
+ }
5803
+ return void 0;
5804
+ }
5805
+ function stringText(node) {
5806
+ if (node.type === "string") {
5807
+ for (let i = 0; i < node.namedChildCount; i++) {
5808
+ const child = node.namedChild(i);
5809
+ if (child?.type === "string_fragment") return child.text;
5810
+ }
5811
+ return "";
5812
+ }
5813
+ return null;
5814
+ }
5815
+ function urlNodeFromConfig(objNode) {
5816
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5817
+ const pair = objNode.namedChild(i);
5818
+ if (!pair || pair.type !== "pair") continue;
5819
+ const k = pair.childForFieldName("key");
5820
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5821
+ if (kText === "url") return pair.childForFieldName("value");
5822
+ }
5823
+ return null;
5824
+ }
5825
+ function pathOf(urlStr) {
5826
+ try {
5827
+ const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
5828
+ const parsed = new URL(candidate);
5829
+ return parsed.pathname || "/";
5830
+ } catch {
5831
+ return null;
5832
+ }
5833
+ }
5834
+ function matchHost(urlStr, knownHosts) {
5835
+ for (const host of knownHosts) {
5836
+ if (urlMatchesHost(urlStr, host)) return host;
5837
+ }
5838
+ return null;
5839
+ }
5840
+ function clientCallSitesFromSource(source, parser, knownHosts) {
5841
+ const tree = parseSource4(parser, source);
5842
+ const out = [];
5843
+ const push = (urlNode, method, callNode) => {
5844
+ const urlStr = reconstructUrl(urlNode);
5845
+ if (!urlStr) return;
5846
+ const host = matchHost(urlStr, knownHosts);
5847
+ if (!host) return;
5848
+ const p = pathOf(urlStr);
5849
+ if (p === null) return;
5850
+ const line = callNode.startPosition.row + 1;
5851
+ out.push({
5852
+ host,
5853
+ method,
5854
+ pathTemplate: p,
5855
+ line,
5856
+ snippet: snippet(source, line)
5857
+ });
5858
+ };
5859
+ walk2(tree.rootNode, (node) => {
5860
+ if (node.type !== "call_expression") return;
5861
+ const fn = node.childForFieldName("function");
5862
+ if (!fn) return;
5863
+ const args = node.childForFieldName("arguments");
5864
+ const first = args?.namedChild(0);
5865
+ if (!first) return;
5866
+ const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
5867
+ if (fn.type === "identifier" && fnName === "fetch") {
5868
+ const opts = args?.namedChild(1);
5869
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5870
+ push(first, method, node);
5871
+ return;
5872
+ }
5873
+ if (fn.type === "identifier" && fnName === "axios") {
5874
+ if (first.type === "object") {
5875
+ const urlNode = urlNodeFromConfig(first);
5876
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5877
+ } else {
5878
+ const opts = args?.namedChild(1);
5879
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5880
+ push(first, method, node);
5881
+ }
5882
+ return;
5883
+ }
5884
+ if (fn.type === "member_expression") {
5885
+ const obj = fn.childForFieldName("object");
5886
+ const objName = obj?.text ?? "";
5887
+ if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
5888
+ if (fnName === "request" && first.type === "object") {
5889
+ const urlNode = urlNodeFromConfig(first);
5890
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5891
+ } else {
5892
+ push(first, fnName.toUpperCase(), node);
5893
+ }
5894
+ return;
5895
+ }
5896
+ if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
5897
+ const opts = args?.namedChild(1);
5898
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
5899
+ push(first, method, node);
5900
+ return;
5901
+ }
5902
+ }
5903
+ });
5904
+ return out;
5905
+ }
5906
+ function buildRouteIndex(graph) {
5907
+ const index = /* @__PURE__ */ new Map();
5908
+ graph.forEachNode((_id, attrs) => {
5909
+ const node = attrs;
5910
+ if (node.type !== import_types14.NodeType.RouteNode) return;
5911
+ const route = attrs;
5912
+ const owner = (0, import_types14.serviceId)(route.service);
5913
+ const entry2 = {
5914
+ method: route.method.toUpperCase(),
5915
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
5916
+ routeNodeId: route.id
5917
+ };
5918
+ const list = index.get(owner);
5919
+ if (list) list.push(entry2);
5920
+ else index.set(owner, [entry2]);
5921
+ });
5922
+ return index;
5923
+ }
5924
+ function findRoute(entries, method, normalizedPath) {
5925
+ return entries.find(
5926
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
5927
+ );
5928
+ }
5929
+ async function addRouteCallEdges(graph, services) {
5930
+ const jsParser = makeJsParser4();
5931
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5932
+ const routeIndex = buildRouteIndex(graph);
5933
+ if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
5934
+ let nodesAdded = 0;
5935
+ let edgesAdded = 0;
5936
+ for (const service of services) {
5937
+ const files = await loadSourceFiles(service.dir);
5938
+ const seen = /* @__PURE__ */ new Set();
5939
+ for (const file of files) {
5940
+ if (isTestPath(file.path)) continue;
5941
+ if (!JS_CLIENT_EXTENSIONS.has(import_node_path25.default.extname(file.path))) continue;
5942
+ let sites;
5943
+ try {
5944
+ sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
5945
+ } catch (err) {
5946
+ recordExtractionError("route-match call extraction", file.path, err);
5947
+ continue;
5948
+ }
5949
+ if (sites.length === 0) continue;
5950
+ const relFile = toPosix2(import_node_path25.default.relative(service.dir, file.path));
5951
+ for (const site of sites) {
5952
+ const serverServiceId = hostToNodeId.get(site.host);
5953
+ if (!serverServiceId || serverServiceId === service.node.id) continue;
5954
+ const entries = routeIndex.get(serverServiceId);
5955
+ if (!entries) continue;
5956
+ const normalizedPath = normalizePathTemplate(site.pathTemplate);
5957
+ const match = findRoute(entries, site.method, normalizedPath);
5958
+ if (!match) continue;
5959
+ const dedupKey = `${relFile}|${match.routeNodeId}`;
5960
+ if (seen.has(dedupKey)) continue;
5961
+ seen.add(dedupKey);
5962
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5963
+ graph,
5964
+ service.pkg.name,
5965
+ service.node.id,
5966
+ relFile
5967
+ );
5968
+ nodesAdded += n;
5969
+ edgesAdded += e;
5970
+ const confidence = (0, import_types14.confidenceForExtracted)("verified-call-site");
5971
+ const ev = {
5972
+ file: relFile,
5973
+ line: site.line,
5974
+ snippet: site.snippet,
5975
+ method: site.method ?? match.method,
5976
+ pathTemplate: site.pathTemplate
5977
+ };
5978
+ if (!(0, import_types14.passesExtractedFloor)(confidence)) {
5979
+ noteExtractedDropped({
5980
+ source: fileNodeId,
5981
+ target: match.routeNodeId,
5982
+ type: import_types14.EdgeType.CALLS,
5983
+ confidence,
5984
+ confidenceKind: "verified-call-site",
5985
+ evidence: ev
5986
+ });
5987
+ continue;
5988
+ }
5989
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, match.routeNodeId, import_types14.EdgeType.CALLS);
5990
+ if (!graph.hasEdge(edgeId)) {
5991
+ const edge = {
5992
+ id: edgeId,
5993
+ source: fileNodeId,
5994
+ target: match.routeNodeId,
5995
+ type: import_types14.EdgeType.CALLS,
5996
+ provenance: import_types14.Provenance.EXTRACTED,
5997
+ confidence,
5998
+ evidence: ev
5999
+ };
6000
+ graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
6001
+ edgesAdded++;
6002
+ }
6003
+ }
6004
+ }
6005
+ }
6006
+ return { nodesAdded, edgesAdded };
6007
+ }
6008
+
4978
6009
  // src/extract/calls/kafka.ts
4979
6010
  init_cjs_shims();
4980
- var import_node_path23 = __toESM(require("path"), 1);
4981
- var import_types12 = require("@neat.is/types");
6011
+ var import_node_path26 = __toESM(require("path"), 1);
6012
+ var import_types15 = require("@neat.is/types");
4982
6013
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
4983
6014
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
4984
6015
  function findAll(re, text) {
@@ -4999,7 +6030,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4999
6030
  seen.add(key);
5000
6031
  const line = lineOf(file.content, topic);
5001
6032
  out.push({
5002
- infraId: (0, import_types12.infraId)("kafka-topic", topic),
6033
+ infraId: (0, import_types15.infraId)("kafka-topic", topic),
5003
6034
  name: topic,
5004
6035
  kind: "kafka-topic",
5005
6036
  edgeType,
@@ -5008,7 +6039,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5008
6039
  // tier (ADR-066).
5009
6040
  confidenceKind: "verified-call-site",
5010
6041
  evidence: {
5011
- file: import_node_path23.default.relative(serviceDir, file.path),
6042
+ file: import_node_path26.default.relative(serviceDir, file.path),
5012
6043
  line,
5013
6044
  snippet: snippet(file.content, line)
5014
6045
  }
@@ -5021,8 +6052,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5021
6052
 
5022
6053
  // src/extract/calls/redis.ts
5023
6054
  init_cjs_shims();
5024
- var import_node_path24 = __toESM(require("path"), 1);
5025
- var import_types13 = require("@neat.is/types");
6055
+ var import_node_path27 = __toESM(require("path"), 1);
6056
+ var import_types16 = require("@neat.is/types");
5026
6057
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
5027
6058
  function redisEndpointsFromFile(file, serviceDir) {
5028
6059
  const out = [];
@@ -5035,7 +6066,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5035
6066
  seen.add(host);
5036
6067
  const line = lineOf(file.content, host);
5037
6068
  out.push({
5038
- infraId: (0, import_types13.infraId)("redis", host),
6069
+ infraId: (0, import_types16.infraId)("redis", host),
5039
6070
  name: host,
5040
6071
  kind: "redis",
5041
6072
  edgeType: "CALLS",
@@ -5044,7 +6075,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5044
6075
  // support tier (ADR-066).
5045
6076
  confidenceKind: "url-with-structural-support",
5046
6077
  evidence: {
5047
- file: import_node_path24.default.relative(serviceDir, file.path),
6078
+ file: import_node_path27.default.relative(serviceDir, file.path),
5048
6079
  line,
5049
6080
  snippet: snippet(file.content, line)
5050
6081
  }
@@ -5055,8 +6086,8 @@ function redisEndpointsFromFile(file, serviceDir) {
5055
6086
 
5056
6087
  // src/extract/calls/aws.ts
5057
6088
  init_cjs_shims();
5058
- var import_node_path25 = __toESM(require("path"), 1);
5059
- var import_types14 = require("@neat.is/types");
6089
+ var import_node_path28 = __toESM(require("path"), 1);
6090
+ var import_types17 = require("@neat.is/types");
5060
6091
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
5061
6092
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
5062
6093
  function hasMarker(text, markers) {
@@ -5080,7 +6111,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5080
6111
  seen.add(key);
5081
6112
  const line = lineOf(file.content, name);
5082
6113
  out.push({
5083
- infraId: (0, import_types14.infraId)(kind, name),
6114
+ infraId: (0, import_types17.infraId)(kind, name),
5084
6115
  name,
5085
6116
  kind,
5086
6117
  edgeType: "CALLS",
@@ -5089,7 +6120,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5089
6120
  // (ADR-066).
5090
6121
  confidenceKind: "verified-call-site",
5091
6122
  evidence: {
5092
- file: import_node_path25.default.relative(serviceDir, file.path),
6123
+ file: import_node_path28.default.relative(serviceDir, file.path),
5093
6124
  line,
5094
6125
  snippet: snippet(file.content, line)
5095
6126
  }
@@ -5114,8 +6145,8 @@ function awsEndpointsFromFile(file, serviceDir) {
5114
6145
 
5115
6146
  // src/extract/calls/grpc.ts
5116
6147
  init_cjs_shims();
5117
- var import_node_path26 = __toESM(require("path"), 1);
5118
- var import_types15 = require("@neat.is/types");
6148
+ var import_node_path29 = __toESM(require("path"), 1);
6149
+ var import_types18 = require("@neat.is/types");
5119
6150
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
5120
6151
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
5121
6152
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -5164,7 +6195,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5164
6195
  const { kind } = classified;
5165
6196
  const line = lineOf(file.content, m[0]);
5166
6197
  out.push({
5167
- infraId: (0, import_types15.infraId)(kind, name),
6198
+ infraId: (0, import_types18.infraId)(kind, name),
5168
6199
  name,
5169
6200
  kind,
5170
6201
  edgeType: "CALLS",
@@ -5173,7 +6204,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5173
6204
  // tier (ADR-066).
5174
6205
  confidenceKind: "verified-call-site",
5175
6206
  evidence: {
5176
- file: import_node_path26.default.relative(serviceDir, file.path),
6207
+ file: import_node_path29.default.relative(serviceDir, file.path),
5177
6208
  line,
5178
6209
  snippet: snippet(file.content, line)
5179
6210
  }
@@ -5184,8 +6215,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
5184
6215
 
5185
6216
  // src/extract/calls/supabase.ts
5186
6217
  init_cjs_shims();
5187
- var import_node_path27 = __toESM(require("path"), 1);
5188
- var import_types16 = require("@neat.is/types");
6218
+ var import_node_path30 = __toESM(require("path"), 1);
6219
+ var import_types19 = require("@neat.is/types");
5189
6220
  var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
5190
6221
  var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
5191
6222
  var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
@@ -5223,7 +6254,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5223
6254
  seen.add(name);
5224
6255
  const line = lineOf(file.content, m[0]);
5225
6256
  out.push({
5226
- infraId: (0, import_types16.infraId)("supabase", name),
6257
+ infraId: (0, import_types19.infraId)("supabase", name),
5227
6258
  name,
5228
6259
  kind: "supabase",
5229
6260
  edgeType: "CALLS",
@@ -5233,7 +6264,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5233
6264
  // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
5234
6265
  confidenceKind: "verified-call-site",
5235
6266
  evidence: {
5236
- file: import_node_path27.default.relative(serviceDir, file.path),
6267
+ file: import_node_path30.default.relative(serviceDir, file.path),
5237
6268
  line,
5238
6269
  snippet: snippet(file.content, line)
5239
6270
  }
@@ -5246,11 +6277,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5246
6277
  function edgeTypeFromEndpoint(ep) {
5247
6278
  switch (ep.edgeType) {
5248
6279
  case "PUBLISHES_TO":
5249
- return import_types17.EdgeType.PUBLISHES_TO;
6280
+ return import_types20.EdgeType.PUBLISHES_TO;
5250
6281
  case "CONSUMES_FROM":
5251
- return import_types17.EdgeType.CONSUMES_FROM;
6282
+ return import_types20.EdgeType.CONSUMES_FROM;
5252
6283
  default:
5253
- return import_types17.EdgeType.CALLS;
6284
+ return import_types20.EdgeType.CALLS;
5254
6285
  }
5255
6286
  }
5256
6287
  function isAwsKind(kind) {
@@ -5278,7 +6309,7 @@ async function addExternalEndpointEdges(graph, services) {
5278
6309
  if (!graph.hasNode(ep.infraId)) {
5279
6310
  const node = {
5280
6311
  id: ep.infraId,
5281
- type: import_types17.NodeType.InfraNode,
6312
+ type: import_types20.NodeType.InfraNode,
5282
6313
  name: ep.name,
5283
6314
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
5284
6315
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -5290,7 +6321,7 @@ async function addExternalEndpointEdges(graph, services) {
5290
6321
  nodesAdded++;
5291
6322
  }
5292
6323
  const edgeType = edgeTypeFromEndpoint(ep);
5293
- const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
6324
+ const confidence = (0, import_types20.confidenceForExtracted)(ep.confidenceKind);
5294
6325
  const relFile = toPosix2(ep.evidence.file);
5295
6326
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5296
6327
  graph,
@@ -5300,7 +6331,7 @@ async function addExternalEndpointEdges(graph, services) {
5300
6331
  );
5301
6332
  nodesAdded += n;
5302
6333
  edgesAdded += e;
5303
- if (!(0, import_types17.passesExtractedFloor)(confidence)) {
6334
+ if (!(0, import_types20.passesExtractedFloor)(confidence)) {
5304
6335
  noteExtractedDropped({
5305
6336
  source: fileNodeId,
5306
6337
  target: ep.infraId,
@@ -5320,7 +6351,7 @@ async function addExternalEndpointEdges(graph, services) {
5320
6351
  source: fileNodeId,
5321
6352
  target: ep.infraId,
5322
6353
  type: edgeType,
5323
- provenance: import_types17.Provenance.EXTRACTED,
6354
+ provenance: import_types20.Provenance.EXTRACTED,
5324
6355
  confidence,
5325
6356
  evidence: ep.evidence
5326
6357
  };
@@ -5334,9 +6365,10 @@ async function addExternalEndpointEdges(graph, services) {
5334
6365
  async function addCallEdges(graph, services) {
5335
6366
  const http = await addHttpCallEdges(graph, services);
5336
6367
  const ext = await addExternalEndpointEdges(graph, services);
6368
+ const routes = await addRouteCallEdges(graph, services);
5337
6369
  return {
5338
- nodesAdded: http.nodesAdded + ext.nodesAdded,
5339
- edgesAdded: http.edgesAdded + ext.edgesAdded
6370
+ nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
6371
+ edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
5340
6372
  };
5341
6373
  }
5342
6374
 
@@ -5345,16 +6377,16 @@ init_cjs_shims();
5345
6377
 
5346
6378
  // src/extract/infra/docker-compose.ts
5347
6379
  init_cjs_shims();
5348
- var import_node_path28 = __toESM(require("path"), 1);
5349
- var import_types19 = require("@neat.is/types");
6380
+ var import_node_path31 = __toESM(require("path"), 1);
6381
+ var import_types22 = require("@neat.is/types");
5350
6382
 
5351
6383
  // src/extract/infra/shared.ts
5352
6384
  init_cjs_shims();
5353
- var import_types18 = require("@neat.is/types");
6385
+ var import_types21 = require("@neat.is/types");
5354
6386
  function makeInfraNode(kind, name, provider = "self", extras) {
5355
6387
  return {
5356
- id: (0, import_types18.infraId)(kind, name),
5357
- type: import_types18.NodeType.InfraNode,
6388
+ id: (0, import_types21.infraId)(kind, name),
6389
+ type: import_types21.NodeType.InfraNode,
5358
6390
  name,
5359
6391
  provider,
5360
6392
  kind,
@@ -5383,7 +6415,7 @@ function dependsOnList(value) {
5383
6415
  }
5384
6416
  function serviceNameToServiceNode(name, services) {
5385
6417
  for (const s of services) {
5386
- if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
6418
+ if (s.node.name === name || import_node_path31.default.basename(s.dir) === name) return s.node.id;
5387
6419
  }
5388
6420
  return null;
5389
6421
  }
@@ -5392,7 +6424,7 @@ async function addComposeInfra(graph, scanPath, services) {
5392
6424
  let edgesAdded = 0;
5393
6425
  let composePath = null;
5394
6426
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5395
- const abs = import_node_path28.default.join(scanPath, name);
6427
+ const abs = import_node_path31.default.join(scanPath, name);
5396
6428
  if (await exists(abs)) {
5397
6429
  composePath = abs;
5398
6430
  break;
@@ -5405,13 +6437,13 @@ async function addComposeInfra(graph, scanPath, services) {
5405
6437
  } catch (err) {
5406
6438
  recordExtractionError(
5407
6439
  "infra docker-compose",
5408
- import_node_path28.default.relative(scanPath, composePath),
6440
+ import_node_path31.default.relative(scanPath, composePath),
5409
6441
  err
5410
6442
  );
5411
6443
  return { nodesAdded, edgesAdded };
5412
6444
  }
5413
6445
  if (!compose?.services) return { nodesAdded, edgesAdded };
5414
- const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
6446
+ const evidenceFile = import_node_path31.default.relative(scanPath, composePath).split(import_node_path31.default.sep).join("/");
5415
6447
  const composeNameToNodeId = /* @__PURE__ */ new Map();
5416
6448
  for (const [composeName, svc] of Object.entries(compose.services)) {
5417
6449
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -5433,15 +6465,15 @@ async function addComposeInfra(graph, scanPath, services) {
5433
6465
  for (const dep of dependsOnList(svc.depends_on)) {
5434
6466
  const targetId = composeNameToNodeId.get(dep);
5435
6467
  if (!targetId) continue;
5436
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
6468
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types22.EdgeType.DEPENDS_ON);
5437
6469
  if (graph.hasEdge(edgeId)) continue;
5438
6470
  const edge = {
5439
6471
  id: edgeId,
5440
6472
  source: sourceId,
5441
6473
  target: targetId,
5442
- type: import_types19.EdgeType.DEPENDS_ON,
5443
- provenance: import_types19.Provenance.EXTRACTED,
5444
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
6474
+ type: import_types22.EdgeType.DEPENDS_ON,
6475
+ provenance: import_types22.Provenance.EXTRACTED,
6476
+ confidence: (0, import_types22.confidenceForExtracted)("structural"),
5445
6477
  evidence: { file: evidenceFile }
5446
6478
  };
5447
6479
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5453,9 +6485,9 @@ async function addComposeInfra(graph, scanPath, services) {
5453
6485
 
5454
6486
  // src/extract/infra/dockerfile.ts
5455
6487
  init_cjs_shims();
5456
- var import_node_path29 = __toESM(require("path"), 1);
5457
- var import_node_fs15 = require("fs");
5458
- var import_types20 = require("@neat.is/types");
6488
+ var import_node_path32 = __toESM(require("path"), 1);
6489
+ var import_node_fs16 = require("fs");
6490
+ var import_types23 = require("@neat.is/types");
5459
6491
  function readDockerfile(content) {
5460
6492
  let image = null;
5461
6493
  const ports = [];
@@ -5484,15 +6516,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5484
6516
  let nodesAdded = 0;
5485
6517
  let edgesAdded = 0;
5486
6518
  for (const service of services) {
5487
- const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
6519
+ const dockerfilePath = import_node_path32.default.join(service.dir, "Dockerfile");
5488
6520
  if (!await exists(dockerfilePath)) continue;
5489
6521
  let content;
5490
6522
  try {
5491
- content = await import_node_fs15.promises.readFile(dockerfilePath, "utf8");
6523
+ content = await import_node_fs16.promises.readFile(dockerfilePath, "utf8");
5492
6524
  } catch (err) {
5493
6525
  recordExtractionError(
5494
6526
  "infra dockerfile",
5495
- import_node_path29.default.relative(scanPath, dockerfilePath),
6527
+ import_node_path32.default.relative(scanPath, dockerfilePath),
5496
6528
  err
5497
6529
  );
5498
6530
  continue;
@@ -5504,8 +6536,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5504
6536
  graph.addNode(node.id, node);
5505
6537
  nodesAdded++;
5506
6538
  }
5507
- const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5508
- const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
6539
+ const relDockerfile = toPosix2(import_node_path32.default.relative(service.dir, dockerfilePath));
6540
+ const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, dockerfilePath));
5509
6541
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5510
6542
  graph,
5511
6543
  service.pkg.name,
@@ -5514,15 +6546,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5514
6546
  );
5515
6547
  nodesAdded += fn;
5516
6548
  edgesAdded += fe;
5517
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
6549
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types23.EdgeType.RUNS_ON);
5518
6550
  if (!graph.hasEdge(edgeId)) {
5519
6551
  const edge = {
5520
6552
  id: edgeId,
5521
6553
  source: fileNodeId,
5522
6554
  target: node.id,
5523
- type: import_types20.EdgeType.RUNS_ON,
5524
- provenance: import_types20.Provenance.EXTRACTED,
5525
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6555
+ type: import_types23.EdgeType.RUNS_ON,
6556
+ provenance: import_types23.Provenance.EXTRACTED,
6557
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
5526
6558
  evidence: {
5527
6559
  file: evidenceFile,
5528
6560
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -5537,15 +6569,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5537
6569
  graph.addNode(portNode.id, portNode);
5538
6570
  nodesAdded++;
5539
6571
  }
5540
- const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
6572
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types23.EdgeType.CONNECTS_TO);
5541
6573
  if (graph.hasEdge(portEdgeId)) continue;
5542
6574
  const portEdge = {
5543
6575
  id: portEdgeId,
5544
6576
  source: fileNodeId,
5545
6577
  target: portNode.id,
5546
- type: import_types20.EdgeType.CONNECTS_TO,
5547
- provenance: import_types20.Provenance.EXTRACTED,
5548
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6578
+ type: import_types23.EdgeType.CONNECTS_TO,
6579
+ provenance: import_types23.Provenance.EXTRACTED,
6580
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
5549
6581
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5550
6582
  };
5551
6583
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -5557,23 +6589,23 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5557
6589
 
5558
6590
  // src/extract/infra/terraform.ts
5559
6591
  init_cjs_shims();
5560
- var import_node_fs16 = require("fs");
5561
- var import_node_path30 = __toESM(require("path"), 1);
5562
- var import_types21 = require("@neat.is/types");
6592
+ var import_node_fs17 = require("fs");
6593
+ var import_node_path33 = __toESM(require("path"), 1);
6594
+ var import_types24 = require("@neat.is/types");
5563
6595
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5564
6596
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5565
6597
  async function walkTfFiles(start, depth = 0, max = 5) {
5566
6598
  if (depth > max) return [];
5567
6599
  const out = [];
5568
- const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
6600
+ const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5569
6601
  for (const entry2 of entries) {
5570
6602
  if (entry2.isDirectory()) {
5571
6603
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
5572
- const child = import_node_path30.default.join(start, entry2.name);
6604
+ const child = import_node_path33.default.join(start, entry2.name);
5573
6605
  if (await isPythonVenvDir(child)) continue;
5574
6606
  out.push(...await walkTfFiles(child, depth + 1, max));
5575
6607
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
5576
- out.push(import_node_path30.default.join(start, entry2.name));
6608
+ out.push(import_node_path33.default.join(start, entry2.name));
5577
6609
  }
5578
6610
  }
5579
6611
  return out;
@@ -5592,7 +6624,7 @@ function blockBody(content, from) {
5592
6624
  }
5593
6625
  return null;
5594
6626
  }
5595
- function lineAt(content, index) {
6627
+ function lineAt2(content, index) {
5596
6628
  let line = 1;
5597
6629
  for (let i = 0; i < index && i < content.length; i++) {
5598
6630
  if (content[i] === "\n") line++;
@@ -5604,8 +6636,8 @@ async function addTerraformResources(graph, scanPath) {
5604
6636
  let edgesAdded = 0;
5605
6637
  const files = await walkTfFiles(scanPath);
5606
6638
  for (const file of files) {
5607
- const content = await import_node_fs16.promises.readFile(file, "utf8");
5608
- const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
6639
+ const content = await import_node_fs17.promises.readFile(file, "utf8");
6640
+ const evidenceFile = toPosix2(import_node_path33.default.relative(scanPath, file));
5609
6641
  const resources = [];
5610
6642
  const byKey = /* @__PURE__ */ new Map();
5611
6643
  RESOURCE_RE.lastIndex = 0;
@@ -5640,16 +6672,16 @@ async function addTerraformResources(graph, scanPath) {
5640
6672
  if (!target) continue;
5641
6673
  if (seen.has(target.nodeId)) continue;
5642
6674
  seen.add(target.nodeId);
5643
- const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
6675
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types24.EdgeType.DEPENDS_ON);
5644
6676
  if (graph.hasEdge(edgeId)) continue;
5645
- const line = lineAt(content, resource.bodyOffset + ref.index);
6677
+ const line = lineAt2(content, resource.bodyOffset + ref.index);
5646
6678
  const edge = {
5647
6679
  id: edgeId,
5648
6680
  source: resource.nodeId,
5649
6681
  target: target.nodeId,
5650
- type: import_types21.EdgeType.DEPENDS_ON,
5651
- provenance: import_types21.Provenance.EXTRACTED,
5652
- confidence: (0, import_types21.confidenceForExtracted)("structural"),
6682
+ type: import_types24.EdgeType.DEPENDS_ON,
6683
+ provenance: import_types24.Provenance.EXTRACTED,
6684
+ confidence: (0, import_types24.confidenceForExtracted)("structural"),
5653
6685
  evidence: { file: evidenceFile, line, snippet: key }
5654
6686
  };
5655
6687
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5662,8 +6694,8 @@ async function addTerraformResources(graph, scanPath) {
5662
6694
 
5663
6695
  // src/extract/infra/k8s.ts
5664
6696
  init_cjs_shims();
5665
- var import_node_fs17 = require("fs");
5666
- var import_node_path31 = __toESM(require("path"), 1);
6697
+ var import_node_fs18 = require("fs");
6698
+ var import_node_path34 = __toESM(require("path"), 1);
5667
6699
  var import_yaml3 = require("yaml");
5668
6700
  var K8S_KIND_TO_INFRA_KIND = {
5669
6701
  Service: "k8s-service",
@@ -5677,15 +6709,15 @@ var K8S_KIND_TO_INFRA_KIND = {
5677
6709
  async function walkYamlFiles2(start, depth = 0, max = 5) {
5678
6710
  if (depth > max) return [];
5679
6711
  const out = [];
5680
- const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
6712
+ const entries = await import_node_fs18.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5681
6713
  for (const entry2 of entries) {
5682
6714
  if (entry2.isDirectory()) {
5683
6715
  if (IGNORED_DIRS.has(entry2.name)) continue;
5684
- const child = import_node_path31.default.join(start, entry2.name);
6716
+ const child = import_node_path34.default.join(start, entry2.name);
5685
6717
  if (await isPythonVenvDir(child)) continue;
5686
6718
  out.push(...await walkYamlFiles2(child, depth + 1, max));
5687
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry2.name))) {
5688
- out.push(import_node_path31.default.join(start, entry2.name));
6719
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path34.default.extname(entry2.name))) {
6720
+ out.push(import_node_path34.default.join(start, entry2.name));
5689
6721
  }
5690
6722
  }
5691
6723
  return out;
@@ -5694,7 +6726,7 @@ async function addK8sResources(graph, scanPath) {
5694
6726
  let nodesAdded = 0;
5695
6727
  const files = await walkYamlFiles2(scanPath);
5696
6728
  for (const file of files) {
5697
- const content = await import_node_fs17.promises.readFile(file, "utf8");
6729
+ const content = await import_node_fs18.promises.readFile(file, "utf8");
5698
6730
  let docs;
5699
6731
  try {
5700
6732
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -5729,17 +6761,17 @@ async function addInfra(graph, scanPath, services) {
5729
6761
  }
5730
6762
 
5731
6763
  // src/extract/index.ts
5732
- var import_node_path33 = __toESM(require("path"), 1);
6764
+ var import_node_path36 = __toESM(require("path"), 1);
5733
6765
 
5734
6766
  // src/extract/retire.ts
5735
6767
  init_cjs_shims();
5736
- var import_node_fs18 = require("fs");
5737
- var import_node_path32 = __toESM(require("path"), 1);
5738
- var import_types22 = require("@neat.is/types");
6768
+ var import_node_fs19 = require("fs");
6769
+ var import_node_path35 = __toESM(require("path"), 1);
6770
+ var import_types25 = require("@neat.is/types");
5739
6771
  function dropOrphanedFileNodes(graph) {
5740
6772
  const orphans = [];
5741
6773
  graph.forEachNode((id, attrs) => {
5742
- if (attrs.type !== import_types22.NodeType.FileNode) return;
6774
+ if (attrs.type !== import_types25.NodeType.FileNode) return;
5743
6775
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5744
6776
  orphans.push(id);
5745
6777
  }
@@ -5752,14 +6784,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5752
6784
  const bases = [scanPath, ...serviceDirs];
5753
6785
  graph.forEachEdge((id, attrs) => {
5754
6786
  const edge = attrs;
5755
- if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
6787
+ if (edge.provenance !== import_types25.Provenance.EXTRACTED) return;
5756
6788
  const evidenceFile = edge.evidence?.file;
5757
6789
  if (!evidenceFile) return;
5758
- if (import_node_path32.default.isAbsolute(evidenceFile)) {
5759
- if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
6790
+ if (import_node_path35.default.isAbsolute(evidenceFile)) {
6791
+ if (!(0, import_node_fs19.existsSync)(evidenceFile)) toDrop.push(id);
5760
6792
  return;
5761
6793
  }
5762
- const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
6794
+ const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path35.default.join(base, evidenceFile)));
5763
6795
  if (!found) toDrop.push(id);
5764
6796
  });
5765
6797
  for (const id of toDrop) graph.dropEdge(id);
@@ -5778,6 +6810,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5778
6810
  const importGraph = await addImports(graph, services);
5779
6811
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
5780
6812
  const phase3 = await addConfigNodes(graph, services, scanPath);
6813
+ const routePhase = await addRoutes(graph, services);
6814
+ const grpcPhase = await addGrpcMethods(graph, services);
5781
6815
  const phase4 = await addCallEdges(graph, services);
5782
6816
  const phase5 = await addInfra(graph, scanPath, services);
5783
6817
  const ghostsRetired = retireExtractedEdgesByMissingFile(
@@ -5799,7 +6833,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5799
6833
  }
5800
6834
  const droppedEntries = drainDroppedExtracted();
5801
6835
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5802
- const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
6836
+ const rejectedPath = import_node_path36.default.join(import_node_path36.default.dirname(opts.errorsPath), "rejected.ndjson");
5803
6837
  try {
5804
6838
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5805
6839
  } catch (err) {
@@ -5809,8 +6843,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5809
6843
  }
5810
6844
  }
5811
6845
  const result = {
5812
- nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
5813
- edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
6846
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
6847
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
5814
6848
  frontiersPromoted,
5815
6849
  extractionErrors: errorEntries.length,
5816
6850
  errorEntries,
@@ -5833,9 +6867,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5833
6867
 
5834
6868
  // src/persist.ts
5835
6869
  init_cjs_shims();
5836
- var import_node_fs19 = require("fs");
5837
- var import_node_path34 = __toESM(require("path"), 1);
5838
- var import_types23 = require("@neat.is/types");
6870
+ var import_node_fs20 = require("fs");
6871
+ var import_node_path37 = __toESM(require("path"), 1);
6872
+ var import_types26 = require("@neat.is/types");
5839
6873
  var SCHEMA_VERSION = 4;
5840
6874
  function migrateV1ToV2(payload) {
5841
6875
  const nodes = payload.graph.nodes;
@@ -5857,12 +6891,12 @@ function migrateV2ToV3(payload) {
5857
6891
  for (const edge of edges) {
5858
6892
  const attrs = edge.attributes;
5859
6893
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5860
- attrs.provenance = import_types23.Provenance.OBSERVED;
6894
+ attrs.provenance = import_types26.Provenance.OBSERVED;
5861
6895
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5862
6896
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5863
6897
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5864
6898
  if (type && source && target) {
5865
- const newId = (0, import_types23.observedEdgeId)(source, target, type);
6899
+ const newId = (0, import_types26.observedEdgeId)(source, target, type);
5866
6900
  attrs.id = newId;
5867
6901
  if (edge.key) edge.key = newId;
5868
6902
  }
@@ -5871,7 +6905,7 @@ function migrateV2ToV3(payload) {
5871
6905
  return { ...payload, schemaVersion: 3 };
5872
6906
  }
5873
6907
  async function ensureDir(filePath) {
5874
- await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
6908
+ await import_node_fs20.promises.mkdir(import_node_path37.default.dirname(filePath), { recursive: true });
5875
6909
  }
5876
6910
  async function saveGraphToDisk(graph, outPath) {
5877
6911
  await ensureDir(outPath);
@@ -5881,13 +6915,13 @@ async function saveGraphToDisk(graph, outPath) {
5881
6915
  graph: graph.export()
5882
6916
  };
5883
6917
  const tmp = `${outPath}.tmp`;
5884
- await import_node_fs19.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
5885
- await import_node_fs19.promises.rename(tmp, outPath);
6918
+ await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
6919
+ await import_node_fs20.promises.rename(tmp, outPath);
5886
6920
  }
5887
6921
  async function loadGraphFromDisk(graph, outPath) {
5888
6922
  let raw;
5889
6923
  try {
5890
- raw = await import_node_fs19.promises.readFile(outPath, "utf8");
6924
+ raw = await import_node_fs20.promises.readFile(outPath, "utf8");
5891
6925
  } catch (err) {
5892
6926
  if (err.code === "ENOENT") return;
5893
6927
  throw err;
@@ -5952,23 +6986,23 @@ function startPersistLoop(graph, outPath, opts = {}) {
5952
6986
 
5953
6987
  // src/projects.ts
5954
6988
  init_cjs_shims();
5955
- var import_node_path35 = __toESM(require("path"), 1);
6989
+ var import_node_path38 = __toESM(require("path"), 1);
5956
6990
  function pathsForProject(project, baseDir) {
5957
6991
  if (project === DEFAULT_PROJECT) {
5958
6992
  return {
5959
- snapshotPath: import_node_path35.default.join(baseDir, "graph.json"),
5960
- errorsPath: import_node_path35.default.join(baseDir, "errors.ndjson"),
5961
- staleEventsPath: import_node_path35.default.join(baseDir, "stale-events.ndjson"),
5962
- embeddingsCachePath: import_node_path35.default.join(baseDir, "embeddings.json"),
5963
- policyViolationsPath: import_node_path35.default.join(baseDir, "policy-violations.ndjson")
6993
+ snapshotPath: import_node_path38.default.join(baseDir, "graph.json"),
6994
+ errorsPath: import_node_path38.default.join(baseDir, "errors.ndjson"),
6995
+ staleEventsPath: import_node_path38.default.join(baseDir, "stale-events.ndjson"),
6996
+ embeddingsCachePath: import_node_path38.default.join(baseDir, "embeddings.json"),
6997
+ policyViolationsPath: import_node_path38.default.join(baseDir, "policy-violations.ndjson")
5964
6998
  };
5965
6999
  }
5966
7000
  return {
5967
- snapshotPath: import_node_path35.default.join(baseDir, `${project}.json`),
5968
- errorsPath: import_node_path35.default.join(baseDir, `errors.${project}.ndjson`),
5969
- staleEventsPath: import_node_path35.default.join(baseDir, `stale-events.${project}.ndjson`),
5970
- embeddingsCachePath: import_node_path35.default.join(baseDir, `embeddings.${project}.json`),
5971
- policyViolationsPath: import_node_path35.default.join(baseDir, `policy-violations.${project}.ndjson`)
7001
+ snapshotPath: import_node_path38.default.join(baseDir, `${project}.json`),
7002
+ errorsPath: import_node_path38.default.join(baseDir, `errors.${project}.ndjson`),
7003
+ staleEventsPath: import_node_path38.default.join(baseDir, `stale-events.${project}.ndjson`),
7004
+ embeddingsCachePath: import_node_path38.default.join(baseDir, `embeddings.${project}.json`),
7005
+ policyViolationsPath: import_node_path38.default.join(baseDir, `policy-violations.${project}.ndjson`)
5972
7006
  };
5973
7007
  }
5974
7008
  var Projects = class {
@@ -6006,19 +7040,19 @@ var Projects = class {
6006
7040
  init_cjs_shims();
6007
7041
  var import_fastify = __toESM(require("fastify"), 1);
6008
7042
  var import_cors = __toESM(require("@fastify/cors"), 1);
6009
- var import_types26 = require("@neat.is/types");
7043
+ var import_types29 = require("@neat.is/types");
6010
7044
 
6011
7045
  // src/extend/index.ts
6012
7046
  init_cjs_shims();
6013
- var import_node_fs21 = require("fs");
6014
- var import_node_path37 = __toESM(require("path"), 1);
7047
+ var import_node_fs22 = require("fs");
7048
+ var import_node_path40 = __toESM(require("path"), 1);
6015
7049
  var import_node_os2 = __toESM(require("os"), 1);
6016
7050
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
6017
7051
 
6018
7052
  // src/installers/package-manager.ts
6019
7053
  init_cjs_shims();
6020
- var import_node_fs20 = require("fs");
6021
- var import_node_path36 = __toESM(require("path"), 1);
7054
+ var import_node_fs21 = require("fs");
7055
+ var import_node_path39 = __toESM(require("path"), 1);
6022
7056
  var import_node_child_process = require("child_process");
6023
7057
  var LOCKFILE_PRIORITY = [
6024
7058
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -6033,29 +7067,29 @@ var LOCKFILE_PRIORITY = [
6033
7067
  var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
6034
7068
  async function exists2(p) {
6035
7069
  try {
6036
- await import_node_fs20.promises.access(p);
7070
+ await import_node_fs21.promises.access(p);
6037
7071
  return true;
6038
7072
  } catch {
6039
7073
  return false;
6040
7074
  }
6041
7075
  }
6042
7076
  async function detectPackageManager(serviceDir) {
6043
- let dir = import_node_path36.default.resolve(serviceDir);
7077
+ let dir = import_node_path39.default.resolve(serviceDir);
6044
7078
  const stops = /* @__PURE__ */ new Set();
6045
7079
  for (let i = 0; i < 64; i++) {
6046
7080
  if (stops.has(dir)) break;
6047
7081
  stops.add(dir);
6048
7082
  for (const candidate of LOCKFILE_PRIORITY) {
6049
- const lockPath = import_node_path36.default.join(dir, candidate.lockfile);
7083
+ const lockPath = import_node_path39.default.join(dir, candidate.lockfile);
6050
7084
  if (await exists2(lockPath)) {
6051
7085
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
6052
7086
  }
6053
7087
  }
6054
- const parent = import_node_path36.default.dirname(dir);
7088
+ const parent = import_node_path39.default.dirname(dir);
6055
7089
  if (parent === dir) break;
6056
7090
  dir = parent;
6057
7091
  }
6058
- return { pm: "npm", cwd: import_node_path36.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7092
+ return { pm: "npm", cwd: import_node_path39.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
6059
7093
  }
6060
7094
  async function runPackageManagerInstall(cmd) {
6061
7095
  return new Promise((resolve) => {
@@ -6097,30 +7131,30 @@ ${err.message}`
6097
7131
  // src/extend/index.ts
6098
7132
  async function fileExists2(p) {
6099
7133
  try {
6100
- await import_node_fs21.promises.access(p);
7134
+ await import_node_fs22.promises.access(p);
6101
7135
  return true;
6102
7136
  } catch {
6103
7137
  return false;
6104
7138
  }
6105
7139
  }
6106
7140
  async function readPackageJson(scanPath) {
6107
- const pkgPath = import_node_path37.default.join(scanPath, "package.json");
6108
- const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
7141
+ const pkgPath = import_node_path40.default.join(scanPath, "package.json");
7142
+ const raw = await import_node_fs22.promises.readFile(pkgPath, "utf8");
6109
7143
  return JSON.parse(raw);
6110
7144
  }
6111
7145
  async function findHookFiles(scanPath) {
6112
- const entries = await import_node_fs21.promises.readdir(scanPath);
7146
+ const entries = await import_node_fs22.promises.readdir(scanPath);
6113
7147
  return entries.filter(
6114
7148
  (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
6115
7149
  ).sort();
6116
7150
  }
6117
7151
  function extendLogPath() {
6118
- return process.env.NEAT_EXTEND_LOG ?? import_node_path37.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
7152
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path40.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
6119
7153
  }
6120
7154
  async function appendExtendLog(entry2) {
6121
7155
  const logPath = extendLogPath();
6122
- await import_node_fs21.promises.mkdir(import_node_path37.default.dirname(logPath), { recursive: true });
6123
- await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
7156
+ await import_node_fs22.promises.mkdir(import_node_path40.default.dirname(logPath), { recursive: true });
7157
+ await import_node_fs22.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
6124
7158
  }
6125
7159
  function splicedContent(fileContent, snippet2) {
6126
7160
  if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
@@ -6178,7 +7212,7 @@ function lookupInstrumentation(library, installedVersion) {
6178
7212
  }
6179
7213
  async function describeProjectInstrumentation(ctx) {
6180
7214
  const hookFiles = await findHookFiles(ctx.scanPath);
6181
- const envNeat = await fileExists2(import_node_path37.default.join(ctx.scanPath, ".env.neat"));
7215
+ const envNeat = await fileExists2(import_node_path40.default.join(ctx.scanPath, ".env.neat"));
6182
7216
  const registryInstrPackages = new Set(
6183
7217
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
6184
7218
  );
@@ -6200,31 +7234,31 @@ async function applyExtension(ctx, args, options) {
6200
7234
  );
6201
7235
  }
6202
7236
  for (const file of hookFiles) {
6203
- const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
7237
+ const content = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
6204
7238
  if (content.includes(args.registration_snippet)) {
6205
7239
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
6206
7240
  }
6207
7241
  }
6208
7242
  const primaryFile = hookFiles[0];
6209
- const primaryPath = import_node_path37.default.join(ctx.scanPath, primaryFile);
7243
+ const primaryPath = import_node_path40.default.join(ctx.scanPath, primaryFile);
6210
7244
  const filesTouched = [];
6211
7245
  const depsAdded = [];
6212
- const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
7246
+ const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
6213
7247
  const pkg = await readPackageJson(ctx.scanPath);
6214
7248
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
6215
7249
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
6216
- await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7250
+ await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
6217
7251
  filesTouched.push("package.json");
6218
7252
  depsAdded.push(`${args.instrumentation_package}@${args.version}`);
6219
7253
  }
6220
- const hookContent = await import_node_fs21.promises.readFile(primaryPath, "utf8");
7254
+ const hookContent = await import_node_fs22.promises.readFile(primaryPath, "utf8");
6221
7255
  const patched = splicedContent(hookContent, args.registration_snippet);
6222
7256
  if (!patched) {
6223
7257
  throw new Error(
6224
7258
  `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
6225
7259
  );
6226
7260
  }
6227
- await import_node_fs21.promises.writeFile(primaryPath, patched, "utf8");
7261
+ await import_node_fs22.promises.writeFile(primaryPath, patched, "utf8");
6228
7262
  filesTouched.push(primaryFile);
6229
7263
  const cmd = await detectPackageManager(ctx.scanPath);
6230
7264
  const installer = options?.runInstall ?? runPackageManagerInstall;
@@ -6255,7 +7289,7 @@ async function dryRunExtension(ctx, args) {
6255
7289
  };
6256
7290
  }
6257
7291
  for (const file of hookFiles) {
6258
- const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
7292
+ const content = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
6259
7293
  if (content.includes(args.registration_snippet)) {
6260
7294
  return {
6261
7295
  library: args.library,
@@ -6277,7 +7311,7 @@ async function dryRunExtension(ctx, args) {
6277
7311
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
6278
7312
  filesTouched.push("package.json");
6279
7313
  }
6280
- const hookContent = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, primaryFile), "utf8");
7314
+ const hookContent = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, primaryFile), "utf8");
6281
7315
  const patched = splicedContent(hookContent, args.registration_snippet);
6282
7316
  if (patched) {
6283
7317
  filesTouched.push(primaryFile);
@@ -6292,28 +7326,28 @@ async function rollbackExtension(ctx, args) {
6292
7326
  if (!await fileExists2(logPath)) {
6293
7327
  return { undone: false, message: "no apply found for library" };
6294
7328
  }
6295
- const raw = await import_node_fs21.promises.readFile(logPath, "utf8");
7329
+ const raw = await import_node_fs22.promises.readFile(logPath, "utf8");
6296
7330
  const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
6297
7331
  const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
6298
7332
  if (!match) {
6299
7333
  return { undone: false, message: "no apply found for library" };
6300
7334
  }
6301
- const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
7335
+ const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
6302
7336
  if (await fileExists2(pkgPath)) {
6303
7337
  const pkg = await readPackageJson(ctx.scanPath);
6304
7338
  if (pkg.dependencies?.[match.instrumentation_package]) {
6305
7339
  const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
6306
7340
  pkg.dependencies = rest;
6307
- await import_node_fs21.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7341
+ await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
6308
7342
  }
6309
7343
  }
6310
7344
  const hookFiles = await findHookFiles(ctx.scanPath);
6311
7345
  for (const file of hookFiles) {
6312
- const filePath = import_node_path37.default.join(ctx.scanPath, file);
6313
- const content = await import_node_fs21.promises.readFile(filePath, "utf8");
7346
+ const filePath = import_node_path40.default.join(ctx.scanPath, file);
7347
+ const content = await import_node_fs22.promises.readFile(filePath, "utf8");
6314
7348
  if (content.includes(match.registration_snippet)) {
6315
7349
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
6316
- await import_node_fs21.promises.writeFile(filePath, filtered, "utf8");
7350
+ await import_node_fs22.promises.writeFile(filePath, filtered, "utf8");
6317
7351
  break;
6318
7352
  }
6319
7353
  }
@@ -6325,7 +7359,7 @@ async function rollbackExtension(ctx, args) {
6325
7359
 
6326
7360
  // src/divergences.ts
6327
7361
  init_cjs_shims();
6328
- var import_types24 = require("@neat.is/types");
7362
+ var import_types27 = require("@neat.is/types");
6329
7363
  function bucketKey(source, target, type) {
6330
7364
  return `${type}|${source}|${target}`;
6331
7365
  }
@@ -6333,22 +7367,22 @@ function bucketEdges(graph) {
6333
7367
  const buckets = /* @__PURE__ */ new Map();
6334
7368
  graph.forEachEdge((id, attrs) => {
6335
7369
  const e = attrs;
6336
- const parsed = (0, import_types24.parseEdgeId)(id);
7370
+ const parsed = (0, import_types27.parseEdgeId)(id);
6337
7371
  const provenance = parsed?.provenance ?? e.provenance;
6338
7372
  const key = bucketKey(e.source, e.target, e.type);
6339
7373
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
6340
7374
  switch (provenance) {
6341
- case import_types24.Provenance.EXTRACTED:
7375
+ case import_types27.Provenance.EXTRACTED:
6342
7376
  cur.extracted = e;
6343
7377
  break;
6344
- case import_types24.Provenance.OBSERVED:
7378
+ case import_types27.Provenance.OBSERVED:
6345
7379
  cur.observed = e;
6346
7380
  break;
6347
- case import_types24.Provenance.INFERRED:
7381
+ case import_types27.Provenance.INFERRED:
6348
7382
  cur.inferred = e;
6349
7383
  break;
6350
7384
  default:
6351
- if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
7385
+ if (e.provenance === import_types27.Provenance.STALE) cur.stale = e;
6352
7386
  }
6353
7387
  buckets.set(key, cur);
6354
7388
  });
@@ -6357,7 +7391,12 @@ function bucketEdges(graph) {
6357
7391
  function nodeIsFrontier(graph, nodeId) {
6358
7392
  if (!graph.hasNode(nodeId)) return false;
6359
7393
  const attrs = graph.getNodeAttributes(nodeId);
6360
- return attrs.type === import_types24.NodeType.FrontierNode;
7394
+ return attrs.type === import_types27.NodeType.FrontierNode;
7395
+ }
7396
+ function nodeIsWebsocketChannel(graph, nodeId) {
7397
+ if (!graph.hasNode(nodeId)) return false;
7398
+ const attrs = graph.getNodeAttributes(nodeId);
7399
+ return attrs.type === import_types27.NodeType.WebSocketChannelNode;
6361
7400
  }
6362
7401
  function clampConfidence(n) {
6363
7402
  if (!Number.isFinite(n)) return 0;
@@ -6377,14 +7416,14 @@ function gradedConfidence(edge) {
6377
7416
  return clampConfidence(confidenceForEdge(edge));
6378
7417
  }
6379
7418
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6380
- import_types24.EdgeType.CALLS,
6381
- import_types24.EdgeType.CONNECTS_TO,
6382
- import_types24.EdgeType.PUBLISHES_TO,
6383
- import_types24.EdgeType.CONSUMES_FROM
7419
+ import_types27.EdgeType.CALLS,
7420
+ import_types27.EdgeType.CONNECTS_TO,
7421
+ import_types27.EdgeType.PUBLISHES_TO,
7422
+ import_types27.EdgeType.CONSUMES_FROM
6384
7423
  ]);
6385
7424
  function detectMissingDivergences(graph, bucket) {
6386
7425
  const out = [];
6387
- if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
7426
+ if (bucket.type === import_types27.EdgeType.CONTAINS) return out;
6388
7427
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
6389
7428
  if (!nodeIsFrontier(graph, bucket.target)) {
6390
7429
  out.push({
@@ -6399,7 +7438,7 @@ function detectMissingDivergences(graph, bucket) {
6399
7438
  });
6400
7439
  }
6401
7440
  }
6402
- if (bucket.observed && !bucket.extracted) {
7441
+ if (bucket.observed && !bucket.extracted && !nodeIsWebsocketChannel(graph, bucket.target)) {
6403
7442
  out.push({
6404
7443
  type: "missing-extracted",
6405
7444
  source: bucket.source,
@@ -6425,7 +7464,7 @@ function declaredHostFor(svc) {
6425
7464
  function hasExtractedConfiguredBy(graph, svcId) {
6426
7465
  for (const edgeId of graph.outboundEdges(svcId)) {
6427
7466
  const e = graph.getEdgeAttributes(edgeId);
6428
- if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
7467
+ if (e.type === import_types27.EdgeType.CONFIGURED_BY && e.provenance === import_types27.Provenance.EXTRACTED) {
6429
7468
  return true;
6430
7469
  }
6431
7470
  }
@@ -6438,10 +7477,10 @@ function detectHostMismatch(graph, svcId, svc) {
6438
7477
  const out = [];
6439
7478
  for (const edgeId of graph.outboundEdges(svcId)) {
6440
7479
  const edge = graph.getEdgeAttributes(edgeId);
6441
- if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6442
- if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
7480
+ if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7481
+ if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
6443
7482
  const target = graph.getNodeAttributes(edge.target);
6444
- if (target.type !== import_types24.NodeType.DatabaseNode) continue;
7483
+ if (target.type !== import_types27.NodeType.DatabaseNode) continue;
6445
7484
  const observedHost = target.host?.trim();
6446
7485
  if (!observedHost) continue;
6447
7486
  if (observedHost === declaredHost) continue;
@@ -6463,10 +7502,10 @@ function detectCompatDivergences(graph, svcId, svc) {
6463
7502
  const deps = svc.dependencies ?? {};
6464
7503
  for (const edgeId of graph.outboundEdges(svcId)) {
6465
7504
  const edge = graph.getEdgeAttributes(edgeId);
6466
- if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6467
- if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
7505
+ if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
7506
+ if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
6468
7507
  const target = graph.getNodeAttributes(edge.target);
6469
- if (target.type !== import_types24.NodeType.DatabaseNode) continue;
7508
+ if (target.type !== import_types27.NodeType.DatabaseNode) continue;
6470
7509
  for (const pair of compatPairs()) {
6471
7510
  if (pair.engine !== target.engine) continue;
6472
7511
  const declared = deps[pair.driver];
@@ -6525,7 +7564,7 @@ function suppressHostMismatchHalves(all) {
6525
7564
  for (const d of all) {
6526
7565
  if (d.type !== "host-mismatch") continue;
6527
7566
  observedHalf.add(`${d.source}->${d.target}`);
6528
- declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
7567
+ declaredHalf.add((0, import_types27.databaseId)(d.extractedHost));
6529
7568
  }
6530
7569
  if (observedHalf.size === 0) return all;
6531
7570
  return all.filter((d) => {
@@ -6544,7 +7583,7 @@ function computeDivergences(graph, opts = {}) {
6544
7583
  }
6545
7584
  graph.forEachNode((nodeId, attrs) => {
6546
7585
  const n = attrs;
6547
- if (n.type !== import_types24.NodeType.ServiceNode) return;
7586
+ if (n.type !== import_types27.NodeType.ServiceNode) return;
6548
7587
  const svc = n;
6549
7588
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
6550
7589
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -6578,7 +7617,7 @@ function computeDivergences(graph, opts = {}) {
6578
7617
  if (a.source !== b.source) return a.source.localeCompare(b.source);
6579
7618
  return a.target.localeCompare(b.target);
6580
7619
  });
6581
- return import_types24.DivergenceResultSchema.parse({
7620
+ return import_types27.DivergenceResultSchema.parse({
6582
7621
  divergences: filtered,
6583
7622
  totalAffected: filtered.length,
6584
7623
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6587,7 +7626,7 @@ function computeDivergences(graph, opts = {}) {
6587
7626
 
6588
7627
  // src/diff.ts
6589
7628
  init_cjs_shims();
6590
- var import_node_fs22 = require("fs");
7629
+ var import_node_fs23 = require("fs");
6591
7630
  async function loadSnapshotForDiff(target) {
6592
7631
  if (/^https?:\/\//i.test(target)) {
6593
7632
  const res = await fetch(target);
@@ -6596,7 +7635,7 @@ async function loadSnapshotForDiff(target) {
6596
7635
  }
6597
7636
  return await res.json();
6598
7637
  }
6599
- const raw = await import_node_fs22.promises.readFile(target, "utf8");
7638
+ const raw = await import_node_fs23.promises.readFile(target, "utf8");
6600
7639
  return JSON.parse(raw);
6601
7640
  }
6602
7641
  function indexEntries(entries) {
@@ -6664,25 +7703,25 @@ function canonicalJson(value) {
6664
7703
 
6665
7704
  // src/registry.ts
6666
7705
  init_cjs_shims();
6667
- var import_node_fs23 = require("fs");
7706
+ var import_node_fs24 = require("fs");
6668
7707
  var import_node_os3 = __toESM(require("os"), 1);
6669
- var import_node_path38 = __toESM(require("path"), 1);
6670
- var import_types25 = require("@neat.is/types");
7708
+ var import_node_path41 = __toESM(require("path"), 1);
7709
+ var import_types28 = require("@neat.is/types");
6671
7710
  var LOCK_TIMEOUT_MS = 5e3;
6672
7711
  var LOCK_RETRY_MS = 50;
6673
7712
  function neatHome() {
6674
7713
  const override = process.env.NEAT_HOME;
6675
- if (override && override.length > 0) return import_node_path38.default.resolve(override);
6676
- return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
7714
+ if (override && override.length > 0) return import_node_path41.default.resolve(override);
7715
+ return import_node_path41.default.join(import_node_os3.default.homedir(), ".neat");
6677
7716
  }
6678
7717
  function registryPath() {
6679
- return import_node_path38.default.join(neatHome(), "projects.json");
7718
+ return import_node_path41.default.join(neatHome(), "projects.json");
6680
7719
  }
6681
7720
  function registryLockPath() {
6682
- return import_node_path38.default.join(neatHome(), "projects.json.lock");
7721
+ return import_node_path41.default.join(neatHome(), "projects.json.lock");
6683
7722
  }
6684
7723
  function daemonPidPath() {
6685
- return import_node_path38.default.join(neatHome(), "neatd.pid");
7724
+ return import_node_path41.default.join(neatHome(), "neatd.pid");
6686
7725
  }
6687
7726
  function isPidAliveDefault(pid) {
6688
7727
  try {
@@ -6694,7 +7733,7 @@ function isPidAliveDefault(pid) {
6694
7733
  }
6695
7734
  async function readPidFile(file) {
6696
7735
  try {
6697
- const raw = await import_node_fs23.promises.readFile(file, "utf8");
7736
+ const raw = await import_node_fs24.promises.readFile(file, "utf8");
6698
7737
  const pid = Number.parseInt(raw.trim(), 10);
6699
7738
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
6700
7739
  } catch {
@@ -6742,24 +7781,24 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
6742
7781
  }
6743
7782
  }
6744
7783
  async function writeAtomically(target, contents) {
6745
- await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
7784
+ await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(target), { recursive: true });
6746
7785
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
6747
- const fd = await import_node_fs23.promises.open(tmp, "w");
7786
+ const fd = await import_node_fs24.promises.open(tmp, "w");
6748
7787
  try {
6749
7788
  await fd.writeFile(contents, "utf8");
6750
7789
  await fd.sync();
6751
7790
  } finally {
6752
7791
  await fd.close();
6753
7792
  }
6754
- await import_node_fs23.promises.rename(tmp, target);
7793
+ await import_node_fs24.promises.rename(tmp, target);
6755
7794
  }
6756
7795
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6757
7796
  const deadline = Date.now() + timeoutMs;
6758
- await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
7797
+ await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(lockPath), { recursive: true });
6759
7798
  let probedHolder = false;
6760
7799
  while (true) {
6761
7800
  try {
6762
- const fd = await import_node_fs23.promises.open(lockPath, "wx");
7801
+ const fd = await import_node_fs24.promises.open(lockPath, "wx");
6763
7802
  try {
6764
7803
  await fd.writeFile(`${process.pid}
6765
7804
  `, "utf8");
@@ -6784,7 +7823,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
6784
7823
  }
6785
7824
  }
6786
7825
  async function releaseLock(lockPath) {
6787
- await import_node_fs23.promises.unlink(lockPath).catch(() => {
7826
+ await import_node_fs24.promises.unlink(lockPath).catch(() => {
6788
7827
  });
6789
7828
  }
6790
7829
  async function withLock(fn) {
@@ -6800,7 +7839,7 @@ async function readRegistry() {
6800
7839
  const file = registryPath();
6801
7840
  let raw;
6802
7841
  try {
6803
- raw = await import_node_fs23.promises.readFile(file, "utf8");
7842
+ raw = await import_node_fs24.promises.readFile(file, "utf8");
6804
7843
  } catch (err) {
6805
7844
  if (err.code === "ENOENT") {
6806
7845
  return { version: 1, projects: [] };
@@ -6808,10 +7847,10 @@ async function readRegistry() {
6808
7847
  throw err;
6809
7848
  }
6810
7849
  const parsed = JSON.parse(raw);
6811
- return import_types25.RegistryFileSchema.parse(parsed);
7850
+ return import_types28.RegistryFileSchema.parse(parsed);
6812
7851
  }
6813
7852
  async function writeRegistry(reg) {
6814
- const validated = import_types25.RegistryFileSchema.parse(reg);
7853
+ const validated = import_types28.RegistryFileSchema.parse(reg);
6815
7854
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6816
7855
  }
6817
7856
  async function getProject(name) {
@@ -6855,7 +7894,7 @@ function pruneTtlMs() {
6855
7894
  }
6856
7895
  async function statPathStatus(p) {
6857
7896
  try {
6858
- const stat = await import_node_fs23.promises.stat(p);
7897
+ const stat = await import_node_fs24.promises.stat(p);
6859
7898
  return stat.isDirectory() ? "present" : "unknown";
6860
7899
  } catch (err) {
6861
7900
  return err.code === "ENOENT" ? "gone" : "unknown";
@@ -7102,11 +8141,11 @@ function registerRoutes(scope, ctx) {
7102
8141
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7103
8142
  const parsed = [];
7104
8143
  for (const c of candidates) {
7105
- const r = import_types26.DivergenceTypeSchema.safeParse(c);
8144
+ const r = import_types29.DivergenceTypeSchema.safeParse(c);
7106
8145
  if (!r.success) {
7107
8146
  return reply.code(400).send({
7108
8147
  error: `unknown divergence type "${c}"`,
7109
- allowed: import_types26.DivergenceTypeSchema.options
8148
+ allowed: import_types29.DivergenceTypeSchema.options
7110
8149
  });
7111
8150
  }
7112
8151
  parsed.push(r.data);
@@ -7325,7 +8364,7 @@ function registerRoutes(scope, ctx) {
7325
8364
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
7326
8365
  let violations = await log.readAll();
7327
8366
  if (req2.query.severity) {
7328
- const sev = import_types26.PolicySeveritySchema.safeParse(req2.query.severity);
8367
+ const sev = import_types29.PolicySeveritySchema.safeParse(req2.query.severity);
7329
8368
  if (!sev.success) {
7330
8369
  return reply.code(400).send({
7331
8370
  error: "invalid severity",
@@ -7364,7 +8403,7 @@ function registerRoutes(scope, ctx) {
7364
8403
  scope.post("/policies/check", async (req2, reply) => {
7365
8404
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
7366
8405
  if (!proj) return;
7367
- const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
8406
+ const parsed = import_types29.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
7368
8407
  if (!parsed.success) {
7369
8408
  return reply.code(400).send({
7370
8409
  error: "invalid /policies/check body",
@@ -7622,12 +8661,91 @@ async function buildApi(opts) {
7622
8661
 
7623
8662
  // src/daemon.ts
7624
8663
  init_otel();
8664
+
8665
+ // src/connectors/index.ts
8666
+ init_cjs_shims();
8667
+ var NO_ENV = "unknown";
8668
+ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
8669
+ const signals = await connector.poll(ctx);
8670
+ let edgesCreated = 0;
8671
+ let edgesUpdated = 0;
8672
+ let unresolved = 0;
8673
+ for (const signal of signals) {
8674
+ const resolved = resolveTarget(signal, ctx);
8675
+ if (!resolved) {
8676
+ unresolved++;
8677
+ continue;
8678
+ }
8679
+ const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
8680
+ const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : void 0;
8681
+ const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
8682
+ const evidence = callSite ? {
8683
+ file: reconcileObservedRelPath(graph, resolved.serviceName, callSite.relPath),
8684
+ line: callSite.line
8685
+ } : void 0;
8686
+ const calls = Math.trunc(signal.callCount);
8687
+ if (calls < 1) continue;
8688
+ const errors = Math.min(Math.max(Math.trunc(signal.errorCount), 0), calls);
8689
+ let created = false;
8690
+ let ok = true;
8691
+ for (let i = 0; i < calls; i++) {
8692
+ const result = upsertObservedEdge(
8693
+ graph,
8694
+ resolved.edgeType,
8695
+ sourceId,
8696
+ resolved.targetNodeId,
8697
+ signal.lastObservedIso,
8698
+ i < errors,
8699
+ evidence
8700
+ );
8701
+ if (!result) {
8702
+ ok = false;
8703
+ break;
8704
+ }
8705
+ if (i === 0) created = result.created;
8706
+ }
8707
+ if (!ok) {
8708
+ unresolved++;
8709
+ continue;
8710
+ }
8711
+ if (created) edgesCreated++;
8712
+ else edgesUpdated++;
8713
+ }
8714
+ return { signalCount: signals.length, edgesCreated, edgesUpdated, unresolved };
8715
+ }
8716
+ var DEFAULT_POLL_INTERVAL_MS = 6e4;
8717
+ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options = {}) {
8718
+ let stopped = false;
8719
+ let since = ctx.since;
8720
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
8721
+ const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
8722
+ const tick = () => {
8723
+ if (stopped) return;
8724
+ void (async () => {
8725
+ const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
8726
+ try {
8727
+ await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
8728
+ since = tickStartedAt;
8729
+ } catch (err) {
8730
+ onError(err);
8731
+ }
8732
+ })();
8733
+ };
8734
+ const interval = setInterval(tick, intervalMs);
8735
+ if (typeof interval.unref === "function") interval.unref();
8736
+ return () => {
8737
+ stopped = true;
8738
+ clearInterval(interval);
8739
+ };
8740
+ }
8741
+
8742
+ // src/daemon.ts
7625
8743
  init_auth();
7626
8744
 
7627
8745
  // src/unrouted.ts
7628
8746
  init_cjs_shims();
7629
- var import_node_fs24 = require("fs");
7630
- var import_node_path41 = __toESM(require("path"), 1);
8747
+ var import_node_fs25 = require("fs");
8748
+ var import_node_path44 = __toESM(require("path"), 1);
7631
8749
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
7632
8750
  return {
7633
8751
  timestamp: now.toISOString(),
@@ -7637,34 +8755,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
7637
8755
  };
7638
8756
  }
7639
8757
  async function appendUnroutedSpan(neatHome3, record) {
7640
- const target = import_node_path41.default.join(neatHome3, "errors.ndjson");
7641
- await import_node_fs24.promises.mkdir(neatHome3, { recursive: true });
7642
- await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
8758
+ const target = import_node_path44.default.join(neatHome3, "errors.ndjson");
8759
+ await import_node_fs25.promises.mkdir(neatHome3, { recursive: true });
8760
+ await import_node_fs25.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
7643
8761
  }
7644
8762
  function unroutedErrorsPath(neatHome3) {
7645
- return import_node_path41.default.join(neatHome3, "errors.ndjson");
8763
+ return import_node_path44.default.join(neatHome3, "errors.ndjson");
7646
8764
  }
7647
8765
 
7648
8766
  // src/daemon.ts
7649
- var import_types27 = require("@neat.is/types");
8767
+ var import_types30 = require("@neat.is/types");
7650
8768
  function daemonJsonPath(scanPath) {
7651
- return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
8769
+ return import_node_path45.default.join(scanPath, "neat-out", "daemon.json");
7652
8770
  }
7653
8771
  function daemonsDiscoveryDir(home) {
7654
8772
  const base = home && home.length > 0 ? home : neatHomeFromEnv();
7655
- return import_node_path42.default.join(base, "daemons");
8773
+ return import_node_path45.default.join(base, "daemons");
7656
8774
  }
7657
8775
  function daemonDiscoveryPath(project, home) {
7658
- return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
8776
+ return import_node_path45.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
7659
8777
  }
7660
8778
  function sanitizeDiscoveryName(project) {
7661
8779
  return project.replace(/[^A-Za-z0-9._-]/g, "_");
7662
8780
  }
7663
8781
  function neatHomeFromEnv() {
7664
8782
  const env = process.env.NEAT_HOME;
7665
- if (env && env.length > 0) return import_node_path42.default.resolve(env);
8783
+ if (env && env.length > 0) return import_node_path45.default.resolve(env);
7666
8784
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7667
- return import_node_path42.default.join(home, ".neat");
8785
+ return import_node_path45.default.join(home, ".neat");
7668
8786
  }
7669
8787
  function resolveNeatVersion() {
7670
8788
  if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
@@ -7696,7 +8814,7 @@ async function clearDaemonRecord(record, home) {
7696
8814
  } catch {
7697
8815
  }
7698
8816
  try {
7699
- await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
8817
+ await import_node_fs26.promises.unlink(daemonDiscoveryPath(record.project, home));
7700
8818
  } catch {
7701
8819
  }
7702
8820
  }
@@ -7705,12 +8823,12 @@ function reconcileDaemonRecordSync(record, home) {
7705
8823
  const stopped = { ...record, status: "stopped" };
7706
8824
  const target = daemonJsonPath(record.projectPath);
7707
8825
  const tmp = `${target}.${process.pid}.tmp`;
7708
- (0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
7709
- (0, import_node_fs25.renameSync)(tmp, target);
8826
+ (0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
8827
+ (0, import_node_fs26.renameSync)(tmp, target);
7710
8828
  } catch {
7711
8829
  }
7712
8830
  try {
7713
- (0, import_node_fs25.unlinkSync)(daemonDiscoveryPath(record.project, home));
8831
+ (0, import_node_fs26.unlinkSync)(daemonDiscoveryPath(record.project, home));
7714
8832
  } catch {
7715
8833
  }
7716
8834
  }
@@ -7719,17 +8837,25 @@ function teardownSlot(slot) {
7719
8837
  slot.stopPersist();
7720
8838
  } catch {
7721
8839
  }
8840
+ try {
8841
+ slot.stopStaleness();
8842
+ } catch {
8843
+ }
8844
+ try {
8845
+ slot.stopConnectors();
8846
+ } catch {
8847
+ }
7722
8848
  try {
7723
8849
  slot.detachEvents();
7724
8850
  } catch {
7725
8851
  }
7726
8852
  }
7727
8853
  function neatHomeFor(opts) {
7728
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
8854
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path45.default.resolve(opts.neatHome);
7729
8855
  const env = process.env.NEAT_HOME;
7730
- if (env && env.length > 0) return import_node_path42.default.resolve(env);
8856
+ if (env && env.length > 0) return import_node_path45.default.resolve(env);
7731
8857
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7732
- return import_node_path42.default.join(home, ".neat");
8858
+ return import_node_path45.default.join(home, ".neat");
7733
8859
  }
7734
8860
  function routeSpanToProject(serviceName, projects) {
7735
8861
  if (!serviceName) return DEFAULT_PROJECT;
@@ -7773,13 +8899,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
7773
8899
  if (!serviceName) return true;
7774
8900
  if (serviceNameMatchesProject(serviceName, project)) return true;
7775
8901
  return graph.someNode(
7776
- (_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
8902
+ (_id, attrs) => attrs.type === import_types30.NodeType.ServiceNode && attrs.name === serviceName
7777
8903
  );
7778
8904
  }
7779
- async function bootstrapProject(entry2) {
7780
- const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
8905
+ async function bootstrapProject(entry2, connectors = []) {
8906
+ const paths = pathsForProject(entry2.name, import_node_path45.default.join(entry2.path, "neat-out"));
7781
8907
  try {
7782
- const stat = await import_node_fs25.promises.stat(entry2.path);
8908
+ const stat = await import_node_fs26.promises.stat(entry2.path);
7783
8909
  if (!stat.isDirectory()) {
7784
8910
  throw new Error(`registered path ${entry2.path} is not a directory`);
7785
8911
  }
@@ -7795,6 +8921,10 @@ async function bootstrapProject(entry2) {
7795
8921
  paths,
7796
8922
  stopPersist: () => {
7797
8923
  },
8924
+ stopStaleness: () => {
8925
+ },
8926
+ stopConnectors: () => {
8927
+ },
7798
8928
  detachEvents: () => {
7799
8929
  },
7800
8930
  status: "broken",
@@ -7809,6 +8939,22 @@ async function bootstrapProject(entry2) {
7809
8939
  try {
7810
8940
  await extractFromDirectory(graph, entry2.path);
7811
8941
  const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
8942
+ const stopStaleness = startStalenessLoop(graph, {
8943
+ staleEventsPath: paths.staleEventsPath,
8944
+ project: entry2.name
8945
+ });
8946
+ const stopFns = connectors.map(
8947
+ (registration) => startConnectorPollLoop(
8948
+ registration.connector,
8949
+ { projectDir: entry2.path, credentials: registration.credentials },
8950
+ graph,
8951
+ registration.resolveTarget,
8952
+ { intervalMs: registration.intervalMs }
8953
+ )
8954
+ );
8955
+ const stopConnectors = () => {
8956
+ for (const stop of stopFns) stop();
8957
+ };
7812
8958
  await touchLastSeen(entry2.name).catch(() => {
7813
8959
  });
7814
8960
  return {
@@ -7817,6 +8963,8 @@ async function bootstrapProject(entry2) {
7817
8963
  outPath,
7818
8964
  paths,
7819
8965
  stopPersist,
8966
+ stopStaleness,
8967
+ stopConnectors,
7820
8968
  detachEvents,
7821
8969
  status: "active"
7822
8970
  };
@@ -7873,7 +9021,7 @@ async function startDaemon(opts = {}) {
7873
9021
  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;
7874
9022
  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;
7875
9023
  const singleProject = projectArg;
7876
- const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
9024
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path45.default.resolve(projectPathArg) : null;
7877
9025
  if (singleProject && !singleProjectPath) {
7878
9026
  throw new Error(
7879
9027
  `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
@@ -7881,14 +9029,14 @@ async function startDaemon(opts = {}) {
7881
9029
  }
7882
9030
  if (!singleProject) {
7883
9031
  try {
7884
- await import_node_fs25.promises.access(regPath);
9032
+ await import_node_fs26.promises.access(regPath);
7885
9033
  } catch {
7886
9034
  throw new Error(
7887
9035
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
7888
9036
  );
7889
9037
  }
7890
9038
  }
7891
- const pidPath = import_node_path42.default.join(home, "neatd.pid");
9039
+ const pidPath = import_node_path45.default.join(home, "neatd.pid");
7892
9040
  await writeAtomically(pidPath, `${process.pid}
7893
9041
  `);
7894
9042
  const slots = /* @__PURE__ */ new Map();
@@ -7932,7 +9080,7 @@ async function startDaemon(opts = {}) {
7932
9080
  }
7933
9081
  async function tryRecoverSlot(entry2) {
7934
9082
  try {
7935
- const fresh = await bootstrapProject(entry2);
9083
+ const fresh = await bootstrapProject(entry2, opts.connectors ?? []);
7936
9084
  const prior = slots.get(entry2.name);
7937
9085
  if (prior) teardownSlot(prior);
7938
9086
  slots.set(entry2.name, fresh);
@@ -7956,7 +9104,7 @@ async function startDaemon(opts = {}) {
7956
9104
  bootstrapStatus.set(entry2.name, "bootstrapping");
7957
9105
  bootstrapStartedAt.set(entry2.name, Date.now());
7958
9106
  try {
7959
- const slot = await bootstrapProject(entry2);
9107
+ const slot = await bootstrapProject(entry2, opts.connectors ?? []);
7960
9108
  const prior = slots.get(entry2.name);
7961
9109
  if (prior) teardownSlot(prior);
7962
9110
  slots.set(entry2.name, slot);
@@ -8082,7 +9230,7 @@ async function startDaemon(opts = {}) {
8082
9230
  }
8083
9231
  if (restApp) await restApp.close().catch(() => {
8084
9232
  });
8085
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9233
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8086
9234
  });
8087
9235
  throw new Error(
8088
9236
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -8208,7 +9356,7 @@ async function startDaemon(opts = {}) {
8208
9356
  });
8209
9357
  if (otlpApp) await otlpApp.close().catch(() => {
8210
9358
  });
8211
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9359
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8212
9360
  });
8213
9361
  throw new Error(
8214
9362
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -8243,7 +9391,7 @@ async function startDaemon(opts = {}) {
8243
9391
  });
8244
9392
  if (otlpApp) await otlpApp.close().catch(() => {
8245
9393
  });
8246
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9394
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8247
9395
  });
8248
9396
  throw new Error(
8249
9397
  `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
@@ -8290,9 +9438,9 @@ async function startDaemon(opts = {}) {
8290
9438
  let registryWatcher = null;
8291
9439
  let reloadTimer = null;
8292
9440
  if (!singleProject) try {
8293
- const regDir = import_node_path42.default.dirname(regPath);
8294
- const regBase = import_node_path42.default.basename(regPath);
8295
- registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
9441
+ const regDir = import_node_path45.default.dirname(regPath);
9442
+ const regBase = import_node_path45.default.basename(regPath);
9443
+ registryWatcher = (0, import_node_fs26.watch)(regDir, (_eventType, filename) => {
8296
9444
  if (filename !== null && filename !== regBase) return;
8297
9445
  if (reloadTimer) clearTimeout(reloadTimer);
8298
9446
  reloadTimer = setTimeout(() => {
@@ -8341,7 +9489,7 @@ async function startDaemon(opts = {}) {
8341
9489
  if (daemonRecord) {
8342
9490
  await clearDaemonRecord(daemonRecord, home);
8343
9491
  }
8344
- await import_node_fs25.promises.unlink(pidPath).catch(() => {
9492
+ await import_node_fs26.promises.unlink(pidPath).catch(() => {
8345
9493
  });
8346
9494
  };
8347
9495
  return {
@@ -8364,9 +9512,9 @@ init_auth();
8364
9512
  // src/web-spawn.ts
8365
9513
  init_cjs_shims();
8366
9514
  var import_node_child_process2 = require("child_process");
8367
- var import_node_fs26 = require("fs");
9515
+ var import_node_fs27 = require("fs");
8368
9516
  var import_node_net = __toESM(require("net"), 1);
8369
- var import_node_path43 = __toESM(require("path"), 1);
9517
+ var import_node_path46 = __toESM(require("path"), 1);
8370
9518
  var DEFAULT_WEB_PORT = 6328;
8371
9519
  var DEFAULT_REST_PORT = 8080;
8372
9520
  function asValidPort(value) {
@@ -8375,11 +9523,11 @@ function asValidPort(value) {
8375
9523
  }
8376
9524
  function projectRoot() {
8377
9525
  const fromEnv = process.env.NEAT_SCAN_PATH;
8378
- return import_node_path43.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
9526
+ return import_node_path46.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
8379
9527
  }
8380
9528
  async function readDaemonPorts(root) {
8381
9529
  try {
8382
- const raw = await import_node_fs26.promises.readFile(import_node_path43.default.join(root, "neat-out", "daemon.json"), "utf8");
9530
+ const raw = await import_node_fs27.promises.readFile(import_node_path46.default.join(root, "neat-out", "daemon.json"), "utf8");
8383
9531
  const parsed = JSON.parse(raw);
8384
9532
  const ports = parsed?.ports ?? {};
8385
9533
  return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) };
@@ -8424,10 +9572,10 @@ function resolveWebPackageDir() {
8424
9572
  eval("require")
8425
9573
  );
8426
9574
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
8427
- return import_node_path43.default.dirname(pkgJsonPath);
9575
+ return import_node_path46.default.dirname(pkgJsonPath);
8428
9576
  }
8429
9577
  function resolveStandaloneServerEntry(webDir) {
8430
- return import_node_path43.default.join(webDir, ".next/standalone/packages/web/server.js");
9578
+ return import_node_path46.default.join(webDir, ".next/standalone/packages/web/server.js");
8431
9579
  }
8432
9580
  async function pickInternalPort() {
8433
9581
  return new Promise((resolve, reject) => {
@@ -8480,7 +9628,7 @@ async function spawnWebUI(restPort, opts = {}) {
8480
9628
  NEAT_API_URL: apiUrl
8481
9629
  };
8482
9630
  child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
8483
- cwd: import_node_path43.default.dirname(serverEntry),
9631
+ cwd: import_node_path46.default.dirname(serverEntry),
8484
9632
  env,
8485
9633
  stdio: ["ignore", "inherit", "inherit"],
8486
9634
  detached: false
@@ -8656,14 +9804,14 @@ function localVersion() {
8656
9804
  }
8657
9805
  function neatHome2() {
8658
9806
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
8659
- return import_node_path44.default.resolve(process.env.NEAT_HOME);
9807
+ return import_node_path47.default.resolve(process.env.NEAT_HOME);
8660
9808
  }
8661
9809
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8662
- return import_node_path44.default.join(home, ".neat");
9810
+ return import_node_path47.default.join(home, ".neat");
8663
9811
  }
8664
9812
  async function readPid() {
8665
9813
  try {
8666
- const raw = await import_node_fs27.promises.readFile(import_node_path44.default.join(neatHome2(), "neatd.pid"), "utf8");
9814
+ const raw = await import_node_fs28.promises.readFile(import_node_path47.default.join(neatHome2(), "neatd.pid"), "utf8");
8667
9815
  const n = Number.parseInt(raw.trim(), 10);
8668
9816
  return Number.isFinite(n) ? n : null;
8669
9817
  } catch {