hypersteeldb 0.3.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
//! Generate the **public demo corpus**: a synthetic Pokémon league, written to exercise every part of the
//! engine the Spaces demonstrate. Deterministic (seeded LCG, no model, no network) so the corpus — and
//! therefore every demo number — is reproducible.
//!
//!   `cargo run --release --bin gen_pokemon_corpus -- <out_dir> [n_docs] [seed]`
//!
//! What each document type is *for*:
//!
//! | doc type            | exercises                                                              |
//! |---------------------|------------------------------------------------------------------------|
//! | Pokédex entry       | typed entities, quantities with units (kg, m, °C), habitat loci        |
//! | Gym record          | `city → region` spatial hierarchy, leader↔gym↔type relations           |
//! | Battle report       | multi-hop **s-paths** (trainer → battle → trainer), dates, durations   |
//! | Tournament notice   | temporal loci (`time/2026/q3`), venue geography, regulation references |
//! | Regulation clause   | numeric constraints (team size, timers, level caps), **negation**      |
//! | Habitat survey      | spatial + quantities (elevation m, temperature °C), species membership |
//! | Roster CSV          | relational/crosstab analytics over the same vocabulary                 |
//!
//! Pokémon species, types and their real weaknesses are factual; trainers, gyms, tournaments, battles and
//! regulations are invented, so nothing here is anyone's private data.

use std::fmt::Write as _;
use std::path::{Path, PathBuf};

/// Deterministic LCG — reproducible corpora without an rng dependency.
struct Rng(u64);
impl Rng {
    fn next(&mut self) -> u64 {
        self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
        self.0 >> 17
    }
    fn below(&mut self, n: usize) -> usize {
        (self.next() % n.max(1) as u64) as usize
    }
    fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
        &xs[self.below(xs.len())]
    }
    fn range(&mut self, lo: i64, hi: i64) -> i64 {
        lo + (self.next() % (hi - lo).max(1) as u64) as i64
    }
}

struct Species {
    name: &'static str,
    primary: &'static str,
    secondary: Option<&'static str>,
    height_m: f64,
    weight_kg: f64,
}

/// Factual species/type data — the type chart below keys off it, which is what makes the weakness
/// s-paths semantically meaningful rather than random edges.
const SPECIES: &[Species] = &[
    Species { name: "Registeel", primary: "steel", secondary: None, height_m: 1.9, weight_kg: 205.0 },
    Species { name: "Regirock", primary: "rock", secondary: None, height_m: 1.7, weight_kg: 230.0 },
    Species { name: "Regice", primary: "ice", secondary: None, height_m: 1.8, weight_kg: 175.0 },
    Species { name: "Metagross", primary: "steel", secondary: Some("psychic"), height_m: 1.6, weight_kg: 550.0 },
    Species { name: "Scizor", primary: "bug", secondary: Some("steel"), height_m: 1.8, weight_kg: 118.0 },
    Species { name: "Charizard", primary: "fire", secondary: Some("flying"), height_m: 1.7, weight_kg: 90.5 },
    Species { name: "Blastoise", primary: "water", secondary: None, height_m: 1.6, weight_kg: 85.5 },
    Species { name: "Venusaur", primary: "grass", secondary: Some("poison"), height_m: 2.0, weight_kg: 100.0 },
    Species { name: "Pikachu", primary: "electric", secondary: None, height_m: 0.4, weight_kg: 6.0 },
    Species { name: "Snorlax", primary: "normal", secondary: None, height_m: 2.1, weight_kg: 460.0 },
    Species { name: "Gengar", primary: "ghost", secondary: Some("poison"), height_m: 1.5, weight_kg: 40.5 },
    Species { name: "Dragonite", primary: "dragon", secondary: Some("flying"), height_m: 2.2, weight_kg: 210.0 },
    Species { name: "Tyranitar", primary: "rock", secondary: Some("dark"), height_m: 2.0, weight_kg: 202.0 },
    Species { name: "Lucario", primary: "fighting", secondary: Some("steel"), height_m: 1.2, weight_kg: 54.0 },
    Species { name: "Garchomp", primary: "dragon", secondary: Some("ground"), height_m: 1.9, weight_kg: 95.0 },
    Species { name: "Milotic", primary: "water", secondary: None, height_m: 6.2, weight_kg: 162.0 },
    Species { name: "Aggron", primary: "steel", secondary: Some("rock"), height_m: 2.1, weight_kg: 360.0 },
    Species { name: "Salamence", primary: "dragon", secondary: Some("flying"), height_m: 1.5, weight_kg: 102.6 },
];

