crouton-generation-reference
Verified lookup pack for crouton's schema→CRUD generation domain — the fieldsFile JSON format, the complete field-type catalog with aliases, the naming contract, the exact artifact list one generate r
SKILL.md.claude/skills/crouton-generation-reference/SKILL.md
GitHubCrouton Generation Reference
The drift-corrected lookup tables for the generation domain: what a schema file is, what one generate run produces, and exactly how everything is named. Every command/path/line here was verified against the repo on 2026-07-02; where the shipped docs are wrong, this file says so explicitly.
When to use / when NOT to use
| You need | Go to |
|---|---|
| The generate workflow (gather fields → config → schema-review gate → generate → post-gen steps), MCP tool workflow, worked examples | crouton skill (.claude/skills/crouton.md) — defer to it; this file is the lookup pack under it |
| Human sign-off on a data model before generating | schema-review skill (#314) |
Migration mechanics, the .nuxt/hub/db/schema.mjs build-first gotcha (#523), package-owned tables |
db-migrations skill |
| Why the system is shaped this way (two-halves rule, invariants, registries) | sibling crouton-architecture-contract |
| Layout engine theory (LayoutTree, viability math, placer internals) | sibling crouton-layout-reference — §6 here covers only the generate→seed handoff |
| Booting/seeding a running app, inspecting the DB | sibling crouton-run-and-operate |
Field-meta long-form descriptions, pagination/FK-filter details, crouton add/init/db-pull |
packages/crouton-cli/CLAUDE.md (owns them; §9 lists its known-stale spots) |
Vocabulary (crouton terms, defined once)
- Collection — one CRUD entity (e.g.
products): a generated, self-contained nested Nuxt layer with form, list, composable, team-scoped API, Drizzle schema. Always team-scoped (every row hasteamId). - Layer — here: the domain folder
layers/<layer>/grouping collections (e.g.shop). (Distinct from Nuxt-layer-as-package; seecrouton-architecture-contract.) - fieldsFile — the JSON file defining a collection's fields (§1). The input to everything.
crouton.config.js— unified config at the app root: collections + targets + dialect for the CLI;featuresis generation input only — a package is switched on byextendsinnuxt.config.ts, not byfeatures(#2286). Full key reference:packages/crouton-cli/CLAUDE.md"Config File Format".- Manifest (
crouton.manifest.ts) — per-package declaration of field types, reserved names, detection patterns. The source of truth for field types (§2). - The CLI —
packages/crouton-cli, binscrouton(=crouton-generate) andcrouton-seed(verifiedpackage.jsonbin map). In an app with@fyit/crouton-clias devDep:pnpm exec crouton <cmd>.
1. fieldsFile JSON format
Shape (each top-level key is a field name):
{
"id": { "type": "string", "meta": { "primaryKey": true } },
"name": { "type": "string", "meta": { "required": true, "maxLength": 255, "translatable": true } },
"price": { "type": "decimal", "meta": { "precision": 10, "scale": 2 } },
"categoryId": { "type": "string", "refTarget": "categories" },
"authorId": { "type": "string", "refTarget": "users", "refScope": "adapter" }
}
Per-field keys: type (§2), meta (below), refTarget (name of the referenced collection — generates a CroutonFormReferenceSelect in the form, an FK query-param filter on the list endpoint, and cache auto-refresh), refScope ('adapter'/'external' for refs outside the generated layers, e.g. users; handled in lib/generators/{form-component,list-component,database-queries}.ts).
meta keys are owned by packages/crouton-cli/CLAUDE.md "Field Meta Properties" — read the full catalog there. What that doc doesn't say (verified behaviour):
| meta key | Non-obvious verified behaviour |
|---|---|
nullable |
.nullish() instead of .optional() in Zod; json and date types get .nullish() automatically (generate-collection.ts) |
translatable |
value lives in the collection's translations JSON column; root column is a cache/fallback — see trap below |
component |
besides overriding the form control, it triggers that package's generator contribution via manifest detects, even without a feature flag |
Trap — translatable fields must be required: false. The real value lives in translations.{locale}.field; a NOT NULL root column fails inserts that only populate translations. (crouton-cli/CLAUDE.md "Why this matters".)
Three ways a field becomes translatable (merged, in this priority — verified generate-collection.ts:678-735): field-level meta.translatable: true → collection-level translatable: true in config (auto-detects common names: name, title, description, label, content, … on string/text fields) → config-level translations.collections.{plural}: [fields].
Never declare (auto-generated on every collection): id, teamId, owner, createdAt, updatedAt, createdBy, updatedBy; hierarchy adds parentId, path, depth, order. Reserved field names additionally include optimisticId, optimisticAction (verified packages/crouton-core/crouton.manifest.ts:27-37). Reserved collection names: api, server, app, pages, components, composables, layouts, middleware, plugins, assets, public, node_modules (same file, :39-43).
2. Field-type catalog
Source of truth is per-package manifests, not any doc table. getTypeMapping() in packages/crouton-cli/lib/utils/manifest-loader.ts (~:297) builds the map from every crouton.manifest.ts; core's types live in packages/crouton-core/crouton.manifest.ts:12-25. The tables in crouton-cli/CLAUDE.md and crouton-mcp/CLAUDE.md are copies (the MCP one is explicitly a fallback used only when manifest loading fails — packages/crouton-mcp/src/utils/field-types.ts).
Verified from the core manifest + the SQLite branch of lib/generators/database-schema.ts:89-100:
| Type | SQLite column (actual) | Zod | TS | Default | Form component | Aliases |
|---|---|---|---|---|---|---|
string |
text() |
z.string() |
string |
'' |
UInput |
|
text |
text() |
z.string() |
string |
'' |
UTextarea |
|
number |
integer() |
z.number() |
number |
0 |
UInputNumber |
integer |
decimal |
real() |
z.number() |
number |
0 |
UInputNumber |
|
boolean |
integer({ mode: 'boolean' }) |
z.boolean() |
boolean |
false |
UCheckbox |
|
date |
integer({ mode: 'timestamp' }) |
z.date() |
Date | null |
null |
CroutonCalendar |
datetime |
json |
jsonColumn() |
z.record(z.string(), z.any()) |
Record<string, any> |
{} |
UTextarea |
|
repeater |
jsonColumn() |
z.array(z.any()) (typed when meta.properties) |
any[] |
[] |
CroutonFormRepeater |
|
array |
text() |
z.array(z.string()) |
string[] |
[] |
UTextarea |
|
reference |
text() |
z.string() |
string |
'' |
CroutonFormReferenceSelect |
|
image |
text() |
z.string() |
string |
'' |
CroutonImageUpload → assets picker¹ |
|
file |
text() |
z.string() |
string |
'' |
CroutonImageUpload → assets picker¹ |
¹ crouton-assets' manifest declares detects: { fieldTypes: ['image','file'], refTargetPatterns: ['asset','file','image','media'] } (verified packages/crouton-assets/crouton.manifest.ts), so its generator contribution swaps in CroutonAssetsPicker (crop for image) whenever those fields appear — no feature flag needed.
Notes:
referenceexists as a first-class type, but in practice relationships are written astype: "string"+refTarget— the user-facing table in thecroutonskill omitsreferencefor that reason.- Alias resolution (#285):
loadFieldsresolves aliases to their canonical type before any generator runs (each mapping entry carriescanonical—manifest-loader.ts:303-318). History:datetimeonce leaked through unresolved → a raw<UInput>+ atextcolumn. Rules of thumb:number= integer column;decimal= float/real; a string-encoded enum ('0'/'1'/'2') isstring, notnumber. - Adding a field type = a manifest change (plus generator branches) — see crouton-cli/CLAUDE.md "Add a new field type".
3. Dialect defaults — split by entry point
Code truth (the docs were corrected to match in the #1073 reshape):
| Entry point | Dialect default | Where |
|---|---|---|
crouton <layer> <collection> (direct generate) |
sqlite |
bin/crouton-generate.js |
crouton init <name> |
sqlite |
bin/crouton-generate.js |
crouton config |
the config file's dialect; if the config omits it → 'pg' |
lib/generate-collection.ts (the config subcommand has no --dialect flag at all) |
Rule: always write dialect: 'sqlite' explicitly in crouton.config.js (every in-repo app targets D1/SQLite) — the silent pg fallback on the config path is the trap.
4. Naming contract
Everything derives from toCase() (lib/utils/helpers.ts — real English pluralization via the pluralize npm package + scule casing; categories↔category works, naive +s does not apply). Given collection products in layer shop:
| Artifact | Name | Derivation |
|---|---|---|
| Directory | layers/shop/collections/products/ |
layer + lowercase plural |
| API base | /api/teams/[id]/shop-products/ |
{layer}-{plural} |
| Patch/delete route files | [productId].patch.ts, [productId].delete.ts |
[{singularCamel}Id] — NOT [id] (docs say [id]; code is generate-collection.ts:932-937, fixture confirms [itemId].patch.ts) |
| Components | ShopProductsForm, ShopProductsList |
the collection's own nuxt.config.ts registers app/components with prefix: 'ShopProducts', global: true |
| Composable | useShopProducts (file useShopProducts.ts) |
use{LayerPascal}{PluralPascal} |
Registry key in croutonCollections |
shopProducts (+ import shopProductsConfig) |
{layerCamel}{PluralPascal} (generate-collection.ts:1121-1122; mirrored by compose-layout.ts registryKeyFor()) |
| Drizzle schema export | shopProducts |
same |
| DB table | shop_products |
toSnakeCase(\${layer}_${plural}`) (database-schema.ts:171; confirmed by a real seed.json's "table": "bookings_locations"`) |
| Test files | ShopProducts.test.ts, ShopProducts.api.test.ts |
{LayerPascal}{PluralPascal} |
System-collection exception: a layer named crouton-* gets API path crouton-<collection-kebab> instead of {layer}-{plural} (generate-collection.ts:882-888) — e.g. layer crouton-events + collectionEvents → /api/teams/[id]/crouton-collection-events/. Its table is toSnakeCase('crouton' + Collection) (database-schema.ts:168).
5. Exact artifact list of one generate run
Verified against writeScaffold (lib/generate-collection.ts:880-1155) and on-disk generated collections (fixtures/minimal/layers/main/collections/items/, pocs/booking-demo/layers/bookings/collections/*/).
Per collection, under layers/{layer}/collections/{plural}/:
app/components/_Form.vue # NOTE THE UNDERSCORE — see below. Skipped when
# formComponent is set (config or auto-detected)
app/components/List.vue
app/components/{FieldPascal}/{Input,Select,CardMini}.vue # one dir per repeater field
app/composables/use{Layer}{Plurals}.ts # Zod schema + columns + collection config
server/api/teams/[id]/{layer}-{plural}/
index.get.ts index.post.ts [{singular}Id].patch.ts [{singular}Id].delete.ts
[{singular}Id]/move.patch.ts + reorder.patch.ts # hierarchy only
reorder.patch.ts # sortable (non-hierarchy) only
server/database/schema.ts # Drizzle (+ translations JSON column when translatable)
server/database/queries.ts # getAll* with FK-filter + opt-in pagination overloads
server/database/seed.ts # only with seed flag (drizzle-seed + Faker)
seed.json # always, except hierarchy collections (#298) — editable sample rows
{Layer}{Plurals}.test.ts # Zod schema-smoke (#785) — only with --tests / tests:true (off by default, #2286)
{Layer}{Plurals}.api.test.ts # mocked route-handler test (#791), carries // @ts-nocheck — same gate
types.ts nuxt.config.ts README.md
The _Form.vue underscore: the actual filename is _Form.vue (generate-collection.ts:912) even though the dry-run output prints Form.vue (:825) and both the crouton skill and crouton-cli/CLAUDE.md draw Form.vue. It still registers as {Layer}{Plurals}Form — the composable's componentName field carries the name (verified fixtures/minimal/.../useMainItems.ts:44: componentName: 'MainItemsForm').
Side effects beyond the collection dir (config-mode run, same file :1091-1152 + runPostGeneration :1165-1293):
| Surface | What happens |
|---|---|
layers/{layer}/nuxt.config.ts |
created/updated to extend the collection |
root nuxt.config.ts |
./layers/{layer} added to extends; enabled feature packages synced in |
app/app.config.ts |
collection upserted into croutonCollections (import + entry) |
| i18n locale JSON | created when the collection has translatable fields |
server/database/schema/index.ts |
named schema export added per collection (config mode automates the "manual export" step the crouton skill lists) |
| migration | drizzle-kit runs directly on the resolved schema graph — no Nuxt build (#1445 WS2). ~2s. On a cold scaffold it soft-defers (↻ Migration deferred … re-run: pnpm db:generate, exit 0) rather than producing it in the same run — #1457. The old npx nuxt db generate 30s-timeout/silent-zero-migrations failure (#523) is retired by design on resolver apps; still possible on an external legacy .nuxt-bundle scaffold (no app in this repo since #1456) → db-migrations skill |
| CSS | Tailwind @source directive ensured |
.nuxt-adjacent registries |
type registry + server/utils/crouton-query-registry.ts (lazy per-collection query lookup) |
crouton.layout.json (app root) |
deterministic default layout (§6) |
| devtools | generation recorded for the DevTools Generators tab |
Real-world caveat: apps generated before #298/#709 (e.g. apps/velo, fixtures/*) lack seed.json-era artifacts at root (crouton.layout.json, query registry) — absence there is history, not a bug. pocs/booking-demo is a current-pipeline example with all of them.
6. Seed + default-layout pipeline (generate → booted app)
Mechanics of crouton-seed (discovery/topo-sort/wrangler transport) are owned by packages/crouton-cli/CLAUDE.md "App Seeding" — current and accurate. What this reference adds, verified from code:
- Layout pass (#709) —
runPostGeneration→lib/compose-layout.tswriteDefaultLayout()callscomposeDefaultLayoutfrom@fyit/crouton-layout/app/utils/layout-compose(a stale comment at the top of compose-layout.ts still says "from crouton-core"; the import at:16is truth — it moved in #751) and writescrouton.layout.json:{ id: 'default', renderer, pattern, viable, tree }. Patterns:calendar-primary|master-detail|form-centric|stacked|empty(verifiedlayout-compose.ts:54). Theory → siblingcrouton-layout-reference.- Drift hazard (declared in code,
compose-layout.ts:22-26): the CLI has no live app.config at generate time, so it hardcodes mirror copies of the block sizing contracts (CORE_BLOCKS/BOOKINGS_BLOCKS). Changing aminWidth/defaultSizeincrouton-layout/crouton-bookingsapp.config must be mirrored here — the hazard and the current values are owned by siblingcrouton-layout-reference§6.
- Drift hazard (declared in code,
- Package seed providers —
crouton-seed --db <name> [--remote|--dry-run]collects each package's<pkg>/seedprovider into idempotent upsert SQL, executed viawrangler d1 execute. - Collection fixtures — each
layers/*/collections/*/seed.json({ table, key, rows }— verified shape inpocs/booking-demo) is upserted with injected system columns + stableseedId(layer, collection, key)ids (lib/seed-app.ts:180-193). - Layout row —
collectDefaultLayoutSql(seed-app.ts:213-242) upserts the tree intolayout_configsrow iddefault— the row the team layout page loads. NB (code comment):layout_configs.idis a global PK, so this seeds one default layout for the single seeded team only.
Result: a fresh POC boots laid out, not blank. The layout is data (editable in CroutonLayout), never generated .vue.
7. Deterministic vs AI-assisted
| Deterministic (no LLM anywhere) | AI-assisted (the agent is the intelligence) |
|---|---|
All template generation (the 16 generators in lib/generators/ are string templates keyed off the schema) |
Schema design — the AI writes the fieldsFile; MCP design_schema/init_schema only feed it reference material |
| Type/alias resolution; Zod/TS/Drizzle mapping (manifest-driven) | The human schema-review gate (#314) on top of machine validation |
MCP validate_schema (pure structural check), dry_run, rollback |
Optional runtime AI features (crouton-ai translate, alt-text) — not part of generation |
The default-layout pass — "No LLM" is stated in compose-layout.ts's own header; the LLM /layout pass (#711) is gated and out of scope |
Layout iteration via the agent⇄human ticket loop (sanitizer keeps ingest deterministic) |
| Seeding (SQL assembly, stable ids, topo-sort; Faker field-name heuristics are heuristic, not AI) | |
Generated tests (samples derived from each field's Zod; no Date.now()/Math.random()) |
Practical consequence: generation output is reproducible and diff-reviewable; the only judgment calls are the schema itself and any post-hoc layout edits — which is exactly where the sign-off gates sit.
8. Rollback & regeneration semantics
Rollback (crouton rollback <layer> <collection> [--dry-run] [--keep-files] [--force]) — verified lib/rollback-collection.ts:252-300, five steps:
- delete
layers/{layer}/collections/{plural}/(skipped by--keep-files) - remove the schema export from
server/database/schema/index.ts - remove the
croutonCollectionsentry + import fromapp.config.ts - remove the collection from the layer root
nuxt.config.ts - remove
./layers/{layer}from the rootnuxt.config.tsonly if no other collections remain in the layer
Also: crouton rollback-bulk --layer=<layer> / --config=<path> (whole layer / everything in a config) and crouton rollback-interactive (selection UI). The MCP rollback tool defaults dryRun: true (verified crouton-mcp/src/tools/rollback.ts:7); the CLI's does not — pass --dry-run yourself first.
Rollback does NOT touch the database: no migration is reverted, no table dropped, no rows deleted. Clean those up separately (a follow-up migration, or accept the orphan table on throwaway POCs).
Regeneration: re-running crouton config / crouton <layer> <collection> overwrites every generated file unconditionally — writeScaffold has no file-existence guard (fsp.writeFile loop at generate-collection.ts:1074-1078; code-derived, not reproduced end-to-end). Two consequences:
- The generated file headers saying "regeneration requires --force flag" are stale;
--forceactually means (a) proceed despite missing package dependencies (:747-754) and (b) override schema-index export conflicts (:373-384). - Hand edits inside generated files are lost on regeneration — including the "editable"
seed.json(it is in the unconditional write list,:1042-1049). Editseed.jsononly if you won't regenerate, or re-apply edits after. If you customize a Form, preferformComponent:(skips_Form.vuegeneration entirely) over editing the generated one.
9. Doc drift in this domain
The 2026-07-02 sweep's corrections (docs paths, docs-MCP tool names, dead nuxt-crouton-* package paths, the --dialect default, _Form.vue/[{singular}Id].patch.ts filenames, the sync-script extension, the manifest-vs-fallback field-type source) were applied to the canonical docs in the #1073 reshape — .claude/skills/crouton.md, packages/crouton-cli/CLAUDE.md, packages/crouton-mcp/CLAUDE.md now match the code. Don't trust this skill over them; if they disagree again, code wins (trust order: sibling crouton-docs-trust-map §1) and the fix goes to the canonical doc via the sync-docs workflow, not to a table here.
One known drift remains in code, not docs: generated file headers still claim "regeneration requires --force" while regeneration actually overwrites unconditionally (§8) — that's a template fix in packages/crouton-cli/lib/generators/, not a doc edit.
Provenance and maintenance
verified: 2026-07-02
Facts verified against: packages/crouton-cli/{bin/crouton-generate.js, lib/generate-collection.ts, lib/compose-layout.ts, lib/seed-app.ts, lib/rollback-collection.ts, lib/utils/{helpers.ts,manifest-loader.ts}, lib/generators/database-schema.ts, package.json}, packages/crouton-core/crouton.manifest.ts, packages/crouton-assets/crouton.manifest.ts, packages/crouton-layout/app/utils/layout-compose.ts, packages/crouton-mcp/src/{index.ts,tools/rollback.ts}, and on-disk generated output in fixtures/minimal/ + pocs/booking-demo/. Line numbers are anchors from that date and will drift — trust the symbol names over the :NNN. Issue numbers (#285, #298, #523, #558, #709, #711, #751, #785, #791) are cited from code comments and discovery-report summaries; check the issue if load-bearing.
Re-verification one-liners:
# dialect defaults (expect sqlite in bin, || 'pg' in runConfig)
grep -n "dialect" packages/crouton-cli/bin/crouton-generate.js | head -5
grep -n "config.dialect ||" packages/crouton-cli/lib/generate-collection.ts
# field-type catalog + aliases (the source of truth)
sed -n '12,25p' packages/crouton-core/crouton.manifest.ts
# artifact list / filenames (underscore Form, [xId] routes)
grep -n "_Form.vue\|Id].patch" packages/crouton-cli/lib/generate-collection.ts | head -5
# naming (registry key, table name)
grep -n "registryKeyFor\|snakeCaseTableName" packages/crouton-cli/lib/{compose-layout.ts,generators/database-schema.ts}
# layout payload + seed row id
grep -n "id: 'default'\|layout_configs" packages/crouton-cli/lib/{compose-layout.ts,seed-app.ts}
# CLI/manifest/skill field-type sync check
node scripts/validate-field-types-sync.mjs