@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.
- package/dist/{chunk-WJGZYEUG.js → chunk-2D4Y5QHE.js} +2 -2
- package/dist/{chunk-LRYPKC3B.js → chunk-EDE4XP2M.js} +232 -28
- package/dist/chunk-EDE4XP2M.js.map +1 -0
- package/dist/cli.cjs +242 -28
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +230 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +230 -27
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +230 -27
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-LRYPKC3B.js.map +0 -1
- /package/dist/{chunk-WJGZYEUG.js.map → chunk-2D4Y5QHE.js.map} +0 -0
package/dist/neatd.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
reconcileDaemonRecordSync,
|
|
4
4
|
startDaemon
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2D4Y5QHE.js";
|
|
6
6
|
import {
|
|
7
7
|
listProjects,
|
|
8
8
|
registryPath
|
|
9
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
|
6121
|
-
|
|
6122
|
-
|
|
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
|
-
|
|
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,6 +7459,120 @@ 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
|
}
|
|
@@ -7481,6 +7658,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7481
7658
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
7482
7659
|
}
|
|
7483
7660
|
});
|
|
7661
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
7662
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
7663
|
+
}
|
|
7484
7664
|
const reconciled = suppressHostMismatchHalves(all);
|
|
7485
7665
|
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
7486
7666
|
let filtered = dampened;
|
|
@@ -7501,7 +7681,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7501
7681
|
"missing-observed": 1,
|
|
7502
7682
|
"version-mismatch": 2,
|
|
7503
7683
|
"host-mismatch": 3,
|
|
7504
|
-
"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
|
|
7505
7690
|
};
|
|
7506
7691
|
filtered.sort((a, b) => {
|
|
7507
7692
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -7512,7 +7697,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7512
7697
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
7513
7698
|
const ac = "column" in a && a.column ? a.column : "";
|
|
7514
7699
|
const bc = "column" in b && b.column ? b.column : "";
|
|
7515
|
-
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);
|
|
7516
7704
|
});
|
|
7517
7705
|
return import_types9.DivergenceResultSchema.parse({
|
|
7518
7706
|
divergences: filtered,
|
|
@@ -16692,10 +16880,15 @@ function divergenceLine(d) {
|
|
|
16692
16880
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16693
16881
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16694
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
|
+
}
|
|
16695
16888
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16696
16889
|
}
|
|
16697
|
-
function buildDivergenceSection(graph, node) {
|
|
16698
|
-
const result = computeDivergences(graph, { node });
|
|
16890
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
16891
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
16699
16892
|
if (result.totalAffected === 0) return null;
|
|
16700
16893
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16701
16894
|
text: divergenceLine(d),
|
|
@@ -16708,8 +16901,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
16708
16901
|
facts
|
|
16709
16902
|
};
|
|
16710
16903
|
}
|
|
16711
|
-
function buildGlobalDivergenceSection(graph) {
|
|
16712
|
-
const result = computeDivergences(graph);
|
|
16904
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
16905
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16713
16906
|
if (result.totalAffected === 0) {
|
|
16714
16907
|
return {
|
|
16715
16908
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -16807,7 +17000,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16807
17000
|
}))
|
|
16808
17001
|
});
|
|
16809
17002
|
}
|
|
16810
|
-
const div = computeDivergences(graph);
|
|
17003
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16811
17004
|
sections.push({
|
|
16812
17005
|
heading: "Divergences",
|
|
16813
17006
|
facts: [
|
|
@@ -16821,7 +17014,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16821
17014
|
function buildGlobalSections(intent, graph, incidents) {
|
|
16822
17015
|
switch (intent) {
|
|
16823
17016
|
case "divergence":
|
|
16824
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17017
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
16825
17018
|
case "incidents":
|
|
16826
17019
|
return [buildGlobalIncidentsSection(incidents)];
|
|
16827
17020
|
case "overview":
|
|
@@ -16852,7 +17045,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
16852
17045
|
case "incidents":
|
|
16853
17046
|
return buildIncidentsSection(node, incidents);
|
|
16854
17047
|
case "divergence":
|
|
16855
|
-
return buildDivergenceSection(graph, node);
|
|
17048
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
16856
17049
|
}
|
|
16857
17050
|
}
|
|
16858
17051
|
function summarizeGlobal(intent, sections) {
|
|
@@ -20657,6 +20850,8 @@ function buildRegistration(entry, graph, env = process.env) {
|
|
|
20657
20850
|
}
|
|
20658
20851
|
|
|
20659
20852
|
// src/api.ts
|
|
20853
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
20854
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
20660
20855
|
function serializeGraph(graph) {
|
|
20661
20856
|
const nodes = [];
|
|
20662
20857
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -20836,10 +21031,13 @@ function registerRoutes(scope, ctx) {
|
|
|
20836
21031
|
}
|
|
20837
21032
|
minConfidence = n;
|
|
20838
21033
|
}
|
|
21034
|
+
const epath = errorsPathFor(proj);
|
|
21035
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
20839
21036
|
return computeDivergences(proj.graph, {
|
|
20840
21037
|
...typeFilter ? { type: typeFilter } : {},
|
|
20841
21038
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
20842
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
21039
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
21040
|
+
incidents
|
|
20843
21041
|
});
|
|
20844
21042
|
});
|
|
20845
21043
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -20849,10 +21047,11 @@ function registerRoutes(scope, ctx) {
|
|
|
20849
21047
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
20850
21048
|
const events = await readErrorEvents(epath);
|
|
20851
21049
|
const total = events.length;
|
|
20852
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
20853
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
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;
|
|
20854
21052
|
const sliced = events.slice(0, safeLimit);
|
|
20855
|
-
|
|
21053
|
+
const omitted = total - sliced.length;
|
|
21054
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
20856
21055
|
});
|
|
20857
21056
|
scope.get("/stale-events", async (req, reply) => {
|
|
20858
21057
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -20961,16 +21160,20 @@ function registerRoutes(scope, ctx) {
|
|
|
20961
21160
|
const filtered = events.filter(
|
|
20962
21161
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
20963
21162
|
);
|
|
20964
|
-
|
|
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
|
+
};
|
|
20965
21174
|
};
|
|
20966
|
-
scope.get(
|
|
20967
|
-
|
|
20968
|
-
incidentHistoryHandler
|
|
20969
|
-
);
|
|
20970
|
-
scope.get(
|
|
20971
|
-
"/graph/incident-history/:nodeId",
|
|
20972
|
-
incidentHistoryHandler
|
|
20973
|
-
);
|
|
21175
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21176
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
20974
21177
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
20975
21178
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
20976
21179
|
if (!proj) return;
|