kcode-kmap-size 0.1.1

Approximate the token footprint of current Kweb Kmap node text
Documentation
use std::{path::PathBuf, sync::Arc};

use chrono::{TimeZone, Utc};
use kcode_kmap_size::{KmapSize, measure, render};
use kcode_kweb_db::{Config, KwebDb, NodeData, NoopGossip, Owner, Provenance, WriterId};
use uuid::Uuid;

struct Fixture {
    root: PathBuf,
    signing_key: [u8; 32],
}

impl Fixture {
    fn new() -> Self {
        Self {
            root: std::env::temp_dir().join(format!("kcode-kmap-size-{}", Uuid::new_v4())),
            signing_key: [19; 32],
        }
    }

    fn config(&self) -> Config {
        Config {
            signing_key: self.signing_key,
            writers_by_priority: vec![WriterId::from_signing_key(&self.signing_key)],
            gossip: Arc::new(NoopGossip),
        }
    }

    fn open(&self) -> KwebDb {
        KwebDb::open(&self.root, self.config()).unwrap()
    }

    fn provenance(label: &str) -> Provenance {
        Provenance {
            author: "kcode-kmap-size-test".into(),
            source: "integration-test".into(),
            source_created_at: Utc.with_ymd_and_hms(2026, 7, 26, 0, 0, 0).unwrap(),
            data: label.into(),
        }
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        if self.root.exists() {
            std::fs::remove_dir_all(&self.root).unwrap();
        }
    }
}

fn node(short_name: &str, short_description: &str, long_description: &str) -> NodeData {
    NodeData {
        short_name: short_name.into(),
        short_description: short_description.into(),
        long_description: long_description.into(),
        owner: Owner::SelfNode,
        fixed_connections: Vec::new(),
        recent_connections: Vec::new(),
        objects: Vec::new(),
    }
}

#[test]
fn empty_kweb_has_a_zero_size_estimate() {
    let fixture = Fixture::new();
    drop(fixture.open());

    assert_eq!(
        measure(&fixture.root, fixture.config()).unwrap(),
        KmapSize {
            node_count: 0,
            full_node_characters: 0,
            full_node_words: 0,
            full_node_tokens: 0,
            long_description_characters: 0,
            long_description_words: 0,
            long_description_tokens: 0,
        }
    );
}

#[test]
fn measurement_counts_current_node_text_only() {
    let fixture = Fixture::new();
    let database = fixture.open();
    let mut initial = database
        .start_transaction(Fixture::provenance("initial nodes"))
        .unwrap();
    let updated_id = initial
        .create_node(node(
            "Obsolete",
            "This revision is historical",
            "A deliberately enormous historical description that must not be counted.",
        ))
        .unwrap();
    initial
        .create_object(vec![b'x'; 1024 * 1024])
        .expect("objects do not contribute to the estimate");
    initial.finalize().unwrap();

    let mut current = database
        .start_transaction(Fixture::provenance("current nodes"))
        .unwrap();
    current
        .update_node(
            updated_id,
            node("Updated", "Current only", "Newest long text"),
        )
        .unwrap();
    current.create_node(node("🦀", "", "naïve café")).unwrap();
    current.finalize().unwrap();
    drop(database);

    let size = measure(&fixture.root, fixture.config()).unwrap();
    assert_eq!(
        size,
        KmapSize {
            node_count: 2,
            full_node_characters: 46,
            full_node_words: 9,
            full_node_tokens: 12,
            long_description_characters: 26,
            long_description_words: 5,
            long_description_tokens: 7,
        }
    );
}

#[test]
fn measurement_ignores_unrelated_files_in_the_node_tree() {
    let fixture = Fixture::new();
    let database = fixture.open();
    let mut transaction = database
        .start_transaction(Fixture::provenance("one node"))
        .unwrap();
    transaction
        .create_node(node("One", "Two", "Three"))
        .unwrap();
    transaction.finalize().unwrap();
    drop(database);

    std::fs::write(fixture.root.join("nodes").join("README"), "not a node").unwrap();
    let unrelated_shard = fixture.root.join("nodes").join("zz");
    std::fs::create_dir_all(&unrelated_shard).unwrap();
    std::fs::write(unrelated_shard.join("ignored.txt"), "not a node").unwrap();

    let size = measure(&fixture.root, fixture.config()).unwrap();
    assert_eq!(size.node_count, 1);
    assert_eq!(size.full_node_characters, 11);
    assert_eq!(size.full_node_words, 3);
}

#[test]
fn missing_kweb_root_returns_contextual_error() {
    let fixture = Fixture::new();
    let error = measure(&fixture.root, fixture.config()).unwrap_err();
    assert!(error.to_string().contains("does not exist"), "{error:#}");
}

#[test]
fn report_is_grouped_and_discloses_its_approximation() {
    let report = render(&KmapSize {
        node_count: 12_345,
        full_node_characters: 4_938_268,
        full_node_words: 987_654,
        full_node_tokens: 1_234_567,
        long_description_characters: 3_950_616,
        long_description_words: 800_000,
        long_description_tokens: 987_654,
    });

    assert!(report.contains("Nodes: 12,345"));
    assert!(report.contains("Full node text: ~1,234,567 tokens"));
    assert!(report.contains("Long descriptions only: ~987,654 tokens"));
    assert!(report.contains("one token per 4 Unicode characters"));
    assert!(report.contains("history, provenance, connections, and other tables are excluded"));
}