@neat.is/core 0.9.3-dev.20260823 → 0.9.3

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
@@ -1933,6 +1933,47 @@ function classifyNode(ctx) {
1933
1933
  function isVictimSeed(ctx) {
1934
1934
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
1935
1935
  }
1936
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
1937
+ "name resolution",
1938
+ "resolve host",
1939
+ "getaddrinfo",
1940
+ "enotfound",
1941
+ "connection refused",
1942
+ "econnrefused",
1943
+ "connection reset",
1944
+ "econnreset",
1945
+ "able to connect",
1946
+ "failed to connect",
1947
+ "cannot connect",
1948
+ "could not connect",
1949
+ "unable to connect",
1950
+ "connection timed out",
1951
+ "etimedout",
1952
+ "no route to host",
1953
+ "host unreachable",
1954
+ "network is unreachable",
1955
+ "connection closed"
1956
+ ];
1957
+ function incidentTextIndicatesOutboundFailure(ev) {
1958
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
1959
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
1960
+ }
1961
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
1962
+ for (const n of nodeScope(graph, nodeId)) {
1963
+ if (!graph.hasNode(n)) continue;
1964
+ for (const edgeId of graph.outboundEdges(n)) {
1965
+ const e = graph.getEdgeAttributes(edgeId);
1966
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1967
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
1968
+ }
1969
+ }
1970
+ if (seedSource === "incident" && incidents) {
1971
+ for (const ev of incidents) {
1972
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
1973
+ }
1974
+ }
1975
+ return false;
1976
+ }
1936
1977
  function grainOf(graph, nodeId) {
1937
1978
  if (!graph.hasNode(nodeId)) return "unknown";
1938
1979
  const t = graph.getNodeAttributes(nodeId).type;
@@ -2096,7 +2137,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2096
2137
  const candidates = [];
2097
2138
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
2098
2139
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
2099
- if (seedCtx && isVictimSeed(seedCtx)) {
2140
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
2141
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
2100
2142
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
2101
2143
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
2102
2144
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -6868,15 +6910,36 @@ function startStalenessLoop(graph, options = {}) {
6868
6910
  clearInterval(interval);
6869
6911
  };
6870
6912
  }
6871
- async function readErrorEvents(errorsPath) {
6913
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
6914
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
6915
+ async function readErrorFileTail(errorsPath, maxBytes) {
6916
+ const handle = await import_node_fs8.promises.open(errorsPath, "r");
6917
+ try {
6918
+ const { size } = await handle.stat();
6919
+ if (size <= maxBytes) {
6920
+ return (await handle.readFile()).toString("utf8");
6921
+ }
6922
+ const buf = Buffer.alloc(maxBytes);
6923
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
6924
+ const raw = buf.toString("utf8");
6925
+ const firstNewline = raw.indexOf("\n");
6926
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
6927
+ } finally {
6928
+ await handle.close();
6929
+ }
6930
+ }
6931
+ async function readErrorEvents(errorsPath, opts) {
6932
+ let raw;
6872
6933
  try {
6873
- const raw = await import_node_fs8.promises.readFile(errorsPath, "utf8");
6874
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6875
- return dedupeIncidents(events);
6934
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
6876
6935
  } catch (err) {
6877
6936
  if (err.code === "ENOENT") return [];
6878
6937
  throw err;
6879
6938
  }
6939
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6940
+ const deduped = dedupeIncidents(events);
6941
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
6942
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
6880
6943
  }
6881
6944
  function isSynthesizedHttpIncident(ev) {
6882
6945
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -15981,6 +16044,120 @@ function detectColumnDrift(node) {
15981
16044
  }
15982
16045
  return out;
15983
16046
  }
16047
+ var SYMBOL_MISMATCH_PATTERNS = [
16048
+ {
16049
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
16050
+ kind: "missing-attribute",
16051
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
16052
+ },
16053
+ {
16054
+ // "object has no field 'X'", "no such field X", "unknown field X".
16055
+ kind: "missing-field",
16056
+ patterns: [
16057
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
16058
+ ]
16059
+ },
16060
+ {
16061
+ // "has no property X", "no property named X".
16062
+ kind: "missing-property",
16063
+ patterns: [
16064
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
16065
+ ]
16066
+ },
16067
+ {
16068
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
16069
+ kind: "missing-column",
16070
+ patterns: [
16071
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
16072
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
16073
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
16074
+ ]
16075
+ },
16076
+ {
16077
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
16078
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
16079
+ // mismatch) does not get miscategorised here.
16080
+ kind: "undefined-method",
16081
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
16082
+ }
16083
+ ];
16084
+ function classifySymbolMismatch(message) {
16085
+ for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
16086
+ for (const re of entry2.patterns) {
16087
+ const m = re.exec(message);
16088
+ if (m) {
16089
+ const captured = m[1];
16090
+ return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
16091
+ }
16092
+ }
16093
+ }
16094
+ return null;
16095
+ }
16096
+ function symbolLocus(graph, ev) {
16097
+ const attrs = ev.attributes ?? {};
16098
+ const filepath = codeFilepathOf(attrs);
16099
+ const lineno = codeLinenoOf(attrs);
16100
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
16101
+ const affected = ev.affectedNode;
16102
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
16103
+ const affectedIsCode = affectedInGraph && ((0, import_types57.parseSymbolId)(affected) !== null || (0, import_types57.parseFileId)(affected) !== null);
16104
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
16105
+ if (!location) return null;
16106
+ if (affectedInGraph) return { node: affected, location };
16107
+ const svc = (0, import_types57.serviceId)(ev.service);
16108
+ if (graph.hasNode(svc)) return { node: svc, location };
16109
+ return null;
16110
+ }
16111
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
16112
+ function detectSymbolMismatches(graph, incidents) {
16113
+ const groups = /* @__PURE__ */ new Map();
16114
+ for (const ev of incidents) {
16115
+ const classified = classifySymbolMismatch(ev.errorMessage);
16116
+ if (!classified) continue;
16117
+ const locus = symbolLocus(graph, ev);
16118
+ if (!locus) continue;
16119
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
16120
+ const existing = groups.get(key);
16121
+ if (!existing) {
16122
+ groups.set(key, {
16123
+ node: locus.node,
16124
+ kind: classified.kind,
16125
+ ...classified.symbol ? { symbol: classified.symbol } : {},
16126
+ ...locus.location ? { location: locus.location } : {},
16127
+ latest: ev,
16128
+ count: 1
16129
+ });
16130
+ } else {
16131
+ existing.count += 1;
16132
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
16133
+ existing.latest = ev;
16134
+ if (locus.location) existing.location = locus.location;
16135
+ }
16136
+ }
16137
+ }
16138
+ const out = [];
16139
+ for (const g of groups.values()) {
16140
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
16141
+ const where = g.location ? ` at ${g.location}` : "";
16142
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
16143
+ out.push({
16144
+ type: "observed-symbol-mismatch",
16145
+ source: g.node,
16146
+ target: g.node,
16147
+ mismatchKind: g.kind,
16148
+ ...g.symbol ? { symbol: g.symbol } : {},
16149
+ ...g.location ? { location: g.location } : {},
16150
+ provenance: import_types57.Provenance.INFERRED,
16151
+ incidentId: g.latest.id,
16152
+ errorMessage: g.latest.errorMessage,
16153
+ incidentCount: g.count,
16154
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
16155
+ reason: `Code${where} declares access to ${member} the runtime object does not have \u2014 ${g.latest.service} raised "${g.latest.errorMessage}"${times}. The declared access (EXTRACTED) and the runtime shape (OBSERVED) disagree at symbol grain.`,
16156
+ recommendation: "Reconcile the declared access with the runtime shape: a field, attribute, method, or column was renamed, removed, or never existed on the object this code reaches. Update the code to the current shape, or restore the member."
16157
+ });
16158
+ }
16159
+ return out;
16160
+ }
15984
16161
  function involvesNode(d, nodeId) {
15985
16162
  return d.source === nodeId || d.target === nodeId;
15986
16163
  }
