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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountBearerAuth,
3
3
  readAuthEnv
4
- } from "./chunk-ZX7PCMGZ.js";
4
+ } from "./chunk-2LNICOVU.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, path41, edges) {
460
+ if (path41.length > best.path.length) {
461
+ best = { path: [...path41], edges: [...edges] };
462
462
  }
463
- if (path39.length - 1 >= maxDepth) return;
463
+ if (path41.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
+ path41.push(srcId);
469
469
  edges.push(edge);
470
- step(srcId, path39, edges);
471
- path39.pop();
470
+ step(srcId, path41, edges);
471
+ path41.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 path41 = [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
+ path41.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: path41, 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 path41 = [...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
+ path41.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: path41,
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: path41,
712
712
  edgeProvenances,
713
713
  confidence,
714
714
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -1377,10 +1377,13 @@ 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,
1383
1385
  inferredEdgeId,
1386
+ infraId,
1384
1387
  observedEdgeId,
1385
1388
  serviceId
1386
1389
  } from "@neat.is/types";
@@ -1719,10 +1722,48 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
1719
1722
  ]);
1720
1723
  var WIRE_SPAN_KIND_CLIENT = 3;
1721
1724
  var WIRE_SPAN_KIND_PRODUCER = 4;
1725
+ var WIRE_SPAN_KIND_CONSUMER = 5;
1722
1726
  function spanMintsObservedEdge(kind) {
1723
1727
  if (kind === void 0 || kind === 0) return true;
1724
1728
  return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
1725
1729
  }
1730
+ function spanMintsMessagingEdge(kind) {
1731
+ return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
1732
+ }
1733
+ function spanServesGraphqlOperation(kind) {
1734
+ return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
1735
+ }
1736
+ function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
1737
+ const id = graphqlOperationId(serviceName, operationType, operationName);
1738
+ if (graph.hasNode(id)) return id;
1739
+ const node = {
1740
+ id,
1741
+ type: NodeType3.GraphQLOperationNode,
1742
+ name: operationName,
1743
+ service: serviceName,
1744
+ operationType: operationType.toLowerCase(),
1745
+ operationName,
1746
+ discoveredVia: "otel"
1747
+ };
1748
+ graph.addNode(id, node);
1749
+ return id;
1750
+ }
1751
+ function messagingDestinationKind(system) {
1752
+ return `${system}-topic`;
1753
+ }
1754
+ function ensureMessagingDestinationNode(graph, system, destination) {
1755
+ const id = infraId(messagingDestinationKind(system), destination);
1756
+ if (graph.hasNode(id)) return id;
1757
+ const node = {
1758
+ id,
1759
+ type: NodeType3.InfraNode,
1760
+ name: destination,
1761
+ provider: "self",
1762
+ kind: messagingDestinationKind(system)
1763
+ };
1764
+ graph.addNode(id, node);
1765
+ return id;
1766
+ }
1726
1767
  var PARENT_SPAN_CACHE_SIZE = 1e4;
1727
1768
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
1728
1769
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -1816,6 +1857,21 @@ function ensureDatabaseNode(graph, host, engine) {
1816
1857
  graph.addNode(id, node);
1817
1858
  return id;
1818
1859
  }
1860
+ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
1861
+ const id = localDatabaseId(serviceName, name);
1862
+ if (graph.hasNode(id)) return id;
1863
+ const node = {
1864
+ id,
1865
+ type: NodeType3.DatabaseNode,
1866
+ name,
1867
+ engine,
1868
+ engineVersion: "unknown",
1869
+ compatibleDrivers: [],
1870
+ discoveredVia: "otel"
1871
+ };
1872
+ graph.addNode(id, node);
1873
+ return id;
1874
+ }
1819
1875
  function ensureFrontierNode(graph, host, ts) {
1820
1876
  const id = frontierIdFor(host);
1821
1877
  if (graph.hasNode(id)) {
@@ -2067,10 +2123,21 @@ async function handleSpan(ctx, span) {
2067
2123
  let affectedNode = sourceId;
2068
2124
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2069
2125
  if (span.dbSystem) {
2070
- const host = pickAddress(span);
2071
- if (mintsFromCallerSide && host) {
2072
- ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2073
- const targetId = databaseId(host);
2126
+ if (mintsFromCallerSide) {
2127
+ const host = pickAddress(span);
2128
+ let targetId;
2129
+ if (host) {
2130
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2131
+ targetId = databaseId(host);
2132
+ } else {
2133
+ const localName = span.dbName ?? span.dbSystem;
2134
+ targetId = ensureLocalDatabaseNode(
2135
+ ctx.graph,
2136
+ span.service,
2137
+ localName,
2138
+ span.dbSystem
2139
+ );
2140
+ }
2074
2141
  const result = upsertObservedEdge(
2075
2142
  ctx.graph,
2076
2143
  EdgeType3.CONNECTS_TO,
@@ -2082,6 +2149,40 @@ async function handleSpan(ctx, span) {
2082
2149
  );
2083
2150
  if (result) affectedNode = targetId;
2084
2151
  }
2152
+ } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
2153
+ const targetId = ensureMessagingDestinationNode(
2154
+ ctx.graph,
2155
+ span.messagingSystem,
2156
+ span.messagingDestination
2157
+ );
2158
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? EdgeType3.CONSUMES_FROM : EdgeType3.PUBLISHES_TO;
2159
+ const result = upsertObservedEdge(
2160
+ ctx.graph,
2161
+ edgeType,
2162
+ observedSource(),
2163
+ targetId,
2164
+ ts,
2165
+ isError,
2166
+ callSiteEvidence
2167
+ );
2168
+ if (result) affectedNode = targetId;
2169
+ } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
2170
+ const targetId = ensureGraphqlOperationNode(
2171
+ ctx.graph,
2172
+ span.service,
2173
+ span.graphqlOperationType,
2174
+ span.graphqlOperationName
2175
+ );
2176
+ const result = upsertObservedEdge(
2177
+ ctx.graph,
2178
+ EdgeType3.CONTAINS,
2179
+ observedSource(),
2180
+ targetId,
2181
+ ts,
2182
+ isError,
2183
+ callSiteEvidence
2184
+ );
2185
+ if (result) affectedNode = targetId;
2085
2186
  } else {
2086
2187
  const host = pickAddress(span);
2087
2188
  let resolvedViaAddress = false;
@@ -2191,29 +2292,29 @@ function promoteFrontierNodes(graph, opts = {}) {
2191
2292
  toPromote.push({ frontierId: id, serviceId: target });
2192
2293
  });
2193
2294
  let promoted = 0;
2194
- for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
2295
+ for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
2195
2296
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
2196
2297
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
2197
2298
  if (!gate.allowed) {
2198
2299
  continue;
2199
2300
  }
2200
2301
  }
2201
- rewireFrontierEdges(graph, frontierId2, serviceId3);
2302
+ rewireFrontierEdges(graph, frontierId2, serviceId4);
2202
2303
  graph.dropNode(frontierId2);
2203
2304
  promoted++;
2204
2305
  }
2205
2306
  return promoted;
2206
2307
  }
2207
- function rewireFrontierEdges(graph, frontierId2, serviceId3) {
2308
+ function rewireFrontierEdges(graph, frontierId2, serviceId4) {
2208
2309
  const inbound = [...graph.inboundEdges(frontierId2)];
2209
2310
  const outbound = [...graph.outboundEdges(frontierId2)];
2210
2311
  for (const edgeId of inbound) {
2211
2312
  const edge = graph.getEdgeAttributes(edgeId);
2212
- rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
2313
+ rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
2213
2314
  }
2214
2315
  for (const edgeId of outbound) {
2215
2316
  const edge = graph.getEdgeAttributes(edgeId);
2216
- rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
2317
+ rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
2217
2318
  }
2218
2319
  }
2219
2320
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -2981,9 +3082,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
2981
3082
  "StatefulSet",
2982
3083
  "DaemonSet"
2983
3084
  ]);
