# Unified CLI Tree Architecture

## Status: Proposal

## Problem Statement

Synapse's CLI currently exhibits a structural asymmetry:

```
                   HAND-ROLLED RECURSION           FLAT PARSING
                   (navigate function)            (--key value)
                         │                              │
                         ▼                              ▼
    synapse substrate cone chat identifier --name haiku35
       │      │     │    │       │          │
       │      │     │    │       │          └── String value
       │      │     │    │       └── Param key (--name)
       │      │     │    └── Method        ◄── BOUNDARY
       │      │     └── Plugin
       │      └── Hub
       └── CLI root
```

**The boundary at Method→Params is unnatural.** Parameters are parsed via flat `--key value` extraction in `parsePathAndParams`, losing structure.

This creates problems:
1. **Tagged unions are second-class**: `ConeIdentifier` (a tagged union with variants `by_name`, `by_id`) becomes flat `--identifier {...}` instead of navigable subcommands
2. **Completion is inconsistent**: Schema-based completion stops at method level
3. **Help generation bifurcates**: Different code paths for plugin/method vs params

## Current Architecture: Two Traversal Mechanisms

### 1. Navigation (Synapse.Algebra.Navigate) - Hand-Rolled

**Does NOT use SchemaF.** It's direct recursive pattern matching:

```haskell
navigateFrom :: PluginSchema -> Path -> Path -> SynapseM SchemaView
navigateFrom schema visited = \case
  [] -> pure $ ViewPlugin schema visited
  (seg:rest) ->
    case findChild seg schema of
      Just child -> do
        childSchema <- fetchCached (csHash child) (fetchSchemaAt (visited ++ [seg]))
        navigateFrom childSchema (visited ++ [seg]) rest  -- manual recursion
      Nothing -> case findMethod seg schema of
        Just method | null rest -> pure $ ViewMethod method (visited ++ [seg])
        ...
```

This is interactive path-following: given a path, find where it leads.

### 2. Walking (Synapse.Algebra.Walk) - Uses SchemaF

**Does use SchemaF** for batch collection operations:

```haskell
data SchemaF a
  = PluginF PluginSchema Path [a]  -- interior: plugin with children
  | MethodF MethodSchema Text Path -- leaf: method is TERMINAL

-- Hylomorphism: unfold then fold (fused)
walkMethods :: Path -> SynapseM [MethodInfo]
walkMethods = hyloM methodAlgebraM schemaCoalgebra

-- Also used by:
buildIR = walkSchema irAlgebra
checkAllSupport = walkSchema supportAlgebra
```

### Why Navigate Doesn't Use SchemaF

Navigation is fundamentally different from walking:

| Aspect | Navigate | Walk |
|--------|----------|------|
| Purpose | Find one target | Collect all |
| Pattern | Path → SchemaView | Path → [Results] |
| Recursion | Conditional (follow path) | Exhaustive (all children) |
| Scheme | Paramorphism-like | Hylomorphism |

Navigation needs to inspect children to decide which branch to take. A pure catamorphism loses the original structure. The doc comment in Navigate.hs notes this is "conceptually a paramorphism" but implemented directly.

## Analysis: Can We Use SchemaF for CLI Traversal?

**Short answer: No, but we can use two functors with a transition.**

### The Category Boundary Problem

SchemaF treats methods as **leaves**:

```haskell
data SchemaF a
  = PluginF PluginSchema Path [a]  -- has recursive positions
  | MethodF MethodSchema Text Path -- NO recursive positions (leaf)
```

But for CLI traversal, we need to "enter" a method and navigate its parameters. Methods aren't leaves in the CLI tree - they're interior nodes with param children.

**This is a category boundary.** Substrate's type system models it correctly:
- Plugins contain plugins and methods
- Methods contain params (different structure)

### Two-Functor Solution

We need **two functors** representing two categories:

```haskell
-- Category 1: Plugin/Method navigation (existing)
data SchemaF a
  = PluginF PluginSchema Path [a]
  | MethodF MethodSchema Text Path  -- leaf in THIS category

-- Category 2: Parameter navigation (new)
data ParamF a
  = ParamNode Text Value ParamKind (Maybe a)  -- might have variant child
  | VariantNode Text [a]                       -- has field children
  | FieldNode Text PrimType                    -- leaf in THIS category

data ParamKind = PKPrimitive PrimType | PKTaggedUnion [Text] | PKObject | PKArray
```

