@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/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 || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
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 || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
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 (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
1582
- return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
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 || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
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 structuralUpstreamPointer(graph, boundaryNode, incidents) {
2201
- const route = boundaryIncidentRoute(boundaryNode, incidents);
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
- if (!chosen) return null;
2207
- let node = chosen.target;
2208
- const seen = /* @__PURE__ */ new Set([boundaryNode, node]);
2209
- for (let depth = 0; depth < ROOT_CAUSE_MAX_DEPTH; depth++) {
2210
- const next = declaredOutboundCallees(graph, node);
2211
- if (next.length !== 1 || seen.has(next[0].target)) break;
2212
- node = next[0].target;
2213
- seen.add(node);
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 route !== void 0 ? { node, route } : { node };
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 routeNote = pointer?.route ? ` serving ${pointer.route}` : "";
2254
- if (pointer) {
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: pointer ? `${seedName} timed out waiting on a downstream that hung and exported nothing \u2014 a boundary reporting an upstream fault, not the fault itself.` : `${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 single declared callee${routeNote} resolves, so no confident cause is named; inspect ${seedName}'s declared downstream.`,
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 named = candidates.filter((c) => terminalName(c.symbol.qualname) === fn);
6049
- if (named.length > 0) return [...named].sort(bySpan)[0].id;
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 named = clause.namedChild(j);
9469
- if (named?.type !== "named_imports") continue;
9470
- for (let k = 0; k < named.namedChildCount; k++) {
9471
- const spec = named.namedChild(k);
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 named = /\{([^}]*)\}/.exec(clause);
12106
- if (named) parseDestructure(named[1], spec, out);
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 named = /(?:module\.exports|export\s+default)\s*=\s*(\w+)\b/.exec(content);
12127
- if (named && byVar.has(named[1])) def = byVar.get(named[1]);
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 named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
17960
- if (matched === 0 && !named) return;
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 = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
17963
- const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
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, nowIso2) {
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: nowIso2,
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 named = params.project;
22871
+ const named2 = params.project;
22755
22872
  if (singleProject) {
22756
- return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
22873
+ return named2 === void 0 || named2 === DEFAULT_PROJECT ? singleProject : named2;
22757
22874
  }
22758
- return named ?? DEFAULT_PROJECT;
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/daemon.ts
23773
+ // src/hang-sensor.ts
23658
23774
  init_cjs_shims();
23659
- var import_node_fs43 = require("fs");
23660
- var import_node_path79 = __toESM(require("path"), 1);
23661
- var import_node_module = require("module");
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 import_types103 = require("@neat.is/types");
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 import_types106 = require("@neat.is/types");
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 named = body.project;
28012
- if (typeof named === "string") return named === project;
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 import_types107 = require("@neat.is/types");
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 === import_types107.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
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: import_types107.Provenance.OBSERVED
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: import_types107.Provenance.OBSERVED
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: import_types107.Provenance.OBSERVED
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: import_types107.Provenance.STALE
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 import_types108 = require("@neat.is/types");
30988
+ var import_types109 = require("@neat.is/types");
30382
30989
  var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
30383
- import_types108.EdgeType.CALLS,
30384
- import_types108.EdgeType.CONNECTS_TO,
30385
- import_types108.EdgeType.PUBLISHES_TO,
30386
- import_types108.EdgeType.CONSUMES_FROM
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, import_types108.parseEdgeId)(edgeId);
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, import_types108.parseEdgeId)(edgeId);
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 !== import_types108.Provenance.OBSERVED) return false;
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 === import_types108.Provenance.OBSERVED) {
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 import_types109 = require("@neat.is/types");
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 = import_types109.DivergenceTypeSchema.safeParse(p);
32665
+ const r = import_types110.DivergenceTypeSchema.safeParse(p);
32059
32666
  if (!r.success) {
32060
32667
  console.error(
32061
- `neat divergences: unknown --type "${p}". allowed: ${import_types109.DivergenceTypeSchema.options.join(", ")}`
32668
+ `neat divergences: unknown --type "${p}". allowed: ${import_types110.DivergenceTypeSchema.options.join(", ")}`
32062
32669
  );
32063
32670
  return 2;
32064
32671
  }