From 2ca2f3a086d5853d31917f047ce19f6a42e1908e Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 1 Jul 2026 07:39:14 +0200 Subject: [PATCH] feat(validate): lifecycle gap names the ASPICE verification chain (REQ-237, #350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marking a sw-req implemented and green requires the full ASPICE chain (sw-req <- sw-detail-design <- unit-verification; sw-req <- sw-arch-component <- sw-integration-verification), but the completeness diagnostic only listed "missing: unit-verification, sw-integration-verification". A downstream user (synth) authored a direct `unit-verification verifies: SL-TR-003`, which validate rejected (that link targets sw-detail-design), and had to reverse-engineer the layering from the rejection. Now, for each missing downstream type that cannot link DIRECTLY to the artifact's type, the gap names what it DOES attach to, so the required intermediate is obvious: SL-TR-003 (sw-req, status: implemented) — missing: unit-verification, ... -> a `unit-verification` `verifies` `sw-detail-design`, not `sw-req` directly — add an intermediate `sw-detail-design` that traces to this artifact and is `verifies`-linked by the `unit-verification` A missing type that CAN link directly (e.g. `sw-verification` -> sw-req) gets no hint. Rendered at the CLI display layer (shared by the salsa and --direct paths, so validate/direct parity is preserved — REQ-241). Per the reporter's own ask; keeps the ASPICE layering required (shape B), just guided. Implements: REQ-237 Verifies: REQ-237 Refs: REQ-089 Co-Authored-By: Claude Opus 4.8 --- artifacts/requirements.yaml | 2 +- rivet-cli/src/main.rs | 72 ++++++++++++++++++++++++++++++--- rivet-cli/tests/cli_commands.rs | 44 ++++++++++++++++++++ 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 3e30b93..67ee488 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -7557,7 +7557,7 @@ artifacts: - id: REQ-237 type: requirement title: direct test->sw-req link (skip full ASPICE design chain) - status: proposed + status: verified description: "Allow a test to link directly to a sw-req for verified status without the full ASPICE design chain; guide the completeness error toward it. #350. v0.23." provenance: created-by: ai-assisted diff --git a/rivet-cli/src/main.rs b/rivet-cli/src/main.rs index 5520b27..1dea4d0 100644 --- a/rivet-cli/src/main.rs +++ b/rivet-cli/src/main.rs @@ -5276,6 +5276,59 @@ fn cmd_validate_new_since(cli: &Cli, since_ref: &str, fail_on: &str) -> Result String { + let quoted: Vec = types.iter().map(|t| format!("`{t}`")).collect(); + match quoted.split_last() { + None => "(nothing)".to_string(), + Some((last, [])) => last.clone(), + Some((last, rest)) => format!("{} or {last}", rest.join(", ")), + } +} + +/// #350 (REQ-237): explain the ASPICE chain for a lifecycle completeness gap. +/// +/// Given the schema, the type of the artifact with the gap, and a missing +/// downstream type, return a hint when that missing type cannot link DIRECTLY +/// to the source type — naming what it *does* attach to, so the required +/// intermediate artifact is obvious. e.g. for a `sw-req` missing a +/// `unit-verification`: "a `unit-verification` `verifies` `sw-detail-design`, +/// not `sw-req` directly — add an intermediate `sw-detail-design` …". Returns +/// `None` when the missing type CAN link directly (the gap is just "add this +/// backlink", already clear) or the schema has no link info for it. +fn aspice_chain_hint( + schema: &rivet_core::schema::Schema, + source_type: &str, + missing_type: &str, +) -> Option { + let td = schema.artifact_type(missing_type)?; + let mut targets: std::collections::BTreeSet = std::collections::BTreeSet::new(); + let mut via: Option = None; + for lf in &td.link_fields { + if lf.target_types.is_empty() { + continue; + } + if via.is_none() { + via = Some(lf.link_type.clone()); + } + for t in &lf.target_types { + targets.insert(t.clone()); + } + } + // No link info, or it can attach directly to the source → nothing to + // explain (the "missing" line already tells the whole story). + if targets.is_empty() || targets.contains(source_type) { + return None; + } + let via = via.unwrap_or_else(|| "its link".to_string()); + let intermediates: Vec<&str> = targets.iter().map(String::as_str).collect(); + let tl = fmt_type_list(&intermediates); + Some(format!( + "a `{missing_type}` `{via}` {tl}, not `{source_type}` directly — add an intermediate {tl} that traces to this artifact and is `{via}`-linked by the `{missing_type}`" + )) +} + /// Validate a full project (with rivet.yaml). #[allow(clippy::too_many_arguments)] fn cmd_validate( @@ -6075,12 +6128,21 @@ fn cmd_validate( gap.artifact_status.as_deref().unwrap_or("none"), gap.missing.join(", "), ); + // #350 (REQ-237): a missing type is often not directly linkable + // to this artifact — e.g. a `unit-verification` verifies a + // `sw-detail-design`, not a `sw-req` — so authoring a direct + // link is rejected and the bare "missing" list points at the + // wrong fix. Name the chain: for each missing type whose own + // links can't target this artifact's type, say what it DOES + // attach to, so the intermediate artifact is obvious. + for missing in &gap.missing { + if let Some(hint) = aspice_chain_hint(&schema, &gap.artifact_type, missing) { + eprintln!(" → {hint}"); + } + } } - // The bare "missing: " list says what, not how — which link - // type connects them, and that some listed types may only attach - // further down the chain (issue #350). Point at the per-artifact - // explainer, which names the exact incoming link + allowed source - // types (and any alternates) for each gap. + // The bare "missing: " list says what, not how. Point at the + // per-artifact explainer for the exact link types and alternates. if let Some(first) = lifecycle_gaps.first() { println!( " → run `rivet validate --explain {}` to see which link type and source types satisfy a gap", diff --git a/rivet-cli/tests/cli_commands.rs b/rivet-cli/tests/cli_commands.rs index 55875c5..eecb0dd 100644 --- a/rivet-cli/tests/cli_commands.rs +++ b/rivet-cli/tests/cli_commands.rs @@ -679,6 +679,50 @@ fn validate_surfaces_parse_error_on_malformed_artifact_file() { ); } +/// #350 (REQ-237): the lifecycle completeness gap for an implemented sw-req +/// must NAME the ASPICE chain — a `unit-verification` verifies a +/// `sw-detail-design`, not the `sw-req` directly, so authoring a direct link is +/// rejected and the bare "missing" list points at the wrong fix. The hint tells +/// the author to add the intermediate. +/// +/// rivet: verifies REQ-237 +#[test] +fn lifecycle_gap_names_the_aspice_verification_chain() { + let tmp = tempfile::tempdir().expect("temp dir"); + let dir = tmp.path(); + let dirs = dir.to_str().unwrap(); + std::fs::create_dir_all(dir.join("artifacts")).unwrap(); + std::fs::write( + dir.join("rivet.yaml"), + "project:\n name: p\n schemas: [common, aspice]\n\ + sources:\n - path: artifacts\n format: generic-yaml\n", + ) + .unwrap(); + // An implemented sw-req with an upstream link (so the gap lists specific + // missing verification types rather than "no downstream artifacts"). + std::fs::write( + dir.join("artifacts/a.yaml"), + "artifacts:\n \ + - id: SYS-001\n type: system-req\n title: sys\n status: approved\n \ + - id: SL-TR-003\n type: sw-req\n title: sw\n status: implemented\n \ + links:\n - type: derives-from\n target: SYS-001\n", + ) + .unwrap(); + + let out = Command::new(rivet_bin()) + .args(["--project", dirs, "validate"]) + .output() + .expect("validate"); + // The gap hints are emitted on stderr alongside the gap list. + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains("not `sw-req` directly") + && err.contains("sw-detail-design") + && err.contains("unit-verification"), + "the lifecycle gap must name the ASPICE chain for the sw-req; stderr:\n{err}" + ); +} + /// #620 (REQ-241): `rivet validate` (default salsa path) and /// `rivet validate --direct` (library path) must produce IDENTICAL results /// on the same project. A user reported them disagreeing — one flagging