Sign inStart creating

The Media Graph

"Every project is a graph, not a gallery. Every generation is a node that knows its dependencies."

Implementation: src/server/services/graph.ts, table GraphEdge.

Why an explicit edge table

The alternative — deriving dependencies from foreign keys at query time — fails for the operation that defines the product. When a creator changes a wardrobe colour, we need to answer "what does this invalidate?" across heterogeneous node types (wardrobe → shot, style → shot, state event → shot, generation → asset) with different relation semantics and different regeneration costs. Foreign keys encode containment; they do not encode invalidation.

GraphEdge is a typed, directed, weighted edge:

fromType  String   // 20 node types — see GRAPH_NODE_TYPES
fromId    String
toType    String
toId      String
relation  String   // depends_on | derived_from | references
                   // supersedes | contains | styled_by | cast_as
weight    Float    // higher = a change here is more likely to invalidate
stale     Boolean  // upstream changed, downstream not yet regenerated

Uniqueness is (projectId, fromType, fromId, toType, toId, relation), so the same pair can carry several relations without collision.

Traversal

resolveImpact(projectId, origin) is a breadth-first walk downstream, one query per level rather than per node — a 200-shot feature resolves in a handful of queries rather than hundreds. Depth is capped at 8 and visited nodes are tracked, so a malformed edge cannot hang the planner.

Each impacted node carries:

  • label — human-readable ("MARA — Relay coveralls")
  • path — the full dependency chain, shown verbatim in the confirmation UI
  • reason — derived from the relation type
  • estimatedCredits — what redoing this actually costs
  • depth

Showing the path is not decoration. It is how a creator confirms the system understood them before spending money.

Cost model

Only nodes that cost something to redo are billed:

NodeCost
shotcreditsPerSec(qualityMode) × durationSec
asset4
dialogue1
music_cue6
Bible objects0 — metadata is free; its dependents are not

The confirmation gate

Nothing regenerates without approval. planCascade writes a CascadePlan row (status pending) holding the impacted set, the price, and the mutation to apply. The UI renders it; approveCascade executes it.

On approval:

  1. Downstream edges from the origin are marked stale.
  2. Affected shots drop to status approved so the queue picks them up.
  3. The existing rendered asset stays attached.

Point 3 is load-bearing and was a real bug during development. Detaching the old render on approval means the timeline clip loses its picture the instant you approve, and the creator has no watchable cut until every regeneration finishes. The whole promise of a graph-aware NLE is per-shot re-render without losing the cut — so the previous output survives until the new one swaps in (assembleFromShots' re-point pass).

Equally load-bearing: camera, lens, movement, framing, lighting and storyboard approval are untouched. That is the difference between "regenerate" and "start over".

Keeping edges honest

reindexShotEdges(projectId, shotId) deletes and rebuilds a shot's inbound edges from current truth. Call it whenever a shot's cast, props, location or style changes. Cheap, idempotent, and it makes stale edges impossible rather than merely unlikely.

High-level operations

All in graph.ts, all returning a priced plan rather than mutating:

FunctionSpec reference
changeWardrobeAttributeThe canonical "black jacket → red" demo
changeActorGlobal recasting; creates a new canon version, earlier scenes stay pinned
addStateEventCharacter State Machine; wires edges only to shots at/after the event scene
applyStyleProject / scene / shot scope; project scope sets Style DNA on the Universe

Adding a node type

  1. Add it to GRAPH_NODE_TYPES in src/lib/enums.ts.
  2. Add a labelFor case so the confirmation UI reads well.
  3. Add an estimateRegenCost case if regenerating it costs credits.
  4. Wire its edges wherever it is created.

No schema migration — GraphEdge is type-agnostic by design.