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

@@ -10,8 +10,7 @@ use kebab_app::classify;
pub fn to_tool_error(err: &anyhow::Error) -> CallToolResult {
let v1 = classify(err, false);
let body = serde_json::to_string(&v1).unwrap_or_else(|_| {
r#"{"schema_version":"error.v1","code":"generic","message":"serialize failed"}"#
.to_string()
r#"{"schema_version":"error.v1","code":"generic","message":"serialize failed"}"#.to_string()
});
CallToolResult::error(vec![Content::text(body)])
}

View File

@@ -142,17 +142,13 @@ impl ServerHandler for KebabHandler {
}
"search" => {
let args = request.arguments.unwrap_or_default();
self.spawn_tool(args, |state, input| {
tools::search::handle(&state, input)
})
.await
self.spawn_tool(args, |state, input| tools::search::handle(&state, input))
.await
}
"ask" => {
let args = request.arguments.unwrap_or_default();
self.spawn_tool(args, |state, input| {
tools::ask::handle(&state, input)
})
.await
self.spawn_tool(args, |state, input| tools::ask::handle(&state, input))
.await
}
"ingest_file" => {
let args = request.arguments.unwrap_or_default();
@@ -170,10 +166,8 @@ impl ServerHandler for KebabHandler {
}
"fetch" => {
let args = request.arguments.unwrap_or_default();
self.spawn_tool(args, |state, input| {
tools::fetch::handle(&state, input)
})
.await
self.spawn_tool(args, |state, input| tools::fetch::handle(&state, input))
.await
}
"bulk_search" => {
let args = request.arguments.unwrap_or_default();

View File

@@ -51,9 +51,7 @@ pub fn handle(state: &KebabAppState, input: AskInput) -> CallToolResult {
};
let cfg_clone = (*state.config).clone();
let result = match input.session_id {
Some(sid) => {
kebab_app::ask_with_session_with_config(cfg_clone, &sid, &input.query, opts)
}
Some(sid) => kebab_app::ask_with_session_with_config(cfg_clone, &sid, &input.query, opts),
None => kebab_app::ask_with_config(cfg_clone, &input.query, opts),
};
match result {

View File

@@ -49,9 +49,7 @@ pub fn handle(state: &KebabAppState, input: FetchInput) -> CallToolResult {
_ => return invalid_input("kind=span requires doc_id, line_start, line_end"),
},
other => {
return invalid_input(&format!(
"unknown kind '{other}'; expected chunk|doc|span"
));
return invalid_input(&format!("unknown kind '{other}'; expected chunk|doc|span"));
}
};

View File

@@ -24,8 +24,9 @@ pub fn handle(state: &KebabAppState, input: IngestFileInput) -> CallToolResult {
Ok(report) => match serde_json::to_value(&report) {
Ok(mut v) => {
if let serde_json::Value::Object(ref mut map) = v {
map.entry("schema_version".to_string())
.or_insert_with(|| serde_json::Value::String("ingest_report.v1".to_string()));
map.entry("schema_version".to_string()).or_insert_with(|| {
serde_json::Value::String("ingest_report.v1".to_string())
});
}
match serde_json::to_string(&v) {
Ok(json) => to_tool_success(json),

View File

@@ -29,8 +29,9 @@ pub fn handle(state: &KebabAppState, input: IngestStdinInput) -> CallToolResult
Ok(report) => match serde_json::to_value(&report) {
Ok(mut v) => {
if let serde_json::Value::Object(ref mut map) = v {
map.entry("schema_version".to_string())
.or_insert_with(|| serde_json::Value::String("ingest_report.v1".to_string()));
map.entry("schema_version".to_string()).or_insert_with(|| {
serde_json::Value::String("ingest_report.v1".to_string())
});
}
match serde_json::to_string(&v) {
Ok(json) => to_tool_success(json),

View File

@@ -1,10 +1,10 @@
//! Tool implementations — one module per tool.
pub mod schema;
pub mod doctor;
pub mod search;
pub mod ask;
pub mod bulk_search;
pub mod doctor;
pub mod fetch;
pub mod ingest_file;
pub mod ingest_stdin;
pub mod fetch;
pub mod bulk_search;
pub mod schema;
pub mod search;

View File

@@ -2,8 +2,8 @@
//! Input: {} (no args). Output: schema.v1 JSON.
use rmcp::model::CallToolResult;
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::error::{to_tool_error, to_tool_success};
use crate::state::KebabAppState;

View File

@@ -79,15 +79,10 @@ pub fn handle(state: &KebabAppState, input: SearchInput) -> CallToolResult {
let ingested_after = match input.ingested_after.as_deref() {
Some(s) => {
match time::OffsetDateTime::parse(
s,
&time::format_description::well_known::Rfc3339,
) {
match time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339) {
Ok(ts) => Some(ts),
Err(e) => {
return invalid_input(&format!(
"ingested_after: invalid RFC3339 '{s}': {e}"
));
return invalid_input(&format!("ingested_after: invalid RFC3339 '{s}': {e}"));
}
}
}
@@ -152,8 +147,7 @@ pub fn handle(state: &KebabAppState, input: SearchInput) -> CallToolResult {
"truncated": resp.truncated,
});
if let Some(trace) = &resp.trace {
let trace_v =
serde_json::to_value(trace).unwrap_or(serde_json::Value::Null);
let trace_v = serde_json::to_value(trace).unwrap_or(serde_json::Value::Null);
if let serde_json::Value::Object(ref mut map) = envelope {
map.insert("trace".to_string(), trace_v);
}

View File

@@ -23,7 +23,11 @@ async fn schema_tool_emits_error_v1_when_db_missing() {
handler.state(),
kebab_mcp::tools::schema::SchemaInput::default(),
);
assert_eq!(result.is_error, Some(true), "expected isError=true on missing DB");
assert_eq!(
result.is_error,
Some(true),
"expected isError=true on missing DB"
);
let content = result.content.first().unwrap();
let text = match &content.raw {
@@ -31,6 +35,9 @@ async fn schema_tool_emits_error_v1_when_db_missing() {
other => panic!("expected text content, got {other:?}"),
};
let v: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(v.get("schema_version").and_then(|s| s.as_str()), Some("error.v1"));
assert_eq!(
v.get("schema_version").and_then(|s| s.as_str()),
Some("error.v1")
);
assert_eq!(v.get("code").and_then(|s| s.as_str()), Some("not_indexed"));
}

View File

@@ -9,10 +9,7 @@ use rmcp::model::RawContent;
fn minimal_config(data_dir: &std::path::Path, workspace_root: &std::path::Path) -> Config {
let mut cfg = Config::defaults();
cfg.storage.data_dir = data_dir.to_string_lossy().into_owned();
cfg.storage.model_dir = data_dir
.join("models")
.to_string_lossy()
.into_owned();
cfg.storage.model_dir = data_dir.join("models").to_string_lossy().into_owned();
cfg.workspace.root = workspace_root.to_string_lossy().into_owned();
cfg.workspace.exclude.clear();
cfg.models.embedding.provider = "none".to_string();

View File

@@ -31,7 +31,11 @@ fn setup() -> (tempfile::TempDir, KebabHandler) {
"# Alpha\n\nThis document mentions kebab and bread.",
)
.unwrap();
let scope = SourceScope { root: workspace_root.clone(), include: vec![], exclude: vec![] };
let scope = SourceScope {
root: workspace_root.clone(),
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(config.clone(), scope, false).unwrap();
let state = KebabAppState::new(config, None);
let handler = KebabHandler::new(state);
@@ -39,7 +43,10 @@ fn setup() -> (tempfile::TempDir, KebabHandler) {
}
fn extract_json(result: &rmcp::model::CallToolResult) -> serde_json::Value {
assert!(!result.is_error.unwrap_or(false), "expected isError=false, got {result:?}");
assert!(
!result.is_error.unwrap_or(false),
"expected isError=false, got {result:?}"
);
let content = result.content.first().expect("at least one content item");
let text = match &content.raw {
RawContent::Text(t) => &t.text,
@@ -89,7 +96,7 @@ async fn bulk_search_invalid_item_field_continues_with_per_item_error() {
let input = kebab_mcp::tools::bulk_search::BulkSearchInput {
queries: vec![
json!({"query": "kebab", "mode": "lexical"}),
json!({"query": "bread", "mode": "bogus"}), // invalid mode
json!({"query": "bread", "mode": "bogus"}), // invalid mode
],
};
let result = kebab_mcp::tools::bulk_search::handle(handler.state(), input);
@@ -117,5 +124,8 @@ async fn bulk_search_over_cap_returns_tool_error() {
RawContent::Text(t) => &t.text,
other => panic!("expected Text content, got {other:?}"),
};
assert!(text.contains("max 100"), "expected 'max 100' in error: {text}");
assert!(
text.contains("max 100"),
"expected 'max 100' in error: {text}"
);
}

View File

@@ -15,10 +15,7 @@ use rmcp::model::RawContent;
fn minimal_config(data_dir: &std::path::Path, workspace_root: &std::path::Path) -> Config {
let mut cfg = Config::defaults();
cfg.storage.data_dir = data_dir.to_string_lossy().into_owned();
cfg.storage.model_dir = data_dir
.join("models")
.to_string_lossy()
.into_owned();
cfg.storage.model_dir = data_dir.join("models").to_string_lossy().into_owned();
cfg.workspace.root = workspace_root.to_string_lossy().into_owned();
cfg.workspace.exclude.clear();
cfg.models.embedding.provider = "none".to_string();

View File

@@ -112,6 +112,14 @@ async fn ingest_file_tool_idempotent_on_second_call() {
other => panic!("expected text, got {other:?}"),
};
let v2: serde_json::Value = serde_json::from_str(text2).unwrap();
assert_eq!(v2.get("new").and_then(serde_json::Value::as_u64), Some(0), "{v2:?}");
assert_eq!(v2.get("unchanged").and_then(serde_json::Value::as_u64), Some(1), "{v2:?}");
assert_eq!(
v2.get("new").and_then(serde_json::Value::as_u64),
Some(0),
"{v2:?}"
);
assert_eq!(
v2.get("unchanged").and_then(serde_json::Value::as_u64),
Some(1),
"{v2:?}"
);
}

View File

@@ -10,10 +10,7 @@ use rmcp::model::RawContent;
fn minimal_config(data_dir: &std::path::Path, workspace_root: &std::path::Path) -> Config {
let mut cfg = Config::defaults();
cfg.storage.data_dir = data_dir.to_string_lossy().into_owned();
cfg.storage.model_dir = data_dir
.join("models")
.to_string_lossy()
.into_owned();
cfg.storage.model_dir = data_dir.join("models").to_string_lossy().into_owned();
cfg.workspace.root = workspace_root.to_string_lossy().into_owned();
cfg.workspace.exclude.clear();
cfg.models.embedding.provider = "none".to_string();
@@ -52,7 +49,10 @@ async fn schema_tool_returns_schema_v1_json() {
"expected isError=false on healthy schema, got {result:?}"
);
let content = result.content.first().expect("expected at least one content item");
let content = result
.content
.first()
.expect("expected at least one content item");
// Content = Annotated<RawContent>; deref to get the inner RawContent.
let text = match &content.raw {
@@ -67,7 +67,9 @@ async fn schema_tool_returns_schema_v1_json() {
"unexpected schema_version in: {v}"
);
assert_eq!(
v.get("capabilities").and_then(|c| c.get("mcp_server")).and_then(serde_json::Value::as_bool),
v.get("capabilities")
.and_then(|c| c.get("mcp_server"))
.and_then(serde_json::Value::as_bool),
Some(true),
"mcp_server capability flag should be true after fb-30",
);

View File

@@ -10,10 +10,7 @@ use rmcp::model::RawContent;
fn minimal_config(data_dir: &std::path::Path, workspace_root: &std::path::Path) -> Config {
let mut cfg = Config::defaults();
cfg.storage.data_dir = data_dir.to_string_lossy().into_owned();
cfg.storage.model_dir = data_dir
.join("models")
.to_string_lossy()
.into_owned();
cfg.storage.model_dir = data_dir.join("models").to_string_lossy().into_owned();
cfg.workspace.root = workspace_root.to_string_lossy().into_owned();
cfg.workspace.exclude.clear();
cfg.models.embedding.provider = "none".to_string();
@@ -99,15 +96,15 @@ async fn search_tool_returns_search_response_v1() {
"expected at least one hit for 'kebab' in 'a.md'"
);
assert_eq!(
hits[0]
.get("schema_version")
.and_then(|s| s.as_str()),
hits[0].get("schema_version").and_then(|s| s.as_str()),
Some("search_hit.v1"),
"first hit should carry schema_version=search_hit.v1"
);
// truncated must be present (bool); next_cursor may be null on last page.
assert!(
v.get("truncated").and_then(serde_json::Value::as_bool).is_some(),
v.get("truncated")
.and_then(serde_json::Value::as_bool)
.is_some(),
"envelope should carry truncated:bool"
);
assert!(

View File

@@ -79,7 +79,10 @@ async fn search_with_trace_true_returns_trace_field() {
let result = kebab_mcp::tools::search::handle(handler.state(), make_input(Some(true)));
let v = extract_json(&result);
assert_eq!(v["schema_version"], "search_response.v1");
assert!(v["trace"].is_object(), "trace field present when trace:true");
assert!(
v["trace"].is_object(),
"trace field present when trace:true"
);
assert!(v["trace"]["timing"]["total_ms"].is_number());
assert!(v["trace"]["lexical"].is_array());
assert!(v["trace"]["vector"].is_array());

View File

@@ -7,7 +7,12 @@ use kebab_mcp::build_tools_vec;
#[test]
fn tools_list_returns_eight_tools() {
let tools = build_tools_vec();
assert_eq!(tools.len(), 8, "expected exactly 8 tools, got {}", tools.len());
assert_eq!(
tools.len(),
8,
"expected exactly 8 tools, got {}",
tools.len()
);
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
assert!(names.contains(&"schema"), "missing 'schema' tool");
@@ -15,7 +20,10 @@ fn tools_list_returns_eight_tools() {
assert!(names.contains(&"search"), "missing 'search' tool");
assert!(names.contains(&"ask"), "missing 'ask' tool");
assert!(names.contains(&"ingest_file"), "missing 'ingest_file' tool");
assert!(names.contains(&"ingest_stdin"), "missing 'ingest_stdin' tool");
assert!(
names.contains(&"ingest_stdin"),
"missing 'ingest_stdin' tool"
);
assert!(names.contains(&"fetch"), "missing 'fetch' tool");
assert!(names.contains(&"bulk_search"), "missing 'bulk_search' tool");
}