@neat.is/core 0.9.10-dev.20260830 → 0.9.10

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.
@@ -5987,6 +5987,24 @@ function incidentCountForNode(nodeId, incidents) {
5987
5987
  if (!incidents || incidents.length === 0) return 0;
5988
5988
  return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
5989
5989
  }
5990
+ var DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
5991
+ EdgeType6.CALLS,
5992
+ EdgeType6.CONNECTS_TO,
5993
+ EdgeType6.PUBLISHES_TO,
5994
+ EdgeType6.CONSUMES_FROM
5995
+ ]);
5996
+ var TIMEOUT_ERROR_PATTERNS = [
5997
+ /\bDEADLINE_EXCEEDED\b/i,
5998
+ /\bdeadline exceeded\b/i,
5999
+ /\bETIMEDOUT\b/i,
6000
+ /\btimed[\s-]?out\b/i,
6001
+ /\btimeout\b/i
6002
+ ];
6003
+ function isBoundaryTimeoutError(ev) {
6004
+ if (ev.httpStatusCode === 504) return true;
6005
+ const haystack = [ev.errorType, ev.exceptionType, ev.errorMessage].filter((s) => typeof s === "string").join(" \n ");
6006
+ return TIMEOUT_ERROR_PATTERNS.some((re) => re.test(haystack));
6007
+ }
5990
6008
  function nodeContext(graph, nodeId, incidents, now = Date.now()) {
5991
6009
  const scope = nodeScope(graph, nodeId);
5992
6010
  let errorsFromCallers = 0;
@@ -5996,6 +6014,8 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
5996
6014
  let latestInboundMs;
5997
6015
  let latencyP95Ms;
5998
6016
  let stale = false;
6017
+ let observedErroringDownstream = false;
6018
+ let hasOutboundDeps = false;
5999
6019
  for (const n of scope) {
6000
6020
  if (!graph.hasNode(n)) continue;
6001
6021
  for (const edgeId of graph.inboundEdges(n)) {
@@ -6017,10 +6037,17 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6017
6037
  outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
6018
6038
  if (e.type === EdgeType6.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
6019
6039
  if (e.provenance === Provenance6.STALE) stale = true;
6040
+ if (DEP_EDGE_TYPES.has(e.type)) {
6041
+ hasOutboundDeps = true;
6042
+ if ((e.signal?.errorCount ?? 0) > 0) observedErroringDownstream = true;
6043
+ }
6020
6044
  }
6021
6045
  }
6022
6046
  const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
6023
6047
  const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
6048
+ const boundaryTimeout = (incidents ?? []).some(
6049
+ (ev) => incidentMatchesNode(ev, nodeId) && isBoundaryTimeoutError(ev)
6050
+ );
6024
6051
  return {
6025
6052
  errorsEmittedHere,
6026
6053
  errorsFromCallers,
@@ -6028,17 +6055,24 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6028
6055
  outboundVolume,
6029
6056
  ...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
6030
6057
  ...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
6031
- stale
6058
+ stale,
6059
+ boundaryTimeout,
6060
+ observedErroringDownstream,
6061
+ hasOutboundDeps
6032
6062
  };
6033
6063
  }
6034
6064
  function isSaturated(ctx) {
6035
6065
  return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
6036
6066
  }
6067
+ function isBoundaryTimeoutSymptom(ctx) {
6068
+ return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
6069
+ }
6037
6070
  function classifyNode(ctx) {
6038
6071
  if (ctx.errorsEmittedHere > 0) {
6039
6072
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
6040
6073
  return "symptom-only";
6041
6074
  }
6075
+ if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
6042
6076
  return "primary-failure";
6043
6077
  }
6044
6078
  if (ctx.errorsFromCallers > 0) return "symptom-only";
@@ -6243,6 +6277,57 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
6243
6277
  if (!navigation) return tagged.result;
6244
6278
  return enrichWithNavigation(graph, errorNodeId, tagged, incidents, opts?.now ?? Date.now());
6245
6279
  }
6280
+ function boundaryIncidentRoute(nodeId, incidents) {
6281
+ for (const ev of incidents ?? []) {
6282
+ if (!incidentMatchesNode(ev, nodeId)) continue;
6283
+ const r = ev.attributes?.["http.route"] ?? ev.attributes?.["http.target"] ?? ev.attributes?.["url.path"];
6284
+ if (typeof r === "string" && r.length > 0) return r;
6285
+ }
6286
+ return void 0;
6287
+ }
6288
+ function declaredOutboundCallees(graph, nodeId) {
6289
+ const byTarget = /* @__PURE__ */ new Map();
6290
+ for (const n of nodeScope(graph, nodeId)) {
6291
+ if (!graph.hasNode(n)) continue;
6292
+ for (const edgeId of graph.outboundEdges(n)) {
6293
+ const e = graph.getEdgeAttributes(edgeId);
6294
+ if (!DEP_EDGE_TYPES.has(e.type)) continue;
6295
+ if (e.provenance !== Provenance6.EXTRACTED) continue;
6296
+ if (e.target === nodeId || byTarget.has(e.target)) continue;
6297
+ const route = e.evidence?.pathTemplate;
6298
+ byTarget.set(
6299
+ e.target,
6300
+ route !== void 0 ? { target: e.target, route } : { target: e.target }
6301
+ );
6302
+ }
6303
+ }
6304
+ return [...byTarget.values()];
6305
+ }
6306
+ function normalizeRoute(s) {
6307
+ return s.replace(/\/+$/, "").toLowerCase();
6308
+ }
6309
+ function routeMatches(declared, incident) {
6310
+ const a = normalizeRoute(declared);
6311
+ const b = normalizeRoute(incident);
6312
+ return a === b || a.startsWith(b) || b.startsWith(a);
6313
+ }
6314
+ function structuralUpstreamPointer(graph, boundaryNode, incidents) {
6315
+ const route = boundaryIncidentRoute(boundaryNode, incidents);
6316
+ const callees = declaredOutboundCallees(graph, boundaryNode);
6317
+ if (callees.length === 0) return null;
6318
+ const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
6319
+ const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
6320
+ if (!chosen) return null;
6321
+ let node = chosen.target;
6322
+ const seen = /* @__PURE__ */ new Set([boundaryNode, node]);
6323
+ for (let depth = 0; depth < ROOT_CAUSE_MAX_DEPTH; depth++) {
6324
+ const next = declaredOutboundCallees(graph, node);
6325
+ if (next.length !== 1 || seen.has(next[0].target)) break;
6326
+ node = next[0].target;
6327
+ seen.add(node);
6328
+ }
6329
+ return route !== void 0 ? { node, route } : { node };
6330
+ }
6246
6331
  function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
6247
6332
  const legacy = tagged.result;
6248
6333
  const seedNode = legacy.rootCauseNode;
@@ -6276,6 +6361,29 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
6276
6361
  confidence: Math.min(legacy.confidence, 0.4),
6277
6362
  ...lastProv ? { provenance: lastProv } : {}
6278
6363
  });
6364
+ } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
6365
+ const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
6366
+ const seedName = displayNameOf(seedNode);
6367
+ const routeNote = pointer?.route ? ` serving ${pointer.route}` : "";
6368
+ if (pointer) {
6369
+ const causeName = displayNameOf(pointer.node);
6370
+ candidates.push({
6371
+ node: pointer.node,
6372
+ classification: "primary-failure",
6373
+ reason: `${causeName} is the likely root cause (structural, INFERRED \u2014 not observed): the boundary ${seedName} only timed out waiting, and the actual culprit hung without exporting a span, so this is walked from the declared call graph${routeNote}, not from runtime signal. Restore instrumentation / inspect ${causeName} to confirm.`,
6374
+ context: nodeContext(graph, pointer.node, incidents, now),
6375
+ confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
6376
+ provenance: Provenance6.INFERRED
6377
+ });
6378
+ }
6379
+ candidates.push({
6380
+ node: seedNode,
6381
+ classification: "symptom-only",
6382
+ reason: pointer ? `${seedName} timed out waiting on a downstream that hung and exported nothing \u2014 a boundary reporting an upstream fault, not the fault itself.` : `${seedName} timed out but nothing it can see downstream is erroring \u2014 the cause is downstream and unobserved (the culprit hung and exported no span). No single declared callee${routeNote} resolves, so no confident cause is named; inspect ${seedName}'s declared downstream.`,
6383
+ context: seedCtx,
6384
+ confidence: Math.min(legacy.confidence, 0.3),
6385
+ ...lastProv ? { provenance: lastProv } : {}
6386
+ });
6279
6387
  } else if (staleChain) {
6280
6388
  const culprit = staleChain.culprit;
6281
6389
  const culpritName = displayNameOf(culprit);
@@ -22510,4 +22618,4 @@ export {
22510
22618
  deprovisionConnector,
22511
22619
  buildApi
22512
22620
  };
22513
- //# sourceMappingURL=chunk-B6KJG5KN.js.map
22621
+ //# sourceMappingURL=chunk-C3LHXC55.js.map