### The Transition Functor

When we reach a `MethodF` (leaf in SchemaF), we transition to ParamF:

```haskell
-- Transition: extract param structure from method's JSON Schema
methodToParams :: MethodSchema -> [Fix ParamF]
methodToParams method = case methodParams method of
  Nothing -> []
  Just schema -> unfoldParams schema

-- Unfold JSON Schema into ParamF tree
unfoldParams :: Value -> [Fix ParamF]
unfoldParams = ana paramCoalgebra
```

### Combined Navigation

```haskell
data CLIView
  = AtPlugin PluginSchema Path
  | AtMethod MethodSchema Path
  | AtParam (Fix ParamF) Path        -- entered param space
  | AtVariant Text [Fix ParamF] Path
  | AtField Text PrimType Path

navigateCLI :: Path -> SynapseM CLIView
navigateCLI path = do
  -- First navigate in SchemaF space
  schemaView <- navigate (takeWhile (not . isFlag) path)
  case schemaView of
    ViewPlugin schema p -> pure $ AtPlugin schema p
    ViewMethod method p ->
      -- Transition to ParamF space
      let paramPath = dropWhile (not . isFlag) path
          paramTree = methodToParams method
      in navigateParams paramTree paramPath
```

## The Unified Vision

The entire CLI is a tree, but composed of **two categories joined at methods**:

```
synapse substrate cone chat identifier by-name --name haiku35
   │      │     │    │       │        │          │
   │      │     │    │       │        │          └── FieldNode (ParamF leaf)
   │      │     │    │       │        └── VariantNode (ParamF)
   │      │     │    │       └── ParamNode (ParamF root)
   │      │     │    │
   │      │     │    └── MethodF ─── TRANSITION ───► ParamF tree
   │      │     │
   │      │     └── PluginF (SchemaF)
   │      └── PluginF (SchemaF)
   └── Root

        SchemaF Category          ParamF Category
       ┌─────────────────┐      ┌─────────────────┐
       │ PluginF → [a]   │      │ ParamNode → a?  │
       │ MethodF → ()    │─────►│ VariantNode→[a] │
       │   (leaf)        │      │ FieldNode → ()  │
       └─────────────────┘      └─────────────────┘
```

**Key insight**: JSON Schema already describes parameter structure. We unfold it into ParamF, giving us the same algebraic machinery (cata/ana/hylo) for params that we have for plugins.

## Proposed: ParamF (New Functor for Parameter Space)

Rather than a single unified functor, we keep SchemaF and add ParamF:

```haskell
-- | Parameter tree functor (new)
-- Represents the structure inside a method's params
data ParamF a
  = ParamNode
      { pnName      :: Text
      , pnSchema    :: Value         -- JSON Schema fragment
      , pnKind      :: ParamKind
      , pnChild     :: Maybe a       -- variant children for tagged unions
      }
  | VariantNode
      { vnName      :: Text
      , vnFields    :: [a]           -- field children
      }
  | FieldNode
      { fnName      :: Text
      , fnType      :: PrimType
      }
  deriving (Functor, Foldable, Traversable)

data ParamKind
  = PKPrimitive PrimType   -- --key value (leaf)
  | PKObject [(Text, a)]   -- nested object (expand to fields)
  | PKTaggedUnion [Text]   -- variant names (becomes subcommand)
  | PKArray ParamKind      -- --key val1 --key val2 (repeated)
  deriving (Show, Eq)

data PrimType
  = PTString | PTInt | PTBool | PTNumber | PTUUID
  deriving (Show, Eq)
```

**Why two functors instead of one?**

1. **Respects substrate's type structure**: Methods really are leaves in the plugin tree
2. **Cleaner separation**: SchemaF for network-fetched schemas, ParamF for JSON Schema interpretation
3. **Composable**: Can use different algebras for each category
4. **Existing code unchanged**: SchemaF and Walk.hs continue to work

### Tree Structure (Two Categories)

