@neat.is/core 0.9.2-dev.20260821 → 0.9.2

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.
@@ -3096,11 +3096,11 @@ function actixAttributeRoute(attrItem, out) {
3096
3096
  if (!nameNode) return;
3097
3097
  const macro = nameNode.type === "identifier" ? nameNode.text : nameNode.type === "scoped_identifier" ? nameNode.childForFieldName("name")?.text ?? null : null;
3098
3098
  if (!macro) return;
3099
- const tokens = attr.childForFieldName("arguments");
3100
- if (!tokens || tokens.type !== "token_tree") return;
3099
+ const tokens2 = attr.childForFieldName("arguments");
3100
+ if (!tokens2 || tokens2.type !== "token_tree") return;
3101
3101
  const strings = [];
3102
- for (let i = 0; i < tokens.namedChildCount; i++) {
3103
- const s = rustStringContent(tokens.namedChild(i));
3102
+ for (let i = 0; i < tokens2.namedChildCount; i++) {
3103
+ const s = rustStringContent(tokens2.namedChild(i));
3104
3104
  if (s !== null) strings.push(s);
3105
3105
  }
3106
3106
  const pathStr = strings[0];
@@ -4992,29 +4992,29 @@ function promoteFrontierNodes(graph, opts = {}) {
4992
4992
  toPromote.push({ frontierId: id, serviceId: target });
4993
4993
  });
4994
4994
  let promoted = 0;
4995
- for (const { frontierId: frontierId2, serviceId: serviceId15 } of toPromote) {
4995
+ for (const { frontierId: frontierId2, serviceId: serviceId16 } of toPromote) {
4996
4996
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
4997
4997
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
4998
4998
  if (!gate.allowed) {
4999
4999
  continue;
5000
5000
  }
5001
5001
  }
5002
- rewireFrontierEdges(graph, frontierId2, serviceId15);
5002
+ rewireFrontierEdges(graph, frontierId2, serviceId16);
5003
5003
  graph.dropNode(frontierId2);
5004
5004
  promoted++;
5005
5005
  }
5006
5006
  return promoted;
5007
5007
  }
