release_kit/commands/
assess.rs1use serde::Serialize;
9
10use crate::assess::{self, Classification, Evidence};
11use crate::cli::assess::AssessArgs;
12use crate::diagnostic::{Diagnostic, Reason};
13use crate::error::RkError;
14use crate::output::Output;
15
16#[derive(Debug, Serialize)]
18struct Report<'a> {
19 schema: &'static str,
21 target: String,
23 classification: Classification,
25 #[serde(flatten)]
27 evidence: &'a Evidence,
28 next: Vec<String>,
30}
31
32pub fn run(args: &AssessArgs) -> Result<(), RkError> {
40 let out = Output::new(args.json);
41 if !args.target.is_dir() {
42 return Err(RkError::missing(
43 Diagnostic::new(
44 Reason::TargetNotFound,
45 format!("target {} is not a directory", args.target),
46 )
47 .expected("an existing repository to classify"),
48 ));
49 }
50 let evidence = assess::gather(&args.target)?;
51 let classification = assess::classify(&evidence);
52 let next = next_lines(args, &evidence, classification);
53
54 out.result_line(format!("classification: {}", classification.as_str()));
55 out.result_line(format!(
56 "landing: {}",
57 evidence.landing.rk_version.as_deref().map_or_else(
58 || "none".to_owned(),
59 |version| format!("recorded at release-kit {version}")
60 )
61 ));
62 out.result_line(format!("tech: {}", evidence.tech.unwrap_or("undetected")));
63 out.result_line(format!(
64 "forge: {}",
65 match (evidence.forge, evidence.repo.as_deref()) {
66 (Some(forge), Some(repo)) => format!("{forge} ({repo})"),
67 (Some(forge), None) => forge.to_owned(),
68 (None, _) => "undetected".to_owned(),
69 }
70 ));
71 out.result_line(format!(
72 "release markers: {}",
73 join_or_none(&evidence.release_markers)
74 ));
75 out.result_line(format!(
76 "payload collisions: {}",
77 join_or_none(&evidence.collisions)
78 ));
79 if evidence.git {
80 out.result_line(format!("tags: {}", evidence.tags));
81 out.result_line(format!(
82 "long-lived branches: {}",
83 join_or_none(&evidence.long_lived_branches)
84 ));
85 } else {
86 out.result_line("git: not a repository");
87 }
88 out.next(&next);
89
90 out.emit(&Report {
91 schema: "rk.assess/1",
92 target: args.target.to_string(),
93 classification,
94 evidence: &evidence,
95 next,
96 })
97}
98
99fn next_lines(args: &AssessArgs, evidence: &Evidence, verdict: Classification) -> Vec<String> {
103 let target = &args.target;
104 if evidence.landing.recorded {
105 return vec![
106 format!(
107 "rk status --target {target} reports this landing; a recorded target routes by its status, not by classification"
108 ),
109 format!("rk upgrade --target {target} takes it to this binary's payload"),
110 ];
111 }
112 match verdict {
113 Classification::Greenfield => vec![
114 format!("rk init --tech <tech> --target {target} lands the workflow; nothing is here to migrate"),
115 ],
116 Classification::Brownfield => vec![
117 "rk guide migration carries the migration procedure".to_owned(),
118 format!("rk adopt --target {target} previews whether what is here matches one rendered candidate"),
119 format!("rk setup check --target {target} reports what the forge already enforces"),
120 ],
121 Classification::NeedsDecision => vec![
122 "the operator says what the release activity is before any plan claims to know; rk guide migration carries the procedure once it is a migration".to_owned(),
123 ],
124 }
125}
126
127fn join_or_none(items: &[String]) -> String {
128 if items.is_empty() {
129 "none".to_owned()
130 } else {
131 items.join(", ")
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 #![allow(clippy::expect_used)]
138
139 use super::Report;
140 use crate::assess::{Classification, Evidence, Landing};
141
142 #[test]
144 fn the_assess_report_schema_snapshot_holds() {
145 let evidence = Evidence {
146 landing: Landing {
147 recorded: false,
148 rk_version: None,
149 },
150 tech: Some("rust"),
151 forge: Some("github"),
152 repo: Some("acme/widget".into()),
153 release_markers: vec!["CHANGELOG.md".into()],
154 collisions: vec!["release-plz.toml".into()],
155 git: true,
156 tags: 3,
157 long_lived_branches: vec!["develop".into()],
158 };
159 let report = Report {
160 schema: "rk.assess/1",
161 target: "/tmp/t".into(),
162 classification: Classification::Brownfield,
163 evidence: &evidence,
164 next: vec!["rk guide migration carries the migration procedure".into()],
165 };
166 assert_eq!(
167 serde_json::to_string(&report).expect("a report serializes"),
168 r#"{"schema":"rk.assess/1","target":"/tmp/t","classification":"brownfield","landing":{"recorded":false},"tech":"rust","forge":"github","repo":"acme/widget","release_markers":["CHANGELOG.md"],"collisions":["release-plz.toml"],"git":true,"tags":3,"long_lived_branches":["develop"],"next":["rk guide migration carries the migration procedure"]}"#
169 );
170 }
171}