@neat.is/core 0.4.31-dev.20260718 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-2AJHEZIB.js";
4
+ } from "./chunk-5MGHWP6R.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-4M2YMGUM.js";
40
+ } from "./chunk-QQDZSIY3.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-I72HTUOG.js";
package/dist/neatd.cjs CHANGED
@@ -1240,7 +1240,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1240
1240
  for (const id of walk3.path) {
1241
1241
  const owner = resolveOwningService(graph, id);
1242
1242
  if (!owner) continue;
1243
- const { id: serviceId5, svc } = owner;
1243
+ const { id: serviceId6, svc } = owner;
1244
1244
  const deps = svc.dependencies ?? {};
1245
1245
  for (const pair of candidatePairs) {
1246
1246
  const declared = deps[pair.driver];
@@ -1253,7 +1253,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1253
1253
  );
1254
1254
  if (!result.compatible) {
1255
1255
  return {
1256
- rootCauseNode: serviceId5,
1256
+ rootCauseNode: serviceId6,
1257
1257
  rootCauseReason: result.reason ?? "incompatible driver",
1258
1258
  ...result.minDriverVersion ? {
1259
1259
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1268,7 +1268,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1268
1268
  for (const id of walk3.path) {
1269
1269
  const owner = resolveOwningService(graph, id);
1270
1270
  if (!owner) continue;
1271
- const { id: serviceId5, svc } = owner;
1271
+ const { id: serviceId6, svc } = owner;
1272
1272
  const deps = svc.dependencies ?? {};
1273
1273
  const serviceNodeEngine = svc.nodeEngine;
1274
1274
  for (const constraint of nodeEngineConstraints()) {
@@ -1277,7 +1277,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1277
1277
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1278
1278
  if (!result.compatible && result.reason) {
1279
1279
  return {
1280
- rootCauseNode: serviceId5,
1280
+ rootCauseNode: serviceId6,
1281
1281
  rootCauseReason: result.reason,
1282
1282
  ...result.requiredNodeVersion ? {
1283
1283
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1292,7 +1292,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1292
1292
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1293
1293
  if (!result.compatible && result.reason) {
1294
1294
  return {
1295
- rootCauseNode: serviceId5,
1295
+ rootCauseNode: serviceId6,
1296
1296
  rootCauseReason: result.reason,
1297
1297
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1298
1298
  };
@@ -1383,9 +1383,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1383
1383
  function isFailingCallEdge(e) {
1384
1384
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1385
1385
  }
1386
- function callSourcesForService(graph, serviceId5) {
1387
- const ids = [serviceId5];
1388
- for (const edgeId of graph.outboundEdges(serviceId5)) {
1386
+ function callSourcesForService(graph, serviceId6) {
1387
+ const ids = [serviceId6];
1388
+ for (const edgeId of graph.outboundEdges(serviceId6)) {
1389
1389
  const e = graph.getEdgeAttributes(edgeId);
1390
1390
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1391
1391
  const tgt = graph.getNodeAttributes(e.target);
@@ -1402,9 +1402,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1402
1402
  }
1403
1403
  return id < curId;
1404
1404
  }
1405
- function dominantFailingCall(graph, serviceId5, visited) {
1405
+ function dominantFailingCall(graph, serviceId6, visited) {
1406
1406
  let best = null;
1407
- for (const src of callSourcesForService(graph, serviceId5)) {
1407
+ for (const src of callSourcesForService(graph, serviceId6)) {
1408
1408
  for (const edgeId of graph.outboundEdges(src)) {
1409
1409
  const e = graph.getEdgeAttributes(edgeId);
1410
1410
  if (!isFailingCallEdge(e)) continue;
@@ -2586,6 +2586,27 @@ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
2586
2586
  graph.addNode(id, node);
2587
2587
  return id;
2588
2588
  }
2589
+ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
2590
+ if (!graph.hasNode(serviceNodeId)) return null;
2591
+ const sources = [serviceNodeId];
2592
+ for (const edgeId of graph.outboundEdges(serviceNodeId)) {
2593
+ const e = graph.getEdgeAttributes(edgeId);
2594
+ if (e.type === import_types3.EdgeType.CONTAINS) sources.push(e.target);
2595
+ }
2596
+ const matches = /* @__PURE__ */ new Set();
2597
+ for (const src of sources) {
2598
+ if (!graph.hasNode(src)) continue;
2599
+ for (const edgeId of graph.outboundEdges(src)) {
2600
+ const edge = graph.getEdgeAttributes(edgeId);
2601
+ if (edge.type !== import_types3.EdgeType.CONNECTS_TO || edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
2602
+ if (!graph.hasNode(edge.target)) continue;
2603
+ const target = graph.getNodeAttributes(edge.target);
2604
+ if (target.type !== import_types3.NodeType.DatabaseNode || target.engine !== engine) continue;
2605
+ matches.add(edge.target);
2606
+ }
2607
+ }
2608
+ return matches.size === 1 ? [...matches][0] : null;
2609
+ }
2589
2610
  function ensureFrontierNode(graph, host, ts) {
2590
2611
  const id = frontierIdFor(host);
2591
2612
  if (graph.hasNode(id)) {
@@ -2844,13 +2865,18 @@ async function handleSpan(ctx, span) {
2844
2865
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2845
2866
  targetId = (0, import_types3.databaseId)(host);
2846
2867
  } else {
2847
- const localName = span.dbName ?? span.dbSystem;
2848
- targetId = ensureLocalDatabaseNode(
2849
- ctx.graph,
2850
- span.service,
2851
- localName,
2852
- span.dbSystem
2853
- );
2868
+ const declared = findDeclaredDatabaseForService(ctx.graph, sourceId, span.dbSystem);
2869
+ if (declared) {
2870
+ targetId = declared;
2871
+ } else {
2872
+ const localName = span.dbName ?? span.dbSystem;
2873
+ targetId = ensureLocalDatabaseNode(
2874
+ ctx.graph,
2875
+ span.service,
2876
+ localName,
2877
+ span.dbSystem
2878
+ );
2879
+ }
2854
2880
  }
2855
2881
  const result = upsertObservedEdge(
2856
2882
  ctx.graph,
@@ -3034,29 +3060,29 @@ function promoteFrontierNodes(graph, opts = {}) {
3034
3060
  toPromote.push({ frontierId: id, serviceId: target });
3035
3061
  });
3036
3062
  let promoted = 0;
3037
- for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
3063
+ for (const { frontierId: frontierId2, serviceId: serviceId6 } of toPromote) {
3038
3064
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
3039
3065
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
3040
3066
  if (!gate.allowed) {
3041
3067
  continue;
3042
3068
  }
3043
3069
  }
3044
- rewireFrontierEdges(graph, frontierId2, serviceId5);
3070
+ rewireFrontierEdges(graph, frontierId2, serviceId6);
3045
3071
  graph.dropNode(frontierId2);
3046
3072
  promoted++;
3047
3073
  }
3048
3074
  return promoted;
3049
3075
  }
3050
- function rewireFrontierEdges(graph, frontierId2, serviceId5) {
3076
+ function rewireFrontierEdges(graph, frontierId2, serviceId6) {
3051
3077
  const inbound = [...graph.inboundEdges(frontierId2)];
3052
3078
  const outbound = [...graph.outboundEdges(frontierId2)];
3053
3079
  for (const edgeId of inbound) {
3054
3080
  const edge = graph.getEdgeAttributes(edgeId);
3055
- rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
3081
+ rebuildEdge(graph, edge, edge.source, serviceId6, edgeId);
3056
3082
  }
3057
3083
  for (const edgeId of outbound) {
3058
3084
  const edge = graph.getEdgeAttributes(edgeId);
3059
- rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
3085
+ rebuildEdge(graph, edge, serviceId6, edge.target, edgeId);
3060
3086
  }
3061
3087
  }
3062
3088
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -3849,9 +3875,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
3849
3875
  "StatefulSet",
3850
3876
  "DaemonSet"
3851
3877
  ]);
3852
- function addAliases(graph, serviceId5, candidates) {
3853
- if (!graph.hasNode(serviceId5)) return;
3854
- const node = graph.getNodeAttributes(serviceId5);
3878
+ function addAliases(graph, serviceId6, candidates) {
3879
+ if (!graph.hasNode(serviceId6)) return;
3880
+ const node = graph.getNodeAttributes(serviceId6);
3855
3881
  if (node.type !== import_types6.NodeType.ServiceNode) return;
3856
3882
  const set = new Set(node.aliases ?? []);
3857
3883
  for (const c of candidates) {
@@ -3861,7 +3887,7 @@ function addAliases(graph, serviceId5, candidates) {
3861
3887
  }
3862
3888
  if (set.size === 0) return;
3863
3889
  const updated = { ...node, aliases: [...set].sort() };
3864
- graph.replaceNodeAttributes(serviceId5, updated);
3890
+ graph.replaceNodeAttributes(serviceId6, updated);
3865
3891
  }
3866
3892
  function indexServicesByName(services) {
3867
3893
  const map = /* @__PURE__ */ new Map();
@@ -3894,12 +3920,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3894
3920
  }
3895
3921
  if (!compose?.services) return;
3896
3922
  for (const [composeName, svc] of Object.entries(compose.services)) {
3897
- const serviceId5 = serviceIndex.get(composeName);
3898
- if (!serviceId5) continue;
3923
+ const serviceId6 = serviceIndex.get(composeName);
3924
+ if (!serviceId6) continue;
3899
3925
  const aliases = /* @__PURE__ */ new Set([composeName]);
3900
3926
  if (svc.container_name) aliases.add(svc.container_name);
3901
3927
  if (svc.hostname) aliases.add(svc.hostname);
3902
- addAliases(graph, serviceId5, aliases);
3928
+ addAliases(graph, serviceId6, aliases);
3903
3929
  }
3904
3930
  }
3905
3931
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -4549,6 +4575,29 @@ async function readIfExists(filePath) {
4549
4575
  return null;
4550
4576
  }
4551
4577
  }
4578
+ async function resolveEnvVar(serviceDir, name) {
4579
+ const entries = await import_node_fs12.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
4580
+ const envNames = entries.filter((e) => e.isFile() && (e.name === ".env" || e.name.startsWith(".env."))).map((e) => e.name);
4581
+ const rank = (n) => n === ".env.local" ? 0 : n === ".env" ? 1 : 2;
4582
+ envNames.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
4583
+ for (const fileName of envNames) {
4584
+ const content = await readIfExists(import_node_path14.default.join(serviceDir, fileName));
4585
+ if (!content) continue;
4586
+ for (const line of content.split("\n")) {
4587
+ const trimmed = line.trim();
4588
+ if (!trimmed || trimmed.startsWith("#")) continue;
4589
+ const eq = trimmed.indexOf("=");
4590
+ if (eq < 0) continue;
4591
+ if (trimmed.slice(0, eq).trim() !== name) continue;
4592
+ let value = trimmed.slice(eq + 1).trim();
4593
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4594
+ value = value.slice(1, -1);
4595
+ }
4596
+ return value || null;
4597
+ }
4598
+ }
4599
+ return null;
4600
+ }
4552
4601
  async function findFirst(serviceDir, candidates) {
4553
4602
  for (const rel of candidates) {
4554
4603
  const abs = import_node_path14.default.join(serviceDir, rel);
@@ -4646,6 +4695,14 @@ async function parse3(serviceDir) {
4646
4695
  const config = parseConnectionString(urlMatch[1]);
4647
4696
  if (config) return [{ ...config, sourceFile: schemaPath }];
4648
4697
  }
4698
+ const envMatch = body.match(/url\s*=\s*env\(\s*"([^"]+)"\s*\)/);
4699
+ if (envMatch) {
4700
+ const resolved = await resolveEnvVar(serviceDir, envMatch[1]);
4701
+ if (resolved) {
4702
+ const config = parseConnectionString(resolved);
4703
+ if (config) return [{ ...config, sourceFile: schemaPath }];
4704
+ }
4705
+ }
4649
4706
  return [
4650
4707
  {
4651
4708
  host: `${engine}-prisma`,
@@ -7882,14 +7939,23 @@ var import_types31 = require("@neat.is/types");
7882
7939
  function bucketKey(source, target, type) {
7883
7940
  return `${type}|${source}|${target}`;
7884
7941
  }
7942
+ function bucketSourceFor(graph, edge) {
7943
+ if (edge.type !== import_types31.EdgeType.CONNECTS_TO) return edge.source;
7944
+ const parsed = (0, import_types31.parseFileId)(edge.source);
7945
+ if (!parsed || !graph.hasNode(edge.target)) return edge.source;
7946
+ const target = graph.getNodeAttributes(edge.target);
7947
+ if (target.type !== import_types31.NodeType.DatabaseNode) return edge.source;
7948
+ return (0, import_types31.serviceId)(parsed.service);
7949
+ }
7885
7950
  function bucketEdges(graph) {
7886
7951
  const buckets2 = /* @__PURE__ */ new Map();
7887
7952
  graph.forEachEdge((id, attrs) => {
7888
7953
  const e = attrs;
7889
7954
  const parsed = (0, import_types31.parseEdgeId)(id);
7890
7955
  const provenance = parsed?.provenance ?? e.provenance;
7891
- const key = bucketKey(e.source, e.target, e.type);
7892
- const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
7956
+ const source = bucketSourceFor(graph, e);
7957
+ const key = bucketKey(source, e.target, e.type);
7958
+ const cur = buckets2.get(key) ?? { source, target: e.target, type: e.type };
7893
7959
  switch (provenance) {
7894
7960
  case import_types31.Provenance.EXTRACTED:
7895
7961
  cur.extracted = e;