@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/index.cjs
CHANGED
|
@@ -1925,6 +1925,47 @@ function classifyNode(ctx) {
|
|
|
1925
1925
|
function isVictimSeed(ctx) {
|
|
1926
1926
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1927
1927
|
}
|
|
1928
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
1929
|
+
"name resolution",
|
|
1930
|
+
"resolve host",
|
|
1931
|
+
"getaddrinfo",
|
|
1932
|
+
"enotfound",
|
|
1933
|
+
"connection refused",
|
|
1934
|
+
"econnrefused",
|
|
1935
|
+
"connection reset",
|
|
1936
|
+
"econnreset",
|
|
1937
|
+
"able to connect",
|
|
1938
|
+
"failed to connect",
|
|
1939
|
+
"cannot connect",
|
|
1940
|
+
"could not connect",
|
|
1941
|
+
"unable to connect",
|
|
1942
|
+
"connection timed out",
|
|
1943
|
+
"etimedout",
|
|
1944
|
+
"no route to host",
|
|
1945
|
+
"host unreachable",
|
|
1946
|
+
"network is unreachable",
|
|
1947
|
+
"connection closed"
|
|
1948
|
+
];
|
|
1949
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
1950
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
1951
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
1952
|
+
}
|
|
1953
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
1954
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
1955
|
+
if (!graph.hasNode(n)) continue;
|
|
1956
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1957
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1958
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1959
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
if (seedSource === "incident" && incidents) {
|
|
1963
|
+
for (const ev of incidents) {
|
|
1964
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1928
1969
|
function grainOf(graph, nodeId) {
|
|
1929
1970
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1930
1971
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -2088,7 +2129,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
2088
2129
|
const candidates = [];
|
|
2089
2130
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
2090
2131
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
2091
|
-
|
|
2132
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
2133
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
2092
2134
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
2093
2135
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
2094
2136
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -6848,15 +6890,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6848
6890
|
clearInterval(interval);
|
|
6849
6891
|
};
|
|
6850
6892
|
}
|
|
6851
|
-
|
|
6893
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
6894
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
6895
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
6896
|
+
const handle = await import_node_fs7.promises.open(errorsPath, "r");
|
|
6852
6897
|
try {
|
|
6853
|
-
const
|
|
6854
|
-
|
|
6855
|
-
|
|
6898
|
+
const { size } = await handle.stat();
|
|
6899
|
+
if (size <= maxBytes) {
|
|
6900
|
+
return (await handle.readFile()).toString("utf8");
|
|
6901
|
+
}
|
|
6902
|
+
const buf = Buffer.alloc(maxBytes);
|
|
6903
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
6904
|
+
const raw = buf.toString("utf8");
|
|
6905
|
+
const firstNewline = raw.indexOf("\n");
|
|
6906
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
6907
|
+
} finally {
|
|
6908
|
+
await handle.close();
|
|
6909
|
+
}
|
|
6910
|
+
}
|
|
6911
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
6912
|
+
let raw;
|
|
6913
|
+
try {
|
|
6914
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6856
6915
|
} catch (err) {
|
|
6857
6916
|
if (err.code === "ENOENT") return [];
|
|
6858
6917
|
throw err;
|
|
6859
6918
|
}
|
|
6919
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6920
|
+
const deduped = dedupeIncidents(events);
|
|
6921
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
6922
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6860
6923
|
}
|
|
6861
6924
|
function isSynthesizedHttpIncident(ev) {
|
|
6862
6925
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -9892,12 +9955,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
|
|
|
9892
9955
|
const value = await interpolateEnvRefs(raw, serviceDir);
|
|
9893
9956
|
if (!looksLike(value)) continue;
|
|
9894
9957
|
const parsed = parse11(value);
|
|
9895
|
-
if (parsed) out.push(parsed);
|
|
9958
|
+
if (parsed) out.push({ ...parsed, hostSource: "config" });
|
|
9896
9959
|
}
|
|
9897
9960
|
for (const lit of literals) {
|
|
9898
9961
|
if (!looksLike(lit)) continue;
|
|
9899
9962
|
const parsed = parse11(lit);
|
|
9900
|
-
if (parsed) out.push(parsed);
|
|
9963
|
+
if (parsed) out.push({ ...parsed, hostSource: "literal" });
|
|
9901
9964
|
}
|
|
9902
9965
|
return out;
|
|
9903
9966
|
}
|
|
@@ -10166,7 +10229,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
10166
10229
|
type: import_types23.EdgeType.CONNECTS_TO,
|
|
10167
10230
|
provenance: import_types23.Provenance.EXTRACTED,
|
|
10168
10231
|
confidence: (0, import_types23.confidenceForExtracted)("structural"),
|
|
10169
|
-
|
|
10232
|
+
// Carry how the host was recovered (ADR-213) so the divergence ranker
|
|
10233
|
+
// can tell a real declared store from a hardcoded fault-injection /
|
|
10234
|
+
// flag-gated probe. Only set when the parser distinguished the two.
|
|
10235
|
+
evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
|
|
10170
10236
|
};
|
|
10171
10237
|
if (!graph.hasEdge(edge.id)) {
|
|
10172
10238
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -16440,9 +16506,170 @@ function detectColumnDrift(node) {
|
|
|
16440
16506
|
}
|
|
16441
16507
|
return out;
|
|
16442
16508
|
}
|
|
16509
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16510
|
+
{
|
|
16511
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
16512
|
+
kind: "missing-attribute",
|
|
16513
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
16514
|
+
},
|
|
16515
|
+
{
|
|
16516
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
16517
|
+
kind: "missing-field",
|
|
16518
|
+
patterns: [
|
|
16519
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
16520
|
+
]
|
|
16521
|
+
},
|
|
16522
|
+
{
|
|
16523
|
+
// "has no property X", "no property named X".
|
|
16524
|
+
kind: "missing-property",
|
|
16525
|
+
patterns: [
|
|
16526
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
16527
|
+
]
|
|
16528
|
+
},
|
|
16529
|
+
{
|
|
16530
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
16531
|
+
kind: "missing-column",
|
|
16532
|
+
patterns: [
|
|
16533
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16534
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16535
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
16536
|
+
]
|
|
16537
|
+
},
|
|
16538
|
+
{
|
|
16539
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
16540
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
16541
|
+
// mismatch) does not get miscategorised here.
|
|
16542
|
+
kind: "undefined-method",
|
|
16543
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
16544
|
+
}
|
|
16545
|
+
];
|
|
16546
|
+
function classifySymbolMismatch(message) {
|
|
16547
|
+
for (const entry of SYMBOL_MISMATCH_PATTERNS) {
|
|
16548
|
+
for (const re of entry.patterns) {
|
|
16549
|
+
const m = re.exec(message);
|
|
16550
|
+
if (m) {
|
|
16551
|
+
const captured = m[1];
|
|
16552
|
+
return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
|
|
16553
|
+
}
|
|
16554
|
+
}
|
|
16555
|
+
}
|
|
16556
|
+
return null;
|
|
16557
|
+
}
|
|
16558
|
+
function symbolLocus(graph, ev) {
|
|
16559
|
+
const attrs = ev.attributes ?? {};
|
|
16560
|
+
const filepath = codeFilepathOf(attrs);
|
|
16561
|
+
const lineno = codeLinenoOf(attrs);
|
|
16562
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
16563
|
+
const affected = ev.affectedNode;
|
|
16564
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
16565
|
+
const affectedIsCode = affectedInGraph && ((0, import_types58.parseSymbolId)(affected) !== null || (0, import_types58.parseFileId)(affected) !== null);
|
|
16566
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
16567
|
+
if (!location) return null;
|
|
16568
|
+
if (affectedInGraph) return { node: affected, location };
|
|
16569
|
+
const svc = (0, import_types58.serviceId)(ev.service);
|
|
16570
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
16571
|
+
return null;
|
|
16572
|
+
}
|
|
16573
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
16574
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
16575
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16576
|
+
for (const ev of incidents) {
|
|
16577
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
16578
|
+
if (!classified) continue;
|
|
16579
|
+
const locus = symbolLocus(graph, ev);
|
|
16580
|
+
if (!locus) continue;
|
|
16581
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
16582
|
+
const existing = groups.get(key);
|
|
16583
|
+
if (!existing) {
|
|
16584
|
+
groups.set(key, {
|
|
16585
|
+
node: locus.node,
|
|
16586
|
+
kind: classified.kind,
|
|
16587
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
16588
|
+
...locus.location ? { location: locus.location } : {},
|
|
16589
|
+
latest: ev,
|
|
16590
|
+
count: 1
|
|
16591
|
+
});
|
|
16592
|
+
} else {
|
|
16593
|
+
existing.count += 1;
|
|
16594
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
16595
|
+
existing.latest = ev;
|
|
16596
|
+
if (locus.location) existing.location = locus.location;
|
|
16597
|
+
}
|
|
16598
|
+
}
|
|
16599
|
+
}
|
|
16600
|
+
const out = [];
|
|
16601
|
+
for (const g of groups.values()) {
|
|
16602
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
16603
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
16604
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
16605
|
+
out.push({
|
|
16606
|
+
type: "observed-symbol-mismatch",
|
|
16607
|
+
source: g.node,
|
|
16608
|
+
target: g.node,
|
|
16609
|
+
mismatchKind: g.kind,
|
|
16610
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
16611
|
+
...g.location ? { location: g.location } : {},
|
|
16612
|
+
provenance: import_types58.Provenance.INFERRED,
|
|
16613
|
+
incidentId: g.latest.id,
|
|
16614
|
+
errorMessage: g.latest.errorMessage,
|
|
16615
|
+
incidentCount: g.count,
|
|
16616
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
16617
|
+
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.`,
|
|
16618
|
+
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."
|
|
16619
|
+
});
|
|
16620
|
+
}
|
|
16621
|
+
return out;
|
|
16622
|
+
}
|
|
16443
16623
|
function involvesNode(d, nodeId) {
|
|
16444
16624
|
return d.source === nodeId || d.target === nodeId;
|
|
16445
16625
|
}
|
|
16626
|
+
function datastoreEverObserved(graph, nodeId) {
|
|
16627
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
16628
|
+
const n = graph.getNodeAttributes(nodeId);
|
|
16629
|
+
if (n.type === import_types58.NodeType.DatabaseNode) {
|
|
16630
|
+
const via = n.discoveredVia;
|
|
16631
|
+
if (via === "otel" || via === "merged") return true;
|
|
16632
|
+
}
|
|
16633
|
+
for (const edgeId of graph.inboundEdges(nodeId)) {
|
|
16634
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
16635
|
+
if (e.provenance === import_types58.Provenance.OBSERVED || e.provenance === import_types58.Provenance.STALE) return true;
|
|
16636
|
+
}
|
|
16637
|
+
return false;
|
|
16638
|
+
}
|
|
16639
|
+
function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
|
|
16640
|
+
for (const bucket of buckets2.values()) {
|
|
16641
|
+
if (bucket.type !== import_types58.EdgeType.CONNECTS_TO) continue;
|
|
16642
|
+
if (bucket.source !== serviceId16) continue;
|
|
16643
|
+
if (bucket.target === excludeTarget) continue;
|
|
16644
|
+
if (!graph.hasNode(bucket.target)) continue;
|
|
16645
|
+
const target = graph.getNodeAttributes(bucket.target);
|
|
16646
|
+
if (target.type !== import_types58.NodeType.DatabaseNode) continue;
|
|
16647
|
+
if (target.engine !== engine) continue;
|
|
16648
|
+
if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
|
|
16649
|
+
}
|
|
16650
|
+
return false;
|
|
16651
|
+
}
|
|
16652
|
+
var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
|
|
16653
|
+
function dampenDeadCodeProbes(graph, buckets2, all) {
|
|
16654
|
+
return all.map((d) => {
|
|
16655
|
+
if (d.type !== "missing-observed") return d;
|
|
16656
|
+
if (!d.extracted || d.edgeType !== import_types58.EdgeType.CONNECTS_TO) return d;
|
|
16657
|
+
if (!graph.hasNode(d.target)) return d;
|
|
16658
|
+
const target = graph.getNodeAttributes(d.target);
|
|
16659
|
+
if (target.type !== import_types58.NodeType.DatabaseNode) return d;
|
|
16660
|
+
if (d.extracted.evidence?.hostSource !== "literal") return d;
|
|
16661
|
+
if (datastoreEverObserved(graph, d.target)) return d;
|
|
16662
|
+
const engine = target.engine;
|
|
16663
|
+
if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
|
|
16664
|
+
const host = target.host ?? target.name;
|
|
16665
|
+
return {
|
|
16666
|
+
...d,
|
|
16667
|
+
confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
|
|
16668
|
+
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.`,
|
|
16669
|
+
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."
|
|
16670
|
+
};
|
|
16671
|
+
});
|
|
16672
|
+
}
|
|
16446
16673
|
function suppressHostMismatchHalves(all) {
|
|
16447
16674
|
const observedHalf = /* @__PURE__ */ new Set();
|
|
16448
16675
|
const declaredHalf = /* @__PURE__ */ new Set();
|
|
@@ -16478,8 +16705,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16478
16705
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
16479
16706
|
}
|
|
16480
16707
|
});
|
|
16708
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
16709
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
16710
|
+
}
|
|
16481
16711
|
const reconciled = suppressHostMismatchHalves(all);
|
|
16482
|
-
|
|
16712
|
+
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
16713
|
+
let filtered = dampened;
|
|
16483
16714
|
if (opts.type) {
|
|
16484
16715
|
const allowed = opts.type;
|
|
16485
16716
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -16497,7 +16728,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16497
16728
|
"missing-observed": 1,
|
|
16498
16729
|
"version-mismatch": 2,
|
|
16499
16730
|
"host-mismatch": 3,
|
|
16500
|
-
"compat-violation": 4
|
|
16731
|
+
"compat-violation": 4,
|
|
16732
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
16733
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
16734
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
16735
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
16736
|
+
"observed-symbol-mismatch": 5
|
|
16501
16737
|
};
|
|
16502
16738
|
filtered.sort((a, b) => {
|
|
16503
16739
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -16508,7 +16744,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16508
16744
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
16509
16745
|
const ac = "column" in a && a.column ? a.column : "";
|
|
16510
16746
|
const bc = "column" in b && b.column ? b.column : "";
|
|
16511
|
-
return ac.localeCompare(bc);
|
|
16747
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
16748
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
16749
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
16750
|
+
return asym.localeCompare(bsym);
|
|
16512
16751
|
});
|
|
16513
16752
|
return import_types58.DivergenceResultSchema.parse({
|
|
16514
16753
|
divergences: filtered,
|
|
@@ -16935,10 +17174,15 @@ function divergenceLine(d) {
|
|
|
16935
17174
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16936
17175
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16937
17176
|
}
|
|
17177
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17178
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17179
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17180
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17181
|
+
}
|
|
16938
17182
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16939
17183
|
}
|
|
16940
|
-
function buildDivergenceSection(graph, node) {
|
|
16941
|
-
const result = computeDivergences(graph, { node });
|
|
17184
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17185
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
16942
17186
|
if (result.totalAffected === 0) return null;
|
|
16943
17187
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16944
17188
|
text: divergenceLine(d),
|
|
@@ -16951,8 +17195,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
16951
17195
|
facts
|
|
16952
17196
|
};
|
|
16953
17197
|
}
|
|
16954
|
-
function buildGlobalDivergenceSection(graph) {
|
|
16955
|
-
const result = computeDivergences(graph);
|
|
17198
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17199
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16956
17200
|
if (result.totalAffected === 0) {
|
|
16957
17201
|
return {
|
|
16958
17202
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -17050,7 +17294,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17050
17294
|
}))
|
|
17051
17295
|
});
|
|
17052
17296
|
}
|
|
17053
|
-
const div = computeDivergences(graph);
|
|
17297
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17054
17298
|
sections.push({
|
|
17055
17299
|
heading: "Divergences",
|
|
17056
17300
|
facts: [
|
|
@@ -17064,7 +17308,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17064
17308
|
function buildGlobalSections(intent, graph, incidents) {
|
|
17065
17309
|
switch (intent) {
|
|
17066
17310
|
case "divergence":
|
|
17067
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17311
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
17068
17312
|
case "incidents":
|
|
17069
17313
|
return [buildGlobalIncidentsSection(incidents)];
|
|
17070
17314
|
case "overview":
|
|
@@ -17095,7 +17339,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
17095
17339
|
case "incidents":
|
|
17096
17340
|
return buildIncidentsSection(node, incidents);
|
|
17097
17341
|
case "divergence":
|
|
17098
|
-
return buildDivergenceSection(graph, node);
|
|
17342
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
17099
17343
|
}
|
|
17100
17344
|
}
|
|
17101
17345
|
function summarizeGlobal(intent, sections) {
|
|
@@ -21080,6 +21324,8 @@ async function startConnectorPolling(input) {
|
|
|
21080
21324
|
}
|
|
21081
21325
|
|
|
21082
21326
|
// src/api.ts
|
|
21327
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
21328
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
21083
21329
|
function serializeGraph(graph) {
|
|
21084
21330
|
const nodes = [];
|
|
21085
21331
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -21259,10 +21505,13 @@ function registerRoutes(scope, ctx) {
|
|
|
21259
21505
|
}
|
|
21260
21506
|
minConfidence = n;
|
|
21261
21507
|
}
|
|
21508
|
+
const epath = errorsPathFor(proj);
|
|
21509
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
21262
21510
|
return computeDivergences(proj.graph, {
|
|
21263
21511
|
...typeFilter ? { type: typeFilter } : {},
|
|
21264
21512
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
21265
|
-
...req.query.node ? { node: req.query.node } : {}
|
|
21513
|
+
...req.query.node ? { node: req.query.node } : {},
|
|
21514
|
+
incidents
|
|
21266
21515
|
});
|
|
21267
21516
|
});
|
|
21268
21517
|
scope.get("/incidents", async (req, reply) => {
|
|
@@ -21272,10 +21521,11 @@ function registerRoutes(scope, ctx) {
|
|
|
21272
21521
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
21273
21522
|
const events = await readErrorEvents(epath);
|
|
21274
21523
|
const total = events.length;
|
|
21275
|
-
const limit = req.query.limit ? Number(req.query.limit) :
|
|
21276
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21524
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21525
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21277
21526
|
const sliced = events.slice(0, safeLimit);
|
|
21278
|
-
|
|
21527
|
+
const omitted = total - sliced.length;
|
|
21528
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
21279
21529
|
});
|
|
21280
21530
|
scope.get("/stale-events", async (req, reply) => {
|
|
21281
21531
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -21384,16 +21634,20 @@ function registerRoutes(scope, ctx) {
|
|
|
21384
21634
|
const filtered = events.filter(
|
|
21385
21635
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
21386
21636
|
);
|
|
21387
|
-
|
|
21637
|
+
const total = filtered.length;
|
|
21638
|
+
const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21639
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21640
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
21641
|
+
const omitted = total - recent.length;
|
|
21642
|
+
return {
|
|
21643
|
+
count: recent.length,
|
|
21644
|
+
total,
|
|
21645
|
+
events: recent,
|
|
21646
|
+
...omitted > 0 ? { omitted } : {}
|
|
21647
|
+
};
|
|
21388
21648
|
};
|
|
21389
|
-
scope.get(
|
|
21390
|
-
|
|
21391
|
-
incidentHistoryHandler
|
|
21392
|
-
);
|
|
21393
|
-
scope.get(
|
|
21394
|
-
"/graph/incident-history/:nodeId",
|
|
21395
|
-
incidentHistoryHandler
|
|
21396
|
-
);
|
|
21649
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21650
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
21397
21651
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
21398
21652
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
21399
21653
|
if (!proj) return;
|