@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
|
@@ -3691,6 +3691,63 @@ function latencyPercentiles(hist) {
|
|
|
3691
3691
|
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
3692
3692
|
}
|
|
3693
3693
|
|
|
3694
|
+
// src/stacktrace.ts
|
|
3695
|
+
var FRAME_SHAPES = [
|
|
3696
|
+
// `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
|
|
3697
|
+
{ re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
|
|
3698
|
+
// `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
|
|
3699
|
+
{ re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
|
|
3700
|
+
// `at <path>:<line>:<col>` — anonymous call frame, most recent first.
|
|
3701
|
+
{ re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
|
|
3702
|
+
// `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
|
|
3703
|
+
{ re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
|
|
3704
|
+
];
|
|
3705
|
+
var VENDOR_MARKERS = [
|
|
3706
|
+
"node_modules",
|
|
3707
|
+
// dependency root
|
|
3708
|
+
"site-packages",
|
|
3709
|
+
// installed-package root
|
|
3710
|
+
"dist-packages",
|
|
3711
|
+
// distro-packaged root
|
|
3712
|
+
"node:"
|
|
3713
|
+
// runtime-internal module scheme (a stdlib/runtime-root frame)
|
|
3714
|
+
];
|
|
3715
|
+
function matchFrame(line) {
|
|
3716
|
+
for (const shape of FRAME_SHAPES) {
|
|
3717
|
+
const m = shape.re.exec(line);
|
|
3718
|
+
if (!m) continue;
|
|
3719
|
+
const file = m[shape.file];
|
|
3720
|
+
const lineNo = Number(m[shape.line]);
|
|
3721
|
+
if (!file || !Number.isFinite(lineNo)) continue;
|
|
3722
|
+
const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
|
|
3723
|
+
return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
|
|
3724
|
+
}
|
|
3725
|
+
return null;
|
|
3726
|
+
}
|
|
3727
|
+
function isApplicationFrame(file) {
|
|
3728
|
+
if (file.startsWith("<")) return false;
|
|
3729
|
+
const norm = file.split("\\").join("/");
|
|
3730
|
+
for (const marker of VENDOR_MARKERS) {
|
|
3731
|
+
if (norm.includes(marker)) return false;
|
|
3732
|
+
}
|
|
3733
|
+
return true;
|
|
3734
|
+
}
|
|
3735
|
+
function deepestApplicationFrame(stacktrace) {
|
|
3736
|
+
if (!stacktrace) return null;
|
|
3737
|
+
const appFrames = [];
|
|
3738
|
+
for (const raw of stacktrace.split("\n")) {
|
|
3739
|
+
const matched = matchFrame(raw);
|
|
3740
|
+
if (!matched) continue;
|
|
3741
|
+
if (!isApplicationFrame(matched.frame.file)) continue;
|
|
3742
|
+
appFrames.push(matched);
|
|
3743
|
+
}
|
|
3744
|
+
if (appFrames.length === 0) return null;
|
|
3745
|
+
const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
|
|
3746
|
+
const oriented = appFrames.filter((f) => f.deepest === orientation);
|
|
3747
|
+
const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
|
|
3748
|
+
return chosen ? chosen.frame : null;
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3694
3751
|
// src/ingest.ts
|
|
3695
3752
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
3696
3753
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -4542,29 +4599,71 @@ async function appendConnectorIncident(errorsPath, input) {
|
|
|
4542
4599
|
await fs7.mkdir(path8.dirname(errorsPath), { recursive: true });
|
|
4543
4600
|
await fs7.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4544
4601
|
}
|
|
4545
|
-
function
|
|
4602
|
+
function landIncidentCallSite(span, callSite, trusted, graph) {
|
|
4603
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
4604
|
+
const canonicalService = serviceNodeName(graph, span) ?? span.service;
|
|
4605
|
+
const recovered = !trusted;
|
|
4606
|
+
if (graph) {
|
|
4607
|
+
const fusedFileId = fileId3(canonicalService, relPath);
|
|
4608
|
+
if (graph.hasNode(fusedFileId)) {
|
|
4609
|
+
const node = landObservedSymbol(
|
|
4610
|
+
graph,
|
|
4611
|
+
fusedFileId,
|
|
4612
|
+
canonicalService,
|
|
4613
|
+
relPath,
|
|
4614
|
+
{ ...callSite, relPath },
|
|
4615
|
+
false
|
|
4616
|
+
);
|
|
4617
|
+
return {
|
|
4618
|
+
affectedNode: node,
|
|
4619
|
+
...recovered ? {
|
|
4620
|
+
codeFilepath: relPath,
|
|
4621
|
+
...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
|
|
4622
|
+
} : {}
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
if (recovered) return null;
|
|
4626
|
+
}
|
|
4627
|
+
return { affectedNode: fileId3(span.service, relPath) };
|
|
4628
|
+
}
|
|
4629
|
+
function serviceNodeName(graph, span) {
|
|
4630
|
+
if (!graph) return void 0;
|
|
4631
|
+
const sid = resolveFusedServiceId(graph, span.service, span.env);
|
|
4632
|
+
if (!graph.hasNode(sid)) return void 0;
|
|
4633
|
+
const node = graph.getNodeAttributes(sid);
|
|
4634
|
+
return typeof node.name === "string" ? node.name : void 0;
|
|
4635
|
+
}
|
|
4636
|
+
function stacktraceCallSite(span, serviceNode, scanPath) {
|
|
4637
|
+
const frame = deepestApplicationFrame(span.exception?.stacktrace);
|
|
4638
|
+
if (!frame) return null;
|
|
4639
|
+
const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
|
|
4640
|
+
if (!relPath) return null;
|
|
4641
|
+
return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
|
|
4642
|
+
}
|
|
4643
|
+
function incidentLocus(span, graph, scanPath) {
|
|
4546
4644
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : serviceId(span.service, span.env);
|
|
4547
4645
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
4548
4646
|
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
4549
4647
|
if (callSite) {
|
|
4550
|
-
const
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
fusedFileId,
|
|
4558
|
-
canonicalService,
|
|
4559
|
-
relPath,
|
|
4560
|
-
{ ...callSite, relPath },
|
|
4561
|
-
false
|
|
4562
|
-
);
|
|
4563
|
-
}
|
|
4648
|
+
const landed = landIncidentCallSite(span, callSite, true, graph);
|
|
4649
|
+
if (landed) return landed;
|
|
4650
|
+
} else {
|
|
4651
|
+
const recovered = stacktraceCallSite(span, serviceNode, scanPath);
|
|
4652
|
+
if (recovered) {
|
|
4653
|
+
const landed = landIncidentCallSite(span, recovered, false, graph);
|
|
4654
|
+
if (landed) return landed;
|
|
4564
4655
|
}
|
|
4565
|
-
return fileId3(span.service, relPath);
|
|
4566
4656
|
}
|
|
4567
|
-
return sid;
|
|
4657
|
+
return { affectedNode: sid };
|
|
4658
|
+
}
|
|
4659
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
4660
|
+
return incidentLocus(span, graph, scanPath).affectedNode;
|
|
4661
|
+
}
|
|
4662
|
+
function withRecoveredCodeAttrs(attrs, locus) {
|
|
4663
|
+
if (locus.codeFilepath === void 0) return attrs;
|
|
4664
|
+
attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
|
|
4665
|
+
if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
|
|
4666
|
+
return attrs;
|
|
4568
4667
|
}
|
|
4569
4668
|
function sanitizeAttributes(attrs) {
|
|
4570
4669
|
const out = {};
|
|
@@ -4577,7 +4676,8 @@ function sanitizeAttributes(attrs) {
|
|
|
4577
4676
|
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
4578
4677
|
if (span.statusCode !== 2) return null;
|
|
4579
4678
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4580
|
-
const
|
|
4679
|
+
const locus = incidentLocus(span, graph, scanPath);
|
|
4680
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
4581
4681
|
return {
|
|
4582
4682
|
id: `${span.traceId}:${span.spanId}`,
|
|
4583
4683
|
timestamp: ts,
|
|
@@ -4588,7 +4688,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
|
4588
4688
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
4589
4689
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
4590
4690
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
4591
|
-
affectedNode:
|
|
4691
|
+
affectedNode: locus.affectedNode
|
|
4592
4692
|
};
|
|
4593
4693
|
}
|
|
4594
4694
|
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
@@ -4622,7 +4722,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
4622
4722
|
await appendErrorEvent(ctx, ev);
|
|
4623
4723
|
}
|
|
4624
4724
|
async function recordExceptionIncident(ctx, span, ts) {
|
|
4625
|
-
const
|
|
4725
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
4726
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
4626
4727
|
const ev = {
|
|
4627
4728
|
id: `${span.traceId}:${span.spanId}`,
|
|
4628
4729
|
timestamp: ts,
|
|
@@ -4633,7 +4734,7 @@ async function recordExceptionIncident(ctx, span, ts) {
|
|
|
4633
4734
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
4634
4735
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
4635
4736
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
4636
|
-
affectedNode:
|
|
4737
|
+
affectedNode: locus.affectedNode
|
|
4637
4738
|
};
|
|
4638
4739
|
await appendErrorEvent(ctx, ev);
|
|
4639
4740
|
}
|
|
@@ -4936,7 +5037,8 @@ async function handleSpan(ctx, span) {
|
|
|
4936
5037
|
if (span.statusCode === 2) {
|
|
4937
5038
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
4938
5039
|
if (ctx.writeErrorEventInline !== false) {
|
|
4939
|
-
const
|
|
5040
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
5041
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
4940
5042
|
const ev = {
|
|
4941
5043
|
id: `${span.traceId}:${span.spanId}`,
|
|
4942
5044
|
timestamp: ts,
|
|
@@ -4948,10 +5050,11 @@ async function handleSpan(ctx, span) {
|
|
|
4948
5050
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
4949
5051
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
4950
5052
|
// Attribute to where the failure originated — the symbol / file / service
|
|
4951
|
-
// the throwing span named (incidentAffectedNode
|
|
4952
|
-
//
|
|
4953
|
-
//
|
|
4954
|
-
|
|
5053
|
+
// the throwing span named (incidentAffectedNode / ADR-191, extended to
|
|
5054
|
+
// recover the locus from the stacktrace when the span stamped no code.*
|
|
5055
|
+
// attrs, ADR-216) — the same source-based attribution the durable
|
|
5056
|
+
// receiver write uses, not the outbound edge target this span minted.
|
|
5057
|
+
affectedNode: locus.affectedNode
|
|
4955
5058
|
};
|
|
4956
5059
|
await appendErrorEvent(ctx, ev);
|
|
4957
5060
|
}
|
|
@@ -5125,15 +5228,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
5125
5228
|
clearInterval(interval);
|
|
5126
5229
|
};
|
|
5127
5230
|
}
|
|
5128
|
-
|
|
5231
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
5232
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
5233
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
5234
|
+
const handle = await fs7.open(errorsPath, "r");
|
|
5235
|
+
try {
|
|
5236
|
+
const { size } = await handle.stat();
|
|
5237
|
+
if (size <= maxBytes) {
|
|
5238
|
+
return (await handle.readFile()).toString("utf8");
|
|
5239
|
+
}
|
|
5240
|
+
const buf = Buffer.alloc(maxBytes);
|
|
5241
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
5242
|
+
const raw = buf.toString("utf8");
|
|
5243
|
+
const firstNewline = raw.indexOf("\n");
|
|
5244
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
5245
|
+
} finally {
|
|
5246
|
+
await handle.close();
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
5250
|
+
let raw;
|
|
5129
5251
|
try {
|
|
5130
|
-
|
|
5131
|
-
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
5132
|
-
return dedupeIncidents(events);
|
|
5252
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
5133
5253
|
} catch (err) {
|
|
5134
5254
|
if (err.code === "ENOENT") return [];
|
|
5135
5255
|
throw err;
|
|
5136
5256
|
}
|
|
5257
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
5258
|
+
const deduped = dedupeIncidents(events);
|
|
5259
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
5260
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
5137
5261
|
}
|
|
5138
5262
|
function isSynthesizedHttpIncident(ev) {
|
|
5139
5263
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -5906,6 +6030,47 @@ function classifyNode(ctx) {
|
|
|
5906
6030
|
function isVictimSeed(ctx) {
|
|
5907
6031
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
5908
6032
|
}
|
|
6033
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
6034
|
+
"name resolution",
|
|
6035
|
+
"resolve host",
|
|
6036
|
+
"getaddrinfo",
|
|
6037
|
+
"enotfound",
|
|
6038
|
+
"connection refused",
|
|
6039
|
+
"econnrefused",
|
|
6040
|
+
"connection reset",
|
|
6041
|
+
"econnreset",
|
|
6042
|
+
"able to connect",
|
|
6043
|
+
"failed to connect",
|
|
6044
|
+
"cannot connect",
|
|
6045
|
+
"could not connect",
|
|
6046
|
+
"unable to connect",
|
|
6047
|
+
"connection timed out",
|
|
6048
|
+
"etimedout",
|
|
6049
|
+
"no route to host",
|
|
6050
|
+
"host unreachable",
|
|
6051
|
+
"network is unreachable",
|
|
6052
|
+
"connection closed"
|
|
6053
|
+
];
|
|
6054
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
6055
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
6056
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
6057
|
+
}
|
|
6058
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
6059
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
6060
|
+
if (!graph.hasNode(n)) continue;
|
|
6061
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
6062
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
6063
|
+
if (e.type === EdgeType6.CONTAINS) continue;
|
|
6064
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
if (seedSource === "incident" && incidents) {
|
|
6068
|
+
for (const ev of incidents) {
|
|
6069
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
return false;
|
|
6073
|
+
}
|
|
5909
6074
|
function grainOf(graph, nodeId) {
|
|
5910
6075
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
5911
6076
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -6069,7 +6234,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
6069
6234
|
const candidates = [];
|
|
6070
6235
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
6071
6236
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
6072
|
-
|
|
6237
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
6238
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
6073
6239
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
6074
6240
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
6075
6241
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -14939,6 +15105,7 @@ import {
|
|
|
14939
15105
|
NodeType as NodeType30,
|
|
14940
15106
|
parseEdgeId,
|
|
14941
15107
|
parseFileId,
|
|
15108
|
+
parseSymbolId as parseSymbolId2,
|
|
14942
15109
|
Provenance as Provenance24,
|
|
14943
15110
|
serviceId as serviceId13
|
|
14944
15111
|
} from "@neat.is/types";
|
|
@@ -15195,6 +15362,120 @@ function detectColumnDrift(node) {
|
|
|
15195
15362
|
}
|
|
15196
15363
|
return out;
|
|
15197
15364
|
}
|
|
15365
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
15366
|
+
{
|
|
15367
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
15368
|
+
kind: "missing-attribute",
|
|
15369
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
15370
|
+
},
|
|
15371
|
+
{
|
|
15372
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
15373
|
+
kind: "missing-field",
|
|
15374
|
+
patterns: [
|
|
15375
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
15376
|
+
]
|
|
15377
|
+
},
|
|
15378
|
+
{
|
|
15379
|
+
// "has no property X", "no property named X".
|
|
15380
|
+
kind: "missing-property",
|
|
15381
|
+
patterns: [
|
|
15382
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
15383
|
+
]
|
|
15384
|
+
},
|
|
15385
|
+
{
|
|
15386
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
15387
|
+
kind: "missing-column",
|
|
15388
|
+
patterns: [
|
|
15389
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
15390
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
15391
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
15392
|
+
]
|
|
15393
|
+
},
|
|
15394
|
+
{
|
|
15395
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
15396
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
15397
|
+
// mismatch) does not get miscategorised here.
|
|
15398
|
+
kind: "undefined-method",
|
|
15399
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
15400
|
+
}
|
|
15401
|
+
];
|
|
15402
|
+
function classifySymbolMismatch(message) {
|
|
15403
|
+
for (const entry of SYMBOL_MISMATCH_PATTERNS) {
|
|
15404
|
+
for (const re of entry.patterns) {
|
|
15405
|
+
const m = re.exec(message);
|
|
15406
|
+
if (m) {
|
|
15407
|
+
const captured = m[1];
|
|
15408
|
+
return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15411
|
+
}
|
|
15412
|
+
return null;
|
|
15413
|
+
}
|
|
15414
|
+
function symbolLocus(graph, ev) {
|
|
15415
|
+
const attrs = ev.attributes ?? {};
|
|
15416
|
+
const filepath = codeFilepathOf(attrs);
|
|
15417
|
+
const lineno = codeLinenoOf(attrs);
|
|
15418
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
15419
|
+
const affected = ev.affectedNode;
|
|
15420
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
15421
|
+
const affectedIsCode = affectedInGraph && (parseSymbolId2(affected) !== null || parseFileId(affected) !== null);
|
|
15422
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
15423
|
+
if (!location) return null;
|
|
15424
|
+
if (affectedInGraph) return { node: affected, location };
|
|
15425
|
+
const svc = serviceId13(ev.service);
|
|
15426
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
15427
|
+
return null;
|
|
15428
|
+
}
|
|
15429
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
15430
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
15431
|
+
const groups = /* @__PURE__ */ new Map();
|
|
15432
|
+
for (const ev of incidents) {
|
|
15433
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
15434
|
+
if (!classified) continue;
|
|
15435
|
+
const locus = symbolLocus(graph, ev);
|
|
15436
|
+
if (!locus) continue;
|
|
15437
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
15438
|
+
const existing = groups.get(key);
|
|
15439
|
+
if (!existing) {
|
|
15440
|
+
groups.set(key, {
|
|
15441
|
+
node: locus.node,
|
|
15442
|
+
kind: classified.kind,
|
|
15443
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
15444
|
+
...locus.location ? { location: locus.location } : {},
|
|
15445
|
+
latest: ev,
|
|
15446
|
+
count: 1
|
|
15447
|
+
});
|
|
15448
|
+
} else {
|
|
15449
|
+
existing.count += 1;
|
|
15450
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
15451
|
+
existing.latest = ev;
|
|
15452
|
+
if (locus.location) existing.location = locus.location;
|
|
15453
|
+
}
|
|
15454
|
+
}
|
|
15455
|
+
}
|
|
15456
|
+
const out = [];
|
|
15457
|
+
for (const g of groups.values()) {
|
|
15458
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
15459
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
15460
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
15461
|
+
out.push({
|
|
15462
|
+
type: "observed-symbol-mismatch",
|
|
15463
|
+
source: g.node,
|
|
15464
|
+
target: g.node,
|
|
15465
|
+
mismatchKind: g.kind,
|
|
15466
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
15467
|
+
...g.location ? { location: g.location } : {},
|
|
15468
|
+
provenance: Provenance24.INFERRED,
|
|
15469
|
+
incidentId: g.latest.id,
|
|
15470
|
+
errorMessage: g.latest.errorMessage,
|
|
15471
|
+
incidentCount: g.count,
|
|
15472
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
15473
|
+
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.`,
|
|
15474
|
+
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."
|
|
15475
|
+
});
|
|
15476
|
+
}
|
|
15477
|
+
return out;
|
|
15478
|
+
}
|
|
15198
15479
|
function involvesNode(d, nodeId) {
|
|
15199
15480
|
return d.source === nodeId || d.target === nodeId;
|
|
15200
15481
|
}
|
|
@@ -15280,6 +15561,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
15280
15561
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
15281
15562
|
}
|
|
15282
15563
|
});
|
|
15564
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
15565
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
15566
|
+
}
|
|
15283
15567
|
const reconciled = suppressHostMismatchHalves(all);
|
|
15284
15568
|
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
15285
15569
|
let filtered = dampened;
|
|
@@ -15300,7 +15584,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
15300
15584
|
"missing-observed": 1,
|
|
15301
15585
|
"version-mismatch": 2,
|
|
15302
15586
|
"host-mismatch": 3,
|
|
15303
|
-
"compat-violation": 4
|
|
15587
|
+
"compat-violation": 4,
|
|
15588
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
15589
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
15590
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
15591
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
15592
|
+
"observed-symbol-mismatch": 5
|
|
15304
15593
|
};
|
|
15305
15594
|
filtered.sort((a, b) => {
|
|
15306
15595
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -15311,7 +15600,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
15311
15600
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
15312
15601
|
const ac = "column" in a && a.column ? a.column : "";
|
|
15313
15602
|
const bc = "column" in b && b.column ? b.column : "";
|
|
15314
|
-
return ac.localeCompare(bc);
|
|
15603
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
15604
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
15605
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
15606
|
+
return asym.localeCompare(bsym);
|
|
15315
15607
|
});
|
|
15316
15608
|
return DivergenceResultSchema.parse({
|
|
15317
15609
|
divergences: filtered,
|
|
@@ -16754,10 +17046,15 @@ function divergenceLine(d) {
|
|
|
16754
17046
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16755
17047
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16756
17048
|
}
|
|
17049
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17050
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17051
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17052
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17053
|
+
}
|
|
16757
17054
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16758
17055
|
}
|
|
16759
|
-
function buildDivergenceSection(graph, node) {
|
|
16760
|
-
const result = computeDivergences(graph, { node });
|
|
17056
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17057
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
16761
17058
|
if (result.totalAffected === 0) return null;
|
|
16762
17059
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16763
17060
|
text: divergenceLine(d),
|
|
@@ -16770,8 +17067,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
16770
17067
|
facts
|
|
16771
17068
|
};
|
|
16772
17069
|
}
|
|
16773
|
-
function buildGlobalDivergenceSection(graph) {
|
|
16774
|
-
const result = computeDivergences(graph);
|
|
17070
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17071
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16775
17072
|
if (result.totalAffected === 0) {
|
|
16776
17073
|
return {
|
|
16777
17074
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -16869,7 +17166,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16869
17166
|
}))
|
|
16870
17167
|
});
|
|
16871
17168
|
}
|
|
16872
|
-
const div = computeDivergences(graph);
|
|
17169
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16873
17170
|
sections.push({
|
|
16874
17171
|
heading: "Divergences",
|
|
16875
17172
|
facts: [
|
|
@@ -16883,7 +17180,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16883
17180
|
function buildGlobalSections(intent, graph, incidents) {
|
|
16884
17181
|
switch (intent) {
|
|
16885
17182
|
case "divergence":
|
|
16886
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17183
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
16887
17184
|
case "incidents":
|
|
16888
17185
|
return [buildGlobalIncidentsSection(incidents)];
|
|
16889
17186
|
case "overview":
|
|
@@ -16914,7 +17211,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
16914
17211
|
case "incidents":
|
|
16915
17212
|
return buildIncidentsSection(node, incidents);
|
|
16916
17213
|
case "divergence":
|
|
16917
|
-
return buildDivergenceSection(graph, node);
|
|
17214
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
16918
17215
|
}
|
|
16919
17216
|
}
|
|
16920
17217
|
function summarizeGlobal(intent, sections) {
|
|
@@ -20536,6 +20833,8 @@ async function deprovisionConnector(entry, env = process.env, fetchImpl) {
|
|
|
20536
20833
|
}
|
|
20537
20834
|
|
|
20538
20835
|
// src/api.ts
|
|
20836
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
20837
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
20539
20838
|
function serializeGraph(graph) {
|
|
20540
20839
|
const nodes = [];
|
|
20541
20840
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -20715,10 +21014,13 @@ function registerRoutes(scope, ctx) {
|
|
|
20715
21014
|
}
|
|
20716
21015
|
minConfidence = n;
|
|
20717
21016
|
}
|
|
21017
|
+
const epath = errorsPathFor(proj);
|
|
21018
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
20718
21019
|
return computeDivergences(proj.graph, {
|
|
20719
21020
|
...typeFilter ? { type: typeFilter } : {},
|
|
20720
21021
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
20721
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
21022
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
21023
|
+
incidents
|
|
20722
21024
|
});
|
|
20723
21025
|
});
|
|
20724
21026
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -20728,10 +21030,11 @@ function registerRoutes(scope, ctx) {
|
|
|
20728
21030
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
20729
21031
|
const events = await readErrorEvents(epath);
|
|
20730
21032
|
const total = events.length;
|
|
20731
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
20732
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21033
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21034
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
20733
21035
|
const sliced = events.slice(0, safeLimit);
|
|
20734
|
-
|
|
21036
|
+
const omitted = total - sliced.length;
|
|
21037
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
20735
21038
|
});
|
|
20736
21039
|
scope.get("/stale-events", async (req, reply) => {
|
|
20737
21040
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -20840,16 +21143,20 @@ function registerRoutes(scope, ctx) {
|
|
|
20840
21143
|
const filtered = events.filter(
|
|
20841
21144
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
20842
21145
|
);
|
|
20843
|
-
|
|
21146
|
+
const total = filtered.length;
|
|
21147
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21148
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21149
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
21150
|
+
const omitted = total - recent.length;
|
|
21151
|
+
return {
|
|
21152
|
+
count: recent.length,
|
|
21153
|
+
total,
|
|
21154
|
+
events: recent,
|
|
21155
|
+
...omitted > 0 ? { omitted } : {}
|
|
21156
|
+
};
|
|
20844
21157
|
};
|
|
20845
|
-
scope.get(
|
|
20846
|
-
|
|
20847
|
-
incidentHistoryHandler
|
|
20848
|
-
);
|
|
20849
|
-
scope.get(
|
|
20850
|
-
"/graph/incident-history/:nodeId",
|
|
20851
|
-
incidentHistoryHandler
|
|
20852
|
-
);
|
|
21158
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21159
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
20853
21160
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
20854
21161
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
20855
21162
|
if (!proj) return;
|
|
@@ -21491,4 +21798,4 @@ export {
|
|
|
21491
21798
|
deprovisionConnector,
|
|
21492
21799
|
buildApi
|
|
21493
21800
|
};
|
|
21494
|
-
//# sourceMappingURL=chunk-
|
|
21801
|
+
//# sourceMappingURL=chunk-IVVF37OU.js.map
|