Skip to content

Adopt structured errors for Ninja generation#152

Merged
leynos merged 12 commits intomainfrom
codex/implement-actionable-error-reporting
Sep 14, 2025
Merged

Adopt structured errors for Ninja generation#152
leynos merged 12 commits intomainfrom
codex/implement-actionable-error-reporting

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Sep 10, 2025

Summary

  • use anyhow for contextual error propagation in runner and manifest
  • add NinjaGenError for actionable Ninja generation failures
  • exercise missing-action paths in unit and behavioural tests

Testing

  • make fmt
  • make lint
  • make test
  • make markdownlint
  • make nixie

https://chatgpt.com/codex/tasks/task_e_68c0a537c8648322b451b857125d22df

Summary by Sourcery

Adopt structured error handling for Ninja generation and manifest parsing by introducing NinjaGenError, propagating errors with anyhow’s context methods, standardizing diagnostics with thiserror and miette, restructuring runner logic into a process module, and updating tests and documentation to cover and reflect these changes

New Features:

  • Introduce NinjaGenError enum for actionable ninja generation errors
  • Change ninja_gen::generate signature to return Result<String, NinjaGenError> and add generate_into for streaming output

Enhancements:

  • Replace custom diag extensions with anyhow’s context for error propagation in runner and manifest modules
  • Extract ninja invocation logic into a dedicated runner::process module
  • Adopt thiserror and miette patterns for structured diagnostics in manifest parsing

Documentation:

  • Update design and user documentation to reflect the structured error handling strategy and Result-based ninja API

Tests:

  • Add unit tests for missing-action and format-error scenarios in ninja_gen
  • Update Cucumber and rstest scenarios to verify ninja generation failure paths

Chores:

  • Remove deprecated diag-based helpers and migrate to anyhow context methods

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Sep 10, 2025

Reviewer's Guide

This PR standardizes error handling using anyhow and thiserror, introduces a structured NinjaGenError for ninja generation failures, updates the ninja_gen API and related call sites to return Result instead of panicking, and refactors the runner’s process logic into a dedicated module.

Sequence diagram for error propagation in ninja_gen::generate

sequenceDiagram
    participant Caller
    participant NinjaGen
    participant BuildGraph
    participant NinjaGenError
    Caller->>NinjaGen: generate(graph)
    NinjaGen->>BuildGraph: iterate build edges
    BuildGraph-->>NinjaGen: edge with action_id
    NinjaGen->>NinjaGenError: if action_id missing, return MissingAction
    NinjaGen-->>Caller: Result::Err(NinjaGenError::MissingAction)
    Note over NinjaGen,Caller: If all actions found, returns Ok(String)
Loading

Sequence diagram for error context propagation using anyhow

sequenceDiagram
    participant Main
    participant Manifest
    participant NinjaGen
    participant NinjaGenError
    Main->>Manifest: from_path(path)
    Manifest->>Manifest: from_str_named(yaml, name)
    Manifest->>NinjaGen: generate(graph)
    NinjaGen-->>Manifest: Result<String, NinjaGenError>
    Manifest-->>Main: Result with context (anyhow)
    Main-->>User: Display error chain with context
Loading

Class diagram for runner process module refactor

classDiagram
    class CommandArg {
        +new(arg: String)
        +as_str(): &str
    }
    class Process {
        +run_ninja(program: Path, cli: Cli, build_file: Path, targets: BuildTargets): io::Result<()>
        +contains_sensitive_keyword(arg: CommandArg): bool
        +is_sensitive_arg(arg: CommandArg): bool
        +redact_argument(arg: CommandArg): CommandArg
        +redact_sensitive_args(args: [CommandArg]): [CommandArg]
    }
    Process ..> CommandArg : uses
Loading

File-Level Changes

Change Details Files
Migrate to anyhow-based contextual error propagation
  • Replace .diag/.diag_with calls with .context()/.with_context()
  • Remove the custom ResultExt trait and miette-based panics
  • Convert error handling in runner, manifest, IR conversion, and Cucumber steps to use anyhow::Context
  • Update design docs, roadmap, and YAML error tests to reflect the new approach
src/runner.rs
src/manifest.rs
docs/netsuke-design.md
docs/roadmap.md
tests/steps/ir_steps.rs
tests/steps/ninja_steps.rs
tests/yaml_error_tests.rs
tests/cucumber.rs
Introduce NinjaGenError and update ninja_gen API
  • Define NinjaGenError enum with MissingAction and Format variants
  • Change generate to return Result<String, NinjaGenError> and add generate_into
  • Adjust call sites and Cucumber steps to handle Result
  • Add unit, integration, and negative-path tests for missing-action and format errors
src/ninja_gen.rs
tests/ninja_gen_tests.rs
tests/features/ninja.feature
Extract and streamline Ninja process invocation
  • Move CommandArg, redaction helpers, and run_ninja into src/runner/process.rs
  • Re-export process::run_ninja and process::doc in runner.rs
  • Remove duplicate definitions and doc wrappers from runner.rs
src/runner.rs
src/runner/process.rs

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 10, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • New Features

    • More actionable, structured error messages across manifest parsing, IR building, and Ninja generation.
    • Ninja generation now returns errors instead of panicking, including a clear message when a referenced action is missing.
    • Added option to write Ninja output to an external writer.
    • New helper to parse manifests directly from a string.
  • Documentation

    • Updated examples to use generate; clarified error behaviour and flows.
    • Netsuke design doc expanded around error handling.
    • Roadmap marks “Actionable Error Reporting” as complete.
    • Editorial/typographic improvements across docs.

Walkthrough

Shift error handling from miette to anyhow across core and tests, add NinjaGenError::MissingAction, change ninja_gen::generate to return Result, add generate_into, sort and deduplicate edges for deterministic output, update docs and tests to exercise the new missing-action error path.

Changes

Cohort / File(s) Summary
Ninja generation API & errors
src/ninja_gen.rs, tests/ninja_gen_tests.rs, tests/ninja_snapshot_tests.rs, tests/features/ninja.feature
Add public NinjaGenError (variants MissingAction { id }, Format); change generateResult<String, NinjaGenError>; add generate_into<W: Write>; replace unwraps/panics with ?/error returns; sort/dedup edges; update tests and add feature scenario for missing action.
Error-handling migration (anyhow)
src/manifest.rs, src/runner.rs, src/diagnostics.rs
Replace miette diagnostics with anyhow::Context/Result; add pub fn from_str(&str) -> Result<NetsukeManifest>; change from_path to return anyhow::Result; mark ResultExt trait with #[allow(dead_code, reason = "unused after migration to anyhow")].
Test harness adaptations
tests/ast_tests.rs, tests/yaml_error_tests.rs, tests/cucumber.rs, tests/steps/ir_steps.rs, tests/steps/ninja_steps.rs, tests/steps/*
Switch tests to anyhow; replace graphical YAML diagnostic rendering with Debug formatting; add CliWorld.ninja_error: Option<String>; add step to remove an action from the graph; capture ninja generation errors in ninja_error and add assertion step for failure messages.
Documentation updates
docs/netsuke-design.md, docs/snapshot-testing-in-netsuke-using-insta.md, docs/roadmap.md, docs/*
Document NinjaGenError::MissingAction; rename examples to ninja_gen::generate(...); mark “Actionable Error Reporting” checklist complete; minor editorial updates and formatting adjustments.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor CLI as CLI
  participant Runner as runner::generate_ninja
  participant Manifest as manifest::from_path / from_str
  participant Graph as BuildGraph::from_manifest
  participant NinjaGen as ninja_gen::generate
  participant Content as NinjaContent

  CLI->>Runner: generate_ninja()
  Runner->>Manifest: parse manifest
  Manifest-->>Runner: Result<NetsukeManifest>
  Runner->>Graph: build graph
  Graph-->>Runner: Result<BuildGraph>
  Runner->>NinjaGen: generate(&graph)
  alt Success
    NinjaGen-->>Runner: Ok(ninja_string)
    Runner->>Content: NinjaContent::new(ninja_string)
    Runner-->>CLI: Ok(NinjaContent)
  else MissingAction
    NinjaGen-->>Runner: Err(NinjaGenError::MissingAction{id})
    Runner-->>CLI: Err(context "generating Ninja file")
  else FormatError
    NinjaGen-->>Runner: Err(NinjaGenError::Format)
    Runner-->>CLI: Err(context "generating Ninja file")
  end
Loading
sequenceDiagram
  autonumber
  participant NG as ninja_gen::generate
  participant Actions as actions map
  participant Edges as edges (sorted, deduped)

  NG->>Edges: iterate edges
  loop for each edge
    NG->>Actions: lookup action by id
    alt Found
      Actions-->>NG: &Action
      NG->>NG: write rule/build/default (propagate fmt errors)
    else Missing
      Actions-->>NG: None
      NG-->>Caller: Err(NinjaGenError::MissingAction{id})
    end
  end
  NG-->>Caller: Ok(ninja_string)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • codescene-delta-analysis

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Poem

Edges march in sorted line, so tidy, calm, and clean,
A missing action waves its hand: “I’m not where I’ve been!”
Anyhow we thread the tale, with context clear and bright,
The Ninja whispers either way: “All’s well—or not tonight.” ✨
Snapshots nod, “Result received,” and tests applaud the write.

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed Affirm that the title succinctly summarises the primary change: adoption of structured errors for Ninja generation (introduction of NinjaGenError and changing generate to return Result). Keep the title concise and specific; it directly reflects the changeset and contains no extraneous detail.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Confirm that the PR description is directly related to the changeset: it documents migrating error propagation to anyhow, adding the NinjaGenError enum and a Result-returning ninja_gen API, and updating tests and documentation to exercise missing-action paths, and it aligns with the provided file-level summaries and PR objectives.

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
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 there - I've reviewed your changes and found some issues that need to be addressed.

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `tests/ninja_gen_tests.rs:338` </location>
<code_context>
+        always: false,
+    };
+    graph.targets.insert(PathBuf::from("out"), edge);
+    let err = generate(&graph).expect_err("missing action");
+    assert!(matches!(err, NinjaGenError::MissingAction { id } if id == "missing"));
+}
</code_context>

<issue_to_address>
Consider adding a test for the Format error variant of NinjaGenError.

Only the MissingAction error is tested. Please add a test that triggers and asserts the Format error variant, such as by simulating a formatting failure.

Suggested implementation:

```rust
    let err = generate(&graph).expect_err("missing action");
    assert!(matches!(err, NinjaGenError::MissingAction { id } if id == "missing"));
}

#[test]
fn generate_format_error() {
    use std::fmt;

    struct BadDisplay;
    impl fmt::Display for BadDisplay {
        fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
            Err(fmt::Error)
        }
    }

    // Simulate a graph/action that will cause a formatting error.
    // This assumes your generate function or related code will attempt to format something
    // and propagate the error as NinjaGenError::Format.
    // You may need to adjust this to fit your actual implementation.

    let mut graph = BuildGraph::default();
    let edge = BuildEdge {
        action_id: "bad_display".into(),
        inputs: Vec::new(),
        explicit_outputs: vec![PathBuf::from("out")],
        implicit_outputs: Vec::new(),
        order_only_deps: Vec::new(),
        phony: false,
        always: false,
    };
    graph.targets.insert(PathBuf::from("out"), edge);

    // Here, you would need to ensure that generate will try to format BadDisplay and fail.
    // For demonstration, let's assume generate will fail with Format error for this action_id.
    let err = generate(&graph).expect_err("formatting should fail");
    assert!(matches!(err, NinjaGenError::Format { .. }));
}

```

If your `generate` function does not currently attempt to format a type that can fail, you may need to:
1. Add logic to your implementation so that an action with id "bad_display" or similar will attempt to format a value of type `BadDisplay`.
2. Ensure that formatting errors are propagated as `NinjaGenError::Format`.

Adjust the test as needed to fit your actual error propagation and formatting logic.
</issue_to_address>

### Comment 2
<location> `src/ninja_gen.rs:45` </location>
<code_context>
-pub fn generate(graph: &BuildGraph) -> String {
+/// Returns [`NinjaGenError`] if a build edge references an unknown action or
+/// writing to the output fails.
+pub fn generate(graph: &BuildGraph) -> Result<String, NinjaGenError> {
     let mut out = String::new();

</code_context>

<issue_to_address>
Add unit tests for error cases in the new NinjaGenError enum, especially MissingAction and Format variants.

The new error handling in generate (returning Result and NinjaGenError) is not covered by unit tests for error cases. Add tests that verify MissingAction is returned when an unknown action is referenced, and that Format errors are handled.
</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.

Comment thread tests/ninja_gen_tests.rs
Comment thread src/ninja_gen.rs
Copy link
Copy Markdown
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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/diagnostics.rs (1)

16-18: Gate miette shims behind a feature flag
Verified no .diag(_with) or ResultExt usage outside src/diagnostics.rs. Add at the top of that file:

#![cfg(any(test, feature = "miette-compat"))]

Alternatively delete the entire module if compatibility isn’t required.

tests/yaml_error_tests.rs (1)

70-75: Prefer Display over Debug for stable assertions.

{err:?} output shape can change with backtraces and sources; {err} is typically more stable for text matching.

-    let msg = normalise_report(&format!("{err:?}"));
+    let msg = normalise_report(&format!("{err}"));

Run the suite to ensure the needles still match:

#!/bin/bash
set -euo pipefail
cargo test -p netsuke --tests yaml_diagnostics_are_actionable
docs/netsuke-design.md (1)

1292-1341: Clarify miette’s role vs anyhow in the current code.

The repo is migrating to anyhow for context; tests stopped using GraphicalReportHandler. Adjust Section 7.2 to state that miette rendering is optional/transitional and identify which code paths still emit miette-formatted diagnostics.

#!/bin/bash
# Grep for explicit miette usage to reflect reality in docs.
rg -n 'miette' -g '!**/target/**' -C2
src/runner.rs (1)

1-434: Split module to satisfy ≤400‑line rule and improve cohesion.

Extract the Ninja process plumbing and redaction helpers into runner/process.rs (or similar). Keep run(), CLI dispatch, and high-level generation orchestration here.

I can draft a minimal split (public API preserved) if desired.

src/manifest.rs (3)

39-41: Replace #[allow] with #[expect] per lint policy

Do not silence lints with #[allow]. Use a narrowly scoped #[expect(..., reason = "...")].

Apply:

-    #[allow(clippy::useless_conversion, reason = "future-proof span length type")]
+    #[expect(clippy::useless_conversion, reason = "future-proof span length type")]

1-2: Split this module; file length exceeds 400 lines

Decompose into focused submodules (e.g., diagnostics.rs, glob.rs, expand.rs, render.rs) to meet the 400‑line limit and improve cohesion.


145-150: Standardise en‑GB spelling in docs (“normalise” vs “normalize”)

Keep docs/comments aligned to en‑GB. Update occurrences in these doc comments.

Apply:

-/// Process a single glob entry and normalize its output.
+/// Process a single glob entry and normalise its output.
@@
-/// Requires matched paths to be valid UTF-8; output is normalized to use
+/// Requires matched paths to be valid UTF-8; output is normalised to use
@@
-    // Normalize separators so `/` and `\\` behave the same on all platforms.
+    // Normalise separators so `/` and `\\` behave the same on all platforms.

Also applies to: 498-506

tests/ast_tests.rs (1)

294-303: Rename test to en‑GB spelling

Use en‑GB in identifiers for consistency with guidelines.

Apply:

-fn actions_behavior(
+fn actions_behaviour(
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65af86d and 947015b.

📒 Files selected for processing (15)
  • docs/netsuke-design.md (7 hunks)
  • docs/roadmap.md (1 hunks)
  • docs/snapshot-testing-in-netsuke-using-insta.md (2 hunks)
  • src/diagnostics.rs (1 hunks)
  • src/manifest.rs (8 hunks)
  • src/ninja_gen.rs (4 hunks)
  • src/runner.rs (5 hunks)
  • tests/ast_tests.rs (1 hunks)
  • tests/cucumber.rs (1 hunks)
  • tests/features/ninja.feature (1 hunks)
  • tests/ninja_gen_tests.rs (5 hunks)
  • tests/ninja_snapshot_tests.rs (1 hunks)
  • tests/steps/ir_steps.rs (3 hunks)
  • tests/steps/ninja_steps.rs (2 hunks)
  • tests/yaml_error_tests.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Comment why, not what; document assumptions, edge cases, trade-offs, and complexity without restating obvious code
Extract reusable logic into functions; prefer composition and declarative constructs when readable
Keep functions small, single-responsibility, and observe command/query separation
Use precise, descriptive names; boolean names should start with is/has/should
Use en-GB-oxendict spelling/grammar in comments (except external API references)
Function docs must include clear usage examples; avoid redundant examples in test docs
No Rust source file may exceed 400 lines; split long switches/tables; move large test data to external files
Fix test warnings in code; do not silence them
Extract helpers when functions are too long; group many parameters into well-named structs
Consider using Arc when returning large errors to reduce data movement
Document public APIs using Rustdoc (///) so cargo doc can generate documentation
Prefer immutable data; avoid unnecessary mut bindings
Prefer Result-based error handling over panicking where feasible
Avoid unsafe code unless absolutely necessary and document any usage clearly
Place function attributes after doc comments
Do not use return in single-line functions
Use predicate helper functions when conditionals have more than two branches
Do not silence lints except as a last resort; suppressions must be tightly scoped and include a clear reason
Prefer #[expect(...)] over #[allow(...)] for lint management
Prefer .expect() over .unwrap()
Use conditional compilation (#[cfg]/#[cfg_attr]) for functions unused under specific feature sets
Use concat!() to join long string literals rather than using backslash-newline escapes
Prefer single-line function bodies where appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) })
Prefer semantic error enums deriving std::error::Error via thiserror for caller-inspectable conditions

Files:

  • tests/ast_tests.rs
  • tests/cucumber.rs
  • tests/ninja_snapshot_tests.rs
  • src/diagnostics.rs
  • tests/steps/ir_steps.rs
  • tests/steps/ninja_steps.rs
  • src/runner.rs
  • tests/ninja_gen_tests.rs
  • src/manifest.rs
  • tests/yaml_error_tests.rs
  • src/ninja_gen.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.

  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such

    • For testing of functionality depending upon environment variables, dependency injection and the mockable crate are the preferred option.
    • If mockable cannot be used, env mutations in tests ...

Files:

  • tests/ast_tests.rs
  • tests/cucumber.rs
  • tests/ninja_snapshot_tests.rs
  • src/diagnostics.rs
  • tests/steps/ir_steps.rs
  • tests/steps/ninja_steps.rs
  • src/runner.rs
  • tests/ninja_gen_tests.rs
  • src/manifest.rs
  • tests/yaml_error_tests.rs
  • src/ninja_gen.rs
tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.rs: Use rstest fixtures for shared setup and replace duplicated tests with #[rstest(...)] parameterised cases
Prefer mockall for mocks/stubs
Mock non-deterministic dependencies via dependency injection (e.g., Env/Clock traits) using mockable crate; follow docs/reliable-testing-in-rust-via-dependency-injection.md

Files:

  • tests/ast_tests.rs
  • tests/cucumber.rs
  • tests/ninja_snapshot_tests.rs
  • tests/steps/ir_steps.rs
  • tests/steps/ninja_steps.rs
  • tests/ninja_gen_tests.rs
  • tests/yaml_error_tests.rs
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/**/*.md: Treat docs/ markdown as the reference source of truth for requirements and decisions; keep updated with changes
Use en-GB-oxendict spelling/grammar in documentation (LICENSE name unchanged)

