@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/cli.cjs CHANGED
@@ -1953,6 +1953,11 @@ function isSaturated(ctx) {
1953
1953
  function isBoundaryTimeoutSymptom(ctx) {
1954
1954
  return ctx.errorsEmittedHere > 0 && ctx.boundaryTimeout === true && ctx.errorsFromCallers === 0 && ctx.hasOutboundDeps === true && ctx.observedErroringDownstream !== true;
1955
1955
  }
1956
+ var UNREACHABLE_INBOUND_ERROR_RATE = 0.5;
1957
+ var UNREACHABLE_MIN_INBOUND = 3;
1958
+ function isUnreachableSeed(ctx) {
1959
+ return ctx.callCount >= UNREACHABLE_MIN_INBOUND && ctx.errorsFromCallers > 0 && ctx.errorsFromCallers >= UNREACHABLE_INBOUND_ERROR_RATE * ctx.callCount && ctx.errorsEmittedHere === 0 && ctx.outboundVolume === 0;
1960
+ }
1956
1961
  function classifyNode(ctx) {
1957
1962
  if (ctx.errorsEmittedHere > 0) {
1958
1963
  if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
@@ -1961,6 +1966,7 @@ function classifyNode(ctx) {
1961
1966
  if (isBoundaryTimeoutSymptom(ctx)) return "symptom-only";
1962
1967
  return "primary-failure";
1963
1968
  }
1969
+ if (isUnreachableSeed(ctx)) return "unreachable";
1964
1970
  if (ctx.errorsFromCallers > 0) return "symptom-only";
1965
1971
  return "unrelated";
1966
1972
  }
@@ -2247,6 +2253,16 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2247
2253
  confidence: Math.min(legacy.confidence, 0.4),
2248
2254
  ...lastProv ? { provenance: lastProv } : {}
2249
2255
  });
