Skip to content

Complete CLI config struct implementation with OrthoConfig (0.8.0)#273

Open
leynos wants to merge 11 commits intomainfrom
add-cli-config-struct-vcf3xy
Open

Complete CLI config struct implementation with OrthoConfig (0.8.0)#273
leynos wants to merge 11 commits intomainfrom
add-cli-config-struct-vcf3xy

Conversation

@leynos
Copy link
Owner

@leynos leynos commented Mar 8, 2026

Summary

    • Completes the layered CLI configuration by introducing a canonical CliConfig derived via OrthoConfig and using it as the authoritative source for CLI configuration.
    • Splits Clap parsing from configuration merging into dedicated modules (parser, merge) and wires CliConfig through the OrthoConfig discovery/merge path.
    • Adds typed config enums (ColourPolicy, SpinnerMode, OutputFormat, Theme) with compatibility handling for existing surfaces like no_emoji.
    • Keeps the existing CLI surface stable while transitioning to a layered configuration model.
    • Upgrades ortho_config to 0.8.0 across Cargo.toml/Cargo.lock/build scripts and updates tests/docs accordingly.
    • Wires global and subcommand defaults (e.g. cmds.build.targets) through the layered config path.
    • Documentation and tests updated to reflect the new architecture and precedence rules.

Changes

  • New modules and schema:

    • src/cli/config.rs: defines CliConfig, BuildConfig, CommandConfigs, and the typed enums; implements defaults and post-merge validation hooks (Theme compatibility with no_emoji, Spinner/Progress compatibility, and unsupported OutputFormat).
    • src/cli/merge.rs: handles merging discovered layers with parsed CLI input, wiring into the CliConfig merge path and producing the runtime Cli state.
    • src/cli/parser.rs: Clap-facing parser types and helper localization utilities.
  • Refactor: moved merge logic and OrthoConfig-driven flow out of src/cli/mod.rs into dedicated modules for maintainability and testability.

  • Parser/merge wiring: CliConfig is wired into the OrthoConfig discovery/merge path so global values and subcommand defaults (e.g. cmds.build.targets) resolve through the same typed fields.

  • Compatibility: preserve hidden config-path behavior and keep --config handling out of scope for this milestone.

  • Tooling and dependencies:

    • Upgraded ortho_config to 0.8.0 across Cargo.toml/Cargo.lock/build.rs/docs.
    • Build script and tests updated to exercise the new layered configuration path.
  • Documentation and tests:

    • Added docs/execplans/3-11-1-cli-config-struct.md describing plan, decisions, and target architecture.
    • Updated docs/ortho-config-users-guide.md to reflect the upgrade to ortho_config 0.8.0 and corresponding guidance.
    • Expanded test coverage for configuration precedence, defaults, and validation; updated/added unit and behavioural tests.
    • Sample TOML config shows a global surface with per-subcommand defaults under [cmds.build].
  • Example config (TOML) (for reference):
    verbose = true
    locale = "es-ES"
    colour_policy = "auto"
    spinner_mode = "auto"
    output_format = "human"
    theme = "ascii"
    progress = true
    accessible = false

    [cmds.build]
    targets = ["all"]

This demonstrates a canonical view where global fields live at the root and subcommand defaults (like build targets) live under their respective sections.

Documentation impact

  • Added docs/execplans/3-11-1-cli-config-struct.md describing plan, decisions, and target architecture.
  • Updated docs/ortho-config-users-guide.md to reflect the upgrade to ortho_config 0.8.0 and related guidance.
  • Updated docs/users-guide.md to reflect the new layered CLI configuration model and precedence rules.
  • Roadmap entry 3.11.1 updated to reflect completion.

How to test locally

  • Run quality gates: make check-fmt, make lint, make test, make markdownlint, make nixie.
  • Validate CLI parsing and merge behavior with existing tests; ensure no regressions in top-level commands (build, clean, graph, manifest) and help/diagnostics messages.
  • Exercise precedence: file/env/CLI overrides against defaults using the new CliConfig paths.

Plan alignment and next steps

  • Stage A/B/C focused on splitting responsibilities and wiring CliConfig into the discovery/merge path.
  • Stage D adds unit and behavioural coverage; Stage E completes documentation and final verification.
  • This PR completes the core wiring and schema: CliConfig now governs the layered CLI configuration, while the parser and merge code remain focused on their respective responsibilities.

Idempotence and recovery

  • This work is implemented in small, reversible steps. If a stage fails, revert to the last green checkpoint and proceed with a narrower diff while updating the ExecPlan accordingly.
  • If formatting tools touch unrelated Markdown files, restore only the incidental edits created during the current turn before proceeding.

📎 Task: https://www.devboxer.com/task/c5d87114-1d80-439d-b1a9-21848725bc15

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 8, 2026

Reviewer's Guide

Refactors the CLI into separate parser, config, and merge layers driven by a new OrthoConfig-derived CliConfig schema, wires layered configuration (including build subcommand defaults and typed presentation enums) through this path, upgrades ortho_config to 0.8.0, and updates tests and documentation to match the new precedence and configuration model.

Sequence diagram for CLI parse and layered configuration merge

sequenceDiagram
    actor User
    participant Main as main_rs
    participant Parser as parser_Cli
    participant Merge as merge_module
    participant Ortho as OrthoConfig_runtime
    participant Runner as runner_run

    User->>Main: invoke_netsuke(args)
    Main->>Parser: parse_with_localizer_from(args, localizer)
    Parser->>Parser: configure_validation_parsers()
    Parser->>Parser: Cli::from_arg_matches_mut()
    Parser-->>Main: (parsed_cli Cli, matches ArgMatches)

    Main->>Merge: merge_with_config(parsed_cli, matches)
    Merge->>Merge: config_discovery(directory)
    Merge->>Ortho: compose_layers(defaults, files, env, cli_overrides)
    Ortho-->>Merge: LayerComposition
    Merge->>Ortho: CliConfig::merge_from_layers(LayerComposition)
    Ortho-->>Merge: merged_config CliConfig
    Merge->>Merge: apply_config(parsed_cli, merged_config)
    Merge-->>Main: merged_cli Cli

    Main->>Main: init_tracing(merged_cli.verbose)
    Main->>Main: prefs = output_prefs::resolve(merged_cli.no_emoji_override())
    Main->>Runner: run(&merged_cli, prefs)
    Runner->>Runner: progress_enabled = merged_cli.progress_enabled()
    Runner-->>Main: Result
    Main-->>User: ExitCode
Loading

Class diagram for layered CLI configuration (CliConfig, parser Cli, and merge)

classDiagram
    class Cli {
        +PathBuf file
        +Option_PathBuf_ directory
        +Option_usize_ jobs
        +bool verbose
        +Option_String_ locale
        +Vec_String_ fetch_allow_scheme
        +Vec_HostPattern_ fetch_allow_host
        +Vec_HostPattern_ fetch_block_host
        +bool fetch_default_deny
        +Option_bool_ accessible
        +Option_bool_ no_emoji
        +Option_bool_ progress
        +Option_ColourPolicy_ colour_policy
        +Option_SpinnerMode_ spinner_mode
        +Option_OutputFormat_ output_format
        +Option_Theme_ theme
        +Option_Commands_ command
        +with_default_command() Cli
        +no_emoji_override() Option_bool_
        +progress_enabled() bool
    }

    class Commands {
        <<enumeration>>
        Build
        Clean
        Graph
        Manifest
    }

    class BuildArgs {
        +Option_PathBuf_ emit
        +Vec_String_ targets
    }

    class CliConfig {
        +PathBuf file
        +Option_usize_ jobs
        +bool verbose
        +Option_String_ locale
        +Vec_String_ fetch_allow_scheme
        +Vec_HostPattern_ fetch_allow_host
        +Vec_HostPattern_ fetch_block_host
        +bool fetch_default_deny
        +Option_bool_ accessible
        +Option_bool_ no_emoji
        +Option_bool_ progress
        +Option_ColourPolicy_ colour_policy
        +Option_SpinnerMode_ spinner_mode
        +Option_OutputFormat_ output_format
        +Option_Theme_ theme
        +CommandConfigs cmds
        +post_merge(ctx PostMergeContext) OrthoResult~()~
    }

    class CommandConfigs {
        +BuildConfig build
    }

    class BuildConfig {
        +Option_PathBuf_ emit
        +Vec_String_ targets
    }

    class ColourPolicy {
        <<enumeration>>
        Auto
        Always
        Never
    }

    class SpinnerMode {
        <<enumeration>>
        Auto
        Enabled
        Disabled
    }

    class OutputFormat {
        <<enumeration>>
        Human
        Json
    }

    class Theme {
        <<enumeration>>
        Auto
        Unicode
        Ascii
    }

    class MergeModule {
        +merge_with_config(cli Cli, matches ArgMatches) OrthoResult~Cli~
        +apply_config(parsed Cli, config CliConfig) Cli
        +resolve_command(parsed Option_Commands_, build_defaults BuildConfig) Commands
        +canonical_theme(theme Option_Theme_, no_emoji Option_bool_) Option_Theme_
    }

    class ParserModule {
        +parse_with_localizer_from(iter I, localizer Arc_Localizer_) Result~(Cli,ArgMatches),clap::Error~
        +validation_message(localizer Localizer, key str, args Option_LocalizationArgs_, fallback str) String
    }

    Cli --> Commands : uses
    Commands --> BuildArgs : Build(BuildArgs)

    CliConfig --> CommandConfigs : has
    CommandConfigs --> BuildConfig : has

    Cli --> ColourPolicy : uses
    Cli --> SpinnerMode : uses
    Cli --> OutputFormat : uses
    Cli --> Theme : uses

    CliConfig --> ColourPolicy : uses
    CliConfig --> SpinnerMode : uses
    CliConfig --> OutputFormat : uses
    CliConfig --> Theme : uses

    MergeModule --> CliConfig : merges
    MergeModule --> Cli : produces

    ParserModule --> Cli : parses
    ParserModule ..> MergeModule : passes Cli, ArgMatches
Loading

File-Level Changes

Change Details Files
Introduce CliConfig and layered configuration schema with typed enums and post-merge validation, and wire it into CLI merge flow.
  • Add src/cli/config.rs defining CliConfig, BuildConfig, CommandConfigs, and enums ColourPolicy, SpinnerMode, OutputFormat, Theme.
  • Derive OrthoConfig on CliConfig with NETSUKE prefix, defaults, merge strategies, and a PostMergeHook to validate theme/no_emoji, spinner/progress, and OutputFormat JSON rejection.
  • Switch merge defaults in tests and BDD steps from Cli::default to CliConfig::default and add helpers to merge defaults with file layers and assert validation errors.
src/cli/config.rs
tests/cli_tests/merge.rs
tests/bdd/steps/locale_resolution.rs
Split CLI module into parser-only and merge-only layers, keep Cli as runtime shape, and route configuration through CliConfig.
  • Replace monolithic src/cli/mod.rs implementation with thin module that re-exports submodules and types, and adds modules config, merge, parser while keeping parsing helpers in parsing.rs.
  • Move clap-facing Cli, BuildArgs, Commands, localized parsing, and validation parser wiring into src/cli/parser.rs; add no_emoji_override and progress_enabled helpers and extra skipped fields for resolved colour_policy/spinner_mode/output_format/theme.
  • Add src/cli/merge.rs to compose defaults, file, env, and CLI overrides into CliConfig, convert it back into a runtime Cli (including applying cmds.build defaults and canonicalizing theme/no_emoji), and preserve existing ConfigDiscovery/NETSUKE_CONFIG_PATH and env semantics.
  • Adapt parsing helpers in src/cli/parsing.rs to refer to parser::validation_message and parser::MAX_JOBS constants.
