@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.d.cts CHANGED
@@ -183,6 +183,10 @@ interface ObservedSignal {
183
183
  callSite?: ConnectorCallSite;
184
184
  columns?: string[];
185
185
  incident?: ConnectorIncident;
186
+ deployState?: {
187
+ image?: string;
188
+ readyReplicas?: number;
189
+ };
186
190
  }
187
191
 
188
192
  /**
package/dist/index.d.ts CHANGED
@@ -183,6 +183,10 @@ interface ObservedSignal {
183
183
  callSite?: ConnectorCallSite;
184
184
  columns?: string[];
185
185
  incident?: ConnectorIncident;
186
+ deployState?: {
187
+ image?: string;
188
+ readyReplicas?: number;
189
+ };
186
190
  }
187
191
 
188
192
  /**
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-WE3AFQYL.js";
4
+ } from "./chunk-BKSM2YLV.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-RBZNXA5L.js";
40
+ } from "./chunk-RL6H3VVD.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-ERE47MCR.js";
package/dist/neatd.cjs CHANGED
@@ -1908,6 +1908,11 @@ function isSaturated(ctx) {
1908
1908
  function isBoundaryTimeoutSymptom(ctx) {
1909
1909
  return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
1910
1910
  }
1911
+ var UNREACHABLE_INBOUND_ERROR_RATE = 0.5;
1912
+ var UNREACHABLE_MIN_INBOUND = 3;
1913
+ function isUnreachableSeed(ctx) {
1914
+ return ctx.callCount >= UNREACHABLE_MIN_INBOUND && ctx.errorsFromCallers > 0 && ctx.errorsFromCallers >= UNREACHABLE_INBOUND_ERROR_RATE * ctx.callCount && ctx.errorsEmittedHere === 0 && ctx.outboundVolume === 0;
1915
+ }
1911
1916
  function classifyNode(ctx) {
1912
1917
  if (ctx.errorsEmittedHere > 0) {
1913
1918
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
@@ -1916,6 +1921,7 @@ function classifyNode(ctx) {
1916
1921
  if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
1917
1922
  return "primary-failure";
1918
1923
  }
1924
+ if (isUnreachableSeed(ctx)) return "unreachable";
1919
1925
  if (ctx.errorsFromCallers > 0) return "symptom-only";
1920
1926
  return "unrelated";
1921
1927
  }
@@ -2202,6 +2208,16 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2202
2208
  confidence: Math.min(legacy.confidence, 0.4),
2203
2209
  ...lastProv ? { provenance: lastProv } : {}
2204
2210
  });
2211
+ } else if (seedCtx && isUnreachableSeed(seedCtx)) {
2212
+ const name = displayNameOf(seedNode);
2213
+ candidates.push({
2214
+ node: seedNode,
2215
+ classification: "unreachable",
2216
+ 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.`,
2217
+ context: seedCtx,
2218
+ confidence: legacy.confidence,
2219
+ provenance: import_types.Provenance.OBSERVED
2220
+ });
2205
2221
  } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
2206
2222
  const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
2207
2223
  const seedName = displayNameOf(seedNode);
@@ -6249,6 +6265,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
6249
6265
  function mergeObservedColumns(graph, tableNodeId, columns) {
6250
6266
  mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
6251
6267
  }
6268
+ function mergeObservedDeployState(graph, serviceNodeId, state) {
6269
+ if (!graph.hasNode(serviceNodeId)) return;
6270
+ const attrs = {};
6271
+ if (typeof state.image === "string" && state.image.length > 0) attrs["observedImage"] = state.image;
6272
+ if (typeof state.readyReplicas === "number") attrs["observedReadyReplicas"] = state.readyReplicas;
6273
+ if (Object.keys(attrs).length === 0) return;
6274
+ graph.mergeNodeAttributes(serviceNodeId, attrs);
6275
+ }
6252
6276
  function ensureDatabaseNode(graph, host, engine) {
6253
6277
  const id = (0, import_types8.databaseId)(host);
6254
6278
  if (graph.hasNode(id)) return id;
@@ -16968,6 +16992,25 @@ function detectColumnDrift(node) {
16968
16992
  }
16969
16993
  return out;
16970
16994
  }
16995
+ 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.";
16996
+ var DEPLOY_DIVERGENCE_CONFIDENCE = 0.9;
16997
+ function detectDeployDivergence(svc) {
16998
+ const out = [];
16999
+ if (typeof svc.declaredImage === "string" && typeof svc.observedImage === "string" && svc.declaredImage !== svc.observedImage) {
17000
+ out.push({
17001
+ type: "deploy-mismatch",
17002
+ kind: "image",
17003
+ source: svc.id,
17004
+ target: svc.id,
17005
+ declaredImage: svc.declaredImage,
17006
+ observedImage: svc.observedImage,
17007
+ confidence: DEPLOY_DIVERGENCE_CONFIDENCE,
17008
+ 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.`,
17009
+ recommendation: RECOMMENDATION_DEPLOY_IMAGE_MISMATCH
17010
+ });
17011
+ }
17012
+ return out;
17013
+ }
16971
17014
  var SYMBOL_MISMATCH_PATTERNS = [
16972
17015
  {
16973
17016
  // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
@@ -17295,6 +17338,7 @@ function computeDivergences(graph, opts = {}) {
17295
17338
  const svc = n;
17296
17339
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
17297
17340
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
17341
+ for (const d of detectDeployDivergence(svc)) all.push(d);
17298
17342
  return;
17299
17343
  }
17300
17344
  if (n.type === import_types60.NodeType.InfraNode && n.kind === "sql-table") {
@@ -17335,7 +17379,12 @@ function computeDivergences(graph, opts = {}) {
17335
17379
  // orders last so at equal confidence the structural and symbol divergences
17336
17380
  // lead, per the contract ("rank below the definitive structural and symbol
17337
17381
  // divergences").
17338
- "observed-failing": 6
17382
+ "observed-failing": 6,
17383
+ // Deploy mismatch (ADR-225) is a definitive structural declared-vs-observed
17384
+ // divergence carrying high confidence (0.9), so the confidence sort already
17385
+ // places it among the structural leaders; this slot only breaks an exact
17386
+ // confidence tie and sits last as a stable tiebreaker.
17387
+ "deploy-mismatch": 7
17339
17388
  };
17340
17389
  filtered.sort((a, b) => {
17341
17390
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -18875,6 +18924,11 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
18875
18924
  unresolved++;
18876
18925
  continue;
18877
18926
  }
18927
+ if (signal.deployState) {
18928
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
18929
+ mergeObservedDeployState(graph, resolved.targetNodeId, signal.deployState);
18930
+ continue;
18931
+ }
18878
18932
  if (signal.incident) {
18879
18933
  ensureServiceNode(graph, resolved.serviceName, NO_ENV);
18880
18934
  if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
@@ -23326,6 +23380,7 @@ var IMAGE_PULL_REASONS = /* @__PURE__ */ new Set(["ImagePullBackOff", "ErrImageP
23326
23380
  var CRASH_LOOP_REASON = "CrashLoopBackOff";
23327
23381
  var FIELD_SEP5 = "\0";
23328
23382
  var K8S_TARGET_KIND = "k8s-workload";
23383
+ var K8S_DEPLOY_STATE = "deploy-state";
23329
23384
  function packK8sTargetName(identity) {
23330
23385
  return [identity.serviceName, identity.fault].join(FIELD_SEP5);
23331
23386
  }
@@ -23457,11 +23512,40 @@ function mapDeploymentToSignal(deployment, pods, config) {
23457
23512
  }
23458
23513
  };
23459
23514
  }
