@neat.is/core 0.9.8-dev.20260829 → 0.9.9

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.
@@ -931,6 +931,7 @@ import {
931
931
  frontierId,
932
932
  graphqlOperationId,
933
933
  grpcMethodId,
934
+ incidentKindOf,
934
935
  inferredEdgeId,
935
936
  infraId,
936
937
  observedEdgeId,
@@ -4580,11 +4581,25 @@ function upsertInferredEdge(graph, type, source, target, ts) {
4580
4581
  };
4581
4582
  graph.addEdgeWithKey(id, source, target, edge);
4582
4583
  }
4584
+ function emitIncidentEvent(project, ev) {
4585
+ emitNeatEvent({
4586
+ type: "incident",
4587
+ project,
4588
+ payload: {
4589
+ incidentId: ev.id,
4590
+ affectedNode: ev.affectedNode,
4591
+ service: ev.service,
4592
+ incidentKind: incidentKindOf(ev),
4593
+ at: ev.timestamp
4594
+ }
4595
+ });
4596
+ }
4583
4597
  async function appendErrorEvent(ctx, ev) {
4584
4598
  await fs7.mkdir(path8.dirname(ctx.errorsPath), { recursive: true });
4585
4599
  await fs7.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
4600
+ emitIncidentEvent(ctx.project ?? DEFAULT_PROJECT, ev);
4586
4601
  }
4587
- async function appendConnectorIncident(errorsPath, input) {
4602
+ async function appendConnectorIncident(errorsPath, input, project) {
4588
4603
  const ev = {
4589
4604
  id: input.id,
4590
4605
  timestamp: input.timestamp,
@@ -4598,6 +4613,7 @@ async function appendConnectorIncident(errorsPath, input) {
4598
4613
  };
4599
4614
  await fs7.mkdir(path8.dirname(errorsPath), { recursive: true });
4600
4615
  await fs7.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4616
+ if (project) emitIncidentEvent(project, ev);
4601
4617
  }
4602
4618
  function landIncidentCallSite(span, callSite, trusted, graph) {
4603
4619
  const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
@@ -4691,12 +4707,13 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
4691
4707
  affectedNode: locus.affectedNode
4692
4708
  };
4693
4709
  }
4694
- function makeErrorSpanWriter(errorsPath, graph, scanPath) {
4710
+ function makeErrorSpanWriter(errorsPath, graph, scanPath, project = DEFAULT_PROJECT) {
4695
4711
  return async (span) => {
4696
4712
  const ev = buildErrorEventForReceiver(span, graph, scanPath);
4697
4713
  if (!ev) return;
4698
4714
  await fs7.mkdir(path8.dirname(errorsPath), { recursive: true });
4699
4715
  await fs7.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
4716
+ emitIncidentEvent(project, ev);
4700
4717
  };
4701
4718
  }
4702
4719
  async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
@@ -16931,8 +16948,162 @@ function queryLogEntries(opts) {
16931
16948
  return merged;
16932
16949
  }
16933
16950
 
16951
+ // src/goodybag.ts
16952
+ import {
16953
+ IncidentCardSchema,
16954
+ Provenance as Provenance26,
16955
+ incidentKindOf as incidentKindOf2
16956
+ } from "@neat.is/types";
16957
+ var CODE_FILEPATH_ATTR2 = "code.filepath";
16958
+ var CODE_LINENO_ATTR2 = "code.lineno";
16959
+ var BLAST_NEAREST_LIMIT = 5;
16960
+ function grainOf2(graph, nodeId) {
16961
+ if (graph.hasNode(nodeId)) {
16962
+ const t = graph.getNodeAttributes(nodeId).type;
16963
+ if (typeof t === "string" && t.length > 0) {
16964
+ return (t.endsWith("Node") ? t.slice(0, -4) : t).toLowerCase();
16965
+ }
16966
+ }
16967
+ const colon = nodeId.indexOf(":");
16968
+ return colon > 0 ? nodeId.slice(0, colon) : "unknown";
16969
+ }
16970
+ function locusOf(graph, ev) {
16971
+ const file = ev.attributes?.[CODE_FILEPATH_ATTR2];
16972
+ if (typeof file !== "string" || file.length === 0) return null;
16973
+ const rawLine = ev.attributes?.[CODE_LINENO_ATTR2];
16974
+ const line = typeof rawLine === "number" ? rawLine : Number(rawLine);
16975
+ const node = graph.hasNode(ev.affectedNode) ? graph.getNodeAttributes(ev.affectedNode) : void 0;
16976
+ return {
16977
+ file,
16978
+ ...Number.isFinite(line) ? { lineStart: line, lineEnd: line } : {},
16979
+ ...node?.name ? { symbol: node.name } : {},
16980
+ service: node?.service ?? ev.service,
16981
+ provenance: Provenance26.OBSERVED
16982
+ };
16983
+ }
16984
+ function locusFromNode(graph, nodeId) {
16985
+ if (!graph.hasNode(nodeId)) return null;
16986
+ const n = graph.getNodeAttributes(nodeId);
16987
+ const file = n.relPath ?? n.path;
16988
+ if (typeof file !== "string" || file.length === 0) return null;
16989
+ const start = n.span?.startLine;
16990
+ const end = n.span?.endLine;
16991
+ return {
16992
+ file,
16993
+ ...typeof start === "number" ? { lineStart: start } : {},
16994
+ ...typeof end === "number" ? { lineEnd: end } : {},
16995
+ ...n.qualname ? { symbol: shortLabel(graph, nodeId) } : {},
16996
+ ...n.service ? { service: n.service } : {},
16997
+ provenance: Provenance26.INFERRED
16998
+ };
16999
+ }
17000
+ function promoteCauseLocus(graph, causeNode, incidents) {
17001
+ const native = incidents.find(
17002
+ (e) => e.affectedNode === causeNode && typeof e.attributes?.[CODE_FILEPATH_ATTR2] === "string"
17003
+ );
17004
+ if (native) {
17005
+ const l = locusOf(graph, native);
17006
+ if (l) return { ...l, symbol: shortLabel(graph, causeNode), provenance: Provenance26.INFERRED };
17007
+ }
17008
+ return locusFromNode(graph, causeNode);
17009
+ }
17010
+ function divergenceSummary(d) {
17011
+ const column = "column" in d && d.column ? `.${d.column}` : "";
17012
+ const label = "table" in d && d.table ? `${d.table}${column}` : `${d.source} \u2192 ${d.target}`;
17013
+ return label;
17014
+ }
17015
+ function baseName(p) {
17016
+ const parts = p.split(/[\\/]/);
17017
+ return parts[parts.length - 1] || p;
17018
+ }
17019
+ function shortLabel(graph, nodeId) {
17020
+ if (graph.hasNode(nodeId)) {
17021
+ const name = graph.getNodeAttributes(nodeId).name;
17022
+ if (typeof name === "string" && name.length > 0) return name;
17023
+ }
17024
+ const afterHash = nodeId.includes("#") ? nodeId.slice(nodeId.lastIndexOf("#") + 1) : nodeId;
17025
+ return afterHash.includes(":") ? afterHash.slice(afterHash.lastIndexOf(":") + 1) : afterHash;
17026
+ }
17027
+ function renderHeadline(graph, ev, locus, causeNode) {
17028
+ const what = ev.exceptionType ? `raised ${ev.exceptionType}` : ev.errorMessage || "failed";
17029
+ const causeLabel = causeNode && causeNode !== ev.affectedNode ? shortLabel(graph, causeNode) : "";
17030
+ if (locus) {
17031
+ const base = baseName(locus.file);
17032
+ const lines = locus.lineStart != null ? locus.lineEnd && locus.lineEnd !== locus.lineStart ? `LINES ${locus.lineStart}-${locus.lineEnd}` : `LINE ${locus.lineStart}` : "";
17033
+ const symbol = locus.symbol ?? (grainOf2(graph, ev.affectedNode) === "symbol" ? shortLabel(graph, ev.affectedNode) : void 0);
17034
+ const subject = symbol ? `SYMBOL ${symbol}` : `FILE ${base}`;
17035
+ const at = symbol ? `${lines ? `${lines} in ` : ""}${base}` : lines;
17036
+ const where = at ? ` at ${at}` : "";
17037
+ const svc = locus.service ?? ev.service;
17038
+ const cause2 = causeLabel && causeLabel !== symbol ? ` \u2192 root cause ${causeLabel}` : "";
17039
+ return `${subject}${where} (SERVICE ${svc}) ${what} at ${ev.timestamp}${cause2}`;
17040
+ }
17041
+ const cause = causeLabel ? ` \u2192 root cause ${causeLabel}` : "";
17042
+ return `SERVICE ${ev.service} ${what} at ${ev.timestamp}${cause}`;
17043
+ }
17044
+ function buildIncidentCard(graph, errorEvent, incidents, policies) {
17045
+ const affected = errorEvent.affectedNode;
17046
+ let locus = locusOf(graph, errorEvent);
17047
+ const inGraph = graph.hasNode(affected);
17048
+ const rc = inGraph ? getRootCause(graph, affected, errorEvent, incidents) : null;
17049
+ let rootCause = null;
17050
+ if (rc) {
17051
+ const provs = rc.edgeProvenances ?? [];
17052
+ const chain = (rc.traversalPath ?? []).map((node, i) => ({
17053
+ node,
17054
+ grain: grainOf2(graph, node),
17055
+ provenance: provs[i] ?? provs[provs.length - 1] ?? Provenance26.INFERRED
17056
+ }));
17057
+ rootCause = {
17058
+ node: rc.rootCauseNode,
17059
+ ...rc.candidates?.[0]?.classification ? { classification: rc.candidates[0].classification } : {},
17060
+ reason: rc.rootCauseReason,
17061
+ confidence: rc.confidence,
17062
+ fix: rc.fixRecommendation ?? null,
17063
+ chain
17064
+ };
17065
+ }
17066
+ if (locus === null && rootCause) {
17067
+ locus = promoteCauseLocus(graph, rootCause.node, incidents);
17068
+ }
17069
+ const blast = inGraph ? getBlastRadius(graph, affected) : { origin: affected, affectedNodes: [], totalAffected: 0 };
17070
+ const nearest = [...blast.affectedNodes].sort((a, b) => a.distance - b.distance).slice(0, BLAST_NEAREST_LIMIT).map((n) => ({ node: n.nodeId, distance: n.distance, provenance: n.edgeProvenance }));
17071
+ const applicable = inGraph ? selectApplicablePolicies(graph, policies, affected) : [];
17072
+ const policyCards = applicable.map((p) => ({
17073
+ policyName: p.policyName,
17074
+ severity: p.severity,
17075
+ message: p.reason
17076
+ }));
17077
+ const divergences = inGraph ? computeDivergences(graph, { node: affected, incidents }).divergences.map((d) => ({
17078
+ type: d.type,
17079
+ summary: divergenceSummary(d)
17080
+ })) : [];
17081
+ const card = {
17082
+ kind: "incident",
17083
+ id: errorEvent.id,
17084
+ at: errorEvent.timestamp,
17085
+ incidentKind: incidentKindOf2(errorEvent),
17086
+ service: errorEvent.service,
17087
+ affectedNode: affected,
17088
+ message: errorEvent.errorMessage,
17089
+ ...errorEvent.exceptionType ? { exceptionType: errorEvent.exceptionType } : {},
17090
+ ...errorEvent.httpStatusCode !== void 0 ? { httpStatusCode: errorEvent.httpStatusCode } : {},
17091
+ ...errorEvent.incidentCount !== void 0 ? { count: errorEvent.incidentCount } : {},
17092
+ ...errorEvent.firstTimestamp && errorEvent.lastTimestamp ? { window: { first: errorEvent.firstTimestamp, last: errorEvent.lastTimestamp } } : {},
17093
+ ...errorEvent.traceId ? { traceId: errorEvent.traceId } : {},
17094
+ ...errorEvent.spanId ? { spanId: errorEvent.spanId } : {},
17095
+ locus,
17096
+ rootCause,
17097
+ ...nearest.length > 0 ? { blastRadius: { totalAffected: blast.totalAffected, nearest } } : {},
17098
+ ...policyCards.length > 0 ? { policies: policyCards } : {},
17099
+ ...divergences.length > 0 ? { divergence: divergences } : {},
17100
+ headline: renderHeadline(graph, errorEvent, locus, rootCause?.node ?? null)
17101
+ };
17102
+ return IncidentCardSchema.parse(card);
17103
+ }
17104
+
16934
17105
  // src/ask.ts
16935
- import { AskResultSchema, NodeType as NodeType32, Provenance as Provenance26, serviceId as serviceId14 } from "@neat.is/types";
17106
+ import { AskResultSchema, NodeType as NodeType32, Provenance as Provenance27, serviceId as serviceId14 } from "@neat.is/types";
16936
17107
  var DEFAULT_MAX_NODES = 3;
16937
17108
  var MAX_FACTS_PER_SECTION = 6;
16938
17109
  var INTENT_RULES = [
@@ -17217,7 +17388,7 @@ function edgeSignalNote(e) {
17217
17388
  function buildRootCauseSection(graph, node, incidents, now) {
17218
17389
  const result = getRootCause(graph, node, void 0, incidents, { now });
17219
17390
  if (!result) return null;
17220
- const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? Provenance26.OBSERVED;
17391
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? Provenance27.OBSERVED;
17221
17392
  const facts = [
17222
17393
  {
17223
17394
  text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
@@ -17267,7 +17438,7 @@ function buildObservedSection(graph, node) {
17267
17438
  if (result.observed && result.inboundObservedCount > 0) {
17268
17439
  facts.push({
17269
17440
  text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
17270
- provenance: Provenance26.OBSERVED
17441
+ provenance: Provenance27.OBSERVED
17271
17442
  });
17272
17443
  } else {
17273
17444
  return null;
@@ -17295,7 +17466,7 @@ function buildIncidentsSection(node, incidents) {
17295
17466
  const facts = ordered.map((ev) => ({
17296
17467
  text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
17297
17468
  // ErrorEvents are observation records — OBSERVED by definition.
17298
- provenance: Provenance26.OBSERVED
17469
+ provenance: Provenance27.OBSERVED
17299
17470
  }));
17300
17471
  return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
17301
17472
  }
@@ -17370,7 +17541,7 @@ function buildGlobalIncidentsSection(incidents) {
17370
17541
  const rows = [...byKey.values()].sort((a, b) => b.count - a.count || b.latest.localeCompare(a.latest) || a.key.localeCompare(b.key)).slice(0, MAX_FACTS_PER_SECTION);
17371
17542
  const facts = rows.map((r) => ({
17372
17543
  text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
17373
- provenance: Provenance26.OBSERVED
17544
+ provenance: Provenance27.OBSERVED
17374
17545
  }));
17375
17546
  return {
17376
17547
  heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
@@ -17398,7 +17569,7 @@ function buildOverviewSections(graph, incidents) {
17398
17569
  text: `${count(NodeType32.ServiceNode)} services, ${count(NodeType32.FileNode)} files, ${count(NodeType32.SymbolNode)} symbols, ${count(NodeType32.DatabaseNode)} databases`
17399
17570
  }
17400
17571
  ];
17401
- for (const p of [Provenance26.EXTRACTED, Provenance26.OBSERVED, Provenance26.INFERRED, Provenance26.STALE]) {
17572
+ for (const p of [Provenance27.EXTRACTED, Provenance27.OBSERVED, Provenance27.INFERRED, Provenance27.STALE]) {
17402
17573
  const n = edgeByProv.get(p) ?? 0;
17403
17574
  if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
17404
17575
  }
@@ -17423,7 +17594,7 @@ function buildOverviewSections(graph, incidents) {
17423
17594
  heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
17424
17595
  facts: top.map(([k, n]) => ({
17425
17596
  text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
17426
- provenance: Provenance26.OBSERVED
17597
+ provenance: Provenance27.OBSERVED
17427
17598
  }))
17428
17599
  });
17429
17600
  }
@@ -17954,14 +18125,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
17954
18125
  }
17955
18126
 
17956
18127
  // src/connectors/index.ts
17957
- import { NodeType as NodeType33, parseFileId as parseFileId2, Provenance as Provenance27 } from "@neat.is/types";
18128
+ import { NodeType as NodeType33, parseFileId as parseFileId2, Provenance as Provenance28 } from "@neat.is/types";
17958
18129
  var NO_ENV = "unknown";
17959
18130
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
17960
18131
  if (!graph.hasNode(targetNodeId)) return void 0;
17961
18132
  const sites = [];
17962
18133
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
17963
18134
  const edge = graph.getEdgeAttributes(edgeId);
17964
- if (edge.provenance !== Provenance27.EXTRACTED) continue;
18135
+ if (edge.provenance !== Provenance28.EXTRACTED) continue;
17965
18136
  const parsed = parseFileId2(edge.source);
17966
18137
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
17967
18138
  const site = { relPath: edge.evidence.file };
@@ -18003,7 +18174,7 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
18003
18174
  errorMessage: signal.incident.errorMessage,
18004
18175
  ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
18005
18176
  affectedNode: resolved.targetNodeId
18006
- });
18177
+ }, ctx.project);
18007
18178
  continue;
18008
18179
  }
18009
18180
  if (resolved.ensureInfraNode) {
@@ -21277,6 +21448,7 @@ async function startConnectorPolling(input) {
21277
21448
  registration.connector,
21278
21449
  {
21279
21450
  projectDir: input.projectDir,
21451
+ project: input.project,
21280
21452
  credentials: registration.credentials,
21281
21453
  ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
21282
21454
  },
@@ -21620,6 +21792,7 @@ function registerRoutes(scope, ctx) {
21620
21792
  reg.connector,
21621
21793
  {
21622
21794
  projectDir: proj.scanPath ?? "",
21795
+ project: proj.name,
21623
21796
  credentials: reg.credentials,
21624
21797
  ...incidentsPath ? { errorsPath: incidentsPath } : {}
21625
21798
  },
@@ -21683,6 +21856,39 @@ function registerRoutes(scope, ctx) {
21683
21856
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
21684
21857
  return result;
21685
21858
  });
21859
+ scope.get("/graph/incident-card/:nodeId", async (req, reply) => {
21860
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21861
+ if (!proj) return;
21862
+ const { nodeId } = req.params;
21863
+ if (!proj.graph.hasNode(nodeId)) {
21864
+ return reply.code(404).send({ error: "node not found", id: nodeId });
21865
+ }
21866
+ const epath = errorsPathFor(proj);
21867
+ const incidents = epath ? await readErrorEvents(epath) : [];
21868
+ let errorEvent;
21869
+ if (req.query.errorId) {
21870
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
21871
+ if (!errorEvent) {
21872
+ return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
21873
+ }
21874
+ } else {
21875
+ const svc = nodeId.replace(/^service:/, "");
21876
+ errorEvent = [...incidents].filter((e) => e.affectedNode === nodeId || e.service === svc).sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))[0];
21877
+ if (!errorEvent) {
21878
+ return reply.code(404).send({ error: "no incident found for node", id: nodeId });
21879
+ }
21880
+ }
21881
+ const policyPath = ctx.policyFilePathFor(proj);
21882
+ let policies = [];
21883
+ if (policyPath) {
21884
+ try {
21885
+ policies = await loadPolicyFile(policyPath);
21886
+ } catch {
21887
+ policies = [];
21888
+ }
21889
+ }
21890
+ return buildIncidentCard(proj.graph, errorEvent, incidents, policies);
21891
+ });
21686
21892
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
21687
21893
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21688
21894
  if (!proj) return;
@@ -22304,4 +22510,4 @@ export {
22304
22510
  deprovisionConnector,
22305
22511
  buildApi
22306
22512
  };
22307
- //# sourceMappingURL=chunk-KZBFP7L6.js.map
22513
+ //# sourceMappingURL=chunk-B6KJG5KN.js.map