style: cargo fmt --all (round 4 ingest log feature follow-up)

Phase C4 executor 의 마지막 `fix(test): clippy + fmt fixes` commit 이
test file 부분만 fmt 적용. workspace 전체 fmt 누락 발견 → cargo fmt --all
적용. 모든 import alphabetical reorder + line wrapping 정합.

추가 untracked artifact 동시 commit:
- docs/superpowers/specs/2026-05-28-v0.20-ingest-log-spec.md (491 line, ACCEPT)
- docs/superpowers/plans/2026-05-28-v0.20-ingest-log-plan.md (616 line, ACCEPT)

workspace test: 1370 passed / 0 failed / 50 ignored, ingest_log_smoke green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 04:18:40 +00:00
parent 445b096215
commit 685007789a
235 changed files with 6520 additions and 3955 deletions

View File

@@ -37,14 +37,16 @@ impl SqliteStore {
.created_at
.format(&time::format_description::well_known::Rfc3339)
.context("format answer.created_at")?;
let citations_json = serde_json::to_string(&answer.citations)
.context("serialize answer.citations")?;
let citations_json =
serde_json::to_string(&answer.citations).context("serialize answer.citations")?;
let refusal_label: Option<&'static str> =
answer.refusal_reason.as_ref().map(refusal_reason_label);
let mode_label = search_mode_label(&answer.retrieval.mode);
let embedding_id: Option<&str> = answer.embedding.as_ref().map(|m| m.id.as_str());
let embedding_dim: Option<i64> =
answer.embedding.as_ref().and_then(|m| m.dimensions.map(|d| d as i64));
let embedding_dim: Option<i64> = answer
.embedding
.as_ref()
.and_then(|m| m.dimensions.map(|d| d as i64));
let conn = self.lock_conn();
conn.execute(

View File

@@ -40,11 +40,7 @@ impl kebab_core::DocumentStore for SqliteStore {
}
};
let conn = self.lock_conn();
purge_orphan_at_workspace_path(
&conn,
&asset.workspace_path.0,
&asset.asset_id.0,
)?;
purge_orphan_at_workspace_path(&conn, &asset.workspace_path.0, &asset.asset_id.0)?;
upsert_asset_row(&conn, asset, storage_kind, &storage_path)
}
@@ -57,11 +53,7 @@ impl kebab_core::DocumentStore for SqliteStore {
Ok(())
}
fn put_blocks(
&self,
doc: &kebab_core::DocumentId,
blocks: &[kebab_core::Block],
) -> Result<()> {
fn put_blocks(&self, doc: &kebab_core::DocumentId, blocks: &[kebab_core::Block]) -> Result<()> {
let mut conn = self.lock_conn();
let tx = conn.transaction().map_err(StoreError::from)?;
// DELETE-then-INSERT: §5.4 has no UNIQUE on (doc_id, ordinal)
@@ -100,11 +92,7 @@ impl kebab_core::DocumentStore for SqliteStore {
Ok(())
}
fn put_chunks(
&self,
doc: &kebab_core::DocumentId,
chunks: &[kebab_core::Chunk],
) -> Result<()> {
fn put_chunks(&self, doc: &kebab_core::DocumentId, chunks: &[kebab_core::Chunk]) -> Result<()> {
let now = OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.context("format chunk created_at")?;
@@ -126,8 +114,8 @@ impl kebab_core::DocumentStore for SqliteStore {
.context("serialize chunk.heading_path")?;
let source_spans = serde_json::to_string(&chunk.source_spans)
.context("serialize chunk.source_spans")?;
let block_ids = serde_json::to_string(&chunk.block_ids)
.context("serialize chunk.block_ids")?;
let block_ids =
serde_json::to_string(&chunk.block_ids).context("serialize chunk.block_ids")?;
// §5.5 has a `section_label` column but the in-memory Chunk
// struct does not carry it (nor does the wire schema §2.6).
// Persist NULL until a future bump introduces the field.
@@ -193,16 +181,15 @@ impl kebab_core::DocumentStore for SqliteStore {
let mut blocks: Vec<kebab_core::Block> = Vec::new();
for row in block_rows {
let payload_json = row.map_err(StoreError::from)?;
let block: kebab_core::Block = serde_json::from_str(&payload_json)
.context("deserialize block payload_json")?;
let block: kebab_core::Block =
serde_json::from_str(&payload_json).context("deserialize block payload_json")?;
blocks.push(block);
}
let metadata: kebab_core::Metadata = serde_json::from_str(&row.metadata_json)
.context("deserialize metadata_json")?;
let metadata: kebab_core::Metadata =
serde_json::from_str(&row.metadata_json).context("deserialize metadata_json")?;
let provenance: kebab_core::Provenance =
serde_json::from_str(&row.provenance_json)
.context("deserialize provenance_json")?;
serde_json::from_str(&row.provenance_json).context("deserialize provenance_json")?;
Ok(Some(kebab_core::CanonicalDocument {
doc_id: kebab_core::DocumentId(row.doc_id),
@@ -248,9 +235,8 @@ impl kebab_core::DocumentStore for SqliteStore {
let source_spans: Vec<kebab_core::SourceSpan> =
serde_json::from_str(&row.source_spans_json)
.context("deserialize chunk.source_spans_json")?;
let block_ids: Vec<kebab_core::BlockId> =
serde_json::from_str(&row.block_ids_json)
.context("deserialize chunk.block_ids_json")?;
let block_ids: Vec<kebab_core::BlockId> = serde_json::from_str(&row.block_ids_json)
.context("deserialize chunk.block_ids_json")?;
Ok(Some(kebab_core::Chunk {
chunk_id: kebab_core::ChunkId(row.chunk_id),
doc_id: kebab_core::DocumentId(row.doc_id),
@@ -264,10 +250,7 @@ impl kebab_core::DocumentStore for SqliteStore {
}))
}
fn get_asset(
&self,
id: &kebab_core::AssetId,
) -> Result<Option<kebab_core::RawAsset>> {
fn get_asset(&self, id: &kebab_core::AssetId) -> Result<Option<kebab_core::RawAsset>> {
let conn = self.lock_conn();
let result = conn.query_row(
r"SELECT
@@ -346,16 +329,15 @@ impl kebab_core::DocumentStore for SqliteStore {
let mut blocks: Vec<kebab_core::Block> = Vec::new();
for block_row in block_rows {
let payload_json = block_row.map_err(StoreError::from)?;
let block: kebab_core::Block = serde_json::from_str(&payload_json)
.context("deserialize block payload_json")?;
let block: kebab_core::Block =
serde_json::from_str(&payload_json).context("deserialize block payload_json")?;
blocks.push(block);
}
let metadata: kebab_core::Metadata = serde_json::from_str(&row.metadata_json)
.context("deserialize metadata_json")?;
let metadata: kebab_core::Metadata =
serde_json::from_str(&row.metadata_json).context("deserialize metadata_json")?;
let provenance: kebab_core::Provenance =
serde_json::from_str(&row.provenance_json)
.context("deserialize provenance_json")?;
serde_json::from_str(&row.provenance_json).context("deserialize provenance_json")?;
Ok(Some(kebab_core::CanonicalDocument {
doc_id,
@@ -421,12 +403,14 @@ impl kebab_core::DocumentStore for SqliteStore {
if let Some(trust_min) = &filter.trust_min {
// Map the enum to its rank: Generated < Secondary < Primary.
// (Higher trust strictly contains lower trust.)
sql.push_str(" AND CASE d.trust_level
sql.push_str(
" AND CASE d.trust_level
WHEN 'primary' THEN 3
WHEN 'secondary' THEN 2
WHEN 'generated' THEN 1
ELSE 0
END >= ?");
END >= ?",
);
let rank: i64 = match trust_min {
kebab_core::TrustLevel::Primary => 3,
kebab_core::TrustLevel::Secondary => 2,
@@ -606,21 +590,27 @@ fn doc_summary_from_sql(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core:
// De-serialize the lowercase string forms that match
// `#[serde(rename_all = "lowercase")]` on each enum.
let source_type: kebab_core::SourceType =
serde_json::from_value(serde_json::Value::String(source_type_raw))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(e)))?;
serde_json::from_value(serde_json::Value::String(source_type_raw)).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(4, rusqlite::types::Type::Text, Box::new(e))
})?;
let trust_level: kebab_core::TrustLevel =
serde_json::from_value(serde_json::Value::String(trust_level_raw))
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(e)))?;
serde_json::from_value(serde_json::Value::String(trust_level_raw)).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(5, rusqlite::types::Type::Text, Box::new(e))
})?;
let created_at = OffsetDateTime::parse(
&created_at_raw,
&time::format_description::well_known::Rfc3339,
)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(e)))?;
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(e))
})?;
let updated_at = OffsetDateTime::parse(
&updated_at_raw,
&time::format_description::well_known::Rfc3339,
)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e)))?;
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e))
})?;
Ok(kebab_core::DocSummary {
doc_id: kebab_core::DocumentId(doc_id),
@@ -678,14 +668,18 @@ fn asset_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core::RawAs
};
let workspace_path = kebab_core::WorkspacePath(workspace_path_raw);
let media_type: kebab_core::MediaType = serde_json::from_str(&media_type_json)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Text, Box::new(e)))?;
let media_type: kebab_core::MediaType =
serde_json::from_str(&media_type_json).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(3, rusqlite::types::Type::Text, Box::new(e))
})?;
let checksum = kebab_core::Checksum(checksum_raw.clone());
let discovered_at = time::OffsetDateTime::parse(
&discovered_at_raw,
&time::format_description::well_known::Rfc3339,
)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e)))?;
.map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(8, rusqlite::types::Type::Text, Box::new(e))
})?;
let storage_path = PathBuf::from(&storage_path_raw);
let stored = if storage_kind == "copied" {
@@ -702,14 +696,15 @@ fn asset_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core::RawAs
source_uri,
workspace_path,
media_type,
byte_len: u64::try_from(byte_len)
.map_err(|e| rusqlite::Error::FromSqlConversionFailure(
byte_len: u64::try_from(byte_len).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
// index parameter for named-column path is unused but the
// type still requires a number; pass 0 with a clear msg.
0,
rusqlite::types::Type::Integer,
Box::new(e),
))?,
)
})?,
checksum,
discovered_at,
stored,
@@ -721,10 +716,8 @@ fn upsert_document(
tx: &rusqlite::Transaction<'_>,
doc: &kebab_core::CanonicalDocument,
) -> Result<()> {
let metadata_json = serde_json::to_string(&doc.metadata)
.context("serialize metadata")?;
let provenance_json = serde_json::to_string(&doc.provenance)
.context("serialize provenance")?;
let metadata_json = serde_json::to_string(&doc.metadata).context("serialize metadata")?;
let provenance_json = serde_json::to_string(&doc.provenance).context("serialize provenance")?;
// String form of the lowercase serde representation. We avoid
// embedding `serde_json::to_string` quotes (`"markdown"` → just
// `markdown` for the column).
@@ -811,8 +804,11 @@ fn replace_document_tags(
doc_id: &kebab_core::DocumentId,
tags: &[String],
) -> Result<()> {
tx.execute("DELETE FROM document_tags WHERE doc_id = ?", params![doc_id.0])
.map_err(StoreError::from)?;
tx.execute(
"DELETE FROM document_tags WHERE doc_id = ?",
params![doc_id.0],
)
.map_err(StoreError::from)?;
let mut stmt = tx
.prepare(
"INSERT INTO document_tags (doc_id, tag) VALUES (?, ?)

View File

@@ -54,10 +54,7 @@ impl SqliteStore {
/// All rows are written in a single transaction; if any row fails
/// the entire batch is rolled back and the caller can retry without
/// worrying about partial pending state.
pub fn put_embedding_records_pending(
&self,
rows: &[EmbeddingRecordRow],
) -> Result<()> {
pub fn put_embedding_records_pending(&self, rows: &[EmbeddingRecordRow]) -> Result<()> {
if rows.is_empty() {
return Ok(());
}
@@ -107,10 +104,7 @@ impl SqliteStore {
/// WHERE embedding_id IN (?, ?, …)`) inside one transaction —
/// avoids the per-row `execute()` round-trip the previous
/// implementation paid.
pub fn mark_embedding_records_committed(
&self,
embedding_ids: &[String],
) -> Result<()> {
pub fn mark_embedding_records_committed(&self, embedding_ids: &[String]) -> Result<()> {
if embedding_ids.is_empty() {
return Ok(());
}
@@ -208,7 +202,11 @@ mod tests {
source_spans_json, token_estimate, chunker_version,
policy_hash, block_ids_json, created_at
) VALUES (?, ?, 'hi', '[]', NULL, '[]', 1, 'v1', 'hash', '[]', ?)",
params![chunk_id, "fedcba9876543210fedcba9876543210", "1970-01-01T00:00:00Z"],
params![
chunk_id,
"fedcba9876543210fedcba9876543210",
"1970-01-01T00:00:00Z"
],
)
.unwrap();
}

View File

@@ -209,11 +209,9 @@ impl SqliteStore {
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
Err(e) => return Err(StoreError::from(e).into()),
};
let created_at = OffsetDateTime::parse(
&created_str,
&time::format_description::well_known::Rfc3339,
)
.with_context(|| format!("parse eval_runs.created_at for {run_id}"))?;
let created_at =
OffsetDateTime::parse(&created_str, &time::format_description::well_known::Rfc3339)
.with_context(|| format!("parse eval_runs.created_at for {run_id}"))?;
Ok(Some(EvalRunRecord {
run_id,
suite,
@@ -228,10 +226,7 @@ impl SqliteStore {
/// `query_id` ASC for determinism (the table has no insertion-order
/// column; query_id ordering matches the BTreeSet sort the loader
/// uses for missing-id reporting).
pub fn load_eval_query_results(
&self,
run_id: &str,
) -> Result<Vec<EvalQueryResultRecord>> {
pub fn load_eval_query_results(&self, run_id: &str) -> Result<Vec<EvalQueryResultRecord>> {
let conn = self.lock_conn();
let mut stmt = conn
.prepare(
@@ -258,11 +253,7 @@ impl SqliteStore {
/// `Err` (not `Ok(0)`) if the run is missing — we never want to
/// silently drop computed metrics. Called once per run by
/// P5-2's `store_aggregate`.
pub fn update_eval_run_aggregate(
&self,
run_id: &str,
aggregate_json: &str,
) -> Result<()> {
pub fn update_eval_run_aggregate(&self, run_id: &str, aggregate_json: &str) -> Result<()> {
let conn = self.lock_conn();
let updated = conn
.execute(

View File

@@ -23,7 +23,7 @@
use std::collections::{HashMap, HashSet};
use anyhow::{Context, Result};
use rusqlite::{params_from_iter, ToSql};
use rusqlite::{ToSql, params_from_iter};
use crate::store::SqliteStore;
@@ -347,9 +347,7 @@ mod tests {
.put_embedding_records_pending(std::slice::from_ref(&embed_row))
.unwrap();
store
.mark_embedding_records_committed(std::slice::from_ref(
&embed_row.embedding_id,
))
.mark_embedding_records_committed(std::slice::from_ref(&embed_row.embedding_id))
.unwrap();
}
@@ -430,9 +428,7 @@ mod tests {
.put_embedding_records_pending(std::slice::from_ref(&embed_row))
.unwrap();
store
.mark_embedding_records_committed(std::slice::from_ref(
&embed_row.embedding_id,
))
.mark_embedding_records_committed(std::slice::from_ref(&embed_row.embedding_id))
.unwrap();
}
@@ -502,9 +498,7 @@ mod tests {
.put_embedding_records_pending(std::slice::from_ref(&embed_row))
.unwrap();
store
.mark_embedding_records_committed(std::slice::from_ref(
&embed_row.embedding_id,
))
.mark_embedding_records_committed(std::slice::from_ref(&embed_row.embedding_id))
.unwrap();
}
@@ -573,10 +567,38 @@ mod tests {
// c3: tags=[ko-style], lang=ko, secondary, notes/c.md
// c4: tags=[ko-style], lang=en, generated, src/d.md
let chunks = [
("11111111111111111111111111111111", "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1", "notes/a.md", "en", "primary", &["ko-style"][..]),
("22222222222222222222222222222222", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "notes/b.md", "en", "primary", &["other"][..]),
("33333333333333333333333333333333", "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", "notes/c.md", "ko", "secondary", &["ko-style"][..]),
("44444444444444444444444444444444", "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4", "src/d.md", "en", "generated", &["ko-style"][..]),
(
"11111111111111111111111111111111",
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"notes/a.md",
"en",
"primary",
&["ko-style"][..],
),
(
"22222222222222222222222222222222",
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"notes/b.md",
"en",
"primary",
&["other"][..],
),
(
"33333333333333333333333333333333",
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
"notes/c.md",
"ko",
"secondary",
&["ko-style"][..],
),
(
"44444444444444444444444444444444",
"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
"src/d.md",
"en",
"generated",
&["ko-style"][..],
),
];
for (c, d, p, l, t, tags) in &chunks {
seed_committed(&store, c, d, p, l, tags, t);
@@ -588,10 +610,7 @@ mod tests {
..Default::default()
};
let out = store
.filter_chunks(
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
&f,
)
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
.unwrap();
let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
got.sort_unstable();
@@ -604,10 +623,7 @@ mod tests {
..Default::default()
};
let out = store
.filter_chunks(
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
&f,
)
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
.unwrap();
let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
got.sort_unstable();
@@ -621,10 +637,7 @@ mod tests {
..Default::default()
};
let out = store
.filter_chunks(
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
&f,
)
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
.unwrap();
let got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
assert_eq!(got, vec![chunks[0].0]);
@@ -635,10 +648,7 @@ mod tests {
..Default::default()
};
let out = store
.filter_chunks(
&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(),
&f,
)
.filter_chunks(&chunks.iter().map(|c| cid(c.0)).collect::<Vec<_>>(), &f)
.unwrap();
let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect();
got.sort_unstable();
@@ -652,9 +662,33 @@ mod tests {
let c1 = "11111111111111111111111111111111";
let c2 = "22222222222222222222222222222222";
let c3 = "33333333333333333333333333333333";
seed_committed(&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1", "a.md", "en", &[], "primary");
seed_committed(&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "b.md", "en", &[], "primary");
seed_committed(&store, c3, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", "c.md", "en", &[], "primary");
seed_committed(
&store,
c1,
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"a.md",
"en",
&[],
"primary",
);
seed_committed(
&store,
c2,
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"b.md",
"en",
&[],
"primary",
);
seed_committed(
&store,
c3,
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
"c.md",
"en",
&[],
"primary",
);
// Ask in the order c3, c1, c2; result must preserve that order.
let out = store
@@ -688,14 +722,24 @@ mod tests {
let c1 = "11111111111111111111111111111111";
let c2 = "22222222222222222222222222222222";
seed_committed_full(
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"notes/a.md", "en", &[], "primary",
&store,
c1,
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"notes/a.md",
"en",
&[],
"primary",
r#""markdown""#,
"1970-01-01T00:00:00Z",
);
seed_committed_full(
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"notes/b.pdf", "en", &[], "primary",
&store,
c2,
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"notes/b.pdf",
"en",
&[],
"primary",
r#""pdf""#,
"1970-01-01T00:00:00Z",
);
@@ -704,10 +748,12 @@ mod tests {
media: vec!["pdf".to_string()],
..Default::default()
};
let out = store
.filter_chunks(&[cid(c1), cid(c2)], &f)
.unwrap();
assert_eq!(out, vec![cid(c2)], "only pdf chunk should survive media filter");
let out = store.filter_chunks(&[cid(c1), cid(c2)], &f).unwrap();
assert_eq!(
out,
vec![cid(c2)],
"only pdf chunk should survive media filter"
);
}
#[test]
@@ -718,14 +764,24 @@ mod tests {
let c1 = "11111111111111111111111111111111";
let c2 = "22222222222222222222222222222222";
seed_committed_full(
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"old.md", "en", &[], "primary",
&store,
c1,
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"old.md",
"en",
&[],
"primary",
r#""markdown""#,
"2020-01-01T00:00:00Z",
);
seed_committed_full(
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"new.md", "en", &[], "primary",
&store,
c2,
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"new.md",
"en",
&[],
"primary",
r#""markdown""#,
"2026-01-01T00:00:00Z",
);
@@ -734,10 +790,12 @@ mod tests {
ingested_after: Some(time::macros::datetime!(2025-01-01 00:00:00 UTC)),
..Default::default()
};
let out = store
.filter_chunks(&[cid(c1), cid(c2)], &f)
.unwrap();
assert_eq!(out, vec![cid(c2)], "only post-2025 chunk should survive ingested_after filter");
let out = store.filter_chunks(&[cid(c1), cid(c2)], &f).unwrap();
assert_eq!(
out,
vec![cid(c2)],
"only post-2025 chunk should survive ingested_after filter"
);
}
#[test]
@@ -749,14 +807,24 @@ mod tests {
let c2 = "22222222222222222222222222222222";
let d1 = "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1";
seed_committed_full(
&store, c1, d1,
"a.md", "en", &[], "primary",
&store,
c1,
d1,
"a.md",
"en",
&[],
"primary",
r#""markdown""#,
"1970-01-01T00:00:00Z",
);
seed_committed_full(
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"b.md", "en", &[], "primary",
&store,
c2,
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"b.md",
"en",
&[],
"primary",
r#""markdown""#,
"1970-01-01T00:00:00Z",
);
@@ -765,10 +833,12 @@ mod tests {
doc_id: Some(kebab_core::DocumentId(d1.to_string())),
..Default::default()
};
let out = store
.filter_chunks(&[cid(c1), cid(c2)], &f)
.unwrap();
assert_eq!(out, vec![cid(c1)], "doc_id filter must scope to the target doc only");
let out = store.filter_chunks(&[cid(c1), cid(c2)], &f).unwrap();
assert_eq!(
out,
vec![cid(c1)],
"doc_id filter must scope to the target doc only"
);
}
// ── p10-1A-1 new filter arms ─────────────────────────────────────────
@@ -783,18 +853,27 @@ mod tests {
let c2 = "22222222222222222222222222222222";
let c3 = "33333333333333333333333333333333";
seed_committed_with_metadata(
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"src/main.py", r#""code""#,
&store,
c1,
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"src/main.py",
r#""code""#,
r#"{"code_lang":"python"}"#,
);
seed_committed_with_metadata(
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"src/lib.rs", r#""code""#,
&store,
c2,
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"src/lib.rs",
r#""code""#,
r#"{"code_lang":"rust"}"#,
);
seed_committed_with_metadata(
&store, c3, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
"README.md", r#""markdown""#,
&store,
c3,
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
"README.md",
r#""markdown""#,
r"{}",
);
@@ -805,7 +884,11 @@ mod tests {
let out = store
.filter_chunks(&[cid(c1), cid(c2), cid(c3)], &f)
.unwrap();
assert_eq!(out, vec![cid(c1)], "only python chunk should survive code_lang filter");
assert_eq!(
out,
vec![cid(c1)],
"only python chunk should survive code_lang filter"
);
}
#[test]
@@ -818,18 +901,27 @@ mod tests {
let c2 = "22222222222222222222222222222222";
let c3 = "33333333333333333333333333333333";
seed_committed_with_metadata(
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"httpx/client.py", r#""code""#,
&store,
c1,
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"httpx/client.py",
r#""code""#,
r#"{"repo":"httpx","code_lang":"python"}"#,
);
seed_committed_with_metadata(
&store, c2, "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"requests/api.py", r#""code""#,
&store,
c2,
"d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"requests/api.py",
r#""code""#,
r#"{"repo":"requests","code_lang":"python"}"#,
);
seed_committed_with_metadata(
&store, c3, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
"standalone.py", r#""code""#,
&store,
c3,
"d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3",
"standalone.py",
r#""code""#,
r#"{"code_lang":"python"}"#,
);
@@ -840,7 +932,11 @@ mod tests {
let out = store
.filter_chunks(&[cid(c1), cid(c2), cid(c3)], &f)
.unwrap();
assert_eq!(out, vec![cid(c1)], "only httpx chunk should survive repo filter");
assert_eq!(
out,
vec![cid(c1)],
"only httpx chunk should survive repo filter"
);
}
#[test]
@@ -865,8 +961,13 @@ mod tests {
let store = open_store(&tmp);
let c1 = "11111111111111111111111111111111";
seed_committed_full(
&store, c1, "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"doc.md", "en", &[], "primary",
&store,
c1,
"d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1",
"doc.md",
"en",
&[],
"primary",
r#""markdown""#,
"2026-04-01T01:00:00Z",
);
@@ -883,9 +984,7 @@ mod tests {
ingested_after: Some(filter_instant),
..Default::default()
};
let out = store
.filter_chunks(&[cid(c1)], &f)
.unwrap();
let out = store.filter_chunks(&[cid(c1)], &f).unwrap();
assert_eq!(
out,
vec![cid(c1)],

View File

@@ -86,11 +86,7 @@ impl SqliteStore {
}
impl kebab_core::JobRepo for SqliteStore {
fn create(
&self,
kind: kebab_core::JobKind,
payload: Value,
) -> Result<kebab_core::JobId> {
fn create(&self, kind: kebab_core::JobKind, payload: Value) -> Result<kebab_core::JobId> {
let now_dt = OffsetDateTime::now_utc();
let now = now_dt
.format(&time::format_description::well_known::Rfc3339)
@@ -100,8 +96,7 @@ impl kebab_core::JobRepo for SqliteStore {
// identical `(kind, payload)` still get distinct IDs.
let job_id = mint_job_id(&kind, &payload, now_dt);
let kind_label = job_kind_label(&kind);
let payload_json = serde_json::to_string(&payload)
.context("serialize job payload")?;
let payload_json = serde_json::to_string(&payload).context("serialize job payload")?;
let conn = self.lock_conn();
conn.execute(
"INSERT INTO jobs (
@@ -114,13 +109,8 @@ impl kebab_core::JobRepo for SqliteStore {
Ok(job_id)
}
fn update_progress(
&self,
id: &kebab_core::JobId,
progress: Value,
) -> Result<()> {
let progress_json = serde_json::to_string(&progress)
.context("serialize job progress")?;
fn update_progress(&self, id: &kebab_core::JobId, progress: Value) -> Result<()> {
let progress_json = serde_json::to_string(&progress).context("serialize job progress")?;
let now = OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.context("format job updated_at")?;
@@ -167,10 +157,7 @@ impl kebab_core::JobRepo for SqliteStore {
Ok(())
}
fn list(
&self,
filter: &kebab_core::JobFilter,
) -> Result<Vec<kebab_core::JobRow>> {
fn list(&self, filter: &kebab_core::JobFilter) -> Result<Vec<kebab_core::JobRow>> {
let conn = self.lock_conn();
let mut sql = String::from(
"SELECT job_id, kind, status, payload_json, progress_json,
@@ -259,11 +246,9 @@ fn job_row_from_sql(row: &rusqlite::Row<'_>) -> rusqlite::Result<kebab_core::Job
let finished_at_raw: Option<String> = row.get(8)?;
let kind: kebab_core::JobKind =
serde_json::from_value(serde_json::Value::String(kind_raw))
.map_err(conv_err(1))?;
serde_json::from_value(serde_json::Value::String(kind_raw)).map_err(conv_err(1))?;
let status: kebab_core::JobStatus =
serde_json::from_value(serde_json::Value::String(status_raw))
.map_err(conv_err(2))?;
serde_json::from_value(serde_json::Value::String(status_raw)).map_err(conv_err(2))?;
let payload: Value = serde_json::from_str(&payload_json).map_err(conv_err(3))?;
let progress: Option<Value> = match progress_json {
Some(s) => Some(serde_json::from_str(&s).map_err(conv_err(4))?),

View File

@@ -27,8 +27,8 @@ mod filters;
mod fts;
mod jobs;
mod schema;
mod store;
pub mod stats_ext;
mod store;
pub use embeddings::EmbeddingRecordRow;
pub use error::StoreError;

View File

@@ -19,10 +19,7 @@ pub struct Breakdowns {
/// `lang` only contains observed languages; NULL lang is
/// keyed as the literal string `"null"`. `stale_doc_count` is 0 when
/// `threshold_days == 0` (mirrors fb-32 staleness disable semantics).
pub fn breakdowns(
conn: &Connection,
threshold_days: u64,
) -> rusqlite::Result<Breakdowns> {
pub fn breakdowns(conn: &Connection, threshold_days: u64) -> rusqlite::Result<Breakdowns> {
// media: dual JSON shape — text variant ("markdown") vs object
// variant ({"image":{"format":"png"}}). Same CASE WHEN as fb-36.
let mut media: BTreeMap<String, u64> = MEDIA_KINDS
@@ -40,9 +37,7 @@ pub fn breakdowns(
FROM documents d JOIN assets a ON a.asset_id = d.asset_id \
GROUP BY kind",
)?;
let rows = stmt.query_map([], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?))
})?;
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?;
for row in rows {
let (kind, n) = row?;
media.insert(kind, n);
@@ -53,9 +48,7 @@ pub fn breakdowns(
"SELECT COALESCE(lang, 'null') AS l, COUNT(*) \
FROM documents GROUP BY l",
)?;
let rows = stmt.query_map([], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?))
})?;
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?;
for row in rows {
let (l, n) = row?;
lang.insert(l, n);
@@ -65,8 +58,7 @@ pub fn breakdowns(
0
} else {
let secs = (threshold_days as i64) * 86_400;
let cutoff = time::OffsetDateTime::now_utc()
- time::Duration::seconds(secs);
let cutoff = time::OffsetDateTime::now_utc() - time::Duration::seconds(secs);
let cutoff_str = cutoff
.format(&time::format_description::well_known::Rfc3339)
.expect("RFC3339 format");
@@ -148,7 +140,10 @@ mod tests {
fn index_bytes_includes_sqlite_main() {
let (dir, _store) = open_fresh();
let b = index_bytes(dir.path()).unwrap();
assert!(b.sqlite > 0, "main sqlite file should exist after migrations");
assert!(
b.sqlite > 0,
"main sqlite file should exist after migrations"
);
assert_eq!(b.lancedb, 0);
}

View File

@@ -163,7 +163,9 @@ impl SqliteStore {
/// safe to reuse — we simply unwrap the inner guard rather than
/// propagate the panic to every subsequent call.
pub(crate) fn lock_conn(&self) -> MutexGuard<'_, Connection> {
self.conn.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
self.conn
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Read-only borrow of the connection.
@@ -179,7 +181,9 @@ impl SqliteStore {
///
/// Poisoning is recovered the same way as [`Self::lock_conn`].
pub fn read_conn(&self) -> MutexGuard<'_, Connection> {
self.conn.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
self.conn
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Persist a `RawAsset` *with its raw bytes*: row goes into `assets`,
@@ -190,11 +194,7 @@ impl SqliteStore {
/// In either branch, `blake3(bytes)` is recomputed and compared to
/// `asset.checksum.0`. A mismatch returns
/// `StoreError::Conflict` wrapped in `anyhow::Error`.
pub fn put_asset_with_bytes(
&self,
asset: &kebab_core::RawAsset,
bytes: &[u8],
) -> Result<()> {
pub fn put_asset_with_bytes(&self, asset: &kebab_core::RawAsset, bytes: &[u8]) -> Result<()> {
// 0. Validate the AssetId shape before any I/O. `kebab_core::AssetId`
// is a `pub String` newtype: `FromStr` enforces the 32-hex-char
// invariant, but a hand-constructed `AssetId("../etc/passwd…")`
@@ -229,9 +229,8 @@ impl SqliteStore {
// of the temp file so we never leak bytes on disk.
let dest = self.assets_path_for(&asset.asset_id);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("create asset shard dir {}", parent.display())
})?;
std::fs::create_dir_all(parent)
.with_context(|| format!("create asset shard dir {}", parent.display()))?;
}
let temp_path = temp_path_for(&dest);
// Inline closure so any `?` in (a)/(b) cleans up the temp
@@ -242,9 +241,8 @@ impl SqliteStore {
format!("create temp asset file {}", temp_path.display())
})?;
use std::io::Write;
f.write_all(bytes).with_context(|| {
format!("write asset bytes to {}", temp_path.display())
})?;
f.write_all(bytes)
.with_context(|| format!("write asset bytes to {}", temp_path.display()))?;
f.sync_all().with_context(|| {
format!("fsync temp asset file {}", temp_path.display())
})?;
@@ -255,9 +253,8 @@ impl SqliteStore {
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&temp_path)?.permissions();
perms.set_mode(0o644);
std::fs::set_permissions(&temp_path, perms).with_context(|| {
format!("chmod 0o644 on {}", temp_path.display())
})?;
std::fs::set_permissions(&temp_path, perms)
.with_context(|| format!("chmod 0o644 on {}", temp_path.display()))?;
}
// UPSERT the row first; only after a successful row write
// do we publish the file via rename. A second
@@ -270,12 +267,7 @@ impl SqliteStore {
&asset.workspace_path.0,
&asset.asset_id.0,
)?;
upsert_asset_row(
&conn,
asset,
"copied",
&dest.to_string_lossy(),
)?;
upsert_asset_row(&conn, asset, "copied", &dest.to_string_lossy())?;
}
std::fs::rename(&temp_path, &dest).with_context(|| {
format!(
@@ -305,11 +297,7 @@ impl SqliteStore {
kebab_core::SourceUri::Kb(u) => u.clone(),
};
let conn = self.lock_conn();
purge_orphan_at_workspace_path(
&conn,
&asset.workspace_path.0,
&asset.asset_id.0,
)?;
purge_orphan_at_workspace_path(&conn, &asset.workspace_path.0, &asset.asset_id.0)?;
upsert_asset_row(&conn, asset, "reference", &storage_path)?;
Ok(())
}
@@ -338,9 +326,7 @@ impl SqliteStore {
/// permits hand-construction, so any function that turns an `AssetId`
/// into a filesystem path must call this first.
pub(crate) fn validate_asset_id(asset_id: &kebab_core::AssetId) -> Result<()> {
if asset_id.0.len() != ASSET_ID_HEX_LEN
|| !asset_id.0.bytes().all(|b| b.is_ascii_hexdigit())
{
if asset_id.0.len() != ASSET_ID_HEX_LEN || !asset_id.0.bytes().all(|b| b.is_ascii_hexdigit()) {
anyhow::bail!(
"invalid AssetId shape (expected {} ASCII hex chars): {:?}",
ASSET_ID_HEX_LEN,
@@ -359,7 +345,8 @@ fn temp_path_for(dest: &Path) -> PathBuf {
let n = TEMP_SUFFIX_COUNTER.fetch_add(1, Ordering::Relaxed);
let parent = dest.parent().unwrap_or_else(|| Path::new("."));
let file_name = dest
.file_name().map_or_else(|| "asset".to_string(), |s| s.to_string_lossy().into_owned());
.file_name()
.map_or_else(|| "asset".to_string(), |s| s.to_string_lossy().into_owned());
parent.join(format!("{file_name}.tmp.{pid}.{n}"))
}
@@ -742,8 +729,7 @@ pub(crate) fn upsert_asset_row(
kebab_core::SourceUri::File(p) => format!("file://{}", p.to_string_lossy()),
kebab_core::SourceUri::Kb(u) => u.clone(),
};
let media_type = serde_json::to_string(&asset.media_type)
.context("serialize media_type")?;
let media_type = serde_json::to_string(&asset.media_type).context("serialize media_type")?;
let discovered_at = asset
.discovered_at
.format(&time::format_description::well_known::Rfc3339)
@@ -864,9 +850,7 @@ impl SqliteStore {
/// skips rows where `code_lang` is NULL (i.e. non-code documents).
/// Returns `BTreeMap<String, u32>` — key is the canonical lowercase
/// language identifier (e.g. `"rust"`), value is the doc count.
pub fn code_lang_breakdown(
&self,
) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
pub fn code_lang_breakdown(&self) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
use anyhow::Context;
let conn = self.read_conn();
let mut stmt = conn
@@ -936,9 +920,7 @@ impl SqliteStore {
/// where `repo` is NULL (documents without an explicit repo tag).
/// Returns `BTreeMap<String, u32>` — key is the repo name as stored in
/// frontmatter, value is the doc count.
pub fn repo_breakdown(
&self,
) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
pub fn repo_breakdown(&self) -> anyhow::Result<std::collections::BTreeMap<String, u32>> {
use anyhow::Context;
let conn = self.read_conn();
let mut stmt = conn
@@ -1299,4 +1281,3 @@ mod tests {
assert_eq!(bd.len(), 1, "expected exactly 1 entry, got: {bd:?}");
}
}

View File

@@ -44,8 +44,16 @@ fn copy_mode_writes_file_with_0o644_and_correct_bytes() {
// Path: data_dir/assets/aa/aaaaaa…aa
let aa = &asset.asset_id.0[..2];
let dest = env.data_dir().join("assets").join(aa).join(&asset.asset_id.0);
assert!(dest.exists(), "asset file not written at {}", dest.display());
let dest = env
.data_dir()
.join("assets")
.join(aa)
.join(&asset.asset_id.0);
assert!(
dest.exists(),
"asset file not written at {}",
dest.display()
);
let on_disk = std::fs::read(&dest).unwrap();
assert_eq!(on_disk, bytes);
@@ -82,10 +90,16 @@ fn reference_mode_does_not_write_file_but_records_path() {
let mut asset = fixed_asset(bytes, 1, &cs);
asset.source_uri = SourceUri::File(PathBuf::from("/path/to/original.md"));
store.put_asset_with_bytes(&asset, bytes).expect("ref write");
store
.put_asset_with_bytes(&asset, bytes)
.expect("ref write");
let aa = &asset.asset_id.0[..2];
let dest = env.data_dir().join("assets").join(aa).join(&asset.asset_id.0);
let dest = env
.data_dir()
.join("assets")
.join(aa)
.join(&asset.asset_id.0);
assert!(!dest.exists(), "reference mode must not copy bytes");
let (storage_kind, storage_path): (String, String) = env.with_conn(|c| {
@@ -161,11 +175,18 @@ fn put_asset_with_bytes_sweeps_workspace_path_orphan() {
|row| row.get(0),
)
});
assert_eq!(new_count, 1, "new asset_id must own the workspace_path slot");
assert_eq!(
new_count, 1,
"new asset_id must own the workspace_path slot"
);
// New asset's bytes published at the final destination.
let aa = &asset.asset_id.0[..2];
let dest = env.data_dir().join("assets").join(aa).join(&asset.asset_id.0);
let dest = env
.data_dir()
.join("assets")
.join(aa)
.join(&asset.asset_id.0);
assert!(
dest.exists(),
"new asset bytes must be visible at {}",
@@ -185,7 +206,11 @@ fn put_asset_with_bytes_rejects_invalid_asset_id() {
// 32 chars but contains a `/` — would let `assets_path_for` stitch
// together a path outside the shard tree.
let evil_id = "../etc/passwd_padded_to_xx_xxxxx".to_string();
assert_eq!(evil_id.len(), 32, "test fixture must be 32 chars to exercise length-only checks");
assert_eq!(
evil_id.len(),
32,
"test fixture must be 32 chars to exercise length-only checks"
);
let mut asset = fixed_asset(b"x", 1, &b3_full_hex(b"x"));
asset.asset_id = AssetId(evil_id.clone());

View File

@@ -49,7 +49,10 @@ fn create_get_roundtrip() {
let store = open_store(&tmp);
let session = make_session("sess-1");
store.create_session(&session).unwrap();
let fetched = store.get_session("sess-1").unwrap().expect("session present");
let fetched = store
.get_session("sess-1")
.unwrap()
.expect("session present");
assert_eq!(fetched, session);
}
@@ -112,20 +115,16 @@ fn append_turn_bumps_session_updated_at() {
let store = open_store(&tmp);
let session = make_session("bump");
store.create_session(&session).unwrap();
let pre = store
.get_session("bump")
.unwrap()
.unwrap()
.updated_at;
let pre = store.get_session("bump").unwrap().unwrap().updated_at;
let mut t = make_turn("bump", 0);
t.created_at = pre + 100;
store.append_turn(&t).unwrap();
let post = store
.get_session("bump")
.unwrap()
.unwrap()
.updated_at;
assert_eq!(post, pre + 100, "updated_at must follow latest turn's created_at");
let post = store.get_session("bump").unwrap().unwrap().updated_at;
assert_eq!(
post,
pre + 100,
"updated_at must follow latest turn's created_at"
);
}
#[test]
@@ -168,7 +167,9 @@ fn list_sessions_respects_limit() {
let tmp = TempDir::new().unwrap();
let store = open_store(&tmp);
for i in 0..5 {
store.create_session(&make_session(&format!("s{i}"))).unwrap();
store
.create_session(&make_session(&format!("s{i}")))
.unwrap();
}
assert_eq!(store.list_sessions(2).unwrap().len(), 2);
assert_eq!(store.list_sessions(100).unwrap().len(), 5);

View File

@@ -10,7 +10,7 @@ use std::path::PathBuf;
use kebab_chunk::MdHeadingV1Chunker;
use kebab_core::{
AssetId, AssetStorage, Checksum, ChunkPolicy, ChunkerVersion, Chunker, DocumentStore,
AssetId, AssetStorage, Checksum, ChunkPolicy, Chunker, ChunkerVersion, DocumentStore,
MediaType, ParserVersion, RawAsset, SourceUri, WorkspacePath,
};
use kebab_parse_md::{BodyHints, build_canonical_document, parse_blocks, parse_frontmatter};
@@ -58,8 +58,7 @@ fn document_and_chunks_round_trip_through_sqlite() {
fs_mtime: asset.discovered_at,
fallback_lang: Some("en".into()),
};
let (mut metadata, _fm_span, _fm_warns) =
parse_frontmatter(&bytes, &hints).unwrap();
let (mut metadata, _fm_span, _fm_warns) = parse_frontmatter(&bytes, &hints).unwrap();
let (parsed_blocks, parse_warns) = parse_blocks(&bytes, 1).unwrap();
metadata.aliases.sort();
@@ -91,9 +90,7 @@ fn document_and_chunks_round_trip_through_sqlite() {
store
.put_blocks(&doc.doc_id, &doc.blocks)
.expect("put_blocks");
store
.put_chunks(&doc.doc_id, &chunks)
.expect("put_chunks");
store.put_chunks(&doc.doc_id, &chunks).expect("put_chunks");
// ── Read back ────────────────────────────────────────────────────
let loaded = store

View File

@@ -334,9 +334,7 @@ fn normalize_ws(s: &str) -> String {
/// - no `CREATE VIRTUAL TABLE chunks_fts` inside that block
/// - no `END;` after the virtual-table line
fn extract_design_5_5_fts_block() -> String {
let doc = include_str!(
"../../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md"
);
let doc = include_str!("../../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md");
let heading_idx = doc
.find("### 5.5 Chunks + FTS5")
.expect("design doc must contain `### 5.5 Chunks + FTS5` heading");
@@ -394,9 +392,7 @@ fn extract_migration_5_5_verbatim_block() -> String {
.expect("V007 must carry the `End §5.5 verbatim block` closing anchor")
+ after_open_line;
// Walk back from the close marker to the start of its comment line.
let close_line_start = migration[..close_idx]
.rfind('\n')
.map_or(0, |n| n + 1);
let close_line_start = migration[..close_idx].rfind('\n').map_or(0, |n| n + 1);
migration[after_open_line..close_line_start].to_string()
}
@@ -476,8 +472,7 @@ fn fts_store_drop_releases_wal_files() {
}
// The main DB file should likewise be removable.
if db_path.exists() {
std::fs::remove_file(&db_path)
.expect("main DB file should be removable after store drop");
std::fs::remove_file(&db_path).expect("main DB file should be removable after store drop");
}
}
@@ -584,11 +579,7 @@ fn fts_trigram_english_substring_hits() {
1,
"substring of 'tokenizer' — trigram recall"
);
assert_eq!(
count_match(&conn, "izer"),
1,
"substring of 'tokenizer'"
);
assert_eq!(count_match(&conn, "izer"), 1, "substring of 'tokenizer'");
// 3-char-minimum applies to English too.
assert_eq!(count_match(&conn, "to"), 0, "2-char English query");
}

View File

@@ -5,10 +5,9 @@
use std::path::PathBuf;
use kebab_core::{
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, Chunk, ChunkerVersion,
CommonBlock, DocumentId, DocumentStore, HeadingBlock, Lang, MediaType, Metadata,
ParserVersion, Provenance, RawAsset, SourceSpan, SourceType, SourceUri, TextBlock,
TrustLevel, WorkspacePath,
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, Chunk, ChunkerVersion, CommonBlock,
DocumentId, DocumentStore, HeadingBlock, Lang, MediaType, Metadata, ParserVersion, Provenance,
RawAsset, SourceSpan, SourceType, SourceUri, TextBlock, TrustLevel, WorkspacePath,
};
use kebab_store_sqlite::SqliteStore;
use time::OffsetDateTime;
@@ -66,7 +65,7 @@ fn make_doc() -> CanonicalDocument {
block_id: kebab_core::BlockId("c".repeat(32)),
heading_path: vec!["Title".into()],
source_span: span,
},
},
text: "body".into(),
inlines: vec![],
});
@@ -138,8 +137,7 @@ fn put_document_idempotent_bumps_doc_version() {
// Tags were re-derived: still exactly the two original tags.
let tags: Vec<String> = env.with_conn(|c| {
let mut stmt =
c.prepare("SELECT tag FROM document_tags WHERE doc_id = ? ORDER BY tag")?;
let mut stmt = c.prepare("SELECT tag FROM document_tags WHERE doc_id = ? ORDER BY tag")?;
let rows = stmt.query_map([&doc.doc_id.0], |r| r.get::<_, String>(0))?;
rows.collect::<rusqlite::Result<Vec<String>>>()
});
@@ -158,7 +156,9 @@ fn put_blocks_and_put_chunks_replace_not_duplicate() {
store.put_document(&doc).unwrap();
store.put_blocks(&doc.doc_id, &doc.blocks).unwrap();
store.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id)).unwrap();
store
.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id))
.unwrap();
let (b1, ch1): (i64, i64) = env.with_conn(|c| {
Ok((
@@ -179,7 +179,9 @@ fn put_blocks_and_put_chunks_replace_not_duplicate() {
// Re-put same data → counts unchanged (DELETE-then-INSERT).
store.put_blocks(&doc.doc_id, &doc.blocks).unwrap();
store.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id)).unwrap();
store
.put_chunks(&doc.doc_id, &make_chunks(&doc.doc_id))
.unwrap();
let (b2, ch2): (i64, i64) = env.with_conn(|c| {
Ok((
c.query_row(
@@ -214,9 +216,8 @@ fn put_blocks_transactional_rollback_on_fk_violation() {
store.put_document(&doc).unwrap();
// Establish a baseline row in `blocks`.
store.put_blocks(&doc.doc_id, &doc.blocks).unwrap();
let baseline: i64 = env.with_conn(|c| {
c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0))
});
let baseline: i64 =
env.with_conn(|c| c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0)));
assert_eq!(baseline, 2);
// Now ask put_blocks to write to a doc_id that does NOT exist.
@@ -237,9 +238,8 @@ fn put_blocks_transactional_rollback_on_fk_violation() {
let res = store.put_blocks(&phantom, &phantom_blocks);
assert!(res.is_err(), "FK violation must surface as Err");
let after: i64 = env.with_conn(|c| {
c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0))
});
let after: i64 =
env.with_conn(|c| c.query_row("SELECT COUNT(*) FROM blocks", [], |r| r.get(0)));
assert_eq!(
after, baseline,
"transaction must roll back; blocks count must be unchanged"

View File

@@ -9,8 +9,8 @@
use std::path::PathBuf;
use kebab_core::{
AssetId, ChunkerVersion, DocumentId, IngestItem, IngestItemKind, IngestReport,
ParserVersion, SourceScope, WorkspacePath,
AssetId, ChunkerVersion, DocumentId, IngestItem, IngestItemKind, IngestReport, ParserVersion,
SourceScope, WorkspacePath,
};
use serde_json::Value;

View File

@@ -29,9 +29,7 @@ fn create_then_progress_then_finish() {
assert_eq!(row[0].progress.as_ref().unwrap()["total"], json!(10));
// Finish with success.
store
.finish(&id, JobStatus::Succeeded, None)
.unwrap();
store.finish(&id, JobStatus::Succeeded, None).unwrap();
let row = store.list(&JobFilter::default()).unwrap();
assert_eq!(row[0].status, JobStatus::Succeeded);
assert!(row[0].finished_at.is_some());

View File

@@ -3,9 +3,9 @@
use std::path::PathBuf;
use kebab_core::{
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, CommonBlock, DocFilter,
DocumentId, DocumentStore, HeadingBlock, Lang, MediaType, Metadata, ParserVersion,
Provenance, RawAsset, SourceSpan, SourceType, SourceUri, TrustLevel, WorkspacePath,
AssetId, AssetStorage, Block, CanonicalDocument, Checksum, CommonBlock, DocFilter, DocumentId,
DocumentStore, HeadingBlock, Lang, MediaType, Metadata, ParserVersion, Provenance, RawAsset,
SourceSpan, SourceType, SourceUri, TrustLevel, WorkspacePath,
};
use kebab_store_sqlite::SqliteStore;
use time::OffsetDateTime;
@@ -84,7 +84,13 @@ fn list_documents_filters_lang_and_tags() {
store.run_migrations().unwrap();
for (asset, doc) in [
make_doc('a', "notes/a.md", "en", vec!["rust", "kb"], TrustLevel::Primary),
make_doc(
'a',
"notes/a.md",
"en",
vec!["rust", "kb"],
TrustLevel::Primary,
),
make_doc('b', "notes/b.md", "ko", vec!["rust"], TrustLevel::Secondary),
make_doc('c', "papers/c.md", "en", vec!["bio"], TrustLevel::Generated),
] {

View File

@@ -23,5 +23,8 @@ fn open_existing_does_not_create_missing_db() {
let dir = tempfile::tempdir().unwrap();
let nonexistent_db = dir.path().join("does-not-exist.sqlite");
let _ = SqliteStore::open_existing(&nonexistent_db);
assert!(!nonexistent_db.exists(), "open_existing must NOT create the file");
assert!(
!nonexistent_db.exists(),
"open_existing must NOT create the file"
);
}