@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
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
startStalenessLoop,
|
|
21
21
|
touchLastSeen,
|
|
22
22
|
writeAtomically
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-EDE4XP2M.js";
|
|
24
24
|
import {
|
|
25
25
|
assertBindAuthority,
|
|
26
26
|
buildOtelReceiver,
|
|
@@ -891,4 +891,4 @@ export {
|
|
|
891
891
|
resolveHost,
|
|
892
892
|
startDaemon
|
|
893
893
|
};
|
|
894
|
-
//# sourceMappingURL=chunk-
|
|
894
|
+
//# sourceMappingURL=chunk-2D4Y5QHE.js.map
|
|
@@ -5125,15 +5125,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
5125
5125
|
clearInterval(interval);
|
|
5126
5126
|
};
|
|
5127
5127
|
}
|
|
5128
|
-
|
|
5128
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
5129
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
5130
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
5131
|
+
const handle = await fs7.open(errorsPath, "r");
|
|
5129
5132
|
try {
|
|
5130
|
-
const
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
+
const { size } = await handle.stat();
|
|
5134
|
+
if (size <= maxBytes) {
|
|
5135
|
+
return (await handle.readFile()).toString("utf8");
|
|
5136
|
+
}
|
|
5137
|
+
const buf = Buffer.alloc(maxBytes);
|
|
5138
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
5139
|
+
const raw = buf.toString("utf8");
|
|
5140
|
+
const firstNewline = raw.indexOf("\n");
|
|
5141
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
5142
|
+
} finally {
|
|
5143
|
+
await handle.close();
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
5147
|
+
let raw;
|
|
5148
|
+
try {
|
|
5149
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
5133
5150
|
} catch (err) {
|
|
5134
5151
|
if (err.code === "ENOENT") return [];
|
|
5135
5152
|
throw err;
|
|
5136
5153
|
}
|
|
5154
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
5155
|
+
const deduped = dedupeIncidents(events);
|
|
5156
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
5157
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
5137
5158
|
}
|
|
5138
5159
|
function isSynthesizedHttpIncident(ev) {
|
|
5139
5160
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -5906,6 +5927,47 @@ function classifyNode(ctx) {
|
|
|
5906
5927
|
function isVictimSeed(ctx) {
|
|
5907
5928
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
5908
5929
|
}
|
|
5930
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
5931
|
+
"name resolution",
|
|
5932
|
+
"resolve host",
|
|
5933
|
+
"getaddrinfo",
|
|
5934
|
+
"enotfound",
|
|
5935
|
+
"connection refused",
|
|
5936
|
+
"econnrefused",
|
|
5937
|
+
"connection reset",
|
|
5938
|
+
"econnreset",
|
|
5939
|
+
"able to connect",
|
|
5940
|
+
"failed to connect",
|
|
5941
|
+
"cannot connect",
|
|
5942
|
+
"could not connect",
|
|
5943
|
+
"unable to connect",
|
|
5944
|
+
"connection timed out",
|
|
5945
|
+
"etimedout",
|
|
5946
|
+
"no route to host",
|
|
5947
|
+
"host unreachable",
|
|
5948
|
+
"network is unreachable",
|
|
5949
|
+
"connection closed"
|
|
5950
|
+
];
|
|
5951
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
5952
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
5953
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
5954
|
+
}
|
|
5955
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
5956
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
5957
|
+
if (!graph.hasNode(n)) continue;
|
|
5958
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
5959
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
5960
|
+
if (e.type === EdgeType6.CONTAINS) continue;
|
|
5961
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
if (seedSource === "incident" && incidents) {
|
|
5965
|
+
for (const ev of incidents) {
|
|
5966
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
return false;
|
|
5970
|
+
}
|
|
5909
5971
|
function grainOf(graph, nodeId) {
|
|
5910
5972
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
5911
5973
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -6069,7 +6131,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
6069
6131
|
const candidates = [];
|
|
6070
6132
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
6071
6133
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
6072
|
-
|
|
6134
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
6135
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
6073
6136
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
6074
6137
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
6075
6138
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -9112,12 +9175,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
|
|
|
9112
9175
|
const value = await interpolateEnvRefs(raw, serviceDir);
|
|
9113
9176
|
if (!looksLike(value)) continue;
|
|
9114
9177
|
const parsed = parse11(value);
|
|
9115
|
-
if (parsed) out.push(parsed);
|
|
9178
|
+
if (parsed) out.push({ ...parsed, hostSource: "config" });
|
|
9116
9179
|
}
|
|
9117
9180
|
for (const lit of literals) {
|
|
9118
9181
|
if (!looksLike(lit)) continue;
|
|
9119
9182
|
const parsed = parse11(lit);
|
|
9120
|
-
if (parsed) out.push(parsed);
|
|
9183
|
+
if (parsed) out.push({ ...parsed, hostSource: "literal" });
|
|
9121
9184
|
}
|
|
9122
9185
|
return out;
|
|
9123
9186
|
}
|
|
@@ -9385,7 +9448,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
9385
9448
|
type: EdgeType10.CONNECTS_TO,
|
|
9386
9449
|
provenance: Provenance10.EXTRACTED,
|
|
9387
9450
|
confidence: confidenceForExtracted7("structural"),
|
|
9388
|
-
|
|
9451
|
+
// Carry how the host was recovered (ADR-213) so the divergence ranker
|
|
9452
|
+
// can tell a real declared store from a hardcoded fault-injection /
|
|
9453
|
+
// flag-gated probe. Only set when the parser distinguished the two.
|
|
9454
|
+
evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
|
|
9389
9455
|
};
|
|
9390
9456
|
if (!graph.hasEdge(edge.id)) {
|
|
9391
9457
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -14936,6 +15002,7 @@ import {
|
|
|
14936
15002
|
NodeType as NodeType30,
|
|
14937
15003
|
parseEdgeId,
|
|
14938
15004
|
parseFileId,
|
|
15005
|
+
parseSymbolId as parseSymbolId2,
|
|
14939
15006
|
Provenance as Provenance24,
|
|
14940
15007
|
serviceId as serviceId13
|
|
14941
15008
|
} from "@neat.is/types";
|
|
@@ -15192,9 +15259,170 @@ function detectColumnDrift(node) {
|
|
|
15192
15259
|
}
|
|
15193
15260
|
return out;
|
|
15194
15261
|
}
|
|
15262
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
15263
|
+
{
|
|
15264
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
15265
|
+
kind: "missing-attribute",
|
|
15266
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
15267
|
+
},
|
|
15268
|
+
{
|
|
15269
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
15270
|
+
kind: "missing-field",
|
|
15271
|
+
patterns: [
|
|
15272
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
15273
|
+
]
|
|
15274
|
+
},
|
|
15275
|
+
{
|
|
15276
|
+
// "has no property X", "no property named X".
|
|
15277
|
+
kind: "missing-property",
|
|
15278
|
+
patterns: [
|
|
15279
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
15280
|
+
]
|
|
15281
|
+
},
|
|
15282
|
+
{
|
|
15283
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
15284
|
+
kind: "missing-column",
|
|
15285
|
+
patterns: [
|
|
15286
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
15287
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
15288
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
15289
|
+
]
|
|
15290
|
+
},
|
|
15291
|
+
{
|
|
15292
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
15293
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
15294
|
+
// mismatch) does not get miscategorised here.
|
|
15295
|
+
kind: "undefined-method",
|
|
15296
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
15297
|
+
}
|
|
15298
|
+
];
|
|
15299
|
+
function classifySymbolMismatch(message) {
|
|
15300
|
+
for (const entry of SYMBOL_MISMATCH_PATTERNS) {
|
|
15301
|
+
for (const re of entry.patterns) {
|
|
15302
|
+
const m = re.exec(message);
|
|
15303
|
+
if (m) {
|
|
15304
|
+
const captured = m[1];
|
|
15305
|
+
return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
|
|
15306
|
+
}
|
|
15307
|
+
}
|
|
15308
|
+
}
|
|
15309
|
+
return null;
|
|
15310
|
+
}
|
|
15311
|
+
function symbolLocus(graph, ev) {
|
|
15312
|
+
const attrs = ev.attributes ?? {};
|
|
15313
|
+
const filepath = codeFilepathOf(attrs);
|
|
15314
|
+
const lineno = codeLinenoOf(attrs);
|
|
15315
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
15316
|
+
const affected = ev.affectedNode;
|
|
15317
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
15318
|
+
const affectedIsCode = affectedInGraph && (parseSymbolId2(affected) !== null || parseFileId(affected) !== null);
|
|
15319
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
15320
|
+
if (!location) return null;
|
|
15321
|
+
if (affectedInGraph) return { node: affected, location };
|
|
15322
|
+
const svc = serviceId13(ev.service);
|
|
15323
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
15324
|
+
return null;
|
|
15325
|
+
}
|
|
15326
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
15327
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
15328
|
+
const groups = /* @__PURE__ */ new Map();
|
|
15329
|
+
for (const ev of incidents) {
|
|
15330
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
15331
|
+
if (!classified) continue;
|
|
15332
|
+
const locus = symbolLocus(graph, ev);
|
|
15333
|
+
if (!locus) continue;
|
|
15334
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
15335
|
+
const existing = groups.get(key);
|
|
15336
|
+
if (!existing) {
|
|
15337
|
+
groups.set(key, {
|
|
15338
|
+
node: locus.node,
|
|
15339
|
+
kind: classified.kind,
|
|
15340
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
15341
|
+
...locus.location ? { location: locus.location } : {},
|
|
15342
|
+
latest: ev,
|
|
15343
|
+
count: 1
|
|
15344
|
+
});
|
|
15345
|
+
} else {
|
|
15346
|
+
existing.count += 1;
|
|
15347
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
15348
|
+
existing.latest = ev;
|
|
15349
|
+
if (locus.location) existing.location = locus.location;
|
|
15350
|
+
}
|
|
15351
|
+
}
|
|
15352
|
+
}
|
|
15353
|
+
const out = [];
|
|
15354
|
+
for (const g of groups.values()) {
|
|
15355
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
15356
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
15357
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
15358
|
+
out.push({
|
|
15359
|
+
type: "observed-symbol-mismatch",
|
|
15360
|
+
source: g.node,
|
|
15361
|
+
target: g.node,
|
|
15362
|
+
mismatchKind: g.kind,
|
|
15363
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
15364
|
+
...g.location ? { location: g.location } : {},
|
|
15365
|
+
provenance: Provenance24.INFERRED,
|
|
15366
|
+
incidentId: g.latest.id,
|
|
15367
|
+
errorMessage: g.latest.errorMessage,
|
|
15368
|
+
incidentCount: g.count,
|
|
15369
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
15370
|
+
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.`,
|
|
15371
|
+
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."
|
|
15372
|
+
});
|
|
15373
|
+
}
|
|
15374
|
+
return out;
|
|
15375
|
+
}
|
|
15195
15376
|
function involvesNode(d, nodeId) {
|
|
15196
15377
|
return d.source === nodeId || d.target === nodeId;
|
|
15197
15378
|
}
|
|
15379
|
+
function datastoreEverObserved(graph, nodeId) {
|
|
15380
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
15381
|
+
const n = graph.getNodeAttributes(nodeId);
|
|
15382
|
+
if (n.type === NodeType30.DatabaseNode) {
|
|
15383
|
+
const via = n.discoveredVia;
|
|
15384
|
+
if (via === "otel" || via === "merged") return true;
|
|
15385
|
+
}
|
|
15386
|
+
for (const edgeId of graph.inboundEdges(nodeId)) {
|
|
15387
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
15388
|
+
if (e.provenance === Provenance24.OBSERVED || e.provenance === Provenance24.STALE) return true;
|
|
15389
|
+
}
|
|
15390
|
+
return false;
|
|
15391
|
+
}
|
|
15392
|
+
function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
|
|
15393
|
+
for (const bucket of buckets2.values()) {
|
|
15394
|
+
if (bucket.type !== EdgeType25.CONNECTS_TO) continue;
|
|
15395
|
+
if (bucket.source !== serviceId16) continue;
|
|
15396
|
+
if (bucket.target === excludeTarget) continue;
|
|
15397
|
+
if (!graph.hasNode(bucket.target)) continue;
|
|
15398
|
+
const target = graph.getNodeAttributes(bucket.target);
|
|
15399
|
+
if (target.type !== NodeType30.DatabaseNode) continue;
|
|
15400
|
+
if (target.engine !== engine) continue;
|
|
15401
|
+
if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
|
|
15402
|
+
}
|
|
15403
|
+
return false;
|
|
15404
|
+
}
|
|
15405
|
+
var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
|
|
15406
|
+
function dampenDeadCodeProbes(graph, buckets2, all) {
|
|
15407
|
+
return all.map((d) => {
|
|
15408
|
+
if (d.type !== "missing-observed") return d;
|
|
15409
|
+
if (!d.extracted || d.edgeType !== EdgeType25.CONNECTS_TO) return d;
|
|
15410
|
+
if (!graph.hasNode(d.target)) return d;
|
|
15411
|
+
const target = graph.getNodeAttributes(d.target);
|
|
15412
|
+
if (target.type !== NodeType30.DatabaseNode) return d;
|
|
15413
|
+
if (d.extracted.evidence?.hostSource !== "literal") return d;
|
|
15414
|
+
if (datastoreEverObserved(graph, d.target)) return d;
|
|
15415
|
+
const engine = target.engine;
|
|
15416
|
+
if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
|
|
15417
|
+
const host = target.host ?? target.name;
|
|
15418
|
+
return {
|
|
15419
|
+
...d,
|
|
15420
|
+
confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
|
|
15421
|
+
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.`,
|
|
15422
|
+
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."
|
|
15423
|
+
};
|
|
15424
|
+
});
|
|
15425
|
+
}
|
|
15198
15426
|
function suppressHostMismatchHalves(all) {
|
|
15199
15427
|
const observedHalf = /* @__PURE__ */ new Set();
|
|
15200
15428
|
const declaredHalf = /* @__PURE__ */ new Set();
|
|
@@ -15230,8 +15458,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
15230
15458
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
15231
15459
|
}
|
|
15232
15460
|
});
|
|
15461
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
15462
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
15463
|
+
}
|
|
15233
15464
|
const reconciled = suppressHostMismatchHalves(all);
|
|
15234
|
-
|
|
15465
|
+
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
15466
|
+
let filtered = dampened;
|
|
15235
15467
|
if (opts.type) {
|
|
15236
15468
|
const allowed = opts.type;
|
|
15237
15469
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -15249,7 +15481,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
15249
15481
|
"missing-observed": 1,
|
|
15250
15482
|
"version-mismatch": 2,
|
|
15251
15483
|
"host-mismatch": 3,
|
|
15252
|
-
"compat-violation": 4
|
|
15484
|
+
"compat-violation": 4,
|
|
15485
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
15486
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
15487
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
15488
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
15489
|
+
"observed-symbol-mismatch": 5
|
|
15253
15490
|
};
|
|
15254
15491
|
filtered.sort((a, b) => {
|
|
15255
15492
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -15260,7 +15497,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
15260
15497
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
15261
15498
|
const ac = "column" in a && a.column ? a.column : "";
|
|
15262
15499
|
const bc = "column" in b && b.column ? b.column : "";
|
|
15263
|
-
return ac.localeCompare(bc);
|
|
15500
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
15501
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
15502
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
15503
|
+
return asym.localeCompare(bsym);
|
|
15264
15504
|
});
|
|
15265
15505
|
return DivergenceResultSchema.parse({
|
|
15266
15506
|
divergences: filtered,
|
|
@@ -16703,10 +16943,15 @@ function divergenceLine(d) {
|
|
|
16703
16943
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16704
16944
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16705
16945
|
}
|
|
16946
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
16947
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
16948
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
16949
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
16950
|
+
}
|
|
16706
16951
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16707
16952
|
}
|
|
16708
|
-
function buildDivergenceSection(graph, node) {
|
|
16709
|
-
const result = computeDivergences(graph, { node });
|
|
16953
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
16954
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
16710
16955
|
if (result.totalAffected === 0) return null;
|
|
16711
16956
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16712
16957
|
text: divergenceLine(d),
|
|
@@ -16719,8 +16964,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
16719
16964
|
facts
|
|
16720
16965
|
};
|
|
16721
16966
|
}
|
|
16722
|
-
function buildGlobalDivergenceSection(graph) {
|
|
16723
|
-
const result = computeDivergences(graph);
|
|
16967
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
16968
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16724
16969
|
if (result.totalAffected === 0) {
|
|
16725
16970
|
return {
|
|
16726
16971
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -16818,7 +17063,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16818
17063
|
}))
|
|
16819
17064
|
});
|
|
16820
17065
|
}
|
|
16821
|
-
const div = computeDivergences(graph);
|
|
17066
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16822
17067
|
sections.push({
|
|
16823
17068
|
heading: "Divergences",
|
|
16824
17069
|
facts: [
|
|
@@ -16832,7 +17077,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
16832
17077
|
function buildGlobalSections(intent, graph, incidents) {
|
|
16833
17078
|
switch (intent) {
|
|
16834
17079
|
case "divergence":
|
|
16835
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17080
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
16836
17081
|
case "incidents":
|
|
16837
17082
|
return [buildGlobalIncidentsSection(incidents)];
|
|
16838
17083
|
case "overview":
|
|
@@ -16863,7 +17108,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
16863
17108
|
case "incidents":
|
|
16864
17109
|
return buildIncidentsSection(node, incidents);
|
|
16865
17110
|
case "divergence":
|
|
16866
|
-
return buildDivergenceSection(graph, node);
|
|
17111
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
16867
17112
|
}
|
|
16868
17113
|
}
|
|
16869
17114
|
function summarizeGlobal(intent, sections) {
|
|
@@ -20485,6 +20730,8 @@ async function deprovisionConnector(entry, env = process.env, fetchImpl) {
|
|
|
20485
20730
|
}
|
|
20486
20731
|
|
|
20487
20732
|
// src/api.ts
|
|
20733
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
20734
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
20488
20735
|
function serializeGraph(graph) {
|
|
20489
20736
|
const nodes = [];
|
|
20490
20737
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -20664,10 +20911,13 @@ function registerRoutes(scope, ctx) {
|
|
|
20664
20911
|
}
|
|
20665
20912
|
minConfidence = n;
|
|
20666
20913
|
}
|
|
20914
|
+
const epath = errorsPathFor(proj);
|
|
20915
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
20667
20916
|
return computeDivergences(proj.graph, {
|
|
20668
20917
|
...typeFilter ? { type: typeFilter } : {},
|
|
20669
20918
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
20670
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
20919
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
20920
|
+
incidents
|
|
20671
20921
|
});
|
|
20672
20922
|
});
|
|
20673
20923
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -20677,10 +20927,11 @@ function registerRoutes(scope, ctx) {
|
|
|
20677
20927
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
20678
20928
|
const events = await readErrorEvents(epath);
|
|
20679
20929
|
const total = events.length;
|
|
20680
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
20681
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
20930
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
20931
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
20682
20932
|
const sliced = events.slice(0, safeLimit);
|
|
20683
|
-
|
|
20933
|
+
const omitted = total - sliced.length;
|
|
20934
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
20684
20935
|
});
|
|
20685
20936
|
scope.get("/stale-events", async (req, reply) => {
|
|
20686
20937
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -20789,16 +21040,20 @@ function registerRoutes(scope, ctx) {
|
|
|
20789
21040
|
const filtered = events.filter(
|
|
20790
21041
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
20791
21042
|
);
|
|
20792
|
-
|
|
21043
|
+
const total = filtered.length;
|
|
21044
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21045
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21046
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
21047
|
+
const omitted = total - recent.length;
|
|
21048
|
+
return {
|
|
21049
|
+
count: recent.length,
|
|
21050
|
+
total,
|
|
21051
|
+
events: recent,
|
|
21052
|
+
...omitted > 0 ? { omitted } : {}
|
|
21053
|
+
};
|
|
20793
21054
|
};
|
|
20794
|
-
scope.get(
|
|
20795
|
-
|
|
20796
|
-
incidentHistoryHandler
|
|
20797
|
-
);
|
|
20798
|
-
scope.get(
|
|
20799
|
-
"/graph/incident-history/:nodeId",
|
|
20800
|
-
incidentHistoryHandler
|
|
20801
|
-
);
|
|
21055
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21056
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
20802
21057
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
20803
21058
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
20804
21059
|
if (!proj) return;
|
|
@@ -21440,4 +21695,4 @@ export {
|
|
|
21440
21695
|
deprovisionConnector,
|
|
21441
21696
|
buildApi
|
|
21442
21697
|
};
|
|
21443
|
-
//# sourceMappingURL=chunk-
|
|
21698
|
+
//# sourceMappingURL=chunk-EDE4XP2M.js.map
|