Code generation
Every generator this repo runs: its inputs, what it writes, the command that runs it and the skill that drives it.
Overview
Large parts of this repo are generated, not hand-written: the token contract (Ghost's and the Euro League club's), the icon set, the Contentful GraphQL types, the Competition Engine API types, the localised dictionary maps, the model baseline, the Contentful app lock, the ticket vocabulary and the app's route shims. This page is the single catalogue of every generator — what it reads, what it writes, the exact command, and the Claude skill that drives it where one exists.
Generated files are an OUTPUT, never a source
The rule holds everywhere in the repo: a generated file is the compiled projection of an upstream source, and is never hand-edited. Editing one is always a mistake — the next regeneration silently overwrites it, and in the meantime the file disagrees with the source it claims to derive from.
Each generator therefore has a single source of truth UPSTREAM of its output:
| Layer | Source of truth | Generated output (never edit) |
|---|---|---|
| Tokens | Token Studio tokens.json |
packages/tokens/src/__generated__/tokens.ts |
| Club tokens | Euro League Token Studio tokens.json (pushed straight to the repo) |
packages/tokens-euro-league/src/__generated__/ |
| Icons | Figma icon pages and node lists (Ghost Core, ELB Core, ELB Content) | packages/tokens/src/icons/*.tsx |
| Model schema | The Contentful space (introspected) | packages/model/schema.graphql |
| Model types | schema.graphql + curated *.graphql fragments |
packages/model/src/__generated__/ |
| Dictionary | src/dictionary/keys.ts + Contentful labels |
packages/model/src/__generated__/dictionary/ |
| Model baseline | packages/model/baseline/ export (generated once, when this repo was cut) |
migrations/core/0001-baseline-core.cjs, migrations/<domain>/0000-baseline-*.cjs |
| Contentful apps | packages/model/apps/registry.json + the orgs' app definitions |
packages/model/apps/apps.lock.json |
| Engine API types | packages/competition-engine/spec/openapi.yaml (vendored at a pinned sha) |
packages/competition-engine/src/__generated__/api.d.ts |
| App page types | app *.graphql queries + model fragments |
apps/web · content-app · video-app · basketball-app __generated__/ |
| Route shims | club.config.ts + routes.json + composition.json |
apps/web/src/app/[locale]/**/{page,loading,layout}.tsx (the generated ones) |
| Ticket vocabulary | domains.json + .claude/manifest.json |
packages/tickets/src/__generated__/vocab.ts (+ cms-types.json, figma-frames.json) |
Every one of these carries a // Generated … do not edit header (or is written
under an __generated__/ directory whose name says the same thing).
Drift gates enforce it
Regeneration is deterministic — same input in, same bytes out — so CI can prove a generated file is current simply by regenerating it and asserting the tree is clean. Every layer has such a gate:
- Tokens —
v4-tokens checkparses and validatestokens.jsonwith a non-zero exit on errors;buildreports a contract diff against the previous build's sidecar. - Model —
pnpm --filter @v4/model driftre-syncs the schema, regenerates the model types and the web / content-app / video-app page types, thengit diff --exit-codes the lot. The dictionary maps are deliberately NOT regenerated or gated (labels are editorial copy Contentful owns), and@v4/basketball-appis not in this gate — it has its owndrift. - Apps — each app's own
driftscript regenerates andgit diff --exit-codessrc/__generated__. - Competition Engine —
pnpm --filter @v4/competition-engine verifyregeneratesapi.d.tsand diffs it;check-specconfirms the vendored spec matches its pinned sha. - Contentful apps —
apps.lock.jsonis generated bysync-appsand drift-gated;sync-apps --checkis the PR gate. - Tickets —
pnpm --filter @v4/tickets driftregeneratesvocab.tsand diffs it, then runs the ticket lints. - Composition —
pnpm compose -- --checkregenerates the shims + composition manifest in memory and diffs against disk.
If a drift gate is red, the fix is to run the generator and commit its output — never to reconcile the file by hand.
Every generator at a glance
| Generator | Reads | Writes | Command | Skill |
|---|---|---|---|---|
| Token contract | packages/tokens/tokens.json (Token Studio export) |
packages/tokens/src/__generated__/tokens.ts + .css + per-component contracts |
pnpm --filter @v4/tokens generate (sync-tokens to refresh) |
/generate-token-contracts, /extract-figma-tokens, /sync-figma-styles |
| Club token layer | packages/tokens-euro-league/tokens.json |
packages/tokens-euro-league/src/__generated__/ (incl. competition-themes.ts) |
pnpm --filter @v4/tokens-euro-league generate |
/generate-token-contracts |
| Icons | Ghost Core, ELB Core and ELB Content Figma files | packages/tokens/src/icons/*.tsx + index.ts (129 icons) |
pnpm --filter @v4/tokens sync-icons |
— |
| Component scaffold | a kebab component name | packages/ghost-core/src/components/<name>/ skeleton + registrations |
pnpm --filter @v4/ghost-core new-component <kebab> |
/generate-from-figma |
| Precompiled styles | each ghost package's built .css.ts |
precompiled component stylesheets in dist/ (ghost-core, -content, -video) |
the package build (precompile-styles.mjs) |
— |
| Model schema (vendor) | the Contentful space/env (GraphQL introspection) | packages/model/schema.graphql |
pnpm --filter @v4/model sync-schema |
/model-migration, /model-review |
| Model types | schema.graphql + packages/model/src/**/*.graphql |
packages/model/src/__generated__/ (gql.ts, graphql.ts, index.ts) |
pnpm --filter @v4/model generate |
/model-fragment |
| Dictionary maps | src/dictionary/keys.ts + Contentful dictionaryKey entries |
packages/model/src/__generated__/dictionary/<locale>.ts |
pnpm --filter @v4/model sync-dictionary |
/model-dictionary |
| Contentful app lock | packages/model/apps/registry.json + the orgs' definitions |
packages/model/apps/apps.lock.json |
pnpm --filter @v4/model sync-apps -- --env=<env> [--apply] |
— |
| Engine API types | packages/competition-engine/spec/openapi.yaml |
packages/competition-engine/src/__generated__/api.d.ts |
pnpm --filter @v4/competition-engine generate (sync-spec first) |
— |
| App page types | app src/**/*.graphql + model fragments |
app src/__generated__/ (in apps/web, content-app, video-app, basketball-app) |
pnpm --filter @v4/web generate (and per app) |
— |
| Route shims + composition | apps/web/club.config.ts + */routes.json + composition.json |
app src/app/[locale]/** (generated) + packages/model/composition.json |
pnpm compose |
/new-route, /add-feature |
| Ticket vocabulary | domains.json + .claude/manifest.json |
packages/tickets/src/__generated__/vocab.ts |
pnpm --filter @v4/tickets gen-vocab |
— |
| Ticket CMS model / frames | @v4/model migrations (or the live space) / the Figma files |
packages/tickets/src/__generated__/cms-types.json / figma-frames.json |
pnpm --filter @v4/tickets build-cms-model / sync-figma-frames |
/ticket-setup |
The model baselines (0001-baseline-core.cjs and the bolt-ons'
0000-baseline-*.cjs) were generated once, when this repo was cut from the
template, and are never regenerated here — see
GraphQL and the model.
The three sections that follow document the main generators in detail: Tokens and icons, GraphQL and the model, and Components and shims.
Tokens and icons
Both the token contract and the icon set are generated by @v4/token-parser —
the v4-tokens CLI (packages/token-parser/src/cli.ts, published as the
v4-tokens bin). It has four commands: build, check, icons and coverage,
plus a drift command that compares Figma file versions against the bridges.
Options: -i/--input, -o/--out, -n/--name, --emit ts,css (build),
-k/--file-key, --page (default - Icons), --filter, --nodes (icons),
--registry (drift), -c/--components and --write (coverage).
The token contract
v4-tokens build reads the Token Studio tokens.json export, resolves it
(references, and Token Studio modify extensions — brand Tints/Shades are a
reference plus a transform, not literal hex), and emits the typed contract.
Reads: packages/tokens/tokens.json — the Ghost 2.0 Token Studio export
(the ghost build; the club package runs the same command, below).
Writes (under packages/tokens/src/__generated__/):
| File | Contents |
|---|---|
tokens.ts |
The full typed token tree — packages/tokens/src/__generated__/tokens.ts is the token source of truth in code |
tokens.css |
The CSS custom-property layer (--ph__*) |
components/<domain>/*.ts |
Per-component contract modules, sliced by namespace, under components/{core,content,football,gamification,video}/ (every Ghost namespace is generated, bought or not) |
tokens.flat.json |
A flat sidecar baseline used to print a contract diff on the next build |
Every generated file opens with // Generated by @v4/token-parser — do not edit by hand. The generated values reach components through ONE ingestion point:
packages/ghost-core/src/theme/default.theme.ts reads tokens and builds the
global contract, and every component theme binds contract paths
(g.components.core.*, g.semantic.*, g.colourSets.*). A component theme
never imports raw tokens (or a ./components/* module) itself — that freezes
the generated literal beyond every club override
(.claude/rules/frontend/theme-variant-patterns.md).
# regenerate the contract from the checked-in tokens.json (+ prettier)
pnpm --filter @v4/tokens generate
# refresh tokens.json from the Ghost export repo, then regenerate
pnpm --filter @v4/tokens sync-tokens
# validate only — non-zero exit on errors (the CI drift gate)
pnpm --filter @v4/tokens check
sync-tokens pulls the latest tokens.json from the ghost-web-tokens-v2
repository via gh api and then runs generate. generate runs the parser's
built dist/ — a src/ fix in @v4/token-parser changes nothing until
pnpm --filter @v4/token-parser build.
The club token layer (@v4/tokens-euro-league)
The club package is generated the same way, into its own tree:
Reads: packages/tokens-euro-league/tokens.json — the Euro League Token
Studio export, which carries the EuroLeague base theme and the EuroCup and
NextGen competition themes.
Writes (under packages/tokens-euro-league/src/__generated__/): tokens.ts,
tokens.css, tokens.flat.json, components/, and competition-themes.ts
(from scripts/generate-competition-themes.mjs — each competition's difference
from the EuroLeague layer). The adapter (src/adapter.ts) projects the club's
names onto the Ghost contract; the brand faces and the club's motion re-tune
(src/fonts.ts, src/motion.ts) are hand-written.
pnpm --filter @v4/tokens-euro-league generate # after every Token Studio push
Do not run the club package's
sync-tokens. It pullstoken.jsonfromstadionHQ/elb-web-tokens, which is no longer the source: Token Studio now pushestokens.jsonstraight to this repo, and a sync overwrites it with an older palette. Rungenerateafter each push instead.
⚡ Do it with Claude —
/extract-figma-tokensextracts a component's Token Studio bindings from Ghost 2.0 Figma into itstokens/directory, and/sync-figma-stylescross-references a component's wiring against Figma and refreshes the generated contract. This section explains what the underlyingv4-tokensbuild produces.
Icons
v4-tokens icons discovers icon components in Figma, exports each as SVG, and
generates a React icon component per master plus a barrel.
Reads: three Figma files via the Figma API (FIGMA_PAT required in the
environment): Ghost – Core 2.0 (67J5FrDZn5HzYzf5hJgoNA), ELB – Web – Core
(HIjezWNeRQSBGkxx8VqmjA) and ELB – Web – Content (9iz5GeR9syO68cmPr1ppE1).
sync-icons makes six icons invocations, in order:
- Ghost Core, the
- Iconspage. - ELB Core, the
- Iconspage. - ELB Core
--nodes— the six plain social icons (Facebook, YouTube, X, Instagram, TikTok, WhatsApp), taken from instances. - ELB Content
--nodes— Feature and Share-Plain. - Ghost Core
--nodes— Chevron-Down and Chevron-Up. - ELB Content
--nodes— the competition marks (EuroLeague, EuroCup, NextGen).
A --nodes list is Name@nodeId,…, for icons that are not masters on an
icons page. Later invocations write over earlier ones with the same name.
Writes (under packages/tokens/src/icons/): one icon-<name>.tsx per icon
(129 today), icon.types.ts (written once if absent), and index.ts — the
barrel, rebuilt to cover every icon present on disk, exported as
@v4/tokens/icons. Each file opens with
// Generated by `v4-tokens icons` from Figma "<node name>" — do not edit by hand.
pnpm --filter @v4/tokens sync-icons
Duplicate masters that collide on the same file base are skipped with a warning (flag to design), and SVG exports are fetched in batches of 50 with a pause between batches to respect Figma rate limits.
Coverage and drift (verification, not generation)
Two more v4-tokens commands support the token work without emitting product
code:
v4-tokens coverage -c <components-dir> [--write]recomputes each component's token-binding coverage as node × property pairs (INSTANCE subtrees excluded) and, with--write, updates thecoverageblock in everytokens/_manifest.json.v4-tokens drift -k <file-key> --registry <figma.json>compares the Figma file's latest version against the version stamped in each component'sfigma.jsonbridge.pnpm --filter @v4/tokens driftruns it against the Ghost Core file and the ghost-core registry.
The Figma scripts beside the CLI
packages/token-parser/scripts/ holds the scripts the client-fork workflow runs
(.claude/rules/frontend/fork-refactor.md). The ELB files are forks of Ghost
that PRESERVE the upstream node ids, so a bridge naming the wrong file resolves
perfectly and measures the wrong design — these are what catch it:
| Script | What it does |
|---|---|
check-bridges.mjs |
OFFLINE gate (no token, runs in scripts/verify.mjs): every bridge names a file this repo is built from, and every cache agrees with it |
design-status.mjs |
The Design / Development status each component carries in the client file (--ready, --unbuilt) |
fork-diff.mjs |
Diffs a component's bindings between the upstream and the client file, ranked by measured difference |
design-drift.mjs |
Re-extracts and reports whether the CLIENT design moved since each cached extraction |
extract-figma-bindings.mjs |
Walks a component's Figma tree and writes the extraction shape the coverage CLI reads, recording the fileKey |
extract-colour-ways.mjs |
Extracts a component's colour-way variants |
The file map these read is .claude/manifest.json (figma.files /
figma.clientFiles).
GraphQL and the model
The @v4/model package is the content model as code. Its generation spine runs:
introspect the space → vendor schema.graphql → generate TypeScript from the
schema + curated fragments → (separately) generate the dictionary maps. Apps
generate their own page-query types against the same vendored schema. Every
output is committed; all but the dictionary maps are drift-gated.
Schema vendoring (sync-schema)
packages/model/scripts/sync-schema.mjs introspects the Contentful GraphQL
schema for the space/environment and writes it to disk. It authenticates with
CONTENTFUL_CMA_TOKEN alone — the GraphQL API wants a DELIVERY token, so the
script derives an environment-scoped delivery key via the CMA api_keys
endpoint, keeping CI to a single secret.
Reads: the Contentful space named by CONTENTFUL_SPACE_ID — for this site
12gd2cvfgb6b ("Euro League") — environment CONTENTFUL_MODEL_ENV (default
develop), via introspection. The model scripts read the space from the
environment or the root .env and REFUSE to run without it; there is no
default space.
Writes: packages/model/schema.graphql — a deterministic SDL:
lexicographicSortSchema applied, all descriptions stripped and every directive
definition dropped, so a freshly cloned environment's schema and the source
env's diff cleanly.
Type SHAPE is the contract; never re-add docstrings or directive definitions.
pnpm --filter @v4/model sync-schema
Model types (generate)
pnpm --filter @v4/model generate runs graphql-codegen --config codegen.ts.
Reads: schema.graphql (the vendored schema) and the curated documents
packages/model/src/**/*.graphql — one {Surface}Fields fragment per component
surface, per domain: src/{core,content,video,basketball}/fragments/*.graphql
(core 6, content 12, video 2, basketball 8).
Writes: packages/model/src/__generated__/ via the graphql-codegen client
preset (fragmentMasking: false, enumsAsTypes: true) — gql.ts, graphql.ts
and index.ts. Import types from @v4/model, never from the codegen paths.
Fragments are curated (prune, never select-all); their types are generated. Codegen validates every field name against the vendored schema and refuses guesses — a fragment for a field no migration has shipped will not compile.
⚡ Do it with Claude —
/model-fragmentauthors a curated fragment and its transform for a content-type surface: an all-fields draft from the vendored schema, a pruning review, then codegen validation.
Dictionary maps (sync-dictionary)
The dictionary is split in two: packages/model/src/dictionary/keys.ts
(hand-curated, reviewed) owns the key vocabulary and its default en-GB copy;
Contentful owns the localised labels editors maintain.
packages/model/scripts/sync-dictionary.mjs has three modes: pull (default),
push, and promote.
pull— reads alldictionaryKeyentries from the environment, writespackages/model/src/__generated__/dictionary/<locale>.ts(en-GB, en-US, fr-FR, it-IT) plusindex.ts. Committed, but deliberately not drift-gated: a label is editorial copy Contentful owns, so gating it turned every editor's label edit into a red build on an unrelated merge. Reports orphans (in the space, not inkeys.ts) and unseeded keys (inkeys.ts, no entry yet).push— creates + publishes entries for unseeded keys (key + default label only). Never updates or deletes existing entries; refuses protected environments outside CI.promote— copies labels env-to-env (--from/--to).
pnpm --filter @v4/model sync-dictionary # pull → regenerate the maps
pnpm --filter @v4/model push-dictionary # seed unseeded keys
The site does not depend on these maps for live copy: it calls
await getDictionary(locale) from @v4/model/dictionary, a server-only read of
the live labels, cached and tagged content:dictionaryKey, so a published
label reaches the site without a deploy. createDictionary(locale) — the
committed snapshot — is the synchronous fallback, for tests and when the fetch
fails.
⚡ Do it with Claude —
/model-dictionaryadds and manages dictionary keys: vocabulary in code, labels in Contentful, seeding and env-to-env promotion.
App page types
Page-level GraphQL queries live in the apps, not in @v4/model — each app
composes model fragments and runs its own codegen against the SAME vendored
schema (packages/model/schema.graphql). None of them introspect Contentful.
| Package | Reads (documents) | Writes | Command |
|---|---|---|---|
apps/web |
src/**/*.graphql + model/src/core/fragments/* + content sitemap.graphql |
apps/web/src/__generated__/ |
pnpm --filter @v4/web generate |
@v4/content-app |
src/**/*.graphql + model/src/content/fragments/* + core link.graphql |
packages/content-app/src/__generated__/ |
pnpm --filter @v4/content-app generate |
@v4/video-app |
src/**/*.graphql + model/src/video/fragments/* |
packages/video-app/src/__generated__/ |
pnpm --filter @v4/video-app generate |
@v4/basketball-app |
src/**/*.graphql + model/src/basketball/fragments/* |
packages/basketball-app/src/__generated__/ |
pnpm --filter @v4/basketball-app generate |
Each app's fragments glob is deliberately narrowed to its own domain, so no
other domain's fragment ASTs land in that package's runtime gql.ts.
The model baseline
The baselines are the model as it stood when this repo was cut from the
template, expressed in the contentful-migration DSL (content types, fields,
editor interfaces), deprecated types and all. They were generated once, by
packages/model/scripts/generate-composed-baseline.mjs from the vendored export
in packages/model/baseline/ plus domains.json and composition.json, and are
never regenerated or hand-edited here:
core/0001-baseline-core.cjs— the core baseline.content/0001-baseline-content.cjs— cut before bolt-on baselines were numbered0000, so it shares its number with the next content migration; the runner always runs a baseline first.video/0000-baseline-video.cjs— numbered to sort before the video domain's own incremental migrations, which start at0001.
generate-baseline.mjs is the template's tool for ITS single core baseline
(core/0001-baseline.cjs); this repo does not have that file and does not run
it.
The basketball domain is this project's own and has no baseline: its content
types are created by our migrations in migrations/club/, which is also where
every other migration of ours goes — never a domain directory, whose numbers
belong to the template and arrive through /sync-upstream. A migration moved
after it was applied is declared in migrations/renamed.json, so the ledger
does not run it twice.
Contentful apps (sync-apps)
The Contentful apps themselves are built and released from the template; this
repo installs them. packages/model/scripts/sync-apps.mjs reads the app registry
(packages/model/apps/registry.json) and the organisations' app definitions,
and writes packages/model/apps/apps.lock.json — the slug → definition id map,
per organisation and release channel, that migrations resolve when they bind an
app by slug. The lock is generated and drift-gated; migrate.mjs runs the sync
after the migrations. Dry by default; --apply mutates, --check is the PR
gate. Rules: .claude/rules/model/contentful-apps.md.
Competition Engine types
@v4/competition-engine vendors the engine's contract from
stadionHQ/stadion-euroleague at a pinned commit (spec/openapi.yaml, with
spec/SOURCE naming the file and commit). sync-spec refreshes it from main
through gh (falling back to a local clone, COMPETITION_ENGINE_REPO_DIR);
check-spec reports whether main has moved without writing. It generates
src/__generated__/api.d.ts with openapi-typescript. verify regenerates and
diffs it.
pnpm --filter @v4/competition-engine generate
pnpm --filter @v4/competition-engine verify
The one drift gate over all of it
pnpm --filter @v4/model drift
re-syncs the schema, regenerates the model types, regenerates the web,
content-app and video-app page types, and git diff --exit-codes
schema.graphql, the model __generated__ (excluding dictionary/), and those
three apps' __generated__. @v4/basketball-app is gated by its own
pnpm --filter @v4/basketball-app drift. A red gate means run the generator and
commit — never hand-edit the output.
Components and shims
Two generators sit either end of the product: the component scaffold creates
a new design-system component's skeleton and its registrations, and the
composition generator wires this site's bought domains into the app as route
shims. Neither writes product logic — the scaffold hands off to the Figma
pipeline, and the shims delegate to per-route factories in the *-app packages.
Component scaffold (new-component)
packages/ghost-core/scripts/new-component.mjs is the mechanical registration
scaffolder. It creates the component directory skeleton and inserts the
registrations idempotently — the generative work stays with the Figma pipeline.
Reads: a kebab component name (validated ^[a-z][a-z0-9-]*$).
Writes / mutates:
| Target | What it does |
|---|---|
packages/ghost-core/src/components/<name>/figma-screenshots/ |
created |
packages/ghost-core/src/components/<name>/tokens/ |
created |
packages/ghost-core/src/index.ts |
appends export * from "./components/<name>"; (alphabetical) |
packages/ghost-core/src/init.css.ts |
appends the side-effect theme import (alphabetical, above the bootstrap block) |
packages/ghost-core/src/init.css.ts (OWN_THEMES) |
inserts the camelCase theme name into the owner list (sorted) |
packages/ghost-core/figma.json |
adds the bridge entry <name> → src/components/<name>/<name>.figma.json |
The OWN_THEMES append is load-bearing: owner-keyed theme emission means a new
component's theme must join that list or the package build fails
check-theme-owners. The scaffold does it so a fresh component never trips the
gate. It writes NO .component.tsx / .css.ts / .theme.ts — those, and the
tests, stories and token extraction, are the pipeline's job.
pnpm --filter @v4/ghost-core new-component <kebab-name>
new-component exists only in @v4/ghost-core. The sibling packages —
@v4/ghost-content, @v4/ghost-video and @v4/ghost-basketball, the most
active in this repo — are scaffolded by /generate-from-figma --package=<name>, and their build runs check-dist, which fails when the
package's OWN_THEMES list and its theme modules disagree. Every ghost
package's build also precompiles its component stylesheets
(packages/ghost-core/scripts/precompile-styles.mjs); ghost-core additionally
runs check-theme-owners.mjs.
⚡ Do it with Claude —
/generate-from-figmaruns the full Ghost 2.0 Figma → component pipeline: token extraction, the typed vanilla-extract component, tests, stories and the quality/visual gates. The scaffold above is the mechanical first step it builds on; the design-to-code agents under.claude/agents/drive the rest.
Composition — route shims (compose)
packages/template-config/scripts/compose-club.ts (run via pnpm compose,
which is pnpm --filter @v4/template-config compose-club --) materialises the
site's composition. It runs under tsx because it imports the app's raw-TS
club.config.ts.
Reads: the app's club.config.ts (bought bolt-on domains — here
content, video and basketball), each composed domain's routes.json
(packages/content-app, packages/video-app, packages/basketball-app), and the current
packages/model/composition.json. Domain names are validated through the
model's own resolveComposition resolver, so unknown / quarantined / implied
names fail loudly here.
Writes:
packages/model/composition.json— the bought bolt-on domains in canonical order (the UNION across every resident app, keyed per app).- Route shims — for every route a composed domain's
*-apppackage registers, one generatedapps/web/src/app/[locale]/<segment>/page.tsxwiring the route factory to the site's client/config/media/locale dependencies — 34 page shims (content 16, video 6, basketball 12), plus the generatedloading.tsxfiles of the basketball data areas and the newslayout.tsx. Each carries the// GENERATED by compose-club — do not editheader. A header-marked shim whose route is no longer composed is pruned; hand-written pages (no header) are never touched.
pnpm compose # write the composition + shims
pnpm compose -- --check # regenerate in memory and diff against disk (CI drift gate)
The generator is deterministic — same config in, same bytes out — which is what
lets --check be a gate. It statically verifies each route's factory exists in
the package's exports map before emitting a shim, so a broken registry fails the
compose rather than the app build.
⚡ Do it with Claude —
/new-routeadds a route to a route package and wires it throughroutes.json, codegen and the shims;/add-featureadds a bought domain to the site.
Where the two meet
The generators this project runs around the composition are pnpm compose,
pnpm add-feature (buy another domain, then compose) and pnpm sync-upstream
(offer the template's changes as a pull request — when one changes a route
package's routes.json, run pnpm compose on its branch, or the compose drift
gate fails). pnpm new-club, which
cut this repo from the template and ran the composed baseline and the first
compose, is not run here again. See
Take template updates.