2256
+ } else if (seedCtx && isUnreachableSeed(seedCtx)) {
2257
+ const name = displayNameOf(seedNode);
2258
+ candidates.push({
2259
+ node: seedNode,
2260
+ classification: "unreachable",
2261
+ 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.`,
2262
+ context: seedCtx,
2263
+ confidence: legacy.confidence,
2264
+ provenance: import_types.Provenance.OBSERVED
2265
+ });
2250
2266
  } else if (seedCtx && isBoundaryTimeoutSymptom(seedCtx)) {
2251
2267
  const pointer = structuralUpstreamPointer(graph, seedNode, incidents);
2252
2268
  const seedName = displayNameOf(seedNode);
@@ -6045,8 +6061,8 @@ function pickContainingSymbol(candidates, fn) {
6045
6061
  return b.symbol.span.startLine - a.symbol.span.startLine;
6046
6062
  };
6047
6063
  if (fn) {
6048
- const named = candidates.filter((c) => terminalName(c.symbol.qualname) === fn);
6049
- if (named.length > 0) return [...named].sort(bySpan)[0].id;
6064
+ const named2 = candidates.filter((c) => terminalName(c.symbol.qualname) === fn);
6065
+ if (named2.length > 0) return [...named2].sort(bySpan)[0].id;
6050
6066
  }
6051
6067
  return [...candidates].sort(bySpan)[0].id;
6052
6068
  }
@@ -6306,6 +6322,14 @@ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
6306
6322
  function mergeObservedColumns(graph, tableNodeId, columns) {
6307
6323
  mergeColumnsAt(graph, tableNodeId, columns, import_types8.Provenance.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
6308
6324
  }
6325
+ function mergeObservedDeployState(graph, serviceNodeId, state) {
6326
+ if (!graph.hasNode(serviceNodeId)) return;
6327
+ const attrs = {};
6328
+ if (typeof state.image === "string" && state.image.length > 0) attrs["observedImage"] = state.image;
6329
+ if (typeof state.readyReplicas === "number") attrs["observedReadyReplicas"] = state.readyReplicas;
6330
+ if (Object.keys(attrs).length === 0) return;
6331
+ graph.mergeNodeAttributes(serviceNodeId, attrs);
6332
+ }
6309
6333
  function ensureDatabaseNode(graph, host, engine) {
6310
6334
  const id = (0, import_types8.databaseId)(host);
6311
6335
  if (graph.hasNode(id)) return id;
@@ -9465,10 +9489,10 @@ function collectNamedImports(root) {
9465
9489
  const clause = node.namedChild(i);
9466
9490
  if (clause?.type !== "import_clause") continue;
9467
9491
  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);
9492
+ const named2 = clause.namedChild(j);
9493
+ if (named2?.type !== "named_imports") continue;
9494
+ for (let k = 0; k < named2.namedChildCount; k++) {
9495
+ const spec = named2.namedChild(k);
9472
9496
  if (spec?.type !== "import_specifier") continue;
9473
9497
  let isType = false;
9474
9498
  for (let t = 0; t < spec.childCount; t++) {
@@ -12102,8 +12126,8 @@ function parseImportBindings(content) {
12102
12126
  out.push({ local: ns[1], kind: "namespace", specifier: spec });
12103
12127
  continue;
12104
12128
  }
12105
- const named = /\{([^}]*)\}/.exec(clause);
12106
- if (named) parseDestructure(named[1], spec, out);
12129
+ const named2 = /\{([^}]*)\}/.exec(clause);
12130
+ if (named2) parseDestructure(named2[1], spec, out);
12107
12131
  const def = /^(\w+)\s*(?:,|$)/.exec(clause);
12108
12132
  if (def && !clause.startsWith("{")) out.push({ local: def[1], kind: "default", specifier: spec });
12109
12133
  }
@@ -12123,8 +12147,8 @@ function fileExportsOf(content, pluralizeOn) {
12123
12147
  for (const d of defs) if (d.varName) byVar.set(d.varName, d.resolved.name);
12124
12148
  const byName = new Map(byVar);
12125
12149
  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]);
12150
+ const named2 = /(?:module\.exports|export\s+default)\s*=\s*(\w+)\b/.exec(content);
12151
+ if (named2 && byVar.has(named2[1])) def = byVar.get(named2[1]);
12128
12152
  if (!def) {
12129
12153
  const inline = /(?:module\.exports|export\s+default)\s*=\s*(?:await\s+)?(?:\w+\s*\.\s*)?model\s*\(\s*['"`]([\w$]+)['"`]/.exec(content);
12130
12154
  if (inline) def = pluralizeOn ? pluralizeCollection(inline[1]) : inline[1];
@@ -16494,6 +16518,25 @@ function detectColumnDrift(node) {
16494
16518
  }
16495
16519
  return out;
16496
16520
  }
16521
+ 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.";
16522
+ var DEPLOY_DIVERGENCE_CONFIDENCE = 0.9;
16523
+ function detectDeployDivergence(svc) {
16524
+ const out = [];
16525
+ if (typeof svc.declaredImage === "string" && typeof svc.observedImage === "string" && svc.declaredImage !== svc.observedImage) {
16526
+ out.push({
16527
+ type: "deploy-mismatch",
16528
+ kind: "image",
16529
+ source: svc.id,
16530
+ target: svc.id,
16531
+ declaredImage: svc.declaredImage,
16532
+ observedImage: svc.observedImage,
16533
+ confidence: DEPLOY_DIVERGENCE_CONFIDENCE,
16534
+ 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.`,
16535
+ recommendation: RECOMMENDATION_DEPLOY_IMAGE_MISMATCH
16536
+ });
16537
+ }
16538
+ return out;
16539
+ }
16497
16540
  var SYMBOL_MISMATCH_PATTERNS = [
16498
16541
  {
16499
16542
  // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
@@ -16821,6 +16864,7 @@ function computeDivergences(graph, opts = {}) {
16821
16864
  const svc = n;
16822
16865
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
16823
16866
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
16867
+ for (const d of detectDeployDivergence(svc)) all.push(d);
16824
16868
  return;
16825
16869
  }
16826
16870
  if (n.type === import_types59.NodeType.InfraNode && n.kind === "sql-table") {
@@ -16861,7 +16905,12 @@ function computeDivergences(graph, opts = {}) {
16861
16905
  // orders last so at equal confidence the structural and symbol divergences
16862
16906
  // lead, per the contract ("rank below the definitive structural and symbol
16863
16907
  // divergences").
16864
- "observed-failing": 6
16908
+ "observed-failing": 6,
16909
+ // Deploy mismatch (ADR-225) is a definitive structural declared-vs-observed
16910
+ // divergence carrying high confidence (0.9), so the confidence sort already
16911
+ // places it among the structural leaders; this slot only breaks an exact
16912
+ // confidence tie and sits last as a stable tiebreaker.
16913
+ "deploy-mismatch": 7
16865
16914
  };
16866
16915
  filtered.sort((a, b) => {
16867
16916
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -17956,11 +18005,11 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17956
18005
  if (labelTokens.size === 0) return;
17957
18006
  let matched = 0;
17958
18007
  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;
18008
+ const named2 = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
18009
+ if (matched === 0 && !named2) return;
17961
18010
  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);
18011
+ const via = named2 ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
18012
+ const score = named2 ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
17964
18013
  consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
17965
18014
  });
17966
18015
  if (searchIndex) {
@@ -19300,6 +19349,11 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
19300
19349
  unresolved++;
19301
19350
  continue;
19302
19351
  }
19352
+ if (signal.deployState) {
19353
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
19354
+ mergeObservedDeployState(graph, resolved.targetNodeId, signal.deployState);
19355
+ continue;
19356
+ }
19303
19357
  if (signal.incident) {
19304
19358
  ensureServiceNode(graph, resolved.serviceName, NO_ENV);
19305
19359
  if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
@@ -19833,7 +19887,7 @@ function tableNameFromQueryText(query) {
19833
19887
  if (SYSTEM_SCHEMA_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null;
19834
19888
  return name;
19835
19889
  }
19836
- function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
19890
+ function diffPgStatStatementsToSignals(rows, previous, nowIso3) {
19837
19891
  const signals = [];
19838
19892
  const seen = /* @__PURE__ */ new Set();
19839
19893
  if (!Array.isArray(rows)) return signals;
@@ -19855,7 +19909,7 @@ function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
19855
19909
  targetName: table,
19856
19910
  callCount: delta,
19857
19911
  errorCount: 0,
19858
- lastObservedIso: nowIso2,
19912
+ lastObservedIso: nowIso3,
19859
19913
  ...columns.length > 0 ? { columns } : {}
19860
19914
  });
19861
19915
  }
@@ -22751,11 +22805,11 @@ function serializeGraph(graph) {
22751
22805
  }
22752
22806
  function projectFromReq(req, singleProject) {
22753
22807
  const params = req.params;
22754
- const named = params.project;
22808
+ const named2 = params.project;
22755
22809
  if (singleProject) {
22756
- return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
22810
+ return named2 === void 0 || named2 === DEFAULT_PROJECT ? singleProject : named2;
22757
22811
  }
22758
- return named ?? DEFAULT_PROJECT;
22812
+ return named2 ?? DEFAULT_PROJECT;
22759
22813
  }
22760
22814
  function resolveProject(registry, req, reply, bootstrap, singleProject) {
22761
22815
  const name = projectFromReq(req, singleProject);
@@ -23654,42 +23708,482 @@ async function buildApi(opts) {
23654
23708
  init_auth();
23655
23709
  init_otel();
23656
23710
 
23657
- // src/daemon.ts
23658
- 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");
23662
- init_otel();
23663
-
23664
23711
  // src/connectors/kubernetes/index.ts
23665
23712
  init_cjs_shims();
23666
23713
 
23667
23714
  // src/connectors/kubernetes/client.ts
23668
23715
  init_cjs_shims();
23669
23716
  var import_node_https = require("https");
23717
+ function deploymentsPath(namespace) {
23718
+ return `/apis/apps/v1/namespaces/${namespace}/deployments`;
23719
+ }
23720
+ function podsPath(namespace) {
23721
+ return `/api/v1/namespaces/${namespace}/pods`;
23722
+ }
23723
+ function makeK8sFetchImpl(transport) {
23724
+ const agent = new import_node_https.Agent({
23725
+ ...transport.ca ? { ca: transport.ca } : {},
23726
+ ...transport.clientCert ? { cert: transport.clientCert } : {},
23727
+ ...transport.clientKey ? { key: transport.clientKey } : {},
23728
+ rejectUnauthorized: !transport.insecureSkipTlsVerify
23729
+ });
23730
+ return ((url, init) => new Promise((resolve, reject) => {
23731
+ const u = new URL(String(url));
23732
+ const req = (0, import_node_https.request)(
23733
+ u,
23734
+ {
23735
+ method: (init?.method ?? "GET").toUpperCase(),
23736
+ headers: init?.headers ?? {},
23737
+ agent
23738
+ },
23739
+ (res) => {
23740
+ const chunks = [];
23741
+ res.on("data", (c) => chunks.push(c));
23742
+ res.on("end", () => {
23743
+ const body = Buffer.concat(chunks).toString("utf8");
23744
+ const status2 = res.statusCode ?? 0;
23745
+ resolve({
23746
+ ok: status2 >= 200 && status2 < 300,
23747
+ status: status2,
23748
+ statusText: res.statusMessage ?? "",
23749
+ json: async () => JSON.parse(body),
23750
+ text: async () => body
23751
+ });
23752
+ });
23753
+ }
23754
+ );
23755
+ req.on("error", reject);
23756
+ const signal = init?.signal;
23757
+ if (signal) {
23758
+ if (signal.aborted) req.destroy(new Error("aborted"));
23759
+ else signal.addEventListener("abort", () => req.destroy(new Error("aborted")), { once: true });
23760
+ }
23761
+ req.end();
23762
+ }));
23763
+ }
23764
+ async function listResource(transport, namespace, path96, opts = {}) {
23765
+ const base = opts.apiUrl ?? transport.server;
23766
+ const url = `${base.replace(/\/$/, "")}${path96}`;
23767
+ const fetchImpl = opts.fetchImpl ?? makeK8sFetchImpl(transport);
23768
+ const res = await junctionFetch(
23769
+ url,
23770
+ { method: "GET", headers: { ...transport.token ? bearerAuthHeader(transport.token) : {}, Accept: "application/json" } },
23771
+ // accountKey: the (cluster, namespace) pair — an identifier, safe to log, the
23772
+ // rate-limit bucket for one namespace on one cluster (ADR-131).
23773
+ { provider: "kubernetes", accountKey: `${safeHost(transport.server)}/${namespace}`, fetchImpl }
23774
+ );
23775
+ if (!res.ok) {
23776
+ throw new Error(`kubernetes ${path96} failed: ${res.status} ${res.statusText}`);
23777
+ }
23778
+ const json = await res.json();
23779
+ return Array.isArray(json.items) ? json.items : [];
23780
+ }
23781
+ function safeHost(server) {
23782
+ try {
23783
+ return new URL(server).host;
23784
+ } catch {
23785
+ return "cluster";
23786
+ }
23787
+ }
23788
+ async function fetchDeployments(transport, namespace, opts = {}) {
23789
+ return listResource(transport, namespace, deploymentsPath(namespace), opts);
23790
+ }
23791
+ async function fetchPods(transport, namespace, opts = {}) {
23792
+ return listResource(transport, namespace, podsPath(namespace), opts);
23793
+ }
23670
23794
 
23671
23795
  // src/connectors/kubernetes/kubeconfig.ts
23672
23796
  init_cjs_shims();
23673
23797
  var import_node_fs41 = require("fs");
23674
23798
  var import_yaml4 = require("yaml");
23799
+ function pemFrom(dataField, pathField) {
23800
+ if (typeof dataField === "string" && dataField.length > 0) {
23801
+ return Buffer.from(dataField, "base64").toString("utf8");
23802
+ }
23803
+ if (typeof pathField === "string" && pathField.length > 0) {
23804
+ return (0, import_node_fs41.readFileSync)(pathField, "utf8");
23805
+ }
23806
+ return void 0;
23807
+ }
23808
+ function named(list, name) {
23809
+ if (!Array.isArray(list)) return void 0;
23810
+ const hit = list.find((e) => e && e.name === name);
23811
+ return hit;
23812
+ }
23813
+ function parseKubeconfig(kubeconfig) {
23814
+ const looksInline = /\n/.test(kubeconfig) || /(^|\s)clusters\s*:/.test(kubeconfig);
23815
+ const text = looksInline ? kubeconfig : (0, import_node_fs41.readFileSync)(kubeconfig, "utf8");
23816
+ let doc;
23817
+ try {
23818
+ doc = (0, import_yaml4.parse)(text);
23819
+ } catch {
23820
+ throw new Error("kubernetes connector: kubeconfig is not valid YAML");
23821
+ }
23822
+ if (!doc || typeof doc !== "object") {
23823
+ throw new Error("kubernetes connector: kubeconfig is empty or malformed");
23824
+ }
23825
+ const currentContext = doc["current-context"];
23826
+ if (typeof currentContext !== "string" || currentContext.length === 0) {
23827
+ throw new Error("kubernetes connector: kubeconfig has no current-context");
23828
+ }
23829
+ const ctxEntry = named(doc["contexts"], currentContext);
23830
+ const ctx = ctxEntry?.context;
23831
+ if (!ctx) {
23832
+ throw new Error(`kubernetes connector: kubeconfig context "${currentContext}" not found`);
23833
+ }
23834
+ const clusterEntry = named(doc["clusters"], String(ctx["cluster"] ?? ""));
23835
+ const cluster = clusterEntry?.cluster;
23836
+ if (!cluster || typeof cluster["server"] !== "string") {
23837
+ throw new Error("kubernetes connector: kubeconfig current context has no cluster server");
23838
+ }
23839
+ const userEntry = named(doc["users"], String(ctx["user"] ?? ""));
23840
+ const user = userEntry?.user ?? {};
23841
+ const transport = {
23842
+ server: cluster["server"],
23843
+ insecureSkipTlsVerify: cluster["insecure-skip-tls-verify"] === true
23844
+ };
23845
+ const ca = pemFrom(cluster["certificate-authority-data"], cluster["certificate-authority"]);
23846
+ if (ca) transport.ca = ca;
23847
+ if (typeof user["token"] === "string" && user["token"].length > 0) transport.token = user["token"];
23848
+ const clientCert = pemFrom(user["client-certificate-data"], user["client-certificate"]);
23849
+ const clientKey = pemFrom(user["client-key-data"], user["client-key"]);
23850
+ if (clientCert) transport.clientCert = clientCert;
23851
+ if (clientKey) transport.clientKey = clientKey;
23852
+ return transport;
23853
+ }
23854
+ function resolveK8sTransport(creds, config) {
23855
+ if (creds.kubeconfig) return parseKubeconfig(creds.kubeconfig);
23856
+ if (!config.apiServerUrl) {
23857
+ throw new Error("kubernetes connector: options.apiServerUrl is required with a token credential");
23858
+ }
23859
+ const transport = {
23860
+ server: config.apiServerUrl,
23861
+ insecureSkipTlsVerify: config.insecureSkipTlsVerify === true
23862
+ };
23863
+ if (creds.token) transport.token = creds.token;
23864
+ if (config.caCert) transport.ca = config.caCert;
23865
+ return transport;
23866
+ }
23675
23867
 
23676
23868
  // src/connectors/kubernetes/map.ts
23677
23869
  init_cjs_shims();
23678
23870
 
23679
23871
  // src/connectors/kubernetes/types.ts
23680
23872
  init_cjs_shims();
23873
+ function readK8sCredentials(raw) {
23874
+ const token = typeof raw["token"] === "string" && raw["token"].length > 0 ? raw["token"] : void 0;
23875
+ const kubeconfig = typeof raw["kubeconfig"] === "string" && raw["kubeconfig"].length > 0 ? raw["kubeconfig"] : void 0;
23876
+ if (!token && !kubeconfig) {
23877
+ throw new Error("kubernetes connector: credentials must carry a token or a kubeconfig");
23878
+ }
23879
+ const out = {};
23880
+ if (token) out.token = token;
23881
+ if (kubeconfig) out.kubeconfig = kubeconfig;
23882
+ return out;
23883
+ }
23884
+ var IMAGE_PULL_REASONS = /* @__PURE__ */ new Set(["ImagePullBackOff", "ErrImagePull", "InvalidImageName"]);
23885
+ var CRASH_LOOP_REASON = "CrashLoopBackOff";
23886
+ var FIELD_SEP5 = "\0";
23887
+ var K8S_TARGET_KIND = "k8s-workload";
23888
+ var K8S_DEPLOY_STATE = "deploy-state";
23889
+ function packK8sTargetName(identity) {
23890
+ return [identity.serviceName, identity.fault].join(FIELD_SEP5);
23891
+ }
23892
+ function parseK8sTargetName(targetName) {
23893
+ const sep = targetName.indexOf(FIELD_SEP5);
23894
+ if (sep === -1) return null;
23895
+ const serviceName = targetName.slice(0, sep);
23896
+ const fault = targetName.slice(sep + 1);
23897
+ if (!serviceName || !fault) return null;
23898
+ return { serviceName, fault };
23899
+ }
23900
+
23901
+ // src/connectors/kubernetes/map.ts
23902
+ function serviceNameFor(deployment, config) {
23903
+ const name = deployment.metadata?.name ?? "";
23904
+ return config.serviceMap?.[name] ?? name;
23905
+ }
23906
+ function podMatchesSelector(pod, selector) {
23907
+ if (!selector || Object.keys(selector).length === 0) return false;
23908
+ const labels = pod.metadata?.labels ?? {};
23909
+ return Object.entries(selector).every(([k, v]) => labels[k] === v);
23910
+ }
23911
+ function podsForDeployment(deployment, pods) {
23912
+ const selector = deployment.spec?.selector?.matchLabels;
23913
+ return pods.filter((p) => podMatchesSelector(p, selector));
23914
+ }
23915
+ function nowIso2() {
23916
+ return (/* @__PURE__ */ new Date()).toISOString();
23917
+ }
23918
+ function podLevelFault(deployment, pods) {
23919
+ const name = deployment.metadata?.name ?? "";
23920
+ const owned = podsForDeployment(deployment, pods);
23921
+ let crash = null;
23922
+ for (const pod of owned) {
23923
+ for (const cs of pod.status?.containerStatuses ?? []) {
23924
+ const reason = cs.state?.waiting?.reason;
23925
+ if (typeof reason !== "string") continue;
23926
+ if (IMAGE_PULL_REASONS.has(reason)) {
23927
+ const image = typeof cs.image === "string" ? cs.image : "unknown image";
23928
+ const attrs = { "k8s.image": image, "k8s.waitingReason": reason };
23929
+ const wm = cs.state?.waiting?.message;
23930
+ if (typeof wm === "string" && wm.length > 0) attrs["k8s.waitingMessage"] = wm;
23931
+ return {
23932
+ fault: "image-pull",
23933
+ message: `Deployment ${name} cannot pull image ${image} (${reason})`,
23934
+ timestamp: pod.status?.startTime ?? nowIso2(),
23935
+ attributes: attrs
23936
+ };
23937
+ }
23938
+ if (reason === CRASH_LOOP_REASON && !crash) crash = { cs, pod };
23939
+ }
23940
+ }
23941
+ if (crash) {
23942
+ const { cs, pod } = crash;
23943
+ const term = cs.lastState?.terminated;
23944
+ const termReason = typeof term?.reason === "string" ? term.reason : void 0;
23945
+ const termMsg = typeof term?.message === "string" ? term.message.trim() : void 0;
23946
+ const restarts = typeof cs.restartCount === "number" ? cs.restartCount : 0;
23947
+ const detail = termReason ? `last terminated: ${termReason}${termMsg ? ` \u2014 ${termMsg}` : ""}${typeof term?.exitCode === "number" ? ` (exit ${term.exitCode})` : ""}` : "no last-termination detail reported";
23948
+ const attrs = { "k8s.waitingReason": CRASH_LOOP_REASON, "k8s.restartCount": restarts };
23949
+ if (termReason) attrs["k8s.terminatedReason"] = termReason;
23950
+ if (termMsg) attrs["k8s.terminatedMessage"] = termMsg;
23951
+ if (typeof term?.exitCode === "number") attrs["k8s.exitCode"] = term.exitCode;
23952
+ if (typeof cs.image === "string") attrs["k8s.image"] = cs.image;
23953
+ return {
23954
+ fault: "crash-loop",
23955
+ message: `Deployment ${name} is crashlooping (restarts: ${restarts}); ${detail}`,
23956
+ timestamp: term?.finishedAt ?? pod.status?.startTime ?? nowIso2(),
23957
+ attributes: attrs
23958
+ };
23959
+ }
23960
+ return null;
23961
+ }
23962
+ function classifyDeployment(deployment, pods, expectedZero) {
23963
+ const name = deployment.metadata?.name ?? "";
23964
+ const desired = typeof deployment.spec?.replicas === "number" ? deployment.spec.replicas : 1;
23965
+ const ready = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
23966
+ if (desired === 0) {
23967
+ if (expectedZero?.has(name)) return null;
23968
+ return {
23969
+ fault: "scaled-to-zero",
23970
+ message: `Deployment ${name} is scaled to 0 \u2014 no running pods (desired 0)`,
23971
+ timestamp: nowIso2(),
23972
+ attributes: { "k8s.desiredReplicas": 0, "k8s.readyReplicas": ready }
23973
+ };
23974
+ }
23975
+ if (ready >= desired) return null;
23976
+ const podFault = podLevelFault(deployment, pods);
23977
+ if (podFault) {
23978
+ podFault.attributes["k8s.desiredReplicas"] = desired;
23979
+ podFault.attributes["k8s.readyReplicas"] = ready;
23980
+ return podFault;
23981
+ }
23982
+ return {
23983
+ fault: "no-ready-replicas",
23984
+ message: `Deployment ${name} has no ready replicas (desired ${desired}, ready ${ready})`,
23985
+ timestamp: nowIso2(),
23986
+ attributes: { "k8s.desiredReplicas": desired, "k8s.readyReplicas": ready }
23987
+ };
23988
+ }
23989
+ function mapDeploymentToSignal(deployment, pods, config) {
23990
+ const name = deployment.metadata?.name;
23991
+ if (typeof name !== "string" || name.length === 0) return null;
23992
+ const expectedZero = config.expectedZero ? new Set(config.expectedZero) : void 0;
23993
+ const finding = classifyDeployment(deployment, pods, expectedZero);
23994
+ if (!finding) return null;
23995
+ const serviceName = serviceNameFor(deployment, config);
23996
+ const namespace = deployment.metadata?.namespace ?? config.namespace;
23997
+ const attributes = {
23998
+ "k8s.namespace": namespace,
23999
+ "k8s.deployment": name,
24000
+ "k8s.fault": finding.fault,
24001
+ ...finding.attributes
24002
+ };
24003
+ return {
24004
+ targetKind: K8S_TARGET_KIND,
24005
+ targetName: packK8sTargetName({ serviceName, fault: finding.fault }),
24006
+ // Incident-only — no edge, so no call/error count to replay.
24007
+ callCount: 0,
24008
+ errorCount: 0,
24009
+ lastObservedIso: finding.timestamp,
24010
+ incident: {
24011
+ id: `k8s:deploy:${namespace}:${name}:${finding.fault}`,
24012
+ timestamp: finding.timestamp,
24013
+ service: serviceName,
24014
+ errorType: "k8s-deploy-failure",
24015
+ errorMessage: finding.message,
24016
+ attributes
24017
+ }
24018
+ };
24019
+ }
24020
+ function observedDeployState(deployment, pods) {
24021
+ const readyReplicas = typeof deployment.status?.readyReplicas === "number" ? deployment.status.readyReplicas : 0;
24022
+ let image;
24023
+ for (const pod of podsForDeployment(deployment, pods)) {
24024
+ for (const cs of pod.status?.containerStatuses ?? []) {
24025
+ if (cs.state?.running && typeof cs.image === "string" && cs.image.length > 0) {
24026
+ image = cs.image;
24027
+ break;
24028
+ }
24029
+ }
24030
+ if (image) break;
24031
+ }
24032
+ return image !== void 0 ? { image, readyReplicas } : { readyReplicas };
24033
+ }
24034
+ function deployStateSignal(deployment, pods, config) {
24035
+ const name = deployment.metadata?.name;
24036
+ if (typeof name !== "string" || name.length === 0) return null;
24037
+ const serviceName = serviceNameFor(deployment, config);
24038
+ return {
24039
+ targetKind: K8S_TARGET_KIND,
24040
+ targetName: packK8sTargetName({ serviceName, fault: K8S_DEPLOY_STATE }),
24041
+ callCount: 0,
24042
+ errorCount: 0,
24043
+ lastObservedIso: nowIso2(),
24044
+ deployState: observedDeployState(deployment, pods)
24045
+ };
24046
+ }
24047
+ function mapWorkloadsToSignals(deployments, pods, config) {
24048
+ const out = [];
24049
+ for (const deployment of deployments) {
24050
+ const deployState = deployStateSignal(deployment, pods, config);
24051
+ if (deployState) out.push(deployState);
24052
+ const incident = mapDeploymentToSignal(deployment, pods, config);
24053
+ if (incident) out.push(incident);
24054
+ }
24055
+ return out;
24056
+ }
23681
24057
 
23682
24058
  // src/connectors/kubernetes/resolve.ts
23683
24059
  init_cjs_shims();
23684
24060
  var import_types103 = require("@neat.is/types");
24061
+ var NO_ENV3 = "unknown";
24062
+ function createK8sResolveTarget(graph) {
24063
+ return (signal) => {
24064
+ if (signal.targetKind !== K8S_TARGET_KIND) return null;
24065
+ const identity = parseK8sTargetName(signal.targetName);
24066
+ if (!identity) return null;
24067
+ const { serviceName } = identity;
24068
+ return {
24069
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV3),
24070
+ serviceName,
24071
+ edgeType: import_types103.EdgeType.CALLS
24072
+ };
24073
+ };
24074
+ }
23685
24075
 
23686
24076
  // src/connectors/kubernetes/substrate.ts
23687
24077
  init_cjs_shims();
23688
24078
  var import_promises = require("fs/promises");
23689
24079
  var import_node_os5 = __toESM(require("os"), 1);
23690
24080
  var import_node_path77 = __toESM(require("path"), 1);
24081
+ function defaultHome() {
24082
+ const override = process.env.NEAT_HOME;
24083
+ if (override && override.length > 0) return import_node_path77.default.resolve(override);
24084
+ return import_node_path77.default.join(import_node_os5.default.homedir(), ".neat");
24085
+ }
24086
+ function k8sSubstrateConfigPath(home = defaultHome()) {
24087
+ return import_node_path77.default.join(home, "k8s.json");
24088
+ }
24089
+ async function readK8sSubstrateConfig(home = defaultHome()) {
24090
+ let raw;
24091
+ try {
24092
+ raw = await (0, import_promises.readFile)(k8sSubstrateConfigPath(home), "utf8");
24093
+ } catch {
24094
+ return { version: 1, deployments: [] };
24095
+ }
24096
+ try {
24097
+ const parsed = JSON.parse(raw);
24098
+ if (!parsed || !Array.isArray(parsed.deployments)) return { version: 1, deployments: [] };
24099
+ return parsed;
24100
+ } catch {
24101
+ return { version: 1, deployments: [] };
24102
+ }
24103
+ }
24104
+ async function startK8sSubstratePolling(input) {
24105
+ const config = await readK8sSubstrateConfig(input.home);
24106
+ const env = input.env ?? process.env;
24107
+ const stops = [];
24108
+ for (const entry2 of config.deployments) {
24109
+ if (entry2.project !== void 0 && entry2.project !== input.project) continue;
24110
+ if (typeof entry2.namespace !== "string" || entry2.namespace.length === 0) {
24111
+ input.onSkip?.(entry2, "missing namespace");
24112
+ continue;
24113
+ }
24114
+ let credentials;
24115
+ try {
24116
+ const resolved = resolveCredential(entry2.credential, env);
24117
+ credentials = resolved.kind === "fields" ? { ...resolved.fields } : { token: resolved.value };
24118
+ } catch (err) {
24119
+ input.onSkip?.(entry2, err.message);
24120
+ continue;
24121
+ }
24122
+ const cfg = {
24123
+ namespace: entry2.namespace,
24124
+ ...entry2.apiServerUrl ? { apiServerUrl: entry2.apiServerUrl } : {},
24125
+ ...entry2.caCert ? { caCert: entry2.caCert } : {},
24126
+ ...entry2.insecureSkipTlsVerify ? { insecureSkipTlsVerify: true } : {},
24127
+ ...entry2.serviceMap ? { serviceMap: entry2.serviceMap } : {},
24128
+ ...entry2.expectedZero ? { expectedZero: entry2.expectedZero } : {}
24129
+ };
24130
+ const { connector, resolveTarget } = createKubernetesConnector(input.graph, cfg, input.fetchImpl);
24131
+ const stop = startConnectorPollLoop(
24132
+ connector,
24133
+ {
24134
+ projectDir: input.projectDir,
24135
+ project: input.project,
24136
+ credentials,
24137
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
24138
+ },
24139
+ input.graph,
24140
+ resolveTarget,
24141
+ { connectorId: `k8s:${entry2.id}`, ...entry2.intervalMs ? { intervalMs: entry2.intervalMs } : {} }
24142
+ );
24143
+ stops.push(stop);
24144
+ }
24145
+ return () => {
24146
+ for (const stop of stops) stop();
24147
+ };
24148
+ }
24149
+
24150
+ // src/connectors/kubernetes/index.ts
24151
+ var KubernetesConnector = class {
24152
+ constructor(config, fetchImpl) {
24153
+ this.config = config;
24154
+ this.fetchImpl = fetchImpl;
24155
+ }
24156
+ config;
24157
+ fetchImpl;
24158
+ provider = "kubernetes";
24159
+ async poll(ctx) {
24160
+ const creds = readK8sCredentials(ctx.credentials);
24161
+ const transport = resolveK8sTransport(creds, this.config);
24162
+ const namespace = this.config.namespace;
24163
+ const opts = {
24164
+ ...this.config.apiUrl ? { apiUrl: this.config.apiUrl } : {},
24165
+ ...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
24166
+ };
24167
+ const [deployments, pods] = await Promise.all([
24168
+ fetchDeployments(transport, namespace, opts),
24169
+ fetchPods(transport, namespace, opts)
24170
+ ]);
24171
+ return mapWorkloadsToSignals(deployments, pods, this.config);
24172
+ }
24173
+ };
24174
+ function createKubernetesConnector(graph, config, fetchImpl) {
24175
+ return {
24176
+ connector: new KubernetesConnector(config, fetchImpl),
24177
+ resolveTarget: createK8sResolveTarget(graph)
24178
+ };
24179
+ }
23691
24180
 
23692
24181
  // src/daemon.ts
24182
+ init_cjs_shims();
24183
+ var import_node_fs43 = require("fs");
24184
+ var import_node_path79 = __toESM(require("path"), 1);
24185
+ var import_node_module = require("module");
24186
+ init_otel();
23693
24187
  init_auth();
23694
24188
 
23695
24189
  // src/unrouted.ts
@@ -24364,6 +24858,14 @@ async function startWatch(graph, opts) {
24364
24858
  `neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
24365
24859
  )
24366
24860
  });
24861
+ const stopK8sSubstrate = await startK8sSubstratePolling({
24862
+ project: projectName,
24863
+ graph,
24864
+ projectDir: opts.scanPath,
24865
+ errorsPath: opts.errorsPath,
24866
+ ...opts.neatHome ? { home: opts.neatHome } : {},
24867
+ onSkip: (skipped, reason) => console.warn(`neat watch: k8s substrate "${skipped.id}" skipped for project "${projectName}" \u2014 ${reason}`)
24868
+ });
24367
24869
  const auth = readAuthEnv();
24368
24870
  const host = opts.host ?? (auth.authToken ? "0.0.0.0" : "127.0.0.1");
24369
24871
  assertBindAuthority(host, auth.authToken);
@@ -24550,6 +25052,7 @@ async function startWatch(graph, opts) {
24550
25052
  }
24551
25053
  await watcher.close();
24552
25054
  stopConnectors();
25055
+ stopK8sSubstrate();
24553
25056
  stopStaleness();
24554
25057
  stopPersist();
24555
25058
  detachEventBus();
@@ -28008,8 +28511,8 @@ async function waitForPeerDaemon(restPort, project, timeoutMs) {
28008
28511
  async function healthIsForProject(restPort, project) {
28009
28512
  const body = await fetchDaemonHealth(restPort);
28010
28513
  if (body === null) return false;
28011
- const named = body.project;
28012
- if (typeof named === "string") return named === project;
28514
+ const named2 = body.project;
28515
+ if (typeof named2 === "string") return named2 === project;
28013
28516
  if (Array.isArray(body.projects)) {
28014
28517
  return body.projects.some((p) => p.name === project);
28015
28518
  }
@@ -29178,6 +29681,12 @@ function formatDivergenceLine(d) {
29178
29681
  const at = d.location ? ` at ${d.location}` : "";
29179
29682
  return ` \u2022 [${d.type}] ${d.source}${at} (${d.failureKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
29180
29683
  }
29684
+ case "deploy-mismatch": {
29685
+ if (d.kind === "image") {
29686
+ return ` \u2022 [${d.type}] ${d.source} \u2014 declared image ${d.declaredImage ?? "?"}, running ${d.observedImage ?? "?"} \u2014 confidence ${d.confidence.toFixed(2)}`;
29687
+ }
29688
+ return ` \u2022 [${d.type}] ${d.source} \u2014 declared ${d.declaredReplicas ?? "?"} replicas, ${d.observedReplicas ?? "?"} ready \u2014 confidence ${d.confidence.toFixed(2)}`;
29689
+ }
29181
29690
  }
29182
29691
  }
29183
29692
  async function runDivergences(client, input) {