23515
+ function observedDeployState(deployment, pods) {
23516
+ const readyReplicas = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
23517
+ let image;
23518
+ for (const pod of podsForDeployment(deployment, pods)) {
23519
+ for (const cs of pod.status?.containerStatuses ?? []) {
23520
+ if (cs.state?.running && typeof cs.image === "string" && cs.image.length > 0) {
23521
+ image = cs.image;
23522
+ break;
23523
+ }
23524
+ }
23525
+ if (image) break;
23526
+ }
23527
+ return image !== void 0 ? { image, readyReplicas } : { readyReplicas };
23528
+ }
23529
+ function deployStateSignal(deployment, pods, config) {
23530
+ const name = deployment.metadata?.name;
23531
+ if (typeof name !== "string" || name.length === 0) return null;
23532
+ const serviceName = serviceNameFor(deployment, config);
23533
+ return {
23534
+ targetKind: K8S_TARGET_KIND,
23535
+ targetName: packK8sTargetName({ serviceName, fault: K8S_DEPLOY_STATE }),
23536
+ callCount: 0,
23537
+ errorCount: 0,
23538
+ lastObservedIso: nowIso2(),
23539
+ deployState: observedDeployState(deployment, pods)
23540
+ };
23541
+ }
23460
23542
  function mapWorkloadsToSignals(deployments, pods, config) {
23461
23543
  const out = [];
23462
23544
  for (const deployment of deployments) {
23463
- const signal = mapDeploymentToSignal(deployment, pods, config);
23464
- if (signal) out.push(signal);
23545
+ const deployState = deployStateSignal(deployment, pods, config);
23546
+ if (deployState) out.push(deployState);
23547
+ const incident = mapDeploymentToSignal(deployment, pods, config);
23548
+ if (incident) out.push(incident);
23465
23549
  }
23466
23550
  return out;
23467
23551
  }