# IR-Based CLI Architecture

**Status**: Proposal
**Date**: 2026-01-04
**Linked**: substrate, synapse

## Problem Statement

Synapse's CLI currently has redundant JSON Schema parsing across multiple subsystems:

```
                         JSON Schema (from plexus.schema)
                                    │
              ┌─────────────────────┼─────────────────────┐
              │                     │                     │
              ▼                     ▼                     ▼
      IR Builder              Navigate.hs            Render.hs
   (structured IR)        (path navigation)      (help rendering)
              │                     │                     │
              ▼                     ▼                     ▼
   ┌─────────────────┐   ┌─────────────────┐   ┌─────────────────┐
   │ TypeRef, TypeDef│   │ checkParamSupport│   │ extractTypeDesc │
   │ KindEnum, etc.  │   │ isPrimitiveType │   │ renderParamFull │
   └─────────────────┘   └─────────────────┘   └─────────────────┘
         CODEGEN            CLI SUPPORT           CLI HELP
```

### The Duplication

1. **IR Builder** (`Synapse.IR.Builder`) parses JSON Schema into structured IR types (`MethodDef`, `ParamDef`, `TypeRef`, `TypeDef`) for code generation.

2. **Support Module** (`Synapse.Schema.Support`) walks the same JSON Schema to determine CLI support:
   ```haskell
   checkParamSupport :: Text -> Bool -> Value -> ParamSupport
   checkParamSupport name isRequired schema = case schema of
     Object obj
       | KM.member "$ref" obj -> ParamSupport name False (Just "uses $ref")
       | KM.member "oneOf" obj -> ParamSupport name False (Just "uses oneOf")
       | otherwise -> case KM.lookup "type" obj of
           Just (String "string") -> ParamSupport name True Nothing
           -- ... repeated pattern matching on schema structure
   ```

3. **Render Module** (`Synapse.Algebra.Render`) parses JSON Schema ad-hoc for help text:
   ```haskell
   extractTypeDesc :: Value -> (Text, Text)
   extractTypeDesc (Object po) =
     ( case KM.lookup "type" po of { Just (String t) -> t; _ -> "any" }
     , case KM.lookup "description" po of { Just (String d) -> d; _ -> "" }
     )
   ```

### Problems with Current Approach

| Issue | Impact |
|-------|--------|
| **Duplicated parsing logic** | Same JSON Schema patterns detected in 3 places |
| **Inconsistent interpretation** | IR Builder detects tagged unions; Support.hs rejects all `oneOf` |
| **Limited help rendering** | `extractTypeDesc` only shows primitive type, not union variants |
| **Maintenance burden** | Changes to schema interpretation require updates in multiple modules |
| **Lost type information** | Support checking can't leverage IR's richer type model |

## Solution: IR-Based CLI

Build the IR once, then use it for all CLI operations.

### Architecture After Unification

```
JSON Schema (wire format)
    │
    ▼
IR Builder (single transformation)
    │
    ▼
IR (MethodDef, ParamDef, TypeRef, TypeDef)
    │
    ├───────────┬───────────┬───────────┬───────────┐
    ▼           ▼           ▼           ▼           ▼
 CLI Help    Support     Complete    Navigate    Codegen
 Rendering   Check       (tab)       (--path)    (TS, etc)
```

All CLI operations now consume the same IR that codegen uses. The JSON Schema to IR transformation happens exactly once.

## Key Types

The IR types that power this unification are defined in `Synapse.IR.Types`:

### MethodDef

Structured representation of a method with all its parameters:

```haskell
data MethodDef = MethodDef
  { mdName        :: Text           -- "chat"
  , mdFullPath    :: Text           -- "cone.chat"
  , mdNamespace   :: Text           -- "cone"
  , mdDescription :: Maybe Text     -- Documentation
  , mdStreaming   :: Bool           -- Does this method stream?
  , mdParams      :: [ParamDef]     -- Structured input parameters
  , mdReturns     :: TypeRef        -- Return type reference
  }
```

### ParamDef

Parameter with type information and metadata:

