Extension points
How element types, effects, transitions, commands, and operators are added to the closed tables.
mcut keeps its type tables closed. Element types, effects, transitions,
commands, operators, renderers, and container formats each live in one literal
table or union in source, and every consumer switches on that union
exhaustively. There is no runtime register* call. Adding an arm means editing
the table, and the compiler then lists every place that has to learn about it.
Why the tables are closed
A registry accepts anything at runtime, so a missing renderer or an unhandled
switch arm only shows up once a project with the new type is loaded. A closed
union fails at typecheck instead. After adding an arm, bun run typecheck
reports every exhaustive switch that ends in assertNever and every mapped
type keyed by the union, and the build stays red until each one is handled.
The one open interface is TranscriptionProvider from @mcut/transcription. A
provider is an object with an id and a transcribe method, and the provider
packages implement it without touching any table.
Element types
An element type is one Zod object in elementSchema and one arm in the
TimelineElement union, both in packages/timeline/src/model.ts.
- Build the schema with
composeFull('name', shape)and export its inferred type. - Add the schema to the
z.discriminatedUnion('type', [...])inelementSchemaand the matchingcomposeInputcall inelementInputSchema. - Add the type to the
TimelineElement,TimelineElementInput, andTimelineElementDraftunions.
ElementType is derived as TimelineElement['type'], so the new literal flows
to every consumer. Typecheck then fails in validateElement and
splitElementAt in model.ts, animatableProperties in keyframes.ts,
getFrameRequests in frame-requests.ts, and summarizeProject in
summarize.ts, because each switch on element.type ends in
assertNever(element) and the new arm is not never. In @mcut/compositor,
elementRenderers is typed { [K in ElementType]: ElementRenderer }, so it
fails until the table has a renderer for the new key.
Effects
An effect is one effectShape('name', shape) entry in the
z.discriminatedUnion that defines effectSchema in
packages/timeline/src/effects.ts. Effect, EffectType, and EFFECT_TYPES
are all derived from that schema, so the schema entry is the only source edit.
Typecheck then fails in effectToFilter in effects.ts and in planEffects
in packages/compositor/src/webgpu/effect-plan.ts, where the switch on
effect.type ends in assertNever(effect). Add the effect to EFFECT_PARAMS
when the inspector should expose a slider for it. That table is optional per
key, so leaving it out compiles.
Transitions
A transition type is one string in the z.enum that defines
transitionTypeSchema in packages/timeline/src/transitions.ts.
TransitionType and TRANSITION_TYPES derive from it.
Typecheck then fails in transitionRenderers in
packages/compositor/src/transition-renderers.ts, which is typed
{ [K in TransitionType]: TransitionRenderer } and needs a renderer for the
new key.
Commands
A command is a defineCommand({ type, description, payloadSchema, reduce })
export in one of the domain files under packages/timeline/src/commands/, plus
one entry in the commandDefinitions table in
packages/timeline/src/commands.ts. That table is the canonical list.
CommandType, BuiltinCommand, CommandOfType, listCommands,
listToolDefinitions, parseCommand, and applyCommand all read from it, so
the MCP tool list and engine.dispatch learn about the command from the same
edit.
The commandTable helper requires each entry's type literal to equal its
key, so a mismatch fails at typecheck. A command that is defined but not
listed compiles and is simply unreachable, which is why the MCP surface tests
pin the tool count.
Operators
An operator is one entry in the operators table in
packages/editor/src/core-operators.ts, built with
defineOperator({ label, description, category, inputSchema, run }). OperatorId is
keyof typeof operators, and operatorIds, parseOperatorId,
listOperators, and runOperator read from the table.
The table is declared with satisfies Record<string, OperatorDefinition>, so
a wrong category or a run whose input does not match inputSchema fails at
typecheck. The MCP server and the Studio live bridge resolve incoming ids
through parseOperatorId, so an id that is not a key of the table is rejected
before it runs.
Container formats
A container format is one entry in the containerFormats table in
packages/media/src/container-formats.ts, declared with
satisfies Record<string, ContainerFormat>. ContainerFormatId is
keyof typeof containerFormats, so listContainerFormats and the export
pipeline pick up the new id without further edits.
What to avoid
Do not patch project JSON with undocumented fields and expect the engine to
keep them. elementSchema is a discriminated union, so an unknown type fails
parseProject, and the object schemas strip keys they do not declare, so an
extra field does not survive a parse. Add the arm to the table instead, or keep
app-specific data outside the project document.