@neat.is/core 0.4.6 → 0.4.7

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.cjs CHANGED
@@ -1673,6 +1673,86 @@ function hostFromUrl(u) {
1673
1673
  function pickAddress(span) {
1674
1674
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1675
1675
  }
1676
+ var CODE_FILEPATH_ATTR = "code.filepath";
1677
+ var CODE_LINENO_ATTR = "code.lineno";
1678
+ var CODE_FUNCTION_ATTR = "code.function";
1679
+ function toPosix(p) {
1680
+ return p.split("\\").join("/");
1681
+ }
1682
+ function languageForExt(relPath) {
1683
+ const dot = relPath.lastIndexOf(".");
1684
+ if (dot === -1) return void 0;
1685
+ switch (relPath.slice(dot).toLowerCase()) {
1686
+ case ".py":
1687
+ return "python";
1688
+ case ".ts":
1689
+ case ".tsx":
1690
+ return "typescript";
1691
+ case ".js":
1692
+ case ".jsx":
1693
+ case ".mjs":
1694
+ case ".cjs":
1695
+ return "javascript";
1696
+ default:
1697
+ return void 0;
1698
+ }
1699
+ }
1700
+ function relPathForRuntimeFile(filepath, serviceNode) {
1701
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1702
+ const root = serviceNode?.repoPath;
1703
+ if (root && root !== "." && root.length > 0) {
1704
+ const rootPosix = toPosix(root);
1705
+ const anchor = `/${rootPosix}/`;
1706
+ const idx = p.lastIndexOf(anchor);
1707
+ if (idx !== -1) return p.slice(idx + anchor.length);
1708
+ const base = rootPosix.split("/").filter(Boolean).pop();
1709
+ if (base) {
1710
+ const baseAnchor = `/${base}/`;
1711
+ const bidx = p.lastIndexOf(baseAnchor);
1712
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1713
+ }
1714
+ }
1715
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1716
+ return p.length > 0 ? p : null;
1717
+ }
1718
+ function callSiteFromSpan(span, serviceNode) {
1719
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1720
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1721
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1722
+ if (!relPath) return null;
1723
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1724
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1725
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1726
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1727
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1728
+ }
1729
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1730
+ const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
1731
+ if (!graph.hasNode(fileNodeId)) {
1732
+ const language = languageForExt(callSite.relPath);
1733
+ const node = {
1734
+ id: fileNodeId,
1735
+ type: import_types3.NodeType.FileNode,
1736
+ service: serviceName,
1737
+ path: callSite.relPath,
1738
+ ...language ? { language } : {},
1739
+ discoveredVia: "otel"
1740
+ };
1741
+ graph.addNode(fileNodeId, node);
1742
+ }
1743
+ const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1744
+ if (!graph.hasEdge(containsId)) {
1745
+ const edge = {
1746
+ id: containsId,
1747
+ source: serviceNodeId,
1748
+ target: fileNodeId,
1749
+ type: import_types3.EdgeType.CONTAINS,
1750
+ provenance: import_types3.Provenance.OBSERVED
1751
+ };
1752
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1753
+ }
1754
+ return fileNodeId;
1755
+ }
1676
1756
  function makeObservedEdgeId(type, source, target) {
1677
1757
  return (0, import_types3.observedEdgeId)(source, target, type);
1678
1758
  }
