hypersteeldb 0.5.5

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
//! **Step 0 as JSON** โ€” the machine-readable form of ontology sensing, for the demo Spaces and for any
//! caller that wants the Vocabulary Space without parsing console output.
//!
//!   `cargo run --release --bin sense_ontology -- <dir> [--sample N] [--min-support N] [--pretty]`
//!
//! This is paper ยง2 (Domain Typing via the Bitmap Symbol Table): before anything is indexed, the corpus is
//! sampled and its recurring fields become the facets of the Vocabulary Space V. Facets are what make
//! subtree wildcards possible โ€” `venue/*` can only mean something if `venue` is a declared dimension โ€” so
//! the wildcard stems are reported alongside them.
//!
//! Deliberately dependency-light: no ONNX, no tokio, no model. Step 0 is model-free, and a demo that needs
//! an API key to show a deterministic step would misrepresent it.

use std::path::{Path, PathBuf};
use steeldb::vocabulary::{candidate_fields, merge_existing, sample_docs, seed_from_sample, VocabularySpace};

fn main() -> Result<(), String> {
    let args: Vec<String> = std::env::args().collect();
    let dir = args
        .iter()
        .skip(1)
        .find(|a| !a.starts_with("--"))
        .cloned()
        .ok_or("usage: sense_ontology <dir> [--sample N] [--min-support N] [--pretty]")?;
    let flag = |name: &str| -> Option<usize> {
        args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).and_then(|v| v.parse().ok())
    };
    let n_sample = flag("--sample").unwrap_or(96);
    let min_support = flag("--min-support").unwrap_or(3);
    let pretty = args.iter().any(|a| a == "--pretty");

    let path = PathBuf::from(&dir);
    if !path.exists() {
        return Err(format!("{dir}: no such path"));
    }
    let samples = sample_docs(&path, n_sample, 4000);
    if samples.is_empty() {
        return Err(format!("no sampleable documents under {dir}"));
    }

    // the raw evidence the seed is drawn from, so a demo can show WHY a facet was proposed
    let candidates = candidate_fields(&samples);
    let seed = seed_from_sample(&dir, &samples, min_support);

    // Mirror real step 0: an existing spec on disk carries authored facets and relations the model-free seed
    // cannot rediscover, and the shipped CLI folds them in. A demo that skipped the merge would show a
    // different ontology than the product produces.
    let existing = VocabularySpace::for_corpus(&path);
    let spec = match &existing {
        Some(e) => merge_existing(seed, e),
        None => seed,
    };

    let out = serde_json::json!({
        "corpus": dir,
        "sampled_docs": samples.len(),
        "min_support": min_support,
        "entity_facets": spec.entity_facets.iter().map(|f| serde_json::json!({
            "name": f.name,
            "path": spec.facet_path(&f.name),
            "parent": f.parent,
            "description": f.description,
            // structural facets come from document field names; semantic ones are authored or proposed
            "structural": f.structural,
            "examples": f.examples,
        })).collect::<Vec<_>>(),
        "relation_facets": spec.relation_facets.iter().map(|r| serde_json::json!({
            "name": r.name, "head": r.head, "tail": r.tail, "uri": format!("rel/{}/+", r.name),
        })).collect::<Vec<_>>(),
        // facets are what make subtree wildcards expressible at all
        "wildcard_stems": spec.valid_prefixes(),
        "taggable_facets": spec.taggable_facets(),
        "gazetteer_size": spec.gazetteer.len(),
        "candidate_fields": candidates.iter().map(|(name, n)| serde_json::json!({
            "field": name, "support": n, "kept": *n >= min_support,
        })).collect::<Vec<_>>(),
        "existing_spec_on_disk": existing.as_ref().map(|e| serde_json::json!({
            "entity_facets": e.entity_facets.len(),
            "relation_facets": e.relation_facets.len(),
        })),
        "valid": spec.validate().is_ok(),
        "validation_error": spec.validate().err().map(|e| e.to_string()),
    });

    let body = if pretty {
        serde_json::to_string_pretty(&out).map_err(|e| e.to_string())?
    } else {
        serde_json::to_string(&out).map_err(|e| e.to_string())?
    };
    println!("{body}");
    Ok(())
}

#[allow(dead_code)]
fn unused(_: &Path) {}