@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/neatd.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  reconcileDaemonRecordSync,
4
4
  startDaemon
5
- } from "./chunk-WE3AFQYL.js";
5
+ } from "./chunk-NV4WWJSU.js";
6
6
  import {
7
7
  listProjects,
8
8
  registryPath
9
- } from "./chunk-RBZNXA5L.js";
9
+ } from "./chunk-SSLPBQWY.js";
10
10
  import {
11
11
  BindAuthorityError,
12
12
  __require
package/dist/server.cjs CHANGED
@@ -5428,6 +5428,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
5428
5428
  function mergeObservedColumns(graph, tableNodeId, columns) {
5429
5429
  mergeColumnsAt(graph, tableNodeId, columns, import_types7.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
5430
5430
  }
5431
+ function mergeObservedDeployState(graph, serviceNodeId, state) {
5432
+ if (!graph.hasNode(serviceNodeId)) return;
5433
+ const attrs = {};
5434
+ if (typeof state.image === "string" && state.image.length > 0) attrs["observedImage"] = state.image;
5435
+ if (typeof state.readyReplicas === "number") attrs["observedReadyReplicas"] = state.readyReplicas;
5436
+ if (Object.keys(attrs).length === 0) return;
5437
+ graph.mergeNodeAttributes(serviceNodeId, attrs);
5438
+ }
5431
5439
  function ensureDatabaseNode(graph, host, engine) {
5432
5440
  const id = (0, import_types7.databaseId)(host);
5433
5441
  if (graph.hasNode(id)) return id;
@@ -6223,6 +6231,7 @@ function startStalenessLoop(graph, options = {}) {
6223
6231
  project: options.project
6224
6232
  });
6225
6233
  if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
6234
+ if (options.onReconcile) await options.onReconcile(graph);
6226
6235
  } catch (err) {
6227
6236
  console.error("staleness tick failed", err);
6228
6237
  }
@@ -6383,13 +6392,19 @@ function resolveOwningService(graph, nodeId) {
6383
6392
  }
6384
6393
  return null;
6385
6394
  }
6395
+ function isFrontierEdge(e) {
6396
+ return e.provenance === import_types8.Provenance.FRONTIER;
6397
+ }
6398
+ function rankOf(p) {
6399
+ return p === import_types8.Provenance.FRONTIER ? -1 : import_types8.PROV_RANK[p];
6400
+ }
6386
6401
  function bestEdgeBySource(graph, edgeIds) {
6387
6402
  const best = /* @__PURE__ */ new Map();
6388
6403
  for (const id of edgeIds) {
6389
6404
  const e = graph.getEdgeAttributes(id);
6390
- if (isFrontierNode(graph, e.source)) continue;
6405
+ if (isFrontierNode(graph, e.source) || isFrontierEdge(e)) continue;
6391
6406
  const cur = best.get(e.source);
6392
- if (!cur || import_types8.PROV_RANK[e.provenance] > import_types8.PROV_RANK[cur.provenance]) {
6407
+ if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
6393
6408
  best.set(e.source, e);
6394
6409
  }
6395
6410
  }
@@ -6399,9 +6414,9 @@ function bestEdgeByTarget(graph, edgeIds) {
6399
6414
  const best = /* @__PURE__ */ new Map();
6400
6415
  for (const id of edgeIds) {
6401
6416
  const e = graph.getEdgeAttributes(id);
6402
- if (isFrontierNode(graph, e.target)) continue;
6417
+ if (isFrontierNode(graph, e.target) || isFrontierEdge(e)) continue;
6403
6418
  const cur = best.get(e.target);
6404
- if (!cur || import_types8.PROV_RANK[e.provenance] > import_types8.PROV_RANK[cur.provenance]) {
6419
+ if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
6405
6420
  best.set(e.target, e);
6406
6421
  }
6407
6422
  }
@@ -6411,7 +6426,10 @@ var PROVENANCE_CEILING = {
6411
6426
  OBSERVED: 1,
6412
6427
  INFERRED: 0.7,
6413
6428
  EXTRACTED: 0.5,
6414
- STALE: 0.3
6429
+ STALE: 0.3,
6430
+ // A FRONTIER surface (ADR-226) is a proposal about a cause NEAT could not observe
6431
+ // — the least-trusted claim it makes, below STALE (which was at least once seen).
6432
+ FRONTIER: 0.2
6415
6433
  };
6416
6434
  function volumeWeight(spanCount) {
6417
6435
  if (!spanCount || spanCount <= 0) return 0.5;
@@ -6646,7 +6664,7 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
6646
6664
  });
6647
6665
  }
