hypersteeldb 0.5.2

A database that compiles questions instead of guessing answers: typed vocabulary discovered from your documents, queries type-checked before they run, roaring-bitmap set algebra over reified hyperedges, and Dempster-Shafer evidence with an explicit conflict guard.
Documentation
//! Structured-data agentic discover CLI — profile a CSV, let a local LLM design the projection
//! (facets / bucketized measures / relations), materialize it into the hypergraph, and optionally
//! answer a question over the result.
//!
//!   `cargo run --features paddock --bin discover -- <file.csv> ["question"]`
//!
//! Local model by default (Paddock → ollama on :11434). STEELDB_LLM=bedrock for Claude. With no server
//! reachable, it falls back to a deterministic projection so ingest still works.

use std::path::Path;
use steeldb::agent::{run_agent, ProviderConfig};
use steeldb::discover::{profile_csv, propose_spec, ProjectionSpec, SpecProjector};
use steeldb::Corpus;

fn provider_config() -> ProviderConfig {
    if std::env::var("STEELDB_LLM").as_deref() == Ok("bedrock") {
        return ProviderConfig::from_env();
    }
    ProviderConfig::Paddock {
        base_url: std::env::var("STEELDB_PADDOCK_URL").unwrap_or_else(|_| "http://localhost:11434/v1".to_string()),
        model: std::env::var("STEELDB_PADDOCK_MODEL").unwrap_or_else(|_| "qwen3:1.7b".to_string()),
        api_key: None,
    }
}

#[tokio::main]
async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let path = match args.get(1) {
        Some(p) => p.clone(),
        None => {
            eprintln!("usage: discover <file.csv> [\"question\"]");
            std::process::exit(2);
        }
    };

    let profile = match profile_csv(Path::new(&path)) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("profile error: {e}");
            std::process::exit(1);
        }
    };
    eprintln!("profiled {} rows, {} columns:", profile.rows, profile.columns.len());
    for c in &profile.columns {
        let key = if c.candidate_key { " [key]" } else { "" };
        eprintln!("  {:<20} {:<11} distinct={:<5} ratio={:.2}{key}", c.name, c.kind, c.distinct, c.distinct_ratio);
    }

    // let the local model design the projection (falls back to deterministic on any failure)
    let spec: ProjectionSpec = match provider_config().build().await {
        Ok(provider) => {
            eprintln!("\nproposing projection with {} …", provider.name());
            propose_spec(provider.as_ref(), &profile).await
        }
        Err(e) => {
            eprintln!("\nno LLM ({e}); using deterministic projection");
            ProjectionSpec::default_for(&profile)
        }
    };

    eprintln!("\nprojection spec:");
    eprintln!("  situation: {}", spec.situation);
    eprintln!("  facets:   {}", spec.facets.iter().map(|f| f.column.clone()).collect::<Vec<_>>().join(", "));
    eprintln!("  measures: {}", spec.measures.iter().map(|m| format!("{}(×{})", m.column, m.bins)).collect::<Vec<_>>().join(", "));
    if !spec.relations.is_empty() {
        eprintln!("  relations: {}", spec.relations.iter().map(|r| format!("{}:{}→{}", r.name, r.head, r.tail)).collect::<Vec<_>>().join(", "));
    }
    if !spec.notes.is_empty() {
        eprintln!("  notes: {}", spec.notes);
    }

    let projector = match SpecProjector::open(&path, spec) {
        Ok(p) => p,
        Err(e) => {
            eprintln!("materialize error: {e}");
            std::process::exit(1);
        }
    };
    let corpus = match Corpus::from_projector(Box::new(projector)) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("ingest error: {e}");
            std::process::exit(1);
        }
    };
    let stats = corpus.stats();
    eprintln!("\nbuilt hypergraph: {} situations, {} tokens", stats.situations, stats.vocab);
    eprintln!("facets: {}", stats.facets.iter().map(|(f, n)| format!("{f}({n})")).collect::<Vec<_>>().join("  "));

    if let Some(question) = args.get(2) {
        let provider = match provider_config().build().await {
            Ok(p) => p,
            Err(e) => {
                eprintln!("provider error: {e}");
                std::process::exit(1);
            }
        };
        eprintln!("\n── asking: {question} ──");
        match run_agent(provider.as_ref(), &corpus, question, 14).await {
            Ok(ans) => {
                eprintln!("({} tool calls)", ans.trace.len());
                println!("\n{}", ans.answer);
            }
            Err(e) => eprintln!("agent error: {e}"),
        }
    }
}