@neat.is/core 0.9.13-dev.20260901 → 0.9.13

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/index.cjs CHANGED
@@ -1945,6 +1945,11 @@ function isSaturated(ctx) {
1945
1945
  function isBoundaryTimeoutSymptom(ctx) {
1946
1946
  return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
1947
1947
  }
1948
+ var UNREACHABLE_INBOUND_ERROR_RATE = 0.5;
1949
+ var UNREACHABLE_MIN_INBOUND = 3;
1950
+ function isUnreachableSeed(ctx) {
1951
+ return ctx.callCount >= UNREACHABLE_MIN_INBOUND && ctx.errorsFromCallers > 0 && ctx.errorsFromCallers >= UNREACHABLE_INBOUND_ERROR_RATE * ctx.callCount && ctx.errorsEmittedHere === 0 && ctx.outboundVolume === 0;
1952
+ }
1948
1953
  function classifyNode(ctx) {
1949
1954
  if (ctx.errorsEmittedHere > 0) {
1950
1955
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
@@ -1953,6 +1958,7 @@ function classifyNode(ctx) {
1953
1958
  if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
1954
1959
  return "primary-failure";
1955
1960
  }
1961
+ if (isUnreachableSeed(ctx)) return "unreachable";
1956
1962
  if (ctx.errorsFromCallers > 0) return "symptom-only";
1957
1963
  return "unrelated";
1958
1964
  }
@@ -2239,6 +2245,16 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2239
2245
  confidence: Math.min(legacy.confidence, 0.4),
2240
2246
  ...lastProv ? { provenance: lastProv } : {}
2241
2247
  });
