@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.
package/dist/neatd.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  reconcileDaemonRecordSync,
4
4
  startDaemon
5
- } from "./chunk-QETJIA2Q.js";
5
+ } from "./chunk-UNO3X6ZV.js";
6
6
  import {
7
7
  listProjects,
8
8
  registryPath
9
- } from "./chunk-B6KJG5KN.js";
9
+ } from "./chunk-3UAUMPIY.js";
10
10
  import {
11
11
  BindAuthorityError,
12
12
  __require
package/dist/server.cjs CHANGED
@@ -5704,6 +5704,26 @@ function sanitizeAttributes(attrs) {
5704
5704
  }
5705
5705
  return out;
5706
5706
  }
5707
+ function buildErrorEventForReceiver(span, graph, scanPath) {
5708
+ if (span.statusCode !== 2) return null;
5709
+ const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
5710
+ const locus = incidentLocus(span, graph, scanPath);
5711
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
5712
+ const httpStatus = httpResponseStatus(span);
5713
+ return {
5714
+ id: `${span.traceId}:${span.spanId}`,
5715
+ timestamp: ts,
5716
+ service: span.service,
5717
+ traceId: span.traceId,
5718
+ spanId: span.spanId,
5719
+ errorMessage: incidentMessage(span),
5720
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
5721
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
5722
+ ...httpStatus !== void 0 ? { httpStatusCode: httpStatus } : {},
5723
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
5724
+ affectedNode: locus.affectedNode
5725
+ };
5726
+ }
5707
5727
  async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
5708
5728
  const attrs = sanitizeAttributes(span.attributes);
5709
5729
  const first = firstTimestamp ?? timestamp;
@@ -6042,26 +6062,8 @@ async function handleSpan(ctx, span) {
6042
6062
  if (span.statusCode === 2) {
6043
6063
  stitchTrace(ctx.graph, sourceId, ts);
6044
6064
  if (ctx.writeErrorEventInline !== false) {
6045
- const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
6046
- const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6047
- const ev = {
6048
- id: `${span.traceId}:${span.spanId}`,
6049
- timestamp: ts,
6050
- service: span.service,
6051
- traceId: span.traceId,
6052
- spanId: span.spanId,
6053
- errorMessage: incidentMessage(span),
6054
- ...span.exception?.type ? { exceptionType: span.exception.type } : {},
6055
- ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6056
- ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6057
- // Attribute to where the failure originated — the symbol / file / service
6058
- // the throwing span named (incidentAffectedNode / ADR-191, extended to
6059
- // recover the locus from the stacktrace when the span stamped no code.*
6060
- // attrs, ADR-216) — the same source-based attribution the durable
6061
- // receiver write uses, not the outbound edge target this span minted.
6062
- affectedNode: locus.affectedNode
6063
- };
6064
- await appendErrorEvent(ctx, ev);
6065
+ const ev = buildErrorEventForReceiver(span, ctx.graph, ctx.scanPath);
6066
+ if (ev) await appendErrorEvent(ctx, ev);
6065
6067
  }
6066
6068
  }
6067
6069
  if (span.statusCode !== 2) {
@@ -6962,6 +6964,24 @@ function incidentCountForNode(nodeId, incidents) {
6962
6964
  if (!incidents || incidents.length === 0) return 0;
6963
6965
  return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
6964
6966
  }
6967
+ var DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
6968
+ import_types8.EdgeType.CALLS,
6969
+ import_types8.EdgeType.CONNECTS_TO,
6970
+ import_types8.EdgeType.PUBLISHES_TO,
6971
+ import_types8.EdgeType.CONSUMES_FROM
6972
+ ]);
6973
+ var TIMEOUT_ERROR_PATTERNS = [
6974
+ /\bDEADLINE_EXCEEDED\b/i,
6975
+ /\bdeadline exceeded\b/i,
6976
+ /\bETIMEDOUT\b/i,
6977
+ /\btimed[\s-]?out\b/i,
6978
+ /\btimeout\b/i
6979
+ ];
6980
+ function isBoundaryTimeoutError(ev) {
6981
+ if (ev.httpStatusCode === 504) return true;
6982
+ const haystack = [ev.errorType, ev.exceptionType, ev.errorMessage].filter((s) => typeof s === "string").join(" \n ");
6983
+ return TIMEOUT_ERROR_PATTERNS.some((re) => re.test(haystack));
6984
+ }
6965
6985
  function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6966
6986
  const scope = nodeScope(graph, nodeId);
6967
6987
  let errorsFromCallers = 0;
@@ -6971,6 +6991,8 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6971
6991
  let latestInboundMs;
6972
6992
  let latencyP95Ms;
6973
6993
  let stale = false;
6994
+ let observedErroringDownstream = false;
6995
+ let hasOutboundDeps = false;
6974
6996
  for (const n of scope) {
6975
6997
  if (!graph.hasNode(n)) continue;
6976
6998
  for (const edgeId of graph.inboundEdges(n)) {
@@ -6992,10 +7014,17 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6992
7014
  outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
6993
7015
  if (e.type === import_types8.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
6994
7016
  if (e.provenance === import_types8.Provenance.STALE) stale = true;
7017
+ if (DEP_EDGE_TYPES.has(e.type)) {
7018
+ hasOutboundDeps = true;
7019
+ if ((e.signal?.errorCount ?? 0) > 0) observedErroringDownstream = true;
7020
+ }
6995
7021
  }
6996
7022
  }
6997
7023
  const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
6998
7024
  const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
7025
+ const boundaryTimeout = (incidents ?? []).some(
7026
+ (ev) => incidentMatchesNode(ev, nodeId) && isBoundaryTimeoutError(ev)
7027
+ );
6999
7028
  return {
7000
7029
  errorsEmittedHere,
7001
7030
  errorsFromCallers,
@@ -7003,17 +7032,24 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
7003
7032
  outboundVolume,
7004
7033
  ...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
7005
7034
  ...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
7006
- stale
7035
+ stale,
7036
+ boundaryTimeout,
7037
+ observedErroringDownstream,
7038
+ hasOutboundDeps
7007
7039
  };
7008
7040
  }
