refactor(editor): Migrate node state consumers to document store facade (no-changelog)#26367
refactor(editor): Migrate node state consumers to document store facade (no-changelog)#26367
Conversation
| workflowsStore.workflow.nodes, | ||
| (node) => node.type === updateInformation.key, | ||
| ) as INodeUi; | ||
| const nodeType = deps.getNodeType(latestNode.type); |
There was a problem hiding this comment.
setLastNodeParameters uses deps.getNodeType(latestNode.type) before checking latestNode; if findLast(...) returns undefined, latestNode.type will throw and the later if (latestNode) cannot prevent it.
Details
✨ AI Reasoning
The code searches for the latest node matching a type. If none exists, the search returns no node. The current control flow still reads a property from that missing node before the later check, so the later check cannot prevent the failure.
🔧 How do I fix it?
Trace execution paths carefully. Ensure precondition checks happen before using values, validate ranges before checking impossible conditions, and don't check for states that the code has already ruled out.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| } | ||
| } | ||
|
|
||
| function setLastNodeParameters(updateInformation: IUpdateInformation): void { |
There was a problem hiding this comment.
Add an early guard for missing latestNode in setLastNodeParameters (return if latestNode is undefined) to avoid using latestNode.type before confirming it exists and to flatten the control flow.
Details
✨ AI Reasoning
setLastNodeParameters reads latestNode via findLast but uses latestNode.type before confirming latestNode exists, then checks latestNode later. An early return when latestNode is undefined (or otherwise invalid) avoids doing work and prevents a possible runtime error, and flattens the control flow.
🔧 How do I fix it?
Place parameter validation and guard clauses at the function start. Use early returns to reduce nesting levels and improve readability.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| // Apply methods — CRDT entry point in Phase C. Today they just delegate. | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| function applySetNodes(nodes: INodeUi[]) { |
There was a problem hiding this comment.
'applySetNodes' purpose is unclear; rename or document whether it applies a CRDT operation, proxies to workflowsStore, or both.
Details
✨ AI Reasoning
A developer reading useWorkflowDocumentNodes should be able to quickly understand what each public helper does. The newly added function named 'applySetNodes' delegates to workflowsStore.setNodes but the 'apply' prefix is vague without additional documentation. Even with the nearby comment indicating 'CRDT entry point', the function name does not make its role explicit (e.g., applying a CRDT operation vs applying a local change). This ambiguity makes it harder to reason about when to call this function vs other setters.
🔧 How do I fix it?
Use descriptive verb-noun function names, add docstrings explaining the function's purpose, or provide meaningful return type hints.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
fee7b84 to
adafbfa
Compare
| } | ||
|
|
||
| function setLastNodeParameters(updateInformation: IUpdateInformation): void { | ||
| const latestNode = findLast( |
There was a problem hiding this comment.
setLastNodeParameters: findLast(...) can return undefined, but latestNode is cast to INodeUi and latestNode.type is accessed unconditionally, causing a guaranteed runtime error when no node matches.
Details
✨ AI Reasoning
The function looks up the most recent node of a given type and then derives parameters from its node type definition. However, the lookup can legitimately return no node. In that case, the code still dereferences properties on the missing result, so the function would throw before it can safely return.
🔧 How do I fix it?
Trace execution paths carefully. Ensure precondition checks happen before using values, validate ranges before checking impossible conditions, and don't check for states that the code has already ruled out.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| const workflowDocumentTags = useWorkflowDocumentTags(); | ||
| const workflowDocumentPinData = useWorkflowDocumentPinData(); | ||
| const workflowDocumentTimestamps = useWorkflowDocumentTimestamps(); | ||
| const nodeTypesStore = useNodeTypesStore(); |
There was a problem hiding this comment.
The workflow document store now calls useUIStore().markStateDirty and depends on useNodeTypesStore; this mixes document persistence with UI side-effects. Move UI notifications out of the store (emit events or use a decoupled callback).
Details
✨ AI Reasoning
A workflow document store factory was extended to directly import and call UI-side mutation functions (markStateDirty) and to pull node-type resolution from the node types store. This couples persistence/document store logic to UI side effects and node-type resolution, mixing responsibilities: document lifecycle/state management should not directly perform UI mutations or import UI-level stores. The added onStateDirty callback calling a UI mutation and direct dependency on node types increases coupling and makes the store responsible both for document state and triggering UI behavior.
🔧 How do I fix it?
Split classes that handle database, HTTP, and UI concerns into separate, focused classes.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| export function useCanvasOperations() { | ||
| const rootStore = useRootStore(); | ||
| const workflowsStore = useWorkflowsStore(); | ||
| const workflowDocumentStore = computed(() => |
There was a problem hiding this comment.
Added a new workflowDocumentStore computed and many document-store call sites inside this already very large composable; this increases cross-cutting responsibilities and makes the module harder to maintain.
Details
✨ AI Reasoning
The modifications add a new reactive computed (workflowDocumentStore) and replace many direct workflowsStore node lookups with calls into the document store throughout the same large module. This increases the module's surface area and mixes multiple sources of truth (workflowsStore and workflowDocumentStore) within an already large composable, making navigation and reasoning harder. The change centralizes migration logic inside this very large file rather than extracting a focused facade or smaller helpers, which worsens maintainability for a file that already contains a lot of responsibilities (node operations, connection ops, workspace lifecycle, import/export, telemetry, UI interactions).
🔧 How do I fix it?
Split large files into smaller, focused modules. Each file should have a single responsibility.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { | ||
| const nodeIndex = workflowsStore.workflow.nodes.findIndex( | ||
| (node) => node.name === updateInformation.name, | ||
| ); | ||
|
|
||
| if (nodeIndex !== -1) { | ||
| for (const key of Object.keys(updateInformation.properties)) { | ||
| const typedKey = key as keyof INodeUpdatePropertiesInformation['properties']; | ||
| const property = updateInformation.properties[typedKey]; | ||
|
|
||
| const changed = updateNodeAtIndex(nodeIndex, { [key]: property }); | ||
|
|
||
| if (changed) { | ||
| void onStateDirty.trigger(); | ||
| } |
There was a problem hiding this comment.
updateNodeProperties nests the main update logic in if (nodeIndex !== -1) { ... }. Use an early return when the nodeIndex is -1 to reduce nesting (e.g., if (nodeIndex === -1) return;).
Show fix
| function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { | |
| const nodeIndex = workflowsStore.workflow.nodes.findIndex( | |
| (node) => node.name === updateInformation.name, | |
| ); | |
| if (nodeIndex !== -1) { | |
| for (const key of Object.keys(updateInformation.properties)) { | |
| const typedKey = key as keyof INodeUpdatePropertiesInformation['properties']; | |
| const property = updateInformation.properties[typedKey]; | |
| const changed = updateNodeAtIndex(nodeIndex, { [key]: property }); | |
| if (changed) { | |
| void onStateDirty.trigger(); | |
| } | |
| function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { | |
| if (nodeIndex === -1) return; | |
| for (const key of Object.keys(updateInformation.properties)) { | |
| const typedKey = key as keyof INodeUpdatePropertiesInformation['properties']; | |
| const property = updateInformation.properties[typedKey]; | |
| const changed = updateNodeAtIndex(nodeIndex, { [key]: property }); | |
| if (changed) { | |
| void onStateDirty.trigger(); |
Details
✨ AI Reasoning
The function's primary work (iterating over properties and updating nodes) is entirely inside a conditional that checks node existence: if (nodeIndex !== -1) { ... }. This nests the main logic one level deeper than needed. Using an early return when the node is not found (i.e., if (nodeIndex === -1) return;) would remove the extra nesting and make the function's intent clearer. The change was introduced by this PR (new file), so it is a newly introduced opportunity for simplification.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
Bundle ReportChanges will increase total bundle size by 14.27kB (0.03%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: editor-ui-esmAssets Changed:
Files in
Files in
Files in
Files in
Files in
Files in
Files in
Files in
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This comment has been minimized.
This comment has been minimized.
bf27bac to
f51a49f
Compare
| workflowsStore.workflow.nodes, | ||
| (node) => node.type === updateInformation.key, | ||
| ) as INodeUi; | ||
| const nodeType = deps.getNodeType(latestNode.type); |
There was a problem hiding this comment.
setLastNodeParameters dereferences latestNode.type even though findLast can return undefined, so it can crash before the nodeType null-check runs.
Details
✨ AI Reasoning
The code searches for a latest node matching a type key, but does not handle the case where no such node exists. Immediately after the search, it accesses a property on the result without checking it is defined, so the guard that follows cannot prevent a crash.
🔧 How do I fix it?
Trace execution paths carefully. Ensure precondition checks happen before using values, validate ranges before checking impossible conditions, and don't check for states that the code has already ruled out.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
|
|
||
| // --- Composable --- | ||
|
|
||
| export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) { |
There was a problem hiding this comment.
useWorkflowDocumentNodes mixes node state mutations, event orchestration, and metadata bookkeeping. Split persistence (read/write) from event/metadata responsibilities to reduce coupling and improve testability.
Details
✨ AI Reasoning
A new composable was added exposing a document-facing API (useWorkflowDocumentNodes) that both directly mutates the underlying workflowsStore (nodes, workflowObject.setNodes, nodeMetadata) and implements event hooks (onNodesChange/onStateDirty) and higher-level helpers (setLastNodeParameters, resetParametersLastUpdatedAt). This groups direct persistence/manipulation, event orchestration, and metadata bookkeeping into one module. Grouping these distinct responsibilities makes the module harder to test, evolve, and swap parts (e.g., CRDT/replication logic vs local store updates) and increases coupling between state mutation and side-effect coordination. Separating the pure read/write API from event/metadata orchestration would keep single responsibility per module.
🔧 How do I fix it?
Split classes that handle database, HTTP, and UI concerns into separate, focused classes.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| const i18n = useI18n(); | ||
| const canvasStore = useCanvasStore(); | ||
|
|
||
| const workflowDocumentStore = computed(() => |
There was a problem hiding this comment.
Added computed 'workflowDocumentStore' inside a large helpers file. This injects document-store access across many helpers, further coupling concerns in a monolithic file; consider splitting responsibilities.
Details
✨ AI Reasoning
useNodeHelpers.ts is already a long file with many helper responsibilities. Introducing a computed 'workflowDocumentStore' that is then used across multiple helper functions spreads document-store concerns into the helper module, increasing cognitive load and mixing concerns that would be clearer if separated.
🔧 How do I fix it?
Split large files into smaller, focused modules. Each file should have a single responsibility.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| } | ||
| } | ||
|
|
||
| function setNodeValue(updateInformation: IUpdateInformation): void { |
There was a problem hiding this comment.
Function 'setNodeValue' has a vague name—unclear whether it sets parameters, position, or other properties; document intent or use a clearer name (e.g., setNodeProperty or setNodeField) to clarify its contract.
Details
✨ AI Reasoning
A function was added that sets an arbitrary key/value on a node, also triggering side-effects (state dirty event and metadata timestamp update). The function name uses a very generic term ('setNodeValue') that does not make it immediately clear whether it targets parameters, position, or general node properties. Because it performs both data mutation and metadata bookkeeping, its purpose is not self-evident from the name alone and would benefit from a more specific name or documentation so callers understand its contract without reading the implementation.
🔧 How do I fix it?
Use descriptive verb-noun function names, add docstrings explaining the function's purpose, or provide meaningful return type hints.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
This comment has been minimized.
This comment has been minimized.
… (no-changelog) Create the nodes composable for the workflowDocument store as the first step of the Strangler Fig migration (PR 1). The facade implements node-mutation methods directly against workflowsStore, with deps injection (getNodeType callback) and onStateDirty event for decoupled side-effects. ESLint no-restricted-syntax warn rules guard against direct workflowsStore node access during migration. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…o-changelog) Redirect read-only node access (allNodes, getNodeByName, getNodeById, getNodes) from workflowsStore to the workflowDocumentStore facade in 16 consumer files. This is the second step of the Strangler Fig migration toward useWorkflowDocumentNodes owning node state.
…store (no-changelog)
… store (no-changelog)
…ore (no-changelog)
…elog) - Fix variable shadowing in useCanvasOperations.copyNodes (TS2448) - Add missing `computed` import in useInstallNode (TS2304) - Add eslint-disable for createEventHook<void>() in useWorkflowDocumentNodes - Add workflowsStore.allNodes fallback in useWorkflowSetupState and useWorkflowHelpers for backward compatibility during migration - Fix useWorkflowSetupState tests: mock injectWorkflowDocumentStore - Fix useCanvasOperations tests: add synchronous document store mock (async vi.mock factories do not intercept transitive dependency imports)
…tore (no-changelog) Migrate SetupWorkflowCredentialsButton, useChatState, useContextMenuItems, and useWorkflowSetupState to read node state from workflowDocumentStore. Fix test suites to provide real document stores instead of Proxy mocks.
…-changelog) Remove unused imports (computed in useChatState.test, workflowsStore in SetupWorkflowCredentialsButton). Reconnect allNodes as a getter from workflow.nodes in builder.store.test to fix 15 pre-existing test failures caused by mockedStore detaching computed properties.
…odals (no-changelog) Composables called from modals (rendered outside WorkflowLayout) cannot use injectWorkflowDocumentStore() since the provide/inject tree doesn't reach AppModals. Switch useWorkflowExtraction to the computed-from-store pattern (same as useCanvasOperations) so the document store resolves regardless of component tree position. Fix related test setups.
… store access (no-changelog) After resetWorkspace() clears the workflow ID, the computed document store in useCanvasOperations returns undefined, making addNode() a no-op during template import. Move setWorkflowId() before importTemplate() in both openWorkflowTemplate and openWorkflowTemplateFromJSON so nodes are properly added. Also switch useSetupWorkflowCredentialsModalState from inject to computed pattern since it runs inside a teleported modal.
…gelog) Fix test failures caused by migration from workflowsStore to workflowDocumentStore. Add document store mocks to SQLEditor, useReviewChanges, and LogsPanel tests so node resolution and expression evaluation work correctly through the new store layer.
f64629f to
93ef5c7
Compare
| { trackHistory = false } = {}, | ||
| ) { | ||
| const node = workflowsStore.getNodeById(id); | ||
| const node = workflowDocumentStore.value?.findNode(id); |
There was a problem hiding this comment.
Replaced direct node lookup with optional workflowDocumentStore.value?.findNode — if the document store is undefined this update becomes a silent no-op. Consider preserving previous behavior or explicitly handling missing store.
Details
✨ AI Reasoning
An update changed direct node lookup and mutation to use workflowDocumentStore.value?.findNode and optional chaining for set operations. If the computed document store is undefined (workflowId not set), previously-intended updates now silently no-op instead of acting on workflowsStore. This can mask failures and alter behavior during migration.
🔧 How do I fix it?
Remove debugging statements like console.log, debugger, dd(), or logic bypasses like || true. Keep legitimate logging for monitoring and error handling.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
|
|
||
| workflowsStore.removeNodeExecutionDataById(id); | ||
| workflowsStore.removeNodeById(id); | ||
| workflowDocumentStore.value?.removeNodeById(id); |
There was a problem hiding this comment.
Switched removal to workflowDocumentStore.value?.removeNodeById — when document store is undefined this will silently skip deletion. Handle missing store instead of relying on optional chaining.
| workflowDocumentStore.value?.removeNodeById(id); | |
| workflowDocumentStore.value.removeNodeById(id); |
Details
✨ AI Reasoning
The deleteNode implementation was changed to call workflowDocumentStore.value?.removeNodeById. When the computed document store resolves to undefined, node deletion will be skipped silently whereas earlier it always removed the node from workflowsStore. This can leave stale nodes and diverge behavior during migration.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| } | ||
|
|
||
| workflowsStore.addNode(nodeData); | ||
| workflowDocumentStore.value?.addNode(nodeData); |
There was a problem hiding this comment.
Changed node addition to workflowDocumentStore.value?.addNode — this becomes a silent no-op if document store is undefined. Ensure adds fallback or explicitly error instead of optional chaining.
Show fix
| workflowDocumentStore.value?.addNode(nodeData); | |
| if (!workflowDocumentStore.value) { | |
| throw new Error(i18n.baseText('nodeViewV2.showError.failedToCreateNode')); | |
| } | |
| workflowDocumentStore.value.addNode(nodeData); |
Details
✨ AI Reasoning
addNode now calls workflowDocumentStore.value?.addNode. If the computed document store is undefined, adding nodes silently does nothing whereas previous code always added to workflowsStore. This can break node creation flows in contexts lacking a resolved document store.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| normalize: false, | ||
| }); | ||
| return NodeViewUtils.getNewNodePosition( | ||
| workflowDocumentStore.value?.allNodes ?? [], |
There was a problem hiding this comment.
Switched from workflowsStore.allNodes to workflowDocumentStore.value?.allNodes — when the document store is undefined callers get empty lists and may miscompute node positions. Consider explicit fallback or guard.
Details
✨ AI Reasoning
Several node-position and layout helpers now pass workflowDocumentStore.value?.allNodes (or use it) instead of workflowsStore.allNodes. If the document store is undefined, callers receive an empty array, which can cause positioning logic to behave incorrectly without explicit error handling.
🔧 How do I fix it?
Remove debugging statements like console.log, debugger, dd(), or logic bypasses like || true. Keep legitimate logging for monitoring and error handling.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| // Filter the downstream nodes to find candidates that need to be shifted right. | ||
| .filter((name) => { | ||
| const node = workflowsStore.getNodeByName(name); | ||
| const node = workflowDocumentStore.value?.findNodeByName(name); |
There was a problem hiding this comment.
Replaced workflowsStore.getNodeByName with workflowDocumentStore.value?.findNodeByName — when document store is undefined this causes nodes to be ignored silently. Add explicit handling or fallback to workflowsStore.
| const node = workflowDocumentStore.value?.findNodeByName(name); | |
| const node = workflowDocumentStore.value?.findNodeByName(name) ?? workflowsStore.getNodeByName(name); |
Details
✨ AI Reasoning
Functions that collect nodes to move/shift now resolve nodes via workflowDocumentStore.value?.findNodeByName. If the document store is undefined some downstream logic will skip nodes quietly, changing layout adjustments compared to prior behavior.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
| function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { | ||
| const nodeIndex = workflowsStore.workflow.nodes.findIndex( | ||
| (node) => node.name === updateInformation.name, | ||
| ); | ||
|
|
||
| if (nodeIndex !== -1) { | ||
| for (const key of Object.keys(updateInformation.properties)) { | ||
| const typedKey = key as keyof INodeUpdatePropertiesInformation['properties']; | ||
| const property = updateInformation.properties[typedKey]; | ||
|
|
||
| const changed = updateNodeAtIndex(nodeIndex, { [key]: property }); | ||
|
|
||
| if (changed) { | ||
| void onStateDirty.trigger(); | ||
| } |
There was a problem hiding this comment.
updateNodeProperties wraps the main update loop in if (nodeIndex !== -1) { ... }. Use an early guard (return if nodeIndex === -1) to flatten the function and reduce nesting.
Show fix
| function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { | |
| const nodeIndex = workflowsStore.workflow.nodes.findIndex( | |
| (node) => node.name === updateInformation.name, | |
| ); | |
| if (nodeIndex !== -1) { | |
| for (const key of Object.keys(updateInformation.properties)) { | |
| const typedKey = key as keyof INodeUpdatePropertiesInformation['properties']; | |
| const property = updateInformation.properties[typedKey]; | |
| const changed = updateNodeAtIndex(nodeIndex, { [key]: property }); | |
| if (changed) { | |
| void onStateDirty.trigger(); | |
| } | |
| function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { | |
| if (nodeIndex === -1) { | |
| return; | |
| } | |
| for (const key of Object.keys(updateInformation.properties)) { | |
| const typedKey = key as keyof INodeUpdatePropertiesInformation['properties']; | |
| const property = updateInformation.properties[typedKey]; | |
| const changed = updateNodeAtIndex(nodeIndex, { [key]: property }); | |
| if (changed) { | |
| void onStateDirty.trigger(); |
Details
✨ AI Reasoning
updateNodeProperties currently checks nodeIndex !== -1 and then places all update logic inside that block. The main behavior (updating properties) is nested inside the if, which increases indentation and hides the primary path. An early guard (if nodeIndex === -1 return) would flatten the function, improving readability and making the update loop the obvious fall-through behavior. This is a straightforward structural simplification without behavioral change.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
…gelog) Provide WorkflowDocumentStoreKey in tests that render components or composables using injectWorkflowDocumentStore(). Without it, node lookups return undefined causing 44 test failures across 8 files.
…changelog) Provide WorkflowDocumentStoreKey in FocusSidebar.test.ts so findNode resolves correctly. Fix consistent-type-imports lint error in useNodeMention.test.ts by using vi.importActual instead of importOriginal with inline type annotation.
…angelog) Provide WorkflowDocumentStoreKey in FocusPanel.test.ts for findNode. Mock injectWorkflowDocumentStore in useNodeExecution.test.ts and assert on document store's updateNodeProperties instead of workflowState's.
…-changelog) The NDV store resolves workflowDocumentStore from workflowsStore.workflowId. Without a workflow ID, activeNode is always null, causing the assistant floating button to be hidden and offset assertions to fail.
…tests (no-changelog)
e985282 to
ccb039f
Compare
Summary
workflowsStore/workflowStateto theworkflowDocumentStorefacade (Strangler Fig pattern)PRs included (as commits)
useWorkflowDocumentNodesfacade + ESLint guardsuseCanvasOperations.tsreads (50 sites)useCanvasOperations.tswrites (8 sites)WorkflowCanvasnodes param to document storeTest plan
pnpm typecheck✅)pnpm build✅)🤖 Generated with Claude Code