@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/index.d.cts CHANGED
@@ -441,7 +441,9 @@ interface StalenessLoopOptions {
441
441
  onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
442
442
  }
443
443
  declare function startStalenessLoop(graph: NeatGraph, options?: StalenessLoopOptions): () => void;
444
- declare function readErrorEvents(errorsPath: string): Promise<ErrorEvent[]>;
444
+ declare function readErrorEvents(errorsPath: string, opts?: {
445
+ limit?: number;
446
+ }): Promise<ErrorEvent[]>;
445
447
 
446
448
  declare function confidenceForEdge(edge: GraphEdge, now?: number): number;
447
449
  declare function getBlastRadius(graph: NeatGraph, nodeId: string, maxDepth?: number): BlastRadiusResult;
package/dist/index.d.ts CHANGED
@@ -441,7 +441,9 @@ interface StalenessLoopOptions {
441
441
  onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
442
442
  }
443
443
  declare function startStalenessLoop(graph: NeatGraph, options?: StalenessLoopOptions): () => void;
444
- declare function readErrorEvents(errorsPath: string): Promise<ErrorEvent[]>;
444
+ declare function readErrorEvents(errorsPath: string, opts?: {
445
+ limit?: number;
446
+ }): Promise<ErrorEvent[]>;
445
447
 
446
448
  declare function confidenceForEdge(edge: GraphEdge, now?: number): number;
447
449
  declare function getBlastRadius(graph: NeatGraph, nodeId: string, maxDepth?: number): BlastRadiusResult;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-WJGZYEUG.js";
