@neat.is/core 0.9.3-dev.20260823 → 0.9.4
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-LRYPKC3B.js → chunk-IVVF37OU.js} +361 -54
- package/dist/chunk-IVVF37OU.js.map +1 -0
- package/dist/{chunk-WJGZYEUG.js → chunk-TGCWMMF6.js} +2 -2
- package/dist/cli.cjs +377 -59
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +360 -53
- 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 +360 -53
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +357 -51
- 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-TGCWMMF6.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -1925,6 +1925,47 @@ function classifyNode(ctx) {
|
|
|
1925
1925
|
function isVictimSeed(ctx) {
|
|
1926
1926
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1927
1927
|
}
|
|
1928
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
1929
|
+
"name resolution",
|
|
1930
|
+
"resolve host",
|
|
1931
|
+
"getaddrinfo",
|
|
1932
|
+
"enotfound",
|
|
1933
|
+
"connection refused",
|
|
1934
|
+
"econnrefused",
|
|
1935
|
+
"connection reset",
|
|
1936
|
+
"econnreset",
|
|
1937
|
+
"able to connect",
|
|
1938
|
+
"failed to connect",
|
|
1939
|
+
"cannot connect",
|
|
1940
|
+
"could not connect",
|
|
1941
|
+
"unable to connect",
|
|
1942
|
+
"connection timed out",
|
|
1943
|
+
"etimedout",
|
|
1944
|
+
"no route to host",
|
|
1945
|
+
"host unreachable",
|
|
1946
|
+
"network is unreachable",
|
|
1947
|
+
"connection closed"
|
|
1948
|
+
];
|
|
1949
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
1950
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
1951
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
1952
|
+
}
|
|
1953
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
1954
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
1955
|
+
if (!graph.hasNode(n)) continue;
|
|
1956
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1957
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1958
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1959
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (seedSource === "incident" && incidents) {
|
|
1963
|
+
for (const ev of incidents) {
|
|
1964
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1928
1969
|
function grainOf(graph, nodeId) {
|
|
1929
1970
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1930
1971
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -2088,7 +2129,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
2088
2129
|
const candidates = [];
|
|
2089
2130
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
2090
2131
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
2091
|
-
|
|
2132
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
2133
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
2092
2134
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
2093
2135
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
2094
2136
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -5414,6 +5456,64 @@ function latencyPercentiles(hist) {
|
|
|
5414
5456
|
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
5415
5457
|
}
|
|
5416
5458
|
|
|
5459
|
+
// src/stacktrace.ts
|
|
5460
|
+
init_cjs_shims();
|
|
5461
|
+
var FRAME_SHAPES = [
|
|
5462
|
+
// `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
|
|
5463
|
+
{ re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
|
|
5464
|
+
// `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
|
|
5465
|
+
{ re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
|
|
5466
|
+
// `at <path>:<line>:<col>` — anonymous call frame, most recent first.
|
|
5467
|
+
{ re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
|
|
5468
|
+
// `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
|
|
5469
|
+
{ re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
|
|
5470
|
+
];
|
|
5471
|
+
var VENDOR_MARKERS = [
|
|
5472
|
+
"node_modules",
|
|
5473
|
+
// dependency root
|
|
5474
|
+
"site-packages",
|
|
5475
|
+
// installed-package root
|
|
5476
|
+
"dist-packages",
|
|
5477
|
+
// distro-packaged root
|
|
5478
|
+
"node:"
|
|
5479
|
+
// runtime-internal module scheme (a stdlib/runtime-root frame)
|
|
5480
|
+
];
|
|
5481
|
+
function matchFrame(line) {
|
|
5482
|
+
for (const shape of FRAME_SHAPES) {
|
|
5483
|
+
const m = shape.re.exec(line);
|
|
5484
|
+
if (!m) continue;
|
|
5485
|
+
const file = m[shape.file];
|
|
5486
|
+
const lineNo = Number(m[shape.line]);
|
|
5487
|
+
if (!file || !Number.isFinite(lineNo)) continue;
|
|
5488
|
+
const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
|
|
5489
|
+
return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
|
|
5490
|
+
}
|
|
5491
|
+
return null;
|
|
5492
|
+
}
|
|
5493
|
+
function isApplicationFrame(file) {
|
|
5494
|
+
if (file.startsWith("<")) return false;
|
|
5495
|
+
const norm = file.split("\\").join("/");
|
|
5496
|
+
for (const marker of VENDOR_MARKERS) {
|
|
5497
|
+
if (norm.includes(marker)) return false;
|
|
5498
|
+
}
|
|
5499
|
+
return true;
|
|
5500
|
+
}
|
|
5501
|
+
function deepestApplicationFrame(stacktrace) {
|
|
5502
|
+
if (!stacktrace) return null;
|
|
5503
|
+
const appFrames = [];
|
|
5504
|
+
for (const raw of stacktrace.split("\n")) {
|
|
5505
|
+
const matched = matchFrame(raw);
|
|
5506
|
+
if (!matched) continue;
|
|
5507
|
+
if (!isApplicationFrame(matched.frame.file)) continue;
|
|
5508
|
+
appFrames.push(matched);
|
|
5509
|
+
}
|
|
5510
|
+
if (appFrames.length === 0) return null;
|
|
5511
|
+
const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
|
|
5512
|
+
const oriented = appFrames.filter((f) => f.deepest === orientation);
|
|
5513
|
+
const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
|
|
5514
|
+
return chosen ? chosen.frame : null;
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5417
5517
|
// src/ingest.ts
|
|
5418
5518
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
5419
5519
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -6265,29 +6365,71 @@ async function appendConnectorIncident(errorsPath, input) {
|
|
|
6265
6365
|
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
|
|
6266
6366
|
await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
6267
6367
|
}
|
|
6268
|
-
function
|
|
6368
|
+
function landIncidentCallSite(span, callSite, trusted, graph) {
|
|
6369
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
6370
|
+
const canonicalService = serviceNodeName(graph, span) ?? span.service;
|
|
6371
|
+
const recovered = !trusted;
|
|
6372
|
+
if (graph) {
|
|
6373
|
+
const fusedFileId = (0, import_types8.fileId)(canonicalService, relPath);
|
|
6374
|
+
if (graph.hasNode(fusedFileId)) {
|
|
6375
|
+
const node = landObservedSymbol(
|
|
6376
|
+
graph,
|
|
6377
|
+
fusedFileId,
|
|
6378
|
+
canonicalService,
|
|
6379
|
+
relPath,
|
|
6380
|
+
{ ...callSite, relPath },
|
|
6381
|
+
false
|
|
6382
|
+
);
|
|
6383
|
+
return {
|
|
6384
|
+
affectedNode: node,
|
|
6385
|
+
...recovered ? {
|
|
6386
|
+
codeFilepath: relPath,
|
|
6387
|
+
...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
|
|
6388
|
+
} : {}
|
|
6389
|
+
};
|
|
6390
|
+
}
|
|
6391
|
+
if (recovered) return null;
|
|
6392
|
+
}
|
|
6393
|
+
return { affectedNode: (0, import_types8.fileId)(span.service, relPath) };
|
|
6394
|
+
}
|
|
6395
|
+
function serviceNodeName(graph, span) {
|
|
6396
|
+
if (!graph) return void 0;
|
|
6397
|
+
const sid = resolveFusedServiceId(graph, span.service, span.env);
|
|
6398
|
+
if (!graph.hasNode(sid)) return void 0;
|
|
6399
|
+
const node = graph.getNodeAttributes(sid);
|
|
6400
|
+
return typeof node.name === "string" ? node.name : void 0;
|
|
6401
|
+
}
|
|
6402
|
+
function stacktraceCallSite(span, serviceNode, scanPath) {
|
|
6403
|
+
const frame = deepestApplicationFrame(span.exception?.stacktrace);
|
|
6404
|
+
if (!frame) return null;
|
|
6405
|
+
const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
|
|
6406
|
+
if (!relPath) return null;
|
|
6407
|
+
return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
|
|
6408
|
+
}
|
|
6409
|
+
function incidentLocus(span, graph, scanPath) {
|
|
6269
6410
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
|
|
6270
6411
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
6271
6412
|
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
6272
6413
|
if (callSite) {
|
|
6273
|
-
const
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
fusedFileId,
|
|
6281
|
-
canonicalService,
|
|
6282
|
-
relPath,
|
|
6283
|
-
{ ...callSite, relPath },
|
|
6284
|
-
false
|
|
6285
|
-
);
|
|
6286
|
-
}
|
|
6414
|
+
const landed = landIncidentCallSite(span, callSite, true, graph);
|
|
6415
|
+
if (landed) return landed;
|
|
6416
|
+
} else {
|
|
6417
|
+
const recovered = stacktraceCallSite(span, serviceNode, scanPath);
|
|
6418
|
+
if (recovered) {
|
|
6419
|
+
const landed = landIncidentCallSite(span, recovered, false, graph);
|
|
6420
|
+
if (landed) return landed;
|
|
6287
6421
|
}
|
|
6288
|
-
return (0, import_types8.fileId)(span.service, relPath);
|
|
6289
6422
|
}
|
|
6290
|
-
return sid;
|
|
6423
|
+
return { affectedNode: sid };
|
|
6424
|
+
}
|
|
6425
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
6426
|
+
return incidentLocus(span, graph, scanPath).affectedNode;
|
|
6427
|
+
}
|
|
6428
|
+
function withRecoveredCodeAttrs(attrs, locus) {
|
|
6429
|
+
if (locus.codeFilepath === void 0) return attrs;
|
|
6430
|
+
attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
|
|
6431
|
+
if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
|
|
6432
|
+
return attrs;
|
|
6291
6433
|
}
|
|
6292
6434
|
function sanitizeAttributes(attrs) {
|
|
6293
6435
|
const out = {};
|
|
@@ -6300,7 +6442,8 @@ function sanitizeAttributes(attrs) {
|
|
|
6300
6442
|
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
6301
6443
|
if (span.statusCode !== 2) return null;
|
|
6302
6444
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6303
|
-
const
|
|
6445
|
+
const locus = incidentLocus(span, graph, scanPath);
|
|
6446
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6304
6447
|
return {
|
|
6305
6448
|
id: `${span.traceId}:${span.spanId}`,
|
|
6306
6449
|
timestamp: ts,
|
|
@@ -6311,7 +6454,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
|
6311
6454
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
6312
6455
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6313
6456
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6314
|
-
affectedNode:
|
|
6457
|
+
affectedNode: locus.affectedNode
|
|
6315
6458
|
};
|
|
6316
6459
|
}
|
|
6317
6460
|
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
@@ -6345,7 +6488,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
6345
6488
|
await appendErrorEvent(ctx, ev);
|
|
6346
6489
|
}
|
|
6347
6490
|
async function recordExceptionIncident(ctx, span, ts) {
|
|
6348
|
-
const
|
|
6491
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
6492
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6349
6493
|
const ev = {
|
|
6350
6494
|
id: `${span.traceId}:${span.spanId}`,
|
|
6351
6495
|
timestamp: ts,
|
|
@@ -6356,7 +6500,7 @@ async function recordExceptionIncident(ctx, span, ts) {
|
|
|
6356
6500
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
6357
6501
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6358
6502
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6359
|
-
affectedNode:
|
|
6503
|
+
affectedNode: locus.affectedNode
|
|
6360
6504
|
};
|
|
6361
6505
|
await appendErrorEvent(ctx, ev);
|
|
6362
6506
|
}
|
|
@@ -6659,7 +6803,8 @@ async function handleSpan(ctx, span) {
|
|
|
6659
6803
|
if (span.statusCode === 2) {
|
|
6660
6804
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
6661
6805
|
if (ctx.writeErrorEventInline !== false) {
|
|
6662
|
-
const
|
|
6806
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
6807
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6663
6808
|
const ev = {
|
|
6664
6809
|
id: `${span.traceId}:${span.spanId}`,
|
|
6665
6810
|
timestamp: ts,
|
|
@@ -6671,10 +6816,11 @@ async function handleSpan(ctx, span) {
|
|
|
6671
6816
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6672
6817
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6673
6818
|
// Attribute to where the failure originated — the symbol / file / service
|
|
6674
|
-
// the throwing span named (incidentAffectedNode
|
|
6675
|
-
//
|
|
6676
|
-
//
|
|
6677
|
-
|
|
6819
|
+
// the throwing span named (incidentAffectedNode / ADR-191, extended to
|
|
6820
|
+
// recover the locus from the stacktrace when the span stamped no code.*
|
|
6821
|
+
// attrs, ADR-216) — the same source-based attribution the durable
|
|
6822
|
+
// receiver write uses, not the outbound edge target this span minted.
|
|
6823
|
+
affectedNode: locus.affectedNode
|
|
6678
6824
|
};
|
|
6679
6825
|
await appendErrorEvent(ctx, ev);
|
|
6680
6826
|
}
|
|
@@ -6848,15 +6994,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6848
6994
|
clearInterval(interval);
|
|
6849
6995
|
};
|
|
6850
6996
|
}
|
|
6851
|
-
|
|
6997
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
6998
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
6999
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
7000
|
+
const handle = await import_node_fs7.promises.open(errorsPath, "r");
|
|
6852
7001
|
try {
|
|
6853
|
-
const
|
|
6854
|
-
|
|
6855
|
-
|
|
7002
|
+
const { size } = await handle.stat();
|
|
7003
|
+
if (size <= maxBytes) {
|
|
7004
|
+
return (await handle.readFile()).toString("utf8");
|
|
7005
|
+
}
|
|
7006
|
+
const buf = Buffer.alloc(maxBytes);
|
|
7007
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
7008
|
+
const raw = buf.toString("utf8");
|
|
7009
|
+
const firstNewline = raw.indexOf("\n");
|
|
7010
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
7011
|
+
} finally {
|
|
7012
|
+
await handle.close();
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
7016
|
+
let raw;
|
|
7017
|
+
try {
|
|
7018
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6856
7019
|
} catch (err) {
|
|
6857
7020
|
if (err.code === "ENOENT") return [];
|
|
6858
7021
|
throw err;
|
|
6859
7022
|
}
|
|
7023
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
7024
|
+
const deduped = dedupeIncidents(events);
|
|
7025
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
7026
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6860
7027
|
}
|
|
6861
7028
|
function isSynthesizedHttpIncident(ev) {
|
|
6862
7029
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -16443,6 +16610,120 @@ function detectColumnDrift(node) {
|
|
|
16443
16610
|
}
|
|
16444
16611
|
return out;
|
|
16445
16612
|
}
|
|
16613
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16614
|
+
{
|
|
16615
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
16616
|
+
kind: "missing-attribute",
|
|
16617
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
16618
|
+
},
|
|
16619
|
+
{
|
|
16620
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
16621
|
+
kind: "missing-field",
|
|
16622
|
+
patterns: [
|
|
16623
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
16624
|
+
]
|
|
16625
|
+
},
|
|
16626
|
+
{
|
|
16627
|
+
// "has no property X", "no property named X".
|
|
16628
|
+
kind: "missing-property",
|
|
16629
|
+
patterns: [
|
|
16630
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
16631
|
+
]
|
|
16632
|
+
},
|
|
16633
|
+
{
|
|
16634
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
16635
|
+
kind: "missing-column",
|
|
16636
|
+
patterns: [
|
|
16637
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16638
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16639
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
16640
|
+
]
|
|
16641
|
+
},
|
|
16642
|
+
{
|
|
16643
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
16644
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
16645
|
+
// mismatch) does not get miscategorised here.
|
|
16646
|
+
kind: "undefined-method",
|
|
16647
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
16648
|
+
}
|
|
16649
|
+
];
|
|
16650
|
+
function classifySymbolMismatch(message) {
|
|
16651
|
+
for (const entry of SYMBOL_MISMATCH_PATTERNS) {
|
|
16652
|
+
for (const re of entry.patterns) {
|
|
16653
|
+
const m = re.exec(message);
|
|
16654
|
+
if (m) {
|
|
16655
|
+
const captured = m[1];
|
|
16656
|
+
return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
|
|
16657
|
+
}
|
|
16658
|
+
}
|
|
16659
|
+
}
|
|
16660
|
+
return null;
|
|
16661
|
+
}
|
|
16662
|
+
function symbolLocus(graph, ev) {
|
|
16663
|
+
const attrs = ev.attributes ?? {};
|
|
16664
|
+
const filepath = codeFilepathOf(attrs);
|
|
16665
|
+
const lineno = codeLinenoOf(attrs);
|
|
16666
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
16667
|
+
const affected = ev.affectedNode;
|
|
16668
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
16669
|
+
const affectedIsCode = affectedInGraph && ((0, import_types58.parseSymbolId)(affected) !== null || (0, import_types58.parseFileId)(affected) !== null);
|
|
16670
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
16671
|
+
if (!location) return null;
|
|
16672
|
+
if (affectedInGraph) return { node: affected, location };
|
|
16673
|
+
const svc = (0, import_types58.serviceId)(ev.service);
|
|
16674
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
16675
|
+
return null;
|
|
16676
|
+
}
|
|
16677
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
16678
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
16679
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16680
|
+
for (const ev of incidents) {
|
|
16681
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
16682
|
+
if (!classified) continue;
|
|
16683
|
+
const locus = symbolLocus(graph, ev);
|
|
16684
|
+
if (!locus) continue;
|
|
16685
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
16686
|
+
const existing = groups.get(key);
|
|
16687
|
+
if (!existing) {
|
|
16688
|
+
groups.set(key, {
|
|
16689
|
+
node: locus.node,
|
|
16690
|
+
kind: classified.kind,
|
|
16691
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
16692
|
+
...locus.location ? { location: locus.location } : {},
|
|
16693
|
+
latest: ev,
|
|
16694
|
+
count: 1
|
|
16695
|
+
});
|
|
16696
|
+
} else {
|
|
16697
|
+
existing.count += 1;
|
|
16698
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
16699
|
+
existing.latest = ev;
|
|
16700
|
+
if (locus.location) existing.location = locus.location;
|
|
16701
|
+
}
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16704
|
+
const out = [];
|
|
16705
|
+
for (const g of groups.values()) {
|
|
16706
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
16707
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
16708
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
16709
|
+
out.push({
|
|
16710
|
+
type: "observed-symbol-mismatch",
|
|
16711
|
+
source: g.node,
|
|
16712
|
+
target: g.node,
|
|
16713
|
+
mismatchKind: g.kind,
|
|
16714
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
16715
|
+
...g.location ? { location: g.location } : {},
|
|
16716
|
+
provenance: import_types58.Provenance.INFERRED,
|
|
16717
|
+
incidentId: g.latest.id,
|
|
16718
|
+
errorMessage: g.latest.errorMessage,
|
|
16719
|
+
incidentCount: g.count,
|
|
16720
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
16721
|
+
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.`,
|
|
16722
|
+
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."
|
|
16723
|
+
});
|
|
16724
|
+
}
|
|
16725
|
+
return out;
|
|
16726
|
+
}
|
|
16446
16727
|
function involvesNode(d, nodeId) {
|
|
16447
16728
|
return d.source === nodeId || d.target === nodeId;
|
|
16448
16729
|
}
|
|
@@ -16528,6 +16809,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16528
16809
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
16529
16810
|
}
|
|
16530
16811
|
});
|
|
16812
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
16813
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
16814
|
+
}
|
|
16531
16815
|
const reconciled = suppressHostMismatchHalves(all);
|
|
16532
16816
|
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
16533
16817
|
let filtered = dampened;
|
|
@@ -16548,7 +16832,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16548
16832
|
"missing-observed": 1,
|
|
16549
16833
|
"version-mismatch": 2,
|
|
16550
16834
|
"host-mismatch": 3,
|
|
16551
|
-
"compat-violation": 4
|
|
16835
|
+
"compat-violation": 4,
|
|
16836
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
16837
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
16838
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
16839
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
16840
|
+
"observed-symbol-mismatch": 5
|
|
16552
16841
|
};
|
|
16553
16842
|
filtered.sort((a, b) => {
|
|
16554
16843
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -16559,7 +16848,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16559
16848
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
16560
16849
|
const ac = "column" in a && a.column ? a.column : "";
|
|
16561
16850
|
const bc = "column" in b && b.column ? b.column : "";
|
|
16562
|
-
return ac.localeCompare(bc);
|
|
16851
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
16852
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
16853
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
16854
|
+
return asym.localeCompare(bsym);
|
|
16563
16855
|
});
|
|
16564
16856
|
return import_types58.DivergenceResultSchema.parse({
|
|
16565
16857
|
divergences: filtered,
|
|
@@ -16986,10 +17278,15 @@ function divergenceLine(d) {
|
|
|
16986
17278
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16987
17279
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16988
17280
|
}
|
|
17281
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17282
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17283
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17284
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17285
|
+
}
|
|
16989
17286
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16990
17287
|
}
|
|
16991
|
-
function buildDivergenceSection(graph, node) {
|
|
16992
|
-
const result = computeDivergences(graph, { node });
|
|
17288
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17289
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
16993
17290
|
if (result.totalAffected === 0) return null;
|
|
16994
17291
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16995
17292
|
text: divergenceLine(d),
|
|
@@ -17002,8 +17299,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
17002
17299
|
facts
|
|
17003
17300
|
};
|
|
17004
17301
|
}
|
|
17005
|
-
function buildGlobalDivergenceSection(graph) {
|
|
17006
|
-
const result = computeDivergences(graph);
|
|
17302
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17303
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17007
17304
|
if (result.totalAffected === 0) {
|
|
17008
17305
|
return {
|
|
17009
17306
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -17101,7 +17398,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17101
17398
|
}))
|
|
17102
17399
|
});
|
|
17103
17400
|
}
|
|
17104
|
-
const div = computeDivergences(graph);
|
|
17401
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17105
17402
|
sections.push({
|
|
17106
17403
|
heading: "Divergences",
|
|
17107
17404
|
facts: [
|
|
@@ -17115,7 +17412,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17115
17412
|
function buildGlobalSections(intent, graph, incidents) {
|
|
17116
17413
|
switch (intent) {
|
|
17117
17414
|
case "divergence":
|
|
17118
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17415
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
17119
17416
|
case "incidents":
|
|
17120
17417
|
return [buildGlobalIncidentsSection(incidents)];
|
|
17121
17418
|
case "overview":
|
|
@@ -17146,7 +17443,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
17146
17443
|
case "incidents":
|
|
17147
17444
|
return buildIncidentsSection(node, incidents);
|
|
17148
17445
|
case "divergence":
|
|
17149
|
-
return buildDivergenceSection(graph, node);
|
|
17446
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
17150
17447
|
}
|
|
17151
17448
|
}
|
|
17152
17449
|
function summarizeGlobal(intent, sections) {
|
|
@@ -21131,6 +21428,8 @@ async function startConnectorPolling(input) {
|
|
|
21131
21428
|
}
|
|
21132
21429
|
|
|
21133
21430
|
// src/api.ts
|
|
21431
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
21432
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
21134
21433
|
function serializeGraph(graph) {
|
|
21135
21434
|
const nodes = [];
|
|
21136
21435
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -21310,10 +21609,13 @@ function registerRoutes(scope, ctx) {
|
|
|
21310
21609
|
}
|
|
21311
21610
|
minConfidence = n;
|
|
21312
21611
|
}
|
|
21612
|
+
const epath = errorsPathFor(proj);
|
|
21613
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
21313
21614
|
return computeDivergences(proj.graph, {
|
|
21314
21615
|
...typeFilter ? { type: typeFilter } : {},
|
|
21315
21616
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
21316
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
21617
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
21618
|
+
incidents
|
|
21317
21619
|
});
|
|
21318
21620
|
});
|
|
21319
21621
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -21323,10 +21625,11 @@ function registerRoutes(scope, ctx) {
|
|
|
21323
21625
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
21324
21626
|
const events = await readErrorEvents(epath);
|
|
21325
21627
|
const total = events.length;
|
|
21326
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
21327
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21628
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21629
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21328
21630
|
const sliced = events.slice(0, safeLimit);
|
|
21329
|
-
|
|
21631
|
+
const omitted = total - sliced.length;
|
|
21632
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
21330
21633
|
});
|
|
21331
21634
|
scope.get("/stale-events", async (req, reply) => {
|
|
21332
21635
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -21435,16 +21738,20 @@ function registerRoutes(scope, ctx) {
|
|
|
21435
21738
|
const filtered = events.filter(
|
|
21436
21739
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
21437
21740
|
);
|
|
21438
|
-
|
|
21741
|
+
const total = filtered.length;
|
|
21742
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21743
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21744
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
21745
|
+
const omitted = total - recent.length;
|
|
21746
|
+
return {
|
|
21747
|
+
count: recent.length,
|
|
21748
|
+
total,
|
|
21749
|
+
events: recent,
|
|
21750
|
+
...omitted > 0 ? { omitted } : {}
|
|
21751
|
+
};
|
|
21439
21752
|
};
|
|
21440
|
-
scope.get(
|
|
21441
|
-
|
|
21442
|
-
incidentHistoryHandler
|
|
21443
|
-
);
|
|
21444
|
-
scope.get(
|
|
21445
|
-
"/graph/incident-history/:nodeId",
|
|
21446
|
-
incidentHistoryHandler
|
|
21447
|
-
);
|
|
21753
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21754
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
21448
21755
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
21449
21756
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
21450
21757
|
if (!proj) return;
|