6648
6666
  function isFailingCallEdge(e) {
6649
- return e.type === import_types8.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
6667
+ return e.type === import_types8.EdgeType.CALLS && !isFrontierEdge(e) && (e.signal?.errorCount ?? 0) > 0;
6650
6668
  }
6651
6669
  function callSourcesForService(graph, serviceId17) {
6652
6670
  const ids = [serviceId17];
@@ -6669,8 +6687,8 @@ function failingCallDominates(e, id, curEdge, curId) {
6669
6687
  const ec = e.signal?.errorCount ?? 0;
6670
6688
  const cc = curEdge.signal?.errorCount ?? 0;
6671
6689
  if (ec !== cc) return ec > cc;
6672
- if (import_types8.PROV_RANK[e.provenance] !== import_types8.PROV_RANK[curEdge.provenance]) {
6673
- return import_types8.PROV_RANK[e.provenance] > import_types8.PROV_RANK[curEdge.provenance];
6690
+ if (rankOf(e.provenance) !== rankOf(curEdge.provenance)) {
6691
+ return rankOf(e.provenance) > rankOf(curEdge.provenance);
6674
6692
  }
6675
6693
  return id < curId;
6676
6694
  }
@@ -6721,11 +6739,11 @@ function dominantStaleCall(graph, serviceId17, visited) {
6721
6739
  for (const edgeId of graph.outboundEdges(src)) {
6722
6740
  const e = graph.getEdgeAttributes(edgeId);
6723
6741
  if (e.type !== import_types8.EdgeType.CALLS) continue;
6724
- if (isFrontierNode(graph, e.target)) continue;
6742
+ if (isFrontierNode(graph, e.target) || isFrontierEdge(e)) continue;
6725
6743
  const owner = resolveOwningService(graph, e.target);
6726
6744
  if (!owner || visited.has(owner.id)) continue;
6727
6745
  const cur = bestByCallee.get(owner.id);
6728
- if (!cur || import_types8.PROV_RANK[e.provenance] > import_types8.PROV_RANK[cur.provenance]) {
6746
+ if (!cur || rankOf(e.provenance) > rankOf(cur.provenance)) {
6729
6747
  bestByCallee.set(owner.id, e);
6730
6748
  }
6731
6749
  }
@@ -6998,6 +7016,7 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
6998
7016
  for (const edgeId of graph.inboundEdges(n)) {
6999
7017
  const e = graph.getEdgeAttributes(edgeId);
7000
7018
  if (e.type === import_types8.EdgeType.CONTAINS) continue;
7019
+ if (isFrontierEdge(e)) continue;
7001
7020
  errorsFromCallers += e.signal?.errorCount ?? 0;
7002
7021
  inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
7003
7022
  if (e.provenance === import_types8.Provenance.STALE) stale = true;
@@ -7011,6 +7030,7 @@ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
7011
7030
  for (const edgeId of graph.outboundEdges(n)) {
7012
7031
  const e = graph.getEdgeAttributes(edgeId);
7013
7032
  if (e.type === import_types8.EdgeType.CONTAINS) continue;
7033
+ if (isFrontierEdge(e)) continue;
7014
7034
  outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
7015
7035
  if (e.type === import_types8.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
7016
7036
  if (e.provenance === import_types8.Provenance.STALE) stale = true;
@@ -7044,6 +7064,11 @@ function isSaturated(ctx) {
7044
7064
  function isBoundaryTimeoutSymptom(ctx) {
7045
7065
  return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
7046
7066
  }
7067
+ var UNREACHABLE_INBOUND_ERROR_RATE = 0.5;
7068
+ var UNREACHABLE_MIN_INBOUND = 3;
7069
+ function isUnreachableSeed(ctx) {
7070
+ return ctx.callCount >= UNREACHABLE_MIN_INBOUND && ctx.errorsFromCallers > 0 && ctx.errorsFromCallers >= UNREACHABLE_INBOUND_ERROR_RATE * ctx.callCount && ctx.errorsEmittedHere === 0 && ctx.outboundVolume === 0;
7071
+ }
7047
7072
  function classifyNode(ctx) {
7048
7073
  if (ctx.errorsEmittedHere > 0) {
7049
7074
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
@@ -7052,6 +7077,7 @@ function classifyNode(ctx) {
7052
7077
  if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
7053
7078
  return "primary-failure";
7054
7079
  }
7080
+ if (isUnreachableSeed(ctx)) return "unreachable";
7055
7081
  if (ctx.errorsFromCallers > 0) return "symptom-only";
7056
7082
  return "unrelated";
7057
7083
  }
@@ -7288,22 +7314,44 @@ function routeMatches(declared, incident) {
7288
7314
  const b = normalizeRoute(incident);
7289
7315
  return a === b || a.startsWith(b) || b.startsWith(a);
7290
7316
  }
7291
- function structuralUpstreamPointer(graph, boundaryNode, incidents) {
7292
- const route = boundaryIncidentRoute(boundaryNode, incidents);
7293
- const callees = declaredOutboundCallees(graph, boundaryNode);
7317
+ function firstDeclaredCallee(graph, node, route) {
7318
+ const callees = declaredOutboundCallees(graph, node);
7294
7319
  if (callees.length === 0) return null;
7295
7320
  const narrowed = route !== void 0 ? callees.filter((c) => c.route !== void 0 && routeMatches(c.route, route)) : callees;
7296
7321
  const chosen = narrowed.length === 1 ? narrowed[0] : callees.length === 1 ? callees[0] : null;
7297
- if (!chosen) return null;
7298
- let node = chosen.target;
7299
- const seen = /* @__PURE__ */ new Set([boundaryNode, node]);
7300
- for (let depth = 0; depth < ROOT_CAUSE_MAX_DEPTH; depth++) {
7301
- const next = declaredOutboundCallees(graph, node);
7302
- if (next.length !== 1 || seen.has(next[0].target)) break;
7303
- node = next[0].target;
7304
- seen.add(node);
7322
+ return chosen ? chosen.target : null;
7323
+ }
7324
+ function observedOutboundNextServices(graph, nodeId) {
7325
+ const targets = /* @__PURE__ */ new Set();
7326
+ for (const n of nodeScope(graph, nodeId)) {
7327
+ if (!graph.hasNode(n)) continue;
7328
+ for (const edgeId of graph.outboundEdges(n)) {
7329
+ const e = graph.getEdgeAttributes(edgeId);
7330
+ if (!DEP_EDGE_TYPES.has(e.type)) continue;
7331
+ if (e.provenance !== import_types8.Provenance.OBSERVED) continue;
7332
+ if (e.target !== nodeId) targets.add(e.target);
7333
+ }
7305
7334
  }
7306
- return route !== void 0 ? { node, route } : { node };
7335
+ return [...targets];
7336
+ }
7337
+ function resolveHangHop(graph, boundaryNode, incidents) {
7338
+ const route = boundaryIncidentRoute(boundaryNode, incidents);
7339
+ const own = firstDeclaredCallee(graph, boundaryNode, route);
7340
+ if (own) {
7341
+ return route !== void 0 ? { source: boundaryNode, target: own, route } : { source: boundaryNode, target: own };
7342
+ }
7343
+ const resolved = observedOutboundNextServices(graph, boundaryNode).map((n) => ({ source: n, target: firstDeclaredCallee(graph, n, route) })).filter((r) => r.target !== null);
7344
+ if (resolved.length === 1) {
7345
+ const hop = resolved[0];
7346
+ return route !== void 0 ? { ...hop, route } : hop;
7347
+ }
7348
+ return null;
7349
+ }
7350
+ function stagedHangProposal(graph, boundaryNode, incidents) {
7351
+ const hop = resolveHangHop(graph, boundaryNode, incidents);
7352
+ if (!hop) return null;
7353
+ if (!graph.hasEdge((0, import_types8.frontierEdgeId)(hop.source, hop.target, import_types8.EdgeType.CALLS))) return null;
7354
+ return hop.route !== void 0 ? { node: hop.target, route: hop.route } : { node: hop.target };
7307
7355
  }
7308
7356
  function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
7309
7357
  const legacy = tagged.result;
@@ -7338,29 +7386,39 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
7338
7386
  confidence: Math.min(legacy.confidence, 0.4),
7339
7387
  ...lastProv ? { provenance: lastProv } : {}
7340
7388
  });
7389
+ } else if (seedCtx && isUnreachableSeed(seedCtx)) {
7390
+ const name = displayNameOf(seedNode);
7391
+ candidates.push({
7392
+ node: seedNode,
7393
+ classification: "unreachable",
7394
+ 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.`,
7395
+ context: seedCtx,
7396
+ confidence: legacy.confidence,
7397
+ provenance: import_types8.Provenance.OBSERVED
7398
+ });
7341
7399
  } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
7342
- const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
7343
7400
  const seedName = displayNameOf(seedNode);
7344
- const routeNote = pointer?.route ? ` serving ${pointer.route}` : "";
7345
- if (pointer) {
7346
- const causeName = displayNameOf(pointer.node);
7347
- candidates.push({
7348
- node: pointer.node,
7349
- classification: "primary-failure",
7350
- 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.`,
7351
- context: nodeContext(graph, pointer.node, incidents, now),
7352
- confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.EXTRACTED),
7353
- provenance: import_types8.Provenance.INFERRED
7354
- });
7355
- }
7401
+ const proposal = stagedHangProposal(graph, seedNode, incidents);
7402
+ const routeNote = proposal?.route ? ` serving ${proposal.route}` : "";
7356
7403
  candidates.push({
7357
7404
  node: seedNode,
7358
7405
  classification: "symptom-only",
7359
- 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.`,
7406
+ 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.`,
7360
7407
  context: seedCtx,
7361
7408
  confidence: Math.min(legacy.confidence, 0.3),
7362
7409
  ...lastProv ? { provenance: lastProv } : {}
7363
7410
  });
7411
+ if (proposal) {
7412
+ const causeName = displayNameOf(proposal.node);
7413
+ candidates.push({
7414
+ node: proposal.node,
7415
+ classification: "primary-failure",
7416
+ 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.`,
7417
+ context: nodeContext(graph, proposal.node, incidents, now),
7418
+ confidence: Math.min(legacy.confidence, PROVENANCE_CEILING.FRONTIER),
7419
+ provenance: import_types8.Provenance.FRONTIER
7420
+ });
7421
+ }
7364
7422
  } else if (staleChain) {
7365
7423
  const culprit = staleChain.culprit;
7366
7424
  const culpritName = displayNameOf(culprit);
@@ -7424,6 +7482,9 @@ function fixRecommendationForTop(top, seedNode, legacy) {
7424
7482
  return legacy.fixRecommendation;
7425
7483
  }
7426
7484
  const name = top.node.replace(/^service:/, "");
7485
+ if (top.classification === "symptom-only" && top.context.boundaryTimeout === true) {
7486
+ 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.`;
7487
+ }
7427
7488
  if (top.provenance === import_types8.Provenance.STALE) {
7428
7489
  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}.`;
7429
7490
  }
@@ -7687,6 +7748,25 @@ function detectColumnDrift(node) {
7687
7748
  }
7688
7749
  return out;
7689
7750
  }
7751
+ 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.";
7752
+ var DEPLOY_DIVERGENCE_CONFIDENCE = 0.9;
7753
+ function detectDeployDivergence(svc) {
7754
+ const out = [];
7755
+ if (typeof svc.declaredImage === "string" && typeof svc.observedImage === "string" && svc.declaredImage !== svc.observedImage) {
7756
+ out.push({
7757
+ type: "deploy-mismatch",
7758
+ kind: "image",
7759
+ source: svc.id,
7760
+ target: svc.id,
7761
+ declaredImage: svc.declaredImage,
7762
+ observedImage: svc.observedImage,
7763
+ confidence: DEPLOY_DIVERGENCE_CONFIDENCE,
7764
+ 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.`,
7765
+ recommendation: RECOMMENDATION_DEPLOY_IMAGE_MISMATCH
7766
+ });
7767
+ }
7768
+ return out;
7769
+ }
7690
7770
  var SYMBOL_MISMATCH_PATTERNS = [
7691
7771
  {
7692
7772
  // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
@@ -8014,6 +8094,7 @@ function computeDivergences(graph, opts = {}) {
8014
8094
  const svc = n;
8015
8095
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
8016
8096
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
8097
+ for (const d of detectDeployDivergence(svc)) all.push(d);
8017
8098
  return;
8018
8099
  }
8019
8100
  if (n.type === import_types9.NodeType.InfraNode && n.kind === "sql-table") {
@@ -8054,7 +8135,12 @@ function computeDivergences(graph, opts = {}) {
8054
8135
  // orders last so at equal confidence the structural and symbol divergences
8055
8136
  // lead, per the contract ("rank below the definitive structural and symbol
8056
8137
  // divergences").
8057
- "observed-failing": 6
8138
+ "observed-failing": 6,
8139
+ // Deploy mismatch (ADR-225) is a definitive structural declared-vs-observed
8140
+ // divergence carrying high confidence (0.9), so the confidence sort already
8141
+ // places it among the structural leaders; this slot only breaks an exact
8142
+ // confidence tie and sits last as a stable tiebreaker.
8143
+ "deploy-mismatch": 7
8058
8144
  };
8059
8145
  filtered.sort((a, b) => {
8060
8146
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -18602,6 +18688,11 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
18602
18688
  unresolved++;
18603
18689
  continue;
18604
18690
  }
18691
+ if (signal.deployState) {
18692
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
18693
+ mergeObservedDeployState(graph, resolved.targetNodeId, signal.deployState);
18694
+ continue;
18695
+ }
18605
18696
  if (signal.incident) {
18606
18697
  ensureServiceNode(graph, resolved.serviceName, NO_ENV);
18607
18698
  if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {