@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.
package/dist/server.cjs CHANGED
@@ -787,7 +787,7 @@ function getGraph(project = DEFAULT_PROJECT) {
787
787
  init_cjs_shims();
788
788
  var import_fastify2 = __toESM(require("fastify"), 1);
789
789
  var import_cors = __toESM(require("@fastify/cors"), 1);
790
- var import_types97 = require("@neat.is/types");
790
+ var import_types98 = require("@neat.is/types");
791
791
 
792
792
  // src/extend/index.ts
793
793
  init_cjs_shims();
@@ -5596,11 +5596,25 @@ function upsertInferredEdge(graph, type, source, target, ts) {
5596
5596
  };
5597
5597
  graph.addEdgeWithKey(id, source, target, edge);
5598
5598
  }
5599
+ function emitIncidentEvent(project, ev) {
5600
+ emitNeatEvent({
5601
+ type: "incident",
5602
+ project,
5603
+ payload: {
5604
+ incidentId: ev.id,
5605
+ affectedNode: ev.affectedNode,
5606
+ service: ev.service,
5607
+ incidentKind: (0, import_types7.incidentKindOf)(ev),
5608
+ at: ev.timestamp
5609
+ }
5610
+ });
5611
+ }
5599
5612
  async function appendErrorEvent(ctx, ev) {
5600
5613
  await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(ctx.errorsPath), { recursive: true });
5601
5614
  await import_node_fs9.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5615
+ emitIncidentEvent(ctx.project ?? DEFAULT_PROJECT, ev);
5602
5616
  }
