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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -56,9 +56,9 @@ function mountBearerAuth(app, opts) {
56
56
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
57
57
  const publicRead = opts.publicRead === true;
58
58
  app.addHook("preHandler", (req, reply, done) => {
59
- const path43 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
59
+ const path45 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
60
  for (const suffix of suffixes) {
61
- if (path43 === suffix || path43.endsWith(suffix)) {
61
+ if (path45 === suffix || path45.endsWith(suffix)) {
62
62
  done();
63
63
  return;
64
64
  }
@@ -69,11 +69,13 @@ function mountBearerAuth(app, opts) {
69
69
  }
70
70
  const header = req.headers.authorization;
71
71
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
72
+ opts.onReject?.();
72
73
  void reply.code(401).send({ error: "unauthorized" });
73
74
  return;
74
75
  }
75
76
  const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
76
77
  if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
78
+ opts.onReject?.();
77
79
  void reply.code(401).send({ error: "unauthorized" });
78
80
  return;
79
81
  }
@@ -186,8 +188,8 @@ function reshapeGrpcRequest(req) {
186
188
  };
187
189
  }
188
190
  function resolveProtoRoot() {
189
- const here = import_node_path39.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
190
- return import_node_path39.default.resolve(here, "..", "proto");
191
+ const here = import_node_path41.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
192
+ return import_node_path41.default.resolve(here, "..", "proto");
191
193
  }
192
194
  function loadTraceService() {
193
195
  const protoRoot = resolveProtoRoot();
@@ -255,13 +257,13 @@ async function startOtelGrpcReceiver(opts) {
255
257
  })
256
258
  };
257
259
  }
