Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// tests.
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(&cli::Cli, &ArgMatches) -> ortho_config::OrthoResult<cli::Cli> =
cli::merge_with_config;
const _: LocalizedParseFn = cli::parse_with_localizer_from;
Expand Down
631 changes: 631 additions & 0 deletions docs/execplans/3-10-3-json-diagnostics-mode.md

Large diffs are not rendered by default.

25 changes: 22 additions & 3 deletions docs/netsuke-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1885,9 +1885,28 @@ enrichment:
the user with a rich, layered explanation of the failure, from the general
to the specific.

For automation use cases, Netsuke will support a `--diag-json` flag. When
enabled, the entire error chain is serialised to JSON, allowing editors and CI
tools to annotate failures inline.
For automation use cases, Netsuke supports a `--diag-json` flag layered through
OrthoConfig as `--diag-json`, `NETSUKE_DIAG_JSON`, and `diag_json = true`. When
enabled, Netsuke emits a Netsuke-owned JSON document on `stderr` instead of
relying on upstream formatter output directly. The current schema is versioned
with `schema_version = 1` and an envelope of:

- `generator`: `name` and `version`
- `diagnostics`: an array of entries containing `message`, `code`, `severity`,
`help`, `url`, `causes`, `source`, `primary_span`, `labels`, and `related`

Design decisions for this mode:

- Netsuke owns the schema rather than exposing `miette`'s raw JSON formatter,
so compatibility can be documented and guarded by snapshot tests.
- JSON mode reserves `stderr` for one machine-readable document only. Progress
updates, verbose timing summaries, emoji prefixes, and tracing logs are
suppressed while the mode is active.
- `stdout` semantics do not change. Commands such as `manifest -` and `graph`
keep streaming their normal artefacts to `stdout`.
- Early startup failures honour only the CLI flag and environment variable.
Configuration files cannot request JSON for errors raised while those same
files are still being located or parsed.

### 7.4 Table: Transforming Errors into User-Friendly Messages

Expand Down
6 changes: 3 additions & 3 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,9 @@ library, and CLI ergonomics.
- [x] 3.10.2. Introduce consistent prefixes for log differentiation.
- [x] Use localizable prefixes or indentation rules.
- [x] Support ASCII and Unicode themes.
- [ ] 3.10.3. Deliver `--diag-json` machine-readable diagnostics mode.
- [ ] Document schema.
- [ ] Add snapshot tests to guard compatibility.
- [x] 3.10.3. Deliver `--diag-json` machine-readable diagnostics mode.
- [x] Document schema.
- [x] Add snapshot tests to guard compatibility.

### 3.11. Configuration and preferences

Expand Down
96 changes: 96 additions & 0 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,17 @@ unsupported locales fall back to English (`en-US`).
For information on contributing translations, see the
[Translator Guide](translators-guide.md).

JSON diagnostics follow the same OrthoConfig layering:

- CLI flag: `--diag-json`
- Environment variable: `NETSUKE_DIAG_JSON=true|false`
- Configuration file: `diag_json = true|false`

For startup failures that happen before configuration files can be loaded,
Netsuke honours the CLI flag and environment variable immediately. A
configuration file cannot request JSON for errors raised while that same file
is being discovered or parsed.

### Output streams

Netsuke separates its output into two streams for scriptability:
Expand Down Expand Up @@ -598,6 +609,91 @@ Ninja file streams to stdout while status messages remain on stderr:
netsuke manifest - | grep 'rule '
```

### JSON diagnostics mode

Use `--diag-json` when a caller needs machine-readable diagnostics on `stderr`
instead of the human-oriented text renderer.

When JSON diagnostics are enabled:

- Failing commands write exactly one JSON document to `stderr`.
- Successful commands write nothing to `stderr`.
- Progress lines, timing summaries, emoji prefixes, and tracing logs are
suppressed so `stderr` remains machine-readable.
- `stdout` behaviour is unchanged. For example, `netsuke --diag-json manifest -`
still streams the generated Ninja file to `stdout`.

The current schema version is `1`. The document shape is:

- `schema_version`: Integer schema version for compatibility checks.
- `generator`: Object containing `name` and `version`.
- `diagnostics`: Array of diagnostic objects.

Each diagnostic object contains:

- `message`: Localized summary line.
- `code`: Stable diagnostic code, or `null` when unavailable.
- `severity`: One of `error`, `warning`, or `advice`.
- `help`: Localized remediation hint, or `null`.
- `url`: Optional documentation URL.
- `causes`: Ordered error-cause chain.
- `source`: Optional source descriptor currently containing `name`.
- `primary_span`: The first labelled span, or `null`.
- `labels`: All labelled spans with `label`, `offset`, `length`, `line`,
`column`, `end_line`, `end_column`, and `snippet`.
- `related`: Nested diagnostics rendered using the same schema.

Example:

```json
{
"schema_version": 1,
"generator": {
"name": "netsuke",
"version": "0.1.0"
},
"diagnostics": [
{
"message": "Manifest parse failed.",
"code": "netsuke::manifest::parse",
"severity": "error",
"help": "YAML does not permit tabs; use spaces for indentation.",
"url": null,
"causes": [
"YAML parse error at line 2, column 2: tabs disallowed within this context",
"tabs disallowed within this context (block indentation) at line 2, column 2"
],
"source": {
"name": "Netsukefile"
},
"primary_span": {
"label": "invalid YAML",
"offset": 10,
"length": 1,
"line": 2,
"column": 2,
"end_line": 2,
"end_column": 2,
"snippet": "-"
},
"labels": [
{
"label": "invalid YAML",
"offset": 10,
"length": 1,
"line": 2,
"column": 2,
"end_line": 2,
"end_column": 2,
"snippet": "-"
}
],
"related": []
}
]
}
```

### Accessible output mode

Netsuke supports an accessible output mode that uses static, labelled status
Expand Down
1 change: 1 addition & 0 deletions locales/en-US/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ cli.flag.fetch_block_host.help = Hostnames that are always blocked, even when al
cli.flag.fetch_default_deny.help = Deny all hosts by default; only allow the declared allowlist.
cli.flag.accessible.help = Force accessible output mode on or off.
cli.flag.progress.help = Force standard stage and task progress summaries on or off.
cli.flag.diag_json.help = Emit machine-readable diagnostics as JSON on stderr.

# Subcommand descriptions.
cli.subcommand.build.about = Build targets defined in the manifest (default).
Expand Down
1 change: 1 addition & 0 deletions locales/es-ES/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ cli.flag.fetch_block_host.help = Nombres de host siempre bloqueados, incluso cua
cli.flag.fetch_default_deny.help = Denegar todos los hosts por defecto; solo permitir la lista de permitidos.
cli.flag.accessible.help = Forzar el modo de salida accesible (activado o desactivado).
cli.flag.progress.help = Forzar los resúmenes de progreso estándar de etapas y tareas (activados o desactivados).
cli.flag.diag_json.help = Emitir diagnósticos legibles por máquinas como JSON en stderr.

# Descripciones de subcomandos.
cli.subcommand.build.about = Compila objetivos definidos en el manifiesto (predeterminado).
Expand Down
19 changes: 18 additions & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;

pub use crate::cli_l10n::locale_hint_from_args;
use crate::cli_l10n::localize_command;
use crate::host_pattern::HostPattern;
use crate::localization::{self, keys};
Expand Down Expand Up @@ -139,6 +138,11 @@ pub struct Cli {
#[arg(long)]
pub no_emoji: Option<bool>,

/// Emit machine-readable diagnostics in JSON on stderr.
#[arg(long)]
#[ortho_config(default = false)]
pub diag_json: bool,

/// Force standard progress summaries on or off.
///
/// When omitted, Netsuke enables progress summaries in standard mode.
Expand Down Expand Up @@ -183,6 +187,7 @@ impl Default for Cli {
accessible: None,
progress: None,
no_emoji: None,
diag_json: false,
command: None,
}
.with_default_command()
Expand Down Expand Up @@ -229,6 +234,18 @@ fn default_manifest_path() -> PathBuf {
PathBuf::from("Netsukefile")
}

/// Inspect raw arguments and extract the requested locale before full parsing.
#[must_use]
pub fn locale_hint_from_args(args: &[OsString]) -> Option<String> {
crate::cli_l10n::locale_hint_from_args(args)
}

/// Inspect raw arguments and extract the requested `--diag-json` state.
#[must_use]
pub fn diag_json_hint_from_args(args: &[OsString]) -> Option<bool> {
crate::cli_l10n::diag_json_hint_from_args(args)
}

/// Parse CLI arguments with localized clap output.
///
/// Returns both the parsed CLI struct and the `ArgMatches` required for
Expand Down
32 changes: 32 additions & 0 deletions src/cli_l10n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ fn flag_help_key(arg_id: &str, subcommand_name: Option<&str>) -> Option<&'static
"accessible" => Some(keys::CLI_FLAG_ACCESSIBLE_HELP),
"progress" => Some(keys::CLI_FLAG_PROGRESS_HELP),
"no_emoji" => Some(keys::CLI_FLAG_NO_EMOJI_HELP),
"diag_json" => Some(keys::CLI_FLAG_DIAG_JSON_HELP),
_ => None,
},
Some("build") => match arg_id {
Expand Down Expand Up @@ -199,3 +200,34 @@ pub fn locale_hint_from_args(args: &[OsString]) -> Option<String> {
}
hint
}

fn parse_bool_hint(value: &str) -> Option<bool> {
match value.to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}

/// Inspect raw arguments and extract the requested `--diag-json` state.
///
/// A bare `--diag-json` enables JSON diagnostics. When multiple forms are
/// present, the last recognised value wins.
#[must_use]
pub fn diag_json_hint_from_args(args: &[OsString]) -> Option<bool> {
let mut hint = None;
for arg in args {
let text = arg.to_string_lossy();
if text == "--" {
break;
}
if text == "--diag-json" {
hint = Some(true);
continue;
}
if let Some(value) = text.strip_prefix("--diag-json=") {
hint = parse_bool_hint(value);
}
}
hint
}
Loading
Loading