Skip to content

Commit 8671384

Browse files
authored
Merge pull request #200 from ModernRelay/feat/no-legacy-config-strict
feat(config): OMNIGRAPH_NO_LEGACY_CONFIG strict mode (RFC-008 stage 4)
2 parents 108d2de + 4c50170 commit 8671384

4 files changed

Lines changed: 95 additions & 21 deletions

File tree

‎crates/omnigraph-cli/tests/cli_schema_config.rs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,3 +610,39 @@ fn config_migrate_splits_legacy_config() {
610610
assert!(output.status.success(), "{output:?}");
611611
assert!(temp.path().join("cluster.yaml.proposed").exists());
612612
}
613+
614+
/// RFC-008 stage 4: OMNIGRAPH_NO_LEGACY_CONFIG refuses a present legacy
615+
/// file (pointing at config migrate) but changes nothing on migrated
616+
/// setups with no file.
617+
#[test]
618+
fn strict_mode_refuses_legacy_file_but_not_its_absence() {
619+
let temp = tempdir().unwrap();
620+
fs::write(temp.path().join("omnigraph.yaml"), "cli:\n actor: a\n").unwrap();
621+
let output = cli()
622+
.current_dir(temp.path())
623+
.env("OMNIGRAPH_NO_LEGACY_CONFIG", "1")
624+
.arg("graphs")
625+
.arg("list")
626+
.arg("--json")
627+
.output()
628+
.unwrap();
629+
assert!(!output.status.success());
630+
let stderr = String::from_utf8_lossy(&output.stderr);
631+
assert!(
632+
stderr.contains("OMNIGRAPH_NO_LEGACY_CONFIG") && stderr.contains("config migrate"),
633+
"{stderr}"
634+
);
635+
636+
// Migrated setup (no file): strict mode is a no-op — a config-loading
637+
// command that tolerates empty defaults succeeds.
638+
let clean = tempdir().unwrap();
639+
let output = cli()
640+
.current_dir(clean.path())
641+
.env("OMNIGRAPH_NO_LEGACY_CONFIG", "1")
642+
.arg("queries")
643+
.arg("list")
644+
.arg("--json")
645+
.output()
646+
.unwrap();
647+
assert!(output.status.success(), "{output:?}");
648+
}