7009
7041
  function isSaturated(ctx) {
7010
7042
  return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
7011
7043
  }
7044
+ function isBoundaryTimeoutSymptom(ctx) {
7045
+ return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
7046
+ }
7012
7047
  function classifyNode(ctx) {
7013
7048
  if (ctx.errorsEmittedHere > 0) {
7014
7049
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
7015
7050
  return "symptom-only";
7016
7051
  }
7052
+ if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
7017
7053
  return "primary-failure";
7018
7054
  }
7019
7055
  if (ctx.errorsFromCallers > 0) return "symptom-only";
@@ -7218,6 +7254,57 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
7218
7254
  if (!navigation) return tagged.result;
7219
7255
  return enrichWithNavigation(graph, errorNodeId, tagged, incidents, opts?.now ?? Date.now());
7220
7256
  }
7257
+ function boundaryIncidentRoute(nodeId, incidents) {
7258
+ for (const ev of incidents ?? []) {
7259
+ if (!incidentMatchesNode(ev, nodeId)) continue;
7260
+ const r = ev.attributes?.["http.route"] ?? ev.attributes?.["http.target"] ?? ev.attributes?.["url.path"];
7261
+ if (typeof r === "string" && r.length > 0) return r;
7262
+ }
7263
+ return void 0;
7264
+ }
7265
+ function declaredOutboundCallees(graph, nodeId) {
7266
+ const byTarget = /* @__PURE__ */ new Map();
7267
+ for (const n of nodeScope(graph, nodeId)) {
7268
+ if (!graph.hasNode(n)) continue;
7269
+ for (const edgeId of graph.outboundEdges(n)) {
7270
+ const e = graph.getEdgeAttributes(edgeId);
7271
+ if (!DEP_EDGE_TYPES.has(e.type)) continue;
7272
+ if (e.provenance !== import_types8.Provenance.EXTRACTED) continue;
7273
+ if (e.target === nodeId || byTarget.has(e.target)) continue;
7274
+ const route = e.evidence?.pathTemplate;
7275
+ byTarget.set(
7276
+ e.target,
7277
+ route !== void 0 ? { target: e.target, route } : { target: e.target }
7278
+ );
7279
+ }
7280
+ }
7281
+ return [...byTarget.values()];
7282
+ }
7283
+ function normalizeRoute(s) {
7284
+ return s.replace(/\/+$/, "").toLowerCase();
7285
+ }
7286
+ function routeMatches(declared, incident) {
7287
+ const a = normalizeRoute(declared);
7288
+ const b = normalizeRoute(incident);
7289
+ return a === b || a.startsWith(b) || b.startsWith(a);
7290
+ }
7291
+ function structuralUpstreamPointer(graph, boundaryNode, incidents) {
7292
+ const route = boundaryIncidentRoute(boundaryNode, incidents);
7293
+ const callees = declaredOutboundCallees(graph, boundaryNode);
7294
+ if (callees.length === 0) return null;
7295
+ const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
7296
+ const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
7297
+ if (!chosen) return null;
7298
+ let node = chosen.target;
7299
+ const seen = /* @__PURE__ */ new Set([boundaryNode, node]);
7300
+ for (let depth = 0; depth < ROOT_CAUSE_MAX_DEPTH; depth++) {
7301
+ const next = declaredOutboundCallees(graph, node);
7302
+ if (next.length !== 1 || seen.has(next[0].target)) break;
7303
+ node = next[0].target;
7304
+ seen.add(node);
7305
+ }
7306
+ return route !== void 0 ? { node, route } : { node };
7307
+ }
7221
7308
  function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
7222
7309
  const legacy = tagged.result;
7223
7310
  const seedNode = legacy.rootCauseNode;
@@ -7251,6 +7338,29 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
7251
7338
  confidence: Math.min(legacy.confidence, 0.4),
7252
7339
  ...lastProv ? { provenance: lastProv } : {}
7253
7340
  });
7341
+ } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
7342
+ const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
7343
+ const seedName = displayNameOf(seedNode);
7344
+ const routeNote = pointer?.route ? ` serving ${pointer.route}` : "";
7345
+ if (pointer) {
7346
+ const causeName = displayNameOf(pointer.node);
7347
+ candidates.push({
7348
+ node: pointer.node,
7349
+ classification: "primary-failure",
7350
+ 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.`,
7351
+ context: nodeContext(graph, pointer.node, incidents, now),
7352
+ confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
7353
+ provenance: import_types8.Provenance.INFERRED
7354
+ });
7355
+ }
7356
+ candidates.push({
7357
+ node: seedNode,
7358
+ classification: "symptom-only",
7359
+ 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.`,
7360
+ context: seedCtx,
7361
+ confidence: Math.min(legacy.confidence, 0.3),
7362
+ ...lastProv ? { provenance: lastProv } : {}
7363
+ });
7254
7364
  } else if (staleChain) {
7255
7365
  const culprit = staleChain.culprit;
7256
7366
  const culpritName = displayNameOf(culprit);