@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.
@@ -3691,6 +3691,63 @@ function latencyPercentiles(hist) {
3691
3691
  return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
3692
3692
  }
3693
3693
 
3694
+ // src/stacktrace.ts
3695
+ var FRAME_SHAPES = [
3696
+ // `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
3697
+ { re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
3698
+ // `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
3699
+ { re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
3700
+ // `at <path>:<line>:<col>` — anonymous call frame, most recent first.
3701
+ { re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
3702
+ // `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
3703
+ { re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
3704
+ ];
3705
+ var VENDOR_MARKERS = [
3706
+ "node_modules",
3707
+ // dependency root
3708
+ "site-packages",
3709
+ // installed-package root
3710
+ "dist-packages",
3711
+ // distro-packaged root
3712
+ "node:"
3713
+ // runtime-internal module scheme (a stdlib/runtime-root frame)
3714
+ ];
3715
+ function matchFrame(line) {
3716
+ for (const shape of FRAME_SHAPES) {
3717
+ const m = shape.re.exec(line);
3718
+ if (!m) continue;
3719
+ const file = m[shape.file];
3720
+ const lineNo = Number(m[shape.line]);
3721
+ if (!file || !Number.isFinite(lineNo)) continue;
3722
+ const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
3723
+ return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
3724
+ }
3725
+ return null;
3726
+ }
3727
+ function isApplicationFrame(file) {
3728
+ if (file.startsWith("<")) return false;
3729
+ const norm = file.split("\\").join("/");
3730
+ for (const marker of VENDOR_MARKERS) {
3731
+ if (norm.includes(marker)) return false;
3732
+ }
3733
+ return true;
3734
+ }
3735
+ function deepestApplicationFrame(stacktrace) {
3736
+ if (!stacktrace) return null;
3737
+ const appFrames = [];
3738
+ for (const raw of stacktrace.split("\n")) {
3739
+ const matched = matchFrame(raw);
3740
+ if (!matched) continue;
3741
+ if (!isApplicationFrame(matched.frame.file)) continue;
3742
+ appFrames.push(matched);
3743
+ }
3744
+ if (appFrames.length === 0) return null;
3745
+ const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
3746
+ const oriented = appFrames.filter((f) => f.deepest === orientation);
3747
+ const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
3748
+ return chosen ? chosen.frame : null;
3749
+ }
3750
+
3694
3751
  // src/ingest.ts
3695
3752
  var HOUR_MS = 60 * 60 * 1e3;
3696
3753
  var DAY_MS = 24 * HOUR_MS;
@@ -4542,29 +4599,71 @@ async function appendConnectorIncident(errorsPath, input) {
4542
4599
  await fs7.mkdir(path8.dirname(errorsPath), { recursive: true });
4543
4600
  await fs7.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4544
4601
  }
4545
- function incidentAffectedNode(span, graph, scanPath) {
4602
+ function landIncidentCallSite(span, callSite, trusted, graph) {
4603
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
4604
+ const canonicalService = serviceNodeName(graph, span) ?? span.service;
4605
+ const recovered = !trusted;
4606
+ if (graph) {
4607
+ const fusedFileId = fileId3(canonicalService, relPath);
4608
+ if (graph.hasNode(fusedFileId)) {
4609
+ const node = landObservedSymbol(
4610
+ graph,
4611
+ fusedFileId,
4612
+ canonicalService,
4613
+ relPath,
4614
+ { ...callSite, relPath },
4615
+ false
4616
+ );
4617
+ return {
4618
+ affectedNode: node,
4619
+ ...recovered ? {
4620
+ codeFilepath: relPath,
4621
+ ...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
4622
+ } : {}
4623
+ };
4624
+ }
4625
+ if (recovered) return null;
4626
+ }
4627
+ return { affectedNode: fileId3(span.service, relPath) };
4628
+ }
4629
+ function serviceNodeName(graph, span) {
4630
+ if (!graph) return void 0;
4631
+ const sid = resolveFusedServiceId(graph, span.service, span.env);
4632
+ if (!graph.hasNode(sid)) return void 0;
4633
+ const node = graph.getNodeAttributes(sid);
4634
+ return typeof node.name === "string" ? node.name : void 0;
4635
+ }
4636
+ function stacktraceCallSite(span, serviceNode, scanPath) {
4637
+ const frame = deepestApplicationFrame(span.exception?.stacktrace);
4638
+ if (!frame) return null;
4639
+ const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
4640
+ if (!relPath) return null;
4641
+ return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
4642
+ }
4643
+ function incidentLocus(span, graph, scanPath) {
4546
4644
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : serviceId(span.service, span.env);
4547
4645
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
4548
4646
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
4549
4647
  if (callSite) {
4550
- const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
4551
- const canonicalService = serviceNode && typeof serviceNode.name === "string" ? serviceNode.name : span.service;
4552
- if (graph) {
4553
- const fusedFileId = fileId3(canonicalService, relPath);
4554
- if (graph.hasNode(fusedFileId)) {
4555
- return landObservedSymbol(
4556
- graph,
4557
- fusedFileId,
4558
- canonicalService,
4559
- relPath,
4560
- { ...callSite, relPath },
4561
- false
4562
- );
4563
- }
4648
+ const landed = landIncidentCallSite(span, callSite, true, graph);
4649
+ if (landed) return landed;
4650
+ } else {
4651
+ const recovered = stacktraceCallSite(span, serviceNode, scanPath);
4652
+ if (recovered) {
4653
+ const landed = landIncidentCallSite(span, recovered, false, graph);
4654
+ if (landed) return landed;
4564
4655
  }
4565
- return fileId3(span.service, relPath);
4566
4656
  }
4567
- return sid;
4657
+ return { affectedNode: sid };
4658
+ }
4659
+ function incidentAffectedNode(span, graph, scanPath) {
4660
+ return incidentLocus(span, graph, scanPath).affectedNode;
4661
+ }
4662
+ function withRecoveredCodeAttrs(attrs, locus) {
4663
+ if (locus.codeFilepath === void 0) return attrs;
4664
+ attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
4665
+ if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
4666
+ return attrs;
4568
4667
  }
4569
4668
  function sanitizeAttributes(attrs) {
4570
4669
  const out = {};
@@ -4577,7 +4676,8 @@ function sanitizeAttributes(attrs) {
4577
4676
  function buildErrorEventForReceiver(span, graph, scanPath) {
4578
4677
  if (span.statusCode !== 2) return null;
4579
4678
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
4580
- const attrs = sanitizeAttributes(span.attributes);
4679
+ const locus = incidentLocus(span, graph, scanPath);
4680
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
4581
4681
  return {
4582
4682
  id: `${span.traceId}:${span.spanId}`,
4583
4683
  timestamp: ts,
@@ -4588,7 +4688,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
4588
4688
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
4589
4689
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
4590
4690
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
4591
- affectedNode: incidentAffectedNode(span, graph, scanPath)
4691
+ affectedNode: locus.affectedNode
4592
4692
  };
4593
4693
  }
4594
4694
  function makeErrorSpanWriter(errorsPath, graph, scanPath) {
@@ -4622,7 +4722,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
4622
4722
  await appendErrorEvent(ctx, ev);
4623
4723
  }
4624
4724
  async function recordExceptionIncident(ctx, span, ts) {
4625
- const attrs = sanitizeAttributes(span.attributes);
4725
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
4726
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
4626
4727
  const ev = {
4627
4728
  id: `${span.traceId}:${span.spanId}`,
4628
4729
  timestamp: ts,
@@ -4633,7 +4734,7 @@ async function recordExceptionIncident(ctx, span, ts) {
4633
4734
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
4634
4735
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
4635
4736
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
4636
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
4737
+ affectedNode: locus.affectedNode
4637
4738
  };
4638
4739
  await appendErrorEvent(ctx, ev);
4639
4740
  }
@@ -4936,7 +5037,8 @@ async function handleSpan(ctx, span) {
4936
5037
  if (span.statusCode === 2) {
4937
5038
  stitchTrace(ctx.graph, sourceId, ts);
4938
5039
  if (ctx.writeErrorEventInline !== false) {
4939
- const attrs = sanitizeAttributes(span.attributes);
5040
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
5041
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
4940
5042
  const ev = {
4941
5043
  id: `${span.traceId}:${span.spanId}`,
4942
5044
  timestamp: ts,
@@ -4948,10 +5050,11 @@ async function handleSpan(ctx, span) {
4948
5050
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
4949
5051
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
4950
5052
  // Attribute to where the failure originated — the symbol / file / service
4951
- // the throwing span named (incidentAffectedNode, ADR-191) the same
4952
- // source-based attribution the durable receiver write uses, not the
4953
- // outbound edge target this span happened to mint.
4954
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
5053
+ // the throwing span named (incidentAffectedNode / ADR-191, extended to
5054
+ // recover the locus from the stacktrace when the span stamped no code.*
5055
+ // attrs, ADR-216) the same source-based attribution the durable
5056
+ // receiver write uses, not the outbound edge target this span minted.
5057
+ affectedNode: locus.affectedNode
4955
5058
  };
4956
5059
  await appendErrorEvent(ctx, ev);
4957
5060
  }
@@ -5125,15 +5228,36 @@ function startStalenessLoop(graph, options = {}) {
5125
5228
  clearInterval(interval);
5126
5229
  };
5127
5230
  }
5128
- async function readErrorEvents(errorsPath) {
5231
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
5232
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
5233
+ async function readErrorFileTail(errorsPath, maxBytes) {
5234
+ const handle = await fs7.open(errorsPath, "r");
5235
+ try {
5236
+ const { size } = await handle.stat();
5237
+ if (size <= maxBytes) {
5238
+ return (await handle.readFile()).toString("utf8");
5239
+ }
5240
+ const buf = Buffer.alloc(maxBytes);
5241
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
5242
+ const raw = buf.toString("utf8");
5243
+ const firstNewline = raw.indexOf("\n");
5244
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
5245
+ } finally {
5246
+ await handle.close();
5247
+ }
5248
+ }
5249
+ async function readErrorEvents(errorsPath, opts) {
5250
+ let raw;
5129
5251
  try {
5130
- const raw = await fs7.readFile(errorsPath, "utf8");
5131
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
5132
- return dedupeIncidents(events);
5252
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
5133
5253
  } catch (err) {
5134
5254
  if (err.code === "ENOENT") return [];
5135
5255
  throw err;
5136
5256
  }
5257
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
5258
+ const deduped = dedupeIncidents(events);
5259
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
5260
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
5137
5261
  }
5138
5262
  function isSynthesizedHttpIncident(ev) {
5139
5263
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -5906,6 +6030,47 @@ function classifyNode(ctx) {
5906
6030
  function isVictimSeed(ctx) {
5907
6031
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
5908
6032
  }
6033
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
6034
+ "name resolution",
6035
+ "resolve host",
6036
+ "getaddrinfo",
6037
+ "enotfound",
6038
+ "connection refused",
6039
+ "econnrefused",
6040
+ "connection reset",
6041
+ "econnreset",
6042
+ "able to connect",
6043
+ "failed to connect",
6044
+ "cannot connect",
6045
+ "could not connect",
6046
+ "unable to connect",
6047
+ "connection timed out",
6048
+ "etimedout",
6049
+ "no route to host",
6050
+ "host unreachable",
6051
+ "network is unreachable",
6052
+ "connection closed"
6053
+ ];
6054
+ function incidentTextIndicatesOutboundFailure(ev) {
6055
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
6056
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
6057
+ }
6058
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
6059
+ for (const n of nodeScope(graph, nodeId)) {
6060
+ if (!graph.hasNode(n)) continue;
6061
+ for (const edgeId of graph.outboundEdges(n)) {
6062
+ const e = graph.getEdgeAttributes(edgeId);
6063
+ if (e.type === EdgeType6.CONTAINS) continue;
6064
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
6065
+ }
6066
+ }
6067
+ if (seedSource === "incident" && incidents) {
6068
+ for (const ev of incidents) {
6069
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
6070
+ }
6071
+ }
6072
+ return false;
6073
+ }
5909
6074
  function grainOf(graph, nodeId) {
5910
6075
  if (!graph.hasNode(nodeId)) return "unknown";
5911
6076
  const t = graph.getNodeAttributes(nodeId).type;
@@ -6069,7 +6234,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
6069
6234
  const candidates = [];
6070
6235
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
6071
6236
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
6072
- if (seedCtx && isVictimSeed(seedCtx)) {
6237
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
6238
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
6073
6239
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
6074
6240
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
6075
6241
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -14939,6 +15105,7 @@ import {
14939
15105
  NodeType as NodeType30,
14940
15106
  parseEdgeId,
14941
15107
  parseFileId,
15108
+ parseSymbolId as parseSymbolId2,
14942
15109
  Provenance as Provenance24,
14943
15110
  serviceId as serviceId13
14944
15111
  } from "@neat.is/types";
@@ -15195,6 +15362,120 @@ function detectColumnDrift(node) {
15195
15362
  }
15196
15363
  return out;
15197
15364
  }
15365
+ var SYMBOL_MISMATCH_PATTERNS = [
15366
+ {
15367
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
15368
+ kind: "missing-attribute",
15369
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
15370
+ },
15371
+ {
15372
+ // "object has no field 'X'", "no such field X", "unknown field X".
15373
+ kind: "missing-field",
15374
+ patterns: [
15375
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
15376
+ ]
15377
+ },
15378
+ {
15379
+ // "has no property X", "no property named X".
15380
+ kind: "missing-property",
15381
+ patterns: [
15382
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
15383
+ ]
15384
+ },
15385
+ {
15386
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
15387
+ kind: "missing-column",
15388
+ patterns: [
15389
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
15390
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
15391
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
15392
+ ]
15393
+ },
15394
+ {
15395
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
15396
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
15397
+ // mismatch) does not get miscategorised here.
15398
+ kind: "undefined-method",
15399
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
15400
+ }
15401
+ ];
15402
+ function classifySymbolMismatch(message) {
15403
+ for (const entry of SYMBOL_MISMATCH_PATTERNS) {
15404
+ for (const re of entry.patterns) {
15405
+ const m = re.exec(message);
15406
+ if (m) {
15407
+ const captured = m[1];
15408
+ return captured ? { kind: entry.kind, symbol: captured } : { kind: entry.kind };
15409
+ }
15410
+ }
15411
+ }
15412
+ return null;
15413
+ }
15414
+ function symbolLocus(graph, ev) {
15415
+ const attrs = ev.attributes ?? {};
15416
+ const filepath = codeFilepathOf(attrs);
15417
+ const lineno = codeLinenoOf(attrs);
15418
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
15419
+ const affected = ev.affectedNode;
15420
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
15421
+ const affectedIsCode = affectedInGraph && (parseSymbolId2(affected) !== null || parseFileId(affected) !== null);
15422
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
15423
+ if (!location) return null;
15424
+ if (affectedInGraph) return { node: affected, location };
15425
+ const svc = serviceId13(ev.service);
15426
+ if (graph.hasNode(svc)) return { node: svc, location };
15427
+ return null;
15428
+ }
15429
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
15430
+ function detectSymbolMismatches(graph, incidents) {
15431
+ const groups = /* @__PURE__ */ new Map();
15432
+ for (const ev of incidents) {
15433
+ const classified = classifySymbolMismatch(ev.errorMessage);
15434
+ if (!classified) continue;
15435
+ const locus = symbolLocus(graph, ev);
15436
+ if (!locus) continue;
15437
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
15438
+ const existing = groups.get(key);
15439
+ if (!existing) {
15440
+ groups.set(key, {
15441
+ node: locus.node,
15442
+ kind: classified.kind,
15443
+ ...classified.symbol ? { symbol: classified.symbol } : {},
15444
+ ...locus.location ? { location: locus.location } : {},
15445
+ latest: ev,
15446
+ count: 1
15447
+ });
15448
+ } else {
15449
+ existing.count += 1;
15450
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
15451
+ existing.latest = ev;
15452
+ if (locus.location) existing.location = locus.location;
15453
+ }
15454
+ }
15455
+ }
15456
+ const out = [];
15457
+ for (const g of groups.values()) {
15458
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
15459
+ const where = g.location ? ` at ${g.location}` : "";
15460
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
15461
+ out.push({
15462
+ type: "observed-symbol-mismatch",
15463
+ source: g.node,
15464
+ target: g.node,
15465
+ mismatchKind: g.kind,
15466
+ ...g.symbol ? { symbol: g.symbol } : {},
15467
+ ...g.location ? { location: g.location } : {},
15468
+ provenance: Provenance24.INFERRED,
15469
+ incidentId: g.latest.id,
15470
+ errorMessage: g.latest.errorMessage,
15471
+ incidentCount: g.count,
15472
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
15473
+ 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.`,
15474
+ 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."
15475
+ });
15476
+ }
15477
+ return out;
15478
+ }
15198
15479
  function involvesNode(d, nodeId) {
15199
15480
  return d.source === nodeId || d.target === nodeId;
15200
15481
  }
@@ -15280,6 +15561,9 @@ function computeDivergences(graph, opts = {}) {
15280
15561
  for (const d of detectColumnDrift(n)) all.push(d);
15281
15562
  }
15282
15563
  });
15564
+ if (opts.incidents && opts.incidents.length > 0) {
15565
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
15566
+ }
15283
15567
  const reconciled = suppressHostMismatchHalves(all);
15284
15568
  const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
15285
15569
  let filtered = dampened;
@@ -15300,7 +15584,12 @@ function computeDivergences(graph, opts = {}) {
15300
15584
  "missing-observed": 1,
15301
15585
  "version-mismatch": 2,
15302
15586
  "host-mismatch": 3,
15303
- "compat-violation": 4
15587
+ "compat-violation": 4,
15588
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
15589
+ // type; this only breaks a confidence tie, and it orders last so a same-
15590
+ // confidence edge finding leads. In practice it carries the INFERRED grade
15591
+ // (0.6), so it sits below the high-confidence edge divergences already.
15592
+ "observed-symbol-mismatch": 5
15304
15593
  };
15305
15594
  filtered.sort((a, b) => {
15306
15595
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -15311,7 +15600,10 @@ function computeDivergences(graph, opts = {}) {
15311
15600
  if (a.target !== b.target) return a.target.localeCompare(b.target);
15312
15601
  const ac = "column" in a && a.column ? a.column : "";
15313
15602
  const bc = "column" in b && b.column ? b.column : "";
15314
- return ac.localeCompare(bc);
15603
+ if (ac !== bc) return ac.localeCompare(bc);
15604
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
15605
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
15606
+ return asym.localeCompare(bsym);
15315
15607
  });
15316
15608
  return DivergenceResultSchema.parse({
15317
15609
  divergences: filtered,
@@ -16754,10 +17046,15 @@ function divergenceLine(d) {
16754
17046
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16755
17047
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16756
17048
  }
17049
+ if (d.type === "observed-symbol-mismatch") {
17050
+ const at = d.location ? ` at ${d.location}` : "";
17051
+ const member = d.symbol ? ` ${d.symbol}` : "";
17052
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
17053
+ }
16757
17054
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16758
17055
  }
16759
- function buildDivergenceSection(graph, node) {
16760
- const result = computeDivergences(graph, { node });
17056
+ function buildDivergenceSection(graph, node, incidents) {
17057
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
16761
17058
  if (result.totalAffected === 0) return null;
16762
17059
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16763
17060
  text: divergenceLine(d),
@@ -16770,8 +17067,8 @@ function buildDivergenceSection(graph, node) {
16770
17067
  facts
16771
17068
  };
16772
17069
  }
16773
- function buildGlobalDivergenceSection(graph) {
16774
- const result = computeDivergences(graph);
17070
+ function buildGlobalDivergenceSection(graph, incidents) {
17071
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
16775
17072
  if (result.totalAffected === 0) {
16776
17073
  return {
16777
17074
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -16869,7 +17166,7 @@ function buildOverviewSections(graph, incidents) {
16869
17166
  }))
16870
17167
  });
16871
17168
  }
16872
- const div = computeDivergences(graph);
17169
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
16873
17170
  sections.push({
16874
17171
  heading: "Divergences",
16875
17172
  facts: [
@@ -16883,7 +17180,7 @@ function buildOverviewSections(graph, incidents) {
16883
17180
  function buildGlobalSections(intent, graph, incidents) {
16884
17181
  switch (intent) {
16885
17182
  case "divergence":
16886
- return [buildGlobalDivergenceSection(graph)];
17183
+ return [buildGlobalDivergenceSection(graph, incidents)];
16887
17184
  case "incidents":
16888
17185
  return [buildGlobalIncidentsSection(incidents)];
16889
17186
  case "overview":
@@ -16914,7 +17211,7 @@ function buildSection(kind, graph, node, incidents, now) {
16914
17211
  case "incidents":
16915
17212
  return buildIncidentsSection(node, incidents);
16916
17213
  case "divergence":
16917
- return buildDivergenceSection(graph, node);
17214
+ return buildDivergenceSection(graph, node, incidents);
16918
17215
  }
16919
17216
  }
16920
17217
  function summarizeGlobal(intent, sections) {
@@ -20536,6 +20833,8 @@ async function deprovisionConnector(entry, env = process.env, fetchImpl) {
20536
20833
  }
20537
20834
 
20538
20835
  // src/api.ts
20836
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
20837
+ var INCIDENT_LIST_MAX_LIMIT = 200;
20539
20838
  function serializeGraph(graph) {
20540
20839
  const nodes = [];
20541
20840
  graph.forEachNode((_id, attrs) => {
@@ -20715,10 +21014,13 @@ function registerRoutes(scope, ctx) {
20715
21014
  }
20716
21015
  minConfidence = n;
20717
21016
  }
21017
+ const epath = errorsPathFor(proj);
21018
+ const incidents = epath ? await readErrorEvents(epath) : [];
20718
21019
  return computeDivergences(proj.graph, {
20719
21020
  ...typeFilter ? { type: typeFilter } : {},
20720
21021
  ...minConfidence !== void 0 ? { minConfidence } : {},
20721
- ...req.query.node ? { node: req.query.node } : {}
21022
+ ...req.query.node ? { node: req.query.node } : {},
21023
+ incidents
20722
21024
  });
20723
21025
  });
20724
21026
  scope.get("/incidents", async (req, reply) => {
@@ -20728,10 +21030,11 @@ function registerRoutes(scope, ctx) {
20728
21030
  if (!epath) return { count: 0, total: 0, events: [] };
20729
21031
  const events = await readErrorEvents(epath);
20730
21032
  const total = events.length;
20731
- const limit = req.query.limit ? Number(req.query.limit) : 50;
20732
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
21033
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21034
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
20733
21035
  const sliced = events.slice(0, safeLimit);
20734
- return { count: sliced.length, total, events: sliced };
21036
+ const omitted = total - sliced.length;
21037
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
20735
21038
  });
20736
21039
  scope.get("/stale-events", async (req, reply) => {
20737
21040
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -20840,16 +21143,20 @@ function registerRoutes(scope, ctx) {
20840
21143
  const filtered = events.filter(
20841
21144
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
20842
21145
  );
20843
- return { count: filtered.length, total: filtered.length, events: filtered };
21146
+ const total = filtered.length;
21147
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21148
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21149
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
21150
+ const omitted = total - recent.length;
21151
+ return {
21152
+ count: recent.length,
21153
+ total,
21154
+ events: recent,
21155
+ ...omitted > 0 ? { omitted } : {}
21156
+ };
20844
21157
  };
20845
- scope.get(
20846
- "/incidents/:nodeId",
20847
- incidentHistoryHandler
20848
- );
20849
- scope.get(
20850
- "/graph/incident-history/:nodeId",
20851
- incidentHistoryHandler
20852
- );
21158
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
21159
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
20853
21160
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
20854
21161
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20855
21162
  if (!proj) return;
@@ -21491,4 +21798,4 @@ export {
21491
21798
  deprovisionConnector,
21492
21799
  buildApi
21493
21800
  };
21494
- //# sourceMappingURL=chunk-LRYPKC3B.js.map
21801
+ //# sourceMappingURL=chunk-IVVF37OU.js.map