@neat.is/core 0.9.13-dev.20260901 → 0.9.14
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-WE3AFQYL.js → chunk-NV4WWJSU.js} +78 -7
- package/dist/chunk-NV4WWJSU.js.map +1 -0
- package/dist/{chunk-RBZNXA5L.js → chunk-SSLPBQWY.js} +162 -38
- package/dist/chunk-SSLPBQWY.js.map +1 -0
- package/dist/cli.cjs +691 -84
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +23 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +225 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +225 -44
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +128 -37
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-RBZNXA5L.js.map +0 -1
- package/dist/chunk-WE3AFQYL.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1284,13 +1284,19 @@ function resolveOwningService(graph, nodeId) {
|
|
|
1284
1284
|
}
|
|
1285
1285
|
return null;
|
|
1286
1286
|
}
|
|
1287
|
+
function isFrontierEdge(e) {
|
|
1288
|
+
return e.provenance === import_types.Provenance.FRONTIER;
|
|
1289
|
+
}
|
|
1290
|
+
function rankOf(p) {
|
|
1291
|
+
return p === import_types.Provenance.FRONTIER ? -1 : import_types.PROV_RANK[p];
|
|
1292
|
+
}
|
|
1287
1293
|
function bestEdgeBySource(graph, edgeIds) {
|
|
1288
1294
|
const best = /* @__PURE__ */ new Map();
|
|
1289
1295
|
for (const id of edgeIds) {
|
|
1290
1296
|
const e = graph.getEdgeAttributes(id);
|
|
1291
|
-
if (isFrontierNode(graph, e.source)) continue;
|
|
1297
|
+
if (isFrontierNode(graph, e.source) || isFrontierEdge(e)) continue;
|
|
1292
1298
|
const cur = best.get(e.source);
|
|
1293
|
-
if (!cur ||
|
|
1299
|
+
if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
|
|
1294
1300
|
best.set(e.source, e);
|
|
1295
1301
|
}
|
|
1296
1302
|
}
|
|
@@ -1300,9 +1306,9 @@ function bestEdgeByTarget(graph, edgeIds) {
|
|
|
1300
1306
|
const best = /* @__PURE__ */ new Map();
|
|
1301
1307
|
for (const id of edgeIds) {
|
|
1302
1308
|
const e = graph.getEdgeAttributes(id);
|
|
1303
|
-
if (isFrontierNode(graph, e.target)) continue;
|
|
1309
|
+
if (isFrontierNode(graph, e.target) || isFrontierEdge(e)) continue;
|
|
1304
1310
|
const cur = best.get(e.target);
|
|
1305
|
-
if (!cur ||
|
|
1311
|
+
if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
|
|
1306
1312
|
best.set(e.target, e);
|
|
1307
1313
|
}
|
|
1308
1314
|
}
|
|
@@ -1312,7 +1318,10 @@ var PROVENANCE_CEILING = {
|
|
|
1312
1318
|
OBSERVED: 1,
|
|
1313
1319
|
INFERRED: 0.7,
|
|
1314
1320
|
EXTRACTED: 0.5,
|
|
1315
|
-
STALE: 0.3
|
|
1321
|
+
STALE: 0.3,
|
|
1322
|
+
// A FRONTIER surface (ADR-226) is a proposal about a cause NEAT could not observe
|
|
1323
|
+
// — the least-trusted claim it makes, below STALE (which was at least once seen).
|
|
1324
|
+
FRONTIER: 0.2
|
|
1316
1325
|
};
|
|
1317
1326
|
function volumeWeight(spanCount) {
|
|
1318
1327
|
if (!spanCount || spanCount <= 0) return 0.5;
|
|
@@ -1547,7 +1556,7 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
|
1547
1556
|
});
|
|
1548
1557
|
}
|
|
1549
1558
|
function isFailingCallEdge(e) {
|
|
1550
|
-
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1559
|
+
return e.type === import_types.EdgeType.CALLS && !isFrontierEdge(e) && (e.signal?.errorCount ?? 0) > 0;
|
|
1551
1560
|
}
|
|
1552
1561
|
function callSourcesForService(graph, serviceId17) {
|
|
1553
1562
|
const ids = [serviceId17];
|
|
@@ -1570,8 +1579,8 @@ function failingCallDominates(e, id, curEdge, curId) {
|
|
|
1570
1579
|
const ec = e.signal?.errorCount ?? 0;
|
|
1571
1580
|
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1572
1581
|
if (ec !== cc) return ec > cc;
|
|
1573
|
-
if (
|
|
1574
|
-
return
|
|
1582
|
+
if (rankOf(e.provenance) !== rankOf(curEdge.provenance)) {
|
|
1583
|
+
return rankOf(e.provenance) > rankOf(curEdge.provenance);
|
|
1575
1584
|
}
|
|
1576
1585
|
return id < curId;
|
|
1577
1586
|
}
|
|
@@ -1622,11 +1631,11 @@ function dominantStaleCall(graph, serviceId17, visited) {
|
|
|
1622
1631
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
1623
1632
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1624
1633
|
if (e.type !== import_types.EdgeType.CALLS) continue;
|
|
1625
|
-
if (isFrontierNode(graph, e.target)) continue;
|
|
1634
|
+
if (isFrontierNode(graph, e.target) || isFrontierEdge(e)) continue;
|
|
1626
1635
|
const owner = resolveOwningService(graph, e.target);
|
|
1627
1636
|
if (!owner || visited.has(owner.id)) continue;
|
|
1628
1637
|
const cur = bestByCallee.get(owner.id);
|
|
1629
|
-
if (!cur ||
|
|
1638
|
+
if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
|
|
1630
1639
|
bestByCallee.set(owner.id, e);
|
|
1631
1640
|
}
|
|
1632
1641
|
}
|
|
@@ -1899,6 +1908,7 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
|
1899
1908
|
for (const edgeId of graph.inboundEdges(n)) {
|
|
1900
1909
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1901
1910
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1911
|
+
if (isFrontierEdge(e)) continue;
|
|
1902
1912
|
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
1903
1913
|
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1904
1914
|
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
@@ -1912,6 +1922,7 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
|
1912
1922
|
for (const edgeId of graph.outboundEdges(n)) {
|
|
1913
1923
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1914
1924
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1925
|
+
if (isFrontierEdge(e)) continue;
|
|
1915
1926
|
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1916
1927
|
if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
1917
1928
|
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
@@ -1945,6 +1956,11 @@ function isSaturated(ctx) {
|
|
|
1945
1956
|
function isBoundaryTimeoutSymptom(ctx) {
|
|
1946
1957
|
return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
|
|
1947
1958
|
}
|
|
1959
|
+
var UNREACHABLE_INBOUND_ERROR_RATE = 0.5;
|
|
1960
|
+
var UNREACHABLE_MIN_INBOUND = 3;
|
|
1961
|
+
function isUnreachableSeed(ctx) {
|
|
1962
|
+
return ctx.callCount >= UNREACHABLE_MIN_INBOUND && ctx.errorsFromCallers > 0 && ctx.errorsFromCallers >= UNREACHABLE_INBOUND_ERROR_RATE * ctx.callCount && ctx.errorsEmittedHere === 0 && ctx.outboundVolume === 0;
|
|
1963
|
+
}
|
|
1948
1964
|
function classifyNode(ctx) {
|
|
1949
1965
|
if (ctx.errorsEmittedHere > 0) {
|
|
1950
1966
|
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
@@ -1953,6 +1969,7 @@ function classifyNode(ctx) {
|
|
|
1953
1969
|
if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
|
|
1954
1970
|
return "primary-failure";
|
|
1955
1971
|
}
|
|
1972
|
+
if (isUnreachableSeed(ctx)) return "unreachable";
|
|
1956
1973
|
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
1957
1974
|
return "unrelated";
|
|
1958
1975
|
}
|
|
@@ -2189,22 +2206,44 @@ function routeMatches(declared, incident) {
|
|
|
2189
2206
|
const b = normalizeRoute(incident);
|
|
2190
2207
|
return a === b || a.startsWith(b) || b.startsWith(a);
|
|
2191
2208
|
}
|
|
2192
|
-
function
|
|
2193
|
-
const
|
|
2194
|
-
const callees = declaredOutboundCallees(graph, boundaryNode);
|
|
2209
|
+
function firstDeclaredCallee(graph, node, route) {
|
|
2210
|
+
const callees = declaredOutboundCallees(graph, node);
|
|
2195
2211
|
if (callees.length === 0) return null;
|
|
2196
2212
|
const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
|
|
2197
2213
|
const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
if (
|
|
2204
|
-
|
|
2205
|
-
|
|
2214
|
+
return chosen ? chosen.target : null;
|
|
2215
|
+
}
|
|
2216
|
+
function observedOutboundNextServices(graph, nodeId) {
|
|
2217
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2218
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
2219
|
+
if (!graph.hasNode(n)) continue;
|
|
2220
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
2221
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2222
|
+
if (!DEP_EDGE_TYPES.has(e.type)) continue;
|
|
2223
|
+
if (e.provenance !== import_types.Provenance.OBSERVED) continue;
|
|
2224
|
+
if (e.target !== nodeId) targets.add(e.target);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return [...targets];
|
|
2228
|
+
}
|
|
2229
|
+
function resolveHangHop(graph, boundaryNode, incidents) {
|
|
2230
|
+
const route = boundaryIncidentRoute(boundaryNode, incidents);
|
|
2231
|
+
const own = firstDeclaredCallee(graph, boundaryNode, route);
|
|
2232
|
+
if (own) {
|
|
2233
|
+
return route !== void 0 ? { source: boundaryNode, target: own, route } : { source: boundaryNode, target: own };
|
|
2234
|
+
}
|
|
2235
|
+
const resolved = observedOutboundNextServices(graph, boundaryNode).map((n) => ({ source: n, target: firstDeclaredCallee(graph, n, route) })).filter((r) => r.target !== null);
|
|
2236
|
+
if (resolved.length === 1) {
|
|
2237
|
+
const hop = resolved[0];
|
|
2238
|
+
return route !== void 0 ? { ...hop, route } : hop;
|
|
2206
2239
|
}
|
|
2207
|
-
return
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
function stagedHangProposal(graph, boundaryNode, incidents) {
|
|
2243
|
+
const hop = resolveHangHop(graph, boundaryNode, incidents);
|
|
2244
|
+
if (!hop) return null;
|
|
2245
|
+
if (!graph.hasEdge((0, import_types.frontierEdgeId)(hop.source, hop.target, import_types.EdgeType.CALLS))) return null;
|
|
2246
|
+
return hop.route !== void 0 ? { node: hop.target, route: hop.route } : { node: hop.target };
|
|
2208
2247
|
}
|
|
2209
2248
|
function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
2210
2249
|
const legacy = tagged.result;
|
|
@@ -2239,29 +2278,39 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
2239
2278
|
confidence: Math.min(legacy.confidence, 0.4),
|
|
2240
2279
|
...lastProv ? { provenance: lastProv } : {}
|
|
2241
2280
|
});
|
|
2281
|
+
} else if (seedCtx && isUnreachableSeed(seedCtx)) {
|
|
2282
|
+
const name = displayNameOf(seedNode);
|
|
2283
|
+
candidates.push({
|
|
2284
|
+
node: seedNode,
|
|
2285
|
+
classification: "unreachable",
|
|
2286
|
+
reason: `${name} is unreachable: its callers' requests fail (${seedCtx.errorsFromCallers} erroring inbound calls) and it produced no telemetry of its own \u2014 no server spans, no outbound calls \u2014 so it never served. The failure is observed, but its cause is not in the trace: a startup failure, a crash before the first span, or an unschedulable / unhealthy pod. Inspect ${name}'s deploy state and logs \u2014 there is no code fault to find in the graph here.`,
|
|
2287
|
+
context: seedCtx,
|
|
2288
|
+
confidence: legacy.confidence,
|
|
2289
|
+
provenance: import_types.Provenance.OBSERVED
|
|
2290
|
+
});
|
|
2242
2291
|
} else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
|
|
2243
|
-
const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
|
|
2244
2292
|
const seedName = displayNameOf(seedNode);
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
const causeName = displayNameOf(pointer.node);
|
|
2248
|
-
candidates.push({
|
|
2249
|
-
node: pointer.node,
|
|
2250
|
-
classification: "primary-failure",
|
|
2251
|
-
reason: `${causeName} is the likely root cause (structural, INFERRED \u2014 not observed): the boundary ${seedName} only timed out waiting, and the actual culprit hung without exporting a span, so this is walked from the declared call graph${routeNote}, not from runtime signal. Restore instrumentation / inspect ${causeName} to confirm.`,
|
|
2252
|
-
context: nodeContext(graph, pointer.node, incidents, now),
|
|
2253
|
-
confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
|
|
2254
|
-
provenance: import_types.Provenance.INFERRED
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2293
|
+
const proposal = stagedHangProposal(graph, seedNode, incidents);
|
|
2294
|
+
const routeNote = proposal?.route ? ` serving ${proposal.route}` : "";
|
|
2257
2295
|
candidates.push({
|
|
2258
2296
|
node: seedNode,
|
|
2259
2297
|
classification: "symptom-only",
|
|
2260
|
-
reason:
|
|
2298
|
+
reason: proposal ? `${seedName} timed out waiting on a downstream that hung and exported no span \u2014 a boundary reporting an upstream fault, not the fault itself. The cause is unobservable; NEAT proposes the staged FRONTIER surface below, a hypothesis to confirm, not an observed cause.` : `${seedName} timed out but nothing it can see downstream is erroring \u2014 the cause is downstream and unobserved (the culprit hung and exported no span). No FRONTIER surface${routeNote} is staged, so no cause is proposed; inspect ${seedName}'s declared downstream and restore instrumentation.`,
|
|
2261
2299
|
context: seedCtx,
|
|
2262
2300
|
confidence: Math.min(legacy.confidence, 0.3),
|
|
2263
2301
|
...lastProv ? { provenance: lastProv } : {}
|
|
2264
2302
|
});
|
|
2303
|
+
if (proposal) {
|
|
2304
|
+
const causeName = displayNameOf(proposal.node);
|
|
2305
|
+
candidates.push({
|
|
2306
|
+
node: proposal.node,
|
|
2307
|
+
classification: "primary-failure",
|
|
2308
|
+
reason: `${causeName} is the proposed hang cause (FRONTIER \u2014 a hypothesis, not observed): the boundary ${seedName} only timed out waiting and the culprit hung without exporting a span, so NEAT staged a FRONTIER surface on the unobservable hop${routeNote}. It graduates to OBSERVED when ${causeName} comes back; until then, inspect it / restore instrumentation to confirm, or it is culled.`,
|
|
2309
|
+
context: nodeContext(graph, proposal.node, incidents, now),
|
|
2310
|
+
confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.FRONTIER),
|
|
2311
|
+
provenance: import_types.Provenance.FRONTIER
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2265
2314
|
} else if (staleChain) {
|
|
2266
2315
|
const culprit = staleChain.culprit;
|
|
2267
2316
|
const culpritName = displayNameOf(culprit);
|
|
@@ -2325,6 +2374,9 @@ function fixRecommendationForTop(top, seedNode, legacy) {
|
|
|
2325
2374
|
return legacy.fixRecommendation;
|
|
2326
2375
|
}
|
|
2327
2376
|
const name = top.node.replace(/^service:/, "");
|
|
2377
|
+
if (top.classification === "symptom-only" && top.context.boundaryTimeout === true) {
|
|
2378
|
+
return `${name} timed out waiting on a downstream that hung and exported no span \u2014 the cause is unobservable from traces. Inspect the FRONTIER surface NEAT staged on the unobservable hop (see candidates); it graduates to OBSERVED when that service recovers. Restore instrumentation on that path to confirm, rather than treating ${name} as the fault.`;
|
|
2379
|
+
}
|
|
2328
2380
|
if (top.provenance === import_types.Provenance.STALE) {
|
|
2329
2381
|
return `Live telemetry for this path has gone quiet; the last-observed topology traces the failure downstream to ${name}. Restore instrumentation (or re-run with live traces) to confirm, then inspect ${name}.`;
|
|
2330
2382
|
}
|
|
@@ -6286,6 +6338,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
|
|
|
6286
6338
|
function mergeObservedColumns(graph, tableNodeId, columns) {
|
|
6287
6339
|
mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
|
|
6288
6340
|
}
|
|
6341
|
+
function mergeObservedDeployState(graph, serviceNodeId, state) {
|
|
6342
|
+
if (!graph.hasNode(serviceNodeId)) return;
|
|
6343
|
+
const attrs = {};
|
|
6344
|
+
if (typeof state.image === "string" && state.image.length > 0) attrs["observedImage"] = state.image;
|
|
6345
|
+
if (typeof state.readyReplicas === "number") attrs["observedReadyReplicas"] = state.readyReplicas;
|
|
6346
|
+
if (Object.keys(attrs).length === 0) return;
|
|
6347
|
+
graph.mergeNodeAttributes(serviceNodeId, attrs);
|
|
6348
|
+
}
|
|
6289
6349
|
function ensureDatabaseNode(graph, host, engine) {
|
|
6290
6350
|
const id = (0, import_types8.databaseId)(host);
|
|
6291
6351
|
if (graph.hasNode(id)) return id;
|
|
@@ -6982,6 +7042,32 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
6982
7042
|
}
|
|
6983
7043
|
return promoted;
|
|
6984
7044
|
}
|
|
7045
|
+
function stageFrontierEdge(graph, source, target, type) {
|
|
7046
|
+
if (!graph.hasNode(source) || !graph.hasNode(target)) return false;
|
|
7047
|
+
if (graph.hasEdge((0, import_types8.observedEdgeId)(source, target, type))) return false;
|
|
7048
|
+
const key = (0, import_types8.frontierEdgeId)(source, target, type);
|
|
7049
|
+
if (graph.hasEdge(key)) return false;
|
|
7050
|
+
graph.addEdgeWithKey(key, source, target, {
|
|
7051
|
+
id: key,
|
|
7052
|
+
source,
|
|
7053
|
+
target,
|
|
7054
|
+
type,
|
|
7055
|
+
provenance: import_types8.Provenance.FRONTIER
|
|
7056
|
+
});
|
|
7057
|
+
return true;
|
|
7058
|
+
}
|
|
7059
|
+
function promoteFrontierEdges(graph) {
|
|
7060
|
+
const graduated = [];
|
|
7061
|
+
graph.forEachEdge((edgeId, attrs) => {
|
|
7062
|
+
const e = attrs;
|
|
7063
|
+
if (e.provenance !== import_types8.Provenance.FRONTIER) return;
|
|
7064
|
+
if (graph.hasEdge((0, import_types8.observedEdgeId)(e.source, e.target, e.type))) {
|
|
7065
|
+
graduated.push(edgeId);
|
|
7066
|
+
}
|
|
7067
|
+
});
|
|
7068
|
+
for (const edgeId of graduated) graph.dropEdge(edgeId);
|
|
7069
|
+
return graduated.length;
|
|
7070
|
+
}
|
|
6985
7071
|
function rewireFrontierEdges(graph, frontierId2, serviceId17) {
|
|
6986
7072
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
6987
7073
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
@@ -7090,6 +7176,7 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
7090
7176
|
project: options.project
|
|
7091
7177
|
});
|
|
7092
7178
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
7179
|
+
if (options.onReconcile) await options.onReconcile(graph);
|
|
7093
7180
|
} catch (err) {
|
|
7094
7181
|
console.error("staleness tick failed", err);
|
|
7095
7182
|
}
|
|
@@ -16956,6 +17043,25 @@ function detectColumnDrift(node) {
|
|
|
16956
17043
|
}
|
|
16957
17044
|
return out;
|
|
16958
17045
|
}
|
|
17046
|
+
var RECOMMENDATION_DEPLOY_IMAGE_MISMATCH = "The running image differs from the manifest-declared image \u2014 the rollout has not taken (the old ReplicaSet is still serving). Re-apply the deployment or investigate why the new image failed to roll out.";
|
|
17047
|
+
var DEPLOY_DIVERGENCE_CONFIDENCE = 0.9;
|
|
17048
|
+
function detectDeployDivergence(svc) {
|
|
17049
|
+
const out = [];
|
|
17050
|
+
if (typeof svc.declaredImage === "string" && typeof svc.observedImage === "string" && svc.declaredImage !== svc.observedImage) {
|
|
17051
|
+
out.push({
|
|
17052
|
+
type: "deploy-mismatch",
|
|
17053
|
+
kind: "image",
|
|
17054
|
+
source: svc.id,
|
|
17055
|
+
target: svc.id,
|
|
17056
|
+
declaredImage: svc.declaredImage,
|
|
17057
|
+
observedImage: svc.observedImage,
|
|
17058
|
+
confidence: DEPLOY_DIVERGENCE_CONFIDENCE,
|
|
17059
|
+
reason: `Service ${svc.name} declares image ${svc.declaredImage} but its running pods report ${svc.observedImage} \u2014 the deploy has not taken (the old version is still serving), so no incident fires.`,
|
|
17060
|
+
recommendation: RECOMMENDATION_DEPLOY_IMAGE_MISMATCH
|
|
17061
|
+
});
|
|
17062
|
+
}
|
|
17063
|
+
return out;
|
|
17064
|
+
}
|
|
16959
17065
|
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16960
17066
|
{
|
|
16961
17067
|
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
@@ -17283,6 +17389,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
17283
17389
|
const svc = n;
|
|
17284
17390
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
17285
17391
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
17392
|
+
for (const d of detectDeployDivergence(svc)) all.push(d);
|
|
17286
17393
|
return;
|
|
17287
17394
|
}
|
|
17288
17395
|
if (n.type === import_types60.NodeType.InfraNode && n.kind === "sql-table") {
|
|
@@ -17323,7 +17430,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
17323
17430
|
// orders last so at equal confidence the structural and symbol divergences
|
|
17324
17431
|
// lead, per the contract ("rank below the definitive structural and symbol
|
|
17325
17432
|
// divergences").
|
|
17326
|
-
"observed-failing": 6
|
|
17433
|
+
"observed-failing": 6,
|
|
17434
|
+
// Deploy mismatch (ADR-225) is a definitive structural declared-vs-observed
|
|
17435
|
+
// divergence carrying high confidence (0.9), so the confidence sort already
|
|
17436
|
+
// places it among the structural leaders; this slot only breaks an exact
|
|
17437
|
+
// confidence tie and sits last as a stable tiebreaker.
|
|
17438
|
+
"deploy-mismatch": 7
|
|
17327
17439
|
};
|
|
17328
17440
|
filtered.sort((a, b) => {
|
|
17329
17441
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -18973,6 +19085,11 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
18973
19085
|
unresolved++;
|
|
18974
19086
|
continue;
|
|
18975
19087
|
}
|
|
19088
|
+
if (signal.deployState) {
|
|
19089
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
19090
|
+
mergeObservedDeployState(graph, resolved.targetNodeId, signal.deployState);
|
|
19091
|
+
continue;
|
|
19092
|
+
}
|
|
18976
19093
|
if (signal.incident) {
|
|
18977
19094
|
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
18978
19095
|
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
@@ -23255,6 +23372,36 @@ var import_node_path77 = __toESM(require("path"), 1);
|
|
|
23255
23372
|
var import_node_module = require("module");
|
|
23256
23373
|
init_otel();
|
|
23257
23374
|
|
|
23375
|
+
// src/hang-sensor.ts
|
|
23376
|
+
init_cjs_shims();
|
|
23377
|
+
var import_types101 = require("@neat.is/types");
|
|
23378
|
+
var RECONCILE_INCIDENT_LIMIT = 500;
|
|
23379
|
+
function stageHangSurfaces(graph, incidents) {
|
|
23380
|
+
const boundaries = /* @__PURE__ */ new Set();
|
|
23381
|
+
for (const ev of incidents) {
|
|
23382
|
+
if (ev.affectedNode && graph.hasNode(ev.affectedNode)) boundaries.add(ev.affectedNode);
|
|
23383
|
+
}
|
|
23384
|
+
let staged = 0;
|
|
23385
|
+
for (const boundary of boundaries) {
|
|
23386
|
+
const ctx = nodeContext(graph, boundary, incidents);
|
|
23387
|
+
if (!isBoundaryTimeoutSymptom(ctx)) continue;
|
|
23388
|
+
const hop = resolveHangHop(graph, boundary, incidents);
|
|
23389
|
+
if (!hop) continue;
|
|
23390
|
+
if (stageFrontierEdge(graph, hop.source, hop.target, import_types101.EdgeType.CALLS)) staged++;
|
|
23391
|
+
}
|
|
23392
|
+
return staged;
|
|
23393
|
+
}
|
|
23394
|
+
async function reconcileFrontierSurfaces(graph, errorsPath) {
|
|
23395
|
+
promoteFrontierEdges(graph);
|
|
23396
|
+
let incidents = [];
|
|
23397
|
+
try {
|
|
23398
|
+
incidents = await readErrorEvents(errorsPath, { limit: RECONCILE_INCIDENT_LIMIT });
|
|
23399
|
+
} catch {
|
|
23400
|
+
incidents = [];
|
|
23401
|
+
}
|
|
23402
|
+
stageHangSurfaces(graph, incidents);
|
|
23403
|
+
}
|
|
23404
|
+
|
|
23258
23405
|
// src/connectors/kubernetes/index.ts
|
|
23259
23406
|
init_cjs_shims();
|
|
23260
23407
|
|
|
@@ -23432,6 +23579,7 @@ var IMAGE_PULL_REASONS = /* @__PURE__ */ new Set(["ImagePullBackOff", "ErrImageP
|
|
|
23432
23579
|
var CRASH_LOOP_REASON = "CrashLoopBackOff";
|
|
23433
23580
|
var FIELD_SEP5 = "\0";
|
|
23434
23581
|
var K8S_TARGET_KIND = "k8s-workload";
|
|
23582
|
+
var K8S_DEPLOY_STATE = "deploy-state";
|
|
23435
23583
|
function packK8sTargetName(identity) {
|
|
23436
23584
|
return [identity.serviceName, identity.fault].join(FIELD_SEP5);
|
|
23437
23585
|
}
|
|
@@ -23563,18 +23711,47 @@ function mapDeploymentToSignal(deployment, pods, config) {
|
|
|
23563
23711
|
}
|
|
23564
23712
|
};
|
|
23565
23713
|
}
|
|
23714
|
+
function observedDeployState(deployment, pods) {
|
|
23715
|
+
const readyReplicas = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
|
|
23716
|
+
let image;
|
|
23717
|
+
for (const pod of podsForDeployment(deployment, pods)) {
|
|
23718
|
+
for (const cs of pod.status?.containerStatuses ?? []) {
|
|
23719
|
+
if (cs.state?.running && typeof cs.image === "string" && cs.image.length > 0) {
|
|
23720
|
+
image = cs.image;
|
|
23721
|
+
break;
|
|
23722
|
+
}
|
|
23723
|
+
}
|
|
23724
|
+
if (image) break;
|
|
23725
|
+
}
|
|
23726
|
+
return image !== void 0 ? { image, readyReplicas } : { readyReplicas };
|
|
23727
|
+
}
|
|
23728
|
+
function deployStateSignal(deployment, pods, config) {
|
|
23729
|
+
const name = deployment.metadata?.name;
|
|
23730
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
23731
|
+
const serviceName = serviceNameFor(deployment, config);
|
|
23732
|
+
return {
|
|
23733
|
+
targetKind: K8S_TARGET_KIND,
|
|
23734
|
+
targetName: packK8sTargetName({ serviceName, fault: K8S_DEPLOY_STATE }),
|
|
23735
|
+
callCount: 0,
|
|
23736
|
+
errorCount: 0,
|
|
23737
|
+
lastObservedIso: nowIso2(),
|
|
23738
|
+
deployState: observedDeployState(deployment, pods)
|
|
23739
|
+
};
|
|
23740
|
+
}
|
|
23566
23741
|
function mapWorkloadsToSignals(deployments, pods, config) {
|
|
23567
23742
|
const out = [];
|
|
23568
23743
|
for (const deployment of deployments) {
|
|
23569
|
-
const
|
|
23570
|
-
if (
|
|
23744
|
+
const deployState = deployStateSignal(deployment, pods, config);
|
|
23745
|
+
if (deployState) out.push(deployState);
|
|
23746
|
+
const incident = mapDeploymentToSignal(deployment, pods, config);
|
|
23747
|
+
if (incident) out.push(incident);
|
|
23571
23748
|
}
|
|
23572
23749
|
return out;
|
|
23573
23750
|
}
|
|
23574
23751
|
|
|
23575
23752
|
// src/connectors/kubernetes/resolve.ts
|
|
23576
23753
|
init_cjs_shims();
|
|
23577
|
-
var
|
|
23754
|
+
var import_types103 = require("@neat.is/types");
|
|
23578
23755
|
var NO_ENV3 = "unknown";
|
|
23579
23756
|
function createK8sResolveTarget(graph) {
|
|
23580
23757
|
return (signal) => {
|
|
@@ -23585,7 +23762,7 @@ function createK8sResolveTarget(graph) {
|
|
|
23585
23762
|
return {
|
|
23586
23763
|
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV3),
|
|
23587
23764
|
serviceName,
|
|
23588
|
-
edgeType:
|
|
23765
|
+
edgeType: import_types103.EdgeType.CALLS
|
|
23589
23766
|
};
|
|
23590
23767
|
};
|
|
23591
23768
|
}
|
|
@@ -23720,7 +23897,7 @@ function unroutedErrorsPath(neatHome3) {
|
|
|
23720
23897
|
}
|
|
23721
23898
|
|
|
23722
23899
|
// src/daemon.ts
|
|
23723
|
-
var
|
|
23900
|
+
var import_types106 = require("@neat.is/types");
|
|
23724
23901
|
function daemonJsonPath(scanPath) {
|
|
23725
23902
|
return import_node_path77.default.join(scanPath, "neat-out", "daemon.json");
|
|
23726
23903
|
}
|
|
@@ -23849,7 +24026,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
23849
24026
|
if (!serviceName) return true;
|
|
23850
24027
|
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
23851
24028
|
return graph.someNode(
|
|
23852
|
-
(_id, attrs) => attrs.type ===
|
|
24029
|
+
(_id, attrs) => attrs.type === import_types106.NodeType.ServiceNode && attrs.name === serviceName
|
|
23853
24030
|
);
|
|
23854
24031
|
}
|
|
23855
24032
|
async function bootstrapProject(entry, connectors = [], neatHome3) {
|
|
@@ -23893,7 +24070,11 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
|
|
|
23893
24070
|
const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
|
|
23894
24071
|
const stopStaleness = startStalenessLoop(graph, {
|
|
23895
24072
|
staleEventsPath: paths.staleEventsPath,
|
|
23896
|
-
project: entry.name
|
|
24073
|
+
project: entry.name,
|
|
24074
|
+
// Graduate FRONTIER surfaces whose OBSERVED twin has arrived + stage surfaces
|
|
24075
|
+
// for current hangs (ADR-226), each post-ingest tick — the periodic point
|
|
24076
|
+
// where OBSERVED state has settled and the errors ledger is current.
|
|
24077
|
+
onReconcile: (g) => reconcileFrontierSurfaces(g, paths.errorsPath)
|
|
23897
24078
|
});
|
|
23898
24079
|
const stopConnectors = await startConnectorPolling({
|
|
23899
24080
|
project: entry.name,
|