Files:

  • docs/roadmap.md
  • docs/snapshot-testing-in-netsuke-using-insta.md
  • docs/netsuke-design.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Validate Markdown with make markdownlint
Wrap Markdown paragraphs and bullet points at 80 columns; do not wrap tables or headings
Wrap code blocks in Markdown at 120 columns
Use dashes (-) for list bullets
Use GitHub-flavoured Markdown footnotes ([^1]) for references
Validate Mermaid diagrams in Markdown using make nixie

Files:

  • docs/roadmap.md
  • docs/snapshot-testing-in-netsuke-using-insta.md
  • docs/netsuke-design.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/roadmap.md
  • docs/snapshot-testing-in-netsuke-using-insta.md
  • docs/netsuke-design.md
docs/**/*.{rs,md}

📄 CodeRabbit inference engine (docs/rust-doctest-dry-guide.md)

In fenced code blocks for docs, explicitly mark code fences with rust (```rust) for clarity

Files:

  • docs/roadmap.md
  • docs/snapshot-testing-in-netsuke-using-insta.md
  • docs/netsuke-design.md
src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.rs: Every module must begin with a //! module-level comment explaining its purpose and utility
Never export eyre::Report from library code; convert to domain error enums at API boundaries

Files:

  • src/diagnostics.rs
  • src/runner.rs
  • src/manifest.rs
  • src/ninja_gen.rs
🧬 Code graph analysis (5)
tests/ninja_snapshot_tests.rs (1)
src/ninja_gen.rs (1)
  • generate (45-86)
tests/steps/ir_steps.rs (1)
src/ir.rs (1)
  • from_manifest (122-133)
tests/steps/ninja_steps.rs (1)
src/ninja_gen.rs (1)
  • generate (45-86)
src/runner.rs (2)
src/ir.rs (1)
  • from_manifest (122-133)
src/ninja_gen.rs (1)
  • generate (45-86)
tests/ninja_gen_tests.rs (1)
src/ninja_gen.rs (1)
  • generate (45-86)
🪛 GitHub Actions: CI
tests/steps/ir_steps.rs

[warning] 1-1: Rust fmt diff detected in netsuke/tests/steps/ir_steps.rs:1. Run 'cargo fmt' to fix formatting.


[warning] 51-51: Rust fmt diff detected in netsuke/tests/steps/ir_steps.rs:51. Run 'cargo fmt' to fix formatting.

src/manifest.rs

[warning] 549-556: Rust fmt diff detected in netsuke/src/manifest.rs. Run 'cargo fmt' to fix formatting.

🪛 GitHub Check: build-test (1.89.0)
src/manifest.rs

[warning] 549-549:
Diff in /home/runner/work/netsuke/netsuke/src/manifest.rs

🪛 GitHub Check: build-test (stable)
src/manifest.rs

[warning] 549-549:
Diff in /home/runner/work/netsuke/netsuke/src/manifest.rs

🔇 Additional comments (24)
docs/netsuke-design.md (1)

1349-1352: Good: Document NinjaGenError::MissingAction as an actionable generation error.

The doc matches the new generator API and makes the failure mode explicit.

tests/steps/ir_steps.rs (3)

5-5: LGTM: switch to anyhow::Context.

The step now composes error context consistently with the rest of the PR.


53-58: LGTM: propagate crisp context for IR build failures.

with_context(|| format!(...)) provides actionable path info.


86-91: Fix rustfmt warnings.

The CI warns about formatting at Line 1 and Line 51. Run cargo fmt.

#!/bin/bash
set -euo pipefail
cargo fmt --all
git diff --name-only --exit-code || (echo "Unformatted files remain"; exit 1)
src/runner.rs (4)

9-9: LGTM: unify on anyhow::{Context, Result}.

Imports align with the migration and simplify error flow.


193-202: LGTM: surface temp-file creation errors via context.

This keeps failure points obvious.


215-226: LGTM: add precise write paths to error messages.

The with_context closures make file I/O failures actionable.


255-257: LGTM: handle ninja_gen::generate fallibly and return NinjaContent.

This matches the new NinjaGenError path.

src/manifest.rs (1)

549-556: Fix rustfmt drift flagged by CI

Format the file to clear the pipeline warning.

Run:

#!/bin/bash
set -euo pipefail
cargo fmt --all -- --check
tests/ast_tests.rs (1)

3-3: LGTM: switch tests to anyhow::Result

The import aligns tests with the new anyhow-based API.

tests/features/ninja.feature (1)

25-29: LGTM: behaviour scenario covers actionable error path

The new scenario exercises the MissingAction path without panicking.

tests/ninja_snapshot_tests.rs (1)

43-43: LGTM!

The test correctly handles the new Result return type from ninja_gen::generate.

docs/roadmap.md (1)

141-154: Excellent completion of Phase 3 error reporting!

The actionable error reporting phase has been successfully completed with comprehensive implementation of structured error types and contextual error propagation. The adoption of anyhow and thiserror provides a robust foundation for clear, actionable error messages throughout the codebase.

tests/steps/ninja_steps.rs (2)

13-22: Well-structured error handling

The match expression properly handles both success and error cases from the new Result-based API, maintaining clear separation between the ninja content and error state.


71-82: Comprehensive error assertion

The new test step provides good coverage for verifying error messages when Ninja generation fails, completing the error path testing.

tests/ninja_gen_tests.rs (3)

11-11: Good public API exposure

Explicitly importing NinjaGenError alongside generate makes the error type part of the public API, enabling proper error handling by consumers.


134-135: Consistent error handling across tests

All test calls to generate consistently use .expect("generate ninja") to handle the Result type, maintaining uniform error handling throughout the test suite.

Also applies to: 141-142, 175-176, 302-303


325-340: Excellent error path test coverage

The test thoroughly validates the MissingAction error case, ensuring the error type and message are correct when an edge references a non-existent action.

src/ninja_gen.rs (6)

13-21: Well-designed error type

The NinjaGenError enum provides clear, actionable error variants with descriptive messages. The use of thiserror for deriving Error and automatic conversion from fmt::Error follows best practices.


39-45: Clear API documentation

The function documentation properly describes the error conditions and return type, making the API contract explicit for consumers.


54-61: Effective deduplication logic

The edge sorting and deduplication using path_key ensures deterministic output whilst preventing duplicate build statements. The implementation correctly maintains insertion order for the first occurrence.


62-68: Robust error handling for missing actions

The explicit check for missing actions with a descriptive error message provides clear feedback when the build graph is inconsistent. The error includes the action ID for easy debugging.


51-51: Consistent error propagation

All write operations properly use the ? operator for error propagation, ensuring formatting errors are captured and returned as NinjaGenError::Format.

Also applies to: 76-76, 82-82


211-211: Test properly updated for new API

The test correctly handles the Result return type with appropriate error expectation.

Comment thread docs/snapshot-testing-in-netsuke-using-insta.md Outdated
Comment thread docs/snapshot-testing-in-netsuke-using-insta.md Outdated
Comment thread src/diagnostics.rs Outdated
Comment thread src/manifest.rs Outdated
Comment thread src/runner.rs
Comment thread src/runner.rs
Comment thread tests/cucumber.rs
Comment thread tests/features/ninja.feature Outdated
Comment thread tests/steps/ir_steps.rs Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 11, 2025

@coderabbitai Re:

1292-1341: Clarify miette’s role vs anyhow in the current code.

The repo is migrating to anyhow for context; tests stopped using GraphicalReportHandler. Adjust Section 7.2 to state that miette rendering is optional/transitional and identify which code paths still emit miette-formatted diagnostics.

Can we make it clear that miette must be used for presentation of YAML / Jinja diagnostics?

Please suggest an AI prompt to achieve this

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 11, 2025

Tip

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

🧩 Analysis chain

@leynos Based on my analysis of the codebase, I can see that while the PR migrates most error handling from miette to anyhow, there are still specific areas where miette diagnostics are essential for presenting structured error information, particularly for YAML parsing errors.

Here's an AI prompt you can use to clarify miette's continued role for YAML/Jinja diagnostics:

Update the error handling documentation in Section 7.2 to clarify the hybrid approach:

1. **anyhow for context propagation**: Used throughout the application for adding contextual information to errors as they bubble up the call stack
2. **miette for structured diagnostics**: Reserved specifically for presenting user-facing diagnostic information, particularly:
   - YAML parsing errors with source location and syntax highlighting
   - Jinja template rendering errors with context and line numbers
   - Any error that benefits from rich, formatted output with source snippets

Make it clear that:
- `anyhow::Context` is used for internal error propagation and context building
- `miette` diagnostics are the final presentation layer for errors that need rich formatting
- The `YamlDiagnostic` type and similar structured error types should continue using miette for user presentation
- This creates a clear separation: anyhow for developers (context), miette for users (presentation)

Add examples showing how YAML parsing errors flow from anyhow context through to miette presentation, and explain why this hybrid approach provides both good developer experience (contextual errors) and good user experience (formatted diagnostics).

This prompt will help ensure the documentation clearly establishes miette's ongoing role in diagnostic presentation while acknowledging anyhow's role in error context management.


🏁 Script executed:

#!/bin/bash
# Find references to miette and anyhow in the codebase to understand current usage patterns
echo "=== Current miette usage ==="
rg -n "miette" --type rust -C2

echo -e "\n=== Current anyhow usage ==="
rg -n "anyhow" --type rust -C2

echo -e "\n=== YAML diagnostic related code ==="
rg -n -i "yaml.*diagnostic\|diagnostic.*yaml" --type rust -C3

echo -e "\n=== Jinja diagnostic related code ==="
rg -n -i "jinja.*diagnostic\|diagnostic.*jinja" --type rust -C3

Length of output: 4541


🏁 Script executed:

#!/bin/bash
# Look for documentation files and find Section 7.2
echo "=== Finding documentation files ==="
fd -e md | head -10

echo -e "\n=== Looking for Section 7.2 in docs ==="
rg -n "7\.2\|Section 7\.2" docs/ || echo "Section 7.2 not found with exact match"

echo -e "\n=== Looking for error handling sections in design docs ==="
rg -n -i "error.*handling\|miette\|anyhow" docs/ -A3 -B3

Length of output: 472


🏁 Script executed:

#!/bin/bash
# Look for YAML error handling and diagnostic code specifically
echo "=== YAML error handling patterns ==="
rg -n "YamlDiagnostic\|yaml.*error\|parse.*yaml" --type rust -C3

echo -e "\n=== Template/Jinja error handling ==="
rg -n "template.*error\|jinja.*error\|render.*error" --type rust -C3

echo -e "\n=== Looking at manifest.rs for YAML handling ==="
cat src/manifest.rs | grep -n -A5 -B5 "YamlDiagnostic\|miette\|anyhow"

Length of output: 3083


Based on my analysis of the codebase, I can now provide you with a more targeted AI prompt. I can see that the code already has a good foundation with YamlDiagnostic using miette's Diagnostic trait for structured error presentation, while anyhow handles context propagation.

Here's an AI prompt tailored to your actual codebase structure:

Update the error handling documentation to clarify the mandatory role of miette for diagnostic presentation:

**Key Points to Emphasize:**

1. **Two-tier error architecture**: 
   - `anyhow` for internal error context and propagation
   - `miette` for user-facing diagnostic presentation (NOT optional)

2. **YamlDiagnostic as the canonical pattern**: 
   - Show how `YamlDiagnostic` implements `miette::Diagnostic` with source code highlighting, spans, and help text
   - Explain how `ManifestError::Parse` wraps `YamlDiagnostic` to preserve rich diagnostic information
   - This pattern MUST be followed for all user-facing errors that benefit from source context

3. **Specific use cases where miette is required**:
   - YAML parsing errors (already implemented via `YamlDiagnostic`)
   - Jinja template rendering errors with line numbers and context
   - Any error where users need to see source code snippets, spans, or structured help

4. **Migration guidance**:
   - The `diagnostics.rs` module is marked as dead code but the miette patterns it demonstrates are still relevant
   - New diagnostic types should follow the `YamlDiagnostic` model: implement `Diagnostic`, provide `NamedSource`, `SourceSpan`, and helpful error messages

5. **Clear separation of concerns**:
   - Use `anyhow::Context` for adding context as errors bubble up
   - Use `miette::Diagnostic` for final presentation to users
   - Never lose diagnostic information by converting miette errors to plain anyhow errors

Make it explicit that miette is not transitional or optional for user-facing diagnostics - it's a core requirement for providing quality error messages with source context.

This prompt will help ensure your documentation clearly establishes that miette is essential for user-facing error presentation, especially following the YamlDiagnostic pattern you've already established.

🐰 ears perked up with curiosity, having learned something valuable about error handling patterns


✏️ Learnings added
Learnt from: leynos
URL: https://github.com/leynos/netsuke/pull/152

Timestamp: 2025-09-11T13:55:59.831Z
Learning: In the netsuke codebase, miette must be used for user-facing diagnostic presentation, particularly for YAML and Jinja errors. The YamlDiagnostic type serves as the canonical pattern, implementing miette::Diagnostic with source code highlighting, spans, and help text. This is wrapped by ManifestError::Parse to preserve rich diagnostic information. The architecture uses anyhow for internal error context propagation and miette for structured user-facing error presentation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/manifest.rs (2)

1-1: Split this module; file exceeds 400-line limit.

Decompose into focused submodules (e.g., yaml_diag.rs, jinja_helpers.rs, glob.rs) to meet the 400‑line cap and lower complexity.

Say the word and a patch splitting out:

  • YAML diagnostics (YamlDiagnostic, map_yaml_error, to_span, hints)
  • Jinja helpers (env_var, render_*, eval_expression)
  • Glob normalisation/validation
    will be generated with updated mod wiring and visibility.

39-41: Replace forbidden #[allow] with #[expect].

Replace the attribute in src/manifest.rs; replace remaining #[allow(...)] occurrences in tests/steps/ninja_steps.rs (lines 25, 38, 62, 71) and src/diagnostics.rs (line 32) with narrowly scoped #[expect(...)] and include a reason.

File: src/manifest.rs (lines 39-41)

-    #[allow(clippy::useless_conversion, reason = "future-proof span length type")]
+    #[expect(clippy::useless_conversion, reason = "future-proof span length type")]
     SourceSpan::new(start.into(), len.into())
♻️ Duplicate comments (4)
tests/steps/ir_steps.rs (1)

91-97: Fix nondeterministic action removal in step.

HashMap iteration order is nondeterministic; delete the action referenced by the first target to produce stable failures. This was flagged previously.

Apply:

 fn remove_action(world: &mut CliWorld) {
     let graph = world.build_graph.as_mut().expect("graph");
-    if let Some(id) = graph.actions.keys().next().cloned() {
-        graph.actions.remove(&id);
-    }
+    let first_action = graph
+        .targets
+        .values()
+        .next()
+        .map(|t| t.action_id.clone());
+    if let Some(id) = first_action {
+        graph.actions.remove(&id);
+    }
 }
src/manifest.rs (1)

531-533: Unify YAML parse errors under ManifestError::Parse.

Return the same domain error for both YAML decoding phases to keep caller-inspectable errors stable. This was requested earlier and remains unaddressed.

-    let mut doc: YamlValue =
-        serde_yml::from_str(yaml).map_err(|e| map_yaml_error(e, yaml, name))?;
+    let mut doc: YamlValue = serde_yml::from_str(yaml).map_err(|e| ManifestError::Parse {
+        source: map_yaml_error(e, yaml, name),
+    })?;

Also applies to: 552-556

tests/ninja_gen_tests.rs (2)

324-340: Cover MissingAction variant with a focused unit test.

Exercise NinjaGenError::MissingAction with precise pattern matching. This addresses prior feedback to test error cases.


342-379: Exercise Format variant by forcing a writer failure.

Inject a failing fmt::Write to assert NinjaGenError::Format. This directly satisfies the earlier request to add a formatting error test.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 947015b and 80cfd9b.

📒 Files selected for processing (5)
  • docs/srgn.md (3 hunks)
  • src/manifest.rs (8 hunks)
  • src/ninja_gen.rs (4 hunks)
  • tests/ninja_gen_tests.rs (5 hunks)
  • tests/steps/ir_steps.rs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Use en-GB-oxendict spelling and grammar in code comments (exceptions allowed for external API names)
Function documentation must include clear examples; test documentation should omit redundant examples
Keep file size manageable: no Rust source file longer than 400 lines; split large switches/dispatch tables and move big test data to external files
Disallow Clippy warnings; fix warnings in code rather than silencing them
Extract helper functions for long functions; maintain separation of concerns and CQRS
Group many related parameters into meaningful structs
If returning a large error type, consider using Arc to reduce data copied/returned
Each Rust module must begin with a module-level //! comment stating purpose and utility
Document public APIs with Rustdoc (///) so cargo doc can generate documentation
Prefer immutable data; avoid unnecessary mut bindings
Handle errors with Result instead of panicking where feasible
Avoid unsafe code unless absolutely necessary and document any usage clearly
Place function attributes after doc comments
Do not use return in single-line functions
Use predicate functions when conditional criteria have more than two branches
Do not silence lints except as a last resort; suppressions must be tightly scoped and include a clear reason
Prefer expect over allow for lint management
Use conditional compilation (#[cfg]/#[cfg_attr]) for functions unused under specific feature sets
Prefer .expect() over .unwrap()
Use concat!() for long string literals instead of escaping newlines with backslashes
Prefer single-line function bodies when appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) })
Prefer semantic error enums (derive std::error::Error via thiserror) for inspectable conditions
Use an opaque eyre::Report only at the application boundary for human-readable logs; do not expose in public APIs
Never export eyre::Report from libraries; convert to domain error enums at API boundaries and to eyre only in main/top-level async tas...

Files:

  • src/ninja_gen.rs
  • src/manifest.rs
  • tests/ninja_gen_tests.rs
  • tests/steps/ir_steps.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.

  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such

    • For testing of functionality depending upon environment variables, dependency injection and the mockable crate are the preferred option.
    • If mockable cannot be used, env mutations in tests ...

Files:

  • src/ninja_gen.rs
  • src/manifest.rs
  • tests/ninja_gen_tests.rs
  • tests/steps/ir_steps.rs
tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.rs: Write unit and behavioural tests for new functionality; run before and after changes
Use rstest fixtures for shared setup and replace duplicated tests with #[rstest(...)] parameterised cases
Prefer mockall for mocks/stubs
Mock non-deterministic dependencies via dependency injection using mockable (traits like Env and Clock) where appropriate; follow docs/reliable-testing-in-rust-via-dependency-injection.md

Files:

  • tests/ninja_gen_tests.rs
  • tests/steps/ir_steps.rs
docs/**/*.{rs,md}

📄 CodeRabbit inference engine (docs/rust-doctest-dry-guide.md)

In fenced code blocks for docs, explicitly mark code fences with rust (```rust) for clarity

