@neat.is/core 0.9.2-dev.20260822 → 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.
package/dist/neatd.cjs CHANGED
@@ -1355,7 +1355,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
1355
1355
  for (const id of walk10.path) {
1356
1356
  const owner = resolveOwningService(graph, id);
1357
1357
  if (!owner) continue;
1358
- const { id: serviceId15, svc } = owner;
1358
+ const { id: serviceId16, svc } = owner;
1359
1359
  const deps = svc.dependencies ?? {};
1360
1360
  for (const pair of candidatePairs) {
1361
1361
  const declared = deps[pair.driver];
@@ -1368,7 +1368,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
1368
1368
  );
1369
1369
  if (!result.compatible) {
1370
1370
  return {
1371
- rootCauseNode: serviceId15,
1371
+ rootCauseNode: serviceId16,
1372
1372
  rootCauseReason: result.reason ?? "incompatible driver",
1373
1373
  ...result.minDriverVersion ? {
1374
1374
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1383,7 +1383,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
1383
1383
  for (const id of walk10.path) {
1384
1384
  const owner = resolveOwningService(graph, id);
1385
1385
  if (!owner) continue;
1386
- const { id: serviceId15, svc } = owner;
1386
+ const { id: serviceId16, svc } = owner;
1387
1387
  const deps = svc.dependencies ?? {};
1388
1388
  const serviceNodeEngine = svc.nodeEngine;
1389
1389
  for (const constraint of nodeEngineConstraints()) {
@@ -1392,7 +1392,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
1392
1392
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1393
1393
  if (!result.compatible && result.reason) {
1394
1394
  return {
1395
- rootCauseNode: serviceId15,
1395
+ rootCauseNode: serviceId16,
1396
1396
  rootCauseReason: result.reason,
1397
1397
  ...result.requiredNodeVersion ? {
1398
1398
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1407,7 +1407,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
1407
1407
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1408
1408
  if (!result.compatible && result.reason) {
1409
1409
  return {
1410
- rootCauseNode: serviceId15,
1410
+ rootCauseNode: serviceId16,
1411
1411
  rootCauseReason: result.reason,
1412
1412
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1413
1413
  };
@@ -1512,9 +1512,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1512
1512
  function isFailingCallEdge(e) {
1513
1513
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1514
1514
  }
1515
- function callSourcesForService(graph, serviceId15) {
1516
- const ids = [serviceId15];
1517
- for (const edgeId of graph.outboundEdges(serviceId15)) {
1515
+ function callSourcesForService(graph, serviceId16) {
1516
+ const ids = [serviceId16];
1517
+ for (const edgeId of graph.outboundEdges(serviceId16)) {
1518
1518
  const e = graph.getEdgeAttributes(edgeId);
1519
1519
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1520
1520
  const tgt = graph.getNodeAttributes(e.target);
@@ -1538,9 +1538,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1538
1538
  }
1539
1539
  return id < curId;
1540
1540
  }
1541
- function dominantFailingCall(graph, serviceId15, visited) {
1541
+ function dominantFailingCall(graph, serviceId16, visited) {
1542
1542
  let best = null;
1543
- for (const src of callSourcesForService(graph, serviceId15)) {
1543
+ for (const src of callSourcesForService(graph, serviceId16)) {
1544
1544
  for (const edgeId of graph.outboundEdges(src)) {
1545
1545
  const e = graph.getEdgeAttributes(edgeId);
1546
1546
  if (!isFailingCallEdge(e)) continue;
@@ -1579,9 +1579,9 @@ function staleCallDominates(e, id, curEdge, curId) {
1579
1579
  if (ev !== cv) return ev > cv;
1580
1580
  return id < curId;
1581
1581
  }
1582
- function dominantStaleCall(graph, serviceId15, visited) {
1582
+ function dominantStaleCall(graph, serviceId16, visited) {
1583
1583
  const bestByCallee = /* @__PURE__ */ new Map();
1584
- for (const src of callSourcesForService(graph, serviceId15)) {
1584
+ for (const src of callSourcesForService(graph, serviceId16)) {
1585
1585
  for (const edgeId of graph.outboundEdges(src)) {
1586
1586
  const e = graph.getEdgeAttributes(edgeId);
1587
1587
  if (e.type !== import_types.EdgeType.CALLS) continue;
@@ -4780,11 +4780,11 @@ function actixAttributeRoute(attrItem, out) {
4780
4780
  if (!nameNode) return;
4781
4781
  const macro = nameNode.type === "identifier" ? nameNode.text : nameNode.type === "scoped_identifier" ? nameNode.childForFieldName("name")?.text ?? null : null;
4782
4782
  if (!macro) return;
4783
- const tokens = attr.childForFieldName("arguments");
4784
- if (!tokens || tokens.type !== "token_tree") return;
4783
+ const tokens2 = attr.childForFieldName("arguments");
4784
+ if (!tokens2 || tokens2.type !== "token_tree") return;
4785
4785
  const strings = [];
4786
- for (let i = 0; i < tokens.namedChildCount; i++) {
4787
- const s = rustStringContent(tokens.namedChild(i));
4786
+ for (let i = 0; i < tokens2.namedChildCount; i++) {
4787
+ const s = rustStringContent(tokens2.namedChild(i));
4788
4788
  if (s !== null) strings.push(s);
4789
4789
  }
4790
4790
  const pathStr = strings[0];
@@ -6678,29 +6678,29 @@ function promoteFrontierNodes(graph, opts = {}) {
6678
6678
  toPromote.push({ frontierId: id, serviceId: target });
6679
6679
  });
6680
6680
  let promoted = 0;
6681
- for (const { frontierId: frontierId2, serviceId: serviceId15 } of toPromote) {
6681
+ for (const { frontierId: frontierId2, serviceId: serviceId16 } of toPromote) {
6682
6682
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
6683
6683
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
6684
6684
  if (!gate.allowed) {
6685
6685
  continue;
6686
6686
  }
6687
6687
  }
6688
- rewireFrontierEdges(graph, frontierId2, serviceId15);
6688
+ rewireFrontierEdges(graph, frontierId2, serviceId16);
6689
6689
  graph.dropNode(frontierId2);
6690
6690
  promoted++;
6691
6691
  }
6692
6692
  return promoted;
6693
6693
  }
6694
- function rewireFrontierEdges(graph, frontierId2, serviceId15) {
6694
+ function rewireFrontierEdges(graph, frontierId2, serviceId16) {
6695
6695
  const inbound = [...graph.inboundEdges(frontierId2)];
6696
6696
  const outbound = [...graph.outboundEdges(frontierId2)];
6697
6697
  for (const edgeId of inbound) {
6698
6698
  const edge = graph.getEdgeAttributes(edgeId);
6699
- rebuildEdge(graph, edge, edge.source, serviceId15, edgeId);
6699
+ rebuildEdge(graph, edge, edge.source, serviceId16, edgeId);
6700
6700
  }
6701
6701
  for (const edgeId of outbound) {
6702
6702
  const edge = graph.getEdgeAttributes(edgeId);
6703
- rebuildEdge(graph, edge, serviceId15, edge.target, edgeId);
6703
+ rebuildEdge(graph, edge, serviceId16, edge.target, edgeId);
6704
6704
  }
6705
6705
  }
6706
6706
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -7812,9 +7812,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
7812
7812
  "StatefulSet",
7813
7813
  "DaemonSet"
7814
7814
  ]);
7815
- function addAliases(graph, serviceId15, candidates) {
7816
- if (!graph.hasNode(serviceId15)) return;
7817
- const node = graph.getNodeAttributes(serviceId15);
7815
+ function addAliases(graph, serviceId16, candidates) {
7816
+ if (!graph.hasNode(serviceId16)) return;
7817
+ const node = graph.getNodeAttributes(serviceId16);
7818
7818
  if (node.type !== import_types19.NodeType.ServiceNode) return;
7819
7819
  const set = new Set(node.aliases ?? []);
7820
7820
  for (const c of candidates) {
@@ -7824,7 +7824,7 @@ function addAliases(graph, serviceId15, candidates) {
7824
7824
  }
7825
7825
  if (set.size === 0) return;
7826
7826
  const updated = { ...node, aliases: [...set].sort() };
7827
- graph.replaceNodeAttributes(serviceId15, updated);
7827
+ graph.replaceNodeAttributes(serviceId16, updated);
7828
7828
  }
7829
7829
  function indexServicesByName(services) {
7830
7830
  const map = /* @__PURE__ */ new Map();
@@ -7857,12 +7857,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
7857
7857
  }
7858
7858
  if (!compose?.services) return;
7859
7859
  for (const [composeName, svc] of Object.entries(compose.services)) {
7860
- const serviceId15 = serviceIndex.get(composeName);
7861
- if (!serviceId15) continue;
7860
+ const serviceId16 = serviceIndex.get(composeName);
7861
+ if (!serviceId16) continue;
7862
7862
  const aliases = /* @__PURE__ */ new Set([composeName]);
7863
7863
  if (svc.container_name) aliases.add(svc.container_name);
7864
7864
  if (svc.hostname) aliases.add(svc.hostname);
7865
- addAliases(graph, serviceId15, aliases);
7865
+ addAliases(graph, serviceId16, aliases);
7866
7866
  }
7867
7867
  }
7868
7868
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -15851,7 +15851,7 @@ var Projects = class {
15851
15851
  init_cjs_shims();
15852
15852
  var import_fastify2 = __toESM(require("fastify"), 1);
15853
15853
  var import_cors = __toESM(require("@fastify/cors"), 1);
15854
- var import_types92 = require("@neat.is/types");
15854
+ var import_types93 = require("@neat.is/types");
15855
15855
 
15856
15856
  // src/extend/index.ts
15857
15857
  init_cjs_shims();
@@ -16574,6 +16574,639 @@ function queryLogEntries(opts) {
16574
16574
  return merged;
16575
16575
  }
16576
16576
 
16577
+ // src/ask.ts
16578
+ init_cjs_shims();
16579
+ var import_types59 = require("@neat.is/types");
16580
+ var DEFAULT_MAX_NODES = 3;
16581
+ var MAX_FACTS_PER_SECTION = 6;
16582
+ var INTENT_RULES = [
16583
+ {
16584
+ intent: "root-cause",
16585
+ test: /\b(why|root[\s-]?cause|failing|fail(?:s|ed)?|breaking|broke(?:n)?|crash(?:ing|ed)?|throw(?:ing|s)?|culprit|5\d\d)\b/
16586
+ },
16587
+ {
16588
+ intent: "blast-radius",
16589
+ 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/
16590
+ },
16591
+ {
16592
+ intent: "divergence",
16593
+ test: /\b(diverg\w*|weird|mismatch|drift|out\s+of\s+sync|inconsistent|disagree\w*|declared\s+vs|anything\s+wrong)\b/
16594
+ },
16595
+ {
16596
+ intent: "incidents",
16597
+ test: /\b(incidents?|recent\s+(?:errors?|failures?)|error\s+history|failure\s+history)\b/
16598
+ },
16599
+ {
16600
+ intent: "observed",
16601
+ test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually\s+call\w*|really\s+call\w*|observed|runtime\s+traffic)\b/
16602
+ },
16603
+ {
16604
+ intent: "dependencies",
16605
+ test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?)\b/
16606
+ }
16607
+ ];
16608
+ function classifyIntent(question) {
16609
+ const q = question.toLowerCase();
16610
+ for (const rule of INTENT_RULES) {
16611
+ if (rule.test.test(q)) return rule.intent;
16612
+ }
16613
+ return "overview";
16614
+ }
16615
+ var STOPWORDS = /* @__PURE__ */ new Set([
16616
+ "the",
16617
+ "a",
16618
+ "an",
16619
+ "is",
16620
+ "are",
16621
+ "was",
16622
+ "were",
16623
+ "be",
16624
+ "been",
16625
+ "being",
16626
+ "of",
16627
+ "to",
16628
+ "in",
16629
+ "on",
16630
+ "at",
16631
+ "by",
16632
+ "for",
16633
+ "with",
16634
+ "and",
16635
+ "or",
16636
+ "but",
16637
+ "if",
16638
+ "as",
16639
+ "this",
16640
+ "that",
16641
+ "these",
16642
+ "those",
16643
+ "it",
16644
+ "its",
16645
+ "do",
16646
+ "does",
16647
+ "did",
16648
+ "has",
16649
+ "have",
16650
+ "had",
16651
+ "what",
16652
+ "which",
16653
+ "who",
16654
+ "whom",
16655
+ "how",
16656
+ "when",
16657
+ "where",
16658
+ "my",
16659
+ "me",
16660
+ "i",
16661
+ "we",
16662
+ "you",
16663
+ "they",
16664
+ "them",
16665
+ "from",
16666
+ "into",
16667
+ "about",
16668
+ "would",
16669
+ "will",
16670
+ "can",
16671
+ "could",
16672
+ "should",
16673
+ "anything",
16674
+ "everything",
16675
+ "something",
16676
+ "all",
16677
+ "any",
16678
+ "some",
16679
+ "no",
16680
+ "not",
16681
+ "so",
16682
+ "up",
16683
+ "out",
16684
+ "over",
16685
+ "get",
16686
+ "got",
16687
+ "show",
16688
+ "tell",
16689
+ "explain",
16690
+ "find",
16691
+ "give",
16692
+ "happen",
16693
+ "happens",
16694
+ "happening",
16695
+ "here",
16696
+ "there",
16697
+ "system",
16698
+ "code",
16699
+ "service",
16700
+ "node",
16701
+ "graph"
16702
+ ]);
16703
+ var INTENT_WORDS = /* @__PURE__ */ new Set([
16704
+ "why",
16705
+ "root",
16706
+ "cause",
16707
+ "failing",
16708
+ "fail",
16709
+ "fails",
16710
+ "failed",
16711
+ "breaking",
16712
+ "broke",
16713
+ "broken",
16714
+ "crash",
16715
+ "crashing",
16716
+ "crashed",
16717
+ "throw",
16718
+ "throwing",
16719
+ "throws",
16720
+ "culprit",
16721
+ "blast",
16722
+ "radius",
16723
+ "break",
16724
+ "breaks",
16725
+ "impact",
16726
+ "downstream",
16727
+ "dependent",
16728
+ "dependents",
16729
+ "redeploy",
16730
+ "affect",
16731
+ "affects",
16732
+ "affected",
16733
+ "diverge",
16734
+ "divergence",
16735
+ "divergences",
16736
+ "weird",
16737
+ "mismatch",
16738
+ "drift",
16739
+ "inconsistent",
16740
+ "disagree",
16741
+ "declared",
16742
+ "observed",
16743
+ "incident",
16744
+ "incidents",
16745
+ "recent",
16746
+ "errors",
16747
+ "error",
16748
+ "failures",
16749
+ "failure",
16750
+ "history",
16751
+ "runtime",
16752
+ "production",
16753
+ "prod",
16754
+ "traffic",
16755
+ "actually",
16756
+ "really",
16757
+ "depend",
16758
+ "depends",
16759
+ "dependency",
16760
+ "dependencies",
16761
+ "call",
16762
+ "calls",
16763
+ "use",
16764
+ "uses",
16765
+ "using",
16766
+ "import",
16767
+ "imports",
16768
+ "relies",
16769
+ "rely",
16770
+ "needs",
16771
+ "need",
16772
+ "sync"
16773
+ ]);
16774
+ function tokens(input) {
16775
+ const spaced = input.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
16776
+ return (spaced.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((t) => t.length >= 2);
16777
+ }
16778
+ function queryTokens(question) {
16779
+ const seen = /* @__PURE__ */ new Set();
16780
+ const out = [];
16781
+ for (const t of tokens(question)) {
16782
+ if (STOPWORDS.has(t) || INTENT_WORDS.has(t)) continue;
16783
+ if (seen.has(t)) continue;
16784
+ seen.add(t);
16785
+ out.push(t);
16786
+ }
16787
+ return out;
16788
+ }
16789
+ function nodeName(node) {
16790
+ return node.name ?? node.id;
16791
+ }
16792
+ function idBody(id) {
16793
+ const colon = id.indexOf(":");
16794
+ return colon >= 0 ? id.slice(colon + 1) : id;
16795
+ }
16796
+ var EMBED_MIN_SCORE = 0.35;
16797
+ async function resolveEntities(graph, question, searchIndex, maxNodes) {
16798
+ const qTokens = queryTokens(question);
16799
+ const normalized = question.toLowerCase();
16800
+ const best = /* @__PURE__ */ new Map();
16801
+ const consider = (cand) => {
16802
+ const cur = best.get(cand.nodeId);
16803
+ if (!cur || cand.score > cur.score) best.set(cand.nodeId, cand);
16804
+ };
16805
+ graph.forEachNode((id, attrs) => {
16806
+ const node = attrs;
16807
+ if (node.type === import_types59.NodeType.FrontierNode) return;
16808
+ const name = nodeName(node);
16809
+ const body = idBody(id);
16810
+ const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
16811
+ if (labelTokens.size === 0) return;
16812
+ let matched = 0;
16813
+ for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
16814
+ const named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
16815
+ if (matched === 0 && !named) return;
16816
+ const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
16817
+ const via = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
16818
+ const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
16819
+ consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
16820
+ });
16821
+ if (searchIndex) {
16822
+ try {
16823
+ const res = await searchIndex.search(question, 10);
16824
+ if (res.provider !== "substring") {
16825
+ for (const m of res.matches) {
16826
+ if (m.node.type === import_types59.NodeType.FrontierNode) continue;
16827
+ const already = best.get(m.node.id);
16828
+ if (!already && m.score < EMBED_MIN_SCORE) continue;
16829
+ consider({
16830
+ nodeId: m.node.id,
16831
+ label: nodeName(m.node),
16832
+ via: "embedding",
16833
+ score: Math.max(0, Math.min(1, m.score))
16834
+ });
16835
+ }
16836
+ }
16837
+ } catch {
16838
+ }
16839
+ }
16840
+ const viaRank = { id: 3, label: 2, token: 1, embedding: 0 };
16841
+ return [...best.values()].sort(
16842
+ (a, b) => b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
16843
+ ).slice(0, maxNodes).map((s) => ({ nodeId: s.nodeId, label: s.label, via: s.via, score: Number(s.score.toFixed(3)) }));
16844
+ }
16845
+ function incidentsForNode(nodeId, incidents) {
16846
+ if (!incidents || incidents.length === 0) return [];
16847
+ const svc = nodeId.replace(/^service:/, "");
16848
+ return incidents.filter((e) => e.affectedNode === nodeId || e.service === svc);
16849
+ }
16850
+ function edgeSignalNote(e) {
16851
+ const bits = [];
16852
+ if (e.signal) {
16853
+ bits.push(`${e.signal.spanCount} spans`);
16854
+ if (e.signal.errorCount > 0) bits.push(`${e.signal.errorCount} errors`);
16855
+ if (e.signal.latencyMs?.p95 !== void 0) bits.push(`p95 ${Math.round(e.signal.latencyMs.p95)}ms`);
16856
+ } else if (e.callCount !== void 0) {
16857
+ bits.push(`${e.callCount} calls`);
16858
+ }
16859
+ return bits.length ? ` (${bits.join(", ")})` : "";
16860
+ }
16861
+ function buildRootCauseSection(graph, node, incidents, now) {
16862
+ const result = getRootCause(graph, node, void 0, incidents, { now });
16863
+ if (!result) return null;
16864
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? import_types59.Provenance.OBSERVED;
16865
+ const facts = [
16866
+ {
16867
+ text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
16868
+ provenance: lastProv,
16869
+ confidence: result.confidence
16870
+ }
16871
+ ];
16872
+ const candidates = result.candidates ?? [];
16873
+ for (const c of candidates.slice(0, 3)) {
16874
+ facts.push({
16875
+ text: `Candidate ${c.node} \u2014 ${c.classification}: ${c.reason}`,
16876
+ ...c.provenance ? { provenance: c.provenance } : {},
16877
+ confidence: c.confidence
16878
+ });
16879
+ }
16880
+ if (result.traversalPath.length > 1) {
16881
+ facts.push({ text: `Traversal: ${result.traversalPath.join(" \u2190 ")}` });
16882
+ }
16883
+ if (result.fixRecommendation) {
16884
+ facts.push({ text: `Recommended fix: ${result.fixRecommendation}` });
16885
+ }
16886
+ return { heading: "Root cause (navigation)", facts: facts.slice(0, MAX_FACTS_PER_SECTION) };
16887
+ }
16888
+ function buildDependenciesSection(graph, node) {
16889
+ const result = getTransitiveDependencies(graph, node, 2);
16890
+ if (result.total === 0) return null;
16891
+ const facts = result.dependencies.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16892
+ text: `${d.nodeId} \u2014 ${d.edgeType} (distance ${d.distance})`,
16893
+ // Per-fact provenance: the transitive walk mixes EXTRACTED and OBSERVED
16894
+ // edges, so the tag is on the fact, not asserted for the whole section.
16895
+ provenance: d.provenance
16896
+ }));
16897
+ return { heading: "Dependencies", facts };
16898
+ }
16899
+ function buildObservedSection(graph, node) {
16900
+ const result = getObservedDependencies(graph, node);
16901
+ const facts = [];
16902
+ for (const e of result.dependencies.slice(0, MAX_FACTS_PER_SECTION)) {
16903
+ const via = e.source !== node ? ` via ${e.source}` : "";
16904
+ facts.push({
16905
+ text: `calls ${e.target}${via}${edgeSignalNote(e)}`,
16906
+ provenance: e.provenance,
16907
+ confidence: confidenceForEdge(e)
16908
+ });
16909
+ }
16910
+ if (facts.length === 0) {
16911
+ if (result.observed && result.inboundObservedCount > 0) {
16912
+ facts.push({
16913
+ text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
16914
+ provenance: import_types59.Provenance.OBSERVED
16915
+ });
16916
+ } else {
16917
+ return null;
16918
+ }
16919
+ }
16920
+ return { heading: "Runtime dependencies (OBSERVED)", facts };
16921
+ }
16922
+ function buildBlastSection(graph, node) {
16923
+ const result = getBlastRadius(graph, node);
16924
+ if (result.totalAffected === 0) return null;
16925
+ const facts = result.affectedNodes.slice(0, MAX_FACTS_PER_SECTION).map((n) => ({
16926
+ text: `${n.nodeId} (distance ${n.distance})`,
16927
+ provenance: n.edgeProvenance,
16928
+ confidence: n.confidence
16929
+ }));
16930
+ return {
16931
+ heading: `Blast radius \u2014 ${result.totalAffected} dependent${result.totalAffected === 1 ? "" : "s"}`,
16932
+ facts
16933
+ };
16934
+ }
16935
+ function buildIncidentsSection(node, incidents) {
16936
+ const relevant = incidentsForNode(node, incidents);
16937
+ if (relevant.length === 0) return null;
16938
+ const ordered = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 4);
16939
+ const facts = ordered.map((ev) => ({
16940
+ text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
16941
+ // ErrorEvents are observation records — OBSERVED by definition.
16942
+ provenance: import_types59.Provenance.OBSERVED
16943
+ }));
16944
+ return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
16945
+ }
16946
+ function divergenceLine(d) {
16947
+ if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16948
+ return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16949
+ }
16950
+ return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16951
+ }
16952
+ function buildDivergenceSection(graph, node) {
16953
+ const result = computeDivergences(graph, { node });
16954
+ if (result.totalAffected === 0) return null;
16955
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16956
+ text: divergenceLine(d),
16957
+ confidence: d.confidence
16958
+ // A divergence is composite by construction (EXTRACTED vs OBSERVED), so it
16959
+ // carries no single provenance — the fact leaves it unset.
16960
+ }));
16961
+ return {
16962
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16963
+ facts
16964
+ };
16965
+ }
16966
+ function buildGlobalDivergenceSection(graph) {
16967
+ const result = computeDivergences(graph);
16968
+ if (result.totalAffected === 0) {
16969
+ return {
16970
+ heading: "Divergences (EXTRACTED vs OBSERVED)",
16971
+ facts: [{ text: "None \u2014 declared code and observed runtime agree across the graph." }]
16972
+ };
16973
+ }
16974
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16975
+ text: divergenceLine(d),
16976
+ confidence: d.confidence
16977
+ // Composite by construction (EXTRACTED vs OBSERVED), so no single provenance.
16978
+ }));
16979
+ return {
16980
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16981
+ facts
16982
+ };
16983
+ }
16984
+ function buildGlobalIncidentsSection(incidents) {
16985
+ if (!incidents || incidents.length === 0) {
16986
+ return {
16987
+ heading: "Incidents across the system",
16988
+ facts: [{ text: "None recorded \u2014 the OBSERVED incident store is empty." }]
16989
+ };
16990
+ }
16991
+ const byKey = /* @__PURE__ */ new Map();
16992
+ for (const ev of incidents) {
16993
+ const key = ev.affectedNode || (0, import_types59.serviceId)(ev.service);
16994
+ const cur = byKey.get(key);
16995
+ if (!cur) {
16996
+ byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
16997
+ } else {
16998
+ cur.count += 1;
16999
+ if (ev.timestamp > cur.latest) {
17000
+ cur.latest = ev.timestamp;
17001
+ cur.sampleMsg = ev.errorMessage;
17002
+ }
17003
+ }
17004
+ }
17005
+ 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);
17006
+ const facts = rows.map((r) => ({
17007
+ text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
17008
+ provenance: import_types59.Provenance.OBSERVED
17009
+ }));
17010
+ return {
17011
+ heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
17012
+ facts
17013
+ };
17014
+ }
17015
+ function buildOverviewSections(graph, incidents) {
17016
+ const nodeByType = /* @__PURE__ */ new Map();
17017
+ const services = [];
17018
+ graph.forEachNode((_id, attrs) => {
17019
+ const node = attrs;
17020
+ nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
17021
+ if (node.type === import_types59.NodeType.ServiceNode) services.push(node.id);
17022
+ });
17023
+ const edgeByProv = /* @__PURE__ */ new Map();
17024
+ graph.forEachEdge((_id, attrs) => {
17025
+ const p = attrs.provenance;
17026
+ edgeByProv.set(p, (edgeByProv.get(p) ?? 0) + 1);
17027
+ });
17028
+ const sections = [];
17029
+ const count = (t) => nodeByType.get(t) ?? 0;
17030
+ const shapeFacts = [
17031
+ { text: `${graph.order} nodes, ${graph.size} edges` },
17032
+ {
17033
+ 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`
17034
+ }
17035
+ ];
17036
+ for (const p of [import_types59.Provenance.EXTRACTED, import_types59.Provenance.OBSERVED, import_types59.Provenance.INFERRED, import_types59.Provenance.STALE]) {
17037
+ const n = edgeByProv.get(p) ?? 0;
17038
+ if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
17039
+ }
17040
+ sections.push({ heading: "System shape", facts: shapeFacts });
17041
+ 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);
17042
+ if (depCounts.length > 0) {
17043
+ sections.push({
17044
+ heading: "Busiest services (by direct dependencies)",
17045
+ facts: depCounts.map((r) => ({
17046
+ text: `${r.s} \u2014 ${r.n} direct dependenc${r.n === 1 ? "y" : "ies"}`
17047
+ }))
17048
+ });
17049
+ }
17050
+ if (incidents && incidents.length > 0) {
17051
+ const incCount = /* @__PURE__ */ new Map();
17052
+ for (const ev of incidents) {
17053
+ const key = ev.affectedNode || (0, import_types59.serviceId)(ev.service);
17054
+ incCount.set(key, (incCount.get(key) ?? 0) + 1);
17055
+ }
17056
+ const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
17057
+ sections.push({
17058
+ heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
17059
+ facts: top.map(([k, n]) => ({
17060
+ text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
17061
+ provenance: import_types59.Provenance.OBSERVED
17062
+ }))
17063
+ });
17064
+ }
17065
+ const div = computeDivergences(graph);
17066
+ sections.push({
17067
+ heading: "Divergences",
17068
+ facts: [
17069
+ {
17070
+ 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.`
17071
+ }
17072
+ ]
17073
+ });
17074
+ return sections;
17075
+ }
17076
+ function buildGlobalSections(intent, graph, incidents) {
17077
+ switch (intent) {
17078
+ case "divergence":
17079
+ return [buildGlobalDivergenceSection(graph)];
17080
+ case "incidents":
17081
+ return [buildGlobalIncidentsSection(incidents)];
17082
+ case "overview":
17083
+ return buildOverviewSections(graph, incidents);
17084
+ default:
17085
+ return null;
17086
+ }
17087
+ }
17088
+ var SECTION_ORDER = {
17089
+ "root-cause": ["root-cause", "incidents", "blast", "observed", "divergence"],
17090
+ "blast-radius": ["blast", "dependencies", "observed", "incidents"],
17091
+ dependencies: ["dependencies", "observed", "blast"],
17092
+ observed: ["observed", "dependencies", "incidents"],
17093
+ incidents: ["incidents", "root-cause", "observed"],
17094
+ divergence: ["divergence", "observed", "dependencies", "incidents"],
17095
+ overview: ["dependencies", "observed", "blast", "incidents", "divergence"]
17096
+ };
17097
+ function buildSection(kind, graph, node, incidents, now) {
17098
+ switch (kind) {
17099
+ case "root-cause":
17100
+ return buildRootCauseSection(graph, node, incidents, now);
17101
+ case "dependencies":
17102
+ return buildDependenciesSection(graph, node);
17103
+ case "observed":
17104
+ return buildObservedSection(graph, node);
17105
+ case "blast":
17106
+ return buildBlastSection(graph, node);
17107
+ case "incidents":
17108
+ return buildIncidentsSection(node, incidents);
17109
+ case "divergence":
17110
+ return buildDivergenceSection(graph, node);
17111
+ }
17112
+ }
17113
+ function summarizeGlobal(intent, sections) {
17114
+ const lead = sections[0];
17115
+ switch (intent) {
17116
+ case "divergence": {
17117
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
17118
+ return none ? "No divergences across the graph \u2014 declared code and observed runtime agree." : `${lead.heading} across the graph, highest-confidence first below.`;
17119
+ }
17120
+ case "incidents": {
17121
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
17122
+ return none ? "No incidents recorded across the system \u2014 the OBSERVED incident store is empty." : `${lead.heading}, aggregated by node below.`;
17123
+ }
17124
+ case "overview":
17125
+ default:
17126
+ 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.`;
17127
+ }
17128
+ }
17129
+ function summarize(question, intent, matched, primary, sections, scope) {
17130
+ if (scope === "global") {
17131
+ return summarizeGlobal(intent, sections);
17132
+ }
17133
+ if (!primary) {
17134
+ 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.`;
17135
+ }
17136
+ const lead = sections[0];
17137
+ const others = matched.slice(1);
17138
+ const alsoNote = others.length ? ` Also matched: ${others.map((m) => m.nodeId).join(", ")}.` : "";
17139
+ let core;
17140
+ switch (intent) {
17141
+ case "root-cause": {
17142
+ const rc = sections.find((s) => s.heading === "Root cause (navigation)");
17143
+ if (rc) {
17144
+ core = rc.facts[0]?.text ?? "";
17145
+ } else if (lead) {
17146
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy, or the failure isn't recorded. Nearest context: ${lead.heading.toLowerCase()}.`;
17147
+ } else {
17148
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy.`;
17149
+ }
17150
+ break;
17151
+ }
17152
+ case "blast-radius":
17153
+ core = lead ? `${lead.heading} of ${primary}.` : `${primary} has no dependents \u2014 nothing else would break if it failed.`;
17154
+ break;
17155
+ case "dependencies":
17156
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
17157
+ break;
17158
+ case "observed":
17159
+ core = lead ? `${primary} at runtime: ${lead.facts.length} OBSERVED fact${lead.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
17160
+ break;
17161
+ case "incidents":
17162
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()}.` : `No incidents recorded against ${primary}.`;
17163
+ break;
17164
+ case "divergence":
17165
+ core = lead ? `${lead.heading} involving ${primary}.` : `No divergences involve ${primary} \u2014 declared and observed agree here.`;
17166
+ break;
17167
+ default:
17168
+ core = `${primary}: ${sections.length} view${sections.length === 1 ? "" : "s"} of its fused local context below.`;
17169
+ }
17170
+ return `${core}${alsoNote}`;
17171
+ }
17172
+ async function askGraph(graph, question, opts = {}) {
17173
+ const now = opts.now ?? Date.now();
17174
+ const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
17175
+ const intent = classifyIntent(question);
17176
+ const matched = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
17177
+ const primary = matched[0]?.nodeId;
17178
+ const sections = [];
17179
+ let scope;
17180
+ if (primary) {
17181
+ scope = "node";
17182
+ for (const kind of SECTION_ORDER[intent]) {
17183
+ const s = buildSection(kind, graph, primary, opts.incidents, now);
17184
+ if (s && s.facts.length > 0) sections.push(s);
17185
+ }
17186
+ } else {
17187
+ const global = buildGlobalSections(intent, graph, opts.incidents);
17188
+ if (global) {
17189
+ scope = "global";
17190
+ for (const s of global) if (s.facts.length > 0) sections.push(s);
17191
+ }
17192
+ }
17193
+ const answer = summarize(question, intent, matched, primary, sections, scope);
17194
+ const provSet = /* @__PURE__ */ new Set();
17195
+ for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
17196
+ const confidence = sections[0]?.facts[0]?.confidence;
17197
+ return import_types59.AskResultSchema.parse({
17198
+ question,
17199
+ intent,
17200
+ matched,
17201
+ ...primary ? { primaryNode: primary } : {},
17202
+ ...scope ? { scope } : {},
17203
+ sections,
17204
+ answer,
17205
+ ...confidence !== void 0 ? { confidence } : {},
17206
+ provenance: [...provSet]
17207
+ });
17208
+ }
17209
+
16577
17210
  // src/diff.ts
16578
17211
  init_cjs_shims();
16579
17212
  var import_node_fs36 = require("fs");
@@ -16656,7 +17289,7 @@ init_cjs_shims();
16656
17289
  var import_node_fs37 = require("fs");
16657
17290
  var import_node_os3 = __toESM(require("os"), 1);
16658
17291
  var import_node_path72 = __toESM(require("path"), 1);
16659
- var import_types59 = require("@neat.is/types");
17292
+ var import_types60 = require("@neat.is/types");
16660
17293
  var LOCK_TIMEOUT_MS = 5e3;
16661
17294
  var LOCK_RETRY_MS = 50;
16662
17295
  function neatHome() {
@@ -16858,10 +17491,10 @@ async function readRegistry() {
16858
17491
  throw err;
16859
17492
  }
16860
17493
  const parsed = JSON.parse(raw);
16861
- return import_types59.RegistryFileSchema.parse(parsed);
17494
+ return import_types60.RegistryFileSchema.parse(parsed);
16862
17495
  }
16863
17496
  async function writeRegistry(reg) {
16864
- const validated = import_types59.RegistryFileSchema.parse(reg);
17497
+ const validated = import_types60.RegistryFileSchema.parse(reg);
16865
17498
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
16866
17499
  }
16867
17500
  async function getProject(name) {
@@ -17212,15 +17845,15 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
17212
17845
 
17213
17846
  // src/connectors/index.ts
17214
17847
  init_cjs_shims();
17215
- var import_types60 = require("@neat.is/types");
17848
+ var import_types61 = require("@neat.is/types");
17216
17849
  var NO_ENV = "unknown";
17217
17850
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
17218
17851
  if (!graph.hasNode(targetNodeId)) return void 0;
17219
17852
  const sites = [];
17220
17853
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
17221
17854
  const edge = graph.getEdgeAttributes(edgeId);
17222
- if (edge.provenance !== import_types60.Provenance.EXTRACTED) continue;
17223
- const parsed = (0, import_types60.parseFileId)(edge.source);
17855
+ if (edge.provenance !== import_types61.Provenance.EXTRACTED) continue;
17856
+ const parsed = (0, import_types61.parseFileId)(edge.source);
17224
17857
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
17225
17858
  const site = { relPath: edge.evidence.file };
17226
17859
  if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
@@ -17231,7 +17864,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
17231
17864
  function routeCallSiteFor(graph, targetNodeId) {
17232
17865
  if (!graph.hasNode(targetNodeId)) return void 0;
17233
17866
  const attrs = graph.getNodeAttributes(targetNodeId);
17234
- if (attrs.type !== import_types60.NodeType.RouteNode || !attrs.path) return void 0;
17867
+ if (attrs.type !== import_types61.NodeType.RouteNode || !attrs.path) return void 0;
17235
17868
  const site = { relPath: attrs.path };
17236
17869
  if (attrs.line !== void 0) site.line = attrs.line;
17237
17870
  return site;
@@ -17843,23 +18476,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
17843
18476
 
17844
18477
  // src/connectors/supabase/resolve.ts
17845
18478
  init_cjs_shims();
17846
- var import_types62 = require("@neat.is/types");
18479
+ var import_types63 = require("@neat.is/types");
17847
18480
  function createSupabaseResolveTarget(graph, config) {
17848
18481
  return (signal, _ctx) => {
17849
18482
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
17850
18483
  return null;
17851
18484
  }
17852
- const subResourceId = (0, import_types62.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
18485
+ const subResourceId = (0, import_types63.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
17853
18486
  if (graph.hasNode(subResourceId)) {
17854
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types62.EdgeType.CALLS };
18487
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
17855
18488
  }
17856
- const bareResourceId = (0, import_types62.infraId)(signal.targetKind, signal.targetName);
18489
+ const bareResourceId = (0, import_types63.infraId)(signal.targetKind, signal.targetName);
17857
18490
  if (graph.hasNode(bareResourceId)) {
17858
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types62.EdgeType.CALLS };
18491
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
17859
18492
  }
17860
- const projectLevelId = (0, import_types62.infraId)("supabase", config.nodeRef);
18493
+ const projectLevelId = (0, import_types63.infraId)("supabase", config.nodeRef);
17861
18494
  if (graph.hasNode(projectLevelId)) {
17862
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types62.EdgeType.CALLS };
18495
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
17863
18496
  }
17864
18497
  return null;
17865
18498
  };
@@ -17952,7 +18585,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
17952
18585
 
17953
18586
  // src/connectors/railway/index.ts
17954
18587
  init_cjs_shims();
17955
- var import_types66 = require("@neat.is/types");
18588
+ var import_types67 = require("@neat.is/types");
17956
18589
 
17957
18590
  // src/connectors/railway/client.ts
17958
18591
  init_cjs_shims();
@@ -18103,7 +18736,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
18103
18736
  const out = [];
18104
18737
  graph.forEachNode((_id, attrs) => {
18105
18738
  const node = attrs;
18106
- if (node.type !== import_types66.NodeType.RouteNode) return;
18739
+ if (node.type !== import_types67.NodeType.RouteNode) return;
18107
18740
  const route = attrs;
18108
18741
  if (route.service !== serviceName) return;
18109
18742
  out.push({
@@ -18207,12 +18840,12 @@ function createRailwayResolveTarget(config) {
18207
18840
  const serviceName = config.serviceNameById[config.serviceId];
18208
18841
  if (!serviceName) return null;
18209
18842
  if (signal.targetKind === ROUTE_TARGET_KIND) {
18210
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types66.EdgeType.CALLS };
18843
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types67.EdgeType.CALLS };
18211
18844
  }
18212
18845
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
18213
18846
  const peerName = config.serviceNameById[signal.targetName];
18214
18847
  if (!peerName) return null;
18215
- return { targetNodeId: (0, import_types66.serviceId)(peerName), serviceName, edgeType: import_types66.EdgeType.CONNECTS_TO };
18848
+ return { targetNodeId: (0, import_types67.serviceId)(peerName), serviceName, edgeType: import_types67.EdgeType.CONNECTS_TO };
18216
18849
  }
18217
18850
  return null;
18218
18851
  };
@@ -18400,7 +19033,7 @@ function mapLogEntriesToSignals(entries) {
18400
19033
 
18401
19034
  // src/connectors/firebase/resolve.ts
18402
19035
  init_cjs_shims();
18403
- var import_types67 = require("@neat.is/types");
19036
+ var import_types68 = require("@neat.is/types");
18404
19037
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
18405
19038
  switch (resourceType) {
18406
19039
  case "cloud_function":
@@ -18415,7 +19048,7 @@ function routeEntriesFor(graph, serviceName) {
18415
19048
  const entries = [];
18416
19049
  graph.forEachNode((_id, attrs) => {
18417
19050
  const node = attrs;
18418
- if (node.type !== import_types67.NodeType.RouteNode) return;
19051
+ if (node.type !== import_types68.NodeType.RouteNode) return;
18419
19052
  const route = attrs;
18420
19053
  if (route.service !== serviceName) return;
18421
19054
  entries.push({
@@ -18447,7 +19080,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
18447
19080
  return {
18448
19081
  targetNodeId: match.routeNodeId,
18449
19082
  serviceName,
18450
- edgeType: import_types67.EdgeType.CALLS
19083
+ edgeType: import_types68.EdgeType.CALLS
18451
19084
  };
18452
19085
  };
18453
19086
  }
@@ -18474,7 +19107,7 @@ init_cjs_shims();
18474
19107
 
18475
19108
  // src/connectors/cloudflare/connector.ts
18476
19109
  init_cjs_shims();
18477
- var import_types69 = require("@neat.is/types");
19110
+ var import_types70 = require("@neat.is/types");
18478
19111
 
18479
19112
  // src/connectors/cloudflare/client.ts
18480
19113
  init_cjs_shims();
@@ -18638,7 +19271,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
18638
19271
  graph.forEachNode((id, attrs) => {
18639
19272
  if (found) return;
18640
19273
  const a = attrs;
18641
- if (a.type === import_types69.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
19274
+ if (a.type === import_types70.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
18642
19275
  found = id;
18643
19276
  }
18644
19277
  });
@@ -18650,7 +19283,7 @@ function findMatchingRouteNode(graph, serviceName, method, path78) {
18650
19283
  graph.forEachNode((id, attrs) => {
18651
19284
  if (found) return;
18652
19285
  const a = attrs;
18653
- if (a.type !== import_types69.NodeType.RouteNode || a.service !== serviceName) return;
19286
+ if (a.type !== import_types70.NodeType.RouteNode || a.service !== serviceName) return;
18654
19287
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
18655
19288
  const routeMethod = (a.method ?? "").toUpperCase();
18656
19289
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -18669,11 +19302,11 @@ function createCloudflareResolveTarget(config, graph) {
18669
19302
  };
18670
19303
  const mapping = config.workers?.[scriptName];
18671
19304
  if (mapping) {
18672
- const wholeFileId = (0, import_types69.fileId)(mapping.service, mapping.entryFile);
19305
+ const wholeFileId = (0, import_types70.fileId)(mapping.service, mapping.entryFile);
18673
19306
  return {
18674
19307
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
18675
19308
  serviceName: mapping.service,
18676
- edgeType: import_types69.EdgeType.CALLS
19309
+ edgeType: import_types70.EdgeType.CALLS
18677
19310
  };
18678
19311
  }
18679
19312
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -18682,13 +19315,13 @@ function createCloudflareResolveTarget(config, graph) {
18682
19315
  return {
18683
19316
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
18684
19317
  serviceName: fileNode.service,
18685
- edgeType: import_types69.EdgeType.CALLS
19318
+ edgeType: import_types70.EdgeType.CALLS
18686
19319
  };
18687
19320
  }
18688
19321
  return {
18689
- targetNodeId: (0, import_types69.infraId)("cloudflare-worker", scriptName),
19322
+ targetNodeId: (0, import_types70.infraId)("cloudflare-worker", scriptName),
18690
19323
  serviceName: scriptName,
18691
- edgeType: import_types69.EdgeType.CALLS,
19324
+ edgeType: import_types70.EdgeType.CALLS,
18692
19325
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
18693
19326
  };
18694
19327
  };
@@ -18884,14 +19517,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
18884
19517
 
18885
19518
  // src/connectors/neon/resolve.ts
18886
19519
  init_cjs_shims();
18887
- var import_types73 = require("@neat.is/types");
19520
+ var import_types74 = require("@neat.is/types");
18888
19521
  function createNeonResolveTarget(config) {
18889
19522
  return (signal) => {
18890
19523
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
18891
19524
  return {
18892
- targetNodeId: (0, import_types73.infraId)("sql-table", signal.targetName),
19525
+ targetNodeId: (0, import_types74.infraId)("sql-table", signal.targetName),
18893
19526
  serviceName: config.serviceName,
18894
- edgeType: import_types73.EdgeType.CALLS,
19527
+ edgeType: import_types74.EdgeType.CALLS,
18895
19528
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
18896
19529
  };
18897
19530
  };
@@ -19072,14 +19705,14 @@ function mapLogEntriesToSignals2(entries) {
19072
19705
 
19073
19706
  // src/connectors/cloud-run/resolve.ts
19074
19707
  init_cjs_shims();
19075
- var import_types77 = require("@neat.is/types");
19708
+ var import_types78 = require("@neat.is/types");
19076
19709
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
19077
19710
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
19078
19711
  let found = null;
19079
19712
  graph.forEachNode((_id, attrs) => {
19080
19713
  if (found) return;
19081
19714
  const node = attrs;
19082
- if (node.type !== import_types77.NodeType.RouteNode) return;
19715
+ if (node.type !== import_types78.NodeType.RouteNode) return;
19083
19716
  const route = attrs;
19084
19717
  if (route.service !== serviceName || !route.pathTemplate) return;
19085
19718
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -19104,13 +19737,13 @@ function createCloudRunResolveTarget(graph, config) {
19104
19737
  normalizePathTemplate(path78)
19105
19738
  );
19106
19739
  if (routeNodeId) {
19107
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types77.EdgeType.CALLS };
19740
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types78.EdgeType.CALLS };
19108
19741
  }
19109
19742
  }
19110
19743
  return {
19111
- targetNodeId: (0, import_types77.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
19744
+ targetNodeId: (0, import_types78.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
19112
19745
  serviceName: mappedService ?? gcpServiceName,
19113
- edgeType: import_types77.EdgeType.CALLS,
19746
+ edgeType: import_types78.EdgeType.CALLS,
19114
19747
  ensureInfraNode: {
19115
19748
  kind: CLOUD_RUN_SERVICE_INFRA_KIND,
19116
19749
  name: gcpServiceName,
@@ -19151,7 +19784,7 @@ function createCloudRunConnector(graph, config = {}) {
19151
19784
 
19152
19785
  // src/connectors/render/index.ts
19153
19786
  init_cjs_shims();
19154
- var import_types80 = require("@neat.is/types");
19787
+ var import_types81 = require("@neat.is/types");
19155
19788
 
19156
19789
  // src/connectors/render/types.ts
19157
19790
  init_cjs_shims();
@@ -19229,7 +19862,7 @@ function buildRenderRouteIndex(graph, serviceName) {
19229
19862
  const out = [];
19230
19863
  graph.forEachNode((_id, attrs) => {
19231
19864
  const node = attrs;
19232
- if (node.type !== import_types80.NodeType.RouteNode) return;
19865
+ if (node.type !== import_types81.NodeType.RouteNode) return;
19233
19866
  const route = attrs;
19234
19867
  if (route.service !== serviceName) return;
19235
19868
  out.push({
@@ -19314,7 +19947,7 @@ function mapRenderRequestLogsToSignals(entries, routeIndex) {
19314
19947
  function createRenderResolveTarget(config) {
19315
19948
  return (signal) => {
19316
19949
  if (signal.targetKind === ROUTE_TARGET_KIND2) {
19317
- return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types80.EdgeType.CALLS };
19950
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types81.EdgeType.CALLS };
19318
19951
  }
19319
19952
  return null;
19320
19953
  };
@@ -19452,21 +20085,21 @@ function mapInsightsToSignals(rows, observedAtIso) {
19452
20085
 
19453
20086
  // src/connectors/planetscale/resolve.ts
19454
20087
  init_cjs_shims();
19455
- var import_types84 = require("@neat.is/types");
20088
+ var import_types85 = require("@neat.is/types");
19456
20089
  var PLANETSCALE_DATABASE_KIND = "planetscale-database";
19457
20090
  function createPlanetscaleResolveTarget(graph, config) {
19458
20091
  const databaseName = `${config.organization}/${config.database}`;
19459
20092
  return (signal, _ctx) => {
19460
20093
  if (signal.targetKind !== PLANETSCALE_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
19461
- const tableId = (0, import_types84.infraId)("sql-table", signal.targetName);
20094
+ const tableId = (0, import_types85.infraId)("sql-table", signal.targetName);
19462
20095
  if (graph.hasNode(tableId)) {
19463
- return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types84.EdgeType.CALLS };
20096
+ return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types85.EdgeType.CALLS };
19464
20097
  }
19465
- const providerId = (0, import_types84.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
20098
+ const providerId = (0, import_types85.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
19466
20099
  return {
19467
20100
  targetNodeId: providerId,
19468
20101
  serviceName: config.serviceName,
19469
- edgeType: import_types84.EdgeType.CALLS,
20102
+ edgeType: import_types85.EdgeType.CALLS,
19470
20103
  ensureInfraNode: { kind: PLANETSCALE_DATABASE_KIND, name: databaseName, provider: "planetscale" }
19471
20104
  };
19472
20105
  };
@@ -19731,7 +20364,7 @@ function mapBuildsToSignals(builds, serviceName) {
19731
20364
 
19732
20365
  // src/connectors/eas/resolve.ts
19733
20366
  init_cjs_shims();
19734
- var import_types89 = require("@neat.is/types");
20367
+ var import_types90 = require("@neat.is/types");
19735
20368
  var NO_ENV2 = "unknown";
19736
20369
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
19737
20370
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -19747,8 +20380,8 @@ function configBasenamesForPhase(phase) {
19747
20380
  function configNodeService(graph, configNodeId) {
19748
20381
  for (const edgeId of graph.inboundEdges(configNodeId)) {
19749
20382
  const edge = graph.getEdgeAttributes(edgeId);
19750
- if (edge.type !== import_types89.EdgeType.CONFIGURED_BY) continue;
19751
- const parsed = (0, import_types89.parseFileId)(edge.source);
20383
+ if (edge.type !== import_types90.EdgeType.CONFIGURED_BY) continue;
20384
+ const parsed = (0, import_types90.parseFileId)(edge.source);
19752
20385
  if (parsed) return parsed.service;
19753
20386
  }
19754
20387
  return null;
@@ -19759,7 +20392,7 @@ function findConfigNode(graph, basenames, serviceName) {
19759
20392
  graph.forEachNode((id, attrs) => {
19760
20393
  if (scoped) return;
19761
20394
  const node = attrs;
19762
- if (node.type !== import_types89.NodeType.ConfigNode) return;
20395
+ if (node.type !== import_types90.NodeType.ConfigNode) return;
19763
20396
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
19764
20397
  if (anyMatch === null) anyMatch = id;
19765
20398
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -19776,13 +20409,13 @@ function createEasResolveTarget(graph) {
19776
20409
  if (basenames.length > 0) {
19777
20410
  const configNodeId = findConfigNode(graph, basenames, serviceName);
19778
20411
  if (configNodeId) {
19779
- return { targetNodeId: configNodeId, serviceName, edgeType: import_types89.EdgeType.CALLS };
20412
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types90.EdgeType.CALLS };
19780
20413
  }
19781
20414
  }
19782
20415
  return {
19783
20416
  targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
19784
20417
  serviceName,
19785
- edgeType: import_types89.EdgeType.CALLS
20418
+ edgeType: import_types90.EdgeType.CALLS
19786
20419
  };
19787
20420
  };
19788
20421
  }
@@ -20507,11 +21140,11 @@ function registerRoutes(scope, ctx) {
20507
21140
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
20508
21141
  const parsed = [];
20509
21142
  for (const c of candidates) {
20510
- const r = import_types92.DivergenceTypeSchema.safeParse(c);
21143
+ const r = import_types93.DivergenceTypeSchema.safeParse(c);
20511
21144
  if (!r.success) {
20512
21145
  return reply.code(400).send({
20513
21146
  error: `unknown divergence type "${c}"`,
20514
- allowed: import_types92.DivergenceTypeSchema.options
21147
+ allowed: import_types93.DivergenceTypeSchema.options
20515
21148
  });
20516
21149
  }
20517
21150
  parsed.push(r.data);
@@ -20753,6 +21386,18 @@ function registerRoutes(scope, ctx) {
20753
21386
  matches: matches.slice(0, safeLimit)
20754
21387
  };
20755
21388
  });
21389
+ scope.get("/graph/ask", async (req2, reply) => {
21390
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
21391
+ if (!proj) return;
21392
+ const question = (req2.query.q ?? "").trim();
21393
+ if (!question) return reply.code(400).send({ error: "query parameter `q` is required" });
21394
+ const epath = errorsPathFor(proj);
21395
+ const incidents = epath ? await readErrorEvents(epath) : [];
21396
+ return askGraph(proj.graph, question, {
21397
+ ...proj.searchIndex ? { searchIndex: proj.searchIndex } : {},
21398
+ incidents
21399
+ });
21400
+ });
20756
21401
  scope.get(
20757
21402
  "/graph/diff",
20758
21403
  async (req2, reply) => {
@@ -20853,7 +21498,7 @@ function registerRoutes(scope, ctx) {
20853
21498
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
20854
21499
  let violations = await log.readAll();
20855
21500
  if (req2.query.severity) {
20856
- const sev = import_types92.PolicySeveritySchema.safeParse(req2.query.severity);
21501
+ const sev = import_types93.PolicySeveritySchema.safeParse(req2.query.severity);
20857
21502
  if (!sev.success) {
20858
21503
  return reply.code(400).send({
20859
21504
  error: "invalid severity",
@@ -20892,7 +21537,7 @@ function registerRoutes(scope, ctx) {
20892
21537
  scope.post("/policies/check", async (req2, reply) => {
20893
21538
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
20894
21539
  if (!proj) return;
20895
- const parsed = import_types92.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
21540
+ const parsed = import_types93.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
20896
21541
  if (!parsed.success) {
20897
21542
  return reply.code(400).send({
20898
21543
  error: "invalid /policies/check body",
@@ -21233,7 +21878,7 @@ function unroutedErrorsPath(neatHome4) {
21233
21878
  }
21234
21879
 
21235
21880
  // src/daemon.ts
21236
- var import_types93 = require("@neat.is/types");
21881
+ var import_types94 = require("@neat.is/types");
21237
21882
  function daemonJsonPath(scanPath) {
21238
21883
  return import_node_path75.default.join(scanPath, "neat-out", "daemon.json");
21239
21884
  }
@@ -21359,8 +22004,8 @@ function isTokenContained(needle, haystack) {
21359
22004
  const n = needle.toLowerCase();
21360
22005
  const h = haystack.toLowerCase();
21361
22006
  if (!h.includes(n)) return false;
21362
- const tokens = h.split(/[-_]/);
21363
- return tokens.includes(n);
22007
+ const tokens2 = h.split(/[-_]/);
22008
+ return tokens2.includes(n);
21364
22009
  }
21365
22010
  function serviceNameMatchesProject(serviceName, project) {
21366
22011
  if (serviceName.toLowerCase() === project.toLowerCase()) return true;
@@ -21372,7 +22017,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
21372
22017
  if (!serviceName) return true;
21373
22018
  if (serviceNameMatchesProject(serviceName, project)) return true;
21374
22019
  return graph.someNode(
21375
- (_id, attrs) => attrs.type === import_types93.NodeType.ServiceNode && attrs.name === serviceName
22020
+ (_id, attrs) => attrs.type === import_types94.NodeType.ServiceNode && attrs.name === serviceName
21376
22021
  );
21377
22022
  }
21378
22023
  async function bootstrapProject(entry2, connectors = [], neatHome4) {