@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/cli.cjs CHANGED
@@ -1649,6 +1649,86 @@ function hostFromUrl(u) {
1649
1649
  function pickAddress(span) {
1650
1650
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1651
1651
  }
1652
+ var CODE_FILEPATH_ATTR = "code.filepath";
1653
+ var CODE_LINENO_ATTR = "code.lineno";
1654
+ var CODE_FUNCTION_ATTR = "code.function";
1655
+ function toPosix(p) {
1656
+ return p.split("\\").join("/");
1657
+ }
1658
+ function languageForExt(relPath) {
1659
+ const dot = relPath.lastIndexOf(".");
1660
+ if (dot === -1) return void 0;
1661
+ switch (relPath.slice(dot).toLowerCase()) {
1662
+ case ".py":
1663
+ return "python";
1664
+ case ".ts":
1665
+ case ".tsx":
1666
+ return "typescript";
1667
+ case ".js":
1668
+ case ".jsx":
1669
+ case ".mjs":
1670
+ case ".cjs":
1671
+ return "javascript";
1672
+ default:
1673
+ return void 0;
1674
+ }
1675
+ }
1676
+ function relPathForRuntimeFile(filepath, serviceNode) {
1677
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1678
+ const root = serviceNode?.repoPath;
1679
+ if (root && root !== "." && root.length > 0) {
1680
+ const rootPosix = toPosix(root);
1681
+ const anchor = `/${rootPosix}/`;
1682
+ const idx = p.lastIndexOf(anchor);
1683
+ if (idx !== -1) return p.slice(idx + anchor.length);
1684
+ const base = rootPosix.split("/").filter(Boolean).pop();
1685
+ if (base) {
1686
+ const baseAnchor = `/${base}/`;
1687
+ const bidx = p.lastIndexOf(baseAnchor);
1688
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1689
+ }
1690
+ }
1691
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1692
+ return p.length > 0 ? p : null;
1693
+ }
1694
+ function callSiteFromSpan(span, serviceNode) {
1695
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1696
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1697
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1698
+ if (!relPath) return null;
1699
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1700
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1701
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1702
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1703
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1704
+ }
1705
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1706
+ const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
1707
+ if (!graph.hasNode(fileNodeId)) {
1708
+ const language = languageForExt(callSite.relPath);
1709
+ const node = {
1710
+ id: fileNodeId,
1711
+ type: import_types3.NodeType.FileNode,
1712
+ service: serviceName,
1713
+ path: callSite.relPath,
1714
+ ...language ? { language } : {},
1715
+ discoveredVia: "otel"
1716
+ };
1717
+ graph.addNode(fileNodeId, node);
1718
+ }
1719
+ const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1720
+ if (!graph.hasEdge(containsId)) {
1721
+ const edge = {
1722
+ id: containsId,
1723
+ source: serviceNodeId,
1724
+ target: fileNodeId,
1725
+ type: import_types3.EdgeType.CONTAINS,
1726
+ provenance: import_types3.Provenance.OBSERVED
1727
+ };
1728
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1729
+ }
1730
+ return fileNodeId;
1731
+ }
1652
1732
  function makeObservedEdgeId(type, source, target) {
1653
1733
  return (0, import_types3.observedEdgeId)(source, target, type);
1654
1734
  }
