From 39befe97462d6aa3db69cd13a3cf4ca16bf292b8 Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Tue, 12 May 2026 12:43:25 +0900 Subject: [PATCH 1/3] refactor(store): extract sqlite storage crate --- Cargo.lock | 19 +++++-- Cargo.toml | 2 + README.md | 3 +- crates/cli/Cargo.toml | 2 +- crates/cli/tests/query_protocol.rs | 2 +- crates/cli/tests/read_scale_bench.rs | 2 +- crates/cli/tests/status.rs | 2 +- crates/core/Cargo.toml | 1 + crates/core/src/index.rs | 3 +- crates/core/src/init/tests.rs | 2 +- crates/core/src/init/types.rs | 2 +- crates/core/src/init/workflow.rs | 2 +- crates/core/src/project/remove.rs | 2 +- crates/core/src/project/tests.rs | 2 +- crates/core/src/query.rs | 2 +- crates/index/Cargo.toml | 3 +- crates/index/src/engine.rs | 19 +++---- crates/index/src/lib.rs | 19 +++---- crates/query/Cargo.toml | 2 +- crates/query/src/query.rs | 11 ++--- crates/query/src/query/insights.rs | 2 +- crates/query/src/tests.rs | 6 +-- crates/store/Cargo.toml | 17 +++++++ crates/{index => store}/src/derived_data.rs | 16 +++--- crates/{index => store}/src/evidence.rs | 0 crates/{index => store}/src/index_db.rs | 49 ++++++++++++++++--- .../src/index_db/migrations.rs | 0 .../{index => store}/src/index_db/schema.rs | 4 +- crates/store/src/lib.rs | 17 +++++++ .../src/policy/active_time.rs | 0 .../src/policy/file_access.rs | 0 crates/{index => store}/src/policy/mod.rs | 0 crates/{index => store}/src/policy/search.rs | 0 crates/{index => store}/src/policy/shell.rs | 0 crates/{index => store}/src/test_support.rs | 0 crates/{index => store}/src/turn_metrics.rs | 42 ++++++++-------- crates/test-utils/Cargo.toml | 2 +- crates/test-utils/src/lib.rs | 2 +- docs/todo.md | 5 +- 39 files changed, 170 insertions(+), 94 deletions(-) create mode 100644 crates/store/Cargo.toml rename crates/{index => store}/src/derived_data.rs (98%) rename crates/{index => store}/src/evidence.rs (100%) rename crates/{index => store}/src/index_db.rs (96%) rename crates/{index => store}/src/index_db/migrations.rs (100%) rename crates/{index => store}/src/index_db/schema.rs (99%) create mode 100644 crates/store/src/lib.rs rename crates/{index => store}/src/policy/active_time.rs (100%) rename crates/{index => store}/src/policy/file_access.rs (100%) rename crates/{index => store}/src/policy/mod.rs (100%) rename crates/{index => store}/src/policy/search.rs (100%) rename crates/{index => store}/src/policy/shell.rs (100%) rename crates/{index => store}/src/test_support.rs (100%) rename crates/{index => store}/src/turn_metrics.rs (92%) diff --git a/Cargo.lock b/Cargo.lock index 223271e..84b8991 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,10 +221,10 @@ dependencies = [ "anyhow", "clap", "darc-core", - "darc-index", "darc-paths", "darc-rollout", "darc-rollout-audit", + "darc-store", "darc-test-utils", "fs2", "notify", @@ -243,6 +243,7 @@ dependencies = [ "darc-index", "darc-paths", "darc-query", + "darc-store", "darc-sync", "darc-test-utils", "directories", @@ -261,6 +262,7 @@ dependencies = [ "anyhow", "darc-paths", "darc-rollout", + "darc-store", "rusqlite", "serde_json", "thiserror", @@ -280,9 +282,9 @@ name = "darc-query" version = "0.1.5" dependencies = [ "anyhow", - "darc-index", "darc-paths", "darc-rollout", + "darc-store", "darc-test-utils", "glob", "regex", @@ -319,6 +321,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "darc-store" +version = "0.1.5" +dependencies = [ + "anyhow", + "darc-paths", + "darc-rollout", + "rusqlite", + "serde_json", +] + [[package]] name = "darc-sync" version = "0.1.5" @@ -337,9 +350,9 @@ name = "darc-test-utils" version = "0.1.5" dependencies = [ "anyhow", - "darc-index", "darc-paths", "darc-rollout", + "darc-store", "rusqlite", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index c830789..41c440f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/query", "crates/rollout-audit", "crates/rollout", + "crates/store", "crates/sync", "crates/test-utils", ] @@ -31,6 +32,7 @@ darc-paths = { path = "crates/paths" } darc-query = { path = "crates/query" } darc-rollout-audit = { path = "crates/rollout-audit" } darc-rollout = { path = "crates/rollout" } +darc-store = { path = "crates/store" } darc-sync = { path = "crates/sync" } darc-test-utils = { path = "crates/test-utils" } # External Dependencies diff --git a/README.md b/README.md index 12ad05d..fd65306 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,8 @@ Darc is split into focused Rust crates: - `crates/cli`: command-line surface. - `darc-core`: thin facade and orchestration layer. - `darc-sync`: source discovery, sync planning, and archive copy execution. -- `darc-index`: normalized ingestion, SQLite schema, migrations, and indexing metrics. +- `darc-store`: SQLite schema, migrations, and derived-index analytics shared by indexing and query. +- `darc-index`: normalized archive ingestion and duplicate resolution. - `darc-query`: read-only query, search, and stats over indexed data. - `darc-rollout`: provider transcript models and parsers. - `darc-paths`, `darc-rollout-audit`, and `darc-test-utils`: shared support, maintainer audits, and tests. diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9ee5d1d..3a4aee6 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -32,9 +32,9 @@ serde_json = { workspace = true } notify = { workspace = true } [dev-dependencies] -darc-index = { workspace = true } darc-paths = { workspace = true } darc-rollout = { workspace = true } +darc-store = { workspace = true } darc-test-utils = { workspace = true } rusqlite = { workspace = true } toml = { workspace = true } diff --git a/crates/cli/tests/query_protocol.rs b/crates/cli/tests/query_protocol.rs index efd581a..a4b87c2 100644 --- a/crates/cli/tests/query_protocol.rs +++ b/crates/cli/tests/query_protocol.rs @@ -8,8 +8,8 @@ use anyhow::{Context, Result}; use darc_core::query::{ DEFAULT_QUERY_PAGE_LIMIT, DEFAULT_SESSION_BUNDLE_TURN_LIMIT, DEFAULT_TURN_STEP_LIMIT, }; -use darc_index::open_index_database; use darc_paths::SourceKind; +use darc_store::open_index_database; use darc_test_utils::{ IndexedSessionFixture, IndexedTurnFixture, insert_indexed_session, insert_indexed_turn, unique_test_dir, write_file, diff --git a/crates/cli/tests/read_scale_bench.rs b/crates/cli/tests/read_scale_bench.rs index 0d41260..24d9b0e 100644 --- a/crates/cli/tests/read_scale_bench.rs +++ b/crates/cli/tests/read_scale_bench.rs @@ -6,8 +6,8 @@ use std::{ }; use anyhow::{Context, Result, bail}; -use darc_index::open_index_database; use darc_paths::SourceKind; +use darc_store::open_index_database; use darc_test_utils::{ IndexedSessionFixture, IndexedTurnFixture, insert_indexed_session, insert_indexed_turn, unique_test_dir, write_file, diff --git a/crates/cli/tests/status.rs b/crates/cli/tests/status.rs index 55d77f2..fc93304 100644 --- a/crates/cli/tests/status.rs +++ b/crates/cli/tests/status.rs @@ -1,8 +1,8 @@ use std::{fs, path::Path, process::Command}; use anyhow::{Context, Result}; -use darc_index::open_index_database; use darc_paths::SourceKind; +use darc_store::open_index_database; use darc_test_utils::{ IndexedSessionFixture, IndexedTurnFixture, insert_indexed_session, insert_indexed_turn, unique_test_dir, write_file, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 66be1bb..13d1935 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true darc-query = { workspace = true } darc-index = { workspace = true } darc-paths = { workspace = true } +darc-store = { workspace = true } darc-sync = { workspace = true } anyhow = { workspace = true } directories = { workspace = true } diff --git a/crates/core/src/index.rs b/crates/core/src/index.rs index 289e604..6c528e1 100644 --- a/crates/core/src/index.rs +++ b/crates/core/src/index.rs @@ -5,9 +5,10 @@ use std::{ }; use anyhow::{Context, Result}; -use darc_index::{INDEX_DB_FILE_NAME, ProjectIndexRequest, index_project_archived_sessions}; pub use darc_index::{IndexReport, SkippedCodexRollout, SkippedRollout}; +use darc_index::{ProjectIndexRequest, index_project_archived_sessions}; use darc_paths::SourceKind; +use darc_store::INDEX_DB_FILE_NAME; use crate::{ active_project::{ActiveProject, load_active_project}, diff --git a/crates/core/src/init/tests.rs b/crates/core/src/init/tests.rs index 2bf2895..86b6b50 100644 --- a/crates/core/src/init/tests.rs +++ b/crates/core/src/init/tests.rs @@ -6,7 +6,7 @@ use std::{ }; use anyhow::Result; -use darc_index::INDEX_DB_FILE_NAME; +use darc_store::INDEX_DB_FILE_NAME; use super::{ config_io::{ExistingConfig, build_config, load_existing_config}, diff --git a/crates/core/src/init/types.rs b/crates/core/src/init/types.rs index 342f664..3d6c31d 100644 --- a/crates/core/src/init/types.rs +++ b/crates/core/src/init/types.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::{Context, Result}; -use darc_index::INDEX_DB_FILE_NAME; +use darc_store::INDEX_DB_FILE_NAME; use crate::{ config::{ProjectConfig, SharedConfig, SourceKind}, diff --git a/crates/core/src/init/workflow.rs b/crates/core/src/init/workflow.rs index 9fe90af..f5b37cc 100644 --- a/crates/core/src/init/workflow.rs +++ b/crates/core/src/init/workflow.rs @@ -1,7 +1,7 @@ use std::{env, fs, path::PathBuf}; use anyhow::{Context, Result, bail}; -use darc_index::ensure_index_database; +use darc_store::ensure_index_database; use directories::BaseDirs; use super::{ diff --git a/crates/core/src/project/remove.rs b/crates/core/src/project/remove.rs index 551d320..c9301e9 100644 --- a/crates/core/src/project/remove.rs +++ b/crates/core/src/project/remove.rs @@ -1,7 +1,7 @@ use std::{fs, path::Path}; use anyhow::{Context, Result, bail}; -use darc_index::{INDEX_DB_FILE_NAME, count_project_index_rows_read_only, open_index_database}; +use darc_store::{INDEX_DB_FILE_NAME, count_project_index_rows_read_only, open_index_database}; use rusqlite::params; use super::{ diff --git a/crates/core/src/project/tests.rs b/crates/core/src/project/tests.rs index f4e1b9f..7c70409 100644 --- a/crates/core/src/project/tests.rs +++ b/crates/core/src/project/tests.rs @@ -6,7 +6,7 @@ use std::{ }; use anyhow::{Context, Result}; -use darc_index::{INDEX_DB_FILE_NAME, open_index_database}; +use darc_store::{INDEX_DB_FILE_NAME, open_index_database}; use darc_test_utils::init_git_repo; use rusqlite::{Connection, params}; diff --git a/crates/core/src/query.rs b/crates/core/src/query.rs index dbbe47e..be5d21d 100644 --- a/crates/core/src/query.rs +++ b/crates/core/src/query.rs @@ -5,7 +5,6 @@ use std::{ }; use anyhow::{Context, Result, bail}; -use darc_index::INDEX_DB_FILE_NAME; use darc_paths::SourceKind; pub use darc_query::{ ActiveProjectSummary, DEFAULT_MATCHED_PATH_LIMIT, DEFAULT_QUERY_PAGE_LIMIT, @@ -33,6 +32,7 @@ use darc_query::{ query_session_turn_details as query_project_session_turn_details, query_turn_detail, query_turn_insights, query_workspace_insights, }; +use darc_store::INDEX_DB_FILE_NAME; use serde_json::{Value as JsonValue, json}; use thiserror::Error; diff --git a/crates/index/Cargo.toml b/crates/index/Cargo.toml index 7279f44..43bc40d 100644 --- a/crates/index/Cargo.toml +++ b/crates/index/Cargo.toml @@ -7,11 +7,12 @@ authors.workspace = true repository.workspace = true [features] -test-support = [] +test-support = ["darc-store/test-support"] [dependencies] darc-paths = { workspace = true } darc-rollout = { workspace = true } +darc-store = { workspace = true } anyhow = { workspace = true } rusqlite = { workspace = true } serde_json = { workspace = true } diff --git a/crates/index/src/engine.rs b/crates/index/src/engine.rs index 9743e73..5f36f63 100644 --- a/crates/index/src/engine.rs +++ b/crates/index/src/engine.rs @@ -25,22 +25,17 @@ use darc_rollout::{ }, model::NormalizedTurn as CodexTurn, }; +#[cfg(test)] +use darc_store::INDEX_DB_FILE_NAME; +use darc_store::{ + TurnDerivedContext, insert_turn_derived_records, open_index_database, + schema::{INSERT_SESSION_SQL, INSERT_TURN_SQL}, + summarize_turn_metrics, +}; use rusqlite::{Connection, Transaction, params}; use thiserror::Error; use walkdir::WalkDir; -use crate::{ - derived_data::{TurnDerivedContext, insert_turn_derived_records}, - index_db::{ - open_index_database, - schema::{INSERT_SESSION_SQL, INSERT_TURN_SQL}, - }, - turn_metrics::summarize_turn_metrics, -}; - -/// Stores the shared SQLite index filename inside the darc workspace root. -pub const INDEX_DB_FILE_NAME: &str = "index.sqlite"; - /// Parses one Codex rollout file into user-visible turns. #[cfg(test)] pub(crate) fn parse_codex_rollout(path: &Path) -> Result { diff --git a/crates/index/src/lib.rs b/crates/index/src/lib.rs index c8ae67e..f0b1c8d 100644 --- a/crates/index/src/lib.rs +++ b/crates/index/src/lib.rs @@ -1,18 +1,15 @@ -mod derived_data; mod engine; -pub mod evidence; -mod index_db; -pub mod policy; -#[cfg(any(test, feature = "test-support"))] -pub mod test_support; #[cfg(test)] mod tests; -mod turn_metrics; +#[cfg(feature = "test-support")] +pub use darc_store::test_support; +pub use darc_store::{ + INDEX_DB_FILE_NAME, count_project_index_rows_read_only, ensure_index_database, evidence, + open_existing_index_database, open_index_database, open_index_database_read_only, policy, + schema, +}; pub use engine::{ - INDEX_DB_FILE_NAME, IndexReport, ProjectIndexRequest, SkippedCodexRollout, SkippedRollout, + IndexReport, ProjectIndexRequest, SkippedCodexRollout, SkippedRollout, index_project_archived_codex_turns, index_project_archived_sessions, }; -pub use index_db::{ - count_project_index_rows_read_only, ensure_index_database, open_index_database, -}; diff --git a/crates/query/Cargo.toml b/crates/query/Cargo.toml index c5e9148..13f4bd2 100644 --- a/crates/query/Cargo.toml +++ b/crates/query/Cargo.toml @@ -7,9 +7,9 @@ authors.workspace = true repository.workspace = true [dependencies] -darc-index = { workspace = true } darc-paths = { workspace = true } darc-rollout = { workspace = true } +darc-store = { workspace = true } anyhow = { workspace = true } glob = { workspace = true } regex = { workspace = true } diff --git a/crates/query/src/query.rs b/crates/query/src/query.rs index 56e372f..6a01740 100644 --- a/crates/query/src/query.rs +++ b/crates/query/src/query.rs @@ -9,10 +9,10 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; pub use bundles::query_project_session_bundle; -pub use darc_index::evidence::EvidenceField as SearchEvidenceField; -use darc_index::open_index_database; use darc_paths::SourceKind; use darc_rollout::model::{NormalizedTokenUsage, NormalizedTurnStatus, NormalizedTurnStep}; +pub use darc_store::evidence::EvidenceField as SearchEvidenceField; +use darc_store::open_existing_index_database as open_existing_store_index_database; pub use files::{display_path_for_access, query_project_files, query_project_session_files}; #[cfg(test)] pub(crate) use insights::{build_project_insights, build_workspace_insights}; @@ -773,12 +773,9 @@ pub struct ProjectInsights { pub total_time_ms: u64, } -/// Opens one existing index database while still applying lightweight migrations. +/// Opens one existing index database while still applying supported storage migrations. pub(crate) fn open_existing_index_database(index_db_path: &Path) -> Result { - if !index_db_path.exists() { - bail!("index database not found at {}", index_db_path.display()); - } - open_index_database(index_db_path) + open_existing_store_index_database(index_db_path) } /// Parses one provider value stored in SQLite back into a source kind. diff --git a/crates/query/src/query/insights.rs b/crates/query/src/query/insights.rs index efd8acf..da1b438 100644 --- a/crates/query/src/query/insights.rs +++ b/crates/query/src/query/insights.rs @@ -4,8 +4,8 @@ use std::{ }; use anyhow::{Context, Result}; -use darc_index::policy::{extract_shell_command, should_include_turn_in_active_time}; use darc_paths::{SourceKind, normalize_access_path_candidate}; +use darc_store::policy::{extract_shell_command, should_include_turn_in_active_time}; use rusqlite::Connection; use super::{ diff --git a/crates/query/src/tests.rs b/crates/query/src/tests.rs index f2d5313..76d8986 100644 --- a/crates/query/src/tests.rs +++ b/crates/query/src/tests.rs @@ -5,7 +5,9 @@ use std::{ }; use anyhow::{Context, Result}; -use darc_index::{ +use darc_paths::SourceKind; +use darc_rollout::model::NormalizedTurnStep; +use darc_store::{ evidence::EvidenceField, open_index_database, policy::{ @@ -14,8 +16,6 @@ use darc_index::{ should_include_turn_in_active_time, }, }; -use darc_paths::SourceKind; -use darc_rollout::model::NormalizedTurnStep; use darc_test_utils::{ IndexedSessionFixture, IndexedTurnFixture, insert_indexed_session, insert_indexed_turn, seed_legacy_codex_index, unique_test_dir, diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml new file mode 100644 index 0000000..81250f5 --- /dev/null +++ b/crates/store/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "darc-store" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[features] +test-support = [] + +[dependencies] +darc-paths = { workspace = true } +darc-rollout = { workspace = true } +anyhow = { workspace = true } +rusqlite = { workspace = true } +serde_json = { workspace = true } diff --git a/crates/index/src/derived_data.rs b/crates/store/src/derived_data.rs similarity index 98% rename from crates/index/src/derived_data.rs rename to crates/store/src/derived_data.rs index 1b4a5aa..150580d 100644 --- a/crates/index/src/derived_data.rs +++ b/crates/store/src/derived_data.rs @@ -24,17 +24,17 @@ struct TurnEvidenceRecord { } /// Stores the canonical turn identity and text needed to derive search analytics rows. -pub(crate) struct TurnDerivedContext<'a> { - pub(crate) project_id: &'a str, - pub(crate) provider: SourceKind, - pub(crate) session_id: &'a str, - pub(crate) turn_ordinal: i64, - pub(crate) user_message: &'a str, - pub(crate) final_answer_text: Option<&'a str>, +pub struct TurnDerivedContext<'a> { + pub project_id: &'a str, + pub provider: SourceKind, + pub session_id: &'a str, + pub turn_ordinal: i64, + pub user_message: &'a str, + pub final_answer_text: Option<&'a str>, } /// Inserts one turn's derived analytics and search records into SQLite. -pub(crate) fn insert_turn_derived_records( +pub fn insert_turn_derived_records( connection: &Connection, context: &TurnDerivedContext<'_>, steps: &[NormalizedTurnStep], diff --git a/crates/index/src/evidence.rs b/crates/store/src/evidence.rs similarity index 100% rename from crates/index/src/evidence.rs rename to crates/store/src/evidence.rs diff --git a/crates/index/src/index_db.rs b/crates/store/src/index_db.rs similarity index 96% rename from crates/index/src/index_db.rs rename to crates/store/src/index_db.rs index fe4c191..450bedd 100644 --- a/crates/index/src/index_db.rs +++ b/crates/store/src/index_db.rs @@ -1,5 +1,5 @@ mod migrations; -pub(crate) mod schema; +pub mod schema; use std::{fs, path::Path, time::Duration}; @@ -17,13 +17,42 @@ use self::{ }, }; +/// Stores the shared SQLite index filename inside the darc workspace root. +pub const INDEX_DB_FILE_NAME: &str = "index.sqlite"; + /// Tracks one-shot SQLite migrations for normalized index tables. const INDEX_DB_SCHEMA_VERSION: i32 = 13; +/// Opens or creates the index database for write-side commands that may initialize storage. +pub fn open_index_database_writer(path: &Path) -> Result { + create_parent_dir(path)?; + open_migrating_connection(path) +} + +/// Opens an existing index database for read-side commands without creating a missing file. +pub fn open_existing_index_database(path: &Path) -> Result { + ensure_existing_database_path(path)?; + open_migrating_connection(path) +} + +/// Opens an existing index database read-only without initializing or migrating storage. +pub fn open_index_database_read_only(path: &Path) -> Result { + ensure_existing_database_path(path)?; + let connection = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY) + .with_context(|| format!("failed to open index database {} read-only", path.display()))?; + connection + .busy_timeout(Duration::from_secs(5)) + .context("failed to configure SQLite busy timeout")?; + Ok(connection) +} + /// Opens the index database and creates the current schema when missing. pub fn open_index_database(path: &Path) -> Result { - create_parent_dir(path)?; + open_index_database_writer(path) +} +/// Opens one SQLite connection and applies all supported index migrations. +fn open_migrating_connection(path: &Path) -> Result { let mut connection = Connection::open(path) .with_context(|| format!("failed to open index database {}", path.display()))?; connection @@ -39,11 +68,7 @@ pub fn count_project_index_rows_read_only(path: &Path, project_id: &str) -> Resu return Ok((0, 0)); } - let connection = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY) - .with_context(|| format!("failed to open index database {} read-only", path.display()))?; - connection - .busy_timeout(Duration::from_secs(5)) - .context("failed to configure SQLite busy timeout")?; + let connection = open_index_database_read_only(path)?; let mut session_count = count_project_rows_if_table_exists(&connection, SchemaTable::Sessions, project_id)?; @@ -65,7 +90,7 @@ pub fn count_project_index_rows_read_only(path: &Path, project_id: &str) -> Resu /// Ensures the index database file and schema exist. pub fn ensure_index_database(path: &Path) -> Result<()> { - let _connection = open_index_database(path)?; + let _connection = open_index_database_writer(path)?; Ok(()) } @@ -152,6 +177,14 @@ fn managed_tables_are_missing(connection: &Connection, tables: &[SchemaTable]) - Ok(false) } +/// Ensures one existing SQLite database path is available before opening it. +fn ensure_existing_database_path(path: &Path) -> Result<()> { + if !path.exists() { + anyhow::bail!("index database not found at {}", path.display()); + } + Ok(()) +} + /// Creates the parent directory for one SQLite database path. fn create_parent_dir(path: &Path) -> Result<()> { let parent = path diff --git a/crates/index/src/index_db/migrations.rs b/crates/store/src/index_db/migrations.rs similarity index 100% rename from crates/index/src/index_db/migrations.rs rename to crates/store/src/index_db/migrations.rs diff --git a/crates/index/src/index_db/schema.rs b/crates/store/src/index_db/schema.rs similarity index 99% rename from crates/index/src/index_db/schema.rs rename to crates/store/src/index_db/schema.rs index c14f923..2b2c573 100644 --- a/crates/index/src/index_db/schema.rs +++ b/crates/store/src/index_db/schema.rs @@ -560,7 +560,7 @@ const CREATE_SUPPLEMENTAL_SCHEMA_SQL: &str = " "; /// Stores the canonical normalized-session insert statement shared across writers and helpers. -pub(crate) const INSERT_SESSION_SQL: &str = " +pub const INSERT_SESSION_SQL: &str = " INSERT INTO sessions ( project_id, provider, @@ -578,7 +578,7 @@ pub(crate) const INSERT_SESSION_SQL: &str = " "; /// Stores the canonical normalized-turn insert statement shared across writers and helpers. -pub(crate) const INSERT_TURN_SQL: &str = " +pub const INSERT_TURN_SQL: &str = " INSERT INTO turns ( project_id, provider, diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs new file mode 100644 index 0000000..defb4af --- /dev/null +++ b/crates/store/src/lib.rs @@ -0,0 +1,17 @@ +//! SQLite storage, migrations, and derived-index analytics shared by indexing and query crates. + +mod derived_data; +pub mod evidence; +mod index_db; +pub mod policy; +#[cfg(any(test, feature = "test-support"))] +pub mod test_support; +mod turn_metrics; + +pub use derived_data::{TurnDerivedContext, insert_turn_derived_records}; +pub use index_db::{ + INDEX_DB_FILE_NAME, count_project_index_rows_read_only, ensure_index_database, + open_existing_index_database, open_index_database, open_index_database_read_only, + open_index_database_writer, schema, +}; +pub use turn_metrics::{IndexedTurnMetrics, summarize_turn_metrics}; diff --git a/crates/index/src/policy/active_time.rs b/crates/store/src/policy/active_time.rs similarity index 100% rename from crates/index/src/policy/active_time.rs rename to crates/store/src/policy/active_time.rs diff --git a/crates/index/src/policy/file_access.rs b/crates/store/src/policy/file_access.rs similarity index 100% rename from crates/index/src/policy/file_access.rs rename to crates/store/src/policy/file_access.rs diff --git a/crates/index/src/policy/mod.rs b/crates/store/src/policy/mod.rs similarity index 100% rename from crates/index/src/policy/mod.rs rename to crates/store/src/policy/mod.rs diff --git a/crates/index/src/policy/search.rs b/crates/store/src/policy/search.rs similarity index 100% rename from crates/index/src/policy/search.rs rename to crates/store/src/policy/search.rs diff --git a/crates/index/src/policy/shell.rs b/crates/store/src/policy/shell.rs similarity index 100% rename from crates/index/src/policy/shell.rs rename to crates/store/src/policy/shell.rs diff --git a/crates/index/src/test_support.rs b/crates/store/src/test_support.rs similarity index 100% rename from crates/index/src/test_support.rs rename to crates/store/src/test_support.rs diff --git a/crates/index/src/turn_metrics.rs b/crates/store/src/turn_metrics.rs similarity index 92% rename from crates/index/src/turn_metrics.rs rename to crates/store/src/turn_metrics.rs index aca9a07..eed2ec0 100644 --- a/crates/index/src/turn_metrics.rs +++ b/crates/store/src/turn_metrics.rs @@ -12,30 +12,30 @@ use crate::policy::{ /// Stores the derived per-turn analytics counters persisted in the SQLite index. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub(crate) struct IndexedTurnMetrics { - pub(crate) step_count: u32, - pub(crate) tool_call_count: u32, - pub(crate) tool_output_count: u32, - pub(crate) attachment_count: u32, - pub(crate) delegation_count: u32, - pub(crate) hook_summary_count: u32, - pub(crate) has_final_answer: bool, - pub(crate) duration_ms: Option, - pub(crate) effective_agent_runtime_ms: Option, - pub(crate) provider_total_token_count: Option, - pub(crate) input_uncached_token_count: Option, - pub(crate) cache_read_token_count: Option, - pub(crate) cache_write_token_count: Option, - pub(crate) output_token_count: Option, - pub(crate) reasoning_token_count: Option, - pub(crate) total_token_count: Option, - pub(crate) changed_file_count: u32, - pub(crate) added_line_count: u32, - pub(crate) removed_line_count: u32, +pub struct IndexedTurnMetrics { + pub step_count: u32, + pub tool_call_count: u32, + pub tool_output_count: u32, + pub attachment_count: u32, + pub delegation_count: u32, + pub hook_summary_count: u32, + pub has_final_answer: bool, + pub duration_ms: Option, + pub effective_agent_runtime_ms: Option, + pub provider_total_token_count: Option, + pub input_uncached_token_count: Option, + pub cache_read_token_count: Option, + pub cache_write_token_count: Option, + pub output_token_count: Option, + pub reasoning_token_count: Option, + pub total_token_count: Option, + pub changed_file_count: u32, + pub added_line_count: u32, + pub removed_line_count: u32, } /// Summarizes one normalized turn into the derived analytics counters stored in SQLite. -pub(crate) fn summarize_turn_metrics(turn: &CodexTurn) -> IndexedTurnMetrics { +pub fn summarize_turn_metrics(turn: &CodexTurn) -> IndexedTurnMetrics { summarize_turn_parts( &turn.started_at, turn.completed_at.as_deref(), diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 0e12d67..988571f 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -7,9 +7,9 @@ authors.workspace = true repository.workspace = true [dependencies] -darc-index = { workspace = true, features = ["test-support"] } darc-paths = { workspace = true } darc-rollout = { workspace = true } +darc-store = { workspace = true, features = ["test-support"] } anyhow = { workspace = true } rusqlite = { workspace = true } serde_json = { workspace = true } diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index cde256e..c6eed11 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -7,7 +7,7 @@ use std::{ }; use anyhow::{Context, Result, bail}; -pub use darc_index::test_support::{ +pub use darc_store::test_support::{ IndexedSessionFixture, IndexedTurnFixture, create_pre_analytics_index_schema, insert_indexed_session, insert_indexed_turn, insert_pre_analytics_turn, seed_legacy_codex_index, diff --git a/docs/todo.md b/docs/todo.md index 05534d3..7e67465 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -6,8 +6,9 @@ behind, and keep code references aligned with the current crate split. Current crate ownership: - `crates/rollout`: Codex and Claude transcript parsing plus schema and version logic. -- `crates/index`: archive ingestion, duplicate resolution, and SQLite indexing. -- `crates/query`: read-side query, search, and insights. +- `crates/index`: archive ingestion, duplicate resolution, and write-side indexing flow. +- `crates/store`: SQLite schema, migrations, storage helpers, and derived-index analytics. +- `crates/query`: read-side query, search, and insights over the shared store. - `crates/sync`: Claude and Codex discovery plus archive copy planning. - `crates/core`: facade and orchestration glue. From 36744ffe80b17aab7ee4115ac45859ac75c78594 Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Tue, 12 May 2026 12:57:18 +0900 Subject: [PATCH 2/3] refactor(store): hide raw schema SQL --- crates/index/src/engine.rs | 140 +++++---------------- crates/index/src/lib.rs | 1 - crates/store/src/derived_data.rs | 16 +-- crates/store/src/index_db.rs | 2 +- crates/store/src/index_db/schema.rs | 4 +- crates/store/src/lib.rs | 9 +- crates/store/src/turn_metrics.rs | 42 +++---- crates/store/src/write.rs | 182 ++++++++++++++++++++++++++++ 8 files changed, 247 insertions(+), 149 deletions(-) create mode 100644 crates/store/src/write.rs diff --git a/crates/index/src/engine.rs b/crates/index/src/engine.rs index 5f36f63..b5c7167 100644 --- a/crates/index/src/engine.rs +++ b/crates/index/src/engine.rs @@ -28,9 +28,8 @@ use darc_rollout::{ #[cfg(test)] use darc_store::INDEX_DB_FILE_NAME; use darc_store::{ - TurnDerivedContext, insert_turn_derived_records, open_index_database, - schema::{INSERT_SESSION_SQL, INSERT_TURN_SQL}, - summarize_turn_metrics, + StoredSessionKind, StoredSessionRecord, StoredTurnRecord, insert_session_record, + insert_turn_record, open_index_database, }; use rusqlite::{Connection, Transaction, params}; use thiserror::Error; @@ -88,11 +87,11 @@ enum IndexedSessionKind { } impl IndexedSessionKind { - /// Returns the stable SQLite string value for one indexed session kind. - fn as_sql_text(self) -> &'static str { + /// Converts one indexed session kind into the store-owned session kind. + fn into_stored_kind(self) -> StoredSessionKind { match self { - Self::Primary => "primary", - Self::Subagent => "subagent", + Self::Primary => StoredSessionKind::Primary, + Self::Subagent => StoredSessionKind::Subagent, } } } @@ -279,36 +278,23 @@ impl<'conn> SqliteSessionWriter<'conn> { schema_id: &str, determinism: ParseDeterminism, ) -> Result<()> { - let source_size = - i64::try_from(self.source_size).context("source_size exceeds SQLite INTEGER range")?; - let source_mtime_ms = i64::try_from(self.source_mtime_ms) - .context("source_mtime_ms exceeds SQLite INTEGER range")?; - - self.connection - .execute( - INSERT_SESSION_SQL, - params![ - self.project_id.as_str(), - self.provider.directory_name(), - session_id, - self.parent_session_id.as_deref(), - self.session_kind.as_sql_text(), - self.archive_path.as_str(), - cwd.to_string_lossy(), - cli_version, - schema_id, - determinism.as_sql_text(), - source_size, - source_mtime_ms, - ], - ) - .with_context(|| { - format!( - "failed to insert {} session {}", - self.provider.title(), - session_id - ) - })?; + insert_session_record( + self.connection, + &StoredSessionRecord { + project_id: &self.project_id, + provider: self.provider, + session_id, + parent_session_id: self.parent_session_id.as_deref(), + session_kind: self.session_kind.into_stored_kind(), + archive_path: &self.archive_path, + cwd, + cli_version, + schema_id, + determinism, + source_size: self.source_size, + source_mtime_ms: self.source_mtime_ms, + }, + )?; self.session_id = Some(session_id.to_owned()); self.turn_ordinal = 0; Ok(()) @@ -321,88 +307,16 @@ impl<'conn> SqliteSessionWriter<'conn> { .as_deref() .context("missing active session id while inserting turn")?; let turn_ordinal = self.turn_ordinal; - let metrics = summarize_turn_metrics(&turn); - let CodexTurn { - turn_id, - user_message, - final_answer, - started_at, - completed_at, - status, - primary_model, - token_usage: _token_usage, - steps, - } = turn; - let steps_json = serde_json::to_string(&steps).context("failed to serialize turn steps")?; - let final_answer_at = final_answer.as_ref().map(|message| &message.timestamp); - let final_answer_text = final_answer.as_ref().map(|message| &message.text); - - self.connection - .execute( - INSERT_TURN_SQL, - params![ - self.project_id.as_str(), - self.provider.directory_name(), - session_id, - turn_ordinal, - turn_id, - started_at, - completed_at, - status.as_sql_text(), - user_message, - final_answer_at, - final_answer_text, - steps_json, - metrics.step_count, - metrics.tool_call_count, - metrics.tool_output_count, - metrics.attachment_count, - metrics.delegation_count, - metrics.hook_summary_count, - metrics.has_final_answer, - metrics.duration_ms, - metrics.effective_agent_runtime_ms, - metrics.provider_total_token_count, - metrics.input_uncached_token_count, - metrics.cache_read_token_count, - metrics.cache_write_token_count, - metrics.output_token_count, - metrics.reasoning_token_count, - metrics.total_token_count, - primary_model.as_deref(), - metrics.changed_file_count, - metrics.added_line_count, - metrics.removed_line_count, - ], - ) - .with_context(|| { - format!( - "failed to insert {} turn {} for session {}", - self.provider.title(), - self.turn_ordinal, - session_id - ) - })?; - insert_turn_derived_records( + insert_turn_record( self.connection, - &TurnDerivedContext { + StoredTurnRecord { project_id: &self.project_id, provider: self.provider, session_id, turn_ordinal, - user_message: &user_message, - final_answer_text: final_answer_text.map(String::as_str), + turn, }, - &steps, - ) - .with_context(|| { - format!( - "failed to insert derived analytics for {} turn {} in session {}", - self.provider.title(), - turn_ordinal, - session_id - ) - })?; + )?; self.turn_ordinal += 1; Ok(()) diff --git a/crates/index/src/lib.rs b/crates/index/src/lib.rs index f0b1c8d..73e653d 100644 --- a/crates/index/src/lib.rs +++ b/crates/index/src/lib.rs @@ -7,7 +7,6 @@ pub use darc_store::test_support; pub use darc_store::{ INDEX_DB_FILE_NAME, count_project_index_rows_read_only, ensure_index_database, evidence, open_existing_index_database, open_index_database, open_index_database_read_only, policy, - schema, }; pub use engine::{ IndexReport, ProjectIndexRequest, SkippedCodexRollout, SkippedRollout, diff --git a/crates/store/src/derived_data.rs b/crates/store/src/derived_data.rs index 150580d..1b4a5aa 100644 --- a/crates/store/src/derived_data.rs +++ b/crates/store/src/derived_data.rs @@ -24,17 +24,17 @@ struct TurnEvidenceRecord { } /// Stores the canonical turn identity and text needed to derive search analytics rows. -pub struct TurnDerivedContext<'a> { - pub project_id: &'a str, - pub provider: SourceKind, - pub session_id: &'a str, - pub turn_ordinal: i64, - pub user_message: &'a str, - pub final_answer_text: Option<&'a str>, +pub(crate) struct TurnDerivedContext<'a> { + pub(crate) project_id: &'a str, + pub(crate) provider: SourceKind, + pub(crate) session_id: &'a str, + pub(crate) turn_ordinal: i64, + pub(crate) user_message: &'a str, + pub(crate) final_answer_text: Option<&'a str>, } /// Inserts one turn's derived analytics and search records into SQLite. -pub fn insert_turn_derived_records( +pub(crate) fn insert_turn_derived_records( connection: &Connection, context: &TurnDerivedContext<'_>, steps: &[NormalizedTurnStep], diff --git a/crates/store/src/index_db.rs b/crates/store/src/index_db.rs index 450bedd..2bcbe28 100644 --- a/crates/store/src/index_db.rs +++ b/crates/store/src/index_db.rs @@ -1,5 +1,5 @@ mod migrations; -pub mod schema; +pub(crate) mod schema; use std::{fs, path::Path, time::Duration}; diff --git a/crates/store/src/index_db/schema.rs b/crates/store/src/index_db/schema.rs index 2b2c573..c14f923 100644 --- a/crates/store/src/index_db/schema.rs +++ b/crates/store/src/index_db/schema.rs @@ -560,7 +560,7 @@ const CREATE_SUPPLEMENTAL_SCHEMA_SQL: &str = " "; /// Stores the canonical normalized-session insert statement shared across writers and helpers. -pub const INSERT_SESSION_SQL: &str = " +pub(crate) const INSERT_SESSION_SQL: &str = " INSERT INTO sessions ( project_id, provider, @@ -578,7 +578,7 @@ pub const INSERT_SESSION_SQL: &str = " "; /// Stores the canonical normalized-turn insert statement shared across writers and helpers. -pub const INSERT_TURN_SQL: &str = " +pub(crate) const INSERT_TURN_SQL: &str = " INSERT INTO turns ( project_id, provider, diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index defb4af..fa1fce9 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -7,11 +7,14 @@ pub mod policy; #[cfg(any(test, feature = "test-support"))] pub mod test_support; mod turn_metrics; +mod write; -pub use derived_data::{TurnDerivedContext, insert_turn_derived_records}; pub use index_db::{ INDEX_DB_FILE_NAME, count_project_index_rows_read_only, ensure_index_database, open_existing_index_database, open_index_database, open_index_database_read_only, - open_index_database_writer, schema, + open_index_database_writer, +}; +pub use write::{ + StoredSessionKind, StoredSessionRecord, StoredTurnRecord, insert_session_record, + insert_turn_record, }; -pub use turn_metrics::{IndexedTurnMetrics, summarize_turn_metrics}; diff --git a/crates/store/src/turn_metrics.rs b/crates/store/src/turn_metrics.rs index eed2ec0..aca9a07 100644 --- a/crates/store/src/turn_metrics.rs +++ b/crates/store/src/turn_metrics.rs @@ -12,30 +12,30 @@ use crate::policy::{ /// Stores the derived per-turn analytics counters persisted in the SQLite index. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct IndexedTurnMetrics { - pub step_count: u32, - pub tool_call_count: u32, - pub tool_output_count: u32, - pub attachment_count: u32, - pub delegation_count: u32, - pub hook_summary_count: u32, - pub has_final_answer: bool, - pub duration_ms: Option, - pub effective_agent_runtime_ms: Option, - pub provider_total_token_count: Option, - pub input_uncached_token_count: Option, - pub cache_read_token_count: Option, - pub cache_write_token_count: Option, - pub output_token_count: Option, - pub reasoning_token_count: Option, - pub total_token_count: Option, - pub changed_file_count: u32, - pub added_line_count: u32, - pub removed_line_count: u32, +pub(crate) struct IndexedTurnMetrics { + pub(crate) step_count: u32, + pub(crate) tool_call_count: u32, + pub(crate) tool_output_count: u32, + pub(crate) attachment_count: u32, + pub(crate) delegation_count: u32, + pub(crate) hook_summary_count: u32, + pub(crate) has_final_answer: bool, + pub(crate) duration_ms: Option, + pub(crate) effective_agent_runtime_ms: Option, + pub(crate) provider_total_token_count: Option, + pub(crate) input_uncached_token_count: Option, + pub(crate) cache_read_token_count: Option, + pub(crate) cache_write_token_count: Option, + pub(crate) output_token_count: Option, + pub(crate) reasoning_token_count: Option, + pub(crate) total_token_count: Option, + pub(crate) changed_file_count: u32, + pub(crate) added_line_count: u32, + pub(crate) removed_line_count: u32, } /// Summarizes one normalized turn into the derived analytics counters stored in SQLite. -pub fn summarize_turn_metrics(turn: &CodexTurn) -> IndexedTurnMetrics { +pub(crate) fn summarize_turn_metrics(turn: &CodexTurn) -> IndexedTurnMetrics { summarize_turn_parts( &turn.started_at, turn.completed_at.as_deref(), diff --git a/crates/store/src/write.rs b/crates/store/src/write.rs new file mode 100644 index 0000000..da3f184 --- /dev/null +++ b/crates/store/src/write.rs @@ -0,0 +1,182 @@ +use std::path::Path; + +use anyhow::{Context, Result}; +use darc_paths::SourceKind; +use darc_rollout::{ParseDeterminism, model::NormalizedTurn}; +use rusqlite::{Connection, params}; + +use crate::{ + derived_data::{TurnDerivedContext, insert_turn_derived_records}, + index_db::schema::{INSERT_SESSION_SQL, INSERT_TURN_SQL}, + turn_metrics::summarize_turn_metrics, +}; + +/// Identifies the normalized session shape stored in SQLite. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StoredSessionKind { + Primary, + Subagent, +} + +impl StoredSessionKind { + /// Returns the stable SQLite string value for one stored session kind. + fn as_sql_text(self) -> &'static str { + match self { + Self::Primary => "primary", + Self::Subagent => "subagent", + } + } +} + +/// Carries one normalized session row into the store-owned SQLite writer. +pub struct StoredSessionRecord<'a> { + pub project_id: &'a str, + pub provider: SourceKind, + pub session_id: &'a str, + pub parent_session_id: Option<&'a str>, + pub session_kind: StoredSessionKind, + pub archive_path: &'a str, + pub cwd: &'a Path, + pub cli_version: Option<&'a str>, + pub schema_id: &'a str, + pub determinism: ParseDeterminism, + pub source_size: u64, + pub source_mtime_ms: u64, +} + +/// Carries one normalized turn into the store-owned SQLite writer. +pub struct StoredTurnRecord<'a> { + pub project_id: &'a str, + pub provider: SourceKind, + pub session_id: &'a str, + pub turn_ordinal: i64, + pub turn: NormalizedTurn, +} + +/// Inserts one normalized session row into SQLite. +pub fn insert_session_record( + connection: &Connection, + record: &StoredSessionRecord<'_>, +) -> Result<()> { + let source_size = + i64::try_from(record.source_size).context("source_size exceeds SQLite INTEGER range")?; + let source_mtime_ms = i64::try_from(record.source_mtime_ms) + .context("source_mtime_ms exceeds SQLite INTEGER range")?; + + connection + .execute( + INSERT_SESSION_SQL, + params![ + record.project_id, + record.provider.directory_name(), + record.session_id, + record.parent_session_id, + record.session_kind.as_sql_text(), + record.archive_path, + record.cwd.to_string_lossy(), + record.cli_version, + record.schema_id, + record.determinism.as_sql_text(), + source_size, + source_mtime_ms, + ], + ) + .with_context(|| { + format!( + "failed to insert {} session {}", + record.provider.title(), + record.session_id + ) + })?; + Ok(()) +} + +/// Inserts one normalized turn row plus its derived analytics rows into SQLite. +pub fn insert_turn_record(connection: &Connection, record: StoredTurnRecord<'_>) -> Result<()> { + u64::try_from(record.turn_ordinal).context("turn ordinal is negative while inserting turn")?; + let metrics = summarize_turn_metrics(&record.turn); + let NormalizedTurn { + turn_id, + user_message, + final_answer, + started_at, + completed_at, + status, + primary_model, + token_usage: _token_usage, + steps, + } = record.turn; + let steps_json = serde_json::to_string(&steps).context("failed to serialize turn steps")?; + let final_answer_at = final_answer.as_ref().map(|message| &message.timestamp); + let final_answer_text = final_answer.as_ref().map(|message| &message.text); + + connection + .execute( + INSERT_TURN_SQL, + params![ + record.project_id, + record.provider.directory_name(), + record.session_id, + record.turn_ordinal, + turn_id, + started_at, + completed_at, + status.as_sql_text(), + user_message, + final_answer_at, + final_answer_text, + steps_json, + metrics.step_count, + metrics.tool_call_count, + metrics.tool_output_count, + metrics.attachment_count, + metrics.delegation_count, + metrics.hook_summary_count, + metrics.has_final_answer, + metrics.duration_ms, + metrics.effective_agent_runtime_ms, + metrics.provider_total_token_count, + metrics.input_uncached_token_count, + metrics.cache_read_token_count, + metrics.cache_write_token_count, + metrics.output_token_count, + metrics.reasoning_token_count, + metrics.total_token_count, + primary_model.as_deref(), + metrics.changed_file_count, + metrics.added_line_count, + metrics.removed_line_count, + ], + ) + .with_context(|| { + format!( + "failed to insert {} turn {} for session {}", + record.provider.title(), + record.turn_ordinal, + record.session_id + ) + })?; + + insert_turn_derived_records( + connection, + &TurnDerivedContext { + project_id: record.project_id, + provider: record.provider, + session_id: record.session_id, + turn_ordinal: record.turn_ordinal, + user_message: &user_message, + final_answer_text: final_answer_text.map(String::as_str), + }, + &steps, + ) + .with_context(|| { + format!( + "failed to insert derived analytics for {} turn {} in session {}", + record.provider.title(), + record.turn_ordinal, + record.session_id + ) + })?; + + Ok(()) +} From 5fc21c07218b601f3686a586513a198cce0164d9 Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Tue, 12 May 2026 13:02:41 +0900 Subject: [PATCH 3/3] docs(changelog): note storage api narrowing --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4ce3f..5fd5a4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable Darc release changes should be summarized here. ## Unreleased +- Narrow internal Rust storage APIs so SQLite schema details are no longer exposed outside the storage crate. + ## [0.1.5] - 2026-05-11 - Clarify README guidance for agent setup and prompt-driven prior-session investigations.