feat(config): from_file load 시 v2→v3 메모리 내 자동 변환(디스크 미변경)
schema_version < CURRENT 이면 migrate_document 경유로 메모리에서 변환 후 파싱. 디스크 파일은 불변(갱신은 kebab config migrate). 일회성 warn. 불변식 #3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -986,7 +986,33 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cfg: Self = toml::from_str(&text).map_err(|e| {
|
// v3: 파일의 schema_version 이 CURRENT 보다 낮으면 메모리에서 변환한다
|
||||||
|
// (디스크 미변경 — 파일 갱신은 `kebab config migrate`). 미변환 v2 파일도
|
||||||
|
// 설정 유실 없이 로드(불변식 #3). non-additive relocation(v2→v3) 은
|
||||||
|
// serde default forward-compat 로는 커버 안 되므로 반드시 거쳐야 한다.
|
||||||
|
let parse_text = {
|
||||||
|
let from = toml::from_str::<toml::Value>(&text)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.get("schema_version").and_then(toml::Value::as_integer))
|
||||||
|
.unwrap_or(1) as u32;
|
||||||
|
if from < crate::migrate::CURRENT_SCHEMA_VERSION {
|
||||||
|
static MIGRATE_WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
|
||||||
|
MIGRATE_WARNED.get_or_init(|| {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "kebab-config",
|
||||||
|
config = %path.display(),
|
||||||
|
from,
|
||||||
|
to = crate::migrate::CURRENT_SCHEMA_VERSION,
|
||||||
|
"config 가 옛 스키마입니다 — 이번 실행은 메모리에서 변환됨. 파일 갱신: `kebab config migrate`."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
crate::migrate::migrate_document(&text).new_text
|
||||||
|
} else {
|
||||||
|
text.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cfg: Self = toml::from_str(&parse_text).map_err(|e| {
|
||||||
anyhow::Error::new(ConfigInvalid {
|
anyhow::Error::new(ConfigInvalid {
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
cause: format!("parse_failed: {e}"),
|
cause: format!("parse_failed: {e}"),
|
||||||
@@ -1479,6 +1505,47 @@ theme = "dark"
|
|||||||
assert_eq!(c, back);
|
assert_eq!(c, back);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 불변식 #3: `from_file` 이 v2 파일을 디스크 미변경으로 메모리에서 v3
|
||||||
|
/// 변환 — 미변환 v2 파일도 설정 유실 0.
|
||||||
|
#[test]
|
||||||
|
fn from_file_auto_migrates_v2_in_memory() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let p = dir.path().join("config.toml");
|
||||||
|
std::fs::write(
|
||||||
|
&p,
|
||||||
|
"\
|
||||||
|
schema_version = 2
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
root = \"/my/notes\"
|
||||||
|
exclude = []
|
||||||
|
|
||||||
|
[chunking]
|
||||||
|
target_tokens = 777
|
||||||
|
|
||||||
|
[image.ocr]
|
||||||
|
enabled = true
|
||||||
|
engine = \"ollama-vision\"
|
||||||
|
model = \"gemma4:e4b\"
|
||||||
|
languages = [\"kor\"]
|
||||||
|
max_pixels = 1600
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let c = Config::from_file(&p).expect("v2 auto-migrate load");
|
||||||
|
// 사용자 v2 값이 새 경로로 살아있어야(기본값 유실 X).
|
||||||
|
assert_eq!(c.ingest.chunking.target_tokens, 777);
|
||||||
|
assert!(c.ingest.image.ocr.enabled);
|
||||||
|
assert_eq!(c.ingest.image.ocr.languages, vec!["kor"]);
|
||||||
|
// 디스크 파일은 안 바뀜(여전히 schema_version = 2 + [chunking]).
|
||||||
|
let on_disk = std::fs::read_to_string(&p).unwrap();
|
||||||
|
assert!(
|
||||||
|
on_disk.contains("schema_version = 2"),
|
||||||
|
"파일이 변경됨:\n{on_disk}"
|
||||||
|
);
|
||||||
|
assert!(on_disk.contains("[chunking]"), "파일이 변경됨:\n{on_disk}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn v3_layout_nests_media_under_ingest() {
|
fn v3_layout_nests_media_under_ingest() {
|
||||||
let c = Config::defaults();
|
let c = Config::defaults();
|
||||||
|
|||||||
Reference in New Issue
Block a user