```haskell
data ParamDef = ParamDef
  { pdName        :: Text           -- "identifier"
  , pdType        :: TypeRef        -- RefNamed "ConeIdentifier"
  , pdDescription :: Maybe Text     -- "Cone name or UUID"
  , pdRequired    :: Bool           -- Is this required?
  , pdDefault     :: Maybe Value    -- Default value if any
  }
```

### TypeRef

References to types - the edges in the type graph:

```haskell
data TypeRef
  = RefNamed Text                   -- Reference to named type: "ConeIdentifier"
  | RefPrimitive Text (Maybe Text)  -- Primitive with format: ("string", Just "uuid")
  | RefArray TypeRef                -- Array: T[]
  | RefOptional TypeRef             -- Optional: T | null
  | RefAny                          -- Intentionally dynamic (serde_json::Value)
  | RefUnknown                      -- Schema gap (should warn)
```

### TypeDef / TypeKind

Full type definitions - the nodes in the type graph:

```haskell
data TypeDef = TypeDef
  { tdName        :: Text           -- "ConeIdentifier"
  , tdDescription :: Maybe Text     -- Documentation
  , tdKind        :: TypeKind       -- What kind of type
  }

data TypeKind
  = KindStruct [FieldDef]                     -- { fields... }
  | KindEnum Text [VariantDef]                -- Tagged union with discriminator
  | KindStringEnum [Text]                     -- "pending" | "completed"
  | KindAlias TypeRef                         -- type Foo = Bar
  | KindPrimitive Text (Maybe Text)           -- string, number, etc.
```

## CLI Help Rendering from IR

Currently, help rendering parses JSON Schema directly:

```haskell
-- Current: ad-hoc JSON Schema parsing
renderParamDoc (name, propSchema) =
  let (typ, desc) = extractTypeDesc propSchema  -- Parses JSON
  in "--" <> name <> " <" <> typ <> ">"
```

With IR-based rendering:

```haskell
-- Proposed: render from IR
renderParamFromIR :: IR -> ParamDef -> Text
renderParamFromIR ir param =
  let typeSig = renderTypeRef ir (pdType param)
      desc = fromMaybe "" (pdDescription param)
      reqMarker = if pdRequired param then "" else "?"
  in "--" <> pdName param <> " <" <> typeSig <> ">" <> reqMarker <> "  " <> desc

renderTypeRef :: IR -> TypeRef -> Text
renderTypeRef ir = \case
  RefPrimitive t Nothing    -> t
  RefPrimitive t (Just fmt) -> t <> "(" <> fmt <> ")"
  RefNamed name             -> renderNamedType ir name
  RefArray inner            -> renderTypeRef ir inner <> "[]"
  RefOptional inner         -> renderTypeRef ir inner <> "?"
  RefAny                    -> "json"
  RefUnknown                -> "?"

-- Expand named types for rich help
renderNamedType :: IR -> Text -> Text
renderNamedType ir name = case Map.lookup name (irTypes ir) of
  Just td -> case tdKind td of
    KindStringEnum values ->
      T.intercalate "|" values  -- "pending|in_progress|completed"
    KindEnum _ variants ->
      "{" <> T.intercalate "|" (map vdName variants) <> "}"  -- "{by_name|by_id}"
    KindStruct _ -> name  -- Keep struct name as-is
    KindPrimitive t _ -> t
    KindAlias target -> renderTypeRef ir target
  Nothing -> name
```

### Example: Before and After

**Current help output** (from JSON Schema):
```
cone chat - Chat with a cone

  --identifier <any>           Cone name or UUID
  --prompt <string>            User message to send
```

**IR-based help output** (proposed):
```
cone chat - Chat with a cone

  --identifier <{by_name|by_id}>  Cone name or UUID
      by_name: --name <string>    Human-readable cone name
      by_id:   --id <uuid>        Cone UUID

  --prompt <string>               User message to send
```

The IR knows that `ConeIdentifier` is a `KindEnum` with variants, so it can expand them in help.

## Support Checking from IR

Currently, support checking re-parses JSON Schema:

