@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/index.cjs CHANGED
@@ -6346,11 +6346,25 @@ function upsertInferredEdge(graph, type, source, target, ts) {
6346
6346
  };
6347
6347
  graph.addEdgeWithKey(id, source, target, edge);
6348
6348
  }
6349
+ function emitIncidentEvent(project, ev) {
6350
+ emitNeatEvent({
6351
+ type: "incident",
6352
+ project,
6353
+ payload: {
6354
+ incidentId: ev.id,
6355
+ affectedNode: ev.affectedNode,
6356
+ service: ev.service,
6357
+ incidentKind: (0, import_types8.incidentKindOf)(ev),
6358
+ at: ev.timestamp
6359
+ }
6360
+ });
6361
+ }
6349
6362
  async function appendErrorEvent(ctx, ev) {
6350
6363
  await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
6351
6364
  await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
6365
+ emitIncidentEvent(ctx.project ?? DEFAULT_PROJECT, ev);
6352
6366
  }
6353
- async function appendConnectorIncident(errorsPath, input) {
6367
+ async function appendConnectorIncident(errorsPath, input, project) {
6354
6368
  const ev = {
6355
6369
  id: input.id,
6356
6370
  timestamp: input.timestamp,
@@ -6364,6 +6378,7 @@ async function appendConnectorIncident(errorsPath, input) {
6364
6378
  };
6365
6379
  await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
6366
6380
  await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
6381
+ if (project) emitIncidentEvent(project, ev);
6367
6382
  }
6368
6383
  function landIncidentCallSite(span, callSite, trusted, graph) {
6369
6384
  const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
@@ -6457,12 +6472,13 @@ function buildErrorEventForReceiver(span, graph, scanPath) {
6457
6472
  affectedNode: locus.affectedNode
6458
6473
  };
6459
6474
  }
6460
- function makeErrorSpanWriter(errorsPath, graph, scanPath) {
6475
+ function makeErrorSpanWriter(errorsPath, graph, scanPath, project = DEFAULT_PROJECT) {
6461
6476
  return async (span) => {
6462
6477
  const ev = buildErrorEventForReceiver(span, graph, scanPath);
6463
6478
  if (!ev) return;
6464
6479
  await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
6465
6480
  await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
6481
+ emitIncidentEvent(project, ev);
6466
6482
  };
6467
6483
  }
6468
6484
  async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
@@ -16120,7 +16136,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
16120
16136
  init_cjs_shims();
16121
16137
  var import_fastify2 = __toESM(require("fastify"), 1);
16122
16138
  var import_cors = __toESM(require("@fastify/cors"), 1);
16123
- var import_types97 = require("@neat.is/types");
16139
+ var import_types98 = require("@neat.is/types");
16124
16140
 
16125
16141
  // src/extend/index.ts
16126
16142
  init_cjs_shims();
@@ -17162,9 +17178,160 @@ function queryLogEntries(opts) {
17162
17178
  return merged;
17163
17179
  }
17164
17180
 
17165
- // src/ask.ts
17181
+ // src/goodybag.ts
17166
17182
  init_cjs_shims();
17167
17183
  var import_types59 = require("@neat.is/types");
17184
+ var CODE_FILEPATH_ATTR2 = "code.filepath";
17185
+ var CODE_LINENO_ATTR2 = "code.lineno";
17186
+ var BLAST_NEAREST_LIMIT = 5;
17187
+ function grainOf2(graph, nodeId) {
17188
+ if (graph.hasNode(nodeId)) {
17189
+ const t = graph.getNodeAttributes(nodeId).type;
17190
+ if (typeof t === "string" && t.length > 0) {
17191
+ return (t.endsWith("Node") ? t.slice(0, -4) : t).toLowerCase();
17192
+ }
17193
+ }
17194
+ const colon = nodeId.indexOf(":");
17195
+ return colon > 0 ? nodeId.slice(0, colon) : "unknown";
17196
+ }
17197
+ function locusOf(graph, ev) {
17198
+ const file = ev.attributes?.[CODE_FILEPATH_ATTR2];
17199
+ if (typeof file !== "string" || file.length === 0) return null;
17200
+ const rawLine = ev.attributes?.[CODE_LINENO_ATTR2];
17201
+ const line = typeof rawLine === "number" ? rawLine : Number(rawLine);
17202
+ const node = graph.hasNode(ev.affectedNode) ? graph.getNodeAttributes(ev.affectedNode) : void 0;
17203
+ return {
17204
+ file,
17205
+ ...Number.isFinite(line) ? { lineStart: line, lineEnd: line } : {},
17206
+ ...node?.name ? { symbol: node.name } : {},
17207
+ service: node?.service ?? ev.service,
17208
+ provenance: import_types59.Provenance.OBSERVED
17209
+ };
17210
+ }
17211
+ function locusFromNode(graph, nodeId) {
17212
+ if (!graph.hasNode(nodeId)) return null;
17213
+ const n = graph.getNodeAttributes(nodeId);
17214
+ const file = n.relPath ?? n.path;
17215
+ if (typeof file !== "string" || file.length === 0) return null;
17216
+ const start = n.span?.startLine;
17217
+ const end = n.span?.endLine;
17218
+ return {
17219
+ file,
17220
+ ...typeof start === "number" ? { lineStart: start } : {},
17221
+ ...typeof end === "number" ? { lineEnd: end } : {},
17222
+ ...n.qualname ? { symbol: shortLabel(graph, nodeId) } : {},
17223
+ ...n.service ? { service: n.service } : {},
17224
+ provenance: import_types59.Provenance.INFERRED
17225
+ };
17226
+ }
17227
+ function promoteCauseLocus(graph, causeNode, incidents) {
17228
+ const native = incidents.find(
17229
+ (e) => e.affectedNode === causeNode && typeof e.attributes?.[CODE_FILEPATH_ATTR2] === "string"
17230
+ );
17231
+ if (native) {
17232
+ const l = locusOf(graph, native);
17233
+ if (l) return { ...l, symbol: shortLabel(graph, causeNode), provenance: import_types59.Provenance.INFERRED };
17234
+ }
17235
+ return locusFromNode(graph, causeNode);
17236
+ }
17237
+ function divergenceSummary(d) {
17238
+ const column = "column" in d && d.column ? `.${d.column}` : "";
17239
+ const label = "table" in d && d.table ? `${d.table}${column}` : `${d.source} \u2192 ${d.target}`;
17240
+ return label;
17241
+ }
17242
+ function baseName(p) {
17243
+ const parts = p.split(/[\\/]/);
17244
+ return parts[parts.length - 1] || p;
17245
+ }
17246
+ function shortLabel(graph, nodeId) {
17247
+ if (graph.hasNode(nodeId)) {
17248
+ const name = graph.getNodeAttributes(nodeId).name;
17249
+ if (typeof name === "string" && name.length > 0) return name;
17250
+ }
17251
+ const afterHash = nodeId.includes("#") ? nodeId.slice(nodeId.lastIndexOf("#") + 1) : nodeId;
17252
+ return afterHash.includes(":") ? afterHash.slice(afterHash.lastIndexOf(":") + 1) : afterHash;
17253
+ }
17254
+ function renderHeadline(graph, ev, locus, causeNode) {
17255
+ const what = ev.exceptionType ? `raised ${ev.exceptionType}` : ev.errorMessage || "failed";
17256
+ const causeLabel = causeNode && causeNode !== ev.affectedNode ? shortLabel(graph, causeNode) : "";
17257
+ if (locus) {
17258
+ const base = baseName(locus.file);
17259
+ const lines = locus.lineStart != null ? locus.lineEnd && locus.lineEnd !== locus.lineStart ? `LINES ${locus.lineStart}-${locus.lineEnd}` : `LINE ${locus.lineStart}` : "";
17260
+ const symbol = locus.symbol ?? (grainOf2(graph, ev.affectedNode) === "symbol" ? shortLabel(graph, ev.affectedNode) : void 0);
17261
+ const subject = symbol ? `SYMBOL ${symbol}` : `FILE ${base}`;
17262
+ const at = symbol ? `${lines ? `${lines} in ` : ""}${base}` : lines;
17263
+ const where = at ? ` at ${at}` : "";
17264
+ const svc = locus.service ?? ev.service;
17265
+ const cause2 = causeLabel && causeLabel !== symbol ? ` \u2192 root cause ${causeLabel}` : "";
17266
+ return `${subject}${where} (SERVICE ${svc}) ${what} at ${ev.timestamp}${cause2}`;
17267
+ }
17268
+ const cause = causeLabel ? ` \u2192 root cause ${causeLabel}` : "";
17269
+ return `SERVICE ${ev.service} ${what} at ${ev.timestamp}${cause}`;
17270
+ }
17271
+ function buildIncidentCard(graph, errorEvent, incidents, policies) {
17272
+ const affected = errorEvent.affectedNode;
17273
+ let locus = locusOf(graph, errorEvent);
17274
+ const inGraph = graph.hasNode(affected);
17275
+ const rc = inGraph ? getRootCause(graph, affected, errorEvent, incidents) : null;
17276
+ let rootCause = null;
17277
+ if (rc) {
17278
+ const provs = rc.edgeProvenances ?? [];
17279
+ const chain = (rc.traversalPath ?? []).map((node, i) => ({
17280
+ node,
17281
+ grain: grainOf2(graph, node),
17282
+ provenance: provs[i] ?? provs[provs.length - 1] ?? import_types59.Provenance.INFERRED
17283
+ }));
17284
+ rootCause = {
17285
+ node: rc.rootCauseNode,
17286
+ ...rc.candidates?.[0]?.classification ? { classification: rc.candidates[0].classification } : {},
17287
+ reason: rc.rootCauseReason,
17288
+ confidence: rc.confidence,
17289
+ fix: rc.fixRecommendation ?? null,
17290
+ chain
17291
+ };
17292
+ }
17293
+ if (locus === null && rootCause) {
17294
+ locus = promoteCauseLocus(graph, rootCause.node, incidents);
17295
+ }
17296
+ const blast = inGraph ? getBlastRadius(graph, affected) : { origin: affected, affectedNodes: [], totalAffected: 0 };
17297
+ 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 }));
17298
+ const applicable = inGraph ? selectApplicablePolicies(graph, policies, affected) : [];
17299
+ const policyCards = applicable.map((p) => ({
17300
+ policyName: p.policyName,
17301
+ severity: p.severity,
17302
+ message: p.reason
17303
+ }));
17304
+ const divergences = inGraph ? computeDivergences(graph, { node: affected, incidents }).divergences.map((d) => ({
17305
+ type: d.type,
17306
+ summary: divergenceSummary(d)
17307
+ })) : [];
17308
+ const card = {
17309
+ kind: "incident",
17310
+ id: errorEvent.id,
17311
+ at: errorEvent.timestamp,
17312
+ incidentKind: (0, import_types59.incidentKindOf)(errorEvent),
17313
+ service: errorEvent.service,
17314
+ affectedNode: affected,
17315
+ message: errorEvent.errorMessage,
17316
+ ...errorEvent.exceptionType ? { exceptionType: errorEvent.exceptionType } : {},
17317
+ ...errorEvent.httpStatusCode !== void 0 ? { httpStatusCode: errorEvent.httpStatusCode } : {},
17318
+ ...errorEvent.incidentCount !== void 0 ? { count: errorEvent.incidentCount } : {},
17319
+ ...errorEvent.firstTimestamp && errorEvent.lastTimestamp ? { window: { first: errorEvent.firstTimestamp, last: errorEvent.lastTimestamp } } : {},
17320
+ ...errorEvent.traceId ? { traceId: errorEvent.traceId } : {},
17321
+ ...errorEvent.spanId ? { spanId: errorEvent.spanId } : {},
17322
+ locus,
17323
+ rootCause,
17324
+ ...nearest.length > 0 ? { blastRadius: { totalAffected: blast.totalAffected, nearest } } : {},
17325
+ ...policyCards.length > 0 ? { policies: policyCards } : {},
17326
+ ...divergences.length > 0 ? { divergence: divergences } : {},
17327
+ headline: renderHeadline(graph, errorEvent, locus, rootCause?.node ?? null)
17328
+ };
17329
+ return import_types59.IncidentCardSchema.parse(card);
17330
+ }
17331
+
17332
+ // src/ask.ts
17333
+ init_cjs_shims();
17334
+ var import_types60 = require("@neat.is/types");
17168
17335
  var DEFAULT_MAX_NODES = 3;
