@neat.is/core 0.4.10 → 0.4.11

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.d.cts CHANGED
@@ -233,6 +233,7 @@ declare function startOtelGrpcReceiver(opts: BuildOtelGrpcReceiverOptions & {
233
233
  interface IngestContext {
234
234
  graph: NeatGraph;
235
235
  errorsPath: string;
236
+ scanPath?: string;
236
237
  project?: string;
237
238
  now?: () => number;
238
239
  writeErrorEventInline?: boolean;
package/dist/index.d.ts CHANGED
@@ -233,6 +233,7 @@ declare function startOtelGrpcReceiver(opts: BuildOtelGrpcReceiverOptions & {
233
233
  interface IngestContext {
234
234
  graph: NeatGraph;
235
235
  errorsPath: string;
236
+ scanPath?: string;
236
237
  project?: string;
237
238
  now?: () => number;
238
239
  writeErrorEventInline?: boolean;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-RC3CIDZO.js";
4
+ } from "./chunk-W4RNPPB5.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-J5CEKCTR.js";
40
+ } from "./chunk-OJHI33LD.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-3QCRUEQD.js";
package/dist/neatd.cjs CHANGED
@@ -631,6 +631,7 @@ init_cjs_shims();
631
631
  init_cjs_shims();
632
632
  var import_node_fs3 = require("fs");
633
633
  var import_node_path3 = __toESM(require("path"), 1);
634
+ var sourceMapJs = __toESM(require("source-map-js"), 1);
634
635
 
635
636
  // src/policy.ts
636
637
  init_cjs_shims();
@@ -1613,6 +1614,14 @@ function warnUnidentifiedSpan(project) {
1613
1614
  `[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
1614
1615
  );
1615
1616
  }
1617
+ var noSourceMapWarnedServices = /* @__PURE__ */ new Set();
1618
+ function warnNoSourceMaps(serviceName) {
1619
+ if (noSourceMapWarnedServices.has(serviceName)) return;
1620
+ noSourceMapWarnedServices.add(serviceName);
1621
+ console.warn(
1622
+ `[neat] ${serviceName}: no .map files found under dist/; observed file edges will land on dist paths, not src. Set sourceMap: true in tsconfig to enable file-level reconciliation.`
1623
+ );
1624
+ }
1616
1625
  function pickAttr(span, ...keys) {
1617
1626
  for (const k of keys) {
1618
1627
  const v = span.attributes[k];
@@ -1655,8 +1664,13 @@ function languageForExt(relPath) {
1655
1664
  return void 0;
1656
1665
  }
1657
1666
  }
1658
- function relPathForRuntimeFile(filepath, serviceNode) {
1667
+ function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
1659
1668
  let p = toPosix(filepath).replace(/^file:\/\//, "");
1669
+ if (scanPath && scanPath.length > 0) {
1670
+ const absRoot = toPosix(import_node_path3.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
1671
+ const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
1672
+ if (p.startsWith(anchor)) return p.slice(anchor.length);
1673
+ }
1660
1674
  const root = serviceNode?.repoPath;
1661
1675
  if (root && root !== "." && root.length > 0) {
1662
1676
  const rootPosix = toPosix(root);
@@ -1673,16 +1687,65 @@ function relPathForRuntimeFile(filepath, serviceNode) {
1673
1687
  p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1674
1688
  return p.length > 0 ? p : null;
1675
1689
  }
1676
- function callSiteFromSpan(span, serviceNode) {
1690
+ var sourceMapCache = /* @__PURE__ */ new Map();
1691
+ function resolveDistToSrc(absFilepath, line) {
1692
+ if (!absFilepath.endsWith(".js")) return null;
1693
+ let entry2 = sourceMapCache.get(absFilepath);
1694
+ if (entry2 === void 0) {
1695
+ entry2 = null;
1696
+ const mapPath = `${absFilepath}.map`;
1697
+ try {
1698
+ if ((0, import_node_fs3.existsSync)(mapPath)) {
1699
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(mapPath, "utf8"));
1700
+ const consumer = new sourceMapJs.SourceMapConsumer(raw);
1701
+ entry2 = { consumer, dir: import_node_path3.default.dirname(mapPath) };
1702
+ }
1703
+ } catch {
1704
+ entry2 = null;
1705
+ }
1706
+ sourceMapCache.set(absFilepath, entry2);
1707
+ }
1708
+ if (!entry2) return null;
1709
+ try {
1710
+ const pos = entry2.consumer.originalPositionFor({
1711
+ line: line !== void 0 && Number.isFinite(line) ? line : 1,
1712
+ column: 0
1713
+ });
1714
+ if (!pos || !pos.source) return null;
1715
+ const root = entry2.consumer.sourceRoot ?? "";
1716
+ const resolved = import_node_path3.default.resolve(entry2.dir, root, pos.source);
1717
+ return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
1718
+ } catch {
1719
+ return null;
1720
+ }
1721
+ }
1722
+ function callSiteFromSpan(span, serviceNode, scanPath) {
1677
1723
  const filepath = span.attributes[CODE_FILEPATH_ATTR];
1678
1724
  if (typeof filepath !== "string" || filepath.length === 0) return null;
1679
- const relPath = relPathForRuntimeFile(filepath, serviceNode);
1680
- if (!relPath) return null;
1681
1725
  const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1682
- const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1726
+ let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1727
+ const abs = toPosix(filepath).replace(/^file:\/\//, "");
1728
+ const resolved = resolveDistToSrc(abs, line);
1729
+ let effectivePath = filepath;
1730
+ let originalRelPath;
1731
+ if (resolved) {
1732
+ originalRelPath = relPathForRuntimeFile(filepath, serviceNode, scanPath) ?? void 0;
1733
+ effectivePath = resolved.filepath;
1734
+ if (resolved.line !== void 0) line = resolved.line;
1735
+ }
1736
+ const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
1737
+ if (!relPath) return null;
1738
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
1739
+ warnNoSourceMaps(serviceNode.name);
1740
+ }
1683
1741
  const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1684
1742
  const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1685
- return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1743
+ return {
1744
+ relPath,
1745
+ ...line !== void 0 ? { line } : {},
1746
+ ...fn ? { fn } : {},
1747
+ ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1748
+ };
1686
1749
  }
1687
1750
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1688
1751
  const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
@@ -1694,6 +1757,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1694
1757
  service: serviceName,
1695
1758
  path: callSite.relPath,
1696
1759
  ...language ? { language } : {},
1760
+ ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1697
1761
  discoveredVia: "otel"
1698
1762
  };
1699
1763
  graph.addNode(fileNodeId, node);
@@ -1719,6 +1783,12 @@ function makeInferredEdgeId(type, source, target) {
1719
1783
  }
1720
1784
  var INFERRED_CONFIDENCE = 0.6;
1721
1785
  var STITCH_MAX_DEPTH = 2;
1786
+ var WIRE_SPAN_KIND_CLIENT = 3;
1787
+ var WIRE_SPAN_KIND_PRODUCER = 4;
1788
+ function spanMintsObservedEdge(kind) {
1789
+ if (kind === void 0 || kind === 0) return true;
1790
+ return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
1791
+ }
1722
1792
  var PARENT_SPAN_CACHE_SIZE = 1e4;
1723
1793
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
1724
1794
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -1954,12 +2024,13 @@ async function handleSpan(ctx, span) {
1954
2024
  const isError = span.statusCode === 2;
1955
2025
  cacheSpanService(span, nowMs);
1956
2026
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1957
- const callSite = callSiteFromSpan(span, sourceServiceNode);
2027
+ const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
1958
2028
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1959
2029
  let affectedNode = sourceId;
2030
+ const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
1960
2031
  if (span.dbSystem) {
1961
2032
  const host = pickAddress(span);
1962
- if (host) {
2033
+ if (mintsFromCallerSide && host) {
1963
2034
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
1964
2035
  const targetId = (0, import_types3.databaseId)(host);
1965
2036
  const result = upsertObservedEdge(
@@ -1975,7 +2046,7 @@ async function handleSpan(ctx, span) {
1975
2046
  } else {
1976
2047
  const host = pickAddress(span);
1977
2048
  let resolvedViaAddress = false;
1978
- if (host && host !== span.service) {
2049
+ if (mintsFromCallerSide && host && host !== span.service) {
1979
2050
  const targetId = resolveServiceId(ctx.graph, host, env);
1980
2051
  if (targetId && targetId !== sourceId) {
1981
2052
  upsertObservedEdge(
@@ -3686,8 +3757,14 @@ function collectStringLiterals(node, out) {
3686
3757
  if (child) collectStringLiterals(child, out);
3687
3758
  }
3688
3759
  }
3760
+ var PARSE_CHUNK = 16384;
3761
+ function parseSource(parser, source) {
3762
+ return parser.parse(
3763
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3764
+ );
3765
+ }
3689
3766
  function callsFromSource(source, parser, knownHosts) {
3690
- const tree = parser.parse(source);
3767
+ const tree = parseSource(parser, source);
3691
3768
  const literals = [];
3692
3769
  collectStringLiterals(tree.rootNode, literals);
3693
3770
  const out = [];
@@ -4982,6 +5059,66 @@ function registryPath() {
4982
5059
  function registryLockPath() {
4983
5060
  return import_node_path33.default.join(neatHome(), "projects.json.lock");
4984
5061
  }
5062
+ function daemonPidPath() {
5063
+ return import_node_path33.default.join(neatHome(), "neatd.pid");
5064
+ }
5065
+ function isPidAliveDefault(pid) {
5066
+ try {
5067
+ process.kill(pid, 0);
5068
+ return true;
5069
+ } catch (err) {
5070
+ return err.code === "EPERM";
5071
+ }
5072
+ }
5073
+ async function readPidFile(file) {
5074
+ try {
5075
+ const raw = await import_node_fs20.promises.readFile(file, "utf8");
5076
+ const pid = Number.parseInt(raw.trim(), 10);
5077
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5078
+ } catch {
5079
+ return void 0;
5080
+ }
5081
+ }
5082
+ var defaultLockHolderProbe = {
5083
+ isPidAlive: isPidAliveDefault,
5084
+ daemonPidFromFile: () => readPidFile(daemonPidPath()),
5085
+ async daemonResponds() {
5086
+ const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
5087
+ try {
5088
+ await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
5089
+ return true;
5090
+ } catch {
5091
+ return false;
5092
+ }
5093
+ }
5094
+ };
5095
+ async function readLockPid(lockPath) {
5096
+ return readPidFile(lockPath);
5097
+ }
5098
+ async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
5099
+ const lockPid = await readLockPid(lockPath);
5100
+ const daemonPid = await probe.daemonPidFromFile();
5101
+ if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
5102
+ // Either confirms a live daemon is in the picture (the second guards
5103
+ // against a stale pidfile whose PID got reused).
5104
+ (daemonPid === lockPid || await probe.daemonResponds())) {
5105
+ return { kind: "daemon", pid: daemonPid };
5106
+ }
5107
+ if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
5108
+ return { kind: "command", pid: lockPid };
5109
+ }
5110
+ return { kind: "stale" };
5111
+ }
5112
+ function lockHolderMessage(holder, lockPath, timeoutMs) {
5113
+ switch (holder.kind) {
5114
+ case "daemon":
5115
+ return `The neat daemon (pid ${holder.pid}) is holding the registry lock. Register this project through the daemon, or stop neatd and re-run \`neat init\`.`;
5116
+ case "command":
5117
+ return `Another neat command (pid ${holder.pid}) is holding the registry lock. Wait for it to finish, or check \`ps\` if you're not sure what's running.`;
5118
+ case "stale":
5119
+ return `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`;
5120
+ }
5121
+ }
4985
5122
  async function writeAtomically(target, contents) {
4986
5123
  await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
4987
5124
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
@@ -4994,21 +5131,31 @@ async function writeAtomically(target, contents) {
4994
5131
  }
4995
5132
  await import_node_fs20.promises.rename(tmp, target);
4996
5133
  }
4997
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
5134
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
4998
5135
  const deadline = Date.now() + timeoutMs;
4999
5136
  await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
5137
+ let probedHolder = false;
5000
5138
  while (true) {
5001
5139
  try {
5002
5140
  const fd = await import_node_fs20.promises.open(lockPath, "wx");
5003
- await fd.close();
5141
+ try {
5142
+ await fd.writeFile(`${process.pid}
5143
+ `, "utf8");
5144
+ } finally {
5145
+ await fd.close();
5146
+ }
5004
5147
  return;
5005
5148
  } catch (err) {
5006
5149
  const code = err.code;
5007
5150
  if (code !== "EEXIST") throw err;
5151
+ if (!probedHolder) {
5152
+ probedHolder = true;
5153
+ const holder = await classifyLockHolder(lockPath, probe);
5154
+ if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5155
+ }
5008
5156
  if (Date.now() >= deadline) {
5009
- throw new Error(
5010
- `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
5011
- );
5157
+ const holder = await classifyLockHolder(lockPath, probe);
5158
+ throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5012
5159
  }
5013
5160
  await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
5014
5161
  }
@@ -5989,6 +6136,7 @@ async function startDaemon(opts = {}) {
5989
6136
  {
5990
6137
  graph: slot.graph,
5991
6138
  errorsPath: slot.paths.errorsPath,
6139
+ scanPath: slot.entry.path,
5992
6140
  project: slot.entry.name,
5993
6141
  // Receiver already wrote the error event synchronously below.
5994
6142
  writeErrorEventInline: false
@@ -6012,6 +6160,7 @@ async function startDaemon(opts = {}) {
6012
6160
  {
6013
6161
  graph: slot.graph,
6014
6162
  errorsPath: slot.paths.errorsPath,
6163
+ scanPath: slot.entry.path,
6015
6164
  project: slot.entry.name,
6016
6165
  writeErrorEventInline: false
6017
6166
  },