feat(kebab-config + kebab-app): p9-fb-05 workspace.root path policy

도그푸딩 item 3 — `workspace.root` 의 허용 형식이 명문화 안 돼 사용자가
\"상대 경로면 어디 기준?\" 가 불명확. 이제 절대/tilde/env/상대 모두
지원하되, 상대 경로의 base 는 **config.toml 자체가 위치한 디렉토리**
(사용자의 cwd 와 무관) 로 일관 정책.

## 핵심 변경

- **`kebab_config::expand_path_with_base(raw, data_dir, base_dir)`**
  신규. 기존 `expand_path` (tilde + env 만) 위에 relative-path
  resolution 추가:
  - tilde / 절대 / `${VAR}` 입력은 base_dir 무시 (이미 absolute)
  - relative 입력만 `base_dir.join(...)` 로 절대화
- **`Config.source_dir: Option<PathBuf>`** 신규 (`#[serde(skip)]`).
  `Config::from_file` / `load` 가 `path.parent()` 로 stamp. defaults
  는 None (cwd fallback).
- **`Config::resolve_workspace_root()`** helper: source_dir 있으면
  그것 기준, 없으면 cwd 기준.
- **callsite 정리**:
  - `kebab-app::lib.rs` 의 3 군데 `expand_tilde(&app.config.workspace
    .root)` → `app.config.resolve_workspace_root()`
  - `kebab-app::init_workspace` 도 동일
  - `kebab-source-fs::FsSourceConnector::new` → 동일
  - kebab-source-fs 의 fork 된 local `expand_tilde` + `dirs_home`
    헬퍼 제거 (kebab-config 가 canonical)
- **`kebab init`** 가 생성하는 `config.toml` 위에 path policy 안내
  헤더 코멘트 prepend (절대/tilde/env/상대 + 상대 base = config dir).

기존 `expand_tilde` 가 kebab-app/lib.rs 에 한 군데 (storage.data_dir)
남음 — spec out-of-scope (\"expand_tilde 통일 P+\") 라 보류.

## 테스트

- `expand_path_with_base` 에 신규 4 unit (relative→base, absolute
  ignores base, tilde ignores base, ${XDG} ignores base)
- 기존 27 kebab-config tests + workspace 전체 (`cargo test --workspace
  --no-fail-fast -j 1` exit 0) 모두 통과
- `cargo clippy --workspace --all-targets -- -D warnings` clean

## 문서

- README Configuration 절: workspace.root 형식 + relative base 규칙
  한 줄 추가
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress

## 영향

기존 사용자: 영향 없음 (defaults 의 `~/KnowledgeBase` 는 tilde-rooted,
relative path 분기 안 탐). 새 사용자가 `--config /tmp/cfg.toml` +
`root = "kb"` 같이 쓰면 cwd 무관하게 `/tmp/kb` 가 워크스페이스가 됨 —
이전엔 이 케이스가 cwd 기준이라 invisible foot-gun.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 04:20:06 +00:00
parent cd4fdd5500
commit f6cc612dbe
7 changed files with 167 additions and 37 deletions

View File

@@ -123,13 +123,36 @@ pub fn init_workspace(force: bool) -> anyhow::Result<()> {
}
}
let workspace_root = expand_tilde(&kebab_config::Config::defaults().workspace.root);
let workspace_root = kebab_config::Config::defaults().resolve_workspace_root();
std::fs::create_dir_all(&workspace_root)?;
if !cfg_path.exists() || force {
let cfg = kebab_config::Config::defaults();
let toml_text = toml::to_string_pretty(&cfg)?;
std::fs::write(&cfg_path, toml_text)?;
// p9-fb-05: prepend a header comment documenting the path
// policy so a user editing this file knows what's allowed
// for `workspace.root` (and how relative paths resolve).
// The actual key lives inside `[workspace]` further down;
// we keep the explanation up top because users skim header
// comments first.
let header = "\
# kebab config — `~/.config/kebab/config.toml`.
#
# `workspace.root` accepts:
# • absolute paths (`/home/me/KnowledgeBase`)
# • tilde (`~/KnowledgeBase`) ← default
# • env vars (`${XDG_DATA_HOME}/kebab`)
# • relative paths (`./notes`, `notes`, `../shared/x`)
# — relative paths resolve against the directory of THIS
# config file, NOT the user's `cwd` at invocation time.
#
# Override individual keys at runtime with `KEBAB_*` env vars
# (e.g. `KEBAB_WORKSPACE_ROOT=/tmp/test kebab ingest`).
\n";
let mut combined = String::with_capacity(header.len() + toml_text.len());
combined.push_str(header);
combined.push_str(&toml_text);
std::fs::write(&cfg_path, combined)?;
}
Ok(())
@@ -881,7 +904,9 @@ fn ingest_one_image_asset(
// `~` / `${XDG_…}` expansion via the same helper the markdown
// path uses, so a `~/KnowledgeBase` workspace.root resolves
// identically across all media (HOTFIXES 2026-05-02 P9-4 follow-up).
let workspace_root = expand_tilde(&app.config.workspace.root);
// p9-fb-05: relative `workspace.root` resolves against the config
// file's directory (Config.source_dir), not the user's cwd.
let workspace_root = app.config.resolve_workspace_root();
let ctx = ExtractContext {
asset,
workspace_root: &workspace_root,
@@ -1185,7 +1210,9 @@ fn ingest_one_pdf_asset(
let extract_config = kebab_core::ExtractConfig::default();
// `~` / `${XDG_…}` expansion (HOTFIXES 2026-05-02 P9-4 follow-up).
let workspace_root = expand_tilde(&app.config.workspace.root);
// p9-fb-05: relative `workspace.root` resolves against the config
// file's directory (Config.source_dir), not the user's cwd.
let workspace_root = app.config.resolve_workspace_root();
let ctx = ExtractContext {
asset,
workspace_root: &workspace_root,