@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.
package/dist/neatd.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  reconcileDaemonRecordSync,
4
4
  startDaemon
5
- } from "./chunk-TMHCS4ZY.js";
5
+ } from "./chunk-2D4Y5QHE.js";
6
6
  import {
7
7
  listProjects,
8
8
  registryPath
9
- } from "./chunk-HD5X5TWY.js";
9
+ } from "./chunk-EDE4XP2M.js";
10
10
  import {
11
11
  BindAuthorityError,
12
12
  __require
package/dist/server.cjs CHANGED
@@ -6115,15 +6115,36 @@ function startStalenessLoop(graph, options = {}) {
6115
6115
  clearInterval(interval);
6116
6116
  };
6117
6117
  }
6118
- async function readErrorEvents(errorsPath) {
6118
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
6119
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
6120
+ async function readErrorFileTail(errorsPath, maxBytes) {
6121
+ const handle = await import_node_fs9.promises.open(errorsPath, "r");
6119
6122
  try {
6120
- const raw = await import_node_fs9.promises.readFile(errorsPath, "utf8");
6121
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6122
- return dedupeIncidents(events);
6123
+ const { size } = await handle.stat();
6124
+ if (size <= maxBytes) {
6125
+ return (await handle.readFile()).toString("utf8");
6126
+ }
6127
+ const buf = Buffer.alloc(maxBytes);
6128
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
6129
+ const raw = buf.toString("utf8");
6130
+ const firstNewline = raw.indexOf("\n");
6131
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
6132
+ } finally {
6133
+ await handle.close();
6134
+ }
6135
+ }
6136
+ async function readErrorEvents(errorsPath, opts) {
6137
+ let raw;
6138
+ try {
6139
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
6123
6140
  } catch (err) {
6124
6141
  if (err.code === "ENOENT") return [];
6125
6142
  throw err;
6126
6143
  }
6144
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6145
+ const deduped = dedupeIncidents(events);
6146
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
6147
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
6127
6148
  }
6128
6149
  function isSynthesizedHttpIncident(ev) {
6129
6150
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -6883,6 +6904,47 @@ function classifyNode(ctx) {
6883
6904
  function isVictimSeed(ctx) {
6884
6905
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
6885
6906
  }
6907
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
6908
+ "name resolution",
6909
+ "resolve host",
6910
+ "getaddrinfo",
6911
+ "enotfound",
6912
+ "connection refused",
6913
+ "econnrefused",
6914
+ "connection reset",
6915
+ "econnreset",
6916
+ "able to connect",
6917
+ "failed to connect",
6918
+ "cannot connect",
6919
+ "could not connect",
6920
+ "unable to connect",
6921
+ "connection timed out",
6922
+ "etimedout",
6923
+ "no route to host",
6924
+ "host unreachable",
6925
+ "network is unreachable",
6926
+ "connection closed"
6927
+ ];
6928
+ function incidentTextIndicatesOutboundFailure(ev) {
6929
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
6930
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
6931
+ }
6932
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
6933
+ for (const n of nodeScope(graph, nodeId)) {
6934
+ if (!graph.hasNode(n)) continue;
6935
+ for (const edgeId of graph.outboundEdges(n)) {
6936
+ const e = graph.getEdgeAttributes(edgeId);
6937
+ if (e.type === import_types8.EdgeType.CONTAINS) continue;
6938
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
6939
+ }
6940
+ }
6941
+ if (seedSource === "incident" && incidents) {
6942
+ for (const ev of incidents) {
6943
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
6944
+ }
6945
+ }
6946
+ return false;
6947
+ }
6886
6948
  function grainOf(graph, nodeId) {
6887
6949
  if (!graph.hasNode(nodeId)) return "unknown";
6888
6950
  const t = graph.getNodeAttributes(nodeId).type;
@@ -7046,7 +7108,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
7046
7108
  const candidates = [];
7047
7109
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
7048
7110
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
7049
- if (seedCtx && isVictimSeed(seedCtx)) {
7111
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
7112
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
7050
7113
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
7051
7114
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
7052
7115
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -7396,9 +7459,170 @@ function detectColumnDrift(node) {
7396
7459
  }
7397
7460
  return out;
7398
7461
  }
7462
+ var SYMBOL_MISMATCH_PATTERNS = [
7463
+ {
7464
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
7465
+ kind: "missing-attribute",
7466
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
7467
+ },
7468
+ {
7469
+ // "object has no field 'X'", "no such field X", "unknown field X".
7470
+ kind: "missing-field",
7471
+ patterns: [
7472
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
7473
+ ]
7474
+ },
7475
+ {
7476
+ // "has no property X", "no property named X".
7477
+ kind: "missing-property",
7478
+ patterns: [
7479
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
7480
+ ]
7481
+ },
7482
+ {
7483
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
7484
+ kind: "missing-column",
7485
+ patterns: [
7486
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
7487
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
7488
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
7489
+ ]
7490
+ },
7491
+ {
7492
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
7493
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
7494
+ // mismatch) does not get miscategorised here.
7495
+ kind: "undefined-method",
7496
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
7497
+ }
7498
+ ];
7499
+ function classifySymbolMismatch(message) {
7500
+ for (const entry of SYMBOL_MISMATCH_PATTERNS) {
7501
+ for (const re of entry.patterns) {
7502
+ const m = re.exec(message);
7503
+ if (m) {
7504
+ const captured = m[1];
7505
+ return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
7506
+ }
7507
+ }
7508
+ }
7509
+ return null;
7510
+ }
7511
+ function symbolLocus(graph, ev) {
7512
+ const attrs = ev.attributes ?? {};
7513
+ const filepath = codeFilepathOf(attrs);
7514
+ const lineno = codeLinenoOf(attrs);
7515
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
7516
+ const affected = ev.affectedNode;
7517
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
7518
+ const affectedIsCode = affectedInGraph && ((0, import_types9.parseSymbolId)(affected) !== null || (0, import_types9.parseFileId)(affected) !== null);
7519
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
7520
+ if (!location) return null;
7521
+ if (affectedInGraph) return { node: affected, location };
7522
+ const svc = (0, import_types9.serviceId)(ev.service);
7523
+ if (graph.hasNode(svc)) return { node: svc, location };
7524
+ return null;
7525
+ }
7526
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
7527
+ function detectSymbolMismatches(graph, incidents) {
7528
+ const groups = /* @__PURE__ */ new Map();
7529
+ for (const ev of incidents) {
7530
+ const classified = classifySymbolMismatch(ev.errorMessage);
7531
+ if (!classified) continue;
7532
+ const locus = symbolLocus(graph, ev);
7533
+ if (!locus) continue;
7534
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
7535
+ const existing = groups.get(key);
7536
+ if (!existing) {
7537
+ groups.set(key, {
7538
+ node: locus.node,
7539
+ kind: classified.kind,
7540
+ ...classified.symbol ? { symbol: classified.symbol } : {},
7541
+ ...locus.location ? { location: locus.location } : {},
7542
+ latest: ev,
7543
+ count: 1
7544
+ });
7545
+ } else {
7546
+ existing.count += 1;
7547
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
7548
+ existing.latest = ev;
7549
+ if (locus.location) existing.location = locus.location;
7550
+ }
7551
+ }
7552
+ }
7553
+ const out = [];
7554
+ for (const g of groups.values()) {
7555
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
7556
+ const where = g.location ? ` at ${g.location}` : "";
7557
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
7558
+ out.push({
7559
+ type: "observed-symbol-mismatch",
7560
+ source: g.node,
7561
+ target: g.node,
7562
+ mismatchKind: g.kind,
7563
+ ...g.symbol ? { symbol: g.symbol } : {},
7564
+ ...g.location ? { location: g.location } : {},
7565
+ provenance: import_types9.Provenance.INFERRED,
7566
+ incidentId: g.latest.id,
7567
+ errorMessage: g.latest.errorMessage,
7568
+ incidentCount: g.count,
7569
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
7570
+ 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.`,
7571
+ 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."
7572
+ });
7573
+ }
7574
+ return out;
7575
+ }
7399
7576
  function involvesNode(d, nodeId) {
7400
7577
  return d.source === nodeId || d.target === nodeId;
7401
7578
  }
7579
+ function datastoreEverObserved(graph, nodeId) {
7580
+ if (!graph.hasNode(nodeId)) return false;
7581
+ const n = graph.getNodeAttributes(nodeId);
7582
+ if (n.type === import_types9.NodeType.DatabaseNode) {
7583
+ const via = n.discoveredVia;
7584
+ if (via === "otel" || via === "merged") return true;
7585
+ }
7586
+ for (const edgeId of graph.inboundEdges(nodeId)) {
7587
+ const e = graph.getEdgeAttributes(edgeId);
7588
+ if (e.provenance === import_types9.Provenance.OBSERVED || e.provenance === import_types9.Provenance.STALE) return true;
7589
+ }
7590
+ return false;
7591
+ }
7592
+ function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
7593
+ for (const bucket of buckets2.values()) {
7594
+ if (bucket.type !== import_types9.EdgeType.CONNECTS_TO) continue;
7595
+ if (bucket.source !== serviceId16) continue;
7596
+ if (bucket.target === excludeTarget) continue;
7597
+ if (!graph.hasNode(bucket.target)) continue;
7598
+ const target = graph.getNodeAttributes(bucket.target);
7599
+ if (target.type !== import_types9.NodeType.DatabaseNode) continue;
7600
+ if (target.engine !== engine) continue;
7601
+ if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
7602
+ }
7603
+ return false;
7604
+ }
7605
+ var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
7606
+ function dampenDeadCodeProbes(graph, buckets2, all) {
7607
+ return all.map((d) => {
7608
+ if (d.type !== "missing-observed") return d;
7609
+ if (!d.extracted || d.edgeType !== import_types9.EdgeType.CONNECTS_TO) return d;
7610
+ if (!graph.hasNode(d.target)) return d;
7611
+ const target = graph.getNodeAttributes(d.target);
7612
+ if (target.type !== import_types9.NodeType.DatabaseNode) return d;
7613
+ if (d.extracted.evidence?.hostSource !== "literal") return d;
7614
+ if (datastoreEverObserved(graph, d.target)) return d;
7615
+ const engine = target.engine;
7616
+ if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
7617
+ const host = target.host ?? target.name;
7618
+ return {
7619
+ ...d,
7620
+ confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
7621
+ 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.`,
7622
+ 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."
7623
+ };
7624
+ });
7625
+ }
7402
7626
  function suppressHostMismatchHalves(all) {
7403
7627
  const observedHalf = /* @__PURE__ */ new Set();
7404
7628
  const declaredHalf = /* @__PURE__ */ new Set();
@@ -7434,8 +7658,12 @@ function computeDivergences(graph, opts = {}) {
7434
7658
  for (const d of detectColumnDrift(n)) all.push(d);
7435
7659
  }
7436
7660
  });
