@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/cli.cjs CHANGED
@@ -1933,6 +1933,47 @@ function classifyNode(ctx) {
1933
1933
  function isVictimSeed(ctx) {
1934
1934
  return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
1935
1935
  }
1936
+ var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
1937
+ "name resolution",
1938
+ "resolve host",
1939
+ "getaddrinfo",
1940
+ "enotfound",
1941
+ "connection refused",
1942
+ "econnrefused",
1943
+ "connection reset",
1944
+ "econnreset",
1945
+ "able to connect",
1946
+ "failed to connect",
1947
+ "cannot connect",
1948
+ "could not connect",
1949
+ "unable to connect",
1950
+ "connection timed out",
1951
+ "etimedout",
1952
+ "no route to host",
1953
+ "host unreachable",
1954
+ "network is unreachable",
1955
+ "connection closed"
1956
+ ];
1957
+ function incidentTextIndicatesOutboundFailure(ev) {
1958
+ const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
1959
+ return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
1960
+ }
1961
+ function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
1962
+ for (const n of nodeScope(graph, nodeId)) {
1963
+ if (!graph.hasNode(n)) continue;
1964
+ for (const edgeId of graph.outboundEdges(n)) {
1965
+ const e = graph.getEdgeAttributes(edgeId);
1966
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1967
+ if ((e.signal?.errorCount ?? 0) > 0) return true;
1968
+ }
1969
+ }
1970
+ if (seedSource === "incident" && incidents) {
1971
+ for (const ev of incidents) {
1972
+ if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
1973
+ }
1974
+ }
1975
+ return false;
1976
+ }
1936
1977
  function grainOf(graph, nodeId) {
1937
1978
  if (!graph.hasNode(nodeId)) return "unknown";
1938
1979
  const t = graph.getNodeAttributes(nodeId).type;
@@ -2096,7 +2137,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2096
2137
  const candidates = [];
2097
2138
  const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
2098
2139
  const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
2099
- if (seedCtx && isVictimSeed(seedCtx)) {
2140
+ const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
2141
+ if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
2100
2142
  const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
2101
2143
  const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
2102
2144
  const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
@@ -5434,6 +5476,64 @@ function latencyPercentiles(hist) {
5434
5476
  return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
5435
5477
  }
5436
5478
 
5479
+ // src/stacktrace.ts
5480
+ init_cjs_shims();
5481
+ var FRAME_SHAPES = [
5482
+ // `File "<path>", line <N>, in <func>` — the "most recent call last" shape.
5483
+ { re: /File "([^"]+)", line (\d+)(?:, in (\S+))?/, file: 1, line: 2, fn: 3, deepest: "last" },
5484
+ // `at <func> (<path>:<line>:<col>)` — named call frame, most recent first.
5485
+ { re: /\bat\s+(.+?)\s+\((.+?):(\d+):\d+\)/, file: 2, line: 3, fn: 1, deepest: "first" },
5486
+ // `at <path>:<line>:<col>` — anonymous call frame, most recent first.
5487
+ { re: /\bat\s+(.+?):(\d+):\d+/, file: 1, line: 2, deepest: "first" },
5488
+ // `at <qualified.method>(<File.ext>:<line>)` — JVM-style, most recent first.
5489
+ { re: /\bat\s+(.+?)\((\S+\.\w+):(\d+)\)/, file: 2, line: 3, fn: 1, deepest: "first" }
5490
+ ];
5491
+ var VENDOR_MARKERS = [
5492
+ "node_modules",
5493
+ // dependency root
5494
+ "site-packages",
5495
+ // installed-package root
5496
+ "dist-packages",
5497
+ // distro-packaged root
5498
+ "node:"
5499
+ // runtime-internal module scheme (a stdlib/runtime-root frame)
5500
+ ];
5501
+ function matchFrame(line) {
5502
+ for (const shape of FRAME_SHAPES) {
5503
+ const m = shape.re.exec(line);
5504
+ if (!m) continue;
5505
+ const file = m[shape.file];
5506
+ const lineNo = Number(m[shape.line]);
5507
+ if (!file || !Number.isFinite(lineNo)) continue;
5508
+ const fn = shape.fn !== void 0 ? m[shape.fn] : void 0;
5509
+ return { frame: { file, line: lineNo, ...fn ? { fn } : {} }, deepest: shape.deepest };
5510
+ }
5511
+ return null;
5512
+ }
5513
+ function isApplicationFrame(file) {
5514
+ if (file.startsWith("<")) return false;
5515
+ const norm = file.split("\\").join("/");
5516
+ for (const marker of VENDOR_MARKERS) {
5517
+ if (norm.includes(marker)) return false;
5518
+ }
5519
+ return true;
5520
+ }
5521
+ function deepestApplicationFrame(stacktrace) {
5522
+ if (!stacktrace) return null;
5523
+ const appFrames = [];
5524
+ for (const raw of stacktrace.split("\n")) {
5525
+ const matched = matchFrame(raw);
5526
+ if (!matched) continue;
5527
+ if (!isApplicationFrame(matched.frame.file)) continue;
5528
+ appFrames.push(matched);
5529
+ }
5530
+ if (appFrames.length === 0) return null;
5531
+ const orientation = appFrames.some((f) => f.deepest === "last") ? "last" : "first";
5532
+ const oriented = appFrames.filter((f) => f.deepest === orientation);
5533
+ const chosen = orientation === "last" ? oriented[oriented.length - 1] : oriented[0];
5534
+ return chosen ? chosen.frame : null;
5535
+ }
5536
+
5437
5537
  // src/ingest.ts
5438
5538
  var HOUR_MS = 60 * 60 * 1e3;
5439
5539
  var DAY_MS = 24 * HOUR_MS;
@@ -6285,29 +6385,71 @@ async function appendConnectorIncident(errorsPath, input) {
6285
6385
  await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
6286
6386
  await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
6287
6387
  }
6288
- function incidentAffectedNode(span, graph, scanPath) {
6388
+ function landIncidentCallSite(span, callSite, trusted, graph) {
6389
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
6390
+ const canonicalService = serviceNodeName(graph, span) ?? span.service;
6391
+ const recovered = !trusted;
6392
+ if (graph) {
6393
+ const fusedFileId = (0, import_types8.fileId)(canonicalService, relPath);
6394
+ if (graph.hasNode(fusedFileId)) {
6395
+ const node = landObservedSymbol(
6396
+ graph,
6397
+ fusedFileId,
6398
+ canonicalService,
6399
+ relPath,
6400
+ { ...callSite, relPath },
6401
+ false
6402
+ );
6403
+ return {
6404
+ affectedNode: node,
6405
+ ...recovered ? {
6406
+ codeFilepath: relPath,
6407
+ ...callSite.line !== void 0 ? { codeLineno: callSite.line } : {}
6408
+ } : {}
6409
+ };
6410
+ }
6411
+ if (recovered) return null;
6412
+ }
6413
+ return { affectedNode: (0, import_types8.fileId)(span.service, relPath) };
6414
+ }
6415
+ function serviceNodeName(graph, span) {
6416
+ if (!graph) return void 0;
6417
+ const sid = resolveFusedServiceId(graph, span.service, span.env);
6418
+ if (!graph.hasNode(sid)) return void 0;
6419
+ const node = graph.getNodeAttributes(sid);
6420
+ return typeof node.name === "string" ? node.name : void 0;
6421
+ }
6422
+ function stacktraceCallSite(span, serviceNode, scanPath) {
6423
+ const frame = deepestApplicationFrame(span.exception?.stacktrace);
6424
+ if (!frame) return null;
6425
+ const relPath = relPathForRuntimeFile(frame.file, serviceNode, scanPath);
6426
+ if (!relPath) return null;
6427
+ return { relPath, line: frame.line, ...frame.fn ? { fn: frame.fn } : {} };
6428
+ }
6429
+ function incidentLocus(span, graph, scanPath) {
6289
6430
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
6290
6431
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
6291
6432
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
6292
6433
  if (callSite) {
6293
- const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
6294
- const canonicalService = serviceNode && typeof serviceNode.name === "string" ? serviceNode.name : span.service;
6295
- if (graph) {
6296
- const fusedFileId = (0, import_types8.fileId)(canonicalService, relPath);
6297
- if (graph.hasNode(fusedFileId)) {
6298
- return landObservedSymbol(
6299
- graph,
6300
- fusedFileId,
6301
- canonicalService,
6302
- relPath,
6303
- { ...callSite, relPath },
6304
- false
6305
- );
6306
- }
6434
+ const landed = landIncidentCallSite(span, callSite, true, graph);
6435
+ if (landed) return landed;
6436
+ } else {
6437
+ const recovered = stacktraceCallSite(span, serviceNode, scanPath);
6438
+ if (recovered) {
6439
+ const landed = landIncidentCallSite(span, recovered, false, graph);
6440
+ if (landed) return landed;
6307
6441
  }
6308
- return (0, import_types8.fileId)(span.service, relPath);
6309
6442
  }
6310
- return sid;
6443
+ return { affectedNode: sid };
6444
+ }
6445
+ function incidentAffectedNode(span, graph, scanPath) {
6446
+ return incidentLocus(span, graph, scanPath).affectedNode;
6447
+ }
6448
+ function withRecoveredCodeAttrs(attrs, locus) {
6449
+ if (locus.codeFilepath === void 0) return attrs;
6450
+ attrs[CODE_FILEPATH_ATTR] = locus.codeFilepath;
6451
+ if (locus.codeLineno !== void 0) attrs[CODE_LINENO_ATTR] = locus.codeLineno;
6452
+ return attrs;
6311
6453
  }
6312
6454
  function sanitizeAttributes(attrs) {
6313
6455
  const out = {};
@@ -6320,7 +6462,8 @@ function sanitizeAttributes(attrs) {
6320
6462
  function buildErrorEventForReceiver(span, graph, scanPath) {
6321
6463
  if (span.statusCode !== 2) return null;
6322
6464
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
6323
- const attrs = sanitizeAttributes(span.attributes);
6465
+ const locus = incidentLocus(span, graph, scanPath);
6466
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6324
6467
  return {
6325
6468
  id: `${span.traceId}:${span.spanId}`,
6326
6469
  timestamp: ts,
@@ -6331,7 +6474,7 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
6331
6474
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
6332
6475
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6333
6476
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6334
- affectedNode: incidentAffectedNode(span, graph, scanPath)
6477
+ affectedNode: locus.affectedNode
6335
6478
  };
6336
6479
  }
6337
6480
  function makeErrorSpanWriter(errorsPath, graph, scanPath) {
@@ -6365,7 +6508,8 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
6365
6508
  await appendErrorEvent(ctx, ev);
6366
6509
  }
6367
6510
  async function recordExceptionIncident(ctx, span, ts) {
6368
- const attrs = sanitizeAttributes(span.attributes);
6511
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
6512
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6369
6513
  const ev = {
6370
6514
  id: `${span.traceId}:${span.spanId}`,
6371
6515
  timestamp: ts,
@@ -6376,7 +6520,7 @@ async function recordExceptionIncident(ctx, span, ts) {
6376
6520
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
6377
6521
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6378
6522
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6379
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
6523
+ affectedNode: locus.affectedNode
6380
6524
  };
6381
6525
  await appendErrorEvent(ctx, ev);
6382
6526
  }
@@ -6679,7 +6823,8 @@ async function handleSpan(ctx, span) {
6679
6823
  if (span.statusCode === 2) {
6680
6824
  stitchTrace(ctx.graph, sourceId, ts);
6681
6825
  if (ctx.writeErrorEventInline !== false) {
6682
- const attrs = sanitizeAttributes(span.attributes);
6826
+ const locus = incidentLocus(span, ctx.graph, ctx.scanPath);
6827
+ const attrs = withRecoveredCodeAttrs(sanitizeAttributes(span.attributes), locus);
6683
6828
  const ev = {
6684
6829
  id: `${span.traceId}:${span.spanId}`,
6685
6830
  timestamp: ts,
@@ -6691,10 +6836,11 @@ async function handleSpan(ctx, span) {
6691
6836
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
6692
6837
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
6693
6838
  // Attribute to where the failure originated — the symbol / file / service
6694
- // the throwing span named (incidentAffectedNode, ADR-191) the same
6695
- // source-based attribution the durable receiver write uses, not the
6696
- // outbound edge target this span happened to mint.
6697
- affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
6839
+ // the throwing span named (incidentAffectedNode / ADR-191, extended to
6840
+ // recover the locus from the stacktrace when the span stamped no code.*
6841
+ // attrs, ADR-216) the same source-based attribution the durable
6842
+ // receiver write uses, not the outbound edge target this span minted.
6843
+ affectedNode: locus.affectedNode
6698
6844
  };
6699
6845
  await appendErrorEvent(ctx, ev);
6700
6846
  }
@@ -6868,15 +7014,36 @@ function startStalenessLoop(graph, options = {}) {
6868
7014
  clearInterval(interval);
6869
7015
  };
6870
7016
  }
6871
- async function readErrorEvents(errorsPath) {
7017
+ var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
7018
+ var INCIDENT_READ_MAX_EVENTS = 5e3;
7019
+ async function readErrorFileTail(errorsPath, maxBytes) {
7020
+ const handle = await import_node_fs8.promises.open(errorsPath, "r");
7021
+ try {
7022
+ const { size } = await handle.stat();
7023
+ if (size <= maxBytes) {
7024
+ return (await handle.readFile()).toString("utf8");
7025
+ }
7026
+ const buf = Buffer.alloc(maxBytes);
7027
+ await handle.read(buf, 0, maxBytes, size - maxBytes);
7028
+ const raw = buf.toString("utf8");
7029
+ const firstNewline = raw.indexOf("\n");
7030
+ return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
7031
+ } finally {
7032
+ await handle.close();
7033
+ }
7034
+ }
7035
+ async function readErrorEvents(errorsPath, opts) {
7036
+ let raw;
6872
7037
  try {
6873
- const raw = await import_node_fs8.promises.readFile(errorsPath, "utf8");
6874
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
6875
- return dedupeIncidents(events);
7038
+ raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
6876
7039
  } catch (err) {
6877
7040
  if (err.code === "ENOENT") return [];
6878
7041
  throw err;
6879
7042
  }
7043
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
7044
+ const deduped = dedupeIncidents(events);
7045
+ const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
7046
+ return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
6880
7047
  }
6881
7048
  function isSynthesizedHttpIncident(ev) {
6882
7049
  if (ev.exceptionType || ev.exceptionStacktrace) return false;
@@ -15981,6 +16148,120 @@ function detectColumnDrift(node) {
15981
16148
  }
15982
16149
  return out;
15983
16150
  }
16151
+ var SYMBOL_MISMATCH_PATTERNS = [
16152
+ {
16153
+ // "'ListProductsResponse' object has no attribute 'products_list'" and kin.
16154
+ kind: "missing-attribute",
16155
+ patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
16156
+ },
16157
+ {
16158
+ // "object has no field 'X'", "no such field X", "unknown field X".
16159
+ kind: "missing-field",
16160
+ patterns: [
16161
+ /\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
16162
+ ]
16163
+ },
16164
+ {
16165
+ // "has no property X", "no property named X".
16166
+ kind: "missing-property",
16167
+ patterns: [
16168
+ /\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
16169
+ ]
16170
+ },
16171
+ {
16172
+ // "no such column: X", "unknown column 'X'", "column X does not exist".
16173
+ kind: "missing-column",
16174
+ patterns: [
16175
+ /\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
16176
+ /\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
16177
+ /\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
16178
+ ]
16179
+ },
16180
+ {
16181
+ // "undefined method `foo' for X" — kept to the unambiguous form so a generic
16182
+ // "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
16183
+ // mismatch) does not get miscategorised here.
16184
+ kind: "undefined-method",
16185
+ patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
16186
+ }
16187
+ ];
16188
+ function classifySymbolMismatch(message) {
16189
+ for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
16190
+ for (const re of entry2.patterns) {
16191
+ const m = re.exec(message);
16192
+ if (m) {
16193
+ const captured = m[1];
16194
+ return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
16195
+ }
16196
+ }
16197
+ }
16198
+ return null;
16199
+ }
16200
+ function symbolLocus(graph, ev) {
16201
+ const attrs = ev.attributes ?? {};
16202
+ const filepath = codeFilepathOf(attrs);
16203
+ const lineno = codeLinenoOf(attrs);
16204
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
16205
+ const affected = ev.affectedNode;
16206
+ const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
16207
+ const affectedIsCode = affectedInGraph && ((0, import_types57.parseSymbolId)(affected) !== null || (0, import_types57.parseFileId)(affected) !== null);
16208
+ if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
16209
+ if (!location) return null;
16210
+ if (affectedInGraph) return { node: affected, location };
16211
+ const svc = (0, import_types57.serviceId)(ev.service);
16212
+ if (graph.hasNode(svc)) return { node: svc, location };
16213
+ return null;
16214
+ }
16215
+ var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
16216
+ function detectSymbolMismatches(graph, incidents) {
16217
+ const groups = /* @__PURE__ */ new Map();
16218
+ for (const ev of incidents) {
16219
+ const classified = classifySymbolMismatch(ev.errorMessage);
16220
+ if (!classified) continue;
16221
+ const locus = symbolLocus(graph, ev);
16222
+ if (!locus) continue;
16223
+ const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
16224
+ const existing = groups.get(key);
16225
+ if (!existing) {
16226
+ groups.set(key, {
16227
+ node: locus.node,
16228
+ kind: classified.kind,
16229
+ ...classified.symbol ? { symbol: classified.symbol } : {},
16230
+ ...locus.location ? { location: locus.location } : {},
16231
+ latest: ev,
16232
+ count: 1
16233
+ });
16234
+ } else {
16235
+ existing.count += 1;
16236
+ if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
16237
+ existing.latest = ev;
16238
+ if (locus.location) existing.location = locus.location;
16239
+ }
16240
+ }
16241
+ }
16242
+ const out = [];
16243
+ for (const g of groups.values()) {
16244
+ const member = g.symbol ? `\`${g.symbol}\`` : "a member";
16245
+ const where = g.location ? ` at ${g.location}` : "";
16246
+ const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
16247
+ out.push({
16248
+ type: "observed-symbol-mismatch",
16249
+ source: g.node,
16250
+ target: g.node,
16251
+ mismatchKind: g.kind,
16252
+ ...g.symbol ? { symbol: g.symbol } : {},
16253
+ ...g.location ? { location: g.location } : {},
16254
+ provenance: import_types57.Provenance.INFERRED,
16255
+ incidentId: g.latest.id,
16256
+ errorMessage: g.latest.errorMessage,
16257
+ incidentCount: g.count,
16258
+ confidence: SYMBOL_MISMATCH_CONFIDENCE,
16259
+ 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.`,
16260
+ 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."
16261
+ });
16262
+ }
16263
+ return out;
16264
+ }
15984
16265
  function involvesNode(d, nodeId) {
15985
16266
  return d.source === nodeId || d.target === nodeId;
15986
16267
  }
@@ -16066,6 +16347,9 @@ function computeDivergences(graph, opts = {}) {
16066
16347
  for (const d of detectColumnDrift(n)) all.push(d);
16067
16348
  }
16068
16349
  });
16350
+ if (opts.incidents && opts.incidents.length > 0) {
16351
+ for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
16352
+ }
16069
16353
  const reconciled = suppressHostMismatchHalves(all);
16070
16354
  const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
16071
16355
  let filtered = dampened;
@@ -16086,7 +16370,12 @@ function computeDivergences(graph, opts = {}) {
16086
16370
  "missing-observed": 1,
16087
16371
  "version-mismatch": 2,
16088
16372
  "host-mismatch": 3,
16089
- "compat-violation": 4
16373
+ "compat-violation": 4,
16374
+ // Symbol/field-grain (ADR-215) rides the confidence sort like every other
16375
+ // type; this only breaks a confidence tie, and it orders last so a same-
16376
+ // confidence edge finding leads. In practice it carries the INFERRED grade
16377
+ // (0.6), so it sits below the high-confidence edge divergences already.
16378
+ "observed-symbol-mismatch": 5
16090
16379
  };
16091
16380
  filtered.sort((a, b) => {
16092
16381
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
@@ -16097,7 +16386,10 @@ function computeDivergences(graph, opts = {}) {
16097
16386
  if (a.target !== b.target) return a.target.localeCompare(b.target);
16098
16387
  const ac = "column" in a && a.column ? a.column : "";
16099
16388
  const bc = "column" in b && b.column ? b.column : "";
16100
- return ac.localeCompare(bc);
16389
+ if (ac !== bc) return ac.localeCompare(bc);
16390
+ const asym = "symbol" in a && a.symbol ? a.symbol : "";
16391
+ const bsym = "symbol" in b && b.symbol ? b.symbol : "";
16392
+ return asym.localeCompare(bsym);
16101
16393
  });
16102
16394
  return import_types57.DivergenceResultSchema.parse({
16103
16395
  divergences: filtered,
@@ -17157,10 +17449,15 @@ function divergenceLine(d) {
17157
17449
  if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
17158
17450
  return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
17159
17451
  }
17452
+ if (d.type === "observed-symbol-mismatch") {
17453
+ const at = d.location ? ` at ${d.location}` : "";
17454
+ const member = d.symbol ? ` ${d.symbol}` : "";
17455
+ return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
17456
+ }
17160
17457
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
17161
17458
  }
17162
- function buildDivergenceSection(graph, node) {
17163
- const result = computeDivergences(graph, { node });
17459
+ function buildDivergenceSection(graph, node, incidents) {
17460
+ const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
17164
17461
  if (result.totalAffected === 0) return null;
17165
17462
  const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17166
17463
  text: divergenceLine(d),
@@ -17173,8 +17470,8 @@ function buildDivergenceSection(graph, node) {
17173
17470
  facts
17174
17471
  };
17175
17472
  }
17176
- function buildGlobalDivergenceSection(graph) {
17177
- const result = computeDivergences(graph);
17473
+ function buildGlobalDivergenceSection(graph, incidents) {
17474
+ const result = computeDivergences(graph, incidents ? { incidents } : {});
17178
17475
  if (result.totalAffected === 0) {
17179
17476
  return {
17180
17477
  heading: "Divergences (EXTRACTED vs OBSERVED)",
@@ -17272,7 +17569,7 @@ function buildOverviewSections(graph, incidents) {
17272
17569
  }))
17273
17570
  });
17274
17571
  }
17275
- const div = computeDivergences(graph);
17572
+ const div = computeDivergences(graph, incidents ? { incidents } : {});
17276
17573
  sections.push({
17277
17574
  heading: "Divergences",
17278
17575
  facts: [
@@ -17286,7 +17583,7 @@ function buildOverviewSections(graph, incidents) {
17286
17583
  function buildGlobalSections(intent, graph, incidents) {
17287
17584
  switch (intent) {
17288
17585
  case "divergence":
17289
- return [buildGlobalDivergenceSection(graph)];
17586
+ return [buildGlobalDivergenceSection(graph, incidents)];
17290
17587
  case "incidents":
17291
17588
  return [buildGlobalIncidentsSection(incidents)];
17292
17589
  case "overview":
@@ -17317,7 +17614,7 @@ function buildSection(kind, graph, node, incidents, now) {
17317
17614
  case "incidents":
17318
17615
  return buildIncidentsSection(node, incidents);
17319
17616
  case "divergence":
17320
- return buildDivergenceSection(graph, node);
17617
+ return buildDivergenceSection(graph, node, incidents);
17321
17618
  }
17322
17619
  }
17323
17620
  function summarizeGlobal(intent, sections) {
@@ -21537,6 +21834,8 @@ async function deprovisionConnector(entry2, env = process.env, fetchImpl) {
21537
21834
  }
21538
21835
 
21539
21836
  // src/api.ts
21837
+ var INCIDENT_LIST_DEFAULT_LIMIT = 50;
21838
+ var INCIDENT_LIST_MAX_LIMIT = 200;
21540
21839
  function serializeGraph(graph) {
21541
21840
  const nodes = [];
21542
21841
  graph.forEachNode((_id, attrs) => {
@@ -21716,10 +22015,13 @@ function registerRoutes(scope, ctx) {
21716
22015
  }
21717
22016
  minConfidence = n;
21718
22017
  }
22018
+ const epath = errorsPathFor(proj);
22019
+ const incidents = epath ? await readErrorEvents(epath) : [];
21719
22020
  return computeDivergences(proj.graph, {
21720
22021
  ...typeFilter ? { type: typeFilter } : {},
21721
22022
  ...minConfidence !== void 0 ? { minConfidence } : {},
21722
- ...req.query.node ? { node: req.query.node } : {}
22023
+ ...req.query.node ? { node: req.query.node } : {},
22024
+ incidents
21723
22025
  });
21724
22026
  });
21725
22027
  scope.get("/incidents", async (req, reply) => {
@@ -21729,10 +22031,11 @@ function registerRoutes(scope, ctx) {
21729
22031
  if (!epath) return { count: 0, total: 0, events: [] };
21730
22032
  const events = await readErrorEvents(epath);
21731
22033
  const total = events.length;
21732
- const limit = req.query.limit ? Number(req.query.limit) : 50;
21733
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
22034
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
22035
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
21734
22036
  const sliced = events.slice(0, safeLimit);
21735
- return { count: sliced.length, total, events: sliced };
22037
+ const omitted = total - sliced.length;
22038
+ return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
21736
22039
  });
21737
22040
  scope.get("/stale-events", async (req, reply) => {
21738
22041
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -21841,16 +22144,20 @@ function registerRoutes(scope, ctx) {
21841
22144
  const filtered = events.filter(
21842
22145
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
21843
22146
  );
21844
- return { count: filtered.length, total: filtered.length, events: filtered };
22147
+ const total = filtered.length;
22148
+ const limit = req.query.limit ? Number(req.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
22149
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
22150
+ const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
22151
+ const omitted = total - recent.length;
22152
+ return {
22153
+ count: recent.length,
22154
+ total,
22155
+ events: recent,
22156
+ ...omitted > 0 ? { omitted } : {}
22157
+ };
21845
22158
  };
21846
- scope.get(
21847
- "/incidents/:nodeId",
21848
- incidentHistoryHandler
21849
- );
21850
- scope.get(
21851
- "/graph/incident-history/:nodeId",
21852
- incidentHistoryHandler
21853
- );
22159
+ scope.get("/incidents/:nodeId", incidentHistoryHandler);
22160
+ scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
21854
22161
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
21855
22162
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21856
22163
  if (!proj) return;
@@ -24109,7 +24416,7 @@ var OTEL_ENV = {
24109
24416
  key: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
24110
24417
  value: "http://localhost:4318/projects/<project>/v1/traces"
24111
24418
  };
24112
- function serviceNodeName(pkg, serviceDir) {
24419
+ function serviceNodeName2(pkg, serviceDir) {
24113
24420
  return pkg.name ?? import_node_path81.default.basename(serviceDir);
24114
24421
  }
24115
24422
  function projectToken(pkg, serviceDir, project) {
@@ -24493,7 +24800,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
24493
24800
  }
24494
24801
  dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
24495
24802
  }
24496
- const svcName = serviceNodeName(pkg, serviceDir);
24803
+ const svcName = serviceNodeName2(pkg, serviceDir);
24497
24804
  const projectName = projectToken(pkg, serviceDir, project);
24498
24805
  const registrations = nonBundled.map((i) => i.registration);
24499
24806
  const generatedFiles = [];
@@ -24592,7 +24899,7 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
24592
24899
  generatedFiles.push({
24593
24900
  file: envNeatFile,
24594
24901
  contents: renderEnvNeat(
24595
- serviceNodeName(pkg, serviceDir),
24902
+ serviceNodeName2(pkg, serviceDir),
24596
24903
  projectToken(pkg, serviceDir, project)
24597
24904
  ),
24598
24905
  skipIfExists: true
@@ -24602,7 +24909,7 @@ async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
24602
24909
  function renderFrameworkOtelInitForPkg(template, pkg, serviceDir, project) {
24603
24910
  return renderFrameworkOtelInit(
24604
24911
  template,
24605
- serviceNodeName(pkg, serviceDir),
24912
+ serviceNodeName2(pkg, serviceDir),
24606
24913
  projectToken(pkg, serviceDir, project)
24607
24914
  );
24608
24915
  }
@@ -24982,7 +25289,7 @@ async function plan(serviceDir, opts) {
24982
25289
  } catch {
24983
25290
  return { ...empty, libOnly: true };
24984
25291
  }
24985
- const svcName = serviceNodeName(pkg, serviceDir);
25292
+ const svcName = serviceNodeName2(pkg, serviceDir);
24986
25293
  const projectName = projectToken(pkg, serviceDir, project);
24987
25294
  const registrations = nonBundled.map((i) => i.registration);
24988
25295
  const generatedFiles = [];
@@ -28841,6 +29148,11 @@ function formatDivergenceLine(d) {
28841
29148
  return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
28842
29149
  case "compat-violation":
28843
29150
  return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
29151
+ case "observed-symbol-mismatch": {
29152
+ const at = d.location ? ` at ${d.location}` : "";
29153
+ const member = d.symbol ? ` ${d.symbol}` : "";
29154
+ return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
29155
+ }
28844
29156
  }
28845
29157
  }
28846
29158
  async function runDivergences(client, input) {
@@ -28994,6 +29306,11 @@ function formatDivergenceLine2(d) {
28994
29306
  return `\u26A0 divergence [host-mismatch] ${d.source} \u2192 ${d.target} declared host ${d.extractedHost}, observed host ${d.observedHost}`;
28995
29307
  case "compat-violation":
28996
29308
  return `\u26A0 divergence [compat-violation] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
29309
+ case "observed-symbol-mismatch": {
29310
+ const at = d.location ? ` at ${d.location}` : "";
29311
+ const member = d.symbol ? ` ${d.symbol}` : " a member";
29312
+ return `\u26A0 divergence [observed-symbol-mismatch] ${d.source}${at} reads${member} the runtime object does not have (${d.mismatchKind})`;
29313
+ }
28997
29314
  }
28998
29315
  }
28999
29316
  function formatStaleLine(edgeId) {
@@ -29970,7 +30287,8 @@ async function runInit(opts) {
29970
30287
  console.log(`snapshot: ${opts.outPath}`);
29971
30288
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
29972
30289
  console.log("");
29973
- const divergenceResult = computeDivergences(graph);
30290
+ const summaryIncidents = await readErrorEvents(errorsPath);
30291
+ const divergenceResult = computeDivergences(graph, { incidents: summaryIncidents });
29974
30292
  console.log(
29975
30293
  renderValueForwardSummary({
29976
30294
  graph,