@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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountBearerAuth,
3
3
  readAuthEnv
4
- } from "./chunk-ZX7PCMGZ.js";
4
+ } from "./chunk-A3322JYS.js";
5
5
 
6
6
  // src/graph.ts
7
7
  import GraphDefault from "graphology";
@@ -456,19 +456,19 @@ function confidenceFromMix(edges, now = Date.now()) {
456
456
  function longestIncomingWalk(graph, start, maxDepth) {
457
457
  let best = { path: [start], edges: [] };
458
458
  const visited = /* @__PURE__ */ new Set([start]);
459
- function step(node, path39, edges) {
460
- if (path39.length > best.path.length) {
461
- best = { path: [...path39], edges: [...edges] };
459
+ function step(node, path42, edges) {
460
+ if (path42.length > best.path.length) {
461
+ best = { path: [...path42], edges: [...edges] };
462
462
  }
463
- if (path39.length - 1 >= maxDepth) return;
463
+ if (path42.length - 1 >= maxDepth) return;
464
464
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
465
465
  for (const [srcId, edge] of incoming) {
466
466
  if (visited.has(srcId)) continue;
467
467
  visited.add(srcId);
468
- path39.push(srcId);
468
+ path42.push(srcId);
469
469
  edges.push(edge);
470
- step(srcId, path39, edges);
471
- path39.pop();
470
+ step(srcId, path42, edges);
471
+ path42.pop();
472
472
  edges.pop();
473
473
  visited.delete(srcId);
474
474
  }
@@ -476,14 +476,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
476
476
  step(start, [start], []);
477
477
  return best;
478
478
  }
479
- function databaseRootCauseShape(graph, origin, walk) {
479
+ function databaseRootCauseShape(graph, origin, walk3) {
480
480
  const targetDb = origin;
481
481
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
482
482
  if (candidatePairs.length === 0) return null;
483
- for (const id of walk.path) {
483
+ for (const id of walk3.path) {
484
484
  const owner = resolveOwningService(graph, id);
485
485
  if (!owner) continue;
486
- const { id: serviceId3, svc } = owner;
486
+ const { id: serviceId4, svc } = owner;
487
487
  const deps = svc.dependencies ?? {};
488
488
  for (const pair of candidatePairs) {
489
489
  const declared = deps[pair.driver];
@@ -496,7 +496,7 @@ function databaseRootCauseShape(graph, origin, walk) {
496
496
  );
497
497
  if (!result.compatible) {
498
498
  return {
499
- rootCauseNode: serviceId3,
499
+ rootCauseNode: serviceId4,
500
500
  rootCauseReason: result.reason ?? "incompatible driver",
501
501
  ...result.minDriverVersion ? {
502
502
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -507,11 +507,11 @@ function databaseRootCauseShape(graph, origin, walk) {
507
507
  }
508
508
  return null;
509
509
  }
510
- function serviceRootCauseShape(graph, _origin, walk) {
511
- for (const id of walk.path) {
510
+ function serviceRootCauseShape(graph, _origin, walk3) {
511
+ for (const id of walk3.path) {
512
512
  const owner = resolveOwningService(graph, id);
513
513
  if (!owner) continue;
514
- const { id: serviceId3, svc } = owner;
514
+ const { id: serviceId4, svc } = owner;
515
515
  const deps = svc.dependencies ?? {};
516
516
  const serviceNodeEngine = svc.nodeEngine;
517
517
  for (const constraint of nodeEngineConstraints()) {
@@ -520,7 +520,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
520
520
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
521
521
  if (!result.compatible && result.reason) {
522
522
  return {
523
- rootCauseNode: serviceId3,
523
+ rootCauseNode: serviceId4,
524
524
  rootCauseReason: result.reason,
525
525
  ...result.requiredNodeVersion ? {
526
526
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -535,7 +535,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
535
535
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
536
536
  if (!result.compatible && result.reason) {
537
537
  return {
538
- rootCauseNode: serviceId3,
538
+ rootCauseNode: serviceId4,
539
539
  rootCauseReason: result.reason,
540
540
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
541
541
  };
@@ -544,10 +544,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
544
544
  }
545
545
  return null;
546
546
  }
547
- function fileRootCauseShape(graph, origin, walk) {
547
+ function fileRootCauseShape(graph, origin, walk3) {
548
548
  const owner = resolveOwningService(graph, origin.id);
549
549
  if (!owner) return null;
550
- return serviceRootCauseShape(graph, owner.svc, walk);
550
+ return serviceRootCauseShape(graph, owner.svc, walk3);
551
551
  }
552
552
  var rootCauseShapes = {
553
553
  [NodeType.DatabaseNode]: databaseRootCauseShape,
@@ -559,16 +559,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
559
559
  const origin = graph.getNodeAttributes(errorNodeId);
560
560
  const shape = rootCauseShapes[origin.type];
561
561
  if (shape) {
562
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
563
- const match = shape(graph, origin, walk);
562
+ const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
563
+ const match = shape(graph, origin, walk3);
564
564
  if (match) {
565
565
  const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
566
566
  return RootCauseResultSchema.parse({
567
567
  rootCauseNode: match.rootCauseNode,
568
568
  rootCauseReason: reason,
569
- traversalPath: walk.path,
570
- edgeProvenances: walk.edges.map((e) => e.provenance),
571
- confidence: confidenceFromMix(walk.edges),
569
+ traversalPath: walk3.path,
570
+ edgeProvenances: walk3.edges.map((e) => e.provenance),
571
+ confidence: confidenceFromMix(walk3.edges),
572
572
  fixRecommendation: match.fixRecommendation
573
573
  });
574
574
  }
@@ -626,9 +626,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
626
626
  function isFailingCallEdge(e) {
627
627
  return e.type === EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
628
628
  }
629
- function callSourcesForService(graph, serviceId3) {
630
- const ids = [serviceId3];
631
- for (const edgeId of graph.outboundEdges(serviceId3)) {
629
+ function callSourcesForService(graph, serviceId4) {
630
+ const ids = [serviceId4];
631
+ for (const edgeId of graph.outboundEdges(serviceId4)) {
632
632
  const e = graph.getEdgeAttributes(edgeId);
633
633
  if (e.type !== EdgeType.CONTAINS) continue;
634
634
  const tgt = graph.getNodeAttributes(e.target);
@@ -645,9 +645,9 @@ function failingCallDominates(e, id, curEdge, curId) {
645
645
  }
646
646
  return id < curId;
647
647
  }
648
- function dominantFailingCall(graph, serviceId3, visited) {
648
+ function dominantFailingCall(graph, serviceId4, visited) {
649
649
  let best = null;
650
- for (const src of callSourcesForService(graph, serviceId3)) {
650
+ for (const src of callSourcesForService(graph, serviceId4)) {
651
651
  for (const edgeId of graph.outboundEdges(src)) {
652
652
  const e = graph.getEdgeAttributes(edgeId);
653
653
  if (!isFailingCallEdge(e)) continue;
@@ -662,26 +662,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
662
662
  return best;
663
663
  }
664
664
  function followFailingCallChain(graph, originServiceId, maxDepth) {
665
- const path39 = [originServiceId];
665
+ const path42 = [originServiceId];
666
666
  const edges = [];
667
667
  const visited = /* @__PURE__ */ new Set([originServiceId]);
668
668
  let current = originServiceId;
669
669
  for (let depth = 0; depth < maxDepth; depth++) {
670
670
  const hop = dominantFailingCall(graph, current, visited);
671
671
  if (!hop) break;
672
- path39.push(hop.nextService);
672
+ path42.push(hop.nextService);
673
673
  edges.push(hop.edge);
674
674
  visited.add(hop.nextService);
675
675
  current = hop.nextService;
676
676
  }
677
677
  if (edges.length === 0) return null;
678
- return { path: path39, edges, culprit: current };
678
+ return { path: path42, edges, culprit: current };
679
679
  }
680
680
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
681
681
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
682
682
  if (!chain) return null;
683
683
  const culprit = chain.culprit;
684
- const path39 = [...chain.path];
684
+ const path42 = [...chain.path];
685
685
  const edgeProvenances = chain.edges.map((e) => e.provenance);
686
686
  const baseConfidence = confidenceFromMix(chain.edges);
687
687
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -689,14 +689,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
689
689
  if (loc) {
690
690
  let rootCauseNode = culprit;
691
691
  if (loc.fileNode) {
692
- path39.push(loc.fileNode);
692
+ path42.push(loc.fileNode);
693
693
  edgeProvenances.push(Provenance.OBSERVED);
694
694
  rootCauseNode = loc.fileNode;
695
695
  }
696
696
  return RootCauseResultSchema.parse({
697
697
  rootCauseNode,
698
698
  rootCauseReason: loc.rootCauseReason,
699
- traversalPath: path39,
699
+ traversalPath: path42,
700
700
  edgeProvenances,
701
701
  confidence,
702
702
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -708,7 +708,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
708
708
  return RootCauseResultSchema.parse({
709
709
  rootCauseNode: culprit,
710
710
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
711
- traversalPath: path39,
711
+ traversalPath: path42,
712
712
  edgeProvenances,
713
713
  confidence,
714
714
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -1377,12 +1377,17 @@ import {
1377
1377
  Provenance as Provenance2,
1378
1378
  confidenceForObservedSignal,
1379
1379
  databaseId,
1380
+ localDatabaseId,
1380
1381
  extractedEdgeId,
1381
1382
  fileId,
1382
1383
  frontierId,
1384
+ graphqlOperationId,
1385
+ grpcMethodId,
1383
1386
  inferredEdgeId,
1387
+ infraId,
1384
1388
  observedEdgeId,
1385
- serviceId
1389
+ serviceId,
1390
+ websocketChannelId
1386
1391
  } from "@neat.is/types";
1387
1392
  var HOUR_MS = 60 * 60 * 1e3;
1388
1393
  var DAY_MS = 24 * HOUR_MS;
@@ -1719,10 +1724,82 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
1719
1724
  ]);
1720
1725
  var WIRE_SPAN_KIND_CLIENT = 3;
1721
1726
  var WIRE_SPAN_KIND_PRODUCER = 4;
1727
+ var WIRE_SPAN_KIND_CONSUMER = 5;
1722
1728
  function spanMintsObservedEdge(kind) {
1723
1729
  if (kind === void 0 || kind === 0) return true;
1724
1730
  return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
1725
1731
  }
1732
+ function spanMintsMessagingEdge(kind) {
1733
+ return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
1734
+ }
1735
+ function spanServesGraphqlOperation(kind) {
1736
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
1737
+ }
1738
+ function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
1739
+ const id = graphqlOperationId(serviceName, operationType, operationName);
1740
+ if (graph.hasNode(id)) return id;
1741
+ const node = {
1742
+ id,
1743
+ type: NodeType3.GraphQLOperationNode,
1744
+ name: operationName,
1745
+ service: serviceName,
1746
+ operationType: operationType.toLowerCase(),
1747
+ operationName,
1748
+ discoveredVia: "otel"
1749
+ };
1750
+ graph.addNode(id, node);
1751
+ return id;
1752
+ }
1753
+ function spanServesGrpcMethod(kind) {
1754
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
1755
+ }
1756
+ function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
1757
+ const id = grpcMethodId(rpcService, rpcMethod);
1758
+ if (graph.hasNode(id)) return id;
1759
+ const node = {
1760
+ id,
1761
+ type: NodeType3.GrpcMethodNode,
1762
+ name: `${rpcService}/${rpcMethod}`,
1763
+ rpcService,
1764
+ rpcMethod,
1765
+ discoveredVia: "otel"
1766
+ };
1767
+ graph.addNode(id, node);
1768
+ return id;
1769
+ }
1770
+ function spanServesWebsocketChannel(kind) {
1771
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
1772
+ }
1773
+ function ensureWebsocketChannelNode(graph, serviceName, channel) {
1774
+ const id = websocketChannelId(serviceName, channel);
1775
+ if (graph.hasNode(id)) return id;
1776
+ const node = {
1777
+ id,
1778
+ type: NodeType3.WebSocketChannelNode,
1779
+ name: channel,
1780
+ service: serviceName,
1781
+ channel,
1782
+ discoveredVia: "otel"
1783
+ };
1784
+ graph.addNode(id, node);
1785
+ return id;
1786
+ }
1787
+ function messagingDestinationKind(system) {
1788
+ return `${system}-topic`;
1789
+ }
1790
+ function ensureMessagingDestinationNode(graph, system, destination) {
1791
+ const id = infraId(messagingDestinationKind(system), destination);
1792
+ if (graph.hasNode(id)) return id;
1793
+ const node = {
1794
+ id,
1795
+ type: NodeType3.InfraNode,
1796
+ name: destination,
1797
+ provider: "self",
1798
+ kind: messagingDestinationKind(system)
1799
+ };
1800
+ graph.addNode(id, node);
1801
+ return id;
1802
+ }
1726
1803
  var PARENT_SPAN_CACHE_SIZE = 1e4;
1727
1804
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
1728
1805
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -1816,6 +1893,21 @@ function ensureDatabaseNode(graph, host, engine) {
1816
1893
  graph.addNode(id, node);
1817
1894
  return id;
1818
1895
  }
1896
+ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
1897
+ const id = localDatabaseId(serviceName, name);
1898
+ if (graph.hasNode(id)) return id;
1899
+ const node = {
1900
+ id,
1901
+ type: NodeType3.DatabaseNode,
1902
+ name,
1903
+ engine,
1904
+ engineVersion: "unknown",
1905
+ compatibleDrivers: [],
1906
+ discoveredVia: "otel"
1907
+ };
1908
+ graph.addNode(id, node);
1909
+ return id;
1910
+ }
1819
1911
  function ensureFrontierNode(graph, host, ts) {
1820
1912
  const id = frontierIdFor(host);
1821
1913
  if (graph.hasNode(id)) {
@@ -2067,10 +2159,21 @@ async function handleSpan(ctx, span) {
2067
2159
  let affectedNode = sourceId;
2068
2160
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2069
2161
  if (span.dbSystem) {
2070
- const host = pickAddress(span);
2071
- if (mintsFromCallerSide && host) {
2072
- ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2073
- const targetId = databaseId(host);
2162
+ if (mintsFromCallerSide) {
2163
+ const host = pickAddress(span);
2164
+ let targetId;
2165
+ if (host) {
2166
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2167
+ targetId = databaseId(host);
2168
+ } else {
2169
+ const localName = span.dbName ?? span.dbSystem;
2170
+ targetId = ensureLocalDatabaseNode(
2171
+ ctx.graph,
2172
+ span.service,
2173
+ localName,
2174
+ span.dbSystem
2175
+ );
2176
+ }
2074
2177
  const result = upsertObservedEdge(
2075
2178
  ctx.graph,
2076
2179
  EdgeType3.CONNECTS_TO,
@@ -2082,6 +2185,68 @@ async function handleSpan(ctx, span) {
2082
2185
  );
2083
2186
  if (result) affectedNode = targetId;
2084
2187
  }
2188
+ } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
2189
+ const targetId = ensureMessagingDestinationNode(
2190
+ ctx.graph,
2191
+ span.messagingSystem,
2192
+ span.messagingDestination
2193
+ );
2194
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? EdgeType3.CONSUMES_FROM : EdgeType3.PUBLISHES_TO;
2195
+ const result = upsertObservedEdge(
2196
+ ctx.graph,
2197
+ edgeType,
2198
+ observedSource(),
2199
+ targetId,
2200
+ ts,
2201
+ isError,
2202
+ callSiteEvidence
2203
+ );
2204
+ if (result) affectedNode = targetId;
2205
+ } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
2206
+ const targetId = ensureGraphqlOperationNode(
2207
+ ctx.graph,
2208
+ span.service,
2209
+ span.graphqlOperationType,
2210
+ span.graphqlOperationName
2211
+ );
2212
+ const result = upsertObservedEdge(
2213
+ ctx.graph,
2214
+ EdgeType3.CONTAINS,
2215
+ observedSource(),
2216
+ targetId,
2217
+ ts,
2218
+ isError,
2219
+ callSiteEvidence
2220
+ );
2221
+ if (result) affectedNode = targetId;
2222
+ } else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
2223
+ const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
2224
+ const result = upsertObservedEdge(
2225
+ ctx.graph,
2226
+ EdgeType3.CONTAINS,
2227
+ observedSource(),
2228
+ targetId,
2229
+ ts,
2230
+ isError,
2231
+ callSiteEvidence
2232
+ );
2233
+ if (result) affectedNode = targetId;
2234
+ } else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
2235
+ const targetId = ensureWebsocketChannelNode(
2236
+ ctx.graph,
2237
+ span.service,
2238
+ span.websocketChannel
2239
+ );
2240
+ const result = upsertObservedEdge(
2241
+ ctx.graph,
2242
+ EdgeType3.CONNECTS_TO,
2243
+ observedSource(),
2244
+ targetId,
2245
+ ts,
2246
+ isError,
2247
+ callSiteEvidence
2248
+ );
2249
+ if (result) affectedNode = targetId;
2085
2250
  } else {
2086
2251
  const host = pickAddress(span);
2087
2252
  let resolvedViaAddress = false;
@@ -2191,29 +2356,29 @@ function promoteFrontierNodes(graph, opts = {}) {
2191
2356
  toPromote.push({ frontierId: id, serviceId: target });
2192
2357
  });
2193
2358
  let promoted = 0;
2194
- for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
2359
+ for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
2195
2360
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
2196
2361
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
2197
2362
  if (!gate.allowed) {
2198
2363
  continue;
2199
2364
  }
2200
2365
  }
2201
- rewireFrontierEdges(graph, frontierId2, serviceId3);
2366
+ rewireFrontierEdges(graph, frontierId2, serviceId4);
2202
2367
  graph.dropNode(frontierId2);
2203
2368
  promoted++;
2204
2369
  }
2205
2370
  return promoted;
2206
2371
  }
2207
- function rewireFrontierEdges(graph, frontierId2, serviceId3) {
2372
+ function rewireFrontierEdges(graph, frontierId2, serviceId4) {
2208
2373
  const inbound = [...graph.inboundEdges(frontierId2)];
2209
2374
  const outbound = [...graph.outboundEdges(frontierId2)];
2210
2375
  for (const edgeId of inbound) {
2211
2376
  const edge = graph.getEdgeAttributes(edgeId);
2212
- rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
2377
+ rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
2213
2378
  }
2214
2379
  for (const edgeId of outbound) {
2215
2380
  const edge = graph.getEdgeAttributes(edgeId);
2216
- rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
2381
+ rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
2217
2382
  }
2218
2383
  }
2219
2384
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -2981,9 +3146,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
2981
3146
  "StatefulSet",
2982
3147
  "DaemonSet"
2983
3148
  ]);
2984
- function addAliases(graph, serviceId3, candidates) {
2985
- if (!graph.hasNode(serviceId3)) return;
2986
- const node = graph.getNodeAttributes(serviceId3);
3149
+ function addAliases(graph, serviceId4, candidates) {
3150
+ if (!graph.hasNode(serviceId4)) return;
3151
+ const node = graph.getNodeAttributes(serviceId4);
2987
3152
  if (node.type !== NodeType5.ServiceNode) return;
2988
3153
  const set = new Set(node.aliases ?? []);
2989
3154
  for (const c of candidates) {
@@ -2993,7 +3158,7 @@ function addAliases(graph, serviceId3, candidates) {
2993
3158
  }
2994
3159
  if (set.size === 0) return;
2995
3160
  const updated = { ...node, aliases: [...set].sort() };
2996
- graph.replaceNodeAttributes(serviceId3, updated);
3161
+ graph.replaceNodeAttributes(serviceId4, updated);
2997
3162
  }
2998
3163
  function indexServicesByName(services) {
2999
3164
  const map = /* @__PURE__ */ new Map();
@@ -3026,12 +3191,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3026
3191
  }
3027
3192
  if (!compose?.services) return;
3028
3193
  for (const [composeName, svc] of Object.entries(compose.services)) {
3029
- const serviceId3 = serviceIndex.get(composeName);
3030
- if (!serviceId3) continue;
3194
+ const serviceId4 = serviceIndex.get(composeName);
3195
+ if (!serviceId4) continue;
3031
3196
  const aliases = /* @__PURE__ */ new Set([composeName]);
3032
3197
  if (svc.container_name) aliases.add(svc.container_name);
3033
3198
  if (svc.hostname) aliases.add(svc.hostname);
3034
- addAliases(graph, serviceId3, aliases);
3199
+ addAliases(graph, serviceId4, aliases);
3035
3200
  }
3036
3201
  }
3037
3202
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -3147,16 +3312,28 @@ import {
3147
3312
  extractedEdgeId as extractedEdgeId3,
3148
3313
  fileId as fileId2
3149
3314
  } from "@neat.is/types";
3315
+ function buildServiceHostIndex(services) {
3316
+ const knownHosts = /* @__PURE__ */ new Set();
3317
+ const hostToNodeId = /* @__PURE__ */ new Map();
3318
+ for (const service of services) {
3319
+ const base = path10.basename(service.dir);
3320
+ knownHosts.add(base);
3321
+ knownHosts.add(service.pkg.name);
3322
+ hostToNodeId.set(base, service.node.id);
3323
+ hostToNodeId.set(service.pkg.name, service.node.id);
3324
+ }
3325
+ return { knownHosts, hostToNodeId };
3326
+ }
3150
3327
  async function walkSourceFiles(dir) {
3151
3328
  const out = [];
3152
- async function walk(current) {
3329
+ async function walk3(current) {
3153
3330
  const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
3154
3331
  for (const entry of entries) {
3155
3332
  const full = path10.join(current, entry.name);
3156
3333
  if (entry.isDirectory()) {
3157
3334
  if (IGNORED_DIRS.has(entry.name)) continue;
3158
3335
  if (await isPythonVenvDir(full)) continue;
3159
- await walk(full);
3336
+ await walk3(full);
3160
3337
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3161
3338
  // would attribute our instrumentation imports to the user's service.
3162
3339
  !isNeatAuthoredSourceFile(entry.name)) {
@@ -3164,7 +3341,7 @@ async function walkSourceFiles(dir) {
3164
3341
  }
3165
3342
  }
3166
3343
  }
3167
- await walk(dir);
3344
+ await walk3(dir);
3168
3345
  return out;
3169
3346
  }
3170
3347
  async function loadSourceFiles(dir) {
@@ -4264,20 +4441,20 @@ import {
4264
4441
  } from "@neat.is/types";
4265
4442
  async function walkConfigFiles(dir) {
4266
4443
  const out = [];
4267
- async function walk(current) {
4444
+ async function walk3(current) {
4268
4445
  const entries = await fs14.readdir(current, { withFileTypes: true });
4269
4446
  for (const entry of entries) {
4270
4447
  const full = path21.join(current, entry.name);
4271
4448
  if (entry.isDirectory()) {
4272
4449
  if (IGNORED_DIRS.has(entry.name)) continue;
4273
4450
  if (await isPythonVenvDir(full)) continue;
4274
- await walk(full);
4451
+ await walk3(full);
4275
4452
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
4276
4453
  out.push(full);
4277
4454
  }
4278
4455
  }
4279
4456
  }
4280
- await walk(dir);
4457
+ await walk3(dir);
4281
4458
  return out;
4282
4459
  }
4283
4460
  async function addConfigNodes(graph, services, scanPath) {
@@ -4325,24 +4502,487 @@ async function addConfigNodes(graph, services, scanPath) {
4325
4502
  return { nodesAdded, edgesAdded };
4326
4503
  }
4327
4504
 
4328
- // src/extract/calls/index.ts
4505
+ // src/extract/routes.ts
4506
+ import path22 from "path";
4507
+ import Parser2 from "tree-sitter";
4508
+ import JavaScript2 from "tree-sitter-javascript";
4329
4509
  import {
4330
- EdgeType as EdgeType9,
4510
+ EdgeType as EdgeType8,
4331
4511
  NodeType as NodeType9,
4512
+ Provenance as Provenance7,
4513
+ confidenceForExtracted as confidenceForExtracted5,
4514
+ extractedEdgeId as extractedEdgeId5,
4515
+ routeId
4516
+ } from "@neat.is/types";
4517
+ var PARSE_CHUNK2 = 16384;
4518
+ function parseSource2(parser, source) {
4519
+ return parser.parse(
4520
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
4521
+ );
4522
+ }
4523
+ function makeJsParser2() {
4524
+ const p = new Parser2();
4525
+ p.setLanguage(JavaScript2);
4526
+ return p;
4527
+ }
4528
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
4529
+ "get",
4530
+ "post",
4531
+ "put",
4532
+ "patch",
4533
+ "delete",
4534
+ "options",
4535
+ "head",
4536
+ "all"
4537
+ ]);
4538
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
4539
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
4540
+ function canonicalizeTemplate(raw) {
4541
+ let p = raw.split("?")[0].split("#")[0];
4542
+ if (!p.startsWith("/")) p = "/" + p;
4543
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
4544
+ return p;
4545
+ }
4546
+ function isDynamicSegment(seg) {
4547
+ if (seg.length === 0) return false;
4548
+ if (seg.includes(":")) return true;
4549
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
4550
+ if (/^\d+$/.test(seg)) return true;
4551
+ 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;
4552
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
4553
+ return false;
4554
+ }
4555
+ function normalizePathTemplate(raw) {
4556
+ const canonical = canonicalizeTemplate(raw);
4557
+ const segments = canonical.split("/").filter((s) => s.length > 0);
4558
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
4559
+ return "/" + normalised.join("/");
4560
+ }
4561
+ function walk(node, visit) {
4562
+ visit(node);
4563
+ for (let i = 0; i < node.namedChildCount; i++) {
4564
+ const child = node.namedChild(i);
4565
+ if (child) walk(child, visit);
4566
+ }
4567
+ }
4568
+ function staticStringText(node) {
4569
+ if (node.type === "string") {
4570
+ for (let i = 0; i < node.namedChildCount; i++) {
4571
+ const child = node.namedChild(i);
4572
+ if (child?.type === "string_fragment") return child.text;
4573
+ }
4574
+ return "";
4575
+ }
4576
+ if (node.type === "template_string") {
4577
+ for (let i = 0; i < node.namedChildCount; i++) {
4578
+ if (node.namedChild(i)?.type === "template_substitution") return null;
4579
+ }
4580
+ const raw = node.text;
4581
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
4582
+ }
4583
+ return null;
4584
+ }
4585
+ function objectStringProp(objNode, key) {
4586
+ for (let i = 0; i < objNode.namedChildCount; i++) {
4587
+ const pair = objNode.namedChild(i);
4588
+ if (!pair || pair.type !== "pair") continue;
4589
+ const k = pair.childForFieldName("key");
4590
+ if (!k) continue;
4591
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
4592
+ if (kText !== key) continue;
4593
+ const v = pair.childForFieldName("value");
4594
+ if (v) return staticStringText(v);
4595
+ }
4596
+ return null;
4597
+ }
4598
+ function fastifyRouteMethods(objNode) {
4599
+ for (let i = 0; i < objNode.namedChildCount; i++) {
4600
+ const pair = objNode.namedChild(i);
4601
+ if (!pair || pair.type !== "pair") continue;
4602
+ const k = pair.childForFieldName("key");
4603
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
4604
+ if (kText !== "method") continue;
4605
+ const v = pair.childForFieldName("value");
4606
+ if (!v) return [];
4607
+ if (v.type === "string" || v.type === "template_string") {
4608
+ const s = staticStringText(v);
4609
+ return s ? [s.toUpperCase()] : [];
4610
+ }
4611
+ if (v.type === "array") {
4612
+ const out = [];
4613
+ for (let j = 0; j < v.namedChildCount; j++) {
4614
+ const el = v.namedChild(j);
4615
+ if (el && (el.type === "string" || el.type === "template_string")) {
4616
+ const s = staticStringText(el);
4617
+ if (s) out.push(s.toUpperCase());
4618
+ }
4619
+ }
4620
+ return out;
4621
+ }
4622
+ }
4623
+ return [];
4624
+ }
4625
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
4626
+ const tree = parseSource2(parser, source);
4627
+ const out = [];
4628
+ const framework = hasExpress ? "express" : "fastify";
4629
+ walk(tree.rootNode, (node) => {
4630
+ if (node.type !== "call_expression") return;
4631
+ const fn = node.childForFieldName("function");
4632
+ if (!fn || fn.type !== "member_expression") return;
4633
+ const prop = fn.childForFieldName("property");
4634
+ if (!prop) return;
4635
+ const method = prop.text.toLowerCase();
4636
+ const args = node.childForFieldName("arguments");
4637
+ const first = args?.namedChild(0);
4638
+ if (!first) return;
4639
+ const line = node.startPosition.row + 1;
4640
+ if (ROUTER_METHODS.has(method)) {
4641
+ const p = staticStringText(first);
4642
+ if (p && p.startsWith("/")) {
4643
+ out.push({
4644
+ method: method === "all" ? "ALL" : method.toUpperCase(),
4645
+ pathTemplate: canonicalizeTemplate(p),
4646
+ line,
4647
+ framework
4648
+ });
4649
+ }
4650
+ return;
4651
+ }
4652
+ if (method === "route" && hasFastify && first.type === "object") {
4653
+ const url = objectStringProp(first, "url");
4654
+ if (!url || !url.startsWith("/")) return;
4655
+ const methods = fastifyRouteMethods(first);
4656
+ const list = methods.length > 0 ? methods : ["ALL"];
4657
+ for (const m of list) {
4658
+ out.push({
4659
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
4660
+ pathTemplate: canonicalizeTemplate(url),
4661
+ line,
4662
+ framework: "fastify"
4663
+ });
4664
+ }
4665
+ }
4666
+ });
4667
+ return out;
4668
+ }
4669
+ function segmentsOf(relFile) {
4670
+ return toPosix2(relFile).split("/").filter((s) => s.length > 0);
4671
+ }
4672
+ function isNextAppRouteFile(relFile) {
4673
+ const segs = segmentsOf(relFile);
4674
+ if (!segs.includes("app")) return false;
4675
+ const base = segs[segs.length - 1] ?? "";
4676
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
4677
+ }
4678
+ function isNextPagesApiFile(relFile) {
4679
+ const segs = segmentsOf(relFile);
4680
+ const pagesIdx = segs.indexOf("pages");
4681
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
4682
+ const base = segs[segs.length - 1] ?? "";
4683
+ if (/^_(app|document|middleware)\./.test(base)) return false;
4684
+ return JS_ROUTE_EXTENSIONS.has(path22.extname(base));
4685
+ }
4686
+ function nextSegment(seg) {
4687
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
4688
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
4689
+ if (catchAll) return ":" + catchAll[1];
4690
+ const dynamic = seg.match(/^\[(.+?)\]$/);
4691
+ if (dynamic) return ":" + dynamic[1];
4692
+ return seg;
4693
+ }
4694
+ function nextAppPathTemplate(relFile) {
4695
+ const segs = segmentsOf(relFile);
4696
+ const appIdx = segs.lastIndexOf("app");
4697
+ const between = segs.slice(appIdx + 1, segs.length - 1);
4698
+ const parts = [];
4699
+ for (const seg of between) {
4700
+ const mapped = nextSegment(seg);
4701
+ if (mapped !== null) parts.push(mapped);
4702
+ }
4703
+ return "/" + parts.join("/");
4704
+ }
4705
+ function nextPagesApiPathTemplate(relFile) {
4706
+ const segs = segmentsOf(relFile);
4707
+ const pagesIdx = segs.indexOf("pages");
4708
+ const rest = segs.slice(pagesIdx + 1);
4709
+ const parts = [];
4710
+ for (let i = 0; i < rest.length; i++) {
4711
+ let seg = rest[i];
4712
+ if (i === rest.length - 1) {
4713
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
4714
+ if (seg === "index") continue;
4715
+ }
4716
+ const mapped = nextSegment(seg);
4717
+ if (mapped !== null) parts.push(mapped);
4718
+ }
4719
+ return "/" + parts.join("/");
4720
+ }
4721
+ function nextAppMethods(root) {
4722
+ const out = [];
4723
+ walk(root, (node) => {
4724
+ if (node.type !== "export_statement") return;
4725
+ const decl = node.childForFieldName("declaration");
4726
+ if (!decl) return;
4727
+ const line = node.startPosition.row + 1;
4728
+ if (decl.type === "function_declaration") {
4729
+ const name = decl.childForFieldName("name")?.text;
4730
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
4731
+ return;
4732
+ }
4733
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
4734
+ for (let i = 0; i < decl.namedChildCount; i++) {
4735
+ const d = decl.namedChild(i);
4736
+ if (d?.type !== "variable_declarator") continue;
4737
+ const name = d.childForFieldName("name")?.text;
4738
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
4739
+ }
4740
+ }
4741
+ });
4742
+ return out;
4743
+ }
4744
+ function nextRoutesFromFile(source, relFile, parser) {
4745
+ if (isNextAppRouteFile(relFile)) {
4746
+ const tree = parseSource2(parser, source);
4747
+ const template = nextAppPathTemplate(relFile);
4748
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
4749
+ method,
4750
+ pathTemplate: canonicalizeTemplate(template),
4751
+ line,
4752
+ framework: "next"
4753
+ }));
4754
+ }
4755
+ if (isNextPagesApiFile(relFile)) {
4756
+ return [
4757
+ {
4758
+ method: "ALL",
4759
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
4760
+ line: 1,
4761
+ framework: "next"
4762
+ }
4763
+ ];
4764
+ }
4765
+ return [];
4766
+ }
4767
+ async function addRoutes(graph, services) {
4768
+ const jsParser = makeJsParser2();
4769
+ let nodesAdded = 0;
4770
+ let edgesAdded = 0;
4771
+ for (const service of services) {
4772
+ const deps = {
4773
+ ...service.pkg.dependencies ?? {},
4774
+ ...service.pkg.devDependencies ?? {}
4775
+ };
4776
+ const hasExpress = deps["express"] !== void 0;
4777
+ const hasFastify = deps["fastify"] !== void 0;
4778
+ const hasNext = deps["next"] !== void 0;
4779
+ if (!hasExpress && !hasFastify && !hasNext) continue;
4780
+ const files = await loadSourceFiles(service.dir);
4781
+ for (const file of files) {
4782
+ if (isTestPath(file.path)) continue;
4783
+ if (!JS_ROUTE_EXTENSIONS.has(path22.extname(file.path))) continue;
4784
+ const relFile = toPosix2(path22.relative(service.dir, file.path));
4785
+ let routes;
4786
+ try {
4787
+ if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
4788
+ routes = nextRoutesFromFile(file.content, relFile, jsParser);
4789
+ } else if (hasExpress || hasFastify) {
4790
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
4791
+ } else {
4792
+ routes = [];
4793
+ }
4794
+ } catch (err) {
4795
+ recordExtractionError("route extraction", file.path, err);
4796
+ continue;
4797
+ }
4798
+ if (routes.length === 0) continue;
4799
+ for (const route of routes) {
4800
+ const rid = routeId(service.pkg.name, route.method, route.pathTemplate);
4801
+ if (!graph.hasNode(rid)) {
4802
+ const node = {
4803
+ id: rid,
4804
+ type: NodeType9.RouteNode,
4805
+ name: `${route.method} ${route.pathTemplate}`,
4806
+ service: service.pkg.name,
4807
+ method: route.method,
4808
+ pathTemplate: route.pathTemplate,
4809
+ path: relFile,
4810
+ line: route.line,
4811
+ framework: route.framework,
4812
+ discoveredVia: "static"
4813
+ };
4814
+ graph.addNode(rid, node);
4815
+ nodesAdded++;
4816
+ }
4817
+ const containsId = extractedEdgeId5(service.node.id, rid, EdgeType8.CONTAINS);
4818
+ if (!graph.hasEdge(containsId)) {
4819
+ const edge = {
4820
+ id: containsId,
4821
+ source: service.node.id,
4822
+ target: rid,
4823
+ type: EdgeType8.CONTAINS,
4824
+ provenance: Provenance7.EXTRACTED,
4825
+ confidence: confidenceForExtracted5("structural"),
4826
+ evidence: {
4827
+ file: relFile,
4828
+ line: route.line,
4829
+ snippet: snippet(file.content, route.line)
4830
+ }
4831
+ };
4832
+ graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
4833
+ edgesAdded++;
4834
+ }
4835
+ }
4836
+ }
4837
+ }
4838
+ return { nodesAdded, edgesAdded };
4839
+ }
4840
+
4841
+ // src/extract/proto.ts
4842
+ import { promises as fs15 } from "fs";
4843
+ import path23 from "path";
4844
+ import {
4845
+ EdgeType as EdgeType9,
4846
+ NodeType as NodeType10,
4332
4847
  Provenance as Provenance8,
4333
4848
  confidenceForExtracted as confidenceForExtracted6,
4334
- passesExtractedFloor as passesExtractedFloor2
4849
+ extractedEdgeId as extractedEdgeId6,
4850
+ grpcMethodId as grpcMethodId2
4851
+ } from "@neat.is/types";
4852
+ var PROTO_EXTENSION = ".proto";
4853
+ function packageOf(content) {
4854
+ const m = content.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/m);
4855
+ return m ? m[1] : null;
4856
+ }
4857
+ function lineAt(content, offset) {
4858
+ return content.slice(0, offset).split("\n").length;
4859
+ }
4860
+ function grpcMethodsFromProto(content, fqPackage) {
4861
+ const out = [];
4862
+ const serviceRe = /\bservice\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g;
4863
+ let sm;
4864
+ while ((sm = serviceRe.exec(content)) !== null) {
4865
+ const serviceName = sm[1];
4866
+ const rpcService = fqPackage ? `${fqPackage}.${serviceName}` : serviceName;
4867
+ const bodyStart = serviceRe.lastIndex;
4868
+ let depth = 1;
4869
+ let i = bodyStart;
4870
+ for (; i < content.length && depth > 0; i++) {
4871
+ const ch = content[i];
4872
+ if (ch === "{") depth++;
4873
+ else if (ch === "}") depth--;
4874
+ }
4875
+ const body = content.slice(bodyStart, i - 1);
4876
+ const rpcRe = /\brpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
4877
+ let rm;
4878
+ while ((rm = rpcRe.exec(body)) !== null) {
4879
+ const rpcMethod = rm[1];
4880
+ const line = lineAt(content, bodyStart + rm.index);
4881
+ out.push({ rpcService, rpcMethod, line });
4882
+ }
4883
+ serviceRe.lastIndex = i;
4884
+ }
4885
+ return out;
4886
+ }
4887
+ async function walkProtoFiles(dir) {
4888
+ const out = [];
4889
+ async function walk3(current) {
4890
+ const entries = await fs15.readdir(current, { withFileTypes: true }).catch(() => []);
4891
+ for (const entry of entries) {
4892
+ const full = path23.join(current, entry.name);
4893
+ if (entry.isDirectory()) {
4894
+ if (IGNORED_DIRS.has(entry.name)) continue;
4895
+ if (await isPythonVenvDir(full)) continue;
4896
+ await walk3(full);
4897
+ } else if (entry.isFile() && path23.extname(entry.name) === PROTO_EXTENSION) {
4898
+ out.push(full);
4899
+ }
4900
+ }
4901
+ }
4902
+ await walk3(dir);
4903
+ return out;
4904
+ }
4905
+ async function addGrpcMethods(graph, services) {
4906
+ let nodesAdded = 0;
4907
+ let edgesAdded = 0;
4908
+ for (const service of services) {
4909
+ const protoPaths = await walkProtoFiles(service.dir);
4910
+ for (const protoPath of protoPaths) {
4911
+ if (isTestPath(protoPath)) continue;
4912
+ const relFile = toPosix2(path23.relative(service.dir, protoPath));
4913
+ let content;
4914
+ try {
4915
+ content = await fs15.readFile(protoPath, "utf8");
4916
+ } catch (err) {
4917
+ recordExtractionError("proto extraction", protoPath, err);
4918
+ continue;
4919
+ }
4920
+ let methods;
4921
+ try {
4922
+ methods = grpcMethodsFromProto(content, packageOf(content));
4923
+ } catch (err) {
4924
+ recordExtractionError("proto extraction", protoPath, err);
4925
+ continue;
4926
+ }
4927
+ if (methods.length === 0) continue;
4928
+ for (const method of methods) {
4929
+ const mid = grpcMethodId2(method.rpcService, method.rpcMethod);
4930
+ if (!graph.hasNode(mid)) {
4931
+ const node = {
4932
+ id: mid,
4933
+ type: NodeType10.GrpcMethodNode,
4934
+ name: `${method.rpcService}/${method.rpcMethod}`,
4935
+ rpcService: method.rpcService,
4936
+ rpcMethod: method.rpcMethod,
4937
+ path: relFile,
4938
+ line: method.line,
4939
+ discoveredVia: "static"
4940
+ };
4941
+ graph.addNode(mid, node);
4942
+ nodesAdded++;
4943
+ }
4944
+ const containsId = extractedEdgeId6(service.node.id, mid, EdgeType9.CONTAINS);
4945
+ if (!graph.hasEdge(containsId)) {
4946
+ const edge = {
4947
+ id: containsId,
4948
+ source: service.node.id,
4949
+ target: mid,
4950
+ type: EdgeType9.CONTAINS,
4951
+ provenance: Provenance8.EXTRACTED,
4952
+ confidence: confidenceForExtracted6("structural"),
4953
+ evidence: {
4954
+ file: relFile,
4955
+ line: method.line,
4956
+ snippet: snippet(content, method.line)
4957
+ }
4958
+ };
4959
+ graph.addEdgeWithKey(containsId, service.node.id, mid, edge);
4960
+ edgesAdded++;
4961
+ }
4962
+ }
4963
+ }
4964
+ }
4965
+ return { nodesAdded, edgesAdded };
4966
+ }
4967
+
4968
+ // src/extract/calls/index.ts
4969
+ import {
4970
+ EdgeType as EdgeType12,
4971
+ NodeType as NodeType12,
4972
+ Provenance as Provenance11,
4973
+ confidenceForExtracted as confidenceForExtracted9,
4974
+ passesExtractedFloor as passesExtractedFloor3
4335
4975
  } from "@neat.is/types";
4336
4976
 
4337
4977
  // src/extract/calls/http.ts
4338
- import path22 from "path";
4339
- import Parser2 from "tree-sitter";
4340
- import JavaScript2 from "tree-sitter-javascript";
4978
+ import path24 from "path";
4979
+ import Parser3 from "tree-sitter";
4980
+ import JavaScript3 from "tree-sitter-javascript";
4341
4981
  import Python2 from "tree-sitter-python";
4342
4982
  import {
4343
- EdgeType as EdgeType8,
4344
- Provenance as Provenance7,
4345
- confidenceForExtracted as confidenceForExtracted5,
4983
+ EdgeType as EdgeType10,
4984
+ Provenance as Provenance9,
4985
+ confidenceForExtracted as confidenceForExtracted7,
4346
4986
  passesExtractedFloor
4347
4987
  } from "@neat.is/types";
4348
4988
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -4372,14 +5012,14 @@ function collectStringLiterals(node, out) {
4372
5012
  if (child) collectStringLiterals(child, out);
4373
5013
  }
4374
5014
  }
4375
- var PARSE_CHUNK2 = 16384;
4376
- function parseSource2(parser, source) {
5015
+ var PARSE_CHUNK3 = 16384;
5016
+ function parseSource3(parser, source) {
4377
5017
  return parser.parse(
4378
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
5018
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
4379
5019
  );
4380
5020
  }
4381
5021
  function callsFromSource(source, parser, knownHosts) {
4382
- const tree = parseSource2(parser, source);
5022
+ const tree = parseSource3(parser, source);
4383
5023
  const literals = [];
4384
5024
  collectStringLiterals(tree.rootNode, literals);
4385
5025
  const out = [];
@@ -4393,27 +5033,20 @@ function callsFromSource(source, parser, knownHosts) {
4393
5033
  }
4394
5034
  return out;
4395
5035
  }
4396
- function makeJsParser2() {
4397
- const p = new Parser2();
4398
- p.setLanguage(JavaScript2);
5036
+ function makeJsParser3() {
5037
+ const p = new Parser3();
5038
+ p.setLanguage(JavaScript3);
4399
5039
  return p;
4400
5040
  }
4401
5041
  function makePyParser2() {
4402
- const p = new Parser2();
5042
+ const p = new Parser3();
4403
5043
  p.setLanguage(Python2);
4404
5044
  return p;
4405
5045
  }
4406
5046
  async function addHttpCallEdges(graph, services) {
4407
- const jsParser = makeJsParser2();
5047
+ const jsParser = makeJsParser3();
4408
5048
  const pyParser = makePyParser2();
4409
- const knownHosts = /* @__PURE__ */ new Set();
4410
- const hostToNodeId = /* @__PURE__ */ new Map();
4411
- for (const service of services) {
4412
- knownHosts.add(path22.basename(service.dir));
4413
- knownHosts.add(service.pkg.name);
4414
- hostToNodeId.set(path22.basename(service.dir), service.node.id);
4415
- hostToNodeId.set(service.pkg.name, service.node.id);
4416
- }
5049
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
4417
5050
  let nodesAdded = 0;
4418
5051
  let edgesAdded = 0;
4419
5052
  for (const service of services) {
@@ -4421,7 +5054,7 @@ async function addHttpCallEdges(graph, services) {
4421
5054
  const seen = /* @__PURE__ */ new Set();
4422
5055
  for (const file of files) {
4423
5056
  if (isTestPath(file.path)) continue;
4424
- const parser = path22.extname(file.path) === ".py" ? pyParser : jsParser;
5057
+ const parser = path24.extname(file.path) === ".py" ? pyParser : jsParser;
4425
5058
  let sites;
4426
5059
  try {
4427
5060
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -4430,14 +5063,14 @@ async function addHttpCallEdges(graph, services) {
4430
5063
  continue;
4431
5064
  }
4432
5065
  if (sites.length === 0) continue;
4433
- const relFile = toPosix2(path22.relative(service.dir, file.path));
5066
+ const relFile = toPosix2(path24.relative(service.dir, file.path));
4434
5067
  for (const site of sites) {
4435
5068
  const targetId = hostToNodeId.get(site.host);
4436
5069
  if (!targetId || targetId === service.node.id) continue;
4437
5070
  const dedupKey = `${relFile}|${targetId}`;
4438
5071
  if (seen.has(dedupKey)) continue;
4439
5072
  seen.add(dedupKey);
4440
- const confidence = confidenceForExtracted5("url-literal-service-target");
5073
+ const confidence = confidenceForExtracted7("url-literal-service-target");
4441
5074
  const ev = {
4442
5075
  file: relFile,
4443
5076
  line: site.line,
@@ -4455,21 +5088,21 @@ async function addHttpCallEdges(graph, services) {
4455
5088
  noteExtractedDropped({
4456
5089
  source: fileNodeId,
4457
5090
  target: targetId,
4458
- type: EdgeType8.CALLS,
5091
+ type: EdgeType10.CALLS,
4459
5092
  confidence,
4460
5093
  confidenceKind: "url-literal-service-target",
4461
5094
  evidence: ev
4462
5095
  });
4463
5096
  continue;
4464
5097
  }
4465
- const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType8.CALLS);
5098
+ const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType10.CALLS);
4466
5099
  if (!graph.hasEdge(edgeId)) {
4467
5100
  const edge = {
4468
5101
  id: edgeId,
4469
5102
  source: fileNodeId,
4470
5103
  target: targetId,
4471
- type: EdgeType8.CALLS,
4472
- provenance: Provenance7.EXTRACTED,
5104
+ type: EdgeType10.CALLS,
5105
+ provenance: Provenance9.EXTRACTED,
4473
5106
  confidence,
4474
5107
  evidence: ev
4475
5108
  };
@@ -4482,9 +5115,284 @@ async function addHttpCallEdges(graph, services) {
4482
5115
  return { nodesAdded, edgesAdded };
4483
5116
  }
4484
5117
 
5118
+ // src/extract/calls/route-match.ts
5119
+ import path25 from "path";
5120
+ import Parser4 from "tree-sitter";
5121
+ import JavaScript4 from "tree-sitter-javascript";
5122
+ import {
5123
+ EdgeType as EdgeType11,
5124
+ NodeType as NodeType11,
5125
+ Provenance as Provenance10,
5126
+ confidenceForExtracted as confidenceForExtracted8,
5127
+ passesExtractedFloor as passesExtractedFloor2,
5128
+ serviceId as serviceId3
5129
+ } from "@neat.is/types";
5130
+ var PARSE_CHUNK4 = 16384;
5131
+ function parseSource4(parser, source) {
5132
+ return parser.parse(
5133
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
5134
+ );
5135
+ }
5136
+ function makeJsParser4() {
5137
+ const p = new Parser4();
5138
+ p.setLanguage(JavaScript4);
5139
+ return p;
5140
+ }
5141
+ var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
5142
+ var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
5143
+ function walk2(node, visit) {
5144
+ visit(node);
5145
+ for (let i = 0; i < node.namedChildCount; i++) {
5146
+ const child = node.namedChild(i);
5147
+ if (child) walk2(child, visit);
5148
+ }
5149
+ }
5150
+ function reconstructUrl(node) {
5151
+ if (node.type === "string") {
5152
+ for (let i = 0; i < node.namedChildCount; i++) {
5153
+ const child = node.namedChild(i);
5154
+ if (child?.type === "string_fragment") return child.text;
5155
+ }
5156
+ return "";
5157
+ }
5158
+ if (node.type === "template_string") {
5159
+ let out = "";
5160
+ for (let i = 0; i < node.namedChildCount; i++) {
5161
+ const child = node.namedChild(i);
5162
+ if (!child) continue;
5163
+ if (child.type === "string_fragment") out += child.text;
5164
+ else if (child.type === "template_substitution") out += ":param";
5165
+ }
5166
+ if (out.length === 0) {
5167
+ const raw = node.text;
5168
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
5169
+ }
5170
+ return out;
5171
+ }
5172
+ return null;
5173
+ }
5174
+ function methodFromOptions(objNode) {
5175
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5176
+ const pair = objNode.namedChild(i);
5177
+ if (!pair || pair.type !== "pair") continue;
5178
+ const k = pair.childForFieldName("key");
5179
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5180
+ if (kText !== "method") continue;
5181
+ const v = pair.childForFieldName("value");
5182
+ if (v && (v.type === "string" || v.type === "template_string")) {
5183
+ const s = stringText(v);
5184
+ return s ? s.toUpperCase() : void 0;
5185
+ }
5186
+ }
5187
+ return void 0;
5188
+ }
5189
+ function stringText(node) {
5190
+ if (node.type === "string") {
5191
+ for (let i = 0; i < node.namedChildCount; i++) {
5192
+ const child = node.namedChild(i);
5193
+ if (child?.type === "string_fragment") return child.text;
5194
+ }
5195
+ return "";
5196
+ }
5197
+ return null;
5198
+ }
5199
+ function urlNodeFromConfig(objNode) {
5200
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5201
+ const pair = objNode.namedChild(i);
5202
+ if (!pair || pair.type !== "pair") continue;
5203
+ const k = pair.childForFieldName("key");
5204
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5205
+ if (kText === "url") return pair.childForFieldName("value");
5206
+ }
5207
+ return null;
5208
+ }
5209
+ function pathOf(urlStr) {
5210
+ try {
5211
+ const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
5212
+ const parsed = new URL(candidate);
5213
+ return parsed.pathname || "/";
5214
+ } catch {
5215
+ return null;
5216
+ }
5217
+ }
5218
+ function matchHost(urlStr, knownHosts) {
5219
+ for (const host of knownHosts) {
5220
+ if (urlMatchesHost(urlStr, host)) return host;
5221
+ }
5222
+ return null;
5223
+ }
5224
+ function clientCallSitesFromSource(source, parser, knownHosts) {
5225
+ const tree = parseSource4(parser, source);
5226
+ const out = [];
5227
+ const push = (urlNode, method, callNode) => {
5228
+ const urlStr = reconstructUrl(urlNode);
5229
+ if (!urlStr) return;
5230
+ const host = matchHost(urlStr, knownHosts);
5231
+ if (!host) return;
5232
+ const p = pathOf(urlStr);
5233
+ if (p === null) return;
5234
+ const line = callNode.startPosition.row + 1;
5235
+ out.push({
5236
+ host,
5237
+ method,
5238
+ pathTemplate: p,
5239
+ line,
5240
+ snippet: snippet(source, line)
5241
+ });
5242
+ };
5243
+ walk2(tree.rootNode, (node) => {
5244
+ if (node.type !== "call_expression") return;
5245
+ const fn = node.childForFieldName("function");
5246
+ if (!fn) return;
5247
+ const args = node.childForFieldName("arguments");
5248
+ const first = args?.namedChild(0);
5249
+ if (!first) return;
5250
+ const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
5251
+ if (fn.type === "identifier" && fnName === "fetch") {
5252
+ const opts = args?.namedChild(1);
5253
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5254
+ push(first, method, node);
5255
+ return;
5256
+ }
5257
+ if (fn.type === "identifier" && fnName === "axios") {
5258
+ if (first.type === "object") {
5259
+ const urlNode = urlNodeFromConfig(first);
5260
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5261
+ } else {
5262
+ const opts = args?.namedChild(1);
5263
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5264
+ push(first, method, node);
5265
+ }
5266
+ return;
5267
+ }
5268
+ if (fn.type === "member_expression") {
5269
+ const obj = fn.childForFieldName("object");
5270
+ const objName = obj?.text ?? "";
5271
+ if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
5272
+ if (fnName === "request" && first.type === "object") {
5273
+ const urlNode = urlNodeFromConfig(first);
5274
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5275
+ } else {
5276
+ push(first, fnName.toUpperCase(), node);
5277
+ }
5278
+ return;
5279
+ }
5280
+ if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
5281
+ const opts = args?.namedChild(1);
5282
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
5283
+ push(first, method, node);
5284
+ return;
5285
+ }
5286
+ }
5287
+ });
5288
+ return out;
5289
+ }
5290
+ function buildRouteIndex(graph) {
5291
+ const index = /* @__PURE__ */ new Map();
5292
+ graph.forEachNode((_id, attrs) => {
5293
+ const node = attrs;
5294
+ if (node.type !== NodeType11.RouteNode) return;
5295
+ const route = attrs;
5296
+ const owner = serviceId3(route.service);
5297
+ const entry = {
5298
+ method: route.method.toUpperCase(),
5299
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
5300
+ routeNodeId: route.id
5301
+ };
5302
+ const list = index.get(owner);
5303
+ if (list) list.push(entry);
5304
+ else index.set(owner, [entry]);
5305
+ });
5306
+ return index;
5307
+ }
5308
+ function findRoute(entries, method, normalizedPath) {
5309
+ return entries.find(
5310
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
5311
+ );
5312
+ }
5313
+ async function addRouteCallEdges(graph, services) {
5314
+ const jsParser = makeJsParser4();
5315
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5316
+ const routeIndex = buildRouteIndex(graph);
5317
+ if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
5318
+ let nodesAdded = 0;
5319
+ let edgesAdded = 0;
5320
+ for (const service of services) {
5321
+ const files = await loadSourceFiles(service.dir);
5322
+ const seen = /* @__PURE__ */ new Set();
5323
+ for (const file of files) {
5324
+ if (isTestPath(file.path)) continue;
5325
+ if (!JS_CLIENT_EXTENSIONS.has(path25.extname(file.path))) continue;
5326
+ let sites;
5327
+ try {
5328
+ sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
5329
+ } catch (err) {
5330
+ recordExtractionError("route-match call extraction", file.path, err);
5331
+ continue;
5332
+ }
5333
+ if (sites.length === 0) continue;
5334
+ const relFile = toPosix2(path25.relative(service.dir, file.path));
5335
+ for (const site of sites) {
5336
+ const serverServiceId = hostToNodeId.get(site.host);
5337
+ if (!serverServiceId || serverServiceId === service.node.id) continue;
5338
+ const entries = routeIndex.get(serverServiceId);
5339
+ if (!entries) continue;
5340
+ const normalizedPath = normalizePathTemplate(site.pathTemplate);
5341
+ const match = findRoute(entries, site.method, normalizedPath);
5342
+ if (!match) continue;
5343
+ const dedupKey = `${relFile}|${match.routeNodeId}`;
5344
+ if (seen.has(dedupKey)) continue;
5345
+ seen.add(dedupKey);
5346
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5347
+ graph,
5348
+ service.pkg.name,
5349
+ service.node.id,
5350
+ relFile
5351
+ );
5352
+ nodesAdded += n;
5353
+ edgesAdded += e;
5354
+ const confidence = confidenceForExtracted8("verified-call-site");
5355
+ const ev = {
5356
+ file: relFile,
5357
+ line: site.line,
5358
+ snippet: site.snippet,
5359
+ method: site.method ?? match.method,
5360
+ pathTemplate: site.pathTemplate
5361
+ };
5362
+ if (!passesExtractedFloor2(confidence)) {
5363
+ noteExtractedDropped({
5364
+ source: fileNodeId,
5365
+ target: match.routeNodeId,
5366
+ type: EdgeType11.CALLS,
5367
+ confidence,
5368
+ confidenceKind: "verified-call-site",
5369
+ evidence: ev
5370
+ });
5371
+ continue;
5372
+ }
5373
+ const edgeId = extractedEdgeId2(fileNodeId, match.routeNodeId, EdgeType11.CALLS);
5374
+ if (!graph.hasEdge(edgeId)) {
5375
+ const edge = {
5376
+ id: edgeId,
5377
+ source: fileNodeId,
5378
+ target: match.routeNodeId,
5379
+ type: EdgeType11.CALLS,
5380
+ provenance: Provenance10.EXTRACTED,
5381
+ confidence,
5382
+ evidence: ev
5383
+ };
5384
+ graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
5385
+ edgesAdded++;
5386
+ }
5387
+ }
5388
+ }
5389
+ }
5390
+ return { nodesAdded, edgesAdded };
5391
+ }
5392
+
4485
5393
  // src/extract/calls/kafka.ts
4486
- import path23 from "path";
4487
- import { infraId } from "@neat.is/types";
5394
+ import path26 from "path";
5395
+ import { infraId as infraId2 } from "@neat.is/types";
4488
5396
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
4489
5397
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
4490
5398
  function findAll(re, text) {
@@ -4505,7 +5413,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4505
5413
  seen.add(key);
4506
5414
  const line = lineOf(file.content, topic);
4507
5415
  out.push({
4508
- infraId: infraId("kafka-topic", topic),
5416
+ infraId: infraId2("kafka-topic", topic),
4509
5417
  name: topic,
4510
5418
  kind: "kafka-topic",
4511
5419
  edgeType,
@@ -4514,7 +5422,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4514
5422
  // tier (ADR-066).
4515
5423
  confidenceKind: "verified-call-site",
4516
5424
  evidence: {
4517
- file: path23.relative(serviceDir, file.path),
5425
+ file: path26.relative(serviceDir, file.path),
4518
5426
  line,
4519
5427
  snippet: snippet(file.content, line)
4520
5428
  }
@@ -4526,8 +5434,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4526
5434
  }
4527
5435
 
4528
5436
  // src/extract/calls/redis.ts
4529
- import path24 from "path";
4530
- import { infraId as infraId2 } from "@neat.is/types";
5437
+ import path27 from "path";
5438
+ import { infraId as infraId3 } from "@neat.is/types";
4531
5439
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
4532
5440
  function redisEndpointsFromFile(file, serviceDir) {
4533
5441
  const out = [];
@@ -4540,7 +5448,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4540
5448
  seen.add(host);
4541
5449
  const line = lineOf(file.content, host);
4542
5450
  out.push({
4543
- infraId: infraId2("redis", host),
5451
+ infraId: infraId3("redis", host),
4544
5452
  name: host,
4545
5453
  kind: "redis",
4546
5454
  edgeType: "CALLS",
@@ -4549,7 +5457,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4549
5457
  // support tier (ADR-066).
4550
5458
  confidenceKind: "url-with-structural-support",
4551
5459
  evidence: {
4552
- file: path24.relative(serviceDir, file.path),
5460
+ file: path27.relative(serviceDir, file.path),
4553
5461
  line,
4554
5462
  snippet: snippet(file.content, line)
4555
5463
  }
@@ -4559,8 +5467,8 @@ function redisEndpointsFromFile(file, serviceDir) {
4559
5467
  }
4560
5468
 
4561
5469
  // src/extract/calls/aws.ts
4562
- import path25 from "path";
4563
- import { infraId as infraId3 } from "@neat.is/types";
5470
+ import path28 from "path";
5471
+ import { infraId as infraId4 } from "@neat.is/types";
4564
5472
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
4565
5473
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
4566
5474
  function hasMarker(text, markers) {
@@ -4584,7 +5492,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4584
5492
  seen.add(key);
4585
5493
  const line = lineOf(file.content, name);
4586
5494
  out.push({
4587
- infraId: infraId3(kind, name),
5495
+ infraId: infraId4(kind, name),
4588
5496
  name,
4589
5497
  kind,
4590
5498
  edgeType: "CALLS",
@@ -4593,7 +5501,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4593
5501
  // (ADR-066).
4594
5502
  confidenceKind: "verified-call-site",
4595
5503
  evidence: {
4596
- file: path25.relative(serviceDir, file.path),
5504
+ file: path28.relative(serviceDir, file.path),
4597
5505
  line,
4598
5506
  snippet: snippet(file.content, line)
4599
5507
  }
@@ -4617,8 +5525,8 @@ function awsEndpointsFromFile(file, serviceDir) {
4617
5525
  }
4618
5526
 
4619
5527
  // src/extract/calls/grpc.ts
4620
- import path26 from "path";
4621
- import { infraId as infraId4 } from "@neat.is/types";
5528
+ import path29 from "path";
5529
+ import { infraId as infraId5 } from "@neat.is/types";
4622
5530
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
4623
5531
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
4624
5532
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -4667,7 +5575,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4667
5575
  const { kind } = classified;
4668
5576
  const line = lineOf(file.content, m[0]);
4669
5577
  out.push({
4670
- infraId: infraId4(kind, name),
5578
+ infraId: infraId5(kind, name),
4671
5579
  name,
4672
5580
  kind,
4673
5581
  edgeType: "CALLS",
@@ -4676,7 +5584,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4676
5584
  // tier (ADR-066).
4677
5585
  confidenceKind: "verified-call-site",
4678
5586
  evidence: {
4679
- file: path26.relative(serviceDir, file.path),
5587
+ file: path29.relative(serviceDir, file.path),
4680
5588
  line,
4681
5589
  snippet: snippet(file.content, line)
4682
5590
  }
@@ -4686,8 +5594,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
4686
5594
  }
4687
5595
 
4688
5596
  // src/extract/calls/supabase.ts
4689
- import path27 from "path";
4690
- import { infraId as infraId5 } from "@neat.is/types";
5597
+ import path30 from "path";
5598
+ import { infraId as infraId6 } from "@neat.is/types";
4691
5599
  var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4692
5600
  var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4693
5601
  var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
@@ -4725,7 +5633,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
4725
5633
  seen.add(name);
4726
5634
  const line = lineOf(file.content, m[0]);
4727
5635
  out.push({
4728
- infraId: infraId5("supabase", name),
5636
+ infraId: infraId6("supabase", name),
4729
5637
  name,
4730
5638
  kind: "supabase",
4731
5639
  edgeType: "CALLS",
@@ -4735,7 +5643,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
4735
5643
  // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4736
5644
  confidenceKind: "verified-call-site",
4737
5645
  evidence: {
4738
- file: path27.relative(serviceDir, file.path),
5646
+ file: path30.relative(serviceDir, file.path),
4739
5647
  line,
4740
5648
  snippet: snippet(file.content, line)
4741
5649
  }
@@ -4748,11 +5656,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
4748
5656
  function edgeTypeFromEndpoint(ep) {
4749
5657
  switch (ep.edgeType) {
4750
5658
  case "PUBLISHES_TO":
4751
- return EdgeType9.PUBLISHES_TO;
5659
+ return EdgeType12.PUBLISHES_TO;
4752
5660
  case "CONSUMES_FROM":
4753
- return EdgeType9.CONSUMES_FROM;
5661
+ return EdgeType12.CONSUMES_FROM;
4754
5662
  default:
4755
- return EdgeType9.CALLS;
5663
+ return EdgeType12.CALLS;
4756
5664
  }
4757
5665
  }
4758
5666
  function isAwsKind(kind) {
@@ -4780,7 +5688,7 @@ async function addExternalEndpointEdges(graph, services) {
4780
5688
  if (!graph.hasNode(ep.infraId)) {
4781
5689
  const node = {
4782
5690
  id: ep.infraId,
4783
- type: NodeType9.InfraNode,
5691
+ type: NodeType12.InfraNode,
4784
5692
  name: ep.name,
4785
5693
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4786
5694
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4792,7 +5700,7 @@ async function addExternalEndpointEdges(graph, services) {
4792
5700
  nodesAdded++;
4793
5701
  }
4794
5702
  const edgeType = edgeTypeFromEndpoint(ep);
4795
- const confidence = confidenceForExtracted6(ep.confidenceKind);
5703
+ const confidence = confidenceForExtracted9(ep.confidenceKind);
4796
5704
  const relFile = toPosix2(ep.evidence.file);
4797
5705
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4798
5706
  graph,
@@ -4802,7 +5710,7 @@ async function addExternalEndpointEdges(graph, services) {
4802
5710
  );
4803
5711
  nodesAdded += n;
4804
5712
  edgesAdded += e;
4805
- if (!passesExtractedFloor2(confidence)) {
5713
+ if (!passesExtractedFloor3(confidence)) {
4806
5714
  noteExtractedDropped({
4807
5715
  source: fileNodeId,
4808
5716
  target: ep.infraId,
@@ -4822,7 +5730,7 @@ async function addExternalEndpointEdges(graph, services) {
4822
5730
  source: fileNodeId,
4823
5731
  target: ep.infraId,
4824
5732
  type: edgeType,
4825
- provenance: Provenance8.EXTRACTED,
5733
+ provenance: Provenance11.EXTRACTED,
4826
5734
  confidence,
4827
5735
  evidence: ep.evidence
4828
5736
  };
@@ -4836,22 +5744,23 @@ async function addExternalEndpointEdges(graph, services) {
4836
5744
  async function addCallEdges(graph, services) {
4837
5745
  const http = await addHttpCallEdges(graph, services);
4838
5746
  const ext = await addExternalEndpointEdges(graph, services);
5747
+ const routes = await addRouteCallEdges(graph, services);
4839
5748
  return {
4840
- nodesAdded: http.nodesAdded + ext.nodesAdded,
4841
- edgesAdded: http.edgesAdded + ext.edgesAdded
5749
+ nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
5750
+ edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
4842
5751
  };
4843
5752
  }
4844
5753
 
4845
5754
  // src/extract/infra/docker-compose.ts
4846
- import path28 from "path";
4847
- import { EdgeType as EdgeType10, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
5755
+ import path31 from "path";
5756
+ import { EdgeType as EdgeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10 } from "@neat.is/types";
4848
5757
 
4849
5758
  // src/extract/infra/shared.ts
4850
- import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
5759
+ import { NodeType as NodeType13, infraId as infraId7 } from "@neat.is/types";
4851
5760
  function makeInfraNode(kind, name, provider = "self", extras) {
4852
5761
  return {
4853
- id: infraId6(kind, name),
4854
- type: NodeType10.InfraNode,
5762
+ id: infraId7(kind, name),
5763
+ type: NodeType13.InfraNode,
4855
5764
  name,
4856
5765
  provider,
4857
5766
  kind,
@@ -4880,7 +5789,7 @@ function dependsOnList(value) {
4880
5789
  }
4881
5790
  function serviceNameToServiceNode(name, services) {
4882
5791
  for (const s of services) {
4883
- if (s.node.name === name || path28.basename(s.dir) === name) return s.node.id;
5792
+ if (s.node.name === name || path31.basename(s.dir) === name) return s.node.id;
4884
5793
  }
4885
5794
  return null;
4886
5795
  }
@@ -4889,7 +5798,7 @@ async function addComposeInfra(graph, scanPath, services) {
4889
5798
  let edgesAdded = 0;
4890
5799
  let composePath = null;
4891
5800
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4892
- const abs = path28.join(scanPath, name);
5801
+ const abs = path31.join(scanPath, name);
4893
5802
  if (await exists(abs)) {
4894
5803
  composePath = abs;
4895
5804
  break;
@@ -4902,13 +5811,13 @@ async function addComposeInfra(graph, scanPath, services) {
4902
5811
  } catch (err) {
4903
5812
  recordExtractionError(
4904
5813
  "infra docker-compose",
4905
- path28.relative(scanPath, composePath),
5814
+ path31.relative(scanPath, composePath),
4906
5815
  err
4907
5816
  );
4908
5817
  return { nodesAdded, edgesAdded };
4909
5818
  }
4910
5819
  if (!compose?.services) return { nodesAdded, edgesAdded };
4911
- const evidenceFile = path28.relative(scanPath, composePath).split(path28.sep).join("/");
5820
+ const evidenceFile = path31.relative(scanPath, composePath).split(path31.sep).join("/");
4912
5821
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4913
5822
  for (const [composeName, svc] of Object.entries(compose.services)) {
4914
5823
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4930,15 +5839,15 @@ async function addComposeInfra(graph, scanPath, services) {
4930
5839
  for (const dep of dependsOnList(svc.depends_on)) {
4931
5840
  const targetId = composeNameToNodeId.get(dep);
4932
5841
  if (!targetId) continue;
4933
- const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType10.DEPENDS_ON);
5842
+ const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType13.DEPENDS_ON);
4934
5843
  if (graph.hasEdge(edgeId)) continue;
4935
5844
  const edge = {
4936
5845
  id: edgeId,
4937
5846
  source: sourceId,
4938
5847
  target: targetId,
4939
- type: EdgeType10.DEPENDS_ON,
4940
- provenance: Provenance9.EXTRACTED,
4941
- confidence: confidenceForExtracted7("structural"),
5848
+ type: EdgeType13.DEPENDS_ON,
5849
+ provenance: Provenance12.EXTRACTED,
5850
+ confidence: confidenceForExtracted10("structural"),
4942
5851
  evidence: { file: evidenceFile }
4943
5852
  };
4944
5853
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4949,9 +5858,9 @@ async function addComposeInfra(graph, scanPath, services) {
4949
5858
  }
4950
5859
 
4951
5860
  // src/extract/infra/dockerfile.ts
4952
- import path29 from "path";
4953
- import { promises as fs15 } from "fs";
4954
- import { EdgeType as EdgeType11, Provenance as Provenance10, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
5861
+ import path32 from "path";
5862
+ import { promises as fs16 } from "fs";
5863
+ import { EdgeType as EdgeType14, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
4955
5864
  function readDockerfile(content) {
4956
5865
  let image = null;
4957
5866
  const ports = [];
@@ -4980,15 +5889,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4980
5889
  let nodesAdded = 0;
4981
5890
  let edgesAdded = 0;
4982
5891
  for (const service of services) {
4983
- const dockerfilePath = path29.join(service.dir, "Dockerfile");
5892
+ const dockerfilePath = path32.join(service.dir, "Dockerfile");
4984
5893
  if (!await exists(dockerfilePath)) continue;
4985
5894
  let content;
4986
5895
  try {
4987
- content = await fs15.readFile(dockerfilePath, "utf8");
5896
+ content = await fs16.readFile(dockerfilePath, "utf8");
4988
5897
  } catch (err) {
4989
5898
  recordExtractionError(
4990
5899
  "infra dockerfile",
4991
- path29.relative(scanPath, dockerfilePath),
5900
+ path32.relative(scanPath, dockerfilePath),
4992
5901
  err
4993
5902
  );
4994
5903
  continue;
@@ -5000,8 +5909,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5000
5909
  graph.addNode(node.id, node);
5001
5910
  nodesAdded++;
5002
5911
  }
5003
- const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
5004
- const evidenceFile = toPosix2(path29.relative(scanPath, dockerfilePath));
5912
+ const relDockerfile = toPosix2(path32.relative(service.dir, dockerfilePath));
5913
+ const evidenceFile = toPosix2(path32.relative(scanPath, dockerfilePath));
5005
5914
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5006
5915
  graph,
5007
5916
  service.pkg.name,
@@ -5010,15 +5919,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5010
5919
  );
5011
5920
  nodesAdded += fn;
5012
5921
  edgesAdded += fe;
5013
- const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType11.RUNS_ON);
5922
+ const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType14.RUNS_ON);
5014
5923
  if (!graph.hasEdge(edgeId)) {
5015
5924
  const edge = {
5016
5925
  id: edgeId,
5017
5926
  source: fileNodeId,
5018
5927
  target: node.id,
5019
- type: EdgeType11.RUNS_ON,
5020
- provenance: Provenance10.EXTRACTED,
5021
- confidence: confidenceForExtracted8("structural"),
5928
+ type: EdgeType14.RUNS_ON,
5929
+ provenance: Provenance13.EXTRACTED,
5930
+ confidence: confidenceForExtracted11("structural"),
5022
5931
  evidence: {
5023
5932
  file: evidenceFile,
5024
5933
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -5033,15 +5942,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5033
5942
  graph.addNode(portNode.id, portNode);
5034
5943
  nodesAdded++;
5035
5944
  }
5036
- const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType11.CONNECTS_TO);
5945
+ const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType14.CONNECTS_TO);
5037
5946
  if (graph.hasEdge(portEdgeId)) continue;
5038
5947
  const portEdge = {
5039
5948
  id: portEdgeId,
5040
5949
  source: fileNodeId,
5041
5950
  target: portNode.id,
5042
- type: EdgeType11.CONNECTS_TO,
5043
- provenance: Provenance10.EXTRACTED,
5044
- confidence: confidenceForExtracted8("structural"),
5951
+ type: EdgeType14.CONNECTS_TO,
5952
+ provenance: Provenance13.EXTRACTED,
5953
+ confidence: confidenceForExtracted11("structural"),
5045
5954
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5046
5955
  };
5047
5956
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -5052,23 +5961,23 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5052
5961
  }
5053
5962
 
5054
5963
  // src/extract/infra/terraform.ts
5055
- import { promises as fs16 } from "fs";
5056
- import path30 from "path";
5057
- import { EdgeType as EdgeType12, Provenance as Provenance11, confidenceForExtracted as confidenceForExtracted9 } from "@neat.is/types";
5964
+ import { promises as fs17 } from "fs";
5965
+ import path33 from "path";
5966
+ import { EdgeType as EdgeType15, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
5058
5967
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5059
5968
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5060
5969
  async function walkTfFiles(start, depth = 0, max = 5) {
5061
5970
  if (depth > max) return [];
5062
5971
  const out = [];
5063
- const entries = await fs16.readdir(start, { withFileTypes: true }).catch(() => []);
5972
+ const entries = await fs17.readdir(start, { withFileTypes: true }).catch(() => []);
5064
5973
  for (const entry of entries) {
5065
5974
  if (entry.isDirectory()) {
5066
5975
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
5067
- const child = path30.join(start, entry.name);
5976
+ const child = path33.join(start, entry.name);
5068
5977
  if (await isPythonVenvDir(child)) continue;
5069
5978
  out.push(...await walkTfFiles(child, depth + 1, max));
5070
5979
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
5071
- out.push(path30.join(start, entry.name));
5980
+ out.push(path33.join(start, entry.name));
5072
5981
  }
5073
5982
  }
5074
5983
  return out;
@@ -5087,7 +5996,7 @@ function blockBody(content, from) {
5087
5996
  }
5088
5997
  return null;
5089
5998
  }
5090
- function lineAt(content, index) {
5999
+ function lineAt2(content, index) {
5091
6000
  let line = 1;
5092
6001
  for (let i = 0; i < index && i < content.length; i++) {
5093
6002
  if (content[i] === "\n") line++;
@@ -5099,8 +6008,8 @@ async function addTerraformResources(graph, scanPath) {
5099
6008
  let edgesAdded = 0;
5100
6009
  const files = await walkTfFiles(scanPath);
5101
6010
  for (const file of files) {
5102
- const content = await fs16.readFile(file, "utf8");
5103
- const evidenceFile = toPosix2(path30.relative(scanPath, file));
6011
+ const content = await fs17.readFile(file, "utf8");
6012
+ const evidenceFile = toPosix2(path33.relative(scanPath, file));
5104
6013
  const resources = [];
5105
6014
  const byKey = /* @__PURE__ */ new Map();
5106
6015
  RESOURCE_RE.lastIndex = 0;
@@ -5135,16 +6044,16 @@ async function addTerraformResources(graph, scanPath) {
5135
6044
  if (!target) continue;
5136
6045
  if (seen.has(target.nodeId)) continue;
5137
6046
  seen.add(target.nodeId);
5138
- const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType12.DEPENDS_ON);
6047
+ const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType15.DEPENDS_ON);
5139
6048
  if (graph.hasEdge(edgeId)) continue;
5140
- const line = lineAt(content, resource.bodyOffset + ref.index);
6049
+ const line = lineAt2(content, resource.bodyOffset + ref.index);
5141
6050
  const edge = {
5142
6051
  id: edgeId,
5143
6052
  source: resource.nodeId,
5144
6053
  target: target.nodeId,
5145
- type: EdgeType12.DEPENDS_ON,
5146
- provenance: Provenance11.EXTRACTED,
5147
- confidence: confidenceForExtracted9("structural"),
6054
+ type: EdgeType15.DEPENDS_ON,
6055
+ provenance: Provenance14.EXTRACTED,
6056
+ confidence: confidenceForExtracted12("structural"),
5148
6057
  evidence: { file: evidenceFile, line, snippet: key }
5149
6058
  };
5150
6059
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5156,8 +6065,8 @@ async function addTerraformResources(graph, scanPath) {
5156
6065
  }
5157
6066
 
5158
6067
  // src/extract/infra/k8s.ts
5159
- import { promises as fs17 } from "fs";
5160
- import path31 from "path";
6068
+ import { promises as fs18 } from "fs";
6069
+ import path34 from "path";
5161
6070
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
5162
6071
  var K8S_KIND_TO_INFRA_KIND = {
5163
6072
  Service: "k8s-service",
@@ -5171,15 +6080,15 @@ var K8S_KIND_TO_INFRA_KIND = {
5171
6080
  async function walkYamlFiles2(start, depth = 0, max = 5) {
5172
6081
  if (depth > max) return [];
5173
6082
  const out = [];
5174
- const entries = await fs17.readdir(start, { withFileTypes: true }).catch(() => []);
6083
+ const entries = await fs18.readdir(start, { withFileTypes: true }).catch(() => []);
5175
6084
  for (const entry of entries) {
5176
6085
  if (entry.isDirectory()) {
5177
6086
  if (IGNORED_DIRS.has(entry.name)) continue;
5178
- const child = path31.join(start, entry.name);
6087
+ const child = path34.join(start, entry.name);
5179
6088
  if (await isPythonVenvDir(child)) continue;
5180
6089
  out.push(...await walkYamlFiles2(child, depth + 1, max));
5181
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path31.extname(entry.name))) {
5182
- out.push(path31.join(start, entry.name));
6090
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path34.extname(entry.name))) {
6091
+ out.push(path34.join(start, entry.name));
5183
6092
  }
5184
6093
  }
5185
6094
  return out;
@@ -5188,7 +6097,7 @@ async function addK8sResources(graph, scanPath) {
5188
6097
  let nodesAdded = 0;
5189
6098
  const files = await walkYamlFiles2(scanPath);
5190
6099
  for (const file of files) {
5191
- const content = await fs17.readFile(file, "utf8");
6100
+ const content = await fs18.readFile(file, "utf8");
5192
6101
  let docs;
5193
6102
  try {
5194
6103
  docs = parseAllDocuments2(content).map((d) => d.toJSON());
@@ -5223,16 +6132,16 @@ async function addInfra(graph, scanPath, services) {
5223
6132
  }
5224
6133
 
5225
6134
  // src/extract/index.ts
5226
- import path33 from "path";
6135
+ import path36 from "path";
5227
6136
 
5228
6137
  // src/extract/retire.ts
5229
6138
  import { existsSync as existsSync2 } from "fs";
5230
- import path32 from "path";
5231
- import { NodeType as NodeType11, Provenance as Provenance12 } from "@neat.is/types";
6139
+ import path35 from "path";
6140
+ import { NodeType as NodeType14, Provenance as Provenance15 } from "@neat.is/types";
5232
6141
  function dropOrphanedFileNodes(graph) {
5233
6142
  const orphans = [];
5234
6143
  graph.forEachNode((id, attrs) => {
5235
- if (attrs.type !== NodeType11.FileNode) return;
6144
+ if (attrs.type !== NodeType14.FileNode) return;
5236
6145
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5237
6146
  orphans.push(id);
5238
6147
  }
@@ -5245,7 +6154,7 @@ function retireEdgesByFile(graph, file) {
5245
6154
  const toDrop = [];
5246
6155
  graph.forEachEdge((id, attrs) => {
5247
6156
  const edge = attrs;
5248
- if (edge.provenance !== Provenance12.EXTRACTED) return;
6157
+ if (edge.provenance !== Provenance15.EXTRACTED) return;
5249
6158
  if (!edge.evidence?.file) return;
5250
6159
  if (edge.evidence.file === normalized) toDrop.push(id);
5251
6160
  });
@@ -5258,14 +6167,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5258
6167
  const bases = [scanPath, ...serviceDirs];
5259
6168
  graph.forEachEdge((id, attrs) => {
5260
6169
  const edge = attrs;
5261
- if (edge.provenance !== Provenance12.EXTRACTED) return;
6170
+ if (edge.provenance !== Provenance15.EXTRACTED) return;
5262
6171
  const evidenceFile = edge.evidence?.file;
5263
6172
  if (!evidenceFile) return;
5264
- if (path32.isAbsolute(evidenceFile)) {
6173
+ if (path35.isAbsolute(evidenceFile)) {
5265
6174
  if (!existsSync2(evidenceFile)) toDrop.push(id);
5266
6175
  return;
5267
6176
  }
5268
- const found = bases.some((base) => existsSync2(path32.join(base, evidenceFile)));
6177
+ const found = bases.some((base) => existsSync2(path35.join(base, evidenceFile)));
5269
6178
  if (!found) toDrop.push(id);
5270
6179
  });
5271
6180
  for (const id of toDrop) graph.dropEdge(id);
@@ -5284,6 +6193,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5284
6193
  const importGraph = await addImports(graph, services);
5285
6194
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
5286
6195
  const phase3 = await addConfigNodes(graph, services, scanPath);
6196
+ const routePhase = await addRoutes(graph, services);
6197
+ const grpcPhase = await addGrpcMethods(graph, services);
5287
6198
  const phase4 = await addCallEdges(graph, services);
5288
6199
  const phase5 = await addInfra(graph, scanPath, services);
5289
6200
  const ghostsRetired = retireExtractedEdgesByMissingFile(
@@ -5305,7 +6216,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5305
6216
  }
5306
6217
  const droppedEntries = drainDroppedExtracted();
5307
6218
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5308
- const rejectedPath = path33.join(path33.dirname(opts.errorsPath), "rejected.ndjson");
6219
+ const rejectedPath = path36.join(path36.dirname(opts.errorsPath), "rejected.ndjson");
5309
6220
  try {
5310
6221
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5311
6222
  } catch (err) {
@@ -5315,8 +6226,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5315
6226
  }
5316
6227
  }
5317
6228
  const result = {
5318
- nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
5319
- edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
6229
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
6230
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
5320
6231
  frontiersPromoted,
5321
6232
  extractionErrors: errorEntries.length,
5322
6233
  errorEntries,
@@ -5341,10 +6252,10 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5341
6252
  import {
5342
6253
  databaseId as databaseId3,
5343
6254
  DivergenceResultSchema,
5344
- EdgeType as EdgeType13,
5345
- NodeType as NodeType12,
6255
+ EdgeType as EdgeType16,
6256
+ NodeType as NodeType15,
5346
6257
  parseEdgeId,
5347
- Provenance as Provenance13
6258
+ Provenance as Provenance16
5348
6259
  } from "@neat.is/types";
5349
6260
  function bucketKey(source, target, type) {
5350
6261
  return `${type}|${source}|${target}`;
@@ -5358,17 +6269,17 @@ function bucketEdges(graph) {
5358
6269
  const key = bucketKey(e.source, e.target, e.type);
5359
6270
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5360
6271
  switch (provenance) {
5361
- case Provenance13.EXTRACTED:
6272
+ case Provenance16.EXTRACTED:
5362
6273
  cur.extracted = e;
5363
6274
  break;
5364
- case Provenance13.OBSERVED:
6275
+ case Provenance16.OBSERVED:
5365
6276
  cur.observed = e;
5366
6277
  break;
5367
- case Provenance13.INFERRED:
6278
+ case Provenance16.INFERRED:
5368
6279
  cur.inferred = e;
5369
6280
  break;
5370
6281
  default:
5371
- if (e.provenance === Provenance13.STALE) cur.stale = e;
6282
+ if (e.provenance === Provenance16.STALE) cur.stale = e;
5372
6283
  }
5373
6284
  buckets.set(key, cur);
5374
6285
  });
@@ -5377,7 +6288,12 @@ function bucketEdges(graph) {
5377
6288
  function nodeIsFrontier(graph, nodeId) {
5378
6289
  if (!graph.hasNode(nodeId)) return false;
5379
6290
  const attrs = graph.getNodeAttributes(nodeId);
5380
- return attrs.type === NodeType12.FrontierNode;
6291
+ return attrs.type === NodeType15.FrontierNode;
6292
+ }
6293
+ function nodeIsWebsocketChannel(graph, nodeId) {
6294
+ if (!graph.hasNode(nodeId)) return false;
6295
+ const attrs = graph.getNodeAttributes(nodeId);
6296
+ return attrs.type === NodeType15.WebSocketChannelNode;
5381
6297
  }
5382
6298
  function clampConfidence(n) {
5383
6299
  if (!Number.isFinite(n)) return 0;
@@ -5397,14 +6313,14 @@ function gradedConfidence(edge) {
5397
6313
  return clampConfidence(confidenceForEdge(edge));
5398
6314
  }
5399
6315
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
5400
- EdgeType13.CALLS,
5401
- EdgeType13.CONNECTS_TO,
5402
- EdgeType13.PUBLISHES_TO,
5403
- EdgeType13.CONSUMES_FROM
6316
+ EdgeType16.CALLS,
6317
+ EdgeType16.CONNECTS_TO,
6318
+ EdgeType16.PUBLISHES_TO,
6319
+ EdgeType16.CONSUMES_FROM
5404
6320
  ]);
5405
6321
  function detectMissingDivergences(graph, bucket) {
5406
6322
  const out = [];
5407
- if (bucket.type === EdgeType13.CONTAINS) return out;
6323
+ if (bucket.type === EdgeType16.CONTAINS) return out;
5408
6324
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5409
6325
  if (!nodeIsFrontier(graph, bucket.target)) {
5410
6326
  out.push({
@@ -5419,7 +6335,7 @@ function detectMissingDivergences(graph, bucket) {
5419
6335
  });
5420
6336
  }
5421
6337
  }
5422
- if (bucket.observed && !bucket.extracted) {
6338
+ if (bucket.observed && !bucket.extracted && !nodeIsWebsocketChannel(graph, bucket.target)) {
5423
6339
  out.push({
5424
6340
  type: "missing-extracted",
5425
6341
  source: bucket.source,
@@ -5445,7 +6361,7 @@ function declaredHostFor(svc) {
5445
6361
  function hasExtractedConfiguredBy(graph, svcId) {
5446
6362
  for (const edgeId of graph.outboundEdges(svcId)) {
5447
6363
  const e = graph.getEdgeAttributes(edgeId);
5448
- if (e.type === EdgeType13.CONFIGURED_BY && e.provenance === Provenance13.EXTRACTED) {
6364
+ if (e.type === EdgeType16.CONFIGURED_BY && e.provenance === Provenance16.EXTRACTED) {
5449
6365
  return true;
5450
6366
  }
5451
6367
  }
@@ -5458,10 +6374,10 @@ function detectHostMismatch(graph, svcId, svc) {
5458
6374
  const out = [];
5459
6375
  for (const edgeId of graph.outboundEdges(svcId)) {
5460
6376
  const edge = graph.getEdgeAttributes(edgeId);
5461
- if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5462
- if (edge.provenance !== Provenance13.OBSERVED) continue;
6377
+ if (edge.type !== EdgeType16.CONNECTS_TO) continue;
6378
+ if (edge.provenance !== Provenance16.OBSERVED) continue;
5463
6379
  const target = graph.getNodeAttributes(edge.target);
5464
- if (target.type !== NodeType12.DatabaseNode) continue;
6380
+ if (target.type !== NodeType15.DatabaseNode) continue;
5465
6381
  const observedHost = target.host?.trim();
5466
6382
  if (!observedHost) continue;
5467
6383
  if (observedHost === declaredHost) continue;
@@ -5483,10 +6399,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5483
6399
  const deps = svc.dependencies ?? {};
5484
6400
  for (const edgeId of graph.outboundEdges(svcId)) {
5485
6401
  const edge = graph.getEdgeAttributes(edgeId);
5486
- if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5487
- if (edge.provenance !== Provenance13.OBSERVED) continue;
6402
+ if (edge.type !== EdgeType16.CONNECTS_TO) continue;
6403
+ if (edge.provenance !== Provenance16.OBSERVED) continue;
5488
6404
  const target = graph.getNodeAttributes(edge.target);
5489
- if (target.type !== NodeType12.DatabaseNode) continue;
6405
+ if (target.type !== NodeType15.DatabaseNode) continue;
5490
6406
  for (const pair of compatPairs()) {
5491
6407
  if (pair.engine !== target.engine) continue;
5492
6408
  const declared = deps[pair.driver];
@@ -5564,7 +6480,7 @@ function computeDivergences(graph, opts = {}) {
5564
6480
  }
5565
6481
  graph.forEachNode((nodeId, attrs) => {
5566
6482
  const n = attrs;
5567
- if (n.type !== NodeType12.ServiceNode) return;
6483
+ if (n.type !== NodeType15.ServiceNode) return;
5568
6484
  const svc = n;
5569
6485
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5570
6486
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5606,9 +6522,9 @@ function computeDivergences(graph, opts = {}) {
5606
6522
  }
5607
6523
 
5608
6524
  // src/persist.ts
5609
- import { promises as fs18 } from "fs";
5610
- import path34 from "path";
5611
- import { Provenance as Provenance14, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
6525
+ import { promises as fs19 } from "fs";
6526
+ import path37 from "path";
6527
+ import { Provenance as Provenance17, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
5612
6528
  var SCHEMA_VERSION = 4;
5613
6529
  function migrateV1ToV2(payload) {
5614
6530
  const nodes = payload.graph.nodes;
@@ -5630,7 +6546,7 @@ function migrateV2ToV3(payload) {
5630
6546
  for (const edge of edges) {
5631
6547
  const attrs = edge.attributes;
5632
6548
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5633
- attrs.provenance = Provenance14.OBSERVED;
6549
+ attrs.provenance = Provenance17.OBSERVED;
5634
6550
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5635
6551
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5636
6552
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -5644,7 +6560,7 @@ function migrateV2ToV3(payload) {
5644
6560
  return { ...payload, schemaVersion: 3 };
5645
6561
  }
5646
6562
  async function ensureDir(filePath) {
5647
- await fs18.mkdir(path34.dirname(filePath), { recursive: true });
6563
+ await fs19.mkdir(path37.dirname(filePath), { recursive: true });
5648
6564
  }
5649
6565
  async function saveGraphToDisk(graph, outPath) {
5650
6566
  await ensureDir(outPath);
@@ -5654,13 +6570,13 @@ async function saveGraphToDisk(graph, outPath) {
5654
6570
  graph: graph.export()
5655
6571
  };
5656
6572
  const tmp = `${outPath}.tmp`;
5657
- await fs18.writeFile(tmp, JSON.stringify(payload), "utf8");
5658
- await fs18.rename(tmp, outPath);
6573
+ await fs19.writeFile(tmp, JSON.stringify(payload), "utf8");
6574
+ await fs19.rename(tmp, outPath);
5659
6575
  }
5660
6576
  async function loadGraphFromDisk(graph, outPath) {
5661
6577
  let raw;
5662
6578
  try {
5663
- raw = await fs18.readFile(outPath, "utf8");
6579
+ raw = await fs19.readFile(outPath, "utf8");
5664
6580
  } catch (err) {
5665
6581
  if (err.code === "ENOENT") return;
5666
6582
  throw err;
@@ -5724,7 +6640,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
5724
6640
  }
5725
6641
 
5726
6642
  // src/diff.ts
5727
- import { promises as fs19 } from "fs";
6643
+ import { promises as fs20 } from "fs";
5728
6644
  async function loadSnapshotForDiff(target) {
5729
6645
  if (/^https?:\/\//i.test(target)) {
5730
6646
  const res = await fetch(target);
@@ -5733,7 +6649,7 @@ async function loadSnapshotForDiff(target) {
5733
6649
  }
5734
6650
  return await res.json();
5735
6651
  }
5736
- const raw = await fs19.readFile(target, "utf8");
6652
+ const raw = await fs20.readFile(target, "utf8");
5737
6653
  return JSON.parse(raw);
5738
6654
  }
5739
6655
  function indexEntries(entries) {
@@ -5800,23 +6716,23 @@ function canonicalJson(value) {
5800
6716
  }
5801
6717
 
5802
6718
  // src/projects.ts
5803
- import path35 from "path";
6719
+ import path38 from "path";
5804
6720
  function pathsForProject(project, baseDir) {
5805
6721
  if (project === DEFAULT_PROJECT) {
5806
6722
  return {
5807
- snapshotPath: path35.join(baseDir, "graph.json"),
5808
- errorsPath: path35.join(baseDir, "errors.ndjson"),
5809
- staleEventsPath: path35.join(baseDir, "stale-events.ndjson"),
5810
- embeddingsCachePath: path35.join(baseDir, "embeddings.json"),
5811
- policyViolationsPath: path35.join(baseDir, "policy-violations.ndjson")
6723
+ snapshotPath: path38.join(baseDir, "graph.json"),
6724
+ errorsPath: path38.join(baseDir, "errors.ndjson"),
6725
+ staleEventsPath: path38.join(baseDir, "stale-events.ndjson"),
6726
+ embeddingsCachePath: path38.join(baseDir, "embeddings.json"),
6727
+ policyViolationsPath: path38.join(baseDir, "policy-violations.ndjson")
5812
6728
  };
5813
6729
  }
5814
6730
  return {
5815
- snapshotPath: path35.join(baseDir, `${project}.json`),
5816
- errorsPath: path35.join(baseDir, `errors.${project}.ndjson`),
5817
- staleEventsPath: path35.join(baseDir, `stale-events.${project}.ndjson`),
5818
- embeddingsCachePath: path35.join(baseDir, `embeddings.${project}.json`),
5819
- policyViolationsPath: path35.join(baseDir, `policy-violations.${project}.ndjson`)
6731
+ snapshotPath: path38.join(baseDir, `${project}.json`),
6732
+ errorsPath: path38.join(baseDir, `errors.${project}.ndjson`),
6733
+ staleEventsPath: path38.join(baseDir, `stale-events.${project}.ndjson`),
6734
+ embeddingsCachePath: path38.join(baseDir, `embeddings.${project}.json`),
6735
+ policyViolationsPath: path38.join(baseDir, `policy-violations.${project}.ndjson`)
5820
6736
  };
5821
6737
  }
5822
6738
  var Projects = class {
@@ -5855,9 +6771,9 @@ function parseExtraProjects(raw) {
5855
6771
  }
5856
6772
 
5857
6773
  // src/registry.ts
5858
- import { promises as fs20 } from "fs";
6774
+ import { promises as fs21 } from "fs";
5859
6775
  import os2 from "os";
5860
- import path36 from "path";
6776
+ import path39 from "path";
5861
6777
  import {
5862
6778
  RegistryFileSchema
5863
6779
  } from "@neat.is/types";
@@ -5865,20 +6781,20 @@ var LOCK_TIMEOUT_MS = 5e3;
5865
6781
  var LOCK_RETRY_MS = 50;
5866
6782
  function neatHome() {
5867
6783
  const override = process.env.NEAT_HOME;
5868
- if (override && override.length > 0) return path36.resolve(override);
5869
- return path36.join(os2.homedir(), ".neat");
6784
+ if (override && override.length > 0) return path39.resolve(override);
6785
+ return path39.join(os2.homedir(), ".neat");
5870
6786
  }
5871
6787
  function registryPath() {
5872
- return path36.join(neatHome(), "projects.json");
6788
+ return path39.join(neatHome(), "projects.json");
5873
6789
  }
5874
6790
  function registryLockPath() {
5875
- return path36.join(neatHome(), "projects.json.lock");
6791
+ return path39.join(neatHome(), "projects.json.lock");
5876
6792
  }
5877
6793
  function daemonPidPath() {
5878
- return path36.join(neatHome(), "neatd.pid");
6794
+ return path39.join(neatHome(), "neatd.pid");
5879
6795
  }
5880
6796
  function daemonsDir() {
5881
- return path36.join(neatHome(), "daemons");
6797
+ return path39.join(neatHome(), "daemons");
5882
6798
  }
5883
6799
  function isFiniteInt(v) {
5884
6800
  return typeof v === "number" && Number.isFinite(v);
@@ -5911,7 +6827,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
5911
6827
  const dir = daemonsDir();
5912
6828
  let names;
5913
6829
  try {
5914
- names = await fs20.readdir(dir);
6830
+ names = await fs21.readdir(dir);
5915
6831
  } catch (err) {
5916
6832
  if (err.code === "ENOENT") return [];
5917
6833
  throw err;
@@ -5919,10 +6835,10 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
5919
6835
  const out = [];
5920
6836
  for (const name of names) {
5921
6837
  if (!name.endsWith(".json")) continue;
5922
- const file = path36.join(dir, name);
6838
+ const file = path39.join(dir, name);
5923
6839
  let raw;
5924
6840
  try {
5925
- raw = await fs20.readFile(file, "utf8");
6841
+ raw = await fs21.readFile(file, "utf8");
5926
6842
  } catch {
5927
6843
  continue;
5928
6844
  }
@@ -5935,7 +6851,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
5935
6851
  return out;
5936
6852
  }
5937
6853
  async function removeDaemonRecord(source) {
5938
- await fs20.unlink(source).catch(() => {
6854
+ await fs21.unlink(source).catch(() => {
5939
6855
  });
5940
6856
  }
5941
6857
  async function listMachineProjects(probe = defaultDiscoveryProbe) {
@@ -5992,7 +6908,7 @@ function isPidAliveDefault(pid) {
5992
6908
  }
5993
6909
  async function readPidFile(file) {
5994
6910
  try {
5995
- const raw = await fs20.readFile(file, "utf8");
6911
+ const raw = await fs21.readFile(file, "utf8");
5996
6912
  const pid = Number.parseInt(raw.trim(), 10);
5997
6913
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5998
6914
  } catch {
@@ -6040,32 +6956,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
6040
6956
  }
6041
6957
  }
6042
6958
  async function normalizeProjectPath(input) {
6043
- const resolved = path36.resolve(input);
6959
+ const resolved = path39.resolve(input);
6044
6960
  try {
6045
- return await fs20.realpath(resolved);
6961
+ return await fs21.realpath(resolved);
6046
6962
  } catch {
6047
6963
  return resolved;
6048
6964
  }
6049
6965
  }
6050
6966
  async function writeAtomically(target, contents) {
6051
- await fs20.mkdir(path36.dirname(target), { recursive: true });
6967
+ await fs21.mkdir(path39.dirname(target), { recursive: true });
6052
6968
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
6053
- const fd = await fs20.open(tmp, "w");
6969
+ const fd = await fs21.open(tmp, "w");
6054
6970
  try {
6055
6971
  await fd.writeFile(contents, "utf8");
6056
6972
  await fd.sync();
6057
6973
  } finally {
6058
6974
  await fd.close();
6059
6975
  }
6060
- await fs20.rename(tmp, target);
6976
+ await fs21.rename(tmp, target);
6061
6977
  }
6062
6978
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6063
6979
  const deadline = Date.now() + timeoutMs;
6064
- await fs20.mkdir(path36.dirname(lockPath), { recursive: true });
6980
+ await fs21.mkdir(path39.dirname(lockPath), { recursive: true });
6065
6981
  let probedHolder = false;
6066
6982
  while (true) {
6067
6983
  try {
6068
- const fd = await fs20.open(lockPath, "wx");
6984
+ const fd = await fs21.open(lockPath, "wx");
6069
6985
  try {
6070
6986
  await fd.writeFile(`${process.pid}
6071
6987
  `, "utf8");
@@ -6090,7 +7006,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
6090
7006
  }
6091
7007
  }
6092
7008
  async function releaseLock(lockPath) {
6093
- await fs20.unlink(lockPath).catch(() => {
7009
+ await fs21.unlink(lockPath).catch(() => {
6094
7010
  });
6095
7011
  }
6096
7012
  async function withLock(fn) {
@@ -6106,7 +7022,7 @@ async function readRegistry() {
6106
7022
  const file = registryPath();
6107
7023
  let raw;
6108
7024
  try {
6109
- raw = await fs20.readFile(file, "utf8");
7025
+ raw = await fs21.readFile(file, "utf8");
6110
7026
  } catch (err) {
6111
7027
  if (err.code === "ENOENT") {
6112
7028
  return { version: 1, projects: [] };
@@ -6211,7 +7127,7 @@ function pruneTtlMs() {
6211
7127
  }
6212
7128
  async function statPathStatus(p) {
6213
7129
  try {
6214
- const stat = await fs20.stat(p);
7130
+ const stat = await fs21.stat(p);
6215
7131
  return stat.isDirectory() ? "present" : "unknown";
6216
7132
  } catch (err) {
6217
7133
  return err.code === "ENOENT" ? "gone" : "unknown";
@@ -6256,14 +7172,14 @@ import cors from "@fastify/cors";
6256
7172
  import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
6257
7173
 
6258
7174
  // src/extend/index.ts
6259
- import { promises as fs22 } from "fs";
6260
- import path38 from "path";
7175
+ import { promises as fs23 } from "fs";
7176
+ import path41 from "path";
6261
7177
  import os3 from "os";
6262
7178
  import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
6263
7179
 
6264
7180
  // src/installers/package-manager.ts
6265
- import { promises as fs21 } from "fs";
6266
- import path37 from "path";
7181
+ import { promises as fs22 } from "fs";
7182
+ import path40 from "path";
6267
7183
  import { spawn } from "child_process";
6268
7184
  var LOCKFILE_PRIORITY = [
6269
7185
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -6278,29 +7194,29 @@ var LOCKFILE_PRIORITY = [
6278
7194
  var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
6279
7195
  async function exists2(p) {
6280
7196
  try {
6281
- await fs21.access(p);
7197
+ await fs22.access(p);
6282
7198
  return true;
6283
7199
  } catch {
6284
7200
  return false;
6285
7201
  }
6286
7202
  }
6287
7203
  async function detectPackageManager(serviceDir) {
6288
- let dir = path37.resolve(serviceDir);
7204
+ let dir = path40.resolve(serviceDir);
6289
7205
  const stops = /* @__PURE__ */ new Set();
6290
7206
  for (let i = 0; i < 64; i++) {
6291
7207
  if (stops.has(dir)) break;
6292
7208
  stops.add(dir);
6293
7209
  for (const candidate of LOCKFILE_PRIORITY) {
6294
- const lockPath = path37.join(dir, candidate.lockfile);
7210
+ const lockPath = path40.join(dir, candidate.lockfile);
6295
7211
  if (await exists2(lockPath)) {
6296
7212
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
6297
7213
  }
6298
7214
  }
6299
- const parent = path37.dirname(dir);
7215
+ const parent = path40.dirname(dir);
6300
7216
  if (parent === dir) break;
6301
7217
  dir = parent;
6302
7218
  }
6303
- return { pm: "npm", cwd: path37.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7219
+ return { pm: "npm", cwd: path40.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
6304
7220
  }
6305
7221
  async function runPackageManagerInstall(cmd) {
6306
7222
  return new Promise((resolve) => {
@@ -6342,30 +7258,30 @@ ${err.message}`
6342
7258
  // src/extend/index.ts
6343
7259
  async function fileExists2(p) {
6344
7260
  try {
6345
- await fs22.access(p);
7261
+ await fs23.access(p);
6346
7262
  return true;
6347
7263
  } catch {
6348
7264
  return false;
6349
7265
  }
6350
7266
  }
6351
7267
  async function readPackageJson(scanPath) {
6352
- const pkgPath = path38.join(scanPath, "package.json");
6353
- const raw = await fs22.readFile(pkgPath, "utf8");
7268
+ const pkgPath = path41.join(scanPath, "package.json");
7269
+ const raw = await fs23.readFile(pkgPath, "utf8");
6354
7270
  return JSON.parse(raw);
6355
7271
  }
6356
7272
  async function findHookFiles(scanPath) {
6357
- const entries = await fs22.readdir(scanPath);
7273
+ const entries = await fs23.readdir(scanPath);
6358
7274
  return entries.filter(
6359
7275
  (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
6360
7276
  ).sort();
6361
7277
  }
6362
7278
  function extendLogPath() {
6363
- return process.env.NEAT_EXTEND_LOG ?? path38.join(os3.homedir(), ".neat", "extend-log.ndjson");
7279
+ return process.env.NEAT_EXTEND_LOG ?? path41.join(os3.homedir(), ".neat", "extend-log.ndjson");
6364
7280
  }
6365
7281
  async function appendExtendLog(entry) {
6366
7282
  const logPath = extendLogPath();
6367
- await fs22.mkdir(path38.dirname(logPath), { recursive: true });
6368
- await fs22.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
7283
+ await fs23.mkdir(path41.dirname(logPath), { recursive: true });
7284
+ await fs23.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
6369
7285
  }
6370
7286
  function splicedContent(fileContent, snippet2) {
6371
7287
  if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
@@ -6423,7 +7339,7 @@ function lookupInstrumentation(library, installedVersion) {
6423
7339
  }
6424
7340
  async function describeProjectInstrumentation(ctx) {
6425
7341
  const hookFiles = await findHookFiles(ctx.scanPath);
6426
- const envNeat = await fileExists2(path38.join(ctx.scanPath, ".env.neat"));
7342
+ const envNeat = await fileExists2(path41.join(ctx.scanPath, ".env.neat"));
6427
7343
  const registryInstrPackages = new Set(
6428
7344
  registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
6429
7345
  );
@@ -6445,31 +7361,31 @@ async function applyExtension(ctx, args, options) {
6445
7361
  );
6446
7362
  }
6447
7363
  for (const file of hookFiles) {
6448
- const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
7364
+ const content = await fs23.readFile(path41.join(ctx.scanPath, file), "utf8");
6449
7365
  if (content.includes(args.registration_snippet)) {
6450
7366
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
6451
7367
  }
6452
7368
  }
6453
7369
  const primaryFile = hookFiles[0];
6454
- const primaryPath = path38.join(ctx.scanPath, primaryFile);
7370
+ const primaryPath = path41.join(ctx.scanPath, primaryFile);
6455
7371
  const filesTouched = [];
6456
7372
  const depsAdded = [];
6457
- const pkgPath = path38.join(ctx.scanPath, "package.json");
7373
+ const pkgPath = path41.join(ctx.scanPath, "package.json");
6458
7374
  const pkg = await readPackageJson(ctx.scanPath);
6459
7375
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
6460
7376
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
6461
- await fs22.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7377
+ await fs23.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
6462
7378
  filesTouched.push("package.json");
6463
7379
  depsAdded.push(`${args.instrumentation_package}@${args.version}`);
6464
7380
  }
6465
- const hookContent = await fs22.readFile(primaryPath, "utf8");
7381
+ const hookContent = await fs23.readFile(primaryPath, "utf8");
6466
7382
  const patched = splicedContent(hookContent, args.registration_snippet);
6467
7383
  if (!patched) {
6468
7384
  throw new Error(
6469
7385
  `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
6470
7386
  );
6471
7387
  }
6472
- await fs22.writeFile(primaryPath, patched, "utf8");
7388
+ await fs23.writeFile(primaryPath, patched, "utf8");
6473
7389
  filesTouched.push(primaryFile);
6474
7390
  const cmd = await detectPackageManager(ctx.scanPath);
6475
7391
  const installer = options?.runInstall ?? runPackageManagerInstall;
@@ -6500,7 +7416,7 @@ async function dryRunExtension(ctx, args) {
6500
7416
  };
6501
7417
  }
6502
7418
  for (const file of hookFiles) {
6503
- const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
7419
+ const content = await fs23.readFile(path41.join(ctx.scanPath, file), "utf8");
6504
7420
  if (content.includes(args.registration_snippet)) {
6505
7421
  return {
6506
7422
  library: args.library,
@@ -6522,7 +7438,7 @@ async function dryRunExtension(ctx, args) {
6522
7438
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
6523
7439
  filesTouched.push("package.json");
6524
7440
  }
6525
- const hookContent = await fs22.readFile(path38.join(ctx.scanPath, primaryFile), "utf8");
7441
+ const hookContent = await fs23.readFile(path41.join(ctx.scanPath, primaryFile), "utf8");
6526
7442
  const patched = splicedContent(hookContent, args.registration_snippet);
6527
7443
  if (patched) {
6528
7444
  filesTouched.push(primaryFile);
@@ -6537,28 +7453,28 @@ async function rollbackExtension(ctx, args) {
6537
7453
  if (!await fileExists2(logPath)) {
6538
7454
  return { undone: false, message: "no apply found for library" };
6539
7455
  }
6540
- const raw = await fs22.readFile(logPath, "utf8");
7456
+ const raw = await fs23.readFile(logPath, "utf8");
6541
7457
  const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
6542
7458
  const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
6543
7459
  if (!match) {
6544
7460
  return { undone: false, message: "no apply found for library" };
6545
7461
  }
6546
- const pkgPath = path38.join(ctx.scanPath, "package.json");
7462
+ const pkgPath = path41.join(ctx.scanPath, "package.json");
6547
7463
  if (await fileExists2(pkgPath)) {
6548
7464
  const pkg = await readPackageJson(ctx.scanPath);
6549
7465
  if (pkg.dependencies?.[match.instrumentation_package]) {
6550
7466
  const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
6551
7467
  pkg.dependencies = rest;
6552
- await fs22.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
7468
+ await fs23.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
6553
7469
  }
6554
7470
  }
6555
7471
  const hookFiles = await findHookFiles(ctx.scanPath);
6556
7472
  for (const file of hookFiles) {
6557
- const filePath = path38.join(ctx.scanPath, file);
6558
- const content = await fs22.readFile(filePath, "utf8");
7473
+ const filePath = path41.join(ctx.scanPath, file);
7474
+ const content = await fs23.readFile(filePath, "utf8");
6559
7475
  if (content.includes(match.registration_snippet)) {
6560
7476
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
6561
- await fs22.writeFile(filePath, filtered, "utf8");
7477
+ await fs23.writeFile(filePath, filtered, "utf8");
6562
7478
  break;
6563
7479
  }
6564
7480
  }
@@ -7308,6 +8224,10 @@ export {
7308
8224
  loadPolicyFile,
7309
8225
  PolicyViolationsLog,
7310
8226
  thresholdForEdgeType,
8227
+ reconcileObservedRelPath,
8228
+ ensureObservedFileNode,
8229
+ ensureServiceNode,
8230
+ upsertObservedEdge,
7311
8231
  stitchTrace,
7312
8232
  makeErrorSpanWriter,
7313
8233
  handleSpan,
@@ -7362,4 +8282,4 @@ export {
7362
8282
  pruneRegistry,
7363
8283
  buildApi
7364
8284
  };
7365
- //# sourceMappingURL=chunk-O25KZNZK.js.map
8285
+ //# sourceMappingURL=chunk-QM6BMPVJ.js.map