skmtcdocs

Enrichments shape

The routing structure for client.json#settings.enrichments. Each projection-base factory reads enrichments from a different key path; there is no single uniform shape across all generators.

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.

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

Source: core/dsl/operation/oas/toOasOperationProjectionBase.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:

{
  "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

Source: core/dsl/model/toModelProjectionBase.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).

Example (single-variant — the common case):

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

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

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

GraphQL operation generators

Source: core/dsl/operation/gql/toGqlOperationProjectionBase.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:

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

Per-generator declaration

Each generator declares its accepted enrichment shape via Valibot in gen-x/src/enrichments.ts. toEnrichmentSchema returns the three-scope umbrellav.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:

// 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):

// 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

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

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

// 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

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

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/.

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?

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

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

  • enrichments concept — full mental model
  • client.json schema reference — the broader settings shape
  • skmtc-cli skill §6 — operational configuration
  • skmtc-generator skill §10 card "Adding enrichment options" — authoring perspective

On this page