use clap::{Arg, ArgMatches, Command};
use crate::domain::model::tag_filter::TagFilter;
use crate::domain::usecases::decision_record::brief::{
brief_decisions, BriefSource, DecisionBrief,
};
use crate::infra::driving::cli::errors::{die1, CliError};
use crate::infra::driving::cli::Context;
pub(in super::super) fn subcommand() -> Command {
Command::new("decisions")
.about("Brief the decisions of every record matching a tag")
.long_about(
"Walk every configured decision kind (ADR, DDR, …) and emit a \
Markdown brief of the matching records. When a record carries \
a `> [!DECISION]` alert, only that block is shown; otherwise \
the full body is included with a footer note.",
)
.arg(
Arg::new("tag")
.long("tag")
.help(
"Filter by tag (repeatable; AND across filters). \
Forms: `name`, `key:value`, or `key:` to match any value of that key.",
)
.value_name("PATTERN")
.action(clap::ArgAction::Append),
)
.arg(
Arg::new("all-statuses")
.long("all-statuses")
.help(
"Include every record regardless of status. By default only \
applicable decisions are listed (active and settled — \
`accepted` on the default preset); `proposed`, `rejected`, \
`deprecated`, `superseded` are filtered out.",
)
.action(clap::ArgAction::SetTrue),
)
}
pub(in super::super) fn execute(sub: &ArgMatches, ctx: &Context<'_>) {
let output_fmt = ctx.output_fmt;
let raw_tags: Vec<String> = sub
.get_many::<String>("tag")
.unwrap_or_default()
.cloned()
.collect();
let tag_filters: Vec<TagFilter> = raw_tags
.iter()
.map(|s| {
TagFilter::parse(s).unwrap_or_else(|e| {
die1(
CliError::new(format!("invalid tag filter '{s}': {e}")).kind("validation"),
output_fmt,
);
})
})
.collect();
let applicable_only = !sub.get_flag("all-statuses");
let dr_repos: Vec<_> = ctx
.config()
.decision_kinds
.iter()
.map(|kind_cfg| ctx.decision_record_repository(kind_cfg))
.collect();
let mut all_briefs: Vec<DecisionBrief> = Vec::new();
for repo in &dr_repos {
let mut briefs =
brief_decisions(repo, None, &tag_filters, applicable_only).unwrap_or_else(|e| {
die1(CliError::new(e.to_string()), output_fmt);
});
all_briefs.append(&mut briefs);
}
all_briefs.sort_by(|a, b| {
a.record
.kind
.as_str()
.cmp(b.record.kind.as_str())
.then_with(|| a.record.id.cmp(&b.record.id))
});
print_brief(&raw_tags, &all_briefs);
}
fn print_brief(raw_tags: &[String], briefs: &[DecisionBrief]) {
println!("{}", header(raw_tags, briefs.len()));
if briefs.is_empty() {
return;
}
for brief in briefs {
println!();
println!(
"## {} — {} ({})",
brief.record.id,
brief.record.title,
brief.record.status.as_str(),
);
let banner_lines = banner_lines(brief);
for line in &banner_lines {
println!();
println!("{line}");
}
println!();
match brief.source {
BriefSource::Marker if brief.content.is_empty() => println!("_(empty marker)_"),
BriefSource::Marker => println!("{}", brief.content),
BriefSource::Absent => println!(
"> _No `[!DECISION]` marker yet — read {} for the rationale._",
brief.record.id,
),
}
}
}
fn banner_lines(brief: &DecisionBrief) -> Vec<String> {
let mut lines = Vec::new();
if !brief.amended_by.is_empty() {
lines.push(format!("_Amended by {}._", join_refs(&brief.amended_by)));
}
if !brief.amends.is_empty() {
lines.push(format!("_Amends {}._", join_refs(&brief.amends)));
}
lines
}
fn join_refs(refs: &[crate::domain::model::entity_ref::EntityRef]) -> String {
refs.iter()
.map(|r| r.as_str().to_string())
.collect::<Vec<_>>()
.join(", ")
}
fn header(raw_tags: &[String], count: usize) -> String {
if raw_tags.is_empty() {
return format!("# Decisions ({count})");
}
let patterns: Vec<String> = raw_tags.iter().map(|s| format!("`{s}`")).collect();
format!("# Decisions matching {} ({count})", patterns.join(" AND "))
}