diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 1d56048..5184d70 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -2180,6 +2180,25 @@ artifacts: status: implemented tags: [codegen, wit, cli, v0100] + - id: REQ-CODEGEN-SUMMARY + type: requirement + title: codegen prints a per-category summary and a hint when WIT was requested but none emitted + description: > + `spar codegen` shall print a one-line summary breaking the + generated files down by category (`.wit`, `.rs`, workspace, config, + other) together with the requested `--format`, so an operator can + tell at a glance both what got produced and whether the requested + format was honoured. When `--format wit` or `--format both` is + passed (`both` is the default) and zero `.wit` files are emitted, + a diagnostic hint shall follow the summary explaining that WIT + interfaces are generated per AADL `process` subcomponent and + pointing to `--format rust` as the alternative for models with + threads only. This addresses the silent failure mode that + surfaces as "the other files but no wit" when an AADL model has + threads but no processes. + status: implemented + tags: [codegen, wit, cli, diagnostics, v0110] + # Research findings tracked separately in research/findings.yaml # ── Mermaid M3 (classDiagram + requirementDiagram) ───────────────────── diff --git a/artifacts/verification.yaml b/artifacts/verification.yaml index a71f177..0dda252 100644 --- a/artifacts/verification.yaml +++ b/artifacts/verification.yaml @@ -2896,6 +2896,33 @@ artifacts: - type: satisfies target: REQ-CODEGEN-WIT-STRICT + - id: TEST-CODEGEN-SUMMARY-HINT + type: feature + title: codegen per-category summary line + empty-WIT hint smoke + description: > + Smoke runs of `spar codegen` confirm the new diagnostic: + threads-only model + default `--format both` emits the summary + `wrote N files (... .rs, ... workspace) (format: both)` followed + by the three-line hint pointing at `process` subcomponents and + `--format rust`; process + `--format wit` emits the summary with + `.wit` count and no hint; threads + `--format rust` emits the + summary with no hint (user did not request WIT). Helpers + `summarise_codegen_output` and `maybe_print_empty_wit_hint` in + crates/spar-cli/src/main.rs are pure functions of the generated + file list + format, ready for promotion to golden tests if the + stdout shape is later contracted. + fields: + method: smoke-test + steps: + - run: cargo run -q -p spar -- codegen --root Threads::Sys.impl --output /tmp/threads-out /tmp/threads.aadl + - run: cargo run -q -p spar -- codegen --root Proc::Sys.impl --format wit --output /tmp/proc-out /tmp/proc.aadl + - run: cargo run -q -p spar -- codegen --root Threads::Sys.impl --format rust --output /tmp/threads-rust-out /tmp/threads.aadl + status: passing + tags: [codegen, wit, cli, diagnostics, v0110] + links: + - type: satisfies + target: REQ-CODEGEN-SUMMARY + # ── Trace-topology fixtures (v0.11.0) ──────────────────────────────────── - id: TEST-TRACE-FIXTURES diff --git a/crates/spar-cli/src/main.rs b/crates/spar-cli/src/main.rs index fec2131..ec14da4 100644 --- a/crates/spar-cli/src/main.rs +++ b/crates/spar-cli/src/main.rs @@ -2073,12 +2073,15 @@ fn cmd_codegen(args: &[String]) { let result = spar_codegen::generate(&inst, &config); if dry_run { - eprintln!("Dry run: {} files would be generated", result.files.len()); + eprintln!( + "Dry run: {}", + summarise_codegen_output(&result.files, output_format) + ); for file in &result.files { println!("{}/{}", output_dir, file.path); } + maybe_print_empty_wit_hint(&result.files, output_format); } else { - let mut count = 0; for file in &result.files { // Validate that the generated file path does not escape the // output directory via path traversal (e.g., "../" components). @@ -2098,10 +2101,110 @@ fn cmd_codegen(args: &[String]) { eprintln!("Cannot write {full_path}: {e}"); process::exit(1); }); - count += 1; } - eprintln!("Generated {count} files in {output_dir}/"); + eprintln!( + "codegen: {} to {output_dir}/", + summarise_codegen_output(&result.files, output_format) + ); + maybe_print_empty_wit_hint(&result.files, output_format); + } +} + +/// Categorise a generated file by extension / well-known name. +/// +/// `(extension_tag, count_label)` — the tag is used for the empty-WIT +/// hint, the label is what shows up in the summary line. +fn codegen_file_categories(files: &[spar_codegen::GeneratedFile]) -> [(u32, &'static str); 5] { + let mut wit = 0u32; + let mut rust = 0u32; + let mut workspace = 0u32; + let mut config = 0u32; + let mut other = 0u32; + for file in files { + let path = std::path::Path::new(&file.path); + let ext = path.extension().and_then(|s| s.to_str()).unwrap_or(""); + let name = path.file_name().and_then(|s| s.to_str()).unwrap_or(""); + match (ext, name) { + ("wit", _) => wit += 1, + ("rs", _) => rust += 1, + (_, "Cargo.toml" | "Cargo.lock" | "BUILD.bazel" | "WORKSPACE") => workspace += 1, + ("toml", _) => config += 1, + _ => other += 1, + } + } + [ + (wit, ".wit"), + (rust, ".rs"), + (workspace, "workspace"), + (config, "config"), + (other, "other"), + ] +} + +/// One-line summary like +/// `wrote 7 files (1 .wit, 4 .rs, 2 workspace) (format: both)`. +/// +/// The format label answers "did spar honour my `--format`?" at a +/// glance, and the per-category counts answer "what got produced?" — +/// the silent default-`both` failure mode (Rust + workspace files but +/// zero `.wit` when the model has no `process` subcomponents) is +/// immediately visible from this line alone. +fn summarise_codegen_output( + files: &[spar_codegen::GeneratedFile], + format: spar_codegen::OutputFormat, +) -> String { + let cats = codegen_file_categories(files); + let format_label = match format { + spar_codegen::OutputFormat::Rust => "rust", + spar_codegen::OutputFormat::Wit => "wit", + spar_codegen::OutputFormat::Both => "both", + }; + let mut parts: Vec = Vec::new(); + for (n, label) in cats { + if n > 0 { + parts.push(format!("{n} {label}")); + } } + let body = if parts.is_empty() { + "empty".to_string() + } else { + parts.join(", ") + }; + let total = files.len(); + let noun = if total == 1 { "file" } else { "files" }; + format!("wrote {total} {noun} ({body}) (format: {format_label})") +} + +/// Explain the silent failure mode: when WIT was requested (`--format wit` +/// or the default `--format both`) but zero `.wit` files were emitted, +/// the AADL model has no `process` subcomponents to generate interfaces +/// for. This is the most common confusing case — agents see "the other +/// files but no wit" and don't know why — so the hint is worth a few +/// lines of stdout. +fn maybe_print_empty_wit_hint( + files: &[spar_codegen::GeneratedFile], + format: spar_codegen::OutputFormat, +) { + let asked_for_wit = matches!( + format, + spar_codegen::OutputFormat::Wit | spar_codegen::OutputFormat::Both + ); + if !asked_for_wit { + return; + } + let wit_count = codegen_file_categories(files)[0].0; + if wit_count > 0 { + return; + } + eprintln!( + "hint: --format {} emits one .wit interface per AADL `process` subcomponent;", + match format { + spar_codegen::OutputFormat::Wit => "wit", + _ => "both", + } + ); + eprintln!(" this model has 0 processes — add `process` declarations to the system"); + eprintln!(" implementation, or pass --format rust for thread skeletons only."); } /// Check that a generated file path is safe to write under the output directory. diff --git a/crates/spar-cli/tests/roundtrip.rs b/crates/spar-cli/tests/roundtrip.rs index b10e8df..262e0bb 100644 --- a/crates/spar-cli/tests/roundtrip.rs +++ b/crates/spar-cli/tests/roundtrip.rs @@ -208,9 +208,12 @@ fn roundtrip_aadl_codegen_produces_all_artifacts() { ); let stderr = String::from_utf8_lossy(&codegen.stderr); + // Codegen prints a per-category summary of the shape + // codegen: wrote N files (… .wit, … .rs, … workspace) (format: both) to / + // — see crates/spar-cli/src/main.rs `summarise_codegen_output`. assert!( - stderr.contains("Generated"), - "should report generation: {stderr}" + stderr.contains("codegen: wrote") && stderr.contains("(format:"), + "should report generation summary: {stderr}" ); // Verify key files were generated