@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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-QETJIA2Q.js";
4
+ } from "./chunk-UHNE7LGF.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-B6KJG5KN.js";
40
+ } from "./chunk-C3LHXC55.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-ERE47MCR.js";
package/dist/neatd.cjs CHANGED
@@ -1828,6 +1828,24 @@ function incidentCountForNode(nodeId, incidents) {
1828
1828
  if (!incidents || incidents.length === 0) return 0;
1829
1829
  return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
1830
1830
  }
1831
+ var DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
1832
+ import_types.EdgeType.CALLS,
1833
+ import_types.EdgeType.CONNECTS_TO,
1834
+ import_types.EdgeType.PUBLISHES_TO,
1835
+ import_types.EdgeType.CONSUMES_FROM
1836
+ ]);
1837
+ var TIMEOUT_ERROR_PATTERNS = [
1838
+ /\bDEADLINE_EXCEEDED\b/i,
1839
+ /\bdeadline exceeded\b/i,
1840
+ /\bETIMEDOUT\b/i,
1841
+ /\btimed[\s-]?out\b/i,
1842
+ /\btimeout\b/i
1843
+ ];
1844
+ function isBoundaryTimeoutError(ev) {
1845
+ if (ev.httpStatusCode === 504) return true;
1846
+ const haystack = [ev.errorType, ev.exceptionType, ev.errorMessage].filter((s) => typeof s === "string").join(" \n ");
1847
+ return TIMEOUT_ERROR_PATTERNS.some((re) => re.test(haystack));
1848
+ }
1831
1849
  function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1832
1850
  const scope = nodeScope(graph, nodeId);
1833
1851
  let errorsFromCallers = 0;
@@ -1837,6 +1855,8 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1837
1855
  let latestInboundMs;
1838
1856
  let latencyP95Ms;
1839
1857
  let stale = false;
1858
+ let observedErroringDownstream = false;
1859
+ let hasOutboundDeps = false;
1840
1860
  for (const n of scope) {
1841
1861
  if (!graph.hasNode(n)) continue;
1842
1862
  for (const edgeId of graph.inboundEdges(n)) {
@@ -1858,10 +1878,17 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1858
1878
  outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
1859
1879
  if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
1860
1880
  if (e.provenance === import_types.Provenance.STALE) stale = true;
1881
+ if (DEP_EDGE_TYPES.has(e.type)) {
1882
+ hasOutboundDeps = true;
1883
+ if ((e.signal?.errorCount ?? 0) > 0) observedErroringDownstream = true;
1884
+ }
1861
1885
  }
1862
1886
  }
1863
1887
  const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
1864
1888
  const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
1889
+ const boundaryTimeout = (incidents ?? []).some(
1890
+ (ev) => incidentMatchesNode(ev, nodeId) && isBoundaryTimeoutError(ev)
1891
+ );
1865
1892
  return {
1866
1893
  errorsEmittedHere,
1867
1894
  errorsFromCallers,
@@ -1869,17 +1896,24 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1869
1896
  outboundVolume,
1870
1897
  ...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
1871
1898
  ...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
1872
- stale
1899
+ stale,
1900
+ boundaryTimeout,
1901
+ observedErroringDownstream,
1902
+ hasOutboundDeps
1873
1903
  };
1874
1904
  }
1875
1905
  function isSaturated(ctx) {
1876
1906
  return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
1877
1907
  }
1908
+ function isBoundaryTimeoutSymptom(ctx) {
1909
+ return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
1910
+ }
1878
1911
  function classifyNode(ctx) {
1879
1912
  if (ctx.errorsEmittedHere > 0) {
1880
1913
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
1881
1914
  return "symptom-only";
1882
1915
  }
1916
+ if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
1883
1917
  return "primary-failure";
1884
1918
  }
1885
1919
  if (ctx.errorsFromCallers > 0) return "symptom-only";
@@ -2084,6 +2118,57 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
2084
2118
  if (!navigation) return tagged.result;
2085
2119
  return enrichWithNavigation(graph, errorNodeId, tagged, incidents, opts?.now ?? Date.now());
2086
2120
  }
2121
+ function boundaryIncidentRoute(nodeId, incidents) {
2122
+ for (const ev of incidents ?? []) {
2123
+ if (!incidentMatchesNode(ev, nodeId)) continue;
2124
+ const r = ev.attributes?.["http.route"] ?? ev.attributes?.["http.target"] ?? ev.attributes?.["url.path"];
2125
+ if (typeof r === "string" && r.length > 0) return r;
2126
+ }
2127
+ return void 0;
2128
+ }
2129
+ function declaredOutboundCallees(graph, nodeId) {
2130
+ const byTarget = /* @__PURE__ */ new Map();
2131
+ for (const n of nodeScope(graph, nodeId)) {
2132
+ if (!graph.hasNode(n)) continue;
2133
+ for (const edgeId of graph.outboundEdges(n)) {
2134
+ const e = graph.getEdgeAttributes(edgeId);
2135
+ if (!DEP_EDGE_TYPES.has(e.type)) continue;
2136
+ if (e.provenance !== import_types.Provenance.EXTRACTED) continue;
2137
+ if (e.target === nodeId || byTarget.has(e.target)) continue;
2138
+ const route = e.evidence?.pathTemplate;
2139
+ byTarget.set(
2140
+ e.target,
2141
+ route !== void 0 ? { target: e.target, route } : { target: e.target }
2142
+ );
2143
+ }
2144
+ }
2145
+ return [...byTarget.values()];
2146
+ }
2147
+ function normalizeRoute(s) {
2148
+ return s.replace(/\/+$/, "").toLowerCase();
2149
+ }
2150
+ function routeMatches(declared, incident) {
2151
+ const a = normalizeRoute(declared);
2152
+ const b = normalizeRoute(incident);
2153
+ return a === b || a.startsWith(b) || b.startsWith(a);
2154
+ }
2155
+ function structuralUpstreamPointer(graph, boundaryNode, incidents) {
2156
+ const route = boundaryIncidentRoute(boundaryNode, incidents);
2157
+ const callees = declaredOutboundCallees(graph, boundaryNode);
2158
+ if (callees.length === 0) return null;
2159
+ const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
2160
+ const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
2161
+ if (!chosen) return null;
2162
+ let node = chosen.target;
2163
+ const seen = /* @__PURE__ */ new Set([boundaryNode, node]);
2164
+ for (let depth = 0; depth < ROOT_CAUSE_MAX_DEPTH; depth++) {
2165
+ const next = declaredOutboundCallees(graph, node);
2166
+ if (next.length !== 1 || seen.has(next[0].target)) break;
2167
+ node = next[0].target;
2168
+ seen.add(node);
2169
+ }
2170
+ return route !== void 0 ? { node, route } : { node };
2171
+ }
2087
2172
  function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2088
2173
  const legacy = tagged.result;
2089
2174
  const seedNode = legacy.rootCauseNode;
@@ -2117,6 +2202,29 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2117
2202
  confidence: Math.min(legacy.confidence, 0.4),
2118
2203
  ...lastProv ? { provenance: lastProv } : {}
2119
2204
  });
2205
+ } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
2206
+ const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
2207
+ const seedName = displayNameOf(seedNode);
2208
+ const routeNote = pointer?.route ? ` serving ${pointer.route}` : "";
2209
+ if (pointer) {
2210
+ const causeName = displayNameOf(pointer.node);
2211
+ candidates.push({
2212
+ node: pointer.node,
2213
+ classification: "primary-failure",
2214
+ 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.`,
2215
+ context: nodeContext(graph, pointer.node, incidents, now),
2216
+ confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
2217
+ provenance: import_types.Provenance.INFERRED
2218
+ });
2219
+ }
2220
+ candidates.push({
2221
+ node: seedNode,
2222
+ classification: "symptom-only",
2223
+ 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.`,
2224
+ context: seedCtx,
2225
+ confidence: Math.min(legacy.confidence, 0.3),
2226
+ ...lastProv ? { provenance: lastProv } : {}
2227
+ });
2120
2228
  } else if (staleChain) {
2121
2229
  const culprit = staleChain.culprit;
2122
2230
  const culpritName = displayNameOf(culprit);