2984
- function addAliases(graph, serviceId3, candidates) {
2985
- if (!graph.hasNode(serviceId3)) return;
2986
- const node = graph.getNodeAttributes(serviceId3);
3085
+ function addAliases(graph, serviceId4, candidates) {
3086
+ if (!graph.hasNode(serviceId4)) return;
3087
+ const node = graph.getNodeAttributes(serviceId4);
2987
3088
  if (node.type !== NodeType5.ServiceNode) return;
2988
3089
  const set = new Set(node.aliases ?? []);
2989
3090
  for (const c of candidates) {
@@ -2993,7 +3094,7 @@ function addAliases(graph, serviceId3, candidates) {
2993
3094
  }
2994
3095
  if (set.size === 0) return;
2995
3096
  const updated = { ...node, aliases: [...set].sort() };
2996
- graph.replaceNodeAttributes(serviceId3, updated);
3097
+ graph.replaceNodeAttributes(serviceId4, updated);
2997
3098
  }
2998
3099
  function indexServicesByName(services) {
2999
3100
  const map = /* @__PURE__ */ new Map();
@@ -3026,12 +3127,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3026
3127
  }
3027
3128
  if (!compose?.services) return;
3028
3129
  for (const [composeName, svc] of Object.entries(compose.services)) {
3029
- const serviceId3 = serviceIndex.get(composeName);
3030
- if (!serviceId3) continue;
3130
+ const serviceId4 = serviceIndex.get(composeName);
3131
+ if (!serviceId4) continue;
3031
3132
  const aliases = /* @__PURE__ */ new Set([composeName]);
3032
3133
  if (svc.container_name) aliases.add(svc.container_name);
3033
3134
  if (svc.hostname) aliases.add(svc.hostname);
3034
- addAliases(graph, serviceId3, aliases);
3135
+ addAliases(graph, serviceId4, aliases);
3035
3136
  }
3036
3137
  }
3037
3138
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -3147,16 +3248,28 @@ import {
3147
3248
  extractedEdgeId as extractedEdgeId3,
3148
3249
  fileId as fileId2
3149
3250
  } from "@neat.is/types";
3251
+ function buildServiceHostIndex(services) {
3252
+ const knownHosts = /* @__PURE__ */ new Set();
3253
+ const hostToNodeId = /* @__PURE__ */ new Map();
3254
+ for (const service of services) {
3255
+ const base = path10.basename(service.dir);
3256
+ knownHosts.add(base);
3257
+ knownHosts.add(service.pkg.name);
3258
+ hostToNodeId.set(base, service.node.id);
3259
+ hostToNodeId.set(service.pkg.name, service.node.id);
3260
+ }
3261
+ return { knownHosts, hostToNodeId };
3262
+ }
3150
3263
  async function walkSourceFiles(dir) {
3151
3264
  const out = [];
3152
- async function walk(current) {
3265
+ async function walk3(current) {
3153
3266
  const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
3154
3267
  for (const entry of entries) {
3155
3268
  const full = path10.join(current, entry.name);
3156
3269
  if (entry.isDirectory()) {
3157
3270
  if (IGNORED_DIRS.has(entry.name)) continue;
3158
3271
  if (await isPythonVenvDir(full)) continue;
3159
- await walk(full);
3272
+ await walk3(full);
3160
3273
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3161
3274
  // would attribute our instrumentation imports to the user's service.
3162
3275
  !isNeatAuthoredSourceFile(entry.name)) {
@@ -3164,7 +3277,7 @@ async function walkSourceFiles(dir) {
3164
3277
  }
3165
3278
  }
3166
3279
  }
3167
- await walk(dir);
3280
+ await walk3(dir);
3168
3281
  return out;
3169
3282
  }
3170
3283
  async function loadSourceFiles(dir) {
@@ -4264,20 +4377,20 @@ import {
4264
4377
  } from "@neat.is/types";
4265
4378
  async function walkConfigFiles(dir) {
4266
4379
  const out = [];
4267
- async function walk(current) {
4380
+ async function walk3(current) {
4268
4381
  const entries = await fs14.readdir(current, { withFileTypes: true });
4269
4382
  for (const entry of entries) {
4270
4383
  const full = path21.join(current, entry.name);
4271
4384
  if (entry.isDirectory()) {
4272
4385
  if (IGNORED_DIRS.has(entry.name)) continue;
4273
4386
  if (await isPythonVenvDir(full)) continue;
4274
- await walk(full);
4387
+ await walk3(full);
4275
4388
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
4276
4389
  out.push(full);
4277
4390
  }
4278
4391
  }
4279
4392
  }
4280
- await walk(dir);
4393
+ await walk3(dir);
4281
4394
  return out;
4282
4395
  }
4283
4396
  async function addConfigNodes(graph, services, scanPath) {
@@ -4325,24 +4438,360 @@ async function addConfigNodes(graph, services, scanPath) {
4325
4438
  return { nodesAdded, edgesAdded };
4326
4439
  }
4327
4440
 
4328
- // src/extract/calls/index.ts
4329
- import {
4330
- EdgeType as EdgeType9,
4331
- NodeType as NodeType9,
4332
- Provenance as Provenance8,
4333
- confidenceForExtracted as confidenceForExtracted6,
4334
- passesExtractedFloor as passesExtractedFloor2
4335
- } from "@neat.is/types";
4336
-
4337
- // src/extract/calls/http.ts
4441
+ // src/extract/routes.ts
4338
4442
  import path22 from "path";
4339
4443
  import Parser2 from "tree-sitter";
4340
4444
  import JavaScript2 from "tree-sitter-javascript";
4341
- import Python2 from "tree-sitter-python";
4342
4445
  import {
4343
4446
  EdgeType as EdgeType8,
4447
+ NodeType as NodeType9,
4344
4448
  Provenance as Provenance7,
4345
4449
  confidenceForExtracted as confidenceForExtracted5,
4450
+ extractedEdgeId as extractedEdgeId5,
4451
+ routeId
4452
+ } from "@neat.is/types";
4453
+ var PARSE_CHUNK2 = 16384;
4454
+ function parseSource2(parser, source) {
4455
+ return parser.parse(
4456
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
4457
+ );
4458
+ }
4459
+ function makeJsParser2() {
4460
+ const p = new Parser2();
4461
+ p.setLanguage(JavaScript2);
4462
+ return p;
4463
+ }
4464
+ var ROUTER_METHODS = /* @__PURE__ */ new Set([
4465
+ "get",
4466
+ "post",
4467
+ "put",
4468
+ "patch",
4469
+ "delete",
4470
+ "options",
4471
+ "head",
4472
+ "all"
4473
+ ]);
4474
+ var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
4475
+ var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
4476
+ function canonicalizeTemplate(raw) {
4477
+ let p = raw.split("?")[0].split("#")[0];
4478
+ if (!p.startsWith("/")) p = "/" + p;
4479
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
4480
+ return p;
4481
+ }
4482
+ function isDynamicSegment(seg) {
4483
+ if (seg.length === 0) return false;
4484
+ if (seg.includes(":")) return true;
4485
+ if (seg.startsWith("{") || seg.startsWith("[")) return true;
4486
+ if (/^\d+$/.test(seg)) return true;
4487
+ 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;
4488
+ if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
4489
+ return false;
4490
+ }
4491
+ function normalizePathTemplate(raw) {
4492
+ const canonical = canonicalizeTemplate(raw);
4493
+ const segments = canonical.split("/").filter((s) => s.length > 0);
4494
+ const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
4495
+ return "/" + normalised.join("/");
4496
+ }
4497
+ function walk(node, visit) {
4498
+ visit(node);
4499
+ for (let i = 0; i < node.namedChildCount; i++) {
4500
+ const child = node.namedChild(i);
4501
+ if (child) walk(child, visit);
4502
+ }
4503
+ }
4504
+ function staticStringText(node) {
4505
+ if (node.type === "string") {
4506
+ for (let i = 0; i < node.namedChildCount; i++) {
4507
+ const child = node.namedChild(i);
4508
+ if (child?.type === "string_fragment") return child.text;
4509
+ }
4510
+ return "";
4511
+ }
4512
+ if (node.type === "template_string") {
4513
+ for (let i = 0; i < node.namedChildCount; i++) {
4514
+ if (node.namedChild(i)?.type === "template_substitution") return null;
4515
+ }
4516
+ const raw = node.text;
4517
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
4518
+ }
4519
+ return null;
4520
+ }
4521
+ function objectStringProp(objNode, key) {
4522
+ for (let i = 0; i < objNode.namedChildCount; i++) {
4523
+ const pair = objNode.namedChild(i);
4524
+ if (!pair || pair.type !== "pair") continue;
4525
+ const k = pair.childForFieldName("key");
4526
+ if (!k) continue;
4527
+ const kText = k.type === "string" ? staticStringText(k) : k.text;
4528
+ if (kText !== key) continue;
4529
+ const v = pair.childForFieldName("value");
4530
+ if (v) return staticStringText(v);
4531
+ }
4532
+ return null;
4533
+ }
4534
+ function fastifyRouteMethods(objNode) {
4535
+ for (let i = 0; i < objNode.namedChildCount; i++) {
4536
+ const pair = objNode.namedChild(i);
4537
+ if (!pair || pair.type !== "pair") continue;
4538
+ const k = pair.childForFieldName("key");
4539
+ const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
4540
+ if (kText !== "method") continue;
4541
+ const v = pair.childForFieldName("value");
4542
+ if (!v) return [];
4543
+ if (v.type === "string" || v.type === "template_string") {
4544
+ const s = staticStringText(v);
4545
+ return s ? [s.toUpperCase()] : [];
4546
+ }
4547
+ if (v.type === "array") {
4548
+ const out = [];
4549
+ for (let j = 0; j < v.namedChildCount; j++) {
4550
+ const el = v.namedChild(j);
4551
+ if (el && (el.type === "string" || el.type === "template_string")) {
4552
+ const s = staticStringText(el);
4553
+ if (s) out.push(s.toUpperCase());
4554
+ }
4555
+ }
4556
+ return out;
4557
+ }
4558
+ }
4559
+ return [];
4560
+ }
4561
+ function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
4562
+ const tree = parseSource2(parser, source);
4563
+ const out = [];
4564
+ const framework = hasExpress ? "express" : "fastify";
4565
+ walk(tree.rootNode, (node) => {
4566
+ if (node.type !== "call_expression") return;
4567
+ const fn = node.childForFieldName("function");
4568
+ if (!fn || fn.type !== "member_expression") return;
4569
+ const prop = fn.childForFieldName("property");
4570
+ if (!prop) return;
4571
+ const method = prop.text.toLowerCase();
4572
+ const args = node.childForFieldName("arguments");
4573
+ const first = args?.namedChild(0);
4574
+ if (!first) return;
4575
+ const line = node.startPosition.row + 1;
4576
+ if (ROUTER_METHODS.has(method)) {
4577
+ const p = staticStringText(first);
4578
+ if (p && p.startsWith("/")) {
4579
+ out.push({
4580
+ method: method === "all" ? "ALL" : method.toUpperCase(),
4581
+ pathTemplate: canonicalizeTemplate(p),
4582
+ line,
4583
+ framework
4584
+ });
4585
+ }
4586
+ return;
4587
+ }
4588
+ if (method === "route" && hasFastify && first.type === "object") {
4589
+ const url = objectStringProp(first, "url");
4590
+ if (!url || !url.startsWith("/")) return;
4591
+ const methods = fastifyRouteMethods(first);
4592
+ const list = methods.length > 0 ? methods : ["ALL"];
4593
+ for (const m of list) {
4594
+ out.push({
4595
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
4596
+ pathTemplate: canonicalizeTemplate(url),
4597
+ line,
4598
+ framework: "fastify"
4599
+ });
4600
+ }
4601
+ }
4602
+ });
4603
+ return out;
4604
+ }
4605
+ function segmentsOf(relFile) {
4606
+ return toPosix2(relFile).split("/").filter((s) => s.length > 0);
4607
+ }
4608
+ function isNextAppRouteFile(relFile) {
4609
+ const segs = segmentsOf(relFile);
4610
+ if (!segs.includes("app")) return false;
4611
+ const base = segs[segs.length - 1] ?? "";
4612
+ return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
4613
+ }
4614
+ function isNextPagesApiFile(relFile) {
4615
+ const segs = segmentsOf(relFile);
4616
+ const pagesIdx = segs.indexOf("pages");
4617
+ if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
4618
+ const base = segs[segs.length - 1] ?? "";
4619
+ if (/^_(app|document|middleware)\./.test(base)) return false;
4620
+ return JS_ROUTE_EXTENSIONS.has(path22.extname(base));
4621
+ }
4622
+ function nextSegment(seg) {
4623
+ if (seg.startsWith("(") && seg.endsWith(")")) return null;
4624
+ const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
4625
+ if (catchAll) return ":" + catchAll[1];
4626
+ const dynamic = seg.match(/^\[(.+?)\]$/);
4627
+ if (dynamic) return ":" + dynamic[1];
4628
+ return seg;
4629
+ }
4630
+ function nextAppPathTemplate(relFile) {
4631
+ const segs = segmentsOf(relFile);
4632
+ const appIdx = segs.lastIndexOf("app");
4633
+ const between = segs.slice(appIdx + 1, segs.length - 1);
4634
+ const parts = [];
4635
+ for (const seg of between) {
4636
+ const mapped = nextSegment(seg);
4637
+ if (mapped !== null) parts.push(mapped);
4638
+ }
4639
+ return "/" + parts.join("/");
4640
+ }
4641
+ function nextPagesApiPathTemplate(relFile) {
4642
+ const segs = segmentsOf(relFile);
4643
+ const pagesIdx = segs.indexOf("pages");
4644
+ const rest = segs.slice(pagesIdx + 1);
4645
+ const parts = [];
4646
+ for (let i = 0; i < rest.length; i++) {
4647
+ let seg = rest[i];
4648
+ if (i === rest.length - 1) {
4649
+ seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
4650
+ if (seg === "index") continue;
4651
+ }
4652
+ const mapped = nextSegment(seg);
4653
+ if (mapped !== null) parts.push(mapped);
4654
+ }
4655
+ return "/" + parts.join("/");
4656
+ }
4657
+ function nextAppMethods(root) {
4658
+ const out = [];
4659
+ walk(root, (node) => {
4660
+ if (node.type !== "export_statement") return;
4661
+ const decl = node.childForFieldName("declaration");
4662
+ if (!decl) return;
4663
+ const line = node.startPosition.row + 1;
4664
+ if (decl.type === "function_declaration") {
4665
+ const name = decl.childForFieldName("name")?.text;
4666
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
4667
+ return;
4668
+ }
4669
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
4670
+ for (let i = 0; i < decl.namedChildCount; i++) {
4671
+ const d = decl.namedChild(i);
4672
+ if (d?.type !== "variable_declarator") continue;
4673
+ const name = d.childForFieldName("name")?.text;
4674
+ if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
4675
+ }
4676
+ }
4677
+ });
4678
+ return out;
4679
+ }
4680
+ function nextRoutesFromFile(source, relFile, parser) {
4681
+ if (isNextAppRouteFile(relFile)) {
4682
+ const tree = parseSource2(parser, source);
4683
+ const template = nextAppPathTemplate(relFile);
4684
+ return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
4685
+ method,
4686
+ pathTemplate: canonicalizeTemplate(template),
4687
+ line,
4688
+ framework: "next"
4689
+ }));
4690
+ }
4691
+ if (isNextPagesApiFile(relFile)) {
4692
+ return [
4693
+ {
4694
+ method: "ALL",
4695
+ pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
4696
+ line: 1,
4697
+ framework: "next"
4698
+ }
4699
+ ];
4700
+ }
4701
+ return [];
4702
+ }
4703
+ async function addRoutes(graph, services) {
4704
+ const jsParser = makeJsParser2();
4705
+ let nodesAdded = 0;
4706
+ let edgesAdded = 0;
4707
+ for (const service of services) {
4708
+ const deps = {
4709
+ ...service.pkg.dependencies ?? {},
4710
+ ...service.pkg.devDependencies ?? {}
4711
+ };
4712
+ const hasExpress = deps["express"] !== void 0;
4713
+ const hasFastify = deps["fastify"] !== void 0;
4714
+ const hasNext = deps["next"] !== void 0;
4715
+ if (!hasExpress && !hasFastify && !hasNext) continue;
4716
+ const files = await loadSourceFiles(service.dir);
4717
+ for (const file of files) {
4718
+ if (isTestPath(file.path)) continue;
4719
+ if (!JS_ROUTE_EXTENSIONS.has(path22.extname(file.path))) continue;
4720
+ const relFile = toPosix2(path22.relative(service.dir, file.path));
4721
+ let routes;
4722
+ try {
4723
+ if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
4724
+ routes = nextRoutesFromFile(file.content, relFile, jsParser);
4725
+ } else if (hasExpress || hasFastify) {
4726
+ routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
4727
+ } else {
4728
+ routes = [];
4729
+ }
4730
+ } catch (err) {
4731
+ recordExtractionError("route extraction", file.path, err);
4732
+ continue;
4733
+ }
4734
+ if (routes.length === 0) continue;
4735
+ for (const route of routes) {
4736
+ const rid = routeId(service.pkg.name, route.method, route.pathTemplate);
4737
+ if (!graph.hasNode(rid)) {
4738
+ const node = {
4739
+ id: rid,
4740
+ type: NodeType9.RouteNode,
4741
+ name: `${route.method} ${route.pathTemplate}`,
4742
+ service: service.pkg.name,
4743
+ method: route.method,
4744
+ pathTemplate: route.pathTemplate,
4745
+ path: relFile,
4746
+ line: route.line,
4747
+ framework: route.framework,
4748
+ discoveredVia: "static"
4749
+ };
4750
+ graph.addNode(rid, node);
4751
+ nodesAdded++;
4752
+ }
4753
+ const containsId = extractedEdgeId5(service.node.id, rid, EdgeType8.CONTAINS);
4754
+ if (!graph.hasEdge(containsId)) {
4755
+ const edge = {
4756
+ id: containsId,
4757
+ source: service.node.id,
4758
+ target: rid,
4759
+ type: EdgeType8.CONTAINS,
4760
+ provenance: Provenance7.EXTRACTED,
4761
+ confidence: confidenceForExtracted5("structural"),
4762
+ evidence: {
4763
+ file: relFile,
4764
+ line: route.line,
4765
+ snippet: snippet(file.content, route.line)
4766
+ }
4767
+ };
4768
+ graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
4769
+ edgesAdded++;
4770
+ }
4771
+ }
4772
+ }
4773
+ }
4774
+ return { nodesAdded, edgesAdded };
4775
+ }
4776
+
4777
+ // src/extract/calls/index.ts
4778
+ import {
4779
+ EdgeType as EdgeType11,
4780
+ NodeType as NodeType11,
4781
+ Provenance as Provenance10,
4782
+ confidenceForExtracted as confidenceForExtracted8,
4783
+ passesExtractedFloor as passesExtractedFloor3
4784
+ } from "@neat.is/types";
4785
+
4786
+ // src/extract/calls/http.ts
4787
+ import path23 from "path";
4788
+ import Parser3 from "tree-sitter";
4789
+ import JavaScript3 from "tree-sitter-javascript";
4790
+ import Python2 from "tree-sitter-python";
4791
+ import {
4792
+ EdgeType as EdgeType9,
4793
+ Provenance as Provenance8,
4794
+ confidenceForExtracted as confidenceForExtracted6,
4346
4795
  passesExtractedFloor
4347
4796
  } from "@neat.is/types";