5603
- async function appendConnectorIncident(errorsPath, input) {
5617
+ async function appendConnectorIncident(errorsPath, input, project) {
5604
5618
  const ev = {
5605
5619
  id: input.id,
5606
5620
  timestamp: input.timestamp,
@@ -5614,6 +5628,7 @@ async function appendConnectorIncident(errorsPath, input) {
5614
5628
  };
5615
5629
  await import_node_fs9.promises.mkdir(import_node_path10.default.dirname(errorsPath), { recursive: true });
5616
5630
  await import_node_fs9.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5631
+ if (project) emitIncidentEvent(project, ev);
5617
5632
  }
5618
5633
  function landIncidentCallSite(span, callSite, trusted, graph) {
5619
5634
  const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
@@ -16867,9 +16882,160 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16867
16882
  return result;
16868
16883
  }
16869
16884
 
16870
- // src/ask.ts
16885
+ // src/goodybag.ts
16871
16886
  init_cjs_shims();
16872
16887
  var import_types58 = require("@neat.is/types");
16888
+ var CODE_FILEPATH_ATTR2 = "code.filepath";
16889
+ var CODE_LINENO_ATTR2 = "code.lineno";
16890
+ var BLAST_NEAREST_LIMIT = 5;
16891
+ function grainOf2(graph, nodeId) {
16892
+ if (graph.hasNode(nodeId)) {
16893
+ const t = graph.getNodeAttributes(nodeId).type;
16894
+ if (typeof t === "string" && t.length > 0) {
16895
+ return (t.endsWith("Node") ? t.slice(0, -4) : t).toLowerCase();
16896
+ }
16897
+ }
16898
+ const colon = nodeId.indexOf(":");
16899
+ return colon > 0 ? nodeId.slice(0, colon) : "unknown";
16900
+ }
16901
+ function locusOf(graph, ev) {
16902
+ const file = ev.attributes?.[CODE_FILEPATH_ATTR2];
16903
+ if (typeof file !== "string" || file.length === 0) return null;
16904
+ const rawLine = ev.attributes?.[CODE_LINENO_ATTR2];
16905
+ const line = typeof rawLine === "number" ? rawLine : Number(rawLine);
16906
+ const node = graph.hasNode(ev.affectedNode) ? graph.getNodeAttributes(ev.affectedNode) : void 0;
16907
+ return {
16908
+ file,
16909
+ ...Number.isFinite(line) ? { lineStart: line, lineEnd: line } : {},
16910
+ ...node?.name ? { symbol: node.name } : {},
16911
+ service: node?.service ?? ev.service,
16912
+ provenance: import_types58.Provenance.OBSERVED
16913
+ };
16914
+ }
16915
+ function locusFromNode(graph, nodeId) {
16916
+ if (!graph.hasNode(nodeId)) return null;
16917
+ const n = graph.getNodeAttributes(nodeId);
16918
+ const file = n.relPath ?? n.path;
16919
+ if (typeof file !== "string" || file.length === 0) return null;
16920
+ const start = n.span?.startLine;
16921
+ const end = n.span?.endLine;
16922
+ return {
16923
+ file,
16924
+ ...typeof start === "number" ? { lineStart: start } : {},
16925
+ ...typeof end === "number" ? { lineEnd: end } : {},
16926
+ ...n.qualname ? { symbol: shortLabel(graph, nodeId) } : {},
16927
+ ...n.service ? { service: n.service } : {},
16928
+ provenance: import_types58.Provenance.INFERRED
16929
+ };
16930
+ }
16931
+ function promoteCauseLocus(graph, causeNode, incidents) {
16932
+ const native = incidents.find(
16933
+ (e) => e.affectedNode === causeNode && typeof e.attributes?.[CODE_FILEPATH_ATTR2] === "string"
16934
+ );
16935
+ if (native) {
16936
+ const l = locusOf(graph, native);
16937
+ if (l) return { ...l, symbol: shortLabel(graph, causeNode), provenance: import_types58.Provenance.INFERRED };
16938
+ }
16939
+ return locusFromNode(graph, causeNode);
16940
+ }
16941
+ function divergenceSummary(d) {
16942
+ const column = "column" in d && d.column ? `.${d.column}` : "";
16943
+ const label = "table" in d && d.table ? `${d.table}${column}` : `${d.source} \u2192 ${d.target}`;
16944
+ return label;
16945
+ }
16946
+ function baseName(p) {
16947
+ const parts = p.split(/[\\/]/);
16948
+ return parts[parts.length - 1] || p;
16949
+ }
16950
+ function shortLabel(graph, nodeId) {
16951
+ if (graph.hasNode(nodeId)) {
16952
+ const name = graph.getNodeAttributes(nodeId).name;
16953
+ if (typeof name === "string" && name.length > 0) return name;
16954
+ }
16955
+ const afterHash = nodeId.includes("#") ? nodeId.slice(nodeId.lastIndexOf("#") + 1) : nodeId;
16956
+ return afterHash.includes(":") ? afterHash.slice(afterHash.lastIndexOf(":") + 1) : afterHash;
16957
+ }
16958
+ function renderHeadline(graph, ev, locus, causeNode) {
16959
+ const what = ev.exceptionType ? `raised ${ev.exceptionType}` : ev.errorMessage || "failed";
16960
+ const causeLabel = causeNode && causeNode !== ev.affectedNode ? shortLabel(graph, causeNode) : "";
16961
+ if (locus) {
16962
+ const base = baseName(locus.file);
16963
+ const lines = locus.lineStart != null ? locus.lineEnd && locus.lineEnd !== locus.lineStart ? `LINES ${locus.lineStart}-${locus.lineEnd}` : `LINE ${locus.lineStart}` : "";
16964
+ const symbol = locus.symbol ?? (grainOf2(graph, ev.affectedNode) === "symbol" ? shortLabel(graph, ev.affectedNode) : void 0);
16965
+ const subject = symbol ? `SYMBOL ${symbol}` : `FILE ${base}`;
16966
+ const at = symbol ? `${lines ? `${lines} in ` : ""}${base}` : lines;
16967
+ const where = at ? ` at ${at}` : "";
16968
+ const svc = locus.service ?? ev.service;
16969
+ const cause2 = causeLabel && causeLabel !== symbol ? ` \u2192 root cause ${causeLabel}` : "";
16970
+ return `${subject}${where} (SERVICE ${svc}) ${what} at ${ev.timestamp}${cause2}`;
16971
+ }
16972
+ const cause = causeLabel ? ` \u2192 root cause ${causeLabel}` : "";
16973
+ return `SERVICE ${ev.service} ${what} at ${ev.timestamp}${cause}`;
16974
+ }
16975
+ function buildIncidentCard(graph, errorEvent, incidents, policies) {
16976
+ const affected = errorEvent.affectedNode;
16977
+ let locus = locusOf(graph, errorEvent);
16978
+ const inGraph = graph.hasNode(affected);
16979
+ const rc = inGraph ? getRootCause(graph, affected, errorEvent, incidents) : null;
16980
+ let rootCause = null;
16981
+ if (rc) {
16982
+ const provs = rc.edgeProvenances ?? [];
16983
+ const chain = (rc.traversalPath ?? []).map((node, i) => ({
16984
+ node,
16985
+ grain: grainOf2(graph, node),
16986
+ provenance: provs[i] ?? provs[provs.length - 1] ?? import_types58.Provenance.INFERRED
16987
+ }));
16988
+ rootCause = {
16989
+ node: rc.rootCauseNode,
16990
+ ...rc.candidates?.[0]?.classification ? { classification: rc.candidates[0].classification } : {},
16991
+ reason: rc.rootCauseReason,
16992
+ confidence: rc.confidence,
16993
+ fix: rc.fixRecommendation ?? null,
16994
+ chain
16995
+ };
16996
+ }
16997
+ if (locus === null && rootCause) {
16998
+ locus = promoteCauseLocus(graph, rootCause.node, incidents);
16999
+ }
17000
+ const blast = inGraph ? getBlastRadius(graph, affected) : { origin: affected, affectedNodes: [], totalAffected: 0 };
17001
+ 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 }));
17002
+ const applicable = inGraph ? selectApplicablePolicies(graph, policies, affected) : [];
17003
+ const policyCards = applicable.map((p) => ({
17004
+ policyName: p.policyName,
17005
+ severity: p.severity,
17006
+ message: p.reason
17007
+ }));
17008
+ const divergences = inGraph ? computeDivergences(graph, { node: affected, incidents }).divergences.map((d) => ({
17009
+ type: d.type,
17010
+ summary: divergenceSummary(d)
17011
+ })) : [];
17012
+ const card = {
17013
+ kind: "incident",
17014
+ id: errorEvent.id,
17015
+ at: errorEvent.timestamp,
17016
+ incidentKind: (0, import_types58.incidentKindOf)(errorEvent),
17017
+ service: errorEvent.service,
17018
+ affectedNode: affected,
17019
+ message: errorEvent.errorMessage,
17020
+ ...errorEvent.exceptionType ? { exceptionType: errorEvent.exceptionType } : {},
17021
+ ...errorEvent.httpStatusCode !== void 0 ? { httpStatusCode: errorEvent.httpStatusCode } : {},
17022
+ ...errorEvent.incidentCount !== void 0 ? { count: errorEvent.incidentCount } : {},
17023
+ ...errorEvent.firstTimestamp && errorEvent.lastTimestamp ? { window: { first: errorEvent.firstTimestamp, last: errorEvent.lastTimestamp } } : {},
17024
+ ...errorEvent.traceId ? { traceId: errorEvent.traceId } : {},
17025
+ ...errorEvent.spanId ? { spanId: errorEvent.spanId } : {},
17026
+ locus,
17027
+ rootCause,
17028
+ ...nearest.length > 0 ? { blastRadius: { totalAffected: blast.totalAffected, nearest } } : {},
17029
+ ...policyCards.length > 0 ? { policies: policyCards } : {},
17030
+ ...divergences.length > 0 ? { divergence: divergences } : {},
17031
+ headline: renderHeadline(graph, errorEvent, locus, rootCause?.node ?? null)
17032
+ };
17033
+ return import_types58.IncidentCardSchema.parse(card);
17034
+ }
17035
+
17036
+ // src/ask.ts
17037
+ init_cjs_shims();
17038
+ var import_types59 = require("@neat.is/types");
16873
17039
  var DEFAULT_MAX_NODES = 3;
