mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
docs: add subgraph boundaries doc, unify graph model across ECS architecture
- New companion doc: subgraph-boundaries-and-promotion.md - Graph model unification: all graphs are isomorphic, 7→6 entity kinds - Typed boundary contracts replace virtual nodes and magic IDs - Widget promotion as open decision (connections-only vs simplified) - Serialization boundary with indefinite backward-compatible loading - Update ADR 0008: remove SubgraphEntityId, add GraphId scope, flat World - Update ecs-target-architecture: World diagram, Entity IDs, problem map - Update ecs-migration-plan: Phase 1a/1c types and World interface - Update ecs-lifecycle-scenarios: pack/unpack use graphScope re-parenting - Update proto-ecs-stores and entity-interactions: annotate subgraph rows Amp-Thread-ID: https://ampcode.com/threads/T-019d2311-3707-746a-ae9c-65e6f0d67f3e Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -34,14 +34,14 @@ Adopt an Entity Component System architecture for the graph domain model. This A
|
||||
|
||||
Six entity kinds, each with a branded ID type:
|
||||
|
||||
| Entity Kind | Current Class(es) | Current ID | Branded ID |
|
||||
| ----------- | ------------------------------------------------- | --------------------------- | ----------------- |
|
||||
| Node | `LGraphNode` | `NodeId = number \| string` | `NodeEntityId` |
|
||||
| Link | `LLink` | `LinkId = number` | `LinkEntityId` |
|
||||
| Widget | `BaseWidget` subclasses (25+) | name + parent node | `WidgetEntityId` |
|
||||
| Slot | `SlotBase` / `INodeInputSlot` / `INodeOutputSlot` | index on parent node | `SlotEntityId` |
|
||||
| Reroute | `Reroute` | `RerouteId = number` | `RerouteEntityId` |
|
||||
| Group | `LGraphGroup` | `number` | `GroupEntityId` |
|
||||
| Entity Kind | Current Class(es) | Current ID | Branded ID |
|
||||
| ----------- | ------------------------------------------------- | --------------------------- | ------------------ |
|
||||
| Node | `LGraphNode` | `NodeId = number \| string` | `NodeEntityId` |
|
||||
| Link | `LLink` | `LinkId = number` | `LinkEntityId` |
|
||||
| Widget | `BaseWidget` subclasses (25+) | name + parent node | `WidgetEntityId` |
|
||||
| Slot | `SlotBase` / `INodeInputSlot` / `INodeOutputSlot` | index on parent node | `SlotEntityId` |
|
||||
| Reroute | `Reroute` | `RerouteId = number` | `RerouteEntityId` |
|
||||
| Group | `LGraphGroup` | `number` | `GroupEntityId` |
|
||||
|
||||
Subgraphs are not a separate entity kind. A subgraph is a node with a `SubgraphStructure` component. See [Subgraph Boundaries and Widget Promotion](../architecture/subgraph-boundaries-and-promotion.md) for the full design rationale.
|
||||
|
||||
@@ -69,16 +69,14 @@ Components are plain data objects — no methods, no back-references to parent e
|
||||
|
||||
#### Shared Components
|
||||
|
||||
- **Position** — `{ pos: Point }` — used by Node, Reroute, Group
|
||||
- **Dimensions** — `{ size: Size, bounding: Rectangle }` — used by Node, Group
|
||||
- **Position** — `{ pos: Point, size: Size, bounding: Rectangle }` — used by Node, Reroute, Group
|
||||
- **Visual** — rendering properties specific to each entity kind (separate interfaces, shared naming convention)
|
||||
|
||||
#### Node
|
||||
|
||||
| Component | Data (from `LGraphNode`) |
|
||||
| ----------------- | --------------------------------------------------- |
|
||||
| `Position` | `pos` |
|
||||
| `Dimensions` | `size`, `_bounding` |
|
||||
| `Position` | `pos`, `size`, `_bounding` |
|
||||
| `NodeVisual` | `color`, `bgcolor`, `boxcolor`, `title` |
|
||||
| `NodeType` | `type`, `category`, `nodeData`, `description` |
|
||||
| `Connectivity` | slot entity refs (replaces `inputs[]`, `outputs[]`) |
|
||||
@@ -98,10 +96,10 @@ Components are plain data objects — no methods, no back-references to parent e
|
||||
|
||||
A node carrying a subgraph gains these additional components. Subgraphs are not a separate entity kind — see [Subgraph Boundaries](../architecture/subgraph-boundaries-and-promotion.md).
|
||||
|
||||
| Component | Data |
|
||||
| ------------------- | ------------------------------------------------------------------------ |
|
||||
| `SubgraphStructure` | `graphId`, typed interface (input/output names, types, slot entity refs) |
|
||||
| `SubgraphMeta` | `name`, `description` |
|
||||
| Component | Data |
|
||||
| ------------------- | ------------------------------------------------------------------------- |
|
||||
| `SubgraphStructure` | `graphId`, typed interface (input/output names, types, slot entity refs) |
|
||||
| `SubgraphMeta` | `name`, `description` |
|
||||
|
||||
#### Widget
|
||||
|
||||
@@ -123,7 +121,7 @@ A node carrying a subgraph gains these additional components. Subgraphs are not
|
||||
|
||||
| Component | Data (from `Reroute`) |
|
||||
| --------------- | --------------------------------- |
|
||||
| `Position` | `pos` (shared) |
|
||||
| `Position` | (shared) |
|
||||
| `RerouteLinks` | `parentId`, input/output link IDs |
|
||||
| `RerouteVisual` | `color`, badge config |
|
||||
|
||||
@@ -131,46 +129,24 @@ A node carrying a subgraph gains these additional components. Subgraphs are not
|
||||
|
||||
| Component | Data (from `LGraphGroup`) |
|
||||
| --------------- | ----------------------------------- |
|
||||
| `Position` | `pos` (shared) |
|
||||
| `Dimensions` | `size`, `bounding` |
|
||||
| `Position` | (shared) |
|
||||
| `GroupMeta` | `title`, `font`, `font_size` |
|
||||
| `GroupVisual` | `color` |
|
||||
| `GroupChildren` | child entity refs (nodes, reroutes) |
|
||||
|
||||
### World
|
||||
|
||||
A central registry (the "World") maps entity IDs to their component sets. One
|
||||
World exists per workflow instance, containing all entities across all nesting
|
||||
levels. Each entity carries a `graphScope` identifier linking it to its
|
||||
containing graph. The World also maintains a scope registry mapping each
|
||||
`graphId` to its parent (or null for the root graph).
|
||||
|
||||
The "single source of truth" claim in this ADR is scoped to one workflow
|
||||
instance. In a future linked-subgraph model, shared definitions can be loaded
|
||||
into multiple workflow instances, but mutable runtime components
|
||||
(`WidgetValue`, execution state, selection, transient layout caches) remain
|
||||
instance-scoped unless explicitly declared shareable.
|
||||
|
||||
### Subgraph recursion model
|
||||
|
||||
The ECS model preserves recursive nesting without inheritance. A subgraph node
|
||||
stores `SubgraphStructure.childGraphId`, and the scope registry stores
|
||||
`childGraphId -> parentGraphId`. This forms a DAG that can represent arbitrary
|
||||
subgraph depth.
|
||||
|
||||
Queries such as "all nodes at depth N" run by traversing the scope registry
|
||||
from the root, materializing graph IDs at depth `N`, and then filtering entity
|
||||
queries by `graphScope`.
|
||||
A central registry (the "World") maps entity IDs to their component sets. One World exists per workflow, containing all entities across all nesting levels. Each entity carries a `graphScope` identifier linking it to its containing graph. The World also maintains a scope registry mapping each `graphId` to its parent (or null for the root graph).
|
||||
|
||||
### Systems (future work)
|
||||
|
||||
Systems are pure functions that query the World for entities with specific component combinations. Initial candidates:
|
||||
|
||||
- **RenderSystem** — queries `Position` + `Dimensions` (where present) + `*Visual` components
|
||||
- **RenderSystem** — queries `Position` + `*Visual` components
|
||||
- **SerializationSystem** — queries all components to produce/consume workflow JSON
|
||||
- **ExecutionSystem** — queries `Execution` + `Connectivity` to determine run order
|
||||
- **LayoutSystem** — queries `Position` + `Dimensions` + structural components for auto-layout
|
||||
- **SelectionSystem** — queries `Position` for point entities and `Position` + `Dimensions` for box hit-testing
|
||||
- **LayoutSystem** — queries `Position` + structural components for auto-layout
|
||||
- **SelectionSystem** — queries `Position` for hit-testing
|
||||
|
||||
System design is deferred to a future ADR.
|
||||
|
||||
@@ -182,18 +158,6 @@ System design is deferred to a future ADR.
|
||||
4. **Incremental extraction** — migrate one component at a time from classes to the World, using the bridge layer for backward compatibility
|
||||
5. **Deprecate class properties** — once all consumers read from the World, mark class properties as deprecated
|
||||
|
||||
### Relationship to ADR 0003 (Command Pattern / CRDT)
|
||||
|
||||
[ADR 0003](0003-crdt-based-layout-system.md) establishes that all mutations flow through serializable, idempotent commands. This ADR (0008) defines the entity data model and the World store. They are complementary architectural layers:
|
||||
|
||||
- **Commands** (ADR 0003) describe mutation intent — serializable objects that can be logged, replayed, sent over a wire, or undone.
|
||||
- **Systems** (ADR 0008) are command handlers — they validate and execute mutations against the World.
|
||||
- **The World** (ADR 0008) is the store — it holds component data. It does not know about commands.
|
||||
|
||||
The World's imperative API (`setComponent`, `deleteEntity`, etc.) is internal. External callers submit commands; the command executor wraps each in a World transaction. This is analogous to Redux: the store's internal mutation is imperative, but the public API is action-based.
|
||||
|
||||
For the full design showing how each lifecycle scenario maps to a command, see [World API and Command Layer](../architecture/ecs-world-command-api.md).
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
- **Refactoring classes in place**: Lower initial cost, but doesn't solve the cross-cutting concern problem. Each new feature still requires modifying multiple god objects.
|
||||
@@ -207,7 +171,7 @@ For the full design showing how each lifecycle scenario maps to a command, see [
|
||||
- Cross-cutting concerns (undo/redo, CRDT sync, serialization) can be implemented as systems without modifying entity classes
|
||||
- Components are independently testable — no need to construct an entire `LGraphNode` to test position logic
|
||||
- Branded IDs prevent a class of bugs where IDs are accidentally used across entity kinds
|
||||
- The World provides a single source of truth for runtime entity state inside a workflow instance, simplifying debugging and state inspection
|
||||
- The World provides a single source of truth for all entity state, simplifying debugging and state inspection
|
||||
- Aligns with the CRDT layout system direction from ADR 0003
|
||||
|
||||
### Negative
|
||||
@@ -217,20 +181,6 @@ For the full design showing how each lifecycle scenario maps to a command, see [
|
||||
- Migration period where both OOP and ECS patterns coexist, increasing cognitive load
|
||||
- Widgets and Slots need synthetic IDs, adding ID management complexity
|
||||
|
||||
### Render-Loop Performance Implications and Mitigations
|
||||
|
||||
Replacing direct property reads (`node.pos`) with component lookups (`world.getComponent(nodeId, Position)`) does add per-read overhead in the hot render path. In modern JS engines, hot `Map.get()` paths are heavily optimized and are often within a low constant factor of object property reads, but this ADR treats render-loop cost as a first-class risk rather than assuming it is free.
|
||||
|
||||
Planned mitigations for the ECS render path:
|
||||
|
||||
1. Pre-collect render queries into frame-stable caches (`visibleNodeIds`, `visibleLinkIds`, and resolved component references) and rebuild only on topology/layout dirty signals, not on every draw call.
|
||||
2. Keep archetype-style buckets for common render signatures (for example: `Node = Position+Dimensions+NodeVisual`, `Reroute = Position+RerouteVisual`) so systems iterate arrays instead of probing unrelated entities.
|
||||
3. Allow a hot-path storage upgrade behind the World API (for example, SoA-style typed arrays for `Position` and `Dimensions`) if profiling shows `Map.get()` dominates frame time.
|
||||
4. Gate migration of each render concern with profiling parity checks against the legacy path (same workflow, same viewport, same frame budget).
|
||||
5. Treat parity as a release gate: ECS render path must stay within agreed frame-time budgets (for example, no statistically significant regression in p95 frame time on representative 200-node and 500-node workflows).
|
||||
|
||||
The design goal is to preserve ECS modularity while keeping render throughput within existing frame-time budgets.
|
||||
|
||||
## Notes
|
||||
|
||||
- The 25+ widget types (`BooleanWidget`, `NumberWidget`, `ComboWidget`, etc.) will share the same ECS component schema. Widget-type-specific behavior lives in systems, not in component data.
|
||||
|
||||
@@ -401,18 +401,20 @@ sequenceDiagram
|
||||
CS->>W: query Connectivity + SlotConnection for selected nodes
|
||||
CS->>CS: classify links as internal vs boundary
|
||||
|
||||
CS->>W: createEntity(SubgraphEntityId)
|
||||
CS->>W: setComponent(sgId, SubgraphMeta, { name: 'New Subgraph' })
|
||||
|
||||
Note over CS,W: Move selected entities into subgraph scope
|
||||
|
||||
CS->>W: setComponent(sgId, SubgraphStructure, {<br/> nodeIds: [...selected],<br/> linkIds: [...internal],<br/> rerouteIds: [...internal]<br/>})
|
||||
CS->>W: create new GraphId scope in scopes registry
|
||||
|
||||
Note over CS,W: Create SubgraphNode entity in parent scope
|
||||
|
||||
CS->>W: createEntity(NodeEntityId) [the SubgraphNode]
|
||||
CS->>W: setComponent(nodeId, Position, { center of selection })
|
||||
CS->>W: setComponent(nodeId, NodeType, { type: sgId })
|
||||
CS->>W: setComponent(nodeId, SubgraphStructure, { graphId, interface })
|
||||
CS->>W: setComponent(nodeId, SubgraphMeta, { name: 'New Subgraph' })
|
||||
|
||||
Note over CS,W: Re-parent selected entities into new graph scope
|
||||
|
||||
loop each selected entity
|
||||
CS->>W: update graphScope to new graphId
|
||||
end
|
||||
|
||||
Note over CS,W: Create boundary slots on SubgraphNode
|
||||
|
||||
@@ -431,7 +433,7 @@ sequenceDiagram
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| -------------------------- | ------------------------------------------------- | ------------------------------------------------------- |
|
||||
| Entity movement | Clone → serialize → configure → remove originals | Move entity IDs into SubgraphStructure component |
|
||||
| Entity movement | Clone → serialize → configure → remove originals | Re-parent entities: update graphScope to new GraphId |
|
||||
| Boundary links | Disconnect → remove → recreate → reconnect | Update LinkEndpoints to point at new SubgraphNode slots |
|
||||
| Intermediate inconsistency | Graph is partially disconnected during operation | Atomic: all component writes happen together |
|
||||
| Code size | 200+ lines | ~50 lines in system |
|
||||
@@ -498,26 +500,27 @@ sequenceDiagram
|
||||
|
||||
Caller->>CS: unpackSubgraph(world, subgraphNodeId)
|
||||
|
||||
CS->>W: getComponent(subgraphNodeId, NodeType)
|
||||
W-->>CS: { type: subgraphEntityId }
|
||||
CS->>W: getComponent(subgraphNodeId, SubgraphStructure)
|
||||
W-->>CS: { graphId, interface }
|
||||
|
||||
CS->>W: getComponent(subgraphEntityId, SubgraphStructure)
|
||||
W-->>CS: { nodeIds, linkIds, rerouteIds }
|
||||
CS->>W: query entities where graphScope = graphId
|
||||
W-->>CS: all child entities (nodes, links, reroutes, etc.)
|
||||
|
||||
Note over CS,W: Move entities back to parent scope
|
||||
Note over CS,W: Re-parent entities to containing graph scope
|
||||
|
||||
CS->>W: remove nodeIds from SubgraphStructure
|
||||
Note over CS,W: Entities already exist in World — just reparent
|
||||
loop each child entity
|
||||
CS->>W: update graphScope to parent scope
|
||||
end
|
||||
|
||||
Note over CS,W: Reconnect boundary links
|
||||
|
||||
loop each boundary slot on SubgraphNode
|
||||
loop each boundary slot in interface
|
||||
CS->>W: getComponent(slotId, SlotConnection)
|
||||
CS->>W: update LinkEndpoints: SubgraphNode slot → internal node slot
|
||||
end
|
||||
|
||||
CS->>W: deleteEntity(subgraphNodeId)
|
||||
CS->>W: deleteEntity(subgraphEntityId) [if no other refs]
|
||||
CS->>W: remove graphId from scopes registry
|
||||
|
||||
Note over CS,W: Offset positions
|
||||
|
||||
@@ -530,7 +533,7 @@ sequenceDiagram
|
||||
|
||||
| Aspect | Current | ECS |
|
||||
| ----------------- | --------------------------------------------------- | ------------------------------------------------ |
|
||||
| ID remapping | `nodeIdMap[oldId] = newId` for every node and link | No remapping — entities keep their IDs |
|
||||
| ID remapping | `nodeIdMap[oldId] = newId` for every node and link | No remapping — entities keep their IDs, only graphScope changes |
|
||||
| Magic IDs | SUBGRAPH_INPUT_ID = -10, SUBGRAPH_OUTPUT_ID = -20 | No magic IDs — boundary modeled as slot entities |
|
||||
| Clone vs move | Clone nodes, assign new IDs, configure from scratch | Move entity references between scopes |
|
||||
| Link reconnection | Remap origin_id/target_id, create new LLink objects | Update LinkEndpoints component in place |
|
||||
|
||||
@@ -88,7 +88,11 @@ Define branded types in a new `src/ecs/entityId.ts`:
|
||||
```
|
||||
type NodeEntityId = number & { readonly __brand: 'NodeEntityId' }
|
||||
type LinkEntityId = number & { readonly __brand: 'LinkEntityId' }
|
||||
// ... etc per ADR 0008
|
||||
type WidgetEntityId = number & { readonly __brand: 'WidgetEntityId' }
|
||||
type SlotEntityId = number & { readonly __brand: 'SlotEntityId' }
|
||||
type RerouteEntityId = number & { readonly __brand: 'RerouteEntityId' }
|
||||
type GroupEntityId = number & { readonly __brand: 'GroupEntityId' }
|
||||
type GraphId = string & { readonly __brand: 'GraphId' } // scope, not entity
|
||||
```
|
||||
|
||||
Add cast helpers (`asNodeEntityId(id: number): NodeEntityId`) for use at
|
||||
@@ -146,7 +150,7 @@ interface World {
|
||||
slots: Map<SlotEntityId, SlotComponents>
|
||||
reroutes: Map<RerouteEntityId, RerouteComponents>
|
||||
groups: Map<GroupEntityId, GroupComponents>
|
||||
subgraphs: Map<SubgraphEntityId, SubgraphComponents>
|
||||
scopes: Map<GraphId, GraphId | null> // graph scope DAG (parent or null for root)
|
||||
|
||||
createEntity<K extends EntityKind>(kind: K): EntityIdFor<K>
|
||||
deleteEntity<K extends EntityKind>(kind: K, id: EntityIdFor<K>): void
|
||||
@@ -155,6 +159,11 @@ interface World {
|
||||
}
|
||||
```
|
||||
|
||||
Subgraphs are not a separate entity kind. A node with a `SubgraphStructure`
|
||||
component represents a subgraph. The `scopes` map tracks the graph nesting DAG.
|
||||
See [Subgraph Boundaries](subgraph-boundaries-and-promotion.md) for the full
|
||||
model.
|
||||
|
||||
Initial implementation: plain `Map`-backed. No reactivity, no CRDT, no
|
||||
persistence. The World exists but nothing populates it yet.
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ graph TD
|
||||
Map<NodeEntityId, NodeComponents>"]
|
||||
LinkStore["Links
|
||||
Map<LinkEntityId, LinkComponents>"]
|
||||
SubgraphStore["Subgraphs
|
||||
Map<SubgraphEntityId, SubgraphComponents>"]
|
||||
ScopeRegistry["Graph Scopes
|
||||
Map<GraphId, ParentGraphId | null>"]
|
||||
WidgetStore["Widgets
|
||||
Map<WidgetEntityId, WidgetComponents>"]
|
||||
SlotStore["Slots
|
||||
@@ -56,8 +56,6 @@ graph LR
|
||||
number & { __brand: 'NodeEntityId' }"]
|
||||
LID["LinkEntityId
|
||||
number & { __brand: 'LinkEntityId' }"]
|
||||
SID["SubgraphEntityId
|
||||
string & { __brand: 'SubgraphEntityId' }"]
|
||||
WID["WidgetEntityId
|
||||
number & { __brand: 'WidgetEntityId' }"]
|
||||
SLID["SlotEntityId
|
||||
@@ -68,10 +66,15 @@ number & { __brand: 'RerouteEntityId' }"]
|
||||
number & { __brand: 'GroupEntityId' }"]
|
||||
end
|
||||
|
||||
GRID["GraphId
|
||||
string & { __brand: 'GraphId' }"]:::scopeId
|
||||
|
||||
NID -.-x LID
|
||||
LID -.-x WID
|
||||
WID -.-x SLID
|
||||
|
||||
classDef scopeId fill:#2a2a4a,stroke:#4a4a6a,color:#e0e0e0,stroke-dasharray:5
|
||||
|
||||
linkStyle 0 stroke:red,stroke-dasharray:5
|
||||
linkStyle 1 stroke:red,stroke-dasharray:5
|
||||
linkStyle 2 stroke:red,stroke-dasharray:5
|
||||
@@ -79,6 +82,8 @@ number & { __brand: 'GroupEntityId' }"]
|
||||
|
||||
Red dashed lines = compile-time errors if mixed. No more accidentally passing a `LinkId` where a `NodeId` is expected.
|
||||
|
||||
Note: `GraphId` is a scope identifier, not an entity ID. It identifies which graph an entity belongs to. Subgraphs are nodes with a `SubgraphStructure` component — see [Subgraph Boundaries](subgraph-boundaries-and-promotion.md).
|
||||
|
||||
## 2. Component Composition
|
||||
|
||||
### Node: Before vs After
|
||||
@@ -414,7 +419,8 @@ graph LR
|
||||
S1["Components: small, focused
|
||||
data objects (5-10 fields each)"]
|
||||
S2["Entities are just IDs.
|
||||
No inheritance hierarchy."]
|
||||
No inheritance hierarchy.
|
||||
Subgraph = node + component."]
|
||||
S3["One system per concern.
|
||||
Systems don't overlap."]
|
||||
S4["Branded per-kind IDs.
|
||||
|
||||
@@ -4,16 +4,16 @@ This document maps the relationships and interaction patterns between all entity
|
||||
|
||||
## Entities
|
||||
|
||||
| Entity | Class | ID Type | Primary Location |
|
||||
| -------- | ------------- | --------------- | ---------------------------------------------------------------------------- |
|
||||
| Graph | `LGraph` | `UUID` | `src/lib/litegraph/src/LGraph.ts` |
|
||||
| Node | `LGraphNode` | `NodeId` | `src/lib/litegraph/src/LGraphNode.ts` |
|
||||
| Link | `LLink` | `LinkId` | `src/lib/litegraph/src/LLink.ts` |
|
||||
| Entity | Class | ID Type | Primary Location |
|
||||
| -------- | ------------- | --------------- | --------------------------------------------- |
|
||||
| Graph | `LGraph` | `UUID` | `src/lib/litegraph/src/LGraph.ts` |
|
||||
| Node | `LGraphNode` | `NodeId` | `src/lib/litegraph/src/LGraphNode.ts` |
|
||||
| Link | `LLink` | `LinkId` | `src/lib/litegraph/src/LLink.ts` |
|
||||
| Subgraph | `Subgraph` | `UUID` | `src/lib/litegraph/src/LGraph.ts` (ECS: node component, not separate entity) |
|
||||
| Widget | `BaseWidget` | name + nodeId | `src/lib/litegraph/src/widgets/BaseWidget.ts` |
|
||||
| Slot | `SlotBase` | index on parent | `src/lib/litegraph/src/node/SlotBase.ts` |
|
||||
| Reroute | `Reroute` | `RerouteId` | `src/lib/litegraph/src/Reroute.ts` |
|
||||
| Group | `LGraphGroup` | `number` | `src/lib/litegraph/src/LGraphGroup.ts` |
|
||||
| Widget | `BaseWidget` | name + nodeId | `src/lib/litegraph/src/widgets/BaseWidget.ts` |
|
||||
| Slot | `SlotBase` | index on parent | `src/lib/litegraph/src/node/SlotBase.ts` |
|
||||
| Reroute | `Reroute` | `RerouteId` | `src/lib/litegraph/src/Reroute.ts` |
|
||||
| Group | `LGraphGroup` | `number` | `src/lib/litegraph/src/LGraphGroup.ts` |
|
||||
|
||||
Under the ECS model, subgraphs are not a separate entity kind — they are nodes with `SubgraphStructure` and `SubgraphMeta` components. See [Subgraph Boundaries](subgraph-boundaries-and-promotion.md).
|
||||
|
||||
|
||||
@@ -353,14 +353,14 @@ graph TD
|
||||
|
||||
What each entity needs to reach the ECS target from [ADR 0008](../adr/0008-entity-component-system.md):
|
||||
|
||||
| Entity | Already Extracted | Still on Class | ECS Target Components | Gap |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| **Node** | pos, size (LayoutStore) | type, visual, connectivity, execution, properties, widgets, rendering, serialization | Position, NodeVisual, NodeType, Connectivity, Execution, Properties, WidgetContainer | Large — 6 components unextracted, all behavior on class |
|
||||
| **Link** | layout (LayoutStore) | endpoints, visual, state, connectivity methods | LinkEndpoints, LinkVisual, LinkState | Medium — 3 components unextracted |
|
||||
| **Widget** | value, label, disabled (WidgetValueStore); promotion (PromotionStore); DOM state (DomWidgetStore) | node back-ref, rendering, events, layout | WidgetIdentity, WidgetValue, WidgetLayout | Small — value extraction done; rendering and layout remain |
|
||||
| **Slot** | (nothing) | name, type, direction, link refs, visual, position | SlotIdentity, SlotConnection, SlotVisual | Full — no extraction started |
|
||||
| **Reroute** | pos (LayoutStore) | links, visual, chain traversal | Position, RerouteLinks, RerouteVisual | Medium — position done, rest unextracted |
|
||||
| **Group** | (nothing) | pos, size, meta, visual, children | Position, GroupMeta, GroupVisual, GroupChildren | Full — no extraction started |
|
||||
| Entity | Already Extracted | Still on Class | ECS Target Components | Gap |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
|
||||
| **Node** | pos, size (LayoutStore) | type, visual, connectivity, execution, properties, widgets, rendering, serialization | Position, NodeVisual, NodeType, Connectivity, Execution, Properties, WidgetContainer | Large — 6 components unextracted, all behavior on class |
|
||||
| **Link** | layout (LayoutStore) | endpoints, visual, state, connectivity methods | LinkEndpoints, LinkVisual, LinkState | Medium — 3 components unextracted |
|
||||
| **Widget** | value, label, disabled (WidgetValueStore); promotion (PromotionStore); DOM state (DomWidgetStore) | node back-ref, rendering, events, layout | WidgetIdentity, WidgetValue, WidgetLayout | Small — value extraction done; rendering and layout remain |
|
||||
| **Slot** | (nothing) | name, type, direction, link refs, visual, position | SlotIdentity, SlotConnection, SlotVisual | Full — no extraction started |
|
||||
| **Reroute** | pos (LayoutStore) | links, visual, chain traversal | Position, RerouteLinks, RerouteVisual | Medium — position done, rest unextracted |
|
||||
| **Group** | (nothing) | pos, size, meta, visual, children | Position, GroupMeta, GroupVisual, GroupChildren | Full — no extraction started |
|
||||
| **Subgraph** | promotions (PromotionStore) | structure, meta, I/O, all LGraph state | SubgraphStructure, SubgraphMeta (as node components) | Large — mostly unextracted; subgraph is a node with components, not a separate entity kind |
|
||||
|
||||
### Priority Order for Extraction
|
||||
|
||||
@@ -29,7 +29,7 @@ These are the same thing.
|
||||
The current codebase almost knows this. `Subgraph extends LGraph` — the
|
||||
inheritance hierarchy encodes the identity. But it encodes it as a special case
|
||||
of a general case, when in truth there is no special case. A subgraph is not a
|
||||
_kind_ of graph. A subgraph _is_ a graph. The root workflow is not a privileged
|
||||
*kind* of graph. A subgraph *is* a graph. The root workflow is not a privileged
|
||||
container — it is simply a graph that happens to have no parent.
|
||||
|
||||
This is the pattern that appears everywhere in nature and mathematics: the
|
||||
@@ -183,8 +183,8 @@ graph LR
|
||||
|
||||
Two separate links. A virtual node with a magic sentinel ID
|
||||
(`SUBGRAPH_INPUT_ID = -10`). A slot-like object that is neither a slot nor a
|
||||
node. Every link in the system carries a latent special case: _am I a boundary
|
||||
link?_ Every pack/unpack operation must remap both links and reconcile both ID
|
||||
node. Every link in the system carries a latent special case: *am I a boundary
|
||||
link?* Every pack/unpack operation must remap both links and reconcile both ID
|
||||
spaces.
|
||||
|
||||
This complexity exists because the boundary was never designed — it accreted.
|
||||
@@ -270,7 +270,6 @@ Under graph unification, packing and unpacking become operations on `graphScope`
|
||||
tags rather than on class hierarchies:
|
||||
|
||||
**Pack** (convert selection to subgraph):
|
||||
|
||||
1. Create a new `graphId`
|
||||
2. Move selected entities: change their `graphScope` to the new graph
|
||||
3. For links that crossed the selection boundary: create boundary slot mappings
|
||||
@@ -279,7 +278,6 @@ tags rather than on class hierarchies:
|
||||
component
|
||||
|
||||
**Unpack** (dissolve subgraph):
|
||||
|
||||
1. Move entities back: change their `graphScope` to the parent
|
||||
2. Reconnect boundary links directly (remove the SubgraphNode intermediary)
|
||||
3. Delete the SubgraphNode
|
||||
@@ -326,7 +324,6 @@ Promotion is not a separate mechanism. It is adding a typed input to the
|
||||
subgraph's interface.
|
||||
|
||||
When a user "promotes" widget X (type `INT`) on interior node N:
|
||||
|
||||
1. A new entry is added to `SubgraphStructure.interface.inputs`:
|
||||
`{ name: "seed", type: "INT", slotId: <new slot> }`
|
||||
2. The SubgraphNode gains a new input slot of type `INT`. The type → widget
|
||||
@@ -376,17 +373,17 @@ concept of promotion as distinct from connection.
|
||||
|
||||
### Tradeoff matrix
|
||||
|
||||
| Dimension | A: Connections-Only | B: Simplified Promotion |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| New concepts | None — reuses slots, links, widgets | `WidgetPromotion` component |
|
||||
| Code removed | PromotionStore, ViewManager, PromotedWidgetView, `_syncPromotions` | ViewManager, proxy reconciliation |
|
||||
| Shared subgraph compat | ✅ Each instance has independent interface inputs with independent values | ⚠️ Promotion delegates to a source widget by entity ID — when multiple SubgraphNode instances share a definition, which instance's source widget is authoritative? |
|
||||
| Dynamic widgets | ✅ Input type drives widget creation via existing registry | ⚠️ Must handle type changes in promotion component |
|
||||
| Serialization | Interface inputs serialized as `SubgraphIO` entries | Separate `proxyWidgets` property |
|
||||
| Backward-compatible loading | Migration: old `proxyWidgets` → interface inputs + boundary links | Direct — same serialization shape |
|
||||
| UX consistency | Promoted widgets look like normal input widgets | Promoted widgets look like proxy widgets (distinct) |
|
||||
| Widget ordering | Slot ordering (reorderable like any input) | Explicit promotion order (`movePromotion`) |
|
||||
| Nested promotion | Adding interface inputs at each nesting level — simple mechanically, but N levels = N manual promote operations for the user | `disambiguatingSourceNodeId` complexity persists |
|
||||
| Dimension | A: Connections-Only | B: Simplified Promotion |
|
||||
| --- | --- | --- |
|
||||
| New concepts | None — reuses slots, links, widgets | `WidgetPromotion` component |
|
||||
| Code removed | PromotionStore, ViewManager, PromotedWidgetView, `_syncPromotions` | ViewManager, proxy reconciliation |
|
||||
| Shared subgraph compat | ✅ Each instance has independent interface inputs with independent values | ⚠️ Promotion delegates to a source widget by entity ID — when multiple SubgraphNode instances share a definition, which instance's source widget is authoritative? |
|
||||
| Dynamic widgets | ✅ Input type drives widget creation via existing registry | ⚠️ Must handle type changes in promotion component |
|
||||
| Serialization | Interface inputs serialized as `SubgraphIO` entries | Separate `proxyWidgets` property |
|
||||
| Backward-compatible loading | Migration: old `proxyWidgets` → interface inputs + boundary links | Direct — same serialization shape |
|
||||
| UX consistency | Promoted widgets look like normal input widgets | Promoted widgets look like proxy widgets (distinct) |
|
||||
| Widget ordering | Slot ordering (reorderable like any input) | Explicit promotion order (`movePromotion`) |
|
||||
| Nested promotion | Adding interface inputs at each nesting level — simple mechanically, but N levels = N manual promote operations for the user | `disambiguatingSourceNodeId` complexity persists |
|
||||
|
||||
### Constraints that hold regardless
|
||||
|
||||
@@ -471,9 +468,9 @@ and produces the recursive `ExportedSubgraph` structure, matching the current
|
||||
format exactly. Existing workflows, the ComfyUI backend, and third-party tools
|
||||
see no change.
|
||||
|
||||
| Direction | Format | Notes |
|
||||
| --------------- | ------------------------------- | ---------------------------------------- |
|
||||
| **Save/export** | Nested (current shape) | SerializationSystem walks scope tree |
|
||||
| Direction | Format | Notes |
|
||||
| --- | --- | --- |
|
||||
| **Save/export** | Nested (current shape) | SerializationSystem walks scope tree |
|
||||
| **Load/import** | Nested (current) or future flat | Ratchet: normalize to flat World on load |
|
||||
|
||||
The "ratchet conversion" pattern: load any supported format, normalize to the
|
||||
@@ -482,10 +479,10 @@ current format on save.
|
||||
|
||||
### Widget identity at the boundary
|
||||
|
||||
| Context | Identity | Example |
|
||||
| -------------------- | ---------------------------------------------------------- | ---------------------------------- |
|
||||
| **Internal (World)** | `WidgetEntityId` (opaque branded number) | `42 as WidgetEntityId` |
|
||||
| **Serialized** | Position in `widgets_values[]` + name from node definition | `widgets_values[2]` → third widget |
|
||||
| Context | Identity | Example |
|
||||
| --- | --- | --- |
|
||||
| **Internal (World)** | `WidgetEntityId` (opaque branded number) | `42 as WidgetEntityId` |
|
||||
| **Serialized** | Position in `widgets_values[]` + name from node definition | `widgets_values[2]` → third widget |
|
||||
|
||||
On save: the `SerializationSystem` queries `WidgetIdentity.name` and
|
||||
`WidgetValue.value`, produces the positional array ordered by widget creation
|
||||
@@ -547,17 +544,17 @@ This is a hard constraint with no expiration:
|
||||
This document proposes or surfaces the following changes to
|
||||
[ADR 0008](../adr/0008-entity-component-system.md):
|
||||
|
||||
| Area | Current ADR 0008 | Proposed Change |
|
||||
| ------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| Entity taxonomy | 7 kinds including `SubgraphEntityId` | 6 kinds — subgraph is a node with `SubgraphStructure` component |
|
||||
| `SubgraphEntityId` | `string & { __brand: 'SubgraphEntityId' }` | Eliminated; replaced by `GraphId` scope identifier |
|
||||
| Subgraph components | `SubgraphStructure`, `SubgraphMeta` listed as separate-entity components | Become node components on SubgraphNode entities |
|
||||
| World structure | Implied per-graph containment | Flat World with `graphScope` tags; one World per workflow |
|
||||
| Acyclicity | Not addressed | DAG invariant on `SubgraphStructure.graphId` references, enforced on mutation |
|
||||
| Boundary model | Deferred | Typed interface contracts on `SubgraphStructure`; no virtual nodes or magic IDs |
|
||||
| Widget promotion | Treated as a given feature to migrate | Open decision: Candidate A (connections-only) vs B (simplified component) |
|
||||
| Serialization | Not explicitly separated from internal model | Internal model ≠ wire format; `SerializationSystem` is the membrane |
|
||||
| Backward compat | Implicit | Explicit contract: load any prior format, indefinitely |
|
||||
| Area | Current ADR 0008 | Proposed Change |
|
||||
| --- | --- | --- |
|
||||
| Entity taxonomy | 7 kinds including `SubgraphEntityId` | 6 kinds — subgraph is a node with `SubgraphStructure` component |
|
||||
| `SubgraphEntityId` | `string & { __brand: 'SubgraphEntityId' }` | Eliminated; replaced by `GraphId` scope identifier |
|
||||
| Subgraph components | `SubgraphStructure`, `SubgraphMeta` listed as separate-entity components | Become node components on SubgraphNode entities |
|
||||
| World structure | Implied per-graph containment | Flat World with `graphScope` tags; one World per workflow |
|
||||
| Acyclicity | Not addressed | DAG invariant on `SubgraphStructure.graphId` references, enforced on mutation |
|
||||
| Boundary model | Deferred | Typed interface contracts on `SubgraphStructure`; no virtual nodes or magic IDs |
|
||||
| Widget promotion | Treated as a given feature to migrate | Open decision: Candidate A (connections-only) vs B (simplified component) |
|
||||
| Serialization | Not explicitly separated from internal model | Internal model ≠ wire format; `SerializationSystem` is the membrane |
|
||||
| Backward compat | Implicit | Explicit contract: load any prior format, indefinitely |
|
||||
|
||||
These amendments should be applied to ADR 0008 and the related architecture
|
||||
documents in a follow-up pass after team review of this document:
|
||||
|
||||
Reference in New Issue
Block a user