@neat.is/core 0.9.2-dev.20260822 → 0.9.3-dev.20260823

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([
@@ -9112,12 +9112,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
9112
9112
  const value = await interpolateEnvRefs(raw, serviceDir);
9113
9113
  if (!looksLike(value)) continue;
9114
9114
  const parsed = parse11(value);
9115
- if (parsed) out.push(parsed);
9115
+ if (parsed) out.push({ ...parsed, hostSource: "config" });
9116
9116
  }
9117
9117
  for (const lit of literals) {
9118
9118
  if (!looksLike(lit)) continue;
9119
9119
  const parsed = parse11(lit);
9120
- if (parsed) out.push(parsed);
9120
+ if (parsed) out.push({ ...parsed, hostSource: "literal" });
9121
9121
  }
9122
9122
  return out;
9123
9123
  }
@@ -9385,7 +9385,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
9385
9385
  type: EdgeType10.CONNECTS_TO,
9386
9386
  provenance: Provenance10.EXTRACTED,
9387
9387
  confidence: confidenceForExtracted7("structural"),
9388
- evidence: { file: evidenceFile }
9388
+ // Carry how the host was recovered (ADR-213) so the divergence ranker
9389
+ // can tell a real declared store from a hardcoded fault-injection /
9390
+ // flag-gated probe. Only set when the parser distinguished the two.
9391
+ evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
9389
9392
  };
9390
9393
  if (!graph.hasEdge(edge.id)) {
9391
9394
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -15195,6 +15198,53 @@ function detectColumnDrift(node) {
15195
15198
  function involvesNode(d, nodeId) {
15196
15199
  return d.source === nodeId || d.target === nodeId;
15197
15200
  }
15201
+ function datastoreEverObserved(graph, nodeId) {
15202
+ if (!graph.hasNode(nodeId)) return false;
15203
+ const n = graph.getNodeAttributes(nodeId);
15204
+ if (n.type === NodeType30.DatabaseNode) {
15205
+ const via = n.discoveredVia;
15206
+ if (via === "otel" || via === "merged") return true;
15207
+ }
15208
+ for (const edgeId of graph.inboundEdges(nodeId)) {
15209
+ const e = graph.getEdgeAttributes(edgeId);
15210
+ if (e.provenance === Provenance24.OBSERVED || e.provenance === Provenance24.STALE) return true;
15211
+ }
15212
+ return false;
15213
+ }
15214
+ function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
15215
+ for (const bucket of buckets2.values()) {
15216
+ if (bucket.type !== EdgeType25.CONNECTS_TO) continue;
15217
+ if (bucket.source !== serviceId16) continue;
15218
+ if (bucket.target === excludeTarget) continue;
15219
+ if (!graph.hasNode(bucket.target)) continue;
15220
+ const target = graph.getNodeAttributes(bucket.target);
15221
+ if (target.type !== NodeType30.DatabaseNode) continue;
15222
+ if (target.engine !== engine) continue;
15223
+ if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
15224
+ }
15225
+ return false;
15226
+ }
15227
+ var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
15228
+ function dampenDeadCodeProbes(graph, buckets2, all) {
15229
+ return all.map((d) => {
15230
+ if (d.type !== "missing-observed") return d;
15231
+ if (!d.extracted || d.edgeType !== EdgeType25.CONNECTS_TO) return d;
15232
+ if (!graph.hasNode(d.target)) return d;
15233
+ const target = graph.getNodeAttributes(d.target);
15234
+ if (target.type !== NodeType30.DatabaseNode) return d;
15235
+ if (d.extracted.evidence?.hostSource !== "literal") return d;
15236
+ if (datastoreEverObserved(graph, d.target)) return d;
15237
+ const engine = target.engine;
15238
+ if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
15239
+ const host = target.host ?? target.name;
15240
+ return {
15241
+ ...d,
15242
+ confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
15243
+ reason: `${d.source} declares a ${engine} connection to a hardcoded-literal host (${host}) that production has never observed, while it does observe another ${engine} store \u2014 this reads as a flag-gated or dead-code declaration (e.g. a fault-injection probe), not a real declared-vs-observed gap.`,
15244
+ recommendation: "Confirm this is an intentional dead alternate \u2014 a fault-injection probe or a flag-gated branch. If it is meant to run in production, check the feature flag or conditional that gates it; if not, it can be ignored or removed."
15245
+ };
15246
+ });
15247
+ }
15198
15248
  function suppressHostMismatchHalves(all) {
15199
15249
  const observedHalf = /* @__PURE__ */ new Set();
15200
15250
  const declaredHalf = /* @__PURE__ */ new Set();
@@ -15231,7 +15281,8 @@ function computeDivergences(graph, opts = {}) {
15231
15281
  }
15232
15282
  });
15233
15283
  const reconciled = suppressHostMismatchHalves(all);