```haskell
-- Current: pattern matches on JSON Schema Value
checkParamSupport name isRequired schema = case schema of
  Object obj
    | KM.member "$ref" obj -> Unsupported "uses $ref"
    | KM.member "oneOf" obj -> Unsupported "uses oneOf"
    | otherwise -> case KM.lookup "type" obj of
        Just (String "string") -> Supported
        ...
```

With IR-based checking:

```haskell
-- Proposed: check TypeRef structure
canCLIRepresent :: IR -> TypeRef -> SupportLevel
canCLIRepresent ir = \case
  RefPrimitive _ _ -> FullSupport
  RefOptional inner -> canCLIRepresent ir inner
  RefArray _ -> Unsupported "array type"
  RefAny -> Unsupported "dynamic type (use -p)"
  RefUnknown -> Unsupported "unknown schema"
  RefNamed name -> canCLIRepresentNamed ir name

canCLIRepresentNamed :: IR -> Text -> SupportLevel
canCLIRepresentNamed ir name = case Map.lookup name (irTypes ir) of
  Nothing -> Unsupported $ "unknown type: " <> name
  Just td -> case tdKind td of
    KindPrimitive _ _ -> FullSupport
    KindStringEnum _ -> FullSupport  -- Tab-completable!
    KindAlias target -> canCLIRepresent ir target
    KindEnum _ variants ->
      -- Check if all variant fields are representable
      let fieldSupports = concatMap (map fdType . vdFields) variants
      in foldl' combineSupport FullSupport
           (map (canCLIRepresent ir) fieldSupports)
    KindStruct fields ->
      Unsupported "nested object structure"

-- Method-level support from IR
checkMethodSupportIR :: IR -> MethodDef -> MethodSupport
checkMethodSupportIR ir method =
  let paramSupports = map (checkParamSupportIR ir) (mdParams method)
      unsupportedRequired = filter (\ps -> not (psSupported ps) && psRequired ps) paramSupports
  in MethodSupport
       { msMethodName = mdName method
       , msFullPath = T.splitOn "." (mdFullPath method)
       , msSupportLevel = if null unsupportedRequired
                          then FullSupport
                          else Unsupported (renderReasons unsupportedRequired)
       , msParamSupports = paramSupports
       }
```

### Key Improvement: String Enums Are Supported

The current JSON Schema parser marks all `oneOf` as unsupported. But the IR distinguishes:

- `KindEnum` - Tagged union with variants (needs subcommand-style parsing)
- `KindStringEnum` - Simple string enum (just a constrained string, fully supported!)

```haskell
-- Example: TodoStatus enum
-- JSON Schema: {"enum": ["pending", "in_progress", "completed"]}
-- IR: TypeDef "TodoStatus" (KindStringEnum ["pending", "in_progress", "completed"])

-- Current: Unsupported (oneOf detected)
-- IR-based: FullSupport (string enum is just a string)
```

## Tab Completion from IR

String enums and tagged union variants provide natural completion candidates:

```haskell
-- Complete from IR type structure
completionsForParam :: IR -> ParamDef -> [Text]
completionsForParam ir param = case pdType param of
  RefNamed name -> completionsForType ir name
  _ -> []

completionsForType :: IR -> Text -> [Text]
completionsForType ir name = case Map.lookup name (irTypes ir) of
  Just td -> case tdKind td of
    KindStringEnum values -> values           -- ["pending", "in_progress", "completed"]
    KindEnum _ variants -> map vdName variants -- ["by_name", "by_id"]
    _ -> []
  Nothing -> []
```

Example session:
```bash
$ synapse substrate cone chat identifier <TAB>
by_name  by_id

$ synapse substrate todo update --status <TAB>
pending  in_progress  completed
```

## Implementation Plan

### Phase 1: IR-Based Support Checking

Replace `Synapse.Schema.Support` internals with IR-based logic:

```haskell
-- Keep the same public API
checkAllSupport :: Path -> SynapseM [MethodSupport]
checkAllSupport path = do
  ir <- buildIR path  -- Build IR once
  pure $ map (checkMethodSupportIR ir) (Map.elems $ irMethods ir)
```