src/cli/mod.rs
src/cli/parser.rs
src/cli/merge.rs
src/cli/parsing.rs
Wire new merged Cli shape into main, runner, build script, and tests, including no_emoji/theme and progress behaviour.
  • Update main.rs to stop calling with_default_command after merge (now done in BDD helper) and to resolve output_prefs via Cli::no_emoji_override.
  • Update runner to use Cli::progress_enabled for progress toggling instead of inspecting progress Option directly.
  • In build.rs, reference new CLI symbols (CliConfig, BuildArgs, Commands) and helper methods (no_emoji_override, progress_enabled) and add rerun-if-changed lines for new CLI submodules.
  • Add BDD steps and feature file (configuration_preferences) that exercise build-target defaults via [cmds.build], locale/verbose precedence, unsupported output_format error, and no_emoji->Theme::Ascii compatibility; track EnvLock in TestWorld to serialize env mutations.
src/main.rs
src/runner/mod.rs
build.rs
tests/bdd/fixtures/mod.rs
tests/bdd/steps/mod.rs
tests/bdd/steps/configuration_preferences.rs
tests/features/configuration_preferences.feature
Upgrade ortho_config to 0.8.0 and expand its documentation, especially around derive semantics, cli_default_as_absent, and OrthoConfigDocs.
  • Bump ortho_config dependency (runtime and build-dependencies) from 0.7.0 to 0.8.0 in Cargo.toml and refresh Cargo.lock accordingly.
  • Expand docs/ortho-config-users-guide.md with 0.8.0 content: dependency architecture for derive macro users, troubleshooting, FAQ about figment/uncased/xdg deps, enhanced cli_default_as_absent behaviour and defaults inference, new OrthoConfigDocs and cargo-orthohelp sections (IR generation, man pages, PowerShell help), and updated notes on naming conventions.
  • Adjust existing examples in the OrthoConfig guide to use 0.8.0 versions and tweak compose_layers_from_iter docs and attribute table wording.
Cargo.toml
Cargo.lock
docs/ortho-config-users-guide.md
Document the new layered CLI config model, CliConfig responsibilities, and roadmap completion; add execution plan doc.
  • Update docs/netsuke-design.md to describe separation of parser Cli from CliConfig, new schema fields (colour policy, spinner mode, output format, theme, build defaults), and validation/conflict handling while keeping locale resolution and runner architecture notes consistent.
  • Extend docs/users-guide.md with an explicit CliConfig key list (global and [cmds.build]), example TOML, and explanations of theme/no_emoji, spinner_mode/progress, output_format constraints, and future colour_policy behaviour.
  • Mark roadmap item 3.11.1 and its sub-bullets as complete in docs/roadmap.md.
  • Add docs/execplans/3-11-1-cli-config-struct.md capturing the execution plan, constraints, decisions, and outcomes for the CliConfig work.
docs/netsuke-design.md
docs/users-guide.md
docs/roadmap.md
docs/execplans/3-11-1-cli-config-struct.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

Walkthrough

Introduce a dedicated OrthoConfig-derived CliConfig, split src/cli into parser, config, and merge submodules, implement layered config discovery/merging with post-merge validation, update tests and docs, and bump ortho_config dependency to 0.8.0. (33 words)

Changes

Cohort / File(s) Summary
Dependency
Cargo.toml
Bump ortho_config from 0.7.00.8.0 in [dependencies] and [build-dependencies], preserving ["serde_json"] feature.
Build script
build.rs
Add compile-time size/type checks referencing cli::BuildArgs, cli::CliConfig, cli::Commands and methods no_emoji_override / progress_enabled; add cargo:rerun-if-changed for src/cli/{config,merge,parser}.rs.
CLI core refactor
src/cli/mod.rs, src/cli/parser.rs, src/cli/config.rs, src/cli/merge.rs
Split monolithic CLI into submodules. Add public CliConfig schema (enums: ColourPolicy, SpinnerMode, OutputFormat, Theme), parser exposes Cli, BuildArgs, Commands (localised), and merge_with_config composes defaults/files/env/CLI and applies post-merge validation.
Parsing helpers
src/cli/parsing.rs
Redirect validation messages and constants to parser module (parser::validation_message, parser::MAX_JOBS).
Application integration
src/main.rs, src/runner/mod.rs
Use no_emoji_override() and progress_enabled() accessors on Cli; update reporter initialisation to read cli.progress_enabled().
Tests: BDD & fixtures
tests/bdd/fixtures/mod.rs, tests/bdd/steps/mod.rs, tests/bdd/steps/configuration_preferences.rs, tests/bdd/steps/locale_resolution.rs, tests/features/configuration_preferences.feature
Add EnvLock guard to TestWorld; add configuration_preferences steps and feature scenarios; adapt locale steps to use CliConfig defaults and merge flows; add utilities for writing netsuke.toml and env management.
Tests: unit/integration
tests/cli_tests/merge.rs, tests/cli_tests/parsing.rs
Migrate tests to use CliConfig for merge/defaults; export Theme from netsuke::cli and expose output_prefs usage; add merge helpers, validation error assertions and emoji-related tests.
Docs & planning
docs/execplans/3-11-1-cli-config-struct.md, docs/netsuke-design.md, docs/ortho-config-users-guide.md, docs/roadmap.md, docs/users-guide.md
Add exec plan and design doc for CliConfig, update ortho_config refs to 0.8.0, document layered keys/TOML examples, mark 3.11.1 checklist items completed.

Sequence Diagram

sequenceDiagram
    participant U as User/CLI
    participant P as parser.rs (Clap)
    participant M as merge.rs (Composer)
    participant CD as ConfigDiscovery (Files)
    participant EP as Env Provider (ENV)
    participant V as Validation (Post-Merge)
    participant C as CliConfig (Schema)

    U->>P: supply CLI args
    P->>P: parse with localizer
    P->>M: send (Cli, ArgMatches)
    M->>M: init CliConfig::default()
    M->>CD: discover config files
    CD-->>M: return file layers
    M->>EP: collect NETSUKE_* env vars
    EP-->>M: return env layer
    M->>M: build CLI overrides from ArgMatches
    M->>M: compose layers (defaults, files, env, CLI)
    M->>C: merge_from_layers()
    C-->>M: merged CliConfig
    M->>V: run post-merge validations
    V-->>M: validation result
    M->>M: apply_config onto parsed Cli
    M-->>U: finalised Cli
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🏗️ From monolith to layers fine,
CLI and config now align —
Merge the files, env and flags,
Validate before the rags,
OrthoConfig hums: defaults in tune.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title 'Complete CLI config struct implementation with OrthoConfig (0.8.0)' directly and clearly summarises the primary change—introducing CliConfig as a layered CLI configuration schema derived from OrthoConfig 0.8.0.
Description check ✅ Passed The description comprehensively details the changeset: introduction of CliConfig with typed enums, module splits (parser, merge, config), ortho_config upgrade to 0.8.0, layered defaults, post-merge validation hooks, documentation updates, and testing guidance—all directly related to the actual changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-cli-config-struct-vcf3xy
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos changed the title Add CliConfig struct derived from OrthoConfig Add CliConfig planning docs and ortho_config upgrade notes Mar 8, 2026
@leynos leynos changed the title Add CliConfig planning docs and ortho_config upgrade notes Introduce CliConfig with modular CLI parsing/merge Mar 9, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Owner Author

leynos commented Mar 10, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/cli_tests/merge.rs

Comment on file

fn cli_config_build_defaults_apply_when_cli_targets_are_absent() -> Result<()> {
    let _env_lock = EnvLock::acquire();
    let temp_dir = tempdir().context("create temporary config directory")?;
    let config_path = temp_dir.path().join("netsuke.toml");
    fs::write(
        &config_path,
        r#"
[cmds.build]
targets = ["all", "docs"]
"#,
    )
    .context("write netsuke.toml")?;

    let _config_guard = EnvVarGuard::set("NETSUKE_CONFIG_PATH", config_path.as_os_str());
    let localizer = Arc::from(cli_localization::build_localizer(None));
    let (cli, matches) = netsuke::cli::parse_with_localizer_from(["netsuke"], &localizer)
        .context("parse CLI args for merge")?;
    let merged = netsuke::cli::merge_with_config(&cli, &matches)
        .context("merge CLI and configuration layers")?;

    let Some(netsuke::cli::Commands::Build(args)) = merged.command else {
        anyhow::bail!("expected merged command to be build");
    };
    ensure!(
        args.targets == vec![String::from("all"), String::from("docs")],
        "configured build targets should be used when CLI targets are absent",
    );
    Ok(())
}

❌ New issue: Code Duplication
The module contains 5 functions with similar structure: cli_config_build_defaults_apply_when_cli_targets_are_absent,cli_config_explicit_targets_override_configured_build_defaults,cli_config_rejects_unsupported_json_output_format,cli_config_validates_spinner_and_progress_conflicts and 1 more functions

@coderabbitai

This comment was marked as resolved.

@leynos leynos changed the title Introduce CliConfig with modular CLI parsing/merge Complete layered CliConfig wiring with OrthoConfig 0.8.0 Mar 10, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Owner Author

leynos commented Mar 10, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/cli_tests/merge.rs

Comment on file

fn cli_config_build_defaults_apply_when_cli_targets_are_absent() -> Result<()> {
    with_config_file(
        r#"
[cmds.build]
targets = ["all", "docs"]
"#,
        &["netsuke"],
        |merged| {
            let Some(netsuke::cli::Commands::Build(args)) = merged.command else {
                anyhow::bail!("expected merged command to be build");
            };
            ensure!(
                args.targets == vec![String::from("all"), String::from("docs")],
                "configured build targets should be used when CLI targets are absent",
            );
            Ok(())
        },
    )
}

❌ New issue: Code Duplication
The module contains 4 functions with similar structure: cli_config_build_defaults_apply_when_cli_targets_are_absent,cli_config_explicit_targets_override_configured_build_defaults,cli_config_validates_spinner_and_progress_conflicts,cli_config_validates_theme_alias_conflicts

@coderabbitai

This comment was marked as resolved.

@leynos leynos changed the title Complete layered CliConfig wiring with OrthoConfig 0.8.0 Wired CliConfig through OrthoConfig 0.8.0; refactor CLI layers Mar 11, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Owner Author

leynos commented Mar 11, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/cli_tests/merge.rs

Comment on file

fn cli_config_validates_theme_alias_conflicts(
    default_cli_json: Result<serde_json::Value>,
) -> Result<()> {
    assert_merge_rejects(
        default_cli_json?,
        json!({
            "theme": "unicode",
            "no_emoji": true
        }),
        "conflicting theme and alias should fail",
        "theme = \"unicode\" conflicts with no_emoji = true",
    )
}

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: cli_config_validates_spinner_and_progress_conflicts,cli_config_validates_theme_alias_conflicts

@leynos
Copy link
Owner Author

leynos commented Mar 11, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/cli_tests/merge.rs

Comment on file

use anyhow::{Context, Result, ensure};
use netsuke::cli::Cli;
use netsuke::cli::{CliConfig, Theme};

❌ New issue: String Heavy Function Arguments
In this module, 44.4% of all arguments to its 14 functions are strings. The threshold for string arguments is 39.0%

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e rs merge tests/cli_tests/ --exec cat {}

Length of output: 9733


@leynos: Both issues share the same root — the three validation-error test functions are structurally identical data variants, and assert_merge_rejects carries a redundant expect_err_hint: &str parameter (rstest generates case names automatically, so the hint is never exercised meaningfully).


