The program tree

Your program is not a flat list: it is a tree. This page describes the data structure, its invariants, and the one module that may touch it.

The shape: SequenceItem

schemas/sequence.schema.ts defines the UI shape of one node:

type SequenceItem = {
    kind: 'algorithm';
    id: string; // unique within the program
    action: ActionName;
    label: string; // translated display name
    from?: number; // drag internals
    sourceId?: string; // drag internals
    default: boolean; // true for the level's starter blocks
    count?: number; // for only
    condition?: ConditionName; // if only
    movable: boolean; // false = locked in place
    modifiable?: boolean; // false = header fields locked
    children?: readonly SequenceItem[]; // for/while/if-then bodies
    elseChildren?: readonly SequenceItem[]; // if-else body
    branches?: readonly {
        condition: SwitchCondition;
        children: SequenceItem[];
        modifiable?: boolean;
    }[]; // switch only
};

The whole program is SequenceItem[], always starting with the synthetic start item (action: 'start', default: true, movable: false). The start head is what makes "the first block runs first" trivial: the runner walks the list in order.

The schema (SequenceItemSchema) re-validates the sequence at every mutation boundary, so a bug in a drag handler cannot silently corrupt the program shape.

The four zones

Every container contributes drop zones, addressed by string ids:

Zone idMeaning
rootthe top-level program
<itemId>that item's children (for/while/if-then bodies, switch branch bodies)
<itemId>:elsethat item's elseChildren
<itemId>:branch-<n>that switch's branch n

The whole tree is addressed with these ids; components never hand-roll recursion.

The one module that mutates: domain/sequence-tree.ts

All traversal and mutation lives here, immutably (every update returns a new tree, reusing untouched branches):

Defaults and locks

The level's defaultActions become real SequenceItems at the head of the program, each flagged default: true and carrying the level's movable and modifiable settings. Nested default children (level 11's switch inside the locked while) are flagged too. The flags matter twice:

  1. In the UI: default blocks render in the gray variant, cannot be deleted (canDelete in the drag session checks default !== true), and locked ones cannot be moved.
  2. In storage: the flag is the bridge that re-attaches the level's internal fields (movable, modifiable, ids, labels) after a reload. See Persistence.

Rebuilding after storage

features/level/stored-sequence.ts converts between the UI tree and the stored shape:

useSequence (in features/level/hooks/) owns the in-memory tree: it restores from storage on mount, saves on every change, and exposes reset to restore the level's defaults.

Next: Simulation.