‎crates/omnigraph-server/src/config.rs‎

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -531,15 +531,24 @@ pub fn default_config_path() -> PathBuf {
531531
/// uses it for the server; RFC-007 §D1 extends it to the CLI).
532532
pub const CONFIG_PATH_ENV: &str = "OMNIGRAPH_CONFIG";
533533

534+
/// RFC-008 stage 4 — opt-in strict mode: when set, loading a legacy
535+
/// `omnigraph.yaml` is a hard error instead of a warning. For teams that
536+
/// finished migrating and want regressions caught (a stray legacy file
537+
/// would otherwise silently outrank operator config during the window).
538+
/// The rehearsal for stage 5's removal.
539+
pub const NO_LEGACY_CONFIG_ENV: &str = "OMNIGRAPH_NO_LEGACY_CONFIG";
540+
534541
pub fn load_config(config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
535542
let env_path = env::var_os(CONFIG_PATH_ENV).map(PathBuf::from);
536-
load_config_in(&env::current_dir()?, config_path, env_path.as_ref())
543+
let strict = env::var_os(NO_LEGACY_CONFIG_ENV).is_some();
544+
load_config_in(&env::current_dir()?, config_path, env_path.as_ref(), strict)
537545
}
538546

539547
fn load_config_in(
540548
cwd: &Path,
541549
config_path: Option<&PathBuf>,
542550
env_path: Option<&PathBuf>,
551+
strict_no_legacy: bool,
543552
) -> Result<OmnigraphConfig> {
544553
// Precedence: explicit --config flag > $OMNIGRAPH_CONFIG > ./omnigraph.yaml.
545554
let explicit_path = config_path.or(env_path).cloned();
@@ -549,6 +558,14 @@ fn load_config_in(
549558
});
550559

551560
let mut config = if let Some(path) = &config_path {
561+
if strict_no_legacy {
562+
// Strict refuses the FILE, not its absence — flag-less
563+
// invocations on migrated setups keep working.
564+
bail!(
565+
"legacy config '{}' refused: {NO_LEGACY_CONFIG_ENV} is set (RFC-008 strict mode); run `omnigraph config migrate`, then remove the file — or unset the variable",
566+
path.display()
567+
);
568+
}
552569
let text = fs::read_to_string(path)?;
553570
warn_yaml_deprecation_once(path, &text);
554571
serde_yaml::from_str::<OmnigraphConfig>(&text)?
@@ -665,19 +682,38 @@ mod tests {
665682
fs::write(&env_path, "cli:\n actor: act-env\n").unwrap();
666683

667684
// $OMNIGRAPH_CONFIG used when no flag…
668-
let config = load_config_in(temp.path(), None, Some(&env_path)).unwrap();
685+
let config = load_config_in(temp.path(), None, Some(&env_path), false).unwrap();
669686
assert_eq!(config.cli.actor.as_deref(), Some("act-env"));
670687

671688
// …loses to an explicit --config…
672-
let config = load_config_in(temp.path(), Some(&flag_path), Some(&env_path)).unwrap();
689+
let config = load_config_in(temp.path(), Some(&flag_path), Some(&env_path), false).unwrap();
673690
assert_eq!(config.cli.actor.as_deref(), Some("act-flag"));
674691

675692
// …and beats the cwd default file.
676693
fs::write(temp.path().join("omnigraph.yaml"), "cli:\n actor: act-cwd\n").unwrap();
677-
let config = load_config_in(temp.path(), None, Some(&env_path)).unwrap();
694+
let config = load_config_in(temp.path(), None, Some(&env_path), false).unwrap();
678695
assert_eq!(config.cli.actor.as_deref(), Some("act-env"));
679696
}
680697

698+
#[test]
699+
fn strict_mode_refuses_the_file_not_its_absence() {
700+
let temp = tempdir().unwrap();
701+
// No file: strict mode changes nothing (defaults load).
702+
let config = load_config_in(temp.path(), None, None, true).unwrap();
703+
assert!(config.cli.actor.is_none());
704+
705+
// File present: strict refuses with the migrate pointer.
706+
fs::write(temp.path().join("omnigraph.yaml"), "cli:\n actor: a\n").unwrap();
707+
let err = load_config_in(temp.path(), None, None, true).unwrap_err();
708+
let message = err.to_string();
709+
assert!(
710+
message.contains("OMNIGRAPH_NO_LEGACY_CONFIG") && message.contains("config migrate"),
711+
"{message}"
712+
);
713+
// Without strict, the same file loads.
714+
assert!(load_config_in(temp.path(), None, None, false).is_ok());
715+
}
716+
681717
#[test]
682718
fn yaml_deprecation_lines_name_present_keys_only() {
683719
let lines = super::yaml_deprecation_lines(
@@ -717,7 +753,7 @@ policy: {}
717753
)
718754
.unwrap();
719755

720-
let config = load_config_in(temp.path(), None, None).unwrap();
756+
let config = load_config_in(temp.path(), None, None, false).unwrap();
721757
assert_eq!(config.cli_graph_name(), Some("local"));
722758
assert_eq!(config.cli_branch(), "main");
723759
assert_eq!(config.cli_output_format(), ReadOutputFormat::Kv);
@@ -752,7 +788,7 @@ policy: {}
752788
)
753789
.unwrap();
754790

755-
let config = load_config_in(&child, None, None).unwrap();
791+
let config = load_config_in(&child, None, None, false).unwrap();
756792
assert!(config.graphs.is_empty());
757793
}
758794

@@ -776,7 +812,7 @@ policy: {}
776812
"graphs:\n local:\n uri: ./demo.omni\n",
777813
)
778814
.unwrap();
779-
let config = load_config_in(temp.path(), None, None).unwrap();
815+
let config = load_config_in(temp.path(), None, None, false).unwrap();
780816

781817
// A known graph passes through unchanged.
782818
assert_eq!(config.resolve_graph_selection(Some("local")).unwrap(), Some("local"));
@@ -799,7 +835,7 @@ policy: {}
799835
"graphs:\n local:\n uri: ./demo.omni\npolicy:\n file: ./top.yaml\n",
800836
)
801837
.unwrap();
802-
let incoherent = load_config_in(temp2.path(), None, None).unwrap();
838+
let incoherent = load_config_in(temp2.path(), None, None, false).unwrap();
803839
let err = incoherent
804840
.resolve_graph_selection(Some("local"))
805841
.unwrap_err()
@@ -824,7 +860,7 @@ policy: {}
824860
server:\n graph: local\ncli:\n graph: prod\n",
825861
)
826862
.unwrap();
827-
let config = load_config_in(temp.path(), None, None).unwrap();
863+
let config = load_config_in(temp.path(), None, None, false).unwrap();
828864
assert_eq!(
829865
config.resolve_policy_tooling_graph_selection().unwrap(),
830866
Some("prod")
@@ -836,15 +872,15 @@ policy: {}
836872
"graphs:\n local:\n uri: ./local.omni\nserver:\n graph: local\n",
837873
)
838874
.unwrap();
839-
let config = load_config_in(temp.path(), None, None).unwrap();
875+
let config = load_config_in(temp.path(), None, None, false).unwrap();
840876
assert_eq!(
841877
config.resolve_policy_tooling_graph_selection().unwrap(),
842878
Some("local")
843879
);
844880

845881
let temp = tempdir().unwrap();
846882
fs::write(temp.path().join("omnigraph.yaml"), "policy: {}\n").unwrap();
847-
let config = load_config_in(temp.path(), None, None).unwrap();
883+
let config = load_config_in(temp.path(), None, None, false).unwrap();
848884
assert_eq!(config.resolve_policy_tooling_graph_selection().unwrap(), None);
849885

