# Enrichments shape



Enrichments are how stock generators expose user-facing options
without compromising the clone-to-customize philosophy. This
reference documents the actual routing read by each projection-base
factory; for the mental model see the
[enrichments concept](/docs/concepts/enrichments).

## Three routing shapes [#three-routing-shapes]

The key path is hardcoded inside each projection-base factory, so
the shape depends on which factory the generator was built from.
There are three:

### OAS operation generators [#oas-operation-generators]

Source: `core/dsl/operation/oas/toOasOperationProjectionBase.ts`:

```ts
get(context.settings, ['enrichments', config.id, operation.path, operation.method, variant])
```

Four levels — the subject leaf sits under a trailing `variant` key:

```
enrichments
  └── [generatorId]       e.g., "@skmtc/gen-shadcn-form"
       └── [path]         e.g., "/customers" or "/orders/{id}"
            └── [method]  e.g., "post", "get", "put"
                 └── [variant]  "main" by default
                      └── { ...subject leaf }
```

`path` is the literal OpenAPI path string (including curly-brace
parameters). `method` is the lowercase HTTP verb. `variant` is
`"main"` unless the generator declares extra variants (and `"main"`
must be present whenever any variant is).

Example `client.json` fragment. The subject leaf's internal shape
is defined by the generator's Valibot schema:

```jsonc
{
  "settings": {
    "enrichments": {
      "@skmtc/gen-shadcn-form": {
        "/customers": {
          "post": {
            "main": { "title": "Create Customer", "submitLabel": "Save" }
          }
        },
        "/orders/{id}": {
          "put": {
            "main": { "title": "Edit Order", "submitLabel": "Update" }
          }
        }
      }
    }
  }
}
```

### Model generators [#model-generators]

Source: `core/dsl/model/toModelProjectionBase.ts`:

```ts
get(context.settings, `enrichments.${config.id}.${refName}.${variant}`)
```

Three levels:

```
enrichments
  └── [generatorId]    e.g., "@scope/gen-zod-variants"
       └── [refName]   e.g., "Customer"
            └── [variant]  e.g., "main" | "coercive"
                 └── { ...enrichment payload }
```

`refName` is the schema component name as it appears under
`components.schemas` in the source document. `variant` defaults to
`'main'` when no variants are declared; whenever any variant is
declared, `'main'` MUST be present (engine throws via
`toVariantList` otherwise — see
[`concepts/variants.md`](/docs/concepts/variants)).

Example (single-variant — the common case):

```jsonc
{
  "settings": {
    "enrichments": {
      "@skmtc/gen-zod": {
        "UserModel":  { "main": { "description": "A user account" } },
        "OrderModel": { "main": { "description": "A customer order" } }
      }
    }
  }
}
```

Example (multi-variant — variants-aware model generator):

```jsonc
{
  "settings": {
    "enrichments": {
      "@scope/gen-zod-variants": {
        "Customer": {
          "main":     { "coerce": false },
          "coercive": { "coerce": true }
        }
      }
    }
  }
}
```

### GraphQL operation generators [#graphql-operation-generators]

Source: `core/dsl/operation/gql/toGqlOperationProjectionBase.ts`:

```ts
get(context.settings, ['enrichments', config.id, operation.rootKind, operation.fieldName, variant])
```

Four levels — the subject leaf sits under a trailing `variant` key:

```
enrichments
  └── [generatorId]       e.g., "@skmtc/gen-graphql-x"
       └── [rootKind]     "query" | "mutation" | "subscription" (lowercase)
            └── [fieldName]
                 └── [variant]  "main" by default
                      └── { ...subject leaf }
```

Example:

```jsonc
{
  "settings": {
    "enrichments": {
      "@skmtc/gen-graphql-x": {
        "mutation": {
          "createUser": { "main": { "title": "Create User" } }
        },
        "query": {
          "user": { "main": { "label": "User detail" } }
        }
      }
    }
  }
}
```

## Per-generator declaration [#per-generator-declaration]

Each generator declares its accepted enrichment shape via Valibot in
`gen-x/src/enrichments.ts`. `toEnrichmentSchema` returns the
**three-scope umbrella** — `v.object({ subject, generator, stack })` —
not a flat payload. The per-item leaf (what a user writes for one
operation/model, under the `variant` key above) lives under
`subject`; the two run-constant scopes are declared `v.undefined()`
when unused:

```ts
// gen-shadcn-form/src/enrichments.ts
import * as v from 'valibot'
import { lensInputModuleType, moduleSelect } from '@skmtc/core'

export const formFieldItem = v.object({
  // `moduleSelect.schemaPath` is the join key (no separate `id`).
  moduleSelect: v.pipe(moduleSelect(lensInputModuleType), v.title('Input')),
  label: v.optional(v.string()),
  placeholder: v.optional(v.string()),
  references: v.optional(v.string())
})

// The subject-scoped leaf — the per-operation form override.
export const formSchema = v.optional(
  v.object({
    title: v.optional(v.string()),
    description: v.optional(v.string()),
    submitLabel: v.optional(v.string()),
    fields: v.optional(v.array(formFieldItem))
  })
)

// The three-scope umbrella. This generator only reads `subject`.
export const enrichmentSchema = v.object({
  subject: formSchema,
  generator: v.undefined(),
  stack: v.undefined()
})

export type EnrichmentSchema = v.InferOutput<typeof enrichmentSchema>
export const toEnrichmentSchema = () => enrichmentSchema
```

The schema is registered via the generator's entry function
(`toEnrichmentSchema` is a **required** config field):

```ts
// gen-shadcn-form/src/mod.ts
export const ShadcnFormEntry = toOasOperationEntry<EnrichmentSchema>({
  id: denoJson.name,
  toEnrichmentSchema,
  // ...
})
```

**The Valibot schema is the canonical source of truth for what
the enrichment payload accepts.** To know what a user writes in
`client.json` under the routing keys, read the `subject` member of
the generator's `enrichmentSchema` — for `gen-shadcn-form` above
that's the object with `title`, `description`, `submitLabel`,
`fields`. A no-enrichment generator passes core's
`emptyEnrichmentSchema` (the umbrella with all three scopes
`v.undefined()`).

## Validation behavior [#validation-behavior]

For each Projection the engine builds:

1. The factory's static `toEnrichments({ operation | refName, context })`
   does the `get(context.settings, ...)` lookup at the path shown
   above for that projection-base kind.
2. The looked-up value (which may be `undefined`) is parsed against
   the generator's declared Valibot schema via `v.parse(schema, value)`.
3. The parsed value becomes `this.settings.enrichments` inside the
   Projection.

Outcomes:

* **Unknown keys**: silently stripped (Valibot default).
* **Missing optional keys**: arrive as `undefined`.
* **Type mismatch on required key**: surfaces as a parse error.
* **Whole payload missing**: most stock generators wrap their
  schema in `v.optional(...)`, so the value arrives as `undefined`.

## Consumption in Projection constructors [#consumption-in-projection-constructors]

The validated, routed payload is available at `this.settings.enrichments`:

```ts
// gen-shadcn-form/src/ShadcnForm.ts
override toString(): string {
  const { title, description, submitLabel } = this.settings.enrichments.subject ?? {}

  return `(${this.parameter}) => {
    return (
      <Form>
        ${title ? `<h2>${title}</h2>` : ''}
        ${description ? `<p>${description}</p>` : ''}
        ${this.fields}
        <Button>${submitLabel || 'Submit'}</Button>
      </Form>
    )
  }`
}
```

The Projection assumes the shape matches the declared Valibot schema
— the engine has already validated. Optional fields may be
`undefined`; the code handles that with `??` defaults.

## What enrichments aren't [#what-enrichments-arent]

Enrichments are **not** a general configuration system. They expose
specifically what each generator's author decided to make
user-configurable.

If you need behavior the generator's enrichment schema doesn't
support:

* **Stop**: enrichments aren't the answer.
* **Clone the generator**: edit the source for behavioral changes.
* **Optionally**: contribute an enrichment field upstream if the
  change is generally useful.

Unknown keys are stripped silently, so attempts to encode behavior
the schema doesn't anticipate will appear to do nothing.

## Common questions [#common-questions]

### How do I know which routing shape a generator uses? [#how-do-i-know-which-routing-shape-a-generator-uses]

Read the first line inside `src/base.ts` — it calls one of
`toOasOperationProjectionBase`, `toModelProjectionBase`, or
`toGqlOperationProjectionBase`. That call determines the routing
shape.

Stock generators are documented in
[reference/stock-generators/](/docs/reference/stock-generators).

### Can I share enrichment payloads across operations? [#can-i-share-enrichment-payloads-across-operations]

Not via the schema. The routing requires repeating the payload for
each `(path, method)`, `refName`, or `(rootKind, fieldName)`. If
you have shared values, duplicate manually — there is no wildcard.

### Can enrichments arrive at a Snippet? [#can-enrichments-arrive-at-a-snippet]

Indirectly. Snippets don't have a `settings` object, but their
parent Projection does. A Snippet can be passed enrichment-derived
values via constructor arguments:

```ts
new MyFieldSnippet({
  context,
  name,
  label: parent.settings.enrichments.subject?.fields
    ?.find(f => f.moduleSelect.schemaPath.at(-1) === name)?.label,
  destinationPath
})
```

The parent does the enrichment lookup; the Snippet receives the
result.

## Cross-references [#cross-references]

* [enrichments concept](/docs/concepts/enrichments) — full mental model
* [client.json schema reference](/docs/reference/settings/client-json-schema) — the broader settings shape
* skmtc-cli skill §6 — operational configuration
* skmtc-generator skill §10 card "Adding enrichment options" — authoring perspective