15234
- let filtered = reconciled;
15284
+ const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
15285
+ let filtered = dampened;
15235
15286
  if (opts.type) {
15236
15287
  const allowed = opts.type;
15237
15288
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -16331,6 +16382,638 @@ function queryLogEntries(opts) {
16331
16382
  return merged;
16332
16383
  }
16333
16384
 
16385
+ // src/ask.ts
16386
+ import { AskResultSchema, NodeType as NodeType32, Provenance as Provenance26, serviceId as serviceId14 } from "@neat.is/types";
16387
+ var DEFAULT_MAX_NODES = 3;
16388
+ var MAX_FACTS_PER_SECTION = 6;
16389
+ var INTENT_RULES = [
16390
+ {
16391
+ intent: "root-cause",
16392
+ test: /\b(why|root[\s-]?cause|failing|fail(?:s|ed)?|breaking|broke(?:n)?|crash(?:ing|ed)?|throw(?:ing|s)?|culprit|5\d\d)\b/
16393
+ },
16394
+ {
16395
+ intent: "blast-radius",
16396
+ 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/
16397
+ },
16398
+ {
16399
+ intent: "divergence",
16400
+ test: /\b(diverg\w*|weird|mismatch|drift|out\s+of\s+sync|inconsistent|disagree\w*|declared\s+vs|anything\s+wrong)\b/
16401
+ },
16402
+ {
16403
+ intent: "incidents",
16404
+ test: /\b(incidents?|recent\s+(?:errors?|failures?)|error\s+history|failure\s+history)\b/
16405
+ },
16406
+ {
16407
+ intent: "observed",
16408
+ test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually\s+call\w*|really\s+call\w*|observed|runtime\s+traffic)\b/
16409
+ },
16410
+ {
16411
+ intent: "dependencies",
16412
+ test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?)\b/
16413
+ }
16414
+ ];
16415
+ function classifyIntent(question) {
16416
+ const q = question.toLowerCase();
16417
+ for (const rule of INTENT_RULES) {
16418
+ if (rule.test.test(q)) return rule.intent;
16419
+ }
16420
+ return "overview";
16421
+ }
16422
+ var STOPWORDS = /* @__PURE__ */ new Set([
16423
+ "the",
16424
+ "a",
16425
+ "an",
16426
+ "is",
16427
+ "are",
16428
+ "was",
16429
+ "were",
16430
+ "be",
16431
+ "been",
16432
+ "being",
16433
+ "of",
16434
+ "to",
16435
+ "in",
16436
+ "on",
16437
+ "at",
16438
+ "by",
16439
+ "for",
16440
+ "with",
16441
+ "and",
16442
+ "or",
16443
+ "but",
16444
+ "if",
16445
+ "as",
16446
+ "this",
16447
+ "that",
16448
+ "these",
16449
+ "those",
16450
+ "it",
16451
+ "its",
16452
+ "do",
16453
+ "does",
16454
+ "did",
16455
+ "has",
16456
+ "have",
16457
+ "had",
16458
+ "what",
16459
+ "which",
16460
+ "who",
16461
+ "whom",
16462
+ "how",
16463
+ "when",
16464
+ "where",
16465
+ "my",
16466
+ "me",
16467
+ "i",
16468
+ "we",
16469
+ "you",
16470
+ "they",
16471
+ "them",
16472
+ "from",
16473
+ "into",
16474
+ "about",
16475
+ "would",
16476
+ "will",
16477
+ "can",
16478
+ "could",
16479
+ "should",
16480
+ "anything",
16481
+ "everything",
16482
+ "something",
16483
+ "all",
16484
+ "any",
16485
+ "some",
16486
+ "no",
16487
+ "not",
16488
+ "so",
16489
+ "up",
16490
+ "out",
16491
+ "over",
16492
+ "get",
16493
+ "got",
16494
+ "show",
16495
+ "tell",
16496
+ "explain",
16497
+ "find",
16498
+ "give",
16499
+ "happen",
16500
+ "happens",
16501
+ "happening",
16502
+ "here",
16503
+ "there",
16504
+ "system",
16505
+ "code",
16506
+ "service",
16507
+ "node",
16508
+ "graph"
16509
+ ]);
16510
+ var INTENT_WORDS = /* @__PURE__ */ new Set([
16511
+ "why",
16512
+ "root",
16513
+ "cause",
16514
+ "failing",
16515
+ "fail",
16516
+ "fails",
16517
+ "failed",
16518
+ "breaking",
16519
+ "broke",
16520
+ "broken",
16521
+ "crash",
16522
+ "crashing",
16523
+ "crashed",
16524
+ "throw",
16525
+ "throwing",
16526
+ "throws",
16527
+ "culprit",
16528
+ "blast",
16529
+ "radius",
16530
+ "break",
16531
+ "breaks",
16532
+ "impact",
16533
+ "downstream",
16534
+ "dependent",
16535
+ "dependents",
16536
+ "redeploy",
16537
+ "affect",
16538
+ "affects",
16539
+ "affected",
16540
+ "diverge",
16541
+ "divergence",
16542
+ "divergences",
16543
+ "weird",
16544
+ "mismatch",
16545
+ "drift",
16546
+ "inconsistent",
16547
+ "disagree",
16548
+ "declared",
16549
+ "observed",
16550
+ "incident",
16551
+ "incidents",
16552
+ "recent",
16553
+ "errors",
16554
+ "error",
16555
+ "failures",
16556
+ "failure",
16557
+ "history",
16558
+ "runtime",
16559
+ "production",
16560
+ "prod",
16561
+ "traffic",
16562
+ "actually",
16563
+ "really",
16564
+ "depend",
16565
+ "depends",
16566
+ "dependency",
16567
+ "dependencies",
16568
+ "call",
16569
+ "calls",
16570
+ "use",
16571
+ "uses",
16572
+ "using",
16573
+ "import",
16574
+ "imports",
16575
+ "relies",
16576
+ "rely",
16577
+ "needs",
16578
+ "need",
16579
+ "sync"
16580
+ ]);
16581
+ function tokens(input) {
16582
+ const spaced = input.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
16583
+ return (spaced.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((t) => t.length >= 2);
16584
+ }
16585
+ function queryTokens(question) {
16586
+ const seen = /* @__PURE__ */ new Set();
16587
+ const out = [];
16588
+ for (const t of tokens(question)) {
16589
+ if (STOPWORDS.has(t) || INTENT_WORDS.has(t)) continue;
16590
+ if (seen.has(t)) continue;
16591
+ seen.add(t);
16592
+ out.push(t);
16593
+ }
16594
+ return out;
16595
+ }
16596
+ function nodeName(node) {
16597
+ return node.name ?? node.id;
16598
+ }
16599
+ function idBody(id) {
16600
+ const colon = id.indexOf(":");
16601
+ return colon >= 0 ? id.slice(colon + 1) : id;
16602
+ }
16603
+ var EMBED_MIN_SCORE = 0.35;
16604
+ async function resolveEntities(graph, question, searchIndex, maxNodes) {
16605
+ const qTokens = queryTokens(question);
16606
+ const normalized = question.toLowerCase();
16607
+ const best = /* @__PURE__ */ new Map();
16608
+ const consider = (cand) => {
16609
+ const cur = best.get(cand.nodeId);
16610
+ if (!cur || cand.score > cur.score) best.set(cand.nodeId, cand);
16611
+ };
16612
+ graph.forEachNode((id, attrs) => {
16613
+ const node = attrs;
16614
+ if (node.type === NodeType32.FrontierNode) return;
16615
+ const name = nodeName(node);
16616
+ const body = idBody(id);
16617
+ const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
16618
+ if (labelTokens.size === 0) return;
16619
+ let matched = 0;
16620
+ for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
16621
+ const named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
16622
+ if (matched === 0 && !named) return;
16623
+ const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
16624
+ const via = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
16625
+ const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
16626
+ consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
16627
+ });
16628
+ if (searchIndex) {
16629
+ try {
16630
+ const res = await searchIndex.search(question, 10);
16631
+ if (res.provider !== "substring") {
16632
+ for (const m of res.matches) {
16633
+ if (m.node.type === NodeType32.FrontierNode) continue;
16634
+ const already = best.get(m.node.id);
16635
+ if (!already && m.score < EMBED_MIN_SCORE) continue;
16636
+ consider({
16637
+ nodeId: m.node.id,
16638
+ label: nodeName(m.node),
16639
+ via: "embedding",
16640
+ score: Math.max(0, Math.min(1, m.score))
16641
+ });
16642
+ }
16643
+ }
16644
+ } catch {
16645
+ }
16646
+ }
16647
+ const viaRank = { id: 3, label: 2, token: 1, embedding: 0 };
16648
+ return [...best.values()].sort(
16649
+ (a, b) => b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
16650
+ ).slice(0, maxNodes).map((s) => ({ nodeId: s.nodeId, label: s.label, via: s.via, score: Number(s.score.toFixed(3)) }));
16651
+ }
16652
+ function incidentsForNode(nodeId, incidents) {
16653
+ if (!incidents || incidents.length === 0) return [];
16654
+ const svc = nodeId.replace(/^service:/, "");
16655
+ return incidents.filter((e) => e.affectedNode === nodeId || e.service === svc);
16656
+ }
16657
+ function edgeSignalNote(e) {
16658
+ const bits = [];
16659
+ if (e.signal) {
16660
+ bits.push(`${e.signal.spanCount} spans`);
16661
+ if (e.signal.errorCount > 0) bits.push(`${e.signal.errorCount} errors`);
16662
+ if (e.signal.latencyMs?.p95 !== void 0) bits.push(`p95 ${Math.round(e.signal.latencyMs.p95)}ms`);
16663
+ } else if (e.callCount !== void 0) {
16664
+ bits.push(`${e.callCount} calls`);
16665
+ }
16666
+ return bits.length ? ` (${bits.join(", ")})` : "";
16667
+ }
16668
+ function buildRootCauseSection(graph, node, incidents, now) {
16669
+ const result = getRootCause(graph, node, void 0, incidents, { now });
16670
+ if (!result) return null;
16671
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? Provenance26.OBSERVED;
16672
+ const facts = [
16673
+ {
16674
+ text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
16675
+ provenance: lastProv,
16676
+ confidence: result.confidence
16677
+ }
16678
+ ];
16679
+ const candidates = result.candidates ?? [];
16680
+ for (const c of candidates.slice(0, 3)) {
16681
+ facts.push({
16682
+ text: `Candidate ${c.node} \u2014 ${c.classification}: ${c.reason}`,
16683
+ ...c.provenance ? { provenance: c.provenance } : {},
16684
+ confidence: c.confidence
16685
+ });
16686
+ }
16687
+ if (result.traversalPath.length > 1) {
16688
+ facts.push({ text: `Traversal: ${result.traversalPath.join(" \u2190 ")}` });
16689
+ }
16690
+ if (result.fixRecommendation) {
16691
+ facts.push({ text: `Recommended fix: ${result.fixRecommendation}` });
16692
+ }
16693
+ return { heading: "Root cause (navigation)", facts: facts.slice(0, MAX_FACTS_PER_SECTION) };
16694
+ }
16695
+ function buildDependenciesSection(graph, node) {
16696
+ const result = getTransitiveDependencies(graph, node, 2);
16697
+ if (result.total === 0) return null;
16698
+ const facts = result.dependencies.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16699
+ text: `${d.nodeId} \u2014 ${d.edgeType} (distance ${d.distance})`,
16700
+ // Per-fact provenance: the transitive walk mixes EXTRACTED and OBSERVED
16701
+ // edges, so the tag is on the fact, not asserted for the whole section.
16702
+ provenance: d.provenance
16703
+ }));
16704
+ return { heading: "Dependencies", facts };
16705
+ }
16706
+ function buildObservedSection(graph, node) {
16707
+ const result = getObservedDependencies(graph, node);
16708
+ const facts = [];
16709
+ for (const e of result.dependencies.slice(0, MAX_FACTS_PER_SECTION)) {
16710
+ const via = e.source !== node ? ` via ${e.source}` : "";
16711
+ facts.push({
16712
+ text: `calls ${e.target}${via}${edgeSignalNote(e)}`,
16713
+ provenance: e.provenance,
16714
+ confidence: confidenceForEdge(e)
16715
+ });
16716
+ }
16717
+ if (facts.length === 0) {
16718
+ if (result.observed && result.inboundObservedCount > 0) {
16719
+ facts.push({
16720
+ text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
16721
+ provenance: Provenance26.OBSERVED
16722
+ });
16723
+ } else {
16724
+ return null;
16725
+ }
16726
+ }
16727
+ return { heading: "Runtime dependencies (OBSERVED)", facts };
16728
+ }
16729
+ function buildBlastSection(graph, node) {
16730
+ const result = getBlastRadius(graph, node);
16731
+ if (result.totalAffected === 0) return null;
16732
+ const facts = result.affectedNodes.slice(0, MAX_FACTS_PER_SECTION).map((n) => ({
16733
+ text: `${n.nodeId} (distance ${n.distance})`,
16734
+ provenance: n.edgeProvenance,
16735
+ confidence: n.confidence
16736
+ }));
16737
+ return {
16738
+ heading: `Blast radius \u2014 ${result.totalAffected} dependent${result.totalAffected === 1 ? "" : "s"}`,
16739
+ facts
16740
+ };
16741
+ }
16742
+ function buildIncidentsSection(node, incidents) {
16743
+ const relevant = incidentsForNode(node, incidents);
16744
+ if (relevant.length === 0) return null;
16745
+ const ordered = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 4);
16746
+ const facts = ordered.map((ev) => ({
16747
+ text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
16748
+ // ErrorEvents are observation records — OBSERVED by definition.
16749
+ provenance: Provenance26.OBSERVED
16750
+ }));
16751
+ return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
16752
+ }
16753
+ function divergenceLine(d) {
16754
+ if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16755
+ return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16756
+ }
16757
+ return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16758
+ }
16759
+ function buildDivergenceSection(graph, node) {
16760
+ const result = computeDivergences(graph, { node });
16761
+ if (result.totalAffected === 0) return null;
16762
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16763
+ text: divergenceLine(d),
16764
+ confidence: d.confidence
16765
+ // A divergence is composite by construction (EXTRACTED vs OBSERVED), so it
16766
+ // carries no single provenance — the fact leaves it unset.
16767
+ }));
16768
+ return {
16769
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16770
+ facts
16771
+ };
16772
+ }
16773
+ function buildGlobalDivergenceSection(graph) {
16774
+ const result = computeDivergences(graph);
16775
+ if (result.totalAffected === 0) {
16776
+ return {
16777
+ heading: "Divergences (EXTRACTED vs OBSERVED)",
16778
+ facts: [{ text: "None \u2014 declared code and observed runtime agree across the graph." }]
16779
+ };
16780
+ }
16781
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16782
+ text: divergenceLine(d),
16783
+ confidence: d.confidence
16784
+ // Composite by construction (EXTRACTED vs OBSERVED), so no single provenance.
16785
+ }));
16786
+ return {
16787
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16788
+ facts
16789
+ };
16790
+ }
16791
+ function buildGlobalIncidentsSection(incidents) {
16792
+ if (!incidents || incidents.length === 0) {
16793
+ return {
16794
+ heading: "Incidents across the system",
16795
+ facts: [{ text: "None recorded \u2014 the OBSERVED incident store is empty." }]
16796
+ };
16797
+ }
16798
+ const byKey = /* @__PURE__ */ new Map();
16799
+ for (const ev of incidents) {
16800
+ const key = ev.affectedNode || serviceId14(ev.service);
16801
+ const cur = byKey.get(key);
16802
+ if (!cur) {
16803
+ byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
16804
+ } else {
16805
+ cur.count += 1;
16806
+ if (ev.timestamp > cur.latest) {
16807
+ cur.latest = ev.timestamp;
16808
+ cur.sampleMsg = ev.errorMessage;
16809
+ }
16810
+ }
16811
+ }
16812
+ 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);
16813
+ const facts = rows.map((r) => ({
16814
+ text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
16815
+ provenance: Provenance26.OBSERVED
16816
+ }));
16817
+ return {
16818
+ heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
16819
+ facts
16820
+ };
16821
+ }
16822
+ function buildOverviewSections(graph, incidents) {
16823
+ const nodeByType = /* @__PURE__ */ new Map();
16824
+ const services = [];
16825
+ graph.forEachNode((_id, attrs) => {
16826
+ const node = attrs;
16827
+ nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
16828
+ if (node.type === NodeType32.ServiceNode) services.push(node.id);
16829
+ });
16830
+ const edgeByProv = /* @__PURE__ */ new Map();
16831
+ graph.forEachEdge((_id, attrs) => {
16832
+ const p = attrs.provenance;
16833
+ edgeByProv.set(p, (edgeByProv.get(p) ?? 0) + 1);
16834
+ });
16835
+ const sections = [];
16836
+ const count = (t) => nodeByType.get(t) ?? 0;
16837
+ const shapeFacts = [
16838
+ { text: `${graph.order} nodes, ${graph.size} edges` },
16839
+ {
16840
+ text: `${count(NodeType32.ServiceNode)} services, ${count(NodeType32.FileNode)} files, ${count(NodeType32.SymbolNode)} symbols, ${count(NodeType32.DatabaseNode)} databases`
16841
+ }
16842
+ ];
16843
+ for (const p of [Provenance26.EXTRACTED, Provenance26.OBSERVED, Provenance26.INFERRED, Provenance26.STALE]) {
16844
+ const n = edgeByProv.get(p) ?? 0;
16845
+ if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
16846
+ }
16847
+ sections.push({ heading: "System shape", facts: shapeFacts });
16848
+ 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);
16849
+ if (depCounts.length > 0) {
16850
+ sections.push({
16851
+ heading: "Busiest services (by direct dependencies)",
16852
+ facts: depCounts.map((r) => ({
16853
+ text: `${r.s} \u2014 ${r.n} direct dependenc${r.n === 1 ? "y" : "ies"}`
16854
+ }))
16855
+ });
16856
+ }
16857
+ if (incidents && incidents.length > 0) {
16858
+ const incCount = /* @__PURE__ */ new Map();
16859
+ for (const ev of incidents) {
16860
+ const key = ev.affectedNode || serviceId14(ev.service);
16861
+ incCount.set(key, (incCount.get(key) ?? 0) + 1);
16862
+ }
16863
+ const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
16864
+ sections.push({
16865
+ heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
16866
+ facts: top.map(([k, n]) => ({
16867
+ text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
16868
+ provenance: Provenance26.OBSERVED
16869
+ }))
16870
+ });
16871
+ }
16872
+ const div = computeDivergences(graph);
16873
+ sections.push({
16874
+ heading: "Divergences",
16875
+ facts: [
16876
+ {
16877
+ 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.`
16878
+ }
16879
+ ]
16880
+ });
16881
+ return sections;
16882
+ }
16883
+ function buildGlobalSections(intent, graph, incidents) {
16884
+ switch (intent) {
16885
+ case "divergence":
16886
+ return [buildGlobalDivergenceSection(graph)];
16887
+ case "incidents":
16888
+ return [buildGlobalIncidentsSection(incidents)];
16889
+ case "overview":
16890
+ return buildOverviewSections(graph, incidents);
16891
+ default:
16892
+ return null;
16893
+ }
16894
+ }
16895
+ var SECTION_ORDER = {
16896
+ "root-cause": ["root-cause", "incidents", "blast", "observed", "divergence"],
16897
+ "blast-radius": ["blast", "dependencies", "observed", "incidents"],
16898
+ dependencies: ["dependencies", "observed", "blast"],
16899
+ observed: ["observed", "dependencies", "incidents"],
16900
+ incidents: ["incidents", "root-cause", "observed"],
16901
+ divergence: ["divergence", "observed", "dependencies", "incidents"],
16902
+ overview: ["dependencies", "observed", "blast", "incidents", "divergence"]
16903
+ };
16904
+ function buildSection(kind, graph, node, incidents, now) {
16905
+ switch (kind) {
16906
+ case "root-cause":
16907
+ return buildRootCauseSection(graph, node, incidents, now);
16908
+ case "dependencies":
16909
+ return buildDependenciesSection(graph, node);
16910
+ case "observed":
16911
+ return buildObservedSection(graph, node);
16912
+ case "blast":
16913
+ return buildBlastSection(graph, node);
16914
+ case "incidents":
16915
+ return buildIncidentsSection(node, incidents);
16916
+ case "divergence":
16917
+ return buildDivergenceSection(graph, node);
16918
+ }
16919
+ }
16920
+ function summarizeGlobal(intent, sections) {
16921
+ const lead = sections[0];
16922
+ switch (intent) {
16923
+ case "divergence": {
16924
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
16925
+ return none ? "No divergences across the graph \u2014 declared code and observed runtime agree." : `${lead.heading} across the graph, highest-confidence first below.`;
16926
+ }
16927
+ case "incidents": {
16928
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
16929
+ return none ? "No incidents recorded across the system \u2014 the OBSERVED incident store is empty." : `${lead.heading}, aggregated by node below.`;
16930
+ }
16931
+ case "overview":
16932
+ default:
16933
+ 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.`;
16934
+ }
16935
+ }
16936
+ function summarize(question, intent, matched, primary, sections, scope) {
16937
+ if (scope === "global") {
16938
+ return summarizeGlobal(intent, sections);
16939
+ }
16940
+ if (!primary) {
16941
+ 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.`;
16942
+ }
16943
+ const lead = sections[0];
16944
+ const others = matched.slice(1);
16945
+ const alsoNote = others.length ? ` Also matched: ${others.map((m) => m.nodeId).join(", ")}.` : "";
16946
+ let core;
16947
+ switch (intent) {
16948
+ case "root-cause": {
16949
+ const rc = sections.find((s) => s.heading === "Root cause (navigation)");
16950
+ if (rc) {
16951
+ core = rc.facts[0]?.text ?? "";
16952
+ } else if (lead) {
16953
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy, or the failure isn't recorded. Nearest context: ${lead.heading.toLowerCase()}.`;
16954
+ } else {
16955
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy.`;
16956
+ }
16957
+ break;
16958
+ }
16959
+ case "blast-radius":
16960
+ core = lead ? `${lead.heading} of ${primary}.` : `${primary} has no dependents \u2014 nothing else would break if it failed.`;
16961
+ break;
16962
+ case "dependencies":
16963
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
16964
+ break;
16965
+ case "observed":
16966
+ core = lead ? `${primary} at runtime: ${lead.facts.length} OBSERVED fact${lead.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
16967
+ break;
16968
+ case "incidents":
16969
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()}.` : `No incidents recorded against ${primary}.`;
16970
+ break;
16971
+ case "divergence":
16972
+ core = lead ? `${lead.heading} involving ${primary}.` : `No divergences involve ${primary} \u2014 declared and observed agree here.`;
16973
+ break;
16974
+ default:
16975
+ core = `${primary}: ${sections.length} view${sections.length === 1 ? "" : "s"} of its fused local context below.`;
16976
+ }
16977
+ return `${core}${alsoNote}`;
16978
+ }
16979
+ async function askGraph(graph, question, opts = {}) {
16980
+ const now = opts.now ?? Date.now();
16981
+ const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
16982
+ const intent = classifyIntent(question);
16983
+ const matched = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
16984
+ const primary = matched[0]?.nodeId;
16985
+ const sections = [];
16986
+ let scope;
16987
+ if (primary) {
16988
+ scope = "node";
16989
+ for (const kind of SECTION_ORDER[intent]) {
16990
+ const s = buildSection(kind, graph, primary, opts.incidents, now);
16991
+ if (s && s.facts.length > 0) sections.push(s);
16992
+ }
16993
+ } else {
16994
+ const global = buildGlobalSections(intent, graph, opts.incidents);
16995
+ if (global) {
16996
+ scope = "global";
16997
+ for (const s of global) if (s.facts.length > 0) sections.push(s);
16998
+ }
16999
+ }
17000
+ const answer = summarize(question, intent, matched, primary, sections, scope);
17001
+ const provSet = /* @__PURE__ */ new Set();
17002
+ for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
17003
+ const confidence = sections[0]?.facts[0]?.confidence;
17004
+ return AskResultSchema.parse({
17005
+ question,
17006
+ intent,
17007
+ matched,
17008
+ ...primary ? { primaryNode: primary } : {},
17009
+ ...scope ? { scope } : {},
17010
+ sections,
17011
+ answer,
17012
+ ...confidence !== void 0 ? { confidence } : {},
17013
+ provenance: [...provSet]
17014
+ });
17015
+ }
17016
+
16334
17017
  // src/streaming.ts
16335
17018
  var SSE_HEARTBEAT_MS = 3e4;
16336
17019
  var SSE_BACKPRESSURE_CAP = 1e3;
@@ -16713,14 +17396,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
16713
17396
  }
16714
17397
 
16715
17398
  // src/connectors/index.ts
16716
- import { NodeType as NodeType32, parseFileId as parseFileId2, Provenance as Provenance26 } from "@neat.is/types";
17399
+ import { NodeType as NodeType33, parseFileId as parseFileId2, Provenance as Provenance27 } from "@neat.is/types";
16717
17400
  var NO_ENV = "unknown";
16718
17401
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
16719
17402
  if (!graph.hasNode(targetNodeId)) return void 0;
16720
17403
  const sites = [];
16721
17404
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
16722
17405
  const edge = graph.getEdgeAttributes(edgeId);
16723
- if (edge.provenance !== Provenance26.EXTRACTED) continue;
17406
+ if (edge.provenance !== Provenance27.EXTRACTED) continue;
16724
17407
  const parsed = parseFileId2(edge.source);
16725
17408
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
16726
17409
  const site = { relPath: edge.evidence.file };
@@ -16732,7 +17415,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
16732
17415
  function routeCallSiteFor(graph, targetNodeId) {
16733
17416
  if (!graph.hasNode(targetNodeId)) return void 0;
16734
17417
  const attrs = graph.getNodeAttributes(targetNodeId);
16735
- if (attrs.type !== NodeType32.RouteNode || !attrs.path) return void 0;
17418
+ if (attrs.type !== NodeType33.RouteNode || !attrs.path) return void 0;
16736
17419
  const site = { relPath: attrs.path };
16737
17420
  if (attrs.line !== void 0) site.line = attrs.line;
16738
17421
  return site;
@@ -17437,7 +18120,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
17437
18120
  }
17438
18121
 
17439
18122
  // src/connectors/railway/index.ts
17440
- import { EdgeType as EdgeType27, NodeType as NodeType33, serviceId as serviceId14 } from "@neat.is/types";
18123
+ import { EdgeType as EdgeType27, NodeType as NodeType34, serviceId as serviceId15 } from "@neat.is/types";
17441
18124
 
17442
18125
  // src/connectors/railway/client.ts
17443
18126
  var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
@@ -17587,7 +18270,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
17587
18270
  const out = [];
17588
18271
  graph.forEachNode((_id, attrs) => {
17589
18272
  const node = attrs;
17590
- if (node.type !== NodeType33.RouteNode) return;
18273
+ if (node.type !== NodeType34.RouteNode) return;
17591
18274
  const route = attrs;
17592
18275
  if (route.service !== serviceName) return;
17593
18276
  out.push({
@@ -17696,7 +18379,7 @@ function createRailwayResolveTarget(config) {
17696
18379
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
17697
18380
  const peerName = config.serviceNameById[signal.targetName];
17698
18381
  if (!peerName) return null;
17699
- return { targetNodeId: serviceId14(peerName), serviceName, edgeType: EdgeType27.CONNECTS_TO };
18382
+ return { targetNodeId: serviceId15(peerName), serviceName, edgeType: EdgeType27.CONNECTS_TO };
17700
18383
  }
17701
18384
  return null;
17702
18385
  };
@@ -17878,7 +18561,7 @@ function mapLogEntriesToSignals(entries) {
17878
18561
  }
17879
18562
 
17880
18563
  // src/connectors/firebase/resolve.ts
17881
- import { NodeType as NodeType34, EdgeType as EdgeType28 } from "@neat.is/types";
18564
+ import { NodeType as NodeType35, EdgeType as EdgeType28 } from "@neat.is/types";
17882
18565
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
17883
18566
  switch (resourceType) {
17884
18567
  case "cloud_function":
@@ -17893,7 +18576,7 @@ function routeEntriesFor(graph, serviceName) {
17893
18576
  const entries = [];
17894
18577
  graph.forEachNode((_id, attrs) => {
17895
18578
  const node = attrs;
17896
- if (node.type !== NodeType34.RouteNode) return;
18579
+ if (node.type !== NodeType35.RouteNode) return;
17897
18580
  const route = attrs;
17898
18581
  if (route.service !== serviceName) return;
17899
18582
  entries.push({
@@ -17948,7 +18631,7 @@ function createFirebaseConnector(graph, serviceMap) {
17948
18631
  }
17949
18632
 
17950
18633
  // src/connectors/cloudflare/connector.ts
17951
- import { EdgeType as EdgeType29, NodeType as NodeType35, fileId as fileId5, infraId as infraId22 } from "@neat.is/types";
18634
+ import { EdgeType as EdgeType29, NodeType as NodeType36, fileId as fileId5, infraId as infraId22 } from "@neat.is/types";
17952
18635
 
17953
18636
  // src/connectors/cloudflare/client.ts
17954
18637
  import { randomUUID } from "crypto";
@@ -18107,7 +18790,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
18107
18790
  graph.forEachNode((id, attrs) => {
18108
18791
  if (found) return;
18109
18792
  const a = attrs;
18110
- if (a.type === NodeType35.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
18793
+ if (a.type === NodeType36.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
18111
18794
  found = id;
18112
18795
  }
18113
18796
  });
@@ -18119,7 +18802,7 @@ function findMatchingRouteNode(graph, serviceName, method, path72) {
18119
18802
  graph.forEachNode((id, attrs) => {
18120
18803
  if (found) return;
18121
18804
  const a = attrs;
18122
- if (a.type !== NodeType35.RouteNode || a.service !== serviceName) return;
18805
+ if (a.type !== NodeType36.RouteNode || a.service !== serviceName) return;
18123
18806
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
18124
18807
  const routeMethod = (a.method ?? "").toUpperCase();
18125
18808
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -18518,14 +19201,14 @@ function mapLogEntriesToSignals2(entries) {
18518
19201
  }
18519
19202
 
18520
19203
  // src/connectors/cloud-run/resolve.ts
18521
- import { EdgeType as EdgeType31, NodeType as NodeType36, infraId as infraId24 } from "@neat.is/types";
19204
+ import { EdgeType as EdgeType31, NodeType as NodeType37, infraId as infraId24 } from "@neat.is/types";
18522
19205
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
18523
19206
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
18524
19207
  let found = null;
18525
19208
  graph.forEachNode((_id, attrs) => {
18526
19209
  if (found) return;
18527
19210
  const node = attrs;
18528
- if (node.type !== NodeType36.RouteNode) return;
19211
+ if (node.type !== NodeType37.RouteNode) return;
18529
19212
  const route = attrs;
18530
19213
  if (route.service !== serviceName || !route.pathTemplate) return;
18531
19214
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -18596,7 +19279,7 @@ function createCloudRunConnector(graph, config = {}) {
18596
19279
  }
18597
19280
 
18598
19281
  // src/connectors/render/index.ts
18599
- import { EdgeType as EdgeType32, NodeType as NodeType37 } from "@neat.is/types";
19282
+ import { EdgeType as EdgeType32, NodeType as NodeType38 } from "@neat.is/types";
18600
19283
 
18601
19284
  // src/connectors/render/types.ts
18602
19285
  function readRenderToken(credentials) {
@@ -18672,7 +19355,7 @@ function buildRenderRouteIndex(graph, serviceName) {
18672
19355
  const out = [];
18673
19356
  graph.forEachNode((_id, attrs) => {
18674
19357
  const node = attrs;
18675
- if (node.type !== NodeType37.RouteNode) return;
19358
+ if (node.type !== NodeType38.RouteNode) return;
18676
19359
  const route = attrs;
18677
19360
  if (route.service !== serviceName) return;
18678
19361
  out.push({
@@ -19156,7 +19839,7 @@ function mapBuildsToSignals(builds, serviceName) {
19156
19839
  }
19157
19840
 
19158
19841
  // src/connectors/eas/resolve.ts
19159
- import { EdgeType as EdgeType34, NodeType as NodeType38, parseFileId as parseFileId3 } from "@neat.is/types";
19842
+ import { EdgeType as EdgeType34, NodeType as NodeType39, parseFileId as parseFileId3 } from "@neat.is/types";
19160
19843
  var NO_ENV2 = "unknown";
19161
19844
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
19162
19845
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -19184,7 +19867,7 @@ function findConfigNode(graph, basenames, serviceName) {
19184
19867
  graph.forEachNode((id, attrs) => {
19185
19868
  if (scoped) return;
19186
19869
  const node = attrs;
19187
- if (node.type !== NodeType38.ConfigNode) return;
19870
+ if (node.type !== NodeType39.ConfigNode) return;
19188
19871
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
19189
19872
  if (anyMatch === null) anyMatch = id;
19190
19873
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -20257,6 +20940,18 @@ function registerRoutes(scope, ctx) {
20257
20940
  matches: matches.slice(0, safeLimit)
20258
20941
  };
20259
20942
  });
20943
+ scope.get("/graph/ask", async (req, reply) => {
20944
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20945
+ if (!proj) return;
20946
+ const question = (req.query.q ?? "").trim();
20947
+ if (!question) return reply.code(400).send({ error: "query parameter `q` is required" });
20948
+ const epath = errorsPathFor(proj);
20949
+ const incidents = epath ? await readErrorEvents(epath) : [];
20950
+ return askGraph(proj.graph, question, {
20951
+ ...proj.searchIndex ? { searchIndex: proj.searchIndex } : {},
20952
+ incidents
20953
+ });
20954
+ });
20260
20955
  scope.get(
20261
20956
  "/graph/diff",
20262
20957
  async (req, reply) => {
@@ -20796,4 +21491,4 @@ export {
20796
21491
  deprovisionConnector,
20797
21492
  buildApi
20798
21493
  };
20799
- //# sourceMappingURL=chunk-RQQUI3NQ.js.map
21494
+ //# sourceMappingURL=chunk-LRYPKC3B.js.map