/// `type → what beats it` (abbreviated but factual) — the edges the weakness s-paths traverse.
const WEAKNESS: &[(&str, &[&str])] = &[
    ("steel", &["fire", "fighting", "ground"]),
    ("rock", &["water", "grass", "fighting", "ground", "steel"]),
    ("ice", &["fire", "fighting", "rock", "steel"]),
    ("fire", &["water", "rock", "ground"]),
    ("water", &["electric", "grass"]),
    ("grass", &["fire", "ice", "poison", "flying", "bug"]),
    ("electric", &["ground"]),
    ("normal", &["fighting"]),
    ("ghost", &["ghost", "dark"]),
    ("dragon", &["ice", "dragon", "fairy"]),
    ("psychic", &["bug", "ghost", "dark"]),
    ("bug", &["fire", "flying", "rock"]),
    ("dark", &["fighting", "bug", "fairy"]),
    ("fighting", &["flying", "psychic", "fairy"]),
    ("ground", &["water", "grass", "ice"]),
    ("flying", &["electric", "ice", "rock"]),
    ("poison", &["ground", "psychic"]),
];

const MOVES: &[(&str, &str, i64)] = &[
    ("Flash Cannon", "steel", 80),
    ("Iron Head", "steel", 80),
    ("Flamethrower", "fire", 90),
    ("Surf", "water", 90),
    ("Thunderbolt", "electric", 90),
    ("Earthquake", "ground", 100),
    ("Ice Beam", "ice", 90),
    ("Shadow Ball", "ghost", 80),
    ("Dragon Claw", "dragon", 80),
    ("Close Combat", "fighting", 120),
    ("Stone Edge", "rock", 100),
    ("Leaf Storm", "grass", 130),
];

/// `(region, city)` — the spatial hierarchy the geo demos walk.
const PLACES: &[(&str, &str)] = &[
    ("kanto", "Pewter City"),
    ("kanto", "Cerulean City"),
    ("kanto", "Saffron City"),
    ("kanto", "Cinnabar Island"),
    ("johto", "Violet City"),
    ("johto", "Ecruteak City"),
    ("johto", "Blackthorn City"),
    ("hoenn", "Rustboro City"),
    ("hoenn", "Mauville City"),
    ("hoenn", "Sootopolis City"),
    ("sinnoh", "Oreburgh City"),
    ("sinnoh", "Snowpoint City"),
];

const TRAINERS: &[&str] = &[
    "Brock Harrow", "Misty Cascade", "Sabrina Vale", "Blaine Ember", "Falkner Gale", "Morty Shade",
    "Clair Wyvern", "Roxanne Slate", "Wattson Volt", "Juan Tide", "Roark Quarry", "Candice Frost",
    "Iris Draco", "Marnie Dusk", "Nessa Reef", "Bea Strike",
];

const TOURNAMENTS: &[&str] = &[
    "Indigo Invitational", "Silver Conference", "Ever Grande Cup", "Lily of the Valley Open",
    "Steel Type Masters", "Regional Doubles Series",
];

fn quarter(month: i64) -> i64 {
    (month - 1) / 3 + 1
}

fn write_doc(dir: &Path, idx: usize, slug: &str, body: &str) -> std::io::Result<()> {
    let name = format!("{idx:05}_{slug}.md");
    std::fs::write(dir.join(name), body)
}

