@neat.is/core 0.9.21-dev.20260924 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -787,7 +787,7 @@ function getGraph(project = DEFAULT_PROJECT) {
787
787
  init_cjs_shims();
788
788
  var import_fastify2 = __toESM(require("fastify"), 1);
789
789
  var import_cors = __toESM(require("@fastify/cors"), 1);
790
- var import_types100 = require("@neat.is/types");
790
+ var import_types101 = require("@neat.is/types");
791
791
 
792
792
  // src/extend/index.ts
793
793
  init_cjs_shims();
@@ -17397,7 +17397,7 @@ var INTENT_RULES = [
17397
17397
  },
17398
17398
  {
17399
17399
  intent: "blast-radius",
17400
- 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/
17400
+ test: /\b(blast|break[\s-]?if|breaks[\s-]?if|impact|downstream|dependents?|redeploy|who\s+(?:uses|calls|depends(?:\s+on)?)|what\s+depends\s+on|consumers?\s+of|callers?\s+of|affect(?:s|ed)?)\b/
17401
17401
  },
17402
17402
  {
17403
17403
  intent: "divergence",
@@ -17409,11 +17409,11 @@ var INTENT_RULES = [
17409
17409
  },
17410
17410
  {
17411
17411
  intent: "observed",
17412
- test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually\s+call\w*|really\s+call\w*|observed|runtime\s+traffic)\b/
17412
+ test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually|really\s+call\w*|observed|runtime\s+traffic|slow|latency|p95|timing)\b/
17413
17413
  },
17414
17414
  {
17415
17415
  intent: "dependencies",
17416
- test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?)\b/
17416
+ test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?|talks?\s+to|connects?\s+to|hits?|reads?\s+from|writes?\s+to)\b/
17417
17417
  }
17418
17418
  ];
17419
17419
  function classifyIntent(question) {
@@ -17580,7 +17580,25 @@ var INTENT_WORDS = /* @__PURE__ */ new Set([
17580
17580
  "rely",
17581
17581
  "needs",
17582
17582
  "need",
17583
- "sync"
17583
+ "sync",
17584
+ "talk",
17585
+ "talks",
17586
+ "connect",
17587
+ "connects",
17588
+ "hit",
17589
+ "hits",
17590
+ "read",
17591
+ "reads",
17592
+ "write",
17593
+ "writes",
17594
+ "consumer",
17595
+ "consumers",
17596
+ "caller",
17597
+ "callers",
17598
+ "slow",
17599
+ "latency",
17600
+ "p95",
17601
+ "timing"
17584
17602
  ]);
17585
17603
  function tokens(input) {
17586
17604
  const spaced = input.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
@@ -17604,6 +17622,13 @@ function idBody(id) {
17604
17622
  const colon = id.indexOf(":");
17605
17623
  return colon >= 0 ? id.slice(colon + 1) : id;
17606
17624
  }
17625
+ var TYPE_NOUNS = [
17626
+ { nouns: ["database", "db", "table"], type: import_types61.NodeType.DatabaseNode },
17627
+ { nouns: ["service", "api", "backend"], type: import_types61.NodeType.ServiceNode },
17628
+ { nouns: ["route", "endpoint"], type: import_types61.NodeType.RouteNode },
17629
+ { nouns: ["file"], type: import_types61.NodeType.FileNode },
17630
+ { nouns: ["config"], type: import_types61.NodeType.ConfigNode }
17631
+ ];
17607
17632
  var EMBED_MIN_SCORE = 0.35;
17608
17633
  async function resolveEntities(graph, question, searchIndex, maxNodes) {
17609
17634
  const qTokens = queryTokens(question);
@@ -17611,6 +17636,7 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17611
17636
  const best = /* @__PURE__ */ new Map();
17612
17637
  const consider = (cand) => {
17613
17638
  const cur = best.get(cand.nodeId);
17639
+ if (cur?.via === "id" && cand.via === "embedding") return;
17614
17640
  if (!cur || cand.score > cur.score) best.set(cand.nodeId, cand);
17615
17641
  };
17616
17642
  graph.forEachNode((id, attrs) => {
@@ -17620,12 +17646,12 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17620
17646
  const body = idBody(id);
17621
17647
  const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
17622
17648
  if (labelTokens.size === 0) return;
17623
- let matched = 0;
17624
- for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
17649
+ let matched2 = 0;
17650
+ for (const t of qTokens) if (labelTokens.has(t)) matched2 += 1;
17625
17651
  const named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
17626
- if (matched === 0 && !named) return;
17627
- const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
17628
- const via = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
17652
+ if (matched2 === 0 && !named) return;
17653
+ const coverage = qTokens.length > 0 ? matched2 / qTokens.length : 0;
17654
+ const via = named ? "id" : matched2 > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
17629
17655
  const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
17630
17656
  consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
17631
17657
  });
@@ -17635,6 +17661,7 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17635
17661
  if (res.provider !== "substring") {
17636
17662
  for (const m of res.matches) {
17637
17663
  if (m.node.type === import_types61.NodeType.FrontierNode) continue;
17664
+ if (!graph.hasNode(m.node.id)) continue;
17638
17665
  const already = best.get(m.node.id);
17639
17666
  if (!already && m.score < EMBED_MIN_SCORE) continue;
17640
17667
  consider({
@@ -17648,10 +17675,31 @@ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17648
17675
  } catch {
17649
17676
  }
17650
17677
  }
17651
- const viaRank = { id: 3, label: 2, token: 1, embedding: 0 };
17652
- return [...best.values()].sort(
17653
- (a, b) => b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
17678
+ if (best.size === 0) {
17679
+ const words = new Set(tokens(question));
17680
+ const kind = TYPE_NOUNS.find((entry) => entry.nouns.some((noun) => words.has(noun)));
17681
+ if (kind) {
17682
+ const ids = graph.filterNodes((_id, attrs) => attrs.type === kind.type).sort();
17683
+ if (ids.length > 1) {
17684
+ return { matched: [], ambiguousType: { noun: kind.nouns.find((noun) => words.has(noun)), ids } };
17685
+ }
17686
+ if (ids.length === 1) {
17687
+ const id = ids[0];
17688
+ return { matched: [{ nodeId: id, label: nodeName(graph.getNodeAttributes(id)), via: "type", score: 0.6 }].slice(0, maxNodes) };
17689
+ }
17690
+ return { matched: [] };
17691
+ }
17692
+ }
17693
+ const viaRank = { id: 4, label: 3, token: 2, type: 1, embedding: 0 };
17694
+ const ranked = [...best.values()].sort(
17695
+ (a, b) => Number(b.via === "id") - Number(a.via === "id") || b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
17696
+ );
17697
+ const primary = ranked[0];
17698
+ const primaryType = primary && graph.getNodeAttributes(primary.nodeId).type;
17699
+ const matched = ranked.filter(
17700
+ (candidate) => !primary || primary.via !== "id" || primary.score < 0.85 || candidate === primary || graph.getNodeAttributes(candidate.nodeId).type === primaryType || candidate.via !== "embedding" && candidate.score >= primary.score - 0.2
17654
17701
  ).slice(0, maxNodes).map((s) => ({ nodeId: s.nodeId, label: s.label, via: s.via, score: Number(s.score.toFixed(3)) }));
17702
+ return { matched };
17655
17703
  }
17656
17704
  function incidentsForNode(nodeId, incidents) {
17657
17705
  if (!incidents || incidents.length === 0) return [];
@@ -17743,6 +17791,23 @@ function buildBlastSection(graph, node) {
17743
17791
  facts
17744
17792
  };
17745
17793
  }
17794
+ function buildDatabaseCallersSection(graph, node) {
17795
+ const callers = getBlastRadius(graph, node).affectedNodes.filter((item) => {
17796
+ if (graph.getNodeAttributes(item.nodeId).type !== import_types61.NodeType.ServiceNode) return false;
17797
+ return item.path.slice(1, -1).every((id) => {
17798
+ const type = graph.getNodeAttributes(id).type;
17799
+ return type === import_types61.NodeType.FileNode || type === import_types61.NodeType.SymbolNode;
17800
+ });
17801
+ });
17802
+ return {
17803
+ heading: `Services calling ${node} \u2014 ${callers.length}`,
17804
+ facts: callers.length > 0 ? callers.slice(0, MAX_FACTS_PER_SECTION).map((item) => ({
17805
+ text: `${item.nodeId} (distance ${item.distance})`,
17806
+ provenance: item.edgeProvenance,
17807
+ confidence: item.confidence
17808
+ })) : [{ text: `No inbound service callers found for ${node}.` }]
17809
+ };
17810
+ }
17746
17811
  function buildIncidentsSection(node, incidents) {
17747
17812
  const relevant = incidentsForNode(node, incidents);
17748
17813
  if (relevant.length === 0) return null;
@@ -17769,6 +17834,29 @@ function divergenceLine(d) {
17769
17834
  }
17770
17835
  return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
17771
17836
  }
17837
+ function runtimeEdgeState(graph) {
17838
+ let observed = false;
17839
+ let stale = false;
17840
+ graph.forEachEdge((_id, attrs) => {
17841
+ if (attrs.provenance === import_types61.Provenance.OBSERVED) observed = true;
17842
+ if (attrs.provenance === import_types61.Provenance.STALE) stale = true;
17843
+ });
17844
+ return { observed, stale };
17845
+ }
17846
+ function pendingRuntimeMessage(graph, divergences) {
17847
+ const count = divergences.filter((d) => d.type === "missing-observed").length;
17848
+ if (count === 0) return void 0;
17849
+ const { observed, stale } = runtimeEdgeState(graph);
17850
+ if (observed) return void 0;
17851
+ const prefix = stale ? "No current runtime observations \u2014 earlier runtime evidence is stale" : "No runtime observed yet";
17852
+ return `${prefix} \u2014 ${count} declared dependenc${count === 1 ? "y is" : "ies are"} waiting to be confirmed. Send traces (or connect a provider) and ask again.`;
17853
+ }
17854
+ function noDivergenceMessage(graph) {
17855
+ if (graph.order === 0) return "Graph is empty \u2014 no declared or observed dependencies to compare.";
17856
+ const { observed, stale } = runtimeEdgeState(graph);
17857
+ if (!observed) return stale ? "No current runtime dependency observations to compare \u2014 earlier runtime evidence is stale." : "No runtime dependency observations to compare with declared code.";
17858
+ return "None \u2014 declared code and observed runtime agree across the graph.";
17859
+ }
17772
17860
  function buildDivergenceSection(graph, node, incidents) {
17773
17861
  const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
17774
17862
  if (result.totalAffected === 0) return null;
@@ -17785,19 +17873,28 @@ function buildDivergenceSection(graph, node, incidents) {
17785
17873
  }
17786
17874
  function buildGlobalDivergenceSection(graph, incidents) {
17787
17875
  const result = computeDivergences(graph, incidents ? { incidents } : {});
17876
+ const pending = pendingRuntimeMessage(graph, result.divergences);
17877
+ const visible = pending ? result.divergences.filter((d) => d.type !== "missing-observed") : result.divergences;
17878
+ if (visible.length === 0 && pending) {
17879
+ return {
17880
+ heading: "Divergences (EXTRACTED vs OBSERVED)",
17881
+ facts: [{ text: pending }]
17882
+ };
17883
+ }
17788
17884
  if (result.totalAffected === 0) {
17789
17885
  return {
17790
17886
  heading: "Divergences (EXTRACTED vs OBSERVED)",
17791
- facts: [{ text: "None \u2014 declared code and observed runtime agree across the graph." }]
17887
+ facts: [{ text: noDivergenceMessage(graph) }]
17792
17888
  };
17793
17889
  }
17794
- const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17890
+ const facts = visible.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17795
17891
  text: divergenceLine(d),
17796
17892
  confidence: d.confidence
17797
17893
  // Composite by construction (EXTRACTED vs OBSERVED), so no single provenance.
17798
17894
  }));
17895
+ if (pending && facts.length < MAX_FACTS_PER_SECTION) facts.push({ text: pending });
17799
17896
  return {
17800
- heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
17897
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${visible.length}`,
17801
17898
  facts
17802
17899
  };
17803
17900
  }
@@ -17883,13 +17980,12 @@ function buildOverviewSections(graph, incidents) {
17883
17980
  });
17884
17981
  }
17885
17982
  const div = computeDivergences(graph, incidents ? { incidents } : {});
17983
+ const pending = pendingRuntimeMessage(graph, div.divergences);
17984
+ const visibleCount = pending ? div.divergences.filter((d) => d.type !== "missing-observed").length : div.totalAffected;
17985
+ const divergenceText = visibleCount > 0 ? `${visibleCount} divergence${visibleCount === 1 ? "" : "s"} between declared code and observed runtime \u2014 ask "are there any divergences?" for the list.${pending ? ` ${pending}` : ""}` : pending ?? noDivergenceMessage(graph);
17886
17986
  sections.push({
17887
17987
  heading: "Divergences",
17888
- facts: [
17889
- {
17890
- 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.`
17891
- }
17892
- ]
17988
+ facts: [{ text: divergenceText }]
17893
17989
  });
17894
17990
  return sections;
17895
17991
  }
