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 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:
// 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 = () => enrichmentSchemaThe 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:
- The factory's static
toEnrichments({ operation | refName, context })does theget(context.settings, ...)lookup at the path shown above for that projection-base kind. - The looked-up value (which may be
undefined) is parsed against the generator's declared Valibot schema viav.parse(schema, value). - The parsed value becomes
this.settings.enrichmentsinside 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 asundefined.
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
client.json schema
The complete shape of .skmtc/<project>/.settings/client.json — the user-facing project configuration file.
Source resolution
How the CLI locates the schema source, loads it, and normalizes it to a JSON-cloneable OAS 3.0 document (or a raw GraphQL SDL string) before handing it to the engine.