@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/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` : "";
|
|
@@ -5434,6 +5476,64 @@ function latencyPercentiles(hist) {
|
|
|
5434
5476
|
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
5435
5477
|
}
|
|
5436
5478
|
|
|
5479
|
+
// src/stacktrace.ts
|
|
5480
|
+
init_cjs_shims();
|
|
5481
|
+
var FRAME_SHAPES = [
|
|
5482
|
+
// `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
|
|
5483
|
+
{ re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
|
|
5484
|
+
// `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
|
|
5485
|
+
{ re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
|
|
5486
|
+
// `at <path>:<line>:<col>` — anonymous call frame, most recent first.
|
|
5487
|
+
{ re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
|
|
5488
|
+
// `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
|
|
5489
|
+
{ re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
|
|
5490
|
+
];
|
|
5491
|
+
var VENDOR_MARKERS = [
|
|
5492
|
+
"node_modules",
|
|
5493
|
+
// dependency root
|
|
5494
|
+
"site-packages",
|
|
5495
|
+
// installed-package root
|
|
5496
|
+
"dist-packages",
|
|
5497
|
+
// distro-packaged root
|
|
5498
|
+
"node:"
|
|
5499
|
+
// runtime-internal module scheme (a stdlib/runtime-root frame)
|
|
5500
|
+
];
|
|
5501
|
+
function matchFrame(line) {
|
|
5502
|
+
for (const shape of FRAME_SHAPES) {
|
|
5503
|
+
const m = shape.re.exec(line);
|
|
5504
|
+
if (!m) continue;
|
|
5505
|
+
const file = m[shape.file];
|
|
5506
|
+
const lineNo = Number(m[shape.line]);
|
|
5507
|
+
if (!file || !Number.isFinite(lineNo)) continue;
|
|
5508
|
+
const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
|
|
5509
|
+
return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
|
|
5510
|
+
}
|
|
5511
|
+
return null;
|
|
5512
|
+
}
|
|
5513
|
+
function isApplicationFrame(file) {
|
|
5514
|
+
if (file.startsWith("<")) return false;
|
|
5515
|
+
const norm = file.split("\\").join("/");
|
|
5516
|
+
for (const marker of VENDOR_MARKERS) {
|
|
5517
|
+
if (norm.includes(marker)) return false;
|
|
5518
|
+
}
|
|
5519
|
+
return true;
|
|
5520
|
+
}
|
|
5521
|
+
function deepestApplicationFrame(stacktrace) {
|
|
5522
|
+
if (!stacktrace) return null;
|
|
5523
|
+
const appFrames = [];
|
|
5524
|
+
for (const raw of stacktrace.split("\n")) {
|
|
5525
|
+
const matched = matchFrame(raw);
|
|
5526
|
+
if (!matched) continue;
|
|
5527
|
+
if (!isApplicationFrame(matched.frame.file)) continue;
|
|
5528
|
+
appFrames.push(matched);
|
|
5529
|
+
}
|
|
5530
|
+
if (appFrames.length === 0) return null;
|
|
5531
|
+
const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
|
|
5532
|
+
const oriented = appFrames.filter((f) => f.deepest === orientation);
|
|
5533
|
+
const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
|
|
5534
|
+
return chosen ? chosen.frame : null;
|
|
5535
|
+
}
|
|
5536
|
+
|
|
5437
5537
|
// src/ingest.ts
|
|
5438
5538
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
5439
5539
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -6285,29 +6385,71 @@ async function appendConnectorIncident(errorsPath, input) {
|
|
|
6285
6385
|
await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
|
|
6286
6386
|
await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
6287
6387
|
}
|
|
6288
|
-
function
|
|
6388
|
+
function landIncidentCallSite(span, callSite, trusted, graph) {
|
|
6389
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
6390
|
+
const canonicalService = serviceNodeName(graph, span) ?? span.service;
|
|
6391
|
+
const recovered = !trusted;
|
|
6392
|
+
if (graph) {
|
|
6393
|
+
const fusedFileId = (0, import_types8.fileId)(canonicalService, relPath);
|
|
6394
|
+
if (graph.hasNode(fusedFileId)) {
|
|
6395
|
+
const node = landObservedSymbol(
|
|
6396
|
+
graph,
|
|
6397
|
+
fusedFileId,
|
|
6398
|
+
canonicalService,
|
|
6399
|
+
relPath,
|
|
6400
|
+
{ ...callSite, relPath },
|
|
6401
|
+
false
|
|
6402
|
+
);
|
|
6403
|
+
return {
|
|
6404
|
+
affectedNode: node,
|
|
6405
|
+
...recovered ? {
|
|
6406
|
+
codeFilepath: relPath,
|
|
6407
|
+
...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
|
|
6408
|
+
} : {}
|
|
6409
|
+
};
|
|
6410
|
+
}
|
|
6411
|
+
if (recovered) return null;
|
|
6412
|
+
}
|
|
6413
|
+
return { affectedNode: (0, import_types8.fileId)(span.service, relPath) };
|
|
6414
|
+
}
|
|
6415
|
+
function serviceNodeName(graph, span) {
|
|
6416
|
+
if (!graph) return void 0;
|
|
6417
|
+
const sid = resolveFusedServiceId(graph, span.service, span.env);
|
|
6418
|
+
if (!graph.hasNode(sid)) return void 0;
|
|
6419
|
+
const node = graph.getNodeAttributes(sid);
|
|
6420
|
+
return typeof node.name === "string" ? node.name : void 0;
|
|
6421
|
+
}
|
|
6422
|
+
function stacktraceCallSite(span, serviceNode, scanPath) {
|
|
6423
|
+
const frame = deepestApplicationFrame(span.exception?.stacktrace);
|
|
6424
|
+
if (!frame) return null;
|
|
6425
|
+
const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
|
|
6426
|
+
if (!relPath) return null;
|
|
6427
|
+
return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
|
|
6428
|
+
}
|
|
6429
|
+
function incidentLocus(span, graph, scanPath) {
|
|
6289
6430
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
|
|
6290
6431
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
6291
6432
|
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
6292
6433
|
if (callSite) {
|
|
6293
|
-
const
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
fusedFileId,
|
|
6301
|
-
canonicalService,
|
|
6302
|
-
relPath,
|
|
6303
|
-
{ ...callSite, relPath },
|
|
6304
|
-
false
|
|
6305
|
-
);
|
|
6306
|
-
}
|
|
6434
|
+
const landed = landIncidentCallSite(span, callSite, true, graph);
|
|
6435
|
+
if (landed) return landed;
|
|
6436
|
+
} else {
|
|
6437
|
+
const recovered = stacktraceCallSite(span, serviceNode, scanPath);
|
|
6438
|
+
if (recovered) {
|
|
6439
|
+
const landed = landIncidentCallSite(span, recovered, false, graph);
|
|
6440
|
+
if (landed) return landed;
|
|
6307
6441
|
}
|
|
6308
|
-
return (0, import_types8.fileId)(span.service, relPath);
|
|
6309
6442
|
}
|
|
6310
|
-
return sid;
|
|
6443
|
+
return { affectedNode: sid };
|
|
6444
|
+
}
|
|
6445
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
6446
|
+
return incidentLocus(span, graph, scanPath).affectedNode;
|
|
6447
|
+
}
|
|
6448
|
+
function withRecoveredCodeAttrs(attrs, locus) {
|
|
6449
|
+
if (locus.codeFilepath === void 0) return attrs;
|
|
6450
|
+
attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
|
|
6451
|
+
if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
|
|
6452
|
+
return attrs;
|
|
6311
6453
|
}
|
|
6312
6454
|
function sanitizeAttributes(attrs) {
|
|
6313
6455
|
const out = {};
|
|
@@ -6320,7 +6462,8 @@ function sanitizeAttributes(attrs) {
|
|
|
6320
6462
|
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
6321
6463
|
if (span.statusCode !== 2) return null;
|
|
6322
6464
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6323
|
-
const
|
|
6465
|
+
const locus = incidentLocus(span, graph, scanPath);
|
|
6466
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6324
6467
|
return {
|
|
6325
6468
|
id: `${span.traceId}:${span.spanId}`,
|
|
6326
6469
|
timestamp: ts,
|
|
@@ -6331,7 +6474,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
|
6331
6474
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
6332
6475
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6333
6476
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6334
|
-
affectedNode:
|
|
6477
|
+
affectedNode: locus.affectedNode
|
|
6335
6478
|
};
|
|
6336
6479
|
}
|
|
6337
6480
|
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
@@ -6365,7 +6508,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
6365
6508
|
await appendErrorEvent(ctx, ev);
|
|
6366
6509
|
}
|
|
6367
6510
|
async function recordExceptionIncident(ctx, span, ts) {
|
|
6368
|
-
const
|
|
6511
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
6512
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6369
6513
|
const ev = {
|
|
6370
6514
|
id: `${span.traceId}:${span.spanId}`,
|
|
6371
6515
|
timestamp: ts,
|
|
@@ -6376,7 +6520,7 @@ async function recordExceptionIncident(ctx, span, ts) {
|
|
|
6376
6520
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
6377
6521
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6378
6522
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6379
|
-
affectedNode:
|
|
6523
|
+
affectedNode: locus.affectedNode
|
|
6380
6524
|
};
|
|
6381
6525
|
await appendErrorEvent(ctx, ev);
|
|
6382
6526
|
}
|
|
@@ -6679,7 +6823,8 @@ async function handleSpan(ctx, span) {
|
|
|
6679
6823
|
if (span.statusCode === 2) {
|
|
6680
6824
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
6681
6825
|
if (ctx.writeErrorEventInline !== false) {
|
|
6682
|
-
const
|
|
6826
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
6827
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
6683
6828
|
const ev = {
|
|
6684
6829
|
id: `${span.traceId}:${span.spanId}`,
|
|
6685
6830
|
timestamp: ts,
|
|
@@ -6691,10 +6836,11 @@ async function handleSpan(ctx, span) {
|
|
|
6691
6836
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
6692
6837
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
6693
6838
|
// Attribute to where the failure originated — the symbol / file / service
|
|
6694
|
-
// the throwing span named (incidentAffectedNode
|
|
6695
|
-
//
|
|
6696
|
-
//
|
|
6697
|
-
|
|
6839
|
+
// the throwing span named (incidentAffectedNode / ADR-191, extended to
|
|
6840
|
+
// recover the locus from the stacktrace when the span stamped no code.*
|
|
6841
|
+
// attrs, ADR-216) — the same source-based attribution the durable
|
|
6842
|
+
// receiver write uses, not the outbound edge target this span minted.
|
|
6843
|
+
affectedNode: locus.affectedNode
|
|
6698
6844
|
};
|
|
6699
6845
|
await appendErrorEvent(ctx, ev);
|
|
6700
6846
|
}
|
|
@@ -6868,15 +7014,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6868
7014
|
clearInterval(interval);
|
|
6869
7015
|
};
|
|
6870
7016
|
}
|
|
6871
|
-
|
|
7017
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
7018
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
7019
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
7020
|
+
const handle = await import_node_fs8.promises.open(errorsPath, "r");
|
|
7021
|
+
try {
|
|
7022
|
+
const { size } = await handle.stat();
|
|
7023
|
+
if (size <= maxBytes) {
|
|
7024
|
+
return (await handle.readFile()).toString("utf8");
|
|
7025
|
+
}
|
|
7026
|
+
const buf = Buffer.alloc(maxBytes);
|
|
7027
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
7028
|
+
const raw = buf.toString("utf8");
|
|
7029
|
+
const firstNewline = raw.indexOf("\n");
|
|
7030
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
7031
|
+
} finally {
|
|
7032
|
+
await handle.close();
|
|
7033
|
+
}
|
|
7034
|
+
}
|
|
7035
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
7036
|
+
let raw;
|
|
6872
7037
|
try {
|
|
6873
|
-
|
|
6874
|
-
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6875
|
-
return dedupeIncidents(events);
|
|
7038
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6876
7039
|
} catch (err) {
|
|
6877
7040
|
if (err.code === "ENOENT") return [];
|
|
6878
7041
|
throw err;
|
|
6879
7042
|
}
|
|
7043
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
7044
|
+
const deduped = dedupeIncidents(events);
|
|
7045
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
7046
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6880
7047
|
}
|
|
6881
7048
|
function isSynthesizedHttpIncident(ev) {
|
|
6882
7049
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -15981,6 +16148,120 @@ function detectColumnDrift(node) {
|
|
|
15981
16148
|
}
|
|
15982
16149
|
return out;
|
|
15983
16150
|
}
|
|
16151
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16152
|
+
{
|
|
16153
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
16154
|
+
kind: "missing-attribute",
|
|
16155
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
16156
|
+
},
|
|
16157
|
+
{
|
|
16158
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
16159
|
+
kind: "missing-field",
|
|
16160
|
+
patterns: [
|
|
16161
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
16162
|
+
]
|
|
16163
|
+
},
|
|
16164
|
+
{
|
|
16165
|
+
// "has no property X", "no property named X".
|
|
16166
|
+
kind: "missing-property",
|
|
16167
|
+
patterns: [
|
|
16168
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
16169
|
+
]
|
|
16170
|
+
},
|
|
16171
|
+
{
|
|
16172
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
16173
|
+
kind: "missing-column",
|
|
16174
|
+
patterns: [
|
|
16175
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16176
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16177
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
16178
|
+
]
|
|
16179
|
+
},
|
|
16180
|
+
{
|
|
16181
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
16182
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
16183
|
+
// mismatch) does not get miscategorised here.
|
|
16184
|
+
kind: "undefined-method",
|
|
16185
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
16186
|
+
}
|
|
16187
|
+
];
|
|
16188
|
+
function classifySymbolMismatch(message) {
|
|
16189
|
+
for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
|
|
16190
|
+
for (const re of entry2.patterns) {
|
|
16191
|
+
const m = re.exec(message);
|
|
16192
|
+
if (m) {
|
|
16193
|
+
const captured = m[1];
|
|
16194
|
+
return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
|
|
16195
|
+
}
|
|
16196
|
+
}
|
|
16197
|
+
}
|
|
16198
|
+
return null;
|
|
16199
|
+
}
|
|
16200
|
+
function symbolLocus(graph, ev) {
|
|
16201
|
+
const attrs = ev.attributes ?? {};
|
|
16202
|
+
const filepath = codeFilepathOf(attrs);
|
|
16203
|
+
const lineno = codeLinenoOf(attrs);
|
|
16204
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
16205
|
+
const affected = ev.affectedNode;
|
|
16206
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
16207
|
+
const affectedIsCode = affectedInGraph && ((0, import_types57.parseSymbolId)(affected) !== null || (0, import_types57.parseFileId)(affected) !== null);
|
|
16208
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
16209
|
+
if (!location) return null;
|
|
16210
|
+
if (affectedInGraph) return { node: affected, location };
|
|
16211
|
+
const svc = (0, import_types57.serviceId)(ev.service);
|
|
16212
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
16213
|
+
return null;
|
|
16214
|
+
}
|
|
16215
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
16216
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
16217
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16218
|
+
for (const ev of incidents) {
|
|
16219
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
16220
|
+
if (!classified) continue;
|
|
16221
|
+
const locus = symbolLocus(graph, ev);
|
|
16222
|
+
if (!locus) continue;
|
|
16223
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
16224
|
+
const existing = groups.get(key);
|
|
16225
|
+
if (!existing) {
|
|
16226
|
+
groups.set(key, {
|
|
16227
|
+
node: locus.node,
|
|
16228
|
+
kind: classified.kind,
|
|
16229
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
16230
|
+
...locus.location ? { location: locus.location } : {},
|
|
16231
|
+
latest: ev,
|
|
16232
|
+
count: 1
|
|
16233
|
+
});
|
|
16234
|
+
} else {
|
|
16235
|
+
existing.count += 1;
|
|
16236
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
16237
|
+
existing.latest = ev;
|
|
16238
|
+
if (locus.location) existing.location = locus.location;
|
|
16239
|
+
}
|
|
16240
|
+
}
|
|
16241
|
+
}
|
|
16242
|
+
const out = [];
|
|
16243
|
+
for (const g of groups.values()) {
|
|
16244
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
16245
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
16246
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
16247
|
+
out.push({
|
|
16248
|
+
type: "observed-symbol-mismatch",
|
|
16249
|
+
source: g.node,
|
|
16250
|
+
target: g.node,
|
|
16251
|
+
mismatchKind: g.kind,
|
|
16252
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
16253
|
+
...g.location ? { location: g.location } : {},
|
|
16254
|
+
provenance: import_types57.Provenance.INFERRED,
|
|
16255
|
+
incidentId: g.latest.id,
|
|
16256
|
+
errorMessage: g.latest.errorMessage,
|
|
16257
|
+
incidentCount: g.count,
|
|
16258
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
16259
|
+
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.`,
|
|
16260
|
+
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."
|
|
16261
|
+
});
|
|
16262
|
+
}
|
|
16263
|
+
return out;
|
|
16264
|
+
}
|
|
15984
16265
|
function involvesNode(d, nodeId) {
|
|
15985
16266
|
return d.source === nodeId || d.target === nodeId;
|
|
15986
16267
|
}
|
|
@@ -16066,6 +16347,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16066
16347
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
16067
16348
|
}
|
|
16068
16349
|
});
|
|
16350
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
16351
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
16352
|
+
}
|
|
16069
16353
|
const reconciled = suppressHostMismatchHalves(all);
|
|
16070
16354
|
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
16071
16355
|
let filtered = dampened;
|
|
@@ -16086,7 +16370,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16086
16370
|
"missing-observed": 1,
|
|
16087
16371
|
"version-mismatch": 2,
|
|
16088
16372
|
"host-mismatch": 3,
|
|
16089
|
-
"compat-violation": 4
|
|
16373
|
+
"compat-violation": 4,
|
|
16374
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
16375
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
16376
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
16377
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
16378
|
+
"observed-symbol-mismatch": 5
|
|
16090
16379
|
};
|
|
16091
16380
|
filtered.sort((a, b) => {
|
|
16092
16381
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -16097,7 +16386,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16097
16386
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
16098
16387
|
const ac = "column" in a && a.column ? a.column : "";
|
|
16099
16388
|
const bc = "column" in b && b.column ? b.column : "";
|
|
16100
|
-
return ac.localeCompare(bc);
|
|
16389
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
16390
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
16391
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
16392
|
+
return asym.localeCompare(bsym);
|
|
16101
16393
|
});
|
|
16102
16394
|
return import_types57.DivergenceResultSchema.parse({
|
|
16103
16395
|
divergences: filtered,
|
|
@@ -17157,10 +17449,15 @@ function divergenceLine(d) {
|
|
|
17157
17449
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
17158
17450
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
17159
17451
|
}
|
|
17452
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17453
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17454
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17455
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17456
|
+
}
|
|
17160
17457
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
17161
17458
|
}
|
|
17162
|
-
function buildDivergenceSection(graph, node) {
|
|
17163
|
-
const result = computeDivergences(graph, { node });
|
|
17459
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17460
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
17164
17461
|
if (result.totalAffected === 0) return null;
|
|
17165
17462
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
17166
17463
|
text: divergenceLine(d),
|
|
@@ -17173,8 +17470,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
17173
17470
|
facts
|
|
17174
17471
|
};
|
|
17175
17472
|
}
|
|
17176
|
-
function buildGlobalDivergenceSection(graph) {
|
|
17177
|
-
const result = computeDivergences(graph);
|
|
17473
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17474
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17178
17475
|
if (result.totalAffected === 0) {
|
|
17179
17476
|
return {
|
|
17180
17477
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -17272,7 +17569,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17272
17569
|
}))
|
|
17273
17570
|
});
|
|
17274
17571
|
}
|
|
17275
|
-
const div = computeDivergences(graph);
|
|
17572
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17276
17573
|
sections.push({
|
|
17277
17574
|
heading: "Divergences",
|
|
17278
17575
|
facts: [
|
|
@@ -17286,7 +17583,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17286
17583
|
function buildGlobalSections(intent, graph, incidents) {
|
|
17287
17584
|
switch (intent) {
|
|
17288
17585
|
case "divergence":
|
|
17289
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17586
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
17290
17587
|
case "incidents":
|
|
17291
17588
|
return [buildGlobalIncidentsSection(incidents)];
|
|
17292
17589
|
case "overview":
|
|
@@ -17317,7 +17614,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
17317
17614
|
case "incidents":
|
|
17318
17615
|
return buildIncidentsSection(node, incidents);
|
|
17319
17616
|
case "divergence":
|
|
17320
|
-
return buildDivergenceSection(graph, node);
|
|
17617
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
17321
17618
|
}
|
|
17322
17619
|
}
|
|
17323
17620
|
function summarizeGlobal(intent, sections) {
|
|
@@ -21537,6 +21834,8 @@ async function deprovisionConnector(entry2, env = process.env, fetchImpl) {
|
|
|
21537
21834
|
}
|
|
21538
21835
|
|
|
21539
21836
|
// src/api.ts
|
|
21837
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
21838
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
21540
21839
|
function serializeGraph(graph) {
|
|
21541
21840
|
const nodes = [];
|
|
21542
21841
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -21716,10 +22015,13 @@ function registerRoutes(scope, ctx) {
|
|
|
21716
22015
|
}
|
|
21717
22016
|
minConfidence = n;
|
|
21718
22017
|
}
|
|
22018
|
+
const epath = errorsPathFor(proj);
|
|
22019
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
21719
22020
|
return computeDivergences(proj.graph, {
|
|
21720
22021
|
...typeFilter ? { type: typeFilter } : {},
|
|
21721
22022
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
21722
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
22023
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
22024
|
+
incidents
|
|
21723
22025
|
});
|
|
21724
22026
|
});
|
|
21725
22027
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -21729,10 +22031,11 @@ function registerRoutes(scope, ctx) {
|
|
|
21729
22031
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
21730
22032
|
const events = await readErrorEvents(epath);
|
|
21731
22033
|
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,
|
|
22034
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
22035
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21734
22036
|
const sliced = events.slice(0, safeLimit);
|
|
21735
|
-
|
|
22037
|
+
const omitted = total - sliced.length;
|
|
22038
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
21736
22039
|
});
|
|
21737
22040
|
scope.get("/stale-events", async (req, reply) => {
|
|
21738
22041
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -21841,16 +22144,20 @@ function registerRoutes(scope, ctx) {
|
|
|
21841
22144
|
const filtered = events.filter(
|
|
21842
22145
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
21843
22146
|
);
|
|
21844
|
-
|
|
22147
|
+
const total = filtered.length;
|
|
22148
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
22149
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
22150
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
22151
|
+
const omitted = total - recent.length;
|
|
22152
|
+
return {
|
|
22153
|
+
count: recent.length,
|
|
22154
|
+
total,
|
|
22155
|
+
events: recent,
|
|
22156
|
+
...omitted > 0 ? { omitted } : {}
|
|
22157
|
+
};
|
|
21845
22158
|
};
|
|
21846
|
-
scope.get(
|
|
21847
|
-
|
|
21848
|
-
incidentHistoryHandler
|
|
21849
|
-
);
|
|
21850
|
-
scope.get(
|
|
21851
|
-
"/graph/incident-history/:nodeId",
|
|
21852
|
-
incidentHistoryHandler
|
|
21853
|
-
);
|
|
22159
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
22160
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
21854
22161
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
21855
22162
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
21856
22163
|
if (!proj) return;
|
|
@@ -24109,7 +24416,7 @@ var OTEL_ENV = {
|
|
|
24109
24416
|
key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
24110
24417
|
value: "http://localhost:4318/projects/<project>/v1/traces"
|
|
24111
24418
|
};
|
|
24112
|
-
function
|
|
24419
|
+
function serviceNodeName2(pkg, serviceDir) {
|
|
24113
24420
|
return pkg.name ?? import_node_path81.default.basename(serviceDir);
|
|
24114
24421
|
}
|
|
24115
24422
|
function projectToken(pkg, serviceDir, project) {
|
|
@@ -24493,7 +24800,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
|
|
|
24493
24800
|
}
|
|
24494
24801
|
dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
|
|
24495
24802
|
}
|
|
24496
|
-
const svcName =
|
|
24803
|
+
const svcName = serviceNodeName2(pkg, serviceDir);
|
|
24497
24804
|
const projectName = projectToken(pkg, serviceDir, project);
|
|
24498
24805
|
const registrations = nonBundled.map((i) => i.registration);
|
|
24499
24806
|
const generatedFiles = [];
|
|
@@ -24592,7 +24899,7 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
|
|
|
24592
24899
|
generatedFiles.push({
|
|
24593
24900
|
file: envNeatFile,
|
|
24594
24901
|
contents: renderEnvNeat(
|
|
24595
|
-
|
|
24902
|
+
serviceNodeName2(pkg, serviceDir),
|
|
24596
24903
|
projectToken(pkg, serviceDir, project)
|
|
24597
24904
|
),
|
|
24598
24905
|
skipIfExists: true
|
|
@@ -24602,7 +24909,7 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
|
|
|
24602
24909
|
function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
|
|
24603
24910
|
return renderFrameworkOtelInit(
|
|
24604
24911
|
template,
|
|
24605
|
-
|
|
24912
|
+
serviceNodeName2(pkg, serviceDir),
|
|
24606
24913
|
projectToken(pkg, serviceDir, project)
|
|
24607
24914
|
);
|
|
24608
24915
|
}
|
|
@@ -24982,7 +25289,7 @@ async function plan(serviceDir, opts) {
|
|
|
24982
25289
|
} catch {
|
|
24983
25290
|
return { ...empty, libOnly: true };
|
|
24984
25291
|
}
|
|
24985
|
-
const svcName =
|
|
25292
|
+
const svcName = serviceNodeName2(pkg, serviceDir);
|
|
24986
25293
|
const projectName = projectToken(pkg, serviceDir, project);
|
|
24987
25294
|
const registrations = nonBundled.map((i) => i.registration);
|
|
24988
25295
|
const generatedFiles = [];
|
|
@@ -28841,6 +29148,11 @@ function formatDivergenceLine(d) {
|
|
|
28841
29148
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
28842
29149
|
case "compat-violation":
|
|
28843
29150
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
29151
|
+
case "observed-symbol-mismatch": {
|
|
29152
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
29153
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
29154
|
+
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29155
|
+
}
|
|
28844
29156
|
}
|
|
28845
29157
|
}
|
|
28846
29158
|
async function runDivergences(client, input) {
|
|
@@ -28994,6 +29306,11 @@ function formatDivergenceLine2(d) {
|
|
|
28994
29306
|
return `\u26A0 divergence [host-mismatch] ${d.source} \u2192 ${d.target} declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
28995
29307
|
case "compat-violation":
|
|
28996
29308
|
return `\u26A0 divergence [compat-violation] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
29309
|
+
case "observed-symbol-mismatch": {
|
|
29310
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
29311
|
+
const member = d.symbol ? ` ${d.symbol}` : " a member";
|
|
29312
|
+
return `\u26A0 divergence [observed-symbol-mismatch] ${d.source}${at} reads${member} the runtime object does not have (${d.mismatchKind})`;
|
|
29313
|
+
}
|
|
28997
29314
|
}
|
|
28998
29315
|
}
|
|
28999
29316
|
function formatStaleLine(edgeId) {
|
|
@@ -29970,7 +30287,8 @@ async function runInit(opts) {
|
|
|
29970
30287
|
console.log(`snapshot: ${opts.outPath}`);
|
|
29971
30288
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
29972
30289
|
console.log("");
|
|
29973
|
-
const
|
|
30290
|
+
const summaryIncidents = await readErrorEvents(errorsPath);
|
|
30291
|
+
const divergenceResult = computeDivergences(graph, { incidents: summaryIncidents });
|
|
29974
30292
|
console.log(
|
|
29975
30293
|
renderValueForwardSummary({
|
|
29976
30294
|
graph,
|