use anyhow::Result;
use serde_json::json;
use std::path::PathBuf;
use std::process::Command;
use crate::cli::BriefArgs;
use crate::model::{Project, Status};
use crate::storage::load_resolved;
pub fn run(args: BriefArgs, file: &Option<PathBuf>) -> Result<()> {
let (_, project) = load_resolved(file)?;
let snap = snapshot(&project);
if args.json {
println!("{}", serde_json::to_string_pretty(&snap.to_json())?);
return Ok(());
}
if args.full {
print!("{}", snap.to_full(&project));
} else {
print!("{}", snap.to_short(&project));
}
Ok(())
}
struct Snapshot {
name: String,
purpose: Option<String>,
total: usize,
by_status: [usize; 6],
delivery_pct: f64,
next_pick: Option<(String, String, String, String)>, implemented_unverified: Vec<String>,
drafts: Vec<String>,
top_verified_must: Vec<(String, String)>,
verified_but_defective: Vec<String>,
sil_escalated: Vec<String>,
hook_mode: Option<String>,
last_change: Option<String>,
}
fn snapshot(project: &Project) -> Snapshot {
let total = project.requirements.len();
let mut by_status = [0usize; 6];
for r in project.requirements.values() {
let i = match r.status {
Status::Draft => 0,
Status::Proposed => 1,
Status::Approved => 2,
Status::Implemented => 3,
Status::Verified => 4,
Status::Obsolete => 5,
};
by_status[i] += 1;
}
let non_obsolete = total - by_status[5];
let done = by_status[3] + by_status[4];
let delivery_pct = if non_obsolete == 0 {
0.0
} else {
100.0 * done as f64 / non_obsolete as f64
};
let mut candidates: Vec<&crate::model::Requirement> = project
.requirements
.values()
.filter(|r| !matches!(r.status, Status::Verified | Status::Obsolete))
.collect();
candidates.sort_by_key(|r| {
use crate::model::Priority;
let p = match r.priority {
Priority::Must => 0,
Priority::Should => 1,
Priority::Could => 2,
Priority::Wont => 3,
};
let s = match r.status {
Status::Draft => 0,
Status::Proposed => 1,
Status::Approved => 2,
Status::Implemented => 3,
_ => 9,
};
(p, s, r.id.clone())
});
let next_pick = candidates.first().map(|r| {
(
r.id.clone(),
r.title.clone(),
r.status.as_str().to_string(),
r.priority.as_str().to_string(),
)
});
let mut implemented_unverified: Vec<String> = project
.requirements
.iter()
.filter(|(_, r)| matches!(r.status, Status::Implemented))
.map(|(id, _)| id.clone())
.collect();
implemented_unverified.sort();
let mut drafts: Vec<String> = project
.requirements
.iter()
.filter(|(_, r)| matches!(r.status, Status::Draft))
.map(|(id, _)| id.clone())
.collect();
drafts.sort();
let hook_mode = detect_hook_mode();
let last_change = detect_last_spec_change();
use crate::model::Priority;
let mut top_verified_must: Vec<(String, String)> = project
.requirements
.iter()
.filter(|(_, r)| {
matches!(r.status, Status::Verified) && matches!(r.priority, Priority::Must)
})
.map(|(id, r)| (id.clone(), r.title.clone()))
.collect();
top_verified_must.sort_by(|a, b| a.0.cmp(&b.0));
top_verified_must.truncate(3);
let verified_but_defective = crate::commands::status::verified_but_defective(project);
let mut sil_escalated: Vec<String> = project
.sil_escalated_srs()
.into_iter()
.map(|(id, ev, cur)| format!("{} ({} → {})", id, ev.as_str(), cur.as_str()))
.collect();
sil_escalated.sort();
Snapshot {
name: project.name.clone(),
purpose: project.purpose.clone(),
total,
by_status,
delivery_pct,
next_pick,
implemented_unverified,
drafts,
top_verified_must,
verified_but_defective,
sil_escalated,
hook_mode,
last_change,
}
}
fn detect_hook_mode() -> Option<String> {
let body = std::fs::read_to_string(".git/hooks/pre-commit").ok()?;
if body.contains("# mode: strict") {
Some("strict".into())
} else if body.contains("# mode: default") {
Some("default".into())
} else if body.contains("# managed-by: req-hooks") {
Some("managed (mode unknown)".into())
} else {
None
}
}
fn detect_last_spec_change() -> Option<String> {
let out = Command::new("git")
.args(["log", "-1", "--format=%cr", "--", "project.req"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() {
None
} else {
Some(s)
}
}
impl Snapshot {
fn to_short(&self, _project: &Project) -> String {
let mut out = String::new();
if let Some(p) = &self.purpose {
out.push_str(p);
out.push_str("\n\n");
}
if !self.top_verified_must.is_empty() {
out.push_str("spine:\n");
for (id, title) in &self.top_verified_must {
let title_short: String = title.chars().take(60).collect();
out.push_str(&format!(" - {} — {}\n", id, title_short));
}
}
out.push_str(&format!(
"req brief: {} — {} req(s), {:.0}% delivered",
self.name, self.total, self.delivery_pct
));
if self.by_status[0] > 0 {
out.push_str(&format!(", {} draft", self.by_status[0]));
}
out.push_str(".\n");
match &self.next_pick {
Some((id, title, status, priority)) => {
let title_short: String = title.chars().take(60).collect();
out.push_str(&format!(
" next : {} [{} / {}] — {}\n",
id, priority, status, title_short
));
}
None => out.push_str(" next : nothing queued. Add one with `req add` or relax filters with `req next`.\n"),
}
if !self.verified_but_defective.is_empty() {
let preview = self
.verified_but_defective
.iter()
.take(3)
.cloned()
.collect::<Vec<_>>()
.join(", ");
let more = if self.verified_but_defective.len() > 3 {
format!(" +{} more", self.verified_but_defective.len() - 3)
} else {
String::new()
};
out.push_str(&format!(
" DEFECT: {} verified req(s) have a failing latest test record — {}{}\n",
self.verified_but_defective.len(),
preview,
more
));
}
if !self.sil_escalated.is_empty() {
out.push_str(&format!(
" SIL ESCALATED: {} safety req(s) inherit a higher SIL than their evidence — re-verify: {}\n",
self.sil_escalated.len(),
self.sil_escalated.join(", ")
));
}
if !self.implemented_unverified.is_empty() {
let preview = self
.implemented_unverified
.iter()
.take(3)
.cloned()
.collect::<Vec<_>>()
.join(", ");
let more = if self.implemented_unverified.len() > 3 {
format!(" +{} more", self.implemented_unverified.len() - 3)
} else {
String::new()
};
out.push_str(&format!(
" loose: {} implemented but not verified — {}{}\n",
self.implemented_unverified.len(),
preview,
more
));
}
out.push_str(" `req brief --full` for the dashboard · `req next` to start work\n");
out
}
fn to_full(&self, project: &Project) -> String {
let mut out = String::new();
out.push_str(&format!("# req brief — {}\n\n", self.name));
if let Some(p) = &self.purpose {
out.push_str("## Purpose\n\n");
out.push_str(p);
out.push_str("\n\n");
}
out.push_str(&format!(
"**{} requirements · {:.1}% delivered**\n\n",
self.total, self.delivery_pct
));
out.push_str("## Status breakdown\n\n");
let labels = [
"draft",
"proposed",
"approved",
"implemented",
"verified",
"obsolete",
];
for (i, lbl) in labels.iter().enumerate() {
if self.by_status[i] > 0 {
out.push_str(&format!(" - {:>12}: {}\n", lbl, self.by_status[i]));
}
}
out.push('\n');
out.push_str("## Suggested next\n\n");
match &self.next_pick {
Some((id, title, status, priority)) => out.push_str(&format!(
" **{}** — {}\n status: {} · priority: {}\n",
id, title, status, priority
)),
None => out.push_str(" nothing queued.\n"),
}
out.push('\n');
if !self.drafts.is_empty() {
out.push_str(&format!("## Drafts ({})\n\n", self.drafts.len()));
for id in &self.drafts {
if let Some(r) = project.requirements.get(id) {
out.push_str(&format!(" - {} — {}\n", id, r.title));
}
}
out.push('\n');
}
if !self.implemented_unverified.is_empty() {
out.push_str(&format!(
"## Implemented but not Verified ({})\n\n next step on each: `req verify <id> --by inspection --notes \"...\" --promote`\n\n",
self.implemented_unverified.len()
));
for id in &self.implemented_unverified {
if let Some(r) = project.requirements.get(id) {
out.push_str(&format!(" - {} — {}\n", id, r.title));
}
}
out.push('\n');
}
if !self.sil_escalated.is_empty() {
out.push_str(&format!(
"## SIL escalated ({})\n\n these Verified safety req(s) inherit a higher SIL than their evidence was justified against — re-verify at the current level\n\n",
self.sil_escalated.len()
));
for s in &self.sil_escalated {
out.push_str(&format!(" - {}\n", s));
}
out.push('\n');
}
out.push_str("## Tooling\n\n");
out.push_str(&format!(
" pre-commit hook: {}\n",
self.hook_mode.as_deref().unwrap_or("not installed")
));
out.push_str(&format!(
" last spec change: {}\n",
self.last_change.as_deref().unwrap_or("(no git history)")
));
out.push_str("\n `req lint` for quality audit · `req review` for PR-style report\n");
out
}
fn to_json(&self) -> serde_json::Value {
json!({
"project": self.name,
"purpose": self.purpose,
"spine": self.top_verified_must.iter().map(|(id, t)| json!({ "id": id, "title": t })).collect::<Vec<_>>(),
"total": self.total,
"by_status": {
"draft": self.by_status[0],
"proposed": self.by_status[1],
"approved": self.by_status[2],
"implemented": self.by_status[3],
"verified": self.by_status[4],
"obsolete": self.by_status[5],
},
"delivery_pct": (self.delivery_pct * 10.0).round() / 10.0,
"next": self.next_pick.as_ref().map(|(id, title, status, priority)| {
json!({ "id": id, "title": title, "status": status, "priority": priority })
}),
"implemented_unverified": self.implemented_unverified,
"drafts": self.drafts,
"verified_but_defective": self.verified_but_defective,
"sil_escalated": self.sil_escalated,
"hook_mode": self.hook_mode,
"last_spec_change": self.last_change,
})
}
}