use crate::checks::{Fact, FactKind, Scope, Status};
use crate::scan::ScanMode;
pub struct ScanSummary {
pub global_tools: Vec<Fact>,
pub local_requirements: Vec<Fact>,
pub recommendations: Vec<Message>,
pub warnings: Vec<Message>,
}
pub struct Message {
pub text: String,
pub evidence: Option<String>,
}
pub fn interpret(mode: ScanMode, facts: Vec<Fact>) -> ScanSummary {
let global_tools: Vec<Fact> = facts
.iter()
.filter(|fact| {
fact.scope == Scope::Global
&& fact.kind == FactKind::Tool
&& fact.status == Status::Found
})
.cloned()
.collect();
let local_requirements: Vec<Fact> = facts
.iter()
.filter(|fact| fact.scope == Scope::Local && fact.kind == FactKind::Requirement)
.cloned()
.collect();
let recommendations = build_recommendations(&facts, &local_requirements);
let warnings = build_warnings(mode, &global_tools, &local_requirements);
ScanSummary {
global_tools,
local_requirements,
recommendations,
warnings,
}
}
fn build_recommendations(facts: &[Fact], local_requirements: &[Fact]) -> Vec<Message> {
let mut recommendations = Vec::new();
if let Some(package_manager) = choose_package_manager(local_requirements) {
add_message(
&mut recommendations,
format!(
"Use {} for package commands.",
package_manager.tool_name.as_deref().unwrap_or("unknown")
),
package_manager.evidence.clone(),
);
}
for requirement in local_requirements {
for guardrail in &requirement.guardrails {
add_message(
&mut recommendations,
guardrail.clone(),
requirement.evidence.clone(),
);
}
}
if facts
.iter()
.any(|fact| fact.id.as_deref() == Some("secret-like-env"))
{
add_message(
&mut recommendations,
"Do not print raw environment variables; redact secret-like values.".to_string(),
Some("secret-like env names detected".to_string()),
);
}
recommendations
}
fn build_warnings(
mode: ScanMode,
global_tools: &[Fact],
local_requirements: &[Fact],
) -> Vec<Message> {
let mut warnings = Vec::new();
if mode != ScanMode::Local {
for requirement in local_requirements {
let tool_name = requirement.tool_name.as_deref().unwrap_or_default();
let available = global_tools
.iter()
.any(|tool| tool.tool_name.as_deref() == Some(tool_name));
if requirement.requires_global_command && !available {
add_message(
&mut warnings,
format!(
"Project expects {}, but no matching command was found globally.",
requirement.label
),
requirement.evidence.clone(),
);
}
}
}
let package_managers: Vec<&str> = local_requirements
.iter()
.filter(|requirement| requirement.category.as_deref() == Some("javascript-package-manager"))
.filter_map(|requirement| requirement.tool_name.as_deref())
.collect();
if package_managers.contains(&"pnpm") {
add_message(
&mut warnings,
"This project indicates pnpm; avoid npm install unless the user explicitly asks."
.to_string(),
Some("pnpm project evidence".to_string()),
);
}
warnings
}
fn choose_package_manager(local_requirements: &[Fact]) -> Option<&Fact> {
["pnpm", "yarn", "bun", "npm"].iter().find_map(|tool_name| {
local_requirements
.iter()
.find(|requirement| requirement.tool_name.as_deref() == Some(*tool_name))
})
}
fn add_message(messages: &mut Vec<Message>, text: String, evidence: Option<String>) {
if !messages.iter().any(|message| message.text == text) {
messages.push(Message { text, evidence });
}
}