@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/neatd.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  reconcileDaemonRecordSync,
4
4
  startDaemon
5
- } from "./chunk-WJGZYEUG.js";
5
+ } from "./chunk-2D4Y5QHE.js";
6
6
  import {
7
7
  listProjects,
8
8
  registryPath
9
- } from "./chunk-LRYPKC3B.js";
9
+ } from "./chunk-EDE4XP2M.js";
10
10
  import {
11
11
  BindAuthorityError,
12
12
  __require
package/dist/server.cjs CHANGED
@@ -6115,15 +6115,36 @@ function startStalenessLoop(graph, options = {}) {
6115
6115
  clearInterval(interval);
6116
6116
  };
6117
6117
  }
6118
- async function readErrorEvents(errorsPath) {
6118
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
6119
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
6120
+ async function readErrorFileTail(errorsPath, maxBytes) {
6121
+ const handle = await import_node_fs9.promises.open(errorsPath, "r");
6119
6122
  try {
6120
- const raw = await import_node_fs9.promises.readFile(errorsPath, "utf8");
6121
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6122
- return dedupeIncidents(events);
6123
+ const { size } = await handle.stat();
6124
+ if (size <= maxBytes) {
6125
+ return (await handle.readFile()).toString("utf8");
6126
+ }
6127
+ const buf = Buffer.alloc(maxBytes);
6128
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
6129
+ const raw = buf.toString("utf8");
6130
+ const firstNewline = raw.indexOf("\n");
6131
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
6132
+ } finally {
6133
+ await handle.close();
6134
+ }
6135
+ }
6136
+ async function readErrorEvents(errorsPath, opts) {
6137
+ let raw;
6138
+ try {
6139
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
6123
6140
  } catch (err) {
6124
6141
  if (err.code === "ENOENT") return [];
6125
6142
  throw err;
6126
6143
  }
6144
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6145
+ const deduped = dedupeIncidents(events);
6146
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
6147
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
6127
6148
  }
6128
6149
  function isSynthesizedHttpIncident(ev) {
6129
6150
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -6883,6 +6904,47 @@ function classifyNode(ctx) {
6883
6904
  function isVictimSeed(ctx) {
6884
6905
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
6885
6906
  }
6907
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
6908
+ "name resolution",
6909
+ "resolve host",
6910
+ "getaddrinfo",
6911
+ "enotfound",
6912
+ "connection refused",
6913
+ "econnrefused",
6914
+ "connection reset",
6915
+ "econnreset",
6916
+ "able to connect",
6917
+ "failed to connect",
6918
+ "cannot connect",
6919
+ "could not connect",
6920
+ "unable to connect",
6921
+ "connection timed out",
6922
+ "etimedout",
6923
+ "no route to host",
6924
+ "host unreachable",
6925
+ "network is unreachable",
6926
+ "connection closed"
6927
+ ];
6928
+ function incidentTextIndicatesOutboundFailure(ev) {
6929
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
6930
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
6931
+ }
6932
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
6933
+ for (const n of nodeScope(graph, nodeId)) {
6934
+ if (!graph.hasNode(n)) continue;
6935
+ for (const edgeId of graph.outboundEdges(n)) {
6936
+ const e = graph.getEdgeAttributes(edgeId);
6937
+ if (e.type === import_types8.EdgeType.CONTAINS) continue;
6938
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
6939
+ }
6940
+ }
6941
+ if (seedSource === "incident" && incidents) {
6942
+ for (const ev of incidents) {
6943
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
6944
+ }
6945
+ }
6946
+ return false;
6947
+ }
6886
6948
  function grainOf(graph, nodeId) {
6887
6949
  if (!graph.hasNode(nodeId)) return "unknown";
6888
6950
  const t = graph.getNodeAttributes(nodeId).type;
@@ -7046,7 +7108,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
7046
7108
  const candidates = [];
7047
7109
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
7048
7110
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
7049
- if (seedCtx && isVictimSeed(seedCtx)) {
7111
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
7112
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
7050
7113
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
7051
7114
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
7052
7115
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -7396,6 +7459,120 @@ function detectColumnDrift(node) {
7396
7459
  }
7397
7460
  return out;
7398
7461
  }
7462
+ var SYMBOL_MISMATCH_PATTERNS = [
7463
+ {
7464
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
7465
+ kind: "missing-attribute",
7466
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
7467
+ },
7468
+ {
7469
+ // "object has no field 'X'", "no such field X", "unknown field X".
7470
+ kind: "missing-field",
7471
+ patterns: [
7472
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
7473
+ ]
7474
+ },
7475
+ {
7476
+ // "has no property X", "no property named X".
7477
+ kind: "missing-property",
7478
+ patterns: [
7479
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
7480
+ ]
7481
+ },
7482
+ {
7483
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
7484
+ kind: "missing-column",
7485
+ patterns: [
7486
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
7487
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
7488
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
7489
+ ]
7490
+ },
7491
+ {
7492
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
7493
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
7494
+ // mismatch) does not get miscategorised here.
7495
+ kind: "undefined-method",
7496
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
7497
+ }
7498
+ ];
7499
+ function classifySymbolMismatch(message) {
7500
+ for (const entry of SYMBOL_MISMATCH_PATTERNS) {
7501
+ for (const re of entry.patterns) {
7502
+ const m = re.exec(message);
7503
+ if (m) {
7504
+ const captured = m[1];
7505
+ return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
7506
+ }
7507
+ }
7508
+ }
7509
+ return null;
7510
+ }
7511
+ function symbolLocus(graph, ev) {
7512
+ const attrs = ev.attributes ?? {};
7513
+ const filepath = codeFilepathOf(attrs);
7514
+ const lineno = codeLinenoOf(attrs);
7515
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
7516
+ const affected = ev.affectedNode;
7517
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
7518
+ const affectedIsCode = affectedInGraph && ((0, import_types9.parseSymbolId)(affected) !== null || (0, import_types9.parseFileId)(affected) !== null);
7519
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
7520
+ if (!location) return null;
7521
+ if (affectedInGraph) return { node: affected, location };
7522
+ const svc = (0, import_types9.serviceId)(ev.service);
7523
+ if (graph.hasNode(svc)) return { node: svc, location };
7524
+ return null;
7525
+ }
7526
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
7527
+ function detectSymbolMismatches(graph, incidents) {
7528
+ const groups = /* @__PURE__ */ new Map();
7529
+ for (const ev of incidents) {
7530
+ const classified = classifySymbolMismatch(ev.errorMessage);
7531
+ if (!classified) continue;
7532
+ const locus = symbolLocus(graph, ev);
7533
+ if (!locus) continue;
7534
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
7535
+ const existing = groups.get(key);
7536
+ if (!existing) {
7537
+ groups.set(key, {
7538
+ node: locus.node,
7539
+ kind: classified.kind,
7540
+ ...classified.symbol ? { symbol: classified.symbol } : {},
7541
+ ...locus.location ? { location: locus.location } : {},
7542
+ latest: ev,
7543
+ count: 1
7544
+ });
7545
+ } else {
7546
+ existing.count += 1;
7547
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
7548
+ existing.latest = ev;
7549
+ if (locus.location) existing.location = locus.location;
7550
+ }
7551
+ }
7552
+ }
7553
+ const out = [];
7554
+ for (const g of groups.values()) {
7555
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
7556
+ const where = g.location ? ` at ${g.location}` : "";
7557
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
7558
+ out.push({
7559
+ type: "observed-symbol-mismatch",
7560
+ source: g.node,
7561
+ target: g.node,
7562
+ mismatchKind: g.kind,
7563
+ ...g.symbol ? { symbol: g.symbol } : {},
7564
+ ...g.location ? { location: g.location } : {},
7565
+ provenance: import_types9.Provenance.INFERRED,
7566
+ incidentId: g.latest.id,
7567
+ errorMessage: g.latest.errorMessage,
7568
+ incidentCount: g.count,
7569
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
7570
+ 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.`,
7571
+ 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."
7572
+ });
7573
+ }
7574
+ return out;
7575
+ }
7399
7576
  function involvesNode(d, nodeId) {
7400
7577
  return d.source === nodeId || d.target === nodeId;
7401
7578
  }
@@ -7481,6 +7658,9 @@ function computeDivergences(graph, opts = {}) {
7481
7658
  for (const d of detectColumnDrift(n)) all.push(d);
7482
7659
  }
7483
7660
  });
7661
+ if (opts.incidents && opts.incidents.length > 0) {
7662
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
7663
+ }
7484
7664
  const reconciled = suppressHostMismatchHalves(all);
7485
7665
  const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
7486
7666
  let filtered = dampened;
@@ -7501,7 +7681,12 @@ function computeDivergences(graph, opts = {}) {
7501
7681
  "missing-observed": 1,
7502
7682
  "version-mismatch": 2,
7503
7683
  "host-mismatch": 3,
7504
- "compat-violation": 4
7684
+ "compat-violation": 4,
7685
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
7686
+ // type; this only breaks a confidence tie, and it orders last so a same-
7687
+ // confidence edge finding leads. In practice it carries the INFERRED grade
7688
+ // (0.6), so it sits below the high-confidence edge divergences already.
7689
+ "observed-symbol-mismatch": 5
7505
7690
  };
7506
7691
  filtered.sort((a, b) => {
7507
7692
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -7512,7 +7697,10 @@ function computeDivergences(graph, opts = {}) {
7512
7697
  if (a.target !== b.target) return a.target.localeCompare(b.target);
7513
7698
  const ac = "column" in a && a.column ? a.column : "";
7514
7699
  const bc = "column" in b && b.column ? b.column : "";
7515
- return ac.localeCompare(bc);
7700
+ if (ac !== bc) return ac.localeCompare(bc);
7701
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
7702
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
7703
+ return asym.localeCompare(bsym);
7516
7704
  });
7517
7705
  return import_types9.DivergenceResultSchema.parse({
7518
7706
  divergences: filtered,
@@ -16692,10 +16880,15 @@ function divergenceLine(d) {
16692
16880
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16693
16881
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16694
16882
  }
16883
+ if (d.type === "observed-symbol-mismatch") {
16884
+ const at = d.location ? ` at ${d.location}` : "";
16885
+ const member = d.symbol ? ` ${d.symbol}` : "";
16886
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
16887
+ }
16695
16888
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16696
16889
  }
16697
- function buildDivergenceSection(graph, node) {
16698
- const result = computeDivergences(graph, { node });
16890
+ function buildDivergenceSection(graph, node, incidents) {
16891
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
16699
16892
  if (result.totalAffected === 0) return null;
16700
16893
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16701
16894
  text: divergenceLine(d),
@@ -16708,8 +16901,8 @@ function buildDivergenceSection(graph, node) {
16708
16901
  facts
16709
16902
  };
16710
16903
  }
16711
- function buildGlobalDivergenceSection(graph) {
16712
- const result = computeDivergences(graph);
16904
+ function buildGlobalDivergenceSection(graph, incidents) {
16905
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
16713
16906
  if (result.totalAffected === 0) {
16714
16907
  return {
16715
16908
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -16807,7 +17000,7 @@ function buildOverviewSections(graph, incidents) {
16807
17000
  }))
16808
17001
  });
16809
17002
  }
16810
- const div = computeDivergences(graph);
17003
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
16811
17004
  sections.push({
16812
17005
  heading: "Divergences",
16813
17006
  facts: [
@@ -16821,7 +17014,7 @@ function buildOverviewSections(graph, incidents) {
16821
17014
  function buildGlobalSections(intent, graph, incidents) {
16822
17015
  switch (intent) {
16823
17016
  case "divergence":
16824
- return [buildGlobalDivergenceSection(graph)];
17017
+ return [buildGlobalDivergenceSection(graph, incidents)];
16825
17018
  case "incidents":
16826
17019
  return [buildGlobalIncidentsSection(incidents)];
16827
17020
  case "overview":
@@ -16852,7 +17045,7 @@ function buildSection(kind, graph, node, incidents, now) {
16852
17045
  case "incidents":
16853
17046
  return buildIncidentsSection(node, incidents);
16854
17047
  case "divergence":
16855
- return buildDivergenceSection(graph, node);
17048
+ return buildDivergenceSection(graph, node, incidents);
16856
17049
  }
16857
17050
  }
16858
17051
  function summarizeGlobal(intent, sections) {
@@ -20657,6 +20850,8 @@ function buildRegistration(entry, graph, env = process.env) {
20657
20850
  }
20658
20851
 
20659
20852
  // src/api.ts
20853
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
20854
+ var INCIDENT_LIST_MAX_LIMIT = 200;
20660
20855
  function serializeGraph(graph) {
20661
20856
  const nodes = [];
20662
20857
  graph.forEachNode((_id, attrs) => {
@@ -20836,10 +21031,13 @@ function registerRoutes(scope, ctx) {
20836
21031
  }
20837
21032
  minConfidence = n;
20838
21033
  }
21034
+ const epath = errorsPathFor(proj);
21035
+ const incidents = epath ? await readErrorEvents(epath) : [];
20839
21036
  return computeDivergences(proj.graph, {
20840
21037
  ...typeFilter ? { type: typeFilter } : {},
20841
21038
  ...minConfidence !== void 0 ? { minConfidence } : {},
20842
- ...req.query.node ? { node: req.query.node } : {}
21039
+ ...req.query.node ? { node: req.query.node } : {},
21040
+ incidents
20843
21041
  });
20844
21042
  });
20845
21043
  scope.get("/incidents", async (req, reply) => {
@@ -20849,10 +21047,11 @@ function registerRoutes(scope, ctx) {
20849
21047
  if (!epath) return { count: 0, total: 0, events: [] };
20850
21048
  const events = await readErrorEvents(epath);
20851
21049
  const total = events.length;
20852
- const limit = req.query.limit ? Number(req.query.limit) : 50;
20853
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
21050
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21051
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
20854
21052
  const sliced = events.slice(0, safeLimit);
20855
- return { count: sliced.length, total, events: sliced };
21053
+ const omitted = total - sliced.length;
21054
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
20856
21055
  });
20857
21056
  scope.get("/stale-events", async (req, reply) => {
20858
21057
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -20961,16 +21160,20 @@ function registerRoutes(scope, ctx) {
20961
21160
  const filtered = events.filter(
20962
21161
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
20963
21162
  );
20964
- return { count: filtered.length, total: filtered.length, events: filtered };
21163
+ const total = filtered.length;
21164
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21165
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21166
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
21167
+ const omitted = total - recent.length;
21168
+ return {
21169
+ count: recent.length,
21170
+ total,
21171
+ events: recent,
21172
+ ...omitted > 0 ? { omitted } : {}
21173
+ };
20965
21174
  };
20966
- scope.get(
20967
- "/incidents/:nodeId",
20968
- incidentHistoryHandler
20969
- );
20970
- scope.get(
20971
- "/graph/incident-history/:nodeId",
20972
- incidentHistoryHandler
20973
- );
21175
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
21176
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
20974
21177
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
20975
21178
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20976
21179
  if (!proj) return;