Root cause

All three of these functions follow the exact same pattern:

unwrap fixture → assert_merge_rejects(defaults, json!({…}), "hint", "msg")

The expect_err_hint argument is passed to expect_err, which only fires when the call unexpectedly succeeds — at which point rstest's own case label already identifies the test. It is dead weight that inflates the string-argument ratio.


Fix

1. Drop expect_err_hint from assert_merge_rejects

fn assert_merge_rejects(
    defaults: serde_json::Value,
    file_layer: serde_json::Value,
    expected_msg: &str,
) -> anyhow::Result<()> {
    let err = merge_defaults_with_file_layer(defaults, file_layer)
        .expect_err("merge should have returned an error");
    ensure!(
        err.to_string().contains(expected_msg),
        "unexpected error text: {err}",
    );
    Ok(())
}

2. Collapse all three validation tests into one #[case]-parametrised test

#[rstest]
#[case(
    json!({ "theme": "unicode", "no_emoji": true }),
    "theme = \"unicode\" conflicts with no_emoji = true",
)]
#[case(
    json!({ "spinner_mode": "disabled", "progress": true }),
    "spinner_mode = \"disabled\" conflicts with progress = true",
)]
#[case(
    json!({ "output_format": "json" }),
    "output_format = \"json\" is not supported yet",
)]
fn cli_config_rejects_conflicting_or_unsupported_settings(
    default_cli_json: Result<serde_json::Value>,
    #[case] file_layer: serde_json::Value,
    #[case] expected_msg: &str,
) -> Result<()> {
    assert_merge_rejects(default_cli_json?, file_layer, expected_msg)
}

This removes three functions and replaces them with one, taking the module from 14 to 12 functions. Dropping expect_err_hint removes one &str parameter from assert_merge_rejects. Together these should pull the string-argument ratio comfortably below the 39% threshold.


AI coding agent prompt

File to modify: tests/cli_tests/merge.rs

## Objective
Address two CodeScene biomarkers in `tests/cli_tests/merge.rs`:
1. Code Duplication — `cli_config_validates_theme_alias_conflicts` and
   `cli_config_validates_spinner_and_progress_conflicts` have identical structure.
2. String Heavy Function Arguments — 44.4% of arguments across the module are strings.

## Change 1 — Remove the `expect_err_hint` parameter from `assert_merge_rejects`

Replace the existing helper:

```rust
fn assert_merge_rejects(
    defaults: serde_json::Value,
    file_layer: serde_json::Value,
    expect_err_hint: &str,
    expected_msg: &str,
) -> anyhow::Result<()> {
    let err = merge_defaults_with_file_layer(defaults, file_layer).expect_err(expect_err_hint);
    ensure!(
        err.to_string().contains(expected_msg),
        "unexpected error text: {err}",
    );
    Ok(())
}
```

with:

```rust
fn assert_merge_rejects(
    defaults: serde_json::Value,
    file_layer: serde_json::Value,
    expected_msg: &str,
) -> anyhow::Result<()> {
    let err = merge_defaults_with_file_layer(defaults, file_layer)
        .expect_err("merge should have returned an error");
    ensure!(
        err.to_string().contains(expected_msg),
        "unexpected error text: {err}",
    );
    Ok(())
}
```

## Change 2 — Delete the three individual validation test functions

Remove the following three test functions in their entirety:
- `cli_config_validates_theme_alias_conflicts`
- `cli_config_validates_spinner_and_progress_conflicts`
- `cli_config_rejects_unsupported_json_output_format`

## Change 3 — Add one parametrised replacement test

Add the following function in their place:

```rust
#[rstest]
#[case(
    json!({ "theme": "unicode", "no_emoji": true }),
    "theme = \"unicode\" conflicts with no_emoji = true",
)]
#[case(
    json!({ "spinner_mode": "disabled", "progress": true }),
    "spinner_mode = \"disabled\" conflicts with progress = true",
)]
#[case(
    json!({ "output_format": "json" }),
    "output_format = \"json\" is not supported yet",
)]
fn cli_config_rejects_conflicting_or_unsupported_settings(
    default_cli_json: Result<serde_json::Value>,
    #[case] file_layer: serde_json::Value,
    #[case] expected_msg: &str,
) -> Result<()> {
    assert_merge_rejects(default_cli_json?, file_layer, expected_msg)
}
```

## Constraints
- Do NOT alter any other test function, helper, fixture, or import.
- Do NOT modify any file other than `tests/cli_tests/merge.rs`.
- After refactoring, the file must compile and all tests must pass.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Owner Author

leynos commented Mar 12, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/cli_tests/merge.rs

Comment on file

use anyhow::{Context, Result, ensure};
use netsuke::cli::Cli;
use netsuke::cli::{CliConfig, Theme};

❌ New issue: String Heavy Function Arguments
In this module, 50.0% of all arguments to its 11 functions are strings. The threshold for string arguments is 39.0%

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos marked this pull request as ready for review March 12, 2026 18:50
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The validation_error helper is implemented in both src/cli/config.rs and src/cli/merge.rs; consider centralizing this in a shared module/function to avoid drift in error shaping and messaging.
  • Building the CLI override JSON in cli_overrides_from_matches and build_cli_overrides is now field-by-field and will require updates in multiple spots when new fields are added; it may be worth factoring this into a more data-driven mapping or a helper to reduce the risk of missing fields during future changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `validation_error` helper is implemented in both `src/cli/config.rs` and `src/cli/merge.rs`; consider centralizing this in a shared module/function to avoid drift in error shaping and messaging.
- Building the CLI override JSON in `cli_overrides_from_matches` and `build_cli_overrides` is now field-by-field and will require updates in multiple spots when new fields are added; it may be worth factoring this into a more data-driven mapping or a helper to reduce the risk of missing fields during future changes.

## Individual Comments

