use super::spec::{Effects, VerbSpec};
use crate::mcp::handlers::*;
use crate::mcp::handlers_ops::*;
use crate::mcp::types::*;
use pforge_runtime::Handler;
macro_rules! verb_table {
($( $name:literal, $eff:expr, $timeout:literal, $desc:literal, $input:ty, $output:ty, $handler:expr ; )*) => {
pub fn verbs() -> Vec<VerbSpec> {
vec![$(
VerbSpec {
name: $name,
description: $desc,
effects: $eff,
timeout_ms: $timeout,
input_schema: || {
serde_json::to_value(schemars::schema_for!($input))
.unwrap_or(serde_json::Value::Null)
},
output_schema: || {
serde_json::to_value(schemars::schema_for!($output))
.unwrap_or(serde_json::Value::Null)
},
invoke: |params: serde_json::Value| -> Result<serde_json::Value, String> {
let input: $input = serde_json::from_value(params)
.map_err(|e| format!("invalid params for `{}`: {e}", $name))?;
let rt = tokio::runtime::Runtime::new()
.map_err(|e| format!("tokio runtime: {e}"))?;
let out = rt
.block_on(async move { $handler.handle(input).await })
.map_err(|e| format!("{e}"))?;
serde_json::to_value(out)
.map_err(|e| format!("result did not serialise: {e}"))
},
},
)*]
}
};
}
verb_table! {
"validate", Effects::ReadOnly, 30_000, "Validate a forjar.yaml configuration file", ValidateInput, ValidateOutput, ValidateHandler;
"plan", Effects::ReadOnly, 60_000, "Show execution plan for infrastructure changes", PlanInput, PlanOutput, PlanHandler;
"drift", Effects::ReadOnly, 60_000, "Detect configuration drift from desired state", DriftInput, DriftOutput, DriftHandler;
"lint", Effects::ReadOnly, 30_000, "Quality gate: shell safety, plaintext secrets, script complexity and compliance rules, with SARIF diagnostics", LintInput, LintOutput, LintHandler;
"graph", Effects::ReadOnly, 10_000, "Generate resource dependency graph (format: mermaid or dot only)", GraphInput, GraphOutput, GraphHandler;
"show", Effects::ReadOnly, 30_000, "Show fully resolved config with templates expanded", ShowInput, ShowOutput, ShowHandler;
"status", Effects::ReadOnly, 10_000, "Show current state from lock files", StatusInput, StatusOutput, StatusHandler;
"trace", Effects::ReadOnly, 30_000, "View trace provenance data from apply runs", TraceInput, TraceOutput, TraceHandler;
"anomaly", Effects::ReadOnly, 30_000, "Detect anomalous resource behavior using ML-inspired analysis", AnomalyInput, AnomalyOutput, AnomalyHandler;
"remediate", Effects::ReadOnly, 30_000, "Compute policy-derived corrections to a forjar.yaml and return the corrected document (never writes)", RemediateInput, RemediateOutput, RemediateHandler;
"audit", Effects::ReadOnly, 30_000, "Read the append-only provenance trail recorded by apply runs", AuditInput, AuditOutput, AuditHandler;
"workspace", Effects::ReadOnly, 10_000, "Report which workspace the forjar CLI has selected and every workspace under the state dir; the selection does not change where the other verbs read state", WorkspaceInput, WorkspaceOutput, WorkspaceHandler;
}
pub fn find(name: &str) -> Option<VerbSpec> {
verbs().into_iter().find(|v| v.name == name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn registry_is_not_empty() {
assert!(
verbs().len() >= 12,
"verb registry collapsed to {} entries — every parity test downstream \
becomes vacuous when this is empty",
verbs().len()
);
}
#[test]
fn names_are_unique() {
let v = verbs();
let uniq: HashSet<_> = v.iter().map(|s| s.name).collect();
assert_eq!(uniq.len(), v.len(), "duplicate verb name in the registry");
}
#[test]
fn mcp_names_are_derived_not_typed() {
for v in verbs() {
assert_eq!(v.mcp_name(), format!("forjar_{}", v.name.replace('-', "_")));
}
}
#[test]
fn every_verb_has_real_schemas() {
for v in verbs() {
let i = (v.input_schema)();
let o = (v.output_schema)();
assert!(
i.is_object() && !i.as_object().unwrap().is_empty(),
"{}: input schema is empty — a schema that is Null validates \
nothing, so FVS-2 would pass while checking no params",
v.name
);
assert!(
o.is_object() && !o.as_object().unwrap().is_empty(),
"{}: output schema is empty",
v.name
);
}
}
#[test]
fn read_only_hint_is_derived_from_effects() {
assert!(Effects::ReadOnly.read_only());
assert!(!Effects::Mutating.read_only());
}
}