Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 116 additions & 16 deletions src/archive/parent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use crate::{
types::{Chunk, ChunkContext, ChunkSummary, ParentChunk},
Error, Result,
};
use rusqlite::{params, OptionalExtension};
use rusqlite::{params, Connection, OptionalExtension};
use std::collections::HashMap;

impl Archive {
pub fn get_parent_chunk(&self, parent_id: &str) -> Result<Option<ParentChunk>> {
Expand Down Expand Up @@ -64,21 +65,13 @@ impl Archive {
}

pub fn all_parent_chunks(&self) -> Result<Vec<ParentChunk>> {
let mut stmt = self
.conn
.prepare("SELECT parent_id FROM parent_chunks ORDER BY started_at, parent_id")
.map_err(Error::Sql)?;
let ids = stmt
.query_map([], |row| row.get::<_, String>(0))
.map_err(Error::Sql)?
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::Sql)?;
ids.into_iter()
.map(|id| {
self.get_parent_chunk(&id)
.and_then(|parent| parent.ok_or(Error::MissingChunk(id)))
})
.collect()
if !self.conn.is_autocommit() {
return load_all_parent_chunks(&self.conn);
}
let tx = self.conn.unchecked_transaction().map_err(Error::Sql)?;
let parents = load_all_parent_chunks(&tx)?;
tx.commit().map_err(Error::Sql)?;
Ok(parents)
}

pub(super) fn window_parent_ids(&self, chunk_id: &str) -> Result<Vec<String>> {
Expand Down Expand Up @@ -158,7 +151,114 @@ impl Archive {
}
}

fn load_all_parent_chunks(conn: &Connection) -> Result<Vec<ParentChunk>> {
let mut parents = {
let mut stmt = conn
.prepare(
"SELECT parent_id, chat_id, chat_name, started_at, ended_at,
text, message_count
FROM parent_chunks
ORDER BY started_at, parent_id",
)
.map_err(Error::Sql)?;
let rows = stmt
.query_map([], |row| {
Ok(ParentChunk {
parent_id: row.get(0)?,
chat_id: row.get(1)?,
chat_name: row.get(2)?,
started_at: row.get(3)?,
ended_at: row.get(4)?,
text: row.get(5)?,
message_count: row.get::<_, i64>(6)? as usize,
child_chunk_ids: Vec::new(),
})
})
.map_err(Error::Sql)?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::Sql)?
};
let children = {
let mut stmt = conn
.prepare(
"SELECT parent_id, chunk_id
FROM parent_chunk_children
ORDER BY parent_id, ordinal",
)
.map_err(Error::Sql)?;
let rows = stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.map_err(Error::Sql)?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::Sql)?
};
attach_parent_children(&mut parents, children)?;
Ok(parents)
}

fn attach_parent_children(
parents: &mut [ParentChunk],
children: impl IntoIterator<Item = (String, String)>,
) -> Result<()> {
let parent_indexes = parents
.iter()
.enumerate()
.map(|(index, parent)| (parent.parent_id.clone(), index))
.collect::<HashMap<_, _>>();
for (parent_id, chunk_id) in children {
let Some(index) = parent_indexes.get(&parent_id).copied() else {
return Err(Error::MissingChunk(parent_id));
};
parents[index].child_chunk_ids.push(chunk_id);
}
Ok(())
}

enum Neighbor {
Previous,
Next,
}

#[cfg(test)]
mod tests {
use super::*;

fn parent(parent_id: &str) -> ParentChunk {
ParentChunk {
parent_id: parent_id.to_string(),
chat_id: "chat-fixture".to_string(),
chat_name: "Synthetic room".to_string(),
started_at: "2026-01-01T00:00:00Z".to_string(),
ended_at: "2026-01-01T00:01:00Z".to_string(),
text: "synthetic text".to_string(),
message_count: 1,
child_chunk_ids: Vec::new(),
}
}

#[test]
fn bulk_parent_assembly_preserves_parent_and_child_order() {
let mut parents = vec![parent("parent-early"), parent("parent-late")];
let children = vec![
("parent-early".to_string(), "child-a".to_string()),
("parent-early".to_string(), "child-b".to_string()),
("parent-late".to_string(), "child-c".to_string()),
];

attach_parent_children(&mut parents, children).expect("attach children");

assert_eq!(parents[0].child_chunk_ids, ["child-a", "child-b"]);
assert_eq!(parents[1].child_chunk_ids, ["child-c"]);
}

#[test]
fn bulk_parent_load_reuses_caller_transaction() {
let dir = tempfile::tempdir().expect("tempdir");
let archive = Archive::open(&dir.path().join("archive.sqlite3")).expect("open archive");

let parents: Result<Vec<ParentChunk>> =
archive.in_transaction(|| archive.all_parent_chunks());

assert!(parents.expect("load parents inside transaction").is_empty());
}
}
22 changes: 14 additions & 8 deletions src/commands/index_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use anyhow::{Context, Result};
use katok::{
archive::Archive,
config::KatokConfig,
semantic::{index_semantic_live, planned_semantic_documents},
semantic::{index_semantic_live_for_parents, planned_semantic_documents_for_parents},
types::ParentChunk,
};
use std::path::Path;

Expand All @@ -20,23 +21,27 @@ pub(crate) fn run(
anyhow::bail!("archive is missing; run katok sync before katok index");
}
let archive = Archive::open(archive_path).context("open archive")?;
let chunks = archive.all_chunks().context("load chunks")?;
let candidate_chunks = archive.chunk_count().context("count chunks")?;
let parents = archive
.all_parent_chunks()
.context("load semantic parent windows")?;
if !dry_run {
return run_live_index(LiveIndexInput {
full,
dry_run,
json,
config,
archive: &archive,
parents: &parents,
semantic_dir,
candidate_chunks: chunks.len(),
candidate_chunks,
});
}
let documents = planned_semantic_documents(&archive, semantic_dir).context("plan documents")?;
let documents = planned_semantic_documents_for_parents(&parents, semantic_dir);
let payload = serde_json::json!({
"full": full,
"dry_run": dry_run,
"candidate_chunks": chunks.len(),
"candidate_chunks": candidate_chunks,
"written_documents": 0,
"embedding_calls": 0,
"documents": documents,
Expand All @@ -52,6 +57,7 @@ struct LiveIndexInput<'a> {
json: bool,
config: &'a KatokConfig,
archive: &'a Archive,
parents: &'a [ParentChunk],
semantic_dir: &'a Path,
candidate_chunks: usize,
}
Expand All @@ -62,17 +68,17 @@ fn run_live_index(input: LiveIndexInput<'_>) -> Result<()> {
.build()
.context("create semantic runtime")?;
let report = runtime
.block_on(index_semantic_live(
.block_on(index_semantic_live_for_parents(
input.archive,
input.parents,
input.semantic_dir,
input.config,
input.full,
))
.context("index semantic documents")?;
let generation = katok::semantic::current_generation(input.semantic_dir)
.context("resolve committed semantic generation")?;
let documents = planned_semantic_documents(input.archive, &generation)
.context("report semantic documents")?;
let documents = planned_semantic_documents_for_parents(input.parents, &generation);
let payload = serde_json::json!({
"full": input.full,
"dry_run": input.dry_run,
Expand Down
15 changes: 10 additions & 5 deletions src/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};

pub use live::{
committed_cursor, index_semantic_live, semantic_search_live_with_config, SemanticIndexReport,
STORE_DIR,
committed_cursor, index_semantic_live, index_semantic_live_for_parents,
semantic_search_live_with_config, SemanticIndexReport, STORE_DIR,
};
pub use mock::{
planned_semantic_documents, semantic_search, semantic_search_with_snippet,
write_semantic_documents, SemanticDocument,
planned_semantic_documents, planned_semantic_documents_for_parents, semantic_search,
semantic_search_with_snippet, write_semantic_documents, write_semantic_documents_for_parents,
SemanticDocument,
};

pub(crate) const CHUNK_SCHEMA_ID: &str = "katok-kakao-parent-window-v1";
Expand All @@ -27,14 +28,18 @@ pub fn semantic_source_dir(root: &std::path::Path) -> std::path::PathBuf {

pub fn archive_revision(archive: &Archive) -> Result<String> {
let parents = archive.all_parent_chunks()?;
Ok(archive_revision_for_parents(&parents))
}

pub(crate) fn archive_revision_for_parents(parents: &[crate::types::ParentChunk]) -> String {
let mut material = String::new();
for parent in parents {
material.push_str(&parent.parent_id);
material.push('\0');
material.push_str(&content_hash(&parent.text));
material.push('\0');
}
Ok(content_hash(&material))
content_hash(&material)
}

pub fn current_generation(root: &Path) -> Result<PathBuf> {
Expand Down
Loading
Loading