fv-compute 0.2.0

The FusionVault transform/compute contract: a transform is a typed function over Arrow data declared by a manifest, discovered by a registry, and run by a pluggable backend.
Documentation
//! A **catalog** view over the registry (/) — the UX read-model.
//!
//! This is pure metadata projection (manifest → serializable descriptor): **no backend, no
//! execution runtime**. A service that only *lists* transforms or models (a console catalog page)
//! depends on `fv-compute` alone — it does not pull `fv-compute-wasm`'s wasmtime or `fv-infer`'s
//! ONNX runtime. Execution crates stay separate from the browse surface.
//!
//! `descriptors` projects every unit; `model_descriptors` is the `impl = onnx` subset (the model
//! catalog). Both feed the same `FieldInfo`/`TransformDescriptor` shape a UI renders.

use crate::manifest::ImplKind;
use crate::registry::{RegisteredTransform, Registry};
use crate::types::ColumnSpec;
use serde::Serialize;

/// One column of a signature, UX-friendly (`type` is the manifest's own snake_case name).
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct FieldInfo {
    pub name: String,
    #[serde(rename = "type")]
    pub dtype: String,
}

impl From<&ColumnSpec> for FieldInfo {
    fn from(c: &ColumnSpec) -> Self {
        let dtype = serde_json::to_value(c.dtype)
            .ok()
            .and_then(|v| v.as_str().map(str::to_string))
            .unwrap_or_else(|| "unknown".into());
        FieldInfo {
            name: c.name.clone(),
            dtype,
        }
    }
}

/// A registry unit (transform or model) as the UI sees it.
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct TransformDescriptor {
    pub id: String,
    pub version: String,
    /// `id@version` — the selector used everywhere (pipeline step, UDF registration, provenance).
    pub key: String,
    /// Which registry root supplied it (the provided package or a business bundle).
    pub root: String,
    #[serde(rename = "impl")]
    pub impl_kind: String,
    /// Input signature (for a model: the feature columns). First input only, flattened for the UI.
    pub inputs: Vec<FieldInfo>,
    /// Output signature (for a model: prediction + any pass-through columns).
    pub outputs: Vec<FieldInfo>,
    pub hardware: String,
    pub deterministic: bool,
    pub io: bool,
    pub streaming: bool,
    /// The artifact/payload path within the unit directory (`.onnx`, `.wasm`, `expr.fvx`, …).
    pub artifact: Option<String>,
}

/// Serialize a small `serde` enum to its string name (its `rename_all` form).
fn enum_str<T: Serialize>(v: T) -> Option<String> {
    serde_json::to_value(v)
        .ok()
        .and_then(|v| v.as_str().map(str::to_string))
}

impl From<&RegisteredTransform> for TransformDescriptor {
    fn from(r: &RegisteredTransform) -> Self {
        let m = &r.manifest;
        let inputs = m
            .inputs
            .first()
            .map(|s| s.columns.iter().map(FieldInfo::from).collect())
            .unwrap_or_default();
        let outputs = m.output.columns.iter().map(FieldInfo::from).collect();
        TransformDescriptor {
            id: m.id.clone(),
            version: m.version.clone(),
            key: r.key(),
            root: r.root.clone(),
            impl_kind: enum_str(m.impl_kind).unwrap_or_else(|| "builtin".into()),
            inputs,
            outputs,
            hardware: enum_str(m.capabilities.hardware).unwrap_or_else(|| "cpu".into()),
            deterministic: m.capabilities.deterministic,
            io: m.capabilities.io,
            streaming: m.capabilities.streaming,
            artifact: m.entry.clone(),
        }
    }
}

/// Every unit in the registry, as descriptors (sorted by `key`).
pub fn descriptors(registry: &Registry) -> Vec<TransformDescriptor> {
    let mut out: Vec<TransformDescriptor> = registry.iter().map(TransformDescriptor::from).collect();
    out.sort_by(|a, b| a.key.cmp(&b.key));
    out
}

/// Only the `impl = onnx` units — the **model catalog**.
pub fn model_descriptors(registry: &Registry) -> Vec<TransformDescriptor> {
    let mut out: Vec<TransformDescriptor> = registry
        .iter()
        .filter(|r| r.manifest.impl_kind == ImplKind::Onnx)
        .map(TransformDescriptor::from)
        .collect();
    out.sort_by(|a, b| a.key.cmp(&b.key));
    out
}

/// The whole transform catalog as JSON (`{ "transforms": [ … ] }`).
pub fn catalog_json(registry: &Registry) -> String {
    serde_json::json!({ "transforms": descriptors(registry) }).to_string()
}

/// The model catalog as JSON (`{ "models": [ … ] }`) — what a model-management UI fetches.
pub fn models_json(registry: &Registry) -> String {
    serde_json::json!({ "models": model_descriptors(registry) }).to_string()
}