@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/cli.cjs CHANGED
@@ -1283,7 +1283,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1283
1283
  for (const id of walk3.path) {
1284
1284
  const owner = resolveOwningService(graph, id);
1285
1285
  if (!owner) continue;
1286
- const { id: serviceId5, svc } = owner;
1286
+ const { id: serviceId6, svc } = owner;
1287
1287
  const deps = svc.dependencies ?? {};
1288
1288
  for (const pair of candidatePairs) {
1289
1289
  const declared = deps[pair.driver];
@@ -1296,7 +1296,7 @@ function databaseRootCauseShape(graph, origin, walk3) {
1296
1296
  );
1297
1297
  if (!result.compatible) {
1298
1298
  return {
1299
- rootCauseNode: serviceId5,
1299
+ rootCauseNode: serviceId6,
1300
1300
  rootCauseReason: result.reason ?? "incompatible driver",
1301
1301
  ...result.minDriverVersion ? {
1302
1302
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1311,7 +1311,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1311
1311
  for (const id of walk3.path) {
1312
1312
  const owner = resolveOwningService(graph, id);
1313
1313
  if (!owner) continue;
1314
- const { id: serviceId5, svc } = owner;
1314
+ const { id: serviceId6, svc } = owner;
1315
1315
  const deps = svc.dependencies ?? {};
1316
1316
  const serviceNodeEngine = svc.nodeEngine;
1317
1317
  for (const constraint of nodeEngineConstraints()) {
@@ -1320,7 +1320,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1320
1320
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1321
1321
  if (!result.compatible && result.reason) {
1322
1322
  return {
1323
- rootCauseNode: serviceId5,
1323
+ rootCauseNode: serviceId6,
1324
1324
  rootCauseReason: result.reason,
1325
1325
  ...result.requiredNodeVersion ? {
1326
1326
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1335,7 +1335,7 @@ function serviceRootCauseShape(graph, _origin, walk3) {
1335
1335
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1336
1336
  if (!result.compatible && result.reason) {
1337
1337
  return {
1338
- rootCauseNode: serviceId5,
1338
+ rootCauseNode: serviceId6,
1339
1339
  rootCauseReason: result.reason,
1340
1340
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1341
1341
  };
@@ -1426,9 +1426,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1426
1426
  function isFailingCallEdge(e) {
1427
1427
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1428
1428
  }
1429
- function callSourcesForService(graph, serviceId5) {
1430
- const ids = [serviceId5];
1431
- for (const edgeId of graph.outboundEdges(serviceId5)) {
1429
+ function callSourcesForService(graph, serviceId6) {
1430
+ const ids = [serviceId6];
1431
+ for (const edgeId of graph.outboundEdges(serviceId6)) {
1432
1432
  const e = graph.getEdgeAttributes(edgeId);
1433
1433
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1434
1434
  const tgt = graph.getNodeAttributes(e.target);
@@ -1445,9 +1445,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1445
1445
  }
1446
1446
  return id < curId;
1447
1447
  }
1448
- function dominantFailingCall(graph, serviceId5, visited) {
1448
+ function dominantFailingCall(graph, serviceId6, visited) {
1449
1449
  let best = null;
1450
- for (const src of callSourcesForService(graph, serviceId5)) {
1450
+ for (const src of callSourcesForService(graph, serviceId6)) {
1451
1451
  for (const edgeId of graph.outboundEdges(src)) {
1452
1452
  const e = graph.getEdgeAttributes(edgeId);
1453
1453
  if (!isFailingCallEdge(e)) continue;
@@ -2616,6 +2616,27 @@ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
2616
2616
  graph.addNode(id, node);
2617
2617
  return id;
2618
2618
  }
2619
+ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
2620
+ if (!graph.hasNode(serviceNodeId)) return null;
2621
+ const sources = [serviceNodeId];
2622
+ for (const edgeId of graph.outboundEdges(serviceNodeId)) {
2623
+ const e = graph.getEdgeAttributes(edgeId);
2624
+ if (e.type === import_types3.EdgeType.CONTAINS) sources.push(e.target);
2625
+ }
2626
+ const matches = /* @__PURE__ */ new Set();
2627
+ for (const src of sources) {
2628
+ if (!graph.hasNode(src)) continue;
2629
+ for (const edgeId of graph.outboundEdges(src)) {
2630
+ const edge = graph.getEdgeAttributes(edgeId);
2631
+ if (edge.type !== import_types3.EdgeType.CONNECTS_TO || edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
2632
+ if (!graph.hasNode(edge.target)) continue;
2633
+ const target = graph.getNodeAttributes(edge.target);
2634
+ if (target.type !== import_types3.NodeType.DatabaseNode || target.engine !== engine) continue;
2635
+ matches.add(edge.target);
2636
+ }
2637
+ }
2638
+ return matches.size === 1 ? [...matches][0] : null;
2639
+ }
2619
2640
  function ensureFrontierNode(graph, host, ts) {
2620
2641
  const id = frontierIdFor(host);
2621
2642
  if (graph.hasNode(id)) {
@@ -2874,13 +2895,18 @@ async function handleSpan(ctx, span) {
2874
2895
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2875
2896
  targetId = (0, import_types3.databaseId)(host);
2876
2897
  } else {
2877
- const localName = span.dbName ?? span.dbSystem;
2878
- targetId = ensureLocalDatabaseNode(
2879
- ctx.graph,
2880
- span.service,
2881
- localName,
2882
- span.dbSystem
2883
- );
2898
+ const declared = findDeclaredDatabaseForService(ctx.graph, sourceId, span.dbSystem);
2899
+ if (declared) {
2900
+ targetId = declared;
2901
+ } else {
2902
+ const localName = span.dbName ?? span.dbSystem;
2903
+ targetId = ensureLocalDatabaseNode(
2904
+ ctx.graph,
2905
+ span.service,
2906
+ localName,
2907
+ span.dbSystem
2908
+ );
2909
+ }
2884
2910
  }
2885
2911
  const result = upsertObservedEdge(
2886
2912
  ctx.graph,
@@ -3064,29 +3090,29 @@ function promoteFrontierNodes(graph, opts = {}) {
3064
3090
  toPromote.push({ frontierId: id, serviceId: target });
3065
3091
  });
3066
3092
  let promoted = 0;
3067
- for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
3093
+ for (const { frontierId: frontierId2, serviceId: serviceId6 } of toPromote) {
3068
3094
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
3069
3095
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
3070
3096
  if (!gate.allowed) {
3071
3097
  continue;
3072
3098
  }
3073
3099
  }
3074
- rewireFrontierEdges(graph, frontierId2, serviceId5);
3100
+ rewireFrontierEdges(graph, frontierId2, serviceId6);
3075
3101
  graph.dropNode(frontierId2);
3076
3102
  promoted++;
3077
3103
  }
3078
3104
  return promoted;
3079
3105
  }
3080
- function rewireFrontierEdges(graph, frontierId2, serviceId5) {
3106
+ function rewireFrontierEdges(graph, frontierId2, serviceId6) {
3081
3107
  const inbound = [...graph.inboundEdges(frontierId2)];
3082
3108
  const outbound = [...graph.outboundEdges(frontierId2)];
3083
3109
  for (const edgeId of inbound) {
3084
3110
  const edge = graph.getEdgeAttributes(edgeId);
3085
- rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
3111
+ rebuildEdge(graph, edge, edge.source, serviceId6, edgeId);
3086
3112
  }
3087
3113
  for (const edgeId of outbound) {
3088
3114
  const edge = graph.getEdgeAttributes(edgeId);
3089
- rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
3115
+ rebuildEdge(graph, edge, serviceId6, edge.target, edgeId);
3090
3116
  }
3091
3117
  }
3092
3118
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -3894,9 +3920,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
3894
3920
  "StatefulSet",
3895
3921
  "DaemonSet"
3896
3922
  ]);
3897
- function addAliases(graph, serviceId5, candidates) {
3898
- if (!graph.hasNode(serviceId5)) return;
3899
- const node = graph.getNodeAttributes(serviceId5);
3923
+ function addAliases(graph, serviceId6, candidates) {
3924
+ if (!graph.hasNode(serviceId6)) return;
3925
+ const node = graph.getNodeAttributes(serviceId6);
3900
3926
  if (node.type !== import_types6.NodeType.ServiceNode) return;
3901
3927
  const set = new Set(node.aliases ?? []);
3902
3928
  for (const c of candidates) {
@@ -3906,7 +3932,7 @@ function addAliases(graph, serviceId5, candidates) {
3906
3932
  }
3907
3933
  if (set.size === 0) return;
3908
3934
  const updated = { ...node, aliases: [...set].sort() };
3909
- graph.replaceNodeAttributes(serviceId5, updated);
3935
+ graph.replaceNodeAttributes(serviceId6, updated);
3910
3936
  }
3911
3937
  function indexServicesByName(services) {
3912
3938
  const map = /* @__PURE__ */ new Map();
@@ -3939,12 +3965,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3939
3965
  }
3940
3966
  if (!compose?.services) return;
3941
3967
  for (const [composeName, svc] of Object.entries(compose.services)) {
3942
- const serviceId5 = serviceIndex.get(composeName);
3943
- if (!serviceId5) continue;
3968
+ const serviceId6 = serviceIndex.get(composeName);
3969
+ if (!serviceId6) continue;
3944
3970
  const aliases = /* @__PURE__ */ new Set([composeName]);
3945
3971
  if (svc.container_name) aliases.add(svc.container_name);
3946
3972
  if (svc.hostname) aliases.add(svc.hostname);
3947
- addAliases(graph, serviceId5, aliases);
3973
+ addAliases(graph, serviceId6, aliases);
3948
3974
  }
3949
3975
  }
3950
3976
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -4594,6 +4620,29 @@ async function readIfExists(filePath) {
4594
4620
  return null;
4595
4621
  }
4596
4622
  }
4623
+ async function resolveEnvVar(serviceDir, name) {
4624
+ const entries = await import_node_fs13.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
4625
+ const envNames = entries.filter((e) => e.isFile() && (e.name === ".env" || e.name.startsWith(".env."))).map((e) => e.name);
4626
+ const rank = (n) => n === ".env.local" ? 0 : n === ".env" ? 1 : 2;
4627
+ envNames.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
4628
+ for (const fileName of envNames) {
4629
+ const content = await readIfExists(import_node_path15.default.join(serviceDir, fileName));
4630
+ if (!content) continue;
4631
+ for (const line of content.split("\n")) {
4632
+ const trimmed = line.trim();
4633
+ if (!trimmed || trimmed.startsWith("#")) continue;
4634
+ const eq = trimmed.indexOf("=");
4635
+ if (eq < 0) continue;
4636
+ if (trimmed.slice(0, eq).trim() !== name) continue;
4637
+ let value = trimmed.slice(eq + 1).trim();
4638
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4639
+ value = value.slice(1, -1);
4640
+ }
4641
+ return value || null;
4642
+ }
4643
+ }
4644
+ return null;
4645
+ }
4597
4646
  async function findFirst(serviceDir, candidates) {
4598
4647
  for (const rel of candidates) {
4599
4648
  const abs = import_node_path15.default.join(serviceDir, rel);
@@ -4691,6 +4740,14 @@ async function parse3(serviceDir) {
4691
4740
  const config = parseConnectionString(urlMatch[1]);
4692
4741
  if (config) return [{ ...config, sourceFile: schemaPath }];
4693
4742
  }
4743
+ const envMatch = body.match(/url\s*=\s*env\(\s*"([^"]+)"\s*\)/);
4744
+ if (envMatch) {
4745
+ const resolved = await resolveEnvVar(serviceDir, envMatch[1]);
4746
+ if (resolved) {
4747
+ const config = parseConnectionString(resolved);
4748
+ if (config) return [{ ...config, sourceFile: schemaPath }];
4749
+ }
4750
+ }
4694
4751
  return [
4695
4752
  {
4696
4753
  host: `${engine}-prisma`,
@@ -7448,14 +7505,23 @@ var import_types30 = require("@neat.is/types");
7448
7505
  function bucketKey(source, target, type) {
7449
7506
  return `${type}|${source}|${target}`;
7450
7507
  }
7508
+ function bucketSourceFor(graph, edge) {
7509
+ if (edge.type !== import_types30.EdgeType.CONNECTS_TO) return edge.source;
7510
+ const parsed = (0, import_types30.parseFileId)(edge.source);
7511
+ if (!parsed || !graph.hasNode(edge.target)) return edge.source;
7512
+ const target = graph.getNodeAttributes(edge.target);
7513
+ if (target.type !== import_types30.NodeType.DatabaseNode) return edge.source;
7514
+ return (0, import_types30.serviceId)(parsed.service);
7515
+ }
7451
7516
  function bucketEdges(graph) {
7452
7517
  const buckets2 = /* @__PURE__ */ new Map();
7453
7518
  graph.forEachEdge((id, attrs) => {
7454
7519
  const e = attrs;
7455
7520
  const parsed = (0, import_types30.parseEdgeId)(id);
7456
7521
  const provenance = parsed?.provenance ?? e.provenance;
7457
- const key = bucketKey(e.source, e.target, e.type);
7458
- const cur = buckets2.get(key) ?? { source: e.source, target: e.target, type: e.type };
7522
+ const source = bucketSourceFor(graph, e);
7523
+ const key = bucketKey(source, e.target, e.type);
7524
+ const cur = buckets2.get(key) ?? { source, target: e.target, type: e.type };
7459
7525
  switch (provenance) {
7460
7526
  case import_types30.Provenance.EXTRACTED:
7461
7527
  cur.extracted = e;