258
- var import_node_url, import_node_path39, import_node_crypto2, grpc, protoLoader;
260
+ var import_node_url, import_node_path41, import_node_crypto2, grpc, protoLoader;
259
261
  var init_otel_grpc = __esm({
260
262
  "src/otel-grpc.ts"() {
261
263
  "use strict";
262
264
  init_cjs_shims();
263
265
  import_node_url = require("url");
264
- import_node_path39 = __toESM(require("path"), 1);
266
+ import_node_path41 = __toESM(require("path"), 1);
265
267
  import_node_crypto2 = require("crypto");
266
268
  grpc = __toESM(require("@grpc/grpc-js"), 1);
267
269
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -334,6 +336,13 @@ function pickEnv(spanAttrs, resourceAttrs) {
334
336
  }
335
337
  return ENV_FALLBACK;
336
338
  }
339
+ function messagingDestinationOf(attrs) {
340
+ for (const key of ["messaging.destination.name", "messaging.destination"]) {
341
+ const v = attrs[key];
342
+ if (typeof v === "string" && v.length > 0) return v;
343
+ }
344
+ return void 0;
345
+ }
337
346
  function parseOtlpRequest(body) {
338
347
  const out = [];
339
348
  for (const rs of body.resourceSpans ?? []) {
@@ -360,6 +369,10 @@ function parseOtlpRequest(body) {
360
369
  attributes: attrs,
361
370
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
362
371
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
372
+ messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
373
+ messagingDestination: messagingDestinationOf(attrs),
374
+ graphqlOperationName: typeof attrs["graphql.operation.name"] === "string" && attrs["graphql.operation.name"].length > 0 ? attrs["graphql.operation.name"] : void 0,
375
+ graphqlOperationType: typeof attrs["graphql.operation.type"] === "string" && attrs["graphql.operation.type"].length > 0 ? attrs["graphql.operation.type"] : void 0,
363
376
  statusCode: span.status?.code,
364
377
  errorMessage: span.status?.message,
365
378
  exception: extractExceptionFromEvents(span.events)
@@ -371,10 +384,10 @@ function parseOtlpRequest(body) {
371
384
  return out;
372
385
  }
373
386
  function loadProtoRoot() {
374
- const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
375
- const protoRoot = import_node_path40.default.resolve(here, "..", "proto");
387
+ const here = import_node_path42.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
388
+ const protoRoot = import_node_path42.default.resolve(here, "..", "proto");
376
389
  const root = new import_protobufjs.default.Root();
377
- root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
390
+ root.resolvePath = (_origin, target) => import_node_path42.default.resolve(protoRoot, target);
378
391
  root.loadSync(
379
392
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
380
393
  { keepCase: true }
@@ -419,7 +432,21 @@ async function buildOtelReceiver(opts) {
419
432
  logger: false,
420
433
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
421
434
  });
422
- mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
435
+ const REJECT_WARN_INTERVAL_MS = 6e4;
436
+ let lastRejectWarnAt = 0;
437
+ const warnRejectedOtlp = () => {
438
+ const now = Date.now();
439
+ if (now - lastRejectWarnAt < REJECT_WARN_INTERVAL_MS) return;
440
+ lastRejectWarnAt = now;
441
+ console.warn(
442
+ "[neatd] rejecting OTLP spans on /v1/traces \u2014 missing or invalid bearer token (set NEAT_OTEL_TOKEN on the instrumented app)"
443
+ );
444
+ };
445
+ mountBearerAuth(app, {
446
+ token: opts.authToken,
447
+ trustProxy: opts.trustProxy,
448
+ onReject: warnRejectedOtlp
449
+ });
423
450
  const queue = [];
424
451
  let draining = false;
425
452
  let drainPromise = Promise.resolve();
@@ -572,12 +599,12 @@ async function buildOtelReceiver(opts) {
572
599
  };
573
600
  return decorated;
574
601
  }
575
- var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
602
+ var import_node_path42, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
576
603
  var init_otel = __esm({
577
604
  "src/otel.ts"() {
578
605
  "use strict";
579
606
  init_cjs_shims();
580
- import_node_path40 = __toESM(require("path"), 1);
607
+ import_node_path42 = __toESM(require("path"), 1);
581
608
  import_node_url2 = require("url");
582
609
  import_fastify2 = __toESM(require("fastify"), 1);
583
610
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -593,7 +620,7 @@ var init_otel = __esm({
593
620
 
594
621
  // src/server.ts
595
622
  init_cjs_shims();
596
- var import_node_path42 = __toESM(require("path"), 1);
623
+ var import_node_path44 = __toESM(require("path"), 1);
597
624
 
598
625
  // src/graph.ts
599
626
  init_cjs_shims();
@@ -617,7 +644,7 @@ function getGraph(project = DEFAULT_PROJECT) {
617
644
  init_cjs_shims();
618
645
  var import_fastify = __toESM(require("fastify"), 1);
619
646
  var import_cors = __toESM(require("@fastify/cors"), 1);
620
- var import_types26 = require("@neat.is/types");
647
+ var import_types28 = require("@neat.is/types");
621
648
 
622
649
  // src/extend/index.ts
623
650
  init_cjs_shims();
@@ -1360,19 +1387,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1360
1387
  function longestIncomingWalk(graph, start, maxDepth) {
1361
1388
  let best = { path: [start], edges: [] };
1362
1389
  const visited = /* @__PURE__ */ new Set([start]);
1363
- function step(node, path43, edges) {
1364
- if (path43.length > best.path.length) {
1365
- best = { path: [...path43], edges: [...edges] };
1390
+ function step(node, path45, edges) {
1391
+ if (path45.length > best.path.length) {
1392
+ best = { path: [...path45], edges: [...edges] };
1366
1393
  }
1367
- if (path43.length - 1 >= maxDepth) return;
1394
+ if (path45.length - 1 >= maxDepth) return;
1368
1395
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1369
1396
  for (const [srcId, edge] of incoming) {
1370
1397
  if (visited.has(srcId)) continue;
1371
1398
  visited.add(srcId);
1372
- path43.push(srcId);
1399
+ path45.push(srcId);
1373
1400
  edges.push(edge);
1374
- step(srcId, path43, edges);
1375
- path43.pop();
1401
+ step(srcId, path45, edges);
1402
+ path45.pop();
1376
1403
  edges.pop();
1377
1404
  visited.delete(srcId);
1378
1405
  }
@@ -1380,14 +1407,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
1380
1407
  step(start, [start], []);
1381
1408
  return best;
1382
1409
  }
1383
- function databaseRootCauseShape(graph, origin, walk) {
1410
+ function databaseRootCauseShape(graph, origin, walk3) {
1384
1411
  const targetDb = origin;
1385
1412
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1386
1413
  if (candidatePairs.length === 0) return null;
1387
- for (const id of walk.path) {
1414
+ for (const id of walk3.path) {
1388
1415
  const owner = resolveOwningService(graph, id);
1389
1416
  if (!owner) continue;
1390
- const { id: serviceId3, svc } = owner;
1417
+ const { id: serviceId4, svc } = owner;
1391
1418
  const deps = svc.dependencies ?? {};
1392
1419
  for (const pair of candidatePairs) {
1393
1420
  const declared = deps[pair.driver];
@@ -1400,7 +1427,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1400
1427
  );
1401
1428
  if (!result.compatible) {
1402
1429
  return {
1403
- rootCauseNode: serviceId3,
1430
+ rootCauseNode: serviceId4,
1404
1431
  rootCauseReason: result.reason ?? "incompatible driver",
1405
1432
  ...result.minDriverVersion ? {
1406
1433
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1411,11 +1438,11 @@ function databaseRootCauseShape(graph, origin, walk) {
1411
1438
  }
1412
1439
  return null;
1413
1440
  }
1414
- function serviceRootCauseShape(graph, _origin, walk) {
1415
- for (const id of walk.path) {
1441
+ function serviceRootCauseShape(graph, _origin, walk3) {
1442
+ for (const id of walk3.path) {
1416
1443
  const owner = resolveOwningService(graph, id);
1417
1444
  if (!owner) continue;
1418
- const { id: serviceId3, svc } = owner;
1445
+ const { id: serviceId4, svc } = owner;
1419
1446
  const deps = svc.dependencies ?? {};
1420
1447
  const serviceNodeEngine = svc.nodeEngine;
1421
1448
  for (const constraint of nodeEngineConstraints()) {
@@ -1424,7 +1451,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1424
1451
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1425
1452
  if (!result.compatible && result.reason) {
1426
1453
  return {
1427
- rootCauseNode: serviceId3,
1454
+ rootCauseNode: serviceId4,
1428
1455
  rootCauseReason: result.reason,
1429
1456
  ...result.requiredNodeVersion ? {
1430
1457
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1439,7 +1466,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1439
1466
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1440
1467
  if (!result.compatible && result.reason) {
1441
1468
  return {
1442
- rootCauseNode: serviceId3,
1469
+ rootCauseNode: serviceId4,
1443
1470
  rootCauseReason: result.reason,
1444
1471
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1445
1472
  };
@@ -1448,10 +1475,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
1448
1475
  }
1449
1476
  return null;
1450
1477
  }
1451
- function fileRootCauseShape(graph, origin, walk) {
1478
+ function fileRootCauseShape(graph, origin, walk3) {
1452
1479
  const owner = resolveOwningService(graph, origin.id);
1453
1480
  if (!owner) return null;
1454
- return serviceRootCauseShape(graph, owner.svc, walk);
1481
+ return serviceRootCauseShape(graph, owner.svc, walk3);
1455
1482
  }
1456
1483
  var rootCauseShapes = {
1457
1484
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
@@ -1463,16 +1490,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1463
1490
  const origin = graph.getNodeAttributes(errorNodeId);
1464
1491
  const shape = rootCauseShapes[origin.type];
1465
1492
  if (shape) {
1466
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1467
- const match = shape(graph, origin, walk);
1493
+ const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1494
+ const match = shape(graph, origin, walk3);
1468
1495
  if (match) {
1469
1496
  const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1470
1497
  return import_types.RootCauseResultSchema.parse({
1471
1498
  rootCauseNode: match.rootCauseNode,
1472
1499
  rootCauseReason: reason,
1473
- traversalPath: walk.path,
1474
- edgeProvenances: walk.edges.map((e) => e.provenance),
1475
- confidence: confidenceFromMix(walk.edges),
1500
+ traversalPath: walk3.path,
1501
+ edgeProvenances: walk3.edges.map((e) => e.provenance),
1502
+ confidence: confidenceFromMix(walk3.edges),
1476
1503
  fixRecommendation: match.fixRecommendation
1477
1504
  });
1478
1505
  }
@@ -1530,9 +1557,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1530
1557
  function isFailingCallEdge(e) {
1531
1558
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1532
1559
  }
1533
- function callSourcesForService(graph, serviceId3) {
1534
- const ids = [serviceId3];
1535
- for (const edgeId of graph.outboundEdges(serviceId3)) {
1560
+ function callSourcesForService(graph, serviceId4) {
1561
+ const ids = [serviceId4];
1562
+ for (const edgeId of graph.outboundEdges(serviceId4)) {
1536
1563
  const e = graph.getEdgeAttributes(edgeId);
1537
1564
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1538
1565
  const tgt = graph.getNodeAttributes(e.target);
@@ -1549,9 +1576,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1549
1576
  }
1550
1577
  return id < curId;
1551
1578
  }
1552
- function dominantFailingCall(graph, serviceId3, visited) {
1579
+ function dominantFailingCall(graph, serviceId4, visited) {
1553
1580
  let best = null;
1554
- for (const src of callSourcesForService(graph, serviceId3)) {
1581
+ for (const src of callSourcesForService(graph, serviceId4)) {
1555
1582
  for (const edgeId of graph.outboundEdges(src)) {
1556
1583
  const e = graph.getEdgeAttributes(edgeId);
1557
1584
  if (!isFailingCallEdge(e)) continue;
@@ -1566,26 +1593,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
1566
1593
  return best;
1567
1594
  }
1568
1595
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1569
- const path43 = [originServiceId];
1596
+ const path45 = [originServiceId];
1570
1597
  const edges = [];
1571
1598
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1572
1599
  let current = originServiceId;
1573
1600
  for (let depth = 0; depth < maxDepth; depth++) {
1574
1601
  const hop = dominantFailingCall(graph, current, visited);
1575
1602
  if (!hop) break;
1576
- path43.push(hop.nextService);
1603
+ path45.push(hop.nextService);
1577
1604
  edges.push(hop.edge);
1578
1605
  visited.add(hop.nextService);
1579
1606
  current = hop.nextService;
1580
1607
  }
1581
1608
  if (edges.length === 0) return null;
1582
- return { path: path43, edges, culprit: current };
1609
+ return { path: path45, edges, culprit: current };
1583
1610
  }
1584
1611
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1585
1612
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1586
1613
  if (!chain) return null;
1587
1614
  const culprit = chain.culprit;
1588
- const path43 = [...chain.path];
1615
+ const path45 = [...chain.path];
1589
1616
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1590
1617
  const baseConfidence = confidenceFromMix(chain.edges);
1591
1618
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1593,14 +1620,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1593
1620
  if (loc) {
1594
1621
  let rootCauseNode = culprit;
1595
1622
  if (loc.fileNode) {
1596
- path43.push(loc.fileNode);
1623
+ path45.push(loc.fileNode);
1597
1624
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1598
1625
  rootCauseNode = loc.fileNode;
1599
1626
  }
1600
1627
  return import_types.RootCauseResultSchema.parse({
1601
1628
  rootCauseNode,
1602
1629
  rootCauseReason: loc.rootCauseReason,
1603
- traversalPath: path43,
1630
+ traversalPath: path45,
1604
1631
  edgeProvenances,
1605
1632
  confidence,
1606
1633
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1612,7 +1639,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1612
1639
  return import_types.RootCauseResultSchema.parse({
1613
1640
  rootCauseNode: culprit,
1614
1641
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1615
- traversalPath: path43,
1642
+ traversalPath: path45,
1616
1643
  edgeProvenances,
1617
1644
  confidence,
1618
1645
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2824,10 +2851,48 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2824
2851
  ]);
2825
2852
  var WIRE_SPAN_KIND_CLIENT = 3;
2826
2853
  var WIRE_SPAN_KIND_PRODUCER = 4;
2854
+ var WIRE_SPAN_KIND_CONSUMER = 5;
2827
2855
  function spanMintsObservedEdge(kind) {
2828
2856
  if (kind === void 0 || kind === 0) return true;
2829
2857
  return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
2830
2858
  }
2859
+ function spanMintsMessagingEdge(kind) {
2860
+ return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
2861
+ }
2862
+ function spanServesGraphqlOperation(kind) {
2863
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2864
+ }
2865
+ function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
2866
+ const id = (0, import_types4.graphqlOperationId)(serviceName, operationType, operationName);
2867
+ if (graph.hasNode(id)) return id;
2868
+ const node = {
2869
+ id,
2870
+ type: import_types4.NodeType.GraphQLOperationNode,
2871
+ name: operationName,
2872
+ service: serviceName,
2873
+ operationType: operationType.toLowerCase(),
2874
+ operationName,
2875
+ discoveredVia: "otel"
2876
+ };
2877
+ graph.addNode(id, node);
2878
+ return id;
2879
+ }
2880
+ function messagingDestinationKind(system) {
2881
+ return `${system}-topic`;
2882
+ }
2883
+ function ensureMessagingDestinationNode(graph, system, destination) {
2884
+ const id = (0, import_types4.infraId)(messagingDestinationKind(system), destination);
2885
+ if (graph.hasNode(id)) return id;
2886
+ const node = {
2887
+ id,
2888
+ type: import_types4.NodeType.InfraNode,
2889
+ name: destination,
2890
+ provider: "self",
2891
+ kind: messagingDestinationKind(system)
2892
+ };
2893
+ graph.addNode(id, node);
2894
+ return id;
2895
+ }
2831
2896
  var PARENT_SPAN_CACHE_SIZE = 1e4;
2832
2897
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
2833
2898
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -2921,6 +2986,21 @@ function ensureDatabaseNode(graph, host, engine) {
2921
2986
  graph.addNode(id, node);
2922
2987
  return id;
2923
2988
  }
2989
+ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
2990
+ const id = (0, import_types4.localDatabaseId)(serviceName, name);
2991
+ if (graph.hasNode(id)) return id;
2992
+ const node = {
2993
+ id,
2994
+ type: import_types4.NodeType.DatabaseNode,
2995
+ name,
2996
+ engine,
2997
+ engineVersion: "unknown",
2998
+ compatibleDrivers: [],
2999
+ discoveredVia: "otel"
3000
+ };
3001
+ graph.addNode(id, node);
3002
+ return id;
3003
+ }
2924
3004
  function ensureFrontierNode(graph, host, ts) {
2925
3005
  const id = frontierIdFor(host);
2926
3006
  if (graph.hasNode(id)) {
@@ -3147,10 +3227,21 @@ async function handleSpan(ctx, span) {
3147
3227
  let affectedNode = sourceId;
3148
3228
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
3149
3229
  if (span.dbSystem) {
3150
- const host = pickAddress(span);
3151
- if (mintsFromCallerSide && host) {
3152
- ensureDatabaseNode(ctx.graph, host, span.dbSystem);
3153
- const targetId = (0, import_types4.databaseId)(host);
3230
+ if (mintsFromCallerSide) {
3231
+ const host = pickAddress(span);
3232
+ let targetId;
3233
+ if (host) {
3234
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
3235
+ targetId = (0, import_types4.databaseId)(host);
3236
+ } else {
3237
+ const localName = span.dbName ?? span.dbSystem;
3238
+ targetId = ensureLocalDatabaseNode(
3239
+ ctx.graph,
3240
+ span.service,
3241
+ localName,
3242
+ span.dbSystem
3243
+ );
3244
+ }
3154
3245
  const result = upsertObservedEdge(
3155
3246
  ctx.graph,
3156
3247
  import_types4.EdgeType.CONNECTS_TO,
@@ -3162,6 +3253,40 @@ async function handleSpan(ctx, span) {
3162
3253
  );
3163
3254
  if (result) affectedNode = targetId;
3164
3255
  }
3256
+ } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
3257
+ const targetId = ensureMessagingDestinationNode(
3258
+ ctx.graph,
3259
+ span.messagingSystem,
3260
+ span.messagingDestination
3261
+ );
3262
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types4.EdgeType.CONSUMES_FROM : import_types4.EdgeType.PUBLISHES_TO;
3263
+ const result = upsertObservedEdge(
3264
+ ctx.graph,
3265
+ edgeType,
3266
+ observedSource(),
3267
+ targetId,
3268
+ ts,
3269
+ isError,
3270
+ callSiteEvidence
3271
+ );
3272
+ if (result) affectedNode = targetId;
3273
+ } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
3274
+ const targetId = ensureGraphqlOperationNode(
3275
+ ctx.graph,
3276
+ span.service,
3277
+ span.graphqlOperationType,
3278
+ span.graphqlOperationName
3279
+ );
3280
+ const result = upsertObservedEdge(
3281
+ ctx.graph,
3282
+ import_types4.EdgeType.CONTAINS,
3283
+ observedSource(),
3284
+ targetId,
3285
+ ts,
3286
+ isError,
3287
+ callSiteEvidence
3288
+ );
3289
+ if (result) affectedNode = targetId;
3165
3290
  } else {
3166
3291
  const host = pickAddress(span);
3167
3292
  let resolvedViaAddress = false;
@@ -3271,29 +3396,29 @@ function promoteFrontierNodes(graph, opts = {}) {
3271
3396
  toPromote.push({ frontierId: id, serviceId: target });
3272
3397
  });
3273
3398
  let promoted = 0;
3274
- for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
3399
+ for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
3275
3400
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
3276
3401
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
3277
3402
  if (!gate.allowed) {
3278
3403
  continue;
3279
3404
  }
3280
3405
  }
3281
- rewireFrontierEdges(graph, frontierId2, serviceId3);
3406
+ rewireFrontierEdges(graph, frontierId2, serviceId4);
3282
3407
  graph.dropNode(frontierId2);
3283
3408
  promoted++;
3284
3409
  }
3285
3410
  return promoted;
3286
3411
  }
3287
- function rewireFrontierEdges(graph, frontierId2, serviceId3) {
3412
+ function rewireFrontierEdges(graph, frontierId2, serviceId4) {
3288
3413
  const inbound = [...graph.inboundEdges(frontierId2)];
3289
3414
  const outbound = [...graph.outboundEdges(frontierId2)];
3290
3415
  for (const edgeId of inbound) {
3291
3416
  const edge = graph.getEdgeAttributes(edgeId);
3292
- rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
3417
+ rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
3293
3418
  }
3294
3419
  for (const edgeId of outbound) {
3295
3420
  const edge = graph.getEdgeAttributes(edgeId);
3296
- rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
3421
+ rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
3297
3422
  }
3298
3423
  }
3299
3424
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -4055,9 +4180,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
4055
4180
  "StatefulSet",
4056
4181
  "DaemonSet"
4057
4182
  ]);
4058
- function addAliases(graph, serviceId3, candidates) {
4059
- if (!graph.hasNode(serviceId3)) return;
4060
- const node = graph.getNodeAttributes(serviceId3);
4183
+ function addAliases(graph, serviceId4, candidates) {
4184
+ if (!graph.hasNode(serviceId4)) return;
4185
+ const node = graph.getNodeAttributes(serviceId4);
4061
4186
  if (node.type !== import_types7.NodeType.ServiceNode) return;
4062
4187
  const set = new Set(node.aliases ?? []);
4063
4188
  for (const c of candidates) {
@@ -4067,7 +4192,7 @@ function addAliases(graph, serviceId3, candidates) {
4067
4192
  }
4068
4193
  if (set.size === 0) return;
4069
4194
  const updated = { ...node, aliases: [...set].sort() };
4070
- graph.replaceNodeAttributes(serviceId3, updated);
4195
+ graph.replaceNodeAttributes(serviceId4, updated);
4071
4196
  }
4072
4197
  function indexServicesByName(services) {
4073
4198
  const map = /* @__PURE__ */ new Map();
@@ -4100,12 +4225,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
4100
4225
  }
4101
4226
  if (!compose?.services) return;
4102
4227
  for (const [composeName, svc] of Object.entries(compose.services)) {
4103
- const serviceId3 = serviceIndex.get(composeName);
4104
- if (!serviceId3) continue;
4228
+ const serviceId4 = serviceIndex.get(composeName);
4229
+ if (!serviceId4) continue;
4105
4230
  const aliases = /* @__PURE__ */ new Set([composeName]);
4106
4231
  if (svc.container_name) aliases.add(svc.container_name);
4107
4232
  if (svc.hostname) aliases.add(svc.hostname);
4108
- addAliases(graph, serviceId3, aliases);
4233
+ addAliases(graph, serviceId4, aliases);
4109
4234
  }
4110
4235
  }
4111
4236
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -4218,16 +4343,28 @@ init_cjs_shims();
4218
4343
  var import_node_fs12 = require("fs");
4219
4344
  var import_node_path12 = __toESM(require("path"), 1);
4220
4345
  var import_types8 = require("@neat.is/types");
4346
+ function buildServiceHostIndex(services) {
4347
+ const knownHosts = /* @__PURE__ */ new Set();
4348
+ const hostToNodeId = /* @__PURE__ */ new Map();
4349
+ for (const service of services) {
4350
+ const base = import_node_path12.default.basename(service.dir);
4351
+ knownHosts.add(base);
4352
+ knownHosts.add(service.pkg.name);
4353
+ hostToNodeId.set(base, service.node.id);
4354
+ hostToNodeId.set(service.pkg.name, service.node.id);
4355
+ }
4356
+ return { knownHosts, hostToNodeId };
4357
+ }
4221
4358
  async function walkSourceFiles(dir) {
4222
4359
  const out = [];
4223
- async function walk(current) {
4360
+ async function walk3(current) {
4224
4361
  const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
4225
4362
  for (const entry of entries) {
4226
4363
  const full = import_node_path12.default.join(current, entry.name);
4227
4364
  if (entry.isDirectory()) {
4228
4365
  if (IGNORED_DIRS.has(entry.name)) continue;
4229
4366
  if (await isPythonVenvDir(full)) continue;
4230
- await walk(full);
4367
+ await walk3(full);
4231
4368
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
4232
4369
  // would attribute our instrumentation imports to the user's service.
4233
4370
  !isNeatAuthoredSourceFile(entry.name)) {
@@ -4235,7 +4372,7 @@ async function walkSourceFiles(dir) {
4235
4372
  }
4236
4373
  }
4237
4374
  }
4238
- await walk(dir);
4375
+ await walk3(dir);
4239
4376
  return out;
4240
4377
  }
4241
4378
  async function loadSourceFiles(dir) {
@@ -5329,20 +5466,20 @@ var import_node_path23 = __toESM(require("path"), 1);
5329
5466
  var import_types11 = require("@neat.is/types");
5330
5467
  async function walkConfigFiles(dir) {
5331
5468
  const out = [];
5332
- async function walk(current) {
5469
+ async function walk3(current) {
5333
5470
  const entries = await import_node_fs16.promises.readdir(current, { withFileTypes: true });
5334
5471
  for (const entry of entries) {
5335
5472
  const full = import_node_path23.default.join(current, entry.name);
5336
5473
  if (entry.isDirectory()) {
5337
5474
  if (IGNORED_DIRS.has(entry.name)) continue;
5338
5475
  if (await isPythonVenvDir(full)) continue;
5339
- await walk(full);
5476
+ await walk3(full);
5340
5477
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
5341
5478
  out.push(full);
5342
5479
  }
5343
5480
  }
5344
5481
  }
5345
- await walk(dir);
5482
+ await walk3(dir);
5346
5483
  return out;
5347
5484
  }
5348
5485
  async function addConfigNodes(graph, services, scanPath) {
@@ -5390,17 +5527,347 @@ async function addConfigNodes(graph, services, scanPath) {
5390
5527
  return { nodesAdded, edgesAdded };
5391
5528
  }
5392
5529
 
5530
+ // src/extract/routes.ts
5531
+ init_cjs_shims();
5532
+ var import_node_path24 = __toESM(require("path"), 1);
5533
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5534
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5535
+ var import_types12 = require("@neat.is/types");
5536
+ var PARSE_CHUNK2 = 16384;
5537
+ function parseSource2(parser, source) {
5538
+ return parser.parse(
5539
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5540
+ );
5541
+ }
5542
+ function makeJsParser2() {
5543
+ const p = new import_tree_sitter2.default();
5544
+ p.setLanguage(import_tree_sitter_javascript2.default);
5545
+ return p;
5546
+ }
5547
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5548
+ "get",
5549
+ "post",
5550
+ "put",
5551
+ "patch",
5552
+ "delete",
5553
+ "options",
5554
+ "head",
5555
+ "all"
5556
+ ]);
5557
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5558
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5559
+ function canonicalizeTemplate(raw) {
5560
+ let p = raw.split("?")[0].split("#")[0];
5561
+ if (!p.startsWith("/")) p = "/" + p;
5562
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
5563
+ return p;
5564
+ }
5565
+ function isDynamicSegment(seg) {
5566
+ if (seg.length === 0) return false;
5567
+ if (seg.includes(":")) return true;
5568
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
5569
+ if (/^\d+$/.test(seg)) return true;
5570
+ 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;
5571
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
5572
+ return false;
5573
+ }
5574
+ function normalizePathTemplate(raw) {
5575
+ const canonical = canonicalizeTemplate(raw);
5576
+ const segments = canonical.split("/").filter((s) => s.length > 0);
5577
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
5578
+ return "/" + normalised.join("/");
5579
+ }
5580
+ function walk(node, visit) {
5581
+ visit(node);
5582
+ for (let i = 0; i < node.namedChildCount; i++) {
5583
+ const child = node.namedChild(i);
5584
+ if (child) walk(child, visit);
5585
+ }
5586
+ }
5587
+ function staticStringText(node) {
5588
+ if (node.type === "string") {
5589
+ for (let i = 0; i < node.namedChildCount; i++) {
5590
+ const child = node.namedChild(i);
5591
+ if (child?.type === "string_fragment") return child.text;
5592
+ }
5593
+ return "";
5594
+ }
5595
+ if (node.type === "template_string") {
5596
+ for (let i = 0; i < node.namedChildCount; i++) {
5597
+ if (node.namedChild(i)?.type === "template_substitution") return null;
5598
+ }
5599
+ const raw = node.text;
5600
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5601
+ }
5602
+ return null;
5603
+ }
5604
+ function objectStringProp(objNode, key) {
5605
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5606
+ const pair = objNode.namedChild(i);
5607
+ if (!pair || pair.type !== "pair") continue;
5608
+ const k = pair.childForFieldName("key");
5609
+ if (!k) continue;
5610
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
5611
+ if (kText !== key) continue;
5612
+ const v = pair.childForFieldName("value");
5613
+ if (v) return staticStringText(v);
5614
+ }
5615
+ return null;
5616
+ }
5617
+ function fastifyRouteMethods(objNode) {
5618
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5619
+ const pair = objNode.namedChild(i);
5620
+ if (!pair || pair.type !== "pair") continue;
5621
+ const k = pair.childForFieldName("key");
5622
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
5623
+ if (kText !== "method") continue;
5624
+ const v = pair.childForFieldName("value");
5625
+ if (!v) return [];
5626
+ if (v.type === "string" || v.type === "template_string") {
5627
+ const s = staticStringText(v);
5628
+ return s ? [s.toUpperCase()] : [];
5629
+ }
5630
+ if (v.type === "array") {
5631
+ const out = [];
5632
+ for (let j = 0; j < v.namedChildCount; j++) {
5633
+ const el = v.namedChild(j);
5634
+ if (el && (el.type === "string" || el.type === "template_string")) {
5635
+ const s = staticStringText(el);
5636
+ if (s) out.push(s.toUpperCase());
5637
+ }
5638
+ }
5639
+ return out;
5640
+ }
5641
+ }
5642
+ return [];
5643
+ }
5644
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
5645
+ const tree = parseSource2(parser, source);
5646
+ const out = [];
5647
+ const framework = hasExpress ? "express" : "fastify";
5648
+ walk(tree.rootNode, (node) => {
5649
+ if (node.type !== "call_expression") return;
5650
+ const fn = node.childForFieldName("function");
5651
+ if (!fn || fn.type !== "member_expression") return;
5652
+ const prop = fn.childForFieldName("property");
5653
+ if (!prop) return;
5654
+ const method = prop.text.toLowerCase();
5655
+ const args = node.childForFieldName("arguments");
5656
+ const first = args?.namedChild(0);
5657
+ if (!first) return;
5658
+ const line = node.startPosition.row + 1;
5659
+ if (ROUTER_METHODS.has(method)) {
5660
+ const p = staticStringText(first);
5661
+ if (p && p.startsWith("/")) {
5662
+ out.push({
5663
+ method: method === "all" ? "ALL" : method.toUpperCase(),
5664
+ pathTemplate: canonicalizeTemplate(p),
5665
+ line,
5666
+ framework
5667
+ });
5668
+ }
5669
+ return;
5670
+ }
5671
+ if (method === "route" && hasFastify && first.type === "object") {
5672
+ const url = objectStringProp(first, "url");
5673
+ if (!url || !url.startsWith("/")) return;
5674
+ const methods = fastifyRouteMethods(first);
5675
+ const list = methods.length > 0 ? methods : ["ALL"];
5676
+ for (const m of list) {
5677
+ out.push({
5678
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5679
+ pathTemplate: canonicalizeTemplate(url),
5680
+ line,
5681
+ framework: "fastify"
5682
+ });
5683
+ }
5684
+ }
5685
+ });
5686
+ return out;
5687
+ }
5688
+ function segmentsOf(relFile) {
5689
+ return toPosix2(relFile).split("/").filter((s) => s.length > 0);
5690
+ }
5691
+ function isNextAppRouteFile(relFile) {
5692
+ const segs = segmentsOf(relFile);
5693
+ if (!segs.includes("app")) return false;
5694
+ const base = segs[segs.length - 1] ?? "";
5695
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
5696
+ }
5697
+ function isNextPagesApiFile(relFile) {
5698
+ const segs = segmentsOf(relFile);
5699
+ const pagesIdx = segs.indexOf("pages");
5700
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
5701
+ const base = segs[segs.length - 1] ?? "";
5702
+ if (/^_(app|document|middleware)\./.test(base)) return false;
5703
+ return JS_ROUTE_EXTENSIONS.has(import_node_path24.default.extname(base));
5704
+ }
5705
+ function nextSegment(seg) {
5706
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
5707
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
5708
+ if (catchAll) return ":" + catchAll[1];
5709
+ const dynamic = seg.match(/^\[(.+?)\]$/);
5710
+ if (dynamic) return ":" + dynamic[1];
5711
+ return seg;
5712
+ }
5713
+ function nextAppPathTemplate(relFile) {
5714
+ const segs = segmentsOf(relFile);
5715
+ const appIdx = segs.lastIndexOf("app");
5716
+ const between = segs.slice(appIdx + 1, segs.length - 1);
5717
+ const parts = [];
5718
+ for (const seg of between) {
5719
+ const mapped = nextSegment(seg);
5720
+ if (mapped !== null) parts.push(mapped);
5721
+ }
5722
+ return "/" + parts.join("/");
5723
+ }
5724
+ function nextPagesApiPathTemplate(relFile) {
5725
+ const segs = segmentsOf(relFile);
5726
+ const pagesIdx = segs.indexOf("pages");
5727
+ const rest = segs.slice(pagesIdx + 1);
5728
+ const parts = [];
5729
+ for (let i = 0; i < rest.length; i++) {
5730
+ let seg = rest[i];
5731
+ if (i === rest.length - 1) {
5732
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
5733
+ if (seg === "index") continue;
5734
+ }
5735
+ const mapped = nextSegment(seg);
5736
+ if (mapped !== null) parts.push(mapped);
5737
+ }
5738
+ return "/" + parts.join("/");
5739
+ }
5740
+ function nextAppMethods(root) {
5741
+ const out = [];
5742
+ walk(root, (node) => {
5743
+ if (node.type !== "export_statement") return;
5744
+ const decl = node.childForFieldName("declaration");
5745
+ if (!decl) return;
5746
+ const line = node.startPosition.row + 1;
5747
+ if (decl.type === "function_declaration") {
5748
+ const name = decl.childForFieldName("name")?.text;
5749
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5750
+ return;
5751
+ }
5752
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
5753
+ for (let i = 0; i < decl.namedChildCount; i++) {
5754
+ const d = decl.namedChild(i);
5755
+ if (d?.type !== "variable_declarator") continue;
5756
+ const name = d.childForFieldName("name")?.text;
5757
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5758
+ }
5759
+ }
5760
+ });
5761
+ return out;
5762
+ }
5763
+ function nextRoutesFromFile(source, relFile, parser) {
5764
+ if (isNextAppRouteFile(relFile)) {
5765
+ const tree = parseSource2(parser, source);
5766
+ const template = nextAppPathTemplate(relFile);
5767
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
5768
+ method,
5769
+ pathTemplate: canonicalizeTemplate(template),
5770
+ line,
5771
+ framework: "next"
5772
+ }));
5773
+ }
5774
+ if (isNextPagesApiFile(relFile)) {
5775
+ return [
5776
+ {
5777
+ method: "ALL",
5778
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
5779
+ line: 1,
5780
+ framework: "next"
5781
+ }
5782
+ ];
5783
+ }
5784
+ return [];
5785
+ }
5786
+ async function addRoutes(graph, services) {
5787
+ const jsParser = makeJsParser2();
5788
+ let nodesAdded = 0;
5789
+ let edgesAdded = 0;
5790
+ for (const service of services) {
5791
+ const deps = {
5792
+ ...service.pkg.dependencies ?? {},
5793
+ ...service.pkg.devDependencies ?? {}
5794
+ };
5795
+ const hasExpress = deps["express"] !== void 0;
5796
+ const hasFastify = deps["fastify"] !== void 0;
5797
+ const hasNext = deps["next"] !== void 0;
5798
+ if (!hasExpress && !hasFastify && !hasNext) continue;
5799
+ const files = await loadSourceFiles(service.dir);
5800
+ for (const file of files) {
5801
+ if (isTestPath(file.path)) continue;
5802
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path24.default.extname(file.path))) continue;
5803
+ const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
5804
+ let routes;
5805
+ try {
5806
+ if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5807
+ routes = nextRoutesFromFile(file.content, relFile, jsParser);
5808
+ } else if (hasExpress || hasFastify) {
5809
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
5810
+ } else {
5811
+ routes = [];
5812
+ }
5813
+ } catch (err) {
5814
+ recordExtractionError("route extraction", file.path, err);
5815
+ continue;
5816
+ }
5817
+ if (routes.length === 0) continue;
5818
+ for (const route of routes) {
5819
+ const rid = (0, import_types12.routeId)(service.pkg.name, route.method, route.pathTemplate);
5820
+ if (!graph.hasNode(rid)) {
5821
+ const node = {
5822
+ id: rid,
5823
+ type: import_types12.NodeType.RouteNode,
5824
+ name: `${route.method} ${route.pathTemplate}`,
5825
+ service: service.pkg.name,
5826
+ method: route.method,
5827
+ pathTemplate: route.pathTemplate,
5828
+ path: relFile,
5829
+ line: route.line,
5830
+ framework: route.framework,
5831
+ discoveredVia: "static"
5832
+ };
5833
+ graph.addNode(rid, node);
5834
+ nodesAdded++;
5835
+ }
5836
+ const containsId = (0, import_types12.extractedEdgeId)(service.node.id, rid, import_types12.EdgeType.CONTAINS);
5837
+ if (!graph.hasEdge(containsId)) {
5838
+ const edge = {
5839
+ id: containsId,
5840
+ source: service.node.id,
5841
+ target: rid,
5842
+ type: import_types12.EdgeType.CONTAINS,
5843
+ provenance: import_types12.Provenance.EXTRACTED,
5844
+ confidence: (0, import_types12.confidenceForExtracted)("structural"),
5845
+ evidence: {
5846
+ file: relFile,
5847
+ line: route.line,
5848
+ snippet: snippet(file.content, route.line)
5849
+ }
5850
+ };
5851
+ graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
5852
+ edgesAdded++;
5853
+ }
5854
+ }
5855
+ }
5856
+ }
5857
+ return { nodesAdded, edgesAdded };
5858
+ }
5859
+
5393
5860
  // src/extract/calls/index.ts
5394
5861
  init_cjs_shims();
5395
- var import_types18 = require("@neat.is/types");
5862
+ var import_types20 = require("@neat.is/types");
5396
5863
 
5397
5864
  // src/extract/calls/http.ts
5398
5865
  init_cjs_shims();
5399
- var import_node_path24 = __toESM(require("path"), 1);
5400
- var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5401
- var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5866
+ var import_node_path25 = __toESM(require("path"), 1);
5867
+ var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
5868
+ var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5402
5869
  var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5403
- var import_types12 = require("@neat.is/types");
5870
+ var import_types13 = require("@neat.is/types");
5404
5871
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
5405
5872
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
5406
5873
  function isInsideJsxExternalLink(node) {
@@ -5428,14 +5895,14 @@ function collectStringLiterals(node, out) {
5428
5895
  if (child) collectStringLiterals(child, out);
5429
5896
  }
5430
5897
  }
5431
- var PARSE_CHUNK2 = 16384;
5432
- function parseSource2(parser, source) {
5898
+ var PARSE_CHUNK3 = 16384;
5899
+ function parseSource3(parser, source) {
5433
5900
  return parser.parse(
5434
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5901
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5435
5902
  );
5436
5903
  }
5437
5904
  function callsFromSource(source, parser, knownHosts) {
5438
- const tree = parseSource2(parser, source);
5905
+ const tree = parseSource3(parser, source);
5439
5906
  const literals = [];
5440
5907
  collectStringLiterals(tree.rootNode, literals);
5441
5908
  const out = [];
@@ -5449,27 +5916,20 @@ function callsFromSource(source, parser, knownHosts) {
5449
5916
  }
5450
5917
  return out;
5451
5918
  }
5452
- function makeJsParser2() {
5453
- const p = new import_tree_sitter2.default();
5454
- p.setLanguage(import_tree_sitter_javascript2.default);
5919
+ function makeJsParser3() {
5920
+ const p = new import_tree_sitter3.default();
5921
+ p.setLanguage(import_tree_sitter_javascript3.default);
5455
5922
  return p;
5456
5923
  }
5457
5924
  function makePyParser2() {
5458
- const p = new import_tree_sitter2.default();
5925
+ const p = new import_tree_sitter3.default();
5459
5926
  p.setLanguage(import_tree_sitter_python2.default);
5460
5927
  return p;
5461
5928
  }
5462
5929
  async function addHttpCallEdges(graph, services) {
5463
- const jsParser = makeJsParser2();
5930
+ const jsParser = makeJsParser3();
5464
5931
  const pyParser = makePyParser2();
5465
- const knownHosts = /* @__PURE__ */ new Set();
5466
- const hostToNodeId = /* @__PURE__ */ new Map();
5467
- for (const service of services) {
5468
- knownHosts.add(import_node_path24.default.basename(service.dir));
5469
- knownHosts.add(service.pkg.name);
5470
- hostToNodeId.set(import_node_path24.default.basename(service.dir), service.node.id);
5471
- hostToNodeId.set(service.pkg.name, service.node.id);
5472
- }
5932
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5473
5933
  let nodesAdded = 0;
5474
5934
  let edgesAdded = 0;
5475
5935
  for (const service of services) {
@@ -5477,7 +5937,7 @@ async function addHttpCallEdges(graph, services) {
5477
5937
  const seen = /* @__PURE__ */ new Set();
5478
5938
  for (const file of files) {
5479
5939
  if (isTestPath(file.path)) continue;
5480
- const parser = import_node_path24.default.extname(file.path) === ".py" ? pyParser : jsParser;
5940
+ const parser = import_node_path25.default.extname(file.path) === ".py" ? pyParser : jsParser;
5481
5941
  let sites;
5482
5942
  try {
5483
5943
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -5486,14 +5946,14 @@ async function addHttpCallEdges(graph, services) {
5486
5946
  continue;
5487
5947
  }
5488
5948
  if (sites.length === 0) continue;
5489
- const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
5949
+ const relFile = toPosix2(import_node_path25.default.relative(service.dir, file.path));
5490
5950
  for (const site of sites) {
5491
5951
  const targetId = hostToNodeId.get(site.host);
5492
5952
  if (!targetId || targetId === service.node.id) continue;
5493
5953
  const dedupKey = `${relFile}|${targetId}`;
5494
5954
  if (seen.has(dedupKey)) continue;
5495
5955
  seen.add(dedupKey);
5496
- const confidence = (0, import_types12.confidenceForExtracted)("url-literal-service-target");
5956
+ const confidence = (0, import_types13.confidenceForExtracted)("url-literal-service-target");
5497
5957
  const ev = {
5498
5958
  file: relFile,
5499
5959
  line: site.line,
@@ -5507,25 +5967,25 @@ async function addHttpCallEdges(graph, services) {
5507
5967
  );
5508
5968
  nodesAdded += n;
5509
5969
  edgesAdded += e;
5510
- if (!(0, import_types12.passesExtractedFloor)(confidence)) {
5970
+ if (!(0, import_types13.passesExtractedFloor)(confidence)) {
5511
5971
  noteExtractedDropped({
5512
5972
  source: fileNodeId,
5513
5973
  target: targetId,
5514
- type: import_types12.EdgeType.CALLS,
5974
+ type: import_types13.EdgeType.CALLS,
5515
5975
  confidence,
5516
5976
  confidenceKind: "url-literal-service-target",
5517
5977
  evidence: ev
5518
5978
  });
5519
5979
  continue;
5520
5980
  }
5521
- const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, targetId, import_types12.EdgeType.CALLS);
5981
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, targetId, import_types13.EdgeType.CALLS);
5522
5982
  if (!graph.hasEdge(edgeId)) {
5523
5983
  const edge = {
5524
5984
  id: edgeId,
5525
5985
  source: fileNodeId,
5526
5986
  target: targetId,
5527
- type: import_types12.EdgeType.CALLS,
5528
- provenance: import_types12.Provenance.EXTRACTED,
5987
+ type: import_types13.EdgeType.CALLS,
5988
+ provenance: import_types13.Provenance.EXTRACTED,
5529
5989
  confidence,
5530
5990
  evidence: ev
5531
5991
  };
@@ -5538,10 +5998,279 @@ async function addHttpCallEdges(graph, services) {
5538
5998
  return { nodesAdded, edgesAdded };
5539
5999
  }
5540
6000
 
6001
+ // src/extract/calls/route-match.ts
6002
+ init_cjs_shims();
6003
+ var import_node_path26 = __toESM(require("path"), 1);
6004
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
6005
+ var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
6006
+ var import_types14 = require("@neat.is/types");
6007
+ var PARSE_CHUNK4 = 16384;
6008
+ function parseSource4(parser, source) {
6009
+ return parser.parse(
6010
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
6011
+ );
6012
+ }
6013
+ function makeJsParser4() {
6014
+ const p = new import_tree_sitter4.default();
6015
+ p.setLanguage(import_tree_sitter_javascript4.default);
6016
+ return p;
6017
+ }
6018
+ var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
6019
+ var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
6020
+ function walk2(node, visit) {
6021
+ visit(node);
6022
+ for (let i = 0; i < node.namedChildCount; i++) {
6023
+ const child = node.namedChild(i);
6024
+ if (child) walk2(child, visit);
6025
+ }
6026
+ }
6027
+ function reconstructUrl(node) {
6028
+ if (node.type === "string") {
6029
+ for (let i = 0; i < node.namedChildCount; i++) {
6030
+ const child = node.namedChild(i);
6031
+ if (child?.type === "string_fragment") return child.text;
6032
+ }
6033
+ return "";
6034
+ }
6035
+ if (node.type === "template_string") {
6036
+ let out = "";
6037
+ for (let i = 0; i < node.namedChildCount; i++) {
6038
+ const child = node.namedChild(i);
6039
+ if (!child) continue;
6040
+ if (child.type === "string_fragment") out += child.text;
6041
+ else if (child.type === "template_substitution") out += ":param";
6042
+ }
6043
+ if (out.length === 0) {
6044
+ const raw = node.text;
6045
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
6046
+ }
6047
+ return out;
6048
+ }
6049
+ return null;
6050
+ }
6051
+ function methodFromOptions(objNode) {
6052
+ for (let i = 0; i < objNode.namedChildCount; i++) {
6053
+ const pair = objNode.namedChild(i);
6054
+ if (!pair || pair.type !== "pair") continue;
6055
+ const k = pair.childForFieldName("key");
6056
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
6057
+ if (kText !== "method") continue;
6058
+ const v = pair.childForFieldName("value");
6059
+ if (v && (v.type === "string" || v.type === "template_string")) {
6060
+ const s = stringText(v);
6061
+ return s ? s.toUpperCase() : void 0;
6062
+ }
6063
+ }
6064
+ return void 0;
6065
+ }
6066
+ function stringText(node) {
6067
+ if (node.type === "string") {
6068
+ for (let i = 0; i < node.namedChildCount; i++) {
6069
+ const child = node.namedChild(i);
6070
+ if (child?.type === "string_fragment") return child.text;
6071
+ }
6072
+ return "";
6073
+ }
6074
+ return null;
6075
+ }
6076
+ function urlNodeFromConfig(objNode) {
6077
+ for (let i = 0; i < objNode.namedChildCount; i++) {
6078
+ const pair = objNode.namedChild(i);
6079
+ if (!pair || pair.type !== "pair") continue;
6080
+ const k = pair.childForFieldName("key");
6081
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
6082
+ if (kText === "url") return pair.childForFieldName("value");
6083
+ }
6084
+ return null;
6085
+ }
6086
+ function pathOf(urlStr) {
6087
+ try {
6088
+ const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
6089
+ const parsed = new URL(candidate);
6090
+ return parsed.pathname || "/";
6091
+ } catch {
6092
+ return null;
6093
+ }
6094
+ }
6095
+ function matchHost(urlStr, knownHosts) {
6096
+ for (const host of knownHosts) {
6097
+ if (urlMatchesHost(urlStr, host)) return host;
6098
+ }
6099
+ return null;
6100
+ }
6101
+ function clientCallSitesFromSource(source, parser, knownHosts) {
6102
+ const tree = parseSource4(parser, source);
6103
+ const out = [];
6104
+ const push = (urlNode, method, callNode) => {
6105
+ const urlStr = reconstructUrl(urlNode);
6106
+ if (!urlStr) return;
6107
+ const host = matchHost(urlStr, knownHosts);
6108
+ if (!host) return;
6109
+ const p = pathOf(urlStr);
6110
+ if (p === null) return;
6111
+ const line = callNode.startPosition.row + 1;
6112
+ out.push({
6113
+ host,
6114
+ method,
6115
+ pathTemplate: p,
6116
+ line,
6117
+ snippet: snippet(source, line)
6118
+ });
6119
+ };
6120
+ walk2(tree.rootNode, (node) => {
6121
+ if (node.type !== "call_expression") return;
6122
+ const fn = node.childForFieldName("function");
6123
+ if (!fn) return;
6124
+ const args = node.childForFieldName("arguments");
6125
+ const first = args?.namedChild(0);
6126
+ if (!first) return;
6127
+ const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
6128
+ if (fn.type === "identifier" && fnName === "fetch") {
6129
+ const opts = args?.namedChild(1);
6130
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
6131
+ push(first, method, node);
6132
+ return;
6133
+ }
6134
+ if (fn.type === "identifier" && fnName === "axios") {
6135
+ if (first.type === "object") {
6136
+ const urlNode = urlNodeFromConfig(first);
6137
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
6138
+ } else {
6139
+ const opts = args?.namedChild(1);
6140
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
6141
+ push(first, method, node);
6142
+ }
6143
+ return;
6144
+ }
6145
+ if (fn.type === "member_expression") {
6146
+ const obj = fn.childForFieldName("object");
6147
+ const objName = obj?.text ?? "";
6148
+ if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
6149
+ if (fnName === "request" && first.type === "object") {
6150
+ const urlNode = urlNodeFromConfig(first);
6151
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
6152
+ } else {
6153
+ push(first, fnName.toUpperCase(), node);
6154
+ }
6155
+ return;
6156
+ }
6157
+ if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
6158
+ const opts = args?.namedChild(1);
6159
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
6160
+ push(first, method, node);
6161
+ return;
6162
+ }
6163
+ }
6164
+ });
6165
+ return out;
6166
+ }
6167
+ function buildRouteIndex(graph) {
6168
+ const index = /* @__PURE__ */ new Map();
6169
+ graph.forEachNode((_id, attrs) => {
6170
+ const node = attrs;
6171
+ if (node.type !== import_types14.NodeType.RouteNode) return;
6172
+ const route = attrs;
6173
+ const owner = (0, import_types14.serviceId)(route.service);
6174
+ const entry = {
6175
+ method: route.method.toUpperCase(),
6176
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
6177
+ routeNodeId: route.id
6178
+ };
6179
+ const list = index.get(owner);
6180
+ if (list) list.push(entry);
6181
+ else index.set(owner, [entry]);
6182
+ });
6183
+ return index;
6184
+ }
6185
+ function findRoute(entries, method, normalizedPath) {
6186
+ return entries.find(
6187
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
6188
+ );
6189
+ }
6190
+ async function addRouteCallEdges(graph, services) {
6191
+ const jsParser = makeJsParser4();
6192
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
6193
+ const routeIndex = buildRouteIndex(graph);
6194
+ if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
6195
+ let nodesAdded = 0;
6196
+ let edgesAdded = 0;
6197
+ for (const service of services) {
6198
+ const files = await loadSourceFiles(service.dir);
6199
+ const seen = /* @__PURE__ */ new Set();
6200
+ for (const file of files) {
6201
+ if (isTestPath(file.path)) continue;
6202
+ if (!JS_CLIENT_EXTENSIONS.has(import_node_path26.default.extname(file.path))) continue;
6203
+ let sites;
6204
+ try {
6205
+ sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
6206
+ } catch (err) {
6207
+ recordExtractionError("route-match call extraction", file.path, err);
6208
+ continue;
6209
+ }
6210
+ if (sites.length === 0) continue;
6211
+ const relFile = toPosix2(import_node_path26.default.relative(service.dir, file.path));
6212
+ for (const site of sites) {
6213
+ const serverServiceId = hostToNodeId.get(site.host);
6214
+ if (!serverServiceId || serverServiceId === service.node.id) continue;
6215
+ const entries = routeIndex.get(serverServiceId);
6216
+ if (!entries) continue;
6217
+ const normalizedPath = normalizePathTemplate(site.pathTemplate);
6218
+ const match = findRoute(entries, site.method, normalizedPath);
6219
+ if (!match) continue;
6220
+ const dedupKey = `${relFile}|${match.routeNodeId}`;
6221
+ if (seen.has(dedupKey)) continue;
6222
+ seen.add(dedupKey);
6223
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
6224
+ graph,
6225
+ service.pkg.name,
6226
+ service.node.id,
6227
+ relFile
6228
+ );
6229
+ nodesAdded += n;
6230
+ edgesAdded += e;
6231
+ const confidence = (0, import_types14.confidenceForExtracted)("verified-call-site");
6232
+ const ev = {
6233
+ file: relFile,
6234
+ line: site.line,
6235
+ snippet: site.snippet,
6236
+ method: site.method ?? match.method,
6237
+ pathTemplate: site.pathTemplate
6238
+ };
6239
+ if (!(0, import_types14.passesExtractedFloor)(confidence)) {
6240
+ noteExtractedDropped({
6241
+ source: fileNodeId,
6242
+ target: match.routeNodeId,
6243
+ type: import_types14.EdgeType.CALLS,
6244
+ confidence,
6245
+ confidenceKind: "verified-call-site",
6246
+ evidence: ev
6247
+ });
6248
+ continue;
6249
+ }
6250
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, match.routeNodeId, import_types14.EdgeType.CALLS);
6251
+ if (!graph.hasEdge(edgeId)) {
6252
+ const edge = {
6253
+ id: edgeId,
6254
+ source: fileNodeId,
6255
+ target: match.routeNodeId,
6256
+ type: import_types14.EdgeType.CALLS,
6257
+ provenance: import_types14.Provenance.EXTRACTED,
6258
+ confidence,
6259
+ evidence: ev
6260
+ };
6261
+ graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
6262
+ edgesAdded++;
6263
+ }
6264
+ }
6265
+ }
6266
+ }
6267
+ return { nodesAdded, edgesAdded };
6268
+ }
6269
+
5541
6270
  // src/extract/calls/kafka.ts
5542
6271
  init_cjs_shims();
5543
- var import_node_path25 = __toESM(require("path"), 1);
5544
- var import_types13 = require("@neat.is/types");
6272
+ var import_node_path27 = __toESM(require("path"), 1);
6273
+ var import_types15 = require("@neat.is/types");
5545
6274
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
5546
6275
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
5547
6276
  function findAll(re, text) {
@@ -5562,7 +6291,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5562
6291
  seen.add(key);
5563
6292
  const line = lineOf(file.content, topic);
5564
6293
  out.push({
5565
- infraId: (0, import_types13.infraId)("kafka-topic", topic),
6294
+ infraId: (0, import_types15.infraId)("kafka-topic", topic),
5566
6295
  name: topic,
5567
6296
  kind: "kafka-topic",
5568
6297
  edgeType,
@@ -5571,7 +6300,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5571
6300
  // tier (ADR-066).
5572
6301
  confidenceKind: "verified-call-site",
5573
6302
  evidence: {
5574
- file: import_node_path25.default.relative(serviceDir, file.path),
6303
+ file: import_node_path27.default.relative(serviceDir, file.path),
5575
6304
  line,
5576
6305
  snippet: snippet(file.content, line)
5577
6306
  }
@@ -5584,8 +6313,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5584
6313
 
5585
6314
  // src/extract/calls/redis.ts
5586
6315
  init_cjs_shims();
5587
- var import_node_path26 = __toESM(require("path"), 1);
5588
- var import_types14 = require("@neat.is/types");
6316
+ var import_node_path28 = __toESM(require("path"), 1);
6317
+ var import_types16 = require("@neat.is/types");
5589
6318
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
5590
6319
  function redisEndpointsFromFile(file, serviceDir) {
5591
6320
  const out = [];
@@ -5598,7 +6327,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5598
6327
  seen.add(host);
5599
6328
  const line = lineOf(file.content, host);
5600
6329
  out.push({
5601
- infraId: (0, import_types14.infraId)("redis", host),
6330
+ infraId: (0, import_types16.infraId)("redis", host),
5602
6331
  name: host,
5603
6332
  kind: "redis",
5604
6333
  edgeType: "CALLS",
@@ -5607,7 +6336,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5607
6336
  // support tier (ADR-066).
5608
6337
  confidenceKind: "url-with-structural-support",
5609
6338
  evidence: {
5610
- file: import_node_path26.default.relative(serviceDir, file.path),
6339
+ file: import_node_path28.default.relative(serviceDir, file.path),
5611
6340
  line,
5612
6341
  snippet: snippet(file.content, line)
5613
6342
  }
@@ -5618,8 +6347,8 @@ function redisEndpointsFromFile(file, serviceDir) {
5618
6347
 
5619
6348
  // src/extract/calls/aws.ts
5620
6349
  init_cjs_shims();
5621
- var import_node_path27 = __toESM(require("path"), 1);
5622
- var import_types15 = require("@neat.is/types");
6350
+ var import_node_path29 = __toESM(require("path"), 1);
6351
+ var import_types17 = require("@neat.is/types");
5623
6352
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
5624
6353
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
5625
6354
  function hasMarker(text, markers) {
@@ -5643,7 +6372,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5643
6372
  seen.add(key);
5644
6373
  const line = lineOf(file.content, name);
5645
6374
  out.push({
5646
- infraId: (0, import_types15.infraId)(kind, name),
6375
+ infraId: (0, import_types17.infraId)(kind, name),
5647
6376
  name,
5648
6377
  kind,
5649
6378
  edgeType: "CALLS",
@@ -5652,7 +6381,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5652
6381
  // (ADR-066).
5653
6382
  confidenceKind: "verified-call-site",
5654
6383
  evidence: {
5655
- file: import_node_path27.default.relative(serviceDir, file.path),
6384
+ file: import_node_path29.default.relative(serviceDir, file.path),
5656
6385
  line,
5657
6386
  snippet: snippet(file.content, line)
5658
6387
  }
@@ -5677,8 +6406,8 @@ function awsEndpointsFromFile(file, serviceDir) {
5677
6406
 
5678
6407
  // src/extract/calls/grpc.ts
5679
6408
  init_cjs_shims();
5680
- var import_node_path28 = __toESM(require("path"), 1);
5681
- var import_types16 = require("@neat.is/types");
6409
+ var import_node_path30 = __toESM(require("path"), 1);
6410
+ var import_types18 = require("@neat.is/types");
5682
6411
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
5683
6412
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
5684
6413
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -5727,7 +6456,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5727
6456
  const { kind } = classified;
5728
6457
  const line = lineOf(file.content, m[0]);
5729
6458
  out.push({
5730
- infraId: (0, import_types16.infraId)(kind, name),
6459
+ infraId: (0, import_types18.infraId)(kind, name),
5731
6460
  name,
5732
6461
  kind,
5733
6462
  edgeType: "CALLS",
@@ -5736,7 +6465,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5736
6465
  // tier (ADR-066).
5737
6466
  confidenceKind: "verified-call-site",
5738
6467
  evidence: {
5739
- file: import_node_path28.default.relative(serviceDir, file.path),
6468
+ file: import_node_path30.default.relative(serviceDir, file.path),
5740
6469
  line,
5741
6470
  snippet: snippet(file.content, line)
5742
6471
  }
@@ -5747,8 +6476,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
5747
6476
 
5748
6477
  // src/extract/calls/supabase.ts
5749
6478
  init_cjs_shims();
5750
- var import_node_path29 = __toESM(require("path"), 1);
5751
- var import_types17 = require("@neat.is/types");
6479
+ var import_node_path31 = __toESM(require("path"), 1);
6480
+ var import_types19 = require("@neat.is/types");
5752
6481
  var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
5753
6482
  var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
5754
6483
  var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
@@ -5786,7 +6515,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5786
6515
  seen.add(name);
5787
6516
  const line = lineOf(file.content, m[0]);
5788
6517
  out.push({
5789
- infraId: (0, import_types17.infraId)("supabase", name),
6518
+ infraId: (0, import_types19.infraId)("supabase", name),
5790
6519
  name,
5791
6520
  kind: "supabase",
5792
6521
  edgeType: "CALLS",
@@ -5796,7 +6525,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5796
6525
  // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
5797
6526
  confidenceKind: "verified-call-site",
5798
6527
  evidence: {
5799
- file: import_node_path29.default.relative(serviceDir, file.path),
6528
+ file: import_node_path31.default.relative(serviceDir, file.path),
5800
6529
  line,
5801
6530
  snippet: snippet(file.content, line)
5802
6531
  }
@@ -5809,11 +6538,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5809
6538
  function edgeTypeFromEndpoint(ep) {
5810
6539
  switch (ep.edgeType) {
5811
6540
  case "PUBLISHES_TO":
5812
- return import_types18.EdgeType.PUBLISHES_TO;
6541
+ return import_types20.EdgeType.PUBLISHES_TO;
5813
6542
  case "CONSUMES_FROM":
5814
- return import_types18.EdgeType.CONSUMES_FROM;
6543
+ return import_types20.EdgeType.CONSUMES_FROM;
5815
6544
  default:
5816
- return import_types18.EdgeType.CALLS;
6545
+ return import_types20.EdgeType.CALLS;
5817
6546
  }
5818
6547
  }
5819
6548
  function isAwsKind(kind) {
@@ -5841,7 +6570,7 @@ async function addExternalEndpointEdges(graph, services) {
5841
6570
  if (!graph.hasNode(ep.infraId)) {
5842
6571
  const node = {
5843
6572
  id: ep.infraId,
5844
- type: import_types18.NodeType.InfraNode,
6573
+ type: import_types20.NodeType.InfraNode,
5845
6574
  name: ep.name,
5846
6575
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
5847
6576
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -5853,7 +6582,7 @@ async function addExternalEndpointEdges(graph, services) {
5853
6582
  nodesAdded++;
5854
6583
  }
5855
6584
  const edgeType = edgeTypeFromEndpoint(ep);
5856
- const confidence = (0, import_types18.confidenceForExtracted)(ep.confidenceKind);
6585
+ const confidence = (0, import_types20.confidenceForExtracted)(ep.confidenceKind);
5857
6586
  const relFile = toPosix2(ep.evidence.file);
5858
6587
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5859
6588
  graph,
@@ -5863,7 +6592,7 @@ async function addExternalEndpointEdges(graph, services) {
5863
6592
  );
5864
6593
  nodesAdded += n;
5865
6594
  edgesAdded += e;
5866
- if (!(0, import_types18.passesExtractedFloor)(confidence)) {
6595
+ if (!(0, import_types20.passesExtractedFloor)(confidence)) {
5867
6596
  noteExtractedDropped({
5868
6597
  source: fileNodeId,
5869
6598
  target: ep.infraId,
@@ -5883,7 +6612,7 @@ async function addExternalEndpointEdges(graph, services) {
5883
6612
  source: fileNodeId,
5884
6613
  target: ep.infraId,
5885
6614
  type: edgeType,
5886
- provenance: import_types18.Provenance.EXTRACTED,
6615
+ provenance: import_types20.Provenance.EXTRACTED,
5887
6616
  confidence,
5888
6617
  evidence: ep.evidence
5889
6618
  };
@@ -5897,9 +6626,10 @@ async function addExternalEndpointEdges(graph, services) {
5897
6626
  async function addCallEdges(graph, services) {
5898
6627
  const http = await addHttpCallEdges(graph, services);
5899
6628
  const ext = await addExternalEndpointEdges(graph, services);
6629
+ const routes = await addRouteCallEdges(graph, services);
5900
6630
  return {
5901
- nodesAdded: http.nodesAdded + ext.nodesAdded,
5902
- edgesAdded: http.edgesAdded + ext.edgesAdded
6631
+ nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
6632
+ edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
5903
6633
  };
5904
6634
  }
5905
6635
 
@@ -5908,16 +6638,16 @@ init_cjs_shims();
5908
6638
 
5909
6639
  // src/extract/infra/docker-compose.ts
5910
6640
  init_cjs_shims();
5911
- var import_node_path30 = __toESM(require("path"), 1);
5912
- var import_types20 = require("@neat.is/types");
6641
+ var import_node_path32 = __toESM(require("path"), 1);
6642
+ var import_types22 = require("@neat.is/types");
5913
6643
 
5914
6644
  // src/extract/infra/shared.ts
5915
6645
  init_cjs_shims();
5916
- var import_types19 = require("@neat.is/types");
6646
+ var import_types21 = require("@neat.is/types");
5917
6647
  function makeInfraNode(kind, name, provider = "self", extras) {
5918
6648
  return {
5919
- id: (0, import_types19.infraId)(kind, name),
5920
- type: import_types19.NodeType.InfraNode,
6649
+ id: (0, import_types21.infraId)(kind, name),
6650
+ type: import_types21.NodeType.InfraNode,
5921
6651
  name,
5922
6652
  provider,
5923
6653
  kind,
@@ -5946,7 +6676,7 @@ function dependsOnList(value) {
5946
6676
  }
5947
6677
  function serviceNameToServiceNode(name, services) {
5948
6678
  for (const s of services) {
5949
- if (s.node.name === name || import_node_path30.default.basename(s.dir) === name) return s.node.id;
6679
+ if (s.node.name === name || import_node_path32.default.basename(s.dir) === name) return s.node.id;
5950
6680
  }
5951
6681
  return null;
5952
6682
  }
@@ -5955,7 +6685,7 @@ async function addComposeInfra(graph, scanPath, services) {
5955
6685
  let edgesAdded = 0;
5956
6686
  let composePath = null;
5957
6687
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5958
- const abs = import_node_path30.default.join(scanPath, name);
6688
+ const abs = import_node_path32.default.join(scanPath, name);
5959
6689
  if (await exists2(abs)) {
5960
6690
  composePath = abs;
5961
6691
  break;
@@ -5968,13 +6698,13 @@ async function addComposeInfra(graph, scanPath, services) {
5968
6698
  } catch (err) {
5969
6699
  recordExtractionError(
5970
6700
  "infra docker-compose",
5971
- import_node_path30.default.relative(scanPath, composePath),
6701
+ import_node_path32.default.relative(scanPath, composePath),
5972
6702
  err
5973
6703
  );
5974
6704
  return { nodesAdded, edgesAdded };
5975
6705
  }
5976
6706
  if (!compose?.services) return { nodesAdded, edgesAdded };
5977
- const evidenceFile = import_node_path30.default.relative(scanPath, composePath).split(import_node_path30.default.sep).join("/");
6707
+ const evidenceFile = import_node_path32.default.relative(scanPath, composePath).split(import_node_path32.default.sep).join("/");
5978
6708
  const composeNameToNodeId = /* @__PURE__ */ new Map();
5979
6709
  for (const [composeName, svc] of Object.entries(compose.services)) {
5980
6710
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -5996,15 +6726,15 @@ async function addComposeInfra(graph, scanPath, services) {
5996
6726
  for (const dep of dependsOnList(svc.depends_on)) {
5997
6727
  const targetId = composeNameToNodeId.get(dep);
5998
6728
  if (!targetId) continue;
5999
- const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types20.EdgeType.DEPENDS_ON);
6729
+ const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types22.EdgeType.DEPENDS_ON);
6000
6730
  if (graph.hasEdge(edgeId)) continue;
6001
6731
  const edge = {
6002
6732
  id: edgeId,
6003
6733
  source: sourceId,
6004
6734
  target: targetId,
6005
- type: import_types20.EdgeType.DEPENDS_ON,
6006
- provenance: import_types20.Provenance.EXTRACTED,
6007
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6735
+ type: import_types22.EdgeType.DEPENDS_ON,
6736
+ provenance: import_types22.Provenance.EXTRACTED,
6737
+ confidence: (0, import_types22.confidenceForExtracted)("structural"),
6008
6738
  evidence: { file: evidenceFile }
6009
6739
  };
6010
6740
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -6016,9 +6746,9 @@ async function addComposeInfra(graph, scanPath, services) {
6016
6746
 
6017
6747
  // src/extract/infra/dockerfile.ts
6018
6748
  init_cjs_shims();
6019
- var import_node_path31 = __toESM(require("path"), 1);
6749
+ var import_node_path33 = __toESM(require("path"), 1);
6020
6750
  var import_node_fs17 = require("fs");
6021
- var import_types21 = require("@neat.is/types");
6751
+ var import_types23 = require("@neat.is/types");
6022
6752
  function readDockerfile(content) {
6023
6753
  let image = null;
6024
6754
  const ports = [];
@@ -6047,7 +6777,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6047
6777
  let nodesAdded = 0;
6048
6778
  let edgesAdded = 0;
6049
6779
  for (const service of services) {
6050
- const dockerfilePath = import_node_path31.default.join(service.dir, "Dockerfile");
6780
+ const dockerfilePath = import_node_path33.default.join(service.dir, "Dockerfile");
6051
6781
  if (!await exists2(dockerfilePath)) continue;
6052
6782
  let content;
6053
6783
  try {
@@ -6055,7 +6785,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6055
6785
  } catch (err) {
6056
6786
  recordExtractionError(
6057
6787
  "infra dockerfile",
6058
- import_node_path31.default.relative(scanPath, dockerfilePath),
6788
+ import_node_path33.default.relative(scanPath, dockerfilePath),
6059
6789
  err
6060
6790
  );
6061
6791
  continue;
@@ -6067,8 +6797,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6067
6797
  graph.addNode(node.id, node);
6068
6798
  nodesAdded++;
6069
6799
  }
6070
- const relDockerfile = toPosix2(import_node_path31.default.relative(service.dir, dockerfilePath));
6071
- const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, dockerfilePath));
6800
+ const relDockerfile = toPosix2(import_node_path33.default.relative(service.dir, dockerfilePath));
6801
+ const evidenceFile = toPosix2(import_node_path33.default.relative(scanPath, dockerfilePath));
6072
6802
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6073
6803
  graph,
6074
6804
  service.pkg.name,
@@ -6077,15 +6807,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6077
6807
  );
6078
6808
  nodesAdded += fn;
6079
6809
  edgesAdded += fe;
6080
- const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types21.EdgeType.RUNS_ON);
6810
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types23.EdgeType.RUNS_ON);
6081
6811
  if (!graph.hasEdge(edgeId)) {
6082
6812
  const edge = {
6083
6813
  id: edgeId,
6084
6814
  source: fileNodeId,
6085
6815
  target: node.id,
6086
- type: import_types21.EdgeType.RUNS_ON,
6087
- provenance: import_types21.Provenance.EXTRACTED,
6088
- confidence: (0, import_types21.confidenceForExtracted)("structural"),
6816
+ type: import_types23.EdgeType.RUNS_ON,
6817
+ provenance: import_types23.Provenance.EXTRACTED,
6818
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
6089
6819
  evidence: {
6090
6820
  file: evidenceFile,
6091
6821
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -6100,15 +6830,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6100
6830
  graph.addNode(portNode.id, portNode);
6101
6831
  nodesAdded++;
6102
6832
  }
6103
- const portEdgeId = (0, import_types5.extractedEdgeId)(fileNodeId, portNode.id, import_types21.EdgeType.CONNECTS_TO);
6833
+ const portEdgeId = (0, import_types5.extractedEdgeId)(fileNodeId, portNode.id, import_types23.EdgeType.CONNECTS_TO);
6104
6834
  if (graph.hasEdge(portEdgeId)) continue;
6105
6835
  const portEdge = {
6106
6836
  id: portEdgeId,
6107
6837
  source: fileNodeId,
6108
6838
  target: portNode.id,
6109
- type: import_types21.EdgeType.CONNECTS_TO,
6110
- provenance: import_types21.Provenance.EXTRACTED,
6111
- confidence: (0, import_types21.confidenceForExtracted)("structural"),
6839
+ type: import_types23.EdgeType.CONNECTS_TO,
6840
+ provenance: import_types23.Provenance.EXTRACTED,
6841
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
6112
6842
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
6113
6843
  };
6114
6844
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -6121,8 +6851,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6121
6851
  // src/extract/infra/terraform.ts
6122
6852
  init_cjs_shims();
6123
6853
  var import_node_fs18 = require("fs");
6124
- var import_node_path32 = __toESM(require("path"), 1);
6125
- var import_types22 = require("@neat.is/types");
6854
+ var import_node_path34 = __toESM(require("path"), 1);
6855
+ var import_types24 = require("@neat.is/types");
6126
6856
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
6127
6857
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
6128
6858
  async function walkTfFiles(start, depth = 0, max = 5) {
@@ -6132,11 +6862,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
6132
6862
  for (const entry of entries) {
6133
6863
  if (entry.isDirectory()) {
6134
6864
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
6135
- const child = import_node_path32.default.join(start, entry.name);
6865
+ const child = import_node_path34.default.join(start, entry.name);
6136
6866
  if (await isPythonVenvDir(child)) continue;
6137
6867
  out.push(...await walkTfFiles(child, depth + 1, max));
6138
6868
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
6139
- out.push(import_node_path32.default.join(start, entry.name));
6869
+ out.push(import_node_path34.default.join(start, entry.name));
6140
6870
  }
6141
6871
  }
6142
6872
  return out;
@@ -6168,7 +6898,7 @@ async function addTerraformResources(graph, scanPath) {
6168
6898
  const files = await walkTfFiles(scanPath);
6169
6899
  for (const file of files) {
6170
6900
  const content = await import_node_fs18.promises.readFile(file, "utf8");
6171
- const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, file));
6901
+ const evidenceFile = toPosix2(import_node_path34.default.relative(scanPath, file));
6172
6902
  const resources = [];
6173
6903
  const byKey = /* @__PURE__ */ new Map();
6174
6904
  RESOURCE_RE.lastIndex = 0;
@@ -6203,16 +6933,16 @@ async function addTerraformResources(graph, scanPath) {
6203
6933
  if (!target) continue;
6204
6934
  if (seen.has(target.nodeId)) continue;
6205
6935
  seen.add(target.nodeId);
6206
- const edgeId = (0, import_types5.extractedEdgeId)(resource.nodeId, target.nodeId, import_types22.EdgeType.DEPENDS_ON);
6936
+ const edgeId = (0, import_types5.extractedEdgeId)(resource.nodeId, target.nodeId, import_types24.EdgeType.DEPENDS_ON);
6207
6937
  if (graph.hasEdge(edgeId)) continue;
6208
6938
  const line = lineAt(content, resource.bodyOffset + ref.index);
6209
6939
  const edge = {
6210
6940
  id: edgeId,
6211
6941
  source: resource.nodeId,
6212
6942
  target: target.nodeId,
6213
- type: import_types22.EdgeType.DEPENDS_ON,
6214
- provenance: import_types22.Provenance.EXTRACTED,
6215
- confidence: (0, import_types22.confidenceForExtracted)("structural"),
6943
+ type: import_types24.EdgeType.DEPENDS_ON,
6944
+ provenance: import_types24.Provenance.EXTRACTED,
6945
+ confidence: (0, import_types24.confidenceForExtracted)("structural"),
6216
6946
  evidence: { file: evidenceFile, line, snippet: key }
6217
6947
  };
6218
6948
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -6226,7 +6956,7 @@ async function addTerraformResources(graph, scanPath) {
6226
6956
  // src/extract/infra/k8s.ts
6227
6957
  init_cjs_shims();
6228
6958
  var import_node_fs19 = require("fs");
6229
- var import_node_path33 = __toESM(require("path"), 1);
6959
+ var import_node_path35 = __toESM(require("path"), 1);
6230
6960
  var import_yaml3 = require("yaml");
6231
6961
  var K8S_KIND_TO_INFRA_KIND = {
6232
6962
  Service: "k8s-service",
@@ -6244,11 +6974,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
6244
6974
  for (const entry of entries) {
6245
6975
  if (entry.isDirectory()) {
6246
6976
  if (IGNORED_DIRS.has(entry.name)) continue;
6247
- const child = import_node_path33.default.join(start, entry.name);
6977
+ const child = import_node_path35.default.join(start, entry.name);
6248
6978
  if (await isPythonVenvDir(child)) continue;
6249
6979
  out.push(...await walkYamlFiles2(child, depth + 1, max));
6250
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path33.default.extname(entry.name))) {
6251
- out.push(import_node_path33.default.join(start, entry.name));
6980
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path35.default.extname(entry.name))) {
6981
+ out.push(import_node_path35.default.join(start, entry.name));
6252
6982
  }
6253
6983
  }
6254
6984
  return out;
@@ -6292,17 +7022,17 @@ async function addInfra(graph, scanPath, services) {
6292
7022
  }
6293
7023
 
6294
7024
  // src/extract/index.ts
6295
- var import_node_path35 = __toESM(require("path"), 1);
7025
+ var import_node_path37 = __toESM(require("path"), 1);
6296
7026
 
6297
7027
  // src/extract/retire.ts
6298
7028
  init_cjs_shims();
6299
7029
  var import_node_fs20 = require("fs");
6300
- var import_node_path34 = __toESM(require("path"), 1);
6301
- var import_types23 = require("@neat.is/types");
7030
+ var import_node_path36 = __toESM(require("path"), 1);
7031
+ var import_types25 = require("@neat.is/types");
6302
7032
  function dropOrphanedFileNodes(graph) {
6303
7033
  const orphans = [];
6304
7034
  graph.forEachNode((id, attrs) => {
6305
- if (attrs.type !== import_types23.NodeType.FileNode) return;
7035
+ if (attrs.type !== import_types25.NodeType.FileNode) return;
6306
7036
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
6307
7037
  orphans.push(id);
6308
7038
  }
@@ -6315,14 +7045,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
6315
7045
  const bases = [scanPath, ...serviceDirs];
6316
7046
  graph.forEachEdge((id, attrs) => {
6317
7047
  const edge = attrs;
6318
- if (edge.provenance !== import_types23.Provenance.EXTRACTED) return;
7048
+ if (edge.provenance !== import_types25.Provenance.EXTRACTED) return;
6319
7049
  const evidenceFile = edge.evidence?.file;
6320
7050
  if (!evidenceFile) return;
6321
- if (import_node_path34.default.isAbsolute(evidenceFile)) {
7051
+ if (import_node_path36.default.isAbsolute(evidenceFile)) {
6322
7052
  if (!(0, import_node_fs20.existsSync)(evidenceFile)) toDrop.push(id);
6323
7053
  return;
6324
7054
  }
6325
- const found = bases.some((base) => (0, import_node_fs20.existsSync)(import_node_path34.default.join(base, evidenceFile)));
7055
+ const found = bases.some((base) => (0, import_node_fs20.existsSync)(import_node_path36.default.join(base, evidenceFile)));
6326
7056
  if (!found) toDrop.push(id);
6327
7057
  });
6328
7058
  for (const id of toDrop) graph.dropEdge(id);
@@ -6341,6 +7071,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6341
7071
  const importGraph = await addImports(graph, services);
6342
7072
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
6343
7073
  const phase3 = await addConfigNodes(graph, services, scanPath);
7074
+ const routePhase = await addRoutes(graph, services);
6344
7075
  const phase4 = await addCallEdges(graph, services);
6345
7076
  const phase5 = await addInfra(graph, scanPath, services);
6346
7077
  const ghostsRetired = retireExtractedEdgesByMissingFile(
@@ -6362,7 +7093,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6362
7093
  }
6363
7094
  const droppedEntries = drainDroppedExtracted();
6364
7095
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
6365
- const rejectedPath = import_node_path35.default.join(import_node_path35.default.dirname(opts.errorsPath), "rejected.ndjson");
7096
+ const rejectedPath = import_node_path37.default.join(import_node_path37.default.dirname(opts.errorsPath), "rejected.ndjson");
6366
7097
  try {
6367
7098
  await writeRejectedExtracted(droppedEntries, rejectedPath);
6368
7099
  } catch (err) {
@@ -6372,8 +7103,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6372
7103
  }
6373
7104
  }
6374
7105
  const result = {
6375
- nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
6376
- edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
7106
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
7107
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
6377
7108
  frontiersPromoted,
6378
7109
  extractionErrors: errorEntries.length,
6379
7110
  errorEntries,
@@ -6474,8 +7205,8 @@ function canonicalJson(value) {
6474
7205
  // src/persist.ts
6475
7206
  init_cjs_shims();
6476
7207
  var import_node_fs22 = require("fs");
6477
- var import_node_path36 = __toESM(require("path"), 1);
6478
- var import_types24 = require("@neat.is/types");
7208
+ var import_node_path38 = __toESM(require("path"), 1);
7209
+ var import_types26 = require("@neat.is/types");
6479
7210
  var SCHEMA_VERSION = 4;
6480
7211
  function migrateV1ToV2(payload) {
6481
7212
  const nodes = payload.graph.nodes;
@@ -6497,12 +7228,12 @@ function migrateV2ToV3(payload) {
6497
7228
  for (const edge of edges) {
6498
7229
  const attrs = edge.attributes;
6499
7230
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
6500
- attrs.provenance = import_types24.Provenance.OBSERVED;
7231
+ attrs.provenance = import_types26.Provenance.OBSERVED;
6501
7232
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
6502
7233
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
6503
7234
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
6504
7235
  if (type && source && target) {
6505
- const newId = (0, import_types24.observedEdgeId)(source, target, type);
7236
+ const newId = (0, import_types26.observedEdgeId)(source, target, type);
6506
7237
  attrs.id = newId;
6507
7238
  if (edge.key) edge.key = newId;
6508
7239
  }
@@ -6511,7 +7242,7 @@ function migrateV2ToV3(payload) {
6511
7242
  return { ...payload, schemaVersion: 3 };
6512
7243
  }
6513
7244
  async function ensureDir(filePath) {
6514
- await import_node_fs22.promises.mkdir(import_node_path36.default.dirname(filePath), { recursive: true });
7245
+ await import_node_fs22.promises.mkdir(import_node_path38.default.dirname(filePath), { recursive: true });
6515
7246
  }
6516
7247
  async function saveGraphToDisk(graph, outPath) {
6517
7248
  await ensureDir(outPath);
@@ -6592,23 +7323,23 @@ function startPersistLoop(graph, outPath, opts = {}) {
6592
7323
 
6593
7324
  // src/projects.ts
6594
7325
  init_cjs_shims();
6595
- var import_node_path37 = __toESM(require("path"), 1);
7326
+ var import_node_path39 = __toESM(require("path"), 1);
6596
7327
  function pathsForProject(project, baseDir) {
6597
7328
  if (project === DEFAULT_PROJECT) {
6598
7329
  return {
6599
- snapshotPath: import_node_path37.default.join(baseDir, "graph.json"),
6600
- errorsPath: import_node_path37.default.join(baseDir, "errors.ndjson"),
6601
- staleEventsPath: import_node_path37.default.join(baseDir, "stale-events.ndjson"),
6602
- embeddingsCachePath: import_node_path37.default.join(baseDir, "embeddings.json"),
6603
- policyViolationsPath: import_node_path37.default.join(baseDir, "policy-violations.ndjson")
7330
+ snapshotPath: import_node_path39.default.join(baseDir, "graph.json"),
7331
+ errorsPath: import_node_path39.default.join(baseDir, "errors.ndjson"),
7332
+ staleEventsPath: import_node_path39.default.join(baseDir, "stale-events.ndjson"),
7333
+ embeddingsCachePath: import_node_path39.default.join(baseDir, "embeddings.json"),
7334
+ policyViolationsPath: import_node_path39.default.join(baseDir, "policy-violations.ndjson")
6604
7335
  };
6605
7336
  }
6606
7337
  return {
6607
- snapshotPath: import_node_path37.default.join(baseDir, `${project}.json`),
6608
- errorsPath: import_node_path37.default.join(baseDir, `errors.${project}.ndjson`),
6609
- staleEventsPath: import_node_path37.default.join(baseDir, `stale-events.${project}.ndjson`),
6610
- embeddingsCachePath: import_node_path37.default.join(baseDir, `embeddings.${project}.json`),
6611
- policyViolationsPath: import_node_path37.default.join(baseDir, `policy-violations.${project}.ndjson`)
7338
+ snapshotPath: import_node_path39.default.join(baseDir, `${project}.json`),
7339
+ errorsPath: import_node_path39.default.join(baseDir, `errors.${project}.ndjson`),
7340
+ staleEventsPath: import_node_path39.default.join(baseDir, `stale-events.${project}.ndjson`),
7341
+ embeddingsCachePath: import_node_path39.default.join(baseDir, `embeddings.${project}.json`),
7342
+ policyViolationsPath: import_node_path39.default.join(baseDir, `policy-violations.${project}.ndjson`)
6612
7343
  };
6613
7344
  }
6614
7345
  var Projects = class {
@@ -6650,15 +7381,15 @@ function parseExtraProjects(raw) {
6650
7381
  init_cjs_shims();
6651
7382
  var import_node_fs23 = require("fs");
6652
7383
  var import_node_os3 = __toESM(require("os"), 1);
6653
- var import_node_path38 = __toESM(require("path"), 1);
6654
- var import_types25 = require("@neat.is/types");
7384
+ var import_node_path40 = __toESM(require("path"), 1);
7385
+ var import_types27 = require("@neat.is/types");
6655
7386
  function neatHome() {
6656
7387
  const override = process.env.NEAT_HOME;
6657
- if (override && override.length > 0) return import_node_path38.default.resolve(override);
6658
- return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
7388
+ if (override && override.length > 0) return import_node_path40.default.resolve(override);
7389
+ return import_node_path40.default.join(import_node_os3.default.homedir(), ".neat");
6659
7390
  }
6660
7391
  function registryPath() {
6661
- return import_node_path38.default.join(neatHome(), "projects.json");
7392
+ return import_node_path40.default.join(neatHome(), "projects.json");
6662
7393
  }
6663
7394
  async function readRegistry() {
6664
7395
  const file = registryPath();
@@ -6672,7 +7403,7 @@ async function readRegistry() {
6672
7403
  throw err;
6673
7404
  }
6674
7405
  const parsed = JSON.parse(raw);
6675
- return import_types25.RegistryFileSchema.parse(parsed);
7406
+ return import_types27.RegistryFileSchema.parse(parsed);
6676
7407
  }
6677
7408
  async function getProject(name) {
6678
7409
  const reg = await readRegistry();
@@ -6893,11 +7624,11 @@ function registerRoutes(scope, ctx) {
6893
7624
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6894
7625
  const parsed = [];
6895
7626
  for (const c of candidates) {
6896
- const r = import_types26.DivergenceTypeSchema.safeParse(c);
7627
+ const r = import_types28.DivergenceTypeSchema.safeParse(c);
6897
7628
  if (!r.success) {
6898
7629
  return reply.code(400).send({
6899
7630
  error: `unknown divergence type "${c}"`,
6900
- allowed: import_types26.DivergenceTypeSchema.options
7631
+ allowed: import_types28.DivergenceTypeSchema.options
6901
7632
  });
6902
7633
  }
6903
7634
  parsed.push(r.data);
@@ -7116,7 +7847,7 @@ function registerRoutes(scope, ctx) {
7116
7847
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
7117
7848
  let violations = await log.readAll();
7118
7849
  if (req.query.severity) {
7119
- const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
7850
+ const sev = import_types28.PolicySeveritySchema.safeParse(req.query.severity);
7120
7851
  if (!sev.success) {
7121
7852
  return reply.code(400).send({
7122
7853
  error: "invalid severity",
@@ -7155,7 +7886,7 @@ function registerRoutes(scope, ctx) {
7155
7886
  scope.post("/policies/check", async (req, reply) => {
7156
7887
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7157
7888
  if (!proj) return;
7158
- const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7889
+ const parsed = import_types28.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7159
7890
  if (!parsed.success) {
7160
7891
  return reply.code(400).send({
7161
7892
  error: "invalid /policies/check body",
@@ -7418,7 +8149,7 @@ init_otel_grpc();
7418
8149
  // src/search.ts
7419
8150
  init_cjs_shims();
7420
8151
  var import_node_fs24 = require("fs");
7421
- var import_node_path41 = __toESM(require("path"), 1);
8152
+ var import_node_path43 = __toESM(require("path"), 1);
7422
8153
  var import_node_crypto3 = require("crypto");
7423
8154
  var DEFAULT_LIMIT = 10;
7424
8155
  var NOMIC_DIM = 768;
@@ -7453,6 +8184,18 @@ function embedText(node) {
7453
8184
  if (filePath) parts.push(`path=${filePath}`);
7454
8185
  break;
7455
8186
  }
8187
+ case "RouteNode": {
8188
+ const method = node.method;
8189
+ const tmpl = node.pathTemplate;
8190
+ if (method) parts.push(`method=${method}`);
8191
+ if (tmpl) parts.push(`path=${tmpl}`);
8192
+ break;
8193
+ }
8194
+ case "GraphQLOperationNode": {
8195
+ const opType = node.operationType;
8196
+ if (opType) parts.push(`operationType=${opType}`);
8197
+ break;
8198
+ }
7456
8199
  default:
7457
8200
  break;
7458
8201
  }
@@ -7557,7 +8300,7 @@ async function readCache(cachePath) {
7557
8300
  }
7558
8301
  }
7559
8302
  async function writeCache(cachePath, cache) {
7560
- await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(cachePath), { recursive: true });
8303
+ await import_node_fs24.promises.mkdir(import_node_path43.default.dirname(cachePath), { recursive: true });
7561
8304
  await import_node_fs24.promises.writeFile(cachePath, JSON.stringify(cache));
7562
8305
  }
7563
8306
  var VectorIndex = class {
@@ -7738,14 +8481,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
7738
8481
  async function main() {
7739
8482
  const baseDirEnv = process.env.NEAT_OUT_DIR;
7740
8483
  const legacyOutPath = process.env.NEAT_OUT_PATH;
7741
- const baseDir = baseDirEnv ? import_node_path42.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path42.default.resolve(import_node_path42.default.dirname(legacyOutPath)) : import_node_path42.default.resolve("./neat-out");
7742
- const defaultScanPath = import_node_path42.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
8484
+ const baseDir = baseDirEnv ? import_node_path44.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path44.default.resolve(import_node_path44.default.dirname(legacyOutPath)) : import_node_path44.default.resolve("./neat-out");
8485
+ const defaultScanPath = import_node_path44.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
7743
8486
  const registry = new Projects();
7744
8487
  await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
7745
8488
  for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
7746
8489
  const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
7747
8490
  const projectScan = process.env[envKey];
7748
- await bootProject(registry, name, projectScan ? import_node_path42.default.resolve(projectScan) : void 0, baseDir);
8491
+ await bootProject(registry, name, projectScan ? import_node_path44.default.resolve(projectScan) : void 0, baseDir);
7749
8492
  }
7750
8493
  const host = process.env.HOST ?? "0.0.0.0";
7751
8494
  const port = Number(process.env.PORT ?? 8080);