@neat.is/core 0.9.2 → 0.9.3
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-TMHCS4ZY.js → chunk-2D4Y5QHE.js} +2 -2
- package/dist/{chunk-HD5X5TWY.js → chunk-EDE4XP2M.js} +287 -32
- package/dist/chunk-EDE4XP2M.js.map +1 -0
- package/dist/cli.cjs +297 -32
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +285 -31
- 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 +285 -31
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +285 -31
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-HD5X5TWY.js.map +0 -1
- /package/dist/{chunk-TMHCS4ZY.js.map → chunk-2D4Y5QHE.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` : "";
|
|
@@ -6868,15 +6910,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6868
6910
|
clearInterval(interval);
|
|
6869
6911
|
};
|
|
6870
6912
|
}
|
|
6871
|
-
|
|
6913
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
6914
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
6915
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
6916
|
+
const handle = await import_node_fs8.promises.open(errorsPath, "r");
|
|
6872
6917
|
try {
|
|
6873
|
-
const
|
|
6874
|
-
|
|
6875
|
-
|
|
6918
|
+
const { size } = await handle.stat();
|
|
6919
|
+
if (size <= maxBytes) {
|
|
6920
|
+
return (await handle.readFile()).toString("utf8");
|
|
6921
|
+
}
|
|
6922
|
+
const buf = Buffer.alloc(maxBytes);
|
|
6923
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
6924
|
+
const raw = buf.toString("utf8");
|
|
6925
|
+
const firstNewline = raw.indexOf("\n");
|
|
6926
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
6927
|
+
} finally {
|
|
6928
|
+
await handle.close();
|
|
6929
|
+
}
|
|
6930
|
+
}
|
|
6931
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
6932
|
+
let raw;
|
|
6933
|
+
try {
|
|
6934
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6876
6935
|
} catch (err) {
|
|
6877
6936
|
if (err.code === "ENOENT") return [];
|
|
6878
6937
|
throw err;
|
|
6879
6938
|
}
|
|
6939
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6940
|
+
const deduped = dedupeIncidents(events);
|
|
6941
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
6942
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6880
6943
|
}
|
|
6881
6944
|
function isSynthesizedHttpIncident(ev) {
|
|
6882
6945
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -9912,12 +9975,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse12) {
|
|
|
9912
9975
|
const value = await interpolateEnvRefs(raw, serviceDir);
|
|
9913
9976
|
if (!looksLike(value)) continue;
|
|
9914
9977
|
const parsed = parse12(value);
|
|
9915
|
-
if (parsed) out.push(parsed);
|
|
9978
|
+
if (parsed) out.push({ ...parsed, hostSource: "config" });
|
|
9916
9979
|
}
|
|
9917
9980
|
for (const lit of literals) {
|
|
9918
9981
|
if (!looksLike(lit)) continue;
|
|
9919
9982
|
const parsed = parse12(lit);
|
|
9920
|
-
if (parsed) out.push(parsed);
|
|
9983
|
+
if (parsed) out.push({ ...parsed, hostSource: "literal" });
|
|
9921
9984
|
}
|
|
9922
9985
|
return out;
|
|
9923
9986
|
}
|
|
@@ -10186,7 +10249,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
10186
10249
|
type: import_types23.EdgeType.CONNECTS_TO,
|
|
10187
10250
|
provenance: import_types23.Provenance.EXTRACTED,
|
|
10188
10251
|
confidence: (0, import_types23.confidenceForExtracted)("structural"),
|
|
10189
|
-
|
|
10252
|
+
// Carry how the host was recovered (ADR-213) so the divergence ranker
|
|
10253
|
+
// can tell a real declared store from a hardcoded fault-injection /
|
|
10254
|
+
// flag-gated probe. Only set when the parser distinguished the two.
|
|
10255
|
+
evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
|
|
10190
10256
|
};
|
|
10191
10257
|
if (!graph.hasEdge(edge.id)) {
|
|
10192
10258
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -15978,9 +16044,170 @@ function detectColumnDrift(node) {
|
|
|
15978
16044
|
}
|
|
15979
16045
|
return out;
|
|
15980
16046
|
}
|
|
16047
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16048
|
+
{
|
|
16049
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
16050
|
+
kind: "missing-attribute",
|
|
16051
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
16052
|
+
},
|
|
16053
|
+
{
|
|
16054
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
16055
|
+
kind: "missing-field",
|
|
16056
|
+
patterns: [
|
|
16057
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
16058
|
+
]
|
|
16059
|
+
},
|
|
16060
|
+
{
|
|
16061
|
+
// "has no property X", "no property named X".
|
|
16062
|
+
kind: "missing-property",
|
|
16063
|
+
patterns: [
|
|
16064
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
16065
|
+
]
|
|
16066
|
+
},
|
|
16067
|
+
{
|
|
16068
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
16069
|
+
kind: "missing-column",
|
|
16070
|
+
patterns: [
|
|
16071
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16072
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16073
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
16074
|
+
]
|
|
16075
|
+
},
|
|
16076
|
+
{
|
|
16077
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
16078
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
16079
|
+
// mismatch) does not get miscategorised here.
|
|
16080
|
+
kind: "undefined-method",
|
|
16081
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
16082
|
+
}
|
|
16083
|
+
];
|
|
16084
|
+
function classifySymbolMismatch(message) {
|
|
16085
|
+
for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
|
|
16086
|
+
for (const re of entry2.patterns) {
|
|
16087
|
+
const m = re.exec(message);
|
|
16088
|
+
if (m) {
|
|
16089
|
+
const captured = m[1];
|
|
16090
|
+
return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
|
|
16091
|
+
}
|
|
16092
|
+
}
|
|
16093
|
+
}
|
|
16094
|
+
return null;
|
|
16095
|
+
}
|
|
16096
|
+
function symbolLocus(graph, ev) {
|
|
16097
|
+
const attrs = ev.attributes ?? {};
|
|
16098
|
+
const filepath = codeFilepathOf(attrs);
|
|
16099
|
+
const lineno = codeLinenoOf(attrs);
|
|
16100
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
16101
|
+
const affected = ev.affectedNode;
|
|
16102
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
16103
|
+
const affectedIsCode = affectedInGraph && ((0, import_types57.parseSymbolId)(affected) !== null || (0, import_types57.parseFileId)(affected) !== null);
|
|
16104
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
16105
|
+
if (!location) return null;
|
|
16106
|
+
if (affectedInGraph) return { node: affected, location };
|
|
16107
|
+
const svc = (0, import_types57.serviceId)(ev.service);
|
|
16108
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
16109
|
+
return null;
|
|
16110
|
+
}
|
|
16111
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
16112
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
16113
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16114
|
+
for (const ev of incidents) {
|
|
16115
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
16116
|
+
if (!classified) continue;
|
|
16117
|
+
const locus = symbolLocus(graph, ev);
|
|
16118
|
+
if (!locus) continue;
|
|
16119
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
16120
|
+
const existing = groups.get(key);
|
|
16121
|
+
if (!existing) {
|
|
16122
|
+
groups.set(key, {
|
|
16123
|
+
node: locus.node,
|
|
16124
|
+
kind: classified.kind,
|
|
16125
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
16126
|
+
...locus.location ? { location: locus.location } : {},
|
|
16127
|
+
latest: ev,
|
|
16128
|
+
count: 1
|
|
16129
|
+
});
|
|
16130
|
+
} else {
|
|
16131
|
+
existing.count += 1;
|
|
16132
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
16133
|
+
existing.latest = ev;
|
|
16134
|
+
if (locus.location) existing.location = locus.location;
|
|
16135
|
+
}
|
|
16136
|
+
}
|
|
16137
|
+
}
|
|
16138
|
+
const out = [];
|
|
16139
|
+
for (const g of groups.values()) {
|
|
16140
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
16141
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
16142
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
16143
|
+
out.push({
|
|
16144
|
+
type: "observed-symbol-mismatch",
|
|
16145
|
+
source: g.node,
|
|
16146
|
+
target: g.node,
|
|
16147
|
+
mismatchKind: g.kind,
|
|
16148
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
16149
|
+
...g.location ? { location: g.location } : {},
|
|
16150
|
+
provenance: import_types57.Provenance.INFERRED,
|
|
16151
|
+
incidentId: g.latest.id,
|
|
16152
|
+
errorMessage: g.latest.errorMessage,
|
|
16153
|
+
incidentCount: g.count,
|
|
16154
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
16155
|
+
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.`,
|
|
16156
|
+
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."
|
|
16157
|
+
});
|
|
16158
|
+
}
|
|
16159
|
+
return out;
|
|
16160
|
+
}
|
|
15981
16161
|
function involvesNode(d, nodeId) {
|
|
15982
16162
|
return d.source === nodeId || d.target === nodeId;
|
|
15983
16163
|
}
|
|
16164
|
+
function datastoreEverObserved(graph, nodeId) {
|
|
16165
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
16166
|
+
const n = graph.getNodeAttributes(nodeId);
|
|
16167
|
+
if (n.type === import_types57.NodeType.DatabaseNode) {
|
|
16168
|
+
const via = n.discoveredVia;
|
|
16169
|
+
if (via === "otel" || via === "merged") return true;
|
|
16170
|
+
}
|
|
16171
|
+
for (const edgeId of graph.inboundEdges(nodeId)) {
|
|
16172
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
16173
|
+
if (e.provenance === import_types57.Provenance.OBSERVED || e.provenance === import_types57.Provenance.STALE) return true;
|
|
16174
|
+
}
|
|
16175
|
+
return false;
|
|
16176
|
+
}
|
|
16177
|
+
function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
|
|
16178
|
+
for (const bucket of buckets2.values()) {
|
|
16179
|
+
if (bucket.type !== import_types57.EdgeType.CONNECTS_TO) continue;
|
|
16180
|
+
if (bucket.source !== serviceId16) continue;
|
|
16181
|
+
if (bucket.target === excludeTarget) continue;
|
|
16182
|
+
if (!graph.hasNode(bucket.target)) continue;
|
|
16183
|
+
const target = graph.getNodeAttributes(bucket.target);
|
|
16184
|
+
if (target.type !== import_types57.NodeType.DatabaseNode) continue;
|
|
16185
|
+
if (target.engine !== engine) continue;
|
|
16186
|
+
if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
|
|
16187
|
+
}
|
|
16188
|
+
return false;
|
|
16189
|
+
}
|
|
16190
|
+
var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
|
|
16191
|
+
function dampenDeadCodeProbes(graph, buckets2, all) {
|
|
16192
|
+
return all.map((d) => {
|
|
16193
|
+
if (d.type !== "missing-observed") return d;
|
|
16194
|
+
if (!d.extracted || d.edgeType !== import_types57.EdgeType.CONNECTS_TO) return d;
|
|
16195
|
+
if (!graph.hasNode(d.target)) return d;
|
|
16196
|
+
const target = graph.getNodeAttributes(d.target);
|
|
16197
|
+
if (target.type !== import_types57.NodeType.DatabaseNode) return d;
|
|
16198
|
+
if (d.extracted.evidence?.hostSource !== "literal") return d;
|
|
16199
|
+
if (datastoreEverObserved(graph, d.target)) return d;
|
|
16200
|
+
const engine = target.engine;
|
|
16201
|
+
if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
|
|
16202
|
+
const host = target.host ?? target.name;
|
|
16203
|
+
return {
|
|
16204
|
+
...d,
|
|
16205
|
+
confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
|
|
16206
|
+
reason: `${d.source} declares a ${engine} connection to a hardcoded-literal host (${host}) that production has never observed, while it does observe another ${engine} store \u2014 this reads as a flag-gated or dead-code declaration (e.g. a fault-injection probe), not a real declared-vs-observed gap.`,
|
|
16207
|
+
recommendation: "Confirm this is an intentional dead alternate \u2014 a fault-injection probe or a flag-gated branch. If it is meant to run in production, check the feature flag or conditional that gates it; if not, it can be ignored or removed."
|
|
16208
|
+
};
|
|
16209
|
+
});
|
|
16210
|
+
}
|
|
15984
16211
|
function suppressHostMismatchHalves(all) {
|
|
15985
16212
|
const observedHalf = /* @__PURE__ */ new Set();
|
|
15986
16213
|
const declaredHalf = /* @__PURE__ */ new Set();
|
|
@@ -16016,8 +16243,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16016
16243
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
16017
16244
|
}
|
|
16018
16245
|
});
|
|
16246
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
16247
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
16248
|
+
}
|
|
16019
16249
|
const reconciled = suppressHostMismatchHalves(all);
|
|
16020
|
-
|
|
16250
|
+
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
16251
|
+
let filtered = dampened;
|
|
16021
16252
|
if (opts.type) {
|
|
16022
16253
|
const allowed = opts.type;
|
|
16023
16254
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -16035,7 +16266,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16035
16266
|
"missing-observed": 1,
|
|
16036
16267
|
"version-mismatch": 2,
|
|
16037
16268
|
"host-mismatch": 3,
|
|
16038
|
-
"compat-violation": 4
|
|
16269
|
+
"compat-violation": 4,
|
|
16270
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
16271
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
16272
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
16273
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
16274
|
+
"observed-symbol-mismatch": 5
|
|
16039
16275
|
};
|
|
16040
16276
|
filtered.sort((a, b) => {
|
|
16041
16277
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -16046,7 +16282,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16046
16282
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
16047
16283
|
const ac = "column" in a && a.column ? a.column : "";
|
|
16048
16284
|
const bc = "column" in b && b.column ? b.column : "";
|
|
16049
|
-
return ac.localeCompare(bc);
|
|
16285
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
16286
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
16287
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
16288
|
+
return asym.localeCompare(bsym);
|
|
16050
16289
|
});
|
|
16051
16290
|
return import_types57.DivergenceResultSchema.parse({
|
|
16052
16291
|
divergences: filtered,
|
|
@@ -17106,10 +17345,15 @@ function divergenceLine(d) {
|
|
|
17106
17345
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
17107
17346
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
17108
17347
|
}
|
|
17348
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17349
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17350
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17351
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17352
|
+
}
|
|
17109
17353
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
17110
17354
|
}
|
|
17111
|
-
function buildDivergenceSection(graph, node) {
|
|
17112
|
-
const result = computeDivergences(graph, { node });
|
|
17355
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17356
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
17113
17357
|
if (result.totalAffected === 0) return null;
|
|
17114
17358
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
17115
17359
|
text: divergenceLine(d),
|
|
@@ -17122,8 +17366,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
17122
17366
|
facts
|
|
17123
17367
|
};
|
|
17124
17368
|
}
|
|
17125
|
-
function buildGlobalDivergenceSection(graph) {
|
|
17126
|
-
const result = computeDivergences(graph);
|
|
17369
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17370
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17127
17371
|
if (result.totalAffected === 0) {
|
|
17128
17372
|
return {
|
|
17129
17373
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -17221,7 +17465,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17221
17465
|
}))
|
|
17222
17466
|
});
|
|
17223
17467
|
}
|
|
17224
|
-
const div = computeDivergences(graph);
|
|
17468
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17225
17469
|
sections.push({
|
|
17226
17470
|
heading: "Divergences",
|
|
17227
17471
|
facts: [
|
|
@@ -17235,7 +17479,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17235
17479
|
function buildGlobalSections(intent, graph, incidents) {
|
|
17236
17480
|
switch (intent) {
|
|
17237
17481
|
case "divergence":
|
|
17238
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17482
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
17239
17483
|
case "incidents":
|
|
17240
17484
|
return [buildGlobalIncidentsSection(incidents)];
|
|
17241
17485
|
case "overview":
|
|
@@ -17266,7 +17510,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
17266
17510
|
case "incidents":
|
|
17267
17511
|
return buildIncidentsSection(node, incidents);
|
|
17268
17512
|
case "divergence":
|
|
17269
|
-
return buildDivergenceSection(graph, node);
|
|
17513
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
17270
17514
|
}
|
|
17271
17515
|
}
|
|
17272
17516
|
function summarizeGlobal(intent, sections) {
|
|
@@ -21486,6 +21730,8 @@ async function deprovisionConnector(entry2, env = process.env, fetchImpl) {
|
|
|
21486
21730
|
}
|
|
21487
21731
|
|
|
21488
21732
|
// src/api.ts
|
|
21733
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
21734
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
21489
21735
|
function serializeGraph(graph) {
|
|
21490
21736
|
const nodes = [];
|
|
21491
21737
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -21665,10 +21911,13 @@ function registerRoutes(scope, ctx) {
|
|
|
21665
21911
|
}
|
|
21666
21912
|
minConfidence = n;
|
|
21667
21913
|
}
|
|
21914
|
+
const epath = errorsPathFor(proj);
|
|
21915
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
21668
21916
|
return computeDivergences(proj.graph, {
|
|
21669
21917
|
...typeFilter ? { type: typeFilter } : {},
|
|
21670
21918
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
21671
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
21919
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
21920
|
+
incidents
|
|
21672
21921
|
});
|
|
21673
21922
|
});
|
|
21674
21923
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -21678,10 +21927,11 @@ function registerRoutes(scope, ctx) {
|
|
|
21678
21927
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
21679
21928
|
const events = await readErrorEvents(epath);
|
|
21680
21929
|
const total = events.length;
|
|
21681
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
21682
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21930
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21931
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21683
21932
|
const sliced = events.slice(0, safeLimit);
|
|
21684
|
-
|
|
21933
|
+
const omitted = total - sliced.length;
|
|
21934
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
21685
21935
|
});
|
|
21686
21936
|
scope.get("/stale-events", async (req, reply) => {
|
|
21687
21937
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -21790,16 +22040,20 @@ function registerRoutes(scope, ctx) {
|
|
|
21790
22040
|
const filtered = events.filter(
|
|
21791
22041
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
21792
22042
|
);
|
|
21793
|
-
|
|
22043
|
+
const total = filtered.length;
|
|
22044
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
22045
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
22046
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
22047
|
+
const omitted = total - recent.length;
|
|
22048
|
+
return {
|
|
22049
|
+
count: recent.length,
|
|
22050
|
+
total,
|
|
22051
|
+
events: recent,
|
|
22052
|
+
...omitted > 0 ? { omitted } : {}
|
|
22053
|
+
};
|
|
21794
22054
|
};
|
|
21795
|
-
scope.get(
|
|
21796
|
-
|
|
21797
|
-
incidentHistoryHandler
|
|
21798
|
-
);
|
|
21799
|
-
scope.get(
|
|
21800
|
-
"/graph/incident-history/:nodeId",
|
|
21801
|
-
incidentHistoryHandler
|
|
21802
|
-
);
|
|
22055
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
22056
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
21803
22057
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
21804
22058
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
21805
22059
|
if (!proj) return;
|
|
@@ -28790,6 +29044,11 @@ function formatDivergenceLine(d) {
|
|
|
28790
29044
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
28791
29045
|
case "compat-violation":
|
|
28792
29046
|
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
29047
|
+
case "observed-symbol-mismatch": {
|
|
29048
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
29049
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
29050
|
+
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29051
|
+
}
|
|
28793
29052
|
}
|
|
28794
29053
|
}
|
|
28795
29054
|
async function runDivergences(client, input) {
|
|
@@ -28943,6 +29202,11 @@ function formatDivergenceLine2(d) {
|
|
|
28943
29202
|
return `\u26A0 divergence [host-mismatch] ${d.source} \u2192 ${d.target} declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
28944
29203
|
case "compat-violation":
|
|
28945
29204
|
return `\u26A0 divergence [compat-violation] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
29205
|
+
case "observed-symbol-mismatch": {
|
|
29206
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
29207
|
+
const member = d.symbol ? ` ${d.symbol}` : " a member";
|
|
29208
|
+
return `\u26A0 divergence [observed-symbol-mismatch] ${d.source}${at} reads${member} the runtime object does not have (${d.mismatchKind})`;
|
|
29209
|
+
}
|
|
28946
29210
|
}
|
|
28947
29211
|
}
|
|
28948
29212
|
function formatStaleLine(edgeId) {
|
|
@@ -29919,7 +30183,8 @@ async function runInit(opts) {
|
|
|
29919
30183
|
console.log(`snapshot: ${opts.outPath}`);
|
|
29920
30184
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
29921
30185
|
console.log("");
|
|
29922
|
-
const
|
|
30186
|
+
const summaryIncidents = await readErrorEvents(errorsPath);
|
|
30187
|
+
const divergenceResult = computeDivergences(graph, { incidents: summaryIncidents });
|
|
29923
30188
|
console.log(
|
|
29924
30189
|
renderValueForwardSummary({
|
|
29925
30190
|
graph,
|