Files:

  • docs/srgn.md
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/**/*.md: Use docs/ as the knowledge base and source of truth for requirements, dependencies, and architecture
Proactively update docs/ when decisions, requirements, libraries, or architecture change to keep documentation current
Documentation must use en-GB-oxendict spelling and grammar (LICENSE name unchanged)

Files:

  • docs/srgn.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Validate Markdown with make markdownlint
Run make fmt after documentation changes to format Markdown and fix table markup
Validate Mermaid diagrams in Markdown files using make nixie
Wrap Markdown paragraphs and bullet points at 80 columns; code blocks at 120; do not wrap tables or headings
Use dashes (-) for list bullets in Markdown
Use GitHub-flavoured Markdown footnotes ([^1]) for references

Files:

  • docs/srgn.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/srgn.md
🧬 Code graph analysis (2)
tests/ninja_gen_tests.rs (1)
src/ninja_gen.rs (4)
  • generate (45-49)
  • generate_into (56-95)
  • fmt (133-160)
  • fmt (170-185)
tests/steps/ir_steps.rs (1)
src/ir.rs (1)
  • from_manifest (122-133)
🔍 Remote MCP Deepwiki

Based on my search of the netsuke repository, I can see this is a pull request that introduces structured error handling with anyhow and a NinjaGenError type. Let me provide a comprehensive summary of the relevant context I've found:

Technical Context for PR Review

Error Handling Migration

This PR implements a migration from miette-heavy error propagation to anyhow for contextual error handling throughout the codebase. The key changes include:

  • Structured Error Types: Introduction of NinjaGenError with variants MissingAction and Format for actionable Ninja generation failures
  • API Changes: The ninja_gen::generate() function now returns Result<String, NinjaGenError> instead of panicking, with a new generate_into() function for writing to external writers
  • Error Context Migration: Replacement of custom ResultExt trait and .diag() methods with standard anyhow::Context and .with_context() throughout runner, manifest, and testing modules

Security and Command Handling

The codebase implements significant security measures:

  • Automatic Shell Escaping: All command interpolation uses the shell-quote crate to prevent command injection attacks
  • Template Sandboxing: Jinja templates are processed in a controlled environment with whitelisted functions (env(), glob())
  • Command Validation: Generated commands are validated with shlex::split() before output

Architecture and Testing

The system follows a five-stage compilation pipeline:

  1. Manifest ingestion
  2. Jinja template evaluation
  3. YAML parsing into AST
  4. IR generation and validation
  5. Ninja synthesis and execution

The testing strategy uses three frameworks:

  • rstest: Fixture-based unit testing
  • cucumber-rs: BDD integration testing
  • insta: Snapshot testing for IR and Ninja output validation

Critical Review Points

Error Handling Strategy: The migration maintains a two-tier architecture where miette remains essential for user-facing diagnostic presentation (especially for YAML and Jinja errors) while anyhow handles internal context propagation. This separation ensures rich diagnostics aren't lost.

API Breaking Changes: The ninja_gen::generate function signature change from returning String to Result<String, NinjaGenError> is a breaking change that requires updates to all calling code, which the PR addresses comprehensively.

Test Coverage: The PR includes extensive test updates, including new Cucumber scenarios for missing action error paths and proper handling of the new Result-returning APIs in snapshot tests.

Documentation Updates: The roadmap is updated to reflect completion of the "Actionable Error Reporting" phase, and design documentation is updated to reflect the new error handling approach.

This appears to be a well-structured refactoring that improves error handling ergonomics while maintaining the system's security guarantees and deterministic build properties.

🔇 Additional comments (14)
tests/steps/ir_steps.rs (3)

4-4: LGTM: adopt anyhow::Context in steps.

Import the correct trait for .context(...) and .with_context(...).


54-56: LGTM: context layering is correct.

Chain from_path(...).and_then(...).with_context(...) to keep a single error type and preserve context.


87-88: LGTM: assertion message formatting.

The trailing comma is fine and keeps rustfmt happy.

src/manifest.rs (1)

766-771: LGTM: file IO context message.

Surface a precise path in the error; .with_context usage is correct.

tests/ninja_gen_tests.rs (5)

11-11: Adopt the new public API in tests.

Import NinjaGenError, generate, and generate_into to align tests with the Result-returning API. LGTM.


134-136: Handle Result explicitly in unit tests.

Use .expect("generate ninja") to fail loudly on generation errors. LGTM.


141-143: Assert empty output for empty graph via Result-aware call.

The change to .expect("generate ninja") is correct and consistent.


175-188: Snapshot the multiline script output via Result-aware call.

Use of .expect("generate ninja") and snapshot assertions is correct. LGTM.


302-303: Write generated Ninja to file after explicit Result handling.

Call .expect("generate ninja") before persisting to disk. LGTM.

src/ninja_gen.rs (5)

15-21: Introduce a domain error type for Ninja generation.

