refactor(rename): kb → kebab — binary, env vars, XDG paths, file renames

두 번째 commit. 사용자 facing surface (CLI binary, env vars, XDG paths)
+ 코드 안 single-letter token (`KB_`, `kb.sqlite`, `/kb/`, tracing
target) 일괄 rename. 그리고 3 개 file rename:

- 디자인 doc `2026-04-27-kb-final-form-design.md` →
  `2026-04-27-kebab-final-form-design.md`
- 최초 보고서 `kb_local_rust_report.md` → `kebab_local_rust_report.md`
- workspace ignore `.kbignore` → `.kebabignore`

## 변경

- `crates/kebab-cli/Cargo.toml`: `[[bin]] name = "kb"` → `"kebab"`.
- `crates/kebab-cli/src/main.rs`: `#[command(name = "kb", …)]` →
  `name = "kebab"`.
- 모든 `KB_*` env var (코드 + doc + 테스트) → `KEBAB_*`. apply_env
  prefix 매칭 + 30+ 개 setting 키 모두.
- XDG paths: `~/.config/kb` / `~/.local/share/kb` / `~/.cache/kb` /
  `~/.local/state/kb` → `~/.config/kebab` 등. config defaults +
  expand_path tests + paths.rs 의 hardcode 모두.
- SQLite filename: `kb.sqlite` → `kebab.sqlite` (`SQLITE_FILE` const
  + 테스트 hardcode 모두).
- tracing target: `target: "kb-*"` → `"kebab-*"` (10+ 곳).
- snapshot fixture: `.kbignore` → `.kebabignore` (`fixtures/source-fs/
  tree-1.snapshot.json` 갱신).

## 검증

- `cargo test --workspace -j 1` clean (linker OOM 회피 위해 직렬).
- `cargo clippy --workspace --all-targets -- -D warnings` clean.

다음 commit 에서 docs sweep.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 04:01:35 +00:00
parent 911fb49550
commit f1a448d6dc
38 changed files with 185 additions and 185 deletions

View File

