@neat.is/core 0.7.9 → 0.8.0-rc.1

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/server.cjs CHANGED
@@ -755,7 +755,7 @@ function getGraph(project = DEFAULT_PROJECT) {
755
755
  init_cjs_shims();
756
756
  var import_fastify2 = __toESM(require("fastify"), 1);
757
757
  var import_cors = __toESM(require("@fastify/cors"), 1);
758
- var import_types80 = require("@neat.is/types");
758
+ var import_types85 = require("@neat.is/types");
759
759
 
760
760
  // src/extend/index.ts
761
761
  init_cjs_shims();
@@ -2017,6 +2017,7 @@ var import_yaml = require("yaml");
2017
2017
  var import_types3 = require("@neat.is/types");
2018
2018
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
2019
2019
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2020
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
2020
2021
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2021
2022
  "node_modules",
2022
2023
  ".git",
@@ -2056,6 +2057,7 @@ async function isPythonVenvDir(dir) {
2056
2057
  function isConfigFile(name) {
2057
2058
  const ext = import_node_path5.default.extname(name);
2058
2059
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2060
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
2059
2061
  if (name === ".env" || name.startsWith(".env.")) {
2060
2062
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2061
2063
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -4321,6 +4323,63 @@ async function addRoutes(graph, services) {
4321
4323
  return { nodesAdded, edgesAdded };
4322
4324
  }
4323
4325
 
4326
+ // src/latency-digest.ts
4327
+ init_cjs_shims();
4328
+ var SUB = 16;
4329
+ var MIN_EXP = -10;
4330
+ var MAX_EXP = 22;
4331
+ var OCTAVES = MAX_EXP - MIN_EXP + 1;
4332
+ var OVERFLOW_INDEX = OCTAVES * SUB + 1;
4333
+ function latencyBucketIndex(ms) {
4334
+ if (!Number.isFinite(ms) || ms <= 0) return 0;
4335
+ const e = Math.floor(Math.log2(ms));
4336
+ if (e < MIN_EXP) return 0;
4337
+ if (e > MAX_EXP) return OVERFLOW_INDEX;
4338
+ const base = 2 ** e;
4339
+ const raw = Math.floor((ms / base - 1) * SUB);
4340
+ const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
4341
+ return (e - MIN_EXP) * SUB + s + 1;
4342
+ }
4343
+ function bucketRepresentativeMs(index) {
4344
+ if (index <= 0) return 0;
4345
+ if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
4346
+ const zeroBased = index - 1;
4347
+ const e = MIN_EXP + Math.floor(zeroBased / SUB);
4348
+ const s = zeroBased % SUB;
4349
+ const base = 2 ** e;
4350
+ const lower = base * (1 + s / SUB);
4351
+ const upper = base * (1 + (s + 1) / SUB);
4352
+ return (lower + upper) / 2;
4353
+ }
4354
+ function recordLatency(hist, ms) {
4355
+ const key = String(latencyBucketIndex(ms));
4356
+ hist[key] = (hist[key] ?? 0) + 1;
4357
+ return hist;
4358
+ }
4359
+ function quantile(hist, q) {
4360
+ const entries = Object.entries(hist).map(([k, c]) => [Number(k), c]).filter(([idx, c]) => Number.isFinite(idx) && c > 0).sort((a, b) => a[0] - b[0]);
4361
+ let total = 0;
4362
+ for (const [, c] of entries) total += c;
4363
+ if (total === 0) return 0;
4364
+ const rank = Math.max(1, Math.ceil(q * total));
4365
+ let cumulative = 0;
4366
+ for (const [idx, c] of entries) {
4367
+ cumulative += c;
4368
+ if (cumulative >= rank) return bucketRepresentativeMs(idx);
4369
+ }
4370
+ return bucketRepresentativeMs(entries[entries.length - 1][0]);
4371
+ }
4372
+ function round(ms) {
4373
+ return Math.round(ms * 100) / 100;
4374
+ }
4375
+ function latencyPercentiles(hist) {
4376
+ if (!hist) return void 0;
4377
+ let total = 0;
4378
+ for (const c of Object.values(hist)) total += c;
4379
+ if (total === 0) return void 0;
4380
+ return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
4381
+ }
4382
+
4324
4383
  // src/ingest.ts
4325
4384
  var HOUR_MS = 60 * 60 * 1e3;
4326
4385
  var DAY_MS = 24 * HOUR_MS;
@@ -5016,7 +5075,7 @@ function ensureFrontierNode(graph, host, ts) {
5016
5075
  graph.addNode(id, node);
5017
5076
  return id;
5018
5077
  }
5019
- function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
5078
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
5020
5079
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
5021
5080
  const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
5022
5081
  const id = makeObservedEdgeId(type, source, target);
@@ -5024,10 +5083,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
5024
5083
  const existing = graph.getEdgeAttributes(id);
5025
5084
  const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
5026
5085
  const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
5086
+ const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
5087
+ const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
5027
5088
  const newSignal = {
5028
5089
  spanCount: newSpanCount,
5029
5090
  errorCount: newErrorCount,
5030
- lastObservedAgeMs: 0
5091
+ lastObservedAgeMs: 0,
5092
+ ...latencyHist2 ? { latencyHist: latencyHist2 } : {},
5093
+ ...latencyMs2 ? { latencyMs: latencyMs2 } : {},
5094
+ ...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
5031
5095
  };
5032
5096
  const updated = {
5033
5097
  ...existing,
@@ -5042,10 +5106,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
5042
5106
  graph.replaceEdgeAttributes(id, updated);
5043
5107
  return { edge: updated, created: false };
5044
5108
  }
5109
+ const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
5110
+ const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
5045
5111
  const signal = {
5046
5112
  spanCount: 1,
5047
5113
  errorCount: isError ? 1 : 0,
5048
- lastObservedAgeMs: 0
5114
+ lastObservedAgeMs: 0,
5115
+ ...latencyHist ? { latencyHist } : {},
5116
+ ...latencyMs ? { latencyMs } : {}
5049
5117
  };
5050
5118
  const edge = {
5051
5119
  id,
@@ -5109,6 +5177,21 @@ async function appendErrorEvent(ctx, ev) {
5109
5177
  await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(ctx.errorsPath), { recursive: true });
5110
5178
  await import_node_fs9.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5111
5179
  }
5180
+ async function appendConnectorIncident(errorsPath, input) {
5181
+ const ev = {
5182
+ id: input.id,
5183
+ timestamp: input.timestamp,
5184
+ service: input.service,
5185
+ traceId: input.id,
5186
+ spanId: input.id,
5187
+ errorType: input.errorType,
5188
+ errorMessage: input.errorMessage,
5189
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
5190
+ affectedNode: input.affectedNode
5191
+ };
5192
+ await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(errorsPath), { recursive: true });
5193
+ await import_node_fs9.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5194
+ }
5112
5195
  function incidentAffectedNode(span, graph, scanPath) {
5113
5196
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types7.serviceId)(span.service, span.env);
5114
5197
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
@@ -5230,6 +5313,7 @@ async function handleSpan(ctx, span) {
5230
5313
  }
5231
5314
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
5232
5315
  const isError = span.statusCode === 2;
5316
+ const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
5233
5317
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
5234
5318
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
5235
5319
  cacheSpanService(span, nowMs, callSite);
@@ -5268,7 +5352,8 @@ async function handleSpan(ctx, span) {
5268
5352
  targetId,
5269
5353
  ts,
5270
5354
  isError,
5271
- callSiteEvidence
5355
+ callSiteEvidence,
5356
+ durationMs
5272
5357
  );
5273
5358
  if (result) affectedNode = targetId;
5274
5359
  if (span.dbSystem === "mongodb" && span.dbCollection) {
@@ -5280,7 +5365,8 @@ async function handleSpan(ctx, span) {
5280
5365
  collectionId,
5281
5366
  ts,
5282
5367
  isError,
5283
- callSiteEvidence
5368
+ callSiteEvidence,
5369
+ durationMs
5284
5370
  );
5285
5371
  }
5286
5372
  if (span.dbTable) {
@@ -5292,7 +5378,8 @@ async function handleSpan(ctx, span) {
5292
5378
  tableId,
5293
5379
  ts,
5294
5380
  isError,
5295
- callSiteEvidence
5381
+ callSiteEvidence,
5382
+ durationMs
5296
5383
  );
5297
5384
  mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
5298
5385
  }
@@ -5311,7 +5398,8 @@ async function handleSpan(ctx, span) {
5311
5398
  targetId,
5312
5399
  ts,
5313
5400
  isError,
5314
- callSiteEvidence
5401
+ callSiteEvidence,
5402
+ durationMs
5315
5403
  );
5316
5404
  if (result) affectedNode = targetId;
5317
5405
  } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
@@ -5328,7 +5416,8 @@ async function handleSpan(ctx, span) {
5328
5416
  targetId,
5329
5417
  ts,
5330
5418
  isError,
5331
- callSiteEvidence
5419
+ callSiteEvidence,
5420
+ durationMs
5332
5421
  );
5333
5422
  if (result) affectedNode = targetId;
5334
5423
  } else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
@@ -5340,7 +5429,8 @@ async function handleSpan(ctx, span) {
5340
5429
  targetId,
5341
5430
  ts,
5342
5431
  isError,
5343
- callSiteEvidence
5432
+ callSiteEvidence,
5433
+ durationMs
5344
5434
  );
5345
5435
  if (result) affectedNode = targetId;
5346
5436
  } else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
@@ -5356,7 +5446,8 @@ async function handleSpan(ctx, span) {
5356
5446
  targetId,
5357
5447
  ts,
5358
5448
  isError,
5359
- callSiteEvidence
5449
+ callSiteEvidence,
5450
+ durationMs
5360
5451
  );
5361
5452
  if (result) affectedNode = targetId;
5362
5453
  } else {
@@ -5372,7 +5463,8 @@ async function handleSpan(ctx, span) {
5372
5463
  targetId,
5373
5464
  ts,
5374
5465
  isError,
5375
- callSiteEvidence
5466
+ callSiteEvidence,
5467
+ durationMs
5376
5468
  );
5377
5469
  affectedNode = targetId;
5378
5470
  resolvedViaAddress = true;
@@ -5385,7 +5477,8 @@ async function handleSpan(ctx, span) {
5385
5477
  frontierNodeId,
5386
5478
  ts,
5387
5479
  isError,
5388
- callSiteEvidence
5480
+ callSiteEvidence,
5481
+ durationMs
5389
5482
  );
5390
5483
  affectedNode = frontierNodeId;
5391
5484
  resolvedViaAddress = true;
@@ -5411,7 +5504,8 @@ async function handleSpan(ctx, span) {
5411
5504
  sourceId,
5412
5505
  ts,
5413
5506
  isError,
5414
- fallbackEvidence
5507
+ fallbackEvidence,
5508
+ durationMs
5415
5509
  );
5416
5510
  }
5417
5511
  }
@@ -5425,7 +5519,7 @@ async function handleSpan(ctx, span) {
5425
5519
  );
5426
5520
  if (routeNodeId) {
5427
5521
  const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
5428
- upsertObservedEdge(ctx.graph, import_types7.EdgeType.CONTAINS, (0, import_types7.serviceId)(routeSvc), routeNodeId, ts, isError);
5522
+ upsertObservedEdge(ctx.graph, import_types7.EdgeType.CONTAINS, (0, import_types7.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
5429
5523
  }
5430
5524
  }
5431
5525
  if (span.statusCode === 2) {
@@ -5925,7 +6019,7 @@ var rootCauseShapes = {
5925
6019
  [import_types8.NodeType.FileNode]: fileRootCauseShape,
5926
6020
  [import_types8.NodeType.SymbolNode]: symbolRootCauseShape
5927
6021
  };
5928
- function getRootCause(graph, errorNodeId, errorEvent, incidents) {
6022
+ function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
5929
6023
  if (!graph.hasNode(errorNodeId)) return null;
5930
6024
  const origin = graph.getNodeAttributes(errorNodeId);
5931
6025
  const shape = rootCauseShapes[origin.type];
@@ -6182,7 +6276,9 @@ function getObservedDependencies(graph, nodeId) {
6182
6276
  dependencies: [],
6183
6277
  observed: false,
6184
6278
  inboundObservedCount: 0,
6185
- hasExtractedOutbound: false
6279
+ hasExtractedOutbound: false,
6280
+ inboundVolume: 0,
6281
+ window: "lifetime"
6186
6282
  });
6187
6283
  }
6188
6284
  const attrs = graph.getNodeAttributes(nodeId);
@@ -6213,11 +6309,19 @@ function getObservedDependencies(graph, nodeId) {
6213
6309
  }
6214
6310
  }
6215
6311
  let inboundObservedCount = 0;
6312
+ let inboundVolume = 0;
6313
+ let inboundLastObserved;
6216
6314
  for (const tgt of scope) {
6217
6315
  for (const edgeId of graph.inboundEdges(tgt)) {
6218
6316
  const e = graph.getEdgeAttributes(edgeId);
6219
6317
  if (e.type === import_types8.EdgeType.CONTAINS) continue;
6220
- if (e.provenance === import_types8.Provenance.OBSERVED) inboundObservedCount += 1;
6318
+ if (e.provenance === import_types8.Provenance.OBSERVED) {
6319
+ inboundObservedCount += 1;
6320
+ inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
6321
+ if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
6322
+ inboundLastObserved = e.lastObserved;
6323
+ }
6324
+ }
6221
6325
  }
6222
6326
  }
6223
6327
  dependencies.sort(
@@ -6228,7 +6332,291 @@ function getObservedDependencies(graph, nodeId) {
6228
6332
  dependencies,
6229
6333
  observed: dependencies.length > 0 || inboundObservedCount > 0,
6230
6334
  inboundObservedCount,
6231
- hasExtractedOutbound
6335
+ hasExtractedOutbound,
6336
+ // The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
6337
+ inboundVolume,
6338
+ window: "lifetime",
6339
+ ...inboundLastObserved ? { inboundLastObserved } : {}
6340
+ });
6341
+ }
6342
+ var SATURATION_P95_MS = 1e3;
6343
+ function nodeScope(graph, nodeId) {
6344
+ const scope = [nodeId];
6345
+ if (!graph.hasNode(nodeId)) return scope;
6346
+ const attrs = graph.getNodeAttributes(nodeId);
6347
+ if (attrs.type === import_types8.NodeType.ServiceNode) {
6348
+ for (const edgeId of graph.outboundEdges(nodeId)) {
6349
+ const e = graph.getEdgeAttributes(edgeId);
6350
+ if (e.type !== import_types8.EdgeType.CONTAINS) continue;
6351
+ const owned = graph.getNodeAttributes(e.target);
6352
+ if (owned.type === import_types8.NodeType.FileNode) scope.push(e.target);
6353
+ }
6354
+ }
6355
+ return scope;
6356
+ }
6357
+ function incidentCountForNode(nodeId, incidents) {
6358
+ if (!incidents || incidents.length === 0) return 0;
6359
+ return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
6360
+ }
6361
+ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6362
+ const scope = nodeScope(graph, nodeId);
6363
+ let errorsFromCallers = 0;
6364
+ let inboundVolume = 0;
6365
+ let outboundVolume = 0;
6366
+ let outboundErrors = 0;
6367
+ let latestInboundMs;
6368
+ let latencyP95Ms;
6369
+ let stale = false;
6370
+ for (const n of scope) {
6371
+ if (!graph.hasNode(n)) continue;
6372
+ for (const edgeId of graph.inboundEdges(n)) {
6373
+ const e = graph.getEdgeAttributes(edgeId);
6374
+ if (e.type === import_types8.EdgeType.CONTAINS) continue;
6375
+ errorsFromCallers += e.signal?.errorCount ?? 0;
6376
+ inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
6377
+ if (e.provenance === import_types8.Provenance.STALE) stale = true;
6378
+ const p95 = e.signal?.latencyMs?.p95;
6379
+ if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
6380
+ if (e.lastObserved) {
6381
+ const t = Date.parse(e.lastObserved);
6382
+ if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
6383
+ }
6384
+ }
6385
+ for (const edgeId of graph.outboundEdges(n)) {
6386
+ const e = graph.getEdgeAttributes(edgeId);
6387
+ if (e.type === import_types8.EdgeType.CONTAINS) continue;
6388
+ outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
6389
+ if (e.type === import_types8.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
6390
+ if (e.provenance === import_types8.Provenance.STALE) stale = true;
6391
+ }
6392
+ }
6393
+ const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
6394
+ const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
6395
+ return {
6396
+ errorsEmittedHere,
6397
+ errorsFromCallers,
6398
+ callCount: inboundVolume,
6399
+ outboundVolume,
6400
+ ...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
6401
+ ...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
6402
+ stale
6403
+ };
6404
+ }
6405
+ function isSaturated(ctx) {
6406
+ return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
6407
+ }
6408
+ function classifyNode(ctx) {
6409
+ if (ctx.errorsEmittedHere > 0) {
6410
+ if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
6411
+ return "symptom-only";
6412
+ }
6413
+ return "primary-failure";
6414
+ }
6415
+ if (ctx.errorsFromCallers > 0) return "symptom-only";
6416
+ return "unrelated";
6417
+ }
6418
+ function isVictimSeed(ctx) {
6419
+ return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
6420
+ }
6421
+ function grainOf(graph, nodeId) {
6422
+ if (!graph.hasNode(nodeId)) return "unknown";
6423
+ const t = graph.getNodeAttributes(nodeId).type;
6424
+ if (t === import_types8.NodeType.ServiceNode) return "service";
6425
+ if (t === import_types8.NodeType.FileNode) return "file";
6426
+ if (t === import_types8.NodeType.SymbolNode) return "symbol";
6427
+ return t;
6428
+ }
6429
+ function findPath(graph, from, to, direction, maxDepth) {
6430
+ if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
6431
+ if (from === to) return { nodes: [from], edges: [] };
6432
+ const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
6433
+ const enqueued = /* @__PURE__ */ new Set([from]);
6434
+ while (queue.length > 0) {
6435
+ const frame = queue.shift();
6436
+ if (frame.depth >= maxDepth) continue;
6437
+ const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
6438
+ const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
6439
+ for (const [nid, edge] of neighbours) {
6440
+ if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
6441
+ if (enqueued.has(nid)) continue;
6442
+ enqueued.add(nid);
6443
+ queue.push({
6444
+ nodeId: nid,
6445
+ depth: frame.depth + 1,
6446
+ nodes: [...frame.nodes, nid],
6447
+ edges: [...frame.edges, edge]
6448
+ });
6449
+ }
6450
+ }
6451
+ return null;
6452
+ }
6453
+ var EMPTY_CONTEXT = {
6454
+ errorsEmittedHere: 0,
6455
+ errorsFromCallers: 0,
6456
+ callCount: 0,
6457
+ outboundVolume: 0,
6458
+ stale: false
6459
+ };
6460
+ function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
6461
+ if (!graph.hasNode(nodeId)) {
6462
+ return import_types8.ExpandResultSchema.parse({
6463
+ origin: nodeId,
6464
+ direction,
6465
+ node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
6466
+ neighbours: []
6467
+ });
6468
+ }
6469
+ const ctx = nodeContext(graph, nodeId, incidents, now);
6470
+ const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
6471
+ const neighbours = [];
6472
+ for (const [nid, edge] of best) {
6473
+ if (edge.type === import_types8.EdgeType.CONTAINS) continue;
6474
+ const nctx = nodeContext(graph, nid, incidents, now);
6475
+ neighbours.push({
6476
+ node: nid,
6477
+ edgeType: edge.type,
6478
+ provenance: edge.provenance,
6479
+ classification: classifyNode(nctx),
6480
+ context: nctx
6481
+ });
6482
+ }
6483
+ neighbours.sort((a, b) => a.node.localeCompare(b.node));
6484
+ return import_types8.ExpandResultSchema.parse({
6485
+ origin: nodeId,
6486
+ direction,
6487
+ node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
6488
+ neighbours
6489
+ });
6490
+ }
6491
+ function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
6492
+ const buildPath = (fp) => ({
6493
+ nodes: fp.nodes,
6494
+ edgeTypes: fp.edges.map((e) => e.type),
6495
+ provenance: fp.edges.map((e) => e.provenance),
6496
+ grain: fp.nodes.map((n) => grainOf(graph, n)),
6497
+ // The failure runs end to end when every hop carries error / latency / alert
6498
+ // signal — that is what turns reachability into cause-confirmation.
6499
+ carriesSignal: fp.edges.length > 0 && fp.edges.every(
6500
+ (e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
6501
+ )
6502
+ });
6503
+ if (!graph.hasNode(a) || !graph.hasNode(b)) {
6504
+ return import_types8.RelateResultSchema.parse({
6505
+ a,
6506
+ b,
6507
+ related: false,
6508
+ direction: null,
6509
+ paths: [],
6510
+ note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
6511
+ });
6512
+ }
6513
+ const down = findPath(graph, a, b, "down", maxDepth);
6514
+ const up = findPath(graph, a, b, "up", maxDepth);
6515
+ if (!down && !up) {
6516
+ return import_types8.RelateResultSchema.parse({
6517
+ a,
6518
+ b,
6519
+ related: false,
6520
+ direction: null,
6521
+ paths: [],
6522
+ note: `no path within ${maxDepth} hops`
6523
+ });
6524
+ }
6525
+ const paths = [];
6526
+ const direction = down ? "a->b" : "b->a";
6527
+ if (down) paths.push(buildPath(down));
6528
+ if (up) paths.push(buildPath(up));
6529
+ const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
6530
+ const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
6531
+ return import_types8.RelateResultSchema.parse({
6532
+ a,
6533
+ b,
6534
+ related: true,
6535
+ direction,
6536
+ paths,
6537
+ ...grainGap ? { grainGap: true } : {}
6538
+ });
6539
+ }
6540
+ function findLoadOrigin(graph, alertNodeId, incidents, now) {
6541
+ const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
6542
+ let best = null;
6543
+ for (const nid of upstream) {
6544
+ if (nid === alertNodeId) continue;
6545
+ const ctx = nodeContext(graph, nid, incidents, now);
6546
+ if (ctx.outboundVolume === 0) continue;
6547
+ const isSource = ctx.callCount === 0;
6548
+ const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
6549
+ if (better) best = { node: nid, ctx, isSource };
6550
+ }
6551
+ return best ? { node: best.node, ctx: best.ctx } : null;
6552
+ }
6553
+ function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
6554
+ const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
6555
+ if (!legacy) return null;
6556
+ const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
6557
+ if (!navigation) return legacy;
6558
+ return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
6559
+ }
6560
+ function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
6561
+ const seedNode = legacy.rootCauseNode;
6562
+ const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
6563
+ const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
6564
+ const candidates = [];
6565
+ if (seedCtx && isVictimSeed(seedCtx)) {
6566
+ const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
6567
+ if (origin) {
6568
+ const path68 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
6569
+ const originConfidence = confidenceFromMix(path68?.edges ?? [], now);
6570
+ candidates.push({
6571
+ node: origin.node,
6572
+ classification: "primary-failure",
6573
+ reason: `Highest-volume upstream source (${origin.ctx.outboundVolume} observed outbound calls) driving a saturated/stale subgraph; the alerting path decays downstream into a starved victim rather than a fault at the callee.`,
6574
+ context: origin.ctx,
6575
+ confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
6576
+ provenance: import_types8.Provenance.OBSERVED
6577
+ });
6578
+ }
6579
+ const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
6580
+ const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
6581
+ candidates.push({
6582
+ node: seedNode,
6583
+ classification: "symptom-only",
6584
+ reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
6585
+ context: seedCtx,
6586
+ confidence: Math.min(legacy.confidence, 0.4),
6587
+ ...lastProv ? { provenance: lastProv } : {}
6588
+ });
6589
+ } else {
6590
+ candidates.push({
6591
+ node: seedNode,
6592
+ classification: "primary-failure",
6593
+ reason: legacy.rootCauseReason,
6594
+ context: seedCtx ?? EMPTY_CONTEXT,
6595
+ confidence: legacy.confidence,
6596
+ ...lastProv ? { provenance: lastProv } : {}
6597
+ });
6598
+ }
6599
+ const top = candidates[0];
6600
+ let traversalPath = legacy.traversalPath;
6601
+ let edgeProvenances = legacy.edgeProvenances;
6602
+ if (top.node !== seedNode) {
6603
+ const path68 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
6604
+ if (path68) {
6605
+ traversalPath = path68.nodes;
6606
+ edgeProvenances = path68.edges.map((e) => e.provenance);
6607
+ } else {
6608
+ traversalPath = [errorNodeId, top.node];
6609
+ edgeProvenances = [top.provenance ?? import_types8.Provenance.OBSERVED];
6610
+ }
6611
+ }
6612
+ return import_types8.RootCauseResultSchema.parse({
6613
+ rootCauseNode: top.node,
6614
+ rootCauseReason: top.reason,
6615
+ traversalPath,
6616
+ edgeProvenances,
6617
+ confidence: top.confidence,
6618
+ ...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
6619
+ candidates
6232
6620
  });
6233
6621
  }
6234
6622
 
@@ -11801,8 +12189,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
11801
12189
  var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
11802
12190
  var import_types36 = require("@neat.is/types");
11803
12191
  init_otel();
11804
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
11805
12192
  var PARSE_CHUNK10 = 16384;
12193
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
12194
+ "Query",
12195
+ "QueryContext",
12196
+ "QueryRow",
12197
+ "QueryRowContext",
12198
+ "Exec",
12199
+ "ExecContext",
12200
+ "Prepare",
12201
+ "PrepareContext"
12202
+ ]);
12203
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
12204
+ "Get",
12205
+ "Select",
12206
+ "Queryx",
12207
+ "QueryRowx",
12208
+ "NamedExec",
12209
+ "NamedQuery",
12210
+ "MustExec",
12211
+ "Preparex",
12212
+ "GetContext",
12213
+ "SelectContext"
12214
+ ]);
12215
+ var DATABASE_SQL_IMPORT = "database/sql";
12216
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
12217
+ function makeGoParser3() {
12218
+ const p = new import_tree_sitter14.default();
12219
+ p.setLanguage(import_tree_sitter_go3.default);
12220
+ return p;
12221
+ }
12222
+ function parseSource10(parser, source) {
12223
+ return parser.parse(
12224
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
12225
+ );
12226
+ }
11806
12227
  function walk7(node, visit) {
11807
12228
  visit(node);
11808
12229
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -11810,25 +12231,54 @@ function walk7(node, visit) {
11810
12231
  if (child) walk7(child, visit);
11811
12232
  }
11812
12233
  }
12234
+ function goStringLiteralValue(node) {
12235
+ if (!node) return null;
12236
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
12237
+ const t = node.text;
12238
+ return t.length >= 2 ? t.slice(1, -1) : "";
12239
+ }
12240
+ return null;
12241
+ }
12242
+ function goImportsAny(root, names) {
12243
+ let found = false;
12244
+ walk7(root, (node) => {
12245
+ if (found || node.type !== "import_spec") return;
12246
+ for (let i = 0; i < node.namedChildCount; i++) {
12247
+ const value = goStringLiteralValue(node.namedChild(i));
12248
+ if (value !== null && names.has(value)) found = true;
12249
+ }
12250
+ });
12251
+ return found;
12252
+ }
12253
+ function firstStringLiteralArg(argsNode) {
12254
+ if (!argsNode) return null;
12255
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
12256
+ const value = goStringLiteralValue(argsNode.namedChild(i));
12257
+ if (value !== null) return value;
12258
+ }
12259
+ return null;
12260
+ }
11813
12261
  function goSqlEndpointsFromFile(file, serviceDir) {
11814
12262
  if (import_node_path49.default.extname(file.path) !== ".go") return [];
11815
- const parser = new import_tree_sitter14.default();
11816
- parser.setLanguage(import_tree_sitter_go3.default);
11817
- const tree = parser.parse(
11818
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
11819
- );
12263
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
12264
+ const tree = parseSource10(makeGoParser3(), file.content);
12265
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
12266
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
12267
+ if (!importsDatabaseSql && !importsSqlx) return [];
11820
12268
  const out = [];
11821
12269
  walk7(tree.rootNode, (node) => {
11822
12270
  if (node.type !== "call_expression") return;
11823
12271
  const fn = node.childForFieldName("function");
11824
12272
  if (fn?.type !== "selector_expression") return;
11825
12273
  const method = fn.childForFieldName("field")?.text;
11826
- if (!method || !SQL_METHODS.has(method)) return;
11827
- const arg = node.childForFieldName("arguments")?.namedChild(0);
11828
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
11829
- const sql = arg.text.slice(1, -1);
12274
+ if (!method) return;
12275
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
12276
+ if (!recognized) return;
12277
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
12278
+ if (sql === null) return;
11830
12279
  const table = tableFromSqlStatement(sql);
11831
12280
  if (!table) return;
12281
+ const columns = columnsFromSqlStatement(sql);
11832
12282
  const line = node.startPosition.row + 1;
11833
12283
  out.push({
11834
12284
  infraId: (0, import_types36.infraId)("sql-table", table),
@@ -11836,7 +12286,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
11836
12286
  kind: "sql-table",
11837
12287
  edgeType: "CALLS",
11838
12288
  confidenceKind: "verified-call-site",
11839
- evidence: { file: toPosix(import_node_path49.default.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
12289
+ ...columns.length > 0 ? { columns } : {},
12290
+ evidence: {
12291
+ file: toPosix(import_node_path49.default.relative(serviceDir, file.path)),
12292
+ line,
12293
+ snippet: snippet(file.content, line)
12294
+ }
11840
12295
  });
11841
12296
  });
11842
12297
  return out;
@@ -11850,12 +12305,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
11850
12305
  var import_types37 = require("@neat.is/types");
11851
12306
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
11852
12307
  var PARSE_CHUNK11 = 16384;
11853
- function makeGoParser3() {
12308
+ function makeGoParser4() {
11854
12309
  const p = new import_tree_sitter15.default();
11855
12310
  p.setLanguage(import_tree_sitter_go4.default);
11856
12311
  return p;
11857
12312
  }
11858
- function parseSource10(parser, source) {
12313
+ function parseSource11(parser, source) {
11859
12314
  return parser.parse(
11860
12315
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
11861
12316
  );
@@ -12279,7 +12734,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
12279
12734
  function gormEndpointsFromFile(file, serviceDir) {
12280
12735
  if (import_node_path50.default.extname(file.path) !== ".go") return [];
12281
12736
  if (!GORM_IMPORT_RE.test(file.content)) return [];
12282
- const tree = parseSource10(makeGoParser3(), file.content);
12737
+ const tree = parseSource11(makeGoParser4(), file.content);
12283
12738
  const { structs, models, tableFor } = analyze(tree);
12284
12739
  const out = [];
12285
12740
  const seenTables = /* @__PURE__ */ new Set();
@@ -12310,7 +12765,7 @@ function gormEndpointsFromFile(file, serviceDir) {
12310
12765
  function gormForeignKeys(file, serviceDir) {
12311
12766
  if (import_node_path50.default.extname(file.path) !== ".go") return [];
12312
12767
  if (!GORM_IMPORT_RE.test(file.content)) return [];
12313
- const tree = parseSource10(makeGoParser3(), file.content);
12768
+ const tree = parseSource11(makeGoParser4(), file.content);
12314
12769
  const { structs, models, tableFor } = analyze(tree);
12315
12770
  const out = [];
12316
12771
  const seen = /* @__PURE__ */ new Set();
@@ -14593,6 +15048,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
14593
15048
  unresolved++;
14594
15049
  continue;
14595
15050
  }
15051
+ if (signal.incident) {
15052
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
15053
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
15054
+ unresolved++;
15055
+ continue;
15056
+ }
15057
+ await appendConnectorIncident(ctx.errorsPath, {
15058
+ id: signal.incident.id,
15059
+ timestamp: signal.incident.timestamp,
15060
+ service: signal.incident.service,
15061
+ errorType: signal.incident.errorType,
15062
+ errorMessage: signal.incident.errorMessage,
15063
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
15064
+ affectedNode: resolved.targetNodeId
15065
+ });
15066
+ continue;
15067
+ }
14596
15068
  if (resolved.ensureInfraNode) {
14597
15069
  const { kind, name, provider } = resolved.ensureInfraNode;
14598
15070
  ensureInfraNode(graph, kind, name, provider);
@@ -16786,6 +17258,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
16786
17258
  };
16787
17259
  }
16788
17260
 
17261
+ // src/connectors/eas/index.ts
17262
+ init_cjs_shims();
17263
+
17264
+ // src/connectors/eas/client.ts
17265
+ init_cjs_shims();
17266
+
17267
+ // src/connectors/eas/types.ts
17268
+ init_cjs_shims();
17269
+ function readEasCredentials(raw) {
17270
+ const token = raw["token"];
17271
+ if (typeof token !== "string" || token.length === 0) {
17272
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
17273
+ }
17274
+ return { token };
17275
+ }
17276
+ var EAS_STATUS_ERRORED = "ERRORED";
17277
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
17278
+ "SPIN_UP_BUILDER",
17279
+ "PREPARE_CREDENTIALS",
17280
+ "RESTORE_CACHE",
17281
+ "UPLOAD_APPLICATION_ARCHIVE"
17282
+ ]);
17283
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
17284
+ function isTransientFailure(err) {
17285
+ if (!err) return false;
17286
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
17287
+ if (phase) {
17288
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
17289
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
17290
+ }
17291
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
17292
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
17293
+ return false;
17294
+ }
17295
+ var FIELD_SEP3 = "\0";
17296
+ var EAS_TARGET_KIND = "eas-build";
17297
+ function packEasTargetName(identity) {
17298
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
17299
+ }
17300
+ function parseEasTargetName(targetName) {
17301
+ const sep = targetName.indexOf(FIELD_SEP3);
17302
+ if (sep === -1) return null;
17303
+ const serviceName = targetName.slice(0, sep);
17304
+ const phase = targetName.slice(sep + 1);
17305
+ if (!serviceName) return null;
17306
+ return { serviceName, phase };
17307
+ }
17308
+
17309
+ // src/connectors/eas/client.ts
17310
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
17311
+ var DEFAULT_PAGE_SIZE = 50;
17312
+ var DEFAULT_MAX_PAGES = 10;
17313
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
17314
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
17315
+ var BUILDS_QUERY = `
17316
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
17317
+ app {
17318
+ byId(appId: $appId) {
17319
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
17320
+ id
17321
+ status
17322
+ platform
17323
+ buildProfile
17324
+ gitCommitHash
17325
+ gitCommitMessage
17326
+ gitRef
17327
+ isGitWorkingTreeDirty
17328
+ createdAt
17329
+ completedAt
17330
+ error {
17331
+ buildPhase
17332
+ errorCode
17333
+ message
17334
+ docsUrl
17335
+ }
17336
+ logFileUrls
17337
+ }
17338
+ }
17339
+ }
17340
+ }
17341
+ `;
17342
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
17343
+ const res = await junctionFetch(
17344
+ apiUrl,
17345
+ {
17346
+ method: "POST",
17347
+ headers: {
17348
+ "Content-Type": "application/json",
17349
+ ...bearerAuthHeader(token)
17350
+ },
17351
+ body: JSON.stringify({ query, variables })
17352
+ },
17353
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
17354
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
17355
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
17356
+ );
17357
+ if (!res.ok) {
17358
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
17359
+ }
17360
+ const body = await res.json();
17361
+ if (body.errors && body.errors.length > 0) {
17362
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
17363
+ }
17364
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
17365
+ return body.data;
17366
+ }
17367
+ async function fetchErroredBuilds(token, config, fetchImpl) {
17368
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
17369
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
17370
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
17371
+ const out = [];
17372
+ const seen = /* @__PURE__ */ new Set();
17373
+ for (let page = 0; page < maxPages; page++) {
17374
+ const data = await easGraphQL(
17375
+ apiUrl,
17376
+ token,
17377
+ BUILDS_QUERY,
17378
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
17379
+ config.appId,
17380
+ fetchImpl
17381
+ );
17382
+ const builds = data.app?.byId?.builds;
17383
+ if (!Array.isArray(builds)) break;
17384
+ let added = 0;
17385
+ for (const b of builds) {
17386
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
17387
+ if (b.status !== EAS_STATUS_ERRORED) continue;
17388
+ if (seen.has(b.id)) continue;
17389
+ seen.add(b.id);
17390
+ out.push(b);
17391
+ added++;
17392
+ }
17393
+ if (builds.length < pageSize) break;
17394
+ if (added === 0) break;
17395
+ }
17396
+ return out;
17397
+ }
17398
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
17399
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
17400
+ const doFetch = fetchImpl ?? fetch;
17401
+ const chunks = [];
17402
+ for (const url of logFileUrls) {
17403
+ if (typeof url !== "string" || url.length === 0) continue;
17404
+ try {
17405
+ const res = await doFetch(url);
17406
+ if (!res.ok) continue;
17407
+ chunks.push(await res.text());
17408
+ } catch {
17409
+ }
17410
+ }
17411
+ const joined = chunks.join("\n");
17412
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
17413
+ }
17414
+
17415
+ // src/connectors/eas/map.ts
17416
+ init_cjs_shims();
17417
+ function buildEventTime(build) {
17418
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
17419
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
17420
+ return (/* @__PURE__ */ new Date()).toISOString();
17421
+ }
17422
+ function incidentMessage2(build) {
17423
+ const err = build.error ?? {};
17424
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
17425
+ const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
17426
+ let msg = `EAS build failed${phase}: ${detail}`;
17427
+ if (build.isGitWorkingTreeDirty === true) {
17428
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
17429
+ }
17430
+ return msg;
17431
+ }
17432
+ function incidentAttributes(build) {
17433
+ const attrs = {};
17434
+ const err = build.error ?? {};
17435
+ const put = (k, v) => {
17436
+ if (typeof v === "string" && v.length === 0) return;
17437
+ if (v !== void 0 && v !== null) attrs[k] = v;
17438
+ };
17439
+ put("eas.buildId", build.id);
17440
+ put("eas.platform", build.platform ?? void 0);
17441
+ put("eas.buildProfile", build.buildProfile ?? void 0);
17442
+ put("eas.buildPhase", err.buildPhase ?? void 0);
17443
+ put("eas.errorCode", err.errorCode ?? void 0);
17444
+ put("eas.docsUrl", err.docsUrl ?? void 0);
17445
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
17446
+ put("eas.gitRef", build.gitRef ?? void 0);
17447
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
17448
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
17449
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
17450
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
17451
+ }
17452
+ put("eas.createdAt", build.createdAt ?? void 0);
17453
+ put("eas.completedAt", build.completedAt ?? void 0);
17454
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
17455
+ attrs["eas.logs"] = build.logsText;
17456
+ }
17457
+ return attrs;
17458
+ }
17459
+ function mapBuildToSignal(build, serviceName) {
17460
+ if (!build || typeof build !== "object") return null;
17461
+ if (build.status !== EAS_STATUS_ERRORED) return null;
17462
+ if (!build.error) return null;
17463
+ if (isTransientFailure(build.error)) return null;
17464
+ const timestamp = buildEventTime(build);
17465
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
17466
+ return {
17467
+ targetKind: EAS_TARGET_KIND,
17468
+ targetName: packEasTargetName({ serviceName, phase }),
17469
+ // Incident-only — no edge, so no call/error count to replay.
17470
+ callCount: 0,
17471
+ errorCount: 0,
17472
+ lastObservedIso: timestamp,
17473
+ incident: {
17474
+ id: `eas:build:${build.id}`,
17475
+ timestamp,
17476
+ service: serviceName,
17477
+ errorType: "eas-build-failure",
17478
+ errorMessage: incidentMessage2(build),
17479
+ attributes: incidentAttributes(build)
17480
+ }
17481
+ };
17482
+ }
17483
+ function mapBuildsToSignals(builds, serviceName) {
17484
+ const out = [];
17485
+ for (const build of builds) {
17486
+ const signal = mapBuildToSignal(build, serviceName);
17487
+ if (signal) out.push(signal);
17488
+ }
17489
+ return out;
17490
+ }
17491
+
17492
+ // src/connectors/eas/resolve.ts
17493
+ init_cjs_shims();
17494
+ var import_types82 = require("@neat.is/types");
17495
+ var NO_ENV2 = "unknown";
17496
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
17497
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
17498
+ "READ_APP_CONFIG",
17499
+ "CONFIGURE_EXPO_UPDATES",
17500
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
17501
+ ]);
17502
+ function configBasenamesForPhase(phase) {
17503
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
17504
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
17505
+ return [];
17506
+ }
17507
+ function configNodeService(graph, configNodeId) {
17508
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
17509
+ const edge = graph.getEdgeAttributes(edgeId);
17510
+ if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
17511
+ const parsed = (0, import_types82.parseFileId)(edge.source);
17512
+ if (parsed) return parsed.service;
17513
+ }
17514
+ return null;
17515
+ }
17516
+ function findConfigNode(graph, basenames, serviceName) {
17517
+ let scoped = null;
17518
+ let anyMatch = null;
17519
+ graph.forEachNode((id, attrs) => {
17520
+ if (scoped) return;
17521
+ const node = attrs;
17522
+ if (node.type !== import_types82.NodeType.ConfigNode) return;
17523
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
17524
+ if (anyMatch === null) anyMatch = id;
17525
+ if (configNodeService(graph, id) === serviceName) scoped = id;
17526
+ });
17527
+ return scoped ?? anyMatch;
17528
+ }
17529
+ function createEasResolveTarget(graph) {
17530
+ return (signal) => {
17531
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
17532
+ const identity = parseEasTargetName(signal.targetName);
17533
+ if (!identity) return null;
17534
+ const { serviceName, phase } = identity;
17535
+ const basenames = configBasenamesForPhase(phase);
17536
+ if (basenames.length > 0) {
17537
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
17538
+ if (configNodeId) {
17539
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
17540
+ }
17541
+ }
17542
+ return {
17543
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
17544
+ serviceName,
17545
+ edgeType: import_types82.EdgeType.CALLS
17546
+ };
17547
+ };
17548
+ }
17549
+
17550
+ // src/connectors/eas/index.ts
17551
+ function isBuildSince(build, sinceIso) {
17552
+ const t = Date.parse(buildEventTime(build));
17553
+ const s = Date.parse(sinceIso);
17554
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
17555
+ return t > s;
17556
+ }
17557
+ function boundedSinceIso2(since, now, maxLookbackMs) {
17558
+ const floor = new Date(now.getTime() - maxLookbackMs);
17559
+ if (!since) return floor.toISOString();
17560
+ const sinceMs = new Date(since).getTime();
17561
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
17562
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
17563
+ }
17564
+ var EasConnector = class {
17565
+ constructor(config, fetchImpl) {
17566
+ this.config = config;
17567
+ this.fetchImpl = fetchImpl;
17568
+ }
17569
+ config;
17570
+ fetchImpl;
17571
+ provider = "eas";
17572
+ async poll(ctx) {
17573
+ const creds = readEasCredentials(ctx.credentials);
17574
+ const serviceName = this.config.serviceName ?? this.config.appId;
17575
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
17576
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
17577
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
17578
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
17579
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
17580
+ for (const build of fresh) {
17581
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
17582
+ }
17583
+ return mapBuildsToSignals(fresh, serviceName);
17584
+ }
17585
+ };
17586
+ function createEasConnector(graph, config, fetchImpl) {
17587
+ return {
17588
+ connector: new EasConnector(config, fetchImpl),
17589
+ resolveTarget: createEasResolveTarget(graph)
17590
+ };
17591
+ }
17592
+
16789
17593
  // src/connectors/registry.ts