@@ -16066,6 +16243,9 @@ function computeDivergences(graph, opts = {}) {
16066
16243
  for (const d of detectColumnDrift(n)) all.push(d);
16067
16244
  }
16068
16245
  });
16246
+ if (opts.incidents && opts.incidents.length > 0) {
16247
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
16248
+ }
16069
16249
  const reconciled = suppressHostMismatchHalves(all);
16070
16250
  const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
16071
16251
  let filtered = dampened;
@@ -16086,7 +16266,12 @@ function computeDivergences(graph, opts = {}) {
16086
16266
  "missing-observed": 1,
16087
16267
  "version-mismatch": 2,
16088
16268
  "host-mismatch": 3,
16089
- "compat-violation": 4
16269
+ "compat-violation": 4,
16270
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
16271
+ // type; this only breaks a confidence tie, and it orders last so a same-
16272
+ // confidence edge finding leads. In practice it carries the INFERRED grade
16273
+ // (0.6), so it sits below the high-confidence edge divergences already.
16274
+ "observed-symbol-mismatch": 5
16090
16275
  };
16091
16276
  filtered.sort((a, b) => {
16092
16277
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -16097,7 +16282,10 @@ function computeDivergences(graph, opts = {}) {
16097
16282
  if (a.target !== b.target) return a.target.localeCompare(b.target);
16098
16283
  const ac = "column" in a && a.column ? a.column : "";
16099
16284
  const bc = "column" in b && b.column ? b.column : "";
16100
- return ac.localeCompare(bc);
16285
+ if (ac !== bc) return ac.localeCompare(bc);
16286
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
16287
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
16288
+ return asym.localeCompare(bsym);
16101
16289
  });
16102
16290
  return import_types57.DivergenceResultSchema.parse({
16103
16291
  divergences: filtered,
@@ -17157,10 +17345,15 @@ function divergenceLine(d) {
17157
17345
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
17158
17346
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
17159
17347
  }
17348
+ if (d.type === "observed-symbol-mismatch") {
17349
+ const at = d.location ? ` at ${d.location}` : "";
17350
+ const member = d.symbol ? ` ${d.symbol}` : "";
17351
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
17352
+ }
17160
17353
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
17161
17354
  }
17162
- function buildDivergenceSection(graph, node) {
17163
- const result = computeDivergences(graph, { node });
17355
+ function buildDivergenceSection(graph, node, incidents) {
17356
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
17164
17357
  if (result.totalAffected === 0) return null;
17165
17358
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17166
17359
  text: divergenceLine(d),
@@ -17173,8 +17366,8 @@ function buildDivergenceSection(graph, node) {
17173
17366
  facts
17174
17367
  };
17175
17368
  }
17176
- function buildGlobalDivergenceSection(graph) {
17177
- const result = computeDivergences(graph);
17369
+ function buildGlobalDivergenceSection(graph, incidents) {
17370
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
17178
17371
  if (result.totalAffected === 0) {
17179
17372
  return {
17180
17373
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -17272,7 +17465,7 @@ function buildOverviewSections(graph, incidents) {
17272
17465
  }))
17273
17466
  });
17274
17467
  }
17275
- const div = computeDivergences(graph);
17468
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
17276
17469
  sections.push({
17277
17470
  heading: "Divergences",
17278
17471
  facts: [
@@ -17286,7 +17479,7 @@ function buildOverviewSections(graph, incidents) {
17286
17479
  function buildGlobalSections(intent, graph, incidents) {
17287
17480
  switch (intent) {
17288
17481
  case "divergence":
17289
- return [buildGlobalDivergenceSection(graph)];
17482
+ return [buildGlobalDivergenceSection(graph, incidents)];
17290
17483
  case "incidents":
17291
17484
  return [buildGlobalIncidentsSection(incidents)];
17292
17485
  case "overview":
@@ -17317,7 +17510,7 @@ function buildSection(kind, graph, node, incidents, now) {
17317
17510
  case "incidents":
17318
17511
  return buildIncidentsSection(node, incidents);
17319
17512
  case "divergence":
17320
- return buildDivergenceSection(graph, node);
17513
+ return buildDivergenceSection(graph, node, incidents);
17321
17514
  }
17322
17515
  }
17323
17516
  function summarizeGlobal(intent, sections) {
@@ -21537,6 +21730,8 @@ async function deprovisionConnector(entry2, env = process.env, fetchImpl) {
21537
21730
  }
21538
21731
 
21539
21732
  // src/api.ts
21733
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
21734
+ var INCIDENT_LIST_MAX_LIMIT = 200;
21540
21735
  function serializeGraph(graph) {
21541
21736
  const nodes = [];
21542
21737
  graph.forEachNode((_id, attrs) => {
@@ -21716,10 +21911,13 @@ function registerRoutes(scope, ctx) {
21716
21911
  }
21717
21912
  minConfidence = n;
21718
21913
  }
21914
+ const epath = errorsPathFor(proj);
21915
+ const incidents = epath ? await readErrorEvents(epath) : [];
21719
21916
  return computeDivergences(proj.graph, {
21720
21917
  ...typeFilter ? { type: typeFilter } : {},
21721
21918
  ...minConfidence !== void 0 ? { minConfidence } : {},
21722
- ...req.query.node ? { node: req.query.node } : {}
21919
+ ...req.query.node ? { node: req.query.node } : {},
21920
+ incidents
21723
21921
  });
21724
21922
  });
21725
21923
  scope.get("/incidents", async (req, reply) => {
@@ -21729,10 +21927,11 @@ function registerRoutes(scope, ctx) {
21729
21927
  if (!epath) return { count: 0, total: 0, events: [] };
21730
21928
  const events = await readErrorEvents(epath);
21731
21929
  const total = events.length;
21732
- const limit = req.query.limit ? Number(req.query.limit) : 50;
21733
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
21930
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21931
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21734
21932
  const sliced = events.slice(0, safeLimit);
21735
- return { count: sliced.length, total, events: sliced };
21933
+ const omitted = total - sliced.length;
21934
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
21736
21935
  });
21737
21936
  scope.get("/stale-events", async (req, reply) => {
21738
21937
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -21841,16 +22040,20 @@ function registerRoutes(scope, ctx) {
21841
22040
  const filtered = events.filter(
21842
22041
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
21843
22042
  );
21844
- return { count: filtered.length, total: filtered.length, events: filtered };
22043
+ const total = filtered.length;
22044
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
22045
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
22046
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
22047
+ const omitted = total - recent.length;
22048
+ return {
22049
+ count: recent.length,
22050
+ total,
22051
+ events: recent,
22052
+ ...omitted > 0 ? { omitted } : {}
22053
+ };
21845
22054
  };
21846
- scope.get(
21847
- "/incidents/:nodeId",
21848
- incidentHistoryHandler
21849
- );
21850
- scope.get(
21851
- "/graph/incident-history/:nodeId",
21852
- incidentHistoryHandler
21853
- );
22055
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
22056
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
21854
22057
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
21855
22058
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21856
22059
  if (!proj) return;
@@ -28841,6 +29044,11 @@ function formatDivergenceLine(d) {
28841
29044
  return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
28842
29045
  case "compat-violation":
28843
29046
  return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
29047
+ case "observed-symbol-mismatch": {
29048
+ const at = d.location ? ` at ${d.location}` : "";
29049
+ const member = d.symbol ? ` ${d.symbol}` : "";
29050
+ return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
29051
+ }
28844
29052
  }
28845
29053
  }
28846
29054
  async function runDivergences(client, input) {
@@ -28994,6 +29202,11 @@ function formatDivergenceLine2(d) {
28994
29202
  return `\u26A0 divergence [host-mismatch] ${d.source} \u2192 ${d.target} declared host ${d.extractedHost}, observed host ${d.observedHost}`;
28995
29203
  case "compat-violation":
28996
29204
  return `\u26A0 divergence [compat-violation] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
29205
+ case "observed-symbol-mismatch": {
29206
+ const at = d.location ? ` at ${d.location}` : "";
29207
+ const member = d.symbol ? ` ${d.symbol}` : " a member";
29208
+ return `\u26A0 divergence [observed-symbol-mismatch] ${d.source}${at} reads${member} the runtime object does not have (${d.mismatchKind})`;
29209
+ }
28997
29210
  }
28998
29211
  }
28999
29212
  function formatStaleLine(edgeId) {
@@ -29970,7 +30183,8 @@ async function runInit(opts) {
29970
30183
  console.log(`snapshot: ${opts.outPath}`);
29971
30184
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
29972
30185
  console.log("");
29973
- const divergenceResult = computeDivergences(graph);
30186
+ const summaryIncidents = await readErrorEvents(errorsPath);
30187
+ const divergenceResult = computeDivergences(graph, { incidents: summaryIncidents });
29974
30188
  console.log(
29975
30189
  renderValueForwardSummary({
29976
30190
  graph,