Define NinjaGenError with MissingAction and Format(#[from] fmt::Error). Error messages are actionable and integrate cleanly with ?. LGTM.


60-61: Propagate formatting failures via Format variant.

Use ? on write! for NamedAction to surface fmt::Error as NinjaGenError::Format. LGTM.


71-77: Return MissingAction instead of panicking.

Replace implicit unwrap with an explicit, inspectable error. This aligns with the structured error strategy. LGTM.


91-91: Propagate write failure when emitting defaults.

Use writeln! with ? to keep error flow consistent. LGTM.


220-228: Update internal unit test to the Result-returning API.

Use .expect("generate ninja") and compare against the expected string. LGTM.

Comment thread docs/srgn.md Outdated
Comment thread src/ninja_gen.rs
Comment thread src/ninja_gen.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 12, 2025

@coderabbitai please create new GitHub issues for the following:

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 12, 2025

Tip

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

@leynos I'd be happy to create GitHub issues for you! However, your comment appears to be incomplete - it ends with "for the following:" but doesn't specify what items you'd like me to create issues for.

Could you please provide the specific topics, problems, or tasks you'd like me to create GitHub issues for? Once you share those details, I'll create the issues using the GitHub CLI with appropriate titles and descriptions.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 12, 2025

@coderabbitai please create a new GitHub issue for the following:

src/runner.rs (1)

1-434: Split module to satisfy ≤400‑line rule and improve cohesion.

Extract the Ninja process plumbing and redaction helpers into runner/process.rs (or similar). Keep run(), CLI dispatch, and high-level generation orchestration here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 12, 2025

Tip

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

🧩 Analysis chain

@leynos I'll create a GitHub issue for the runner.rs refactoring task.

The GitHub issue has been created to track the refactoring of src/runner.rs into smaller, more cohesive modules while maintaining the ≤400-line rule.


🏁 Script executed:

#!/bin/bash
gh issue create \
  --title "Refactor src/runner.rs: Split module to satisfy ≤400-line rule" \
  --body "## Overview

The \`src/runner.rs\` module currently contains 434 lines, exceeding the project's ≤400-line rule. This issue tracks the refactoring needed to split this module and improve code cohesion.

## Proposed Changes

### Extract to \`runner/process.rs\` (or similar):
- Ninja process plumbing functionality
- Redaction helpers
- Process-related utilities

### Keep in \`src/runner.rs\`:
- \`run()\` function
- CLI dispatch logic  
- High-level generation orchestration

## Benefits
- Improved code organization and cohesion
- Adherence to project line count standards
- Better separation of concerns between orchestration and process management

## Context
This refactoring was identified during code review of PR #152.

**Reference:** https://github.com/leynos/netsuke/pull/152" \
  --assignee leynos

Length of output: 44


Perfect! I've successfully created GitHub issue #155 for refactoring src/runner.rs. The issue includes:

  • A clear overview of the problem (434 lines exceeding the ≤400-line rule)
  • Detailed proposed changes specifying what to extract and what to keep
  • Benefits of the refactoring
  • Reference back to PR Adopt structured errors for Ninja generation #152 where this was identified
  • Assignment to you (@leynos)

The issue is now available at: #155

🐰 satisfied tail wiggle after successfully organizing the refactoring task

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 12, 2025

@coderabbitai "normalize" is correct for en-GB-oxendict as used in this project:

"""
145-150: Standardise en‑GB spelling in docs (“normalise” vs “normalize”)

Keep docs/comments aligned to en‑GB. Update occurrences in these doc comments.

Apply:

-/// Process a single glob entry and normalize its output.
+/// Process a single glob entry and normalise its output.
@@
-/// Requires matched paths to be valid UTF-8; output is normalized to use
+/// Requires matched paths to be valid UTF-8; output is normalised to use
@@
-    // Normalize separators so `/` and `\\` behave the same on all platforms.
+    // Normalise separators so `/` and `\\` behave the same on all platforms.

Also applies to: 498-506

"""

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 12, 2025

Tip

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

@leynos Thank you for the correction! I appreciate you clarifying that "normalize" is indeed the correct spelling for this project's en-GB-oxendict standard. I'll make sure to follow the project's established spelling conventions going forward.

🐰 ears drooping slightly in acknowledgment of the correction, then perking up with newfound understanding


✏️ Learnings added
Learnt from: leynos
URL: https://github.com/leynos/netsuke/pull/152

Timestamp: 2025-09-12T01:53:50.746Z
Learning: In the netsuke project, "normalize" (with 'z') is the correct en-GB-oxendict spelling, not "normalise" (with 's'). The project follows en-GB-oxendict spelling conventions where "normalize" is preferred.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 12, 2025

@coderabbitai please create a new GitHub issue for the following:

src/manifest.rs:

1-2: Split this module; file length exceeds 400 lines

Decompose into focused submodules (e.g., diagnostics.rs, glob.rs, expand.rs, render.rs) to meet the 400‑line limit and improve cohesion.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 12, 2025

Tip

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

🧩 Analysis chain

@leynos I'll create a GitHub issue for refactoring src/manifest.rs to split it into smaller, focused submodules.

🐰 focused concentration while organizing the module splitting task


🏁 Script executed:

#!/bin/bash
gh issue create \
  --title "Refactor src/manifest.rs: Split module to satisfy ≤400-line rule" \
  --body "## Overview

The \`src/manifest.rs\` module currently exceeds the project's ≤400-line rule and needs to be decomposed into smaller, focused submodules to improve code organization and cohesion.

## Proposed Changes

### Suggested submodule decomposition:
- **\`manifest/diagnostics.rs\`** - Error handling and diagnostic types
- **\`manifest/glob.rs\`** - Glob pattern processing and path utilities  
- **\`manifest/expand.rs\`** - Manifest expansion logic
- **\`manifest/render.rs\`** - Template rendering functionality

### Keep in \`src/manifest.rs\`:
- Main manifest structure definitions
- Public API surface
- Module re-exports and orchestration

## Benefits
- Adherence to project's ≤400-line standard
- Improved separation of concerns
- Better code organization and maintainability
- Enhanced testability of individual components

## Context
This refactoring requirement was identified during code review of PR #152.

**Reference:** https://github.com/leynos/netsuke/pull/152" \
  --assignee leynos

Length of output: 44


Perfect! I've successfully created GitHub issue #156 for refactoring src/manifest.rs. The issue includes:

  • Clear overview of the problem (module exceeding 400-line rule)
  • Detailed proposed submodule decomposition:
    • manifest/diagnostics.rs for error handling
    • manifest/glob.rs for glob pattern processing
    • manifest/expand.rs for manifest expansion logic
    • manifest/render.rs for template rendering
  • Benefits of the refactoring including improved separation of concerns
  • Reference back to PR Adopt structured errors for Ninja generation #152 where this was identified
  • Assignment to you (@leynos)

The issue is now available at: #156

🐰 pleased ear twitch after successfully organizing another module refactoring task

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 12, 2025

@coderabbitai pause

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 12, 2025

✅ Actions performed

Reviews paused.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 13, 2025

@coderabbitai new GitHub issue, please:

src/manifest.rs (2)

1-1: Split this module; file exceeds 400-line limit.

Decompose into focused submodules (e.g., yaml_diag.rs, jinja_helpers.rs, glob.rs) to meet the 400‑line cap and lower complexity.

    YAML diagnostics (YamlDiagnostic, map_yaml_error, to_span, hints)
    Jinja helpers (env_var, render_*, eval_expression)
    Glob normalisation/validation

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 13, 2025

Tip

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

🧩 Analysis chain

@leynos I'll create a new GitHub issue for refactoring src/manifest.rs with the specific submodule breakdown you've outlined.


🏁 Script executed:

#!/bin/bash
gh issue create \
  --title "Refactor src/manifest.rs: Split module into focused submodules (≤400-line limit)" \
  --body "## Overview

The \`src/manifest.rs\` module currently exceeds the project's ≤400-line limit and needs to be decomposed into focused submodules to reduce complexity and improve code organization.

## Proposed Submodule Decomposition

### \`manifest/yaml_diag.rs\`
- \`YamlDiagnostic\` type and implementation
- \`map_yaml_error\` function
- \`to_span\` utility
- Error hints and diagnostic helpers

### \`manifest/jinja_helpers.rs\`
- \`env_var\` function
- \`render_*\` functions (template rendering utilities)
- \`eval_expression\` function
- Template processing helpers

### \`manifest/glob.rs\`
- Glob normalisation utilities
- Path validation logic
- Glob pattern processing

### Keep in \`src/manifest.rs\`
- Main manifest structure definitions
- Public API surface
- Module re-exports and coordination

## Benefits
- Adherence to project's ≤400-line standard
- Reduced module complexity
- Improved separation of concerns by functionality
- Enhanced maintainability and testability
- Clearer organization of YAML diagnostics, Jinja processing, and glob handling

## Context
This refactoring requirement was identified during code review of PR #152.

**Reference:** https://github.com/leynos/netsuke/pull/152" \
  --assignee leynos

Length of output: 44


Perfect! I've successfully created GitHub issue #157 for refactoring src/manifest.rs with your specific submodule breakdown. The issue includes:

  • Clear overview of the 400-line limit problem
  • Detailed submodule decomposition exactly as you specified:
    • manifest/yaml_diag.rs for YAML diagnostics (YamlDiagnostic, map_yaml_error, to_span, hints)
    • manifest/jinja_helpers.rs for Jinja helpers (env_var, render_*, eval_expression)
    • manifest/glob.rs for glob normalisation/validation
  • Benefits highlighting reduced complexity and improved organization
  • Context linking back to PR Adopt structured errors for Ninja generation #152
  • Assignment to you (@leynos)

The issue is now available at: #157

🐰 satisfied ear twitch after precisely organizing the module refactoring with your exact specifications

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Sep 13, 2025

@sourcery-ai review

Copy link
Copy Markdown
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 there - I've reviewed your changes and found some issues that need to be addressed.

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/manifest.rs:533` </location>
<code_context>
 ///
 /// Returns an error if YAML parsing or Jinja evaluation fails.
 fn from_str_named(yaml: &str, name: &str) -> Result<NetsukeManifest> {
-    let mut doc: YamlValue =
-        serde_yml::from_str(yaml).map_err(|e| Report::new(map_yaml_error(e, yaml, name)))?;
+    let mut doc: YamlValue = serde_yml::from_str(yaml).map_err(|e| ManifestError::Parse {
+        source: map_yaml_error(e, yaml, name),
+    })?;
</code_context>

<issue_to_address>
Error mapping now uses ManifestError::Parse, but map_yaml_error may return a type not compatible with ManifestError.

Ensure that map_yaml_error returns a type compatible with ManifestError::Parse's source field to avoid type mismatches and preserve error details.

Suggested implementation:

```rust
fn from_str_named(yaml: &str, name: &str) -> Result<NetsukeManifest> {
    let mut doc: YamlValue = serde_yml::from_str(yaml).map_err(|e| ManifestError::Parse {
        source: map_yaml_error(e, yaml, name),
    })?;

```

```rust
        source: map_yaml_error(e, yaml, name),

```

```rust
use std::error::Error;
use std::sync::Arc;

// Update map_yaml_error to return a boxed error compatible with ManifestError::Parse
fn map_yaml_error(e: serde_yml::Error, yaml: &str, name: &str) -> Box<dyn Error + Send + Sync> {
    // You can wrap the error with context using anyhow or custom error type
    // For example, using anyhow:
    use anyhow::{anyhow, Context};
    let msg = format!("YAML parse error in '{}': {}", name, e);
    anyhow!(msg).context(format!("YAML content:\n{}", yaml)).into()
}

```

- You may need to update the definition of `ManifestError::Parse` to ensure its `source` field is of type `Box<dyn Error + Send + Sync>`, or another compatible type.
- If other usages of `map_yaml_error` exist, update them to handle the new return type.
- If you use a custom error type instead of `anyhow`, adjust the implementation accordingly.
</issue_to_address>

### Comment 2
<location> `src/runner/process.rs:130` </location>
<code_context>
-/// # Panics
-///
-/// Panics if the child's output streams cannot be captured.
-pub fn run_ninja(
-    program: &Path,
-    cli: &Cli,
</code_context>

<issue_to_address>
Consider refactoring run_ninja into three focused helper functions to simplify its structure and improve readability.

```markdown
You can dramatically reduce the cognitive load in `run_ninja` by pulling out three focused helpers:

1. **Command‐builder** – assemble args, cwd, jobs, build file  
2. **Command‐logger** – redact & log the final invocation  
3. **Output‐streamer** – spawn threads to pump `stdout`/`stderr`, and return when both finish  

Then `run_ninja` becomes a simple orchestrator:

```rust
pub fn run_ninja(
    program: &Path,
    cli: &Cli,
    build_file: &Path,
    targets: &BuildTargets<'_>,
) -> io::Result<()> {
    let mut cmd = build_ninja_command(program, cli, build_file, targets)?;
    log_command(&cmd);

    let mut child = cmd.spawn()?;
    let (out_handle, err_handle) = stream_output(child.stdout.take().unwrap(), child.stderr.take().unwrap());

    let status = child.wait()?;
    out_handle.join().ok();
    err_handle.join().ok();

    if status.success() {
        Ok(())
    } else {
        Err(io::Error::new(
            io::ErrorKind::Other,
            format!("ninja exited with {status}"),
        ))
    }
}
```

And the helpers might look like:

```rust
fn build_ninja_command(
    program: &Path,
    cli: &Cli,
    build_file: &Path,
    targets: &BuildTargets<'_>,
) -> io::Result<Command> {
    let mut cmd = Command::new(program);
    if let Some(d) = &cli.directory {
        cmd.current_dir(fs::canonicalize(d)?);
    }
    if let Some(j) = cli.jobs {
        cmd.args(&["-j", &j.to_string()]);
    }
    let bf = build_file.canonicalize().unwrap_or_else(|_| build_file.to_path_buf());
    cmd.args(&["-f", bf.to_str().unwrap()]);
    cmd.args(targets.as_slice());
    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
    Ok(cmd)
}

fn log_command(cmd: &Command) {
    let program = cmd.get_program().to_string_lossy();
    let args = cmd
        .get_args()
        .map(|a| CommandArg::new(a.to_string_lossy().into_owned()))
        .collect::<Vec<_>>();
    let redacted = redact_sensitive_args(&args);
    let joined = redacted.iter().map(|a| a.as_str()).join(" ");
    info!("Running command: {program} {joined}");
}

fn stream_output(
    stdout: impl Read + Send + 'static,
    stderr: impl Read + Send + 'static,
) -> (thread::JoinHandle<()>, thread::JoinHandle<()>) {
    let out = thread::spawn(move || {
        BufReader::new(stdout).lines().for_each(|l| {
            if let Ok(line) = l {
                println!("{line}");
            }
        })
    });
    let err = thread::spawn(move || {
        BufReader::new(stderr).lines().for_each(|l| {
            if let Ok(line) = l {
                eprintln!("{line}");
            }
        })
    });
    (out, err)
}
```

This keeps all functionality, but splits the large `run_ninja` into three self‐contained pieces, each trivially testable and understandable in isolation.
</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.

Comment thread src/manifest.rs
Comment thread src/runner/process.rs
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos force-pushed the codex/implement-actionable-error-reporting branch from e60ad53 to 8c4c456 Compare September 14, 2025 11:33
@leynos leynos merged commit 0a22d68 into main Sep 14, 2025
7 checks passed
@leynos leynos deleted the codex/implement-actionable-error-reporting branch September 14, 2025 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant