@neat.is/core 0.9.8-dev.20260828 → 0.9.8

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,130 @@ 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 divergenceSummary(d) {
16985
+ const column = "column" in d && d.column ? `.${d.column}` : "";
16986
+ const label = "table" in d && d.table ? `${d.table}${column}` : `${d.source} \u2192 ${d.target}`;
16987
+ return label;
16988
+ }
16989
+ function baseName(p) {
16990
+ const parts = p.split(/[\\/]/);
16991
+ return parts[parts.length - 1] || p;
16992
+ }
16993
+ function shortLabel(graph, nodeId) {
16994
+ if (graph.hasNode(nodeId)) {
16995
+ const name = graph.getNodeAttributes(nodeId).name;
16996
+ if (typeof name === "string" && name.length > 0) return name;
16997
+ }
16998
+ const afterHash = nodeId.includes("#") ? nodeId.slice(nodeId.lastIndexOf("#") + 1) : nodeId;
16999
+ return afterHash.includes(":") ? afterHash.slice(afterHash.lastIndexOf(":") + 1) : afterHash;
17000
+ }
17001
+ function renderHeadline(graph, ev, locus, causeNode) {
17002
+ const what = ev.exceptionType ? `raised ${ev.exceptionType}` : ev.errorMessage || "failed";
17003
+ const cause = causeNode && causeNode !== ev.affectedNode ? ` \u2192 root cause ${shortLabel(graph, causeNode)}` : "";
17004
+ if (locus) {
17005
+ const base = baseName(locus.file);
17006
+ const lines = locus.lineStart != null ? locus.lineEnd && locus.lineEnd !== locus.lineStart ? `LINES ${locus.lineStart}-${locus.lineEnd}` : `LINE ${locus.lineStart}` : "";
17007
+ const symbol = locus.symbol ?? (grainOf2(graph, ev.affectedNode) === "symbol" ? shortLabel(graph, ev.affectedNode) : void 0);
17008
+ const subject = symbol ? `SYMBOL ${symbol}` : `FILE ${base}`;
17009
+ const at = symbol ? `${lines ? `${lines} in ` : ""}${base}` : lines;
17010
+ const where = at ? ` at ${at}` : "";
17011
+ return `${subject}${where} (SERVICE ${ev.service}) ${what} at ${ev.timestamp}${cause}`;
17012
+ }
17013
+ return `SERVICE ${ev.service} ${what} at ${ev.timestamp}${cause}`;
17014
+ }
17015
+ function buildIncidentCard(graph, errorEvent, incidents, policies) {
17016
+ const affected = errorEvent.affectedNode;
17017
+ const locus = locusOf(graph, errorEvent);
17018
+ const inGraph = graph.hasNode(affected);
17019
+ const rc = inGraph ? getRootCause(graph, affected, errorEvent, incidents) : null;
17020
+ let rootCause = null;
17021
+ if (rc) {
17022
+ const provs = rc.edgeProvenances ?? [];
17023
+ const chain = (rc.traversalPath ?? []).map((node, i) => ({
17024
+ node,
17025
+ grain: grainOf2(graph, node),
17026
+ provenance: provs[i] ?? provs[provs.length - 1] ?? Provenance26.INFERRED
17027
+ }));
17028
+ rootCause = {
17029
+ node: rc.rootCauseNode,
17030
+ ...rc.candidates?.[0]?.classification ? { classification: rc.candidates[0].classification } : {},
17031
+ reason: rc.rootCauseReason,
17032
+ confidence: rc.confidence,
17033
+ fix: rc.fixRecommendation ?? null,
17034
+ chain
17035
+ };
17036
+ }
17037
+ const blast = inGraph ? getBlastRadius(graph, affected) : { origin: affected, affectedNodes: [], totalAffected: 0 };
17038
+ 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 }));
17039
+ const applicable = inGraph ? selectApplicablePolicies(graph, policies, affected) : [];
17040
+ const policyCards = applicable.map((p) => ({
17041
+ policyName: p.policyName,
17042
+ severity: p.severity,
17043
+ message: p.reason
17044
+ }));
17045
+ const divergences = inGraph ? computeDivergences(graph, { node: affected, incidents }).divergences.map((d) => ({
17046
+ type: d.type,
17047
+ summary: divergenceSummary(d)
17048
+ })) : [];
17049
+ const card = {
17050
+ kind: "incident",
17051
+ id: errorEvent.id,
17052
+ at: errorEvent.timestamp,
17053
+ incidentKind: incidentKindOf2(errorEvent),
17054
+ service: errorEvent.service,
17055
+ affectedNode: affected,
17056
+ message: errorEvent.errorMessage,
17057
+ ...errorEvent.exceptionType ? { exceptionType: errorEvent.exceptionType } : {},
17058
+ ...errorEvent.httpStatusCode !== void 0 ? { httpStatusCode: errorEvent.httpStatusCode } : {},
17059
+ ...errorEvent.incidentCount !== void 0 ? { count: errorEvent.incidentCount } : {},
17060
+ ...errorEvent.firstTimestamp && errorEvent.lastTimestamp ? { window: { first: errorEvent.firstTimestamp, last: errorEvent.lastTimestamp } } : {},
17061
+ ...errorEvent.traceId ? { traceId: errorEvent.traceId } : {},
17062
+ ...errorEvent.spanId ? { spanId: errorEvent.spanId } : {},
17063
+ locus,
17064
+ rootCause,
17065
+ ...nearest.length > 0 ? { blastRadius: { totalAffected: blast.totalAffected, nearest } } : {},
17066
+ ...policyCards.length > 0 ? { policies: policyCards } : {},
17067
+ ...divergences.length > 0 ? { divergence: divergences } : {},
17068
+ headline: renderHeadline(graph, errorEvent, locus, rootCause?.node ?? null)
17069
+ };
17070
+ return IncidentCardSchema.parse(card);
17071
+ }
17072
+
16934
17073
  // src/ask.ts
