# Recursive Plugin Schema: Migration Path

## Current State

- `Plexus.schema()` returns a flat list of all activations
- Individual activations don't implement `schema`
- No recursive structure — clients get everything at once or nothing
- Hub/plugin distinction is implicit, not encoded in types

## Target State

Every plugin implements `schema()` returning:

```rust
struct PluginSchema {
    namespace: String,
    version: String,
    description: String,
    methods: Vec<MethodSchema>,
    children: Option<Vec<PluginSchema>>,  // None = leaf, Some = hub
}

struct MethodSchema {
    name: String,
    description: String,
    params: Option<JsonSchema>,
    returns: Option<JsonSchema>,
}
```

- **Leaf plugin**: `children = None` (e.g., `health`, `echo`)
- **Hub plugin**: `children = Some([...])` (e.g., `plexus`)

Clients recursively call `schema()` to discover structure at any depth.

## Category-Theoretic Foundation

```
Plugin ≅ μX. Methods × (1 + List(X))
       ≅ Methods × Option(List(Plugin))
```

The `schema()` method is the coalgebra structure map:
```
unfold: Plugin → PluginSchema
```

Applied recursively (anamorphism) until all leaves are reached.

## Migration Steps

### Phase 1: Define Core Types

**File**: `src/plexus/schema.rs`

```rust
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PluginSchema {
    pub namespace: String,
    pub version: String,
    pub description: String,
    pub methods: Vec<MethodSchema>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub children: Option<Vec<PluginSchema>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MethodSchema {
    pub name: String,
    pub description: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<schemars::Schema>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub returns: Option<schemars::Schema>,
}
```

### Phase 2: Extend Activation Trait

**File**: `src/plexus/plexus.rs`

Add to `Activation` trait:

```rust
#[async_trait]
pub trait Activation: Send + Sync + 'static {
    // ... existing methods ...

    /// Return this plugin's schema (methods + optional children)
    fn plugin_schema(&self) -> PluginSchema {
        PluginSchema {
            namespace: self.namespace().to_string(),
            version: self.version().to_string(),
            description: self.description().to_string(),
            methods: self.method_schemas(),
            children: None,  // Default: leaf plugin
        }
    }

    /// Return method schemas (generated by hub-macro for most activations)
    fn method_schemas(&self) -> Vec<MethodSchema> {
        // Default implementation from methods() + method_help()
        self.methods().iter().map(|name| {
            MethodSchema {
                name: name.to_string(),
                description: self.method_help(name).unwrap_or_default(),
                params: None,
                returns: None,
            }
        }).collect()
    }
}
```

### Phase 3: Update hub-macro

**File**: `hub-macro/src/codegen/activation.rs`

Generate `method_schemas()` with actual JSON schemas:

```rust
fn method_schemas(&self) -> Vec<MethodSchema> {
    vec![
        MethodSchema {
            name: "echo".to_string(),
            description: "Echo a message".to_string(),
            params: Some(schemars::schema_for!(EchoParams)),
            returns: Some(schemars::schema_for!(EchoEvent)),
        },
        // ... for each method
    ]
}
```

This requires:
1. Generating param structs for methods with multiple params
2. Extracting `Stream::Item` type for return schema
3. Using `schemars::schema_for!` on both

### Phase 4: Override for Hubs

**File**: `src/plexus/plexus.rs`

Plexus overrides `plugin_schema()` to include children:

```rust
impl Plexus {
    fn plugin_schema(&self) -> PluginSchema {
        PluginSchema {
            namespace: "plexus".to_string(),
            version: "1.0.0".to_string(),
            description: "Central routing and introspection".to_string(),
            methods: self.method_schemas(),
            children: Some(
                self.inner.activations.values()
                    .map(|a| a.plugin_schema())
                    .collect()
            ),
        }
    }
}
```

### Phase 5: Unify schema() Method

Replace current `schema()` RPC method to return `PluginSchema`:

```rust
#[hub_macro::hub_method(description = "Get this plugin's schema")]
async fn schema(&self) -> impl Stream<Item = PluginSchemaEvent> + Send + 'static {
    let schema = self.plugin_schema();
    stream! { yield PluginSchemaEvent::Schema(schema); }
}
```

### Phase 6: MCP Bridge

The MCP bridge already dynamically generates tools from schemas. Update to use `plugin_schema()` for richer discovery.

## Ordering Constraints

```
Phase 1 (types)
    │
    ▼
Phase 2 (trait extension)
    │
    ├──────────────────┐
    ▼                  ▼
Phase 3 (hub-macro)   Phase 4 (Plexus override)
    │                  │
    └────────┬─────────┘
             ▼
      Phase 5 (unify RPC)
             │
             ▼
      Phase 6 (MCP bridge)
```

Phases 3 and 4 can be done in parallel.

## Backwards Compatibility

During migration:

1. Keep `list_activations()` working (deprecated)
2. New `schema()` returns `PluginSchema`
3. MCP clients can use either
4. Remove `list_activations()` after clients migrate

## Validation Criteria

1. `plexus.schema` returns recursive structure with children
2. `echo.schema` returns leaf structure (no children)
3. Client can reconstruct full hub tree from recursive schema calls
4. hub-macro generates complete method schemas (params + returns)
5. MCP tools generated from new schema format

## Future: Nested Hubs

Once this is in place, a plugin can itself be a hub:

```rust
struct MyHub {
    sub_plugins: Vec<Arc<dyn Activation>>,
}

impl Activation for MyHub {
    fn plugin_schema(&self) -> PluginSchema {
        PluginSchema {
            // ...
            children: Some(self.sub_plugins.iter().map(|p| p.plugin_schema()).collect()),
        }
    }
}
```

This enables arbitrary nesting: `plexus → my_hub → sub_plugin → ...`

The category-theoretic structure ensures this composition is well-founded as long as the children relation forms a DAG (no cycles).

## Non-Goals (For This Migration)

- Dynamic plugin loading (future work)
- Plugin versioning/compatibility checks (future work)
- Remote hub federation (future work)

These build on top of the recursive schema foundation but are out of scope for this migration.
