use crate::params::{
AnalyzeParams, AuditParams, CheckChangedParams, CheckRuntimeCoverageParams, CombinedParams,
ExplainParams, FeatureFlagsParams, FindDupesParams, HealthParams, ImpactParams,
ListBoundariesParams, ProjectInfoParams, SecurityCandidatesParams, TraceCloneParams,
TraceDependencyParams, TraceExportParams, TraceFileParams,
};
use fallow_api::{
AnalysisOptions, CombinedOptions, ComplexityOptions, DuplicationMode, DuplicationOptions,
RootEnvelopeMode, run_combined, serialize_combined_programmatic_json,
serialize_explain_programmatic_json,
};
use super::super::{
analyze::run_analyze_api_value,
api_runtime::{changed_since_from_param, env_diff_file, non_empty_path, non_empty_string},
audit::run_audit_api_value,
build_analyze_args, build_audit_args, build_check_changed_args,
build_check_runtime_coverage_args, build_explain_args, build_feature_flags_args,
build_find_dupes_args, build_get_blast_radius_args, build_get_cleanup_candidates_args,
build_get_hot_paths_args, build_get_importance_args, build_health_args, build_impact_args,
build_list_boundaries_args, build_project_info_args, build_security_candidates_args,
build_trace_clone_args, build_trace_dependency_args, build_trace_export_args,
build_trace_file_args,
check_changed::run_check_changed_api_value,
dupes::run_find_dupes_api_value,
flags::run_feature_flags_api_value,
health::run_health_api_value,
list_boundaries::run_list_boundaries_api_value,
project_info::run_project_info_api_value,
push_global,
trace::{
run_trace_clone_api_value, run_trace_dependency_api_value, run_trace_export_api_value,
run_trace_file_api_value,
},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum CodeModeTool {
Analyze,
Combined,
CheckChanged,
SecurityCandidates,
FindDupes,
ProjectInfo,
TraceExport,
TraceFile,
TraceDependency,
TraceClone,
CheckHealth,
Audit,
FallowExplain,
ListBoundaries,
FeatureFlags,
Impact,
CheckRuntimeCoverage,
GetHotPaths,
GetBlastRadius,
GetImportance,
GetCleanupCandidates,
}
impl CodeModeTool {
pub(super) fn from_name(name: &str) -> Result<Self, String> {
match name {
"analyze" => Ok(Self::Analyze),
"combined" => Ok(Self::Combined),
"check_changed" => Ok(Self::CheckChanged),
"security_candidates" => Ok(Self::SecurityCandidates),
"find_dupes" => Ok(Self::FindDupes),
"project_info" => Ok(Self::ProjectInfo),
"trace_export" => Ok(Self::TraceExport),
"trace_file" => Ok(Self::TraceFile),
"trace_dependency" => Ok(Self::TraceDependency),
"trace_clone" => Ok(Self::TraceClone),
"check_health" => Ok(Self::CheckHealth),
"audit" => Ok(Self::Audit),
"fallow_explain" => Ok(Self::FallowExplain),
"list_boundaries" => Ok(Self::ListBoundaries),
"feature_flags" => Ok(Self::FeatureFlags),
"impact" => Ok(Self::Impact),
"check_runtime_coverage" => Ok(Self::CheckRuntimeCoverage),
"get_hot_paths" => Ok(Self::GetHotPaths),
"get_blast_radius" => Ok(Self::GetBlastRadius),
"get_importance" => Ok(Self::GetImportance),
"get_cleanup_candidates" => Ok(Self::GetCleanupCandidates),
"fix_preview" | "fix_apply" => Err(
"code mode does not expose fix tools; use standalone MCP tools for previews"
.to_string(),
),
_ => Err(format!("unsupported code mode fallow tool '{name}'")),
}
}
pub(super) fn name(self) -> &'static str {
match self {
Self::Analyze => "analyze",
Self::Combined => "combined",
Self::CheckChanged => "check_changed",
Self::SecurityCandidates => "security_candidates",
Self::FindDupes => "find_dupes",
Self::ProjectInfo => "project_info",
Self::TraceExport => "trace_export",
Self::TraceFile => "trace_file",
Self::TraceDependency => "trace_dependency",
Self::TraceClone => "trace_clone",
Self::CheckHealth => "check_health",
Self::Audit => "audit",
Self::FallowExplain => "fallow_explain",
Self::ListBoundaries => "list_boundaries",
Self::FeatureFlags => "feature_flags",
Self::Impact => "impact",
Self::CheckRuntimeCoverage => "check_runtime_coverage",
Self::GetHotPaths => "get_hot_paths",
Self::GetBlastRadius => "get_blast_radius",
Self::GetImportance => "get_importance",
Self::GetCleanupCandidates => "get_cleanup_candidates",
}
}
pub(super) fn is_api_backed(self) -> bool {
API_BACKED_CODE_MODE_TOOLS.contains(&self)
}
pub(super) fn is_code_mode_api_backed(self) -> bool {
self.is_api_backed()
&& !matches!(
self,
Self::Analyze | Self::FindDupes | Self::CheckHealth | Self::Audit
)
}
}
pub(super) const CODE_MODE_ALIASES: &[(&str, &str)] = &[
("analyze", "analyze"),
("combined", "combined"),
("checkChanged", "check_changed"),
("securityCandidates", "security_candidates"),
("findDupes", "find_dupes"),
("projectInfo", "project_info"),
("traceExport", "trace_export"),
("traceFile", "trace_file"),
("traceDependency", "trace_dependency"),
("traceClone", "trace_clone"),
("checkHealth", "check_health"),
("audit", "audit"),
("explain", "fallow_explain"),
("listBoundaries", "list_boundaries"),
("featureFlags", "feature_flags"),
("impact", "impact"),
("checkRuntimeCoverage", "check_runtime_coverage"),
("getHotPaths", "get_hot_paths"),
("getBlastRadius", "get_blast_radius"),
("getImportance", "get_importance"),
("getCleanupCandidates", "get_cleanup_candidates"),
];
pub(super) const API_BACKED_CODE_MODE_TOOLS: &[CodeModeTool] = &[
CodeModeTool::Analyze,
CodeModeTool::Combined,
CodeModeTool::CheckChanged,
CodeModeTool::FindDupes,
CodeModeTool::ProjectInfo,
CodeModeTool::TraceExport,
CodeModeTool::TraceFile,
CodeModeTool::TraceDependency,
CodeModeTool::TraceClone,
CodeModeTool::CheckHealth,
CodeModeTool::Audit,
CodeModeTool::FallowExplain,
CodeModeTool::ListBoundaries,
CodeModeTool::FeatureFlags,
];
pub(super) fn merge_default_root(
params_json: &str,
default_root: Option<&str>,
) -> Result<serde_json::Value, String> {
let mut params: serde_json::Value =
serde_json::from_str(params_json).map_err(|err| format!("invalid params JSON: {err}"))?;
if !params.is_object() {
return Err("fallow host call params must be an object".to_string());
}
if let Some(root) = default_root
&& params.get("root").is_none()
&& let Some(object) = params.as_object_mut()
{
object.insert(
"root".to_string(),
serde_json::Value::String(root.to_string()),
);
}
Ok(params)
}
pub(super) fn run_api_tool(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Option<serde_json::Value>, String> {
if !tool.is_api_backed() {
return Ok(None);
}
match tool {
CodeModeTool::Analyze
| CodeModeTool::Combined
| CodeModeTool::CheckChanged
| CodeModeTool::FindDupes
| CodeModeTool::ProjectInfo => run_analysis_api_tool(tool, params),
CodeModeTool::TraceExport
| CodeModeTool::TraceFile
| CodeModeTool::TraceDependency
| CodeModeTool::TraceClone => run_trace_api_tool(tool, params),
CodeModeTool::CheckHealth
| CodeModeTool::Audit
| CodeModeTool::FallowExplain
| CodeModeTool::FeatureFlags
| CodeModeTool::ListBoundaries => run_report_api_tool(tool, params),
CodeModeTool::SecurityCandidates
| CodeModeTool::Impact
| CodeModeTool::CheckRuntimeCoverage
| CodeModeTool::GetHotPaths
| CodeModeTool::GetBlastRadius
| CodeModeTool::GetImportance
| CodeModeTool::GetCleanupCandidates => unreachable!(
"{} is not API-backed and should have returned before dispatch",
tool.name()
),
}
}
fn run_analysis_api_tool(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Option<serde_json::Value>, String> {
match tool {
CodeModeTool::Analyze => {
let params: AnalyzeParams = parse_params(params)?;
run_analyze_api_value(¶ms)
}
CodeModeTool::Combined => {
let params: CombinedParams = parse_params(params)?;
run_combined_api_value(¶ms)
}
CodeModeTool::CheckChanged => {
let params: CheckChangedParams = parse_params(params)?;
run_check_changed_api_value(¶ms)
}
CodeModeTool::FindDupes => {
let params: FindDupesParams = parse_params(params)?;
run_find_dupes_api_value(¶ms)
}
CodeModeTool::ProjectInfo => {
let params: ProjectInfoParams = parse_params(params)?;
run_project_info_api_value(¶ms)
}
_ => unreachable!("analysis API helper called with {}", tool.name()),
}
}
fn run_trace_api_tool(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Option<serde_json::Value>, String> {
match tool {
CodeModeTool::TraceExport => {
let params: TraceExportParams = parse_params(params)?;
run_trace_export_api_value(¶ms).map(Some)
}
CodeModeTool::TraceFile => {
let params: TraceFileParams = parse_params(params)?;
run_trace_file_api_value(¶ms).map(Some)
}
CodeModeTool::TraceDependency => {
let params: TraceDependencyParams = parse_params(params)?;
run_trace_dependency_api_value(¶ms).map(Some)
}
CodeModeTool::TraceClone => {
let params: TraceCloneParams = parse_params(params)?;
run_trace_clone_api_value(¶ms).map(Some)
}
_ => unreachable!("trace API helper called with {}", tool.name()),
}
}
fn run_report_api_tool(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Option<serde_json::Value>, String> {
match tool {
CodeModeTool::CheckHealth => {
let params: HealthParams = parse_params(params)?;
run_health_api_value(¶ms)
}
CodeModeTool::Audit => {
let params: AuditParams = parse_params(params)?;
run_audit_api_value(¶ms)
}
CodeModeTool::FallowExplain => {
let params: ExplainParams = parse_params(params)?;
serialize_explain_programmatic_json(¶ms.issue_type, RootEnvelopeMode::Tagged, None)
.map(Some)
.map_err(|error| error.message)
}
CodeModeTool::FeatureFlags => {
let params: FeatureFlagsParams = parse_params(params)?;
run_feature_flags_api_value(¶ms)
}
CodeModeTool::ListBoundaries => {
let params: ListBoundariesParams = parse_params(params)?;
run_list_boundaries_api_value(¶ms)
}
_ => unreachable!("report API helper called with {}", tool.name()),
}
}
pub(super) fn build_tool_args(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Vec<String>, String> {
match tool {
CodeModeTool::Analyze
| CodeModeTool::Combined
| CodeModeTool::CheckChanged
| CodeModeTool::SecurityCandidates
| CodeModeTool::FindDupes
| CodeModeTool::ProjectInfo => build_project_tool_args(tool, params),
CodeModeTool::TraceExport
| CodeModeTool::TraceFile
| CodeModeTool::TraceDependency
| CodeModeTool::TraceClone => build_trace_tool_args(tool, params),
CodeModeTool::CheckHealth
| CodeModeTool::Audit
| CodeModeTool::FallowExplain
| CodeModeTool::ListBoundaries
| CodeModeTool::FeatureFlags
| CodeModeTool::Impact => build_health_and_config_tool_args(tool, params),
CodeModeTool::CheckRuntimeCoverage
| CodeModeTool::GetHotPaths
| CodeModeTool::GetBlastRadius
| CodeModeTool::GetImportance
| CodeModeTool::GetCleanupCandidates => build_runtime_coverage_tool_args(tool, params),
}
}
fn build_project_tool_args(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Vec<String>, String> {
match tool {
CodeModeTool::Analyze => {
let params: AnalyzeParams = parse_params(params)?;
build_analyze_args(¶ms)
}
CodeModeTool::Combined => {
let params: CombinedParams = parse_params(params)?;
Ok(build_combined_args(¶ms))
}
CodeModeTool::CheckChanged => {
let params: CheckChangedParams = parse_params(params)?;
Ok(build_check_changed_args(params))
}
CodeModeTool::SecurityCandidates => {
let params: SecurityCandidatesParams = parse_params(params)?;
build_security_candidates_args(¶ms)
}
CodeModeTool::FindDupes => {
let params: FindDupesParams = parse_params(params)?;
build_find_dupes_args(¶ms)
}
CodeModeTool::ProjectInfo => {
let params: ProjectInfoParams = parse_params(params)?;
Ok(build_project_info_args(¶ms))
}
_ => unreachable!("project tool helper called with non-project tool"),
}
}
fn build_trace_tool_args(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Vec<String>, String> {
match tool {
CodeModeTool::TraceExport => {
let params: TraceExportParams = parse_params(params)?;
build_trace_export_args(¶ms)
}
CodeModeTool::TraceFile => {
let params: TraceFileParams = parse_params(params)?;
build_trace_file_args(¶ms)
}
CodeModeTool::TraceDependency => {
let params: TraceDependencyParams = parse_params(params)?;
build_trace_dependency_args(¶ms)
}
CodeModeTool::TraceClone => {
let params: TraceCloneParams = parse_params(params)?;
build_trace_clone_args(¶ms)
}
_ => unreachable!("trace tool helper called with non-trace tool"),
}
}
fn build_health_and_config_tool_args(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Vec<String>, String> {
match tool {
CodeModeTool::CheckHealth => {
let params: HealthParams = parse_params(params)?;
Ok(build_health_args(¶ms))
}
CodeModeTool::Audit => {
let params: AuditParams = parse_params(params)?;
build_audit_args(¶ms)
}
CodeModeTool::FallowExplain => {
let params: ExplainParams = parse_params(params)?;
Ok(build_explain_args(¶ms))
}
CodeModeTool::ListBoundaries => {
let params: ListBoundariesParams = parse_params(params)?;
Ok(build_list_boundaries_args(¶ms))
}
CodeModeTool::FeatureFlags => {
let params: FeatureFlagsParams = parse_params(params)?;
Ok(build_feature_flags_args(¶ms))
}
CodeModeTool::Impact => {
let params: ImpactParams = parse_params(params)?;
Ok(build_impact_args(¶ms))
}
_ => unreachable!("health/config helper called with unrelated tool"),
}
}
fn build_runtime_coverage_tool_args(
tool: CodeModeTool,
params: serde_json::Value,
) -> Result<Vec<String>, String> {
match tool {
CodeModeTool::CheckRuntimeCoverage => {
let params: CheckRuntimeCoverageParams = parse_params(params)?;
Ok(build_check_runtime_coverage_args(¶ms))
}
CodeModeTool::GetHotPaths => {
let params: CheckRuntimeCoverageParams = parse_params(params)?;
Ok(build_get_hot_paths_args(¶ms))
}
CodeModeTool::GetBlastRadius => {
let params: CheckRuntimeCoverageParams = parse_params(params)?;
Ok(build_get_blast_radius_args(¶ms))
}
CodeModeTool::GetImportance => {
let params: CheckRuntimeCoverageParams = parse_params(params)?;
Ok(build_get_importance_args(¶ms))
}
CodeModeTool::GetCleanupCandidates => {
let params: CheckRuntimeCoverageParams = parse_params(params)?;
Ok(build_get_cleanup_candidates_args(¶ms))
}
_ => unreachable!("runtime coverage helper called with unrelated tool"),
}
}
fn run_combined_api_value(params: &CombinedParams) -> Result<Option<serde_json::Value>, String> {
let options = combined_options_from_params(params)?;
let value = run_combined(&options)
.and_then(serialize_combined_programmatic_json)
.map_err(|err| err.to_string())?;
Ok(Some(value))
}
fn combined_options_from_params(params: &CombinedParams) -> Result<CombinedOptions, String> {
Ok(CombinedOptions {
analysis: AnalysisOptions {
root: non_empty_path(params.root.as_deref()),
config_path: non_empty_path(params.config.as_deref()),
no_cache: params.no_cache.unwrap_or(false),
threads: params.threads,
production: params.production.unwrap_or(false),
production_override: params.production,
changed_since: changed_since_from_param(params.changed_since.as_deref()),
diff_file: env_diff_file(),
workspace: non_empty_string(params.workspace.as_deref())
.map(|workspace| vec![workspace]),
explain: true,
..AnalysisOptions::default()
},
include_entry_exports: params.include_entry_exports.unwrap_or(false),
duplication_options: DuplicationOptions {
mode: combined_duplication_mode(params.dupes_mode.as_deref())?,
min_tokens: params.dupes_min_tokens.map(|value| value as usize),
min_lines: params.dupes_min_lines.map(|value| value as usize),
min_occurrences: params.dupes_min_occurrences.map(|value| value as usize),
threshold: params.dupes_threshold,
skip_local: params.dupes_skip_local,
cross_language: params.dupes_cross_language,
ignore_imports: params.dupes_ignore_imports,
..DuplicationOptions::default()
},
health_options: ComplexityOptions {
max_cyclomatic: params.max_cyclomatic,
max_cognitive: params.max_cognitive,
max_crap: params.max_crap,
complexity: params.complexity.unwrap_or(true),
file_scores: params.file_scores.unwrap_or(true),
hotspots: params.hotspots.unwrap_or(true),
targets: params.targets.unwrap_or(true),
score: params.score.unwrap_or(false),
..ComplexityOptions::default()
},
..CombinedOptions::default()
})
}
fn combined_duplication_mode(value: Option<&str>) -> Result<Option<DuplicationMode>, String> {
match value {
None | Some("") => Ok(None),
Some("strict") => Ok(Some(DuplicationMode::Strict)),
Some("mild") => Ok(Some(DuplicationMode::Mild)),
Some("weak") => Ok(Some(DuplicationMode::Weak)),
Some("semantic") => Ok(Some(DuplicationMode::Semantic)),
Some(value) => Err(format!(
"Invalid dupes_mode '{value}'. Valid values: strict, mild, weak, semantic"
)),
}
}
fn build_combined_args(params: &CombinedParams) -> Vec<String> {
let mut args = vec![
"--format".to_string(),
"json".to_string(),
"--quiet".to_string(),
"--explain".to_string(),
];
push_global(
&mut args,
params.root.as_deref(),
params.config.as_deref(),
params.no_cache,
params.threads,
);
if params.production == Some(true) {
args.push("--production".to_string());
}
push_opt_arg(&mut args, "--workspace", params.workspace.as_deref());
push_opt_arg(
&mut args,
"--changed-since",
params.changed_since.as_deref(),
);
if params.include_entry_exports == Some(true) {
args.push("--include-entry-exports".to_string());
}
push_combined_duplication_args(&mut args, params);
if params.score == Some(true) {
args.push("--score".to_string());
}
args
}
fn push_combined_duplication_args(args: &mut Vec<String>, params: &CombinedParams) {
push_opt_arg(args, "--dupes-mode", params.dupes_mode.as_deref());
push_opt_arg(
args,
"--dupes-min-tokens",
params
.dupes_min_tokens
.map(|value| value.to_string())
.as_deref(),
);
push_opt_arg(
args,
"--dupes-min-lines",
params
.dupes_min_lines
.map(|value| value.to_string())
.as_deref(),
);
push_opt_arg(
args,
"--dupes-min-occurrences",
params
.dupes_min_occurrences
.map(|value| value.to_string())
.as_deref(),
);
push_opt_arg(
args,
"--dupes-threshold",
params
.dupes_threshold
.map(|value| value.to_string())
.as_deref(),
);
if params.dupes_skip_local == Some(true) {
args.push("--dupes-skip-local".to_string());
}
if params.dupes_cross_language == Some(true) {
args.push("--dupes-cross-language".to_string());
}
match params.dupes_ignore_imports {
Some(true) => args.push("--dupes-ignore-imports".to_string()),
Some(false) => args.push("--dupes-no-ignore-imports".to_string()),
None => {}
}
}
fn push_opt_arg(args: &mut Vec<String>, flag: &str, value: Option<&str>) {
if let Some(value) = value.filter(|value| !value.is_empty()) {
args.extend([flag.to_string(), value.to_string()]);
}
}
fn parse_params<T>(params: serde_json::Value) -> Result<T, String>
where
T: serde::de::DeserializeOwned,
{
serde_json::from_value(params).map_err(|err| format!("invalid tool params: {err}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_backed_code_mode_tools_are_explicitly_registered() {
let names = API_BACKED_CODE_MODE_TOOLS
.iter()
.map(|tool| tool.name())
.collect::<Vec<_>>();
assert_eq!(
names,
vec![
"analyze",
"combined",
"check_changed",
"find_dupes",
"project_info",
"trace_export",
"trace_file",
"trace_dependency",
"trace_clone",
"check_health",
"audit",
"fallow_explain",
"list_boundaries",
"feature_flags",
]
);
for tool in API_BACKED_CODE_MODE_TOOLS {
assert!(
tool.is_api_backed(),
"{} should use fallow-api",
tool.name()
);
}
}
#[test]
fn heavy_code_mode_tools_keep_cancellable_cli_path() {
for tool in [
CodeModeTool::Analyze,
CodeModeTool::FindDupes,
CodeModeTool::CheckHealth,
CodeModeTool::Audit,
] {
assert!(
tool.is_api_backed(),
"{} should still be API-backed for standalone MCP tools",
tool.name()
);
assert!(
!tool.is_code_mode_api_backed(),
"{} should use Code Mode's cancellable subprocess path",
tool.name()
);
}
}
#[test]
fn combined_params_default_to_cli_combined_health_sections() {
let options =
combined_options_from_params(&CombinedParams::default()).expect("combined options");
assert!(options.health_options.complexity);
assert!(options.health_options.file_scores);
assert!(options.health_options.hotspots);
assert!(options.health_options.targets);
assert!(!options.health_options.score);
}
#[test]
fn combined_args_preserve_ignore_imports_override() {
let args = build_combined_args(&CombinedParams {
dupes_ignore_imports: Some(true),
..CombinedParams::default()
});
assert!(args.contains(&"--dupes-ignore-imports".to_string()));
assert!(!args.contains(&"--dupes-no-ignore-imports".to_string()));
let args = build_combined_args(&CombinedParams {
dupes_ignore_imports: Some(false),
..CombinedParams::default()
});
assert!(args.contains(&"--dupes-no-ignore-imports".to_string()));
assert!(!args.contains(&"--dupes-ignore-imports".to_string()));
}
#[test]
fn cli_only_code_mode_tools_are_not_api_backed() {
for tool in [
CodeModeTool::SecurityCandidates,
CodeModeTool::Impact,
CodeModeTool::CheckRuntimeCoverage,
CodeModeTool::GetHotPaths,
CodeModeTool::GetBlastRadius,
CodeModeTool::GetImportance,
CodeModeTool::GetCleanupCandidates,
] {
assert!(
!tool.is_api_backed(),
"{} should use CLI fallback",
tool.name()
);
assert_eq!(
run_api_tool(tool, serde_json::json!({})).expect("fallback decision"),
None
);
}
}
}