5008
- function rewireFrontierEdges(graph, frontierId2, serviceId15) {
5008
+ function rewireFrontierEdges(graph, frontierId2, serviceId16) {
5009
5009
  const inbound = [...graph.inboundEdges(frontierId2)];
5010
5010
  const outbound = [...graph.outboundEdges(frontierId2)];
5011
5011
  for (const edgeId of inbound) {
5012
5012
  const edge = graph.getEdgeAttributes(edgeId);
5013
- rebuildEdge(graph, edge, edge.source, serviceId15, edgeId);
5013
+ rebuildEdge(graph, edge, edge.source, serviceId16, edgeId);
5014
5014
  }
5015
5015
  for (const edgeId of outbound) {
5016
5016
  const edge = graph.getEdgeAttributes(edgeId);
5017
- rebuildEdge(graph, edge, serviceId15, edge.target, edgeId);
5017
+ rebuildEdge(graph, edge, serviceId16, edge.target, edgeId);
5018
5018
  }
5019
5019
  }
5020
5020
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -5373,7 +5373,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
5373
5373
  for (const id of walk10.path) {
5374
5374
  const owner = resolveOwningService(graph, id);
5375
5375
  if (!owner) continue;
5376
- const { id: serviceId15, svc } = owner;
5376
+ const { id: serviceId16, svc } = owner;
5377
5377
  const deps = svc.dependencies ?? {};
5378
5378
  for (const pair of candidatePairs) {
5379
5379
  const declared = deps[pair.driver];
@@ -5386,7 +5386,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
5386
5386
  );
5387
5387
  if (!result.compatible) {
5388
5388
  return {
5389
- rootCauseNode: serviceId15,
5389
+ rootCauseNode: serviceId16,
5390
5390
  rootCauseReason: result.reason ?? "incompatible driver",
5391
5391
  ...result.minDriverVersion ? {
5392
5392
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -5401,7 +5401,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
5401
5401
  for (const id of walk10.path) {
5402
5402
  const owner = resolveOwningService(graph, id);
5403
5403
  if (!owner) continue;
5404
- const { id: serviceId15, svc } = owner;
5404
+ const { id: serviceId16, svc } = owner;
5405
5405
  const deps = svc.dependencies ?? {};
5406
5406
  const serviceNodeEngine = svc.nodeEngine;
5407
5407
  for (const constraint of nodeEngineConstraints()) {
@@ -5410,7 +5410,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
5410
5410
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
5411
5411
  if (!result.compatible && result.reason) {
5412
5412
  return {
5413
- rootCauseNode: serviceId15,
5413
+ rootCauseNode: serviceId16,
5414
5414
  rootCauseReason: result.reason,
5415
5415
  ...result.requiredNodeVersion ? {
5416
5416
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -5425,7 +5425,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
5425
5425
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
5426
5426
  if (!result.compatible && result.reason) {
5427
5427
  return {
5428
- rootCauseNode: serviceId15,
5428
+ rootCauseNode: serviceId16,
5429
5429
  rootCauseReason: result.reason,
5430
5430
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
5431
5431
  };
@@ -5530,9 +5530,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
5530
5530
  function isFailingCallEdge(e) {
5531
5531
  return e.type === EdgeType6.CALLS && (e.signal?.errorCount ?? 0) > 0;
5532
5532
  }
5533
- function callSourcesForService(graph, serviceId15) {
5534
- const ids = [serviceId15];
5535
- for (const edgeId of graph.outboundEdges(serviceId15)) {
5533
+ function callSourcesForService(graph, serviceId16) {
5534
+ const ids = [serviceId16];
5535
+ for (const edgeId of graph.outboundEdges(serviceId16)) {
5536
5536
  const e = graph.getEdgeAttributes(edgeId);
5537
5537
  if (e.type !== EdgeType6.CONTAINS) continue;
5538
5538
  const tgt = graph.getNodeAttributes(e.target);
@@ -5556,9 +5556,9 @@ function failingCallDominates(e, id, curEdge, curId) {
5556
5556
  }
5557
5557
  return id < curId;
5558
5558
  }
5559
- function dominantFailingCall(graph, serviceId15, visited) {
5559
+ function dominantFailingCall(graph, serviceId16, visited) {
5560
5560
  let best = null;
5561
- for (const src of callSourcesForService(graph, serviceId15)) {
5561
+ for (const src of callSourcesForService(graph, serviceId16)) {
5562
5562
  for (const edgeId of graph.outboundEdges(src)) {
5563
5563
  const e = graph.getEdgeAttributes(edgeId);
5564
5564
  if (!isFailingCallEdge(e)) continue;
@@ -5597,9 +5597,9 @@ function staleCallDominates(e, id, curEdge, curId) {
5597
5597
  if (ev !== cv) return ev > cv;
5598
5598
  return id < curId;
5599
5599
  }
5600
- function dominantStaleCall(graph, serviceId15, visited) {
5600
+ function dominantStaleCall(graph, serviceId16, visited) {
5601
5601
  const bestByCallee = /* @__PURE__ */ new Map();
5602
- for (const src of callSourcesForService(graph, serviceId15)) {
5602
+ for (const src of callSourcesForService(graph, serviceId16)) {
5603
5603
  for (const edgeId of graph.outboundEdges(src)) {
5604
5604
  const e = graph.getEdgeAttributes(edgeId);
5605
5605
  if (e.type !== EdgeType6.CALLS) continue;
@@ -7059,9 +7059,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
7059
7059
  "StatefulSet",
7060
7060
  "DaemonSet"
7061
7061
  ]);
7062
- function addAliases(graph, serviceId15, candidates) {
7063
- if (!graph.hasNode(serviceId15)) return;
7064
- const node = graph.getNodeAttributes(serviceId15);
7062
+ function addAliases(graph, serviceId16, candidates) {
7063
+ if (!graph.hasNode(serviceId16)) return;
7064
+ const node = graph.getNodeAttributes(serviceId16);
7065
7065
  if (node.type !== NodeType16.ServiceNode) return;
7066
7066
  const set = new Set(node.aliases ?? []);
7067
7067
  for (const c of candidates) {
@@ -7071,7 +7071,7 @@ function addAliases(graph, serviceId15, candidates) {
7071
7071
  }
7072
7072
  if (set.size === 0) return;
7073
7073
  const updated = { ...node, aliases: [...set].sort() };
7074
- graph.replaceNodeAttributes(serviceId15, updated);
7074
+ graph.replaceNodeAttributes(serviceId16, updated);
7075
7075
  }
7076
7076
  function indexServicesByName(services) {
7077
7077
  const map = /* @__PURE__ */ new Map();
@@ -7104,12 +7104,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
7104
7104
  }
7105
7105
  if (!compose?.services) return;
7106
7106
  for (const [composeName, svc] of Object.entries(compose.services)) {
7107
- const serviceId15 = serviceIndex.get(composeName);
7108
- if (!serviceId15) continue;
7107
+ const serviceId16 = serviceIndex.get(composeName);
7108
+ if (!serviceId16) continue;
7109
7109
  const aliases = /* @__PURE__ */ new Set([composeName]);
7110
7110
  if (svc.container_name) aliases.add(svc.container_name);
7111
7111
  if (svc.hostname) aliases.add(svc.hostname);
7112
- addAliases(graph, serviceId15, aliases);
7112
+ addAliases(graph, serviceId16, aliases);
7113
7113
  }
7114
7114
  }
7115
7115
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -16331,6 +16331,638 @@ function queryLogEntries(opts) {
16331
16331
  return merged;
16332
16332
  }
16333
16333
 
16334
+ // src/ask.ts
16335
+ import { AskResultSchema, NodeType as NodeType32, Provenance as Provenance26, serviceId as serviceId14 } from "@neat.is/types";
16336
+ var DEFAULT_MAX_NODES = 3;
16337
+ var MAX_FACTS_PER_SECTION = 6;
16338
+ var INTENT_RULES = [
16339
+ {
16340
+ intent: "root-cause",
16341
+ test: /\b(why|root[\s-]?cause|failing|fail(?:s|ed)?|breaking|broke(?:n)?|crash(?:ing|ed)?|throw(?:ing|s)?|culprit|5\d\d)\b/
16342
+ },
16343
+ {
16344
+ intent: "blast-radius",
16345
+ test: /\b(blast|break[\s-]?if|breaks[\s-]?if|impact|downstream|dependents?|redeploy|who\s+(?:uses|calls|depends)|what\s+depends\s+on|affect(?:s|ed)?)\b/
16346
+ },
16347
+ {
16348
+ intent: "divergence",
16349
+ test: /\b(diverg\w*|weird|mismatch|drift|out\s+of\s+sync|inconsistent|disagree\w*|declared\s+vs|anything\s+wrong)\b/
16350
+ },
16351
+ {
16352
+ intent: "incidents",
16353
+ test: /\b(incidents?|recent\s+(?:errors?|failures?)|error\s+history|failure\s+history)\b/
16354
+ },
16355
+ {
16356
+ intent: "observed",
16357
+ test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually\s+call\w*|really\s+call\w*|observed|runtime\s+traffic)\b/
16358
+ },
16359
+ {
16360
+ intent: "dependencies",
16361
+ test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?)\b/
16362
+ }
16363
+ ];
16364
+ function classifyIntent(question) {
16365
+ const q = question.toLowerCase();
16366
+ for (const rule of INTENT_RULES) {
16367
+ if (rule.test.test(q)) return rule.intent;
16368
+ }
16369
+ return "overview";
16370
+ }
16371
+ var STOPWORDS = /* @__PURE__ */ new Set([
16372
+ "the",
16373
+ "a",
16374
+ "an",
16375
+ "is",
16376
+ "are",
16377
+ "was",
16378
+ "were",
16379
+ "be",
16380
+ "been",
16381
+ "being",
16382
+ "of",
16383
+ "to",
16384
+ "in",
16385
+ "on",
16386
+ "at",
16387
+ "by",
16388
+ "for",
16389
+ "with",
16390
+ "and",
16391
+ "or",
16392
+ "but",
16393
+ "if",
16394
+ "as",
16395
+ "this",
16396
+ "that",
16397
+ "these",
16398
+ "those",
16399
+ "it",
16400
+ "its",
16401
+ "do",
16402
+ "does",
16403
+ "did",
16404
+ "has",
16405
+ "have",
16406
+ "had",
16407
+ "what",
16408
+ "which",
16409
+ "who",
16410
+ "whom",
16411
+ "how",
16412
+ "when",
16413
+ "where",
16414
+ "my",
16415
+ "me",
16416
+ "i",
16417
+ "we",
16418
+ "you",
16419
+ "they",
16420
+ "them",
16421
+ "from",
16422
+ "into",
16423
+ "about",
16424
+ "would",
16425
+ "will",
16426
+ "can",
16427
+ "could",
16428
+ "should",
16429
+ "anything",
16430
+ "everything",
16431
+ "something",
16432
+ "all",
16433
+ "any",
16434
+ "some",
16435
+ "no",
16436
+ "not",
16437
+ "so",
16438
+ "up",
16439
+ "out",
16440
+ "over",
16441
+ "get",
16442
+ "got",
16443
+ "show",
16444
+ "tell",
16445
+ "explain",
16446
+ "find",
16447
+ "give",
16448
+ "happen",
16449
+ "happens",
16450
+ "happening",
16451
+ "here",
16452
+ "there",
16453
+ "system",
16454
+ "code",
16455
+ "service",
16456
+ "node",
16457
+ "graph"
16458
+ ]);
16459
+ var INTENT_WORDS = /* @__PURE__ */ new Set([
16460
+ "why",
16461
+ "root",
16462
+ "cause",
16463
+ "failing",
16464
+ "fail",
16465
+ "fails",
16466
+ "failed",
16467
+ "breaking",
16468
+ "broke",
16469
+ "broken",
16470
+ "crash",
16471
+ "crashing",
16472
+ "crashed",
16473
+ "throw",
16474
+ "throwing",
16475
+ "throws",
16476
+ "culprit",
16477
+ "blast",
16478
+ "radius",
16479
+ "break",
16480
+ "breaks",
16481
+ "impact",
16482
+ "downstream",
16483
+ "dependent",
16484
+ "dependents",
16485
+ "redeploy",
16486
+ "affect",
16487
+ "affects",
16488
+ "affected",
16489
+ "diverge",
16490
+ "divergence",
16491
+ "divergences",
16492
+ "weird",
16493
+ "mismatch",
16494
+ "drift",
16495
+ "inconsistent",
16496
+ "disagree",
16497
+ "declared",
16498
+ "observed",
16499
+ "incident",
16500
+ "incidents",
16501
+ "recent",
16502
+ "errors",
16503
+ "error",
16504
+ "failures",
16505
+ "failure",
16506
+ "history",
16507
+ "runtime",
16508
+ "production",
16509
+ "prod",
16510
+ "traffic",
16511
+ "actually",
16512
+ "really",
16513
+ "depend",
16514
+ "depends",
16515
+ "dependency",
16516
+ "dependencies",
16517
+ "call",
16518
+ "calls",
16519
+ "use",
16520
+ "uses",
16521
+ "using",
16522
+ "import",
16523
+ "imports",
16524
+ "relies",
16525
+ "rely",
16526
+ "needs",
16527
+ "need",
16528
+ "sync"
16529
+ ]);
16530
+ function tokens(input) {
16531
+ const spaced = input.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
16532
+ return (spaced.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((t) => t.length >= 2);
16533
+ }
16534
+ function queryTokens(question) {
16535
+ const seen = /* @__PURE__ */ new Set();
16536
+ const out = [];
16537
+ for (const t of tokens(question)) {
16538
+ if (STOPWORDS.has(t) || INTENT_WORDS.has(t)) continue;
16539
+ if (seen.has(t)) continue;
16540
+ seen.add(t);
16541
+ out.push(t);
16542
+ }
16543
+ return out;
16544
+ }
16545
+ function nodeName(node) {
16546
+ return node.name ?? node.id;
16547
+ }
16548
+ function idBody(id) {
16549
+ const colon = id.indexOf(":");
16550
+ return colon >= 0 ? id.slice(colon + 1) : id;
16551
+ }
16552
+ var EMBED_MIN_SCORE = 0.35;
16553
+ async function resolveEntities(graph, question, searchIndex, maxNodes) {
16554
+ const qTokens = queryTokens(question);
16555
+ const normalized = question.toLowerCase();
16556
+ const best = /* @__PURE__ */ new Map();
16557
+ const consider = (cand) => {
16558
+ const cur = best.get(cand.nodeId);
16559
+ if (!cur || cand.score > cur.score) best.set(cand.nodeId, cand);
16560
+ };
16561
+ graph.forEachNode((id, attrs) => {
16562
+ const node = attrs;
16563
+ if (node.type === NodeType32.FrontierNode) return;
16564
+ const name = nodeName(node);
16565
+ const body = idBody(id);
16566
+ const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
16567
+ if (labelTokens.size === 0) return;
16568
+ let matched = 0;
16569
+ for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
16570
+ const named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
16571
+ if (matched === 0 && !named) return;
16572
+ const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
16573
+ const via = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
16574
+ const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
16575
+ consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
16576
+ });
16577
+ if (searchIndex) {
16578
+ try {
16579
+ const res = await searchIndex.search(question, 10);
16580
+ if (res.provider !== "substring") {
16581
+ for (const m of res.matches) {
16582
+ if (m.node.type === NodeType32.FrontierNode) continue;
16583
+ const already = best.get(m.node.id);
16584
+ if (!already && m.score < EMBED_MIN_SCORE) continue;
16585
+ consider({
16586
+ nodeId: m.node.id,
16587
+ label: nodeName(m.node),
16588
+ via: "embedding",
16589
+ score: Math.max(0, Math.min(1, m.score))
16590
+ });
16591
+ }
16592
+ }
16593
+ } catch {
16594
+ }
16595
+ }
16596
+ const viaRank = { id: 3, label: 2, token: 1, embedding: 0 };
16597
+ return [...best.values()].sort(
16598
+ (a, b) => b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
16599
+ ).slice(0, maxNodes).map((s) => ({ nodeId: s.nodeId, label: s.label, via: s.via, score: Number(s.score.toFixed(3)) }));
16600
+ }
16601
+ function incidentsForNode(nodeId, incidents) {
16602
+ if (!incidents || incidents.length === 0) return [];
16603
+ const svc = nodeId.replace(/^service:/, "");
16604
+ return incidents.filter((e) => e.affectedNode === nodeId || e.service === svc);
16605
+ }
16606
+ function edgeSignalNote(e) {
16607
+ const bits = [];
16608
+ if (e.signal) {
16609
+ bits.push(`${e.signal.spanCount} spans`);
16610
+ if (e.signal.errorCount > 0) bits.push(`${e.signal.errorCount} errors`);
16611
+ if (e.signal.latencyMs?.p95 !== void 0) bits.push(`p95 ${Math.round(e.signal.latencyMs.p95)}ms`);
16612
+ } else if (e.callCount !== void 0) {
16613
+ bits.push(`${e.callCount} calls`);
16614
+ }
16615
+ return bits.length ? ` (${bits.join(", ")})` : "";
16616
+ }
16617
+ function buildRootCauseSection(graph, node, incidents, now) {
16618
+ const result = getRootCause(graph, node, void 0, incidents, { now });
16619
+ if (!result) return null;
16620
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? Provenance26.OBSERVED;
16621
+ const facts = [
16622
+ {
16623
+ text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
16624
+ provenance: lastProv,
16625
+ confidence: result.confidence
16626
+ }
16627
+ ];
16628
+ const candidates = result.candidates ?? [];
16629
+ for (const c of candidates.slice(0, 3)) {
16630
+ facts.push({
16631
+ text: `Candidate ${c.node} \u2014 ${c.classification}: ${c.reason}`,
16632
+ ...c.provenance ? { provenance: c.provenance } : {},
16633
+ confidence: c.confidence
16634
+ });
16635
+ }
16636
+ if (result.traversalPath.length > 1) {
16637
+ facts.push({ text: `Traversal: ${result.traversalPath.join(" \u2190 ")}` });
16638
+ }
16639
+ if (result.fixRecommendation) {
16640
+ facts.push({ text: `Recommended fix: ${result.fixRecommendation}` });
16641
+ }
16642
+ return { heading: "Root cause (navigation)", facts: facts.slice(0, MAX_FACTS_PER_SECTION) };
16643
+ }
16644
+ function buildDependenciesSection(graph, node) {
16645
+ const result = getTransitiveDependencies(graph, node, 2);
16646
+ if (result.total === 0) return null;
16647
+ const facts = result.dependencies.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16648
+ text: `${d.nodeId} \u2014 ${d.edgeType} (distance ${d.distance})`,
16649
+ // Per-fact provenance: the transitive walk mixes EXTRACTED and OBSERVED
16650
+ // edges, so the tag is on the fact, not asserted for the whole section.
16651
+ provenance: d.provenance
16652
+ }));
16653
+ return { heading: "Dependencies", facts };
16654
+ }
16655
+ function buildObservedSection(graph, node) {
16656
+ const result = getObservedDependencies(graph, node);
16657
+ const facts = [];
16658
+ for (const e of result.dependencies.slice(0, MAX_FACTS_PER_SECTION)) {
16659
+ const via = e.source !== node ? ` via ${e.source}` : "";
16660
+ facts.push({
16661
+ text: `calls ${e.target}${via}${edgeSignalNote(e)}`,
16662
+ provenance: e.provenance,
16663
+ confidence: confidenceForEdge(e)
16664
+ });
16665
+ }
16666
+ if (facts.length === 0) {
16667
+ if (result.observed && result.inboundObservedCount > 0) {
16668
+ facts.push({
16669
+ text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
16670
+ provenance: Provenance26.OBSERVED
16671
+ });
16672
+ } else {
16673
+ return null;
16674
+ }
16675
+ }
16676
+ return { heading: "Runtime dependencies (OBSERVED)", facts };
16677
+ }
16678
+ function buildBlastSection(graph, node) {
16679
+ const result = getBlastRadius(graph, node);
16680
+ if (result.totalAffected === 0) return null;
16681
+ const facts = result.affectedNodes.slice(0, MAX_FACTS_PER_SECTION).map((n) => ({
16682
+ text: `${n.nodeId} (distance ${n.distance})`,
16683
+ provenance: n.edgeProvenance,
16684
+ confidence: n.confidence
16685
+ }));
16686
+ return {
16687
+ heading: `Blast radius \u2014 ${result.totalAffected} dependent${result.totalAffected === 1 ? "" : "s"}`,
16688
+ facts
16689
+ };
16690
+ }
16691
+ function buildIncidentsSection(node, incidents) {
16692
+ const relevant = incidentsForNode(node, incidents);
16693
+ if (relevant.length === 0) return null;
16694
+ const ordered = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 4);
16695
+ const facts = ordered.map((ev) => ({
16696
+ text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
16697
+ // ErrorEvents are observation records — OBSERVED by definition.
16698
+ provenance: Provenance26.OBSERVED
16699
+ }));
16700
+ return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
16701
+ }
16702
+ function divergenceLine(d) {
16703
+ if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16704
+ return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16705
+ }
16706
+ return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16707
+ }
16708
+ function buildDivergenceSection(graph, node) {
16709
+ const result = computeDivergences(graph, { node });
16710
+ if (result.totalAffected === 0) return null;
16711
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16712
+ text: divergenceLine(d),
16713
+ confidence: d.confidence
16714
+ // A divergence is composite by construction (EXTRACTED vs OBSERVED), so it
16715
+ // carries no single provenance — the fact leaves it unset.
16716
+ }));
16717
+ return {
16718
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16719
+ facts
16720
+ };
16721
+ }
16722
+ function buildGlobalDivergenceSection(graph) {
16723
+ const result = computeDivergences(graph);
16724
+ if (result.totalAffected === 0) {
16725
+ return {
16726
+ heading: "Divergences (EXTRACTED vs OBSERVED)",
16727
+ facts: [{ text: "None \u2014 declared code and observed runtime agree across the graph." }]
16728
+ };
16729
+ }
16730
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16731
+ text: divergenceLine(d),
16732
+ confidence: d.confidence
16733
+ // Composite by construction (EXTRACTED vs OBSERVED), so no single provenance.
16734
+ }));
16735
+ return {
16736
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16737
+ facts
16738
+ };
16739
+ }
16740
+ function buildGlobalIncidentsSection(incidents) {
16741
+ if (!incidents || incidents.length === 0) {
16742
+ return {
16743
+ heading: "Incidents across the system",
16744
+ facts: [{ text: "None recorded \u2014 the OBSERVED incident store is empty." }]
16745
+ };
16746
+ }
16747
+ const byKey = /* @__PURE__ */ new Map();
16748
+ for (const ev of incidents) {
16749
+ const key = ev.affectedNode || serviceId14(ev.service);
16750
+ const cur = byKey.get(key);
16751
+ if (!cur) {
16752
+ byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
16753
+ } else {
16754
+ cur.count += 1;
16755
+ if (ev.timestamp > cur.latest) {
16756
+ cur.latest = ev.timestamp;
16757
+ cur.sampleMsg = ev.errorMessage;
16758
+ }
16759
+ }
16760
+ }
16761
+ 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);
16762
+ const facts = rows.map((r) => ({
16763
+ text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
16764
+ provenance: Provenance26.OBSERVED
16765
+ }));
16766
+ return {
16767
+ heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
16768
+ facts
16769
+ };
16770
+ }
16771
+ function buildOverviewSections(graph, incidents) {
16772
+ const nodeByType = /* @__PURE__ */ new Map();
16773
+ const services = [];
16774
+ graph.forEachNode((_id, attrs) => {
16775
+ const node = attrs;
16776
+ nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
16777
+ if (node.type === NodeType32.ServiceNode) services.push(node.id);
16778
+ });
16779
+ const edgeByProv = /* @__PURE__ */ new Map();
16780
+ graph.forEachEdge((_id, attrs) => {
16781
+ const p = attrs.provenance;
16782
+ edgeByProv.set(p, (edgeByProv.get(p) ?? 0) + 1);
16783
+ });
16784
+ const sections = [];
16785
+ const count = (t) => nodeByType.get(t) ?? 0;
16786
+ const shapeFacts = [
16787
+ { text: `${graph.order} nodes, ${graph.size} edges` },
16788
+ {
16789
+ text: `${count(NodeType32.ServiceNode)} services, ${count(NodeType32.FileNode)} files, ${count(NodeType32.SymbolNode)} symbols, ${count(NodeType32.DatabaseNode)} databases`
16790
+ }
16791
+ ];
16792
+ for (const p of [Provenance26.EXTRACTED, Provenance26.OBSERVED, Provenance26.INFERRED, Provenance26.STALE]) {
16793
+ const n = edgeByProv.get(p) ?? 0;
16794
+ if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
16795
+ }
16796
+ sections.push({ heading: "System shape", facts: shapeFacts });
16797
+ const depCounts = services.map((s) => ({ s, n: getTransitiveDependencies(graph, s, 1).total })).filter((r) => r.n > 0).sort((a, b) => b.n - a.n || a.s.localeCompare(b.s)).slice(0, MAX_FACTS_PER_SECTION);
16798
+ if (depCounts.length > 0) {
16799
+ sections.push({
16800
+ heading: "Busiest services (by direct dependencies)",
16801
+ facts: depCounts.map((r) => ({
16802
+ text: `${r.s} \u2014 ${r.n} direct dependenc${r.n === 1 ? "y" : "ies"}`
16803
+ }))
16804
+ });
16805
+ }
16806
+ if (incidents && incidents.length > 0) {
16807
+ const incCount = /* @__PURE__ */ new Map();
16808
+ for (const ev of incidents) {
16809
+ const key = ev.affectedNode || serviceId14(ev.service);
16810
+ incCount.set(key, (incCount.get(key) ?? 0) + 1);
16811
+ }
16812
+ const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
16813
+ sections.push({
16814
+ heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
16815
+ facts: top.map(([k, n]) => ({
16816
+ text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
16817
+ provenance: Provenance26.OBSERVED
16818
+ }))
16819
+ });
16820
+ }
16821
+ const div = computeDivergences(graph);
16822
+ sections.push({
16823
+ heading: "Divergences",
16824
+ facts: [
16825
+ {
16826
+ text: div.totalAffected === 0 ? "None \u2014 declared code and observed runtime agree across the graph." : `${div.totalAffected} divergence${div.totalAffected === 1 ? "" : "s"} between declared code and observed runtime \u2014 ask "are there any divergences?" for the list.`
16827
+ }
16828
+ ]
16829
+ });
16830
+ return sections;
16831
+ }
16832
+ function buildGlobalSections(intent, graph, incidents) {
16833
+ switch (intent) {
16834
+ case "divergence":
16835
+ return [buildGlobalDivergenceSection(graph)];
16836
+ case "incidents":
16837
+ return [buildGlobalIncidentsSection(incidents)];
16838
+ case "overview":
16839
+ return buildOverviewSections(graph, incidents);
16840
+ default:
16841
+ return null;
16842
+ }
16843
+ }
16844
+ var SECTION_ORDER = {
16845
+ "root-cause": ["root-cause", "incidents", "blast", "observed", "divergence"],
16846
+ "blast-radius": ["blast", "dependencies", "observed", "incidents"],
16847
+ dependencies: ["dependencies", "observed", "blast"],
16848
+ observed: ["observed", "dependencies", "incidents"],
16849
+ incidents: ["incidents", "root-cause", "observed"],
16850
+ divergence: ["divergence", "observed", "dependencies", "incidents"],
16851
+ overview: ["dependencies", "observed", "blast", "incidents", "divergence"]
16852
+ };
16853
+ function buildSection(kind, graph, node, incidents, now) {
16854
+ switch (kind) {
16855
+ case "root-cause":
16856
+ return buildRootCauseSection(graph, node, incidents, now);
16857
+ case "dependencies":
16858
+ return buildDependenciesSection(graph, node);
16859
+ case "observed":
16860
+ return buildObservedSection(graph, node);
16861
+ case "blast":
16862
+ return buildBlastSection(graph, node);
16863
+ case "incidents":
16864
+ return buildIncidentsSection(node, incidents);
16865
+ case "divergence":
16866
+ return buildDivergenceSection(graph, node);
16867
+ }
16868
+ }
16869
+ function summarizeGlobal(intent, sections) {
16870
+ const lead = sections[0];
16871
+ switch (intent) {
16872
+ case "divergence": {
16873
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
16874
+ return none ? "No divergences across the graph \u2014 declared code and observed runtime agree." : `${lead.heading} across the graph, highest-confidence first below.`;
16875
+ }
16876
+ case "incidents": {
16877
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
16878
+ return none ? "No incidents recorded across the system \u2014 the OBSERVED incident store is empty." : `${lead.heading}, aggregated by node below.`;
16879
+ }
16880
+ case "overview":
16881
+ default:
16882
+ return `System overview across the whole graph: ${sections.length} view${sections.length === 1 ? "" : "s"} below \u2014 shape (nodes/edges by provenance), busiest services, incidents, and divergences.`;
16883
+ }
16884
+ }
16885
+ function summarize(question, intent, matched, primary, sections, scope) {
16886
+ if (scope === "global") {
16887
+ return summarizeGlobal(intent, sections);
16888
+ }
16889
+ if (!primary) {
16890
+ return `Nothing in "${question}" resolved to a node in the graph. Name a service, file, route, or table \u2014 e.g. \`ask "what does <service> depend on?"\`. For a graph-wide look, ask for an overview, divergences, or incidents.`;
16891
+ }
16892
+ const lead = sections[0];
16893
+ const others = matched.slice(1);
16894
+ const alsoNote = others.length ? ` Also matched: ${others.map((m) => m.nodeId).join(", ")}.` : "";
16895
+ let core;
16896
+ switch (intent) {
16897
+ case "root-cause": {
16898
+ const rc = sections.find((s) => s.heading === "Root cause (navigation)");
16899
+ if (rc) {
16900
+ core = rc.facts[0]?.text ?? "";
16901
+ } else if (lead) {
16902
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy, or the failure isn't recorded. Nearest context: ${lead.heading.toLowerCase()}.`;
16903
+ } else {
16904
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy.`;
16905
+ }
16906
+ break;
16907
+ }
16908
+ case "blast-radius":
16909
+ core = lead ? `${lead.heading} of ${primary}.` : `${primary} has no dependents \u2014 nothing else would break if it failed.`;
16910
+ break;
16911
+ case "dependencies":
16912
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
16913
+ break;
16914
+ case "observed":
16915
+ core = lead ? `${primary} at runtime: ${lead.facts.length} OBSERVED fact${lead.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
16916
+ break;
16917
+ case "incidents":
16918
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()}.` : `No incidents recorded against ${primary}.`;
16919
+ break;
16920
+ case "divergence":
16921
+ core = lead ? `${lead.heading} involving ${primary}.` : `No divergences involve ${primary} \u2014 declared and observed agree here.`;
16922
+ break;
16923
+ default:
16924
+ core = `${primary}: ${sections.length} view${sections.length === 1 ? "" : "s"} of its fused local context below.`;
16925
+ }
16926
+ return `${core}${alsoNote}`;
16927
+ }
16928
+ async function askGraph(graph, question, opts = {}) {
16929
+ const now = opts.now ?? Date.now();
16930
+ const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
16931
+ const intent = classifyIntent(question);
16932
+ const matched = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
16933
+ const primary = matched[0]?.nodeId;
16934
+ const sections = [];
16935
+ let scope;
16936
+ if (primary) {
16937
+ scope = "node";
16938
+ for (const kind of SECTION_ORDER[intent]) {
16939
+ const s = buildSection(kind, graph, primary, opts.incidents, now);
16940
+ if (s && s.facts.length > 0) sections.push(s);
16941
+ }
16942
+ } else {
16943
+ const global = buildGlobalSections(intent, graph, opts.incidents);
16944
+ if (global) {
16945
+ scope = "global";
16946
+ for (const s of global) if (s.facts.length > 0) sections.push(s);
16947
+ }
16948
+ }
16949
+ const answer = summarize(question, intent, matched, primary, sections, scope);
16950
+ const provSet = /* @__PURE__ */ new Set();
16951
+ for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
16952
+ const confidence = sections[0]?.facts[0]?.confidence;
16953
+ return AskResultSchema.parse({
16954
+ question,
16955
+ intent,
16956
+ matched,
16957
+ ...primary ? { primaryNode: primary } : {},
16958
+ ...scope ? { scope } : {},
16959
+ sections,
16960
+ answer,
16961
+ ...confidence !== void 0 ? { confidence } : {},
16962
+ provenance: [...provSet]
16963
+ });
16964
+ }
16965
+
16334
16966
  // src/streaming.ts
16335
16967
  var SSE_HEARTBEAT_MS = 3e4;
16336
16968
  var SSE_BACKPRESSURE_CAP = 1e3;
@@ -16713,14 +17345,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
16713
17345
  }
16714
17346
 
16715
17347
  // src/connectors/index.ts
16716
- import { NodeType as NodeType32, parseFileId as parseFileId2, Provenance as Provenance26 } from "@neat.is/types";
17348
+ import { NodeType as NodeType33, parseFileId as parseFileId2, Provenance as Provenance27 } from "@neat.is/types";
16717
17349
  var NO_ENV = "unknown";
16718
17350
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
16719
17351
  if (!graph.hasNode(targetNodeId)) return void 0;
16720
17352
  const sites = [];
16721
17353
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
16722
17354
  const edge = graph.getEdgeAttributes(edgeId);
16723
- if (edge.provenance !== Provenance26.EXTRACTED) continue;
17355
+ if (edge.provenance !== Provenance27.EXTRACTED) continue;
16724
17356
  const parsed = parseFileId2(edge.source);
16725
17357
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
16726
17358
  const site = { relPath: edge.evidence.file };
@@ -16732,7 +17364,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
16732
17364
  function routeCallSiteFor(graph, targetNodeId) {
16733
17365
  if (!graph.hasNode(targetNodeId)) return void 0;
16734
17366
  const attrs = graph.getNodeAttributes(targetNodeId);
16735
- if (attrs.type !== NodeType32.RouteNode || !attrs.path) return void 0;
17367
+ if (attrs.type !== NodeType33.RouteNode || !attrs.path) return void 0;
16736
17368
  const site = { relPath: attrs.path };
16737
17369
  if (attrs.line !== void 0) site.line = attrs.line;
16738
17370
  return site;
@@ -17437,7 +18069,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
17437
18069
  }
17438
18070
 
17439
18071
  // src/connectors/railway/index.ts
17440
- import { EdgeType as EdgeType27, NodeType as NodeType33, serviceId as serviceId14 } from "@neat.is/types";
18072
+ import { EdgeType as EdgeType27, NodeType as NodeType34, serviceId as serviceId15 } from "@neat.is/types";
17441
18073
 
17442
18074
  // src/connectors/railway/client.ts
17443
18075
  var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
@@ -17587,7 +18219,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
17587
18219
  const out = [];
17588
18220
  graph.forEachNode((_id, attrs) => {
17589
18221
  const node = attrs;
17590
- if (node.type !== NodeType33.RouteNode) return;
18222
+ if (node.type !== NodeType34.RouteNode) return;
17591
18223
  const route = attrs;
17592
18224
  if (route.service !== serviceName) return;
17593
18225
  out.push({
@@ -17696,7 +18328,7 @@ function createRailwayResolveTarget(config) {
17696
18328
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
17697
18329
  const peerName = config.serviceNameById[signal.targetName];
17698
18330
  if (!peerName) return null;
17699
- return { targetNodeId: serviceId14(peerName), serviceName, edgeType: EdgeType27.CONNECTS_TO };
18331
+ return { targetNodeId: serviceId15(peerName), serviceName, edgeType: EdgeType27.CONNECTS_TO };
17700
18332
  }
17701
18333
  return null;
17702
18334
  };
@@ -17878,7 +18510,7 @@ function mapLogEntriesToSignals(entries) {
17878
18510
  }
17879
18511
 
17880
18512
  // src/connectors/firebase/resolve.ts
17881
- import { NodeType as NodeType34, EdgeType as EdgeType28 } from "@neat.is/types";
18513
+ import { NodeType as NodeType35, EdgeType as EdgeType28 } from "@neat.is/types";
17882
18514
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
17883
18515
  switch (resourceType) {
17884
18516
  case "cloud_function":
@@ -17893,7 +18525,7 @@ function routeEntriesFor(graph, serviceName) {
17893
18525
  const entries = [];
17894
18526
  graph.forEachNode((_id, attrs) => {
17895
18527
  const node = attrs;
17896
- if (node.type !== NodeType34.RouteNode) return;
18528
+ if (node.type !== NodeType35.RouteNode) return;
17897
18529
  const route = attrs;
17898
18530
  if (route.service !== serviceName) return;
17899
18531
  entries.push({
@@ -17948,7 +18580,7 @@ function createFirebaseConnector(graph, serviceMap) {
17948
18580
  }
17949
18581
 
17950
18582
  // src/connectors/cloudflare/connector.ts
17951
- import { EdgeType as EdgeType29, NodeType as NodeType35, fileId as fileId5, infraId as infraId22 } from "@neat.is/types";
18583
+ import { EdgeType as EdgeType29, NodeType as NodeType36, fileId as fileId5, infraId as infraId22 } from "@neat.is/types";
17952
18584
 
17953
18585
  // src/connectors/cloudflare/client.ts
17954
18586
  import { randomUUID } from "crypto";
@@ -18107,7 +18739,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
18107
18739
  graph.forEachNode((id, attrs) => {
18108
18740
  if (found) return;
18109
18741
  const a = attrs;
18110
- if (a.type === NodeType35.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
18742
+ if (a.type === NodeType36.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
18111
18743
  found = id;
18112
18744
  }
18113
18745
  });
@@ -18119,7 +18751,7 @@ function findMatchingRouteNode(graph, serviceName, method, path72) {
18119
18751
  graph.forEachNode((id, attrs) => {
18120
18752
  if (found) return;
18121
18753
  const a = attrs;
18122
- if (a.type !== NodeType35.RouteNode || a.service !== serviceName) return;
18754
+ if (a.type !== NodeType36.RouteNode || a.service !== serviceName) return;
18123
18755
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
18124
18756
  const routeMethod = (a.method ?? "").toUpperCase();
18125
18757
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -18518,14 +19150,14 @@ function mapLogEntriesToSignals2(entries) {
18518
19150
  }
18519
19151
 
18520
19152
  // src/connectors/cloud-run/resolve.ts
18521
- import { EdgeType as EdgeType31, NodeType as NodeType36, infraId as infraId24 } from "@neat.is/types";
19153
+ import { EdgeType as EdgeType31, NodeType as NodeType37, infraId as infraId24 } from "@neat.is/types";
18522
19154
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
18523
19155
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
18524
19156
  let found = null;
18525
19157
  graph.forEachNode((_id, attrs) => {
18526
19158
  if (found) return;
18527
19159
  const node = attrs;
18528
- if (node.type !== NodeType36.RouteNode) return;
19160
+ if (node.type !== NodeType37.RouteNode) return;
18529
19161
  const route = attrs;
18530
19162
  if (route.service !== serviceName || !route.pathTemplate) return;
18531
19163
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -18596,7 +19228,7 @@ function createCloudRunConnector(graph, config = {}) {
18596
19228
  }
18597
19229
 
18598
19230
  // src/connectors/render/index.ts
18599
- import { EdgeType as EdgeType32, NodeType as NodeType37 } from "@neat.is/types";
19231
+ import { EdgeType as EdgeType32, NodeType as NodeType38 } from "@neat.is/types";
18600
19232
 
18601
19233
  // src/connectors/render/types.ts
18602
19234
  function readRenderToken(credentials) {
@@ -18672,7 +19304,7 @@ function buildRenderRouteIndex(graph, serviceName) {
18672
19304
  const out = [];
18673
19305
  graph.forEachNode((_id, attrs) => {
18674
19306
  const node = attrs;
18675
- if (node.type !== NodeType37.RouteNode) return;
19307
+ if (node.type !== NodeType38.RouteNode) return;
18676
19308
  const route = attrs;
18677
19309
  if (route.service !== serviceName) return;
18678
19310
  out.push({
@@ -19156,7 +19788,7 @@ function mapBuildsToSignals(builds, serviceName) {
19156
19788
  }
19157
19789
 
19158
19790
  // src/connectors/eas/resolve.ts
19159
- import { EdgeType as EdgeType34, NodeType as NodeType38, parseFileId as parseFileId3 } from "@neat.is/types";
19791
+ import { EdgeType as EdgeType34, NodeType as NodeType39, parseFileId as parseFileId3 } from "@neat.is/types";
19160
19792
  var NO_ENV2 = "unknown";
19161
19793
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
19162
19794
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -19184,7 +19816,7 @@ function findConfigNode(graph, basenames, serviceName) {
19184
19816
  graph.forEachNode((id, attrs) => {
19185
19817
  if (scoped) return;
19186
19818
  const node = attrs;
19187
- if (node.type !== NodeType38.ConfigNode) return;
19819
+ if (node.type !== NodeType39.ConfigNode) return;
19188
19820
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
19189
19821
  if (anyMatch === null) anyMatch = id;
19190
19822
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -20257,6 +20889,18 @@ function registerRoutes(scope, ctx) {
20257
20889
  matches: matches.slice(0, safeLimit)
20258
20890
  };
20259
20891
  });
20892
+ scope.get("/graph/ask", async (req, reply) => {
20893
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20894
+ if (!proj) return;
20895
+ const question = (req.query.q ?? "").trim();
20896
+ if (!question) return reply.code(400).send({ error: "query parameter `q` is required" });
20897
+ const epath = errorsPathFor(proj);
20898
+ const incidents = epath ? await readErrorEvents(epath) : [];
20899
+ return askGraph(proj.graph, question, {
20900
+ ...proj.searchIndex ? { searchIndex: proj.searchIndex } : {},
20901
+ incidents
20902
+ });
20903
+ });
20260
20904
  scope.get(
20261
20905
  "/graph/diff",
20262
20906
  async (req, reply) => {
@@ -20796,4 +21440,4 @@ export {
20796
21440
  deprovisionConnector,
20797
21441
  buildApi
20798
21442
  };
20799
- //# sourceMappingURL=chunk-RQQUI3NQ.js.map
21443
+ //# sourceMappingURL=chunk-HD5X5TWY.js.map