@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.
@@ -655,8 +655,9 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
655
655
  }
656
656
 
657
657
  // src/ingest.ts
658
- import { promises as fs3 } from "fs";
658
+ import { promises as fs3, existsSync, readFileSync } from "fs";
659
659
  import path3 from "path";
660
+ import * as sourceMapJs from "source-map-js";
660
661
 
661
662
  // src/policy.ts
662
663
  import { promises as fs2 } from "fs";
@@ -1103,6 +1104,14 @@ function warnUnidentifiedSpan(project) {
1103
1104
  `[neatd] span lacked service.name; routed to 'unidentified' in project ${project}; check your OTel SDK config.`
1104
1105
  );
1105
1106
  }
1107
+ var noSourceMapWarnedServices = /* @__PURE__ */ new Set();
1108
+ function warnNoSourceMaps(serviceName) {
1109
+ if (noSourceMapWarnedServices.has(serviceName)) return;
1110
+ noSourceMapWarnedServices.add(serviceName);
1111
+ console.warn(
1112
+ `[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.`
1113
+ );
1114
+ }
1106
1115
  function pickAttr(span, ...keys) {
1107
1116
  for (const k of keys) {
1108
1117
  const v = span.attributes[k];
@@ -1145,8 +1154,13 @@ function languageForExt(relPath) {
1145
1154
  return void 0;
1146
1155
  }
1147
1156
  }
1148
- function relPathForRuntimeFile(filepath, serviceNode) {
1157
+ function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
1149
1158
  let p = toPosix(filepath).replace(/^file:\/\//, "");
1159
+ if (scanPath && scanPath.length > 0) {
1160
+ const absRoot = toPosix(path3.resolve(scanPath, serviceNode?.repoPath ?? ""));
1161
+ const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
1162
+ if (p.startsWith(anchor)) return p.slice(anchor.length);
1163
+ }
1150
1164
  const root = serviceNode?.repoPath;
1151
1165
  if (root && root !== "." && root.length > 0) {
1152
1166
  const rootPosix = toPosix(root);
@@ -1163,16 +1177,65 @@ function relPathForRuntimeFile(filepath, serviceNode) {
1163
1177
  p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1164
1178
  return p.length > 0 ? p : null;
1165
1179
  }
1166
- function callSiteFromSpan(span, serviceNode) {
1180
+ var sourceMapCache = /* @__PURE__ */ new Map();
1181
+ function resolveDistToSrc(absFilepath, line) {
1182
+ if (!absFilepath.endsWith(".js")) return null;
1183
+ let entry = sourceMapCache.get(absFilepath);
1184
+ if (entry === void 0) {
1185
+ entry = null;
1186
+ const mapPath = `${absFilepath}.map`;
1187
+ try {
1188
+ if (existsSync(mapPath)) {
1189
+ const raw = JSON.parse(readFileSync(mapPath, "utf8"));
1190
+ const consumer = new sourceMapJs.SourceMapConsumer(raw);
1191
+ entry = { consumer, dir: path3.dirname(mapPath) };
1192
+ }
1193
+ } catch {
1194
+ entry = null;
1195
+ }
1196
+ sourceMapCache.set(absFilepath, entry);
1197
+ }
1198
+ if (!entry) return null;
1199
+ try {
1200
+ const pos = entry.consumer.originalPositionFor({
1201
+ line: line !== void 0 && Number.isFinite(line) ? line : 1,
1202
+ column: 0
1203
+ });
1204
+ if (!pos || !pos.source) return null;
1205
+ const root = entry.consumer.sourceRoot ?? "";
1206
+ const resolved = path3.resolve(entry.dir, root, pos.source);
1207
+ return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
1208
+ } catch {
1209
+ return null;
1210
+ }
1211
+ }
1212
+ function callSiteFromSpan(span, serviceNode, scanPath) {
1167
1213
  const filepath = span.attributes[CODE_FILEPATH_ATTR];
1168
1214
  if (typeof filepath !== "string" || filepath.length === 0) return null;
1169
- const relPath = relPathForRuntimeFile(filepath, serviceNode);
1170
- if (!relPath) return null;
1171
1215
  const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1172
- const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1216
+ let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1217
+ const abs = toPosix(filepath).replace(/^file:\/\//, "");
1218
+ const resolved = resolveDistToSrc(abs, line);
1219
+ let effectivePath = filepath;
1220
+ let originalRelPath;
1221
+ if (resolved) {
1222
+ originalRelPath = relPathForRuntimeFile(filepath, serviceNode, scanPath) ?? void 0;
1223
+ effectivePath = resolved.filepath;
1224
+ if (resolved.line !== void 0) line = resolved.line;
1225
+ }
1226
+ const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
1227
+ if (!relPath) return null;
1228
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
1229
+ warnNoSourceMaps(serviceNode.name);
1230
+ }
1173
1231
  const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1174
1232
  const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1175
- return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1233
+ return {
1234
+ relPath,
1235
+ ...line !== void 0 ? { line } : {},
1236
+ ...fn ? { fn } : {},
1237
+ ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1238
+ };
1176
1239
  }
1177
1240
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1178
1241
  const fileNodeId = fileId(serviceName, callSite.relPath);
@@ -1184,6 +1247,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1184
1247
  service: serviceName,
1185
1248
  path: callSite.relPath,
1186
1249
  ...language ? { language } : {},
1250
+ ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1187
1251
  discoveredVia: "otel"
1188
1252
  };
1189
1253
  graph.addNode(fileNodeId, node);
@@ -1209,6 +1273,12 @@ function makeInferredEdgeId(type, source, target) {
1209
1273
  }
1210
1274
  var INFERRED_CONFIDENCE = 0.6;
1211
1275
  var STITCH_MAX_DEPTH = 2;
1276
+ var WIRE_SPAN_KIND_CLIENT = 3;
1277
+ var WIRE_SPAN_KIND_PRODUCER = 4;
1278
+ function spanMintsObservedEdge(kind) {
1279
+ if (kind === void 0 || kind === 0) return true;
1280
+ return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
1281
+ }
1212
1282
  var PARENT_SPAN_CACHE_SIZE = 1e4;
1213
1283
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
1214
1284
  var parentSpanCache = /* @__PURE__ */ new Map();
@@ -1444,12 +1514,13 @@ async function handleSpan(ctx, span) {
1444
1514
  const isError = span.statusCode === 2;
1445
1515
  cacheSpanService(span, nowMs);
1446
1516
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1447
- const callSite = callSiteFromSpan(span, sourceServiceNode);
1517
+ const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
1448
1518
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1449
1519
  let affectedNode = sourceId;
1520
+ const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
1450
1521
  if (span.dbSystem) {
1451
1522
  const host = pickAddress(span);
1452
- if (host) {
1523
+ if (mintsFromCallerSide && host) {
1453
1524
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
1454
1525
  const targetId = databaseId(host);
1455
1526
  const result = upsertObservedEdge(
@@ -1465,7 +1536,7 @@ async function handleSpan(ctx, span) {
1465
1536
  } else {
1466
1537
  const host = pickAddress(span);
1467
1538
  let resolvedViaAddress = false;
1468
- if (host && host !== span.service) {
1539
+ if (mintsFromCallerSide && host && host !== span.service) {
1469
1540
  const targetId = resolveServiceId(ctx.graph, host, env);
1470
1541
  if (targetId && targetId !== sourceId) {
1471
1542
  upsertObservedEdge(
@@ -3270,8 +3341,14 @@ function collectStringLiterals(node, out) {
3270
3341
  if (child) collectStringLiterals(child, out);
3271
3342
  }
3272
3343
  }
3344
+ var PARSE_CHUNK = 16384;
3345
+ function parseSource(parser, source) {
3346
+ return parser.parse(
3347
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3348
+ );
3349
+ }
3273
3350
  function callsFromSource(source, parser, knownHosts) {
3274
- const tree = parser.parse(source);
3351
+ const tree = parseSource(parser, source);
3275
3352
  const literals = [];
3276
3353
  collectStringLiterals(tree.rootNode, literals);
3277
3354
  const out = [];
@@ -3952,7 +4029,7 @@ async function addInfra(graph, scanPath, services) {
3952
4029
  import path30 from "path";
3953
4030
 
3954
4031
  // src/extract/retire.ts
3955
- import { existsSync } from "fs";
4032
+ import { existsSync as existsSync2 } from "fs";
3956
4033
  import path29 from "path";
3957
4034
  import { NodeType as NodeType11, Provenance as Provenance9 } from "@neat.is/types";
3958
4035
  function dropOrphanedFileNodes(graph) {
@@ -3988,10 +4065,10 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
3988
4065
  const evidenceFile = edge.evidence?.file;
3989
4066
  if (!evidenceFile) return;
3990
4067
  if (path29.isAbsolute(evidenceFile)) {
3991
- if (!existsSync(evidenceFile)) toDrop.push(id);
4068
+ if (!existsSync2(evidenceFile)) toDrop.push(id);
3992
4069
  return;
3993
4070
  }
3994
- const found = bases.some((base) => existsSync(path29.join(base, evidenceFile)));
4071
+ const found = bases.some((base) => existsSync2(path29.join(base, evidenceFile)));
3995
4072
  if (!found) toDrop.push(id);
3996
4073
  });
3997
4074
  for (const id of toDrop) graph.dropEdge(id);
@@ -4567,6 +4644,66 @@ function registryPath() {
4567
4644
  function registryLockPath() {
4568
4645
  return path33.join(neatHome(), "projects.json.lock");
4569
4646
  }
4647
+ function daemonPidPath() {
4648
+ return path33.join(neatHome(), "neatd.pid");
4649
+ }
4650
+ function isPidAliveDefault(pid) {
4651
+ try {
4652
+ process.kill(pid, 0);
4653
+ return true;
4654
+ } catch (err) {
4655
+ return err.code === "EPERM";
4656
+ }
4657
+ }
4658
+ async function readPidFile(file) {
4659
+ try {
4660
+ const raw = await fs19.readFile(file, "utf8");
4661
+ const pid = Number.parseInt(raw.trim(), 10);
4662
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
4663
+ } catch {
4664
+ return void 0;
4665
+ }
4666
+ }
4667
+ var defaultLockHolderProbe = {
4668
+ isPidAlive: isPidAliveDefault,
4669
+ daemonPidFromFile: () => readPidFile(daemonPidPath()),
4670
+ async daemonResponds() {
4671
+ const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
4672
+ try {
4673
+ await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
4674
+ return true;
4675
+ } catch {
4676
+ return false;
4677
+ }
4678
+ }
4679
+ };
4680
+ async function readLockPid(lockPath) {
4681
+ return readPidFile(lockPath);
4682
+ }
4683
+ async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
4684
+ const lockPid = await readLockPid(lockPath);
4685
+ const daemonPid = await probe.daemonPidFromFile();
4686
+ if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
4687
+ // Either confirms a live daemon is in the picture (the second guards
4688
+ // against a stale pidfile whose PID got reused).
4689
+ (daemonPid === lockPid || await probe.daemonResponds())) {
4690
+ return { kind: "daemon", pid: daemonPid };
4691
+ }
4692
+ if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
4693
+ return { kind: "command", pid: lockPid };
4694
+ }
4695
+ return { kind: "stale" };
4696
+ }
4697
+ function lockHolderMessage(holder, lockPath, timeoutMs) {
4698
+ switch (holder.kind) {
4699
+ case "daemon":
4700
+ 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\`.`;
4701
+ case "command":
4702
+ 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.`;
4703
+ case "stale":
4704
+ 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.`;
4705
+ }
4706
+ }
4570
4707
  async function normalizeProjectPath(input) {
4571
4708
  const resolved = path33.resolve(input);
4572
4709
  try {
@@ -4587,21 +4724,31 @@ async function writeAtomically(target, contents) {
4587
4724
  }
4588
4725
  await fs19.rename(tmp, target);
4589
4726
  }
4590
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4727
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
4591
4728
  const deadline = Date.now() + timeoutMs;
4592
4729
  await fs19.mkdir(path33.dirname(lockPath), { recursive: true });
4730
+ let probedHolder = false;
4593
4731
  while (true) {
4594
4732
  try {
4595
4733
  const fd = await fs19.open(lockPath, "wx");
4596
- await fd.close();
4734
+ try {
4735
+ await fd.writeFile(`${process.pid}
4736
+ `, "utf8");
4737
+ } finally {
4738
+ await fd.close();
4739
+ }
4597
4740
  return;
4598
4741
  } catch (err) {
4599
4742
  const code = err.code;
4600
4743
  if (code !== "EEXIST") throw err;
4744
+ if (!probedHolder) {
4745
+ probedHolder = true;
4746
+ const holder = await classifyLockHolder(lockPath, probe);
4747
+ if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
4748
+ }
4601
4749
  if (Date.now() >= deadline) {
4602
- throw new Error(
4603
- `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.`
4604
- );
4750
+ const holder = await classifyLockHolder(lockPath, probe);
4751
+ throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
4605
4752
  }
4606
4753
  await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
4607
4754
  }
@@ -5341,4 +5488,4 @@ export {
5341
5488
  removeProject,
5342
5489
  buildApi
5343
5490
  };
5344
- //# sourceMappingURL=chunk-J5CEKCTR.js.map
5491
+ //# sourceMappingURL=chunk-OJHI33LD.js.map