Skip to content
Merged
138 changes: 98 additions & 40 deletions docs/netsuke-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1181,17 +1181,17 @@ The command construction will follow this pattern:
1. A new `Command` is created via `Command::new("ninja")`. Netsuke will assume
`ninja` is available in the system's `PATH`.

1. Arguments passed to Netsuke's own CLI will be translated and forwarded to
2. Arguments passed to Netsuke's own CLI will be translated and forwarded to
Ninja. For example, a `Netsuke build my_target` command would result in
`Command::new("ninja").arg("my_target")`. Flags like `-j` for parallelism
will also be passed through.[^8]

1. The working directory for the Ninja process will be set using
3. The working directory for the Ninja process will be set using
`.current_dir()`. When the user supplies a `-C` flag, Netsuke canonicalises
the path and applies it via `current_dir` rather than forwarding the flag to
Ninja.

1. Standard I/O streams (`stdin`, `stdout`, `stderr`) will be configured using
4. Standard I/O streams (`stdin`, `stdout`, `stderr`) will be configured using
`.stdout(Stdio::piped())` and `.stderr(Stdio::piped())`.[^24] This allows
Netsuke to capture the real-time output from Ninja, which can then be
streamed to the user's console, potentially with additional formatting or
Expand Down Expand Up @@ -1279,27 +1279,32 @@ three fundamental questions:
1. **What** went wrong? A concise summary of the failure (e.g., "YAML parsing
failed," "Build configuration is invalid").

1. **Where** did it go wrong? Precise location information, including the file,
2. **Where** did it go wrong? Precise location information, including the file,
line number, and column where applicable (e.g., "in `Netsukefile` at line
15, column 3").

1. **Why** did it go wrong, and what can be done about it? The underlying cause
3. **Why** did it go wrong, and what can be done about it? The underlying cause
of the error and a concrete suggestion for how to fix it (e.g., "Cause:
Found a tab character, which is not allowed. Hint: Use spaces for
indentation instead.").

### 7.2 Crate Selection and Strategy: `anyhow`, `thiserror`, and `miette`

To implement this philosophy, Netsuke adopts a hybrid error handling strategy
using the `anyhow`, `thiserror`, and `miette` crates. This is a common and
highly effective pattern in the Rust ecosystem for creating robust applications
and libraries.[^27] `miette` renders user-facing diagnostics, computing spans
directly from parser locations.
Netsuke uses a two-tier error architecture:

- `thiserror`: This crate will be used *within* Netsuke's internal library
modules (e.g., `parser`, `ir`, `ninja_gen`) to define specific, structured
error types. The `#[derive(Error)]` macro reduces boilerplate and allows for
the creation of rich, semantic errors.[^29]
1. `anyhow` captures internal context as errors propagate through the
application.
2. `miette` renders user-facing diagnostics and **is not optional**. All
surface errors must implement `miette::Diagnostic` so the CLI can present
spans, annotated source, and helpful suggestions.

This hybrid strategy is common in the Rust ecosystem and provides both rich
context and polished user output.[^27]

- `thiserror`: This crate is used *within* Netsuke's internal library modules
(e.g., `parser`, `ir`, `ninja_gen`) to define specific, structured error
types. The `#[derive(Error)]` macro reduces boilerplate and allows for the
creation of rich, semantic errors.[^29]

Rust

Expand Down Expand Up @@ -1327,17 +1332,64 @@ pub enum IrGenError {
ActionSerialisation(#[from] serde_json::Error), }
```

- `anyhow`: This crate will be used in the main application logic (`main.rs`)
and at the boundaries between modules. `anyhow::Result` serves as a
convenient, dynamic error type that can wrap any underlying error that
implements `std::error::Error`.[^30] The primary tools used will be the

`?` operator for clean error propagation and the `.context()` and
`.with_context()` methods for adding high-level, human-readable context to
errors as they bubble up the call stack.[^31]
- `anyhow`: Used in the main application logic (`main.rs`) and at the
boundaries between modules. `anyhow::Result` wraps any error implementing
`std::error::Error`.[^30] The `?` operator provides clean propagation, while
`.context()` and `.with_context()` attach high-level explanations as errors
bubble up.[^31]

- `miette`: Presents human-friendly diagnostics, highlighting exact error
locations with computed spans.
locations with computed spans. Every diagnostic must retain `miette`'s
`Diagnostic` implementation as it travels through `anyhow`.

#### Canonical pattern: `YamlDiagnostic`

`YamlDiagnostic` is the reference implementation of a Netsuke diagnostic. It
wraps `yaml-rust` errors with annotated source, spans, and optional help text:

```rust
#[derive(Debug, Error, Diagnostic)]
#[error("{message}")]
#[diagnostic(code(netsuke::yaml::parse))]
pub struct YamlDiagnostic {
#[source_code]
src: NamedSource<String>,
#[label("parse error here")]
span: Option<SourceSpan>,
#[help]
help: Option<String>,
#[source]
source: YamlError,
message: String,
}

#[derive(Debug, Error, Diagnostic)]
pub enum ManifestError {
#[error("manifest parse error")]
#[diagnostic(code(netsuke::manifest::parse))]
Parse {
#[source]
#[diagnostic_source]
source: Box<dyn Diagnostic + Send + Sync + 'static>,
},
}
```

`ManifestError::Parse` boxes the diagnostic to preserve the rich error so
`miette` can show the offending YAML snippet. All new user-facing errors with
source context must follow this model.

Common use cases requiring `miette` diagnostics include:

- YAML parsing errors.
- Jinja template rendering failures with line numbers and context.
- Any scenario where highlighting spans or providing structured help benefits
the user.

Although `src/diagnostics.rs` is currently unused, it contains prototypes for
`miette` patterns and remains a valuable reference. Future diagnostics should
mirror the `YamlDiagnostic` approach by implementing `Diagnostic`, providing a
`NamedSource`, a `SourceSpan`, and actionable help text.

### 7.3 Error Handling Flow

Expand All @@ -1346,23 +1398,28 @@ enrichment:

1. A specific, low-level error occurs within a module. For instance, the IR
generator detects a missing rule and creates an `IrGenError::RuleNotFound`.
Likewise, the Ninja generator returns `NinjaGenError::MissingAction` when a
build edge references an undefined action, preventing panics during file
generation.

1. The function where the error occurred returns
2. The function where the error occurred returns
`Err(IrGenError::RuleNotFound {... }.into())`. The `.into()` call converts
the specific `thiserror` enum variant into a generic `anyhow::Error` object,
preserving the original error as its source.

1. A higher-level function in the call stack, which called the failing function,
3. A higher-level function in the call stack, which called the failing function,
receives this `Err` value. It uses the `.with_context()` method to wrap the
error with more application-level context. For example:
`ir::from_manifest(ast)`
`.with_context(|| "Failed to build the internal build graph from the manifest")?`
.

1. This process of propagation and contextualization repeats as the error
bubbles up towards `main`.
4. This process of propagation and contextualisation repeats as the error
bubbles up towards `main`. Use `anyhow::Context` to add detail, but never
convert a `miette::Diagnostic` into a plain `anyhow::Error`—doing so would
discard spans and help text.

1. Finally, the `main` function receives the `Err` result. It prints the entire
5. Finally, the `main` function receives the `Err` result. It prints the entire
error chain provided by `anyhow`, which displays the highest-level context
first, followed by a list of underlying "Caused by:" messages. This provides
the user with a rich, layered explanation of the failure, from the general
Expand Down Expand Up @@ -1530,15 +1587,15 @@ goal.

1. Implement the initial `clap` CLI structure for the `build` command.

1. Implement the YAML parser using `serde_yml` and the AST data structures
2. Implement the YAML parser using `serde_yml` and the AST data structures
(`ast.rs`).

1. Implement the AST-to-IR transformation logic, including basic validation
3. Implement the AST-to-IR transformation logic, including basic validation
like checking for rule existence.

1. Implement the IR-to-Ninja file generator (`ninja_gen.rs`).
4. Implement the IR-to-Ninja file generator (`ninja_gen.rs`).

1. Implement the `std::process::Command` logic to invoke `ninja`.
5. Implement the `std::process::Command` logic to invoke `ninja`.

- **Success Criterion:** Netsuke can successfully take a `Netsukefile` file
*without any Jinja syntax* and compile it to a `build.ninja` file, then
Expand All @@ -1554,13 +1611,13 @@ goal.

1. Integrate the `minijinja` crate into the build pipeline.

1. Implement the two-pass parsing mechanism: first render the manifest with
2. Implement the two-pass parsing mechanism: first render the manifest with
`minijinja`, then parse the result with `serde_yml`.

1. Populate the initial Jinja context with the global `vars` from the
3. Populate the initial Jinja context with the global `vars` from the
manifest.

1. Implement basic Jinja control flow (`{% if... %}`, `{% for... %}`) and
4. Implement basic Jinja control flow (`{% if... %}`, `{% for... %}`) and
variable substitution.

- **Success Criterion:** Netsuke can successfully build a manifest that uses
Expand All @@ -1577,15 +1634,15 @@ goal.
1. Implement the full suite of custom Jinja functions (`glob`, `env`, etc.)
and filters (`shell_escape`).

1. Mandate the use of `shell-quote` for all command variable substitutions.
2. Mandate the use of `shell-quote` for all command variable substitutions.

1. Refactor the error handling to fully adopt the `anyhow`/`thiserror`
3. Refactor the error handling to fully adopt the `anyhow`/`thiserror`
strategy, ensuring all user-facing errors are contextual and actionable
as specified in Section 7.

1. Implement the `clean` and `graph` subcommands.
4. Implement the `clean` and `graph` subcommands.

1. Refine the CLI output for clarity and readability.
5. Refine the CLI output for clarity and readability.

- **Success Criterion:** Netsuke is a feature-complete, secure, and
user-friendly build tool that meets all the initial design goals.
Expand Down Expand Up @@ -1652,7 +1709,8 @@ projects.
### **Works cited**

[^1]: Ninja, a small build system with a focus on speed. Accessed on 12 July
2025. <https://ninja-build.org/>

1. <https://ninja-build.org/>

[^2]: "Ninja (build system)." Wikipedia. Accessed on 12 July 2025.
<https://en.wikipedia.org/wiki/Ninja_(build_system)>
Expand Down
12 changes: 7 additions & 5 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,20 @@ library, and CLI ergonomics.
- [x] After interpolation, validate the final command string is parsable using
the shlex crate.

- [ ] **Actionable Error Reporting:**
- [x] **Actionable Error Reporting:**

- [ ] Adopt the `anyhow` and `thiserror` error handling strategy.
- [x] Adopt the `anyhow` and `thiserror` error handling strategy.

- [ ] Use thiserror to define specific, structured error types within library
- [x] Use thiserror to define specific, structured error types within library

modules (e.g., IrGenError::RuleNotFound, IrGenError::CircularDependency).

- [ ] Use anyhow in the application logic to add human-readable context to
- [x] Use anyhow in the application logic to add human-readable context to
errors as they propagate (e.g., using .with_context()).
- [x] Use `miette` to render diagnostics with source spans and helpful
messages.

- [ ] Refactor all error-producing code to provide the clear, contextual, and
- [x] Refactor all error-producing code to provide the clear, contextual, and
actionable error messages specified in the design document.

- [ ] **Template Standard Library:**
Expand Down
18 changes: 13 additions & 5 deletions docs/snapshot-testing-in-netsuke-using-insta.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,11 @@ fn simple_manifest_ninja_snapshot() {
let build_graph = BuildGraph::from_manifest(&manifest).expect("IR generation succeeded");

// Generate Ninja file content from the IR
let ninja_file = ninja_gen::generate_ninja(&build_graph)
.expect("Ninja file generation succeeded");
// `generate` returns `Result<String, NinjaGenError>`; handle errors
let ninja_file = match ninja_gen::generate(&build_graph) {
Ok(ninja) => ninja,
Err(e) => panic!("Ninja file generation failed: {e}"),
};

// The output is a multi-line Ninja build script (as a String)
// Ensure the output is deterministic
Expand All @@ -190,15 +193,20 @@ fn simple_manifest_ninja_snapshot() {
}
```

The match explicitly handles the `Result` from `generate` so any formatting or
missing action errors surface during tests. Production code should propagate
the error and report it with `miette` rather than panicking.

Key points for Ninja snapshot tests:

- Use a known manifest input and first derive the IR. An IR can also be
constructed directly for tests, but using the manifest→IR pipeline ensures
realistic coverage.

- Call the Ninja generation function (e.g. `ninja_gen::generate_ninja`), which
produces the Ninja file contents as a `String`. This function traverses the
IR and outputs rules and build statements in Ninja syntax.
- Call the Ninja generation function (`ninja_gen::generate`), which
yields a `Result<String, NinjaGenError>`. This function traverses the IR and
outputs rules and build statements in Ninja syntax, returning an error if any
build edge references an undefined action.

- As with IR, **determinism is crucial**. The Ninja output should list rules,
targets, and dependencies in a consistent order. For example, if the IR does
Expand Down
4 changes: 4 additions & 0 deletions src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ use std::fmt::Display;
/// File::open(path).diag("open file")
/// }
/// ```
#[expect(
dead_code,
reason = "temporarily retained during anyhow migration; remove when no call sites remain"
)]
pub(crate) trait ResultExt<T> {
/// Attach a static context message to any error.
///
Expand Down
Loading
Loading