**Benefits:**
- String enums now correctly marked as supported
- Tagged unions can be partially supported (if variants have simple fields)
- Single source of truth for type interpretation

### Phase 2: IR-Based Help Rendering

Add IR-aware rendering to `Synapse.Algebra.Render`:

```haskell
-- New: render method help from IR
renderMethodFromIR :: IR -> MethodDef -> Text
renderMethodFromIR ir method = T.unlines $
  [ mdName method <> " - " <> fromMaybe "" (mdDescription method)
  , ""
  ] ++ map (renderParamFromIR ir) (mdParams method)
```

**Benefits:**
- Tagged unions show their variants
- String enums show their values
- Type documentation from IR propagates to help

### Phase 3: IR-Based Completion

Wire IR type information into the completion system:

```haskell
-- Complete parameter values from IR types
completeParamValue :: IR -> ParamDef -> Text -> [Text]
completeParamValue ir param prefix =
  filter (T.isPrefixOf prefix) (completionsForParam ir param)
```

**Benefits:**
- Enum values completable
- Variant names completable
- Foundation for richer completion (UUIDs, dates, etc.)

## Benefits Summary

| Aspect | Current | IR-Based |
|--------|---------|----------|
| Schema parsing | 3 places | 1 place |
| String enum support | Unsupported | Fully supported |
| Tagged union help | Shows "any" | Shows variants |
| Tab completion | Plugin/method only | Enum values too |
| Maintenance | Update 3 modules | Update IR Builder only |
| Consistency | Each module interprets differently | Single interpretation |

## Relationship to Other Documents

### `unified-cli-tree.md`

That document proposes `ParamF` as a functor for parameter space navigation. IR-based CLI is complementary:

- **ParamF**: Structural functor for recursive param tree traversal
- **IR**: Semantic representation of types for rendering/support/completion

The two can work together: use IR types to seed the ParamF coalgebra:

```haskell
-- ParamF from unified-cli-tree, seeded by IR
paramsToParamTree :: IR -> [ParamDef] -> [Fix ParamF]
paramsToParamTree ir params = map (unfoldParam ir) params
```

### `structured-params.md`

That document proposes moving schema interpretation from clients to substrate (emitting structured `ParamType`/`TypeDef` on the wire instead of raw JSON Schema).

IR-based CLI is the **client-side** implementation of the same idea:
- `structured-params.md`: Substrate emits structured types
- `ir-based-cli.md`: Synapse builds structured types from JSON Schema

If substrate adopts structured params, the IR Builder becomes trivial (just deserialize the wire format). Until then, IR Builder does the interpretation.

### `ir-codegen-chain.md`

That document describes the full codegen pipeline: JSON Schema -> IR -> TypeScript/etc.

IR-based CLI extends the same IR to power CLI features, not just codegen:

```
JSON Schema
    │
    ▼
IR Builder
    │
    ▼
    IR ────────────────────────────────┐
    │                                   │
    ├── hub-codegen (TypeScript)        │
    ├── future codegen (Python, Swift)  │
    │                                   │
    └── CLI features ◄──────────────────┘
         ├── Help rendering
         ├── Support checking
         └── Tab completion
```

## Open Questions

1. **Should IR building be lazy or eager?**
   - Eager: Build full IR at startup, cache it
   - Lazy: Build IR for method on first access
   - Current answer: Eager via `walkSchema irAlgebra`

2. **How to handle IR build failures?**
   - Some complex schemas may not parse cleanly
   - Fall back to raw JSON Schema for those params?
   - Mark as `RefAny` and continue?

3. **Performance of full IR build?**
   - Walking entire plugin tree to build IR is O(n) in methods
   - For large hubs, may want incremental/cached IR
   - Plexus hash provides cache key

## Conclusion

By building the IR once and using it for all CLI operations, we:

1. **Eliminate duplication**: JSON Schema interpretation in one place
2. **Enable richer help**: Tagged unions expand to show variants
3. **Support more methods**: String enums correctly identified as CLI-compatible
4. **Lay foundation for completion**: Type structure provides completion candidates
5. **Maintain consistency**: All CLI features share the same type model
