@neat.is/core 0.9.3-dev.20260823 → 0.9.3

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.
@@ -20,7 +20,7 @@ import {
20
20
  startStalenessLoop,
21
21
  touchLastSeen,
22
22
  writeAtomically
23
- } from "./chunk-LRYPKC3B.js";
23
+ } from "./chunk-EDE4XP2M.js";
24
24
  import {
25
25
  assertBindAuthority,
26
26
  buildOtelReceiver,
@@ -891,4 +891,4 @@ export {
891
891
  resolveHost,
892
892
  startDaemon
893
893
  };
894
- //# sourceMappingURL=chunk-WJGZYEUG.js.map
894
+ //# sourceMappingURL=chunk-2D4Y5QHE.js.map
@@ -5125,15 +5125,36 @@ function startStalenessLoop(graph, options = {}) {
5125
5125
  clearInterval(interval);
5126
5126
  };
5127
5127
  }
5128
- async function readErrorEvents(errorsPath) {
5128
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
5129
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
5130
+ async function readErrorFileTail(errorsPath, maxBytes) {
5131
+ const handle = await fs7.open(errorsPath, "r");
5129
5132
  try {
5130
- const raw = await fs7.readFile(errorsPath, "utf8");
5131
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
5132
- return dedupeIncidents(events);
5133
+ const { size } = await handle.stat();
5134
+ if (size <= maxBytes) {
5135
+ return (await handle.readFile()).toString("utf8");
5136
+ }
5137
+ const buf = Buffer.alloc(maxBytes);
5138
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
5139
+ const raw = buf.toString("utf8");
5140
+ const firstNewline = raw.indexOf("\n");
5141
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
5142
+ } finally {
5143
+ await handle.close();
5144
+ }
5145
+ }
5146
+ async function readErrorEvents(errorsPath, opts) {
5147
+ let raw;
5148
+ try {
5149
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
5133
5150
  } catch (err) {
5134
5151
  if (err.code === "ENOENT") return [];
5135
5152
  throw err;
5136
5153
  }
5154
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
5155
+ const deduped = dedupeIncidents(events);
5156
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
5157
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
5137
5158
  }
5138
5159
  function isSynthesizedHttpIncident(ev) {
5139
5160
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -5906,6 +5927,47 @@ function classifyNode(ctx) {
5906
5927
  function isVictimSeed(ctx) {
5907
5928
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
5908
5929
  }
5930
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
5931
+ "name resolution",
5932
+ "resolve host",
5933
+ "getaddrinfo",
5934
+ "enotfound",
5935
+ "connection refused",
5936
+ "econnrefused",
5937
+ "connection reset",
5938
+ "econnreset",
5939
+ "able to connect",
5940
+ "failed to connect",
5941
+ "cannot connect",
5942
+ "could not connect",
5943
+ "unable to connect",
5944
+ "connection timed out",
5945
+ "etimedout",
5946
+ "no route to host",
5947
+ "host unreachable",
5948
+ "network is unreachable",
5949
+ "connection closed"
5950
+ ];
5951
+ function incidentTextIndicatesOutboundFailure(ev) {
5952
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
5953
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
5954
+ }
5955
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
5956
+ for (const n of nodeScope(graph, nodeId)) {
5957
+ if (!graph.hasNode(n)) continue;
5958
+ for (const edgeId of graph.outboundEdges(n)) {
5959
+ const e = graph.getEdgeAttributes(edgeId);
5960
+ if (e.type === EdgeType6.CONTAINS) continue;
5961
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
5962
+ }
5963
+ }
5964
+ if (seedSource === "incident" && incidents) {
5965
+ for (const ev of incidents) {
5966
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
5967
+ }
5968
+ }
5969
+ return false;
5970
+ }
5909
5971
  function grainOf(graph, nodeId) {
5910
5972
  if (!graph.hasNode(nodeId)) return "unknown";
5911
5973
  const t = graph.getNodeAttributes(nodeId).type;
@@ -6069,7 +6131,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
6069
6131
  const candidates = [];
6070
6132
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
6071
6133
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
6072
- if (seedCtx && isVictimSeed(seedCtx)) {
6134
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
6135
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
6073
6136
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
6074
6137
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
6075
6138
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -14939,6 +15002,7 @@ import {
14939
15002
  NodeType as NodeType30,
14940
15003
  parseEdgeId,
14941
15004
  parseFileId,
15005
+ parseSymbolId as parseSymbolId2,
14942
15006
  Provenance as Provenance24,
14943
15007
  serviceId as serviceId13
14944
15008
  } from "@neat.is/types";
@@ -15195,6 +15259,120 @@ function detectColumnDrift(node) {
15195
15259
  }
15196
15260
  return out;
15197
15261
  }
15262
+ var SYMBOL_MISMATCH_PATTERNS = [
15263
+ {
15264
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
15265
+ kind: "missing-attribute",
15266
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
15267
+ },
15268
+ {
15269
+ // "object has no field 'X'", "no such field X", "unknown field X".
15270
+ kind: "missing-field",
15271
+ patterns: [
15272
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
15273
+ ]
15274
+ },
15275
+ {
15276
+ // "has no property X", "no property named X".
15277
+ kind: "missing-property",
15278
+ patterns: [
15279
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
15280
+ ]
15281
+ },
15282
+ {
15283
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
15284
+ kind: "missing-column",
15285
+ patterns: [
15286
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
15287
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
15288
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
15289
+ ]
15290
+ },
15291
+ {
15292
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
15293
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
15294
+ // mismatch) does not get miscategorised here.
15295
+ kind: "undefined-method",
15296
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
15297
+ }
15298
+ ];
15299
+ function classifySymbolMismatch(message) {
15300
+ for (const entry of SYMBOL_MISMATCH_PATTERNS) {
15301
+ for (const re of entry.patterns) {
15302
+ const m = re.exec(message);
15303
+ if (m) {
15304
+ const captured = m[1];
15305
+ return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
15306
+ }
15307
+ }
15308
+ }
15309
+ return null;
15310
+ }
15311
+ function symbolLocus(graph, ev) {
15312
+ const attrs = ev.attributes ?? {};
15313
+ const filepath = codeFilepathOf(attrs);
15314
+ const lineno = codeLinenoOf(attrs);
15315
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
15316
+ const affected = ev.affectedNode;
15317
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
15318
+ const affectedIsCode = affectedInGraph && (parseSymbolId2(affected) !== null || parseFileId(affected) !== null);
15319
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
15320
+ if (!location) return null;
15321
+ if (affectedInGraph) return { node: affected, location };
15322
+ const svc = serviceId13(ev.service);
15323
+ if (graph.hasNode(svc)) return { node: svc, location };
15324
+ return null;
15325
+ }
15326
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
15327
+ function detectSymbolMismatches(graph, incidents) {
15328
+ const groups = /* @__PURE__ */ new Map();
15329
+ for (const ev of incidents) {
15330
+ const classified = classifySymbolMismatch(ev.errorMessage);
15331
+ if (!classified) continue;
15332
+ const locus = symbolLocus(graph, ev);
15333
+ if (!locus) continue;
15334
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
15335
+ const existing = groups.get(key);
15336
+ if (!existing) {
15337
+ groups.set(key, {
15338
+ node: locus.node,
15339
+ kind: classified.kind,
15340
+ ...classified.symbol ? { symbol: classified.symbol } : {},
15341
+ ...locus.location ? { location: locus.location } : {},
15342
+ latest: ev,
15343
+ count: 1
15344
+ });
15345
+ } else {
15346
+ existing.count += 1;
15347
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
15348
+ existing.latest = ev;
15349
+ if (locus.location) existing.location = locus.location;
15350
+ }
15351
+ }
15352
+ }
15353
+ const out = [];
15354
+ for (const g of groups.values()) {
15355
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
15356
+ const where = g.location ? ` at ${g.location}` : "";
15357
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
15358
+ out.push({
15359
+ type: "observed-symbol-mismatch",
15360
+ source: g.node,
15361
+ target: g.node,
15362
+ mismatchKind: g.kind,
15363
+ ...g.symbol ? { symbol: g.symbol } : {},
15364
+ ...g.location ? { location: g.location } : {},
15365
+ provenance: Provenance24.INFERRED,
15366
+ incidentId: g.latest.id,
15367
+ errorMessage: g.latest.errorMessage,
15368
+ incidentCount: g.count,
15369
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
15370
+ reason: `Code${where} declares access to ${member} the runtime object does not have \u2014 ${g.latest.service} raised "${g.latest.errorMessage}"${times}. The declared access (EXTRACTED) and the runtime shape (OBSERVED) disagree at symbol grain.`,
15371
+ recommendation: "Reconcile the declared access with the runtime shape: a field, attribute, method, or column was renamed, removed, or never existed on the object this code reaches. Update the code to the current shape, or restore the member."
15372
+ });
15373
+ }
15374
+ return out;
15375
+ }
15198
15376
  function involvesNode(d, nodeId) {
15199
15377
  return d.source === nodeId || d.target === nodeId;
15200
15378
  }
@@ -15280,6 +15458,9 @@ function computeDivergences(graph, opts = {}) {
15280
15458
  for (const d of detectColumnDrift(n)) all.push(d);
15281
15459
  }
15282
15460
  });
15461
+ if (opts.incidents && opts.incidents.length > 0) {
15462
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
15463
+ }
15283
15464
  const reconciled = suppressHostMismatchHalves(all);
15284
15465
  const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
15285
15466
  let filtered = dampened;
@@ -15300,7 +15481,12 @@ function computeDivergences(graph, opts = {}) {
15300
15481
  "missing-observed": 1,
15301
15482
  "version-mismatch": 2,
15302
15483
  "host-mismatch": 3,
15303
- "compat-violation": 4
15484
+ "compat-violation": 4,
15485
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
15486
+ // type; this only breaks a confidence tie, and it orders last so a same-
15487
+ // confidence edge finding leads. In practice it carries the INFERRED grade
15488
+ // (0.6), so it sits below the high-confidence edge divergences already.
15489
+ "observed-symbol-mismatch": 5
15304
15490
  };
15305
15491
  filtered.sort((a, b) => {
15306
15492
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -15311,7 +15497,10 @@ function computeDivergences(graph, opts = {}) {
15311
15497
  if (a.target !== b.target) return a.target.localeCompare(b.target);
15312
15498
  const ac = "column" in a && a.column ? a.column : "";
15313
15499
  const bc = "column" in b && b.column ? b.column : "";
15314
- return ac.localeCompare(bc);
15500
+ if (ac !== bc) return ac.localeCompare(bc);
15501
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
15502
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
15503
+ return asym.localeCompare(bsym);
15315
15504
  });
15316
15505
  return DivergenceResultSchema.parse({
15317
15506
  divergences: filtered,
@@ -16754,10 +16943,15 @@ function divergenceLine(d) {
16754
16943
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16755
16944
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16756
16945
  }
16946
+ if (d.type === "observed-symbol-mismatch") {
16947
+ const at = d.location ? ` at ${d.location}` : "";
16948
+ const member = d.symbol ? ` ${d.symbol}` : "";
16949
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
16950
+ }
16757
16951
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16758
16952
  }
16759
- function buildDivergenceSection(graph, node) {
16760
- const result = computeDivergences(graph, { node });
16953
+ function buildDivergenceSection(graph, node, incidents) {
16954
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
16761
16955
  if (result.totalAffected === 0) return null;
16762
16956
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16763
16957
  text: divergenceLine(d),
@@ -16770,8 +16964,8 @@ function buildDivergenceSection(graph, node) {
16770
16964
  facts
16771
16965
  };
16772
16966
  }
16773
- function buildGlobalDivergenceSection(graph) {
16774
- const result = computeDivergences(graph);
16967
+ function buildGlobalDivergenceSection(graph, incidents) {
16968
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
16775
16969
  if (result.totalAffected === 0) {
16776
16970
  return {
16777
16971
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -16869,7 +17063,7 @@ function buildOverviewSections(graph, incidents) {
16869
17063
  }))
16870
17064
  });
16871
17065
  }
16872
- const div = computeDivergences(graph);
17066
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
16873
17067
  sections.push({
16874
17068
  heading: "Divergences",
16875
17069
  facts: [
@@ -16883,7 +17077,7 @@ function buildOverviewSections(graph, incidents) {
16883
17077
  function buildGlobalSections(intent, graph, incidents) {
16884
17078
  switch (intent) {
16885
17079
  case "divergence":
16886
- return [buildGlobalDivergenceSection(graph)];
17080
+ return [buildGlobalDivergenceSection(graph, incidents)];
16887
17081
  case "incidents":
16888
17082
  return [buildGlobalIncidentsSection(incidents)];
16889
17083
  case "overview":
@@ -16914,7 +17108,7 @@ function buildSection(kind, graph, node, incidents, now) {
16914
17108
  case "incidents":
16915
17109
  return buildIncidentsSection(node, incidents);
16916
17110
  case "divergence":
16917
- return buildDivergenceSection(graph, node);
17111
+ return buildDivergenceSection(graph, node, incidents);
16918
17112
  }
16919
17113
  }
16920
17114
  function summarizeGlobal(intent, sections) {
@@ -20536,6 +20730,8 @@ async function deprovisionConnector(entry, env = process.env, fetchImpl) {
20536
20730
  }
20537
20731
 
20538
20732
  // src/api.ts
20733
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
20734
+ var INCIDENT_LIST_MAX_LIMIT = 200;
20539
20735
  function serializeGraph(graph) {
20540
20736
  const nodes = [];
20541
20737
  graph.forEachNode((_id, attrs) => {
@@ -20715,10 +20911,13 @@ function registerRoutes(scope, ctx) {
20715
20911
  }
20716
20912
  minConfidence = n;
20717
20913
  }
20914
+ const epath = errorsPathFor(proj);
20915
+ const incidents = epath ? await readErrorEvents(epath) : [];
20718
20916
  return computeDivergences(proj.graph, {
20719
20917
  ...typeFilter ? { type: typeFilter } : {},
20720
20918
  ...minConfidence !== void 0 ? { minConfidence } : {},
20721
- ...req.query.node ? { node: req.query.node } : {}
20919
+ ...req.query.node ? { node: req.query.node } : {},
20920
+ incidents
20722
20921
  });
20723
20922
  });
20724
20923
  scope.get("/incidents", async (req, reply) => {
@@ -20728,10 +20927,11 @@ function registerRoutes(scope, ctx) {
20728
20927
  if (!epath) return { count: 0, total: 0, events: [] };
20729
20928
  const events = await readErrorEvents(epath);
20730
20929
  const total = events.length;
20731
- const limit = req.query.limit ? Number(req.query.limit) : 50;
20732
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
20930
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
20931
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
20733
20932
  const sliced = events.slice(0, safeLimit);
20734
- return { count: sliced.length, total, events: sliced };
20933
+ const omitted = total - sliced.length;
20934
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
20735
20935
  });
20736
20936
  scope.get("/stale-events", async (req, reply) => {
20737
20937
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -20840,16 +21040,20 @@ function registerRoutes(scope, ctx) {
20840
21040
  const filtered = events.filter(
20841
21041
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
20842
21042
  );
20843
- return { count: filtered.length, total: filtered.length, events: filtered };
21043
+ const total = filtered.length;
21044
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21045
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21046
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
21047
+ const omitted = total - recent.length;
21048
+ return {
21049
+ count: recent.length,
21050
+ total,
21051
+ events: recent,
21052
+ ...omitted > 0 ? { omitted } : {}
21053
+ };
20844
21054
  };
20845
- scope.get(
20846
- "/incidents/:nodeId",
20847
- incidentHistoryHandler
20848
- );
20849
- scope.get(
20850
- "/graph/incident-history/:nodeId",
20851
- incidentHistoryHandler
20852
- );
21055
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
21056
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
20853
21057
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
20854
21058
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20855
21059
  if (!proj) return;
@@ -21491,4 +21695,4 @@ export {
21491
21695
  deprovisionConnector,
21492
21696
  buildApi
21493
21697
  };
21494
- //# sourceMappingURL=chunk-LRYPKC3B.js.map
21698
+ //# sourceMappingURL=chunk-EDE4XP2M.js.map