@neat.is/core 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -610,7 +610,7 @@ function getGraph(project = DEFAULT_PROJECT) {
610
610
  init_cjs_shims();
611
611
  var import_fastify = __toESM(require("fastify"), 1);
612
612
  var import_cors = __toESM(require("@fastify/cors"), 1);
613
- var import_types22 = require("@neat.is/types");
613
+ var import_types23 = require("@neat.is/types");
614
614
 
615
615
  // src/divergences.ts
616
616
  init_cjs_shims();
@@ -939,6 +939,24 @@ function isFrontierNode(graph, nodeId) {
939
939
  const attrs = graph.getNodeAttributes(nodeId);
940
940
  return attrs.type === import_types.NodeType.FrontierNode;
941
941
  }
942
+ function resolveOwningService(graph, nodeId) {
943
+ if (!graph.hasNode(nodeId)) return null;
944
+ const attrs = graph.getNodeAttributes(nodeId);
945
+ if (attrs.type === import_types.NodeType.ServiceNode) {
946
+ return { id: nodeId, svc: attrs };
947
+ }
948
+ if (attrs.type === import_types.NodeType.FileNode) {
949
+ for (const edgeId of graph.inboundEdges(nodeId)) {
950
+ const e = graph.getEdgeAttributes(edgeId);
951
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
952
+ const owner = graph.getNodeAttributes(e.source);
953
+ if (owner.type === import_types.NodeType.ServiceNode) {
954
+ return { id: e.source, svc: owner };
955
+ }
956
+ }
957
+ }
958
+ return null;
959
+ }
942
960
  function bestEdgeBySource(graph, edgeIds) {
943
961
  const best = /* @__PURE__ */ new Map();
944
962
  for (const id of edgeIds) {
@@ -1045,9 +1063,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1045
1063
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1046
1064
  if (candidatePairs.length === 0) return null;
1047
1065
  for (const id of walk.path) {
1048
- const attrs = graph.getNodeAttributes(id);
1049
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1050
- const svc = attrs;
1066
+ const owner = resolveOwningService(graph, id);
1067
+ if (!owner) continue;
1068
+ const { id: serviceId3, svc } = owner;
1051
1069
  const deps = svc.dependencies ?? {};
1052
1070
  for (const pair of candidatePairs) {
1053
1071
  const declared = deps[pair.driver];
@@ -1060,7 +1078,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1060
1078
  );
1061
1079
  if (!result.compatible) {
1062
1080
  return {
1063
- rootCauseNode: id,
1081
+ rootCauseNode: serviceId3,
1064
1082
  rootCauseReason: result.reason ?? "incompatible driver",
1065
1083
  ...result.minDriverVersion ? {
1066
1084
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1073,9 +1091,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1073
1091
  }
1074
1092
  function serviceRootCauseShape(graph, _origin, walk) {
1075
1093
  for (const id of walk.path) {
1076
- const attrs = graph.getNodeAttributes(id);
1077
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1078
- const svc = attrs;
1094
+ const owner = resolveOwningService(graph, id);
1095
+ if (!owner) continue;
1096
+ const { id: serviceId3, svc } = owner;
1079
1097
  const deps = svc.dependencies ?? {};
1080
1098
  const serviceNodeEngine = svc.nodeEngine;
1081
1099
  for (const constraint of nodeEngineConstraints()) {
@@ -1084,7 +1102,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1084
1102
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1085
1103
  if (!result.compatible && result.reason) {
1086
1104
  return {
1087
- rootCauseNode: id,
1105
+ rootCauseNode: serviceId3,
1088
1106
  rootCauseReason: result.reason,
1089
1107
  ...result.requiredNodeVersion ? {
1090
1108
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1099,7 +1117,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1099
1117
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1100
1118
  if (!result.compatible && result.reason) {
1101
1119
  return {
1102
- rootCauseNode: id,
1120
+ rootCauseNode: serviceId3,
1103
1121
  rootCauseReason: result.reason,
1104
1122
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1105
1123
  };
@@ -1108,9 +1126,15 @@ function serviceRootCauseShape(graph, _origin, walk) {
1108
1126
  }
1109
1127
  return null;
1110
1128
  }
1129
+ function fileRootCauseShape(graph, origin, walk) {
1130
+ const owner = resolveOwningService(graph, origin.id);
1131
+ if (!owner) return null;
1132
+ return serviceRootCauseShape(graph, owner.svc, walk);
1133
+ }
1111
1134
  var rootCauseShapes = {
1112
1135
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
1113
- [import_types.NodeType.ServiceNode]: serviceRootCauseShape
1136
+ [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1137
+ [import_types.NodeType.FileNode]: fileRootCauseShape
1114
1138
  };
1115
1139
  function getRootCause(graph, errorNodeId, errorEvent) {
1116
1140
  if (!graph.hasNode(errorNodeId)) return null;
@@ -1267,6 +1291,7 @@ function gradedConfidence(edge) {
1267
1291
  }
1268
1292
  function detectMissingDivergences(graph, bucket) {
1269
1293
  const out = [];
1294
+ if (bucket.type === import_types2.EdgeType.CONTAINS) return out;
1270
1295
  if (bucket.extracted && !bucket.observed) {
1271
1296
  if (!nodeIsFrontier(graph, bucket.target)) {
1272
1297
  out.push({
@@ -1857,6 +1882,86 @@ function hostFromUrl(u) {
1857
1882
  function pickAddress(span) {
1858
1883
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1859
1884
  }
1885
+ var CODE_FILEPATH_ATTR = "code.filepath";
1886
+ var CODE_LINENO_ATTR = "code.lineno";
1887
+ var CODE_FUNCTION_ATTR = "code.function";
1888
+ function toPosix(p) {
1889
+ return p.split("\\").join("/");
1890
+ }
1891
+ function languageForExt(relPath) {
1892
+ const dot = relPath.lastIndexOf(".");
1893
+ if (dot === -1) return void 0;
1894
+ switch (relPath.slice(dot).toLowerCase()) {
1895
+ case ".py":
1896
+ return "python";
1897
+ case ".ts":
1898
+ case ".tsx":
1899
+ return "typescript";
1900
+ case ".js":
1901
+ case ".jsx":
1902
+ case ".mjs":
1903
+ case ".cjs":
1904
+ return "javascript";
1905
+ default:
1906
+ return void 0;
1907
+ }
1908
+ }
1909
+ function relPathForRuntimeFile(filepath, serviceNode) {
1910
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1911
+ const root = serviceNode?.repoPath;
1912
+ if (root && root !== "." && root.length > 0) {
1913
+ const rootPosix = toPosix(root);
1914
+ const anchor = `/${rootPosix}/`;
1915
+ const idx = p.lastIndexOf(anchor);
1916
+ if (idx !== -1) return p.slice(idx + anchor.length);
1917
+ const base = rootPosix.split("/").filter(Boolean).pop();
1918
+ if (base) {
1919
+ const baseAnchor = `/${base}/`;
1920
+ const bidx = p.lastIndexOf(baseAnchor);
1921
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1922
+ }
1923
+ }
1924
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1925
+ return p.length > 0 ? p : null;
1926
+ }
1927
+ function callSiteFromSpan(span, serviceNode) {
1928
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1929
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1930
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1931
+ if (!relPath) return null;
1932
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1933
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1934
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1935
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1936
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1937
+ }
1938
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1939
+ const fileNodeId = (0, import_types4.fileId)(serviceName, callSite.relPath);
1940
+ if (!graph.hasNode(fileNodeId)) {
1941
+ const language = languageForExt(callSite.relPath);
1942
+ const node = {
1943
+ id: fileNodeId,
1944
+ type: import_types4.NodeType.FileNode,
1945
+ service: serviceName,
1946
+ path: callSite.relPath,
1947
+ ...language ? { language } : {},
1948
+ discoveredVia: "otel"
1949
+ };
1950
+ graph.addNode(fileNodeId, node);
1951
+ }
1952
+ const containsId = makeObservedEdgeId(import_types4.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1953
+ if (!graph.hasEdge(containsId)) {
1954
+ const edge = {
1955
+ id: containsId,
1956
+ source: serviceNodeId,
1957
+ target: fileNodeId,
1958
+ type: import_types4.EdgeType.CONTAINS,
1959
+ provenance: import_types4.Provenance.OBSERVED
1960
+ };
1961
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1962
+ }
1963
+ return fileNodeId;
1964
+ }
1860
1965
  function makeObservedEdgeId(type, source, target) {
1861
1966
  return (0, import_types4.observedEdgeId)(source, target, type);
1862
1967
  }
@@ -2074,6 +2179,9 @@ async function handleSpan(ctx, span) {
2074
2179
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
2075
2180
  const isError = span.statusCode === 2;
2076
2181
  cacheSpanService(span, nowMs);
2182
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2183
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
2184
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2077
2185
  let affectedNode = sourceId;
2078
2186
  if (span.dbSystem) {
2079
2187
  const host = pickAddress(span);
@@ -2083,7 +2191,7 @@ async function handleSpan(ctx, span) {
2083
2191
  const result = upsertObservedEdge(
2084
2192
  ctx.graph,
2085
2193
  import_types4.EdgeType.CONNECTS_TO,
2086
- sourceId,
2194
+ observedSource(),
2087
2195
  targetId,
2088
2196
  ts,
2089
2197
  isError
@@ -2099,7 +2207,7 @@ async function handleSpan(ctx, span) {
2099
2207
  upsertObservedEdge(
2100
2208
  ctx.graph,
2101
2209
  import_types4.EdgeType.CALLS,
2102
- sourceId,
2210
+ observedSource(),
2103
2211
  targetId,
2104
2212
  ts,
2105
2213
  isError
@@ -2111,7 +2219,7 @@ async function handleSpan(ctx, span) {
2111
2219
  upsertObservedEdge(
2112
2220
  ctx.graph,
2113
2221
  import_types4.EdgeType.CALLS,
2114
- sourceId,
2222
+ observedSource(),
2115
2223
  frontierNodeId,
2116
2224
  ts,
2117
2225
  isError
@@ -3742,7 +3850,7 @@ async function addConfigNodes(graph, services, scanPath) {
3742
3850
 
3743
3851
  // src/extract/calls/index.ts
3744
3852
  init_cjs_shims();
3745
- var import_types15 = require("@neat.is/types");
3853
+ var import_types16 = require("@neat.is/types");
3746
3854
 
3747
3855
  // src/extract/calls/http.ts
3748
3856
  init_cjs_shims();
@@ -3750,12 +3858,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3750
3858
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3751
3859
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3752
3860
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3753
- var import_types10 = require("@neat.is/types");
3861
+ var import_types11 = require("@neat.is/types");
3754
3862
 
3755
3863
  // src/extract/calls/shared.ts
3756
3864
  init_cjs_shims();
3757
3865
  var import_node_fs13 = require("fs");
3758
3866
  var import_node_path19 = __toESM(require("path"), 1);
3867
+ var import_types10 = require("@neat.is/types");
3759
3868
  async function walkSourceFiles(dir) {
3760
3869
  const out = [];
3761
3870
  async function walk(current) {
@@ -3795,6 +3904,58 @@ function snippet(text, line) {
3795
3904
  const lines = text.split("\n");
3796
3905
  return (lines[line - 1] ?? "").trim();
3797
3906
  }
3907
+ function toPosix2(p) {
3908
+ return p.split("\\").join("/");
3909
+ }
3910
+ function languageForPath(relPath) {
3911
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3912
+ case ".py":
3913
+ return "python";
3914
+ case ".ts":
3915
+ case ".tsx":
3916
+ return "typescript";
3917
+ case ".js":
3918
+ case ".jsx":
3919
+ case ".mjs":
3920
+ case ".cjs":
3921
+ return "javascript";
3922
+ default:
3923
+ return void 0;
3924
+ }
3925
+ }
3926
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3927
+ let nodesAdded = 0;
3928
+ let edgesAdded = 0;
3929
+ const fileNodeId = (0, import_types10.fileId)(serviceName, relPath);
3930
+ if (!graph.hasNode(fileNodeId)) {
3931
+ const language = languageForPath(relPath);
3932
+ const node = {
3933
+ id: fileNodeId,
3934
+ type: import_types10.NodeType.FileNode,
3935
+ service: serviceName,
3936
+ path: relPath,
3937
+ ...language ? { language } : {},
3938
+ discoveredVia: "static"
3939
+ };
3940
+ graph.addNode(fileNodeId, node);
3941
+ nodesAdded++;
3942
+ }
3943
+ const containsId = (0, import_types10.extractedEdgeId)(serviceNodeId, fileNodeId, import_types10.EdgeType.CONTAINS);
3944
+ if (!graph.hasEdge(containsId)) {
3945
+ const edge = {
3946
+ id: containsId,
3947
+ source: serviceNodeId,
3948
+ target: fileNodeId,
3949
+ type: import_types10.EdgeType.CONTAINS,
3950
+ provenance: import_types10.Provenance.EXTRACTED,
3951
+ confidence: (0, import_types10.confidenceForExtracted)("structural"),
3952
+ evidence: { file: relPath }
3953
+ };
3954
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3955
+ edgesAdded++;
3956
+ }
3957
+ return { fileNodeId, nodesAdded, edgesAdded };
3958
+ }
3798
3959
 
3799
3960
  // src/extract/calls/http.ts
3800
3961
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3828,16 +3989,16 @@ function callsFromSource(source, parser, knownHosts) {
3828
3989
  const tree = parser.parse(source);
3829
3990
  const literals = [];
3830
3991
  collectStringLiterals(tree.rootNode, literals);
3831
- const targets = /* @__PURE__ */ new Set();
3992
+ const out = [];
3832
3993
  for (const lit of literals) {
3833
3994
  if (isInsideJsxExternalLink(lit.node)) continue;
3834
3995
  for (const host of knownHosts) {
3835
3996
  if (urlMatchesHost(lit.text, host)) {
3836
- targets.add(host);
3997
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3837
3998
  }
3838
3999
  }
3839
4000
  }
3840
- return targets;
4001
+ return out;
3841
4002
  }
3842
4003
  function makeJsParser() {
3843
4004
  const p = new import_tree_sitter.default();
@@ -3860,71 +4021,78 @@ async function addHttpCallEdges(graph, services) {
3860
4021
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3861
4022
  hostToNodeId.set(service.pkg.name, service.node.id);
3862
4023
  }
4024
+ let nodesAdded = 0;
3863
4025
  let edgesAdded = 0;
3864
4026
  for (const service of services) {
3865
4027
  const files = await loadSourceFiles(service.dir);
3866
- const seenTargets = /* @__PURE__ */ new Map();
4028
+ const seen = /* @__PURE__ */ new Set();
3867
4029
  for (const file of files) {
3868
4030
  if (isTestPath(file.path)) continue;
3869
4031
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3870
- let targets;
4032
+ let sites;
3871
4033
  try {
3872
- targets = callsFromSource(file.content, parser, knownHosts);
4034
+ sites = callsFromSource(file.content, parser, knownHosts);
3873
4035
  } catch (err) {
3874
4036
  recordExtractionError("http call extraction", file.path, err);
3875
4037
  continue;
3876
4038
  }
3877
- for (const t of targets) {
3878
- const targetId = hostToNodeId.get(t);
4039
+ if (sites.length === 0) continue;
4040
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
4041
+ for (const site of sites) {
4042
+ const targetId = hostToNodeId.get(site.host);
3879
4043
  if (!targetId || targetId === service.node.id) continue;
3880
- if (!seenTargets.has(targetId)) {
3881
- seenTargets.set(targetId, { file: file.path, host: t });
4044
+ const dedupKey = `${relFile}|${targetId}`;
4045
+ if (seen.has(dedupKey)) continue;
4046
+ seen.add(dedupKey);
4047
+ const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
4048
+ const ev = {
4049
+ file: relFile,
4050
+ line: site.line,
4051
+ snippet: snippet(file.content, site.line)
4052
+ };
4053
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4054
+ graph,
4055
+ service.pkg.name,
4056
+ service.node.id,
4057
+ relFile
4058
+ );
4059
+ nodesAdded += n;
4060
+ edgesAdded += e;
4061
+ if (!(0, import_types11.passesExtractedFloor)(confidence)) {
4062
+ noteExtractedDropped({
4063
+ source: fileNodeId,
4064
+ target: targetId,
4065
+ type: import_types11.EdgeType.CALLS,
4066
+ confidence,
4067
+ confidenceKind: "hostname-shape-match",
4068
+ evidence: ev
4069
+ });
4070
+ continue;
4071
+ }
4072
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, targetId, import_types11.EdgeType.CALLS);
4073
+ if (!graph.hasEdge(edgeId)) {
4074
+ const edge = {
4075
+ id: edgeId,
4076
+ source: fileNodeId,
4077
+ target: targetId,
4078
+ type: import_types11.EdgeType.CALLS,
4079
+ provenance: import_types11.Provenance.EXTRACTED,
4080
+ confidence,
4081
+ evidence: ev
4082
+ };
4083
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
4084
+ edgesAdded++;
3882
4085
  }
3883
- }
3884
- }
3885
- for (const [targetId, evidenceFile] of seenTargets) {
3886
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3887
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3888
- const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3889
- const ev = {
3890
- file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3891
- line,
3892
- snippet: snippet(fileContent, line)
3893
- };
3894
- const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, targetId, import_types10.EdgeType.CALLS);
3895
- if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3896
- noteExtractedDropped({
3897
- source: service.node.id,
3898
- target: targetId,
3899
- type: import_types10.EdgeType.CALLS,
3900
- confidence,
3901
- confidenceKind: "hostname-shape-match",
3902
- evidence: ev
3903
- });
3904
- continue;
3905
- }
3906
- const edge = {
3907
- id: edgeId,
3908
- source: service.node.id,
3909
- target: targetId,
3910
- type: import_types10.EdgeType.CALLS,
3911
- provenance: import_types10.Provenance.EXTRACTED,
3912
- confidence,
3913
- evidence: ev
3914
- };
3915
- if (!graph.hasEdge(edge.id)) {
3916
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3917
- edgesAdded++;
3918
4086
  }
3919
4087
  }
3920
4088
  }
3921
- return edgesAdded;
4089
+ return { nodesAdded, edgesAdded };
3922
4090
  }
3923
4091
 
3924
4092
  // src/extract/calls/kafka.ts
3925
4093
  init_cjs_shims();
3926
4094
  var import_node_path21 = __toESM(require("path"), 1);
3927
- var import_types11 = require("@neat.is/types");
4095
+ var import_types12 = require("@neat.is/types");
3928
4096
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3929
4097
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3930
4098
  function findAll(re, text) {
@@ -3945,7 +4113,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3945
4113
  seen.add(key);
3946
4114
  const line = lineOf(file.content, topic);
3947
4115
  out.push({
3948
- infraId: (0, import_types11.infraId)("kafka-topic", topic),
4116
+ infraId: (0, import_types12.infraId)("kafka-topic", topic),
3949
4117
  name: topic,
3950
4118
  kind: "kafka-topic",
3951
4119
  edgeType,
@@ -3968,7 +4136,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3968
4136
  // src/extract/calls/redis.ts
3969
4137
  init_cjs_shims();
3970
4138
  var import_node_path22 = __toESM(require("path"), 1);
3971
- var import_types12 = require("@neat.is/types");
4139
+ var import_types13 = require("@neat.is/types");
3972
4140
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3973
4141
  function redisEndpointsFromFile(file, serviceDir) {
3974
4142
  const out = [];
@@ -3981,7 +4149,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3981
4149
  seen.add(host);
3982
4150
  const line = lineOf(file.content, host);
3983
4151
  out.push({
3984
- infraId: (0, import_types12.infraId)("redis", host),
4152
+ infraId: (0, import_types13.infraId)("redis", host),
3985
4153
  name: host,
3986
4154
  kind: "redis",
3987
4155
  edgeType: "CALLS",
@@ -4002,7 +4170,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4002
4170
  // src/extract/calls/aws.ts
4003
4171
  init_cjs_shims();
4004
4172
  var import_node_path23 = __toESM(require("path"), 1);
4005
- var import_types13 = require("@neat.is/types");
4173
+ var import_types14 = require("@neat.is/types");
4006
4174
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
4007
4175
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
4008
4176
  function hasMarker(text, markers) {
@@ -4026,7 +4194,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4026
4194
  seen.add(key);
4027
4195
  const line = lineOf(file.content, name);
4028
4196
  out.push({
4029
- infraId: (0, import_types13.infraId)(kind, name),
4197
+ infraId: (0, import_types14.infraId)(kind, name),
4030
4198
  name,
4031
4199
  kind,
4032
4200
  edgeType: "CALLS",
@@ -4061,7 +4229,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4061
4229
  // src/extract/calls/grpc.ts
4062
4230
  init_cjs_shims();
4063
4231
  var import_node_path24 = __toESM(require("path"), 1);
4064
- var import_types14 = require("@neat.is/types");
4232
+ var import_types15 = require("@neat.is/types");
4065
4233
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
4066
4234
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
4067
4235
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -4109,7 +4277,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4109
4277
  const { kind } = classified;
4110
4278
  const line = lineOf(file.content, m[0]);
4111
4279
  out.push({
4112
- infraId: (0, import_types14.infraId)(kind, name),
4280
+ infraId: (0, import_types15.infraId)(kind, name),
4113
4281
  name,
4114
4282
  kind,
4115
4283
  edgeType: "CALLS",
@@ -4131,11 +4299,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
4131
4299
  function edgeTypeFromEndpoint(ep) {
4132
4300
  switch (ep.edgeType) {
4133
4301
  case "PUBLISHES_TO":
4134
- return import_types15.EdgeType.PUBLISHES_TO;
4302
+ return import_types16.EdgeType.PUBLISHES_TO;
4135
4303
  case "CONSUMES_FROM":
4136
- return import_types15.EdgeType.CONSUMES_FROM;
4304
+ return import_types16.EdgeType.CONSUMES_FROM;
4137
4305
  default:
4138
- return import_types15.EdgeType.CALLS;
4306
+ return import_types16.EdgeType.CALLS;
4139
4307
  }
4140
4308
  }
4141
4309
  function isAwsKind(kind) {
@@ -4162,7 +4330,7 @@ async function addExternalEndpointEdges(graph, services) {
4162
4330
  if (!graph.hasNode(ep.infraId)) {
4163
4331
  const node = {
4164
4332
  id: ep.infraId,
4165
- type: import_types15.NodeType.InfraNode,
4333
+ type: import_types16.NodeType.InfraNode,
4166
4334
  name: ep.name,
4167
4335
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4168
4336
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4174,13 +4342,19 @@ async function addExternalEndpointEdges(graph, services) {
4174
4342
  nodesAdded++;
4175
4343
  }
4176
4344
  const edgeType = edgeTypeFromEndpoint(ep);
4177
- const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
4178
- if (seenEdges.has(edgeId)) continue;
4179
- seenEdges.add(edgeId);
4180
- const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4181
- if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4345
+ const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4346
+ const relFile = toPosix2(ep.evidence.file);
4347
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4348
+ graph,
4349
+ service.pkg.name,
4350
+ service.node.id,
4351
+ relFile
4352
+ );
4353
+ nodesAdded += n;
4354
+ edgesAdded += e;
4355
+ if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4182
4356
  noteExtractedDropped({
4183
- source: service.node.id,
4357
+ source: fileNodeId,
4184
4358
  target: ep.infraId,
4185
4359
  type: edgeType,
4186
4360
  confidence,
@@ -4189,13 +4363,16 @@ async function addExternalEndpointEdges(graph, services) {
4189
4363
  });
4190
4364
  continue;
4191
4365
  }
4366
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4367
+ if (seenEdges.has(edgeId)) continue;
4368
+ seenEdges.add(edgeId);
4192
4369
  if (!graph.hasEdge(edgeId)) {
4193
4370
  const edge = {
4194
4371
  id: edgeId,
4195
- source: service.node.id,
4372
+ source: fileNodeId,
4196
4373
  target: ep.infraId,
4197
4374
  type: edgeType,
4198
- provenance: import_types15.Provenance.EXTRACTED,
4375
+ provenance: import_types16.Provenance.EXTRACTED,
4199
4376
  confidence,
4200
4377
  evidence: ep.evidence
4201
4378
  };
@@ -4207,11 +4384,11 @@ async function addExternalEndpointEdges(graph, services) {
4207
4384
  return { nodesAdded, edgesAdded };
4208
4385
  }
4209
4386
  async function addCallEdges(graph, services) {
4210
- const httpEdges = await addHttpCallEdges(graph, services);
4387
+ const http = await addHttpCallEdges(graph, services);
4211
4388
  const ext = await addExternalEndpointEdges(graph, services);
4212
4389
  return {
4213
- nodesAdded: ext.nodesAdded,
4214
- edgesAdded: httpEdges + ext.edgesAdded
4390
+ nodesAdded: http.nodesAdded + ext.nodesAdded,
4391
+ edgesAdded: http.edgesAdded + ext.edgesAdded
4215
4392
  };
4216
4393
  }
4217
4394
 
@@ -4221,15 +4398,15 @@ init_cjs_shims();
4221
4398
  // src/extract/infra/docker-compose.ts
4222
4399
  init_cjs_shims();
4223
4400
  var import_node_path25 = __toESM(require("path"), 1);
4224
- var import_types17 = require("@neat.is/types");
4401
+ var import_types18 = require("@neat.is/types");
4225
4402
 
4226
4403
  // src/extract/infra/shared.ts
4227
4404
  init_cjs_shims();
4228
- var import_types16 = require("@neat.is/types");
4405
+ var import_types17 = require("@neat.is/types");
4229
4406
  function makeInfraNode(kind, name, provider = "self", extras) {
4230
4407
  return {
4231
- id: (0, import_types16.infraId)(kind, name),
4232
- type: import_types16.NodeType.InfraNode,
4408
+ id: (0, import_types17.infraId)(kind, name),
4409
+ type: import_types17.NodeType.InfraNode,
4233
4410
  name,
4234
4411
  provider,
4235
4412
  kind,
@@ -4308,15 +4485,15 @@ async function addComposeInfra(graph, scanPath, services) {
4308
4485
  for (const dep of dependsOnList(svc.depends_on)) {
4309
4486
  const targetId = composeNameToNodeId.get(dep);
4310
4487
  if (!targetId) continue;
4311
- const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4488
+ const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4312
4489
  if (graph.hasEdge(edgeId)) continue;
4313
4490
  const edge = {
4314
4491
  id: edgeId,
4315
4492
  source: sourceId,
4316
4493
  target: targetId,
4317
- type: import_types17.EdgeType.DEPENDS_ON,
4318
- provenance: import_types17.Provenance.EXTRACTED,
4319
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4494
+ type: import_types18.EdgeType.DEPENDS_ON,
4495
+ provenance: import_types18.Provenance.EXTRACTED,
4496
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4320
4497
  evidence: { file: evidenceFile }
4321
4498
  };
4322
4499
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4330,7 +4507,7 @@ async function addComposeInfra(graph, scanPath, services) {
4330
4507
  init_cjs_shims();
4331
4508
  var import_node_path26 = __toESM(require("path"), 1);
4332
4509
  var import_node_fs14 = require("fs");
4333
- var import_types18 = require("@neat.is/types");
4510
+ var import_types19 = require("@neat.is/types");
4334
4511
  function runtimeImage(content) {
4335
4512
  const lines = content.split("\n");
4336
4513
  let last = null;
@@ -4369,15 +4546,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4369
4546
  graph.addNode(node.id, node);
4370
4547
  nodesAdded++;
4371
4548
  }
4372
- const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4549
+ const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types19.EdgeType.RUNS_ON);
4373
4550
  if (!graph.hasEdge(edgeId)) {
4374
4551
  const edge = {
4375
4552
  id: edgeId,
4376
4553
  source: service.node.id,
4377
4554
  target: node.id,
4378
- type: import_types18.EdgeType.RUNS_ON,
4379
- provenance: import_types18.Provenance.EXTRACTED,
4380
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4555
+ type: import_types19.EdgeType.RUNS_ON,
4556
+ provenance: import_types19.Provenance.EXTRACTED,
4557
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4381
4558
  evidence: {
4382
4559
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4383
4560
  }
@@ -4505,13 +4682,24 @@ var import_node_path30 = __toESM(require("path"), 1);
4505
4682
  init_cjs_shims();
4506
4683
  var import_node_fs17 = require("fs");
4507
4684
  var import_node_path29 = __toESM(require("path"), 1);
4508
- var import_types19 = require("@neat.is/types");
4685
+ var import_types20 = require("@neat.is/types");
4686
+ function dropOrphanedFileNodes(graph) {
4687
+ const orphans = [];
4688
+ graph.forEachNode((id, attrs) => {
4689
+ if (attrs.type !== import_types20.NodeType.FileNode) return;
4690
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4691
+ orphans.push(id);
4692
+ }
4693
+ });
4694
+ for (const id of orphans) graph.dropNode(id);
4695
+ return orphans.length;
4696
+ }
4509
4697
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4510
4698
  const toDrop = [];
4511
4699
  const bases = [scanPath, ...serviceDirs];
4512
4700
  graph.forEachEdge((id, attrs) => {
4513
4701
  const edge = attrs;
4514
- if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4702
+ if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
4515
4703
  const evidenceFile = edge.evidence?.file;
4516
4704
  if (!evidenceFile) return;
4517
4705
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4522,6 +4710,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4522
4710
  if (!found) toDrop.push(id);
4523
4711
  });
4524
4712
  for (const id of toDrop) graph.dropEdge(id);
4713
+ dropOrphanedFileNodes(graph);
4525
4714
  return toDrop.length;
4526
4715
  }
4527
4716
 
@@ -4668,7 +4857,7 @@ function canonicalJson(value) {
4668
4857
  init_cjs_shims();
4669
4858
  var import_node_fs19 = require("fs");
4670
4859
  var import_node_path31 = __toESM(require("path"), 1);
4671
- var import_types20 = require("@neat.is/types");
4860
+ var import_types21 = require("@neat.is/types");
4672
4861
  var SCHEMA_VERSION = 4;
4673
4862
  function migrateV1ToV2(payload) {
4674
4863
  const nodes = payload.graph.nodes;
@@ -4690,12 +4879,12 @@ function migrateV2ToV3(payload) {
4690
4879
  for (const edge of edges) {
4691
4880
  const attrs = edge.attributes;
4692
4881
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4693
- attrs.provenance = import_types20.Provenance.OBSERVED;
4882
+ attrs.provenance = import_types21.Provenance.OBSERVED;
4694
4883
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4695
4884
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4696
4885
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4697
4886
  if (type && source && target) {
4698
- const newId = (0, import_types20.observedEdgeId)(source, target, type);
4887
+ const newId = (0, import_types21.observedEdgeId)(source, target, type);
4699
4888
  attrs.id = newId;
4700
4889
  if (edge.key) edge.key = newId;
4701
4890
  }
@@ -4838,7 +5027,7 @@ init_cjs_shims();
4838
5027
  var import_node_fs20 = require("fs");
4839
5028
  var import_node_os2 = __toESM(require("os"), 1);
4840
5029
  var import_node_path33 = __toESM(require("path"), 1);
4841
- var import_types21 = require("@neat.is/types");
5030
+ var import_types22 = require("@neat.is/types");
4842
5031
  function neatHome() {
4843
5032
  const override = process.env.NEAT_HOME;
4844
5033
  if (override && override.length > 0) return import_node_path33.default.resolve(override);
@@ -4859,7 +5048,7 @@ async function readRegistry() {
4859
5048
  throw err;
4860
5049
  }
4861
5050
  const parsed = JSON.parse(raw);
4862
- return import_types21.RegistryFileSchema.parse(parsed);
5051
+ return import_types22.RegistryFileSchema.parse(parsed);
4863
5052
  }
4864
5053
  async function getProject(name) {
4865
5054
  const reg = await readRegistry();
@@ -5061,11 +5250,11 @@ function registerRoutes(scope, ctx) {
5061
5250
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5062
5251
  const parsed = [];
5063
5252
  for (const c of candidates) {
5064
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5253
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
5065
5254
  if (!r.success) {
5066
5255
  return reply.code(400).send({
5067
5256
  error: `unknown divergence type "${c}"`,
5068
- allowed: import_types22.DivergenceTypeSchema.options
5257
+ allowed: import_types23.DivergenceTypeSchema.options
5069
5258
  });
5070
5259
  }
5071
5260
  parsed.push(r.data);
@@ -5278,7 +5467,7 @@ function registerRoutes(scope, ctx) {
5278
5467
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5279
5468
  let violations = await log.readAll();
5280
5469
  if (req.query.severity) {
5281
- const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5470
+ const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5282
5471
  if (!sev.success) {
5283
5472
  return reply.code(400).send({
5284
5473
  error: "invalid severity",
@@ -5295,7 +5484,7 @@ function registerRoutes(scope, ctx) {
5295
5484
  scope.post("/policies/check", async (req, reply) => {
5296
5485
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5297
5486
  if (!proj) return;
5298
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5487
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5299
5488
  if (!parsed.success) {
5300
5489
  return reply.code(400).send({
5301
5490
  error: "invalid /policies/check body",
@@ -5332,14 +5521,18 @@ function registerRoutes(scope, ctx) {
5332
5521
  async function buildApi(opts) {
5333
5522
  const app = (0, import_fastify.default)({ logger: false });
5334
5523
  await app.register(import_cors.default, { origin: true });
5524
+ const env = readAuthEnv();
5525
+ const authToken = opts.authToken ?? env.authToken;
5526
+ const trustProxy = opts.trustProxy ?? env.trustProxy;
5527
+ const publicRead = opts.publicRead ?? env.publicRead;
5335
5528
  mountBearerAuth(app, {
5336
- token: opts.authToken,
5337
- trustProxy: opts.trustProxy,
5338
- publicRead: opts.publicRead
5529
+ token: authToken,
5530
+ trustProxy,
5531
+ publicRead
5339
5532
  });
5340
5533
  app.get("/api/config", async () => ({
5341
- publicRead: opts.publicRead === true,
5342
- authProxy: opts.trustProxy === true
5534
+ publicRead: publicRead === true,
5535
+ authProxy: trustProxy === true
5343
5536
  }));
5344
5537
  const startedAt = opts.startedAt ?? Date.now();
5345
5538
  const registry = buildLegacyRegistry(opts);