@neat.is/core 0.4.11 → 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/neatd.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", (req2, reply, done) => {
60
- const path40 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path42 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path40 === suffix || path40.endsWith(suffix)) {
62
+ if (path42 === suffix || path42.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -185,8 +185,8 @@ function reshapeGrpcRequest(req2) {
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 }
@@ -566,12 +566,12 @@ async function buildOtelReceiver(opts) {
566
566
  };
567
567
  return decorated;
568
568
  }
569
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
569
+ var import_node_path37, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
570
570
  var init_otel = __esm({
571
571
  "src/otel.ts"() {
572
572
  "use strict";
573
573
  init_cjs_shims();
574
- import_node_path35 = __toESM(require("path"), 1);
574
+ import_node_path37 = __toESM(require("path"), 1);
575
575
  import_node_url2 = require("url");
576
576
  import_fastify2 = __toESM(require("fastify"), 1);
577
577
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -587,14 +587,14 @@ var init_otel = __esm({
587
587
 
588
588
  // src/neatd.ts
589
589
  init_cjs_shims();
590
- var import_node_fs23 = require("fs");
591
- var import_node_path39 = __toESM(require("path"), 1);
590
+ var import_node_fs25 = require("fs");
591
+ var import_node_path41 = __toESM(require("path"), 1);
592
592
  var import_node_module = require("module");
593
593
 
594
594
  // src/daemon.ts
595
595
  init_cjs_shims();
596
- var import_node_fs22 = require("fs");
597
- var import_node_path37 = __toESM(require("path"), 1);
596
+ var import_node_fs24 = require("fs");
597
+ var import_node_path39 = __toESM(require("path"), 1);
598
598
 
599
599
  // src/graph.ts
600
600
  init_cjs_shims();
@@ -1073,19 +1073,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1073
1073
  function longestIncomingWalk(graph, start, maxDepth) {
1074
1074
  let best = { path: [start], edges: [] };
1075
1075
  const visited = /* @__PURE__ */ new Set([start]);
1076
- function step(node, path40, edges) {
1077
- if (path40.length > best.path.length) {
1078
- best = { path: [...path40], edges: [...edges] };
1076
+ function step(node, path42, edges) {
1077
+ if (path42.length > best.path.length) {
1078
+ best = { path: [...path42], edges: [...edges] };
1079
1079
  }
1080
- if (path40.length - 1 >= maxDepth) return;
1080
+ if (path42.length - 1 >= maxDepth) return;
1081
1081
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1082
1082
  for (const [srcId, edge] of incoming) {
1083
1083
  if (visited.has(srcId)) continue;
1084
1084
  visited.add(srcId);
1085
- path40.push(srcId);
1085
+ path42.push(srcId);
1086
1086
  edges.push(edge);
1087
- step(srcId, path40, edges);
1088
- path40.pop();
1087
+ step(srcId, path42, edges);
1088
+ path42.pop();
1089
1089
  edges.pop();
1090
1090
  visited.delete(srcId);
1091
1091
  }
@@ -1895,7 +1895,7 @@ function ensureFrontierNode(graph, host, ts) {
1895
1895
  graph.addNode(id, node);
1896
1896
  return id;
1897
1897
  }
1898
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1898
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1899
1899
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1900
1900
  const id = makeObservedEdgeId(type, source, target);
1901
1901
  if (graph.hasEdge(id)) {
@@ -1932,7 +1932,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1932
1932
  confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1933
1933
  lastObserved: ts,
1934
1934
  callCount: 1,
1935
- signal
1935
+ signal,
1936
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
1937
+ // Only set when code.filepath was present on the span — never fabricated.
1938
+ ...evidence ? { evidence } : {}
1936
1939
  };
1937
1940
  graph.addEdgeWithKey(id, source, target, edge);
1938
1941
  return { edge, created: true };
@@ -2026,6 +2029,7 @@ async function handleSpan(ctx, span) {
2026
2029
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2027
2030
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2028
2031
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2032
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2029
2033
  let affectedNode = sourceId;
2030
2034
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2031
2035
  if (span.dbSystem) {
@@ -2039,7 +2043,8 @@ async function handleSpan(ctx, span) {
2039
2043
  observedSource(),
2040
2044
  targetId,
2041
2045
  ts,
2042
- isError
2046
+ isError,
2047
+ callSiteEvidence
2043
2048
  );
2044
2049
  if (result) affectedNode = targetId;
2045
2050
  }
@@ -2055,7 +2060,8 @@ async function handleSpan(ctx, span) {
2055
2060
  observedSource(),
2056
2061
  targetId,
2057
2062
  ts,
2058
- isError
2063
+ isError,
2064
+ callSiteEvidence
2059
2065
  );
2060
2066
  affectedNode = targetId;
2061
2067
  resolvedViaAddress = true;
@@ -2067,7 +2073,8 @@ async function handleSpan(ctx, span) {
2067
2073
  observedSource(),
2068
2074
  frontierNodeId,
2069
2075
  ts,
2070
- isError
2076
+ isError,
2077
+ callSiteEvidence
2071
2078
  );
2072
2079
  affectedNode = frontierNodeId;
2073
2080
  resolvedViaAddress = true;
@@ -2945,14 +2952,111 @@ async function addServiceAliases(graph, scanPath, services) {
2945
2952
 
2946
2953
  // src/extract/databases/index.ts
2947
2954
  init_cjs_shims();
2948
- var import_node_path17 = __toESM(require("path"), 1);
2955
+ var import_node_path18 = __toESM(require("path"), 1);
2956
+ var import_types8 = require("@neat.is/types");
2957
+
2958
+ // src/extract/calls/shared.ts
2959
+ init_cjs_shims();
2960
+ var import_node_fs10 = require("fs");
2961
+ var import_node_path10 = __toESM(require("path"), 1);
2949
2962
  var import_types7 = require("@neat.is/types");
2963
+ async function walkSourceFiles(dir) {
2964
+ const out = [];
2965
+ async function walk(current) {
2966
+ const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2967
+ for (const entry2 of entries) {
2968
+ const full = import_node_path10.default.join(current, entry2.name);
2969
+ if (entry2.isDirectory()) {
2970
+ if (IGNORED_DIRS.has(entry2.name)) continue;
2971
+ if (await isPythonVenvDir(full)) continue;
2972
+ await walk(full);
2973
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name))) {
2974
+ out.push(full);
2975
+ }
2976
+ }
2977
+ }
2978
+ await walk(dir);
2979
+ return out;
2980
+ }
2981
+ async function loadSourceFiles(dir) {
2982
+ const paths = await walkSourceFiles(dir);
2983
+ const out = [];
2984
+ for (const p of paths) {
2985
+ try {
2986
+ const content = await import_node_fs10.promises.readFile(p, "utf8");
2987
+ out.push({ path: p, content });
2988
+ } catch {
2989
+ }
2990
+ }
2991
+ return out;
2992
+ }
2993
+ function lineOf(text, needle) {
2994
+ const idx = text.indexOf(needle);
2995
+ if (idx < 0) return 1;
2996
+ return text.slice(0, idx).split("\n").length;
2997
+ }
2998
+ function snippet(text, line) {
2999
+ const lines = text.split("\n");
3000
+ return (lines[line - 1] ?? "").trim();
3001
+ }
3002
+ function toPosix2(p) {
3003
+ return p.split("\\").join("/");
3004
+ }
3005
+ function languageForPath(relPath) {
3006
+ switch (import_node_path10.default.extname(relPath).toLowerCase()) {
3007
+ case ".py":
3008
+ return "python";
3009
+ case ".ts":
3010
+ case ".tsx":
3011
+ return "typescript";
3012
+ case ".js":
3013
+ case ".jsx":
3014
+ case ".mjs":
3015
+ case ".cjs":
3016
+ return "javascript";
3017
+ default:
3018
+ return void 0;
3019
+ }
3020
+ }
3021
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3022
+ let nodesAdded = 0;
3023
+ let edgesAdded = 0;
3024
+ const fileNodeId = (0, import_types7.fileId)(serviceName, relPath);
3025
+ if (!graph.hasNode(fileNodeId)) {
3026
+ const language = languageForPath(relPath);
3027
+ const node = {
3028
+ id: fileNodeId,
3029
+ type: import_types7.NodeType.FileNode,
3030
+ service: serviceName,
3031
+ path: relPath,
3032
+ ...language ? { language } : {},
3033
+ discoveredVia: "static"
3034
+ };
3035
+ graph.addNode(fileNodeId, node);
3036
+ nodesAdded++;
3037
+ }
3038
+ const containsId = (0, import_types7.extractedEdgeId)(serviceNodeId, fileNodeId, import_types7.EdgeType.CONTAINS);
3039
+ if (!graph.hasEdge(containsId)) {
3040
+ const edge = {
3041
+ id: containsId,
3042
+ source: serviceNodeId,
3043
+ target: fileNodeId,
3044
+ type: import_types7.EdgeType.CONTAINS,
3045
+ provenance: import_types7.Provenance.EXTRACTED,
3046
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3047
+ evidence: { file: relPath }
3048
+ };
3049
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3050
+ edgesAdded++;
3051
+ }
3052
+ return { fileNodeId, nodesAdded, edgesAdded };
3053
+ }
2950
3054
 
2951
3055
  // src/extract/databases/db-config-yaml.ts
2952
3056
  init_cjs_shims();
2953
- var import_node_path10 = __toESM(require("path"), 1);
3057
+ var import_node_path11 = __toESM(require("path"), 1);
2954
3058
  async function parse(serviceDir) {
2955
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3059
+ const yamlPath = import_node_path11.default.join(serviceDir, "db-config.yaml");
2956
3060
  if (!await exists(yamlPath)) return [];
2957
3061
  const raw = await readYaml(yamlPath);
2958
3062
  return [
@@ -2970,13 +3074,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2970
3074
 
2971
3075
  // src/extract/databases/dotenv.ts
2972
3076
  init_cjs_shims();
2973
- var import_node_fs11 = require("fs");
2974
- var import_node_path12 = __toESM(require("path"), 1);
3077
+ var import_node_fs12 = require("fs");
3078
+ var import_node_path13 = __toESM(require("path"), 1);
2975
3079
 
2976
3080
  // src/extract/databases/shared.ts
2977
3081
  init_cjs_shims();
2978
- var import_node_fs10 = require("fs");
2979
- var import_node_path11 = __toESM(require("path"), 1);
3082
+ var import_node_fs11 = require("fs");
3083
+ var import_node_path12 = __toESM(require("path"), 1);
2980
3084
  function schemeToEngine(scheme) {
2981
3085
  const s = scheme.toLowerCase().split("+")[0];
2982
3086
  switch (s) {
@@ -3015,14 +3119,14 @@ function parseConnectionString(url) {
3015
3119
  }
3016
3120
  async function readIfExists(filePath) {
3017
3121
  try {
3018
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3122
+ return await import_node_fs11.promises.readFile(filePath, "utf8");
3019
3123
  } catch {
3020
3124
  return null;
3021
3125
  }
3022
3126
  }
3023
3127
  async function findFirst(serviceDir, candidates) {
3024
3128
  for (const rel of candidates) {
3025
- const abs = import_node_path11.default.join(serviceDir, rel);
3129
+ const abs = import_node_path12.default.join(serviceDir, rel);
3026
3130
  const content = await readIfExists(abs);
3027
3131
  if (content !== null) return abs;
3028
3132
  }
@@ -3073,15 +3177,15 @@ function parseDotenvLine(line) {
3073
3177
  return { key, value };
3074
3178
  }
3075
3179
  async function parse2(serviceDir) {
3076
- const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3180
+ const entries = await import_node_fs12.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3077
3181
  const configs = [];
3078
3182
  const seen = /* @__PURE__ */ new Set();
3079
3183
  for (const entry2 of entries) {
3080
3184
  if (!entry2.isFile()) continue;
3081
3185
  const match = isConfigFile(entry2.name);
3082
3186
  if (!match.match || match.fileType !== "env") continue;
3083
- const filePath = import_node_path12.default.join(serviceDir, entry2.name);
3084
- const content = await import_node_fs11.promises.readFile(filePath, "utf8");
3187
+ const filePath = import_node_path13.default.join(serviceDir, entry2.name);
3188
+ const content = await import_node_fs12.promises.readFile(filePath, "utf8");
3085
3189
  for (const line of content.split("\n")) {
3086
3190
  const parsed = parseDotenvLine(line);
3087
3191
  if (!parsed) continue;
@@ -3100,9 +3204,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3100
3204
 
3101
3205
  // src/extract/databases/prisma.ts
3102
3206
  init_cjs_shims();
3103
- var import_node_path13 = __toESM(require("path"), 1);
3207
+ var import_node_path14 = __toESM(require("path"), 1);
3104
3208
  async function parse3(serviceDir) {
3105
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
3209
+ const schemaPath = import_node_path14.default.join(serviceDir, "prisma", "schema.prisma");
3106
3210
  const content = await readIfExists(schemaPath);
3107
3211
  if (!content) return [];
3108
3212
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3234,10 +3338,10 @@ var knexParser = { name: "knex", parse: parse5 };
3234
3338
 
3235
3339
  // src/extract/databases/ormconfig.ts
3236
3340
  init_cjs_shims();
3237
- var import_node_path14 = __toESM(require("path"), 1);
3341
+ var import_node_path15 = __toESM(require("path"), 1);
3238
3342
  async function parse6(serviceDir) {
3239
3343
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
3240
- const abs = import_node_path14.default.join(serviceDir, candidate);
3344
+ const abs = import_node_path15.default.join(serviceDir, candidate);
3241
3345
  if (!await exists(abs)) continue;
3242
3346
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3243
3347
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -3297,9 +3401,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3297
3401
 
3298
3402
  // src/extract/databases/sequelize.ts
3299
3403
  init_cjs_shims();
3300
- var import_node_path15 = __toESM(require("path"), 1);
3404
+ var import_node_path16 = __toESM(require("path"), 1);
3301
3405
  async function parse8(serviceDir) {
3302
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3406
+ const configPath = import_node_path16.default.join(serviceDir, "config", "config.json");
3303
3407
  if (!await exists(configPath)) return [];
3304
3408
  const raw = await readJson(configPath);
3305
3409
  const out = [];
@@ -3326,7 +3430,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3326
3430
 
3327
3431
  // src/extract/databases/docker-compose.ts
3328
3432
  init_cjs_shims();
3329
- var import_node_path16 = __toESM(require("path"), 1);
3433
+ var import_node_path17 = __toESM(require("path"), 1);
3330
3434
  function portFromService(svc) {
3331
3435
  for (const raw of svc.ports ?? []) {
3332
3436
  const str = String(raw);
@@ -3353,7 +3457,7 @@ function databaseFromEnv(svc) {
3353
3457
  }
3354
3458
  async function parse9(serviceDir) {
3355
3459
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3356
- const abs = import_node_path16.default.join(serviceDir, name);
3460
+ const abs = import_node_path17.default.join(serviceDir, name);
3357
3461
  if (!await exists(abs)) continue;
3358
3462
  const raw = await readYaml(abs);
3359
3463
  if (!raw?.services) return [];
@@ -3394,8 +3498,8 @@ function compatibleDriversFor(engine) {
3394
3498
  }
3395
3499
  function toDatabaseNode(config) {
3396
3500
  return {
3397
- id: (0, import_types7.databaseId)(config.host),
3398
- type: import_types7.NodeType.DatabaseNode,
3501
+ id: (0, import_types8.databaseId)(config.host),
3502
+ type: import_types8.NodeType.DatabaseNode,
3399
3503
  name: config.database || config.host,
3400
3504
  engine: config.engine,
3401
3505
  engineVersion: config.engineVersion,
@@ -3525,19 +3629,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3525
3629
  discoveredVia: mergedDiscoveredVia
3526
3630
  });
3527
3631
  }
3632
+ const relConfigFile = toPosix2(import_node_path18.default.relative(service.dir, config.sourceFile));
3633
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3634
+ graph,
3635
+ service.pkg.name,
3636
+ service.node.id,
3637
+ relConfigFile
3638
+ );
3639
+ nodesAdded += fn;
3640
+ edgesAdded += fe;
3641
+ const evidenceFile = toPosix2(import_node_path18.default.relative(scanPath, config.sourceFile));
3528
3642
  const edge = {
3529
- id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
3530
- source: service.node.id,
3643
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, dbNode.id, import_types8.EdgeType.CONNECTS_TO),
3644
+ source: fileNodeId,
3531
3645
  target: dbNode.id,
3532
- type: import_types7.EdgeType.CONNECTS_TO,
3533
- provenance: import_types7.Provenance.EXTRACTED,
3534
- confidence: (0, import_types7.confidenceForExtracted)("structural"),
3535
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3536
- // Ghost-edge cleanup keys retirement on this; the conditional
3537
- // sourceFile spread that used to live here was a v0.1.x leftover.
3538
- evidence: {
3539
- file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3540
- }
3646
+ type: import_types8.EdgeType.CONNECTS_TO,
3647
+ provenance: import_types8.Provenance.EXTRACTED,
3648
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3649
+ evidence: { file: evidenceFile }
3541
3650
  };
3542
3651
  if (!graph.hasEdge(edge.id)) {
3543
3652
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3563,15 +3672,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3563
3672
 
3564
3673
  // src/extract/configs.ts
3565
3674
  init_cjs_shims();
3566
- var import_node_fs12 = require("fs");
3567
- var import_node_path18 = __toESM(require("path"), 1);
3568
- var import_types8 = require("@neat.is/types");
3675
+ var import_node_fs13 = require("fs");
3676
+ var import_node_path19 = __toESM(require("path"), 1);
3677
+ var import_types9 = require("@neat.is/types");
3569
3678
  async function walkConfigFiles(dir) {
3570
3679
  const out = [];
3571
3680
  async function walk(current) {
3572
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3681
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true });
3573
3682
  for (const entry2 of entries) {
3574
- const full = import_node_path18.default.join(current, entry2.name);
3683
+ const full = import_node_path19.default.join(current, entry2.name);
3575
3684
  if (entry2.isDirectory()) {
3576
3685
  if (IGNORED_DIRS.has(entry2.name)) continue;
3577
3686
  if (await isPythonVenvDir(full)) continue;
@@ -3590,26 +3699,35 @@ async function addConfigNodes(graph, services, scanPath) {
3590
3699
  for (const service of services) {
3591
3700
  const configFiles = await walkConfigFiles(service.dir);
3592
3701
  for (const file of configFiles) {
3593
- const relPath = import_node_path18.default.relative(scanPath, file);
3702
+ const relPath = import_node_path19.default.relative(scanPath, file);
3594
3703
  const node = {
3595
- id: (0, import_types8.configId)(relPath),
3596
- type: import_types8.NodeType.ConfigNode,
3597
- name: import_node_path18.default.basename(file),
3704
+ id: (0, import_types9.configId)(relPath),
3705
+ type: import_types9.NodeType.ConfigNode,
3706
+ name: import_node_path19.default.basename(file),
3598
3707
  path: relPath,
3599
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3708
+ fileType: isConfigFile(import_node_path19.default.basename(file)).fileType
3600
3709
  };
3601
3710
  if (!graph.hasNode(node.id)) {
3602
3711
  graph.addNode(node.id, node);
3603
3712
  nodesAdded++;
3604
3713
  }
3714
+ const relToService = toPosix2(import_node_path19.default.relative(service.dir, file));
3715
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3716
+ graph,
3717
+ service.pkg.name,
3718
+ service.node.id,
3719
+ relToService
3720
+ );
3721
+ nodesAdded += fn;
3722
+ edgesAdded += fe;
3605
3723
  const edge = {
3606
- id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
3607
- source: service.node.id,
3724
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types9.EdgeType.CONFIGURED_BY),
3725
+ source: fileNodeId,
3608
3726
  target: node.id,
3609
- type: import_types8.EdgeType.CONFIGURED_BY,
3610
- provenance: import_types8.Provenance.EXTRACTED,
3611
- confidence: (0, import_types8.confidenceForExtracted)("structural"),
3612
- evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3727
+ type: import_types9.EdgeType.CONFIGURED_BY,
3728
+ provenance: import_types9.Provenance.EXTRACTED,
3729
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3730
+ evidence: { file: relPath.split(import_node_path19.default.sep).join("/") }
3613
3731
  };
3614
3732
  if (!graph.hasEdge(edge.id)) {
3615
3733
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3631,105 +3749,6 @@ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3631
3749
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3632
3750
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3633
3751
  var import_types10 = require("@neat.is/types");
3634
-
3635
- // src/extract/calls/shared.ts
3636
- init_cjs_shims();
3637
- var import_node_fs13 = require("fs");
3638
- var import_node_path19 = __toESM(require("path"), 1);
3639
- var import_types9 = require("@neat.is/types");
3640
- async function walkSourceFiles(dir) {
3641
- const out = [];
3642
- async function walk(current) {
3643
- const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3644
- for (const entry2 of entries) {
3645
- const full = import_node_path19.default.join(current, entry2.name);
3646
- if (entry2.isDirectory()) {
3647
- if (IGNORED_DIRS.has(entry2.name)) continue;
3648
- if (await isPythonVenvDir(full)) continue;
3649
- await walk(full);
3650
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3651
- out.push(full);
3652
- }
3653
- }
3654
- }
3655
- await walk(dir);
3656
- return out;
3657
- }
3658
- async function loadSourceFiles(dir) {
3659
- const paths = await walkSourceFiles(dir);
3660
- const out = [];
3661
- for (const p of paths) {
3662
- try {
3663
- const content = await import_node_fs13.promises.readFile(p, "utf8");
3664
- out.push({ path: p, content });
3665
- } catch {
3666
- }
3667
- }
3668
- return out;
3669
- }
3670
- function lineOf(text, needle) {
3671
- const idx = text.indexOf(needle);
3672
- if (idx < 0) return 1;
3673
- return text.slice(0, idx).split("\n").length;
3674
- }
3675
- function snippet(text, line) {
3676
- const lines = text.split("\n");
3677
- return (lines[line - 1] ?? "").trim();
3678
- }
3679
- function toPosix2(p) {
3680
- return p.split("\\").join("/");
3681
- }
3682
- function languageForPath(relPath) {
3683
- switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3684
- case ".py":
3685
- return "python";
3686
- case ".ts":
3687
- case ".tsx":
3688
- return "typescript";
3689
- case ".js":
3690
- case ".jsx":
3691
- case ".mjs":
3692
- case ".cjs":
3693
- return "javascript";
3694
- default:
3695
- return void 0;
3696
- }
3697
- }
3698
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3699
- let nodesAdded = 0;
3700
- let edgesAdded = 0;
3701
- const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3702
- if (!graph.hasNode(fileNodeId)) {
3703
- const language = languageForPath(relPath);
3704
- const node = {
3705
- id: fileNodeId,
3706
- type: import_types9.NodeType.FileNode,
3707
- service: serviceName,
3708
- path: relPath,
3709
- ...language ? { language } : {},
3710
- discoveredVia: "static"
3711
- };
3712
- graph.addNode(fileNodeId, node);
3713
- nodesAdded++;
3714
- }
3715
- const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3716
- if (!graph.hasEdge(containsId)) {
3717
- const edge = {
3718
- id: containsId,
3719
- source: serviceNodeId,
3720
- target: fileNodeId,
3721
- type: import_types9.EdgeType.CONTAINS,
3722
- provenance: import_types9.Provenance.EXTRACTED,
3723
- confidence: (0, import_types9.confidenceForExtracted)("structural"),
3724
- evidence: { file: relPath }
3725
- };
3726
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3727
- edgesAdded++;
3728
- }
3729
- return { fileNodeId, nodesAdded, edgesAdded };
3730
- }
3731
-
3732
- // src/extract/calls/http.ts
3733
3752
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3734
3753
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3735
3754
  function isInsideJsxExternalLink(node) {
@@ -4013,6 +4032,7 @@ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-
4013
4032
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
4014
4033
  function isLikelyAddress(value) {
4015
4034
  if (!value) return false;
4035
+ if (/\s/.test(value) || value.startsWith("{")) return false;
4016
4036
  return /:\d{2,5}$/.test(value) || value.includes(".");
4017
4037
  }
4018
4038
  function normaliseForMatch(s) {
@@ -4324,17 +4344,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4324
4344
  graph.addNode(node.id, node);
4325
4345
  nodesAdded++;
4326
4346
  }
4327
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4347
+ const relDockerfile = toPosix2(import_node_path26.default.relative(service.dir, dockerfilePath));
4348
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4349
+ graph,
4350
+ service.pkg.name,
4351
+ service.node.id,
4352
+ relDockerfile
4353
+ );
4354
+ nodesAdded += fn;
4355
+ edgesAdded += fe;
4356
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types18.EdgeType.RUNS_ON);
4328
4357
  if (!graph.hasEdge(edgeId)) {
4329
4358
  const edge = {
4330
4359
  id: edgeId,
4331
- source: service.node.id,
4360
+ source: fileNodeId,
4332
4361
  target: node.id,
4333
4362
  type: import_types18.EdgeType.RUNS_ON,
4334
4363
  provenance: import_types18.Provenance.EXTRACTED,
4335
4364
  confidence: (0, import_types18.confidenceForExtracted)("structural"),
4336
4365
  evidence: {
4337
- file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4366
+ file: toPosix2(import_node_path26.default.relative(scanPath, dockerfilePath))
4338
4367
  }
4339
4368
  };
4340
4369
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4725,6 +4754,321 @@ var import_fastify = __toESM(require("fastify"), 1);
4725
4754
  var import_cors = __toESM(require("@fastify/cors"), 1);
4726
4755
  var import_types23 = require("@neat.is/types");
4727
4756
 
4757
+ // src/extend/index.ts
4758
+ init_cjs_shims();
4759
+ var import_node_fs20 = require("fs");
4760
+ var import_node_path34 = __toESM(require("path"), 1);
4761
+ var import_node_os2 = __toESM(require("os"), 1);
4762
+ var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
4763
+
4764
+ // src/installers/package-manager.ts
4765
+ init_cjs_shims();
4766
+ var import_node_fs19 = require("fs");
4767
+ var import_node_path33 = __toESM(require("path"), 1);
4768
+ var import_node_child_process = require("child_process");
4769
+ var LOCKFILE_PRIORITY = [
4770
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
4771
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
4772
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
4773
+ {
4774
+ lockfile: "package-lock.json",
4775
+ pm: "npm",
4776
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
4777
+ }
4778
+ ];
4779
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
4780
+ async function exists2(p) {
4781
+ try {
4782
+ await import_node_fs19.promises.access(p);
4783
+ return true;
4784
+ } catch {
4785
+ return false;
4786
+ }
4787
+ }
4788
+ async function detectPackageManager(serviceDir) {
4789
+ let dir = import_node_path33.default.resolve(serviceDir);
4790
+ const stops = /* @__PURE__ */ new Set();
4791
+ for (let i = 0; i < 64; i++) {
4792
+ if (stops.has(dir)) break;
4793
+ stops.add(dir);
4794
+ for (const candidate of LOCKFILE_PRIORITY) {
4795
+ const lockPath = import_node_path33.default.join(dir, candidate.lockfile);
4796
+ if (await exists2(lockPath)) {
4797
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
4798
+ }
4799
+ }
4800
+ const parent = import_node_path33.default.dirname(dir);
4801
+ if (parent === dir) break;
4802
+ dir = parent;
4803
+ }
4804
+ return { pm: "npm", cwd: import_node_path33.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
4805
+ }
4806
+ async function runPackageManagerInstall(cmd) {
4807
+ return new Promise((resolve) => {
4808
+ const child = (0, import_node_child_process.spawn)(cmd.pm, cmd.args, {
4809
+ cwd: cmd.cwd,
4810
+ // Inherit PATH + HOME so the user's installed managers resolve.
4811
+ env: process.env,
4812
+ // `false` keeps the parent in control of cleanup if the orchestrator
4813
+ // exits before install finishes. Cross-platform-safe.
4814
+ shell: false,
4815
+ stdio: ["ignore", "ignore", "pipe"]
4816
+ });
4817
+ let stderr = "";
4818
+ child.stderr?.on("data", (chunk) => {
4819
+ stderr += chunk.toString("utf8");
4820
+ });
4821
+ child.on("error", (err) => {
4822
+ resolve({
4823
+ pm: cmd.pm,
4824
+ cwd: cmd.cwd,
4825
+ args: cmd.args,
4826
+ exitCode: 127,
4827
+ stderr: stderr + `
4828
+ ${err.message}`
4829
+ });
4830
+ });
4831
+ child.on("close", (code) => {
4832
+ resolve({
4833
+ pm: cmd.pm,
4834
+ cwd: cmd.cwd,
4835
+ args: cmd.args,
4836
+ exitCode: code ?? 1,
4837
+ stderr: stderr.trim()
4838
+ });
4839
+ });
4840
+ });
4841
+ }
4842
+
4843
+ // src/extend/index.ts
4844
+ async function fileExists(p) {
4845
+ try {
4846
+ await import_node_fs20.promises.access(p);
4847
+ return true;
4848
+ } catch {
4849
+ return false;
4850
+ }
4851
+ }
4852
+ async function readPackageJson(scanPath) {
4853
+ const pkgPath = import_node_path34.default.join(scanPath, "package.json");
4854
+ const raw = await import_node_fs20.promises.readFile(pkgPath, "utf8");
4855
+ return JSON.parse(raw);
4856
+ }
4857
+ async function findHookFiles(scanPath) {
4858
+ const entries = await import_node_fs20.promises.readdir(scanPath);
4859
+ return entries.filter(
4860
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
4861
+ ).sort();
4862
+ }
4863
+ function extendLogPath() {
4864
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path34.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
4865
+ }
4866
+ async function appendExtendLog(entry2) {
4867
+ const logPath = extendLogPath();
4868
+ await import_node_fs20.promises.mkdir(import_node_path34.default.dirname(logPath), { recursive: true });
4869
+ await import_node_fs20.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
4870
+ }
4871
+ function splicedContent(fileContent, snippet2) {
4872
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
4873
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
4874
+ __INSTRUMENTATION_BLOCK__`);
4875
+ }
4876
+ const lines = fileContent.split("\n");
4877
+ let lastPushIdx = -1;
4878
+ for (let i = 0; i < lines.length; i++) {
4879
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
4880
+ }
4881
+ if (lastPushIdx >= 0) {
4882
+ lines.splice(lastPushIdx + 1, 0, snippet2);
4883
+ return lines.join("\n");
4884
+ }
4885
+ for (let i = 0; i < lines.length; i++) {
4886
+ if (lines[i].includes("new NodeSDK(")) {
4887
+ lines.splice(i, 0, snippet2);
4888
+ return lines.join("\n");
4889
+ }
4890
+ }
4891
+ return null;
4892
+ }
4893
+ async function listUninstrumented(ctx) {
4894
+ const pkg = await readPackageJson(ctx.scanPath);
4895
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4896
+ const results = [];
4897
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
4898
+ const entry2 = (0, import_instrumentation_registry.resolve)(library, installedVersion);
4899
+ if (!entry2) continue;
4900
+ if (entry2.coverage === "bundled" || entry2.coverage === "http-only") continue;
4901
+ results.push({
4902
+ library,
4903
+ coverage: entry2.coverage,
4904
+ installedVersion,
4905
+ instrumentation_package: entry2.instrumentation_package,
4906
+ package_version: entry2.package_version,
4907
+ registration: entry2.registration,
4908
+ notes: entry2.notes
4909
+ });
4910
+ }
4911
+ return results;
4912
+ }
4913
+ function lookupInstrumentation(library, installedVersion) {
4914
+ const entry2 = (0, import_instrumentation_registry.resolve)(library, installedVersion);
4915
+ if (!entry2) return null;
4916
+ return {
4917
+ library: entry2.library,
4918
+ coverage: entry2.coverage,
4919
+ instrumentation_package: entry2.instrumentation_package,
4920
+ package_version: entry2.package_version,
4921
+ registration: entry2.registration,
4922
+ notes: entry2.notes
4923
+ };
4924
+ }
4925
+ async function describeProjectInstrumentation(ctx) {
4926
+ const hookFiles = await findHookFiles(ctx.scanPath);
4927
+ const envNeat = await fileExists(import_node_path34.default.join(ctx.scanPath, ".env.neat"));
4928
+ const registryInstrPackages = new Set(
4929
+ (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
4930
+ );
4931
+ const pkg = await readPackageJson(ctx.scanPath);
4932
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
4933
+ const installedDeps = {};
4934
+ for (const [key, version] of Object.entries(allDeps)) {
4935
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
4936
+ installedDeps[key] = version;
4937
+ }
4938
+ }
4939
+ return { hookFiles, envNeat, installedDeps };
4940
+ }
4941
+ async function applyExtension(ctx, args, options) {
4942
+ const hookFiles = await findHookFiles(ctx.scanPath);
4943
+ if (hookFiles.length === 0) {
4944
+ throw new Error(
4945
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
4946
+ );
4947
+ }
4948
+ for (const file of hookFiles) {
4949
+ const content = await import_node_fs20.promises.readFile(import_node_path34.default.join(ctx.scanPath, file), "utf8");
4950
+ if (content.includes(args.registration_snippet)) {
4951
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
4952
+ }
4953
+ }
4954
+ const primaryFile = hookFiles[0];
4955
+ const primaryPath = import_node_path34.default.join(ctx.scanPath, primaryFile);
4956
+ const filesTouched = [];
4957
+ const depsAdded = [];
4958
+ const pkgPath = import_node_path34.default.join(ctx.scanPath, "package.json");
4959
+ const pkg = await readPackageJson(ctx.scanPath);
4960
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
4961
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
4962
+ await import_node_fs20.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
4963
+ filesTouched.push("package.json");
4964
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
4965
+ }
4966
+ const hookContent = await import_node_fs20.promises.readFile(primaryPath, "utf8");
4967
+ const patched = splicedContent(hookContent, args.registration_snippet);
4968
+ if (!patched) {
4969
+ throw new Error(
4970
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
4971
+ );
4972
+ }
4973
+ await import_node_fs20.promises.writeFile(primaryPath, patched, "utf8");
4974
+ filesTouched.push(primaryFile);
4975
+ const cmd = await detectPackageManager(ctx.scanPath);
4976
+ const installer = options?.runInstall ?? runPackageManagerInstall;
4977
+ const install = await installer(cmd);
4978
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
4979
+ await appendExtendLog({
4980
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4981
+ project: ctx.project,
4982
+ library: args.library,
4983
+ instrumentation_package: args.instrumentation_package,
4984
+ version: args.version,
4985
+ registration_snippet: args.registration_snippet,
4986
+ filesTouched,
4987
+ depsAdded,
4988
+ installOutput
4989
+ });
4990
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
4991
+ }
4992
+ async function dryRunExtension(ctx, args) {
4993
+ const hookFiles = await findHookFiles(ctx.scanPath);
4994
+ if (hookFiles.length === 0) {
4995
+ return {
4996
+ library: args.library,
4997
+ filesTouched: [],
4998
+ depsToAdd: [],
4999
+ packageJsonPatch: {},
5000
+ templatePatch: "No hook files found. Run 'neat init' first."
5001
+ };
5002
+ }
5003
+ for (const file of hookFiles) {
5004
+ const content = await import_node_fs20.promises.readFile(import_node_path34.default.join(ctx.scanPath, file), "utf8");
5005
+ if (content.includes(args.registration_snippet)) {
5006
+ return {
5007
+ library: args.library,
5008
+ filesTouched: [],
5009
+ depsToAdd: [],
5010
+ packageJsonPatch: {},
5011
+ templatePatch: "Already applied \u2014 no changes would be made."
5012
+ };
5013
+ }
5014
+ }
5015
+ const primaryFile = hookFiles[0];
5016
+ const filesTouched = [];
5017
+ const depsToAdd = [];
5018
+ let packageJsonPatch = {};
5019
+ let templatePatch = "";
5020
+ const pkg = await readPackageJson(ctx.scanPath);
5021
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5022
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
5023
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5024
+ filesTouched.push("package.json");
5025
+ }
5026
+ const hookContent = await import_node_fs20.promises.readFile(import_node_path34.default.join(ctx.scanPath, primaryFile), "utf8");
5027
+ const patched = splicedContent(hookContent, args.registration_snippet);
5028
+ if (patched) {
5029
+ filesTouched.push(primaryFile);
5030
+ templatePatch = `+ ${args.registration_snippet}`;
5031
+ } else {
5032
+ templatePatch = "Could not find insertion point in hook file.";
5033
+ }
5034
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
5035
+ }
5036
+ async function rollbackExtension(ctx, args) {
5037
+ const logPath = extendLogPath();
5038
+ if (!await fileExists(logPath)) {
5039
+ return { undone: false, message: "no apply found for library" };
5040
+ }
5041
+ const raw = await import_node_fs20.promises.readFile(logPath, "utf8");
5042
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5043
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
5044
+ if (!match) {
5045
+ return { undone: false, message: "no apply found for library" };
5046
+ }
5047
+ const pkgPath = import_node_path34.default.join(ctx.scanPath, "package.json");
5048
+ if (await fileExists(pkgPath)) {
5049
+ const pkg = await readPackageJson(ctx.scanPath);
5050
+ if (pkg.dependencies?.[match.instrumentation_package]) {
5051
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
5052
+ pkg.dependencies = rest;
5053
+ await import_node_fs20.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5054
+ }
5055
+ }
5056
+ const hookFiles = await findHookFiles(ctx.scanPath);
5057
+ for (const file of hookFiles) {
5058
+ const filePath = import_node_path34.default.join(ctx.scanPath, file);
5059
+ const content = await import_node_fs20.promises.readFile(filePath, "utf8");
5060
+ if (content.includes(match.registration_snippet)) {
5061
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
5062
+ await import_node_fs20.promises.writeFile(filePath, filtered, "utf8");
5063
+ break;
5064
+ }
5065
+ }
5066
+ return {
5067
+ undone: true,
5068
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
5069
+ };
5070
+ }
5071
+
4728
5072
  // src/divergences.ts
4729
5073
  init_cjs_shims();
4730
5074
  var import_types21 = require("@neat.is/types");
@@ -4965,7 +5309,7 @@ function computeDivergences(graph, opts = {}) {
4965
5309
 
4966
5310
  // src/diff.ts
4967
5311
  init_cjs_shims();
4968
- var import_node_fs19 = require("fs");
5312
+ var import_node_fs21 = require("fs");
4969
5313
  async function loadSnapshotForDiff(target) {
4970
5314
  if (/^https?:\/\//i.test(target)) {
4971
5315
  const res = await fetch(target);
@@ -4974,7 +5318,7 @@ async function loadSnapshotForDiff(target) {
4974
5318
  }
4975
5319
  return await res.json();
4976
5320
  }
4977
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
5321
+ const raw = await import_node_fs21.promises.readFile(target, "utf8");
4978
5322
  return JSON.parse(raw);
4979
5323
  }
4980
5324
  function indexEntries(entries) {
@@ -5042,25 +5386,25 @@ function canonicalJson(value) {
5042
5386
 
5043
5387
  // src/registry.ts
5044
5388
  init_cjs_shims();
5045
- var import_node_fs20 = require("fs");
5046
- var import_node_os2 = __toESM(require("os"), 1);
5047
- var import_node_path33 = __toESM(require("path"), 1);
5389
+ var import_node_fs22 = require("fs");
5390
+ var import_node_os3 = __toESM(require("os"), 1);
5391
+ var import_node_path35 = __toESM(require("path"), 1);
5048
5392
  var import_types22 = require("@neat.is/types");
5049
5393
  var LOCK_TIMEOUT_MS = 5e3;
5050
5394
  var LOCK_RETRY_MS = 50;
5051
5395
  function neatHome() {
5052
5396
  const override = process.env.NEAT_HOME;
5053
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
5054
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
5397
+ if (override && override.length > 0) return import_node_path35.default.resolve(override);
5398
+ return import_node_path35.default.join(import_node_os3.default.homedir(), ".neat");
5055
5399
  }
5056
5400
  function registryPath() {
5057
- return import_node_path33.default.join(neatHome(), "projects.json");
5401
+ return import_node_path35.default.join(neatHome(), "projects.json");
5058
5402
  }
5059
5403
  function registryLockPath() {
5060
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
5404
+ return import_node_path35.default.join(neatHome(), "projects.json.lock");
5061
5405
  }
5062
5406
  function daemonPidPath() {
5063
- return import_node_path33.default.join(neatHome(), "neatd.pid");
5407
+ return import_node_path35.default.join(neatHome(), "neatd.pid");
5064
5408
  }
5065
5409
  function isPidAliveDefault(pid) {
5066
5410
  try {
@@ -5072,7 +5416,7 @@ function isPidAliveDefault(pid) {
5072
5416
  }
5073
5417
  async function readPidFile(file) {
5074
5418
  try {
5075
- const raw = await import_node_fs20.promises.readFile(file, "utf8");
5419
+ const raw = await import_node_fs22.promises.readFile(file, "utf8");
5076
5420
  const pid = Number.parseInt(raw.trim(), 10);
5077
5421
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5078
5422
  } catch {
@@ -5120,24 +5464,24 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5120
5464
  }
5121
5465
  }
5122
5466
  async function writeAtomically(target, contents) {
5123
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
5467
+ await import_node_fs22.promises.mkdir(import_node_path35.default.dirname(target), { recursive: true });
5124
5468
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5125
- const fd = await import_node_fs20.promises.open(tmp, "w");
5469
+ const fd = await import_node_fs22.promises.open(tmp, "w");
5126
5470
  try {
5127
5471
  await fd.writeFile(contents, "utf8");
5128
5472
  await fd.sync();
5129
5473
  } finally {
5130
5474
  await fd.close();
5131
5475
  }
5132
- await import_node_fs20.promises.rename(tmp, target);
5476
+ await import_node_fs22.promises.rename(tmp, target);
5133
5477
  }
5134
5478
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5135
5479
  const deadline = Date.now() + timeoutMs;
5136
- await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
5480
+ await import_node_fs22.promises.mkdir(import_node_path35.default.dirname(lockPath), { recursive: true });
5137
5481
  let probedHolder = false;
5138
5482
  while (true) {
5139
5483
  try {
5140
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
5484
+ const fd = await import_node_fs22.promises.open(lockPath, "wx");
5141
5485
  try {
5142
5486
  await fd.writeFile(`${process.pid}
5143
5487
  `, "utf8");
@@ -5162,7 +5506,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
5162
5506
  }
5163
5507
  }
5164
5508
  async function releaseLock(lockPath) {
5165
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
5509
+ await import_node_fs22.promises.unlink(lockPath).catch(() => {
5166
5510
  });
5167
5511
  }
5168
5512
  async function withLock(fn) {
@@ -5178,7 +5522,7 @@ async function readRegistry() {
5178
5522
  const file = registryPath();
5179
5523
  let raw;
5180
5524
  try {
5181
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5525
+ raw = await import_node_fs22.promises.readFile(file, "utf8");
5182
5526
  } catch (err) {
5183
5527
  if (err.code === "ENOENT") {
5184
5528
  return { version: 1, projects: [] };
@@ -5678,6 +6022,105 @@ function registerRoutes(scope, ctx) {
5678
6022
  violations
5679
6023
  };
5680
6024
  });
6025
+ scope.get("/extend/list-uninstrumented", async (req2, reply) => {
6026
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6027
+ if (!proj) return;
6028
+ if (!proj.scanPath) {
6029
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6030
+ }
6031
+ try {
6032
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
6033
+ return { libraries: results };
6034
+ } catch (err) {
6035
+ return reply.code(500).send({ error: err.message });
6036
+ }
6037
+ });
6038
+ scope.get("/extend/lookup", async (req2, reply) => {
6039
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6040
+ if (!proj) return;
6041
+ const { library, version } = req2.query;
6042
+ if (!library) {
6043
+ return reply.code(400).send({ error: "query parameter `library` is required" });
6044
+ }
6045
+ const result = lookupInstrumentation(library, version);
6046
+ if (!result) {
6047
+ return reply.code(404).send({ error: "library not found in registry", library });
6048
+ }
6049
+ return result;
6050
+ });
6051
+ scope.get("/extend/describe", async (req2, reply) => {
6052
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6053
+ if (!proj) return;
6054
+ if (!proj.scanPath) {
6055
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6056
+ }
6057
+ try {
6058
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
6059
+ return state;
6060
+ } catch (err) {
6061
+ return reply.code(500).send({ error: err.message });
6062
+ }
6063
+ });
6064
+ scope.post("/extend/apply", async (req2, reply) => {
6065
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6066
+ if (!proj) return;
6067
+ if (!proj.scanPath) {
6068
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6069
+ }
6070
+ const { library, instrumentation_package, version, registration_snippet } = req2.body ?? {};
6071
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6072
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6073
+ }
6074
+ try {
6075
+ const result = await applyExtension(
6076
+ { project: proj.name, scanPath: proj.scanPath },
6077
+ { library, instrumentation_package, version, registration_snippet }
6078
+ );
6079
+ return result;
6080
+ } catch (err) {
6081
+ return reply.code(500).send({ error: err.message });
6082
+ }
6083
+ });
6084
+ scope.post("/extend/dry-run", async (req2, reply) => {
6085
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6086
+ if (!proj) return;
6087
+ if (!proj.scanPath) {
6088
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6089
+ }
6090
+ const { library, instrumentation_package, version, registration_snippet } = req2.body ?? {};
6091
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6092
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6093
+ }
6094
+ try {
6095
+ const result = await dryRunExtension(
6096
+ { project: proj.name, scanPath: proj.scanPath },
6097
+ { library, instrumentation_package, version, registration_snippet }
6098
+ );
6099
+ return result;
6100
+ } catch (err) {
6101
+ return reply.code(500).send({ error: err.message });
6102
+ }
6103
+ });
6104
+ scope.post("/extend/rollback", async (req2, reply) => {
6105
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6106
+ if (!proj) return;
6107
+ if (!proj.scanPath) {
6108
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6109
+ }
6110
+ const { library } = req2.body ?? {};
6111
+ if (!library) {
6112
+ return reply.code(400).send({ error: "body must include library" });
6113
+ }
6114
+ try {
6115
+ const result = await rollbackExtension(
6116
+ { project: proj.name, scanPath: proj.scanPath },
6117
+ { library }
6118
+ );
6119
+ return result;
6120
+ } catch (err) {
6121
+ return reply.code(500).send({ error: err.message });
6122
+ }
6123
+ });
5681
6124
  }
5682
6125
  async function buildApi(opts) {
5683
6126
  const app = (0, import_fastify.default)({ logger: false });
@@ -5788,8 +6231,8 @@ init_auth();
5788
6231
 
5789
6232
  // src/unrouted.ts
5790
6233
  init_cjs_shims();
5791
- var import_node_fs21 = require("fs");
5792
- var import_node_path36 = __toESM(require("path"), 1);
6234
+ var import_node_fs23 = require("fs");
6235
+ var import_node_path38 = __toESM(require("path"), 1);
5793
6236
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
5794
6237
  return {
5795
6238
  timestamp: now.toISOString(),
@@ -5799,21 +6242,21 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
5799
6242
  };
5800
6243
  }
5801
6244
  async function appendUnroutedSpan(neatHome3, record) {
5802
- const target = import_node_path36.default.join(neatHome3, "errors.ndjson");
5803
- await import_node_fs21.promises.mkdir(neatHome3, { recursive: true });
5804
- await import_node_fs21.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6245
+ const target = import_node_path38.default.join(neatHome3, "errors.ndjson");
6246
+ await import_node_fs23.promises.mkdir(neatHome3, { recursive: true });
6247
+ await import_node_fs23.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
5805
6248
  }
5806
6249
  function unroutedErrorsPath(neatHome3) {
5807
- return import_node_path36.default.join(neatHome3, "errors.ndjson");
6250
+ return import_node_path38.default.join(neatHome3, "errors.ndjson");
5808
6251
  }
5809
6252
 
5810
6253
  // src/daemon.ts
5811
6254
  function neatHomeFor(opts) {
5812
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path37.default.resolve(opts.neatHome);
6255
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path39.default.resolve(opts.neatHome);
5813
6256
  const env = process.env.NEAT_HOME;
5814
- if (env && env.length > 0) return import_node_path37.default.resolve(env);
6257
+ if (env && env.length > 0) return import_node_path39.default.resolve(env);
5815
6258
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5816
- return import_node_path37.default.join(home, ".neat");
6259
+ return import_node_path39.default.join(home, ".neat");
5817
6260
  }
5818
6261
  function routeSpanToProject(serviceName, projects) {
5819
6262
  if (!serviceName) return DEFAULT_PROJECT;
@@ -5848,9 +6291,9 @@ function isTokenContained(needle, haystack) {
5848
6291
  return tokens.includes(needle);
5849
6292
  }
5850
6293
  async function bootstrapProject(entry2) {
5851
- const paths = pathsForProject(entry2.name, import_node_path37.default.join(entry2.path, "neat-out"));
6294
+ const paths = pathsForProject(entry2.name, import_node_path39.default.join(entry2.path, "neat-out"));
5852
6295
  try {
5853
- const stat = await import_node_fs22.promises.stat(entry2.path);
6296
+ const stat = await import_node_fs24.promises.stat(entry2.path);
5854
6297
  if (!stat.isDirectory()) {
5855
6298
  throw new Error(`registered path ${entry2.path} is not a directory`);
5856
6299
  }
@@ -5916,13 +6359,13 @@ async function startDaemon(opts = {}) {
5916
6359
  const home = neatHomeFor(opts);
5917
6360
  const regPath = registryPath();
5918
6361
  try {
5919
- await import_node_fs22.promises.access(regPath);
6362
+ await import_node_fs24.promises.access(regPath);
5920
6363
  } catch {
5921
6364
  throw new Error(
5922
6365
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
5923
6366
  );
5924
6367
  }
5925
- const pidPath = import_node_path37.default.join(home, "neatd.pid");
6368
+ const pidPath = import_node_path39.default.join(home, "neatd.pid");
5926
6369
  await writeAtomically(pidPath, `${process.pid}
5927
6370
  `);
5928
6371
  const slots = /* @__PURE__ */ new Map();
@@ -6080,7 +6523,7 @@ async function startDaemon(opts = {}) {
6080
6523
  }
6081
6524
  if (restApp) await restApp.close().catch(() => {
6082
6525
  });
6083
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6526
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6084
6527
  });
6085
6528
  throw new Error(
6086
6529
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -6186,7 +6629,7 @@ async function startDaemon(opts = {}) {
6186
6629
  });
6187
6630
  if (otlpApp) await otlpApp.close().catch(() => {
6188
6631
  });
6189
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6632
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6190
6633
  });
6191
6634
  throw new Error(
6192
6635
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -6232,9 +6675,9 @@ async function startDaemon(opts = {}) {
6232
6675
  let registryWatcher = null;
6233
6676
  let reloadTimer = null;
6234
6677
  try {
6235
- const regDir = import_node_path37.default.dirname(regPath);
6236
- const regBase = import_node_path37.default.basename(regPath);
6237
- registryWatcher = (0, import_node_fs22.watch)(regDir, (_eventType, filename) => {
6678
+ const regDir = import_node_path39.default.dirname(regPath);
6679
+ const regBase = import_node_path39.default.basename(regPath);
6680
+ registryWatcher = (0, import_node_fs24.watch)(regDir, (_eventType, filename) => {
6238
6681
  if (filename !== null && filename !== regBase) return;
6239
6682
  if (reloadTimer) clearTimeout(reloadTimer);
6240
6683
  reloadTimer = setTimeout(() => {
@@ -6277,7 +6720,7 @@ async function startDaemon(opts = {}) {
6277
6720
  } catch {
6278
6721
  }
6279
6722
  }
6280
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6723
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6281
6724
  });
6282
6725
  };
6283
6726
  return {
@@ -6297,9 +6740,9 @@ init_auth();
6297
6740
 
6298
6741
  // src/web-spawn.ts
6299
6742
  init_cjs_shims();
6300
- var import_node_child_process = require("child_process");
6743
+ var import_node_child_process2 = require("child_process");
6301
6744
  var import_node_net = __toESM(require("net"), 1);
6302
- var import_node_path38 = __toESM(require("path"), 1);
6745
+ var import_node_path40 = __toESM(require("path"), 1);
6303
6746
  var DEFAULT_WEB_PORT = 6328;
6304
6747
  async function assertPortFree(port) {
6305
6748
  await new Promise((resolve, reject) => {
@@ -6327,10 +6770,10 @@ function resolveWebPackageDir() {
6327
6770
  eval("require")
6328
6771
  );
6329
6772
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
6330
- return import_node_path38.default.dirname(pkgJsonPath);
6773
+ return import_node_path40.default.dirname(pkgJsonPath);
6331
6774
  }
6332
6775
  function resolveStandaloneServerEntry(webDir) {
6333
- return import_node_path38.default.join(webDir, ".next/standalone/packages/web/server.js");
6776
+ return import_node_path40.default.join(webDir, ".next/standalone/packages/web/server.js");
6334
6777
  }
6335
6778
  async function spawnWebUI(restPort) {
6336
6779
  const portRaw = process.env.NEAT_WEB_PORT;
@@ -6354,8 +6797,8 @@ async function spawnWebUI(restPort) {
6354
6797
  HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0",
6355
6798
  NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
6356
6799
  };
6357
- const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
6358
- cwd: import_node_path38.default.dirname(serverEntry),
6800
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
6801
+ cwd: import_node_path40.default.dirname(serverEntry),
6359
6802
  env,
6360
6803
  stdio: ["ignore", "inherit", "inherit"],
6361
6804
  detached: false
@@ -6468,14 +6911,14 @@ function localVersion() {
6468
6911
  }
6469
6912
  function neatHome2() {
6470
6913
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
6471
- return import_node_path39.default.resolve(process.env.NEAT_HOME);
6914
+ return import_node_path41.default.resolve(process.env.NEAT_HOME);
6472
6915
  }
6473
6916
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6474
- return import_node_path39.default.join(home, ".neat");
6917
+ return import_node_path41.default.join(home, ".neat");
6475
6918
  }
6476
6919
  async function readPid() {
6477
6920
  try {
6478
- const raw = await import_node_fs23.promises.readFile(import_node_path39.default.join(neatHome2(), "neatd.pid"), "utf8");
6921
+ const raw = await import_node_fs25.promises.readFile(import_node_path41.default.join(neatHome2(), "neatd.pid"), "utf8");
6479
6922
  const n = Number.parseInt(raw.trim(), 10);
6480
6923
  return Number.isFinite(n) ? n : null;
6481
6924
  } catch {