review(p9-fb-05): 회차 1 nit 반영

- `Config.source_dir` 를 `pub(crate)` 로 좁힘. invariant ("from_file
  / load 만이 정당한 setter") 가 외부 mutation 으로 깨지지 않도록.
  대신 `pub fn source_dir(&self) -> Option<&Path>` (read-only) +
  `pub fn with_source_dir(self, dir) -> Self` (builder) 노출 — 테스트
  / 프로그래마틱 사용은 builder 통과.
- `resolve_workspace_root` 의 `current_dir()` 실패 fallback 에
  `tracing::warn!` 추가. chroot / deleted-cwd / permission 문제로
  cwd 가 안 잡힐 때 silently `./root` 로 떨어지지 않고 로그가 남음.
  `tracing` 을 kebab-config 의 deps 에 추가 (workspace dep).

테스트 27 통과 + 워크스페이스 clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 04:23:16 +00:00
parent f6cc612dbe
commit 702c7c89f7
3 changed files with 40 additions and 5 deletions

1
Cargo.lock generated
View File

@@ -3571,6 +3571,7 @@ dependencies = [
"serde",
"serde_json",
"toml",
"tracing",
]
[[package]]

View File

@@ -15,3 +15,6 @@ serde = { workspace = true }
serde_json = { workspace = true }
toml = "0.8"
dirs = "5"
# p9-fb-05: warn-log when current_dir() fails (chroot, deleted cwd)
# during workspace.root resolution.
tracing = { workspace = true }

View File

@@ -39,8 +39,13 @@ pub struct Config {
/// against the config file's location instead of the user's
/// `cwd` (so `--config /tmp/cfg.toml` + `root = "kb"` reads
/// `/tmp/kb` no matter where the user invoked from).
///
/// `pub(crate)` so external callers can't break the
/// "stamped only by from_file/load" invariant by hand. Use
/// [`Config::with_source_dir`] for tests / programmatic
/// construction that need a specific `source_dir`.
#[serde(skip)]
pub source_dir: Option<PathBuf>,
pub(crate) source_dir: Option<PathBuf>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -308,6 +313,21 @@ impl Config {
}
}
/// p9-fb-05: read-only accessor for the source-file directory
/// (where `from_file` / `load` stamped it). Returns `None` for
/// `Config::defaults()` and other in-memory constructions.
pub fn source_dir(&self) -> Option<&Path> {
self.source_dir.as_deref()
}
/// p9-fb-05: builder for tests / programmatic callers that need
/// to pin `source_dir` without going through `from_file`. Returns
/// `self` so it chains: `Config::defaults().with_source_dir(p)`.
pub fn with_source_dir(mut self, dir: PathBuf) -> Self {
self.source_dir = Some(dir);
self
}
/// p9-fb-05: resolve `workspace.root` to an absolute `PathBuf`.
/// Order:
/// 1. tilde / env / `${VAR}` substitutions per [`expand_path`].
@@ -321,10 +341,21 @@ impl Config {
/// arise when the user is using defaults AND has a relative
/// root, which is rare (defaults ship `~/KnowledgeBase`).
pub fn resolve_workspace_root(&self) -> PathBuf {
let base = self
.source_dir
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let base = self.source_dir.clone().unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|e| {
// chroot / deleted-cwd / permission failure: log so a
// user with an environment problem doesn't silently
// wonder why their workspace.root resolved to "./root"
// (which then fails at `create_dir_all` time with a
// less obvious error).
tracing::warn!(
target: "kebab-config",
error = %e,
"current_dir() failed; falling back to '.' for workspace.root resolution"
);
PathBuf::from(".")
})
});
paths::expand_path_with_base(&self.workspace.root, "", &base)
}