@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/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  resolveHost,
7
7
  resolveNeatVersion,
8
8
  writeDaemonRecord
9
- } from "./chunk-QETJIA2Q.js";
9
+ } from "./chunk-UNO3X6ZV.js";
10
10
  import {
11
11
  buildSearchIndex
12
12
  } from "./chunk-BC53SCT7.js";
@@ -75,7 +75,7 @@ import {
75
75
  startStalenessLoop,
76
76
  upsertConnectorEntry,
77
77
  validateConnectorEntry
78
- } from "./chunk-B6KJG5KN.js";
78
+ } from "./chunk-3UAUMPIY.js";
79
79
  import {
80
80
  startOtelGrpcReceiver
81
81
  } from "./chunk-ERE47MCR.js";
package/dist/index.cjs CHANGED
@@ -1865,6 +1865,24 @@ function incidentCountForNode(nodeId, incidents) {
1865
1865
  if (!incidents || incidents.length === 0) return 0;
1866
1866
  return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
1867
1867
  }
1868
+ var DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
1869
+ import_types.EdgeType.CALLS,
1870
+ import_types.EdgeType.CONNECTS_TO,
1871
+ import_types.EdgeType.PUBLISHES_TO,
1872
+ import_types.EdgeType.CONSUMES_FROM
1873
+ ]);
1874
+ var TIMEOUT_ERROR_PATTERNS = [
1875
+ /\bDEADLINE_EXCEEDED\b/i,
1876
+ /\bdeadline exceeded\b/i,
1877
+ /\bETIMEDOUT\b/i,
1878
+ /\btimed[\s-]?out\b/i,
1879
+ /\btimeout\b/i
1880
+ ];
1881
+ function isBoundaryTimeoutError(ev) {
1882
+ if (ev.httpStatusCode === 504) return true;
1883
+ const haystack = [ev.errorType, ev.exceptionType, ev.errorMessage].filter((s) => typeof s === "string").join(" \n ");
1884
+ return TIMEOUT_ERROR_PATTERNS.some((re) => re.test(haystack));
1885
+ }
1868
1886
  function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1869
1887
  const scope = nodeScope(graph, nodeId);
1870
1888
  let errorsFromCallers = 0;
@@ -1874,6 +1892,8 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1874
1892
  let latestInboundMs;
1875
1893
  let latencyP95Ms;
1876
1894
  let stale = false;
1895
+ let observedErroringDownstream = false;
1896
+ let hasOutboundDeps = false;
1877
1897
  for (const n of scope) {
1878
1898
  if (!graph.hasNode(n)) continue;
1879
1899
  for (const edgeId of graph.inboundEdges(n)) {
@@ -1895,10 +1915,17 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1895
1915
  outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
1896
1916
  if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
1897
1917
  if (e.provenance === import_types.Provenance.STALE) stale = true;
1918
+ if (DEP_EDGE_TYPES.has(e.type)) {
1919
+ hasOutboundDeps = true;
1920
+ if ((e.signal?.errorCount ?? 0) > 0) observedErroringDownstream = true;
1921
+ }
1898
1922
  }
1899
1923
  }
1900
1924
  const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
1901
1925
  const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
1926
+ const boundaryTimeout = (incidents ?? []).some(
1927
+ (ev) => incidentMatchesNode(ev, nodeId) && isBoundaryTimeoutError(ev)
1928
+ );
1902
1929
  return {
1903
1930
  errorsEmittedHere,
1904
1931
  errorsFromCallers,
@@ -1906,17 +1933,24 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1906
1933
  outboundVolume,
1907
1934
  ...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
1908
1935
  ...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
1909
- stale
1936
+ stale,
1937
+ boundaryTimeout,
1938
+ observedErroringDownstream,
1939
+ hasOutboundDeps
1910
1940
  };
1911
1941
  }
1912
1942
  function isSaturated(ctx) {
1913
1943
  return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
1914
1944
  }
1945
+ function isBoundaryTimeoutSymptom(ctx) {
1946
+ return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
1947
+ }
1915
1948
  function classifyNode(ctx) {
1916
1949
  if (ctx.errorsEmittedHere > 0) {
1917
1950
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
1918
1951
  return "symptom-only";
1919
1952
  }
1953
+ if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
1920
1954
  return "primary-failure";
1921
1955
  }
1922
1956
  if (ctx.errorsFromCallers > 0) return "symptom-only";
@@ -2121,6 +2155,57 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
2121
2155
  if (!navigation) return tagged.result;
2122
2156
  return enrichWithNavigation(graph, errorNodeId, tagged, incidents, opts?.now ?? Date.now());
2123
2157
  }