850886
let temp = tempdir().unwrap();
@@ -853,7 +889,7 @@ policy: {}
853889
"graphs:\n local:\n uri: ./local.omni\nserver:\n graph: ghost\n",
854890
)
855891
.unwrap();
856-
let config = load_config_in(temp.path(), None, None).unwrap();
892+
let config = load_config_in(temp.path(), None, None, false).unwrap();
857893
let err = config
858894
.resolve_policy_tooling_graph_selection()
859895
.unwrap_err()
@@ -879,7 +915,7 @@ policy: {}
879915
)
880916
.unwrap();
881917

882-
let config = load_config_in(temp.path(), None, None).unwrap();
918+
let config = load_config_in(temp.path(), None, None, false).unwrap();
883919
let resolved = config.resolve_query_path(Path::new("test.gq")).unwrap();
884920
assert_eq!(resolved, temp.path().join("queries").join("test.gq"));
885921
}
@@ -896,7 +932,7 @@ policy: {}
896932
fs::write(ambient_dir.join("local.gq"), "query ambient { return {} }").unwrap();
897933

898934
let config =
899-
load_config_in(&ambient_dir, Some(&config_dir.join("omnigraph.yaml")), None).unwrap();
935+
load_config_in(&ambient_dir, Some(&config_dir.join("omnigraph.yaml")), None, false).unwrap();
900936
let resolved = config.resolve_query_path(Path::new("local.gq")).unwrap();
901937

902938
assert_eq!(resolved, config_dir.join("local.gq"));
@@ -926,7 +962,7 @@ queries:
926962
)
927963
.unwrap();
928964

929-
let config = load_config_in(temp.path(), None, None).unwrap();
965+
let config = load_config_in(temp.path(), None, None, false).unwrap();
930966

931967
// Per-graph registry (multi-graph mode).
932968
let prod = config.target_query_entries("prod").unwrap();
@@ -967,7 +1003,7 @@ queries:
9671003
policy:\n file: ./prod.yaml\n bare:\n uri: s3://b/bare\n",
9681004
)
9691005
.unwrap();
970-
let config = load_config_in(temp.path(), None, None).unwrap();
1006+
let config = load_config_in(temp.path(), None, None, false).unwrap();
9711007

9721008
// Named graph with its own policy → per-graph (not top-level).
9731009
assert!(
@@ -1003,7 +1039,7 @@ queries:
10031039
)
10041040
.unwrap();
10051041

1006-
let config = load_config_in(temp.path(), None, None).unwrap();
1042+
let config = load_config_in(temp.path(), None, None, false).unwrap();
10071043
// Additive: no `queries:` anywhere → empty registries everywhere.
10081044
assert!(config.query_entries().is_empty());
10091045
assert!(
@@ -1023,7 +1059,7 @@ queries:
10231059
)
10241060
.unwrap();
10251061

1026-
let config = load_config_in(temp.path(), None, None).unwrap();
1062+
let config = load_config_in(temp.path(), None, None, false).unwrap();
10271063
assert_eq!(
10281064
config.resolve_policy_file().unwrap(),
10291065
temp.path().join("policy.yaml")
@@ -1046,7 +1082,7 @@ cli:
10461082
)
10471083
.unwrap();
10481084

1049-
let config = load_config_in(temp.path(), None, None).unwrap();
1085+
let config = load_config_in(temp.path(), None, None, false).unwrap();
10501086
assert_eq!(
10511087
config.graph_bearer_token_env(
10521088
Some("https://override.example.com"),

‎docs/dev/rfc-008-deprecate-omnigraph-yaml.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ contract), retirement is staged, loud, and tooled:
132132
hand-edited anyway (Terraform has no config scaffolder either). New
133133
users copy from the cluster quick-start; migrants get a ready-to-review
134134
`cluster.yaml` from `config migrate`.
135-
4. **Opt-in strict.** `OMNIGRAPH_NO_LEGACY_CONFIG=1` turns the warning into
135+
4. **Opt-in strict** *(landed — the release gap to stages 1–3 collapsed: no version boundary was crossed between them, so all four ship in the same release)*. `OMNIGRAPH_NO_LEGACY_CONFIG=1` turns the warning into
136136
an error — for teams that finished migrating and want regressions caught.
137137
5. **Remove at the next major.** Loading the file becomes an error pointing
138138
at `config migrate`. The `OmnigraphConfig` code path, the dual

‎docs/user/cli-reference.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ operator server use the legacy chain alone.
109109
> naming each present key's new home (suppress in CI with
110110
> `OMNIGRAPH_SUPPRESS_YAML_DEPRECATION=1`); `omnigraph config migrate`
111111
> produces the split. The file keeps working through the deprecation
112-
> window.
112+
> window. Migrated teams can set `OMNIGRAPH_NO_LEGACY_CONFIG=1` to turn
113+
> any legacy-file load into a hard error (regression guard; the file's
114+
> absence is always fine).
113115

114116
```yaml
115117
project: { name }

0 commit comments

Comments
 (0)