@neat.is/core 0.9.2 → 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-HD5X5TWY.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-TMHCS4ZY.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` : "";
@@ -9112,12 +9175,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
9112
9175
  const value = await interpolateEnvRefs(raw, serviceDir);
9113
9176
  if (!looksLike(value)) continue;
9114
9177
  const parsed = parse11(value);
9115
- if (parsed) out.push(parsed);
9178
+ if (parsed) out.push({ ...parsed, hostSource: "config" });
9116
9179
  }
9117
9180
  for (const lit of literals) {
9118
9181
  if (!looksLike(lit)) continue;
9119
9182
  const parsed = parse11(lit);
9120
- if (parsed) out.push(parsed);
9183
+ if (parsed) out.push({ ...parsed, hostSource: "literal" });
9121
9184
  }
9122
9185
  return out;
9123
9186
  }
@@ -9385,7 +9448,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
9385
9448
  type: EdgeType10.CONNECTS_TO,
9386
9449
  provenance: Provenance10.EXTRACTED,
9387
9450
  confidence: confidenceForExtracted7("structural"),
9388
- evidence: { file: evidenceFile }
9451
+ // Carry how the host was recovered (ADR-213) so the divergence ranker
9452
+ // can tell a real declared store from a hardcoded fault-injection /
9453
+ // flag-gated probe. Only set when the parser distinguished the two.
9454
+ evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
9389
9455
  };
9390
9456
  if (!graph.hasEdge(edge.id)) {
9391
9457
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -14936,6 +15002,7 @@ import {
14936
15002
  NodeType as NodeType30,
14937
15003
  parseEdgeId,
14938
15004
  parseFileId,
15005
+ parseSymbolId as parseSymbolId2,
14939
15006
  Provenance as Provenance24,
14940
15007
  serviceId as serviceId13
14941
15008
  } from "@neat.is/types";
@@ -15192,9 +15259,170 @@ function detectColumnDrift(node) {
15192
15259
  }
15193
15260
  return out;
15194
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
+ }
15195
15376
  function involvesNode(d, nodeId) {
15196
15377
  return d.source === nodeId || d.target === nodeId;
15197
15378
  }
15379
+ function datastoreEverObserved(graph, nodeId) {
15380
+ if (!graph.hasNode(nodeId)) return false;
15381
+ const n = graph.getNodeAttributes(nodeId);
15382
+ if (n.type === NodeType30.DatabaseNode) {
15383
+ const via = n.discoveredVia;
15384
+ if (via === "otel" || via === "merged") return true;
15385
+ }
15386
+ for (const edgeId of graph.inboundEdges(nodeId)) {
15387
+ const e = graph.getEdgeAttributes(edgeId);
15388
+ if (e.provenance === Provenance24.OBSERVED || e.provenance === Provenance24.STALE) return true;
15389
+ }
15390
+ return false;
15391
+ }
15392
+ function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
15393
+ for (const bucket of buckets2.values()) {
15394
+ if (bucket.type !== EdgeType25.CONNECTS_TO) continue;
15395
+ if (bucket.source !== serviceId16) continue;
15396
+ if (bucket.target === excludeTarget) continue;
15397
+ if (!graph.hasNode(bucket.target)) continue;
15398
+ const target = graph.getNodeAttributes(bucket.target);
15399
+ if (target.type !== NodeType30.DatabaseNode) continue;
15400
+ if (target.engine !== engine) continue;
15401
+ if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
15402
+ }
15403
+ return false;
15404
+ }
15405
+ var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
15406
+ function dampenDeadCodeProbes(graph, buckets2, all) {
15407
+ return all.map((d) => {
15408
+ if (d.type !== "missing-observed") return d;
15409
+ if (!d.extracted || d.edgeType !== EdgeType25.CONNECTS_TO) return d;
15410
+ if (!graph.hasNode(d.target)) return d;
15411
+ const target = graph.getNodeAttributes(d.target);
15412
+ if (target.type !== NodeType30.DatabaseNode) return d;
15413
+ if (d.extracted.evidence?.hostSource !== "literal") return d;
15414
+ if (datastoreEverObserved(graph, d.target)) return d;
15415
+ const engine = target.engine;
15416
+ if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
15417
+ const host = target.host ?? target.name;
15418
+ return {
15419
+ ...d,
15420
+ confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
15421
+ reason: `${d.source} declares a ${engine} connection to a hardcoded-literal host (${host}) that production has never observed, while it does observe another ${engine} store \u2014 this reads as a flag-gated or dead-code declaration (e.g. a fault-injection probe), not a real declared-vs-observed gap.`,
15422
+ recommendation: "Confirm this is an intentional dead alternate \u2014 a fault-injection probe or a flag-gated branch. If it is meant to run in production, check the feature flag or conditional that gates it; if not, it can be ignored or removed."
15423
+ };
15424
+ });
15425
+ }
15198
15426
  function suppressHostMismatchHalves(all) {
15199
15427
  const observedHalf = /* @__PURE__ */ new Set();
15200
15428
  const declaredHalf = /* @__PURE__ */ new Set();
@@ -15230,8 +15458,12 @@ function computeDivergences(graph, opts = {}) {
15230
15458
  for (const d of detectColumnDrift(n)) all.push(d);
15231
15459
  }
15232
15460
  });
15461
+ if (opts.incidents && opts.incidents.length > 0) {
15462
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
15463
+ }
15233
15464
  const reconciled = suppressHostMismatchHalves(all);
15234
- let filtered = reconciled;
15465
+ const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
15466
+ let filtered = dampened;
15235
15467
  if (opts.type) {
15236
15468
  const allowed = opts.type;
15237
15469
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -15249,7 +15481,12 @@ function computeDivergences(graph, opts = {}) {
15249
15481
  "missing-observed": 1,
15250
15482
  "version-mismatch": 2,
15251
15483
  "host-mismatch": 3,
15252
- "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
15253
15490
  };
15254
15491
  filtered.sort((a, b) => {
15255
15492
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -15260,7 +15497,10 @@ function computeDivergences(graph, opts = {}) {
15260
15497
  if (a.target !== b.target) return a.target.localeCompare(b.target);
15261
15498
  const ac = "column" in a && a.column ? a.column : "";
15262
15499
  const bc = "column" in b && b.column ? b.column : "";
15263
- 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);
15264
15504
  });
15265
15505
  return DivergenceResultSchema.parse({
15266
15506
  divergences: filtered,
@@ -16703,10 +16943,15 @@ function divergenceLine(d) {
16703
16943
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16704
16944
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16705
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
+ }
16706
16951
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16707
16952
  }
16708
- function buildDivergenceSection(graph, node) {
16709
- const result = computeDivergences(graph, { node });
16953
+ function buildDivergenceSection(graph, node, incidents) {
16954
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
16710
16955
  if (result.totalAffected === 0) return null;
16711
16956
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16712
16957
  text: divergenceLine(d),
@@ -16719,8 +16964,8 @@ function buildDivergenceSection(graph, node) {
16719
16964
  facts
16720
16965
  };
16721
16966
  }
16722
- function buildGlobalDivergenceSection(graph) {
16723
- const result = computeDivergences(graph);
16967
+ function buildGlobalDivergenceSection(graph, incidents) {
16968
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
16724
16969
  if (result.totalAffected === 0) {
16725
16970
  return {
16726
16971
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -16818,7 +17063,7 @@ function buildOverviewSections(graph, incidents) {
16818
17063
  }))
16819
17064
  });
16820
17065
  }
16821
- const div = computeDivergences(graph);
17066
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
16822
17067
  sections.push({
16823
17068
  heading: "Divergences",
16824
17069
  facts: [
@@ -16832,7 +17077,7 @@ function buildOverviewSections(graph, incidents) {
16832
17077
  function buildGlobalSections(intent, graph, incidents) {
16833
17078
  switch (intent) {
16834
17079
  case "divergence":
16835
- return [buildGlobalDivergenceSection(graph)];
17080
+ return [buildGlobalDivergenceSection(graph, incidents)];
16836
17081
  case "incidents":
16837
17082
  return [buildGlobalIncidentsSection(incidents)];
16838
17083
  case "overview":
@@ -16863,7 +17108,7 @@ function buildSection(kind, graph, node, incidents, now) {
16863
17108
  case "incidents":
16864
17109
  return buildIncidentsSection(node, incidents);
16865
17110
  case "divergence":
16866
- return buildDivergenceSection(graph, node);
17111
+ return buildDivergenceSection(graph, node, incidents);
16867
17112
  }
16868
17113
  }
16869
17114
  function summarizeGlobal(intent, sections) {
@@ -20485,6 +20730,8 @@ async function deprovisionConnector(entry, env = process.env, fetchImpl) {
20485
20730
  }
20486
20731
 
20487
20732
  // src/api.ts
20733
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
20734
+ var INCIDENT_LIST_MAX_LIMIT = 200;
20488
20735
  function serializeGraph(graph) {
20489
20736
  const nodes = [];
20490
20737
  graph.forEachNode((_id, attrs) => {
@@ -20664,10 +20911,13 @@ function registerRoutes(scope, ctx) {
20664
20911
  }
20665
20912
  minConfidence = n;
20666
20913
  }
20914
+ const epath = errorsPathFor(proj);
20915
+ const incidents = epath ? await readErrorEvents(epath) : [];
20667
20916
  return computeDivergences(proj.graph, {
20668
20917
  ...typeFilter ? { type: typeFilter } : {},
20669
20918
  ...minConfidence !== void 0 ? { minConfidence } : {},
20670
- ...req.query.node ? { node: req.query.node } : {}
20919
+ ...req.query.node ? { node: req.query.node } : {},
20920
+ incidents
20671
20921
  });
20672
20922
  });
20673
20923
  scope.get("/incidents", async (req, reply) => {
@@ -20677,10 +20927,11 @@ function registerRoutes(scope, ctx) {
20677
20927
  if (!epath) return { count: 0, total: 0, events: [] };
20678
20928
  const events = await readErrorEvents(epath);
20679
20929
  const total = events.length;
20680
- const limit = req.query.limit ? Number(req.query.limit) : 50;
20681
- 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;
20682
20932
  const sliced = events.slice(0, safeLimit);
20683
- return { count: sliced.length, total, events: sliced };
20933
+ const omitted = total - sliced.length;
20934
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
20684
20935
  });
20685
20936
  scope.get("/stale-events", async (req, reply) => {
20686
20937
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -20789,16 +21040,20 @@ function registerRoutes(scope, ctx) {
20789
21040
  const filtered = events.filter(
20790
21041
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
20791
21042
  );
20792
- 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
+ };
20793
21054
  };
20794
- scope.get(
20795
- "/incidents/:nodeId",
20796
- incidentHistoryHandler
20797
- );
20798
- scope.get(
20799
- "/graph/incident-history/:nodeId",
20800
- incidentHistoryHandler
20801
- );
21055
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
21056
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
20802
21057
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
20803
21058
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20804
21059
  if (!proj) return;
@@ -21440,4 +21695,4 @@ export {
21440
21695
  deprovisionConnector,
21441
21696
  buildApi
21442
21697
  };
21443
- //# sourceMappingURL=chunk-HD5X5TWY.js.map
21698
+ //# sourceMappingURL=chunk-EDE4XP2M.js.map