@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/neatd.cjs CHANGED
@@ -1607,6 +1607,86 @@ function hostFromUrl(u) {
1607
1607
  function pickAddress(span) {
1608
1608
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1609
1609
  }
1610
+ var CODE_FILEPATH_ATTR = "code.filepath";
1611
+ var CODE_LINENO_ATTR = "code.lineno";
1612
+ var CODE_FUNCTION_ATTR = "code.function";
1613
+ function toPosix(p) {
1614
+ return p.split("\\").join("/");
1615
+ }
1616
+ function languageForExt(relPath) {
1617
+ const dot = relPath.lastIndexOf(".");
1618
+ if (dot === -1) return void 0;
1619
+ switch (relPath.slice(dot).toLowerCase()) {
1620
+ case ".py":
1621
+ return "python";
1622
+ case ".ts":
1623
+ case ".tsx":
1624
+ return "typescript";
1625
+ case ".js":
1626
+ case ".jsx":
1627
+ case ".mjs":
1628
+ case ".cjs":
1629
+ return "javascript";
1630
+ default:
1631
+ return void 0;
1632
+ }
1633
+ }
1634
+ function relPathForRuntimeFile(filepath, serviceNode) {
1635
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1636
+ const root = serviceNode?.repoPath;
1637
+ if (root && root !== "." && root.length > 0) {
1638
+ const rootPosix = toPosix(root);
1639
+ const anchor = `/${rootPosix}/`;
1640
+ const idx = p.lastIndexOf(anchor);
1641
+ if (idx !== -1) return p.slice(idx + anchor.length);
1642
+ const base = rootPosix.split("/").filter(Boolean).pop();
1643
+ if (base) {
1644
+ const baseAnchor = `/${base}/`;
1645
+ const bidx = p.lastIndexOf(baseAnchor);
1646
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1647
+ }
1648
+ }
1649
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1650
+ return p.length > 0 ? p : null;
1651
+ }
1652
+ function callSiteFromSpan(span, serviceNode) {
1653
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1654
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1655
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1656
+ if (!relPath) return null;
1657
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1658
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1659
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1660
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1661
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1662
+ }
1663
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1664
+ const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
1665
+ if (!graph.hasNode(fileNodeId)) {
1666
+ const language = languageForExt(callSite.relPath);
1667
+ const node = {
1668
+ id: fileNodeId,
1669
+ type: import_types3.NodeType.FileNode,
1670
+ service: serviceName,
1671
+ path: callSite.relPath,
1672
+ ...language ? { language } : {},
1673
+ discoveredVia: "otel"
1674
+ };
1675
+ graph.addNode(fileNodeId, node);
1676
+ }
1677
+ const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1678
+ if (!graph.hasEdge(containsId)) {
1679
+ const edge = {
1680
+ id: containsId,
1681
+ source: serviceNodeId,
1682
+ target: fileNodeId,
1683
+ type: import_types3.EdgeType.CONTAINS,
1684
+ provenance: import_types3.Provenance.OBSERVED
1685
+ };
1686
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1687
+ }
1688
+ return fileNodeId;
1689
+ }
1610
1690
  function makeObservedEdgeId(type, source, target) {
1611
1691
  return (0, import_types3.observedEdgeId)(source, target, type);
1612
1692
  }
