@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/neatd.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
reconcileDaemonRecordSync,
|
|
4
4
|
startDaemon
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TGCWMMF6.js";
|
|
6
6
|
import {
|
|
7
7
|
listProjects,
|
|
8
8
|
registryPath
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IVVF37OU.js";
|
|
10
10
|
import {
|
|
11
11
|
BindAuthorityError,
|
|
12
12
|
__require
|
package/dist/server.cjs
CHANGED
|
@@ -4706,6 +4706,64 @@ function latencyPercentiles(hist) {
|
|
|
4706
4706
|
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
4707
4707
|
}
|
|
4708
4708
|
|
|
4709
|
+
// src/stacktrace.ts
|
|
4710
|
+
init_cjs_shims();
|
|
4711
|
+
var FRAME_SHAPES = [
|
|
4712
|
+
// `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
|
|
4713
|
+
{ re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
|
|
4714
|
+
// `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
|
|
4715
|
+
{ re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
|
|
4716
|
+
// `at <path>:<line>:<col>` — anonymous call frame, most recent first.
|
|
4717
|
+
{ re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
|
|
4718
|
+
// `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
|
|
4719
|
+
{ re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
|
|
4720
|
+
];
|
|
4721
|
+
var VENDOR_MARKERS = [
|
|
4722
|
+
"node_modules",
|
|
4723
|
+
// dependency root
|
|
4724
|
+
"site-packages",
|
|
4725
|
+
// installed-package root
|
|
4726
|
+
"dist-packages",
|
|
4727
|
+
// distro-packaged root
|
|
4728
|
+
"node:"
|
|
4729
|
+
// runtime-internal module scheme (a stdlib/runtime-root frame)
|
|
4730
|
+
];
|
|
4731
|
+
function matchFrame(line) {
|
|
4732
|
+
for (const shape of FRAME_SHAPES) {
|
|
4733
|
+
const m = shape.re.exec(line);
|
|
4734
|
+
if (!m) continue;
|
|
4735
|
+
const file = m[shape.file];
|
|
4736
|
+
const lineNo = Number(m[shape.line]);
|
|
4737
|
+
if (!file || !Number.isFinite(lineNo)) continue;
|
|
4738
|
+
const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
|
|
4739
|
+
return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
|
|
4740
|
+
}
|
|
4741
|
+
return null;
|
|
4742
|
+
}
|
|
4743
|
+
function isApplicationFrame(file) {
|
|
4744
|
+
if (file.startsWith("<")) return false;
|
|
4745
|
+
const norm = file.split("\\").join("/");
|
|
4746
|
+
for (const marker of VENDOR_MARKERS) {
|
|
4747
|
+
if (norm.includes(marker)) return false;
|
|
4748
|
+
}
|
|
4749
|
+
return true;
|
|
4750
|
+
}
|
|
4751
|
+
function deepestApplicationFrame(stacktrace) {
|
|
4752
|
+
if (!stacktrace) return null;
|
|
4753
|
+
const appFrames = [];
|
|
4754
|
+
for (const raw of stacktrace.split("\n")) {
|
|
4755
|
+
const matched = matchFrame(raw);
|
|
4756
|
+
if (!matched) continue;
|
|
4757
|
+
if (!isApplicationFrame(matched.frame.file)) continue;
|
|
4758
|
+
appFrames.push(matched);
|
|
4759
|
+
}
|
|
4760
|
+
if (appFrames.length === 0) return null;
|
|
4761
|
+
const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
|
|
4762
|
+
const oriented = appFrames.filter((f) => f.deepest === orientation);
|
|
4763
|
+
const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
|
|
4764
|
+
return chosen ? chosen.frame : null;
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4709
4767
|
// src/ingest.ts
|
|
4710
4768
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
4711
4769
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -5557,29 +5615,71 @@ async function appendConnectorIncident(errorsPath, input) {
|
|
|
5557
5615
|
await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(errorsPath), { recursive: true });
|
|
5558
5616
|
await import_node_fs9.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5559
5617
|
}
|
|
5560
|
-
function
|
|
5618
|
+
function landIncidentCallSite(span, callSite, trusted, graph) {
|
|
5619
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
5620
|
+
const canonicalService = serviceNodeName(graph, span) ?? span.service;
|
|
5621
|
+
const recovered = !trusted;
|
|
5622
|
+
if (graph) {
|
|
5623
|
+
const fusedFileId = (0, import_types7.fileId)(canonicalService, relPath);
|
|
5624
|
+
if (graph.hasNode(fusedFileId)) {
|
|
5625
|
+
const node = landObservedSymbol(
|
|
5626
|
+
graph,
|
|
5627
|
+
fusedFileId,
|
|
5628
|
+
canonicalService,
|
|
5629
|
+
relPath,
|
|
5630
|
+
{ ...callSite, relPath },
|
|
5631
|
+
false
|
|
5632
|
+
);
|
|
5633
|
+
return {
|
|
5634
|
+
affectedNode: node,
|
|
5635
|
+
...recovered ? {
|
|
5636
|
+
codeFilepath: relPath,
|
|
5637
|
+
...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
|
|
5638
|
+
} : {}
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
if (recovered) return null;
|
|
5642
|
+
}
|
|
5643
|
+
return { affectedNode: (0, import_types7.fileId)(span.service, relPath) };
|
|
5644
|
+
}
|
|
5645
|
+
function serviceNodeName(graph, span) {
|
|
5646
|
+
if (!graph) return void 0;
|
|
5647
|
+
const sid = resolveFusedServiceId(graph, span.service, span.env);
|
|
5648
|
+
if (!graph.hasNode(sid)) return void 0;
|
|
5649
|
+
const node = graph.getNodeAttributes(sid);
|
|
5650
|
+
return typeof node.name === "string" ? node.name : void 0;
|
|
5651
|
+
}
|
|
5652
|
+
function stacktraceCallSite(span, serviceNode, scanPath) {
|
|
5653
|
+
const frame = deepestApplicationFrame(span.exception?.stacktrace);
|
|
5654
|
+
if (!frame) return null;
|
|
5655
|
+
const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
|
|
5656
|
+
if (!relPath) return null;
|
|
5657
|
+
return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
|
|
5658
|
+
}
|
|
5659
|
+
function incidentLocus(span, graph, scanPath) {
|
|
5561
5660
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types7.serviceId)(span.service, span.env);
|
|
5562
5661
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
5563
5662
|
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
5564
5663
|
if (callSite) {
|
|
5565
|
-
const
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
fusedFileId,
|
|
5573
|
-
canonicalService,
|
|
5574
|
-
relPath,
|
|
5575
|
-
{ ...callSite, relPath },
|
|
5576
|
-
false
|
|
5577
|
-
);
|
|
5578
|
-
}
|
|
5664
|
+
const landed = landIncidentCallSite(span, callSite, true, graph);
|
|
5665
|
+
if (landed) return landed;
|
|
5666
|
+
} else {
|
|
5667
|
+
const recovered = stacktraceCallSite(span, serviceNode, scanPath);
|
|
5668
|
+
if (recovered) {
|
|
5669
|
+
const landed = landIncidentCallSite(span, recovered, false, graph);
|
|
5670
|
+
if (landed) return landed;
|
|
5579
5671
|
}
|
|
5580
|
-
return (0, import_types7.fileId)(span.service, relPath);
|
|
5581
5672
|
}
|
|
5582
|
-
return sid;
|
|
5673
|
+
return { affectedNode: sid };
|
|
5674
|
+
}
|
|
5675
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
5676
|
+
return incidentLocus(span, graph, scanPath).affectedNode;
|
|
5677
|
+
}
|
|
5678
|
+
function withRecoveredCodeAttrs(attrs, locus) {
|
|
5679
|
+
if (locus.codeFilepath === void 0) return attrs;
|
|
5680
|
+
attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
|
|
5681
|
+
if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
|
|
5682
|
+
return attrs;
|
|
5583
5683
|
}
|
|
5584
5684
|
function sanitizeAttributes(attrs) {
|
|
5585
5685
|
const out = {};
|
|
@@ -5612,7 +5712,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
5612
5712
|
await appendErrorEvent(ctx, ev);
|
|
5613
5713
|
}
|
|
5614
5714
|
async function recordExceptionIncident(ctx, span, ts) {
|
|
5615
|
-
const
|
|
5715
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
5716
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
5616
5717
|
const ev = {
|
|
5617
5718
|
id: `${span.traceId}:${span.spanId}`,
|
|
5618
5719
|
timestamp: ts,
|
|
@@ -5623,7 +5724,7 @@ async function recordExceptionIncident(ctx, span, ts) {
|
|
|
5623
5724
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
5624
5725
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
5625
5726
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
5626
|
-
affectedNode:
|
|
5727
|
+
affectedNode: locus.affectedNode
|
|
5627
5728
|
};
|
|
5628
5729
|
await appendErrorEvent(ctx, ev);
|
|
5629
5730
|
}
|
|
@@ -5926,7 +6027,8 @@ async function handleSpan(ctx, span) {
|
|
|
5926
6027
|
if (span.statusCode === 2) {
|
|
5927
6028
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
5928
6029
|
if (ctx.writeErrorEventInline !== false) {
|
|
5929
|
-
const
|
|
6030
|
+
const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
|
|
6031
|
+
const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
|
|
5930
6032
|
const ev = {
|
|
5931
6033
|
id: `${span.traceId}:${span.spanId}`,
|
|
5932
6034
|
timestamp: ts,
|
|
@@ -5938,10 +6040,11 @@ async function handleSpan(ctx, span) {
|
|
|
5938
6040
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
5939
6041
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
5940
6042
|
// Attribute to where the failure originated — the symbol / file / service
|
|
5941
|
-
// the throwing span named (incidentAffectedNode
|
|
5942
|
-
//
|
|
5943
|
-
//
|
|
5944
|
-
|
|
6043
|
+
// the throwing span named (incidentAffectedNode / ADR-191, extended to
|
|
6044
|
+
// recover the locus from the stacktrace when the span stamped no code.*
|
|
6045
|
+
// attrs, ADR-216) — the same source-based attribution the durable
|
|
6046
|
+
// receiver write uses, not the outbound edge target this span minted.
|
|
6047
|
+
affectedNode: locus.affectedNode
|
|
5945
6048
|
};
|
|
5946
6049
|
await appendErrorEvent(ctx, ev);
|
|
5947
6050
|
}
|
|
@@ -6115,15 +6218,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6115
6218
|
clearInterval(interval);
|
|
6116
6219
|
};
|
|
6117
6220
|
}
|
|
6118
|
-
|
|
6221
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
6222
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
6223
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
6224
|
+
const handle = await import_node_fs9.promises.open(errorsPath, "r");
|
|
6225
|
+
try {
|
|
6226
|
+
const { size } = await handle.stat();
|
|
6227
|
+
if (size <= maxBytes) {
|
|
6228
|
+
return (await handle.readFile()).toString("utf8");
|
|
6229
|
+
}
|
|
6230
|
+
const buf = Buffer.alloc(maxBytes);
|
|
6231
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
6232
|
+
const raw = buf.toString("utf8");
|
|
6233
|
+
const firstNewline = raw.indexOf("\n");
|
|
6234
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
6235
|
+
} finally {
|
|
6236
|
+
await handle.close();
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
6240
|
+
let raw;
|
|
6119
6241
|
try {
|
|
6120
|
-
|
|
6121
|
-
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6122
|
-
return dedupeIncidents(events);
|
|
6242
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6123
6243
|
} catch (err) {
|
|
6124
6244
|
if (err.code === "ENOENT") return [];
|
|
6125
6245
|
throw err;
|
|
6126
6246
|
}
|
|
6247
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6248
|
+
const deduped = dedupeIncidents(events);
|
|
6249
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
6250
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6127
6251
|
}
|
|
6128
6252
|
function isSynthesizedHttpIncident(ev) {
|
|
6129
6253
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -6883,6 +7007,47 @@ function classifyNode(ctx) {
|
|
|
6883
7007
|
function isVictimSeed(ctx) {
|
|
6884
7008
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
6885
7009
|
}
|
|
7010
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
7011
|
+
"name resolution",
|
|
7012
|
+
"resolve host",
|
|
7013
|
+
"getaddrinfo",
|
|
7014
|
+
"enotfound",
|
|
7015
|
+
"connection refused",
|
|
7016
|
+
"econnrefused",
|
|
7017
|
+
"connection reset",
|
|
7018
|
+
"econnreset",
|
|
7019
|
+
"able to connect",
|
|
7020
|
+
"failed to connect",
|
|
7021
|
+
"cannot connect",
|
|
7022
|
+
"could not connect",
|
|
7023
|
+
"unable to connect",
|
|
7024
|
+
"connection timed out",
|
|
7025
|
+
"etimedout",
|
|
7026
|
+
"no route to host",
|
|
7027
|
+
"host unreachable",
|
|
7028
|
+
"network is unreachable",
|
|
7029
|
+
"connection closed"
|
|
7030
|
+
];
|
|
7031
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
7032
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
7033
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
7034
|
+
}
|
|
7035
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
7036
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
7037
|
+
if (!graph.hasNode(n)) continue;
|
|
7038
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
7039
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
7040
|
+
if (e.type === import_types8.EdgeType.CONTAINS) continue;
|
|
7041
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
7044
|
+
if (seedSource === "incident" && incidents) {
|
|
7045
|
+
for (const ev of incidents) {
|
|
7046
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
7047
|
+
}
|
|
7048
|
+
}
|
|
7049
|
+
return false;
|
|
7050
|
+
}
|
|
6886
7051
|
function grainOf(graph, nodeId) {
|
|
6887
7052
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
6888
7053
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -7046,7 +7211,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
7046
7211
|
const candidates = [];
|
|
7047
7212
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
7048
7213
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
7049
|
-
|
|
7214
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
7215
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
7050
7216
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
7051
7217
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
7052
7218
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -7396,6 +7562,120 @@ function detectColumnDrift(node) {
|
|
|
7396
7562
|
}
|
|
7397
7563
|
return out;
|
|
7398
7564
|
}
|
|
7565
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
7566
|
+
{
|
|
7567
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
7568
|
+
kind: "missing-attribute",
|
|
7569
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
7570
|
+
},
|
|
7571
|
+
{
|
|
7572
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
7573
|
+
kind: "missing-field",
|
|
7574
|
+
patterns: [
|
|
7575
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
7576
|
+
]
|
|
7577
|
+
},
|
|
7578
|
+
{
|
|
7579
|
+
// "has no property X", "no property named X".
|
|
7580
|
+
kind: "missing-property",
|
|
7581
|
+
patterns: [
|
|
7582
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
7583
|
+
]
|
|
7584
|
+
},
|
|
7585
|
+
{
|
|
7586
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
7587
|
+
kind: "missing-column",
|
|
7588
|
+
patterns: [
|
|
7589
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
7590
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
7591
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
7592
|
+
]
|
|
7593
|
+
},
|
|
7594
|
+
{
|
|
7595
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
7596
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
7597
|
+
// mismatch) does not get miscategorised here.
|
|
7598
|
+
kind: "undefined-method",
|
|
7599
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
7600
|
+
}
|
|
7601
|
+
];
|
|
7602
|
+
function classifySymbolMismatch(message) {
|
|
7603
|
+
for (const entry of SYMBOL_MISMATCH_PATTERNS) {
|
|
7604
|
+
for (const re of entry.patterns) {
|
|
7605
|
+
const m = re.exec(message);
|
|
7606
|
+
if (m) {
|
|
7607
|
+
const captured = m[1];
|
|
7608
|
+
return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
return null;
|
|
7613
|
+
}
|
|
7614
|
+
function symbolLocus(graph, ev) {
|
|
7615
|
+
const attrs = ev.attributes ?? {};
|
|
7616
|
+
const filepath = codeFilepathOf(attrs);
|
|
7617
|
+
const lineno = codeLinenoOf(attrs);
|
|
7618
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
7619
|
+
const affected = ev.affectedNode;
|
|
7620
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
7621
|
+
const affectedIsCode = affectedInGraph && ((0, import_types9.parseSymbolId)(affected) !== null || (0, import_types9.parseFileId)(affected) !== null);
|
|
7622
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
7623
|
+
if (!location) return null;
|
|
7624
|
+
if (affectedInGraph) return { node: affected, location };
|
|
7625
|
+
const svc = (0, import_types9.serviceId)(ev.service);
|
|
7626
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
7627
|
+
return null;
|
|
7628
|
+
}
|
|
7629
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
7630
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
7631
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7632
|
+
for (const ev of incidents) {
|
|
7633
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
7634
|
+
if (!classified) continue;
|
|
7635
|
+
const locus = symbolLocus(graph, ev);
|
|
7636
|
+
if (!locus) continue;
|
|
7637
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
7638
|
+
const existing = groups.get(key);
|
|
7639
|
+
if (!existing) {
|
|
7640
|
+
groups.set(key, {
|
|
7641
|
+
node: locus.node,
|
|
7642
|
+
kind: classified.kind,
|
|
7643
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
7644
|
+
...locus.location ? { location: locus.location } : {},
|
|
7645
|
+
latest: ev,
|
|
7646
|
+
count: 1
|
|
7647
|
+
});
|
|
7648
|
+
} else {
|
|
7649
|
+
existing.count += 1;
|
|
7650
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
7651
|
+
existing.latest = ev;
|
|
7652
|
+
if (locus.location) existing.location = locus.location;
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7655
|
+
}
|
|
7656
|
+
const out = [];
|
|
7657
|
+
for (const g of groups.values()) {
|
|
7658
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
7659
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
7660
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
7661
|
+
out.push({
|
|
7662
|
+
type: "observed-symbol-mismatch",
|
|
7663
|
+
source: g.node,
|
|
7664
|
+
target: g.node,
|
|
7665
|
+
mismatchKind: g.kind,
|
|
7666
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
7667
|
+
...g.location ? { location: g.location } : {},
|
|
7668
|
+
provenance: import_types9.Provenance.INFERRED,
|
|
7669
|
+
incidentId: g.latest.id,
|
|
7670
|
+
errorMessage: g.latest.errorMessage,
|
|
7671
|
+
incidentCount: g.count,
|
|
7672
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
7673
|
+
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.`,
|
|
7674
|
+
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."
|
|
7675
|
+
});
|
|
7676
|
+
}
|
|
7677
|
+
return out;
|
|
7678
|
+
}
|
|
7399
7679
|
function involvesNode(d, nodeId) {
|
|
7400
7680
|
return d.source === nodeId || d.target === nodeId;
|
|
7401
7681
|
}
|
|
@@ -7481,6 +7761,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7481
7761
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
7482
7762
|
}
|
|
7483
7763
|
});
|
|
7764
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
7765
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
7766
|
+
}
|
|
7484
7767
|
const reconciled = suppressHostMismatchHalves(all);
|
|
7485
7768
|
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
7486
7769
|
let filtered = dampened;
|
|
@@ -7501,7 +7784,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7501
7784
|
"missing-observed": 1,
|
|
7502
7785
|
"version-mismatch": 2,
|
|
7503
7786
|
"host-mismatch": 3,
|
|
7504
|
-
"compat-violation": 4
|
|
7787
|
+
"compat-violation": 4,
|
|
7788
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
7789
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
7790
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
7791
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
7792
|
+
"observed-symbol-mismatch": 5
|
|
7505
7793
|
};
|
|
7506
7794
|
filtered.sort((a, b) => {
|
|
7507
7795
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -7512,7 +7800,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7512
7800
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
7513
7801
|
const ac = "column" in a && a.column ? a.column : "";
|
|
7514
7802
|
const bc = "column" in b && b.column ? b.column : "";
|
|
7515
|
-
return ac.localeCompare(bc);
|
|
7803
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
7804
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
7805
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
7806
|
+
return asym.localeCompare(bsym);
|
|
7516
7807
|
});
|
|
7517
7808
|
return import_types9.DivergenceResultSchema.parse({
|
|
7518
7809
|
divergences: filtered,
|
|
@@ -16692,10 +16983,15 @@ function divergenceLine(d) {
|
|
|
16692
16983
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16693
16984
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16694
16985
|
}
|
|
16986
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
16987
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
16988
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
16989
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
16990
|
+
}
|
|
16695
16991
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16696
16992
|
}
|
|
16697
|
-
function buildDivergenceSection(graph, node) {
|
|
16698
|
-
const result = computeDivergences(graph, { node });
|
|
16993
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
16994
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
16699
16995
|
if (result.totalAffected === 0) return null;
|
|
16700
16996
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16701
16997
|
text: divergenceLine(d),
|
|
@@ -16708,8 +17004,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
16708
17004
|
facts
|
|
16709
17005
|
};
|
|
16710
17006
|
}
|
|
16711
|
-
function buildGlobalDivergenceSection(graph) {
|
|
16712
|
-
const result = computeDivergences(graph);
|
|
17007
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17008
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16713
17009
|
if (result.totalAffected === 0) {
|
|
16714
17010
|
return {
|
|
16715
17011
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -16807,7 +17103,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16807
17103
|
}))
|
|
16808
17104
|
});
|
|
16809
17105
|
}
|
|
16810
|
-
const div = computeDivergences(graph);
|
|
17106
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16811
17107
|
sections.push({
|
|
16812
17108
|
heading: "Divergences",
|
|
16813
17109
|
facts: [
|
|
@@ -16821,7 +17117,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16821
17117
|
function buildGlobalSections(intent, graph, incidents) {
|
|
16822
17118
|
switch (intent) {
|
|
16823
17119
|
case "divergence":
|
|
16824
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17120
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
16825
17121
|
case "incidents":
|
|
16826
17122
|
return [buildGlobalIncidentsSection(incidents)];
|
|
16827
17123
|
case "overview":
|
|
@@ -16852,7 +17148,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
16852
17148
|
case "incidents":
|
|
16853
17149
|
return buildIncidentsSection(node, incidents);
|
|
16854
17150
|
case "divergence":
|
|
16855
|
-
return buildDivergenceSection(graph, node);
|
|
17151
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
16856
17152
|
}
|
|
16857
17153
|
}
|
|
16858
17154
|
function summarizeGlobal(intent, sections) {
|
|
@@ -20657,6 +20953,8 @@ function buildRegistration(entry, graph, env = process.env) {
|
|
|
20657
20953
|
}
|
|
20658
20954
|
|
|
20659
20955
|
// src/api.ts
|
|
20956
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
20957
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
20660
20958
|
function serializeGraph(graph) {
|
|
20661
20959
|
const nodes = [];
|
|
20662
20960
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -20836,10 +21134,13 @@ function registerRoutes(scope, ctx) {
|
|
|
20836
21134
|
}
|
|
20837
21135
|
minConfidence = n;
|
|
20838
21136
|
}
|
|
21137
|
+
const epath = errorsPathFor(proj);
|
|
21138
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
20839
21139
|
return computeDivergences(proj.graph, {
|
|
20840
21140
|
...typeFilter ? { type: typeFilter } : {},
|
|
20841
21141
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
20842
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
21142
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
21143
|
+
incidents
|
|
20843
21144
|
});
|
|
20844
21145
|
});
|
|
20845
21146
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -20849,10 +21150,11 @@ function registerRoutes(scope, ctx) {
|
|
|
20849
21150
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
20850
21151
|
const events = await readErrorEvents(epath);
|
|
20851
21152
|
const total = events.length;
|
|
20852
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
20853
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21153
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21154
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
20854
21155
|
const sliced = events.slice(0, safeLimit);
|
|
20855
|
-
|
|
21156
|
+
const omitted = total - sliced.length;
|
|
21157
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
20856
21158
|
});
|
|
20857
21159
|
scope.get("/stale-events", async (req, reply) => {
|
|
20858
21160
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -20961,16 +21263,20 @@ function registerRoutes(scope, ctx) {
|
|
|
20961
21263
|
const filtered = events.filter(
|
|
20962
21264
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
20963
21265
|
);
|
|
20964
|
-
|
|
21266
|
+
const total = filtered.length;
|
|
21267
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21268
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21269
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
21270
|
+
const omitted = total - recent.length;
|
|
21271
|
+
return {
|
|
21272
|
+
count: recent.length,
|
|
21273
|
+
total,
|
|
21274
|
+
events: recent,
|
|
21275
|
+
...omitted > 0 ? { omitted } : {}
|
|
21276
|
+
};
|
|
20965
21277
|
};
|
|
20966
|
-
scope.get(
|
|
20967
|
-
|
|
20968
|
-
incidentHistoryHandler
|
|
20969
|
-
);
|
|
20970
|
-
scope.get(
|
|
20971
|
-
"/graph/incident-history/:nodeId",
|
|
20972
|
-
incidentHistoryHandler
|
|
20973
|
-
);
|
|
21278
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21279
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
20974
21280
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
20975
21281
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
20976
21282
|
if (!proj) return;
|