@@ -1891,6 +1971,9 @@ async function handleSpan(ctx, span) {
1891
1971
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1892
1972
  const isError = span.statusCode === 2;
1893
1973
  cacheSpanService(span, nowMs);
1974
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1975
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
1976
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1894
1977
  let affectedNode = sourceId;
1895
1978
  if (span.dbSystem) {
1896
1979
  const host = pickAddress(span);
@@ -1900,7 +1983,7 @@ async function handleSpan(ctx, span) {
1900
1983
  const result = upsertObservedEdge(
1901
1984
  ctx.graph,
1902
1985
  import_types3.EdgeType.CONNECTS_TO,
1903
- sourceId,
1986
+ observedSource(),
1904
1987
  targetId,
1905
1988
  ts,
1906
1989
  isError
@@ -1916,7 +1999,7 @@ async function handleSpan(ctx, span) {
1916
1999
  upsertObservedEdge(
1917
2000
  ctx.graph,
1918
2001
  import_types3.EdgeType.CALLS,
1919
- sourceId,
2002
+ observedSource(),
1920
2003
  targetId,
1921
2004
  ts,
1922
2005
  isError
@@ -1928,7 +2011,7 @@ async function handleSpan(ctx, span) {
1928
2011
  upsertObservedEdge(
1929
2012
  ctx.graph,
1930
2013
  import_types3.EdgeType.CALLS,
1931
- sourceId,
2014
+ observedSource(),
1932
2015
  frontierNodeId,
1933
2016
  ts,
1934
2017
  isError
@@ -3571,7 +3654,7 @@ async function addConfigNodes(graph, services, scanPath) {
3571
3654
 
3572
3655
  // src/extract/calls/index.ts
3573
3656
  init_cjs_shims();
3574
- var import_types14 = require("@neat.is/types");
3657
+ var import_types15 = require("@neat.is/types");
3575
3658
 
3576
3659
  // src/extract/calls/http.ts
3577
3660
  init_cjs_shims();
@@ -3579,12 +3662,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3579
3662
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3580
3663
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3581
3664
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3582
- var import_types9 = require("@neat.is/types");
3665
+ var import_types10 = require("@neat.is/types");
3583
3666
 
3584
3667
  // src/extract/calls/shared.ts
3585
3668
  init_cjs_shims();
3586
3669
  var import_node_fs13 = require("fs");
3587
3670
  var import_node_path19 = __toESM(require("path"), 1);
3671
+ var import_types9 = require("@neat.is/types");
3588
3672
  async function walkSourceFiles(dir) {
3589
3673
  const out = [];
3590
3674
  async function walk(current) {
@@ -3624,6 +3708,58 @@ function snippet(text, line) {
3624
3708
  const lines = text.split("\n");
3625
3709
  return (lines[line - 1] ?? "").trim();
3626
3710
  }
3711
+ function toPosix2(p) {
3712
+ return p.split("\\").join("/");
3713
+ }
3714
+ function languageForPath(relPath) {
3715
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3716
+ case ".py":
3717
+ return "python";
3718
+ case ".ts":
3719
+ case ".tsx":
3720
+ return "typescript";
3721
+ case ".js":
3722
+ case ".jsx":
3723
+ case ".mjs":
3724
+ case ".cjs":
3725
+ return "javascript";
3726
+ default:
3727
+ return void 0;
3728
+ }
3729
+ }
3730
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3731
+ let nodesAdded = 0;
3732
+ let edgesAdded = 0;
3733
+ const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3734
+ if (!graph.hasNode(fileNodeId)) {
3735
+ const language = languageForPath(relPath);
3736
+ const node = {
3737
+ id: fileNodeId,
3738
+ type: import_types9.NodeType.FileNode,
3739
+ service: serviceName,
3740
+ path: relPath,
3741
+ ...language ? { language } : {},
3742
+ discoveredVia: "static"
3743
+ };
3744
+ graph.addNode(fileNodeId, node);
3745
+ nodesAdded++;
3746
+ }
3747
+ const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3748
+ if (!graph.hasEdge(containsId)) {
3749
+ const edge = {
3750
+ id: containsId,
3751
+ source: serviceNodeId,
3752
+ target: fileNodeId,
3753
+ type: import_types9.EdgeType.CONTAINS,
3754
+ provenance: import_types9.Provenance.EXTRACTED,
3755
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3756
+ evidence: { file: relPath }
3757
+ };
3758
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3759
+ edgesAdded++;
3760
+ }
3761
+ return { fileNodeId, nodesAdded, edgesAdded };
3762
+ }
3627
3763
 
3628
3764
  // src/extract/calls/http.ts
3629
3765
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3657,16 +3793,16 @@ function callsFromSource(source, parser, knownHosts) {
3657
3793
  const tree = parser.parse(source);
3658
3794
  const literals = [];
3659
3795
  collectStringLiterals(tree.rootNode, literals);
3660
- const targets = /* @__PURE__ */ new Set();
3796
+ const out = [];
3661
3797
  for (const lit of literals) {
3662
3798
  if (isInsideJsxExternalLink(lit.node)) continue;
3663
3799
  for (const host of knownHosts) {
3664
3800
  if (urlMatchesHost(lit.text, host)) {
3665
- targets.add(host);
3801
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3666
3802
  }
3667
3803
  }
3668
3804
  }
3669
- return targets;
3805
+ return out;
3670
3806
  }
3671
3807
  function makeJsParser() {
3672
3808
  const p = new import_tree_sitter.default();
@@ -3689,71 +3825,78 @@ async function addHttpCallEdges(graph, services) {
3689
3825
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3690
3826
  hostToNodeId.set(service.pkg.name, service.node.id);
3691
3827
  }
3828
+ let nodesAdded = 0;
3692
3829
  let edgesAdded = 0;
3693
3830
  for (const service of services) {
3694
3831
  const files = await loadSourceFiles(service.dir);
3695
- const seenTargets = /* @__PURE__ */ new Map();
3832
+ const seen = /* @__PURE__ */ new Set();
3696
3833
  for (const file of files) {
3697
3834
  if (isTestPath(file.path)) continue;
3698
3835
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3699
- let targets;
3836
+ let sites;
3700
3837
  try {
3701
- targets = callsFromSource(file.content, parser, knownHosts);
3838
+ sites = callsFromSource(file.content, parser, knownHosts);
3702
3839
  } catch (err) {
3703
3840
  recordExtractionError("http call extraction", file.path, err);
3704
3841
  continue;
3705
3842
  }
3706
- for (const t of targets) {
3707
- const targetId = hostToNodeId.get(t);
3843
+ if (sites.length === 0) continue;
3844
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
3845
+ for (const site of sites) {
3846
+ const targetId = hostToNodeId.get(site.host);
3708
3847
  if (!targetId || targetId === service.node.id) continue;
3709
- if (!seenTargets.has(targetId)) {
3710
- seenTargets.set(targetId, { file: file.path, host: t });
3848
+ const dedupKey = `${relFile}|${targetId}`;
3849
+ if (seen.has(dedupKey)) continue;
3850
+ seen.add(dedupKey);
3851
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3852
+ const ev = {
3853
+ file: relFile,
3854
+ line: site.line,
3855
+ snippet: snippet(file.content, site.line)
3856
+ };
3857
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3858
+ noteExtractedDropped({
3859
+ source: service.node.id,
3860
+ target: targetId,
3861
+ type: import_types10.EdgeType.CALLS,
3862
+ confidence,
3863
+ confidenceKind: "hostname-shape-match",
3864
+ evidence: ev
3865
+ });
3866
+ continue;
3867
+ }
3868
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3869
+ graph,
3870
+ service.pkg.name,
3871
+ service.node.id,
3872
+ relFile
3873
+ );
3874
+ nodesAdded += n;
3875
+ edgesAdded += e;
3876
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
3877
+ if (!graph.hasEdge(edgeId)) {
3878
+ const edge = {
3879
+ id: edgeId,
3880
+ source: fileNodeId,
3881
+ target: targetId,
3882
+ type: import_types10.EdgeType.CALLS,
3883
+ provenance: import_types10.Provenance.EXTRACTED,
3884
+ confidence,
3885
+ evidence: ev
3886
+ };
3887
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3888
+ edgesAdded++;
3711
3889
  }
3712
- }
3713
- }
3714
- for (const [targetId, evidenceFile] of seenTargets) {
3715
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3716
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3717
- const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3718
- const ev = {
3719
- file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3720
- line,
3721
- snippet: snippet(fileContent, line)
3722
- };
3723
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3724
- if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3725
- noteExtractedDropped({
3726
- source: service.node.id,
3727
- target: targetId,
3728
- type: import_types9.EdgeType.CALLS,
3729
- confidence,
3730
- confidenceKind: "hostname-shape-match",
3731
- evidence: ev
3732
- });
3733
- continue;
3734
- }
3735
- const edge = {
3736
- id: edgeId,
3737
- source: service.node.id,
3738
- target: targetId,
3739
- type: import_types9.EdgeType.CALLS,
3740
- provenance: import_types9.Provenance.EXTRACTED,
3741
- confidence,
3742
- evidence: ev
3743
- };
3744
- if (!graph.hasEdge(edge.id)) {
3745
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3746
- edgesAdded++;
3747
3890
  }
3748
3891
  }
3749
3892
  }
3750
- return edgesAdded;
3893
+ return { nodesAdded, edgesAdded };
3751
3894
  }
3752
3895
 
3753
3896
  // src/extract/calls/kafka.ts
3754
3897
  init_cjs_shims();
3755
3898
  var import_node_path21 = __toESM(require("path"), 1);
3756
- var import_types10 = require("@neat.is/types");
3899
+ var import_types11 = require("@neat.is/types");
3757
3900
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3758
3901
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3759
3902
  function findAll(re, text) {
@@ -3774,7 +3917,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3774
3917
  seen.add(key);
3775
3918
  const line = lineOf(file.content, topic);
3776
3919
  out.push({
3777
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3920
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3778
3921
  name: topic,
3779
3922
  kind: "kafka-topic",
3780
3923
  edgeType,
@@ -3797,7 +3940,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3797
3940
  // src/extract/calls/redis.ts
3798
3941
  init_cjs_shims();
3799
3942
  var import_node_path22 = __toESM(require("path"), 1);
3800
- var import_types11 = require("@neat.is/types");
3943
+ var import_types12 = require("@neat.is/types");
3801
3944
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3802
3945
  function redisEndpointsFromFile(file, serviceDir) {
3803
3946
  const out = [];
@@ -3810,7 +3953,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3810
3953
  seen.add(host);
3811
3954
  const line = lineOf(file.content, host);
3812
3955
  out.push({
3813
- infraId: (0, import_types11.infraId)("redis", host),
3956
+ infraId: (0, import_types12.infraId)("redis", host),
3814
3957
  name: host,
3815
3958
  kind: "redis",
3816
3959
  edgeType: "CALLS",
@@ -3831,7 +3974,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3831
3974
  // src/extract/calls/aws.ts
3832
3975
  init_cjs_shims();
3833
3976
  var import_node_path23 = __toESM(require("path"), 1);
3834
- var import_types12 = require("@neat.is/types");
3977
+ var import_types13 = require("@neat.is/types");
3835
3978
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3836
3979
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3837
3980
  function hasMarker(text, markers) {
@@ -3855,7 +3998,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3855
3998
  seen.add(key);
3856
3999
  const line = lineOf(file.content, name);
3857
4000
  out.push({
3858
- infraId: (0, import_types12.infraId)(kind, name),
4001
+ infraId: (0, import_types13.infraId)(kind, name),
3859
4002
  name,
3860
4003
  kind,
3861
4004
  edgeType: "CALLS",
@@ -3890,7 +4033,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3890
4033
  // src/extract/calls/grpc.ts
3891
4034
  init_cjs_shims();
3892
4035
  var import_node_path24 = __toESM(require("path"), 1);
3893
- var import_types13 = require("@neat.is/types");
4036
+ var import_types14 = require("@neat.is/types");
3894
4037
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3895
4038
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3896
4039
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -3938,7 +4081,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3938
4081
  const { kind } = classified;
3939
4082
  const line = lineOf(file.content, m[0]);
3940
4083
  out.push({
3941
- infraId: (0, import_types13.infraId)(kind, name),
4084
+ infraId: (0, import_types14.infraId)(kind, name),
3942
4085
  name,
3943
4086
  kind,
3944
4087
  edgeType: "CALLS",
@@ -3960,11 +4103,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3960
4103
  function edgeTypeFromEndpoint(ep) {
3961
4104
  switch (ep.edgeType) {
3962
4105
  case "PUBLISHES_TO":
3963
- return import_types14.EdgeType.PUBLISHES_TO;
4106
+ return import_types15.EdgeType.PUBLISHES_TO;
3964
4107
  case "CONSUMES_FROM":
3965
- return import_types14.EdgeType.CONSUMES_FROM;
4108
+ return import_types15.EdgeType.CONSUMES_FROM;
3966
4109
  default:
3967
- return import_types14.EdgeType.CALLS;
4110
+ return import_types15.EdgeType.CALLS;
3968
4111
  }
3969
4112
  }
3970
4113
  function isAwsKind(kind) {
@@ -3991,7 +4134,7 @@ async function addExternalEndpointEdges(graph, services) {
3991
4134
  if (!graph.hasNode(ep.infraId)) {
3992
4135
  const node = {
3993
4136
  id: ep.infraId,
3994
- type: import_types14.NodeType.InfraNode,
4137
+ type: import_types15.NodeType.InfraNode,
3995
4138
  name: ep.name,
3996
4139
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3997
4140
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4003,11 +4146,8 @@ async function addExternalEndpointEdges(graph, services) {
4003
4146
  nodesAdded++;
4004
4147
  }
4005
4148
  const edgeType = edgeTypeFromEndpoint(ep);
4006
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
4007
- if (seenEdges.has(edgeId)) continue;
4008
- seenEdges.add(edgeId);
4009
- const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
4010
- if (!(0, import_types14.passesExtractedFloor)(confidence)) {
4149
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4150
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4011
4151
  noteExtractedDropped({
4012
4152
  source: service.node.id,
4013
4153
  target: ep.infraId,
@@ -4018,13 +4158,25 @@ async function addExternalEndpointEdges(graph, services) {
4018
4158
  });
4019
4159
  continue;
4020
4160
  }
4161
+ const relFile = toPosix2(ep.evidence.file);
4162
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4163
+ graph,
4164
+ service.pkg.name,
4165
+ service.node.id,
4166
+ relFile
4167
+ );
4168
+ nodesAdded += n;
4169
+ edgesAdded += e;
4170
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4171
+ if (seenEdges.has(edgeId)) continue;
4172
+ seenEdges.add(edgeId);
4021
4173
  if (!graph.hasEdge(edgeId)) {
4022
4174
  const edge = {
4023
4175
  id: edgeId,
4024
- source: service.node.id,
4176
+ source: fileNodeId,
4025
4177
  target: ep.infraId,
4026
4178
  type: edgeType,
4027
- provenance: import_types14.Provenance.EXTRACTED,
4179
+ provenance: import_types15.Provenance.EXTRACTED,
4028
4180
  confidence,
4029
4181
  evidence: ep.evidence
4030
4182
  };
@@ -4036,11 +4188,11 @@ async function addExternalEndpointEdges(graph, services) {
4036
4188
  return { nodesAdded, edgesAdded };
4037
4189
  }
4038
4190
  async function addCallEdges(graph, services) {
4039
- const httpEdges = await addHttpCallEdges(graph, services);
4191
+ const http2 = await addHttpCallEdges(graph, services);
4040
4192
  const ext = await addExternalEndpointEdges(graph, services);
4041
4193
  return {
4042
- nodesAdded: ext.nodesAdded,
4043
- edgesAdded: httpEdges + ext.edgesAdded
4194
+ nodesAdded: http2.nodesAdded + ext.nodesAdded,
4195
+ edgesAdded: http2.edgesAdded + ext.edgesAdded
4044
4196
  };
4045
4197
  }
4046
4198
 
@@ -4050,15 +4202,15 @@ init_cjs_shims();
4050
4202
  // src/extract/infra/docker-compose.ts
4051
4203
  init_cjs_shims();
4052
4204
  var import_node_path25 = __toESM(require("path"), 1);
4053
- var import_types16 = require("@neat.is/types");
4205
+ var import_types17 = require("@neat.is/types");
4054
4206
 
4055
4207
  // src/extract/infra/shared.ts
4056
4208
  init_cjs_shims();
4057
- var import_types15 = require("@neat.is/types");
4209
+ var import_types16 = require("@neat.is/types");
4058
4210
  function makeInfraNode(kind, name, provider = "self", extras) {
4059
4211
  return {
4060
- id: (0, import_types15.infraId)(kind, name),
4061
- type: import_types15.NodeType.InfraNode,
4212
+ id: (0, import_types16.infraId)(kind, name),
4213
+ type: import_types16.NodeType.InfraNode,
4062
4214
  name,
4063
4215
  provider,
4064
4216
  kind,
@@ -4137,15 +4289,15 @@ async function addComposeInfra(graph, scanPath, services) {
4137
4289
  for (const dep of dependsOnList(svc.depends_on)) {
4138
4290
  const targetId = composeNameToNodeId.get(dep);
4139
4291
  if (!targetId) continue;
4140
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
4292
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4141
4293
  if (graph.hasEdge(edgeId)) continue;
4142
4294
  const edge = {
4143
4295
  id: edgeId,
4144
4296
  source: sourceId,
4145
4297
  target: targetId,
4146
- type: import_types16.EdgeType.DEPENDS_ON,
4147
- provenance: import_types16.Provenance.EXTRACTED,
4148
- confidence: (0, import_types16.confidenceForExtracted)("structural"),
4298
+ type: import_types17.EdgeType.DEPENDS_ON,
4299
+ provenance: import_types17.Provenance.EXTRACTED,
4300
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
4149
4301
  evidence: { file: evidenceFile }
4150
4302
  };
4151
4303
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4159,7 +4311,7 @@ async function addComposeInfra(graph, scanPath, services) {
4159
4311
  init_cjs_shims();
4160
4312
  var import_node_path26 = __toESM(require("path"), 1);
4161
4313
  var import_node_fs14 = require("fs");
4162
- var import_types17 = require("@neat.is/types");
4314
+ var import_types18 = require("@neat.is/types");
4163
4315
  function runtimeImage(content) {
4164
4316
  const lines = content.split("\n");
4165
4317
  let last = null;
@@ -4198,15 +4350,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4198
4350
  graph.addNode(node.id, node);
4199
4351
  nodesAdded++;
4200
4352
  }
4201
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
4353
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4202
4354
  if (!graph.hasEdge(edgeId)) {
4203
4355
  const edge = {
4204
4356
  id: edgeId,
4205
4357
  source: service.node.id,
4206
4358
  target: node.id,
4207
- type: import_types17.EdgeType.RUNS_ON,
4208
- provenance: import_types17.Provenance.EXTRACTED,
4209
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4359
+ type: import_types18.EdgeType.RUNS_ON,
4360
+ provenance: import_types18.Provenance.EXTRACTED,
4361
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4210
4362
  evidence: {
4211
4363
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4212
4364
  }
@@ -4334,17 +4486,29 @@ var import_node_path30 = __toESM(require("path"), 1);
4334
4486
  init_cjs_shims();
4335
4487
  var import_node_fs17 = require("fs");
4336
4488
  var import_node_path29 = __toESM(require("path"), 1);
4337
- var import_types18 = require("@neat.is/types");
4489
+ var import_types19 = require("@neat.is/types");
4490
+ function dropOrphanedFileNodes(graph) {
4491
+ const orphans = [];
4492
+ graph.forEachNode((id, attrs) => {
4493
+ if (attrs.type !== import_types19.NodeType.FileNode) return;
4494
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4495
+ orphans.push(id);
4496
+ }
4497
+ });
4498
+ for (const id of orphans) graph.dropNode(id);
4499
+ return orphans.length;
4500
+ }
4338
4501
  function retireEdgesByFile(graph, file) {
4339
4502
  const normalized = file.split("\\").join("/");
4340
4503
  const toDrop = [];
4341
4504
  graph.forEachEdge((id, attrs) => {
4342
4505
  const edge = attrs;
4343
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4506
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4344
4507
  if (!edge.evidence?.file) return;
4345
4508
  if (edge.evidence.file === normalized) toDrop.push(id);
4346
4509
  });
4347
4510
  for (const id of toDrop) graph.dropEdge(id);
4511
+ dropOrphanedFileNodes(graph);
4348
4512
  return toDrop.length;
4349
4513
  }
4350
4514
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
@@ -4352,7 +4516,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4352
4516
  const bases = [scanPath, ...serviceDirs];
4353
4517
  graph.forEachEdge((id, attrs) => {
4354
4518
  const edge = attrs;
4355
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4519
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4356
4520
  const evidenceFile = edge.evidence?.file;
4357
4521
  if (!evidenceFile) return;
4358
4522
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4363,6 +4527,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4363
4527
  if (!found) toDrop.push(id);
4364
4528
  });
4365
4529
  for (const id of toDrop) graph.dropEdge(id);
4530
+ dropOrphanedFileNodes(graph);
4366
4531
  return toDrop.length;
4367
4532
  }
4368
4533
 
@@ -4430,7 +4595,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4430
4595
 
4431
4596
  // src/divergences.ts
4432
4597
  init_cjs_shims();
4433
- var import_types19 = require("@neat.is/types");
4598
+ var import_types20 = require("@neat.is/types");
4434
4599
  function bucketKey(source, target, type) {
4435
4600
  return `${type}|${source}|${target}`;
4436
4601
  }
@@ -4438,22 +4603,22 @@ function bucketEdges(graph) {
4438
4603
  const buckets = /* @__PURE__ */ new Map();
4439
4604
  graph.forEachEdge((id, attrs) => {
4440
4605
  const e = attrs;
4441
- const parsed = (0, import_types19.parseEdgeId)(id);
4606
+ const parsed = (0, import_types20.parseEdgeId)(id);
4442
4607
  const provenance = parsed?.provenance ?? e.provenance;
4443
4608
  const key = bucketKey(e.source, e.target, e.type);
4444
4609
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4445
4610
  switch (provenance) {
4446
- case import_types19.Provenance.EXTRACTED:
4611
+ case import_types20.Provenance.EXTRACTED:
4447
4612
  cur.extracted = e;
4448
4613
  break;
4449
- case import_types19.Provenance.OBSERVED:
4614
+ case import_types20.Provenance.OBSERVED:
4450
4615
  cur.observed = e;
4451
4616
  break;
4452
- case import_types19.Provenance.INFERRED:
4617
+ case import_types20.Provenance.INFERRED:
4453
4618
  cur.inferred = e;
4454
4619
  break;
4455
4620
  default:
4456
- if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4621
+ if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4457
4622
  }
4458
4623
  buckets.set(key, cur);
4459
4624
  });
@@ -4462,7 +4627,7 @@ function bucketEdges(graph) {
4462
4627
  function nodeIsFrontier(graph, nodeId) {
4463
4628
  if (!graph.hasNode(nodeId)) return false;
4464
4629
  const attrs = graph.getNodeAttributes(nodeId);
4465
- return attrs.type === import_types19.NodeType.FrontierNode;
4630
+ return attrs.type === import_types20.NodeType.FrontierNode;
4466
4631
  }
4467
4632
  function clampConfidence(n) {
4468
4633
  if (!Number.isFinite(n)) return 0;
@@ -4483,6 +4648,7 @@ function gradedConfidence(edge) {
4483
4648
  }
4484
4649
  function detectMissingDivergences(graph, bucket) {
4485
4650
  const out = [];
4651
+ if (bucket.type === import_types20.EdgeType.CONTAINS) return out;
4486
4652
  if (bucket.extracted && !bucket.observed) {
4487
4653
  if (!nodeIsFrontier(graph, bucket.target)) {
4488
4654
  out.push({
@@ -4523,7 +4689,7 @@ function declaredHostFor(svc) {
4523
4689
  function hasExtractedConfiguredBy(graph, svcId) {
4524
4690
  for (const edgeId of graph.outboundEdges(svcId)) {
4525
4691
  const e = graph.getEdgeAttributes(edgeId);
4526
- if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4692
+ if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4527
4693
  return true;
4528
4694
  }
4529
4695
  }
@@ -4536,10 +4702,10 @@ function detectHostMismatch(graph, svcId, svc) {
4536
4702
  const out = [];
4537
4703
  for (const edgeId of graph.outboundEdges(svcId)) {
4538
4704
  const edge = graph.getEdgeAttributes(edgeId);
4539
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4540
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4705
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4706
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4541
4707
  const target = graph.getNodeAttributes(edge.target);
4542
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4708
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4543
4709
  const observedHost = target.host?.trim();
4544
4710
  if (!observedHost) continue;
4545
4711
  if (observedHost === declaredHost) continue;
@@ -4561,10 +4727,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4561
4727
  const deps = svc.dependencies ?? {};
4562
4728
  for (const edgeId of graph.outboundEdges(svcId)) {
4563
4729
  const edge = graph.getEdgeAttributes(edgeId);
4564
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4565
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4730
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4731
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4566
4732
  const target = graph.getNodeAttributes(edge.target);
4567
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4733
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4568
4734
  for (const pair of compatPairs()) {
4569
4735
  if (pair.engine !== target.engine) continue;
4570
4736
  const declared = deps[pair.driver];
@@ -4625,7 +4791,7 @@ function computeDivergences(graph, opts = {}) {
4625
4791
  }
4626
4792
  graph.forEachNode((nodeId, attrs) => {
4627
4793
  const n = attrs;
4628
- if (n.type !== import_types19.NodeType.ServiceNode) return;
4794
+ if (n.type !== import_types20.NodeType.ServiceNode) return;
4629
4795
  const svc = n;
4630
4796
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4631
4797
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4658,7 +4824,7 @@ function computeDivergences(graph, opts = {}) {
4658
4824
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4659
4825
  return a.target.localeCompare(b.target);
4660
4826
  });
4661
- return import_types19.DivergenceResultSchema.parse({
4827
+ return import_types20.DivergenceResultSchema.parse({
4662
4828
  divergences: filtered,
4663
4829
  totalAffected: filtered.length,
4664
4830
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4669,7 +4835,7 @@ function computeDivergences(graph, opts = {}) {
4669
4835
  init_cjs_shims();
4670
4836
  var import_node_fs18 = require("fs");
4671
4837
  var import_node_path31 = __toESM(require("path"), 1);
4672
- var import_types20 = require("@neat.is/types");
4838
+ var import_types21 = require("@neat.is/types");
4673
4839
  var SCHEMA_VERSION = 4;
4674
4840
  function migrateV1ToV2(payload) {
4675
4841
  const nodes = payload.graph.nodes;
@@ -4691,12 +4857,12 @@ function migrateV2ToV3(payload) {
4691
4857
  for (const edge of edges) {
4692
4858
  const attrs = edge.attributes;
4693
4859
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4694
- attrs.provenance = import_types20.Provenance.OBSERVED;
4860
+ attrs.provenance = import_types21.Provenance.OBSERVED;
4695
4861
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4696
4862
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4697
4863
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4698
4864
  if (type && source && target) {
4699
- const newId = (0, import_types20.observedEdgeId)(source, target, type);
4865
+ const newId = (0, import_types21.observedEdgeId)(source, target, type);
4700
4866
  attrs.id = newId;
4701
4867
  if (edge.key) edge.key = newId;
4702
4868
  }
@@ -4816,7 +4982,7 @@ ${NEAT_OUT_LINE}
4816
4982
 
4817
4983
  // src/summary.ts
4818
4984
  init_cjs_shims();
4819
- var import_types21 = require("@neat.is/types");
4985
+ var import_types22 = require("@neat.is/types");
4820
4986
  function renderOtelEnvBlock() {
4821
4987
  return [
4822
4988
  "for prod OTel routing, set these in your deploy platform's env:",
@@ -4826,19 +4992,19 @@ function renderOtelEnvBlock() {
4826
4992
  }
4827
4993
  function findIncompatServices(nodes) {
4828
4994
  return nodes.filter(
4829
- (n) => n.type === import_types21.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
4995
+ (n) => n.type === import_types22.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
4830
4996
  );
4831
4997
  }
4832
4998
  function servicesWithoutObserved(nodes, edges) {
4833
4999
  const seen = /* @__PURE__ */ new Set();
4834
5000
  for (const e of edges) {
4835
- if (e.provenance === import_types21.Provenance.OBSERVED) {
5001
+ if (e.provenance === import_types22.Provenance.OBSERVED) {
4836
5002
  seen.add(e.source);
4837
5003
  seen.add(e.target);
4838
5004
  }
4839
5005
  }
4840
5006
  return nodes.filter(
4841
- (n) => n.type === import_types21.NodeType.ServiceNode && !seen.has(n.id)
5007
+ (n) => n.type === import_types22.NodeType.ServiceNode && !seen.has(n.id)
4842
5008
  );
4843
5009
  }
4844
5010
  function formatDivergence(d) {
@@ -4920,7 +5086,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
4920
5086
  init_cjs_shims();
4921
5087
  var import_fastify = __toESM(require("fastify"), 1);
4922
5088
  var import_cors = __toESM(require("@fastify/cors"), 1);
4923
- var import_types23 = require("@neat.is/types");
5089
+ var import_types24 = require("@neat.is/types");
4924
5090
 
4925
5091
  // src/diff.ts
4926
5092
  init_cjs_shims();
@@ -5056,7 +5222,7 @@ init_cjs_shims();
5056
5222
  var import_node_fs21 = require("fs");
5057
5223
  var import_node_os2 = __toESM(require("os"), 1);
5058
5224
  var import_node_path34 = __toESM(require("path"), 1);
5059
- var import_types22 = require("@neat.is/types");
5225
+ var import_types23 = require("@neat.is/types");
5060
5226
  var LOCK_TIMEOUT_MS = 5e3;
5061
5227
  var LOCK_RETRY_MS = 50;
5062
5228
  function neatHome() {
@@ -5135,10 +5301,10 @@ async function readRegistry() {
5135
5301
  throw err;
5136
5302
  }
5137
5303
  const parsed = JSON.parse(raw);
5138
- return import_types22.RegistryFileSchema.parse(parsed);
5304
+ return import_types23.RegistryFileSchema.parse(parsed);
5139
5305
  }
5140
5306
  async function writeRegistry(reg) {
5141
- const validated = import_types22.RegistryFileSchema.parse(reg);
5307
+ const validated = import_types23.RegistryFileSchema.parse(reg);
5142
5308
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5143
5309
  }
5144
5310
  var ProjectNameCollisionError = class extends Error {
@@ -5401,11 +5567,11 @@ function registerRoutes(scope, ctx) {
5401
5567
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5402
5568
  const parsed = [];
5403
5569
  for (const c of candidates) {
5404
- const r = import_types23.DivergenceTypeSchema.safeParse(c);
5570
+ const r = import_types24.DivergenceTypeSchema.safeParse(c);
5405
5571
  if (!r.success) {
5406
5572
  return reply.code(400).send({
5407
5573
  error: `unknown divergence type "${c}"`,
5408
- allowed: import_types23.DivergenceTypeSchema.options
5574
+ allowed: import_types24.DivergenceTypeSchema.options
5409
5575
  });
5410
5576
  }
5411
5577
  parsed.push(r.data);
@@ -5618,7 +5784,7 @@ function registerRoutes(scope, ctx) {
5618
5784
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5619
5785
  let violations = await log.readAll();
5620
5786
  if (req.query.severity) {
5621
- const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5787
+ const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
5622
5788
  if (!sev.success) {
5623
5789
  return reply.code(400).send({
5624
5790
  error: "invalid severity",
@@ -5635,7 +5801,7 @@ function registerRoutes(scope, ctx) {
5635
5801
  scope.post("/policies/check", async (req, reply) => {
5636
5802
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5637
5803
  if (!proj) return;
5638
- const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5804
+ const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5639
5805
  if (!parsed.success) {
5640
5806
  return reply.code(400).send({
5641
5807
  error: "invalid /policies/check body",
@@ -6569,38 +6735,119 @@ var import_node_path40 = __toESM(require("path"), 1);
6569
6735
  // src/installers/templates.ts
6570
6736
  init_cjs_shims();
6571
6737
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
6738
+ var OTEL_INIT_STAMP = "// neat-template-version: 2 \u2014 file-first call-site capture (ADR-089).";
6739
+ function neatCallsiteProcessorSource(ts) {
6740
+ const stackT = ts ? ": string" : "";
6741
+ const spanT = ts ? ": any" : "";
6742
+ const strOpt = ts ? ": string | undefined" : "";
6743
+ return `function __neatPickUserFrame(stack${stackT}) {
6744
+ const lines = String(stack || '').split('\\n')
6745
+ for (let i = 0; i < lines.length; i++) {
6746
+ const raw = lines[i].trim()
6747
+ if (raw.indexOf('at ') !== 0) continue
6748
+ if (raw.indexOf('node_modules') !== -1) continue
6749
+ if (raw.indexOf('@opentelemetry') !== -1) continue
6750
+ if (raw.indexOf('node:') !== -1) continue
6751
+ if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
6752
+ const bodyText = raw.slice(3)
6753
+ const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
6754
+ if (!loc) continue
6755
+ const paren = bodyText.lastIndexOf('(')
6756
+ let filepath${strOpt}
6757
+ let fn${strOpt}
6758
+ if (paren !== -1) {
6759
+ fn = bodyText.slice(0, paren).trim()
6760
+ if (fn.indexOf('async ') === 0) fn = fn.slice(6)
6761
+ if (fn.indexOf('new ') === 0) fn = fn.slice(4)
6762
+ filepath = bodyText.slice(paren + 1, bodyText.length - loc[0].length)
6763
+ } else {
6764
+ filepath = bodyText.slice(0, bodyText.length - loc[0].length)
6765
+ }
6766
+ if (filepath.indexOf('file://') === 0) filepath = filepath.slice(7)
6767
+ if (!filepath) continue
6768
+ return { filepath: filepath, lineno: Number(loc[1]), function: fn || undefined }
6769
+ }
6770
+ return null
6771
+ }
6772
+
6773
+ class NeatCallSiteSpanProcessor {
6774
+ onStart(span${spanT}) {
6775
+ if (!span || (span.kind !== 2 && span.kind !== 3)) return
6776
+ const frame = __neatPickUserFrame(new Error().stack)
6777
+ if (!frame) return
6778
+ span.setAttribute('code.filepath', frame.filepath)
6779
+ if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
6780
+ if (frame.function) span.setAttribute('code.function', frame.function)
6781
+ }
6782
+ onEnd() {}
6783
+ forceFlush() { return Promise.resolve() }
6784
+ shutdown() { return Promise.resolve() }
6785
+ }`;
6786
+ }
6787
+ var CALLSITE_PROCESSOR_JS = neatCallsiteProcessorSource(false);
6788
+ var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
6789
+ function neatRegisterCallsiteSource(ts) {
6790
+ const providerT = ts ? ": any" : "";
6791
+ return `try {
6792
+ const provider${providerT} = trace.getTracerProvider()
6793
+ const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
6794
+ if (delegate && typeof delegate.addSpanProcessor === 'function') {
6795
+ delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
6796
+ }
6797
+ } catch {
6798
+ // capture is best-effort; span export is unaffected
6799
+ }`;
6800
+ }
6572
6801
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
6802
+ ${OTEL_INIT_STAMP}
6573
6803
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6574
6804
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6575
6805
 
6576
6806
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6577
6807
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6808
+ const { trace } = require('@opentelemetry/api')
6809
+
6810
+ ${CALLSITE_PROCESSOR_JS}
6578
6811
 
6579
6812
  const instrumentations = [getNodeAutoInstrumentations()]
6580
6813
  __INSTRUMENTATION_BLOCK__
6581
- new NodeSDK({ instrumentations }).start()
6814
+ const sdk = new NodeSDK({ instrumentations })
6815
+ sdk.start()
6816
+ ${neatRegisterCallsiteSource(false)}
6582
6817
  `;
6583
6818
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
6819
+ ${OTEL_INIT_STAMP}
6584
6820
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6585
6821
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6586
6822
 
6587
6823
  import { NodeSDK } from '@opentelemetry/sdk-node'
6588
6824
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6825
+ import { trace } from '@opentelemetry/api'
6826
+
6827
+ ${CALLSITE_PROCESSOR_JS}
6589
6828
 
6590
6829
  const instrumentations = [getNodeAutoInstrumentations()]
6591
6830
  __INSTRUMENTATION_BLOCK__
6592
- new NodeSDK({ instrumentations }).start()
6831
+ const sdk = new NodeSDK({ instrumentations })
6832
+ sdk.start()
6833
+ ${neatRegisterCallsiteSource(false)}
6593
6834
  `;
6594
6835
  var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
6836
+ ${OTEL_INIT_STAMP}
6595
6837
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6596
6838
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6597
6839
 
6598
6840
  import { NodeSDK } from '@opentelemetry/sdk-node'
6599
6841
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6842
+ import { trace } from '@opentelemetry/api'
6843
+
6844
+ ${CALLSITE_PROCESSOR_TS}
6600
6845
 
6601
6846
  const instrumentations = [getNodeAutoInstrumentations()]
6602
6847
  __INSTRUMENTATION_BLOCK__
6603
- new NodeSDK({ instrumentations }).start()
6848
+ const sdk = new NodeSDK({ instrumentations })
6849
+ sdk.start()
6850
+ ${neatRegisterCallsiteSource(true)}
6604
6851
  `;
6605
6852
  function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
6606
6853
  const block = registrations.length === 0 ? "" : `
@@ -6824,6 +7071,23 @@ async function exists2(p) {
6824
7071
  return false;
6825
7072
  }
6826
7073
  }
7074
+ async function readFileMaybe(p) {
7075
+ try {
7076
+ return await import_node_fs25.promises.readFile(p, "utf8");
7077
+ } catch {
7078
+ return null;
7079
+ }
7080
+ }
7081
+ async function planOtelInitGeneration(file, contents) {
7082
+ const existing = await readFileMaybe(file);
7083
+ if (existing === null) {
7084
+ return { file, contents, skipIfExists: true };
7085
+ }
7086
+ if (existing.includes(OTEL_INIT_HEADER) && !existing.includes(OTEL_INIT_STAMP)) {
7087
+ return { file, contents, skipIfExists: false };
7088
+ }
7089
+ return null;
7090
+ }
6827
7091
  async function detect(serviceDir) {
6828
7092
  const pkg = await readPackageJson(serviceDir);
6829
7093
  return pkg !== null && typeof pkg.name === "string";
@@ -7554,18 +7818,11 @@ async function plan(serviceDir, opts) {
7554
7818
  const projectName = projectToken(pkg, serviceDir, project);
7555
7819
  const registrations = nonBundled.map((i) => i.registration);
7556
7820
  const generatedFiles = [];
7557
- if (!await exists2(otelInitFile)) {
7558
- generatedFiles.push({
7559
- file: otelInitFile,
7560
- contents: renderNodeOtelInit(
7561
- otelInitContents(flavor),
7562
- svcName,
7563
- projectName,
7564
- registrations
7565
- ),
7566
- skipIfExists: true
7567
- });
7568
- }
7821
+ const otelInitGen = await planOtelInitGeneration(
7822
+ otelInitFile,
7823
+ renderNodeOtelInit(otelInitContents(flavor), svcName, projectName, registrations)
7824
+ );
7825
+ if (otelInitGen) generatedFiles.push(otelInitGen);
7569
7826
  if (!await exists2(envNeatFile)) {
7570
7827
  generatedFiles.push({
7571
7828
  file: envNeatFile,
@@ -8608,7 +8865,7 @@ var import_node_path44 = __toESM(require("path"), 1);
8608
8865
 
8609
8866
  // src/cli-client.ts
8610
8867
  init_cjs_shims();
8611
- var import_types24 = require("@neat.is/types");
8868
+ var import_types25 = require("@neat.is/types");
8612
8869
  var HttpError = class extends Error {
8613
8870
  constructor(status2, message, responseBody = "") {
8614
8871
  super(message);
@@ -8746,7 +9003,7 @@ async function runBlastRadius(client, input) {
8746
9003
  }
8747
9004
  }
8748
9005
  function formatBlastEntry(n) {
8749
- const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
9006
+ const tag = n.edgeProvenance === import_types25.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
8750
9007
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
8751
9008
  }
8752
9009
  async function runDependencies(client, input) {
@@ -8792,9 +9049,9 @@ async function runObservedDependencies(client, input) {
8792
9049
  const edges = await client.get(
8793
9050
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
8794
9051
  );
8795
- const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
9052
+ const observed = edges.outbound.filter((e) => e.provenance === import_types25.Provenance.OBSERVED);
8796
9053
  if (observed.length === 0) {
8797
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
9054
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types25.Provenance.EXTRACTED);
8798
9055
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
8799
9056
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
8800
9057
  }
@@ -8802,7 +9059,7 @@ async function runObservedDependencies(client, input) {
8802
9059
  return {
8803
9060
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
8804
9061
  block: blockLines.join("\n"),
8805
- provenance: import_types24.Provenance.OBSERVED
9062
+ provenance: import_types25.Provenance.OBSERVED
8806
9063
  };
8807
9064
  } catch (err) {
8808
9065
  if (err instanceof HttpError && err.status === 404) {
@@ -8856,7 +9113,7 @@ async function runIncidents(client, input) {
8856
9113
  return {
8857
9114
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
8858
9115
  block: blockLines.join("\n"),
8859
- provenance: import_types24.Provenance.OBSERVED
9116
+ provenance: import_types25.Provenance.OBSERVED
8860
9117
  };
8861
9118
  } catch (err) {
8862
9119
  if (err instanceof HttpError && err.status === 404) {
@@ -8965,7 +9222,7 @@ async function runStaleEdges(client, input) {
8965
9222
  return {
8966
9223
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
8967
9224
  block: blockLines.join("\n"),
8968
- provenance: import_types24.Provenance.STALE
9225
+ provenance: import_types25.Provenance.STALE
8969
9226
  };
8970
9227
  }
8971
9228
  async function runPolicies(client, input) {
@@ -9279,7 +9536,7 @@ async function runSync(opts) {
9279
9536
  }
9280
9537
 
9281
9538
  // src/cli.ts
9282
- var import_types25 = require("@neat.is/types");
9539
+ var import_types26 = require("@neat.is/types");
9283
9540
  function usage() {
9284
9541
  console.log("usage: neat <command> [args] [--project <name>]");
9285
9542
  console.log("");
@@ -10112,10 +10369,10 @@ async function runQueryVerb(cmd, parsed) {
10112
10369
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
10113
10370
  const out = [];
10114
10371
  for (const p of parts) {
10115
- const r = import_types25.DivergenceTypeSchema.safeParse(p);
10372
+ const r = import_types26.DivergenceTypeSchema.safeParse(p);
10116
10373
  if (!r.success) {
10117
10374
  console.error(
10118
- `neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
10375
+ `neat divergences: unknown --type "${p}". allowed: ${import_types26.DivergenceTypeSchema.options.join(", ")}`
10119
10376
  );
10120
10377
  return 2;
10121
10378
  }