16874
17040
  var MAX_FACTS_PER_SECTION = 6;
16875
17041
  var INTENT_RULES = [
@@ -17097,7 +17263,7 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17097
17263
  };
17098
17264
  graph.forEachNode((id, attrs) => {
17099
17265
  const node = attrs;
17100
- if (node.type === import_types58.NodeType.FrontierNode) return;
17266
+ if (node.type === import_types59.NodeType.FrontierNode) return;
17101
17267
  const name = nodeName(node);
17102
17268
  const body = idBody(id);
17103
17269
  const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
@@ -17116,7 +17282,7 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17116
17282
  const res = await searchIndex.search(question, 10);
17117
17283
  if (res.provider !== "substring") {
17118
17284
  for (const m of res.matches) {
17119
- if (m.node.type === import_types58.NodeType.FrontierNode) continue;
17285
+ if (m.node.type === import_types59.NodeType.FrontierNode) continue;
17120
17286
  const already = best.get(m.node.id);
17121
17287
  if (!already && m.score < EMBED_MIN_SCORE) continue;
17122
17288
  consider({
@@ -17154,7 +17320,7 @@ function edgeSignalNote(e) {
17154
17320
  function buildRootCauseSection(graph, node, incidents, now) {
17155
17321
  const result = getRootCause(graph, node, void 0, incidents, { now });
17156
17322
  if (!result) return null;
17157
- const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? import_types58.Provenance.OBSERVED;
17323
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? import_types59.Provenance.OBSERVED;
17158
17324
  const facts = [
17159
17325
  {
17160
17326
  text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
@@ -17204,7 +17370,7 @@ function buildObservedSection(graph, node) {
17204
17370
  if (result.observed && result.inboundObservedCount > 0) {
17205
17371
  facts.push({
17206
17372
  text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
17207
- provenance: import_types58.Provenance.OBSERVED
17373
+ provenance: import_types59.Provenance.OBSERVED
17208
17374
  });
17209
17375
  } else {
17210
17376
  return null;
@@ -17232,7 +17398,7 @@ function buildIncidentsSection(node, incidents) {
17232
17398
  const facts = ordered.map((ev) => ({
17233
17399
  text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
17234
17400
  // ErrorEvents are observation records — OBSERVED by definition.
17235
- provenance: import_types58.Provenance.OBSERVED
17401
+ provenance: import_types59.Provenance.OBSERVED
17236
17402
  }));
17237
17403
  return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
17238
17404
  }
@@ -17292,7 +17458,7 @@ function buildGlobalIncidentsSection(incidents) {
17292
17458
  }
17293
17459
  const byKey = /* @__PURE__ */ new Map();
17294
17460
  for (const ev of incidents) {
17295
- const key = ev.affectedNode || (0, import_types58.serviceId)(ev.service);
17461
+ const key = ev.affectedNode || (0, import_types59.serviceId)(ev.service);
17296
17462
  const cur = byKey.get(key);
17297
17463
  if (!cur) {
17298
17464
  byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
@@ -17307,7 +17473,7 @@ function buildGlobalIncidentsSection(incidents) {
17307
17473
  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);
17308
17474
  const facts = rows.map((r) => ({
17309
17475
  text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
17310
- provenance: import_types58.Provenance.OBSERVED
17476
+ provenance: import_types59.Provenance.OBSERVED
17311
17477
  }));
17312
17478
  return {
17313
17479
  heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
@@ -17320,7 +17486,7 @@ function buildOverviewSections(graph, incidents) {
17320
17486
  graph.forEachNode((_id, attrs) => {
17321
17487
  const node = attrs;
17322
17488
  nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
17323
- if (node.type === import_types58.NodeType.ServiceNode) services.push(node.id);
17489
+ if (node.type === import_types59.NodeType.ServiceNode) services.push(node.id);
17324
17490
  });
17325
17491
  const edgeByProv = /* @__PURE__ */ new Map();
17326
17492
  graph.forEachEdge((_id, attrs) => {
@@ -17332,10 +17498,10 @@ function buildOverviewSections(graph, incidents) {
17332
17498
  const shapeFacts = [
17333
17499
  { text: `${graph.order} nodes, ${graph.size} edges` },
17334
17500
  {
17335
- text: `${count(import_types58.NodeType.ServiceNode)} services, ${count(import_types58.NodeType.FileNode)} files, ${count(import_types58.NodeType.SymbolNode)} symbols, ${count(import_types58.NodeType.DatabaseNode)} databases`
17501
+ text: `${count(import_types59.NodeType.ServiceNode)} services, ${count(import_types59.NodeType.FileNode)} files, ${count(import_types59.NodeType.SymbolNode)} symbols, ${count(import_types59.NodeType.DatabaseNode)} databases`
17336
17502
  }
17337
17503
  ];
17338
- for (const p of [import_types58.Provenance.EXTRACTED, import_types58.Provenance.OBSERVED, import_types58.Provenance.INFERRED, import_types58.Provenance.STALE]) {
17504
+ for (const p of [import_types59.Provenance.EXTRACTED, import_types59.Provenance.OBSERVED, import_types59.Provenance.INFERRED, import_types59.Provenance.STALE]) {
17339
17505
  const n = edgeByProv.get(p) ?? 0;
17340
17506
  if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
17341
17507
  }
@@ -17352,7 +17518,7 @@ function buildOverviewSections(graph, incidents) {
17352
17518
  if (incidents && incidents.length > 0) {
17353
17519
  const incCount = /* @__PURE__ */ new Map();
17354
17520
  for (const ev of incidents) {
17355
- const key = ev.affectedNode || (0, import_types58.serviceId)(ev.service);
17521
+ const key = ev.affectedNode || (0, import_types59.serviceId)(ev.service);
17356
17522
  incCount.set(key, (incCount.get(key) ?? 0) + 1);
17357
17523
  }
17358
17524
  const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
@@ -17360,7 +17526,7 @@ function buildOverviewSections(graph, incidents) {
17360
17526
  heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
17361
17527
  facts: top.map(([k, n]) => ({
17362
17528
  text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
17363
- provenance: import_types58.Provenance.OBSERVED
17529
+ provenance: import_types59.Provenance.OBSERVED
17364
17530
  }))
17365
17531
  });
17366
17532
  }
@@ -17496,7 +17662,7 @@ async function askGraph(graph, question, opts = {}) {
17496
17662
  const provSet = /* @__PURE__ */ new Set();
17497
17663
  for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
17498
17664
  const confidence = sections[0]?.facts[0]?.confidence;
17499
- return import_types58.AskResultSchema.parse({
17665
+ return import_types59.AskResultSchema.parse({
17500
17666
  question,
17501
17667
  intent,
17502
17668
  matched,
@@ -17590,7 +17756,7 @@ function canonicalJson(value) {
17590
17756
  init_cjs_shims();
17591
17757
  var import_node_fs36 = require("fs");
17592
17758
  var import_node_path70 = __toESM(require("path"), 1);
17593
- var import_types59 = require("@neat.is/types");
17759
+ var import_types60 = require("@neat.is/types");
17594
17760
  var SCHEMA_VERSION = 7;
17595
17761
  function migrateV1ToV2(payload) {
17596
17762
  const nodes = payload.graph.nodes;
@@ -17614,7 +17780,7 @@ function migrateV5ToV6(payload) {
17614
17780
  if (Array.isArray(nodes)) {
17615
17781
  for (const node of nodes) {
17616
17782
  const attrs = node.attributes;
17617
- if (!attrs || attrs.type !== import_types59.NodeType.InfraNode) continue;
17783
+ if (!attrs || attrs.type !== import_types60.NodeType.InfraNode) continue;
17618
17784
  if (attrs.kind !== "sql-table" && attrs.kind !== "supabase-table") continue;
17619
17785
  if (!Array.isArray(attrs.columns)) attrs.columns = [];
17620
17786
  }
@@ -17630,12 +17796,12 @@ function migrateV2ToV3(payload) {
17630
17796
  for (const edge of edges) {
17631
17797
  const attrs = edge.attributes;
17632
17798
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
17633
- attrs.provenance = import_types59.Provenance.OBSERVED;
17799
+ attrs.provenance = import_types60.Provenance.OBSERVED;
17634
17800
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
17635
17801
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
17636
17802
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
17637
17803
  if (type && source && target) {
17638
- const newId = (0, import_types59.observedEdgeId)(source, target, type);
17804
+ const newId = (0, import_types60.observedEdgeId)(source, target, type);
17639
17805
  attrs.id = newId;
17640
17806
  if (edge.key) edge.key = newId;
17641
17807
  }
@@ -17793,7 +17959,7 @@ init_cjs_shims();
17793
17959
  var import_node_fs37 = require("fs");
17794
17960
  var import_node_os3 = __toESM(require("os"), 1);
17795
17961
  var import_node_path72 = __toESM(require("path"), 1);
17796
- var import_types60 = require("@neat.is/types");
17962
+ var import_types61 = require("@neat.is/types");
17797
17963
  function neatHome() {
17798
17964
  const override = process.env.NEAT_HOME;
17799
17965
  if (override && override.length > 0) return import_node_path72.default.resolve(override);
@@ -17883,7 +18049,7 @@ async function readRegistry() {
17883
18049
  throw err;
17884
18050
  }
17885
18051
  const parsed = JSON.parse(raw);
17886
- return import_types60.RegistryFileSchema.parse(parsed);
18052
+ return import_types61.RegistryFileSchema.parse(parsed);
17887
18053
  }
17888
18054
  async function getProject(name) {
17889
18055
  const reg = await readRegistry();
@@ -18164,15 +18330,15 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
18164
18330
 
18165
18331
  // src/connectors/index.ts
18166
18332
  init_cjs_shims();
18167
- var import_types61 = require("@neat.is/types");
18333
+ var import_types62 = require("@neat.is/types");
18168
18334
  var NO_ENV = "unknown";
18169
18335
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
18170
18336
  if (!graph.hasNode(targetNodeId)) return void 0;
18171
18337
  const sites = [];
18172
18338
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
18173
18339
  const edge = graph.getEdgeAttributes(edgeId);
18174
- if (edge.provenance !== import_types61.Provenance.EXTRACTED) continue;
18175
- const parsed = (0, import_types61.parseFileId)(edge.source);
18340
+ if (edge.provenance !== import_types62.Provenance.EXTRACTED) continue;
18341
+ const parsed = (0, import_types62.parseFileId)(edge.source);
18176
18342
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
18177
18343
  const site = { relPath: edge.evidence.file };
18178
18344
  if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
@@ -18183,7 +18349,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
18183
18349
  function routeCallSiteFor(graph, targetNodeId) {
18184
18350
  if (!graph.hasNode(targetNodeId)) return void 0;
18185
18351
  const attrs = graph.getNodeAttributes(targetNodeId);
18186
- if (attrs.type !== import_types61.NodeType.RouteNode || !attrs.path) return void 0;
18352
+ if (attrs.type !== import_types62.NodeType.RouteNode || !attrs.path) return void 0;
18187
18353
  const site = { relPath: attrs.path };
18188
18354
  if (attrs.line !== void 0) site.line = attrs.line;
18189
18355
  return site;
@@ -18213,7 +18379,7 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
18213
18379
  errorMessage: signal.incident.errorMessage,
18214
18380
  ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
18215
18381
  affectedNode: resolved.targetNodeId
18216
- });
18382
+ }, ctx.project);
18217
18383
  continue;
18218
18384
  }
18219
18385
  if (resolved.ensureInfraNode) {
@@ -18754,23 +18920,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
18754
18920
 
18755
18921
  // src/connectors/supabase/resolve.ts
18756
18922
  init_cjs_shims();
18757
- var import_types63 = require("@neat.is/types");
18923
+ var import_types64 = require("@neat.is/types");
18758
18924
  function createSupabaseResolveTarget(graph, config) {
18759
18925
  return (signal, _ctx) => {
18760
18926
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
18761
18927
  return null;
18762
18928
  }
18763
- const subResourceId = (0, import_types63.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
18929
+ const subResourceId = (0, import_types64.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
18764
18930
  if (graph.hasNode(subResourceId)) {
18765
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
18931
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
18766
18932
  }
18767
- const bareResourceId = (0, import_types63.infraId)(signal.targetKind, signal.targetName);
18933
+ const bareResourceId = (0, import_types64.infraId)(signal.targetKind, signal.targetName);
18768
18934
  if (graph.hasNode(bareResourceId)) {
18769
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
18935
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
18770
18936
  }
18771
- const projectLevelId = (0, import_types63.infraId)("supabase", config.nodeRef);
18937
+ const projectLevelId = (0, import_types64.infraId)("supabase", config.nodeRef);
18772
18938
  if (graph.hasNode(projectLevelId)) {
18773
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
18939
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
18774
18940
  }
18775
18941
  return null;
18776
18942
  };
@@ -18863,7 +19029,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
18863
19029
 
18864
19030
  // src/connectors/railway/index.ts
18865
19031
  init_cjs_shims();
18866
- var import_types67 = require("@neat.is/types");
19032
+ var import_types68 = require("@neat.is/types");
18867
19033
 
18868
19034
  // src/connectors/railway/client.ts
18869
19035
  init_cjs_shims();
@@ -19014,7 +19180,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
19014
19180
  const out = [];
19015
19181
  graph.forEachNode((_id, attrs) => {
19016
19182
  const node = attrs;
19017
- if (node.type !== import_types67.NodeType.RouteNode) return;
19183
+ if (node.type !== import_types68.NodeType.RouteNode) return;
19018
19184
  const route = attrs;
19019
19185
  if (route.service !== serviceName) return;
19020
19186
  out.push({
@@ -19118,12 +19284,12 @@ function createRailwayResolveTarget(config) {
19118
19284
  const serviceName = config.serviceNameById[config.serviceId];
19119
19285
  if (!serviceName) return null;
19120
19286
  if (signal.targetKind === ROUTE_TARGET_KIND) {
19121
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types67.EdgeType.CALLS };
19287
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types68.EdgeType.CALLS };
19122
19288
  }
19123
19289
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
19124
19290
  const peerName = config.serviceNameById[signal.targetName];
19125
19291
  if (!peerName) return null;
19126
- return { targetNodeId: (0, import_types67.serviceId)(peerName), serviceName, edgeType: import_types67.EdgeType.CONNECTS_TO };
19292
+ return { targetNodeId: (0, import_types68.serviceId)(peerName), serviceName, edgeType: import_types68.EdgeType.CONNECTS_TO };
19127
19293
  }
19128
19294
  return null;
19129
19295
  };
@@ -19311,7 +19477,7 @@ function mapLogEntriesToSignals(entries) {
19311
19477
 
19312
19478
  // src/connectors/firebase/resolve.ts
19313
19479
  init_cjs_shims();
19314
- var import_types68 = require("@neat.is/types");
19480
+ var import_types69 = require("@neat.is/types");
19315
19481
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
19316
19482
  switch (resourceType) {
19317
19483
  case "cloud_function":
@@ -19326,7 +19492,7 @@ function routeEntriesFor(graph, serviceName) {
19326
19492
  const entries = [];
19327
19493
  graph.forEachNode((_id, attrs) => {
19328
19494
  const node = attrs;
19329
- if (node.type !== import_types68.NodeType.RouteNode) return;
19495
+ if (node.type !== import_types69.NodeType.RouteNode) return;
19330
19496
  const route = attrs;
19331
19497
  if (route.service !== serviceName) return;
19332
19498
  entries.push({
@@ -19358,7 +19524,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
19358
19524
  return {
19359
19525
  targetNodeId: match.routeNodeId,
19360
19526
  serviceName,
19361
- edgeType: import_types68.EdgeType.CALLS
19527
+ edgeType: import_types69.EdgeType.CALLS
19362
19528
  };
19363
19529
  };
19364
19530
  }
@@ -19385,7 +19551,7 @@ init_cjs_shims();
19385
19551
 
19386
19552
  // src/connectors/cloudflare/connector.ts
19387
19553
  init_cjs_shims();
19388
- var import_types70 = require("@neat.is/types");
19554
+ var import_types71 = require("@neat.is/types");
19389
19555
 
19390
19556
  // src/connectors/cloudflare/client.ts
19391
19557
  init_cjs_shims();
@@ -19549,7 +19715,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
19549
19715
  graph.forEachNode((id, attrs) => {
19550
19716
  if (found) return;
19551
19717
  const a = attrs;
19552
- if (a.type === import_types70.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
19718
+ if (a.type === import_types71.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
19553
19719
  found = id;
19554
19720
  }
19555
19721
  });
@@ -19561,7 +19727,7 @@ function findMatchingRouteNode(graph, serviceName, method, path76) {
19561
19727
  graph.forEachNode((id, attrs) => {
19562
19728
  if (found) return;
19563
19729
  const a = attrs;
19564
- if (a.type !== import_types70.NodeType.RouteNode || a.service !== serviceName) return;
19730
+ if (a.type !== import_types71.NodeType.RouteNode || a.service !== serviceName) return;
19565
19731
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
19566
19732
  const routeMethod = (a.method ?? "").toUpperCase();
19567
19733
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -19580,11 +19746,11 @@ function createCloudflareResolveTarget(config, graph) {
19580
19746
  };
19581
19747
  const mapping = config.workers?.[scriptName];
19582
19748
  if (mapping) {
19583
- const wholeFileId = (0, import_types70.fileId)(mapping.service, mapping.entryFile);
19749
+ const wholeFileId = (0, import_types71.fileId)(mapping.service, mapping.entryFile);
19584
19750
  return {
19585
19751
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
19586
19752
  serviceName: mapping.service,
19587
- edgeType: import_types70.EdgeType.CALLS
19753
+ edgeType: import_types71.EdgeType.CALLS
19588
19754
  };
19589
19755
  }
19590
19756
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -19593,13 +19759,13 @@ function createCloudflareResolveTarget(config, graph) {
19593
19759
  return {
19594
19760
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
19595
19761
  serviceName: fileNode.service,
19596
- edgeType: import_types70.EdgeType.CALLS
19762
+ edgeType: import_types71.EdgeType.CALLS
19597
19763
  };
19598
19764
  }
19599
19765
  return {
19600
- targetNodeId: (0, import_types70.infraId)("cloudflare-worker", scriptName),
19766
+ targetNodeId: (0, import_types71.infraId)("cloudflare-worker", scriptName),
19601
19767
  serviceName: scriptName,
19602
- edgeType: import_types70.EdgeType.CALLS,
19768
+ edgeType: import_types71.EdgeType.CALLS,
19603
19769
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
19604
19770
  };
19605
19771
  };
@@ -19795,14 +19961,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
19795
19961
 
19796
19962
  // src/connectors/neon/resolve.ts
19797
19963
  init_cjs_shims();
19798
- var import_types74 = require("@neat.is/types");
19964
+ var import_types75 = require("@neat.is/types");
19799
19965
  function createNeonResolveTarget(config) {
19800
19966
  return (signal) => {
19801
19967
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
19802
19968
  return {
19803
- targetNodeId: (0, import_types74.infraId)("sql-table", signal.targetName),
19969
+ targetNodeId: (0, import_types75.infraId)("sql-table", signal.targetName),
19804
19970
  serviceName: config.serviceName,
19805
- edgeType: import_types74.EdgeType.CALLS,
19971
+ edgeType: import_types75.EdgeType.CALLS,
19806
19972
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
19807
19973
  };
19808
19974
  };
@@ -19983,14 +20149,14 @@ function mapLogEntriesToSignals2(entries) {
19983
20149
 
19984
20150
  // src/connectors/cloud-run/resolve.ts
19985
20151
  init_cjs_shims();
19986
- var import_types78 = require("@neat.is/types");
20152
+ var import_types79 = require("@neat.is/types");
19987
20153
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
19988
20154
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
19989
20155
  let found = null;
19990
20156
  graph.forEachNode((_id, attrs) => {
19991
20157
  if (found) return;
19992
20158
  const node = attrs;
19993
- if (node.type !== import_types78.NodeType.RouteNode) return;
20159
+ if (node.type !== import_types79.NodeType.RouteNode) return;
19994
20160
  const route = attrs;
19995
20161
  if (route.service !== serviceName || !route.pathTemplate) return;
19996
20162
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -20015,13 +20181,13 @@ function createCloudRunResolveTarget(graph, config) {
20015
20181
  normalizePathTemplate(path76)
20016
20182
  );
20017
20183
  if (routeNodeId) {
20018
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types78.EdgeType.CALLS };
20184
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types79.EdgeType.CALLS };
20019
20185
  }
20020
20186
  }
20021
20187
  return {
20022
- targetNodeId: (0, import_types78.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
20188
+ targetNodeId: (0, import_types79.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
20023
20189
  serviceName: mappedService ?? gcpServiceName,
20024
- edgeType: import_types78.EdgeType.CALLS,
20190
+ edgeType: import_types79.EdgeType.CALLS,
20025
20191
  ensureInfraNode: {
20026
20192
  kind: CLOUD_RUN_SERVICE_INFRA_KIND,
20027
20193
  name: gcpServiceName,
@@ -20203,14 +20369,14 @@ function mapLogEntriesToSignals3(entries) {
20203
20369
 
20204
20370
  // src/connectors/gcp-lb/resolve.ts
20205
20371
  init_cjs_shims();
20206
- var import_types82 = require("@neat.is/types");
20372
+ var import_types83 = require("@neat.is/types");
20207
20373
  var GCP_LB_BACKEND_INFRA_KIND = "gcp-lb-backend";
20208
20374
  function findMatchingRouteNode3(graph, serviceName, method, normalizedPath) {
20209
20375
  let found = null;
20210
20376
  graph.forEachNode((_id, attrs) => {
20211
20377
  if (found) return;
20212
20378
  const node = attrs;
20213
- if (node.type !== import_types82.NodeType.RouteNode) return;
20379
+ if (node.type !== import_types83.NodeType.RouteNode) return;
20214
20380
  const route = attrs;
20215
20381
  if (route.service !== serviceName || !route.pathTemplate) return;
20216
20382
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -20235,13 +20401,13 @@ function createGcpLbResolveTarget(graph, config) {
20235
20401
  normalizePathTemplate(path76)
20236
20402
  );
20237
20403
  if (routeNodeId) {
20238
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types82.EdgeType.CALLS };
20404
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types83.EdgeType.CALLS };
20239
20405
  }
20240
20406
  }
20241
20407
  return {
20242
- targetNodeId: (0, import_types82.infraId)(GCP_LB_BACKEND_INFRA_KIND, backendServiceName),
20408
+ targetNodeId: (0, import_types83.infraId)(GCP_LB_BACKEND_INFRA_KIND, backendServiceName),
20243
20409
  serviceName: mappedService ?? backendServiceName,
20244
- edgeType: import_types82.EdgeType.CALLS,
20410
+ edgeType: import_types83.EdgeType.CALLS,
20245
20411
  ensureInfraNode: {
20246
20412
  kind: GCP_LB_BACKEND_INFRA_KIND,
20247
20413
  name: backendServiceName,
@@ -20282,7 +20448,7 @@ function createGcpLbConnector(graph, config = {}) {
20282
20448
 
20283
20449
  // src/connectors/render/index.ts
20284
20450
  init_cjs_shims();
20285
- var import_types85 = require("@neat.is/types");
20451
+ var import_types86 = require("@neat.is/types");
20286
20452
 
20287
20453
  // src/connectors/render/types.ts
20288
20454
  init_cjs_shims();
@@ -20360,7 +20526,7 @@ function buildRenderRouteIndex(graph, serviceName) {
20360
20526
  const out = [];
20361
20527
  graph.forEachNode((_id, attrs) => {
20362
20528
  const node = attrs;
20363
- if (node.type !== import_types85.NodeType.RouteNode) return;
20529
+ if (node.type !== import_types86.NodeType.RouteNode) return;
20364
20530
  const route = attrs;
20365
20531
  if (route.service !== serviceName) return;
20366
20532
  out.push({
@@ -20445,7 +20611,7 @@ function mapRenderRequestLogsToSignals(entries, routeIndex) {
20445
20611
  function createRenderResolveTarget(config) {
20446
20612
  return (signal) => {
20447
20613
  if (signal.targetKind === ROUTE_TARGET_KIND2) {
20448
- return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types85.EdgeType.CALLS };
20614
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types86.EdgeType.CALLS };
20449
20615
  }
20450
20616
  return null;
20451
20617
  };
@@ -20583,21 +20749,21 @@ function mapInsightsToSignals(rows, observedAtIso) {
20583
20749
 
20584
20750
  // src/connectors/planetscale/resolve.ts
20585
20751
  init_cjs_shims();
20586
- var import_types89 = require("@neat.is/types");
20752
+ var import_types90 = require("@neat.is/types");
20587
20753
  var PLANETSCALE_DATABASE_KIND = "planetscale-database";
20588
20754
  function createPlanetscaleResolveTarget(graph, config) {
20589
20755
  const databaseName = `${config.organization}/${config.database}`;
20590
20756
  return (signal, _ctx) => {
20591
20757
  if (signal.targetKind !== PLANETSCALE_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
20592
- const tableId = (0, import_types89.infraId)("sql-table", signal.targetName);
20758
+ const tableId = (0, import_types90.infraId)("sql-table", signal.targetName);
20593
20759
  if (graph.hasNode(tableId)) {
20594
- return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types89.EdgeType.CALLS };
20760
+ return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types90.EdgeType.CALLS };
20595
20761
  }
20596
- const providerId = (0, import_types89.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
20762
+ const providerId = (0, import_types90.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
20597
20763
  return {
20598
20764
  targetNodeId: providerId,
20599
20765
  serviceName: config.serviceName,
20600
- edgeType: import_types89.EdgeType.CALLS,
20766
+ edgeType: import_types90.EdgeType.CALLS,
20601
20767
  ensureInfraNode: { kind: PLANETSCALE_DATABASE_KIND, name: databaseName, provider: "planetscale" }
20602
20768
  };
20603
20769
  };
@@ -20862,7 +21028,7 @@ function mapBuildsToSignals(builds, serviceName) {
20862
21028
 
20863
21029
  // src/connectors/eas/resolve.ts
20864
21030
  init_cjs_shims();
20865
- var import_types94 = require("@neat.is/types");
21031
+ var import_types95 = require("@neat.is/types");
20866
21032
  var NO_ENV2 = "unknown";
20867
21033
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
20868
21034
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -20878,8 +21044,8 @@ function configBasenamesForPhase(phase) {
20878
21044
  function configNodeService(graph, configNodeId) {
20879
21045
  for (const edgeId of graph.inboundEdges(configNodeId)) {
20880
21046
  const edge = graph.getEdgeAttributes(edgeId);
20881
- if (edge.type !== import_types94.EdgeType.CONFIGURED_BY) continue;
20882
- const parsed = (0, import_types94.parseFileId)(edge.source);
21047
+ if (edge.type !== import_types95.EdgeType.CONFIGURED_BY) continue;
21048
+ const parsed = (0, import_types95.parseFileId)(edge.source);
20883
21049
  if (parsed) return parsed.service;
20884
21050
  }
20885
21051
  return null;
@@ -20890,7 +21056,7 @@ function findConfigNode(graph, basenames, serviceName) {
20890
21056
  graph.forEachNode((id, attrs) => {
20891
21057
  if (scoped) return;
20892
21058
  const node = attrs;
20893
- if (node.type !== import_types94.NodeType.ConfigNode) return;
21059
+ if (node.type !== import_types95.NodeType.ConfigNode) return;
20894
21060
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
20895
21061
  if (anyMatch === null) anyMatch = id;
20896
21062
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -20907,13 +21073,13 @@ function createEasResolveTarget(graph) {
20907
21073
  if (basenames.length > 0) {
20908
21074
  const configNodeId = findConfigNode(graph, basenames, serviceName);
20909
21075
  if (configNodeId) {
20910
- return { targetNodeId: configNodeId, serviceName, edgeType: import_types94.EdgeType.CALLS };
21076
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types95.EdgeType.CALLS };
20911
21077
  }
20912
21078
  }
20913
21079
  return {
20914
21080
  targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
20915
21081
  serviceName,
20916
- edgeType: import_types94.EdgeType.CALLS
21082
+ edgeType: import_types95.EdgeType.CALLS
20917
21083
  };
20918
21084
  };
20919
21085
  }
@@ -21628,11 +21794,11 @@ function registerRoutes(scope, ctx) {
21628
21794
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
21629
21795
  const parsed = [];
21630
21796
  for (const c of candidates) {
21631
- const r = import_types97.DivergenceTypeSchema.safeParse(c);
21797
+ const r = import_types98.DivergenceTypeSchema.safeParse(c);
21632
21798
  if (!r.success) {
21633
21799
  return reply.code(400).send({
21634
21800
  error: `unknown divergence type "${c}"`,
21635
- allowed: import_types97.DivergenceTypeSchema.options
21801
+ allowed: import_types98.DivergenceTypeSchema.options
21636
21802
  });
21637
21803
  }
21638
21804
  parsed.push(r.data);
@@ -21749,6 +21915,7 @@ function registerRoutes(scope, ctx) {
21749
21915
  reg.connector,
21750
21916
  {
21751
21917
  projectDir: proj.scanPath ?? "",
21918
+ project: proj.name,
21752
21919
  credentials: reg.credentials,
21753
21920
  ...incidentsPath ? { errorsPath: incidentsPath } : {}
21754
21921
  },
@@ -21812,6 +21979,39 @@ function registerRoutes(scope, ctx) {
21812
21979
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
21813
21980
  return result;
21814
21981
  });
21982
+ scope.get("/graph/incident-card/:nodeId", async (req, reply) => {
21983
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21984
+ if (!proj) return;
21985
+ const { nodeId } = req.params;
21986
+ if (!proj.graph.hasNode(nodeId)) {
21987
+ return reply.code(404).send({ error: "node not found", id: nodeId });
21988
+ }
21989
+ const epath = errorsPathFor(proj);
21990
+ const incidents = epath ? await readErrorEvents(epath) : [];
21991
+ let errorEvent;
21992
+ if (req.query.errorId) {
21993
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
21994
+ if (!errorEvent) {
21995
+ return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
21996
+ }
21997
+ } else {
21998
+ const svc = nodeId.replace(/^service:/, "");
21999
+ errorEvent = [...incidents].filter((e) => e.affectedNode === nodeId || e.service === svc).sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))[0];
22000
+ if (!errorEvent) {
22001
+ return reply.code(404).send({ error: "no incident found for node", id: nodeId });
22002
+ }
22003
+ }
22004
+ const policyPath = ctx.policyFilePathFor(proj);
22005
+ let policies = [];
22006
+ if (policyPath) {
22007
+ try {
22008
+ policies = await loadPolicyFile(policyPath);
22009
+ } catch {
22010
+ policies = [];
22011
+ }
22012
+ }
22013
+ return buildIncidentCard(proj.graph, errorEvent, incidents, policies);
22014
+ });
21815
22015
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
21816
22016
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21817
22017
  if (!proj) return;
@@ -21994,7 +22194,7 @@ function registerRoutes(scope, ctx) {
21994
22194
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
21995
22195
  let violations = await log.readAll();
21996
22196
  if (req.query.severity) {
21997
- const sev = import_types97.PolicySeveritySchema.safeParse(req.query.severity);
22197
+ const sev = import_types98.PolicySeveritySchema.safeParse(req.query.severity);
21998
22198
  if (!sev.success) {
21999
22199
  return reply.code(400).send({
22000
22200
  error: "invalid severity",
@@ -22033,7 +22233,7 @@ function registerRoutes(scope, ctx) {
22033
22233
  scope.post("/policies/check", async (req, reply) => {
22034
22234
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
22035
22235
  if (!proj) return;
22036
- const parsed = import_types97.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22236
+ const parsed = import_types98.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22037
22237
  if (!parsed.success) {
22038
22238
  return reply.code(400).send({
22039
22239
  error: "invalid /policies/check body",