```
═══════════════════ SchemaF Category ═══════════════════

                          PluginF (plexus)
                                │
              ┌─────────────────┼─────────────────┐
              ▼                 ▼                 ▼
          PluginF           PluginF          PluginF
          (arbor)            (cone)           (echo)
                               │
                    ┌──────────┼──────────┐
                    ▼          ▼          ▼
              MethodF     MethodF     MethodF
               (chat)     (create)    (delete)
                  │
                  │ ◄─── TRANSITION: methodToParams
                  ▼
═══════════════════ ParamF Category ═══════════════════

          ┌───────┼───────┐
          ▼       ▼       ▼
     ParamNode ParamNode ParamNode
   (identifier) (prompt) (ephemeral)
        │
        ├─── TaggedUnion: ["by_name", "by_id"]
        │
    VariantNode         VariantNode
    (by_name)            (by_id)
        │                    │
   FieldNode             FieldNode
    (name)                 (id)
```

## ParamF Coalgebra

The coalgebra unfolds JSON Schema into ParamF:

```haskell
-- | Seed for unfolding params from JSON Schema
data ParamSeed = ParamSeed
  { psName   :: Text
  , psSchema :: Value        -- JSON Schema fragment
  , psPath   :: Path         -- for error reporting
  }

-- | Coalgebra: one layer of ParamF from a JSON Schema fragment
paramCoalgebra :: ParamSeed -> ParamF ParamSeed
paramCoalgebra (ParamSeed name schema path) =
  case classifySchema schema of
    SKPrimitive pt ->
      FieldNode name pt

    SKTaggedUnion variants ->
      let variantSeeds =
            [ ParamSeed vname vschema (path ++ [vname])
            | (vname, vschema) <- variants
            ]
      in ParamNode name schema (PKTaggedUnion (map fst variants)) (Just variantSeeds)

    SKObject fields ->
      let fieldSeeds =
            [ ParamSeed fname fschema (path ++ [fname])
            | (fname, fschema) <- fields
            ]
      in ParamNode name schema (PKObject fieldSeeds) Nothing

    SKArray itemSchema ->
      -- Arrays don't add tree depth, just mark the kind
      ParamNode name schema (PKArray (classifySchema itemSchema)) Nothing

-- | Classify a JSON Schema fragment
data SchemaKind
  = SKPrimitive PrimType
  | SKTaggedUnion [(Text, Value)]  -- variant name -> variant schema
  | SKObject [(Text, Value)]       -- field name -> field schema
  | SKArray Value                  -- item schema

classifySchema :: Value -> SchemaKind
classifySchema schema
  | Just variants <- detectTaggedUnion schema = SKTaggedUnion variants
  | Just fields <- detectObject schema = SKObject fields
  | Just itemSchema <- detectArray schema = SKArray itemSchema
  | otherwise = SKPrimitive (detectPrimitive schema)
```

### Detecting Tagged Unions

JSON Schema encodes tagged unions via `oneOf` with const discriminators:

```json
{
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "type": { "const": "by_name" },
        "name": { "type": "string" }
      },
      "required": ["type", "name"]
    },
    {
      "type": "object",
      "properties": {
        "type": { "const": "by_id" },
        "id": { "type": "string", "format": "uuid" }
      },
      "required": ["type", "id"]
    }
  ]
}
```

Detection heuristic:
```haskell
detectTaggedUnion :: Value -> Maybe [(Text, [(Text, Value)])]
detectTaggedUnion schema = do
  oneOf <- schema ^? key "oneOf" . _Array
  variants <- forM oneOf $ \variant -> do
    props <- variant ^? key "properties" . _Object
    typeField <- props ^? ix "type" . key "const" . _String
    let fields = [(k, v) | (k, v) <- KM.toList props, k /= "type"]
    pure (typeField, fields)
  pure variants
```

## Algebras Over ParamF

Once we have a `Fix ParamF` tree, we can fold over it with various algebras.

### Help Generation

```haskell
paramHelpAlgebra :: ParamF Text -> Text
paramHelpAlgebra = \case
  ParamNode name _ kind mVariantHelp -> case kind of
    PKPrimitive pt -> "  --" <> name <> " <" <> renderType pt <> ">"
    PKTaggedUnion variants ->
      "  " <> name <> " {" <> T.intercalate "|" variants <> "}\n"
      <> fromMaybe "" mVariantHelp
    PKObject _ -> "  --" <> name <> " <json>"
    PKArray k -> "  --" <> name <> " <" <> renderType k <> ">..."

  VariantNode name fieldHelp ->
    "    " <> name <> "\n" <> unlines (map ("      " <>) fieldHelp)

  FieldNode name ty ->
    "--" <> name <> " <" <> renderType ty <> ">"

-- Fold the param tree into help text
renderParamHelp :: Fix ParamF -> Text
renderParamHelp = cata paramHelpAlgebra
```

### Completion

```haskell
paramCompletionAlgebra :: ParamF [Text] -> [Text]
paramCompletionAlgebra = \case
  ParamNode name _ kind _ -> case kind of
    PKTaggedUnion variants -> variants  -- complete variant names
    _ -> ["--" <> name]                  -- complete flag
  VariantNode name _ -> [name]
  FieldNode name _ -> ["--" <> name]

-- Get completions for current position in param tree
paramCompletions :: Fix ParamF -> [Text]
paramCompletions = cata paramCompletionAlgebra
```

### Argument Parsing

```haskell
-- The parser accumulates (remaining args, parsed Value)
type ParseState = ([Text], Value)

paramParseAlgebra :: ParamF (ParseState -> Either ParseError ParseState)
                  -> (ParseState -> Either ParseError ParseState)
paramParseAlgebra = \case
  FieldNode name ty -> \(args, acc) ->
    case extractFlag name args of
      Just (val, rest) -> do
        parsed <- parsePrimitive ty val
        Right (rest, insertField name parsed acc)
      Nothing -> Left $ MissingRequired name

  VariantNode name fieldParsers -> \state ->
    -- Run all field parsers in sequence, then tag the result
    foldM (\s p -> p s) state fieldParsers

  ParamNode name _ (PKTaggedUnion _) (Just variantParser) -> \(args, acc) ->
    case args of
      (variant:rest) -> do
        (rest', val) <- variantParser (rest, object [])
        Right (rest', insertField name (addTypeTag variant val) acc)
      [] -> Left $ MissingVariant name

  ParamNode name _ _ Nothing -> \(args, acc) ->
    -- Primitive or object - extract directly
    case extractFlag name args of
      Just (val, rest) -> Right (rest, insertField name (parseValue val) acc)
      Nothing -> Right (args, acc)  -- optional, skip
```

### Composing with SchemaF Algebras

For operations that span both categories, compose the algebras:

```haskell
-- Full support check: SchemaF algebra that uses ParamF algebra internally
fullSupportAlgebra :: SchemaF [MethodSupport] -> SynapseM [MethodSupport]
fullSupportAlgebra = \case
  PluginF schema path childResults ->
    pure $ concat childResults

  MethodF method ns path -> do
    -- Transition to ParamF space
    let paramTree = methodToParams method
        paramSupport = cata paramSupportAlgebra paramTree
    pure [MethodSupport (methodName method) path paramSupport]

-- Algebra in ParamF space
paramSupportAlgebra :: ParamF SupportLevel -> SupportLevel
paramSupportAlgebra = \case
  FieldNode _ _ -> FullSupport
  ParamNode _ _ (PKPrimitive _) _ -> FullSupport
  ParamNode _ _ (PKTaggedUnion _) _ -> Unsupported "tagged union"
  ParamNode _ _ (PKObject _) _ -> Unsupported "nested object"
  VariantNode _ _ -> FullSupport
```

## CLI UX Implications

### Simple Params (Primitives)

No change from current:
```bash
synapse substrate echo once --message "hello"
```

### Nested Objects

Flatten with dot notation:
```bash
synapse substrate foo bar --config.host localhost --config.port 8080
```

Or JSON escape hatch:
```bash
synapse substrate foo bar --config '{"host":"localhost","port":8080}'
```

### Tagged Unions (Two Options)

**Option A: Subcommand style** (proposed for better UX)
```bash
synapse substrate cone chat by-name --name haiku35
synapse substrate cone chat by-id --id 550e8400-e29b-41d4-a716-446655440000
```

**Option B: Flag with variant prefix** (fallback)
```bash
synapse substrate cone chat --identifier.by-name.name haiku35
```

The subcommand style is superior because:
- Tab completion naturally suggests variants
- Help shows variant-specific fields
- Mirrors how optparse-applicative handles subcommands

### Arrays