@@ -61,7 +61,7 @@ pub use app::App;
/// Kept in lock-step with the literal used in the `kb-store-sqlite`
/// idempotency / round-trip tests so the version label written by the
/// app and the one used in cross-crate fixtures match.
const KB_PARSE_MD_VERSION: &str = "pulldown-cmark-0.x";
const KEBAB_PARSE_MD_VERSION: &str = "pulldown-cmark-0.x";
/// Caller-supplied knobs for one [`ask`] invocation.
///
@@ -187,7 +187,7 @@ pub fn ingest_with_config(
.context("kb-app::ingest: ensure Lance table")?;
}
let parser_version = ParserVersion(KB_PARSE_MD_VERSION.to_string());
let parser_version = ParserVersion(KEBAB_PARSE_MD_VERSION.to_string());
let chunk_policy = chunk_policy_from_config(&app.config);
// Pre-load every existing doc_id so we can label `IngestItem.kind`
@@ -236,7 +236,7 @@ pub fn ingest_with_config(
Ok(i) => i,
Err(e) => {
tracing::error!(
target: "kb-app",
target: "kebab-app",
path = %asset.workspace_path.0,
error = %e,
"kb-app::ingest: per-file fatal"
@@ -318,7 +318,7 @@ pub fn ingest_with_config(
progress,
) {
tracing::warn!(
target: "kb-app",
target: "kebab-app",
error = %e,
"kb-app::ingest: JobRepo::update_progress failed"
);
@@ -330,7 +330,7 @@ pub fn ingest_with_config(
None,
) {
tracing::warn!(
target: "kb-app",
target: "kebab-app",
error = %e,
"kb-app::ingest: JobRepo::finish failed"
);
@@ -338,7 +338,7 @@ pub fn ingest_with_config(
}
Err(e) => {
tracing::warn!(
target: "kb-app",
target: "kebab-app",
error = %e,
"kb-app::ingest: JobRepo::create failed; run not recorded in `jobs`"
);
@@ -361,7 +361,7 @@ pub fn ingest_with_config(
Ok(s) => Some(s),
Err(e) => {
tracing::warn!(
target: "kb-app",
target: "kebab-app",
error = %e,
"kb-app::ingest: failed to serialize items_json; storing NULL"
);
@@ -385,14 +385,14 @@ pub fn ingest_with_config(
};
if let Err(e) = app.sqlite.record_ingest_run(&row) {
tracing::warn!(
target: "kb-app",
target: "kebab-app",
error = %e,
"kb-app::ingest: record_ingest_run failed"
);
}
tracing::info!(
target: "kb-app",
target: "kebab-app",
scanned = scanned_count,
new = new_count,
updated = updated_count,
@@ -448,7 +448,7 @@ fn ingest_one_asset(
existing_doc_ids: &std::collections::HashSet<String>,
) -> anyhow::Result<kebab_core::IngestItem> {
tracing::debug!(
target: "kb-app::ingest",
target: "kebab-app::ingest",
path = %asset.workspace_path.0,
"processing asset"
);
@@ -795,7 +795,7 @@ pub fn doctor_with_config_path(config_path: Option<&std::path::Path>) -> anyhow:
// data_dir_writable — probe the resolved storage.data_dir from the
// loaded config when present, else the XDG default. Apply env
// overrides so KB_STORAGE_DATA_DIR is respected too.
// overrides so KEBAB_STORAGE_DATA_DIR is respected too.
let data_dir = match loaded_cfg.as_ref() {
Some(c) => {
// Re-apply env overrides on top so the same precedence as

View File

@@ -1,6 +1,6 @@
//! Tracing initialization helper for `kb-cli`.
//!
//! Daily-rolling file appender at `~/.local/state/kb/logs/` per task spec.
//! Daily-rolling file appender at `~/.local/state/kebab/logs/` per task spec.
//! Returns a `WorkerGuard` that the caller must keep alive until program
//! exit (so buffered log lines flush).

View File

@@ -87,8 +87,8 @@ fn ingest_records_ingest_runs_row_with_aggregate_counts() {
assert_eq!(report.scanned, 3);
let db_path = std::path::PathBuf::from(&env.config.storage.data_dir)
.join("kb.sqlite");
let conn = rusqlite::Connection::open(&db_path).expect("open kb.sqlite");
.join("kebab.sqlite");
let conn = rusqlite::Connection::open(&db_path).expect("open kebab.sqlite");
let (scanned, new_c, updated, skipped, errors, items_json): (
i64,
i64,

View File

@@ -186,7 +186,7 @@ impl Chunker for MdHeadingV1Chunker {
flush(&mut acc, doc, &chunker_version, &policy_hash, &mut out);
tracing::debug!(
target: "kb-chunk",
target: "kebab-chunk",
doc_id = %doc.doc_id,
chunks = out.len(),
"md-heading-v1 chunked",

View File

@@ -8,7 +8,7 @@ repository = { workspace = true }
description = "kb command-line interface"
[[bin]]
name = "kb"
name = "kebab"
path = "src/main.rs"
[dependencies]

View File

@@ -11,7 +11,7 @@ use kebab_app::doctor_signal::{DoctorUnhealthy, NoHitSignal, RefusalSignal};
mod wire;
#[derive(Parser, Debug)]
#[command(name = "kb", version, about = "personal local knowledge base")]
#[command(name = "kebab", version, about = "personal local knowledge base")]
struct Cli {
/// Path to a non-default `config.toml`.
#[arg(long, global = true)]

View File

@@ -1,6 +1,6 @@
//! `kb-config` — `Config` schema and XDG path resolution (§6).
//!
//! Layer order (`Config::load`): defaults → file → env (`KB_<SECTION>_<KEY>`).
//! Layer order (`Config::load`): defaults → file → env (`KEBAB_<SECTION>_<KEY>`).
//! CLI overrides land later, applied by `kb-cli` after `Config::load`.
use std::collections::HashMap;
@@ -113,8 +113,8 @@ impl Config {
],
},
storage: StorageCfg {
data_dir: "${XDG_DATA_HOME:-~/.local/share}/kb".to_string(),
sqlite: "{data_dir}/kb.sqlite".to_string(),
data_dir: "${XDG_DATA_HOME:-~/.local/share}/kebab".to_string(),
sqlite: "{data_dir}/kebab.sqlite".to_string(),
vector_dir: "{data_dir}/lancedb".to_string(),
asset_dir: "{data_dir}/assets".to_string(),
artifact_dir: "{data_dir}/artifacts".to_string(),
@@ -191,139 +191,139 @@ impl Config {
Ok(cfg)
}
/// Apply `KB_<SECTION>_<KEY>` env overrides. Unknown keys are ignored.
/// Apply `KEBAB_<SECTION>_<KEY>` env overrides. Unknown keys are ignored.
///
/// The mapping is an explicit grep-friendly whitelist — one match arm
/// per leaf key in `Config`. Booleans accept `1` / `true` / `yes`
/// (case-insensitive) for true and anything else for false. Numeric
/// keys silently keep their prior value if the env value fails to
/// parse, so a malformed `KB_*` cannot crash startup.
/// parse, so a malformed `KEBAB_*` cannot crash startup.
pub fn apply_env(mut self, env: &HashMap<String, String>) -> Self {
for (k, v) in env {
if !k.starts_with("KB_") {
if !k.starts_with("KEBAB_") {
continue;
}
match k.as_str() {
// workspace
"KB_WORKSPACE_ROOT" => self.workspace.root = v.clone(),
"KEBAB_WORKSPACE_ROOT" => self.workspace.root = v.clone(),
// storage
"KB_STORAGE_DATA_DIR" => self.storage.data_dir = v.clone(),
"KB_STORAGE_SQLITE" => self.storage.sqlite = v.clone(),
"KB_STORAGE_VECTOR_DIR" => self.storage.vector_dir = v.clone(),
"KB_STORAGE_ASSET_DIR" => self.storage.asset_dir = v.clone(),
"KB_STORAGE_ARTIFACT_DIR" => self.storage.artifact_dir = v.clone(),
"KB_STORAGE_MODEL_DIR" => self.storage.model_dir = v.clone(),
"KB_STORAGE_RUNS_DIR" => self.storage.runs_dir = v.clone(),
"KB_STORAGE_COPY_THRESHOLD_MB" => {
"KEBAB_STORAGE_DATA_DIR" => self.storage.data_dir = v.clone(),
"KEBAB_STORAGE_SQLITE" => self.storage.sqlite = v.clone(),
"KEBAB_STORAGE_VECTOR_DIR" => self.storage.vector_dir = v.clone(),
"KEBAB_STORAGE_ASSET_DIR" => self.storage.asset_dir = v.clone(),
"KEBAB_STORAGE_ARTIFACT_DIR" => self.storage.artifact_dir = v.clone(),
"KEBAB_STORAGE_MODEL_DIR" => self.storage.model_dir = v.clone(),
"KEBAB_STORAGE_RUNS_DIR" => self.storage.runs_dir = v.clone(),
"KEBAB_STORAGE_COPY_THRESHOLD_MB" => {
if let Ok(n) = v.parse::<u64>() {
self.storage.copy_threshold_mb = n;
}
}
// indexing
"KB_INDEXING_MAX_PARALLEL_EXTRACTORS" => {
"KEBAB_INDEXING_MAX_PARALLEL_EXTRACTORS" => {
if let Ok(n) = v.parse::<u32>() {
self.indexing.max_parallel_extractors = n;
}
}
"KB_INDEXING_MAX_PARALLEL_EMBEDDINGS" => {
"KEBAB_INDEXING_MAX_PARALLEL_EMBEDDINGS" => {
if let Ok(n) = v.parse::<u32>() {
self.indexing.max_parallel_embeddings = n;
}
}
"KB_INDEXING_WATCH_FILESYSTEM" => {
"KEBAB_INDEXING_WATCH_FILESYSTEM" => {
self.indexing.watch_filesystem = parse_bool(v);
}
// chunking
"KB_CHUNKING_TARGET_TOKENS" => {
"KEBAB_CHUNKING_TARGET_TOKENS" => {
if let Ok(n) = v.parse::<usize>() {
self.chunking.target_tokens = n;
}
}
"KB_CHUNKING_OVERLAP_TOKENS" => {
"KEBAB_CHUNKING_OVERLAP_TOKENS" => {
if let Ok(n) = v.parse::<usize>() {
self.chunking.overlap_tokens = n;
}
}
"KB_CHUNKING_RESPECT_MARKDOWN_HEADINGS" => {
"KEBAB_CHUNKING_RESPECT_MARKDOWN_HEADINGS" => {
self.chunking.respect_markdown_headings = parse_bool(v);
}
"KB_CHUNKING_CHUNKER_VERSION" => self.chunking.chunker_version = v.clone(),
"KEBAB_CHUNKING_CHUNKER_VERSION" => self.chunking.chunker_version = v.clone(),
// models.embedding
"KB_MODELS_EMBEDDING_PROVIDER" => self.models.embedding.provider = v.clone(),
"KB_MODELS_EMBEDDING_MODEL" => self.models.embedding.model = v.clone(),
"KB_MODELS_EMBEDDING_VERSION" => self.models.embedding.version = v.clone(),
"KB_MODELS_EMBEDDING_DIMENSIONS" => {
"KEBAB_MODELS_EMBEDDING_PROVIDER" => self.models.embedding.provider = v.clone(),
"KEBAB_MODELS_EMBEDDING_MODEL" => self.models.embedding.model = v.clone(),
"KEBAB_MODELS_EMBEDDING_VERSION" => self.models.embedding.version = v.clone(),
"KEBAB_MODELS_EMBEDDING_DIMENSIONS" => {
if let Ok(n) = v.parse::<usize>() {
self.models.embedding.dimensions = n;
}
}
"KB_MODELS_EMBEDDING_BATCH_SIZE" => {
"KEBAB_MODELS_EMBEDDING_BATCH_SIZE" => {
if let Ok(n) = v.parse::<usize>() {
self.models.embedding.batch_size = n;
}
}
// models.llm
"KB_MODELS_LLM_PROVIDER" => self.models.llm.provider = v.clone(),
"KB_MODELS_LLM_MODEL" => self.models.llm.model = v.clone(),
"KB_MODELS_LLM_CONTEXT_TOKENS" => {
"KEBAB_MODELS_LLM_PROVIDER" => self.models.llm.provider = v.clone(),
"KEBAB_MODELS_LLM_MODEL" => self.models.llm.model = v.clone(),
"KEBAB_MODELS_LLM_CONTEXT_TOKENS" => {
if let Ok(n) = v.parse::<usize>() {
self.models.llm.context_tokens = n;
}
}
"KB_MODELS_LLM_ENDPOINT" => self.models.llm.endpoint = v.clone(),
"KB_MODELS_LLM_TEMPERATURE" => {
"KEBAB_MODELS_LLM_ENDPOINT" => self.models.llm.endpoint = v.clone(),
"KEBAB_MODELS_LLM_TEMPERATURE" => {
if let Ok(f) = v.parse::<f32>() {
self.models.llm.temperature = f;
}
}
"KB_MODELS_LLM_SEED" => {
"KEBAB_MODELS_LLM_SEED" => {
if let Ok(n) = v.parse::<u64>() {
self.models.llm.seed = n;
}
}
// search
"KB_SEARCH_DEFAULT_K" => {
"KEBAB_SEARCH_DEFAULT_K" => {
if let Ok(n) = v.parse::<usize>() {
self.search.default_k = n;
}
}
"KB_SEARCH_HYBRID_FUSION" => self.search.hybrid_fusion = v.clone(),
"KB_SEARCH_RRF_K" => {
"KEBAB_SEARCH_HYBRID_FUSION" => self.search.hybrid_fusion = v.clone(),
"KEBAB_SEARCH_RRF_K" => {
if let Ok(n) = v.parse::<u32>() {
self.search.rrf_k = n;
}
}
"KB_SEARCH_SNIPPET_CHARS" => {
"KEBAB_SEARCH_SNIPPET_CHARS" => {
if let Ok(n) = v.parse::<usize>() {
self.search.snippet_chars = n;
}
}
// rag
"KB_RAG_PROMPT_TEMPLATE_VERSION" => {
"KEBAB_RAG_PROMPT_TEMPLATE_VERSION" => {
self.rag.prompt_template_version = v.clone();
}
"KB_RAG_SCORE_GATE" => {
"KEBAB_RAG_SCORE_GATE" => {
if let Ok(f) = v.parse::<f32>() {
self.rag.score_gate = f;
}
}
"KB_RAG_EXPLAIN_DEFAULT" => {
"KEBAB_RAG_EXPLAIN_DEFAULT" => {
self.rag.explain_default = parse_bool(v);
}
"KB_RAG_MAX_CONTEXT_TOKENS" => {
"KEBAB_RAG_MAX_CONTEXT_TOKENS" => {
if let Ok(n) = v.parse::<usize>() {
self.rag.max_context_tokens = n;
}
}
// Unknown KB_* keys are silently ignored — see
// Unknown KEBAB_* keys are silently ignored — see
// `env_unknown_key_is_ignored` test.
_ => {}
}
@@ -331,58 +331,58 @@ impl Config {
self
}
/// `~/.config/kb/config.toml` (honors `XDG_CONFIG_HOME`).
/// `~/.config/kebab/config.toml` (honors `XDG_CONFIG_HOME`).
pub fn xdg_config_path() -> PathBuf {
if let Ok(custom) = std::env::var("XDG_CONFIG_HOME") {
if !custom.is_empty() {
return PathBuf::from(custom).join("kb").join("config.toml");
return PathBuf::from(custom).join("kebab").join("config.toml");
}
}
match dirs::config_dir() {
Some(d) => d.join("kb").join("config.toml"),
None => PathBuf::from("./kb/config.toml"),
Some(d) => d.join("kebab").join("config.toml"),
None => PathBuf::from("./kebab/config.toml"),
}
}
/// `~/.local/share/kb` (honors `XDG_DATA_HOME`).
/// `~/.local/share/kebab` (honors `XDG_DATA_HOME`).
pub fn xdg_data_dir() -> PathBuf {
if let Ok(custom) = std::env::var("XDG_DATA_HOME") {
if !custom.is_empty() {
return PathBuf::from(custom).join("kb");
return PathBuf::from(custom).join("kebab");
}
}
match dirs::data_dir() {
Some(d) => d.join("kb"),
None => PathBuf::from("./kb-data"),
Some(d) => d.join("kebab"),
None => PathBuf::from("./kebab-data"),
}
}
/// `~/.cache/kb` (honors `XDG_CACHE_HOME`).
/// `~/.cache/kebab` (honors `XDG_CACHE_HOME`).
pub fn xdg_cache_dir() -> PathBuf {
if let Ok(custom) = std::env::var("XDG_CACHE_HOME") {
if !custom.is_empty() {
return PathBuf::from(custom).join("kb");
return PathBuf::from(custom).join("kebab");
}
}
match dirs::cache_dir() {
Some(d) => d.join("kb"),
None => PathBuf::from("./kb-cache"),
Some(d) => d.join("kebab"),
None => PathBuf::from("./kebab-cache"),
}
}
/// `~/.local/state/kb` (honors `XDG_STATE_HOME`).
/// `~/.local/state/kebab` (honors `XDG_STATE_HOME`).
pub fn xdg_state_dir() -> PathBuf {
if let Ok(custom) = std::env::var("XDG_STATE_HOME") {
if !custom.is_empty() {
return PathBuf::from(custom).join("kb");
return PathBuf::from(custom).join("kebab");
}
}
// `dirs` doesn't expose state_dir on all platforms; fall back to
// `$HOME/.local/state/kb` if XDG_STATE_HOME is unset.
// `$HOME/.local/state/kebab` if XDG_STATE_HOME is unset.
if let Some(home) = dirs::home_dir() {
return home.join(".local").join("state").join("kb");
return home.join(".local").join("state").join("kebab");
}
PathBuf::from("./kb-state")
PathBuf::from("./kebab-state")
}
}
@@ -417,7 +417,7 @@ mod tests {
#[test]
fn env_override_score_gate() {
let mut env = HashMap::new();
env.insert("KB_RAG_SCORE_GATE".to_string(), "0.5".to_string());
env.insert("KEBAB_RAG_SCORE_GATE".to_string(), "0.5".to_string());
let c = Config::defaults().apply_env(&env);
assert!((c.rag.score_gate - 0.5).abs() < 1e-6);
}
@@ -425,7 +425,7 @@ mod tests {
#[test]
fn env_override_search_k() {
let mut env = HashMap::new();
env.insert("KB_SEARCH_DEFAULT_K".to_string(), "25".to_string());
env.insert("KEBAB_SEARCH_DEFAULT_K".to_string(), "25".to_string());
let c = Config::defaults().apply_env(&env);
assert_eq!(c.search.default_k, 25);
}
@@ -434,7 +434,7 @@ mod tests {
fn env_unknown_key_is_ignored() {
let baseline = Config::defaults();
let mut env = HashMap::new();
env.insert("KB_NOPE_FOO".to_string(), "garbage".to_string());
env.insert("KEBAB_NOPE_FOO".to_string(), "garbage".to_string());
let c = Config::defaults().apply_env(&env);
assert_eq!(c, baseline);
}
@@ -442,7 +442,7 @@ mod tests {
#[test]
fn env_overrides_chunking_target_tokens() {
let mut env = HashMap::new();
env.insert("KB_CHUNKING_TARGET_TOKENS".to_string(), "777".to_string());
env.insert("KEBAB_CHUNKING_TARGET_TOKENS".to_string(), "777".to_string());
let c = Config::defaults().apply_env(&env);
assert_eq!(c.chunking.target_tokens, 777);
}
@@ -451,10 +451,10 @@ mod tests {
fn env_overrides_models_llm_endpoint_and_temperature() {
let mut env = HashMap::new();
env.insert(
"KB_MODELS_LLM_ENDPOINT".to_string(),
"KEBAB_MODELS_LLM_ENDPOINT".to_string(),
"http://10.0.0.1:11434".to_string(),
);
env.insert("KB_MODELS_LLM_TEMPERATURE".to_string(), "0.7".to_string());
env.insert("KEBAB_MODELS_LLM_TEMPERATURE".to_string(), "0.7".to_string());
let c = Config::defaults().apply_env(&env);
assert_eq!(c.models.llm.endpoint, "http://10.0.0.1:11434");
assert!((c.models.llm.temperature - 0.7).abs() < 1e-6);
@@ -464,7 +464,7 @@ mod tests {
fn env_overrides_indexing_watch_filesystem_bool() {
let mut env = HashMap::new();
env.insert(
"KB_INDEXING_WATCH_FILESYSTEM".to_string(),
"KEBAB_INDEXING_WATCH_FILESYSTEM".to_string(),
"true".to_string(),
);
let c = Config::defaults().apply_env(&env);
@@ -477,10 +477,10 @@ mod tests {
let prev = std::env::var("XDG_CONFIG_HOME").ok();
// SAFETY: tests in this module run sequentially; we restore below.
unsafe {
std::env::set_var("XDG_CONFIG_HOME", "/tmp/kbtest-xdg-config");
std::env::set_var("XDG_CONFIG_HOME", "/tmp/kebabtest-xdg-config");
}
let p = Config::xdg_config_path();
assert_eq!(p, PathBuf::from("/tmp/kbtest-xdg-config/kb/config.toml"));
assert_eq!(p, PathBuf::from("/tmp/kebabtest-xdg-config/kebab/config.toml"));
// SAFETY: scope-local restore.
unsafe {
match prev {

View File

@@ -1,7 +1,7 @@
//! Shared path expansion helper.
//!
//! `Config::storage.*` fields are stored as raw template strings (e.g.
//! `${XDG_DATA_HOME:-~/.local/share}/kb`, `{data_dir}/runs`). Every
//! `${XDG_DATA_HOME:-~/.local/share}/kebab`, `{data_dir}/runs`). Every
//! crate that turns one of those strings into a real filesystem path
//! needs to apply the same set of substitutions; this module is the
//! single source of truth so the behavior cannot drift.
@@ -133,8 +133,8 @@ mod tests {
// SAFETY: lock held for the duration of this test.
unsafe { std::env::set_var("XDG_DATA_HOME", "/custom/path") };
let p = expand_path("${XDG_DATA_HOME:-~/.local/share}/kb", "");
assert_eq!(p, PathBuf::from("/custom/path/kb"));
let p = expand_path("${XDG_DATA_HOME:-~/.local/share}/kebab", "");
assert_eq!(p, PathBuf::from("/custom/path/kebab"));
}
#[test]
@@ -145,8 +145,8 @@ mod tests {
unsafe { std::env::remove_var("XDG_DATA_HOME") };
let home = std::env::var("HOME").expect("HOME must be set in tests");
let expected = PathBuf::from(home).join(".local/share/kb");
let p = expand_path("${XDG_DATA_HOME:-~/.local/share}/kb", "");
let expected = PathBuf::from(home).join(".local/share/kebab");
let p = expand_path("${XDG_DATA_HOME:-~/.local/share}/kebab", "");
assert_eq!(p, expected);
}
@@ -180,7 +180,7 @@ mod tests {
// SAFETY: lock held for the duration of this test.
unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") };
let p = expand_path("{data_dir}/runs", "/xdg/data/kb");
assert_eq!(p, PathBuf::from("/xdg/data/kb/runs"));
let p = expand_path("{data_dir}/runs", "/xdg/data/kebab");
assert_eq!(p, PathBuf::from("/xdg/data/kebab/runs"));
}
}

View File

@@ -4,7 +4,7 @@
//! `kb-*` crate, so every other crate in the workspace can depend on it
//! freely.
//!
//! See `docs/superpowers/specs/2026-04-27-kb-final-form-design.md` for
//! See `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` for
//! the canonical type bodies — this crate is the byte-for-byte mirror.
pub mod ids;

View File

@@ -19,7 +19,7 @@
//! rules `kb-store-sqlite` applies to `data_dir` (`${XDG_DATA_HOME:-…}`,
//! leading `~`, `{data_dir}` substitution).
//!
//! See `docs/superpowers/specs/2026-04-27-kb-final-form-design.md`
//! See `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`
//! §7.2 (Embedder), §6.4 ([models.embedding]), §9 (versioning).
use std::sync::Mutex;
@@ -82,7 +82,7 @@ impl FastembedEmbedder {
check_dim(model_info.dim, config.models.embedding.dimensions)?;
tracing::info!(
target: "kb-embed-local",
target: "kebab-embed-local",
cache_dir = %cache_dir.display(),
model = %config.models.embedding.model,
dims = model_info.dim,
@@ -97,7 +97,7 @@ impl FastembedEmbedder {
.with_cache_dir(cache_dir.clone())
.with_show_download_progress(false);
tracing::info!(
target: "kb-embed-local",
target: "kebab-embed-local",
model = %config.models.embedding.model,
cache_dir = %cache_dir.display(),
"loading embedding model (first run will download ~470MB)"
@@ -106,7 +106,7 @@ impl FastembedEmbedder {
.context("fastembed: TextEmbedding::try_new")?;
let dimensions = model_info.dim;
tracing::info!(
target: "kb-embed-local",
target: "kebab-embed-local",
model = %config.models.embedding.model,
dimensions,
"embedding model loaded"

View File

@@ -26,7 +26,7 @@ use kebab_embed::{Embedder, EmbeddingInput, EmbeddingKind};
use kebab_embed_local::FastembedEmbedder;
/// Build a `Config` whose `data_dir` lives in a per-process temp dir so
/// the test never writes into the developer's real `~/.local/share/kb`.
/// the test never writes into the developer's real `~/.local/share/kebab`.
/// Returns the `Config` and the `TempDir` guard (caller keeps the guard
/// alive for the test duration).
fn test_config() -> (kebab_config::Config, tempfile::TempDir) {

View File

@@ -11,7 +11,7 @@
//! deterministic test double. Real adapters (fastembed, candle, ollama-embed)
//! live in p3-2 and MUST NOT be implemented here.
//!
//! See `docs/superpowers/specs/2026-04-27-kb-final-form-design.md` §7.1, §7.2,
//! See `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §7.1, §7.2,
//! §11 for the contract.
// ── Trait re-exports ──────────────────────────────────────────────────────

View File

@@ -40,10 +40,10 @@ const STORAGE_DECIMALS: u32 = 4;
/// (P5-1) used — otherwise `expected_*` / `must_contain` won't line up
/// with the stored `query_id`s. `pub(crate)` so the runner shares the
/// exact same name + default rather than duplicating constants.
pub(crate) const KB_EVAL_GOLDEN: &str = "KB_EVAL_GOLDEN";
pub(crate) const KEBAB_EVAL_GOLDEN: &str = "KEBAB_EVAL_GOLDEN";
/// Default golden YAML path (relative to CWD when set). Same
/// rationale as [`KB_EVAL_GOLDEN`] — single source of truth.
/// rationale as [`KEBAB_EVAL_GOLDEN`] — single source of truth.
pub(crate) const DEFAULT_GOLDEN_PATH: &str = "fixtures/golden_queries.yaml";
/// Aggregate metrics for one stored eval run.
@@ -151,7 +151,7 @@ pub fn store_aggregate_with_config(
/// the runner uses, same default path. Pulled into its own helper so
/// `compare_runs` can share it.
pub(crate) fn resolve_golden_path() -> PathBuf {
match std::env::var(KB_EVAL_GOLDEN) {
match std::env::var(KEBAB_EVAL_GOLDEN) {
Ok(s) if !s.is_empty() => PathBuf::from(s),
_ => PathBuf::from(DEFAULT_GOLDEN_PATH),
}
@@ -161,7 +161,7 @@ fn load_golden_for_metrics() -> Result<Vec<GoldenQuery>> {
let path = resolve_golden_path();
load_golden_set(&path).with_context(|| {
format!(
"load golden set from {} (override via KB_EVAL_GOLDEN)",
"load golden set from {} (override via KEBAB_EVAL_GOLDEN)",
path.display()
)
})

View File

@@ -13,7 +13,7 @@ use kebab_store_sqlite::{EvalRunRow, SqliteStore};
use time::OffsetDateTime;
use crate::loader::{load_golden_set, validate_against_db};
use crate::metrics::{DEFAULT_GOLDEN_PATH, KB_EVAL_GOLDEN};
use crate::metrics::{DEFAULT_GOLDEN_PATH, KEBAB_EVAL_GOLDEN};
use crate::types::{EvalRun, EvalRunOpts, GoldenQuery, QueryResult};
/// Convert a wall-clock duration since `start` into milliseconds clamped
@@ -46,7 +46,7 @@ pub fn run_eval_with_config(cfg: &kebab_config::Config, opts: &EvalRunOpts) -> R
let golden_path = resolve_golden_path();
let queries = load_golden_set(&golden_path).with_context(|| {
format!(
"load golden set from {} (override via KB_EVAL_GOLDEN)",
"load golden set from {} (override via KEBAB_EVAL_GOLDEN)",
golden_path.display()
)
})?;
@@ -55,7 +55,7 @@ pub fn run_eval_with_config(cfg: &kebab_config::Config, opts: &EvalRunOpts) -> R
// ── 2. Mint identifiers + open store ──────────────────────────────────
let run_id = mint_run_id();
let created_at = OffsetDateTime::now_utc();
let commit_hash = std::env::var("KB_COMMIT_HASH")
let commit_hash = std::env::var("KEBAB_COMMIT_HASH")
.ok()
.filter(|s| !s.is_empty());
@@ -110,7 +110,7 @@ pub fn run_eval_with_config(cfg: &kebab_config::Config, opts: &EvalRunOpts) -> R
let duration_ms = elapsed_ms_u32(started);
tracing::info!(
target: "kb-eval",
target: "kebab-eval",
run_id = %run_id,
suite = %opts.suite,
queries = per_query.len(),
@@ -136,11 +136,11 @@ fn mint_run_id() -> String {
format!("run_{id}")
}
/// Resolve the golden YAML path. Honors the `KB_EVAL_GOLDEN` env
/// Resolve the golden YAML path. Honors the `KEBAB_EVAL_GOLDEN` env
/// override; otherwise relative to CWD. The path is NOT expanded for
/// `~` / `${...}` placeholders — direct file paths only.
fn resolve_golden_path() -> PathBuf {
match std::env::var(KB_EVAL_GOLDEN) {
match std::env::var(KEBAB_EVAL_GOLDEN) {
Ok(s) if !s.is_empty() => PathBuf::from(s),
_ => PathBuf::from(DEFAULT_GOLDEN_PATH),
}

View File

@@ -34,7 +34,7 @@ fn cfg_with_data_dir(tmp: &TempDir, golden_yaml: &str) -> Config {
// SAFELY scoped — `set_var` is process-global so callers serialise
// tests via the `serial_test`-style guard below.
unsafe {
std::env::set_var("KB_EVAL_GOLDEN", &golden_path);
std::env::set_var("KEBAB_EVAL_GOLDEN", &golden_path);
}
cfg
}
@@ -127,9 +127,9 @@ fn write_run(
store.record_eval_run_with_results(&row, &results).unwrap();
}
/// Each test mutates a process-global env var (`KB_EVAL_GOLDEN`) and
/// Each test mutates a process-global env var (`KEBAB_EVAL_GOLDEN`) and
/// expects to see its own write. Take this mutex around the body of
/// every test that touches `KB_EVAL_GOLDEN` so two concurrent test
/// every test that touches `KEBAB_EVAL_GOLDEN` so two concurrent test
/// threads don't trip over each other's golden YAML.
fn env_guard() -> std::sync::MutexGuard<'static, ()> {
use std::sync::{Mutex, OnceLock};

View File

@@ -7,7 +7,7 @@
//! workspace's source-of-truth,
//! - lexical-only retrieval (`SearchMode::Lexical`) so no embedder is
//! required (`models.embedding.provider = "none"`),
//! - golden YAML pointed at via `KB_EVAL_GOLDEN`.
//! - golden YAML pointed at via `KEBAB_EVAL_GOLDEN`.
//!
//! Determinism: lexical-only with a fixed seed corpus produces
//! byte-identical `per_query.jsonl` content (modulo `run_id` /
@@ -24,7 +24,7 @@ use kebab_store_sqlite::SqliteStore;
use rusqlite::params;
use tempfile::TempDir;
/// `KB_EVAL_GOLDEN` is process-global state. Tests touching it must
/// `KEBAB_EVAL_GOLDEN` is process-global state. Tests touching it must
/// serialize so they don't trample each other when `cargo test`
/// runs them in parallel.
static GOLDEN_ENV_LOCK: Mutex<()> = Mutex::new(());
@@ -143,19 +143,19 @@ fn lexical_opts() -> EvalRunOpts {
}
}
/// Run the eval after pointing `KB_EVAL_GOLDEN` at `yaml`. The env
/// Run the eval after pointing `KEBAB_EVAL_GOLDEN` at `yaml`. The env
/// guard must outlive the call so concurrent tests don't reset the
/// var mid-run.
fn run_with_golden<F: FnOnce() -> R, R>(yaml: &Path, f: F) -> R {
let _g = GOLDEN_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
// SAFETY: `KB_EVAL_GOLDEN` is a benign env var; the GOLDEN_ENV_LOCK
// SAFETY: `KEBAB_EVAL_GOLDEN` is a benign env var; the GOLDEN_ENV_LOCK
// serializes mutations so concurrent tests don't race.
unsafe {
std::env::set_var("KB_EVAL_GOLDEN", yaml);
std::env::set_var("KEBAB_EVAL_GOLDEN", yaml);
}
let out = f();
unsafe {
std::env::remove_var("KB_EVAL_GOLDEN");
std::env::remove_var("KEBAB_EVAL_GOLDEN");
}
out
}

View File

@@ -29,7 +29,7 @@
//! - **Lazy connect.** [`OllamaLanguageModel::new`] does not hit the network;
//! the first error surfaces on [`LanguageModel::generate_stream`].
//!
//! See `docs/superpowers/specs/2026-04-27-kb-final-form-design.md` §7.2,
//! See `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §7.2,
//! §6.4 (`[models.llm]`), §0 Q5 (streaming), §10 (errors), and report §11.2
//! (Ollama protocol notes).

View File

@@ -19,8 +19,8 @@ use kebab_llm_local::{LanguageModel, OllamaLanguageModel};
#[ignore = "requires a local Ollama daemon + pulled model"]
fn real_ollama_streams_non_empty_response() {
// Use whatever model the workspace defaults select. Override via the
// KB_MODELS_LLM_MODEL env var if you want a different one for this run
// (e.g. `KB_MODELS_LLM_MODEL=qwen2.5:7b-instruct cargo test ... -- --ignored`).
// KEBAB_MODELS_LLM_MODEL env var if you want a different one for this run
// (e.g. `KEBAB_MODELS_LLM_MODEL=qwen2.5:7b-instruct cargo test ... -- --ignored`).
let cfg = Config::load(None).expect("config should load");
let llm = OllamaLanguageModel::new(&cfg).unwrap();

View File

@@ -12,7 +12,7 @@
//! from `generate_stream` itself (e.g., connection refused) before any chunk
//! is yielded; the mock never does.
//!
//! See `docs/superpowers/specs/2026-04-27-kb-final-form-design.md` §7.1, §7.2,
//! See `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §7.1, §7.2,
//! §0 Q5 (streaming), §3.8 (`ModelRef`) for the contract.
// ── Trait re-exports ──────────────────────────────────────────────────────

View File

@@ -96,7 +96,7 @@ pub fn build_canonical_document(
.collect();
tracing::debug!(
target: "kb-normalize",
target: "kebab-normalize",
"built canonical document doc_id={} blocks={}",
doc_id.0,
lifted_blocks.len()

View File

@@ -135,7 +135,7 @@ impl RagPipeline {
let top_score = hits.first().map(|h| h.retrieval.fusion_score).unwrap_or(0.0);
tracing::debug!(
target: "kb-rag",
target: "kebab-rag",
chunks_returned,
top_score,
mode = ?opts.mode,
@@ -161,7 +161,7 @@ impl RagPipeline {
// collapse to the more accurate `NoChunks` refusal here.
if packed_entries.is_empty() {
tracing::warn!(
target: "kb-rag",
target: "kebab-rag",
chunks_returned = hits.len(),
"kb-rag: all retrieved chunks were unfetchable from the store; \
falling back to NoChunks refusal"
@@ -324,7 +324,7 @@ impl RagPipeline {
// Drop the moved `finish_reason` early into a tracing breadcrumb; the
// wire schema does not surface it (per design §3.8).
tracing::debug!(
target: "kb-rag",
target: "kebab-rag",
grounded = answer.grounded,
refusal = ?answer.refusal_reason,
refusal_phrase_detected = matched_refusal_phrase,
@@ -354,7 +354,7 @@ impl RagPipeline {
self.docs.put_answer(&answer, query, packed_chunks_json.as_deref())
{
tracing::warn!(
target: "kb-rag",
target: "kebab-rag",
error = %e,
"kb-rag: put_answer failed; in-memory Answer still returned"
);
@@ -386,7 +386,7 @@ impl RagPipeline {
Some(c) => c.text,
None => {
tracing::warn!(
target: "kb-rag",
target: "kebab-rag",
chunk_id = %hit.chunk_id.0,
"kb-rag: chunk not found in store; skipping"
);
@@ -454,7 +454,7 @@ impl RagPipeline {
created_at: OffsetDateTime::now_utc(),
};
if let Err(e) = self.docs.put_answer(&answer, query, None) {
tracing::warn!(target: "kb-rag", error = %e, "kb-rag: put_answer (NoChunks) failed");
tracing::warn!(target: "kebab-rag", error = %e, "kb-rag: put_answer (NoChunks) failed");
}
Ok(answer)
}
@@ -529,7 +529,7 @@ impl RagPipeline {
created_at: OffsetDateTime::now_utc(),
};
if let Err(e) = self.docs.put_answer(&answer, query, None) {
tracing::warn!(target: "kb-rag", error = %e, "kb-rag: put_answer (ScoreGate) failed");
tracing::warn!(target: "kebab-rag", error = %e, "kb-rag: put_answer (ScoreGate) failed");
}
Ok(answer)
}

View File

@@ -93,7 +93,7 @@ impl HybridRetriever {
let vec_iv = vector.index_version();
if lex_iv.0 != vec_iv.0 {
tracing::warn!(
target: "kb-search",
target: "kebab-search",
lexical_index = %lex_iv.0,
vector_index = %vec_iv.0,
"kb-search hybrid: lexical and vector index_version differ; consider re-indexing"
@@ -323,7 +323,7 @@ fn parse_fusion(name: &str, k_rrf: u32) -> FusionPolicy {
"rrf" => FusionPolicy::Rrf { k_rrf: k },
other => {
tracing::warn!(
target: "kb-search",
target: "kebab-search",
policy = other,
"kb-search hybrid: unknown fusion policy; falling back to RRF"
);

View File

@@ -157,17 +157,17 @@ fn hybrid_snapshot_run_1() {
.join("hybrid")
.join("run-1.json");
if std::env::var_os("KB_UPDATE_SNAPSHOTS").is_some() {
if std::env::var_os("KEBAB_UPDATE_SNAPSHOTS").is_some() {
std::fs::create_dir_all(fixture.parent().unwrap()).unwrap();
std::fs::write(&fixture, serde_json::to_string_pretty(&actual).unwrap()).unwrap();
eprintln!("[snapshot] regenerated {}", fixture.display());
// Fail loudly so that accidentally setting KB_UPDATE_SNAPSHOTS
// Fail loudly so that accidentally setting KEBAB_UPDATE_SNAPSHOTS
// in CI surfaces as a test failure rather than a silent
// overwrite + green run. Same fail-loud-instead-of-silent-pass
// philosophy as P3-2's `SNAPSHOT_HASH_BASELINE = 0` and P3-3's
// placeholder fixture guards.
panic!(
"[snapshot] regenerated {}, re-run without KB_UPDATE_SNAPSHOTS to verify pin",
"[snapshot] regenerated {}, re-run without KEBAB_UPDATE_SNAPSHOTS to verify pin",
fixture.display()
);
}
@@ -176,7 +176,7 @@ fn hybrid_snapshot_run_1() {
serde_json::from_str(&std::fs::read_to_string(&fixture).unwrap_or_else(|_| {
panic!(
"missing snapshot fixture at {}; run with \
KB_UPDATE_SNAPSHOTS=1 to create",
KEBAB_UPDATE_SNAPSHOTS=1 to create",
fixture.display()
)
}))
@@ -189,14 +189,14 @@ fn hybrid_snapshot_run_1() {
panic!(
"snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \
Path: {}. To regenerate: \
`KB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.",
`KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.",
fixture.display()
);
}
assert_eq!(
actual, expected,
"hybrid snapshot drift; rerun with KB_UPDATE_SNAPSHOTS=1 to regenerate"
"hybrid snapshot drift; rerun with KEBAB_UPDATE_SNAPSHOTS=1 to regenerate"
);
// Independent guard: fusion scores must be non-increasing across

View File

@@ -29,7 +29,7 @@ impl Env {
config.storage.data_dir = temp.path().to_string_lossy().into_owned();
let store = SqliteStore::open(&config).expect("open store");
store.run_migrations().expect("run migrations");
let db_path = temp.path().join("kb.sqlite");
let db_path = temp.path().join("kebab.sqlite");
Self {
_temp: temp,
store: Arc::new(store),
@@ -618,7 +618,7 @@ fn lexical_snapshot_run_1() {
// `Vec<SearchHit>` for a fixed query is checked verbatim against
// `tests/fixtures/search/lexical/run-1.json`. Update both sides in
// the same commit when intentional changes ship.
// Stable because rusqlite ships bundled SQLite — a tokenizer/bm25 algorithm change in a future SQLite bump will require regenerating run-1.json via `KB_UPDATE_SNAPSHOTS=1`.
// Stable because rusqlite ships bundled SQLite — a tokenizer/bm25 algorithm change in a future SQLite bump will require regenerating run-1.json via `KEBAB_UPDATE_SNAPSHOTS=1`.
let env = Env::new();
let conn = env.raw_conn();
insert_document(&conn, &id32("d"), "notes/snap.md", "Snap", "en", "primary", &[]);
@@ -656,11 +656,11 @@ fn lexical_snapshot_run_1() {
let baseline_path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/search/lexical/run-1.json");
if std::env::var_os("KB_UPDATE_SNAPSHOTS").is_some() {
if std::env::var_os("KEBAB_UPDATE_SNAPSHOTS").is_some() {
std::fs::write(&baseline_path, serde_json::to_string_pretty(&actual).unwrap()).unwrap();
}
let baseline_text = std::fs::read_to_string(&baseline_path)
.expect("baseline snapshot must exist; run with KB_UPDATE_SNAPSHOTS=1 to seed");
.expect("baseline snapshot must exist; run with KEBAB_UPDATE_SNAPSHOTS=1 to seed");
let expected: serde_json::Value = serde_json::from_str(&baseline_text).unwrap();
assert_eq!(actual, expected, "lexical run-1 snapshot drift");
}

View File

@@ -74,13 +74,13 @@ impl SourceConnector for FsSourceConnector {
scope.root.clone()
};
// Union: config.workspace.exclude scope.exclude .kbignore.
// Per §6.2 the union of `.kbignore` and `config.workspace.exclude`
// Union: config.workspace.exclude scope.exclude .kebabignore.
// Per §6.2 the union of `.kebabignore` and `config.workspace.exclude`
// is the filter set. `scope.exclude` is added on top so a caller
// can layer a per-call narrowing.
let mut excludes = self.default_exclude.clone();
excludes.extend(scope.exclude.iter().cloned());
// .kbignore is re-read on every scan() so users can edit it without
// .kebabignore is re-read on every scan() so users can edit it without
// restarting any long-running process.
let kbignore = read_kbignore(&root)?;
@@ -243,7 +243,7 @@ mod tests {
fn scan_filters_by_kbignore() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join(".kbignore"), "*.tmp\n").unwrap();
std::fs::write(root.join(".kebabignore"), "*.tmp\n").unwrap();
std::fs::write(root.join("a.md"), b"x").unwrap();
std::fs::write(root.join("b.tmp"), b"x").unwrap();
@@ -252,14 +252,14 @@ mod tests {
.unwrap();
let v = conn.scan(&SourceScope::default()).unwrap();
let names: Vec<_> = v.iter().map(|a| a.workspace_path.0.clone()).collect();
// Decision: `.kbignore` itself IS emitted as a RawAsset (MediaType::Other("")).
// Decision: `.kebabignore` itself IS emitted as a RawAsset (MediaType::Other("")).
// Rationale: a config file that affects ingest is itself part of the
// workspace contents; the markdown extractor (P1-2) will reject Other("")
// on its own. If we ever decide to omit `.kbignore` from the asset list,
// on its own. If we ever decide to omit `.kebabignore` from the asset list,
// this test will catch it.
assert!(
names.contains(&".kbignore".to_string()),
".kbignore must be emitted as an asset; got: {names:?}"
names.contains(&".kebabignore".to_string()),
".kebabignore must be emitted as an asset; got: {names:?}"
);
assert!(names.contains(&"a.md".to_string()));
assert!(!names.contains(&"b.tmp".to_string()));
@@ -285,7 +285,7 @@ mod tests {
fn scan_unions_config_exclude_and_kbignore() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join(".kbignore"), "*.tmp\n").unwrap();
std::fs::write(root.join(".kebabignore"), "*.tmp\n").unwrap();
std::fs::write(root.join("a.md"), b"x").unwrap();
std::fs::write(root.join("b.tmp"), b"x").unwrap();
std::fs::write(root.join("c.log"), b"x").unwrap();

View File

@@ -1,10 +1,10 @@
//! `kb-source-fs` — local filesystem `SourceConnector`.
//!
//! Walks `config.workspace.root`, applies gitignore-style filters from
//! `config.workspace.exclude` `.kbignore`, computes BLAKE3 of every file,
//! `config.workspace.exclude` `.kebabignore`, computes BLAKE3 of every file,
//! and emits `Vec<RawAsset>` sorted by `workspace_path` for determinism.
//!
//! Per design §3.3 (RawAsset), §6.2 (workspace + .kbignore), §6.6 (POSIX
//! Per design §3.3 (RawAsset), §6.2 (workspace + .kebabignore), §6.6 (POSIX
//! normalization), §7.1 (SourceScope), §7.2 (SourceConnector), §8 (module
//! boundaries).

View File

@@ -3,7 +3,7 @@
//!
//! Filter set (per task spec, design §6.2):
//! - `config.workspace.exclude` (passed in by `FsSourceConnector`)
//! - `<root>/.kbignore` (optional file at workspace root)
//! - `<root>/.kebabignore` (optional file at workspace root)
//! - default-excludes for `.DS_Store` and macOS resource forks (`._*`)
//!
//! All three are merged via `ignore::overrides::OverrideBuilder`, which
@@ -36,7 +36,7 @@ use walkdir::{DirEntry, WalkDir};
/// Default-excludes baked into the connector. These are NOT configurable;
/// they cover noise that is never useful to ingest and would otherwise need
/// to appear in every user's `.kbignore`.
/// to appear in every user's `.kebabignore`.
const DEFAULT_EXCLUDES: &[&str] = &[
// Finder metadata
".DS_Store",
@@ -46,7 +46,7 @@ const DEFAULT_EXCLUDES: &[&str] = &[
"**/._*",
];
/// Build the merged `Override` from `config.workspace.exclude` `.kbignore`
/// Build the merged `Override` from `config.workspace.exclude` `.kebabignore`
/// baked-in default excludes.
///
/// Each input pattern is registered as an *exclude* (gitignore-style: a
@@ -73,16 +73,16 @@ pub(crate) fn build_overrides(
for pat in kbignore_patterns {
builder
.add(&format!("!{pat}"))
.with_context(|| format!("invalid .kbignore pattern: {pat}"))?;
.with_context(|| format!("invalid .kebabignore pattern: {pat}"))?;
}
builder.build().context("failed to compile override set")
}
/// Read `<root>/.kbignore` if it exists. Each non-blank, non-comment line is
/// Read `<root>/.kebabignore` if it exists. Each non-blank, non-comment line is
/// a gitignore pattern. Missing file → empty Vec (not an error).
pub(crate) fn read_kbignore(root: &Path) -> Result<Vec<String>> {
let path = root.join(".kbignore");
let path = root.join(".kebabignore");
if !path.exists() {
return Ok(Vec::new());
}
@@ -250,7 +250,7 @@ mod tests {
fn read_kbignore_strips_blanks_and_comments() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join(".kbignore"),
dir.path().join(".kebabignore"),
"# comment\n*.tmp\n\nignored/**\n",
)
.unwrap();

View File

@@ -9,8 +9,8 @@
//! │ ├── alpha.md
//! │ └── beta.md
//! ├── ignored/
//! │ └── skip.tmp # excluded by .kbignore
//! ├── .kbignore # contains: *.tmp
//! │ └── skip.tmp # excluded by .kebabignore
//! ├── .kebabignore # contains: *.tmp
//! └── .DS_Store # implicitly excluded
//! ```
//!
@@ -52,7 +52,7 @@ fn cfg_for_fixture(root: &str) -> Config {
let mut c = Config::defaults();
c.workspace.root = root.to_string();
// Clear default excludes (`.git/**`, `node_modules/**`, `.obsidian/**`)
// so the snapshot is purely a function of the fixture + .kbignore +
// so the snapshot is purely a function of the fixture + .kebabignore +
// baked-in default-excludes.
c.workspace.exclude.clear();
c
@@ -101,18 +101,18 @@ fn scan_and_strip() -> Value {
fn tree_1_snapshot_matches_baseline() {
let actual = scan_and_strip();
// If KB_REGEN_SNAPSHOT is set, (re)write the baseline and exit
// If KEBAB_REGEN_SNAPSHOT is set, (re)write the baseline and exit
// *before* attempting to read it. This is the only path that may
// create the file from scratch.
if std::env::var_os("KB_REGEN_SNAPSHOT").is_some() {
if std::env::var_os("KEBAB_REGEN_SNAPSHOT").is_some() {
let pretty = serde_json::to_string_pretty(&actual).unwrap() + "\n";
std::fs::write(baseline_path(), pretty).expect("write baseline");
panic!("regenerated baseline; rerun without KB_REGEN_SNAPSHOT to verify");
panic!("regenerated baseline; rerun without KEBAB_REGEN_SNAPSHOT to verify");
}
let baseline_text = std::fs::read_to_string(baseline_path()).unwrap_or_else(|_| {
panic!(
"missing baseline at {} — regenerate via `KB_REGEN_SNAPSHOT=1 cargo test \
"missing baseline at {} — regenerate via `KEBAB_REGEN_SNAPSHOT=1 cargo test \
-p kb-source-fs --test snapshot_tree1 -- tree_1_snapshot_matches_baseline`",
baseline_path().display()
)

View File

@@ -28,7 +28,7 @@ const ASSET_ID_HEX_LEN: usize = 32;
/// Default file name under `config.storage.data_dir`. Kept private — the
/// path layout is a §6.3 design decision, not part of the store's public
/// surface.
const SQLITE_FILE: &str = "kb.sqlite";
const SQLITE_FILE: &str = "kebab.sqlite";
/// Subdirectory under `data_dir` holding shard-prefixed asset bytes
/// (`<aa>/<asset_id>`). Mirrors design §6.3.
@@ -74,7 +74,7 @@ impl SqliteStore {
apply_pragmas(&conn)?;
tracing::debug!(
target: "kb-store-sqlite",
target: "kebab-store-sqlite",
data_dir = %data_dir.display(),
db = %db_path.display(),
"opened sqlite store"
@@ -94,7 +94,7 @@ impl SqliteStore {
schema::runner()
.run(&mut *conn)
.map_err(|e| StoreError::Migration(e.to_string()))?;
tracing::debug!(target: "kb-store-sqlite", "migrations applied");
tracing::debug!(target: "kebab-store-sqlite", "migrations applied");
Ok(())
}

View File

@@ -37,7 +37,7 @@ impl TestEnv {
}
pub fn db_path(&self) -> PathBuf {
self.temp.path().join("kb.sqlite")
self.temp.path().join("kebab.sqlite")
}
/// Open a side-channel rusqlite connection for direct SQL inspection.

View File

@@ -335,7 +335,7 @@ fn normalize_ws(s: &str) -> String {
/// - 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-kb-final-form-design.md"
"../../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md"
);
let heading_idx = doc
.find("### 5.5 Chunks + FTS5")
@@ -437,7 +437,7 @@ fn fts_v002_matches_design_section_5_5_verbatim() {
// ── 6. WAL cleanup: drop store before tempdir reaps WAL/SHM ──────────
/// Mirror the P1-6 pattern: opening + migrating + dropping the store
/// must not strand `kb.sqlite-wal`/`-shm` files such that the tempdir
/// must not strand `kebab.sqlite-wal`/`-shm` files such that the tempdir
/// can't be cleaned up. After dropping the store + side-channel conn,
/// the WAL/SHM siblings must either not exist or be removable — if a
/// stray handle were holding them open, on Windows the remove would

View File

@@ -20,7 +20,7 @@
//! serializes vector ops, and current-thread saves the two worker
//! threads a multi-thread runtime spawns by default.
//!
//! See `docs/superpowers/specs/2026-04-27-kb-final-form-design.md`
//! See `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`
//! §5.6 (embedding_records DDL), §6.3 (lancedb table naming),
//! §7.2 (VectorStore), §9 (versioning).

View File

@@ -111,7 +111,7 @@ impl LanceVectorStore {
})?;
tracing::debug!(
target: "kb-store-vector",
target: "kebab-store-vector",
vector_dir = %vector_dir.display(),
"opened LanceVectorStore"
);
@@ -141,7 +141,7 @@ impl LanceVectorStore {
.await
.context("create_empty_table")?;
tracing::info!(
target: "kb-store-vector",
target: "kebab-store-vector",
table = table_name,
dim,
"created Lance table"
@@ -275,7 +275,7 @@ impl VectorStore for LanceVectorStore {
.context("phase 3: mark embedding_records committed")?;
tracing::info!(
target: "kb-store-vector",
target: "kebab-store-vector",
table = %table_name,
rows = recs.len(),
"upsert committed"
@@ -306,7 +306,7 @@ impl VectorStore for LanceVectorStore {
Some(name) => name,
None => {
tracing::debug!(
target: "kb-store-vector",
target: "kebab-store-vector",
dim,
"search: no Lance table matches query dim — returning empty"
);
@@ -477,7 +477,7 @@ fn decode_lance_hits(batches: &[RecordBatch]) -> Result<Vec<LanceCandidate>> {
fn score_from_distance(distance: f32) -> f32 {
if distance.is_nan() {
tracing::warn!(
target: "kb-store-vector",
target: "kebab-store-vector",
"NaN cosine distance from Lance — coercing to score 0"
);
return 0.0;
@@ -515,7 +515,7 @@ async fn find_matching_table(
}
Err(e) => {
tracing::warn!(
target: "kb-store-vector",
target: "kebab-store-vector",
table = %name,
error = %e,
"search: skipped unopenable table"

View File

@@ -73,7 +73,7 @@ fn vector_hits_snapshot_run_1() {
.join("vector")
.join("run-1.json");
if std::env::var_os("KB_UPDATE_SNAPSHOTS").is_some() {
if std::env::var_os("KEBAB_UPDATE_SNAPSHOTS").is_some() {
std::fs::create_dir_all(fixture.parent().unwrap()).unwrap();
std::fs::write(&fixture, serde_json::to_string_pretty(&actual).unwrap())
.unwrap();
@@ -83,7 +83,7 @@ fn vector_hits_snapshot_run_1() {
let expected: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&fixture).unwrap_or_else(
|_| panic!(
"missing snapshot fixture at {}; run with KB_UPDATE_SNAPSHOTS=1 to create",
"missing snapshot fixture at {}; run with KEBAB_UPDATE_SNAPSHOTS=1 to create",
fixture.display()
),
))
@@ -97,14 +97,14 @@ fn vector_hits_snapshot_run_1() {
panic!(
"snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \
Path: {}. To regenerate: \
`KB_UPDATE_SNAPSHOTS=1 cargo test -p kb-store-vector -- --ignored snapshot`.",
`KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-store-vector -- --ignored snapshot`.",
fixture.display()
);
}
assert_eq!(
actual, expected,
"snapshot drift; rerun with KB_UPDATE_SNAPSHOTS=1 to regenerate"
"snapshot drift; rerun with KEBAB_UPDATE_SNAPSHOTS=1 to regenerate"
);
// Independent guard: scores must be non-increasing.

View File

@@ -15,7 +15,7 @@
"kind": "copied",
"path": "<stripped>"
},
"workspace_path": ".kbignore"
"workspace_path": ".kebabignore"
},
{
"asset_id": "ba6cd31cab86eff7a86638ee76494bcf",