Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 28 additions & 0 deletions artifacts/requirements.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2118,3 +2118,31 @@ artifacts:
tags: [ci, rivet, verification, v0100]

# Research findings tracked separately in research/findings.yaml

# ── Mermaid M3 (classDiagram + requirementDiagram) ─────────────────────

- id: REQ-MERMAID-CLASS-001
type: requirement
title: classDiagram emitter for AADL component types
description: >
spar-mermaid shall provide an emit_class_diagram function that walks a
SystemInstance and produces a Mermaid classDiagram with one class block
per distinct component type, showing the AADL category as a Mermaid
stereotype and listing each FeatureInstance as a class attribute.
The emitter shall honour the categories and max_depth filters from
MermaidOptions.
status: implemented
tags: [mermaid, emission, v0100]

- id: REQ-MERMAID-REQ-001
type: requirement
title: requirementDiagram emitter from rivet artifacts
description: >
spar-mermaid shall provide an emit_requirement_diagram function that
reads artifacts/requirements.yaml and produces a Mermaid
requirementDiagram with one block per requirement/feature artifact and
relationship edges (satisfies, verifies, derives) derived from the
artifact links section. Artifacts with type design-decision or other
non-requirement types shall be silently skipped.
status: implemented
tags: [mermaid, emission, v0100]
46 changes: 46 additions & 0 deletions artifacts/verification.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2765,3 +2765,49 @@ artifacts:
links:
- type: satisfies
target: REQ-VERIFY-GATE-001

# ── Mermaid M3 ──────────────────────────────────────────────────────────

- id: TEST-MERMAID-CLASS
type: feature
title: classDiagram emitter unit + integration tests
description: >
Unit tests in crates/spar-mermaid/src/lib.rs verify: (a) one class
block is emitted per distinct component type_name; (b) the AADL category
appears as a Mermaid stereotype (<<system>>, <<thread>>, etc.); (c) the
categories filter excludes components whose category is not listed.
Integration test in crates/spar-cli/tests/emit_mermaid.rs invokes
spar emit --format mermaid-class and asserts classDiagram header plus
at least one stereotype.
fields:
method: automated-test
steps:
- run: cargo test -p spar-mermaid
- run: cargo test -p spar --test emit_mermaid emit_mermaid_class_happy_path
status: passing
tags: [mermaid, emission, v0100]
links:
- type: satisfies
target: REQ-MERMAID-CLASS-001