### Comment 1
<location path="src/cli/parser.rs" line_range="180" />
<code_context>
+impl Default for Cli {
+    fn default() -> Self {
+        Self {
+            file: PathBuf::from("Netsukefile"),
+            directory: None,
+            jobs: None,
</code_context>
<issue_to_address>
**suggestion:** Avoid duplicating the default manifest path literal in multiple places.

`CliConfig` already centralizes this value via `default_manifest_path()`, but this `Default` impl hard-codes "Netsukefile" again. Please pull the default from a shared constant or helper so parser and config defaults can’t drift if the path changes in the future.

Suggested implementation:

```rust
        Self {
            file: CliConfig::default_manifest_path(),
            directory: None,

```

To compile, ensure `CliConfig` is in scope in this file. At the top of `src/cli/parser.rs`, add or adjust an import such as:

- `use crate::config::CliConfig;`

If `default_manifest_path()` is an associated function with a different path or name, update the call accordingly (e.g. `config::CliConfig::default_manifest_path()` or whatever matches your existing `CliConfig` definition).
</issue_to_address>

### Comment 2
<location path="tests/cli_tests/merge.rs" line_range="108" />
<code_context>
+    Ok(())
 }

 #[rstest]
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for the complementary validation error cases in CliConfig

The `CliConfig` post-merge hook enforces four conflict combinations:
- `theme = "unicode"` with `no_emoji = true`
- `theme = "ascii"` with `no_emoji = false`
- `spinner_mode = "disabled"` with `progress = true`
- `spinner_mode = "enabled"` with `progress = false`

This rstest only covers the first and third cases plus the unsupported `output_format` case. Please add `#[case]` rows for:
- `json!({ "theme": "ascii", "no_emoji": false })` -> `ExpectedValidationError::ThemeAliasConflict`
- `json!({ "spinner_mode": "enabled", "progress": false })` -> `ExpectedValidationError::SpinnerProgressConflict`

so that all branches in `validate_theme_compatibility` and `validate_spinner_mode_compatibility` are exercised.

Suggested implementation:

```rust
#[rstest]
#[case(json!({ "theme": "unicode", "no_emoji": true }), ExpectedValidationError::ThemeAliasConflict)]
#[case(json!({ "theme": "ascii", "no_emoji": false }), ExpectedValidationError::ThemeAliasConflict)]
#[case(json!({ "spinner_mode": "disabled", "progress": true }), ExpectedValidationError::SpinnerProgressConflict)]
#[case(json!({ "spinner_mode": "enabled", "progress": false }), ExpectedValidationError::SpinnerProgressConflict)]
#[case(json!({ "output_format": "foo" }), ExpectedValidationError::UnsupportedOutputFormat)]

```

I inferred the existing `#[case]` lines and their formatting. If the actual test uses a different layout (e.g. multi-line `#[case(...)]` or different argument order), mirror that style when adding:
1. A case for `json!({ "theme": "ascii", "no_emoji": false })` with `ExpectedValidationError::ThemeAliasConflict`.
2. A case for `json!({ "spinner_mode": "enabled", "progress": false })` with `ExpectedValidationError::SpinnerProgressConflict`.

Ensure these are added to the same rstest that currently covers the unicode/no_emoji, disabled/progress, and unsupported `output_format` cases.
</issue_to_address>

### Comment 3
<location path="src/cli/merge.rs" line_range="208" />
<code_context>
+    Ok(())
+}
+
+fn validation_error(key: &str, message: &str) -> Arc<OrthoError> {
+    Arc::new(OrthoError::Validation {
+        key: key.to_owned(),
</code_context>
<issue_to_address>
**issue (review_instructions):** Deduplicate the validation_error helper to avoid repeating identical logic defined in multiple modules.

Extract validation_error into a shared helper (for example in a small internal module or a common cli/utils module) and reuse it from both config.rs and merge.rs instead of redefining it twice. This keeps error construction consistent and satisfies the DRY requirement without harming readability.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
Keep code DRY, but readable. Use refactoring approaches best suited for the language in question.

</details>
</issue_to_address>

### Comment 4
<location path="docs/execplans/3-11-1-cli-config-struct.md" line_range="266" />
<code_context>
+- Keeping the parser/runtime type stable while introducing a separate merge
+  schema is a pragmatic migration path when downstream code already consumes
+  the parser type pervasively.
+- BDD coverage that touches process-wide environment variables must hold
+  `EnvLock` for the full scenario, not only for individual mutations.
+
</code_context>
<issue_to_address>
**suggestion (review_instructions):** The acronym “BDD” is used here without having been defined earlier in the document.

Please expand this at first use to something like “behaviour-driven development (BDD) coverage …”, and then use “BDD” on its own afterwards.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf2ccff65d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/execplans/3-11-1-cli-config-struct.md`:
- Around line 17-22: Update the documentation to remove hard-coded absolute
paths and commands: replace occurrences of "/home/user/project/..." and
"/root/.bun/bin" (and any other machine-local paths) with repo-relative links
and repository-root command examples (e.g., use ./docs/..., <repo-root>/path or
repository-relative commands like ./scripts/... or yarn/npm run from the repo
root); adjust the examples in this section and the same pattern in lines 491-537
so all links and shell commands work when the repo is checked out in any
location and do not assume a specific user/home directory.
- Around line 271-288: Update the docs/execplans/3-11-1-cli-config-struct.md
section so it is either marked explicitly as a "pre-change baseline" or
rewritten to reflect the final shipped state: replace present-tense phrases like
"The current implementation" and the literal mention of Cargo.toml pinning
`ortho_config = "0.7.0"` with historical/contextual wording or the actual
shipped values (e.g., `ortho_config = "0.8.0"` and the current toolchain), and
ensure references to `src/cli/mod.rs`, `src/main.rs`, `src/output_mode.rs`,
`src/output_prefs.rs`, `src/runner/mod.rs`, and `tests/cli_tests/merge.rs` are
accurate to the final layout.

In `@docs/netsuke-design.md`:
- Around line 2013-2037: Replace the US spelling "canonicalizes" with the
en-GB-oxendict spelling "canonicalises" in the documentation sentence that
discusses the `theme` field and the compatibility alias `no_emoji` (the
occurrence of the word "canonicalizes" in that paragraph should be changed to
"canonicalises").

In `@docs/ortho-config-users-guide.md`:
- Around line 1197-1234: In the "Generating PowerShell help" section update the
sentence that reads "alongside a wrapper module so `Get-Help {BinName} -Full`
surfaces the same configuration metadata as the man page generator" by inserting
a comma after "wrapper module" (i.e., "alongside a wrapper module, so `Get-Help
{BinName} -Full`...") to improve readability; locate this text near the
cargo-orthohelp PowerShell example and adjust the copy accordingly.

In `@docs/users-guide.md`:
- Around line 560-574: The users-guide top-level schema list omits the
fetch-policy fields from CliConfig; update the schema paragraph under CliConfig
to include the four fetch-related keys: fetch_allow_scheme, fetch_allow_host,
fetch_block_host, and fetch_default_deny, showing their expected types/values
(e.g. arrays for allow/block and boolean for default deny) so the docs match the
shipped CliConfig schema; ensure the new entries appear in the same style as the
other keys and keep ordering consistent around locale and accessible.

In `@src/cli/merge.rs`:
- Around line 208-213: The helper function validation_error (returning
Arc<OrthoError::Validation>) is duplicated between the CLI modules; remove the
duplicate in src/cli/merge.rs and centralize it by extracting it to a shared CLI
error helper (e.g., a new cli::error module) or by exporting the existing
validation_error from src/cli/config.rs and importing it in merge.rs; update
references in merge.rs to call the single shared validation_error and ensure the
function signature (validation_error(key: &str, message: &str) ->
Arc<OrthoError>) and the OrthoError variant names remain unchanged.

In `@src/cli/parser.rs`:
- Around line 156-164: The no_emoji_override function currently returns
self.no_emoji verbatim so --no-emoji false yields Some(false) and acts as a hard
override; change no_emoji_override (pub const fn no_emoji_override(&self) ->
Option<bool>) so that only Some(true) or Theme::Ascii produce Some(true) and any
Some(false) is treated as None (i.e., defer to env/auto-detection); additionally
add a regression test (e.g., test_no_emoji_false_defers_to_env) that sets
self.no_emoji = Some(false) and verifies the function defers to
environment-based suppression rather than returning Some(false).

In `@tests/features/configuration_preferences.feature`:
- Around line 10-16: The scenario "CLI locale and verbose flags override
configuration and environment" currently only sets a config file and CLI flags,
so either add a step that explicitly sets an environment value (e.g., Given the
environment variable NETSUKE_LOCALE is "es-ES" or similar) before parsing to
exercise env precedence, or rename the scenario to remove "and environment"
(e.g., "CLI locale and verbose flags override configuration") to accurately
reflect what it tests; update the scenario title and/or add the Given
environment step and use the existing CLI parsing/merging steps (the scenario
name is the unique identifier to change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 51194ab1-c2b6-4616-8579-de5d0567360d

📥 Commits

Reviewing files that changed from the base of the PR and between b7321a8 and cf2ccff.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • Cargo.toml
  • build.rs
  • docs/execplans/3-11-1-cli-config-struct.md
  • docs/netsuke-design.md
  • docs/ortho-config-users-guide.md
  • docs/roadmap.md
  • docs/users-guide.md
  • src/cli/config.rs
  • src/cli/merge.rs
  • src/cli/mod.rs
  • src/cli/parser.rs
  • src/cli/parsing.rs
  • src/main.rs
  • src/runner/mod.rs
  • tests/bdd/fixtures/mod.rs
  • tests/bdd/steps/configuration_preferences.rs
  • tests/bdd/steps/locale_resolution.rs
  • tests/bdd/steps/mod.rs
  • tests/cli_tests/merge.rs
  • tests/features/configuration_preferences.feature

@leynos leynos changed the title Wired CliConfig through OrthoConfig 0.8.0; refactor CLI layers Complete CLI config struct task: CliConfig as OrthoConfig surface (0.8.0) Mar 12, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/users-guide.md`:
- Around line 612-614: Edit the sentence containing `output_format = "json"` and
roadmap item `3.10.3` to insert a comma after the introductory phrase "until
then" so it reads "...until then, the only supported value is `"human"`"; ensure
the change follows en-GB-oxendict spelling and punctuation conventions.

In `@src/cli/merge.rs`:
- Around line 82-116: The CLI override builder cli_overrides_from_matches is
missing explicit display-format fields so flags like colour_policy,
spinner_mode, output_format, and theme are never injected into the overrides
map; add maybe_insert_explicit(...) calls for each missing field (using the same
pattern and &mut root) referencing the corresponding Cli struct members
(cli.colour_policy, cli.spinner_mode, cli.output_format, cli.theme) so these
values are present in the overrides used later when merging configs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 50379dd5-d37a-419c-852c-6a0ee3aff9fc

📥 Commits

Reviewing files that changed from the base of the PR and between cf2ccff and bf276d8.

📒 Files selected for processing (11)
  • docs/execplans/3-11-1-cli-config-struct.md
  • docs/ortho-config-users-guide.md
  • docs/users-guide.md
  • src/cli/config.rs
  • src/cli/merge.rs
  • src/cli/mod.rs
  • src/cli/parser.rs
  • tests/bdd/steps/configuration_preferences.rs
  • tests/cli_tests/merge.rs
  • tests/cli_tests/parsing.rs
  • tests/features/configuration_preferences.feature

Comment on lines +82 to +116
fn cli_overrides_from_matches(cli: &Cli, matches: &ArgMatches) -> OrthoResult<Value> {
let mut root = Map::new();

maybe_insert_explicit(matches, "file", &cli.file, &mut root)?;
maybe_insert_explicit(matches, "jobs", &cli.jobs, &mut root)?;
maybe_insert_explicit(matches, "verbose", &cli.verbose, &mut root)?;
maybe_insert_explicit(matches, "locale", &cli.locale, &mut root)?;
maybe_insert_explicit(
matches,
"fetch_allow_scheme",
&cli.fetch_allow_scheme,
&mut root,
)?;
maybe_insert_explicit(
matches,
"fetch_allow_host",
&cli.fetch_allow_host,
&mut root,
)?;
maybe_insert_explicit(
matches,
"fetch_block_host",
&cli.fetch_block_host,
&mut root,
)?;
maybe_insert_explicit(
matches,
"fetch_default_deny",
&cli.fetch_default_deny,
&mut root,
)?;
maybe_insert_explicit(matches, "accessible", &cli.accessible, &mut root)?;
maybe_insert_explicit(matches, "progress", &cli.progress, &mut root)?;
maybe_insert_explicit(matches, "no_emoji", &cli.no_emoji, &mut root)?;

Copy link
Contributor

@coderabbitai coderabbitai bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Insert all explicit display-format CLI fields into overrides.

Fix this now. Lines 82-116 omit colour_policy, spinner_mode,
output_format, and theme. Line 172-Line 176 then read those values only
from merged config, so explicit CLI flags are lost or overridden by lower
precedence layers.

🐛 Patch
     maybe_insert_explicit(matches, "accessible", &cli.accessible, &mut root)?;
     maybe_insert_explicit(matches, "progress", &cli.progress, &mut root)?;
     maybe_insert_explicit(matches, "no_emoji", &cli.no_emoji, &mut root)?;
+    maybe_insert_explicit(matches, "colour_policy", &cli.colour_policy, &mut root)?;
+    maybe_insert_explicit(matches, "spinner_mode", &cli.spinner_mode, &mut root)?;
+    maybe_insert_explicit(matches, "output_format", &cli.output_format, &mut root)?;
+    maybe_insert_explicit(matches, "theme", &cli.theme, &mut root)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/merge.rs` around lines 82 - 116, The CLI override builder
cli_overrides_from_matches is missing explicit display-format fields so flags
like colour_policy, spinner_mode, output_format, and theme are never injected
into the overrides map; add maybe_insert_explicit(...) calls for each missing
field (using the same pattern and &mut root) referencing the corresponding Cli
struct members (cli.colour_policy, cli.spinner_mode, cli.output_format,
cli.theme) so these values are present in the overrides used later when merging
configs.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat src/cli/merge.rs

Length of output: 6713


@leynos, the issue has not been resolved in the latest commit. The cli_overrides_from_matches function still terminates at no_emoji without inserting colour_policy, spinner_mode, output_format, or theme:

    maybe_insert_explicit(matches, "accessible", &cli.accessible, &mut root)?;
    maybe_insert_explicit(matches, "progress", &cli.progress, &mut root)?;
    maybe_insert_explicit(matches, "no_emoji", &cli.no_emoji, &mut root)?;
    // colour_policy, spinner_mode, output_format, theme still missing

apply_config confirms Cli holds all four fields (they are assigned there from CliConfig), so explicit CLI flags for those options will currently be lost, overridden by file or environment layers.

Here is the AI agent prompt to address this:

In `src/cli/merge.rs`, inside `cli_overrides_from_matches`, add four
`maybe_insert_explicit` calls immediately after the existing `no_emoji` call
and before the `if let Some(Commands::Build(args))` block:

```rust
    maybe_insert_explicit(matches, "colour_policy", &cli.colour_policy, &mut root)?;
    maybe_insert_explicit(matches, "spinner_mode", &cli.spinner_mode, &mut root)?;
    maybe_insert_explicit(matches, "output_format", &cli.output_format, &mut root)?;
    maybe_insert_explicit(matches, "theme", &cli.theme, &mut root)?;
```

Constraints:
- Do not change any other function, test, or file.
- Follow the same pattern as the existing `maybe_insert_explicit` calls: pass
  `matches`, the kebab-case field name matching the Clap argument ID, a shared
  reference to the corresponding `cli` field, and `&mut root`.
- Verify that `Cli` exposes `colour_policy`, `spinner_mode`, `output_format`,
  and `theme` fields (confirmed visible in `apply_config`).

Postconditions:
- `src/cli/merge.rs` compiles without errors or new warnings.
- An explicit `--colour-policy`, `--spinner-mode`, `--output-format`, or
  `--theme` flag supplied on the command line is no longer silently discarded
  when file or environment layers are also present.

🧠 Learnings used
Learnt from: leynos
Repo: leynos/netsuke PR: 251
File: tests/bdd/types.rs:309-318
Timestamp: 2025-12-29T19:00:20.890Z
Learning: In the netsuke repository, rustfmt formatting rules take precedence over manual formatting preferences. Ensure that functions like HelperName::as_str() and From trait implementations remain in multi-line format as enforced by cargo fmt, even if internal coding guidelines suggest single-line forms, to avoid breaking make check-fmt.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: In Rust projects, when a type is #[must_use] and you would otherwise discard its value, prefer using drop(...) instead of lines like 'let _ = ...' to satisfy must_use checks. This aligns with an enforcement in Cargo.toml: let_underscore_must_use = "deny". Apply this guideline across Rust source files (e.g., in src/status.rs and other Rust files) to ensure lint compliance and avoid accidental ignored results.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: Enforce the deny(lint) setting for must_use results in Cargo.toml. When discarding must_use values (e.g., Result) after operations like writeln!(io::stderr(), ...), prefer the drop(...) idiom over using let _ = to satisfy the lint. Apply this pattern across all Rust source files (src/ and beyond) to ensure consistent lint compliance.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai the engineer writes:

In src/cli/merge.rs, inside cli_overrides_from_matches, add four maybe_insert_explicit calls immediately after the existing no_emoji call and before the if let Some(Commands::Build(args)) block:

    maybe_insert_explicit(matches, "colour_policy", &cli.colour_policy, &mut root)?;
    maybe_insert_explicit(matches, "spinner_mode", &cli.spinner_mode, &mut root)?;
    maybe_insert_explicit(matches, "output_format", &cli.output_format, &mut root)?;
    maybe_insert_explicit(matches, "theme", &cli.theme, &mut root)?;

Constraints:

  • Do not change any other function, test, or file.
  • Follow the same pattern as the existing maybe_insert_explicit calls: pass matches, the kebab-case field name matching the Clap argument ID, a shared reference to the corresponding cli field, and &mut root.
  • Verify that Cli exposes colour_policy, spinner_mode, output_format, and theme fields (confirmed visible in apply_config).

Postconditions:

  • src/cli/merge.rs compiles without errors or new warnings.
  • An explicit --colour-policy, --spinner-mode, --output-format, or--theme flag supplied on the command line is no longer silently discarded when file or environment layers are also present.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Task: Add missing maybe_insert_explicit calls for display-format fields

  • Verify current state of cli_overrides_from_matches in src/cli/merge.rs
  • Verify Cli struct exposes colour_policy, spinner_mode, output_format, and theme fields
  • Insert the four missing calls after no_emoji and before the Commands::Build block

🏁 Script executed:

#!/bin/bash
# Confirm current state of cli_overrides_from_matches
rg -n "maybe_insert_explicit|Commands::Build|no_emoji" src/cli/merge.rs

# Confirm Cli struct fields for the four display-format options
rg -n "colour_policy|spinner_mode|output_format|theme" src/cli/parser.rs

Length of output: 1728


@leynos, the field names are confirmed on Cli in src/cli/parser.rs and the insertion point is clear (after Line 115, before the if let Some(Commands::Build(args)) block at Line 117). Apply this diff to src/cli/merge.rs:

     maybe_insert_explicit(matches, "accessible", &cli.accessible, &mut root)?;
     maybe_insert_explicit(matches, "progress", &cli.progress, &mut root)?;
     maybe_insert_explicit(matches, "no_emoji", &cli.no_emoji, &mut root)?;
+    maybe_insert_explicit(matches, "colour_policy", &cli.colour_policy, &mut root)?;
+    maybe_insert_explicit(matches, "spinner_mode", &cli.spinner_mode, &mut root)?;
+    maybe_insert_explicit(matches, "output_format", &cli.output_format, &mut root)?;
+    maybe_insert_explicit(matches, "theme", &cli.theme, &mut root)?;
 
     if let Some(Commands::Build(args)) = cli.command.as_ref()

No other files need to change. Once applied:

  • Explicit --colour-policy, --spinner-mode, --output-format, and --theme flags will be injected into the overrides map at the highest-precedence layer, so they can no longer be silently overridden by file or environment layers.
  • The existing canonical_theme logic in apply_config is unaffected — it still runs over the final merged CliConfig after all layers are resolved.

🧠 Learnings used
Learnt from: leynos
Repo: leynos/netsuke PR: 251
File: tests/bdd/types.rs:309-318
Timestamp: 2025-12-29T19:00:20.890Z
Learning: In the netsuke repository, rustfmt formatting rules take precedence over manual formatting preferences. Ensure that functions like HelperName::as_str() and From trait implementations remain in multi-line format as enforced by cargo fmt, even if internal coding guidelines suggest single-line forms, to avoid breaking make check-fmt.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: In Rust projects, when a type is #[must_use] and you would otherwise discard its value, prefer using drop(...) instead of lines like 'let _ = ...' to satisfy must_use checks. This aligns with an enforcement in Cargo.toml: let_underscore_must_use = "deny". Apply this guideline across Rust source files (e.g., in src/status.rs and other Rust files) to ensure lint compliance and avoid accidental ignored results.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: Enforce the deny(lint) setting for must_use results in Cargo.toml. When discarding must_use values (e.g., Result) after operations like writeln!(io::stderr(), ...), prefer the drop(...) idiom over using let _ = to satisfy the lint. Apply this pattern across all Rust source files (src/ and beyond) to ensure consistent lint compliance.

Failed to handle agent chat message. Please try again.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "maybe_insert_explicit|Commands::Build|colour_policy|spinner_mode|output_format|theme" src/cli/merge.rs

Length of output: 1449


@leynos, the fix has not been applied in the latest commit. The cli_overrides_from_matches function still terminates at diag_json (Line 157) without inserting colour_policy, spinner_mode, output_format, or theme:

154:    maybe_insert_explicit(matches, "accessible", &cli.accessible, &mut root)?;
155:    maybe_insert_explicit(matches, "progress", &cli.progress, &mut root)?;
156:    maybe_insert_explicit(matches, "no_emoji", &cli.no_emoji, &mut root)?;
157:    maybe_insert_explicit(matches, "diag_json", &cli.diag_json, &mut root)?;
159:    if let Some(Commands::Build(args)) = cli.command.as_ref()   // ← all four still missing

Here is the updated AI agent prompt:

In `src/cli/merge.rs`, inside `cli_overrides_from_matches`, add four
`maybe_insert_explicit` calls immediately after the existing `diag_json` call
(currently Line 157) and before the `if let Some(Commands::Build(args))` block
(currently Line 159):

```rust
    maybe_insert_explicit(matches, "colour_policy", &cli.colour_policy, &mut root)?;
    maybe_insert_explicit(matches, "spinner_mode", &cli.spinner_mode, &mut root)?;
    maybe_insert_explicit(matches, "output_format", &cli.output_format, &mut root)?;
    maybe_insert_explicit(matches, "theme", &cli.theme, &mut root)?;
```

Constraints:
- Modify only `src/cli/merge.rs`; do not change any other file.
- The four new calls must appear after `maybe_insert_explicit(matches, "diag_json", ...)`
  and before `if let Some(Commands::Build(args))`.
- Use the underscore-separated field names matching the Clap argument IDs as
  already used for the other fields: `"colour_policy"`, `"spinner_mode"`,
  `"output_format"`, `"theme"`.
- Follow the same single-line pattern as the `accessible`, `progress`,
  `no_emoji`, and `diag_json` calls.

Postconditions:
- `src/cli/merge.rs` compiles without errors or new warnings.
- Explicit `--colour-policy`, `--spinner-mode`, `--output-format`, and `--theme`
  flags supplied on the command line are no longer silently discarded when file
  or environment layers are also present.

🧠 Learnings used
Learnt from: leynos
Repo: leynos/netsuke PR: 251
File: tests/bdd/types.rs:309-318
Timestamp: 2025-12-29T19:00:20.890Z
Learning: In the netsuke repository, rustfmt formatting rules take precedence over manual formatting preferences. Ensure that functions like HelperName::as_str() and From trait implementations remain in multi-line format as enforced by cargo fmt, even if internal coding guidelines suggest single-line forms, to avoid breaking make check-fmt.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: In Rust projects, when a type is #[must_use] and you would otherwise discard its value, prefer using drop(...) instead of lines like 'let _ = ...' to satisfy must_use checks. This aligns with an enforcement in Cargo.toml: let_underscore_must_use = "deny". Apply this guideline across Rust source files (e.g., in src/status.rs and other Rust files) to ensure lint compliance and avoid accidental ignored results.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: Enforce the deny(lint) setting for must_use results in Cargo.toml. When discarding must_use values (e.g., Result) after operations like writeln!(io::stderr(), ...), prefer the drop(...) idiom over using let _ = to satisfy the lint. Apply this pattern across all Rust source files (src/ and beyond) to ensure consistent lint compliance.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "maybe_insert_explicit|Commands::Build|colour_policy|spinner_mode|output_format|theme" src/cli/merge.rs

Length of output: 1449


@leynos, the fix has not been applied in the latest commit. After Line 157 (diag_json), the four fields are still absent before the Commands::Build block at Line 159:

156:    maybe_insert_explicit(matches, "no_emoji", &cli.no_emoji, &mut root)?;
157:    maybe_insert_explicit(matches, "diag_json", &cli.diag_json, &mut root)?;
                                                                                  ← colour_policy, spinner_mode, output_format, theme still missing
159:    if let Some(Commands::Build(args)) = cli.command.as_ref()

Here is the AI agent prompt:

In `src/cli/merge.rs`, inside `cli_overrides_from_matches`, add four
`maybe_insert_explicit` calls immediately after the existing `diag_json` call
(Line 157) and before the `if let Some(Commands::Build(args))` block (Line 159):

```rust
    maybe_insert_explicit(matches, "colour_policy", &cli.colour_policy, &mut root)?;
    maybe_insert_explicit(matches, "spinner_mode", &cli.spinner_mode, &mut root)?;
    maybe_insert_explicit(matches, "output_format", &cli.output_format, &mut root)?;
    maybe_insert_explicit(matches, "theme", &cli.theme, &mut root)?;
```

Constraints:
- Modify only `src/cli/merge.rs`; do not change any other file.
- The four new calls must appear after `maybe_insert_explicit(matches, "diag_json", ...)`
  and before `if let Some(Commands::Build(args))`.
- Use the underscore-separated field names as already used for the other fields:
  `"colour_policy"`, `"spinner_mode"`, `"output_format"`, `"theme"`.
- Follow the same single-line pattern as the `accessible`, `progress`,
  `no_emoji`, and `diag_json` calls immediately above.
- `cli.colour_policy`, `cli.spinner_mode`, `cli.output_format`, and `cli.theme`
  are confirmed present on `Cli` (visible in `apply_config` at Lines 215–218).

Postconditions:
- `src/cli/merge.rs` compiles without errors or new warnings.
- Explicit `--colour-policy`, `--spinner-mode`, `--output-format`, and `--theme`
  flags supplied on the command line are no longer silently discarded when file
  or environment layers are also present.

🧠 Learnings used
Learnt from: leynos
Repo: leynos/netsuke PR: 251
File: tests/bdd/types.rs:309-318
Timestamp: 2025-12-29T19:00:20.890Z
Learning: In the netsuke repository, rustfmt formatting rules take precedence over manual formatting preferences. Ensure that functions like HelperName::as_str() and From trait implementations remain in multi-line format as enforced by cargo fmt, even if internal coding guidelines suggest single-line forms, to avoid breaking make check-fmt.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: In Rust projects, when a type is #[must_use] and you would otherwise discard its value, prefer using drop(...) instead of lines like 'let _ = ...' to satisfy must_use checks. This aligns with an enforcement in Cargo.toml: let_underscore_must_use = "deny". Apply this guideline across Rust source files (e.g., in src/status.rs and other Rust files) to ensure lint compliance and avoid accidental ignored results.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: Enforce the deny(lint) setting for must_use results in Cargo.toml. When discarding must_use values (e.g., Result) after operations like writeln!(io::stderr(), ...), prefer the drop(...) idiom over using let _ = to satisfy the lint. Apply this pattern across all Rust source files (src/ and beyond) to ensure consistent lint compliance.

@leynos

This comment was marked as resolved.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== src/cli/parser.rs: Default for Cli ==="
rg -n "default_manifest_path|Netsukefile" src/cli/parser.rs

echo ""
echo "=== tests/cli_tests/merge.rs: case rows ==="
rg -n 'ascii.*no_emoji|no_emoji.*false|spinner.*enabled.*progress|progress.*false|#\[case\]' tests/cli_tests/merge.rs

echo ""
echo "=== src/cli/config.rs: validation_error ==="
rg -n "fn validation_error" src/cli/config.rs

echo ""
echo "=== src/cli/merge.rs: validation_error ==="
rg -n "fn validation_error" src/cli/merge.rs

echo ""
echo "=== src/cli/mod.rs: validation_error ==="
rg -n "fn validation_error|pub.super.*validation_error" src/cli/mod.rs

echo ""
echo "=== docs/execplans/3-11-1-cli-config-struct.md: BDD first use ==="
grep -n "BDD\|behaviour-driven\|behavior-driven" docs/execplans/3-11-1-cli-config-struct.md | head -20

Length of output: 1287


@leynos: All four comments are resolved. Here is the status of each:

# Location Resolution
1 src/cli/parser.rs Default for Cli file: CliConfig::default_manifest_path() — the hard-coded "Netsukefile" literal has been replaced with the shared helper.
2 tests/cli_tests/merge.rs ✅ Both complementary cases are present: json!({ "theme": "ascii", "no_emoji": false }) and json!({ "spinner_mode": "enabled", "progress": false }), with corresponding expected-fragment strings.
3 src/cli/merge.rs / src/cli/config.rs ✅ Neither file defines validation_error locally any more; the single canonical definition lives in src/cli/mod.rs as pub(super) fn validation_error.
4 docs/execplans/3-11-1-cli-config-struct.md ✅ "BDD" is expanded as "behaviour-driven development (BDD)" at first use (en-GB spelling correct).

No requirements were annulled — none of the four violate en-GB-oxendict conventions, and none introduce unnecessary cyclomatic complexity.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos changed the title Complete CLI config struct task: CliConfig as OrthoConfig surface (0.8.0) Introduce CliConfig as OrthoConfig-driven layered CLI config (0.8.0) Mar 13, 2026
leynos and others added 10 commits March 14, 2026 00:32
Add a comprehensive execution plan document outlining the introduction of the `CliConfig` struct as the layered CLI configuration schema. This living document covers the purpose, constraints, tolerances, risks, progress stages, design decisions, and future steps for implementing a typed `CliConfig` configuration with OrthoConfig integration, separating CLI parsing from configuration merging, and improving schema explicitness for Netsuke project roadmap item 3.11.1.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
…ance

- Updated docs/execplans/3-11-1-cli-config-struct.md to align with ortho_config 0.8.0 semantics and migration rules.
- Replaced ortho-config-users-guide.md with upstream 0.8.0 version, adding detailed guidance on derive macro usage, dependency architecture, new attributes, typed Clap defaults with `cli_default_as_absent`, and documentation metadata generation.
- Clarified dependency and macro usage with ortho_config crate aliasing support.
- Introduced best practices for maintaining compatibility and upgrading from 0.7.x to 0.8.0.
- Enhanced examples for default handling and integration with cargo-orthohelp documentation tooling.
- Focused on improving reliability and usability of config schema derivation and CLI integration.

These documentation enhancements provide a comprehensive reference for implementing the roadmap item 3.11.1 and safely upgrading ortho_config dependency while preserving existing CLI and config behavior.

Refs #3.11.1 roadmap, ortho_config 0.8.0 migration

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
- Added `CliConfig` as the authoritative OrthoConfig-derived schema for CLI configuration.
- Split CLI module into parser, config, and merge submodules to separate parsing from merging.
- Implement layered merging of defaults, configuration files, environment variables, and CLI overrides.
- Added typed config fields for verbosity, locale, colour_policy, spinner_mode, output_format, theme, and build defaults.
- Added validation for incompatible theme and spinner/progress settings.
- Introduced `no_emoji` compatibility alias resolving to ASCII theme.
- Provided default build command targets from config when CLI targets are absent.
- Updated CLI parsing to preserve older `Cli` type for syntax and localized parsing.
- Added extensive unit and BDD tests to cover merging behaviour and validation.
- Updated documentation and roadmap for configuration preferences milestone 3.11.1.

This refactor improves configuration schema clarity and layering, enabling unified merging from multiple sources while preserving CLI parsing ergonomics.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Refactor CLI merge layer tests by introducing utility functions `with_config_file` and `merge_defaults_with_file_layer` to handle configuration file setup and merging. Replace repetitive tempdir and environment variable management with these helpers to improve test clarity and reduce boilerplate. Updated various tests to use these helpers, keeping test behavior unchanged but improving maintainability.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
- Introduced assert_build_targets helper to simplify build target checks.
- Introduced assert_merge_rejects helper to validate expected merge errors.
- Replaced repetitive inline assertions with helper functions in merge.rs tests.
- Improved test readability and reduced duplication in cli merge tests.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Refactor CLI config validation tests to use parameterized test cases with rstest's #[case] attributes. This reduces test code duplication and improves clarity when testing conflicting or unsupported CLI config settings.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
- Introduce ExpectedValidationError enum to represent specific merge error cases.
- Refactor assert_merge_rejects to use ExpectedValidationError instead of raw strings.
- Update test cases to use the enum, improving clarity and maintainability.
- Enhance error assertion messages for better diagnostics in build target tests.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
- Added `CliConfig` in `src/cli/config.rs` to encapsulate global CLI settings and
  subcommand defaults with OrthoConfig derive.
- Split `src/cli/mod.rs` responsibilities into parser, config, and merge modules.
- Upgraded ortho_config to v0.8.0 and migrated configuration layering accordingly.
- Updated merge and parsing logic to use `CliConfig` as authoritative merge target.
- Extended unit tests (`tests/cli_tests/merge.rs`), behavioural tests
  (`tests/features/configuration_preferences.feature`), and BDD steps.
- Updated documentation (`docs/users-guide.md`, `docs/netsuke-design.md`,
  `docs/roadmap.md`) to reflect new config schema, precedence rules, and CLI defaults.
- Improved handling for output preferences, locale overrides, and theme compatibility validation.

This change modularizes CLI configuration management, clarifies schema separation,
and sets foundation for safer future extensions while preserving legacy behavior.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Apply formatter-required whitespace and import ordering changes\nafter rebasing the CLI config work onto origin/main.\n\nThis keeps the rebased branch passing the repository quality\ngates without introducing unrelated documentation churn.
@devboxerhub devboxerhub bot force-pushed the add-cli-config-struct-vcf3xy branch from c7a0211 to 82a1f69 Compare March 14, 2026 00:37
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/netsuke-design.md`:
- Around line 2032-2036: Update the paragraph to point readers at the parser
module rather than the old location and clarify the re-export: mention the
parser-facing Cli type (symbol: Cli) is defined in the parser module and that
layered configuration now lives in CliConfig (derived with OrthoConfig), while
the top-level module re-exports the public CLI surface; replace the sentence
that references the old location with this clarified wording so docs match the
code split between the parser, CliConfig, and the module that re-exports the
public CLI surface.

In `@docs/users-guide.md`:
- Around line 616-618: In the sentence containing `colour_policy` and "Netsuke"
(the line that reads "`colour_policy` is accepted and layered today so users can
standardize..."), insert a comma after the word "today" so the clause reads
"...layered today, so users can standardize..." to correctly join the
independent clauses.

In `@src/cli/parser.rs`:
- Around line 304-306: Insert a single blank line between the preceding function
closing brace and the module-level constant declaration so the constant
pub(super) const MAX_JOBS: usize = 64; is separated from the function above;
locate the closing '}' just before MAX_JOBS in parser.rs and add one empty line
before the constant to improve readability.
- Around line 68-69: Replace the hardcoded default_value on the `file` field in
parser.rs with the centralized default from `CliConfig` to avoid drift: use
`default_value_t` (or `default_value_os_t` if
`CliConfig::default_manifest_path()` cannot be const) pointing to
`CliConfig::default_manifest_path()` (or a new `const DEFAULT_MANIFEST_PATH`) so
the `pub file: PathBuf` argument's default is sourced from the single canonical
method/constant; if necessary make `CliConfig::default_manifest_path()` a `const
fn` or introduce a `const DEFAULT_MANIFEST_PATH` and reference that in the
attribute.

In `@tests/bdd/steps/configuration_preferences.rs`:
- Around line 40-43: Replace the unsafe direct call to std::env::set_var with
the shared test helper: import test_support::env::set_var and call that helper
instead of unsafe { std::env::set_var(CONFIG_ENV_VAR,
OsStr::new(path.as_os_str())) }; keep the surrounding logic (capturing previous
via std::env::var_os(CONFIG_ENV_VAR), the SAFETY note about EnvLock, and
world.track_env_var(CONFIG_ENV_VAR.to_owned(), previous)) unchanged so the
EnvLock and world tracking continue to protect the environment mutation.

In `@tests/cli_tests/merge.rs`:
- Around line 102-113: The helper function assert_merge_rejects should stop
using expect_err (which is banned outside tests); update the call to
merge_defaults_with_file_layer to a match: on Ok(value) call anyhow::bail! with
a message like "merge should have failed, got {value:#?}", and on Err(e) bind e
to err for the subsequent assertions; reference assert_merge_rejects and
merge_defaults_with_file_layer when changing the code and preserve the existing
ensure! check that verifies err.to_string() contains
expected_error.expected_fragment().

In `@tests/features/configuration_preferences.feature`:
- Around line 26-34: Move the Gherkin step "Then parsing succeeds" so it appears
immediately after the step "And the success prefix is rendered" (i.e., place the
primary Then assertion before its trailing And-steps), ensuring the scenario
reads: Given..., And the Netsuke config..., When..., And merged output
preferences are resolved, And the success prefix is rendered, Then parsing
succeeds, And the merged theme is ascii, And the prefix contains no non-ASCII
characters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b2a7bc7-be70-4ff8-9d22-d0abb6304a56

📥 Commits

Reviewing files that changed from the base of the PR and between bf276d8 and 82a1f69.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • Cargo.toml
  • build.rs
  • docs/execplans/3-11-1-cli-config-struct.md
  • docs/netsuke-design.md
  • docs/ortho-config-users-guide.md
  • docs/roadmap.md
  • docs/users-guide.md
  • src/cli/config.rs
  • src/cli/merge.rs
  • src/cli/mod.rs
  • src/cli/parser.rs
  • src/cli/parsing.rs
  • src/main.rs
  • src/runner/mod.rs
  • tests/bdd/fixtures/mod.rs
  • tests/bdd/steps/configuration_preferences.rs
  • tests/bdd/steps/locale_resolution.rs
  • tests/bdd/steps/mod.rs
  • tests/cli_tests/merge.rs
  • tests/cli_tests/parsing.rs
  • tests/features/configuration_preferences.feature

Comment on lines +40 to +43
let previous = std::env::var_os(CONFIG_ENV_VAR);
// SAFETY: `EnvLock` is held in `world.env_lock` for the lifetime of the scenario.
unsafe { std::env::set_var(CONFIG_ENV_VAR, OsStr::new(path.as_os_str())) };
world.track_env_var(CONFIG_ENV_VAR.to_owned(), previous);
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the file content at the specified lines
echo "=== Content at lines 40-43 and 81-84 in configuration_preferences.rs ==="
if [ -f "tests/bdd/steps/configuration_preferences.rs" ]; then
  echo "Lines 35-50:"
  sed -n '35,50p' tests/bdd/steps/configuration_preferences.rs
  echo
  echo "Lines 76-90:"
  sed -n '76,90p' tests/bdd/steps/configuration_preferences.rs
else
  echo "File not found at expected path"
  git ls-files | grep -i configuration_preferences || true
fi

echo
echo "=== Search for direct std::env::(set_var|remove_var) calls in test code ==="
rg -n --type rust 'std::env::(set_var|remove_var)' tests/ --max-count 20 || true

echo
echo "=== Search for existing env helpers (EnvLock, EnvVarGuard, etc.) ==="
rg -n --type rust '\b(EnvLock|EnvVarGuard|track_env_var|set_var|remove_var)\b' tests/ --max-count 30 || true

echo
echo "=== Check for test_utils, test_helpers, or shared test support crates ==="
find tests -type d -name '*test*' -o -name '*helper*' -o -name '*util*' | head -20

echo
echo "=== Check repository structure for shared test infrastructure ==="
ls -la tests/ | head -20

Repository: leynos/netsuke

Length of output: 11884


Replace direct std::env::set_var calls with the shared test_support::env::set_var helper.

Direct calls to std::env::set_var in test code violate the repository rule requiring environment mutations to use shared guards from the test-support layer. Other BDD steps already follow this pattern (fs.rs, manifest/mod.rs). Import and use test_support::env::set_var instead.

Affects lines 42 and 83.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/bdd/steps/configuration_preferences.rs` around lines 40 - 43, Replace
the unsafe direct call to std::env::set_var with the shared test helper: import
test_support::env::set_var and call that helper instead of unsafe {
std::env::set_var(CONFIG_ENV_VAR, OsStr::new(path.as_os_str())) }; keep the
surrounding logic (capturing previous via std::env::var_os(CONFIG_ENV_VAR), the
SAFETY note about EnvLock, and world.track_env_var(CONFIG_ENV_VAR.to_owned(),
previous)) unchanged so the EnvLock and world tracking continue to protect the
environment mutation.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full content of configuration_preferences.rs ==="
if [ -f "tests/bdd/steps/configuration_preferences.rs" ]; then
  cat -n tests/bdd/steps/configuration_preferences.rs
else
  echo "File not found"
  git ls-files | grep -i configuration_preferences || true
fi

echo
echo "=== Search for direct std::env::(set_var|remove_var) in this file ==="
rg -n 'std::env::(set_var|remove_var)|env::set_var|env::remove_var' tests/bdd/steps/configuration_preferences.rs || echo "No matches found"

echo
echo "=== Search for test_support::env usage in this file ==="
rg -n 'test_support::env' tests/bdd/steps/configuration_preferences.rs || echo "No matches found"

echo
echo "=== Check what test_support::env provides ==="
rg -rn 'pub fn set_var\|pub fn remove_var' --type rust tests/ || true
find . -path "*/test_support*" -name "*.rs" | head -20

Length of output: 8925


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Content of test_support/src/env.rs ==="
cat -n test_support/src/env.rs

echo
echo "=== Content of test_support/src/lib.rs (to see public API) ==="
cat -n test_support/src/lib.rs

Length of output: 17955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Content of test_support/src/env_lock.rs ==="
cat -n test_support/src/env_lock.rs

Length of output: 1117


@leynos, this is not yet resolved. Lines 42 and 83 still contain direct unsafe { std::env::set_var(...) } calls in the test file.

There is also a subtlety that prevents a naïve replacement: EnvLock uses a plain std::sync::Mutex, which is not re-entrant. Both write_config and set_environment_locale_override call ensure_env_lock(world) first, storing an EnvLock in world.env_lock. Calling test_support::env::set_var (which internally calls EnvLock::acquire()) while that lock is already held on the same thread would deadlock.

The correct fix is to add a set_var_locked helper to test_support::env that accepts an already-held &EnvLock as proof of serialisation, keeping the unsafe call inside the shared crate:

In `test_support/src/env.rs`, add the following public function after `set_var`:

```rust
/// Set an environment variable while the caller already holds an [`EnvLock`].
///
/// Returns the previous value. Use this when a scenario-wide lock must span
/// several mutations; taking `&EnvLock` proves serialisation without
/// re-acquiring the mutex (which would deadlock, as `Mutex` is not
/// re-entrant).
pub fn set_var_locked(_lock: &EnvLock, key: &str, value: &OsStr) -> Option<OsString> {
    let previous = std::env::var_os(key);
    // SAFETY: caller holds `EnvLock`, which serialises all mutations.
    unsafe { std::env::set_var(key, value) };
    previous
}
```

Then in `tests/bdd/steps/configuration_preferences.rs`:

1. Add `use test_support::env::set_var_locked;` to the imports.
2. In `write_config` (around line 40–43), replace:
   ```rust
   let previous = std::env::var_os(CONFIG_ENV_VAR);
   // SAFETY: `EnvLock` is held in `world.env_lock` for the lifetime of the scenario.
   unsafe { std::env::set_var(CONFIG_ENV_VAR, OsStr::new(path.as_os_str())) };
   ```
   with:
   ```rust
   let lock = world.env_lock.borrow();
   let lock_ref = lock.as_ref().expect("env_lock must be held before write_config");
   let previous = set_var_locked(lock_ref, CONFIG_ENV_VAR, OsStr::new(path.as_os_str()));
   ```
3. In `set_environment_locale_override` (around line 81–83), replace:
   ```rust
   let previous = std::env::var_os(LOCALE_ENV_VAR);
   // SAFETY: `EnvLock` is held in `world.env_lock` for the lifetime of the scenario.
   unsafe { std::env::set_var(LOCALE_ENV_VAR, OsStr::new(locale)) };
   ```
   with:
   ```rust
   let lock = world.env_lock.borrow();
   let lock_ref = lock.as_ref().expect("env_lock must be held before set_environment_locale_override");
   let previous = set_var_locked(lock_ref, LOCALE_ENV_VAR, OsStr::new(locale));
   ```
4. Remove the `use std::ffi::OsStr;` import only if it is no longer used elsewhere in the file (it is still needed for `OsStr::new` in the calls above, so keep it).
5. Ensure the `SAFETY` comments and the `unsafe` block are now absent from `configuration_preferences.rs`.

DO NOT change test names, step definitions, `#[rstest]` annotations, fixtures, or any logic outside the two env-mutation sites described above.

🧠 Learnings used
Learnt from: leynos
Repo: leynos/netsuke PR: 251
File: tests/bdd/types.rs:309-318
Timestamp: 2025-12-29T19:00:20.890Z
Learning: In the netsuke repository, rustfmt formatting rules take precedence over manual formatting preferences. Ensure that functions like HelperName::as_str() and From trait implementations remain in multi-line format as enforced by cargo fmt, even if internal coding guidelines suggest single-line forms, to avoid breaking make check-fmt.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: In Rust projects, when a type is #[must_use] and you would otherwise discard its value, prefer using drop(...) instead of lines like 'let _ = ...' to satisfy must_use checks. This aligns with an enforcement in Cargo.toml: let_underscore_must_use = "deny". Apply this guideline across Rust source files (e.g., in src/status.rs and other Rust files) to ensure lint compliance and avoid accidental ignored results.

Learnt from: leynos
Repo: leynos/netsuke PR: 266
File: src/status.rs:92-106
Timestamp: 2026-02-15T18:36:42.457Z
Learning: Enforce the deny(lint) setting for must_use results in Cargo.toml. When discarding must_use values (e.g., Result) after operations like writeln!(io::stderr(), ...), prefer the drop(...) idiom over using let _ = to satisfy the lint. Apply this pattern across all Rust source files (src/ and beyond) to ensure consistent lint compliance.

Comment on lines +26 to +34
Scenario: no_emoji compatibility alias resolves to the ASCII theme
Given a minimal Netsuke workspace
And the Netsuke config file sets no_emoji to true
When the CLI is parsed and merged with ""
And merged output preferences are resolved
And the success prefix is rendered
Then parsing succeeds
And the merged theme is ascii
And the prefix contains no non-ASCII characters
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Reorder the "Then parsing succeeds" step before the "And" steps.

The Gherkin step "Then parsing succeeds" at line 32 appears after several "And" continuation steps. Standard BDD practice places the primary assertion ("Then") before its refinements. Move line 32 to follow line 29 immediately.

♻️ Proposed fix
   Scenario: no_emoji compatibility alias resolves to the ASCII theme
     Given a minimal Netsuke workspace
     And the Netsuke config file sets no_emoji to true
     When the CLI is parsed and merged with ""
+    Then parsing succeeds
     And merged output preferences are resolved
     And the success prefix is rendered
-    Then parsing succeeds
     And the merged theme is ascii
     And the prefix contains no non-ASCII characters
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Scenario: no_emoji compatibility alias resolves to the ASCII theme
Given a minimal Netsuke workspace
And the Netsuke config file sets no_emoji to true
When the CLI is parsed and merged with ""
And merged output preferences are resolved
And the success prefix is rendered
Then parsing succeeds
And the merged theme is ascii
And the prefix contains no non-ASCII characters
Scenario: no_emoji compatibility alias resolves to the ASCII theme
Given a minimal Netsuke workspace
And the Netsuke config file sets no_emoji to true
When the CLI is parsed and merged with ""
Then parsing succeeds
And merged output preferences are resolved
And the success prefix is rendered
And the merged theme is ascii
And the prefix contains no non-ASCII characters
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/features/configuration_preferences.feature` around lines 26 - 34, Move
the Gherkin step "Then parsing succeeds" so it appears immediately after the
step "And the success prefix is rendered" (i.e., place the primary Then
assertion before its trailing And-steps), ensuring the scenario reads: Given...,
And the Netsuke config..., When..., And merged output preferences are resolved,
And the success prefix is rendered, Then parsing succeeds, And the merged theme
is ascii, And the prefix contains no non-ASCII characters.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai Has this now been resolved in the latest commit?

Use codegraph analysis to determine your answer.

If this comment is now resolved, please mark it as such using the API. Otherwise, please provide an AI agent prompt for the remaining work to be done to address this comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat tests/features/configuration_preferences.feature

Length of output: 1450


@leynos, the fix has not been applied in the latest commit. The no_emoji scenario in tests/features/configuration_preferences.feature still reads:

  Scenario: no_emoji compatibility alias resolves to the ASCII theme
    Given a minimal Netsuke workspace
    And the Netsuke config file sets no_emoji to true
    When the CLI is parsed and merged with ""
    And merged output preferences are resolved
    And the success prefix is rendered
    Then parsing succeeds          ← still misplaced here
    And the merged theme is ascii
    And the prefix contains no non-ASCII characters

Here is an agent prompt to address it:

In `tests/features/configuration_preferences.feature`, fix the step ordering in the "no_emoji compatibility alias resolves to the ASCII theme" scenario. Move the `Then parsing succeeds` step so it appears immediately after `When the CLI is parsed and merged with ""`, before the `And merged output preferences are resolved` and `And the success prefix is rendered` steps. The corrected scenario should read:

  Scenario: no_emoji compatibility alias resolves to the ASCII theme
    Given a minimal Netsuke workspace
    And the Netsuke config file sets no_emoji to true
    When the CLI is parsed and merged with ""
    Then parsing succeeds
    And merged output preferences are resolved
    And the success prefix is rendered
    And the merged theme is ascii
    And the prefix contains no non-ASCII characters

Do NOT alter any other scenario or any other file. Ensure the file is valid Gherkin and that the BDD test suite still passes.

@leynos
Copy link
Owner Author

leynos commented Mar 14, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

build.rs

Comment on file

    const _: usize = std::mem::size_of::<cli::BuildArgs>();
    const _: usize = std::mem::size_of::<cli::CliConfig>();
    const _: usize = std::mem::size_of::<cli::Commands>();

❌ New issue: Large Method
main has 70 lines, threshold = 70

@coderabbitai

This comment was marked as resolved.

Moved parser-facing `Cli` type to `src/cli/parser.rs` and layered configuration
into a `CliConfig` struct in `src/cli/config.rs`. The top-level `src/cli/mod.rs`
now re-exports the public CLI surface. This separation improves modularity by
distinguishing parsing, configuration discovery, and runtime command selection
while preserving existing command syntax.

Also updated default manifest path handling in CLI arguments and enhanced test
support environment isolation.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Copy link

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates Failed
Enforce advisory code health rules (1 file with Large Method)

Gates Passed
5 Quality Gates Passed

See analysis details in CodeScene

Reason for failure
Enforce advisory code health rules Violations Code Health Impact
build.rs 1 advisory rule 10.00 → 9.61 Suppress

Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@leynos leynos changed the title Introduce CliConfig as OrthoConfig-driven layered CLI config (0.8.0) Complete CLI config struct implementation with OrthoConfig (0.8.0) Mar 14, 2026
@leynos
Copy link
Owner Author

leynos commented Mar 15, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Large Method

build.rs: main

What lead to degradation?

main has 70 lines, threshold = 70

Why does this problem occur?

Overly long functions make the code harder to read. The recommended maximum function length for the Rust language is 70 lines of code. Severity: Brain Method - Complex Method - Long Method.

How to fix it?

We recommend to be careful here -- just splitting long functions don't necessarily make the code easier to read. Instead, look for natural chunks inside the functions that expresses a specific task or concern. Often, such concerns are indicated by a Code Comment followed by an if-statement. Use the EXTRACT FUNCTION refactoring to encapsulate that concern.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== build.rs: main function ==="
rg -n "^fn main|^}" build.rs

echo ""
echo "=== build.rs: helper functions present? ==="
rg -n "^fn verify_public_api_symbols\|^fn emit_rerun_directives\|^fn generate_man_page" build.rs

echo ""
echo "=== build.rs: full file ==="
cat -n build.rs

Length of output: 7882


@leynos: The previous prompt was not yet applied — main currently spans lines 91–172 (82 lines). The fix is identical to the one supplied last round; here it is restated precisely against the current file.


Root cause

main inlines three independent concerns:

Block Lines Concern
92–109 18 const _: API symbol checks
112–127 16 cargo:rerun-if-* directives
132–169 38 Man-page construction, rendering, and writing

Extracting all three drops main to 7 lines.


Fix

verify_public_api_symbols

fn verify_public_api_symbols() {
    // Exercise CLI localization, config merge, and host pattern symbols so the
    // shared modules remain linked when the build script is compiled without
    // tests.
    const _: usize = std::mem::size_of::<cli::BuildArgs>();
    const _: usize = std::mem::size_of::<cli::CliConfig>();
    const _: usize = std::mem::size_of::<cli::Commands>();
    const _: usize = std::mem::size_of::<HostPattern>();
    const _: fn(&[OsString]) -> Option<String> = cli::locale_hint_from_args;
    const _: fn(&[OsString]) -> Option<bool> = cli::diag_json_hint_from_args;
    const _: fn(&str) -> Option<bool> = cli_l10n::parse_bool_hint;
    const _: fn(&cli::Cli, &ArgMatches) -> bool = cli::resolve_merged_diag_json;
    const _: fn(&cli::Cli, &ArgMatches) -> ortho_config::OrthoResult<cli::Cli> =
        cli::merge_with_config;
    const _: LocalizedParseFn = cli::parse_with_localizer_from;
    const _: fn(&cli::Cli) -> Option<bool> = cli::Cli::no_emoji_override;
    const _: fn(&cli::Cli) -> bool = cli::Cli::progress_enabled;
    const _: fn(&str) -> Result<HostPattern, HostPatternError> = HostPattern::parse;
    const _: fn(&HostPattern, host_pattern::HostCandidate<'_>) -> bool = HostPattern::matches;
}

emit_rerun_directives

fn emit_rerun_directives() {
    // Regenerate the manual page when the CLI or metadata changes.
    println!("cargo:rerun-if-changed=src/cli/mod.rs");
    println!("cargo:rerun-if-changed=src/cli/config.rs");
    println!("cargo:rerun-if-changed=src/cli/merge.rs");
    println!("cargo:rerun-if-changed=src/cli/parser.rs");
    println!("cargo:rerun-if-changed=src/cli/parsing.rs");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_NAME");
    println!("cargo:rerun-if-env-changed=CARGO_BIN_NAME");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_DESCRIPTION");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_AUTHORS");
    println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
    println!("cargo:rerun-if-env-changed=TARGET");
    println!("cargo:rerun-if-env-changed=PROFILE");
    println!("cargo:rerun-if-changed=src/localization/keys.rs");
    println!("cargo:rerun-if-changed=locales/en-US/messages.ftl");
    println!("cargo:rerun-if-changed=locales/es-ES/messages.ftl");
}

generate_man_page

fn generate_man_page(out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
    // The top-level page documents the entire command interface.
    let cmd = cli::Cli::command();
    let name = cmd
        .get_bin_name()
        .unwrap_or_else(|| cmd.get_name())
        .to_owned();
    let cargo_bin = env::var("CARGO_BIN_NAME")
        .or_else(|_| env::var("CARGO_PKG_NAME"))
        .unwrap_or_else(|_| name.clone());
    if name != cargo_bin {
        return Err(format!(
            "CLI name {name} differs from Cargo bin/package name {cargo_bin}; packaging expects {cargo_bin}.1"
        )
        .into());
    }
    let version = env::var("CARGO_PKG_VERSION").map_err(
        |_| "CARGO_PKG_VERSION must be set by Cargo; cannot render manual page without it.",
    )?;
    let man = Man::new(cmd)
        .section("1")
        .source(format!("{cargo_bin} {version}"))
        .date(manual_date());
    let mut buf = Vec::new();
    man.render(&mut buf)?;
    let page_name = format!("{cargo_bin}.1");
    write_man_page(&buf, out_dir, &page_name)?;
    if let Some(extra_dir) = env::var_os("OUT_DIR") {
        let extra_dir_path = PathBuf::from(extra_dir);
        if let Err(err) = write_man_page(&buf, &extra_dir_path, &page_name) {
            println!(
                "cargo:warning=Failed to stage manual page in OUT_DIR ({}): {err}",
                extra_dir_path.display()
            );
        }
    }
    Ok(())
}

Resulting main (7 lines)

fn main() -> Result<(), Box<dyn std::error::Error>> {
    verify_public_api_symbols();
    emit_rerun_directives();
    build_l10n_audit::audit_localization_keys()?;
    let out_dir = out_dir_for_target_profile();
    generate_man_page(&out_dir)
}

AI coding agent prompt

File to modify: build.rs

## Objective
Reduce `main` in `build.rs` below CodeScene's 70-line threshold by extracting three
private helper functions. The current `main` runs from line 91 to 172 (82 lines).

## Step 1 — Extract `verify_public_api_symbols`

Move the entire `const _:` block (lines 92–109, including its preceding comment) out of
`main` into a new private function inserted immediately before `main`:

```rust
fn verify_public_api_symbols() {
    // Exercise CLI localization, config merge, and host pattern symbols so the
    // shared modules remain linked when the build script is compiled without
    // tests.
    const _: usize = std::mem::size_of::<cli::BuildArgs>();
    const _: usize = std::mem::size_of::<cli::CliConfig>();
    const _: usize = std::mem::size_of::<cli::Commands>();
    const _: usize = std::mem::size_of::<HostPattern>();
    const _: fn(&[OsString]) -> Option<String> = cli::locale_hint_from_args;
    const _: fn(&[OsString]) -> Option<bool> = cli::diag_json_hint_from_args;
    const _: fn(&str) -> Option<bool> = cli_l10n::parse_bool_hint;
    const _: fn(&cli::Cli, &ArgMatches) -> bool = cli::resolve_merged_diag_json;
    const _: fn(&cli::Cli, &ArgMatches) -> ortho_config::OrthoResult<cli::Cli> =
        cli::merge_with_config;
    const _: LocalizedParseFn = cli::parse_with_localizer_from;
    const _: fn(&cli::Cli) -> Option<bool> = cli::Cli::no_emoji_override;
    const _: fn(&cli::Cli) -> bool = cli::Cli::progress_enabled;
    const _: fn(&str) -> Result<HostPattern, HostPatternError> = HostPattern::parse;
    const _: fn(&HostPattern, host_pattern::HostCandidate<'_>) -> bool = HostPattern::matches;
}
```

## Step 2 — Extract `emit_rerun_directives`

Move all `println!("cargo:rerun-if-*")` statements (lines 112–127, including the preceding
comment) out of `main` into a new private function inserted immediately before `main`:

```rust
fn emit_rerun_directives() {
    // Regenerate the manual page when the CLI or metadata changes.
    println!("cargo:rerun-if-changed=src/cli/mod.rs");
    println!("cargo:rerun-if-changed=src/cli/config.rs");
    println!("cargo:rerun-if-changed=src/cli/merge.rs");
    println!("cargo:rerun-if-changed=src/cli/parser.rs");
    println!("cargo:rerun-if-changed=src/cli/parsing.rs");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_NAME");
    println!("cargo:rerun-if-env-changed=CARGO_BIN_NAME");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_DESCRIPTION");
    println!("cargo:rerun-if-env-changed=CARGO_PKG_AUTHORS");
    println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
    println!("cargo:rerun-if-env-changed=TARGET");
    println!("cargo:rerun-if-env-changed=PROFILE");
    println!("cargo:rerun-if-changed=src/localization/keys.rs");
    println!("cargo:rerun-if-changed=locales/en-US/messages.ftl");
    println!("cargo:rerun-if-changed=locales/es-ES/messages.ftl");
}
```

## Step 3 — Extract `generate_man_page`

Move the man-page construction and writing block (lines 132–169, including its preceding
comments) out of `main` into a new private function inserted immediately before `main`:

```rust
fn generate_man_page(out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
    // The top-level page documents the entire command interface.
    let cmd = cli::Cli::command();
    let name = cmd
        .get_bin_name()
        .unwrap_or_else(|| cmd.get_name())
        .to_owned();
    let cargo_bin = env::var("CARGO_BIN_NAME")
        .or_else(|_| env::var("CARGO_PKG_NAME"))
        .unwrap_or_else(|_| name.clone());
    if name != cargo_bin {
        return Err(format!(
            "CLI name {name} differs from Cargo bin/package name {cargo_bin}; packaging expects {cargo_bin}.1"
        )
        .into());
    }
    let version = env::var("CARGO_PKG_VERSION").map_err(
        |_| "CARGO_PKG_VERSION must be set by Cargo; cannot render manual page without it.",
    )?;
    let man = Man::new(cmd)
        .section("1")
        .source(format!("{cargo_bin} {version}"))
        .date(manual_date());
    let mut buf = Vec::new();
    man.render(&mut buf)?;
    let page_name = format!("{cargo_bin}.1");
    write_man_page(&buf, out_dir, &page_name)?;
    if let Some(extra_dir) = env::var_os("OUT_DIR") {
        let extra_dir_path = PathBuf::from(extra_dir);
        if let Err(err) = write_man_page(&buf, &extra_dir_path, &page_name) {
            println!(
                "cargo:warning=Failed to stage manual page in OUT_DIR ({}): {err}",
                extra_dir_path.display()
            );
        }
    }
    Ok(())
}
```

## Step 4 — Replace `main` with a delegation body

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    verify_public_api_symbols();
    emit_rerun_directives();
    build_l10n_audit::audit_localization_keys()?;
    let out_dir = out_dir_for_target_profile();
    generate_man_page(&out_dir)
}
```

## Constraints
- Do NOT alter the logic, order of operations, or any string literals in the extracted code.
- Do NOT modify any file other than `build.rs`.
- After refactoring, the build script must compile and the build must succeed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant