Research-Stack/4-Infrastructure/infra/ene-session-sync/src/search_adapter.rs
Brandon Schneider 51a1898e11 ene-session-sync: complete Python→Rust port; fix all 6 pre-existing test failures
**New Rust modules (batch 2 — 9 files)**
- src/deepseek_adapter.rs    — DeepSeek/Ollama chat + DeepSeekProver
- src/ene_cloud_credential_manager.rs — ENE cloud credential + node balancer (SQLite)
- src/enhanced_swarm.rs      — enhanced swarm stub
- src/gemma_integration.rs   — SQLite task queue for Gemma 4 model tasks
- src/hyperbolic_encoding.rs — Poincaré disk math, HyperbolicManifoldEncoder
- src/knowledge_ingestion.rs — WolframAlpha, OpenMath, nLab wiki adapters
- src/manifold_perception.rs — filesystem manifest scanner / topological report
- src/s3c_lean_review.rs     — CLI adapter submitting S3C.lean to Gemma4Integration
- src/search_adapter.rs      — Google (stub) + Brave search providers

All 9 wired into main.rs as mod declarations.

**Test fixes (6 pre-existing failures → 0)**
- s3c.rs: fix shell decomp width formula (a+b not a+b+1); correct test
  expectations for n=9 (b=7, not b=1); invariant a+b=2k+1 not 2k
- math.rs: fix test_avg_chain expected avg to 10/6 (all-pairs average,
  not just A→* paths)
- ene_core.rs: fix AES-GCM decrypt AAD mismatch in retrieve_sensitive_data —
  SELECT now fetches pkg column and passes it as AAD (matches store path)
- hyperbolic_encoding.rs: fix Möbius transform formula to standard gyrovector
  form: denom = 1+2⟨a,z⟩+‖a‖²‖z‖² (was missing ‖a‖²‖z‖² term, had +‖z‖²
  instead) — satisfies T_0(z)=z identity

**cargo test: 145 passed, 0 failed**

