use std::path::Path;
use anyhow::{Context, Result};
use crate::output::{OutputMode, pretty_kv, pretty_section};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SemanticIndexState {
tables_ready: bool,
embeddings: usize,
deferred: bool,
}
fn ensure_semantic_index_state(conn: &rusqlite::Connection) -> Result<SemanticIndexState> {
bones_search::semantic::ensure_semantic_index_schema(conn)
.context("initialize semantic index schema")?;
let embeddings: i64 = conn
.query_row("SELECT COUNT(*) FROM item_embeddings", [], |row| row.get(0))
.context("count semantic embeddings after rebuild")?;
let projection_cursor = bones_core::db::query::get_projection_cursor(conn)
.context("read projection cursor after rebuild")?;
let semantic_cursor: (i64, Option<String>) = conn
.query_row(
"SELECT last_event_offset, last_event_hash FROM semantic_meta WHERE id = 1",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.context("read semantic cursor after rebuild")?;
Ok(SemanticIndexState {
tables_ready: true,
embeddings: usize::try_from(embeddings).unwrap_or(0),
deferred: semantic_cursor != projection_cursor,
})
}
pub fn run_rebuild(project_root: &Path, _incremental: bool, output: OutputMode) -> Result<()> {
let bones_dir = project_root.join(".bones");
let events_dir = bones_dir.join("events");
let db_path = bones_dir.join("bones.db");
let cache_path = bones_dir.join("cache/events.bin");
let (db_report, conn) = if _incremental {
let apply = bones_core::db::incremental::incremental_apply(&events_dir, &db_path, false)?;
let conn = bones_core::db::open_projection(&db_path)?;
let item_count: usize =
conn.query_row("SELECT COUNT(*) FROM items", [], |row| row.get(0))?;
(
bones_core::db::rebuild::RebuildReport {
event_count: apply.events_applied,
item_count,
elapsed: apply.elapsed,
shard_count: apply.shards_scanned,
fts5_rebuilt: false,
},
conn,
)
} else {
let report = bones_core::db::rebuild::rebuild(&events_dir, &db_path)?;
let conn = bones_core::db::open_projection(&db_path)?;
(report, conn)
};
let cache_stats = bones_core::cache::rebuild_cache(&events_dir, &cache_path)?;
let semantic_state = ensure_semantic_index_state(&conn)?;
match output {
OutputMode::Json => {
let val = serde_json::json!({
"projection_events": db_report.event_count,
"projection_items": db_report.item_count,
"shards": db_report.shard_count,
"cache_events": cache_stats.total_events,
"cache_bytes": cache_stats.file_size_bytes,
"semantic_tables_ready": semantic_state.tables_ready,
"semantic_embeddings": semantic_state.embeddings,
"semantic_deferred": semantic_state.deferred,
});
println!("{}", serde_json::to_string_pretty(&val)?);
}
OutputMode::Text => {
println!(
"rebuild projection_events={} items={} shards={} cache_events={} cache_bytes={} semantic_tables_ready={} semantic_embeddings={} semantic_deferred={}",
db_report.event_count,
db_report.item_count,
db_report.shard_count,
cache_stats.total_events,
cache_stats.file_size_bytes,
semantic_state.tables_ready,
semantic_state.embeddings,
semantic_state.deferred,
);
}
OutputMode::Pretty => {
let stdout = std::io::stdout();
let mut w = stdout.lock();
pretty_section(&mut w, "Rebuild Complete")?;
pretty_kv(
&mut w,
"Projection events",
db_report.event_count.to_string(),
)?;
pretty_kv(&mut w, "Items", db_report.item_count.to_string())?;
pretty_kv(&mut w, "Shards", db_report.shard_count.to_string())?;
pretty_kv(&mut w, "Cache events", cache_stats.total_events.to_string())?;
pretty_kv(
&mut w,
"Cache bytes",
cache_stats.file_size_bytes.to_string(),
)?;
pretty_kv(
&mut w,
"Semantic tables",
if semantic_state.tables_ready {
"ready".to_string()
} else {
"missing".to_string()
},
)?;
pretty_kv(
&mut w,
"Semantic embeddings",
semantic_state.embeddings.to_string(),
)?;
pretty_kv(
&mut w,
"Semantic indexing",
if semantic_state.deferred {
"deferred (run semantic search to populate embeddings)".to_string()
} else {
"up-to-date".to_string()
},
)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use bones_core::event::data::CreateData;
use bones_core::event::types::EventType;
use bones_core::event::{Event, EventData, writer};
use bones_core::model::item::{Kind, Size, Urgency};
use bones_core::model::item_id::ItemId;
use bones_core::shard::ShardManager;
use rusqlite::Connection;
use std::collections::BTreeMap;
fn setup_project_with_single_event() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let bones_dir = dir.path().join(".bones");
let shard_mgr = ShardManager::new(&bones_dir);
shard_mgr.ensure_dirs().expect("ensure bones dirs");
shard_mgr.init().expect("init shard");
let mut create = Event {
wall_ts_us: 1000,
agent: "test-agent".into(),
itc: "itc:AQ".into(),
parents: vec![],
event_type: EventType::Create,
item_id: ItemId::new_unchecked("bn-001"),
data: EventData::Create(CreateData {
title: "Semantic rebuild coverage".into(),
kind: Kind::Task,
size: Some(Size::M),
urgency: Urgency::Default,
labels: vec!["semantic".into()],
parent: None,
causation: None,
description: Some("Verify rebuild semantic schema behavior".into()),
extra: BTreeMap::new(),
}),
event_hash: String::new(),
};
writer::write_event(&mut create).expect("compute event hash");
let line = writer::write_line(&create).expect("serialize event line");
let (year, month) = shard_mgr
.active_shard()
.expect("active shard")
.expect("exists");
shard_mgr
.append_raw(year, month, &line)
.expect("append create event");
dir
}
#[test]
fn rebuild_materializes_semantic_tables() {
let dir = setup_project_with_single_event();
run_rebuild(dir.path(), false, OutputMode::Json).expect("rebuild should succeed");
let db_path = dir.path().join(".bones").join("bones.db");
let conn = Connection::open(db_path).expect("open rebuilt db");
let table_count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name IN ('item_embeddings', 'semantic_meta')",
[],
|row| row.get(0),
)
.expect("count semantic tables");
assert_eq!(table_count, 2, "expected semantic tables to exist");
}
#[test]
fn rebuild_reports_semantic_index_as_deferred_after_event_replay() {
let dir = setup_project_with_single_event();
run_rebuild(dir.path(), false, OutputMode::Json).expect("rebuild should succeed");
let db_path = dir.path().join(".bones").join("bones.db");
let conn = Connection::open(db_path).expect("open rebuilt db");
let state = ensure_semantic_index_state(&conn).expect("semantic state");
assert!(state.tables_ready);
assert_eq!(state.embeddings, 0);
assert!(
state.deferred,
"fresh rebuild should defer embedding generation until semantic queries run"
);
}
}