@@ -17934,6 +18030,8 @@ function summarizeGlobal(intent, sections) {
17934
18030
  const lead = sections[0];
17935
18031
  switch (intent) {
17936
18032
  case "divergence": {
18033
+ const pending = lead?.facts[0]?.text;
18034
+ if (pending?.startsWith("No runtime") || pending?.startsWith("No current runtime") || pending?.startsWith("Graph is empty")) return pending;
17937
18035
  const none = lead?.facts[0]?.text.startsWith("None") ?? true;
17938
18036
  return none ? "No divergences across the graph \u2014 declared code and observed runtime agree." : `${lead.heading} across the graph, highest-confidence first below.`;
17939
18037
  }
@@ -17946,11 +18044,16 @@ function summarizeGlobal(intent, sections) {
17946
18044
  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.`;
17947
18045
  }
17948
18046
  }
17949
- function summarize(question, intent, matched, primary, sections, scope) {
18047
+ function summarize(question, intent, matched, primary, sections, scope, ambiguousType) {
17950
18048
  if (scope === "global") {
17951
18049
  return summarizeGlobal(intent, sections);
17952
18050
  }
17953
18051
  if (!primary) {
18052
+ if (ambiguousType) {
18053
+ const shown = ambiguousType.ids.slice(0, 10);
18054
+ const more = ambiguousType.ids.length - shown.length;
18055
+ return `"${ambiguousType.noun}" matches ${ambiguousType.ids.length} nodes. Name one by ID: ${shown.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`;
18056
+ }
17954
18057
  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.`;
17955
18058
  }
17956
18059
  const lead = sections[0];
@@ -17973,10 +18076,13 @@ function summarize(question, intent, matched, primary, sections, scope) {
17973
18076
  core = lead ? `${lead.heading} of ${primary}.` : `${primary} has no dependents \u2014 nothing else would break if it failed.`;
17974
18077
  break;
17975
18078
  case "dependencies":
17976
- core = lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
18079
+ core = lead?.heading.startsWith("Services calling") ? `${lead.heading}.` : lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
17977
18080
  break;
17978
18081
  case "observed":
17979
- core = lead ? `${primary} at runtime: ${lead.facts.length} OBSERVED fact${lead.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
18082
+ {
18083
+ const observed = sections.find((s) => s.heading === "Runtime dependencies (OBSERVED)");
18084
+ core = observed ? `${primary} at runtime: ${observed.facts.length} OBSERVED fact${observed.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
18085
+ }
17980
18086
  break;
17981
18087
  case "incidents":
17982
18088
  core = lead ? `${primary}: ${lead.heading.toLowerCase()}.` : `No incidents recorded against ${primary}.`;
@@ -17993,24 +18099,28 @@ async function askGraph(graph, question, opts = {}) {
17993
18099
  const now = opts.now ?? Date.now();
17994
18100
  const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
17995
18101
  const intent = classifyIntent(question);
17996
- const matched = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
18102
+ const resolution = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
18103
+ const matched = resolution.matched;
17997
18104
  const primary = matched[0]?.nodeId;
17998
18105
  const sections = [];
17999
18106
  let scope;
18000
18107
  if (primary) {
18001
18108
  scope = "node";
18109
+ const callersQuestion = intent === "dependencies" && graph.getNodeAttributes(primary).type === import_types61.NodeType.DatabaseNode && /\b(?:which|what)\s+services?\s+(?:talks?\s+to|connects?\s+to|uses?|hits?|calls?|reads?\s+from|writes?\s+to)\b/i.test(question);
18110
+ if (callersQuestion) sections.push(buildDatabaseCallersSection(graph, primary));
18002
18111
  for (const kind of SECTION_ORDER[intent]) {
18112
+ if (callersQuestion && kind === "blast") continue;
18003
18113
  const s = buildSection(kind, graph, primary, opts.incidents, now);
18004
18114
  if (s && s.facts.length > 0) sections.push(s);
18005
18115
  }
18006
18116
  } else {
18007
- const global = buildGlobalSections(intent, graph, opts.incidents);
18117
+ const global = resolution.ambiguousType ? null : buildGlobalSections(intent, graph, opts.incidents);
18008
18118
  if (global) {
18009
18119
  scope = "global";
18010
18120
  for (const s of global) if (s.facts.length > 0) sections.push(s);
18011
18121
  }
18012
18122
  }
18013
- const answer = summarize(question, intent, matched, primary, sections, scope);
18123
+ const answer = summarize(question, intent, matched, primary, sections, scope, resolution.ambiguousType);
18014
18124
  const provSet = /* @__PURE__ */ new Set();
18015
18125
  for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
18016
18126
  const confidence = sections[0]?.facts[0]?.confidence;
@@ -19813,9 +19923,9 @@ var RESOURCE_TYPES = [
19813
19923
  function isFirebaseResourceType(value) {
19814
19924
  return RESOURCE_TYPES.includes(value);
19815
19925
  }
19816
- function buildEntriesFilter(sinceIso) {
19926
+ function buildEntriesFilter(sinceIso, resourceTypes = RESOURCE_TYPES) {
19817
19927
  return [
19818
- 'resource.type = ("cloud_function" OR "cloud_run_revision" OR "firebase_domain")',
19928
+ `resource.type = (${resourceTypes.map((t) => `"${t}"`).join(" OR ")})`,
19819
19929
  "httpRequest:*",
19820
19930
  `timestamp >= "${sinceIso}"`
19821
19931
  ].join(" AND ");
@@ -19824,8 +19934,8 @@ var DEFAULT_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
19824
19934
  var ENTRIES_LIST_URL = "https://logging.googleapis.com/v2/entries:list";
19825
19935
  var PAGE_SIZE = 1e3;
19826
19936
  var MAX_PAGES = 20;
19827
- async function fetchHttpRequestLogEntries(creds, sinceIso) {
19828
- const filter = buildEntriesFilter(sinceIso);
19937
+ async function fetchHttpRequestLogEntries(creds, sinceIso, resourceTypes) {
19938
+ const filter = buildEntriesFilter(sinceIso, resourceTypes);
19829
19939
  const out = [];
19830
19940
  let pageToken;
19831
19941
  for (let page = 0; page < MAX_PAGES; page++) {
@@ -19940,7 +20050,38 @@ function mapLogEntriesToSignals(entries) {
19940
20050
 
19941
20051
  // src/connectors/firebase/resolve.ts
19942
20052
  init_cjs_shims();
20053
+ var import_types72 = require("@neat.is/types");
20054
+
20055
+ // src/connectors/infer-service.ts
20056
+ init_cjs_shims();
19943
20057
  var import_types71 = require("@neat.is/types");
20058
+ function normalizeName(name) {
20059
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
20060
+ }
20061
+ function inferServiceName(graph, resourceName, method, path77) {
20062
+ const wantedMethod = method.toUpperCase();
20063
+ const wantedPath = normalizePathTemplate(path77);
20064
+ const services = [];
20065
+ const owners = /* @__PURE__ */ new Set();
20066
+ graph.forEachNode((_id, attrs) => {
20067
+ const node = attrs;
20068
+ if (node.type === import_types71.NodeType.ServiceNode && typeof node.name === "string") {
20069
+ services.push(node.name);
20070
+ return;
20071
+ }
20072
+ if (node.type !== import_types71.NodeType.RouteNode) return;
20073
+ const route = attrs;
20074
+ if (!route.pathTemplate || normalizePathTemplate(route.pathTemplate) !== wantedPath) return;
20075
+ const routeMethod = route.method.toUpperCase();
20076
+ if (routeMethod === "ALL" || routeMethod === wantedMethod) owners.add(route.service);
20077
+ });
20078
+ const wanted = normalizeName(resourceName);
20079
+ const byName = services.find((s) => normalizeName(s) === wanted);
20080
+ if (byName) return byName;
20081
+ return owners.size === 1 ? [...owners][0] : null;
20082
+ }
20083
+
20084
+ // src/connectors/firebase/resolve.ts
19944
20085
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
19945
20086
  switch (resourceType) {
19946
20087
  case "cloud_function":
@@ -19955,7 +20096,7 @@ function routeEntriesFor(graph, serviceName) {
19955
20096
  const entries = [];
19956
20097
  graph.forEachNode((_id, attrs) => {
19957
20098
  const node = attrs;
19958
- if (node.type !== import_types71.NodeType.RouteNode) return;
20099
+ if (node.type !== import_types72.NodeType.RouteNode) return;
19959
20100
  const route = attrs;
19960
20101
  if (route.service !== serviceName) return;
19961
20102
  entries.push({
@@ -19979,32 +20120,38 @@ function createFirebaseResolveTarget(graph, serviceMap) {
19979
20120
  }
19980
20121
  const identity = parseFirebaseTargetName(signal.targetName);
19981
20122
  if (!identity) return null;
19982
- const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap);
19983
- if (!serviceName) return null;
19984
20123
  const normalizedPath = normalizePathTemplate(identity.path);
20124
+ const serviceName = neatServiceNameFor(resourceType, identity.resourceName, serviceMap) ?? (serviceMap.inferServices ? inferServiceName(graph, identity.resourceName, identity.method, identity.path) : null);
20125
+ if (!serviceName) return null;
19985
20126
  const match = findRoute2(routeEntriesFor(graph, serviceName), identity.method, normalizedPath);
19986
20127
  if (!match) return null;
19987
20128
  return {
19988
20129
  targetNodeId: match.routeNodeId,
19989
20130
  serviceName,
19990
- edgeType: import_types71.EdgeType.CALLS
20131
+ edgeType: import_types72.EdgeType.CALLS
19991
20132
  };
19992
20133
  };
19993
20134
  }
19994
20135
 
19995
20136
  // src/connectors/firebase/index.ts
19996
20137
  var FirebaseConnector = class {
20138
+ constructor(opts = {}) {
20139
+ this.opts = opts;
20140
+ }
20141
+ opts;
19997
20142
  provider = "firebase";
19998
20143
  async poll(ctx) {
19999
20144
  const creds = readFirebaseCredentials(ctx.credentials);
20000
20145
  const sinceIso = ctx.since ?? new Date(Date.now() - DEFAULT_LOOKBACK_MS).toISOString();
20001
- const entries = await fetchHttpRequestLogEntries(creds, sinceIso);
20146
+ const entries = await fetchHttpRequestLogEntries(creds, sinceIso, this.opts.resourceTypes);
20002
20147
  return mapLogEntriesToSignals(entries);
20003
20148
  }
20004
20149
  };
20005
20150
  function createFirebaseConnector(graph, serviceMap) {
20006
20151
  return {
20007
- connector: new FirebaseConnector(),
20152
+ connector: new FirebaseConnector(
20153
+ serviceMap.excludeCloudRun ? { resourceTypes: ["cloud_function", "firebase_domain"] } : {}
20154
+ ),
20008
20155
  resolveTarget: createFirebaseResolveTarget(graph, serviceMap)
20009
20156
  };
20010
20157
  }
@@ -20014,7 +20161,7 @@ init_cjs_shims();
20014
20161
 
20015
20162
  // src/connectors/cloudflare/connector.ts
20016
20163
  init_cjs_shims();
20017
- var import_types73 = require("@neat.is/types");
20164
+ var import_types74 = require("@neat.is/types");
20018
20165
 
20019
20166
  // src/connectors/cloudflare/client.ts
20020
20167
  init_cjs_shims();
@@ -20178,7 +20325,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
20178
20325
  graph.forEachNode((id, attrs) => {
20179
20326
  if (found) return;
20180
20327
  const a = attrs;
20181
- if (a.type === import_types73.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
20328
+ if (a.type === import_types74.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
20182
20329
  found = id;
20183
20330
  }
20184
20331
  });
@@ -20190,7 +20337,7 @@ function findMatchingRouteNode(graph, serviceName, method, path77) {
20190
20337
  graph.forEachNode((id, attrs) => {
20191
20338
  if (found) return;
20192
20339
  const a = attrs;
20193
- if (a.type !== import_types73.NodeType.RouteNode || a.service !== serviceName) return;
20340
+ if (a.type !== import_types74.NodeType.RouteNode || a.service !== serviceName) return;
20194
20341
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
20195
20342
  const routeMethod = (a.method ?? "").toUpperCase();
20196
20343
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -20209,11 +20356,11 @@ function createCloudflareResolveTarget(config, graph) {
20209
20356
  };
20210
20357
  const mapping = config.workers?.[scriptName];
20211
20358
  if (mapping) {
20212
- const wholeFileId = (0, import_types73.fileId)(mapping.service, mapping.entryFile);
20359
+ const wholeFileId = (0, import_types74.fileId)(mapping.service, mapping.entryFile);
20213
20360
  return {
20214
20361
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
20215
20362
  serviceName: mapping.service,
20216
- edgeType: import_types73.EdgeType.CALLS
20363
+ edgeType: import_types74.EdgeType.CALLS
20217
20364
  };
20218
20365
  }
20219
20366
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -20222,13 +20369,13 @@ function createCloudflareResolveTarget(config, graph) {
20222
20369
  return {
20223
20370
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
20224
20371
  serviceName: fileNode.service,
20225
- edgeType: import_types73.EdgeType.CALLS
20372
+ edgeType: import_types74.EdgeType.CALLS
20226
20373
  };
20227
20374
  }
20228
20375
  return {
20229
- targetNodeId: (0, import_types73.infraId)("cloudflare-worker", scriptName),
20376
+ targetNodeId: (0, import_types74.infraId)("cloudflare-worker", scriptName),
20230
20377
  serviceName: scriptName,
20231
- edgeType: import_types73.EdgeType.CALLS,
20378
+ edgeType: import_types74.EdgeType.CALLS,
20232
20379
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
20233
20380
  };
20234
20381
  };
@@ -20424,14 +20571,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
20424
20571
 
20425
20572
  // src/connectors/neon/resolve.ts
20426
20573
  init_cjs_shims();
20427
- var import_types77 = require("@neat.is/types");
20574
+ var import_types78 = require("@neat.is/types");
20428
20575
  function createNeonResolveTarget(config) {
20429
20576
  return (signal) => {
20430
20577
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
20431
20578
  return {
20432
- targetNodeId: (0, import_types77.infraId)("sql-table", signal.targetName),
20579
+ targetNodeId: (0, import_types78.infraId)("sql-table", signal.targetName),
20433
20580
  serviceName: config.serviceName,
20434
- edgeType: import_types77.EdgeType.CALLS,
20581
+ edgeType: import_types78.EdgeType.CALLS,
20435
20582
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
20436
20583
  };
20437
20584
  };
@@ -20612,14 +20759,14 @@ function mapLogEntriesToSignals2(entries) {
20612
20759
 
20613
20760
  // src/connectors/cloud-run/resolve.ts
20614
20761
  init_cjs_shims();
20615
- var import_types81 = require("@neat.is/types");
20762
+ var import_types82 = require("@neat.is/types");
20616
20763
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
20617
20764
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
20618
20765
  let found = null;
20619
20766
  graph.forEachNode((_id, attrs) => {
20620
20767
  if (found) return;
20621
20768
  const node = attrs;
20622
- if (node.type !== import_types81.NodeType.RouteNode) return;
20769
+ if (node.type !== import_types82.NodeType.RouteNode) return;
20623
20770
  const route = attrs;
20624
20771
  if (route.service !== serviceName || !route.pathTemplate) return;
20625
20772
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -20635,7 +20782,7 @@ function createCloudRunResolveTarget(graph, config) {
20635
20782
  const identity = parseCloudRunTargetName(signal.targetName);
20636
20783
  if (!identity) return null;
20637
20784
  const { serviceName: gcpServiceName, method, path: path77 } = identity;
20638
- const mappedService = config.serviceMap?.[gcpServiceName];
20785
+ const mappedService = config.serviceMap?.[gcpServiceName] ?? (config.inferServices ? inferServiceName(graph, gcpServiceName, method, path77) ?? void 0 : void 0);
20639
20786
  if (mappedService) {
20640
20787
  const routeNodeId = findMatchingRouteNode2(
20641
20788
  graph,
@@ -20644,13 +20791,13 @@ function createCloudRunResolveTarget(graph, config) {
20644
20791
  normalizePathTemplate(path77)
20645
20792
  );
20646
20793
  if (routeNodeId) {
20647
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types81.EdgeType.CALLS };
20794
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types82.EdgeType.CALLS };
20648
20795
  }
20649
20796
  }
20650
20797
  return {
20651
- targetNodeId: (0, import_types81.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
20798
+ targetNodeId: (0, import_types82.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
20652
20799
  serviceName: mappedService ?? gcpServiceName,
20653
- edgeType: import_types81.EdgeType.CALLS,
20800
+ edgeType: import_types82.EdgeType.CALLS,
20654
20801
  ensureInfraNode: {
20655
20802
  kind: CLOUD_RUN_SERVICE_INFRA_KIND,
20656
20803
  name: gcpServiceName,
@@ -20832,14 +20979,14 @@ function mapLogEntriesToSignals3(entries) {
20832
20979
 
20833
20980
  // src/connectors/gcp-lb/resolve.ts
20834
20981
  init_cjs_shims();
20835
- var import_types85 = require("@neat.is/types");
20982
+ var import_types86 = require("@neat.is/types");
20836
20983
  var GCP_LB_BACKEND_INFRA_KIND = "gcp-lb-backend";
20837
20984
  function findMatchingRouteNode3(graph, serviceName, method, normalizedPath) {
20838
20985
  let found = null;
20839
20986
  graph.forEachNode((_id, attrs) => {
20840
20987
  if (found) return;
20841
20988
  const node = attrs;
20842
- if (node.type !== import_types85.NodeType.RouteNode) return;
20989
+ if (node.type !== import_types86.NodeType.RouteNode) return;
20843
20990
  const route = attrs;
20844
20991
  if (route.service !== serviceName || !route.pathTemplate) return;
20845
20992
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -20855,7 +21002,7 @@ function createGcpLbResolveTarget(graph, config) {
20855
21002
  const identity = parseGcpLbTargetName(signal.targetName);
20856
21003
  if (!identity) return null;
20857
21004
  const { backendServiceName, method, path: path77 } = identity;
20858
- const mappedService = config.backendServiceMap?.[backendServiceName];
21005
+ const mappedService = config.backendServiceMap?.[backendServiceName] ?? (config.inferServices ? inferServiceName(graph, backendServiceName, method, path77) ?? void 0 : void 0);
20859
21006
  if (mappedService) {
20860
21007
  const routeNodeId = findMatchingRouteNode3(
20861
21008
  graph,
@@ -20864,13 +21011,13 @@ function createGcpLbResolveTarget(graph, config) {
20864
21011
  normalizePathTemplate(path77)
20865
21012
  );
20866
21013
  if (routeNodeId) {
20867
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types85.EdgeType.CALLS };
21014
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types86.EdgeType.CALLS };
20868
21015
  }
20869
21016
  }
20870
21017
  return {
20871
- targetNodeId: (0, import_types85.infraId)(GCP_LB_BACKEND_INFRA_KIND, backendServiceName),
21018
+ targetNodeId: (0, import_types86.infraId)(GCP_LB_BACKEND_INFRA_KIND, backendServiceName),
20872
21019
  serviceName: mappedService ?? backendServiceName,
20873
- edgeType: import_types85.EdgeType.CALLS,
21020
+ edgeType: import_types86.EdgeType.CALLS,
20874
21021
  ensureInfraNode: {
20875
21022
  kind: GCP_LB_BACKEND_INFRA_KIND,
20876
21023
  name: backendServiceName,
@@ -20911,7 +21058,7 @@ function createGcpLbConnector(graph, config = {}) {
20911
21058
 
20912
21059
  // src/connectors/render/index.ts
20913
21060
  init_cjs_shims();
20914
- var import_types88 = require("@neat.is/types");
21061
+ var import_types89 = require("@neat.is/types");
20915
21062
 
20916
21063
  // src/connectors/render/types.ts
20917
21064
  init_cjs_shims();
@@ -20989,7 +21136,7 @@ function buildRenderRouteIndex(graph, serviceName) {
20989
21136
  const out = [];
20990
21137
  graph.forEachNode((_id, attrs) => {
20991
21138
  const node = attrs;
20992
- if (node.type !== import_types88.NodeType.RouteNode) return;
21139
+ if (node.type !== import_types89.NodeType.RouteNode) return;
20993
21140
  const route = attrs;
20994
21141
  if (route.service !== serviceName) return;
20995
21142
  out.push({
@@ -21074,7 +21221,7 @@ function mapRenderRequestLogsToSignals(entries, routeIndex) {
21074
21221
  function createRenderResolveTarget(config) {
21075
21222
  return (signal) => {
21076
21223
  if (signal.targetKind === ROUTE_TARGET_KIND2) {
21077
- return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types88.EdgeType.CALLS };
21224
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types89.EdgeType.CALLS };
21078
21225
  }
21079
21226
  return null;
21080
21227
  };
@@ -21212,21 +21359,21 @@ function mapInsightsToSignals(rows, observedAtIso) {
21212
21359
 
21213
21360
  // src/connectors/planetscale/resolve.ts
21214
21361
  init_cjs_shims();
21215
- var import_types92 = require("@neat.is/types");
21362
+ var import_types93 = require("@neat.is/types");
21216
21363
  var PLANETSCALE_DATABASE_KIND = "planetscale-database";
21217
21364
  function createPlanetscaleResolveTarget(graph, config) {
21218
21365
  const databaseName = `${config.organization}/${config.database}`;
21219
21366
  return (signal, _ctx) => {
21220
21367
  if (signal.targetKind !== PLANETSCALE_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
21221
- const tableId = (0, import_types92.infraId)("sql-table", signal.targetName);
21368
+ const tableId = (0, import_types93.infraId)("sql-table", signal.targetName);
21222
21369
  if (graph.hasNode(tableId)) {
21223
- return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types92.EdgeType.CALLS };
21370
+ return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types93.EdgeType.CALLS };
21224
21371
  }
21225
- const providerId = (0, import_types92.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
21372
+ const providerId = (0, import_types93.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
21226
21373
  return {
21227
21374
  targetNodeId: providerId,
21228
21375
  serviceName: config.serviceName,
21229
- edgeType: import_types92.EdgeType.CALLS,
21376
+ edgeType: import_types93.EdgeType.CALLS,
21230
21377
  ensureInfraNode: { kind: PLANETSCALE_DATABASE_KIND, name: databaseName, provider: "planetscale" }
21231
21378
  };
21232
21379
  };
@@ -21491,7 +21638,7 @@ function mapBuildsToSignals(builds, serviceName) {
21491
21638
 
21492
21639
  // src/connectors/eas/resolve.ts
21493
21640
  init_cjs_shims();
21494
- var import_types97 = require("@neat.is/types");
21641
+ var import_types98 = require("@neat.is/types");
21495
21642
  var NO_ENV2 = "unknown";
21496
21643
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
21497
21644
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -21507,8 +21654,8 @@ function configBasenamesForPhase(phase) {
21507
21654
  function configNodeService(graph, configNodeId) {
21508
21655
  for (const edgeId of graph.inboundEdges(configNodeId)) {
21509
21656
  const edge = graph.getEdgeAttributes(edgeId);
21510
- if (edge.type !== import_types97.EdgeType.CONFIGURED_BY) continue;
21511
- const parsed = (0, import_types97.parseFileId)(edge.source);
21657
+ if (edge.type !== import_types98.EdgeType.CONFIGURED_BY) continue;
21658
+ const parsed = (0, import_types98.parseFileId)(edge.source);
21512
21659
  if (parsed) return parsed.service;
21513
21660
  }
21514
21661
  return null;
@@ -21519,7 +21666,7 @@ function findConfigNode(graph, basenames, serviceName) {
21519
21666
  graph.forEachNode((id, attrs) => {
21520
21667
  if (scoped) return;
21521
21668
  const node = attrs;
21522
- if (node.type !== import_types97.NodeType.ConfigNode) return;
21669
+ if (node.type !== import_types98.NodeType.ConfigNode) return;
21523
21670
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
21524
21671
  if (anyMatch === null) anyMatch = id;
21525
21672
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -21536,13 +21683,13 @@ function createEasResolveTarget(graph) {
21536
21683
  if (basenames.length > 0) {
21537
21684
  const configNodeId = findConfigNode(graph, basenames, serviceName);
21538
21685
  if (configNodeId) {
21539
- return { targetNodeId: configNodeId, serviceName, edgeType: import_types97.EdgeType.CALLS };
21686
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types98.EdgeType.CALLS };
21540
21687
  }
21541
21688
  }
21542
21689
  return {
21543
21690
  targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
21544
21691
  serviceName,
21545
- edgeType: import_types97.EdgeType.CALLS
21692
+ edgeType: import_types98.EdgeType.CALLS
21546
21693
  };
21547
21694
  };
21548
21695
  }
@@ -22270,11 +22417,11 @@ function registerRoutes(scope, ctx) {
22270
22417
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
22271
22418
  const parsed = [];
22272
22419
  for (const c of candidates) {
22273
- const r = import_types100.DivergenceTypeSchema.safeParse(c);
22420
+ const r = import_types101.DivergenceTypeSchema.safeParse(c);
22274
22421
  if (!r.success) {
22275
22422
  return reply.code(400).send({
22276
22423
  error: `unknown divergence type "${c}"`,
22277
- allowed: import_types100.DivergenceTypeSchema.options
22424
+ allowed: import_types101.DivergenceTypeSchema.options
22278
22425
  });
22279
22426
  }
22280
22427
  parsed.push(r.data);
@@ -22670,7 +22817,7 @@ function registerRoutes(scope, ctx) {
22670
22817
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
22671
22818
  let violations = await log.readAll();
22672
22819
  if (req.query.severity) {
22673
- const sev = import_types100.PolicySeveritySchema.safeParse(req.query.severity);
22820
+ const sev = import_types101.PolicySeveritySchema.safeParse(req.query.severity);
22674
22821
  if (!sev.success) {
22675
22822
  return reply.code(400).send({
22676
22823
  error: "invalid severity",
@@ -22709,7 +22856,7 @@ function registerRoutes(scope, ctx) {
22709
22856
  scope.post("/policies/check", async (req, reply) => {
22710
22857
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
22711
22858
  if (!proj) return;
22712
- const parsed = import_types100.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22859
+ const parsed = import_types101.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22713
22860
  if (!parsed.success) {
22714
22861
  return reply.code(400).send({
22715
22862
  error: "invalid /policies/check body",