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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 path46 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
60
  for (const suffix of suffixes) {
61
- if (path43 === suffix || path43.endsWith(suffix)) {
61
+ if (path46 === suffix || path46.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_path42.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
192
+ return import_node_path42.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_path42, 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_path42 = __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,36 @@ 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
+ }
346
+ function hasWebsocketUpgradeHeader(attrs) {
347
+ const v = attrs["http.request.header.upgrade"];
348
+ const matches = (s) => typeof s === "string" && s.trim().toLowerCase() === "websocket";
349
+ if (Array.isArray(v)) return v.some(matches);
350
+ return matches(v);
351
+ }
352
+ function websocketChannelPathOf(attrs) {
353
+ const route = attrs["http.route"];
354
+ if (typeof route === "string" && route.length > 0) return route;
355
+ for (const key of ["url.path", "http.target"]) {
356
+ const v = attrs[key];
357
+ if (typeof v === "string" && v.length > 0) {
358
+ const q = v.indexOf("?");
359
+ const path46 = q === -1 ? v : v.slice(0, q);
360
+ if (path46.length > 0) return path46;
361
+ }
362
+ }
363
+ return void 0;
364
+ }
365
+ function websocketChannelOf(attrs) {
366
+ if (!hasWebsocketUpgradeHeader(attrs)) return void 0;
367
+ return websocketChannelPathOf(attrs);
368
+ }
337
369
  function parseOtlpRequest(body) {
338
370
  const out = [];
339
371
  for (const rs of body.resourceSpans ?? []) {
@@ -360,6 +392,14 @@ function parseOtlpRequest(body) {
360
392
  attributes: attrs,
361
393
  dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
362
394
  dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
395
+ messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
396
+ messagingDestination: messagingDestinationOf(attrs),
397
+ graphqlOperationName: typeof attrs["graphql.operation.name"] === "string" && attrs["graphql.operation.name"].length > 0 ? attrs["graphql.operation.name"] : void 0,
398
+ graphqlOperationType: typeof attrs["graphql.operation.type"] === "string" && attrs["graphql.operation.type"].length > 0 ? attrs["graphql.operation.type"] : void 0,
399
+ rpcSystem: typeof attrs["rpc.system"] === "string" && attrs["rpc.system"].length > 0 ? attrs["rpc.system"] : void 0,
400
+ rpcService: typeof attrs["rpc.service"] === "string" && attrs["rpc.service"].length > 0 ? attrs["rpc.service"] : void 0,
401
+ rpcMethod: typeof attrs["rpc.method"] === "string" && attrs["rpc.method"].length > 0 ? attrs["rpc.method"] : void 0,
402
+ websocketChannel: websocketChannelOf(attrs),
363
403
  statusCode: span.status?.code,
364
404
  errorMessage: span.status?.message,
365
405
  exception: extractExceptionFromEvents(span.events)
@@ -371,10 +411,10 @@ function parseOtlpRequest(body) {
371
411
  return out;
372
412
  }
373
413
  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");
414
+ const here = import_node_path43.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
415
+ const protoRoot = import_node_path43.default.resolve(here, "..", "proto");
376
416
  const root = new import_protobufjs.default.Root();
377
- root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
417
+ root.resolvePath = (_origin, target) => import_node_path43.default.resolve(protoRoot, target);
378
418
  root.loadSync(
379
419
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
380
420
  { keepCase: true }
@@ -419,7 +459,21 @@ async function buildOtelReceiver(opts) {
419
459
  logger: false,
420
460
  bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
421
461
  });
422
- mountBearerAuth(app, { token: opts.authToken, trustProxy: opts.trustProxy });
462
+ const REJECT_WARN_INTERVAL_MS = 6e4;
463
+ let lastRejectWarnAt = 0;
464
+ const warnRejectedOtlp = () => {
465
+ const now = Date.now();
466
+ if (now - lastRejectWarnAt < REJECT_WARN_INTERVAL_MS) return;
467
+ lastRejectWarnAt = now;
468
+ console.warn(
469
+ "[neatd] rejecting OTLP spans on /v1/traces \u2014 missing or invalid bearer token (set NEAT_OTEL_TOKEN on the instrumented app)"
470
+ );
471
+ };
472
+ mountBearerAuth(app, {
473
+ token: opts.authToken,
474
+ trustProxy: opts.trustProxy,
475
+ onReject: warnRejectedOtlp
476
+ });
423
477
  const queue = [];
424
478
  let draining = false;
425
479
  let drainPromise = Promise.resolve();
@@ -572,12 +626,12 @@ async function buildOtelReceiver(opts) {
572
626
  };
573
627
  return decorated;
574
628
  }
575
- var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
629
+ var import_node_path43, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
576
630
  var init_otel = __esm({
577
631
  "src/otel.ts"() {
578
632
  "use strict";
579
633
  init_cjs_shims();
580
- import_node_path40 = __toESM(require("path"), 1);
634
+ import_node_path43 = __toESM(require("path"), 1);
581
635
  import_node_url2 = require("url");
582
636
  import_fastify2 = __toESM(require("fastify"), 1);
583
637
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -593,7 +647,7 @@ var init_otel = __esm({
593
647
 
594
648
  // src/server.ts
595
649
  init_cjs_shims();
596
- var import_node_path42 = __toESM(require("path"), 1);
650
+ var import_node_path45 = __toESM(require("path"), 1);
597
651
 
598
652
  // src/graph.ts
599
653
  init_cjs_shims();
@@ -617,7 +671,7 @@ function getGraph(project = DEFAULT_PROJECT) {
617
671
  init_cjs_shims();
618
672
  var import_fastify = __toESM(require("fastify"), 1);
619
673
  var import_cors = __toESM(require("@fastify/cors"), 1);
620
- var import_types26 = require("@neat.is/types");
674
+ var import_types29 = require("@neat.is/types");
621
675
 
622
676
  // src/extend/index.ts
623
677
  init_cjs_shims();
@@ -1360,19 +1414,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1360
1414
  function longestIncomingWalk(graph, start, maxDepth) {
1361
1415
  let best = { path: [start], edges: [] };
1362
1416
  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] };
1417
+ function step(node, path46, edges) {
1418
+ if (path46.length > best.path.length) {
1419
+ best = { path: [...path46], edges: [...edges] };
1366
1420
  }
1367
- if (path43.length - 1 >= maxDepth) return;
1421
+ if (path46.length - 1 >= maxDepth) return;
1368
1422
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1369
1423
  for (const [srcId, edge] of incoming) {
1370
1424
  if (visited.has(srcId)) continue;
1371
1425
  visited.add(srcId);
1372
- path43.push(srcId);
1426
+ path46.push(srcId);
1373
1427
  edges.push(edge);
1374
- step(srcId, path43, edges);
1375
- path43.pop();
1428
+ step(srcId, path46, edges);
1429
+ path46.pop();
1376
1430
  edges.pop();
1377
1431
  visited.delete(srcId);
1378
1432
  }
@@ -1380,14 +1434,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
1380
1434
  step(start, [start], []);
1381
1435
  return best;
1382
1436
  }
1383
- function databaseRootCauseShape(graph, origin, walk) {
1437
+ function databaseRootCauseShape(graph, origin, walk3) {
1384
1438
  const targetDb = origin;
1385
1439
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1386
1440
  if (candidatePairs.length === 0) return null;
1387
- for (const id of walk.path) {
1441
+ for (const id of walk3.path) {
1388
1442
  const owner = resolveOwningService(graph, id);
1389
1443
  if (!owner) continue;
1390
- const { id: serviceId3, svc } = owner;
1444
+ const { id: serviceId4, svc } = owner;
1391
1445
  const deps = svc.dependencies ?? {};
1392
1446
  for (const pair of candidatePairs) {
1393
1447
  const declared = deps[pair.driver];
@@ -1400,7 +1454,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1400
1454
  );
1401
1455
  if (!result.compatible) {
1402
1456
  return {
1403
- rootCauseNode: serviceId3,
1457
+ rootCauseNode: serviceId4,
1404
1458
  rootCauseReason: result.reason ?? "incompatible driver",
1405
1459
  ...result.minDriverVersion ? {
1406
1460
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1411,11 +1465,11 @@ function databaseRootCauseShape(graph, origin, walk) {
1411
1465
  }
1412
1466
  return null;
1413
1467
  }
1414
- function serviceRootCauseShape(graph, _origin, walk) {
1415
- for (const id of walk.path) {
1468
+ function serviceRootCauseShape(graph, _origin, walk3) {
1469
+ for (const id of walk3.path) {
1416
1470
  const owner = resolveOwningService(graph, id);
1417
1471
  if (!owner) continue;
1418
- const { id: serviceId3, svc } = owner;
1472
+ const { id: serviceId4, svc } = owner;
1419
1473
  const deps = svc.dependencies ?? {};
1420
1474
  const serviceNodeEngine = svc.nodeEngine;
1421
1475
  for (const constraint of nodeEngineConstraints()) {
@@ -1424,7 +1478,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1424
1478
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1425
1479
  if (!result.compatible && result.reason) {
1426
1480
  return {
1427
- rootCauseNode: serviceId3,
1481
+ rootCauseNode: serviceId4,
1428
1482
  rootCauseReason: result.reason,
1429
1483
  ...result.requiredNodeVersion ? {
1430
1484
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1439,7 +1493,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1439
1493
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1440
1494
  if (!result.compatible && result.reason) {
1441
1495
  return {
1442
- rootCauseNode: serviceId3,
1496
+ rootCauseNode: serviceId4,
1443
1497
  rootCauseReason: result.reason,
1444
1498
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1445
1499
  };
@@ -1448,10 +1502,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
1448
1502
  }
1449
1503
  return null;
1450
1504
  }
1451
- function fileRootCauseShape(graph, origin, walk) {
1505
+ function fileRootCauseShape(graph, origin, walk3) {
1452
1506
  const owner = resolveOwningService(graph, origin.id);
1453
1507
  if (!owner) return null;
1454
- return serviceRootCauseShape(graph, owner.svc, walk);
1508
+ return serviceRootCauseShape(graph, owner.svc, walk3);
1455
1509
  }
1456
1510
  var rootCauseShapes = {
1457
1511
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
@@ -1463,16 +1517,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1463
1517
  const origin = graph.getNodeAttributes(errorNodeId);
1464
1518
  const shape = rootCauseShapes[origin.type];
1465
1519
  if (shape) {
1466
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1467
- const match = shape(graph, origin, walk);
1520
+ const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1521
+ const match = shape(graph, origin, walk3);
1468
1522
  if (match) {
1469
1523
  const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1470
1524
  return import_types.RootCauseResultSchema.parse({
1471
1525
  rootCauseNode: match.rootCauseNode,
1472
1526
  rootCauseReason: reason,
1473
- traversalPath: walk.path,
1474
- edgeProvenances: walk.edges.map((e) => e.provenance),
1475
- confidence: confidenceFromMix(walk.edges),
1527
+ traversalPath: walk3.path,
1528
+ edgeProvenances: walk3.edges.map((e) => e.provenance),
1529
+ confidence: confidenceFromMix(walk3.edges),
1476
1530
  fixRecommendation: match.fixRecommendation
1477
1531
  });
1478
1532
  }
@@ -1530,9 +1584,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1530
1584
  function isFailingCallEdge(e) {
1531
1585
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1532
1586
  }
1533
- function callSourcesForService(graph, serviceId3) {
1534
- const ids = [serviceId3];
1535
- for (const edgeId of graph.outboundEdges(serviceId3)) {
1587
+ function callSourcesForService(graph, serviceId4) {
1588
+ const ids = [serviceId4];
1589
+ for (const edgeId of graph.outboundEdges(serviceId4)) {
1536
1590
  const e = graph.getEdgeAttributes(edgeId);
1537
1591
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1538
1592
  const tgt = graph.getNodeAttributes(e.target);
@@ -1549,9 +1603,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1549
1603
  }
1550
1604
  return id < curId;
1551
1605
  }
1552
- function dominantFailingCall(graph, serviceId3, visited) {
1606
+ function dominantFailingCall(graph, serviceId4, visited) {
1553
1607
  let best = null;
1554
- for (const src of callSourcesForService(graph, serviceId3)) {
1608
+ for (const src of callSourcesForService(graph, serviceId4)) {
1555
1609
  for (const edgeId of graph.outboundEdges(src)) {
1556
1610
  const e = graph.getEdgeAttributes(edgeId);
1557
1611
  if (!isFailingCallEdge(e)) continue;
@@ -1566,26 +1620,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
1566
1620
  return best;
1567
1621
  }
1568
1622
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1569
- const path43 = [originServiceId];
1623
+ const path46 = [originServiceId];
1570
1624
  const edges = [];
1571
1625
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1572
1626
  let current = originServiceId;
1573
1627
  for (let depth = 0; depth < maxDepth; depth++) {
1574
1628
  const hop = dominantFailingCall(graph, current, visited);
1575
1629
  if (!hop) break;
1576
- path43.push(hop.nextService);
1630
+ path46.push(hop.nextService);
1577
1631
  edges.push(hop.edge);
1578
1632
  visited.add(hop.nextService);
1579
1633
  current = hop.nextService;
1580
1634
  }
1581
1635
  if (edges.length === 0) return null;
1582
- return { path: path43, edges, culprit: current };
1636
+ return { path: path46, edges, culprit: current };
1583
1637
  }
1584
1638
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1585
1639
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1586
1640
  if (!chain) return null;
1587
1641
  const culprit = chain.culprit;
1588
- const path43 = [...chain.path];
1642
+ const path46 = [...chain.path];
1589
1643
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1590
1644
  const baseConfidence = confidenceFromMix(chain.edges);
1591
1645
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1593,14 +1647,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1593
1647
  if (loc) {
1594
1648
  let rootCauseNode = culprit;
1595
1649
  if (loc.fileNode) {
1596
- path43.push(loc.fileNode);
1650
+ path46.push(loc.fileNode);
1597
1651
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1598
1652
  rootCauseNode = loc.fileNode;
1599
1653
  }
1600
1654
  return import_types.RootCauseResultSchema.parse({
1601
1655
  rootCauseNode,
1602
1656
  rootCauseReason: loc.rootCauseReason,
1603
- traversalPath: path43,
1657
+ traversalPath: path46,
1604
1658
  edgeProvenances,
1605
1659
  confidence,
1606
1660
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1612,7 +1666,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1612
1666
  return import_types.RootCauseResultSchema.parse({
1613
1667
  rootCauseNode: culprit,
1614
1668
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1615
- traversalPath: path43,
1669
+ traversalPath: path46,
1616
1670
  edgeProvenances,
1617
1671
  confidence,
1618
1672
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -1792,6 +1846,11 @@ function nodeIsFrontier(graph, nodeId) {
1792
1846
  const attrs = graph.getNodeAttributes(nodeId);
1793
1847
  return attrs.type === import_types2.NodeType.FrontierNode;
1794
1848
  }
1849
+ function nodeIsWebsocketChannel(graph, nodeId) {
1850
+ if (!graph.hasNode(nodeId)) return false;
1851
+ const attrs = graph.getNodeAttributes(nodeId);
1852
+ return attrs.type === import_types2.NodeType.WebSocketChannelNode;
1853
+ }
1795
1854
  function clampConfidence(n) {
1796
1855
  if (!Number.isFinite(n)) return 0;
1797
1856
  return Math.max(0, Math.min(1, n));
@@ -1832,7 +1891,7 @@ function detectMissingDivergences(graph, bucket) {
1832
1891
  });
1833
1892
  }
1834
1893
  }
1835
- if (bucket.observed && !bucket.extracted) {
1894
+ if (bucket.observed && !bucket.extracted && !nodeIsWebsocketChannel(graph, bucket.target)) {
1836
1895
  out.push({
1837
1896
  type: "missing-extracted",
1838
1897
  source: bucket.source,
@@ -2824,10 +2883,82 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2824
2883
  ]);
2825
2884
  var WIRE_SPAN_KIND_CLIENT = 3;
2826
2885
  var WIRE_SPAN_KIND_PRODUCER = 4;
2886
+ var WIRE_SPAN_KIND_CONSUMER = 5;
2827
2887
  function spanMintsObservedEdge(kind) {
2828
2888
  if (kind === void 0 || kind === 0) return true;
2829
2889
  return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
2830
2890
  }
2891
+ function spanMintsMessagingEdge(kind) {
2892
+ return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
2893
+ }
2894
+ function spanServesGraphqlOperation(kind) {
2895
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2896
+ }
2897
+ function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
2898
+ const id = (0, import_types4.graphqlOperationId)(serviceName, operationType, operationName);
2899
+ if (graph.hasNode(id)) return id;
2900
+ const node = {
2901
+ id,
2902
+ type: import_types4.NodeType.GraphQLOperationNode,
2903
+ name: operationName,
2904
+ service: serviceName,
2905
+ operationType: operationType.toLowerCase(),
2906
+ operationName,
2907
+ discoveredVia: "otel"
2908
+ };
2909
+ graph.addNode(id, node);
2910
+ return id;
2911
+ }
2912
+ function spanServesGrpcMethod(kind) {
2913
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2914
+ }
2915
+ function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
2916
+ const id = (0, import_types4.grpcMethodId)(rpcService, rpcMethod);
2917
+ if (graph.hasNode(id)) return id;
2918
+ const node = {
2919
+ id,
2920
+ type: import_types4.NodeType.GrpcMethodNode,
2921
+ name: `${rpcService}/${rpcMethod}`,
2922
+ rpcService,
2923
+ rpcMethod,
2924
+ discoveredVia: "otel"
2925
+ };
2926
+ graph.addNode(id, node);
2927
+ return id;
2928
+ }
2929
+ function spanServesWebsocketChannel(kind) {
2930
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
2931
+ }
2932
+ function ensureWebsocketChannelNode(graph, serviceName, channel) {
2933
+ const id = (0, import_types4.websocketChannelId)(serviceName, channel);
2934
+ if (graph.hasNode(id)) return id;
2935
+ const node = {
2936
+ id,
2937
+ type: import_types4.NodeType.WebSocketChannelNode,
2938
+ name: channel,
2939
+ service: serviceName,
2940
+ channel,
2941
+ discoveredVia: "otel"
2942
+ };
2943
+ graph.addNode(id, node);
2944
+ return id;
2945
+ }
2946
+ function messagingDestinationKind(system) {
2947
+ return `${system}-topic`;
2948
+ }
2949
+ function ensureMessagingDestinationNode(graph, system, destination) {
2950
+ const id = (0, import_types4.infraId)(messagingDestinationKind(system), destination);
2951
+ if (graph.hasNode(id)) return id;
2952
+ const node = {
2953
+ id,
2954
+ type: import_types4.NodeType.InfraNode,
2955
+ name: destination,
2956
+ provider: "self",
2957
+ kind: messagingDestinationKind(system)
2958
+ };
2959
+ graph.addNode(id, node);
2960
+ return id;
2961
+ }
2831
2962
  var PARENT_SPAN_CACHE_SIZE = 1e4;
2832
2963
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
2833
2964
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -2921,6 +3052,21 @@ function ensureDatabaseNode(graph, host, engine) {
2921
3052
  graph.addNode(id, node);
2922
3053
  return id;
2923
3054
  }
3055
+ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
3056
+ const id = (0, import_types4.localDatabaseId)(serviceName, name);
3057
+ if (graph.hasNode(id)) return id;
3058
+ const node = {
3059
+ id,
3060
+ type: import_types4.NodeType.DatabaseNode,
3061
+ name,
3062
+ engine,
3063
+ engineVersion: "unknown",
3064
+ compatibleDrivers: [],
3065
+ discoveredVia: "otel"
3066
+ };
3067
+ graph.addNode(id, node);
3068
+ return id;
3069
+ }
2924
3070
  function ensureFrontierNode(graph, host, ts) {
2925
3071
  const id = frontierIdFor(host);
2926
3072
  if (graph.hasNode(id)) {
@@ -3147,10 +3293,21 @@ async function handleSpan(ctx, span) {
3147
3293
  let affectedNode = sourceId;
3148
3294
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
3149
3295
  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);
3296
+ if (mintsFromCallerSide) {
3297
+ const host = pickAddress(span);
3298
+ let targetId;
3299
+ if (host) {
3300
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
3301
+ targetId = (0, import_types4.databaseId)(host);
3302
+ } else {
3303
+ const localName = span.dbName ?? span.dbSystem;
3304
+ targetId = ensureLocalDatabaseNode(
3305
+ ctx.graph,
3306
+ span.service,
3307
+ localName,
3308
+ span.dbSystem
3309
+ );
3310
+ }
3154
3311
  const result = upsertObservedEdge(
3155
3312
  ctx.graph,
3156
3313
  import_types4.EdgeType.CONNECTS_TO,
@@ -3162,6 +3319,68 @@ async function handleSpan(ctx, span) {
3162
3319
  );
3163
3320
  if (result) affectedNode = targetId;
3164
3321
  }
3322
+ } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
3323
+ const targetId = ensureMessagingDestinationNode(
3324
+ ctx.graph,
3325
+ span.messagingSystem,
3326
+ span.messagingDestination
3327
+ );
3328
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types4.EdgeType.CONSUMES_FROM : import_types4.EdgeType.PUBLISHES_TO;
3329
+ const result = upsertObservedEdge(
3330
+ ctx.graph,
3331
+ edgeType,
3332
+ observedSource(),
3333
+ targetId,
3334
+ ts,
3335
+ isError,
3336
+ callSiteEvidence
3337
+ );
3338
+ if (result) affectedNode = targetId;
3339
+ } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
3340
+ const targetId = ensureGraphqlOperationNode(
3341
+ ctx.graph,
3342
+ span.service,
3343
+ span.graphqlOperationType,
3344
+ span.graphqlOperationName
3345
+ );
3346
+ const result = upsertObservedEdge(
3347
+ ctx.graph,
3348
+ import_types4.EdgeType.CONTAINS,
3349
+ observedSource(),
3350
+ targetId,
3351
+ ts,
3352
+ isError,
3353
+ callSiteEvidence
3354
+ );
3355
+ if (result) affectedNode = targetId;
3356
+ } else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
3357
+ const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
3358
+ const result = upsertObservedEdge(
3359
+ ctx.graph,
3360
+ import_types4.EdgeType.CONTAINS,
3361
+ observedSource(),
3362
+ targetId,
3363
+ ts,
3364
+ isError,
3365
+ callSiteEvidence
3366
+ );
3367
+ if (result) affectedNode = targetId;
3368
+ } else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
3369
+ const targetId = ensureWebsocketChannelNode(
3370
+ ctx.graph,
3371
+ span.service,
3372
+ span.websocketChannel
3373
+ );
3374
+ const result = upsertObservedEdge(
3375
+ ctx.graph,
3376
+ import_types4.EdgeType.CONNECTS_TO,
3377
+ observedSource(),
3378
+ targetId,
3379
+ ts,
3380
+ isError,
3381
+ callSiteEvidence
3382
+ );
3383
+ if (result) affectedNode = targetId;
3165
3384
  } else {
3166
3385
  const host = pickAddress(span);
3167
3386
  let resolvedViaAddress = false;
@@ -3271,29 +3490,29 @@ function promoteFrontierNodes(graph, opts = {}) {
3271
3490
  toPromote.push({ frontierId: id, serviceId: target });
3272
3491
  });
3273
3492
  let promoted = 0;
3274
- for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
3493
+ for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
3275
3494
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
3276
3495
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
3277
3496
  if (!gate.allowed) {
3278
3497
  continue;
3279
3498
  }
3280
3499
  }
3281
- rewireFrontierEdges(graph, frontierId2, serviceId3);
3500
+ rewireFrontierEdges(graph, frontierId2, serviceId4);
3282
3501
  graph.dropNode(frontierId2);
3283
3502
  promoted++;
3284
3503
  }
3285
3504
  return promoted;
3286
3505
  }
3287
- function rewireFrontierEdges(graph, frontierId2, serviceId3) {
3506
+ function rewireFrontierEdges(graph, frontierId2, serviceId4) {
3288
3507
  const inbound = [...graph.inboundEdges(frontierId2)];
3289
3508
  const outbound = [...graph.outboundEdges(frontierId2)];
3290
3509
  for (const edgeId of inbound) {
3291
3510
  const edge = graph.getEdgeAttributes(edgeId);
3292
- rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
3511
+ rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
3293
3512
  }
3294
3513
  for (const edgeId of outbound) {
3295
3514
  const edge = graph.getEdgeAttributes(edgeId);
3296
- rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
3515
+ rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
3297
3516
  }
3298
3517
  }
3299
3518
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -4055,9 +4274,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
4055
4274
  "StatefulSet",
4056
4275
  "DaemonSet"
4057
4276
  ]);
4058
- function addAliases(graph, serviceId3, candidates) {
4059
- if (!graph.hasNode(serviceId3)) return;
4060
- const node = graph.getNodeAttributes(serviceId3);
4277
+ function addAliases(graph, serviceId4, candidates) {
4278
+ if (!graph.hasNode(serviceId4)) return;
4279
+ const node = graph.getNodeAttributes(serviceId4);
4061
4280
  if (node.type !== import_types7.NodeType.ServiceNode) return;
4062
4281
  const set = new Set(node.aliases ?? []);
4063
4282
  for (const c of candidates) {
@@ -4067,7 +4286,7 @@ function addAliases(graph, serviceId3, candidates) {
4067
4286
  }
4068
4287
  if (set.size === 0) return;
4069
4288
  const updated = { ...node, aliases: [...set].sort() };
4070
- graph.replaceNodeAttributes(serviceId3, updated);
4289
+ graph.replaceNodeAttributes(serviceId4, updated);
4071
4290
  }
4072
4291
  function indexServicesByName(services) {
4073
4292
  const map = /* @__PURE__ */ new Map();
@@ -4100,12 +4319,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
4100
4319
  }
4101
4320
  if (!compose?.services) return;
4102
4321
  for (const [composeName, svc] of Object.entries(compose.services)) {
4103
- const serviceId3 = serviceIndex.get(composeName);
4104
- if (!serviceId3) continue;
4322
+ const serviceId4 = serviceIndex.get(composeName);
4323
+ if (!serviceId4) continue;
4105
4324
  const aliases = /* @__PURE__ */ new Set([composeName]);
4106
4325
  if (svc.container_name) aliases.add(svc.container_name);
4107
4326
  if (svc.hostname) aliases.add(svc.hostname);
4108
- addAliases(graph, serviceId3, aliases);
4327
+ addAliases(graph, serviceId4, aliases);
4109
4328
  }
4110
4329
  }
4111
4330
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -4218,16 +4437,28 @@ init_cjs_shims();
4218
4437
  var import_node_fs12 = require("fs");
4219
4438
  var import_node_path12 = __toESM(require("path"), 1);
4220
4439
  var import_types8 = require("@neat.is/types");
4440
+ function buildServiceHostIndex(services) {
4441
+ const knownHosts = /* @__PURE__ */ new Set();
4442
+ const hostToNodeId = /* @__PURE__ */ new Map();
4443
+ for (const service of services) {
4444
+ const base = import_node_path12.default.basename(service.dir);
4445
+ knownHosts.add(base);
4446
+ knownHosts.add(service.pkg.name);
4447
+ hostToNodeId.set(base, service.node.id);
4448
+ hostToNodeId.set(service.pkg.name, service.node.id);
4449
+ }
4450
+ return { knownHosts, hostToNodeId };
4451
+ }
4221
4452
  async function walkSourceFiles(dir) {
4222
4453
  const out = [];
4223
- async function walk(current) {
4454
+ async function walk3(current) {
4224
4455
  const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
4225
4456
  for (const entry of entries) {
4226
4457
  const full = import_node_path12.default.join(current, entry.name);
4227
4458
  if (entry.isDirectory()) {
4228
4459
  if (IGNORED_DIRS.has(entry.name)) continue;
4229
4460
  if (await isPythonVenvDir(full)) continue;
4230
- await walk(full);
4461
+ await walk3(full);
4231
4462
  } 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
4463
  // would attribute our instrumentation imports to the user's service.
4233
4464
  !isNeatAuthoredSourceFile(entry.name)) {
@@ -4235,7 +4466,7 @@ async function walkSourceFiles(dir) {
4235
4466
  }
4236
4467
  }
4237
4468
  }
4238
- await walk(dir);
4469
+ await walk3(dir);
4239
4470
  return out;
4240
4471
  }
4241
4472
  async function loadSourceFiles(dir) {
@@ -5329,20 +5560,20 @@ var import_node_path23 = __toESM(require("path"), 1);
5329
5560
  var import_types11 = require("@neat.is/types");
5330
5561
  async function walkConfigFiles(dir) {
5331
5562
  const out = [];
5332
- async function walk(current) {
5563
+ async function walk3(current) {
5333
5564
  const entries = await import_node_fs16.promises.readdir(current, { withFileTypes: true });
5334
5565
  for (const entry of entries) {
5335
5566
  const full = import_node_path23.default.join(current, entry.name);
5336
5567
  if (entry.isDirectory()) {
5337
5568
  if (IGNORED_DIRS.has(entry.name)) continue;
5338
5569
  if (await isPythonVenvDir(full)) continue;
5339
- await walk(full);
5570
+ await walk3(full);
5340
5571
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
5341
5572
  out.push(full);
5342
5573
  }
5343
5574
  }
5344
5575
  }
5345
- await walk(dir);
5576
+ await walk3(dir);
5346
5577
  return out;
5347
5578
  }
5348
5579
  async function addConfigNodes(graph, services, scanPath) {
@@ -5390,17 +5621,468 @@ async function addConfigNodes(graph, services, scanPath) {
5390
5621
  return { nodesAdded, edgesAdded };
5391
5622
  }
5392
5623
 
5624
+ // src/extract/routes.ts
5625
+ init_cjs_shims();
5626
+ var import_node_path24 = __toESM(require("path"), 1);
5627
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
5628
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
5629
+ var import_types12 = require("@neat.is/types");
5630
+ var PARSE_CHUNK2 = 16384;
5631
+ function parseSource2(parser, source) {
5632
+ return parser.parse(
5633
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5634
+ );
5635
+ }
5636
+ function makeJsParser2() {
5637
+ const p = new import_tree_sitter2.default();
5638
+ p.setLanguage(import_tree_sitter_javascript2.default);
5639
+ return p;
5640
+ }
5641
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
5642
+ "get",
5643
+ "post",
5644
+ "put",
5645
+ "patch",
5646
+ "delete",
5647
+ "options",
5648
+ "head",
5649
+ "all"
5650
+ ]);
5651
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5652
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5653
+ function canonicalizeTemplate(raw) {
5654
+ let p = raw.split("?")[0].split("#")[0];
5655
+ if (!p.startsWith("/")) p = "/" + p;
5656
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
5657
+ return p;
5658
+ }
5659
+ function isDynamicSegment(seg) {
5660
+ if (seg.length === 0) return false;
5661
+ if (seg.includes(":")) return true;
5662
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
5663
+ if (/^\d+$/.test(seg)) return true;
5664
+ 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;
5665
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
5666
+ return false;
5667
+ }
5668
+ function normalizePathTemplate(raw) {
5669
+ const canonical = canonicalizeTemplate(raw);
5670
+ const segments = canonical.split("/").filter((s) => s.length > 0);
5671
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
5672
+ return "/" + normalised.join("/");
5673
+ }
5674
+ function walk(node, visit) {
5675
+ visit(node);
5676
+ for (let i = 0; i < node.namedChildCount; i++) {
5677
+ const child = node.namedChild(i);
5678
+ if (child) walk(child, visit);
5679
+ }
5680
+ }
5681
+ function staticStringText(node) {
5682
+ if (node.type === "string") {
5683
+ for (let i = 0; i < node.namedChildCount; i++) {
5684
+ const child = node.namedChild(i);
5685
+ if (child?.type === "string_fragment") return child.text;
5686
+ }
5687
+ return "";
5688
+ }
5689
+ if (node.type === "template_string") {
5690
+ for (let i = 0; i < node.namedChildCount; i++) {
5691
+ if (node.namedChild(i)?.type === "template_substitution") return null;
5692
+ }
5693
+ const raw = node.text;
5694
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5695
+ }
5696
+ return null;
5697
+ }
5698
+ function objectStringProp(objNode, key) {
5699
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5700
+ const pair = objNode.namedChild(i);
5701
+ if (!pair || pair.type !== "pair") continue;
5702
+ const k = pair.childForFieldName("key");
5703
+ if (!k) continue;
5704
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
5705
+ if (kText !== key) continue;
5706
+ const v = pair.childForFieldName("value");
5707
+ if (v) return staticStringText(v);
5708
+ }
5709
+ return null;
5710
+ }
5711
+ function fastifyRouteMethods(objNode) {
5712
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5713
+ const pair = objNode.namedChild(i);
5714
+ if (!pair || pair.type !== "pair") continue;
5715
+ const k = pair.childForFieldName("key");
5716
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
5717
+ if (kText !== "method") continue;
5718
+ const v = pair.childForFieldName("value");
5719
+ if (!v) return [];
5720
+ if (v.type === "string" || v.type === "template_string") {
5721
+ const s = staticStringText(v);
5722
+ return s ? [s.toUpperCase()] : [];
5723
+ }
5724
+ if (v.type === "array") {
5725
+ const out = [];
5726
+ for (let j = 0; j < v.namedChildCount; j++) {
5727
+ const el = v.namedChild(j);
5728
+ if (el && (el.type === "string" || el.type === "template_string")) {
5729
+ const s = staticStringText(el);
5730
+ if (s) out.push(s.toUpperCase());
5731
+ }
5732
+ }
5733
+ return out;
5734
+ }
5735
+ }
5736
+ return [];
5737
+ }
5738
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
5739
+ const tree = parseSource2(parser, source);
5740
+ const out = [];
5741
+ const framework = hasExpress ? "express" : "fastify";
5742
+ walk(tree.rootNode, (node) => {
5743
+ if (node.type !== "call_expression") return;
5744
+ const fn = node.childForFieldName("function");
5745
+ if (!fn || fn.type !== "member_expression") return;
5746
+ const prop = fn.childForFieldName("property");
5747
+ if (!prop) return;
5748
+ const method = prop.text.toLowerCase();
5749
+ const args = node.childForFieldName("arguments");
5750
+ const first = args?.namedChild(0);
5751
+ if (!first) return;
5752
+ const line = node.startPosition.row + 1;
5753
+ if (ROUTER_METHODS.has(method)) {
5754
+ const p = staticStringText(first);
5755
+ if (p && p.startsWith("/")) {
5756
+ out.push({
5757
+ method: method === "all" ? "ALL" : method.toUpperCase(),
5758
+ pathTemplate: canonicalizeTemplate(p),
5759
+ line,
5760
+ framework
5761
+ });
5762
+ }
5763
+ return;
5764
+ }
5765
+ if (method === "route" && hasFastify && first.type === "object") {
5766
+ const url = objectStringProp(first, "url");
5767
+ if (!url || !url.startsWith("/")) return;
5768
+ const methods = fastifyRouteMethods(first);
5769
+ const list = methods.length > 0 ? methods : ["ALL"];
5770
+ for (const m of list) {
5771
+ out.push({
5772
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5773
+ pathTemplate: canonicalizeTemplate(url),
5774
+ line,
5775
+ framework: "fastify"
5776
+ });
5777
+ }
5778
+ }
5779
+ });
5780
+ return out;
5781
+ }
5782
+ function segmentsOf(relFile) {
5783
+ return toPosix2(relFile).split("/").filter((s) => s.length > 0);
5784
+ }
5785
+ function isNextAppRouteFile(relFile) {
5786
+ const segs = segmentsOf(relFile);
5787
+ if (!segs.includes("app")) return false;
5788
+ const base = segs[segs.length - 1] ?? "";
5789
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
5790
+ }
5791
+ function isNextPagesApiFile(relFile) {
5792
+ const segs = segmentsOf(relFile);
5793
+ const pagesIdx = segs.indexOf("pages");
5794
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
5795
+ const base = segs[segs.length - 1] ?? "";
5796
+ if (/^_(app|document|middleware)\./.test(base)) return false;
5797
+ return JS_ROUTE_EXTENSIONS.has(import_node_path24.default.extname(base));
5798
+ }
5799
+ function nextSegment(seg) {
5800
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
5801
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
5802
+ if (catchAll) return ":" + catchAll[1];
5803
+ const dynamic = seg.match(/^\[(.+?)\]$/);
5804
+ if (dynamic) return ":" + dynamic[1];
5805
+ return seg;
5806
+ }
5807
+ function nextAppPathTemplate(relFile) {
5808
+ const segs = segmentsOf(relFile);
5809
+ const appIdx = segs.lastIndexOf("app");
5810
+ const between = segs.slice(appIdx + 1, segs.length - 1);
5811
+ const parts = [];
5812
+ for (const seg of between) {
5813
+ const mapped = nextSegment(seg);
5814
+ if (mapped !== null) parts.push(mapped);
5815
+ }
5816
+ return "/" + parts.join("/");
5817
+ }
5818
+ function nextPagesApiPathTemplate(relFile) {
5819
+ const segs = segmentsOf(relFile);
5820
+ const pagesIdx = segs.indexOf("pages");
5821
+ const rest = segs.slice(pagesIdx + 1);
5822
+ const parts = [];
5823
+ for (let i = 0; i < rest.length; i++) {
5824
+ let seg = rest[i];
5825
+ if (i === rest.length - 1) {
5826
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
5827
+ if (seg === "index") continue;
5828
+ }
5829
+ const mapped = nextSegment(seg);
5830
+ if (mapped !== null) parts.push(mapped);
5831
+ }
5832
+ return "/" + parts.join("/");
5833
+ }
5834
+ function nextAppMethods(root) {
5835
+ const out = [];
5836
+ walk(root, (node) => {
5837
+ if (node.type !== "export_statement") return;
5838
+ const decl = node.childForFieldName("declaration");
5839
+ if (!decl) return;
5840
+ const line = node.startPosition.row + 1;
5841
+ if (decl.type === "function_declaration") {
5842
+ const name = decl.childForFieldName("name")?.text;
5843
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5844
+ return;
5845
+ }
5846
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
5847
+ for (let i = 0; i < decl.namedChildCount; i++) {
5848
+ const d = decl.namedChild(i);
5849
+ if (d?.type !== "variable_declarator") continue;
5850
+ const name = d.childForFieldName("name")?.text;
5851
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
5852
+ }
5853
+ }
5854
+ });
5855
+ return out;
5856
+ }
5857
+ function nextRoutesFromFile(source, relFile, parser) {
5858
+ if (isNextAppRouteFile(relFile)) {
5859
+ const tree = parseSource2(parser, source);
5860
+ const template = nextAppPathTemplate(relFile);
5861
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
5862
+ method,
5863
+ pathTemplate: canonicalizeTemplate(template),
5864
+ line,
5865
+ framework: "next"
5866
+ }));
5867
+ }
5868
+ if (isNextPagesApiFile(relFile)) {
5869
+ return [
5870
+ {
5871
+ method: "ALL",
5872
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
5873
+ line: 1,
5874
+ framework: "next"
5875
+ }
5876
+ ];
5877
+ }
5878
+ return [];
5879
+ }
5880
+ async function addRoutes(graph, services) {
5881
+ const jsParser = makeJsParser2();
5882
+ let nodesAdded = 0;
5883
+ let edgesAdded = 0;
5884
+ for (const service of services) {
5885
+ const deps = {
5886
+ ...service.pkg.dependencies ?? {},
5887
+ ...service.pkg.devDependencies ?? {}
5888
+ };
5889
+ const hasExpress = deps["express"] !== void 0;
5890
+ const hasFastify = deps["fastify"] !== void 0;
5891
+ const hasNext = deps["next"] !== void 0;
5892
+ if (!hasExpress && !hasFastify && !hasNext) continue;
5893
+ const files = await loadSourceFiles(service.dir);
5894
+ for (const file of files) {
5895
+ if (isTestPath(file.path)) continue;
5896
+ if (!JS_ROUTE_EXTENSIONS.has(import_node_path24.default.extname(file.path))) continue;
5897
+ const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
5898
+ let routes;
5899
+ try {
5900
+ if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5901
+ routes = nextRoutesFromFile(file.content, relFile, jsParser);
5902
+ } else if (hasExpress || hasFastify) {
5903
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
5904
+ } else {
5905
+ routes = [];
5906
+ }
5907
+ } catch (err) {
5908
+ recordExtractionError("route extraction", file.path, err);
5909
+ continue;
5910
+ }
5911
+ if (routes.length === 0) continue;
5912
+ for (const route of routes) {
5913
+ const rid = (0, import_types12.routeId)(service.pkg.name, route.method, route.pathTemplate);
5914
+ if (!graph.hasNode(rid)) {
5915
+ const node = {
5916
+ id: rid,
5917
+ type: import_types12.NodeType.RouteNode,
5918
+ name: `${route.method} ${route.pathTemplate}`,
5919
+ service: service.pkg.name,
5920
+ method: route.method,
5921
+ pathTemplate: route.pathTemplate,
5922
+ path: relFile,
5923
+ line: route.line,
5924
+ framework: route.framework,
5925
+ discoveredVia: "static"
5926
+ };
5927
+ graph.addNode(rid, node);
5928
+ nodesAdded++;
5929
+ }
5930
+ const containsId = (0, import_types12.extractedEdgeId)(service.node.id, rid, import_types12.EdgeType.CONTAINS);
5931
+ if (!graph.hasEdge(containsId)) {
5932
+ const edge = {
5933
+ id: containsId,
5934
+ source: service.node.id,
5935
+ target: rid,
5936
+ type: import_types12.EdgeType.CONTAINS,
5937
+ provenance: import_types12.Provenance.EXTRACTED,
5938
+ confidence: (0, import_types12.confidenceForExtracted)("structural"),
5939
+ evidence: {
5940
+ file: relFile,
5941
+ line: route.line,
5942
+ snippet: snippet(file.content, route.line)
5943
+ }
5944
+ };
5945
+ graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
5946
+ edgesAdded++;
5947
+ }
5948
+ }
5949
+ }
5950
+ }
5951
+ return { nodesAdded, edgesAdded };
5952
+ }
5953
+
5954
+ // src/extract/proto.ts
5955
+ init_cjs_shims();
5956
+ var import_node_fs17 = require("fs");
5957
+ var import_node_path25 = __toESM(require("path"), 1);
5958
+ var import_types13 = require("@neat.is/types");
5959
+ var PROTO_EXTENSION = ".proto";
5960
+ function packageOf(content) {
5961
+ const m = content.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/m);
5962
+ return m ? m[1] : null;
5963
+ }
5964
+ function lineAt(content, offset) {
5965
+ return content.slice(0, offset).split("\n").length;
5966
+ }
5967
+ function grpcMethodsFromProto(content, fqPackage) {
5968
+ const out = [];
5969
+ const serviceRe = /\bservice\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g;
5970
+ let sm;
5971
+ while ((sm = serviceRe.exec(content)) !== null) {
5972
+ const serviceName = sm[1];
5973
+ const rpcService = fqPackage ? `${fqPackage}.${serviceName}` : serviceName;
5974
+ const bodyStart = serviceRe.lastIndex;
5975
+ let depth = 1;
5976
+ let i = bodyStart;
5977
+ for (; i < content.length && depth > 0; i++) {
5978
+ const ch = content[i];
5979
+ if (ch === "{") depth++;
5980
+ else if (ch === "}") depth--;
5981
+ }
5982
+ const body = content.slice(bodyStart, i - 1);
5983
+ const rpcRe = /\brpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
5984
+ let rm;
5985
+ while ((rm = rpcRe.exec(body)) !== null) {
5986
+ const rpcMethod = rm[1];
5987
+ const line = lineAt(content, bodyStart + rm.index);
5988
+ out.push({ rpcService, rpcMethod, line });
5989
+ }
5990
+ serviceRe.lastIndex = i;
5991
+ }
5992
+ return out;
5993
+ }
5994
+ async function walkProtoFiles(dir) {
5995
+ const out = [];
5996
+ async function walk3(current) {
5997
+ const entries = await import_node_fs17.promises.readdir(current, { withFileTypes: true }).catch(() => []);
5998
+ for (const entry of entries) {
5999
+ const full = import_node_path25.default.join(current, entry.name);
6000
+ if (entry.isDirectory()) {
6001
+ if (IGNORED_DIRS.has(entry.name)) continue;
6002
+ if (await isPythonVenvDir(full)) continue;
6003
+ await walk3(full);
6004
+ } else if (entry.isFile() && import_node_path25.default.extname(entry.name) === PROTO_EXTENSION) {
6005
+ out.push(full);
6006
+ }
6007
+ }
6008
+ }
6009
+ await walk3(dir);
6010
+ return out;
6011
+ }
6012
+ async function addGrpcMethods(graph, services) {
6013
+ let nodesAdded = 0;
6014
+ let edgesAdded = 0;
6015
+ for (const service of services) {
6016
+ const protoPaths = await walkProtoFiles(service.dir);
6017
+ for (const protoPath of protoPaths) {
6018
+ if (isTestPath(protoPath)) continue;
6019
+ const relFile = toPosix2(import_node_path25.default.relative(service.dir, protoPath));
6020
+ let content;
6021
+ try {
6022
+ content = await import_node_fs17.promises.readFile(protoPath, "utf8");
6023
+ } catch (err) {
6024
+ recordExtractionError("proto extraction", protoPath, err);
6025
+ continue;
6026
+ }
6027
+ let methods;
6028
+ try {
6029
+ methods = grpcMethodsFromProto(content, packageOf(content));
6030
+ } catch (err) {
6031
+ recordExtractionError("proto extraction", protoPath, err);
6032
+ continue;
6033
+ }
6034
+ if (methods.length === 0) continue;
6035
+ for (const method of methods) {
6036
+ const mid = (0, import_types13.grpcMethodId)(method.rpcService, method.rpcMethod);
6037
+ if (!graph.hasNode(mid)) {
6038
+ const node = {
6039
+ id: mid,
6040
+ type: import_types13.NodeType.GrpcMethodNode,
6041
+ name: `${method.rpcService}/${method.rpcMethod}`,
6042
+ rpcService: method.rpcService,
6043
+ rpcMethod: method.rpcMethod,
6044
+ path: relFile,
6045
+ line: method.line,
6046
+ discoveredVia: "static"
6047
+ };
6048
+ graph.addNode(mid, node);
6049
+ nodesAdded++;
6050
+ }
6051
+ const containsId = (0, import_types13.extractedEdgeId)(service.node.id, mid, import_types13.EdgeType.CONTAINS);
6052
+ if (!graph.hasEdge(containsId)) {
6053
+ const edge = {
6054
+ id: containsId,
6055
+ source: service.node.id,
6056
+ target: mid,
6057
+ type: import_types13.EdgeType.CONTAINS,
6058
+ provenance: import_types13.Provenance.EXTRACTED,
6059
+ confidence: (0, import_types13.confidenceForExtracted)("structural"),
6060
+ evidence: {
6061
+ file: relFile,
6062
+ line: method.line,
6063
+ snippet: snippet(content, method.line)
6064
+ }
6065
+ };
6066
+ graph.addEdgeWithKey(containsId, service.node.id, mid, edge);
6067
+ edgesAdded++;
6068
+ }
6069
+ }
6070
+ }
6071
+ }
6072
+ return { nodesAdded, edgesAdded };
6073
+ }
6074
+
5393
6075
  // src/extract/calls/index.ts
5394
6076
  init_cjs_shims();
5395
- var import_types18 = require("@neat.is/types");
6077
+ var import_types21 = require("@neat.is/types");
5396
6078
 
5397
6079
  // src/extract/calls/http.ts
5398
6080
  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);
6081
+ var import_node_path26 = __toESM(require("path"), 1);
6082
+ var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
6083
+ var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
5402
6084
  var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
5403
- var import_types12 = require("@neat.is/types");
6085
+ var import_types14 = require("@neat.is/types");
5404
6086
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
5405
6087
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
5406
6088
  function isInsideJsxExternalLink(node) {
@@ -5428,14 +6110,14 @@ function collectStringLiterals(node, out) {
5428
6110
  if (child) collectStringLiterals(child, out);
5429
6111
  }
5430
6112
  }
5431
- var PARSE_CHUNK2 = 16384;
5432
- function parseSource2(parser, source) {
6113
+ var PARSE_CHUNK3 = 16384;
6114
+ function parseSource3(parser, source) {
5433
6115
  return parser.parse(
5434
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
6116
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
5435
6117
  );
5436
6118
  }
5437
6119
  function callsFromSource(source, parser, knownHosts) {
5438
- const tree = parseSource2(parser, source);
6120
+ const tree = parseSource3(parser, source);
5439
6121
  const literals = [];
5440
6122
  collectStringLiterals(tree.rootNode, literals);
5441
6123
  const out = [];
@@ -5449,27 +6131,20 @@ function callsFromSource(source, parser, knownHosts) {
5449
6131
  }
5450
6132
  return out;
5451
6133
  }
5452
- function makeJsParser2() {
5453
- const p = new import_tree_sitter2.default();
5454
- p.setLanguage(import_tree_sitter_javascript2.default);
6134
+ function makeJsParser3() {
6135
+ const p = new import_tree_sitter3.default();
6136
+ p.setLanguage(import_tree_sitter_javascript3.default);
5455
6137
  return p;
5456
6138
  }
5457
6139
  function makePyParser2() {
5458
- const p = new import_tree_sitter2.default();
6140
+ const p = new import_tree_sitter3.default();
5459
6141
  p.setLanguage(import_tree_sitter_python2.default);
5460
6142
  return p;
5461
6143
  }
5462
6144
  async function addHttpCallEdges(graph, services) {
5463
- const jsParser = makeJsParser2();
6145
+ const jsParser = makeJsParser3();
5464
6146
  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
- }
6147
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5473
6148
  let nodesAdded = 0;
5474
6149
  let edgesAdded = 0;
5475
6150
  for (const service of services) {
@@ -5477,7 +6152,7 @@ async function addHttpCallEdges(graph, services) {
5477
6152
  const seen = /* @__PURE__ */ new Set();
5478
6153
  for (const file of files) {
5479
6154
  if (isTestPath(file.path)) continue;
5480
- const parser = import_node_path24.default.extname(file.path) === ".py" ? pyParser : jsParser;
6155
+ const parser = import_node_path26.default.extname(file.path) === ".py" ? pyParser : jsParser;
5481
6156
  let sites;
5482
6157
  try {
5483
6158
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -5486,14 +6161,14 @@ async function addHttpCallEdges(graph, services) {
5486
6161
  continue;
5487
6162
  }
5488
6163
  if (sites.length === 0) continue;
5489
- const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
6164
+ const relFile = toPosix2(import_node_path26.default.relative(service.dir, file.path));
5490
6165
  for (const site of sites) {
5491
6166
  const targetId = hostToNodeId.get(site.host);
5492
6167
  if (!targetId || targetId === service.node.id) continue;
5493
6168
  const dedupKey = `${relFile}|${targetId}`;
5494
6169
  if (seen.has(dedupKey)) continue;
5495
6170
  seen.add(dedupKey);
5496
- const confidence = (0, import_types12.confidenceForExtracted)("url-literal-service-target");
6171
+ const confidence = (0, import_types14.confidenceForExtracted)("url-literal-service-target");
5497
6172
  const ev = {
5498
6173
  file: relFile,
5499
6174
  line: site.line,
@@ -5507,25 +6182,25 @@ async function addHttpCallEdges(graph, services) {
5507
6182
  );
5508
6183
  nodesAdded += n;
5509
6184
  edgesAdded += e;
5510
- if (!(0, import_types12.passesExtractedFloor)(confidence)) {
6185
+ if (!(0, import_types14.passesExtractedFloor)(confidence)) {
5511
6186
  noteExtractedDropped({
5512
6187
  source: fileNodeId,
5513
6188
  target: targetId,
5514
- type: import_types12.EdgeType.CALLS,
6189
+ type: import_types14.EdgeType.CALLS,
5515
6190
  confidence,
5516
6191
  confidenceKind: "url-literal-service-target",
5517
6192
  evidence: ev
5518
6193
  });
5519
6194
  continue;
5520
6195
  }
5521
- const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, targetId, import_types12.EdgeType.CALLS);
6196
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, targetId, import_types14.EdgeType.CALLS);
5522
6197
  if (!graph.hasEdge(edgeId)) {
5523
6198
  const edge = {
5524
6199
  id: edgeId,
5525
6200
  source: fileNodeId,
5526
6201
  target: targetId,
5527
- type: import_types12.EdgeType.CALLS,
5528
- provenance: import_types12.Provenance.EXTRACTED,
6202
+ type: import_types14.EdgeType.CALLS,
6203
+ provenance: import_types14.Provenance.EXTRACTED,
5529
6204
  confidence,
5530
6205
  evidence: ev
5531
6206
  };
@@ -5538,10 +6213,279 @@ async function addHttpCallEdges(graph, services) {
5538
6213
  return { nodesAdded, edgesAdded };
5539
6214
  }
5540
6215
 
6216
+ // src/extract/calls/route-match.ts
6217
+ init_cjs_shims();
6218
+ var import_node_path27 = __toESM(require("path"), 1);
6219
+ var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
6220
+ var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
6221
+ var import_types15 = require("@neat.is/types");
6222
+ var PARSE_CHUNK4 = 16384;
6223
+ function parseSource4(parser, source) {
6224
+ return parser.parse(
6225
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
6226
+ );
6227
+ }
6228
+ function makeJsParser4() {
6229
+ const p = new import_tree_sitter4.default();
6230
+ p.setLanguage(import_tree_sitter_javascript4.default);
6231
+ return p;
6232
+ }
6233
+ var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
6234
+ var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
6235
+ function walk2(node, visit) {
6236
+ visit(node);
6237
+ for (let i = 0; i < node.namedChildCount; i++) {
6238
+ const child = node.namedChild(i);
6239
+ if (child) walk2(child, visit);
6240
+ }
6241
+ }
6242
+ function reconstructUrl(node) {
6243
+ if (node.type === "string") {
6244
+ for (let i = 0; i < node.namedChildCount; i++) {
6245
+ const child = node.namedChild(i);
6246
+ if (child?.type === "string_fragment") return child.text;
6247
+ }
6248
+ return "";
6249
+ }
6250
+ if (node.type === "template_string") {
6251
+ let out = "";
6252
+ for (let i = 0; i < node.namedChildCount; i++) {
6253
+ const child = node.namedChild(i);
6254
+ if (!child) continue;
6255
+ if (child.type === "string_fragment") out += child.text;
6256
+ else if (child.type === "template_substitution") out += ":param";
6257
+ }
6258
+ if (out.length === 0) {
6259
+ const raw = node.text;
6260
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
6261
+ }
6262
+ return out;
6263
+ }
6264
+ return null;
6265
+ }
6266
+ function methodFromOptions(objNode) {
6267
+ for (let i = 0; i < objNode.namedChildCount; i++) {
6268
+ const pair = objNode.namedChild(i);
6269
+ if (!pair || pair.type !== "pair") continue;
6270
+ const k = pair.childForFieldName("key");
6271
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
6272
+ if (kText !== "method") continue;
6273
+ const v = pair.childForFieldName("value");
6274
+ if (v && (v.type === "string" || v.type === "template_string")) {
6275
+ const s = stringText(v);
6276
+ return s ? s.toUpperCase() : void 0;
6277
+ }
6278
+ }
6279
+ return void 0;
6280
+ }
6281
+ function stringText(node) {
6282
+ if (node.type === "string") {
6283
+ for (let i = 0; i < node.namedChildCount; i++) {
6284
+ const child = node.namedChild(i);
6285
+ if (child?.type === "string_fragment") return child.text;
6286
+ }
6287
+ return "";
6288
+ }
6289
+ return null;
6290
+ }
6291
+ function urlNodeFromConfig(objNode) {
6292
+ for (let i = 0; i < objNode.namedChildCount; i++) {
6293
+ const pair = objNode.namedChild(i);
6294
+ if (!pair || pair.type !== "pair") continue;
6295
+ const k = pair.childForFieldName("key");
6296
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
6297
+ if (kText === "url") return pair.childForFieldName("value");
6298
+ }
6299
+ return null;
6300
+ }
6301
+ function pathOf(urlStr) {
6302
+ try {
6303
+ const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
6304
+ const parsed = new URL(candidate);
6305
+ return parsed.pathname || "/";
6306
+ } catch {
6307
+ return null;
6308
+ }
6309
+ }
6310
+ function matchHost(urlStr, knownHosts) {
6311
+ for (const host of knownHosts) {
6312
+ if (urlMatchesHost(urlStr, host)) return host;
6313
+ }
6314
+ return null;
6315
+ }
6316
+ function clientCallSitesFromSource(source, parser, knownHosts) {
6317
+ const tree = parseSource4(parser, source);
6318
+ const out = [];
6319
+ const push = (urlNode, method, callNode) => {
6320
+ const urlStr = reconstructUrl(urlNode);
6321
+ if (!urlStr) return;
6322
+ const host = matchHost(urlStr, knownHosts);
6323
+ if (!host) return;
6324
+ const p = pathOf(urlStr);
6325
+ if (p === null) return;
6326
+ const line = callNode.startPosition.row + 1;
6327
+ out.push({
6328
+ host,
6329
+ method,
6330
+ pathTemplate: p,
6331
+ line,
6332
+ snippet: snippet(source, line)
6333
+ });
6334
+ };
6335
+ walk2(tree.rootNode, (node) => {
6336
+ if (node.type !== "call_expression") return;
6337
+ const fn = node.childForFieldName("function");
6338
+ if (!fn) return;
6339
+ const args = node.childForFieldName("arguments");
6340
+ const first = args?.namedChild(0);
6341
+ if (!first) return;
6342
+ const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
6343
+ if (fn.type === "identifier" && fnName === "fetch") {
6344
+ const opts = args?.namedChild(1);
6345
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
6346
+ push(first, method, node);
6347
+ return;
6348
+ }
6349
+ if (fn.type === "identifier" && fnName === "axios") {
6350
+ if (first.type === "object") {
6351
+ const urlNode = urlNodeFromConfig(first);
6352
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
6353
+ } else {
6354
+ const opts = args?.namedChild(1);
6355
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
6356
+ push(first, method, node);
6357
+ }
6358
+ return;
6359
+ }
6360
+ if (fn.type === "member_expression") {
6361
+ const obj = fn.childForFieldName("object");
6362
+ const objName = obj?.text ?? "";
6363
+ if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
6364
+ if (fnName === "request" && first.type === "object") {
6365
+ const urlNode = urlNodeFromConfig(first);
6366
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
6367
+ } else {
6368
+ push(first, fnName.toUpperCase(), node);
6369
+ }
6370
+ return;
6371
+ }
6372
+ if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
6373
+ const opts = args?.namedChild(1);
6374
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
6375
+ push(first, method, node);
6376
+ return;
6377
+ }
6378
+ }
6379
+ });
6380
+ return out;
6381
+ }
6382
+ function buildRouteIndex(graph) {
6383
+ const index = /* @__PURE__ */ new Map();
6384
+ graph.forEachNode((_id, attrs) => {
6385
+ const node = attrs;
6386
+ if (node.type !== import_types15.NodeType.RouteNode) return;
6387
+ const route = attrs;
6388
+ const owner = (0, import_types15.serviceId)(route.service);
6389
+ const entry = {
6390
+ method: route.method.toUpperCase(),
6391
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
6392
+ routeNodeId: route.id
6393
+ };
6394
+ const list = index.get(owner);
6395
+ if (list) list.push(entry);
6396
+ else index.set(owner, [entry]);
6397
+ });
6398
+ return index;
6399
+ }
6400
+ function findRoute(entries, method, normalizedPath) {
6401
+ return entries.find(
6402
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
6403
+ );
6404
+ }
6405
+ async function addRouteCallEdges(graph, services) {
6406
+ const jsParser = makeJsParser4();
6407
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
6408
+ const routeIndex = buildRouteIndex(graph);
6409
+ if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
6410
+ let nodesAdded = 0;
6411
+ let edgesAdded = 0;
6412
+ for (const service of services) {
6413
+ const files = await loadSourceFiles(service.dir);
6414
+ const seen = /* @__PURE__ */ new Set();
6415
+ for (const file of files) {
6416
+ if (isTestPath(file.path)) continue;
6417
+ if (!JS_CLIENT_EXTENSIONS.has(import_node_path27.default.extname(file.path))) continue;
6418
+ let sites;
6419
+ try {
6420
+ sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
6421
+ } catch (err) {
6422
+ recordExtractionError("route-match call extraction", file.path, err);
6423
+ continue;
6424
+ }
6425
+ if (sites.length === 0) continue;
6426
+ const relFile = toPosix2(import_node_path27.default.relative(service.dir, file.path));
6427
+ for (const site of sites) {
6428
+ const serverServiceId = hostToNodeId.get(site.host);
6429
+ if (!serverServiceId || serverServiceId === service.node.id) continue;
6430
+ const entries = routeIndex.get(serverServiceId);
6431
+ if (!entries) continue;
6432
+ const normalizedPath = normalizePathTemplate(site.pathTemplate);
6433
+ const match = findRoute(entries, site.method, normalizedPath);
6434
+ if (!match) continue;
6435
+ const dedupKey = `${relFile}|${match.routeNodeId}`;
6436
+ if (seen.has(dedupKey)) continue;
6437
+ seen.add(dedupKey);
6438
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
6439
+ graph,
6440
+ service.pkg.name,
6441
+ service.node.id,
6442
+ relFile
6443
+ );
6444
+ nodesAdded += n;
6445
+ edgesAdded += e;
6446
+ const confidence = (0, import_types15.confidenceForExtracted)("verified-call-site");
6447
+ const ev = {
6448
+ file: relFile,
6449
+ line: site.line,
6450
+ snippet: site.snippet,
6451
+ method: site.method ?? match.method,
6452
+ pathTemplate: site.pathTemplate
6453
+ };
6454
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
6455
+ noteExtractedDropped({
6456
+ source: fileNodeId,
6457
+ target: match.routeNodeId,
6458
+ type: import_types15.EdgeType.CALLS,
6459
+ confidence,
6460
+ confidenceKind: "verified-call-site",
6461
+ evidence: ev
6462
+ });
6463
+ continue;
6464
+ }
6465
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, match.routeNodeId, import_types15.EdgeType.CALLS);
6466
+ if (!graph.hasEdge(edgeId)) {
6467
+ const edge = {
6468
+ id: edgeId,
6469
+ source: fileNodeId,
6470
+ target: match.routeNodeId,
6471
+ type: import_types15.EdgeType.CALLS,
6472
+ provenance: import_types15.Provenance.EXTRACTED,
6473
+ confidence,
6474
+ evidence: ev
6475
+ };
6476
+ graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
6477
+ edgesAdded++;
6478
+ }
6479
+ }
6480
+ }
6481
+ }
6482
+ return { nodesAdded, edgesAdded };
6483
+ }
6484
+
5541
6485
  // src/extract/calls/kafka.ts
5542
6486
  init_cjs_shims();
5543
- var import_node_path25 = __toESM(require("path"), 1);
5544
- var import_types13 = require("@neat.is/types");
6487
+ var import_node_path28 = __toESM(require("path"), 1);
6488
+ var import_types16 = require("@neat.is/types");
5545
6489
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
5546
6490
  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
6491
  function findAll(re, text) {
@@ -5562,7 +6506,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5562
6506
  seen.add(key);
5563
6507
  const line = lineOf(file.content, topic);
5564
6508
  out.push({
5565
- infraId: (0, import_types13.infraId)("kafka-topic", topic),
6509
+ infraId: (0, import_types16.infraId)("kafka-topic", topic),
5566
6510
  name: topic,
5567
6511
  kind: "kafka-topic",
5568
6512
  edgeType,
@@ -5571,7 +6515,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5571
6515
  // tier (ADR-066).
5572
6516
  confidenceKind: "verified-call-site",
5573
6517
  evidence: {
5574
- file: import_node_path25.default.relative(serviceDir, file.path),
6518
+ file: import_node_path28.default.relative(serviceDir, file.path),
5575
6519
  line,
5576
6520
  snippet: snippet(file.content, line)
5577
6521
  }
@@ -5584,8 +6528,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
5584
6528
 
5585
6529
  // src/extract/calls/redis.ts
5586
6530
  init_cjs_shims();
5587
- var import_node_path26 = __toESM(require("path"), 1);
5588
- var import_types14 = require("@neat.is/types");
6531
+ var import_node_path29 = __toESM(require("path"), 1);
6532
+ var import_types17 = require("@neat.is/types");
5589
6533
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
5590
6534
  function redisEndpointsFromFile(file, serviceDir) {
5591
6535
  const out = [];
@@ -5598,7 +6542,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5598
6542
  seen.add(host);
5599
6543
  const line = lineOf(file.content, host);
5600
6544
  out.push({
5601
- infraId: (0, import_types14.infraId)("redis", host),
6545
+ infraId: (0, import_types17.infraId)("redis", host),
5602
6546
  name: host,
5603
6547
  kind: "redis",
5604
6548
  edgeType: "CALLS",
@@ -5607,7 +6551,7 @@ function redisEndpointsFromFile(file, serviceDir) {
5607
6551
  // support tier (ADR-066).
5608
6552
  confidenceKind: "url-with-structural-support",
5609
6553
  evidence: {
5610
- file: import_node_path26.default.relative(serviceDir, file.path),
6554
+ file: import_node_path29.default.relative(serviceDir, file.path),
5611
6555
  line,
5612
6556
  snippet: snippet(file.content, line)
5613
6557
  }
@@ -5618,8 +6562,8 @@ function redisEndpointsFromFile(file, serviceDir) {
5618
6562
 
5619
6563
  // src/extract/calls/aws.ts
5620
6564
  init_cjs_shims();
5621
- var import_node_path27 = __toESM(require("path"), 1);
5622
- var import_types15 = require("@neat.is/types");
6565
+ var import_node_path30 = __toESM(require("path"), 1);
6566
+ var import_types18 = require("@neat.is/types");
5623
6567
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
5624
6568
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
5625
6569
  function hasMarker(text, markers) {
@@ -5643,7 +6587,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5643
6587
  seen.add(key);
5644
6588
  const line = lineOf(file.content, name);
5645
6589
  out.push({
5646
- infraId: (0, import_types15.infraId)(kind, name),
6590
+ infraId: (0, import_types18.infraId)(kind, name),
5647
6591
  name,
5648
6592
  kind,
5649
6593
  edgeType: "CALLS",
@@ -5652,7 +6596,7 @@ function awsEndpointsFromFile(file, serviceDir) {
5652
6596
  // (ADR-066).
5653
6597
  confidenceKind: "verified-call-site",
5654
6598
  evidence: {
5655
- file: import_node_path27.default.relative(serviceDir, file.path),
6599
+ file: import_node_path30.default.relative(serviceDir, file.path),
5656
6600
  line,
5657
6601
  snippet: snippet(file.content, line)
5658
6602
  }
@@ -5677,8 +6621,8 @@ function awsEndpointsFromFile(file, serviceDir) {
5677
6621
 
5678
6622
  // src/extract/calls/grpc.ts
5679
6623
  init_cjs_shims();
5680
- var import_node_path28 = __toESM(require("path"), 1);
5681
- var import_types16 = require("@neat.is/types");
6624
+ var import_node_path31 = __toESM(require("path"), 1);
6625
+ var import_types19 = require("@neat.is/types");
5682
6626
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
5683
6627
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
5684
6628
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -5727,7 +6671,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5727
6671
  const { kind } = classified;
5728
6672
  const line = lineOf(file.content, m[0]);
5729
6673
  out.push({
5730
- infraId: (0, import_types16.infraId)(kind, name),
6674
+ infraId: (0, import_types19.infraId)(kind, name),
5731
6675
  name,
5732
6676
  kind,
5733
6677
  edgeType: "CALLS",
@@ -5736,7 +6680,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
5736
6680
  // tier (ADR-066).
5737
6681
  confidenceKind: "verified-call-site",
5738
6682
  evidence: {
5739
- file: import_node_path28.default.relative(serviceDir, file.path),
6683
+ file: import_node_path31.default.relative(serviceDir, file.path),
5740
6684
  line,
5741
6685
  snippet: snippet(file.content, line)
5742
6686
  }
@@ -5747,8 +6691,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
5747
6691
 
5748
6692
  // src/extract/calls/supabase.ts
5749
6693
  init_cjs_shims();
5750
- var import_node_path29 = __toESM(require("path"), 1);
5751
- var import_types17 = require("@neat.is/types");
6694
+ var import_node_path32 = __toESM(require("path"), 1);
6695
+ var import_types20 = require("@neat.is/types");
5752
6696
  var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
5753
6697
  var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
5754
6698
  var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
@@ -5786,7 +6730,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5786
6730
  seen.add(name);
5787
6731
  const line = lineOf(file.content, m[0]);
5788
6732
  out.push({
5789
- infraId: (0, import_types17.infraId)("supabase", name),
6733
+ infraId: (0, import_types20.infraId)("supabase", name),
5790
6734
  name,
5791
6735
  kind: "supabase",
5792
6736
  edgeType: "CALLS",
@@ -5796,7 +6740,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5796
6740
  // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
5797
6741
  confidenceKind: "verified-call-site",
5798
6742
  evidence: {
5799
- file: import_node_path29.default.relative(serviceDir, file.path),
6743
+ file: import_node_path32.default.relative(serviceDir, file.path),
5800
6744
  line,
5801
6745
  snippet: snippet(file.content, line)
5802
6746
  }
@@ -5809,11 +6753,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
5809
6753
  function edgeTypeFromEndpoint(ep) {
5810
6754
  switch (ep.edgeType) {
5811
6755
  case "PUBLISHES_TO":
5812
- return import_types18.EdgeType.PUBLISHES_TO;
6756
+ return import_types21.EdgeType.PUBLISHES_TO;
5813
6757
  case "CONSUMES_FROM":
5814
- return import_types18.EdgeType.CONSUMES_FROM;
6758
+ return import_types21.EdgeType.CONSUMES_FROM;
5815
6759
  default:
5816
- return import_types18.EdgeType.CALLS;
6760
+ return import_types21.EdgeType.CALLS;
5817
6761
  }
5818
6762
  }
5819
6763
  function isAwsKind(kind) {
@@ -5841,7 +6785,7 @@ async function addExternalEndpointEdges(graph, services) {
5841
6785
  if (!graph.hasNode(ep.infraId)) {
5842
6786
  const node = {
5843
6787
  id: ep.infraId,
5844
- type: import_types18.NodeType.InfraNode,
6788
+ type: import_types21.NodeType.InfraNode,
5845
6789
  name: ep.name,
5846
6790
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
5847
6791
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -5853,7 +6797,7 @@ async function addExternalEndpointEdges(graph, services) {
5853
6797
  nodesAdded++;
5854
6798
  }
5855
6799
  const edgeType = edgeTypeFromEndpoint(ep);
5856
- const confidence = (0, import_types18.confidenceForExtracted)(ep.confidenceKind);
6800
+ const confidence = (0, import_types21.confidenceForExtracted)(ep.confidenceKind);
5857
6801
  const relFile = toPosix2(ep.evidence.file);
5858
6802
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5859
6803
  graph,
@@ -5863,7 +6807,7 @@ async function addExternalEndpointEdges(graph, services) {
5863
6807
  );
5864
6808
  nodesAdded += n;
5865
6809
  edgesAdded += e;
5866
- if (!(0, import_types18.passesExtractedFloor)(confidence)) {
6810
+ if (!(0, import_types21.passesExtractedFloor)(confidence)) {
5867
6811
  noteExtractedDropped({
5868
6812
  source: fileNodeId,
5869
6813
  target: ep.infraId,
@@ -5883,7 +6827,7 @@ async function addExternalEndpointEdges(graph, services) {
5883
6827
  source: fileNodeId,
5884
6828
  target: ep.infraId,
5885
6829
  type: edgeType,
5886
- provenance: import_types18.Provenance.EXTRACTED,
6830
+ provenance: import_types21.Provenance.EXTRACTED,
5887
6831
  confidence,
5888
6832
  evidence: ep.evidence
5889
6833
  };
@@ -5897,9 +6841,10 @@ async function addExternalEndpointEdges(graph, services) {
5897
6841
  async function addCallEdges(graph, services) {
5898
6842
  const http = await addHttpCallEdges(graph, services);
5899
6843
  const ext = await addExternalEndpointEdges(graph, services);
6844
+ const routes = await addRouteCallEdges(graph, services);
5900
6845
  return {
5901
- nodesAdded: http.nodesAdded + ext.nodesAdded,
5902
- edgesAdded: http.edgesAdded + ext.edgesAdded
6846
+ nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
6847
+ edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
5903
6848
  };
5904
6849
  }
5905
6850
 
@@ -5908,16 +6853,16 @@ init_cjs_shims();
5908
6853
 
5909
6854
  // src/extract/infra/docker-compose.ts
5910
6855
  init_cjs_shims();
5911
- var import_node_path30 = __toESM(require("path"), 1);
5912
- var import_types20 = require("@neat.is/types");
6856
+ var import_node_path33 = __toESM(require("path"), 1);
6857
+ var import_types23 = require("@neat.is/types");
5913
6858
 
5914
6859
  // src/extract/infra/shared.ts
5915
6860
  init_cjs_shims();
5916
- var import_types19 = require("@neat.is/types");
6861
+ var import_types22 = require("@neat.is/types");
5917
6862
  function makeInfraNode(kind, name, provider = "self", extras) {
5918
6863
  return {
5919
- id: (0, import_types19.infraId)(kind, name),
5920
- type: import_types19.NodeType.InfraNode,
6864
+ id: (0, import_types22.infraId)(kind, name),
6865
+ type: import_types22.NodeType.InfraNode,
5921
6866
  name,
5922
6867
  provider,
5923
6868
  kind,
@@ -5946,7 +6891,7 @@ function dependsOnList(value) {
5946
6891
  }
5947
6892
  function serviceNameToServiceNode(name, services) {
5948
6893
  for (const s of services) {
5949
- if (s.node.name === name || import_node_path30.default.basename(s.dir) === name) return s.node.id;
6894
+ if (s.node.name === name || import_node_path33.default.basename(s.dir) === name) return s.node.id;
5950
6895
  }
5951
6896
  return null;
5952
6897
  }
@@ -5955,7 +6900,7 @@ async function addComposeInfra(graph, scanPath, services) {
5955
6900
  let edgesAdded = 0;
5956
6901
  let composePath = null;
5957
6902
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
5958
- const abs = import_node_path30.default.join(scanPath, name);
6903
+ const abs = import_node_path33.default.join(scanPath, name);
5959
6904
  if (await exists2(abs)) {
5960
6905
  composePath = abs;
5961
6906
  break;
@@ -5968,13 +6913,13 @@ async function addComposeInfra(graph, scanPath, services) {
5968
6913
  } catch (err) {
5969
6914
  recordExtractionError(
5970
6915
  "infra docker-compose",
5971
- import_node_path30.default.relative(scanPath, composePath),
6916
+ import_node_path33.default.relative(scanPath, composePath),
5972
6917
  err
5973
6918
  );
5974
6919
  return { nodesAdded, edgesAdded };
5975
6920
  }
5976
6921
  if (!compose?.services) return { nodesAdded, edgesAdded };
5977
- const evidenceFile = import_node_path30.default.relative(scanPath, composePath).split(import_node_path30.default.sep).join("/");
6922
+ const evidenceFile = import_node_path33.default.relative(scanPath, composePath).split(import_node_path33.default.sep).join("/");
5978
6923
  const composeNameToNodeId = /* @__PURE__ */ new Map();
5979
6924
  for (const [composeName, svc] of Object.entries(compose.services)) {
5980
6925
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -5996,15 +6941,15 @@ async function addComposeInfra(graph, scanPath, services) {
5996
6941
  for (const dep of dependsOnList(svc.depends_on)) {
5997
6942
  const targetId = composeNameToNodeId.get(dep);
5998
6943
  if (!targetId) continue;
5999
- const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types20.EdgeType.DEPENDS_ON);
6944
+ const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types23.EdgeType.DEPENDS_ON);
6000
6945
  if (graph.hasEdge(edgeId)) continue;
6001
6946
  const edge = {
6002
6947
  id: edgeId,
6003
6948
  source: sourceId,
6004
6949
  target: targetId,
6005
- type: import_types20.EdgeType.DEPENDS_ON,
6006
- provenance: import_types20.Provenance.EXTRACTED,
6007
- confidence: (0, import_types20.confidenceForExtracted)("structural"),
6950
+ type: import_types23.EdgeType.DEPENDS_ON,
6951
+ provenance: import_types23.Provenance.EXTRACTED,
6952
+ confidence: (0, import_types23.confidenceForExtracted)("structural"),
6008
6953
  evidence: { file: evidenceFile }
6009
6954
  };
6010
6955
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -6016,9 +6961,9 @@ async function addComposeInfra(graph, scanPath, services) {
6016
6961
 
6017
6962
  // src/extract/infra/dockerfile.ts
6018
6963
  init_cjs_shims();
6019
- var import_node_path31 = __toESM(require("path"), 1);
6020
- var import_node_fs17 = require("fs");
6021
- var import_types21 = require("@neat.is/types");
6964
+ var import_node_path34 = __toESM(require("path"), 1);
6965
+ var import_node_fs18 = require("fs");
6966
+ var import_types24 = require("@neat.is/types");
6022
6967
  function readDockerfile(content) {
6023
6968
  let image = null;
6024
6969
  const ports = [];
@@ -6047,15 +6992,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6047
6992
  let nodesAdded = 0;
6048
6993
  let edgesAdded = 0;
6049
6994
  for (const service of services) {
6050
- const dockerfilePath = import_node_path31.default.join(service.dir, "Dockerfile");
6995
+ const dockerfilePath = import_node_path34.default.join(service.dir, "Dockerfile");
6051
6996
  if (!await exists2(dockerfilePath)) continue;
6052
6997
  let content;
6053
6998
  try {
6054
- content = await import_node_fs17.promises.readFile(dockerfilePath, "utf8");
6999
+ content = await import_node_fs18.promises.readFile(dockerfilePath, "utf8");
6055
7000
  } catch (err) {
6056
7001
  recordExtractionError(
6057
7002
  "infra dockerfile",
6058
- import_node_path31.default.relative(scanPath, dockerfilePath),
7003
+ import_node_path34.default.relative(scanPath, dockerfilePath),
6059
7004
  err
6060
7005
  );
6061
7006
  continue;
@@ -6067,8 +7012,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6067
7012
  graph.addNode(node.id, node);
6068
7013
  nodesAdded++;
6069
7014
  }
6070
- const relDockerfile = toPosix2(import_node_path31.default.relative(service.dir, dockerfilePath));
6071
- const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, dockerfilePath));
7015
+ const relDockerfile = toPosix2(import_node_path34.default.relative(service.dir, dockerfilePath));
7016
+ const evidenceFile = toPosix2(import_node_path34.default.relative(scanPath, dockerfilePath));
6072
7017
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6073
7018
  graph,
6074
7019
  service.pkg.name,
@@ -6077,15 +7022,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6077
7022
  );
6078
7023
  nodesAdded += fn;
6079
7024
  edgesAdded += fe;
6080
- const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types21.EdgeType.RUNS_ON);
7025
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types24.EdgeType.RUNS_ON);
6081
7026
  if (!graph.hasEdge(edgeId)) {
6082
7027
  const edge = {
6083
7028
  id: edgeId,
6084
7029
  source: fileNodeId,
6085
7030
  target: node.id,
6086
- type: import_types21.EdgeType.RUNS_ON,
6087
- provenance: import_types21.Provenance.EXTRACTED,
6088
- confidence: (0, import_types21.confidenceForExtracted)("structural"),
7031
+ type: import_types24.EdgeType.RUNS_ON,
7032
+ provenance: import_types24.Provenance.EXTRACTED,
7033
+ confidence: (0, import_types24.confidenceForExtracted)("structural"),
6089
7034
  evidence: {
6090
7035
  file: evidenceFile,
6091
7036
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -6100,15 +7045,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6100
7045
  graph.addNode(portNode.id, portNode);
6101
7046
  nodesAdded++;
6102
7047
  }
6103
- const portEdgeId = (0, import_types5.extractedEdgeId)(fileNodeId, portNode.id, import_types21.EdgeType.CONNECTS_TO);
7048
+ const portEdgeId = (0, import_types5.extractedEdgeId)(fileNodeId, portNode.id, import_types24.EdgeType.CONNECTS_TO);
6104
7049
  if (graph.hasEdge(portEdgeId)) continue;
6105
7050
  const portEdge = {
6106
7051
  id: portEdgeId,
6107
7052
  source: fileNodeId,
6108
7053
  target: portNode.id,
6109
- type: import_types21.EdgeType.CONNECTS_TO,
6110
- provenance: import_types21.Provenance.EXTRACTED,
6111
- confidence: (0, import_types21.confidenceForExtracted)("structural"),
7054
+ type: import_types24.EdgeType.CONNECTS_TO,
7055
+ provenance: import_types24.Provenance.EXTRACTED,
7056
+ confidence: (0, import_types24.confidenceForExtracted)("structural"),
6112
7057
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
6113
7058
  };
6114
7059
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -6120,23 +7065,23 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6120
7065
 
6121
7066
  // src/extract/infra/terraform.ts
6122
7067
  init_cjs_shims();
6123
- var import_node_fs18 = require("fs");
6124
- var import_node_path32 = __toESM(require("path"), 1);
6125
- var import_types22 = require("@neat.is/types");
7068
+ var import_node_fs19 = require("fs");
7069
+ var import_node_path35 = __toESM(require("path"), 1);
7070
+ var import_types25 = require("@neat.is/types");
6126
7071
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
6127
7072
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
6128
7073
  async function walkTfFiles(start, depth = 0, max = 5) {
6129
7074
  if (depth > max) return [];
6130
7075
  const out = [];
6131
- const entries = await import_node_fs18.promises.readdir(start, { withFileTypes: true }).catch(() => []);
7076
+ const entries = await import_node_fs19.promises.readdir(start, { withFileTypes: true }).catch(() => []);
6132
7077
  for (const entry of entries) {
6133
7078
  if (entry.isDirectory()) {
6134
7079
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
6135
- const child = import_node_path32.default.join(start, entry.name);
7080
+ const child = import_node_path35.default.join(start, entry.name);
6136
7081
  if (await isPythonVenvDir(child)) continue;
6137
7082
  out.push(...await walkTfFiles(child, depth + 1, max));
6138
7083
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
6139
- out.push(import_node_path32.default.join(start, entry.name));
7084
+ out.push(import_node_path35.default.join(start, entry.name));
6140
7085
  }
6141
7086
  }
6142
7087
  return out;
@@ -6155,7 +7100,7 @@ function blockBody(content, from) {
6155
7100
  }
6156
7101
  return null;
6157
7102
  }
6158
- function lineAt(content, index) {
7103
+ function lineAt2(content, index) {
6159
7104
  let line = 1;
6160
7105
  for (let i = 0; i < index && i < content.length; i++) {
6161
7106
  if (content[i] === "\n") line++;
@@ -6167,8 +7112,8 @@ async function addTerraformResources(graph, scanPath) {
6167
7112
  let edgesAdded = 0;
6168
7113
  const files = await walkTfFiles(scanPath);
6169
7114
  for (const file of files) {
6170
- const content = await import_node_fs18.promises.readFile(file, "utf8");
6171
- const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, file));
7115
+ const content = await import_node_fs19.promises.readFile(file, "utf8");
7116
+ const evidenceFile = toPosix2(import_node_path35.default.relative(scanPath, file));
6172
7117
  const resources = [];
6173
7118
  const byKey = /* @__PURE__ */ new Map();
6174
7119
  RESOURCE_RE.lastIndex = 0;
@@ -6203,16 +7148,16 @@ async function addTerraformResources(graph, scanPath) {
6203
7148
  if (!target) continue;
6204
7149
  if (seen.has(target.nodeId)) continue;
6205
7150
  seen.add(target.nodeId);
6206
- const edgeId = (0, import_types5.extractedEdgeId)(resource.nodeId, target.nodeId, import_types22.EdgeType.DEPENDS_ON);
7151
+ const edgeId = (0, import_types5.extractedEdgeId)(resource.nodeId, target.nodeId, import_types25.EdgeType.DEPENDS_ON);
6207
7152
  if (graph.hasEdge(edgeId)) continue;
6208
- const line = lineAt(content, resource.bodyOffset + ref.index);
7153
+ const line = lineAt2(content, resource.bodyOffset + ref.index);
6209
7154
  const edge = {
6210
7155
  id: edgeId,
6211
7156
  source: resource.nodeId,
6212
7157
  target: target.nodeId,
6213
- type: import_types22.EdgeType.DEPENDS_ON,
6214
- provenance: import_types22.Provenance.EXTRACTED,
6215
- confidence: (0, import_types22.confidenceForExtracted)("structural"),
7158
+ type: import_types25.EdgeType.DEPENDS_ON,
7159
+ provenance: import_types25.Provenance.EXTRACTED,
7160
+ confidence: (0, import_types25.confidenceForExtracted)("structural"),
6216
7161
  evidence: { file: evidenceFile, line, snippet: key }
6217
7162
  };
6218
7163
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -6225,8 +7170,8 @@ async function addTerraformResources(graph, scanPath) {
6225
7170
 
6226
7171
  // src/extract/infra/k8s.ts
6227
7172
  init_cjs_shims();
6228
- var import_node_fs19 = require("fs");
6229
- var import_node_path33 = __toESM(require("path"), 1);
7173
+ var import_node_fs20 = require("fs");
7174
+ var import_node_path36 = __toESM(require("path"), 1);
6230
7175
  var import_yaml3 = require("yaml");
6231
7176
  var K8S_KIND_TO_INFRA_KIND = {
6232
7177
  Service: "k8s-service",
@@ -6240,15 +7185,15 @@ var K8S_KIND_TO_INFRA_KIND = {
6240
7185
  async function walkYamlFiles2(start, depth = 0, max = 5) {
6241
7186
  if (depth > max) return [];
6242
7187
  const out = [];
6243
- const entries = await import_node_fs19.promises.readdir(start, { withFileTypes: true }).catch(() => []);
7188
+ const entries = await import_node_fs20.promises.readdir(start, { withFileTypes: true }).catch(() => []);
6244
7189
  for (const entry of entries) {
6245
7190
  if (entry.isDirectory()) {
6246
7191
  if (IGNORED_DIRS.has(entry.name)) continue;
6247
- const child = import_node_path33.default.join(start, entry.name);
7192
+ const child = import_node_path36.default.join(start, entry.name);
6248
7193
  if (await isPythonVenvDir(child)) continue;
6249
7194
  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));
7195
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path36.default.extname(entry.name))) {
7196
+ out.push(import_node_path36.default.join(start, entry.name));
6252
7197
  }
6253
7198
  }
6254
7199
  return out;
@@ -6257,7 +7202,7 @@ async function addK8sResources(graph, scanPath) {
6257
7202
  let nodesAdded = 0;
6258
7203
  const files = await walkYamlFiles2(scanPath);
6259
7204
  for (const file of files) {
6260
- const content = await import_node_fs19.promises.readFile(file, "utf8");
7205
+ const content = await import_node_fs20.promises.readFile(file, "utf8");
6261
7206
  let docs;
6262
7207
  try {
6263
7208
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -6292,17 +7237,17 @@ async function addInfra(graph, scanPath, services) {
6292
7237
  }
6293
7238
 
6294
7239
  // src/extract/index.ts
6295
- var import_node_path35 = __toESM(require("path"), 1);
7240
+ var import_node_path38 = __toESM(require("path"), 1);
6296
7241
 
6297
7242
  // src/extract/retire.ts
6298
7243
  init_cjs_shims();
6299
- var import_node_fs20 = require("fs");
6300
- var import_node_path34 = __toESM(require("path"), 1);
6301
- var import_types23 = require("@neat.is/types");
7244
+ var import_node_fs21 = require("fs");
7245
+ var import_node_path37 = __toESM(require("path"), 1);
7246
+ var import_types26 = require("@neat.is/types");
6302
7247
  function dropOrphanedFileNodes(graph) {
6303
7248
  const orphans = [];
6304
7249
  graph.forEachNode((id, attrs) => {
6305
- if (attrs.type !== import_types23.NodeType.FileNode) return;
7250
+ if (attrs.type !== import_types26.NodeType.FileNode) return;
6306
7251
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
6307
7252
  orphans.push(id);
6308
7253
  }
@@ -6315,14 +7260,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
6315
7260
  const bases = [scanPath, ...serviceDirs];
6316
7261
  graph.forEachEdge((id, attrs) => {
6317
7262
  const edge = attrs;
6318
- if (edge.provenance !== import_types23.Provenance.EXTRACTED) return;
7263
+ if (edge.provenance !== import_types26.Provenance.EXTRACTED) return;
6319
7264
  const evidenceFile = edge.evidence?.file;
6320
7265
  if (!evidenceFile) return;
6321
- if (import_node_path34.default.isAbsolute(evidenceFile)) {
6322
- if (!(0, import_node_fs20.existsSync)(evidenceFile)) toDrop.push(id);
7266
+ if (import_node_path37.default.isAbsolute(evidenceFile)) {
7267
+ if (!(0, import_node_fs21.existsSync)(evidenceFile)) toDrop.push(id);
6323
7268
  return;
6324
7269
  }
6325
- const found = bases.some((base) => (0, import_node_fs20.existsSync)(import_node_path34.default.join(base, evidenceFile)));
7270
+ const found = bases.some((base) => (0, import_node_fs21.existsSync)(import_node_path37.default.join(base, evidenceFile)));
6326
7271
  if (!found) toDrop.push(id);
6327
7272
  });
6328
7273
  for (const id of toDrop) graph.dropEdge(id);
@@ -6341,6 +7286,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6341
7286
  const importGraph = await addImports(graph, services);
6342
7287
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
6343
7288
  const phase3 = await addConfigNodes(graph, services, scanPath);
7289
+ const routePhase = await addRoutes(graph, services);
7290
+ const grpcPhase = await addGrpcMethods(graph, services);
6344
7291
  const phase4 = await addCallEdges(graph, services);
6345
7292
  const phase5 = await addInfra(graph, scanPath, services);
6346
7293
  const ghostsRetired = retireExtractedEdgesByMissingFile(
@@ -6362,7 +7309,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6362
7309
  }
6363
7310
  const droppedEntries = drainDroppedExtracted();
6364
7311
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
6365
- const rejectedPath = import_node_path35.default.join(import_node_path35.default.dirname(opts.errorsPath), "rejected.ndjson");
7312
+ const rejectedPath = import_node_path38.default.join(import_node_path38.default.dirname(opts.errorsPath), "rejected.ndjson");
6366
7313
  try {
6367
7314
  await writeRejectedExtracted(droppedEntries, rejectedPath);
6368
7315
  } catch (err) {
@@ -6372,8 +7319,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6372
7319
  }
6373
7320
  }
6374
7321
  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,
7322
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
7323
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
6377
7324
  frontiersPromoted,
6378
7325
  extractionErrors: errorEntries.length,
6379
7326
  errorEntries,
@@ -6396,7 +7343,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
6396
7343
 
6397
7344
  // src/diff.ts
6398
7345
  init_cjs_shims();
6399
- var import_node_fs21 = require("fs");
7346
+ var import_node_fs22 = require("fs");
6400
7347
  async function loadSnapshotForDiff(target) {
6401
7348
  if (/^https?:\/\//i.test(target)) {
6402
7349
  const res = await fetch(target);
@@ -6405,7 +7352,7 @@ async function loadSnapshotForDiff(target) {
6405
7352
  }
6406
7353
  return await res.json();
6407
7354
  }
6408
- const raw = await import_node_fs21.promises.readFile(target, "utf8");
7355
+ const raw = await import_node_fs22.promises.readFile(target, "utf8");
6409
7356
  return JSON.parse(raw);
6410
7357
  }
6411
7358
  function indexEntries(entries) {
@@ -6473,9 +7420,9 @@ function canonicalJson(value) {
6473
7420
 
6474
7421
  // src/persist.ts
6475
7422
  init_cjs_shims();
6476
- var import_node_fs22 = require("fs");
6477
- var import_node_path36 = __toESM(require("path"), 1);
6478
- var import_types24 = require("@neat.is/types");
7423
+ var import_node_fs23 = require("fs");
7424
+ var import_node_path39 = __toESM(require("path"), 1);
7425
+ var import_types27 = require("@neat.is/types");
6479
7426
  var SCHEMA_VERSION = 4;
6480
7427
  function migrateV1ToV2(payload) {
6481
7428
  const nodes = payload.graph.nodes;
@@ -6497,12 +7444,12 @@ function migrateV2ToV3(payload) {
6497
7444
  for (const edge of edges) {
6498
7445
  const attrs = edge.attributes;
6499
7446
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
6500
- attrs.provenance = import_types24.Provenance.OBSERVED;
7447
+ attrs.provenance = import_types27.Provenance.OBSERVED;
6501
7448
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
6502
7449
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
6503
7450
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
6504
7451
  if (type && source && target) {
6505
- const newId = (0, import_types24.observedEdgeId)(source, target, type);
7452
+ const newId = (0, import_types27.observedEdgeId)(source, target, type);
6506
7453
  attrs.id = newId;
6507
7454
  if (edge.key) edge.key = newId;
6508
7455
  }
@@ -6511,7 +7458,7 @@ function migrateV2ToV3(payload) {
6511
7458
  return { ...payload, schemaVersion: 3 };
6512
7459
  }
6513
7460
  async function ensureDir(filePath) {
6514
- await import_node_fs22.promises.mkdir(import_node_path36.default.dirname(filePath), { recursive: true });
7461
+ await import_node_fs23.promises.mkdir(import_node_path39.default.dirname(filePath), { recursive: true });
6515
7462
  }
6516
7463
  async function saveGraphToDisk(graph, outPath) {
6517
7464
  await ensureDir(outPath);
@@ -6521,13 +7468,13 @@ async function saveGraphToDisk(graph, outPath) {
6521
7468
  graph: graph.export()
6522
7469
  };
6523
7470
  const tmp = `${outPath}.tmp`;
6524
- await import_node_fs22.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
6525
- await import_node_fs22.promises.rename(tmp, outPath);
7471
+ await import_node_fs23.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
7472
+ await import_node_fs23.promises.rename(tmp, outPath);
6526
7473
  }
6527
7474
  async function loadGraphFromDisk(graph, outPath) {
6528
7475
  let raw;
6529
7476
  try {
6530
- raw = await import_node_fs22.promises.readFile(outPath, "utf8");
7477
+ raw = await import_node_fs23.promises.readFile(outPath, "utf8");
6531
7478
  } catch (err) {
6532
7479
  if (err.code === "ENOENT") return;
6533
7480
  throw err;
@@ -6592,23 +7539,23 @@ function startPersistLoop(graph, outPath, opts = {}) {
6592
7539
 
6593
7540
  // src/projects.ts
6594
7541
  init_cjs_shims();
6595
- var import_node_path37 = __toESM(require("path"), 1);
7542
+ var import_node_path40 = __toESM(require("path"), 1);
6596
7543
  function pathsForProject(project, baseDir) {
6597
7544
  if (project === DEFAULT_PROJECT) {
6598
7545
  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")
7546
+ snapshotPath: import_node_path40.default.join(baseDir, "graph.json"),
7547
+ errorsPath: import_node_path40.default.join(baseDir, "errors.ndjson"),
7548
+ staleEventsPath: import_node_path40.default.join(baseDir, "stale-events.ndjson"),
7549
+ embeddingsCachePath: import_node_path40.default.join(baseDir, "embeddings.json"),
7550
+ policyViolationsPath: import_node_path40.default.join(baseDir, "policy-violations.ndjson")
6604
7551
  };
6605
7552
  }
6606
7553
  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`)
7554
+ snapshotPath: import_node_path40.default.join(baseDir, `${project}.json`),
7555
+ errorsPath: import_node_path40.default.join(baseDir, `errors.${project}.ndjson`),
7556
+ staleEventsPath: import_node_path40.default.join(baseDir, `stale-events.${project}.ndjson`),
7557
+ embeddingsCachePath: import_node_path40.default.join(baseDir, `embeddings.${project}.json`),
7558
+ policyViolationsPath: import_node_path40.default.join(baseDir, `policy-violations.${project}.ndjson`)
6612
7559
  };
6613
7560
  }
6614
7561
  var Projects = class {
@@ -6648,23 +7595,23 @@ function parseExtraProjects(raw) {
6648
7595
 
6649
7596
  // src/registry.ts
6650
7597
  init_cjs_shims();
6651
- var import_node_fs23 = require("fs");
7598
+ var import_node_fs24 = require("fs");
6652
7599
  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");
7600
+ var import_node_path41 = __toESM(require("path"), 1);
7601
+ var import_types28 = require("@neat.is/types");
6655
7602
  function neatHome() {
6656
7603
  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");
7604
+ if (override && override.length > 0) return import_node_path41.default.resolve(override);
7605
+ return import_node_path41.default.join(import_node_os3.default.homedir(), ".neat");
6659
7606
  }
6660
7607
  function registryPath() {
6661
- return import_node_path38.default.join(neatHome(), "projects.json");
7608
+ return import_node_path41.default.join(neatHome(), "projects.json");
6662
7609
  }
6663
7610
  async function readRegistry() {
6664
7611
  const file = registryPath();
6665
7612
  let raw;
6666
7613
  try {
6667
- raw = await import_node_fs23.promises.readFile(file, "utf8");
7614
+ raw = await import_node_fs24.promises.readFile(file, "utf8");
6668
7615
  } catch (err) {
6669
7616
  if (err.code === "ENOENT") {
6670
7617
  return { version: 1, projects: [] };
@@ -6672,7 +7619,7 @@ async function readRegistry() {
6672
7619
  throw err;
6673
7620
  }
6674
7621
  const parsed = JSON.parse(raw);
6675
- return import_types25.RegistryFileSchema.parse(parsed);
7622
+ return import_types28.RegistryFileSchema.parse(parsed);
6676
7623
  }
6677
7624
  async function getProject(name) {
6678
7625
  const reg = await readRegistry();
@@ -6893,11 +7840,11 @@ function registerRoutes(scope, ctx) {
6893
7840
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6894
7841
  const parsed = [];
6895
7842
  for (const c of candidates) {
6896
- const r = import_types26.DivergenceTypeSchema.safeParse(c);
7843
+ const r = import_types29.DivergenceTypeSchema.safeParse(c);
6897
7844
  if (!r.success) {
6898
7845
  return reply.code(400).send({
6899
7846
  error: `unknown divergence type "${c}"`,
6900
- allowed: import_types26.DivergenceTypeSchema.options
7847
+ allowed: import_types29.DivergenceTypeSchema.options
6901
7848
  });
6902
7849
  }
6903
7850
  parsed.push(r.data);
@@ -7116,7 +8063,7 @@ function registerRoutes(scope, ctx) {
7116
8063
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
7117
8064
  let violations = await log.readAll();
7118
8065
  if (req.query.severity) {
7119
- const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
8066
+ const sev = import_types29.PolicySeveritySchema.safeParse(req.query.severity);
7120
8067
  if (!sev.success) {
7121
8068
  return reply.code(400).send({
7122
8069
  error: "invalid severity",
@@ -7155,7 +8102,7 @@ function registerRoutes(scope, ctx) {
7155
8102
  scope.post("/policies/check", async (req, reply) => {
7156
8103
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7157
8104
  if (!proj) return;
7158
- const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
8105
+ const parsed = import_types29.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7159
8106
  if (!parsed.success) {
7160
8107
  return reply.code(400).send({
7161
8108
  error: "invalid /policies/check body",
@@ -7417,8 +8364,8 @@ init_otel_grpc();
7417
8364
 
7418
8365
  // src/search.ts
7419
8366
  init_cjs_shims();
7420
- var import_node_fs24 = require("fs");
7421
- var import_node_path41 = __toESM(require("path"), 1);
8367
+ var import_node_fs25 = require("fs");
8368
+ var import_node_path44 = __toESM(require("path"), 1);
7422
8369
  var import_node_crypto3 = require("crypto");
7423
8370
  var DEFAULT_LIMIT = 10;
7424
8371
  var NOMIC_DIM = 768;
@@ -7453,6 +8400,30 @@ function embedText(node) {
7453
8400
  if (filePath) parts.push(`path=${filePath}`);
7454
8401
  break;
7455
8402
  }
8403
+ case "RouteNode": {
8404
+ const method = node.method;
8405
+ const tmpl = node.pathTemplate;
8406
+ if (method) parts.push(`method=${method}`);
8407
+ if (tmpl) parts.push(`path=${tmpl}`);
8408
+ break;
8409
+ }
8410
+ case "GraphQLOperationNode": {
8411
+ const opType = node.operationType;
8412
+ if (opType) parts.push(`operationType=${opType}`);
8413
+ break;
8414
+ }
8415
+ case "GrpcMethodNode": {
8416
+ const rpcService = node.rpcService;
8417
+ const rpcMethod = node.rpcMethod;
8418
+ if (rpcService) parts.push(`rpcService=${rpcService}`);
8419
+ if (rpcMethod) parts.push(`rpcMethod=${rpcMethod}`);
8420
+ break;
8421
+ }
8422
+ case "WebSocketChannelNode": {
8423
+ const channel = node.channel;
8424
+ if (channel) parts.push(`channel=${channel}`);
8425
+ break;
8426
+ }
7456
8427
  default:
7457
8428
  break;
7458
8429
  }
@@ -7548,7 +8519,7 @@ async function pickEmbedder() {
7548
8519
  }
7549
8520
  async function readCache(cachePath) {
7550
8521
  try {
7551
- const raw = await import_node_fs24.promises.readFile(cachePath, "utf8");
8522
+ const raw = await import_node_fs25.promises.readFile(cachePath, "utf8");
7552
8523
  const parsed = JSON.parse(raw);
7553
8524
  if (parsed.version !== 1) return null;
7554
8525
  return parsed;
@@ -7557,8 +8528,8 @@ async function readCache(cachePath) {
7557
8528
  }
7558
8529
  }
7559
8530
  async function writeCache(cachePath, cache) {
7560
- await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(cachePath), { recursive: true });
7561
- await import_node_fs24.promises.writeFile(cachePath, JSON.stringify(cache));
8531
+ await import_node_fs25.promises.mkdir(import_node_path44.default.dirname(cachePath), { recursive: true });
8532
+ await import_node_fs25.promises.writeFile(cachePath, JSON.stringify(cache));
7562
8533
  }
7563
8534
  var VectorIndex = class {
7564
8535
  constructor(embedder, cachePath) {
@@ -7738,14 +8709,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
7738
8709
  async function main() {
7739
8710
  const baseDirEnv = process.env.NEAT_OUT_DIR;
7740
8711
  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");
8712
+ const baseDir = baseDirEnv ? import_node_path45.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path45.default.resolve(import_node_path45.default.dirname(legacyOutPath)) : import_node_path45.default.resolve("./neat-out");
8713
+ const defaultScanPath = import_node_path45.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
7743
8714
  const registry = new Projects();
7744
8715
  await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
7745
8716
  for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
7746
8717
  const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
7747
8718
  const projectScan = process.env[envKey];
7748
- await bootProject(registry, name, projectScan ? import_node_path42.default.resolve(projectScan) : void 0, baseDir);
8719
+ await bootProject(registry, name, projectScan ? import_node_path45.default.resolve(projectScan) : void 0, baseDir);
7749
8720
  }
7750
8721
  const host = process.env.HOST ?? "0.0.0.0";
7751
8722
  const port = Number(process.env.PORT ?? 8080);