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