@@ -1915,6 +1995,9 @@ async function handleSpan(ctx, span) {
1915
1995
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1916
1996
  const isError = span.statusCode === 2;
1917
1997
  cacheSpanService(span, nowMs);
1998
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1999
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
2000
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1918
2001
  let affectedNode = sourceId;
1919
2002
  if (span.dbSystem) {
1920
2003
  const host = pickAddress(span);
@@ -1924,7 +2007,7 @@ async function handleSpan(ctx, span) {
1924
2007
  const result = upsertObservedEdge(
1925
2008
  ctx.graph,
1926
2009
  import_types3.EdgeType.CONNECTS_TO,
1927
- sourceId,
2010
+ observedSource(),
1928
2011
  targetId,
1929
2012
  ts,
1930
2013
  isError
@@ -1940,7 +2023,7 @@ async function handleSpan(ctx, span) {
1940
2023
  upsertObservedEdge(
1941
2024
  ctx.graph,
1942
2025
  import_types3.EdgeType.CALLS,
1943
- sourceId,
2026
+ observedSource(),
1944
2027
  targetId,
1945
2028
  ts,
1946
2029
  isError
@@ -1952,7 +2035,7 @@ async function handleSpan(ctx, span) {
1952
2035
  upsertObservedEdge(
1953
2036
  ctx.graph,
1954
2037
  import_types3.EdgeType.CALLS,
1955
- sourceId,
2038
+ observedSource(),
1956
2039
  frontierNodeId,
1957
2040
  ts,
1958
2041
  isError
@@ -3583,7 +3666,7 @@ async function addConfigNodes(graph, services, scanPath) {
3583
3666
 
3584
3667
  // src/extract/calls/index.ts
3585
3668
  init_cjs_shims();
3586
- var import_types14 = require("@neat.is/types");
3669
+ var import_types15 = require("@neat.is/types");
3587
3670
 
3588
3671
  // src/extract/calls/http.ts
3589
3672
  init_cjs_shims();
@@ -3591,12 +3674,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3591
3674
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3592
3675
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3593
3676
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3594
- var import_types9 = require("@neat.is/types");
3677
+ var import_types10 = require("@neat.is/types");
3595
3678
 
3596
3679
  // src/extract/calls/shared.ts
3597
3680
  init_cjs_shims();
3598
3681
  var import_node_fs13 = require("fs");
3599
3682
  var import_node_path19 = __toESM(require("path"), 1);
3683
+ var import_types9 = require("@neat.is/types");
3600
3684
  async function walkSourceFiles(dir) {
3601
3685
  const out = [];
3602
3686
  async function walk(current) {
@@ -3636,6 +3720,58 @@ function snippet(text, line) {
3636
3720
  const lines = text.split("\n");
3637
3721
  return (lines[line - 1] ?? "").trim();
3638
3722
  }
3723
+ function toPosix2(p) {
3724
+ return p.split("\\").join("/");
3725
+ }
3726
+ function languageForPath(relPath) {
3727
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3728
+ case ".py":
3729
+ return "python";
3730
+ case ".ts":
3731
+ case ".tsx":
3732
+ return "typescript";
3733
+ case ".js":
3734
+ case ".jsx":
3735
+ case ".mjs":
3736
+ case ".cjs":
3737
+ return "javascript";
3738
+ default:
3739
+ return void 0;
3740
+ }
3741
+ }
3742
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3743
+ let nodesAdded = 0;
3744
+ let edgesAdded = 0;
3745
+ const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3746
+ if (!graph.hasNode(fileNodeId)) {
3747
+ const language = languageForPath(relPath);
3748
+ const node = {
3749
+ id: fileNodeId,
3750
+ type: import_types9.NodeType.FileNode,
3751
+ service: serviceName,
3752
+ path: relPath,
3753
+ ...language ? { language } : {},
3754
+ discoveredVia: "static"
3755
+ };
3756
+ graph.addNode(fileNodeId, node);
3757
+ nodesAdded++;
3758
+ }
3759
+ const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3760
+ if (!graph.hasEdge(containsId)) {
3761
+ const edge = {
3762
+ id: containsId,
3763
+ source: serviceNodeId,
3764
+ target: fileNodeId,
3765
+ type: import_types9.EdgeType.CONTAINS,
3766
+ provenance: import_types9.Provenance.EXTRACTED,
3767
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3768
+ evidence: { file: relPath }
3769
+ };
3770
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3771
+ edgesAdded++;
3772
+ }
3773
+ return { fileNodeId, nodesAdded, edgesAdded };
3774
+ }
3639
3775
 
3640
3776
  // src/extract/calls/http.ts
3641
3777
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3669,16 +3805,16 @@ function callsFromSource(source, parser, knownHosts) {
3669
3805
  const tree = parser.parse(source);
3670
3806
  const literals = [];
3671
3807
  collectStringLiterals(tree.rootNode, literals);
3672
- const targets = /* @__PURE__ */ new Set();
3808
+ const out = [];
3673
3809
  for (const lit of literals) {
3674
3810
  if (isInsideJsxExternalLink(lit.node)) continue;
3675
3811
  for (const host of knownHosts) {
3676
3812
  if (urlMatchesHost(lit.text, host)) {
3677
- targets.add(host);
3813
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3678
3814
  }
3679
3815
  }
3680
3816
  }
3681
- return targets;
3817
+ return out;
3682
3818
  }
3683
3819
  function makeJsParser() {
3684
3820
  const p = new import_tree_sitter.default();
@@ -3701,71 +3837,78 @@ async function addHttpCallEdges(graph, services) {
3701
3837
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3702
3838
  hostToNodeId.set(service.pkg.name, service.node.id);
3703
3839
  }
3840
+ let nodesAdded = 0;
3704
3841
  let edgesAdded = 0;
3705
3842
  for (const service of services) {
3706
3843
  const files = await loadSourceFiles(service.dir);
3707
- const seenTargets = /* @__PURE__ */ new Map();
3844
+ const seen = /* @__PURE__ */ new Set();
3708
3845
  for (const file of files) {
3709
3846
  if (isTestPath(file.path)) continue;
3710
3847
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3711
- let targets;
3848
+ let sites;
3712
3849
  try {
3713
- targets = callsFromSource(file.content, parser, knownHosts);
3850
+ sites = callsFromSource(file.content, parser, knownHosts);
3714
3851
  } catch (err) {
3715
3852
  recordExtractionError("http call extraction", file.path, err);
3716
3853
  continue;
3717
3854
  }
3718
- for (const t of targets) {
3719
- const targetId = hostToNodeId.get(t);
3855
+ if (sites.length === 0) continue;
3856
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
3857
+ for (const site of sites) {
3858
+ const targetId = hostToNodeId.get(site.host);
3720
3859
  if (!targetId || targetId === service.node.id) continue;
3721
- if (!seenTargets.has(targetId)) {
3722
- seenTargets.set(targetId, { file: file.path, host: t });
3860
+ const dedupKey = `${relFile}|${targetId}`;
3861
+ if (seen.has(dedupKey)) continue;
3862
+ seen.add(dedupKey);
3863
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3864
+ const ev = {
3865
+ file: relFile,
3866
+ line: site.line,
3867
+ snippet: snippet(file.content, site.line)
3868
+ };
3869
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3870
+ noteExtractedDropped({
3871
+ source: service.node.id,
3872
+ target: targetId,
3873
+ type: import_types10.EdgeType.CALLS,
3874
+ confidence,
3875
+ confidenceKind: "hostname-shape-match",
3876
+ evidence: ev
3877
+ });
3878
+ continue;
3879
+ }
3880
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3881
+ graph,
3882
+ service.pkg.name,
3883
+ service.node.id,
3884
+ relFile
3885
+ );
3886
+ nodesAdded += n;
3887
+ edgesAdded += e;
3888
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
3889
+ if (!graph.hasEdge(edgeId)) {
3890
+ const edge = {
3891
+ id: edgeId,
3892
+ source: fileNodeId,
3893
+ target: targetId,
3894
+ type: import_types10.EdgeType.CALLS,
3895
+ provenance: import_types10.Provenance.EXTRACTED,
3896
+ confidence,
3897
+ evidence: ev
3898
+ };
3899
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3900
+ edgesAdded++;
3723
3901
  }
3724
- }
3725
- }
3726
- for (const [targetId, evidenceFile] of seenTargets) {
3727
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3728
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3729
- const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3730
- const ev = {
3731
- file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3732
- line,
3733
- snippet: snippet(fileContent, line)
3734
- };
3735
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3736
- if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3737
- noteExtractedDropped({
3738
- source: service.node.id,
3739
- target: targetId,
3740
- type: import_types9.EdgeType.CALLS,
3741
- confidence,
3742
- confidenceKind: "hostname-shape-match",
3743
- evidence: ev
3744
- });
3745
- continue;
3746
- }
3747
- const edge = {
3748
- id: edgeId,
3749
- source: service.node.id,
3750
- target: targetId,
3751
- type: import_types9.EdgeType.CALLS,
3752
- provenance: import_types9.Provenance.EXTRACTED,
3753
- confidence,
3754
- evidence: ev
3755
- };
3756
- if (!graph.hasEdge(edge.id)) {
3757
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3758
- edgesAdded++;
3759
3902
  }
3760
3903
  }
3761
3904
  }
3762
- return edgesAdded;
3905
+ return { nodesAdded, edgesAdded };
3763
3906
  }
3764
3907
 
3765
3908
  // src/extract/calls/kafka.ts
3766
3909
  init_cjs_shims();
3767
3910
  var import_node_path21 = __toESM(require("path"), 1);
3768
- var import_types10 = require("@neat.is/types");
3911
+ var import_types11 = require("@neat.is/types");
3769
3912
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3770
3913
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3771
3914
  function findAll(re, text) {
@@ -3786,7 +3929,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3786
3929
  seen.add(key);
3787
3930
  const line = lineOf(file.content, topic);
3788
3931
  out.push({
3789
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3932
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3790
3933
  name: topic,
3791
3934
  kind: "kafka-topic",
3792
3935
  edgeType,
@@ -3809,7 +3952,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3809
3952
  // src/extract/calls/redis.ts
3810
3953
  init_cjs_shims();
3811
3954
  var import_node_path22 = __toESM(require("path"), 1);
3812
- var import_types11 = require("@neat.is/types");
3955
+ var import_types12 = require("@neat.is/types");
3813
3956
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3814
3957
  function redisEndpointsFromFile(file, serviceDir) {
3815
3958
  const out = [];
@@ -3822,7 +3965,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3822
3965
  seen.add(host);
3823
3966
  const line = lineOf(file.content, host);
3824
3967
  out.push({
3825
- infraId: (0, import_types11.infraId)("redis", host),
3968
+ infraId: (0, import_types12.infraId)("redis", host),
3826
3969
  name: host,
3827
3970
  kind: "redis",
3828
3971
  edgeType: "CALLS",
@@ -3843,7 +3986,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3843
3986
  // src/extract/calls/aws.ts
3844
3987
  init_cjs_shims();
3845
3988
  var import_node_path23 = __toESM(require("path"), 1);
3846
- var import_types12 = require("@neat.is/types");
3989
+ var import_types13 = require("@neat.is/types");
3847
3990
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3848
3991
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3849
3992
  function hasMarker(text, markers) {
@@ -3867,7 +4010,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3867
4010
  seen.add(key);
3868
4011
  const line = lineOf(file.content, name);
3869
4012
  out.push({
3870
- infraId: (0, import_types12.infraId)(kind, name),
4013
+ infraId: (0, import_types13.infraId)(kind, name),
3871
4014
  name,
3872
4015
  kind,
3873
4016
  edgeType: "CALLS",
@@ -3902,7 +4045,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3902
4045
  // src/extract/calls/grpc.ts
3903
4046
  init_cjs_shims();
3904
4047
  var import_node_path24 = __toESM(require("path"), 1);
3905
- var import_types13 = require("@neat.is/types");
4048
+ var import_types14 = require("@neat.is/types");
3906
4049
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3907
4050
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3908
4051
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -3950,7 +4093,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3950
4093
  const { kind } = classified;
3951
4094
  const line = lineOf(file.content, m[0]);
3952
4095
  out.push({
3953
- infraId: (0, import_types13.infraId)(kind, name),
4096
+ infraId: (0, import_types14.infraId)(kind, name),
3954
4097
  name,
3955
4098
  kind,
3956
4099
  edgeType: "CALLS",
@@ -3972,11 +4115,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3972
4115
  function edgeTypeFromEndpoint(ep) {
3973
4116
  switch (ep.edgeType) {
3974
4117
  case "PUBLISHES_TO":
3975
- return import_types14.EdgeType.PUBLISHES_TO;
4118
+ return import_types15.EdgeType.PUBLISHES_TO;
3976
4119
  case "CONSUMES_FROM":
3977
- return import_types14.EdgeType.CONSUMES_FROM;
4120
+ return import_types15.EdgeType.CONSUMES_FROM;
3978
4121
  default:
3979
- return import_types14.EdgeType.CALLS;
4122
+ return import_types15.EdgeType.CALLS;
3980
4123
  }
3981
4124
  }
3982
4125
  function isAwsKind(kind) {
@@ -4003,7 +4146,7 @@ async function addExternalEndpointEdges(graph, services) {
4003
4146
  if (!graph.hasNode(ep.infraId)) {
4004
4147
  const node = {
4005
4148
  id: ep.infraId,
4006
- type: import_types14.NodeType.InfraNode,
4149
+ type: import_types15.NodeType.InfraNode,
4007
4150
  name: ep.name,
4008
4151
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4009
4152
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4015,11 +4158,8 @@ async function addExternalEndpointEdges(graph, services) {
4015
4158
  nodesAdded++;
4016
4159
  }
4017
4160
  const edgeType = edgeTypeFromEndpoint(ep);
4018
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
4019
- if (seenEdges.has(edgeId)) continue;
4020
- seenEdges.add(edgeId);
4021
- const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
4022
- if (!(0, import_types14.passesExtractedFloor)(confidence)) {
4161
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4162
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4023
4163
  noteExtractedDropped({
4024
4164
  source: service.node.id,
4025
4165
  target: ep.infraId,
@@ -4030,13 +4170,25 @@ async function addExternalEndpointEdges(graph, services) {
4030
4170
  });
4031
4171
  continue;
4032
4172
  }
4173
+ const relFile = toPosix2(ep.evidence.file);
4174
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4175
+ graph,
4176
+ service.pkg.name,
4177
+ service.node.id,
4178
+ relFile
4179
+ );
4180
+ nodesAdded += n;
4181
+ edgesAdded += e;
4182
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4183
+ if (seenEdges.has(edgeId)) continue;
4184
+ seenEdges.add(edgeId);
4033
4185
  if (!graph.hasEdge(edgeId)) {
4034
4186
  const edge = {
4035
4187
  id: edgeId,
4036
- source: service.node.id,
4188
+ source: fileNodeId,
4037
4189
  target: ep.infraId,
4038
4190
  type: edgeType,
4039
- provenance: import_types14.Provenance.EXTRACTED,
4191
+ provenance: import_types15.Provenance.EXTRACTED,
4040
4192
  confidence,
4041
4193
  evidence: ep.evidence
4042
4194
  };
@@ -4048,11 +4200,11 @@ async function addExternalEndpointEdges(graph, services) {
4048
4200
  return { nodesAdded, edgesAdded };
4049
4201
  }
4050
4202
  async function addCallEdges(graph, services) {
4051
- const httpEdges = await addHttpCallEdges(graph, services);
4203
+ const http = await addHttpCallEdges(graph, services);
4052
4204
  const ext = await addExternalEndpointEdges(graph, services);
4053
4205
  return {
4054
- nodesAdded: ext.nodesAdded,
4055
- edgesAdded: httpEdges + ext.edgesAdded
4206
+ nodesAdded: http.nodesAdded + ext.nodesAdded,
4207
+ edgesAdded: http.edgesAdded + ext.edgesAdded
4056
4208
  };
4057
4209
  }
4058
4210
 
@@ -4062,15 +4214,15 @@ init_cjs_shims();
4062
4214
  // src/extract/infra/docker-compose.ts
4063
4215
  init_cjs_shims();
4064
4216
  var import_node_path25 = __toESM(require("path"), 1);
4065
- var import_types16 = require("@neat.is/types");
4217
+ var import_types17 = require("@neat.is/types");
4066
4218
 
4067
4219
  // src/extract/infra/shared.ts
4068
4220
  init_cjs_shims();
4069
- var import_types15 = require("@neat.is/types");
4221
+ var import_types16 = require("@neat.is/types");
4070
4222
  function makeInfraNode(kind, name, provider = "self", extras) {
4071
4223
  return {
4072
- id: (0, import_types15.infraId)(kind, name),
4073
- type: import_types15.NodeType.InfraNode,
4224
+ id: (0, import_types16.infraId)(kind, name),
4225
+ type: import_types16.NodeType.InfraNode,
4074
4226
  name,
4075
4227
  provider,
4076
4228
  kind,
@@ -4149,15 +4301,15 @@ async function addComposeInfra(graph, scanPath, services) {
4149
4301
  for (const dep of dependsOnList(svc.depends_on)) {
4150
4302
  const targetId = composeNameToNodeId.get(dep);
4151
4303
  if (!targetId) continue;
4152
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
4304
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4153
4305
  if (graph.hasEdge(edgeId)) continue;
4154
4306
  const edge = {
4155
4307
  id: edgeId,
4156
4308
  source: sourceId,
4157
4309
  target: targetId,
4158
- type: import_types16.EdgeType.DEPENDS_ON,
4159
- provenance: import_types16.Provenance.EXTRACTED,
4160
- confidence: (0, import_types16.confidenceForExtracted)("structural"),
4310
+ type: import_types17.EdgeType.DEPENDS_ON,
4311
+ provenance: import_types17.Provenance.EXTRACTED,
4312
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
4161
4313
  evidence: { file: evidenceFile }
4162
4314
  };
4163
4315
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4171,7 +4323,7 @@ async function addComposeInfra(graph, scanPath, services) {
4171
4323
  init_cjs_shims();
4172
4324
  var import_node_path26 = __toESM(require("path"), 1);
4173
4325
  var import_node_fs14 = require("fs");
4174
- var import_types17 = require("@neat.is/types");
4326
+ var import_types18 = require("@neat.is/types");
4175
4327
  function runtimeImage(content) {
4176
4328
  const lines = content.split("\n");
4177
4329
  let last = null;
@@ -4210,15 +4362,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4210
4362
  graph.addNode(node.id, node);
4211
4363
  nodesAdded++;
4212
4364
  }
4213
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
4365
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4214
4366
  if (!graph.hasEdge(edgeId)) {
4215
4367
  const edge = {
4216
4368
  id: edgeId,
4217
4369
  source: service.node.id,
4218
4370
  target: node.id,
4219
- type: import_types17.EdgeType.RUNS_ON,
4220
- provenance: import_types17.Provenance.EXTRACTED,
4221
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4371
+ type: import_types18.EdgeType.RUNS_ON,
4372
+ provenance: import_types18.Provenance.EXTRACTED,
4373
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4222
4374
  evidence: {
4223
4375
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4224
4376
  }
@@ -4346,13 +4498,24 @@ var import_node_path30 = __toESM(require("path"), 1);
4346
4498
  init_cjs_shims();
4347
4499
  var import_node_fs17 = require("fs");
4348
4500
  var import_node_path29 = __toESM(require("path"), 1);
4349
- var import_types18 = require("@neat.is/types");
4501
+ var import_types19 = require("@neat.is/types");
4502
+ function dropOrphanedFileNodes(graph) {
4503
+ const orphans = [];
4504
+ graph.forEachNode((id, attrs) => {
4505
+ if (attrs.type !== import_types19.NodeType.FileNode) return;
4506
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4507
+ orphans.push(id);
4508
+ }
4509
+ });
4510
+ for (const id of orphans) graph.dropNode(id);
4511
+ return orphans.length;
4512
+ }
4350
4513
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4351
4514
  const toDrop = [];
4352
4515
  const bases = [scanPath, ...serviceDirs];
4353
4516
  graph.forEachEdge((id, attrs) => {
4354
4517
  const edge = attrs;
4355
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4518
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4356
4519
  const evidenceFile = edge.evidence?.file;
4357
4520
  if (!evidenceFile) return;
4358
4521
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4363,6 +4526,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4363
4526
  if (!found) toDrop.push(id);
4364
4527
  });
4365
4528
  for (const id of toDrop) graph.dropEdge(id);
4529
+ dropOrphanedFileNodes(graph);
4366
4530
  return toDrop.length;
4367
4531
  }
4368
4532
 
@@ -4432,7 +4596,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4432
4596
  init_cjs_shims();
4433
4597
  var import_node_fs18 = require("fs");
4434
4598
  var import_node_path31 = __toESM(require("path"), 1);
4435
- var import_types19 = require("@neat.is/types");
4599
+ var import_types20 = require("@neat.is/types");
4436
4600
  var SCHEMA_VERSION = 4;
4437
4601
  function migrateV1ToV2(payload) {
4438
4602
  const nodes = payload.graph.nodes;
@@ -4454,12 +4618,12 @@ function migrateV2ToV3(payload) {
4454
4618
  for (const edge of edges) {
4455
4619
  const attrs = edge.attributes;
4456
4620
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4457
- attrs.provenance = import_types19.Provenance.OBSERVED;
4621
+ attrs.provenance = import_types20.Provenance.OBSERVED;
4458
4622
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4459
4623
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4460
4624
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4461
4625
  if (type && source && target) {
4462
- const newId = (0, import_types19.observedEdgeId)(source, target, type);
4626
+ const newId = (0, import_types20.observedEdgeId)(source, target, type);
4463
4627
  attrs.id = newId;
4464
4628
  if (edge.key) edge.key = newId;
4465
4629
  }
@@ -4545,11 +4709,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4545
4709
  init_cjs_shims();
4546
4710
  var import_fastify = __toESM(require("fastify"), 1);
4547
4711
  var import_cors = __toESM(require("@fastify/cors"), 1);
4548
- var import_types22 = require("@neat.is/types");
4712
+ var import_types23 = require("@neat.is/types");
4549
4713
 
4550
4714
  // src/divergences.ts
4551
4715
  init_cjs_shims();
4552
- var import_types20 = require("@neat.is/types");
4716
+ var import_types21 = require("@neat.is/types");
4553
4717
  function bucketKey(source, target, type) {
4554
4718
  return `${type}|${source}|${target}`;
4555
4719
  }
@@ -4557,22 +4721,22 @@ function bucketEdges(graph) {
4557
4721
  const buckets = /* @__PURE__ */ new Map();
4558
4722
  graph.forEachEdge((id, attrs) => {
4559
4723
  const e = attrs;
4560
- const parsed = (0, import_types20.parseEdgeId)(id);
4724
+ const parsed = (0, import_types21.parseEdgeId)(id);
4561
4725
  const provenance = parsed?.provenance ?? e.provenance;
4562
4726
  const key = bucketKey(e.source, e.target, e.type);
4563
4727
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4564
4728
  switch (provenance) {
4565
- case import_types20.Provenance.EXTRACTED:
4729
+ case import_types21.Provenance.EXTRACTED:
4566
4730
  cur.extracted = e;
4567
4731
  break;
4568
- case import_types20.Provenance.OBSERVED:
4732
+ case import_types21.Provenance.OBSERVED:
4569
4733
  cur.observed = e;
4570
4734
  break;
4571
- case import_types20.Provenance.INFERRED:
4735
+ case import_types21.Provenance.INFERRED:
4572
4736
  cur.inferred = e;
4573
4737
  break;
4574
4738
  default:
4575
- if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4739
+ if (e.provenance === import_types21.Provenance.STALE) cur.stale = e;
4576
4740
  }
4577
4741
  buckets.set(key, cur);
4578
4742
  });
@@ -4581,7 +4745,7 @@ function bucketEdges(graph) {
4581
4745
  function nodeIsFrontier(graph, nodeId) {
4582
4746
  if (!graph.hasNode(nodeId)) return false;
4583
4747
  const attrs = graph.getNodeAttributes(nodeId);
4584
- return attrs.type === import_types20.NodeType.FrontierNode;
4748
+ return attrs.type === import_types21.NodeType.FrontierNode;
4585
4749
  }
4586
4750
  function clampConfidence(n) {
4587
4751
  if (!Number.isFinite(n)) return 0;
@@ -4602,6 +4766,7 @@ function gradedConfidence(edge) {
4602
4766
  }
4603
4767
  function detectMissingDivergences(graph, bucket) {
4604
4768
  const out = [];
4769
+ if (bucket.type === import_types21.EdgeType.CONTAINS) return out;
4605
4770
  if (bucket.extracted && !bucket.observed) {
4606
4771
  if (!nodeIsFrontier(graph, bucket.target)) {
4607
4772
  out.push({
@@ -4642,7 +4807,7 @@ function declaredHostFor(svc) {
4642
4807
  function hasExtractedConfiguredBy(graph, svcId) {
4643
4808
  for (const edgeId of graph.outboundEdges(svcId)) {
4644
4809
  const e = graph.getEdgeAttributes(edgeId);
4645
- if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4810
+ if (e.type === import_types21.EdgeType.CONFIGURED_BY && e.provenance === import_types21.Provenance.EXTRACTED) {
4646
4811
  return true;
4647
4812
  }
4648
4813
  }
@@ -4655,10 +4820,10 @@ function detectHostMismatch(graph, svcId, svc) {
4655
4820
  const out = [];
4656
4821
  for (const edgeId of graph.outboundEdges(svcId)) {
4657
4822
  const edge = graph.getEdgeAttributes(edgeId);
4658
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4659
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4823
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4824
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4660
4825
  const target = graph.getNodeAttributes(edge.target);
4661
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4826
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4662
4827
  const observedHost = target.host?.trim();
4663
4828
  if (!observedHost) continue;
4664
4829
  if (observedHost === declaredHost) continue;
@@ -4680,10 +4845,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4680
4845
  const deps = svc.dependencies ?? {};
4681
4846
  for (const edgeId of graph.outboundEdges(svcId)) {
4682
4847
  const edge = graph.getEdgeAttributes(edgeId);
4683
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4684
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4848
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4849
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4685
4850
  const target = graph.getNodeAttributes(edge.target);
4686
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4851
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4687
4852
  for (const pair of compatPairs()) {
4688
4853
  if (pair.engine !== target.engine) continue;
4689
4854
  const declared = deps[pair.driver];
@@ -4744,7 +4909,7 @@ function computeDivergences(graph, opts = {}) {
4744
4909
  }
4745
4910
  graph.forEachNode((nodeId, attrs) => {
4746
4911
  const n = attrs;
4747
- if (n.type !== import_types20.NodeType.ServiceNode) return;
4912
+ if (n.type !== import_types21.NodeType.ServiceNode) return;
4748
4913
  const svc = n;
4749
4914
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4750
4915
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4777,7 +4942,7 @@ function computeDivergences(graph, opts = {}) {
4777
4942
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4778
4943
  return a.target.localeCompare(b.target);
4779
4944
  });
4780
- return import_types20.DivergenceResultSchema.parse({
4945
+ return import_types21.DivergenceResultSchema.parse({
4781
4946
  divergences: filtered,
4782
4947
  totalAffected: filtered.length,
4783
4948
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4918,7 +5083,7 @@ init_cjs_shims();
4918
5083
  var import_node_fs20 = require("fs");
4919
5084
  var import_node_os2 = __toESM(require("os"), 1);
4920
5085
  var import_node_path33 = __toESM(require("path"), 1);
4921
- var import_types21 = require("@neat.is/types");
5086
+ var import_types22 = require("@neat.is/types");
4922
5087
  var LOCK_TIMEOUT_MS = 5e3;
4923
5088
  var LOCK_RETRY_MS = 50;
4924
5089
  function neatHome() {
@@ -4997,10 +5162,10 @@ async function readRegistry() {
4997
5162
  throw err;
4998
5163
  }
4999
5164
  const parsed = JSON.parse(raw);
5000
- return import_types21.RegistryFileSchema.parse(parsed);
5165
+ return import_types22.RegistryFileSchema.parse(parsed);
5001
5166
  }
5002
5167
  async function writeRegistry(reg) {
5003
- const validated = import_types21.RegistryFileSchema.parse(reg);
5168
+ const validated = import_types22.RegistryFileSchema.parse(reg);
5004
5169
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5005
5170
  }
5006
5171
  var ProjectNameCollisionError = class extends Error {
@@ -5272,11 +5437,11 @@ function registerRoutes(scope, ctx) {
5272
5437
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5273
5438
  const parsed = [];
5274
5439
  for (const c of candidates) {
5275
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5440
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
5276
5441
  if (!r.success) {
5277
5442
  return reply.code(400).send({
5278
5443
  error: `unknown divergence type "${c}"`,
5279
- allowed: import_types22.DivergenceTypeSchema.options
5444
+ allowed: import_types23.DivergenceTypeSchema.options
5280
5445
  });
5281
5446
  }
5282
5447
  parsed.push(r.data);
@@ -5489,7 +5654,7 @@ function registerRoutes(scope, ctx) {
5489
5654
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5490
5655
  let violations = await log.readAll();
5491
5656
  if (req.query.severity) {
5492
- const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5657
+ const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5493
5658
  if (!sev.success) {
5494
5659
  return reply.code(400).send({
5495
5660
  error: "invalid severity",
@@ -5506,7 +5671,7 @@ function registerRoutes(scope, ctx) {
5506
5671
  scope.post("/policies/check", async (req, reply) => {
5507
5672
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5508
5673
  if (!proj) return;
5509
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5674
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5510
5675
  if (!parsed.success) {
5511
5676
  return reply.code(400).send({
5512
5677
  error: "invalid /policies/check body",