@neat.is/core 0.4.10 → 0.4.12

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
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
57
57
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
58
58
  const publicRead = opts.publicRead === true;
59
59
  app.addHook("preHandler", (req, reply, done) => {
60
- const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path40 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path38 === suffix || path38.endsWith(suffix)) {
62
+ if (path40 === suffix || path40.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -185,8 +185,8 @@ function reshapeGrpcRequest(req) {
185
185
  };
186
186
  }
187
187
  function resolveProtoRoot() {
188
- const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
189
- return import_node_path34.default.resolve(here, "..", "proto");
188
+ const here = import_node_path36.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
189
+ return import_node_path36.default.resolve(here, "..", "proto");
190
190
  }
191
191
  function loadTraceService() {
192
192
  const protoRoot = resolveProtoRoot();
@@ -254,13 +254,13 @@ async function startOtelGrpcReceiver(opts) {
254
254
  })
255
255
  };
256
256
  }
257
- var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
257
+ var import_node_url, import_node_path36, import_node_crypto2, grpc, protoLoader;
258
258
  var init_otel_grpc = __esm({
259
259
  "src/otel-grpc.ts"() {
260
260
  "use strict";
261
261
  init_cjs_shims();
262
262
  import_node_url = require("url");
263
- import_node_path34 = __toESM(require("path"), 1);
263
+ import_node_path36 = __toESM(require("path"), 1);
264
264
  import_node_crypto2 = require("crypto");
265
265
  grpc = __toESM(require("@grpc/grpc-js"), 1);
266
266
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -370,10 +370,10 @@ function parseOtlpRequest(body) {
370
370
  return out;
371
371
  }
372
372
  function loadProtoRoot() {
373
- const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
374
- const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
373
+ const here = import_node_path37.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
374
+ const protoRoot = import_node_path37.default.resolve(here, "..", "proto");
375
375
  const root = new import_protobufjs.default.Root();
376
- root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
376
+ root.resolvePath = (_origin, target) => import_node_path37.default.resolve(protoRoot, target);
377
377
  root.loadSync(
378
378
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
379
379
  { keepCase: true }
@@ -574,12 +574,12 @@ function logSpanHandler(span) {
574
574
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
575
575
  );
576
576
  }
577
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
577
+ var import_node_path37, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
578
578
  var init_otel = __esm({
579
579
  "src/otel.ts"() {
580
580
  "use strict";
581
581
  init_cjs_shims();
582
- import_node_path35 = __toESM(require("path"), 1);
582
+ import_node_path37 = __toESM(require("path"), 1);
583
583
  import_node_url2 = require("url");
584
584
  import_fastify2 = __toESM(require("fastify"), 1);
585
585
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -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();
@@ -1116,19 +1117,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1116
1117
  function longestIncomingWalk(graph, start, maxDepth) {
1117
1118
  let best = { path: [start], edges: [] };
1118
1119
  const visited = /* @__PURE__ */ new Set([start]);
1119
- function step(node, path38, edges) {
1120
- if (path38.length > best.path.length) {
1121
- best = { path: [...path38], edges: [...edges] };
1120
+ function step(node, path40, edges) {
1121
+ if (path40.length > best.path.length) {
1122
+ best = { path: [...path40], edges: [...edges] };
1122
1123
  }
1123
- if (path38.length - 1 >= maxDepth) return;
1124
+ if (path40.length - 1 >= maxDepth) return;
1124
1125
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1125
1126
  for (const [srcId, edge] of incoming) {
1126
1127
  if (visited.has(srcId)) continue;
1127
1128
  visited.add(srcId);
1128
- path38.push(srcId);
1129
+ path40.push(srcId);
1129
1130
  edges.push(edge);
1130
- step(srcId, path38, edges);
1131
- path38.pop();
1131
+ step(srcId, path40, edges);
1132
+ path40.pop();
1132
1133
  edges.pop();
1133
1134
  visited.delete(srcId);
1134
1135
  }
@@ -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();
@@ -1891,7 +1961,7 @@ function ensureFrontierNode(graph, host, ts) {
1891
1961
  graph.addNode(id, node);
1892
1962
  return id;
1893
1963
  }
1894
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1964
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1895
1965
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1896
1966
  const id = makeObservedEdgeId(type, source, target);
1897
1967
  if (graph.hasEdge(id)) {
@@ -1928,7 +1998,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1928
1998
  confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1929
1999
  lastObserved: ts,
1930
2000
  callCount: 1,
1931
- signal
2001
+ signal,
2002
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
2003
+ // Only set when code.filepath was present on the span — never fabricated.
2004
+ ...evidence ? { evidence } : {}
1932
2005
  };
1933
2006
  graph.addEdgeWithKey(id, source, target, edge);
1934
2007
  return { edge, created: true };
@@ -2020,12 +2093,14 @@ async function handleSpan(ctx, span) {
2020
2093
  const isError = span.statusCode === 2;
2021
2094
  cacheSpanService(span, nowMs);
2022
2095
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2023
- const callSite = callSiteFromSpan(span, sourceServiceNode);
2096
+ const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2024
2097
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2098
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2025
2099
  let affectedNode = sourceId;
2100
+ const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2026
2101
  if (span.dbSystem) {
2027
2102
  const host = pickAddress(span);
2028
- if (host) {
2103
+ if (mintsFromCallerSide && host) {
2029
2104
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
2030
2105
  const targetId = (0, import_types3.databaseId)(host);
2031
2106
  const result = upsertObservedEdge(
@@ -2034,14 +2109,15 @@ async function handleSpan(ctx, span) {
2034
2109
  observedSource(),
2035
2110
  targetId,
2036
2111
  ts,
2037
- isError
2112
+ isError,
2113
+ callSiteEvidence
2038
2114
  );
2039
2115
  if (result) affectedNode = targetId;
2040
2116
  }
2041
2117
  } else {
2042
2118
  const host = pickAddress(span);
2043
2119
  let resolvedViaAddress = false;
2044
- if (host && host !== span.service) {
2120
+ if (mintsFromCallerSide && host && host !== span.service) {
2045
2121
  const targetId = resolveServiceId(ctx.graph, host, env);
2046
2122
  if (targetId && targetId !== sourceId) {
2047
2123
  upsertObservedEdge(
@@ -2050,7 +2126,8 @@ async function handleSpan(ctx, span) {
2050
2126
  observedSource(),
2051
2127
  targetId,
2052
2128
  ts,
2053
- isError
2129
+ isError,
2130
+ callSiteEvidence
2054
2131
  );
2055
2132
  affectedNode = targetId;
2056
2133
  resolvedViaAddress = true;
@@ -2062,7 +2139,8 @@ async function handleSpan(ctx, span) {
2062
2139
  observedSource(),
2063
2140
  frontierNodeId,
2064
2141
  ts,
2065
- isError
2142
+ isError,
2143
+ callSiteEvidence
2066
2144
  );
2067
2145
  affectedNode = frontierNodeId;
2068
2146
  resolvedViaAddress = true;
@@ -3013,14 +3091,111 @@ async function addServiceAliases(graph, scanPath, services) {
3013
3091
 
3014
3092
  // src/extract/databases/index.ts
3015
3093
  init_cjs_shims();
3016
- var import_node_path17 = __toESM(require("path"), 1);
3094
+ var import_node_path18 = __toESM(require("path"), 1);
3095
+ var import_types8 = require("@neat.is/types");
3096
+
3097
+ // src/extract/calls/shared.ts
3098
+ init_cjs_shims();
3099
+ var import_node_fs10 = require("fs");
3100
+ var import_node_path10 = __toESM(require("path"), 1);
3017
3101
  var import_types7 = require("@neat.is/types");
3102
+ async function walkSourceFiles(dir) {
3103
+ const out = [];
3104
+ async function walk(current) {
3105
+ const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3106
+ for (const entry of entries) {
3107
+ const full = import_node_path10.default.join(current, entry.name);
3108
+ if (entry.isDirectory()) {
3109
+ if (IGNORED_DIRS.has(entry.name)) continue;
3110
+ if (await isPythonVenvDir(full)) continue;
3111
+ await walk(full);
3112
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name))) {
3113
+ out.push(full);
3114
+ }
3115
+ }
3116
+ }
3117
+ await walk(dir);
3118
+ return out;
3119
+ }
3120
+ async function loadSourceFiles(dir) {
3121
+ const paths = await walkSourceFiles(dir);
3122
+ const out = [];
3123
+ for (const p of paths) {
3124
+ try {
3125
+ const content = await import_node_fs10.promises.readFile(p, "utf8");
3126
+ out.push({ path: p, content });
3127
+ } catch {
3128
+ }
3129
+ }
3130
+ return out;
3131
+ }
3132
+ function lineOf(text, needle) {
3133
+ const idx = text.indexOf(needle);
3134
+ if (idx < 0) return 1;
3135
+ return text.slice(0, idx).split("\n").length;
3136
+ }
3137
+ function snippet(text, line) {
3138
+ const lines = text.split("\n");
3139
+ return (lines[line - 1] ?? "").trim();
3140
+ }
3141
+ function toPosix2(p) {
3142
+ return p.split("\\").join("/");
3143
+ }
3144
+ function languageForPath(relPath) {
3145
+ switch (import_node_path10.default.extname(relPath).toLowerCase()) {
3146
+ case ".py":
3147
+ return "python";
3148
+ case ".ts":
3149
+ case ".tsx":
3150
+ return "typescript";
3151
+ case ".js":
3152
+ case ".jsx":
3153
+ case ".mjs":
3154
+ case ".cjs":
3155
+ return "javascript";
3156
+ default:
3157
+ return void 0;
3158
+ }
3159
+ }
3160
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3161
+ let nodesAdded = 0;
3162
+ let edgesAdded = 0;
3163
+ const fileNodeId = (0, import_types7.fileId)(serviceName, relPath);
3164
+ if (!graph.hasNode(fileNodeId)) {
3165
+ const language = languageForPath(relPath);
3166
+ const node = {
3167
+ id: fileNodeId,
3168
+ type: import_types7.NodeType.FileNode,
3169
+ service: serviceName,
3170
+ path: relPath,
3171
+ ...language ? { language } : {},
3172
+ discoveredVia: "static"
3173
+ };
3174
+ graph.addNode(fileNodeId, node);
3175
+ nodesAdded++;
3176
+ }
3177
+ const containsId = (0, import_types7.extractedEdgeId)(serviceNodeId, fileNodeId, import_types7.EdgeType.CONTAINS);
3178
+ if (!graph.hasEdge(containsId)) {
3179
+ const edge = {
3180
+ id: containsId,
3181
+ source: serviceNodeId,
3182
+ target: fileNodeId,
3183
+ type: import_types7.EdgeType.CONTAINS,
3184
+ provenance: import_types7.Provenance.EXTRACTED,
3185
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3186
+ evidence: { file: relPath }
3187
+ };
3188
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3189
+ edgesAdded++;
3190
+ }
3191
+ return { fileNodeId, nodesAdded, edgesAdded };
3192
+ }
3018
3193
 
3019
3194
  // src/extract/databases/db-config-yaml.ts
3020
3195
  init_cjs_shims();
3021
- var import_node_path10 = __toESM(require("path"), 1);
3196
+ var import_node_path11 = __toESM(require("path"), 1);
3022
3197
  async function parse(serviceDir) {
3023
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3198
+ const yamlPath = import_node_path11.default.join(serviceDir, "db-config.yaml");
3024
3199
  if (!await exists(yamlPath)) return [];
3025
3200
  const raw = await readYaml(yamlPath);
3026
3201
  return [
@@ -3038,13 +3213,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
3038
3213
 
3039
3214
  // src/extract/databases/dotenv.ts
3040
3215
  init_cjs_shims();
3041
- var import_node_fs11 = require("fs");
3042
- var import_node_path12 = __toESM(require("path"), 1);
3216
+ var import_node_fs12 = require("fs");
3217
+ var import_node_path13 = __toESM(require("path"), 1);
3043
3218
 
3044
3219
  // src/extract/databases/shared.ts
3045
3220
  init_cjs_shims();
3046
- var import_node_fs10 = require("fs");
3047
- var import_node_path11 = __toESM(require("path"), 1);
3221
+ var import_node_fs11 = require("fs");
3222
+ var import_node_path12 = __toESM(require("path"), 1);
3048
3223
  function schemeToEngine(scheme) {
3049
3224
  const s = scheme.toLowerCase().split("+")[0];
3050
3225
  switch (s) {
@@ -3083,14 +3258,14 @@ function parseConnectionString(url) {
3083
3258
  }
3084
3259
  async function readIfExists(filePath) {
3085
3260
  try {
3086
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3261
+ return await import_node_fs11.promises.readFile(filePath, "utf8");
3087
3262
  } catch {
3088
3263
  return null;
3089
3264
  }
3090
3265
  }
3091
3266
  async function findFirst(serviceDir, candidates) {
3092
3267
  for (const rel of candidates) {
3093
- const abs = import_node_path11.default.join(serviceDir, rel);
3268
+ const abs = import_node_path12.default.join(serviceDir, rel);
3094
3269
  const content = await readIfExists(abs);
3095
3270
  if (content !== null) return abs;
3096
3271
  }
@@ -3141,15 +3316,15 @@ function parseDotenvLine(line) {
3141
3316
  return { key, value };
3142
3317
  }
3143
3318
  async function parse2(serviceDir) {
3144
- const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3319
+ const entries = await import_node_fs12.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3145
3320
  const configs = [];
3146
3321
  const seen = /* @__PURE__ */ new Set();
3147
3322
  for (const entry of entries) {
3148
3323
  if (!entry.isFile()) continue;
3149
3324
  const match = isConfigFile(entry.name);
3150
3325
  if (!match.match || match.fileType !== "env") continue;
3151
- const filePath = import_node_path12.default.join(serviceDir, entry.name);
3152
- const content = await import_node_fs11.promises.readFile(filePath, "utf8");
3326
+ const filePath = import_node_path13.default.join(serviceDir, entry.name);
3327
+ const content = await import_node_fs12.promises.readFile(filePath, "utf8");
3153
3328
  for (const line of content.split("\n")) {
3154
3329
  const parsed = parseDotenvLine(line);
3155
3330
  if (!parsed) continue;
@@ -3168,9 +3343,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3168
3343
 
3169
3344
  // src/extract/databases/prisma.ts
3170
3345
  init_cjs_shims();
3171
- var import_node_path13 = __toESM(require("path"), 1);
3346
+ var import_node_path14 = __toESM(require("path"), 1);
3172
3347
  async function parse3(serviceDir) {
3173
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
3348
+ const schemaPath = import_node_path14.default.join(serviceDir, "prisma", "schema.prisma");
3174
3349
  const content = await readIfExists(schemaPath);
3175
3350
  if (!content) return [];
3176
3351
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3302,10 +3477,10 @@ var knexParser = { name: "knex", parse: parse5 };
3302
3477
 
3303
3478
  // src/extract/databases/ormconfig.ts
3304
3479
  init_cjs_shims();
3305
- var import_node_path14 = __toESM(require("path"), 1);
3480
+ var import_node_path15 = __toESM(require("path"), 1);
3306
3481
  async function parse6(serviceDir) {
3307
3482
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
3308
- const abs = import_node_path14.default.join(serviceDir, candidate);
3483
+ const abs = import_node_path15.default.join(serviceDir, candidate);
3309
3484
  if (!await exists(abs)) continue;
3310
3485
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3311
3486
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -3365,9 +3540,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3365
3540
 
3366
3541
  // src/extract/databases/sequelize.ts
3367
3542
  init_cjs_shims();
3368
- var import_node_path15 = __toESM(require("path"), 1);
3543
+ var import_node_path16 = __toESM(require("path"), 1);
3369
3544
  async function parse8(serviceDir) {
3370
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3545
+ const configPath = import_node_path16.default.join(serviceDir, "config", "config.json");
3371
3546
  if (!await exists(configPath)) return [];
3372
3547
  const raw = await readJson(configPath);
3373
3548
  const out = [];
@@ -3394,7 +3569,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3394
3569
 
3395
3570
  // src/extract/databases/docker-compose.ts
3396
3571
  init_cjs_shims();
3397
- var import_node_path16 = __toESM(require("path"), 1);
3572
+ var import_node_path17 = __toESM(require("path"), 1);
3398
3573
  function portFromService(svc) {
3399
3574
  for (const raw of svc.ports ?? []) {
3400
3575
  const str = String(raw);
@@ -3421,7 +3596,7 @@ function databaseFromEnv(svc) {
3421
3596
  }
3422
3597
  async function parse9(serviceDir) {
3423
3598
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3424
- const abs = import_node_path16.default.join(serviceDir, name);
3599
+ const abs = import_node_path17.default.join(serviceDir, name);
3425
3600
  if (!await exists(abs)) continue;
3426
3601
  const raw = await readYaml(abs);
3427
3602
  if (!raw?.services) return [];
@@ -3462,8 +3637,8 @@ function compatibleDriversFor(engine) {
3462
3637
  }
3463
3638
  function toDatabaseNode(config) {
3464
3639
  return {
3465
- id: (0, import_types7.databaseId)(config.host),
3466
- type: import_types7.NodeType.DatabaseNode,
3640
+ id: (0, import_types8.databaseId)(config.host),
3641
+ type: import_types8.NodeType.DatabaseNode,
3467
3642
  name: config.database || config.host,
3468
3643
  engine: config.engine,
3469
3644
  engineVersion: config.engineVersion,
@@ -3593,19 +3768,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3593
3768
  discoveredVia: mergedDiscoveredVia
3594
3769
  });
3595
3770
  }
3771
+ const relConfigFile = toPosix2(import_node_path18.default.relative(service.dir, config.sourceFile));
3772
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3773
+ graph,
3774
+ service.pkg.name,
3775
+ service.node.id,
3776
+ relConfigFile
3777
+ );
3778
+ nodesAdded += fn;
3779
+ edgesAdded += fe;
3780
+ const evidenceFile = toPosix2(import_node_path18.default.relative(scanPath, config.sourceFile));
3596
3781
  const edge = {
3597
- id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
3598
- source: service.node.id,
3782
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, dbNode.id, import_types8.EdgeType.CONNECTS_TO),
3783
+ source: fileNodeId,
3599
3784
  target: dbNode.id,
3600
- type: import_types7.EdgeType.CONNECTS_TO,
3601
- provenance: import_types7.Provenance.EXTRACTED,
3602
- confidence: (0, import_types7.confidenceForExtracted)("structural"),
3603
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3604
- // Ghost-edge cleanup keys retirement on this; the conditional
3605
- // sourceFile spread that used to live here was a v0.1.x leftover.
3606
- evidence: {
3607
- file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3608
- }
3785
+ type: import_types8.EdgeType.CONNECTS_TO,
3786
+ provenance: import_types8.Provenance.EXTRACTED,
3787
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3788
+ evidence: { file: evidenceFile }
3609
3789
  };
3610
3790
  if (!graph.hasEdge(edge.id)) {
3611
3791
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3631,15 +3811,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3631
3811
 
3632
3812
  // src/extract/configs.ts
3633
3813
  init_cjs_shims();
3634
- var import_node_fs12 = require("fs");
3635
- var import_node_path18 = __toESM(require("path"), 1);
3636
- var import_types8 = require("@neat.is/types");
3814
+ var import_node_fs13 = require("fs");
3815
+ var import_node_path19 = __toESM(require("path"), 1);
3816
+ var import_types9 = require("@neat.is/types");
3637
3817
  async function walkConfigFiles(dir) {
3638
3818
  const out = [];
3639
3819
  async function walk(current) {
3640
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3820
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true });
3641
3821
  for (const entry of entries) {
3642
- const full = import_node_path18.default.join(current, entry.name);
3822
+ const full = import_node_path19.default.join(current, entry.name);
3643
3823
  if (entry.isDirectory()) {
3644
3824
  if (IGNORED_DIRS.has(entry.name)) continue;
3645
3825
  if (await isPythonVenvDir(full)) continue;
@@ -3658,26 +3838,35 @@ async function addConfigNodes(graph, services, scanPath) {
3658
3838
  for (const service of services) {
3659
3839
  const configFiles = await walkConfigFiles(service.dir);
3660
3840
  for (const file of configFiles) {
3661
- const relPath = import_node_path18.default.relative(scanPath, file);
3841
+ const relPath = import_node_path19.default.relative(scanPath, file);
3662
3842
  const node = {
3663
- id: (0, import_types8.configId)(relPath),
3664
- type: import_types8.NodeType.ConfigNode,
3665
- name: import_node_path18.default.basename(file),
3843
+ id: (0, import_types9.configId)(relPath),
3844
+ type: import_types9.NodeType.ConfigNode,
3845
+ name: import_node_path19.default.basename(file),
3666
3846
  path: relPath,
3667
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3847
+ fileType: isConfigFile(import_node_path19.default.basename(file)).fileType
3668
3848
  };
3669
3849
  if (!graph.hasNode(node.id)) {
3670
3850
  graph.addNode(node.id, node);
3671
3851
  nodesAdded++;
3672
3852
  }
3853
+ const relToService = toPosix2(import_node_path19.default.relative(service.dir, file));
3854
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3855
+ graph,
3856
+ service.pkg.name,
3857
+ service.node.id,
3858
+ relToService
3859
+ );
3860
+ nodesAdded += fn;
3861
+ edgesAdded += fe;
3673
3862
  const edge = {
3674
- id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
3675
- source: service.node.id,
3863
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types9.EdgeType.CONFIGURED_BY),
3864
+ source: fileNodeId,
3676
3865
  target: node.id,
3677
- type: import_types8.EdgeType.CONFIGURED_BY,
3678
- provenance: import_types8.Provenance.EXTRACTED,
3679
- confidence: (0, import_types8.confidenceForExtracted)("structural"),
3680
- evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3866
+ type: import_types9.EdgeType.CONFIGURED_BY,
3867
+ provenance: import_types9.Provenance.EXTRACTED,
3868
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3869
+ evidence: { file: relPath.split(import_node_path19.default.sep).join("/") }
3681
3870
  };
3682
3871
  if (!graph.hasEdge(edge.id)) {
3683
3872
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3699,105 +3888,6 @@ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3699
3888
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3700
3889
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3701
3890
  var import_types10 = require("@neat.is/types");
3702
-
3703
- // src/extract/calls/shared.ts
3704
- init_cjs_shims();
3705
- var import_node_fs13 = require("fs");
3706
- var import_node_path19 = __toESM(require("path"), 1);
3707
- var import_types9 = require("@neat.is/types");
3708
- async function walkSourceFiles(dir) {
3709
- const out = [];
3710
- async function walk(current) {
3711
- const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3712
- for (const entry of entries) {
3713
- const full = import_node_path19.default.join(current, entry.name);
3714
- if (entry.isDirectory()) {
3715
- if (IGNORED_DIRS.has(entry.name)) continue;
3716
- if (await isPythonVenvDir(full)) continue;
3717
- await walk(full);
3718
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3719
- out.push(full);
3720
- }
3721
- }
3722
- }
3723
- await walk(dir);
3724
- return out;
3725
- }
3726
- async function loadSourceFiles(dir) {
3727
- const paths = await walkSourceFiles(dir);
3728
- const out = [];
3729
- for (const p of paths) {
3730
- try {
3731
- const content = await import_node_fs13.promises.readFile(p, "utf8");
3732
- out.push({ path: p, content });
3733
- } catch {
3734
- }
3735
- }
3736
- return out;
3737
- }
3738
- function lineOf(text, needle) {
3739
- const idx = text.indexOf(needle);
3740
- if (idx < 0) return 1;
3741
- return text.slice(0, idx).split("\n").length;
3742
- }
3743
- function snippet(text, line) {
3744
- const lines = text.split("\n");
3745
- return (lines[line - 1] ?? "").trim();
3746
- }
3747
- function toPosix2(p) {
3748
- return p.split("\\").join("/");
3749
- }
3750
- function languageForPath(relPath) {
3751
- switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3752
- case ".py":
3753
- return "python";
3754
- case ".ts":
3755
- case ".tsx":
3756
- return "typescript";
3757
- case ".js":
3758
- case ".jsx":
3759
- case ".mjs":
3760
- case ".cjs":
3761
- return "javascript";
3762
- default:
3763
- return void 0;
3764
- }
3765
- }
3766
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3767
- let nodesAdded = 0;
3768
- let edgesAdded = 0;
3769
- const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3770
- if (!graph.hasNode(fileNodeId)) {
3771
- const language = languageForPath(relPath);
3772
- const node = {
3773
- id: fileNodeId,
3774
- type: import_types9.NodeType.FileNode,
3775
- service: serviceName,
3776
- path: relPath,
3777
- ...language ? { language } : {},
3778
- discoveredVia: "static"
3779
- };
3780
- graph.addNode(fileNodeId, node);
3781
- nodesAdded++;
3782
- }
3783
- const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3784
- if (!graph.hasEdge(containsId)) {
3785
- const edge = {
3786
- id: containsId,
3787
- source: serviceNodeId,
3788
- target: fileNodeId,
3789
- type: import_types9.EdgeType.CONTAINS,
3790
- provenance: import_types9.Provenance.EXTRACTED,
3791
- confidence: (0, import_types9.confidenceForExtracted)("structural"),
3792
- evidence: { file: relPath }
3793
- };
3794
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3795
- edgesAdded++;
3796
- }
3797
- return { fileNodeId, nodesAdded, edgesAdded };
3798
- }
3799
-
3800
- // src/extract/calls/http.ts
3801
3891
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3802
3892
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3803
3893
  function isInsideJsxExternalLink(node) {
@@ -3825,8 +3915,14 @@ function collectStringLiterals(node, out) {
3825
3915
  if (child) collectStringLiterals(child, out);
3826
3916
  }
3827
3917
  }
3918
+ var PARSE_CHUNK = 16384;
3919
+ function parseSource(parser, source) {
3920
+ return parser.parse(
3921
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3922
+ );
3923
+ }
3828
3924
  function callsFromSource(source, parser, knownHosts) {
3829
- const tree = parser.parse(source);
3925
+ const tree = parseSource(parser, source);
3830
3926
  const literals = [];
3831
3927
  collectStringLiterals(tree.rootNode, literals);
3832
3928
  const out = [];
@@ -4075,6 +4171,7 @@ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-
4075
4171
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
4076
4172
  function isLikelyAddress(value) {
4077
4173
  if (!value) return false;
4174
+ if (/\s/.test(value) || value.startsWith("{")) return false;
4078
4175
  return /:\d{2,5}$/.test(value) || value.includes(".");
4079
4176
  }
4080
4177
  function normaliseForMatch(s) {
@@ -4386,17 +4483,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4386
4483
  graph.addNode(node.id, node);
4387
4484
  nodesAdded++;
4388
4485
  }
4389
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4486
+ const relDockerfile = toPosix2(import_node_path26.default.relative(service.dir, dockerfilePath));
4487
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4488
+ graph,
4489
+ service.pkg.name,
4490
+ service.node.id,
4491
+ relDockerfile
4492
+ );
4493
+ nodesAdded += fn;
4494
+ edgesAdded += fe;
4495
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types18.EdgeType.RUNS_ON);
4390
4496
  if (!graph.hasEdge(edgeId)) {
4391
4497
  const edge = {
4392
4498
  id: edgeId,
4393
- source: service.node.id,
4499
+ source: fileNodeId,
4394
4500
  target: node.id,
4395
4501
  type: import_types18.EdgeType.RUNS_ON,
4396
4502
  provenance: import_types18.Provenance.EXTRACTED,
4397
4503
  confidence: (0, import_types18.confidenceForExtracted)("structural"),
4398
4504
  evidence: {
4399
- file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4505
+ file: toPosix2(import_node_path26.default.relative(scanPath, dockerfilePath))
4400
4506
  }
4401
4507
  };
4402
4508
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4735,6 +4841,321 @@ var import_fastify = __toESM(require("fastify"), 1);
4735
4841
  var import_cors = __toESM(require("@fastify/cors"), 1);
4736
4842
  var import_types23 = require("@neat.is/types");
4737
4843
 
4844
+ // src/extend/index.ts
4845
+ init_cjs_shims();
4846
+ var import_node_fs20 = require("fs");
4847
+ var import_node_path33 = __toESM(require("path"), 1);
4848
+ var import_node_os2 = __toESM(require("os"), 1);
4849
+ var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
4850
+
4851
+ // src/installers/package-manager.ts
4852
+ init_cjs_shims();
4853
+ var import_node_fs19 = require("fs");
4854
+ var import_node_path32 = __toESM(require("path"), 1);
4855
+ var import_node_child_process = require("child_process");
4856
+ var LOCKFILE_PRIORITY = [
4857
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
4858
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
4859
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
4860
+ {
4861
+ lockfile: "package-lock.json",
4862
+ pm: "npm",
4863
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
4864
+ }
4865
+ ];
4866
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
4867
+ async function exists2(p) {
4868
+ try {
4869
+ await import_node_fs19.promises.access(p);
4870
+ return true;
4871
+ } catch {
4872
+ return false;
4873
+ }
4874
+ }
4875
+ async function detectPackageManager(serviceDir) {
4876
+ let dir = import_node_path32.default.resolve(serviceDir);
4877
+ const stops = /* @__PURE__ */ new Set();
4878
+ for (let i = 0; i < 64; i++) {
4879
+ if (stops.has(dir)) break;
4880
+ stops.add(dir);
4881
+ for (const candidate of LOCKFILE_PRIORITY) {
4882
+ const lockPath = import_node_path32.default.join(dir, candidate.lockfile);
4883
+ if (await exists2(lockPath)) {
4884
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
4885
+ }
4886
+ }
4887
+ const parent = import_node_path32.default.dirname(dir);
4888
+ if (parent === dir) break;
4889
+ dir = parent;
4890
+ }
4891
+ return { pm: "npm", cwd: import_node_path32.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
4892
+ }
4893
+ async function runPackageManagerInstall(cmd) {
4894
+ return new Promise((resolve) => {
4895
+ const child = (0, import_node_child_process.spawn)(cmd.pm, cmd.args, {
4896
+ cwd: cmd.cwd,
4897
+ // Inherit PATH + HOME so the user's installed managers resolve.
4898
+ env: process.env,
4899
+ // `false` keeps the parent in control of cleanup if the orchestrator
4900
+ // exits before install finishes. Cross-platform-safe.
4901
+ shell: false,
4902
+ stdio: ["ignore", "ignore", "pipe"]
4903
+ });
4904
+ let stderr = "";
4905
+ child.stderr?.on("data", (chunk) => {
4906
+ stderr += chunk.toString("utf8");
4907
+ });
4908
+ child.on("error", (err) => {
4909
+ resolve({
4910
+ pm: cmd.pm,
4911
+ cwd: cmd.cwd,
4912
+ args: cmd.args,
4913
+ exitCode: 127,
4914
+ stderr: stderr + `
4915
+ ${err.message}`
4916
+ });
4917
+ });
4918
+ child.on("close", (code) => {
4919
+ resolve({
4920
+ pm: cmd.pm,
4921
+ cwd: cmd.cwd,
4922
+ args: cmd.args,
4923
+ exitCode: code ?? 1,
4924
+ stderr: stderr.trim()
4925
+ });
4926
+ });
4927
+ });
4928
+ }
4929
+
4930
+ // src/extend/index.ts
4931
+ async function fileExists(p) {
4932
+ try {
4933
+ await import_node_fs20.promises.access(p);
4934
+ return true;
4935
+ } catch {
4936
+ return false;
4937
+ }
4938
+ }
4939
+ async function readPackageJson(scanPath) {
4940
+ const pkgPath = import_node_path33.default.join(scanPath, "package.json");
4941
+ const raw = await import_node_fs20.promises.readFile(pkgPath, "utf8");
4942
+ return JSON.parse(raw);
4943
+ }
4944
+ async function findHookFiles(scanPath) {
4945
+ const entries = await import_node_fs20.promises.readdir(scanPath);
4946
+ return entries.filter(
4947
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
4948
+ ).sort();
4949
+ }
4950
+ function extendLogPath() {
4951
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path33.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
4952
+ }
4953
+ async function appendExtendLog(entry) {
4954
+ const logPath = extendLogPath();
4955
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(logPath), { recursive: true });
4956
+ await import_node_fs20.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
4957
+ }
4958
+ function splicedContent(fileContent, snippet2) {
4959
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
4960
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
4961
+ __INSTRUMENTATION_BLOCK__`);
4962
+ }
4963
+ const lines = fileContent.split("\n");
4964
+ let lastPushIdx = -1;
4965
+ for (let i = 0; i < lines.length; i++) {
4966
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
4967
+ }
4968
+ if (lastPushIdx >= 0) {
4969
+ lines.splice(lastPushIdx + 1, 0, snippet2);
4970
+ return lines.join("\n");
4971
+ }
4972
+ for (let i = 0; i < lines.length; i++) {
4973
+ if (lines[i].includes("new NodeSDK(")) {
4974
+ lines.splice(i, 0, snippet2);
4975
+ return lines.join("\n");
4976
+ }
4977
+ }
4978
+ return null;
4979
+ }
4980
+ async function listUninstrumented(ctx) {
4981
+ const pkg = await readPackageJson(ctx.scanPath);
4982
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4983
+ const results = [];
4984
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
4985
+ const entry = (0, import_instrumentation_registry.resolve)(library, installedVersion);
4986
+ if (!entry) continue;
4987
+ if (entry.coverage === "bundled" || entry.coverage === "http-only") continue;
4988
+ results.push({
4989
+ library,
4990
+ coverage: entry.coverage,
4991
+ installedVersion,
4992
+ instrumentation_package: entry.instrumentation_package,
4993
+ package_version: entry.package_version,
4994
+ registration: entry.registration,
4995
+ notes: entry.notes
4996
+ });
4997
+ }
4998
+ return results;
4999
+ }
5000
+ function lookupInstrumentation(library, installedVersion) {
5001
+ const entry = (0, import_instrumentation_registry.resolve)(library, installedVersion);
5002
+ if (!entry) return null;
5003
+ return {
5004
+ library: entry.library,
5005
+ coverage: entry.coverage,
5006
+ instrumentation_package: entry.instrumentation_package,
5007
+ package_version: entry.package_version,
5008
+ registration: entry.registration,
5009
+ notes: entry.notes
5010
+ };
5011
+ }
5012
+ async function describeProjectInstrumentation(ctx) {
5013
+ const hookFiles = await findHookFiles(ctx.scanPath);
5014
+ const envNeat = await fileExists(import_node_path33.default.join(ctx.scanPath, ".env.neat"));
5015
+ const registryInstrPackages = new Set(
5016
+ (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5017
+ );
5018
+ const pkg = await readPackageJson(ctx.scanPath);
5019
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5020
+ const installedDeps = {};
5021
+ for (const [key, version] of Object.entries(allDeps)) {
5022
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
5023
+ installedDeps[key] = version;
5024
+ }
5025
+ }
5026
+ return { hookFiles, envNeat, installedDeps };
5027
+ }
5028
+ async function applyExtension(ctx, args, options) {
5029
+ const hookFiles = await findHookFiles(ctx.scanPath);
5030
+ if (hookFiles.length === 0) {
5031
+ throw new Error(
5032
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
5033
+ );
5034
+ }
5035
+ for (const file of hookFiles) {
5036
+ const content = await import_node_fs20.promises.readFile(import_node_path33.default.join(ctx.scanPath, file), "utf8");
5037
+ if (content.includes(args.registration_snippet)) {
5038
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5039
+ }
5040
+ }
5041
+ const primaryFile = hookFiles[0];
5042
+ const primaryPath = import_node_path33.default.join(ctx.scanPath, primaryFile);
5043
+ const filesTouched = [];
5044
+ const depsAdded = [];
5045
+ const pkgPath = import_node_path33.default.join(ctx.scanPath, "package.json");
5046
+ const pkg = await readPackageJson(ctx.scanPath);
5047
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5048
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
5049
+ await import_node_fs20.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5050
+ filesTouched.push("package.json");
5051
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
5052
+ }
5053
+ const hookContent = await import_node_fs20.promises.readFile(primaryPath, "utf8");
5054
+ const patched = splicedContent(hookContent, args.registration_snippet);
5055
+ if (!patched) {
5056
+ throw new Error(
5057
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
5058
+ );
5059
+ }
5060
+ await import_node_fs20.promises.writeFile(primaryPath, patched, "utf8");
5061
+ filesTouched.push(primaryFile);
5062
+ const cmd = await detectPackageManager(ctx.scanPath);
5063
+ const installer = options?.runInstall ?? runPackageManagerInstall;
5064
+ const install = await installer(cmd);
5065
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
5066
+ await appendExtendLog({
5067
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5068
+ project: ctx.project,
5069
+ library: args.library,
5070
+ instrumentation_package: args.instrumentation_package,
5071
+ version: args.version,
5072
+ registration_snippet: args.registration_snippet,
5073
+ filesTouched,
5074
+ depsAdded,
5075
+ installOutput
5076
+ });
5077
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
5078
+ }
5079
+ async function dryRunExtension(ctx, args) {
5080
+ const hookFiles = await findHookFiles(ctx.scanPath);
5081
+ if (hookFiles.length === 0) {
5082
+ return {
5083
+ library: args.library,
5084
+ filesTouched: [],
5085
+ depsToAdd: [],
5086
+ packageJsonPatch: {},
5087
+ templatePatch: "No hook files found. Run 'neat init' first."
5088
+ };
5089
+ }
5090
+ for (const file of hookFiles) {
5091
+ const content = await import_node_fs20.promises.readFile(import_node_path33.default.join(ctx.scanPath, file), "utf8");
5092
+ if (content.includes(args.registration_snippet)) {
5093
+ return {
5094
+ library: args.library,
5095
+ filesTouched: [],
5096
+ depsToAdd: [],
5097
+ packageJsonPatch: {},
5098
+ templatePatch: "Already applied \u2014 no changes would be made."
5099
+ };
5100
+ }
5101
+ }
5102
+ const primaryFile = hookFiles[0];
5103
+ const filesTouched = [];
5104
+ const depsToAdd = [];
5105
+ let packageJsonPatch = {};
5106
+ let templatePatch = "";
5107
+ const pkg = await readPackageJson(ctx.scanPath);
5108
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5109
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
5110
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5111
+ filesTouched.push("package.json");
5112
+ }
5113
+ const hookContent = await import_node_fs20.promises.readFile(import_node_path33.default.join(ctx.scanPath, primaryFile), "utf8");
5114
+ const patched = splicedContent(hookContent, args.registration_snippet);
5115
+ if (patched) {
5116
+ filesTouched.push(primaryFile);
5117
+ templatePatch = `+ ${args.registration_snippet}`;
5118
+ } else {
5119
+ templatePatch = "Could not find insertion point in hook file.";
5120
+ }
5121
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
5122
+ }
5123
+ async function rollbackExtension(ctx, args) {
5124
+ const logPath = extendLogPath();
5125
+ if (!await fileExists(logPath)) {
5126
+ return { undone: false, message: "no apply found for library" };
5127
+ }
5128
+ const raw = await import_node_fs20.promises.readFile(logPath, "utf8");
5129
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5130
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
5131
+ if (!match) {
5132
+ return { undone: false, message: "no apply found for library" };
5133
+ }
5134
+ const pkgPath = import_node_path33.default.join(ctx.scanPath, "package.json");
5135
+ if (await fileExists(pkgPath)) {
5136
+ const pkg = await readPackageJson(ctx.scanPath);
5137
+ if (pkg.dependencies?.[match.instrumentation_package]) {
5138
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
5139
+ pkg.dependencies = rest;
5140
+ await import_node_fs20.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5141
+ }
5142
+ }
5143
+ const hookFiles = await findHookFiles(ctx.scanPath);
5144
+ for (const file of hookFiles) {
5145
+ const filePath = import_node_path33.default.join(ctx.scanPath, file);
5146
+ const content = await import_node_fs20.promises.readFile(filePath, "utf8");
5147
+ if (content.includes(match.registration_snippet)) {
5148
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
5149
+ await import_node_fs20.promises.writeFile(filePath, filtered, "utf8");
5150
+ break;
5151
+ }
5152
+ }
5153
+ return {
5154
+ undone: true,
5155
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
5156
+ };
5157
+ }
5158
+
4738
5159
  // src/divergences.ts
4739
5160
  init_cjs_shims();
4740
5161
  var import_types21 = require("@neat.is/types");
@@ -4975,7 +5396,7 @@ function computeDivergences(graph, opts = {}) {
4975
5396
 
4976
5397
  // src/diff.ts
4977
5398
  init_cjs_shims();
4978
- var import_node_fs19 = require("fs");
5399
+ var import_node_fs21 = require("fs");
4979
5400
  async function loadSnapshotForDiff(target) {
4980
5401
  if (/^https?:\/\//i.test(target)) {
4981
5402
  const res = await fetch(target);
@@ -4984,7 +5405,7 @@ async function loadSnapshotForDiff(target) {
4984
5405
  }
4985
5406
  return await res.json();
4986
5407
  }
4987
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
5408
+ const raw = await import_node_fs21.promises.readFile(target, "utf8");
4988
5409
  return JSON.parse(raw);
4989
5410
  }
4990
5411
  function indexEntries(entries) {
@@ -5052,23 +5473,23 @@ function canonicalJson(value) {
5052
5473
 
5053
5474
  // src/projects.ts
5054
5475
  init_cjs_shims();
5055
- var import_node_path32 = __toESM(require("path"), 1);
5476
+ var import_node_path34 = __toESM(require("path"), 1);
5056
5477
  function pathsForProject(project, baseDir) {
5057
5478
  if (project === DEFAULT_PROJECT) {
5058
5479
  return {
5059
- snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
5060
- errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
5061
- staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
5062
- embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
5063
- policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
5480
+ snapshotPath: import_node_path34.default.join(baseDir, "graph.json"),
5481
+ errorsPath: import_node_path34.default.join(baseDir, "errors.ndjson"),
5482
+ staleEventsPath: import_node_path34.default.join(baseDir, "stale-events.ndjson"),
5483
+ embeddingsCachePath: import_node_path34.default.join(baseDir, "embeddings.json"),
5484
+ policyViolationsPath: import_node_path34.default.join(baseDir, "policy-violations.ndjson")
5064
5485
  };
5065
5486
  }
5066
5487
  return {
5067
- snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
5068
- errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
5069
- staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
5070
- embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
5071
- policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
5488
+ snapshotPath: import_node_path34.default.join(baseDir, `${project}.json`),
5489
+ errorsPath: import_node_path34.default.join(baseDir, `errors.${project}.ndjson`),
5490
+ staleEventsPath: import_node_path34.default.join(baseDir, `stale-events.${project}.ndjson`),
5491
+ embeddingsCachePath: import_node_path34.default.join(baseDir, `embeddings.${project}.json`),
5492
+ policyViolationsPath: import_node_path34.default.join(baseDir, `policy-violations.${project}.ndjson`)
5072
5493
  };
5073
5494
  }
5074
5495
  var Projects = class {
@@ -5104,65 +5525,135 @@ var Projects = class {
5104
5525
 
5105
5526
  // src/registry.ts
5106
5527
  init_cjs_shims();
5107
- var import_node_fs20 = require("fs");
5108
- var import_node_os2 = __toESM(require("os"), 1);
5109
- var import_node_path33 = __toESM(require("path"), 1);
5528
+ var import_node_fs22 = require("fs");
5529
+ var import_node_os3 = __toESM(require("os"), 1);
5530
+ var import_node_path35 = __toESM(require("path"), 1);
5110
5531
  var import_types22 = require("@neat.is/types");
5111
5532
  var LOCK_TIMEOUT_MS = 5e3;
5112
5533
  var LOCK_RETRY_MS = 50;
5113
5534
  function neatHome() {
5114
5535
  const override = process.env.NEAT_HOME;
5115
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
5116
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
5536
+ if (override && override.length > 0) return import_node_path35.default.resolve(override);
5537
+ return import_node_path35.default.join(import_node_os3.default.homedir(), ".neat");
5117
5538
  }
5118
5539
  function registryPath() {
5119
- return import_node_path33.default.join(neatHome(), "projects.json");
5540
+ return import_node_path35.default.join(neatHome(), "projects.json");
5120
5541
  }
5121
5542
  function registryLockPath() {
5122
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
5543
+ return import_node_path35.default.join(neatHome(), "projects.json.lock");
5544
+ }
5545
+ function daemonPidPath() {
5546
+ return import_node_path35.default.join(neatHome(), "neatd.pid");
5547
+ }
5548
+ function isPidAliveDefault(pid) {
5549
+ try {
5550
+ process.kill(pid, 0);
5551
+ return true;
5552
+ } catch (err) {
5553
+ return err.code === "EPERM";
5554
+ }
5555
+ }
5556
+ async function readPidFile(file) {
5557
+ try {
5558
+ const raw = await import_node_fs22.promises.readFile(file, "utf8");
5559
+ const pid = Number.parseInt(raw.trim(), 10);
5560
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5561
+ } catch {
5562
+ return void 0;
5563
+ }
5564
+ }
5565
+ var defaultLockHolderProbe = {
5566
+ isPidAlive: isPidAliveDefault,
5567
+ daemonPidFromFile: () => readPidFile(daemonPidPath()),
5568
+ async daemonResponds() {
5569
+ const base = process.env.NEAT_API_URL ?? "http://localhost:8080";
5570
+ try {
5571
+ await fetch(`${base}/health`, { signal: AbortSignal.timeout(750) });
5572
+ return true;
5573
+ } catch {
5574
+ return false;
5575
+ }
5576
+ }
5577
+ };
5578
+ async function readLockPid(lockPath) {
5579
+ return readPidFile(lockPath);
5580
+ }
5581
+ async function classifyLockHolder(lockPath, probe = defaultLockHolderProbe) {
5582
+ const lockPid = await readLockPid(lockPath);
5583
+ const daemonPid = await probe.daemonPidFromFile();
5584
+ if (daemonPid !== void 0 && daemonPid !== process.pid && probe.isPidAlive(daemonPid) && // The lock already names the daemon, or the daemon answers on its port.
5585
+ // Either confirms a live daemon is in the picture (the second guards
5586
+ // against a stale pidfile whose PID got reused).
5587
+ (daemonPid === lockPid || await probe.daemonResponds())) {
5588
+ return { kind: "daemon", pid: daemonPid };
5589
+ }
5590
+ if (lockPid !== void 0 && lockPid !== process.pid && probe.isPidAlive(lockPid)) {
5591
+ return { kind: "command", pid: lockPid };
5592
+ }
5593
+ return { kind: "stale" };
5594
+ }
5595
+ function lockHolderMessage(holder, lockPath, timeoutMs) {
5596
+ switch (holder.kind) {
5597
+ case "daemon":
5598
+ 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\`.`;
5599
+ case "command":
5600
+ 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.`;
5601
+ case "stale":
5602
+ 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.`;
5603
+ }
5123
5604
  }
5124
5605
  async function normalizeProjectPath(input) {
5125
- const resolved = import_node_path33.default.resolve(input);
5606
+ const resolved = import_node_path35.default.resolve(input);
5126
5607
  try {
5127
- return await import_node_fs20.promises.realpath(resolved);
5608
+ return await import_node_fs22.promises.realpath(resolved);
5128
5609
  } catch {
5129
5610
  return resolved;
5130
5611
  }
5131
5612
  }
5132
5613
  async function writeAtomically(target, contents) {
5133
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
5614
+ await import_node_fs22.promises.mkdir(import_node_path35.default.dirname(target), { recursive: true });
5134
5615
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5135
- const fd = await import_node_fs20.promises.open(tmp, "w");
5616
+ const fd = await import_node_fs22.promises.open(tmp, "w");
5136
5617
  try {
5137
5618
  await fd.writeFile(contents, "utf8");
5138
5619
  await fd.sync();
5139
5620
  } finally {
5140
5621
  await fd.close();
5141
5622
  }
5142
- await import_node_fs20.promises.rename(tmp, target);
5623
+ await import_node_fs22.promises.rename(tmp, target);
5143
5624
  }
5144
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
5625
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5145
5626
  const deadline = Date.now() + timeoutMs;
5146
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
5627
+ await import_node_fs22.promises.mkdir(import_node_path35.default.dirname(lockPath), { recursive: true });
5628
+ let probedHolder = false;
5147
5629
  while (true) {
5148
5630
  try {
5149
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
5150
- await fd.close();
5631
+ const fd = await import_node_fs22.promises.open(lockPath, "wx");
5632
+ try {
5633
+ await fd.writeFile(`${process.pid}
5634
+ `, "utf8");
5635
+ } finally {
5636
+ await fd.close();
5637
+ }
5151
5638
  return;
5152
5639
  } catch (err) {
5153
5640
  const code = err.code;
5154
5641
  if (code !== "EEXIST") throw err;
5642
+ if (!probedHolder) {
5643
+ probedHolder = true;
5644
+ const holder = await classifyLockHolder(lockPath, probe);
5645
+ if (holder.kind === "daemon") throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5646
+ }
5155
5647
  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
- );
5648
+ const holder = await classifyLockHolder(lockPath, probe);
5649
+ throw new Error(lockHolderMessage(holder, lockPath, timeoutMs));
5159
5650
  }
5160
5651
  await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
5161
5652
  }
5162
5653
  }
5163
5654
  }
5164
5655
  async function releaseLock(lockPath) {
5165
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
5656
+ await import_node_fs22.promises.unlink(lockPath).catch(() => {
5166
5657
  });
5167
5658
  }
5168
5659
  async function withLock(fn) {
@@ -5178,7 +5669,7 @@ async function readRegistry() {
5178
5669
  const file = registryPath();
5179
5670
  let raw;
5180
5671
  try {
5181
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5672
+ raw = await import_node_fs22.promises.readFile(file, "utf8");
5182
5673
  } catch (err) {
5183
5674
  if (err.code === "ENOENT") {
5184
5675
  return { version: 1, projects: [] };
@@ -5728,6 +6219,105 @@ function registerRoutes(scope, ctx) {
5728
6219
  violations
5729
6220
  };
5730
6221
  });
6222
+ scope.get("/extend/list-uninstrumented", async (req, reply) => {
6223
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6224
+ if (!proj) return;
6225
+ if (!proj.scanPath) {
6226
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6227
+ }
6228
+ try {
6229
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
6230
+ return { libraries: results };
6231
+ } catch (err) {
6232
+ return reply.code(500).send({ error: err.message });
6233
+ }
6234
+ });
6235
+ scope.get("/extend/lookup", async (req, reply) => {
6236
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6237
+ if (!proj) return;
6238
+ const { library, version } = req.query;
6239
+ if (!library) {
6240
+ return reply.code(400).send({ error: "query parameter `library` is required" });
6241
+ }
6242
+ const result = lookupInstrumentation(library, version);
6243
+ if (!result) {
6244
+ return reply.code(404).send({ error: "library not found in registry", library });
6245
+ }
6246
+ return result;
6247
+ });
6248
+ scope.get("/extend/describe", async (req, reply) => {
6249
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6250
+ if (!proj) return;
6251
+ if (!proj.scanPath) {
6252
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6253
+ }
6254
+ try {
6255
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
6256
+ return state;
6257
+ } catch (err) {
6258
+ return reply.code(500).send({ error: err.message });
6259
+ }
6260
+ });
6261
+ scope.post("/extend/apply", async (req, reply) => {
6262
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6263
+ if (!proj) return;
6264
+ if (!proj.scanPath) {
6265
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6266
+ }
6267
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6268
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6269
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6270
+ }
6271
+ try {
6272
+ const result = await applyExtension(
6273
+ { project: proj.name, scanPath: proj.scanPath },
6274
+ { library, instrumentation_package, version, registration_snippet }
6275
+ );
6276
+ return result;
6277
+ } catch (err) {
6278
+ return reply.code(500).send({ error: err.message });
6279
+ }
6280
+ });
6281
+ scope.post("/extend/dry-run", async (req, reply) => {
6282
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6283
+ if (!proj) return;
6284
+ if (!proj.scanPath) {
6285
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6286
+ }
6287
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6288
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6289
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6290
+ }
6291
+ try {
6292
+ const result = await dryRunExtension(
6293
+ { project: proj.name, scanPath: proj.scanPath },
6294
+ { library, instrumentation_package, version, registration_snippet }
6295
+ );
6296
+ return result;
6297
+ } catch (err) {
6298
+ return reply.code(500).send({ error: err.message });
6299
+ }
6300
+ });
6301
+ scope.post("/extend/rollback", async (req, reply) => {
6302
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6303
+ if (!proj) return;
6304
+ if (!proj.scanPath) {
6305
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6306
+ }
6307
+ const { library } = req.body ?? {};
6308
+ if (!library) {
6309
+ return reply.code(400).send({ error: "body must include library" });
6310
+ }
6311
+ try {
6312
+ const result = await rollbackExtension(
6313
+ { project: proj.name, scanPath: proj.scanPath },
6314
+ { library }
6315
+ );
6316
+ return result;
6317
+ } catch (err) {
6318
+ return reply.code(500).send({ error: err.message });
6319
+ }
6320
+ });
5731
6321
  }
5732
6322
  async function buildApi(opts) {
5733
6323
  const app = (0, import_fastify.default)({ logger: false });
@@ -5838,15 +6428,15 @@ init_otel_grpc();
5838
6428
 
5839
6429
  // src/daemon.ts
5840
6430
  init_cjs_shims();
5841
- var import_node_fs22 = require("fs");
5842
- var import_node_path37 = __toESM(require("path"), 1);
6431
+ var import_node_fs24 = require("fs");
6432
+ var import_node_path39 = __toESM(require("path"), 1);
5843
6433
  init_otel();
5844
6434
  init_auth();
5845
6435
 
5846
6436
  // src/unrouted.ts
5847
6437
  init_cjs_shims();
5848
- var import_node_fs21 = require("fs");
5849
- var import_node_path36 = __toESM(require("path"), 1);
6438
+ var import_node_fs23 = require("fs");
6439
+ var import_node_path38 = __toESM(require("path"), 1);
5850
6440
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
5851
6441
  return {
5852
6442
  timestamp: now.toISOString(),
@@ -5856,21 +6446,21 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
5856
6446
  };
5857
6447
  }
5858
6448
  async function appendUnroutedSpan(neatHome2, record) {
5859
- const target = import_node_path36.default.join(neatHome2, "errors.ndjson");
5860
- await import_node_fs21.promises.mkdir(neatHome2, { recursive: true });
5861
- await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6449
+ const target = import_node_path38.default.join(neatHome2, "errors.ndjson");
6450
+ await import_node_fs23.promises.mkdir(neatHome2, { recursive: true });
6451
+ await import_node_fs23.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
5862
6452
  }
5863
6453
  function unroutedErrorsPath(neatHome2) {
5864
- return import_node_path36.default.join(neatHome2, "errors.ndjson");
6454
+ return import_node_path38.default.join(neatHome2, "errors.ndjson");
5865
6455
  }
5866
6456
 
5867
6457
  // src/daemon.ts
5868
6458
  function neatHomeFor(opts) {
5869
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
6459
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path39.default.resolve(opts.neatHome);
5870
6460
  const env = process.env.NEAT_HOME;
5871
- if (env && env.length > 0) return import_node_path37.default.resolve(env);
6461
+ if (env && env.length > 0) return import_node_path39.default.resolve(env);
5872
6462
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5873
- return import_node_path37.default.join(home, ".neat");
6463
+ return import_node_path39.default.join(home, ".neat");
5874
6464
  }
5875
6465
  function routeSpanToProject(serviceName, projects) {
5876
6466
  if (!serviceName) return DEFAULT_PROJECT;
@@ -5905,9 +6495,9 @@ function isTokenContained(needle, haystack) {
5905
6495
  return tokens.includes(needle);
5906
6496
  }
5907
6497
  async function bootstrapProject(entry) {
5908
- const paths = pathsForProject(entry.name, import_node_path37.default.join(entry.path, "neat-out"));
6498
+ const paths = pathsForProject(entry.name, import_node_path39.default.join(entry.path, "neat-out"));
5909
6499
  try {
5910
- const stat = await import_node_fs22.promises.stat(entry.path);
6500
+ const stat = await import_node_fs24.promises.stat(entry.path);
5911
6501
  if (!stat.isDirectory()) {
5912
6502
  throw new Error(`registered path ${entry.path} is not a directory`);
5913
6503
  }
@@ -5973,13 +6563,13 @@ async function startDaemon(opts = {}) {
5973
6563
  const home = neatHomeFor(opts);
5974
6564
  const regPath = registryPath();
5975
6565
  try {
5976
- await import_node_fs22.promises.access(regPath);
6566
+ await import_node_fs24.promises.access(regPath);
5977
6567
  } catch {
5978
6568
  throw new Error(
5979
6569
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
5980
6570
  );
5981
6571
  }
5982
- const pidPath = import_node_path37.default.join(home, "neatd.pid");
6572
+ const pidPath = import_node_path39.default.join(home, "neatd.pid");
5983
6573
  await writeAtomically(pidPath, `${process.pid}
5984
6574
  `);
5985
6575
  const slots = /* @__PURE__ */ new Map();
@@ -6137,7 +6727,7 @@ async function startDaemon(opts = {}) {
6137
6727
  }
6138
6728
  if (restApp) await restApp.close().catch(() => {
6139
6729
  });
6140
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6730
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6141
6731
  });
6142
6732
  throw new Error(
6143
6733
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -6193,6 +6783,7 @@ async function startDaemon(opts = {}) {
6193
6783
  {
6194
6784
  graph: slot.graph,
6195
6785
  errorsPath: slot.paths.errorsPath,
6786
+ scanPath: slot.entry.path,
6196
6787
  project: slot.entry.name,
6197
6788
  // Receiver already wrote the error event synchronously below.
6198
6789
  writeErrorEventInline: false
@@ -6216,6 +6807,7 @@ async function startDaemon(opts = {}) {
6216
6807
  {
6217
6808
  graph: slot.graph,
6218
6809
  errorsPath: slot.paths.errorsPath,
6810
+ scanPath: slot.entry.path,
6219
6811
  project: slot.entry.name,
6220
6812
  writeErrorEventInline: false
6221
6813
  },
@@ -6241,7 +6833,7 @@ async function startDaemon(opts = {}) {
6241
6833
  });
6242
6834
  if (otlpApp) await otlpApp.close().catch(() => {
6243
6835
  });
6244
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6836
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6245
6837
  });
6246
6838
  throw new Error(
6247
6839
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -6287,9 +6879,9 @@ async function startDaemon(opts = {}) {
6287
6879
  let registryWatcher = null;
6288
6880
  let reloadTimer = null;
6289
6881
  try {
6290
- const regDir = import_node_path37.default.dirname(regPath);
6291
- const regBase = import_node_path37.default.basename(regPath);
6292
- registryWatcher = (0, import_node_fs22.watch)(regDir, (_eventType, filename) => {
6882
+ const regDir = import_node_path39.default.dirname(regPath);
6883
+ const regBase = import_node_path39.default.basename(regPath);
6884
+ registryWatcher = (0, import_node_fs24.watch)(regDir, (_eventType, filename) => {
6293
6885
  if (filename !== null && filename !== regBase) return;
6294
6886
  if (reloadTimer) clearTimeout(reloadTimer);
6295
6887
  reloadTimer = setTimeout(() => {
@@ -6332,7 +6924,7 @@ async function startDaemon(opts = {}) {
6332
6924
  } catch {
6333
6925
  }
6334
6926
  }
6335
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6927
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6336
6928
  });
6337
6929
  };
6338
6930
  return {