@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/cli.cjs
CHANGED
|
@@ -1292,13 +1292,19 @@ function resolveOwningService(graph, nodeId) {
|
|
|
1292
1292
|
}
|
|
1293
1293
|
return null;
|
|
1294
1294
|
}
|
|
1295
|
+
function isFrontierEdge(e) {
|
|
1296
|
+
return e.provenance === import_types.Provenance.FRONTIER;
|
|
1297
|
+
}
|
|
1298
|
+
function rankOf(p) {
|
|
1299
|
+
return p === import_types.Provenance.FRONTIER ? -1 : import_types.PROV_RANK[p];
|
|
1300
|
+
}
|
|
1295
1301
|
function bestEdgeBySource(graph, edgeIds) {
|
|
1296
1302
|
const best = /* @__PURE__ */ new Map();
|
|
1297
1303
|
for (const id of edgeIds) {
|
|
1298
1304
|
const e = graph.getEdgeAttributes(id);
|
|
1299
|
-
if (isFrontierNode(graph, e.source)) continue;
|
|
1305
|
+
if (isFrontierNode(graph, e.source) || isFrontierEdge(e)) continue;
|
|
1300
1306
|
const cur = best.get(e.source);
|
|
1301
|
-
if (!cur ||
|
|
1307
|
+
if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
|
|
1302
1308
|
best.set(e.source, e);
|
|
1303
1309
|
}
|
|
1304
1310
|
}
|
|
@@ -1308,9 +1314,9 @@ function bestEdgeByTarget(graph, edgeIds) {
|
|
|
1308
1314
|
const best = /* @__PURE__ */ new Map();
|
|
1309
1315
|
for (const id of edgeIds) {
|
|
1310
1316
|
const e = graph.getEdgeAttributes(id);
|
|
1311
|
-
if (isFrontierNode(graph, e.target)) continue;
|
|
1317
|
+
if (isFrontierNode(graph, e.target) || isFrontierEdge(e)) continue;
|
|
1312
1318
|
const cur = best.get(e.target);
|
|
1313
|
-
if (!cur ||
|
|
1319
|
+
if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
|
|
1314
1320
|
best.set(e.target, e);
|
|
1315
1321
|
}
|
|
1316
1322
|
}
|
|
@@ -1320,7 +1326,10 @@ var PROVENANCE_CEILING = {
|
|
|
1320
1326
|
OBSERVED: 1,
|
|
1321
1327
|
INFERRED: 0.7,
|
|
1322
1328
|
EXTRACTED: 0.5,
|
|
1323
|
-
STALE: 0.3
|
|
1329
|
+
STALE: 0.3,
|
|
1330
|
+
// A FRONTIER surface (ADR-226) is a proposal about a cause NEAT could not observe
|
|
1331
|
+
// — the least-trusted claim it makes, below STALE (which was at least once seen).
|
|
1332
|
+
FRONTIER: 0.2
|
|
1324
1333
|
};
|
|
1325
1334
|
function volumeWeight(spanCount) {
|
|
1326
1335
|
if (!spanCount || spanCount <= 0) return 0.5;
|
|
@@ -1555,7 +1564,7 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
|
1555
1564
|
});
|
|
1556
1565
|
}
|
|
1557
1566
|
function isFailingCallEdge(e) {
|
|
1558
|
-
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1567
|
+
return e.type === import_types.EdgeType.CALLS && !isFrontierEdge(e) && (e.signal?.errorCount ?? 0) > 0;
|
|
1559
1568
|
}
|
|
1560
1569
|
function callSourcesForService(graph, serviceId17) {
|
|
1561
1570
|
const ids = [serviceId17];
|
|
@@ -1578,8 +1587,8 @@ function failingCallDominates(e, id, curEdge, curId) {
|
|
|
1578
1587
|
const ec = e.signal?.errorCount ?? 0;
|
|
1579
1588
|
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1580
1589
|
if (ec !== cc) return ec > cc;
|
|
1581
|
-
if (
|
|
1582
|
-
return
|
|
1590
|
+
if (rankOf(e.provenance) !== rankOf(curEdge.provenance)) {
|
|
1591
|
+
return rankOf(e.provenance) > rankOf(curEdge.provenance);
|
|
1583
1592
|
}
|
|
1584
1593
|
return id < curId;
|
|
1585
1594
|
}
|
|
@@ -1630,11 +1639,11 @@ function dominantStaleCall(graph, serviceId17, visited) {
|
|
|
1630
1639
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
1631
1640
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1632
1641
|
if (e.type !== import_types.EdgeType.CALLS) continue;
|
|
1633
|
-
if (isFrontierNode(graph, e.target)) continue;
|
|
1642
|
+
if (isFrontierNode(graph, e.target) || isFrontierEdge(e)) continue;
|
|
1634
1643
|
const owner = resolveOwningService(graph, e.target);
|
|
1635
1644
|
if (!owner || visited.has(owner.id)) continue;
|
|
1636
1645
|
const cur = bestByCallee.get(owner.id);
|
|
1637
|
-
if (!cur ||
|
|
1646
|
+
if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
|
|
1638
1647
|
bestByCallee.set(owner.id, e);
|
|
1639
1648
|
}
|
|
1640
1649
|
}
|
|
@@ -1907,6 +1916,7 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
|
1907
1916
|
for (const edgeId of graph.inboundEdges(n)) {
|
|
1908
1917
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1909
1918
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1919
|
+
if (isFrontierEdge(e)) continue;
|
|
1910
1920
|
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
1911
1921
|
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1912
1922
|
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
@@ -1920,6 +1930,7 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
|
1920
1930
|
for (const edgeId of graph.outboundEdges(n)) {
|
|
1921
1931
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1922
1932
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1933
|
+
if (isFrontierEdge(e)) continue;
|
|
1923
1934
|
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1924
1935
|
if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
1925
1936
|
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
@@ -1953,6 +1964,11 @@ function isSaturated(ctx) {
|
|
|
1953
1964
|
function isBoundaryTimeoutSymptom(ctx) {
|
|
1954
1965
|
return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
|
|
1955
1966
|
}
|
|
1967
|
+
var UNREACHABLE_INBOUND_ERROR_RATE = 0.5;
|
|
1968
|
+
var UNREACHABLE_MIN_INBOUND = 3;
|
|
1969
|
+
function isUnreachableSeed(ctx) {
|
|
1970
|
+
return ctx.callCount >= UNREACHABLE_MIN_INBOUND && ctx.errorsFromCallers > 0 && ctx.errorsFromCallers >= UNREACHABLE_INBOUND_ERROR_RATE * ctx.callCount && ctx.errorsEmittedHere === 0 && ctx.outboundVolume === 0;
|
|
1971
|
+
}
|
|
1956
1972
|
function classifyNode(ctx) {
|
|
1957
1973
|
if (ctx.errorsEmittedHere > 0) {
|
|
1958
1974
|
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
@@ -1961,6 +1977,7 @@ function classifyNode(ctx) {
|
|
|
1961
1977
|
if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
|
|
1962
1978
|
return "primary-failure";
|
|
1963
1979
|
}
|
|
1980
|
+
if (isUnreachableSeed(ctx)) return "unreachable";
|
|
1964
1981
|
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
1965
1982
|
return "unrelated";
|
|
1966
1983
|
}
|
|
@@ -2197,22 +2214,44 @@ function routeMatches(declared, incident) {
|
|
|
2197
2214
|
const b = normalizeRoute(incident);
|
|
2198
2215
|
return a === b || a.startsWith(b) || b.startsWith(a);
|
|
2199
2216
|
}
|
|
2200
|
-
function
|
|
2201
|
-
const
|
|
2202
|
-
const callees = declaredOutboundCallees(graph, boundaryNode);
|
|
2217
|
+
function firstDeclaredCallee(graph, node, route) {
|
|
2218
|
+
const callees = declaredOutboundCallees(graph, node);
|
|
2203
2219
|
if (callees.length === 0) return null;
|
|
2204
2220
|
const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
|
|
2205
2221
|
const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
if (
|
|
2212
|
-
|
|
2213
|
-
|
|
2222
|
+
return chosen ? chosen.target : null;
|
|
2223
|
+
}
|
|
2224
|
+
function observedOutboundNextServices(graph, nodeId) {
|
|
2225
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2226
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
2227
|
+
if (!graph.hasNode(n)) continue;
|
|
2228
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
2229
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2230
|
+
if (!DEP_EDGE_TYPES.has(e.type)) continue;
|
|
2231
|
+
if (e.provenance !== import_types.Provenance.OBSERVED) continue;
|
|
2232
|
+
if (e.target !== nodeId) targets.add(e.target);
|
|
2233
|
+
}
|
|
2214
2234
|
}
|
|
2215
|
-
return
|
|
2235
|
+
return [...targets];
|
|
2236
|
+
}
|
|
2237
|
+
function resolveHangHop(graph, boundaryNode, incidents) {
|
|
2238
|
+
const route = boundaryIncidentRoute(boundaryNode, incidents);
|
|
2239
|
+
const own = firstDeclaredCallee(graph, boundaryNode, route);
|
|
2240
|
+
if (own) {
|
|
2241
|
+
return route !== void 0 ? { source: boundaryNode, target: own, route } : { source: boundaryNode, target: own };
|
|
2242
|
+
}
|
|
2243
|
+
const resolved = observedOutboundNextServices(graph, boundaryNode).map((n) => ({ source: n, target: firstDeclaredCallee(graph, n, route) })).filter((r) => r.target !== null);
|
|
2244
|
+
if (resolved.length === 1) {
|
|
2245
|
+
const hop = resolved[0];
|
|
2246
|
+
return route !== void 0 ? { ...hop, route } : hop;
|
|
2247
|
+
}
|
|
2248
|
+
return null;
|
|
2249
|
+
}
|
|
2250
|
+
function stagedHangProposal(graph, boundaryNode, incidents) {
|
|
2251
|
+
const hop = resolveHangHop(graph, boundaryNode, incidents);
|
|
2252
|
+
if (!hop) return null;
|
|
2253
|
+
if (!graph.hasEdge((0, import_types.frontierEdgeId)(hop.source, hop.target, import_types.EdgeType.CALLS))) return null;
|
|
2254
|
+
return hop.route !== void 0 ? { node: hop.target, route: hop.route } : { node: hop.target };
|
|
2216
2255
|
}
|
|
2217
2256
|
function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
2218
2257
|
const legacy = tagged.result;
|
|
@@ -2247,29 +2286,39 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
2247
2286
|
confidence: Math.min(legacy.confidence, 0.4),
|
|
2248
2287
|
...lastProv ? { provenance: lastProv } : {}
|
|
2249
2288
|
});
|
|
2289
|
+
} else if (seedCtx && isUnreachableSeed(seedCtx)) {
|
|
2290
|
+
const name = displayNameOf(seedNode);
|
|
2291
|
+
candidates.push({
|
|
2292
|
+
node: seedNode,
|
|
2293
|
+
classification: "unreachable",
|
|
2294
|
+
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.`,
|
|
2295
|
+
context: seedCtx,
|
|
2296
|
+
confidence: legacy.confidence,
|
|
2297
|
+
provenance: import_types.Provenance.OBSERVED
|
|
2298
|
+
});
|
|
2250
2299
|
} else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
|
|
2251
|
-
const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
|
|
2252
2300
|
const seedName = displayNameOf(seedNode);
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
const causeName = displayNameOf(pointer.node);
|
|
2256
|
-
candidates.push({
|
|
2257
|
-
node: pointer.node,
|
|
2258
|
-
classification: "primary-failure",
|
|
2259
|
-
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.`,
|
|
2260
|
-
context: nodeContext(graph, pointer.node, incidents, now),
|
|
2261
|
-
confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
|
|
2262
|
-
provenance: import_types.Provenance.INFERRED
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
2301
|
+
const proposal = stagedHangProposal(graph, seedNode, incidents);
|
|
2302
|
+
const routeNote = proposal?.route ? ` serving ${proposal.route}` : "";
|
|
2265
2303
|
candidates.push({
|
|
2266
2304
|
node: seedNode,
|
|
2267
2305
|
classification: "symptom-only",
|
|
2268
|
-
reason:
|
|
2306
|
+
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.`,
|
|
2269
2307
|
context: seedCtx,
|
|
2270
2308
|
confidence: Math.min(legacy.confidence, 0.3),
|
|
2271
2309
|
...lastProv ? { provenance: lastProv } : {}
|
|
2272
2310
|
});
|
|
2311
|
+
if (proposal) {
|
|
2312
|
+
const causeName = displayNameOf(proposal.node);
|
|
2313
|
+
candidates.push({
|
|
2314
|
+
node: proposal.node,
|
|
2315
|
+
classification: "primary-failure",
|
|
2316
|
+
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.`,
|
|
2317
|
+
context: nodeContext(graph, proposal.node, incidents, now),
|
|
2318
|
+
confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.FRONTIER),
|
|
2319
|
+
provenance: import_types.Provenance.FRONTIER
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2273
2322
|
} else if (staleChain) {
|
|
2274
2323
|
const culprit = staleChain.culprit;
|
|
2275
2324
|
const culpritName = displayNameOf(culprit);
|
|
@@ -2333,6 +2382,9 @@ function fixRecommendationForTop(top, seedNode, legacy) {
|
|
|
2333
2382
|
return legacy.fixRecommendation;
|
|
2334
2383
|
}
|
|
2335
2384
|
const name = top.node.replace(/^service:/, "");
|
|
2385
|
+
if (top.classification === "symptom-only" && top.context.boundaryTimeout === true) {
|
|
2386
|
+
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.`;
|
|
2387
|
+
}
|
|
2336
2388
|
if (top.provenance === import_types.Provenance.STALE) {
|
|
2337
2389
|
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}.`;
|
|
2338
2390
|
}
|
|
@@ -6045,8 +6097,8 @@ function pickContainingSymbol(candidates, fn) {
|
|
|
6045
6097
|
return b.symbol.span.startLine - a.symbol.span.startLine;
|
|
6046
6098
|
};
|
|
6047
6099
|
if (fn) {
|
|
6048
|
-
const
|
|
6049
|
-
if (
|
|
6100
|
+
const named2 = candidates.filter((c) => terminalName(c.symbol.qualname) === fn);
|
|
6101
|
+
if (named2.length > 0) return [...named2].sort(bySpan)[0].id;
|
|
6050
6102
|
}
|
|
6051
6103
|
return [...candidates].sort(bySpan)[0].id;
|
|
6052
6104
|
}
|
|
@@ -6306,6 +6358,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
|
|
|
6306
6358
|
function mergeObservedColumns(graph, tableNodeId, columns) {
|
|
6307
6359
|
mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
|
|
6308
6360
|
}
|
|
6361
|
+
function mergeObservedDeployState(graph, serviceNodeId, state) {
|
|
6362
|
+
if (!graph.hasNode(serviceNodeId)) return;
|
|
6363
|
+
const attrs = {};
|
|
6364
|
+
if (typeof state.image === "string" && state.image.length > 0) attrs["observedImage"] = state.image;
|
|
6365
|
+
if (typeof state.readyReplicas === "number") attrs["observedReadyReplicas"] = state.readyReplicas;
|
|
6366
|
+
if (Object.keys(attrs).length === 0) return;
|
|
6367
|
+
graph.mergeNodeAttributes(serviceNodeId, attrs);
|
|
6368
|
+
}
|
|
6309
6369
|
function ensureDatabaseNode(graph, host, engine) {
|
|
6310
6370
|
const id = (0, import_types8.databaseId)(host);
|
|
6311
6371
|
if (graph.hasNode(id)) return id;
|
|
@@ -7002,6 +7062,32 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
7002
7062
|
}
|
|
7003
7063
|
return promoted;
|
|
7004
7064
|
}
|
|
7065
|
+
function stageFrontierEdge(graph, source, target, type) {
|
|
7066
|
+
if (!graph.hasNode(source) || !graph.hasNode(target)) return false;
|
|
7067
|
+
if (graph.hasEdge((0, import_types8.observedEdgeId)(source, target, type))) return false;
|
|
7068
|
+
const key = (0, import_types8.frontierEdgeId)(source, target, type);
|
|
7069
|
+
if (graph.hasEdge(key)) return false;
|
|
7070
|
+
graph.addEdgeWithKey(key, source, target, {
|
|
7071
|
+
id: key,
|
|
7072
|
+
source,
|
|
7073
|
+
target,
|
|
7074
|
+
type,
|
|
7075
|
+
provenance: import_types8.Provenance.FRONTIER
|
|
7076
|
+
});
|
|
7077
|
+
return true;
|
|
7078
|
+
}
|
|
7079
|
+
function promoteFrontierEdges(graph) {
|
|
7080
|
+
const graduated = [];
|
|
7081
|
+
graph.forEachEdge((edgeId, attrs) => {
|
|
7082
|
+
const e = attrs;
|
|
7083
|
+
if (e.provenance !== import_types8.Provenance.FRONTIER) return;
|
|
7084
|
+
if (graph.hasEdge((0, import_types8.observedEdgeId)(e.source, e.target, e.type))) {
|
|
7085
|
+
graduated.push(edgeId);
|
|
7086
|
+
}
|
|
7087
|
+
});
|
|
7088
|
+
for (const edgeId of graduated) graph.dropEdge(edgeId);
|
|
7089
|
+
return graduated.length;
|
|
7090
|
+
}
|
|
7005
7091
|
function rewireFrontierEdges(graph, frontierId2, serviceId17) {
|
|
7006
7092
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
7007
7093
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
@@ -7110,6 +7196,7 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
7110
7196
|
project: options.project
|
|
7111
7197
|
});
|
|
7112
7198
|
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
7199
|
+
if (options.onReconcile) await options.onReconcile(graph);
|
|
7113
7200
|
} catch (err) {
|
|
7114
7201
|
console.error("staleness tick failed", err);
|
|
7115
7202
|
}
|
|
@@ -9465,10 +9552,10 @@ function collectNamedImports(root) {
|
|
|
9465
9552
|
const clause = node.namedChild(i);
|
|
9466
9553
|
if (clause?.type !== "import_clause") continue;
|
|
9467
9554
|
for (let j = 0; j < clause.namedChildCount; j++) {
|
|
9468
|
-
const
|
|
9469
|
-
if (
|
|
9470
|
-
for (let k = 0; k <
|
|
9471
|
-
const spec =
|
|
9555
|
+
const named2 = clause.namedChild(j);
|
|
9556
|
+
if (named2?.type !== "named_imports") continue;
|
|
9557
|
+
for (let k = 0; k < named2.namedChildCount; k++) {
|
|
9558
|
+
const spec = named2.namedChild(k);
|
|
9472
9559
|
if (spec?.type !== "import_specifier") continue;
|
|
9473
9560
|
let isType = false;
|
|
9474
9561
|
for (let t = 0; t < spec.childCount; t++) {
|
|
@@ -12102,8 +12189,8 @@ function parseImportBindings(content) {
|
|
|
12102
12189
|
out.push({ local: ns[1], kind: "namespace", specifier: spec });
|
|
12103
12190
|
continue;
|
|
12104
12191
|
}
|
|
12105
|
-
const
|
|
12106
|
-
if (
|
|
12192
|
+
const named2 = /\{([^}]*)\}/.exec(clause);
|
|
12193
|
+
if (named2) parseDestructure(named2[1], spec, out);
|
|
12107
12194
|
const def = /^(\w+)\s*(?:,|$)/.exec(clause);
|
|
12108
12195
|
if (def && !clause.startsWith("{")) out.push({ local: def[1], kind: "default", specifier: spec });
|
|
12109
12196
|
}
|
|
@@ -12123,8 +12210,8 @@ function fileExportsOf(content, pluralizeOn) {
|
|
|
12123
12210
|
for (const d of defs) if (d.varName) byVar.set(d.varName, d.resolved.name);
|
|
12124
12211
|
const byName = new Map(byVar);
|
|
12125
12212
|
let def;
|
|
12126
|
-
const
|
|
12127
|
-
if (
|
|
12213
|
+
const named2 = /(?:module\.exports|export\s+default)\s*=\s*(\w+)\b/.exec(content);
|
|
12214
|
+
if (named2 && byVar.has(named2[1])) def = byVar.get(named2[1]);
|
|
12128
12215
|
if (!def) {
|
|
12129
12216
|
const inline = /(?:module\.exports|export\s+default)\s*=\s*(?:await\s+)?(?:\w+\s*\.\s*)?model\s*\(\s*['"`]([\w$]+)['"`]/.exec(content);
|
|
12130
12217
|
if (inline) def = pluralizeOn ? pluralizeCollection(inline[1]) : inline[1];
|
|
@@ -16494,6 +16581,25 @@ function detectColumnDrift(node) {
|
|
|
16494
16581
|
}
|
|
16495
16582
|
return out;
|
|
16496
16583
|
}
|
|
16584
|
+
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.";
|
|
16585
|
+
var DEPLOY_DIVERGENCE_CONFIDENCE = 0.9;
|
|
16586
|
+
function detectDeployDivergence(svc) {
|
|
16587
|
+
const out = [];
|
|
16588
|
+
if (typeof svc.declaredImage === "string" && typeof svc.observedImage === "string" && svc.declaredImage !== svc.observedImage) {
|
|
16589
|
+
out.push({
|
|
16590
|
+
type: "deploy-mismatch",
|
|
16591
|
+
kind: "image",
|
|
16592
|
+
source: svc.id,
|
|
16593
|
+
target: svc.id,
|
|
16594
|
+
declaredImage: svc.declaredImage,
|
|
16595
|
+
observedImage: svc.observedImage,
|
|
16596
|
+
confidence: DEPLOY_DIVERGENCE_CONFIDENCE,
|
|
16597
|
+
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.`,
|
|
16598
|
+
recommendation: RECOMMENDATION_DEPLOY_IMAGE_MISMATCH
|
|
16599
|
+
});
|
|
16600
|
+
}
|
|
16601
|
+
return out;
|
|
16602
|
+
}
|
|
16497
16603
|
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16498
16604
|
{
|
|
16499
16605
|
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
@@ -16821,6 +16927,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16821
16927
|
const svc = n;
|
|
16822
16928
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
16823
16929
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
16930
|
+
for (const d of detectDeployDivergence(svc)) all.push(d);
|
|
16824
16931
|
return;
|
|
16825
16932
|
}
|
|
16826
16933
|
if (n.type === import_types59.NodeType.InfraNode && n.kind === "sql-table") {
|
|
@@ -16861,7 +16968,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16861
16968
|
// orders last so at equal confidence the structural and symbol divergences
|
|
16862
16969
|
// lead, per the contract ("rank below the definitive structural and symbol
|
|
16863
16970
|
// divergences").
|
|
16864
|
-
"observed-failing": 6
|
|
16971
|
+
"observed-failing": 6,
|
|
16972
|
+
// Deploy mismatch (ADR-225) is a definitive structural declared-vs-observed
|
|
16973
|
+
// divergence carrying high confidence (0.9), so the confidence sort already
|
|
16974
|
+
// places it among the structural leaders; this slot only breaks an exact
|
|
16975
|
+
// confidence tie and sits last as a stable tiebreaker.
|
|
16976
|
+
"deploy-mismatch": 7
|
|
16865
16977
|
};
|
|
16866
16978
|
filtered.sort((a, b) => {
|
|
16867
16979
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -17956,11 +18068,11 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
|
|
|
17956
18068
|
if (labelTokens.size === 0) return;
|
|
17957
18069
|
let matched = 0;
|
|
17958
18070
|
for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
|
|
17959
|
-
const
|
|
17960
|
-
if (matched === 0 && !
|
|
18071
|
+
const named2 = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
|
|
18072
|
+
if (matched === 0 && !named2) return;
|
|
17961
18073
|
const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
|
|
17962
|
-
const via =
|
|
17963
|
-
const score =
|
|
18074
|
+
const via = named2 ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
|
|
18075
|
+
const score = named2 ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
|
|
17964
18076
|
consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
|
|
17965
18077
|
});
|
|
17966
18078
|
if (searchIndex) {
|
|
@@ -19300,6 +19412,11 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
19300
19412
|
unresolved++;
|
|
19301
19413
|
continue;
|
|
19302
19414
|
}
|
|
19415
|
+
if (signal.deployState) {
|
|
19416
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
19417
|
+
mergeObservedDeployState(graph, resolved.targetNodeId, signal.deployState);
|
|
19418
|
+
continue;
|
|
19419
|
+
}
|
|
19303
19420
|
if (signal.incident) {
|
|
19304
19421
|
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
19305
19422
|
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
@@ -19833,7 +19950,7 @@ function tableNameFromQueryText(query) {
|
|
|
19833
19950
|
if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null;
|
|
19834
19951
|
return name;
|
|
19835
19952
|
}
|
|
19836
|
-
function diffPgStatStatementsToSignals(rows, previous,
|
|
19953
|
+
function diffPgStatStatementsToSignals(rows, previous, nowIso3) {
|
|
19837
19954
|
const signals = [];
|
|
19838
19955
|
const seen = /* @__PURE__ */ new Set();
|
|
19839
19956
|
if (!Array.isArray(rows)) return signals;
|
|
@@ -19855,7 +19972,7 @@ function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
|
|
|
19855
19972
|
targetName: table,
|
|
19856
19973
|
callCount: delta,
|
|
19857
19974
|
errorCount: 0,
|
|
19858
|
-
lastObservedIso:
|
|
19975
|
+
lastObservedIso: nowIso3,
|
|
19859
19976
|
...columns.length > 0 ? { columns } : {}
|
|
19860
19977
|
});
|
|
19861
19978
|
}
|
|
@@ -22751,11 +22868,11 @@ function serializeGraph(graph) {
|
|
|
22751
22868
|
}
|
|
22752
22869
|
function projectFromReq(req, singleProject) {
|
|
22753
22870
|
const params = req.params;
|
|
22754
|
-
const
|
|
22871
|
+
const named2 = params.project;
|
|
22755
22872
|
if (singleProject) {
|
|
22756
|
-
return
|
|
22873
|
+
return named2 === void 0 || named2 === DEFAULT_PROJECT ? singleProject : named2;
|
|
22757
22874
|
}
|
|
22758
|
-
return
|
|
22875
|
+
return named2 ?? DEFAULT_PROJECT;
|
|
22759
22876
|
}
|
|
22760
22877
|
function resolveProject(registry, req, reply, bootstrap, singleProject) {
|
|
22761
22878
|
const name = projectFromReq(req, singleProject);
|
|
@@ -23652,13 +23769,38 @@ async function buildApi(opts) {
|
|
|
23652
23769
|
|
|
23653
23770
|
// src/watch.ts
|
|
23654
23771
|
init_auth();
|
|
23655
|
-
init_otel();
|
|
23656
23772
|
|
|
23657
|
-
// src/
|
|
23773
|
+
// src/hang-sensor.ts
|
|
23658
23774
|
init_cjs_shims();
|
|
23659
|
-
var
|
|
23660
|
-
var
|
|
23661
|
-
|
|
23775
|
+
var import_types102 = require("@neat.is/types");
|
|
23776
|
+
var RECONCILE_INCIDENT_LIMIT = 500;
|
|
23777
|
+
function stageHangSurfaces(graph, incidents) {
|
|
23778
|
+
const boundaries = /* @__PURE__ */ new Set();
|
|
23779
|
+
for (const ev of incidents) {
|
|
23780
|
+
if (ev.affectedNode && graph.hasNode(ev.affectedNode)) boundaries.add(ev.affectedNode);
|
|
23781
|
+
}
|
|
23782
|
+
let staged = 0;
|
|
23783
|
+
for (const boundary of boundaries) {
|
|
23784
|
+
const ctx = nodeContext(graph, boundary, incidents);
|
|
23785
|
+
if (!isBoundaryTimeoutSymptom(ctx)) continue;
|
|
23786
|
+
const hop = resolveHangHop(graph, boundary, incidents);
|
|
23787
|
+
if (!hop) continue;
|
|
23788
|
+
if (stageFrontierEdge(graph, hop.source, hop.target, import_types102.EdgeType.CALLS)) staged++;
|
|
23789
|
+
}
|
|
23790
|
+
return staged;
|
|
23791
|
+
}
|
|
23792
|
+
async function reconcileFrontierSurfaces(graph, errorsPath) {
|
|
23793
|
+
promoteFrontierEdges(graph);
|
|
23794
|
+
let incidents = [];
|
|
23795
|
+
try {
|
|
23796
|
+
incidents = await readErrorEvents(errorsPath, { limit: RECONCILE_INCIDENT_LIMIT });
|
|
23797
|
+
} catch {
|
|
23798
|
+
incidents = [];
|
|
23799
|
+
}
|
|
23800
|
+
stageHangSurfaces(graph, incidents);
|
|
23801
|
+
}
|
|
23802
|
+
|
|
23803
|
+
// src/watch.ts
|
|
23662
23804
|
init_otel();
|
|
23663
23805
|
|
|
23664
23806
|
// src/connectors/kubernetes/index.ts
|
|
@@ -23667,29 +23809,476 @@ init_cjs_shims();
|
|
|
23667
23809
|
// src/connectors/kubernetes/client.ts
|
|
23668
23810
|
init_cjs_shims();
|
|
23669
23811
|
var import_node_https = require("https");
|
|
23812
|
+
function deploymentsPath(namespace) {
|
|
23813
|
+
return `/apis/apps/v1/namespaces/${namespace}/deployments`;
|
|
23814
|
+
}
|
|
23815
|
+
function podsPath(namespace) {
|
|
23816
|
+
return `/api/v1/namespaces/${namespace}/pods`;
|
|
23817
|
+
}
|
|
23818
|
+
function makeK8sFetchImpl(transport) {
|
|
23819
|
+
const agent = new import_node_https.Agent({
|
|
23820
|
+
...transport.ca ? { ca: transport.ca } : {},
|
|
23821
|
+
...transport.clientCert ? { cert: transport.clientCert } : {},
|
|
23822
|
+
...transport.clientKey ? { key: transport.clientKey } : {},
|
|
23823
|
+
rejectUnauthorized: !transport.insecureSkipTlsVerify
|
|
23824
|
+
});
|
|
23825
|
+
return ((url, init) => new Promise((resolve, reject) => {
|
|
23826
|
+
const u = new URL(String(url));
|
|
23827
|
+
const req = (0, import_node_https.request)(
|
|
23828
|
+
u,
|
|
23829
|
+
{
|
|
23830
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
23831
|
+
headers: init?.headers ?? {},
|
|
23832
|
+
agent
|
|
23833
|
+
},
|
|
23834
|
+
(res) => {
|
|
23835
|
+
const chunks = [];
|
|
23836
|
+
res.on("data", (c) => chunks.push(c));
|
|
23837
|
+
res.on("end", () => {
|
|
23838
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
23839
|
+
const status2 = res.statusCode ?? 0;
|
|
23840
|
+
resolve({
|
|
23841
|
+
ok: status2 >= 200 && status2 < 300,
|
|
23842
|
+
status: status2,
|
|
23843
|
+
statusText: res.statusMessage ?? "",
|
|
23844
|
+
json: async () => JSON.parse(body),
|
|
23845
|
+
text: async () => body
|
|
23846
|
+
});
|
|
23847
|
+
});
|
|
23848
|
+
}
|
|
23849
|
+
);
|
|
23850
|
+
req.on("error", reject);
|
|
23851
|
+
const signal = init?.signal;
|
|
23852
|
+
if (signal) {
|
|
23853
|
+
if (signal.aborted) req.destroy(new Error("aborted"));
|
|
23854
|
+
else signal.addEventListener("abort", () => req.destroy(new Error("aborted")), { once: true });
|
|
23855
|
+
}
|
|
23856
|
+
req.end();
|
|
23857
|
+
}));
|
|
23858
|
+
}
|
|
23859
|
+
async function listResource(transport, namespace, path96, opts = {}) {
|
|
23860
|
+
const base = opts.apiUrl ?? transport.server;
|
|
23861
|
+
const url = `${base.replace(/\/$/, "")}${path96}`;
|
|
23862
|
+
const fetchImpl = opts.fetchImpl ?? makeK8sFetchImpl(transport);
|
|
23863
|
+
const res = await junctionFetch(
|
|
23864
|
+
url,
|
|
23865
|
+
{ method: "GET", headers: { ...transport.token ? bearerAuthHeader(transport.token) : {}, Accept: "application/json" } },
|
|
23866
|
+
// accountKey: the (cluster, namespace) pair — an identifier, safe to log, the
|
|
23867
|
+
// rate-limit bucket for one namespace on one cluster (ADR-131).
|
|
23868
|
+
{ provider: "kubernetes", accountKey: `${safeHost(transport.server)}/${namespace}`, fetchImpl }
|
|
23869
|
+
);
|
|
23870
|
+
if (!res.ok) {
|
|
23871
|
+
throw new Error(`kubernetes ${path96} failed: ${res.status} ${res.statusText}`);
|
|
23872
|
+
}
|
|
23873
|
+
const json = await res.json();
|
|
23874
|
+
return Array.isArray(json.items) ? json.items : [];
|
|
23875
|
+
}
|
|
23876
|
+
function safeHost(server) {
|
|
23877
|
+
try {
|
|
23878
|
+
return new URL(server).host;
|
|
23879
|
+
} catch {
|
|
23880
|
+
return "cluster";
|
|
23881
|
+
}
|
|
23882
|
+
}
|
|
23883
|
+
async function fetchDeployments(transport, namespace, opts = {}) {
|
|
23884
|
+
return listResource(transport, namespace, deploymentsPath(namespace), opts);
|
|
23885
|
+
}
|
|
23886
|
+
async function fetchPods(transport, namespace, opts = {}) {
|
|
23887
|
+
return listResource(transport, namespace, podsPath(namespace), opts);
|
|
23888
|
+
}
|
|
23670
23889
|
|
|
23671
23890
|
// src/connectors/kubernetes/kubeconfig.ts
|
|
23672
23891
|
init_cjs_shims();
|
|
23673
23892
|
var import_node_fs41 = require("fs");
|
|
23674
23893
|
var import_yaml4 = require("yaml");
|
|
23894
|
+
function pemFrom(dataField, pathField) {
|
|
23895
|
+
if (typeof dataField === "string" && dataField.length > 0) {
|
|
23896
|
+
return Buffer.from(dataField, "base64").toString("utf8");
|
|
23897
|
+
}
|
|
23898
|
+
if (typeof pathField === "string" && pathField.length > 0) {
|
|
23899
|
+
return (0, import_node_fs41.readFileSync)(pathField, "utf8");
|
|
23900
|
+
}
|
|
23901
|
+
return void 0;
|
|
23902
|
+
}
|
|
23903
|
+
function named(list, name) {
|
|
23904
|
+
if (!Array.isArray(list)) return void 0;
|
|
23905
|
+
const hit = list.find((e) => e && e.name === name);
|
|
23906
|
+
return hit;
|
|
23907
|
+
}
|
|
23908
|
+
function parseKubeconfig(kubeconfig) {
|
|
23909
|
+
const looksInline = /\n/.test(kubeconfig) || /(^|\s)clusters\s*:/.test(kubeconfig);
|
|
23910
|
+
const text = looksInline ? kubeconfig : (0, import_node_fs41.readFileSync)(kubeconfig, "utf8");
|
|
23911
|
+
let doc;
|
|
23912
|
+
try {
|
|
23913
|
+
doc = (0, import_yaml4.parse)(text);
|
|
23914
|
+
} catch {
|
|
23915
|
+
throw new Error("kubernetes connector: kubeconfig is not valid YAML");
|
|
23916
|
+
}
|
|
23917
|
+
if (!doc || typeof doc !== "object") {
|
|
23918
|
+
throw new Error("kubernetes connector: kubeconfig is empty or malformed");
|
|
23919
|
+
}
|
|
23920
|
+
const currentContext = doc["current-context"];
|
|
23921
|
+
if (typeof currentContext !== "string" || currentContext.length === 0) {
|
|
23922
|
+
throw new Error("kubernetes connector: kubeconfig has no current-context");
|
|
23923
|
+
}
|
|
23924
|
+
const ctxEntry = named(doc["contexts"], currentContext);
|
|
23925
|
+
const ctx = ctxEntry?.context;
|
|
23926
|
+
if (!ctx) {
|
|
23927
|
+
throw new Error(`kubernetes connector: kubeconfig context "${currentContext}" not found`);
|
|
23928
|
+
}
|
|
23929
|
+
const clusterEntry = named(doc["clusters"], String(ctx["cluster"] ?? ""));
|
|
23930
|
+
const cluster = clusterEntry?.cluster;
|
|
23931
|
+
if (!cluster || typeof cluster["server"] !== "string") {
|
|
23932
|
+
throw new Error("kubernetes connector: kubeconfig current context has no cluster server");
|
|
23933
|
+
}
|
|
23934
|
+
const userEntry = named(doc["users"], String(ctx["user"] ?? ""));
|
|
23935
|
+
const user = userEntry?.user ?? {};
|
|
23936
|
+
const transport = {
|
|
23937
|
+
server: cluster["server"],
|
|
23938
|
+
insecureSkipTlsVerify: cluster["insecure-skip-tls-verify"] === true
|
|
23939
|
+
};
|
|
23940
|
+
const ca = pemFrom(cluster["certificate-authority-data"], cluster["certificate-authority"]);
|
|
23941
|
+
if (ca) transport.ca = ca;
|
|
23942
|
+
if (typeof user["token"] === "string" && user["token"].length > 0) transport.token = user["token"];
|
|
23943
|
+
const clientCert = pemFrom(user["client-certificate-data"], user["client-certificate"]);
|
|
23944
|
+
const clientKey = pemFrom(user["client-key-data"], user["client-key"]);
|
|
23945
|
+
if (clientCert) transport.clientCert = clientCert;
|
|
23946
|
+
if (clientKey) transport.clientKey = clientKey;
|
|
23947
|
+
return transport;
|
|
23948
|
+
}
|
|
23949
|
+
function resolveK8sTransport(creds, config) {
|
|
23950
|
+
if (creds.kubeconfig) return parseKubeconfig(creds.kubeconfig);
|
|
23951
|
+
if (!config.apiServerUrl) {
|
|
23952
|
+
throw new Error("kubernetes connector: options.apiServerUrl is required with a token credential");
|
|
23953
|
+
}
|
|
23954
|
+
const transport = {
|
|
23955
|
+
server: config.apiServerUrl,
|
|
23956
|
+
insecureSkipTlsVerify: config.insecureSkipTlsVerify === true
|
|
23957
|
+
};
|
|
23958
|
+
if (creds.token) transport.token = creds.token;
|
|
23959
|
+
if (config.caCert) transport.ca = config.caCert;
|
|
23960
|
+
return transport;
|
|
23961
|
+
}
|
|
23675
23962
|
|
|
23676
23963
|
// src/connectors/kubernetes/map.ts
|
|
23677
23964
|
init_cjs_shims();
|
|
23678
23965
|
|
|
23679
23966
|
// src/connectors/kubernetes/types.ts
|
|
23680
23967
|
init_cjs_shims();
|
|
23968
|
+
function readK8sCredentials(raw) {
|
|
23969
|
+
const token = typeof raw["token"] === "string" && raw["token"].length > 0 ? raw["token"] : void 0;
|
|
23970
|
+
const kubeconfig = typeof raw["kubeconfig"] === "string" && raw["kubeconfig"].length > 0 ? raw["kubeconfig"] : void 0;
|
|
23971
|
+
if (!token && !kubeconfig) {
|
|
23972
|
+
throw new Error("kubernetes connector: credentials must carry a token or a kubeconfig");
|
|
23973
|
+
}
|
|
23974
|
+
const out = {};
|
|
23975
|
+
if (token) out.token = token;
|
|
23976
|
+
if (kubeconfig) out.kubeconfig = kubeconfig;
|
|
23977
|
+
return out;
|
|
23978
|
+
}
|
|
23979
|
+
var IMAGE_PULL_REASONS = /* @__PURE__ */ new Set(["ImagePullBackOff", "ErrImagePull", "InvalidImageName"]);
|
|
23980
|
+
var CRASH_LOOP_REASON = "CrashLoopBackOff";
|
|
23981
|
+
var FIELD_SEP5 = "\0";
|
|
23982
|
+
var K8S_TARGET_KIND = "k8s-workload";
|
|
23983
|
+
var K8S_DEPLOY_STATE = "deploy-state";
|
|
23984
|
+
function packK8sTargetName(identity) {
|
|
23985
|
+
return [identity.serviceName, identity.fault].join(FIELD_SEP5);
|
|
23986
|
+
}
|
|
23987
|
+
function parseK8sTargetName(targetName) {
|
|
23988
|
+
const sep = targetName.indexOf(FIELD_SEP5);
|
|
23989
|
+
if (sep === -1) return null;
|
|
23990
|
+
const serviceName = targetName.slice(0, sep);
|
|
23991
|
+
const fault = targetName.slice(sep + 1);
|
|
23992
|
+
if (!serviceName || !fault) return null;
|
|
23993
|
+
return { serviceName, fault };
|
|
23994
|
+
}
|
|
23995
|
+
|
|
23996
|
+
// src/connectors/kubernetes/map.ts
|
|
23997
|
+
function serviceNameFor(deployment, config) {
|
|
23998
|
+
const name = deployment.metadata?.name ?? "";
|
|
23999
|
+
return config.serviceMap?.[name] ?? name;
|
|
24000
|
+
}
|
|
24001
|
+
function podMatchesSelector(pod, selector) {
|
|
24002
|
+
if (!selector || Object.keys(selector).length === 0) return false;
|
|
24003
|
+
const labels = pod.metadata?.labels ?? {};
|
|
24004
|
+
return Object.entries(selector).every(([k, v]) => labels[k] === v);
|
|
24005
|
+
}
|
|
24006
|
+
function podsForDeployment(deployment, pods) {
|
|
24007
|
+
const selector = deployment.spec?.selector?.matchLabels;
|
|
24008
|
+
return pods.filter((p) => podMatchesSelector(p, selector));
|
|
24009
|
+
}
|
|
24010
|
+
function nowIso2() {
|
|
24011
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
24012
|
+
}
|
|
24013
|
+
function podLevelFault(deployment, pods) {
|
|
24014
|
+
const name = deployment.metadata?.name ?? "";
|
|
24015
|
+
const owned = podsForDeployment(deployment, pods);
|
|
24016
|
+
let crash = null;
|
|
24017
|
+
for (const pod of owned) {
|
|
24018
|
+
for (const cs of pod.status?.containerStatuses ?? []) {
|
|
24019
|
+
const reason = cs.state?.waiting?.reason;
|
|
24020
|
+
if (typeof reason !== "string") continue;
|
|
24021
|
+
if (IMAGE_PULL_REASONS.has(reason)) {
|
|
24022
|
+
const image = typeof cs.image === "string" ? cs.image : "unknown image";
|
|
24023
|
+
const attrs = { "k8s.image": image, "k8s.waitingReason": reason };
|
|
24024
|
+
const wm = cs.state?.waiting?.message;
|
|
24025
|
+
if (typeof wm === "string" && wm.length > 0) attrs["k8s.waitingMessage"] = wm;
|
|
24026
|
+
return {
|
|
24027
|
+
fault: "image-pull",
|
|
24028
|
+
message: `Deployment ${name} cannot pull image ${image} (${reason})`,
|
|
24029
|
+
timestamp: pod.status?.startTime ?? nowIso2(),
|
|
24030
|
+
attributes: attrs
|
|
24031
|
+
};
|
|
24032
|
+
}
|
|
24033
|
+
if (reason === CRASH_LOOP_REASON && !crash) crash = { cs, pod };
|
|
24034
|
+
}
|
|
24035
|
+
}
|
|
24036
|
+
if (crash) {
|
|
24037
|
+
const { cs, pod } = crash;
|
|
24038
|
+
const term = cs.lastState?.terminated;
|
|
24039
|
+
const termReason = typeof term?.reason === "string" ? term.reason : void 0;
|
|
24040
|
+
const termMsg = typeof term?.message === "string" ? term.message.trim() : void 0;
|
|
24041
|
+
const restarts = typeof cs.restartCount === "number" ? cs.restartCount : 0;
|
|
24042
|
+
const detail = termReason ? `last terminated: ${termReason}${termMsg ? ` \u2014 ${termMsg}` : ""}${typeof term?.exitCode === "number" ? ` (exit ${term.exitCode})` : ""}` : "no last-termination detail reported";
|
|
24043
|
+
const attrs = { "k8s.waitingReason": CRASH_LOOP_REASON, "k8s.restartCount": restarts };
|
|
24044
|
+
if (termReason) attrs["k8s.terminatedReason"] = termReason;
|
|
24045
|
+
if (termMsg) attrs["k8s.terminatedMessage"] = termMsg;
|
|
24046
|
+
if (typeof term?.exitCode === "number") attrs["k8s.exitCode"] = term.exitCode;
|
|
24047
|
+
if (typeof cs.image === "string") attrs["k8s.image"] = cs.image;
|
|
24048
|
+
return {
|
|
24049
|
+
fault: "crash-loop",
|
|
24050
|
+
message: `Deployment ${name} is crashlooping (restarts: ${restarts}); ${detail}`,
|
|
24051
|
+
timestamp: term?.finishedAt ?? pod.status?.startTime ?? nowIso2(),
|
|
24052
|
+
attributes: attrs
|
|
24053
|
+
};
|
|
24054
|
+
}
|
|
24055
|
+
return null;
|
|
24056
|
+
}
|
|
24057
|
+
function classifyDeployment(deployment, pods, expectedZero) {
|
|
24058
|
+
const name = deployment.metadata?.name ?? "";
|
|
24059
|
+
const desired = typeof deployment.spec?.replicas === "number" ? deployment.spec.replicas : 1;
|
|
24060
|
+
const ready = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
|
|
24061
|
+
if (desired === 0) {
|
|
24062
|
+
if (expectedZero?.has(name)) return null;
|
|
24063
|
+
return {
|
|
24064
|
+
fault: "scaled-to-zero",
|
|
24065
|
+
message: `Deployment ${name} is scaled to 0 \u2014 no running pods (desired 0)`,
|
|
24066
|
+
timestamp: nowIso2(),
|
|
24067
|
+
attributes: { "k8s.desiredReplicas": 0, "k8s.readyReplicas": ready }
|
|
24068
|
+
};
|
|
24069
|
+
}
|
|
24070
|
+
if (ready >= desired) return null;
|
|
24071
|
+
const podFault = podLevelFault(deployment, pods);
|
|
24072
|
+
if (podFault) {
|
|
24073
|
+
podFault.attributes["k8s.desiredReplicas"] = desired;
|
|
24074
|
+
podFault.attributes["k8s.readyReplicas"] = ready;
|
|
24075
|
+
return podFault;
|
|
24076
|
+
}
|
|
24077
|
+
return {
|
|
24078
|
+
fault: "no-ready-replicas",
|
|
24079
|
+
message: `Deployment ${name} has no ready replicas (desired ${desired}, ready ${ready})`,
|
|
24080
|
+
timestamp: nowIso2(),
|
|
24081
|
+
attributes: { "k8s.desiredReplicas": desired, "k8s.readyReplicas": ready }
|
|
24082
|
+
};
|
|
24083
|
+
}
|
|
24084
|
+
function mapDeploymentToSignal(deployment, pods, config) {
|
|
24085
|
+
const name = deployment.metadata?.name;
|
|
24086
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
24087
|
+
const expectedZero = config.expectedZero ? new Set(config.expectedZero) : void 0;
|
|
24088
|
+
const finding = classifyDeployment(deployment, pods, expectedZero);
|
|
24089
|
+
if (!finding) return null;
|
|
24090
|
+
const serviceName = serviceNameFor(deployment, config);
|
|
24091
|
+
const namespace = deployment.metadata?.namespace ?? config.namespace;
|
|
24092
|
+
const attributes = {
|
|
24093
|
+
"k8s.namespace": namespace,
|
|
24094
|
+
"k8s.deployment": name,
|
|
24095
|
+
"k8s.fault": finding.fault,
|
|
24096
|
+
...finding.attributes
|
|
24097
|
+
};
|
|
24098
|
+
return {
|
|
24099
|
+
targetKind: K8S_TARGET_KIND,
|
|
24100
|
+
targetName: packK8sTargetName({ serviceName, fault: finding.fault }),
|
|
24101
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
24102
|
+
callCount: 0,
|
|
24103
|
+
errorCount: 0,
|
|
24104
|
+
lastObservedIso: finding.timestamp,
|
|
24105
|
+
incident: {
|
|
24106
|
+
id: `k8s:deploy:${namespace}:${name}:${finding.fault}`,
|
|
24107
|
+
timestamp: finding.timestamp,
|
|
24108
|
+
service: serviceName,
|
|
24109
|
+
errorType: "k8s-deploy-failure",
|
|
24110
|
+
errorMessage: finding.message,
|
|
24111
|
+
attributes
|
|
24112
|
+
}
|
|
24113
|
+
};
|
|
24114
|
+
}
|
|
24115
|
+
function observedDeployState(deployment, pods) {
|
|
24116
|
+
const readyReplicas = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
|
|
24117
|
+
let image;
|
|
24118
|
+
for (const pod of podsForDeployment(deployment, pods)) {
|
|
24119
|
+
for (const cs of pod.status?.containerStatuses ?? []) {
|
|
24120
|
+
if (cs.state?.running && typeof cs.image === "string" && cs.image.length > 0) {
|
|
24121
|
+
image = cs.image;
|
|
24122
|
+
break;
|
|
24123
|
+
}
|
|
24124
|
+
}
|
|
24125
|
+
if (image) break;
|
|
24126
|
+
}
|
|
24127
|
+
return image !== void 0 ? { image, readyReplicas } : { readyReplicas };
|
|
24128
|
+
}
|
|
24129
|
+
function deployStateSignal(deployment, pods, config) {
|
|
24130
|
+
const name = deployment.metadata?.name;
|
|
24131
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
24132
|
+
const serviceName = serviceNameFor(deployment, config);
|
|
24133
|
+
return {
|
|
24134
|
+
targetKind: K8S_TARGET_KIND,
|
|
24135
|
+
targetName: packK8sTargetName({ serviceName, fault: K8S_DEPLOY_STATE }),
|
|
24136
|
+
callCount: 0,
|
|
24137
|
+
errorCount: 0,
|
|
24138
|
+
lastObservedIso: nowIso2(),
|
|
24139
|
+
deployState: observedDeployState(deployment, pods)
|
|
24140
|
+
};
|
|
24141
|
+
}
|
|
24142
|
+
function mapWorkloadsToSignals(deployments, pods, config) {
|
|
24143
|
+
const out = [];
|
|
24144
|
+
for (const deployment of deployments) {
|
|
24145
|
+
const deployState = deployStateSignal(deployment, pods, config);
|
|
24146
|
+
if (deployState) out.push(deployState);
|
|
24147
|
+
const incident = mapDeploymentToSignal(deployment, pods, config);
|
|
24148
|
+
if (incident) out.push(incident);
|
|
24149
|
+
}
|
|
24150
|
+
return out;
|
|
24151
|
+
}
|
|
23681
24152
|
|
|
23682
24153
|
// src/connectors/kubernetes/resolve.ts
|
|
23683
24154
|
init_cjs_shims();
|
|
23684
|
-
var
|
|
24155
|
+
var import_types104 = require("@neat.is/types");
|
|
24156
|
+
var NO_ENV3 = "unknown";
|
|
24157
|
+
function createK8sResolveTarget(graph) {
|
|
24158
|
+
return (signal) => {
|
|
24159
|
+
if (signal.targetKind !== K8S_TARGET_KIND) return null;
|
|
24160
|
+
const identity = parseK8sTargetName(signal.targetName);
|
|
24161
|
+
if (!identity) return null;
|
|
24162
|
+
const { serviceName } = identity;
|
|
24163
|
+
return {
|
|
24164
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV3),
|
|
24165
|
+
serviceName,
|
|
24166
|
+
edgeType: import_types104.EdgeType.CALLS
|
|
24167
|
+
};
|
|
24168
|
+
};
|
|
24169
|
+
}
|
|
23685
24170
|
|
|
23686
24171
|
// src/connectors/kubernetes/substrate.ts
|
|
23687
24172
|
init_cjs_shims();
|
|
23688
24173
|
var import_promises = require("fs/promises");
|
|
23689
24174
|
var import_node_os5 = __toESM(require("os"), 1);
|
|
23690
24175
|
var import_node_path77 = __toESM(require("path"), 1);
|
|
24176
|
+
function defaultHome() {
|
|
24177
|
+
const override = process.env.NEAT_HOME;
|
|
24178
|
+
if (override && override.length > 0) return import_node_path77.default.resolve(override);
|
|
24179
|
+
return import_node_path77.default.join(import_node_os5.default.homedir(), ".neat");
|
|
24180
|
+
}
|
|
24181
|
+
function k8sSubstrateConfigPath(home = defaultHome()) {
|
|
24182
|
+
return import_node_path77.default.join(home, "k8s.json");
|
|
24183
|
+
}
|
|
24184
|
+
async function readK8sSubstrateConfig(home = defaultHome()) {
|
|
24185
|
+
let raw;
|
|
24186
|
+
try {
|
|
24187
|
+
raw = await (0, import_promises.readFile)(k8sSubstrateConfigPath(home), "utf8");
|
|
24188
|
+
} catch {
|
|
24189
|
+
return { version: 1, deployments: [] };
|
|
24190
|
+
}
|
|
24191
|
+
try {
|
|
24192
|
+
const parsed = JSON.parse(raw);
|
|
24193
|
+
if (!parsed || !Array.isArray(parsed.deployments)) return { version: 1, deployments: [] };
|
|
24194
|
+
return parsed;
|
|
24195
|
+
} catch {
|
|
24196
|
+
return { version: 1, deployments: [] };
|
|
24197
|
+
}
|
|
24198
|
+
}
|
|
24199
|
+
async function startK8sSubstratePolling(input) {
|
|
24200
|
+
const config = await readK8sSubstrateConfig(input.home);
|
|
24201
|
+
const env = input.env ?? process.env;
|
|
24202
|
+
const stops = [];
|
|
24203
|
+
for (const entry2 of config.deployments) {
|
|
24204
|
+
if (entry2.project !== void 0 && entry2.project !== input.project) continue;
|
|
24205
|
+
if (typeof entry2.namespace !== "string" || entry2.namespace.length === 0) {
|
|
24206
|
+
input.onSkip?.(entry2, "missing namespace");
|
|
24207
|
+
continue;
|
|
24208
|
+
}
|
|
24209
|
+
let credentials;
|
|
24210
|
+
try {
|
|
24211
|
+
const resolved = resolveCredential(entry2.credential, env);
|
|
24212
|
+
credentials = resolved.kind === "fields" ? { ...resolved.fields } : { token: resolved.value };
|
|
24213
|
+
} catch (err) {
|
|
24214
|
+
input.onSkip?.(entry2, err.message);
|
|
24215
|
+
continue;
|
|
24216
|
+
}
|
|
24217
|
+
const cfg = {
|
|
24218
|
+
namespace: entry2.namespace,
|
|
24219
|
+
...entry2.apiServerUrl ? { apiServerUrl: entry2.apiServerUrl } : {},
|
|
24220
|
+
...entry2.caCert ? { caCert: entry2.caCert } : {},
|
|
24221
|
+
...entry2.insecureSkipTlsVerify ? { insecureSkipTlsVerify: true } : {},
|
|
24222
|
+
...entry2.serviceMap ? { serviceMap: entry2.serviceMap } : {},
|
|
24223
|
+
...entry2.expectedZero ? { expectedZero: entry2.expectedZero } : {}
|
|
24224
|
+
};
|
|
24225
|
+
const { connector, resolveTarget } = createKubernetesConnector(input.graph, cfg, input.fetchImpl);
|
|
24226
|
+
const stop = startConnectorPollLoop(
|
|
24227
|
+
connector,
|
|
24228
|
+
{
|
|
24229
|
+
projectDir: input.projectDir,
|
|
24230
|
+
project: input.project,
|
|
24231
|
+
credentials,
|
|
24232
|
+
...input.errorsPath ? { errorsPath: input.errorsPath } : {}
|
|
24233
|
+
},
|
|
24234
|
+
input.graph,
|
|
24235
|
+
resolveTarget,
|
|
24236
|
+
{ connectorId: `k8s:${entry2.id}`, ...entry2.intervalMs ? { intervalMs: entry2.intervalMs } : {} }
|
|
24237
|
+
);
|
|
24238
|
+
stops.push(stop);
|
|
24239
|
+
}
|
|
24240
|
+
return () => {
|
|
24241
|
+
for (const stop of stops) stop();
|
|
24242
|
+
};
|
|
24243
|
+
}
|
|
24244
|
+
|
|
24245
|
+
// src/connectors/kubernetes/index.ts
|
|
24246
|
+
var KubernetesConnector = class {
|
|
24247
|
+
constructor(config, fetchImpl) {
|
|
24248
|
+
this.config = config;
|
|
24249
|
+
this.fetchImpl = fetchImpl;
|
|
24250
|
+
}
|
|
24251
|
+
config;
|
|
24252
|
+
fetchImpl;
|
|
24253
|
+
provider = "kubernetes";
|
|
24254
|
+
async poll(ctx) {
|
|
24255
|
+
const creds = readK8sCredentials(ctx.credentials);
|
|
24256
|
+
const transport = resolveK8sTransport(creds, this.config);
|
|
24257
|
+
const namespace = this.config.namespace;
|
|
24258
|
+
const opts = {
|
|
24259
|
+
...this.config.apiUrl ? { apiUrl: this.config.apiUrl } : {},
|
|
24260
|
+
...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
|
|
24261
|
+
};
|
|
24262
|
+
const [deployments, pods] = await Promise.all([
|
|
24263
|
+
fetchDeployments(transport, namespace, opts),
|
|
24264
|
+
fetchPods(transport, namespace, opts)
|
|
24265
|
+
]);
|
|
24266
|
+
return mapWorkloadsToSignals(deployments, pods, this.config);
|
|
24267
|
+
}
|
|
24268
|
+
};
|
|
24269
|
+
function createKubernetesConnector(graph, config, fetchImpl) {
|
|
24270
|
+
return {
|
|
24271
|
+
connector: new KubernetesConnector(config, fetchImpl),
|
|
24272
|
+
resolveTarget: createK8sResolveTarget(graph)
|
|
24273
|
+
};
|
|
24274
|
+
}
|
|
23691
24275
|
|
|
23692
24276
|
// src/daemon.ts
|
|
24277
|
+
init_cjs_shims();
|
|
24278
|
+
var import_node_fs43 = require("fs");
|
|
24279
|
+
var import_node_path79 = __toESM(require("path"), 1);
|
|
24280
|
+
var import_node_module = require("module");
|
|
24281
|
+
init_otel();
|
|
23693
24282
|
init_auth();
|
|
23694
24283
|
|
|
23695
24284
|
// src/unrouted.ts
|
|
@@ -23698,7 +24287,7 @@ var import_node_fs42 = require("fs");
|
|
|
23698
24287
|
var import_node_path78 = __toESM(require("path"), 1);
|
|
23699
24288
|
|
|
23700
24289
|
// src/daemon.ts
|
|
23701
|
-
var
|
|
24290
|
+
var import_types107 = require("@neat.is/types");
|
|
23702
24291
|
function daemonJsonPath(scanPath) {
|
|
23703
24292
|
return import_node_path79.default.join(scanPath, "neat-out", "daemon.json");
|
|
23704
24293
|
}
|
|
@@ -24350,7 +24939,10 @@ async function startWatch(graph, opts) {
|
|
|
24350
24939
|
const stopStaleness = startStalenessLoop(graph, {
|
|
24351
24940
|
staleEventsPath: opts.staleEventsPath,
|
|
24352
24941
|
project: projectName,
|
|
24353
|
-
onPolicyTrigger
|
|
24942
|
+
onPolicyTrigger,
|
|
24943
|
+
// Graduate FRONTIER surfaces whose twin arrived + stage surfaces for current
|
|
24944
|
+
// hangs (ADR-226), each post-ingest tick.
|
|
24945
|
+
onReconcile: (g) => reconcileFrontierSurfaces(g, opts.errorsPath)
|
|
24354
24946
|
});
|
|
24355
24947
|
const stopConnectors = await startConnectorPolling({
|
|
24356
24948
|
project: projectName,
|
|
@@ -24364,6 +24956,14 @@ async function startWatch(graph, opts) {
|
|
|
24364
24956
|
`neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
|
|
24365
24957
|
)
|
|
24366
24958
|
});
|
|
24959
|
+
const stopK8sSubstrate = await startK8sSubstratePolling({
|
|
24960
|
+
project: projectName,
|
|
24961
|
+
graph,
|
|
24962
|
+
projectDir: opts.scanPath,
|
|
24963
|
+
errorsPath: opts.errorsPath,
|
|
24964
|
+
...opts.neatHome ? { home: opts.neatHome } : {},
|
|
24965
|
+
onSkip: (skipped, reason) => console.warn(`neat watch: k8s substrate "${skipped.id}" skipped for project "${projectName}" \u2014 ${reason}`)
|
|
24966
|
+
});
|
|
24367
24967
|
const auth = readAuthEnv();
|
|
24368
24968
|
const host = opts.host ?? (auth.authToken ? "0.0.0.0" : "127.0.0.1");
|
|
24369
24969
|
assertBindAuthority(host, auth.authToken);
|
|
@@ -24550,6 +25150,7 @@ async function startWatch(graph, opts) {
|
|
|
24550
25150
|
}
|
|
24551
25151
|
await watcher.close();
|
|
24552
25152
|
stopConnectors();
|
|
25153
|
+
stopK8sSubstrate();
|
|
24553
25154
|
stopStaleness();
|
|
24554
25155
|
stopPersist();
|
|
24555
25156
|
detachEventBus();
|
|
@@ -28008,8 +28609,8 @@ async function waitForPeerDaemon(restPort, project, timeoutMs) {
|
|
|
28008
28609
|
async function healthIsForProject(restPort, project) {
|
|
28009
28610
|
const body = await fetchDaemonHealth(restPort);
|
|
28010
28611
|
if (body === null) return false;
|
|
28011
|
-
const
|
|
28012
|
-
if (typeof
|
|
28612
|
+
const named2 = body.project;
|
|
28613
|
+
if (typeof named2 === "string") return named2 === project;
|
|
28013
28614
|
if (Array.isArray(body.projects)) {
|
|
28014
28615
|
return body.projects.some((p) => p.name === project);
|
|
28015
28616
|
}
|
|
@@ -28715,7 +29316,7 @@ var import_node_path89 = __toESM(require("path"), 1);
|
|
|
28715
29316
|
|
|
28716
29317
|
// src/cli-client.ts
|
|
28717
29318
|
init_cjs_shims();
|
|
28718
|
-
var
|
|
29319
|
+
var import_types108 = require("@neat.is/types");
|
|
28719
29320
|
var HttpError = class extends Error {
|
|
28720
29321
|
constructor(status2, message, responseBody = "") {
|
|
28721
29322
|
super(message);
|
|
@@ -28857,7 +29458,7 @@ async function runBlastRadius(client, input) {
|
|
|
28857
29458
|
}
|
|
28858
29459
|
}
|
|
28859
29460
|
function formatBlastEntry(n) {
|
|
28860
|
-
const tag = n.edgeProvenance ===
|
|
29461
|
+
const tag = n.edgeProvenance === import_types108.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
28861
29462
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
28862
29463
|
}
|
|
28863
29464
|
async function runDependencies(client, input) {
|
|
@@ -28914,7 +29515,7 @@ async function runObservedDependencies(client, input) {
|
|
|
28914
29515
|
if (result.observed) {
|
|
28915
29516
|
return {
|
|
28916
29517
|
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
28917
|
-
provenance:
|
|
29518
|
+
provenance: import_types108.Provenance.OBSERVED
|
|
28918
29519
|
};
|
|
28919
29520
|
}
|
|
28920
29521
|
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
@@ -28924,7 +29525,7 @@ async function runObservedDependencies(client, input) {
|
|
|
28924
29525
|
return {
|
|
28925
29526
|
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
28926
29527
|
block: blockLines.join("\n"),
|
|
28927
|
-
provenance:
|
|
29528
|
+
provenance: import_types108.Provenance.OBSERVED
|
|
28928
29529
|
};
|
|
28929
29530
|
} catch (err) {
|
|
28930
29531
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -28978,7 +29579,7 @@ async function runIncidents(client, input) {
|
|
|
28978
29579
|
return {
|
|
28979
29580
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
28980
29581
|
block: blockLines.join("\n"),
|
|
28981
|
-
provenance:
|
|
29582
|
+
provenance: import_types108.Provenance.OBSERVED
|
|
28982
29583
|
};
|
|
28983
29584
|
} catch (err) {
|
|
28984
29585
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -29087,7 +29688,7 @@ async function runStaleEdges(client, input) {
|
|
|
29087
29688
|
return {
|
|
29088
29689
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
29089
29690
|
block: blockLines.join("\n"),
|
|
29090
|
-
provenance:
|
|
29691
|
+
provenance: import_types108.Provenance.STALE
|
|
29091
29692
|
};
|
|
29092
29693
|
}
|
|
29093
29694
|
async function runPolicies(client, input) {
|
|
@@ -29178,6 +29779,12 @@ function formatDivergenceLine(d) {
|
|
|
29178
29779
|
const at = d.location ? ` at ${d.location}` : "";
|
|
29179
29780
|
return ` \u2022 [${d.type}] ${d.source}${at} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29180
29781
|
}
|
|
29782
|
+
case "deploy-mismatch": {
|
|
29783
|
+
if (d.kind === "image") {
|
|
29784
|
+
return ` \u2022 [${d.type}] ${d.source} \u2014 declared image ${d.declaredImage ?? "?"}, running ${d.observedImage ?? "?"} \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29785
|
+
}
|
|
29786
|
+
return ` \u2022 [${d.type}] ${d.source} \u2014 declared ${d.declaredReplicas ?? "?"} replicas, ${d.observedReplicas ?? "?"} ready \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
29787
|
+
}
|
|
29181
29788
|
}
|
|
29182
29789
|
}
|
|
29183
29790
|
async function runDivergences(client, input) {
|
|
@@ -30378,12 +30985,12 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
|
30378
30985
|
|
|
30379
30986
|
// src/monitor.ts
|
|
30380
30987
|
init_cjs_shims();
|
|
30381
|
-
var
|
|
30988
|
+
var import_types109 = require("@neat.is/types");
|
|
30382
30989
|
var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
30383
|
-
|
|
30384
|
-
|
|
30385
|
-
|
|
30386
|
-
|
|
30990
|
+
import_types109.EdgeType.CALLS,
|
|
30991
|
+
import_types109.EdgeType.CONNECTS_TO,
|
|
30992
|
+
import_types109.EdgeType.PUBLISHES_TO,
|
|
30993
|
+
import_types109.EdgeType.CONSUMES_FROM
|
|
30387
30994
|
]);
|
|
30388
30995
|
function divergenceKey(d) {
|
|
30389
30996
|
const column = "column" in d && d.column ? d.column : "";
|
|
@@ -30444,7 +31051,7 @@ function formatDivergenceLine2(d) {
|
|
|
30444
31051
|
}
|
|
30445
31052
|
}
|
|
30446
31053
|
function formatStaleLine(edgeId) {
|
|
30447
|
-
const parsed = (0,
|
|
31054
|
+
const parsed = (0, import_types109.parseEdgeId)(edgeId);
|
|
30448
31055
|
if (parsed) {
|
|
30449
31056
|
return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
|
|
30450
31057
|
}
|
|
@@ -30457,7 +31064,7 @@ function divergenceJson(d) {
|
|
|
30457
31064
|
return JSON.stringify({ kind: "divergence", ...d });
|
|
30458
31065
|
}
|
|
30459
31066
|
function staleJson(edgeId) {
|
|
30460
|
-
const parsed = (0,
|
|
31067
|
+
const parsed = (0, import_types109.parseEdgeId)(edgeId);
|
|
30461
31068
|
return JSON.stringify({
|
|
30462
31069
|
kind: "stale",
|
|
30463
31070
|
edgeId,
|
|
@@ -30540,7 +31147,7 @@ var MonitorEmitter = class {
|
|
|
30540
31147
|
// ignores non-OBSERVED edges and non-dependency edge types (structural
|
|
30541
31148
|
// ownership), so only real runtime dependencies reach stdout.
|
|
30542
31149
|
emitObservedEdge(edge) {
|
|
30543
|
-
if (edge.provenance !==
|
|
31150
|
+
if (edge.provenance !== import_types109.Provenance.OBSERVED) return false;
|
|
30544
31151
|
if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
|
|
30545
31152
|
const key = `edge|${edge.id}`;
|
|
30546
31153
|
if (this.seen.has(key)) return false;
|
|
@@ -30721,7 +31328,7 @@ async function runMonitor(opts) {
|
|
|
30721
31328
|
case "edge-added": {
|
|
30722
31329
|
const payload = safeParse(frame.data);
|
|
30723
31330
|
const edge = payload?.edge;
|
|
30724
|
-
if (edge && edge.provenance ===
|
|
31331
|
+
if (edge && edge.provenance === import_types109.Provenance.OBSERVED) {
|
|
30725
31332
|
emitter.emitObservedEdge(edge);
|
|
30726
31333
|
divergences.schedule();
|
|
30727
31334
|
}
|
|
@@ -30971,7 +31578,7 @@ async function runSync(opts) {
|
|
|
30971
31578
|
}
|
|
30972
31579
|
|
|
30973
31580
|
// src/cli.ts
|
|
30974
|
-
var
|
|
31581
|
+
var import_types110 = require("@neat.is/types");
|
|
30975
31582
|
function isNpxInvocation() {
|
|
30976
31583
|
if (process.env.npm_command === "exec") return true;
|
|
30977
31584
|
const execpath = process.env.npm_execpath ?? "";
|
|
@@ -32055,10 +32662,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
32055
32662
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
32056
32663
|
const out = [];
|
|
32057
32664
|
for (const p of parts) {
|
|
32058
|
-
const r =
|
|
32665
|
+
const r = import_types110.DivergenceTypeSchema.safeParse(p);
|
|
32059
32666
|
if (!r.success) {
|
|
32060
32667
|
console.error(
|
|
32061
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
32668
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types110.DivergenceTypeSchema.options.join(", ")}`
|
|
32062
32669
|
);
|
|
32063
32670
|
return 2;
|
|
32064
32671
|
}
|