@@ -1849,6 +1929,9 @@ async function handleSpan(ctx, span) {
1849
1929
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1850
1930
  const isError = span.statusCode === 2;
1851
1931
  cacheSpanService(span, nowMs);
1932
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1933
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
1934
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1852
1935
  let affectedNode = sourceId;
1853
1936
  if (span.dbSystem) {
1854
1937
  const host = pickAddress(span);
@@ -1858,7 +1941,7 @@ async function handleSpan(ctx, span) {
1858
1941
  const result = upsertObservedEdge(
1859
1942
  ctx.graph,
1860
1943
  import_types3.EdgeType.CONNECTS_TO,
1861
- sourceId,
1944
+ observedSource(),
1862
1945
  targetId,
1863
1946
  ts,
1864
1947
  isError
@@ -1874,7 +1957,7 @@ async function handleSpan(ctx, span) {
1874
1957
  upsertObservedEdge(
1875
1958
  ctx.graph,
1876
1959
  import_types3.EdgeType.CALLS,
1877
- sourceId,
1960
+ observedSource(),
1878
1961
  targetId,
1879
1962
  ts,
1880
1963
  isError
@@ -1886,7 +1969,7 @@ async function handleSpan(ctx, span) {
1886
1969
  upsertObservedEdge(
1887
1970
  ctx.graph,
1888
1971
  import_types3.EdgeType.CALLS,
1889
- sourceId,
1972
+ observedSource(),
1890
1973
  frontierNodeId,
1891
1974
  ts,
1892
1975
  isError
@@ -3444,7 +3527,7 @@ async function addConfigNodes(graph, services, scanPath) {
3444
3527
 
3445
3528
  // src/extract/calls/index.ts
3446
3529
  init_cjs_shims();
3447
- var import_types14 = require("@neat.is/types");
3530
+ var import_types15 = require("@neat.is/types");
3448
3531
 
3449
3532
  // src/extract/calls/http.ts
3450
3533
  init_cjs_shims();
@@ -3452,12 +3535,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3452
3535
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3453
3536
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3454
3537
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3455
- var import_types9 = require("@neat.is/types");
3538
+ var import_types10 = require("@neat.is/types");
3456
3539
 
3457
3540
  // src/extract/calls/shared.ts
3458
3541
  init_cjs_shims();
3459
3542
  var import_node_fs13 = require("fs");
3460
3543
  var import_node_path19 = __toESM(require("path"), 1);
3544
+ var import_types9 = require("@neat.is/types");
3461
3545
  async function walkSourceFiles(dir) {
3462
3546
  const out = [];
3463
3547
  async function walk(current) {
@@ -3497,6 +3581,58 @@ function snippet(text, line) {
3497
3581
  const lines = text.split("\n");
3498
3582
  return (lines[line - 1] ?? "").trim();
3499
3583
  }
3584
+ function toPosix2(p) {
3585
+ return p.split("\\").join("/");
3586
+ }
3587
+ function languageForPath(relPath) {
3588
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3589
+ case ".py":
3590
+ return "python";
3591
+ case ".ts":
3592
+ case ".tsx":
3593
+ return "typescript";
3594
+ case ".js":
3595
+ case ".jsx":
3596
+ case ".mjs":
3597
+ case ".cjs":
3598
+ return "javascript";
3599
+ default:
3600
+ return void 0;
3601
+ }
3602
+ }
3603
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3604
+ let nodesAdded = 0;
3605
+ let edgesAdded = 0;
3606
+ const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3607
+ if (!graph.hasNode(fileNodeId)) {
3608
+ const language = languageForPath(relPath);
3609
+ const node = {
3610
+ id: fileNodeId,
3611
+ type: import_types9.NodeType.FileNode,
3612
+ service: serviceName,
3613
+ path: relPath,
3614
+ ...language ? { language } : {},
3615
+ discoveredVia: "static"
3616
+ };
3617
+ graph.addNode(fileNodeId, node);
3618
+ nodesAdded++;
3619
+ }
3620
+ const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3621
+ if (!graph.hasEdge(containsId)) {
3622
+ const edge = {
3623
+ id: containsId,
3624
+ source: serviceNodeId,
3625
+ target: fileNodeId,
3626
+ type: import_types9.EdgeType.CONTAINS,
3627
+ provenance: import_types9.Provenance.EXTRACTED,
3628
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3629
+ evidence: { file: relPath }
3630
+ };
3631
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3632
+ edgesAdded++;
3633
+ }
3634
+ return { fileNodeId, nodesAdded, edgesAdded };
3635
+ }
3500
3636
 
3501
3637
  // src/extract/calls/http.ts
3502
3638
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3530,16 +3666,16 @@ function callsFromSource(source, parser, knownHosts) {
3530
3666
  const tree = parser.parse(source);
3531
3667
  const literals = [];
3532
3668
  collectStringLiterals(tree.rootNode, literals);
3533
- const targets = /* @__PURE__ */ new Set();
3669
+ const out = [];
3534
3670
  for (const lit of literals) {
3535
3671
  if (isInsideJsxExternalLink(lit.node)) continue;
3536
3672
  for (const host of knownHosts) {
3537
3673
  if (urlMatchesHost(lit.text, host)) {
3538
- targets.add(host);
3674
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3539
3675
  }
3540
3676
  }
3541
3677
  }
3542
- return targets;
3678
+ return out;
3543
3679
  }
3544
3680
  function makeJsParser() {
3545
3681
  const p = new import_tree_sitter.default();
@@ -3562,71 +3698,78 @@ async function addHttpCallEdges(graph, services) {
3562
3698
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3563
3699
  hostToNodeId.set(service.pkg.name, service.node.id);
3564
3700
  }
3701
+ let nodesAdded = 0;
3565
3702
  let edgesAdded = 0;
3566
3703
  for (const service of services) {
3567
3704
  const files = await loadSourceFiles(service.dir);
3568
- const seenTargets = /* @__PURE__ */ new Map();
3705
+ const seen = /* @__PURE__ */ new Set();
3569
3706
  for (const file of files) {
3570
3707
  if (isTestPath(file.path)) continue;
3571
3708
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3572
- let targets;
3709
+ let sites;
3573
3710
  try {
3574
- targets = callsFromSource(file.content, parser, knownHosts);
3711
+ sites = callsFromSource(file.content, parser, knownHosts);
3575
3712
  } catch (err) {
3576
3713
  recordExtractionError("http call extraction", file.path, err);
3577
3714
  continue;
3578
3715
  }
3579
- for (const t of targets) {
3580
- const targetId = hostToNodeId.get(t);
3716
+ if (sites.length === 0) continue;
3717
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
3718
+ for (const site of sites) {
3719
+ const targetId = hostToNodeId.get(site.host);
3581
3720
  if (!targetId || targetId === service.node.id) continue;
3582
- if (!seenTargets.has(targetId)) {
3583
- seenTargets.set(targetId, { file: file.path, host: t });
3721
+ const dedupKey = `${relFile}|${targetId}`;
3722
+ if (seen.has(dedupKey)) continue;
3723
+ seen.add(dedupKey);
3724
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3725
+ const ev = {
3726
+ file: relFile,
3727
+ line: site.line,
3728
+ snippet: snippet(file.content, site.line)
3729
+ };
3730
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3731
+ noteExtractedDropped({
3732
+ source: service.node.id,
3733
+ target: targetId,
3734
+ type: import_types10.EdgeType.CALLS,
3735
+ confidence,
3736
+ confidenceKind: "hostname-shape-match",
3737
+ evidence: ev
3738
+ });
3739
+ continue;
3740
+ }
3741
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3742
+ graph,
3743
+ service.pkg.name,
3744
+ service.node.id,
3745
+ relFile
3746
+ );
3747
+ nodesAdded += n;
3748
+ edgesAdded += e;
3749
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
3750
+ if (!graph.hasEdge(edgeId)) {
3751
+ const edge = {
3752
+ id: edgeId,
3753
+ source: fileNodeId,
3754
+ target: targetId,
3755
+ type: import_types10.EdgeType.CALLS,
3756
+ provenance: import_types10.Provenance.EXTRACTED,
3757
+ confidence,
3758
+ evidence: ev
3759
+ };
3760
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3761
+ edgesAdded++;
3584
3762
  }
3585
- }
3586
- }
3587
- for (const [targetId, evidenceFile] of seenTargets) {
3588
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3589
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3590
- const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3591
- const ev = {
3592
- file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3593
- line,
3594
- snippet: snippet(fileContent, line)
3595
- };
3596
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3597
- if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3598
- noteExtractedDropped({
3599
- source: service.node.id,
3600
- target: targetId,
3601
- type: import_types9.EdgeType.CALLS,
3602
- confidence,
3603
- confidenceKind: "hostname-shape-match",
3604
- evidence: ev
3605
- });
3606
- continue;
3607
- }
3608
- const edge = {
3609
- id: edgeId,
3610
- source: service.node.id,
3611
- target: targetId,
3612
- type: import_types9.EdgeType.CALLS,
3613
- provenance: import_types9.Provenance.EXTRACTED,
3614
- confidence,
3615
- evidence: ev
3616
- };
3617
- if (!graph.hasEdge(edge.id)) {
3618
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3619
- edgesAdded++;
3620
3763
  }
3621
3764
  }
3622
3765
  }
3623
- return edgesAdded;
3766
+ return { nodesAdded, edgesAdded };
3624
3767
  }
3625
3768
 
3626
3769
  // src/extract/calls/kafka.ts
3627
3770
  init_cjs_shims();
3628
3771
  var import_node_path21 = __toESM(require("path"), 1);
3629
- var import_types10 = require("@neat.is/types");
3772
+ var import_types11 = require("@neat.is/types");
3630
3773
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3631
3774
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3632
3775
  function findAll(re, text) {
@@ -3647,7 +3790,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3647
3790
  seen.add(key);
3648
3791
  const line = lineOf(file.content, topic);
3649
3792
  out.push({
3650
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3793
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3651
3794
  name: topic,
3652
3795
  kind: "kafka-topic",
3653
3796
  edgeType,
@@ -3670,7 +3813,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3670
3813
  // src/extract/calls/redis.ts
3671
3814
  init_cjs_shims();
3672
3815
  var import_node_path22 = __toESM(require("path"), 1);
3673
- var import_types11 = require("@neat.is/types");
3816
+ var import_types12 = require("@neat.is/types");
3674
3817
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3675
3818
  function redisEndpointsFromFile(file, serviceDir) {
3676
3819
  const out = [];
@@ -3683,7 +3826,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3683
3826
  seen.add(host);
3684
3827
  const line = lineOf(file.content, host);
3685
3828
  out.push({
3686
- infraId: (0, import_types11.infraId)("redis", host),
3829
+ infraId: (0, import_types12.infraId)("redis", host),
3687
3830
  name: host,
3688
3831
  kind: "redis",
3689
3832
  edgeType: "CALLS",
@@ -3704,7 +3847,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3704
3847
  // src/extract/calls/aws.ts
3705
3848
  init_cjs_shims();
3706
3849
  var import_node_path23 = __toESM(require("path"), 1);
3707
- var import_types12 = require("@neat.is/types");
3850
+ var import_types13 = require("@neat.is/types");
3708
3851
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3709
3852
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3710
3853
  function hasMarker(text, markers) {
@@ -3728,7 +3871,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3728
3871
  seen.add(key);
3729
3872
  const line = lineOf(file.content, name);
3730
3873
  out.push({
3731
- infraId: (0, import_types12.infraId)(kind, name),
3874
+ infraId: (0, import_types13.infraId)(kind, name),
3732
3875
  name,
3733
3876
  kind,
3734
3877
  edgeType: "CALLS",
@@ -3763,7 +3906,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3763
3906
  // src/extract/calls/grpc.ts
3764
3907
  init_cjs_shims();
3765
3908
  var import_node_path24 = __toESM(require("path"), 1);
3766
- var import_types13 = require("@neat.is/types");
3909
+ var import_types14 = require("@neat.is/types");
3767
3910
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3768
3911
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3769
3912
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -3811,7 +3954,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3811
3954
  const { kind } = classified;
3812
3955
  const line = lineOf(file.content, m[0]);
3813
3956
  out.push({
3814
- infraId: (0, import_types13.infraId)(kind, name),
3957
+ infraId: (0, import_types14.infraId)(kind, name),
3815
3958
  name,
3816
3959
  kind,
3817
3960
  edgeType: "CALLS",
@@ -3833,11 +3976,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3833
3976
  function edgeTypeFromEndpoint(ep) {
3834
3977
  switch (ep.edgeType) {
3835
3978
  case "PUBLISHES_TO":
3836
- return import_types14.EdgeType.PUBLISHES_TO;
3979
+ return import_types15.EdgeType.PUBLISHES_TO;
3837
3980
  case "CONSUMES_FROM":
3838
- return import_types14.EdgeType.CONSUMES_FROM;
3981
+ return import_types15.EdgeType.CONSUMES_FROM;
3839
3982
  default:
3840
- return import_types14.EdgeType.CALLS;
3983
+ return import_types15.EdgeType.CALLS;
3841
3984
  }
3842
3985
  }
3843
3986
  function isAwsKind(kind) {
@@ -3864,7 +4007,7 @@ async function addExternalEndpointEdges(graph, services) {
3864
4007
  if (!graph.hasNode(ep.infraId)) {
3865
4008
  const node = {
3866
4009
  id: ep.infraId,
3867
- type: import_types14.NodeType.InfraNode,
4010
+ type: import_types15.NodeType.InfraNode,
3868
4011
  name: ep.name,
3869
4012
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3870
4013
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -3876,11 +4019,8 @@ async function addExternalEndpointEdges(graph, services) {
3876
4019
  nodesAdded++;
3877
4020
  }
3878
4021
  const edgeType = edgeTypeFromEndpoint(ep);
3879
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3880
- if (seenEdges.has(edgeId)) continue;
3881
- seenEdges.add(edgeId);
3882
- const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
3883
- if (!(0, import_types14.passesExtractedFloor)(confidence)) {
4022
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4023
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
3884
4024
  noteExtractedDropped({
3885
4025
  source: service.node.id,
3886
4026
  target: ep.infraId,
@@ -3891,13 +4031,25 @@ async function addExternalEndpointEdges(graph, services) {
3891
4031
  });
3892
4032
  continue;
3893
4033
  }
4034
+ const relFile = toPosix2(ep.evidence.file);
4035
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4036
+ graph,
4037
+ service.pkg.name,
4038
+ service.node.id,
4039
+ relFile
4040
+ );
4041
+ nodesAdded += n;
4042
+ edgesAdded += e;
4043
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4044
+ if (seenEdges.has(edgeId)) continue;
4045
+ seenEdges.add(edgeId);
3894
4046
  if (!graph.hasEdge(edgeId)) {
3895
4047
  const edge = {
3896
4048
  id: edgeId,
3897
- source: service.node.id,
4049
+ source: fileNodeId,
3898
4050
  target: ep.infraId,
3899
4051
  type: edgeType,
3900
- provenance: import_types14.Provenance.EXTRACTED,
4052
+ provenance: import_types15.Provenance.EXTRACTED,
3901
4053
  confidence,
3902
4054
  evidence: ep.evidence
3903
4055
  };
@@ -3909,11 +4061,11 @@ async function addExternalEndpointEdges(graph, services) {
3909
4061
  return { nodesAdded, edgesAdded };
3910
4062
  }
3911
4063
  async function addCallEdges(graph, services) {
3912
- const httpEdges = await addHttpCallEdges(graph, services);
4064
+ const http = await addHttpCallEdges(graph, services);
3913
4065
  const ext = await addExternalEndpointEdges(graph, services);
3914
4066
  return {
3915
- nodesAdded: ext.nodesAdded,
3916
- edgesAdded: httpEdges + ext.edgesAdded
4067
+ nodesAdded: http.nodesAdded + ext.nodesAdded,
4068
+ edgesAdded: http.edgesAdded + ext.edgesAdded
3917
4069
  };
3918
4070
  }
3919
4071
 
@@ -3923,15 +4075,15 @@ init_cjs_shims();
3923
4075
  // src/extract/infra/docker-compose.ts
3924
4076
  init_cjs_shims();
3925
4077
  var import_node_path25 = __toESM(require("path"), 1);
3926
- var import_types16 = require("@neat.is/types");
4078
+ var import_types17 = require("@neat.is/types");
3927
4079
 
3928
4080
  // src/extract/infra/shared.ts
3929
4081
  init_cjs_shims();
3930
- var import_types15 = require("@neat.is/types");
4082
+ var import_types16 = require("@neat.is/types");
3931
4083
  function makeInfraNode(kind, name, provider = "self", extras) {
3932
4084
  return {
3933
- id: (0, import_types15.infraId)(kind, name),
3934
- type: import_types15.NodeType.InfraNode,
4085
+ id: (0, import_types16.infraId)(kind, name),
4086
+ type: import_types16.NodeType.InfraNode,
3935
4087
  name,
3936
4088
  provider,
3937
4089
  kind,
@@ -4010,15 +4162,15 @@ async function addComposeInfra(graph, scanPath, services) {
4010
4162
  for (const dep of dependsOnList(svc.depends_on)) {
4011
4163
  const targetId = composeNameToNodeId.get(dep);
4012
4164
  if (!targetId) continue;
4013
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
4165
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4014
4166
  if (graph.hasEdge(edgeId)) continue;
4015
4167
  const edge = {
4016
4168
  id: edgeId,
4017
4169
  source: sourceId,
4018
4170
  target: targetId,
4019
- type: import_types16.EdgeType.DEPENDS_ON,
4020
- provenance: import_types16.Provenance.EXTRACTED,
4021
- confidence: (0, import_types16.confidenceForExtracted)("structural"),
4171
+ type: import_types17.EdgeType.DEPENDS_ON,
4172
+ provenance: import_types17.Provenance.EXTRACTED,
4173
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
4022
4174
  evidence: { file: evidenceFile }
4023
4175
  };
4024
4176
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4032,7 +4184,7 @@ async function addComposeInfra(graph, scanPath, services) {
4032
4184
  init_cjs_shims();
4033
4185
  var import_node_path26 = __toESM(require("path"), 1);
4034
4186
  var import_node_fs14 = require("fs");
4035
- var import_types17 = require("@neat.is/types");
4187
+ var import_types18 = require("@neat.is/types");
4036
4188
  function runtimeImage(content) {
4037
4189
  const lines = content.split("\n");
4038
4190
  let last = null;
@@ -4071,15 +4223,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4071
4223
  graph.addNode(node.id, node);
4072
4224
  nodesAdded++;
4073
4225
  }
4074
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
4226
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4075
4227
  if (!graph.hasEdge(edgeId)) {
4076
4228
  const edge = {
4077
4229
  id: edgeId,
4078
4230
  source: service.node.id,
4079
4231
  target: node.id,
4080
- type: import_types17.EdgeType.RUNS_ON,
4081
- provenance: import_types17.Provenance.EXTRACTED,
4082
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4232
+ type: import_types18.EdgeType.RUNS_ON,
4233
+ provenance: import_types18.Provenance.EXTRACTED,
4234
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4083
4235
  evidence: {
4084
4236
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4085
4237
  }
@@ -4207,13 +4359,24 @@ var import_node_path30 = __toESM(require("path"), 1);
4207
4359
  init_cjs_shims();
4208
4360
  var import_node_fs17 = require("fs");
4209
4361
  var import_node_path29 = __toESM(require("path"), 1);
4210
- var import_types18 = require("@neat.is/types");
4362
+ var import_types19 = require("@neat.is/types");
4363
+ function dropOrphanedFileNodes(graph) {
4364
+ const orphans = [];
4365
+ graph.forEachNode((id, attrs) => {
4366
+ if (attrs.type !== import_types19.NodeType.FileNode) return;
4367
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4368
+ orphans.push(id);
4369
+ }
4370
+ });
4371
+ for (const id of orphans) graph.dropNode(id);
4372
+ return orphans.length;
4373
+ }
4211
4374
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4212
4375
  const toDrop = [];
4213
4376
  const bases = [scanPath, ...serviceDirs];
4214
4377
  graph.forEachEdge((id, attrs) => {
4215
4378
  const edge = attrs;
4216
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4379
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4217
4380
  const evidenceFile = edge.evidence?.file;
4218
4381
  if (!evidenceFile) return;
4219
4382
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4224,6 +4387,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4224
4387
  if (!found) toDrop.push(id);
4225
4388
  });
4226
4389
  for (const id of toDrop) graph.dropEdge(id);
4390
+ dropOrphanedFileNodes(graph);
4227
4391
  return toDrop.length;
4228
4392
  }
4229
4393
 
@@ -4293,7 +4457,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4293
4457
  init_cjs_shims();
4294
4458
  var import_node_fs18 = require("fs");
4295
4459
  var import_node_path31 = __toESM(require("path"), 1);
4296
- var import_types19 = require("@neat.is/types");
4460
+ var import_types20 = require("@neat.is/types");
4297
4461
  var SCHEMA_VERSION = 4;
4298
4462
  function migrateV1ToV2(payload) {
4299
4463
  const nodes = payload.graph.nodes;
@@ -4315,12 +4479,12 @@ function migrateV2ToV3(payload) {
4315
4479
  for (const edge of edges) {
4316
4480
  const attrs = edge.attributes;
4317
4481
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4318
- attrs.provenance = import_types19.Provenance.OBSERVED;
4482
+ attrs.provenance = import_types20.Provenance.OBSERVED;
4319
4483
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4320
4484
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4321
4485
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4322
4486
  if (type && source && target) {
4323
- const newId = (0, import_types19.observedEdgeId)(source, target, type);
4487
+ const newId = (0, import_types20.observedEdgeId)(source, target, type);
4324
4488
  attrs.id = newId;
4325
4489
  if (edge.key) edge.key = newId;
4326
4490
  }
@@ -4458,11 +4622,11 @@ var Projects = class {
4458
4622
  init_cjs_shims();
4459
4623
  var import_fastify = __toESM(require("fastify"), 1);
4460
4624
  var import_cors = __toESM(require("@fastify/cors"), 1);
4461
- var import_types22 = require("@neat.is/types");
4625
+ var import_types23 = require("@neat.is/types");
4462
4626
 
4463
4627
  // src/divergences.ts
4464
4628
  init_cjs_shims();
4465
- var import_types20 = require("@neat.is/types");
4629
+ var import_types21 = require("@neat.is/types");
4466
4630
  function bucketKey(source, target, type) {
4467
4631
  return `${type}|${source}|${target}`;
4468
4632
  }
@@ -4470,22 +4634,22 @@ function bucketEdges(graph) {
4470
4634
  const buckets = /* @__PURE__ */ new Map();
4471
4635
  graph.forEachEdge((id, attrs) => {
4472
4636
  const e = attrs;
4473
- const parsed = (0, import_types20.parseEdgeId)(id);
4637
+ const parsed = (0, import_types21.parseEdgeId)(id);
4474
4638
  const provenance = parsed?.provenance ?? e.provenance;
4475
4639
  const key = bucketKey(e.source, e.target, e.type);
4476
4640
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4477
4641
  switch (provenance) {
4478
- case import_types20.Provenance.EXTRACTED:
4642
+ case import_types21.Provenance.EXTRACTED:
4479
4643
  cur.extracted = e;
4480
4644
  break;
4481
- case import_types20.Provenance.OBSERVED:
4645
+ case import_types21.Provenance.OBSERVED:
4482
4646
  cur.observed = e;
4483
4647
  break;
4484
- case import_types20.Provenance.INFERRED:
4648
+ case import_types21.Provenance.INFERRED:
4485
4649
  cur.inferred = e;
4486
4650
  break;
4487
4651
  default:
4488
- if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4652
+ if (e.provenance === import_types21.Provenance.STALE) cur.stale = e;
4489
4653
  }
4490
4654
  buckets.set(key, cur);
4491
4655
  });
@@ -4494,7 +4658,7 @@ function bucketEdges(graph) {
4494
4658
  function nodeIsFrontier(graph, nodeId) {
4495
4659
  if (!graph.hasNode(nodeId)) return false;
4496
4660
  const attrs = graph.getNodeAttributes(nodeId);
4497
- return attrs.type === import_types20.NodeType.FrontierNode;
4661
+ return attrs.type === import_types21.NodeType.FrontierNode;
4498
4662
  }
4499
4663
  function clampConfidence(n) {
4500
4664
  if (!Number.isFinite(n)) return 0;
@@ -4515,6 +4679,7 @@ function gradedConfidence(edge) {
4515
4679
  }
4516
4680
  function detectMissingDivergences(graph, bucket) {
4517
4681
  const out = [];
4682
+ if (bucket.type === import_types21.EdgeType.CONTAINS) return out;
4518
4683
  if (bucket.extracted && !bucket.observed) {
4519
4684
  if (!nodeIsFrontier(graph, bucket.target)) {
4520
4685
  out.push({
@@ -4555,7 +4720,7 @@ function declaredHostFor(svc) {
4555
4720
  function hasExtractedConfiguredBy(graph, svcId) {
4556
4721
  for (const edgeId of graph.outboundEdges(svcId)) {
4557
4722
  const e = graph.getEdgeAttributes(edgeId);
4558
- if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4723
+ if (e.type === import_types21.EdgeType.CONFIGURED_BY && e.provenance === import_types21.Provenance.EXTRACTED) {
4559
4724
  return true;
4560
4725
  }
4561
4726
  }
@@ -4568,10 +4733,10 @@ function detectHostMismatch(graph, svcId, svc) {
4568
4733
  const out = [];
4569
4734
  for (const edgeId of graph.outboundEdges(svcId)) {
4570
4735
  const edge = graph.getEdgeAttributes(edgeId);
4571
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4572
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4736
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4737
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4573
4738
  const target = graph.getNodeAttributes(edge.target);
4574
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4739
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4575
4740
  const observedHost = target.host?.trim();
4576
4741
  if (!observedHost) continue;
4577
4742
  if (observedHost === declaredHost) continue;
@@ -4593,10 +4758,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4593
4758
  const deps = svc.dependencies ?? {};
4594
4759
  for (const edgeId of graph.outboundEdges(svcId)) {
4595
4760
  const edge = graph.getEdgeAttributes(edgeId);
4596
- if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4597
- if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4761
+ if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
4762
+ if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
4598
4763
  const target = graph.getNodeAttributes(edge.target);
4599
- if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4764
+ if (target.type !== import_types21.NodeType.DatabaseNode) continue;
4600
4765
  for (const pair of compatPairs()) {
4601
4766
  if (pair.engine !== target.engine) continue;
4602
4767
  const declared = deps[pair.driver];
@@ -4657,7 +4822,7 @@ function computeDivergences(graph, opts = {}) {
4657
4822
  }
4658
4823
  graph.forEachNode((nodeId, attrs) => {
4659
4824
  const n = attrs;
4660
- if (n.type !== import_types20.NodeType.ServiceNode) return;
4825
+ if (n.type !== import_types21.NodeType.ServiceNode) return;
4661
4826
  const svc = n;
4662
4827
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4663
4828
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4690,7 +4855,7 @@ function computeDivergences(graph, opts = {}) {
4690
4855
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4691
4856
  return a.target.localeCompare(b.target);
4692
4857
  });
4693
- return import_types20.DivergenceResultSchema.parse({
4858
+ return import_types21.DivergenceResultSchema.parse({
4694
4859
  divergences: filtered,
4695
4860
  totalAffected: filtered.length,
4696
4861
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4779,7 +4944,7 @@ init_cjs_shims();
4779
4944
  var import_node_fs20 = require("fs");
4780
4945
  var import_node_os2 = __toESM(require("os"), 1);
4781
4946
  var import_node_path33 = __toESM(require("path"), 1);
4782
- var import_types21 = require("@neat.is/types");
4947
+ var import_types22 = require("@neat.is/types");
4783
4948
  var LOCK_TIMEOUT_MS = 5e3;
4784
4949
  var LOCK_RETRY_MS = 50;
4785
4950
  function neatHome() {
@@ -4850,10 +5015,10 @@ async function readRegistry() {
4850
5015
  throw err;
4851
5016
  }
4852
5017
  const parsed = JSON.parse(raw);
4853
- return import_types21.RegistryFileSchema.parse(parsed);
5018
+ return import_types22.RegistryFileSchema.parse(parsed);
4854
5019
  }
4855
5020
  async function writeRegistry(reg) {
4856
- const validated = import_types21.RegistryFileSchema.parse(reg);
5021
+ const validated = import_types22.RegistryFileSchema.parse(reg);
4857
5022
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4858
5023
  }
4859
5024
  async function getProject(name) {
@@ -5075,11 +5240,11 @@ function registerRoutes(scope, ctx) {
5075
5240
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5076
5241
  const parsed = [];
5077
5242
  for (const c of candidates) {
5078
- const r = import_types22.DivergenceTypeSchema.safeParse(c);
5243
+ const r = import_types23.DivergenceTypeSchema.safeParse(c);
5079
5244
  if (!r.success) {
5080
5245
  return reply.code(400).send({
5081
5246
  error: `unknown divergence type "${c}"`,
5082
- allowed: import_types22.DivergenceTypeSchema.options
5247
+ allowed: import_types23.DivergenceTypeSchema.options
5083
5248
  });
5084
5249
  }
5085
5250
  parsed.push(r.data);
@@ -5292,7 +5457,7 @@ function registerRoutes(scope, ctx) {
5292
5457
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5293
5458
  let violations = await log.readAll();
5294
5459
  if (req2.query.severity) {
5295
- const sev = import_types22.PolicySeveritySchema.safeParse(req2.query.severity);
5460
+ const sev = import_types23.PolicySeveritySchema.safeParse(req2.query.severity);
5296
5461
  if (!sev.success) {
5297
5462
  return reply.code(400).send({
5298
5463
  error: "invalid severity",
@@ -5309,7 +5474,7 @@ function registerRoutes(scope, ctx) {
5309
5474
  scope.post("/policies/check", async (req2, reply) => {
5310
5475
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
5311
5476
  if (!proj) return;
5312
- const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
5477
+ const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
5313
5478
  if (!parsed.success) {
5314
5479
  return reply.code(400).send({
5315
5480
  error: "invalid /policies/check body",