7661
+ if (opts.incidents && opts.incidents.length > 0) {
7662
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
7663
+ }
7437
7664
  const reconciled = suppressHostMismatchHalves(all);
7438
- let filtered = reconciled;
7665
+ const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
7666
+ let filtered = dampened;
7439
7667
  if (opts.type) {
7440
7668
  const allowed = opts.type;
7441
7669
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -7453,7 +7681,12 @@ function computeDivergences(graph, opts = {}) {
7453
7681
  "missing-observed": 1,
7454
7682
  "version-mismatch": 2,
7455
7683
  "host-mismatch": 3,
7456
- "compat-violation": 4
7684
+ "compat-violation": 4,
7685
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
7686
+ // type; this only breaks a confidence tie, and it orders last so a same-
7687
+ // confidence edge finding leads. In practice it carries the INFERRED grade
7688
+ // (0.6), so it sits below the high-confidence edge divergences already.
7689
+ "observed-symbol-mismatch": 5
7457
7690
  };
7458
7691
  filtered.sort((a, b) => {
7459
7692
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -7464,7 +7697,10 @@ function computeDivergences(graph, opts = {}) {
7464
7697
  if (a.target !== b.target) return a.target.localeCompare(b.target);
7465
7698
  const ac = "column" in a && a.column ? a.column : "";
7466
7699
  const bc = "column" in b && b.column ? b.column : "";
7467
- return ac.localeCompare(bc);
7700
+ if (ac !== bc) return ac.localeCompare(bc);
7701
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
7702
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
7703
+ return asym.localeCompare(bsym);
7468
7704
  });
7469
7705
  return import_types9.DivergenceResultSchema.parse({
7470
7706
  divergences: filtered,
@@ -10471,12 +10707,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
10471
10707
  const value = await interpolateEnvRefs(raw, serviceDir);
10472
10708
  if (!looksLike(value)) continue;
10473
10709
  const parsed = parse11(value);
10474
- if (parsed) out.push(parsed);
10710
+ if (parsed) out.push({ ...parsed, hostSource: "config" });
10475
10711
  }
10476
10712
  for (const lit of literals) {
10477
10713
  if (!looksLike(lit)) continue;
10478
10714
  const parsed = parse11(lit);
10479
- if (parsed) out.push(parsed);
10715
+ if (parsed) out.push({ ...parsed, hostSource: "literal" });
10480
10716
  }
10481
10717
  return out;
10482
10718
  }
@@ -10745,7 +10981,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
10745
10981
  type: import_types24.EdgeType.CONNECTS_TO,
10746
10982
  provenance: import_types24.Provenance.EXTRACTED,
10747
10983
  confidence: (0, import_types24.confidenceForExtracted)("structural"),
10748
- evidence: { file: evidenceFile }
10984
+ // Carry how the host was recovered (ADR-213) so the divergence ranker
10985
+ // can tell a real declared store from a hardcoded fault-injection /
10986
+ // flag-gated probe. Only set when the parser distinguished the two.
10987
+ evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
10749
10988
  };
10750
10989
  if (!graph.hasEdge(edge.id)) {
10751
10990
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -16641,10 +16880,15 @@ function divergenceLine(d) {
16641
16880
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16642
16881
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16643
16882
  }
16883
+ if (d.type === "observed-symbol-mismatch") {
16884
+ const at = d.location ? ` at ${d.location}` : "";
16885
+ const member = d.symbol ? ` ${d.symbol}` : "";
16886
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
16887
+ }
16644
16888
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16645
16889
  }
16646
- function buildDivergenceSection(graph, node) {
16647
- const result = computeDivergences(graph, { node });
16890
+ function buildDivergenceSection(graph, node, incidents) {
16891
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
16648
16892
  if (result.totalAffected === 0) return null;
16649
16893
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16650
16894
  text: divergenceLine(d),
@@ -16657,8 +16901,8 @@ function buildDivergenceSection(graph, node) {
16657
16901
  facts
16658
16902
  };
16659
16903
  }
16660
- function buildGlobalDivergenceSection(graph) {
16661
- const result = computeDivergences(graph);
16904
+ function buildGlobalDivergenceSection(graph, incidents) {
16905
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
16662
16906
  if (result.totalAffected === 0) {
16663
16907
  return {
16664
16908
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -16756,7 +17000,7 @@ function buildOverviewSections(graph, incidents) {
16756
17000
  }))
16757
17001
  });
16758
17002
  }
16759
- const div = computeDivergences(graph);
17003
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
16760
17004
  sections.push({
16761
17005
  heading: "Divergences",
16762
17006
  facts: [
@@ -16770,7 +17014,7 @@ function buildOverviewSections(graph, incidents) {
16770
17014
  function buildGlobalSections(intent, graph, incidents) {
16771
17015
  switch (intent) {
16772
17016
  case "divergence":
16773
- return [buildGlobalDivergenceSection(graph)];
17017
+ return [buildGlobalDivergenceSection(graph, incidents)];
16774
17018
  case "incidents":
16775
17019
  return [buildGlobalIncidentsSection(incidents)];
16776
17020
  case "overview":
@@ -16801,7 +17045,7 @@ function buildSection(kind, graph, node, incidents, now) {
16801
17045
  case "incidents":
16802
17046
  return buildIncidentsSection(node, incidents);
16803
17047
  case "divergence":
16804
- return buildDivergenceSection(graph, node);
17048
+ return buildDivergenceSection(graph, node, incidents);
16805
17049
  }
16806
17050
  }
16807
17051
  function summarizeGlobal(intent, sections) {
@@ -20606,6 +20850,8 @@ function buildRegistration(entry, graph, env = process.env) {
20606
20850
  }
20607
20851
 
20608
20852
  // src/api.ts
20853
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
20854
+ var INCIDENT_LIST_MAX_LIMIT = 200;
20609
20855
  function serializeGraph(graph) {
20610
20856
  const nodes = [];
20611
20857
  graph.forEachNode((_id, attrs) => {
@@ -20785,10 +21031,13 @@ function registerRoutes(scope, ctx) {
20785
21031
  }
20786
21032
  minConfidence = n;
20787
21033
  }
21034
+ const epath = errorsPathFor(proj);
21035
+ const incidents = epath ? await readErrorEvents(epath) : [];
20788
21036
  return computeDivergences(proj.graph, {
20789
21037
  ...typeFilter ? { type: typeFilter } : {},
20790
21038
  ...minConfidence !== void 0 ? { minConfidence } : {},
20791
- ...req.query.node ? { node: req.query.node } : {}
21039
+ ...req.query.node ? { node: req.query.node } : {},
21040
+ incidents
20792
21041
  });
20793
21042
  });
20794
21043
  scope.get("/incidents", async (req, reply) => {
@@ -20798,10 +21047,11 @@ function registerRoutes(scope, ctx) {
20798
21047
  if (!epath) return { count: 0, total: 0, events: [] };
20799
21048
  const events = await readErrorEvents(epath);
20800
21049
  const total = events.length;
20801
- const limit = req.query.limit ? Number(req.query.limit) : 50;
20802
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
21050
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21051
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
20803
21052
  const sliced = events.slice(0, safeLimit);
20804
- return { count: sliced.length, total, events: sliced };
21053
+ const omitted = total - sliced.length;
21054
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
20805
21055
  });
20806
21056
  scope.get("/stale-events", async (req, reply) => {
20807
21057
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -20910,16 +21160,20 @@ function registerRoutes(scope, ctx) {
20910
21160
  const filtered = events.filter(
20911
21161
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
20912
21162
  );
20913
- return { count: filtered.length, total: filtered.length, events: filtered };
21163
+ const total = filtered.length;
21164
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21165
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21166
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
21167
+ const omitted = total - recent.length;
21168
+ return {
21169
+ count: recent.length,
21170
+ total,
21171
+ events: recent,
21172
+ ...omitted > 0 ? { omitted } : {}
21173
+ };
20914
21174
  };
20915
- scope.get(
20916
- "/incidents/:nodeId",
20917
- incidentHistoryHandler
20918
- );
20919
- scope.get(
20920
- "/graph/incident-history/:nodeId",
20921
- incidentHistoryHandler
20922
- );
21175
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
21176
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
20923
21177
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
20924
21178
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20925
21179
  if (!proj) return;