2248
+ } else if (seedCtx && isUnreachableSeed(seedCtx)) {
2249
+ const name = displayNameOf(seedNode);
2250
+ candidates.push({
2251
+ node: seedNode,
2252
+ classification: "unreachable",
2253
+ 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.`,
2254
+ context: seedCtx,
2255
+ confidence: legacy.confidence,
2256
+ provenance: import_types.Provenance.OBSERVED
2257
+ });
2242
2258
  } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
2243
2259
  const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
2244
2260
  const seedName = displayNameOf(seedNode);
@@ -6286,6 +6302,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
6286
6302
  function mergeObservedColumns(graph, tableNodeId, columns) {
6287
6303
  mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
6288
6304
  }
6305
+ function mergeObservedDeployState(graph, serviceNodeId, state) {
6306
+ if (!graph.hasNode(serviceNodeId)) return;
6307
+ const attrs = {};
6308
+ if (typeof state.image === "string" && state.image.length > 0) attrs["observedImage"] = state.image;
6309
+ if (typeof state.readyReplicas === "number") attrs["observedReadyReplicas"] = state.readyReplicas;
6310
+ if (Object.keys(attrs).length === 0) return;
6311
+ graph.mergeNodeAttributes(serviceNodeId, attrs);
6312
+ }
6289
6313
  function ensureDatabaseNode(graph, host, engine) {
6290
6314
  const id = (0, import_types8.databaseId)(host);
6291
6315
  if (graph.hasNode(id)) return id;
@@ -16956,6 +16980,25 @@ function detectColumnDrift(node) {
16956
16980
  }
16957
16981
  return out;
16958
16982
  }
16983
+ 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.";
16984
+ var DEPLOY_DIVERGENCE_CONFIDENCE = 0.9;
16985
+ function detectDeployDivergence(svc) {
16986
+ const out = [];
16987
+ if (typeof svc.declaredImage === "string" && typeof svc.observedImage === "string" && svc.declaredImage !== svc.observedImage) {
16988
+ out.push({
16989
+ type: "deploy-mismatch",
16990
+ kind: "image",
16991
+ source: svc.id,
16992
+ target: svc.id,
16993
+ declaredImage: svc.declaredImage,
16994
+ observedImage: svc.observedImage,
16995
+ confidence: DEPLOY_DIVERGENCE_CONFIDENCE,
16996
+ 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.`,
16997
+ recommendation: RECOMMENDATION_DEPLOY_IMAGE_MISMATCH
16998
+ });
16999
+ }
17000
+ return out;
17001
+ }
16959
17002
  var SYMBOL_MISMATCH_PATTERNS = [
16960
17003
  {
16961
17004
  // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
@@ -17283,6 +17326,7 @@ function computeDivergences(graph, opts = {}) {
17283
17326
  const svc = n;
17284
17327
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
17285
17328
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
17329
+ for (const d of detectDeployDivergence(svc)) all.push(d);
17286
17330
  return;
17287
17331
  }
17288
17332
  if (n.type === import_types60.NodeType.InfraNode && n.kind === "sql-table") {
@@ -17323,7 +17367,12 @@ function computeDivergences(graph, opts = {}) {
17323
17367
  // orders last so at equal confidence the structural and symbol divergences
17324
17368
  // lead, per the contract ("rank below the definitive structural and symbol
17325
17369
  // divergences").
17326
- "observed-failing": 6
17370
+ "observed-failing": 6,
17371
+ // Deploy mismatch (ADR-225) is a definitive structural declared-vs-observed
17372
+ // divergence carrying high confidence (0.9), so the confidence sort already
17373
+ // places it among the structural leaders; this slot only breaks an exact
17374
+ // confidence tie and sits last as a stable tiebreaker.
17375
+ "deploy-mismatch": 7
17327
17376
  };
17328
17377
  filtered.sort((a, b) => {
17329
17378
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -18973,6 +19022,11 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
18973
19022
  unresolved++;
18974
19023
  continue;
18975
19024
  }
19025
+ if (signal.deployState) {
19026
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
19027
+ mergeObservedDeployState(graph, resolved.targetNodeId, signal.deployState);
19028
+ continue;
19029
+ }
18976
19030
  if (signal.incident) {
18977
19031
  ensureServiceNode(graph, resolved.serviceName, NO_ENV);
18978
19032
  if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
@@ -23432,6 +23486,7 @@ var IMAGE_PULL_REASONS = /* @__PURE__ */ new Set(["ImagePullBackOff", "ErrImageP
23432
23486
  var CRASH_LOOP_REASON = "CrashLoopBackOff";
23433
23487
  var FIELD_SEP5 = "\0";
23434
23488
  var K8S_TARGET_KIND = "k8s-workload";
23489
+ var K8S_DEPLOY_STATE = "deploy-state";
23435
23490
  function packK8sTargetName(identity) {
23436
23491
  return [identity.serviceName, identity.fault].join(FIELD_SEP5);
23437
23492
  }
@@ -23563,11 +23618,40 @@ function mapDeploymentToSignal(deployment, pods, config) {
23563
23618
  }
23564
23619
  };
23565
23620
  }
23621
+ function observedDeployState(deployment, pods) {
23622
+ const readyReplicas = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
23623
+ let image;
23624
+ for (const pod of podsForDeployment(deployment, pods)) {
23625
+ for (const cs of pod.status?.containerStatuses ?? []) {
23626
+ if (cs.state?.running && typeof cs.image === "string" && cs.image.length > 0) {
23627
+ image = cs.image;
23628
+ break;
23629
+ }
23630
+ }
23631
+ if (image) break;
23632
+ }
23633
+ return image !== void 0 ? { image, readyReplicas } : { readyReplicas };
23634
+ }
23635
+ function deployStateSignal(deployment, pods, config) {
23636
+ const name = deployment.metadata?.name;
23637
+ if (typeof name !== "string" || name.length === 0) return null;
23638
+ const serviceName = serviceNameFor(deployment, config);
23639
+ return {
23640
+ targetKind: K8S_TARGET_KIND,
23641
+ targetName: packK8sTargetName({ serviceName, fault: K8S_DEPLOY_STATE }),
23642
+ callCount: 0,
23643
+ errorCount: 0,
23644
+ lastObservedIso: nowIso2(),
23645
+ deployState: observedDeployState(deployment, pods)
23646
+ };
23647
+ }
23566
23648
  function mapWorkloadsToSignals(deployments, pods, config) {
23567
23649
  const out = [];
23568
23650
  for (const deployment of deployments) {
23569
- const signal = mapDeploymentToSignal(deployment, pods, config);
23570
- if (signal) out.push(signal);
23651
+ const deployState = deployStateSignal(deployment, pods, config);
23652
+ if (deployState) out.push(deployState);
23653
+ const incident = mapDeploymentToSignal(deployment, pods, config);
23654
+ if (incident) out.push(incident);
23571
23655
  }
23572
23656
  return out;
23573
23657
  }