4
+ } from "./chunk-TGCWMMF6.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-LRYPKC3B.js";
40
+ } from "./chunk-IVVF37OU.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-ERE47MCR.js";
package/dist/neatd.cjs CHANGED
@@ -1888,6 +1888,47 @@ function classifyNode(ctx) {
1888
1888
  function isVictimSeed(ctx) {
1889
1889
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
1890
1890
  }
1891
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
1892
+ "name resolution",
1893
+ "resolve host",
1894
+ "getaddrinfo",
1895
+ "enotfound",
1896
+ "connection refused",
1897
+ "econnrefused",
1898
+ "connection reset",
1899
+ "econnreset",
1900
+ "able to connect",
1901
+ "failed to connect",
1902
+ "cannot connect",
1903
+ "could not connect",
1904
+ "unable to connect",
1905
+ "connection timed out",
1906
+ "etimedout",
1907
+ "no route to host",
1908
+ "host unreachable",
1909
+ "network is unreachable",
1910
+ "connection closed"
1911
+ ];
1912
+ function incidentTextIndicatesOutboundFailure(ev) {
1913
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
1914
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
1915
+ }
1916
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
1917
+ for (const n of nodeScope(graph, nodeId)) {
1918
+ if (!graph.hasNode(n)) continue;
1919
+ for (const edgeId of graph.outboundEdges(n)) {
1920
+ const e = graph.getEdgeAttributes(edgeId);
1921
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1922
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
1923
+ }
1924
+ }
1925
+ if (seedSource === "incident" && incidents) {
1926
+ for (const ev of incidents) {
1927
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
1928
+ }
1929
+ }
1930
+ return false;
1931
+ }
1891
1932
  function grainOf(graph, nodeId) {
1892
1933
  if (!graph.hasNode(nodeId)) return "unknown";
1893
1934
  const t = graph.getNodeAttributes(nodeId).type;
@@ -2051,7 +2092,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2051
2092
  const candidates = [];
2052
2093
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
2053
2094
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
2054
- if (seedCtx && isVictimSeed(seedCtx)) {
2095
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
2096
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
2055
2097
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
2056
2098
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
2057
2099
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -5377,6 +5419,64 @@ function latencyPercentiles(hist) {
5377
5419
  return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
5378
5420
  }
5379
5421
 
5422
+ // src/stacktrace.ts
5423
+ init_cjs_shims();
5424
+ var FRAME_SHAPES = [
5425
+ // `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
5426
+ { re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
5427
+ // `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
5428
+ { re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
5429
+ // `at <path>:<line>:<col>` — anonymous call frame, most recent first.
5430
+ { re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
5431
+ // `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
5432
+ { re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
5433
+ ];
5434
+ var VENDOR_MARKERS = [
5435
+ "node_modules",
5436
+ // dependency root
5437
+ "site-packages",
5438
+ // installed-package root
5439
+ "dist-packages",
5440
+ // distro-packaged root
5441
+ "node:"
5442
+ // runtime-internal module scheme (a stdlib/runtime-root frame)
5443
+ ];
5444
+ function matchFrame(line) {
5445
+ for (const shape of FRAME_SHAPES) {
5446
+ const m = shape.re.exec(line);
5447
+ if (!m) continue;
5448
+ const file = m[shape.file];
5449
+ const lineNo = Number(m[shape.line]);
5450
+ if (!file || !Number.isFinite(lineNo)) continue;
5451
+ const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
5452
+ return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
5453
+ }
5454
+ return null;
5455
+ }
5456
+ function isApplicationFrame(file) {
5457
+ if (file.startsWith("<")) return false;
5458
+ const norm = file.split("\\").join("/");
5459
+ for (const marker of VENDOR_MARKERS) {
5460
+ if (norm.includes(marker)) return false;
5461
+ }
5462
+ return true;
5463
+ }
5464
+ function deepestApplicationFrame(stacktrace) {
5465
+ if (!stacktrace) return null;
5466
+ const appFrames = [];
5467
+ for (const raw of stacktrace.split("\n")) {
5468
+ const matched = matchFrame(raw);
5469
+ if (!matched) continue;
5470
+ if (!isApplicationFrame(matched.frame.file)) continue;
5471
+ appFrames.push(matched);
5472
+ }
5473
+ if (appFrames.length === 0) return null;
5474
+ const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
5475
+ const oriented = appFrames.filter((f) => f.deepest === orientation);
5476
+ const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
5477
+ return chosen ? chosen.frame : null;
5478
+ }
5479
+
5380
5480
  // src/ingest.ts
5381
5481
  var HOUR_MS = 60 * 60 * 1e3;
5382
5482
  var DAY_MS = 24 * HOUR_MS;
@@ -6228,29 +6328,71 @@ async function appendConnectorIncident(errorsPath, input) {
6228
6328
  await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
6229
6329
  await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
6230
6330
  }
6231
- function incidentAffectedNode(span, graph, scanPath) {
6331
+ function landIncidentCallSite(span, callSite, trusted, graph) {
6332
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
6333
+ const canonicalService = serviceNodeName(graph, span) ?? span.service;
6334
+ const recovered = !trusted;
6335
+ if (graph) {
6336
+ const fusedFileId = (0, import_types8.fileId)(canonicalService, relPath);
6337
+ if (graph.hasNode(fusedFileId)) {
6338
+ const node = landObservedSymbol(
6339
+ graph,
6340
+ fusedFileId,
6341
+ canonicalService,
6342
+ relPath,
6343
+ { ...callSite, relPath },
6344
+ false
6345
+ );
6346
+ return {
6347
+ affectedNode: node,
6348
+ ...recovered ? {
6349
+ codeFilepath: relPath,
6350
+ ...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
6351
+ } : {}
6352
+ };
6353
+ }
6354
+ if (recovered) return null;
6355
+ }
6356
+ return { affectedNode: (0, import_types8.fileId)(span.service, relPath) };
6357
+ }
6358
+ function serviceNodeName(graph, span) {
6359
+ if (!graph) return void 0;
6360
+ const sid = resolveFusedServiceId(graph, span.service, span.env);
6361
+ if (!graph.hasNode(sid)) return void 0;
6362
+ const node = graph.getNodeAttributes(sid);
6363
+ return typeof node.name === "string" ? node.name : void 0;
6364
+ }
6365
+ function stacktraceCallSite(span, serviceNode, scanPath) {
6366
+ const frame = deepestApplicationFrame(span.exception?.stacktrace);
6367
+ if (!frame) return null;
6368
+ const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
6369
+ if (!relPath) return null;
6370
+ return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
6371
+ }
6372
+ function incidentLocus(span, graph, scanPath) {
6232
6373
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
6233
6374
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
6234
6375
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
6235
6376
  if (callSite) {
6236
- const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
6237
- const canonicalService = serviceNode && typeof serviceNode.name === "string" ? serviceNode.name : span.service;
6238
- if (graph) {
6239
- const fusedFileId = (0, import_types8.fileId)(canonicalService, relPath);
6240
- if (graph.hasNode(fusedFileId)) {
6241
- return landObservedSymbol(
6242
- graph,
6243
- fusedFileId,
6244
- canonicalService,
6245
- relPath,
6246
- { ...callSite, relPath },
6247
- false
6248
- );
6249
- }
6377
+ const landed = landIncidentCallSite(span, callSite, true, graph);
6378
+ if (landed) return landed;
6379
+ } else {
6380
+ const recovered = stacktraceCallSite(span, serviceNode, scanPath);
6381
+ if (recovered) {
6382
+ const landed = landIncidentCallSite(span, recovered, false, graph);
6383
+ if (landed) return landed;
6250
6384
  }
6251
- return (0, import_types8.fileId)(span.service, relPath);
6252
6385
  }
6253
- return sid;
6386
+ return { affectedNode: sid };
6387
+ }
6388
+ function incidentAffectedNode(span, graph, scanPath) {
6389
+ return incidentLocus(span, graph, scanPath).affectedNode;
6390
+ }
6391
+ function withRecoveredCodeAttrs(attrs, locus) {
6392
+ if (locus.codeFilepath === void 0) return attrs;
6393
+ attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
6394
+ if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
6395
+ return attrs;
6254
6396
  }
6255
6397
  function sanitizeAttributes(attrs) {
6256
6398
  const out = {};
@@ -6263,7 +6405,8 @@ function sanitizeAttributes(attrs) {
6263
6405
  function buildErrorEventForReceiver(span, graph, scanPath) {
6264
6406
  if (span.statusCode !== 2) return null;
6265
6407
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
6266
- const attrs = sanitizeAttributes(span.attributes);
6408
+ const locus = incidentLocus(span, graph, scanPath);
6409
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6267
6410
  return {
6268
6411
  id: `${span.traceId}:${span.spanId}`,
6269
6412
  timestamp: ts,
@@ -6274,7 +6417,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
6274
6417
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
6275
6418
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6276
6419
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6277
- affectedNode: incidentAffectedNode(span, graph, scanPath)
6420
+ affectedNode: locus.affectedNode
6278
6421
  };
6279
6422
  }
6280
6423
  function makeErrorSpanWriter(errorsPath, graph, scanPath) {
@@ -6308,7 +6451,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
6308
6451
  await appendErrorEvent(ctx, ev);
6309
6452
  }
6310
6453
  async function recordExceptionIncident(ctx, span, ts) {
6311
- const attrs = sanitizeAttributes(span.attributes);
6454
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
6455
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6312
6456
  const ev = {
6313
6457
  id: `${span.traceId}:${span.spanId}`,
6314
6458
  timestamp: ts,
@@ -6319,7 +6463,7 @@ async function recordExceptionIncident(ctx, span, ts) {
6319
6463
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
6320
6464
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6321
6465
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6322
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
6466
+ affectedNode: locus.affectedNode
6323
6467
  };
6324
6468
  await appendErrorEvent(ctx, ev);
6325
6469
  }
@@ -6622,7 +6766,8 @@ async function handleSpan(ctx, span) {
6622
6766
  if (span.statusCode === 2) {
6623
6767
  stitchTrace(ctx.graph, sourceId, ts);
6624
6768
  if (ctx.writeErrorEventInline !== false) {
6625
- const attrs = sanitizeAttributes(span.attributes);
6769
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
6770
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6626
6771
  const ev = {
6627
6772
  id: `${span.traceId}:${span.spanId}`,
6628
6773
  timestamp: ts,
@@ -6634,10 +6779,11 @@ async function handleSpan(ctx, span) {
6634
6779
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6635
6780
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6636
6781
  // Attribute to where the failure originated — the symbol / file / service
6637
- // the throwing span named (incidentAffectedNode, ADR-191) the same
6638
- // source-based attribution the durable receiver write uses, not the
6639
- // outbound edge target this span happened to mint.
6640
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
6782
+ // the throwing span named (incidentAffectedNode / ADR-191, extended to
6783
+ // recover the locus from the stacktrace when the span stamped no code.*
6784
+ // attrs, ADR-216) the same source-based attribution the durable
6785
+ // receiver write uses, not the outbound edge target this span minted.
6786
+ affectedNode: locus.affectedNode
6641
6787
  };
6642
6788
  await appendErrorEvent(ctx, ev);
6643
6789
  }
@@ -6808,15 +6954,36 @@ function startStalenessLoop(graph, options = {}) {
6808
6954
  clearInterval(interval);
6809
6955
  };
6810
6956
  }
6811
- async function readErrorEvents(errorsPath) {
6957
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
6958
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
6959
+ async function readErrorFileTail(errorsPath, maxBytes) {
6960
+ const handle = await import_node_fs7.promises.open(errorsPath, "r");
6812
6961
  try {
6813
- const raw = await import_node_fs7.promises.readFile(errorsPath, "utf8");
6814
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6815
- return dedupeIncidents(events);
6962
+ const { size } = await handle.stat();
6963
+ if (size <= maxBytes) {
6964
+ return (await handle.readFile()).toString("utf8");
6965
+ }
6966
+ const buf = Buffer.alloc(maxBytes);
6967
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
6968
+ const raw = buf.toString("utf8");
6969
+ const firstNewline = raw.indexOf("\n");
6970
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
6971
+ } finally {
6972
+ await handle.close();
6973
+ }
6974
+ }
6975
+ async function readErrorEvents(errorsPath, opts) {
6976
+ let raw;
6977
+ try {
6978
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
6816
6979
  } catch (err) {
6817
6980
  if (err.code === "ENOENT") return [];
6818
6981
  throw err;
6819
6982
  }
6983
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6984
+ const deduped = dedupeIncidents(events);
6985
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
6986
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
6820
6987
  }
6821
6988
  function isSynthesizedHttpIncident(ev) {
6822
6989
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -16455,6 +16622,120 @@ function detectColumnDrift(node) {
16455
16622
  }
16456
16623
  return out;
16457
16624
  }
16625
+ var SYMBOL_MISMATCH_PATTERNS = [
16626
+ {
16627
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
16628
+ kind: "missing-attribute",
16629
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
16630
+ },
16631
+ {
16632
+ // "object has no field 'X'", "no such field X", "unknown field X".
16633
+ kind: "missing-field",
16634
+ patterns: [
16635
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
16636
+ ]
16637
+ },
16638
+ {
16639
+ // "has no property X", "no property named X".
16640
+ kind: "missing-property",
16641
+ patterns: [
16642
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
16643
+ ]
16644
+ },
16645
+ {
16646
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
16647
+ kind: "missing-column",
16648
+ patterns: [
16649
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
16650
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
16651
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
16652
+ ]
16653
+ },
16654
+ {
16655
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
16656
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
16657
+ // mismatch) does not get miscategorised here.
16658
+ kind: "undefined-method",
16659
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
16660
+ }
16661
+ ];
16662
+ function classifySymbolMismatch(message) {
16663
+ for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
16664
+ for (const re of entry2.patterns) {
16665
+ const m = re.exec(message);
16666
+ if (m) {
16667
+ const captured = m[1];
16668
+ return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
16669
+ }
16670
+ }
16671
+ }
16672
+ return null;
16673
+ }
16674
+ function symbolLocus(graph, ev) {
16675
+ const attrs = ev.attributes ?? {};
16676
+ const filepath = codeFilepathOf(attrs);
16677
+ const lineno = codeLinenoOf(attrs);
16678
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
16679
+ const affected = ev.affectedNode;
16680
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
16681
+ const affectedIsCode = affectedInGraph && ((0, import_types58.parseSymbolId)(affected) !== null || (0, import_types58.parseFileId)(affected) !== null);
16682
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
16683
+ if (!location) return null;
16684
+ if (affectedInGraph) return { node: affected, location };
16685
+ const svc = (0, import_types58.serviceId)(ev.service);
16686
+ if (graph.hasNode(svc)) return { node: svc, location };
16687
+ return null;
16688
+ }
16689
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
16690
+ function detectSymbolMismatches(graph, incidents) {
16691
+ const groups = /* @__PURE__ */ new Map();
16692
+ for (const ev of incidents) {
16693
+ const classified = classifySymbolMismatch(ev.errorMessage);
16694
+ if (!classified) continue;
16695
+ const locus = symbolLocus(graph, ev);
16696
+ if (!locus) continue;
16697
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
16698
+ const existing = groups.get(key);
16699
+ if (!existing) {
16700
+ groups.set(key, {
16701
+ node: locus.node,
16702
+ kind: classified.kind,
16703
+ ...classified.symbol ? { symbol: classified.symbol } : {},
16704
+ ...locus.location ? { location: locus.location } : {},
16705
+ latest: ev,
16706
+ count: 1
16707
+ });
16708
+ } else {
16709
+ existing.count += 1;
16710
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
16711
+ existing.latest = ev;
16712
+ if (locus.location) existing.location = locus.location;
16713
+ }
16714
+ }
16715
+ }
16716
+ const out = [];
16717
+ for (const g of groups.values()) {
16718
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
16719
+ const where = g.location ? ` at ${g.location}` : "";
16720
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
16721
+ out.push({
16722
+ type: "observed-symbol-mismatch",
16723
+ source: g.node,
16724
+ target: g.node,
16725
+ mismatchKind: g.kind,
16726
+ ...g.symbol ? { symbol: g.symbol } : {},
16727
+ ...g.location ? { location: g.location } : {},
16728
+ provenance: import_types58.Provenance.INFERRED,
16729
+ incidentId: g.latest.id,
16730
+ errorMessage: g.latest.errorMessage,
16731
+ incidentCount: g.count,
16732
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
16733
+ 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.`,
16734
+ 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."
16735
+ });
16736
+ }
16737
+ return out;
16738
+ }
16458
16739
  function involvesNode(d, nodeId) {
16459
16740
  return d.source === nodeId || d.target === nodeId;
16460
16741
  }
@@ -16540,6 +16821,9 @@ function computeDivergences(graph, opts = {}) {
16540
16821
  for (const d of detectColumnDrift(n)) all.push(d);
16541
16822
  }
16542
16823
  });
16824
+ if (opts.incidents && opts.incidents.length > 0) {
16825
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
16826
+ }
16543
16827
  const reconciled = suppressHostMismatchHalves(all);
16544
16828
  const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
16545
16829
  let filtered = dampened;
@@ -16560,7 +16844,12 @@ function computeDivergences(graph, opts = {}) {
16560
16844
  "missing-observed": 1,
16561
16845
  "version-mismatch": 2,
16562
16846
  "host-mismatch": 3,
16563
- "compat-violation": 4
16847
+ "compat-violation": 4,
16848
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
16849
+ // type; this only breaks a confidence tie, and it orders last so a same-
16850
+ // confidence edge finding leads. In practice it carries the INFERRED grade
16851
+ // (0.6), so it sits below the high-confidence edge divergences already.
16852
+ "observed-symbol-mismatch": 5
16564
16853
  };
16565
16854
  filtered.sort((a, b) => {
16566
16855
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -16571,7 +16860,10 @@ function computeDivergences(graph, opts = {}) {
16571
16860
  if (a.target !== b.target) return a.target.localeCompare(b.target);
16572
16861
  const ac = "column" in a && a.column ? a.column : "";
16573
16862
  const bc = "column" in b && b.column ? b.column : "";
16574
- return ac.localeCompare(bc);
16863
+ if (ac !== bc) return ac.localeCompare(bc);
16864
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
16865
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
16866
+ return asym.localeCompare(bsym);
16575
16867
  });
16576
16868
  return import_types58.DivergenceResultSchema.parse({
16577
16869
  divergences: filtered,
@@ -16998,10 +17290,15 @@ function divergenceLine(d) {
16998
17290
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16999
17291
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
17000
17292
  }
17293
+ if (d.type === "observed-symbol-mismatch") {
17294
+ const at = d.location ? ` at ${d.location}` : "";
17295
+ const member = d.symbol ? ` ${d.symbol}` : "";
17296
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
17297
+ }
17001
17298
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
17002
17299
  }
17003
- function buildDivergenceSection(graph, node) {
17004
- const result = computeDivergences(graph, { node });
17300
+ function buildDivergenceSection(graph, node, incidents) {
17301
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
17005
17302
  if (result.totalAffected === 0) return null;
17006
17303
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17007
17304
  text: divergenceLine(d),
@@ -17014,8 +17311,8 @@ function buildDivergenceSection(graph, node) {
17014
17311
  facts
17015
17312
  };
17016
17313
  }
17017
- function buildGlobalDivergenceSection(graph) {
17018
- const result = computeDivergences(graph);
17314
+ function buildGlobalDivergenceSection(graph, incidents) {
17315
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
17019
17316
  if (result.totalAffected === 0) {
17020
17317
  return {
17021
17318
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -17113,7 +17410,7 @@ function buildOverviewSections(graph, incidents) {
17113
17410
  }))
17114
17411
  });
17115
17412
  }
17116
- const div = computeDivergences(graph);
17413
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
17117
17414
  sections.push({
17118
17415
  heading: "Divergences",
17119
17416
  facts: [
@@ -17127,7 +17424,7 @@ function buildOverviewSections(graph, incidents) {
17127
17424
  function buildGlobalSections(intent, graph, incidents) {
17128
17425
  switch (intent) {
17129
17426
  case "divergence":
17130
- return [buildGlobalDivergenceSection(graph)];
17427
+ return [buildGlobalDivergenceSection(graph, incidents)];
17131
17428
  case "incidents":
17132
17429
  return [buildGlobalIncidentsSection(incidents)];
17133
17430
  case "overview":
@@ -17158,7 +17455,7 @@ function buildSection(kind, graph, node, incidents, now) {
17158
17455
  case "incidents":
17159
17456
  return buildIncidentsSection(node, incidents);
17160
17457
  case "divergence":
17161
- return buildDivergenceSection(graph, node);
17458
+ return buildDivergenceSection(graph, node, incidents);
17162
17459
  }
17163
17460
  }
17164
17461
  function summarizeGlobal(intent, sections) {
@@ -21033,6 +21330,8 @@ async function startConnectorPolling(input) {
21033
21330
  }
21034
21331
 
21035
21332
  // src/api.ts
21333
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
21334
+ var INCIDENT_LIST_MAX_LIMIT = 200;
21036
21335
  function serializeGraph(graph) {
21037
21336
  const nodes = [];
21038
21337
  graph.forEachNode((_id, attrs) => {
@@ -21212,10 +21511,13 @@ function registerRoutes(scope, ctx) {
21212
21511
  }
21213
21512
  minConfidence = n;
21214
21513
  }
21514
+ const epath = errorsPathFor(proj);
21515
+ const incidents = epath ? await readErrorEvents(epath) : [];
21215
21516
  return computeDivergences(proj.graph, {
21216
21517
  ...typeFilter ? { type: typeFilter } : {},
21217
21518
  ...minConfidence !== void 0 ? { minConfidence } : {},
21218
- ...req2.query.node ? { node: req2.query.node } : {}
21519
+ ...req2.query.node ? { node: req2.query.node } : {},
21520
+ incidents
21219
21521
  });
21220
21522
  });
21221
21523
  scope.get("/incidents", async (req2, reply) => {
@@ -21225,10 +21527,11 @@ function registerRoutes(scope, ctx) {
21225
21527
  if (!epath) return { count: 0, total: 0, events: [] };
21226
21528
  const events = await readErrorEvents(epath);
21227
21529
  const total = events.length;
21228
- const limit = req2.query.limit ? Number(req2.query.limit) : 50;
21229
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
21530
+ const limit = req2.query.limit ? Number(req2.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21531
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21230
21532
  const sliced = events.slice(0, safeLimit);
21231
- return { count: sliced.length, total, events: sliced };
21533
+ const omitted = total - sliced.length;
21534
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
21232
21535
  });
21233
21536
  scope.get("/stale-events", async (req2, reply) => {
21234
21537
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
@@ -21337,16 +21640,20 @@ function registerRoutes(scope, ctx) {
21337
21640
  const filtered = events.filter(
21338
21641
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
21339
21642
  );
21340
- return { count: filtered.length, total: filtered.length, events: filtered };
21643
+ const total = filtered.length;
21644
+ const limit = req2.query.limit ? Number(req2.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
21645
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21646
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
21647
+ const omitted = total - recent.length;
21648
+ return {
21649
+ count: recent.length,
21650
+ total,
21651
+ events: recent,
21652
+ ...omitted > 0 ? { omitted } : {}
21653
+ };
21341
21654
  };
21342
- scope.get(
21343
- "/incidents/:nodeId",
21344
- incidentHistoryHandler
21345
- );
21346
- scope.get(
21347
- "/graph/incident-history/:nodeId",
21348
- incidentHistoryHandler
21349
- );
21655
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
21656
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
21350
21657
  scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
21351
21658
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
21352
21659
  if (!proj) return;