mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 00:10:40 +00:00
## Summary
Architecture documentation proposing an Entity Component System for the
litegraph layer.
```mermaid
graph LR
subgraph Today["Today: Spaghetti"]
God["🍝 God Objects"]
Circ["🔄 Circular Deps"]
Mut["💥 Render Mutations"]
end
subgraph Tomorrow["Tomorrow: ECS"]
ID["🏷️ Branded IDs"]
Comp["📦 Components"]
Sys["⚙️ Systems"]
World["🌍 World"]
end
God -->|"decompose"| Comp
Circ -->|"flatten"| ID
Mut -->|"separate"| Sys
Comp --> World
ID --> World
Sys -->|"query"| World
```
## Changes
- **What**: ADR 0008 + 4 architecture docs (no code changes)
- `docs/adr/0008-entity-component-system.md` — entity taxonomy, branded
IDs, component decomposition, migration strategy
- `docs/architecture/entity-interactions.md` — as-is Mermaid diagrams of
all entity relationships
- `docs/architecture/entity-problems.md` — structural problems with
file:line evidence
- `docs/architecture/ecs-target-architecture.md` — target architecture
diagrams
- `docs/architecture/proto-ecs-stores.md` — analysis of existing Pinia
stores as proto-ECS patterns
## Review Focus
- Does the entity taxonomy (Node, Link, Subgraph, Widget, Slot, Reroute,
Group) cover all cases?
- Are the component decompositions reasonable starting points?
- Is the migration strategy (bridge layer, incremental extraction)
feasible?
- Are there entity interactions or problems we missed?
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10420-docs-ADR-0008-Entity-Component-System-32d6d73d365081feb048d16a5231d350)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
569 lines
16 KiB
Markdown
569 lines
16 KiB
Markdown
# ECS Target Architecture
|
|
|
|
This document describes the target ECS architecture for the litegraph entity system. It shows how the entities and interactions from the [current system](entity-interactions.md) transform under ECS, and how the [structural problems](entity-problems.md) are resolved. For the full design rationale, see [ADR 0008](../adr/0008-entity-component-system.md).
|
|
|
|
## 1. World Overview
|
|
|
|
The World is the single source of truth for runtime entity state in one
|
|
workflow instance. Entities are just branded IDs. Components are plain data
|
|
objects. Systems are functions that query the World.
|
|
|
|
```mermaid
|
|
graph TD
|
|
subgraph World["World (Central Registry)"]
|
|
direction TB
|
|
NodeStore["Nodes
|
|
Map<NodeEntityId, NodeComponents>"]
|
|
LinkStore["Links
|
|
Map<LinkEntityId, LinkComponents>"]
|
|
ScopeRegistry["Graph Scopes
|
|
Map<GraphId, ParentGraphId | null>"]
|
|
WidgetStore["Widgets
|
|
Map<WidgetEntityId, WidgetComponents>"]
|
|
SlotStore["Slots
|
|
Map<SlotEntityId, SlotComponents>"]
|
|
RerouteStore["Reroutes
|
|
Map<RerouteEntityId, RerouteComponents>"]
|
|
GroupStore["Groups
|
|
Map<GroupEntityId, GroupComponents>"]
|
|
end
|
|
|
|
subgraph Systems["Systems (Behavior)"]
|
|
direction TB
|
|
RS["RenderSystem"]
|
|
SS["SerializationSystem"]
|
|
CS["ConnectivitySystem"]
|
|
LS["LayoutSystem"]
|
|
ES["ExecutionSystem"]
|
|
VS["VersionSystem"]
|
|
end
|
|
|
|
RS -->|reads| World
|
|
SS -->|reads/writes| World
|
|
CS -->|reads/writes| World
|
|
LS -->|reads/writes| World
|
|
ES -->|reads| World
|
|
VS -->|reads/writes| World
|
|
|
|
style World fill:#1a1a2e,stroke:#16213e,color:#e0e0e0
|
|
style Systems fill:#0f3460,stroke:#16213e,color:#e0e0e0
|
|
```
|
|
|
|
### Entity IDs
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph "Branded IDs (compile-time distinct)"
|
|
NID["NodeEntityId
|
|
number & { __brand: 'NodeEntityId' }"]
|
|
LID["LinkEntityId
|
|
number & { __brand: 'LinkEntityId' }"]
|
|
WID["WidgetEntityId
|
|
number & { __brand: 'WidgetEntityId' }"]
|
|
SLID["SlotEntityId
|
|
number & { __brand: 'SlotEntityId' }"]
|
|
RID["RerouteEntityId
|
|
number & { __brand: 'RerouteEntityId' }"]
|
|
GID["GroupEntityId
|
|
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
|
|
```
|
|
|
|
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).
|
|
|
|
### Linked subgraphs and instance-varying state
|
|
|
|
Linked subgraph definitions can be shared structurally, but mutable values are
|
|
instance-scoped.
|
|
|
|
- Shared definition-level data (interface shape, default metadata) can be reused
|
|
across instances.
|
|
- Runtime state (`WidgetValue`, execution/transient state, selection) is scoped
|
|
to the containing `graphId` chain inside one World instance.
|
|
- "Single source of truth" therefore means one source per workflow instance,
|
|
not one global source across all linked instances.
|
|
|
|
### Recursive subgraphs without inheritance
|
|
|
|
Recursive containment is represented through graph scopes rather than
|
|
`Subgraph extends LGraph` inheritance.
|
|
|
|
- A subgraph node points to a child graph via
|
|
`SubgraphStructure.childGraphId`.
|
|
- The scope registry stores `childGraphId -> parentGraphId` links.
|
|
- Depth queries traverse this scope DAG, then filter entities by `graphScope`.
|
|
|
|
## 2. Component Composition
|
|
|
|
### Node: Before vs After
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Before["LGraphNode (monolith)"]
|
|
direction TB
|
|
B1["pos, size, bounding"]
|
|
B2["color, bgcolor, title"]
|
|
B3["type, category, nodeData"]
|
|
B4["inputs[], outputs[]"]
|
|
B5["order, mode, flags"]
|
|
B6["properties, properties_info"]
|
|
B7["widgets[]"]
|
|
B8["serialize(), configure()"]
|
|
B9["drawSlots(), drawWidgets()"]
|
|
B10["execute(), triggerSlot()"]
|
|
B11["graph._version++"]
|
|
B12["connect(), disconnect()"]
|
|
end
|
|
|
|
subgraph After["NodeEntityId + Components"]
|
|
direction TB
|
|
A1["Position
|
|
{ pos, size, bounding }"]
|
|
A2["NodeVisual
|
|
{ color, bgcolor, boxcolor, title }"]
|
|
A3["NodeType
|
|
{ type, category, nodeData }"]
|
|
A4["Connectivity
|
|
{ inputSlotIds[], outputSlotIds[] }"]
|
|
A5["Execution
|
|
{ order, mode, flags }"]
|
|
A6["Properties
|
|
{ properties, propertiesInfo }"]
|
|
A7["WidgetContainer
|
|
{ widgetIds[] }"]
|
|
end
|
|
|
|
B1 -.-> A1
|
|
B2 -.-> A2
|
|
B3 -.-> A3
|
|
B4 -.-> A4
|
|
B5 -.-> A5
|
|
B6 -.-> A6
|
|
B7 -.-> A7
|
|
|
|
B8 -.->|"moves to"| SYS1["SerializationSystem"]
|
|
B9 -.->|"moves to"| SYS2["RenderSystem"]
|
|
B10 -.->|"moves to"| SYS3["ExecutionSystem"]
|
|
B11 -.->|"moves to"| SYS4["VersionSystem"]
|
|
B12 -.->|"moves to"| SYS5["ConnectivitySystem"]
|
|
|
|
style Before fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
style After fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
|
```
|
|
|
|
### Link: Before vs After
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Before["LLink (class)"]
|
|
direction TB
|
|
B1["origin_id, origin_slot
|
|
target_id, target_slot, type"]
|
|
B2["color, path, _pos"]
|
|
B3["_dragging, data"]
|
|
B4["disconnect()"]
|
|
B5["resolve()"]
|
|
end
|
|
|
|
subgraph After["LinkEntityId + Components"]
|
|
direction TB
|
|
A1["LinkEndpoints
|
|
{ originId, originSlot,
|
|
targetId, targetSlot, type }"]
|
|
A2["LinkVisual
|
|
{ color, path, centerPos }"]
|
|
A3["LinkState
|
|
{ dragging, data }"]
|
|
end
|
|
|
|
B1 -.-> A1
|
|
B2 -.-> A2
|
|
B3 -.-> A3
|
|
B4 -.->|"moves to"| SYS1["ConnectivitySystem"]
|
|
B5 -.->|"moves to"| SYS2["ConnectivitySystem"]
|
|
|
|
style Before fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
style After fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
|
```
|
|
|
|
### Widget: Before vs After
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Before["BaseWidget (class)"]
|
|
direction TB
|
|
B1["name, type, _node"]
|
|
B2["value, options, serialize"]
|
|
B3["computedHeight, margin"]
|
|
B4["drawWidget(), onClick()"]
|
|
B5["useWidgetValueStore()"]
|
|
B6["usePromotionStore()"]
|
|
end
|
|
|
|
subgraph After["WidgetEntityId + Components"]
|
|
direction TB
|
|
A1["WidgetIdentity
|
|
{ name, widgetType, parentNodeId }"]
|
|
A2["WidgetValue
|
|
{ value, options, serialize }"]
|
|
A3["WidgetLayout
|
|
{ computedHeight, constraints }"]
|
|
end
|
|
|
|
B1 -.-> A1
|
|
B2 -.-> A2
|
|
B3 -.-> A3
|
|
B4 -.->|"moves to"| SYS1["RenderSystem"]
|
|
B5 -.->|"absorbed by"| SYS2["World (is the store)"]
|
|
B6 -.->|"moves to"| SYS3["PromotionSystem"]
|
|
|
|
style Before fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
style After fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
|
```
|
|
|
|
## 3. System Architecture
|
|
|
|
Systems are pure functions that query the World for entities with specific component combinations. Each system owns exactly one concern.
|
|
|
|
```mermaid
|
|
graph TD
|
|
subgraph InputPhase["Input Phase"]
|
|
UserInput["User Input
|
|
(pointer, keyboard)"]
|
|
APIInput["API Input
|
|
(backend execution results)"]
|
|
end
|
|
|
|
subgraph UpdatePhase["Update Phase (ordered)"]
|
|
direction TB
|
|
CS["ConnectivitySystem
|
|
Manages link/slot mutations.
|
|
Writes: LinkEndpoints, SlotConnection,
|
|
Connectivity"]
|
|
VS["VersionSystem
|
|
Centralizes change tracking.
|
|
Replaces 15+ scattered _version++.
|
|
Writes: version counter"]
|
|
LS["LayoutSystem
|
|
Computes positions and sizes.
|
|
Runs BEFORE render, not during.
|
|
Reads: Connectivity, WidgetContainer
|
|
Writes: Position, SlotVisual, WidgetLayout"]
|
|
ES["ExecutionSystem
|
|
Determines run order.
|
|
Reads: Connectivity, Execution
|
|
Writes: Execution.order"]
|
|
end
|
|
|
|
subgraph RenderPhase["Render Phase (read-only)"]
|
|
RS["RenderSystem
|
|
Pure read of components.
|
|
No state mutation.
|
|
Reads: Position, *Visual, *Layout"]
|
|
end
|
|
|
|
subgraph PersistPhase["Persist Phase"]
|
|
SS["SerializationSystem
|
|
Reads/writes all components.
|
|
Handles workflow JSON."]
|
|
end
|
|
|
|
UserInput --> CS
|
|
APIInput --> ES
|
|
CS --> VS
|
|
VS --> LS
|
|
LS --> RS
|
|
CS --> SS
|
|
|
|
style InputPhase fill:#2a2a4a,stroke:#3a3a5a,color:#e0e0e0
|
|
style UpdatePhase fill:#1a3a2a,stroke:#2a4a3a,color:#e0e0e0
|
|
style RenderPhase fill:#3a2a1a,stroke:#4a3a2a,color:#e0e0e0
|
|
style PersistPhase fill:#2a2a3a,stroke:#3a3a4a,color:#e0e0e0
|
|
```
|
|
|
|
### System-Component Access Matrix
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Systems
|
|
RS["Render"]
|
|
SS["Serialization"]
|
|
CS["Connectivity"]
|
|
LS["Layout"]
|
|
ES["Execution"]
|
|
VS["Version"]
|
|
end
|
|
|
|
subgraph Components
|
|
Pos["Position"]
|
|
NV["NodeVisual"]
|
|
NT["NodeType"]
|
|
Con["Connectivity"]
|
|
Exe["Execution"]
|
|
Props["Properties"]
|
|
WC["WidgetContainer"]
|
|
LE["LinkEndpoints"]
|
|
LV["LinkVisual"]
|
|
SC["SlotConnection"]
|
|
SV["SlotVisual"]
|
|
WVal["WidgetValue"]
|
|
WL["WidgetLayout"]
|
|
end
|
|
|
|
RS -.->|read| Pos
|
|
RS -.->|read| NV
|
|
RS -.->|read| LV
|
|
RS -.->|read| SV
|
|
RS -.->|read| WL
|
|
|
|
LS -->|write| Pos
|
|
LS -->|write| SV
|
|
LS -->|write| WL
|
|
LS -.->|read| Con
|
|
LS -.->|read| WC
|
|
|
|
CS -->|write| LE
|
|
CS -->|write| SC
|
|
CS -->|write| Con
|
|
|
|
ES -.->|read| Con
|
|
ES -->|write| Exe
|
|
|
|
SS -.->|read/write| Pos
|
|
SS -.->|read/write| NT
|
|
SS -.->|read/write| Props
|
|
SS -.->|read/write| WVal
|
|
SS -.->|read/write| LE
|
|
|
|
VS -.->|read| Pos
|
|
VS -.->|read| Con
|
|
```
|
|
|
|
## 4. Dependency Flow
|
|
|
|
### Before: Tangled References
|
|
|
|
```mermaid
|
|
graph TD
|
|
Node["LGraphNode"] <-->|"circular"| Graph["LGraph"]
|
|
Graph <-->|"circular"| Subgraph["Subgraph"]
|
|
Node -->|"this.graph._links"| Links["LLink Map"]
|
|
Node -->|"this.graph.getNodeById"| Node
|
|
Canvas["LGraphCanvas"] -->|"node.graph._version++"| Graph
|
|
Canvas -->|"node.graph.remove(node)"| Graph
|
|
Widget["BaseWidget"] -->|"useWidgetValueStore()"| Store1["Pinia Store"]
|
|
Widget -->|"usePromotionStore()"| Store2["Pinia Store"]
|
|
Node -->|"useLayoutMutations()"| Store3["Layout Store"]
|
|
Graph -->|"useLayoutMutations()"| Store3
|
|
LLink["LLink"] -->|"useLayoutMutations()"| Store3
|
|
|
|
style Node fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
style Graph fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
style Canvas fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
style Widget fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
```
|
|
|
|
### After: Unidirectional Data Flow
|
|
|
|
```mermaid
|
|
graph TD
|
|
subgraph Systems["Systems"]
|
|
RS["RenderSystem"]
|
|
CS["ConnectivitySystem"]
|
|
LS["LayoutSystem"]
|
|
ES["ExecutionSystem"]
|
|
SS["SerializationSystem"]
|
|
VS["VersionSystem"]
|
|
end
|
|
|
|
World["World
|
|
(instance-scoped source of truth)"]
|
|
|
|
subgraph Components["Component Stores"]
|
|
Pos["Position"]
|
|
Vis["*Visual"]
|
|
Con["Connectivity"]
|
|
Val["*Value"]
|
|
end
|
|
|
|
Systems -->|"query/mutate"| World
|
|
World -->|"contains"| Components
|
|
|
|
style Systems fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
|
style World fill:#1a1a4a,stroke:#2a2a6a,color:#e0e0e0
|
|
style Components fill:#1a3a3a,stroke:#2a4a4a,color:#e0e0e0
|
|
```
|
|
|
|
Key differences:
|
|
|
|
- **No circular dependencies**: entities are IDs, not class instances
|
|
- **No Demeter violations**: systems query the World directly, never reach through entities
|
|
- **No scattered store access**: the World _is_ the store; systems are the only writers
|
|
- **Unidirectional**: Input → Systems → World → Render (no back-edges)
|
|
- **Instance safety**: linked definitions can be reused without forcing shared
|
|
mutable widget/execution state across instances
|
|
|
|
## 5. Problem Resolution Map
|
|
|
|
How each problem from [entity-problems.md](entity-problems.md) is resolved:
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Problems["Current Problems"]
|
|
P1["God Objects
|
|
(9k+ line classes)"]
|
|
P2["Circular Deps
|
|
(LGraph ↔ Subgraph)"]
|
|
P3["Mixed Concerns
|
|
(render + domain + state)"]
|
|
P4["Inconsistent IDs
|
|
(number|string, no safety)"]
|
|
P5["Demeter Violations
|
|
(graph._links, graph._version++)"]
|
|
P6["Scattered Side Effects
|
|
(15+ _version++ sites)"]
|
|
P7["Render-Time Mutations
|
|
(arrange() during draw)"]
|
|
end
|
|
|
|
subgraph Solutions["ECS Solutions"]
|
|
S1["Components: small, focused
|
|
data objects (5-10 fields each)"]
|
|
S2["Entities are just IDs.
|
|
No inheritance hierarchy.
|
|
Subgraph = node + component."]
|
|
S3["One system per concern.
|
|
Systems don't overlap."]
|
|
S4["Branded per-kind IDs.
|
|
Compile-time type errors."]
|
|
S5["Systems query World.
|
|
No entity→entity refs."]
|
|
S6["VersionSystem owns
|
|
all change tracking."]
|
|
S7["LayoutSystem runs in
|
|
update phase, before render.
|
|
RenderSystem is read-only."]
|
|
end
|
|
|
|
P1 --> S1
|
|
P2 --> S2
|
|
P3 --> S3
|
|
P4 --> S4
|
|
P5 --> S5
|
|
P6 --> S6
|
|
P7 --> S7
|
|
|
|
style Problems fill:#4a1a1a,stroke:#6a2a2a,color:#e0e0e0
|
|
style Solutions fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
|
```
|
|
|
|
## 6. Migration Bridge
|
|
|
|
The migration is incremental. During the transition, a bridge layer keeps legacy class properties and ECS components in sync.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Legacy as Legacy Code
|
|
participant Class as LGraphNode (class)
|
|
participant Bridge as Bridge Adapter
|
|
participant World as World (ECS)
|
|
participant New as New Code / Systems
|
|
|
|
Note over Legacy,New: Phase 1: Bridge reads from class, writes to World
|
|
|
|
Legacy->>Class: node.pos = [100, 200]
|
|
Class->>Bridge: pos setter intercepted
|
|
Bridge->>World: world.setComponent(nodeId, Position, { pos: [100, 200] })
|
|
|
|
New->>World: world.getComponent(nodeId, Position)
|
|
World-->>New: { pos: [100, 200], size: [...] }
|
|
|
|
Note over Legacy,New: Phase 2: New features build on ECS directly
|
|
|
|
New->>World: world.setComponent(nodeId, Position, { pos: [150, 250] })
|
|
World->>Bridge: change detected
|
|
Bridge->>Class: node._pos = [150, 250]
|
|
Legacy->>Class: node.pos
|
|
Class-->>Legacy: [150, 250]
|
|
|
|
Note over Legacy,New: Phase 3: Legacy code migrated, bridge removed
|
|
|
|
New->>World: world.getComponent(nodeId, Position)
|
|
World-->>New: { pos: [150, 250] }
|
|
```
|
|
|
|
### Incremental layout/render separation
|
|
|
|
Layout extraction is staged by node family, not all-at-once:
|
|
|
|
1. Mark `arrange()` as deprecated in render paths and collect call-site
|
|
telemetry.
|
|
2. Run `LayoutSystem` during update for a selected node family behind a feature
|
|
gate.
|
|
3. Keep a temporary compatibility fallback for un-migrated node families only.
|
|
4. Remove the fallback once parity tests and frame-time budgets pass.
|
|
|
|
This keeps `RenderSystem` read-only for migrated families while preserving
|
|
incremental rollout safety.
|
|
|
|
### Migration Phases
|
|
|
|
```mermaid
|
|
graph LR
|
|
subgraph Phase1["Phase 1: Types Only"]
|
|
T1["Define branded IDs"]
|
|
T2["Define component interfaces"]
|
|
T3["Define World type"]
|
|
end
|
|
|
|
subgraph Phase2["Phase 2: Bridge"]
|
|
B1["Bridge adapters
|
|
class ↔ World sync"]
|
|
B2["New features use
|
|
World as source"]
|
|
B3["Old code unchanged"]
|
|
end
|
|
|
|
subgraph Phase3["Phase 3: Extract"]
|
|
E1["Migrate one component
|
|
at a time"]
|
|
E2["Deprecate class
|
|
properties"]
|
|
E3["Systems replace
|
|
methods"]
|
|
end
|
|
|
|
subgraph Phase4["Phase 4: Clean"]
|
|
C1["Remove bridge"]
|
|
C2["Remove legacy classes"]
|
|
C3["Systems are sole
|
|
behavior layer"]
|
|
end
|
|
|
|
Phase1 --> Phase2 --> Phase3 --> Phase4
|
|
|
|
style Phase1 fill:#1a2a4a,stroke:#2a3a5a,color:#e0e0e0
|
|
style Phase2 fill:#1a3a3a,stroke:#2a4a4a,color:#e0e0e0
|
|
style Phase3 fill:#2a3a1a,stroke:#3a4a2a,color:#e0e0e0
|
|
style Phase4 fill:#1a4a1a,stroke:#2a6a2a,color:#e0e0e0
|
|
```
|
|
|
|
This diagram is intentionally high level. The operational Phase 4 -> 5 entry
|
|
criteria (compatibility matrix, bridge fallback usage, rollback requirements)
|
|
are defined in [ecs-migration-plan.md](ecs-migration-plan.md).
|