16935
- import { AskResultSchema, NodeType as NodeType32, Provenance as Provenance26, serviceId as serviceId14 } from "@neat.is/types";
17074
+ import { AskResultSchema, NodeType as NodeType32, Provenance as Provenance27, serviceId as serviceId14 } from "@neat.is/types";
16936
17075
  var DEFAULT_MAX_NODES = 3;
16937
17076
  var MAX_FACTS_PER_SECTION = 6;
16938
17077
  var INTENT_RULES = [
@@ -17217,7 +17356,7 @@ function edgeSignalNote(e) {
17217
17356
  function buildRootCauseSection(graph, node, incidents, now) {
17218
17357
  const result = getRootCause(graph, node, void 0, incidents, { now });
17219
17358
  if (!result) return null;
17220
- const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? Provenance26.OBSERVED;
17359
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? Provenance27.OBSERVED;
17221
17360
  const facts = [
17222
17361
  {
17223
17362
  text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
@@ -17267,7 +17406,7 @@ function buildObservedSection(graph, node) {
17267
17406
  if (result.observed && result.inboundObservedCount > 0) {
17268
17407
  facts.push({
17269
17408
  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
17409
+ provenance: Provenance27.OBSERVED
17271
17410
  });
17272
17411
  } else {
17273
17412
  return null;
@@ -17295,7 +17434,7 @@ function buildIncidentsSection(node, incidents) {
17295
17434
  const facts = ordered.map((ev) => ({
17296
17435
  text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
17297
17436
  // ErrorEvents are observation records — OBSERVED by definition.
17298
- provenance: Provenance26.OBSERVED
17437
+ provenance: Provenance27.OBSERVED
17299
17438
  }));
17300
17439
  return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
17301
17440
  }
@@ -17370,7 +17509,7 @@ function buildGlobalIncidentsSection(incidents) {
17370
17509
  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
17510
  const facts = rows.map((r) => ({
17372
17511
  text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
17373
- provenance: Provenance26.OBSERVED
17512
+ provenance: Provenance27.OBSERVED
17374
17513
  }));
17375
17514
  return {
17376
17515
  heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
@@ -17398,7 +17537,7 @@ function buildOverviewSections(graph, incidents) {
17398
17537
  text: `${count(NodeType32.ServiceNode)} services, ${count(NodeType32.FileNode)} files, ${count(NodeType32.SymbolNode)} symbols, ${count(NodeType32.DatabaseNode)} databases`
17399
17538
  }
17400
17539
  ];
17401
- for (const p of [Provenance26.EXTRACTED, Provenance26.OBSERVED, Provenance26.INFERRED, Provenance26.STALE]) {
17540
+ for (const p of [Provenance27.EXTRACTED, Provenance27.OBSERVED, Provenance27.INFERRED, Provenance27.STALE]) {
17402
17541
  const n = edgeByProv.get(p) ?? 0;
17403
17542
  if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
17404
17543
  }
@@ -17423,7 +17562,7 @@ function buildOverviewSections(graph, incidents) {
17423
17562
  heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
17424
17563
  facts: top.map(([k, n]) => ({
17425
17564
  text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
17426
- provenance: Provenance26.OBSERVED
17565
+ provenance: Provenance27.OBSERVED
17427
17566
  }))
17428
17567
  });
17429
17568
  }
@@ -17954,14 +18093,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
17954
18093
  }
17955
18094
 
17956
18095
  // src/connectors/index.ts
17957
- import { NodeType as NodeType33, parseFileId as parseFileId2, Provenance as Provenance27 } from "@neat.is/types";
18096
+ import { NodeType as NodeType33, parseFileId as parseFileId2, Provenance as Provenance28 } from "@neat.is/types";
17958
18097
  var NO_ENV = "unknown";
17959
18098
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
17960
18099
  if (!graph.hasNode(targetNodeId)) return void 0;
17961
18100
  const sites = [];
17962
18101
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
17963
18102
  const edge = graph.getEdgeAttributes(edgeId);
17964
- if (edge.provenance !== Provenance27.EXTRACTED) continue;
18103
+ if (edge.provenance !== Provenance28.EXTRACTED) continue;
17965
18104
  const parsed = parseFileId2(edge.source);
17966
18105
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
17967
18106
  const site = { relPath: edge.evidence.file };
@@ -18003,7 +18142,7 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
18003
18142
  errorMessage: signal.incident.errorMessage,
18004
18143
  ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
18005
18144
  affectedNode: resolved.targetNodeId
18006
- });
18145
+ }, ctx.project);
18007
18146
  continue;
18008
18147
  }
18009
18148
  if (resolved.ensureInfraNode) {
@@ -21277,6 +21416,7 @@ async function startConnectorPolling(input) {
21277
21416
  registration.connector,
21278
21417
  {
21279
21418
  projectDir: input.projectDir,
21419
+ project: input.project,
21280
21420
  credentials: registration.credentials,
21281
21421
  ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
21282
21422
  },
@@ -21620,6 +21760,7 @@ function registerRoutes(scope, ctx) {
21620
21760
  reg.connector,
21621
21761
  {
21622
21762
  projectDir: proj.scanPath ?? "",
21763
+ project: proj.name,
21623
21764
  credentials: reg.credentials,
21624
21765
  ...incidentsPath ? { errorsPath: incidentsPath } : {}
21625
21766
  },
@@ -21683,6 +21824,39 @@ function registerRoutes(scope, ctx) {
21683
21824
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
21684
21825
  return result;
21685
21826
  });
21827
+ scope.get("/graph/incident-card/:nodeId", async (req, reply) => {
21828
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21829
+ if (!proj) return;
21830
+ const { nodeId } = req.params;
21831
+ if (!proj.graph.hasNode(nodeId)) {
21832
+ return reply.code(404).send({ error: "node not found", id: nodeId });
21833
+ }
21834
+ const epath = errorsPathFor(proj);
21835
+ const incidents = epath ? await readErrorEvents(epath) : [];
21836
+ let errorEvent;
21837
+ if (req.query.errorId) {
21838
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
21839
+ if (!errorEvent) {
21840
+ return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
21841
+ }
21842
+ } else {
21843
+ const svc = nodeId.replace(/^service:/, "");
21844
+ errorEvent = [...incidents].filter((e) => e.affectedNode === nodeId || e.service === svc).sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))[0];
21845
+ if (!errorEvent) {
21846
+ return reply.code(404).send({ error: "no incident found for node", id: nodeId });
21847
+ }
21848
+ }
21849
+ const policyPath = ctx.policyFilePathFor(proj);
21850
+ let policies = [];
21851
+ if (policyPath) {
21852
+ try {
21853
+ policies = await loadPolicyFile(policyPath);
21854
+ } catch {
21855
+ policies = [];
21856
+ }
21857
+ }
21858
+ return buildIncidentCard(proj.graph, errorEvent, incidents, policies);
21859
+ });
21686
21860
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
21687
21861
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21688
21862
  if (!proj) return;
@@ -22304,4 +22478,4 @@ export {
22304
22478
  deprovisionConnector,
22305
22479
  buildApi
22306
22480
  };
22307
- //# sourceMappingURL=chunk-KZBFP7L6.js.map
22481
+ //# sourceMappingURL=chunk-G4A6FCKG.js.map