Reimagining the inspector graph layer (
MutableGraphImpl,Graph,Node,Edge,GraphQueries,DescribeResultCache) as SCA controllers underGraphController.
┌─────────────────────────────────────────────────────────────────┐
│ SCA Layer │
│ │
│ GraphController (@field-backed reactive state) │
│ ├─ _graph: GraphDescriptor │
│ ├─ version / topologyVersion │
│ ├─ _myTools, _agentModeTools, _components │
│ └─ implements MutableGraphStore { set/get } │
│ │ │
│ │ stores │
│ ▼ │
│ ~~MutableGraphImpl (inspector layer, non-reactive)~~ │
│ ~~├─ .graph → GraphDescriptor (duplicated)~~ │
│ ~~(deleted — GraphController now IS the MutableGraph)~~ │
│ ├─ .nodes → Node (InspectableNode per node) │
│ └─ .describe → DescribeResultCache (signal-backed) │
│ │ │
│ │ used by │
│ ▼ │
│ EditableGraph (mutation API, edit operations, history) │
│ ├─ .edit([specs]) → applies operations via context.mutable │
│ ├─ .inspect(id) → reads mutable.graphs.get() │
│ └─ fires graphchange/graphchangereject events │
└──────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SCA Layer │
│ │
│ GraphController (implements MutableGraph directly) │
│ ├─ @field graph: GraphDescriptor │
│ ├─ @field version / topologyVersion │
│ ├─ NodeController[] │
│ │ ├─ @field describeResult (replaces DescribeResultCache) │
│ │ ├─ ports (derived) │
│ │ └─ incoming/outgoing edges (derived) │
│ ├─ EdgeController[] │
│ ├─ SubgraphController[] │
│ └─ AssetController[] │
│ │ │
│ │ satisfies MutableGraph contract │
│ ▼ │
│ EditableGraph (unchanged initially, then → Actions) │
│ ├─ takes GraphController as its MutableGraph │
│ └─ eventually inlined into SCA Actions │
└──────────────────────────────────────────────────────────────────┘
| File | Role |
|---|---|
packages/visual-editor/src/engine/inspector/graph/mutable-graph.ts |
MutableGraphImpl, now GraphController |
packages/visual-editor/src/engine/inspector/graph/graph.ts |
InspectableGraph per sub-graph — to become SubgraphController |
packages/visual-editor/src/engine/inspector/graph/node.ts |
InspectableNode — to become NodeController |
packages/visual-editor/src/engine/inspector/graph/edge.ts |
InspectableEdge — to become EdgeController |
packages/visual-editor/src/engine/inspector/graph/graph-queries.ts |
Stateless queries — to fold into controllers |
packages/visual-editor/src/engine/inspector/graph/describe-cache.ts |
Signal-backed cache — to become @field on NodeController |
packages/visual-editor/src/engine/editor/graph.ts |
EditableGraph — consumer of MutableGraph |
packages/visual-editor/src/sca/controller/subcontrollers/editor/graph/graph-controller.ts |
Target: absorbs all of the above |
packages/visual-editor/src/sca/actions/board/helpers/initialize-editor.ts |
Creates MutableGraphImpl — will pass GraphController instead |
The MutableGraph type contract that EditableGraph depends on:
type MutableGraph = {
graph: GraphDescriptor;
readonly id: MainGraphIdentifier;
readonly deps: GraphStoreArgs;
readonly graphs: InspectableGraphCache; // .get(), .graphs()
readonly nodes: InspectableNodeCache; // .get(), .nodes(), .byType()
readonly describe: InspectableDescriberResultCache;
readonly store: MutableGraphStore;
update(graph, visualOnly, affectedNodes): void;
rebuild(graph): void;
};Only 2 of 18 edit operations touch context.mutable directly:
replace-graph.ts→rebuild()move-to-graph.ts→graphs.get()
No consumers of InspectableGraph/InspectableNode/InspectableEdge exist
outside visual-editor.
Make GraphController satisfy the MutableGraph type contract directly, then
swap out the MutableGraphImpl instantiation in initialize-editor.ts.
- Add
MutableGraphfields toGraphController(id,deps,graphs,nodes,describe) - Implement
update()andrebuild()onGraphController, delegating to existingDescribeResultCache,Graph,Nodeclasses initially - Update
initialize-editor.tsto passgraphControlleras theMutableGraphinstead of creatingMutableGraphImpl - Update
EditableGraphconstructor to receiveGraphController - Verify build passes
- Verify all tests pass
Replace editor.inspect() call sites with direct GraphController access, then
delete the inspector layer wholesale.
Strategy: Migrate outside-in.
GraphQueriesis internal plumbing consumed only byGraphandNode— it gets swept away automatically once nothing callseditor.inspect().
Stage 1 — Simple cache hits (done)
- Remove
MutableGraphImplclass, deaddryRuncode, relocate factory to test helpers - Replace 5
editor.inspect().nodeById()calls insideGraphControllerwiththis.nodes.get()/this.graph
Stage 2 — Enrich GraphController API
- Add
inspect(graphId)method backed bynew Graph(graphId, this)—GraphControllerIS aMutableGraph, soGraph/Nodework out-of-the-box - Write tests for
inspect()(main graph, sub-graphs, node methods) andgetFilteredComponents - 2 internal calls (
getRoutes,#updateComponents) still use_editor.inspect()because they run duringsetEditor()beforeinitialize()populates caches. These will be migrated whensetEditor + initializelifecycle is unified.
Stage 3 — Migrate consumers + delete
All consumers call editor.inspect(graphId) → InspectableGraph. With
graphController.inspect(graphId) available, these can switch to
controller.editor.graph.inspect(graphId) instead.
- Migrate
node-actions.ts(8 calls:autoname,onNodeChange,onMoveSelection,onDelete,onSelectAll,onCopy,onCut,onPaste/onDuplicate) - Migrate
graph-actions.ts(1 call:replaceWithTheme) - Migrate
graph-editing-chat.ts(1 call) - Migrate
graph-utils.ts(7 functions acceptInspectableGraph— these acceptgraphController.inspect()output unchanged since it returns the sameInspectableGraphtype; no changes needed) - Migrate 2 remaining internal
_editor.inspect()calls inGraphController(getRoutes,#updateComponents) — usesthis.inspect("")directly;_editor.inspect()fallback removed - Verify build + tests pass
Stage 4 — Relocate utility functions out of inspector layer
Move standalone utility functions so the inspector directory becomes
self-contained (only Graph, Node, DescribeResultCache remain, consumed
solely by GraphController.inspect()).
- Move
routesFromConfiguration()+toolsFromConfiguration()fromgraph-queries.ts→utils/control.ts - Move
fixUpStarEdge()+unfixUpStarEdge()+fixupConstantEdge()fromedge.ts→engine/editor/operations/edge-utils.ts[NEW] - Move
GraphDescriptorHandlefrominspector/graph/→engine/editor/graph-descriptor-handle.ts[MOVED, original deleted] - Verify inspector files are now internal-only:
graph-queries.tsandedge.tsstill provideEdge/GraphQueriesclasses used bygraph.ts/node.ts; they will be deleted with the inspector layer - Verify build + tests pass
Make GraphController fully reactive by absorbing describe refresh and
eliminating the event bridge. EditableGraph stays as a mutation/validation API
— the focus is on shifting the underlying model.
Key insight:
DescribeResultCachewas the last piece not owned byGraphController. With it absorbed,GraphControllerhas full control over all derived state (nodes, graphs, tools, components, describe). The next step is eliminating the event bridge soEditableGraphcallsGraphControllerdirectly.
Stage 1 — Absorb DescribeResultCache into GraphController ✅
- Define
NodeDescriberfunction type (SCA Service→Controller boundary) - Create signal-backed
NodeDescribeEntryclass (replaces cache entries) - Replace
MutableGraph.describefield withdescribeNode()method - Remove
InspectableDescriberResultCachetypes from@breadboard-ai/types -
GraphControllerowns#describeEntriesmap,describeNode(),#refreshDescribers(), and acceptsNodeDescriberininitialize() - Build
NodeDescriberclosure ininitialize-editor.tsfromgetHandler+sandbox(avoids circular deps) - Update
Nodeclass to usedescribeNode()instead ofdescribe.get() - Delete
describe-cache.ts - Verify build + tests pass
Stage 2 — Eliminate event bridge
Have EditableGraph call GraphController methods directly instead of firing
graphchange/graphchangereject events. GraphController.setEditor() stops
adding event listeners.
- Add direct notification methods on
GraphController - Wire
EditableGraphto call them instead of dispatching events - Remove event listener setup in
setEditor() - Remove
ChangeEvent/ChangeRejectEventclasses - Verify build + tests pass
Stage 3 — Migrate remaining editor?.inspect() UI calls
~5 UI components still call editor?.inspect("") (via EditableGraph) instead
of graphController.inspect().
- Migrate
entity-editor.ts,renderer.ts,canvas-controller.ts - Verify build + tests pass
Stage 4 — (Optional) Flatten EditableGraph into Actions
With events gone, EditableGraph is just validation + history. Flattening it
into SCA Actions becomes trivial and optional.
- Move edit history into
HistoryController - Convert
EditOperations into SCA Actions - Remove
EditableGraphclass - Update/deprecate
EditableGraphtype in@breadboard-ai/types