Skip to content

SY-3833-3: Migrate Schematic Component to Pluto-Owned State#2291

Open
emilbon99 wants to merge 19 commits intosy-3833-2from
sy-3833-3
Open

SY-3833-3: Migrate Schematic Component to Pluto-Owned State#2291
emilbon99 wants to merge 19 commits intosy-3833-2from
sy-3833-3

Conversation

@emilbon99
Copy link
Copy Markdown
Contributor

@emilbon99 emilbon99 commented May 2, 2026

Issue Pull Request

Linear Issue

SY-3833

Description

Third PR in the SY-3833 schematic split series. Wires the schematic action codec from #2290 through to the consumer side: Pluto now owns schematic graph state via flux + dispatch, and Console renders the new self-contained Schematic component.

Stacked on #2290. Targets sy-3833-2. Flatten in order.

What changes

Oracle codegen — TS actions plugin

  • New oracle/plugin/ts/types/actions.go mirrors the Go actions plugin. Generates actions.gen.ts per schema-with-actions: payload zod schemas, discriminated union actionZ, action constructors, reduce / reduceAll (the latter using structuredClone, no immer dependency).
  • Wired into (*Plugin).Generate.

Client SDK

  • client/ts/src/schematic/actions.gen.ts (generated) — payload schemas + constructors for the six actions.
  • actions.ts — hand-written handle* mutators called by the generated reducer; semantics mirror the Go Handle methods byte-for-byte.
  • scoped.tsscopedActionZ for decoding sy_schematic_set signal frames.
  • client.schematics.dispatch(key, sessionKey, actions) — calls the /schematic/dispatch endpoint added in Part 2.

Pluto

  • queries.ts gains: useDispatch (optimistic + rollback + augment edge segments on node moves via connector.updateSegmentsForPositionChanges), useSelectProps, useSelectEdge, useSelectElementDigests, useSelectElementsInfo, useSelectElementNames, useSelectSnapshot, useSelectAuthority, useAddNode, plus a sy_schematic_set listener that re-applies remote action batches and self-dedups via session key.
  • Schematic.tsx becomes self-contained: drops the create(hooks) factory. Pulls graph state with useRetrieve, dispatches actions for node / edge changes and drops. Stubs onUndo / onRedo.
  • New node/Node.tsx extracted from the old factory; reads per-node props via useSelectProps, dispatches setProps on changes.
  • edge/Edge.tsx reads via useSelectProps and dispatches directly. EdgeProps extension dropped.

Console

  • console/src/schematic/Schematic.tsx shrinks 521 → 100 lines. Drops Base.create(hooks), useUndoableDispatch, useSyncComponent, useLoadRemote, useAddSymbol, and the in-component graph-state effect handlers. Renders <Base.Schematic resourceKey={layoutKey}> directly. Re-exports HAUL_TYPE from Pluto for the channel ontology drag-and-drop wiring.

Out of scope (follow-ups)

  • Console schematic slice still carries graph fields (nodes/edges/props/legend.colors/etc.). Those are unread now since the canvas reads through Pluto, but slice gut + v6 simplification + selectors trim + toolbar migration are scoped to a follow-up to keep this PR reviewable.
  • Undo / redo for action history (stubbed).
  • Off-page-reference cross-schematic navigation (was deleted in v1; will be ported back via Schematic.useRetrieve in the toolbar follow-up).
  • pendingUpload deferred-import path (depends on imex which isn't in v2 Pluto).
  • Removing the SetData endpoint (kept alive alongside Dispatch until console toolbar is fully migrated).

Basic Readiness

  • I have performed a self-review of my code.
  • I have added relevant, automated tests to cover the changes.
  • I have updated documentation to reflect the changes.

Greptile Summary

This PR wires schematic graph state out of the Console Redux slice and into Pluto-owned flux state, with optimistic dispatch going to the new server dispatch endpoint and a channel listener re-applying remote action batches for real-time replication. The Console renderer shrinks from 521 to ~100 lines, delegating nodes/edges/configs to Base.Schematic while keeping only UI state (editable, selected, viewport, legend) in Redux.

  • Pluto queries.ts adds useDispatch with optimistic updates, rollback, and edge-segment augmentation, plus a suite of useSelect* hooks for node and edge renderers.
  • Console Schematic.tsx drops sync, undo, and load-remote logic in favor of Base.Schematic with a resourceKey prop that fetches its own data.
  • New useUpload.ts migrates pre-v6 local schematics to the server via a pendingUpload field in the Redux slice.

Confidence Score: 2/5

Multiple regressions across core user-facing paths make this unsafe to merge as-is.

The import regression means a user who exports a schematic and re-imports it gets a blank canvas. The always-released control string means the edit-lock toggle renders incorrectly for every schematic under any control authority. The viewport reset discards the user saved pan/zoom on every tab open. The ontology loadSchematic change means schematics that previously opened read-only now open in edit mode.

console/src/schematic/Schematic.tsx (control status, viewport, fitViewOnResize all hard-coded), console/src/schematic/services/import.ts (imported content silently discarded), console/src/schematic/services/ontology.tsx (editable flag dropped), console/src/schematic/useUpload.ts (create not awaited), pluto/src/schematic/queries.ts (missing guard in useAddNode, silent data loss when store entry absent)

Important Files Changed

Filename Overview
pluto/src/schematic/queries.ts Adds useDispatch (optimistic + rollback + edge-segment augmentation), multiple useSelect* selectors, useAddNode, and an ACTION_LISTENER for sy_schematic_set. The optimistic dispatch path has a known silent-data-loss window when current is null. Missing null guard for Symbol.REGISTRY in useAddNode (flagged in previous review).
pluto/src/schematic/Schematic.tsx Schematic becomes self-contained: owns nodes/edges via useRetrieve, dispatches actions on changes, handles drops. Several regressions flagged in the previous review round (stale handleClearSelection closure, onDoubleClick double-firing, copy/paste/select-all silently disabled) remain open.
console/src/schematic/Schematic.tsx Shrinks 521 to 100 lines by delegating graph state to Pluto. Multiple regressions remain open from the prior review round: control always released, viewport hardcoded to origin, fitViewOnResize hardcoded to false, legend position not persisted. Ontology-opened schematics now default to edit mode (new finding).
console/src/schematic/useUpload.ts New file: migrates pre-v6 local schematics to the server via pendingUpload. create() is called without await, so clearPendingUpload fires immediately and errors from the server silently drop the migration data (flagged in previous review).
console/src/schematic/services/import.ts void state discards all imported schematic content (nodes, edges, props, legend). Import now creates an empty canvas, a regression flagged in the previous review round.
console/src/schematic/services/ontology.tsx loadSchematic drops editable: false, so schematics opened from the ontology panel now open in edit mode instead of read-only. handleMosaicDrop updated similarly but without that regression.
console/src/schematic/toolbar/Properties.tsx Multi-element operations (rotate, label, color) call dispatchSchematic N times per element, triggering N separate server round-trips. Flagged in previous review round as a partial-failure divergence risk.
pluto/src/schematic/node/Node.tsx New file: per-node renderer using useSelectConfig and useDispatch. Reads its own config and dispatches setConfig on changes. Logic is sound.
console/src/schematic/slice.ts Heavily trimmed: drops nodes/edges/configs/props from Redux slice; retains only UI state (editable, selected, viewport, legend, control, toolbar tabs, pendingUpload). Migrations kept intact.
core/pkg/service/schematic/writer.go Skips DefineRelationship when workspace is uuid.Nil, preventing a spurious ontology edge for workspace-less schematics. Small, targeted, correct.

Comments Outside Diff (3)

  1. console/src/schematic/Schematic.tsx, line 496 (link)

    P1 Viewport hard-coded to origin on every mount — saved pan/zoom position lost

    viewport={{ position: { x: 0, y: 0 }, zoom: 1 }} is hard-coded, so every time the schematic tab is opened or the component remounts, the view resets to origin at 1× zoom regardless of where the user had panned/zoomed. Changes are still persisted to Redux via handleViewportChange, but the initial value is never restored. useSelectViewport already exists in selectors.ts and returns the stored viewport; it just isn't called here.

  2. pluto/src/schematic/queries.ts, line 1098-1116 (link)

    P1 Silent data loss when optimistic dispatch races with missing store entry

    When current is null (schematic not yet loaded in the flux store), the optimistic reduceAll update is skipped and no rollback is pushed, but the network call still fires. After the server writes the action, the ACTION_LISTENER self-deduplicates via changed.sessionKey === client.key and also returns early — so the local store is never updated. The canvas will silently show stale state until the next full useRetrieve cycle. This can happen if useDispatch is called (e.g. from a toolbar interaction) before the initial retrieve has resolved.

  3. pluto/src/schematic/Schematic.tsx, line 771 (link)

    P1 handleClearSelection missing onSelectionChange in its deps array

    handleClearSelection closes over onSelectionChange but the useCallback dependency array is empty ([]). If the parent re-renders with a new onSelectionChange reference (e.g. because a prop changed), the stale callback will be kept — Ctrl+A deselect will call the old handler. Add onSelectionChange to the dependency array.

Reviews (10): Last reviewed commit: "Drop unused eslint-disable directive on ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.88%. Comparing base (3ab7395) to head (6aca3f2).

Files with missing lines Patch % Lines
core/pkg/service/schematic/writer.go 0.00% 1 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (0.00%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@              Coverage Diff              @@
##           sy-3833-2    #2291      +/-   ##
=============================================
+ Coverage      64.67%   64.88%   +0.21%     
=============================================
  Files           2595     2594       -1     
  Lines         112925   112313     -612     
  Branches        8334     8246      -88     
=============================================
- Hits           73029    72875     -154     
+ Misses         33769    33359     -410     
+ Partials        6127     6079      -48     
Flag Coverage Δ
client-py 85.94% <ø> (+0.02%) ⬆️
client-ts 90.30% <ø> (ø)
core 67.43% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread client/ts/src/schematic/actions.ts Outdated
Comment thread pluto/src/schematic/Schematic.tsx
Comment thread pluto/src/schematic/Schematic.tsx Outdated
Comment thread console/src/schematic/Schematic.tsx
Comment thread client/ts/src/schematic/actions.gen.ts Outdated
Comment thread pluto/src/schematic/queries.ts Outdated
Lays the server-side foundation for granular schematic mutations.

Oracle codegen:
- Adds the 'action' grammar production to the oracle DSL alongside fields,
  domains, and field omissions inside struct bodies.
- Adds resolution.Action and analyzer.collectAction. resolveTypeRefs now
  walks action payload field types so cross-references inside action bodies
  resolve correctly to their qualified names.
- New oracle/plugin/go/actions plugin generates a discriminated-union
  codec (Action envelope, Reduce, ReduceAll, NewXxxAction constructors)
  for any struct that declares actions. Optional fields use the same
  pointer-prefix logic as gotypes (skip slice / map / msgpack.EncodedJSON).
- Formatter learns to emit action blocks so 'oracle fmt' / 'oracle sync'
  preserves them.

Schematic schema and runtime:
- schemas/schematic.oracle declares six actions: SetNodePosition, AddNode,
  RemoveNode, SetEdge, RemoveEdge, SetProps. Hand-written Handle methods
  live in core/pkg/service/schematic/actions.go alongside the ScopedAction
  envelope used for cluster broadcast.
- Writer.Dispatch applies a sequence of actions atomically inside a single
  gorp transaction, rejects snapshots, and notifies actionObserver on
  success. SetData remains in place; no callers are migrated yet.
- Service wires actionObserver to a signals translator that publishes
  scoped action sequences to sy_schematic_set / sy_schematic_delete when
  cfg.Signals is non-nil.
- API exposes Dispatch as a new freighter endpoint
  (/api/v1/schematic/dispatch) registered alongside the existing
  SetData endpoint.
Wires the schematic action codec from Part 2 through to the consumer
side: Pluto now owns schematic graph state via flux + dispatch, and
Console renders the new self-contained Schematic component.

## Oracle codegen (TS actions)

- New `oracle/plugin/ts/types/actions.go` mirrors the Go actions plugin
  for TypeScript output: emits `actions.gen.ts` with payload schemas, a
  discriminated union `actionZ`, action constructors, and `reduce` /
  `reduceAll` reducers backed by `structuredClone` (no immer dependency).
- Wired into `(*Plugin).Generate` so `oracle sync` produces TS actions
  alongside the existing Go ones.

## Client SDK

- `client/ts/src/schematic/actions.gen.ts` — generated payload schemas
  and action constructors for the six schematic actions defined in Part 2.
- `actions.ts` — hand-written `handle*` mutators called by the generated
  reducer; semantics mirror the Go `Handle` methods byte-for-byte
  (Add/Remove/SetEdge/SetProps/SetNodePosition).
- `scoped.ts` — `scopedActionZ` schema for decoding `sy_schematic_set`
  signal channel frames.
- `client.schematics.dispatch(key, sessionKey, actions)` calls the
  `/schematic/dispatch` endpoint added in Part 2.

## Pluto

- `queries.ts` gains: `useDispatch` (optimistic + rollback + augment
  edge segments on node moves), `useSelectProps`, `useSelectEdge`,
  `useSelectElementDigests`, `useSelectElementsInfo`,
  `useSelectElementNames`, `useSelectSnapshot`, `useSelectAuthority`,
  `useAddNode`, plus a `sy_schematic_set` listener that re-applies
  remote action batches to the flux store and self-dedups via session key.
- `Schematic.tsx` is now self-contained: no factory, no hook injection.
  It pulls graph state with `useRetrieve` and dispatches actions for
  node/edge changes and drops. Stubs `onUndo` / `onRedo` (deferred).
- New `node/Node.tsx` extracted from the old `create()` factory; reads
  per-node props via `useSelectProps`, dispatches `setProps` on changes.
- `edge/Edge.tsx` reads via `useSelectProps` and dispatches directly;
  `EdgeProps` extension dropped.

## Console

- `console/src/schematic/Schematic.tsx` shrinks 521 → 100 lines. Drops
  `Base.create(hooks)`, `useUndoableDispatch`, `useSyncComponent`,
  `useLoadRemote`, `useAddSymbol`, and the in-component graph-state
  effect handlers. Renders `<Base.Schematic resourceKey={layoutKey}>`
  directly. Re-exports `HAUL_TYPE` from pluto for the channel ontology
  drag-and-drop wiring.

The console schematic slice still carries graph fields (nodes/edges/
props/legend.colors/etc.) for now — those become unread state since the
canvas reads through Pluto. Slice gut + v6 simplification + toolbar
migration are scoped to a follow-up so this PR stays reviewable.

Undo/redo, off-page-reference cross-schematic navigation, and the
deferred-upload (`pendingUpload`) path are not in this PR.
Comment thread pluto/src/schematic/Schematic.tsx
emilbon99 added 3 commits May 2, 2026 12:30
- Add useAutoUpload hook in console/src/schematic/useUpload.ts.
  On schematic mount, if the v6 migration parked a pendingUpload,
  ensure the schematic exists on the server (lazy create against the
  user's active workspace, falling back to no workspace) and clear
  the pending entry.
- Restore navigateToLinkedSchematic / useHandleNodeClickAction in
  console Schematic.tsx, reading off-page-reference props through the
  pluto flux store instead of the deleted slice graph state.
- Drop the useSelectRequiredViewportMode hardcoded shim and the
  matching slice setViewportMode no-op; viewport mode now lives as
  component-local state in Schematic.tsx.
- Drop the useSelectRequired ZERO_STATE-fallback shim from
  selectors.ts; nothing reads it now.
- Switch all Schematic.create call sites that previously spread server
  Schematic into CreateArg to pass { key, name } only — graph state is
  server-owned and nothing else belongs in the layout payload.
Comment thread console/src/schematic/useUpload.ts Outdated
Comment on lines 250 to 252
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 control always resolves to "released" — edit lock and toggle button always wrong

Both branches of the ternary produce the same string literal "released":

const control =
  (doc?.snapshot ?? false) ? "released" : ("released" as Control.Status);

As a result, Diagram.Controls.ToggleEdit disabled={control === "acquired"} is never disabled and ControlToggleButton never renders as acquired, even when a user holds control authority. The original code read state.control from the Redux store. This should read the control status from the Redux selector (useSelectControlStatus) or from the server-returned authority.

# Conflicts:
#	client/ts/src/schematic/actions.gen.ts
#	client/ts/src/schematic/actions.ts
#	client/ts/src/schematic/client.ts
#	client/ts/src/schematic/external.ts
#	console/src/range/overview/Snapshots.tsx
#	console/src/schematic/Schematic.tsx
#	console/src/schematic/services/link.ts
#	console/src/schematic/services/ontology.tsx
#	console/src/workspace/services/ontology.tsx
#	core/pkg/service/schematic/writer_test.go
#	oracle/cmd/plugins.go
The merge from sy-3833-2 deleted actions.go from plugin/ts/types/
(it now lives in plugin/ts/actions/), but the call site in
types.go's Generate() was missed and references an undefined method.
Comment on lines +24 to +27
void state;
// create with an undefined key so we do not have to worry about the key that was from
// the imported data overwriting existing schematics in the cluster
placeLayout(create({ ...state, key: layout?.key, ...layout, type: LAYOUT_TYPE }));
placeLayout(create({ ...layout, key: layout?.key, type: LAYOUT_TYPE }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Imported schematic content silently discarded

void state throws away every field from the exported file (nodes, edges, props, legend, etc.) — the create() call that follows only accepts key and name. A user who exports a schematic and then imports it gets an empty canvas. The previous code spread state into the create call; that mechanism has been removed without a replacement (the pendingUpload / useAutoUpload path only handles the v5→v6 migration and is never set here).

- Drop redundant <div> wrapper in Schematic.tsx; Diagram already provides
  its own container with ref forwarding and onDoubleClick handling.
- Default workspace to uuid.ZERO in useCreate when none is provided so
  client.schematics.create receives a valid string. Matches Go's
  uuid.Nil handling in service/schematic/writer.go.
- Run prettier on the four files CI flagged.
Resolves the merge of sy-3833-2 into sy-3833-3:

- Adopts sy-3833-2's oracle action rename: SetProps -> SetConfig, props
  field -> config, props map -> configs map. Propagates the rename to all
  hand-written code in pluto/src/schematic and the console schematic
  toolbar.
- Adopts sy-3833-2's per-variant Spec architecture for nodes and edges
  (Node.resolveSpec / Edge.resolve). Deletes the legacy
  pluto/src/schematic/edge/Edge.tsx; rewrites Schematic.tsx and Node.tsx
  to dispatch rendering through the registry while still using
  Pluto-owned state (useRetrieve / useDispatch / useSelectConfig /
  useAddNode).
- Keeps sy-3833-3's gutted console slice / selectors / Schematic shell
  but combines it with sy-3833-2's migration helpers (migrateEdge,
  migratePropsToConfigs, migrateLegendColors). The v6 pendingUpload
  field now stores fully-typed v6 data so useUpload can hand it
  straight to client.schematics.create.
onNodeDoubleClick={handleNodeDoubleClick}
fitViewOnResize={state.fitViewOnResize}
setFitViewOnResize={handleSetFitViewOnResize}
fitViewOnResize={false}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 fitViewOnResize is hardcoded to false here, but the handleFitViewOnResizeChange callback still dispatches to the store. The user can toggle "Fit View on Resize" in the toolbar — the store updates — but the diagram always receives false, so the setting has no visible effect. The value should be read from the store, just as editable is read via useSelectFitViewOnResize.

Suggested change
fitViewOnResize={false}
fitViewOnResize={useSelectFitViewOnResize(layoutKey)}

Comment on lines 364 to 388
@@ -320,27 +380,24 @@ const MultiElementProperties = ({
key: K,
value: Schematic.Node.Label.Config[K],
): void => {
selectedConfigs.forEach((cfg, i) => {
if (!("label" in cfg) || cfg.label == null) return;
onChange(selected[i], { label: { ...cfg.label, [key]: value } });
elements.forEach((e) => {
if (e.type !== "node") return;
const config = e.config as NodeProps;
if (config.label == null) return;
onChange(e.key, { label: { ...config.label, [key]: value } });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unbatched multi-element operations trigger N separate server requests

handleRotateIndividual, handleLabelProp, and the color swatch onChange each call the local onChange helper once per selected element, which invokes dispatchSchematic N separate times. Every invocation registers its own optimistic update and makes an independent server round-trip. Contrast with applyNodePositions, which correctly batches all position actions into a single dispatchSchematic call.

The divergence risk: each optimistic update pushes its own rollback entry. If intermediate request i fails, its rollback reverts the flux store to the state after request i−1, while requests i+1 through N may have already succeeded on both the client and the server. The ACTION_LISTENER self-dedup (changed.sessionKey === client.key) prevents the server's acknowledged state from being re-applied locally, so client and server diverge silently on any partial failure.

Comment on lines 181 to +184
placeLayout: Layout.Placer,
) => {
const schematic = await client.schematics.retrieve({ key });
placeLayout(
Schematic.create({ ...Schematic.fromRemote(schematic), editable: false }),
);
placeLayout(Schematic.create({ key: schematic.key, name: schematic.name }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Schematics from ontology now open in edit mode instead of read-only

loadSchematic previously called Schematic.create({ ...Schematic.fromRemote(schematic), editable: false }), which forced read-only mode when a user clicked a schematic in the ontology panel. The new call passes only { key, name }, so the Redux entry is initialized with ZERO_STATE, where editable defaults to true. Users who single-click a schematic in the ontology panel will now land in edit mode, which is the opposite of the previous behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant