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

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.
@@ -4694,6 +4694,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
4694
4694
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
4695
4695
  const locus = incidentLocus(span, graph, scanPath);
4696
4696
  const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
4697
+ const httpStatus = httpResponseStatus(span);
4697
4698
  return {
4698
4699
  id: `${span.traceId}:${span.spanId}`,
4699
4700
  timestamp: ts,
@@ -4703,6 +4704,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
4703
4704
  errorMessage: incidentMessage(span),
4704
4705
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
4705
4706
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
4707
+ ...httpStatus !== void 0 ? { httpStatusCode: httpStatus } : {},
4706
4708
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
4707
4709
  affectedNode: locus.affectedNode
4708
4710
  };
@@ -5054,26 +5056,8 @@ async function handleSpan(ctx, span) {
5054
5056
  if (span.statusCode === 2) {
5055
5057
  stitchTrace(ctx.graph, sourceId, ts);
5056
5058
  if (ctx.writeErrorEventInline !== false) {
5057
- const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
5058
- const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
5059
- const ev = {
5060
- id: `${span.traceId}:${span.spanId}`,
5061
- timestamp: ts,
5062
- service: span.service,
5063
- traceId: span.traceId,
5064
- spanId: span.spanId,
5065
- errorMessage: incidentMessage(span),
5066
- ...span.exception?.type ? { exceptionType: span.exception.type } : {},
5067
- ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
5068
- ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
5069
- // Attribute to where the failure originated — the symbol / file / service
5070
- // the throwing span named (incidentAffectedNode / ADR-191, extended to
5071
- // recover the locus from the stacktrace when the span stamped no code.*
5072
- // attrs, ADR-216) — the same source-based attribution the durable
5073
- // receiver write uses, not the outbound edge target this span minted.
5074
- affectedNode: locus.affectedNode
5075
- };
5076
- await appendErrorEvent(ctx, ev);
5059
+ const ev = buildErrorEventForReceiver(span, ctx.graph, ctx.scanPath);
5060
+ if (ev) await appendErrorEvent(ctx, ev);
5077
5061
  }
5078
5062
  }
5079
5063
  if (span.statusCode !== 2) {
@@ -5987,6 +5971,24 @@ function incidentCountForNode(nodeId, incidents) {
5987
5971
  if (!incidents || incidents.length === 0) return 0;
5988
5972
  return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
5989
5973
  }
5974
+ var DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
5975
+ EdgeType6.CALLS,
5976
+ EdgeType6.CONNECTS_TO,
5977
+ EdgeType6.PUBLISHES_TO,
5978
+ EdgeType6.CONSUMES_FROM
5979
+ ]);
5980
+ var TIMEOUT_ERROR_PATTERNS = [
5981
+ /\bDEADLINE_EXCEEDED\b/i,
5982
+ /\bdeadline exceeded\b/i,
5983
+ /\bETIMEDOUT\b/i,
5984
+ /\btimed[\s-]?out\b/i,
5985
+ /\btimeout\b/i
5986
+ ];
5987
+ function isBoundaryTimeoutError(ev) {
5988
+ if (ev.httpStatusCode === 504) return true;
5989
+ const haystack = [ev.errorType, ev.exceptionType, ev.errorMessage].filter((s) => typeof s === "string").join(" \n ");
5990
+ return TIMEOUT_ERROR_PATTERNS.some((re) => re.test(haystack));
5991
+ }
5990
5992
  function nodeContext(graph, nodeId, incidents, now = Date.now()) {
5991
5993
  const scope = nodeScope(graph, nodeId);
5992
5994
  let errorsFromCallers = 0;
@@ -5996,6 +5998,8 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
5996
5998
  let latestInboundMs;
5997
5999
  let latencyP95Ms;
5998
6000
  let stale = false;
6001
+ let observedErroringDownstream = false;
6002
+ let hasOutboundDeps = false;
5999
6003
  for (const n of scope) {
6000
6004
  if (!graph.hasNode(n)) continue;
6001
6005
  for (const edgeId of graph.inboundEdges(n)) {
@@ -6017,10 +6021,17 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6017
6021
  outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
6018
6022
  if (e.type === EdgeType6.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
6019
6023
  if (e.provenance === Provenance6.STALE) stale = true;
6024
+ if (DEP_EDGE_TYPES.has(e.type)) {
6025
+ hasOutboundDeps = true;
6026
+ if ((e.signal?.errorCount ?? 0) > 0) observedErroringDownstream = true;
6027
+ }
6020
6028
  }
6021
6029
  }
6022
6030
  const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
6023
6031
  const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
6032
+ const boundaryTimeout = (incidents ?? []).some(
6033
+ (ev) => incidentMatchesNode(ev, nodeId) && isBoundaryTimeoutError(ev)
6034
+ );
6024
6035
  return {
6025
6036
  errorsEmittedHere,
6026
6037
  errorsFromCallers,
@@ -6028,17 +6039,24 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6028
6039
  outboundVolume,
6029
6040
  ...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
6030
6041
  ...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
6031
- stale
6042
+ stale,
6043
+ boundaryTimeout,
6044
+ observedErroringDownstream,
6045
+ hasOutboundDeps
6032
6046
  };
6033
6047
  }
6034
6048
  function isSaturated(ctx) {
6035
6049
  return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
6036
6050
  }
6051
+ function isBoundaryTimeoutSymptom(ctx) {
6052
+ return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
6053
+ }
6037
6054
  function classifyNode(ctx) {
6038
6055
  if (ctx.errorsEmittedHere > 0) {
6039
6056
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
6040
6057
  return "symptom-only";
6041
6058
  }
6059
+ if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
6042
6060
  return "primary-failure";
6043
6061
  }
6044
6062
  if (ctx.errorsFromCallers > 0) return "symptom-only";
@@ -6243,6 +6261,57 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
6243
6261
  if (!navigation) return tagged.result;
6244
6262
  return enrichWithNavigation(graph, errorNodeId, tagged, incidents, opts?.now ?? Date.now());
6245
6263
  }
6264
+ function boundaryIncidentRoute(nodeId, incidents) {
6265
+ for (const ev of incidents ?? []) {
6266
+ if (!incidentMatchesNode(ev, nodeId)) continue;
6267
+ const r = ev.attributes?.["http.route"] ?? ev.attributes?.["http.target"] ?? ev.attributes?.["url.path"];
6268
+ if (typeof r === "string" && r.length > 0) return r;
6269
+ }
6270
+ return void 0;
6271
+ }
6272
+ function declaredOutboundCallees(graph, nodeId) {
6273
+ const byTarget = /* @__PURE__ */ new Map();
6274
+ for (const n of nodeScope(graph, nodeId)) {
6275
+ if (!graph.hasNode(n)) continue;
6276
+ for (const edgeId of graph.outboundEdges(n)) {
6277
+ const e = graph.getEdgeAttributes(edgeId);
6278
+ if (!DEP_EDGE_TYPES.has(e.type)) continue;
6279
+ if (e.provenance !== Provenance6.EXTRACTED) continue;
6280
+ if (e.target === nodeId || byTarget.has(e.target)) continue;
6281
+ const route = e.evidence?.pathTemplate;
6282
+ byTarget.set(
6283
+ e.target,
6284
+ route !== void 0 ? { target: e.target, route } : { target: e.target }
6285
+ );
6286
+ }
6287
+ }
6288
+ return [...byTarget.values()];
6289
+ }
6290
+ function normalizeRoute(s) {
6291
+ return s.replace(/\/+$/, "").toLowerCase();
6292
+ }
6293
+ function routeMatches(declared, incident) {
6294
+ const a = normalizeRoute(declared);
6295
+ const b = normalizeRoute(incident);
6296
+ return a === b || a.startsWith(b) || b.startsWith(a);
6297
+ }
6298
+ function structuralUpstreamPointer(graph, boundaryNode, incidents) {
6299
+ const route = boundaryIncidentRoute(boundaryNode, incidents);
6300
+ const callees = declaredOutboundCallees(graph, boundaryNode);
6301
+ if (callees.length === 0) return null;
6302
+ const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
6303
+ const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
6304
+ if (!chosen) return null;
6305
+ let node = chosen.target;
6306
+ const seen = /* @__PURE__ */ new Set([boundaryNode, node]);
6307
+ for (let depth = 0; depth < ROOT_CAUSE_MAX_DEPTH; depth++) {
6308
+ const next = declaredOutboundCallees(graph, node);
6309
+ if (next.length !== 1 || seen.has(next[0].target)) break;
6310
+ node = next[0].target;
6311
+ seen.add(node);
6312
+ }
6313
+ return route !== void 0 ? { node, route } : { node };
6314
+ }
6246
6315
  function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
6247
6316
  const legacy = tagged.result;
6248
6317
  const seedNode = legacy.rootCauseNode;
@@ -6276,6 +6345,29 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
6276
6345
  confidence: Math.min(legacy.confidence, 0.4),
6277
6346
  ...lastProv ? { provenance: lastProv } : {}
6278
6347
  });
6348
+ } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
6349
+ const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
6350
+ const seedName = displayNameOf(seedNode);
6351
+ const routeNote = pointer?.route ? ` serving ${pointer.route}` : "";
6352
+ if (pointer) {
6353
+ const causeName = displayNameOf(pointer.node);
6354
+ candidates.push({
6355
+ node: pointer.node,
6356
+ classification: "primary-failure",
6357
+ 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.`,
6358
+ context: nodeContext(graph, pointer.node, incidents, now),
6359
+ confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
6360
+ provenance: Provenance6.INFERRED
6361
+ });
6362
+ }
6363
+ candidates.push({
6364
+ node: seedNode,
6365
+ classification: "symptom-only",
6366
+ 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.`,
6367
+ context: seedCtx,
6368
+ confidence: Math.min(legacy.confidence, 0.3),
6369
+ ...lastProv ? { provenance: lastProv } : {}
6370
+ });
6279
6371
  } else if (staleChain) {
6280
6372
  const culprit = staleChain.culprit;
6281
6373
  const culpritName = displayNameOf(culprit);
@@ -22510,4 +22602,4 @@ export {
22510
22602
  deprovisionConnector,
22511
22603
  buildApi
22512
22604
  };
22513
- //# sourceMappingURL=chunk-B6KJG5KN.js.map
22605
+ //# sourceMappingURL=chunk-3UAUMPIY.js.map