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

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-TGCWMMF6.js";
6
6
  import {
7
7
  listProjects,
8
8
  registryPath
9
- } from "./chunk-LRYPKC3B.js";
9
+ } from "./chunk-IVVF37OU.js";
10
10
  import {
11
11
  BindAuthorityError,
12
12
  __require
package/dist/server.cjs CHANGED
@@ -4706,6 +4706,64 @@ function latencyPercentiles(hist) {
4706
4706
  return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
4707
4707
  }
4708
4708
 
4709
+ // src/stacktrace.ts
4710
+ init_cjs_shims();
4711
+ var FRAME_SHAPES = [
4712
+ // `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
4713
+ { re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
4714
+ // `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
4715
+ { re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
4716
+ // `at <path>:<line>:<col>` — anonymous call frame, most recent first.
4717
+ { re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
4718
+ // `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
4719
+ { re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
4720
+ ];
4721
+ var VENDOR_MARKERS = [
4722
+ "node_modules",
4723
+ // dependency root
4724
+ "site-packages",
4725
+ // installed-package root
4726
+ "dist-packages",
4727
+ // distro-packaged root
4728
+ "node:"
4729
+ // runtime-internal module scheme (a stdlib/runtime-root frame)
4730
+ ];
4731
+ function matchFrame(line) {
4732
+ for (const shape of FRAME_SHAPES) {
4733
+ const m = shape.re.exec(line);
4734
+ if (!m) continue;
4735
+ const file = m[shape.file];
4736
+ const lineNo = Number(m[shape.line]);
4737
+ if (!file || !Number.isFinite(lineNo)) continue;
4738
+ const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
4739
+ return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
4740
+ }
4741
+ return null;
4742
+ }
4743
+ function isApplicationFrame(file) {
4744
+ if (file.startsWith("<")) return false;
4745
+ const norm = file.split("\\").join("/");
4746
+ for (const marker of VENDOR_MARKERS) {
4747
+ if (norm.includes(marker)) return false;
4748
+ }
4749
+ return true;
4750
+ }
4751
+ function deepestApplicationFrame(stacktrace) {
4752
+ if (!stacktrace) return null;
4753
+ const appFrames = [];
4754
+ for (const raw of stacktrace.split("\n")) {
4755
+ const matched = matchFrame(raw);
4756
+ if (!matched) continue;
4757
+ if (!isApplicationFrame(matched.frame.file)) continue;
4758
+ appFrames.push(matched);
4759
+ }
4760
+ if (appFrames.length === 0) return null;
4761
+ const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
4762
+ const oriented = appFrames.filter((f) => f.deepest === orientation);
4763
+ const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
4764
+ return chosen ? chosen.frame : null;
4765
+ }
4766
+
4709
4767
  // src/ingest.ts
4710
4768
  var HOUR_MS = 60 * 60 * 1e3;
4711
4769
  var DAY_MS = 24 * HOUR_MS;
@@ -5557,29 +5615,71 @@ async function appendConnectorIncident(errorsPath, input) {
5557
5615
  await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(errorsPath), { recursive: true });
5558
5616
  await import_node_fs9.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5559
5617
  }
5560
- function incidentAffectedNode(span, graph, scanPath) {
5618
+ function landIncidentCallSite(span, callSite, trusted, graph) {
5619
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
5620
+ const canonicalService = serviceNodeName(graph, span) ?? span.service;
5621
+ const recovered = !trusted;
5622
+ if (graph) {
5623
+ const fusedFileId = (0, import_types7.fileId)(canonicalService, relPath);
5624
+ if (graph.hasNode(fusedFileId)) {
5625
+ const node = landObservedSymbol(
5626
+ graph,
5627
+ fusedFileId,
5628
+ canonicalService,
5629
+ relPath,
5630
+ { ...callSite, relPath },
5631
+ false
5632
+ );
5633
+ return {
5634
+ affectedNode: node,
5635
+ ...recovered ? {
5636
+ codeFilepath: relPath,
5637
+ ...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
5638
+ } : {}
5639
+ };
5640
+ }
5641
+ if (recovered) return null;
5642
+ }
5643
+ return { affectedNode: (0, import_types7.fileId)(span.service, relPath) };
5644
+ }
5645
+ function serviceNodeName(graph, span) {
5646
+ if (!graph) return void 0;
5647
+ const sid = resolveFusedServiceId(graph, span.service, span.env);
5648
+ if (!graph.hasNode(sid)) return void 0;
5649
+ const node = graph.getNodeAttributes(sid);
5650
+ return typeof node.name === "string" ? node.name : void 0;
5651
+ }
5652
+ function stacktraceCallSite(span, serviceNode, scanPath) {
5653
+ const frame = deepestApplicationFrame(span.exception?.stacktrace);
5654
+ if (!frame) return null;
5655
+ const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
5656
+ if (!relPath) return null;
5657
+ return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
5658
+ }
5659
+ function incidentLocus(span, graph, scanPath) {
5561
5660
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types7.serviceId)(span.service, span.env);
5562
5661
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
5563
5662
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
5564
5663
  if (callSite) {
5565
- const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
5566
- const canonicalService = serviceNode && typeof serviceNode.name === "string" ? serviceNode.name : span.service;
5567
- if (graph) {
5568
- const fusedFileId = (0, import_types7.fileId)(canonicalService, relPath);
5569
- if (graph.hasNode(fusedFileId)) {
5570
- return landObservedSymbol(
5571
- graph,
5572
- fusedFileId,
5573
- canonicalService,
5574
- relPath,
5575
- { ...callSite, relPath },
5576
- false
5577
- );
5578
- }
5664
+ const landed = landIncidentCallSite(span, callSite, true, graph);
5665
+ if (landed) return landed;
5666
+ } else {
5667
+ const recovered = stacktraceCallSite(span, serviceNode, scanPath);
5668
+ if (recovered) {
5669
+ const landed = landIncidentCallSite(span, recovered, false, graph);
5670
+ if (landed) return landed;
5579
5671
  }
5580
- return (0, import_types7.fileId)(span.service, relPath);
5581
5672
  }
5582
- return sid;
5673
+ return { affectedNode: sid };
5674
+ }
5675
+ function incidentAffectedNode(span, graph, scanPath) {
5676
+ return incidentLocus(span, graph, scanPath).affectedNode;
5677
+ }
5678
+ function withRecoveredCodeAttrs(attrs, locus) {
5679
+ if (locus.codeFilepath === void 0) return attrs;
5680
+ attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
5681
+ if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
5682
+ return attrs;
5583
5683
  }
5584
5684
  function sanitizeAttributes(attrs) {
5585
5685
  const out = {};
@@ -5612,7 +5712,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
5612
5712
  await appendErrorEvent(ctx, ev);
5613
5713
  }
5614
5714
  async function recordExceptionIncident(ctx, span, ts) {
5615
- const attrs = sanitizeAttributes(span.attributes);
5715
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
5716
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
5616
5717
  const ev = {
5617
5718
  id: `${span.traceId}:${span.spanId}`,
5618
5719
  timestamp: ts,
@@ -5623,7 +5724,7 @@ async function recordExceptionIncident(ctx, span, ts) {
5623
5724
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
5624
5725
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
5625
5726
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
5626
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
5727
+ affectedNode: locus.affectedNode
5627
5728
  };
5628
5729
  await appendErrorEvent(ctx, ev);
5629
5730
  }
@@ -5926,7 +6027,8 @@ async function handleSpan(ctx, span) {
5926
6027
  if (span.statusCode === 2) {
5927
6028
  stitchTrace(ctx.graph, sourceId, ts);
5928
6029
  if (ctx.writeErrorEventInline !== false) {
5929
- const attrs = sanitizeAttributes(span.attributes);
6030
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
6031
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
5930
6032
  const ev = {
5931
6033
  id: `${span.traceId}:${span.spanId}`,
5932
6034
  timestamp: ts,
@@ -5938,10 +6040,11 @@ async function handleSpan(ctx, span) {
5938
6040
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
5939
6041
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
5940
6042
  // Attribute to where the failure originated — the symbol / file / service
5941
- // the throwing span named (incidentAffectedNode, ADR-191) the same
5942
- // source-based attribution the durable receiver write uses, not the
5943
- // outbound edge target this span happened to mint.
5944
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
6043
+ // the throwing span named (incidentAffectedNode / ADR-191, extended to
6044
+ // recover the locus from the stacktrace when the span stamped no code.*
6045
+ // attrs, ADR-216) the same source-based attribution the durable
6046
+ // receiver write uses, not the outbound edge target this span minted.
6047
+ affectedNode: locus.affectedNode
5945
6048
  };
5946
6049
  await appendErrorEvent(ctx, ev);
5947
6050
  }
@@ -6115,15 +6218,36 @@ function startStalenessLoop(graph, options = {}) {
6115
6218
  clearInterval(interval);
6116
6219
  };
6117
6220
  }
6118
- async function readErrorEvents(errorsPath) {
6221
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
6222
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
6223
+ async function readErrorFileTail(errorsPath, maxBytes) {
6224
+ const handle = await import_node_fs9.promises.open(errorsPath, "r");
6225
+ try {
6226
+ const { size } = await handle.stat();
6227
+ if (size <= maxBytes) {
6228
+ return (await handle.readFile()).toString("utf8");
6229
+ }
6230
+ const buf = Buffer.alloc(maxBytes);
6231
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
6232
+ const raw = buf.toString("utf8");
6233
+ const firstNewline = raw.indexOf("\n");
6234
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
6235
+ } finally {
6236
+ await handle.close();
6237
+ }
6238
+ }
6239
+ async function readErrorEvents(errorsPath, opts) {
6240
+ let raw;
6119
6241
  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);
6242
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
6123
6243
  } catch (err) {
6124
6244
  if (err.code === "ENOENT") return [];
6125
6245
  throw err;
6126
6246
  }
6247
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6248
+ const deduped = dedupeIncidents(events);
6249
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
6250
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
6127
6251
  }
6128
6252
  function isSynthesizedHttpIncident(ev) {
6129
6253
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -6883,6 +7007,47 @@ function classifyNode(ctx) {
6883
7007
  function isVictimSeed(ctx) {
6884
7008
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
6885
7009
  }
7010
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
7011
+ "name resolution",
7012
+ "resolve host",
7013
+ "getaddrinfo",
7014
+ "enotfound",
7015
+ "connection refused",
7016
+ "econnrefused",
7017
+ "connection reset",
7018
+ "econnreset",
7019
+ "able to connect",
7020
+ "failed to connect",
7021
+ "cannot connect",
7022
+ "could not connect",
7023
+ "unable to connect",
7024
+ "connection timed out",
7025
+ "etimedout",
7026
+ "no route to host",
7027
+ "host unreachable",
7028
+ "network is unreachable",
7029
+ "connection closed"
7030
+ ];
7031
+ function incidentTextIndicatesOutboundFailure(ev) {
7032
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
7033
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
7034
+ }
7035
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
7036
+ for (const n of nodeScope(graph, nodeId)) {
7037
+ if (!graph.hasNode(n)) continue;
7038
+ for (const edgeId of graph.outboundEdges(n)) {
7039
+ const e = graph.getEdgeAttributes(edgeId);
7040
+ if (e.type === import_types8.EdgeType.CONTAINS) continue;
7041
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
7042
+ }
7043
+ }
7044
+ if (seedSource === "incident" && incidents) {
7045
+ for (const ev of incidents) {
7046
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
7047
+ }
7048
+ }
7049
+ return false;
7050
+ }
6886
7051
  function grainOf(graph, nodeId) {
6887
7052
  if (!graph.hasNode(nodeId)) return "unknown";
6888
7053
  const t = graph.getNodeAttributes(nodeId).type;
@@ -7046,7 +7211,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
7046
7211
  const candidates = [];
7047
7212
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
7048
7213
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
7049
- if (seedCtx && isVictimSeed(seedCtx)) {
7214
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
7215
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
7050
7216
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
7051
7217
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
7052
7218
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -7396,6 +7562,120 @@ function detectColumnDrift(node) {
7396
7562
  }
7397
7563
  return out;
7398
7564
  }
7565
+ var SYMBOL_MISMATCH_PATTERNS = [
7566
+ {
7567
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
7568
+ kind: "missing-attribute",
7569
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
7570
+ },
7571
+ {
7572
+ // "object has no field 'X'", "no such field X", "unknown field X".
7573
+ kind: "missing-field",
7574
+ patterns: [
7575
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
7576
+ ]
7577
+ },
7578
+ {
7579
+ // "has no property X", "no property named X".
7580
+ kind: "missing-property",
7581
+ patterns: [
7582
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
7583
+ ]
7584
+ },
7585
+ {
7586
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
7587
+ kind: "missing-column",
7588
+ patterns: [
7589
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
7590
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
7591
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
7592
+ ]
7593
+ },
7594
+ {
7595
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
7596
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
7597
+ // mismatch) does not get miscategorised here.
7598
+ kind: "undefined-method",
7599
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
7600
+ }
7601
+ ];
7602
+ function classifySymbolMismatch(message) {
7603
+ for (const entry of SYMBOL_MISMATCH_PATTERNS) {
7604
+ for (const re of entry.patterns) {
7605
+ const m = re.exec(message);
7606
+ if (m) {
7607
+ const captured = m[1];
7608
+ return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
7609
+ }
7610
+ }
7611
+ }
7612
+ return null;
7613
+ }
7614
+ function symbolLocus(graph, ev) {
7615
+ const attrs = ev.attributes ?? {};
7616
+ const filepath = codeFilepathOf(attrs);
7617
+ const lineno = codeLinenoOf(attrs);
7618
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
7619
+ const affected = ev.affectedNode;
7620
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
7621
+ const affectedIsCode = affectedInGraph && ((0, import_types9.parseSymbolId)(affected) !== null || (0, import_types9.parseFileId)(affected) !== null);
7622
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
7623
+ if (!location) return null;
7624
+ if (affectedInGraph) return { node: affected, location };
7625
+ const svc = (0, import_types9.serviceId)(ev.service);
7626
+ if (graph.hasNode(svc)) return { node: svc, location };
7627
+ return null;
7628
+ }
7629
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
7630
+ function detectSymbolMismatches(graph, incidents) {
7631
+ const groups = /* @__PURE__ */ new Map();
7632
+ for (const ev of incidents) {
7633
+ const classified = classifySymbolMismatch(ev.errorMessage);
7634
+ if (!classified) continue;
7635
+ const locus = symbolLocus(graph, ev);
7636
+ if (!locus) continue;
7637
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
7638
+ const existing = groups.get(key);
7639
+ if (!existing) {
7640
+ groups.set(key, {
7641
+ node: locus.node,
7642
+ kind: classified.kind,
7643
+ ...classified.symbol ? { symbol: classified.symbol } : {},
7644
+ ...locus.location ? { location: locus.location } : {},
7645
+ latest: ev,
7646
+ count: 1
7647
+ });
7648
+ } else {
7649
+ existing.count += 1;
7650
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
7651
+ existing.latest = ev;
7652
+ if (locus.location) existing.location = locus.location;
7653
+ }
7654
+ }
7655
+ }
7656
+ const out = [];
7657
+ for (const g of groups.values()) {
7658
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
7659
+ const where = g.location ? ` at ${g.location}` : "";
7660
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
7661
+ out.push({
7662
+ type: "observed-symbol-mismatch",
7663
+ source: g.node,
7664
+ target: g.node,
7665
+ mismatchKind: g.kind,
7666
+ ...g.symbol ? { symbol: g.symbol } : {},
7667
+ ...g.location ? { location: g.location } : {},
7668
+ provenance: import_types9.Provenance.INFERRED,
7669
+ incidentId: g.latest.id,
7670
+ errorMessage: g.latest.errorMessage,
7671
+ incidentCount: g.count,
7672
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
7673
+ 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.`,
7674
+ 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."
7675
+ });
7676
+ }
7677
+ return out;
7678
+ }
7399
7679
  function involvesNode(d, nodeId) {
7400
7680
  return d.source === nodeId || d.target === nodeId;
7401
7681
  }
@@ -7481,6 +7761,9 @@ function computeDivergences(graph, opts = {}) {
7481
7761
  for (const d of detectColumnDrift(n)) all.push(d);
7482
7762
  }
7483
7763
  });
7764
+ if (opts.incidents && opts.incidents.length > 0) {
7765
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
7766
+ }
7484
7767
  const reconciled = suppressHostMismatchHalves(all);
7485
7768
  const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
7486
7769
  let filtered = dampened;
@@ -7501,7 +7784,12 @@ function computeDivergences(graph, opts = {}) {
7501
7784
  "missing-observed": 1,
7502
7785
  "version-mismatch": 2,
7503
7786
  "host-mismatch": 3,
7504
- "compat-violation": 4
7787
+ "compat-violation": 4,
7788
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
7789
+ // type; this only breaks a confidence tie, and it orders last so a same-
7790
+ // confidence edge finding leads. In practice it carries the INFERRED grade
7791
+ // (0.6), so it sits below the high-confidence edge divergences already.
7792
+ "observed-symbol-mismatch": 5
7505
7793
  };
7506
7794
  filtered.sort((a, b) => {
7507
7795
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -7512,7 +7800,10 @@ function computeDivergences(graph, opts = {}) {
7512
7800
  if (a.target !== b.target) return a.target.localeCompare(b.target);
7513
7801
  const ac = "column" in a && a.column ? a.column : "";
7514
7802
  const bc = "column" in b && b.column ? b.column : "";
7515
- return ac.localeCompare(bc);
7803
+ if (ac !== bc) return ac.localeCompare(bc);
7804
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
7805
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
7806
+ return asym.localeCompare(bsym);
7516
7807
  });
7517
7808
  return import_types9.DivergenceResultSchema.parse({
7518
7809
  divergences: filtered,
@@ -16692,10 +16983,15 @@ function divergenceLine(d) {
16692
16983
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16693
16984
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16694
16985
  }
16986
+ if (d.type === "observed-symbol-mismatch") {
16987
+ const at = d.location ? ` at ${d.location}` : "";
16988
+ const member = d.symbol ? ` ${d.symbol}` : "";
16989
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
16990
+ }
16695
16991
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16696
16992
  }
16697
- function buildDivergenceSection(graph, node) {
16698
- const result = computeDivergences(graph, { node });
16993
+ function buildDivergenceSection(graph, node, incidents) {
16994
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
16699
16995
  if (result.totalAffected === 0) return null;
16700
16996
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16701
16997
  text: divergenceLine(d),
@@ -16708,8 +17004,8 @@ function buildDivergenceSection(graph, node) {
16708
17004
  facts
16709
17005
  };
16710
17006
  }
16711
- function buildGlobalDivergenceSection(graph) {
16712
- const result = computeDivergences(graph);
17007
+ function buildGlobalDivergenceSection(graph, incidents) {
17008
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
16713
17009
  if (result.totalAffected === 0) {
16714
17010
  return {
16715
17011
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -16807,7 +17103,7 @@ function buildOverviewSections(graph, incidents) {
16807
17103
  }))
16808
17104
  });
16809
17105
  }
16810
- const div = computeDivergences(graph);
17106
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
16811
17107
  sections.push({
16812
17108
  heading: "Divergences",
16813
17109
  facts: [
@@ -16821,7 +17117,7 @@ function buildOverviewSections(graph, incidents) {
16821
17117
  function buildGlobalSections(intent, graph, incidents) {
16822
17118
  switch (intent) {
16823
17119
  case "divergence":
16824
- return [buildGlobalDivergenceSection(graph)];
17120
+ return [buildGlobalDivergenceSection(graph, incidents)];
16825
17121
  case "incidents":
16826
17122
  return [buildGlobalIncidentsSection(incidents)];
16827
17123
  case "overview":
@@ -16852,7 +17148,7 @@ function buildSection(kind, graph, node, incidents, now) {
16852
17148
  case "incidents":
16853
17149
  return buildIncidentsSection(node, incidents);
16854
17150
  case "divergence":
16855
- return buildDivergenceSection(graph, node);
17151
+ return buildDivergenceSection(graph, node, incidents);
16856
17152
  }
16857
17153
  }
16858
17154
  function summarizeGlobal(intent, sections) {
@@ -20657,6 +20953,8 @@ function buildRegistration(entry, graph, env = process.env) {
20657
20953
  }
20658
20954
 
20659
20955
  // src/api.ts
20956
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
20957
+ var INCIDENT_LIST_MAX_LIMIT = 200;
20660
20958
  function serializeGraph(graph) {
20661
20959
  const nodes = [];
20662
20960
  graph.forEachNode((_id, attrs) => {
@@ -20836,10 +21134,13 @@ function registerRoutes(scope, ctx) {
20836
21134
  }
20837
21135
  minConfidence = n;
20838
21136
  }
21137
+ const epath = errorsPathFor(proj);
21138
+ const incidents = epath ? await readErrorEvents(epath) : [];
20839
21139
  return computeDivergences(proj.graph, {
20840
21140
  ...typeFilter ? { type: typeFilter } : {},
20841
21141
  ...minConfidence !== void 0 ? { minConfidence } : {},
20842
- ...req.query.node ? { node: req.query.node } : {}
21142
+ ...req.query.node ? { node: req.query.node } : {},
21143
+ incidents
20843
21144
  });
20844
21145
  });
20845
21146
  scope.get("/incidents", async (req, reply) => {
@@ -20849,10 +21150,11 @@ function registerRoutes(scope, ctx) {
20849
21150
  if (!epath) return { count: 0, total: 0, events: [] };
20850
21151
  const events = await readErrorEvents(epath);
20851
21152
  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;
21153
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21154
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
20854
21155
  const sliced = events.slice(0, safeLimit);
20855
- return { count: sliced.length, total, events: sliced };
21156
+ const omitted = total - sliced.length;
21157
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
20856
21158
  });
20857
21159
  scope.get("/stale-events", async (req, reply) => {
20858
21160
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -20961,16 +21263,20 @@ function registerRoutes(scope, ctx) {
20961
21263
  const filtered = events.filter(
20962
21264
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
20963
21265
  );
20964
- return { count: filtered.length, total: filtered.length, events: filtered };
21266
+ const total = filtered.length;
21267
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21268
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21269
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
21270
+ const omitted = total - recent.length;
21271
+ return {
21272
+ count: recent.length,
21273
+ total,
21274
+ events: recent,
21275
+ ...omitted > 0 ? { omitted } : {}
21276
+ };
20965
21277
  };
20966
- scope.get(
20967
- "/incidents/:nodeId",
20968
- incidentHistoryHandler
20969
- );
20970
- scope.get(
20971
- "/graph/incident-history/:nodeId",
20972
- incidentHistoryHandler
20973
- );
21278
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
21279
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
20974
21280
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
20975
21281
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20976
21282
  if (!proj) return;