@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/cli.cjs
CHANGED
|
@@ -1933,6 +1933,47 @@ function classifyNode(ctx) {
|
|
|
1933
1933
|
function isVictimSeed(ctx) {
|
|
1934
1934
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1935
1935
|
}
|
|
1936
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
1937
|
+
"name resolution",
|
|
1938
|
+
"resolve host",
|
|
1939
|
+
"getaddrinfo",
|
|
1940
|
+
"enotfound",
|
|
1941
|
+
"connection refused",
|
|
1942
|
+
"econnrefused",
|
|
1943
|
+
"connection reset",
|
|
1944
|
+
"econnreset",
|
|
1945
|
+
"able to connect",
|
|
1946
|
+
"failed to connect",
|
|
1947
|
+
"cannot connect",
|
|
1948
|
+
"could not connect",
|
|
1949
|
+
"unable to connect",
|
|
1950
|
+
"connection timed out",
|
|
1951
|
+
"etimedout",
|
|
1952
|
+
"no route to host",
|
|
1953
|
+
"host unreachable",
|
|
1954
|
+
"network is unreachable",
|
|
1955
|
+
"connection closed"
|
|
1956
|
+
];
|
|
1957
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
1958
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
1959
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
1960
|
+
}
|
|
1961
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
1962
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
1963
|
+
if (!graph.hasNode(n)) continue;
|
|
1964
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1965
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1966
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1967
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
if (seedSource === "incident" && incidents) {
|
|
1971
|
+
for (const ev of incidents) {
|
|
1972
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1936
1977
|
function grainOf(graph, nodeId) {
|
|
1937
1978
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1938
1979
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -2096,7 +2137,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
2096
2137
|
const candidates = [];
|
|
2097
2138
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
2098
2139
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
2099
|
-
|
|
2140
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
2141
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
2100
2142
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
2101
2143
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
2102
2144
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -6868,15 +6910,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6868
6910
|
clearInterval(interval);
|
|
6869
6911
|
};
|
|
6870
6912
|
}
|
|
6871
|
-
|
|
6913
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
6914
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
6915
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
6916
|
+
const handle = await import_node_fs8.promises.open(errorsPath, "r");
|
|
6917
|
+
try {
|
|
6918
|
+
const { size } = await handle.stat();
|
|
6919
|
+
if (size <= maxBytes) {
|
|
6920
|
+
return (await handle.readFile()).toString("utf8");
|
|
6921
|
+
}
|
|
6922
|
+
const buf = Buffer.alloc(maxBytes);
|
|
6923
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
6924
|
+
const raw = buf.toString("utf8");
|
|
6925
|
+
const firstNewline = raw.indexOf("\n");
|
|
6926
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
6927
|
+
} finally {
|
|
6928
|
+
await handle.close();
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6931
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
6932
|
+
let raw;
|
|
6872
6933
|
try {
|
|
6873
|
-
|
|
6874
|
-
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6875
|
-
return dedupeIncidents(events);
|
|
6934
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6876
6935
|
} catch (err) {
|
|
6877
6936
|
if (err.code === "ENOENT") return [];
|
|
6878
6937
|
throw err;
|
|
6879
6938
|
}
|
|
6939
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6940
|
+
const deduped = dedupeIncidents(events);
|
|
6941
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
6942
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6880
6943
|
}
|
|
6881
6944
|
function isSynthesizedHttpIncident(ev) {
|
|
6882
6945
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -15981,6 +16044,120 @@ function detectColumnDrift(node) {
|
|
|
15981
16044
|
}
|
|
15982
16045
|
return out;
|
|
15983
16046
|
}
|
|
16047
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16048
|
+
{
|
|
16049
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
16050
|
+
kind: "missing-attribute",
|
|
16051
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
16052
|
+
},
|
|
16053
|
+
{
|
|
16054
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
16055
|
+
kind: "missing-field",
|
|
16056
|
+
patterns: [
|
|
16057
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
16058
|
+
]
|
|
16059
|
+
},
|
|
16060
|
+
{
|
|
16061
|
+
// "has no property X", "no property named X".
|
|
16062
|
+
kind: "missing-property",
|
|
16063
|
+
patterns: [
|
|
16064
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
16065
|
+
]
|
|
16066
|
+
},
|
|
16067
|
+
{
|
|
16068
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
16069
|
+
kind: "missing-column",
|
|
16070
|
+
patterns: [
|
|
16071
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16072
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16073
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
16074
|
+
]
|
|
16075
|
+
},
|
|
16076
|
+
{
|
|
16077
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
16078
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
16079
|
+
// mismatch) does not get miscategorised here.
|
|
16080
|
+
kind: "undefined-method",
|
|
16081
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
16082
|
+
}
|
|
16083
|
+
];
|
|
16084
|
+
function classifySymbolMismatch(message) {
|
|
16085
|
+
for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
|
|
16086
|
+
for (const re of entry2.patterns) {
|
|
16087
|
+
const m = re.exec(message);
|
|
16088
|
+
if (m) {
|
|
16089
|
+
const captured = m[1];
|
|
16090
|
+
return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
|
|
16091
|
+
}
|
|
16092
|
+
}
|
|
16093
|
+
}
|
|
16094
|
+
return null;
|
|
16095
|
+
}
|
|
16096
|
+
function symbolLocus(graph, ev) {
|
|
16097
|
+
const attrs = ev.attributes ?? {};
|
|
16098
|
+
const filepath = codeFilepathOf(attrs);
|
|
16099
|
+
const lineno = codeLinenoOf(attrs);
|
|
16100
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
16101
|
+
const affected = ev.affectedNode;
|
|
16102
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
16103
|
+
const affectedIsCode = affectedInGraph && ((0, import_types57.parseSymbolId)(affected) !== null || (0, import_types57.parseFileId)(affected) !== null);
|
|
16104
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
16105
|
+
if (!location) return null;
|
|
16106
|
+
if (affectedInGraph) return { node: affected, location };
|
|
16107
|
+
const svc = (0, import_types57.serviceId)(ev.service);
|
|
16108
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
16109
|
+
return null;
|
|
16110
|
+
}
|
|
16111
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
16112
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
16113
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16114
|
+
for (const ev of incidents) {
|
|
16115
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
16116
|
+
if (!classified) continue;
|
|
16117
|
+
const locus = symbolLocus(graph, ev);
|
|
16118
|
+
if (!locus) continue;
|
|
16119
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
16120
|
+
const existing = groups.get(key);
|
|
16121
|
+
if (!existing) {
|
|
16122
|
+
groups.set(key, {
|
|
16123
|
+
node: locus.node,
|
|
16124
|
+
kind: classified.kind,
|
|
16125
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
16126
|
+
...locus.location ? { location: locus.location } : {},
|
|
16127
|
+
latest: ev,
|
|
16128
|
+
count: 1
|
|
16129
|
+
});
|
|
16130
|
+
} else {
|
|
16131
|
+
existing.count += 1;
|
|
16132
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
16133
|
+
existing.latest = ev;
|
|
16134
|
+
if (locus.location) existing.location = locus.location;
|
|
16135
|
+
}
|
|
16136
|
+
}
|
|
16137
|
+
}
|
|
16138
|
+
const out = [];
|
|
16139
|
+
for (const g of groups.values()) {
|
|
16140
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
16141
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
16142
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
16143
|
+
out.push({
|
|
16144
|
+
type: "observed-symbol-mismatch",
|
|
16145
|
+
source: g.node,
|
|
16146
|
+
target: g.node,
|
|
16147
|
+
mismatchKind: g.kind,
|
|
16148
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
16149
|
+
...g.location ? { location: g.location } : {},
|
|
16150
|
+
provenance: import_types57.Provenance.INFERRED,
|
|
16151
|
+
incidentId: g.latest.id,
|
|
16152
|
+
errorMessage: g.latest.errorMessage,
|
|
16153
|
+
incidentCount: g.count,
|
|
16154
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
16155
|
+
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.`,
|
|
16156
|
+
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."
|
|
16157
|
+
});
|
|
16158
|
+
}
|
|
16159
|
+
return out;
|
|
16160
|
+
}
|
|
15984
16161
|
function involvesNode(d, nodeId) {
|
|
15985
16162
|
return d.source === nodeId || d.target === nodeId;
|
|
15986
16163
|
}
|
|
@@ -16066,6 +16243,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16066
16243
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
16067
16244
|
}
|
|
16068
16245
|
});
|
|
16246
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
16247
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
16248
|
+
}
|
|
16069
16249
|
const reconciled = suppressHostMismatchHalves(all);
|
|
16070
16250
|
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
16071
16251
|
let filtered = dampened;
|
|
@@ -16086,7 +16266,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16086
16266
|
"missing-observed": 1,
|
|
16087
16267
|
"version-mismatch": 2,
|
|
16088
16268
|
"host-mismatch": 3,
|
|
16089
|
-
"compat-violation": 4
|
|
16269
|
+
"compat-violation": 4,
|
|
16270
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
16271
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
16272
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
16273
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
16274
|
+
"observed-symbol-mismatch": 5
|
|
16090
16275
|
};
|
|
16091
16276
|
filtered.sort((a, b) => {
|
|
16092
16277
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -16097,7 +16282,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16097
16282
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
16098
16283
|
const ac = "column" in a && a.column ? a.column : "";
|
|
16099
16284
|
const bc = "column" in b && b.column ? b.column : "";
|
|
16100
|
-
return ac.localeCompare(bc);
|
|
16285
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
16286
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
16287
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
16288
|
+
return asym.localeCompare(bsym);
|
|
16101
16289
|
});
|
|
16102
16290
|
return import_types57.DivergenceResultSchema.parse({
|
|
16103
16291
|
divergences: filtered,
|
|
@@ -17157,10 +17345,15 @@ function divergenceLine(d) {
|
|
|
17157
17345
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
17158
17346
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
17159
17347
|
}
|
|
17348
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17349
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17350
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17351
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17352
|
+
}
|
|
17160
17353
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
17161
17354
|
}
|
|
17162
|
-
function buildDivergenceSection(graph, node) {
|
|
17163
|
-
const result = computeDivergences(graph, { node });
|
|
17355
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17356
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
17164
17357
|
if (result.totalAffected === 0) return null;
|
|
17165
17358
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
17166
17359
|
text: divergenceLine(d),
|
|
@@ -17173,8 +17366,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
17173
17366
|
facts
|
|
17174
17367
|
};
|
|
17175
17368
|
}
|
|
17176
|
-
function buildGlobalDivergenceSection(graph) {
|
|
17177
|
-
const result = computeDivergences(graph);
|
|
17369
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17370
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17178
17371
|
if (result.totalAffected === 0) {
|
|
17179
17372
|
return {
|
|
17180
17373
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -17272,7 +17465,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17272
17465
|
}))
|
|
17273
17466
|
});
|
|
17274
17467
|
}
|
|
17275
|
-
const div = computeDivergences(graph);
|
|
17468
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17276
17469
|
sections.push({
|
|
17277
17470
|
heading: "Divergences",
|
|
17278
17471
|
facts: [
|
|
@@ -17286,7 +17479,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17286
17479
|
function buildGlobalSections(intent, graph, incidents) {
|
|
17287
17480
|
switch (intent) {
|
|
17288
17481
|
case "divergence":
|
|
17289
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17482
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
17290
17483
|
case "incidents":
|
|
17291
17484
|
return [buildGlobalIncidentsSection(incidents)];
|
|
17292
17485
|
case "overview":
|
|
@@ -17317,7 +17510,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
17317
17510
|
case "incidents":
|
|
17318
17511
|
return buildIncidentsSection(node, incidents);
|
|
17319
17512
|
case "divergence":
|
|
17320
|
-
return buildDivergenceSection(graph, node);
|
|
17513
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
17321
17514
|
}
|
|
17322
17515
|
}
|
|
17323
17516
|
function summarizeGlobal(intent, sections) {
|
|
@@ -21537,6 +21730,8 @@ async function deprovisionConnector(entry2, env = process.env, fetchImpl) {
|
|
|
21537
21730
|
}
|
|
21538
21731
|
|
|
21539
21732
|
// src/api.ts
|
|
21733
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
21734
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
21540
21735
|
function serializeGraph(graph) {
|
|
21541
21736
|
const nodes = [];
|
|
21542
21737
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -21716,10 +21911,13 @@ function registerRoutes(scope, ctx) {
|
|
|
21716
21911
|
}
|
|
21717
21912
|
minConfidence = n;
|
|
21718
21913
|
}
|
|
21914
|
+
const epath = errorsPathFor(proj);
|
|
21915
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
21719
21916
|
return computeDivergences(proj.graph, {
|
|
21720
21917
|
...typeFilter ? { type: typeFilter } : {},
|
|
21721
21918
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
21722
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
21919
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
21920
|
+
incidents
|
|
21723
21921
|
});
|
|
21724
21922
|
});
|
|
21725
21923
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -21729,10 +21927,11 @@ function registerRoutes(scope, ctx) {
|
|
|
21729
21927
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
21730
21928
|
const events = await readErrorEvents(epath);
|
|
21731
21929
|
const total = events.length;
|
|
21732
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
21733
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21930
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21931
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21734
21932
|
const sliced = events.slice(0, safeLimit);
|
|
21735
|
-
|
|
21933
|
+
const omitted = total - sliced.length;
|
|
21934
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
21736
21935
|
});
|
|
21737
21936
|
scope.get("/stale-events", async (req, reply) => {
|
|
21738
21937
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -21841,16 +22040,20 @@ function registerRoutes(scope, ctx) {
|
|
|
21841
22040
|
const filtered = events.filter(
|
|
21842
22041
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
21843
22042
|
);
|
|
21844
|
-
|
|
22043
|
+
const total = filtered.length;
|
|
22044
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
22045
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
22046
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
22047
|
+
const omitted = total - recent.length;
|
|
22048
|
+
return {
|
|
22049
|
+
count: recent.length,
|
|
22050
|
+
total,
|
|
22051
|
+
events: recent,
|
|
22052
|
+
...omitted > 0 ? { omitted } : {}
|
|
22053
|
+
};
|
|
21845
22054
|
};
|
|
21846
|
-
scope.get(
|
|
21847
|
-
|
|
21848
|
-
incidentHistoryHandler
|
|
21849
|
-
);
|
|
21850
|
-
scope.get(
|
|
21851
|
-
"/graph/incident-history/:nodeId",
|
|
21852
|
-
incidentHistoryHandler
|
|
21853
|
-
);
|
|
22055
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
22056
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
21854
22057
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
21855
22058
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
21856
22059
|
if (!proj) return;
|
|
@@ -28841,6 +29044,11 @@ function formatDivergenceLine(d) {
|
|
|
28841
29044
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
28842
29045
|
case "compat-violation":
|
|
28843
29046
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
29047
|
+
case "observed-symbol-mismatch": {
|
|
29048
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
29049
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
29050
|
+
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29051
|
+
}
|
|
28844
29052
|
}
|
|
28845
29053
|
}
|
|
28846
29054
|
async function runDivergences(client, input) {
|
|
@@ -28994,6 +29202,11 @@ function formatDivergenceLine2(d) {
|
|
|
28994
29202
|
return `\u26A0 divergence [host-mismatch] ${d.source} \u2192 ${d.target} declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
28995
29203
|
case "compat-violation":
|
|
28996
29204
|
return `\u26A0 divergence [compat-violation] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
29205
|
+
case "observed-symbol-mismatch": {
|
|
29206
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
29207
|
+
const member = d.symbol ? ` ${d.symbol}` : " a member";
|
|
29208
|
+
return `\u26A0 divergence [observed-symbol-mismatch] ${d.source}${at} reads${member} the runtime object does not have (${d.mismatchKind})`;
|
|
29209
|
+
}
|
|
28997
29210
|
}
|
|
28998
29211
|
}
|
|
28999
29212
|
function formatStaleLine(edgeId) {
|
|
@@ -29970,7 +30183,8 @@ async function runInit(opts) {
|
|
|
29970
30183
|
console.log(`snapshot: ${opts.outPath}`);
|
|
29971
30184
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
29972
30185
|
console.log("");
|
|
29973
|
-
const
|
|
30186
|
+
const summaryIncidents = await readErrorEvents(errorsPath);
|
|
30187
|
+
const divergenceResult = computeDivergences(graph, { incidents: summaryIncidents });
|
|
29974
30188
|
console.log(
|
|
29975
30189
|
renderValueForwardSummary({
|
|
29976
30190
|
graph,
|