16790
17594
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
16791
17595
  async function authProbe(input) {
@@ -17073,6 +17877,41 @@ var PROVIDER_DISPATCH = {
17073
17877
  ...fetchImpl ? { fetchImpl } : {}
17074
17878
  });
17075
17879
  }
17880
+ },
17881
+ eas: {
17882
+ provider: "eas",
17883
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
17884
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
17885
+ primaryCredentialKey: "token",
17886
+ requiredCredentialFields: ["token"],
17887
+ requiredOptionFields: ["appId"],
17888
+ build(graph, options) {
17889
+ return createEasConnector(graph, options);
17890
+ },
17891
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
17892
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
17893
+ // authenticates and that this app id is reachable, the same probe-the-real-
17894
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
17895
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
17896
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
17897
+ // silently at the first poll.
17898
+ async validate({ credentials, options, fetchImpl }) {
17899
+ const cfg = options;
17900
+ const appId = String(cfg.appId ?? "");
17901
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
17902
+ const probeConfig = {
17903
+ appId,
17904
+ pageSize: 1,
17905
+ maxPages: 1,
17906
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
17907
+ };
17908
+ try {
17909
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
17910
+ return { ok: true };
17911
+ } catch (err) {
17912
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
17913
+ }
17914
+ }
17076
17915
  }