2158
+ function boundaryIncidentRoute(nodeId, incidents) {
2159
+ for (const ev of incidents ?? []) {
2160
+ if (!incidentMatchesNode(ev, nodeId)) continue;
2161
+ const r = ev.attributes?.["http.route"] ?? ev.attributes?.["http.target"] ?? ev.attributes?.["url.path"];
2162
+ if (typeof r === "string" && r.length > 0) return r;
2163
+ }
2164
+ return void 0;
2165
+ }
2166
+ function declaredOutboundCallees(graph, nodeId) {
2167
+ const byTarget = /* @__PURE__ */ new Map();
2168
+ for (const n of nodeScope(graph, nodeId)) {
2169
+ if (!graph.hasNode(n)) continue;
2170
+ for (const edgeId of graph.outboundEdges(n)) {
2171
+ const e = graph.getEdgeAttributes(edgeId);
2172
+ if (!DEP_EDGE_TYPES.has(e.type)) continue;
2173
+ if (e.provenance !== import_types.Provenance.EXTRACTED) continue;
2174
+ if (e.target === nodeId || byTarget.has(e.target)) continue;
2175
+ const route = e.evidence?.pathTemplate;
2176
+ byTarget.set(
2177
+ e.target,
2178
+ route !== void 0 ? { target: e.target, route } : { target: e.target }
2179
+ );
2180
+ }
2181
+ }
2182
+ return [...byTarget.values()];
2183
+ }
2184
+ function normalizeRoute(s) {
2185
+ return s.replace(/\/+$/, "").toLowerCase();
2186
+ }
2187
+ function routeMatches(declared, incident) {
2188
+ const a = normalizeRoute(declared);
2189
+ const b = normalizeRoute(incident);
2190
+ return a === b || a.startsWith(b) || b.startsWith(a);
2191
+ }
2192
+ function structuralUpstreamPointer(graph, boundaryNode, incidents) {
2193
+ const route = boundaryIncidentRoute(boundaryNode, incidents);
2194
+ const callees = declaredOutboundCallees(graph, boundaryNode);
2195
+ if (callees.length === 0) return null;
2196
+ const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
2197
+ const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
2198
+ if (!chosen) return null;
2199
+ let node = chosen.target;
2200
+ const seen = /* @__PURE__ */ new Set([boundaryNode, node]);
2201
+ for (let depth = 0; depth < ROOT_CAUSE_MAX_DEPTH; depth++) {
2202
+ const next = declaredOutboundCallees(graph, node);
2203
+ if (next.length !== 1 || seen.has(next[0].target)) break;
2204
+ node = next[0].target;
2205
+ seen.add(node);
2206
+ }
2207
+ return route !== void 0 ? { node, route } : { node };
2208
+ }
2124
2209
  function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2125
2210
  const legacy = tagged.result;
2126
2211
  const seedNode = legacy.rootCauseNode;
@@ -2154,6 +2239,29 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2154
2239
  confidence: Math.min(legacy.confidence, 0.4),
2155
2240
  ...lastProv ? { provenance: lastProv } : {}
2156
2241
  });
2242
+ } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
2243
+ const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
2244
+ const seedName = displayNameOf(seedNode);
2245
+ const routeNote = pointer?.route ? ` serving ${pointer.route}` : "";
2246
+ if (pointer) {
2247
+ const causeName = displayNameOf(pointer.node);
2248
+ candidates.push({
2249
+ node: pointer.node,
2250
+ classification: "primary-failure",
2251
+ 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.`,
2252
+ context: nodeContext(graph, pointer.node, incidents, now),
2253
+ confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
2254
+ provenance: import_types.Provenance.INFERRED
2255
+ });
2256
+ }
2257
+ candidates.push({
2258
+ node: seedNode,
2259
+ classification: "symptom-only",
2260
+ 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.`,
2261
+ context: seedCtx,
2262
+ confidence: Math.min(legacy.confidence, 0.3),
2263
+ ...lastProv ? { provenance: lastProv } : {}
2264
+ });
2157
2265
  } else if (staleChain) {
2158
2266
  const culprit = staleChain.culprit;
2159
2267
  const culpritName = displayNameOf(culprit);
@@ -6459,6 +6567,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
6459
6567
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
6460
6568
  const locus = incidentLocus(span, graph, scanPath);
6461
6569
  const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6570
+ const httpStatus = httpResponseStatus(span);
6462
6571
  return {
6463
6572
  id: `${span.traceId}:${span.spanId}`,
6464
6573
  timestamp: ts,
@@ -6468,6 +6577,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
6468
6577
  errorMessage: incidentMessage(span),
6469
6578
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
6470
6579
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6580
+ ...httpStatus !== void 0 ? { httpStatusCode: httpStatus } : {},
6471
6581
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6472
6582
  affectedNode: locus.affectedNode
6473
6583
  };
@@ -6819,26 +6929,8 @@ async function handleSpan(ctx, span) {
6819
6929
  if (span.statusCode === 2) {
6820
6930
  stitchTrace(ctx.graph, sourceId, ts);
6821
6931
  if (ctx.writeErrorEventInline !== false) {
6822
- const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
6823
- const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6824
- const ev = {
6825
- id: `${span.traceId}:${span.spanId}`,
6826
- timestamp: ts,
6827
- service: span.service,
6828
- traceId: span.traceId,
6829
- spanId: span.spanId,
6830
- errorMessage: incidentMessage(span),
6831
- ...span.exception?.type ? { exceptionType: span.exception.type } : {},
6832
- ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6833
- ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6834
- // Attribute to where the failure originated — the symbol / file / service
6835
- // the throwing span named (incidentAffectedNode / ADR-191, extended to
6836
- // recover the locus from the stacktrace when the span stamped no code.*
6837
- // attrs, ADR-216) — the same source-based attribution the durable
6838
- // receiver write uses, not the outbound edge target this span minted.
6839
- affectedNode: locus.affectedNode
6840
- };
6841
- await appendErrorEvent(ctx, ev);
6932
+ const ev = buildErrorEventForReceiver(span, ctx.graph, ctx.scanPath);
6933
+ if (ev) await appendErrorEvent(ctx, ev);
6842
6934
  }
6843
6935
  }
6844
6936
  if (span.statusCode !== 2) {