diff --git a/.github/scripts/release/smoke/smoke.ps1 b/.github/scripts/release/smoke/smoke.ps1 index c151596..f9e41f1 100644 --- a/.github/scripts/release/smoke/smoke.ps1 +++ b/.github/scripts/release/smoke/smoke.ps1 @@ -13,7 +13,7 @@ try { New-Item -ItemType Directory -Force -Path $env:FLAVOR_INSTALL_ROOT, $env:FLAVOR_LOCAL_BIN_DIR | Out-Null & (Join-Path $root 'manage.ps1') install --channel $channel --version $version --retain=false & (Join-Path $env:FLAVOR_LOCAL_BIN_DIR 'flavor.exe') --version - & (Join-Path $env:FLAVOR_LOCAL_BIN_DIR 'flavor.exe') check --root $root --config (Join-Path $root 'flavor.json') + & (Join-Path $env:FLAVOR_LOCAL_BIN_DIR 'flavor.exe') check --root $root --config (Join-Path $root 'flavor.toml') & (Join-Path $root 'manage.ps1') uninstall --version $version if (Test-Path (Join-Path $env:FLAVOR_INSTALL_ROOT $version)) { throw "version uninstall left $(Join-Path $env:FLAVOR_INSTALL_ROOT $version)" @@ -24,7 +24,7 @@ try { $env:FLAVOR_INSTALL_ROOT = Join-Path $tmpdir 'latest-smoke' & (Join-Path $root 'manage.ps1') install --channel $channel --retain=false & (Join-Path $env:FLAVOR_LOCAL_BIN_DIR 'flavor.exe') --version - & (Join-Path $env:FLAVOR_LOCAL_BIN_DIR 'flavor.exe') check --root $root --config (Join-Path $root 'flavor.json') + & (Join-Path $env:FLAVOR_LOCAL_BIN_DIR 'flavor.exe') check --root $root --config (Join-Path $root 'flavor.toml') & (Join-Path $root 'manage.ps1') uninstall --install-root $env:FLAVOR_INSTALL_ROOT if (Test-Path $env:FLAVOR_INSTALL_ROOT) { throw "full uninstall left $env:FLAVOR_INSTALL_ROOT" diff --git a/.github/scripts/release/smoke/smoke.sh b/.github/scripts/release/smoke/smoke.sh index 0f400c6..ddffb7e 100755 --- a/.github/scripts/release/smoke/smoke.sh +++ b/.github/scripts/release/smoke/smoke.sh @@ -17,7 +17,7 @@ mkdir -p "$HOME" "$FLAVOR_INSTALL_ROOT" "$FLAVOR_LOCAL_BIN_DIR" sh "$ROOT/manage.sh" install --channel "$CHANNEL" --version "$VERSION" --retain=false "$FLAVOR_LOCAL_BIN_DIR/flavor" --version -"$FLAVOR_LOCAL_BIN_DIR/flavor" check --root "$ROOT" --config "$ROOT/flavor.json" +"$FLAVOR_LOCAL_BIN_DIR/flavor" check --root "$ROOT" --config "$ROOT/flavor.toml" sh "$ROOT/manage.sh" uninstall --version "$VERSION" [ ! -e "$FLAVOR_INSTALL_ROOT/$VERSION" ] || { printf '%s\n' "version uninstall left $FLAVOR_INSTALL_ROOT/$VERSION" >&2; exit 1; } @@ -26,7 +26,7 @@ if [ "${SMOKE_LATEST:-}" = "1" ]; then rm -rf "$FLAVOR_INSTALL_ROOT/latest-smoke" sh "$ROOT/manage.sh" install --channel "$CHANNEL" --install-root "$FLAVOR_INSTALL_ROOT/latest-smoke" --retain=false "$FLAVOR_LOCAL_BIN_DIR/flavor" --version - "$FLAVOR_LOCAL_BIN_DIR/flavor" check --root "$ROOT" --config "$ROOT/flavor.json" + "$FLAVOR_LOCAL_BIN_DIR/flavor" check --root "$ROOT" --config "$ROOT/flavor.toml" sh "$ROOT/manage.sh" uninstall --install-root "$FLAVOR_INSTALL_ROOT/latest-smoke" [ ! -e "$FLAVOR_INSTALL_ROOT/latest-smoke" ] || { printf '%s\n' "full uninstall left $FLAVOR_INSTALL_ROOT/latest-smoke" >&2; exit 1; } fi diff --git a/.github/workflows/guard.yml b/.github/workflows/guard.yml index 598c05b..cd8f2c5 100644 --- a/.github/workflows/guard.yml +++ b/.github/workflows/guard.yml @@ -35,4 +35,4 @@ jobs: run: cargo test --locked --workspace - name: Flavor self-check - run: cargo run --locked -p flavor-cli -- check --root . --config flavor.json + run: cargo run --locked -p flavor-cli -- check --root . --config flavor.toml diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index a269caa..553bc56 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -73,7 +73,7 @@ jobs: run: cargo clippy --locked --workspace --all-targets -- -D warnings - name: Flavor self-check - run: cargo run --locked -p flavor-cli -- check --root . --config flavor.json + run: cargo run --locked -p flavor-cli -- check --root . --config flavor.toml build: needs: [metadata, verify] diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 80695e2..054fa39 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -72,7 +72,7 @@ jobs: run: cargo clippy --locked --workspace --all-targets -- -D warnings - name: Flavor self-check - run: cargo run --locked -p flavor-cli -- check --root . --config flavor.json + run: cargo run --locked -p flavor-cli -- check --root . --config flavor.toml build: needs: [metadata, verify] diff --git a/AGENTS.md b/AGENTS.md index 52c8a37..60d4035 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,7 @@ python3 scripts/init.py cargo fmt --all --check cargo clippy --locked --workspace --all-targets -- -D warnings cargo test --locked --workspace -cargo run --locked -p flavor-cli -- check --root . --config flavor.json +cargo run --locked -p flavor-cli -- check --root . --config flavor.toml python3 scripts/dev/antlr.py check runseal :pr --help ``` @@ -165,7 +165,7 @@ Every PR must pass these commands before review: cargo fmt --all --check cargo clippy --locked --workspace --all-targets -- -D warnings cargo test --locked --workspace -cargo run --locked -p flavor-cli -- check --root . --config flavor.json +cargo run --locked -p flavor-cli -- check --root . --config flavor.toml ``` CI reruns them across Linux, Windows, and macOS. diff --git a/Cargo.lock b/Cargo.lock index 4063164..fb6d48a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,7 +78,7 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flavor-cli" -version = "0.3.1" +version = "0.3.2" dependencies = [ "flavor-core", "flavor-grammar", @@ -92,6 +92,8 @@ dependencies = [ "rayon", "serde", "serde_json", + "serde_yaml", + "toml", "tracing", "tracing-subscriber", "walkdir", @@ -338,6 +340,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -391,6 +399,28 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -444,6 +474,47 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" version = "0.1.44" @@ -537,6 +608,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "valuable" version = "0.1.1" @@ -577,6 +654,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/README.md b/README.md index 46af900..2e82bb0 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ curl -fsSL https://flavor.perish.uk/manage.sh | sh -s -- uninstall --version v0. ## Usage ```bash -flavor check # auto-discovers flavor.json at --root -flavor check --config flavor.json # explicit path +flavor check # auto-discovers flavor.* at --root +flavor check --config flavor.toml # explicit path flavor check --format json flavor check --strict-warnings flavor rules # browse the built-in rule catalog @@ -71,9 +71,9 @@ Run `flavor help` for the product boundary and `flavor rules` for the full rule ## Config -A `flavor.json` has one required top-level key, `scan`. Optional `preferences` -expand named rule sets over consumer paths, and `overrides` remain the final -rule adjustment layer. +A `flavor.json`, `flavor.toml`, `flavor.yaml`, or `flavor.yml` has one required +top-level key, `scan`. Optional `preferences` expand named rule sets over +consumer paths, and `overrides` remain the final rule adjustment layer. ```json { @@ -162,7 +162,7 @@ Use `flavor rules` to browse rule ids, default severity, and payload keys withou `flavor check` resolves the config in this order: 1. The `--config ` argument if provided. Missing or malformed → error. -2. `/flavor.json` if it exists. flavor prints `flavor: using config ` on stderr so a stray file at the scan root never silently changes the check. +2. The first supported config under `` if it exists. Format priority is `flavor.toml`, `flavor.yaml`, `flavor.yml`, then `flavor.json`. flavor prints `flavor: using config ` on stderr so a stray file at the scan root never silently changes the check. 3. Built-in defaults. In user repos these match nothing; the empty-scan warning will flag that. ## Workspace @@ -188,7 +188,7 @@ python3 scripts/init.py # initialize hooks and verify loca cargo fmt --all --check cargo clippy --locked --workspace --all-targets -- -D warnings cargo test --locked --workspace -cargo run --locked -p flavor-cli -- check --root . --config flavor.json +cargo run --locked -p flavor-cli -- check --root . --config flavor.toml python3 scripts/dev/antlr.py check # optional Dockerized G4 validation ``` diff --git a/crates/flavor-cli/AGENTS.md b/crates/flavor-cli/AGENTS.md index 73761ab..2a1c214 100644 --- a/crates/flavor-cli/AGENTS.md +++ b/crates/flavor-cli/AGENTS.md @@ -30,7 +30,7 @@ repository orchestration, or runtime management here. ```bash cargo test --locked -p flavor-cli --test unit cargo run --locked -p flavor-cli -- rules -cargo run --locked -p flavor-cli -- check --root . --config flavor.json +cargo run --locked -p flavor-cli -- check --root . --config flavor.toml ``` ## Standard Workflow diff --git a/crates/flavor-cli/Cargo.toml b/crates/flavor-cli/Cargo.toml index 372ac62..6fc0929 100644 --- a/crates/flavor-cli/Cargo.toml +++ b/crates/flavor-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flavor-cli" -version = "0.3.1" +version = "0.3.2" edition = "2021" license = "MIT" description = "Personal check-only AST-backed code flavor lint CLI." @@ -17,6 +17,8 @@ flavor-plugin-vue = { path = "../flavor-plugin-vue" } rayon = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_yaml = "0.9" +toml = "0.8" tracing = "0.1" tracing-subscriber = "0.3" walkdir = "2" diff --git a/crates/flavor-cli/src/cli.rs b/crates/flavor-cli/src/cli.rs index 80e8920..bdcd3e3 100644 --- a/crates/flavor-cli/src/cli.rs +++ b/crates/flavor-cli/src/cli.rs @@ -155,19 +155,19 @@ Commands: version Config: - --config Load this JSON config. The file's directory becomes the + --config Load this JSON, TOML, or YAML config. The file's directory becomes the project root for scan patterns. (no --config) Walk ancestors of --root (default: cwd) looking for a - flavor.json. The nearest match wins; its directory - becomes the project root. Falls back to built-in - include/exclude patterns if none is found before the - filesystem root. + flavor.toml, flavor.yaml, flavor.yml, or flavor.json. + The nearest match wins; its directory becomes the project + root. Falls back to built-in include/exclude patterns if + none is found before the filesystem root. - Optional flavor.json field: + Optional config field: allowEmptyScan Suppress the "0 files matched" warning + exit 1. Reserved for workspace-root configs that intentionally exclude every submodule and delegate to per-submodule - flavor.json files via walk-up. + flavor.* files via walk-up. Scope: The check covers handwritten Python, Rust, TypeScript, TSX, Vue, and Svelte source diff --git a/crates/flavor-cli/src/config/formats.rs b/crates/flavor-cli/src/config/formats.rs new file mode 100644 index 0000000..ce2aaed --- /dev/null +++ b/crates/flavor-cli/src/config/formats.rs @@ -0,0 +1,43 @@ +use std::{fs, path::Path}; + +use super::{GuardConfigFile, DEFAULT_CONFIG_FILENAME}; + +pub(super) const CONFIG_FILENAMES: &[&str] = &[ + "flavor.toml", + "flavor.yaml", + "flavor.yml", + DEFAULT_CONFIG_FILENAME, +]; + +pub(super) fn parse_config_file(config_path: &Path) -> Result { + let source = fs::read_to_string(config_path) + .map_err(|error| format!("failed to read config {}: {error}", config_path.display()))?; + let extension = config_path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + match extension { + "json" => serde_json::from_str(&source).map_err(|error| { + format!( + "failed to parse JSON config {}: {error}", + config_path.display() + ) + }), + "toml" => toml::from_str(&source).map_err(|error| { + format!( + "failed to parse TOML config {}: {error}", + config_path.display() + ) + }), + "yaml" | "yml" => serde_yaml::from_str(&source).map_err(|error| { + format!( + "failed to parse YAML config {}: {error}", + config_path.display() + ) + }), + _ => Err(format!( + "unsupported config format for {}: expected .json, .toml, .yaml, or .yml", + config_path.display() + )), + } +} diff --git a/crates/flavor-cli/src/config/mod.rs b/crates/flavor-cli/src/config/mod.rs index e15be5c..fb328e5 100644 --- a/crates/flavor-cli/src/config/mod.rs +++ b/crates/flavor-cli/src/config/mod.rs @@ -1,12 +1,12 @@ use std::{ collections::BTreeMap, - fs, path::{Path, PathBuf}, }; use serde::Deserialize; use serde_json::Value; +mod formats; mod preferences; use crate::{ @@ -14,6 +14,7 @@ use crate::{ path_match::PathPattern, rules::{self, RuleTarget}, }; +use formats::{parse_config_file, CONFIG_FILENAMES}; #[derive(Debug, Clone)] pub(crate) struct GuardConfig { @@ -68,14 +69,14 @@ impl RuleSettings { } } -/// File name flavor walks ancestors of `--root` to find. +/// Primary file name flavor walks ancestors of `--root` to find. pub(crate) const DEFAULT_CONFIG_FILENAME: &str = "flavor.json"; /// Where the active `GuardConfig` came from. /// /// `Explicit` and `Discovered` both point at a file on disk; the split lets /// callers tell the user when a config was picked up without being asked for, -/// so a stray `flavor.json` somewhere above the scan root never silently +/// so a stray `flavor.*` somewhere above the scan root never silently /// changes behavior. #[derive(Debug, Clone, Eq, PartialEq)] pub(crate) enum ConfigSource { @@ -89,8 +90,9 @@ pub(crate) enum ConfigSource { /// Order: /// 1. `--config ` is honoured verbatim. The directory containing the /// explicit file becomes the project root for scan patterns (tsconfig-style). -/// 2. Otherwise, walk ancestors of `start` looking for `flavor.json`. The -/// nearest match wins; its directory becomes the project root. +/// 2. Otherwise, walk ancestors of `start` looking for `flavor.toml`, +/// `flavor.yaml`, `flavor.yml`, or `flavor.json`. The nearest match wins; +/// format priority within a directory follows `CONFIG_FILENAMES`. /// 3. Otherwise, fall back to the built-in defaults rooted at `start`. /// /// `start` is the walk-up entry point (typically `--root`, defaulting to the @@ -122,9 +124,11 @@ fn canonicalize_start(start: &Path) -> Result { fn walk_up_for_config(start: &Path) -> Option { for ancestor in start.ancestors() { - let candidate = ancestor.join(DEFAULT_CONFIG_FILENAME); - if candidate.is_file() { - return Some(candidate); + for name in CONFIG_FILENAMES { + let candidate = ancestor.join(name); + if candidate.is_file() { + return Some(candidate); + } } } None @@ -156,7 +160,7 @@ impl GuardConfig { /// Whether the active config opted out of the "0 files matched" failure. /// - /// A workspace-root `flavor.json` that intentionally excludes every + /// A workspace-root `flavor.*` that intentionally excludes every /// submodule (delegating real checks to per-submodule configs) sets this /// so the 0-match warning + exit 1 from PR #6 stays quiet. pub(crate) fn allow_empty_scan(&self) -> bool { @@ -164,11 +168,7 @@ impl GuardConfig { } pub(crate) fn from_file(root: PathBuf, config_path: &Path) -> Result { - let source = fs::read_to_string(config_path) - .map_err(|error| format!("failed to read config {}: {error}", config_path.display()))?; - let file: GuardConfigFile = serde_json::from_str(&source).map_err(|error| { - format!("failed to parse config {}: {error}", config_path.display()) - })?; + let file = parse_config_file(config_path)?; Self::from_config_file(root, file) } @@ -264,7 +264,7 @@ impl GuardConfig { } #[derive(Debug, Deserialize)] -struct GuardConfigFile { +pub(super) struct GuardConfigFile { scan: ScanConfigFile, #[serde(default)] preferences: Vec, diff --git a/crates/flavor-cli/src/model.rs b/crates/flavor-cli/src/model.rs index 161845f..7e601fe 100644 --- a/crates/flavor-cli/src/model.rs +++ b/crates/flavor-cli/src/model.rs @@ -45,7 +45,7 @@ impl Report { /// Construct a report that opts out of the empty-scan failure. /// - /// Used when the active `flavor.json` declared `allowEmptyScan: true` — + /// Used when the active `flavor.*` config declared `allowEmptyScan: true` — /// typically a workspace-root config that intentionally excludes every /// submodule and delegates real checks to per-submodule configs. pub(crate) fn with_scan_allow_empty( diff --git a/crates/flavor-cli/tests/unit/config.rs b/crates/flavor-cli/tests/unit/config.rs index 9b8e3f3..ab83159 100644 --- a/crates/flavor-cli/tests/unit/config.rs +++ b/crates/flavor-cli/tests/unit/config.rs @@ -6,6 +6,15 @@ use std::{ use crate::config::{resolve, ConfigSource, DEFAULT_CONFIG_FILENAME}; const SAMPLE_CONFIG: &str = r#"{ "scan": { "include": ["**/*.rs"] } }"#; +const SAMPLE_TOML_CONFIG: &str = r#" +[scan] +include = ["**/*.rs"] +"#; +const SAMPLE_YAML_CONFIG: &str = r#" +scan: + include: + - "**/*.rs" +"#; #[test] fn resolves_explicit_config_path() { @@ -21,6 +30,34 @@ fn resolves_explicit_config_path() { let _ = fs::remove_dir_all(&root); } +#[test] +fn resolves_explicit_toml() { + let root = test_root("explicit-toml"); + fs::create_dir_all(&root).unwrap(); + let explicit = root.join("custom.toml"); + fs::write(&explicit, SAMPLE_TOML_CONFIG).unwrap(); + + let (_, source) = resolve(root.clone(), Some(explicit.clone())).unwrap(); + + assert_eq!(source, ConfigSource::Explicit(explicit)); + + let _ = fs::remove_dir_all(&root); +} + +#[test] +fn resolves_explicit_yaml() { + let root = test_root("explicit-yaml"); + fs::create_dir_all(&root).unwrap(); + let explicit = root.join("custom.yaml"); + fs::write(&explicit, SAMPLE_YAML_CONFIG).unwrap(); + + let (_, source) = resolve(root.clone(), Some(explicit.clone())).unwrap(); + + assert_eq!(source, ConfigSource::Explicit(explicit)); + + let _ = fs::remove_dir_all(&root); +} + #[test] fn discovers_flavor_json_root() { let root = test_root("discovery"); @@ -38,6 +75,48 @@ fn discovers_flavor_json_root() { let _ = fs::remove_dir_all(&root); } +#[test] +fn discovers_flavor_toml_root() { + let root = test_root("discovery-toml"); + fs::create_dir_all(&root).unwrap(); + let discovered = root.join("flavor.toml"); + fs::write(&discovered, SAMPLE_TOML_CONFIG).unwrap(); + + let (_, source) = resolve(root.clone(), None).unwrap(); + + assert_eq!(source, ConfigSource::Discovered(canonical(&discovered))); + + let _ = fs::remove_dir_all(&root); +} + +#[test] +fn discovers_flavor_yaml_root() { + let root = test_root("discovery-yaml"); + fs::create_dir_all(&root).unwrap(); + let discovered = root.join("flavor.yaml"); + fs::write(&discovered, SAMPLE_YAML_CONFIG).unwrap(); + + let (_, source) = resolve(root.clone(), None).unwrap(); + + assert_eq!(source, ConfigSource::Discovered(canonical(&discovered))); + + let _ = fs::remove_dir_all(&root); +} + +#[test] +fn discovers_flavor_yml_root() { + let root = test_root("discovery-yml"); + fs::create_dir_all(&root).unwrap(); + let discovered = root.join("flavor.yml"); + fs::write(&discovered, SAMPLE_YAML_CONFIG).unwrap(); + + let (_, source) = resolve(root.clone(), None).unwrap(); + + assert_eq!(source, ConfigSource::Discovered(canonical(&discovered))); + + let _ = fs::remove_dir_all(&root); +} + #[test] fn walk_up_finds_ancestor() { let root = test_root("walk-up-ancestor"); @@ -55,6 +134,24 @@ fn walk_up_finds_ancestor() { let _ = fs::remove_dir_all(&root); } +#[test] +fn flavor_toml_wins_priority() { + let root = test_root("format-priority"); + fs::create_dir_all(&root).unwrap(); + let json = root.join(DEFAULT_CONFIG_FILENAME); + fs::write(&json, SAMPLE_CONFIG).unwrap(); + let toml = root.join("flavor.toml"); + fs::write(&toml, SAMPLE_TOML_CONFIG).unwrap(); + fs::write(root.join("flavor.yaml"), SAMPLE_YAML_CONFIG).unwrap(); + fs::write(root.join("flavor.yml"), SAMPLE_YAML_CONFIG).unwrap(); + + let (_, source) = resolve(root.clone(), None).unwrap(); + + assert_eq!(source, ConfigSource::Discovered(canonical(&toml))); + + let _ = fs::remove_dir_all(&root); +} + #[test] fn nearest_config_wins() { let root = test_root("walk-up-nearest"); @@ -186,6 +283,23 @@ fn propagates_explicit_path_errors() { let _ = fs::remove_dir_all(&root); } +#[test] +fn rejects_unsupported_format() { + let root = test_root("explicit-unsupported"); + fs::create_dir_all(&root).unwrap(); + let config_path = root.join("flavor.txt"); + fs::write(&config_path, SAMPLE_CONFIG).unwrap(); + + let error = resolve(root.clone(), Some(config_path)).unwrap_err(); + + assert!( + error.contains("unsupported config format"), + "expected unsupported-format message, got: {error}" + ); + + let _ = fs::remove_dir_all(&root); +} + #[test] fn explicit_overrides_discovery() { let root = test_root("explicit-wins"); diff --git a/flavor.json b/flavor.json deleted file mode 100644 index 4c0cb82..0000000 --- a/flavor.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "scan": { - "include": ["crates/*/src/**", "crates/*/tests/**", "grammars/**/*.g4", "scripts/**/*.py"], - "exclude": ["**/target/**", "scripts/.venv/**"] - }, - "overrides": [] -} diff --git a/flavor.toml b/flavor.toml new file mode 100644 index 0000000..8d7ec3c --- /dev/null +++ b/flavor.toml @@ -0,0 +1,13 @@ +[scan] +include = [ + "crates/*/src/**", + "crates/*/tests/**", + "grammars/**/*.g4", + "scripts/**/*.py", +] +exclude = [ + "**/target/**", + "scripts/.venv/**", +] + +overrides = [] diff --git a/scripts/init.py b/scripts/init.py index 6039d9a..6a852af 100644 --- a/scripts/init.py +++ b/scripts/init.py @@ -32,7 +32,7 @@ REQUIRED_PATHS = ( "Cargo.toml", "Cargo.lock", - "flavor.json", + "flavor.toml", "manage.sh", "manage.ps1", "runseal.toml", @@ -66,7 +66,7 @@ cargo check --locked --workspace echo "==> flavor self-check" -cargo run --locked -p flavor-cli -- check --root . --config flavor.json +cargo run --locked -p flavor-cli -- check --root . --config flavor.toml echo "==> shell syntax" sh -n .runseal/lib/python-module