@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.d.cts
CHANGED
|
@@ -441,7 +441,9 @@ interface StalenessLoopOptions {
|
|
|
441
441
|
onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
|
|
442
442
|
}
|
|
443
443
|
declare function startStalenessLoop(graph: NeatGraph, options?: StalenessLoopOptions): () => void;
|
|
444
|
-
declare function readErrorEvents(errorsPath: string
|
|
444
|
+
declare function readErrorEvents(errorsPath: string, opts?: {
|
|
445
|
+
limit?: number;
|
|
446
|
+
}): Promise<ErrorEvent[]>;
|
|
445
447
|
|
|
446
448
|
declare function confidenceForEdge(edge: GraphEdge, now?: number): number;
|
|
447
449
|
declare function getBlastRadius(graph: NeatGraph, nodeId: string, maxDepth?: number): BlastRadiusResult;
|
package/dist/index.d.ts
CHANGED
|
@@ -441,7 +441,9 @@ interface StalenessLoopOptions {
|
|
|
441
441
|
onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
|
|
442
442
|
}
|
|
443
443
|
declare function startStalenessLoop(graph: NeatGraph, options?: StalenessLoopOptions): () => void;
|
|
444
|
-
declare function readErrorEvents(errorsPath: string
|
|
444
|
+
declare function readErrorEvents(errorsPath: string, opts?: {
|
|
445
|
+
limit?: number;
|
|
446
|
+
}): Promise<ErrorEvent[]>;
|
|
445
447
|
|
|
446
448
|
declare function confidenceForEdge(edge: GraphEdge, now?: number): number;
|
|
447
449
|
declare function getBlastRadius(graph: NeatGraph, nodeId: string, maxDepth?: number): BlastRadiusResult;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-TGCWMMF6.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
thresholdForEdgeType,
|
|
38
38
|
touchLastSeen,
|
|
39
39
|
writeAtomically
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-IVVF37OU.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
43
|
} from "./chunk-ERE47MCR.js";
|
package/dist/neatd.cjs
CHANGED
|
@@ -1888,6 +1888,47 @@ function classifyNode(ctx) {
|
|
|
1888
1888
|
function isVictimSeed(ctx) {
|
|
1889
1889
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1890
1890
|
}
|
|
1891
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
1892
|
+
"name resolution",
|
|
1893
|
+
"resolve host",
|
|
1894
|
+
"getaddrinfo",
|
|
1895
|
+
"enotfound",
|
|
1896
|
+
"connection refused",
|
|
1897
|
+
"econnrefused",
|
|
1898
|
+
"connection reset",
|
|
1899
|
+
"econnreset",
|
|
1900
|
+
"able to connect",
|
|
1901
|
+
"failed to connect",
|
|
1902
|
+
"cannot connect",
|
|
1903
|
+
"could not connect",
|
|
1904
|
+
"unable to connect",
|
|
1905
|
+
"connection timed out",
|
|
1906
|
+
"etimedout",
|
|
1907
|
+
"no route to host",
|
|
1908
|
+
"host unreachable",
|
|
1909
|
+
"network is unreachable",
|
|
1910
|
+
"connection closed"
|
|
1911
|
+
];
|
|
1912
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
1913
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
1914
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
1915
|
+
}
|
|
1916
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
1917
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
1918
|
+
if (!graph.hasNode(n)) continue;
|
|
1919
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1920
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1921
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1922
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (seedSource === "incident" && incidents) {
|
|
1926
|
+
for (const ev of incidents) {
|
|
1927
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1891
1932
|
function grainOf(graph, nodeId) {
|
|
1892
1933
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1893
1934
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -2051,7 +2092,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
2051
2092
|
const candidates = [];
|
|
2052
2093
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
2053
2094
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
2054
|
-
|
|
2095
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
2096
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
2055
2097
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
2056
2098
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
2057
2099
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -5377,6 +5419,64 @@ function latencyPercentiles(hist) {
|
|
|
5377
5419
|
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
5378
5420
|
}
|
|
5379
5421
|
|
|
5422
|
+
// src/stacktrace.ts
|
|
5423
|
+
init_cjs_shims();
|
|
5424
|
+
var FRAME_SHAPES = [
|
|
5425
|
+
// `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
|
|
5426
|
+
{ re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
|
|
5427
|
+
// `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
|
|
5428
|
+
{ re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
|
|
5429
|
+
// `at <path>:<line>:<col>` — anonymous call frame, most recent first.
|
|
5430
|
+
{ re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
|
|
5431
|
+
// `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
|
|
5432
|
+
{ re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
|
|
5433
|
+
];
|
|
5434
|
+
var VENDOR_MARKERS = [
|
|
5435
|
+
"node_modules",
|
|
5436
|
+
// dependency root
|
|
5437
|
+
"site-packages",
|
|
5438
|
+
// installed-package root
|
|
5439
|
+
"dist-packages",
|
|
5440
|
+
// distro-packaged root
|
|
5441
|
+
"node:"
|
|
5442
|
+
// runtime-internal module scheme (a stdlib/runtime-root frame)
|
|
5443
|
+
];
|
|
5444
|
+
function matchFrame(line) {
|
|
5445
|
+
for (const shape of FRAME_SHAPES) {
|
|
5446
|
+
const m = shape.re.exec(line);
|
|
5447
|
+
if (!m) continue;
|
|
5448
|
+
const file = m[shape.file];
|
|
5449
|
+
const lineNo = Number(m[shape.line]);
|
|
5450
|
+
if (!file || !Number.isFinite(lineNo)) continue;
|
|
5451
|
+
const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
|
|
5452
|
+
return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
|
|
5453
|
+
}
|
|
5454
|
+
return null;
|
|
5455
|
+
}
|
|
5456
|
+
function isApplicationFrame(file) {
|
|
5457
|
+
if (file.startsWith("<")) return false;
|
|
5458
|
+
const norm = file.split("\\").join("/");
|
|
5459
|
+
for (const marker of VENDOR_MARKERS) {
|
|
5460
|
+
if (norm.includes(marker)) return false;
|
|
5461
|
+
}
|
|
5462
|
+
return true;
|
|
5463
|
+
}
|
|
5464
|
+
function deepestApplicationFrame(stacktrace) {
|
|
5465
|
+
if (!stacktrace) return null;
|
|
5466
|
+
const appFrames = [];
|
|
5467
|
+
for (const raw of stacktrace.split("\n")) {
|
|
5468
|
+
const matched = matchFrame(raw);
|
|
5469
|
+
if (!matched) continue;
|
|
5470
|
+
if (!isApplicationFrame(matched.frame.file)) continue;
|
|
5471
|
+
appFrames.push(matched);
|
|
5472
|
+
}
|
|
5473
|
+
if (appFrames.length === 0) return null;
|
|
5474
|
+
const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
|
|
5475
|
+
const oriented = appFrames.filter((f) => f.deepest === orientation);
|
|
5476
|
+
const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
|
|
5477
|
+
return chosen ? chosen.frame : null;
|
|
5478
|
+
}
|
|
5479
|
+
|
|
5380
5480
|
// src/ingest.ts
|
|
5381
5481
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
5382
5482
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -6228,29 +6328,71 @@ async function appendConnectorIncident(errorsPath, input) {
|
|
|
6228
6328
|
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
|
|
6229
6329
|
await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
6230
6330
|
}
|
|
6231
|
-
function
|
|
6331
|
+
function landIncidentCallSite(span, callSite, trusted, graph) {
|
|
6332
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
6333
|
+
const canonicalService = serviceNodeName(graph, span) ?? span.service;
|
|
6334
|
+
const recovered = !trusted;
|
|
6335
|
+
if (graph) {
|
|
6336
|
+
const fusedFileId = (0, import_types8.fileId)(canonicalService, relPath);
|
|
6337
|
+
if (graph.hasNode(fusedFileId)) {
|
|
6338
|
+
const node = landObservedSymbol(
|
|
6339
|
+
graph,
|
|
6340
|
+
fusedFileId,
|
|
6341
|
+
canonicalService,
|
|
6342
|
+
relPath,
|
|
6343
|
+
{ ...callSite, relPath },
|
|
6344
|
+
false
|
|
6345
|
+
);
|
|
6346
|
+
return {
|
|
6347
|
+
affectedNode: node,
|
|
6348
|
+
...recovered ? {
|
|
6349
|
+
codeFilepath: relPath,
|
|
6350
|
+
...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
|
|
6351
|
+
} : {}
|
|
6352
|
+
};
|
|
6353
|
+
}
|
|
6354
|
+
if (recovered) return null;
|
|
6355
|
+
}
|
|
6356
|
+
return { affectedNode: (0, import_types8.fileId)(span.service, relPath) };
|
|
6357
|
+
}
|
|
6358
|
+
function serviceNodeName(graph, span) {
|
|
6359
|
+
if (!graph) return void 0;
|
|
6360
|
+
const sid = resolveFusedServiceId(graph, span.service, span.env);
|
|
6361
|
+
if (!graph.hasNode(sid)) return void 0;
|
|
6362
|
+
const node = graph.getNodeAttributes(sid);
|
|
6363
|
+
return typeof node.name === "string" ? node.name : void 0;
|
|
6364
|
+
}
|
|
6365
|
+
function stacktraceCallSite(span, serviceNode, scanPath) {
|
|
6366
|
+
const frame = deepestApplicationFrame(span.exception?.stacktrace);
|
|
6367
|
+
if (!frame) return null;
|
|
6368
|
+
const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
|
|
6369
|
+
if (!relPath) return null;
|
|
6370
|
+
return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
|
|
6371
|
+
}
|
|
6372
|
+
function incidentLocus(span, graph, scanPath) {
|
|
6232
6373
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
|
|
6233
6374
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
6234
6375
|
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
6235
6376
|
if (callSite) {
|
|
6236
|
-
const
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
fusedFileId,
|
|
6244
|
-
canonicalService,
|
|
6245
|
-
relPath,
|
|
6246
|
-
{ ...callSite, relPath },
|
|
6247
|
-
false
|
|
6248
|
-
);
|
|
6249
|
-
}
|
|
6377
|
+
const landed = landIncidentCallSite(span, callSite, true, graph);
|
|
6378
|
+
if (landed) return landed;
|
|
6379
|
+
} else {
|
|
6380
|
+
const recovered = stacktraceCallSite(span, serviceNode, scanPath);
|
|
6381
|
+
if (recovered) {
|
|
6382
|
+
const landed = landIncidentCallSite(span, recovered, false, graph);
|
|
6383
|
+
if (landed) return landed;
|
|
6250
6384
|
}
|
|
6251
|
-
return (0, import_types8.fileId)(span.service, relPath);
|
|
6252
6385
|
}
|
|
6253
|
-
return sid;
|
|
6386
|
+
return { affectedNode: sid };
|
|
6387
|
+
}
|
|
6388
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
6389
|
+
return incidentLocus(span, graph, scanPath).affectedNode;
|
|
6390
|
+
}
|
|
6391
|
+
function withRecoveredCodeAttrs(attrs, locus) {
|
|
6392
|
+
if (locus.codeFilepath === void 0) return attrs;
|
|
6393
|
+
attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
|
|
6394
|
+
if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
|
|
6395
|
+
return attrs;
|
|
6254
6396
|
}
|
|
6255
6397
|
function sanitizeAttributes(attrs) {
|
|
6256
6398
|
const out = {};
|
|
@@ -6263,7 +6405,8 @@ function sanitizeAttributes(attrs) {
|
|
|
6263
6405
|
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
6264
6406
|
if (span.statusCode !== 2) return null;
|
|
6265
6407
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6266
|
-
const
|
|
6408
|
+
const locus = incidentLocus(span, graph, scanPath);
|
|
6409
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6267
6410
|
return {
|
|
6268
6411
|
id: `${span.traceId}:${span.spanId}`,
|
|
6269
6412
|
timestamp: ts,
|
|
@@ -6274,7 +6417,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
|
6274
6417
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
6275
6418
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6276
6419
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6277
|
-
affectedNode:
|
|
6420
|
+
affectedNode: locus.affectedNode
|
|
6278
6421
|
};
|
|
6279
6422
|
}
|
|
6280
6423
|
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
@@ -6308,7 +6451,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
6308
6451
|
await appendErrorEvent(ctx, ev);
|
|
6309
6452
|
}
|
|
6310
6453
|
async function recordExceptionIncident(ctx, span, ts) {
|
|
6311
|
-
const
|
|
6454
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
6455
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6312
6456
|
const ev = {
|
|
6313
6457
|
id: `${span.traceId}:${span.spanId}`,
|
|
6314
6458
|
timestamp: ts,
|
|
@@ -6319,7 +6463,7 @@ async function recordExceptionIncident(ctx, span, ts) {
|
|
|
6319
6463
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
6320
6464
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6321
6465
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6322
|
-
affectedNode:
|
|
6466
|
+
affectedNode: locus.affectedNode
|
|
6323
6467
|
};
|
|
6324
6468
|
await appendErrorEvent(ctx, ev);
|
|
6325
6469
|
}
|
|
@@ -6622,7 +6766,8 @@ async function handleSpan(ctx, span) {
|
|
|
6622
6766
|
if (span.statusCode === 2) {
|
|
6623
6767
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
6624
6768
|
if (ctx.writeErrorEventInline !== false) {
|
|
6625
|
-
const
|
|
6769
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
6770
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6626
6771
|
const ev = {
|
|
6627
6772
|
id: `${span.traceId}:${span.spanId}`,
|
|
6628
6773
|
timestamp: ts,
|
|
@@ -6634,10 +6779,11 @@ async function handleSpan(ctx, span) {
|
|
|
6634
6779
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6635
6780
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6636
6781
|
// Attribute to where the failure originated — the symbol / file / service
|
|
6637
|
-
// the throwing span named (incidentAffectedNode
|
|
6638
|
-
//
|
|
6639
|
-
//
|
|
6640
|
-
|
|
6782
|
+
// the throwing span named (incidentAffectedNode / ADR-191, extended to
|
|
6783
|
+
// recover the locus from the stacktrace when the span stamped no code.*
|
|
6784
|
+
// attrs, ADR-216) — the same source-based attribution the durable
|
|
6785
|
+
// receiver write uses, not the outbound edge target this span minted.
|
|
6786
|
+
affectedNode: locus.affectedNode
|
|
6641
6787
|
};
|
|
6642
6788
|
await appendErrorEvent(ctx, ev);
|
|
6643
6789
|
}
|
|
@@ -6808,15 +6954,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6808
6954
|
clearInterval(interval);
|
|
6809
6955
|
};
|
|
6810
6956
|
}
|
|
6811
|
-
|
|
6957
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
6958
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
6959
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
6960
|
+
const handle = await import_node_fs7.promises.open(errorsPath, "r");
|
|
6812
6961
|
try {
|
|
6813
|
-
const
|
|
6814
|
-
|
|
6815
|
-
|
|
6962
|
+
const { size } = await handle.stat();
|
|
6963
|
+
if (size <= maxBytes) {
|
|
6964
|
+
return (await handle.readFile()).toString("utf8");
|
|
6965
|
+
}
|
|
6966
|
+
const buf = Buffer.alloc(maxBytes);
|
|
6967
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
6968
|
+
const raw = buf.toString("utf8");
|
|
6969
|
+
const firstNewline = raw.indexOf("\n");
|
|
6970
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
6971
|
+
} finally {
|
|
6972
|
+
await handle.close();
|
|
6973
|
+
}
|
|
6974
|
+
}
|
|
6975
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
6976
|
+
let raw;
|
|
6977
|
+
try {
|
|
6978
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6816
6979
|
} catch (err) {
|
|
6817
6980
|
if (err.code === "ENOENT") return [];
|
|
6818
6981
|
throw err;
|
|
6819
6982
|
}
|
|
6983
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6984
|
+
const deduped = dedupeIncidents(events);
|
|
6985
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
6986
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6820
6987
|
}
|
|
6821
6988
|
function isSynthesizedHttpIncident(ev) {
|
|
6822
6989
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -16455,6 +16622,120 @@ function detectColumnDrift(node) {
|
|
|
16455
16622
|
}
|
|
16456
16623
|
return out;
|
|
16457
16624
|
}
|
|
16625
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16626
|
+
{
|
|
16627
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
16628
|
+
kind: "missing-attribute",
|
|
16629
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
16630
|
+
},
|
|
16631
|
+
{
|
|
16632
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
16633
|
+
kind: "missing-field",
|
|
16634
|
+
patterns: [
|
|
16635
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
16636
|
+
]
|
|
16637
|
+
},
|
|
16638
|
+
{
|
|
16639
|
+
// "has no property X", "no property named X".
|
|
16640
|
+
kind: "missing-property",
|
|
16641
|
+
patterns: [
|
|
16642
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
16643
|
+
]
|
|
16644
|
+
},
|
|
16645
|
+
{
|
|
16646
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
16647
|
+
kind: "missing-column",
|
|
16648
|
+
patterns: [
|
|
16649
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16650
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16651
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
16652
|
+
]
|
|
16653
|
+
},
|
|
16654
|
+
{
|
|
16655
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
16656
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
16657
|
+
// mismatch) does not get miscategorised here.
|
|
16658
|
+
kind: "undefined-method",
|
|
16659
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
16660
|
+
}
|
|
16661
|
+
];
|
|
16662
|
+
function classifySymbolMismatch(message) {
|
|
16663
|
+
for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
|
|
16664
|
+
for (const re of entry2.patterns) {
|
|
16665
|
+
const m = re.exec(message);
|
|
16666
|
+
if (m) {
|
|
16667
|
+
const captured = m[1];
|
|
16668
|
+
return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
|
|
16669
|
+
}
|
|
16670
|
+
}
|
|
16671
|
+
}
|
|
16672
|
+
return null;
|
|
16673
|
+
}
|
|
16674
|
+
function symbolLocus(graph, ev) {
|
|
16675
|
+
const attrs = ev.attributes ?? {};
|
|
16676
|
+
const filepath = codeFilepathOf(attrs);
|
|
16677
|
+
const lineno = codeLinenoOf(attrs);
|
|
16678
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
16679
|
+
const affected = ev.affectedNode;
|
|
16680
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
16681
|
+
const affectedIsCode = affectedInGraph && ((0, import_types58.parseSymbolId)(affected) !== null || (0, import_types58.parseFileId)(affected) !== null);
|
|
16682
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
16683
|
+
if (!location) return null;
|
|
16684
|
+
if (affectedInGraph) return { node: affected, location };
|
|
16685
|
+
const svc = (0, import_types58.serviceId)(ev.service);
|
|
16686
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
16687
|
+
return null;
|
|
16688
|
+
}
|
|
16689
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
16690
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
16691
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16692
|
+
for (const ev of incidents) {
|
|
16693
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
16694
|
+
if (!classified) continue;
|
|
16695
|
+
const locus = symbolLocus(graph, ev);
|
|
16696
|
+
if (!locus) continue;
|
|
16697
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
16698
|
+
const existing = groups.get(key);
|
|
16699
|
+
if (!existing) {
|
|
16700
|
+
groups.set(key, {
|
|
16701
|
+
node: locus.node,
|
|
16702
|
+
kind: classified.kind,
|
|
16703
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
16704
|
+
...locus.location ? { location: locus.location } : {},
|
|
16705
|
+
latest: ev,
|
|
16706
|
+
count: 1
|
|
16707
|
+
});
|
|
16708
|
+
} else {
|
|
16709
|
+
existing.count += 1;
|
|
16710
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
16711
|
+
existing.latest = ev;
|
|
16712
|
+
if (locus.location) existing.location = locus.location;
|
|
16713
|
+
}
|
|
16714
|
+
}
|
|
16715
|
+
}
|
|
16716
|
+
const out = [];
|
|
16717
|
+
for (const g of groups.values()) {
|
|
16718
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
16719
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
16720
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
16721
|
+
out.push({
|
|
16722
|
+
type: "observed-symbol-mismatch",
|
|
16723
|
+
source: g.node,
|
|
16724
|
+
target: g.node,
|
|
16725
|
+
mismatchKind: g.kind,
|
|
16726
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
16727
|
+
...g.location ? { location: g.location } : {},
|
|
16728
|
+
provenance: import_types58.Provenance.INFERRED,
|
|
16729
|
+
incidentId: g.latest.id,
|
|
16730
|
+
errorMessage: g.latest.errorMessage,
|
|
16731
|
+
incidentCount: g.count,
|
|
16732
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
16733
|
+
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.`,
|
|
16734
|
+
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."
|
|
16735
|
+
});
|
|
16736
|
+
}
|
|
16737
|
+
return out;
|
|
16738
|
+
}
|
|
16458
16739
|
function involvesNode(d, nodeId) {
|
|
16459
16740
|
return d.source === nodeId || d.target === nodeId;
|
|
16460
16741
|
}
|
|
@@ -16540,6 +16821,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16540
16821
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
16541
16822
|
}
|
|
16542
16823
|
});
|
|
16824
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
16825
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
16826
|
+
}
|
|
16543
16827
|
const reconciled = suppressHostMismatchHalves(all);
|
|
16544
16828
|
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
16545
16829
|
let filtered = dampened;
|
|
@@ -16560,7 +16844,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16560
16844
|
"missing-observed": 1,
|
|
16561
16845
|
"version-mismatch": 2,
|
|
16562
16846
|
"host-mismatch": 3,
|
|
16563
|
-
"compat-violation": 4
|
|
16847
|
+
"compat-violation": 4,
|
|
16848
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
16849
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
16850
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
16851
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
16852
|
+
"observed-symbol-mismatch": 5
|
|
16564
16853
|
};
|
|
16565
16854
|
filtered.sort((a, b) => {
|
|
16566
16855
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -16571,7 +16860,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16571
16860
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
16572
16861
|
const ac = "column" in a && a.column ? a.column : "";
|
|
16573
16862
|
const bc = "column" in b && b.column ? b.column : "";
|
|
16574
|
-
return ac.localeCompare(bc);
|
|
16863
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
16864
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
16865
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
16866
|
+
return asym.localeCompare(bsym);
|
|
16575
16867
|
});
|
|
16576
16868
|
return import_types58.DivergenceResultSchema.parse({
|
|
16577
16869
|
divergences: filtered,
|
|
@@ -16998,10 +17290,15 @@ function divergenceLine(d) {
|
|
|
16998
17290
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16999
17291
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
17000
17292
|
}
|
|
17293
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17294
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17295
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17296
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17297
|
+
}
|
|
17001
17298
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
17002
17299
|
}
|
|
17003
|
-
function buildDivergenceSection(graph, node) {
|
|
17004
|
-
const result = computeDivergences(graph, { node });
|
|
17300
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17301
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
17005
17302
|
if (result.totalAffected === 0) return null;
|
|
17006
17303
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
17007
17304
|
text: divergenceLine(d),
|
|
@@ -17014,8 +17311,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
17014
17311
|
facts
|
|
17015
17312
|
};
|
|
17016
17313
|
}
|
|
17017
|
-
function buildGlobalDivergenceSection(graph) {
|
|
17018
|
-
const result = computeDivergences(graph);
|
|
17314
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17315
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17019
17316
|
if (result.totalAffected === 0) {
|
|
17020
17317
|
return {
|
|
17021
17318
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -17113,7 +17410,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17113
17410
|
}))
|
|
17114
17411
|
});
|
|
17115
17412
|
}
|
|
17116
|
-
const div = computeDivergences(graph);
|
|
17413
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17117
17414
|
sections.push({
|
|
17118
17415
|
heading: "Divergences",
|
|
17119
17416
|
facts: [
|
|
@@ -17127,7 +17424,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17127
17424
|
function buildGlobalSections(intent, graph, incidents) {
|
|
17128
17425
|
switch (intent) {
|
|
17129
17426
|
case "divergence":
|
|
17130
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17427
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
17131
17428
|
case "incidents":
|
|
17132
17429
|
return [buildGlobalIncidentsSection(incidents)];
|
|
17133
17430
|
case "overview":
|
|
@@ -17158,7 +17455,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
17158
17455
|
case "incidents":
|
|
17159
17456
|
return buildIncidentsSection(node, incidents);
|
|
17160
17457
|
case "divergence":
|
|
17161
|
-
return buildDivergenceSection(graph, node);
|
|
17458
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
17162
17459
|
}
|
|
17163
17460
|
}
|
|
17164
17461
|
function summarizeGlobal(intent, sections) {
|
|
@@ -21033,6 +21330,8 @@ async function startConnectorPolling(input) {
|
|
|
21033
21330
|
}
|
|
21034
21331
|
|
|
21035
21332
|
// src/api.ts
|
|
21333
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
21334
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
21036
21335
|
function serializeGraph(graph) {
|
|
21037
21336
|
const nodes = [];
|
|
21038
21337
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -21212,10 +21511,13 @@ function registerRoutes(scope, ctx) {
|
|
|
21212
21511
|
}
|
|
21213
21512
|
minConfidence = n;
|
|
21214
21513
|
}
|
|
21514
|
+
const epath = errorsPathFor(proj);
|
|
21515
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
21215
21516
|
return computeDivergences(proj.graph, {
|
|
21216
21517
|
...typeFilter ? { type: typeFilter } : {},
|
|
21217
21518
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
21218
|
-
...req2.query.node ? { node: req2.query.node } : {}
|
|
21519
|
+
...req2.query.node ? { node: req2.query.node } : {},
|
|
21520
|
+
incidents
|
|
21219
21521
|
});
|
|
21220
21522
|
});
|
|
21221
21523
|
scope.get("/incidents", async (req2, reply) => {
|
|
@@ -21225,10 +21527,11 @@ function registerRoutes(scope, ctx) {
|
|
|
21225
21527
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
21226
21528
|
const events = await readErrorEvents(epath);
|
|
21227
21529
|
const total = events.length;
|
|
21228
|
-
const limit = req2.query.limit ? Number(req2.query.limit) :
|
|
21229
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21530
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21531
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21230
21532
|
const sliced = events.slice(0, safeLimit);
|
|
21231
|
-
|
|
21533
|
+
const omitted = total - sliced.length;
|
|
21534
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
21232
21535
|
});
|
|
21233
21536
|
scope.get("/stale-events", async (req2, reply) => {
|
|
21234
21537
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -21337,16 +21640,20 @@ function registerRoutes(scope, ctx) {
|
|
|
21337
21640
|
const filtered = events.filter(
|
|
21338
21641
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
21339
21642
|
);
|
|
21340
|
-
|
|
21643
|
+
const total = filtered.length;
|
|
21644
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21645
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21646
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
21647
|
+
const omitted = total - recent.length;
|
|
21648
|
+
return {
|
|
21649
|
+
count: recent.length,
|
|
21650
|
+
total,
|
|
21651
|
+
events: recent,
|
|
21652
|
+
...omitted > 0 ? { omitted } : {}
|
|
21653
|
+
};
|
|
21341
21654
|
};
|
|
21342
|
-
scope.get(
|
|
21343
|
-
|
|
21344
|
-
incidentHistoryHandler
|
|
21345
|
-
);
|
|
21346
|
-
scope.get(
|
|
21347
|
-
"/graph/incident-history/:nodeId",
|
|
21348
|
-
incidentHistoryHandler
|
|
21349
|
-
);
|
|
21655
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21656
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
21350
21657
|
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
21351
21658
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
21352
21659
|
if (!proj) return;
|