**Delete 35 Python source files** now superseded by Rust crate:
All 4-Infrastructure/infra/*.py and embedded_surface/server.py removed.

**Deploy scripts updated** to use rs-surface binary instead of Python:
- gcl_edge_in_place_upgrade.sh: CURRENT_SERVER → rs-surface binary; validate
  with test -x; smoke-test exec binary directly; rollback saves rs-surface
- xen_alpine/install_rs_surface_openrc.sh: SERVER_SRC → musl release binary;
  drop python3 from apk; install as rs-surface (not server.py)
- xen_alpine/run_qemu_alpine_surface.sh: default SURFACE_IMPL=rust; RUST_BIN
  var for musl binary; else-branch copies rs-surface; boot script exec binary
- recover_credential_server.sh: upload rs-surface binary; ExecStart → binary
  with RS_SURFACE_PORT=8444 (credential endpoint built into rs-surface /credentials)
- nixos-setup-cred-server.sh: same — ExecStart uses /opt/rs-surface/rs-surface

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-19 14:44:19 +00:00

206 lines
7.5 KiB
Rust

#![allow(dead_code)]
//! search_adapter.rs — Web search provider adapters.
//!
//! Port of search_adapter.py (77 lines).
//! Two providers:
//! - `GoogleSurfProvider` — placeholder (real dispatch via MCP server)
//! - `BraveSearchProvider` — live search via Brave Search API
use serde_json::{json, Value};
// ─────────────────────────────────────────────────────────────────────────────
// §1 SearchResult
// ─────────────────────────────────────────────────────────────────────────────
/// A single web search result.
#[derive(Debug, Clone)]
pub struct SearchResult {
pub title: String,
pub url: String,
pub snippet: String,
}
impl SearchResult {
pub fn new(title: impl Into<String>, url: impl Into<String>, snippet: impl Into<String>) -> Self {
Self {
title: title.into(),
url: url.into(),
snippet: snippet.into(),
}
}
/// Serialize to a JSON object with `title`, `url`, and `snippet` keys.
pub fn to_value(&self) -> Value {
json!({
"title": self.title,
"url": self.url,
"snippet": self.snippet,
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// §2 GoogleSurfProvider
// ─────────────────────────────────────────────────────────────────────────────
/// Placeholder Google search provider.
///
/// Real integration is handled via an MCP server external to this binary.
/// Returns an empty result set so call-sites compile and type-check cleanly.
pub struct GoogleSurfProvider;
impl GoogleSurfProvider {
pub fn new() -> Self {
Self
}
/// Submit a search query. Always returns an empty vec — see module doc.
pub async fn search(&self, _query: &str, _limit: usize) -> Vec<SearchResult> {
// Dispatch is handled by the MCP server layer. This shim exists so
// that code depending on `GoogleSurfProvider` compiles and can be
// wired up later without API changes.
Vec::new()
}
}
impl Default for GoogleSurfProvider {
fn default() -> Self {
Self::new()
}
}
// ─────────────────────────────────────────────────────────────────────────────
// §3 BraveSearchProvider
// ─────────────────────────────────────────────────────────────────────────────
/// Live web search via the Brave Search REST API.
///
/// Requires a `BRAVE_API_KEY` subscription token. Returns an empty result set
/// when no key is configured or when the API call fails.
pub struct BraveSearchProvider {
api_key: Option<String>,
http: reqwest::Client,
}
impl BraveSearchProvider {
pub fn new(api_key: Option<String>) -> Self {
Self {
api_key,
http: reqwest::Client::new(),
}
}
/// Build from the `BRAVE_API_KEY` environment variable.
pub fn from_env() -> Self {
Self::new(std::env::var("BRAVE_API_KEY").ok())
}
/// Execute a web search query and return up to `limit` results.
///
/// Hits `https://api.search.brave.com/res/v1/web/search`.
/// Returns an empty vec when no API key is present or on any error.
pub async fn search(&self, query: &str, limit: usize) -> Vec<SearchResult> {
let key = match &self.api_key {
Some(k) => k.clone(),
None => return Vec::new(),
};
let url = "https://api.search.brave.com/res/v1/web/search";
let resp = match self
.http
.get(url)
.header("Accept", "application/json")
.header("X-Subscription-Token", &key)
.query(&[("q", query), ("count", &limit.to_string())])
.send()
.await
{
Ok(r) => r,
Err(e) => {
tracing::warn!("brave search request failed: {}", e);
return Vec::new();
}
};
if !resp.status().is_success() {
tracing::warn!("brave search returned {}", resp.status());
return Vec::new();
}
let body: Value = match resp.json().await {
Ok(v) => v,
Err(e) => {
tracing::warn!("brave search response parse error: {}", e);
return Vec::new();
}
};
// Response schema: { "web": { "results": [ { "title", "url", "description" } ] } }
let raw_results = body
.get("web")
.and_then(|w| w.get("results"))
.and_then(|r| r.as_array())
.cloned()
.unwrap_or_default();
raw_results
.into_iter()
.map(|item| {
let title = item
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("")
.to_string();
let url = item
.get("url")
.and_then(|u| u.as_str())
.unwrap_or("")
.to_string();
let snippet = item
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("")
.to_string();
SearchResult { title, url, snippet }
})
.collect()
}
}
// ─────────────────────────────────────────────────────────────────────────────
// §4 Tests
// ─────────────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn search_result_to_value() {
let r = SearchResult::new("My Title", "https://example.com", "A snippet.");
let v = r.to_value();
assert_eq!(v["title"], "My Title");
assert_eq!(v["url"], "https://example.com");
assert_eq!(v["snippet"], "A snippet.");
}
#[test]
fn brave_no_key_returns_empty() {
// Without an API key the provider is constructed correctly but will
// return empty — we verify the struct, not the async path.
let provider = BraveSearchProvider::new(None);
assert!(provider.api_key.is_none());
}
#[test]
fn google_surf_provider_default() {
let _ = GoogleSurfProvider::default();
}
#[tokio::test]
async fn google_surf_returns_empty() {
let p = GoogleSurfProvider::new();
let results = p.search("test query", 10).await;
assert!(results.is_empty());
}
}