Service Projections

Service Projections derive a JSON-UI layout automatically from the structure and semantics of your data. Define what your data IS — fields, types, meanings, and workflow states — and the framework infers the appropriate UI intent and generates a component tree.

Overview

The pipeline has three stages:

ServiceDef  →  derive_intents(&service_def)  →  IntentScore[]
                                                      ↓
JsonUiRenderer.render(&service_def, &intents, &ctx)  →  serde_json::Value
  1. ServiceDef — describe your service: field names, data types, semantic meanings, state machines, guards, and actions.
  2. derive_intents — analyzes the service definition and returns a ranked list of IntentScore values. The highest-scoring intent is the primary one.
  3. JsonUiRenderer — takes the service definition, the ranked intents, and a render context, and produces a ferro-json-ui component tree.

Quick Start

Minimal example: a product service deriving its intent and rendering to JSON-UI.

#![allow(unused)]
fn main() {
use ferro::{
    DataType, FieldMeaning, ServiceDef,
    derive_intents, JsonUiRenderer, Renderer, RenderContext,
};

let product = ServiceDef::new("product")
    .display_name("Product")
    .field("id", DataType::Integer, FieldMeaning::Identifier)
    .field("name", DataType::String, FieldMeaning::EntityName)
    .field("price", DataType::Float, FieldMeaning::Money);

let intents = derive_intents(&product);
// intents[0] is the highest-confidence intent (Browse for a simple list-like service)

let renderer = JsonUiRenderer;
let json = renderer
    .render(&product, &intents, &RenderContext::default())
    .expect("rendering a valid service definition should not fail");
// json["$schema"] == "ferro-json-ui/v1"
// json["components"] contains the generated component tree
}

Core Concepts

ServiceDef Builder

ServiceDef is the entry point. Build it with a method chain:

#![allow(unused)]
fn main() {
use ferro::{
    ActionDef, DataType, FieldMeaning, GuardDef,
    ServiceDef, StateDef, StateMachine, Transition,
};

pub fn order_service() -> ServiceDef {
    ServiceDef::new("order")
        .display_name("Order")
        // Fields define the data shape
        .field("id", DataType::Integer, FieldMeaning::Identifier)
        .field("total", DataType::Float, FieldMeaning::Money)
        // Workflow states and transitions
        .state_machine(
            StateMachine::new("order_lifecycle")
                .initial("draft")
                .state(StateDef::new("draft"))
                .state(StateDef::new("completed").final_state())
                .transition(Transition::new("draft", "complete", "completed")),
        )
        // Guards control who can perform actions
        .guard(GuardDef::new("is_manager").display_name("Manager Approval Required"))
        // Actions are operations users can trigger
        .action(ActionDef::new("approve").precondition("is_manager"))
}
}

Builder methods:

MethodDescription
ServiceDef::new(name)Create a new service definition with a machine-readable name
.display_name(label)Human-readable label shown in the UI
.field(name, type, meaning)Add a field with its data type and semantic meaning
.state_machine(sm)Attach a workflow state machine
.guard(guard_def)Define a guard (permission or condition)
.action(action_def)Define an action users can trigger
.intent_hint(hint)Override or exclude derived intents (see Intent Overrides)

Fields and Meanings

Every field needs a DataType and a FieldMeaning. The data type describes the storage format; the meaning describes what the value represents semantically.

DataType:

VariantWhen to use
DataType::IntegerWhole numbers: IDs, counts, quantities
DataType::FloatDecimal numbers: prices, measurements, scores
DataType::StringShort text: names, titles, codes
DataType::BooleanTrue/false flags
DataType::DateCalendar date (no time)
DataType::DateTimeDate plus time
DataType::TextLong-form prose: descriptions, body content
DataType::EnumFixed set of values: status, category

FieldMeaning:

VariantWhen to use
FieldMeaning::IdentifierPrimary key or unique ID
FieldMeaning::EntityNameDisplay name of the record
FieldMeaning::MoneyMonetary amount
FieldMeaning::DescriptionLong descriptive text
FieldMeaning::StatusCurrent state or lifecycle value
FieldMeaning::EmailEmail address
FieldMeaning::PhonePhone number
FieldMeaning::UrlWeb URL
FieldMeaning::ImageImage URL or path
FieldMeaning::TimestampCreated/updated timestamps
FieldMeaning::CountAggregate count
FieldMeaning::LocationGeographic location
FieldMeaning::GenericNo specific semantic meaning

Intent Derivation

derive_intents examines five structural signals to score and rank the seven intents:

Signal analyzers:

  1. Field count — many fields suggest a form (Collect); few fields suggest a list (Browse).
  2. Field meanings — presence of Money, Status, Count meanings shifts scores toward specific intents.
  3. State machines — a state machine with transitions strongly scores Process.
  4. Guards and actions — approval workflows score Track; rich action sets score Process.
  5. Naming patterns — service name patterns like "report", "summary", "dashboard" shift scores toward Summarize or Analyze.

The seven intents:

IntentStructural signal
Intent::BrowseList of records with identifier and name fields
Intent::FocusSingle record detail view
Intent::CollectInput form (many fields, writable)
Intent::ProcessWorkflow with state machine and transitions
Intent::SummarizeAggregated or summary-level data
Intent::AnalyzeMetric-heavy or analytical view
Intent::TrackAudit trail or progress tracking

derive_intents returns Vec<IntentScore>, sorted by confidence descending. Each IntentScore has:

#![allow(unused)]
fn main() {
// intents[0] is primary
let primary = &intents[0];
println!("Intent: {:?}", primary.intent);
println!("Confidence: {}", primary.confidence);
println!("Signals: {:?}", primary.matching_signals);
}

Intent Overrides

If the derived intent is not what you want, use IntentHint to force or exclude intents:

#![allow(unused)]
fn main() {
use ferro::{DataType, FieldMeaning, Intent, IntentHint, ServiceDef};

let service = ServiceDef::new("order_summary")
    .field("id", DataType::Integer, FieldMeaning::Identifier)
    .field("total", DataType::Float, FieldMeaning::Money)
    // Force Summarize even though signals might score Browse higher
    .intent_hint(IntentHint::Primary(Intent::Summarize))
    // Prevent Browse from appearing even as a fallback
    .intent_hint(IntentHint::Exclude(Intent::Browse));
}

Use IntentHint::Primary to promote a specific intent to the top. Use IntentHint::Exclude to prevent an intent from being selected at all. Multiple hints can be combined.

Rendering

JsonUiRenderer implements the Renderer trait. Use it with a RenderContext to control how the output is shaped.

#![allow(unused)]
fn main() {
use ferro::{JsonUiRenderer, RenderContext, RenderMode, Renderer};

// Display mode: read-only view of data
let display_ctx = RenderContext {
    intent_index: 0,              // use primary intent
    current_state: None,          // no workflow state active
    mode: RenderMode::Display,    // read-only layout
    templates: None,              // use default layouts
};

// Input mode: editable form
let input_ctx = RenderContext {
    intent_index: 0,
    current_state: Some("draft".to_string()), // current workflow state
    mode: RenderMode::Input,                  // form layout
    templates: None,
};

let renderer = JsonUiRenderer;
let json = renderer.render(&service_def, &intents, &input_ctx).expect("rendering a valid service definition should not fail");
}

RenderContext fields:

FieldTypeDescription
intent_indexusizeIndex into the IntentScore list; 0 for primary intent
current_stateOption<String>Active state name from the state machine, if applicable
modeRenderModeRenderMode::Display for read-only; RenderMode::Input for forms
templatesOption<...>Custom layout overrides (from theme.json); None uses defaults

RenderMode:

VariantOutput
RenderMode::DisplayRead-only component tree for viewing data
RenderMode::InputEditable form component tree for creating or updating data

Complete Example

An order management service with a workflow, a guard, and an action, rendered in input mode at the "draft" state:

#![allow(unused)]
fn main() {
use ferro::{
    ActionDef, DataType, FieldMeaning, GuardDef,
    JsonUiRenderer, RenderContext, RenderMode, Renderer,
    ServiceDef, StateDef, StateMachine, Transition,
    derive_intents,
};

let order = ServiceDef::new("order")
    .display_name("Order")
    .field("id", DataType::Integer, FieldMeaning::Identifier)
    .field("customer", DataType::String, FieldMeaning::EntityName)
    .field("total", DataType::Float, FieldMeaning::Money)
    .field("status", DataType::Enum, FieldMeaning::Status)
    .field("created_at", DataType::DateTime, FieldMeaning::Timestamp)
    .state_machine(
        StateMachine::new("lifecycle")
            .initial("draft")
            .state(StateDef::new("draft"))
            .state(StateDef::new("approved"))
            .state(StateDef::new("shipped"))
            .state(StateDef::new("completed").final_state())
            .transition(Transition::new("draft", "approve", "approved"))
            .transition(Transition::new("approved", "ship", "shipped"))
            .transition(Transition::new("shipped", "complete", "completed")),
    )
    .guard(GuardDef::new("is_manager").display_name("Manager Approval Required"))
    .action(ActionDef::new("approve").precondition("is_manager"));

let intents = derive_intents(&order);
// Process intent scores highest due to state machine + actions

let ctx = RenderContext {
    intent_index: 0,
    current_state: Some("draft".to_string()),
    mode: RenderMode::Input,
    templates: None,
};

let json = JsonUiRenderer.render(&order, &intents, &ctx).expect("rendering a valid service definition should not fail");
// Produces a ferro-json-ui component tree with:
// - Fields rendered as form inputs
// - Available transitions ("approve") rendered as action buttons
// - Guard labels displayed on the action
}

Reference

TypeDescription
ServiceDefBuilder for describing a service's data shape, workflow, and capabilities
DataTypeEnum of storage types for a field (Integer, String, Float, etc.)
FieldMeaningSemantic meaning of a field (Identifier, Money, Status, etc.)
StateMachineBuilder for workflow states and transitions
StateDefA single workflow state; call .final_state() to mark terminal states
TransitionA directed edge between two states, triggered by an action name
GuardDefA named permission or condition checked before an action is allowed
ActionDefA user-triggerable operation; optionally requires a guard via .precondition()
IntentEnum of seven structural intents: Browse, Focus, Collect, Process, Summarize, Analyze, Track
IntentScoreA ranked intent result with intent, confidence, and matching_signals
IntentHintOverride directive: Primary(intent) promotes, Exclude(intent) blocks
derive_intentsAnalyzes a ServiceDef and returns a confidence-ranked Vec<IntentScore>
JsonUiRendererImplements Renderer; converts ServiceDef + intents + context to JSON-UI
RendererTrait implemented by renderers; one method: render(def, intents, ctx)
RenderContextRender parameters: intent index, current state, mode, template overrides
RenderModeDisplay for read-only output; Input for editable form output

MCP Tools

Five MCP tools support Service Projections development: listing, inspection, rendering, validation, and coverage analysis.

list_projections

  • Returns: All ServiceDef definitions found in the project, with service name, field count, detected intents, and whether a state machine is defined
  • When to use: Audit what services are defined; find the correct service name before calling inspect_projection

inspect_projection

  • Returns: Full ServiceDef breakdown: all fields with their DataType and FieldMeaning, state machine states and transitions, guards, actions, and the full ranked IntentScore list from derive_intents
  • When to use: Understand why a particular intent was derived; verify field meanings before rendering; debug unexpected component tree output

render_projection

  • Returns: The rendered JSON-UI component tree for a named service, given an intent index and render mode
  • When to use: Preview what the renderer produces without writing a handler; compare Display vs Input output; verify that state machine transitions appear correctly at a given current state

validate_projection

  • Returns: Validation results for a ServiceDef: missing required fields, unresolvable guards, unreachable states, and mismatched IntentHint directives
  • When to use: Catch definition errors before runtime; verify a service definition is well-formed after editing

projection_coverage

  • Returns: A summary of which models have corresponding ServiceDef projections and which do not, along with suggestions for field meanings based on column names
  • When to use: Identify models that could benefit from projection-based UIs; plan which services to define next