OpenCut/notes/primitives-vs-domains.md

2.6 KiB

Primitives vs domains

The codebase has a recurring smell: primitive value types defined inside domain folders. The clearest current example is Transform, which lives in apps/web/src/rendering/index.ts. Rendering happens to consume it — but so do @/timeline, @/preview, @/animation, @/text, and anything else that positions things on a 2D canvas. It's not "of" rendering; rendering just owns the file.

The test

If a type can be described without mentioning clips, tracks, effects, layers, keyframes, or any other product concept — and it has no behavior beyond shape — it's a primitive. The moment a type needs to know what a clip is, it has crossed into domain territory.

Primitives have:

  • No domain-specific invariants (a 2D position doesn't care that it's a clip's position; it's just { x, y }).
  • No dependencies on other parts of the app — they're leaves.
  • Multiple unrelated consumers across domains.
  • A name that would make sense in any 2D editor / video tool / graphics lib.

Domains, in contrast, can name things that only make sense given the rest of the product (TimelineElement, Effect, GraphicDefinition, MediaAsset).

Why it matters

When a primitive lives in a domain folder, every other domain that consumes it takes a misleading dependency — @/timeline ends up importing from @/rendering not because timeline needs rendering, but because that's where Transform happens to sit. The dependency graph lies, and pieces that should move freely become anchored to the wrong layer.

The refactor

Move primitives out of domain folders into a primitives location (somewhere like apps/web/src/primitives/, or split by concern — geometry/, time/, color/, etc.). Whatever the bucket, the rule is "no product concepts, no behavior, no upward dependencies".

Don't bulk-move. Each move is deliberate — the right destination depends on what other primitives already exist and what naming convention has emerged.

Side effects to watch for

Files often end up parked next to misplaced primitives because they had nowhere better to live. Example: apps/web/src/rendering/animation-values.ts exists only because Transform lives next door. Once Transform moves to a primitive location, that file collapses back into apps/web/src/animation/values.ts alongside the other resolve-at-time helpers — there's no remaining reason to split them.

When moving a primitive, look at what else in its current folder only exists because of that primitive. Those usually want to move too (or merge somewhere else once the anchor is gone).