# Entity System Structural Problems

This document catalogs the structural problems in the current litegraph entity system. It provides the concrete "why" behind the ECS migration proposed in [ADR 0008](../adr/0008-entity-component-system.md). For the as-is relationship map, see [Entity Interactions](entity-interactions.md).

All file references are relative to `src/lib/litegraph/src/`.

## 1. God Objects

The three largest classes carry far too many responsibilities:

| Class          | Lines  | Responsibilities                                                                                                             |
| -------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `LGraphCanvas` | ~9,100 | Rendering, input handling, selection, link dragging, context menus, clipboard, undo/redo hooks, node layout triggers         |
| `LGraphNode`   | ~4,300 | Domain model, connectivity, serialization, rendering (slots, widgets, badges, title), layout, execution, property management |
| `LGraph`       | ~3,100 | Container management, serialization, canvas notification, subgraph lifecycle, execution ordering, link deduplication         |

`LGraphNode` alone has ~539 method/property definitions. A sampling of the concerns it mixes:

| Concern       | Examples                                                                                                               |
| ------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Rendering     | `renderingColor` (line 328), `renderingBgColor` (line 335), `drawSlots()`, `drawWidgets()`, `measure(ctx)` (line 2074) |
| Serialization | `serialize()` (line 943), `configure()` (line 831), `toJSON()` (line 1033)                                             |
| Connectivity  | `connect()`, `connectSlots()`, `disconnectInput()`, `disconnectOutput()`                                               |
| Execution     | `execute()` (line 1418), `triggerSlot()`                                                                               |
| Layout        | `arrange()`, `_arrangeWidgets()`, `computeSize()`                                                                      |
| State mgmt    | `setProperty()`, `onWidgetChanged()`, direct `graph._version++`                                                        |

## 2. Circular Dependencies

**LGraph ↔ Subgraph**: `Subgraph` extends `LGraph`, but `LGraph` creates and manages `Subgraph` instances. This forces:

- A barrel export in `litegraph.ts` that re-exports 40+ modules with **order-dependent imports**
- An explicit comment at `litegraph.ts:15`: _"Must remain above LiteGraphGlobal (circular dependency due to abstract factory behaviour in 'configure')"_
- Test files must use the barrel import (`import { LGraph, Subgraph } from '.../litegraph'`) rather than direct imports, or they break

The `Subgraph` class is defined inside `LGraph.ts` (line 2761) rather than in its own file — a symptom of the circular dependency being unresolvable with the current class hierarchy.

## 3. Mixed Concerns

### Rendering in Domain Objects

`LGraphNode.measure()` (line 2074) accepts a `CanvasRenderingContext2D` parameter and sets `ctx.font` — a rendering operation embedded in what should be a domain model:

```
measure(ctx?: CanvasRenderingContext2D, options?: MeasureOptions): void {
  ...
  if (ctx) ctx.font = this.innerFontStyle
```

### State Mutation During Render

`LGraphCanvas.drawNode()` (line 5554) mutates node state as a side effect of rendering:

- Line 5562: `node._setConcreteSlots()` — rebuilds slot arrays
- Line 5564: `node.arrange()` — recalculates widget layout
- Lines 5653-5655: same mutations repeated for a second code path

This means the render pass is not idempotent — drawing a node changes its state.

### Store Dependencies in Domain Objects

`BaseWidget` imports a Pinia store at the module level:

- `useWidgetValueStore` — widget state delegation via `setNodeId()`

Similarly, `LGraph` imports `useLayoutMutations` and `useWidgetValueStore`. Domain objects should not have direct dependencies on UI framework stores.

### Serialization Interleaved with Container Logic

`LGraph.configure()` (line 2400) mixes deserialization, event dispatch, store clearing, and container state setup in a single 180-line method. A change to serialization format risks breaking container lifecycle, and vice versa.

## 4. Inconsistent ID Systems

### Ambiguous NodeId

```ts
export type NodeId = number | string // LGraphNode.ts:100
```

Most nodes use numeric IDs, but subgraph-related nodes use strings. Code must use runtime type guards (`typeof node.id === 'number'` at LGraph.ts:978, LGraphCanvas.ts:9045). This is a source of subtle bugs.

### Magic Numbers

```ts
export const SUBGRAPH_INPUT_ID = -10 // constants.ts:8
export const SUBGRAPH_OUTPUT_ID = -20 // constants.ts:11
```

Negative sentinel values in the ID space. Links check `origin_id === SUBGRAPH_INPUT_ID` to determine if they cross a subgraph boundary — a special case baked into the general-purpose `LLink` class.

### No Independent Widget or Slot IDs

**Widgets** are identified by `name + parent node`. Code searches by name in multiple places:

- `LGraphNode.ts:904` — `this.inputs.find((i) => i.widget?.name === w.name)`
- `LGraphNode.ts:4077` — `slot.widget.name === widget.name`
- `LGraphNode.ts:4086` — `this.widgets?.find((w) => w.name === slot.widget.name)`

