Plugin System
:::info For engine extenders This page describes internal extension APIs used when building the engine or adding custom metrics. When using the closed-source engine via arxo-loader or the FFI, you cannot register plugins; the public API is load, version, and analyze → JSON. See Rust API and FFI API for the public library API. :::
Plugin System
Section titled “Plugin System”You can extend the engine with custom metric plugins when building or extending the engine. Plugins implement the MetricPlugin trait (from arxo-types), declare which data they need, and receive a DataStore at runtime to query graphs and derived indices.
MetricPlugin Trait
Section titled “MetricPlugin Trait”All metric plugins implement this trait:
use arxo_types::core::types::{MetricPlugin, MetricContext, MetricResult, DataRequirement};use async_trait::async_trait;
#[async_trait]pub trait MetricPlugin: Send + Sync { fn id(&self) -> &str; fn version(&self) -> &str; fn requires(&self) -> Vec<DataRequirement>; async fn compute(&self, ctx: &MetricContext) -> anyhow::Result<MetricResult>;}- id: Unique plugin identifier (e.g.
"custom.my_metric"). - version: Semantic version string for the plugin.
- requires: List of data dependencies; the engine builds and caches these before calling
compute. - compute: Async computation; receives
MetricContextand returns aMetricResult.
MetricContext
Section titled “MetricContext”During compute, you receive:
pub struct MetricContext<'a> { pub data: &'a dyn DataStore, // Access to graphs and derived data pub config: serde_json::Value, // Metric-specific config from Config.metrics[].config pub cache: Cache, // Shared cache for plugin use}- data: Implement the
DataStoretrait; use it to read import graph, call graph, SCC DAG, etc. - config: The
configfield from the metric entry inConfig.metrics. - cache: Shared cache; you can store and retrieve values across plugin runs.
DataRequirement
Section titled “DataRequirement”Declare what the engine must provide before running your plugin:
pub enum DataRequirement { ImportGraph, CallGraph, EntityGraph, CallReachability, CallDependencies, EffectIndex, SccDag, CallSccDag, Reachability, ExportsIndex, GitHistory, DataFlowGraph, ComputedMetrics, WorkspaceConfig, BuildGraph, Telemetry, TelemetryMapping,}Only request what you need; the engine builds and caches data on demand. Dependencies between metrics are resolved automatically (e.g. if your plugin requires ComputedMetrics, other metrics that feed into that cache run first).
DataStore Accessors
Section titled “DataStore Accessors”Through ctx.data you can access (all methods are async unless noted):
| Method | Returns | Description |
|---|---|---|
import_graph() | Arc<ImportGraph> | Module/file import dependencies |
call_graph() | Arc<CallGraph> | Function/method call graph |
entity_graph() | Arc<EntityGraph> | Entity-level call graph (functions, classes, etc.) |
type_graph() | Arc<TypeGraph> | Type inheritance/implementation graph |
effect_index() | Arc<EffectIndex> | Side-effect detection per file |
scc_dag() | Arc<SccDag> | Strongly connected components (cycles) on import graph |
call_scc_dag() | CallSccDag | SCC DAG on call graph |
reachability() | Arc<ReachabilityIndex> | Transitive dependency reachability |
call_reachability() | Arc<CallReachabilityIndex> | Transitive call reachability |
call_dependencies() | Arc<CallDependencyIndex> | Call dependency analysis |
git_history() | Arc<GitHistory> | Git history (churn, co-change, authors) |
telemetry() | Telemetry | OpenTelemetry trace data |
telemetry_mapping() | TelemetryMappingIndex | Trace-to-code mapping |
workspace_config() | WorkspaceConfig | Monorepo/workspace configuration |
build_graph() | Option<BuildGraph> | Build dependency graph |
computed_metrics() | ComputedMetricsCache | Results from other metrics (values, UI details) |
set_computed_metrics() | () | Write back to computed metrics cache |
project_path() | PathBuf | Project root path |
exports_index() | () | Exports index (reserved) |
dataflow_graph() | () | Dataflow graph (reserved) |
Use only the accessors that match the DataRequirement values you declared in requires().
MetricResult
Section titled “MetricResult”Your plugin returns:
pub struct MetricResult { pub id: MetricId, // Same as plugin id pub version: String, // Same as plugin version pub values: HashMap<MetricKey, f64>, // Key-value metrics (e.g. "cycle_count" -> 2.0) pub ui_schema: Option<MetricUISchema>, pub details: Option<HashMap<String, serde_json::Value>>, pub findings: Option<Vec<Finding>>,}- values: Numeric metrics exposed to policy and reports (e.g.
scc.cycle_count). - findings: Optional violations or insights with evidence and recommendations.
Example: Custom Metric
Section titled “Example: Custom Metric”use arxo_types::core::types::{ DataRequirement, MetricContext, MetricPlugin, MetricResult,};use async_trait::async_trait;use std::collections::HashMap;
struct MyCustomMetric;
#[async_trait]impl MetricPlugin for MyCustomMetric { fn id(&self) -> &str { "custom.module_count" }
fn version(&self) -> &str { "1.0.0" }
fn requires(&self) -> Vec<DataRequirement> { vec![DataRequirement::ImportGraph] }
async fn compute(&self, ctx: &MetricContext) -> anyhow::Result<MetricResult> { let graph = ctx.data.import_graph().await?; let node_count = graph.node_count(); let edge_count = graph.edge_count();
let mut values = HashMap::new(); values.insert("node_count".to_string(), node_count as f64); values.insert("edge_count".to_string(), edge_count as f64);
Ok(MetricResult::new( self.id().to_string(), self.version().to_string(), values, None, )) }}Registering Plugins
Section titled “Registering Plugins”The engine exposes PluginRegistry (with new() and register(Box<dyn MetricPlugin>)). How custom plugins are registered depends on your distribution: some builds allow constructing an orchestrator with a custom registry; others use a fixed set of built-in metrics. See your distribution or engine documentation for the exact API (e.g. Orchestrator::with_registry(registry) if available).
Once registered, the engine runs your plugin when the config includes a metric with matching id (and enabled: true). Plugin order and dependencies are resolved from requires() and the computed-metrics cache.
ComputedMetricsCache
Section titled “ComputedMetricsCache”Plugins that depend on other metrics use DataRequirement::ComputedMetrics. The engine fills the cache with results from plugins that have already run. In compute you can:
- Read numeric values:
ctx.data.computed_metrics().await?.get_metric("scc.cycle_count") - Read full plugin result:
get_plugin_result(plugin_id) - Read UI details:
get_plugin_ui_details(plugin_id)
Do not document or rely on internal cache keys; use the public metric ids from the config.
Best Practices
Section titled “Best Practices”- Minimal requires: List only the
DataRequirementvalues you use so the engine can skip building unused data. - Stable id and version: Use a unique, stable
idand semanticversionfor compatibility and caching. - Errors: Return
Errfromcomputeon failure; the engine reports it and can skip dependent metrics. - No side effects: Plugins should only read from
DataStoreand write their result; avoid mutating global state.
Next Steps
Section titled “Next Steps”- Configuration — Enable your metric in
Config.metrics - Rust API — Run the orchestrator with your registry
- Examples — Full project examples