fn main() -> std::io::Result<()> {
    let args: Vec<String> = std::env::args().collect();
    let out = PathBuf::from(args.get(1).cloned().unwrap_or_else(|| "pokemon_corpus".to_string()));
    let n_docs: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(400);
    let seed: u64 = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0x9E3779B97F4A7C15);
    std::fs::create_dir_all(&out)?;
    let mut rng = Rng(seed);
    let mut idx = 0usize;
    let mut roster_rows: Vec<String> = vec!["trainer,region,city,species,type,level,cp,weight_kg,tournament,result".to_string()];

    while idx < n_docs {
        match idx % 6 {
            // ── Pokédex entry: typed entity + quantities with units + habitat locus
            0 => {
                let sp = rng.pick(SPECIES);
                let (region, city) = rng.pick(PLACES);
                let temp = rng.range(-5, 38);
                let types = match sp.secondary {
                    Some(s) => format!("{}/{}", sp.primary, s),
                    None => sp.primary.to_string(),
                };
                let weak = WEAKNESS.iter().find(|(t, _)| *t == sp.primary).map(|(_, w)| *w).unwrap_or(&[]);
                let mut body = String::new();
                let _ = write!(
                    body,
                    "# Pokédex Entry — {}\n\n\
                     - **Species:** {}\n- **Type:** {}\n- **Height:** {} m\n- **Weight:** {} kg\n\
                     - **Habitat:** {} in the {} region\n- **Recorded habitat temperature:** {} °C\n\n\
                     ## Field notes\n\n\
                     The {} specimen observed near {} weighs {} kg and stands {} m tall. \
                     Field researchers logged an ambient temperature of {} °C during the survey. \
                     {} is a {} type and takes increased damage from {} attacks.\n",
                    sp.name, sp.name, types, sp.height_m, sp.weight_kg, city, region, temp,
                    sp.name, city, sp.weight_kg, sp.height_m, temp, sp.name, sp.primary,
                    weak.join(", ")
                );
                write_doc(&out, idx, &format!("pokedex_{}", sp.name.to_lowercase()), &body)?;
            }
            // ── Gym record: city→region hierarchy + leader/gym/type relations
            1 => {
                let (region, city) = rng.pick(PLACES);
                let leader = rng.pick(TRAINERS);
                let sp = rng.pick(SPECIES);
                let badge_count = rng.range(1, 9);
                let mut body = String::new();
                let _ = write!(
                    body,
                    "# Gym Record — {}\n\n\
                     - **City:** {}\n- **Region:** {}\n- **Leader:** {}\n- **Specialty type:** {}\n\
                     - **Badges awarded this season:** {}\n\n\
                     ## Summary\n\n\
                     {} operates the {} gym in the {} region and specialises in {} type battles. \
                     The gym awarded {} badges this season. {} registers {} as the gym's signature Pokémon.\n",
                    city, city, region, leader, sp.primary, badge_count,
                    leader, city, region, sp.primary, badge_count, leader, sp.name
                );
                write_doc(&out, idx, &format!("gym_{}", city.to_lowercase().replace(' ', "_")), &body)?;
            }
            // ── Battle report: the s-path substrate (trainer → battle → trainer) + dates + durations
            2 => {
                let a = rng.pick(TRAINERS);
                let b = loop {
                    let c = rng.pick(TRAINERS);
                    if c != a {
                        break c;
                    }
                };
                let (region, city) = rng.pick(PLACES);
                let (sp_a, sp_b) = (rng.pick(SPECIES), rng.pick(SPECIES));
                let (mv, mv_type, power) = *rng.pick(MOVES);
                let year = rng.range(2025, 2027);
                let month = rng.range(1, 13);
                let day = rng.range(1, 28);
                let minutes = rng.range(6, 41);
                let tour = rng.pick(TOURNAMENTS);
                let mut body = String::new();
                let _ = write!(
                    body,
                    "# Battle Report — {} vs {}\n\n\
                     - **Date:** {year}-{month:02}-{day:02}\n- **Venue:** {}\n- **Region:** {}\n\
                     - **Tournament:** {}\n- **Duration:** {} minutes\n\n\
                     ## Account\n\n\
                     {} faced {} at {} in the {} region during the {} on {year}-{month:02}-{day:02}. \
                     {} led with {} while {} answered with {}. The deciding move was {}, a {} type attack \
                     with {} base power. The match lasted {} minutes and {} took the win.\n",
                    a, b, city, region, tour, minutes,
                    a, b, city, region, tour,
                    a, sp_a.name, b, sp_b.name, mv, mv_type, power, minutes, a
                );
                write_doc(&out, idx, "battle_report", &body)?;
                roster_rows.push(format!(
                    "{a},{region},{city},{},{},{},{},{},{tour},win",
                    sp_a.name, sp_a.primary, rng.range(50, 101), rng.range(200, 900), sp_a.weight_kg
                ));
                roster_rows.push(format!(
                    "{b},{region},{city},{},{},{},{},{},{tour},loss",
                    sp_b.name, sp_b.primary, rng.range(50, 101), rng.range(200, 900), sp_b.weight_kg
                ));
            }
            // ── Tournament notice: temporal loci + venue geography + regulation reference
            3 => {
                let tour = rng.pick(TOURNAMENTS);
                let (region, city) = rng.pick(PLACES);
                let year = rng.range(2025, 2027);
                let month = rng.range(1, 13);
                let q = quarter(month);
                let entrants = rng.range(32, 257);
                let fee = rng.range(10, 80);
                let mut body = String::new();
                let _ = write!(
                    body,
                    "# Tournament Notice — {} {year}\n\n\
                     - **Series:** Q{q} {year}\n- **Venue:** {}\n- **Region:** {}\n\
                     - **Entrants:** {}\n- **Entry fee:** {} credits\n- **Regulation set:** Series {q}\n\n\
                     ## Announcement\n\n\
                     The {} will be held at {} in the {} region in Q{q} {year}. \
                     Registration closes {year}-{month:02}-15 and {} entrants are expected. \
                     Teams must comply with the Series {q} regulation set. The entry fee is {} credits.\n",
                    tour, city, region, entrants, fee,
                    tour, city, region, entrants, fee
                );
                write_doc(&out, idx, "tournament_notice", &body)?;
            }
            // ── Regulation clause: numeric constraints + explicit negation/hedging
            4 => {
                let q = rng.range(1, 5);
                let year = rng.range(2025, 2027);
                let team = rng.range(4, 7);
                let bring = team - rng.range(1, 3);
                let timer = rng.range(15, 31);
                let level_cap = rng.range(50, 101);
                let weight_cap = rng.range(300, 601);
                let sp = rng.pick(SPECIES);
                let banned = rng.pick(SPECIES);
                let mut body = String::new();
                let _ = write!(
                    body,
                    "# Competition Regulations — Series {q} {year}\n\n\
                     ## 1. Team composition\n\n\
                     Each trainer registers a team of {team} Pokémon and brings {bring} to each battle. \
                     No Pokémon above level {level_cap} is permitted. A Pokémon heavier than {weight_cap} kg \
                     may not be entered in the doubles bracket.\n\n\
                     ## 2. Restrictions\n\n\
                     {} is not permitted in Series {q} play. {} does not qualify for the open division. \
                     Restricted legendaries are reportedly under review and may be allowed in a later series.\n\n\
                     ## 3. Timing\n\n\
                     The battle timer is {timer} minutes. A match that exceeds {timer} minutes is decided on \
                     remaining team count. Registration closes 14 days before the event.\n",
                    banned.name, sp.name
                );
                write_doc(&out, idx, &format!("regulations_series{q}_{year}"), &body)?;
            }
            // ── Habitat survey: spatial + quantities + species membership
            _ => {
                let (region, city) = rng.pick(PLACES);
                let sp1 = rng.pick(SPECIES);
                let sp2 = rng.pick(SPECIES);
                let elevation = rng.range(5, 2400);
                let temp = rng.range(-12, 36);
                let rainfall = rng.range(200, 2600);
                let year = rng.range(2025, 2027);
                let month = rng.range(1, 13);
                let mut body = String::new();
                let _ = write!(
                    body,
                    "# Habitat Survey — {} ({})\n\n\
                     - **Region:** {}\n- **Survey date:** {year}-{month:02}-08\n\
                     - **Elevation:** {} m\n- **Mean temperature:** {} °C\n- **Annual rainfall:** {} mm\n\n\
                     ## Observations\n\n\
                     The survey team recorded {} and {} near {} in the {} region at an elevation of {} m. \
                     Mean temperature was {} °C with {} mm of annual rainfall. \
                     {} was not observed above {} m during this survey window.\n",
                    city, region, region, elevation, temp, rainfall,
                    sp1.name, sp2.name, city, region, elevation, temp, rainfall, sp2.name, elevation
                );
                write_doc(&out, idx, "habitat_survey", &body)?;
            }
        }
        idx += 1;
    }

    // relational companion: one row per roster entry, same vocabulary as the prose
    std::fs::write(out.join("rosters.csv"), roster_rows.join("\n") + "\n")?;

    println!("wrote {} documents + rosters.csv ({} rows) → {}", n_docs, roster_rows.len() - 1, out.display());
    println!("doc types: pokedex, gym record, battle report, tournament notice, regulations, habitat survey");
    Ok(())
}