If a widget is renamed, all these lookups silently break.

**Slots** are identified by their array index on the parent node. The serialized link format (`SerialisedLLinkArray`) stores slot indices:

```ts
type SerialisedLLinkArray = [
  id,
  origin_id,
  origin_slot,
  target_id,
  target_slot,
  type
]
```

If slots are reordered (e.g., by an extension adding a slot), all links referencing that node become stale.

### No Cross-Kind ID Safety

Nothing prevents passing a `LinkId` where a `NodeId` is expected — they're both `number`. The dedicated-store direction addresses this with branded string keys where cross-kind safety pays off (for example `WidgetId` in `widgetValueStore`, `src/types/widgetId.ts`).

## 5. Law of Demeter Violations

Entities routinely reach through their container to access internal state and sibling entities.

### Nodes Reaching Into Graph Internals

8+ locations in `LGraphNode` access the graph's private `_links` map directly:

- Line 877: `this.graph._links.get(input.link)`
- Line 891: `this.graph._links.get(linkId)`
- Line 1254: `const link_info = this.graph._links.get(input.link)`

Nodes also reach through the graph to access sibling nodes' slots:

- Line 1150: `this.graph.getNodeById(link.origin_id)` → read origin's outputs
- Line 1342: `this.graph.getNodeById(link.target_id)` → read target's inputs
- Line 1556: `node.inputs[link_info.target_slot]` (accessing a sibling's slot by index)

### Canvas Mutating Graph Internals

`LGraphCanvas` directly increments the graph's version counter:

- Line 3084: `node.graph._version++`
- Line 7880: `node.graph._version++`

The canvas also reaches through nodes to their container:

- Line 8337: `node.graph.remove(node)` — canvas deletes a node by reaching through the node to its graph

### Entities Mutating Container State

`LGraphNode` directly mutates `graph._version++` from 8+ locations (lines 833, 2989, 3138, 3176, 3304, 3539, 3550, 3567). There is no encapsulated method for signaling a version change — every call site manually increments the counter.

## 6. Scattered Side Effects

### Version Counter

`graph._version` is incremented from **15+ locations** across three files:

| File              | Locations                                           |
| ----------------- | --------------------------------------------------- |
| `LGraph.ts`       | Lines 956, 989, 1042, 1109, 2643                    |
| `LGraphNode.ts`   | Lines 833, 2989, 3138, 3176, 3304, 3539, 3550, 3567 |
| `LGraphCanvas.ts` | Lines 3084, 7880                                    |

No central mechanism exists. It's easy to forget an increment (stale render) or add a redundant one (wasted work).

### Module-Scope Store Access

Domain objects call Pinia composables at the module level or in methods, creating implicit dependencies on the Vue runtime:

- `LLink.ts:24` — `const layoutMutations = useLayoutMutations()` (module scope)
- `Reroute.ts` — same pattern at module scope
- `BaseWidget.ts` — imports `useWidgetValueStore`

These make the domain objects untestable without a Vue app context.

### Change Notification Sprawl

`beforeChange()` and `afterChange()` (undo/redo checkpoints) are called from
**12+ locations** in `LGraphCanvas` alone (lines 1574, 1592, 1604, 1620, 1752,
1770, 8754, 8760, 8771, 8777, 8803, 8811). These calls are grouping brackets:
misplaced or missing pairs can split one logical operation across multiple undo
entries, while unmatched extra calls can delay checkpoint emission until the
nesting counter returns to zero.

## 7. Render-Time Mutations

The render pass is not pure — it mutates state as a side effect:

| Location                            | Mutation                                                            |
| ----------------------------------- | ------------------------------------------------------------------- |
| `LGraphCanvas.drawNode()` line 5562 | `node._setConcreteSlots()` — rebuilds concrete slot arrays          |
| `LGraphCanvas.drawNode()` line 5564 | `node.arrange()` — recalculates widget positions and sizes          |
| Link rendering                      | Caches `_pos` center point and `_centreAngle` on the LLink instance |

This means:

- Rendering order matters (later nodes see side effects from earlier nodes)
- Performance profiling conflates render cost with layout cost
- Concurrent or partial renders would produce inconsistent state

## How ECS Addresses These Problems

| Problem                | ECS Solution                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------- |
| God objects            | Data split into small, focused components in dedicated stores; behavior lives in systems    |
| Circular dependencies  | Entities are addressed by string keys; components have no inheritance hierarchy             |
| Mixed concerns         | Each system handles exactly one concern (render, serialize, execute)                        |
| Inconsistent IDs       | Branded string keys per store (for example `WidgetId`) for cross-kind safety                |
| Demeter violations     | Systems query the relevant store directly; no entity-to-entity references                   |
| Scattered side effects | Version tracking becomes a system responsibility; mutations flow through store command APIs |
| Render-time mutations  | Render system reads components without writing; layout system runs separately               |
