feat: implement visual workflow editor (#247)#1018
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
🧰 Additional context used📓 Path-based instructions (2)web/src/**/*.{ts,tsx}📄 CodeRabbit inference engine (web/CLAUDE.md)
Files:
web/**/*.{ts,tsx}📄 CodeRabbit inference engine (CLAUDE.md)
Files:
🧠 Learnings (6)📚 Learning: 2026-03-27T12:44:29.466ZApplied to files:
📚 Learning: 2026-03-30T10:41:40.176ZApplied to files:
📚 Learning: 2026-04-02T12:19:36.889ZApplied to files:
📚 Learning: 2026-03-30T10:20:08.544ZApplied to files:
📚 Learning: 2026-03-30T10:41:40.176ZApplied to files:
📚 Learning: 2026-04-02T12:21:16.739ZApplied to files:
🔇 Additional comments (10)
WalkthroughAdds design-time visual workflow-definition support across backend and frontend. Backend: new immutable graph models, node/edge and validation enums, semantic validator, YAML export, controller endpoints (CRUD, validate, export), DTOs, persistence protocol extension, SQLite schema and repository with optimistic concurrency and VersionConflictError, and observability event constants. Frontend: Workflow Editor page, React Flow node/edge components, node-config schemas, client YAML generator, TypeScript API endpoints/types, routes and sidebar entry, Zustand editor store with undo/redo, many unit tests and Storybook stories. Suggested labels
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive visual workflow editor system, featuring backend Pydantic models for graph definitions, semantic validation for reachability and cycles, and YAML export capabilities. The implementation includes a SQLite persistence layer and a React-based frontend utilizing React Flow with a custom toolbar and property drawer. Feedback suggests improving testability by replacing lambdas with named functions in model defaults and enhancing robustness in graph traversal by explicitly handling potential missing terminal nodes during reachability checks.
| ) | ||
| created_by: NotBlankStr = Field(description="Creator identity") | ||
| created_at: datetime = Field( | ||
| default_factory=lambda: datetime.now(UTC), |
| description="Creation timestamp (UTC)", | ||
| ) | ||
| updated_at: datetime = Field( | ||
| default_factory=lambda: datetime.now(UTC), |
| start = next(n for n in definition.nodes if n.type == WorkflowNodeType.START) | ||
| end = next(n for n in definition.nodes if n.type == WorkflowNodeType.END) |
There was a problem hiding this comment.
There was a problem hiding this comment.
Pull request overview
Implements a full-stack visual workflow editor feature, including new workflow-definition domain models with validation/YAML export on the backend and a ReactFlow-based editor UI with persistence and live YAML preview on the frontend.
Changes:
- Add backend workflow-definition models, validation, YAML export, persistence (SQLite), and
/workflowsAPI controller. - Add React workflow editor page with custom node/edge components, Zustand store (undo/redo, YAML preview), and workflow API client endpoints.
- Add supporting docs and test coverage for the new backend workflow engine functionality.
Reviewed changes
Copilot reviewed 56 out of 56 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/stores/workflow-editor.ts | Zustand store for editor state, persistence actions, YAML preview, undo/redo. |
| web/src/router/routes.ts | Add workflow route constants. |
| web/src/router/index.tsx | Register lazy-loaded workflow editor route. |
| web/src/pages/WorkflowEditorPage.tsx | Main workflow editor page wiring ReactFlow, toolbar, drawer, preview, persistence actions. |
| web/src/pages/workflow-editor/WorkflowYamlPreview.tsx | Collapsible read-only YAML preview panel. |
| web/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsx | Storybook stories for YAML preview. |
| web/src/pages/workflow-editor/WorkflowToolbar.tsx | Editor toolbar (palette, undo/redo, zoom, validate/export/save). |
| web/src/pages/workflow-editor/WorkflowToolbar.stories.tsx | Storybook stories for toolbar. |
| web/src/pages/workflow-editor/WorkflowNodeDrawer.tsx | Right-side property drawer for node configuration. |
| web/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsx | Storybook stories for node drawer. |
| web/src/pages/workflow-editor/WorkflowEditorSkeleton.tsx | Loading skeleton for editor UI. |
| web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsx | Storybook stories for editor skeleton. |
| web/src/pages/workflow-editor/workflow-to-yaml.ts | Client-side YAML preview generator. |
| web/src/pages/workflow-editor/TerminalNode.stories.tsx | Storybook for Start/End node rendering. |
| web/src/pages/workflow-editor/TaskNode.tsx | Custom Task node component. |
| web/src/pages/workflow-editor/TaskNode.stories.tsx | Storybook stories for Task node. |
| web/src/pages/workflow-editor/StartNode.tsx | Custom Start node component. |
| web/src/pages/workflow-editor/SequentialEdge.tsx | Custom sequential edge renderer. |
| web/src/pages/workflow-editor/ParallelSplitNode.tsx | Custom parallel split node component. |
| web/src/pages/workflow-editor/ParallelNode.stories.tsx | Storybook for parallel nodes. |
| web/src/pages/workflow-editor/ParallelJoinNode.tsx | Custom parallel join node component. |
| web/src/pages/workflow-editor/node-config-schemas.ts | Config field schemas per node type for the drawer. |
| web/src/pages/workflow-editor/EndNode.tsx | Custom End node component. |
| web/src/pages/workflow-editor/ConditionalNode.tsx | Custom conditional node component. |
| web/src/pages/workflow-editor/ConditionalNode.stories.tsx | Storybook for conditional node. |
| web/src/pages/workflow-editor/ConditionalEdge.tsx | Custom conditional edge renderer. |
| web/src/pages/workflow-editor/AgentAssignmentNode.tsx | Custom agent-assignment node component. |
| web/src/pages/workflow-editor/AgentAssignmentNode.stories.tsx | Storybook for agent-assignment node. |
| web/src/components/layout/Sidebar.tsx | Add sidebar navigation entry for workflow editor. |
| web/src/api/types.ts | Add workflow definition API types (nodes/edges/validation). |
| web/src/api/endpoints/workflows.ts | Add /workflows API client functions. |
| web/CLAUDE.md | Update web codebase map to include workflow-editor store. |
| tests/unit/persistence/test_protocol.py | Extend persistence protocol compliance fakes with workflow_definitions repo. |
| tests/unit/persistence/sqlite/test_migrations.py | Update expected tables/indexes for new workflow_definitions schema. |
| tests/unit/observability/test_events.py | Register/discover new workflow_definition observability module. |
| tests/unit/engine/workflow/test_yaml_export.py | Unit tests for backend YAML export behavior. |
| tests/unit/engine/workflow/test_validation.py | Unit tests for backend graph validation. |
| tests/unit/engine/workflow/test_definition.py | Unit tests for backend workflow definition models. |
| tests/unit/api/fakes.py | Add fake workflow definition repository to API test fakes. |
| src/synthorg/persistence/workflow_definition_repo.py | Define WorkflowDefinitionRepository protocol. |
| src/synthorg/persistence/sqlite/workflow_definition_repo.py | SQLite implementation for workflow definition persistence. |
| src/synthorg/persistence/sqlite/schema.sql | Add workflow_definitions table + indexes. |
| src/synthorg/persistence/sqlite/backend.py | Wire workflow_definitions repo into SQLite persistence backend. |
| src/synthorg/persistence/protocol.py | Extend PersistenceBackend protocol with workflow_definitions. |
| src/synthorg/observability/events/workflow_definition.py | Add workflow-definition event constants. |
| src/synthorg/observability/events/persistence.py | Add persistence event constants for workflow definitions. |
| src/synthorg/engine/workflow/yaml_export.py | Backend YAML export implementation. |
| src/synthorg/engine/workflow/validation.py | Backend semantic graph validation implementation. |
| src/synthorg/engine/workflow/definition.py | Backend workflow definition Pydantic models. |
| src/synthorg/core/enums.py | Add WorkflowNodeType / WorkflowEdgeType enums. |
| src/synthorg/api/dto.py | Add workflow definition create/update DTOs. |
| src/synthorg/api/controllers/workflows.py | Add /workflows CRUD/validate/export controller. |
| src/synthorg/api/controllers/init.py | Register WorkflowController. |
| docs/design/page-structure.md | Document new workflow editor route/nav entry. |
| docs/design/engine.md | Document workflow definitions, validation, export, persistence. |
| CLAUDE.md | Update backend repo structure notes for workflow definitions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
web/src/stores/workflow-editor.ts
Outdated
| const current: WorkflowSnapshot = { nodes: [...nodes], edges: [...edges] } | ||
| const yaml = regenerateYaml(snapshot.nodes, snapshot.edges, definition) |
There was a problem hiding this comment.
Undo/redo history uses shallow copies for the “current” snapshot ({ nodes: [...nodes], edges: [...edges] }). Because ReactFlow/Zustand state can reuse node/edge object references across updates, later mutations can leak into the stored redo snapshot. Use structuredClone (or another deep copy strategy) here as well to keep history immutable and deterministic.
web/src/stores/workflow-editor.ts
Outdated
| const current: WorkflowSnapshot = { nodes: [...nodes], edges: [...edges] } | ||
| const yaml = regenerateYaml(snapshot.nodes, snapshot.edges, definition) |
There was a problem hiding this comment.
Redo path has the same shallow-copy issue as undo ({ nodes: [...nodes], edges: [...edges] }), which can allow later edits to mutate redo history. Prefer deep cloning (e.g., structuredClone) when capturing the current snapshot for undo/redo stacks.
| onNodesChange: (changes: NodeChange[]) => { | ||
| set((s) => { | ||
| const newNodes = applyNodeChanges(changes, s.nodes) | ||
| // Only mark dirty for non-selection changes | ||
| const hasMoves = changes.some((c) => c.type === 'position' || c.type === 'remove') | ||
| return { | ||
| nodes: newNodes, | ||
| dirty: s.dirty || hasMoves, | ||
| yamlPreview: hasMoves ? regenerateYaml(newNodes, s.edges, s.definition) : s.yamlPreview, | ||
| } |
There was a problem hiding this comment.
onNodesChange marks the editor dirty for position/remove changes but does not push a snapshot to the undo stack. As a result, node moves and node removals triggered via ReactFlow change events (e.g., keyboard delete) won’t be undoable, which breaks expected undo/redo behavior.
| onEdgesChange: (changes: EdgeChange[]) => { | ||
| set((s) => { | ||
| const newEdges = applyEdgeChanges(changes, s.edges) | ||
| const hasRemoves = changes.some((c) => c.type === 'remove') | ||
| return { | ||
| edges: newEdges, | ||
| dirty: s.dirty || hasRemoves, | ||
| yamlPreview: hasRemoves ? regenerateYaml(s.nodes, newEdges, s.definition) : s.yamlPreview, | ||
| } |
There was a problem hiding this comment.
onEdgesChange updates dirty/YAML on edge removals but doesn’t record undo snapshots. Edge deletions performed via ReactFlow change events won’t be reversible via Undo, even though the toolbar exposes undo/redo.
| const fields = nodeType ? NODE_CONFIG_SCHEMAS[nodeType] : [] | ||
|
|
||
| const handleFieldChange = useCallback( | ||
| (key: string, value: string) => { | ||
| onConfigChange({ ...config, [key]: value }) | ||
| }, | ||
| [config, onConfigChange], |
There was a problem hiding this comment.
The drawer treats every field value as a string and always routes changes through onConfigChange, including the label fields defined for start/end nodes. This means editing “Label” will update config.label but won’t update the actual node label used by the canvas (node.data.label). Also, type="number" inputs will still store string values in config; numeric fields like max_concurrency should be parsed to numbers (or omitted when blank) so preview/export match backend expectations.
| className={cn( | ||
| 'flex h-8 min-w-32 items-center justify-center gap-1.5 rounded-md border border-accent/40 bg-accent/5 px-4', | ||
| data.selected && 'ring-2 ring-accent', | ||
| data.hasError && 'ring-2 ring-danger', | ||
| )} |
There was a problem hiding this comment.
Selection/error styling is tied to data.selected / data.hasError, but ReactFlow selection is provided via NodeProps.selected and is not written into data. Since the store never sets data.selected, selected split/join nodes won’t highlight. Use selected from props for selection styles.
| className={cn( | ||
| 'flex h-8 min-w-32 items-center justify-center gap-1.5 rounded-md border border-accent/40 bg-accent/5 px-4', | ||
| data.selected && 'ring-2 ring-accent', | ||
| data.hasError && 'ring-2 ring-danger', | ||
| )} |
There was a problem hiding this comment.
Selection/error styling relies on data.selected / data.hasError, but ReactFlow selection is provided on NodeProps.selected (not in data). As written, selected join nodes won’t show selection highlight unless data.selected is manually injected. Prefer selected from props for selection styles.
web/src/stores/workflow-editor.ts
Outdated
| function nodeTypeToEdgeType( | ||
| sourceType: string | undefined, | ||
| sourceHandle: string | null | undefined, | ||
| ): string { | ||
| if (sourceType === 'conditional') { | ||
| return sourceHandle === 'true' ? 'conditional' : 'conditional' | ||
| } | ||
| if (sourceType === 'parallel_split') return 'sequential' |
There was a problem hiding this comment.
nodeTypeToEdgeType has a redundant conditional branch (sourceHandle === 'true' ? 'conditional' : 'conditional'). This can be simplified to improve readability and avoid dead-code-looking logic.
| export const useWorkflowEditorStore = create<WorkflowEditorState>()((set, get) => ({ | ||
| definition: null, | ||
| nodes: [], | ||
| edges: [], | ||
| selectedNodeId: null, | ||
| dirty: false, |
There was a problem hiding this comment.
There are extensive Vitest store tests under web/src/__tests__/stores/, but this new workflow-editor Zustand store has no corresponding tests. Adding targeted unit tests for core behaviors (load/create/save optimistic concurrency, undo/redo semantics, YAML preview regeneration on node/edge changes) would help prevent regressions in this fairly stateful feature.
| // Add config fields based on type | ||
| if (node.type === 'task' && config) { | ||
| if (config.title) step.title = config.title | ||
| if (config.task_type) step.task_type = config.task_type | ||
| if (config.priority) step.priority = config.priority | ||
| if (config.complexity) step.complexity = config.complexity | ||
| } else if (node.type === 'conditional' && config?.condition_expression) { | ||
| step.condition = config.condition_expression | ||
| } else if (node.type === 'parallel_split') { | ||
| const branches = (outgoing.get(nodeId) ?? []) | ||
| .filter((e) => e.data?.edgeType === 'parallel_branch') | ||
| .map((e) => e.target) | ||
| if (branches.length > 0) step.branches = branches | ||
| } else if (node.type === 'parallel_join') { | ||
| step.join_strategy = (config?.join_strategy as string) || 'all' | ||
| } else if (node.type === 'agent_assignment' && config) { | ||
| if (config.routing_strategy) step.strategy = config.routing_strategy | ||
| if (config.role_filter) step.role = config.role_filter | ||
| } |
There was a problem hiding this comment.
generateYamlPreview() claims to mirror backend YAML export, but it currently omits several fields that the backend includes: task coordination_topology and embedded agent_assignment fields, agent_name on agent_assignment nodes, and max_concurrency on parallel_split. This will make the live preview diverge from the server export, which is confusing when validating/exporting.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1018 +/- ##
==========================================
- Coverage 91.82% 91.78% -0.05%
==========================================
Files 670 677 +7
Lines 36957 37569 +612
Branches 3666 3740 +74
==========================================
+ Hits 33937 34481 +544
- Misses 2389 2445 +56
- Partials 631 643 +12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 31
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/page-structure.md`:
- Line 301: The controllers/pages list is out of sync: after adding the
/workflows/editor page entry you must also add WorkflowController to the
Controller-to-Page Map and update the controller-count statement near the top to
reflect the new total; locate the Controller-to-Page Map section and insert an
entry mapping WorkflowController to /workflows/editor (or the appropriate page
name), and increment the controller count text at the document header so the
counts and map remain consistent.
In `@src/synthorg/api/controllers/workflows.py`:
- Around line 281-298: The delete_workflow handler always returns 204 even when
no workflow was deleted; change this so that after calling
repo.delete(workflow_id) you check the boolean result and if False raise an
HTTPException(status_code=404) (or use Starlette/FastAPI's NotFound) so clients
get 404 for missing workflows, otherwise keep logging WORKFLOW_DEF_DELETED and
return 204 as before; adjust code in delete_workflow (function name:
delete_workflow, local var: deleted, repo:
state.app_state.persistence.workflow_definitions) to perform this conditional
raise.
- Around line 59-72: Replace the legacy tuple-except usage in the Workflow
controller blocks by switching from "except (ValueError, ValidationError) as
exc:" to the PEP 758 form without parentheses (e.g., "except ValueError,
ValidationError as exc:") in the try/except that validates nodes (the block that
sets updates["nodes"] = tuple(WorkflowNode.model_validate(n) for n in
data.nodes)) and apply the same change to the other two identical handlers
mentioned (the except clauses around the code at the other affected locations
referenced in the review).
- Around line 300-326: The controller method validate_workflow is shadowing the
imported validate_workflow from synthorg.engine.workflow.validation; update the
import to an alias (e.g., import validate_workflow as validate_workflow_def or
validate_definition) and change the call inside the controller (currently result
= validate_workflow(definition)) to use the new alias (e.g., result =
validate_workflow_def(definition)) so the engine function is unambiguously
referenced while keeping the controller method name unchanged.
In `@src/synthorg/api/dto.py`:
- Around line 611-618: Replace the loose dict-typed graph fields with explicit
Pydantic DTOs and use them as the Field types so malformed graphs fail at
parsing: define NodeDTO and EdgeDTO (or similarly named BaseModel classes)
capturing required node properties (id, type, metadata, etc.) and edge
properties (source, target, label, etc.), then change the nodes and edges
annotations from tuple[dict[str, object], ...] to tuple[NodeDTO, ...] and
tuple[EdgeDTO, ...] (and update the other similar occurrences), and add any
necessary Field validators (constrains/validators on ids, required keys, and
length limits) so validation happens at the API boundary instead of deeper
layers.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py`:
- Around line 117-124: The except clauses in workflow_definition_repo.py are
using tuple-style exception grouping (except (sqlite3.Error, aiosqlite.Error) as
exc) which violates the project's PEP 758 style; update each occurrence to the
PEP 758 form (except sqlite3.Error, aiosqlite.Error:), capture the exception
variable appropriately where used (e.g., logger.exception(..., error=str(exc))
and raise QueryError(msg) from exc) and apply this fix for all listed sites
referenced in the file such as inside the save method handling (definition.id),
and the other handlers at the locations corresponding to lines near 151, 170,
222, 232, and 269 so the exception variable name remains available for logging
and re-raising.
In `@tests/unit/api/fakes.py`:
- Around line 572-595: Replace the liberal use of Any in
FakeWorkflowDefinitionRepository with the concrete domain types used across
other fakes: annotate self._definitions as dict[str, WorkflowDefinition]; change
method signatures to use WorkflowDefinition for save(definition:
WorkflowDefinition) -> None, get(definition_id: str) -> WorkflowDefinition |
None, list_definitions(..., workflow_type: WorkflowType | None) ->
tuple[WorkflowDefinition, ...], and delete(definition_id: str) -> bool; ensure
you import or forward-declare the WorkflowDefinition and WorkflowType symbols so
mypy strict mode passes and the repository matches the established pattern.
In `@tests/unit/engine/workflow/test_validation.py`:
- Around line 195-265: The three tests (test_missing_true_branch,
test_missing_false_branch, test_extra_non_conditional_outgoing) repeat the same
setup and assertions; refactor them into a single parametrized test using
pytest.mark.parametrize to supply different edges and expected
ValidationErrorCode values, keep using validate_workflow, _wf, _node, _edge and
WorkflowEdgeType to construct cases, and assert result.valid is False and that
the expected ValidationErrorCode is in {err.code for err in result.errors};
replace the three individual test_* methods with one parametrized test function
(e.g., test_conditional_branch_requirements) enumerating the three
edge/expected_code combos.
In `@tests/unit/persistence/test_protocol.py`:
- Around line 475-477: The test is missing an explicit protocol compliance
assertion for the new workflow_definitions property on _FakeBackend; add the
same protocol assertion used for other repos to verify that
_FakeWorkflowDefinitionRepository implements the WorkflowDefinitionRepository
protocol. Locate the test class where other fake repos are asserted and add an
assertion referencing _FakeBackend.workflow_definitions and
_FakeWorkflowDefinitionRepository against WorkflowDefinitionRepository so the
fake repository is checked for protocol conformance.
- Around line 368-380: Update the fake repository to use the actual protocol
types: replace the generic object annotations in class
_FakeWorkflowDefinitionRepository (methods save, get, list_definitions) with the
real WorkflowDefinition type (and use NotBlankStr already present for get), and
change list_definitions' return annotation from tuple[()] to
tuple[WorkflowDefinition, ...] (and type the workflow_type parameter as the
correct WorkflowType | None if applicable); import the WorkflowDefinition (and
WorkflowType if needed) from the module where the protocol is defined so the
fake matches the protocol signatures exactly while keeping save -> None, get ->
WorkflowDefinition | None, and list_definitions -> tuple[WorkflowDefinition,
...].
In `@web/src/pages/workflow-editor/AgentAssignmentNode.stories.tsx`:
- Around line 30-44: Add three additional Storybook variants alongside Default
to cover selected, error, and auto-strategy states: create stories named
Selected, WithError, and AutoStrategy that call the same Wrapper with a nodes
array (matching the Default node shape: id '1', type 'agent_assignment',
position {x:0,y:0}) but modify the data payload—Selected should include
selected: true in data, WithError should set hasError: true and a minimal/empty
config, and AutoStrategy should set config: { routing_strategy: 'auto' } (no
role_filter). Keep the node id/type/position consistent with the existing
Default story and export each as a StoryObj like Default.
In `@web/src/pages/workflow-editor/AgentAssignmentNode.tsx`:
- Around line 15-18: The role extraction in AgentAssignmentNodeComponent is
over-defensive: remove the redundant "|| undefined" on the role assignment and
just use const role = data.config?.role_filter as string; this relies on the
existing {role && ...} guard to handle missing or empty values and keeps
behavior identical while simplifying the code.
In `@web/src/pages/workflow-editor/ConditionalEdge.tsx`:
- Around line 26-35: The BaseEdge call is using the hardcoded enum
MarkerType.ArrowClosed which in `@xyflow/react` v12 expects a resolved SVG marker
URL string; replace the hardcoded value with the incoming resolved marker from
the edge props by setting markerEnd to props.markerEnd on the BaseEdge component
(keep id={props.id}, path={edgePath}, style as-is). Ensure marker types like
MarkerType.ArrowClosed are defined on your edge objects so React Flow resolves
them into the string that arrives in EdgeProps.markerEnd.
In `@web/src/pages/workflow-editor/ConditionalNode.stories.tsx`:
- Around line 23-26: Add a component reference to the Storybook meta (set
meta.component = ConditionalNode) so docs render correctly, and add two new
StoryObj exports (e.g., Selected and WithError) that render the Wrapper with a
nodes array containing a conditional node demonstrating the selected and
hasError states respectively (set node.data.selected = true for Selected and
node.data.hasError = true for WithError); ensure you import ConditionalNode and
StoryObj/Meta types and keep the existing default story intact.
In `@web/src/pages/workflow-editor/EndNode.tsx`:
- Line 19: The Handle components use incorrect Tailwind important modifier
suffix syntax (e.g., className "bg-border-bright! size-2!"); update the
className on Handle (the JSX element using Position.Top/Position.Bottom) to use
the important prefix form (e.g., "!bg-border-bright !size-2") so Tailwind
applies the ! importance correctly; apply the same fix across all workflow node
components that render Handle (StartNode, EndNode, ParallelSplitNode,
ParallelJoinNode, ConditionalNode, TaskNode, AgentAssignmentNode) to keep
styling consistent.
In `@web/src/pages/workflow-editor/TerminalNode.stories.tsx`:
- Line 11: Story container uses hardcoded pixel inline styles (style={{ width:
200, height: 150 }}) which violates spacing-token rules; replace the inline
style on the div in TerminalNode.stories.tsx with a className that uses Tailwind
utility classes or the project’s token-backed CSS class (e.g., a width/height
utility or design-token class provided by the dashboard design system) so
spacing is driven by tokens rather than fixed pixels; remove the style prop and
apply the appropriate className on the same div to match the intended 200x150
visual size using token-backed utilities.
- Around line 34-35: Extract the inline node arrays used in the story renders
into named constants (e.g., START_NODE, TERMINAL_NODE or NODES_START and
NODES_TERMINAL) at the top of TerminalNode.stories.tsx and replace the inline
objects passed to Wrapper (the nodes prop) with those constants; ensure the
constants are typed appropriately (matching the node type/interface used by the
Wrapper) and used for both occurrences (lines around the start and terminal
stories) to shorten lines and comply with TypeScript line-length rules.
In `@web/src/pages/workflow-editor/workflow-to-yaml.ts`:
- Around line 95-99: The task node conversion omits coordination_topology,
causing the client preview to differ from backend exports; update the logic
where node.type === 'task' (the block that reads config and sets
step.title/task_type/priority/complexity) to also copy
config.coordination_topology into step.coordination_topology when present so the
preview matches the backend's _TASK_CONFIG_KEYS handling.
- Around line 50-51: Move the module-level mutable variable lastSortHadCycle
into the local scope of generateYamlPreview: remove the top-level declaration of
lastSortHadCycle, declare and initialize a local let lastSortHadCycle = false at
the start of generateYamlPreview, and update the places where it’s assigned (the
topological sort code that currently sets lastSortHadCycle) and where it’s read
(the later conditional that checks lastSortHadCycle) to use the new local
variable so concurrent calls don’t share state.
In `@web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsx`:
- Line 13: The story wrapper currently uses an inline style height: 600 on the
div in WorkflowEditorSkeleton.stories.tsx; replace this hardcoded pixel height
with a design-token-backed class or a standard Tailwind spacing utility (e.g., a
dashboard height token class or h-... utility) on that div wrapper so spacing
follows the dashboard design tokens and avoids inline pixels — update the div
(the story wrapper element) to use the appropriate token/Tailwind class instead
of style={{ height: 600 }}.
In `@web/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsx`:
- Around line 6-9: The Storybook meta object named meta is missing the component
property which prevents autodocs; update the meta constant to include component:
WorkflowNodeDrawer (or the exported React component used by the stories) so
Storybook can generate autodocs automatically — locate the meta const in
WorkflowNodeDrawer.stories.tsx and add the component reference alongside title
and parameters.
In `@web/src/pages/workflow-editor/WorkflowNodeDrawer.tsx`:
- Around line 29-34: handleFieldChange is being recreated on every config change
because config is in the dependency array; change it to use a functional update
so the callback doesn't capture a stale config and remove config from deps.
Specifically, update handleFieldChange to call onConfigChange with a functional
updater (using previous config to merge in {[key]: value}) and memoize it with
useCallback that depends only on onConfigChange (or remove useCallback if
referential stability isn't needed). This keeps handleFieldChange stable while
still applying the latest config when updating.
In `@web/src/pages/workflow-editor/WorkflowToolbar.stories.tsx`:
- Around line 30-33: The meta object for the story is missing the component
property which prevents Storybook autodocs; update the meta (the Meta constant
named meta) to set component: WorkflowToolbar and also add a decorator supplying
ReactFlowProvider (e.g., decorators: [(Story) =>
<ReactFlowProvider><Story/></ReactFlowProvider>]) so the WorkflowToolbar
receives required context and Storybook can generate docs/controls.
In `@web/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsx`:
- Around line 18-21: The Storybook meta object (meta) is missing the component
reference which prevents Storybook from generating docs and controls; import the
actual component used in this story (e.g., WorkflowYamlPreview) into
WorkflowYamlPreview.stories.tsx and add component: WorkflowYamlPreview to the
meta object so Storybook can wire up docs/controls correctly (ensure the Meta
type import stays intact).
In `@web/src/pages/workflow-editor/WorkflowYamlPreview.tsx`:
- Around line 19-31: The component WorkflowYamlPreview.tsx currently uses a
hardcoded id "yaml-preview-content" for the preview container and sets
aria-controls on the toggle button to that id, which will collide when the
component is rendered multiple times; change this to generate a unique id per
instance (e.g., via React's useId or a useRef + unique suffix) and use that
identifier for both the aria-controls on the button and the id on the div
(references: the toggle button that sets aria-controls and the div with id
"yaml-preview-content", and the LazyCodeMirrorEditor wrapper). Ensure the
generated id is stable across renders for the lifetime of the component.
In `@web/src/pages/WorkflowEditorPage.tsx`:
- Around line 247-249: The ARIA live region is empty and never updated, so it
doesn't announce editor actions; add a piece of state like announcement via
useState and update it in action handlers (e.g., handleSave, handleValidate,
handleExport or any functions that perform save/validate/export) by calling
setAnnouncement('...') after success/failure, then render that announcement
inside the existing <div className="sr-only"
aria-live="assertive">{announcement}</div>; alternatively, if your toast system
already performs proper ARIA announcements, remove the empty live region
instead.
- Around line 114-119: The handler handleAddNode uses Math.random() causing
non-deterministic placement; change it to compute the new node position
deterministically (e.g., derive an x/y offset from the current node count or an
incremental counter) before calling addNode(type, position). Update
handleAddNode to reference the existing node list or a stable counter (e.g.,
nodes.length or a useRef counter) to compute positions so each added node is
placed predictably; keep the function name handleAddNode and the addNode(type,
position) call unchanged. Ensure the logic uses WorkflowNodeType as before and
remains wrapped in useCallback with the same dependencies (update dependencies
if you read nodes or a counter).
In `@web/src/stores/workflow-editor.ts`:
- Around line 325-348: onNodesChange and onEdgesChange mark dirty and regenerate
yaml for position/remove changes but never record an undo snapshot, so node
drags and ReactFlow-initiated edge removals bypass undo; update both handlers
(onNodesChange, onEdgesChange) to, when hasMoves or hasRemoves is true, call the
editor's existing undo snapshot routine (e.g., pushUndoSnapshot /
createHistorySnapshot) before returning and pass the prior state (s.nodes,
s.edges, s.definition) so the previous layout/edges can be restored; keep the
applyNodeChanges/applyEdgeChanges usage and yamlPreview logic but ensure
snapshot capture happens using the state values from s (or a deep copy) so
drags/removals become undoable.
- Around line 371-386: In the redo handler, avoid shallow copies made with
[...nodes] and [...edges]; replace those with deep clones using structuredClone
when building the current WorkflowSnapshot (current) and when restoring
nodes/edges from the snapshot (i.e., where you set nodes: snapshot.nodes and
edges: snapshot.edges). Update usages in the redo function (redo, current
variable creation, and the set call) to use structuredClone(nodes),
structuredClone(edges), and
structuredClone(snapshot.nodes)/structuredClone(snapshot.edges) so state is
fully deep-copied and immutable.
- Around line 354-369: The undo handler creates a shallow redo snapshot using
current: WorkflowSnapshot = { nodes: [...nodes], edges: [...edges] } which keeps
references to nested node/edge data; replace that shallow copy with a deep clone
using structuredClone so the pushed redo snapshot is independent (e.g., create
current by calling structuredClone({ nodes, edges }) before using
get().redoStack), leaving other logic (regenerateYaml, set, undoStack update)
unchanged and referencing the same functions/variables (undo, redoStack,
WorkflowSnapshot, regenerateYaml, get, set).
- Around line 83-92: The nodeTypeToEdgeType function contains dead logic: the
conditional branch checks sourceHandle but returns 'conditional' for both cases;
either remove the unused sourceHandle parameter and its ternary (if conditional
edges truly share the same style) or implement distinct returns for conditional
handles (e.g., return 'conditional-true' when sourceHandle === 'true' and
'conditional-false' otherwise) so the function's behavior matches the intent;
update the function signature and all callers of nodeTypeToEdgeType accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f053cf6d-4ef0-4dbd-b8a3-22d63db68042
📒 Files selected for processing (56)
CLAUDE.mddocs/design/engine.mddocs/design/page-structure.mdsrc/synthorg/api/controllers/__init__.pysrc/synthorg/api/controllers/workflows.pysrc/synthorg/api/dto.pysrc/synthorg/core/enums.pysrc/synthorg/engine/workflow/definition.pysrc/synthorg/engine/workflow/validation.pysrc/synthorg/engine/workflow/yaml_export.pysrc/synthorg/observability/events/persistence.pysrc/synthorg/observability/events/workflow_definition.pysrc/synthorg/persistence/protocol.pysrc/synthorg/persistence/sqlite/backend.pysrc/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/persistence/workflow_definition_repo.pytests/unit/api/fakes.pytests/unit/engine/workflow/test_definition.pytests/unit/engine/workflow/test_validation.pytests/unit/engine/workflow/test_yaml_export.pytests/unit/observability/test_events.pytests/unit/persistence/sqlite/test_migrations.pytests/unit/persistence/test_protocol.pyweb/CLAUDE.mdweb/src/api/endpoints/workflows.tsweb/src/api/types.tsweb/src/components/layout/Sidebar.tsxweb/src/pages/WorkflowEditorPage.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.stories.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.tsxweb/src/pages/workflow-editor/ConditionalEdge.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.tsxweb/src/pages/workflow-editor/EndNode.tsxweb/src/pages/workflow-editor/ParallelJoinNode.tsxweb/src/pages/workflow-editor/ParallelNode.stories.tsxweb/src/pages/workflow-editor/ParallelSplitNode.tsxweb/src/pages/workflow-editor/SequentialEdge.tsxweb/src/pages/workflow-editor/StartNode.tsxweb/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/pages/workflow-editor/TaskNode.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/WorkflowEditorSkeleton.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.tsxweb/src/pages/workflow-editor/node-config-schemas.tsweb/src/pages/workflow-editor/workflow-to-yaml.tsweb/src/router/index.tsxweb/src/router/routes.tsweb/src/stores/workflow-editor.ts
| try: | ||
| updates["nodes"] = tuple(WorkflowNode.model_validate(n) for n in data.nodes) | ||
| except (ValueError, ValidationError) as exc: | ||
| logger.warning( | ||
| WORKFLOW_DEF_INVALID_REQUEST, | ||
| field="nodes", | ||
| error=str(exc), | ||
| ) | ||
| return Response( | ||
| content=ApiResponse[WorkflowDefinition]( | ||
| error=f"Invalid nodes: {exc}", | ||
| ), | ||
| status_code=422, | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use PEP 758 except syntax (no parentheses).
Per coding guidelines, Python 3.14 requires except A, B: without parentheses.
Proposed fix
- except (ValueError, ValidationError) as exc:
+ except ValueError, ValidationError as exc:Apply the same change to lines 76, 190, and 262.
As per coding guidelines: "Use PEP 758 except syntax: use except A, B: (no parentheses) in Python 3.14; ruff enforces this"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| updates["nodes"] = tuple(WorkflowNode.model_validate(n) for n in data.nodes) | |
| except (ValueError, ValidationError) as exc: | |
| logger.warning( | |
| WORKFLOW_DEF_INVALID_REQUEST, | |
| field="nodes", | |
| error=str(exc), | |
| ) | |
| return Response( | |
| content=ApiResponse[WorkflowDefinition]( | |
| error=f"Invalid nodes: {exc}", | |
| ), | |
| status_code=422, | |
| ) | |
| try: | |
| updates["nodes"] = tuple(WorkflowNode.model_validate(n) for n in data.nodes) | |
| except ValueError, ValidationError as exc: | |
| logger.warning( | |
| WORKFLOW_DEF_INVALID_REQUEST, | |
| field="nodes", | |
| error=str(exc), | |
| ) | |
| return Response( | |
| content=ApiResponse[WorkflowDefinition]( | |
| error=f"Invalid nodes: {exc}", | |
| ), | |
| status_code=422, | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/controllers/workflows.py` around lines 59 - 72, Replace the
legacy tuple-except usage in the Workflow controller blocks by switching from
"except (ValueError, ValidationError) as exc:" to the PEP 758 form without
parentheses (e.g., "except ValueError, ValidationError as exc:") in the
try/except that validates nodes (the block that sets updates["nodes"] =
tuple(WorkflowNode.model_validate(n) for n in data.nodes)) and apply the same
change to the other two identical handlers mentioned (the except clauses around
the code at the other affected locations referenced in the review).
| nodes: tuple[dict[str, object], ...] = Field( | ||
| max_length=500, | ||
| description="Workflow nodes", | ||
| ) | ||
| edges: tuple[dict[str, object], ...] = Field( | ||
| max_length=1000, | ||
| description="Workflow edges", | ||
| ) |
There was a problem hiding this comment.
Harden workflow graph payload validation at the API boundary.
nodes/edges as dict[str, object] accept effectively arbitrary shapes, which can push malformed payload failures into deeper layers. Prefer typed nested DTOs (or strict field validators) here so invalid graph objects fail fast at request parsing.
As per coding guidelines, “Validate at system boundaries in Python (user input, external APIs, config files).”
Also applies to: 640-647
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/api/dto.py` around lines 611 - 618, Replace the loose dict-typed
graph fields with explicit Pydantic DTOs and use them as the Field types so
malformed graphs fail at parsing: define NodeDTO and EdgeDTO (or similarly named
BaseModel classes) capturing required node properties (id, type, metadata, etc.)
and edge properties (source, target, label, etc.), then change the nodes and
edges annotations from tuple[dict[str, object], ...] to tuple[NodeDTO, ...] and
tuple[EdgeDTO, ...] (and update the other similar occurrences), and add any
necessary Field validators (constrains/validators on ids, required keys, and
length limits) so validation happens at the API boundary instead of deeper
layers.
There was a problem hiding this comment.
Actionable comments posted: 15
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/design/page-structure.md`:
- Around line 169-170: Update the node and edge type lists to match the enums:
replace the node list on lines referencing node types so it reads start, end,
task, agent_assignment, conditional, parallel_split, parallel_join (matching
WorkflowNodeType), and update the edge list to read sequential,
conditional_true, conditional_false, parallel_branch (matching
WorkflowEdgeType); ensure the old names notification, parallel_fork, and
parallel_join (for edges) are removed and the new explicit
conditional_true/conditional_false and parallel_branch names are used.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py`:
- Line 236: The SQL builder currently appends a hardcoded " ORDER BY updated_at
DESC LIMIT 10000"; change this to a configurable limit or pagination parameter
by replacing the literal with a parameterized clause (e.g., " ORDER BY
updated_at DESC LIMIT :limit" or "LIMIT ?") and surface a new argument on the
repository method that builds this query (the method that defines the local
variable query and appends that string) such as
list_workflow_definitions/get_workflow_definitions, defaulting to a sensible
value and validating bounds; ensure the method passes the limit value into the
DB call, update any callers to accept the new optional parameter, and add tests
for custom limits and default behavior.
- Around line 77-84: Replace the parenthesized multi-exception clause with the
PEP 758 comma-separated form: change the line "except (ValueError,
ValidationError, json.JSONDecodeError, KeyError) as exc:" to "except ValueError,
ValidationError, json.JSONDecodeError, KeyError as exc:" in the deserialization
error-handling block around the logger.exception call for
PERSISTENCE_WORKFLOW_DEF_DESERIALIZE_FAILED (the block that builds msg =
f\"Failed to deserialize workflow definition {context_id!r}\" and raises
QueryError).
In `@tests/unit/api/controllers/test_workflows.py`:
- Around line 119-121: The test helper _seed directly mutates the repository
internals via repo._definitions which couples tests to implementation; change
_seed to call the repository's public save method (e.g., repo.save(defn) or
equivalent) to persist the definition, or if direct mutation is intentional for
performance, add a clear comment in _seed explaining it's a test-only shortcut
and why it bypasses repo.save. Ensure you reference repo and defn.id usage so
tests still return the expected id.
In `@tests/unit/api/fakes_workflow.py`:
- Around line 13-30: The fake repository currently stores and returns live
WorkflowDefinition instances which allows tests to mutate repository state;
import copy and deep-copy definitions at the persistence boundary: when saving
(in __init__/save ensure you store copy.deepcopy(definition)) and when returning
(in get and list_definitions return copy.deepcopy of stored items or copies of
the tuple), so callers receive independent objects and behavior matches
serialize/deserialize repos; update references to save, get, and
list_definitions accordingly.
In `@tests/unit/engine/workflow/test_validation.py`:
- Around line 10-18: The imports from the same module are duplicated;
consolidate the three separate import blocks into a single statement importing
make_edge, make_node, and make_workflow from tests.unit.engine.workflow.conftest
(use the existing aliases _edge, _node, _wf) so replace the multiple from ...
import blocks with one line that imports make_edge as _edge, make_node as _node,
and make_workflow as _wf to reduce redundancy and improve readability.
In `@tests/unit/persistence/sqlite/test_workflow_definition_repo.py`:
- Around line 147-168: Add a unit test that verifies
SQLiteWorkflowDefinitionRepository.save enforces optimistic concurrency by
rejecting an update whose incoming version is not exactly one greater than the
stored version: create an initial definition via _make_definition(version=1) and
await repo.save(...), then attempt to save a second definition for the same id
with a non-sequential version (e.g., version=3) and assert the repository does
not apply the update (repo.get("wf-001") still returns the original values and
version remains 1) or that save raises the expected rejection behavior; place
this as a new test function (e.g., test_save_rejects_on_version_mismatch)
alongside test_save_upsert_updates_existing.
In `@web/src/pages/workflow-editor/ConditionalNode.stories.tsx`:
- Line 10: The story wrapper in ConditionalNode.stories.tsx currently uses
inline pixel dimensions on the outer <div> (style={{ width: 300, height: 250
}}); replace that inline styling with Tailwind utility classes (e.g., use
appropriate w- and h- classes or design-token-based class names) so pixel
spacing isn’t hardcoded. Update the story’s wrapper element where the inline
style is applied (the outer div in the ConditionalNode story) to use
project-approved Tailwind/design-token classes consistent with other
web/src/**/*.tsx components.
In `@web/src/pages/workflow-editor/ParallelJoinNode.tsx`:
- Around line 29-42: The ParallelJoinNode currently hardcodes two incoming
handles (Handle elements with ids "branch-0" and "branch-1"), which prevents
representing 2+ branches; update ParallelJoinNode to render handles dynamically
by iterating over the node's incoming branches/count (use the prop or workflow
model that lists incoming branch ids), creating a Handle per branch with a
unique id like `branch-${i}` and computing the Position.Top `style.left` as
evenly spaced percentages (e.g., (i+1)/(n+1)*100%). Ensure each generated Handle
keeps the same type/position/className props so the canvas matches the persisted
model.
In `@web/src/pages/workflow-editor/ParallelSplitNode.tsx`:
- Around line 31-45: The ParallelSplitNode currently hardcodes two source
Handles ("branch-0" and "branch-1"), which enforces a binary split; update the
component (ParallelSplitNode) to render N handles dynamically by iterating over
the node's branch data (e.g., props.data.branches or node.data.outputs) and
creating a Handle for each branch with id `branch-{i}` and position computed as
an evenly spaced left percentage ((i+1)/(count+1) * 100%) so additional branches
produce unique ids and non-overlapping positions; ensure each Handle uses
type="source" and Position.Bottom and retains existing classes/styles.
In `@web/src/pages/workflow-editor/WorkflowNodeDrawer.tsx`:
- Around line 29-33: The handleFieldChange callback currently writes
Number(value) directly which can produce NaN; change it to parse the number and
only persist when finite: in handleFieldChange, when fieldType === 'number'
compute const num = Number(value) and use Number.isFinite(num) ? num : keep the
existing value from config[key] (or bail out without calling onConfigChange) so
NaN is never written to config; update the onConfigChange call to use the
guarded parsed value. Reference: handleFieldChange, config, onConfigChange.
In `@web/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsx`:
- Around line 18-22: Update the generic typing so TypeScript infers props from
the component: change the meta declaration to use Meta<typeof
WorkflowYamlPreview> and any story objects to use StoryObj<typeof
WorkflowYamlPreview>, then export the meta and stories with those types (replace
plain Meta and StoryObj usages) so props, args and controls are strongly typed
for WorkflowYamlPreview.
In `@web/src/pages/workflow-editor/WorkflowYamlPreview.tsx`:
- Around line 23-27: The chevron icons are inverted: when the panel is collapsed
(variable collapsed) the UI shows <ChevronUp> but should show <ChevronDown> and
vice versa when expanded; update the JSX in WorkflowYamlPreview (the conditional
rendering that currently uses ChevronUp/ChevronDown) to swap the two components
so collapsed renders ChevronDown and expanded renders ChevronUp, keeping the
existing aria/label text intact.
In `@web/src/stores/workflow-editor.ts`:
- Around line 188-218: The saveDefinition flow clears validationResult after a
successful save which can leave validation stale; after updateWorkflow returns
(in saveDefinition) re-run the store's validation routine (e.g., call the
existing validateDefinition/validateWorkflow function or validation API) using
the updatedDef, and set validationResult to that returned result instead of null
(or set to null only if no validator exists), so users get up-to-date validation
immediately after saving.
- Around line 370-372: In the redo function, avoid assigning snapshot.nodes and
snapshot.edges directly—deep clone the restored snapshot before calling set to
prevent later mutations from corrupting undo history; mirror the pattern used
earlier where you deep-clone state before pushing to redo/undo (i.e., create
clones of snapshot.nodes and snapshot.edges and pass those clones into set
within the redo function).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0da618e8-aafd-4bdb-bd3d-32516b9fbfb5
📒 Files selected for processing (33)
docs/design/engine.mddocs/design/page-structure.mdsrc/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/persistence/sqlite/workflow_definition_repo.pytests/unit/api/controllers/test_workflows.pytests/unit/api/fakes.pytests/unit/api/fakes_workflow.pytests/unit/engine/workflow/conftest.pytests/unit/engine/workflow/test_definition.pytests/unit/engine/workflow/test_validation.pytests/unit/engine/workflow/test_yaml_export.pytests/unit/persistence/sqlite/test_workflow_definition_repo.pytests/unit/persistence/test_protocol.pyweb/src/pages/WorkflowEditorPage.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.tsxweb/src/pages/workflow-editor/ParallelJoinNode.tsxweb/src/pages/workflow-editor/ParallelSplitNode.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/TaskNode.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.tsxweb/src/pages/workflow-editor/workflow-to-yaml.tsweb/src/stores/workflow-editor.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Sandbox
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (6)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors in React and TypeScript files
NEVER hardcode hex color values or rgba() with hardcoded values in.tsx/.tsfiles -- use design token variables instead
Do NOT recreate status dots inline -- use<StatusBadge>component instead
Do NOT build card-with-header layouts from scratch -- use<SectionCard>component instead
Do NOT create metric displays withtext-metric font-boldinline -- use<MetricCard>component instead
Do NOT render initials circles manually -- use<Avatar>component instead
Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component instead
Do NOT usergba()with hardcoded values -- use design token variables instead
Do NOT hardcode Framer Motion transition durations -- use presets from@/lib/motioninstead
web/src/**/*.{ts,tsx}: Always reuse existing components from web/src/components/ui/ before creating new ones in the web dashboard
Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web dashboard code; use design tokens and@/lib/motionpresets instead
TypeScript line length in web dashboard must be concise; enforce via ESLint
React 19 dashboard uses TypeScript 6.0+, design tokens, Radix UI, Tailwind CSS 4, Zustand,@tanstack/react-query,@xyflow/react,@dagrejs/dagre, d3-force,@dnd-kit, Recharts, Framer Motion, cmdk, js-yaml, Axios, Lucide React,@fontsource-variablefonts, CodeMirror 6, Storybook 10, MSW, Vitest,@testing-library/react, fast-check, ESLint, and Playwright
Files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.tsxweb/src/pages/workflow-editor/ParallelSplitNode.tsxweb/src/pages/workflow-editor/ConditionalNode.tsxweb/src/pages/workflow-editor/TaskNode.tsxweb/src/pages/workflow-editor/ConditionalEdge.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/ParallelJoinNode.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.tsxweb/src/pages/workflow-editor/workflow-to-yaml.tsweb/src/pages/WorkflowEditorPage.tsxweb/src/stores/workflow-editor.ts
web/**/*.stories.tsx
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/**/*.stories.tsx: Import fromstorybook/test(not@storybook/test),storybook/actions(not@storybook/addon-actions), and usedefineMain/definePreviewtype-safe config in Storybook 10
In Storybook 10 stories, useparameters.backgrounds.options(object keyed by name) +initialGlobals.backgrounds.valuefor background configuration (replaces olddefault+valuesarray)
Files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Nofrom __future__ import annotationsin Python code; Python 3.14 has PEP 649 native lazy annotations
Use PEP 758 except syntax: useexcept A, B:(no parentheses) in Python 3.14; ruff enforces this
All public functions in Python must have type hints; mypy strict mode enforced
Use Google-style docstrings on public classes and functions in Python; enforced by ruff D rules
Create new objects and never mutate existing ones; for non-Pydantic internal collections (registries, BaseTool), use copy.deepcopy() at construction + MappingProxyType wrapping for read-only enforcement
For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence)
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves
Use Pydantic v2 (BaseModel, model_validator, computed_field, ConfigDict); use allow_inf_nan=False in all ConfigDict declarations to reject NaN/Inf in numeric fields at validation time
Use@computed_fieldfor derived values instead of storing + validating redundant fields in Pydantic models (e.g. TokenUsage.total_tokens)
Use NotBlankStr from core.types for all identifier/name fields in Python (including optional and tuple variants) instead of manual whitespace validators
Prefer asyncio.TaskGroup for fan-out/fan-in parallel operations in Python (e.g. multiple tool invocations, parallel agent calls); prefer structured concurrency over bare create_task
Python line length must not exceed 88 characters; enforced by ruff
Python functions must be under 50 lines; files must be under 800 lines
Handle errors explicitly in Python; never silently swallow exceptions
Validate at system boundaries in Python (user input, external APIs, config files)
Files:
tests/unit/api/fakes.pytests/unit/persistence/test_protocol.pytests/unit/api/fakes_workflow.pytests/unit/engine/workflow/test_validation.pytests/unit/engine/workflow/test_yaml_export.pytests/unit/persistence/sqlite/test_workflow_definition_repo.pytests/unit/api/controllers/test_workflows.pytests/unit/engine/workflow/test_definition.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.pytests/unit/engine/workflow/conftest.pysrc/synthorg/api/controllers/workflows.py
tests/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.py: All Python test files must use@pytest.mark.unit,@pytest.mark.integration,@pytest.mark.e2e, or@pytest.mark.slowmarkers
Python tests must maintain 80% minimum code coverage (enforced in CI)
Prefer@pytest.mark.parametrizefor testing similar cases in Python
Use test-provider, test-small-001, etc. in Python tests instead of real vendor names
Property-based testing in Python uses Hypothesis (@given+@settings); profiles: ci (50 examples, default) and dev (1000 examples), controlled via HYPOTHESIS_PROFILE env var
Never skip, dismiss, or ignore flaky Python tests; always fix them fully and fundamentally; for timing-sensitive tests, mock time.monotonic() and asyncio.sleep() to make them deterministic instead of widening timing margins
For Python tasks that must block indefinitely until cancelled (e.g. simulating a slow provider or stubborn coroutine), use asyncio.Event().wait() instead of asyncio.sleep(large_number) -- it is cancellation-safe and carries no timing assumptions
Files:
tests/unit/api/fakes.pytests/unit/persistence/test_protocol.pytests/unit/api/fakes_workflow.pytests/unit/engine/workflow/test_validation.pytests/unit/engine/workflow/test_yaml_export.pytests/unit/persistence/sqlite/test_workflow_definition_repo.pytests/unit/api/controllers/test_workflows.pytests/unit/engine/workflow/test_definition.pytests/unit/engine/workflow/conftest.py
docs/**/*.md
📄 CodeRabbit inference engine (CLAUDE.md)
Documentation files in docs/ are Markdown, built with Zensical, configured in mkdocs.yml; design spec in docs/design/ (12 pages), Architecture in docs/architecture/, Roadmap in docs/roadmap/
Files:
docs/design/engine.mddocs/design/page-structure.md
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/synthorg/**/*.py: Every Python module with business logic must have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__)
Never useimport logging/logging.getLogger()/print()in Python application code; exceptions are observability/setup.py, observability/sinks.py, observability/syslog_handler.py, and observability/http_handler.py
Python logger variable name must always belogger(not_logger, notlog)
Use event name constants from domain-specific modules under synthorg.observability.events (e.g., API_REQUEST_STARTED from events.api, TOOL_INVOKE_START from events.tool); import directly:from synthorg.observability.events.<domain> import EVENT_CONSTANT
Use structured logging with kwargs in Python: alwayslogger.info(EVENT, key=value)-- neverlogger.info('msg %s', val)
All error paths in Python must log at WARNING or ERROR with context before raising
All state transitions in Python must log at INFO level
Use DEBUG logging level in Python for object creation, internal flow, entry/exit of key functions
Pure data models, enums, and re-exports in Python do NOT need logging
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned Python code, docstrings, comments, tests, or config examples; use generic names: example-provider, example-large-001, example-medium-001, example-small-001, large/medium/small as aliases
Files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
🧠 Learnings (58)
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Create a `.stories.tsx` file alongside every new shared component in `web/src/components/ui/` with all states (default, hover, loading, error, empty)
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsxweb/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/**/*.stories.tsx : Import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`), and use `defineMain`/`definePreview` type-safe config in Storybook 10
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/**/*.stories.tsx : In Storybook 10 stories, use `parameters.backgrounds.options` (object keyed by name) + `initialGlobals.backgrounds.value` for background configuration (replaces old `default` + `values` array)
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/.storybook/main.ts : Use type-safe Storybook 10 config with `defineMain` from `storybook/react-vite/node` and `definePreview` from `storybook/react-vite`, and include an explicit `framework` field
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : NEVER hardcode pixel values for layout spacing -- use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing classes
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsxweb/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-04-02T18:54:07.757Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T18:54:07.757Z
Learning: Applies to web/src/**/*.{ts,tsx} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web dashboard code; use design tokens and `@/lib/motion` presets instead
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to web/src/**/*.{ts,tsx} : NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and `@/lib/motion` presets
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/components/ui/*.{ts,tsx} : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use React 19, TypeScript 6.0+, and design system tokens from shadcn/ui + Tailwind CSS 4 + Radix UI in web dashboard
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/TaskNode.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/**/*.{ts,tsx} : NEVER hardcode hex color values or rgba() with hardcoded values in `.tsx`/`.ts` files -- use design token variables instead
Applied to files:
web/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/engine/**/*.py : Engine package (engine/): agent orchestration, parallel execution, task decomposition, routing, TaskEngine (centralized single-writer), task lifecycle/recovery/shutdown, workspace isolation, coordination (4 dispatchers: SAS/centralized/decentralized/context-dependent, wave execution), approval gates (escalation detection, context parking/resume), stagnation detection (ToolRepetitionDetector, corrective prompt injection), AgentRuntimeState (execution status), context budget management, conversation compaction (oldest-turns summarizer)
Applied to files:
tests/unit/persistence/test_protocol.pydocs/design/engine.md
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to src/synthorg/hr/**/*.py : HR engine must provide: hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, trend detection), promotion/demotion
Applied to files:
tests/unit/persistence/test_protocol.py
📚 Learning: 2026-03-19T07:13:44.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:13:44.964Z
Learning: Applies to src/synthorg/hr/**/*.py : HR package (hr/): hiring, firing, onboarding, offboarding, agent registry, performance tracking (task metrics, collaboration scoring, LLM calibration, collaboration overrides, trend detection), promotion/demotion (criteria evaluation, approval strategies, model mapping)
Applied to files:
tests/unit/persistence/test_protocol.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/**/*.py : Package structure: src/synthorg/ organized as: api/ (REST+WebSocket, Litestar), auth/ (auth subpackage), backup/ (scheduled/manual backups), budget/ (cost tracking, CFO), cli/ (superseded by Go CLI), communication/ (message bus, meetings), config/ (YAML loading), core/ (domain models, resilience config), engine/ (orchestration, task state, coordination, approval gates, stagnation detection, context budget, compaction), hr/ (hiring, performance, promotion), memory/ (pluggable backend, Mem0, retrieval, consolidation), persistence/ (operational data, SQLite, settings), observability/ (logging, correlation, sinks), providers/ (LLM abstraction, LiteLLM, auth types, presets, runtime CRUD), settings/ (runtime-editable, typed definitions, encryption, config bridge), security/ (SecOps, rule engine, output scanning, progressive trust, autonomy levels), templates/ (company templates, personalities), tools/ (registry, built-in tools, git, sandbox, code_runner, MCP...
Applied to files:
tests/unit/persistence/test_protocol.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Engine: Agent orchestration, execution loops, parallel execution, task decomposition, routing, task assignment, centralized single-writer task state engine (TaskEngine), task lifecycle, recovery, shutdown, workspace isolation, coordination (multi-agent pipeline: TopologyDispatcher protocol, 4 dispatchers — SAS/centralized/decentralized/context-dependent, wave execution, workspace lifecycle integration, CoordinationSectionConfig company config bridge, build_coordinator factory), coordination error classification, prompt policy validation, checkpoint recovery (checkpoint/, per-turn persistence, heartbeat detection, CheckpointRecoveryStrategy), approval gate (escalation detection, context parking/resume, EscalationInfo/ResumePayload models), stagnation detection (stagnation/, StagnationDetector protocol, ToolRepetitionDetector, dual-signal analysis, corrective prompt injection), agent runtime state (AgentRuntimeState, lightweight per-agent execution status for dashboard queries and recove...
Applied to files:
tests/unit/persistence/test_protocol.pydocs/design/engine.md
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Import `cn` from `@/lib/utils` for conditional class merging in React components
Applied to files:
web/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.tsxweb/src/pages/workflow-editor/ConditionalEdge.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : ALWAYS reuse existing components from `web/src/components/ui/` before creating new ones (StatusBadge, MetricCard, Sparkline, SectionCard, AgentCard, DeptHealthBar, ProgressGauge, StatPill, Avatar, Button, Toast, Skeleton variants, EmptyState, ErrorBoundary, ConfirmDialog, CommandPalette, InlineEdit, AnimatedPresence, StaggerGroup/Item, Drawer, InputField, SelectField, SliderField, ToggleField, TaskStatusIndicator, PriorityBadge, ProviderHealthBadge, TokenUsageBar, CodeMirrorEditor, SegmentedControl, ThemeToggle, LiveRegion, MobileUnsupportedOverlay, LazyCodeMirrorEditor, TagInput, MetadataGrid, ProjectStatusBadge, ContentTypeBadge)
Applied to files:
web/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.tsxweb/src/pages/workflow-editor/TaskNode.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/.storybook/preview.tsx : In Storybook 10 stories, use `parameters.a11y.test: 'error' | 'todo' | 'off'` for a11y testing (replaces old `.element` and `.manual`). Set globally in `preview.tsx` to enforce WCAG compliance on all stories
Applied to files:
web/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsx
📚 Learning: 2026-04-02T18:54:07.757Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T18:54:07.757Z
Learning: Applies to site/**/*.{ts,tsx,astro} : Astro landing page (synthorg.io) uses Astro 6, astrojs/react, React 19, Tailwind CSS 4, js-yaml; React islands for interactive sections
Applied to files:
web/src/pages/workflow-editor/WorkflowYamlPreview.tsx
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/engine/coordination/**/*.py : Task coordination uses multi-agent pipeline with 4 dispatchers (SAS/centralized/decentralized/context-dependent), wave execution, and workspace lifecycle integration.
Applied to files:
docs/design/engine.md
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to tests/**/*.py : Prefer `pytest.mark.parametrize` for testing similar cases.
Applied to files:
tests/unit/engine/workflow/test_validation.py
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to tests/**/*.py : Prefer `pytest.mark.parametrize` for testing similar cases
Applied to files:
tests/unit/engine/workflow/test_validation.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Parametrize: Prefer pytest.mark.parametrize for testing similar cases.
Applied to files:
tests/unit/engine/workflow/test_validation.py
📚 Learning: 2026-04-02T18:54:07.757Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T18:54:07.757Z
Learning: Applies to tests/**/*.py : Prefer pytest.mark.parametrize for testing similar cases in Python
Applied to files:
tests/unit/engine/workflow/test_validation.py
📚 Learning: 2026-03-15T18:28:13.207Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:28:13.207Z
Learning: Applies to tests/**/*.py : Test markers: pytest.mark.unit, pytest.mark.integration, pytest.mark.e2e, pytest.mark.slow. Coverage: 80% minimum (enforced in CI).
Applied to files:
tests/unit/engine/workflow/test_validation.py
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/components/ui/*.{ts,tsx} : Export component props as a TypeScript interface in new shared components
Applied to files:
web/src/pages/workflow-editor/ParallelSplitNode.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.tsx
📚 Learning: 2026-04-02T18:54:07.757Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T18:54:07.757Z
Learning: Applies to web/src/**/*.{ts,tsx} : React 19 dashboard uses TypeScript 6.0+, design tokens, Radix UI, Tailwind CSS 4, Zustand, tanstack/react-query, xyflow/react, dagrejs/dagre, d3-force, dnd-kit, Recharts, Framer Motion, cmdk, js-yaml, Axios, Lucide React, fontsource-variable fonts, CodeMirror 6, Storybook 10, MSW, Vitest, testing-library/react, fast-check, ESLint, and Playwright
Applied to files:
web/src/pages/workflow-editor/TaskNode.tsxweb/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to docs/design/*.md : Update the relevant `docs/design/` page when approved deviations occur to reflect the new reality
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-18T08:23:08.912Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-18T08:23:08.912Z
Learning: When approved deviations occur, update the relevant `docs/design/` page to reflect the new reality.
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-04-02T18:54:07.757Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T18:54:07.757Z
Learning: When approved deviations occur, update the relevant docs/design/ page to reflect the new reality
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-16T06:24:56.341Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T06:24:56.341Z
Learning: Applies to docs/design/**/*.md : Design specification pages in `docs/design/` must be consulted before implementing features (7 pages: index, agents, organization, communication, engine, memory, operations)
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to docs/design/*.md : Design spec pages: 7 pages in `docs/design/` — index, agents, organization, communication, engine, memory, operations
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Web dashboard: see `web/CLAUDE.md` for commands, design system, and component inventory
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to docs/** : Docs source in docs/ (Markdown, built with Zensical); design spec in docs/design/ (7 pages: index, agents, organization, communication, engine, memory, operations)
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-19T07:13:44.964Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:13:44.964Z
Learning: Always read the relevant `docs/design/` page before implementing any feature or planning any issue — DESIGN_SPEC.md is a pointer file linking to 7 design pages (Agents, Organization, Communication, Engine, Memory, Operations)
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Always read the relevant `docs/design/` page before implementing any feature or planning any issue. DESIGN_SPEC.md is a pointer file linking to the 7 design pages (index, agents, organization, communication, engine, memory, operations).
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Documentation source in `docs/` (Markdown, built with Zensical). Design spec in `docs/design/` (7 pages: index, agents, organization, communication, engine, memory, operations). Architecture in `docs/architecture/` (overview, tech-stack, decision log). Roadmap in `docs/roadmap/`. Security in `docs/security.md`. Licensing in `docs/licensing.md`. Reference in `docs/reference/`. REST API reference in `docs/rest-api.md`. Library reference in `docs/api/` (auto-generated from docstrings). Custom templates in `docs/overrides/`. Config in `mkdocs.yml`.
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Settings: Runtime-editable settings persistence (DB > env > YAML > code defaults), typed definitions (9 namespaces), Fernet encryption for sensitive values, config bridge, ConfigResolver (typed composed reads for controllers), validation, registry, change notifications via message bus. Per-namespace setting definitions in definitions/ submodule (api, company, providers, memory, budget, security, coordination, observability, backup).
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-03-15T21:20:09.993Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T21:20:09.993Z
Learning: Applies to web/src/components/** : Vue components organized by feature (agents/, approvals/, budget/, common/, dashboard/, layout/, messages/, org-chart/, tasks/).
Applied to files:
docs/design/page-structure.md
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT create metric displays with `text-metric font-bold` inline -- use `<MetricCard>` component instead
Applied to files:
web/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/**/*.{ts,tsx} : Do NOT use `rgba()` with hardcoded values -- use design token variables instead
Applied to files:
web/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : Use token variables (`var(--so-shadow-card-hover)`, `border-border`, `border-bright`) for shadows and borders instead of hardcoded values
Applied to files:
web/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.876Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.876Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Tailwind semantic classes (`text-foreground`, `bg-card`, `text-accent`, `text-success`, `bg-danger`, etc.) or CSS variables (`var(--so-*)`) for colors in React and TypeScript files
Applied to files:
web/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-04-02T18:54:07.757Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T18:54:07.757Z
Learning: Applies to web/src/**/*.{ts,tsx} : TypeScript line length in web dashboard must be concise; enforce via ESLint
Applied to files:
web/src/pages/workflow-editor/TerminalNode.stories.tsx
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/persistence/**/*.py : Persistence uses pluggable PersistenceBackend protocol. SQLite is the initial backend. Settings use SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-31T14:17:24.182Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:17:24.182Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-02T18:54:07.757Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T18:54:07.757Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) in Python 3.14; ruff enforces this
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/api/controllers/workflows.py
| except (ValueError, ValidationError, json.JSONDecodeError, KeyError) as exc: | ||
| msg = f"Failed to deserialize workflow definition {context_id!r}" | ||
| logger.exception( | ||
| PERSISTENCE_WORKFLOW_DEF_DESERIALIZE_FAILED, | ||
| definition_id=context_id, | ||
| error=str(exc), | ||
| ) | ||
| raise QueryError(msg) from exc |
There was a problem hiding this comment.
Use PEP 758 except syntax (no parentheses).
Per coding guidelines, Python 3.14 requires except A, B, C, D: syntax without parentheses for multiple exception types.
Proposed fix
- except (ValueError, ValidationError, json.JSONDecodeError, KeyError) as exc:
+ except ValueError, ValidationError, json.JSONDecodeError, KeyError as exc:As per coding guidelines: "Use PEP 758 except syntax: use except A, B: (no parentheses) in Python 3.14; ruff enforces this"
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| except (ValueError, ValidationError, json.JSONDecodeError, KeyError) as exc: | |
| msg = f"Failed to deserialize workflow definition {context_id!r}" | |
| logger.exception( | |
| PERSISTENCE_WORKFLOW_DEF_DESERIALIZE_FAILED, | |
| definition_id=context_id, | |
| error=str(exc), | |
| ) | |
| raise QueryError(msg) from exc | |
| except ValueError, ValidationError, json.JSONDecodeError, KeyError as exc: | |
| msg = f"Failed to deserialize workflow definition {context_id!r}" | |
| logger.exception( | |
| PERSISTENCE_WORKFLOW_DEF_DESERIALIZE_FAILED, | |
| definition_id=context_id, | |
| error=str(exc), | |
| ) | |
| raise QueryError(msg) from exc |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py` around lines 77
- 84, Replace the parenthesized multi-exception clause with the PEP 758
comma-separated form: change the line "except (ValueError, ValidationError,
json.JSONDecodeError, KeyError) as exc:" to "except ValueError, ValidationError,
json.JSONDecodeError, KeyError as exc:" in the deserialization error-handling
block around the logger.exception call for
PERSISTENCE_WORKFLOW_DEF_DESERIALIZE_FAILED (the block that builds msg =
f\"Failed to deserialize workflow definition {context_id!r}\" and raises
QueryError).
|
|
||
| if conditions: | ||
| query += " WHERE " + " AND ".join(conditions) | ||
| query += " ORDER BY updated_at DESC LIMIT 10000" |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Hardcoded result limit may need future consideration.
The LIMIT 10000 is hardcoded. While reasonable for initial implementation, consider making this configurable or implementing proper pagination at the repository level if workflow counts grow significantly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py` at line 236, The
SQL builder currently appends a hardcoded " ORDER BY updated_at DESC LIMIT
10000"; change this to a configurable limit or pagination parameter by replacing
the literal with a parameterized clause (e.g., " ORDER BY updated_at DESC LIMIT
:limit" or "LIMIT ?") and surface a new argument on the repository method that
builds this query (the method that defines the local variable query and appends
that string) such as list_workflow_definitions/get_workflow_definitions,
defaulting to a sensible value and validating bounds; ensure the method passes
the limit value into the DB call, update any callers to accept the new optional
parameter, and add tests for custom limits and default behavior.
| repo = client.app.state.app_state.persistence.workflow_definitions | ||
| repo._definitions[defn.id] = defn | ||
| return defn.id |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Test helper accesses private repository attribute.
The _seed function directly modifies repo._definitions, coupling the test to the repository's internal implementation. If the repository internals change, this test will break even if the public API remains stable. Consider using the public save method instead, or document this as an intentional test-only shortcut.
- repo._definitions[defn.id] = defn
+ # Use public API for better decoupling from implementation details
+ import asyncio
+ asyncio.get_event_loop().run_until_complete(repo.save(defn))Alternatively, if direct seeding is intentional for performance, add a comment explaining this choice.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/unit/api/controllers/test_workflows.py` around lines 119 - 121, The
test helper _seed directly mutates the repository internals via
repo._definitions which couples tests to implementation; change _seed to call
the repository's public save method (e.g., repo.save(defn) or equivalent) to
persist the definition, or if direct mutation is intentional for performance,
add a clear comment in _seed explaining it's a test-only shortcut and why it
bypasses repo.save. Ensure you reference repo and defn.id usage so tests still
return the expected id.
| {collapsed ? ( | ||
| <ChevronUp className="size-3.5" aria-hidden="true" /> | ||
| ) : ( | ||
| <ChevronDown className="size-3.5" aria-hidden="true" /> | ||
| )} |
There was a problem hiding this comment.
Swap the disclosure icons.
When the panel is collapsed, the button announces “Expand YAML preview” but shows an up-chevron; when expanded it shows a down-chevron. That inverts the visual state cue.
Suggested fix
- {collapsed ? (
- <ChevronUp className="size-3.5" aria-hidden="true" />
- ) : (
- <ChevronDown className="size-3.5" aria-hidden="true" />
- )}
+ {collapsed ? (
+ <ChevronDown className="size-3.5" aria-hidden="true" />
+ ) : (
+ <ChevronUp className="size-3.5" aria-hidden="true" />
+ )}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {collapsed ? ( | |
| <ChevronUp className="size-3.5" aria-hidden="true" /> | |
| ) : ( | |
| <ChevronDown className="size-3.5" aria-hidden="true" /> | |
| )} | |
| {collapsed ? ( | |
| <ChevronDown className="size-3.5" aria-hidden="true" /> | |
| ) : ( | |
| <ChevronUp className="size-3.5" aria-hidden="true" /> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/workflow-editor/WorkflowYamlPreview.tsx` around lines 23 - 27,
The chevron icons are inverted: when the panel is collapsed (variable collapsed)
the UI shows <ChevronUp> but should show <ChevronDown> and vice versa when
expanded; update the JSX in WorkflowYamlPreview (the conditional rendering that
currently uses ChevronUp/ChevronDown) to swap the two components so collapsed
renders ChevronDown and expanded renders ChevronUp, keeping the existing
aria/label text intact.
| saveDefinition: async () => { | ||
| const { definition, nodes, edges } = get() | ||
| if (!definition) { | ||
| set({ error: 'Cannot save: no workflow loaded' }) | ||
| return | ||
| } | ||
| set({ saving: true, error: null }) | ||
| try { | ||
| const updatedDef = await updateWorkflow(definition.id, { | ||
| nodes: nodes.map((n) => ({ | ||
| id: n.id, | ||
| type: n.type ?? 'task', | ||
| label: (n.data as Record<string, unknown>)?.label ?? n.id, | ||
| position_x: n.position.x, | ||
| position_y: n.position.y, | ||
| config: ((n.data as Record<string, unknown>)?.config as Record<string, unknown>) ?? {}, | ||
| })), | ||
| edges: edges.map((e) => ({ | ||
| id: e.id, | ||
| source_node_id: e.source, | ||
| target_node_id: e.target, | ||
| type: (e.data as Record<string, unknown>)?.edgeType ?? 'sequential', | ||
| label: (e.label as string) ?? null, | ||
| })), | ||
| expected_version: definition.version, | ||
| }) | ||
| set({ definition: updatedDef, saving: false, dirty: false, validationResult: null }) | ||
| } catch (err) { | ||
| set({ saving: false, error: getErrorMessage(err) }) | ||
| } | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider saving unsaved changes before validation.
The saveDefinition method clears validationResult after a successful save, but if a user validates, then makes changes, then saves, the validation result may become stale. The current behavior is reasonable, but consider whether validation should auto-run after save for better UX.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/stores/workflow-editor.ts` around lines 188 - 218, The saveDefinition
flow clears validationResult after a successful save which can leave validation
stale; after updateWorkflow returns (in saveDefinition) re-run the store's
validation routine (e.g., call the existing validateDefinition/validateWorkflow
function or validation API) using the updatedDef, and set validationResult to
that returned result instead of null (or set to null only if no validator
exists), so users get up-to-date validation immediately after saving.
9c220e8 to
6739037
Compare
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (9)
src/synthorg/api/dto.py (1)
611-618:⚠️ Potential issue | 🟠 MajorHarden workflow graph DTOs with typed node/edge models at the API boundary.
Line 611 and Line 640 currently accept arbitrary
dict[str, object]payloads for graph elements. That weakens request-time validation and defers shape/type failures deeper into service logic.♻️ Proposed fix (typed nested DTOs)
+class WorkflowNodeInput(BaseModel): + """Workflow node payload.""" + + model_config = ConfigDict(frozen=True, allow_inf_nan=False) + id: NotBlankStr = Field(max_length=128) + type: NotBlankStr = Field(max_length=64) + config: dict[str, object] = Field(default_factory=dict) + + +class WorkflowEdgeInput(BaseModel): + """Workflow edge payload.""" + + model_config = ConfigDict(frozen=True, allow_inf_nan=False) + source: NotBlankStr = Field(max_length=128) + target: NotBlankStr = Field(max_length=128) + type: NotBlankStr = Field(max_length=64) + config: dict[str, object] = Field(default_factory=dict) + class CreateWorkflowDefinitionRequest(BaseModel): @@ - nodes: tuple[dict[str, object], ...] = Field( + nodes: tuple[WorkflowNodeInput, ...] = Field( max_length=500, description="Workflow nodes", ) - edges: tuple[dict[str, object], ...] = Field( + edges: tuple[WorkflowEdgeInput, ...] = Field( max_length=1000, description="Workflow edges", ) @@ - nodes: tuple[dict[str, object], ...] | None = Field( + nodes: tuple[WorkflowNodeInput, ...] | None = Field( default=None, max_length=500, ) - edges: tuple[dict[str, object], ...] | None = Field( + edges: tuple[WorkflowEdgeInput, ...] | None = Field( default=None, max_length=1000, )As per coding guidelines: "Validate at system boundaries (user input, external APIs, config files)".
Also applies to: 640-647
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/dto.py` around lines 611 - 618, The nodes and edges fields currently accept arbitrary dict[str, object] which bypasses request-time validation; define explicit Pydantic models (e.g., NodeDTO and EdgeDTO) with concrete fields (for example NodeDTO: id: str, type: str, metadata: dict[str, Any] | None; EdgeDTO: id: str, source: str, target: str, type: str, metadata: dict[str, Any] | None) and replace the annotations on nodes and edges to tuple[NodeDTO, ...] and tuple[EdgeDTO, ...]; keep the existing Field(...) metadata (description and max length) or use conlist/typing constraints if you want strict length validation, and update any imports so DTO parsing/validation happens at the API boundary (referencing nodes and edges in src/synthorg/api/dto.py).web/src/pages/workflow-editor/EndNode.tsx (1)
19-19:⚠️ Potential issue | 🟡 MinorFix invalid Tailwind important modifier syntax on
Handle.At Line 19,
bg-border-bright! size-2!should use prefix form (!bg-border-bright !size-2), otherwise the important modifier may not be applied.Suggested fix
- <Handle type="target" position={Position.Top} className="bg-border-bright! size-2!" /> + <Handle + type="target" + position={Position.Top} + className="!bg-border-bright !size-2" + />#!/bin/bash # Verify invalid Tailwind "suffix !" usage in workflow-editor TSX files. rg -nP --type=tsx 'className=.*\b[a-z0-9:-]+!\b' web/src/pages/workflow-editor # Expected after fix: no matches for utilities ending with "!"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/EndNode.tsx` at line 19, The Tailwind important modifier is used with the suffix form incorrectly on the Handle component in EndNode.tsx; update the className on the Handle (the JSX <Handle ... />) to use the prefix form for important utilities (e.g. replace any "bg-border-bright! size-2!" tokens with "!bg-border-bright !size-2") so the important flag is applied correctly and ensure spacing between utilities.web/src/pages/workflow-editor/WorkflowYamlPreview.tsx (1)
23-27:⚠️ Potential issue | 🟡 MinorSwap disclosure icons to match collapsed/expanded state.
At Line 23-27, the icon direction is inverted relative to the current state/ARIA label (collapsed should show down-chevron; expanded should show up-chevron).
Suggested fix
- {collapsed ? ( - <ChevronUp className="size-3.5" aria-hidden="true" /> - ) : ( - <ChevronDown className="size-3.5" aria-hidden="true" /> - )} + {collapsed ? ( + <ChevronDown className="size-3.5" aria-hidden="true" /> + ) : ( + <ChevronUp className="size-3.5" aria-hidden="true" /> + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/WorkflowYamlPreview.tsx` around lines 23 - 27, The disclosure icons are inverted: change the JSX in the conditional using the collapsed boolean so that when collapsed is true it renders <ChevronDown ... /> and when collapsed is false (expanded) it renders <ChevronUp ... />; update the conditional around the collapsed variable (the JSX that currently renders ChevronUp/ChevronDown) to swap the two icon components so the visual chevron matches the collapsed/expanded state (keep existing props like className and aria-hidden).web/src/pages/workflow-editor/AgentAssignmentNode.stories.tsx (1)
10-10:⚠️ Potential issue | 🟡 MinorReplace hardcoded pixel dimensions with utility classes.
At Line 10, inline
style={{ width: 300, height: 200 }}violates the dashboard styling rule against hardcoded pixel spacing.As per coding guidelines: "Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets."Suggested fix
- <div style={{ width: 300, height: 200 }}> + <div className="h-52 w-80">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/AgentAssignmentNode.stories.tsx` at line 10, The div in AgentAssignmentNode.stories.tsx uses an inline style prop style={{ width: 300, height: 200 }} which hardcodes pixel spacing; remove the style prop and replace it with the project's design-token-based utility classes or spacing tokens (e.g., a className that uses the design system/Tailwind utility or CSS variable-based token for width/height) so sizing comes from approved tokens rather than raw pixels; update the JSX that renders the div in AgentAssignmentNode.stories.tsx accordingly and ensure no other inline pixel spacing remains.web/src/pages/workflow-editor/ParallelSplitNode.tsx (1)
31-45:⚠️ Potential issue | 🟠 MajorDon't cap parallel fan-out at two handle slots.
This node only exposes
branch-0andbranch-1, so any third+ outgoing edge has to reuse one of those two handle ids/positions. That makes >2-way splits ambiguous on the canvas even though the workflow model supports larger parallel fan-out. Please render the bottom handles from explicit branch metadata/count instead of hardcoding two slots.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/ParallelSplitNode.tsx` around lines 31 - 45, The component currently hardcodes two bottom source Handles ("branch-0" and "branch-1") using Position.Bottom which prevents >2 outgoing edges; change ParallelSplitNode to iterate over the node's branch metadata/count (e.g., the branches array or node.data.branches) and render a Handle per branch, generating id `branch-${i}` and className/position using Position.Bottom, and compute style.left dynamically (even spacing, e.g. (i+1)/(count+1) * 100%) so each outgoing branch gets its own unique handle instead of reusing branch-0/branch-1.web/src/pages/workflow-editor/ParallelNode.stories.tsx (1)
11-11:⚠️ Potential issue | 🟡 MinorUse token-backed sizing for the Storybook canvas.
Line 11 hardcodes the viewport size with inline px values. Please switch this wrapper to Tailwind/design-token sizing so this story stays consistent with the dashboard spacing rules. As per coding guidelines, “Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and
@/lib/motionpresets.”🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/ParallelNode.stories.tsx` at line 11, The wrapper div in ParallelNode.stories.tsx uses inline px sizing (style={{ width: 300, height: 200 }}); remove the hardcoded inline style and replace it with token-backed sizing via Tailwind/design tokens (use a className that references the project spacing tokens or the appropriate Tailwind utility classes mapped to those tokens) so the story canvas follows the dashboard spacing rules and avoids hardcoded pixel values; update the JSX wrapper div to use the token-based className instead of style.web/src/pages/WorkflowEditorPage.tsx (1)
114-119: 🧹 Nitpick | 🔵 TrivialNon-deterministic node positioning via
Math.random()remains.Using
Math.random()creates inconsistent layouts when adding nodes in quick succession. Consider deterministic positioning based on node count or a grid-snapping algorithm.Alternative: offset based on node count
const handleAddNode = useCallback( (type: WorkflowNodeType) => { - addNode(type, { x: 250 + Math.random() * 100, y: 150 + Math.random() * 200 }) + const offset = nodes.length * 30 + addNode(type, { x: 250 + (offset % 150), y: 150 + Math.floor(offset / 150) * 80 }) }, - [addNode], + [addNode, nodes.length], )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/WorkflowEditorPage.tsx` around lines 114 - 119, The handler handleAddNode currently uses Math.random() for node coordinates which yields non-deterministic layouts; replace that with a deterministic placement strategy (e.g., grid-snapping or offset by existing node count) inside handleAddNode so new nodes added via addNode(type, coords) get predictable positions; reference the addNode call in handleAddNode and compute coords from a deterministic source (like nodes.length or a nextIndex counter) rather than Math.random(), ensuring coordinates follow a consistent grid or incremental offset strategy.tests/unit/api/controllers/test_workflows.py (1)
119-123: 🧹 Nitpick | 🔵 TrivialTest seeding via private attribute — acknowledged but coupling remains.
The comment on lines 119-120 explains the sync context limitation. While this is a reasonable test-only shortcut, the test is coupled to
repo._definitionsinternals. If the fake repository implementation changes, these tests will break.Consider adding a sync-compatible
seed()method to the fake repository for test-only use, or accept this tradeoff with the existing documentation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/api/controllers/test_workflows.py` around lines 119 - 123, The test currently mutates the fake repository's private storage (repo._definitions) which couples tests to implementation; add a test-only synchronous seed method on the fake repository (e.g., FakeWorkflowDefinitionsRepository.seed(definition) or seed_definitions(dict)) and implement it to populate the internal store, then update the test to call repo.seed(defn) (or repo.seed_definitions({defn.id: defn})) instead of touching repo._definitions directly; keep the internal field name (e.g., _definitions) private and let the new seed API encapsulate the mutation for Litestar TestClient sync tests.web/src/pages/workflow-editor/ParallelJoinNode.tsx (1)
29-42:⚠️ Potential issue | 🟠 MajorHardcoded two-branch limit persists — parallel join should support 2+ incoming branches.
The backend model allows
parallel_jointo merge 2+ branches, but this node still renders only two fixed handles (branch-0andbranch-1). A third incoming edge cannot be represented correctly on the canvas.Consider making the handle count dynamic based on incoming edge count or a configurable
branch_countindata.config.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/ParallelJoinNode.tsx` around lines 29 - 42, ParallelJoinNode currently renders only two fixed Handle components (ids "branch-0" and "branch-1"); update the render to generate Handles dynamically based on the actual incoming branch count (derive from props.data.config.branch_count if present, or fallback to props.incomingEdges.length or data.incoming.length) and map over that count to create Handle elements with ids like `branch-{i}` and unique keys; compute each Handle's left style using a distribution formula (e.g. (i+1)/(count+1) as a percentage) so handles spread evenly, preserve type="target" and position={Position.Top}, and keep existing classes (e.g. "bg-accent! size-1.5!")—also ensure any event handlers or references expecting branch ids are updated to accept the dynamic `branch-{i}` naming.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/controllers/workflows.py`:
- Around line 257-277: The code uses existing.model_copy(update=updates) which
bypasses Pydantic v2 field coercion and `@model_validator` hooks (so
_validate_unique_ids, _validate_edge_references, _validate_terminal_nodes and
workflow_type coercion are skipped); instead merge the existing model dump with
updates (e.g., base = existing.model_dump()), apply updates to that dict, then
re-validate by calling WorkflowDefinition.model_validate(merged_dict) (or
existing.__class__.model_validate) and catch ValidationError/ValueError as
before; then pass the validated instance to repo.save(updated) so validators run
before persisting.
In `@src/synthorg/engine/workflow/validation.py`:
- Around line 178-242: _check_conditional_edges currently only ensures presence
of TRUE/FALSE but allows duplicates; update it to require exactly one
WorkflowEdgeType.CONDITIONAL_TRUE and exactly one
WorkflowEdgeType.CONDITIONAL_FALSE (use counts on out_types and emit
ValidationErrorCode.CONDITIONAL_MISSING_TRUE/CONDITIONAL_MISSING_FALSE when
count==0 and a new or existing error code like CONDITIONAL_DUPLICATE_TRUE/FALSE
when count>1 or reuse CONDITIONAL_EXTRA_OUTGOING) and still reject any edges not
in _CONDITIONAL_EDGE_TYPES; similarly, change _check_parallel_splits to not only
count WorkflowEdgeType.PARALLEL_BRANCH but also treat any outgoing edge types
that are not WorkflowEdgeType.PARALLEL_BRANCH as an error (emit
ValidationErrorCode.SPLIT_HAS_NON_PARALLEL_OUTGOING or reuse
SPLIT_TOO_FEW_BRANCHES/CONDITIONAL_EXTRA_OUTGOING as appropriate), so parallel
splits must have only parallel_branch edges and at least _MIN_SPLIT_BRANCHES of
them.
In `@src/synthorg/persistence/sqlite/schema.sql`:
- Around line 319-320: The version column currently defaults to 1 but lacks a
constraint preventing zero or negative values; update the schema definition for
the version column (the column named "version") to enforce a positive invariant
by adding a CHECK that version >= 1 (either as an inline CHECK on the "version"
column or a named table-level constraint such as "positive_version") so
optimistic concurrency assumptions cannot be violated.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py`:
- Around line 121-162: The INSERT...ON CONFLICT with the optimistic-concurrency
WHERE clause can succeed as a no-op (zero rows affected) on version mismatch;
detect this by capturing the cursor returned from await self._db.execute(...)
(assign to a variable like cursor), check cursor.rowcount (or
cursor.get_changes() if using a different DB API) before committing, and if
rowcount == 0 then rollback the transaction (await self._db.rollback()),
log/raise a concurrency-specific error (e.g., raise QueryError or a new
ConcurrencyError) referencing definition.id to signal the optimistic concurrency
failure instead of committing and logging PERSISTENCE_WORKFLOW_DEF_SAVED; keep
the sqlite3.Error except block for actual DB errors.
In `@web/src/pages/workflow-editor/ConditionalNode.tsx`:
- Around line 15-16: The code reads data.config?.condition_expression as a
string without validation in ConditionalNodeComponent, which can render "[object
Object]" or throw; change the assignment for condition to first check typeof
data.config?.condition_expression === 'string', trim() that string, and if it's
empty or not a string fall back to data.label so only a validated string is
rendered (reference ConditionalNodeComponent and the condition
variable/data.config?.condition_expression).
In `@web/src/pages/workflow-editor/TaskNode.stories.tsx`:
- Line 10: The container div in TaskNode.stories.tsx currently uses hardcoded
inline pixel sizes (the element with style={{ width: 300, height: 200 }});
remove the inline style and replace it with the design-system/tailwind sizing
tokens via a className (use the project’s token scale equivalents for 300x200 —
e.g., the corresponding w- and h- tokens or design-system size tokens) so sizing
comes from the design system; ensure the className is applied to the same div
and verify the story layout remains visually identical in Storybook.
In `@web/src/pages/workflow-editor/TaskNode.tsx`:
- Line 35: Update the Tailwind important modifier to prefix form on the Handle
components: replace postfix classes like "bg-border-bright! size-1.5!" with the
prefix form "!bg-border-bright !size-1.5" so the styles are not ignored. Locate
the Handle usages (e.g., in TaskNode.tsx's Handle component and the equivalent
handles in ParallelJoinNode.tsx, StartNode.tsx, EndNode.tsx,
ParallelSplitNode.tsx) and update their className strings accordingly; ensure
all occurrences of trailing "!" modifiers are converted to leading "!" for each
utility.
In `@web/src/pages/workflow-editor/WorkflowNodeDrawer.tsx`:
- Around line 55-84: The JSX inside fields.map in WorkflowNodeDrawer.tsx is too
large; extract the per-field renderer into a new component or helper function
(e.g., FieldRenderer or renderConfigField) that accepts a ConfigField, current
value, and handleFieldChange callback and returns either the SelectField or
InputField JSX; replace the inline map body with fields.map(field =>
<FieldRenderer key={field.key} field={field} value={String(config[field.key] ??
'')} onChange={handleFieldChange} />) and ensure the new component uses
field.type, field.options, field.placeholder, SelectField, InputField, and calls
handleFieldChange(field.key, ...) maintaining the same props/behavior.
In `@web/src/pages/workflow-editor/WorkflowToolbar.tsx`:
- Around line 72-84: Extract the repeated JSX in the NODE_PALETTE.map into a
small reusable component (e.g., PaletteButton) to improve readability: create a
PaletteButton functional component that accepts props (type, label, icon, onAdd)
and renders the Button plus Icon, then replace the inline map body with
<PaletteButton key={type} ... onAdd={onAddNode} />; reference NODE_PALETTE,
onAddNode and the new PaletteButton in the change so the map becomes a single
call and the button rendering logic lives in PaletteButton.
In `@web/src/stores/workflow-editor.ts`:
- Around line 401-419: The validate and exportYaml handlers currently use only
definition.id and ignore unsaved canvas edits; update validate and exportYaml in
workflow-editor.ts to check the store's dirty flag (from get()) and either block
the action (set an error like 'Cannot validate/export while there are unsaved
changes') or, preferably, gather the current in-memory graph (nodes, edges,
yamlPreview from get()) and send that payload to the backend by calling
validateWorkflow and exportWorkflowYaml with the full definition object or a new
API overload that accepts the live graph; ensure you update set() calls to
toggle validating/exporting flags and keep existing error handling (use
getErrorMessage(err)) when network calls fail.
- Around line 128-135: loadDefinition() currently maps persisted edge types like
'parallel_branch' back to 'sequential', causing edge.type instability versus
edges created by onConnect() (which uses 'parallel_branch'); update the mapping
in the edges construction (the const edges: Edge[] block in loadDefinition()) to
preserve 'parallel_branch' (i.e., set type to 'parallel_branch' when e.type ===
'parallel_branch') and ensure the data.edgeType/sourceHandle/branch logic
remains consistent; also apply the same preservation change in the other mapping
block referenced (around lines 282-299) so persisted parallel edges keep the
same React Flow type as edges created by onConnect().
---
Duplicate comments:
In `@src/synthorg/api/dto.py`:
- Around line 611-618: The nodes and edges fields currently accept arbitrary
dict[str, object] which bypasses request-time validation; define explicit
Pydantic models (e.g., NodeDTO and EdgeDTO) with concrete fields (for example
NodeDTO: id: str, type: str, metadata: dict[str, Any] | None; EdgeDTO: id: str,
source: str, target: str, type: str, metadata: dict[str, Any] | None) and
replace the annotations on nodes and edges to tuple[NodeDTO, ...] and
tuple[EdgeDTO, ...]; keep the existing Field(...) metadata (description and max
length) or use conlist/typing constraints if you want strict length validation,
and update any imports so DTO parsing/validation happens at the API boundary
(referencing nodes and edges in src/synthorg/api/dto.py).
In `@tests/unit/api/controllers/test_workflows.py`:
- Around line 119-123: The test currently mutates the fake repository's private
storage (repo._definitions) which couples tests to implementation; add a
test-only synchronous seed method on the fake repository (e.g.,
FakeWorkflowDefinitionsRepository.seed(definition) or seed_definitions(dict))
and implement it to populate the internal store, then update the test to call
repo.seed(defn) (or repo.seed_definitions({defn.id: defn})) instead of touching
repo._definitions directly; keep the internal field name (e.g., _definitions)
private and let the new seed API encapsulate the mutation for Litestar
TestClient sync tests.
In `@web/src/pages/workflow-editor/AgentAssignmentNode.stories.tsx`:
- Line 10: The div in AgentAssignmentNode.stories.tsx uses an inline style prop
style={{ width: 300, height: 200 }} which hardcodes pixel spacing; remove the
style prop and replace it with the project's design-token-based utility classes
or spacing tokens (e.g., a className that uses the design system/Tailwind
utility or CSS variable-based token for width/height) so sizing comes from
approved tokens rather than raw pixels; update the JSX that renders the div in
AgentAssignmentNode.stories.tsx accordingly and ensure no other inline pixel
spacing remains.
In `@web/src/pages/workflow-editor/EndNode.tsx`:
- Line 19: The Tailwind important modifier is used with the suffix form
incorrectly on the Handle component in EndNode.tsx; update the className on the
Handle (the JSX <Handle ... />) to use the prefix form for important utilities
(e.g. replace any "bg-border-bright! size-2!" tokens with "!bg-border-bright
!size-2") so the important flag is applied correctly and ensure spacing between
utilities.
In `@web/src/pages/workflow-editor/ParallelJoinNode.tsx`:
- Around line 29-42: ParallelJoinNode currently renders only two fixed Handle
components (ids "branch-0" and "branch-1"); update the render to generate
Handles dynamically based on the actual incoming branch count (derive from
props.data.config.branch_count if present, or fallback to
props.incomingEdges.length or data.incoming.length) and map over that count to
create Handle elements with ids like `branch-{i}` and unique keys; compute each
Handle's left style using a distribution formula (e.g. (i+1)/(count+1) as a
percentage) so handles spread evenly, preserve type="target" and
position={Position.Top}, and keep existing classes (e.g. "bg-accent!
size-1.5!")—also ensure any event handlers or references expecting branch ids
are updated to accept the dynamic `branch-{i}` naming.
In `@web/src/pages/workflow-editor/ParallelNode.stories.tsx`:
- Line 11: The wrapper div in ParallelNode.stories.tsx uses inline px sizing
(style={{ width: 300, height: 200 }}); remove the hardcoded inline style and
replace it with token-backed sizing via Tailwind/design tokens (use a className
that references the project spacing tokens or the appropriate Tailwind utility
classes mapped to those tokens) so the story canvas follows the dashboard
spacing rules and avoids hardcoded pixel values; update the JSX wrapper div to
use the token-based className instead of style.
In `@web/src/pages/workflow-editor/ParallelSplitNode.tsx`:
- Around line 31-45: The component currently hardcodes two bottom source Handles
("branch-0" and "branch-1") using Position.Bottom which prevents >2 outgoing
edges; change ParallelSplitNode to iterate over the node's branch metadata/count
(e.g., the branches array or node.data.branches) and render a Handle per branch,
generating id `branch-${i}` and className/position using Position.Bottom, and
compute style.left dynamically (even spacing, e.g. (i+1)/(count+1) * 100%) so
each outgoing branch gets its own unique handle instead of reusing
branch-0/branch-1.
In `@web/src/pages/workflow-editor/WorkflowYamlPreview.tsx`:
- Around line 23-27: The disclosure icons are inverted: change the JSX in the
conditional using the collapsed boolean so that when collapsed is true it
renders <ChevronDown ... /> and when collapsed is false (expanded) it renders
<ChevronUp ... />; update the conditional around the collapsed variable (the JSX
that currently renders ChevronUp/ChevronDown) to swap the two icon components so
the visual chevron matches the collapsed/expanded state (keep existing props
like className and aria-hidden).
In `@web/src/pages/WorkflowEditorPage.tsx`:
- Around line 114-119: The handler handleAddNode currently uses Math.random()
for node coordinates which yields non-deterministic layouts; replace that with a
deterministic placement strategy (e.g., grid-snapping or offset by existing node
count) inside handleAddNode so new nodes added via addNode(type, coords) get
predictable positions; reference the addNode call in handleAddNode and compute
coords from a deterministic source (like nodes.length or a nextIndex counter)
rather than Math.random(), ensuring coordinates follow a consistent grid or
incremental offset strategy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b5b007e6-0393-4c6f-9a51-31e4f5061be2
📒 Files selected for processing (62)
CLAUDE.mddocs/design/engine.mddocs/design/page-structure.mdsrc/synthorg/api/controllers/__init__.pysrc/synthorg/api/controllers/workflows.pysrc/synthorg/api/dto.pysrc/synthorg/core/enums.pysrc/synthorg/engine/workflow/definition.pysrc/synthorg/engine/workflow/validation.pysrc/synthorg/engine/workflow/yaml_export.pysrc/synthorg/observability/events/persistence.pysrc/synthorg/observability/events/workflow_definition.pysrc/synthorg/persistence/protocol.pysrc/synthorg/persistence/sqlite/backend.pysrc/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/persistence/workflow_definition_repo.pytests/unit/api/controllers/test_workflows.pytests/unit/api/fakes.pytests/unit/api/fakes_workflow.pytests/unit/engine/workflow/conftest.pytests/unit/engine/workflow/test_definition.pytests/unit/engine/workflow/test_validation.pytests/unit/engine/workflow/test_yaml_export.pytests/unit/observability/test_events.pytests/unit/persistence/sqlite/test_migrations.pytests/unit/persistence/sqlite/test_workflow_definition_repo.pytests/unit/persistence/test_protocol.pyweb/CLAUDE.mdweb/src/api/endpoints/workflows.tsweb/src/api/types.tsweb/src/components/layout/Sidebar.tsxweb/src/pages/WorkflowEditorPage.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.stories.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.tsxweb/src/pages/workflow-editor/EndNode.tsxweb/src/pages/workflow-editor/ParallelJoinNode.tsxweb/src/pages/workflow-editor/ParallelNode.stories.tsxweb/src/pages/workflow-editor/ParallelSplitNode.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/SequentialEdge.tsxweb/src/pages/workflow-editor/StartNode.tsxweb/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/pages/workflow-editor/TaskNode.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/WorkflowEditorSkeleton.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.tsxweb/src/pages/workflow-editor/node-config-schemas.tsweb/src/pages/workflow-editor/workflow-to-yaml.tsweb/src/router/index.tsxweb/src/router/routes.tsweb/src/stores/workflow-editor.ts
| def _check_conditional_edges( | ||
| definition: WorkflowDefinition, | ||
| outgoing: dict[str, list[WorkflowEdgeType]], | ||
| ) -> list[WorkflowValidationError]: | ||
| """Validate conditional node edge constraints.""" | ||
| errors: list[WorkflowValidationError] = [] | ||
| for node in definition.nodes: | ||
| if node.type != WorkflowNodeType.CONDITIONAL: | ||
| continue | ||
| out_types = outgoing.get(node.id, []) | ||
| if WorkflowEdgeType.CONDITIONAL_TRUE not in out_types: | ||
| errors.append( | ||
| WorkflowValidationError( | ||
| code=ValidationErrorCode.CONDITIONAL_MISSING_TRUE, | ||
| message=(f"Conditional node {node.id!r} missing TRUE branch"), | ||
| node_id=node.id, | ||
| ) | ||
| ) | ||
| if WorkflowEdgeType.CONDITIONAL_FALSE not in out_types: | ||
| errors.append( | ||
| WorkflowValidationError( | ||
| code=ValidationErrorCode.CONDITIONAL_MISSING_FALSE, | ||
| message=(f"Conditional node {node.id!r} missing FALSE branch"), | ||
| node_id=node.id, | ||
| ) | ||
| ) | ||
| extra = [t for t in out_types if t not in _CONDITIONAL_EDGE_TYPES] | ||
| if extra: | ||
| errors.append( | ||
| WorkflowValidationError( | ||
| code=ValidationErrorCode.CONDITIONAL_EXTRA_OUTGOING, | ||
| message=( | ||
| f"Conditional node {node.id!r} has " | ||
| f"non-conditional outgoing edges: {extra}" | ||
| ), | ||
| node_id=node.id, | ||
| ) | ||
| ) | ||
| return errors | ||
|
|
||
|
|
||
| def _check_parallel_splits( | ||
| definition: WorkflowDefinition, | ||
| outgoing: dict[str, list[WorkflowEdgeType]], | ||
| ) -> list[WorkflowValidationError]: | ||
| """Validate parallel split nodes have enough branches.""" | ||
| errors: list[WorkflowValidationError] = [] | ||
| for node in definition.nodes: | ||
| if node.type != WorkflowNodeType.PARALLEL_SPLIT: | ||
| continue | ||
| out_types = outgoing.get(node.id, []) | ||
| count = out_types.count(WorkflowEdgeType.PARALLEL_BRANCH) | ||
| if count < _MIN_SPLIT_BRANCHES: | ||
| errors.append( | ||
| WorkflowValidationError( | ||
| code=ValidationErrorCode.SPLIT_TOO_FEW_BRANCHES, | ||
| message=( | ||
| f"Parallel split {node.id!r} has {count} " | ||
| f"branch(es), needs at least " | ||
| f"{_MIN_SPLIT_BRANCHES}" | ||
| ), | ||
| node_id=node.id, | ||
| ) | ||
| ) | ||
| return errors |
There was a problem hiding this comment.
Require exact outgoing shapes for branching nodes.
A conditional with [TRUE, TRUE, FALSE] and a parallel split with [PARALLEL_BRANCH, PARALLEL_BRANCH, SEQUENTIAL] both pass today. That makes CONDITIONAL and PARALLEL_SPLIT behave like ambiguous fan-out nodes instead of fixed binary/parallel constructs. Count TRUE/FALSE exactly once on conditionals, and reject any non-parallel_branch outgoing edges from splits.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/engine/workflow/validation.py` around lines 178 - 242,
_check_conditional_edges currently only ensures presence of TRUE/FALSE but
allows duplicates; update it to require exactly one
WorkflowEdgeType.CONDITIONAL_TRUE and exactly one
WorkflowEdgeType.CONDITIONAL_FALSE (use counts on out_types and emit
ValidationErrorCode.CONDITIONAL_MISSING_TRUE/CONDITIONAL_MISSING_FALSE when
count==0 and a new or existing error code like CONDITIONAL_DUPLICATE_TRUE/FALSE
when count>1 or reuse CONDITIONAL_EXTRA_OUTGOING) and still reject any edges not
in _CONDITIONAL_EDGE_TYPES; similarly, change _check_parallel_splits to not only
count WorkflowEdgeType.PARALLEL_BRANCH but also treat any outgoing edge types
that are not WorkflowEdgeType.PARALLEL_BRANCH as an error (emit
ValidationErrorCode.SPLIT_HAS_NON_PARALLEL_OUTGOING or reuse
SPLIT_TOO_FEW_BRANCHES/CONDITIONAL_EXTRA_OUTGOING as appropriate), so parallel
splits must have only parallel_branch edges and at least _MIN_SPLIT_BRANCHES of
them.
| version INTEGER NOT NULL DEFAULT 1 | ||
| ); |
There was a problem hiding this comment.
Enforce positive version invariant for optimistic concurrency.
version defaults to 1, but without a constraint it can still be set to 0/negative values, which can break concurrency assumptions.
Suggested fix
- version INTEGER NOT NULL DEFAULT 1
+ version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| version INTEGER NOT NULL DEFAULT 1 | |
| ); | |
| version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1) | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/persistence/sqlite/schema.sql` around lines 319 - 320, The
version column currently defaults to 1 but lacks a constraint preventing zero or
negative values; update the schema definition for the version column (the column
named "version") to enforce a positive invariant by adding a CHECK that version
>= 1 (either as an inline CHECK on the "version" column or a named table-level
constraint such as "positive_version") so optimistic concurrency assumptions
cannot be violated.
| data-testid="task-node" | ||
| aria-label={`Task: ${title}`} | ||
| > | ||
| <Handle type="target" position={Position.Top} className="bg-border-bright! size-1.5!" /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether postfix "!" utilities are used and whether Tailwind config customizes this behavior.
rg -n --type=ts --type=tsx "bg-border-bright!|size-1.5!" web/src
CONFIGS=$(fd -i "tailwind.config.*" .)
if [ -n "$CONFIGS" ]; then
rg -n "important|plugins|safelist" $CONFIGS
fiRepository: Aureliolo/synthorg
Length of output: 89
🏁 Script executed:
#!/bin/bash
# Read the specific file to inspect lines 35 and 54
cat -n web/src/pages/workflow-editor/TaskNode.tsx | sed -n '30,60p'Repository: Aureliolo/synthorg
Length of output: 1307
🏁 Script executed:
#!/bin/bash
# Search for postfix "!" usage in TypeScript/React files
rg -n "!\s*[\"']" web/src --type ts -A 2 -B 2 | head -50Repository: Aureliolo/synthorg
Length of output: 3572
🏁 Script executed:
#!/bin/bash
# Find and inspect Tailwind config
fd "tailwind" . -type f | grep -E "\.(ts|js|json)$"Repository: Aureliolo/synthorg
Length of output: 232
🏁 Script executed:
#!/bin/bash
# Check for any custom Tailwind plugins or important-modifier customization
find . -name "tailwind.config.*" -o -name "*.config.*" | xargs cat 2>/dev/null | head -100Repository: Aureliolo/synthorg
Length of output: 3106
🏁 Script executed:
#!/bin/bash
# Search for tailwind config in web directory
find web -name "tailwind.config.*" -o -name "tailwind.json"Repository: Aureliolo/synthorg
Length of output: 44
🏁 Script executed:
#!/bin/bash
# Check web package.json for tailwind config or build info
cat web/package.json | head -100Repository: Aureliolo/synthorg
Length of output: 3150
🏁 Script executed:
#!/bin/bash
# Search for any custom Tailwind setup or plugins
rg -n "important|@tailwindcss" web --type ts --type js -B 2 -A 2 | head -80Repository: Aureliolo/synthorg
Length of output: 1776
Fix invalid Tailwind important modifier syntax — use prefix form instead of postfix.
The bg-border-bright! and size-1.5! syntax uses postfix !, but Tailwind CSS 4.2.2 requires the prefix form: !bg-border-bright !size-1.5. The postfix syntax will be silently ignored by the Tailwind parser, causing these handle styles to not apply.
🔧 Proposed fix
- <Handle type="target" position={Position.Top} className="bg-border-bright! size-1.5!" />
+ <Handle type="target" position={Position.Top} className="!bg-border-bright !size-1.5" />- <Handle type="source" position={Position.Bottom} className="bg-border-bright! size-1.5!" />
+ <Handle type="source" position={Position.Bottom} className="!bg-border-bright !size-1.5" />Also applies to: ParallelJoinNode.tsx (lines 33, 40, 49), StartNode.tsx (line 19), EndNode.tsx (line 19), ParallelSplitNode.tsx (line 26).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/workflow-editor/TaskNode.tsx` at line 35, Update the Tailwind
important modifier to prefix form on the Handle components: replace postfix
classes like "bg-border-bright! size-1.5!" with the prefix form
"!bg-border-bright !size-1.5" so the styles are not ignored. Locate the Handle
usages (e.g., in TaskNode.tsx's Handle component and the equivalent handles in
ParallelJoinNode.tsx, StartNode.tsx, EndNode.tsx, ParallelSplitNode.tsx) and
update their className strings accordingly; ensure all occurrences of trailing
"!" modifiers are converted to leading "!" for each utility.
| {NODE_PALETTE.map(({ type, label, icon: Icon }) => ( | ||
| <Button | ||
| key={type} | ||
| variant="ghost" | ||
| size="sm" | ||
| title={`Add ${label}`} | ||
| aria-label={`Add ${label} node`} | ||
| onClick={() => onAddNode(type)} | ||
| className="size-8 p-0" | ||
| > | ||
| <Icon className="size-4" aria-hidden="true" /> | ||
| </Button> | ||
| ))} |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider extracting the palette button to a small component.
The JSX inside .map() is ~11 lines. While not overly complex, extracting a PaletteButton component would improve readability and align with the guideline to avoid complex JSX inside .map().
♻️ Optional refactor
function PaletteButton({ type, label, icon: Icon, onAdd }: {
type: WorkflowNodeType
label: string
icon: React.ComponentType<{ className?: string }>
onAdd: (type: WorkflowNodeType) => void
}) {
return (
<Button
variant="ghost"
size="sm"
title={`Add ${label}`}
aria-label={`Add ${label} node`}
onClick={() => onAdd(type)}
className="size-8 p-0"
>
<Icon className="size-4" aria-hidden="true" />
</Button>
)
}Then in the toolbar:
{NODE_PALETTE.map(({ type, label, icon }) => (
<PaletteButton key={type} type={type} label={label} icon={icon} onAdd={onAddNode} />
))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/workflow-editor/WorkflowToolbar.tsx` around lines 72 - 84,
Extract the repeated JSX in the NODE_PALETTE.map into a small reusable component
(e.g., PaletteButton) to improve readability: create a PaletteButton functional
component that accepts props (type, label, icon, onAdd) and renders the Button
plus Icon, then replace the inline map body with <PaletteButton key={type} ...
onAdd={onAddNode} />; reference NODE_PALETTE, onAddNode and the new
PaletteButton in the change so the map becomes a single call and the button
rendering logic lives in PaletteButton.
Full-stack implementation of the visual workflow editor: Backend: - WorkflowNodeType/WorkflowEdgeType enums - WorkflowNode, WorkflowEdge, WorkflowDefinition domain models - WorkflowValidator with graph connectivity, cycle detection, conditional/parallel constraints, config completeness checks - WorkflowYamlExporter with topological sort and step generation - WorkflowDefinitionRepository protocol + SQLite implementation - WorkflowController with CRUD, validate, and YAML export endpoints - Structured observability events for all operations Frontend: - 7 custom @xyflow/react node types (Start, End, Task, AgentAssignment, Conditional, ParallelSplit, ParallelJoin) with design tokens - 2 custom edge types (Sequential, Conditional) - Zustand store with undo/redo, live YAML preview, optimistic updates - WorkflowEditorPage with toolbar, node property drawer, YAML preview - Dagre auto-layout, node config schemas, client-side YAML generation - Sidebar navigation entry, lazy-loaded route at /workflows/editor - Storybook stories for all node types Tests: - 71 backend unit tests (models, validation, YAML export) - Frontend: clean type-check + lint Closes #247
Pre-reviewed by 13 agents, 36 findings addressed: Backend: - Narrow except clauses to ValueError, ValidationError (not bare Exception) - WorkflowValidationResult.valid is now @computed_field - Iterative DFS replaces recursive (no stack overflow on deep graphs) - BFS uses deque.popleft() instead of list.pop(0) - Add max_length=500/1000 on DTO node/edge tuples (DoS prevention) - DTO workflow_type uses WorkflowType enum directly - Datetime normalization with .astimezone(UTC) in persistence - Add indexes on workflow_type and updated_at - Event constants use Final[str] - Add logging on all controller error paths - Fix duplicate END_NOT_REACHABLE + UNREACHABLE_NODE errors - WorkflowEdge.label uses NotBlankStr | None - WorkflowValidationError.message uses NotBlankStr - Catch yaml.YAMLError in export - Fix import ordering in yaml_export.py - Split export_workflow_yaml into sub-functions (50-line limit) Frontend: - Use individual Zustand selectors (performance) - Replace mutable module-level ID counters with crypto.randomUUID() - Fix handleSave success toast firing on failure - Add validation toast feedback - Export error includes error message context - saveDefinition/exportYaml handle null definition properly - Clear stale validationResult on validation failure - structuredClone for undo snapshots - Client-side topo-sort warns on cycles - DOM download appends anchor to body - Remove unused layout.ts dead code - Add 4 missing Storybook stories Docs: - CLAUDE.md Package Structure updated - web/CLAUDE.md endpoint count + stores list - docs/design/page-structure.md sidebar + route table - docs/design/engine.md workflow definitions section
…nd CodeRabbit Backend: - Fix delete_workflow to raise NotFoundError on missing definition (was silent 204) - Add optimistic concurrency WHERE version check to SQLite upsert - Remove redundant aiosqlite.Error from catch clauses (subclass of sqlite3.Error) - Replace SELECT * with explicit column list in repository queries - Add CHECK constraint on workflow_type column in schema.sql - Add timezone guard for datetime.fromisoformat in row deserialization - Initialize cursor before try block in delete to prevent UnboundLocalError - Replace f-string SQL LIMIT with hardcoded literal - Extract _deserialize_row helper to reduce function lengths - Rename validate_workflow import to avoid method name shadowing - Add status_code=200 to validate/export POST endpoints - Fix error message extraction pattern (TRY003/EM101) Frontend: - Fix undo/redo to use structuredClone instead of shallow copies - Fix nodeTypeToEdgeType: remove dead conditional logic, return parallel_branch for splits - Fix node selection: use NodeProps.selected instead of data.selected (5 components) - Use getErrorMessage() utility instead of inline err instanceof Error pattern - Fix validate() to set error state when definition is null - Add undo snapshots to onNodesChange/onEdgesChange for drag/delete operations - Clear stale validationResult on save - Fix module-level lastSortHadCycle mutable state (now local variable) - Add coordination_topology, max_concurrency, agent_name to client YAML preview - Fix WorkflowYamlPreview: use useId() instead of hardcoded id for ARIA - Fix ConditionalEdge: use props.markerEnd instead of hardcoded MarkerType - Fix WorkflowNodeDrawer: parse numeric fields - Add console.warn to empty localStorage catch blocks - Remove empty ARIA live region (toast system handles announcements) Storybook: - Add component property to 4 story meta objects for autodocs - Create ConditionalEdge.stories.tsx and SequentialEdge.stories.tsx - Replace hardcoded pixel dimensions with Tailwind classes in 2 stories Tests: - Create test_workflow_definition_repo.py (8 SQLite integration tests) - Create test_workflows.py (21 API controller tests) - Fix fake repositories: replace Any/object with concrete domain types - Add WorkflowDefinitionRepository protocol compliance test - Extract shared test helpers to conftest.py - Split fakes_workflow.py to stay under 800-line limit - Remove stale imports, use enum values in test constants Docs: - Update page-structure.md: controller count, map, page section, edge types - Update engine.md front matter to include workflow definitions
- Fix page-structure.md node/edge type names to match enums - Add deepcopy at persistence boundary in FakeWorkflowDefinitionRepository - Deep clone snapshot nodes/edges on undo/redo restore - Add NaN guard to WorkflowNodeDrawer numeric field parsing - Add optimistic concurrency rejection test for SQLite repo - Replace hardcoded inline pixels in ConditionalNode.stories.tsx - Use typed Meta/StoryObj generics in WorkflowYamlPreview.stories.tsx - Add explanatory comment on test_workflows.py _seed direct repo mutation
Backend: - Use model_validate instead of model_copy in update_workflow to re-run validators - Detect rowcount==0 on optimistic concurrency save and raise QueryError - Add CHECK(version >= 1) constraint to schema.sql Frontend: - Add typeof guard for condition_expression in ConditionalNode - Fix loadDefinition to preserve parallel_branch edge type (was mapped to sequential) - Replace hardcoded inline pixels in 3 more story files (Task, AgentAssignment, Parallel) - Extract FieldRenderer component from WorkflowNodeDrawer map body Tests: - Update optimistic concurrency test to expect QueryError on version mismatch
6739037 to
d5524f5
Compare
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (14)
web/src/pages/workflow-editor/SequentialEdge.tsx (1)
2-3:⚠️ Potential issue | 🟡 MinorUse the resolved edge marker from props on
BaseEdge.At Line 19, passing
MarkerType.ArrowCloseddirectly can break marker rendering in custom edges; forwardprops.markerEndinstead.🔧 Proposed fix
-import { BaseEdge, getSmoothStepPath, MarkerType, type EdgeProps } from '@xyflow/react' +import { BaseEdge, getSmoothStepPath, type EdgeProps } from '@xyflow/react' @@ - markerEnd={MarkerType.ArrowClosed} + markerEnd={props.markerEnd}In `@xyflow/react` v12 custom edges, what value/type should be passed to BaseEdge.markerEnd?Also applies to: 19-19
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/SequentialEdge.tsx` around lines 2 - 3, The BaseEdge invocation in SequentialEdge.tsx currently hardcodes MarkerType.ArrowClosed for the markerEnd which can break custom edge marker resolution; replace that hardcoded value by forwarding the resolved marker from the edge props (use props.markerEnd) when rendering <BaseEdge ... markerEnd={...}> so BaseEdge receives the marker provided by the flow system or caller (reference: BaseEdge, markerEnd prop, and the SequentialEdge component).web/src/pages/workflow-editor/EndNode.tsx (1)
19-19:⚠️ Potential issue | 🟡 MinorFix Tailwind important modifier syntax on the Handle class.
At Line 19,
bg-border-bright! size-2!should use prefix form:!bg-border-bright !size-2.🔧 Proposed fix
- <Handle type="target" position={Position.Top} className="bg-border-bright! size-2!" /> + <Handle type="target" position={Position.Top} className="!bg-border-bright !size-2" />#!/bin/bash # Verify invalid Tailwind important suffix usage in workflow-editor nodes. rg -nP --type=tsx 'className=.*\b[a-z0-9-]+!\b' web/src/pages/workflow-editor # Expected after fixes: no matches.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/EndNode.tsx` at line 19, The Tailwind important modifier is used in suffix form in the Handle's className prop; update the className on the Handle (the JSX element using Handle with Position.Top in EndNode.tsx) to use prefix form by replacing "bg-border-bright! size-2!" with "!bg-border-bright !size-2" so both utilities become properly forced; ensure you edit the className prop on the Handle element (the one with type="target" position={Position.Top}) and run the ripgrep check provided to confirm no trailing "!" suffixes remain.web/src/pages/workflow-editor/ParallelJoinNode.tsx (1)
29-42:⚠️ Potential issue | 🟠 MajorDon’t hardcode
parallel_jointo two incoming branches.At Line 29-42, this node can only represent two explicit branch handles. For fan-in workflows beyond two branches, the canvas model can drift from persisted workflow semantics.
Please render target handles dynamically from branch metadata/count in node data, with stable ids (e.g.,
branch-${i}) and evenly spaced top positions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/ParallelJoinNode.tsx` around lines 29 - 42, The ParallelJoinNode currently hardcodes two target Handles; change the render in the ParallelJoinNode component to iterate over the node's branch metadata/count (e.g., node.data.branches or node.data.branchCount) and render a Handle for each branch with type="target", position=Position.Top, id={`branch-${i}`} and a computed left style like `${((i+1)/(n+1))*100}%` to evenly space them; keep existing className and other props but generate them via .map so the number of handles matches persisted workflow branches and ids remain stable.web/src/pages/workflow-editor/WorkflowYamlPreview.tsx (1)
23-27:⚠️ Potential issue | 🟡 MinorSwap disclosure icons to match panel state.
Line 23 currently shows
ChevronUpwhen collapsed andChevronDownwhen expanded, which inverts the state cue.Proposed fix
{collapsed ? ( - <ChevronUp className="size-3.5" aria-hidden="true" /> + <ChevronDown className="size-3.5" aria-hidden="true" /> ) : ( - <ChevronDown className="size-3.5" aria-hidden="true" /> + <ChevronUp className="size-3.5" aria-hidden="true" /> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/WorkflowYamlPreview.tsx` around lines 23 - 27, The disclosure icons are inverted: the JSX in WorkflowYamlPreview.tsx uses ChevronUp when collapsed and ChevronDown when expanded; change the conditional to render ChevronDown when collapsed and ChevronUp when expanded so the icon matches the panel state (use the existing collapsed boolean and the ChevronUp/ChevronDown components).src/synthorg/api/dto.py (1)
611-618:⚠️ Potential issue | 🟠 MajorHarden workflow graph payload validation at the API boundary.
nodesandedgesasdict[str, object]are too permissive and allow malformed graph objects to pass request parsing. Please switch to explicit nested DTOs (or strict validators) so invalid node/edge shape is rejected at parse time.Suggested direction
+class WorkflowNodePayload(BaseModel): + model_config = ConfigDict(frozen=True, allow_inf_nan=False) + id: NotBlankStr + type: NotBlankStr + # add strict typed fields used by controller/domain mapping + data: dict[str, object] = Field(default_factory=dict) + +class WorkflowEdgePayload(BaseModel): + model_config = ConfigDict(frozen=True, allow_inf_nan=False) + id: NotBlankStr + source: NotBlankStr + target: NotBlankStr + type: NotBlankStr + data: dict[str, object] = Field(default_factory=dict) @@ - nodes: tuple[dict[str, object], ...] = Field( + nodes: tuple[WorkflowNodePayload, ...] = Field( max_length=500, description="Workflow nodes", ) - edges: tuple[dict[str, object], ...] = Field( + edges: tuple[WorkflowEdgePayload, ...] = Field( max_length=1000, description="Workflow edges", )As per coding guidelines, “Validate at system boundaries (user input, external APIs, config files)”.
Also applies to: 640-647
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/dto.py` around lines 611 - 618, Replace the permissive dict types for the nodes and edges fields with explicit nested Pydantic DTOs: define a NodeDTO (e.g., with id: str, type: str, metadata: dict[str, Any] | None and any required positional/coordinate fields) and an EdgeDTO (e.g., with source: str, target: str, label: str | None, metadata: dict[str, Any] | None), then change the fields nodes: tuple[NodeDTO, ...] and edges: tuple[EdgeDTO, ...] on the existing DTO so malformed shapes are rejected at parse time; add field validators (or con validators) on NodeDTO/EdgeDTO to enforce required keys and value types and apply the same change to the other nodes/edges declaration duplicated later in the file.web/src/pages/workflow-editor/ParallelSplitNode.tsx (1)
31-45:⚠️ Potential issue | 🟠 MajorTwo-handle hardcoding limits parallel fan-out to binary splits.
The backend and documentation define
parallel_splitas supporting 2+ branches, but this component hardcodes exactly two source handles (branch-0andbranch-1). Users cannot visually author workflows with 3+ parallel branches.Consider rendering handles dynamically based on the number of connected edges or a configurable branch count in
data.config:♻️ Suggested approach
+ {/* Dynamic source handles based on branch count */} + {Array.from({ length: data.config.branchCount ?? 2 }).map((_, i, arr) => ( + <Handle + key={`branch-${i}`} + type="source" + position={Position.Bottom} + id={`branch-${i}`} + className="bg-accent! size-1.5!" + style={{ left: `${((i + 1) / (arr.length + 1)) * 100}%` }} + /> + ))} - <Handle - type="source" - position={Position.Bottom} - id="branch-0" - className="bg-accent! size-1.5!" - style={{ left: '33%' }} - /> - <Handle - type="source" - position={Position.Bottom} - id="branch-1" - className="bg-accent! size-1.5!" - style={{ left: '66%' }} - />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/ParallelSplitNode.tsx` around lines 31 - 45, The component currently hardcodes exactly two source Handles (ids "branch-0" and "branch-1") which prevents 3+ parallel branches; update the ParallelSplitNode render to compute the number of branches from the node data (e.g., data.config.branchCount or data.config.branches) or from connected edges, then map over that count to emit a Handle for each branch with id `branch-{i}`, position={Position.Bottom}, and a computed style.left (e.g., evenly spaced percentage based on index/total); ensure each Handle has a stable key and keep existing className and other props, and include a sensible default (2) when no config is present.web/src/pages/workflow-editor/WorkflowToolbar.tsx (1)
72-84: 🧹 Nitpick | 🔵 TrivialOptional: Extract palette button to reduce
.map()complexity.The JSX is ~11 lines, slightly exceeding the 8-line guideline. Extracting to a
PaletteButtoncomponent would improve readability but is not critical.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/WorkflowToolbar.tsx` around lines 72 - 84, The NODE_PALETTE mapping in WorkflowToolbar.tsx is rendering Button JSX inline which makes the .map() block longer than the project guideline; extract the repeated JSX into a small functional component (e.g., PaletteButton) that accepts props {type, label, Icon, onAddNode} and returns the Button with the same props (title, aria-label, onClick calling onAddNode(type), className and rendering <Icon />). Replace the inline map with NODE_PALETTE.map(item => <PaletteButton key={item.type} {...item} onAddNode={onAddNode} />) to collapse complexity and keep behavior identical.web/src/pages/workflow-editor/WorkflowToolbar.stories.tsx (1)
30-34: 🧹 Nitpick | 🔵 TrivialConsider adding decorator to meta for context provisioning.
While the
Wrappercomponent providesReactFlowProvider, adding a decorator to meta would enable Storybook's autodocs and controls to work properly withWorkflowToolbardirectly.♻️ Optional enhancement
const meta: Meta = { title: 'Workflow Editor/Toolbar', component: WorkflowToolbar, parameters: { layout: 'padded' }, + decorators: [ + (Story) => ( + <ReactFlowProvider> + <Story /> + </ReactFlowProvider> + ), + ], }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/pages/workflow-editor/WorkflowToolbar.stories.tsx` around lines 30 - 34, The Storybook meta for WorkflowToolbar lacks a decorator that provides the ReactFlow context, so controls/autodocs can't render the component standalone; update the exported meta object (symbol: meta) to include a decorators array that wraps stories with the same context used by Wrapper (i.e., provide a decorator that renders children inside ReactFlowProvider or reuses the Wrapper component) so WorkflowToolbar can be previewed directly with full context for controls and docs.src/synthorg/persistence/sqlite/workflow_definition_repo.py (2)
239-249:⚠️ Potential issue | 🟠 MajorRepository-level
LIMIT 10000breaks API pagination semantics.
WorkflowController.list_workflows()paginates this tuple in memory, so this cap truncates the result set before pagination and underreportspagination.totalonce the table grows past 10k rows. Pushoffset/limitdown into the query instead of hard-clamping here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py` around lines 239 - 249, The query currently hard-caps results with "ORDER BY updated_at DESC LIMIT 10000", which breaks pagination; modify the method that builds this SQL (the query construction around _SELECT_COLUMNS in workflow_definition_repo.py) to accept limit and offset parameters and push them into the SQL instead of the fixed 10k cap: remove the hardcoded "LIMIT 10000", append " ORDER BY updated_at DESC LIMIT ? OFFSET ?" (or the DB's param order) and add the corresponding numeric values to the params list after any workflow_type param so callers can pass page size and offset through the repository method (ensure existing workflow_type handling/conditions remain unchanged and that params order matches the SQL placeholders).
77-84: 🛠️ Refactor suggestion | 🟠 MajorUse PEP 758
except A, B:syntax here.This parenthesized multi-exception form still violates the repo’s Python 3.14 rule and will keep tripping the formatter/linter.
#!/bin/bash rg -n 'except \(' src/synthorg/persistence/sqlite/workflow_definition_repo.pyAs per coding guidelines: "Use PEP 758 except syntax:
except A, B:(no parentheses) -- ruff enforces this on Python 3.14"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py` around lines 77 - 84, The except clause currently uses a parenthesized multi-exception form (except (ValueError, ValidationError, json.JSONDecodeError, KeyError) as exc) which violates the repo rule; replace it with PEP 758 style using commas without parentheses (except ValueError, ValidationError, json.JSONDecodeError, KeyError as exc) so the linter/formatter accepts it, keeping the existing logger.exception call (PERSISTENCE_WORKFLOW_DEF_DESERIALIZE_FAILED, definition_id=context_id, error=str(exc)) and re-raising QueryError(msg) from exc unchanged.src/synthorg/engine/workflow/validation.py (2)
178-216:⚠️ Potential issue | 🟠 MajorRequire exactly one TRUE and one FALSE branch per conditional.
This still only checks presence, so a node with
[conditional_true, conditional_true, conditional_false]passes validation. That leavesCONDITIONALnodes with ambiguous fan-out semantics instead of enforcing a strict binary branch shape.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/engine/workflow/validation.py` around lines 178 - 216, The _check_conditional_edges function currently only validates presence of TRUE/FALSE edges but not multiplicity; update _check_conditional_edges to count occurrences in out_types for WorkflowEdgeType.CONDITIONAL_TRUE and WorkflowEdgeType.CONDITIONAL_FALSE and emit ValidationError (use ValidationErrorCode.CONDITIONAL_MISSING_TRUE/FALSE for zero and the existing CONDITIONAL_EXTRA_OUTGOING or add distinct codes if present) when counts are not exactly one (i.e., count == 0 or count > 1) and still detect any non-conditional types via _CONDITIONAL_EDGE_TYPES as extra; reference the function name _check_conditional_edges, the variables out_types and extra, and WorkflowEdgeType.CONDITIONAL_TRUE/FALSE when implementing the checks.
219-242:⚠️ Potential issue | 🟠 MajorReject non-
parallel_branchoutgoing edges fromPARALLEL_SPLIT.The current check only counts parallel branches, so a split with two
parallel_branchedges plus onesequentialedge still validates.PARALLEL_SPLITshould fail as soon as any outgoing edge is not a parallel branch.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/engine/workflow/validation.py` around lines 219 - 242, The current _check_parallel_splits only counts WorkflowEdgeType.PARALLEL_BRANCH but doesn't fail when other outgoing edge types are present; update _check_parallel_splits to, for each node of type WorkflowNodeType.PARALLEL_SPLIT, inspect outgoing.get(node.id, []) and if any edge type is not WorkflowEdgeType.PARALLEL_BRANCH immediately append a WorkflowValidationError (e.g., use a new or existing ValidationErrorCode such as SPLIT_INVALID_OUTGOING_EDGE) with a clear message naming the node id and the invalid edge type(s) and return that error alongside the existing SPLIT_TOO_FEW_BRANCHES check.src/synthorg/api/controllers/workflows.py (1)
62-64: 🛠️ Refactor suggestion | 🟠 MajorUse PEP 758
except A, B:syntax in these handlers.These parenthesized multi-exception clauses still violate the Python 3.14 rule enforced by the repo.
#!/bin/bash rg -n 'except \(' src/synthorg/api/controllers/workflows.pyAs per coding guidelines: "Use PEP 758 except syntax:
except A, B:(no parentheses) -- ruff enforces this on Python 3.14"Also applies to: 77-79, 193-193, 266-266
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/controllers/workflows.py` around lines 62 - 64, Replace the parenthesized multi-exception clauses like "except (ValueError, ValidationError) as exc" with the PEP 758 style "except ValueError, ValidationError as exc" in the workflow controller; locate the try/except blocks around WorkflowNode.model_validate (the one assigning updates["nodes"] from data.nodes) and the other spots called out (around lines handling similar ValidationError/ValueError pairs) and change each except (A, B) as exc to except A, B as exc so ruff/Python 3.14-compatible syntax is used.web/src/stores/workflow-editor.ts (1)
401-419:⚠️ Potential issue | 🟠 MajorValidate and export the live graph, not the last saved revision.
Once
dirtyis true, both actions only senddefinition.id, so users can validate/export something different from the canvas and YAML preview they are looking at. Either block these actions while dirty or submit the in-memory nodes/edges.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/workflow-editor.ts` around lines 401 - 419, The validate and exportYaml handlers currently only send definition.id so they validate/export the last saved revision, not the live canvas; update validate and exportYaml in workflow-editor.ts to check get().dirty and, if dirty, send the in-memory graph (nodes/edges) to the backend instead of only definition.id: change calls to validateWorkflow(definition.id) and exportWorkflowYaml(definition.id) to versions that accept the live graph (e.g., validateWorkflow(definition.id, { nodes, edges }) and exportWorkflowYaml(definition.id, { nodes, edges })), or if such APIs don't exist, add/extend them and pass get().nodes and get().edges; alternatively, if you prefer blocking, set an error and return when get().dirty is true (use set({ error: 'Save or apply changes before validating/exporting' })), referencing the validate and exportYaml handlers and get().dirty, get().nodes, get().edges, and the functions validateWorkflow/exportWorkflowYaml.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/controllers/workflows.py`:
- Around line 182-190: The WorkflowDefinition is hardcoding created_by="api";
instead populate created_by from the authenticated caller (e.g., use the request
principal or current_user) when building the WorkflowDefinition in the function
that constructs it (where id, name, description, workflow_type, nodes, edges,
created_at are set). Retrieve the authenticated user identifier (user.id or
user.username/email depending on your auth model) from the request context
(request.user, current_user, flask.g.user, or the auth helper used elsewhere in
this controller) and assign that to created_by, falling back to a safe default
like "api" only if no authenticated principal is present. Ensure you reference
the same auth extraction used elsewhere in this controller to keep behavior
consistent.
- Around line 236-279: The save path in workflows.py must handle repository
version conflicts: around the await repo.save(updated) call in the update flow
(after WorkflowDefinition.model_validate and before logger.info), catch the
repository-specific QueryError (the version-conflict subclass of
PersistenceError) and translate it into an HTTP 409 response (or raise a domain
VersionConflictError that your existing exception_handlers.py maps to 409).
Specifically, wrap await repo.save(updated) in a try/except QueryError block,
and on QueryError return Response with an ApiResponse[WorkflowDefinition] error
message indicating a version conflict and status_code=409 (or re-raise a
VersionConflictError) so concurrent write conflicts no longer surface as 500.
In `@src/synthorg/engine/workflow/yaml_export.py`:
- Around line 183-257: The export_workflow_yaml function is too large; extract
step generation and YAML serialization/error handling into helpers to reduce its
size. Create a new helper (e.g., _generate_steps(definition, node_map,
adjacency, reverse_adj, outgoing_edges)) that encapsulates the loop that builds
steps via _build_step and returns the steps list, and another helper (e.g.,
_serialize_document(document, workflow_id)) that calls yaml.dump and handles
yaml.YAMLError by logging WORKFLOW_DEF_EXPORT_FAILED with reason="yaml_error"
and raising ValueError; then simplify export_workflow_yaml to call
_topological_sort, use _generate_steps to build steps, call _assemble_document,
call _serialize_document to get result, and keep the existing logging for
WORKFLOW_DEF_EXPORTED. Ensure you reference node_map, _build_step,
_assemble_document and preserve the existing exception logging for
cycle_detected from _topological_sort.
- Around line 127-133: The CONDITIONAL node export currently only writes
step["condition"] and ignores routing edges, so inspect the node's
outgoing_edges for WorkflowEdgeType.CONDITIONAL_TRUE and
WorkflowEdgeType.CONDITIONAL_FALSE (in the same area handling
WorkflowNodeType.CONDITIONAL and using config["condition_expression"]) and add
step["true_branch"] and step["false_branch"] values (the target node ids/names
from those edges) when present; keep the existing condition_expression handling
and ensure you still handle the PARALLEL_SPLIT branch logic unchanged.
In `@web/src/api/types.ts`:
- Around line 1576-1604: The request/response DTOs currently widen
workflow_type, nodes, and edges losing compile-time safety; update
WorkflowDefinition, CreateWorkflowDefinitionRequest, and
UpdateWorkflowDefinitionRequest to use the existing types and a union for
workflow_type: replace workflow_type: string with a WorkflowType union (mirror
backend enum), replace nodes: readonly Record<string, unknown>[] and edges:
readonly Record<string, unknown>[] with readonly WorkflowNodeData[] and readonly
WorkflowEdgeData[] (and optional variants in UpdateWorkflowDefinitionRequest),
and ensure all references to WorkflowNodeData and WorkflowEdgeData are
imported/used so callers no longer need downstream casts.
In `@web/src/pages/workflow-editor/SequentialEdge.stories.tsx`:
- Around line 55-64: The Meta and StoryObj types are untyped here; update the
declarations to use generics for better inference—use Meta<typeof
SequentialEdge> for the meta constant and StoryObj<typeof SequentialEdge> for
the Default story (or equivalent variable names) so the framework infers
props/args from the SequentialEdge component and keeps Wrapper/Default correctly
typed.
In `@web/src/pages/workflow-editor/TaskNode.stories.tsx`:
- Around line 53-57: The story places selected inside the node's data object so
TaskNodeComponent (which destructures selected from NodeProps<TaskNodeType>)
won't see it; move the selected property out of data to the node root (e.g.,
alongside data in the story object) so the component's selected && 'ring-2
ring-accent' styling is triggered, ensuring NodeProps.selected is set rather
than data.selected.
In `@web/src/pages/workflow-editor/workflow-to-yaml.ts`:
- Around line 37-38: The loop uses queue.shift() which is O(n) per pop; replace
it with an index-based queue pointer or a deque to avoid quadratic behavior:
keep the existing array named queue but use a numeric head index (e.g., let head
= 0) and read current = queue[head++] while checking head < queue.length (or
swap to a proper deque implementation) so you no longer call queue.shift();
update any code that relied on emptying the array to use head/length checks and,
if needed, slice or reset the array when head grows large to reclaim memory.
In `@web/src/stores/workflow-editor.ts`:
- Around line 196-214: The current save silently substitutes missing node/edge
types (nodes.map/edges.map) to 'task'/'sequential' before calling
updateWorkflow, which hides corrupted editor state; instead, validate that each
node has a defined type (node.type) and each edge has a defined semantic type
((e.data as Record<string, unknown>)?.edgeType) prior to calling updateWorkflow,
and if any are missing abort the save: set saving to false via set({...}),
populate validationResult or surface an explicit error indicating which
node/edge ids are missing types, and do not call updateWorkflow or advance
definition.version.
- Around line 118-148: When replacing the current workflow in loadDefinition
(and similarly in createDefinition) clear workflow-scoped UI state so stale UI
from the previous workflow doesn't persist: explicitly reset selectedNodeId (set
to null), selectedEdgeId (if present), validationResult (set to null) and any
drawer/open flags related to node selection in the set({...}) call that updates
definition/nodes/edges (i.e., inside loadDefinition and the createDefinition
state update) so the new workflow starts with a clean UI state.
---
Duplicate comments:
In `@src/synthorg/api/controllers/workflows.py`:
- Around line 62-64: Replace the parenthesized multi-exception clauses like
"except (ValueError, ValidationError) as exc" with the PEP 758 style "except
ValueError, ValidationError as exc" in the workflow controller; locate the
try/except blocks around WorkflowNode.model_validate (the one assigning
updates["nodes"] from data.nodes) and the other spots called out (around lines
handling similar ValidationError/ValueError pairs) and change each except (A, B)
as exc to except A, B as exc so ruff/Python 3.14-compatible syntax is used.
In `@src/synthorg/api/dto.py`:
- Around line 611-618: Replace the permissive dict types for the nodes and edges
fields with explicit nested Pydantic DTOs: define a NodeDTO (e.g., with id: str,
type: str, metadata: dict[str, Any] | None and any required
positional/coordinate fields) and an EdgeDTO (e.g., with source: str, target:
str, label: str | None, metadata: dict[str, Any] | None), then change the fields
nodes: tuple[NodeDTO, ...] and edges: tuple[EdgeDTO, ...] on the existing DTO so
malformed shapes are rejected at parse time; add field validators (or con
validators) on NodeDTO/EdgeDTO to enforce required keys and value types and
apply the same change to the other nodes/edges declaration duplicated later in
the file.
In `@src/synthorg/engine/workflow/validation.py`:
- Around line 178-216: The _check_conditional_edges function currently only
validates presence of TRUE/FALSE edges but not multiplicity; update
_check_conditional_edges to count occurrences in out_types for
WorkflowEdgeType.CONDITIONAL_TRUE and WorkflowEdgeType.CONDITIONAL_FALSE and
emit ValidationError (use ValidationErrorCode.CONDITIONAL_MISSING_TRUE/FALSE for
zero and the existing CONDITIONAL_EXTRA_OUTGOING or add distinct codes if
present) when counts are not exactly one (i.e., count == 0 or count > 1) and
still detect any non-conditional types via _CONDITIONAL_EDGE_TYPES as extra;
reference the function name _check_conditional_edges, the variables out_types
and extra, and WorkflowEdgeType.CONDITIONAL_TRUE/FALSE when implementing the
checks.
- Around line 219-242: The current _check_parallel_splits only counts
WorkflowEdgeType.PARALLEL_BRANCH but doesn't fail when other outgoing edge types
are present; update _check_parallel_splits to, for each node of type
WorkflowNodeType.PARALLEL_SPLIT, inspect outgoing.get(node.id, []) and if any
edge type is not WorkflowEdgeType.PARALLEL_BRANCH immediately append a
WorkflowValidationError (e.g., use a new or existing ValidationErrorCode such as
SPLIT_INVALID_OUTGOING_EDGE) with a clear message naming the node id and the
invalid edge type(s) and return that error alongside the existing
SPLIT_TOO_FEW_BRANCHES check.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py`:
- Around line 239-249: The query currently hard-caps results with "ORDER BY
updated_at DESC LIMIT 10000", which breaks pagination; modify the method that
builds this SQL (the query construction around _SELECT_COLUMNS in
workflow_definition_repo.py) to accept limit and offset parameters and push them
into the SQL instead of the fixed 10k cap: remove the hardcoded "LIMIT 10000",
append " ORDER BY updated_at DESC LIMIT ? OFFSET ?" (or the DB's param order)
and add the corresponding numeric values to the params list after any
workflow_type param so callers can pass page size and offset through the
repository method (ensure existing workflow_type handling/conditions remain
unchanged and that params order matches the SQL placeholders).
- Around line 77-84: The except clause currently uses a parenthesized
multi-exception form (except (ValueError, ValidationError, json.JSONDecodeError,
KeyError) as exc) which violates the repo rule; replace it with PEP 758 style
using commas without parentheses (except ValueError, ValidationError,
json.JSONDecodeError, KeyError as exc) so the linter/formatter accepts it,
keeping the existing logger.exception call
(PERSISTENCE_WORKFLOW_DEF_DESERIALIZE_FAILED, definition_id=context_id,
error=str(exc)) and re-raising QueryError(msg) from exc unchanged.
In `@web/src/pages/workflow-editor/EndNode.tsx`:
- Line 19: The Tailwind important modifier is used in suffix form in the
Handle's className prop; update the className on the Handle (the JSX element
using Handle with Position.Top in EndNode.tsx) to use prefix form by replacing
"bg-border-bright! size-2!" with "!bg-border-bright !size-2" so both utilities
become properly forced; ensure you edit the className prop on the Handle element
(the one with type="target" position={Position.Top}) and run the ripgrep check
provided to confirm no trailing "!" suffixes remain.
In `@web/src/pages/workflow-editor/ParallelJoinNode.tsx`:
- Around line 29-42: The ParallelJoinNode currently hardcodes two target
Handles; change the render in the ParallelJoinNode component to iterate over the
node's branch metadata/count (e.g., node.data.branches or node.data.branchCount)
and render a Handle for each branch with type="target", position=Position.Top,
id={`branch-${i}`} and a computed left style like `${((i+1)/(n+1))*100}%` to
evenly space them; keep existing className and other props but generate them via
.map so the number of handles matches persisted workflow branches and ids remain
stable.
In `@web/src/pages/workflow-editor/ParallelSplitNode.tsx`:
- Around line 31-45: The component currently hardcodes exactly two source
Handles (ids "branch-0" and "branch-1") which prevents 3+ parallel branches;
update the ParallelSplitNode render to compute the number of branches from the
node data (e.g., data.config.branchCount or data.config.branches) or from
connected edges, then map over that count to emit a Handle for each branch with
id `branch-{i}`, position={Position.Bottom}, and a computed style.left (e.g.,
evenly spaced percentage based on index/total); ensure each Handle has a stable
key and keep existing className and other props, and include a sensible default
(2) when no config is present.
In `@web/src/pages/workflow-editor/SequentialEdge.tsx`:
- Around line 2-3: The BaseEdge invocation in SequentialEdge.tsx currently
hardcodes MarkerType.ArrowClosed for the markerEnd which can break custom edge
marker resolution; replace that hardcoded value by forwarding the resolved
marker from the edge props (use props.markerEnd) when rendering <BaseEdge ...
markerEnd={...}> so BaseEdge receives the marker provided by the flow system or
caller (reference: BaseEdge, markerEnd prop, and the SequentialEdge component).
In `@web/src/pages/workflow-editor/WorkflowToolbar.stories.tsx`:
- Around line 30-34: The Storybook meta for WorkflowToolbar lacks a decorator
that provides the ReactFlow context, so controls/autodocs can't render the
component standalone; update the exported meta object (symbol: meta) to include
a decorators array that wraps stories with the same context used by Wrapper
(i.e., provide a decorator that renders children inside ReactFlowProvider or
reuses the Wrapper component) so WorkflowToolbar can be previewed directly with
full context for controls and docs.
In `@web/src/pages/workflow-editor/WorkflowToolbar.tsx`:
- Around line 72-84: The NODE_PALETTE mapping in WorkflowToolbar.tsx is
rendering Button JSX inline which makes the .map() block longer than the project
guideline; extract the repeated JSX into a small functional component (e.g.,
PaletteButton) that accepts props {type, label, Icon, onAddNode} and returns the
Button with the same props (title, aria-label, onClick calling onAddNode(type),
className and rendering <Icon />). Replace the inline map with
NODE_PALETTE.map(item => <PaletteButton key={item.type} {...item}
onAddNode={onAddNode} />) to collapse complexity and keep behavior identical.
In `@web/src/pages/workflow-editor/WorkflowYamlPreview.tsx`:
- Around line 23-27: The disclosure icons are inverted: the JSX in
WorkflowYamlPreview.tsx uses ChevronUp when collapsed and ChevronDown when
expanded; change the conditional to render ChevronDown when collapsed and
ChevronUp when expanded so the icon matches the panel state (use the existing
collapsed boolean and the ChevronUp/ChevronDown components).
In `@web/src/stores/workflow-editor.ts`:
- Around line 401-419: The validate and exportYaml handlers currently only send
definition.id so they validate/export the last saved revision, not the live
canvas; update validate and exportYaml in workflow-editor.ts to check
get().dirty and, if dirty, send the in-memory graph (nodes/edges) to the backend
instead of only definition.id: change calls to validateWorkflow(definition.id)
and exportWorkflowYaml(definition.id) to versions that accept the live graph
(e.g., validateWorkflow(definition.id, { nodes, edges }) and
exportWorkflowYaml(definition.id, { nodes, edges })), or if such APIs don't
exist, add/extend them and pass get().nodes and get().edges; alternatively, if
you prefer blocking, set an error and return when get().dirty is true (use set({
error: 'Save or apply changes before validating/exporting' })), referencing the
validate and exportYaml handlers and get().dirty, get().nodes, get().edges, and
the functions validateWorkflow/exportWorkflowYaml.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 16ad2b0b-de08-4f9b-a134-dd9a2f12711d
📒 Files selected for processing (62)
CLAUDE.mddocs/design/engine.mddocs/design/page-structure.mdsrc/synthorg/api/controllers/__init__.pysrc/synthorg/api/controllers/workflows.pysrc/synthorg/api/dto.pysrc/synthorg/core/enums.pysrc/synthorg/engine/workflow/definition.pysrc/synthorg/engine/workflow/validation.pysrc/synthorg/engine/workflow/yaml_export.pysrc/synthorg/observability/events/persistence.pysrc/synthorg/observability/events/workflow_definition.pysrc/synthorg/persistence/protocol.pysrc/synthorg/persistence/sqlite/backend.pysrc/synthorg/persistence/sqlite/schema.sqlsrc/synthorg/persistence/sqlite/workflow_definition_repo.pysrc/synthorg/persistence/workflow_definition_repo.pytests/unit/api/controllers/test_workflows.pytests/unit/api/fakes.pytests/unit/api/fakes_workflow.pytests/unit/engine/workflow/conftest.pytests/unit/engine/workflow/test_definition.pytests/unit/engine/workflow/test_validation.pytests/unit/engine/workflow/test_yaml_export.pytests/unit/observability/test_events.pytests/unit/persistence/sqlite/test_migrations.pytests/unit/persistence/sqlite/test_workflow_definition_repo.pytests/unit/persistence/test_protocol.pyweb/CLAUDE.mdweb/src/api/endpoints/workflows.tsweb/src/api/types.tsweb/src/components/layout/Sidebar.tsxweb/src/pages/WorkflowEditorPage.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.stories.tsxweb/src/pages/workflow-editor/AgentAssignmentNode.tsxweb/src/pages/workflow-editor/ConditionalEdge.stories.tsxweb/src/pages/workflow-editor/ConditionalEdge.tsxweb/src/pages/workflow-editor/ConditionalNode.stories.tsxweb/src/pages/workflow-editor/ConditionalNode.tsxweb/src/pages/workflow-editor/EndNode.tsxweb/src/pages/workflow-editor/ParallelJoinNode.tsxweb/src/pages/workflow-editor/ParallelNode.stories.tsxweb/src/pages/workflow-editor/ParallelSplitNode.tsxweb/src/pages/workflow-editor/SequentialEdge.stories.tsxweb/src/pages/workflow-editor/SequentialEdge.tsxweb/src/pages/workflow-editor/StartNode.tsxweb/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/pages/workflow-editor/TaskNode.tsxweb/src/pages/workflow-editor/TerminalNode.stories.tsxweb/src/pages/workflow-editor/WorkflowEditorSkeleton.stories.tsxweb/src/pages/workflow-editor/WorkflowEditorSkeleton.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.stories.tsxweb/src/pages/workflow-editor/WorkflowNodeDrawer.tsxweb/src/pages/workflow-editor/WorkflowToolbar.stories.tsxweb/src/pages/workflow-editor/WorkflowToolbar.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.stories.tsxweb/src/pages/workflow-editor/WorkflowYamlPreview.tsxweb/src/pages/workflow-editor/node-config-schemas.tsweb/src/pages/workflow-editor/workflow-to-yaml.tsweb/src/router/index.tsxweb/src/router/routes.tsweb/src/stores/workflow-editor.ts
| export interface WorkflowDefinition { | ||
| readonly id: string | ||
| readonly name: string | ||
| readonly description: string | ||
| readonly workflow_type: string | ||
| readonly nodes: readonly WorkflowNodeData[] | ||
| readonly edges: readonly WorkflowEdgeData[] | ||
| readonly created_by: string | ||
| readonly created_at: string | ||
| readonly updated_at: string | ||
| readonly version: number | ||
| } | ||
|
|
||
| export interface CreateWorkflowDefinitionRequest { | ||
| readonly name: string | ||
| readonly description?: string | ||
| readonly workflow_type: string | ||
| readonly nodes: readonly Record<string, unknown>[] | ||
| readonly edges: readonly Record<string, unknown>[] | ||
| } | ||
|
|
||
| export interface UpdateWorkflowDefinitionRequest { | ||
| readonly name?: string | ||
| readonly description?: string | ||
| readonly workflow_type?: string | ||
| readonly nodes?: readonly Record<string, unknown>[] | ||
| readonly edges?: readonly Record<string, unknown>[] | ||
| readonly expected_version?: number | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Tighten the workflow request/response shapes.
These DTOs are the API boundary, but workflow_type, nodes, and edges are widened to string / Record<string, unknown>[]. That drops compile-time protection for malformed workflow payloads and forces downstream casts in the editor store. Reuse WorkflowNodeData / WorkflowEdgeData here and introduce a workflow-type union that mirrors the backend enum.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/api/types.ts` around lines 1576 - 1604, The request/response DTOs
currently widen workflow_type, nodes, and edges losing compile-time safety;
update WorkflowDefinition, CreateWorkflowDefinitionRequest, and
UpdateWorkflowDefinitionRequest to use the existing types and a union for
workflow_type: replace workflow_type: string with a WorkflowType union (mirror
backend enum), replace nodes: readonly Record<string, unknown>[] and edges:
readonly Record<string, unknown>[] with readonly WorkflowNodeData[] and readonly
WorkflowEdgeData[] (and optional variants in UpdateWorkflowDefinitionRequest),
and ensure all references to WorkflowNodeData and WorkflowEdgeData are
imported/used so callers no longer need downstream casts.
| const meta: Meta = { | ||
| title: 'Workflow Editor/Sequential Edge', | ||
| component: SequentialEdge, | ||
| parameters: { layout: 'centered' }, | ||
| } | ||
|
|
||
| export default meta | ||
|
|
||
| export const Default: StoryObj = { | ||
| render: () => <Wrapper />, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider stronger typing for Meta and StoryObj.
For consistency with other story files and better type inference, use generic parameters:
♻️ Suggested improvement
-const meta: Meta = {
+const meta: Meta<typeof SequentialEdge> = {
title: 'Workflow Editor/Sequential Edge',
component: SequentialEdge,
parameters: { layout: 'centered' },
}
export default meta
-export const Default: StoryObj = {
+export const Default: StoryObj<typeof SequentialEdge> = {
render: () => <Wrapper />,
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const meta: Meta = { | |
| title: 'Workflow Editor/Sequential Edge', | |
| component: SequentialEdge, | |
| parameters: { layout: 'centered' }, | |
| } | |
| export default meta | |
| export const Default: StoryObj = { | |
| render: () => <Wrapper />, | |
| const meta: Meta<typeof SequentialEdge> = { | |
| title: 'Workflow Editor/Sequential Edge', | |
| component: SequentialEdge, | |
| parameters: { layout: 'centered' }, | |
| } | |
| export default meta | |
| export const Default: StoryObj<typeof SequentialEdge> = { | |
| render: () => <Wrapper />, | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/workflow-editor/SequentialEdge.stories.tsx` around lines 55 -
64, The Meta and StoryObj types are untyped here; update the declarations to use
generics for better inference—use Meta<typeof SequentialEdge> for the meta
constant and StoryObj<typeof SequentialEdge> for the Default story (or
equivalent variable names) so the framework infers props/args from the
SequentialEdge component and keeps Wrapper/Default correctly typed.
| while (queue.length > 0) { | ||
| const current = queue.shift()! |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider performance for large graphs — queue.shift() is O(n).
For typical workflow sizes this is fine, but shift() on arrays is O(n). If workflows could grow large, consider using an index-based approach or a proper deque structure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/workflow-editor/workflow-to-yaml.ts` around lines 37 - 38, The
loop uses queue.shift() which is O(n) per pop; replace it with an index-based
queue pointer or a deque to avoid quadratic behavior: keep the existing array
named queue but use a numeric head index (e.g., let head = 0) and read current =
queue[head++] while checking head < queue.length (or swap to a proper deque
implementation) so you no longer call queue.shift(); update any code that relied
on emptying the array to use head/length checks and, if needed, slice or reset
the array when head grows large to reclaim memory.
| loadDefinition: async (id: string) => { | ||
| set({ loading: true, error: null }) | ||
| try { | ||
| const def = await getWorkflow(id) | ||
| const nodes: Node[] = def.nodes.map((n) => ({ | ||
| id: n.id, | ||
| type: n.type, | ||
| position: { x: n.position_x, y: n.position_y }, | ||
| data: { label: n.label, config: n.config }, | ||
| })) | ||
| const edges: Edge[] = def.edges.map((e) => ({ | ||
| id: e.id, | ||
| source: e.source_node_id, | ||
| target: e.target_node_id, | ||
| type: e.type === 'conditional_true' || e.type === 'conditional_false' ? 'conditional' : e.type === 'parallel_branch' ? 'parallel_branch' : 'sequential', | ||
| sourceHandle: e.type === 'conditional_true' ? 'true' : e.type === 'conditional_false' ? 'false' : undefined, | ||
| data: { edgeType: e.type, branch: e.type === 'conditional_true' ? 'true' : e.type === 'conditional_false' ? 'false' : undefined }, | ||
| label: e.label ?? undefined, | ||
| })) | ||
| const yaml = regenerateYaml(nodes, edges, def) | ||
| set({ | ||
| definition: def, | ||
| nodes, | ||
| edges, | ||
| loading: false, | ||
| dirty: false, | ||
| undoStack: [], | ||
| redoStack: [], | ||
| yamlPreview: yaml, | ||
| validationResult: null, | ||
| }) |
There was a problem hiding this comment.
Clear workflow-scoped UI state when replacing the current definition.
selectedNodeId survives both code paths, and createDefinition() also leaves the previous validationResult intact. Switching workflows can therefore reopen the drawer for a nonexistent node or show validation errors from the prior graph.
Also applies to: 154-182
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/stores/workflow-editor.ts` around lines 118 - 148, When replacing the
current workflow in loadDefinition (and similarly in createDefinition) clear
workflow-scoped UI state so stale UI from the previous workflow doesn't persist:
explicitly reset selectedNodeId (set to null), selectedEdgeId (if present),
validationResult (set to null) and any drawer/open flags related to node
selection in the set({...}) call that updates definition/nodes/edges (i.e.,
inside loadDefinition and the createDefinition state update) so the new workflow
starts with a clean UI state.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (4)
src/synthorg/api/controllers/workflows.py (1)
64-77:⚠️ Potential issue | 🟡 MinorUse PEP 758 except syntax (no parentheses).
Per coding guidelines for Python 3.14, use
except A, B as exc:without parentheses.🔧 Proposed fix
- except (ValueError, ValidationError) as exc: + except ValueError, ValidationError as exc:Apply the same change to lines 81, 200, 273, and 350.
As per coding guidelines: "Use PEP 758 except syntax:
except A, B:(no parentheses) -- ruff enforces this on Python 3.14"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/api/controllers/workflows.py` around lines 64 - 77, The except clauses use tuple-style syntax (e.g. "except (ValueError, ValidationError) as exc:") which must be changed to PEP 758 form; update the try/except around the WorkflowNode.model_validate loop (the block that sets updates["nodes"] and logs WORKFLOW_DEF_INVALID_REQUEST) to use "except ValueError, ValidationError as exc:" and make the identical change in the same pattern at the other occurrences referenced (around the similar validation blocks at the locations corresponding to lines 81, 200, 273, and 350) so all multi-exception handlers follow the PEP 758 syntax while leaving the logging and Response construction unchanged.web/src/stores/workflow-editor.ts (2)
199-217:⚠️ Potential issue | 🟠 MajorSilent type fallbacks can mask corrupted editor state.
Lines 202 and 212 fall back to
'task'and'sequential'when node/edge types are undefined. This silently rewrites potentially corrupted graph state into a different workflow instead of surfacing the bug. Consider validating types and failing the save if any are missing.🔧 Proposed validation before save
saveDefinition: async () => { const { definition, nodes, edges } = get() if (!definition) { set({ error: 'Cannot save: no workflow loaded' }) return } + + const missingNodeTypes = nodes.filter((n) => !n.type).map((n) => n.id) + const missingEdgeTypes = edges.filter((e) => !(e.data as Record<string, unknown>)?.edgeType).map((e) => e.id) + if (missingNodeTypes.length > 0 || missingEdgeTypes.length > 0) { + set({ error: `Corrupted state: missing types on nodes [${missingNodeTypes.join(', ')}] or edges [${missingEdgeTypes.join(', ')}]` }) + return + } + set({ saving: true, error: null })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/workflow-editor.ts` around lines 199 - 217, The current save silently substitutes missing types when building the payload for updateWorkflow; instead validate node.type and edge.data.edgeType before calling updateWorkflow (in the block that maps nodes and edges), and if any are missing produce a validationResult, set saving:false and dirty:true (or similar) and return early to avoid calling updateWorkflow; reference the mapping of nodes (nodes.map(...)) and edges (edges.map(...)) and the updateWorkflow call to locate where to add the checks and early exit.
437-441:⚠️ Potential issue | 🟠 MajorExport uses persisted definition, ignoring unsaved canvas changes.
exportYamlcallsexportWorkflowYaml(definition.id)which fetches the persisted workflow from the backend. If the editor is dirty, users will export stale data that doesn't match what's on screen. Consider either blocking export while dirty or generating YAML from the current canvas state.🔧 Option 1: Block export while dirty
exportYaml: async () => { - const { definition } = get() + const { definition, dirty } = get() if (!definition) throw new Error('Cannot export: no workflow loaded') + if (dirty) throw new Error('Cannot export: save changes first') return exportWorkflowYaml(definition.id) },🔧 Option 2: Export current canvas state
Return the current
yamlPreviewwhich is already synchronized with the canvas, or call the backendvalidate-draft-style endpoint that accepts the full graph payload.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/workflow-editor.ts` around lines 437 - 441, exportYaml currently calls exportWorkflowYaml(definition.id) which pulls the persisted workflow and ignores unsaved canvas changes; update exportYaml to check the editor dirty state and either (a) block export when dirty by throwing an error or returning a user-facing signal, or (b) export the current in-memory canvas by returning the existing yamlPreview (which is kept in sync with the canvas) or by calling a draft/validate endpoint with the full graph payload; locate the exportYaml function and adjust its logic to use get().yamlPreview when available or to enforce a dirty-check before calling exportWorkflowYaml(definition.id).src/synthorg/engine/workflow/yaml_export.py (1)
127-129:⚠️ Potential issue | 🟠 MajorConditional branch routing targets are not exported.
The
CONDITIONALnode only exports thecondition_expression, but the true/false branch targets (fromCONDITIONAL_TRUEandCONDITIONAL_FALSEedges) are not included in the YAML. This means the downstream consumer cannot determine which nodes to route to based on condition evaluation.🔧 Proposed fix to include branch targets
elif node_type == WorkflowNodeType.CONDITIONAL: if "condition_expression" in config: step["condition"] = config["condition_expression"] + true_targets = [ + target + for target, edge_type in outgoing_edges + if edge_type == WorkflowEdgeType.CONDITIONAL_TRUE + ] + false_targets = [ + target + for target, edge_type in outgoing_edges + if edge_type == WorkflowEdgeType.CONDITIONAL_FALSE + ] + if true_targets: + step["on_true"] = true_targets + if false_targets: + step["on_false"] = false_targets🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/engine/workflow/yaml_export.py` around lines 127 - 129, When handling WorkflowNodeType.CONDITIONAL (the block that currently sets step["condition"] = config["condition_expression"]), also collect the conditional branch targets from outgoing edges of types CONDITIONAL_TRUE and CONDITIONAL_FALSE and write them into the step dict (e.g., step["true"] and step["false"] or a step["branches"] map). Locate the node's outgoing edges (the same place other node routing is derived in this file), find edges whose type equals WorkflowEdgeType.CONDITIONAL_TRUE and WorkflowEdgeType.CONDITIONAL_FALSE, extract their target node IDs, and add those IDs to the exported YAML step alongside the existing condition_expression. Ensure you reference the same variables used there (node_type, config, step) and use the WorkflowEdgeType names to identify the correct edges.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/api/controllers/workflows.py`:
- Around line 285-300: The controller in the workflow save path is detecting
version conflicts by matching the text of QueryError which is fragile; instead
add a specific VersionConflictError subclass in the persistence layer (e.g.,
define VersionConflictError(QueryError) in src/synthorg/persistence/errors.py)
and update this handler to catch VersionConflictError explicitly around the
await repo.save(updated) call, log using WORKFLOW_DEF_VERSION_CONFLICT with
definition_id=updated.id and error=str(exc), and return the existing
ApiResponse[WorkflowDefinition] 409 response when a VersionConflictError is
caught; leave other QueryError cases to re-raise.
In `@web/src/api/endpoints/workflows.ts`:
- Around line 51-53: The deleteWorkflow function currently calls
apiClient.delete directly
(apiClient.delete(`/workflows/${encodeURIComponent(id)}`)); replace this with
using unwrapVoid to consistently unwrap the response envelope (e.g. await
unwrapVoid(apiClient.delete(...))) so any non-204 success-with-error bodies are
handled; also add/import unwrapVoid alongside apiClient at the top of the file
and keep encodeURIComponent(id) in place when calling unwrapVoid.
In `@web/src/pages/WorkflowEditorPage.tsx`:
- Around line 266-277: The container in WorkflowEditorPage currently uses a
hardcoded height calc via the class h-[calc(100vh-7rem)]; update it to use a
layout variable instead (e.g., a CSS custom property like --header-height) or
switch to a parent-driven flex layout so the component adapts when header size
changes. Locate WorkflowEditorPage and replace the hardcoded calc with a
reference to the CSS variable (or restructure the parent/child flex styling) and
ensure the header component exposes/sets the same CSS variable (or that the
parent provides the flex sizing) so the editor height responds to header
changes.
---
Duplicate comments:
In `@src/synthorg/api/controllers/workflows.py`:
- Around line 64-77: The except clauses use tuple-style syntax (e.g. "except
(ValueError, ValidationError) as exc:") which must be changed to PEP 758 form;
update the try/except around the WorkflowNode.model_validate loop (the block
that sets updates["nodes"] and logs WORKFLOW_DEF_INVALID_REQUEST) to use "except
ValueError, ValidationError as exc:" and make the identical change in the same
pattern at the other occurrences referenced (around the similar validation
blocks at the locations corresponding to lines 81, 200, 273, and 350) so all
multi-exception handlers follow the PEP 758 syntax while leaving the logging and
Response construction unchanged.
In `@src/synthorg/engine/workflow/yaml_export.py`:
- Around line 127-129: When handling WorkflowNodeType.CONDITIONAL (the block
that currently sets step["condition"] = config["condition_expression"]), also
collect the conditional branch targets from outgoing edges of types
CONDITIONAL_TRUE and CONDITIONAL_FALSE and write them into the step dict (e.g.,
step["true"] and step["false"] or a step["branches"] map). Locate the node's
outgoing edges (the same place other node routing is derived in this file), find
edges whose type equals WorkflowEdgeType.CONDITIONAL_TRUE and
WorkflowEdgeType.CONDITIONAL_FALSE, extract their target node IDs, and add those
IDs to the exported YAML step alongside the existing condition_expression.
Ensure you reference the same variables used there (node_type, config, step) and
use the WorkflowEdgeType names to identify the correct edges.
In `@web/src/stores/workflow-editor.ts`:
- Around line 199-217: The current save silently substitutes missing types when
building the payload for updateWorkflow; instead validate node.type and
edge.data.edgeType before calling updateWorkflow (in the block that maps nodes
and edges), and if any are missing produce a validationResult, set saving:false
and dirty:true (or similar) and return early to avoid calling updateWorkflow;
reference the mapping of nodes (nodes.map(...)) and edges (edges.map(...)) and
the updateWorkflow call to locate where to add the checks and early exit.
- Around line 437-441: exportYaml currently calls
exportWorkflowYaml(definition.id) which pulls the persisted workflow and ignores
unsaved canvas changes; update exportYaml to check the editor dirty state and
either (a) block export when dirty by throwing an error or returning a
user-facing signal, or (b) export the current in-memory canvas by returning the
existing yamlPreview (which is kept in sync with the canvas) or by calling a
draft/validate endpoint with the full graph payload; locate the exportYaml
function and adjust its logic to use get().yamlPreview when available or to
enforce a dirty-check before calling exportWorkflowYaml(definition.id).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f3113dc7-2a41-4a1e-99e3-918cfe200098
📒 Files selected for processing (7)
src/synthorg/api/controllers/workflows.pysrc/synthorg/engine/workflow/yaml_export.pyweb/src/api/endpoints/workflows.tsweb/src/pages/WorkflowEditorPage.tsxweb/src/pages/workflow-editor/SequentialEdge.tsxweb/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/stores/workflow-editor.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Sandbox
- GitHub Check: Build Web
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors in React and TypeScript files
NEVER hardcode hex color values or rgba() with hardcoded values in.tsx/.tsfiles -- use design token variables instead
Do NOT recreate status dots inline -- use<StatusBadge>component instead
Do NOT build card-with-header layouts from scratch -- use<SectionCard>component instead
Do NOT create metric displays withtext-metric font-boldinline -- use<MetricCard>component instead
Do NOT render initials circles manually -- use<Avatar>component instead
Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component instead
Do NOT usergba()with hardcoded values -- use design token variables instead
Do NOT hardcode Framer Motion transition durations -- use presets from@/lib/motioninsteadReact dashboard design system: always reuse existing components from
web/src/components/ui/before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and@/lib/motionpresets.
Files:
web/src/pages/workflow-editor/SequentialEdge.tsxweb/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/api/endpoints/workflows.tsweb/src/pages/WorkflowEditorPage.tsxweb/src/stores/workflow-editor.ts
web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
TypeScript 6.0+ required for web dashboard; property-based testing in React uses fast-check (
fc.assert+fc.property)
Files:
web/src/pages/workflow-editor/SequentialEdge.tsxweb/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/api/endpoints/workflows.tsweb/src/pages/WorkflowEditorPage.tsxweb/src/stores/workflow-editor.ts
web/**/*.stories.tsx
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/**/*.stories.tsx: Import fromstorybook/test(not@storybook/test),storybook/actions(not@storybook/addon-actions), and usedefineMain/definePreviewtype-safe config in Storybook 10
In Storybook 10 stories, useparameters.backgrounds.options(object keyed by name) +initialGlobals.backgrounds.valuefor background configuration (replaces olddefault+valuesarray)
Files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Nofrom __future__ import annotationsin Python code -- Python 3.14 has PEP 649
Use PEP 758 except syntax:except A, B:(no parentheses) -- ruff enforces this on Python 3.14
All public functions must have type hints and pass mypy strict mode
Docstrings must use Google style and are required on all public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries,BaseTool), usecopy.deepcopy()at construction +MappingProxyTypewrapping for read-only enforcement.
Fordict/listfields in frozen Pydantic models, rely onfrozen=Truefor field reassignment prevention andcopy.deepcopy()at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (usingmodel_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel,model_validator,computed_field,ConfigDict). Always useallow_inf_nan=FalseinConfigDictdeclarations to rejectNaN/Infin numeric fields at validation time.
Use@computed_fieldfor derived values instead of storing + validating redundant fields (e.g.TokenUsage.total_tokens)
UseNotBlankStr(fromcore.types) for all identifier/name fields -- including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants -- instead of manual whitespace validators.
Preferasyncio.TaskGroupfor fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over barecreate_task.
Maximum line length: 88 characters (enforced by ruff)
Functions must be less than 50 lines, files must be less than 800 lines
Files:
src/synthorg/engine/workflow/yaml_export.pysrc/synthorg/api/controllers/workflows.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/synthorg/**/*.py: Every module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__)
Never useimport logging/logging.getLogger()/print()in application code (exceptions:observability/setup.py,observability/sinks.py,observability/syslog_handler.py, andobservability/http_handler.pymay use stdlibloggingandprint(..., file=sys.stderr))
Logger variable name must always belogger(not_logger, notlog)
Always use event constants from domain-specific modules undersynthorg.observability.events(e.g.,API_REQUEST_STARTEDfromevents.api). Import directly:from synthorg.observability.events.<domain> import EVENT_CONSTANT
Always use structured kwargs for logging:logger.info(EVENT, key=value)-- neverlogger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO level
Use DEBUG level for object creation, internal flow, entry/exit of key functions
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names:example-provider,example-large-001,example-medium-001,example-small-001,large/medium/small.
In Pydantic models, useallow_inf_nan=Falsein allConfigDictdeclarations
Pure data models, enums, and re-exports do NOT need logging
Files:
src/synthorg/engine/workflow/yaml_export.pysrc/synthorg/api/controllers/workflows.py
🧠 Learnings (48)
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Create a `.stories.tsx` file alongside every new shared component in `web/src/components/ui/` with all states (default, hover, loading, error, empty)
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/components/ui/**/*.{ts,tsx} : Create new shared components in `web/src/components/ui/` with `.stories.tsx` Storybook file covering all states (default, hover, loading, error, empty)
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`)
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Import from `storybook/test` instead of `storybook/test`
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use storybook/test (not storybook/test) and storybook/actions (not storybook/addon-actions) import paths
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/**/*.stories.tsx : Import from `storybook/test` (not `storybook/test`), `storybook/actions` (not `storybook/addon-actions`), and use `defineMain`/`definePreview` type-safe config in Storybook 10
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.stories.tsx : Storybook 10: Use `parameters.a11y.test: 'error' | 'todo' | 'off'` for a11y testing configuration (replaces old `.element` and `.manual`)
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/components/ui/*.{tsx,ts} : For new shared React components: place in web/src/components/ui/ with kebab-case filename, create .stories.tsx with all states, export props as TypeScript interface, use design tokens exclusively
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/**/*.stories.tsx : In Storybook 10 stories, use `parameters.backgrounds.options` (object keyed by name) + `initialGlobals.backgrounds.value` for background configuration (replaces old `default` + `values` array)
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-03-30T10:20:08.544Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:20:08.544Z
Learning: Applies to web/**/*.stories.{ts,tsx} : Storybook 10: Use parameters.backgrounds.options (object keyed by name) + initialGlobals.backgrounds.value for background options (replaces old default + values array)
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/src/components/**/*.{ts,tsx} : NEVER hardcode pixel values for layout spacing -- use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing classes
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsxweb/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (`p-card`, `gap-section-gap`, `gap-grid-gap`) or standard Tailwind spacing. NEVER hardcode pixel values for layout spacing
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-03-27T22:32:26.927Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T22:32:26.927Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use density-aware tokens (p-card, gap-section-gap, gap-grid-gap) or standard Tailwind spacing; never hardcode pixel values for layout spacing
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/src/components/ui/*.{ts,tsx} : Use design tokens exclusively in new components -- no hardcoded colors, fonts, or spacing
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-03-31T14:31:11.894Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T14:31:11.894Z
Learning: Applies to web/src/**/*.{ts,tsx} : NEVER hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions — use design tokens and `@/lib/motion` presets
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{ts,tsx,css} : Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions in web code — use design tokens and `@/lib/motion` presets
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to web/src/**/*.{ts,tsx} : React dashboard design system: always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-01T20:43:51.878Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T20:43:51.878Z
Learning: Applies to web/src/**/*.{ts,tsx} : Always reuse existing components from `web/src/components/ui/` before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and `@/lib/motion` presets.
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/src/**/*.{ts,tsx} : NEVER hardcode hex color values or rgba() with hardcoded values in `.tsx`/`.ts` files -- use design token variables instead
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/**/*.{tsx,ts} : Use `color?` and `animated?` props for Sparkline component (inline SVG trend lines)
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/.storybook/main.ts : Use type-safe Storybook 10 config with `defineMain` from `storybook/react-vite/node` and `definePreview` from `storybook/react-vite`, and include an explicit `framework` field
Applied to files:
web/src/pages/workflow-editor/TaskNode.stories.tsx
📚 Learning: 2026-04-01T06:12:03.047Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T06:12:03.047Z
Learning: Applies to {**/*.py,web/src/**/*.{ts,tsx}} : Keep functions under 50 lines and files under 800 lines
Applied to files:
src/synthorg/engine/workflow/yaml_export.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Functions must be less than 50 lines; files must be less than 800 lines
Applied to files:
src/synthorg/engine/workflow/yaml_export.py
📚 Learning: 2026-03-28T09:24:37.044Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-28T09:24:37.044Z
Learning: Applies to **/*.{py,ts,tsx,go} : Functions should be under 50 lines; files should be under 800 lines
Applied to files:
src/synthorg/engine/workflow/yaml_export.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : Functions must be less than 50 lines, files must be less than 800 lines
Applied to files:
src/synthorg/engine/workflow/yaml_export.py
📚 Learning: 2026-04-02T12:21:16.739Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:21:16.739Z
Learning: Applies to web/src/components/ui/**/*.tsx : Export props as a TypeScript interface for new components
Applied to files:
web/src/pages/WorkflowEditorPage.tsx
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) -- ruff enforces this on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-01T09:37:49.451Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:37:49.451Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with `model_copy(update=...)` for runtime state that evolves
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : For `dict`/`list` fields in frozen Pydantic models, rely on `frozen=True` for field reassignment prevention and `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 BaseModel, model_validator, computed_field, ConfigDict.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/stores/workflow-editor.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/stores/workflow-editor.ts
🔇 Additional comments (8)
web/src/pages/workflow-editor/SequentialEdge.tsx (1)
1-24: LGTM!Clean implementation using CSS variables for theming and proper memoization for React Flow performance optimization.
web/src/pages/workflow-editor/TaskNode.stories.tsx (1)
1-78: LGTM!Stories correctly place
selectedat the node root level (line 53), use Tailwind sizing classes for the container, and cover the key visual states (default, selected, error).src/synthorg/engine/workflow/yaml_export.py (1)
235-274: LGTM!The main export function is well-structured with proper error handling, logging at appropriate levels, and clean delegation to helper functions. All functions are under the 50-line limit.
src/synthorg/api/controllers/workflows.py (1)
173-219: LGTM on create workflow implementation.Proper extraction of authenticated user, generation of unique IDs, validation via
model_validate, structured error handling, and logging.web/src/pages/WorkflowEditorPage.tsx (1)
71-99: LGTM on individual selector pattern.Using individual selectors for each state slice (
useWorkflowEditorStore((s) => s.nodes)) avoids unnecessary re-renders when unrelated state changes. This is a well-applied Zustand optimization pattern.web/src/api/endpoints/workflows.ts (1)
72-79: LGTM on YAML export implementation.Correctly uses
responseType: 'text'to receive raw YAML string instead of JSON-parsed response.web/src/stores/workflow-editor.ts (2)
366-402: LGTM on undo/redo implementation.Both functions now properly use
structuredClonefor the current snapshot pushed to the opposite stack and for the restored state, ensuring full immutability and preventing reference corruption.
118-153: LGTM on loadDefinition.Properly clears
selectedNodeId,validationResult, and undo/redo stacks when loading a new workflow. Edge type mapping now preservesparallel_branchfor consistency withonConnect.
| export default function WorkflowEditorPage() { | ||
| return ( | ||
| <div className="flex h-[calc(100vh-7rem)] flex-col gap-section-gap"> | ||
| <h1 className="text-lg font-semibold text-foreground">Workflow Editor</h1> | ||
|
|
||
| <ErrorBoundary level="section"> | ||
| <ReactFlowProvider> | ||
| <WorkflowEditorInner /> | ||
| </ReactFlowProvider> | ||
| </ErrorBoundary> | ||
| </div> | ||
| ) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider using a layout variable instead of hardcoded 7rem.
The h-[calc(100vh-7rem)] hardcodes the header height. If the header layout changes, this will need manual updates.
♻️ Alternative approaches
- Use a CSS variable for the header height:
- <div className="flex h-[calc(100vh-7rem)] flex-col gap-section-gap">
+ <div className="flex h-[calc(100vh-var(--header-height))] flex-col gap-section-gap">- Or use flex layout with the parent container handling overflow:
- <div className="flex h-[calc(100vh-7rem)] flex-col gap-section-gap">
+ <div className="flex min-h-0 flex-1 flex-col gap-section-gap">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export default function WorkflowEditorPage() { | |
| return ( | |
| <div className="flex h-[calc(100vh-7rem)] flex-col gap-section-gap"> | |
| <h1 className="text-lg font-semibold text-foreground">Workflow Editor</h1> | |
| <ErrorBoundary level="section"> | |
| <ReactFlowProvider> | |
| <WorkflowEditorInner /> | |
| </ReactFlowProvider> | |
| </ErrorBoundary> | |
| </div> | |
| ) | |
| export default function WorkflowEditorPage() { | |
| return ( | |
| <div className="flex min-h-0 flex-1 flex-col gap-section-gap"> | |
| <h1 className="text-lg font-semibold text-foreground">Workflow Editor</h1> | |
| <ErrorBoundary level="section"> | |
| <ReactFlowProvider> | |
| <WorkflowEditorInner /> | |
| </ReactFlowProvider> | |
| </ErrorBoundary> | |
| </div> | |
| ) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/pages/WorkflowEditorPage.tsx` around lines 266 - 277, The container
in WorkflowEditorPage currently uses a hardcoded height calc via the class
h-[calc(100vh-7rem)]; update it to use a layout variable instead (e.g., a CSS
custom property like --header-height) or switch to a parent-driven flex layout
so the component adapts when header size changes. Locate WorkflowEditorPage and
replace the hardcoded calc with a reference to the CSS variable (or restructure
the parent/child flex styling) and ensure the header component exposes/sets the
same CSS variable (or that the parent provides the flex sizing) so the editor
height responds to header changes.
- Add VersionConflictError subclass in persistence.errors (replaces string matching) - Repository raises VersionConflictError, controller catches it explicitly for 409 - deleteWorkflow uses unwrapVoid for consistent envelope handling - exportYaml returns yamlPreview when dirty (exports current canvas, not persisted)
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
web/src/stores/workflow-editor.ts (1)
199-216: 🧹 Nitpick | 🔵 TrivialSilent type fallbacks may mask corrupted state.
Lines 202 and 212 fall back to
'task'and'sequential'when node/edge types are missing. This silently converts corrupted editor state into a valid-looking workflow, making debugging harder. Consider validating types before save and surfacing an error if any are missing.♻️ Proposed validation before save
saveDefinition: async () => { const { definition, nodes, edges } = get() if (!definition) { set({ error: 'Cannot save: no workflow loaded' }) return } + const missingNodeTypes = nodes.filter((n) => !n.type).map((n) => n.id) + const missingEdgeTypes = edges.filter((e) => !(e.data as Record<string, unknown>)?.edgeType).map((e) => e.id) + if (missingNodeTypes.length > 0 || missingEdgeTypes.length > 0) { + set({ error: `Corrupted state: missing types on nodes [${missingNodeTypes.join(', ')}] or edges [${missingEdgeTypes.join(', ')}]` }) + return + } set({ saving: true, error: null })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/workflow-editor.ts` around lines 199 - 216, The current save uses silent fallbacks in the updateWorkflow payload (nodes.map and edges.map) for node.type ('task') and edge.data.edgeType ('sequential'), which can hide corrupted editor state; instead, before calling updateWorkflow, validate every node in nodes (ensure n.type exists and is a valid type) and every edge in edges (ensure (e.data as Record<string,unknown>)?.edgeType exists and is valid), and if any are missing/invalid, abort the save and surface a clear error/validation message rather than substituting defaults; update the call site around updateWorkflow to only map after validation and return the validation error to the UI so the user can fix the corrupted state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py`:
- Around line 283-299: Remove the redundant pre-initialization and None check
for `cursor` in the delete path of `workflow_definition_repo.py`: in the method
that executes `await self._db.execute("DELETE FROM workflow_definitions WHERE id
= ?", (definition_id,))` (the block that catches `sqlite3.Error` and raises
`QueryError`), delete the `cursor = None` line and replace the `deleted = cursor
is not None and cursor.rowcount > 0` check with a direct `deleted =
cursor.rowcount > 0` (or equivalent) since `cursor` will always be set when
execute succeeds; keep the existing exception handling (logger.exception with
`PERSISTENCE_WORKFLOW_DEF_DELETE_FAILED` and re-raising `QueryError`) unchanged.
In `@web/src/stores/workflow-editor.ts`:
- Around line 218-221: The catch block in the save flow currently sets
saving=false and an error via getErrorMessage(err) but doesn't handle 409
version conflicts or refresh the stale store definition; update the catch to
detect a 409 (inspect err.response?.status), and if it's a conflict and the
store's definition exists, call the store loader (use
get().loadDefinition(definition.id)) to reload the current server state and set
a user-friendly conflict error message, otherwise fall back to set({ saving:
false, error: getErrorMessage(err) }); reference the symbols set, get(),
loadDefinition, definition, and getErrorMessage to locate and implement this
change.
---
Duplicate comments:
In `@web/src/stores/workflow-editor.ts`:
- Around line 199-216: The current save uses silent fallbacks in the
updateWorkflow payload (nodes.map and edges.map) for node.type ('task') and
edge.data.edgeType ('sequential'), which can hide corrupted editor state;
instead, before calling updateWorkflow, validate every node in nodes (ensure
n.type exists and is a valid type) and every edge in edges (ensure (e.data as
Record<string,unknown>)?.edgeType exists and is valid), and if any are
missing/invalid, abort the save and surface a clear error/validation message
rather than substituting defaults; update the call site around updateWorkflow to
only map after validation and return the validation error to the UI so the user
can fix the corrupted state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f2e73a13-ceeb-4852-bfc8-d5fca7b0b2c2
📒 Files selected for processing (6)
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/errors.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.pytests/unit/persistence/sqlite/test_workflow_definition_repo.pyweb/src/api/endpoints/workflows.tsweb/src/stores/workflow-editor.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Nofrom __future__ import annotationsin Python code -- Python 3.14 has PEP 649
Use PEP 758 except syntax:except A, B:(no parentheses) -- ruff enforces this on Python 3.14
All public functions must have type hints and pass mypy strict mode
Docstrings must use Google style and are required on all public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries,BaseTool), usecopy.deepcopy()at construction +MappingProxyTypewrapping for read-only enforcement.
Fordict/listfields in frozen Pydantic models, rely onfrozen=Truefor field reassignment prevention andcopy.deepcopy()at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (usingmodel_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel,model_validator,computed_field,ConfigDict). Always useallow_inf_nan=FalseinConfigDictdeclarations to rejectNaN/Infin numeric fields at validation time.
Use@computed_fieldfor derived values instead of storing + validating redundant fields (e.g.TokenUsage.total_tokens)
UseNotBlankStr(fromcore.types) for all identifier/name fields -- including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants -- instead of manual whitespace validators.
Preferasyncio.TaskGroupfor fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over barecreate_task.
Maximum line length: 88 characters (enforced by ruff)
Functions must be less than 50 lines, files must be less than 800 lines
Files:
src/synthorg/persistence/errors.pytests/unit/persistence/sqlite/test_workflow_definition_repo.pysrc/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/synthorg/**/*.py: Every module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__)
Never useimport logging/logging.getLogger()/print()in application code (exceptions:observability/setup.py,observability/sinks.py,observability/syslog_handler.py, andobservability/http_handler.pymay use stdlibloggingandprint(..., file=sys.stderr))
Logger variable name must always belogger(not_logger, notlog)
Always use event constants from domain-specific modules undersynthorg.observability.events(e.g.,API_REQUEST_STARTEDfromevents.api). Import directly:from synthorg.observability.events.<domain> import EVENT_CONSTANT
Always use structured kwargs for logging:logger.info(EVENT, key=value)-- neverlogger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO level
Use DEBUG level for object creation, internal flow, entry/exit of key functions
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names:example-provider,example-large-001,example-medium-001,example-small-001,large/medium/small.
In Pydantic models, useallow_inf_nan=Falsein allConfigDictdeclarations
Pure data models, enums, and re-exports do NOT need logging
Files:
src/synthorg/persistence/errors.pysrc/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
tests/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.py: All test files must use markers:@pytest.mark.unit,@pytest.mark.integration,@pytest.mark.e2e,@pytest.mark.slow
Async tests: useasyncio_mode = "auto"-- no manual@pytest.mark.asyncioneeded
Test timeout: 30 seconds per test (global inpyproject.toml-- do not add per-filepytest.mark.timeout(30)markers; non-default overrides liketimeout(60)are allowed)
Prefer@pytest.mark.parametrizefor testing similar cases
For property-based testing in Python, use Hypothesis (@given+@settings). Profiles:ci(50 examples, default) anddev(1000 examples). Run dev profile:HYPOTHESIS_PROFILE=dev uv run python -m pytest tests/ -m unit -n 8 -k properties
Never skip, dismiss, or ignore flaky tests -- always fix them fully and fundamentally. For timing-sensitive tests, mocktime.monotonic()andasyncio.sleep(). For tasks that must block indefinitely, useasyncio.Event().wait()instead ofasyncio.sleep(large_number).
Files:
tests/unit/persistence/sqlite/test_workflow_definition_repo.py
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors in React and TypeScript files
NEVER hardcode hex color values or rgba() with hardcoded values in.tsx/.tsfiles -- use design token variables instead
Do NOT recreate status dots inline -- use<StatusBadge>component instead
Do NOT build card-with-header layouts from scratch -- use<SectionCard>component instead
Do NOT create metric displays withtext-metric font-boldinline -- use<MetricCard>component instead
Do NOT render initials circles manually -- use<Avatar>component instead
Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component instead
Do NOT usergba()with hardcoded values -- use design token variables instead
Do NOT hardcode Framer Motion transition durations -- use presets from@/lib/motioninsteadReact dashboard design system: always reuse existing components from
web/src/components/ui/before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and@/lib/motionpresets.
Files:
web/src/api/endpoints/workflows.tsweb/src/stores/workflow-editor.ts
web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
TypeScript 6.0+ required for web dashboard; property-based testing in React uses fast-check (
fc.assert+fc.property)
Files:
web/src/api/endpoints/workflows.tsweb/src/stores/workflow-editor.ts
🧠 Learnings (26)
📚 Learning: 2026-03-19T07:12:14.508Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-19T07:12:14.508Z
Learning: Applies to src/synthorg/api/**/*.py : API package (api/): Litestar REST + WebSocket with controllers, guards, channels, JWT + API key + WS ticket auth, approval gate integration, coordination endpoint, collaboration endpoint, settings endpoint, provider management endpoint (CRUD + test + presets), backup endpoint, RFC 9457 structured errors, AppState hot-reload slots, service auto-wiring (Phase 1 at construction, Phase 2 on startup), lifecycle helpers
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) -- ruff enforces this on Python 3.14
Applied to files:
src/synthorg/api/controllers/workflows.pysrc/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Use Pydantic v2 conventions: `BaseModel`, `model_validator`, `computed_field`, `ConfigDict`. For derived values use `computed_field` instead of storing + validating redundant fields. Use `NotBlankStr` (from `core.types`) for all identifier/name fields — including optional (`NotBlankStr | None`) and tuple (`tuple[NotBlankStr, ...]`) variants — instead of manual whitespace validators.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using model_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (via `model_copy(update=...)`) for runtime state that evolves
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T18:38:44.202Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T18:38:44.202Z
Learning: Applies to src/synthorg/**/*.py : Use frozen Pydantic models for config/identity; separate mutable-via-copy models (using `model_copy(update=...)`) for runtime state
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-15T19:14:27.144Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T19:14:27.144Z
Learning: Applies to src/synthorg/**/*.py : For dict/list fields in frozen Pydantic models, rely on frozen=True for field reassignment prevention and copy.deepcopy() at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-01T09:37:49.451Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:37:49.451Z
Learning: Applies to **/*.py : Use frozen Pydantic models for config/identity; use separate mutable-via-copy models with `model_copy(update=...)` for runtime state that evolves
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-01T09:58:27.410Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-01T09:58:27.410Z
Learning: Applies to **/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for dict/list fields in frozen Pydantic models
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : For `dict`/`list` fields in frozen Pydantic models, rely on `frozen=True` for field reassignment prevention and `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to src/synthorg/**/*.py : Use `copy.deepcopy()` at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, persistence serialization) for `dict`/`list` fields
Applied to files:
src/synthorg/api/controllers/workflows.py
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/persistence/**/*.py : Persistence uses pluggable PersistenceBackend protocol. SQLite is the initial backend. Settings use SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/stores/workflow-editor.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/stores/workflow-editor.ts
🔇 Additional comments (11)
src/synthorg/persistence/sqlite/workflow_definition_repo.py (2)
77-84: Use PEP 758 except syntax (no parentheses).Per coding guidelines, Python 3.14 requires
except A, B, C, D:syntax without parentheses for multiple exception types.Proposed fix
- except (ValueError, ValidationError, json.JSONDecodeError, KeyError) as exc: + except ValueError, ValidationError, json.JSONDecodeError, KeyError as exc:As per coding guidelines: "Use PEP 758 except syntax:
except A, B:(no parentheses) -- ruff enforces this on Python 3.14"
102-176: LGTM!The optimistic concurrency implementation is well-designed:
- Version guard in
ON CONFLICT ... WHERE version = excluded.version - 1prevents stale updatesrowcount == 0 and version > 1correctly distinguishes version conflicts from new inserts- Proper rollback before raising
VersionConflictError- Structured logging on all code paths
src/synthorg/api/controllers/workflows.py (3)
66-77: Use PEP 758 except syntax (no parentheses).Per coding guidelines, Python 3.14 requires
except A, B:without parentheses.Proposed fix
- except (ValueError, ValidationError) as exc: + except ValueError, ValidationError as exc:Apply the same change to lines 81, 200, 273, and 348.
As per coding guidelines: "Use PEP 758 except syntax:
except A, B:(no parentheses) -- ruff enforces this on Python 3.14"
221-303: LGTM!The update flow is well-implemented:
- Pre-flight version check catches client-side staleness early (lines 243-262)
model_dump() | updatesfollowed bymodel_validate()correctly re-runs all validators (lines 270-272)VersionConflictErrorcatch handles concurrent conflicts from the repository (lines 285-298)- Both conflict cases return consistent 409 responses
305-328: LGTM!Good REST semantics - returning 404 when the workflow doesn't exist (via
NotFoundError) rather than always 204.src/synthorg/persistence/errors.py (1)
37-39: LGTM!Clean exception class addition following the existing error hierarchy pattern. The inheritance from
QueryErroris appropriate since version conflicts are a specific type of query/update failure.tests/unit/persistence/sqlite/test_workflow_definition_repo.py (2)
114-115: LGTM!Comprehensive test suite with proper
@pytest.mark.unitmarker. Good coverage of:
- CRUD roundtrip with deep comparison of nodes/edges
- Optimistic concurrency conflict detection
- Filtering by workflow type
- UTC timestamp preservation
171-190: Good test coverage for optimistic concurrency.This test correctly verifies that:
- Skipping versions (1 → 3) raises
VersionConflictError- The original data remains intact after a rejected update
web/src/api/endpoints/workflows.ts (1)
1-80: LGTM!Clean API client implementation:
- Proper URL encoding on all path parameters
unwrapVoidused for DELETE (addresses previous feedback)responseType: 'text'correctly configured for YAML export- Consistent use of
unwrap/unwrapPaginatedfor typed responsesweb/src/stores/workflow-editor.ts (2)
366-402: LGTM!Undo/redo implementation correctly uses
structuredClonefor both:
- Current state before pushing to the opposite stack
- Restored state from the snapshot
This ensures complete immutability and prevents cross-contamination between history states.
404-442: LGTM!Good design for handling dirty state:
validate()sends the current in-memory graph tovalidateWorkflowDraft, ensuring validation reflects unsaved changesexportYaml()returns the cachedyamlPreviewwhen dirty (immediate feedback), and fetches from API when clean (canonical source)
…ve UI - Remove redundant cursor=None and 'is not None' check in delete (cursor always set on success) - Save catch detects 409 version conflict and auto-reloads server state
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
web/src/stores/workflow-editor.ts (1)
199-216:⚠️ Potential issue | 🟡 MinorSilent type fallbacks could mask corrupted editor state.
Lines 202 and 212 fall back to
'task'and'sequential'when node/edge types are missing. If React Flow nodes lose their semantic type metadata due to a bug, this silently changes workflow semantics rather than surfacing the error. Consider validating types before save and failing if any are undefined.Proposed validation before mapping
set({ saving: true, error: null }) try { + const invalidNodes = nodes.filter((n) => !n.type) + const invalidEdges = edges.filter((e) => !(e.data as Record<string, unknown>)?.edgeType) + if (invalidNodes.length > 0 || invalidEdges.length > 0) { + set({ saving: false, error: `Cannot save: missing type on ${invalidNodes.length} node(s), ${invalidEdges.length} edge(s)` }) + return + } const updatedDef = await updateWorkflow(definition.id, { nodes: nodes.map((n) => ({ id: n.id, - type: n.type ?? 'task', + type: n.type, label: (n.data as Record<string, unknown>)?.label ?? n.id, position_x: n.position.x, position_y: n.position.y, config: ((n.data as Record<string, unknown>)?.config as Record<string, unknown>) ?? {}, })), edges: edges.map((e) => ({ id: e.id, source_node_id: e.source, target_node_id: e.target, - type: (e.data as Record<string, unknown>)?.edgeType ?? 'sequential', + type: (e.data as Record<string, unknown>)?.edgeType, label: (e.label as string) ?? null, })),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/workflow-editor.ts` around lines 199 - 216, The current save silently substitutes missing node/edge types in the updateWorkflow payload (see the nodes.map and edges.map blocks), which can mask corrupted editor state; before calling updateWorkflow, validate that every node has a defined type (inspect node.type) and every edge has a defined edgeType ((edge.data as Record<string, unknown>)?.edgeType), and if any are missing, abort the save and surface a clear error to the user (or throw) listing the offending node/edge ids so the issue can be fixed instead of defaulting to 'task' or 'sequential'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py`:
- Around line 122-162: The current upsert always uses INSERT which lets an
UPDATE racing with a DELETE recreate a row; change save logic so that if
definition.version == 1 you perform the INSERT path, otherwise perform an UPDATE
with a WHERE that requires id = ? AND version = definition.version - 1 (use
self._db.execute to run UPDATE ... SET ... WHERE id = ? AND version = ?) and
then check cursor.rowcount; if rowcount == 0 rollback and raise
VersionConflictError (and log via PERSISTENCE_WORKFLOW_DEF_SAVE_FAILED with
definition_id and error) to surface conflicts instead of recreating deleted
workflows.
- Around line 102-176: The save() method is too long; split payload
serialization and the DB transaction into small helpers. Create a helper like
_serialize_definition(definition) that returns nodes_json and edges_json
(serializing nodes/edges with model_dump and UTC timestamps), and a helper like
_upsert_definition(definition, nodes_json, edges_json) that performs the
execute/rowcount check, rollback on version conflict, commit on success, and
raises VersionConflictError/QueryError as appropriate. Update save() to call
these two helpers and keep only orchestration and final logging
(PERSISTENCE_WORKFLOW_DEF_SAVED) so each function stays under 50 lines and
failure paths remain testable. Ensure you preserve existing behavior and
exception handling from save(), including logger.exception and logger.warning
calls and use of UTC for timestamps.
- Around line 99-100: The repository shares a single aiosqlite.Connection and
must use a connection-scoped asyncio.Lock to prevent concurrent
execute()/commit() interleaving; modify WorkflowDefinitionRepo to accept and
store a write lock (e.g., add a parameter self._write_lock in __init__) or
obtain it from the backend, then wrap all mutating methods (notably save(),
delete(), and any other methods that call execute() followed by
commit()/rollback()) with "async with self._write_lock:" so the execute/commit
pairs cannot be interleaved by other coroutines; ensure the backend that
constructs the repo provides a single asyncio.Lock instance and that every
repository uses it for write operations.
In `@web/src/stores/workflow-editor.ts`:
- Around line 229-249: In addNode, the label construction uses
type.slice(1).replace('_', ' ') which only replaces the first underscore; update
the label logic in the addNode function (variable label) to replace all
underscores (e.g., use replaceAll('_', ' ') or a global regex) so
multi-underscore node types render correctly, keeping the same capitalization
behavior.
---
Duplicate comments:
In `@web/src/stores/workflow-editor.ts`:
- Around line 199-216: The current save silently substitutes missing node/edge
types in the updateWorkflow payload (see the nodes.map and edges.map blocks),
which can mask corrupted editor state; before calling updateWorkflow, validate
that every node has a defined type (inspect node.type) and every edge has a
defined edgeType ((edge.data as Record<string, unknown>)?.edgeType), and if any
are missing, abort the save and surface a clear error to the user (or throw)
listing the offending node/edge ids so the issue can be fixed instead of
defaulting to 'task' or 'sequential'.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: dc1d21bd-8100-4d03-a331-a656e1028847
📒 Files selected for processing (2)
src/synthorg/persistence/sqlite/workflow_definition_repo.pyweb/src/stores/workflow-editor.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Nofrom __future__ import annotationsin Python code -- Python 3.14 has PEP 649
Use PEP 758 except syntax:except A, B:(no parentheses) -- ruff enforces this on Python 3.14
All public functions must have type hints and pass mypy strict mode
Docstrings must use Google style and are required on all public classes/functions (enforced by ruff D rules)
Create new objects instead of mutating existing ones. For non-Pydantic internal collections (registries,BaseTool), usecopy.deepcopy()at construction +MappingProxyTypewrapping for read-only enforcement.
Fordict/listfields in frozen Pydantic models, rely onfrozen=Truefor field reassignment prevention andcopy.deepcopy()at system boundaries (tool execution, LLM provider serialization, inter-agent delegation, serializing for persistence).
Use frozen Pydantic models for config/identity; use separate mutable-via-copy models (usingmodel_copy(update=...)) for runtime state that evolves. Never mix static config fields with mutable runtime fields in one model.
Use Pydantic v2 (BaseModel,model_validator,computed_field,ConfigDict). Always useallow_inf_nan=FalseinConfigDictdeclarations to rejectNaN/Infin numeric fields at validation time.
Use@computed_fieldfor derived values instead of storing + validating redundant fields (e.g.TokenUsage.total_tokens)
UseNotBlankStr(fromcore.types) for all identifier/name fields -- including optional (NotBlankStr | None) and tuple (tuple[NotBlankStr, ...]) variants -- instead of manual whitespace validators.
Preferasyncio.TaskGroupfor fan-out/fan-in parallel operations in new code (e.g. multiple tool invocations, parallel agent calls). Prefer structured concurrency over barecreate_task.
Maximum line length: 88 characters (enforced by ruff)
Functions must be less than 50 lines, files must be less than 800 lines
Files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
src/synthorg/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/synthorg/**/*.py: Every module with business logic MUST have:from synthorg.observability import get_loggerthenlogger = get_logger(__name__)
Never useimport logging/logging.getLogger()/print()in application code (exceptions:observability/setup.py,observability/sinks.py,observability/syslog_handler.py, andobservability/http_handler.pymay use stdlibloggingandprint(..., file=sys.stderr))
Logger variable name must always belogger(not_logger, notlog)
Always use event constants from domain-specific modules undersynthorg.observability.events(e.g.,API_REQUEST_STARTEDfromevents.api). Import directly:from synthorg.observability.events.<domain> import EVENT_CONSTANT
Always use structured kwargs for logging:logger.info(EVENT, key=value)-- neverlogger.info("msg %s", val)
All error paths must log at WARNING or ERROR with context before raising
All state transitions must log at INFO level
Use DEBUG level for object creation, internal flow, entry/exit of key functions
Never use real vendor names (Anthropic, OpenAI, Claude, GPT, etc.) in project-owned code, docstrings, comments, tests, or config examples. Use generic names:example-provider,example-large-001,example-medium-001,example-small-001,large/medium/small.
In Pydantic models, useallow_inf_nan=Falsein allConfigDictdeclarations
Pure data models, enums, and re-exports do NOT need logging
Files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors in React and TypeScript files
NEVER hardcode hex color values or rgba() with hardcoded values in.tsx/.tsfiles -- use design token variables instead
Do NOT recreate status dots inline -- use<StatusBadge>component instead
Do NOT build card-with-header layouts from scratch -- use<SectionCard>component instead
Do NOT create metric displays withtext-metric font-boldinline -- use<MetricCard>component instead
Do NOT render initials circles manually -- use<Avatar>component instead
Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component instead
Do NOT usergba()with hardcoded values -- use design token variables instead
Do NOT hardcode Framer Motion transition durations -- use presets from@/lib/motioninsteadReact dashboard design system: always reuse existing components from
web/src/components/ui/before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and@/lib/motionpresets.
Files:
web/src/stores/workflow-editor.ts
web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
TypeScript 6.0+ required for web dashboard; property-based testing in React uses fast-check (
fc.assert+fc.property)
Files:
web/src/stores/workflow-editor.ts
🧠 Learnings (15)
📚 Learning: 2026-03-17T06:30:14.180Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T06:30:14.180Z
Learning: Applies to src/synthorg/persistence/**/*.py : Persistence uses pluggable PersistenceBackend protocol. SQLite is the initial backend. Settings use SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Persistence backend: pluggable PersistenceBackend protocol in `src/synthorg/persistence/`, SQLite initial, SettingsRepository (namespaced settings CRUD).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-31T21:07:37.470Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-31T21:07:37.470Z
Learning: Applies to **/*.py : Use `except A, B:` (no parentheses) per PEP 758 exception syntax on Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (no parentheses) for exception handling — PEP 758 exception syntax enforced by ruff on Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-20T21:44:04.528Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T21:44:04.528Z
Learning: Applies to **/*.py : Use `except A, B:` syntax (without parentheses) per PEP 758 for exception handling in Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) — enforced by ruff on Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Use PEP 758 except syntax with `except A, B:` (no parentheses) for multiple exceptions—ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-15T16:55:07.730Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-15T16:55:07.730Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: use `except A, B:` (no parentheses) — ruff enforces this on Python 3.14.
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-04-03T07:34:51.027Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-03T07:34:51.027Z
Learning: Applies to **/*.py : Use PEP 758 except syntax: `except A, B:` (no parentheses) -- ruff enforces this on Python 3.14
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T15:43:05.601Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T15:43:05.601Z
Learning: Applies to **/*.py : Handle errors explicitly, never silently swallow exceptions
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-17T22:08:13.456Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T22:08:13.456Z
Learning: Applies to src/synthorg/**/*.py : Handle errors explicitly, never silently swallow. Validate at system boundaries (user input, external APIs, config files).
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-14T16:18:57.267Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T16:18:57.267Z
Learning: Applies to **/*.py : Handle errors explicitly—never silently swallow exceptions.
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-16T07:22:28.134Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-16T07:22:28.134Z
Learning: Applies to **/*.py : Handle errors explicitly; never silently swallow exceptions
Applied to files:
src/synthorg/persistence/sqlite/workflow_definition_repo.py
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/stores/workflow-editor.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/stores/workflow-editor.ts
🔇 Additional comments (11)
web/src/stores/workflow-editor.ts (11)
1-74: Well-structured Zustand store with comprehensive type definitions.The store follows recommended Zustand patterns with proper separation of state and actions. The
WorkflowSnapshottype for undo/redo and the completeWorkflowEditorStateinterface are well-defined. Based on learnings: "Use Zustand stores in web dashboard for state management."
76-101: Helper functions are clean and correctly implemented.The
nodeTypeToEdgeTypefunction has been simplified per previous feedback—dead conditional logic is removed. TheregenerateYamlwrapper handles null definitions gracefully with sensible defaults.
118-153: Load logic properly resets UI state and preserves edge type semantics.The implementation correctly clears
selectedNodeId,validationResult, and undo/redo stacks when loading a new workflow, addressing previous review feedback. The edge type mapping at line 132 now preservesparallel_branchtype for round-trip consistency.
155-189: Creation flow correctly initializes a clean editor state.New workflows start with properly cleared UI state (
selectedNodeId,validationResult, undo/redo stacks). The dual node construction ensures both API and local state are in sync.
218-226: Good: 409 conflict detection and auto-reload implemented.The conflict handling properly detects HTTP 409, displays a user-friendly message, and reloads the server state so the user can retry with the current version.
251-332: Graph editing actions follow consistent patterns with proper undo support.All mutation actions (
updateNodeConfig,removeNode,onConnect,removeEdge) correctly:
- Capture deep snapshots with
structuredClonebefore mutation- Push to
undoStackand clearredoStack- Regenerate YAML preview
- The
removeNodehandler properly cleans up connected edges to prevent orphans
334-366: ReactFlow change handlers now properly support undo.Both
onNodesChangeandonEdgesChangecapture snapshots before applying position moves or removals, enabling users to undo interactive graph edits. This addresses previous feedback about bypassing undo history.
372-408: Undo/redo correctly uses deep cloning throughout.Both functions now use
structuredClonefor the current state (before pushing to redo/undo stack) and for the restored snapshot, ensuring complete immutability of the history. This addresses previous feedback about shallow copies corrupting history.
410-441: Validate correctly uses the current in-memory graph.The implementation sends the current
nodesandedgestovalidateWorkflowDraft, so validation results reflect unsaved changes. This addresses previous feedback about validating stale data.
443-466: Export and reset actions are correctly implemented.The
exportYamllogic sensibly returns the localyamlPreviewwhen dirty (matching the canvas state) or fetches from the server when clean. Theresetfunction comprehensively clears all state fields.
215-215: No action needed —expected_versionis properly defined in the API contract.The
UpdateWorkflowDefinitionRequestinterface inweb/src/api/types.ts(line 1603) includesreadonly expected_version?: number. The field is correctly used inworkflow-editor.tsat line 215 for optimistic concurrency control.
| def __init__(self, db: aiosqlite.Connection) -> None: | ||
| self._db = db |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that the backend shares one SQLite connection and that write paths are
# not wrapped in a connection-scoped lock/transaction helper.
sed -n '133,154p' src/synthorg/persistence/sqlite/backend.py
sed -n '99,176p' src/synthorg/persistence/sqlite/workflow_definition_repo.py
sed -n '271,304p' src/synthorg/persistence/sqlite/workflow_definition_repo.py
rg -n --type=py '_write_lock|asyncio\.Lock|BEGIN IMMEDIATE|SAVEPOINT|async with .*lock' src/synthorg/persistence/sqliteRepository: Aureliolo/synthorg
Length of output: 5148
Add a connection-scoped write lock to prevent concurrent transaction corruption.
The backend shares one aiosqlite.Connection across all repositories (src/synthorg/persistence/sqlite/backend.py:133-154). Both save() and delete() split execute() and commit()/rollback() across separate awaits without any connection-wide guard. Concurrent requests can interleave these pairs—one coroutine's execute can be followed by another coroutine's execute before the first calls commit—causing transaction corruption or lost updates. The existing _lifecycle_lock is used only for initialization and shutdown, not for write operations.
Add an asyncio.Lock() at the backend level and wrap all repository write operations (save, delete, and any other mutations) with async with self._write_lock: before execute/commit pairs, or migrate to a queued execution pattern.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py` around lines 99
- 100, The repository shares a single aiosqlite.Connection and must use a
connection-scoped asyncio.Lock to prevent concurrent execute()/commit()
interleaving; modify WorkflowDefinitionRepo to accept and store a write lock
(e.g., add a parameter self._write_lock in __init__) or obtain it from the
backend, then wrap all mutating methods (notably save(), delete(), and any other
methods that call execute() followed by commit()/rollback()) with "async with
self._write_lock:" so the execute/commit pairs cannot be interleaved by other
coroutines; ensure the backend that constructs the repo provides a single
asyncio.Lock instance and that every repository uses it for write operations.
| async def save(self, definition: WorkflowDefinition) -> None: | ||
| """Persist a workflow definition via upsert. | ||
|
|
||
| The upsert enforces optimistic concurrency: updates only | ||
| succeed when the existing row's version is exactly one | ||
| behind the incoming version. | ||
|
|
||
| Args: | ||
| definition: Workflow definition model to persist. | ||
|
|
||
| Raises: | ||
| QueryError: If the database operation fails. | ||
| """ | ||
| nodes_json = json.dumps( | ||
| [n.model_dump(mode="json") for n in definition.nodes], | ||
| ) | ||
| edges_json = json.dumps( | ||
| [e.model_dump(mode="json") for e in definition.edges], | ||
| ) | ||
| try: | ||
| cursor = await self._db.execute( | ||
| """\ | ||
| INSERT INTO workflow_definitions | ||
| (id, name, description, workflow_type, nodes, edges, | ||
| created_by, created_at, updated_at, version) | ||
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| ON CONFLICT(id) DO UPDATE SET | ||
| name=excluded.name, | ||
| description=excluded.description, | ||
| workflow_type=excluded.workflow_type, | ||
| nodes=excluded.nodes, | ||
| edges=excluded.edges, | ||
| updated_at=excluded.updated_at, | ||
| version=excluded.version | ||
| WHERE workflow_definitions.version = excluded.version - 1""", | ||
| ( | ||
| definition.id, | ||
| definition.name, | ||
| definition.description, | ||
| definition.workflow_type.value, | ||
| nodes_json, | ||
| edges_json, | ||
| definition.created_by, | ||
| definition.created_at.astimezone(UTC).isoformat(), | ||
| definition.updated_at.astimezone(UTC).isoformat(), | ||
| definition.version, | ||
| ), | ||
| ) | ||
| if cursor.rowcount == 0 and definition.version > 1: | ||
| await self._db.rollback() | ||
| msg = ( | ||
| f"Version conflict saving workflow definition" | ||
| f" {definition.id!r}: expected version" | ||
| f" {definition.version - 1}, not found" | ||
| ) | ||
| logger.warning( | ||
| PERSISTENCE_WORKFLOW_DEF_SAVE_FAILED, | ||
| definition_id=definition.id, | ||
| error=msg, | ||
| ) | ||
| raise VersionConflictError(msg) | ||
| await self._db.commit() | ||
| except sqlite3.Error as exc: | ||
| msg = f"Failed to save workflow definition {definition.id!r}" | ||
| logger.exception( | ||
| PERSISTENCE_WORKFLOW_DEF_SAVE_FAILED, | ||
| definition_id=definition.id, | ||
| error=str(exc), | ||
| ) | ||
| raise QueryError(msg) from exc | ||
| logger.info( | ||
| PERSISTENCE_WORKFLOW_DEF_SAVED, | ||
| definition_id=definition.id, | ||
| ) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Split save() into smaller helpers.
It now mixes payload serialization, SQL construction, version checks, rollback, and logging in one method that is well over the repo's 50-line limit. Extracting the payload build and the write transaction into helpers will make the failure paths easier to audit and test.
As per coding guidelines: "Functions must be less than 50 lines, files must be less than 800 lines".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py` around lines 102
- 176, The save() method is too long; split payload serialization and the DB
transaction into small helpers. Create a helper like
_serialize_definition(definition) that returns nodes_json and edges_json
(serializing nodes/edges with model_dump and UTC timestamps), and a helper like
_upsert_definition(definition, nodes_json, edges_json) that performs the
execute/rowcount check, rollback on version conflict, commit on success, and
raises VersionConflictError/QueryError as appropriate. Update save() to call
these two helpers and keep only orchestration and final logging
(PERSISTENCE_WORKFLOW_DEF_SAVED) so each function stays under 50 lines and
failure paths remain testable. Ensure you preserve existing behavior and
exception handling from save(), including logger.exception and logger.warning
calls and use of UTC for timestamps.
| cursor = await self._db.execute( | ||
| """\ | ||
| INSERT INTO workflow_definitions | ||
| (id, name, description, workflow_type, nodes, edges, | ||
| created_by, created_at, updated_at, version) | ||
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | ||
| ON CONFLICT(id) DO UPDATE SET | ||
| name=excluded.name, | ||
| description=excluded.description, | ||
| workflow_type=excluded.workflow_type, | ||
| nodes=excluded.nodes, | ||
| edges=excluded.edges, | ||
| updated_at=excluded.updated_at, | ||
| version=excluded.version | ||
| WHERE workflow_definitions.version = excluded.version - 1""", | ||
| ( | ||
| definition.id, | ||
| definition.name, | ||
| definition.description, | ||
| definition.workflow_type.value, | ||
| nodes_json, | ||
| edges_json, | ||
| definition.created_by, | ||
| definition.created_at.astimezone(UTC).isoformat(), | ||
| definition.updated_at.astimezone(UTC).isoformat(), | ||
| definition.version, | ||
| ), | ||
| ) | ||
| if cursor.rowcount == 0 and definition.version > 1: | ||
| await self._db.rollback() | ||
| msg = ( | ||
| f"Version conflict saving workflow definition" | ||
| f" {definition.id!r}: expected version" | ||
| f" {definition.version - 1}, not found" | ||
| ) | ||
| logger.warning( | ||
| PERSISTENCE_WORKFLOW_DEF_SAVE_FAILED, | ||
| definition_id=definition.id, | ||
| error=msg, | ||
| ) | ||
| raise VersionConflictError(msg) |
There was a problem hiding this comment.
version > 1 can still recreate a deleted workflow.
Because this statement always starts with INSERT, an update racing with a delete will insert a brand-new row instead of surfacing a version conflict. Treat version == 1 as the only create path; for later versions, issue an UPDATE ... WHERE id = ? AND version = ? and raise VersionConflictError when zero rows change.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/synthorg/persistence/sqlite/workflow_definition_repo.py` around lines 122
- 162, The current upsert always uses INSERT which lets an UPDATE racing with a
DELETE recreate a row; change save logic so that if definition.version == 1 you
perform the INSERT path, otherwise perform an UPDATE with a WHERE that requires
id = ? AND version = definition.version - 1 (use self._db.execute to run UPDATE
... SET ... WHERE id = ? AND version = ?) and then check cursor.rowcount; if
rowcount == 0 rollback and raise VersionConflictError (and log via
PERSISTENCE_WORKFLOW_DEF_SAVE_FAILED with definition_id and error) to surface
conflicts instead of recreating deleted workflows.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
web/src/stores/workflow-editor.ts (1)
199-216:⚠️ Potential issue | 🟡 MinorSilent type fallback masks corrupted editor state.
Lines 202 and 212 use fallbacks (
?? 'task'and?? 'sequential') that silently rewrite missing node/edge types. If a React Flow node/edge loses its semantic type metadata due to a bug, this converts corrupted state into a semantically different persisted workflow without user awareness.Consider validating that all nodes and edges have defined types before saving, and surface an error if any are missing:
Sketch for pre-save validation
saveDefinition: async () => { const { definition, nodes, edges } = get() if (!definition) { set({ error: 'Cannot save: no workflow loaded' }) return } + // Validate type integrity before save + const invalidNodes = nodes.filter((n) => !n.type) + const invalidEdges = edges.filter((e) => !(e.data as Record<string, unknown>)?.edgeType) + if (invalidNodes.length > 0 || invalidEdges.length > 0) { + set({ error: `Cannot save: ${invalidNodes.length} node(s) and ${invalidEdges.length} edge(s) missing type metadata` }) + return + } set({ saving: true, error: null })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/stores/workflow-editor.ts` around lines 199 - 216, The save path currently silently substitutes missing node/edge types in the updateWorkflow payload (see the nodes map that uses "type ?? 'task'" and the edges map that uses "edgeType ?? 'sequential'"), which can persist corrupted editor state; change this to validate that every node has a non-empty type and every edge has a non-empty edgeType before calling updateWorkflow (e.g., iterate the nodes and edges used to build the payload, and if any type is missing/invalid, surface an error or reject the save with a clear message instead of falling back), referencing the updateWorkflow call and the nodes/edges mapping logic so you prevent submission when types are absent and inform the user to fix the editor state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/stores/workflow-editor.ts`:
- Around line 443-448: The exportYaml method currently returns the client-side
yamlPreview when dirty but calls exportWorkflowYaml(definition.id) when clean,
causing inconsistent outputs; update exportYaml to always return the client-side
yamlPreview (from generateYamlPreview) regardless of dirty, and remove/avoid the
conditional server call to exportWorkflowYaml to ensure a single source of truth
for exported YAML (leave server exporter for a separate "canonical export" flow
if needed). Ensure you reference and use the existing yamlPreview and
definition.id in exportYaml so callers always get the same client-generated
YAML.
---
Duplicate comments:
In `@web/src/stores/workflow-editor.ts`:
- Around line 199-216: The save path currently silently substitutes missing
node/edge types in the updateWorkflow payload (see the nodes map that uses "type
?? 'task'" and the edges map that uses "edgeType ?? 'sequential'"), which can
persist corrupted editor state; change this to validate that every node has a
non-empty type and every edge has a non-empty edgeType before calling
updateWorkflow (e.g., iterate the nodes and edges used to build the payload, and
if any type is missing/invalid, surface an error or reject the save with a clear
message instead of falling back), referencing the updateWorkflow call and the
nodes/edges mapping logic so you prevent submission when types are absent and
inform the user to fix the editor state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1733925d-3d44-430b-a313-6e04c9a5a59b
📒 Files selected for processing (1)
web/src/stores/workflow-editor.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Test (Python 3.14)
- GitHub Check: Dashboard Test
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Build Backend
- GitHub Check: Dependency Review
- GitHub Check: Analyze (go)
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors in React and TypeScript files
NEVER hardcode hex color values or rgba() with hardcoded values in.tsx/.tsfiles -- use design token variables instead
Do NOT recreate status dots inline -- use<StatusBadge>component instead
Do NOT build card-with-header layouts from scratch -- use<SectionCard>component instead
Do NOT create metric displays withtext-metric font-boldinline -- use<MetricCard>component instead
Do NOT render initials circles manually -- use<Avatar>component instead
Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component instead
Do NOT usergba()with hardcoded values -- use design token variables instead
Do NOT hardcode Framer Motion transition durations -- use presets from@/lib/motioninsteadReact dashboard design system: always reuse existing components from
web/src/components/ui/before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and@/lib/motionpresets.
Files:
web/src/stores/workflow-editor.ts
web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
TypeScript 6.0+ required for web dashboard; property-based testing in React uses fast-check (
fc.assert+fc.property)
Files:
web/src/stores/workflow-editor.ts
🧠 Learnings (3)
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/stores/workflow-editor.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/stores/workflow-editor.ts
📚 Learning: 2026-04-02T12:19:36.889Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: web/CLAUDE.md:0-0
Timestamp: 2026-04-02T12:19:36.889Z
Learning: Applies to web/src/components/ui/**/*.stories.tsx : Create a `.stories.tsx` file alongside every new shared component in `web/src/components/ui/` with all states (default, hover, loading, error, empty)
Applied to files:
web/src/stores/workflow-editor.ts
🔇 Additional comments (11)
web/src/stores/workflow-editor.ts (11)
1-31: LGTM!Imports, constants, and type definitions are well-structured. The
MAX_UNDO = 50limit is reasonable for preventing unbounded memory growth. Based on learnings, Zustand is the correct choice for state management in this web dashboard.
76-101: LGTM!Helper functions are clean. The
nodeTypeToEdgeTypefunction was correctly simplified per past review feedback, removing the unusedsourceHandleparameter.
118-153: LGTM!The
loadDefinitionmethod properly clears workflow-scoped UI state (selectedNodeId,validationResult, undo/redo stacks) when loading a new definition. Edge type mapping now correctly preservesparallel_branchtype for React Flow consistency.
155-189: LGTM!The
createDefinitionmethod properly initializes a new workflow with Start/End nodes and correctly clears all workflow-scoped UI state.
229-249: LGTM!The
addNodemethod correctly usesreplaceAllfor robust underscore replacement per past review feedback. Undo snapshot handling withstructuredCloneand stack capping is properly implemented.
251-284: LGTM!Both
updateNodeConfigandremoveNodeproperly handle undo snapshots with deep cloning. TheremoveNodemethod correctly clearsselectedNodeIdwhen the selected node is removed.
286-318: LGTM!The
onConnectmethod correctly creates edges with consistent type metadata, matching the mapping inloadDefinition. The semanticedgeType(e.g.,conditional_true,conditional_false,parallel_branch) is properly stored inedge.datafor persistence.
334-366: LGTM!The
onNodesChangeandonEdgesChangehandlers now correctly push undo snapshots for position changes and removals, addressing the past review feedback. Deep cloning withstructuredCloneensures snapshot integrity.
372-408: LGTM!The
undoandredomethods properly usestructuredClonefor both the current state (pushed to the opposite stack) and the restored snapshot, ensuring full immutability as requested in past reviews.
410-441: LGTM!The
validatemethod now correctly sends the current in-memory graph (nodes/edges) to thevalidateWorkflowDraftAPI, addressing the past review feedback about ignoring unsaved canvas changes.
450-467: LGTM!The
resetmethod properly clears all store state to initial values.
…YAML - Save aborts with descriptive error if any node or edge has missing type - Canvas state preserved on validation failure (no data loss) - exportYaml always returns client-side yamlPreview for consistency - Remove unused exportWorkflowYaml import
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/stores/workflow-editor.ts`:
- Around line 420-451: The validate() function is inconsistent with
saveDefinition(): instead of silently defaulting missing node types to 'task'
and edge types to 'sequential' in the nodes.map/edges.map, replicate
saveDefinition()'s pre-validation logic to detect any nodes or edges missing an
explicit type and abort validation with set({ error: ... , validating: false });
return; so validation fails early on the same condition that would cause
saveDefinition() to abort; update validate() (the validate: async () => handler)
to check node.type and (e.data as Record<string, unknown>)?.edgeType before
mapping and only proceed to call validateWorkflowDraft when all types are
present.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 34487af3-5388-4cdb-a84a-c947ed29dbc2
📒 Files selected for processing (1)
web/src/stores/workflow-editor.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: Build Backend
- GitHub Check: Build Web
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors in React and TypeScript files
NEVER hardcode hex color values or rgba() with hardcoded values in.tsx/.tsfiles -- use design token variables instead
Do NOT recreate status dots inline -- use<StatusBadge>component instead
Do NOT build card-with-header layouts from scratch -- use<SectionCard>component instead
Do NOT create metric displays withtext-metric font-boldinline -- use<MetricCard>component instead
Do NOT render initials circles manually -- use<Avatar>component instead
Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component instead
Do NOT usergba()with hardcoded values -- use design token variables instead
Do NOT hardcode Framer Motion transition durations -- use presets from@/lib/motioninsteadReact dashboard design system: always reuse existing components from
web/src/components/ui/before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and@/lib/motionpresets.
Files:
web/src/stores/workflow-editor.ts
web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
TypeScript 6.0+ required for web dashboard; property-based testing in React uses fast-check (
fc.assert+fc.property)
Files:
web/src/stores/workflow-editor.ts
🧠 Learnings (2)
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/stores/workflow-editor.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/stores/workflow-editor.ts
🔇 Additional comments (10)
web/src/stores/workflow-editor.ts (10)
1-73: LGTM!Well-structured Zustand store interface with clear separation of data, UI state, validation state, undo/redo stacks, and actions. The documentation header is helpful.
75-100: LGTM!Helper functions are clean. ID generation uses secure
crypto.randomUUID(), and thenodeTypeToEdgeTypelogic correctly maps source node types to React Flow edge types.
117-152: LGTM!The
loadDefinitionaction properly maps API data to React Flow structures and correctly resets all workflow-scoped UI state (selectedNodeId,validationResult, undo/redo stacks) when loading a new workflow.
154-188: LGTM!Clean implementation that initializes a workflow with start/end nodes and properly resets all workflow-scoped state.
190-237: LGTM!Solid implementation with:
- Pre-save validation that aborts with descriptive errors when node/edge types are missing
- Optimistic concurrency via
expected_version- HTTP 409 conflict detection with automatic reload
239-277: LGTM!Both actions properly capture deep-cloned snapshots for undo history before mutation, use
replaceAllfor robust underscore replacement, and correctly manage the undo/redo stacks.
279-342: LGTM!Well-implemented graph editing actions:
removeNodecorrectly cascades to remove connected edges and clears selection if neededonConnectproperly maps source node type to edge semantics (conditional branches, parallel branches)- All actions capture deep-cloned snapshots for undo history
344-376: LGTM!The change handlers now properly capture undo snapshots for position moves and removals, addressing the earlier gap where these operations bypassed undo history.
382-418: LGTM!Undo/redo actions now use
structuredCloneconsistently for both:
- Capturing current state before pushing to the opposite stack
- Restoring state from the snapshot
This ensures full immutability and prevents shared reference corruption.
453-475: LGTM!
exportYamlnow consistently returns the client-sideyamlPreviewregardless of dirty state, andresetcomprehensively clears all state fields.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/stores/workflow-editor.ts`:
- Around line 127-135: The nested ternary used to compute the edge visual type
and related values inside the edges mapping is hard to read; extract the logic
into a small helper (e.g., getEdgeMeta or mapEdgeType) that accepts the raw edge
object (or e.type) and returns an object with { visualType, sourceHandle?,
branch?, edgeType } so the mapping in the edges creation becomes a simple call
that sets id, source, target, type: meta.visualType, sourceHandle:
meta.sourceHandle, data: { edgeType: meta.edgeType, branch: meta.branch }, and
label. Update the current edges: Edge[] = def.edges.map((e) => (…)) usage to
call this helper and keep existing behavior for 'conditional_true',
'conditional_false', 'parallel_branch', and default 'sequential'.
- Around line 344-358: Rename the local boolean `hasMoves` in the onNodesChange
handler to a clearer name like `hasSignificantChanges` or `needsSnapshot` to
reflect that it also includes 'remove' events; update all uses within that
function (the variable declaration, the ternary that creates `snapshot`, the
`dirty` calculation, `yamlPreview` conditional using `regenerateYaml(newNodes,
s.edges, s.definition)`, and the `undoStack`/`redoStack` logic) so behavior is
unchanged but the intent is clearer.
- Around line 463-467: The exportYaml action is declared async but does no async
work; remove the async keyword from exportYaml (the function in the
workflow-editor store) so it returns the cached yamlPreview directly instead of
an unnecessary Promise, and update any consuming typings or the store interface
if other code expects a Promise-returning exportYaml to preserve compatibility
(or add a brief comment explaining the non-async change).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 922dd828-ac93-4fcd-8aa8-89e00e47752b
📒 Files selected for processing (1)
web/src/stores/workflow-editor.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Build Web
- GitHub Check: Build Sandbox
- GitHub Check: Build Backend
- GitHub Check: Dashboard Test
- GitHub Check: Test (Python 3.14)
- GitHub Check: Dependency Review
- GitHub Check: Analyze (python)
- GitHub Check: Analyze (go)
🧰 Additional context used
📓 Path-based instructions (2)
web/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (web/CLAUDE.md)
web/src/**/*.{ts,tsx}: Use Tailwind semantic classes (text-foreground,bg-card,text-accent,text-success,bg-danger, etc.) or CSS variables (var(--so-*)) for colors in React and TypeScript files
NEVER hardcode hex color values or rgba() with hardcoded values in.tsx/.tsfiles -- use design token variables instead
Do NOT recreate status dots inline -- use<StatusBadge>component instead
Do NOT build card-with-header layouts from scratch -- use<SectionCard>component instead
Do NOT create metric displays withtext-metric font-boldinline -- use<MetricCard>component instead
Do NOT render initials circles manually -- use<Avatar>component instead
Do NOT create complex (>8 line) JSX inside.map()-- extract to a shared component instead
Do NOT usergba()with hardcoded values -- use design token variables instead
Do NOT hardcode Framer Motion transition durations -- use presets from@/lib/motioninsteadReact dashboard design system: always reuse existing components from
web/src/components/ui/before creating new ones. Never hardcode hex colors, font-family, pixel spacing, or Framer Motion transitions -- use design tokens and@/lib/motionpresets.
Files:
web/src/stores/workflow-editor.ts
web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
TypeScript 6.0+ required for web dashboard; property-based testing in React uses fast-check (
fc.assert+fc.property)
Files:
web/src/stores/workflow-editor.ts
🧠 Learnings (2)
📚 Learning: 2026-03-27T12:44:29.466Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-27T12:44:29.466Z
Learning: Applies to web/src/stores/**/*.{ts,tsx} : Use Zustand stores in web dashboard for state management (auth, WebSocket, toast, analytics, domain shells)
Applied to files:
web/src/stores/workflow-editor.ts
📚 Learning: 2026-03-30T10:41:40.176Z
Learnt from: CR
Repo: Aureliolo/synthorg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-30T10:41:40.176Z
Learning: Applies to web/src/**/*.{ts,tsx} : Use Zustand stores for state management in the web dashboard; each domain has its own store module (auth, WebSocket, toast, analytics, setup, company, agents, budget, tasks, settings, providers, theme, per-domain stores)
Applied to files:
web/src/stores/workflow-editor.ts
🔇 Additional comments (1)
web/src/stores/workflow-editor.ts (1)
1-100: Well-structured Zustand store with proper state management.The store correctly implements:
- Deep cloning with
structuredClonefor undo/redo integrity- Type validation before save/validate operations
- Optimistic concurrency with 409 conflict handling and reload
- Bounded undo history (50 levels)
- Live YAML preview regeneration on state changes
Previous review feedback has been addressed comprehensively.
| onNodesChange: (changes: NodeChange[]) => { | ||
| set((s) => { | ||
| const hasMoves = changes.some((c) => c.type === 'position' || c.type === 'remove') | ||
| const snapshot = hasMoves | ||
| ? { nodes: structuredClone(s.nodes), edges: structuredClone(s.edges) } | ||
| : null | ||
| const newNodes = applyNodeChanges(changes, s.nodes) | ||
| return { | ||
| nodes: newNodes, | ||
| dirty: s.dirty || hasMoves, | ||
| yamlPreview: hasMoves ? regenerateYaml(newNodes, s.edges, s.definition) : s.yamlPreview, | ||
| undoStack: snapshot ? [...s.undoStack.slice(-MAX_UNDO + 1), snapshot] : s.undoStack, | ||
| redoStack: snapshot ? [] : s.redoStack, | ||
| } | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Minor: Variable name hasMoves could be clearer.
hasMoves also checks for 'remove' changes, which is semantically different from moves. A name like hasSignificantChanges or needsSnapshot would better reflect the intent.
♻️ Suggested rename
onNodesChange: (changes: NodeChange[]) => {
set((s) => {
- const hasMoves = changes.some((c) => c.type === 'position' || c.type === 'remove')
- const snapshot = hasMoves
+ const needsSnapshot = changes.some((c) => c.type === 'position' || c.type === 'remove')
+ const snapshot = needsSnapshot
? { nodes: structuredClone(s.nodes), edges: structuredClone(s.edges) }
: null
const newNodes = applyNodeChanges(changes, s.nodes)
return {
nodes: newNodes,
- dirty: s.dirty || hasMoves,
- yamlPreview: hasMoves ? regenerateYaml(newNodes, s.edges, s.definition) : s.yamlPreview,
+ dirty: s.dirty || needsSnapshot,
+ yamlPreview: needsSnapshot ? regenerateYaml(newNodes, s.edges, s.definition) : s.yamlPreview,
undoStack: snapshot ? [...s.undoStack.slice(-MAX_UNDO + 1), snapshot] : s.undoStack,
redoStack: snapshot ? [] : s.redoStack,
}
})
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/stores/workflow-editor.ts` around lines 344 - 358, Rename the local
boolean `hasMoves` in the onNodesChange handler to a clearer name like
`hasSignificantChanges` or `needsSnapshot` to reflect that it also includes
'remove' events; update all uses within that function (the variable declaration,
the ternary that creates `snapshot`, the `dirty` calculation, `yamlPreview`
conditional using `regenerateYaml(newNodes, s.edges, s.definition)`, and the
`undoStack`/`redoStack` logic) so behavior is unchanged but the intent is
clearer.
| exportYaml: async () => { | ||
| const { definition, yamlPreview } = get() | ||
| if (!definition) throw new Error('Cannot export: no workflow loaded') | ||
| return yamlPreview | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
exportYaml doesn't need to be async.
The function performs no asynchronous operations—it simply returns the cached yamlPreview. Removing async simplifies the signature and avoids unnecessary Promise wrapping. If the interface requires it for consistency with other actions, document why.
♻️ Suggested simplification
- exportYaml: async () => {
+ exportYaml: () => {
const { definition, yamlPreview } = get()
if (!definition) throw new Error('Cannot export: no workflow loaded')
return yamlPreview
},Update the interface accordingly:
- exportYaml: () => Promise<string>
+ exportYaml: () => string🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/stores/workflow-editor.ts` around lines 463 - 467, The exportYaml
action is declared async but does no async work; remove the async keyword from
exportYaml (the function in the workflow-editor store) so it returns the cached
yamlPreview directly instead of an unnecessary Promise, and update any consuming
typings or the store interface if other code expects a Promise-returning
exportYaml to preserve compatibility (or add a brief comment explaining the
non-async change).
🤖 I have created a release *beep* *boop* --- ## [0.5.9](v0.5.8...v0.5.9) (2026-04-03) ### Features * ceremony template defaults + strategy migration UX ([#1031](#1031)) ([da4a8e1](da4a8e1)), closes [#976](#976) [#978](#978) * hybrid search (dense + BM25 sparse) for memory retrieval pipeline ([#1016](#1016)) ([fccac4a](fccac4a)), closes [#694](#694) * implement network hosting and multi-user access ([#1032](#1032)) ([398c378](398c378)), closes [#244](#244) * implement visual workflow editor ([#247](#247)) ([#1018](#1018)) ([ef5d3c1](ef5d3c1)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Full-stack visual workflow editor for designing task execution pipelines. Users can visually build workflow graphs with drag-and-drop, configure individual steps, validate for execution readiness, and export as YAML.
Backend
WorkflowNode,WorkflowEdge,WorkflowDefinition(frozen Pydantic, 7 node types, 4 edge types)WorkflowDefinitionRepositoryprotocol + SQLite implementation (JSON-serialized nodes/edges, indexed)/workflows(list, get, create, update with optimistic concurrency, delete, validate, export)Final[str]event constants on all pathsFrontend
/workflows/editorTests
Review Coverage
Pre-reviewed by 13 agents, 36 findings addressed:
valid, Final[str] event constants, NotBlankStr on message/label fieldsWhen to Test
npm --prefix web run devthen navigate to/workflows/editorCloses #247