@neat.is/core 0.4.9 → 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.cjs CHANGED
@@ -675,6 +675,7 @@ init_cjs_shims();
675
675
  init_cjs_shims();
676
676
  var import_node_fs3 = require("fs");
677
677
  var import_node_path3 = __toESM(require("path"), 1);
678
+ var sourceMapJs = __toESM(require("source-map-js"), 1);
678
679
 
679
680
  // src/policy.ts
680
681
  init_cjs_shims();
@@ -1679,6 +1680,14 @@ function warnUnidentifiedSpan(project) {
1679
1680
  `[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
1680
1681
  );
1681
1682
  }
1683
+ var noSourceMapWarnedServices = /* @__PURE__ */ new Set();
1684
+ function warnNoSourceMaps(serviceName) {
1685
+ if (noSourceMapWarnedServices.has(serviceName)) return;
1686
+ noSourceMapWarnedServices.add(serviceName);
1687
+ console.warn(
1688
+ `[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.`
1689
+ );
1690
+ }
1682
1691
  function pickAttr(span, ...keys) {
1683
1692
  for (const k of keys) {
1684
1693
  const v = span.attributes[k];
@@ -1721,8 +1730,13 @@ function languageForExt(relPath) {
1721
1730
  return void 0;
1722
1731
  }
1723
1732
  }
1724
- function relPathForRuntimeFile(filepath, serviceNode) {
1733
+ function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
1725
1734
  let p = toPosix(filepath).replace(/^file:\/\//, "");
1735
+ if (scanPath && scanPath.length > 0) {
1736
+ const absRoot = toPosix(import_node_path3.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
1737
+ const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
1738
+ if (p.startsWith(anchor)) return p.slice(anchor.length);
1739
+ }
1726
1740
  const root = serviceNode?.repoPath;
1727
1741
  if (root && root !== "." && root.length > 0) {
1728
1742
  const rootPosix = toPosix(root);
@@ -1739,16 +1753,65 @@ function relPathForRuntimeFile(filepath, serviceNode) {
1739
1753
  p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1740
1754
  return p.length > 0 ? p : null;
1741
1755
  }
1742
- function callSiteFromSpan(span, serviceNode) {
1756
+ var sourceMapCache = /* @__PURE__ */ new Map();
1757
+ function resolveDistToSrc(absFilepath, line) {
1758
+ if (!absFilepath.endsWith(".js")) return null;
1759
+ let entry = sourceMapCache.get(absFilepath);
1760
+ if (entry === void 0) {
1761
+ entry = null;
1762
+ const mapPath = `${absFilepath}.map`;
1763
+ try {
1764
+ if ((0, import_node_fs3.existsSync)(mapPath)) {
1765
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(mapPath, "utf8"));
1766
+ const consumer = new sourceMapJs.SourceMapConsumer(raw);
1767
+ entry = { consumer, dir: import_node_path3.default.dirname(mapPath) };
1768
+ }
1769
+ } catch {
1770
+ entry = null;
1771
+ }
1772
+ sourceMapCache.set(absFilepath, entry);
1773
+ }
1774
+ if (!entry) return null;
1775
+ try {
1776
+ const pos = entry.consumer.originalPositionFor({
1777
+ line: line !== void 0 && Number.isFinite(line) ? line : 1,
1778
+ column: 0
1779
+ });
1780
+ if (!pos || !pos.source) return null;
1781
+ const root = entry.consumer.sourceRoot ?? "";
1782
+ const resolved = import_node_path3.default.resolve(entry.dir, root, pos.source);
1783
+ return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
1784
+ } catch {
1785
+ return null;
1786
+ }
1787
+ }
1788
+ function callSiteFromSpan(span, serviceNode, scanPath) {
1743
1789
  const filepath = span.attributes[CODE_FILEPATH_ATTR];
1744
1790
  if (typeof filepath !== "string" || filepath.length === 0) return null;
1745
- const relPath = relPathForRuntimeFile(filepath, serviceNode);
1746
- if (!relPath) return null;
1747
1791
  const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1748
- const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1792
+ let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1793
+ const abs = toPosix(filepath).replace(/^file:\/\//, "");
1794
+ const resolved = resolveDistToSrc(abs, line);
1795
+ let effectivePath = filepath;
1796
+ let originalRelPath;
1797
+ if (resolved) {
1798
+ originalRelPath = relPathForRuntimeFile(filepath, serviceNode, scanPath) ?? void 0;
1799
+ effectivePath = resolved.filepath;
1800
+ if (resolved.line !== void 0) line = resolved.line;
1801
+ }
1802
+ const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
1803
+ if (!relPath) return null;
1804
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
1805
+ warnNoSourceMaps(serviceNode.name);
1806
+ }
1749
1807
  const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1750
1808
  const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1751
- return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1809
+ return {
1810
+ relPath,
1811
+ ...line !== void 0 ? { line } : {},
1812
+ ...fn ? { fn } : {},
1813
+ ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1814
+ };
1752
1815
  }
1753
1816
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1754
1817
  const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
@@ -1760,6 +1823,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1760
1823
  service: serviceName,
1761
1824
  path: callSite.relPath,
1762
1825
  ...language ? { language } : {},
1826
+ ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1763
1827
  discoveredVia: "otel"
1764
1828
  };
1765
1829
  graph.addNode(fileNodeId, node);
@@ -1785,6 +1849,12 @@ function makeInferredEdgeId(type, source, target) {
1785
1849
  }
1786
1850
  var INFERRED_CONFIDENCE = 0.6;
1787
1851
  var STITCH_MAX_DEPTH = 2;
1852
+ var WIRE_SPAN_KIND_CLIENT = 3;
1853
+ var WIRE_SPAN_KIND_PRODUCER = 4;
1854
+ function spanMintsObservedEdge(kind) {
1855
+ if (kind === void 0 || kind === 0) return true;
1856
+ return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
1857
+ }
1788
1858
  var PARENT_SPAN_CACHE_SIZE = 1e4;
1789
1859
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
1790
1860
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -2020,12 +2090,13 @@ async function handleSpan(ctx, span) {
2020
2090
  const isError = span.statusCode === 2;
2021
2091
  cacheSpanService(span, nowMs);
2022
2092
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2023
- const callSite = callSiteFromSpan(span, sourceServiceNode);
2093
+ const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2024
2094
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2025
2095
  let affectedNode = sourceId;
2096
+ const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2026
2097
  if (span.dbSystem) {
2027
2098
  const host = pickAddress(span);
2028
- if (host) {
2099
+ if (mintsFromCallerSide && host) {
2029
2100
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2030
2101
  const targetId = (0, import_types3.databaseId)(host);
2031
2102
  const result = upsertObservedEdge(
@@ -2041,7 +2112,7 @@ async function handleSpan(ctx, span) {
2041
2112
  } else {
2042
2113
  const host = pickAddress(span);
2043
2114
  let resolvedViaAddress = false;
2044
- if (host && host !== span.service) {
2115
+ if (mintsFromCallerSide && host && host !== span.service) {
2045
2116
  const targetId = resolveServiceId(ctx.graph, host, env);
2046
2117
  if (targetId && targetId !== sourceId) {
2047
2118
  upsertObservedEdge(
@@ -3825,8 +3896,14 @@ function collectStringLiterals(node, out) {
3825
3896
  if (child) collectStringLiterals(child, out);
3826
3897
  }
3827
3898
  }
3899
+ var PARSE_CHUNK = 16384;
3900
+ function parseSource(parser, source) {
3901
+ return parser.parse(
3902
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3903
+ );
3904
+ }
3828
3905
  function callsFromSource(source, parser, knownHosts) {
3829
- const tree = parser.parse(source);
3906
+ const tree = parseSource(parser, source);
3830
3907
  const literals = [];
3831
3908
  collectStringLiterals(tree.rootNode, literals);
3832
3909
  const out = [];
@@ -5121,6 +5198,66 @@ function registryPath() {
5121
5198
  function registryLockPath() {
5122
5199
  return import_node_path33.default.join(neatHome(), "projects.json.lock");
5123
5200
  }
5201
+ function daemonPidPath() {
5202
+ return import_node_path33.default.join(neatHome(), "neatd.pid");
5203
+ }
5204
+ function isPidAliveDefault(pid) {
5205
+ try {
5206
+ process.kill(pid, 0);
5207
+ return true;
5208
+ } catch (err) {
5209
+ return err.code === "EPERM";
5210
+ }
5211
+ }
5212
+ async function readPidFile(file) {
5213
+ try {
5214
+ const raw = await import_node_fs20.promises.readFile(file, "utf8");
5215
+ const pid = Number.parseInt(raw.trim(), 10);
5216
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5217
+ } catch {
5218
+ return void 0;
5219
+ }
5220
+ }
5221
+ var defaultLockHolderProbe = {
5222
+ isPidAlive: isPidAliveDefault,
5223
+ daemonPidFromFile: () => readPidFile(daemonPidPath()),
5224
+ async daemonResponds() {
5225
+ const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
5226
+ try {
5227
+ await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
5228
+ return true;
5229
+ } catch {
5230
+ return false;
5231
+ }
5232
+ }
5233
+ };
5234
+ async function readLockPid(lockPath) {
5235
+ return readPidFile(lockPath);
5236
+ }
5237
+ async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
5238
+ const lockPid = await readLockPid(lockPath);
5239
+ const daemonPid = await probe.daemonPidFromFile();
5240
+ if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
5241
+ // Either confirms a live daemon is in the picture (the second guards
5242
+ // against a stale pidfile whose PID got reused).
5243
+ (daemonPid === lockPid || await probe.daemonResponds())) {
5244
+ return { kind: "daemon", pid: daemonPid };
5245
+ }
5246
+ if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
5247
+ return { kind: "command", pid: lockPid };
5248
+ }
5249
+ return { kind: "stale" };
5250
+ }
5251
+ function lockHolderMessage(holder, lockPath, timeoutMs) {
5252
+ switch (holder.kind) {
5253
+ case "daemon":
5254
+ 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\`.`;
5255
+ case "command":
5256
+ 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.`;
5257
+ case "stale":
5258
+ 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.`;
5259
+ }
5260
+ }
5124
5261
  async function normalizeProjectPath(input) {
5125
5262
  const resolved = import_node_path33.default.resolve(input);
5126
5263
  try {
@@ -5141,21 +5278,31 @@ async function writeAtomically(target, contents) {
5141
5278
  }
5142
5279
  await import_node_fs20.promises.rename(tmp, target);
5143
5280
  }
5144
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
5281
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5145
5282
  const deadline = Date.now() + timeoutMs;
5146
5283
  await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
5284
+ let probedHolder = false;
5147
5285
  while (true) {
5148
5286
  try {
5149
5287
  const fd = await import_node_fs20.promises.open(lockPath, "wx");
5150
- await fd.close();
5288
+ try {
5289
+ await fd.writeFile(`${process.pid}
5290
+ `, "utf8");
5291
+ } finally {
5292
+ await fd.close();
5293
+ }
5151
5294
  return;
5152
5295
  } catch (err) {
5153
5296
  const code = err.code;
5154
5297
  if (code !== "EEXIST") throw err;
5298
+ if (!probedHolder) {
5299
+ probedHolder = true;
5300
+ const holder = await classifyLockHolder(lockPath, probe);
5301
+ if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5302
+ }
5155
5303
  if (Date.now() >= deadline) {
5156
- throw new Error(
5157
- `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.`
5158
- );
5304
+ const holder = await classifyLockHolder(lockPath, probe);
5305
+ throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5159
5306
  }
5160
5307
  await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
5161
5308
  }
@@ -6193,6 +6340,7 @@ async function startDaemon(opts = {}) {
6193
6340
  {
6194
6341
  graph: slot.graph,
6195
6342
  errorsPath: slot.paths.errorsPath,
6343
+ scanPath: slot.entry.path,
6196
6344
  project: slot.entry.name,
6197
6345
  // Receiver already wrote the error event synchronously below.
6198
6346
  writeErrorEventInline: false
@@ -6216,6 +6364,7 @@ async function startDaemon(opts = {}) {
6216
6364
  {
6217
6365
  graph: slot.graph,
6218
6366
  errorsPath: slot.paths.errorsPath,
6367
+ scanPath: slot.entry.path,
6219
6368
  project: slot.entry.name,
6220
6369
  writeErrorEventInline: false
6221
6370
  },