@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/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);
@@ -1117,19 +1117,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1117
1117
  function longestIncomingWalk(graph, start, maxDepth) {
1118
1118
  let best = { path: [start], edges: [] };
1119
1119
  const visited = /* @__PURE__ */ new Set([start]);
1120
- function step(node, path38, edges) {
1121
- if (path38.length > best.path.length) {
1122
- best = { path: [...path38], edges: [...edges] };
1120
+ function step(node, path40, edges) {
1121
+ if (path40.length > best.path.length) {
1122
+ best = { path: [...path40], edges: [...edges] };
1123
1123
  }
1124
- if (path38.length - 1 >= maxDepth) return;
1124
+ if (path40.length - 1 >= maxDepth) return;
1125
1125
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1126
1126
  for (const [srcId, edge] of incoming) {
1127
1127
  if (visited.has(srcId)) continue;
1128
1128
  visited.add(srcId);
1129
- path38.push(srcId);
1129
+ path40.push(srcId);
1130
1130
  edges.push(edge);
1131
- step(srcId, path38, edges);
1132
- path38.pop();
1131
+ step(srcId, path40, edges);
1132
+ path40.pop();
1133
1133
  edges.pop();
1134
1134
  visited.delete(srcId);
1135
1135
  }
@@ -1961,7 +1961,7 @@ function ensureFrontierNode(graph, host, ts) {
1961
1961
  graph.addNode(id, node);
1962
1962
  return id;
1963
1963
  }
1964
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1964
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1965
1965
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1966
1966
  const id = makeObservedEdgeId(type, source, target);
1967
1967
  if (graph.hasEdge(id)) {
@@ -1998,7 +1998,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1998
1998
  confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1999
1999
  lastObserved: ts,
2000
2000
  callCount: 1,
2001
- 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 } : {}
2002
2005
  };
2003
2006
  graph.addEdgeWithKey(id, source, target, edge);
2004
2007
  return { edge, created: true };
@@ -2092,6 +2095,7 @@ async function handleSpan(ctx, span) {
2092
2095
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2093
2096
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2094
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;
2095
2099
  let affectedNode = sourceId;
2096
2100
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2097
2101
  if (span.dbSystem) {
@@ -2105,7 +2109,8 @@ async function handleSpan(ctx, span) {
2105
2109
  observedSource(),
2106
2110
  targetId,
2107
2111
  ts,
2108
- isError
2112
+ isError,
2113
+ callSiteEvidence
2109
2114
  );
2110
2115
  if (result) affectedNode = targetId;
2111
2116
  }
@@ -2121,7 +2126,8 @@ async function handleSpan(ctx, span) {
2121
2126
  observedSource(),
2122
2127
  targetId,
2123
2128
  ts,
2124
- isError
2129
+ isError,
2130
+ callSiteEvidence
2125
2131
  );
2126
2132
  affectedNode = targetId;
2127
2133
  resolvedViaAddress = true;
@@ -2133,7 +2139,8 @@ async function handleSpan(ctx, span) {
2133
2139
  observedSource(),
2134
2140
  frontierNodeId,
2135
2141
  ts,
2136
- isError
2142
+ isError,
2143
+ callSiteEvidence
2137
2144
  );
2138
2145
  affectedNode = frontierNodeId;
2139
2146
  resolvedViaAddress = true;
@@ -3084,14 +3091,111 @@ async function addServiceAliases(graph, scanPath, services) {
3084
3091
 
3085
3092
  // src/extract/databases/index.ts
3086
3093
  init_cjs_shims();
3087
- 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);
3088
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
+ }
3089
3193
 
3090
3194
  // src/extract/databases/db-config-yaml.ts
3091
3195
  init_cjs_shims();
3092
- var import_node_path10 = __toESM(require("path"), 1);
3196
+ var import_node_path11 = __toESM(require("path"), 1);
3093
3197
  async function parse(serviceDir) {
3094
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3198
+ const yamlPath = import_node_path11.default.join(serviceDir, "db-config.yaml");
3095
3199
  if (!await exists(yamlPath)) return [];
3096
3200
  const raw = await readYaml(yamlPath);
3097
3201
  return [
@@ -3109,13 +3213,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
3109
3213
 
3110
3214
  // src/extract/databases/dotenv.ts
3111
3215
  init_cjs_shims();
3112
- var import_node_fs11 = require("fs");
3113
- var import_node_path12 = __toESM(require("path"), 1);
3216
+ var import_node_fs12 = require("fs");
3217
+ var import_node_path13 = __toESM(require("path"), 1);
3114
3218
 
3115
3219
  // src/extract/databases/shared.ts
3116
3220
  init_cjs_shims();
3117
- var import_node_fs10 = require("fs");
3118
- var import_node_path11 = __toESM(require("path"), 1);
3221
+ var import_node_fs11 = require("fs");
3222
+ var import_node_path12 = __toESM(require("path"), 1);
3119
3223
  function schemeToEngine(scheme) {
3120
3224
  const s = scheme.toLowerCase().split("+")[0];
3121
3225
  switch (s) {
@@ -3154,14 +3258,14 @@ function parseConnectionString(url) {
3154
3258
  }
3155
3259
  async function readIfExists(filePath) {
3156
3260
  try {
3157
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3261
+ return await import_node_fs11.promises.readFile(filePath, "utf8");
3158
3262
  } catch {
3159
3263
  return null;
3160
3264
  }
3161
3265
  }
3162
3266
  async function findFirst(serviceDir, candidates) {
3163
3267
  for (const rel of candidates) {
3164
- const abs = import_node_path11.default.join(serviceDir, rel);
3268
+ const abs = import_node_path12.default.join(serviceDir, rel);
3165
3269
  const content = await readIfExists(abs);
3166
3270
  if (content !== null) return abs;
3167
3271
  }
@@ -3212,15 +3316,15 @@ function parseDotenvLine(line) {
3212
3316
  return { key, value };
3213
3317
  }
3214
3318
  async function parse2(serviceDir) {
3215
- 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(() => []);
3216
3320
  const configs = [];
3217
3321
  const seen = /* @__PURE__ */ new Set();
3218
3322
  for (const entry of entries) {
3219
3323
  if (!entry.isFile()) continue;
3220
3324
  const match = isConfigFile(entry.name);
3221
3325
  if (!match.match || match.fileType !== "env") continue;
3222
- const filePath = import_node_path12.default.join(serviceDir, entry.name);
3223
- 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");
3224
3328
  for (const line of content.split("\n")) {
3225
3329
  const parsed = parseDotenvLine(line);
3226
3330
  if (!parsed) continue;
@@ -3239,9 +3343,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3239
3343
 
3240
3344
  // src/extract/databases/prisma.ts
3241
3345
  init_cjs_shims();
3242
- var import_node_path13 = __toESM(require("path"), 1);
3346
+ var import_node_path14 = __toESM(require("path"), 1);
3243
3347
  async function parse3(serviceDir) {
3244
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
3348
+ const schemaPath = import_node_path14.default.join(serviceDir, "prisma", "schema.prisma");
3245
3349
  const content = await readIfExists(schemaPath);
3246
3350
  if (!content) return [];
3247
3351
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3373,10 +3477,10 @@ var knexParser = { name: "knex", parse: parse5 };
3373
3477
 
3374
3478
  // src/extract/databases/ormconfig.ts
3375
3479
  init_cjs_shims();
3376
- var import_node_path14 = __toESM(require("path"), 1);
3480
+ var import_node_path15 = __toESM(require("path"), 1);
3377
3481
  async function parse6(serviceDir) {
3378
3482
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
3379
- const abs = import_node_path14.default.join(serviceDir, candidate);
3483
+ const abs = import_node_path15.default.join(serviceDir, candidate);
3380
3484
  if (!await exists(abs)) continue;
3381
3485
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3382
3486
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -3436,9 +3540,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3436
3540
 
3437
3541
  // src/extract/databases/sequelize.ts
3438
3542
  init_cjs_shims();
3439
- var import_node_path15 = __toESM(require("path"), 1);
3543
+ var import_node_path16 = __toESM(require("path"), 1);
3440
3544
  async function parse8(serviceDir) {
3441
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3545
+ const configPath = import_node_path16.default.join(serviceDir, "config", "config.json");
3442
3546
  if (!await exists(configPath)) return [];
3443
3547
  const raw = await readJson(configPath);
3444
3548
  const out = [];
@@ -3465,7 +3569,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3465
3569
 
3466
3570
  // src/extract/databases/docker-compose.ts
3467
3571
  init_cjs_shims();
3468
- var import_node_path16 = __toESM(require("path"), 1);
3572
+ var import_node_path17 = __toESM(require("path"), 1);
3469
3573
  function portFromService(svc) {
3470
3574
  for (const raw of svc.ports ?? []) {
3471
3575
  const str = String(raw);
@@ -3492,7 +3596,7 @@ function databaseFromEnv(svc) {
3492
3596
  }
3493
3597
  async function parse9(serviceDir) {
3494
3598
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3495
- const abs = import_node_path16.default.join(serviceDir, name);
3599
+ const abs = import_node_path17.default.join(serviceDir, name);
3496
3600
  if (!await exists(abs)) continue;
3497
3601
  const raw = await readYaml(abs);
3498
3602
  if (!raw?.services) return [];
@@ -3533,8 +3637,8 @@ function compatibleDriversFor(engine) {
3533
3637
  }
3534
3638
  function toDatabaseNode(config) {
3535
3639
  return {
3536
- id: (0, import_types7.databaseId)(config.host),
3537
- type: import_types7.NodeType.DatabaseNode,
3640
+ id: (0, import_types8.databaseId)(config.host),
3641
+ type: import_types8.NodeType.DatabaseNode,
3538
3642
  name: config.database || config.host,
3539
3643
  engine: config.engine,
3540
3644
  engineVersion: config.engineVersion,
@@ -3664,19 +3768,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3664
3768
  discoveredVia: mergedDiscoveredVia
3665
3769
  });
3666
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));
3667
3781
  const edge = {
3668
- id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
3669
- source: service.node.id,
3782
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, dbNode.id, import_types8.EdgeType.CONNECTS_TO),
3783
+ source: fileNodeId,
3670
3784
  target: dbNode.id,
3671
- type: import_types7.EdgeType.CONNECTS_TO,
3672
- provenance: import_types7.Provenance.EXTRACTED,
3673
- confidence: (0, import_types7.confidenceForExtracted)("structural"),
3674
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3675
- // Ghost-edge cleanup keys retirement on this; the conditional
3676
- // sourceFile spread that used to live here was a v0.1.x leftover.
3677
- evidence: {
3678
- file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3679
- }
3785
+ type: import_types8.EdgeType.CONNECTS_TO,
3786
+ provenance: import_types8.Provenance.EXTRACTED,
3787
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3788
+ evidence: { file: evidenceFile }
3680
3789
  };
3681
3790
  if (!graph.hasEdge(edge.id)) {
3682
3791
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3702,15 +3811,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3702
3811
 
3703
3812
  // src/extract/configs.ts
3704
3813
  init_cjs_shims();
3705
- var import_node_fs12 = require("fs");
3706
- var import_node_path18 = __toESM(require("path"), 1);
3707
- 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");
3708
3817
  async function walkConfigFiles(dir) {
3709
3818
  const out = [];
3710
3819
  async function walk(current) {
3711
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3820
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true });
3712
3821
  for (const entry of entries) {
3713
- const full = import_node_path18.default.join(current, entry.name);
3822
+ const full = import_node_path19.default.join(current, entry.name);
3714
3823
  if (entry.isDirectory()) {
3715
3824
  if (IGNORED_DIRS.has(entry.name)) continue;
3716
3825
  if (await isPythonVenvDir(full)) continue;
@@ -3729,26 +3838,35 @@ async function addConfigNodes(graph, services, scanPath) {
3729
3838
  for (const service of services) {
3730
3839
  const configFiles = await walkConfigFiles(service.dir);
3731
3840
  for (const file of configFiles) {
3732
- const relPath = import_node_path18.default.relative(scanPath, file);
3841
+ const relPath = import_node_path19.default.relative(scanPath, file);
3733
3842
  const node = {
3734
- id: (0, import_types8.configId)(relPath),
3735
- type: import_types8.NodeType.ConfigNode,
3736
- 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),
3737
3846
  path: relPath,
3738
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3847
+ fileType: isConfigFile(import_node_path19.default.basename(file)).fileType
3739
3848
  };
3740
3849
  if (!graph.hasNode(node.id)) {
3741
3850
  graph.addNode(node.id, node);
3742
3851
  nodesAdded++;
3743
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;
3744
3862
  const edge = {
3745
- id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
3746
- source: service.node.id,
3863
+ id: (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types9.EdgeType.CONFIGURED_BY),
3864
+ source: fileNodeId,
3747
3865
  target: node.id,
3748
- type: import_types8.EdgeType.CONFIGURED_BY,
3749
- provenance: import_types8.Provenance.EXTRACTED,
3750
- confidence: (0, import_types8.confidenceForExtracted)("structural"),
3751
- 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("/") }
3752
3870
  };
3753
3871
  if (!graph.hasEdge(edge.id)) {
3754
3872
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3770,105 +3888,6 @@ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3770
3888
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3771
3889
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3772
3890
  var import_types10 = require("@neat.is/types");
3773
-
3774
- // src/extract/calls/shared.ts
3775
- init_cjs_shims();
3776
- var import_node_fs13 = require("fs");
3777
- var import_node_path19 = __toESM(require("path"), 1);
3778
- var import_types9 = require("@neat.is/types");
3779
- async function walkSourceFiles(dir) {
3780
- const out = [];
3781
- async function walk(current) {
3782
- const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3783
- for (const entry of entries) {
3784
- const full = import_node_path19.default.join(current, entry.name);
3785
- if (entry.isDirectory()) {
3786
- if (IGNORED_DIRS.has(entry.name)) continue;
3787
- if (await isPythonVenvDir(full)) continue;
3788
- await walk(full);
3789
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3790
- out.push(full);
3791
- }
3792
- }
3793
- }
3794
- await walk(dir);
3795
- return out;
3796
- }
3797
- async function loadSourceFiles(dir) {
3798
- const paths = await walkSourceFiles(dir);
3799
- const out = [];
3800
- for (const p of paths) {
3801
- try {
3802
- const content = await import_node_fs13.promises.readFile(p, "utf8");
3803
- out.push({ path: p, content });
3804
- } catch {
3805
- }
3806
- }
3807
- return out;
3808
- }
3809
- function lineOf(text, needle) {
3810
- const idx = text.indexOf(needle);
3811
- if (idx < 0) return 1;
3812
- return text.slice(0, idx).split("\n").length;
3813
- }
3814
- function snippet(text, line) {
3815
- const lines = text.split("\n");
3816
- return (lines[line - 1] ?? "").trim();
3817
- }
3818
- function toPosix2(p) {
3819
- return p.split("\\").join("/");
3820
- }
3821
- function languageForPath(relPath) {
3822
- switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3823
- case ".py":
3824
- return "python";
3825
- case ".ts":
3826
- case ".tsx":
3827
- return "typescript";
3828
- case ".js":
3829
- case ".jsx":
3830
- case ".mjs":
3831
- case ".cjs":
3832
- return "javascript";
3833
- default:
3834
- return void 0;
3835
- }
3836
- }
3837
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3838
- let nodesAdded = 0;
3839
- let edgesAdded = 0;
3840
- const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3841
- if (!graph.hasNode(fileNodeId)) {
3842
- const language = languageForPath(relPath);
3843
- const node = {
3844
- id: fileNodeId,
3845
- type: import_types9.NodeType.FileNode,
3846
- service: serviceName,
3847
- path: relPath,
3848
- ...language ? { language } : {},
3849
- discoveredVia: "static"
3850
- };
3851
- graph.addNode(fileNodeId, node);
3852
- nodesAdded++;
3853
- }
3854
- const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3855
- if (!graph.hasEdge(containsId)) {
3856
- const edge = {
3857
- id: containsId,
3858
- source: serviceNodeId,
3859
- target: fileNodeId,
3860
- type: import_types9.EdgeType.CONTAINS,
3861
- provenance: import_types9.Provenance.EXTRACTED,
3862
- confidence: (0, import_types9.confidenceForExtracted)("structural"),
3863
- evidence: { file: relPath }
3864
- };
3865
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3866
- edgesAdded++;
3867
- }
3868
- return { fileNodeId, nodesAdded, edgesAdded };
3869
- }
3870
-
3871
- // src/extract/calls/http.ts
3872
3891
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3873
3892
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3874
3893
  function isInsideJsxExternalLink(node) {
@@ -4152,6 +4171,7 @@ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-
4152
4171
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
4153
4172
  function isLikelyAddress(value) {
4154
4173
  if (!value) return false;
4174
+ if (/\s/.test(value) || value.startsWith("{")) return false;
4155
4175
  return /:\d{2,5}$/.test(value) || value.includes(".");
4156
4176
  }
4157
4177
  function normaliseForMatch(s) {
@@ -4463,17 +4483,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4463
4483
  graph.addNode(node.id, node);
4464
4484
  nodesAdded++;
4465
4485
  }
4466
- 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);
4467
4496
  if (!graph.hasEdge(edgeId)) {
4468
4497
  const edge = {
4469
4498
  id: edgeId,
4470
- source: service.node.id,
4499
+ source: fileNodeId,
4471
4500
  target: node.id,
4472
4501
  type: import_types18.EdgeType.RUNS_ON,
4473
4502
  provenance: import_types18.Provenance.EXTRACTED,
4474
4503
  confidence: (0, import_types18.confidenceForExtracted)("structural"),
4475
4504
  evidence: {
4476
- 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))
4477
4506
  }
4478
4507
  };
4479
4508
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4812,6 +4841,321 @@ var import_fastify = __toESM(require("fastify"), 1);
4812
4841
  var import_cors = __toESM(require("@fastify/cors"), 1);
4813
4842
  var import_types23 = require("@neat.is/types");
4814
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
+
4815
5159
  // src/divergences.ts
4816
5160
  init_cjs_shims();
4817
5161
  var import_types21 = require("@neat.is/types");
@@ -5052,7 +5396,7 @@ function computeDivergences(graph, opts = {}) {
5052
5396
 
5053
5397
  // src/diff.ts
5054
5398
  init_cjs_shims();
5055
- var import_node_fs19 = require("fs");
5399
+ var import_node_fs21 = require("fs");
5056
5400
  async function loadSnapshotForDiff(target) {
5057
5401
  if (/^https?:\/\//i.test(target)) {
5058
5402
  const res = await fetch(target);
@@ -5061,7 +5405,7 @@ async function loadSnapshotForDiff(target) {
5061
5405
  }
5062
5406
  return await res.json();
5063
5407
  }
5064
- const raw = await import_node_fs19.promises.readFile(target, "utf8");
5408
+ const raw = await import_node_fs21.promises.readFile(target, "utf8");
5065
5409
  return JSON.parse(raw);
5066
5410
  }
5067
5411
  function indexEntries(entries) {
@@ -5129,23 +5473,23 @@ function canonicalJson(value) {
5129
5473
 
5130
5474
  // src/projects.ts
5131
5475
  init_cjs_shims();
5132
- var import_node_path32 = __toESM(require("path"), 1);
5476
+ var import_node_path34 = __toESM(require("path"), 1);
5133
5477
  function pathsForProject(project, baseDir) {
5134
5478
  if (project === DEFAULT_PROJECT) {
5135
5479
  return {
5136
- snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
5137
- errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
5138
- staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
5139
- embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
5140
- 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")
5141
5485
  };
5142
5486
  }
5143
5487
  return {
5144
- snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
5145
- errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
5146
- staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
5147
- embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
5148
- 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`)
5149
5493
  };
5150
5494
  }
5151
5495
  var Projects = class {
@@ -5181,25 +5525,25 @@ var Projects = class {
5181
5525
 
5182
5526
  // src/registry.ts
5183
5527
  init_cjs_shims();
5184
- var import_node_fs20 = require("fs");
5185
- var import_node_os2 = __toESM(require("os"), 1);
5186
- 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);
5187
5531
  var import_types22 = require("@neat.is/types");
5188
5532
  var LOCK_TIMEOUT_MS = 5e3;
5189
5533
  var LOCK_RETRY_MS = 50;
5190
5534
  function neatHome() {
5191
5535
  const override = process.env.NEAT_HOME;
5192
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
5193
- 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");
5194
5538
  }
5195
5539
  function registryPath() {
5196
- return import_node_path33.default.join(neatHome(), "projects.json");
5540
+ return import_node_path35.default.join(neatHome(), "projects.json");
5197
5541
  }
5198
5542
  function registryLockPath() {
5199
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
5543
+ return import_node_path35.default.join(neatHome(), "projects.json.lock");
5200
5544
  }
5201
5545
  function daemonPidPath() {
5202
- return import_node_path33.default.join(neatHome(), "neatd.pid");
5546
+ return import_node_path35.default.join(neatHome(), "neatd.pid");
5203
5547
  }
5204
5548
  function isPidAliveDefault(pid) {
5205
5549
  try {
@@ -5211,7 +5555,7 @@ function isPidAliveDefault(pid) {
5211
5555
  }
5212
5556
  async function readPidFile(file) {
5213
5557
  try {
5214
- const raw = await import_node_fs20.promises.readFile(file, "utf8");
5558
+ const raw = await import_node_fs22.promises.readFile(file, "utf8");
5215
5559
  const pid = Number.parseInt(raw.trim(), 10);
5216
5560
  return Number.isInteger(pid) && pid > 0 ? pid : void 0;
5217
5561
  } catch {
@@ -5259,32 +5603,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5259
5603
  }
5260
5604
  }
5261
5605
  async function normalizeProjectPath(input) {
5262
- const resolved = import_node_path33.default.resolve(input);
5606
+ const resolved = import_node_path35.default.resolve(input);
5263
5607
  try {
5264
- return await import_node_fs20.promises.realpath(resolved);
5608
+ return await import_node_fs22.promises.realpath(resolved);
5265
5609
  } catch {
5266
5610
  return resolved;
5267
5611
  }
5268
5612
  }
5269
5613
  async function writeAtomically(target, contents) {
5270
- 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 });
5271
5615
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5272
- const fd = await import_node_fs20.promises.open(tmp, "w");
5616
+ const fd = await import_node_fs22.promises.open(tmp, "w");
5273
5617
  try {
5274
5618
  await fd.writeFile(contents, "utf8");
5275
5619
  await fd.sync();
5276
5620
  } finally {
5277
5621
  await fd.close();
5278
5622
  }
5279
- await import_node_fs20.promises.rename(tmp, target);
5623
+ await import_node_fs22.promises.rename(tmp, target);
5280
5624
  }
5281
5625
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5282
5626
  const deadline = Date.now() + timeoutMs;
5283
- 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 });
5284
5628
  let probedHolder = false;
5285
5629
  while (true) {
5286
5630
  try {
5287
- const fd = await import_node_fs20.promises.open(lockPath, "wx");
5631
+ const fd = await import_node_fs22.promises.open(lockPath, "wx");
5288
5632
  try {
5289
5633
  await fd.writeFile(`${process.pid}
5290
5634
  `, "utf8");
@@ -5309,7 +5653,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
5309
5653
  }
5310
5654
  }
5311
5655
  async function releaseLock(lockPath) {
5312
- await import_node_fs20.promises.unlink(lockPath).catch(() => {
5656
+ await import_node_fs22.promises.unlink(lockPath).catch(() => {
5313
5657
  });
5314
5658
  }
5315
5659
  async function withLock(fn) {
@@ -5325,7 +5669,7 @@ async function readRegistry() {
5325
5669
  const file = registryPath();
5326
5670
  let raw;
5327
5671
  try {
5328
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5672
+ raw = await import_node_fs22.promises.readFile(file, "utf8");
5329
5673
  } catch (err) {
5330
5674
  if (err.code === "ENOENT") {
5331
5675
  return { version: 1, projects: [] };
@@ -5875,6 +6219,105 @@ function registerRoutes(scope, ctx) {
5875
6219
  violations
5876
6220
  };
5877
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
+ });
5878
6321
  }
5879
6322
  async function buildApi(opts) {
5880
6323
  const app = (0, import_fastify.default)({ logger: false });
@@ -5985,15 +6428,15 @@ init_otel_grpc();
5985
6428
 
5986
6429
  // src/daemon.ts
5987
6430
  init_cjs_shims();
5988
- var import_node_fs22 = require("fs");
5989
- var import_node_path37 = __toESM(require("path"), 1);
6431
+ var import_node_fs24 = require("fs");
6432
+ var import_node_path39 = __toESM(require("path"), 1);
5990
6433
  init_otel();
5991
6434
  init_auth();
5992
6435
 
5993
6436
  // src/unrouted.ts
5994
6437
  init_cjs_shims();
5995
- var import_node_fs21 = require("fs");
5996
- var import_node_path36 = __toESM(require("path"), 1);
6438
+ var import_node_fs23 = require("fs");
6439
+ var import_node_path38 = __toESM(require("path"), 1);
5997
6440
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
5998
6441
  return {
5999
6442
  timestamp: now.toISOString(),
@@ -6003,21 +6446,21 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
6003
6446
  };
6004
6447
  }
6005
6448
  async function appendUnroutedSpan(neatHome2, record) {
6006
- const target = import_node_path36.default.join(neatHome2, "errors.ndjson");
6007
- await import_node_fs21.promises.mkdir(neatHome2, { recursive: true });
6008
- 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");
6009
6452
  }
6010
6453
  function unroutedErrorsPath(neatHome2) {
6011
- return import_node_path36.default.join(neatHome2, "errors.ndjson");
6454
+ return import_node_path38.default.join(neatHome2, "errors.ndjson");
6012
6455
  }
6013
6456
 
6014
6457
  // src/daemon.ts
6015
6458
  function neatHomeFor(opts) {
6016
- 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);
6017
6460
  const env = process.env.NEAT_HOME;
6018
- 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);
6019
6462
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6020
- return import_node_path37.default.join(home, ".neat");
6463
+ return import_node_path39.default.join(home, ".neat");
6021
6464
  }
6022
6465
  function routeSpanToProject(serviceName, projects) {
6023
6466
  if (!serviceName) return DEFAULT_PROJECT;
@@ -6052,9 +6495,9 @@ function isTokenContained(needle, haystack) {
6052
6495
  return tokens.includes(needle);
6053
6496
  }
6054
6497
  async function bootstrapProject(entry) {
6055
- 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"));
6056
6499
  try {
6057
- const stat = await import_node_fs22.promises.stat(entry.path);
6500
+ const stat = await import_node_fs24.promises.stat(entry.path);
6058
6501
  if (!stat.isDirectory()) {
6059
6502
  throw new Error(`registered path ${entry.path} is not a directory`);
6060
6503
  }
@@ -6120,13 +6563,13 @@ async function startDaemon(opts = {}) {
6120
6563
  const home = neatHomeFor(opts);
6121
6564
  const regPath = registryPath();
6122
6565
  try {
6123
- await import_node_fs22.promises.access(regPath);
6566
+ await import_node_fs24.promises.access(regPath);
6124
6567
  } catch {
6125
6568
  throw new Error(
6126
6569
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
6127
6570
  );
6128
6571
  }
6129
- const pidPath = import_node_path37.default.join(home, "neatd.pid");
6572
+ const pidPath = import_node_path39.default.join(home, "neatd.pid");
6130
6573
  await writeAtomically(pidPath, `${process.pid}
6131
6574
  `);
6132
6575
  const slots = /* @__PURE__ */ new Map();
@@ -6284,7 +6727,7 @@ async function startDaemon(opts = {}) {
6284
6727
  }
6285
6728
  if (restApp) await restApp.close().catch(() => {
6286
6729
  });
6287
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6730
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6288
6731
  });
6289
6732
  throw new Error(
6290
6733
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -6390,7 +6833,7 @@ async function startDaemon(opts = {}) {
6390
6833
  });
6391
6834
  if (otlpApp) await otlpApp.close().catch(() => {
6392
6835
  });
6393
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6836
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6394
6837
  });
6395
6838
  throw new Error(
6396
6839
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -6436,9 +6879,9 @@ async function startDaemon(opts = {}) {
6436
6879
  let registryWatcher = null;
6437
6880
  let reloadTimer = null;
6438
6881
  try {
6439
- const regDir = import_node_path37.default.dirname(regPath);
6440
- const regBase = import_node_path37.default.basename(regPath);
6441
- 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) => {
6442
6885
  if (filename !== null && filename !== regBase) return;
6443
6886
  if (reloadTimer) clearTimeout(reloadTimer);
6444
6887
  reloadTimer = setTimeout(() => {
@@ -6481,7 +6924,7 @@ async function startDaemon(opts = {}) {
6481
6924
  } catch {
6482
6925
  }
6483
6926
  }
6484
- await import_node_fs22.promises.unlink(pidPath).catch(() => {
6927
+ await import_node_fs24.promises.unlink(pidPath).catch(() => {
6485
6928
  });
6486
6929
  };
6487
6930
  return {