17169
17336
  var MAX_FACTS_PER_SECTION = 6;
17170
17337
  var INTENT_RULES = [
@@ -17392,7 +17559,7 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17392
17559
  };
17393
17560
  graph.forEachNode((id, attrs) => {
17394
17561
  const node = attrs;
17395
- if (node.type === import_types59.NodeType.FrontierNode) return;
17562
+ if (node.type === import_types60.NodeType.FrontierNode) return;
17396
17563
  const name = nodeName(node);
17397
17564
  const body = idBody(id);
17398
17565
  const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
@@ -17411,7 +17578,7 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17411
17578
  const res = await searchIndex.search(question, 10);
17412
17579
  if (res.provider !== "substring") {
17413
17580
  for (const m of res.matches) {
17414
- if (m.node.type === import_types59.NodeType.FrontierNode) continue;
17581
+ if (m.node.type === import_types60.NodeType.FrontierNode) continue;
17415
17582
  const already = best.get(m.node.id);
17416
17583
  if (!already && m.score < EMBED_MIN_SCORE) continue;
17417
17584
  consider({
@@ -17449,7 +17616,7 @@ function edgeSignalNote(e) {
17449
17616
  function buildRootCauseSection(graph, node, incidents, now) {
17450
17617
  const result = getRootCause(graph, node, void 0, incidents, { now });
17451
17618
  if (!result) return null;
17452
- const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? import_types59.Provenance.OBSERVED;
17619
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? import_types60.Provenance.OBSERVED;
17453
17620
  const facts = [
17454
17621
  {
17455
17622
  text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
@@ -17499,7 +17666,7 @@ function buildObservedSection(graph, node) {
17499
17666
  if (result.observed && result.inboundObservedCount > 0) {
17500
17667
  facts.push({
17501
17668
  text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
17502
- provenance: import_types59.Provenance.OBSERVED
17669
+ provenance: import_types60.Provenance.OBSERVED
17503
17670
  });
17504
17671
  } else {
17505
17672
  return null;
@@ -17527,7 +17694,7 @@ function buildIncidentsSection(node, incidents) {
17527
17694
  const facts = ordered.map((ev) => ({
17528
17695
  text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
17529
17696
  // ErrorEvents are observation records — OBSERVED by definition.
17530
- provenance: import_types59.Provenance.OBSERVED
17697
+ provenance: import_types60.Provenance.OBSERVED
17531
17698
  }));
17532
17699
  return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
17533
17700
  }
@@ -17587,7 +17754,7 @@ function buildGlobalIncidentsSection(incidents) {
17587
17754
  }
17588
17755
  const byKey = /* @__PURE__ */ new Map();
17589
17756
  for (const ev of incidents) {
17590
- const key = ev.affectedNode || (0, import_types59.serviceId)(ev.service);
17757
+ const key = ev.affectedNode || (0, import_types60.serviceId)(ev.service);
17591
17758
  const cur = byKey.get(key);
17592
17759
  if (!cur) {
17593
17760
  byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
@@ -17602,7 +17769,7 @@ function buildGlobalIncidentsSection(incidents) {
17602
17769
  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);
17603
17770
  const facts = rows.map((r) => ({
17604
17771
  text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
17605
- provenance: import_types59.Provenance.OBSERVED
17772
+ provenance: import_types60.Provenance.OBSERVED
17606
17773
  }));
17607
17774
  return {
17608
17775
  heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
@@ -17615,7 +17782,7 @@ function buildOverviewSections(graph, incidents) {
17615
17782
  graph.forEachNode((_id, attrs) => {
17616
17783
  const node = attrs;
17617
17784
  nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
17618
- if (node.type === import_types59.NodeType.ServiceNode) services.push(node.id);
17785
+ if (node.type === import_types60.NodeType.ServiceNode) services.push(node.id);
17619
17786
  });
17620
17787
  const edgeByProv = /* @__PURE__ */ new Map();
17621
17788
  graph.forEachEdge((_id, attrs) => {
@@ -17627,10 +17794,10 @@ function buildOverviewSections(graph, incidents) {
17627
17794
  const shapeFacts = [
17628
17795
  { text: `${graph.order} nodes, ${graph.size} edges` },
17629
17796
  {
17630
- 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`
17797
+ text: `${count(import_types60.NodeType.ServiceNode)} services, ${count(import_types60.NodeType.FileNode)} files, ${count(import_types60.NodeType.SymbolNode)} symbols, ${count(import_types60.NodeType.DatabaseNode)} databases`
17631
17798
  }
17632
17799
  ];
17633
- for (const p of [import_types59.Provenance.EXTRACTED, import_types59.Provenance.OBSERVED, import_types59.Provenance.INFERRED, import_types59.Provenance.STALE]) {
17800
+ for (const p of [import_types60.Provenance.EXTRACTED, import_types60.Provenance.OBSERVED, import_types60.Provenance.INFERRED, import_types60.Provenance.STALE]) {
17634
17801
  const n = edgeByProv.get(p) ?? 0;
17635
17802
  if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
17636
17803
  }
@@ -17647,7 +17814,7 @@ function buildOverviewSections(graph, incidents) {
17647
17814
  if (incidents && incidents.length > 0) {
17648
17815
  const incCount = /* @__PURE__ */ new Map();
17649
17816
  for (const ev of incidents) {
17650
- const key = ev.affectedNode || (0, import_types59.serviceId)(ev.service);
17817
+ const key = ev.affectedNode || (0, import_types60.serviceId)(ev.service);
17651
17818
  incCount.set(key, (incCount.get(key) ?? 0) + 1);
17652
17819
  }
17653
17820
  const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
@@ -17655,7 +17822,7 @@ function buildOverviewSections(graph, incidents) {
17655
17822
  heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
17656
17823
  facts: top.map(([k, n]) => ({
17657
17824
  text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
17658
- provenance: import_types59.Provenance.OBSERVED
17825
+ provenance: import_types60.Provenance.OBSERVED
17659
17826
  }))
17660
17827
  });
17661
17828
  }
@@ -17791,7 +17958,7 @@ async function askGraph(graph, question, opts = {}) {
17791
17958
  const provSet = /* @__PURE__ */ new Set();
17792
17959
  for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
17793
17960
  const confidence = sections[0]?.facts[0]?.confidence;
17794
- return import_types59.AskResultSchema.parse({
17961
+ return import_types60.AskResultSchema.parse({
17795
17962
  question,
17796
17963
  intent,
17797
17964
  matched,
@@ -17938,7 +18105,7 @@ init_cjs_shims();
17938
18105
  var import_node_fs37 = require("fs");
17939
18106
  var import_node_os3 = __toESM(require("os"), 1);
17940
18107
  var import_node_path72 = __toESM(require("path"), 1);
17941
- var import_types60 = require("@neat.is/types");
18108
+ var import_types61 = require("@neat.is/types");
17942
18109
  var LOCK_TIMEOUT_MS = 5e3;
17943
18110
  var LOCK_RETRY_MS = 50;
17944
18111
  function neatHome() {
@@ -18148,10 +18315,10 @@ async function readRegistry() {
18148
18315
  throw err;
18149
18316
  }
18150
18317
  const parsed = JSON.parse(raw);
18151
- return import_types60.RegistryFileSchema.parse(parsed);
18318
+ return import_types61.RegistryFileSchema.parse(parsed);
18152
18319
  }
18153
18320
  async function writeRegistry(reg) {
18154
- const validated = import_types60.RegistryFileSchema.parse(reg);
18321
+ const validated = import_types61.RegistryFileSchema.parse(reg);
18155
18322
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
18156
18323
  }
18157
18324
  var ProjectNameCollisionError = class extends Error {
@@ -18552,15 +18719,15 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
18552
18719
 
18553
18720
  // src/connectors/index.ts
18554
18721
  init_cjs_shims();
18555
- var import_types61 = require("@neat.is/types");
18722
+ var import_types62 = require("@neat.is/types");
18556
18723
  var NO_ENV = "unknown";
18557
18724
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
18558
18725
  if (!graph.hasNode(targetNodeId)) return void 0;
18559
18726
  const sites = [];
18560
18727
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
18561
18728
  const edge = graph.getEdgeAttributes(edgeId);
18562
- if (edge.provenance !== import_types61.Provenance.EXTRACTED) continue;
18563
- const parsed = (0, import_types61.parseFileId)(edge.source);
18729
+ if (edge.provenance !== import_types62.Provenance.EXTRACTED) continue;
18730
+ const parsed = (0, import_types62.parseFileId)(edge.source);
18564
18731
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
18565
18732
  const site = { relPath: edge.evidence.file };
18566
18733
  if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
@@ -18571,7 +18738,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
18571
18738
  function routeCallSiteFor(graph, targetNodeId) {
18572
18739
  if (!graph.hasNode(targetNodeId)) return void 0;
18573
18740
  const attrs = graph.getNodeAttributes(targetNodeId);
18574
- if (attrs.type !== import_types61.NodeType.RouteNode || !attrs.path) return void 0;
18741
+ if (attrs.type !== import_types62.NodeType.RouteNode || !attrs.path) return void 0;
18575
18742
  const site = { relPath: attrs.path };
18576
18743
  if (attrs.line !== void 0) site.line = attrs.line;
18577
18744
  return site;
@@ -18601,7 +18768,7 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
18601
18768
  errorMessage: signal.incident.errorMessage,
18602
18769
  ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
18603
18770
  affectedNode: resolved.targetNodeId
18604
- });
18771
+ }, ctx.project);
18605
18772
  continue;
18606
18773
  }
18607
18774
  if (resolved.ensureInfraNode) {
@@ -19183,23 +19350,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
19183
19350
 
19184
19351
  // src/connectors/supabase/resolve.ts
19185
19352
  init_cjs_shims();
19186
- var import_types63 = require("@neat.is/types");
19353
+ var import_types64 = require("@neat.is/types");
19187
19354
  function createSupabaseResolveTarget(graph, config) {
19188
19355
  return (signal, _ctx) => {
19189
19356
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
19190
19357
  return null;
19191
19358
  }
19192
- const subResourceId = (0, import_types63.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
19359
+ const subResourceId = (0, import_types64.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
19193
19360
  if (graph.hasNode(subResourceId)) {
19194
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
19361
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
19195
19362
  }
19196
- const bareResourceId = (0, import_types63.infraId)(signal.targetKind, signal.targetName);
19363
+ const bareResourceId = (0, import_types64.infraId)(signal.targetKind, signal.targetName);
19197
19364
  if (graph.hasNode(bareResourceId)) {
19198
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
19365
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
19199
19366
  }
19200
- const projectLevelId = (0, import_types63.infraId)("supabase", config.nodeRef);
19367
+ const projectLevelId = (0, import_types64.infraId)("supabase", config.nodeRef);
19201
19368
  if (graph.hasNode(projectLevelId)) {
19202
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
19369
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
19203
19370
  }
19204
19371
  return null;
19205
19372
  };
@@ -19292,7 +19459,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
19292
19459
 
19293
19460
  // src/connectors/railway/index.ts
19294
19461
  init_cjs_shims();
19295
- var import_types67 = require("@neat.is/types");
19462
+ var import_types68 = require("@neat.is/types");
19296
19463
 
19297
19464
  // src/connectors/railway/client.ts
19298
19465
  init_cjs_shims();
@@ -19443,7 +19610,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
19443
19610
  const out = [];
19444
19611
  graph.forEachNode((_id, attrs) => {
19445
19612
  const node = attrs;
19446
- if (node.type !== import_types67.NodeType.RouteNode) return;
19613
+ if (node.type !== import_types68.NodeType.RouteNode) return;
19447
19614
  const route = attrs;
19448
19615
  if (route.service !== serviceName) return;
19449
19616
  out.push({
@@ -19547,12 +19714,12 @@ function createRailwayResolveTarget(config) {
19547
19714
  const serviceName = config.serviceNameById[config.serviceId];
19548
19715
  if (!serviceName) return null;
19549
19716
  if (signal.targetKind === ROUTE_TARGET_KIND) {
19550
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types67.EdgeType.CALLS };
19717
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types68.EdgeType.CALLS };
19551
19718
  }
19552
19719
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
19553
19720
  const peerName = config.serviceNameById[signal.targetName];
19554
19721
  if (!peerName) return null;
19555
- return { targetNodeId: (0, import_types67.serviceId)(peerName), serviceName, edgeType: import_types67.EdgeType.CONNECTS_TO };
19722
+ return { targetNodeId: (0, import_types68.serviceId)(peerName), serviceName, edgeType: import_types68.EdgeType.CONNECTS_TO };
19556
19723
  }
19557
19724
  return null;
19558
19725
  };
@@ -19740,7 +19907,7 @@ function mapLogEntriesToSignals(entries) {
19740
19907
 
19741
19908
  // src/connectors/firebase/resolve.ts
19742
19909
  init_cjs_shims();
19743
- var import_types68 = require("@neat.is/types");
19910
+ var import_types69 = require("@neat.is/types");
19744
19911
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
19745
19912
  switch (resourceType) {
19746
19913
  case "cloud_function":
@@ -19755,7 +19922,7 @@ function routeEntriesFor(graph, serviceName) {
19755
19922
  const entries = [];
19756
19923
  graph.forEachNode((_id, attrs) => {
19757
19924
  const node = attrs;
19758
- if (node.type !== import_types68.NodeType.RouteNode) return;
19925
+ if (node.type !== import_types69.NodeType.RouteNode) return;
19759
19926
  const route = attrs;
19760
19927
  if (route.service !== serviceName) return;
19761
19928
  entries.push({
@@ -19787,7 +19954,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
19787
19954
  return {
19788
19955
  targetNodeId: match.routeNodeId,
19789
19956
  serviceName,
19790
- edgeType: import_types68.EdgeType.CALLS
19957
+ edgeType: import_types69.EdgeType.CALLS
19791
19958
  };
19792
19959
  };
19793
19960
  }
@@ -19814,7 +19981,7 @@ init_cjs_shims();
19814
19981
 
19815
19982
  // src/connectors/cloudflare/connector.ts
19816
19983
  init_cjs_shims();
19817
- var import_types70 = require("@neat.is/types");
19984
+ var import_types71 = require("@neat.is/types");
19818
19985
 
19819
19986
  // src/connectors/cloudflare/client.ts
19820
19987
  init_cjs_shims();
@@ -19978,7 +20145,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
19978
20145
  graph.forEachNode((id, attrs) => {
19979
20146
  if (found) return;
19980
20147
  const a = attrs;
19981
- if (a.type === import_types70.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
20148
+ if (a.type === import_types71.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
19982
20149
  found = id;
19983
20150
  }
19984
20151
  });
@@ -19990,7 +20157,7 @@ function findMatchingRouteNode(graph, serviceName, method, path76) {
19990
20157
  graph.forEachNode((id, attrs) => {
19991
20158
  if (found) return;
19992
20159
  const a = attrs;
19993
- if (a.type !== import_types70.NodeType.RouteNode || a.service !== serviceName) return;
20160
+ if (a.type !== import_types71.NodeType.RouteNode || a.service !== serviceName) return;
19994
20161
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
19995
20162
  const routeMethod = (a.method ?? "").toUpperCase();
19996
20163
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -20009,11 +20176,11 @@ function createCloudflareResolveTarget(config, graph) {
20009
20176
  };
20010
20177
  const mapping = config.workers?.[scriptName];
20011
20178
  if (mapping) {
20012
- const wholeFileId = (0, import_types70.fileId)(mapping.service, mapping.entryFile);
20179
+ const wholeFileId = (0, import_types71.fileId)(mapping.service, mapping.entryFile);
20013
20180
  return {
20014
20181
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
20015
20182
  serviceName: mapping.service,
20016
- edgeType: import_types70.EdgeType.CALLS
20183
+ edgeType: import_types71.EdgeType.CALLS
20017
20184
  };
20018
20185
  }
20019
20186
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -20022,13 +20189,13 @@ function createCloudflareResolveTarget(config, graph) {
20022
20189
  return {
20023
20190
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
20024
20191
  serviceName: fileNode.service,
20025
- edgeType: import_types70.EdgeType.CALLS
20192
+ edgeType: import_types71.EdgeType.CALLS
20026
20193
  };
20027
20194
  }
20028
20195
  return {
20029
- targetNodeId: (0, import_types70.infraId)("cloudflare-worker", scriptName),
20196
+ targetNodeId: (0, import_types71.infraId)("cloudflare-worker", scriptName),
20030
20197
  serviceName: scriptName,
20031
- edgeType: import_types70.EdgeType.CALLS,
20198
+ edgeType: import_types71.EdgeType.CALLS,
20032
20199
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
20033
20200
  };
20034
20201
  };
@@ -20224,14 +20391,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
20224
20391
 
20225
20392
  // src/connectors/neon/resolve.ts
20226
20393
  init_cjs_shims();
20227
- var import_types74 = require("@neat.is/types");
20394
+ var import_types75 = require("@neat.is/types");
20228
20395
  function createNeonResolveTarget(config) {
20229
20396
  return (signal) => {
20230
20397
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
20231
20398
  return {
20232
- targetNodeId: (0, import_types74.infraId)("sql-table", signal.targetName),
20399
+ targetNodeId: (0, import_types75.infraId)("sql-table", signal.targetName),
20233
20400
  serviceName: config.serviceName,
20234
- edgeType: import_types74.EdgeType.CALLS,
20401
+ edgeType: import_types75.EdgeType.CALLS,
20235
20402
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
20236
20403
  };
20237
20404
  };
@@ -20412,14 +20579,14 @@ function mapLogEntriesToSignals2(entries) {
20412
20579
 
20413
20580
  // src/connectors/cloud-run/resolve.ts
20414
20581
  init_cjs_shims();
20415
- var import_types78 = require("@neat.is/types");
20582
+ var import_types79 = require("@neat.is/types");
20416
20583
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
20417
20584
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
20418
20585
  let found = null;
20419
20586
  graph.forEachNode((_id, attrs) => {
20420
20587
  if (found) return;
20421
20588
  const node = attrs;
20422
- if (node.type !== import_types78.NodeType.RouteNode) return;
20589
+ if (node.type !== import_types79.NodeType.RouteNode) return;
20423
20590
  const route = attrs;
20424
20591
  if (route.service !== serviceName || !route.pathTemplate) return;
20425
20592
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -20444,13 +20611,13 @@ function createCloudRunResolveTarget(graph, config) {
20444
20611
  normalizePathTemplate(path76)
20445
20612
  );
20446
20613
  if (routeNodeId) {
20447
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types78.EdgeType.CALLS };
20614
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types79.EdgeType.CALLS };
20448
20615
  }
20449
20616
  }
20450
20617
  return {
20451
- targetNodeId: (0, import_types78.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
20618
+ targetNodeId: (0, import_types79.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
20452
20619
  serviceName: mappedService ?? gcpServiceName,
20453
- edgeType: import_types78.EdgeType.CALLS,
20620
+ edgeType: import_types79.EdgeType.CALLS,
20454
20621
  ensureInfraNode: {
20455
20622
  kind: CLOUD_RUN_SERVICE_INFRA_KIND,
20456
20623
  name: gcpServiceName,
@@ -20632,14 +20799,14 @@ function mapLogEntriesToSignals3(entries) {
20632
20799
 
20633
20800
  // src/connectors/gcp-lb/resolve.ts
20634
20801
  init_cjs_shims();
20635
- var import_types82 = require("@neat.is/types");
20802
+ var import_types83 = require("@neat.is/types");
20636
20803
  var GCP_LB_BACKEND_INFRA_KIND = "gcp-lb-backend";
20637
20804
  function findMatchingRouteNode3(graph, serviceName, method, normalizedPath) {
20638
20805
  let found = null;
20639
20806
  graph.forEachNode((_id, attrs) => {
20640
20807
  if (found) return;
20641
20808
  const node = attrs;
20642
- if (node.type !== import_types82.NodeType.RouteNode) return;
20809
+ if (node.type !== import_types83.NodeType.RouteNode) return;
20643
20810
  const route = attrs;
20644
20811
  if (route.service !== serviceName || !route.pathTemplate) return;
20645
20812
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -20664,13 +20831,13 @@ function createGcpLbResolveTarget(graph, config) {
20664
20831
  normalizePathTemplate(path76)
20665
20832
  );
20666
20833
  if (routeNodeId) {
20667
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types82.EdgeType.CALLS };
20834
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types83.EdgeType.CALLS };
20668
20835
  }
20669
20836
  }
20670
20837
  return {
20671
- targetNodeId: (0, import_types82.infraId)(GCP_LB_BACKEND_INFRA_KIND, backendServiceName),
20838
+ targetNodeId: (0, import_types83.infraId)(GCP_LB_BACKEND_INFRA_KIND, backendServiceName),
20672
20839
  serviceName: mappedService ?? backendServiceName,
20673
- edgeType: import_types82.EdgeType.CALLS,
20840
+ edgeType: import_types83.EdgeType.CALLS,
20674
20841
  ensureInfraNode: {
20675
20842
  kind: GCP_LB_BACKEND_INFRA_KIND,
20676
20843
  name: backendServiceName,
@@ -20711,7 +20878,7 @@ function createGcpLbConnector(graph, config = {}) {
20711
20878
 
20712
20879
  // src/connectors/render/index.ts
20713
20880
  init_cjs_shims();
20714
- var import_types85 = require("@neat.is/types");
20881
+ var import_types86 = require("@neat.is/types");
20715
20882
 
20716
20883
  // src/connectors/render/types.ts
20717
20884
  init_cjs_shims();
@@ -20789,7 +20956,7 @@ function buildRenderRouteIndex(graph, serviceName) {
20789
20956
  const out = [];
20790
20957
  graph.forEachNode((_id, attrs) => {
20791
20958
  const node = attrs;
20792
- if (node.type !== import_types85.NodeType.RouteNode) return;
20959
+ if (node.type !== import_types86.NodeType.RouteNode) return;
20793
20960
  const route = attrs;
20794
20961
  if (route.service !== serviceName) return;
20795
20962
  out.push({
@@ -20874,7 +21041,7 @@ function mapRenderRequestLogsToSignals(entries, routeIndex) {
20874
21041
  function createRenderResolveTarget(config) {
20875
21042
  return (signal) => {
20876
21043
  if (signal.targetKind === ROUTE_TARGET_KIND2) {
20877
- return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types85.EdgeType.CALLS };
21044
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types86.EdgeType.CALLS };
20878
21045
  }
20879
21046
  return null;
20880
21047
  };
@@ -21012,21 +21179,21 @@ function mapInsightsToSignals(rows, observedAtIso) {
21012
21179
 
21013
21180
  // src/connectors/planetscale/resolve.ts
21014
21181
  init_cjs_shims();
21015
- var import_types89 = require("@neat.is/types");
21182
+ var import_types90 = require("@neat.is/types");
21016
21183
  var PLANETSCALE_DATABASE_KIND = "planetscale-database";
21017
21184
  function createPlanetscaleResolveTarget(graph, config) {
21018
21185
  const databaseName = `${config.organization}/${config.database}`;
21019
21186
  return (signal, _ctx) => {
21020
21187
  if (signal.targetKind !== PLANETSCALE_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
21021
- const tableId = (0, import_types89.infraId)("sql-table", signal.targetName);
21188
+ const tableId = (0, import_types90.infraId)("sql-table", signal.targetName);
21022
21189
  if (graph.hasNode(tableId)) {
21023
- return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types89.EdgeType.CALLS };
21190
+ return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types90.EdgeType.CALLS };
21024
21191
  }
21025
- const providerId = (0, import_types89.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
21192
+ const providerId = (0, import_types90.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
21026
21193
  return {
21027
21194
  targetNodeId: providerId,
21028
21195
  serviceName: config.serviceName,
21029
- edgeType: import_types89.EdgeType.CALLS,
21196
+ edgeType: import_types90.EdgeType.CALLS,
21030
21197
  ensureInfraNode: { kind: PLANETSCALE_DATABASE_KIND, name: databaseName, provider: "planetscale" }
21031
21198
  };
21032
21199
  };
@@ -21291,7 +21458,7 @@ function mapBuildsToSignals(builds, serviceName) {
21291
21458
 
21292
21459
  // src/connectors/eas/resolve.ts
21293
21460
  init_cjs_shims();
21294
- var import_types94 = require("@neat.is/types");
21461
+ var import_types95 = require("@neat.is/types");
21295
21462
  var NO_ENV2 = "unknown";
21296
21463
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
21297
21464
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -21307,8 +21474,8 @@ function configBasenamesForPhase(phase) {
21307
21474
  function configNodeService(graph, configNodeId) {
21308
21475
  for (const edgeId of graph.inboundEdges(configNodeId)) {
21309
21476
  const edge = graph.getEdgeAttributes(edgeId);
21310
- if (edge.type !== import_types94.EdgeType.CONFIGURED_BY) continue;
21311
- const parsed = (0, import_types94.parseFileId)(edge.source);
21477
+ if (edge.type !== import_types95.EdgeType.CONFIGURED_BY) continue;
21478
+ const parsed = (0, import_types95.parseFileId)(edge.source);
21312
21479
  if (parsed) return parsed.service;
21313
21480
  }
21314
21481
  return null;
@@ -21319,7 +21486,7 @@ function findConfigNode(graph, basenames, serviceName) {
21319
21486
  graph.forEachNode((id, attrs) => {
21320
21487
  if (scoped) return;
21321
21488
  const node = attrs;
21322
- if (node.type !== import_types94.NodeType.ConfigNode) return;
21489
+ if (node.type !== import_types95.NodeType.ConfigNode) return;
21323
21490
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
21324
21491
  if (anyMatch === null) anyMatch = id;
21325
21492
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -21336,13 +21503,13 @@ function createEasResolveTarget(graph) {
21336
21503
  if (basenames.length > 0) {
21337
21504
  const configNodeId = findConfigNode(graph, basenames, serviceName);
21338
21505
  if (configNodeId) {
21339
- return { targetNodeId: configNodeId, serviceName, edgeType: import_types94.EdgeType.CALLS };
21506
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types95.EdgeType.CALLS };
21340
21507
  }
21341
21508
  }
21342
21509
  return {
21343
21510
  targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
21344
21511
  serviceName,
21345
- edgeType: import_types94.EdgeType.CALLS
21512
+ edgeType: import_types95.EdgeType.CALLS
21346
21513
  };
21347
21514
  };
21348
21515
  }
@@ -21929,6 +22096,7 @@ async function startConnectorPolling(input) {
21929
22096
  registration.connector,
21930
22097
  {
21931
22098
  projectDir: input.projectDir,
22099
+ project: input.project,
21932
22100
  credentials: registration.credentials,
21933
22101
  ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
21934
22102
  },
@@ -22103,11 +22271,11 @@ function registerRoutes(scope, ctx) {
22103
22271
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
22104
22272
  const parsed = [];
22105
22273
  for (const c of candidates) {
22106
- const r = import_types97.DivergenceTypeSchema.safeParse(c);
22274
+ const r = import_types98.DivergenceTypeSchema.safeParse(c);
22107
22275
  if (!r.success) {
22108
22276
  return reply.code(400).send({
22109
22277
  error: `unknown divergence type "${c}"`,
22110
- allowed: import_types97.DivergenceTypeSchema.options
22278
+ allowed: import_types98.DivergenceTypeSchema.options
22111
22279
  });
22112
22280
  }
22113
22281
  parsed.push(r.data);
@@ -22224,6 +22392,7 @@ function registerRoutes(scope, ctx) {
22224
22392
  reg.connector,
22225
22393
  {
22226
22394
  projectDir: proj.scanPath ?? "",
22395
+ project: proj.name,
22227
22396
  credentials: reg.credentials,
22228
22397
  ...incidentsPath ? { errorsPath: incidentsPath } : {}
22229
22398
  },
@@ -22287,6 +22456,39 @@ function registerRoutes(scope, ctx) {
22287
22456
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
22288
22457
  return result;
22289
22458
  });
22459
+ scope.get("/graph/incident-card/:nodeId", async (req, reply) => {
22460
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
22461
+ if (!proj) return;
22462
+ const { nodeId } = req.params;
22463
+ if (!proj.graph.hasNode(nodeId)) {
22464
+ return reply.code(404).send({ error: "node not found", id: nodeId });
22465
+ }
22466
+ const epath = errorsPathFor(proj);
22467
+ const incidents = epath ? await readErrorEvents(epath) : [];
22468
+ let errorEvent;
22469
+ if (req.query.errorId) {
22470
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
22471
+ if (!errorEvent) {
22472
+ return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
22473
+ }
22474
+ } else {
22475
+ const svc = nodeId.replace(/^service:/, "");
22476
+ errorEvent = [...incidents].filter((e) => e.affectedNode === nodeId || e.service === svc).sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))[0];
22477
+ if (!errorEvent) {
22478
+ return reply.code(404).send({ error: "no incident found for node", id: nodeId });
22479
+ }
22480
+ }
22481
+ const policyPath = ctx.policyFilePathFor(proj);
22482
+ let policies = [];
22483
+ if (policyPath) {
22484
+ try {
22485
+ policies = await loadPolicyFile(policyPath);
22486
+ } catch {
22487
+ policies = [];
22488
+ }
22489
+ }
22490
+ return buildIncidentCard(proj.graph, errorEvent, incidents, policies);
22491
+ });
22290
22492
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
22291
22493
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
22292
22494
  if (!proj) return;
@@ -22469,7 +22671,7 @@ function registerRoutes(scope, ctx) {
22469
22671
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
22470
22672
  let violations = await log.readAll();
22471
22673
  if (req.query.severity) {
22472
- const sev = import_types97.PolicySeveritySchema.safeParse(req.query.severity);
22674
+ const sev = import_types98.PolicySeveritySchema.safeParse(req.query.severity);
22473
22675
  if (!sev.success) {
22474
22676
  return reply.code(400).send({
22475
22677
  error: "invalid severity",
@@ -22508,7 +22710,7 @@ function registerRoutes(scope, ctx) {
22508
22710
  scope.post("/policies/check", async (req, reply) => {
22509
22711
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
22510
22712
  if (!proj) return;
22511
- const parsed = import_types97.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22713
+ const parsed = import_types98.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22512
22714
  if (!parsed.success) {
22513
22715
  return reply.code(400).send({
22514
22716
  error: "invalid /policies/check body",
@@ -22857,7 +23059,7 @@ function unroutedErrorsPath(neatHome3) {
22857
23059
  }
22858
23060
 
22859
23061
  // src/daemon.ts
22860
- var import_types98 = require("@neat.is/types");
23062
+ var import_types99 = require("@neat.is/types");
22861
23063
  function daemonJsonPath(scanPath) {
22862
23064
  return import_node_path75.default.join(scanPath, "neat-out", "daemon.json");
22863
23065
  }
@@ -22982,7 +23184,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
22982
23184
  if (!serviceName) return true;
22983
23185
  if (serviceNameMatchesProject(serviceName, project)) return true;
22984
23186
  return graph.someNode(
22985
- (_id, attrs) => attrs.type === import_types98.NodeType.ServiceNode && attrs.name === serviceName
23187
+ (_id, attrs) => attrs.type === import_types99.NodeType.ServiceNode && attrs.name === serviceName
22986
23188
  );
22987
23189
  }
22988
23190
  async function bootstrapProject(entry, connectors = [], neatHome3) {
@@ -23437,7 +23639,12 @@ async function startDaemon(opts = {}) {
23437
23639
  onErrorSpanSync: async (span) => {
23438
23640
  const slot = await resolveTargetSlot(span.service, span.traceId);
23439
23641
  if (!slot) return;
23440
- await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
23642
+ await makeErrorSpanWriter(
23643
+ slot.paths.errorsPath,
23644
+ slot.graph,
23645
+ slot.entry.path,
23646
+ slot.entry.name
23647
+ )(span);
23441
23648
  },
23442
23649
  // Project-scoped route (issue #367) — the URL already named the
23443
23650
  // project. Resolution is a direct slot lookup; service.name resolves
@@ -23460,7 +23667,12 @@ async function startDaemon(opts = {}) {
23460
23667
  onProjectErrorSpanSync: async (project, span) => {
23461
23668
  const slot = await resolveSlotByName(project, span.service, span.traceId);
23462
23669
  if (!slot) return;
23463
- await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
23670
+ await makeErrorSpanWriter(
23671
+ slot.paths.errorsPath,
23672
+ slot.graph,
23673
+ slot.entry.path,
23674
+ slot.entry.name
23675
+ )(span);
23464
23676
  },
23465
23677
  // #881 — 404 a project-scoped POST for a project this daemon doesn't
23466
23678
  // host, rather than accepting it and dropping the batch. `slots` covers