- id: TEST-MERMAID-REQ
type: feature
title: requirementDiagram emitter unit + integration tests
description: >
Unit tests in crates/spar-mermaid/src/lib.rs verify: (a) the
requirementDiagram header and at least one requirement block are present;
(b) satisfies edges appear in output for linked requirements; (c)
artifacts with type design-decision are excluded from the diagram.
Integration test in crates/spar-cli/tests/emit_mermaid.rs invokes
spar emit --format mermaid-req from the workspace root and asserts
requirementDiagram header plus at least one REQ_*_001 block.
fields:
method: automated-test
steps:
- run: cargo test -p spar-mermaid
- run: cargo test -p spar --test emit_mermaid emit_mermaid_req_happy_path
status: passing
tags: [mermaid, emission, v0100]
links:
- type: satisfies
target: REQ-MERMAID-REQ-001
53 changes: 42 additions & 11 deletions crates/spar-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,13 @@ fn print_usage() {
);
eprintln!(" modes --root Package::Type.Impl [--format text|smv|dot] <file...>");
eprintln!(
" emit --root Package::Type.Impl [--format mermaid] [--category <cat,...>] \
[--max-depth N] [--no-connections] [-o output.md] <file...>"
" emit [--format mermaid|mermaid-class|mermaid-req] \
[--root Package::Type.Impl] [--category <cat,...>] \
[--max-depth N] [--no-connections] [-o output.md] [<file...>]\n\
\n\
\t\t mermaid flowchart TD (default)\n\
\t\t mermaid-class classDiagram of component types\n\
\t\t mermaid-req requirementDiagram from artifacts/requirements.yaml"
);
eprintln!(" render --root Package::Type.Impl [-o output.svg] <file...>");
eprintln!(
Expand Down Expand Up @@ -1316,7 +1321,9 @@ fn parse_category(s: &str) -> spar_hir_def::item_tree::ComponentCategory {

fn cmd_emit(args: &[String]) {
let mut root: Option<String> = None;
let mut format: Option<String> = None;
// Default format is "mermaid" (flowchart). Also accepts "mermaid-class"
// and "mermaid-req".
let mut format: String = "mermaid".to_string();
let mut categories: Vec<spar_hir_def::item_tree::ComponentCategory> = Vec::new();
let mut max_depth: Option<usize> = None;
let mut include_connections = true;
Expand All @@ -1338,14 +1345,20 @@ fn cmd_emit(args: &[String]) {
"--format" => {
i += 1;
if i < args.len() {
let f = args[i].clone();
if f != "mermaid" {
eprintln!("--format only supports 'mermaid' (got '{f}')");
process::exit(1);
let f = args[i].as_str();
match f {
"mermaid" | "mermaid-class" | "mermaid-req" => {
format = f.to_string();
}
other => {
eprintln!(
"--format only supports 'mermaid' | 'mermaid-class' | 'mermaid-req' (got '{other}')"
);
process::exit(1);
}
}
format = Some(f);
} else {
eprintln!("--format requires a value (mermaid)");
eprintln!("--format requires a value (mermaid|mermaid-class|mermaid-req)");
process::exit(1);
}
}
Expand Down Expand Up @@ -1399,8 +1412,17 @@ fn cmd_emit(args: &[String]) {
i += 1;
}

let _ = format; // "mermaid" is the only valid value; validated above.
// ── mermaid-req: no AADL files needed ───────────────────────────────────
if format == "mermaid-req" {
let req_path = std::path::Path::new("artifacts/requirements.yaml");
let diagram = spar_mermaid::emit_requirement_diagram(req_path).unwrap_or_else(|e| {
eprintln!("error: {e}");
process::exit(1);
});
return emit_output(diagram, output);
}

// ── mermaid / mermaid-class: require --root and AADL files ──────────────
let root = root.unwrap_or_else(|| {
eprintln!("--root Package::Type.Impl is required");
process::exit(1);
Expand Down Expand Up @@ -1450,8 +1472,17 @@ fn cmd_emit(args: &[String]) {
include_connections,
};

let diagram = spar_mermaid::emit_flowchart(&inst, &opts);
let diagram = if format == "mermaid-class" {
spar_mermaid::emit_class_diagram(&inst, &opts)
} else {
spar_mermaid::emit_flowchart(&inst, &opts)
};

emit_output(diagram, output);
}

/// Write `diagram` to `output` path or stdout.
fn emit_output(diagram: String, output: Option<String>) {
match output {
Some(path) => {
fs::write(&path, &diagram).unwrap_or_else(|e| {
Expand Down
91 changes: 88 additions & 3 deletions crates/spar-cli/tests/emit_mermaid.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! Integration tests for `spar emit --format mermaid`.
//!
//! Tests the happy-path flowchart emission and the `--category` filter.
//! Integration tests for `spar emit --format mermaid` (M2) and the M3
//! extensions `mermaid-class` and `mermaid-req`.

use std::env;
use std::fs;
Expand Down Expand Up @@ -145,3 +144,89 @@ fn emit_mermaid_category_filter_excludes_non_thread() {

let _ = fs::remove_file(&path);
}

// ── M3: mermaid-class ───────────────────────────────────────────────────────

/// `spar emit --format mermaid-class --root ...` should produce a Mermaid
/// classDiagram containing the header and at least one stereotype.
#[test]
fn emit_mermaid_class_happy_path() {
let path = write_model("class_happy");

let output = spar()
.arg("emit")
.arg("--root")
.arg("Emit_Test::TestSys.Impl")
.arg("--format")
.arg("mermaid-class")
.arg(&path)
.output()
.expect("failed to run spar emit --format mermaid-class");

let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(
output.status.success(),
"spar emit --format mermaid-class failed; stderr:\n{stderr}"
);

assert!(
stdout.starts_with("classDiagram\n"),
"expected 'classDiagram' header; got:\n{stdout}"
);

// At least one stereotype should be present.
assert!(
stdout.contains("<<"),
"expected at least one stereotype (<<...>>) in output; got:\n{stdout}"
);

let _ = fs::remove_file(&path);
}

// ── M3: mermaid-req ─────────────────────────────────────────────────────────

/// `spar emit --format mermaid-req` should produce a Mermaid requirementDiagram
/// containing the header and at least one of the well-known REQ-* IDs from
/// artifacts/requirements.yaml (present in repo root).
#[test]
fn emit_mermaid_req_happy_path() {
// mermaid-req does not require AADL files or --root.
let output = spar()
.arg("emit")
.arg("--format")
.arg("mermaid-req")
.current_dir(
// Run from the workspace root so "artifacts/requirements.yaml" resolves.
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap(),
)
.output()
.expect("failed to run spar emit --format mermaid-req");

let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(
output.status.success(),
"spar emit --format mermaid-req failed; stderr:\n{stderr}"
);

assert!(
stdout.starts_with("requirementDiagram\n"),
"expected 'requirementDiagram' header; got:\n{stdout}"
);

// At least one well-known REQ should appear (sanitised hyphens → underscores).
let has_known_req = stdout.contains("REQ_PARSE_001")
|| stdout.contains("REQ_MODEL_001")
|| stdout.contains("REQ_MERMAID");
assert!(
has_known_req,
"expected at least one well-known REQ_* block; got:\n{stdout}"
);
}
Loading
Loading