use crate::audit::Audit;
use crate::project::Project;
use crate::runner::HelpOutput;
use crate::types::{AuditGroup, AuditLayer, AuditResult, AuditStatus, Confidence};
const EXTRA_FORMATS: &[&str] = &["csv", "tsv", "yaml", "yml", "toml", "xml", "ndjson"];
pub struct MoreFormatsAudit;
impl Audit for MoreFormatsAudit {
fn id(&self) -> &str {
"p2-more-formats"
}
fn label(&self) -> &'static str {
"`--output` advertises additional formats beyond text/json"
}
fn group(&self) -> AuditGroup {
AuditGroup::P2
}
fn layer(&self) -> AuditLayer {
AuditLayer::Behavioral
}
fn covers(&self) -> &'static [&'static str] {
&["p2-may-more-formats"]
}
fn applicable(&self, project: &Project) -> bool {
project.runner.is_some()
}
fn run(&self, project: &Project) -> anyhow::Result<AuditResult> {
let status = match project.help_output() {
None => AuditStatus::Skip("could not probe --help".into()),
Some(help) => audit_more_formats(help),
};
Ok(AuditResult {
id: self.id().to_string(),
label: self.label().into(),
group: self.group(),
layer: self.layer(),
status,
confidence: Confidence::Medium,
})
}
}
pub(crate) fn audit_more_formats(help: &HelpOutput) -> AuditStatus {
let has_output_flag = help
.flags()
.iter()
.any(|f| f.matches("--output") || f.matches("--format"));
if !has_output_flag {
return AuditStatus::Skip(
"no `--output` or `--format` flag advertised; vacuous skip for MAY-tier extra formats."
.into(),
);
}
let raw_lower = help.raw().to_lowercase();
let found: Vec<&&str> = EXTRA_FORMATS
.iter()
.filter(|fmt| raw_lower.contains(*fmt.to_owned()))
.collect();
if found.is_empty() {
AuditStatus::Warn(format!(
"`--output` / `--format` flag found but no extra format tokens detected \
(looked for {}). MAY-tier — additional formats let downstream agents \
pick the shape that matches their pipeline.",
EXTRA_FORMATS.join(", "),
))
} else {
AuditStatus::Pass
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skip_when_no_output_flag() {
let help = HelpOutput::from_raw("Options:\n -h, --help Show help.\n");
match audit_more_formats(&help) {
AuditStatus::Skip(msg) => assert!(msg.contains("vacuous")),
other => panic!("expected Skip, got {other:?}"),
}
}
#[test]
fn warn_when_only_core_formats() {
let help = HelpOutput::from_raw(
"Options:\n --output <FMT> [possible values: text, json]\n -h, --help\n",
);
match audit_more_formats(&help) {
AuditStatus::Warn(msg) => assert!(msg.contains("extra format")),
other => panic!("expected Warn, got {other:?}"),
}
}
#[test]
fn pass_when_yaml_present() {
let help = HelpOutput::from_raw(
"Options:\n --output <FMT> [possible values: text, json, yaml]\n -h, --help\n",
);
assert_eq!(audit_more_formats(&help), AuditStatus::Pass);
}
#[test]
fn pass_when_csv_present() {
let help = HelpOutput::from_raw(
"Options:\n --output <FMT> [possible values: text, json, csv]\n -h, --help\n",
);
assert_eq!(audit_more_formats(&help), AuditStatus::Pass);
}
}