4348
4797
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -4372,14 +4821,14 @@ function collectStringLiterals(node, out) {
4372
4821
  if (child) collectStringLiterals(child, out);
4373
4822
  }
4374
4823
  }
4375
- var PARSE_CHUNK2 = 16384;
4376
- function parseSource2(parser, source) {
4824
+ var PARSE_CHUNK3 = 16384;
4825
+ function parseSource3(parser, source) {
4377
4826
  return parser.parse(
4378
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
4827
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
4379
4828
  );
4380
4829
  }
4381
4830
  function callsFromSource(source, parser, knownHosts) {
4382
- const tree = parseSource2(parser, source);
4831
+ const tree = parseSource3(parser, source);
4383
4832
  const literals = [];
4384
4833
  collectStringLiterals(tree.rootNode, literals);
4385
4834
  const out = [];
@@ -4393,27 +4842,20 @@ function callsFromSource(source, parser, knownHosts) {
4393
4842
  }
4394
4843
  return out;
4395
4844
  }
4396
- function makeJsParser2() {
4397
- const p = new Parser2();
4398
- p.setLanguage(JavaScript2);
4845
+ function makeJsParser3() {
4846
+ const p = new Parser3();
4847
+ p.setLanguage(JavaScript3);
4399
4848
  return p;
4400
4849
  }
4401
4850
  function makePyParser2() {
4402
- const p = new Parser2();
4851
+ const p = new Parser3();
4403
4852
  p.setLanguage(Python2);
4404
4853
  return p;
4405
4854
  }
4406
4855
  async function addHttpCallEdges(graph, services) {
4407
- const jsParser = makeJsParser2();
4856
+ const jsParser = makeJsParser3();
4408
4857
  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
- }
4858
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
4417
4859
  let nodesAdded = 0;
4418
4860
  let edgesAdded = 0;
4419
4861
  for (const service of services) {
@@ -4421,7 +4863,7 @@ async function addHttpCallEdges(graph, services) {
4421
4863
  const seen = /* @__PURE__ */ new Set();
4422
4864
  for (const file of files) {
4423
4865
  if (isTestPath(file.path)) continue;
4424
- const parser = path22.extname(file.path) === ".py" ? pyParser : jsParser;
4866
+ const parser = path23.extname(file.path) === ".py" ? pyParser : jsParser;
4425
4867
  let sites;
4426
4868
  try {
4427
4869
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -4430,14 +4872,14 @@ async function addHttpCallEdges(graph, services) {
4430
4872
  continue;
4431
4873
  }
4432
4874
  if (sites.length === 0) continue;
4433
- const relFile = toPosix2(path22.relative(service.dir, file.path));
4875
+ const relFile = toPosix2(path23.relative(service.dir, file.path));
4434
4876
  for (const site of sites) {
4435
4877
  const targetId = hostToNodeId.get(site.host);
4436
4878
  if (!targetId || targetId === service.node.id) continue;
4437
4879
  const dedupKey = `${relFile}|${targetId}`;
4438
4880
  if (seen.has(dedupKey)) continue;
4439
4881
  seen.add(dedupKey);
4440
- const confidence = confidenceForExtracted5("url-literal-service-target");
4882
+ const confidence = confidenceForExtracted6("url-literal-service-target");
4441
4883
  const ev = {
4442
4884
  file: relFile,
4443
4885
  line: site.line,
@@ -4455,21 +4897,21 @@ async function addHttpCallEdges(graph, services) {
4455
4897
  noteExtractedDropped({
4456
4898
  source: fileNodeId,
4457
4899
  target: targetId,
4458
- type: EdgeType8.CALLS,
4900
+ type: EdgeType9.CALLS,
4459
4901
  confidence,
4460
4902
  confidenceKind: "url-literal-service-target",
4461
4903
  evidence: ev
4462
4904
  });
4463
4905
  continue;
4464
4906
  }
4465
- const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType8.CALLS);
4907
+ const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType9.CALLS);
4466
4908
  if (!graph.hasEdge(edgeId)) {
4467
4909
  const edge = {
4468
4910
  id: edgeId,
4469
4911
  source: fileNodeId,
4470
4912
  target: targetId,
4471
- type: EdgeType8.CALLS,
4472
- provenance: Provenance7.EXTRACTED,
4913
+ type: EdgeType9.CALLS,
4914
+ provenance: Provenance8.EXTRACTED,
4473
4915
  confidence,
4474
4916
  evidence: ev
4475
4917
  };
@@ -4482,9 +4924,284 @@ async function addHttpCallEdges(graph, services) {
4482
4924
  return { nodesAdded, edgesAdded };
4483
4925
  }
4484
4926
 
4927
+ // src/extract/calls/route-match.ts
4928
+ import path24 from "path";
4929
+ import Parser4 from "tree-sitter";
4930
+ import JavaScript4 from "tree-sitter-javascript";
4931
+ import {
4932
+ EdgeType as EdgeType10,
4933
+ NodeType as NodeType10,
4934
+ Provenance as Provenance9,
4935
+ confidenceForExtracted as confidenceForExtracted7,
4936
+ passesExtractedFloor as passesExtractedFloor2,
4937
+ serviceId as serviceId3
4938
+ } from "@neat.is/types";
4939
+ var PARSE_CHUNK4 = 16384;
4940
+ function parseSource4(parser, source) {
4941
+ return parser.parse(
4942
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
4943
+ );
4944
+ }
4945
+ function makeJsParser4() {
4946
+ const p = new Parser4();
4947
+ p.setLanguage(JavaScript4);
4948
+ return p;
4949
+ }
4950
+ var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
4951
+ var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
4952
+ function walk2(node, visit) {
4953
+ visit(node);
4954
+ for (let i = 0; i < node.namedChildCount; i++) {
4955
+ const child = node.namedChild(i);
4956
+ if (child) walk2(child, visit);
4957
+ }
4958
+ }
4959
+ function reconstructUrl(node) {
4960
+ if (node.type === "string") {
4961
+ for (let i = 0; i < node.namedChildCount; i++) {
4962
+ const child = node.namedChild(i);
4963
+ if (child?.type === "string_fragment") return child.text;
4964
+ }
4965
+ return "";
4966
+ }
4967
+ if (node.type === "template_string") {
4968
+ let out = "";
4969
+ for (let i = 0; i < node.namedChildCount; i++) {
4970
+ const child = node.namedChild(i);
4971
+ if (!child) continue;
4972
+ if (child.type === "string_fragment") out += child.text;
4973
+ else if (child.type === "template_substitution") out += ":param";
4974
+ }
4975
+ if (out.length === 0) {
4976
+ const raw = node.text;
4977
+ return raw.length >= 2 ? raw.slice(1, -1) : "";
4978
+ }
4979
+ return out;
4980
+ }
4981
+ return null;
4982
+ }
4983
+ function methodFromOptions(objNode) {
4984
+ for (let i = 0; i < objNode.namedChildCount; i++) {
4985
+ const pair = objNode.namedChild(i);
4986
+ if (!pair || pair.type !== "pair") continue;
4987
+ const k = pair.childForFieldName("key");
4988
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
4989
+ if (kText !== "method") continue;
4990
+ const v = pair.childForFieldName("value");
4991
+ if (v && (v.type === "string" || v.type === "template_string")) {
4992
+ const s = stringText(v);
4993
+ return s ? s.toUpperCase() : void 0;
4994
+ }
4995
+ }
4996
+ return void 0;
4997
+ }
4998
+ function stringText(node) {
4999
+ if (node.type === "string") {
5000
+ for (let i = 0; i < node.namedChildCount; i++) {
5001
+ const child = node.namedChild(i);
5002
+ if (child?.type === "string_fragment") return child.text;
5003
+ }
5004
+ return "";
5005
+ }
5006
+ return null;
5007
+ }
5008
+ function urlNodeFromConfig(objNode) {
5009
+ for (let i = 0; i < objNode.namedChildCount; i++) {
5010
+ const pair = objNode.namedChild(i);
5011
+ if (!pair || pair.type !== "pair") continue;
5012
+ const k = pair.childForFieldName("key");
5013
+ const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
5014
+ if (kText === "url") return pair.childForFieldName("value");
5015
+ }
5016
+ return null;
5017
+ }
5018
+ function pathOf(urlStr) {
5019
+ try {
5020
+ const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
5021
+ const parsed = new URL(candidate);
5022
+ return parsed.pathname || "/";
5023
+ } catch {
5024
+ return null;
5025
+ }
5026
+ }
5027
+ function matchHost(urlStr, knownHosts) {
5028
+ for (const host of knownHosts) {
5029
+ if (urlMatchesHost(urlStr, host)) return host;
5030
+ }
5031
+ return null;
5032
+ }
5033
+ function clientCallSitesFromSource(source, parser, knownHosts) {
5034
+ const tree = parseSource4(parser, source);
5035
+ const out = [];
5036
+ const push = (urlNode, method, callNode) => {
5037
+ const urlStr = reconstructUrl(urlNode);
5038
+ if (!urlStr) return;
5039
+ const host = matchHost(urlStr, knownHosts);
5040
+ if (!host) return;
5041
+ const p = pathOf(urlStr);
5042
+ if (p === null) return;
5043
+ const line = callNode.startPosition.row + 1;
5044
+ out.push({
5045
+ host,
5046
+ method,
5047
+ pathTemplate: p,
5048
+ line,
5049
+ snippet: snippet(source, line)
5050
+ });
5051
+ };
5052
+ walk2(tree.rootNode, (node) => {
5053
+ if (node.type !== "call_expression") return;
5054
+ const fn = node.childForFieldName("function");
5055
+ if (!fn) return;
5056
+ const args = node.childForFieldName("arguments");
5057
+ const first = args?.namedChild(0);
5058
+ if (!first) return;
5059
+ const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
5060
+ if (fn.type === "identifier" && fnName === "fetch") {
5061
+ const opts = args?.namedChild(1);
5062
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5063
+ push(first, method, node);
5064
+ return;
5065
+ }
5066
+ if (fn.type === "identifier" && fnName === "axios") {
5067
+ if (first.type === "object") {
5068
+ const urlNode = urlNodeFromConfig(first);
5069
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5070
+ } else {
5071
+ const opts = args?.namedChild(1);
5072
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
5073
+ push(first, method, node);
5074
+ }
5075
+ return;
5076
+ }
5077
+ if (fn.type === "member_expression") {
5078
+ const obj = fn.childForFieldName("object");
5079
+ const objName = obj?.text ?? "";
5080
+ if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
5081
+ if (fnName === "request" && first.type === "object") {
5082
+ const urlNode = urlNodeFromConfig(first);
5083
+ if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
5084
+ } else {
5085
+ push(first, fnName.toUpperCase(), node);
5086
+ }
5087
+ return;
5088
+ }
5089
+ if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
5090
+ const opts = args?.namedChild(1);
5091
+ const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
5092
+ push(first, method, node);
5093
+ return;
5094
+ }
5095
+ }
5096
+ });
5097
+ return out;
5098
+ }
5099
+ function buildRouteIndex(graph) {
5100
+ const index = /* @__PURE__ */ new Map();
5101
+ graph.forEachNode((_id, attrs) => {
5102
+ const node = attrs;
5103
+ if (node.type !== NodeType10.RouteNode) return;
5104
+ const route = attrs;
5105
+ const owner = serviceId3(route.service);
5106
+ const entry = {
5107
+ method: route.method.toUpperCase(),
5108
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
5109
+ routeNodeId: route.id
5110
+ };
5111
+ const list = index.get(owner);
5112
+ if (list) list.push(entry);
5113
+ else index.set(owner, [entry]);
5114
+ });
5115
+ return index;
5116
+ }
5117
+ function findRoute(entries, method, normalizedPath) {
5118
+ return entries.find(
5119
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
5120
+ );
5121
+ }
5122
+ async function addRouteCallEdges(graph, services) {
5123
+ const jsParser = makeJsParser4();
5124
+ const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5125
+ const routeIndex = buildRouteIndex(graph);
5126
+ if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
5127
+ let nodesAdded = 0;
5128
+ let edgesAdded = 0;
5129
+ for (const service of services) {
5130
+ const files = await loadSourceFiles(service.dir);
5131
+ const seen = /* @__PURE__ */ new Set();
5132
+ for (const file of files) {
5133
+ if (isTestPath(file.path)) continue;
5134
+ if (!JS_CLIENT_EXTENSIONS.has(path24.extname(file.path))) continue;
5135
+ let sites;
5136
+ try {
5137
+ sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
5138
+ } catch (err) {
5139
+ recordExtractionError("route-match call extraction", file.path, err);
5140
+ continue;
5141
+ }
5142
+ if (sites.length === 0) continue;
5143
+ const relFile = toPosix2(path24.relative(service.dir, file.path));
5144
+ for (const site of sites) {
5145
+ const serverServiceId = hostToNodeId.get(site.host);
5146
+ if (!serverServiceId || serverServiceId === service.node.id) continue;
5147
+ const entries = routeIndex.get(serverServiceId);
5148
+ if (!entries) continue;
5149
+ const normalizedPath = normalizePathTemplate(site.pathTemplate);
5150
+ const match = findRoute(entries, site.method, normalizedPath);
5151
+ if (!match) continue;
5152
+ const dedupKey = `${relFile}|${match.routeNodeId}`;
5153
+ if (seen.has(dedupKey)) continue;
5154
+ seen.add(dedupKey);
5155
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
5156
+ graph,
5157
+ service.pkg.name,
5158
+ service.node.id,
5159
+ relFile
5160
+ );
5161
+ nodesAdded += n;
5162
+ edgesAdded += e;
5163
+ const confidence = confidenceForExtracted7("verified-call-site");
5164
+ const ev = {
5165
+ file: relFile,
5166
+ line: site.line,
5167
+ snippet: site.snippet,
5168
+ method: site.method ?? match.method,
5169
+ pathTemplate: site.pathTemplate
5170
+ };
5171
+ if (!passesExtractedFloor2(confidence)) {
5172
+ noteExtractedDropped({
5173
+ source: fileNodeId,
5174
+ target: match.routeNodeId,
5175
+ type: EdgeType10.CALLS,
5176
+ confidence,
5177
+ confidenceKind: "verified-call-site",
5178
+ evidence: ev
5179
+ });
5180
+ continue;
5181
+ }
5182
+ const edgeId = extractedEdgeId2(fileNodeId, match.routeNodeId, EdgeType10.CALLS);
5183
+ if (!graph.hasEdge(edgeId)) {
5184
+ const edge = {
5185
+ id: edgeId,
5186
+ source: fileNodeId,
5187
+ target: match.routeNodeId,
5188
+ type: EdgeType10.CALLS,
5189
+ provenance: Provenance9.EXTRACTED,
5190
+ confidence,
5191
+ evidence: ev
5192
+ };
5193
+ graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
5194
+ edgesAdded++;
5195
+ }
5196
+ }
5197
+ }
5198
+ }
5199
+ return { nodesAdded, edgesAdded };
5200
+ }
5201
+
4485
5202
  // src/extract/calls/kafka.ts
4486
- import path23 from "path";
4487
- import { infraId } from "@neat.is/types";
5203
+ import path25 from "path";
5204
+ import { infraId as infraId2 } from "@neat.is/types";
4488
5205
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
4489
5206
  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
5207
  function findAll(re, text) {
@@ -4505,7 +5222,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4505
5222
  seen.add(key);
4506
5223
  const line = lineOf(file.content, topic);
4507
5224
  out.push({
4508
- infraId: infraId("kafka-topic", topic),
5225
+ infraId: infraId2("kafka-topic", topic),
4509
5226
  name: topic,
4510
5227
  kind: "kafka-topic",
4511
5228
  edgeType,
@@ -4514,7 +5231,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4514
5231
  // tier (ADR-066).
4515
5232
  confidenceKind: "verified-call-site",
4516
5233
  evidence: {
4517
- file: path23.relative(serviceDir, file.path),
5234
+ file: path25.relative(serviceDir, file.path),
4518
5235
  line,
4519
5236
  snippet: snippet(file.content, line)
4520
5237
  }
@@ -4526,8 +5243,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4526
5243
  }
4527
5244
 
4528
5245
  // src/extract/calls/redis.ts
4529
- import path24 from "path";
4530
- import { infraId as infraId2 } from "@neat.is/types";
5246
+ import path26 from "path";
5247
+ import { infraId as infraId3 } from "@neat.is/types";
4531
5248
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
4532
5249
  function redisEndpointsFromFile(file, serviceDir) {
4533
5250
  const out = [];
@@ -4540,7 +5257,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4540
5257
  seen.add(host);
4541
5258
  const line = lineOf(file.content, host);
4542
5259
  out.push({
4543
- infraId: infraId2("redis", host),
5260
+ infraId: infraId3("redis", host),
4544
5261
  name: host,
4545
5262
  kind: "redis",
4546
5263
  edgeType: "CALLS",
@@ -4549,7 +5266,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4549
5266
  // support tier (ADR-066).
4550
5267
  confidenceKind: "url-with-structural-support",
4551
5268
  evidence: {
4552
- file: path24.relative(serviceDir, file.path),
5269
+ file: path26.relative(serviceDir, file.path),
4553
5270
  line,
4554
5271
  snippet: snippet(file.content, line)
4555
5272
  }
@@ -4559,8 +5276,8 @@ function redisEndpointsFromFile(file, serviceDir) {
4559
5276
  }
4560
5277
 
4561
5278
  // src/extract/calls/aws.ts
4562
- import path25 from "path";
4563
- import { infraId as infraId3 } from "@neat.is/types";
5279
+ import path27 from "path";
5280
+ import { infraId as infraId4 } from "@neat.is/types";
4564
5281
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
4565
5282
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
4566
5283
  function hasMarker(text, markers) {
@@ -4584,7 +5301,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4584
5301
  seen.add(key);
4585
5302
  const line = lineOf(file.content, name);
4586
5303
  out.push({
4587
- infraId: infraId3(kind, name),
5304
+ infraId: infraId4(kind, name),
4588
5305
  name,
4589
5306
  kind,
4590
5307
  edgeType: "CALLS",
@@ -4593,7 +5310,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4593
5310
  // (ADR-066).
4594
5311
  confidenceKind: "verified-call-site",
4595
5312
  evidence: {
4596
- file: path25.relative(serviceDir, file.path),
5313
+ file: path27.relative(serviceDir, file.path),
4597
5314
  line,
4598
5315
  snippet: snippet(file.content, line)
4599
5316
  }
@@ -4617,8 +5334,8 @@ function awsEndpointsFromFile(file, serviceDir) {
4617
5334
  }
4618
5335
 
4619
5336
  // src/extract/calls/grpc.ts
4620
- import path26 from "path";
4621
- import { infraId as infraId4 } from "@neat.is/types";
5337
+ import path28 from "path";
5338
+ import { infraId as infraId5 } from "@neat.is/types";
4622
5339
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
4623
5340
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
4624
5341
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -4667,7 +5384,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4667
5384
  const { kind } = classified;
4668
5385
  const line = lineOf(file.content, m[0]);
4669
5386
  out.push({
4670
- infraId: infraId4(kind, name),
5387
+ infraId: infraId5(kind, name),
4671
5388
  name,
4672
5389
  kind,
4673
5390
  edgeType: "CALLS",
@@ -4676,7 +5393,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4676
5393
  // tier (ADR-066).
4677
5394
  confidenceKind: "verified-call-site",
4678
5395
  evidence: {
4679
- file: path26.relative(serviceDir, file.path),
5396
+ file: path28.relative(serviceDir, file.path),
4680
5397
  line,
4681
5398
  snippet: snippet(file.content, line)
4682
5399
  }
@@ -4686,8 +5403,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
4686
5403
  }
4687
5404
 
4688
5405
  // src/extract/calls/supabase.ts
4689
- import path27 from "path";
4690
- import { infraId as infraId5 } from "@neat.is/types";
5406
+ import path29 from "path";
5407
+ import { infraId as infraId6 } from "@neat.is/types";
4691
5408
  var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4692
5409
  var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4693
5410
  var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
@@ -4725,7 +5442,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
4725
5442
  seen.add(name);
4726
5443
  const line = lineOf(file.content, m[0]);
4727
5444
  out.push({
4728
- infraId: infraId5("supabase", name),
5445
+ infraId: infraId6("supabase", name),
4729
5446
  name,
4730
5447
  kind: "supabase",
4731
5448
  edgeType: "CALLS",
@@ -4735,7 +5452,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
4735
5452
  // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4736
5453
  confidenceKind: "verified-call-site",
4737
5454
  evidence: {
4738
- file: path27.relative(serviceDir, file.path),
5455
+ file: path29.relative(serviceDir, file.path),
4739
5456
  line,
4740
5457
  snippet: snippet(file.content, line)
4741
5458
  }
@@ -4748,11 +5465,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
4748
5465
  function edgeTypeFromEndpoint(ep) {
4749
5466
  switch (ep.edgeType) {
4750
5467
  case "PUBLISHES_TO":
4751
- return EdgeType9.PUBLISHES_TO;
5468
+ return EdgeType11.PUBLISHES_TO;
4752
5469
  case "CONSUMES_FROM":
4753
- return EdgeType9.CONSUMES_FROM;
5470
+ return EdgeType11.CONSUMES_FROM;
4754
5471
  default:
4755
- return EdgeType9.CALLS;
5472
+ return EdgeType11.CALLS;
4756
5473
  }
4757
5474
  }
4758
5475
  function isAwsKind(kind) {
@@ -4780,7 +5497,7 @@ async function addExternalEndpointEdges(graph, services) {
4780
5497
  if (!graph.hasNode(ep.infraId)) {
4781
5498
  const node = {
4782
5499
  id: ep.infraId,
4783
- type: NodeType9.InfraNode,
5500
+ type: NodeType11.InfraNode,
4784
5501
  name: ep.name,
4785
5502
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4786
5503
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4792,7 +5509,7 @@ async function addExternalEndpointEdges(graph, services) {
4792
5509
  nodesAdded++;
4793
5510
  }
4794
5511
  const edgeType = edgeTypeFromEndpoint(ep);
4795
- const confidence = confidenceForExtracted6(ep.confidenceKind);
5512
+ const confidence = confidenceForExtracted8(ep.confidenceKind);
4796
5513
  const relFile = toPosix2(ep.evidence.file);
4797
5514
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4798
5515
  graph,
@@ -4802,7 +5519,7 @@ async function addExternalEndpointEdges(graph, services) {
4802
5519
  );
4803
5520
  nodesAdded += n;
4804
5521
  edgesAdded += e;
4805
- if (!passesExtractedFloor2(confidence)) {
5522
+ if (!passesExtractedFloor3(confidence)) {
4806
5523
  noteExtractedDropped({
4807
5524
  source: fileNodeId,
4808
5525
  target: ep.infraId,
@@ -4822,7 +5539,7 @@ async function addExternalEndpointEdges(graph, services) {
4822
5539
  source: fileNodeId,
4823
5540
  target: ep.infraId,
4824
5541
  type: edgeType,
4825
- provenance: Provenance8.EXTRACTED,
5542
+ provenance: Provenance10.EXTRACTED,
4826
5543
  confidence,
4827
5544
  evidence: ep.evidence
4828
5545
  };
@@ -4836,22 +5553,23 @@ async function addExternalEndpointEdges(graph, services) {
4836
5553
  async function addCallEdges(graph, services) {
4837
5554
  const http = await addHttpCallEdges(graph, services);
4838
5555
  const ext = await addExternalEndpointEdges(graph, services);
5556
+ const routes = await addRouteCallEdges(graph, services);
4839
5557
  return {
4840
- nodesAdded: http.nodesAdded + ext.nodesAdded,
4841
- edgesAdded: http.edgesAdded + ext.edgesAdded
5558
+ nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
5559
+ edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
4842
5560
  };
4843
5561
  }
4844
5562
 
4845
5563
  // 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";
5564
+ import path30 from "path";
5565
+ import { EdgeType as EdgeType12, Provenance as Provenance11, confidenceForExtracted as confidenceForExtracted9 } from "@neat.is/types";
4848
5566
 
4849
5567
  // src/extract/infra/shared.ts
4850
- import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
5568
+ import { NodeType as NodeType12, infraId as infraId7 } from "@neat.is/types";
4851
5569
  function makeInfraNode(kind, name, provider = "self", extras) {
4852
5570
  return {
4853
- id: infraId6(kind, name),
4854
- type: NodeType10.InfraNode,
5571
+ id: infraId7(kind, name),
5572
+ type: NodeType12.InfraNode,
4855
5573
  name,
4856
5574
  provider,
4857
5575
  kind,
@@ -4880,7 +5598,7 @@ function dependsOnList(value) {
4880
5598
  }
4881
5599
  function serviceNameToServiceNode(name, services) {
4882
5600
  for (const s of services) {
4883
- if (s.node.name === name || path28.basename(s.dir) === name) return s.node.id;
5601
+ if (s.node.name === name || path30.basename(s.dir) === name) return s.node.id;
4884
5602
  }
4885
5603
  return null;
4886
5604
  }
@@ -4889,7 +5607,7 @@ async function addComposeInfra(graph, scanPath, services) {
4889
5607
  let edgesAdded = 0;
4890
5608
  let composePath = null;
4891
5609
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4892
- const abs = path28.join(scanPath, name);
5610
+ const abs = path30.join(scanPath, name);
4893
5611
  if (await exists(abs)) {
4894
5612
  composePath = abs;
4895
5613
  break;
@@ -4902,13 +5620,13 @@ async function addComposeInfra(graph, scanPath, services) {
4902
5620
  } catch (err) {
4903
5621
  recordExtractionError(
4904
5622
  "infra docker-compose",
4905
- path28.relative(scanPath, composePath),
5623
+ path30.relative(scanPath, composePath),
4906
5624
  err
4907
5625
  );
4908
5626
  return { nodesAdded, edgesAdded };
4909
5627
  }
4910
5628
  if (!compose?.services) return { nodesAdded, edgesAdded };
4911
- const evidenceFile = path28.relative(scanPath, composePath).split(path28.sep).join("/");
5629
+ const evidenceFile = path30.relative(scanPath, composePath).split(path30.sep).join("/");
4912
5630
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4913
5631
  for (const [composeName, svc] of Object.entries(compose.services)) {
4914
5632
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4930,15 +5648,15 @@ async function addComposeInfra(graph, scanPath, services) {
4930
5648
  for (const dep of dependsOnList(svc.depends_on)) {
4931
5649
  const targetId = composeNameToNodeId.get(dep);
4932
5650
  if (!targetId) continue;
4933
- const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType10.DEPENDS_ON);
5651
+ const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType12.DEPENDS_ON);
4934
5652
  if (graph.hasEdge(edgeId)) continue;
4935
5653
  const edge = {
4936
5654
  id: edgeId,
4937
5655
  source: sourceId,
4938
5656
  target: targetId,
4939
- type: EdgeType10.DEPENDS_ON,
4940
- provenance: Provenance9.EXTRACTED,
4941
- confidence: confidenceForExtracted7("structural"),
5657
+ type: EdgeType12.DEPENDS_ON,
5658
+ provenance: Provenance11.EXTRACTED,
5659
+ confidence: confidenceForExtracted9("structural"),
4942
5660
  evidence: { file: evidenceFile }
4943
5661
  };
4944
5662
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4949,9 +5667,9 @@ async function addComposeInfra(graph, scanPath, services) {
4949
5667
  }
4950
5668
 
4951
5669
  // src/extract/infra/dockerfile.ts
4952
- import path29 from "path";
5670
+ import path31 from "path";
4953
5671
  import { promises as fs15 } from "fs";
4954
- import { EdgeType as EdgeType11, Provenance as Provenance10, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
5672
+ import { EdgeType as EdgeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10 } from "@neat.is/types";
4955
5673
  function readDockerfile(content) {
4956
5674
  let image = null;
4957
5675
  const ports = [];
@@ -4980,7 +5698,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4980
5698
  let nodesAdded = 0;
4981
5699
  let edgesAdded = 0;
4982
5700
  for (const service of services) {
4983
- const dockerfilePath = path29.join(service.dir, "Dockerfile");
5701
+ const dockerfilePath = path31.join(service.dir, "Dockerfile");
4984
5702
  if (!await exists(dockerfilePath)) continue;
4985
5703
  let content;
4986
5704
  try {
@@ -4988,7 +5706,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4988
5706
  } catch (err) {
4989
5707
  recordExtractionError(
4990
5708
  "infra dockerfile",
4991
- path29.relative(scanPath, dockerfilePath),
5709
+ path31.relative(scanPath, dockerfilePath),
4992
5710
  err
4993
5711
  );
4994
5712
  continue;
@@ -5000,8 +5718,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5000
5718
  graph.addNode(node.id, node);
5001
5719
  nodesAdded++;
5002
5720
  }
5003
- const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
5004
- const evidenceFile = toPosix2(path29.relative(scanPath, dockerfilePath));
5721
+ const relDockerfile = toPosix2(path31.relative(service.dir, dockerfilePath));
5722
+ const evidenceFile = toPosix2(path31.relative(scanPath, dockerfilePath));
5005
5723
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5006
5724
  graph,
5007
5725
  service.pkg.name,
@@ -5010,15 +5728,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5010
5728
  );
5011
5729
  nodesAdded += fn;
5012
5730
  edgesAdded += fe;
5013
- const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType11.RUNS_ON);
5731
+ const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType13.RUNS_ON);
5014
5732
  if (!graph.hasEdge(edgeId)) {
5015
5733
  const edge = {
5016
5734
  id: edgeId,
5017
5735
  source: fileNodeId,
5018
5736
  target: node.id,
5019
- type: EdgeType11.RUNS_ON,
5020
- provenance: Provenance10.EXTRACTED,
5021
- confidence: confidenceForExtracted8("structural"),
5737
+ type: EdgeType13.RUNS_ON,
5738
+ provenance: Provenance12.EXTRACTED,
5739
+ confidence: confidenceForExtracted10("structural"),
5022
5740
  evidence: {
5023
5741
  file: evidenceFile,
5024
5742
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -5033,15 +5751,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5033
5751
  graph.addNode(portNode.id, portNode);
5034
5752
  nodesAdded++;
5035
5753
  }
5036
- const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType11.CONNECTS_TO);
5754
+ const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType13.CONNECTS_TO);
5037
5755
  if (graph.hasEdge(portEdgeId)) continue;
5038
5756
  const portEdge = {
5039
5757
  id: portEdgeId,
5040
5758
  source: fileNodeId,
5041
5759
  target: portNode.id,
5042
- type: EdgeType11.CONNECTS_TO,
5043
- provenance: Provenance10.EXTRACTED,
5044
- confidence: confidenceForExtracted8("structural"),
5760
+ type: EdgeType13.CONNECTS_TO,
5761
+ provenance: Provenance12.EXTRACTED,
5762
+ confidence: confidenceForExtracted10("structural"),
5045
5763
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5046
5764
  };
5047
5765
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -5053,8 +5771,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5053
5771
 
5054
5772
  // src/extract/infra/terraform.ts
5055
5773
  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";
5774
+ import path32 from "path";
5775
+ import { EdgeType as EdgeType14, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
5058
5776
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5059
5777
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5060
5778
  async function walkTfFiles(start, depth = 0, max = 5) {
@@ -5064,11 +5782,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
5064
5782
  for (const entry of entries) {
5065
5783
  if (entry.isDirectory()) {
5066
5784
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
5067
- const child = path30.join(start, entry.name);
5785
+ const child = path32.join(start, entry.name);
5068
5786
  if (await isPythonVenvDir(child)) continue;
5069
5787
  out.push(...await walkTfFiles(child, depth + 1, max));
5070
5788
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
5071
- out.push(path30.join(start, entry.name));
5789
+ out.push(path32.join(start, entry.name));
5072
5790
  }
5073
5791
  }
5074
5792
  return out;
@@ -5100,7 +5818,7 @@ async function addTerraformResources(graph, scanPath) {
5100
5818
  const files = await walkTfFiles(scanPath);
5101
5819
  for (const file of files) {
5102
5820
  const content = await fs16.readFile(file, "utf8");
5103
- const evidenceFile = toPosix2(path30.relative(scanPath, file));
5821
+ const evidenceFile = toPosix2(path32.relative(scanPath, file));
5104
5822
  const resources = [];
5105
5823
  const byKey = /* @__PURE__ */ new Map();
5106
5824
  RESOURCE_RE.lastIndex = 0;
@@ -5135,16 +5853,16 @@ async function addTerraformResources(graph, scanPath) {
5135
5853
  if (!target) continue;
5136
5854
  if (seen.has(target.nodeId)) continue;
5137
5855
  seen.add(target.nodeId);
5138
- const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType12.DEPENDS_ON);
5856
+ const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType14.DEPENDS_ON);
5139
5857
  if (graph.hasEdge(edgeId)) continue;
5140
5858
  const line = lineAt(content, resource.bodyOffset + ref.index);
5141
5859
  const edge = {
5142
5860
  id: edgeId,
5143
5861
  source: resource.nodeId,
5144
5862
  target: target.nodeId,
5145
- type: EdgeType12.DEPENDS_ON,
5146
- provenance: Provenance11.EXTRACTED,
5147
- confidence: confidenceForExtracted9("structural"),
5863
+ type: EdgeType14.DEPENDS_ON,
5864
+ provenance: Provenance13.EXTRACTED,
5865
+ confidence: confidenceForExtracted11("structural"),
5148
5866
  evidence: { file: evidenceFile, line, snippet: key }
5149
5867
  };
5150
5868
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -5157,7 +5875,7 @@ async function addTerraformResources(graph, scanPath) {
5157
5875
 
5158
5876
  // src/extract/infra/k8s.ts
5159
5877
  import { promises as fs17 } from "fs";
5160
- import path31 from "path";
5878
+ import path33 from "path";
5161
5879
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
5162
5880
  var K8S_KIND_TO_INFRA_KIND = {
5163
5881
  Service: "k8s-service",
@@ -5175,11 +5893,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
5175
5893
  for (const entry of entries) {
5176
5894
  if (entry.isDirectory()) {
5177
5895
  if (IGNORED_DIRS.has(entry.name)) continue;
5178
- const child = path31.join(start, entry.name);
5896
+ const child = path33.join(start, entry.name);
5179
5897
  if (await isPythonVenvDir(child)) continue;
5180
5898
  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));
5899
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path33.extname(entry.name))) {
5900
+ out.push(path33.join(start, entry.name));
5183
5901
  }
5184
5902
  }
5185
5903
  return out;
@@ -5223,16 +5941,16 @@ async function addInfra(graph, scanPath, services) {
5223
5941
  }
5224
5942
 
5225
5943
  // src/extract/index.ts
5226
- import path33 from "path";
5944
+ import path35 from "path";
5227
5945
 
5228
5946
  // src/extract/retire.ts
5229
5947
  import { existsSync as existsSync2 } from "fs";
5230
- import path32 from "path";
5231
- import { NodeType as NodeType11, Provenance as Provenance12 } from "@neat.is/types";
5948
+ import path34 from "path";
5949
+ import { NodeType as NodeType13, Provenance as Provenance14 } from "@neat.is/types";
5232
5950
  function dropOrphanedFileNodes(graph) {
5233
5951
  const orphans = [];
5234
5952
  graph.forEachNode((id, attrs) => {
5235
- if (attrs.type !== NodeType11.FileNode) return;
5953
+ if (attrs.type !== NodeType13.FileNode) return;
5236
5954
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5237
5955
  orphans.push(id);
5238
5956
  }
@@ -5245,7 +5963,7 @@ function retireEdgesByFile(graph, file) {
5245
5963
  const toDrop = [];
5246
5964
  graph.forEachEdge((id, attrs) => {
5247
5965
  const edge = attrs;
5248
- if (edge.provenance !== Provenance12.EXTRACTED) return;
5966
+ if (edge.provenance !== Provenance14.EXTRACTED) return;
5249
5967
  if (!edge.evidence?.file) return;
5250
5968
  if (edge.evidence.file === normalized) toDrop.push(id);
5251
5969
  });
@@ -5258,14 +5976,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5258
5976
  const bases = [scanPath, ...serviceDirs];
5259
5977
  graph.forEachEdge((id, attrs) => {
5260
5978
  const edge = attrs;
5261
- if (edge.provenance !== Provenance12.EXTRACTED) return;
5979
+ if (edge.provenance !== Provenance14.EXTRACTED) return;
5262
5980
  const evidenceFile = edge.evidence?.file;
5263
5981
  if (!evidenceFile) return;
5264
- if (path32.isAbsolute(evidenceFile)) {
5982
+ if (path34.isAbsolute(evidenceFile)) {
5265
5983
  if (!existsSync2(evidenceFile)) toDrop.push(id);
5266
5984
  return;
5267
5985
  }
5268
- const found = bases.some((base) => existsSync2(path32.join(base, evidenceFile)));
5986
+ const found = bases.some((base) => existsSync2(path34.join(base, evidenceFile)));
5269
5987
  if (!found) toDrop.push(id);
5270
5988
  });
5271
5989
  for (const id of toDrop) graph.dropEdge(id);
@@ -5284,6 +6002,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5284
6002
  const importGraph = await addImports(graph, services);
5285
6003
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
5286
6004
  const phase3 = await addConfigNodes(graph, services, scanPath);
6005
+ const routePhase = await addRoutes(graph, services);
5287
6006
  const phase4 = await addCallEdges(graph, services);
5288
6007
  const phase5 = await addInfra(graph, scanPath, services);
5289
6008
  const ghostsRetired = retireExtractedEdgesByMissingFile(
@@ -5305,7 +6024,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5305
6024
  }
5306
6025
  const droppedEntries = drainDroppedExtracted();
5307
6026
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5308
- const rejectedPath = path33.join(path33.dirname(opts.errorsPath), "rejected.ndjson");
6027
+ const rejectedPath = path35.join(path35.dirname(opts.errorsPath), "rejected.ndjson");
5309
6028
  try {
5310
6029
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5311
6030
  } catch (err) {
@@ -5315,8 +6034,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5315
6034
  }
5316
6035
  }
5317
6036
  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,
6037
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
6038
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
5320
6039
  frontiersPromoted,
5321
6040
  extractionErrors: errorEntries.length,
5322
6041
  errorEntries,
@@ -5341,10 +6060,10 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5341
6060
  import {
5342
6061
  databaseId as databaseId3,
5343
6062
  DivergenceResultSchema,
5344
- EdgeType as EdgeType13,
5345
- NodeType as NodeType12,
6063
+ EdgeType as EdgeType15,
6064
+ NodeType as NodeType14,
5346
6065
  parseEdgeId,
5347
- Provenance as Provenance13
6066
+ Provenance as Provenance15
5348
6067
  } from "@neat.is/types";
5349
6068
  function bucketKey(source, target, type) {
5350
6069
  return `${type}|${source}|${target}`;
@@ -5358,17 +6077,17 @@ function bucketEdges(graph) {
5358
6077
  const key = bucketKey(e.source, e.target, e.type);
5359
6078
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5360
6079
  switch (provenance) {
5361
- case Provenance13.EXTRACTED:
6080
+ case Provenance15.EXTRACTED:
5362
6081
  cur.extracted = e;
5363
6082
  break;
5364
- case Provenance13.OBSERVED:
6083
+ case Provenance15.OBSERVED:
5365
6084
  cur.observed = e;
5366
6085
  break;
5367
- case Provenance13.INFERRED:
6086
+ case Provenance15.INFERRED:
5368
6087
  cur.inferred = e;
5369
6088
  break;
5370
6089
  default:
5371
- if (e.provenance === Provenance13.STALE) cur.stale = e;
6090
+ if (e.provenance === Provenance15.STALE) cur.stale = e;
5372
6091
  }
5373
6092
  buckets.set(key, cur);
5374
6093
  });
@@ -5377,7 +6096,7 @@ function bucketEdges(graph) {
5377
6096
  function nodeIsFrontier(graph, nodeId) {
5378
6097
  if (!graph.hasNode(nodeId)) return false;
5379
6098
  const attrs = graph.getNodeAttributes(nodeId);
5380
- return attrs.type === NodeType12.FrontierNode;
6099
+ return attrs.type === NodeType14.FrontierNode;
5381
6100
  }
5382
6101
  function clampConfidence(n) {
5383
6102
  if (!Number.isFinite(n)) return 0;
@@ -5397,14 +6116,14 @@ function gradedConfidence(edge) {
5397
6116
  return clampConfidence(confidenceForEdge(edge));
5398
6117
  }
5399
6118
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
5400
- EdgeType13.CALLS,
5401
- EdgeType13.CONNECTS_TO,
5402
- EdgeType13.PUBLISHES_TO,
5403
- EdgeType13.CONSUMES_FROM
6119
+ EdgeType15.CALLS,
6120
+ EdgeType15.CONNECTS_TO,
6121
+ EdgeType15.PUBLISHES_TO,
6122
+ EdgeType15.CONSUMES_FROM
5404
6123
  ]);
5405
6124
  function detectMissingDivergences(graph, bucket) {
5406
6125
  const out = [];
5407
- if (bucket.type === EdgeType13.CONTAINS) return out;
6126
+ if (bucket.type === EdgeType15.CONTAINS) return out;
5408
6127
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5409
6128
  if (!nodeIsFrontier(graph, bucket.target)) {
5410
6129
  out.push({
@@ -5445,7 +6164,7 @@ function declaredHostFor(svc) {
5445
6164
  function hasExtractedConfiguredBy(graph, svcId) {
5446
6165
  for (const edgeId of graph.outboundEdges(svcId)) {
5447
6166
  const e = graph.getEdgeAttributes(edgeId);
5448
- if (e.type === EdgeType13.CONFIGURED_BY && e.provenance === Provenance13.EXTRACTED) {
6167
+ if (e.type === EdgeType15.CONFIGURED_BY && e.provenance === Provenance15.EXTRACTED) {
5449
6168
  return true;
5450
6169
  }
5451
6170
  }
@@ -5458,10 +6177,10 @@ function detectHostMismatch(graph, svcId, svc) {
5458
6177
  const out = [];
5459
6178
  for (const edgeId of graph.outboundEdges(svcId)) {
5460
6179
  const edge = graph.getEdgeAttributes(edgeId);
5461
- if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5462
- if (edge.provenance !== Provenance13.OBSERVED) continue;
6180
+ if (edge.type !== EdgeType15.CONNECTS_TO) continue;
6181
+ if (edge.provenance !== Provenance15.OBSERVED) continue;
5463
6182
  const target = graph.getNodeAttributes(edge.target);
5464
- if (target.type !== NodeType12.DatabaseNode) continue;
6183
+ if (target.type !== NodeType14.DatabaseNode) continue;
5465
6184
  const observedHost = target.host?.trim();
5466
6185
  if (!observedHost) continue;
5467
6186
  if (observedHost === declaredHost) continue;
@@ -5483,10 +6202,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5483
6202
  const deps = svc.dependencies ?? {};
5484
6203
  for (const edgeId of graph.outboundEdges(svcId)) {
5485
6204
  const edge = graph.getEdgeAttributes(edgeId);
5486
- if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5487
- if (edge.provenance !== Provenance13.OBSERVED) continue;
6205
+ if (edge.type !== EdgeType15.CONNECTS_TO) continue;
6206
+ if (edge.provenance !== Provenance15.OBSERVED) continue;
5488
6207
  const target = graph.getNodeAttributes(edge.target);
5489
- if (target.type !== NodeType12.DatabaseNode) continue;
6208
+ if (target.type !== NodeType14.DatabaseNode) continue;
5490
6209
  for (const pair of compatPairs()) {
5491
6210
  if (pair.engine !== target.engine) continue;
5492
6211
  const declared = deps[pair.driver];
@@ -5564,7 +6283,7 @@ function computeDivergences(graph, opts = {}) {
5564
6283
  }
5565
6284
  graph.forEachNode((nodeId, attrs) => {
5566
6285
  const n = attrs;
5567
- if (n.type !== NodeType12.ServiceNode) return;
6286
+ if (n.type !== NodeType14.ServiceNode) return;
5568
6287
  const svc = n;
5569
6288
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5570
6289
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5607,8 +6326,8 @@ function computeDivergences(graph, opts = {}) {
5607
6326
 
5608
6327
  // src/persist.ts
5609
6328
  import { promises as fs18 } from "fs";
5610
- import path34 from "path";
5611
- import { Provenance as Provenance14, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
6329
+ import path36 from "path";
6330
+ import { Provenance as Provenance16, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
5612
6331
  var SCHEMA_VERSION = 4;
5613
6332
  function migrateV1ToV2(payload) {
5614
6333
  const nodes = payload.graph.nodes;
@@ -5630,7 +6349,7 @@ function migrateV2ToV3(payload) {
5630
6349
  for (const edge of edges) {
5631
6350
  const attrs = edge.attributes;
5632
6351
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5633
- attrs.provenance = Provenance14.OBSERVED;
6352
+ attrs.provenance = Provenance16.OBSERVED;
5634
6353
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5635
6354
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5636
6355
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -5644,7 +6363,7 @@ function migrateV2ToV3(payload) {
5644
6363
  return { ...payload, schemaVersion: 3 };
5645
6364
  }
5646
6365
  async function ensureDir(filePath) {
5647
- await fs18.mkdir(path34.dirname(filePath), { recursive: true });
6366
+ await fs18.mkdir(path36.dirname(filePath), { recursive: true });
5648
6367
  }
5649
6368
  async function saveGraphToDisk(graph, outPath) {
5650
6369
  await ensureDir(outPath);
@@ -5800,23 +6519,23 @@ function canonicalJson(value) {
5800
6519
  }
5801
6520
 
5802
6521
  // src/projects.ts
5803
- import path35 from "path";
6522
+ import path37 from "path";
5804
6523
  function pathsForProject(project, baseDir) {
5805
6524
  if (project === DEFAULT_PROJECT) {
5806
6525
  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")
6526
+ snapshotPath: path37.join(baseDir, "graph.json"),
6527
+ errorsPath: path37.join(baseDir, "errors.ndjson"),
6528
+ staleEventsPath: path37.join(baseDir, "stale-events.ndjson"),
6529
+ embeddingsCachePath: path37.join(baseDir, "embeddings.json"),
6530
+ policyViolationsPath: path37.join(baseDir, "policy-violations.ndjson")
5812
6531
  };
5813
6532
  }
5814
6533
  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`)
6534
+ snapshotPath: path37.join(baseDir, `${project}.json`),
6535
+ errorsPath: path37.join(baseDir, `errors.${project}.ndjson`),
6536
+ staleEventsPath: path37.join(baseDir, `stale-events.${project}.ndjson`),
6537
+ embeddingsCachePath: path37.join(baseDir, `embeddings.${project}.json`),
6538
+ policyViolationsPath: path37.join(baseDir, `policy-violations.${project}.ndjson`)
5820
6539
  };
5821
6540
  }
5822
6541
  var Projects = class {
@@ -5857,7 +6576,7 @@ function parseExtraProjects(raw) {
5857
6576
  // src/registry.ts
5858
6577
  import { promises as fs20 } from "fs";
5859
6578
  import os2 from "os";
5860
- import path36 from "path";
6579
+ import path38 from "path";
5861
6580
  import {
5862
6581
  RegistryFileSchema
5863
6582
  } from "@neat.is/types";
@@ -5865,20 +6584,20 @@ var LOCK_TIMEOUT_MS = 5e3;
5865
6584
  var LOCK_RETRY_MS = 50;
5866
6585
  function neatHome() {
5867
6586
  const override = process.env.NEAT_HOME;
5868
- if (override && override.length > 0) return path36.resolve(override);
5869
- return path36.join(os2.homedir(), ".neat");
6587
+ if (override && override.length > 0) return path38.resolve(override);
6588
+ return path38.join(os2.homedir(), ".neat");
5870
6589
  }
5871
6590
  function registryPath() {
5872
- return path36.join(neatHome(), "projects.json");
6591
+ return path38.join(neatHome(), "projects.json");
5873
6592
  }
5874
6593
  function registryLockPath() {
5875
- return path36.join(neatHome(), "projects.json.lock");
6594
+ return path38.join(neatHome(), "projects.json.lock");
5876
6595
  }
5877
6596
  function daemonPidPath() {
5878
- return path36.join(neatHome(), "neatd.pid");
6597
+ return path38.join(neatHome(), "neatd.pid");
5879
6598
  }
5880
6599
  function daemonsDir() {
5881
- return path36.join(neatHome(), "daemons");
6600
+ return path38.join(neatHome(), "daemons");
5882
6601
  }
5883
6602
  function isFiniteInt(v) {
5884
6603
  return typeof v === "number" && Number.isFinite(v);
@@ -5919,7 +6638,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
5919
6638
  const out = [];
5920
6639
  for (const name of names) {
5921
6640
  if (!name.endsWith(".json")) continue;
5922
- const file = path36.join(dir, name);
6641
+ const file = path38.join(dir, name);
5923
6642
  let raw;
5924
6643
  try {
5925
6644
  raw = await fs20.readFile(file, "utf8");
@@ -6040,7 +6759,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
6040
6759
  }
6041
6760
  }
6042
6761
  async function normalizeProjectPath(input) {
6043
- const resolved = path36.resolve(input);
6762
+ const resolved = path38.resolve(input);
6044
6763
  try {
6045
6764
  return await fs20.realpath(resolved);
6046
6765
  } catch {
@@ -6048,7 +6767,7 @@ async function normalizeProjectPath(input) {
6048
6767
  }
6049
6768
  }
6050
6769
  async function writeAtomically(target, contents) {
6051
- await fs20.mkdir(path36.dirname(target), { recursive: true });
6770
+ await fs20.mkdir(path38.dirname(target), { recursive: true });
6052
6771
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
6053
6772
  const fd = await fs20.open(tmp, "w");
6054
6773
  try {
@@ -6061,7 +6780,7 @@ async function writeAtomically(target, contents) {
6061
6780
  }
6062
6781
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6063
6782
  const deadline = Date.now() + timeoutMs;
6064
- await fs20.mkdir(path36.dirname(lockPath), { recursive: true });
6783
+ await fs20.mkdir(path38.dirname(lockPath), { recursive: true });
6065
6784
  let probedHolder = false;
6066
6785
  while (true) {
6067
6786
  try {
@@ -6257,13 +6976,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
6257
6976
 
6258
6977
  // src/extend/index.ts
6259
6978
  import { promises as fs22 } from "fs";
6260
- import path38 from "path";
6979
+ import path40 from "path";
6261
6980
  import os3 from "os";
6262
6981
  import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
6263
6982
 
6264
6983
  // src/installers/package-manager.ts
6265
6984
  import { promises as fs21 } from "fs";
6266
- import path37 from "path";
6985
+ import path39 from "path";
6267
6986
  import { spawn } from "child_process";
6268
6987
  var LOCKFILE_PRIORITY = [
6269
6988
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -6285,22 +7004,22 @@ async function exists2(p) {
6285
7004
  }
6286
7005
  }
6287
7006
  async function detectPackageManager(serviceDir) {
6288
- let dir = path37.resolve(serviceDir);
7007
+ let dir = path39.resolve(serviceDir);
6289
7008
  const stops = /* @__PURE__ */ new Set();
6290
7009
  for (let i = 0; i < 64; i++) {
6291
7010
  if (stops.has(dir)) break;
6292
7011
  stops.add(dir);
6293
7012
  for (const candidate of LOCKFILE_PRIORITY) {
6294
- const lockPath = path37.join(dir, candidate.lockfile);
7013
+ const lockPath = path39.join(dir, candidate.lockfile);
6295
7014
  if (await exists2(lockPath)) {
6296
7015
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
6297
7016
  }
6298
7017
  }
6299
- const parent = path37.dirname(dir);
7018
+ const parent = path39.dirname(dir);
6300
7019
  if (parent === dir) break;
6301
7020
  dir = parent;
6302
7021
  }
6303
- return { pm: "npm", cwd: path37.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
7022
+ return { pm: "npm", cwd: path39.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
6304
7023
  }
6305
7024
  async function runPackageManagerInstall(cmd) {
6306
7025
  return new Promise((resolve) => {
@@ -6349,7 +7068,7 @@ async function fileExists2(p) {
6349
7068
  }
6350
7069
  }
6351
7070
  async function readPackageJson(scanPath) {
6352
- const pkgPath = path38.join(scanPath, "package.json");
7071
+ const pkgPath = path40.join(scanPath, "package.json");
6353
7072
  const raw = await fs22.readFile(pkgPath, "utf8");
6354
7073
  return JSON.parse(raw);
6355
7074
  }
@@ -6360,11 +7079,11 @@ async function findHookFiles(scanPath) {
6360
7079
  ).sort();
6361
7080
  }
6362
7081
  function extendLogPath() {
6363
- return process.env.NEAT_EXTEND_LOG ?? path38.join(os3.homedir(), ".neat", "extend-log.ndjson");
7082
+ return process.env.NEAT_EXTEND_LOG ?? path40.join(os3.homedir(), ".neat", "extend-log.ndjson");
6364
7083
  }
6365
7084
  async function appendExtendLog(entry) {
6366
7085
  const logPath = extendLogPath();
6367
- await fs22.mkdir(path38.dirname(logPath), { recursive: true });
7086
+ await fs22.mkdir(path40.dirname(logPath), { recursive: true });
6368
7087
  await fs22.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
6369
7088
  }
6370
7089
  function splicedContent(fileContent, snippet2) {
@@ -6423,7 +7142,7 @@ function lookupInstrumentation(library, installedVersion) {
6423
7142
  }
6424
7143
  async function describeProjectInstrumentation(ctx) {
6425
7144
  const hookFiles = await findHookFiles(ctx.scanPath);
6426
- const envNeat = await fileExists2(path38.join(ctx.scanPath, ".env.neat"));
7145
+ const envNeat = await fileExists2(path40.join(ctx.scanPath, ".env.neat"));
6427
7146
  const registryInstrPackages = new Set(
6428
7147
  registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
6429
7148
  );
@@ -6445,16 +7164,16 @@ async function applyExtension(ctx, args, options) {
6445
7164
  );
6446
7165
  }
6447
7166
  for (const file of hookFiles) {
6448
- const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
7167
+ const content = await fs22.readFile(path40.join(ctx.scanPath, file), "utf8");
6449
7168
  if (content.includes(args.registration_snippet)) {
6450
7169
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
6451
7170
  }
6452
7171
  }
6453
7172
  const primaryFile = hookFiles[0];
6454
- const primaryPath = path38.join(ctx.scanPath, primaryFile);
7173
+ const primaryPath = path40.join(ctx.scanPath, primaryFile);
6455
7174
  const filesTouched = [];
6456
7175
  const depsAdded = [];
6457
- const pkgPath = path38.join(ctx.scanPath, "package.json");
7176
+ const pkgPath = path40.join(ctx.scanPath, "package.json");
6458
7177
  const pkg = await readPackageJson(ctx.scanPath);
6459
7178
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
6460
7179
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -6500,7 +7219,7 @@ async function dryRunExtension(ctx, args) {
6500
7219
  };
6501
7220
  }
6502
7221
  for (const file of hookFiles) {
6503
- const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
7222
+ const content = await fs22.readFile(path40.join(ctx.scanPath, file), "utf8");
6504
7223
  if (content.includes(args.registration_snippet)) {
6505
7224
  return {
6506
7225
  library: args.library,
@@ -6522,7 +7241,7 @@ async function dryRunExtension(ctx, args) {
6522
7241
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
6523
7242
  filesTouched.push("package.json");
6524
7243
  }
6525
- const hookContent = await fs22.readFile(path38.join(ctx.scanPath, primaryFile), "utf8");
7244
+ const hookContent = await fs22.readFile(path40.join(ctx.scanPath, primaryFile), "utf8");
6526
7245
  const patched = splicedContent(hookContent, args.registration_snippet);
6527
7246
  if (patched) {
6528
7247
  filesTouched.push(primaryFile);
@@ -6543,7 +7262,7 @@ async function rollbackExtension(ctx, args) {
6543
7262
  if (!match) {
6544
7263
  return { undone: false, message: "no apply found for library" };
6545
7264
  }
6546
- const pkgPath = path38.join(ctx.scanPath, "package.json");
7265
+ const pkgPath = path40.join(ctx.scanPath, "package.json");
6547
7266
  if (await fileExists2(pkgPath)) {
6548
7267
  const pkg = await readPackageJson(ctx.scanPath);
6549
7268
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -6554,7 +7273,7 @@ async function rollbackExtension(ctx, args) {
6554
7273
  }
6555
7274
  const hookFiles = await findHookFiles(ctx.scanPath);
6556
7275
  for (const file of hookFiles) {
6557
- const filePath = path38.join(ctx.scanPath, file);
7276
+ const filePath = path40.join(ctx.scanPath, file);
6558
7277
  const content = await fs22.readFile(filePath, "utf8");
6559
7278
  if (content.includes(match.registration_snippet)) {
6560
7279
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -7362,4 +8081,4 @@ export {
7362
8081
  pruneRegistry,
7363
8082
  buildApi
7364
8083
  };
7365
- //# sourceMappingURL=chunk-O25KZNZK.js.map
8084
+ //# sourceMappingURL=chunk-WZYH5DVG.js.map