17077
17916
  };
17078
17917
  function vercelCredsFrom(credentials) {
@@ -17382,11 +18221,11 @@ function registerRoutes(scope, ctx) {
17382
18221
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
17383
18222
  const parsed = [];
17384
18223
  for (const c of candidates) {
17385
- const r = import_types80.DivergenceTypeSchema.safeParse(c);
18224
+ const r = import_types85.DivergenceTypeSchema.safeParse(c);
17386
18225
  if (!r.success) {
17387
18226
  return reply.code(400).send({
17388
18227
  error: `unknown divergence type "${c}"`,
17389
- allowed: import_types80.DivergenceTypeSchema.options
18228
+ allowed: import_types85.DivergenceTypeSchema.options
17390
18229
  });
17391
18230
  }
17392
18231
  parsed.push(r.data);
@@ -17493,10 +18332,15 @@ function registerRoutes(scope, ctx) {
17493
18332
  }
17494
18333
  const reg = built.registration;
17495
18334
  const at = (/* @__PURE__ */ new Date()).toISOString();
18335
+ const incidentsPath = errorsPathFor(proj);
17496
18336
  try {
17497
18337
  const result = await ctx.runPoll(
17498
18338
  reg.connector,
17499
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
18339
+ {
18340
+ projectDir: proj.scanPath ?? "",
18341
+ credentials: reg.credentials,
18342
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
18343
+ },
17500
18344
  proj.graph,
17501
18345
  reg.resolveTarget
17502
18346
  );
@@ -17566,6 +18410,34 @@ function registerRoutes(scope, ctx) {
17566
18410
  }
17567
18411
  return getBlastRadius(proj.graph, nodeId, depth);
17568
18412
  });
18413
+ scope.get("/graph/expand/:nodeId", async (req, reply) => {
18414
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
18415
+ if (!proj) return;
18416
+ const { nodeId } = req.params;
18417
+ if (!proj.graph.hasNode(nodeId)) {
18418
+ return reply.code(404).send({ error: "node not found", id: nodeId });
18419
+ }
18420
+ const direction = req.query.direction;
18421
+ if (direction !== "up" && direction !== "down") {
18422
+ return reply.code(400).send({ error: 'direction must be "up" or "down"' });
18423
+ }
18424
+ const epath = errorsPathFor(proj);
18425
+ const incidents = epath ? await readErrorEvents(epath) : [];
18426
+ return expandNode(proj.graph, nodeId, direction, incidents);
18427
+ });
18428
+ scope.get("/graph/relate", async (req, reply) => {
18429
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
18430
+ if (!proj) return;
18431
+ const { a, b } = req.query;
18432
+ if (!a || !b) {
18433
+ return reply.code(400).send({ error: "both a and b query params are required" });
18434
+ }
18435
+ const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
18436
+ if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
18437
+ return reply.code(400).send({ error: "maxDepth must be a positive integer" });
18438
+ }
18439
+ return relate(proj.graph, a, b, maxDepth);
18440
+ });
17569
18441
  scope.get("/search", async (req, reply) => {
17570
18442
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
17571
18443
  if (!proj) return;
@@ -17695,7 +18567,7 @@ function registerRoutes(scope, ctx) {
17695
18567
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
17696
18568
  let violations = await log.readAll();
17697
18569
  if (req.query.severity) {
17698
- const sev = import_types80.PolicySeveritySchema.safeParse(req.query.severity);
18570
+ const sev = import_types85.PolicySeveritySchema.safeParse(req.query.severity);
17699
18571
  if (!sev.success) {
17700
18572
  return reply.code(400).send({
17701
18573
  error: "invalid severity",
@@ -17734,7 +18606,7 @@ function registerRoutes(scope, ctx) {
17734
18606
  scope.post("/policies/check", async (req, reply) => {
17735
18607
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
17736
18608
  if (!proj) return;
17737
- const parsed = import_types80.PoliciesCheckBodySchema.safeParse(req.body ?? {});
18609
+ const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req.body ?? {});
17738
18610
  if (!parsed.success) {
17739
18611
  return reply.code(400).send({
17740
18612
  error: "invalid /policies/check body",