@@ -454,6 +454,25 @@ enum Command {
454454 id: String,
455455 },
456456
457+ /// Trace a requirement FORWARD to the test results that cover it (#547).
458+ /// Walks backlinks (what verifies it, possibly multi-hop through the ASPICE
459+ /// V) and reports each reached artifact with its latest test-result status,
460+ /// plus a roll-up verdict. Text or JSON — the JSON is what the `rivet serve`
461+ /// graphical trace view consumes.
462+ #[command(name = "trace-results")]
463+ TraceResults {
464+ /// Requirement (or any artifact) ID to trace forward from.
465+ id: String,
466+
467+ /// Maximum hops to walk (default: 4 — the ASPICE V depth).
468+ #[arg(long, default_value = "4")]
469+ depth: usize,
470+
471+ /// Output format: "text" (default) or "json".
472+ #[arg(short, long, default_value = "text")]
473+ format: String,
474+ },
475+
457476 /// Bundle an artifact and its link-graph closure as a single pasteable document
458477 Bundle {
459478 /// Root artifact ID
@@ -2370,6 +2389,7 @@ fn run(cli: Cli) -> Result<bool> {
23702389 // per-artifact traceability view; it renders the same as
23712390 // `validate --explain <id>` (which stays as an alias).
23722391 Command::Trace { id } => cmd_explain(&cli, id),
2392+ Command::TraceResults { id, depth, format } => cmd_trace_results(&cli, id, *depth, format),
23732393 Command::Bundle {
23742394 id,
23752395 depth,
@@ -7709,6 +7729,69 @@ fn cmd_check_verification_evidence(
77097729 Ok(missing.is_empty())
77107730}
77117731
7732+ /// #547 (REQ-238): trace a requirement FORWARD to the test results that cover
7733+ /// it — the reverse of the authored `verifies` direction. Text tree or JSON
7734+ /// (the JSON is what the `rivet serve` graphical trace view consumes). Exits
7735+ /// non-zero only when a covering test recorded a FAILING result, so it is
7736+ /// usable as a per-requirement gate.
7737+ fn cmd_trace_results(cli: &Cli, id: &str, depth: usize, format: &str) -> Result<bool> {
7738+ use rivet_core::result_trace::{TraceVerdict, trace_test_results, verdict};
7739+ use rivet_core::results::{ResultStore, TestStatus};
7740+ validate_format(format, &["text", "json"])?;
7741+ let ctx = ProjectContext::load_full(cli)?;
7742+ ctx.warn_parse_error_skips(cli);
7743+ if !ctx.store.contains(id) {
7744+ anyhow::bail!("artifact '{id}' not found");
7745+ }
7746+ let empty = ResultStore::new();
7747+ let results = ctx.result_store.as_ref().unwrap_or(&empty);
7748+ let nodes = trace_test_results(id, &ctx.graph, results, depth);
7749+ let v = verdict(&nodes);
7750+ let ok = !matches!(v, TraceVerdict::Failing);
7751+
7752+ let badge = |s: Option<&TestStatus>| match s {
7753+ Some(TestStatus::Pass) => "pass",
7754+ Some(TestStatus::Fail) => "FAIL",
7755+ Some(TestStatus::Error) => "ERROR",
7756+ Some(TestStatus::Skip) => "skip",
7757+ Some(TestStatus::Blocked) => "blocked",
7758+ None => "·",
7759+ };
7760+
7761+ if format == "json" {
7762+ println!(
7763+ "{}",
7764+ serde_json::to_string_pretty(&serde_json::json!({
7765+ "command": "trace-results",
7766+ "root": id,
7767+ "verdict": v,
7768+ "nodes": nodes,
7769+ }))?
7770+ );
7771+ } else {
7772+ let verdict_str = match v {
7773+ TraceVerdict::Passing => "\u{2713} passing",
7774+ TraceVerdict::Failing => "\u{2717} failing",
7775+ TraceVerdict::NoEvidence => "\u{2014} no test evidence",
7776+ };
7777+ println!("Test-result trace for {id}: {verdict_str}");
7778+ if nodes.is_empty() {
7779+ println!(" (nothing traces to {id})");
7780+ }
7781+ for n in &nodes {
7782+ let indent = " ".repeat(n.distance);
7783+ println!(
7784+ "{indent}{} --{}--> {} [{}]",
7785+ n.artifact_id,
7786+ n.link_type,
7787+ n.via_target,
7788+ badge(n.status.as_ref())
7789+ );
7790+ }
7791+ }
7792+ Ok(ok)
7793+ }
7794+
77127795/// #559: advance an artifact to `verified` when it has verifying evidence —
77137796/// an incoming `verifies` link, OR a `// rivet: verifies <ID>` source marker.
77147797/// Opt-in and auditable (no auto-advance); the artifact must be `implemented`.
@@ -15271,7 +15354,6 @@ impl ProjectContext {
1527115354 }
1527215355
1527315356 /// Load project with artifacts, schema, link graph, documents, and test results.
15274- #[allow(dead_code)]
1527515357 fn load_full(cli: &Cli) -> Result<Self> {
1527615358 let mut ctx = Self::load_with_docs(cli)?;
1527715359
0 commit comments