Repeated flags:
```bash
synapse substrate foo bar --tag important --tag urgent --tag v2
```

## Implementation Phases

### Phase 1: ParamF Module

New module `Synapse.Schema.ParamFunctor`:
```haskell
-- Define the functor
data ParamF a = ParamNode ... | VariantNode ... | FieldNode ...

-- Coalgebra for unfolding from JSON Schema
paramCoalgebra :: ParamSeed -> ParamF ParamSeed

-- Helper to detect schema kinds
classifySchema :: Value -> SchemaKind
```

### Phase 2: Transition Function

Add to `Synapse.Schema.ParamFunctor`:
```haskell
-- Transition from SchemaF to ParamF
methodToParams :: MethodSchema -> [Fix ParamF]
methodToParams method = case methodParams method of
  Nothing -> []
  Just schema -> unfoldParamsFromSchema schema

unfoldParamsFromSchema :: Value -> [Fix ParamF]
unfoldParamsFromSchema = ... -- extract properties, unfold each
```

### Phase 3: ParamF Algebras

New module `Synapse.Algebra.Param`:
```haskell
-- Help generation over params
paramHelpAlgebra :: ParamF Text -> Text

-- Completion over params
paramCompletionAlgebra :: ParamF [Text] -> [Text]

-- Parsing over params
paramParseAlgebra :: ParamF (ParseState -> Either ParseError ParseState)
                  -> (ParseState -> Either ParseError ParseState)
```

### Phase 4: Integration

Update existing modules to use ParamF at the transition point:
- `Synapse.Algebra.Render` - compose with `paramHelpAlgebra` for method help
- `Synapse.Algebra.Complete` - compose with `paramCompletionAlgebra` for param completion
- `Main.hs` - replace `parsePathAndParams` with `paramParseAlgebra`

## Relationship to Existing Modules

```
Synapse.Schema.Functor         Synapse.Schema.ParamFunctor
       │                                │
       │  SchemaF                       │  ParamF
       │  (plugin/method)               │  (params/variants/fields)
       │                                │
       ▼                                ▼
Synapse.Algebra.Walk  ───────►  Synapse.Algebra.Param
       │               transition        │
       │               at MethodF        │
       ├─ walkMethods                   ├─ paramHelpAlgebra
       ├─ buildIR                       ├─ paramCompletionAlgebra
       └─ checkAllSupport               └─ paramParseAlgebra
```

**Key principle: Two functors, many algebras, one transition point.**

- SchemaF algebras handle plugin/method space
- ParamF algebras handle parameter space
- At MethodF (leaf of SchemaF), transition to ParamF via `methodToParams`
- Composed algebras can span both categories

## Non-Goals

- **GADTs or type-level complexity**: Keep it simple ADTs with `Functor` instance
- **Single unified functor**: SchemaF and ParamF are separate by design (respects category boundary)
- **Breaking existing CLI**: Current `--key value` continues to work for primitives
- **Requiring schema changes**: Works with existing JSON Schema in `methodParams`
- **Replacing navigate**: Hand-rolled navigation stays; ParamF is for parsing/help/completion

## Open Questions

1. **Should variants be positional or flagged?**
   - Positional: `cone chat by-name haiku35`
   - Flagged: `cone chat by-name --name haiku35`
   - Hybrid: `cone chat by-name haiku35` where single-field variants use positional

2. **How deep to recurse into objects?**
   - Shallow: top-level fields only, nested objects use JSON
   - Deep: full recursive flattening with dot paths

3. **Error recovery in parse algebra?**
   - Fail fast vs collect all errors
   - Suggestion of closest valid completion

4. **Should ParamF be pure or monadic?**
   - Pure: `paramCoalgebra :: ParamSeed -> ParamF ParamSeed`
   - Monadic: `paramCoalgebraM :: ParamSeed -> SynapseM (ParamF ParamSeed)` (for $ref resolution)

## References

- `Synapse.Schema.Functor` - SchemaF definition (plugins/methods)
- `Synapse.Algebra.Walk` - hyloM usage pattern for SchemaF
- `Synapse.Algebra.Navigate` - hand-rolled navigation (not SchemaF-based)
- `Synapse.Schema.Support` - current param analysis (to be replaced by ParamF)
- `Main.hs:parsePathAndParams` - current flat param parsing (to be replaced)
