From c89d268b236d96d270803e602e631f42f48a4e4d Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Thu, 11 Jun 2026 23:28:33 +0300 Subject: [PATCH] feat(config): per-key deprecation warnings on legacy omnigraph.yaml load (RFC-008 stage 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loading a legacy file (flag, env, or cwd-found — never on defaults) emits one stderr block listing each key actually present with its destination from RFC-008's migration map — the map applied to YOUR file, not a generic banner. Once per process; both binaries warn (cluster-mode boots never reach load_config, silent by construction); suppressible via OMNIGRAPH_SUPPRESS_YAML_DEPRECATION=1 for CI logs during the window. Co-Authored-By: Claude Fable 5 --- .../omnigraph-cli/tests/cli_schema_config.rs | 40 +++++++++ crates/omnigraph-server/src/config.rs | 88 ++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/crates/omnigraph-cli/tests/cli_schema_config.rs b/crates/omnigraph-cli/tests/cli_schema_config.rs index a0dac0a..a55aaed 100644 --- a/crates/omnigraph-cli/tests/cli_schema_config.rs +++ b/crates/omnigraph-cli/tests/cli_schema_config.rs @@ -498,3 +498,43 @@ fn graphs_list_against_local_uri_errors_with_remote_only_message() { "expected 'remote multi-graph server URL' rejection in stderr; got:\n{stderr}" ); } + +/// RFC-008 stage 1: loading a legacy omnigraph.yaml emits the per-key +/// deprecation block (the migration map applied to THIS file), suppressible +/// via OMNIGRAPH_SUPPRESS_YAML_DEPRECATION. +#[test] +fn legacy_config_load_warns_per_key_and_suppression_silences() { + let temp = tempdir().unwrap(); + fs::write( + temp.path().join("omnigraph.yaml"), + "cli:\n actor: act-x\ngraphs:\n g:\n uri: /tmp/never-opened\n", + ) + .unwrap(); + + // `graphs list --json` loads the config and exits without touching the + // graph URI. + let output = cli() + .current_dir(temp.path()) + .arg("graphs") + .arg("list") + .arg("--json") + .output() + .unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("deprecated (RFC-008)") && stderr.contains("`cli.actor` -> `operator.actor`"), + "{stderr}" + ); + assert!(stderr.contains("config migrate"), "{stderr}"); + + let output = cli() + .current_dir(temp.path()) + .env("OMNIGRAPH_SUPPRESS_YAML_DEPRECATION", "1") + .arg("graphs") + .arg("list") + .arg("--json") + .output() + .unwrap(); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!stderr.contains("deprecated (RFC-008)"), "{stderr}"); +} diff --git a/crates/omnigraph-server/src/config.rs b/crates/omnigraph-server/src/config.rs index 52bac2e..ea7ce30 100644 --- a/crates/omnigraph-server/src/config.rs +++ b/crates/omnigraph-server/src/config.rs @@ -549,7 +549,9 @@ fn load_config_in( }); let mut config = if let Some(path) = &config_path { - serde_yaml::from_str::(&fs::read_to_string(path)?)? + let text = fs::read_to_string(path)?; + warn_yaml_deprecation_once(path, &text); + serde_yaml::from_str::(&text)? } else { OmnigraphConfig::default() }; @@ -563,6 +565,74 @@ fn load_config_in( Ok(config) } +/// RFC-008 stage 1: suppress the legacy-config deprecation warning +/// (one process), for CI logs during the deprecation window. +pub const SUPPRESS_YAML_DEPRECATION_ENV: &str = "OMNIGRAPH_SUPPRESS_YAML_DEPRECATION"; + +/// RFC-008's migration map (the "Where every key goes" table), applied to +/// the keys actually present in a loaded file — never a generic banner. +/// Keys are `(yaml pointer, destination)`; the pointer is matched against +/// the file's real top-level/nested keys. +const YAML_DEPRECATION_MAP: &[(&str, &str)] = &[ + ("graphs", "cluster.yaml `graphs:` (team surface) — or flags/env for the zero-config tier"), + ("queries", "the cluster catalog (`.gq` discovery in cluster.yaml)"), + ("policy", "cluster.yaml `policies:` + `applies_to` bindings"), + ("server", "flags/env (`--bind`); meaningless under cluster boot"), + ("auth", "the operator credentials chain (`omnigraph login `)"), + ("aliases", "operator `aliases:` (bindings) + catalog stored queries (content)"), + ("query", "obsolete — cluster query discovery replaced `query.roots`"), + ("project", "cluster.yaml `metadata.name`"), + ("cli.actor", "`operator.actor` in ~/.omnigraph/config.yaml"), + ("cli.output_format", "`defaults.output` in ~/.omnigraph/config.yaml"), + ("cli.table_max_column_width", "`defaults.table_max_column_width` in ~/.omnigraph/config.yaml"), + ("cli.table_cell_layout", "`defaults.table_cell_layout` in ~/.omnigraph/config.yaml"), + ("cli.graph", "explicit `--target`/`--server` (no operator default-target yet)"), + ("cli.branch", "explicit `--branch`"), +]; + +/// Emit the per-key deprecation block once per process when a legacy +/// `omnigraph.yaml` is actually loaded. `omnigraph config migrate` +/// produces the split these lines describe. +fn warn_yaml_deprecation_once(path: &Path, text: &str) { + static WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new(); + if env::var_os(SUPPRESS_YAML_DEPRECATION_ENV).is_some() { + return; + } + let lines = yaml_deprecation_lines(text); + if lines.is_empty() { + return; + } + WARNED.get_or_init(|| { + eprintln!( + "warning: '{}' is deprecated (RFC-008) — its keys have new homes; run `omnigraph config migrate` for the split, set {SUPPRESS_YAML_DEPRECATION_ENV}=1 to silence:", + path.display() + ); + for line in &lines { + eprintln!(" {line}"); + } + }); +} + +fn yaml_deprecation_lines(text: &str) -> Vec { + let Ok(mapping) = serde_yaml::from_str::(text) else { + return Vec::new(); + }; + let present = |pointer: &str| -> bool { + match pointer.split_once('.') { + None => mapping.contains_key(pointer), + Some((outer, inner)) => mapping + .get(outer) + .and_then(|value| value.as_mapping()) + .is_some_and(|nested| nested.contains_key(inner)), + } + }; + YAML_DEPRECATION_MAP + .iter() + .filter(|(pointer, _)| present(pointer)) + .map(|(pointer, destination)| format!("`{pointer}` -> {destination}")) + .collect() +} + fn absolute_base_dir(cwd: &Path, path: &Path) -> Result { let path = if path.is_absolute() { path.to_path_buf() @@ -608,6 +678,22 @@ mod tests { assert_eq!(config.cli.actor.as_deref(), Some("act-env")); } + #[test] + fn yaml_deprecation_lines_name_present_keys_only() { + let lines = super::yaml_deprecation_lines( + "graphs:\n g:\n uri: /tmp/x\ncli:\n actor: a\n branch: main\n", + ); + let joined = lines.join("\n"); + assert!(joined.contains("`graphs` ->"), "{joined}"); + assert!(joined.contains("`cli.actor` -> `operator.actor`"), "{joined}"); + assert!(joined.contains("`cli.branch` ->"), "{joined}"); + assert!(!joined.contains("`aliases`"), "{joined}"); + assert!(!joined.contains("`cli.output_format`"), "{joined}"); + + assert!(super::yaml_deprecation_lines("").is_empty()); + assert!(super::yaml_deprecation_lines("not: [valid").is_empty()); + } + #[test] fn load_config_reads_yaml_defaults_from_current_dir() { let temp = tempdir().unwrap();